diff --git a/.github/workflows/api-ci.yml b/.github/workflows/api-ci.yml index e92a37fb4e..cbd2cab0c7 100644 --- a/.github/workflows/api-ci.yml +++ b/.github/workflows/api-ci.yml @@ -17,9 +17,9 @@ jobs: api-lint: name: Run Lint (API) if: ${{ inputs.skip == false }} - uses: codecov/gha-workflows/.github/workflows/lint.yml@v1.2.31 + uses: codecov/gha-workflows/.github/workflows/lint.yml@matt/make-target-prefix with: - working_directory: apps/codecov-api + make_target_prefix: api. api-mypy: name: Patch typing (API) @@ -28,72 +28,98 @@ jobs: with: working_directory: apps/codecov-api + # Once we cut over to umbrella, we can just replace the default cache key in build-app.yml + # and self-hosted.yml and get rid of this step. + api-compute-reqs-cache-key: + name: Compute cache key for requirements image + runs-on: ubuntu-latest + outputs: + reqs-cache-key: ${{ steps.compute.outputs.cache-key }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: 'recursive' + - id: compute + run: | + echo cache-key=${{ hashFiles('apps/codecov-api/uv.lock') }}-${{ hashFiles('docker/Dockerfile.requirements') }}-${{ hashFiles('libs/shared/**') }} >> "$GITHUB_OUTPUT" + api-build: name: Build App (API) if: ${{ inputs.skip == false }} - uses: codecov/gha-workflows/.github/workflows/build-app.yml@v1.2.31 + needs: [api-compute-reqs-cache-key] + uses: codecov/gha-workflows/.github/workflows/build-app.yml@matt/make-target-prefix secrets: inherit with: repo: ${{ vars.CODECOV_API_IMAGE_V2 || vars.CODECOV_API_IMAGE_V2_SELF_HOSTED || 'codecov/self-hosted-api' }} - working_directory: apps/codecov-api + output_directory: apps/codecov-api + make_target_prefix: api. + reqs_cache_key: ${{ needs.api-compute-reqs-cache-key.outputs.reqs-cache-key }} api-test: name: Test (API) if: ${{ inputs.skip == false }} needs: [api-build] - uses: codecov/gha-workflows/.github/workflows/run-tests.yml@v1.2.35 + uses: codecov/gha-workflows/.github/workflows/run-tests.yml@matt/make-target-prefix secrets: inherit with: run_integration: false repo: ${{ vars.CODECOV_API_IMAGE_V2 || vars.CODECOV_API_IMAGE_V2_SELF_HOSTED || 'codecov/self-hosted-api' }} - working_directory: apps/codecov-api + output_directory: apps/codecov-api flag_prefix: api pytest_rootdir: /app + make_target_prefix: api. api-build-self-hosted: name: Build Self Hosted (API) if: ${{ inputs.skip == false }} needs: [api-build, api-test] - uses: codecov/gha-workflows/.github/workflows/self-hosted.yml@v1.2.31 + uses: codecov/gha-workflows/.github/workflows/self-hosted.yml@matt/make-target-prefix secrets: inherit with: repo: ${{ vars.CODECOV_API_IMAGE_V2 || vars.CODECOV_API_IMAGE_V2_SELF_HOSTED || 'codecov/self-hosted-api' }} - working_directory: apps/codecov-api + output_directory: apps/codecov-api + make_target_prefix: api. + reqs_cache_key: ${{ needs.api-compute-reqs-cache-key.outputs.reqs-cache-key }} api-staging: name: Push Staging Image (API) needs: [api-build, api-test] if: ${{ inputs.skip == false && github.event_name == 'push' && github.event.ref == 'refs/heads/staging' && github.repository_owner == 'codecov' }} - uses: codecov/gha-workflows/.github/workflows/push-env.yml@v1.2.31 + uses: codecov/gha-workflows/.github/workflows/push-env.yml@matt/make-target-prefix secrets: inherit with: environment: staging repo: ${{ vars.CODECOV_API_IMAGE_V2 || vars.CODECOV_API_IMAGE_V2_SELF_HOSTED || 'codecov/self-hosted-api' }} - working_directory: apps/codecov-api + output_directory: apps/codecov-api sentry_project: api + make_target_prefix: api. api-production: name: Push Production Image (API) needs: [api-build, api-test] if: ${{ inputs.skip == false && github.event_name == 'push' && github.event.ref == 'refs/heads/main' && github.repository_owner == 'codecov' }} - uses: codecov/gha-workflows/.github/workflows/push-env.yml@v1.2.31 + uses: codecov/gha-workflows/.github/workflows/push-env.yml@matt/make-target-prefix secrets: inherit with: environment: production repo: ${{ vars.CODECOV_API_IMAGE_V2 || vars.CODECOV_API_IMAGE_V2_SELF_HOSTED || 'codecov/self-hosted-api' }} - working_directory: apps/codecov-api + output_directory: apps/codecov-api sentry_project: api + make_target_prefix: api. api-self-hosted: name: Push Self Hosted Image (API) needs: [api-build-self-hosted, api-test] secrets: inherit if: ${{ inputs.skip == false && github.event_name == 'push' && github.event.ref == 'refs/heads/main' && github.repository_owner == 'codecov' }} - uses: codecov/gha-workflows/.github/workflows/self-hosted.yml@v1.2.31 + uses: codecov/gha-workflows/.github/workflows/self-hosted.yml@matt/make-target-prefix with: push_rolling: true repo: ${{ vars.CODECOV_API_IMAGE_V2 || vars.CODECOV_API_IMAGE_V2_SELF_HOSTED || 'codecov/self-hosted-api' }} - working_directory: apps/codecov-api + output_directory: apps/codecov-api + make_target_prefix: api. + reqs_cache_key: ${{ needs.api-compute-reqs-cache-key.outputs.reqs-cache-key }} # This job works around a strange interaction between reusable workflows and # path filters. diff --git a/.github/workflows/shared-ci.yml b/.github/workflows/shared-ci.yml index 1a4981cb46..9603e1bc85 100644 --- a/.github/workflows/shared-ci.yml +++ b/.github/workflows/shared-ci.yml @@ -12,9 +12,9 @@ jobs: shared-lint: name: Run Lint (Shared) if: ${{ inputs.skip == false }} - uses: codecov/gha-workflows/.github/workflows/lint.yml@v1.2.31 + uses: codecov/gha-workflows/.github/workflows/lint.yml@matt/make-target-prefix with: - working_directory: libs/shared + make_target_prefix: shared. shared-benchmark: name: Benchmarks (Shared) @@ -138,7 +138,6 @@ jobs: # The coverage action will have installed codecovcli with pip. The # actual binary will be found in $PATH. binary: codecovcli - recurse_submodules: true # This job works around a strange interaction between reusable workflows and # path filters. diff --git a/.github/workflows/worker-ci.yml b/.github/workflows/worker-ci.yml index adbcf7e9bd..a9a5206f78 100644 --- a/.github/workflows/worker-ci.yml +++ b/.github/workflows/worker-ci.yml @@ -2,7 +2,7 @@ name: Worker CI on: workflow_call: - inputs: + inputs: skip: type: boolean default: false @@ -17,9 +17,9 @@ jobs: worker-lint: name: Run Lint (Worker) if: ${{ inputs.skip == false }} - uses: codecov/gha-workflows/.github/workflows/lint.yml@v1.2.31 + uses: codecov/gha-workflows/.github/workflows/lint.yml@matt/make-target-prefix with: - working_directory: apps/worker + make_target_prefix: worker. worker-mypy: name: Patch typing (Worker) @@ -28,71 +28,97 @@ jobs: with: working_directory: apps/worker + # Once we cut over to umbrella, we can just replace the default cache key in build-app.yml + # and self-hosted.yml and get rid of this step. + worker-compute-reqs-cache-key: + name: Compute cache key for requirements image + runs-on: ubuntu-latest + outputs: + reqs-cache-key: ${{ steps.compute.outputs.cache-key }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: 'recursive' + - id: compute + run: | + echo cache-key=${{ hashFiles('apps/worker/uv.lock') }}-${{ hashFiles('docker/Dockerfile.requirements') }}-${{ hashFiles('libs/shared/**') }} >> "$GITHUB_OUTPUT" + worker-build: name: Build App (Worker) if: ${{ inputs.skip == false }} - uses: codecov/gha-workflows/.github/workflows/build-app.yml@v1.2.31 + needs: [worker-compute-reqs-cache-key] + uses: codecov/gha-workflows/.github/workflows/build-app.yml@matt/make-target-prefix secrets: inherit with: repo: ${{ vars.CODECOV_WORKER_IMAGE_V2 || vars.CODECOV_WORKER_IMAGE_V2_SELF_HOSTED || 'codecov/self-hosted-worker' }} - working_directory: apps/worker + output_directory: apps/worker + make_target_prefix: worker. + reqs_cache_key: ${{ needs.worker-compute-reqs-cache-key.outputs.reqs-cache-key }} worker-test: name: Test (Worker) if: ${{ inputs.skip == false }} needs: [worker-build] - uses: codecov/gha-workflows/.github/workflows/run-tests.yml@v1.2.35 + uses: codecov/gha-workflows/.github/workflows/run-tests.yml@matt/make-target-prefix secrets: inherit with: repo: ${{ vars.CODECOV_WORKER_IMAGE_V2 || vars.CODECOV_WORKER_IMAGE_V2_SELF_HOSTED || 'codecov/self-hosted-worker' }} - working_directory: apps/worker + output_directory: apps/worker flag_prefix: worker pytest_rootdir: /app + make_target_prefix: worker. worker-build-self-hosted: name: Build Self Hosted (Worker) if: ${{ inputs.skip == false }} needs: [worker-build, worker-test] - uses: codecov/gha-workflows/.github/workflows/self-hosted.yml@v1.2.31 + uses: codecov/gha-workflows/.github/workflows/self-hosted.yml@matt/make-target-prefix secrets: inherit with: repo: ${{ vars.CODECOV_WORKER_IMAGE_V2 || vars.CODECOV_WORKER_IMAGE_V2_SELF_HOSTED || 'codecov/self-hosted-worker' }} - working_directory: apps/worker + output_directory: apps/worker + make_target_prefix: worker. + reqs_cache_key: ${{ needs.worker-compute-reqs-cache-key.outputs.reqs-cache-key }} worker-staging: name: Push Staging Image (Worker) needs: [worker-build, worker-test] if: ${{ inputs.skip == false && github.event_name == 'push' && (github.event.ref == 'refs/heads/main' || github.event.ref == 'refs/heads/staging') && github.repository_owner == 'codecov' }} - uses: codecov/gha-workflows/.github/workflows/push-env.yml@v1.2.31 + uses: codecov/gha-workflows/.github/workflows/push-env.yml@matt/make-target-prefix secrets: inherit with: environment: staging repo: ${{ vars.CODECOV_WORKER_IMAGE_V2 || vars.CODECOV_WORKER_IMAGE_V2_SELF_HOSTED || 'codecov/self-hosted-worker' }} - working_directory: apps/worker + output_directory: apps/worker sentry_project: worker + make_target_prefix: worker. worker-production: name: Push Production Image (Worker) needs: [worker-build, worker-test] if: ${{ inputs.skip == false && github.event_name == 'push' && github.event.ref == 'refs/heads/main' && github.repository_owner == 'codecov' }} - uses: codecov/gha-workflows/.github/workflows/push-env.yml@v1.2.31 + uses: codecov/gha-workflows/.github/workflows/push-env.yml@matt/make-target-prefix secrets: inherit with: environment: production repo: ${{ vars.CODECOV_WORKER_IMAGE_V2 || vars.CODECOV_WORKER_IMAGE_V2_SELF_HOSTED || 'codecov/self-hosted-worker' }} - working_directory: apps/worker + output_directory: apps/worker sentry_project: worker + make_target_prefix: worker. worker-self-hosted: name: Push Self Hosted Image (Worker) - needs: [worker-build-self-hosted, worker-test] + needs: [worker-compute-reqs-cache-key, worker-build-self-hosted, worker-test] secrets: inherit if: ${{ inputs.skip == false && github.event_name == 'push' && github.event.ref == 'refs/heads/main' && github.repository_owner == 'codecov' }} - uses: codecov/gha-workflows/.github/workflows/self-hosted.yml@v1.2.31 + uses: codecov/gha-workflows/.github/workflows/self-hosted.yml@matt/make-target-prefix with: push_rolling: true repo: ${{ vars.CODECOV_WORKER_IMAGE_V2 || vars.CODECOV_WORKER_IMAGE_V2_SELF_HOSTED || 'codecov/self-hosted-worker' }} - working_directory: apps/worker + output_directory: apps/worker + make_target_prefix: worker. + reqs_cache_key: ${{ needs.worker-compute-reqs-cache-key.outputs.reqs-cache-key }} # This job works around a strange interaction between reusable workflows and # path filters. diff --git a/.gitmodules b/.gitmodules index f64a466b33..e69de29bb2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +0,0 @@ -[submodule "apps/worker"] - path = apps/worker - url = git@github.com:codecov/worker.git -[submodule "apps/codecov-api"] - path = apps/codecov-api - url = git@github.com:codecov/codecov-api.git -[submodule "libs/shared"] - path = libs/shared - url = git@github.com:codecov/shared.git diff --git a/Makefile b/Makefile index 61b7e65ddf..94d85ce54b 100644 --- a/Makefile +++ b/Makefile @@ -1,280 +1,102 @@ -sha := $(shell git rev-parse --short=7 HEAD) -long_sha := $(shell git rev-parse HEAD) -release_version := `cat VERSION` -build_date ?= $(shell git show -s --date=iso8601-strict --pretty=format:%cd $$sha) -branch = $(shell git branch | grep \* | cut -f2 -d' ') -epoch := $(shell date +"%s") -AR_REPO ?= codecov/umbrella -DOCKERHUB_REPO ?= codecov/self-hosted-api -REQUIREMENTS_TAG := requirements-v1-$(shell sha1sum requirements.txt | cut -d ' ' -f 1)-$(shell sha1sum docker/Dockerfile.requirements | cut -d ' ' -f 1) -VERSION := release-${sha} -CODECOV_UPLOAD_TOKEN ?= "notset" -CODECOV_STATIC_TOKEN ?= "notset" -TIMESERIES_ENABLED ?= "true" -CODECOV_URL ?= "https://api.codecov.io" -export DOCKER_BUILDKIT=1 -export API_DOCKER_REPO=${AR_REPO} -export API_DOCKER_VERSION=${VERSION} -export CODECOV_TOKEN=${CODECOV_UPLOAD_TOKEN} -API_DOMAIN ?= api -PROXY_NETWORK ?= api_default - -# Codecov CLI version to use -CODECOV_CLI_VERSION := 0.5.1 - -include tools/devenv/Makefile.devenv - -update-reqs: - cd codecov-api && git pull - cd worker && git pull - cd shared && git pull - cat worker/requirements.in codecov-api/requirements.in | sort -u > requirements.in - pip-compile requirements.in - -build: - make build.requirements - make build.app - -check-for-migration-conflicts: - python manage.py check_for_migration_conflicts +export sha := $(shell git rev-parse --short=7 HEAD) +export full_sha := $(shell git rev-parse HEAD) +export long_sha := ${full_sha} +export merge_sha := $(shell git merge-base HEAD^ origin/main) +export VERSION := release-${sha} -test: - COVERAGE_CORE=sysmon python -m pytest --cov=./ --junitxml=junit.xml +export build_date ?= $(shell git show -s --date=iso8601-strict --pretty=format:%cd $$sha) +export branch := $(shell git branch | grep \* | cut -f2 -d' ') -test.unit: - COVERAGE_CORE=sysmon python -m pytest --cov=./ -m "not integration" --cov-report=xml:unit.coverage.xml --junitxml=unit.junit.xml - -test.integration: - COVERAGE_CORE=sysmon python -m pytest --cov=./ -m "integration" --cov-report=xml:integration.coverage.xml --junitxml=integration.junit.xml - -lint: - make lint.install - make lint.run - -lint.install: - echo "Installing..." - pip install -Iv ruff - -lint.run: - ruff check - ruff format +export DOCKER_BUILDKIT=1 -lint.check: - echo "Linting..." - ruff check - echo "Formatting..." - ruff format --check +export SHARED_SHA := $(shell git archive --format=tar HEAD libs/shared | sha1sum | head -c 40) +export DOCKER_REQS_SHA := $(shell sha1sum docker/Dockerfile.requirements | head -c 40) -build.requirements: - # if docker pull succeeds, we have already build this version of - # requirements.txt. Otherwise, build and push a version tagged - # with the hash of this requirements.txt - touch .testenv +# Generic target for building a requirements image. You probably want +# `worker.build.requirements` or `api.build.requirements`. +_build.requirements: docker pull ${AR_REPO}:${REQUIREMENTS_TAG} || docker build \ - -f docker/Dockerfile.requirements . \ - -t ${AR_REPO}:${REQUIREMENTS_TAG} \ - -t codecov/umbrella-ci-requirements:${REQUIREMENTS_TAG} - -build.local: - docker build -f docker/Dockerfile . \ - -t ${AR_REPO}:latest \ - -t ${AR_REPO}:${VERSION} \ - --build-arg REQUIREMENTS_IMAGE=${AR_REPO}:${REQUIREMENTS_TAG} \ - --build-arg BUILD_ENV=local - -build.app: - docker build -f docker/Dockerfile . \ - -t ${AR_REPO}:latest \ - -t ${AR_REPO}:${VERSION} \ - --label "org.label-schema.vendor"="Codecov" \ - --label "org.label-schema.version"="${release_version}-${sha}" \ - --label "org.opencontainers.image.revision"="$(long_sha)" \ - --label "org.opencontainers.image.source"="github.com/codecov/umbrella" \ - --build-arg REQUIREMENTS_IMAGE=${AR_REPO}:${REQUIREMENTS_TAG} \ - --build-arg RELEASE_VERSION=${VERSION} \ - --build-arg BUILD_ENV=cloud - -build.self-hosted: - make build.self-hosted-base - make build.self-hosted-runtime - -build.self-hosted-base: - docker build -f docker/Dockerfile . \ - -t ${DOCKERHUB_REPO}:latest-no-dependencies \ - -t ${DOCKERHUB_REPO}:${VERSION}-no-dependencies \ - --build-arg REQUIREMENTS_IMAGE=${AR_REPO}:${REQUIREMENTS_TAG} \ - --build-arg RELEASE_VERSION=${VERSION} \ - --build-arg BUILD_ENV=self-hosted - -build.self-hosted-runtime: - docker build -f docker/Dockerfile . \ - -t ${DOCKERHUB_REPO}:latest \ - -t ${DOCKERHUB_REPO}:${VERSION} \ - --label "org.label-schema.vendor"="Codecov" \ - --label "org.label-schema.version"="${release_version}-${sha}" \ - --build-arg REQUIREMENTS_IMAGE=${AR_REPO}:${REQUIREMENTS_TAG} \ - --build-arg RELEASE_VERSION=${VERSION} \ - --build-arg BUILD_ENV=self-hosted-runtime - -tag.latest: - docker tag ${AR_REPO}:${VERSION} ${AR_REPO}:latest - -tag.staging: - docker tag ${AR_REPO}:${VERSION} ${AR_REPO}:staging-${VERSION} - -tag.production: - docker tag ${AR_REPO}:${VERSION} ${AR_REPO}:production-${VERSION} - -tag.self-hosted-rolling: - docker tag ${DOCKERHUB_REPO}:${VERSION}-no-dependencies ${DOCKERHUB_REPO}:rolling_no_dependencies - docker tag ${DOCKERHUB_REPO}:${VERSION} ${DOCKERHUB_REPO}:rolling - -tag.self-hosted-release: - docker tag ${DOCKERHUB_REPO}:${VERSION}-no-dependencies ${DOCKERHUB_REPO}:${release_version}_no_dependencies - docker tag ${DOCKERHUB_REPO}:${VERSION}-no-dependencies ${DOCKERHUB_REPO}:latest_calver_no_dependencies - docker tag ${DOCKERHUB_REPO}:${VERSION}-no-dependencies ${DOCKERHUB_REPO}:latest_stable_no_dependencies - docker tag ${DOCKERHUB_REPO}:${VERSION} ${DOCKERHUB_REPO}:${release_version} - docker tag ${DOCKERHUB_REPO}:${VERSION} ${DOCKERHUB_REPO}:latest-stable - docker tag ${DOCKERHUB_REPO}:${VERSION} ${DOCKERHUB_REPO}:latest-calver - -load.requirements: - docker load --input requirements.tar - docker tag codecov/umbrella-ci-requirements:${REQUIREMENTS_TAG} ${AR_REPO}:${REQUIREMENTS_TAG} - -load.self-hosted: - docker load --input self-hosted-runtime.tar - docker load --input self-hosted.tar - -save.app: - docker save -o app.tar ${AR_REPO}:${VERSION} - -save.requirements: - docker tag ${AR_REPO}:${REQUIREMENTS_TAG} codecov/umbrella-ci-requirements:${REQUIREMENTS_TAG} - docker save -o requirements.tar codecov/umbrella-ci-requirements:${REQUIREMENTS_TAG} - -save.self-hosted: - make save.self-hosted-base - make save.self-hosted-runtime - -save.self-hosted-base: - docker save -o self-hosted.tar ${DOCKERHUB_REPO}:${VERSION}-no-dependencies - -save.self-hosted-runtime: - docker save -o self-hosted-runtime.tar ${DOCKERHUB_REPO}:${VERSION} - -push.latest: - docker push ${AR_REPO}:latest - -push.staging: - docker push ${AR_REPO}:staging-${VERSION} - -push.production: - docker push ${AR_REPO}:production-${VERSION} - -push.requirements: - docker push ${AR_REPO}:${REQUIREMENTS_TAG} - -push.self-hosted-release: - docker push ${DOCKERHUB_REPO}:${release_version}_no_dependencies - docker push ${DOCKERHUB_REPO}:latest_calver_no_dependencies - docker push ${DOCKERHUB_REPO}:latest_stable_no_dependencies - docker push ${DOCKERHUB_REPO}:${release_version} - docker push ${DOCKERHUB_REPO}:latest-stable - docker push ${DOCKERHUB_REPO}:latest-calver - -push.self-hosted-rolling: - docker push ${DOCKERHUB_REPO}:rolling_no_dependencies - docker push ${DOCKERHUB_REPO}:rolling - -test_env.up: - env | grep GITHUB > .testenv; true - TIMESERIES_ENABLED=${TIMESERIES_ENABLED} docker-compose up -d - -test_env.prepare: - docker-compose exec api make test_env.container_prepare - -test_env.check_db: - docker-compose exec umbrella make test_env.container_check_db - make test_env.check-for-migration-conflicts - -test_env.install_cli: - pip install codecov-cli==$(CODECOV_CLI_VERSION) - -test_env.container_prepare: - apt-get -y install git build-essential netcat-traditional - make test_env.install_cli - git config --global --add safe.directory /app - -test_env.container_check_db: - while ! nc -vz postgres 5432; do sleep 1; echo "waiting for postgres"; done - while ! nc -vz timescale 5432; do sleep 1; echo "waiting for timescale"; done - -test_env.run_unit: - docker-compose exec umbrella make test.unit - -test_env.run_integration: - #docker-compose exec umbrella make test.integration - echo "Skipping. No Tests" - -test_env.check-for-migration-conflicts: - docker-compose exec umbrella python manage.py check_for_migration_conflicts - -test_env.upload: - docker-compose exec umbrella make test_env.container_upload CODECOV_UPLOAD_TOKEN=${CODECOV_UPLOAD_TOKEN} CODECOV_URL=${CODECOV_URL} - docker-compose exec umbrella make test_env.container_upload_test_results CODECOV_UPLOAD_TOKEN=${CODECOV_UPLOAD_TOKEN} CODECOV_URL=${CODECOV_URL} - -test_env.container_upload: - codecovcli -u ${CODECOV_URL} upload-process --flag unit-latest-uploader --flag unit \ - --coverage-files-search-exclude-folder=graphql_api/types/** \ - --coverage-files-search-exclude-folder=api/internal/tests/unit/views/cassetes/** - -test_env.container_upload_test_results: - codecovcli -u ${CODECOV_URL} do-upload --report-type "test_results" \ - --files-search-exclude-folder=graphql_api/types/** \ - --files-search-exclude-folder=api/internal/tests/unit/views/cassetes/** || true - -test_env: - make test_env.up - make test_env.prepare - make test_env.check_db - make test_env.run_unit - make test_env.check-for-migration-conflicts - - -### START Proxy Commands -.PHONY: proxy.build -proxy.build: # Used to build the proxy -proxy.build: - docker build -f docker/Dockerfile-proxy . -t ${API_DOCKER_REPO}/proxy:latest -t ${API_DOCKER_REPO}/proxy:${release_version}-${sha} \ - --label "org.label-schema.build-date"="$(build_date)" \ - --label "org.label-schema.name"="API Proxy" \ - --label "org.label-schema.vendor"="api" \ - --label "org.label-schema.version"="${release_version}" - -.PHONY: proxy.run -proxy.run: # Used to run the proxy -proxy.run: - make proxy.build - make proxy.down - docker run --rm --network ${PROXY_NETWORK} -e FRP_TOKEN=${FRP_TOKEN} -e DOMAIN=${API_DOMAIN} --name api-proxy ${API_DOCKER_REPO}/proxy:latest - sleep 3 - make proxy.logs - # You should see "[api] start proxy success" - # If no logs then proxy failed to start. Check if you are on VPN. If you get a 404, check if you are on VPN - -.PHONY: proxy.logs -proxy.logs: # Used to logs the proxy -proxy.logs: - docker logs api-proxy - -.PHONY: proxy.shell -proxy.shell: # Used to shell the proxy -proxy.shell: - docker exec -it api-proxy sh - -.PHONY: proxy.down -proxy.down: # Used to down the proxy -proxy.down: - docker kill api-proxy || true - -### END PROXY Commands + -f docker/Dockerfile.requirements . \ + --build-arg APP_DIR=${APP_DIR} \ + -t ${AR_REPO}:${REQUIREMENTS_TAG} \ + -t ${CI_REQS_REPO}:${REQUIREMENTS_TAG} + +###### +# codecov-api targets +###### +API_UV_LOCK_SHA := $(shell sha1sum apps/codecov-api/uv.lock | head -c 40) +API_REQS_TAG := reqs-${API_UV_LOCK_SHA}-${DOCKER_REQS_SHA}-${SHARED_SHA} + +define api_rule_prefix +.PHONY: $(1) +$(1): export APP_DIR := apps/codecov-api +$(1): export REQUIREMENTS_TAG := ${API_REQS_TAG} +$(1): export AR_REPO ?= codecov/api-umbrella +$(1): export DOCKERHUB_REPO ?= codecov/self-hosted-api-umbrella +$(1): export CI_REQS_REPO ?= codecov/api-ci-requirements +endef + +# umbrella builds a special requirements image for api that installs shared properly. +$(eval $(call api_rule_prefix,api.build.requirements)) +api.build.requirements: + $(MAKE) _build.requirements + +# This target calls `make build.requirements` for api so we have to make sure it calls our +# custom `build.requirements` instead. +$(eval $(call api_rule_prefix,api.build)) +api.build: + $(MAKE) api.build.requirements + $(MAKE) -C apps/codecov-api build.local + +# Any other target starting with `api.` should be forwarded to `apps/codecov-api`. +# The `$*` variable is the string caught by the `%` in the rule pattern. +$(eval $(call api_rule_prefix,api.%)) +api.%: + $(MAKE) -C apps/codecov-api $* + +###### +# worker targets +###### +WORKER_UV_LOCK_SHA := $(shell sha1sum apps/worker/uv.lock | head -c 40) +WORKER_REQS_TAG := reqs-${WORKER_UV_LOCK_SHA}-${DOCKER_REQS_SHA}-${SHARED_SHA} + +define worker_rule_prefix +.PHONY: $(1) +$(1): export APP_DIR := apps/worker +$(1): export REQUIREMENTS_TAG := ${WORKER_REQS_TAG} +$(1): export AR_REPO ?= codecov/worker-umbrella +$(1): export DOCKERHUB_REPO ?= codecov/self-hosted-worker-umbrella +$(1): export CI_REQS_REPO ?= codecov/worker-ci-requirements +endef + +# umbrella builds a special requirements image for worker that installs shared properly. +$(eval $(call worker_rule_prefix,worker.build.requirements)) +worker.build.requirements: + $(MAKE) _build.requirements + +# This target calls `make build.requirements` for worker so we have to make sure it calls our +# custom `build.requirements` instead. +$(eval $(call worker_rule_prefix,worker.build)) +worker.build: + $(MAKE) worker.build.requirements + $(MAKE) -C apps/worker build.local + +# Any other target starting with `worker.` should be forwarded to `apps/worker`. +# The `$*` variable is the string caught by the `%` in the rule pattern. +$(eval $(call worker_rule_prefix,worker.%)) +worker.%: + $(MAKE) -C apps/worker $* + +###### +# shared targets +###### + +# No need to override any of shared's targets. Just run make in `libs/shared`. +.PHONY: shared.% +shared.%: + $(MAKE) -C libs/shared $* + +###### +# Development environment targets +###### +include tools/devenv/Makefile.devenv diff --git a/apps/codecov-api b/apps/codecov-api deleted file mode 160000 index defc6a5acf..0000000000 --- a/apps/codecov-api +++ /dev/null @@ -1 +0,0 @@ -Subproject commit defc6a5acf7c03dda8ef757c47b42daa3468aeb1 diff --git a/apps/codecov-api/.deep-dive.yaml b/apps/codecov-api/.deep-dive.yaml new file mode 100644 index 0000000000..b232e532f9 --- /dev/null +++ b/apps/codecov-api/.deep-dive.yaml @@ -0,0 +1,6 @@ +search: + exclude_files: + python3libs: '/usr/lib/python3\.9.*' + files: + python: '.*\.pyc?' + json: '.*\.json' \ No newline at end of file diff --git a/apps/codecov-api/.dockerignore b/apps/codecov-api/.dockerignore new file mode 100644 index 0000000000..ab95e851c5 --- /dev/null +++ b/apps/codecov-api/.dockerignore @@ -0,0 +1,21 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +env +pip-log.txt +pip-delete-this-directory.txt +.tox +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +*.log +.git +service.json +.github +.circleci +gha-creds-*.json diff --git a/apps/codecov-api/.git-blame-ignore-revs b/apps/codecov-api/.git-blame-ignore-revs new file mode 100644 index 0000000000..5c3a1becab --- /dev/null +++ b/apps/codecov-api/.git-blame-ignore-revs @@ -0,0 +1,15 @@ +# Since git version 2.23, git-blame has a feature to ignore +# certain commits. +# +# This file contains a list of commits that are not likely what +# you are looking for in `git blame`. You can set this file as +# a default ignore file for blame by running the following +# command. +# +# $ git config blame.ignoreRevsFile .git-blame-ignore-revs + +# Run black on entire codebase +714d1f8da36b57b59d2a40d66c0a9a2cefa60b87 + +# Linting the whole codecov codebase +899fe2bc94c1d4e6c07c5b04d3d353b889a37229 diff --git a/apps/codecov-api/.github/CODEOWNERS b/apps/codecov-api/.github/CODEOWNERS new file mode 100644 index 0000000000..d2fd6267c8 --- /dev/null +++ b/apps/codecov-api/.github/CODEOWNERS @@ -0,0 +1,7 @@ +upload/ @codecov/Platform +graphql_api/ @codecov/API +**/migrations/ @codecov/database-migration-reviewers + +codecov_auth/models.py @adrian-codecov +core/models.py @adrian-codecov +reports/models.py @adrian-codecov diff --git a/apps/codecov-api/.github/ISSUE_TEMPLATE/bug_report.md b/apps/codecov-api/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..07f3be3e12 --- /dev/null +++ b/apps/codecov-api/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,16 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + + + +# 📣 Feedback / 🐛 Bugs + +Do you want to file a bug report and/or feature request for Codecov? [Please use our feedback repo instead](https://github.com/codecov/feedback/issues). diff --git a/apps/codecov-api/.github/ISSUE_TEMPLATE/feature_request.md b/apps/codecov-api/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..03313301fe --- /dev/null +++ b/apps/codecov-api/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,16 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + + + +# 📣 Feedback / 🐛 Bugs + +Do you want to file a bug report and/or feature request for Codecov? [Please use our feedback repo instead](https://github.com/codecov/feedback/issues). diff --git a/apps/codecov-api/.github/pull_request_template.md b/apps/codecov-api/.github/pull_request_template.md new file mode 100644 index 0000000000..6bc2b10270 --- /dev/null +++ b/apps/codecov-api/.github/pull_request_template.md @@ -0,0 +1,19 @@ +### Purpose/Motivation +What is the feature? Why is this being done? + +### Links to relevant tickets + +### What does this PR do? +Include a brief description of the changes in this PR. Bullet points are your friend. + +### Notes to Reviewer +Anything to note to the team? Any tips on how to review, or where to start? + + +### Legal Boilerplate + +Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. In 2022 this entity acquired Codecov and as result Sentry is going to need some rights from me in order to utilize my contributions in this PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms. diff --git a/apps/codecov-api/.github/workflows/cache_cleanup.yml b/apps/codecov-api/.github/workflows/cache_cleanup.yml new file mode 100644 index 0000000000..a8da8d01a7 --- /dev/null +++ b/apps/codecov-api/.github/workflows/cache_cleanup.yml @@ -0,0 +1,33 @@ +name: cleanup caches by a branch +on: + pull_request: + types: + - closed + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Cleanup + run: | + gh extension install actions/gh-actions-cache + + REPO=${{ github.repository }} + BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge" + + echo "Fetching list of cache key" + cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 ) + + ## Setting this to not fail the workflow while deleting cache keys. + set +e + echo "Deleting caches..." + for cacheKey in $cacheKeysForPR + do + gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm + done + echo "Done" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/apps/codecov-api/.github/workflows/ci.yml b/apps/codecov-api/.github/workflows/ci.yml new file mode 100644 index 0000000000..ff00f3ffc6 --- /dev/null +++ b/apps/codecov-api/.github/workflows/ci.yml @@ -0,0 +1,87 @@ +name: API CI + +on: + push: + branches: + - main + - staging + pull_request: + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: "read" + id-token: "write" + issues: "write" + pull-requests: "write" + +jobs: + lint: + name: Run Lint + uses: codecov/gha-workflows/.github/workflows/lint.yml@v1.2.33 + + build: + name: Build API + uses: codecov/gha-workflows/.github/workflows/build-app.yml@v1.2.33 + secrets: inherit + with: + repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-api' }} + cache_file: "uv.lock" + + codecovstartup: + name: Codecov Startup + needs: build + uses: codecov/gha-workflows/.github/workflows/codecov-startup.yml@v1.2.33 + secrets: inherit + + test: + name: Test + needs: [build] + uses: codecov/gha-workflows/.github/workflows/run-tests.yml@v1.2.33 + secrets: inherit + with: + run_integration: false + repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-api' }} + + build-self-hosted: + name: Build Self Hosted API + needs: [build, test] + uses: codecov/gha-workflows/.github/workflows/self-hosted.yml@v1.2.33 + secrets: inherit + with: + repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-api' }} + cache_file: "uv.lock" + + staging: + name: Push Staging Image + needs: [build, test] + if: ${{ github.event_name == 'push' && github.event.ref == 'refs/heads/staging' && github.repository_owner == 'codecov' }} + uses: codecov/gha-workflows/.github/workflows/push-env.yml@v1.2.33 + secrets: inherit + with: + environment: staging + repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-api' }} + + production: + name: Push Production Image + needs: [build, test] + if: ${{ github.event_name == 'push' && github.event.ref == 'refs/heads/main' && github.repository_owner == 'codecov' }} + uses: codecov/gha-workflows/.github/workflows/push-env.yml@v1.2.33 + secrets: inherit + with: + environment: production + repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-api' }} + + self-hosted: + name: Push Self Hosted Image + needs: [build-self-hosted, test] + secrets: inherit + if: ${{ github.event_name == 'push' && github.event.ref == 'refs/heads/main' && github.repository_owner == 'codecov' }} + uses: codecov/gha-workflows/.github/workflows/self-hosted.yml@v1.2.33 + with: + push_rolling: true + repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-api' }} + cache_file: "uv.lock" diff --git a/apps/codecov-api/.github/workflows/enforce-license-compliance.yml b/apps/codecov-api/.github/workflows/enforce-license-compliance.yml new file mode 100644 index 0000000000..86be74100e --- /dev/null +++ b/apps/codecov-api/.github/workflows/enforce-license-compliance.yml @@ -0,0 +1,14 @@ +name: Enforce License Compliance + +on: + pull_request: + branches: [main, master] + +jobs: + enforce-license-compliance: + runs-on: ubuntu-latest + steps: + - name: 'Enforce License Compliance' + uses: getsentry/action-enforce-license-compliance@57ba820387a1a9315a46115ee276b2968da51f3d # main + with: + fossa_api_key: ${{ secrets.FOSSA_API_KEY }} diff --git a/apps/codecov-api/.github/workflows/mypy.yml b/apps/codecov-api/.github/workflows/mypy.yml new file mode 100644 index 0000000000..d51e19b1e5 --- /dev/null +++ b/apps/codecov-api/.github/workflows/mypy.yml @@ -0,0 +1,14 @@ +name: "Patch typing check" + +on: + push: + branches: + - main + - staging + pull_request: + merge_group: + +jobs: + patch-typing-check: + name: Run Patch Type Check + uses: codecov/gha-workflows/.github/workflows/mypy.yml@v1.2.33 diff --git a/apps/codecov-api/.github/workflows/pr_detect_shared_changes.yml b/apps/codecov-api/.github/workflows/pr_detect_shared_changes.yml new file mode 100644 index 0000000000..15d4e96d9f --- /dev/null +++ b/apps/codecov-api/.github/workflows/pr_detect_shared_changes.yml @@ -0,0 +1,14 @@ +name: Detect dep version changes + +on: + pull_request: + +permissions: + pull-requests: "write" + +jobs: + shared-change-checker: + name: See if shared changed + uses: codecov/gha-workflows/.github/workflows/diff-dep.yml@main + with: + dep: 'shared' diff --git a/apps/codecov-api/.github/workflows/self-hosted-release-pr.yml b/apps/codecov-api/.github/workflows/self-hosted-release-pr.yml new file mode 100644 index 0000000000..aa094c802d --- /dev/null +++ b/apps/codecov-api/.github/workflows/self-hosted-release-pr.yml @@ -0,0 +1,14 @@ +name: Create Self Hosted Release PR + +on: + workflow_dispatch: + inputs: + versionName: + description: "Name of version (ie 23.9.5)" + required: true + +jobs: + create-release-pr: + name: Create PR for Release ${{ github.event.inputs.versionName }} + uses: codecov/gha-workflows/.github/workflows/create-release-pr.yml@v1.2.33 + secrets: inherit diff --git a/apps/codecov-api/.github/workflows/self-hosted-release.yml b/apps/codecov-api/.github/workflows/self-hosted-release.yml new file mode 100644 index 0000000000..f6c1ee6f2b --- /dev/null +++ b/apps/codecov-api/.github/workflows/self-hosted-release.yml @@ -0,0 +1,30 @@ +name: Create Self Hosted Release + +on: + pull_request: + branches: + - main + types: [closed] + +permissions: + contents: "read" + id-token: "write" + +jobs: + create-release: + name: Tag Release ${{ github.head_ref }} and Push Docker image to Docker Hub + if: ${{ github.event.pull_request.merged == true && startsWith(github.head_ref, 'release/') && github.repository_owner == 'codecov' }} + uses: codecov/gha-workflows/.github/workflows/create-release.yml@v1.2.33 + with: + tag_to_prepend: self-hosted- + secrets: inherit + + push-image: + needs: [create-release] + if: ${{ github.event.pull_request.merged == true && startsWith(github.head_ref, 'release/') && github.repository_owner == 'codecov' }} + uses: codecov/gha-workflows/.github/workflows/self-hosted.yml@v1.2.33 + secrets: inherit + with: + push_release: true + repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-api' }} + cache_file: "uv.lock" diff --git a/apps/codecov-api/.github/workflows/upload-overwatch.yml b/apps/codecov-api/.github/workflows/upload-overwatch.yml new file mode 100644 index 0000000000..c7639e5045 --- /dev/null +++ b/apps/codecov-api/.github/workflows/upload-overwatch.yml @@ -0,0 +1,37 @@ +name: Upload Overwatch + +on: + pull_request: + types: + - opened + - synchronize + +jobs: + upload-overwatch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - name: Install UV + run: pip install uv + - name: Install Project Dependencies + run: | + uv export --format requirements-txt > requirements.txt + uv pip install -r requirements.txt --system + - name: Install Static Analysis Tools + run: | + pip install mypy==1.15.0 + pip install ruff==0.9.6 + - name: Install Overwatch CLI + run: | + curl -o overwatch-cli https://overwatch.codecov.dev/linux/cli + chmod +x overwatch-cli + - name: Run Overwatch CLI + run: | + ./overwatch-cli \ + --auth-token ${{ secrets.SENTRY_AUTH_TOKEN }} \ + --organization-slug codecov \ + python \ + --python-path $(which python3) diff --git a/apps/codecov-api/.gitignore b/apps/codecov-api/.gitignore new file mode 100644 index 0000000000..6ba5590c6c --- /dev/null +++ b/apps/codecov-api/.gitignore @@ -0,0 +1,136 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +gha-creds-*.json + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ +*.coverage.xml +*junit.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.env.* +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.testenv +.envrc +.debug + +codecov/static/* +!codecov/static/__init__.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# vim temp files +*.swp +*.swo +.vscode/ + +# PyCharm stuff +.idea +.run + +*.pem diff --git a/apps/codecov-api/.gitmodules b/apps/codecov-api/.gitmodules new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/.pre-commit-config.yaml b/apps/codecov-api/.pre-commit-config.yaml new file mode 100644 index 0000000000..8715a2e0b8 --- /dev/null +++ b/apps/codecov-api/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +repos: + - repo: local + hooks: + - id: lint + name: lint + description: "Lint and sort" + entry: make lint.local + pass_filenames: false + require_serial: true + language: system + - repo: https://github.com/pre-commit/mirrors-mypy + rev: "v1.10.0" + hooks: + - id: mypy + verbose: true + entry: bash -c 'mypy "$@" || true' -- diff --git a/apps/codecov-api/LICENSE.md b/apps/codecov-api/LICENSE.md new file mode 100644 index 0000000000..630ce64ac6 --- /dev/null +++ b/apps/codecov-api/LICENSE.md @@ -0,0 +1,105 @@ +# Functional Source License, Version 1.1, Apache 2.0 Future License + +## Abbreviation + +FSL-1.1-Apache-2.0 + +## Notice + +Copyright 2018-2024 Functional Software, Inc. dba Sentry + +## Terms and Conditions + +### Licensor ("We") + +The party offering the Software under these Terms and Conditions. + +### The Software + +The "Software" is each version of the software that we make available under +these Terms and Conditions, as indicated by our inclusion of these Terms and +Conditions with the Software. + +### License Grant + +Subject to your compliance with this License Grant and the Patents, +Redistribution and Trademark clauses below, we hereby grant you the right to +use, copy, modify, create derivative works, publicly perform, publicly display +and redistribute the Software for any Permitted Purpose identified below. + +### Permitted Purpose + +A Permitted Purpose is any purpose other than a Competing Use. A Competing Use +means making the Software available to others in a commercial product or +service that: + +1. substitutes for the Software; + +2. substitutes for any other product or service we offer using the Software + that exists as of the date we make the Software available; or + +3. offers the same or substantially similar functionality as the Software. + +Permitted Purposes specifically include using the Software: + +1. for your internal use and access; + +2. for non-commercial education; + +3. for non-commercial research; and + +4. in connection with professional services that you provide to a licensee + using the Software in accordance with these Terms and Conditions. + +### Patents + +To the extent your use for a Permitted Purpose would necessarily infringe our +patents, the license grant above includes a license under our patents. If you +make a claim against any party that the Software infringes or contributes to +the infringement of any patent, then your patent license to the Software ends +immediately. + +### Redistribution + +The Terms and Conditions apply to all copies, modifications and derivatives of +the Software. + +If you redistribute any copies, modifications or derivatives of the Software, +you must include a copy of or a link to these Terms and Conditions and not +remove any copyright notices provided in or with the Software. + +### Disclaimer + +THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR +PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT. + +IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE +SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, +EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE. + +### Trademarks + +Except for displaying the License Details and identifying us as the origin of +the Software, you have no right under these Terms and Conditions to use our +trademarks, trade names, service marks or product names. + +## Grant of Future License + +We hereby irrevocably grant you an additional license to use the Software under +the Apache License, Version 2.0 that is effective on the second anniversary of +the date we make the Software available. On or after that date, you may use the +Software under the Apache License, Version 2.0, in which case the following +will apply: + +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. diff --git a/apps/codecov-api/Makefile b/apps/codecov-api/Makefile new file mode 100644 index 0000000000..d9748d3dfb --- /dev/null +++ b/apps/codecov-api/Makefile @@ -0,0 +1,281 @@ +sha ?= $(shell git rev-parse --short=7 HEAD) +long_sha ?= $(shell git rev-parse HEAD) +merge_sha ?= $(shell git merge-base HEAD^ origin/main) +release_version := `cat VERSION` +build_date ?= $(shell git show -s --date=iso8601-strict --pretty=format:%cd $$sha) +branch ?= $(shell git branch | grep \* | cut -f2 -d' ') +epoch ?= $(shell date +"%s") +AR_REPO ?= codecov/api +DOCKERHUB_REPO ?= codecov/self-hosted-api +REQUIREMENTS_TAG ?= requirements-v1-$(shell sha1sum uv.lock | cut -d ' ' -f 1)-$(shell sha1sum docker/Dockerfile.requirements | cut -d ' ' -f 1) +VERSION ?= release-${sha} +CODECOV_UPLOAD_TOKEN ?= "notset" +CODECOV_STATIC_TOKEN ?= "notset" +CODECOV_URL ?= "https://api.codecov.io" +export DOCKER_BUILDKIT=1 +export API_DOCKER_REPO=${AR_REPO} +export API_DOCKER_VERSION=${VERSION} +export CODECOV_TOKEN=${CODECOV_UPLOAD_TOKEN} +API_DOMAIN ?= api +PROXY_NETWORK ?= api_default + +DEFAULT_REQS_TAG := requirements-v1-$(shell sha1sum uv.lock | cut -d ' ' -f 1)-$(shell sha1sum docker/Dockerfile.requirements | cut -d ' ' -f 1) +REQUIREMENTS_TAG ?= ${DEFAULT_REQS_TAG} + +# We allow this to be overridden so that we can run `pytest` from this directory +# but have the junit file use paths relative to a parent directory. This will +# help us move to a monorepo. +PYTEST_ROOTDIR ?= "." + +# Codecov CLI version to use +CODECOV_CLI_VERSION := 9.0.4 + +build: + make build.requirements + make build.app + +check-for-migration-conflicts: + python manage.py check_for_migration_conflicts + +test: + COVERAGE_CORE=sysmon pytest --cov=./ --junitxml=junit.xml -o junit_family=legacy -c pytest.ini --rootdir=${PYTEST_ROOTDIR} + +test.unit: + COVERAGE_CORE=sysmon pytest --cov=./ -m "not integration" --cov-report=xml:unit.coverage.xml --junitxml=unit.junit.xml -o junit_family=legacy -c pytest.ini --rootdir=${PYTEST_ROOTDIR} + +test.integration: + COVERAGE_CORE=sysmon pytest --cov=./ -m "integration" --cov-report=xml:integration.coverage.xml --junitxml=integration.junit.xml -o junit_family=legacy -c pytest.ini --rootdir=${PYTEST_ROOTDIR} + +lint: + make lint.install + make lint.run + +lint.install: + echo "Installing..." + pip install -Iv ruff + +lint.local: + make lint.install.local + make lint.run + +lint.install.local: + echo "Installing..." + uv add --dev ruff + +lint.run: + ruff check + ruff format + +lint.check: + echo "Linting..." + ruff check + echo "Formatting..." + ruff format --check + +build.requirements: + # If make was given a different requirements tag, we assume a suitable image + # was already built (e.g. by umbrella) and don't want to build this one. + ifneq (${REQUIREMENTS_TAG},${DEFAULT_REQS_TAG}) + echo "Error: building api reqs image despite another being provided" + exit 1 + endif + # if docker pull succeeds, we have already build this version of + # requirements.txt. Otherwise, build and push a version tagged + # with the hash of this requirements.txt + touch .testenv + docker pull ${AR_REPO}:${REQUIREMENTS_TAG} || docker build \ + -f docker/Dockerfile.requirements . \ + -t ${AR_REPO}:${REQUIREMENTS_TAG} \ + -t codecov/api-ci-requirements:${REQUIREMENTS_TAG} + +build.local: + docker build -f docker/Dockerfile . \ + -t ${AR_REPO}:latest \ + -t ${AR_REPO}:${VERSION} \ + --build-arg REQUIREMENTS_IMAGE=${AR_REPO}:${REQUIREMENTS_TAG} \ + --build-arg BUILD_ENV=local + +build.app: + docker build -f docker/Dockerfile . \ + -t ${AR_REPO}:latest \ + -t ${AR_REPO}:${VERSION} \ + --label "org.label-schema.vendor"="Codecov" \ + --label "org.label-schema.version"="${release_version}-${sha}" \ + --label "org.opencontainers.image.revision"="$(long_sha)" \ + --label "org.opencontainers.image.source"="github.com/codecov/codecov-api" \ + --build-arg REQUIREMENTS_IMAGE=${AR_REPO}:${REQUIREMENTS_TAG} \ + --build-arg RELEASE_VERSION=${VERSION} \ + --build-arg BUILD_ENV=cloud + +build.self-hosted: + make build.self-hosted-base + make build.self-hosted-runtime + +build.self-hosted-base: + docker build -f docker/Dockerfile . \ + -t ${DOCKERHUB_REPO}:latest-no-dependencies \ + -t ${DOCKERHUB_REPO}:${VERSION}-no-dependencies \ + --build-arg REQUIREMENTS_IMAGE=${AR_REPO}:${REQUIREMENTS_TAG} \ + --build-arg RELEASE_VERSION=${VERSION} \ + --build-arg BUILD_ENV=self-hosted + +build.self-hosted-runtime: + docker build -f docker/Dockerfile . \ + -t ${DOCKERHUB_REPO}:latest \ + -t ${DOCKERHUB_REPO}:${VERSION} \ + --label "org.label-schema.vendor"="Codecov" \ + --label "org.label-schema.version"="${release_version}-${sha}" \ + --build-arg REQUIREMENTS_IMAGE=${AR_REPO}:${REQUIREMENTS_TAG} \ + --build-arg RELEASE_VERSION=${VERSION} \ + --build-arg BUILD_ENV=self-hosted-runtime + +tag.latest: + docker tag ${AR_REPO}:${VERSION} ${AR_REPO}:latest + +tag.staging: + docker tag ${AR_REPO}:${VERSION} ${AR_REPO}:staging-${VERSION} + +tag.production: + docker tag ${AR_REPO}:${VERSION} ${AR_REPO}:production-${VERSION} + +tag.self-hosted-rolling: + docker tag ${DOCKERHUB_REPO}:${VERSION}-no-dependencies ${DOCKERHUB_REPO}:rolling_no_dependencies + docker tag ${DOCKERHUB_REPO}:${VERSION} ${DOCKERHUB_REPO}:rolling + +tag.self-hosted-release: + docker tag ${DOCKERHUB_REPO}:${VERSION}-no-dependencies ${DOCKERHUB_REPO}:${release_version}_no_dependencies + docker tag ${DOCKERHUB_REPO}:${VERSION}-no-dependencies ${DOCKERHUB_REPO}:latest_calver_no_dependencies + docker tag ${DOCKERHUB_REPO}:${VERSION}-no-dependencies ${DOCKERHUB_REPO}:latest_stable_no_dependencies + docker tag ${DOCKERHUB_REPO}:${VERSION} ${DOCKERHUB_REPO}:${release_version} + docker tag ${DOCKERHUB_REPO}:${VERSION} ${DOCKERHUB_REPO}:latest-stable + docker tag ${DOCKERHUB_REPO}:${VERSION} ${DOCKERHUB_REPO}:latest-calver + +load.requirements: + docker load --input requirements.tar + docker tag codecov/api-ci-requirements:${REQUIREMENTS_TAG} ${AR_REPO}:${REQUIREMENTS_TAG} + +load.self-hosted: + docker load --input self-hosted-runtime.tar + docker load --input self-hosted.tar + +save.app: + docker save -o app.tar ${AR_REPO}:${VERSION} + +save.requirements: + docker tag ${AR_REPO}:${REQUIREMENTS_TAG} codecov/api-ci-requirements:${REQUIREMENTS_TAG} + docker save -o requirements.tar codecov/api-ci-requirements:${REQUIREMENTS_TAG} + +save.self-hosted: + make save.self-hosted-base + make save.self-hosted-runtime + +save.self-hosted-base: + docker save -o self-hosted.tar ${DOCKERHUB_REPO}:${VERSION}-no-dependencies + +save.self-hosted-runtime: + docker save -o self-hosted-runtime.tar ${DOCKERHUB_REPO}:${VERSION} + +push.latest: + docker push ${AR_REPO}:latest + +push.staging: + docker push ${AR_REPO}:staging-${VERSION} + +push.production: + docker push ${AR_REPO}:production-${VERSION} + +push.requirements: + docker push ${AR_REPO}:${REQUIREMENTS_TAG} + +push.self-hosted-release: + docker push ${DOCKERHUB_REPO}:${release_version}_no_dependencies + docker push ${DOCKERHUB_REPO}:latest_calver_no_dependencies + docker push ${DOCKERHUB_REPO}:latest_stable_no_dependencies + docker push ${DOCKERHUB_REPO}:${release_version} + docker push ${DOCKERHUB_REPO}:latest-stable + docker push ${DOCKERHUB_REPO}:latest-calver + +push.self-hosted-rolling: + docker push ${DOCKERHUB_REPO}:rolling_no_dependencies + docker push ${DOCKERHUB_REPO}:rolling + +shell: + docker compose exec api bash + +test_env.up: + env | grep GITHUB > .testenv; true + docker-compose up -d + +test_env.prepare: + docker compose exec api make test_env.container_prepare + +test_env.check_db: + docker compose exec api make test_env.container_check_db + make test_env.check-for-migration-conflicts + +test_env.install_cli: + pip install codecov-cli==$(CODECOV_CLI_VERSION) + +test_env.container_prepare: + apt-get -y install git build-essential netcat-traditional + git config --global --add safe.directory /app/apps/codecov-api || true + +test_env.container_check_db: + while ! nc -vz postgres 5432; do sleep 1; echo "waiting for postgres"; done + while ! nc -vz timescale 5432; do sleep 1; echo "waiting for timescale"; done + +test_env.run_unit: + docker compose exec api make test.unit PYTEST_ROOTDIR=${PYTEST_ROOTDIR} + +test_env.run_integration: + # docker compose exec api make test.integration + echo "Skipping. No Tests" + +test_env.check-for-migration-conflicts: + docker compose exec api python manage.py check_for_migration_conflicts + +test_env: + make test_env.up + make test_env.prepare + make test_env.check_db + make test_env.run_unit + make test_env.check-for-migration-conflicts + + +### START Proxy Commands +.PHONY: proxy.build +proxy.build: # Used to build the proxy +proxy.build: + docker build -f docker/Dockerfile-proxy . -t ${API_DOCKER_REPO}/proxy:latest -t ${API_DOCKER_REPO}/proxy:${release_version}-${sha} \ + --label "org.label-schema.build-date"="$(build_date)" \ + --label "org.label-schema.name"="API Proxy" \ + --label "org.label-schema.vendor"="api" \ + --label "org.label-schema.version"="${release_version}" + +.PHONY: proxy.run +proxy.run: # Used to run the proxy +proxy.run: + make proxy.build + make proxy.down + docker run --rm --network ${PROXY_NETWORK} -e FRP_TOKEN=${FRP_TOKEN} -e DOMAIN=${API_DOMAIN} --name api-proxy ${API_DOCKER_REPO}/proxy:latest + sleep 3 + make proxy.logs + # You should see "[api] start proxy success" + # If no logs then proxy failed to start. Check if you are on VPN. If you get a 404, check if you are on VPN + +.PHONY: proxy.logs +proxy.logs: # Used to logs the proxy +proxy.logs: + docker logs api-proxy + +.PHONY: proxy.shell +proxy.shell: # Used to shell the proxy +proxy.shell: + docker exec -it api-proxy sh + +.PHONY: proxy.down +proxy.down: # Used to down the proxy +proxy.down: + docker kill api-proxy || true + +### END PROXY Commands diff --git a/apps/codecov-api/README.md b/apps/codecov-api/README.md new file mode 100644 index 0000000000..de8b96ec95 --- /dev/null +++ b/apps/codecov-api/README.md @@ -0,0 +1,85 @@ +## Codecov API + +> We believe that everyone should have access to quality software (like Sentry), that’s why we have always offered Codecov for free to open source maintainers. +> +> By making our code public, we’re not only joining the community that’s supported us from the start — but also want to make sure that every developer can contribute to and build on the Codecov experience. + +A private Django REST Framework API intended to serve Codecov's front end. + +## Getting Started + +### Building + +This project contains a makefile. To build the docker image: + + make build + +`requirements.txt` is used in the base image. If you make changes to `requirements.txt` you will need to rebuild. + +Note, you'll need to install Rust to build `ribs` which is a dependency of `shared`. Go here for more info on how to do this: https://www.rust-lang.org/tools/install + +### Running Standalone + +This project contains a `docker-compose.yml` file that is intended to run the api standalone. In this configuration it **does not** share codecov.io's development database; so don't expect parity there. + +To start the service, do + +`docker-compose up` + +Utilizing its own database provides a convenient way for the REST API to provide its own helpful seeds and migrations for active development without potentially destroying/modifying your development database for codecov.io. + +Once running, the api will be available at `http://localhost:5100` + +### Running with codecov.io + +This service will startup when you run codecov.io normally. It is under that `api` block of codecov.io's `docker-compose.yml` file. + +### Testing + +The easiest way to run tests (that doesn't require installing postgres and other dependencies) is to run inside of docker: + + docker-compose up + docker exec -it codecov-api_api_1 pytest -rf --no-migrations + +### Testing standalone + +If you would like to use pytest directly (Either through an IDE like PyCharm or with the CLI), you will need to change the settings file used by pytest. Run this command to have the tests executed (You will need an instance of postgres running locally): + + RUN_ENV=TESTING DJANGO_SETTINGS_MODULE=codecov.settings_test pytest + +Make sure to have all the latest dependencies installed via `uv sync`. + +### Deploying + +All work merged into the `main` branch is immediately deployed to the production environment. More context on this strategy can be found [here](https://codecovio.atlassian.net/wiki/spaces/ENG/pages/507445249/Branching+and+Continuous+Delivery+Strategy+Proposal). + +### Deploying to Staging environment + +To deploy to our staging environment it's crucial to follow these steps: + +1. Check in Slack to see if anyone is currently using the staging environment +2. If not, delete the current `staging` branch +3. Create a new `staging` branch and merge your feature branch into it + +Steps 2 and 3 are important to limit interaction between features not yet merged into `main`. This approach was inspired by this document: https://codecovio.atlassian.net/wiki/spaces/ENG/pages/507445249/Branching+and+Continuous+Delivery+Strategy+Proposal + +### Secret and Credential Management + +This project should store no secrets or credentials in its source. If you need to add to / modify / setup secrets for this project, contact Eli and he'll get you started.. + +### Adding dependencies + +This repository uses `uv` to manage dependencies, so make sure you've installed it with `pip install uv`. To add or update dependencies, simply run `uv add __package_name__` or `uv sync`. + +### Formatting + +This project uses `ruff` for formatting. +You can run the linter using the command `make lint_local`. + +### Migrations + +We leverage Django's migration system to keep the state of our models in sync with the state of our database. You can read more about how we work with migrations at https://codecovio.atlassian.net/wiki/spaces/ENG/pages/1696530442/Migrations + +## Contributing + +This repository, like all of Codecov's repositories, strives to follow our general [Contributing guidlines](https://github.com/codecov/contributing). If you're considering making a contribution to this repository, we encourage review of our Contributing guidelines first. diff --git a/apps/codecov-api/VERSION b/apps/codecov-api/VERSION new file mode 100644 index 0000000000..86a8f0262a --- /dev/null +++ b/apps/codecov-api/VERSION @@ -0,0 +1 @@ +25.4.1 \ No newline at end of file diff --git a/apps/codecov-api/api/__init__.py b/apps/codecov-api/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/apps.py b/apps/codecov-api/api/apps.py new file mode 100644 index 0000000000..14b89a8298 --- /dev/null +++ b/apps/codecov-api/api/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + name = "api" diff --git a/apps/codecov-api/api/gen_ai/__init__.py b/apps/codecov-api/api/gen_ai/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/gen_ai/serializers.py b/apps/codecov-api/api/gen_ai/serializers.py new file mode 100644 index 0000000000..1b7ceec225 --- /dev/null +++ b/apps/codecov-api/api/gen_ai/serializers.py @@ -0,0 +1,5 @@ +from rest_framework import serializers + + +class GenAIAuthSerializer(serializers.Serializer): + is_valid = serializers.BooleanField() diff --git a/apps/codecov-api/api/gen_ai/tests/test_gen_ai.py b/apps/codecov-api/api/gen_ai/tests/test_gen_ai.py new file mode 100644 index 0000000000..4b63d51d74 --- /dev/null +++ b/apps/codecov-api/api/gen_ai/tests/test_gen_ai.py @@ -0,0 +1,114 @@ +import hmac +from hashlib import sha256 +from unittest.mock import patch + +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov_auth.models import GithubAppInstallation + +PAYLOAD_SECRET = b"testixik8qdauiab1yiffydimvi72ekq" +VIEW_URL = reverse("auth") + + +def sign_payload(data: bytes, secret=PAYLOAD_SECRET): + signature = "sha256=" + hmac.new(secret, data, digestmod=sha256).hexdigest() + return signature, data + + +class GenAIAuthViewTests(APITestCase): + @patch("api.gen_ai.views.get_config", return_value=PAYLOAD_SECRET) + def test_missing_parameters(self, mock_config): + payload = b"{}" + sig, data = sign_payload(payload) + response = self.client.post( + VIEW_URL, + data=data, + content_type="application/json", + HTTP_HTTP_X_GEN_AI_AUTH_SIGNATURE=sig, + ) + self.assertEqual(response.status_code, 400) + self.assertIn("Missing required parameters", response.data) + + @patch("api.gen_ai.views.get_config", return_value=PAYLOAD_SECRET) + def test_invalid_signature(self, mock_config): + # Correct payload + payload = b'{"external_owner_id":"owner1","repo_service_id":"101"}' + # Wrong signature based on a different payload + wrong_sig = "sha256=" + hmac.new(PAYLOAD_SECRET, b"{}", sha256).hexdigest() + response = self.client.post( + VIEW_URL, + data=payload, + content_type="application/json", + HTTP_HTTP_X_GEN_AI_AUTH_SIGNATURE=wrong_sig, + ) + self.assertEqual(response.status_code, 403) + + @patch("api.gen_ai.views.get_config", return_value=PAYLOAD_SECRET) + def test_owner_not_found(self, mock_config): + payload = b'{"external_owner_id":"nonexistent_owner","repo_service_id":"101"}' + sig, data = sign_payload(payload) + response = self.client.post( + VIEW_URL, + data=data, + content_type="application/json", + HTTP_HTTP_X_GEN_AI_AUTH_SIGNATURE=sig, + ) + self.assertEqual(response.status_code, 404) + + @patch("api.gen_ai.views.get_config", return_value=PAYLOAD_SECRET) + def test_no_installation(self, mock_config): + # Create a valid owner but no installation + OwnerFactory(service="github", service_id="owner1", username="test1") + payload = b'{"external_owner_id":"owner1","repo_service_id":"101"}' + sig, data = sign_payload(payload) + response = self.client.post( + VIEW_URL, + data=data, + content_type="application/json", + HTTP_HTTP_X_GEN_AI_AUTH_SIGNATURE=sig, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data, {"is_valid": False}) + + @patch("api.gen_ai.views.get_config", return_value=PAYLOAD_SECRET) + def test_authorized(self, mock_config): + owner = OwnerFactory(service="github", service_id="owner2", username="test2") + GithubAppInstallation.objects.create( + installation_id=12345, + owner=owner, + name="ai-features", + repository_service_ids=["101", "202"], + ) + payload = b'{"external_owner_id":"owner2","repo_service_id":"101"}' + sig, data = sign_payload(payload) + response = self.client.post( + VIEW_URL, + data=data, + content_type="application/json", + HTTP_HTTP_X_GEN_AI_AUTH_SIGNATURE=sig, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {"is_valid": True}) + + @patch("api.gen_ai.views.get_config", return_value=PAYLOAD_SECRET) + def test_unauthorized(self, mock_config): + owner = OwnerFactory(service="github", service_id="owner3", username="test3") + GithubAppInstallation.objects.create( + installation_id=2, + owner=owner, + name="ai-features", + repository_service_ids=["303", "404"], + ) + payload = b'{"external_owner_id":"owner3","repo_service_id":"101"}' + sig, data = sign_payload(payload) + response = self.client.post( + VIEW_URL, + data=data, + content_type="application/json", + HTTP_HTTP_X_GEN_AI_AUTH_SIGNATURE=sig, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data, {"is_valid": False}) diff --git a/apps/codecov-api/api/gen_ai/urls.py b/apps/codecov-api/api/gen_ai/urls.py new file mode 100644 index 0000000000..1273948008 --- /dev/null +++ b/apps/codecov-api/api/gen_ai/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from .views import GenAIAuthView + +urlpatterns = [ + path("auth/", GenAIAuthView.as_view(), name="auth"), +] diff --git a/apps/codecov-api/api/gen_ai/views.py b/apps/codecov-api/api/gen_ai/views.py new file mode 100644 index 0000000000..e857c5a3a9 --- /dev/null +++ b/apps/codecov-api/api/gen_ai/views.py @@ -0,0 +1,61 @@ +import hmac +import logging +from hashlib import sha256 + +from rest_framework.exceptions import NotFound, PermissionDenied +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework.views import APIView + +from api.gen_ai.serializers import GenAIAuthSerializer +from codecov_auth.models import GithubAppInstallation, Owner +from graphql_api.types.owner.owner import AI_FEATURES_GH_APP_ID +from utils.config import get_config + +log = logging.getLogger(__name__) + + +class GenAIAuthView(APIView): + permission_classes = [AllowAny] + serializer_class = GenAIAuthSerializer + + def validate_signature(self, request): + key = get_config("gen_ai", "auth_secret") + if not key: + raise PermissionDenied("Invalid signature") + + if isinstance(key, str): + key = key.encode("utf-8") + expected_sig = request.headers.get("HTTP-X-GEN-AI-AUTH-SIGNATURE") + computed_sig = ( + "sha256=" + hmac.new(key, request.body, digestmod=sha256).hexdigest() + ) + if not hmac.compare_digest(computed_sig, expected_sig): + raise PermissionDenied("Invalid signature") + + def post(self, request, *args, **kwargs): + self.validate_signature(request) + external_owner_id = request.data.get("external_owner_id") + repo_service_id = request.data.get("repo_service_id") + if not external_owner_id or not repo_service_id: + return Response("Missing required parameters", status=400) + try: + owner = Owner.objects.get(service_id=external_owner_id) + except Owner.DoesNotExist: + raise NotFound("Owner not found") + + is_authorized = True + + app_install = GithubAppInstallation.objects.filter( + owner_id=owner.ownerid, app_id=AI_FEATURES_GH_APP_ID + ).first() + + if not app_install: + is_authorized = False + + else: + repo_ids = app_install.repository_service_ids + if repo_ids and repo_service_id not in repo_ids: + is_authorized = False + + return Response({"is_valid": is_authorized}) diff --git a/apps/codecov-api/api/internal/branch/__init__.py b/apps/codecov-api/api/internal/branch/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/internal/branch/serializers.py b/apps/codecov-api/api/internal/branch/serializers.py new file mode 100644 index 0000000000..5c0e086c71 --- /dev/null +++ b/apps/codecov-api/api/internal/branch/serializers.py @@ -0,0 +1,13 @@ +from rest_framework import serializers + +from core.models import Branch + + +class BranchSerializer(serializers.ModelSerializer): + name = serializers.CharField() + most_recent_commiter = serializers.CharField() + updatestamp = serializers.DateTimeField() + + class Meta: + model = Branch + fields = ("name", "most_recent_commiter", "updatestamp") diff --git a/apps/codecov-api/api/internal/branch/views.py b/apps/codecov-api/api/internal/branch/views.py new file mode 100644 index 0000000000..f3a984d009 --- /dev/null +++ b/apps/codecov-api/api/internal/branch/views.py @@ -0,0 +1,25 @@ +from django.db.models import OuterRef, Subquery +from rest_framework import mixins + +from api.shared.branch.mixins import BranchViewSetMixin +from core.models import Commit + +from .serializers import BranchSerializer + + +class BranchViewSet(BranchViewSetMixin, mixins.ListModelMixin): + serializer_class = BranchSerializer + + def get_queryset(self): + return ( + super() + .get_queryset() + .annotate( + most_recent_commiter=Subquery( + Commit.objects.filter( + commitid=OuterRef("head"), + repository_id=OuterRef("repository__repoid"), + ).values("author__username")[:1] + ) + ) + ) diff --git a/apps/codecov-api/api/internal/chart/__init__.py b/apps/codecov-api/api/internal/chart/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/internal/chart/filters.py b/apps/codecov-api/api/internal/chart/filters.py new file mode 100644 index 0000000000..409b15e9c0 --- /dev/null +++ b/apps/codecov-api/api/internal/chart/filters.py @@ -0,0 +1,66 @@ +from dateutil import parser +from django.db.models import F, Q + +from core.models import Repository +from utils.services import get_long_service_name + + +def apply_default_filters(queryset): + """ + By default we only want to include commits with meaningful coverage values when representing charts, + so exclude from consideration commits where CI failed, commits that are still pending, etc. + """ + return queryset.filter(state="complete", totals__isnull=False).filter( + Q(deleted__isnull=True) | Q(deleted=False) + ) + + +def apply_simple_filters(queryset, data, user): + """ + Apply any coverage chart filtering parameters that can be construed as a simple queryset.filter call. + """ + + service = get_long_service_name(data.get("service")) + + queryset = ( + queryset.filter( + repository__author__username=data.get( + "owner_username" + ) # filter by the organization in the request route + ) + .filter( + repository__author__service=service + # this avoids fetching the wrong user that uses the same username for different providers + ) + .filter( + # make sure we only return repositories that are either public or that the logged-in user has permission to view. + # this is important because if no "repository" param was provided then the permissions check will succeed, but we still + # want to make sure we return only all repositories the logged-in user has permissions to view. + repository__in=Repository.objects.viewable_repos(user) + ) + ) + + # Handle branch filtering + if data.get("branch"): + queryset = queryset.filter(branch=data.get("branch")) + else: + # if no branch param was provided, default to filtering commits based on the repository's default branch + queryset = queryset.filter(branch=F("repository__branch")) + + # Optional filters + if data.get("repositories"): + queryset = queryset.filter(repository__name__in=data.get("repositories", [])) + if data.get("start_date"): + # The __date cast function will case the datetime based timestamp on the commit to a date object that only + # contains the year, month and day. This allows us to filter through a daily granularity rather than + # a second granularity since this is the level of granularity we get from other parts of the API. + # We also have to convert the parameter to a datetime object for this to work, rather than pass a string. + queryset = queryset.filter( + timestamp__date__gte=parser.parse(data.get("start_date")) + ) + if data.get("end_date"): + # Same as above. + queryset = queryset.filter( + timestamp__date__lte=parser.parse(data.get("end_date")) + ) + return queryset diff --git a/apps/codecov-api/api/internal/chart/helpers.py b/apps/codecov-api/api/internal/chart/helpers.py new file mode 100644 index 0000000000..76de872e53 --- /dev/null +++ b/apps/codecov-api/api/internal/chart/helpers.py @@ -0,0 +1,408 @@ +from datetime import datetime + +from cerberus import Validator +from dateutil import parser +from django.db import connection +from django.db.models import Case, FloatField, Value, When +from django.db.models.fields.json import KeyTextTransform +from django.db.models.functions import Cast, Trunc +from django.utils import timezone +from django.utils.functional import cached_property +from rest_framework.exceptions import ValidationError + +from codecov_auth.models import Owner +from core.models import Repository + + +class ChartParamValidator(Validator): + # Custom validation rule to require "agg_value" and "agg_function" fields only when not grouping by commit. + # When grouping by commit, we return commits directly without applying any aggregation, so those fields aren't needed. + def _validate_check_aggregation_fields( + self, check_aggregation_fields, field, value + ): + agg_fields_present = self.document.get("agg_value") and self.document.get( + "agg_function" + ) + if check_aggregation_fields and value != "commit" and not agg_fields_present: + self._error( + field, + "Must provide a value for agg_value and agg_function fields if not grouping by commit", + ) + + +def validate_params(data): + """ + Explanation of parameters and how they impact the chart: + + - organization: username of the owner associated with the repositories/commits we're generating the chart for + - repositories: indicates only commits in the list of repositories should be included. Note that the RepositoryChartHandler doesn't + perform any aggregation if multiple repos are provided. + - branch: indicates only commits in this branch should be included + - start_date: indicates only commits after this date should be included + - end_date: indicates only commits before this date should be included + - grouping_unit: indicates how to group the commits. if this is 'commit' we'll just return ungrouped commits, if this is a unit of time + (day, month, year) we'll group the commits by that time unit when applying aggregation. + - agg_function: indicates how to aggregate the commits over . example: if this is 'max', we'll retrieve the commit within a time window with the + highest value of whatever 'agg_value' is. *(See below for more explanation on this field) + - agg_value: indicates which value we should perform aggregation/grouping on. example: if this is 'coverage', the aggregation function + (min, max, etc.) will be applied to commit coverage. *(See below for more explanation on this field.) + - coverage_timestamp_ordering: indicates in which order the coverage entries should be ordered by. Increasing will return the latest coverage + at the end of the coverage array while decreasing will return the latest coverage at the beginning of the array. + + Aggregation fields - when grouping by a unit of time, we need to know which commit to retrieve over that unit of time - e.g. the latest commit + in a given month, or the commit with the highest coverage, etc. The `agg_function` and `agg_value` parameters are used to determine this. + Examples: { "grouping_unit": "month", "agg_function": "min", "agg_value": "coverage" } --> get the commit with the highest coverage in a given month + Examples: { "grouping_unit": "week", "agg_function": "max", "agg_value": "timestmap" } --> get the most recent commit in a given week + """ + + params_schema = { + "owner_username": {"type": "string", "required": True}, + "service": {"type": "string", "required": False}, + "repositories": {"type": "list"}, + "branch": {"type": "string"}, + "start_date": {"type": "string"}, + "end_date": {"type": "string"}, + "grouping_unit": { + "type": "string", + "required": True, + "check_aggregation_fields": True, + "allowed": [ + "commit", + "hour", + "day", + "week", + "month", + "quarter", + "year", + ], # must be one of the values accepted by Django's Trunc function; for more info see https://docs.djangoproject.com/en/3.0/ref/models/database-functions/#trunc + }, + "agg_function": {"type": "string", "allowed": ["min", "max"]}, + "agg_value": {"type": "string", "allowed": ["timestamp", "coverage"]}, + "coverage_timestamp_ordering": { + "type": "string", + "allowed": ["increasing", "decreasing"], + }, + } + v = ChartParamValidator(params_schema) + if not v.validate(data): + raise ValidationError(v.errors) + + +def annotate_commits_with_totals(queryset): + """ + Extract values from a commit's "totals" field and annotate the commit directly with those values. + This is necessary when using Django aggregation functions, and otherwise is generally more convenient than wrangling with the totals JSON field. + See "CommitTotalsSerializer" for reference on what the values ("c", "N", etc) represent + """ + complexity = Cast(KeyTextTransform("C", "totals"), FloatField()) or 0 + complexity_total = ( + Cast(KeyTextTransform("N", "totals"), output_field=FloatField()) or 0 + ) + return queryset.annotate( + coverage=Cast(KeyTextTransform("c", "totals"), output_field=FloatField()), + complexity=complexity, + complexity_total=complexity_total, + complexity_ratio=Case( + When(complexity_total__gt=0, then=complexity / complexity_total), + default=Value(None), + ), + ) + + +def apply_grouping(queryset, data): + """ + On the provided queryset, group commits by the time unit provided. Within each time window and for each repository represented in the + given queryset, retrieve the appropriate commit based on the aggregation parameters (e.g. commit with "max" timestamp which will be the latest commit) + See the params_schema in validate_params for info on the acceptable values here. + """ + grouping_unit = data.get("grouping_unit") + agg_function = data.get("agg_function") + agg_value = data.get("agg_value") + commit_order = data.get("coverage_timestamp_ordering", "increasing") + + # Truncate the commit's timestamp so we can group it in the appropriate time unit. + # For example, if we're grouping by quarter, commits in Jan/Feb/March 2020 will all share the same truncated_date + queryset = queryset.annotate(truncated_date=Trunc("timestamp", grouping_unit)) + date_ordering = "" if commit_order == "increasing" else "-" + ordering = "" if agg_function == "min" else "-" + return queryset.order_by( + f"{date_ordering}truncated_date", "repository__name", f"{ordering}{agg_value}" + ).distinct( + "truncated_date", "repository__name" + ) # this will select the first row for a given date/repo combo, which since we've just ordered the commits + # should be the one with the min/max value we want to aggregate by + + +class ChartQueryRunner: + """ + Houses the SQL query that retrieves data for analytics chart, and + the associated parameter validation + transformation required for it. + """ + + def __init__(self, user, request_params): + self.user = user + self.request_params = request_params + self._validate_parameters() + + def _dictfetchall(self, cursor): + """ + Return all rows from a cursor as a dict + Copied from: https://docs.djangoproject.com/en/3.1/topics/db/sql/#executing-custom-sql-directly + """ + columns = [col[0] for col in cursor.description] + return [dict(zip(columns, row)) for row in cursor.fetchall()] + + @property + def start_date(self): + """ + Lower bound on the date-range of commit data returned by query. + Returns date of first commit made in any repo of 'repoids' is + used if not set. + """ + if "start_date" in self.request_params: + return datetime.date(parser.parse(self.request_params.get("start_date"))) + return self.first_complete_commit_date + + @property + def end_date(self): + """ + Returns 'end_date' to use in date spine. + """ + if "end_date" in self.request_params: + return datetime.date(parser.parse(self.request_params.get("end_date"))) + return datetime.date(timezone.now()) + + @property + def interval(self): + """ + Time interval between datapoints constructed in query. + Derived from 'grouping_unit' request parameter. + """ + if self.grouping_unit == "quarter": + return "3 months" + return f"1 {self.grouping_unit}" + + @property + def grouping_unit(self): + return self.request_params.get("grouping_unit") + + @property + def ordering(self): + """ + Data ordering is by ascending date, unless "decreasing" is + supplied as ordering param. + """ + if self.request_params.get("coverage_timestamp_ordering") == "decreasing": + return "DESC" + return "" + + @cached_property + def repoids(self): + """ + Returns a string of repoids of the repositories being queried. + """ + organization = Owner.objects.get( + service=self.request_params["service"], + username=self.request_params["owner_username"], + ) + + # Get list of relevant repoids + repos = Repository.objects.filter( + author=organization, + active=True, + ).viewable_repos(self.user) + + if self.request_params.get("repositories", []): + repos = repos.filter(name__in=self.request_params.get("repositories", [])) + + if repos: + # Get repoids into a format easily plugged into raw SQL + return ( + "(" + + ",".join(map(str, list(repos.values_list("repoid", flat=True)))) + + ")" + ) + + @cached_property + def first_complete_commit_date(self): + """ + Date of first commit made to any repo in 'self.repoids'. Used as initial + date for date_spine query. + """ + with connection.cursor() as cursor: + cursor.execute( + f""" + WITH relevant_repo_branches AS ( + SELECT + r.repoid, + r.branch + FROM repos r + WHERE r.repoid IN {self.repoids} + ) + + SELECT + DATE_TRUNC('{self.grouping_unit}', c.timestamp AT TIME ZONE 'UTC') as truncated_date + FROM commits c + INNER JOIN relevant_repo_branches r ON c.repoid = r.repoid AND c.branch = r.branch + WHERE c.state = 'complete' + ORDER BY c.timestamp ASC LIMIT 1; + """ + ) + date = self._dictfetchall(cursor) + + if date: + return datetime.date(date[0]["truncated_date"]) + + def _validate_parameters(self): + params_schema = { + "owner_username": {"type": "string", "required": True}, + "service": {"type": "string", "required": True}, + "repositories": {"type": "list", "required": False}, + "start_date": {"type": "string", "required": False}, + "end_date": {"type": "string", "required": False}, + "agg_function": {"type": "string", "required": False}, # Deprecated + "agg_value": {"type": "string", "required": False}, # Deprecated + "grouping_unit": { + "type": "string", + "required": True, + "allowed": [ + "day", + "week", + "month", + "quarter", + "year", + ], # Must be one acceptable by Postgres DATE_TRUNC + }, + "coverage_timestamp_ordering": { + "type": "string", + "allowed": ["increasing", "decreasing"], + "required": False, + }, + } + v = Validator(params_schema) + if not v.validate(self.request_params): + raise ValidationError(v.errors) + + def run_query(self): + # Edge cases -- no repos or no commits + if not self.repoids: + return [] + if not self.first_complete_commit_date: + return [] + + with connection.cursor() as cursor: + cursor.execute( + f""" + WITH date_series AS ( + SELECT + t::date AS "date" + FROM generate_series( + timestamp '{self.first_complete_commit_date}', + timestamp '{self.end_date}', + '{self.interval}' + ) t + ), graph_repos AS ( + SELECT + r.repoid, + r.name, + r.branch + FROM + repos r + WHERE r.repoid IN {self.repoids} + ), spine AS ( + SELECT + ds.date, + r.repoid + FROM date_series ds + CROSS JOIN graph_repos r + ), t_ranked_commits AS ( + SELECT + ROW_NUMBER() OVER ( + PARTITION BY c.repoid, DATE_TRUNC('{self.grouping_unit}', c.timestamp) + ORDER BY timestamp DESC NULLS LAST + ) AS commit_rank, + DATE_TRUNC('{self.grouping_unit}', c.timestamp) AS "truncated_date", + c.timestamp AS commit_timestamp, + c.totals, + r.repoid + FROM + commits c + INNER JOIN graph_repos r ON r.repoid = c.repoid + AND r.branch = c.branch + AND c.state = 'complete' + ), commits_spine AS ( + SELECT + s.date AS spine_date, + trc.truncated_date AS truncated_commit_date, + trc.commit_timestamp, + trc.totals AS totals, + s.repoid + FROM spine s + LEFT JOIN t_ranked_commits trc ON trc.truncated_date = s.date + AND trc.repoid = s.repoid + AND trc.commit_rank = 1 + ), grouped AS ( + SELECT + spine_date, + truncated_commit_date, + totals, + repoid, + SUM(CASE + WHEN totals IS NOT NULL THEN 1 END + ) OVER ( + PARTITION BY repoid + ORDER BY spine_date + ) AS grp_commit + FROM commits_spine + ), corrected AS ( + SELECT + spine_date, + FIRST_VALUE(totals) OVER ( + PARTITION BY repoid, grp_commit + ORDER BY spine_date + ) AS corrected_totals + FROM + grouped + ), parsed_totals AS ( + SELECT + spine_date, + (CASE + WHEN corrected_totals IS NOT NULL then (corrected_totals->>'h')::numeric + WHEN corrected_totals IS NULL then 0 END + ) as hits, + (CASE + WHEN corrected_totals IS NOT NULL then (corrected_totals->>'m')::numeric + WHEN corrected_totals IS NULL then 0 END + ) as misses, + (CASE + WHEN corrected_totals IS NOT NULL then (corrected_totals->>'p')::numeric + WHEN corrected_totals IS NULL then 0 END + ) as partials, + (CASE + WHEN corrected_totals IS NOT NULL then (corrected_totals->>'n')::numeric + WHEN corrected_totals IS NULL then 0 END + ) as lines + FROM + corrected + ), summed_totals AS ( + SELECT + spine_date::timestamp at time zone 'UTC' AS date, + SUM(hits) AS total_hits, + SUM(misses) AS total_misses, + SUM(partials) AS total_partials, + SUM(lines) AS total_lines, + ROUND((SUM(hits) + SUM(partials)) / SUM(lines) * 100, 2) AS coverage + FROM + parsed_totals + GROUP BY spine_date + ORDER BY spine_date {self.ordering} + ) + + SELECT + * + FROM summed_totals + WHERE date >= DATE_TRUNC('{self.grouping_unit}', timestamp '{self.start_date}'); + """ + ) + + return self._dictfetchall(cursor) diff --git a/apps/codecov-api/api/internal/chart/urls.py b/apps/codecov-api/api/internal/chart/urls.py new file mode 100644 index 0000000000..3091df9f29 --- /dev/null +++ b/apps/codecov-api/api/internal/chart/urls.py @@ -0,0 +1,16 @@ +from django.urls import re_path + +from .views import OrganizationChartHandler, RepositoryChartHandler + +urlpatterns = [ + re_path( + r"^(?P\w+)/(?P[\w|-]+)/coverage/repository\/?$", + RepositoryChartHandler.as_view(), + name="chart-coverage-repository", + ), + re_path( + r"^(?P\w+)/(?P[\w|-]+)/coverage/organization\/?$", + OrganizationChartHandler.as_view(), + name="chart-coverage-organization", + ), +] diff --git a/apps/codecov-api/api/internal/chart/views.py b/apps/codecov-api/api/internal/chart/views.py new file mode 100644 index 0000000000..152a9e3765 --- /dev/null +++ b/apps/codecov-api/api/internal/chart/views.py @@ -0,0 +1,199 @@ +from rest_framework.parsers import JSONParser +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from api.shared.mixins import RepositoriesMixin +from api.shared.permissions import ChartPermissions +from core.models import Commit +from utils import round_decimals_down + +from .filters import apply_default_filters, apply_simple_filters +from .helpers import ( + ChartQueryRunner, + annotate_commits_with_totals, + apply_grouping, + validate_params, +) + + +class RepositoryChartHandler(APIView, RepositoriesMixin): + """ + Returns data used to populate the repository-level coverage chart. See "validate_params" for documentation on accepted parameters. + Can either group and aggregate commits by a unit of time, or just return latest commits from the repo within the given time frame. + When aggregating by coverage, will also apply aggregation based on complexity ratio and return that. + + Responses take the following format (semantics of the response depend on whether we're grouping by time or not): + { + "coverage": [ + { + "date": "2019-06-01 00:00:00+00:00", + # grouping by time: NOT the commit timestamp, the date for this time window + # no grouping: when returning ungrouped commits: commit timestamp + "coverage": + # grouping by time: coverage from the commit retrieved (the one with min/max coverage) for this time unit + # no grouping: coverage from the commit + "commitid": + # grouping by time: id of the commit retrieved (the one with min/max coverage) for this time unit + # no grouping: id of the commit + }, + { + "date": "2019-07-01 00:00:00+00:00", + "coverage": + "commitid": + ... + }, + ... + ], + "complexity": [ + { + "date": "2019-07-01 00:00:00+00:00", + "complexity_ratio": + "commitid": + }, + { + "date": "2019-07-01 00:00:00+00:00", + ... + }, + ... + ] + } + """ + + permission_classes = [ChartPermissions] + parser_classes = [JSONParser] + + def post(self, request, *args, **kwargs): + request_params = {**self.request.data, **self.kwargs} + validate_params(request_params) + coverage_ordering = ( + "" + if request_params.get("coverage_timestamp_order", "increasing") + == "increasing" + else "-" + ) + + # We don't use the "report" field in this endpoint and it can be many MBs of JSON choosing not to + # fetch it for perf reasons + queryset = apply_simple_filters( + apply_default_filters(Commit.objects.defer("_report").all()), + request_params, + self.request.current_owner, + ) + + annotated_queryset = annotate_commits_with_totals(queryset) + + # if grouping_unit doesn't specify time, return all values + if self.request.data.get("grouping_unit") == "commit": + max_num_commits = 1000 + commits = annotated_queryset.order_by(f"{coverage_ordering}timestamp")[ + :max_num_commits + ] + coverage = [ + { + "date": commits[index].timestamp, + "coverage": commits[index].coverage, + "coverage_change": commits[index].coverage + - commits[max(index - 1, 0)].coverage, + "commitid": commits[index].commitid, + } + for index in range(len(commits)) + ] + + complexity = [ + { + "date": commit.timestamp, + "complexity_ratio": round_decimals_down( + commit.complexity_ratio * 100, 2 + ), + "commitid": commit.commitid, + } + for commit in commits + if commit.complexity_ratio is not None + ] + + else: + # Coverage + coverage_grouped_queryset = apply_grouping( + annotated_queryset, self.request.data + ) + + commits = coverage_grouped_queryset + coverage = [ + { + "date": commits[index].truncated_date, + "coverage": commits[index].coverage, + "coverage_change": commits[index].coverage + - commits[max(index - 1, 0)].coverage, + "commitid": commits[index].commitid, + } + for index in range(len(commits)) + ] + + # Complexity + complexity_params = self.request.data.copy() + complexity_params["agg_value"] = "complexity_ratio" + complexity_grouped_queryset = apply_grouping( + annotated_queryset, complexity_params + ) + complexity = [ + { + "date": commit.truncated_date, + "complexity_ratio": round_decimals_down( + commit.complexity_ratio * 100, 2 + ), + "commitid": commit.commitid, + } + for commit in complexity_grouped_queryset + if commit.complexity_ratio is not None + ] + + return Response(data={"coverage": coverage, "complexity": complexity}) + + +class OrganizationChartHandler(APIView): + """ + Returns array of datapoints retrieved by ChartQueryRunner. + Response data format is: + { + "coverage": [ + { + "date": "2019-06-01 00:00:00+00:00", + "coverage": , + "total_lines": , + "total_hits": , + "total_partials": , + }, + { + "date": "2019-07-01 00:00:00+00:00", + ... + }, + ... + ] + } + """ + + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + # this method is deprecated and will be removed + def post(self, request, *args, **kwargs): + query_runner = ChartQueryRunner( + user=request.current_owner, request_params={**kwargs, **request.data} + ) + return Response(data={"coverage": query_runner.run_query()}) + + def get(self, request, *args, **kwargs): + # Get request params as a dict. We take special care to preserve + # the 'repositories' entry as a list, since the 'MultiValuedDict.dict' + # method clobbers list values + request_params_dict = request.query_params.dict() + if "repositories" in request.query_params: + request_params_dict.update( + {"repositories": request.query_params.getlist("repositories")} + ) + + query_runner = ChartQueryRunner( + user=request.current_owner, request_params={**kwargs, **request_params_dict} + ) + return Response(data={"coverage": query_runner.run_query()}) diff --git a/apps/codecov-api/api/internal/commit/__init__.py b/apps/codecov-api/api/internal/commit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/internal/commit/serializers.py b/apps/codecov-api/api/internal/commit/serializers.py new file mode 100644 index 0000000000..58ace3903b --- /dev/null +++ b/apps/codecov-api/api/internal/commit/serializers.py @@ -0,0 +1,69 @@ +import logging +from typing import Dict, List + +import shared.reports.api_report_service as report_service +from rest_framework import serializers +from shared.reports.types import TOTALS_MAP + +from api.internal.owner.serializers import OwnerSerializer +from api.shared.commit.serializers import CommitTotalsSerializer +from core.models import Commit + +log = logging.getLogger(__name__) + + +class CommitSerializer(serializers.ModelSerializer): + author = OwnerSerializer() + totals = CommitTotalsSerializer() + + class Meta: + model = Commit + fields = ( + "commitid", + "message", + "timestamp", + "ci_passed", + "author", + "branch", + "totals", + "state", + ) + + +class CommitWithFileLevelReportSerializer(CommitSerializer): + report = serializers.SerializerMethodField() + + def get_report(self, commit: Commit) -> Dict[str, List[Dict] | Dict] | None: + report = report_service.build_report_from_commit(commit) + if report is None: + return None + + files = [] + for filename in report.files: + file_report = report.get(filename) + file_totals = CommitTotalsSerializer( + dict(zip(TOTALS_MAP, file_report.totals)) + ) + files.append( + { + "name": filename, + "totals": file_totals.data, + } + ) + + return { + "files": files, + "totals": CommitTotalsSerializer(commit.totals).data, + } + + class Meta: + model = Commit + fields = ( + "report", + "commitid", + "timestamp", + "ci_passed", + "repository", + "author", + "message", + ) diff --git a/apps/codecov-api/api/internal/commit/views.py b/apps/codecov-api/api/internal/commit/views.py new file mode 100644 index 0000000000..19cc2d2e3b --- /dev/null +++ b/apps/codecov-api/api/internal/commit/views.py @@ -0,0 +1,9 @@ +from rest_framework import mixins + +from api.shared.commit.mixins import CommitsViewSetMixin + +from .serializers import CommitSerializer + + +class CommitsViewSet(CommitsViewSetMixin, mixins.ListModelMixin): + serializer_class = CommitSerializer diff --git a/apps/codecov-api/api/internal/compare/__init__.py b/apps/codecov-api/api/internal/compare/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/internal/compare/serializers.py b/apps/codecov-api/api/internal/compare/serializers.py new file mode 100644 index 0000000000..6979be4568 --- /dev/null +++ b/apps/codecov-api/api/internal/compare/serializers.py @@ -0,0 +1,8 @@ +from api.internal.commit.serializers import CommitSerializer +from api.shared.compare.serializers import ( + ComparisonSerializer as BaseComparisonSerializer, +) + + +class ComparisonSerializer(BaseComparisonSerializer): + commit_uploads = CommitSerializer(many=True, source="upload_commits") diff --git a/apps/codecov-api/api/internal/compare/views.py b/apps/codecov-api/api/internal/compare/views.py new file mode 100644 index 0000000000..1a61a3a92f --- /dev/null +++ b/apps/codecov-api/api/internal/compare/views.py @@ -0,0 +1,12 @@ +from rest_framework import mixins + +from api.shared.compare.mixins import CompareViewSetMixin + +from .serializers import ComparisonSerializer + + +class CompareViewSet( + CompareViewSetMixin, + mixins.RetrieveModelMixin, +): + serializer_class = ComparisonSerializer diff --git a/apps/codecov-api/api/internal/constants.py b/apps/codecov-api/api/internal/constants.py new file mode 100644 index 0000000000..5944951dfb --- /dev/null +++ b/apps/codecov-api/api/internal/constants.py @@ -0,0 +1 @@ +INTERNAL_API_PREFIX = "internal/" diff --git a/apps/codecov-api/api/internal/coverage/__init__.py b/apps/codecov-api/api/internal/coverage/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/internal/coverage/views.py b/apps/codecov-api/api/internal/coverage/views.py new file mode 100644 index 0000000000..66998f9337 --- /dev/null +++ b/apps/codecov-api/api/internal/coverage/views.py @@ -0,0 +1,69 @@ +from typing import Any + +from django.http import HttpRequest +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.exceptions import NotFound +from rest_framework.response import Response + +import services.components as components_service +from api.shared.mixins import RepoPropertyMixin +from api.shared.permissions import RepositoryArtifactPermissions +from api.shared.report.serializers import TreeSerializer +from services.path import ReportPaths + + +class CoverageViewSet(viewsets.ViewSet, RepoPropertyMixin): + permission_classes = [RepositoryArtifactPermissions] + + def get_object(self) -> ReportPaths: + commit_sha = self.request.query_params.get("sha") + if not commit_sha: + branch_name = self.request.query_params.get("branch", self.repo.branch) + branch = self.repo.branches.filter(name=branch_name).first() + if branch is None: + raise NotFound( + f"The branch '{branch_name}' is not in our records. Please provide a valid branch name.", + 404, + ) + commit_sha = branch.head + + commit = self.repo.commits.filter(commitid=commit_sha).first() + if commit is None: + raise NotFound( + f"The commit {commit_sha} is not in our records. Please specify a valid commit.", + 404, + ) + + report = commit.full_report + if report is None: + raise NotFound(f"Coverage report for {commit_sha} not found") + + components = self.request.query_params.getlist("components") + component_paths = [] + if components: + all_components = components_service.commit_components(commit, self.owner) + filtered_components = components_service.filter_components_by_name_or_id( + all_components, components + ) + + if not filtered_components: + raise NotFound( + f"Coverage report for components {filtered_components} not found" + ) + + for component in filtered_components: + component_paths.extend(component.paths) + flags = self.request.query_params.getlist("flags") + + paths = ReportPaths( + report=report, filter_flags=flags, filter_paths=component_paths + ) + + return paths + + @action(detail=False, methods=["get"], url_path="tree") + def tree(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Response: + paths = self.get_object() + serializer = TreeSerializer(paths.single_directory(), many=True) + return Response(serializer.data) diff --git a/apps/codecov-api/api/internal/enterprise_urls.py b/apps/codecov-api/api/internal/enterprise_urls.py new file mode 100644 index 0000000000..cd8448b546 --- /dev/null +++ b/apps/codecov-api/api/internal/enterprise_urls.py @@ -0,0 +1,12 @@ +from django.urls import include, path + +from api.internal.self_hosted.views import UserViewSet +from utils.routers import OptionalTrailingSlashRouter + +self_hosted_router = OptionalTrailingSlashRouter() +self_hosted_router.register(r"users", UserViewSet, basename="selfhosted-users") + +urlpatterns = [ + path("license/", include("api.internal.license.urls")), + path("", include(self_hosted_router.urls)), +] diff --git a/apps/codecov-api/api/internal/feature/helpers.py b/apps/codecov-api/api/internal/feature/helpers.py new file mode 100644 index 0000000000..1a8700d98c --- /dev/null +++ b/apps/codecov-api/api/internal/feature/helpers.py @@ -0,0 +1,23 @@ +from shared.django_apps.rollouts.models import FeatureFlag, RolloutUniverse + +FEATURES_CACHE_REDIS_KEY = "features_endpoint_cache" + + +def get_flag_cache_redis_key(flag_name): + return FEATURES_CACHE_REDIS_KEY + ":" + flag_name + + +def get_identifier(feature_flag: FeatureFlag, identifier_data): + """ + Returns the appropriate identifier string based on the rollout identifier type. + """ + if feature_flag.rollout_universe == RolloutUniverse.OWNER_ID: + return identifier_data["user_id"] + elif feature_flag.rollout_universe == RolloutUniverse.REPO_ID: + return identifier_data["repo_id"] + elif feature_flag.rollout_universe == RolloutUniverse.EMAIL: + return identifier_data["email"] + elif feature_flag.rollout_universe == RolloutUniverse.ORG_ID: + return identifier_data["org_id"] + else: + raise ValueError("Unknown RolloutUniverse type") diff --git a/apps/codecov-api/api/internal/feature/serializers.py b/apps/codecov-api/api/internal/feature/serializers.py new file mode 100644 index 0000000000..86426aa95f --- /dev/null +++ b/apps/codecov-api/api/internal/feature/serializers.py @@ -0,0 +1,15 @@ +from rest_framework import serializers + + +class FeatureIdentifierDataSerializer(serializers.Serializer): + email = serializers.CharField(max_length=200, allow_blank=True) + user_id = serializers.IntegerField() + repo_id = serializers.IntegerField() + org_id = serializers.IntegerField() + + +class FeatureRequestSerializer(serializers.Serializer): + feature_flags = serializers.ListField( + child=serializers.CharField(max_length=200), allow_empty=True + ) + identifier_data = FeatureIdentifierDataSerializer() diff --git a/apps/codecov-api/api/internal/feature/views.py b/apps/codecov-api/api/internal/feature/views.py new file mode 100644 index 0000000000..d02860e6b2 --- /dev/null +++ b/apps/codecov-api/api/internal/feature/views.py @@ -0,0 +1,104 @@ +import logging +import pickle +from typing import Any, Dict, List + +from rest_framework import status +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView +from shared.django_apps.rollouts.models import FeatureFlag +from shared.helpers.redis import get_redis_connection +from shared.rollouts import Feature + +from api.internal.feature.helpers import get_flag_cache_redis_key, get_identifier +from utils.config import get_config + +from .serializers import FeatureRequestSerializer + +log = logging.getLogger(__name__) + + +class FeaturesView(APIView): + skip_feature_cache = get_config("setup", "skip_feature_cache", default=False) + timeout = 300 + + def __init__(self, *args: Any, **kwargs: Any) -> None: + self.redis = get_redis_connection() + super().__init__(*args, **kwargs) + + def get_many_from_redis(self, keys: List) -> Dict[str, Any]: + ret = self.redis.mget(keys) + return {k: pickle.loads(v) for k, v in zip(keys, ret) if v is not None} + + def set_many_to_redis(self, data: Dict[str, Any]) -> None: + pipeline = self.redis.pipeline() + pipeline.mset({k: pickle.dumps(v) for k, v in data.items()}) + + # Setting timeout for each key as redis does not support timeout + # with mset(). + for key in data: + pipeline.expire(key, self.timeout) + pipeline.execute() + + def post(self, request: Request) -> Response: + serializer = FeatureRequestSerializer(data=request.data) + if serializer.is_valid(): + flag_evaluations = {} + identifier_data = serializer.validated_data["identifier_data"] + feature_flag_names = serializer.validated_data["feature_flags"] + + feature_flag_cache_keys = [ + get_flag_cache_redis_key(flag_name) for flag_name in feature_flag_names + ] + cache_misses = [] + + if not self.skip_feature_cache: + # fetch flags from cache + cached_flags = self.get_many_from_redis(feature_flag_cache_keys) + + for ind in range(len(feature_flag_cache_keys)): + cache_key = feature_flag_cache_keys[ind] + flag_name = feature_flag_names[ind] + + # if flag is in cache, make the evaluation. Otherwise, we'll + # fetch the flag from DB later + if cache_key in cached_flags: + feature_flag = cached_flags[cache_key] + identifier = get_identifier(feature_flag, identifier_data) + + flag_evaluations[flag_name] = Feature( + flag_name, feature_flag, list(feature_flag.variants.all()) + ).check_value_no_fetch(identifier=identifier) + else: + cache_misses.append(flag_name) + else: + cache_misses = feature_flag_names + log.warning( + "skip_feature_cache for Feature should only be turned on in development environments, and should not be used in production" + ) + + flags_to_add_to_cache = {} + + # fetch flags not in cache + missed_feature_flags = FeatureFlag.objects.filter( + name__in=cache_misses + ).prefetch_related("variants") # include the feature flag variants aswell + + # evaluate the remaining flags + for feature_flag in missed_feature_flags: + identifier = get_identifier(feature_flag, identifier_data) + + flag_evaluations[feature_flag.name] = Feature( + feature_flag.name, feature_flag, list(feature_flag.variants.all()) + ).check_value_no_fetch(identifier=identifier) + flags_to_add_to_cache[get_flag_cache_redis_key(feature_flag.name)] = ( + feature_flag + ) + + # add the new flags to cache + if len(flags_to_add_to_cache) >= 1: + self.set_many_to_redis(flags_to_add_to_cache) + + return Response(flag_evaluations, status=status.HTTP_200_OK) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/apps/codecov-api/api/internal/license/__init__.py b/apps/codecov-api/api/internal/license/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/internal/license/serializers.py b/apps/codecov-api/api/internal/license/serializers.py new file mode 100644 index 0000000000..cf36d1846c --- /dev/null +++ b/apps/codecov-api/api/internal/license/serializers.py @@ -0,0 +1,10 @@ +from rest_framework import serializers + + +class LicenseSerializer(serializers.Serializer): + trial = serializers.BooleanField(source="is_trial") + url = serializers.CharField() + users = serializers.IntegerField(source="number_allowed_users") + repos = serializers.IntegerField(source="number_allowed_repos") + expires_at = serializers.DateTimeField(source="expires") + pr_billing = serializers.BooleanField(source="is_pr_billing") diff --git a/apps/codecov-api/api/internal/license/urls.py b/apps/codecov-api/api/internal/license/urls.py new file mode 100644 index 0000000000..12b8656e0a --- /dev/null +++ b/apps/codecov-api/api/internal/license/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from .views import LicenseView + +urlpatterns = [ + path("", LicenseView.as_view(), name="license"), +] diff --git a/apps/codecov-api/api/internal/license/views.py b/apps/codecov-api/api/internal/license/views.py new file mode 100644 index 0000000000..4a04514889 --- /dev/null +++ b/apps/codecov-api/api/internal/license/views.py @@ -0,0 +1,16 @@ +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView +from shared.license import get_current_license + +from .serializers import LicenseSerializer + + +class LicenseView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, format=None): + license = get_current_license() + serializer = LicenseSerializer(license) + + return Response(serializer.data) diff --git a/apps/codecov-api/api/internal/owner/__init__.py b/apps/codecov-api/api/internal/owner/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/internal/owner/serializers.py b/apps/codecov-api/api/internal/owner/serializers.py new file mode 100644 index 0000000000..32bf23d175 --- /dev/null +++ b/apps/codecov-api/api/internal/owner/serializers.py @@ -0,0 +1,407 @@ +import logging +from datetime import datetime +from typing import Any, Dict + +from dateutil.relativedelta import relativedelta +from rest_framework import serializers +from rest_framework.exceptions import PermissionDenied +from shared.plan.constants import ( + TEAM_PLAN_MAX_USERS, + TierName, +) +from shared.plan.service import PlanService + +from codecov_auth.models import Owner, Plan +from services.billing import BillingService +from services.sentry import send_user_webhook as send_sentry_webhook + +log = logging.getLogger(__name__) + + +class OwnerSerializer(serializers.ModelSerializer): + stats = serializers.SerializerMethodField() + + class Meta: + model = Owner + fields = ( + "avatar_url", + "service", + "username", + "name", + "stats", + "ownerid", + "integration_id", + ) + + read_only_fields = fields + + def get_stats(self, obj: Owner) -> str | None: + if obj.cache and "stats" in obj.cache: + return obj.cache["stats"] + + +class StripeLineItemSerializer(serializers.Serializer): + description = serializers.CharField() + amount = serializers.FloatField() + currency = serializers.CharField() + period = serializers.JSONField() + plan_name = serializers.SerializerMethodField() + quantity = serializers.IntegerField() + + def get_plan_name(self, line_item: Dict[str, str]) -> str | None: + plan = line_item.get("plan") + if plan: + return plan.get("name") + + +class StripeInvoiceSerializer(serializers.Serializer): + id = serializers.CharField() + number = serializers.CharField() + status = serializers.CharField() + created = serializers.IntegerField() + period_start = serializers.IntegerField() + period_end = serializers.IntegerField() + due_date = serializers.IntegerField() + customer_name = serializers.CharField() + customer_address = serializers.CharField() + currency = serializers.CharField() + amount_paid = serializers.FloatField() + amount_due = serializers.FloatField() + amount_remaining = serializers.FloatField() + total = serializers.FloatField() + subtotal = serializers.FloatField() + invoice_pdf = serializers.CharField() + line_items = StripeLineItemSerializer(many=True, source="lines.data") + footer = serializers.CharField() + customer_email = serializers.CharField() + customer_shipping = serializers.CharField() + + +class StripeDiscountSerializer(serializers.Serializer): + name = serializers.CharField(source="coupon.name") + percent_off = serializers.FloatField(source="coupon.percent_off") + duration_in_months = serializers.IntegerField(source="coupon.duration_in_months") + expires = serializers.SerializerMethodField() + + def get_expires(self, customer: Dict[str, Dict]) -> int | None: + coupon = customer.get("coupon") + if coupon: + months = coupon.get("duration_in_months") + created = coupon.get("created") + if months and created: + expires = datetime.fromtimestamp(created) + relativedelta(months=months) + return int(expires.timestamp()) + + +class StripeCustomerSerializer(serializers.Serializer): + id = serializers.CharField() + discount = StripeDiscountSerializer() + email = serializers.CharField() + + +class StripeCardSerializer(serializers.Serializer): + brand = serializers.CharField() + exp_month = serializers.IntegerField() + exp_year = serializers.IntegerField() + last4 = serializers.CharField() + + +class StripeUSBankAccountSerializer(serializers.Serializer): + bank_name = serializers.CharField() + last4 = serializers.CharField() + + +class StripePaymentMethodSerializer(serializers.Serializer): + card = StripeCardSerializer(read_only=True) + us_bank_account = StripeUSBankAccountSerializer(read_only=True) + billing_details = serializers.JSONField(read_only=True) + + +class PlanSerializer(serializers.Serializer): + marketing_name = serializers.CharField(read_only=True) + value = serializers.CharField() + billing_rate = serializers.CharField(read_only=True) + base_unit_price = serializers.IntegerField(read_only=True) + benefits = serializers.JSONField(read_only=True) + quantity = serializers.IntegerField(required=False) + + def validate_value(self, value: str) -> str: + current_org = self.context["view"].owner + current_owner = self.context["request"].current_owner + + plan_service = PlanService(current_org=current_org) + plan_values = [ + plan.name for plan in plan_service.available_plans(current_owner) + ] + if value not in plan_values: + raise serializers.ValidationError( + f"Invalid value for plan: {value}; must be one of {plan_values}" + ) + return value + + def validate(self, plan: Dict[str, Any]) -> Dict[str, Any]: + current_org = self.context["view"].owner + if current_org.account: + raise serializers.ValidationError( + detail="You cannot update your plan manually, for help or changes to plan, connect with sales@codecov.io" + ) + + active_plans = Plan.objects.select_related("tier").filter( + paid_plan=True, is_active=True + ) + + active_plan_names = set(active_plans.values_list("name", flat=True)) + team_tier_plans = active_plans.filter( + tier__tier_name=TierName.TEAM.value + ).values_list("name", flat=True) + + # Validate quantity here because we need access to whole plan object + if plan["value"] in active_plan_names: + if "quantity" not in plan: + raise serializers.ValidationError( + "Field 'quantity' required for updating to paid plans" + ) + if plan["quantity"] <= 1: + raise serializers.ValidationError( + "Quantity for paid plan must be greater than 1" + ) + + plan_service = PlanService(current_org=current_org) + is_org_trialing = plan_service.is_org_trialing + + if ( + plan["quantity"] < current_org.activated_user_count + and not is_org_trialing + ): + raise serializers.ValidationError( + "Quantity cannot be lower than currently activated user count" + ) + if ( + plan["quantity"] == current_org.plan_user_count + and plan["value"] == current_org.plan + and not is_org_trialing + ): + raise serializers.ValidationError( + "Quantity or plan for paid plan must be different from the existing one" + ) + if ( + plan["value"] in team_tier_plans + and plan["quantity"] > TEAM_PLAN_MAX_USERS + ): + raise serializers.ValidationError( + f"Quantity for Team plan cannot exceed {TEAM_PLAN_MAX_USERS}" + ) + return plan + + +class SubscriptionDetailSerializer(serializers.Serializer): + latest_invoice = StripeInvoiceSerializer() + default_payment_method = StripePaymentMethodSerializer( + source="customer.invoice_settings.default_payment_method" + ) + cancel_at_period_end = serializers.BooleanField() + current_period_end = serializers.IntegerField() + customer = StripeCustomerSerializer() + collection_method = serializers.CharField() + tax_ids = serializers.ListField( + source="customer.tax_ids.data", read_only=True, allow_null=True + ) + trial_end = serializers.IntegerField() + + +class StripeScheduledPhaseSerializer(serializers.Serializer): + start_date = serializers.IntegerField() + plan = serializers.SerializerMethodField() + quantity = serializers.SerializerMethodField() + + def get_plan(self, phase: Dict[str, Any]) -> str: + plan_id = phase["items"][0]["plan"] + marketing_plan_name = Plan.objects.get(stripe_id=plan_id).marketing_name + return marketing_plan_name + + def get_quantity(self, phase: Dict[str, Any]) -> int: + return phase["items"][0]["quantity"] + + +class ScheduleDetailSerializer(serializers.Serializer): + id = serializers.CharField() + scheduled_phase = serializers.SerializerMethodField() + + def get_scheduled_phase(self, schedule: Dict[str, Any]) -> Dict[str, Any] | None: + if len(schedule["phases"]) > 1: + return StripeScheduledPhaseSerializer(schedule["phases"][-1]).data + else: + # This error represents the phases object not having 2 phases; we are interested in the 2nd entry within phases + # since it represents the scheduled phase. + # It should not be possible for a schedule to have one phase, but we have seen certain cases where this is true + # after manual intervention on a subscription. + log.error( + "Expecting schedule object to have 2 phases, returning None", + extra=dict( + ownerid=schedule.metadata.get("obo_organization"), + requesting_user_id=schedule.metadata.get("obo"), + phases=schedule.get("phases", "no phases"), + ), + ) + return None + + +class RootOrganizationSerializer(serializers.Serializer): + """ + Minimalist serializer to expose the root organization of a sub group + so we can expose the minimal data required for the UI while hiding data + that might only be for admin (invoice, billing data, etc) + """ + + username = serializers.CharField() + plan = PlanSerializer(source="pretty_plan") + + +class AccountDetailsSerializer(serializers.ModelSerializer): + plan = PlanSerializer(source="pretty_plan") + checkout_session_id = serializers.SerializerMethodField() + subscription_detail = serializers.SerializerMethodField() + root_organization = RootOrganizationSerializer() + schedule_detail = serializers.SerializerMethodField() + apply_cancellation_discount = serializers.BooleanField(write_only=True) + activated_student_count = serializers.SerializerMethodField() + activated_user_count = serializers.SerializerMethodField() + delinquent = serializers.SerializerMethodField() + uses_invoice = serializers.SerializerMethodField() + + class Meta: + model = Owner + + read_only_fields = ("integration_id",) + + fields = read_only_fields + ( + "activated_student_count", + "activated_user_count", + "apply_cancellation_discount", + "checkout_session_id", + "delinquent", + "email", + "inactive_user_count", + "name", + "nb_active_private_repos", + "plan_auto_activate", + "plan_provider", + "plan", + "repo_total_credits", + "root_organization", + "schedule_detail", + "student_count", + "subscription_detail", + "uses_invoice", + ) + + def _get_billing(self) -> BillingService: + current_owner = self.context["request"].current_owner + return BillingService(requesting_user=current_owner) + + def get_subscription_detail(self, owner: Owner) -> Dict[str, Any] | None: + subscription_detail = self._get_billing().get_subscription(owner) + if subscription_detail: + return SubscriptionDetailSerializer(subscription_detail).data + + def get_schedule_detail(self, owner: Owner) -> Dict[str, Any] | None: + schedule_detail = self._get_billing().get_schedule(owner) + if schedule_detail: + return ScheduleDetailSerializer(schedule_detail).data + + def get_checkout_session_id(self, _: Any) -> str: + return self.context.get("checkout_session_id") + + def get_activated_student_count(self, owner: Owner) -> int: + if owner.account: + return owner.account.activated_student_count + return owner.activated_student_count + + def get_activated_user_count(self, owner: Owner) -> int: + if owner.account: + return owner.account.activated_user_count + return owner.activated_user_count + + def get_delinquent(self, owner: Owner) -> bool: + if owner.account: + return owner.account.is_delinquent + return owner.delinquent + + def get_uses_invoice(self, owner: Owner) -> bool: + if owner.account: + return owner.account.invoice_billing.filter(is_active=True).exists() + return owner.uses_invoice + + def update(self, instance: Owner, validated_data: Dict[str, Any]) -> object: + if "pretty_plan" in validated_data: + desired_plan = validated_data.pop("pretty_plan") + checkout_session_id_or_none = self._get_billing().update_plan( + instance, desired_plan + ) + + plan = ( + Plan.objects.select_related("tier") + .filter(name=desired_plan["value"]) + .first() + ) + + if plan and plan.tier.tier_name == TierName.SENTRY.value: + current_owner = self.context["view"].request.current_owner + send_sentry_webhook(current_owner, instance) + + if checkout_session_id_or_none is not None: + self.context["checkout_session_id"] = checkout_session_id_or_none + + if validated_data.get("apply_cancellation_discount") is True: + self._get_billing().apply_cancellation_discount(instance) + + super().update(instance, validated_data) + return self.context["view"].get_object() + + +class UserSerializer(serializers.ModelSerializer): + activated = serializers.BooleanField() + is_admin = serializers.BooleanField() + last_pull_timestamp = serializers.SerializerMethodField() + + class Meta: + model = Owner + fields = ( + "activated", + "is_admin", + "username", + "email", + "ownerid", + "student", + "name", + "last_pull_timestamp", + ) + + def update(self, instance: Owner, validated_data: Dict[str, Any]) -> object: + owner = self.context["view"].owner + + if "activated" in validated_data: + if validated_data["activated"] is True and owner.can_activate_user( + instance + ): + owner.activate_user(instance) + elif validated_data["activated"] is False: + owner.deactivate_user(instance) + else: + raise PermissionDenied( + f"Cannot activate user {instance.username} -- not enough seats left." + ) + + if "is_admin" in validated_data: + if validated_data["is_admin"]: + owner.add_admin(instance) + else: + owner.remove_admin(instance) + + # Re-fetch from DB to set activated and admin fields + return self.context["view"].get_object() + + def get_last_pull_timestamp(self, obj: Owner) -> str | None: + # this field comes from an annotation that may not always be applied to the queryset + if hasattr(obj, "last_pull_timestamp"): + return obj.last_pull_timestamp diff --git a/apps/codecov-api/api/internal/owner/views.py b/apps/codecov-api/api/internal/owner/views.py new file mode 100644 index 0000000000..a367f6882f --- /dev/null +++ b/apps/codecov-api/api/internal/owner/views.py @@ -0,0 +1,198 @@ +import logging + +from django.db.models import F +from django_filters import rest_framework as django_filters +from rest_framework import filters, mixins, status, viewsets +from rest_framework.decorators import action +from rest_framework.exceptions import PermissionDenied, ValidationError +from rest_framework.response import Response +from shared.django_apps.codecov_auth.models import Owner +from shared.plan.constants import DEFAULT_FREE_PLAN + +from api.shared.mixins import OwnerPropertyMixin +from api.shared.owner.mixins import OwnerViewSetMixin, UserViewSetMixin +from api.shared.permissions import MemberOfOrgPermissions +from billing.helpers import on_enterprise_plan +from services.billing import BillingService +from services.decorators import stripe_safe +from services.task import TaskService + +from .serializers import ( + AccountDetailsSerializer, + OwnerSerializer, + UserSerializer, +) + +log = logging.getLogger(__name__) + + +class OwnerViewSet(OwnerViewSetMixin, mixins.RetrieveModelMixin): + serializer_class = OwnerSerializer + + +class AccountDetailsViewSet( + viewsets.GenericViewSet, + mixins.UpdateModelMixin, + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + OwnerPropertyMixin, +): + serializer_class = AccountDetailsSerializer + permission_classes = [MemberOfOrgPermissions] + + @stripe_safe + def retrieve(self, *args, **kwargs): + res = super().retrieve(*args, **kwargs) + return res + + @stripe_safe + def update(self, request, *args, **kwargs): + # Temporary fix. Remove once Gazebo uses the new free plan + plan_value = request.data.get("plan", {}).get("value") + if plan_value == "users-basic": + request.data["plan"]["value"] = DEFAULT_FREE_PLAN + + return super().update(request, *args, **kwargs) + + def destroy(self, request, *args, **kwargs): + if self.owner.ownerid != request.current_owner.ownerid: + raise PermissionDenied("You can only delete your own account") + + TaskService().delete_owner(self.owner.ownerid) + return Response(status=status.HTTP_204_NO_CONTENT) + + def get_object(self): + if self.owner.account: + # gets the related account and invoice_billing objects from db in 1 query + # otherwise, each reference to owner.account would be an additional query + self.owner = ( + Owner.objects.filter(pk=self.owner.ownerid) + .select_related("account__invoice_billing") + .first() + ) + return self.owner + + @action(detail=False, methods=["patch"]) + @stripe_safe + def update_payment(self, request, *args, **kwargs): + payment_method = request.data.get("payment_method") + if not payment_method: + raise ValidationError(detail="No payment_method sent") + owner = self.get_object() + billing = BillingService(requesting_user=request.current_owner) + billing.update_payment_method(owner, payment_method) + return Response(self.get_serializer(owner).data) + + @action(detail=False, methods=["patch"]) + @stripe_safe + def update_email(self, request, *args, **kwargs): + """ + Update the email address associated with the owner's billing account. + + Args: + request: The HTTP request object containing: + - new_email: The new email address to update to + - apply_to_default_payment_method: Boolean flag to update email on the default payment method (default False) + + Returns: + Response with serialized owner data + + Raises: + ValidationError: If no new_email is provided in the request + """ + new_email = request.data.get("new_email") + if not new_email: + raise ValidationError(detail="No new_email sent") + owner = self.get_object() + billing = BillingService(requesting_user=request.current_owner) + apply_to_default_payment_method = request.data.get( + "apply_to_default_payment_method", False + ) + billing.update_email_address( + owner, + new_email, + apply_to_default_payment_method=apply_to_default_payment_method, + ) + return Response(self.get_serializer(owner).data) + + @action(detail=False, methods=["patch"]) + @stripe_safe + def update_billing_address(self, request, *args, **kwargs): + name = request.data.get("name") + if not name: + raise ValidationError(detail="No name sent") + billing_address = request.data.get("billing_address") + if not billing_address: + raise ValidationError(detail="No billing_address sent") + owner = self.get_object() + + formatted_address = { + "line1": billing_address["line_1"], + "line2": billing_address["line_2"], + "city": billing_address["city"], + "state": billing_address["state"], + "postal_code": billing_address["postal_code"], + "country": billing_address["country"], + } + + billing = BillingService(requesting_user=request.current_owner) + billing.update_billing_address(owner, name, billing_address=formatted_address) + return Response(self.get_serializer(owner).data) + + +class UsersOrderingFilter(filters.OrderingFilter): + def get_valid_fields(self, queryset, view, context=None): + fields = super().get_valid_fields(queryset, view, context=context or {}) + + if "last_pull_timestamp" not in queryset.query.annotations: + # queryset not always annotated with `last_pull_timestamp` + fields = [ + (name, verbose_name) + for (name, verbose_name) in fields + if name != "last_pull_timestamp" + ] + + return fields + + def filter_queryset(self, request, queryset, view): + ordering = self.get_ordering(request, queryset, view) + + if ordering: + ordering = [self._order_expression(order) for order in ordering] + ordering += ["ownerid"] # secondary sort column makes this deterministic + return queryset.order_by(*ordering) + + return queryset + + def _order_expression(self, order): + """ + Special cases for `last_pull_timestamp`: + - nulls first when ascending + - nulls last when descending + """ + if order == "last_pull_timestamp": + return F("last_pull_timestamp").asc(nulls_first=True) + elif order == "-last_pull_timestamp": + return F("last_pull_timestamp").desc(nulls_last=True) + else: + return order + + +class UserViewSet( + UserViewSetMixin, + mixins.ListModelMixin, + mixins.UpdateModelMixin, +): + serializer_class = UserSerializer + filter_backends = ( + django_filters.DjangoFilterBackend, + UsersOrderingFilter, + filters.SearchFilter, + ) + + def get_queryset(self): + qs = super().get_queryset() + if on_enterprise_plan(self.owner): + # pull ordering only available for enterprise + qs = qs.annotate_last_pull_timestamp() + return qs diff --git a/apps/codecov-api/api/internal/pull/__init__.py b/apps/codecov-api/api/internal/pull/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/internal/pull/serializers.py b/apps/codecov-api/api/internal/pull/serializers.py new file mode 100644 index 0000000000..55aff3f68a --- /dev/null +++ b/apps/codecov-api/api/internal/pull/serializers.py @@ -0,0 +1,33 @@ +from rest_framework import serializers + +from api.internal.owner.serializers import OwnerSerializer +from api.shared.commit.serializers import CommitTotalsSerializer +from core.models import Pull + + +class PullSerializer(serializers.ModelSerializer): + most_recent_commiter = serializers.CharField() + base_totals = CommitTotalsSerializer() + head_totals = CommitTotalsSerializer() + ci_passed = serializers.BooleanField() + + class Meta: + model = Pull + fields = ( + "pullid", + "title", + "most_recent_commiter", + "base_totals", + "head_totals", + "updatestamp", + "state", + "ci_passed", + ) + + +class PullDetailSerializer(PullSerializer): + author = OwnerSerializer() + + class Meta: + model = Pull + fields = PullSerializer.Meta.fields + ("author",) diff --git a/apps/codecov-api/api/internal/pull/views.py b/apps/codecov-api/api/internal/pull/views.py new file mode 100644 index 0000000000..c3520ee546 --- /dev/null +++ b/apps/codecov-api/api/internal/pull/views.py @@ -0,0 +1,32 @@ +from django.db.models import OuterRef, Subquery +from rest_framework import mixins + +from api.shared.pull.mixins import PullViewSetMixin +from core.models import Commit + +from .serializers import PullDetailSerializer, PullSerializer + + +class PullViewSet( + PullViewSetMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, +): + def get_serializer_class(self): + if self.action == "retrieve": + return PullDetailSerializer + elif self.action == "list": + return PullSerializer + + def get_queryset(self): + return ( + super() + .get_queryset() + .annotate( + most_recent_commiter=Subquery( + Commit.objects.filter( + commitid=OuterRef("head"), repository=OuterRef("repository") + ).values("author__username")[:1] + ), + ) + ) diff --git a/apps/codecov-api/api/internal/repo/__init__.py b/apps/codecov-api/api/internal/repo/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/internal/repo/filter.py b/apps/codecov-api/api/internal/repo/filter.py new file mode 100644 index 0000000000..6d844d90c7 --- /dev/null +++ b/apps/codecov-api/api/internal/repo/filter.py @@ -0,0 +1,68 @@ +from django.db.models import FloatField +from django.db.models.fields.json import KeyTextTransform +from django.db.models.functions import Cast +from rest_framework import filters + + +class RepositoryOrderingFilter(filters.OrderingFilter): + """ + Ordering filter that lazy-loads data into queryset + when filtering on coverage metrics. This delays expensive queries + so that they only slow down requests that require them. + """ + + def _order_by_totals_field(self, ordering_field, queryset): + if ordering_field in ["coverage", "-coverage"]: + annotation_args = dict( + coverage=Cast( + KeyTextTransform("c", "latest_commit_totals"), + output_field=FloatField(), + ) + ) + elif ordering_field in ["lines", "-lines"]: + annotation_args = dict( + lines=Cast( + KeyTextTransform("n", "latest_commit_totals"), + output_field=FloatField(), + ) + ) + elif ordering_field in ["hits", "-hits"]: + annotation_args = dict( + hits=Cast( + KeyTextTransform("h", "latest_commit_totals"), + output_field=FloatField(), + ) + ) + elif ordering_field in ["partials", "-partials"]: + annotation_args = dict( + partials=Cast( + KeyTextTransform("p", "latest_commit_totals"), + output_field=FloatField(), + ) + ) + elif ordering_field in ["misses", "-misses"]: + annotation_args = dict( + misses=Cast( + KeyTextTransform("m", "latest_commit_totals"), + output_field=FloatField(), + ) + ) + elif ordering_field in ["complexity", "-complexity"]: + annotation_args = dict( + complexity=Cast( + KeyTextTransform("C", "latest_commit_totals"), + output_field=FloatField(), + ) + ) + else: + return queryset.order_by(ordering_field) + + return queryset.annotate(**annotation_args).order_by(ordering_field) + + def filter_queryset(self, request, queryset, view): + ordering = self.get_ordering(request, queryset, view) + + if ordering: + for ordering_field in ordering: + queryset = self._order_by_totals_field(ordering_field, queryset) + return queryset diff --git a/apps/codecov-api/api/internal/repo/serializers.py b/apps/codecov-api/api/internal/repo/serializers.py new file mode 100644 index 0000000000..2b79c2ffda --- /dev/null +++ b/apps/codecov-api/api/internal/repo/serializers.py @@ -0,0 +1,107 @@ +from rest_framework import serializers + +from api.internal.commit.serializers import ( + CommitTotalsSerializer, + CommitWithFileLevelReportSerializer, +) +from api.internal.owner.serializers import OwnerSerializer +from core.models import Commit, Repository +from services.analytics import AnalyticsService + + +class RepoSerializer(serializers.ModelSerializer): + author = OwnerSerializer() + + class Meta: + model = Repository + read_only_fields = ( + "repoid", + "service_id", + "name", + "private", + "updatestamp", + "author", + "language", + "hookid", + "using_integration", + ) + fields = read_only_fields + ("branch", "active", "activated") + + +class RepoWithMetricsSerializer(RepoSerializer): + latest_commit_totals = CommitTotalsSerializer() + latest_coverage_change = serializers.FloatField() + + class Meta(RepoSerializer.Meta): + fields = ( + "latest_commit_totals", + "latest_coverage_change", + ) + RepoSerializer.Meta.fields + + +class RepoDetailsSerializer(RepoSerializer): + fork = RepoSerializer() + latest_commit = serializers.SerializerMethodField(source="get_latest_commit") + bot = serializers.SerializerMethodField() + + # Permissions + can_view = serializers.SerializerMethodField() + can_edit = serializers.SerializerMethodField() + + class Meta(RepoSerializer.Meta): + read_only_fields = ( + "fork", + "upload_token", + "yaml", + "image_token", + ) + RepoSerializer.Meta.read_only_fields + fields = ( + ("can_edit", "can_view", "latest_commit", "bot") + + RepoSerializer.Meta.fields + + read_only_fields + ) + + def get_bot(self, repo): + if repo.bot: + return repo.bot.username + + def get_latest_commit(self, repo): + commits_queryset = ( + repo.commits.filter(state=Commit.CommitStates.COMPLETE) + .defer("_report") + .order_by("-timestamp") + ) + + branch_param = self.context["request"].query_params.get("branch", None) + + commits_queryset = commits_queryset.filter(branch=branch_param or repo.branch) + + commit = commits_queryset.first() + if commit: + return CommitWithFileLevelReportSerializer(commit).data + + def get_can_view(self, _): + return self.context.get("can_view") + + def get_can_edit(self, _): + return self.context.get("can_edit") + + def to_representation(self, repo): + rep = super().to_representation(repo) + if not rep.get("can_edit"): + del rep["upload_token"] + return rep + + def update(self, instance, validated_data): + analytics = AnalyticsService() + if "active" in validated_data: + if validated_data["active"] and not instance.active: + analytics.account_activated_repository( + self.context["request"].current_owner.ownerid, instance + ) + + return super().update(instance, validated_data) + + +class SecretStringPayloadSerializer(serializers.Serializer): + value = serializers.CharField(required=True) diff --git a/apps/codecov-api/api/internal/repo/views.py b/apps/codecov-api/api/internal/repo/views.py new file mode 100644 index 0000000000..ff39f9ff62 --- /dev/null +++ b/apps/codecov-api/api/internal/repo/views.py @@ -0,0 +1,81 @@ +import logging + +from django.utils import timezone +from django_filters import rest_framework as django_filters +from rest_framework import filters, mixins +from rest_framework.exceptions import PermissionDenied + +from api.internal.repo.filter import RepositoryOrderingFilter +from api.shared.repo.filter import RepositoryFilters +from api.shared.repo.mixins import RepositoryViewSetMixin + +from .serializers import ( + RepoDetailsSerializer, + RepoWithMetricsSerializer, +) + +log = logging.getLogger(__name__) + + +class RepositoryViewSet( + RepositoryViewSetMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, +): + filter_backends = ( + django_filters.DjangoFilterBackend, + filters.SearchFilter, + RepositoryOrderingFilter, + ) + filterset_class = RepositoryFilters + search_fields = ("name",) + ordering_fields = ( + "updatestamp", + "name", + "latest_coverage_change", + "coverage", + "lines", + "hits", + "partials", + "misses", + "complexity", + ) + + def get_serializer_class(self): + if self.action == "list": + return RepoWithMetricsSerializer + return RepoDetailsSerializer + + def get_serializer_context(self, *args, **kwargs): + context = super().get_serializer_context(*args, **kwargs) + if self.action != "list": + context.update({"can_edit": self.can_edit, "can_view": self.can_view}) + return context + + def get_queryset(self): + queryset = super().get_queryset() + + if self.action == "list": + before_date = self.request.query_params.get( + "before_date", timezone.now().isoformat() + ) + branch = self.request.query_params.get("branch", None) + + queryset = queryset.with_latest_commit_totals_before( + before_date=before_date, branch=branch, include_previous_totals=True + ).with_latest_coverage_change() + + if self.request.query_params.get("exclude_uncovered", False): + queryset = queryset.exclude_uncovered() + + return queryset + + def perform_update(self, serializer): + # Check repo limits for users with legacy plans + owner = self.owner + if serializer.validated_data.get("active"): + if owner.has_legacy_plan and owner.repo_credits <= 0: + raise PermissionDenied("Private repository limit reached.") + return super().perform_update(serializer) diff --git a/apps/codecov-api/api/internal/self_hosted/__init__.py b/apps/codecov-api/api/internal/self_hosted/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/internal/self_hosted/filters.py b/apps/codecov-api/api/internal/self_hosted/filters.py new file mode 100644 index 0000000000..61e58547dd --- /dev/null +++ b/apps/codecov-api/api/internal/self_hosted/filters.py @@ -0,0 +1,14 @@ +import django_filters + + +class UserFilters(django_filters.FilterSet): + activated = django_filters.BooleanFilter( + method="filter_activated", label="activated" + ) + is_admin = django_filters.BooleanFilter(method="filter_is_admin", label="is_admin") + + def filter_activated(self, queryset, name, value): + return queryset.filter(activated=value) + + def filter_is_admin(self, queryset, name, value): + return queryset.filter(is_admin=value) diff --git a/apps/codecov-api/api/internal/self_hosted/permissions.py b/apps/codecov-api/api/internal/self_hosted/permissions.py new file mode 100644 index 0000000000..f5b6245999 --- /dev/null +++ b/apps/codecov-api/api/internal/self_hosted/permissions.py @@ -0,0 +1,10 @@ +from rest_framework.permissions import BasePermission + +import services.self_hosted as self_hosted + + +class AdminPermissions(BasePermission): + def has_permission(self, request, view): + return request.current_owner and self_hosted.is_admin_owner( + request.current_owner + ) diff --git a/apps/codecov-api/api/internal/self_hosted/serializers.py b/apps/codecov-api/api/internal/self_hosted/serializers.py new file mode 100644 index 0000000000..384a02315c --- /dev/null +++ b/apps/codecov-api/api/internal/self_hosted/serializers.py @@ -0,0 +1,34 @@ +from rest_framework import serializers +from rest_framework.exceptions import PermissionDenied + +import services.self_hosted as self_hosted +from codecov_auth.models import Owner + + +class UserSerializer(serializers.ModelSerializer): + is_admin = serializers.BooleanField() + activated = serializers.BooleanField() + + class Meta: + model = Owner + fields = ( + "ownerid", + "username", + "email", + "name", + "is_admin", + "activated", + ) + + def update(self, instance, validated_data): + if "activated" in validated_data: + if validated_data["activated"] is True: + try: + self_hosted.activate_owner(instance) + except self_hosted.LicenseException as err: + raise PermissionDenied(err.message) + else: + self_hosted.deactivate_owner(instance) + + # re-query for object to get updated `activated` value + return self.context["view"].get_queryset().filter(pk=instance.pk).first() diff --git a/apps/codecov-api/api/internal/self_hosted/views.py b/apps/codecov-api/api/internal/self_hosted/views.py new file mode 100644 index 0000000000..246030bcbb --- /dev/null +++ b/apps/codecov-api/api/internal/self_hosted/views.py @@ -0,0 +1,65 @@ +from django.db.models import Exists, OuterRef, Q +from django.db.models.functions import Coalesce +from django_filters import rest_framework as django_filters +from rest_framework import filters, mixins, viewsets +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +import services.self_hosted as self_hosted +from codecov_auth.models import Owner + +from .filters import UserFilters +from .permissions import AdminPermissions +from .serializers import UserSerializer + + +class UserViewSet( + viewsets.GenericViewSet, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, +): + serializer_class = UserSerializer + filter_backends = ( + django_filters.DjangoFilterBackend, + filters.SearchFilter, + ) + filterset_class = UserFilters + permission_classes = [AdminPermissions] + ordering_fields = ("name", "username", "email") + search_fields = ["name", "username", "email"] + + def get_queryset(self): + activated_owners = self_hosted.activated_owners() + condition = (Q(oauth_token__isnull=False) & Q(organizations__isnull=False)) | Q( + pk__in=activated_owners + ) + return Owner.objects.filter(condition).annotate( + is_admin=Coalesce( + Exists(self_hosted.admin_owners().filter(pk=OuterRef("pk"))), False + ), + activated=Coalesce( + Exists(activated_owners.filter(pk=OuterRef("pk"))), + False, + ), + ) + + @action( + detail=False, + methods=["get"], + url_path="current", + permission_classes=[IsAuthenticated], + ) + def current(self, request): + current_owner = self.get_queryset().filter(pk=request.current_owner.pk).first() + serializer = self.get_serializer(current_owner) + return Response(serializer.data) + + @current.mapping.patch + def current_update(self, request): + current_owner = self.get_queryset().filter(pk=request.current_owner.pk).first() + serializer = self.get_serializer(current_owner, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data) diff --git a/apps/codecov-api/api/internal/slack/__init__.py b/apps/codecov-api/api/internal/slack/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/internal/slack/helpers.py b/apps/codecov-api/api/internal/slack/helpers.py new file mode 100644 index 0000000000..a116841894 --- /dev/null +++ b/apps/codecov-api/api/internal/slack/helpers.py @@ -0,0 +1,14 @@ +from rest_framework.exceptions import ValidationError + +from codecov_auth.models import Service + + +def validate_params(username, service): + """ + Validates the parameters of the request. + """ + if not username or not service: + raise ValidationError("Username and service are required") + + if service not in Service: + raise ValidationError("Invalid service") diff --git a/apps/codecov-api/api/internal/slack/urls.py b/apps/codecov-api/api/internal/slack/urls.py new file mode 100644 index 0000000000..b4dc61c282 --- /dev/null +++ b/apps/codecov-api/api/internal/slack/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from api.internal.slack.views import GenerateAccessTokenView + +urlpatterns = [ + path("generate-token/", GenerateAccessTokenView.as_view(), name="generate-token"), +] diff --git a/apps/codecov-api/api/internal/slack/views.py b/apps/codecov-api/api/internal/slack/views.py new file mode 100644 index 0000000000..47c3644021 --- /dev/null +++ b/apps/codecov-api/api/internal/slack/views.py @@ -0,0 +1,35 @@ +from rest_framework.exceptions import NotFound +from rest_framework.response import Response +from rest_framework.views import APIView + +from api.shared.permissions import InternalTokenPermissions +from codecov_auth.authentication import InternalTokenAuthentication +from codecov_auth.models import Owner, UserToken + +from .helpers import validate_params + + +class GenerateAccessTokenView(APIView): + """ + Returns a new access token for the given user for slack integration + """ + + authentication_classes = [InternalTokenAuthentication] + permission_classes = [InternalTokenPermissions] + + def post(self, request, *args, **kwargs): + username = request.data.get("username") + service = request.data.get("service") + validate_params(username, service) + + owner = Owner.objects.filter(username=username, service=service).first() + if not owner: + raise NotFound("Owner not found") + + token_type = UserToken.TokenType.API.value + user_token, _ = UserToken.objects.get_or_create( + name="slack-codecov-access-token", + owner=owner, + token_type=token_type, + ) + return Response({"token": user_token.token}, status=200) diff --git a/apps/codecov-api/api/internal/tests/__init__.py b/apps/codecov-api/api/internal/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/internal/tests/samples/00c7b4b49778b3c79427f9c4c13a8612a376ff19_chunks.txt b/apps/codecov-api/api/internal/tests/samples/00c7b4b49778b3c79427f9c4c13a8612a376ff19_chunks.txt new file mode 100644 index 0000000000..e18a385a25 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/samples/00c7b4b49778b3c79427f9c4c13a8612a376ff19_chunks.txt @@ -0,0 +1,49 @@ +{} +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[0, null, [[0, 0, null, null, null], [1, 0, null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[0, null, [[0, 0, null, null, null], [1, 0, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[0, null, [[0, 0, null, null, null], [1, 0, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[0, null, [[0, 0, null, null, null], [1, 0, null, null, null]]] + + + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[0, null, [[0, 0, null, null, null], [1, 0, null, null, null]]] \ No newline at end of file diff --git a/apps/codecov-api/api/internal/tests/samples/00c7b4b49778b3c79427f9c4c13a8612a376ff19_report.json b/apps/codecov-api/api/internal/tests/samples/00c7b4b49778b3c79427f9c4c13a8612a376ff19_report.json new file mode 100644 index 0000000000..ae86688688 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/samples/00c7b4b49778b3c79427f9c4c13a8612a376ff19_report.json @@ -0,0 +1,219 @@ +{ + "files": { + "awesome/__init__.py": [ + 2, + [ + 0, + 14, + 10, + 4, + 0, + "71.42857", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + [ + 0, + 14, + 10, + 4, + 0, + "71.42857", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 14, + 10, + 4, + 0, + "71.42857", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + null + ], + "tests/__init__.py": [ + 0, + [ + 0, + 3, + 2, + 1, + 0, + "66.66667", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + [ + 0, + 3, + 2, + 1, + 0, + "66.66667", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 3, + 2, + 1, + 0, + "66.66667", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + null + ], + "tests/test_sample.py": [ + 1, + [ + 0, + 7, + 7, + 0, + 0, + "100", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + [ + 0, + 7, + 7, + 0, + 0, + "100", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 7, + 7, + 0, + 0, + "100", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + null + ] + }, + "sessions": { + "0": { + "N": null, + "a": "v4/raw/2019-07-02/8AD32B748ABB0F88B6E68B3E4FA7A71F/00c7b4b49778b3c79427f9c4c13a8612a376ff19/e83211bb-55b8-4549-baef-cb159ba78cf0.txt", + "c": null, + "d": 1562093100, + "e": null, + "f": [ + "flagone" + ], + "j": null, + "n": null, + "p": null, + "storage_path": "v4/raw/2019-07-02/8AD32B748ABB0F88B6E68B3E4FA7A71F/00c7b4b49778b3c79427f9c4c13a8612a376ff19/e83211bb-55b8-4549-baef-cb159ba78cf0.txt", + "t": [ + 3, + 24, + 19, + 5, + 0, + "79.16667", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "u": null + }, + "1": { + "N": null, + "a": "v4/raw/2019-07-02/8AD32B748ABB0F88B6E68B3E4FA7A71F/00c7b4b49778b3c79427f9c4c13a8612a376ff19/14b2108c-552e-4277-a918-478c597c10bd.txt", + "c": null, + "d": 1562093214, + "e": null, + "f": [ + "flagtwo" + ], + "j": null, + "n": null, + "p": null, + "storage_path": "v4/raw/2019-07-02/8AD32B748ABB0F88B6E68B3E4FA7A71F/00c7b4b49778b3c79427f9c4c13a8612a376ff19/14b2108c-552e-4277-a918-478c597c10bd.txt", + "t": [ + 3, + 24, + 19, + 5, + 0, + "79.16667", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "u": null + } + } +} \ No newline at end of file diff --git a/apps/codecov-api/api/internal/tests/samples/68946ef98daec68c7798459150982fc799c87d85_chunks.txt b/apps/codecov-api/api/internal/tests/samples/68946ef98daec68c7798459150982fc799c87d85_chunks.txt new file mode 100644 index 0000000000..b42fbcdbde --- /dev/null +++ b/apps/codecov-api/api/internal/tests/samples/68946ef98daec68c7798459150982fc799c87d85_chunks.txt @@ -0,0 +1,50 @@ +{} +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[0, null, [[0, 0, null, null, null], [1, 0, null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 0, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 0, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 0, null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[0, null, [[0, 0, null, null, null], [1, 0, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[0, null, [[0, 0, null, null, null], [1, 0, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 0, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 0, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 0, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[0, null, [[0, 0, null, null, null], [1, 0, null, null, null]]] + + + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[0, null, [[0, 0, null, null, null], [1, 0, null, null, null]]] \ No newline at end of file diff --git a/apps/codecov-api/api/internal/tests/samples/68946ef98daec68c7798459150982fc799c87d85_report.json b/apps/codecov-api/api/internal/tests/samples/68946ef98daec68c7798459150982fc799c87d85_report.json new file mode 100644 index 0000000000..d5183dad03 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/samples/68946ef98daec68c7798459150982fc799c87d85_report.json @@ -0,0 +1,234 @@ +{ + "files": { + "awesome/__init__.py": [ + 2, + [ + 0, + 14, + 10, + 4, + 0, + "71.42857", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + [ + 0, + 14, + 10, + 4, + 0, + "71.42857", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 14, + 10, + 4, + 0, + "71.42857", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + null + ], + "tests/__init__.py": [ + 0, + [ + 0, + 3, + 2, + 1, + 0, + "66.66667", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + [ + 0, + 3, + 2, + 1, + 0, + "66.66667", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 3, + 2, + 1, + 0, + "66.66667", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + null + ], + "tests/test_sample.py": [ + 1, + [ + 0, + 7, + 7, + 0, + 0, + "100", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + [ + 0, + 7, + 7, + 0, + 0, + "100", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 7, + 7, + 0, + 0, + "100", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + [ + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + 0 + ] + ] + }, + "sessions": { + "0": { + "N": null, + "a": "v4/raw/2019-07-02/8AD32B748ABB0F88B6E68B3E4FA7A71F/d48148e85b32c37c821476c236d9c7a8aa7dcf8e/89b4620c-2740-42f9-bb95-eba438beb197.txt", + "c": null, + "d": 1562093431, + "e": null, + "f": [ + "flagone" + ], + "j": null, + "n": null, + "p": null, + "storage_path": "v4/raw/2019-07-02/8AD32B748ABB0F88B6E68B3E4FA7A71F/d48148e85b32c37c821476c236d9c7a8aa7dcf8e/89b4620c-2740-42f9-bb95-eba438beb197.txt", + "t": [ + 3, + 24, + 19, + 5, + 0, + "79.16667", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "u": null, + "st": "carriedforward" + }, + "1": { + "N": null, + "a": "v4/raw/2019-07-02/8AD32B748ABB0F88B6E68B3E4FA7A71F/d48148e85b32c37c821476c236d9c7a8aa7dcf8e/ac50a236-058d-4507-922a-4a13dca6762e.txt", + "c": null, + "d": 1562093484, + "e": null, + "f": [ + "flagtwo" + ], + "j": null, + "n": null, + "p": null, + "storage_path": "v4/raw/2019-07-02/8AD32B748ABB0F88B6E68B3E4FA7A71F/d48148e85b32c37c821476c236d9c7a8aa7dcf8e/ac50a236-058d-4507-922a-4a13dca6762e.txt", + "t": [ + 3, + 24, + 19, + 5, + 0, + "79.16667", + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "u": null + } + } +} \ No newline at end of file diff --git a/apps/codecov-api/api/internal/tests/samples/9sa8790asdf9agyasdg7a90sd9f89as7ga0sdf98a_chunks.txt b/apps/codecov-api/api/internal/tests/samples/9sa8790asdf9agyasdg7a90sd9f89as7ga0sdf98a_chunks.txt new file mode 100644 index 0000000000..b42fbcdbde --- /dev/null +++ b/apps/codecov-api/api/internal/tests/samples/9sa8790asdf9agyasdg7a90sd9f89as7ga0sdf98a_chunks.txt @@ -0,0 +1,50 @@ +{} +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[0, null, [[0, 0, null, null, null], [1, 0, null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 0, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 0, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 0, null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[0, null, [[0, 0, null, null, null], [1, 0, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[0, null, [[0, 0, null, null, null], [1, 0, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 0, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 0, null, null, null]]] +[1, null, [[0, 1, null, null, null], [1, 0, null, null, null]]] + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[0, null, [[0, 0, null, null, null], [1, 0, null, null, null]]] + + + + +[1, null, [[0, 1, null, null, null], [1, 1, null, null, null]]] +[0, null, [[0, 0, null, null, null], [1, 0, null, null, null]]] \ No newline at end of file diff --git a/apps/codecov-api/api/internal/tests/samples/get_commit_diff-response.json b/apps/codecov-api/api/internal/tests/samples/get_commit_diff-response.json new file mode 100644 index 0000000000..075df98aaa --- /dev/null +++ b/apps/codecov-api/api/internal/tests/samples/get_commit_diff-response.json @@ -0,0 +1,90 @@ +{ + "files": { + "awesome/__init__.py": { + "before": "None", + "segments": [ + { + "header": [ + "10", + "3", + "10", + "7" + ], + "lines": [ + " if n ", + "< 2:", + " ", + "return 1", + " ", + "return ", + "fib(n - 2) ", + "+ fib(n - ", + "1)", + "+", + "+", + "+def ", + "coala(k):", + "+ ", + "return k * ", + "k" + ] + } + ], + "stats": { + "added": 4, + "removed": 0 + }, + "type": "modified" + }, + "coverage.xml": { + "before": "None", + "segments": [ + { + "header": [ + "1", + "5", + "1", + "5" + ], + "lines": [ + " ", + "-", + "+", + " \t", + " \t", + " \t" + ] + } + ], + "stats": { + "added": 1, + "removed": 1 + }, + "type": "modified" + } + } +} \ No newline at end of file diff --git a/apps/codecov-api/api/internal/tests/samples/report-field-example.json b/apps/codecov-api/api/internal/tests/samples/report-field-example.json new file mode 100644 index 0000000000..a8e3fcc7fd --- /dev/null +++ b/apps/codecov-api/api/internal/tests/samples/report-field-example.json @@ -0,0 +1,70 @@ +{ + "files": { + "lib/calc.ts": [ + 0, + [ + 0, + 4, + 4, + 0, + 0, + "100", + 0, + 2, + 0, + 0, + 0, + 0, + 0 + ], + [ + [ + 0, + 4, + 4, + 0, + 0, + "100", + 0, + 2, + 0, + 0, + 0, + 0, + 0 + ] + ], + null + ] + }, + "sessions": { + "0": { + "N": null, + "a": "v4/raw/2019-04-11/F841A2E295426932B8AE33B2E5739E94/edcf349a9e0cd7bd1c255028597357e6e2f9538d/42e252be-3cac-43e0-b418-5c3cc2102b6e.txt", + "c": null, + "d": 1554977264, + "e": null, + "f": null, + "j": null, + "n": null, + "p": null, + "t": [ + 1, + 4, + 4, + 0, + 0, + "100", + 0, + 2, + 0, + 0, + 0, + 0, + 0 + ], + "u": null, + "storage_path": "v4/raw/2019-04-11/F841A2E295426932B8AE33B2E5739E94/edcf349a9e0cd7bd1c255028597357e6e2f9538d/42e252be-3cac-43e0-b418-5c3cc2102b6e.txt" + } + } +} \ No newline at end of file diff --git a/apps/codecov-api/api/internal/tests/test_charts.py b/apps/codecov-api/api/internal/tests/test_charts.py new file mode 100644 index 0000000000..7fa9476f08 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/test_charts.py @@ -0,0 +1,990 @@ +from datetime import datetime, timedelta +from decimal import Decimal +from math import isclose +from random import randint +from unittest.mock import patch + +import pytest +from dateutil.relativedelta import relativedelta +from django.test import TestCase, override_settings +from django.utils import timezone +from factory.faker import faker +from pytz import UTC +from rest_framework.exceptions import ValidationError +from rest_framework.reverse import reverse +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) + +from api.internal.chart.filters import apply_default_filters, apply_simple_filters +from api.internal.chart.helpers import ( + ChartQueryRunner, + annotate_commits_with_totals, + apply_grouping, + validate_params, +) +from codecov.tests.base_test import InternalAPITest +from core.models import Commit +from utils.test_utils import Client + +fake = faker.Faker() + + +def generate_random_totals( + include_complexity=True, lines=None, hits=None, partials=None +): + lines = lines or randint(5, 5000) + hits = hits or randint(0, lines) + partials = partials or randint(0, lines - hits) + misses = lines - hits - partials + coverage = (hits + partials) / lines + complexity = randint(0, 5) if include_complexity else 0 + complexity_total = randint(complexity, 10) if include_complexity else 0 + + totals = { + "n": lines, + "h": hits, + "p": partials, + "m": misses, + "c": coverage, + "C": complexity, + "N": complexity_total, + # Not currenly used: diff, files, sessions, branches, methods + } + return totals + + +def setup_commits( + repo, + num_commits, + branch="main", + start_date=None, + meets_default_filters=True, + **kwargs, +): + """ + Generate random commits with different configurations, to accommodate different testing scenarios. + + :param repo: repo to associate the commits with + :param num_commits: number of commits to create + :param branch: branch to associate with the commit; randomly generated by DDF if none provided + :param start_date: if provided, commit timestamp will be set to after this date. + for more info on acceptable values for start_date, see: https://faker.readthedocs.io/en/master/providers/faker.providers.date_time.html#faker.providers.date_time.Provider.date_time_between + :param meets_default_filters: when true the commit will meet all the conditions for the initial filtering done on commits + :param kwargs: passed to generate_random_totals to manually set totals values + """ + for _ in range(num_commits): + timestamp = ( + fake.date_time_between(start_date=start_date, tzinfo=UTC) + if start_date + else fake.date_time(tzinfo=UTC) + ) + + totals = generate_random_totals(**kwargs) if meets_default_filters else None + state = "complete" if meets_default_filters else "pending" + ci_passed = True if meets_default_filters else False + deleted = False if meets_default_filters else True + + CommitFactory( + repository=repo, + branch=branch, + timestamp=timestamp, + totals=totals, + state=state, + ci_passed=ci_passed, + deleted=deleted, + ) + + +def check_grouping_correctness(grouped_queryset, initial_queryset, data): + """ + Used to test "apply_grouping" correctness. Programmatically verify that commits were grouped + by the correct unit of time, and that within that grouping the correct commit was returned based on the + query params provided. + + :param grouped_queryset: the queryset generated by calling "apply_grouping" on the initial_queryset + :param initial_queryset: the annotated and filtered queryset provided to the apply_grouping call + :param data: the grouping and filtering parameters that were applied when generating the grouping + """ + grouping_unit = data.get("grouping_unit") + agg_function = data.get("agg_function") + agg_value = data.get("agg_value") + + """ + For each of the grouped commits, retrieve all the commits from the initial queryset that are within the same time window + and verify that we can't find any that that better match the given aggregation function better. + + For example, if we grouped by max coverage per month, we'll get the commits for that month and verify that none of them have a coverage + value greater than the grouped commit. + """ + for commit in grouped_queryset: + # Get the unit of time we grouped by so we can filter for all the commits in this commit's time window + relative_delta_args = ( + {f"{grouping_unit}s": 1} + if grouping_unit != "quarter" + else { + "months": 3 + } # relativedelta doesn't except quarter as an argument so set that manually + ) + + # example: if agg_function is "min" and agg_value is "coverage", this will pass + # "coverage__lt: " to the filter call below, to check if any commits in this window had lower coverage + filtering_key = agg_value + ("__lt" if agg_function == "min" else "__gt") + filtering_value_args = {filtering_key: getattr(commit, agg_value)} + + assert ( + not initial_queryset.filter( + timestamp__gt=commit.truncated_date, + timestamp__lt=commit.truncated_date + + relativedelta(**relative_delta_args), + repository__name=commit.repository.name, + ) + .filter( + **filtering_value_args + ) # make two filter calls to avoid potentially filtering by timestamp multiple time in the same call which makes django unhappy + .exclude(commitid=commit.commitid) + .exists() + ) + + +class CoverageChartHelpersTest(TestCase): + def setUp(self): + self.org1 = OwnerFactory(username="org1") + self.repo1_org1 = RepositoryFactory(author=self.org1, name="repo1") + setup_commits(self.repo1_org1, 10) + + self.repo2_org1 = RepositoryFactory(author=self.org1, name="repo2") + setup_commits(self.repo2_org1, 10) + + self.org2 = OwnerFactory(username="org2", service_id=1239128) + self.repo1_org2 = RepositoryFactory(author=self.org2, name="repo1") + setup_commits(self.repo1_org2, 10) + + self.user = OwnerFactory( + organizations=[self.org1.ownerid], + permission=[ + self.repo1_org1.repoid, + self.repo2_org1.repoid, + self.repo1_org2.repoid, + ], + ) + + def test_validate_params_invalid(self): + data = { + "agg_function": "potato", + "grouping_unit": "potato", + "coverage_timestamp_ordering": "potato", + "repositories": [], + "field_not_in_schema": True, + } + + with self.assertRaises(ValidationError) as err: + validate_params(data) + + # Check that only the expected validation errors occurred + validation_errors = err.exception.detail + assert len(validation_errors) == 5 + assert "owner_username" in validation_errors # required field missing + assert "grouping_unit" in validation_errors # value not allowed + assert "agg_function" in validation_errors # value not allowed + assert ( + "field_not_in_schema" in validation_errors + ) # only fields in the schema are allowed in params + assert "coverage_timestamp_ordering" in validation_errors # value not allowed + + def test_validate_params_valid(self): + data = { + "owner_username": self.org1.username, + "agg_function": "max", + "agg_value": "coverage", + "grouping_unit": "month", + "coverage_timestamp_ordering": "increasing", + } + + validate_params(data) + + def test_validate_params_agg_fields(self): + data_aggregated = { + "owner_username": self.org1.username, + "grouping_unit": "day", + "agg_function": "min", + "agg_value": "timestamp", + } + validate_params(data_aggregated) + + # Check that aggregation parameters are not required when grouping by commit + data_grouped_by_commit = { + "owner_username": self.org1.username, + "grouping_unit": "commit", + } + validate_params(data_grouped_by_commit) + + # Check that aggregation parameters are required when grouping by commit + data_aggregated_missing_agg_fields = { + "owner_username": self.org1.username, + "grouping_unit": "month", + } + with self.assertRaises(ValidationError) as err: + validate_params(data_aggregated_missing_agg_fields) + + validation_errors = err.exception.detail + assert len(validation_errors) == 1 + assert "grouping_unit" in validation_errors + + def test_apply_default_filters(self): + setup_commits(self.repo1_org1, 10, meets_default_filters=False) + + queryset = apply_default_filters(Commit.objects.all()) + + assert queryset.count() > 0 and queryset.count() < Commit.objects.count() + for commit in queryset: + assert commit.state == "complete" + assert commit.deleted is False + assert commit.ci_passed is True + assert commit.totals is not None + + def test_apply_simple_filters(self): + setup_commits(self.repo1_org1, 10, start_date="-7d") + setup_commits(self.repo1_org1, 2, branch="production", start_date="-7d") + + start_date = datetime.now() - relativedelta(days=7) + end_date = datetime.now() + data = { + "owner_username": self.org1.username, + "branch": "main", + "start_date": start_date.isoformat(), + "end_date": end_date.isoformat(), + "repositories": [self.repo1_org1.name, self.repo2_org1.name], + "service": "github", + } + queryset = apply_simple_filters(Commit.objects.all(), data, self.user) + + assert queryset.count() > 0 + for commit in queryset: + assert commit.repository.name in data.get("repositories") + assert commit.repository.author.username == data["owner_username"] + assert commit.branch == data["branch"] + assert commit.timestamp >= start_date + assert commit.timestamp <= end_date + + def test_apply_simple_filters_repo_filtering(self): + """ + This test verifies that when no "repository" parameters are returned, we only return all repositories + in the organization that the logged-in user has permissions to view. + """ + no_permissions_repo = RepositoryFactory( + author=self.org1, name="no_permissions_to_this_repo", private=True + ) + setup_commits(no_permissions_repo, 10) + + data = {"owner_username": self.org1.username, "service": "github"} + + queryset = apply_simple_filters(Commit.objects.all(), data, self.user) + assert queryset.count() > 0 + for commit in queryset: + assert commit.repository.name != no_permissions_repo + + def test_apply_simple_filters_without_service(self): + """ + This test verifies that when no commits are returned if the user doesn't provide both a username and the service + """ + repo = RepositoryFactory(author=self.org1, name="random_repo") + setup_commits(repo, 10) + + data = {"owner_username": self.org1.username} + + queryset = apply_simple_filters(Commit.objects.all(), data, self.user) + assert queryset.count() == 0 + + def test_apply_simple_filters_branch_filtering(self): + # Verify that when no "branch" param is provided, we filter commits by the repo's default branch + + branch_test = RepositoryFactory( + author=self.org1, name="branch_test", private=False, branch="main" + ) # "main" is the default branch + setup_commits(branch_test, 10, branch="main") + setup_commits(branch_test, 10, branch="not_default") + + # we shouldn't get commits on "main" branch for a repo that has a different default branch + setup_commits(self.repo1_org1, 10, branch="main") + + data = { + "owner_username": self.org1.username, + "service": "gh", + "repositories": [self.repo1_org1.name, branch_test.name], + } + queryset = apply_simple_filters(Commit.objects.all(), data, self.user) + assert queryset.count() > 0 + for commit in queryset: + assert ( + commit.repository.name == self.repo1_org1.name + and commit.branch == "main" + ) or ( + commit.repository.name == branch_test.name and commit.branch == "main" + ) + + # should still be able to query by non-default branch if desired + data = { + "owner_username": self.org1.username, + "repositories": [branch_test.name], + "service": "github", + "branch": "not_default", + } + queryset = apply_simple_filters(Commit.objects.all(), data, self.user) + assert queryset.count() > 0 + for commit in queryset: + assert ( + commit.repository.name == branch_test.name + and commit.branch == "not_default" + ) + + def test_annotate_commits_with_totals(self): + with_complexity_commitid = "i230tky2" + CommitFactory( + commitid=with_complexity_commitid, + totals={"n": 0, "h": 0, "p": 0, "m": 0, "c": 0, "C": 0, "N": 1}, + ) + annotated_commits = annotate_commits_with_totals( + Commit.objects.filter(commitid=with_complexity_commitid) + ) + + assert annotated_commits.count() > 0 + for commit in annotated_commits: + # direct float equality checks in python are finicky so use "isclose" to check we got the expected value + assert isclose(commit.coverage, commit.totals["c"]) + assert isclose(commit.complexity, commit.totals["C"]) + assert isclose(commit.complexity_total, commit.totals["N"]) + assert isclose( + commit.complexity_ratio, commit.totals["C"] / commit.totals["N"] + ) + + def test_annotate_commit_with_totals_no_complexity_sets_ratio_to_None(self): + no_complexity_commitid = "sdfkjwepj42" + CommitFactory( + commitid=no_complexity_commitid, + totals={"n": 0, "h": 0, "p": 0, "m": 0, "c": 0, "C": 0, "N": 0}, + ) + annotated_commits = annotate_commits_with_totals( + Commit.objects.filter(commitid=no_complexity_commitid) + ) + + assert annotated_commits.count() > 0 + for commit in annotated_commits: + assert commit.complexity_ratio is None + + def test_apply_grouping(self): + with self.subTest("min coverage"): + setup_commits(self.repo1_org1, 20, start_date="-7d") + + data = { + "owner_username": self.org1.username, + "grouping_unit": "day", + "agg_function": "min", + "agg_value": "coverage", + "start_date": (timezone.now() - relativedelta(days=7)).isoformat(), + "end_date": timezone.now().isoformat(), + "repositories": [self.repo1_org1.name], + } + + initial_queryset = annotate_commits_with_totals( + apply_simple_filters( + apply_default_filters(Commit.objects.all()), data, self.user + ) + ) + grouped_queryset = apply_grouping(initial_queryset, data) + check_grouping_correctness(grouped_queryset, initial_queryset, data) + + with self.subTest("max coverage"): + setup_commits(self.repo1_org1, 20, start_date="-180d") + + data = { + "owner_username": self.org1.username, + "grouping_unit": "month", + "agg_function": "max", + "agg_value": "coverage", + "start_date": (timezone.now() - relativedelta(months=6)).isoformat(), + "end_date": timezone.now().isoformat(), + "repositories": [self.repo1_org1.name], + } + + initial_queryset = annotate_commits_with_totals( + apply_simple_filters( + apply_default_filters(Commit.objects.all()), data, self.user + ) + ) + grouped_queryset = apply_grouping(initial_queryset, data) + check_grouping_correctness(grouped_queryset, initial_queryset, data) + + with self.subTest("min complexity"): + setup_commits(self.repo1_org1, 20, start_date="-7d") + + data = { + "owner_username": self.org1.username, + "grouping_unit": "day", + "agg_function": "max", + "agg_value": "complexity", + "start_date": (timezone.now() - relativedelta(days=7)).isoformat(), + "end_date": timezone.now().isoformat(), + "repositories": [self.repo1_org1.name], + } + + initial_queryset = annotate_commits_with_totals( + apply_simple_filters( + apply_default_filters(Commit.objects.all()), data, self.user + ) + ) + grouped_queryset = apply_grouping(initial_queryset, data) + check_grouping_correctness(grouped_queryset, initial_queryset, data) + + with self.subTest("max complexity"): + setup_commits(self.repo1_org1, 20, start_date="-7d") + + data = { + "owner_username": self.org1.username, + "grouping_unit": "day", + "agg_function": "max", + "agg_value": "complexity", + "start_date": (timezone.now() - relativedelta(days=7)).isoformat(), + "end_date": timezone.now().isoformat(), + "repositories": [self.repo1_org1.name], + } + + initial_queryset = annotate_commits_with_totals( + apply_simple_filters( + apply_default_filters(Commit.objects.all()), data, self.user + ) + ) + grouped_queryset = apply_grouping(initial_queryset, data) + check_grouping_correctness(grouped_queryset, initial_queryset, data) + + def test_ordering(self): + with self.subTest("order by increasing dates"): + data = { + "organization": self.org1.username, + "grouping_unit": "day", + "agg_function": "min", + "coverage_timestamp_ordering": "increasing", + "agg_value": "coverage", + "repositories": [self.repo1_org1.name], + } + + queryset = annotate_commits_with_totals( + apply_simple_filters( + apply_default_filters(Commit.objects.all()), data, self.user + ) + ) + queryset = apply_grouping(queryset, data) + + results = queryset.values() + # -1 because the last result doesn't need to be tested against + for i in range(len(results) - 1): + assert results[i]["timestamp"] < results[i + 1]["timestamp"] + + with self.subTest("order by decreasing dates"): + data = { + "organization": self.org1.username, + "grouping_unit": "day", + "agg_function": "min", + "coverage_timestamp_ordering": "decreasing", + "agg_value": "coverage", + "repositories": [self.repo1_org1.name], + } + + queryset = annotate_commits_with_totals( + apply_simple_filters( + apply_default_filters(Commit.objects.all()), data, self.user + ) + ) + queryset = apply_grouping(queryset, data) + + results = queryset.values() + # -1 because the last result doesn't need to be tested against + for i in range(len(results) - 1): + assert results[i]["timestamp"] > results[i + 1]["timestamp"] + + +class TestChartQueryRunnerQuery(TestCase): + """ + Tests for the querying-part of the ChartQueryRunner. + """ + + def setUp(self): + self.org = OwnerFactory() + self.repo1 = RepositoryFactory(author=self.org, active=True) + self.repo2 = RepositoryFactory(author=self.org, active=True) + self.repo3 = RepositoryFactory(author=self.org) + self.repo4 = RepositoryFactory(author=self.org, active=True) + self.user = OwnerFactory( + permission=[ + self.repo1.repoid, + self.repo2.repoid, + self.repo3.repoid, + self.repo4.repoid, + ] + ) + self.commit1 = CommitFactory( + repository=self.repo1, + totals={"h": 100, "n": 120, "p": 10, "m": 10}, + branch=self.repo1.branch, + state="complete", + ) + self.commit2 = CommitFactory( + repository=self.repo2, + totals={"h": 14, "n": 25, "p": 6, "m": 5}, + branch=self.repo2.branch, + state="complete", + ) + self.commit3 = CommitFactory( + repository=self.repo3, + totals={"h": 14, "n": 25, "p": 6, "m": 5}, + branch=self.repo3.branch, + state="complete", + ) + + @override_settings(GITHUB_CLIENT_ID="3d44be0e772666136a13") + def test_query_aggregates_multiple_repository_totals(self): + query_runner = ChartQueryRunner( + user=self.user, + request_params={ + "owner_username": self.org.username, + "service": self.org.service, + "end_date": str(timezone.now()), + "grouping_unit": "day", + }, + ) + + results = query_runner.run_query() + + assert len(results) == 1 + assert results[0]["total_hits"] == 114 + assert results[0]["total_lines"] == 145 + assert results[0]["total_misses"] == 15 + assert results[0]["total_partials"] == 16 + + @pytest.mark.skip(reason="flaky") + def test_query_aggregates_with_latest_commit_if_no_recent_upload(self): + # set timestamp to past, before 'start_date' + self.commit1.timestamp = timezone.now() - timedelta(days=7) + self.commit1.save() + + query_runner = ChartQueryRunner( + user=self.user, + request_params={ + "owner_username": self.org.username, + "service": self.org.service, + "start_date": str(timezone.now() - timedelta(days=1)), + "grouping_unit": "day", + }, + ) + + results = query_runner.run_query() + + assert len(results) == 2 + + # Day before commit2 is created, a few days after commit1 is created + assert results[0]["total_hits"] == 100 + assert results[0]["total_lines"] == 120 + assert results[0]["total_misses"] == 10 + assert results[0]["total_partials"] == 10 + assert results[0]["coverage"] == Decimal("91.67") + + # Day commit2 is created + assert results[1]["total_hits"] == 114 + assert results[1]["total_lines"] == 145 + assert results[1]["total_misses"] == 15 + assert results[1]["total_partials"] == 16 + assert results[1]["coverage"] == Decimal("89.66") + + @pytest.mark.skip(reason="flaky, skipping until re write") + def test_query_supports_different_grouping_params(self): + end_date = datetime.fromisoformat("2019-01-01") + self.commit1.timestamp = end_date - timedelta(days=365) + self.commit1.save() + pairs = [("day", 365), ("week", 52), ("month", 12), ("quarter", 4), ("year", 1)] + for grouping_unit, expected_num_datapoints in pairs: + query_runner = ChartQueryRunner( + user=self.user, + request_params={ + "owner_username": self.org.username, + "service": self.org.service, + "start_date": str(end_date - timedelta(days=365)), + "end_date": str(end_date), + "grouping_unit": grouping_unit, + }, + ) + + results = query_runner.run_query() + + assert ( + len(results) == expected_num_datapoints + 1 + ) # We add one because the date range is inclusive + + @pytest.mark.skip( + reason="flaky, skipping since we're moving away from ChartQueryRunner soon anyway" + ) + def test_query_supports_reverse_ordering(self): + self.commit1.timestamp = timezone.now() - timedelta(days=7) + self.commit1.save() + + query_runner = ChartQueryRunner( + user=self.user, + request_params={ + "owner_username": self.org.username, + "service": self.org.service, + "start_date": str(timezone.now() - timedelta(days=1)), + "grouping_unit": "day", + "coverage_timestamp_ordering": "decreasing", + }, + ) + + results = query_runner.run_query() + + assert len(results) == 2 + assert results[0]["date"] > results[1]["date"] + + def test_query_doesnt_crash_if_no_commits(self): + with self.subTest("no repos case"): + self.org.repository_set.all().delete() + ChartQueryRunner( + user=self.user, + request_params={ + "owner_username": self.org.username, + "service": self.org.service, + "grouping_unit": "day", + }, + ).run_query() + + with self.subTest("no commits case"): + repo = RepositoryFactory(author=self.org) + self.user.permission = [repo.repoid] + self.user.save() + ChartQueryRunner( + user=self.user, + request_params={ + "owner_username": self.org.username, + "service": self.org.service, + "grouping_unit": "day", + }, + ).run_query() + + +class TestChartQueryRunnerHelperMethods(TestCase): + """ + Tests for the non-querying-parts of the ChartQueryRunner, such + as validation and parameter transformation. + """ + + def setUp(self): + self.org = OwnerFactory() + self.user = OwnerFactory() + + def test_repoids(self): + repo1, repo2 = ( + RepositoryFactory(author=self.org, active=True), + RepositoryFactory(author=self.org, active=True), + ) + self.user.permission = [repo1.repoid, repo2.repoid] + self.user.save() + qr = ChartQueryRunner( + self.user, + { + "owner_username": self.org.username, + "service": self.org.service, + "grouping_unit": "day", + }, + ) + + with self.subTest("returns repoids"): + assert qr.repoids == f"({repo2.repoid},{repo1.repoid})" + + with self.subTest("filters by supplied repo names"): + qr = ChartQueryRunner( + self.user, + { + "owner_username": self.org.username, + "service": self.org.service, + "grouping_unit": "day", + "repositories": [repo1.name], + }, + ) + assert qr.repoids == f"({repo1.repoid})" + + def test_interval(self): + with self.subTest("translates quarter into 3 months"): + assert ( + ChartQueryRunner( + self.user, + { + "owner_username": self.org.username, + "service": self.org.service, + "grouping_unit": "quarter", + }, + ).interval + == "3 months" + ) + + with self.subTest("transforms grouping unit into '1 {grouping_unit}'"): + for grouping_unit in ["day", "week", "month", "year"]: + assert ( + ChartQueryRunner( + self.user, + { + "owner_username": self.org.username, + "service": self.org.service, + "grouping_unit": grouping_unit, + }, + ).interval + == f"1 {grouping_unit}" + ) + + def test_first_complete_commit_date_returns_date_of_first_complete_commit_in_repoids( + self, + ): + repo1, repo2 = ( + RepositoryFactory(author=self.org, active=True), + RepositoryFactory(author=self.org, active=True), + ) + self.user.permission = [repo1.repoid, repo2.repoid] + self.user.save() + CommitFactory( + repository=repo1, + branch=repo1.branch, + state="pending", + timestamp=timezone.now() - timedelta(days=7), + ) + commit1 = CommitFactory( + repository=repo1, + branch=repo1.branch, + state="complete", + timestamp=timezone.now() - timedelta(days=3), + ) + CommitFactory(repository=repo2, branch=repo2.branch, state="complete") + + qr = ChartQueryRunner( + self.user, + { + "owner_username": self.org.username, + "service": self.org.service, + "grouping_unit": "day", + }, + ) + + assert qr.first_complete_commit_date == datetime.date(commit1.timestamp) + + def test_start_date(self): + with self.subTest("returns parsed start date if supplied"): + start_date = timezone.now() + assert ChartQueryRunner( + self.user, + { + "owner_username": self.org.username, + "service": self.org.service, + "grouping_unit": "day", + "start_date": str(start_date), + }, + ).start_date == datetime.date(start_date) + + with self.subTest("returns first_commit_date if not supplied"): + repo = RepositoryFactory(author=self.org, active=True) + self.user.permission = [repo.repoid] + self.user.save() + commit = CommitFactory( + repository=repo, + branch=repo.branch, + state="complete", + timestamp=timezone.now() - timedelta(days=3), + ) + assert ChartQueryRunner( + self.user, + { + "owner_username": self.org.username, + "service": self.org.service, + "grouping_unit": "day", + }, + ).start_date == datetime.date(commit.timestamp) + + def test_end_date(self): + with self.subTest("returns parsed end date if supplied"): + end_date = timezone.now() - timedelta(days=7) + assert ChartQueryRunner( + self.user, + { + "owner_username": self.org.username, + "service": self.org.service, + "grouping_unit": "day", + "end_date": str(end_date), + }, + ).end_date == datetime.date(end_date) + + with self.subTest("returns timezone.now() if not supplied"): + assert ChartQueryRunner( + self.user, + { + "owner_username": self.org.username, + "service": self.org.service, + "grouping_unit": "day", + }, + ).end_date == datetime.date(timezone.now()) + + +@patch("api.shared.permissions.RepositoryPermissionsService.has_read_permissions") +class RepositoryCoverageChartTest(InternalAPITest): + def _retrieve(self, kwargs={}, data={}): + return self.client.post( + reverse("chart-coverage-repository", kwargs=kwargs), + data=data, + content_type="application/json", + ) + + def setUp(self): + self.org1 = OwnerFactory() + self.repo1_org1 = RepositoryFactory(author=self.org1) + setup_commits(self.repo1_org1, 10, start_date="-4d") + + self.current_owner = OwnerFactory( + service="github", + organizations=[self.org1.ownerid], + permission=[self.repo1_org1.repoid], + ) + + self.client = Client() + self.client.force_login_owner(self.current_owner) + + def test_no_permissions(self, mocked_get_permissions): + data = { + "branch": "main", + "start_date": timezone.now() - timedelta(7), + "end_date": timezone.now(), + "grouping_unit": "commit", + "repositories": [self.repo1_org1.name], + } + + kwargs = {"owner_username": self.org1.username, "service": "gh"} + + mocked_get_permissions.return_value = False + response = self._retrieve(kwargs=kwargs, data=data) + + # 404 for security to hide existence of repo + assert response.status_code == 404 + + # when "grouping_unit" is commit we just return all the commits with no grouping/aggregation + @pytest.mark.skip(reason="flaky, skipping until re write") + def test_get_commits_no_time_grouping(self, mocked_get_permissions): + data = { + "branch": "main", + "start_date": timezone.now() - timedelta(7), + "end_date": timezone.now(), + "grouping_unit": "commit", + "repositories": [self.repo1_org1.name], + } + + kwargs = {"owner_username": self.org1.username, "service": "gh"} + + mocked_get_permissions.return_value = True + response = self._retrieve(kwargs=kwargs, data=data) + + assert response.status_code == 200 + assert len(response.data["coverage"]) == 10 + assert len(response.data["complexity"]) == 10 + + def test_get_commits_with_time_grouping(self, mocked_get_permissions): + data = { + "branch": "main", + "start_date": timezone.now() - timedelta(7), + "end_date": timezone.now(), + "grouping_unit": "day", + "agg_function": "max", + "agg_value": "coverage", + "repositories": [self.repo1_org1.name], + } + + kwargs = {"owner_username": self.org1.username, "service": "gh"} + + mocked_get_permissions.return_value = True + response = self._retrieve(kwargs=kwargs, data=data) + + assert response.status_code == 200 + assert len(response.data["coverage"]) > 0 + assert len(response.data["complexity"]) > 0 + + def test_get_commits_with_coverage_change(self, mocked_get_permissions): + data = { + "branch": "main", + "start_date": timezone.now() - timedelta(7), + "end_date": timezone.now(), + "grouping_unit": "day", + "agg_function": "max", + "agg_value": "coverage", + "repositories": [self.repo1_org1.name], + } + + kwargs = {"owner_username": self.org1.username, "service": "gh"} + + mocked_get_permissions.return_value = True + response = self._retrieve(kwargs=kwargs, data=data) + + assert response.status_code == 200 + + assert len(response.data["coverage"]) > 1 + # Verify that the coverage change was properly computed + for index in range(len(response.data["coverage"])): + commit = response.data["coverage"][index] + + # First commit should always have change = 0 since it changed nothing + if index == 0: + assert commit["coverage_change"] == 0 + else: + assert ( + commit["coverage_change"] + == commit["coverage"] + - response.data["coverage"][index - 1]["coverage"] + ) + + +class TestOrganizationChartHandler(InternalAPITest): + def setUp(self): + self.org = OwnerFactory() + self.repo1 = RepositoryFactory(author=self.org, active=True) + self.repo2 = RepositoryFactory(author=self.org, active=True) + self.current_owner = OwnerFactory( + permission=[self.repo1.repoid, self.repo2.repoid] + ) + self.commit1 = CommitFactory( + repository=self.repo1, + totals={"h": 100, "n": 120, "p": 10, "m": 10}, + branch=self.repo1.branch, + state="complete", + ) + self.commit2 = CommitFactory( + repository=self.repo2, + totals={"h": 14, "n": 25, "p": 6, "m": 5}, + branch=self.repo2.branch, + state="complete", + ) + self.client = Client() + self.client.force_login_owner(self.current_owner) + + def _get(self, kwargs={}, data={}): + return self.client.get( + reverse("chart-coverage-organization", kwargs=kwargs), + data=data, + content_type="application/json", + ) + + def test_basic_success(self): + response = self._get( + kwargs={"owner_username": self.org.username, "service": self.org.service}, + data={ + "grouping_unit": "day", + "repositories": [self.repo1.name, self.repo2.name], + }, + ) + + assert response.status_code == 200 + assert len(response.data["coverage"]) == 1 + assert response.data["coverage"][0]["total_hits"] == 114 + assert response.data["coverage"][0]["total_lines"] == 145 + assert response.data["coverage"][0]["total_misses"] == 15 + assert response.data["coverage"][0]["total_partials"] == 16 diff --git a/apps/codecov-api/api/internal/tests/test_feature.py b/apps/codecov-api/api/internal/tests/test_feature.py new file mode 100644 index 0000000000..9712ce4f69 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/test_feature.py @@ -0,0 +1,230 @@ +import json + +import pytest +from django.urls import reverse +from rest_framework.test import APITestCase +from shared.django_apps.core.tests.factories import OwnerFactory +from shared.django_apps.rollouts.models import ( + FeatureFlag, + FeatureFlagVariant, + RolloutUniverse, +) + +from utils.test_utils import Client + + +class FeatureEndpointTests(APITestCase): + def setUp(self): + self.client = Client() + self.owner = OwnerFactory(plan="users-free", plan_user_count=5) + self.client.force_login_owner(self.owner) + + def send_feature_request(self, data: dict): + return self.client.post( + reverse("features"), data=json.dumps(data), content_type="application/json" + ) + + def test_invalid_request_body(self): + data = { + "feature_flagsssss": ["fjdsioj"], + "identifier_dataa": { + "email": "dsfio", + "user_id": 1, + "org_id": 2, + "repo_id": 3, + }, + } + + res = self.send_feature_request(data) + self.assertEqual(res.status_code, 400) + + def test_valid_request_body(self): + data = { + "feature_flags": [], + "identifier_data": { + "email": "daniel.yu@sentry.io", + "user_id": 0, + "org_id": 0, + "repo_id": 0, + }, + } + + res = self.send_feature_request(data) + self.assertEqual(res.status_code, 200) + + def test_variant_assigned_true(self): + feature_a = FeatureFlag.objects.create( + name="feature_a", proportion=1.0, salt="random_salt" + ) + FeatureFlagVariant.objects.create( + name="enabled", + feature_flag=feature_a, + proportion=1.0, + value=True, + ) + + feature_b = FeatureFlag.objects.create( + name="feature_b", proportion=1.0, salt="random_salt" + ) + FeatureFlagVariant.objects.create( + name="enabled", + feature_flag=feature_b, + proportion=1.0, + value=True, + ) + + data = { + "feature_flags": ["feature_a", "feature_b"], + "identifier_data": { + "email": "d", + "user_id": 1, + "org_id": 1, + "repo_id": 1, + }, + } + + res = self.send_feature_request(data) + + self.assertEqual(res.status_code, 200) + self.assertEqual(res.data["feature_a"], True) + self.assertEqual(res.data["feature_b"], True) + + def test_variant_assigned_false(self): + feature_aaa = FeatureFlag.objects.create( + name="feature_aaa", proportion=1.0, salt="random_salt" + ) + FeatureFlagVariant.objects.create( + name="disabled", + feature_flag=feature_aaa, + proportion=1.0, + value=False, + ) + + feature_bbb = FeatureFlag.objects.create( + name="feature_bbb", proportion=1.0, salt="random_salt" + ) + FeatureFlagVariant.objects.create( + name="disabled", + feature_flag=feature_bbb, + proportion=1.0, + value=False, + ) + + data = { + "feature_flags": ["feature_aaa", "feature_bbb"], + "identifier_data": { + "email": "d", + "user_id": 1, + "org_id": 1, + "repo_id": 1, + }, + } + + res = self.send_feature_request(data) + + self.assertEqual(res.status_code, 200) + self.assertEqual(res.data["feature_aaa"], False) + self.assertEqual(res.data["feature_bbb"], False) + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "rollout_universe,o_emails,o_owner_ids,o_repo_ids,o_org_ids,o_values", + [ + ( + RolloutUniverse.EMAIL, + (["david@gmail.com"], ["daniel@gmail.com"]), + ([], []), + ([], []), + ([], []), + (1, 2), + ), + ( + RolloutUniverse.OWNER_ID, + ([], []), + (["1"], ["2"]), + ([], []), + ([], []), + (3, 4), + ), + ( + RolloutUniverse.REPO_ID, + ([], []), + ([], []), + (["21"], ["31"]), + ([], []), + (5, 6), + ), + ( + RolloutUniverse.ORG_ID, + ([], []), + ([], []), + ([], []), + (["11"], ["21"]), + (7, 8), + ), + ], +) +def test_overrides_by_email( + rollout_universe, o_emails, o_owner_ids, o_repo_ids, o_org_ids, o_values +): + overrides = FeatureFlag.objects.create( + name="overrides_" + str(rollout_universe), + proportion=1.0, + rollout_universe=rollout_universe, + ) + FeatureFlagVariant.objects.create( + name="overrides_a", + feature_flag=overrides, + proportion=1 / 3, + value=o_values[0], + override_emails=o_emails[0], + override_owner_ids=o_owner_ids[0], + override_repo_ids=o_repo_ids[0], + override_org_ids=o_org_ids[0], + ) + FeatureFlagVariant.objects.create( + name="overrides_b", + feature_flag=overrides, + proportion=1 / 3, + value=o_values[1], + override_emails=o_emails[1], + override_owner_ids=o_owner_ids[1], + override_repo_ids=o_repo_ids[1], + override_org_ids=o_org_ids[1], + ) + FeatureFlagVariant.objects.create( + name="overrides_c", + feature_flag=overrides, + proportion=1 / 3, + value="dfjosijsdiofjdos", + ) + + data1 = { + "feature_flags": ["overrides_" + str(rollout_universe)], + "identifier_data": { + "email": o_emails[0][0] if o_emails[0] else "", + "user_id": o_owner_ids[0][0] if o_owner_ids[0] else 0, + "org_id": o_org_ids[0][0] if o_org_ids[0] else 0, + "repo_id": o_repo_ids[0][0] if o_repo_ids[0] else 0, + }, + } + mock = FeatureEndpointTests() + mock.setUp() + res1 = mock.send_feature_request(data1) + + data2 = { + "feature_flags": ["overrides_" + str(rollout_universe)], + "identifier_data": { + "email": o_emails[1][0] if o_emails[1] else "", + "user_id": o_owner_ids[1][0] if o_owner_ids[1] else 0, + "org_id": o_org_ids[1][0] if o_org_ids[1] else 0, + "repo_id": o_repo_ids[1][0] if o_repo_ids[1] else 0, + }, + } + res2 = mock.send_feature_request(data2) + + assert res1.status_code == 200 + assert res1.data["overrides_" + str(rollout_universe)] == o_values[0] + assert res2.status_code == 200 + assert res2.data["overrides_" + str(rollout_universe)] == o_values[1] diff --git a/apps/codecov-api/api/internal/tests/test_pagination.py b/apps/codecov-api/api/internal/tests/test_pagination.py new file mode 100644 index 0000000000..ca972f18c2 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/test_pagination.py @@ -0,0 +1,45 @@ +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.django_apps.codecov_auth.tests.factories import PlanFactory, TierFactory +from shared.django_apps.core.tests.factories import OwnerFactory +from shared.plan.constants import TierName + +from utils.test_utils import Client + + +class PageNumberPaginationTests(APITestCase): + def setUp(self): + self.client = Client() + tier = TierFactory(tier_name=TierName.BASIC.value) + plan = PlanFactory(tier=tier, is_active=True) + self.owner = OwnerFactory(plan=plan.name, plan_user_count=5) + self.users = [ + OwnerFactory(organizations=[self.owner.ownerid]), + OwnerFactory(organizations=[self.owner.ownerid]), + OwnerFactory(organizations=[self.owner.ownerid]), + ] + + def test_pagination_returned_page_size(self): + self.client.force_login_owner(self.owner) + + def _list(kwargs={}, query_params={}): + if not kwargs: + kwargs = { + "service": self.owner.service, + "owner_username": self.owner.username, + } + return self.client.get( + reverse("users-list", kwargs=kwargs), data=query_params + ) + + response = _list() + + assert response.data["total_pages"] == 1 + + response = _list(query_params={"page_size": "1"}) + + assert response.data["total_pages"] == 3 + + response = _list(query_params={"page_size": "100"}) + + assert response.data["total_pages"] == 1 diff --git a/apps/codecov-api/api/internal/tests/test_permissions.py b/apps/codecov-api/api/internal/tests/test_permissions.py new file mode 100644 index 0000000000..2b23d6753e --- /dev/null +++ b/apps/codecov-api/api/internal/tests/test_permissions.py @@ -0,0 +1,195 @@ +from unittest.mock import patch + +from django.test import TestCase, override_settings +from rest_framework.exceptions import APIException +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from api.internal.tests.test_utils import ( + GetAdminErrorProviderAdapter, + GetAdminProviderAdapter, +) +from api.shared.permissions import RepositoryPermissionsService, UserIsAdminPermissions + + +class MockedPermissionsAdapter: + async def get_authenticated(self): + return True, True + + +class TestRepositoryPermissionsService(TestCase): + def setUp(self): + self.permissions_service = RepositoryPermissionsService() + + def test_has_read_permissions_returns_true_if_user_is_owner(self): + owner = OwnerFactory() + repo = RepositoryFactory(author=owner) + assert self.permissions_service.has_read_permissions(owner, repo) is True + + def test_has_read_permissions_returns_true_if_repoid_in_permission_array(self): + repo = RepositoryFactory(author=OwnerFactory()) + owner = OwnerFactory(permission=[repo.repoid]) + assert self.permissions_service.has_read_permissions(owner, repo) is True + + def test_has_read_permissions_returns_true_if_repo_not_private(self): + repo = RepositoryFactory(private=False) + owner = OwnerFactory() + assert self.permissions_service.has_read_permissions(owner, repo) is True + + @patch( + "api.shared.permissions.RepositoryPermissionsService._fetch_provider_permissions" + ) + def test_has_read_permissions_gets_permissions_from_provider_if_above_conds_not_met( + self, fetch_mock + ): + fetch_mock.return_value = True, False + repo = RepositoryFactory() + owner = OwnerFactory() + + assert self.permissions_service.has_read_permissions(owner, repo) is True + + fetch_mock.assert_called_once_with(owner, repo) + + @patch("services.repo_providers.RepoProviderService.get_adapter") + def test_fetch_provider_permissions_fetches_permissions_from_provider( + self, mocked_provider + ): + mocked_provider.return_value = MockedPermissionsAdapter() + repo = RepositoryFactory() + owner = OwnerFactory() + + assert self.permissions_service._fetch_provider_permissions(owner, repo) == ( + True, + True, + ) + + @patch("services.repo_providers.RepoProviderService.get_adapter") + def test_fetch_provider_permissions_caches_read_permissions(self, mocked_provider): + mocked_provider.return_value = MockedPermissionsAdapter() + repo = RepositoryFactory() + owner = OwnerFactory() + self.permissions_service._fetch_provider_permissions(owner, repo) + + owner.refresh_from_db() + assert repo.repoid in owner.permission + + @patch("services.repo_providers.RepoProviderService.get_adapter") + def test_fetch_provider_permissions_caches_read_permissions_when_owner_has_no_permissions( + self, mocked_provider + ): + mocked_provider.return_value = MockedPermissionsAdapter() + repo = RepositoryFactory() + owner = OwnerFactory(permission=None) + self.permissions_service._fetch_provider_permissions(owner, repo) + + owner.refresh_from_db() + assert repo.repoid in owner.permission + + def test_user_is_activated_returns_false_if_user_not_in_owner_org(self): + with self.subTest("user orgs is None"): + user = OwnerFactory() + owner = OwnerFactory(plan="users-inappy") + assert self.permissions_service.user_is_activated(user, owner) is False + + with self.subTest("owner not in user orgs"): + owner = OwnerFactory(plan="users-inappy") + user = OwnerFactory(organizations=[]) + assert self.permissions_service.user_is_activated(user, owner) is False + + def test_user_is_activated_returns_false_if_owner_is_none(self): + user = OwnerFactory() + assert self.permissions_service.user_is_activated(user, None) is False + + def test_user_is_activated_returns_false_if_user_is_none(self): + owner = OwnerFactory() + assert self.permissions_service.user_is_activated(None, owner) is False + + def test_user_is_activated_returns_true_when_owner_has_legacy_plan(self): + user = OwnerFactory() + owner = OwnerFactory(plan="v4-50m") + assert self.permissions_service.user_is_activated(user, owner) is True + + def test_user_is_activated_returns_true_when_user_is_owner(self): + user = OwnerFactory() + assert self.permissions_service.user_is_activated(user, user) is True + + def test_user_is_activated_returns_true_if_user_is_activated(self): + user = OwnerFactory() + owner = OwnerFactory(plan="users-inappy", plan_activated_users=[user.ownerid]) + user.organizations = [owner.ownerid] + user.save() + + assert self.permissions_service.user_is_activated(user, owner) is True + + def test_user_is_activated_activates_user_and_returns_true_if_can_auto_activate( + self, + ): + user = OwnerFactory() + owner = OwnerFactory( + plan="users-inappy", plan_auto_activate=True, plan_user_count=1 + ) + user.organizations = [owner.ownerid] + user.save() + + assert self.permissions_service.user_is_activated(user, owner) is True + + owner.refresh_from_db() + assert user.ownerid in owner.plan_activated_users + + @patch("services.self_hosted.license_seats") + @override_settings(IS_ENTERPRISE=True) + def test_user_is_activated_when_self_hosted(self, license_seats): + license_seats.return_value = 5 + + user = OwnerFactory() + owner = OwnerFactory(plan_auto_activate=True) + user.organizations = [owner.ownerid] + user.save() + + assert self.permissions_service.user_is_activated(user, owner) is True + + owner.refresh_from_db() + assert user.ownerid in owner.plan_activated_users + + def test_user_is_activated_returns_false_if_cant_auto_activate(self): + owner = OwnerFactory(plan="users-inappy", plan_user_count=10) + user = OwnerFactory(organizations=[owner.ownerid]) + + with self.subTest("auto activate set to false"): + owner.plan_auto_activate = False + owner.save() + assert self.permissions_service.user_is_activated(user, owner) is False + + with self.subTest("auto activate true but not enough seats"): + owner.plan_auto_activate = True + owner.plan_user_count = 0 + owner.save() + assert self.permissions_service.user_is_activated(user, owner) is False + + +class TestUserIsAdminPermissions(TestCase): + def setUp(self): + self.permissions_class = UserIsAdminPermissions() + + @patch("api.shared.permissions.get_provider") + def test_is_admin_on_provider_invokes_torngit_adapter_when_user_not_in_admin_array( + self, mocked_get_adapter + ): + org = OwnerFactory() + user = OwnerFactory() + + mocked_get_adapter.return_value = GetAdminProviderAdapter() + self.permissions_class._is_admin_on_provider(user, org) + assert mocked_get_adapter.return_value.last_call_args == { + "username": user.username, + "service_id": user.service_id, + } + + @patch("api.shared.permissions.get_provider") + def test_is_admin_on_provider_handles_torngit_exception(self, mock_get_provider): + code, message = 404, "uh oh" + mock_get_provider.return_value = GetAdminErrorProviderAdapter(code, message) + org = OwnerFactory() + user = OwnerFactory() + + with self.assertRaises(APIException): + self.permissions_class._is_admin_on_provider(user, org) diff --git a/apps/codecov-api/api/internal/tests/test_repo_accessors.py b/apps/codecov-api/api/internal/tests/test_repo_accessors.py new file mode 100644 index 0000000000..2ac448146b --- /dev/null +++ b/apps/codecov-api/api/internal/tests/test_repo_accessors.py @@ -0,0 +1,89 @@ +from unittest.mock import patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory +from shared.torngit.exceptions import TorngitClientError, TorngitClientGeneralError + +from api.shared.repo.repository_accessors import RepoAccessors + + +class RepositoryAccessorsTestCase(TestCase): + def setUp(self): + self.org = OwnerFactory(username="codecov", service="github") + + self.repo1 = RepositoryFactory( + author=self.org, active=True, private=True, name="A" + ) + self.repo2 = RepositoryFactory( + author=self.org, active=True, private=True, name="B" + ) + + self.user = OwnerFactory( + username="codecov-user", service="github", organizations=[self.org.ownerid] + ) + + self.client.force_login(user=self.user) + + def test_get_repo_permissions_when_author(self): + user = OwnerFactory(username="myself", service="github") + repo = RepositoryFactory(author=user, active=True, private=True, name="A") + can_view, can_edit = RepoAccessors().get_repo_permissions(user, repo) + assert (can_view, can_edit) == (True, True) + + def test_get_repo_details_if_exists(self): + repo = RepoAccessors.get_repo_details( + self, self.user, self.repo1.name, self.org.username, self.org.service + ) + self.assertEqual(repo, self.repo1) + + def test_get_repo_details_if_not_exists(self): + repo = RepoAccessors.get_repo_details( + self, self.user, "repo-not-in-db", self.org.username, self.org.service + ) + self.assertEqual(repo, None) + + @patch("services.repo_providers.RepoProviderService.get_by_name") + def test_fetch_and_create_repo(self, mocked_repo_provider_service): + git_repo_response = { + "repo": { + "name": "new-repo", + "branch": "default", + "private": True, + "service_id": "7293846", + "fork": { + "repo": { + "name": "fork-repo", + "branch": "master", + "private": True, + "service_id": "4720394", + }, + "owner": {"username": "fork_owner", "service_id": "0956093"}, + }, + }, + "owner": {"username": "new-org", "service_id": "9437469"}, + } + + class MockedRepoService: + async def get_repository(self): + return git_repo_response + + mocked_repo_provider_service.return_value = MockedRepoService() + + repo = RepoAccessors.fetch_from_git_and_create_repo( + self, self.user, "new-repo", "new-org", "github" + ) + assert repo.name == git_repo_response["repo"]["name"] + assert repo.fork is not None + + @patch("services.repo_providers.RepoProviderService") + def test_fetch_and_create_repo_if_torngit_error(self, mocked_repo_provider_service): + class MockedRepoService: + async def get_repository(self): + raise TorngitClientGeneralError(404, response=None, message="Not Found") + + mocked_repo_provider_service.return_value = MockedRepoService() + + with self.assertRaises(TorngitClientError): + RepoAccessors.fetch_from_git_and_create_repo( + self, self.user, "repo-not-in-db", self.org.username, self.org.service + ) diff --git a/apps/codecov-api/api/internal/tests/test_utils.py b/apps/codecov-api/api/internal/tests/test_utils.py new file mode 100644 index 0000000000..f0c52477c4 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/test_utils.py @@ -0,0 +1,45 @@ +from shared.torngit.exceptions import TorngitClientGeneralError + + +def to_drf_datetime_str(datetime): + """ + DRF does custom datetime representation, which makes comparing + expected timestamps in tests really annoying. This function tries + to mimic DRF datetime representation using ISO-8601 format, minus + reading from gobal formatting settings. + + source: https://github.com/encode/django-rest-framework/blob/aed74961ba03e3e6f53c468353f4e255eb788555/rest_framework/fields.py#L1227 + """ + value = datetime.isoformat() + if value.endswith("+00:00"): + value = value[:-6] + "Z" + return value + + +class GetAdminProviderAdapter: + """ + Mock adapter providing the `get_is_admin` coroutine, which returns `self.result`. + """ + + def __init__(self, result=False): + self.result = result + self.last_call_args = None + + async def get_is_admin(self, user): + self.last_call_args = user + return self.result + + +class GetAdminErrorProviderAdapter: + """ + Mock adapter that raises a torngit error. + """ + + def __init__(self, code, message): + self.code = code + self.message = message + + async def get_is_admin(self, user): + raise TorngitClientGeneralError( + self.code, response_data=None, message=self.message + ) diff --git a/apps/codecov-api/api/internal/tests/test_views.py b/apps/codecov-api/api/internal/tests/test_views.py new file mode 100644 index 0000000000..8ee8a1c5ef --- /dev/null +++ b/apps/codecov-api/api/internal/tests/test_views.py @@ -0,0 +1,742 @@ +import json +from unittest.mock import patch + +from rest_framework import status +from rest_framework.reverse import reverse +from shared.django_apps.core.tests.factories import ( + BranchFactory, + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) + +from codecov.tests.base_test import InternalAPITest +from utils.test_utils import Client + +get_permissions_method = ( + "api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions" +) + + +@patch(get_permissions_method) +class RepoPullList(InternalAPITest): + def setUp(self): + self.org = OwnerFactory(username="codecov", service="github") + other_org = OwnerFactory(username="other_org") + # Create different types of repos / pulls + self.repo = RepositoryFactory(author=self.org, name="testRepoName", active=True) + other_repo = RepositoryFactory( + author=other_org, name="otherRepoName", active=True + ) + repo_with_permission = [self.repo.repoid] + self.current_owner = OwnerFactory( + username="codecov-user", + service="github", + organizations=[self.org.ownerid], + permission=repo_with_permission, + ) + PullFactory( + pullid=10, + author=self.org, + repository=self.repo, + state="open", + head=CommitFactory( + repository=self.repo, author=self.current_owner + ).commitid, + base=CommitFactory( + repository=self.repo, author=self.current_owner + ).commitid, + ) + PullFactory(pullid=11, author=self.org, repository=self.repo, state="closed") + PullFactory(pullid=12, author=other_org, repository=other_repo) + self.correct_kwargs = { + "service": "github", + "owner_username": "codecov", + "repo_name": "testRepoName", + } + self.incorrect_kwargs = { + "service": "github", + "owner_username": "codecov", + "repo_name": "otherRepoName", + } + + self.client = Client() + self.client.force_login_owner(self.current_owner) + + def test_can_get_public_repo_pulls_when_not_authenticated(self, mock_provider): + self.client.logout() + mock_provider.return_value = True, True + author = OwnerFactory() + repo = RepositoryFactory(private=False, author=author) + response = self.client.get( + reverse( + "pulls-list", + kwargs={ + "service": author.service, + "owner_username": author.username, + "repo_name": repo.name, + }, + ) + ) + assert response.status_code == 200 + assert response.data["results"] == [] + + def test_get_pulls(self, mock_provider): + mock_provider.return_value = True, True + response = self.client.get(reverse("pulls-list", kwargs=self.correct_kwargs)) + self.assertEqual(response.status_code, 200) + content = self.json_content(response) + self.assertEqual( + len(content["results"]), + 3, + "got the wrong number of pulls: {}".format(content["results"]), + ) + + def test_get_pulls_no_permissions(self, mock_provider): + mock_provider.return_value = False, False + self.current_owner.permission = [] + self.current_owner.save() + response = self.client.get(reverse("pulls-list", kwargs=self.correct_kwargs)) + self.assertEqual(response.status_code, 404) + + def test_get_pulls_filter_state(self, mock_provider): + mock_provider.return_value = True, True + response = self.client.get( + reverse("pulls-list", kwargs=self.correct_kwargs), data={"state": "open"} + ) + self.assertEqual(response.status_code, 200) + content = self.json_content(response) + self.assertEqual( + len(content["results"]), + 2, + "got the wrong number of open pulls: {}".format(content["results"]), + ) + + def test_get_pulls_ordered_by_pullid(self, mock_provider): + mock_provider.return_value = True, True + # Test increasing ordering + response = self.client.get( + reverse("pulls-list", kwargs=self.correct_kwargs), + data={"ordering": "pullid"}, + ) + content = self.json_content(response) + pullids = [r["pullid"] for r in content["results"]] + self.assertEqual(pullids, [1, 10, 11]) + # Test decreasing ordering + response = self.client.get( + reverse("pulls-list", kwargs=self.correct_kwargs), + data={"ordering": "-pullid"}, + ) + content = self.json_content(response) + pullids = [r["pullid"] for r in content["results"]] + self.assertEqual(pullids, [11, 10, 1]) + + def test_get_pulls_default_ordering(self, mock_provider): + mock_provider.return_value = True, True + # Test default ordering + response = self.client.get(reverse("pulls-list", kwargs=self.correct_kwargs)) + content = self.json_content(response) + pullids = [r["pullid"] for r in content["results"]] + self.assertEqual(pullids, [11, 10, 1]) + + def test_get_pull_wrong_org(self, mock_provider): + mock_provider.return_value = True, True + response = self.client.get(reverse("pulls-list", kwargs=self.incorrect_kwargs)) + content = self.json_content(response) + self.assertEqual( + response.status_code, 404, "got unexpected response: {}".format(content) + ) + + def test_pulls_list_returns_most_recent_commiter(self, mock_provider): + mock_provider.return_value = True, True + response = self.client.get(reverse("pulls-list", kwargs=self.correct_kwargs)) + + assert ( + response.data["results"][1]["most_recent_commiter"] + == self.current_owner.username + ) + + def test_get_pulls_null_head_author_doesnt_crash(self, mock_provider): + mock_provider.return_value = True, True + new_owner = OwnerFactory() + + PullFactory( + pullid=13, + author=self.org, + repository=self.repo, + state="open", + head=CommitFactory(repository=self.repo, author=new_owner).commitid, + base=CommitFactory(repository=self.repo, author=new_owner).commitid, + ) + + new_owner.delete() + + response = self.client.get(reverse("pulls-list", kwargs=self.correct_kwargs)) + assert response.status_code == status.HTTP_200_OK + + def test_get_pulls_no_head_commit_returns_null_for_head_totals(self, mock_provider): + mock_provider.return_value = True, True + + PullFactory( + pullid=13, + author=self.org, + repository=self.repo, + state="open", + head="", + base=CommitFactory( + repository=self.repo, author=self.current_owner + ).commitid, + ) + + response = self.client.get(reverse("pulls-list", kwargs=self.correct_kwargs)) + assert response.status_code == status.HTTP_200_OK + assert [p for p in response.data["results"] if p["pullid"] == 13][0][ + "head_totals" + ] is None + + def test_get_pulls_no_base_commit_returns_null_for_base_totals(self, mock_provider): + mock_provider.return_value = True, True + + PullFactory( + pullid=13, + author=self.org, + repository=self.repo, + state="open", + base="", + head=CommitFactory( + repository=self.repo, author=self.current_owner + ).commitid, + ) + + response = self.client.get(reverse("pulls-list", kwargs=self.correct_kwargs)) + assert response.status_code == status.HTTP_200_OK + assert [p for p in response.data["results"] if p["pullid"] == 13][0][ + "base_totals" + ] is None + + def test_get_pulls_as_inactive_user_returns_403(self, mock_provider): + self.org.plan = "users-inappm" + self.org.plan_auto_activate = False + self.org.save() + response = self.client.get(reverse("pulls-list", kwargs=self.correct_kwargs)) + assert response.status_code == 403 + + def test_list_pulls_comparedto_not_base(self, mock_provider): + repo = RepositoryFactory.create( + author=self.org, name="test_list_pulls_comparedto_not_base", active=True + ) + repo.save() + user = OwnerFactory.create( + username="test_list_pulls_comparedto_not_base", + service="github", + organizations=[self.org.ownerid], + permission=[repo.repoid], + ) + user.save() + mock_provider.return_value = True, True + pull = PullFactory.create( + pullid=101, + author=self.org, + repository=repo, + state="open", + head=CommitFactory(repository=repo, author=user, pullid=None).commitid, + base=CommitFactory( + repository=repo, pullid=None, author=user, totals=None + ).commitid, + compared_to=CommitFactory( + pullid=None, + repository=repo, + author=user, + totals={ + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "30.00000", + "d": 0, + "diff": [1, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + "f": 3, + "h": 6, + "m": 10, + "n": 20, + "p": 4, + "s": 1, + }, + ).commitid, + ) + response = self.client.get( + reverse( + "pulls-list", + kwargs={ + "service": "github", + "owner_username": "codecov", + "repo_name": "test_list_pulls_comparedto_not_base", + }, + ) + ) + self.assertEqual(response.status_code, 200) + content = self.json_content(response) + expected_content = { + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "pullid": 101, + "title": pull.title, + "most_recent_commiter": "test_list_pulls_comparedto_not_base", + "base_totals": { + "files": 3, + "lines": 20, + "hits": 6, + "misses": 10, + "partials": 4, + "coverage": 30.0, + "branches": 0, + "methods": 0, + "sessions": 1, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "head_totals": { + "files": 3, + "lines": 20, + "hits": 17, + "misses": 3, + "partials": 0, + "coverage": 85.0, + "branches": 0, + "methods": 0, + "sessions": 1, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + # This whole TZ settings is messing things up a bit + "updatestamp": pull.updatestamp.replace(tzinfo=None).isoformat() + + "Z", + "state": "open", + "ci_passed": True, + } + ], + "total_pages": 1, + } + assert content["results"][0] == expected_content["results"][0] + assert content == expected_content + + +@patch(get_permissions_method) +class RepoPullDetail(InternalAPITest): + def setUp(self): + self.org = OwnerFactory(username="codecov", service="github") + other_org = OwnerFactory(username="other_org") + # Create different types of repos / pulls + repo = RepositoryFactory(author=self.org, name="testRepoName", active=True) + RepositoryFactory(author=other_org, name="otherRepoName", active=True) + repo_with_permission = [repo.repoid] + self.current_owner = OwnerFactory( + username="codecov-user", + service="github", + organizations=[self.org.ownerid], + permission=repo_with_permission, + ) + PullFactory(pullid=10, author=self.org, repository=repo, state="open") + PullFactory(pullid=11, author=self.org, repository=repo, state="closed") + + self.client = Client() + self.client.force_login_owner(self.current_owner) + + def test_can_get_public_repo_pull_detail_when_not_authenticated( + self, mock_provider + ): + self.client.logout() + mock_provider.return_value = True, True + author = OwnerFactory() + repo = RepositoryFactory(private=False, author=author) + pull = PullFactory(repository=repo) + response = self.client.get( + reverse( + "pulls-detail", + kwargs={ + "service": author.service, + "owner_username": author.username, + "repo_name": repo.name, + "pullid": pull.pullid, + }, + ) + ) + assert response.status_code == 200 + assert response.data["pullid"] == pull.pullid + + def test_get_pull(self, mock_provider): + mock_provider.return_value = True, True + response = self.client.get("/internal/github/codecov/testRepoName/pulls/10/") + self.assertEqual(response.status_code, 200) + content = self.json_content(response) + self.assertEqual(content["pullid"], 10) + + def test_get_pull_no_permissions(self, mock_provider): + self.current_owner.permission = [] + self.current_owner.save() + mock_provider.return_value = False, False + response = self.client.get("/internal/github/codecov/testRepoName/pulls/10/") + self.assertEqual(response.status_code, 404) + + def test_get_pull_as_inactive_user_returns_403(self, mock_provider): + self.org.plan = "users-inappm" + self.org.plan_auto_activate = False + self.org.save() + response = self.client.get("/internal/github/codecov/testRepoName/pulls/10/") + assert response.status_code == 403 + + +@patch(get_permissions_method) +class RepoCommitList(InternalAPITest): + def setUp(self): + self.org = OwnerFactory(username="codecov", service="github") + other_org = OwnerFactory(username="other_org") + # Create different types of repos / commits + self.repo = RepositoryFactory(author=self.org, name="testRepoName", active=True) + other_repo = RepositoryFactory( + author=other_org, name="otherRepoName", active=True + ) + repo_with_permission = [self.repo.repoid] + self.current_owner = OwnerFactory( + username="codecov-user", + service="github", + organizations=[self.org.ownerid], + permission=repo_with_permission, + ) + self.first_test_commit = CommitFactory( + author=self.org, + repository=self.repo, + totals={ + "C": 2, + "M": 0, + "N": 5, + "b": 0, + "c": "79.16667", + "d": 0, + "f": 3, + "h": 19, + "m": 5, + "n": 24, + "p": 0, + "s": 2, + "diff": 0, + }, + ) + self.second_test_commit = CommitFactory( + author=self.org, + repository=self.repo, + totals={ + "C": 3, + "M": 0, + "N": 5, + "b": 0, + "c": "79.16667", + "d": 0, + "f": 3, + "h": 19, + "m": 5, + "n": 24, + "p": 0, + "s": 2, + "diff": 0, + }, + ) + self.third_test_commit = CommitFactory( + author=other_org, + repository=other_repo, + totals={ + "C": 3, + "M": 0, + "N": 6, + "b": 0, + "c": "79.16667", + "d": 0, + "f": 3, + "h": 19, + "m": 5, + "n": 24, + "p": 0, + "s": 2, + "diff": 0, + }, + ) + + self.client = Client() + self.client.force_login_owner(self.current_owner) + + def test_can_get_public_repo_commits_if_not_authenticated(self, mocked_provider): + mocked_provider.return_value = True, True + self.client.logout() + author = OwnerFactory() + repo = RepositoryFactory(author=author, private=False) + response = self.client.get( + reverse( + "commits-list", + kwargs={ + "service": author.service, + "owner_username": author.username, + "repo_name": repo.name, + }, + ) + ) + assert response.status_code == 200 + + # TODO: Improve this test to not assert the pagination data + def test_get_commits(self, mock_provider): + mock_provider.return_value = True, True + response = self.client.get("/internal/github/codecov/testRepoName/commits/") + self.assertEqual(response.status_code, 200) + content = self.json_content(response) + self.assertEqual( + len(content["results"]), + 2, + "got the wrong number of commits: {}".format(content["results"]), + ) + expected_result = { + "count": 2, + "next": None, + "previous": None, + "total_pages": 1, + "results": [ + { + "commitid": self.second_test_commit.commitid, + "message": self.second_test_commit.message, + "timestamp": self.second_test_commit.timestamp.strftime( + "%Y-%m-%dT%H:%M:%S.%fZ" + ), + "ci_passed": self.second_test_commit.ci_passed, + "author": { + "service": self.org.service, + "username": self.org.username, + "avatar_url": self.org.avatar_url, + "stats": self.org.cache["stats"] + if self.org.cache and "stats" in self.org.cache + else None, + "name": self.org.name, + "ownerid": self.org.ownerid, + "integration_id": self.org.integration_id, + }, + "branch": self.second_test_commit.branch, + "totals": { + "branches": 0, + "complexity": 3.0, + "complexity_total": 5.0, + "complexity_ratio": 60.0, + "coverage": 79.16, + "diff": 0, + "files": 3, + "hits": 19, + "lines": 24, + "methods": 0, + "misses": 5, + "partials": 0, + "sessions": 2, + }, + "state": self.second_test_commit.state, + }, + { + "commitid": self.first_test_commit.commitid, + "message": self.first_test_commit.message, + "timestamp": self.first_test_commit.timestamp.strftime( + "%Y-%m-%dT%H:%M:%S.%fZ" + ), + "ci_passed": self.first_test_commit.ci_passed, + "author": { + "service": self.org.service, + "username": self.org.username, + "avatar_url": self.org.avatar_url, + "stats": self.org.cache["stats"] + if self.org.cache and "stats" in self.org.cache + else None, + "name": self.org.name, + "ownerid": self.org.ownerid, + "integration_id": self.org.integration_id, + }, + "branch": self.first_test_commit.branch, + "totals": { + "branches": 0, + "complexity": 2.0, + "complexity_total": 5.0, + "complexity_ratio": 40.0, + "coverage": 79.16, + "diff": 0, + "files": 3, + "hits": 19, + "lines": 24, + "methods": 0, + "misses": 5, + "partials": 0, + "sessions": 2, + }, + "state": self.first_test_commit.state, + }, + ], + } + assert content == expected_result + + def test_get_commits_wrong_org(self, mock_provider): + response = self.client.get("/internal/github/codecov/otherRepoName/commits/") + content = self.json_content(response) + self.assertEqual( + response.status_code, 404, "got unexpected response: {}".format(content) + ) + + def test_filters_by_branch_name(self, mock_provider): + mock_provider.return_value = True, True + repo = RepositoryFactory( + author=self.current_owner, active=True, private=True, name="banana" + ) + CommitFactory.create( + message="test_commits_base", + commitid="9193232a8fe3429496956ba82b5fed2583d1b5ec", + repository=repo, + ) + commit_non_master = CommitFactory.create( + message="another_commit_not_on_master", + commitid="ddcc232a8fe3429496956ba82b5fed2583d1b5ec", + repository=repo, + branch="other-branch", + ) + + response = self.client.get("/internal/github/codecov-user/banana/commits/") + content = json.loads(response.content.decode()) + assert len(content["results"]) == 2 + assert content["results"][0]["commitid"] == commit_non_master.commitid + + response = self.client.get( + "/internal/github/codecov-user/banana/commits/?branch=other-branch" + ) + content = json.loads(response.content.decode()) + assert len(content["results"]) == 1 + assert content["results"][0]["commitid"] == commit_non_master.commitid + + def test_fetch_commits_no_permissions(self, mock_provider): + mock_provider.return_value = False, False + self.current_owner.permission = [] + self.current_owner.save() + + response = self.client.get("/internal/github/codecov/testRepoName/commits/") + + assert response.status_code == 404 + + def test_fetch_commits_inactive_user_returns_403(self, mock_provider): + self.org.plan = "users-inappm" + self.org.plan_auto_activate = False + self.org.save() + + response = self.client.get("/internal/github/codecov/testRepoName/commits/") + + assert response.status_code == 403 + + +@patch(get_permissions_method) +class BranchViewSetTests(InternalAPITest): + def setUp(self): + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org) + self.current_owner = OwnerFactory( + permission=[self.repo.repoid], organizations=[self.org.ownerid] + ) + self.other_user = OwnerFactory(permission=[self.repo.repoid]) + + self.branches = [ + BranchFactory(repository=self.repo, name="foo"), + BranchFactory(repository=self.repo, name="bar"), + ] + + self.client = Client() + self.client.force_login_owner(self.current_owner) + + def _get_branches(self, kwargs={}, query={}): + if not kwargs: + kwargs = { + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + } + return self.client.get(reverse("branches-list", kwargs=kwargs), data=query) + + def test_can_get_public_repo_branches_if_not_authenticated(self, mocked_provider): + mocked_provider.return_value = True, True + self.client.logout() + author = OwnerFactory() + repo = RepositoryFactory(author=author, private=False) + response = self._get_branches( + kwargs={ + "service": author.service, + "owner_username": author.username, + "repo_name": repo.name, + } + ) + assert response.status_code == 200 + + def test_list_returns_200_and_expected_branches(self, mock_provider): + response = self._get_branches() + assert response.status_code == 200 + assert response.data["results"][0]["name"] == self.branches[1].name + assert response.data["results"][1]["name"] == self.branches[0].name + + def test_list_without_permission_returns_403(self, mock_provider): + mock_provider.return_value = False, False + repo_no_permissions = RepositoryFactory(author=self.org) + response = self._get_branches( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": repo_no_permissions.name, + } + ) + assert response.status_code == 404 + + def test_list_with_nonexistent_repo_returns_404(self, mock_provider): + nonexistent_repo_name = "existant" + response = self._get_branches( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": nonexistent_repo_name, + } + ) + assert response.status_code == 404 + + def test_branch_data_includes_most_recent_commiter_of_each_branch( + self, mock_provider + ): + self.branches[0].head = CommitFactory( + repository=self.repo, + author=self.current_owner, + branch=self.branches[0].name, + ).commitid + self.branches[0].save() + self.branches[1].head = CommitFactory( + repository=self.repo, author=self.other_user, branch=self.branches[1].name + ).commitid + self.branches[1].save() + + response = self._get_branches() + + assert ( + response.data["results"][0]["most_recent_commiter"] + == self.other_user.username + ) + assert ( + response.data["results"][1]["most_recent_commiter"] + == self.current_owner.username + ) + + def test_list_as_inactive_user_returns_403(self, mock_provider): + self.org.plan = "users-inappy" + self.org.plan_auto_activate = False + self.org.save() + + response = self._get_branches() + + assert response.status_code == 403 diff --git a/apps/codecov-api/api/internal/tests/unit/views/cassetes/test_compare_file_view/TestCompareSingleFileChangesView/test_compare_file_src_accepts_pullid_query_parameter.yaml b/apps/codecov-api/api/internal/tests/unit/views/cassetes/test_compare_file_view/TestCompareSingleFileChangesView/test_compare_file_src_accepts_pullid_query_parameter.yaml new file mode 100644 index 0000000000..004e59e96d --- /dev/null +++ b/apps/codecov-api/api/internal/tests/unit/views/cassetes/test_compare_file_view/TestCompareSingleFileChangesView/test_compare_file_src_accepts_pullid_query_parameter.yaml @@ -0,0 +1,97 @@ +interactions: +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/tests/unit/adder/test_adder.py?ref=48a4883890d71d777794ed998a1ad4d24a9ebab5 + response: + body: {string: '{"name":"test_adder.py","path":"tests/unit/adder/test_adder.py","sha":"abf2ea13871a916997281e1273d28b3c3c72e818","size":251,"url":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/tests/unit/adder/test_adder.py?ref=48a4883890d71d777794ed998a1ad4d24a9ebab5","html_url":"https://github.com/eyoel-cov/codecov-assume-flag-test/blob/48a4883890d71d777794ed998a1ad4d24a9ebab5/tests/unit/adder/test_adder.py","git_url":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/git/blobs/abf2ea13871a916997281e1273d28b3c3c72e818","download_url":"https://raw.githubusercontent.com/eyoel-cov/codecov-assume-flag-test/48a4883890d71d777794ed998a1ad4d24a9ebab5/tests/unit/adder/test_adder.py?token=test1f92hh3qe9dbdpw0bgqfu4l1s","type":"file","content":"aW1wb3J0IHB5dGVzdApmcm9tIHNyYy5hZGRlci5hZGRlcnMgaW1wb3J0IEFk\nZGVyCgpkZWYgdGVzdF9zdW1fdHdvX3BsdXNfdHdvX2lzX2ZvdXIoKToKICAg\nIGFzc2VydCBBZGRlcigpLmFkZCgzLDMpID09IDYKCmRlZiB0ZXN0X3N1bV90\nd29fcGx1c190d29faXNfbm90X2ZpdmUoKToKICAgIGFzc2VydCBBZGRlcigp\nLmFkZCgzLDQpICE9IDYKCmRlZiB0ZXN0X2FkZDJfd2l0aF9mb3VyKCk6Cglh\nc3NlcnQgQWRkZXIoKS5hZGQyKDQpID09IDY=\n","encoding":"base64","_links":{"self":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/tests/unit/adder/test_adder.py?ref=48a4883890d71d777794ed998a1ad4d24a9ebab5","git":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/git/blobs/abf2ea13871a916997281e1273d28b3c3c72e818","html":"https://github.com/eyoel-cov/codecov-assume-flag-test/blob/48a4883890d71d777794ed998a1ad4d24a9ebab5/tests/unit/adder/test_adder.py"}}'} + headers: + - !!python/tuple + - Date + - ['Tue, 17 Sep 2019 23:50:24 GMT'] + - !!python/tuple + - Content-Type + - [application/json; charset=utf-8] + - !!python/tuple + - Transfer-Encoding + - [chunked] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Server + - [GitHub.com] + - !!python/tuple + - Status + - [200 OK] + - !!python/tuple + - X-Ratelimit-Limit + - ['5000'] + - !!python/tuple + - X-Ratelimit-Remaining + - ['4999'] + - !!python/tuple + - X-Ratelimit-Reset + - ['1568767823'] + - !!python/tuple + - Cache-Control + - ['private, max-age=60, s-maxage=60'] + - !!python/tuple + - Vary + - ['Accept, Authorization, Cookie, X-GitHub-OTP', Accept-Encoding] + - !!python/tuple + - Etag + - [W/"abf2ea13871a916997281e1273d28b3c3c72e818"] + - !!python/tuple + - Last-Modified + - ['Wed, 28 Aug 2019 02:06:22 GMT'] + - !!python/tuple + - X-Oauth-Scopes + - ['read:org, repo, user:email, write:repo_hook'] + - !!python/tuple + - X-Accepted-Oauth-Scopes + - [''] + - !!python/tuple + - X-Oauth-Client-Id + - [3d44be0e772666136a13] + - !!python/tuple + - X-Github-Media-Type + - [github.v3] + - !!python/tuple + - Access-Control-Expose-Headers + - ['ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type'] + - !!python/tuple + - Access-Control-Allow-Origin + - ['*'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000; includeSubdomains; preload] + - !!python/tuple + - X-Frame-Options + - [deny] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Xss-Protection + - [1; mode=block] + - !!python/tuple + - Referrer-Policy + - ['origin-when-cross-origin, strict-origin-when-cross-origin'] + - !!python/tuple + - Content-Security-Policy + - [default-src 'none'] + - !!python/tuple + - X-Github-Request-Id + - ['CCB3:2D00:9AFF35:10BD1F8:5D81713F'] + - !!python/tuple + - X-Consumed-Content-Encoding + - [gzip] + status: {code: 200, message: OK} + url: https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/tests/unit/adder/test_adder.py?ref=48a4883890d71d777794ed998a1ad4d24a9ebab5 +version: 1 diff --git a/apps/codecov-api/api/internal/tests/unit/views/cassetes/test_compare_file_view/TestCompareSingleFileChangesView/test_fetch_file_cov_decrease___success.yaml b/apps/codecov-api/api/internal/tests/unit/views/cassetes/test_compare_file_view/TestCompareSingleFileChangesView/test_fetch_file_cov_decrease___success.yaml new file mode 100644 index 0000000000..1f8f25a2fe --- /dev/null +++ b/apps/codecov-api/api/internal/tests/unit/views/cassetes/test_compare_file_view/TestCompareSingleFileChangesView/test_fetch_file_cov_decrease___success.yaml @@ -0,0 +1,251 @@ +interactions: +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/src/subtractor/subtractor.py?ref=f64b22e6af4bbe3917ac4a7c346feb03493c2b67 + response: + body: {string: '{"name":"subtractor.py","path":"src/subtractor/subtractor.py","sha":"75e0c8aabc2ff179e022d8262aa32ab844a1a12c","size":277,"url":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/src/subtractor/subtractor.py?ref=f64b22e6af4bbe3917ac4a7c346feb03493c2b67","html_url":"https://github.com/eyoel-cov/codecov-assume-flag-test/blob/f64b22e6af4bbe3917ac4a7c346feb03493c2b67/src/subtractor/subtractor.py","git_url":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/git/blobs/75e0c8aabc2ff179e022d8262aa32ab844a1a12c","download_url":"https://raw.githubusercontent.com/eyoel-cov/codecov-assume-flag-test/f64b22e6af4bbe3917ac4a7c346feb03493c2b67/src/subtractor/subtractor.py?token=testv485nsrwfeixy8agrq8k3s64j","type":"file","content":"aW1wb3J0IG1hdGgKCmNsYXNzIFN1YnRyYWN0b3Iob2JqZWN0KToKICAgIGRl\nZiBzdWJ0cmFjdChzZWxmLCB4LCB5KToKICAgICAgICByZXR1cm4geCAtIHkK\nICAgIAogICAgZGVmIGRpdmlkZShzZWxmLCB4LCB5KToKICAgICAgICByZXR1\ncm4gZmxvYXQoeCkgLyBmbG9hdCh5KQogICAgCiAgICBkZWYgZnJhY3Rpb25h\ndGUoc2VsZiwgeCk6CiAgICAgICAgcmV0dXJuIDEgLyBmbG9hdCh4KQogICAg\nCiAgICBkZWYgaGFsZihzZWxmLCB4KToKICAgICAgICByZXR1cm4gZmxvYXQo\neCkgLyAyCg==\n","encoding":"base64","_links":{"self":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/src/subtractor/subtractor.py?ref=f64b22e6af4bbe3917ac4a7c346feb03493c2b67","git":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/git/blobs/75e0c8aabc2ff179e022d8262aa32ab844a1a12c","html":"https://github.com/eyoel-cov/codecov-assume-flag-test/blob/f64b22e6af4bbe3917ac4a7c346feb03493c2b67/src/subtractor/subtractor.py"}}'} + headers: + - !!python/tuple + - Date + - ['Wed, 28 Aug 2019 06:00:52 GMT'] + - !!python/tuple + - Content-Type + - [application/json; charset=utf-8] + - !!python/tuple + - Transfer-Encoding + - [chunked] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Server + - [GitHub.com] + - !!python/tuple + - Status + - [200 OK] + - !!python/tuple + - X-Ratelimit-Limit + - ['5000'] + - !!python/tuple + - X-Ratelimit-Remaining + - ['4871'] + - !!python/tuple + - X-Ratelimit-Reset + - ['1566975458'] + - !!python/tuple + - Cache-Control + - ['private, max-age=60, s-maxage=60'] + - !!python/tuple + - Vary + - ['Accept, Authorization, Cookie, X-GitHub-OTP', Accept-Encoding] + - !!python/tuple + - Etag + - [W/"75e0c8aabc2ff179e022d8262aa32ab844a1a12c"] + - !!python/tuple + - Last-Modified + - ['Wed, 28 Aug 2019 02:16:58 GMT'] + - !!python/tuple + - X-Oauth-Scopes + - ['read:org, repo, user:email, write:repo_hook'] + - !!python/tuple + - X-Accepted-Oauth-Scopes + - [''] + - !!python/tuple + - X-Oauth-Client-Id + - [3d44be0e772666136a13] + - !!python/tuple + - X-Github-Media-Type + - [github.v3] + - !!python/tuple + - Access-Control-Expose-Headers + - ['ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type'] + - !!python/tuple + - Access-Control-Allow-Origin + - ['*'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000; includeSubdomains; preload] + - !!python/tuple + - X-Frame-Options + - [deny] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Xss-Protection + - [1; mode=block] + - !!python/tuple + - Referrer-Policy + - ['origin-when-cross-origin, strict-origin-when-cross-origin'] + - !!python/tuple + - Content-Security-Policy + - [default-src 'none'] + - !!python/tuple + - X-Github-Request-Id + - ['E5CF:467C:37F6F6:427F28:5D661893'] + - !!python/tuple + - X-Consumed-Content-Encoding + - [gzip] + status: {code: 200, message: OK} + url: https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/src/subtractor/subtractor.py?ref=f64b22e6af4bbe3917ac4a7c346feb03493c2b67 +- request: + body: null + headers: + Host: ['minio:9000'] + User-Agent: [Minio (Linux; x86_64) minio-py/4.0.8] + X-Amz-Content-Sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + X-Amz-Date: [20190828T060051Z] + method: PUT + uri: http://minio:9000/archive/ + response: + body: {string: ' + + BucketAlreadyOwnedByYouYour previous request + to create the named bucket succeeded and you already own it./archive/15BF00C75F7EF9903L137'} + headers: + Accept-Ranges: [bytes] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [application/xml] + Date: ['Wed, 28 Aug 2019 06:00:51 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Transfer-Encoding: [chunked] + Vary: [Origin] + X-Amz-Request-Id: [15BF00C75F7EF990] + X-Xss-Protection: [1; mode=block] + status: {code: 409, message: Conflict} +- request: + body: null + headers: + Host: ['minio:9000'] + X-Amz-Content-Sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + X-Amz-Date: [20190828T060051Z] + method: GET + uri: http://minio:9000/archive?location= + response: + body: {string: ' + + '} + headers: + Accept-Ranges: [bytes] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [application/xml] + Date: ['Wed, 28 Aug 2019 06:00:51 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Transfer-Encoding: [chunked] + Vary: [Origin] + X-Amz-Request-Id: [15BF00C75FFEDAAC] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} +- request: + body: null + headers: + host: ['minio:9000'] + user-agent: [Minio (Linux; x86_64) minio-py/4.0.8] + x-amz-content-sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + x-amz-date: [20190828T060051Z] + method: GET + uri: http://minio:9000/archive/v4/repos/4A3A4B0C7B00EAEA3F77AA7EB90BE4AB/commits/791886122b02a2e52e5dd09e8be5f92e09e83f0b/chunks.txt + response: + body: + string: !!binary | + H4sIAAkPZl0C/6uu5Yo21FHIK83J0VGIjjbQUTCMjY3FKsZFC5UDabkNCCik5qXE56fFJ2eU5mUr + 2IEAV3UtDTxAK1cRbTB9nD/w/sSRArDrN0ARMwCKAQAa5Y0UEgMAAA== + headers: + Accept-Ranges: [bytes] + Content-Encoding: [gzip] + Content-Length: ['97'] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [text/plain] + Date: ['Wed, 28 Aug 2019 06:00:51 GMT'] + Etag: ['"018949c5c5d6b0fbf4f15fc9b51f07b7"'] + Expires: ['1566969619'] + Last-Modified: ['Wed, 28 Aug 2019 05:20:09 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Vary: [Origin] + X-Amz-Request-Id: [15BF00C760610E20] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} +- request: + body: null + headers: + Host: ['minio:9000'] + User-Agent: [Minio (Linux; x86_64) minio-py/4.0.8] + X-Amz-Content-Sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + X-Amz-Date: [20190828T060051Z] + method: PUT + uri: http://minio:9000/archive/ + response: + body: {string: ' + + BucketAlreadyOwnedByYouYour previous request + to create the named bucket succeeded and you already own it./archive/15BF00C7616075B83L137'} + headers: + Accept-Ranges: [bytes] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [application/xml] + Date: ['Wed, 28 Aug 2019 06:00:51 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Transfer-Encoding: [chunked] + Vary: [Origin] + X-Amz-Request-Id: [15BF00C7616075B8] + X-Xss-Protection: [1; mode=block] + status: {code: 409, message: Conflict} +- request: + body: null + headers: + Host: ['minio:9000'] + X-Amz-Content-Sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + X-Amz-Date: [20190828T060051Z] + method: GET + uri: http://minio:9000/archive?location= + response: + body: {string: ' + + '} + headers: + Accept-Ranges: [bytes] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [application/xml] + Date: ['Wed, 28 Aug 2019 06:00:51 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Transfer-Encoding: [chunked] + Vary: [Origin] + X-Amz-Request-Id: [15BF00C761D9DCA0] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} +- request: + body: null + headers: + host: ['minio:9000'] + user-agent: [Minio (Linux; x86_64) minio-py/4.0.8] + x-amz-content-sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + x-amz-date: [20190828T060051Z] + method: GET + uri: http://minio:9000/archive/v4/repos/4A3A4B0C7B00EAEA3F77AA7EB90BE4AB/commits/f64b22e6af4bbe3917ac4a7c346feb03493c2b67/chunks.txt + response: + body: + string: !!binary | + H4sIALMUZl0C/6uu5Yo21FHIK83J0VGIjjbQUTCMjY3FKkZ9hTYgoJCalxKfnxafnFGal61gBwJc + 1SS4apDaRorKQeBZog0mXrMBipjB4EhClMYKsV4FAFtPdXVYAwAA + headers: + Accept-Ranges: [bytes] + Content-Encoding: [gzip] + Content-Length: ['96'] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [text/plain] + Date: ['Wed, 28 Aug 2019 06:00:51 GMT'] + Etag: ['"75e94db29362b38856098010597b4c55"'] + Expires: ['1566971069'] + Last-Modified: ['Wed, 28 Aug 2019 05:44:19 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Vary: [Origin] + X-Amz-Request-Id: [15BF00C7623B1EAC] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} +version: 1 diff --git a/apps/codecov-api/api/internal/tests/unit/views/cassetes/test_compare_file_view/TestCompareSingleFileChangesView/test_fetch_file_with_diff_change.yaml b/apps/codecov-api/api/internal/tests/unit/views/cassetes/test_compare_file_view/TestCompareSingleFileChangesView/test_fetch_file_with_diff_change.yaml new file mode 100644 index 0000000000..a3c617e721 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/unit/views/cassetes/test_compare_file_view/TestCompareSingleFileChangesView/test_fetch_file_with_diff_change.yaml @@ -0,0 +1,251 @@ +interactions: +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/tests/unit/adder/test_adder.py?ref=48a4883890d71d777794ed998a1ad4d24a9ebab5 + response: + body: {string: '{"name":"test_adder.py","path":"tests/unit/adder/test_adder.py","sha":"abf2ea13871a916997281e1273d28b3c3c72e818","size":251,"url":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/tests/unit/adder/test_adder.py?ref=48a4883890d71d777794ed998a1ad4d24a9ebab5","html_url":"https://github.com/eyoel-cov/codecov-assume-flag-test/blob/48a4883890d71d777794ed998a1ad4d24a9ebab5/tests/unit/adder/test_adder.py","git_url":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/git/blobs/abf2ea13871a916997281e1273d28b3c3c72e818","download_url":"https://raw.githubusercontent.com/eyoel-cov/codecov-assume-flag-test/48a4883890d71d777794ed998a1ad4d24a9ebab5/tests/unit/adder/test_adder.py?token=testadwrd6hmwu6i6vcbmabkmlz45","type":"file","content":"aW1wb3J0IHB5dGVzdApmcm9tIHNyYy5hZGRlci5hZGRlcnMgaW1wb3J0IEFk\nZGVyCgpkZWYgdGVzdF9zdW1fdHdvX3BsdXNfdHdvX2lzX2ZvdXIoKToKICAg\nIGFzc2VydCBBZGRlcigpLmFkZCgzLDMpID09IDYKCmRlZiB0ZXN0X3N1bV90\nd29fcGx1c190d29faXNfbm90X2ZpdmUoKToKICAgIGFzc2VydCBBZGRlcigp\nLmFkZCgzLDQpICE9IDYKCmRlZiB0ZXN0X2FkZDJfd2l0aF9mb3VyKCk6Cglh\nc3NlcnQgQWRkZXIoKS5hZGQyKDQpID09IDY=\n","encoding":"base64","_links":{"self":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/tests/unit/adder/test_adder.py?ref=48a4883890d71d777794ed998a1ad4d24a9ebab5","git":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/git/blobs/abf2ea13871a916997281e1273d28b3c3c72e818","html":"https://github.com/eyoel-cov/codecov-assume-flag-test/blob/48a4883890d71d777794ed998a1ad4d24a9ebab5/tests/unit/adder/test_adder.py"}}'} + headers: + - !!python/tuple + - Date + - ['Wed, 28 Aug 2019 06:27:03 GMT'] + - !!python/tuple + - Content-Type + - [application/json; charset=utf-8] + - !!python/tuple + - Transfer-Encoding + - [chunked] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Server + - [GitHub.com] + - !!python/tuple + - Status + - [200 OK] + - !!python/tuple + - X-Ratelimit-Limit + - ['5000'] + - !!python/tuple + - X-Ratelimit-Remaining + - ['4971'] + - !!python/tuple + - X-Ratelimit-Reset + - ['1566976200'] + - !!python/tuple + - Cache-Control + - ['private, max-age=60, s-maxage=60'] + - !!python/tuple + - Vary + - ['Accept, Authorization, Cookie, X-GitHub-OTP', Accept-Encoding] + - !!python/tuple + - Etag + - [W/"abf2ea13871a916997281e1273d28b3c3c72e818"] + - !!python/tuple + - Last-Modified + - ['Wed, 28 Aug 2019 02:06:22 GMT'] + - !!python/tuple + - X-Oauth-Scopes + - ['read:org, repo, user:email, write:repo_hook'] + - !!python/tuple + - X-Accepted-Oauth-Scopes + - [''] + - !!python/tuple + - X-Oauth-Client-Id + - [3d44be0e772666136a13] + - !!python/tuple + - X-Github-Media-Type + - [github.v3] + - !!python/tuple + - Access-Control-Expose-Headers + - ['ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type'] + - !!python/tuple + - Access-Control-Allow-Origin + - ['*'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000; includeSubdomains; preload] + - !!python/tuple + - X-Frame-Options + - [deny] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Xss-Protection + - [1; mode=block] + - !!python/tuple + - Referrer-Policy + - ['origin-when-cross-origin, strict-origin-when-cross-origin'] + - !!python/tuple + - Content-Security-Policy + - [default-src 'none'] + - !!python/tuple + - X-Github-Request-Id + - ['FFDB:8C50:30C734:3942EA:5D661EB7'] + - !!python/tuple + - X-Consumed-Content-Encoding + - [gzip] + status: {code: 200, message: OK} + url: https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/tests/unit/adder/test_adder.py?ref=48a4883890d71d777794ed998a1ad4d24a9ebab5 +- request: + body: null + headers: + Host: ['minio:9000'] + User-Agent: [Minio (Linux; x86_64) minio-py/4.0.8] + X-Amz-Content-Sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + X-Amz-Date: [20190828T062703Z] + method: PUT + uri: http://minio:9000/archive/ + response: + body: {string: ' + + BucketAlreadyOwnedByYouYour previous request + to create the named bucket succeeded and you already own it./archive/15BF023545A9FD983L137'} + headers: + Accept-Ranges: [bytes] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [application/xml] + Date: ['Wed, 28 Aug 2019 06:27:03 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Transfer-Encoding: [chunked] + Vary: [Origin] + X-Amz-Request-Id: [15BF023545A9FD98] + X-Xss-Protection: [1; mode=block] + status: {code: 409, message: Conflict} +- request: + body: null + headers: + Host: ['minio:9000'] + X-Amz-Content-Sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + X-Amz-Date: [20190828T062703Z] + method: GET + uri: http://minio:9000/archive?location= + response: + body: {string: ' + + '} + headers: + Accept-Ranges: [bytes] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [application/xml] + Date: ['Wed, 28 Aug 2019 06:27:03 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Transfer-Encoding: [chunked] + Vary: [Origin] + X-Amz-Request-Id: [15BF0235472C75EC] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} +- request: + body: null + headers: + host: ['minio:9000'] + user-agent: [Minio (Linux; x86_64) minio-py/4.0.8] + x-amz-content-sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + x-amz-date: [20190828T062703Z] + method: GET + uri: http://minio:9000/archive/v4/repos/4A3A4B0C7B00EAEA3F77AA7EB90BE4AB/commits/791886122b02a2e52e5dd09e8be5f92e09e83f0b/chunks.txt + response: + body: + string: !!binary | + H4sIAAkPZl0C/6uu5Yo21FHIK83J0VGIjjbQUTCMjY3FKsZFC5UDabkNCCik5qXE56fFJ2eU5mUr + 2IEAV3UtDTxAK1cRbTB9nD/w/sSRArDrN0ARMwCKAQAa5Y0UEgMAAA== + headers: + Accept-Ranges: [bytes] + Content-Encoding: [gzip] + Content-Length: ['97'] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [text/plain] + Date: ['Wed, 28 Aug 2019 06:27:03 GMT'] + Etag: ['"018949c5c5d6b0fbf4f15fc9b51f07b7"'] + Expires: ['1566969619'] + Last-Modified: ['Wed, 28 Aug 2019 05:20:09 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Vary: [Origin] + X-Amz-Request-Id: [15BF0235486EC8C4] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} +- request: + body: null + headers: + Host: ['minio:9000'] + User-Agent: [Minio (Linux; x86_64) minio-py/4.0.8] + X-Amz-Content-Sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + X-Amz-Date: [20190828T062703Z] + method: PUT + uri: http://minio:9000/archive/ + response: + body: {string: ' + + BucketAlreadyOwnedByYouYour previous request + to create the named bucket succeeded and you already own it./archive/15BF02354A8276243L137'} + headers: + Accept-Ranges: [bytes] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [application/xml] + Date: ['Wed, 28 Aug 2019 06:27:03 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Transfer-Encoding: [chunked] + Vary: [Origin] + X-Amz-Request-Id: [15BF02354A827624] + X-Xss-Protection: [1; mode=block] + status: {code: 409, message: Conflict} +- request: + body: null + headers: + Host: ['minio:9000'] + X-Amz-Content-Sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + X-Amz-Date: [20190828T062703Z] + method: GET + uri: http://minio:9000/archive?location= + response: + body: {string: ' + + '} + headers: + Accept-Ranges: [bytes] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [application/xml] + Date: ['Wed, 28 Aug 2019 06:27:03 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Transfer-Encoding: [chunked] + Vary: [Origin] + X-Amz-Request-Id: [15BF02354BAE2480] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} +- request: + body: null + headers: + host: ['minio:9000'] + user-agent: [Minio (Linux; x86_64) minio-py/4.0.8] + x-amz-content-sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + x-amz-date: [20190828T062703Z] + method: GET + uri: http://minio:9000/archive/v4/repos/4A3A4B0C7B00EAEA3F77AA7EB90BE4AB/commits/48a4883890d71d777794ed998a1ad4d24a9ebab5/chunks.txt + response: + body: + string: !!binary | + H4sIAMfiZV0C/6uu5Yo21FHIK83J0VGIjjbQUTCMjY3FKjaACm1AQCE1LyU+Py0+OaM0L1vBDgS4 + qklw/iC1jRSVg8CzRBtMvGYDFDGDwZGEKI0VqnsVABdi/36qAwAA + headers: + Accept-Ranges: [bytes] + Content-Encoding: [gzip] + Content-Length: ['96'] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [text/plain] + Date: ['Wed, 28 Aug 2019 06:27:03 GMT'] + Etag: ['"033429e66a86d5d4aabcbcfef61e7a4e"'] + Expires: ['1566958289'] + Last-Modified: ['Wed, 28 Aug 2019 02:11:19 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Vary: [Origin] + X-Amz-Request-Id: [15BF02354C87C820] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} +version: 1 diff --git a/apps/codecov-api/api/internal/tests/unit/views/cassetes/test_compare_file_view/TestCompareSingleFileChangesView/test_fetch_file_with_filename_change.yaml b/apps/codecov-api/api/internal/tests/unit/views/cassetes/test_compare_file_view/TestCompareSingleFileChangesView/test_fetch_file_with_filename_change.yaml new file mode 100644 index 0000000000..c9f408db14 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/unit/views/cassetes/test_compare_file_view/TestCompareSingleFileChangesView/test_fetch_file_with_filename_change.yaml @@ -0,0 +1,346 @@ +interactions: +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/src/adder/adders.py?ref=f64b22e6af4bbe3917ac4a7c346feb03493c2b67 + response: + body: {string: '{"name":"adders.py","path":"src/adder/adders.py","sha":"0e949ad4302ff0b9bbc8a6bc5d33a2b66eda2524","size":254,"url":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/src/adder/adders.py?ref=f64b22e6af4bbe3917ac4a7c346feb03493c2b67","html_url":"https://github.com/eyoel-cov/codecov-assume-flag-test/blob/f64b22e6af4bbe3917ac4a7c346feb03493c2b67/src/adder/adders.py","git_url":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/git/blobs/0e949ad4302ff0b9bbc8a6bc5d33a2b66eda2524","download_url":"https://raw.githubusercontent.com/eyoel-cov/codecov-assume-flag-test/f64b22e6af4bbe3917ac4a7c346feb03493c2b67/src/adder/adders.py?token=testvo6iptaqeppdciif0dnbm14cl","type":"file","content":"aW1wb3J0IG1hdGgKCmNsYXNzIEFkZGVyKG9iamVjdCk6CiAgICBkZWYgYWRk\nKHNlbGYsIHgsIHkpOgogICAgICAgICMgYSBsaW5lIC0tCiAgICAgICAgIyBh\nbm90aGVyIGxpbmUKICAgICAgICAjIGEgdGhpcmQgbGluZQogICAgICAgIHJl\ndHVybiB4ICsgeQogICAgICAgIAogICAgZGVmIG11bHRpcGx5KHNlbGYsIHgs\nIHkpOgogICAgICAgIHJldHVybiB4ICogeQogICAgCiAgICBkZWYgYWRkMihz\nZWxmLCB4KToKICAgICAgICByZXR1cm4geCArIDI=\n","encoding":"base64","_links":{"self":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/src/adder/adders.py?ref=f64b22e6af4bbe3917ac4a7c346feb03493c2b67","git":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/git/blobs/0e949ad4302ff0b9bbc8a6bc5d33a2b66eda2524","html":"https://github.com/eyoel-cov/codecov-assume-flag-test/blob/f64b22e6af4bbe3917ac4a7c346feb03493c2b67/src/adder/adders.py"}}'} + headers: + - !!python/tuple + - Date + - ['Wed, 28 Aug 2019 06:02:31 GMT'] + - !!python/tuple + - Content-Type + - [application/json; charset=utf-8] + - !!python/tuple + - Transfer-Encoding + - [chunked] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Server + - [GitHub.com] + - !!python/tuple + - Status + - [200 OK] + - !!python/tuple + - X-Ratelimit-Limit + - ['5000'] + - !!python/tuple + - X-Ratelimit-Remaining + - ['4870'] + - !!python/tuple + - X-Ratelimit-Reset + - ['1566975458'] + - !!python/tuple + - Cache-Control + - ['private, max-age=60, s-maxage=60'] + - !!python/tuple + - Vary + - ['Accept, Authorization, Cookie, X-GitHub-OTP', Accept-Encoding] + - !!python/tuple + - Etag + - [W/"0e949ad4302ff0b9bbc8a6bc5d33a2b66eda2524"] + - !!python/tuple + - Last-Modified + - ['Wed, 28 Aug 2019 02:16:58 GMT'] + - !!python/tuple + - X-Oauth-Scopes + - ['read:org, repo, user:email, write:repo_hook'] + - !!python/tuple + - X-Accepted-Oauth-Scopes + - [''] + - !!python/tuple + - X-Oauth-Client-Id + - [3d44be0e772666136a13] + - !!python/tuple + - X-Github-Media-Type + - [github.v3] + - !!python/tuple + - Access-Control-Expose-Headers + - ['ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type'] + - !!python/tuple + - Access-Control-Allow-Origin + - ['*'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000; includeSubdomains; preload] + - !!python/tuple + - X-Frame-Options + - [deny] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Xss-Protection + - [1; mode=block] + - !!python/tuple + - Referrer-Policy + - ['origin-when-cross-origin, strict-origin-when-cross-origin'] + - !!python/tuple + - Content-Security-Policy + - [default-src 'none'] + - !!python/tuple + - X-Github-Request-Id + - ['E76D:9A4C:15292E:192CD7:5D6618F7'] + - !!python/tuple + - X-Consumed-Content-Encoding + - [gzip] + status: {code: 200, message: OK} + url: https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/src/adder/adders.py?ref=f64b22e6af4bbe3917ac4a7c346feb03493c2b67 +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/src/adder/adder.py?ref=791886122b02a2e52e5dd09e8be5f92e09e83f0b + response: + body: {string: '{"name":"adder.py","path":"src/adder/adder.py","sha":"ab899c4500a413d034be1df900727113859ced40","size":206,"url":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/src/adder/adder.py?ref=791886122b02a2e52e5dd09e8be5f92e09e83f0b","html_url":"https://github.com/eyoel-cov/codecov-assume-flag-test/blob/791886122b02a2e52e5dd09e8be5f92e09e83f0b/src/adder/adder.py","git_url":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/git/blobs/ab899c4500a413d034be1df900727113859ced40","download_url":"https://raw.githubusercontent.com/eyoel-cov/codecov-assume-flag-test/791886122b02a2e52e5dd09e8be5f92e09e83f0b/src/adder/adder.py?token=test30t9gcnxo8gnj976iajbx62v9","type":"file","content":"aW1wb3J0IG1hdGgKCmNsYXNzIEFkZGVyKG9iamVjdCk6CiAgICBkZWYgYWRk\nKHNlbGYsIHgsIHkpOgogICAgICAgICMgYSBsaW5lIC0tCiAgICAgICAgIyBh\nbm90aGVyIGxpbmUKICAgICAgICAjIGEgdGhpcmQgbGluZQogICAgICAgIHJl\ndHVybiB4ICsgeQogICAgICAgIAogICAgZGVmIG11bHRpcGx5KHNlbGYsIHgs\nIHkpOgogICAgICAgIHJldHVybiB4ICogeQo=\n","encoding":"base64","_links":{"self":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/src/adder/adder.py?ref=791886122b02a2e52e5dd09e8be5f92e09e83f0b","git":"https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/git/blobs/ab899c4500a413d034be1df900727113859ced40","html":"https://github.com/eyoel-cov/codecov-assume-flag-test/blob/791886122b02a2e52e5dd09e8be5f92e09e83f0b/src/adder/adder.py"}}'} + headers: + - !!python/tuple + - Date + - ['Wed, 28 Aug 2019 06:02:31 GMT'] + - !!python/tuple + - Content-Type + - [application/json; charset=utf-8] + - !!python/tuple + - Transfer-Encoding + - [chunked] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Server + - [GitHub.com] + - !!python/tuple + - Status + - [200 OK] + - !!python/tuple + - X-Ratelimit-Limit + - ['5000'] + - !!python/tuple + - X-Ratelimit-Remaining + - ['4869'] + - !!python/tuple + - X-Ratelimit-Reset + - ['1566975458'] + - !!python/tuple + - Cache-Control + - ['private, max-age=60, s-maxage=60'] + - !!python/tuple + - Vary + - ['Accept, Authorization, Cookie, X-GitHub-OTP', Accept-Encoding] + - !!python/tuple + - Etag + - [W/"ab899c4500a413d034be1df900727113859ced40"] + - !!python/tuple + - Last-Modified + - ['Tue, 28 May 2019 12:12:04 GMT'] + - !!python/tuple + - X-Oauth-Scopes + - ['read:org, repo, user:email, write:repo_hook'] + - !!python/tuple + - X-Accepted-Oauth-Scopes + - [''] + - !!python/tuple + - X-Oauth-Client-Id + - [3d44be0e772666136a13] + - !!python/tuple + - X-Github-Media-Type + - [github.v3] + - !!python/tuple + - Access-Control-Expose-Headers + - ['ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type'] + - !!python/tuple + - Access-Control-Allow-Origin + - ['*'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000; includeSubdomains; preload] + - !!python/tuple + - X-Frame-Options + - [deny] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Xss-Protection + - [1; mode=block] + - !!python/tuple + - Referrer-Policy + - ['origin-when-cross-origin, strict-origin-when-cross-origin'] + - !!python/tuple + - Content-Security-Policy + - [default-src 'none'] + - !!python/tuple + - X-Github-Request-Id + - ['E772:2500:3B1B1A:463989:5D6618F7'] + - !!python/tuple + - X-Consumed-Content-Encoding + - [gzip] + status: {code: 200, message: OK} + url: https://api.github.com/repos/eyoel-cov/codecov-assume-flag-test/contents/src/adder/adder.py?ref=791886122b02a2e52e5dd09e8be5f92e09e83f0b +- request: + body: null + headers: + Host: ['minio:9000'] + User-Agent: [Minio (Linux; x86_64) minio-py/4.0.8] + X-Amz-Content-Sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + X-Amz-Date: [20190828T060231Z] + method: PUT + uri: http://minio:9000/archive/ + response: + body: {string: ' + + BucketAlreadyOwnedByYouYour previous request + to create the named bucket succeeded and you already own it./archive/15BF00DE9F51482C3L137'} + headers: + Accept-Ranges: [bytes] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [application/xml] + Date: ['Wed, 28 Aug 2019 06:02:31 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Transfer-Encoding: [chunked] + Vary: [Origin] + X-Amz-Request-Id: [15BF00DE9F51482C] + X-Xss-Protection: [1; mode=block] + status: {code: 409, message: Conflict} +- request: + body: null + headers: + Host: ['minio:9000'] + X-Amz-Content-Sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + X-Amz-Date: [20190828T060231Z] + method: GET + uri: http://minio:9000/archive?location= + response: + body: {string: ' + + '} + headers: + Accept-Ranges: [bytes] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [application/xml] + Date: ['Wed, 28 Aug 2019 06:02:31 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Transfer-Encoding: [chunked] + Vary: [Origin] + X-Amz-Request-Id: [15BF00DE9FD8F36C] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} +- request: + body: null + headers: + host: ['minio:9000'] + user-agent: [Minio (Linux; x86_64) minio-py/4.0.8] + x-amz-content-sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + x-amz-date: [20190828T060231Z] + method: GET + uri: http://minio:9000/archive/v4/repos/4A3A4B0C7B00EAEA3F77AA7EB90BE4AB/commits/791886122b02a2e52e5dd09e8be5f92e09e83f0b/chunks.txt + response: + body: + string: !!binary | + H4sIAAkPZl0C/6uu5Yo21FHIK83J0VGIjjbQUTCMjY3FKsZFC5UDabkNCCik5qXE56fFJ2eU5mUr + 2IEAV3UtDTxAK1cRbTB9nD/w/sSRArDrN0ARMwCKAQAa5Y0UEgMAAA== + headers: + Accept-Ranges: [bytes] + Content-Encoding: [gzip] + Content-Length: ['97'] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [text/plain] + Date: ['Wed, 28 Aug 2019 06:02:31 GMT'] + Etag: ['"018949c5c5d6b0fbf4f15fc9b51f07b7"'] + Expires: ['1566969619'] + Last-Modified: ['Wed, 28 Aug 2019 05:20:09 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Vary: [Origin] + X-Amz-Request-Id: [15BF00DEA039C87C] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} +- request: + body: null + headers: + Host: ['minio:9000'] + User-Agent: [Minio (Linux; x86_64) minio-py/4.0.8] + X-Amz-Content-Sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + X-Amz-Date: [20190828T060231Z] + method: PUT + uri: http://minio:9000/archive/ + response: + body: {string: ' + + BucketAlreadyOwnedByYouYour previous request + to create the named bucket succeeded and you already own it./archive/15BF00DEA124A9003L137'} + headers: + Accept-Ranges: [bytes] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [application/xml] + Date: ['Wed, 28 Aug 2019 06:02:31 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Transfer-Encoding: [chunked] + Vary: [Origin] + X-Amz-Request-Id: [15BF00DEA124A900] + X-Xss-Protection: [1; mode=block] + status: {code: 409, message: Conflict} +- request: + body: null + headers: + Host: ['minio:9000'] + X-Amz-Content-Sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + X-Amz-Date: [20190828T060231Z] + method: GET + uri: http://minio:9000/archive?location= + response: + body: {string: ' + + '} + headers: + Accept-Ranges: [bytes] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [application/xml] + Date: ['Wed, 28 Aug 2019 06:02:31 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Transfer-Encoding: [chunked] + Vary: [Origin] + X-Amz-Request-Id: [15BF00DEA1893028] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} +- request: + body: null + headers: + host: ['minio:9000'] + user-agent: [Minio (Linux; x86_64) minio-py/4.0.8] + x-amz-content-sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + x-amz-date: [20190828T060231Z] + method: GET + uri: http://minio:9000/archive/v4/repos/4A3A4B0C7B00EAEA3F77AA7EB90BE4AB/commits/f64b22e6af4bbe3917ac4a7c346feb03493c2b67/chunks.txt + response: + body: + string: !!binary | + H4sIALMUZl0C/6uu5Yo21FHIK83J0VGIjjbQUTCMjY3FKkZ9hTYgoJCalxKfnxafnFGal61gBwJc + 1SS4apDaRorKQeBZog0mXrMBipjB4EhClMYKsV4FAFtPdXVYAwAA + headers: + Accept-Ranges: [bytes] + Content-Encoding: [gzip] + Content-Length: ['96'] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [text/plain] + Date: ['Wed, 28 Aug 2019 06:02:31 GMT'] + Etag: ['"75e94db29362b38856098010597b4c55"'] + Expires: ['1566971069'] + Last-Modified: ['Wed, 28 Aug 2019 05:44:19 GMT'] + Server: [Minio/RELEASE.2018-08-02T23-11-36Z (linux; amd64)] + Vary: [Origin] + X-Amz-Request-Id: [15BF00DEA1F06BE4] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} +version: 1 diff --git a/apps/codecov-api/api/internal/tests/unit/views/cassetes/test_compare_view/TestCompareDetailsView/test_compare_line_coverage_view.yaml b/apps/codecov-api/api/internal/tests/unit/views/cassetes/test_compare_view/TestCompareDetailsView/test_compare_line_coverage_view.yaml new file mode 100644 index 0000000000..6004175b57 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/unit/views/cassetes/test_compare_view/TestCompareDetailsView/test_compare_line_coverage_view.yaml @@ -0,0 +1,74 @@ +interactions: +- request: + body: null + headers: + sdfkjgt + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/compare/9193232a8fe3429496956ba82b5fed2583d1b5eb...abf6d4df662c47e32460020ab14abf9303581429 + response: + body: {string: '{"message":"Bad credentials","documentation_url":"https://developer.github.com/v3"}'} + headers: + - !!python/tuple + - Server + - [GitHub.com] + - !!python/tuple + - Date + - ['Mon, 16 Sep 2019 18:36:06 GMT'] + - !!python/tuple + - Content-Type + - [application/json; charset=utf-8] + - !!python/tuple + - Content-Length + - ['83'] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Status + - [401 Unauthorized] + - !!python/tuple + - X-Github-Media-Type + - [github.v3] + - !!python/tuple + - X-Ratelimit-Limit + - ['60'] + - !!python/tuple + - X-Ratelimit-Remaining + - ['57'] + - !!python/tuple + - X-Ratelimit-Reset + - ['1568662022'] + - !!python/tuple + - Access-Control-Expose-Headers + - ['ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type'] + - !!python/tuple + - Access-Control-Allow-Origin + - ['*'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000; includeSubdomains; preload] + - !!python/tuple + - X-Frame-Options + - [deny] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Xss-Protection + - [1; mode=block] + - !!python/tuple + - Referrer-Policy + - ['origin-when-cross-origin, strict-origin-when-cross-origin'] + - !!python/tuple + - Content-Security-Policy + - [default-src 'none'] + - !!python/tuple + - X-Github-Request-Id + - ['F95D:3B2F:6A7076:BDB881:5D7FD616'] + status: {code: 401, message: Unauthorized} + url: https://api.github.com/repos/ThiagoCodecov/example-python/compare/9193232a8fe3429496956ba82b5fed2583d1b5eb...abf6d4df662c47e32460020ab14abf9303581429 +version: 1 diff --git a/apps/codecov-api/api/internal/tests/unit/views/test_compare_flags_view.py b/apps/codecov-api/api/internal/tests/unit/views/test_compare_flags_view.py new file mode 100644 index 0000000000..94ff0b235a --- /dev/null +++ b/apps/codecov-api/api/internal/tests/unit/views/test_compare_flags_view.py @@ -0,0 +1,422 @@ +from pathlib import Path +from unittest.mock import PropertyMock, patch + +from rest_framework.reverse import reverse +from shared.django_apps.core.tests.factories import ( + CommitWithReportFactory, + PullFactory, + RepositoryFactory, +) +from shared.reports.types import ReportTotals + +from codecov.tests.base_test import InternalAPITest + +current_file = Path(__file__) + + +@patch("services.comparison.Comparison.git_comparison", new_callable=PropertyMock) +@patch("shared.api_archive.archive.ArchiveService.read_chunks") +@patch("shared.reports.filtered.FilteredReport.apply_diff") +@patch( + "api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions", + lambda self, repo, user: (True, True), +) +class TestCompareFlagsView(InternalAPITest): + def _get_compare_flags(self, kwargs, query_params): + return self.client.get( + reverse("compare-flags", kwargs=kwargs), data=query_params + ) + + def setUp(self): + self.repo = RepositoryFactory.create(author__username="ThiagoCodecov") + self.parent_commit = CommitWithReportFactory.create( + commitid="00c7b4b49778b3c79427f9c4c13a8612a376ff19", repository=self.repo + ) + self.commit = CommitWithReportFactory.create( + message="test_report_serializer", + commitid="68946ef98daec68c7798459150982fc799c87d85", + parent_commit_id=self.parent_commit.commitid, + repository=self.repo, + ) + + self.client.session["current_owner_id"] = self.repo.author.pk + self.client.force_login(self.repo.author.user) + + def test_compare_flags___success( + self, diff_totals_mock, read_chunks_mock, git_comparison_mock + ): + head_chunks = open( + current_file.parent.parent.parent + / f"samples/{self.commit.commitid}_chunks.txt", + "r", + ).read() + base_chunks = open( + current_file.parent.parent.parent + / f"samples/{self.parent_commit.commitid}_chunks.txt", + "r", + ).read() + read_chunks_mock.side_effect = ( + lambda x: head_chunks if x == self.commit.commitid else base_chunks + ) + diff_totals_mock.return_value = ReportTotals( + files=0, + lines=0, + hits=0, + misses=0, + partials=0, + coverage="0", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + git_comparison_mock.return_value = {"diff": {"files": {}}} + response = self._get_compare_flags( + kwargs={ + "service": self.repo.author.service, + "owner_username": self.repo.author.username, + "repo_name": self.repo.name, + }, + query_params={ + "base": self.parent_commit.commitid, + "head": self.commit.commitid, + }, + ) + + assert response.status_code == 200 + assert response.data == [ + { + "name": "unittests", + "base_report_totals": { + "branches": 0, + "complexity": 0, + "complexity_total": 0, + "complexity_ratio": 0, + "coverage": 79.16, + "diff": 0, + "files": 3, + "hits": 19, + "lines": 24, + "messages": 0, + "methods": 0, + "misses": 5, + "partials": 0, + "sessions": 1, + }, + "diff_totals": { + "branches": 0, + "complexity": 0, + "complexity_total": 0, + "complexity_ratio": 0, + "coverage": 0, + "diff": 0, + "files": 0, + "hits": 0, + "lines": 0, + "messages": 0, + "methods": 0, + "misses": 0, + "partials": 0, + "sessions": 0, + }, + "head_report_totals": { + "branches": 0, + "complexity": 0, + "complexity_total": 0, + "complexity_ratio": 0, + "coverage": 80.00, + "diff": 0, + "files": 3, + "hits": 20, + "lines": 25, + "messages": 0, + "methods": 0, + "misses": 5, + "partials": 0, + "sessions": 1, + }, + }, + { + "name": "integrations", + "base_report_totals": { + "branches": 0, + "complexity": 0, + "complexity_total": 0, + "complexity_ratio": 0, + "coverage": 79.16, + "diff": 0, + "files": 3, + "hits": 19, + "lines": 24, + "messages": 0, + "methods": 0, + "misses": 5, + "partials": 0, + "sessions": 1, + }, + "diff_totals": { + "branches": 0, + "complexity": 0, + "complexity_total": 0, + "complexity_ratio": 0, + "coverage": 0, + "diff": 0, + "files": 0, + "hits": 0, + "lines": 0, + "messages": 0, + "methods": 0, + "misses": 0, + "partials": 0, + "sessions": 0, + }, + "head_report_totals": { + "branches": 0, + "complexity": 0, + "complexity_total": 0, + "complexity_ratio": 0, + "coverage": 56.00, + "diff": 0, + "files": 3, + "hits": 14, + "lines": 25, + "messages": 0, + "methods": 0, + "misses": 11, + "partials": 0, + "sessions": 1, + }, + }, + ] + + def test_compare_flags_with_report_with_cff_and_non_cff( + self, diff_totals_mock, read_chunks_mock, git_comparison_mock + ): + commit_with_custom_reports = CommitWithReportFactory.create( + message="test_report_serializer", + commitid="9sa8790asdf9agyasdg7a90sd9f89as7ga0sdf98a", + parent_commit_id=self.parent_commit.commitid, + repository=self.repo, + ) + report = commit_with_custom_reports._report + report["sessions"]["0"].update( + st="carriedforward", + se={"carriedforward_from": "56e05fced214c44a37759efa2dfc25a65d8ae98d"}, + ) + commit_with_custom_reports.save() + + upload = ( + commit_with_custom_reports.reports.first() + .sessions.filter(flags__flag_name="unittests") + .first() + ) + upload.upload_type = "carriedforward" + upload.upload_extras = { + "carriedforward_from": "56e05fced214c44a37759efa2dfc25a65d8ae98d" + } + upload.save() + + head_chunks = open( + current_file.parent.parent.parent + / f"samples/{commit_with_custom_reports.commitid}_chunks.txt", + "r", + ).read() + base_chunks = open( + current_file.parent.parent.parent + / f"samples/{self.parent_commit.commitid}_chunks.txt", + "r", + ).read() + read_chunks_mock.side_effect = ( + lambda x: head_chunks + if x == commit_with_custom_reports.commitid + else base_chunks + ) + diff_totals_mock.return_value = ReportTotals( + files=0, + lines=0, + hits=0, + misses=0, + partials=0, + coverage="0", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + git_comparison_mock.return_value = {"diff": {"files": {}}} + response = self._get_compare_flags( + kwargs={ + "service": self.repo.author.service, + "owner_username": self.repo.author.username, + "repo_name": self.repo.name, + }, + query_params={ + "base": self.parent_commit.commitid, + "head": commit_with_custom_reports.commitid, + }, + ) + + assert response.status_code == 200 + + expected_result = [ + { + "name": "integrations", + "base_report_totals": { + "branches": 0, + "complexity": 0, + "complexity_total": 0, + "complexity_ratio": 0, + "coverage": 79.16, + "diff": 0, + "files": 3, + "hits": 19, + "lines": 24, + "messages": 0, + "methods": 0, + "misses": 5, + "partials": 0, + "sessions": 1, + }, + "diff_totals": { + "branches": 0, + "complexity": 0, + "complexity_total": 0, + "complexity_ratio": 0, + "coverage": 0, + "diff": 0, + "files": 0, + "hits": 0, + "lines": 0, + "messages": 0, + "methods": 0, + "misses": 0, + "partials": 0, + "sessions": 0, + }, + "head_report_totals": { + "branches": 0, + "complexity": 0, + "complexity_total": 0, + "complexity_ratio": 0, + "coverage": 56.00, + "diff": 0, + "files": 3, + "hits": 14, + "lines": 25, + "messages": 0, + "methods": 0, + "misses": 11, + "partials": 0, + "sessions": 1, + }, + } + ] + # Only the non-carried forward report is returned + assert len(response.data) == 1 + assert ( + response.data[0]["base_report_totals"] + == expected_result[0]["base_report_totals"] + ) + assert ( + response.data[0]["head_report_totals"] + == expected_result[0]["head_report_totals"] + ) + assert response.data[0] == expected_result[0] + assert response.data == expected_result + + @patch("redis.Redis.get", lambda self, key: None) + @patch("redis.Redis.set", lambda self, key, val, ex: None) + def test_compare_flags_view_accepts_pullid_query_param( + self, diff_totals_mock, read_chunks_mock, git_comparison_mock + ): + git_comparison_mock.return_value = {"diff": {"files": {}}} + read_chunks_mock.return_value = "" + diff_totals_mock.return_value = ReportTotals() + + response = self._get_compare_flags( + kwargs={ + "service": self.repo.author.service, + "owner_username": self.repo.author.username, + "repo_name": self.repo.name, + }, + query_params={ + "pullid": PullFactory( + base=self.parent_commit.commitid, + head=self.commit.commitid, + compared_to=self.parent_commit.commitid, + pullid=2, + author=self.commit.author, + repository=self.repo, + ).pullid + }, + ) + + assert response.status_code == 200 + + @patch("services.comparison.FlagComparison.base_report", new_callable=PropertyMock) + def test_compare_flags_doesnt_crash_if_base_doesnt_have_flags( + self, base_flag_mock, diff_totals_mock, read_chunks_mock, git_comparison_mock + ): + git_comparison_mock.return_value = {"diff": {"files": {}}} + read_chunks_mock.return_value = "" + base_flag_mock.return_value = None + diff_totals_mock.return_value = ReportTotals() + + # should not crash + self._get_compare_flags( + kwargs={ + "service": self.repo.author.service, + "owner_username": self.repo.author.username, + "repo_name": self.repo.name, + }, + query_params={ + "base": self.parent_commit.commitid, + "head": self.commit.commitid, + }, + ) + + @patch("shared.reports.resources.Report.totals", new_callable=PropertyMock) + def test_compare_flags_view_doesnt_crash_if_coverage_is_none( + self, + report_totals_mock, + diff_totals_mock, + read_chunks_mock, + git_comparison_mock, + ): + diff_totals_mock.return_value = ReportTotals() + read_chunks_mock.return_value = "" + git_comparison_mock.return_value = {"diff": {"files": {}}} + report_totals_mock.return_value = ReportTotals( + branches=0, + complexity=0, + complexity_total=0, + coverage=None, + diff=0, + files=3, + hits=19, + lines=24, + messages=0, + methods=0, + misses=5, + partials=0, + sessions=2, + ) + + # should not crash + self._get_compare_flags( + kwargs={ + "service": self.repo.author.service, + "owner_username": self.repo.author.username, + "repo_name": self.repo.name, + }, + query_params={ + "base": self.parent_commit.commitid, + "head": self.commit.commitid, + }, + ) diff --git a/apps/codecov-api/api/internal/tests/unit/views/test_compare_view.py b/apps/codecov-api/api/internal/tests/unit/views/test_compare_view.py new file mode 100644 index 0000000000..439578ffd9 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/unit/views/test_compare_view.py @@ -0,0 +1,284 @@ +import json +from unittest.mock import PropertyMock, patch + +from rest_framework import status +from rest_framework.reverse import reverse +from shared.django_apps.core.tests.factories import ( + BranchFactory, + CommitWithReportFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) + +from api.internal.commit.serializers import CommitTotalsSerializer +from codecov.tests.base_test import InternalAPITest + + +def build_commits(client): + """ + build commits in mock_db that are based on a real git commit for using VCR + :param user: + :param client: + :return: repo, commit_base, commit_head + """ + repo = RepositoryFactory.create( + author__unencrypted_oauth_token="testqmit3okrgutcoyzscveipor3toi3nsmb927v", + author__username="ThiagoCodecov", + ) + parent_commit = CommitWithReportFactory.create( + message="test_compare_parent", commitid="c5b6730", repository=repo + ) + commit_base = CommitWithReportFactory.create( + message="test_compare_commits_base", + commitid="9193232a8fe3429496956ba82b5fed2583d1b5eb", + parent_commit_id=parent_commit.commitid, + repository=repo, + ) + commit_head = CommitWithReportFactory.create( + message="test_compare_commits_head", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + parent_commit_id=parent_commit.commitid, + repository=repo, + ) + client.session["current_owner_id"] = repo.author.pk + client.force_login(user=repo.author.user) + return repo, commit_base, commit_head + + +@patch("shared.api_archive.archive.ArchiveService.read_chunks", lambda obj, sha: "") +@patch( + "api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions", + lambda self, repo, user: (True, True), +) +class TestCompareCommitsView(InternalAPITest): + def setUp(self): + org = OwnerFactory(username="Codecov") + self.user = OwnerFactory( + username="codecov-user", + email="codecov-user@codecov.io", + organizations=[org.ownerid], + ) + self.repo, self.commit_base, self.commit_head = build_commits(self.client) + self.commit_base_totals_serialized = { + "files": self.commit_base.totals["f"], + "lines": self.commit_base.totals["n"], + "hits": self.commit_base.totals["h"], + "misses": self.commit_base.totals["m"], + "partials": self.commit_base.totals["p"], + "coverage": round(float(self.commit_base.totals["c"]), 2), + "branches": self.commit_base.totals["b"], + "methods": self.commit_base.totals["d"], + "sessions": self.commit_base.totals["s"], + "diff": self.commit_base.totals["diff"], + "complexity": self.commit_base.totals["C"], + "complexity_total": self.commit_base.totals["N"], + } + self.commit_head_totals_serialized = { + "files": self.commit_head.totals["f"], + "lines": self.commit_head.totals["n"], + "hits": self.commit_head.totals["h"], + "misses": self.commit_head.totals["m"], + "partials": self.commit_head.totals["p"], + "coverage": round(float(self.commit_head.totals["c"]), 2), + "branches": self.commit_head.totals["b"], + "methods": self.commit_head.totals["d"], + "sessions": self.commit_head.totals["s"], + "diff": self.commit_head.totals["diff"], + "complexity": self.commit_head.totals["C"], + "complexity_total": self.commit_head.totals["N"], + } + + def _get_commits_comparison(self, kwargs, query_params): + return self.client.get( + reverse("compare-detail", kwargs=kwargs), data=query_params + ) + + def _configure_mocked_comparison_with_commits(self, mock): + mock.return_value = { + "diff": {"files": {}}, + "commits": [ + { + "commitid": self.commit_base.commitid, + "message": self.commit_base.message, + "timestamp": "2019-03-31T02:28:02Z", + "author": { + "id": self.repo.author.ownerid, + "username": self.repo.author.username, + "name": self.repo.author.name, + "email": self.repo.author.email, + }, + }, + { + "commitid": self.commit_head.commitid, + "message": self.commit_head.message, + "timestamp": "2019-03-31T07:23:19Z", + "author": { + "id": self.repo.author.ownerid, + "username": self.repo.author.username, + "name": self.repo.author.name, + "email": self.repo.author.email, + }, + }, + ], + } + + def test_compare_commits_bad_commit(self): + bad_commitid = "9193232a8fe3429496123ba82b5fed2583d1b5eb" + response = self._get_commits_comparison( + kwargs={ + "service": self.repo.author.service, + "owner_username": self.repo.author.username, + "repo_name": self.repo.name, + }, + query_params={"base": self.commit_base.commitid, "head": bad_commitid}, + ) + assert response.status_code == 404 + + def test_compare_commits_bad_branch(self): + bad_branch = "bad-branch" + branch_base = BranchFactory.create(head=self.commit_base, repository=self.repo) + response = self._get_commits_comparison( + kwargs={ + "service": self.repo.author.service, + "owner_username": self.repo.author.username, + "repo_name": self.repo.name, + }, + query_params={"base": branch_base.name, "head": bad_branch}, + ) + assert response.status_code == 404 + + @patch("services.comparison.Comparison.git_comparison", new_callable=PropertyMock) + def test_compare_commits_view_with_branchname(self, mocked_comparison): + self._configure_mocked_comparison_with_commits(mocked_comparison) + branch_base = BranchFactory.create( + head=self.commit_base.commitid, repository=self.commit_base.repository + ) + branch_head = BranchFactory.create( + head=self.commit_head.commitid, repository=self.commit_head.repository + ) + + response = self._get_commits_comparison( + kwargs={ + "service": self.repo.author.service, + "owner_username": self.repo.author.username, + "repo_name": self.repo.name, + }, + query_params={"base": branch_base.name, "head": branch_head.name}, + ) + + assert response.status_code == 200 + content = json.loads(response.content.decode()) + assert ( + content["diff"]["git_commits"] == mocked_comparison.return_value["commits"] + ) + + head_upload = next( + commit + for commit in content["commit_uploads"] + if commit["commitid"] == self.commit_head.commitid + ) + assert ( + head_upload["totals"] + == CommitTotalsSerializer(self.commit_head.totals).data + ) + + base_upload = next( + commit + for commit in content["commit_uploads"] + if commit["commitid"] == self.commit_base.commitid + ) + assert ( + base_upload["totals"] + == CommitTotalsSerializer(self.commit_base.totals).data + ) + + @patch("services.comparison.Comparison.git_comparison", new_callable=PropertyMock) + def test_compare_commits_view_with_commitid(self, mocked_comparison): + self._configure_mocked_comparison_with_commits(mocked_comparison) + response = self._get_commits_comparison( + kwargs={ + "service": self.repo.author.service, + "owner_username": self.repo.author.username, + "repo_name": self.repo.name, + }, + query_params={ + "base": self.commit_base.commitid, + "head": self.commit_head.commitid, + }, + ) + assert response.status_code == 200 + content = json.loads(response.content.decode()) + + assert ( + content["diff"]["git_commits"] == mocked_comparison.return_value["commits"] + ) + + head_upload = next( + commit + for commit in content["commit_uploads"] + if commit["commitid"] == self.commit_head.commitid + ) + assert ( + head_upload["totals"] + == CommitTotalsSerializer(self.commit_head.totals).data + ) + + base_upload = next( + commit + for commit in content["commit_uploads"] + if commit["commitid"] == self.commit_base.commitid + ) + assert ( + base_upload["totals"] + == CommitTotalsSerializer(self.commit_base.totals).data + ) + + @patch("services.comparison.Comparison.git_comparison", new_callable=PropertyMock) + @patch("redis.Redis.get", lambda self, key: None) + @patch("redis.Redis.set", lambda self, key, val, ex: None) + def test_compare_commits_view_with_pullid(self, mocked_comparison): + self._configure_mocked_comparison_with_commits(mocked_comparison) + pull = PullFactory( + pullid=2, + repository=self.repo, + author=self.repo.author, + base=self.commit_base.commitid, + compared_to=self.commit_base.commitid, + head=self.commit_head.commitid, + ) + + response = self._get_commits_comparison( + kwargs={ + "service": self.repo.author.service, + "owner_username": self.repo.author.username, + "repo_name": self.repo.name, + }, + query_params={"pullid": pull.pullid}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + content = json.loads(response.content.decode()) + + assert ( + content["diff"]["git_commits"] == mocked_comparison.return_value["commits"] + ) + + head_upload = next( + commit + for commit in content["commit_uploads"] + if commit["commitid"] == self.commit_head.commitid + ) + assert ( + head_upload["totals"] + == CommitTotalsSerializer(self.commit_head.totals).data + ) + + base_upload = next( + commit + for commit in content["commit_uploads"] + if commit["commitid"] == self.commit_base.commitid + ) + assert ( + base_upload["totals"] + == CommitTotalsSerializer(self.commit_base.totals).data + ) diff --git a/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_billing_address.yaml b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_billing_address.yaml new file mode 100644 index 0000000000..51c8537256 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_billing_address.yaml @@ -0,0 +1,79 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Stripe-Version: + - '2024-04-10' + User-Agent: + - Stripe/v1 PythonBindings/9.6.0 + X-Stripe-Client-User-Agent: + - '{"bindings_version": "9.6.0", "lang": "python", "publisher": "stripe", "httplib": + "requests", "lang_version": "3.12.3", "platform": "Linux-6.6.31-linuxkit-aarch64-with-glibc2.36", + "uname": "Linux b0efe3849169 6.6.31-linuxkit #1 SMP Thu May 23 08:36:57 UTC + 2024 aarch64 "}' + method: GET + uri: https://api.stripe.com/v1/subscriptions/djfos?expand%5B0%5D=latest_invoice&expand%5B1%5D=customer&expand%5B2%5D=customer.invoice_settings.default_payment_method + response: + body: + string: "{\n \"error\": {\n \"code\": \"resource_missing\",\n \"doc_url\": + \"https://stripe.com/docs/error-codes/resource-missing\",\n \"message\": + \"No such subscription: 'djfos'\",\n \"param\": \"id\",\n \"request_log_url\": + \"https://dashboard.stripe.com/test/logs/req_4POjCjRMnbE5Wg?t=1718056644\",\n + \ \"type\": \"invalid_request_error\"\n }\n}\n" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET,HEAD,PUT,PATCH,POST,DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, Stripe-Should-Retry, X-Stripe-External-Auth-Required, + X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Connection: + - keep-alive + Content-Length: + - '324' + Content-Security-Policy: + - report-uri https://q.stripe.com/csp-report?p=v1%2Fsubscriptions%2F%3Asubscription_exposed_id; + block-all-mixed-content; default-src 'none'; base-uri 'none'; form-action + 'none'; frame-ancestors 'none'; img-src 'self'; script-src 'self' 'report-sample'; + style-src 'self' + Content-Type: + - application/json + Cross-Origin-Opener-Policy-Report-Only: + - same-origin; report-to="coop" + Date: + - Mon, 10 Jun 2024 21:57:24 GMT + Report-To: + - '{"group":"coop","max_age":8640,"endpoints":[{"url":"https://q.stripe.com/coop-report?s=billing-api-srv"}],"include_subdomains":true}' + Reporting-Endpoints: + - coop="https://q.stripe.com/coop-report?s=billing-api-srv" + Request-Id: + - req_4POjCjRMnbE5Wg + Server: + - nginx + Strict-Transport-Security: + - max-age=63072000; includeSubDomains; preload + Stripe-Version: + - '2024-04-10' + Vary: + - Origin + X-Content-Type-Options: + - nosniff + X-Stripe-Routing-Context-Priority-Tier: + - api-testmode + status: + code: 404 + message: Not Found +version: 1 diff --git a/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_handles_stripe_error.yaml b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_handles_stripe_error.yaml new file mode 100644 index 0000000000..254ef9a55d --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_handles_stripe_error.yaml @@ -0,0 +1,82 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Stripe-Version: + - 2024-12-18.acacia + User-Agent: + - Stripe/v1 PythonBindings/11.4.1 + X-Stripe-Client-User-Agent: + - '{"bindings_version": "11.4.1", "lang": "python", "publisher": "stripe", "httplib": + "requests", "lang_version": "3.12.8", "platform": "Linux-6.10.14-linuxkit-aarch64-with-glibc2.36", + "uname": "Linux f69fe8d5c257 6.10.14-linuxkit #1 SMP Fri Nov 29 17:22:03 UTC + 2024 aarch64 "}' + method: GET + uri: https://api.stripe.com/v1/subscriptions/djfos?expand%5B0%5D=latest_invoice&expand%5B1%5D=customer&expand%5B2%5D=customer.invoice_settings.default_payment_method&expand%5B3%5D=customer.tax_ids + response: + body: + string: "{\n \"error\": {\n \"code\": \"resource_missing\",\n \"doc_url\": + \"https://stripe.com/docs/error-codes/resource-missing\",\n \"message\": + \"No such subscription: 'djfos'\",\n \"param\": \"id\",\n \"request_log_url\": + \"https://dashboard.stripe.com/test/logs/req_fCLmExiHliLLAy?t=1738274612\",\n + \ \"type\": \"invalid_request_error\"\n }\n}\n" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, HEAD, PUT, PATCH, POST, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, Stripe-Should-Retry, X-Stripe-External-Auth-Required, + X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Connection: + - keep-alive + Content-Length: + - '324' + Content-Security-Policy: + - base-uri 'none'; default-src 'none'; form-action 'none'; frame-ancestors 'none'; + img-src 'self'; script-src 'self' 'report-sample'; style-src 'self'; upgrade-insecure-requests; + report-uri https://q.stripe.com/csp-violation?q=JU_aZLssk7a3_VZEeiDM3UWQN0mgWJiEG8zz5aFpDfoiI4Itt-XeW-vHYyCYd8ZJIklaArUO0YdslYml + Content-Type: + - application/json + Cross-Origin-Opener-Policy-Report-Only: + - same-origin; report-to="coop" + Date: + - Thu, 30 Jan 2025 22:03:32 GMT + Report-To: + - '{"group":"coop","max_age":8640,"endpoints":[{"url":"https://q.stripe.com/coop-report"}],"include_subdomains":true}' + Reporting-Endpoints: + - coop="https://q.stripe.com/coop-report" + Request-Id: + - req_fCLmExiHliLLAy + Server: + - nginx + Strict-Transport-Security: + - max-age=63072000; includeSubDomains; preload + Stripe-Version: + - 2024-12-18.acacia + Vary: + - Origin + X-Content-Type-Options: + - nosniff + X-Stripe-Priority-Routing-Enabled: + - 'true' + X-Stripe-Routing-Context-Priority-Tier: + - api-testmode + X-Wc: + - AB + status: + code: 404 + message: Not Found +version: 1 diff --git a/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_must_fail_if_team_plan_and_too_many_users.yaml b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_must_fail_if_team_plan_and_too_many_users.yaml new file mode 100644 index 0000000000..2ee7c35712 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_must_fail_if_team_plan_and_too_many_users.yaml @@ -0,0 +1,92 @@ +interactions: +- request: + body: billing_address_collection=required&payment_method_collection=if_required&client_reference_id=65&success_url=http%3A%2F%2Flocalhost%3A3000%2Fplan%2Fgh%2Fjustin47%3Fsuccess&cancel_url=http%3A%2F%2Flocalhost%3A3000%2Fplan%2Fgh%2Fjustin47%3Fcancel&customer=1000&mode=subscription&line_items[0][price]=price_1OCM0gGlVGuVgOrkWDYEBtSL&line_items[0][quantity]=11&subscription_data[metadata][service]=github&subscription_data[metadata][obo_organization]=65&subscription_data[metadata][username]=justin47&subscription_data[metadata][obo_name]=Kelly+Williams&subscription_data[metadata][obo_email]=christopher27%40lopez-welch.com&subscription_data[metadata][obo]=65&tax_id_collection[enabled]=True&customer_update[name]=auto&customer_update[address]=auto + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '744' + Content-Type: + - application/x-www-form-urlencoded + Idempotency-Key: + - 2e7e12e1-9051-4766-b947-1abe985b5e98 + Stripe-Version: + - 2024-12-18.acacia + User-Agent: + - Stripe/v1 PythonBindings/11.4.1 + X-Stripe-Client-User-Agent: + - '{"bindings_version": "11.4.1", "lang": "python", "publisher": "stripe", "httplib": + "requests", "lang_version": "3.12.8", "platform": "Linux-6.10.14-linuxkit-aarch64-with-glibc2.36", + "uname": "Linux 35c9e7c77efc 6.10.14-linuxkit #1 SMP Fri Nov 29 17:22:03 UTC + 2024 aarch64 "}' + method: POST + uri: https://api.stripe.com/v1/checkout/sessions + response: + body: + string: "{\n \"error\": {\n \"code\": \"resource_missing\",\n \"doc_url\": + \"https://stripe.com/docs/error-codes/resource-missing\",\n \"message\": + \"No such customer: '1000'\",\n \"param\": \"customer\",\n \"request_log_url\": + \"https://dashboard.stripe.com/test/logs/req_oevRZUbMiaT1kM?t=1737668618\",\n + \ \"type\": \"invalid_request_error\"\n }\n}\n" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, HEAD, PUT, PATCH, POST, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, Stripe-Should-Retry, X-Stripe-External-Auth-Required, + X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Connection: + - keep-alive + Content-Length: + - '325' + Content-Security-Policy: + - base-uri 'none'; default-src 'none'; form-action 'none'; frame-ancestors 'none'; + img-src 'self'; script-src 'self' 'report-sample'; style-src 'self'; upgrade-insecure-requests; + report-uri https://q.stripe.com/csp-violation?q=ieXMYrsoTw4hsNfwL6RhDPN6zQnVwnAAc0QsFb8i8xtl4N7V94VZzDmgDhKzWxW8mHRRg08-d5GW6oHr + Content-Type: + - application/json + Cross-Origin-Opener-Policy-Report-Only: + - same-origin; report-to="coop" + Date: + - Thu, 23 Jan 2025 21:43:38 GMT + Idempotency-Key: + - 2e7e12e1-9051-4766-b947-1abe985b5e98 + Original-Request: + - req_oevRZUbMiaT1kM + Report-To: + - '{"group":"coop","max_age":8640,"endpoints":[{"url":"https://q.stripe.com/coop-report"}],"include_subdomains":true}' + Reporting-Endpoints: + - coop="https://q.stripe.com/coop-report" + Request-Id: + - req_oevRZUbMiaT1kM + Server: + - nginx + Strict-Transport-Security: + - max-age=63072000; includeSubDomains; preload + Stripe-Version: + - 2024-12-18.acacia + Vary: + - Origin + X-Content-Type-Options: + - nosniff + X-Stripe-Priority-Routing-Enabled: + - 'true' + X-Stripe-Routing-Context-Priority-Tier: + - api-testmode + X-Wc: + - AB + status: + code: 400 + message: Bad Request +version: 1 diff --git a/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_payment_method.yaml b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_payment_method.yaml new file mode 100644 index 0000000000..acef70d627 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_payment_method.yaml @@ -0,0 +1,91 @@ +interactions: +- request: + body: default_payment_method=pm_123 + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '29' + Content-Type: + - application/x-www-form-urlencoded + Idempotency-Key: + - 7c40f9e9-3a01-4109-87bf-3218dfbcf27f + Stripe-Version: + - '2024-04-10' + User-Agent: + - Stripe/v1 PythonBindings/9.6.0 + X-Stripe-Client-User-Agent: + - '{"bindings_version": "9.6.0", "lang": "python", "publisher": "stripe", "httplib": + "requests", "lang_version": "3.12.4", "platform": "Linux-6.6.31-linuxkit-aarch64-with-glibc2.36", + "uname": "Linux 2b87f96d1995 6.6.31-linuxkit #1 SMP Thu May 23 08:36:57 UTC + 2024 aarch64 "}' + method: POST + uri: https://api.stripe.com/v1/subscriptions/djfos + response: + body: + string: "{\n \"error\": {\n \"code\": \"resource_missing\",\n \"doc_url\": + \"https://stripe.com/docs/error-codes/resource-missing\",\n \"message\": + \"No such PaymentMethod: 'pm_123'\",\n \"param\": \"default_payment_method\",\n + \ \"request_log_url\": \"https://dashboard.stripe.com/test/logs/req_xT5h1VWY7P75Lu?t=1719007484\",\n + \ \"type\": \"invalid_request_error\"\n }\n}\n" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET,HEAD,PUT,PATCH,POST,DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, Stripe-Should-Retry, X-Stripe-External-Auth-Required, + X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Connection: + - keep-alive + Content-Length: + - '346' + Content-Security-Policy: + - report-uri https://q.stripe.com/csp-report?p=v1%2Fsubscriptions%2F%3Asubscription_exposed_id; + block-all-mixed-content; default-src 'none'; base-uri 'none'; form-action + 'none'; frame-ancestors 'none'; img-src 'self'; script-src 'self' 'report-sample'; + style-src 'self' + Content-Type: + - application/json + Cross-Origin-Opener-Policy-Report-Only: + - same-origin; report-to="coop" + Date: + - Fri, 21 Jun 2024 22:04:44 GMT + Idempotency-Key: + - 7c40f9e9-3a01-4109-87bf-3218dfbcf27f + Original-Request: + - req_xT5h1VWY7P75Lu + Report-To: + - '{"group":"coop","max_age":8640,"endpoints":[{"url":"https://q.stripe.com/coop-report?s=billing-api-srv"}],"include_subdomains":true}' + Reporting-Endpoints: + - coop="https://q.stripe.com/coop-report?s=billing-api-srv" + Request-Id: + - req_xT5h1VWY7P75Lu + Server: + - nginx + Strict-Transport-Security: + - max-age=63072000; includeSubDomains; preload + Stripe-Version: + - '2024-04-10' + Vary: + - Origin + X-Content-Type-Options: + - nosniff + X-Stripe-Priority-Routing-Enabled: + - 'true' + X-Stripe-Routing-Context-Priority-Tier: + - api-testmode + status: + code: 400 + message: Bad Request +version: 1 diff --git a/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_quantity_must_be_greater_or_equal_to_current_activated_users_if_paid_plan.yaml b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_quantity_must_be_greater_or_equal_to_current_activated_users_if_paid_plan.yaml new file mode 100644 index 0000000000..d48e89ce02 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_quantity_must_be_greater_or_equal_to_current_activated_users_if_paid_plan.yaml @@ -0,0 +1,71 @@ +interactions: +- request: + body: billing_address_collection=required&payment_method_types[0]=card&payment_method_collection=if_required&client_reference_id=65&success_url=http%3A%2F%2Flocalhost%3A3000%2Fplan%2Fgh%2Fysantos%3Fsuccess&cancel_url=http%3A%2F%2Flocalhost%3A3000%2Fplan%2Fgh%2Fysantos%3Fcancel&subscription_data[items][0][plan]=plan_H6P16wij3lUuxg&subscription_data[items][0][quantity]=14&subscription_data[payment_behavior]=allow_incomplete&subscription_data[metadata][service]=github&subscription_data[metadata][obo_organization]=65&subscription_data[metadata][username]=ysantos&subscription_data[metadata][obo_name]=Matthew+Guzman&subscription_data[metadata][obo_email]=ybrown%40fitzgerald-cummings.org&subscription_data[metadata][obo]=65&customer=1000 + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '733' + Content-Type: + - application/x-www-form-urlencoded + Idempotency-Key: + - 21e170c5-bf4a-47f7-9da5-262844bbdbf2 + User-Agent: + - Stripe/v1 PythonBindings/2.55.2 + X-Stripe-Client-User-Agent: + - '{"bindings_version": "2.55.2", "lang": "python", "publisher": "stripe", "httplib": + "requests", "lang_version": "3.9.16", "platform": "Linux-5.15.49-linuxkit-pr-aarch64-with", + "uname": "Linux 7e5f26339edc 5.15.49-linuxkit-pr #1 SMP PREEMPT Thu May 25 + 07:27:39 UTC 2023 aarch64 "}' + method: POST + uri: https://api.stripe.com/v1/checkout/sessions + response: + body: + string: "{\n \"error\": {\n \"code\": \"resource_missing\",\n \"doc_url\"\ + : \"https://stripe.com/docs/error-codes/resource-missing\",\n \"message\"\ + : \"No such customer: '1000'\",\n \"param\": \"customer\",\n \"request_log_url\"\ + : \"https://dashboard.stripe.com/test/logs/req_vVNzxP8mblHEcJ?t=1689601951\"\ + ,\n \"type\": \"invalid_request_error\"\n }\n}\n" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Connection: + - keep-alive + Content-Length: + - '325' + Content-Type: + - application/json + Date: + - Mon, 17 Jul 2023 13:52:31 GMT + Idempotency-Key: + - 21e170c5-bf4a-47f7-9da5-262844bbdbf2 + Original-Request: + - req_vVNzxP8mblHEcJ + Request-Id: + - req_vVNzxP8mblHEcJ + Server: + - nginx + Strict-Transport-Security: + - max-age=63072000; includeSubDomains; preload + Stripe-Version: + - '2017-05-25' + X-Stripe-Routing-Context-Priority-Tier: + - api-testmode + status: + code: 400 + message: Bad Request +version: 1 diff --git a/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_sentry_plan_annual.yaml b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_sentry_plan_annual.yaml new file mode 100644 index 0000000000..355d48135e --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_sentry_plan_annual.yaml @@ -0,0 +1,64 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - Stripe/v1 PythonBindings/2.55.2 + X-Stripe-Client-Telemetry: + - '{"last_request_metrics": {"request_id": "req_vVNzxP8mblHEcJ", "request_duration_ms": + 353}}' + X-Stripe-Client-User-Agent: + - '{"bindings_version": "2.55.2", "lang": "python", "publisher": "stripe", "httplib": + "requests", "lang_version": "3.9.16", "platform": "Linux-5.15.49-linuxkit-pr-aarch64-with", + "uname": "Linux 7e5f26339edc 5.15.49-linuxkit-pr #1 SMP PREEMPT Thu May 25 + 07:27:39 UTC 2023 aarch64 "}' + method: GET + uri: https://api.stripe.com/v1/subscriptions/djfos?expand%5B0%5D=latest_invoice&expand%5B1%5D=customer&expand%5B2%5D=customer.invoice_settings.default_payment_method + response: + body: + string: "{\n \"error\": {\n \"code\": \"resource_missing\",\n \"doc_url\"\ + : \"https://stripe.com/docs/error-codes/resource-missing\",\n \"message\"\ + : \"No such subscription: 'djfos'\",\n \"param\": \"id\",\n \"request_log_url\"\ + : \"https://dashboard.stripe.com/test/logs/req_5JWnxFXQbAG4xJ?t=1689601951\"\ + ,\n \"type\": \"invalid_request_error\"\n }\n}\n" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Connection: + - keep-alive + Content-Length: + - '324' + Content-Type: + - application/json + Date: + - Mon, 17 Jul 2023 13:52:31 GMT + Request-Id: + - req_5JWnxFXQbAG4xJ + Server: + - nginx + Strict-Transport-Security: + - max-age=63072000; includeSubDomains; preload + Stripe-Version: + - '2017-05-25' + X-Stripe-Routing-Context-Priority-Tier: + - api-testmode + status: + code: 404 + message: Not Found +version: 1 diff --git a/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_sentry_plan_annual_with_users_org.yaml b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_sentry_plan_annual_with_users_org.yaml new file mode 100644 index 0000000000..b99d1a0fbb --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_sentry_plan_annual_with_users_org.yaml @@ -0,0 +1,115 @@ +interactions: +- request: + body: billing_address_collection=auto&payment_method_types[0]=card&payment_method_collection=if_required&client_reference_id=69&success_url=http%3A%2F%2Flocalhost%3A3000%2Fplan%2Fgh%2Fdustinbrown%3Fsuccess&cancel_url=http%3A%2F%2Flocalhost%3A3000%2Fplan%2Fgh%2Fdustinbrown%3Fcancel&subscription_data[items][0][plan]=price_1Mj1mMGlVGuVgOrkC0ORc6iW&subscription_data[items][0][quantity]=12&subscription_data[payment_behavior]=allow_incomplete&subscription_data[metadata][service]=github&subscription_data[metadata][obo_organization]=69&subscription_data[metadata][username]=dustinbrown&subscription_data[metadata][obo_name]=Melanie+Young&subscription_data[metadata][obo_email]=nelsondavid%40gutierrez-carlson.biz&subscription_data[metadata][obo]=68&subscription_data[trial_period_days]=14&subscription_data[trial_settings][end_behavior][missing_payment_method]=cancel&customer_email=karenmorgan%40hotmail.com + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '900' + Content-Type: + - application/x-www-form-urlencoded + Idempotency-Key: + - 4a99c686-ec33-4be8-a5e0-84bfd1d25a5f + User-Agent: + - Stripe/v1 PythonBindings/2.55.2 + X-Stripe-Client-Telemetry: + - '{"last_request_metrics": {"request_id": "req_5JWnxFXQbAG4xJ", "request_duration_ms": + 329}}' + X-Stripe-Client-User-Agent: + - '{"bindings_version": "2.55.2", "lang": "python", "publisher": "stripe", "httplib": + "requests", "lang_version": "3.9.16", "platform": "Linux-5.15.49-linuxkit-pr-aarch64-with", + "uname": "Linux 7e5f26339edc 5.15.49-linuxkit-pr #1 SMP PREEMPT Thu May 25 + 07:27:39 UTC 2023 aarch64 "}' + method: POST + uri: https://api.stripe.com/v1/checkout/sessions + response: + body: + string: "{\n \"id\": \"cs_test_a1VJ7uPphpFIRYNicL2xduJkZf7kMlUsgom2pcLv35tQFZGQmbeZXWMZJp\"\ + ,\n \"object\": \"checkout.session\",\n \"after_expiration\": null,\n \"\ + allow_promotion_codes\": null,\n \"amount_subtotal\": 0,\n \"amount_total\"\ + : 0,\n \"automatic_tax\": {\n \"enabled\": false,\n \"status\": null\n\ + \ },\n \"billing_address_collection\": \"auto\",\n \"cancel_url\": \"http://localhost:3000/plan/gh/dustinbrown?cancel\"\ + ,\n \"client_reference_id\": \"69\",\n \"consent\": null,\n \"consent_collection\"\ + : null,\n \"created\": 1689601952,\n \"currency\": \"usd\",\n \"currency_conversion\"\ + : null,\n \"custom_fields\": [],\n \"custom_text\": {\n \"shipping_address\"\ + : null,\n \"submit\": null\n },\n \"customer\": null,\n \"customer_creation\"\ + : \"always\",\n \"customer_details\": {\n \"address\": null,\n \"email\"\ + : \"karenmorgan@hotmail.com\",\n \"name\": null,\n \"phone\": null,\n\ + \ \"tax_exempt\": \"none\",\n \"tax_ids\": null\n },\n \"customer_email\"\ + : \"karenmorgan@hotmail.com\",\n \"display_items\": [\n {\n \"amount\"\ + : 0,\n \"currency\": \"usd\",\n \"plan\": {\n \"id\": \"\ + price_1Mj1mMGlVGuVgOrkC0ORc6iW\",\n \"object\": \"plan\",\n \ + \ \"active\": true,\n \"aggregate_usage\": null,\n \"amount\"\ + : null,\n \"amount_decimal\": null,\n \"billing_scheme\": \"\ + tiered\",\n \"created\": 1678200374,\n \"currency\": \"usd\"\ + ,\n \"interval\": \"year\",\n \"interval_count\": 1,\n \ + \ \"livemode\": false,\n \"metadata\": {},\n \"name\": \"\ + users-sentryy\",\n \"nickname\": null,\n \"product\": \"prod_NTzlixoYR2xMef\"\ + ,\n \"statement_descriptor\": null,\n \"tiers\": [\n \ + \ {\n \"amount\": null,\n \"flat_amount\": 34800,\n\ + \ \"flat_amount_decimal\": \"34800\",\n \"unit_amount_decimal\"\ + : null,\n \"up_to\": 5\n },\n {\n \ + \ \"amount\": 12000,\n \"flat_amount\": null,\n \"flat_amount_decimal\"\ + : null,\n \"unit_amount_decimal\": \"12000\",\n \"up_to\"\ + : null\n }\n ],\n \"tiers_mode\": \"graduated\",\n\ + \ \"transform_usage\": null,\n \"trial_period_days\": 14,\n\ + \ \"usage_type\": \"licensed\"\n },\n \"quantity\": 12,\n\ + \ \"type\": \"plan\"\n }\n ],\n \"expires_at\": 1689688352,\n \"\ + invoice\": null,\n \"invoice_creation\": null,\n \"livemode\": false,\n\ + \ \"locale\": null,\n \"metadata\": {},\n \"mode\": \"subscription\",\n\ + \ \"payment_intent\": null,\n \"payment_link\": null,\n \"payment_method_collection\"\ + : \"if_required\",\n \"payment_method_options\": null,\n \"payment_method_types\"\ + : [\n \"card\"\n ],\n \"payment_status\": \"unpaid\",\n \"phone_number_collection\"\ + : {\n \"enabled\": false\n },\n \"recovered_from\": null,\n \"setup_intent\"\ + : null,\n \"shipping\": null,\n \"shipping_address_collection\": null,\n\ + \ \"shipping_options\": [],\n \"shipping_rate\": null,\n \"status\": \"\ + open\",\n \"submit_type\": null,\n \"subscription\": null,\n \"success_url\"\ + : \"http://localhost:3000/plan/gh/dustinbrown?success\",\n \"total_details\"\ + : {\n \"amount_discount\": 0,\n \"amount_shipping\": 0,\n \"amount_tax\"\ + : 0\n },\n \"url\": \"https://checkout.stripe.com/c/pay/cs_test_a1VJ7uPphpFIRYNicL2xduJkZf7kMlUsgom2pcLv35tQFZGQmbeZXWMZJp#fidkdWxOYHwnPyd1blpxYHZxWjA0MVZPUUpCaVNCcFNiSnduV2JtMn99dTZxVDJnXTFGXDN1a319cjN%2FV19fVmpBU3FQUG9Ed1VORjJqXWBgTGdPS0xGUXRWMk09PUNXY3Jfa1JIdm5VTn1qNTVnRGtwN2w8TCcpJ2N3amhWYHdzYHcnP3F3cGApJ2lkfGpwcVF8dWAnPyd2bGtiaWBabHFgaCcpJ2BrZGdpYFVpZGZgbWppYWB3dic%2FcXdwYHgl\"\ + \n}" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Connection: + - keep-alive + Content-Length: + - '3341' + Content-Type: + - application/json + Date: + - Mon, 17 Jul 2023 13:52:32 GMT + Idempotency-Key: + - 4a99c686-ec33-4be8-a5e0-84bfd1d25a5f + Original-Request: + - req_RwdTyPtxxWL9eu + Request-Id: + - req_RwdTyPtxxWL9eu + Server: + - nginx + Strict-Transport-Security: + - max-age=63072000; includeSubDomains; preload + Stripe-Should-Retry: + - 'false' + Stripe-Version: + - '2017-05-25' + X-Stripe-Routing-Context-Priority-Tier: + - api-testmode + status: + code: 200 + message: OK +version: 1 diff --git a/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_sentry_plan_monthly.yaml b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_sentry_plan_monthly.yaml new file mode 100644 index 0000000000..3b2144fcf8 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_sentry_plan_monthly.yaml @@ -0,0 +1,64 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - Stripe/v1 PythonBindings/2.55.2 + X-Stripe-Client-Telemetry: + - '{"last_request_metrics": {"request_id": "req_RwdTyPtxxWL9eu", "request_duration_ms": + 899}}' + X-Stripe-Client-User-Agent: + - '{"bindings_version": "2.55.2", "lang": "python", "publisher": "stripe", "httplib": + "requests", "lang_version": "3.9.16", "platform": "Linux-5.15.49-linuxkit-pr-aarch64-with", + "uname": "Linux 7e5f26339edc 5.15.49-linuxkit-pr #1 SMP PREEMPT Thu May 25 + 07:27:39 UTC 2023 aarch64 "}' + method: GET + uri: https://api.stripe.com/v1/subscriptions/djfos?expand%5B0%5D=latest_invoice&expand%5B1%5D=customer&expand%5B2%5D=customer.invoice_settings.default_payment_method + response: + body: + string: "{\n \"error\": {\n \"code\": \"resource_missing\",\n \"doc_url\"\ + : \"https://stripe.com/docs/error-codes/resource-missing\",\n \"message\"\ + : \"No such subscription: 'djfos'\",\n \"param\": \"id\",\n \"request_log_url\"\ + : \"https://dashboard.stripe.com/test/logs/req_3UbPl8augcgjaw?t=1689601953\"\ + ,\n \"type\": \"invalid_request_error\"\n }\n}\n" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Connection: + - keep-alive + Content-Length: + - '324' + Content-Type: + - application/json + Date: + - Mon, 17 Jul 2023 13:52:33 GMT + Request-Id: + - req_3UbPl8augcgjaw + Server: + - nginx + Strict-Transport-Security: + - max-age=63072000; includeSubDomains; preload + Stripe-Version: + - '2017-05-25' + X-Stripe-Routing-Context-Priority-Tier: + - api-testmode + status: + code: 404 + message: Not Found +version: 1 diff --git a/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_sentry_plan_monthly_with_users_org.yaml b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_sentry_plan_monthly_with_users_org.yaml new file mode 100644 index 0000000000..344e985206 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_sentry_plan_monthly_with_users_org.yaml @@ -0,0 +1,115 @@ +interactions: +- request: + body: billing_address_collection=auto&payment_method_types[0]=card&payment_method_collection=if_required&client_reference_id=72&success_url=http%3A%2F%2Flocalhost%3A3000%2Fplan%2Fgh%2Fjohnmathews%3Fsuccess&cancel_url=http%3A%2F%2Flocalhost%3A3000%2Fplan%2Fgh%2Fjohnmathews%3Fcancel&subscription_data[items][0][plan]=price_1Mj1kYGlVGuVgOrk7jucaZAa&subscription_data[items][0][quantity]=12&subscription_data[payment_behavior]=allow_incomplete&subscription_data[metadata][service]=github&subscription_data[metadata][obo_organization]=72&subscription_data[metadata][username]=johnmathews&subscription_data[metadata][obo_name]=Joshua+Murray&subscription_data[metadata][obo_email]=phammatthew%40lynn.org&subscription_data[metadata][obo]=71&subscription_data[trial_period_days]=14&subscription_data[trial_settings][end_behavior][missing_payment_method]=cancel&customer_email=hollowaydeborah%40yahoo.com + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '889' + Content-Type: + - application/x-www-form-urlencoded + Idempotency-Key: + - 74f2d021-03a1-4843-baf9-901640e18259 + User-Agent: + - Stripe/v1 PythonBindings/2.55.2 + X-Stripe-Client-Telemetry: + - '{"last_request_metrics": {"request_id": "req_3UbPl8augcgjaw", "request_duration_ms": + 428}}' + X-Stripe-Client-User-Agent: + - '{"bindings_version": "2.55.2", "lang": "python", "publisher": "stripe", "httplib": + "requests", "lang_version": "3.9.16", "platform": "Linux-5.15.49-linuxkit-pr-aarch64-with", + "uname": "Linux 7e5f26339edc 5.15.49-linuxkit-pr #1 SMP PREEMPT Thu May 25 + 07:27:39 UTC 2023 aarch64 "}' + method: POST + uri: https://api.stripe.com/v1/checkout/sessions + response: + body: + string: "{\n \"id\": \"cs_test_a1L24ai3WCyYK7UzTMS6HE1Yfhnc28ng781GDaufn1rT6l23ouNABrCsBA\"\ + ,\n \"object\": \"checkout.session\",\n \"after_expiration\": null,\n \"\ + allow_promotion_codes\": null,\n \"amount_subtotal\": 0,\n \"amount_total\"\ + : 0,\n \"automatic_tax\": {\n \"enabled\": false,\n \"status\": null\n\ + \ },\n \"billing_address_collection\": \"auto\",\n \"cancel_url\": \"http://localhost:3000/plan/gh/johnmathews?cancel\"\ + ,\n \"client_reference_id\": \"72\",\n \"consent\": null,\n \"consent_collection\"\ + : null,\n \"created\": 1689601954,\n \"currency\": \"usd\",\n \"currency_conversion\"\ + : null,\n \"custom_fields\": [],\n \"custom_text\": {\n \"shipping_address\"\ + : null,\n \"submit\": null\n },\n \"customer\": null,\n \"customer_creation\"\ + : \"always\",\n \"customer_details\": {\n \"address\": null,\n \"email\"\ + : \"hollowaydeborah@yahoo.com\",\n \"name\": null,\n \"phone\": null,\n\ + \ \"tax_exempt\": \"none\",\n \"tax_ids\": null\n },\n \"customer_email\"\ + : \"hollowaydeborah@yahoo.com\",\n \"display_items\": [\n {\n \"\ + amount\": 0,\n \"currency\": \"usd\",\n \"plan\": {\n \"\ + id\": \"price_1Mj1kYGlVGuVgOrk7jucaZAa\",\n \"object\": \"plan\",\n\ + \ \"active\": true,\n \"aggregate_usage\": null,\n \"\ + amount\": null,\n \"amount_decimal\": null,\n \"billing_scheme\"\ + : \"tiered\",\n \"created\": 1678200262,\n \"currency\": \"\ + usd\",\n \"interval\": \"month\",\n \"interval_count\": 1,\n\ + \ \"livemode\": false,\n \"metadata\": {},\n \"name\"\ + : \"users-sentrym\",\n \"nickname\": null,\n \"product\": \"\ + prod_NTzkoJuGDejxOc\",\n \"statement_descriptor\": null,\n \"\ + tiers\": [\n {\n \"amount\": null,\n \"flat_amount\"\ + : 2900,\n \"flat_amount_decimal\": \"2900\",\n \"unit_amount_decimal\"\ + : null,\n \"up_to\": 5\n },\n {\n \ + \ \"amount\": 1200,\n \"flat_amount\": null,\n \"flat_amount_decimal\"\ + : null,\n \"unit_amount_decimal\": \"1200\",\n \"up_to\"\ + : null\n }\n ],\n \"tiers_mode\": \"graduated\",\n\ + \ \"transform_usage\": null,\n \"trial_period_days\": 14,\n\ + \ \"usage_type\": \"licensed\"\n },\n \"quantity\": 12,\n\ + \ \"type\": \"plan\"\n }\n ],\n \"expires_at\": 1689688353,\n \"\ + invoice\": null,\n \"invoice_creation\": null,\n \"livemode\": false,\n\ + \ \"locale\": null,\n \"metadata\": {},\n \"mode\": \"subscription\",\n\ + \ \"payment_intent\": null,\n \"payment_link\": null,\n \"payment_method_collection\"\ + : \"if_required\",\n \"payment_method_options\": null,\n \"payment_method_types\"\ + : [\n \"card\"\n ],\n \"payment_status\": \"unpaid\",\n \"phone_number_collection\"\ + : {\n \"enabled\": false\n },\n \"recovered_from\": null,\n \"setup_intent\"\ + : null,\n \"shipping\": null,\n \"shipping_address_collection\": null,\n\ + \ \"shipping_options\": [],\n \"shipping_rate\": null,\n \"status\": \"\ + open\",\n \"submit_type\": null,\n \"subscription\": null,\n \"success_url\"\ + : \"http://localhost:3000/plan/gh/johnmathews?success\",\n \"total_details\"\ + : {\n \"amount_discount\": 0,\n \"amount_shipping\": 0,\n \"amount_tax\"\ + : 0\n },\n \"url\": \"https://checkout.stripe.com/c/pay/cs_test_a1L24ai3WCyYK7UzTMS6HE1Yfhnc28ng781GDaufn1rT6l23ouNABrCsBA#fidkdWxOYHwnPyd1blpxYHZxWjA0MVZPUUpCaVNCcFNiSnduV2JtMn99dTZxVDJnXTFGXDN1a319cjN%2FV19fVmpBU3FQUG9Ed1VORjJqXWBgTGdPS0xGUXRWMk09PUNXY3Jfa1JIdm5VTn1qNTVnRGtwN2w8TCcpJ2N3amhWYHdzYHcnP3F3cGApJ2lkfGpwcVF8dWAnPyd2bGtiaWBabHFgaCcpJ2BrZGdpYFVpZGZgbWppYWB3dic%2FcXdwYHgl\"\ + \n}" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Connection: + - keep-alive + Content-Length: + - '3342' + Content-Type: + - application/json + Date: + - Mon, 17 Jul 2023 13:52:34 GMT + Idempotency-Key: + - 74f2d021-03a1-4843-baf9-901640e18259 + Original-Request: + - req_AaY8IvHbbSDcvz + Request-Id: + - req_AaY8IvHbbSDcvz + Server: + - nginx + Strict-Transport-Security: + - max-age=63072000; includeSubDomains; preload + Stripe-Should-Retry: + - 'false' + Stripe-Version: + - '2017-05-25' + X-Stripe-Routing-Context-Priority-Tier: + - api-testmode + status: + code: 200 + message: OK +version: 1 diff --git a/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_team_plan_must_fail_if_currently_team_plan_add_too_many_users.yaml b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_team_plan_must_fail_if_currently_team_plan_add_too_many_users.yaml new file mode 100644 index 0000000000..f8c6ccc4d2 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/cassetes/test_account_viewset/AccountViewSetTests/test_update_team_plan_must_fail_if_currently_team_plan_add_too_many_users.yaml @@ -0,0 +1,95 @@ +interactions: +- request: + body: billing_address_collection=required&payment_method_collection=if_required&client_reference_id=93&success_url=http%3A%2F%2Flocalhost%3A3000%2Fplan%2Fgh%2Fjocelyn62%3Fsuccess&cancel_url=http%3A%2F%2Flocalhost%3A3000%2Fplan%2Fgh%2Fjocelyn62%3Fcancel&customer=1000&mode=subscription&line_items[0][price]=price_1OCM0gGlVGuVgOrkWDYEBtSL&line_items[0][quantity]=11&subscription_data[metadata][service]=github&subscription_data[metadata][obo_organization]=93&subscription_data[metadata][username]=jocelyn62&subscription_data[metadata][obo_name]=Crystal+Schmitt&subscription_data[metadata][obo_email]=smithamanda%40flowers.biz&subscription_data[metadata][obo]=93&tax_id_collection[enabled]=True&customer_update[name]=auto&customer_update[address]=auto + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '742' + Content-Type: + - application/x-www-form-urlencoded + Idempotency-Key: + - 26000f5a-feb8-4647-ac57-32a5e5ff8729 + Stripe-Version: + - 2024-12-18.acacia + User-Agent: + - Stripe/v1 PythonBindings/11.4.1 + X-Stripe-Client-Telemetry: + - '{"last_request_metrics": {"request_id": "req_AaY8IvHbbSDcvz", "request_duration_ms": + 2}}' + X-Stripe-Client-User-Agent: + - '{"bindings_version": "11.4.1", "lang": "python", "publisher": "stripe", "httplib": + "requests", "lang_version": "3.12.8", "platform": "Linux-6.10.14-linuxkit-aarch64-with-glibc2.36", + "uname": "Linux 35c9e7c77efc 6.10.14-linuxkit #1 SMP Fri Nov 29 17:22:03 UTC + 2024 aarch64 "}' + method: POST + uri: https://api.stripe.com/v1/checkout/sessions + response: + body: + string: "{\n \"error\": {\n \"code\": \"resource_missing\",\n \"doc_url\": + \"https://stripe.com/docs/error-codes/resource-missing\",\n \"message\": + \"No such customer: '1000'\",\n \"param\": \"customer\",\n \"request_log_url\": + \"https://dashboard.stripe.com/test/logs/req_k8lY68XdxWIFHo?t=1737668619\",\n + \ \"type\": \"invalid_request_error\"\n }\n}\n" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, HEAD, PUT, PATCH, POST, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, Stripe-Should-Retry, X-Stripe-External-Auth-Required, + X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Connection: + - keep-alive + Content-Length: + - '325' + Content-Security-Policy: + - base-uri 'none'; default-src 'none'; form-action 'none'; frame-ancestors 'none'; + img-src 'self'; script-src 'self' 'report-sample'; style-src 'self'; upgrade-insecure-requests; + report-uri https://q.stripe.com/csp-violation?q=1vgSiZ6UPd0qoBXo1Mjsk02GXFGP3M7PXsjua2jiowWQKm8jByxTHbhTeRirsQsrZ7jscQLjXtdCc_sh + Content-Type: + - application/json + Cross-Origin-Opener-Policy-Report-Only: + - same-origin; report-to="coop" + Date: + - Thu, 23 Jan 2025 21:43:39 GMT + Idempotency-Key: + - 26000f5a-feb8-4647-ac57-32a5e5ff8729 + Original-Request: + - req_k8lY68XdxWIFHo + Report-To: + - '{"group":"coop","max_age":8640,"endpoints":[{"url":"https://q.stripe.com/coop-report"}],"include_subdomains":true}' + Reporting-Endpoints: + - coop="https://q.stripe.com/coop-report" + Request-Id: + - req_k8lY68XdxWIFHo + Server: + - nginx + Strict-Transport-Security: + - max-age=63072000; includeSubDomains; preload + Stripe-Version: + - 2024-12-18.acacia + Vary: + - Origin + X-Content-Type-Options: + - nosniff + X-Stripe-Priority-Routing-Enabled: + - 'true' + X-Stripe-Routing-Context-Priority-Tier: + - api-testmode + X-Wc: + - AB + status: + code: 400 + message: Bad Request +version: 1 diff --git a/apps/codecov-api/api/internal/tests/views/test_account_viewset.py b/apps/codecov-api/api/internal/tests/views/test_account_viewset.py new file mode 100644 index 0000000000..0cb97d449a --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/test_account_viewset.py @@ -0,0 +1,1845 @@ +import json +import os +from datetime import datetime +from unittest.mock import MagicMock, patch + +import pytest +from django.test import override_settings +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.django_apps.codecov_auth.tests.factories import ( + AccountFactory, + InvoiceBillingFactory, + OwnerFactory, + UserFactory, +) +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName, TrialStatus +from stripe import StripeError + +from api.internal.tests.test_utils import GetAdminProviderAdapter +from billing.helpers import mock_all_plans_and_tiers +from codecov_auth.models import Service +from utils.test_utils import APIClient + +curr_path = os.path.dirname(__file__) + + +class MockSubscription(object): + def __init__(self, subscription_params: dict): + self.items = {"data": [{"id": "abc"}]} + self.cancel_at_period_end = False + self.current_period_end = 1633512445 + self.latest_invoice = subscription_params.get( + "latest_invoice", + { + "id": "in_123", + "status": "complete", + }, + ) + + default_payment_method = { + "id": "pm_123", + "card": { + "brand": "visa", + "exp_month": 12, + "exp_year": 2024, + "last4": "abcd", + }, + } + self.customer = { + "invoice_settings": { + "default_payment_method": subscription_params.get( + "default_payment_method", default_payment_method + ) + }, + "id": "cus_LK&*Hli8YLIO", + "discount": None, + "email": None, + } + self.schedule = subscription_params.get("schedule_id") + self.status = subscription_params.get("status", "active") + self.collection_method = subscription_params.get( + "collection_method", "charge_automatically" + ) + self.trial_end = subscription_params.get("trial_end") + + customer_coupon = subscription_params.get("customer_coupon") + if customer_coupon: + self.customer["discount"] = {"coupon": customer_coupon} + + pending_update = subscription_params.get("pending_update") + if pending_update: + self.pending_update = pending_update + + def __getitem__(self, key): + return getattr(self, key) + + +class MockMetadata(object): + def __init__(self): + self.obo = 2 + self.obo_organization = 3 + + def __getitem__(self, key): + return getattr(self, key) + + +class MockSchedule(object): + def __init__(self, schedule_params, phases): + self.id = schedule_params["id"] + self.phases = phases + self.metadata = MockMetadata() + + def __getitem__(self, key): + return getattr(self, key) + + +@pytest.mark.usefixtures("codecov_vcr") +class AccountViewSetTests(APITestCase): + def _retrieve(self, kwargs={}): + if not kwargs: + kwargs = { + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + return self.client.get(reverse("account_details-detail", kwargs=kwargs)) + + def _update(self, kwargs, data): + return self.client.patch( + reverse("account_details-detail", kwargs=kwargs), data=data, format="json" + ) + + def _destroy(self, kwargs): + return self.client.delete(reverse("account_details-detail", kwargs=kwargs)) + + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + + def setUp(self): + self.service = "gitlab" + self.current_owner = OwnerFactory( + stripe_customer_id=1000, + service=Service.GITHUB.value, + service_id="10238974029348", + ) + self.expected_invoice = { + "number": "EF0A41E-0001", + "status": "paid", + "id": "in_19yTU92eZvKYlo2C7uDjvu6v", + "created": 1489789429, + "period_start": 1487370220, + "period_end": 1489789420, + "due_date": None, + "customer_name": "Peer Company", + "customer_address": "6639 Boulevard Dr, Westwood FL 34202 USA", + "currency": "usd", + "amount_paid": 999, + "amount_due": 999, + "amount_remaining": 0, + "total": 999, + "subtotal": 999, + "invoice_pdf": "https://pay.stripe.com/invoice/acct_1032D82eZvKYlo2C/invst_a7KV10HpLw2QxrihgVyuOkOjMZ/pdf", + "line_items": [ + { + "description": "(10) users-pr-inappm", + "amount": 120, + "currency": "usd", + "plan_name": PlanName.CODECOV_PRO_MONTHLY.value, + "quantity": 1, + "period": {"end": 1521326190, "start": 1518906990}, + } + ], + "footer": None, + "customer_email": "olivia.williams.03@example.com", + "customer_shipping": None, + } + + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + def test_retrieve_own_account_give_200(self): + response = self._retrieve( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + ) + assert response.status_code == status.HTTP_200_OK + + def test_retrieve_account_gets_account_fields(self): + owner = OwnerFactory(admins=[self.current_owner.ownerid]) + self.current_owner.organizations = [owner.ownerid] + self.current_owner.save() + response = self._retrieve( + kwargs={"service": owner.service, "owner_username": owner.username} + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "activated_user_count": 0, + "root_organization": None, + "integration_id": owner.integration_id, + "plan_auto_activate": owner.plan_auto_activate, + "inactive_user_count": 1, + "plan": { + "marketing_name": "Developer", + "value": DEFAULT_FREE_PLAN, + "billing_rate": None, + "base_unit_price": 0, + "benefits": [ + "Up to 1 user", + "Unlimited public repositories", + "Unlimited private repositories", + ], + "quantity": 1, + }, + "subscription_detail": None, + "checkout_session_id": None, + "name": owner.name, + "email": owner.email, + "nb_active_private_repos": 0, + "repo_total_credits": 99999999, + "plan_provider": owner.plan_provider, + "activated_student_count": 0, + "student_count": 0, + "schedule_detail": None, + "uses_invoice": False, + "delinquent": None, + } + + @patch("services.billing.stripe.SubscriptionSchedule.retrieve") + @patch("services.billing.stripe.Subscription.retrieve") + def test_retrieve_account_gets_account_fields_when_there_are_scheduled_details( + self, mock_retrieve_subscription, mock_retrieve_schedule + ): + owner = OwnerFactory( + admins=[self.current_owner.ownerid], stripe_subscription_id="sub_123" + ) + self.current_owner.organizations = [owner.ownerid] + self.current_owner.save() + + subscription_params = { + "default_payment_method": None, + "cancel_at_period_end": False, + "current_period_end": 1633512445, + "latest_invoice": None, + "schedule_id": "sub_sched_456", + "collection_method": "charge_automatically", + "tax_ids": None, + } + + mock_retrieve_subscription.return_value = MockSubscription(subscription_params) + schedule_params = { + "id": 123, + "start_date": 123689126736, + "stripe_plan_id": "plan_pro_yearly", + "quantity": 6, + } + phases = [ + {}, + { + "start_date": schedule_params["start_date"], + "items": [ + { + "plan": schedule_params["stripe_plan_id"], + "quantity": schedule_params["quantity"], + } + ], + }, + ] + + mock_retrieve_schedule.return_value = MockSchedule(schedule_params, phases) + + response = self._retrieve( + kwargs={"service": owner.service, "owner_username": owner.username} + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "integration_id": owner.integration_id, + "activated_student_count": 0, + "activated_user_count": 0, + "checkout_session_id": None, + "delinquent": None, + "email": owner.email, + "inactive_user_count": 1, + "name": owner.name, + "nb_active_private_repos": 0, + "plan_auto_activate": True, + "plan_provider": owner.plan_provider, + "plan": { + "marketing_name": "Developer", + "value": DEFAULT_FREE_PLAN, + "billing_rate": None, + "base_unit_price": 0, + "benefits": [ + "Up to 1 user", + "Unlimited public repositories", + "Unlimited private repositories", + ], + "quantity": 1, + }, + "repo_total_credits": 99999999, + "root_organization": None, + "schedule_detail": { + "id": "123", + "scheduled_phase": { + "start_date": schedule_params["start_date"], + "plan": "Pro", + "quantity": schedule_params["quantity"], + }, + }, + "student_count": 0, + "subscription_detail": { + "latest_invoice": None, + "default_payment_method": None, + "cancel_at_period_end": False, + "current_period_end": 1633512445, + "customer": {"id": "cus_LK&*Hli8YLIO", "discount": None, "email": None}, + "collection_method": "charge_automatically", + "tax_ids": None, + "trial_end": None, + }, + "uses_invoice": False, + } + + @patch("services.billing.stripe.SubscriptionSchedule.retrieve") + @patch("services.billing.stripe.Subscription.retrieve") + def test_retrieve_account_returns_last_phase_when_more_than_one_scheduled_phases( + self, mock_retrieve_subscription, mock_retrieve_schedule + ): + owner = OwnerFactory( + admins=[self.current_owner.ownerid], stripe_subscription_id="sub_2345687" + ) + self.current_owner.organizations = [owner.ownerid] + self.current_owner.save() + + subscription_params = { + "default_payment_method": None, + "cancel_at_period_end": False, + "current_period_end": 1633512445, + "latest_invoice": None, + "schedule_id": "sub_sched_456678999", + "collection_method": "charge_automatically", + "trial_end": 1633512445, + "tax_ids": None, + } + + mock_retrieve_subscription.return_value = MockSubscription(subscription_params) + schedule_params = { + "id": 123, + "start_date": 123689126736, + "stripe_plan_id": "plan_pro_yearly", + "quantity": 6, + } + phases = [ + { + "start_date": 123689126536, + "items": [{"plan": "test_plan_123", "quantity": 4}], + }, + { + "start_date": 123689126636, + "items": [{"plan": "test_plan_456", "quantity": 5}], + }, + { + "start_date": schedule_params["start_date"], + "items": [ + { + "plan": schedule_params["stripe_plan_id"], + "quantity": schedule_params["quantity"], + } + ], + }, + ] + + mock_retrieve_schedule.return_value = MockSchedule(schedule_params, phases) + + response = self._retrieve( + kwargs={"service": owner.service, "owner_username": owner.username} + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "activated_user_count": 0, + "root_organization": None, + "integration_id": owner.integration_id, + "plan_auto_activate": owner.plan_auto_activate, + "inactive_user_count": 1, + "plan": { + "marketing_name": "Developer", + "value": DEFAULT_FREE_PLAN, + "billing_rate": None, + "base_unit_price": 0, + "benefits": [ + "Up to 1 user", + "Unlimited public repositories", + "Unlimited private repositories", + ], + "quantity": 1, + }, + "subscription_detail": { + "latest_invoice": None, + "default_payment_method": None, + "cancel_at_period_end": False, + "current_period_end": 1633512445, + "customer": {"id": "cus_LK&*Hli8YLIO", "discount": None, "email": None}, + "collection_method": "charge_automatically", + "trial_end": 1633512445, + "tax_ids": None, + }, + "checkout_session_id": None, + "name": owner.name, + "email": owner.email, + "nb_active_private_repos": 0, + "repo_total_credits": 99999999, + "plan_provider": owner.plan_provider, + "activated_student_count": 0, + "student_count": 0, + "schedule_detail": { + "id": "123", + "scheduled_phase": { + "plan": "Pro", + "quantity": schedule_params["quantity"], + "start_date": schedule_params["start_date"], + }, + }, + "uses_invoice": False, + "delinquent": None, + } + + @patch("services.billing.stripe.Subscription.retrieve") + def test_retrieve_account_gets_none_for_schedule_details_when_schedule_is_nonexistent( + self, mock_retrieve_subscription + ): + owner = OwnerFactory( + admins=[self.current_owner.ownerid], stripe_subscription_id="sub_123" + ) + self.current_owner.organizations = [owner.ownerid] + self.current_owner.save() + + subscription_params = { + "default_payment_method": None, + "cancel_at_period_end": False, + "current_period_end": 1633512445, + "latest_invoice": None, + "schedule_id": None, + "collection_method": "charge_automatically", + "tax_ids": None, + } + + mock_retrieve_subscription.return_value = MockSubscription(subscription_params) + + response = self._retrieve( + kwargs={"service": owner.service, "owner_username": owner.username} + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "activated_user_count": 0, + "root_organization": None, + "integration_id": owner.integration_id, + "plan_auto_activate": owner.plan_auto_activate, + "inactive_user_count": 1, + "plan": { + "marketing_name": "Developer", + "value": DEFAULT_FREE_PLAN, + "billing_rate": None, + "base_unit_price": 0, + "benefits": [ + "Up to 1 user", + "Unlimited public repositories", + "Unlimited private repositories", + ], + "quantity": 1, + }, + "subscription_detail": { + "latest_invoice": None, + "default_payment_method": None, + "cancel_at_period_end": False, + "current_period_end": 1633512445, + "customer": {"id": "cus_LK&*Hli8YLIO", "discount": None, "email": None}, + "collection_method": "charge_automatically", + "trial_end": None, + "tax_ids": None, + }, + "checkout_session_id": None, + "name": owner.name, + "email": owner.email, + "nb_active_private_repos": 0, + "repo_total_credits": 99999999, + "plan_provider": owner.plan_provider, + "activated_student_count": 0, + "student_count": 0, + "schedule_detail": None, + "uses_invoice": False, + "delinquent": None, + } + + def test_retrieve_account_gets_account_students(self): + owner = OwnerFactory( + admins=[self.current_owner.ownerid], + plan_activated_users=[OwnerFactory(student=True).ownerid], + ) + self.current_owner.organizations = [owner.ownerid] + self.current_owner.save() + OwnerFactory(organizations=[owner.ownerid], student=True) + OwnerFactory(organizations=[owner.ownerid], student=True) + response = self._retrieve( + kwargs={"service": owner.service, "owner_username": owner.username} + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "activated_user_count": 0, + "root_organization": None, + "integration_id": owner.integration_id, + "plan_auto_activate": owner.plan_auto_activate, + "inactive_user_count": 1, + "plan": response.data["plan"], + "subscription_detail": None, + "checkout_session_id": None, + "name": owner.name, + "email": owner.email, + "nb_active_private_repos": 0, + "repo_total_credits": 99999999, + "plan_provider": owner.plan_provider, + "activated_student_count": 1, + "student_count": 3, + "schedule_detail": None, + "uses_invoice": False, + "delinquent": None, + } + + def test_account_with_free_user_plan(self): + self.current_owner.plan = DEFAULT_FREE_PLAN + self.current_owner.save() + response = self._retrieve() + assert response.status_code == status.HTTP_200_OK + assert response.data["plan"] == { + "marketing_name": "Developer", + "value": DEFAULT_FREE_PLAN, + "billing_rate": None, + "base_unit_price": 0, + "benefits": [ + "Up to 1 user", + "Unlimited public repositories", + "Unlimited private repositories", + ], + "quantity": self.current_owner.plan_user_count, + } + + def test_account_with_paid_user_plan_billed_monthly(self): + self.current_owner.plan = PlanName.CODECOV_PRO_MONTHLY.value + self.current_owner.save() + response = self._retrieve() + assert response.status_code == status.HTTP_200_OK + assert response.data["plan"] == { + "marketing_name": "Pro", + "value": PlanName.CODECOV_PRO_MONTHLY.value, + "billing_rate": "monthly", + "base_unit_price": 12, + "benefits": [ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + "quantity": self.current_owner.plan_user_count, + } + + def test_account_with_paid_user_plan_billed_annually(self): + self.current_owner.plan = PlanName.CODECOV_PRO_YEARLY.value + self.current_owner.save() + response = self._retrieve() + assert response.status_code == status.HTTP_200_OK + assert response.data["plan"] == { + "marketing_name": "Pro", + "value": PlanName.CODECOV_PRO_YEARLY.value, + "billing_rate": "annually", + "base_unit_price": 10, + "benefits": [ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + "quantity": self.current_owner.plan_user_count, + } + + def test_retrieve_account_returns_401_if_not_authenticated(self): + owner = OwnerFactory() + self.client.logout() + response = self._retrieve( + kwargs={"service": owner.service, "owner_username": owner.username} + ) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + def test_retrieve_account_returns_401_if_no_current_owner(self): + owner = OwnerFactory() + user = UserFactory() + self.client.logout() + self.client.force_login(user) + response = self._retrieve( + kwargs={"service": owner.service, "owner_username": owner.username} + ) + assert response.status_code == 403 + + def test_retrieve_account_returns_404_if_user_not_member(self): + owner = OwnerFactory() + response = self._retrieve( + kwargs={"service": owner.service, "owner_username": owner.username} + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + @patch("services.billing.stripe.Subscription.retrieve") + def test_retrieve_subscription_with_stripe_invoice_data(self, mock_subscription): + f = open("./services/tests/samples/stripe_invoice.json") + + default_payment_method = { + "card": { + "brand": "visa", + "exp_month": 12, + "exp_year": 2024, + "last4": "abcd", + "should be": "removed", + } + } + + subscription_params = { + "default_payment_method": default_payment_method, + "cancel_at_period_end": False, + "current_period_end": 1633512445, + "latest_invoice": json.load(f)["data"][0], + "schedule_id": None, + "collection_method": "charge_automatically", + "tax_ids": None, + } + + mock_subscription.return_value = MockSubscription(subscription_params) + + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.save() + + response = self._retrieve() + + assert response.status_code == status.HTTP_200_OK + assert response.data["subscription_detail"] == { + "cancel_at_period_end": False, + "current_period_end": 1633512445, + "latest_invoice": self.expected_invoice, + "default_payment_method": { + "card": { + "brand": "visa", + "exp_month": 12, + "exp_year": 2024, + "last4": "abcd", + } + }, + "customer": {"id": "cus_LK&*Hli8YLIO", "discount": None, "email": None}, + "collection_method": "charge_automatically", + "trial_end": None, + "tax_ids": None, + } + + @patch("services.billing.stripe.Subscription.retrieve") + def test_retrieve_handles_stripe_error(self, mock_get_subscription): + code, message = 404, "Didn't find that" + mock_get_subscription.side_effect = StripeError( + message=message, http_status=code + ) + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.save() + + response = self._retrieve() + + assert response.status_code == code + assert response.data["detail"] == message + + def test_update_can_set_plan_auto_activate_to_true(self): + self.current_owner.plan_auto_activate = False + self.current_owner.save() + + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan_auto_activate": True}, + ) + + assert response.status_code == status.HTTP_200_OK + + self.current_owner.refresh_from_db() + + assert self.current_owner.plan_auto_activate is True + assert response.data["plan_auto_activate"] is True + + def test_update_can_set_plan_auto_activate_to_false(self): + self.current_owner.plan_auto_activate = True + self.current_owner.save() + + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan_auto_activate": False}, + ) + + assert response.status_code == status.HTTP_200_OK + + self.current_owner.refresh_from_db() + + assert self.current_owner.plan_auto_activate is False + assert response.data["plan_auto_activate"] is False + + def test_update_can_set_plan_auto_activate_on_org_with_account(self): + self.current_owner.account = AccountFactory() + self.current_owner.plan_auto_activate = True + self.current_owner.save() + + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan_auto_activate": False}, + ) + + assert response.status_code == status.HTTP_200_OK + + self.current_owner.refresh_from_db() + + assert self.current_owner.plan_auto_activate is False + assert response.data["plan_auto_activate"] is False + + def test_update_can_set_plan_to_users_developer_should_set_to_developer(self): + self.current_owner.plan = PlanName.CODECOV_PRO_YEARLY.value + self.current_owner.save() + + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": {"value": DEFAULT_FREE_PLAN}}, + ) + + assert response.status_code == status.HTTP_200_OK + + self.current_owner.refresh_from_db() + + assert self.current_owner.plan == DEFAULT_FREE_PLAN + assert self.current_owner.plan_activated_users is None + assert self.current_owner.plan_user_count == 1 + assert response.data["plan_auto_activate"] is True + + @patch("services.billing.stripe.checkout.Session.create") + def test_update_can_upgrade_to_paid_plan_for_new_customer_and_return_checkout_session_id( + self, create_checkout_session_mock + ): + expected_id = "this is the id" + create_checkout_session_mock.return_value = {"id": expected_id} + self.current_owner.stripe_subscription_id = None + self.current_owner.save() + + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": {"quantity": 25, "value": PlanName.CODECOV_PRO_YEARLY.value}}, + ) + + create_checkout_session_mock.assert_called_once() + + assert response.status_code == status.HTTP_200_OK + assert response.data["checkout_session_id"] == expected_id + + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.Subscription.modify") + def test_update_can_upgrade_to_paid_plan_for_existing_customer_and_set_plan_info( + self, modify_subscription_mock, retrieve_subscription_mock + ): + desired_plan = {"value": PlanName.CODECOV_PRO_MONTHLY.value, "quantity": 12} + self.current_owner.stripe_customer_id = "flsoe" + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.plan = PlanName.CODECOV_PRO_MONTHLY.value + self.current_owner.plan_user_count = 8 + self.current_owner.save() + + f = open("./services/tests/samples/stripe_invoice.json") + + default_payment_method = { + "card": { + "brand": "visa", + "exp_month": 12, + "exp_year": 2024, + "last4": "abcd", + "should be": "removed", + } + } + + subscription_params = { + "default_payment_method": default_payment_method, + "latest_invoice": json.load(f)["data"][0], + "schedule_id": None, + "collection_method": "charge_automatically", + "tax_ids": None, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + modify_subscription_mock.return_value = MockSubscription(subscription_params) + + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": desired_plan}, + ) + + modify_subscription_mock.assert_called_once() + + assert response.status_code == status.HTTP_200_OK + assert response.data["plan"]["value"] == desired_plan["value"] + assert response.data["plan"]["quantity"] == desired_plan["quantity"] + + self.current_owner.refresh_from_db() + assert self.current_owner.plan == desired_plan["value"] + assert self.current_owner.plan_user_count == desired_plan["quantity"] + + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.Subscription.modify") + def test_upgrade_payment_failure( + self, modify_subscription_mock, retrieve_subscription_mock + ): + desired_plan = {"value": PlanName.CODECOV_PRO_MONTHLY.value, "quantity": 12} + self.current_owner.stripe_customer_id = "flsoe" + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.plan = PlanName.CODECOV_PRO_MONTHLY.value + self.current_owner.plan_user_count = 8 + self.current_owner.delinquent = False + self.current_owner.save() + + f = open("./services/tests/samples/stripe_invoice.json") + + default_payment_method = { + "card": { + "brand": "visa", + "exp_month": 12, + "exp_year": 2024, + "last4": "abcd", + "should be": "removed", + } + } + subscription_params = { + "default_payment_method": default_payment_method, + "latest_invoice": json.load(f)["data"][0], + "schedule_id": None, + "collection_method": "charge_automatically", + "tax_ids": None, + "pending_update": { + "expires_at": 1571194285, + "subscription_items": [ + { + "id": "si_09IkI4u3ZypJUk5onGUZpe8O", + "price": "price_CBb6IXqvTLXp3f", + } + ], + }, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + modify_subscription_mock.return_value = MockSubscription(subscription_params) + + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": desired_plan}, + ) + + modify_subscription_mock.assert_called_once() + + assert response.status_code == status.HTTP_200_OK + assert response.data["plan"]["value"] == desired_plan["value"] + assert response.data["plan"]["quantity"] == 8 + + self.current_owner.refresh_from_db() + assert self.current_owner.plan == desired_plan["value"] + assert self.current_owner.plan_user_count == 8 + assert self.current_owner.delinquent == True + + def test_update_requires_quantity_if_updating_to_paid_plan(self): + desired_plan = {"value": PlanName.CODECOV_PRO_YEARLY.value} + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": desired_plan}, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_update_quantity_must_be_greater_or_equal_to_current_activated_users_if_paid_plan( + self, + ): + self.current_owner.plan_activated_users = [1] * 15 + self.current_owner.save() + desired_plan = {"value": PlanName.CODECOV_PRO_MONTHLY.value, "quantity": 14} + + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": desired_plan}, + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @patch("services.billing.stripe.checkout.Session.create") + def test_update_must_validate_active_users_without_counting_active_students( + self, create_checkout_session_mock + ): + expected_id = "sample id" + create_checkout_session_mock.return_value = {"id": expected_id} + self.current_owner.stripe_subscription_id = None + self.current_owner.plan_activated_users = [ + OwnerFactory(student=False).ownerid, + OwnerFactory(student=False).ownerid, + OwnerFactory(student=False).ownerid, + OwnerFactory(student=False).ownerid, + OwnerFactory(student=False).ownerid, + OwnerFactory(student=False).ownerid, + OwnerFactory(student=False).ownerid, + OwnerFactory(student=True).ownerid, + OwnerFactory(student=True).ownerid, + OwnerFactory(student=True).ownerid, + ] + self.current_owner.save() + desired_plan = {"value": PlanName.CODECOV_PRO_YEARLY.value, "quantity": 8} + + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": desired_plan}, + ) + + create_checkout_session_mock.assert_called_once() + assert response.status_code == status.HTTP_200_OK + + def test_update_must_fail_if_quantity_is_lower_than_activated_user_count(self): + self.current_owner.plan_activated_users = [ + OwnerFactory(student=False).ownerid, + OwnerFactory(student=False).ownerid, + OwnerFactory(student=False).ownerid, + OwnerFactory(student=False).ownerid, + OwnerFactory(student=False).ownerid, + OwnerFactory(student=False).ownerid, + OwnerFactory(student=False).ownerid, + OwnerFactory(student=False).ownerid, + OwnerFactory(student=False).ownerid, + OwnerFactory(student=False).ownerid, + ] + self.current_owner.save() + desired_plan = {"value": PlanName.CODECOV_PRO_YEARLY.value, "quantity": 8} + + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": desired_plan}, + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert ( + response.data["plan"]["non_field_errors"][0] + == "Quantity cannot be lower than currently activated user count" + ) + + def test_update_must_fail_if_quantity_and_plan_are_equal_to_the_owners_current_ones( + self, + ): + self.current_owner.plan = PlanName.CODECOV_PRO_YEARLY.value + self.current_owner.plan_user_count = 14 + self.current_owner.save() + desired_plan = {"value": PlanName.CODECOV_PRO_YEARLY.value, "quantity": 14} + + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": desired_plan}, + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert ( + response.data["plan"]["non_field_errors"][0] + == "Quantity or plan for paid plan must be different from the existing one" + ) + + def test_update_team_plan_must_fail_if_too_many_activated_users_during_trial(self): + self.current_owner.plan = DEFAULT_FREE_PLAN + self.current_owner.plan_user_count = 1 + self.current_owner.trial_status = TrialStatus.ONGOING.value + self.current_owner.plan_activated_users = list(range(11)) + self.current_owner.save() + + desired_plans = [ + {"value": PlanName.TEAM_MONTHLY.value, "quantity": 10}, + {"value": PlanName.TEAM_YEARLY.value, "quantity": 10}, + ] + + for desired_plan in desired_plans: + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": desired_plan}, + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert "Invalid value for plan:" in response.json()["plan"]["value"][0] + + def test_update_team_plan_must_fail_if_currently_team_plan_add_too_many_users(self): + self.current_owner.plan = PlanName.TEAM_MONTHLY.value + self.current_owner.plan_user_count = 1 + self.current_owner.save() + + desired_plans = [ + {"value": PlanName.TEAM_MONTHLY.value, "quantity": 11}, + {"value": PlanName.TEAM_YEARLY.value, "quantity": 11}, + ] + + for desired_plan in desired_plans: + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": desired_plan}, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert ( + response.data["plan"]["non_field_errors"][0] + == "Quantity for Team plan cannot exceed 10" + ) + + def test_update_must_fail_if_team_plan_and_too_many_users(self): + desired_plans = [ + {"value": PlanName.TEAM_MONTHLY.value, "quantity": 11}, + {"value": PlanName.TEAM_YEARLY.value, "quantity": 11}, + ] + + for desired_plan in desired_plans: + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": desired_plan}, + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert ( + response.data["plan"]["non_field_errors"][0] + == "Quantity for Team plan cannot exceed 10" + ) + + def test_update_quantity_must_fail_if_account(self): + desired_plans = [ + {"quantity": 10}, + ] + self.current_owner.account = AccountFactory() + self.current_owner.save() + for desired_plan in desired_plans: + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": desired_plan}, + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert ( + str(response.data["plan"]["non_field_errors"][0]) + == "You cannot update your plan manually, for help or changes to plan, connect with sales@codecov.io" + ) + + def test_update_plan_must_fail_if_account(self): + desired_plans = [ + {"value": PlanName.CODECOV_PRO_YEARLY.value}, + ] + self.current_owner.account = AccountFactory() + self.current_owner.save() + for desired_plan in desired_plans: + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": desired_plan}, + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert ( + str(response.data["plan"]["non_field_errors"][0]) + == "You cannot update your plan manually, for help or changes to plan, connect with sales@codecov.io" + ) + + def test_update_quantity_must_be_at_least_2_if_paid_plan(self): + desired_plan = {"value": PlanName.CODECOV_PRO_YEARLY.value, "quantity": 1} + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": desired_plan}, + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert ( + response.data["plan"]["non_field_errors"][0] + == "Quantity for paid plan must be greater than 1" + ) + + def test_update_payment_method_without_body(self): + kwargs = { + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + url = reverse("account_details-update-payment", kwargs=kwargs) + response = self.client.patch(url, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @patch("services.billing.StripeService._is_unverified_payment_method") + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.PaymentMethod.attach") + @patch("services.billing.stripe.Customer.modify") + @patch("services.billing.stripe.Subscription.modify") + def test_update_payment_method( + self, + modify_subscription_mock, + modify_customer_mock, + attach_payment_mock, + retrieve_subscription_mock, + is_unverified_payment_method_mock, + ): + self.current_owner.stripe_customer_id = "flsoe" + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.save() + f = open("./services/tests/samples/stripe_invoice.json") + + is_unverified_payment_method_mock.return_value = False + + default_payment_method = { + "card": { + "brand": "visa", + "exp_month": 12, + "exp_year": 2024, + "last4": "abcd", + "should be": "removed", + } + } + + subscription_params = { + "default_payment_method": default_payment_method, + "cancel_at_period_end": False, + "current_period_end": 1633512445, + "latest_invoice": json.load(f)["data"][0], + "schedule_id": None, + "collection_method": "charge_automatically", + "tax_ids": None, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + + payment_method_id = "pm_123" + kwargs = { + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + data = {"payment_method": payment_method_id} + url = reverse("account_details-update-payment", kwargs=kwargs) + response = self.client.patch(url, data=data, format="json") + assert response.status_code == status.HTTP_200_OK + attach_payment_mock.assert_called_once_with( + payment_method_id, customer=self.current_owner.stripe_customer_id + ) + modify_customer_mock.assert_called_once_with( + self.current_owner.stripe_customer_id, + invoice_settings={"default_payment_method": payment_method_id}, + ) + + modify_subscription_mock.assert_called_once_with( + self.current_owner.stripe_subscription_id, + default_payment_method=payment_method_id, + ) + + @patch("services.billing.StripeService.update_payment_method") + def test_update_payment_method_handles_stripe_error(self, upm_mock): + code, message = 402, "Oops, nope" + self.current_owner.stripe_customer_id = "flsoe" + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.save() + + upm_mock.side_effect = StripeError(message=message, http_status=code) + + payment_method_id = "pm_123" + kwargs = { + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + data = {"payment_method": payment_method_id} + url = reverse("account_details-update-payment", kwargs=kwargs) + response = self.client.patch(url, data=data, format="json") + assert response.status_code == code + assert response.data["detail"] == message + + def test_update_email_address_without_body(self): + kwargs = { + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + url = reverse("account_details-update-email", kwargs=kwargs) + response = self.client.patch(url, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @patch("services.billing.StripeService.update_email_address") + def test_update_email_address_handles_stripe_error(self, stripe_mock): + code, message = 402, "Oops, nope" + self.current_owner.stripe_customer_id = "flsoe" + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.save() + + stripe_mock.side_effect = StripeError(message=message, http_status=code) + + new_email = "test@gmail.com" + kwargs = { + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + data = {"new_email": new_email} + url = reverse("account_details-update-email", kwargs=kwargs) + response = self.client.patch(url, data=data, format="json") + assert response.status_code == code + assert response.data["detail"] == message + + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.Customer.modify") + def test_update_email_address(self, modify_customer_mock, retrieve_mock): + self.current_owner.stripe_customer_id = "flsoe" + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.save() + + new_email = "test@gmail.com" + kwargs = { + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + data = {"new_email": new_email} + url = reverse("account_details-update-email", kwargs=kwargs) + response = self.client.patch(url, data=data, format="json") + assert response.status_code == status.HTTP_200_OK + + modify_customer_mock.assert_called_once_with( + self.current_owner.stripe_customer_id, email=new_email + ) + + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.Customer.modify") + @patch("services.billing.stripe.PaymentMethod.modify") + @patch("services.billing.stripe.Customer.retrieve") + def test_update_email_address_with_propagate( + self, + customer_retrieve_mock, + payment_method_mock, + modify_customer_mock, + retrieve_mock, + ): + self.current_owner.stripe_customer_id = "flsoe" + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.save() + + payment_method_id = "pm_123" + customer_retrieve_mock.return_value = { + "invoice_settings": {"default_payment_method": payment_method_id} + } + + new_email = "test@gmail.com" + kwargs = { + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + data = {"new_email": new_email, "apply_to_default_payment_method": True} + url = reverse("account_details-update-email", kwargs=kwargs) + response = self.client.patch(url, data=data, format="json") + assert response.status_code == status.HTTP_200_OK + + modify_customer_mock.assert_called_once_with( + self.current_owner.stripe_customer_id, email=new_email + ) + customer_retrieve_mock.assert_called_once_with( + self.current_owner.stripe_customer_id + ) + payment_method_mock.assert_called_once_with( + payment_method_id, billing_details={"email": new_email} + ) + + def test_update_billing_address_without_body(self): + kwargs = { + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + url = reverse("account_details-update-billing-address", kwargs=kwargs) + response = self.client.patch(url, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_update_billing_address_without_name(self): + kwargs = { + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + billing_address = { + "line_1": "45 Fremont St.", + "line_2": "", + "city": "San Francisco", + "state": "CA", + "country": "US", + "postal_code": "94105", + } + data = {"billing_address": billing_address} + url = reverse("account_details-update-billing-address", kwargs=kwargs) + response = self.client.patch(url, data=data, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_update_billing_address_without_address(self): + kwargs = { + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + data = {"name": "John Doe"} + url = reverse("account_details-update-billing-address", kwargs=kwargs) + response = self.client.patch(url, data=data, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @patch("services.billing.StripeService.update_billing_address") + def test_update_billing_address_handles_stripe_error(self, stripe_mock): + code, message = 402, "Oops, nope" + self.current_owner.stripe_customer_id = "flsoe" + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.save() + + stripe_mock.side_effect = StripeError(message=message, http_status=code) + + billing_address = { + "line_1": "45 Fremont St.", + "line_2": "", + "city": "San Francisco", + "state": "CA", + "country": "US", + "postal_code": "94105", + } + kwargs = { + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + data = {"name": "John Doe", "billing_address": billing_address} + url = reverse("account_details-update-billing-address", kwargs=kwargs) + response = self.client.patch(url, data=data, format="json") + assert response.status_code == code + assert response.data["detail"] == message + + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.Customer.retrieve") + @patch("services.billing.stripe.PaymentMethod.modify") + @patch("services.billing.stripe.Customer.modify") + def test_update_billing_address( + self, + modify_customer_mock, + modify_payment_mock, + retrieve_customer_mock, + retrieve_sub_mock, + ): + self.current_owner.stripe_customer_id = "flsoe" + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.save() + f = open("./services/tests/samples/stripe_invoice.json") + + billing_address = { + "line_1": "45 Fremont St.", + "line_2": "", + "city": "San Francisco", + "state": "CA", + "country": "US", + "postal_code": "94105", + } + + formatted_address = { + "line1": "45 Fremont St.", + "line2": "", + "city": "San Francisco", + "state": "CA", + "country": "US", + "postal_code": "94105", + } + + default_payment_method = { + "id": "pm_123", + "card": { + "brand": "visa", + "exp_month": 12, + "exp_year": 2024, + "last4": "abcd", + }, + } + + subscription_params = { + "default_payment_method": default_payment_method, + "cancel_at_period_end": False, + "current_period_end": 1633512445, + "latest_invoice": json.load(f)["data"][0], + "schedule_id": None, + "collection_method": "charge_automatically", + "tax_ids": None, + } + + retrieve_sub_mock.return_value = MockSubscription(subscription_params) + + kwargs = { + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + data = {"name": "John Doe", "billing_address": billing_address} + url = reverse("account_details-update-billing-address", kwargs=kwargs) + response = self.client.patch(url, data=data, format="json") + assert response.status_code == status.HTTP_200_OK + + retrieve_customer_mock.assert_called_once() + modify_payment_mock.assert_called_once() + modify_customer_mock.assert_called_once_with( + self.current_owner.stripe_customer_id, address=formatted_address + ) + + @patch("api.shared.permissions.get_provider") + def test_update_without_admin_permissions_returns_404(self, get_provider_mock): + get_provider_mock.return_value = GetAdminProviderAdapter() + owner = OwnerFactory() + response = self._update( + kwargs={"service": owner.service, "owner_username": owner.username}, data={} + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_update_can_change_name_and_email(self): + expected_name, expected_email = "Scooby Doo", "scoob@snack.com" + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"name": expected_name, "email": expected_email}, + ) + + assert response.data["name"] == expected_name + assert response.data["email"] == expected_email + self.current_owner.refresh_from_db() + assert self.current_owner.name == expected_name + assert self.current_owner.email == expected_email + + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.StripeService.modify_subscription") + def test_update_handles_stripe_error(self, retrieve_sub_mock, modify_sub_mock): + code, message = 402, "Not right, wrong in fact" + desired_plan = {"value": PlanName.CODECOV_PRO_MONTHLY.value, "quantity": 12} + self.current_owner.stripe_customer_id = "flsoe" + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.save() + retrieve_sub_mock.return_value = MockSubscription({}) + modify_sub_mock.side_effect = StripeError(message=message, http_status=code) + + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": desired_plan}, + ) + + assert response.status_code == code + assert response.data["detail"] == message + + @patch("services.billing.stripe.Subscription.retrieve") + @patch("api.internal.owner.serializers.send_sentry_webhook") + @patch("services.billing.StripeService.modify_subscription") + def test_update_sentry_plan_monthly( + self, modify_sub_mock, send_sentry_webhook, retrieve_sub_mock + ): + desired_plan = {"value": PlanName.SENTRY_MONTHLY.value, "quantity": 12} + self.current_owner.stripe_customer_id = "flsoe" + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.sentry_user_id = "sentry-user-id" + self.current_owner.save() + + self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": desired_plan}, + ) + send_sentry_webhook.assert_called_once_with( + self.current_owner, self.current_owner + ) + + @patch("api.internal.owner.serializers.send_sentry_webhook") + @patch("services.billing.StripeService.modify_subscription") + def test_update_sentry_plan_monthly_with_users_org( + self, modify_sub_mock, send_sentry_webhook + ): + desired_plan = {"value": PlanName.SENTRY_MONTHLY.value, "quantity": 12} + org = OwnerFactory( + service=Service.GITHUB.value, + service_id="923836740", + ) + + self.current_owner.stripe_customer_id = "flsoe" + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.sentry_user_id = "sentry-user-id" + self.current_owner.organizations = [org.ownerid] + self.current_owner.save() + + self._update( + kwargs={"service": org.service, "owner_username": org.username}, + data={"plan": desired_plan}, + ) + send_sentry_webhook.assert_called_once_with(self.current_owner, org) + + @patch("services.billing.stripe.Subscription.retrieve") + @patch("api.internal.owner.serializers.send_sentry_webhook") + @patch("services.billing.StripeService.modify_subscription") + def test_update_sentry_plan_annual( + self, + modify_sub_mock, + send_sentry_webhook, + retrieve_sub_mock, + ): + desired_plan = {"value": PlanName.SENTRY_YEARLY.value, "quantity": 12} + self.current_owner.stripe_customer_id = "flsoe" + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.sentry_user_id = "sentry-user-id" + self.current_owner.save() + + self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"plan": desired_plan}, + ) + send_sentry_webhook.assert_called_once_with( + self.current_owner, self.current_owner + ) + + @patch("api.internal.owner.serializers.send_sentry_webhook") + @patch("services.billing.StripeService.modify_subscription") + def test_update_sentry_plan_annual_with_users_org( + self, modify_sub_mock, send_sentry_webhook + ): + desired_plan = {"value": PlanName.SENTRY_YEARLY.value, "quantity": 12} + org = OwnerFactory( + service=Service.GITHUB.value, + service_id="923836740", + ) + self.current_owner.stripe_customer_id = "flsoe" + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.sentry_user_id = "sentry-user-id" + self.current_owner.organizations = [org.ownerid] + self.current_owner.save() + + self._update( + kwargs={"service": org.service, "owner_username": org.username}, + data={"plan": desired_plan}, + ) + send_sentry_webhook.assert_called_once_with(self.current_owner, org) + + @patch("api.internal.owner.serializers.send_sentry_webhook") + @patch("services.billing.StripeService.modify_subscription") + def test_update_sentry_plan_non_sentry_user( + self, modify_sub_mock, send_sentry_webhook + ): + desired_plan = {"value": PlanName.SENTRY_MONTHLY.value, "quantity": 5} + org = OwnerFactory( + service=Service.GITHUB.value, + service_id="923836740", + ) + self.current_owner.stripe_customer_id = "flsoe" + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.sentry_user_id = None + self.current_owner.organizations = [org.ownerid] + self.current_owner.save() + + res = self._update( + kwargs={"service": org.service, "owner_username": org.username}, + data={"plan": desired_plan}, + ) + + # cannot upgrade to Sentry plan + assert res.status_code == 400 + assert res.json() == { + "plan": { + "value": [ + f"Invalid value for plan: users-sentrym; must be one of ['users-pr-inappm', 'users-pr-inappy', 'users-teamm', 'users-teamy', '{DEFAULT_FREE_PLAN}']" + ] + } + } + + @patch("services.billing.stripe.Coupon.create") + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.Customer.modify") + def test_update_apply_cancellation_discount( + self, modify_customer_mock, retrieve_subscription_mock, coupon_create_mock + ): + coupon_create_mock.return_value = MagicMock(id="test-coupon-id") + + self.current_owner.plan = PlanName.CODECOV_PRO_MONTHLY.value + self.current_owner.stripe_customer_id = "flsoe" + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.save() + + subscription_params = { + "default_payment_method": None, + "cancel_at_period_end": False, + "current_period_end": 1633512445, + "latest_invoice": None, + "schedule_id": None, + "collection_method": "charge_automatically", + "customer_coupon": { + "name": "30% off for 6 months", + "percent_off": 30.0, + "duration_in_months": 6, + "created": int(datetime(2023, 1, 1, 0, 0, 0).timestamp()), + }, + "tax_ids": None, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"apply_cancellation_discount": True}, + ) + + modify_customer_mock.assert_called_once_with( + self.current_owner.stripe_customer_id, + coupon="test-coupon-id", + ) + + assert response.status_code == status.HTTP_200_OK + assert response.json()["subscription_detail"]["customer"]["discount"] == { + "name": "30% off for 6 months", + "percent_off": 30.0, + "duration_in_months": 6, + "expires": int(datetime(2023, 7, 1, 0, 0, 0).timestamp()), + } + + @patch("services.billing.stripe.Coupon.create") + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.Customer.modify") + def test_update_apply_cancellation_discount_yearly( + self, modify_customer_mock, retrieve_subscription_mock, coupon_create_mock + ): + coupon_create_mock.return_value = MagicMock(id="test-coupon-id") + + self.current_owner.plan = PlanName.CODECOV_PRO_YEARLY.value + self.current_owner.stripe_customer_id = "flsoe" + self.current_owner.stripe_subscription_id = "djfos" + self.current_owner.save() + + subscription_params = { + "default_payment_method": None, + "cancel_at_period_end": False, + "current_period_end": 1633512445, + "latest_invoice": None, + "schedule_id": None, + "collection_method": "charge_automatically", + "tax_ids": None, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + + response = self._update( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + }, + data={"apply_cancellation_discount": True}, + ) + + assert not modify_customer_mock.called + assert not coupon_create_mock.called + assert response.status_code == status.HTTP_200_OK + assert response.json()["subscription_detail"]["customer"]["discount"] is None + + @patch("services.task.TaskService.delete_owner") + def test_destroy_triggers_delete_owner_task(self, delete_owner_mock): + response = self._destroy( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + ) + assert response.status_code == status.HTTP_204_NO_CONTENT + delete_owner_mock.assert_called_once_with(self.current_owner.ownerid) + + def test_destroy_not_own_account_returns_404(self): + owner = OwnerFactory(admins=[self.current_owner.ownerid]) + response = self._destroy( + kwargs={"service": owner.service, "owner_username": owner.username} + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_retrieve_org_with_account(self): + account = AccountFactory( + name="Hello World", + plan_seat_count=5, + free_seat_count=3, + plan=PlanName.ENTERPRISE_CLOUD_YEARLY.value, + is_delinquent=False, + ) + InvoiceBillingFactory(is_active=True, account=account) + org_1 = OwnerFactory( + account=account, + service=Service.GITHUB.value, + username="Test", + delinquent=True, + uses_invoice=False, + ) + org_2 = OwnerFactory( + account=account, + service=Service.GITHUB.value, + ) + activated_owner = OwnerFactory( + user=UserFactory(), organizations=[org_1.ownerid, org_2.ownerid] + ) + account.users.add(activated_owner.user) + student_owner = OwnerFactory( + user=UserFactory(), + student=True, + organizations=[org_1.ownerid, org_2.ownerid], + ) + account.users.add(student_owner.user) + other_activated_owner = OwnerFactory( + user=UserFactory(), organizations=[org_2.ownerid] + ) + account.users.add(other_activated_owner.user) + other_student_owner = OwnerFactory( + user=UserFactory(), + student=True, + organizations=[org_2.ownerid], + ) + account.users.add(other_student_owner.user) + org_1.plan_activated_users = [activated_owner.ownerid, student_owner.ownerid] + org_1.admins = [activated_owner.ownerid] + org_1.save() + org_2.plan_activated_users = [ + activated_owner.ownerid, + student_owner.ownerid, + other_activated_owner.ownerid, + other_student_owner.ownerid, + ] + org_2.save() + + self.client.force_login_owner(activated_owner) + response = self._retrieve( + kwargs={"service": Service.GITHUB.value, "owner_username": org_1.username} + ) + assert response.status_code == status.HTTP_200_OK + # these fields are all overridden by account fields if the org has an account + self.assertEqual(org_1.activated_user_count, 1) + self.assertEqual(org_1.activated_student_count, 1) + self.assertTrue(org_1.delinquent) + self.assertFalse(org_1.uses_invoice) + self.assertEqual(org_1.plan_user_count, 1) + expected_response = { + "activated_user_count": 2, + "activated_student_count": 2, + "delinquent": False, + "uses_invoice": True, + "plan": { + "marketing_name": "Enterprise Cloud", + "value": PlanName.ENTERPRISE_CLOUD_YEARLY.value, + "billing_rate": "annually", + "base_unit_price": 10, + "benefits": [ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + "quantity": 5, + }, + "root_organization": None, + "integration_id": org_1.integration_id, + "plan_auto_activate": org_1.plan_auto_activate, + "inactive_user_count": 0, + "subscription_detail": None, + "checkout_session_id": None, + "name": org_1.name, + "email": org_1.email, + "nb_active_private_repos": 0, + "repo_total_credits": 99999999, + "plan_provider": org_1.plan_provider, + "student_count": 1, + "schedule_detail": None, + } + self.assertDictEqual(response.data["plan"], expected_response["plan"]) + self.assertDictEqual(response.data, expected_response) + + +@override_settings(IS_ENTERPRISE=True) +class EnterpriseAccountViewSetTests(APITestCase): + def _retrieve(self, kwargs={}): + if not kwargs: + kwargs = { + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + return self.client.get(reverse("account_details-detail", kwargs=kwargs)) + + def _update(self, kwargs, data): + return self.client.patch( + reverse("account_details-detail", kwargs=kwargs), data=data, format="json" + ) + + def _destroy(self, kwargs): + return self.client.delete(reverse("account_details-detail", kwargs=kwargs)) + + def setUp(self): + self.service = "gitlab" + self.current_owner = OwnerFactory( + stripe_customer_id=1000, + service=Service.GITHUB.value, + service_id="10238974029348", + ) + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + def test_retrieve_own_account_give_200(self): + response = self._retrieve( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + ) + assert response.status_code == status.HTTP_200_OK diff --git a/apps/codecov-api/api/internal/tests/views/test_compare_viewset.py b/apps/codecov-api/api/internal/tests/views/test_compare_viewset.py new file mode 100644 index 0000000000..b7fcf603c7 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/test_compare_viewset.py @@ -0,0 +1,462 @@ +from unittest.mock import PropertyMock, patch + +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) +from shared.reports.api_report_service import SerializableReport +from shared.reports.resources import ReportFile +from shared.reports.types import ReportTotals +from shared.utils.merge import LineType + +import services.comparison as comparison +from api.shared.commit.serializers import ReportTotalsSerializer +from utils.test_utils import Client + + +class MockSerializableReport(SerializableReport): + """ + Stubs the 'get' method of SerializableReport, which usually constructs + report files on the fly from information not provided by these test, like the chunks + for example. + """ + + def get(self, file_name): + return self.mocked_files.get(file_name) + + @property + def files(self): + return self.mocked_files.keys() + + def __contains__(self, f): + return f in self.mocked_files.keys() + + +class MockedComparisonAdapter: + def __init__(self, test_diff, test_lines=[]): + self.test_lines = test_lines + self.test_diff = test_diff + + async def get_source(self, file_name, commitid): + return {"content": self.test_lines} + + async def get_compare(self, base, head): + return self.test_diff + + async def get_authenticated(self): + return False, False + + +@patch("services.comparison.Comparison.head_report", new_callable=PropertyMock) +@patch("services.comparison.Comparison.base_report", new_callable=PropertyMock) +@patch("services.repo_providers.RepoProviderService.get_adapter") +class TestCompareViewSetRetrieve(APITestCase): + """ + Tests for retrieving a comparison. Does not test data that will be deprecated, + eg base and head report fields. Tests for commits etc will be added as the + compare-api refactor progresses. + """ + + def setUp(self): + self.file_name = "myfile.py" + + self.mock_git_compare_data = { + "commits": [], + "diff": { + "files": { + self.file_name: { + "type": "modified", + "segments": [ + { + "header": ["4", "43", "4", "3"], + "lines": ["", "", ""] + ["-this line is removed"] * 40, + } + ], + "stats": {"removed": 40, "added": 0}, + "totals": ReportTotals.default_totals(), + } + } + }, + } + + self.mocked_compare_adapter = MockedComparisonAdapter( + self.mock_git_compare_data + ) + + self.base_file = ReportFile( + name=self.file_name, totals=[46, 46, 0, 0, 100, 0, 0, 0, 1, 0, 0, 0] + ) + self.base_file._parsed_lines = [[1, "", [[1, 1, 0, 0, 0]], 0, 0]] * 46 + self.base_report = MockSerializableReport() + self.base_report.mocked_files = {self.file_name: self.base_file} + + self.head_file = ReportFile( + name=self.file_name, totals=[6, 6, 0, 0, 100, 0, 0, 0, 1, 0, 0, 0] + ) + self.head_file._parsed_lines = [[1, "", [[1, 1, 0, 0, 0]], 0, 0]] * 6 + self.head_file.totals.diff = ReportTotals.default_totals() + self.head_report = MockSerializableReport() + self.head_report.mocked_files = {self.file_name: self.head_file} + + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org) + self.base, self.head = ( + CommitFactory(repository=self.repo), + CommitFactory(repository=self.repo), + ) + self.current_owner = OwnerFactory( + service=self.org.service, + permission=[self.repo.repoid], + organizations=[self.org.ownerid], + ) + self.client = Client() + self.client.force_login_owner(self.current_owner) + + self.expected_files = [ + { + "name": {"base": self.file_name, "head": self.file_name}, + "totals": { + "base": ReportTotalsSerializer(self.base_file.totals).data, + "head": ReportTotalsSerializer(self.head_file.totals).data, + "patch": ReportTotalsSerializer(ReportTotals.default_totals()).data, + }, + "has_diff": True, + "stats": {"added": 0, "removed": 40}, + "change_summary": {}, + "lines": [ + { + "value": "", + "number": {"base": idx, "head": idx}, + "coverage": {"base": LineType.hit, "head": LineType.hit}, + "added": False, + "removed": False, + "is_diff": True, + "sessions": 1, + } + for idx in range(4, 7) + ] + + [ + { + "value": "-this line is removed", + "number": {"base": idx, "head": None}, + "coverage": {"base": LineType.hit, "head": None}, + "added": False, + "removed": True, + "is_diff": True, + "sessions": None, + } + for idx in range(7, 47) + ], + } + ] + + def _get_comparison(self, kwargs={}, query_params={}): + if kwargs == {}: + kwargs = { + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + } + if query_params == {}: + query_params = {"base": self.base.commitid, "head": self.head.commitid} + + return self.client.get( + reverse("compare-detail", kwargs=kwargs), + data=query_params, + content_type="application/json", + ) + + def _get_file_comparison(self, file_name="", kwargs={}, query_params={}): + if kwargs == {}: + kwargs = { + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "file_path": file_name or self.file_name, + } + if query_params == {}: + query_params = {"base": self.base.commitid, "head": self.head.commitid} + + return self.client.get( + reverse("compare-file", kwargs=kwargs), + data=query_params, + content_type="application/json", + ) + + def test_can_return_public_repo_comparison_with_not_authenticated( + self, adapter_mock, base_report_mock, head_report_mock + ): + adapter_mock.return_value = self.mocked_compare_adapter + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + public_repo = RepositoryFactory(author=self.org, private=False) + base, head = ( + CommitFactory(repository=public_repo), + CommitFactory(repository=public_repo), + ) + + self.client.logout() + response = self._get_comparison( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": public_repo.name, + }, + query_params={"base": base.commitid, "head": head.commitid}, + ) + assert response.status_code == status.HTTP_200_OK + + def test_returns_200_and_expected_files_on_success( + self, adapter_mock, base_report_mock, head_report_mock + ): + adapter_mock.return_value = self.mocked_compare_adapter + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + response = self._get_comparison() + + assert response.status_code == status.HTTP_200_OK + assert response.data["files"] == self.expected_files + + def test_returns_404_if_base_or_head_references_not_found( + self, adapter_mock, base_report_mock, head_report_mock + ): + response = self._get_comparison(query_params={"base": 12345, "head": 678}) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_returns_404_if_user_doesnt_have_permissions( + self, adapter_mock, base_report_mock, head_report_mock + ): + other_user = OwnerFactory() + self.client.force_login(user=other_user) + + adapter_mock.return_value = self.mocked_compare_adapter + + response = self._get_comparison() + + assert response.status_code == 404 + + @patch("redis.Redis.get", lambda self, key: None) + @patch("redis.Redis.set", lambda self, key, val, ex: None) + def test_accepts_pullid_query_param( + self, adapter_mock, base_report_mock, head_report_mock + ): + adapter_mock.return_value = self.mocked_compare_adapter + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + response = self._get_comparison( + query_params={ + "pullid": PullFactory( + base=self.base.commitid, + head=self.head.commitid, + compared_to=self.base.commitid, + pullid=2, + repository=self.repo, + ).pullid + } + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data["files"] == self.expected_files + + def test_pullid_with_nonexistent_base_returns_404( + self, adapter_mock, base_report_mock, head_report_mock + ): + adapter_mock.return_value = self.mocked_compare_adapter + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + response = self._get_comparison( + query_params={ + "pullid": PullFactory( + base="123456", + head=self.head.commitid, + pullid=2, + repository=self.repo, + ).pullid + } + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_pullid_with_nonexistent_head_returns_404( + self, adapter_mock, base_report_mock, head_report_mock + ): + adapter_mock.return_value = self.mocked_compare_adapter + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + response = self._get_comparison( + query_params={ + "pullid": PullFactory( + base=self.base.commitid, + head="123456", + compared_to=self.base.commitid, + pullid=2, + repository=self.repo, + ).pullid + } + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_diffs_larger_than_MAX_DIFF_SIZE_doesnt_include_lines( + self, adapter_mock, base_report_mock, head_report_mock + ): + adapter_mock.return_value = self.mocked_compare_adapter + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + previous_max = comparison.MAX_DIFF_SIZE + comparison.MAX_DIFF_SIZE = ( + len( + self.mock_git_compare_data["diff"]["files"][self.file_name]["segments"][ + 0 + ]["lines"] + ) + - 1 + ) + + response = self._get_comparison() + + assert response.status_code == status.HTTP_200_OK + assert ( + response.data["files"][0]["lines"] is None + ) # None means diff was truncated + + comparison.MAX_DIFF_SIZE = previous_max + + def test_file_returns_compare_file_with_diff_and_src_data( + self, adapter_mock, base_report_mock, head_report_mock + ): + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + src = b"first\nfirst\nfirst\nfirst\nfirst\nfirst" + + adapter_mock.return_value = MockedComparisonAdapter( + test_diff=self.mock_git_compare_data, test_lines=src + ) + + response = self._get_file_comparison() + + assert response.status_code == status.HTTP_200_OK + + expected_lines = [ + { + "value": "first", + "number": {"base": idx, "head": idx}, + "coverage": {"base": LineType.hit, "head": LineType.hit}, + "added": False, + "removed": False, + "is_diff": False, + "sessions": 1, + } + for idx in range(1, 4) + ] + self.expected_files[0]["lines"] + + assert response.data["lines"] == expected_lines + + def test_file_ignores_MAX_DIFF_SIZE( + self, adapter_mock, base_report_mock, head_report_mock + ): + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + previous_max = comparison.MAX_DIFF_SIZE + comparison.MAX_DIFF_SIZE = -1 + + src = b"first\nfirst\nfirst\nfirst\nfirst\nfirst" + adapter_mock.return_value = MockedComparisonAdapter( + test_diff=self.mock_git_compare_data, test_lines=src + ) + + response = self._get_file_comparison() + + comparison.MAX_DIFF_SIZE = previous_max + + assert response.status_code == status.HTTP_200_OK + assert len(response.data["lines"]) == 46 + + def test_missing_base_report_returns_none_base_totals( + self, adapter_mock, base_report_mock, head_report_mock + ): + base_report_mock.return_value = None + head_report_mock.return_value = self.head_report + adapter_mock.return_value = self.mocked_compare_adapter + + response = self._get_comparison() + + assert response.status_code == status.HTTP_200_OK + assert response.data["totals"]["base"] is None + + def test_no_raw_reports_returns_404( + self, adapter_mock, base_report_mock, head_report_mock + ): + base_report_mock.return_value = None + head_report_mock.side_effect = comparison.MissingComparisonReport( + "Missing head report" + ) + adapter_mock.return_value = self.mocked_compare_adapter + + response = self._get_comparison() + + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_returns_403_if_user_inactive( + self, adapter_mock, base_report_mock, head_report_mock + ): + self.org.plan = "users-inappy" + self.org.plan_auto_activate = False + self.org.save() + + response = self._get_comparison() + assert response.status_code == 403 + + @patch("redis.Redis.get", lambda self, key: None) + @patch("redis.Redis.set", lambda self, key, val, ex: None) + @patch( + "services.comparison.PullRequestComparison.pseudo_diff_adjusts_tracked_lines", + new_callable=PropertyMock, + ) + @patch( + "services.comparison.PullRequestComparison.update_base_report_with_pseudo_diff" + ) + def test_pull_request_pseudo_comparison_can_update_base_report( + self, + update_base_report_mock, + pseudo_diff_adjusts_tracked_lines_mock, + adapter_mock, + base_report_mock, + head_report_mock, + ): + adapter_mock.return_value = self.mocked_compare_adapter + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + pseudo_diff_adjusts_tracked_lines_mock.return_value = True + + response = self._get_comparison( + query_params={ + "pullid": PullFactory( + base=self.base.commitid, + head=self.head.commitid, + compared_to=self.base.commitid, + pullid=2, + repository=self.repo, + ).pullid + } + ) + + update_base_report_mock.assert_called_once() + + assert response.status_code == status.HTTP_200_OK + assert response.data["files"] == self.expected_files diff --git a/apps/codecov-api/api/internal/tests/views/test_coverage_viewset.py b/apps/codecov-api/api/internal/tests/views/test_coverage_viewset.py new file mode 100644 index 0000000000..b3248490e5 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/test_coverage_viewset.py @@ -0,0 +1,348 @@ +from unittest.mock import patch +from urllib.parse import urlencode + +from django.db import connection +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.django_apps.core.tests.factories import ( + BranchFactory, + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine +from shared.utils.sessions import Session + +from services.components import Component +from utils.test_utils import Client + + +def sample_report(): + report = Report() + first_file = ReportFile("foo/file1.py") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("bar/file2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + third_file = ReportFile("file3.py") + third_file.append(1, ReportLine.create(coverage=1, sessions=[[0, 1]])) + report.append(first_file) + report.append(second_file) + report.append(third_file) + report.add_session(Session(flags=["flag1", "flag2"])) + return report + + +class CoverageViewSetTests(APITestCase): + def _tree(self, **params): + url = reverse( + "coverage-tree", + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + "repo_name": self.repo.name, + }, + ) + + qs = urlencode(params) + url = f"{url}?{qs}" + + return self.client.get(url) + + def setUp(self): + self.current_owner = OwnerFactory() + self.repo = RepositoryFactory(author=self.current_owner) + + # the order in which these commits are created matters + # because the branch head is the one that is created + # later + self.commit1 = CommitFactory( + author=self.current_owner, + repository=self.repo, + ) + self.commit2 = CommitFactory( + author=self.current_owner, + repository=self.repo, + ) + self.branch = BranchFactory(repository=self.repo, name="test-branch") + + self.commit3 = CommitFactory( + author=self.current_owner, + repository=self.repo, + branch=self.branch.name, + ) + with connection.cursor() as cursor: + cursor.execute( + "UPDATE branches SET head = %s WHERE branches.repoid = %s AND branches.branch = %s", + [self.commit3.commitid, self.repo.repoid, self.branch.name], + ) + + self.client = Client() + self.client.force_login_owner(self.current_owner) + + @patch("shared.reports.api_report_service.build_report_from_commit") + @patch("services.components.commit_components") + def test_tree(self, commit_components_mock, build_report_from_commit): + commit_components_mock.return_value = [ + Component.from_dict( + { + "component_id": "global", + "name": "Global", + "paths": [".*/*.py"], + } + ), + ] + build_report_from_commit.return_value = sample_report() + res = self._tree(components="Global") + assert res.status_code == 200 + assert res.json() == [ + { + "name": "foo", + "full_path": "foo", + "coverage": 62.5, + "lines": 8, + "hits": 5, + "partials": 0, + "misses": 3, + "children": [ + { + "name": "file1.py", + "full_path": "foo/file1.py", + "coverage": 62.5, + "lines": 8, + "hits": 5, + "partials": 0, + "misses": 3, + } + ], + }, + { + "name": "bar", + "full_path": "bar", + "coverage": 50.0, + "lines": 2, + "hits": 1, + "partials": 1, + "misses": 0, + "children": [ + { + "name": "file2.py", + "full_path": "bar/file2.py", + "coverage": 50.0, + "lines": 2, + "hits": 1, + "partials": 1, + "misses": 0, + } + ], + }, + { + "name": "file3.py", + "full_path": "file3.py", + "coverage": 100.0, + "lines": 1, + "hits": 1, + "partials": 0, + "misses": 0, + }, + ] + + build_report_from_commit.assert_called_once_with(self.commit1) + commit_components_mock.assert_called_once_with(self.commit1, self.current_owner) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_tree_sha(self, build_report_from_commit): + build_report_from_commit.return_value = sample_report() + + res = self._tree(sha=self.commit2.commitid) + assert res.status_code == 200 + assert res.json() == [ + { + "name": "foo", + "full_path": "foo", + "coverage": 62.5, + "lines": 8, + "hits": 5, + "partials": 0, + "misses": 3, + "children": [ + { + "name": "file1.py", + "full_path": "foo/file1.py", + "coverage": 62.5, + "lines": 8, + "hits": 5, + "partials": 0, + "misses": 3, + } + ], + }, + { + "name": "bar", + "full_path": "bar", + "coverage": 50.0, + "lines": 2, + "hits": 1, + "partials": 1, + "misses": 0, + "children": [ + { + "name": "file2.py", + "full_path": "bar/file2.py", + "coverage": 50.0, + "lines": 2, + "hits": 1, + "partials": 1, + "misses": 0, + } + ], + }, + { + "name": "file3.py", + "full_path": "file3.py", + "coverage": 100.0, + "lines": 1, + "hits": 1, + "partials": 0, + "misses": 0, + }, + ] + + build_report_from_commit.assert_called_once_with(self.commit2) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_tree_missing_sha(self, build_report_from_commit): + build_report_from_commit.return_value = sample_report() + + res = self._tree(sha="wrong") + assert res.status_code == 404 + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_tree_branch(self, build_report_from_commit): + build_report_from_commit.return_value = sample_report() + + res = self._tree(branch="test-branch") + assert res.status_code == 200 + assert res.json() == [ + { + "name": "foo", + "full_path": "foo", + "coverage": 62.5, + "lines": 8, + "hits": 5, + "partials": 0, + "misses": 3, + "children": [ + { + "name": "file1.py", + "full_path": "foo/file1.py", + "coverage": 62.5, + "lines": 8, + "hits": 5, + "partials": 0, + "misses": 3, + } + ], + }, + { + "name": "bar", + "full_path": "bar", + "coverage": 50.0, + "lines": 2, + "hits": 1, + "partials": 1, + "misses": 0, + "children": [ + { + "name": "file2.py", + "full_path": "bar/file2.py", + "coverage": 50.0, + "lines": 2, + "hits": 1, + "partials": 1, + "misses": 0, + } + ], + }, + { + "name": "file3.py", + "full_path": "file3.py", + "coverage": 100.0, + "lines": 1, + "hits": 1, + "partials": 0, + "misses": 0, + }, + ] + + build_report_from_commit.assert_called_once_with(self.commit3) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_tree_missing_branch(self, build_report_from_commit): + build_report_from_commit.return_value = sample_report() + + res = self._tree(branch="wrong-branch") + assert res.status_code == 404 + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_tree_missing_report(self, build_report_from_commit): + build_report_from_commit.return_value = None + + res = self._tree() + assert res.status_code == 404 + + @patch("shared.reports.api_report_service.build_report_from_commit") + @patch("services.components.commit_components") + def test_tree_no_data_for_components( + self, commit_components_mock, build_report_from_commit + ): + commit_components_mock.return_value = [ + Component.from_dict( + { + "component_id": "c1", + "name": "ComponentOne", + "paths": ["dne.py"], + } + ), + ] + build_report_from_commit.return_value = sample_report() + res = self._tree(components="ComponentOne") + assert res.json() == [] + commit_components_mock.assert_called_once_with(self.commit1, self.current_owner) + + @patch("shared.reports.api_report_service.build_report_from_commit") + @patch("services.components.commit_components") + def test_tree_not_found_for_components( + self, commit_components_mock, build_report_from_commit + ): + commit_components_mock.return_value = [ + Component.from_dict( + { + "component_id": "c1", + "name": "ComponentOne", + "paths": ["dne.py"], + } + ), + ] + build_report_from_commit.return_value = sample_report() + res = self._tree(components="Does_not_exist") + assert res.status_code == 404 + commit_components_mock.assert_called_once_with(self.commit1, self.current_owner) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_tree_no_data_for_flags(self, build_report_from_commit): + build_report_from_commit.return_value = sample_report() + res = self._tree(flags="Does_not_exist") + assert res.json() == [] diff --git a/apps/codecov-api/api/internal/tests/views/test_current_user_view.py b/apps/codecov-api/api/internal/tests/views/test_current_user_view.py new file mode 100644 index 0000000000..4a8ac2044c --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/test_current_user_view.py @@ -0,0 +1,36 @@ +from django.utils import timezone +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory, UserFactory + + +class CurrentUserViewTests(APITestCase): + def setUp(self): + self.user = UserFactory(terms_agreement=True, terms_agreement_at=timezone.now()) + self.owner1 = OwnerFactory(user=self.user) + self.owner2 = OwnerFactory(user=self.user) + self.owner3 = OwnerFactory() + + def test_current_user_unauthenticated(self): + res = self.client.get(reverse("current-user")) + assert res.status_code == 401 + + def test_current_user_authenticated(self): + self.client.force_login(self.user) + res = self.client.get(reverse("current-user")) + assert res.status_code == 200 + data = res.data + ownerids = [owner["ownerid"] for owner in data.pop("owners")] + assert data == { + "email": self.user.email, + "name": self.user.name, + "external_id": str(self.user.external_id), + "terms_agreement": self.user.terms_agreement, + "terms_agreement_at": self.user.terms_agreement_at.strftime( + "%Y-%m-%dT%H:%M:%S.%fZ" + ), + } + assert len(ownerids) == 2 + assert self.owner1.ownerid in ownerids + assert self.owner2.ownerid in ownerids + assert self.owner3.ownerid not in ownerids diff --git a/apps/codecov-api/api/internal/tests/views/test_license_view.py b/apps/codecov-api/api/internal/tests/views/test_license_view.py new file mode 100644 index 0000000000..4a7189fc0d --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/test_license_view.py @@ -0,0 +1,78 @@ +from datetime import datetime +from unittest.mock import patch + +from django.test import RequestFactory, override_settings +from rest_framework.reverse import reverse +from shared.django_apps.core.tests.factories import OwnerFactory +from shared.license import LicenseInformation + +from api.internal.license.views import LicenseView +from codecov.tests.base_test import InternalAPITest +from utils.test_utils import Client + + +class LicenseViewTest(InternalAPITest): + def setUp(self): + self.current_owner = OwnerFactory() + self.client = Client() + self.client.force_login_owner(self.current_owner) + + @patch("api.internal.license.views.get_current_license") + def test_license_view(self, mocked_license): + mocked_license.return_value = LicenseInformation( + is_valid=True, + message=None, + url="https://codeov.mysite.com", + number_allowed_users=5, + number_allowed_repos=10, + expires=datetime.strptime("2020-05-09 00:00:00", "%Y-%m-%d %H:%M:%S"), + is_trial=True, + is_pr_billing=False, + ) + + request = RequestFactory().get("/") + view = LicenseView() + view.setup(request) + response = view.get(request) + + expected_result = { + "trial": True, + "url": "https://codeov.mysite.com", + "users": 5, + "repos": 10, + "expires_at": "2020-05-09T00:00:00Z", + "pr_billing": False, + } + + assert response.data == expected_result + + @override_settings(ROOT_URLCONF="api.internal.enterprise_urls") + @patch("api.internal.license.views.get_current_license") + def test_license_url(self, mocked_license): + mocked_license.return_value = LicenseInformation( + is_valid=True, + message=None, + url=None, + number_allowed_users=5, + number_allowed_repos=None, + expires=datetime.strptime("2020-05-09 00:00:00", "%Y-%m-%d %H:%M:%S"), + is_trial=True, + is_pr_billing=False, + ) + + response = self.client.get( + reverse( + "license", + ) + ) + + expected_result = { + "trial": True, + "url": None, + "users": 5, + "repos": None, + "expires_at": "2020-05-09T00:00:00Z", + "pr_billing": False, + } + + assert response.data == expected_result diff --git a/apps/codecov-api/api/internal/tests/views/test_owner_viewset.py b/apps/codecov-api/api/internal/tests/views/test_owner_viewset.py new file mode 100644 index 0000000000..282f389fb2 --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/test_owner_viewset.py @@ -0,0 +1,66 @@ +from rest_framework import status +from rest_framework.exceptions import ErrorDetail +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.django_apps.core.tests.factories import OwnerFactory + + +class OwnerViewSetTests(APITestCase): + def _retrieve(self, kwargs): + return self.client.get(reverse("owners-detail", kwargs=kwargs)) + + def setUp(self): + self.service = "bitbucket" + self.user = OwnerFactory(service="github", stripe_customer_id=1000) + + def test_retrieve_returns_owner_with_username(self): + owner = OwnerFactory() + response = self._retrieve( + kwargs={"service": owner.service, "owner_username": owner.username} + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": owner.service, + "username": owner.username, + "name": owner.name, + "stats": owner.cache["stats"], + "avatar_url": owner.avatar_url, + "ownerid": owner.ownerid, + "integration_id": owner.integration_id, + } + + def test_retrieve_returns_owner_with_period_username(self): + owner = OwnerFactory(username="codecov.test") + response = self._retrieve( + kwargs={"service": owner.service, "owner_username": owner.username} + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": owner.service, + "username": owner.username, + "name": owner.name, + "stats": owner.cache["stats"], + "avatar_url": owner.avatar_url, + "ownerid": owner.ownerid, + "integration_id": owner.integration_id, + } + + def test_retrieve_returns_404_if_no_matching_username(self): + response = self._retrieve(kwargs={"service": "github", "owner_username": "fff"}) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data == { + "detail": ErrorDetail( + string="No Owner matches the given query.", code="not_found" + ) + } + + def test_retrieve_owner_unknown_service_returns_404(self): + response = self._retrieve( + kwargs={"service": "not-real", "owner_username": "anything"} + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data == { + "detail": ErrorDetail( + string="Service not found: not-real", code="not_found" + ) + } diff --git a/apps/codecov-api/api/internal/tests/views/test_repo_view.py b/apps/codecov-api/api/internal/tests/views/test_repo_view.py new file mode 100644 index 0000000000..78634528fd --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/test_repo_view.py @@ -0,0 +1,941 @@ +from unittest.mock import patch + +from django.utils import timezone +from rest_framework.reverse import reverse +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.torngit.exceptions import TorngitClientGeneralError + +from api.internal.commit.serializers import CommitTotalsSerializer +from api.internal.tests.test_utils import GetAdminProviderAdapter +from codecov.tests.base_test import InternalAPITest +from core.models import Repository +from utils.test_utils import Client + + +class RepositoryViewSetTestSuite(InternalAPITest): + def _list(self, kwargs={}, query_params={}): + if kwargs == {}: + kwargs = {"service": self.org.service, "owner_username": self.org.username} + + return self.client.get(reverse("repos-list", kwargs=kwargs), data=query_params) + + def _retrieve(self, kwargs={}, data={}): + if kwargs == {}: + kwargs = { + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + } + return self.client.get(reverse("repos-detail", kwargs=kwargs), data=data) + + def _update(self, kwargs={}, data={}): + if kwargs == {}: + kwargs = { + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + } + return self.client.patch( + reverse("repos-detail", kwargs=kwargs), + data=data, + content_type="application/json", + ) + + def _destroy(self, kwargs={}): + if kwargs == {}: + kwargs = { + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + } + return self.client.delete(reverse("repos-detail", kwargs=kwargs)) + + +class TestRepositoryViewSetList(RepositoryViewSetTestSuite): + def setUp(self): + self.org = OwnerFactory(username="codecov", service="github") + + self.repo1 = RepositoryFactory( + author=self.org, active=True, private=True, name="A" + ) + self.repo2 = RepositoryFactory( + author=self.org, active=True, private=True, name="B" + ) + + repos_with_permission = [self.repo1.repoid, self.repo2.repoid] + + self.current_owner = OwnerFactory( + username="codecov-user", + service="github", + organizations=[self.org.ownerid], + permission=repos_with_permission, + ) + + self.client = Client() + self.client.force_login_owner(self.current_owner) + + def test_can_retrieve_repo_list_if_not_authenticated(self): + self.client.logout() + response = self._list() + assert response.status_code == 200 + + def test_order_by_updatestamp(self): + response = self._list(query_params={"ordering": "updatestamp"}) + + assert response.data["results"][0]["repoid"] == self.repo1.repoid + assert response.data["results"][1]["repoid"] == self.repo2.repoid + + reverse_response = self._list(query_params={"ordering": "-updatestamp"}) + + assert reverse_response.data["results"][0]["repoid"] == self.repo2.repoid + assert reverse_response.data["results"][1]["repoid"] == self.repo1.repoid + + def test_order_by_name(self): + response = self._list(query_params={"ordering": "name"}) + + assert response.data["results"][0]["repoid"] == self.repo1.repoid + assert response.data["results"][1]["repoid"] == self.repo2.repoid + + reverse_response = self._list(query_params={"ordering": "-name"}) + + assert reverse_response.data["results"][0]["repoid"] == self.repo2.repoid + assert reverse_response.data["results"][1]["repoid"] == self.repo1.repoid + + def test_order_by_coverage(self): + default_totals = { + "f": 1, + "n": 4, + "h": 4, + "m": 0, + "p": 0, + "c": 100.0, + "b": 0, + "d": 0, + "s": 1, + "C": 0.0, + "N": 0.0, + "diff": "", + } + + CommitFactory(repository=self.repo1, totals={**default_totals, "c": 25}) + CommitFactory(repository=self.repo1, totals={**default_totals, "c": 41}) + CommitFactory(repository=self.repo2, totals={**default_totals, "c": 32}) + + response = self._list(query_params={"ordering": "coverage"}) + + assert response.data["results"][0]["repoid"] == self.repo2.repoid + assert response.data["results"][1]["repoid"] == self.repo1.repoid + + reverse_response = self._list(query_params={"ordering": "-coverage"}) + + assert reverse_response.data["results"][0]["repoid"] == self.repo1.repoid + assert reverse_response.data["results"][1]["repoid"] == self.repo2.repoid + + def test_order_by_lines(self): + default_totals = { + "f": 1, + "n": 4, + "h": 4, + "m": 0, + "p": 0, + "c": 100.0, + "b": 0, + "d": 0, + "s": 1, + "C": 0.0, + "N": 0.0, + "diff": "", + } + + CommitFactory(repository=self.repo1, totals={**default_totals, "n": 25}) + CommitFactory(repository=self.repo2, totals={**default_totals, "n": 32}) + + response = self._list(query_params={"ordering": "lines"}) + + assert response.data["results"][0]["repoid"] == self.repo1.repoid + assert response.data["results"][1]["repoid"] == self.repo2.repoid + + reverse_response = self._list(query_params={"ordering": "-lines"}) + + assert reverse_response.data["results"][0]["repoid"] == self.repo2.repoid + assert reverse_response.data["results"][1]["repoid"] == self.repo1.repoid + + def test_totals_serializer(self): + default_totals = { + "f": 1, + "n": 4, + "h": 4, + "m": 0, + "p": 0, + "c": 100.0, + "b": 0, + "d": 0, + "s": 1, + "C": 0.0, + "N": 0.0, + "diff": "", + } + + CommitFactory(repository=self.repo1, totals=default_totals) + # Make sure we only get the commit from the default branch + CommitFactory( + repository=self.repo1, totals={**default_totals, "c": 90.0}, branch="other" + ) + + response = self._list(query_params={"names": "A"}) + + assert ( + response.data["results"][0]["latest_commit_totals"]["files"] + == default_totals["f"] + ) + assert ( + response.data["results"][0]["latest_commit_totals"]["lines"] + == default_totals["n"] + ) + assert ( + response.data["results"][0]["latest_commit_totals"]["hits"] + == default_totals["h"] + ) + assert ( + response.data["results"][0]["latest_commit_totals"]["misses"] + == default_totals["m"] + ) + assert ( + response.data["results"][0]["latest_commit_totals"]["partials"] + == default_totals["p"] + ) + assert ( + response.data["results"][0]["latest_commit_totals"]["coverage"] + == default_totals["c"] + ) + assert ( + response.data["results"][0]["latest_commit_totals"]["branches"] + == default_totals["b"] + ) + assert ( + response.data["results"][0]["latest_commit_totals"]["methods"] + == default_totals["d"] + ) + assert ( + response.data["results"][0]["latest_commit_totals"]["sessions"] + == default_totals["s"] + ) + assert ( + response.data["results"][0]["latest_commit_totals"]["complexity"] + == default_totals["C"] + ) + assert ( + response.data["results"][0]["latest_commit_totals"]["complexity_total"] + == default_totals["N"] + ) + assert ( + response.data["results"][0]["latest_commit_totals"]["complexity_ratio"] == 0 + ) + + def test_get_totals_with_timestamp(self): + default_totals = { + "f": 1, + "n": 4, + "h": 4, + "m": 0, + "p": 0, + "c": 100.0, + "b": 0, + "d": 0, + "s": 1, + "C": 0.0, + "N": 0.0, + "diff": "", + } + older_coverage = 90.0 + + CommitFactory( + repository=self.repo1, totals={**default_totals, "c": older_coverage} + ) + # We're testing that the lte works as expected, so we're not sending the exact same timestamp + fetching_time = timezone.now().isoformat() + + CommitFactory(repository=self.repo1, totals=default_totals) + + response = self._list(query_params={"names": "A", "before_date": fetching_time}) + + # The fetching truncates the time, so it will not take into account the time part of the date time + assert response.data["results"][0]["latest_commit_totals"]["coverage"] == 100.0 + + def test_get_repos_with_totals(self): + default_totals = { + "f": 1, + "n": 4, + "h": 4, + "m": 0, + "p": 0, + "c": 100.0, + "b": 0, + "d": 0, + "s": 1, + "C": 0.0, + "N": 0.0, + "diff": "", + } + + CommitFactory(repository=self.repo1, totals=default_totals) + + response = self._list(query_params={"exclude_uncovered": True}) + + assert response.data["count"] == 1 + + def test_get_active_repos(self): + RepositoryFactory(author=self.org, name="C") + response = self._list(query_params={"active": True}) + self.assertEqual(response.status_code, 200) + self.assertEqual( + len(response.data["results"]), + 2, + "got the wrong number of repos: {}".format(len(response.data["results"])), + ) + + def test_get_inactive_repos(self): + RepositoryFactory(author=self.org, name="C", private=False) + + response = self._list(query_params={"active": False}) + self.assertEqual(response.status_code, 200) + self.assertEqual( + len(response.data["results"]), + 1, + "got the wrong number of repos: {}".format(len(response.data["results"])), + ) + + def test_get_all_repos(self): + RepositoryFactory(author=self.org, name="C", private=False) + + response = self._list() + self.assertEqual(response.status_code, 200) + self.assertEqual( + len(response.data["results"]), + 3, + "got the wrong number of repos: {}".format(len(response.data["results"])), + ) + + def test_get_all_repos_by_name(self): + RepositoryFactory(author=self.org, name="C", private=False) + + response = self._list(query_params={"names": ["A", "B"]}) + self.assertEqual(response.status_code, 200) + self.assertEqual( + len(response.data["results"]), + 2, + "got the wrong number of repos: {}".format(len(response.data["results"])), + ) + + def test_returns_private_repos_if_user_has_permission(self): + new_repo = RepositoryFactory(author=self.org, name="C") + self.current_owner.permission.append(new_repo.repoid) + self.current_owner.save() + + response = self._list() + + assert response.status_code == 200 + assert len(response.data["results"]) == 3 + + def test_returns_private_repos_if_user_owns_repo(self): + new_repo = RepositoryFactory(author=self.current_owner, name="C") + + response = self._list( + { + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + ) + + assert response.status_code == 200 + assert new_repo.name in [repo["name"] for repo in response.data["results"]] + + def test_returns_public_repos_if_not_owned_by_user_and_not_in_permissions_array( + self, + ): + new_repo = RepositoryFactory(author=self.org, name="C", private=False) + + response = self._list() + + assert response.status_code == 200 + assert new_repo.name in [repo["name"] for repo in response.data["results"]] + + def test_doesnt_return_private_repos_if_above_conditions_not_met(self): + # Private repo, not owned by user, not in users permissions array + private_repo = RepositoryFactory(author=self.org, name="C", private=True) + + response = self._list() + + assert response.status_code == 200 + assert private_repo.name not in [ + repo["name"] for repo in response.data["results"] + ] + + def test_returns_latest_coverage_change(self): + CommitFactory( + totals={ + "f": 1, + "n": 4, + "h": 4, + "m": 0, + "p": 0, + "c": 100.0, + "b": 0, + "d": 0, + "s": 1, + "C": 0.0, + "N": 0.0, + "diff": "", + }, + repository=self.repo1, + ) + CommitFactory( + totals={ + "f": 1, + "n": 4, + "h": 4, + "m": 0, + "p": 0, + "c": 70.0, + "b": 0, + "d": 0, + "s": 1, + "C": 0.0, + "N": 0.0, + "diff": "", + }, + repository=self.repo1, + ) + + response = self._list() + repo1 = [repo for repo in response.data["results"] if repo["name"] == "A"][0] + assert repo1["latest_coverage_change"] == -30 + + def test_latest_commit_null(self): + response = self._list() + repo1 = [repo for repo in response.data["results"] if repo["name"] == "A"][0] + + # When the commit is missing, its set to None or empty string. + assert repo1["latest_commit_totals"] is None + + def test_returns_latest_commit_totals(self): + commit = CommitFactory(repository=self.repo1) + response = self._list() + repo1 = [repo for repo in response.data["results"] if repo["name"] == "A"][0] + + assert ( + repo1["latest_commit_totals"] == CommitTotalsSerializer(commit.totals).data + ) + + +class TestRepositoryViewSetExtraActions(RepositoryViewSetTestSuite): + def setUp(self): + self.org = OwnerFactory(username="codecov", service="github") + + self.repo1 = RepositoryFactory( + author=self.org, active=True, private=True, name="A" + ) + self.repo2 = RepositoryFactory( + author=self.org, active=True, private=True, name="B" + ) + self.repo1Commit1 = CommitFactory( + totals={ + "f": 1, + "n": 4, + "h": 4, + "m": 0, + "p": 0, + "c": 100.0, + "b": 0, + "d": 0, + "s": 1, + "C": 0.0, + "N": 0.0, + "diff": "", + }, + repository=self.repo1, + ) + self.repo1Commit2 = CommitFactory( + totals={ + "f": 1, + "n": 4, + "h": 0, + "m": 0, + "p": 4, + "c": 70.0, + "b": 0, + "d": 0, + "s": 1, + "C": 0.0, + "N": 0.0, + "diff": "", + }, + repository=self.repo1, + ) + self.repo2Commit1 = CommitFactory( + totals={ + "f": 1, + "n": 8, + "h": 4, + "m": 4, + "p": 0, + "c": 100.0, + "b": 0, + "d": 0, + "s": 1, + "C": 0.0, + "N": 0.0, + "diff": "", + }, + repository=self.repo2, + ) + self.repo2Commit2 = CommitFactory( + totals={ + "f": 1, + "n": 8, + "h": 3, + "m": 5, + "p": 0, + "c": 60.0, + "b": 0, + "d": 0, + "s": 1, + "C": 0.0, + "N": 0.0, + "diff": "", + }, + repository=self.repo2, + ) + + repos_with_permission = [self.repo1.repoid, self.repo2.repoid] + + self.current_owner = OwnerFactory( + username="codecov-user", + service="github", + organizations=[self.org.ownerid], + permission=repos_with_permission, + ) + + self.client = Client() + self.client.force_login_owner(self.current_owner) + + +@patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions") +class TestRepositoryViewSetDetailActions(RepositoryViewSetTestSuite): + def setUp(self): + self.org = OwnerFactory( + username="codecov", service="github", service_id="5767537" + ) + self.repo = RepositoryFactory( + author=self.org, + active=True, + private=True, + name="repo1", + service_id="201298242", + ) + + self.current_owner = OwnerFactory( + username="codecov-user", service="github", organizations=[self.org.ownerid] + ) + + self.client = Client() + self.client.force_login_owner(self.current_owner) + + def test_can_retrieve_repo_if_not_authenticated(self, mocked_get_permissions): + mocked_get_permissions.return_value = True, True + self.client.logout() + author = OwnerFactory() + public_repo = RepositoryFactory(author=author, private=False) + response = self._retrieve( + kwargs={ + "service": public_repo.author.service, + "owner_username": public_repo.author.username, + "repo_name": public_repo.name, + } + ) + assert response.status_code == 200 + + def test_cant_access_private_repo_if_not_authenticated( + self, mocked_get_permissions + ): + mocked_get_permissions.return_value = False, False + self.client.logout() + author = OwnerFactory() + public_repo = RepositoryFactory(author=author, private=True) + response = self._retrieve( + kwargs={ + "service": public_repo.author.service, + "owner_username": public_repo.author.username, + "repo_name": public_repo.name, + } + ) + assert response.status_code == 404 + + def test_retrieve_with_view_and_edit_permissions_succeeds( + self, mocked_get_permissions + ): + mocked_get_permissions.return_value = True, True + response = self._retrieve() + self.assertEqual(response.status_code, 200) + assert "upload_token" in response.data + + def test_retrieve_without_read_permissions_returns_404( + self, mocked_get_permissions + ): + mocked_get_permissions.return_value = False, False + response = self._retrieve() + assert response.status_code == 404 + + def test_retrieve_for_inactive_user_returns_403(self, mocked_get_permissions): + mocked_get_permissions.return_value = True, True + self.org.plan = "users-inappy" + self.org.plan_auto_activate = False + self.org.save() + + response = self._retrieve() + assert response.status_code == 403 + assert response.data["detail"] == "User not activated" + + def test_retrieve_without_edit_permissions_returns_detail_view_without_upload_token( + self, mocked_get_permissions + ): + mocked_get_permissions.return_value = True, False + response = self._retrieve() + assert response.status_code == 200 + assert "upload_token" not in response.data + + def test_destroy_repo_with_admin_rights_succeeds(self, mocked_get_permissions): + mocked_get_permissions.return_value = True, True + self.org.admins = [self.current_owner.ownerid] + self.org.save() + response = self._destroy() + assert response.status_code == 204 + assert not Repository.objects.filter(name="repo1").exists() + + @patch("api.shared.permissions.get_provider") + def test_destroy_repo_with_provider_admin_rights_succeedes( + self, mocked_get_provider, mocked_get_permissions + ): + mocked_get_provider.return_value = GetAdminProviderAdapter(result=True) + mocked_get_permissions.return_value = True, True + response = self._destroy() + assert response.status_code == 204 + assert not Repository.objects.filter(name="repo1").exists() + + @patch("api.shared.permissions.get_provider") + def test_destroy_repo_without_admin_rights_returns_403( + self, mocked_get_provider, mocked_get_permissions + ): + mocked_get_provider.return_value = GetAdminProviderAdapter() + mocked_get_permissions.return_value = True, True + + assert self.current_owner.ownerid not in self.org.admins + + response = self._destroy() + assert response.status_code == 403 + assert Repository.objects.filter(name="repo1").exists() + + def test_destroy_repo_as_inactive_user_returns_403(self, mocked_get_permissions): + mocked_get_permissions.return_value = True, True + self.org.admins = [self.current_owner.ownerid] + self.org.plan = "users-inappy" + self.org.plan_auto_activate = False + self.org.save() + + response = self._destroy() + assert response.status_code == 403 + assert response.data["detail"] == "User not activated" + assert Repository.objects.filter(name="repo1").exists() + + def test_update_default_branch_with_permissions_succeeds( + self, mocked_get_permissions + ): + mocked_get_permissions.return_value = True, True + new_default_branch = "dev" + + response = self._update(data={"branch": new_default_branch}) + + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.data["branch"], + "dev", + "got unexpected response: {}".format(response.data["branch"]), + ) + self.repo.refresh_from_db() + assert self.repo.branch == new_default_branch + + def test_update_default_branch_without_write_permissions_returns_403( + self, mocked_get_permissions + ): + mocked_get_permissions.return_value = True, False + + response = self._update(data={"branch": "dev"}) + self.assertEqual(response.status_code, 403) + + def test_retrieve_returns_yaml(self, mocked_get_permissions): + mocked_get_permissions.return_value = True, False + + yaml = {"yaml": "val"} + self.repo.yaml = yaml + self.repo.save() + + response = self._retrieve() + assert response.status_code == 200 + assert response.data["yaml"] == yaml + + def test_activation_checks_if_credits_available_for_legacy_users( + self, mocked_get_permissions + ): + mocked_get_permissions.return_value = True, True + + self.org.plan = "v4-10m" + self.org.save() + + for i in range(9): # including the one used by other tests, should be 10 total + RepositoryFactory( + name=str(i) + "random", author=self.org, private=True, active=True + ) + + RepositoryFactory(author=self.org, private=True, active=False) + + activation_data = {"active": True} + response = self._update(data=activation_data) + + assert response.status_code == 403 + + def test_repo_bot_returns_username_if_bot_not_null(self, mocked_get_permissions): + mocked_get_permissions.return_value = True, True + username = "huecoTanks" + self.repo.bot = OwnerFactory(username=username) + self.repo.save() + + response = self._retrieve() + + assert "bot" in response.data + assert response.data["bot"] == username + + def test_retrieve_with_no_commits_doesnt_crash(self, mocked_get_permissions): + mocked_get_permissions.return_value = True, True + + self.repo.commits.all().delete() + + response = self._retrieve() + assert response.status_code == 200 + + @patch("shared.api_archive.archive.ArchiveService.read_chunks", lambda obj, _: "") + def test_retrieve_returns_latest_commit_data(self, mocked_get_permissions): + self.maxDiff = None + mocked_get_permissions.return_value = True, True + commit = CommitFactory( + repository=self.repo, + _report={ + "files": { + "test_file_1.py": [ + 2, + [1, 10, 8, 2, 5, "80.00000", 6, 7, 9, 8, 20, 40, 13], + [[0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0]], + [0, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + ], + "test_file_2.py": [ + 0, + [1, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0], + [[0, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + }, + "sessions": {}, + }, + ) + + from api.internal.commit.serializers import CommitWithFileLevelReportSerializer + + expected_commit_payload = CommitWithFileLevelReportSerializer(commit).data + + response = self._retrieve() + assert response.status_code == 200 + assert ( + response.data["latest_commit"]["report"]["totals"] + == expected_commit_payload["report"]["totals"] + ) + self.assertEqual( + response.data["latest_commit"]["report"]["files"], + [ + { + "name": "test_file_1.py", + "totals": { + "files": 1, + "lines": 10, + "hits": 8, + "misses": 2, + "partials": 5, + "coverage": 80.0, + "branches": 6, + "methods": 7, + "sessions": 8, + "complexity": 20.0, + "complexity_total": 40.0, + "complexity_ratio": 50.0, + "diff": 0, + }, + }, + { + "name": "test_file_2.py", + "totals": { + "files": 1, + "lines": 3, + "hits": 2, + "misses": 1, + "partials": 0, + "coverage": 66.66, + "branches": 0, + "methods": 0, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "complexity_ratio": 0, + "diff": 0, + }, + }, + ], + ) + + @patch("shared.api_archive.archive.ArchiveService.read_chunks", lambda obj, _: "") + def test_retrieve_returns_latest_commit_of_default_branch_if_branch_not_specified( + self, mocked_get_permissions + ): + mocked_get_permissions.return_value = True, True + + commit = CommitFactory(repository=self.repo) + more_recent_commit = CommitFactory(repository=self.repo, branch="other-branch") + + response = self._retrieve() + + assert response.data["latest_commit"]["commitid"] == commit.commitid + assert response.data["latest_commit"]["commitid"] != more_recent_commit.commitid + + @patch("shared.api_archive.archive.ArchiveService.read_chunks", lambda obj, _: "") + def test_retrieve_accepts_branch_query_param_to_specify_latest_commit( + self, mocked_get_permissions + ): + mocked_get_permissions.return_value = True, True + + commit = CommitFactory(repository=self.repo, branch="other-branch") + more_recent_commit = CommitFactory(repository=self.repo) + + response = self._retrieve(data={"branch": "other-branch"}) + + assert response.data["latest_commit"]["commitid"] == commit.commitid + assert response.data["latest_commit"]["commitid"] != more_recent_commit.commitid + + @patch("shared.api_archive.archive.ArchiveService.read_chunks", lambda obj, _: "") + def test_latest_commit_is_none_if_dne(self, mocked_get_permissions): + mocked_get_permissions.return_value = True, True + + response = self._retrieve() + + assert response.data["latest_commit"] is None + + def test_can_retrieve_repo_name_containing_dot(self, mocked_get_permissions): + mocked_get_permissions.return_value = True, True + + self.repo.name = "codecov.io" + self.repo.save() + + response = self._retrieve() + self.assertEqual(response.status_code, 200) + + # Note (Matt): the only special char that github isn't + # filtering is . + def test_can_retrieve_repo_name_containing_special_char( + self, mocked_get_permissions + ): + mocked_get_permissions.return_value = True, True + self.repo.name = "codec@v.i" + self.repo.save() + + response = self._retrieve() + self.assertEqual(response.status_code, 200) + + def test_permissions_check_handles_torngit_error(self, mocked_get_permissions): + err_code, err_message = 403, "yo, no." + mocked_get_permissions.side_effect = TorngitClientGeneralError( + err_code, message=err_message, response_data=None + ) + response = self._retrieve() + assert response.status_code == err_code + assert response.data == {"detail": err_message} + + @patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_details") + def test_get_object_handles_torngit_error( + self, mocked_get_details, mocked_get_perms + ): + mocked_get_perms.return_value = True, True + err_code, err_message = 403, "yo, no." + mocked_get_details.side_effect = TorngitClientGeneralError( + status_code=err_code, message=err_message, response_data=None + ) + response = self._retrieve() + assert response.status_code == err_code + assert response.data == {"detail": err_message} + + @patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_details") + @patch( + "api.shared.repo.repository_accessors.RepoAccessors.fetch_from_git_and_create_repo" + ) + def test_create_repo_on_fetch_if_dne( + self, mocked_fetch_and_create, mocked_get_repo_details, mocked_get_permissions + ): + mocked_get_permissions.return_value = True, True + mocked_get_repo_details.return_value = None + mocked_fetch_and_create.return_value = self.repo + + response = self._retrieve( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + } + ) + mocked_fetch_and_create.assert_called() + mocked_fetch_and_create.assert_called() + self.assertEqual(response.status_code, 200) + + @patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_details") + @patch( + "api.shared.repo.repository_accessors.RepoAccessors.fetch_from_git_and_create_repo" + ) + def test_unable_to_fetch_git_repo( + self, mocked_fetch_and_create, mocked_get_repo_details, mocked_get_permissions + ): + mocked_get_permissions.return_value = True, True + mocked_get_repo_details.return_value = None + mocked_fetch_and_create.side_effect = TorngitClientGeneralError( + 403, response_data=None, message="Forbidden" + ) + + response = self._retrieve( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": "new-repo", + } + ) + mocked_fetch_and_create.assert_called() + mocked_fetch_and_create.assert_called() + self.assertEqual(response.status_code, 403) + + def test_fetch_repo_with_fork_doesnt_crash(self, mocked_get_perms): + mocked_get_perms.return_value = True, True + author = OwnerFactory() + repo = RepositoryFactory(author=author, fork=RepositoryFactory()) + self._retrieve( + kwargs={ + "service": repo.author.service, + "owner_username": author.username, + "repo_name": repo.name, + } + ) diff --git a/apps/codecov-api/api/internal/tests/views/test_self_hosted_settings_viewset.py b/apps/codecov-api/api/internal/tests/views/test_self_hosted_settings_viewset.py new file mode 100644 index 0000000000..404e5437ff --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/test_self_hosted_settings_viewset.py @@ -0,0 +1,26 @@ +from django.test import TestCase, override_settings +from rest_framework.reverse import reverse +from shared.django_apps.core.tests.factories import OwnerFactory + +from utils.test_utils import APIClient + + +@override_settings(IS_ENTERPRISE=True, ROOT_URLCONF="api.internal.enterprise_urls") +class SettingsViewsetUnauthenticatedTestCase(TestCase): + def test_settings(self): + res = self.client.get(reverse("selfhosted-users-list")) + # not authenticated + assert res.status_code == 401 + + +@override_settings(IS_ENTERPRISE=True, ROOT_URLCONF="api.internal.enterprise_urls") +class SettingsViewsetNonadminTestCase(TestCase): + def setUp(self): + self.current_owner = OwnerFactory() + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + def test_settings(self): + res = self.client.get(reverse("selfhosted-users-list")) + # not authenticated + assert res.status_code == 403 diff --git a/apps/codecov-api/api/internal/tests/views/test_self_hosted_user_viewset.py b/apps/codecov-api/api/internal/tests/views/test_self_hosted_user_viewset.py new file mode 100644 index 0000000000..9bbcdc486c --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/test_self_hosted_user_viewset.py @@ -0,0 +1,293 @@ +from unittest.mock import patch + +from django.test import TestCase, override_settings +from rest_framework.reverse import reverse +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov_auth.models import Owner +from services.self_hosted import activate_owner, is_activated_owner +from utils.test_utils import APIClient + + +@override_settings(IS_ENTERPRISE=True, ROOT_URLCONF="api.internal.enterprise_urls") +class UserViewsetUnauthenticatedTestCase(TestCase): + def test_list_users(self): + res = self.client.get(reverse("selfhosted-users-list")) + # not authenticated + assert res.status_code == 401 + + +@override_settings(IS_ENTERPRISE=True, ROOT_URLCONF="api.internal.enterprise_urls") +class UserViewsetTestCase(TestCase): + def setUp(self): + self.owner = OwnerFactory() + self.current_owner = OwnerFactory(organizations=[self.owner.ownerid]) + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + +class UserViewsetAuthenticatedTestCase(UserViewsetTestCase): + def test_list_users(self): + res = self.client.get(reverse("selfhosted-users-list")) + # not an admin + assert res.status_code == 403 + + def test_detail(self): + other_owner = OwnerFactory() + + res = self.client.get( + reverse("selfhosted-users-detail", kwargs={"pk": other_owner.pk}) + ) + assert res.status_code == 403 + + def test_detail_self(self): + res = self.client.get( + reverse("selfhosted-users-detail", kwargs={"pk": self.current_owner.pk}) + ) + assert res.status_code == 403 + + def test_current(self): + res = self.client.get(reverse("selfhosted-users-current")) + assert res.status_code == 200 + assert res.json() == { + "ownerid": self.current_owner.pk, + "username": self.current_owner.username, + "email": self.current_owner.email, + "name": self.current_owner.name, + "is_admin": False, + "activated": False, + } + + @patch("services.self_hosted.license_seats") + def test_current_update(self, license_seats): + license_seats.return_value = 5 + + org = OwnerFactory() + self.current_owner.organizations = [org.pk] + self.current_owner.save() + + res = self.client.patch( + reverse("selfhosted-users-current"), data={"activated": True}, format="json" + ) + assert res.status_code == 200 + assert res.json() == { + "ownerid": self.current_owner.pk, + "username": self.current_owner.username, + "email": self.current_owner.email, + "name": self.current_owner.name, + "is_admin": False, + "activated": True, + } + assert is_activated_owner(self.current_owner) == True + + +class UserViewsetAdminTestCase(UserViewsetTestCase): + @patch("services.self_hosted.admin_owners") + def test_list_users(self, admin_owners): + admin_owners.return_value = Owner.objects.filter(pk__in=[self.current_owner.pk]) + + OwnerFactory() + OwnerFactory(oauth_token=None, organizations=[self.owner.ownerid]) + activated_owner = OwnerFactory( + oauth_token=None, + organizations=None, + ) + self.owner.plan_activated_users = [activated_owner.pk] + self.owner.save() + + res = self.client.get(reverse("selfhosted-users-list")) + assert res.status_code == 200 + assert res.json() == { + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "ownerid": self.current_owner.pk, + "username": self.current_owner.username, + "email": self.current_owner.email, + "name": self.current_owner.name, + "is_admin": True, + "activated": False, + }, + { + "ownerid": activated_owner.pk, + "username": activated_owner.username, + "email": activated_owner.email, + "name": activated_owner.name, + "is_admin": False, + "activated": True, + }, + ], + "total_pages": 1, + } + + @patch("services.self_hosted.admin_owners") + def test_list_users_filter_admin(self, admin_owners): + admin_owners.return_value = Owner.objects.filter(pk__in=[self.current_owner.pk]) + + OwnerFactory() + + res = self.client.get(reverse("selfhosted-users-list"), {"is_admin": True}) + assert res.status_code == 200 + assert res.json() == { + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "ownerid": self.current_owner.pk, + "username": self.current_owner.username, + "email": self.current_owner.email, + "name": self.current_owner.name, + "is_admin": True, + "activated": False, + }, + ], + "total_pages": 1, + } + + @patch("services.self_hosted.activated_owners") + @patch("services.self_hosted.admin_owners") + def test_list_users_filter_activated(self, admin_owners, activated_owners): + admin_owners.return_value = Owner.objects.filter(pk__in=[self.current_owner.pk]) + + other_owner = OwnerFactory(organizations=[self.owner.ownerid]) + activated_owners.return_value = Owner.objects.filter(pk__in=[other_owner.pk]) + + res = self.client.get(reverse("selfhosted-users-list"), {"activated": True}) + assert res.status_code == 200 + assert res.json() == { + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "ownerid": other_owner.pk, + "username": other_owner.username, + "email": other_owner.email, + "name": other_owner.name, + "is_admin": False, + "activated": True, + }, + ], + "total_pages": 1, + } + + @patch("services.self_hosted.admin_owners") + def test_list_users_search(self, admin_owners): + admin_owners.return_value = Owner.objects.filter(pk__in=[self.current_owner.pk]) + + other_owner = OwnerFactory( + username="foobar", organizations=[self.owner.ownerid] + ) + + res = self.client.get(reverse("selfhosted-users-list"), {"search": "foo"}) + assert res.status_code == 200 + assert res.json() == { + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "ownerid": other_owner.pk, + "username": other_owner.username, + "email": other_owner.email, + "name": other_owner.name, + "is_admin": False, + "activated": False, + }, + ], + "total_pages": 1, + } + + @patch("services.self_hosted.admin_owners") + def test_detail(self, admin_owners): + admin_owners.return_value = Owner.objects.filter(pk__in=[self.current_owner.pk]) + + other_owner = OwnerFactory(organizations=[self.owner.ownerid]) + + res = self.client.get( + reverse("selfhosted-users-detail", kwargs={"pk": other_owner.pk}) + ) + assert res.status_code == 200 + assert res.json() == { + "ownerid": other_owner.pk, + "username": other_owner.username, + "email": other_owner.email, + "name": other_owner.name, + "is_admin": False, + "activated": False, + } + + @patch("services.self_hosted.license_seats") + @patch("services.self_hosted.admin_owners") + def test_update_activate(self, admin_owners, license_seats): + admin_owners.return_value = Owner.objects.filter(pk__in=[self.current_owner.pk]) + license_seats.return_value = 5 + + org = OwnerFactory() + other_owner = OwnerFactory(organizations=[org.pk]) + + res = self.client.patch( + reverse("selfhosted-users-detail", kwargs={"pk": other_owner.pk}), + data={"activated": True}, + format="json", + ) + assert res.status_code == 200 + assert res.json() == { + "ownerid": other_owner.pk, + "username": other_owner.username, + "email": other_owner.email, + "name": other_owner.name, + "is_admin": False, + "activated": True, + } + assert is_activated_owner(other_owner) == True + + @patch("services.self_hosted.license_seats") + @patch("services.self_hosted.admin_owners") + def test_update_activate_no_more_seats(self, admin_owners, license_seats): + admin_owners.return_value = Owner.objects.filter(pk__in=[self.current_owner.pk]) + license_seats.return_value = 0 + + org = OwnerFactory() + other_owner = OwnerFactory(organizations=[org.pk]) + + res = self.client.patch( + reverse("selfhosted-users-detail", kwargs={"pk": other_owner.pk}), + data={"activated": True}, + format="json", + ) + assert res.status_code == 403 + assert res.json() == { + "detail": "No seats remaining. Please contact Codecov support or deactivate users." + } + assert is_activated_owner(other_owner) == False + + @patch("services.self_hosted.license_seats") + @patch("services.self_hosted.admin_owners") + def test_update_deactivate(self, admin_owners, license_seats): + admin_owners.return_value = Owner.objects.filter(pk__in=[self.current_owner.pk]) + license_seats.return_value = 5 + + org = OwnerFactory() + other_owner = OwnerFactory(organizations=[org.pk]) + + activate_owner(other_owner) + + res = self.client.patch( + reverse("selfhosted-users-detail", kwargs={"pk": other_owner.pk}), + data={"activated": False}, + format="json", + ) + assert res.status_code == 200 + assert res.json() == { + "ownerid": other_owner.pk, + "username": other_owner.username, + "email": other_owner.email, + "name": other_owner.name, + "is_admin": False, + "activated": False, + } + assert is_activated_owner(other_owner) == False diff --git a/apps/codecov-api/api/internal/tests/views/test_slack_view.py b/apps/codecov-api/api/internal/tests/views/test_slack_view.py new file mode 100644 index 0000000000..2339dd789f --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/test_slack_view.py @@ -0,0 +1,99 @@ +from django.test import override_settings +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov_auth.models import UserToken + +codecov_internal_token = "test3n4d079myhiy9fu7d3j7gsepz80df3da" + + +class SlackViewSetTests(APITestCase): + def setUp(self): + self.owner = OwnerFactory() + self.data = {"username": self.owner.username, "service": self.owner.service} + + def test_generate_access_token_missing_headers(self): + response = self.client.post( + reverse( + "generate-token", + ) + ) + + assert response.status_code == 401 + assert response.data == { + "detail": "Authentication credentials were not provided." + } + + def test_generate_access_token_with_invalid_token(self): + response = self.client.post( + reverse( + "generate-token", + ), + data=self.data, + HTTP_AUTHORIZATION=f"Bearer {codecov_internal_token}", + ) + assert response.status_code == 401 + assert response.data == {"detail": "Invalid token."} + + @override_settings(CODECOV_INTERNAL_TOKEN=codecov_internal_token) + def test_generate_access_token_with_invalid_owner(self): + response = self.client.post( + reverse( + "generate-token", + ), + HTTP_AUTHORIZATION=f"Bearer {codecov_internal_token}", + data={ + "username": "random-owner", + "service": self.owner.service, + }, + ) + + assert response.status_code == 404 + assert response.data == {"detail": "Owner not found"} + + @override_settings(CODECOV_INTERNAL_TOKEN=codecov_internal_token) + def test_generate_access_token_with_invalid_service(self): + response = self.client.post( + reverse( + "generate-token", + ), + HTTP_AUTHORIZATION=f"Bearer {codecov_internal_token}", + data={ + "username": self.owner.username, + "service": "random-service", + }, + ) + + assert response.status_code == 400 + + @override_settings(CODECOV_INTERNAL_TOKEN=codecov_internal_token) + def test_generate_access_token_already_exists(self): + UserToken.objects.create( + name="slack-codecov-access-token", + owner=self.owner, + token_type=UserToken.TokenType.API.value, + ) + response = self.client.post( + reverse( + "generate-token", + ), + HTTP_AUTHORIZATION=f"Bearer {codecov_internal_token}", + data=self.data, + ) + + assert response.status_code == 200 + assert response.data["token"] == self.owner.user_tokens.first().token + + @override_settings(CODECOV_INTERNAL_TOKEN=codecov_internal_token) + def test_generate_access_token_success(self): + response = self.client.post( + reverse( + "generate-token", + ), + data=self.data, + HTTP_AUTHORIZATION=f"Bearer {codecov_internal_token}", + ) + + assert response.status_code == 200 + assert response.data["token"] == self.owner.user_tokens.first().token diff --git a/apps/codecov-api/api/internal/tests/views/test_user_viewset.py b/apps/codecov-api/api/internal/tests/views/test_user_viewset.py new file mode 100644 index 0000000000..32bee8b1eb --- /dev/null +++ b/apps/codecov-api/api/internal/tests/views/test_user_viewset.py @@ -0,0 +1,561 @@ +from datetime import datetime +from unittest.mock import patch + +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.django_apps.codecov_auth.tests.factories import PlanFactory, TierFactory +from shared.django_apps.core.tests.factories import ( + OwnerFactory, + PullFactory, + RepositoryFactory, +) +from shared.plan.constants import DEFAULT_FREE_PLAN, TierName + +from core.models import Pull +from utils.test_utils import APIClient + + +class UserViewSetTests(APITestCase): + def setUp(self): + non_org_active_user = OwnerFactory() + tier = TierFactory(tier_name=TierName.BASIC.value) + plan = PlanFactory(name=DEFAULT_FREE_PLAN, tier=tier) + self.current_owner = OwnerFactory( + plan=plan.name, + plan_user_count=5, + plan_activated_users=[non_org_active_user.ownerid], + ) + self.users = [ + non_org_active_user, + OwnerFactory(organizations=[self.current_owner.ownerid]), + OwnerFactory(organizations=[self.current_owner.ownerid]), + OwnerFactory(organizations=[self.current_owner.ownerid]), + ] + + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + def _list(self, kwargs={}, query_params={}): + if not kwargs: + kwargs = { + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + } + return self.client.get(reverse("users-list", kwargs=kwargs), data=query_params) + + def _patch(self, kwargs, data): + return self.client.patch(reverse("users-detail", kwargs=kwargs), data=data) + + def test_list_returns_200_and_user_list_on_success(self): + response = self._list() + assert response.status_code == status.HTTP_200_OK + expected = [ + { + "name": user.name, + "is_admin": False, + "activated": user.ownerid in self.current_owner.plan_activated_users, + "username": user.username, + "email": user.email, + "ownerid": user.ownerid, + "student": user.student, + "last_pull_timestamp": None, + } + for user in self.users + ] + self.assertCountEqual(response.data["results"], expected) + + def test_list_sets_activated(self): + self.current_owner.plan_activated_users = [self.users[0].ownerid] + self.current_owner.save() + + response = self._list() + + assert response.status_code == status.HTTP_200_OK + assert response.data["results"][0] == { + "name": self.users[0].name, + "activated": True, + "is_admin": False, + "username": self.users[0].username, + "email": self.users[0].email, + "ownerid": self.users[0].ownerid, + "student": self.users[0].student, + "last_pull_timestamp": None, + } + + def test_list_sets_is_admin(self): + self.current_owner.admins = [self.users[1].ownerid] + self.current_owner.save() + + response = self._list() + + assert response.status_code == status.HTTP_200_OK + assert response.data["results"][1] == { + "name": self.users[1].name, + "activated": False, + "is_admin": True, + "username": self.users[1].username, + "email": self.users[1].email, + "ownerid": self.users[1].ownerid, + "student": self.users[1].student, + "last_pull_timestamp": None, + } + + def test_list_can_filter_by_activated(self): + self.current_owner.plan_activated_users = [self.users[0].ownerid] + self.current_owner.save() + + response = self._list(query_params={"activated": True}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["results"] == [ + { + "name": self.users[0].name, + "activated": True, + "is_admin": False, + "username": self.users[0].username, + "email": self.users[0].email, + "ownerid": self.users[0].ownerid, + "student": self.users[0].student, + "last_pull_timestamp": None, + } + ] + + def test_list_can_filter_by_is_admin(self): + self.current_owner.admins = [self.users[1].ownerid] + self.current_owner.save() + + response = self._list(query_params={"is_admin": True}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["results"] == [ + { + "name": self.users[1].name, + "activated": False, + "is_admin": True, + "username": self.users[1].username, + "email": self.users[1].email, + "ownerid": self.users[1].ownerid, + "student": self.users[1].student, + "last_pull_timestamp": None, + } + ] + + def test_list_can_search_by_username(self): + # search_fields = ["name", "username", "email"], cannot have any overlaps + self.users[0].name = "thor45" # non_org_active_user + self.users[0].username = "thor45" + self.users[0].email = "thor45@gmail.com" + self.users[0].save() + self.users[1].name = "thanos" + self.users[1].username = "thanos" + self.users[1].email = "huntrobert@gmail.com" + self.users[1].save() + self.users[2].name = "thor23" + self.users[2].username = "thor23" + self.users[2].email = "thor23@gmail.com" + self.users[2].save() + self.users[3].name = "thor" + self.users[3].username = "thor" + self.users[3].email = "thor@gmail.com" + self.users[3].save() + + expected_response = [ + { + "name": self.users[3].name, + "activated": False, + "is_admin": False, + "username": "thor", + "email": self.users[3].email, + "ownerid": self.users[3].ownerid, + "student": self.users[3].student, + "last_pull_timestamp": None, + }, + { + "name": self.users[2].name, + "activated": False, + "is_admin": False, + "username": "thor23", + "email": self.users[2].email, + "ownerid": self.users[2].ownerid, + "student": self.users[2].student, + "last_pull_timestamp": None, + }, + { + "name": self.users[0].name, + "activated": True, + "is_admin": False, + "username": "thor45", + "email": self.users[0].email, + "ownerid": self.users[0].ownerid, + "student": self.users[0].student, + "last_pull_timestamp": None, + }, + ] + + response = self._list(query_params={"search": "hor", "ordering": "name"}) + assert response.status_code == status.HTTP_200_OK + assert len(response.data["results"]) == len(expected_response) + assert response.data["results"] == expected_response + + def test_list_can_search_by_name(self): + # search_fields = ["name", "username", "email"], cannot have any overlaps + self.users[0].name = "thanos" # non_org_active_user + self.users[0].username = "thanos" + self.users[0].email = "huntrobert@gmail.com" + self.users[0].save() + self.users[1].name = "thor23" + self.users[1].username = "thor23" + self.users[1].email = "thor23@gmail.com" + self.users[1].save() + self.users[2].name = "thor" + self.users[2].username = "thor" + self.users[2].email = "thor@gmail.com" + self.users[2].save() + self.users[3].name = "loki" + self.users[3].username = "loki" + self.users[3].email = "loki@gmail.com" + self.users[3].save() + + expected_result = [ + { + "name": "thor", + "activated": False, + "is_admin": False, + "username": self.users[2].username, + "email": self.users[2].email, + "ownerid": self.users[2].ownerid, + "student": self.users[2].student, + "last_pull_timestamp": None, + }, + { + "name": "thor23", + "activated": False, + "is_admin": False, + "username": self.users[1].username, + "email": self.users[1].email, + "ownerid": self.users[1].ownerid, + "student": self.users[1].student, + "last_pull_timestamp": None, + }, + ] + + response = self._list(query_params={"search": "tho", "ordering": "name"}) + assert response.status_code == status.HTTP_200_OK + assert len(response.data["results"]) == len(expected_result) + assert response.data["results"] == expected_result + + def test_list_can_search_by_email(self): + # search_fields = ["name", "username", "email"], cannot have any overlaps + self.users[0].name = "thanos" # non_org_active_user + self.users[0].username = "thanos" + self.users[0].email = "thanos@gmail.com" + self.users[0].save() + self.users[1].name = "ironman" + self.users[1].username = "ironman" + self.users[1].email = "ironman@gmail.com" + self.users[1].save() + self.users[2].name = "thor" + self.users[2].username = "thor" + self.users[2].email = "thor@gmail.com" + self.users[2].save() + self.users[3].name = "loki" + self.users[3].username = "loki" + self.users[3].email = "loki@gmail.com" + self.users[3].save() + + expected_response = [ + { + "name": self.users[0].name, + "activated": True, + "is_admin": False, + "username": self.users[0].username, + "email": "thanos@gmail.com", + "ownerid": self.users[0].ownerid, + "student": self.users[0].student, + "last_pull_timestamp": None, + }, + { + "name": self.users[2].name, + "activated": False, + "is_admin": False, + "username": self.users[2].username, + "email": "thor@gmail.com", + "ownerid": self.users[2].ownerid, + "student": self.users[2].student, + "last_pull_timestamp": None, + }, + ] + + response = self._list(query_params={"search": "th", "ordering": "name"}) + assert response.status_code == status.HTTP_200_OK + assert len(response.data["results"]) == len(expected_response) + assert response.data["results"] == expected_response + + def test_list_can_order_by_name(self): + self.users[0].name = "a" + self.users[0].save() + self.users[1].name = "b" + self.users[1].save() + self.users[2].name = "c" + self.users[2].save() + self.users[3].name = "d" + self.users[3].save() + + response = self._list(query_params={"ordering": "name"}) + + assert [r["name"] for r in response.data["results"]] == ["a", "b", "c", "d"] + + response = self._list(query_params={"ordering": "-name"}) + + assert [r["name"] for r in response.data["results"]] == ["d", "c", "b", "a"] + + def test_list_can_order_by_username(self): + self.users[0].username = "a" + self.users[0].save() + self.users[1].username = "b" + self.users[1].save() + self.users[2].username = "c" + self.users[2].save() + self.users[3].username = "d" + self.users[3].save() + + response = self._list(query_params={"ordering": "username"}) + + assert [r["username"] for r in response.data["results"]] == ["a", "b", "c", "d"] + + response = self._list(query_params={"ordering": "-username"}) + + assert [r["username"] for r in response.data["results"]] == ["d", "c", "b", "a"] + + def test_list_can_order_by_email(self): + self.users[0].email = "a" + self.users[0].save() + self.users[1].email = "b" + self.users[1].save() + self.users[2].email = "c" + self.users[2].save() + self.users[3].email = "d" + self.users[3].save() + + response = self._list(query_params={"ordering": "email"}) + + assert [r["email"] for r in response.data["results"]] == ["a", "b", "c", "d"] + + response = self._list(query_params={"ordering": "-email"}) + + assert [r["email"] for r in response.data["results"]] == ["d", "c", "b", "a"] + + def test_list_can_order_by_activated(self): + self.users[0].activated = False + self.users[0].save() + self.users[1].activated = True + self.users[1].save() + self.users[2].activated = False + self.users[2].save() + self.users[3].activated = False + self.users[3].save() + + response = self._list(query_params={"ordering": "activated"}) + + assert [r["activated"] for r in response.data["results"]] == [ + False, + False, + False, + True, + ] + + response = self._list(query_params={"ordering": "-activated"}) + + assert [r["activated"] for r in response.data["results"]] == [ + True, + False, + False, + False, + ] + + @patch("api.internal.owner.views.on_enterprise_plan") + def test_list_can_order_by_last_pull_timestamp(self, on_enterprise_plan): + on_enterprise_plan.return_value = True + + repo = RepositoryFactory() + pull1 = PullFactory(author=self.users[1], repository=repo) + pull2 = PullFactory(author=self.users[2], repository=repo) + + # avoid `save` which sets `updatestamp` + Pull.objects.filter(pk=pull1.pk).update( + updatestamp=datetime(2022, 1, 1, 0, 0, 0) + ) + Pull.objects.filter(pk=pull2.pk).update( + updatestamp=datetime(2022, 2, 1, 0, 0, 0) + ) + + response = self._list(query_params={"ordering": "last_pull_timestamp"}) + assert [r["ownerid"] for r in response.data["results"]] == [ + self.users[0].ownerid, + self.users[3].ownerid, + self.users[1].ownerid, + self.users[2].ownerid, + ] + assert [r["last_pull_timestamp"] for r in response.data["results"]] == [ + None, + None, + datetime(2022, 1, 1, 0, 0, 0), + datetime(2022, 2, 1, 0, 0, 0), + ] + + response = self._list(query_params={"ordering": "-last_pull_timestamp"}) + assert [r["ownerid"] for r in response.data["results"]] == [ + self.users[2].ownerid, + self.users[1].ownerid, + self.users[0].ownerid, + self.users[3].ownerid, + ] + assert [r["last_pull_timestamp"] for r in response.data["results"]] == [ + datetime(2022, 2, 1, 0, 0, 0), + datetime(2022, 1, 1, 0, 0, 0), + None, + None, + ] + + def test_patch_with_ownerid(self): + response = self._patch( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + "user_username_or_ownerid": self.users[0].ownerid, + }, + data={"activated": True}, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "name": self.users[0].name, + "activated": True, + "is_admin": False, + "username": self.users[0].username, + "email": self.users[0].email, + "ownerid": self.users[0].ownerid, + "student": self.users[0].student, + "last_pull_timestamp": None, + } + + def test_patch_can_set_activated_to_true(self): + response = self._patch( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + "user_username_or_ownerid": self.users[0].username, + }, + data={"activated": True}, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "name": self.users[0].name, + "activated": True, + "is_admin": False, + "username": self.users[0].username, + "email": self.users[0].email, + "ownerid": self.users[0].ownerid, + "student": self.users[0].student, + "last_pull_timestamp": None, + } + + self.current_owner.refresh_from_db() + assert self.users[0].ownerid in self.current_owner.plan_activated_users + + def test_patch_can_set_activated_to_false(self): + # setup activated user + self.current_owner.plan_activated_users = [self.users[1].ownerid] + self.current_owner.save() + + response = self._patch( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + "user_username_or_ownerid": self.users[1].username, + }, + data={"activated": False}, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "name": self.users[1].name, + "activated": False, + "is_admin": False, + "username": self.users[1].username, + "email": self.users[1].email, + "ownerid": self.users[1].ownerid, + "student": self.users[1].student, + "last_pull_timestamp": None, + } + + self.current_owner.refresh_from_db() + assert self.users[1].ownerid not in self.current_owner.plan_activated_users + + @patch("codecov_auth.models.Owner.can_activate_user", lambda self, user: False) + def test_patch_returns_403_if_cannot_activate_user(self): + response = self._patch( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + "user_username_or_ownerid": self.users[0].username, + }, + data={"activated": True}, + ) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + def test_patch_can_set_is_admin_to_true(self): + response = self._patch( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + "user_username_or_ownerid": self.users[2].username, + }, + data={"is_admin": True}, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "name": self.users[2].name, + "activated": False, + "is_admin": True, + "username": self.users[2].username, + "email": self.users[2].email, + "ownerid": self.users[2].ownerid, + "student": self.users[2].student, + "last_pull_timestamp": None, + } + + self.current_owner.refresh_from_db() + assert self.users[2].ownerid in self.current_owner.admins + + def test_patch_can_set_is_admin_to_false(self): + self.current_owner.admins = [self.users[2].ownerid] + self.current_owner.save() + + response = self._patch( + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + "user_username_or_ownerid": self.users[2].username, + }, + data={"is_admin": False}, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "name": self.users[2].name, + "activated": False, + "is_admin": False, + "username": self.users[2].username, + "email": self.users[2].email, + "ownerid": self.users[2].ownerid, + "student": self.users[2].student, + "last_pull_timestamp": None, + } + + self.current_owner.refresh_from_db() + assert self.users[2].ownerid not in self.current_owner.admins diff --git a/apps/codecov-api/api/internal/urls.py b/apps/codecov-api/api/internal/urls.py new file mode 100644 index 0000000000..312eb44523 --- /dev/null +++ b/apps/codecov-api/api/internal/urls.py @@ -0,0 +1,79 @@ +from django.conf import settings, urls +from django.urls import include, path +from rest_framework.exceptions import server_error + +from api.internal.branch.views import BranchViewSet +from api.internal.commit.views import CommitsViewSet +from api.internal.compare.views import CompareViewSet +from api.internal.coverage.views import CoverageViewSet +from api.internal.enterprise_urls import urlpatterns as enterprise_urlpatterns +from api.internal.feature.views import FeaturesView +from api.internal.owner.views import ( + AccountDetailsViewSet, + OwnerViewSet, + UserViewSet, +) +from api.internal.pull.views import PullViewSet +from api.internal.repo.views import RepositoryViewSet +from api.internal.user.views import CurrentUserView +from api.shared.error_views import not_found +from utils.routers import OptionalTrailingSlashRouter, RetrieveUpdateDestroyRouter + +urls.handler404 = not_found +urls.handler500 = server_error + + +owners_router = OptionalTrailingSlashRouter() +owners_router.register(r"owners", OwnerViewSet, basename="owners") + +owner_artifacts_router = OptionalTrailingSlashRouter() +owner_artifacts_router.register(r"users", UserViewSet, basename="users") + +account_details_router = RetrieveUpdateDestroyRouter() +account_details_router.register( + r"account-details", AccountDetailsViewSet, basename="account_details" +) + +repository_router = OptionalTrailingSlashRouter() +repository_router.register(r"repos", RepositoryViewSet, basename="repos") + +repository_artifacts_router = OptionalTrailingSlashRouter() +repository_artifacts_router.register(r"pulls", PullViewSet, basename="pulls") +repository_artifacts_router.register(r"commits", CommitsViewSet, basename="commits") +repository_artifacts_router.register(r"branches", BranchViewSet, basename="branches") +repository_artifacts_router.register(r"coverage", CoverageViewSet, basename="coverage") + +compare_router = RetrieveUpdateDestroyRouter() +compare_router.register(r"compare", CompareViewSet, basename="compare") + +urlpatterns = [] + +if settings.IS_ENTERPRISE: + urlpatterns += enterprise_urlpatterns + +urlpatterns += [ + path("user", CurrentUserView.as_view(), name="current-user"), + path("slack/", include("api.internal.slack.urls")), + path("charts/", include("api.internal.chart.urls")), + path("/", include(owners_router.urls)), + path("//", include(owner_artifacts_router.urls)), + path("//", include(account_details_router.urls)), + path("//", include(repository_router.urls)), + path( + "///", + include(repository_artifacts_router.urls), + ), + path( + "//repos//", + include(repository_artifacts_router.urls), + ), + path( + "///", + include(compare_router.urls), + ), + path( + "//repos//", + include(compare_router.urls), + ), + path("features", FeaturesView.as_view(), name="features"), +] diff --git a/apps/codecov-api/api/internal/user/serializers.py b/apps/codecov-api/api/internal/user/serializers.py new file mode 100644 index 0000000000..873494980d --- /dev/null +++ b/apps/codecov-api/api/internal/user/serializers.py @@ -0,0 +1,21 @@ +from rest_framework import serializers + +from api.internal.owner.serializers import OwnerSerializer +from codecov_auth.models import User + + +class UserSerializer(serializers.ModelSerializer): + owners = OwnerSerializer(many=True) + + class Meta: + model = User + fields = ( + "email", + "name", + "external_id", + "owners", + "terms_agreement", + "terms_agreement_at", + ) + + read_only_fields = fields diff --git a/apps/codecov-api/api/internal/user/views.py b/apps/codecov-api/api/internal/user/views.py new file mode 100644 index 0000000000..5104db9f2d --- /dev/null +++ b/apps/codecov-api/api/internal/user/views.py @@ -0,0 +1,13 @@ +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from .serializers import UserSerializer + + +class CurrentUserView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + serializer = UserSerializer(request.user) + return Response(serializer.data) diff --git a/apps/codecov-api/api/public/__init__.py b/apps/codecov-api/api/public/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/public/v1/__init__.py b/apps/codecov-api/api/public/v1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/public/v1/permissions.py b/apps/codecov-api/api/public/v1/permissions.py new file mode 100644 index 0000000000..0293691238 --- /dev/null +++ b/apps/codecov-api/api/public/v1/permissions.py @@ -0,0 +1,10 @@ +from rest_framework.permissions import BasePermission + + +class PullUpdatePermission(BasePermission): + def has_permission(self, request, view): + return ( + request.auth + and "upload" in request.auth.get_scopes() + and view.repo in request.auth.get_repositories() + ) diff --git a/apps/codecov-api/api/public/v1/serializers.py b/apps/codecov-api/api/public/v1/serializers.py new file mode 100644 index 0000000000..2f783694c4 --- /dev/null +++ b/apps/codecov-api/api/public/v1/serializers.py @@ -0,0 +1,22 @@ +from rest_framework import serializers + +from core.models import Pull + + +class PullSerializer(serializers.ModelSerializer): + class Meta: + model = Pull + read_only_fields = ( + "pullid", + "title", + "base", + "head", + "compared_to", + "updatestamp", + "state", + ) + fields = read_only_fields + ("user_provided_base_sha",) + + +class PullIdSerializer(serializers.Serializer): + pullid = serializers.IntegerField() diff --git a/apps/codecov-api/api/public/v1/tests/views/test_pull_viewset.py b/apps/codecov-api/api/public/v1/tests/views/test_pull_viewset.py new file mode 100644 index 0000000000..f83e2b4386 --- /dev/null +++ b/apps/codecov-api/api/public/v1/tests/views/test_pull_viewset.py @@ -0,0 +1,142 @@ +import json +from unittest.mock import patch + +from rest_framework.test import APIClient, APITestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) + +from core.models import Pull + + +class PullViewSetTests(APITestCase): + def setUp(self): + self.client = APIClient() + self.org = OwnerFactory(username="codecov", service="github") + other_org = OwnerFactory(username="other_org") + # Create different types of repos / pulls + self.repo = RepositoryFactory(author=self.org, name="testRepoName", active=True) + self.other_repo = RepositoryFactory( + author=other_org, name="otherRepoName", active=True + ) + repo_with_permission = [self.repo.repoid] + self.user = OwnerFactory( + username="codecov-user", + service="github", + organizations=[self.org.ownerid], + permission=repo_with_permission, + ) + self.open_pull = PullFactory( + pullid=10, + author=self.org, + repository=self.repo, + state="open", + head=CommitFactory(repository=self.repo, author=self.user).commitid, + base=CommitFactory(repository=self.repo, author=self.user).commitid, + ) + PullFactory(pullid=11, author=self.org, repository=self.repo, state="closed") + PullFactory(pullid=12, author=other_org, repository=self.other_repo) + self.correct_kwargs = { + "service": "github", + "owner_username": "codecov", + "repo_name": "testRepoName", + } + + def test_get_pulls(self): + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.repo.upload_token) + response = self.client.get("/api/github/codecov/testRepoName/pulls/") + self.assertEqual(response.status_code, 200) + content = json.loads(response.content.decode()) + self.assertEqual( + len(content["results"]), + 3, + "got the wrong number of pulls: {}".format(content["results"]), + ) + + def test_get_pulls_wrong_repo_token(self): + self.client.credentials( + HTTP_AUTHORIZATION="Token " + self.other_repo.upload_token + ) + response = self.client.get("/api/github/codecov/testRepoName/pulls/") + self.assertEqual(response.status_code, 403) + + def test_get_pulls_no_permissions(self): + response = self.client.get("/api/github/codecov/testRepoName/pulls/") + self.assertEqual(response.status_code, 401) + + def test_get_pull(self): + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.repo.upload_token) + response = self.client.get("/api/github/codecov/testRepoName/pulls/10/") + self.assertEqual(response.status_code, 200) + content = json.loads(response.content.decode()) + self.assertEqual(content["pullid"], 10) + + def test_get_pull_no_permissions(self): + self.client.credentials( + HTTP_AUTHORIZATION="Token " + self.other_repo.upload_token + ) + response = self.client.get("/api/github/codecov/testRepoName/pulls/10/") + self.assertEqual(response.status_code, 403) + + @patch("services.task.TaskService.pulls_sync") + def test_update_pull_user_provided_base(self, pulls_sync_mock): + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.repo.upload_token) + response = self.client.put( + "/api/github/codecov/testRepoName/pulls/10/", + {"user_provided_base_sha": "new-sha"}, + ) + self.assertEqual(response.status_code, 200) + content = json.loads(response.content.decode()) + self.assertEqual(content["user_provided_base_sha"], "new-sha") + self.assertEqual( + Pull.objects.get(pullid=10, repository=self.repo).user_provided_base_sha, + "new-sha", + ) + pulls_sync_mock.assert_called_once_with(repoid=self.repo.repoid, pullid="10") + + def test_update_pull_user_provided_base_no_permissions(self): + self.client.credentials( + HTTP_AUTHORIZATION="Token " + self.other_repo.upload_token + ) + response = self.client.put( + "/api/github/codecov/testRepoName/pulls/10/", + {"user_provided_base_sha": "new-sha"}, + ) + self.assertEqual(response.status_code, 403) + + @patch("services.task.TaskService.pulls_sync") + def test_create_new_pull_user_provided_base(self, pulls_sync_mock): + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.repo.upload_token) + self.client.force_login(user=self.user) + response = self.client.put( + "/api/github/codecov/testRepoName/pulls/15/", + {"user_provided_base_sha": "new-sha"}, + ) + self.assertEqual(response.status_code, 200) + content = json.loads(response.content.decode()) + self.assertEqual(content["user_provided_base_sha"], "new-sha") + self.assertEqual( + Pull.objects.get(pullid=15, repository=self.repo).user_provided_base_sha, + "new-sha", + ) + pulls_sync_mock.assert_called_once_with(repoid=self.repo.repoid, pullid="15") + + @patch("services.task.TaskService.pulls_sync") + def test_post_pull_user_provided_base(self, pulls_sync_mock): + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.repo.upload_token) + response = self.client.post( + "/api/github/codecov/testRepoName/pulls/15/", + {"user_provided_base_sha": "new-sha"}, + ) + self.assertEqual(response.status_code, 405) + assert not pulls_sync_mock.called + + def test_get_pull_no_pullid_provided(self): + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.repo.upload_token) + response = self.client.get("/api/github/codecov/testRepoName/pulls/abc") + self.assertEqual(response.status_code, 400) + content = json.loads(response.content.decode()) + self.assertEqual(content["pullid"], ["A valid integer is required."]) diff --git a/apps/codecov-api/api/public/v1/urls.py b/apps/codecov-api/api/public/v1/urls.py new file mode 100644 index 0000000000..0542d5379e --- /dev/null +++ b/apps/codecov-api/api/public/v1/urls.py @@ -0,0 +1,15 @@ +from django.urls import include, path + +from api.public.v1.views import PullViewSet +from utils.routers import OptionalTrailingSlashRouter + +repository_router = OptionalTrailingSlashRouter() +repository_router.register(r"pulls", PullViewSet, basename="pulls") + + +urlpatterns = [ + path( + "///", + include(repository_router.urls), + ) +] diff --git a/apps/codecov-api/api/public/v1/views.py b/apps/codecov-api/api/public/v1/views.py new file mode 100644 index 0000000000..b2e8546812 --- /dev/null +++ b/apps/codecov-api/api/public/v1/views.py @@ -0,0 +1,59 @@ +import logging + +from django.db.models import OuterRef, QuerySet, Subquery +from django.shortcuts import get_object_or_404 +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import filters, mixins, viewsets + +from api.shared.mixins import RepoPropertyMixin +from codecov_auth.authentication.repo_auth import RepositoryLegacyTokenAuthentication +from core.models import Commit, Pull +from services.task import TaskService + +from .permissions import PullUpdatePermission +from .serializers import PullIdSerializer, PullSerializer + +log = logging.getLogger(__name__) + + +class PullViewSet( + RepoPropertyMixin, + viewsets.GenericViewSet, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, +): + serializer_class = PullSerializer + filter_backends = [DjangoFilterBackend, filters.OrderingFilter] + filterset_fields = ["state"] + ordering_fields = ("pullid",) + authentication_classes = [RepositoryLegacyTokenAuthentication] + permission_classes = [PullUpdatePermission] + + def get_object(self) -> Pull: + serializer = PullIdSerializer(data={"pullid": self.kwargs.get("pk")}) + serializer.is_valid(raise_exception=True) + pullid = serializer.validated_data["pullid"] + + if self.request.method == "PUT": + # Note: We create a new pull if needed to make sure that they can be updated + # with a base before the upload has finished processing. + obj, _created = self.get_queryset().get_or_create( + repository=self.repo, pullid=pullid + ) + return obj + return get_object_or_404(self.get_queryset(), pullid=pullid) + + def get_queryset(self) -> QuerySet: + return self.repo.pull_requests.annotate( + ci_passed=Subquery( + Commit.objects.filter( + commitid=OuterRef("head"), repository=OuterRef("repository") + ).values("ci_passed")[:1] + ) + ) + + def perform_update(self, serializer: PullSerializer) -> Pull: + result = super().perform_update(serializer) + TaskService().pulls_sync(repoid=self.repo.repoid, pullid=self.kwargs.get("pk")) + return result diff --git a/apps/codecov-api/api/public/v2/__init__.py b/apps/codecov-api/api/public/v2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/public/v2/branch/__init__.py b/apps/codecov-api/api/public/v2/branch/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/public/v2/branch/serializers.py b/apps/codecov-api/api/public/v2/branch/serializers.py new file mode 100644 index 0000000000..289634f6bb --- /dev/null +++ b/apps/codecov-api/api/public/v2/branch/serializers.py @@ -0,0 +1,33 @@ +from rest_framework import serializers + +from api.public.v2.commit.serializers import CommitDetailSerializer +from core.models import Branch, Commit + + +class BranchSerializer(serializers.ModelSerializer): + name = serializers.CharField(label="branch name") + updatestamp = serializers.DateTimeField(label="last updated timestamp") + + class Meta: + model = Branch + fields = ("name", "updatestamp") + + +class BranchDetailSerializer(BranchSerializer): + head_commit = serializers.SerializerMethodField( + label="branch's current head commit" + ) + + def get_head_commit(self, branch: Branch) -> CommitDetailSerializer: + commit = ( + Commit.objects.filter( + repository_id=branch.repository_id, commitid=branch.head + ) + .defer("_report") + .first() + ) + return CommitDetailSerializer(commit).data + + class Meta: + model = Branch + fields = BranchSerializer.Meta.fields + ("head_commit",) diff --git a/apps/codecov-api/api/public/v2/branch/views.py b/apps/codecov-api/api/public/v2/branch/views.py new file mode 100644 index 0000000000..6d2d7119c6 --- /dev/null +++ b/apps/codecov-api/api/public/v2/branch/views.py @@ -0,0 +1,50 @@ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema +from rest_framework import mixins + +from api.public.v2.schema import repo_parameters +from api.shared.branch.mixins import BranchViewSetMixin +from core.models import Branch + +from .serializers import BranchDetailSerializer, BranchSerializer + + +@extend_schema(parameters=repo_parameters, tags=["Branches"]) +class BranchViewSet( + BranchViewSetMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, +): + queryset = Branch.objects.none() + lookup_value_regex = "[^/]+" + + def get_serializer_class(self): + if self.action == "retrieve": + return BranchDetailSerializer + elif self.action == "list": + return BranchSerializer + + @extend_schema(summary="Branch list") + def list(self, request, *args, **kwargs): + """ + Returns a paginated list of branches for the specified repository + """ + return super().list(request, *args, **kwargs) + + @extend_schema( + summary="Branch detail", + parameters=[ + OpenApiParameter( + "name", + OpenApiTypes.STR, + OpenApiParameter.PATH, + description="branch name", + ), + ], + ) + def retrieve(self, request, *args, **kwargs): + """ + Returns a single branch by name. + Includes head commit information embedded in the response. + """ + return super().retrieve(request, *args, **kwargs) diff --git a/apps/codecov-api/api/public/v2/commit/__init__.py b/apps/codecov-api/api/public/v2/commit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/public/v2/commit/serializers.py b/apps/codecov-api/api/public/v2/commit/serializers.py new file mode 100644 index 0000000000..6e8903a413 --- /dev/null +++ b/apps/codecov-api/api/public/v2/commit/serializers.py @@ -0,0 +1,91 @@ +from rest_framework import serializers + +from api.public.v2.owner.serializers import OwnerSerializer +from api.shared.commit.serializers import ( + CommitTotalsSerializer, + ReportSerializer, + UploadTotalsSerializer, +) +from api.shared.serializers import StringListField +from core.models import Commit +from reports.models import ReportSession + + +class CommitSerializer(serializers.ModelSerializer): + commitid = serializers.CharField(label="commit SHA") + message = serializers.CharField(label="commit message") + timestamp = serializers.DateTimeField(label="timestamp when commit was made") + ci_passed = serializers.BooleanField( + label="indicates whether the CI process passed for this commit" + ) + author = OwnerSerializer(label="author of the commit") + branch = serializers.CharField( + label="branch name on which this commit currently lives" + ) + totals = CommitTotalsSerializer(label="coverage totals") + state = serializers.ChoiceField( + label="Codecov processing state for this commit", + choices=Commit.CommitStates.choices, + ) + parent = serializers.CharField( + label="commit SHA of first ancestor commit with coverage", + source="parent_commit_id", + ) + + class Meta: + model = Commit + fields = ( + "commitid", + "message", + "timestamp", + "ci_passed", + "author", + "branch", + "totals", + "state", + "parent", + ) + + +class CommitDetailSerializer(CommitSerializer): + report = ReportSerializer(source="full_report", label="coverage report") + + class Meta: + model = Commit + fields = CommitSerializer.Meta.fields + ("report",) + + +class CommitUploadsSerializer(serializers.ModelSerializer): + created_at = serializers.CharField() + updated_at = serializers.CharField() + storage_path = serializers.CharField() + flags = StringListField(source="flag_names") + provider = serializers.CharField() + build_code = serializers.CharField() + name = serializers.CharField() + job_code = serializers.CharField() + build_url = serializers.CharField() + state = serializers.CharField() + env = serializers.JSONField() + upload_type = serializers.CharField() + upload_extras = serializers.JSONField() + totals = UploadTotalsSerializer(source="uploadleveltotals") + + class Meta: + model = ReportSession + fields = ( + "created_at", + "updated_at", + "storage_path", + "flags", + "provider", + "build_code", + "name", + "job_code", + "build_url", + "state", + "env", + "upload_type", + "upload_extras", + "totals", + ) diff --git a/apps/codecov-api/api/public/v2/commit/views.py b/apps/codecov-api/api/public/v2/commit/views.py new file mode 100644 index 0000000000..978eb83269 --- /dev/null +++ b/apps/codecov-api/api/public/v2/commit/views.py @@ -0,0 +1,85 @@ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema +from rest_framework import mixins, viewsets + +from api.public.v2.schema import repo_parameters +from api.shared.commit.mixins import CommitsViewSetMixin +from api.shared.mixins import RepoPropertyMixin +from api.shared.permissions import RepositoryArtifactPermissions +from core.models import Commit +from reports.models import ReportSession + +from .serializers import ( + CommitDetailSerializer, + CommitSerializer, + CommitUploadsSerializer, +) + +commits_parameters = [ + OpenApiParameter( + "commitid", + OpenApiTypes.STR, + OpenApiParameter.PATH, + description="commit SHA", + ), +] + + +@extend_schema(parameters=repo_parameters, tags=["Commits"]) +class CommitsViewSet( + CommitsViewSetMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, +): + queryset = Commit.objects.none() + + def get_serializer_context(self, *args, **kwargs): + context = super().get_serializer_context(*args, **kwargs) + context.update({"include_line_coverage": True}) + return context + + def get_serializer_class(self): + if self.action == "retrieve": + return CommitDetailSerializer + elif self.action == "list": + return CommitSerializer + + @extend_schema(summary="Commit list") + def list(self, request, *args, **kwargs): + """ + Returns a paginated list of commits for the specified repository + + Optionally filterable by: + * a `branch` name + """ + return super().list(request, *args, **kwargs) + + @extend_schema(summary="Commit detail", parameters=commits_parameters) + def retrieve(self, request, *args, **kwargs): + """ + Returns a single commit by commitid (SHA) + """ + return super().retrieve(request, *args, **kwargs) + + +@extend_schema(parameters=repo_parameters, tags=["Commits"]) +class CommitsUploadsViewSet( + viewsets.GenericViewSet, + RepoPropertyMixin, + mixins.ListModelMixin, +): + permission_classes = [RepositoryArtifactPermissions] + serializer_class = CommitUploadsSerializer + + def get_queryset(self): + commit = self.get_commit(self.kwargs["commitid"]) + return ReportSession.objects.filter(report__commit=commit.id).select_related( + "uploadleveltotals" + ) + + @extend_schema(summary="Commit uploads", parameters=commits_parameters) + def list(self, request, *args, **kwargs): + """ + Returns a paginated list of uploads for a single commit by commitid (SHA) + """ + return super().list(request, *args, **kwargs) diff --git a/apps/codecov-api/api/public/v2/compare/__init__.py b/apps/codecov-api/api/public/v2/compare/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/public/v2/compare/serializers.py b/apps/codecov-api/api/public/v2/compare/serializers.py new file mode 100644 index 0000000000..e85843fe66 --- /dev/null +++ b/apps/codecov-api/api/public/v2/compare/serializers.py @@ -0,0 +1,34 @@ +from typing import List + +from rest_framework import serializers + +from api.public.v2.commit.serializers import CommitSerializer +from api.shared.commit.serializers import ReportTotalsSerializer +from api.shared.compare.serializers import ( + ComparisonSerializer as BaseComparisonSerializer, +) +from api.shared.compare.serializers import FileComparisonSerializer +from services.comparison import Comparison + + +class ComparisonSerializer(BaseComparisonSerializer): + commit_uploads = CommitSerializer(many=True, source="upload_commits") + + def get_files(self, comparison: Comparison) -> List[dict]: + data = [] + if comparison.head_report is not None: + for filename in comparison.head_report.files: + file = comparison.get_file_comparison(filename, bypass_max_diff=True) + if self._should_include_file(file): + data.append(FileComparisonSerializer(file).data) + return data + + +class ComponentComparisonSerializer(serializers.Serializer): + component_id = serializers.CharField(source="component.component_id") + name = serializers.CharField(source="component.name") + + # field names here are meant to match `FlagComparisonSerializer` + base_report_totals = ReportTotalsSerializer(source="base_totals") + head_report_totals = ReportTotalsSerializer(source="head_totals") + diff_totals = ReportTotalsSerializer(source="patch_totals") diff --git a/apps/codecov-api/api/public/v2/compare/views.py b/apps/codecov-api/api/public/v2/compare/views.py new file mode 100644 index 0000000000..d2805d7b63 --- /dev/null +++ b/apps/codecov-api/api/public/v2/compare/views.py @@ -0,0 +1,169 @@ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema +from rest_framework import mixins +from rest_framework.decorators import action +from rest_framework.response import Response + +from api.public.v2.schema import repo_parameters +from api.shared.compare.mixins import CompareViewSetMixin +from api.shared.compare.serializers import ( + FileComparisonSerializer, + FlagComparisonSerializer, + ImpactedFilesComparisonSerializer, + ImpactedFileSegmentsSerializer, +) +from services.components import ComponentComparison, commit_components +from services.decorators import torngit_safe +from utils import strtobool + +from .serializers import ComparisonSerializer, ComponentComparisonSerializer + +comparison_parameters = [ + OpenApiParameter( + "pullid", + OpenApiTypes.INT, + OpenApiParameter.QUERY, + description="pull ID on which to perform the comparison (alternative to specifying `base` and `head`)", + ), + OpenApiParameter( + "base", + OpenApiTypes.STR, + OpenApiParameter.QUERY, + description="base commit SHA (`head` also required)", + ), + OpenApiParameter( + "head", + OpenApiTypes.STR, + OpenApiParameter.QUERY, + description="head commit SHA (`base` also required)", + ), +] + + +@extend_schema(parameters=repo_parameters, tags=["Comparison"]) +class CompareViewSet( + CompareViewSetMixin, + mixins.RetrieveModelMixin, +): + serializer_class = ComparisonSerializer + + def get_queryset(self): + return None + + def get_serializer_context(self): + context = super().get_serializer_context() + if "has_diff" in self.request.query_params: + context.update( + {"has_diff": strtobool(self.request.query_params["has_diff"])} + ) + return context + + @extend_schema( + summary="Comparison", + parameters=comparison_parameters, + ) + def retrieve(self, request, *args, **kwargs): + """ + Returns a comparison for either a pair of commits or a pull + """ + return super().retrieve(request, *args, **kwargs) + + @extend_schema( + summary="File comparison", + parameters=comparison_parameters + + [ + OpenApiParameter( + "file_path", + OpenApiTypes.STR, + OpenApiParameter.PATH, + description="file path", + ), + ], + responses={200: FileComparisonSerializer}, + ) + @action( + detail=False, + methods=["get"], + url_path="file/(?P.+)", + url_name="file", + ) + def file(self, request, *args, **kwargs): + """ + Returns a comparison for a specific file path + """ + return super().file(request, *args, **kwargs) + + @extend_schema( + summary="Flag comparison", + parameters=comparison_parameters, + responses={200: FlagComparisonSerializer}, + ) + @action(detail=False, methods=["get"]) + def flags(self, request, *args, **kwargs): + """ + Returns flag comparisons + """ + return super().flags(request, *args, **kwargs) + + @extend_schema( + summary="Component comparison", + parameters=comparison_parameters, + responses={200: ComponentComparisonSerializer}, + ) + @action(detail=False, methods=["get"]) + @torngit_safe + def components(self, request, *args, **kwargs): + """ + Returns component comparisons + """ + comparison = self.get_object() + components = commit_components(comparison.head_commit, self.owner) + component_comparisons = [ + ComponentComparison(comparison, component) for component in components + ] + + serializer = ComponentComparisonSerializer(component_comparisons, many=True) + return Response(serializer.data) + + @extend_schema( + summary="Impacted files comparison", + parameters=comparison_parameters, + responses={200: ImpactedFilesComparisonSerializer}, + ) + @action(detail=False, methods=["get"]) + def impacted_files(self, request, *args, **kwargs): + """ + Returns a comparison for either a pair of commits or a pull + Will only return pre-computed impacted files comparisons if available + If unavailable `files` will be empty, however once the computation is ready + the files will appear on subsequent calls + `state: "processed"` means `files` are finished computing and returned + `state: "pending"` means `files` are still computing, poll again later + """ + return super().impacted_files(request, *args, **kwargs) + + @extend_schema( + summary="Segmented file comparison", + parameters=comparison_parameters + + [ + OpenApiParameter( + "file_path", + OpenApiTypes.STR, + OpenApiParameter.PATH, + description="file path", + ), + ], + responses={200: ImpactedFileSegmentsSerializer}, + ) + @action( + detail=False, + methods=["get"], + url_path="segments/(?P.+)", + url_name="segments", + ) + def segments(self, request, *args, **kwargs): + """ + Returns a comparison for a specific file path only showing the segments + of the file that are impacted instead of all lines in file + """ + return super().segments(request, *args, **kwargs) diff --git a/apps/codecov-api/api/public/v2/component/__init__.py b/apps/codecov-api/api/public/v2/component/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/public/v2/component/serializers.py b/apps/codecov-api/api/public/v2/component/serializers.py new file mode 100644 index 0000000000..ce182cee5a --- /dev/null +++ b/apps/codecov-api/api/public/v2/component/serializers.py @@ -0,0 +1,7 @@ +from rest_framework import serializers + + +class ComponentSerializer(serializers.Serializer): + component_id = serializers.CharField(label="component id") + name = serializers.CharField(label="component name") + coverage = serializers.FloatField(label="component coverage") diff --git a/apps/codecov-api/api/public/v2/component/views.py b/apps/codecov-api/api/public/v2/component/views.py new file mode 100644 index 0000000000..4e50de67da --- /dev/null +++ b/apps/codecov-api/api/public/v2/component/views.py @@ -0,0 +1,64 @@ +from typing import Any + +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema +from rest_framework import viewsets +from rest_framework.request import Request +from rest_framework.response import Response + +from api.public.v2.component.serializers import ComponentSerializer +from api.public.v2.schema import repo_parameters +from api.shared.mixins import RepoPropertyMixin +from api.shared.permissions import RepositoryArtifactPermissions +from services.components import commit_components, component_filtered_report +from utils import round_decimals_down + + +@extend_schema( + parameters=repo_parameters + + [ + OpenApiParameter( + "sha", + OpenApiTypes.STR, + OpenApiParameter.QUERY, + description="commit SHA for which to return components", + ), + OpenApiParameter( + "branch", + OpenApiTypes.STR, + OpenApiParameter.QUERY, + description="branch name for which to return components (of head commit)", + ), + ], + tags=["Components"], +) +class ComponentViewSet(viewsets.ViewSet, RepoPropertyMixin): + serializer_class = ComponentSerializer + permission_classes = [RepositoryArtifactPermissions] + + @extend_schema(summary="Component list") + def list(self, request: Request, *args: Any, **kwargs: Any) -> Response: + """ + Returns a list of components for the specified repository + """ + commit = self.get_commit() + report = commit.full_report + components = commit_components(commit, self.owner) + components_with_coverage = [] + for component in components: + component_report = component_filtered_report(report, [component]) + coverage = None + if component_report.totals.coverage is not None: + coverage = round_decimals_down( + float(component_report.totals.coverage), 2 + ) + components_with_coverage.append( + { + "component_id": component.component_id, + "name": component.name, + "coverage": coverage, + } + ) + + serializer = ComponentSerializer(components_with_coverage, many=True) + return Response(serializer.data) diff --git a/apps/codecov-api/api/public/v2/coverage/__init__.py b/apps/codecov-api/api/public/v2/coverage/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/public/v2/coverage/filters.py b/apps/codecov-api/api/public/v2/coverage/filters.py new file mode 100644 index 0000000000..47a29e4f62 --- /dev/null +++ b/apps/codecov-api/api/public/v2/coverage/filters.py @@ -0,0 +1,35 @@ +import django_filters + +INTERVAL_CHOICES = ( + ("1d", "1 day"), + ("7d", "7 day"), + ("30d", "30 day"), +) + + +class MeasurementFilters(django_filters.FilterSet): + interval = django_filters.ChoiceFilter( + choices=INTERVAL_CHOICES, method="filter_interval", required=True + ) + start_date = django_filters.DateTimeFilter( + label="start datetime (inclusive)", method="filter_start_date" + ) + end_date = django_filters.DateTimeFilter( + label="end datetime (inclusive)", method="filter_end_date" + ) + branch = django_filters.CharFilter(label="branch name", method="filter_branch") + + # the filtering for these methods happens in the view since they + # all need to be passed in to some of the timeseries helper functions + + def filter_interval(self, queryset, name, value): + return queryset + + def filter_start_date(self, queryset, name, value): + return queryset + + def filter_end_date(self, queryset, name, value): + return queryset + + def filter_branch(self, queryset, name, value): + return queryset diff --git a/apps/codecov-api/api/public/v2/coverage/serializers.py b/apps/codecov-api/api/public/v2/coverage/serializers.py new file mode 100644 index 0000000000..ad9453c156 --- /dev/null +++ b/apps/codecov-api/api/public/v2/coverage/serializers.py @@ -0,0 +1,10 @@ +from rest_framework import serializers + + +class MeasurementSerializer(serializers.Serializer): + timestamp = serializers.DateTimeField( + source="timestamp_bin", label="timestamp at the start of the interval" + ) + min = serializers.FloatField(label="minimum value in the interval") + max = serializers.FloatField(label="maximum value in the interval") + avg = serializers.FloatField(label="average value in the interval") diff --git a/apps/codecov-api/api/public/v2/coverage/views.py b/apps/codecov-api/api/public/v2/coverage/views.py new file mode 100644 index 0000000000..0e60b8d536 --- /dev/null +++ b/apps/codecov-api/api/public/v2/coverage/views.py @@ -0,0 +1,114 @@ +from datetime import datetime + +from drf_spectacular.utils import extend_schema +from rest_framework import mixins, viewsets +from rest_framework.exceptions import APIException + +from api.public.v2.schema import repo_parameters +from api.shared.mixins import RepoPropertyMixin +from api.shared.permissions import RepositoryArtifactPermissions +from reports.models import RepositoryFlag +from timeseries.helpers import ( + aggregate_measurements, + repository_coverage_measurements_with_fallback, +) +from timeseries.models import ( + Interval, + MeasurementName, + MeasurementSummary, + MeasurementSummary1Day, +) + +from .filters import MeasurementFilters +from .serializers import MeasurementSerializer + + +class InvalidInterval(APIException): + status_code = 422 + default_detail = "You must specify an interval (1d/7d/30d)" + default_code = "invalid_interval" + + +intervals = { + "1d": Interval.INTERVAL_1_DAY, + "7d": Interval.INTERVAL_7_DAY, + "30d": Interval.INTERVAL_30_DAY, +} + + +@extend_schema(parameters=repo_parameters, tags=["Coverage"]) +class CoverageViewSet( + viewsets.GenericViewSet, + mixins.ListModelMixin, + RepoPropertyMixin, +): + permission_classes = [RepositoryArtifactPermissions] + serializer_class = MeasurementSerializer + filterset_class = MeasurementFilters + + # this is here so that drf-spectacular can introspect the model filters + queryset = MeasurementSummary1Day.objects.none() + + def get_queryset(self): + return repository_coverage_measurements_with_fallback( + self.repo, + self.get_measurement_interval(), + start_date=self.request.query_params.get( + "start_date", datetime(2000, 1, 1) + ), + end_date=self.request.query_params.get("end_date", datetime.now()), + branch=self.request.query_params.get("branch"), + ) + + def get_measurement_interval(self) -> Interval: + interval_name = self.request.query_params.get("interval") + if interval_name not in intervals: + raise InvalidInterval() + + return intervals[interval_name] + + @extend_schema(summary="Coverage trend") + def list(self, request, *args, **kwargs): + """ + Returns a paginated list of timeseries measurements aggregated by the specified + `interval`. If there are no measurements on `start_date` then the response will include + 1 measurement older than `start_date` so that the coverage value can be carried forward + if necessary. + + Optionally filterable by: + * `branch` + * `start_date` + * `end_date` + """ + return super().list(request, *args, **kwargs) + + +@extend_schema(parameters=repo_parameters, tags=["Flags"]) +class FlagCoverageViewSet(CoverageViewSet): + def get_queryset(self): + queryset = MeasurementSummary.agg_by(self.get_measurement_interval()) + + flag = RepositoryFlag.objects.filter( + repository_id=self.repo.pk, + flag_name=self.kwargs["flag_name"], + ).first() + if not flag: + return queryset.none() + + queryset = queryset.filter( + name=MeasurementName.FLAG_COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(flag.pk), + ) + + start_date = self.request.query_params.get("start_date") + if start_date is not None: + queryset = queryset.filter(timestamp_bin__gte=start_date) + end_date = self.request.query_params.get("end_date") + if end_date is not None: + queryset = queryset.filter(timestamp_bin__lte=end_date) + + return aggregate_measurements( + queryset, ["timestamp_bin", "owner_id", "repo_id", "measurable_id"] + ) diff --git a/apps/codecov-api/api/public/v2/flag/__init__.py b/apps/codecov-api/api/public/v2/flag/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/public/v2/flag/serializers.py b/apps/codecov-api/api/public/v2/flag/serializers.py new file mode 100644 index 0000000000..0c2b94c4d0 --- /dev/null +++ b/apps/codecov-api/api/public/v2/flag/serializers.py @@ -0,0 +1,6 @@ +from rest_framework import serializers + + +class FlagSerializer(serializers.Serializer): + flag_name = serializers.CharField(label="flag name") + coverage = serializers.FloatField(label="flag coverage") diff --git a/apps/codecov-api/api/public/v2/flag/views.py b/apps/codecov-api/api/public/v2/flag/views.py new file mode 100644 index 0000000000..a4832248a2 --- /dev/null +++ b/apps/codecov-api/api/public/v2/flag/views.py @@ -0,0 +1,45 @@ +from typing import Any + +from django.db.models import QuerySet +from drf_spectacular.utils import extend_schema +from rest_framework import mixins, viewsets +from rest_framework.exceptions import NotFound +from rest_framework.request import Request +from rest_framework.response import Response + +from api.public.v2.flag.serializers import FlagSerializer +from api.public.v2.schema import repo_parameters +from api.shared.mixins import RepoPropertyMixin +from api.shared.permissions import RepositoryArtifactPermissions +from reports.models import RepositoryFlag + + +@extend_schema(parameters=repo_parameters, tags=["Flags"]) +class FlagViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, RepoPropertyMixin): + serializer_class = FlagSerializer + permission_classes = [RepositoryArtifactPermissions] + lookup_field = "flag_name" + queryset = RepositoryFlag.objects.none() + + def get_queryset(self) -> QuerySet: + results = [ + {"flag_name": f.flag_name, "coverage": None} for f in self.repo.flags.all() + ] + try: + report = self.get_commit().full_report + if not report: + return results + except NotFound: + return results + + for i, val in enumerate(results): + flag_report = report.filter(flags=[val["flag_name"]]) + results[i]["coverage"] = flag_report.totals.coverage or 0 + return results + + @extend_schema(summary="Flag list") + def list(self, request: Request, *args: Any, **kwargs: Any) -> Response: + """ + Returns a paginated list of flags for the specified repository + """ + return super().list(request, *args, **kwargs) diff --git a/apps/codecov-api/api/public/v2/owner/__init__.py b/apps/codecov-api/api/public/v2/owner/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/public/v2/owner/serializers.py b/apps/codecov-api/api/public/v2/owner/serializers.py new file mode 100644 index 0000000000..754d4b7c90 --- /dev/null +++ b/apps/codecov-api/api/public/v2/owner/serializers.py @@ -0,0 +1,41 @@ +from rest_framework import serializers + +from codecov_auth.models import Owner + + +class OwnerSerializer(serializers.ModelSerializer): + class Meta: + model = Owner + fields = ( + "service", + "username", + "name", + ) + read_only_fields = fields + + +class UserSerializer(OwnerSerializer): + activated = serializers.BooleanField() + is_admin = serializers.BooleanField() + + class Meta: + model = Owner + fields = OwnerSerializer.Meta.fields + ("activated", "is_admin", "email") + + +class UserSessionSerializer(serializers.ModelSerializer): + has_active_session = serializers.BooleanField() + expiry_date = serializers.DateTimeField() + + class Meta: + model = Owner + fields = ("username", "name", "has_active_session", "expiry_date") + read_only_fields = fields + + +class UserUpdateActivationSerializer(serializers.ModelSerializer): + activated = serializers.BooleanField() + + class Meta: + model = Owner + fields = ("activated",) diff --git a/apps/codecov-api/api/public/v2/owner/views.py b/apps/codecov-api/api/public/v2/owner/views.py new file mode 100644 index 0000000000..8677f6f221 --- /dev/null +++ b/apps/codecov-api/api/public/v2/owner/views.py @@ -0,0 +1,139 @@ +from typing import Any + +from django.db.models import Q, QuerySet +from drf_spectacular.utils import extend_schema +from rest_framework import mixins, viewsets +from rest_framework.exceptions import APIException, NotFound +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.response import Response + +from api.public.v2.schema import ( + owner_parameters, + service_parameter, +) +from api.shared.owner.mixins import ( + OwnerViewSetMixin, + UserSessionViewSetMixin, + UserViewSetMixin, +) +from codecov_auth.models import Owner, Service + +from .serializers import ( + OwnerSerializer, + UserSerializer, + UserSessionSerializer, + UserUpdateActivationSerializer, +) + + +class NotEnoughSeatsLeft(APIException): + status_code = 400 + default_detail = "Cannot activate user -- not enough seats left." + default_code = "no_seats_left" + + +@extend_schema(parameters=owner_parameters, tags=["Users"]) +class OwnerViewSet( + OwnerViewSetMixin, viewsets.GenericViewSet, mixins.RetrieveModelMixin +): + serializer_class = OwnerSerializer + queryset = Owner.objects.none() + + @extend_schema(summary="Owner detail") + def retrieve(self, request: Request, *args: Any, **kwargs: Any) -> Owner: + """ + Returns a single owner by name + """ + return super().retrieve(request, *args, **kwargs) + + +@extend_schema(parameters=owner_parameters, tags=["Users"]) +class UserViewSet(UserViewSetMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin): + serializer_class = UserSerializer + queryset = Owner.objects.none() + + @extend_schema(summary="User list") + def list(self, request: Request, *args: Any, **kwargs: Any) -> Response: + """ + Returns a paginated list of users for the specified owner (org) + """ + return super().list(request, *args, **kwargs) + + @extend_schema(summary="User detail") + def retrieve(self, request: Request, *args: Any, **kwargs: Any) -> Owner: + """ + Returns a user for the specified owner_username or ownerid + """ + return super().retrieve(request, *args, **kwargs) + + @extend_schema(summary="Update a user", request=UserUpdateActivationSerializer) + def partial_update(self, request: Request, *args: Any, **kwargs: Any) -> Response: + """ + Updates a user for the specified owner_username or ownerid + + Allowed fields + - activated: boolean value to activate or deactivate the user + """ + instance = self.get_object() + serializer = UserUpdateActivationSerializer( + instance, + data=request.data, + ) + serializer.is_valid(raise_exception=True) + + if serializer.validated_data["activated"]: + if self.owner.can_activate_user(instance): + self.owner.activate_user(instance) + else: + raise NotEnoughSeatsLeft() + else: + self.owner.deactivate_user(instance) + + return super().retrieve(request, *args, **kwargs) + + +@extend_schema(parameters=owner_parameters, tags=["Users"]) +class UserSessionViewSet(UserSessionViewSetMixin, mixins.ListModelMixin): + serializer_class = UserSessionSerializer + queryset = Owner.objects.none() + + @extend_schema(summary="User session list") + def list(self, request: Request, *args: Any, **kwargs: Any) -> Response: + """ + Returns a paginated list of users' login session for the specified owner (org) + + Note: Requires the caller to be an admin of the requested organization + """ + return super().list(request, *args, **kwargs) + + +@extend_schema( + parameters=[ + service_parameter, + ], + tags=["Users"], +) +class OwnersViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): + serializer_class = OwnerSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self) -> QuerySet: + service = self.kwargs.get("service") + try: + Service(service) + except ValueError: + raise NotFound(f"Service not found: {service}") + + current_owner = self.request.current_owner + return Owner.objects.filter( + Q(service=service, ownerid__in=current_owner.organizations) + | Q(service=service, username=current_owner.username) + ) + + @extend_schema(summary="Service owners") + def list(self, request: Request, *args: Any, **kwargs: Any) -> Response: + """ + Returns all owners to which the currently authenticated user has access + """ + return super().list(request, *args, **kwargs) diff --git a/apps/codecov-api/api/public/v2/pull/__init__.py b/apps/codecov-api/api/public/v2/pull/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/public/v2/pull/serializers.py b/apps/codecov-api/api/public/v2/pull/serializers.py new file mode 100644 index 0000000000..9e4ab98be6 --- /dev/null +++ b/apps/codecov-api/api/public/v2/pull/serializers.py @@ -0,0 +1,66 @@ +from typing import Dict, Optional + +from rest_framework import serializers + +from api.public.v2.owner.serializers import OwnerSerializer +from api.shared.commit.serializers import ( + CommitTotalsSerializer, + PatchCoverageSerializer, +) +from core.models import Pull, PullStates +from services.comparison import CommitComparisonService, ComparisonReport + + +class PullSerializer(serializers.ModelSerializer): + pullid = serializers.IntegerField(label="pull ID number") + title = serializers.CharField(label="title of the pull") + base_totals = CommitTotalsSerializer(label="coverage totals of base commit") + head_totals = CommitTotalsSerializer(label="coverage totals of head commit") + updatestamp = serializers.DateTimeField(label="last updated timestamp") + state = serializers.ChoiceField( + label="state of the pull", choices=PullStates.choices + ) + ci_passed = serializers.BooleanField( + label="indicates whether the CI process passed for the head commit of this pull" + ) + author = OwnerSerializer(label="pull author") + patch = serializers.SerializerMethodField() + + class Meta: + model = Pull + read_only_fields = ( + "pullid", + "title", + "base_totals", + "head_totals", + "updatestamp", + "state", + "ci_passed", + "author", + "patch", + ) + fields = read_only_fields + + def get_patch(self, obj: Pull) -> Optional[Dict[str, float]]: + commit_comparison = CommitComparisonService.get_commit_comparison_for_pull(obj) + if not commit_comparison or not commit_comparison.is_processed: + return None + cr = ComparisonReport(commit_comparison) + hits = misses = partials = 0 + for f in cr.impacted_files: + pc = f.patch_coverage + if pc: + hits += pc.hits + misses += pc.misses + partials += pc.partials + total_branches = hits + misses + partials + coverage = 0.0 + if total_branches != 0: + coverage = round(100 * hits / total_branches, 2) + data = dict( + hits=hits, + misses=misses, + partials=partials, + coverage=coverage, + ) + return PatchCoverageSerializer(data).data diff --git a/apps/codecov-api/api/public/v2/pull/views.py b/apps/codecov-api/api/public/v2/pull/views.py new file mode 100644 index 0000000000..ddb092a8d4 --- /dev/null +++ b/apps/codecov-api/api/public/v2/pull/views.py @@ -0,0 +1,96 @@ +import django_filters +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema +from rest_framework import mixins +from rest_framework.authentication import BasicAuthentication, SessionAuthentication + +from api.public.v2.schema import repo_parameters +from api.shared.pagination import PaginationMixin +from api.shared.permissions import RepositoryArtifactPermissions, SuperTokenPermissions +from api.shared.pull.mixins import PullViewSetMixin +from codecov_auth.authentication import ( + SuperTokenAuthentication, + UserTokenAuthentication, +) +from core.models import Pull, PullStates + +from .serializers import PullSerializer + + +class PullFilters(django_filters.FilterSet): + state = django_filters.ChoiceFilter(choices=PullStates.choices) + start_date = django_filters.DateTimeFilter(method="filter_start_date") + + def filter_start_date(self, queryset, name, value): + return queryset.filter(updatestamp__gte=value) + + +@extend_schema(parameters=repo_parameters, tags=["Pulls"]) +class PullViewSet( + PaginationMixin, + PullViewSetMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, +): + authentication_classes = [ + SuperTokenAuthentication, + UserTokenAuthentication, + BasicAuthentication, + SessionAuthentication, + ] + + permission_classes = [SuperTokenPermissions | RepositoryArtifactPermissions] + + serializer_class = PullSerializer + queryset = Pull.objects.none() + filterset_class = PullFilters + + def get_queryset(self): + return super().get_queryset().select_related("author") + + @extend_schema( + summary="Pull list", + parameters=[ + OpenApiParameter( + "state", + OpenApiTypes.STR, + OpenApiParameter.QUERY, + description="the state of the pull (open/merged/closed)", + ), + OpenApiParameter( + "start_date", + OpenApiTypes.DATETIME, + OpenApiParameter.QUERY, + description="only return pulls with updatestamp on or after this date", + ), + ], + ) + def list(self, request, *args, **kwargs): + """ + Returns a paginated list of pulls for the specified repository + + Optionally filterable by: + * `state` + * `start_date` + + Orderable by: + * `pullid` + """ + return super().list(request, *args, **kwargs) + + @extend_schema( + summary="Pull detail", + parameters=[ + OpenApiParameter( + "pullid", + OpenApiTypes.STR, + OpenApiParameter.PATH, + description="pull ID", + ), + ], + ) + def retrieve(self, request, *args, **kwargs): + """ + Returns a single pull by ID + """ + return super().retrieve(request, *args, **kwargs) diff --git a/apps/codecov-api/api/public/v2/repo/__init__.py b/apps/codecov-api/api/public/v2/repo/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/public/v2/repo/permissions.py b/apps/codecov-api/api/public/v2/repo/permissions.py new file mode 100644 index 0000000000..feb1334a33 --- /dev/null +++ b/apps/codecov-api/api/public/v2/repo/permissions.py @@ -0,0 +1,8 @@ +from rest_framework.permissions import BasePermission + +from codecov_auth.helpers import current_user_part_of_org + + +class RepositoryOrgMemberPermissions(BasePermission): + def has_permission(self, request, view): + return current_user_part_of_org(request.current_owner, view.repo.author) diff --git a/apps/codecov-api/api/public/v2/repo/serializers.py b/apps/codecov-api/api/public/v2/repo/serializers.py new file mode 100644 index 0000000000..a99cd57c45 --- /dev/null +++ b/apps/codecov-api/api/public/v2/repo/serializers.py @@ -0,0 +1,55 @@ +from rest_framework import serializers + +from api.public.v2.owner.serializers import OwnerSerializer +from api.shared.commit.serializers import CommitTotalsSerializer +from core.models import Repository + + +class RepoSerializer(serializers.ModelSerializer): + name = serializers.CharField(label="repository name") + private = serializers.BooleanField(label="indicates private vs. public repository") + updatestamp = serializers.DateTimeField(label="last updated timestamp") + language = serializers.CharField(label="primary programming language used") + branch = serializers.CharField(label="default branch name") + active = serializers.BooleanField( + label="indicates whether the repository has received a coverage upload" + ) + activated = serializers.BooleanField( + label="indicates whether the repository has been manually deactivated" + ) + author = OwnerSerializer(label="repository owner") + totals = CommitTotalsSerializer( + label="recent commit totals on the default branch", + source="recent_commit_totals", + ) + + class Meta: + model = Repository + read_only_fields = ( + "name", + "private", + "updatestamp", + "author", + "language", + "branch", + "active", + "activated", + "totals", + ) + fields = read_only_fields + + +class RepoConfigSerializer(serializers.ModelSerializer): + upload_token = serializers.CharField( + label="token used for uploading coverage reports for this repo" + ) + + graph_token = serializers.CharField( + source="image_token", + label="token used for repository graphs", + ) + + class Meta: + model = Repository + read_only_fields = ("upload_token", "graph_token") + fields = read_only_fields diff --git a/apps/codecov-api/api/public/v2/repo/views.py b/apps/codecov-api/api/public/v2/repo/views.py new file mode 100644 index 0000000000..c0a35157d5 --- /dev/null +++ b/apps/codecov-api/api/public/v2/repo/views.py @@ -0,0 +1,111 @@ +from django_filters import rest_framework as django_filters +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema +from rest_framework import filters, mixins, views, viewsets +from rest_framework.response import Response + +from api.public.v2.schema import owner_username_parameter, service_parameter +from api.shared.mixins import RepoPropertyMixin +from api.shared.permissions import RepositoryArtifactPermissions +from api.shared.repo.filter import RepositoryFilters +from api.shared.repo.mixins import RepositoryViewSetMixin +from core.models import Repository + +from .permissions import RepositoryOrgMemberPermissions +from .serializers import RepoConfigSerializer, RepoSerializer + + +@extend_schema( + parameters=[ + service_parameter, + owner_username_parameter, + ], + tags=["Repos"], +) +class RepositoryViewSet( + RepositoryViewSetMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet, +): + filter_backends = ( + django_filters.DjangoFilterBackend, + filters.SearchFilter, + ) + filterset_class = RepositoryFilters + search_fields = ("name",) + ordering_fields = ( + "updatestamp", + "name", + ) + serializer_class = RepoSerializer + queryset = Repository.objects.none() + + def get_queryset(self): + return super().get_queryset().with_recent_coverage() + + @extend_schema( + summary="Repository list", + parameters=[ + OpenApiParameter( + name="names", + type={"type": "array", "items": "string"}, + explode=True, + description="list of repository names", + ) + ], + ) + def list(self, request, *args, **kwargs): + """ + Returns a paginated list of repositories for the specified provider service and owner username + + Optionally filterable by: + * a list of repository `name`s + * a `search` term which matches against the name + * whether the repository is `active` or not + """ + return super().list(request, *args, **kwargs) + + @extend_schema( + summary="Repository detail", + parameters=[ + OpenApiParameter( + "repo_name", + OpenApiTypes.STR, + OpenApiParameter.PATH, + description="repository name", + ), + ], + ) + def retrieve(self, request, *args, **kwargs): + """ + Returns a single repository by name + """ + return super().retrieve(request, *args, **kwargs) + + +@extend_schema( + parameters=[ + service_parameter, + owner_username_parameter, + ], + tags=["Repos"], +) +class RepositoryConfigView(views.APIView, RepoPropertyMixin): + permission_classes = [RepositoryArtifactPermissions, RepositoryOrgMemberPermissions] + + @extend_schema( + summary="Repository config", + parameters=[ + OpenApiParameter( + "repo_name", + OpenApiTypes.STR, + OpenApiParameter.PATH, + description="repository name", + ), + ], + responses={200: RepoConfigSerializer}, + ) + def get(self, request, *args, **kwargs): + serializer = RepoConfigSerializer(self.repo) + return Response(serializer.data) diff --git a/apps/codecov-api/api/public/v2/report/__init__.py b/apps/codecov-api/api/public/v2/report/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/public/v2/report/serializers.py b/apps/codecov-api/api/public/v2/report/serializers.py new file mode 100644 index 0000000000..1ef3df74b8 --- /dev/null +++ b/apps/codecov-api/api/public/v2/report/serializers.py @@ -0,0 +1,24 @@ +from rest_framework import serializers + +from api.shared.commit.serializers import ReportFileSerializer, ReportSerializer + + +class CoverageReportSerializer(ReportSerializer): + commit_file_url = serializers.CharField( + label="Codecov url to see file coverage on commit. Can be unreliable with partial path names." + ) + + +class FileReportSerializer(ReportFileSerializer): + commit_sha = serializers.SerializerMethodField( + label="commit SHA of the commit for which coverage info was found" + ) + commit_file_url = serializers.SerializerMethodField( + label="Codecov URL to see file coverage on commit." + ) + + def get_commit_sha(self, obj): + return self.context["commit_sha"] + + def get_commit_file_url(self, obj): + return self.context["commit_file_url"] diff --git a/apps/codecov-api/api/public/v2/report/views.py b/apps/codecov-api/api/public/v2/report/views.py new file mode 100644 index 0000000000..43d63a12bd --- /dev/null +++ b/apps/codecov-api/api/public/v2/report/views.py @@ -0,0 +1,377 @@ +from typing import Optional + +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema +from rest_framework import mixins, viewsets +from rest_framework.authentication import BasicAuthentication, SessionAuthentication +from rest_framework.decorators import action +from rest_framework.exceptions import NotFound, ValidationError +from rest_framework.response import Response +from shared.reports.resources import Report +from shared.utils.match import match + +from api.public.v2.report.serializers import ( + CoverageReportSerializer, + FileReportSerializer, +) +from api.public.v2.schema import repo_parameters +from api.shared.mixins import RepoPropertyMixin +from api.shared.permissions import RepositoryArtifactPermissions, SuperTokenPermissions +from api.shared.report.serializers import TreeSerializer +from codecov_auth.authentication import ( + SuperTokenAuthentication, + UserTokenAuthentication, +) +from core.models import Commit +from services.components import commit_components +from services.path import ReportPaths, dashboard_commit_file_url + + +class ReportMixin: + def _commit_file_url(self, commit: Commit, path: str): + service, owner, repo = ( + self.kwargs["service"], + self.kwargs["owner_username"], + self.kwargs["repo_name"], + ) + commit_file_url = dashboard_commit_file_url( + path=path, + service=service, + owner=owner, + repo=repo, + commit=commit, + ) + return commit_file_url + + +@extend_schema( + parameters=repo_parameters + + [ + OpenApiParameter( + "sha", + OpenApiTypes.STR, + OpenApiParameter.QUERY, + description="commit SHA for which to return report", + ), + OpenApiParameter( + "branch", + OpenApiTypes.STR, + OpenApiParameter.QUERY, + description="branch name for which to return report (of head commit)", + ), + OpenApiParameter( + "path", + OpenApiTypes.STR, + OpenApiParameter.QUERY, + description="filter report to only include file paths starting with this value", + ), + OpenApiParameter( + "flag", + OpenApiTypes.STR, + OpenApiParameter.QUERY, + description="filter report to only include info pertaining to given flag name", + ), + OpenApiParameter( + "component_id", + OpenApiTypes.STR, + OpenApiParameter.QUERY, + description="filter report to only include info pertaining to given component id", + ), + ], + tags=["Coverage"], +) +class BaseReportViewSet( + viewsets.GenericViewSet, mixins.RetrieveModelMixin, RepoPropertyMixin, ReportMixin +): + serializer_class = CoverageReportSerializer + permission_classes = [RepositoryArtifactPermissions] + + def filter_report( + self, + commit: Commit, + report: Report, + path: Optional[str] = None, + flag: Optional[str] = None, + component_id: Optional[str] = None, + ) -> Report: + if component_id: + component = next( + ( + component + for component in commit_components(commit, self.owner) + if component.component_id == component_id + ), + None, + ) + if component is None: + raise NotFound( + f"The component {component_id} does not exist in commit {commit.commitid}" + ) + + if path and not match(component.paths, path): + # empty report since the path is not part of the component + return Report() + + component_flags = component.get_matching_flags(report.get_flag_names()) + if flag and len(component.flag_regexes) > 0 and flag not in component_flags: + # empty report since the flag is not part of the component + return Report() + + if path and flag: + report = report.filter(flags=[flag], paths=[f"{path}.*"]) + elif path: + report = report.filter(paths=[f"{path}.*"]) + elif flag: + report = report.filter(flags=[flag]) + elif component_id: + report = report.filter(flags=component_flags, paths=component.paths) + + if path and len(report.files) == 0: + raise NotFound(f"No files or directories found matching path: {path}") + + return report + + def get_object(self): + commit = self.get_commit() + report = commit.full_report + + if report is None: + raise NotFound(f"No coverage report found for commit {commit.commitid}") + + path = self.request.query_params.get("path", None) + report = self.filter_report( + commit, + report, + path=path, + flag=self.request.query_params.get("flag", None), + component_id=self.request.query_params.get("component_id", None), + ) + + # Add commit url to report object + report.commit_file_url = self._commit_file_url(commit, path) + + return report + + def retrieve(self, request, *args, **kwargs): + report = self.get_object() + serializer = self.get_serializer(report) + return Response(serializer.data) + + +class TotalsViewSet(BaseReportViewSet): + def get_serializer_context(self, *args, **kwargs): + context = super().get_serializer_context(*args, **kwargs) + context.update({"include_line_coverage": False}) + return context + + @extend_schema(summary="Commit coverage totals") + def retrieve(self, request, *args, **kwargs): + """ + Returns the coverage totals for a given commit and the + coverage totals broken down by file. + + By default that commit is the head of the default branch but can also be specified explictily by: + * `sha` - return totals for the commit with the given SHA + * `branch` - return totals for the head commit of the branch with the given name + + The totals can be optionally filtered by specifying: + * `path` - only show totals for pathnames that start with this value + * `flag` - only show totals that applies to the specified flag name + * `component_id` - only show totals that applies to the specified component + """ + return super().retrieve(request, *args, **kwargs) + + +class ReportViewSet(BaseReportViewSet): + authentication_classes = [ + SuperTokenAuthentication, + UserTokenAuthentication, + BasicAuthentication, + SessionAuthentication, + ] + permission_classes = [SuperTokenPermissions | RepositoryArtifactPermissions] + + def get_queryset(self): + return None + + def get_serializer_context(self, *args, **kwargs): + context = super().get_serializer_context(*args, **kwargs) + context.update({"include_line_coverage": True}) + return context + + @extend_schema(summary="Commit coverage report") + def retrieve(self, request, *args, **kwargs): + """ + Similar to the coverage totals endpoint but also returns line-by-line + coverage info (hit=0/miss=1/partial=2). + + By default that commit is the head of the default branch but can also be specified explictily by: + * `sha` - return report for the commit with the given SHA + * `branch` - return report for the head commit of the branch with the given name + + The report can be optionally filtered by specifying: + * `path` - only show report info for pathnames that start with this value + * `flag` - only show report info that applies to the specified flag name + * `component_id` - only show report info that applies to the specified component + """ + return super().retrieve(request, *args, **kwargs) + + @extend_schema( + summary="Coverage report tree", + parameters=[ + OpenApiParameter( + "depth", + OpenApiTypes.STR, + OpenApiParameter.QUERY, + description="depth of the traversal (default=1)", + ), + OpenApiParameter( + "path", + OpenApiTypes.STR, + OpenApiParameter.QUERY, + description="starting path of the traversal (default is root path)", + ), + ], + responses={200: TreeSerializer}, + ) + @action( + detail=False, + methods=["get"], + url_path="tree", + ) + def tree(self, request, *args, **kwargs): + """ + Returns a hierarchical view of the report that matches the file structure of the covered files + with coverage info rollups at each level. + + Returns only top-level data by default but the depth of the traversal can be controlled via + the `depth` parameter. + + * `depth` - how deep in the tree to traverse (default=1) + * `path` - path in the tree from which to start the traversal (default is the root) + """ + report = self.get_object() + path = request.query_params.get("path") + paths = ReportPaths(report, path=path) + serializer = TreeSerializer( + paths.single_directory(), + many=True, + context={ + "max_depth": int(request.query_params.get("depth", 1)), + }, + ) + return Response(serializer.data) + + +@extend_schema( + parameters=repo_parameters + + [ + OpenApiParameter( + "path", + OpenApiTypes.STR, + OpenApiParameter.PATH, + description="the file path for which to retrieve coverage info", + ), + OpenApiParameter( + "sha", + OpenApiTypes.STR, + OpenApiParameter.QUERY, + description="commit SHA for which to return report", + ), + OpenApiParameter( + "branch", + OpenApiTypes.STR, + OpenApiParameter.QUERY, + description="branch name for which to return report (of head commit)", + ), + ], + tags=["Coverage"], +) +class FileReportViewSet( + viewsets.GenericViewSet, mixins.RetrieveModelMixin, RepoPropertyMixin, ReportMixin +): + authentication_classes = [ + SuperTokenAuthentication, + UserTokenAuthentication, + BasicAuthentication, + SessionAuthentication, + ] + permission_classes = [SuperTokenPermissions | RepositoryArtifactPermissions] + serializer_class = FileReportSerializer + + def get_queryset(self): + return None + + def get_object(self): + self.path = self.kwargs.get("path") + + walk_back = int(self.request.query_params.get("walk_back", 0)) + if walk_back > 20: + raise ValidationError("walk_back must be <= 20") + + self.commit = self.get_commit() + report = self.commit.full_report + + oldest_sha = self.request.query_params.get("oldest_sha") + + for i in range(walk_back): + if self._is_valid_commit(self.commit) and self._is_valid_report( + report, self.path + ): + break + else: + # walk commit ancestors until we find coverage info for the given path + if not self.commit.parent_commit_id: + report = None + break + self.commit = self.repo.commits.filter( + commitid=self.commit.parent_commit_id + ).first() + if not self.commit: + report = None + break + report = self.commit.full_report + + if oldest_sha and oldest_sha == self.commit.commitid: + break + + if not self._is_valid_report(report, self.path): + raise NotFound(f"coverage info not found for path '{self.path}'") + + return report.get(self.path) + + def get_serializer_context(self, *args, **kwargs): + context = super().get_serializer_context(*args, **kwargs) + context.update( + { + "include_line_coverage": True, + "commit_sha": self.commit.commitid, + "commit_file_url": self._commit_file_url(self.commit, self.path), + } + ) + return context + + @extend_schema(summary="File coverage report") + def retrieve(self, request, *args, **kwargs): + """ + Similar to the coverage report endpoint but only returns coverage info for a single + file specified by `path`. + + By default that commit is the head of the default branch but can also be specified explictily by: + * `sha` - return report for the commit with the given SHA + * `branch` - return report for the head commit of the branch with the given name + """ + return super().retrieve(request, *args, **kwargs) + + def _is_valid_commit(self, commit: Commit) -> bool: + return commit.state == Commit.CommitStates.COMPLETE + + def _is_valid_report(self, report: Report, path: str) -> bool: + if report is None: + return False + + report_file = report.get(path) + if report_file is None: + return False + + return True diff --git a/apps/codecov-api/api/public/v2/schema.py b/apps/codecov-api/api/public/v2/schema.py new file mode 100644 index 0000000000..b4b9983b53 --- /dev/null +++ b/apps/codecov-api/api/public/v2/schema.py @@ -0,0 +1,29 @@ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter + +from codecov_auth.models import Service + +service_parameter = OpenApiParameter( + "service", + OpenApiTypes.STR, + OpenApiParameter.PATH, + description="Git hosting service provider", + enum=[name for name, desc in Service.choices], +) + +owner_username_parameter = OpenApiParameter( + "owner_username", + OpenApiTypes.STR, + OpenApiParameter.PATH, + description="username from service provider", +) + +repo_name_parameter = OpenApiParameter( + "repo_name", + OpenApiTypes.STR, + OpenApiParameter.PATH, + description="repository name", +) + +owner_parameters = [service_parameter, owner_username_parameter] +repo_parameters = owner_parameters + [repo_name_parameter] diff --git a/apps/codecov-api/api/public/v2/test_results/__init__.py b/apps/codecov-api/api/public/v2/test_results/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/public/v2/test_results/serializers.py b/apps/codecov-api/api/public/v2/test_results/serializers.py new file mode 100644 index 0000000000..161368567c --- /dev/null +++ b/apps/codecov-api/api/public/v2/test_results/serializers.py @@ -0,0 +1,40 @@ +from rest_framework import serializers + +from reports.models import TestInstance + + +class TestInstanceSerializer(serializers.ModelSerializer): + id = serializers.IntegerField(label="id") + name = serializers.CharField(source="test.name", read_only=True, label="test name") + test_id = serializers.CharField(label="test id") + failure_message = serializers.CharField(label="test name") + duration_seconds = serializers.FloatField(label="duration in seconds") + commitid = serializers.CharField(label="commit SHA") + outcome = serializers.CharField(label="outcome") + branch = serializers.CharField(label="branch name") + repoid = serializers.IntegerField(label="repo id") + failure_rate = serializers.FloatField( + source="test.failure_rate", read_only=True, label="failure rate" + ) + commits_where_fail = serializers.ListField( + source="test.commits_where_fail", + read_only=True, + label="commits where test failed", + ) + + class Meta: + model = TestInstance + read_only_fields = ( + "id", + "test_id", + "failure_message", + "duration_seconds", + "commitid", + "outcome", + "branch", + "repoid", + "failure_rate", + "name", + "commits_where_fail", + ) + fields = read_only_fields diff --git a/apps/codecov-api/api/public/v2/test_results/views.py b/apps/codecov-api/api/public/v2/test_results/views.py new file mode 100644 index 0000000000..efa5739032 --- /dev/null +++ b/apps/codecov-api/api/public/v2/test_results/views.py @@ -0,0 +1,114 @@ +import django_filters +from django_filters.rest_framework import DjangoFilterBackend +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema +from rest_framework import mixins, viewsets +from rest_framework.authentication import BasicAuthentication, SessionAuthentication + +from api.shared.mixins import RepoPropertyMixin +from api.shared.permissions import RepositoryArtifactPermissions, SuperTokenPermissions +from codecov_auth.authentication import ( + SuperTokenAuthentication, + UserTokenAuthentication, +) +from reports.models import TestInstance + +from .serializers import TestInstanceSerializer + + +class TestResultsFilters(django_filters.FilterSet): + commit_id = django_filters.CharFilter(field_name="commitid") + outcome = django_filters.CharFilter(field_name="outcome") + duration_min = django_filters.NumberFilter( + field_name="duration_seconds", lookup_expr="gte" + ) + duration_max = django_filters.NumberFilter( + field_name="duration_seconds", lookup_expr="lte" + ) + branch = django_filters.CharFilter(field_name="branch") + + class Meta: + model = TestInstance + fields = ["commit_id", "outcome", "duration_min", "duration_max", "branch"] + + +@extend_schema( + parameters=[ + OpenApiParameter( + "commit_id", + OpenApiTypes.STR, + OpenApiParameter.QUERY, + description="Commit SHA for which to return test results", + ), + OpenApiParameter( + "outcome", + OpenApiTypes.STR, + OpenApiParameter.QUERY, + description="Status of the test (failure, skip, error, pass)", + ), + OpenApiParameter( + "duration_min", + OpenApiTypes.INT, + OpenApiParameter.QUERY, + description="Minimum duration of the test in seconds", + ), + OpenApiParameter( + "duration_max", + OpenApiTypes.INT, + OpenApiParameter.QUERY, + description="Maximum duration of the test in seconds", + ), + OpenApiParameter( + "branch", + OpenApiTypes.STR, + OpenApiParameter.QUERY, + description="Branch name for which to return test results", + ), + ], + tags=["Test Results"], + summary="Retrieve test results", +) +class TestResultsView( + viewsets.GenericViewSet, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + RepoPropertyMixin, +): + authentication_classes = [ + SuperTokenAuthentication, + UserTokenAuthentication, + BasicAuthentication, + SessionAuthentication, + ] + + serializer_class = TestInstanceSerializer + permission_classes = [SuperTokenPermissions | RepositoryArtifactPermissions] + filter_backends = [DjangoFilterBackend] + filterset_class = TestResultsFilters + + def get_queryset(self): + return TestInstance.objects.filter(repoid=self.repo.repoid) + + @extend_schema(summary="Test results list") + def list(self, request, *args, **kwargs): + """ + Returns a list of test results for the specified repository and commit + """ + return super().list(request, *args, **kwargs) + + @extend_schema( + summary="Test results detail", + parameters=[ + OpenApiParameter( + "id", + OpenApiTypes.INT, + OpenApiParameter.PATH, + description="Test instance ID", + ), + ], + ) + def retrieve(self, request, *args, **kwargs): + """ + Returns a single test result by ID + """ + return super().retrieve(request, *args, **kwargs) diff --git a/apps/codecov-api/api/public/v2/tests/test_api_branch_viewset.py b/apps/codecov-api/api/public/v2/tests/test_api_branch_viewset.py new file mode 100644 index 0000000000..5291a83b10 --- /dev/null +++ b/apps/codecov-api/api/public/v2/tests/test_api_branch_viewset.py @@ -0,0 +1,89 @@ +from django.urls import reverse +from freezegun import freeze_time +from shared.django_apps.core.tests.factories import ( + BranchFactory, + OwnerFactory, + RepositoryFactory, +) + +from codecov.tests.base_test import InternalAPITest +from utils.test_utils import APIClient + +get_permissions_method = ( + "api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions" +) + + +@freeze_time("2022-01-01T00:00:00") +class BranchViewsetTests(InternalAPITest): + def setUp(self): + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org) + self.current_owner = OwnerFactory( + permission=[self.repo.repoid], organizations=[self.org.ownerid] + ) + self.branches = [ + BranchFactory(repository=self.repo, name="foo"), + BranchFactory(repository=self.repo, name="bar"), + BranchFactory(name="baz"), + ] + + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + def test_list(self): + res = self.client.get( + reverse( + "api-v2-branches-list", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + ) + assert res.status_code == 200 + assert res.json() == { + "count": 2, + "next": None, + "previous": None, + "results": [ + {"name": "foo", "updatestamp": "2022-01-01T00:00:00Z"}, + {"name": "bar", "updatestamp": "2022-01-01T00:00:00Z"}, + ], + "total_pages": 1, + } + + def test_retrieve(self): + res = self.client.get( + reverse( + "api-v2-branches-detail", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "name": self.branches[0].name, + }, + ) + ) + assert res.status_code == 200 + assert res.data["name"] == self.branches[0].name + assert res.data["head_commit"]["report"] + + def test_retrieve_period(self): + branch = BranchFactory(repository=self.repo, name="test.dot") + + res = self.client.get( + reverse( + "api-v2-branches-detail", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "name": branch.name, + }, + ) + ) + assert res.status_code == 200 + assert res.data["name"] == branch.name + assert res.data["head_commit"]["report"] diff --git a/apps/codecov-api/api/public/v2/tests/test_api_commit_viewset.py b/apps/codecov-api/api/public/v2/tests/test_api_commit_viewset.py new file mode 100644 index 0000000000..a039515a42 --- /dev/null +++ b/apps/codecov-api/api/public/v2/tests/test_api_commit_viewset.py @@ -0,0 +1,508 @@ +from unittest.mock import patch + +from django.test import TestCase +from django.urls import reverse +from shared.django_apps.core.tests.factories import ( + CommitFactory, + CommitWithReportFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.reports.types import ReportTotals + +from utils.test_utils import APIClient + + +class MockCoverage(object): + def __init__(self, cov): + self.coverage = cov + + +class MockReportFile(object): + def __init__(self, name): + self.name = name + self.lines = [ + [1, MockCoverage("1/2")], # partial => 2 + [2, MockCoverage(1)], # hit => 0 + [3, MockCoverage(0)], # miss => 1 + ] + self.totals = ReportTotals( + lines=3, + hits=1, + misses=1, + partials=1, + coverage=33.33, + ) + + +class MockReport(object): + def __init__(self): + self.files = ["foo/a.py", "bar/b.py"] + self.totals = ReportTotals( + files=2, + lines=6, + hits=2, + misses=2, + partials=2, + coverage=33.33, + ) + + def get(self, name): + return MockReportFile(name) + + +class BaseRepoCommitTestCase(TestCase): + def setUp(self) -> None: + self.org = OwnerFactory(username="codecov", service="github") + self.repo = RepositoryFactory(author=self.org, name="test-repo", active=True) + self.current_owner = OwnerFactory( + username="codecov-user", + service="github", + organizations=[self.org.ownerid], + permission=[self.repo.repoid], + ) + self.commit = CommitFactory( + author=self.org, + repository=self.repo, + totals={ + "C": 2, + "M": 0, + "N": 5, + "b": 0, + "c": "79.16667", + "d": 0, + "f": 3, + "h": 19, + "m": 5, + "n": 24, + "p": 0, + "s": 2, + "diff": 0, + }, + ) + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + +@patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions") +class RepoCommitListTestCase(BaseRepoCommitTestCase): + def test_commit_list_not_authenticated(self, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + + author = OwnerFactory() + repo = RepositoryFactory(author=author, private=False) + CommitFactory(repository=repo) + + self.client.logout() + response = self.client.get( + reverse( + "api-v2-commits-list", + kwargs={ + "service": author.service, + "owner_username": author.username, + "repo_name": repo.name, + }, + ) + ) + + # allows access to public repos + assert response.status_code == 200 + + def test_commit_list_authenticated(self, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + + response = self.client.get( + reverse( + "api-v2-commits-list", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + ) + assert response.status_code == 200 + assert response.json() == { + "count": 1, + "total_pages": 1, + "next": None, + "previous": None, + "results": [ + { + "commitid": self.commit.commitid, + "message": self.commit.message, + "timestamp": self.commit.timestamp.replace(tzinfo=None).isoformat() + + "Z", + "ci_passed": True, + "author": { + "service": "github", + "username": "codecov", + "name": self.org.name, + }, + "branch": "main", + "totals": { + "files": 3, + "lines": 24, + "hits": 19, + "misses": 5, + "partials": 0, + "coverage": 79.16, + "branches": 0, + "methods": 0, + "sessions": 2, + "complexity": 2.0, + "complexity_total": 5.0, + "complexity_ratio": 40.0, + "diff": 0, + }, + "state": "complete", + "parent": self.commit.parent_commit_id, + } + ], + } + + def test_commit_list_null_coverage(self, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + + self.commit.totals["c"] = None + self.commit.save() + + response = self.client.get( + reverse( + "api-v2-commits-list", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + ) + assert response.status_code == 200 + assert response.json() == { + "count": 1, + "total_pages": 1, + "next": None, + "previous": None, + "results": [ + { + "commitid": self.commit.commitid, + "message": self.commit.message, + "timestamp": self.commit.timestamp.replace(tzinfo=None).isoformat() + + "Z", + "ci_passed": True, + "author": { + "service": "github", + "username": "codecov", + "name": self.org.name, + }, + "branch": "main", + "totals": { + "files": 3, + "lines": 24, + "hits": 19, + "misses": 5, + "partials": 0, + "coverage": None, + "branches": 0, + "methods": 0, + "sessions": 2, + "complexity": 2.0, + "complexity_total": 5.0, + "complexity_ratio": 40.0, + "diff": 0, + }, + "state": "complete", + "parent": self.commit.parent_commit_id, + } + ], + } + + +@patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions") +class RepoCommitDetailTestCase(BaseRepoCommitTestCase): + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_commit_detail_not_authenticated( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = MockReport() + + author = OwnerFactory() + repo = RepositoryFactory(author=author, private=False) + commit = CommitFactory(author=author, repository=repo) + + self.client.logout() + response = self.client.get( + reverse( + "api-v2-commits-detail", + kwargs={ + "service": author.service, + "owner_username": author.username, + "repo_name": repo.name, + "commitid": commit.commitid, + }, + ) + ) + # allows access to public repos + assert response.status_code == 200 + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_commit_detail_authenticated( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = MockReport() + + response = self.client.get( + reverse( + "api-v2-commits-detail", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "commitid": self.commit.commitid, + }, + ) + ) + assert response.status_code == 200 + assert response.json() == { + "commitid": self.commit.commitid, + "message": self.commit.message, + "timestamp": self.commit.timestamp.replace(tzinfo=None).isoformat() + "Z", + "ci_passed": True, + "author": { + "service": "github", + "username": "codecov", + "name": self.org.name, + }, + "branch": "main", + "totals": { + "files": 3, + "lines": 24, + "hits": 19, + "misses": 5, + "partials": 0, + "coverage": 79.16, + "branches": 0, + "methods": 0, + "sessions": 2, + "complexity": 2.0, + "complexity_total": 5.0, + "complexity_ratio": 40.0, + "diff": 0, + }, + "state": "complete", + "parent": self.commit.parent_commit_id, + "report": { + "files": [ + { + "name": "foo/a.py", + "totals": { + "files": 0, + "lines": 3, + "hits": 1, + "misses": 1, + "partials": 1, + "coverage": 33.33, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "line_coverage": [ + [1, 2], + [2, 0], + [3, 1], + ], + }, + { + "name": "bar/b.py", + "totals": { + "files": 0, + "lines": 3, + "hits": 1, + "misses": 1, + "partials": 1, + "coverage": 33.33, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "line_coverage": [ + [1, 2], + [2, 0], + [3, 1], + ], + }, + ], + "totals": { + "files": 2, + "lines": 6, + "hits": 2, + "misses": 2, + "partials": 2, + "coverage": 33.33, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + }, + } + + +@patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions") +class RepoCommitUploadsTestCase(BaseRepoCommitTestCase): + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_commit_uploads_not_authenticated( + self, build_report_from_commit, get_repo_permissions + ): + build_report_from_commit.return_value = MockReport() + get_repo_permissions.return_value = (True, True) + + author = OwnerFactory() + repo_public = RepositoryFactory(author=author, private=False) + repo_private = RepositoryFactory(author=author, private=True) + commit_public = CommitWithReportFactory(author=author, repository=repo_public) + commit_private = CommitWithReportFactory(author=author, repository=repo_private) + + self.client.logout() + response = self.client.get( + reverse( + "api-v2-commits-detail", + kwargs={ + "service": author.service, + "owner_username": author.username, + "repo_name": repo_public.name, + "commitid": commit_public.commitid, + }, + ) + ) + + # allows access to public repos + assert response.status_code == 200 + + response = self.client.get( + reverse( + "api-v2-commits-detail", + kwargs={ + "service": author.service, + "owner_username": author.username, + "repo_name": repo_private.name, + "commitid": commit_private.commitid, + }, + ) + ) + + # does not allow access to private repos + assert response.status_code == 404 + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_commit_uploads_authenticated( + self, build_report_from_commit, get_repo_permissions + ): + build_report_from_commit.return_value = MockReport() + get_repo_permissions.return_value = (True, True) + commit = CommitWithReportFactory(author=self.org, repository=self.repo) + + response = self.client.get( + reverse( + "api-v2-commits-uploads", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "commitid": commit.commitid, + }, + ) + ) + data = response.json() + + expected_storage_path = "v4/raw/2019-01-10/4434BC2A2EC4FCA57F77B473D83F928C/abf6d4df662c47e32460020ab14abf9303581429/9ccc55a1-8b41-4bb1-a946-ee7a33a7fb56.txt" + + assert response.status_code == 200 + assert len(data["results"]) == 2 + assert data["results"][0]["storage_path"] == expected_storage_path + assert data["results"][1]["storage_path"] == expected_storage_path + assert data["results"][0]["totals"] == { + "files": 3, + "lines": 20, + "hits": 17, + "misses": 3, + "partials": 0, + "coverage": 85.0, + "branches": 0, + "methods": 0, + } + assert data["results"][1]["totals"] == { + "files": 3, + "lines": 20, + "hits": 17, + "misses": 3, + "partials": 0, + "coverage": 85.0, + "branches": 0, + "methods": 0, + } + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_commit_uploads_pagination( + self, build_report_from_commit, get_repo_permissions + ): + build_report_from_commit.return_value = MockReport() + get_repo_permissions.return_value = (True, True) + commit = CommitWithReportFactory(author=self.org, repository=self.repo) + + response = self.client.get( + reverse( + "api-v2-commits-uploads", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "commitid": commit.commitid, + }, + ), + data={"page_size": 1, "page": 1}, + ) + data_1 = response.json() + # Check that first page has one item + assert response.status_code == 200 + assert data_1["total_pages"] == 2 + assert data_1["count"] == 2 + assert len(data_1["results"]) == 1 + + response = self.client.get( + reverse( + "api-v2-commits-uploads", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "commitid": commit.commitid, + }, + ), + data={"page_size": 1, "page": 2}, + ) + data_2 = response.json() + # Check that second page has one item + assert response.status_code == 200 + assert data_2["total_pages"] == 2 + assert data_2["count"] == 2 + assert len(data_2["results"]) == 1 + + # Check that first and second items are different + assert data_1["results"][0]["created_at"] != data_2["results"][0]["created_at"] diff --git a/apps/codecov-api/api/public/v2/tests/test_api_compare_viewset.py b/apps/codecov-api/api/public/v2/tests/test_api_compare_viewset.py new file mode 100644 index 0000000000..868e86c3f4 --- /dev/null +++ b/apps/codecov-api/api/public/v2/tests/test_api_compare_viewset.py @@ -0,0 +1,1032 @@ +from dataclasses import dataclass +from unittest.mock import PropertyMock, patch + +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + CommitWithReportFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) +from shared.reports.api_report_service import SerializableReport +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine, ReportTotals +from shared.utils.merge import LineType +from shared.utils.sessions import Session + +import services.comparison as comparison +from api.shared.commit.serializers import ReportTotalsSerializer +from compare.models import CommitComparison +from compare.tests.factories import CommitComparisonFactory +from services.comparison import ComparisonReport +from services.components import Component +from utils.test_utils import APIClient + + +def sample_report1(): + report = Report() + first_file = ReportFile("foo/file1.py") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("bar/file2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=["flag1", "flag2"])) + return report + + +def sample_report2(): + report = Report() + first_file = ReportFile("foo/file1.py") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(11, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("bar/file2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append(13, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=["flag1", "flag2"])) + return report + + +class MockSerializableReport(SerializableReport): + """ + Stubs the 'get' method of SerializableReport, which usually constructs + report files on the fly from information not provided by these test, like the chunks + for example. + """ + + def get(self, file_name): + return self.mocked_files.get(file_name) + + @property + def files(self): + return self.mocked_files.keys() + + def __contains__(self, f): + return f in self.mocked_files.keys() + + +class MockedComparisonAdapter: + def __init__(self, test_diff, test_lines=[]): + self.test_lines = test_lines + self.test_diff = test_diff + + async def get_source(self, file_name, commitid): + return {"content": self.test_lines} + + async def get_compare(self, base, head): + return self.test_diff + + async def get_authenticated(self): + return False, False + + +def sample_report_impacted(): + report = Report(flags={"flag1": True}) + first_file = ReportFile("foo/file1.py") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("bar/file2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + third_file = ReportFile("file3.py") + third_file.append(1, ReportLine.create(coverage=1, sessions=[[0, 1]])) + report.append(first_file) + report.append(second_file) + report.append(third_file) + report.add_session(Session(flags=["flag1"])) + return report + + +mock_data_from_archive = """ +{ + "files": [{ + "head_name": "fileA", + "base_name": "fileA", + "head_coverage": { + "hits": 12, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [ + [9,"h"], + [10,"m"] + ], + "unexpected_line_changes": [] + }, + { + "head_name": "fileB", + "base_name": "fileB", + "head_coverage": { + "hits": 12, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [ + [9,"h"], + [10,"h"], + [13,"h"], + [14,"h"], + [15,"h"], + [16,"m"], + [17,"h"] + ], + "unexpected_line_changes": [[[1, "h"], [1, "m"]]] + }] +} +""" + + +@dataclass +class MockSegment: + has_diff_changes: bool = False + has_unintended_changes: bool = False + + +class MockFileComparison(object): + def __init__(self): + self.segments = [ + MockSegment(has_unintended_changes=True, has_diff_changes=False), + MockSegment(has_unintended_changes=False, has_diff_changes=True), + MockSegment(has_unintended_changes=True, has_diff_changes=True), + ] + + +@patch("services.comparison.Comparison.head_report", new_callable=PropertyMock) +@patch("services.comparison.Comparison.base_report", new_callable=PropertyMock) +@patch("services.repo_providers.RepoProviderService.get_adapter") +class TestCompareViewSetRetrieve(APITestCase): + """ + Tests for retrieving a comparison. Does not test data that will be deprecated, + eg base and head report fields. Tests for commits etc will be added as the + compare-api refactor progresses. + """ + + def setUp(self): + self.file_name = "myfile.py" + + self.mock_git_compare_data = { + "commits": [], + "diff": { + "files": { + self.file_name: { + "type": "modified", + "segments": [ + { + "header": ["4", "43", "4", "3"], + "lines": ["", "", ""] + ["-this line is removed"] * 40, + } + ], + "stats": {"removed": 40, "added": 0}, + "totals": ReportTotals.default_totals(), + } + } + }, + } + + self.mocked_compare_adapter = MockedComparisonAdapter( + self.mock_git_compare_data + ) + + self.base_file = ReportFile( + name=self.file_name, totals=[46, 46, 0, 0, 100, 0, 0, 0, 1, 0, 0, 0] + ) + self.base_file._parsed_lines = [[1, "", [[1, 1, 0, 0, 0]], 0, 0]] * 46 + self.base_report = MockSerializableReport() + self.base_report.mocked_files = {self.file_name: self.base_file} + + self.head_file = ReportFile( + name=self.file_name, totals=[6, 6, 0, 0, 100, 0, 0, 0, 1, 0, 0, 0] + ) + self.head_file._parsed_lines = [[1, "", [[1, 1, 0, 0, 0]], 0, 0]] * 6 + self.head_file.totals.diff = ReportTotals.default_totals() + self.head_report = MockSerializableReport() + self.head_report.mocked_files = {self.file_name: self.head_file} + + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org) + self.base, self.head = ( + CommitFactory(repository=self.repo), + CommitFactory(repository=self.repo), + ) + self.current_owner = OwnerFactory( + service=self.org.service, + permission=[self.repo.repoid], + organizations=[self.org.ownerid], + ) + + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + self.expected_files = [ + { + "name": {"base": self.file_name, "head": self.file_name}, + "totals": { + "base": ReportTotalsSerializer(self.base_file.totals).data, + "head": ReportTotalsSerializer(self.head_file.totals).data, + "patch": ReportTotalsSerializer(ReportTotals.default_totals()).data, + }, + "has_diff": True, + "stats": {"added": 0, "removed": 40}, + "change_summary": {}, + "lines": [ + { + "value": "", + "number": {"base": idx, "head": idx}, + "coverage": {"base": LineType.hit, "head": LineType.hit}, + "added": False, + "removed": False, + "is_diff": True, + "sessions": 1, + } + for idx in range(4, 7) + ] + + [ + { + "value": "-this line is removed", + "number": {"base": idx, "head": None}, + "coverage": {"base": LineType.hit, "head": None}, + "added": False, + "removed": True, + "is_diff": True, + "sessions": None, + } + for idx in range(7, 47) + ], + } + ] + + def _get_comparison(self, kwargs={}, query_params={}): + if kwargs == {}: + kwargs = { + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + } + if query_params == {}: + query_params = {"base": self.base.commitid, "head": self.head.commitid} + + return self.client.get( + reverse("api-v2-compare-detail", kwargs=kwargs), + data=query_params, + content_type="application/json", + ) + + def _get_file_comparison(self, file_name="", kwargs={}, query_params={}): + if kwargs == {}: + kwargs = { + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "file_path": file_name or self.file_name, + } + if query_params == {}: + query_params = {"base": self.base.commitid, "head": self.head.commitid} + + return self.client.get( + reverse("api-v2-compare-file", kwargs=kwargs), + data=query_params, + content_type="application/json", + ) + + def _get_flag_comparison(self, kwargs=None, query_params=None): + if kwargs is None: + kwargs = { + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + } + if query_params is None: + query_params = {"base": self.base.commitid, "head": self.head.commitid} + + return self.client.get( + reverse("api-v2-compare-flags", kwargs=kwargs), + data=query_params, + content_type="application/json", + ) + + def test_can_return_public_repo_comparison_with_not_authenticated( + self, adapter_mock, base_report_mock, head_report_mock + ): + adapter_mock.return_value = self.mocked_compare_adapter + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + public_repo = RepositoryFactory(author=self.org, private=False) + base, head = ( + CommitFactory(repository=public_repo), + CommitFactory(repository=public_repo), + ) + + self.client.logout() + response = self._get_comparison( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": public_repo.name, + }, + query_params={"base": base.commitid, "head": head.commitid}, + ) + assert response.status_code == status.HTTP_200_OK + + def test_returns_200_and_expected_files_on_success( + self, adapter_mock, base_report_mock, head_report_mock + ): + adapter_mock.return_value = self.mocked_compare_adapter + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + response = self._get_comparison() + + assert response.status_code == status.HTTP_200_OK + assert response.data["files"] == self.expected_files + + def test_returns_404_if_base_or_head_references_not_found( + self, adapter_mock, base_report_mock, head_report_mock + ): + response = self._get_comparison(query_params={"base": 12345, "head": 678}) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_returns_404_if_user_doesnt_have_permissions( + self, adapter_mock, base_report_mock, head_report_mock + ): + other_user = OwnerFactory() + self.client.force_login(user=other_user) + + adapter_mock.return_value = self.mocked_compare_adapter + + response = self._get_comparison() + + assert response.status_code == 404 + + @patch("redis.Redis.get", lambda self, key: None) + @patch("redis.Redis.set", lambda self, key, val, ex: None) + def test_accepts_pullid_query_param( + self, adapter_mock, base_report_mock, head_report_mock + ): + adapter_mock.return_value = self.mocked_compare_adapter + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + response = self._get_comparison( + query_params={ + "pullid": PullFactory( + base=self.base.commitid, + head=self.head.commitid, + compared_to=self.base.commitid, + pullid=2, + repository=self.repo, + ).pullid + } + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data["files"] == self.expected_files + + @patch("redis.Redis.get", lambda self, key: None) + @patch("redis.Redis.set", lambda self, key, val, ex: None) + def test_has_diff_query_param( + self, adapter_mock, base_report_mock, head_report_mock + ): + adapter_mock.return_value = self.mocked_compare_adapter + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + pull = PullFactory( + base=self.base.commitid, + head=self.head.commitid, + compared_to=self.base.commitid, + pullid=2, + repository=self.repo, + ) + + response = self._get_comparison( + query_params={ + "pullid": pull.pullid, + "has_diff": "false", + } + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data["files"] == [] + + response = self._get_comparison( + query_params={ + "pullid": pull.pullid, + "has_diff": "true", + } + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data["files"] == self.expected_files + + def test_pullid_with_nonexistent_base_returns_404( + self, adapter_mock, base_report_mock, head_report_mock + ): + adapter_mock.return_value = self.mocked_compare_adapter + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + response = self._get_comparison( + query_params={ + "pullid": PullFactory( + base="123456", + head=self.head.commitid, + pullid=2, + repository=self.repo, + ).pullid + } + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_pullid_with_nonexistent_head_returns_404( + self, adapter_mock, base_report_mock, head_report_mock + ): + adapter_mock.return_value = self.mocked_compare_adapter + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + response = self._get_comparison( + query_params={ + "pullid": PullFactory( + base=self.base.commitid, + head="123456", + compared_to=self.base.commitid, + pullid=2, + repository=self.repo, + ).pullid + } + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_file_returns_compare_file_with_diff_and_src_data( + self, adapter_mock, base_report_mock, head_report_mock + ): + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + src = b"first\nfirst\nfirst\nfirst\nfirst\nfirst" + + adapter_mock.return_value = MockedComparisonAdapter( + test_diff=self.mock_git_compare_data, test_lines=src + ) + + response = self._get_file_comparison() + + assert response.status_code == status.HTTP_200_OK + + expected_lines = [ + { + "value": "first", + "number": {"base": idx, "head": idx}, + "coverage": {"base": LineType.hit, "head": LineType.hit}, + "added": False, + "removed": False, + "is_diff": False, + "sessions": 1, + } + for idx in range(1, 4) + ] + self.expected_files[0]["lines"] + + assert response.data["lines"] == expected_lines + + def test_file_ignores_MAX_DIFF_SIZE( + self, adapter_mock, base_report_mock, head_report_mock + ): + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + previous_max = comparison.MAX_DIFF_SIZE + comparison.MAX_DIFF_SIZE = -1 + + src = b"first\nfirst\nfirst\nfirst\nfirst\nfirst" + adapter_mock.return_value = MockedComparisonAdapter( + test_diff=self.mock_git_compare_data, test_lines=src + ) + + response = self._get_file_comparison() + + comparison.MAX_DIFF_SIZE = previous_max + + assert response.status_code == status.HTTP_200_OK + assert len(response.data["lines"]) == 46 + + def test_missing_base_report_returns_none_base_totals( + self, adapter_mock, base_report_mock, head_report_mock + ): + base_report_mock.return_value = None + head_report_mock.return_value = self.head_report + adapter_mock.return_value = self.mocked_compare_adapter + + response = self._get_comparison() + + assert response.status_code == status.HTTP_200_OK + assert response.data["totals"]["base"] is None + + def test_no_raw_reports_returns_404( + self, adapter_mock, base_report_mock, head_report_mock + ): + base_report_mock.return_value = None + head_report_mock.side_effect = comparison.MissingComparisonReport( + "Missing head report" + ) + adapter_mock.return_value = self.mocked_compare_adapter + + response = self._get_comparison() + + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_returns_403_if_user_inactive( + self, adapter_mock, base_report_mock, head_report_mock + ): + self.org.plan = "users-inappy" + self.org.plan_auto_activate = False + self.org.save() + + response = self._get_comparison() + assert response.status_code == 403 + + @patch("redis.Redis.get", lambda self, key: None) + @patch("redis.Redis.set", lambda self, key, val, ex: None) + @patch( + "services.comparison.PullRequestComparison.pseudo_diff_adjusts_tracked_lines", + new_callable=PropertyMock, + ) + @patch( + "services.comparison.PullRequestComparison.update_base_report_with_pseudo_diff" + ) + def test_pull_request_pseudo_comparison_can_update_base_report( + self, + update_base_report_mock, + pseudo_diff_adjusts_tracked_lines_mock, + adapter_mock, + base_report_mock, + head_report_mock, + ): + adapter_mock.return_value = self.mocked_compare_adapter + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + pseudo_diff_adjusts_tracked_lines_mock.return_value = True + + response = self._get_comparison( + query_params={ + "pullid": PullFactory( + base=self.base.commitid, + head=self.head.commitid, + compared_to=self.base.commitid, + pullid=2, + repository=self.repo, + ).pullid + } + ) + + update_base_report_mock.assert_called_once() + + assert response.status_code == status.HTTP_200_OK + assert response.data["files"] == self.expected_files + + def test_flags_comparison(self, adapter_mock, base_report_mock, head_report_mock): + adapter_mock.return_value = self.mocked_compare_adapter + base_report_mock.return_value = self.base_report + head_report_mock.return_value = self.head_report + + res = self._get_flag_comparison() + assert res.status_code == 200 + + @patch("api.public.v2.compare.views.commit_components") + def test_components_comparison( + self, commit_components, adapter_mock, base_report_mock, head_report_mock + ): + commit_components.return_value = [ + Component( + component_id="foo", + paths=[r"^foo/.+"], + name="Foo", + flag_regexes=[], + statuses=[], + ), + Component( + component_id="bar", + paths=[r"^bar/.+"], + name="Bar", + flag_regexes=[], + statuses=[], + ), + ] + adapter_mock.return_value = self.mocked_compare_adapter + base_report_mock.return_value = sample_report1() + head_report_mock.return_value = sample_report2() + + res = self.client.get( + reverse( + "api-v2-compare-components", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ), + data={ + "base": self.base.commitid, + "head": self.head.commitid, + }, + content_type="application/json", + ) + + commit_components.assert_called_once_with(self.head, self.org) + assert res.status_code == 200 + assert res.json() == [ + { + "component_id": "foo", + "name": "Foo", + "base_report_totals": { + "files": 1, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "head_report_totals": { + "files": 1, + "lines": 9, + "hits": 5, + "misses": 4, + "partials": 0, + "coverage": 55.55, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "diff_totals": { + "files": 0, + "lines": 0, + "hits": 0, + "misses": 0, + "partials": 0, + "coverage": 0, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": None, + "complexity_total": None, + "complexity_ratio": 0, + "diff": 0, + }, + }, + { + "component_id": "bar", + "name": "Bar", + "base_report_totals": { + "files": 1, + "lines": 2, + "hits": 1, + "misses": 0, + "partials": 1, + "coverage": 50.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "head_report_totals": { + "files": 1, + "lines": 3, + "hits": 2, + "misses": 0, + "partials": 1, + "coverage": 66.66, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "diff_totals": { + "files": 0, + "lines": 0, + "hits": 0, + "misses": 0, + "partials": 0, + "coverage": 0, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": None, + "complexity_total": None, + "complexity_ratio": 0, + "diff": 0, + }, + }, + ] + + +class TestImpactedFilesComparison(APITestCase): + def setUp(self): + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org) + + self.current_owner = OwnerFactory( + service=self.org.service, + permission=[self.repo.repoid], + organizations=[self.org.ownerid], + ) + + self.parent_commit = CommitWithReportFactory.create( + message="this is a commit message for parent", + commitid="39a24eeb9a00f78e0fd91a091960eee86d415497", + repository=self.repo, + ) + self.commit = CommitWithReportFactory.create( + message="this is a commit message for current", + commitid="fc02b87aac39d16a1626722004e3ec36d046e718", + parent_commit_id=self.parent_commit.commitid, + repository=self.repo, + ) + + self.comparison = CommitComparisonFactory( + base_commit=self.parent_commit, + compare_commit=self.commit, + state=CommitComparison.CommitComparisonStates.PROCESSED, + report_storage_path="v4/test.json", + ) + self.comparison_report = ComparisonReport(self.comparison) + + self.mock_git_compare_data = { + "commits": [], + "diff": { + "files": { + "fileA": { + "type": "modified", + "segments": [ + { + "header": ["4", "43", "4", "3"], + "lines": ["", "", ""] + ["-this line is removed"] * 40, + } + ], + "stats": {"removed": 40, "added": 0}, + "totals": ReportTotals.default_totals(), + } + } + }, + } + + self.src = b"first\nfirst\nfirst\nfirst\nfirst\nfirst" + + self.mocked_compare_adapter = MockedComparisonAdapter( + self.mock_git_compare_data, self.src + ) + + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + @patch("shared.reports.api_report_service.build_report_from_commit") + @patch("shared.api_archive.archive.ArchiveService.read_file") + @patch("services.repo_providers.RepoProviderService.get_adapter") + def test_impacted_files_200_found( + self, adapter_mock, read_file, build_report_from_commit + ): + adapter_mock.return_value = self.mocked_compare_adapter + build_report_from_commit.return_value = sample_report_impacted() + read_file.return_value = mock_data_from_archive + + kwargs = { + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + } + query_params = { + "base": self.parent_commit.commitid, + "head": self.commit.commitid, + } + + response = self.client.get( + reverse("api-v2-compare-impacted-files", kwargs=kwargs), + data=query_params, + content_type="application/json", + ) + data = response.data + + assert response.status_code == status.HTTP_200_OK + assert data.get("base_commit") == "39a24eeb9a00f78e0fd91a091960eee86d415497" + assert data.get("head_commit") == "fc02b87aac39d16a1626722004e3ec36d046e718" + assert data["totals"]["head"]["hits"] == 7 + assert data["totals"]["base"]["hits"] == 7 + assert data["totals"]["patch"]["hits"] == 0 + assert len(data["files"]) == 2 + assert data["state"] == "processed" + + @patch("shared.reports.api_report_service.build_report_from_commit") + @patch("shared.api_archive.archive.ArchiveService.read_file") + @patch("services.repo_providers.RepoProviderService.get_adapter") + @patch("services.task.TaskService.compute_comparison") + @patch("api.shared.compare.serializers.ComparisonSerializer.get_files") + def test_impacted_files_200_not_found( + self, + mock_parent_get_files, + mock_task_service, + adapter_mock, + read_file, + build_report_from_commit, + ): + mock_parent_get_files.return_value = [] + mock_task_service.return_value = None + adapter_mock.return_value = self.mocked_compare_adapter + build_report_from_commit.return_value = sample_report_impacted() + read_file.return_value = mock_data_from_archive + + self.comparison.delete() + + kwargs = { + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + } + query_params = { + "base": self.parent_commit.commitid, + "head": self.commit.commitid, + } + + response = self.client.get( + reverse("api-v2-compare-impacted-files", kwargs=kwargs), + data=query_params, + content_type="application/json", + ) + assert response.status_code == status.HTTP_200_OK + assert not mock_parent_get_files.called + assert mock_task_service.called + assert response.data["files"] == [] + assert response.data["state"] == "pending" + + @patch("services.comparison.Comparison.validate") + @patch("services.comparison.PullRequestComparison.get_file_comparison") + @patch("shared.api_archive.archive.ArchiveService.read_file") + @patch("services.repo_providers.RepoProviderService.get_adapter") + def test_impacted_file_segment_found( + self, adapter_mock, read_file, mock_get_file_comparison, mock_compare_validate + ): + adapter_mock.return_value = self.mocked_compare_adapter + read_file.return_value = mock_data_from_archive + + mock_get_file_comparison.return_value = MockFileComparison() + mock_compare_validate.return_value = True + + kwargs = { + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "file_path": "fileA", + } + query_params = { + "base": self.parent_commit.commitid, + "head": self.commit.commitid, + } + + response = self.client.get( + reverse("api-v2-compare-segments", kwargs=kwargs), + data=query_params, + content_type="application/json", + ) + data = response.data + + assert response.status_code == status.HTTP_200_OK + assert len(data["segments"]) == 1 + assert data["segments"][0]["header"] == "-4,43 +4,3" + assert data["segments"][0]["has_unintended_changes"] == False + assert len(data["segments"][0]["lines"]) > 0 + + @patch("services.task.TaskService.compute_comparison") + @patch("services.comparison.Comparison.validate") + @patch("services.comparison.PullRequestComparison.get_file_comparison") + @patch("shared.api_archive.archive.ArchiveService.read_file") + @patch("services.repo_providers.RepoProviderService.get_adapter") + def test_impacted_file_segment_not_found( + self, + adapter_mock, + read_file, + mock_get_file_comparison, + mock_compare_validate, + mock_task_service, + ): + adapter_mock.return_value = self.mocked_compare_adapter + read_file.return_value = mock_data_from_archive + mock_get_file_comparison.return_value = MockFileComparison() + mock_compare_validate.return_value = True + mock_task_service.return_value = None + + self.comparison.delete() + + kwargs = { + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "file_path": "notarealfile", + } + query_params = { + "base": self.parent_commit.commitid, + "head": self.commit.commitid, + } + + response = self.client.get( + reverse("api-v2-compare-segments", kwargs=kwargs), + data=query_params, + content_type="application/json", + ) + data = response.data + + assert response.status_code == status.HTTP_200_OK + assert data["segments"] == [] diff --git a/apps/codecov-api/api/public/v2/tests/test_api_component_viewset.py b/apps/codecov-api/api/public/v2/tests/test_api_component_viewset.py new file mode 100644 index 0000000000..8fc5e7e2dd --- /dev/null +++ b/apps/codecov-api/api/public/v2/tests/test_api_component_viewset.py @@ -0,0 +1,134 @@ +from unittest.mock import patch + +from django.test import TestCase +from rest_framework.reverse import reverse +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine +from shared.utils.sessions import Session + +from services.components import Component +from utils.test_utils import APIClient + + +# Borrowed from ./test_file_report_viewset.py +def sample_report(): + report = Report() + first_file = ReportFile("foo/file1.py") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + # (1 * 5 hits + 0 * 3 misses) / 8 lines = 0.625 coverage + second_file = ReportFile("bar/file2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + # (1 * 1 hit + 0 * 1 partial) / 2 lines = 0.5 coverage + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=["flag1", "flag2"])) + return report + + +def empty_report(): + return Report() + + +@patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions") +class ComponentViewSetTestCase(TestCase): + def setUp(self): + self.org = OwnerFactory(username="codecov", service="github") + self.repo = RepositoryFactory(author=self.org, name="test-repo", active=True) + self.commit = CommitFactory(repository=self.repo) + self.current_owner = OwnerFactory( + username="codecov-user", + service="github", + organizations=[self.org.ownerid], + permission=[self.repo.repoid], + ) + + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + def _request_components(self): + url = reverse( + "api-v2-components-list", + kwargs={ + "service": "github", + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + return self.client.get(url) + + @patch("api.public.v2.component.views.commit_components") + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_component_list( + self, build_report_from_commit, commit_components, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.side_effect = [sample_report()] + commit_components.return_value = [ + Component( + component_id="foo", + paths=[r".*foo"], + name="Foo", + flag_regexes=[], + statuses=[], + ), + Component( + component_id="bar", + paths=[r".*bar"], + name="Bar", + flag_regexes=[], + statuses=[], + ), + ] + + res = self._request_components() + commit_components.assert_called_once_with(self.commit, self.org) + assert res.status_code == 200 + assert res.json() == [ + {"component_id": "foo", "name": "Foo", "coverage": 62.5}, + {"component_id": "bar", "name": "Bar", "coverage": 50.0}, + ] + + @patch("api.public.v2.component.views.commit_components") + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_component_list_no_coverage( + self, build_report_from_commit, commit_components, get_repo_permissions + ) -> None: + get_repo_permissions.return_value = (True, True) + build_report_from_commit.side_effect = [empty_report()] + commit_components.return_value = [ + Component( + component_id="foo", + paths=[r".*foo"], + name="Foo", + flag_regexes=[], + statuses=[], + ) + ] + + res = self._request_components() + commit_components.assert_called_once_with(self.commit, self.org) + assert res.status_code == 200 + assert res.json() == [ + { + "component_id": "foo", + "name": "Foo", + "coverage": None, + } + ] diff --git a/apps/codecov-api/api/public/v2/tests/test_api_coverage_viewset.py b/apps/codecov-api/api/public/v2/tests/test_api_coverage_viewset.py new file mode 100644 index 0000000000..71a56f0b20 --- /dev/null +++ b/apps/codecov-api/api/public/v2/tests/test_api_coverage_viewset.py @@ -0,0 +1,299 @@ +from unittest.mock import patch + +import pytest +from django.conf import settings +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from reports.tests.factories import RepositoryFlagFactory +from timeseries.models import MeasurementName +from timeseries.tests.factories import DatasetFactory, MeasurementFactory +from utils.test_utils import APIClient + + +@pytest.mark.skipif( + not settings.TIMESERIES_ENABLED, reason="requires timeseries data storage" +) +@patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions") +class CoverageViewSetTestCase(TestCase): + databases = {"default", "timeseries"} + + def setUp(self): + self.org = OwnerFactory(username="codecov", service="github") + self.repo = RepositoryFactory(author=self.org, name="test-repo", active=True) + self.current_owner = OwnerFactory( + username="codecov-user", + service="github", + organizations=[self.org.ownerid], + permission=[self.repo.repoid], + ) + + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + @patch("timeseries.models.Dataset.is_backfilled") + def test_repo_coverage(self, get_repo_permissions, is_backfilled): + get_repo_permissions.return_value = (True, True) + is_backfilled.return_value = True + + DatasetFactory( + repository_id=self.repo.pk, + name=MeasurementName.COVERAGE.value, + ) + + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + timestamp="2022-08-18T00:12:00", + owner_id=self.org.pk, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + branch="main", + value=80.0, + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + timestamp="2022-08-18T00:13:00", + owner_id=self.org.pk, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + branch="main", + value=90.0, + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + timestamp="2022-08-19T00:01:00", + owner_id=self.org.pk, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + branch="main", + value=100.0, + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + timestamp="2022-08-18T00:12:00", + owner_id=self.org.pk, + repo_id=9999, + measurable_id="9999", + branch="main", + value=10.0, + ) + + response = self.client.get( + f"/api/v2/github/codecov/repos/{self.repo.name}/coverage?interval=1d&start_date=2022-08-18&end_date=2022-08-19" + ) + assert response.status_code == 200 + assert response.json() == { + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "timestamp": "2022-08-18T00:00:00Z", + "min": 80.0, + "max": 90.0, + "avg": 85.0, + }, + { + "timestamp": "2022-08-19T00:00:00Z", + "min": 100.0, + "max": 100.0, + "avg": 100.0, + }, + ], + "total_pages": 1, + } + + @patch("timeseries.models.Dataset.is_backfilled") + def test_repo_coverage_branch(self, get_repo_permissions, is_backfilled): + get_repo_permissions.return_value = (True, True) + is_backfilled.return_value = True + + DatasetFactory( + repository_id=self.repo.pk, + name=MeasurementName.COVERAGE.value, + ) + + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + timestamp="2022-08-18T00:12:00", + owner_id=self.org.pk, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + branch="other", + value=80.0, + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + timestamp="2022-08-18T00:13:00", + owner_id=self.org.pk, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + branch="main", + value=90.0, + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + timestamp="2022-08-19T00:01:00", + owner_id=self.org.pk, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + branch="other", + value=100.0, + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + timestamp="2022-08-18T00:12:00", + owner_id=self.org.pk, + repo_id=9999, + measurable_id="9999", + branch="other", + value=10.0, + ) + + response = self.client.get( + f"/api/v2/github/codecov/repos/{self.repo.name}/coverage?interval=1d&start_date=2022-08-18&end_date=2022-08-19&branch=other" + ) + assert response.status_code == 200 + assert response.json() == { + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "timestamp": "2022-08-18T00:00:00Z", + "min": 80.0, + "max": 80.0, + "avg": 80.0, + }, + { + "timestamp": "2022-08-19T00:00:00Z", + "min": 100.0, + "max": 100.0, + "avg": 100.0, + }, + ], + "total_pages": 1, + } + + def test_repo_coverage_no_interval(self, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + + response = self.client.get( + f"/api/v2/github/codecov/repos/{self.repo.name}/coverage" + ) + assert response.status_code == 422 + + def test_repo_coverage_invalid_interval(self, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + + response = self.client.get( + f"/api/v2/github/codecov/repos/{self.repo.name}/coverage?interval=wrong" + ) + assert response.status_code == 422 + + def test_flag_coverage(self, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + + flag1 = RepositoryFlagFactory( + repository=self.repo, + flag_name="flag1", + ) + flag2 = RepositoryFlagFactory( + repository=self.repo, + flag_name="flag1", + ) + + MeasurementFactory( + name=MeasurementName.FLAG_COVERAGE.value, + timestamp="2022-08-10T00:12:00", + owner_id=self.org.pk, + repo_id=self.repo.pk, + measurable_id=str(flag1.pk), + branch="main", + value=100.0, + ) + MeasurementFactory( + name=MeasurementName.FLAG_COVERAGE.value, + timestamp="2022-08-18T00:12:00", + owner_id=self.org.pk, + repo_id=self.repo.pk, + measurable_id=str(flag1.pk), + branch="main", + value=80.0, + ) + MeasurementFactory( + name=MeasurementName.FLAG_COVERAGE.value, + timestamp="2022-08-18T00:13:00", + owner_id=self.org.pk, + repo_id=self.repo.pk, + measurable_id=str(flag1.pk), + branch="main", + value=90.0, + ) + MeasurementFactory( + name=MeasurementName.FLAG_COVERAGE.value, + timestamp="2022-08-19T00:01:00", + owner_id=self.org.pk, + repo_id=self.repo.pk, + measurable_id=str(flag1.pk), + branch="main", + value=100.0, + ) + MeasurementFactory( + name=MeasurementName.FLAG_COVERAGE.value, + timestamp="2022-08-18T00:12:00", + owner_id=self.org.pk, + repo_id=self.repo.pk, + measurable_id=str(flag2.pk), + branch="main", + value=10.0, + ) + MeasurementFactory( + name=MeasurementName.FLAG_COVERAGE.value, + timestamp="2022-08-20T00:12:00", + owner_id=self.org.pk, + repo_id=self.repo.pk, + measurable_id=str(flag1.pk), + branch="main", + value=100.0, + ) + + response = self.client.get( + f"/api/v2/github/codecov/repos/{self.repo.name}/flags/{flag1.flag_name}/coverage?interval=1d&start_date=2022-08-18&end_date=2022-08-19" + ) + assert response.status_code == 200 + assert response.json() == { + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "timestamp": "2022-08-18T00:00:00Z", + "min": 80.0, + "max": 90.0, + "avg": 85.0, + }, + { + "timestamp": "2022-08-19T00:00:00Z", + "min": 100.0, + "max": 100.0, + "avg": 100.0, + }, + ], + "total_pages": 1, + } + + def test_flag_coverage_missing_flag(self, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + + response = self.client.get( + f"/api/v2/github/codecov/repos/{self.repo.name}/flags/wrong-flag/coverage?interval=1d&start_date=2022-08-18&end_date=2022-08-19" + ) + assert response.status_code == 200 + assert response.json() == { + "count": 0, + "next": None, + "previous": None, + "results": [], + "total_pages": 1, + } diff --git a/apps/codecov-api/api/public/v2/tests/test_api_owner_viewset.py b/apps/codecov-api/api/public/v2/tests/test_api_owner_viewset.py new file mode 100644 index 0000000000..d2339026b5 --- /dev/null +++ b/apps/codecov-api/api/public/v2/tests/test_api_owner_viewset.py @@ -0,0 +1,638 @@ +from datetime import timedelta + +from django.utils import timezone +from rest_framework import status +from rest_framework.exceptions import ErrorDetail +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory, SessionFactory + +from codecov_auth.tests.factories import DjangoSessionFactory +from utils.test_utils import APIClient + + +class OwnerViewSetTests(APITestCase): + def _retrieve(self, kwargs): + return self.client.get(reverse("api-v2-owners-detail", kwargs=kwargs)) + + def test_retrieve_returns_owner_with_username(self): + owner = OwnerFactory() + response = self._retrieve( + kwargs={"service": owner.service, "owner_username": owner.username} + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": owner.service, + "username": owner.username, + "name": owner.name, + } + + def test_retrieve_returns_owner_with_period_username(self): + owner = OwnerFactory(username="codecov.test") + response = self._retrieve( + kwargs={"service": owner.service, "owner_username": owner.username} + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": owner.service, + "username": owner.username, + "name": owner.name, + } + + def test_retrieve_returns_404_if_no_matching_username(self): + response = self._retrieve(kwargs={"service": "github", "owner_username": "fff"}) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data == { + "detail": ErrorDetail( + string="No Owner matches the given query.", code="not_found" + ) + } + + def test_retrieve_owner_unknown_service_returns_404(self): + response = self._retrieve( + kwargs={"service": "not-real", "owner_username": "anything"} + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data == {"detail": "Service not found: not-real"} + + +class UserViewSetTests(APITestCase): + def _list(self, kwargs): + return self.client.get(reverse("api-v2-users-list", kwargs=kwargs)) + + def _detail(self, kwargs): + return self.client.get(reverse("api-v2-users-detail", kwargs=kwargs)) + + def _patch(self, kwargs, data): + return self.client.patch( + reverse("api-v2-users-detail", kwargs=kwargs), data=data + ) + + def setUp(self): + self.org = OwnerFactory(service="github") + self.current_owner = OwnerFactory(service="github", organizations=[self.org.pk]) + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + def test_list(self): + response = self._list( + kwargs={"service": self.org.service, "owner_username": self.org.username} + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "service": "github", + "username": self.current_owner.username, + "name": self.current_owner.name, + "activated": False, + "is_admin": False, + "email": self.current_owner.email, + } + ], + "total_pages": 1, + } + + def test_retrieve_by_username(self): + another_user = OwnerFactory(service="github", organizations=[self.org.pk]) + response = self._detail( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "user_username_or_ownerid": another_user.username, + } + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": "github", + "username": another_user.username, + "name": another_user.name, + "activated": False, + "is_admin": False, + "email": another_user.email, + } + + def test_retrieve_by_ownerid(self): + another_user = OwnerFactory(service="github", organizations=[self.org.pk]) + response = self._detail( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "user_username_or_ownerid": another_user.ownerid, + } + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": "github", + "username": another_user.username, + "name": another_user.name, + "activated": False, + "is_admin": False, + "email": another_user.email, + } + + def test_retrieve_cannot_get_details_of_members_of_other_orgs(self): + another_org = OwnerFactory(service="github") + another_user = OwnerFactory(service="github", organizations=[another_org.pk]) + kwargs = { + "service": self.org.service, + "owner_username": self.org.username, + "user_username_or_ownerid": another_user.username, + } + response = self._detail(kwargs=kwargs) + assert response.status_code == status.HTTP_404_NOT_FOUND + + another_user.organizations.append(self.org.pk) + another_user.save() + + response = self._detail(kwargs=kwargs) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": "github", + "username": another_user.username, + "name": another_user.name, + "activated": False, + "is_admin": False, + "email": another_user.email, + } + + def test_retrieve_cannot_get_details_if_not_member_of_org(self): + another_org = OwnerFactory(service="github") + another_user = OwnerFactory(service="github", organizations=[another_org.pk]) + kwargs = { + "service": another_org.service, + "owner_username": another_org.username, + "user_username_or_ownerid": another_user.username, + } + response = self._detail(kwargs=kwargs) + assert response.status_code == status.HTTP_404_NOT_FOUND + + self.current_owner.organizations.append(another_org.pk) + self.current_owner.save() + + response = self._detail(kwargs=kwargs) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": "github", + "username": another_user.username, + "name": another_user.name, + "activated": False, + "is_admin": False, + "email": another_user.email, + } + + def test_update_activate_by_username(self): + another_user = OwnerFactory(service="github", organizations=[self.org.pk]) + + # Activate user + response = self._patch( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "user_username_or_ownerid": another_user.username, + }, + data={"activated": True}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": "github", + "username": another_user.username, + "name": another_user.name, + "activated": True, + "is_admin": False, + "email": another_user.email, + } + + # Deactivate user + response = self._patch( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "user_username_or_ownerid": another_user.username, + }, + data={"activated": False}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": "github", + "username": another_user.username, + "name": another_user.name, + "activated": False, + "is_admin": False, + "email": another_user.email, + } + + def test_update_activate_by_ownerid(self): + another_user = OwnerFactory(service="github", organizations=[self.org.pk]) + + # Activate user + response = self._patch( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "user_username_or_ownerid": another_user.ownerid, + }, + data={"activated": True}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": "github", + "username": another_user.username, + "name": another_user.name, + "activated": True, + "is_admin": False, + "email": another_user.email, + } + + # Deactivate user + response = self._patch( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "user_username_or_ownerid": another_user.ownerid, + }, + data={"activated": False}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": "github", + "username": another_user.username, + "name": another_user.name, + "activated": False, + "is_admin": False, + "email": another_user.email, + } + + def test_update_activate_unauthorized_members_of_other_orgs(self): + another_org = OwnerFactory(service="github") + another_user = OwnerFactory(service="github", organizations=[another_org.pk]) + + # Activate user - not allowed + response = self._patch( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "user_username_or_ownerid": another_user.username, + }, + data={"activated": True}, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + # Deactivate user - not allowed + response = self._patch( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "user_username_or_ownerid": another_user.username, + }, + data={"activated": False}, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + # Request allowed after user joins the org + another_user.organizations.append(self.org.pk) + another_user.save() + + # Activate user + response = self._patch( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "user_username_or_ownerid": another_user.username, + }, + data={"activated": True}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": "github", + "username": another_user.username, + "name": another_user.name, + "activated": True, + "is_admin": False, + "email": another_user.email, + } + + # Deactivate user + response = self._patch( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "user_username_or_ownerid": another_user.username, + }, + data={"activated": False}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": "github", + "username": another_user.username, + "name": another_user.name, + "activated": False, + "is_admin": False, + "email": another_user.email, + } + + def test_update_activate_unauthorized_not_member_of_org(self): + another_org = OwnerFactory(service="github") + another_user = OwnerFactory(service="github", organizations=[another_org.pk]) + + # Activate user - not allowed + response = self._patch( + kwargs={ + "service": another_org.service, + "owner_username": another_org.username, + "user_username_or_ownerid": another_user.username, + }, + data={"activated": True}, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + # Deactivate user - not allowed + response = self._patch( + kwargs={ + "service": another_org.service, + "owner_username": another_org.username, + "user_username_or_ownerid": another_user.username, + }, + data={"activated": False}, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + # Request owner now joins the other org and thus is allowed to activate/deactivate + self.current_owner.organizations.append(another_org.pk) + self.current_owner.save() + + # Activate user + response = self._patch( + kwargs={ + "service": another_org.service, + "owner_username": another_org.username, + "user_username_or_ownerid": another_user.username, + }, + data={"activated": True}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": "github", + "username": another_user.username, + "name": another_user.name, + "activated": True, + "is_admin": False, + "email": another_user.email, + } + + # Deactivate user + response = self._patch( + kwargs={ + "service": another_org.service, + "owner_username": another_org.username, + "user_username_or_ownerid": another_user.username, + }, + data={"activated": False}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": "github", + "username": another_user.username, + "name": another_user.name, + "activated": False, + "is_admin": False, + "email": another_user.email, + } + + def test_update_activate_no_seats_left(self): + another_user = OwnerFactory(service="github", organizations=[self.org.pk]) + another_user_2 = OwnerFactory(service="github", organizations=[self.org.pk]) + + # Activate user 1 + response = self._patch( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "user_username_or_ownerid": another_user.username, + }, + data={"activated": True}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": "github", + "username": another_user.username, + "name": another_user.name, + "activated": True, + "is_admin": False, + "email": another_user.email, + } + + # Activate user 2 + response = self._patch( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "user_username_or_ownerid": another_user_2.username, + }, + data={"activated": True}, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data == { + "detail": ErrorDetail( + string="Cannot activate user -- not enough seats left.", + code="no_seats_left", + ) + } + + # Deactivate user 1 to make room for user 2 + response = self._patch( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "user_username_or_ownerid": another_user.username, + }, + data={"activated": False}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": "github", + "username": another_user.username, + "name": another_user.name, + "activated": False, + "is_admin": False, + "email": another_user.email, + } + + # Activate user 2 now that there's room + response = self._patch( + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "user_username_or_ownerid": another_user_2.username, + }, + data={"activated": True}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "service": "github", + "username": another_user_2.username, + "name": another_user_2.name, + "activated": True, + "is_admin": False, + "email": another_user_2.email, + } + + +class UserSessionViewSetTests(APITestCase): + def _list(self, kwargs): + return self.client.get(reverse("api-v2-user-sessions-list", kwargs=kwargs)) + + def setUp(self): + self.org = OwnerFactory(service="github") + self.admin_owner = OwnerFactory(service="github", organizations=[self.org.pk]) + self.org.admins = [self.admin_owner.pk] + self.org.save() + self.client = APIClient() + + def test_not_part_of_org(self): + self.current_owner = OwnerFactory(service="github", organizations=[]) + self.client.force_login_owner(self.current_owner) + + response = self._list( + kwargs={"service": self.org.service, "owner_username": self.org.username} + ) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + def test_not_admin_of_org(self): + self.not_in_org_owner = OwnerFactory( + service="github", organizations=[self.org.pk] + ) + self.client.force_login_owner(self.not_in_org_owner) + + response = self._list( + kwargs={"service": self.org.service, "owner_username": self.org.username} + ) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + def test_no_sessions(self): + self.client.force_login_owner(self.admin_owner) + + response = self._list( + kwargs={"service": self.org.service, "owner_username": self.org.username} + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "username": self.admin_owner.username, + "name": self.admin_owner.name, + "has_active_session": False, + "expiry_date": None, + } + ], + "total_pages": 1, + } + + def test_has_active_session(self): + expiry_date = timezone.now() + timedelta(days=1) + expiry_date_response = str(expiry_date).replace(" ", "T").replace("+00:00", "Z") + + self.session = SessionFactory( + owner=self.admin_owner, + login_session=DjangoSessionFactory(expire_date=expiry_date), + ) + self.client.force_login_owner(self.admin_owner) + + response = self._list( + kwargs={"service": self.org.service, "owner_username": self.org.username} + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "username": self.admin_owner.username, + "name": self.admin_owner.name, + "has_active_session": True, + "expiry_date": expiry_date_response, + } + ], + "total_pages": 1, + } + + def test_multiple_sessions_one(self): + expiry_date = timezone.now() + timedelta(days=1) + expiry_date_response = str(expiry_date).replace(" ", "T").replace("+00:00", "Z") + + self.session_1 = SessionFactory( + owner=self.admin_owner, + login_session=DjangoSessionFactory(expire_date=expiry_date), + ) + self.session_2 = SessionFactory( + owner=self.admin_owner, + login_session=DjangoSessionFactory( + expire_date=timezone.now() - timedelta(days=1) + ), + ) + self.client.force_login_owner(self.admin_owner) + + response = self._list( + kwargs={"service": self.org.service, "owner_username": self.org.username} + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "username": self.admin_owner.username, + "name": self.admin_owner.name, + "has_active_session": True, + "expiry_date": expiry_date_response, + } + ], + "total_pages": 1, + } + + def test_multiple_sessions_two(self): + expiry_date = timezone.now() + expiry_date_response = str(expiry_date).replace(" ", "T").replace("+00:00", "Z") + + self.session_1 = SessionFactory( + owner=self.admin_owner, + login_session=DjangoSessionFactory(expire_date=expiry_date), + ) + self.session_2 = SessionFactory( + owner=self.admin_owner, + login_session=DjangoSessionFactory( + expire_date=timezone.now() - timedelta(days=1) + ), + ) + self.client.force_login_owner(self.admin_owner) + + response = self._list( + kwargs={"service": self.org.service, "owner_username": self.org.username} + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "username": self.admin_owner.username, + "name": self.admin_owner.name, + "has_active_session": False, + "expiry_date": expiry_date_response, + } + ], + "total_pages": 1, + } diff --git a/apps/codecov-api/api/public/v2/tests/test_api_pull_viewset.py b/apps/codecov-api/api/public/v2/tests/test_api_pull_viewset.py new file mode 100644 index 0000000000..a4eff3b1e3 --- /dev/null +++ b/apps/codecov-api/api/public/v2/tests/test_api_pull_viewset.py @@ -0,0 +1,418 @@ +from unittest.mock import MagicMock, patch + +from django.test import override_settings +from django.urls import reverse +from freezegun import freeze_time +from shared.django_apps.core.tests.factories import ( + OwnerFactory, + PullFactory, + RepositoryFactory, +) + +from codecov.tests.base_test import InternalAPITest +from core.models import Pull +from utils.test_utils import APIClient + + +@freeze_time("2022-01-01T00:00:00") +class PullViewsetTests(InternalAPITest): + def setUp(self): + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org) + self.current_owner = OwnerFactory( + permission=[self.repo.repoid], + organizations=[self.org.ownerid], + ) + self.pulls = [ + PullFactory(repository=self.repo), + PullFactory(repository=self.repo), + ] + Pull.objects.filter(pk=self.pulls[1].pk).update( + updatestamp="2023-01-01T00:00:00" + ) + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + self.no_patch_response = dict(hits=0, misses=0, partials=0, coverage=0.0) + + @patch("api.public.v2.pull.serializers.PullSerializer.get_patch") + def test_list(self, mock_patch): + mock_patch.return_value = self.no_patch_response + res = self.client.get( + reverse( + "api-v2-pulls-list", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + ) + assert res.status_code == 200 + assert res.json() == { + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "pullid": self.pulls[1].pullid, + "title": self.pulls[1].title, + "base_totals": None, + "head_totals": None, + "updatestamp": "2023-01-01T00:00:00Z", + "state": "open", + "ci_passed": None, + "author": None, + "patch": {"hits": 0, "misses": 0, "partials": 0, "coverage": 0.0}, + }, + { + "pullid": self.pulls[0].pullid, + "title": self.pulls[0].title, + "base_totals": None, + "head_totals": None, + "updatestamp": "2022-01-01T00:00:00Z", + "state": "open", + "ci_passed": None, + "author": None, + "patch": {"hits": 0, "misses": 0, "partials": 0, "coverage": 0.0}, + }, + ], + "total_pages": 1, + } + + @patch("api.public.v2.pull.serializers.PullSerializer.get_patch") + def test_list_state(self, mock_patch): + mock_patch.return_value = self.no_patch_response + pull = PullFactory(repository=self.repo, state="closed") + url = reverse( + "api-v2-pulls-list", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + res = self.client.get(f"{url}?state=closed") + assert res.status_code == 200 + assert res.json() == { + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "pullid": pull.pullid, + "title": pull.title, + "base_totals": None, + "head_totals": None, + "updatestamp": "2022-01-01T00:00:00Z", + "state": "closed", + "ci_passed": None, + "author": None, + "patch": {"hits": 0, "misses": 0, "partials": 0, "coverage": 0.0}, + } + ], + "total_pages": 1, + } + + @patch("api.public.v2.pull.serializers.PullSerializer.get_patch") + def test_list_start_date(self, mock_patch): + mock_patch.return_value = self.no_patch_response + url = reverse( + "api-v2-pulls-list", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + res = self.client.get(f"{url}?start_date=2022-12-01") + assert res.status_code == 200 + assert res.json() == { + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "pullid": self.pulls[1].pullid, + "title": self.pulls[1].title, + "base_totals": None, + "head_totals": None, + "updatestamp": "2023-01-01T00:00:00Z", + "state": "open", + "ci_passed": None, + "author": None, + "patch": {"hits": 0, "misses": 0, "partials": 0, "coverage": 0.0}, + } + ], + "total_pages": 1, + } + + @patch("api.public.v2.pull.serializers.PullSerializer.get_patch") + def test_list_cursor_pagination(self, mock_patch): + mock_patch.return_value = self.no_patch_response + url = reverse( + "api-v2-pulls-list", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + res = self.client.get(f"{url}?page_size=1&cursor=") + assert res.status_code == 200 + data = res.json() + assert data["results"] == [ + { + "pullid": self.pulls[1].pullid, + "title": self.pulls[1].title, + "base_totals": None, + "head_totals": None, + "updatestamp": "2023-01-01T00:00:00Z", + "state": "open", + "ci_passed": None, + "author": None, + "patch": {"hits": 0, "misses": 0, "partials": 0, "coverage": 0.0}, + } + ] + assert data["previous"] is None + assert data["next"] is not None + + res = self.client.get(data["next"]) + data = res.json() + assert data["results"] == [ + { + "pullid": self.pulls[0].pullid, + "title": self.pulls[0].title, + "base_totals": None, + "head_totals": None, + "updatestamp": "2022-01-01T00:00:00Z", + "state": "open", + "ci_passed": None, + "author": None, + "patch": {"hits": 0, "misses": 0, "partials": 0, "coverage": 0.0}, + } + ] + assert data["previous"] is not None + assert data["next"] is None + + @patch("api.public.v2.pull.serializers.PullSerializer.get_patch") + @patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions") + def test_retrieve(self, get_repo_permissions, mock_patch): + mock_patch.return_value = self.no_patch_response + get_repo_permissions.return_value = (True, True) + res = self.client.get( + reverse( + "api-v2-pulls-detail", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "pullid": self.pulls[0].pullid, + }, + ) + ) + assert res.status_code == 200 + assert res.json() == { + "pullid": self.pulls[0].pullid, + "title": self.pulls[0].title, + "base_totals": None, + "head_totals": None, + "updatestamp": "2022-01-01T00:00:00Z", + "state": "open", + "ci_passed": None, + "author": None, + "patch": {"hits": 0, "misses": 0, "partials": 0, "coverage": 0.0}, + } + + @patch("api.shared.permissions.RepositoryArtifactPermissions.has_permission") + @patch("api.shared.permissions.SuperTokenPermissions.has_permission") + def test_no_pull_if_unauthenticated_token_request( + self, + super_token_permissions_has_permission, + repository_artifact_permissions_has_permission, + ): + repository_artifact_permissions_has_permission.return_value = False + super_token_permissions_has_permission.return_value = False + res = self.client.get( + reverse( + "api-v2-pulls-detail", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "pullid": self.pulls[0].pullid, + }, + ) + ) + assert res.status_code == 403 + assert ( + res.data["detail"] == "You do not have permission to perform this action." + ) + + @override_settings(SUPER_API_TOKEN="testaxs3o76rdcdpfzexuccx3uatui2nw73r") + @patch("api.shared.permissions.RepositoryArtifactPermissions.has_permission") + def test_no_pull_if_not_super_token_nor_user_token( + self, repository_artifact_permissions_has_permission + ): + repository_artifact_permissions_has_permission.return_value = False + res = self.client.get( + reverse( + "api-v2-pulls-detail", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "pullid": self.pulls[0].pullid, + }, + ), + HTTP_AUTHORIZATION="Bearer 73c8d301-2e0b-42c0-9ace-95eef6b68e86", + ) + assert res.status_code == 401 + assert res.data["detail"] == "Invalid token." + + @override_settings(SUPER_API_TOKEN="testaxs3o76rdcdpfzexuccx3uatui2nw73r") + @patch("api.shared.permissions.RepositoryArtifactPermissions.has_permission") + def test_no_pull_if_super_token_but_no_GET_request( + self, repository_artifact_permissions_has_permission + ): + repository_artifact_permissions_has_permission.return_value = False + res = self.client.post( + reverse( + "api-v2-pulls-detail", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "pullid": self.pulls[0].pullid, + }, + ), + HTTP_AUTHORIZATION="Bearer testaxs3o76rdcdpfzexuccx3uatui2nw73r", + ) + assert res.status_code == 403 + assert ( + res.data["detail"] == "You do not have permission to perform this action." + ) + + @override_settings(SUPER_API_TOKEN="testaxs3o76rdcdpfzexuccx3uatui2nw73r") + @patch("api.public.v2.pull.serializers.PullSerializer.get_patch") + def test_pull_with_valid_super_token(self, mock_patch): + mock_patch.return_value = self.no_patch_response + res = self.client.get( + reverse( + "api-v2-pulls-detail", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "pullid": self.pulls[0].pullid, + }, + ), + HTTP_AUTHORIZATION="Bearer testaxs3o76rdcdpfzexuccx3uatui2nw73r", + ) + assert res.status_code == 200 + assert res.json() == { + "pullid": self.pulls[0].pullid, + "title": self.pulls[0].title, + "base_totals": None, + "head_totals": None, + "updatestamp": "2022-01-01T00:00:00Z", + "state": "open", + "ci_passed": None, + "author": None, + "patch": {"hits": 0, "misses": 0, "partials": 0, "coverage": 0.0}, + } + + @patch("api.public.v2.pull.serializers.ComparisonReport") + @patch("services.comparison.CommitComparison.objects.filter") + def test_retrieve_with_patch_coverage(self, mock_cc_filter, mock_comparison_report): + mock_cc_instance = MagicMock(is_processed=True) + mock_cc_filter.return_value.select_related.return_value.first.return_value = ( + mock_cc_instance + ) + + mock_file = MagicMock() + mock_file.patch_coverage.hits = 10 + mock_file.patch_coverage.misses = 5 + mock_file.patch_coverage.partials = 2 + mock_comparison_report.return_value.impacted_files = [mock_file] + + res = self.client.get( + reverse( + "api-v2-pulls-detail", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "pullid": self.pulls[0].pullid, + }, + ) + ) + assert res.status_code == 200 + data = res.json() + assert data["patch"] == { + "hits": 10, + "misses": 5, + "partials": 2, + "coverage": 58.82, + } + + @patch("api.public.v2.pull.serializers.ComparisonReport") + @patch("services.comparison.CommitComparison.objects.filter") + def test_retrieve_with_patch_coverage_no_branches( + self, mock_cc_filter, mock_comparison_report + ): + mock_cc_instance = MagicMock(is_processed=True) + mock_cc_filter.return_value.select_related.return_value.first.return_value = ( + mock_cc_instance + ) + + mock_file = MagicMock() + mock_file.patch_coverage.hits = 0 + mock_file.patch_coverage.misses = 0 + mock_file.patch_coverage.partials = 0 + mock_comparison_report.return_value.impacted_files = [mock_file] + + res = self.client.get( + reverse( + "api-v2-pulls-detail", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "pullid": self.pulls[0].pullid, + }, + ) + ) + assert res.status_code == 200 + data = res.json() + assert data["patch"] == self.no_patch_response + + @patch("api.public.v2.pull.serializers.ComparisonReport") + @patch("services.comparison.CommitComparison.objects.filter") + def test_retrieve_with_patch_coverage_no_commit_comparison( + self, mock_cc_filter, mock_comparison_report + ): + mock_cc_instance = MagicMock(is_processed=False) + mock_cc_filter.return_value.select_related.return_value.first.return_value = ( + mock_cc_instance + ) + + mock_file = MagicMock() + mock_file.patch_coverage.hits = 0 + mock_file.patch_coverage.misses = 0 + mock_file.patch_coverage.partials = 0 + mock_comparison_report.return_value.impacted_files = [mock_file] + + res = self.client.get( + reverse( + "api-v2-pulls-detail", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "pullid": self.pulls[0].pullid, + }, + ) + ) + assert res.status_code == 200 + data = res.json() + assert data["patch"] is None diff --git a/apps/codecov-api/api/public/v2/tests/test_api_repo_config.py b/apps/codecov-api/api/public/v2/tests/test_api_repo_config.py new file mode 100644 index 0000000000..abfcc30fc0 --- /dev/null +++ b/apps/codecov-api/api/public/v2/tests/test_api_repo_config.py @@ -0,0 +1,62 @@ +from unittest.mock import patch + +from django.urls import reverse +from freezegun import freeze_time +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from codecov.tests.base_test import InternalAPITest +from utils.test_utils import APIClient + + +@freeze_time("2022-01-01T00:00:00") +class RepoConfigViewTests(InternalAPITest): + def setUp(self): + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org) + self.current_owner = OwnerFactory( + permission=[self.repo.repoid], organizations=[self.org.ownerid] + ) + + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + @patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions") + def test_get(self, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + + res = self.client.get( + reverse( + "api-v2-repo-config", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + ) + assert res.status_code == 200 + assert res.json() == { + "upload_token": self.repo.upload_token, + "graph_token": self.repo.image_token, + } + + @patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions") + def test_get_no_part_of_org(self, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + + self.current_owner.organizations = [] + self.current_owner.save() + + res = self.client.get( + reverse( + "api-v2-repo-config", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + ) + assert res.status_code == 403 + assert self.repo.upload_token not in str(res.content) + assert self.repo.image_token not in str(res.content) diff --git a/apps/codecov-api/api/public/v2/tests/test_api_repo_viewset.py b/apps/codecov-api/api/public/v2/tests/test_api_repo_viewset.py new file mode 100644 index 0000000000..10579207c8 --- /dev/null +++ b/apps/codecov-api/api/public/v2/tests/test_api_repo_viewset.py @@ -0,0 +1,134 @@ +from datetime import timedelta +from unittest.mock import patch + +from django.urls import reverse +from django.utils import timezone +from freezegun import freeze_time +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) + +from codecov.tests.base_test import InternalAPITest +from utils.test_utils import APIClient + + +@freeze_time("2022-01-01T00:00:00") +class RepoViewsetTests(InternalAPITest): + def setUp(self): + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org) + self.commit = CommitFactory( + repository=self.repo, timestamp=timezone.now() - timedelta(days=1) + ) + self.current_owner = OwnerFactory( + permission=[self.repo.repoid], organizations=[self.org.ownerid] + ) + + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + def test_list(self): + res = self.client.get( + reverse( + "api-v2-repos-list", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + }, + ) + ) + assert res.status_code == 200 + data = res.json() + + # there's a SQL trigger that updates this - not sure how to test the value + assert data["results"][0]["updatestamp"] is not None + del data["results"][0]["updatestamp"] + + assert data == { + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "name": self.repo.name, + "private": True, + "author": { + "service": self.org.service, + "username": self.org.username, + "name": self.org.name, + }, + "language": self.repo.language, + "branch": "main", + "active": False, + "activated": False, + "totals": { + "branches": 0, + "complexity": 0.0, + "complexity_ratio": 0, + "complexity_total": 0.0, + "coverage": 85.0, + "diff": 0, + "files": 3, + "hits": 17, + "lines": 20, + "methods": 0, + "misses": 3, + "partials": 0, + "sessions": 1, + }, + } + ], + "total_pages": 1, + } + + @patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions") + def test_retrieve(self, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + + res = self.client.get( + reverse( + "api-v2-repos-detail", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + ) + assert res.status_code == 200 + data = res.json() + + # there's a SQL trigger that updates this - not sure how to test the value + assert data["updatestamp"] is not None + del data["updatestamp"] + + assert data == { + "name": self.repo.name, + "private": True, + "author": { + "service": self.org.service, + "username": self.org.username, + "name": self.org.name, + }, + "language": self.repo.language, + "branch": "main", + "active": False, + "activated": False, + "totals": { + "branches": 0, + "complexity": 0.0, + "complexity_ratio": 0, + "complexity_total": 0.0, + "coverage": 85.0, + "diff": 0, + "files": 3, + "hits": 17, + "lines": 20, + "methods": 0, + "misses": 3, + "partials": 0, + "sessions": 1, + }, + } diff --git a/apps/codecov-api/api/public/v2/tests/test_file_report_viewset.py b/apps/codecov-api/api/public/v2/tests/test_file_report_viewset.py new file mode 100644 index 0000000000..836637d826 --- /dev/null +++ b/apps/codecov-api/api/public/v2/tests/test_file_report_viewset.py @@ -0,0 +1,362 @@ +from unittest.mock import call, patch +from urllib.parse import urlencode + +from django.conf import settings +from django.test import TestCase +from rest_framework.reverse import reverse +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine +from shared.utils.sessions import Session + +from core.models import Branch +from utils.test_utils import APIClient + + +def sample_report(): + report = Report() + first_file = ReportFile("foo/file1.py") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("bar/file2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=["flag1", "flag2"])) + return report + + +@patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions") +class FileReportViewSetTestCase(TestCase): + def setUp(self): + self.service = "github" + self.username = "codecov" + self.repo_name = "test-repo" + self.org = OwnerFactory(username=self.username, service=self.service) + self.repo = RepositoryFactory(author=self.org, name=self.repo_name, active=True) + self.current_owner = OwnerFactory( + username="codecov-user", + service="github", + organizations=[self.org.ownerid], + permission=[self.repo.repoid], + ) + self.commit1 = CommitFactory( + author=self.org, + repository=self.repo, + ) + self.commit2 = CommitFactory( + author=self.org, + repository=self.repo, + parent_commit_id=self.commit1.commitid, + ) + self.commit3 = CommitFactory( + author=self.org, + repository=self.repo, + parent_commit_id=self.commit2.commitid, + ) + self.branch = Branch.objects.get(repository=self.repo, name=self.repo.branch) + self.branch.head = self.commit3.commitid + self.branch.save() + + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + def _request_file_report(self, path=None, **params): + url = reverse( + "api-v2-file-report-detail", + kwargs={ + "service": "github", + "owner_username": self.org.username, + "repo_name": self.repo.name, + "path": path, + }, + ) + + qs = urlencode(params) + url = f"{url}?{qs}" + return self.client.get(url) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_file_report(self, build_report_from_commit, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.side_effect = [sample_report()] + + res = self._request_file_report(path="foo/file1.py") + assert res.status_code == 200 + assert res.json() == { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "line_coverage": [ + [1, 0], + [2, 1], + [3, 0], + [5, 0], + [6, 1], + [8, 0], + [9, 0], + [10, 1], + ], + "commit_sha": self.commit3.commitid, + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit3.commitid}/blob/foo/file1.py", + } + + build_report_from_commit.assert_called_once_with(self.commit3) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_file_report_no_walk_back( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.side_effect = [None, sample_report()] + + res = self._request_file_report(path="foo/file1.py") + assert res.status_code == 404 + + build_report_from_commit.assert_called_once_with(self.commit3) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_file_report_not_enough_walk_back( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.side_effect = [None, None, sample_report()] + + res = self._request_file_report(path="foo/file1.py", walk_back=1) + assert res.status_code == 404 + + build_report_from_commit.assert_has_calls( + [call(self.commit3), call(self.commit2)] + ) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_file_report_with_walk_back( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.side_effect = [None, None, sample_report()] + + res = self._request_file_report(path="foo/file1.py", walk_back=2) + assert res.status_code == 200 + assert res.json() == { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "line_coverage": [ + [1, 0], + [2, 1], + [3, 0], + [5, 0], + [6, 1], + [8, 0], + [9, 0], + [10, 1], + ], + "commit_sha": self.commit1.commitid, + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/blob/foo/file1.py", + } + + build_report_from_commit.assert_has_calls( + [call(self.commit3), call(self.commit2), call(self.commit1)] + ) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_file_report_with_walk_back_oldest_sha( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.side_effect = [None, None, sample_report()] + + res = self._request_file_report( + path="foo/file1.py", walk_back=2, oldest_sha=self.commit2.commitid + ) + assert res.status_code == 404 + + # does not walk back to commit1 + build_report_from_commit.assert_has_calls( + [call(self.commit3), call(self.commit2)] + ) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_file_report_large_walk_back( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.side_effect = [sample_report()] + + res = self._request_file_report(path="foo/file1.py", walk_back=21) + assert res.status_code == 400 + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_file_report_walk_back_no_parent( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.side_effect = [None, None, None] + + res = self._request_file_report(path="foo/file1.py", walk_back=20) + assert res.status_code == 404 + + build_report_from_commit.assert_has_calls( + [call(self.commit3), call(self.commit2), call(self.commit1)] + ) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_file_report_walk_back_commit_not_found( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.side_effect = [None, None, None] + + self.commit3.parent_commit_id = "wrong" + self.commit3.save() + + res = self._request_file_report(path="foo/file1.py", walk_back=20) + assert res.status_code == 404 + + build_report_from_commit.assert_has_calls([call(self.commit3)]) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_file_report_walk_back_commit_not_complete( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + + self.commit1.state = "pending" + self.commit1.save() + + build_report_from_commit.side_effect = [ + sample_report(), # skips since the state is pending + None, # skips since there's no report + sample_report(), # found + ] + + res = self._request_file_report(path="foo/file1.py", walk_back=20) + assert res.status_code == 200 + assert res.json() == { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "line_coverage": [ + [1, 0], + [2, 1], + [3, 0], + [5, 0], + [6, 1], + [8, 0], + [9, 0], + [10, 1], + ], + "commit_sha": self.commit3.commitid, + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit3.commitid}/blob/foo/file1.py", + } + + build_report_from_commit.assert_has_calls([call(self.commit3)]) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_file_report_walk_back_found( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.side_effect = [None, sample_report(), sample_report()] + + res = self._request_file_report(path="foo/file1.py", walk_back=20) + assert res.status_code == 200 + + build_report_from_commit.assert_has_calls( + [call(self.commit3), call(self.commit2)] + ) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_file_report_missing_file( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.side_effect = [ + sample_report(), + sample_report(), + sample_report(), + ] + + res = self._request_file_report(path="bar/file1.py", walk_back=20) + assert res.status_code == 404 + + build_report_from_commit.assert_has_calls( + [call(self.commit3), call(self.commit2), call(self.commit1)] + ) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_file_report_missing_parent_commit( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.side_effect = [ + sample_report(), + sample_report(), + sample_report(), + ] + + self.commit3.parent_commit_id = None + self.commit3.save() + + res = self._request_file_report(path="bar/file1.py", walk_back=20) + assert res.status_code == 404 + + build_report_from_commit.assert_has_calls([call(self.commit3)]) diff --git a/apps/codecov-api/api/public/v2/tests/test_flag_viewset.py b/apps/codecov-api/api/public/v2/tests/test_flag_viewset.py new file mode 100644 index 0000000000..ae11dafc45 --- /dev/null +++ b/apps/codecov-api/api/public/v2/tests/test_flag_viewset.py @@ -0,0 +1,133 @@ +from unittest.mock import patch + +from django.test import TestCase +from rest_framework.reverse import reverse +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine +from shared.utils.sessions import Session + +from reports.tests.factories import RepositoryFlagFactory +from utils.test_utils import APIClient + + +def flags_report(): + report = Report() + session_a_id, _ = report.add_session(Session(flags=["foo"])) + session_b_id, _ = report.add_session(Session(flags=["bar"])) + + file_a = ReportFile("foo/file1.py") + file_a.append(1, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + file_a.append(2, ReportLine.create(coverage=0, sessions=[[session_a_id, 0]])) + file_a.append(3, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + file_a.append(5, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + file_a.append(6, ReportLine.create(coverage=0, sessions=[[session_a_id, 0]])) + file_a.append(8, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + file_a.append(9, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + file_a.append(10, ReportLine.create(coverage=0, sessions=[[session_a_id, 0]])) + report.append(file_a) + + file_b = ReportFile("bar/file2.py") + file_b.append(12, ReportLine.create(coverage=1, sessions=[[session_b_id, 1]])) + file_b.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[session_b_id, 2]]) + ) + report.append(file_b) + + return report + + +@patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions") +class FlagViewSetTestCase(TestCase): + def setUp(self): + self.org = OwnerFactory(username="codecov", service="github") + self.repo = RepositoryFactory(author=self.org, name="test-repo", active=True) + self.current_owner = OwnerFactory( + username="codecov-user", + service="github", + organizations=[self.org.ownerid], + permission=[self.repo.repoid], + ) + self.flag1 = RepositoryFlagFactory(flag_name="foo", repository=self.repo) + self.flag2 = RepositoryFlagFactory(flag_name="bar", repository=self.repo) + self.flag2 = RepositoryFlagFactory(flag_name="baz") + + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + def _request_flags(self): + url = reverse( + "api-v2-flags-list", + kwargs={ + "service": "github", + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + return self.client.get(url) + + def test_flag_list_no_commit(self, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + + res = self._request_flags() + assert res.status_code == 200 + assert res.json() == { + "count": 2, + "next": None, + "previous": None, + "results": [ + {"flag_name": "foo", "coverage": None}, + {"flag_name": "bar", "coverage": None}, + ], + "total_pages": 1, + } + + def test_flag_list_no_report(self, get_repo_permissions): + CommitFactory( + author=self.org, + repository=self.repo, + branch=self.repo.branch, + ) + get_repo_permissions.return_value = (True, True) + + res = self._request_flags() + assert res.status_code == 200 + assert res.json() == { + "count": 2, + "next": None, + "previous": None, + "results": [ + {"flag_name": "foo", "coverage": None}, + {"flag_name": "bar", "coverage": None}, + ], + "total_pages": 1, + } + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_flag_list_with_coverage( + self, build_report_from_commit, get_repo_permissions + ): + CommitFactory( + author=self.org, + repository=self.repo, + branch=self.repo.branch, + ) + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = flags_report() + + res = self._request_flags() + assert res.status_code == 200 + assert res.json() == { + "count": 2, + "next": None, + "previous": None, + "results": [ + {"flag_name": "foo", "coverage": 62.5}, + {"flag_name": "bar", "coverage": 100}, + ], + "total_pages": 1, + } diff --git a/apps/codecov-api/api/public/v2/tests/test_owners_view.py b/apps/codecov-api/api/public/v2/tests/test_owners_view.py new file mode 100644 index 0000000000..6870d69f65 --- /dev/null +++ b/apps/codecov-api/api/public/v2/tests/test_owners_view.py @@ -0,0 +1,49 @@ +from django.test import TestCase +from rest_framework.reverse import reverse +from shared.django_apps.core.tests.factories import OwnerFactory + +from utils.test_utils import APIClient + + +class OwnersViewTestCase(TestCase): + def setUp(self): + self.service = "github" + self.org1 = OwnerFactory(username="org1", service=self.service) + self.org2 = OwnerFactory(username="org2", service=self.service) + self.org3 = OwnerFactory(username="org3", service=self.service) + + self.current_owner = OwnerFactory( + username="codecov-user", + service="github", + organizations=[self.org1.pk, self.org2.pk], + ) + + def _request_owners(self, service="github", login=True): + if login: + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + url = reverse( + "api-v2-service-owners", + kwargs={ + "service": service, + }, + ) + + return self.client.get(url) + + def test_owners_list(self): + res = self._request_owners() + assert res.status_code == 200 + assert [item["username"] for item in res.json()["results"]] == [ + "org1", + "org2", + "codecov-user", + ] + + def test_owners_list_invalid_service(self): + res = self._request_owners(service="unknown") + assert res.status_code == 404 + + def test_owners_list_unauthenticated(self): + res = self._request_owners(login=False) + assert res.status_code == 401 diff --git a/apps/codecov-api/api/public/v2/tests/test_report_tree.py b/apps/codecov-api/api/public/v2/tests/test_report_tree.py new file mode 100644 index 0000000000..84b4051de3 --- /dev/null +++ b/apps/codecov-api/api/public/v2/tests/test_report_tree.py @@ -0,0 +1,188 @@ +from unittest.mock import patch +from urllib.parse import urlencode + +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine +from shared.utils.sessions import Session + +from utils.test_utils import APIClient + + +def sample_report(): + report = Report() + first_file = ReportFile("foo/file1.py") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("bar/file2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + third_file = ReportFile("file3.py") + third_file.append(1, ReportLine.create(coverage=1, sessions=[[0, 1]])) + report.append(first_file) + report.append(second_file) + report.append(third_file) + report.add_session(Session(flags=["flag1", "flag2"])) + return report + + +class ReportTreeTests(APITestCase): + def _tree(self, **params): + url = reverse( + "api-v2-report-tree", + kwargs={ + "service": self.current_owner.service, + "owner_username": self.current_owner.username, + "repo_name": self.repo.name, + }, + ) + + qs = urlencode(params) + url = f"{url}?{qs}" + + return self.client.get(url) + + def setUp(self): + self.current_owner = OwnerFactory() + self.repo = RepositoryFactory(author=self.current_owner) + self.commit = CommitFactory( + author=self.current_owner, + repository=self.repo, + ) + + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_tree(self, build_report_from_commit): + build_report_from_commit.return_value = sample_report() + + res = self._tree() + assert res.status_code == 200 + assert res.json() == [ + { + "name": "foo", + "full_path": "foo", + "coverage": 62.5, + "lines": 8, + "hits": 5, + "partials": 0, + "misses": 3, + }, + { + "name": "bar", + "full_path": "bar", + "coverage": 50.0, + "lines": 2, + "hits": 1, + "partials": 1, + "misses": 0, + }, + { + "name": "file3.py", + "full_path": "file3.py", + "coverage": 100.0, + "lines": 1, + "hits": 1, + "partials": 0, + "misses": 0, + }, + ] + + build_report_from_commit.assert_called_once_with(self.commit) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_tree_depth(self, build_report_from_commit): + build_report_from_commit.return_value = sample_report() + + res = self._tree(depth=2) + assert res.status_code == 200 + assert res.json() == [ + { + "name": "foo", + "full_path": "foo", + "coverage": 62.5, + "lines": 8, + "hits": 5, + "partials": 0, + "misses": 3, + "children": [ + { + "name": "file1.py", + "full_path": "foo/file1.py", + "coverage": 62.5, + "lines": 8, + "hits": 5, + "partials": 0, + "misses": 3, + } + ], + }, + { + "name": "bar", + "full_path": "bar", + "coverage": 50.0, + "lines": 2, + "hits": 1, + "partials": 1, + "misses": 0, + "children": [ + { + "name": "file2.py", + "full_path": "bar/file2.py", + "coverage": 50.0, + "lines": 2, + "hits": 1, + "partials": 1, + "misses": 0, + } + ], + }, + { + "name": "file3.py", + "full_path": "file3.py", + "coverage": 100.0, + "lines": 1, + "hits": 1, + "partials": 0, + "misses": 0, + }, + ] + + build_report_from_commit.assert_called_once_with(self.commit) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_tree_path(self, build_report_from_commit): + build_report_from_commit.return_value = sample_report() + + res = self._tree(path="foo") + assert res.status_code == 200 + assert res.json() == [ + { + "name": "file1.py", + "full_path": "foo/file1.py", + "coverage": 62.5, + "lines": 8, + "hits": 5, + "partials": 0, + "misses": 3, + } + ] + + build_report_from_commit.assert_called_once_with(self.commit) diff --git a/apps/codecov-api/api/public/v2/tests/test_report_viewset.py b/apps/codecov-api/api/public/v2/tests/test_report_viewset.py new file mode 100644 index 0000000000..f6ef37c987 --- /dev/null +++ b/apps/codecov-api/api/public/v2/tests/test_report_viewset.py @@ -0,0 +1,1126 @@ +from unittest.mock import call, patch +from urllib.parse import urlencode + +from django.conf import settings +from django.test import TestCase, override_settings +from rest_framework.reverse import reverse +from shared.django_apps.codecov_auth.tests.factories import ( + OwnerFactory, + UserTokenFactory, +) +from shared.django_apps.core.tests.factories import ( + BranchFactory, + CommitFactory, + RepositoryFactory, +) +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine +from shared.utils.sessions import Session + +from services.components import Component +from utils.test_utils import APIClient + + +def sample_report(): + report = Report() + first_file = ReportFile("foo/file1.py") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("bar/file2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=["flag1", "flag2"])) + return report + + +def sample_report_2(): + report = Report() + first_file = ReportFile("App/file1.py") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + second_file = ReportFile("AppOld/file2.py") + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + return report + + +def flags_report(): + report = Report() + session_a_id, _ = report.add_session(Session(flags=["flag-a"])) + session_b_id, _ = report.add_session(Session(flags=["flag-b"])) + + file_a = ReportFile("foo/file1.py") + file_a.append(1, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + file_a.append(2, ReportLine.create(coverage=0, sessions=[[session_a_id, 0]])) + file_a.append(3, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + file_a.append(5, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + file_a.append(6, ReportLine.create(coverage=0, sessions=[[session_a_id, 0]])) + file_a.append(8, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + file_a.append(9, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + file_a.append(10, ReportLine.create(coverage=0, sessions=[[session_a_id, 0]])) + report.append(file_a) + + file_b = ReportFile("bar/file2.py") + file_b.append(12, ReportLine.create(coverage=1, sessions=[[session_b_id, 1]])) + file_b.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[session_b_id, 2]]) + ) + report.append(file_b) + + return report + + +@patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions") +class ReportViewSetTestCase(TestCase): + def setUp(self): + self.service = "github" + self.username = "codecov" + self.repo_name = "test-repo" + self.org = OwnerFactory(username=self.username, service=self.service) + self.repo = RepositoryFactory(author=self.org, name=self.repo_name, active=True) + self.current_owner = OwnerFactory( + username="codecov-user", + service="github", + organizations=[self.org.ownerid], + permission=[self.repo.repoid], + ) + # the order in which these commits are created matters + # because the branch head is the one that is created + # later + self.commit1 = CommitFactory( + author=self.org, + repository=self.repo, + ) + self.commit2 = CommitFactory( + author=self.org, + repository=self.repo, + ) + self.branch = BranchFactory(repository=self.repo, name="test-branch") + + self.commit3 = CommitFactory( + author=self.org, + repository=self.repo, + branch=self.branch.name, + ) + self.branch.head = self.commit3.commitid + self.branch.save() + + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + def _request_report(self, user_token=None, **params): + if user_token: + self.client.logout() + + url = reverse( + "api-v2-report-detail", + kwargs={ + "service": "github", + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + + qs = urlencode(params) + url = f"{url}?{qs}" + return ( + self.client.get(url, HTTP_AUTHORIZATION=f"Bearer {user_token}") + if user_token + else self.client.get(url) + ) + + def _post_report(self, user_token=None, **params): + if user_token: + self.client.logout() + + url = reverse( + "api-v2-report-detail", + kwargs={ + "service": "github", + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + + qs = urlencode(params) + url = f"{url}?{qs}" + return ( + self.client.post(url, HTTP_AUTHORIZATION=f"Bearer {user_token}") + if user_token + else self.client.post(url) + ) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report(self, build_report_from_commit, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = sample_report() + + res = self._request_report() + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 2, + "lines": 10, + "hits": 6, + "misses": 3, + "partials": 1, + "coverage": 60.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "files": [ + { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "line_coverage": [ + [1, 0], + [2, 1], + [3, 0], + [5, 0], + [6, 1], + [8, 0], + [9, 0], + [10, 1], + ], + }, + { + "name": "bar/file2.py", + "totals": { + "files": 0, + "lines": 2, + "hits": 1, + "misses": 0, + "partials": 1, + "coverage": 50.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "line_coverage": [[12, 0], [51, 2]], + }, + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/tree/", + } + + build_report_from_commit.assert_called_once_with(self.commit1) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_commit_sha(self, build_report_from_commit, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = sample_report() + + res = self._request_report(sha=self.commit2.commitid) + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 2, + "lines": 10, + "hits": 6, + "misses": 3, + "partials": 1, + "coverage": 60.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "files": [ + { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "line_coverage": [ + [1, 0], + [2, 1], + [3, 0], + [5, 0], + [6, 1], + [8, 0], + [9, 0], + [10, 1], + ], + }, + { + "name": "bar/file2.py", + "totals": { + "files": 0, + "lines": 2, + "hits": 1, + "misses": 0, + "partials": 1, + "coverage": 50.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "line_coverage": [[12, 0], [51, 2]], + }, + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit2.commitid}/tree/", + } + + build_report_from_commit.assert_called_once_with(self.commit2) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_nonexistent_commit_sha( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = sample_report() + + sha = "aSD*FAJ#GVUAJS-random-sha" + res = self._request_report(sha=sha) + assert res.status_code == 404 + assert res.json() == { + "detail": f"The commit {sha} is not in our records. Please specify valid commit." + } + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_missing_report( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = None + + res = self._request_report() + assert res.status_code == 404 + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_branch(self, build_report_from_commit, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = sample_report() + + res = self._request_report(branch="test-branch") + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 2, + "lines": 10, + "hits": 6, + "misses": 3, + "partials": 1, + "coverage": 60.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "files": [ + { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "line_coverage": [ + [1, 0], + [2, 1], + [3, 0], + [5, 0], + [6, 1], + [8, 0], + [9, 0], + [10, 1], + ], + }, + { + "name": "bar/file2.py", + "totals": { + "files": 0, + "lines": 2, + "hits": 1, + "misses": 0, + "partials": 1, + "coverage": 50.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "line_coverage": [[12, 0], [51, 2]], + }, + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit3.commitid}/tree/", + } + + build_report_from_commit.assert_called_once_with(self.commit3) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_nonexistent_branch( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = sample_report() + + branch = "random-nonexistent-branch-aaa" + res = self._request_report(branch=branch) + assert res.status_code == 404 + assert res.json() == { + "detail": f"The branch '{branch}' in not in our records. Please provide a valid branch name." + } + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_path(self, build_report_from_commit, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = sample_report() + + res = self._request_report(path="bar") + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 1, + "lines": 2, + "hits": 1, + "misses": 0, + "partials": 1, + "coverage": 50.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "files": [ + { + "name": "bar/file2.py", + "totals": { + "files": 0, + "lines": 2, + "hits": 1, + "misses": 0, + "partials": 1, + "coverage": 50.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "line_coverage": [[12, 0], [51, 2]], + } + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/tree/bar", + } + + build_report_from_commit.assert_called_once_with(self.commit1) + assert res.status_code == 200 + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_path_regex_filter( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = sample_report_2() + + # Report has files App/file1.py and AppOld/file2.py + + # Only match App/file1.py + res = self._request_report(path="App/") + assert res.status_code == 200 + data = res.json() + assert data["totals"]["files"] == 1 + assert data["files"][0]["name"] == "App/file1.py" + + # Match both files + res = self._request_report(path="App") + assert res.status_code == 200 + data = res.json() + assert data["totals"]["files"] == 2 + assert data["files"][0]["name"] in ["App/file1.py", "AppOld/file2.py"] + + # Match no files + res = self._request_report(path="Apps") + assert res.status_code == 404 + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_invalid_path(self, build_report_from_commit, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = sample_report() + path = "random-path-that-doesnt-exist-1234" + + res = self._request_report(path=path) + assert res.status_code == 404 + assert res.json() == { + "detail": f"No files or directories found matching path: {path}" + } + + build_report_from_commit.assert_called_once_with(self.commit1) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_flag(self, build_report_from_commit, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = flags_report() + + res = self._request_report(flag="flag-a") + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 1, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "files": [ + { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "line_coverage": [ + [1, 0], + [2, 1], + [3, 0], + [5, 0], + [6, 1], + [8, 0], + [9, 0], + [10, 1], + ], + }, + { + "name": "bar/file2.py", + "totals": { + "files": 0, + "lines": 0, + "hits": 0, + "misses": 0, + "partials": 0, + "coverage": 0, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "line_coverage": [], + }, + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/tree/", + } + + build_report_from_commit.assert_called_once_with(self.commit1) + + res = self._request_report(flag="flag-b") + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 1, + "lines": 2, + "hits": 2, + "misses": 0, + "partials": 0, + "coverage": 100.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "files": [ + { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 0, + "hits": 0, + "misses": 0, + "partials": 0, + "coverage": 0, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "line_coverage": [], + }, + { + "name": "bar/file2.py", + "totals": { + "files": 0, + "lines": 2, + "hits": 2, + "misses": 0, + "partials": 0, + "coverage": 100.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "line_coverage": [[12, 0], [51, 0]], + }, + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/tree/", + } + + build_report_from_commit.assert_has_calls( + [call(self.commit1), call(self.commit1)] + ) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_flag_and_path(self, build_report_from_commit, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = flags_report() + + res = self._request_report(flag="flag-a", path="foo") + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 1, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "files": [ + { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "line_coverage": [ + [1, 0], + [2, 1], + [3, 0], + [5, 0], + [6, 1], + [8, 0], + [9, 0], + [10, 1], + ], + }, + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/tree/foo", + } + + build_report_from_commit.assert_called_once_with(self.commit1) + + @patch("api.shared.permissions.RepositoryArtifactPermissions.has_permission") + @patch("api.shared.permissions.SuperTokenPermissions.has_permission") + def test_no_report_if_unauthenticated_token_request( + self, + super_token_permissions_has_permission, + repository_artifact_permissions_has_permission, + _, + ): + super_token_permissions_has_permission.return_value = False + repository_artifact_permissions_has_permission.return_value = False + + res = self._request_report() + assert res.status_code == 403 + assert ( + res.data["detail"] == "You do not have permission to perform this action." + ) + + @override_settings(SUPER_API_TOKEN="testaxs3o76rdcdpfzexuccx3uatui2nw73r") + @patch("api.shared.permissions.RepositoryArtifactPermissions.has_permission") + def test_no_report_if_not_super_token_nor_user_token( + self, repository_artifact_permissions_has_permission, _ + ): + repository_artifact_permissions_has_permission.return_value = False + res = self._request_report("73c8d301-2e0b-42c0-9ace-95eef6b68e86") + assert res.status_code == 401 + assert res.data["detail"] == "Invalid token." + + @override_settings(SUPER_API_TOKEN="testaxs3o76rdcdpfzexuccx3uatui2nw73r") + @patch("api.shared.permissions.RepositoryArtifactPermissions.has_permission") + def test_no_report_if_super_token_but_no_GET_request( + self, repository_artifact_permissions_has_permission, _ + ): + repository_artifact_permissions_has_permission.return_value = False + res = self._post_report("testaxs3o76rdcdpfzexuccx3uatui2nw73r") + assert res.status_code == 403 + assert ( + res.data["detail"] == "You do not have permission to perform this action." + ) + + @override_settings(SUPER_API_TOKEN="testaxs3o76rdcdpfzexuccx3uatui2nw73r") + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_super_token_permission_success( + self, + build_report_from_commit, + _, + ): + build_report_from_commit.return_value = sample_report() + res = self._request_report("testaxs3o76rdcdpfzexuccx3uatui2nw73r") + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 2, + "lines": 10, + "hits": 6, + "misses": 3, + "partials": 1, + "coverage": 60.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "files": [ + { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "line_coverage": [ + [1, 0], + [2, 1], + [3, 0], + [5, 0], + [6, 1], + [8, 0], + [9, 0], + [10, 1], + ], + }, + { + "name": "bar/file2.py", + "totals": { + "files": 0, + "lines": 2, + "hits": 1, + "misses": 0, + "partials": 1, + "coverage": 50.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "line_coverage": [[12, 0], [51, 2]], + }, + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/tree/", + } + + build_report_from_commit.assert_called_once_with(self.commit1) + + @override_settings(SUPER_API_TOKEN="testaxs3o76rdcdpfzexuccx3uatui2nw73r") + @patch("api.shared.permissions.RepositoryPermissionsService.user_is_activated") + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_success_if_token_is_not_super_but_is_user_token( + self, build_report_from_commit, mock_is_user_activated, get_repo_permissions + ): + build_report_from_commit.return_value = sample_report() + mock_is_user_activated.return_value = True + get_repo_permissions.return_value = (True, True) + user_token = UserTokenFactory( + owner=self.current_owner, + ) + res = self._request_report(user_token.token) + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 2, + "lines": 10, + "hits": 6, + "misses": 3, + "partials": 1, + "coverage": 60.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "files": [ + { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "line_coverage": [ + [1, 0], + [2, 1], + [3, 0], + [5, 0], + [6, 1], + [8, 0], + [9, 0], + [10, 1], + ], + }, + { + "name": "bar/file2.py", + "totals": { + "files": 0, + "lines": 2, + "hits": 1, + "misses": 0, + "partials": 1, + "coverage": 50.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "line_coverage": [[12, 0], [51, 2]], + }, + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/tree/", + } + + build_report_from_commit.assert_called_once_with(self.commit1) + + @patch("api.public.v2.report.views.commit_components") + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_component( + self, build_report_from_commit, commit_components, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + commit_components.return_value = [ + Component( + component_id="foo", + paths=[r"^foo/.+"], + name="Foo", + flag_regexes=["flag1"], + statuses=[], + ), + Component( + component_id="bar", + paths=[r"^bar/.+"], + name="Bar", + flag_regexes=[], + statuses=[], + ), + ] + build_report_from_commit.return_value = flags_report() + + res = self._request_report(component_id="foo") + commit_components.assert_called_once_with(self.commit1, self.org) + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 1, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 2, + "complexity": 0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "files": [ + { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "line_coverage": [ + [1, 0], + [2, 1], + [3, 0], + [5, 0], + [6, 1], + [8, 0], + [9, 0], + [10, 1], + ], + } + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/tree/", + } + + res = self._request_report(component_id="bar") + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 1, + "lines": 2, + "hits": 1, + "misses": 0, + "partials": 1, + "coverage": 50.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 2, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "files": [ + { + "name": "bar/file2.py", + "totals": { + "files": 0, + "lines": 2, + "hits": 1, + "misses": 0, + "partials": 1, + "coverage": 50.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "line_coverage": [[12, 0], [51, 2]], + } + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/tree/", + } + + res = self._request_report(component_id="invalid") + assert res.status_code == 404 + + res = self._request_report(component_id="foo", path="bar") + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 0, + "lines": 0, + "hits": 0, + "misses": 0, + "partials": 0, + "coverage": 0, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "files": [], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/tree/bar", + } + + res = self._request_report(component_id="foo", flag="flag-b") + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 0, + "lines": 0, + "hits": 0, + "misses": 0, + "partials": 0, + "coverage": 0, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "files": [], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/tree/", + } diff --git a/apps/codecov-api/api/public/v2/tests/test_test_results_view.py b/apps/codecov-api/api/public/v2/tests/test_test_results_view.py new file mode 100644 index 0000000000..aec5bcc6a7 --- /dev/null +++ b/apps/codecov-api/api/public/v2/tests/test_test_results_view.py @@ -0,0 +1,229 @@ +from unittest.mock import patch + +from django.test import override_settings +from django.urls import reverse +from freezegun import freeze_time +from rest_framework import status +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from codecov.tests.base_test import InternalAPITest +from reports.tests.factories import TestInstanceFactory +from utils.test_utils import APIClient + + +@freeze_time("2022-01-01T00:00:00") +class TestResultsViewsetTests(InternalAPITest): + def setUp(self): + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org) + self.current_owner = OwnerFactory( + permission=[self.repo.repoid], organizations=[self.org.ownerid] + ) + self.test_instances = [ + TestInstanceFactory(repoid=self.repo.repoid, commitid="1234"), + TestInstanceFactory(repoid=self.repo.repoid, commitid="3456"), + ] + + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + def test_list(self): + url = reverse( + "api-v2-tests-results-list", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + res = self.client.get(url) + assert res.status_code == status.HTTP_200_OK + assert res.json() == { + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "id": self.test_instances[0].id, + "name": self.test_instances[0].test.name, + "test_id": self.test_instances[0].test_id, + "failure_message": self.test_instances[0].failure_message, + "duration_seconds": self.test_instances[0].duration_seconds, + "commitid": self.test_instances[0].commitid, + "outcome": self.test_instances[0].outcome, + "branch": self.test_instances[0].branch, + "repoid": self.test_instances[0].repoid, + }, + { + "id": self.test_instances[1].id, + "name": self.test_instances[1].test.name, + "test_id": self.test_instances[1].test_id, + "failure_message": self.test_instances[1].failure_message, + "duration_seconds": self.test_instances[1].duration_seconds, + "commitid": self.test_instances[1].commitid, + "outcome": self.test_instances[1].outcome, + "branch": self.test_instances[1].branch, + "repoid": self.test_instances[1].repoid, + }, + ], + "total_pages": 1, + } + + def test_list_filters(self): + url = reverse( + "api-v2-tests-results-list", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + res = self.client.get(f"{url}?commit_id={self.test_instances[0].commitid}") + assert res.status_code == status.HTTP_200_OK + assert res.json() == { + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "id": self.test_instances[0].id, + "name": self.test_instances[0].test.name, + "test_id": self.test_instances[0].test_id, + "failure_message": self.test_instances[0].failure_message, + "duration_seconds": self.test_instances[0].duration_seconds, + "commitid": self.test_instances[0].commitid, + "outcome": self.test_instances[0].outcome, + "branch": self.test_instances[0].branch, + "repoid": self.test_instances[0].repoid, + }, + ], + "total_pages": 1, + } + + @patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions") + def test_retrieve(self, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + res = self.client.get( + reverse( + "api-v2-tests-results-detail", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "pk": self.test_instances[0].pk, + }, + ) + ) + assert res.status_code == status.HTTP_200_OK + assert res.json() == { + "id": self.test_instances[0].id, + "name": self.test_instances[0].test.name, + "test_id": self.test_instances[0].test_id, + "failure_message": self.test_instances[0].failure_message, + "duration_seconds": self.test_instances[0].duration_seconds, + "commitid": self.test_instances[0].commitid, + "outcome": self.test_instances[0].outcome, + "branch": self.test_instances[0].branch, + "repoid": self.test_instances[0].repoid, + } + + @patch("api.shared.permissions.RepositoryArtifactPermissions.has_permission") + @patch("api.shared.permissions.SuperTokenPermissions.has_permission") + def test_no_test_result_if_unauthenticated_token_request( + self, + super_token_permissions_has_permission, + repository_artifact_permissions_has_permission, + ): + repository_artifact_permissions_has_permission.return_value = False + super_token_permissions_has_permission.return_value = False + + url = reverse( + "api-v2-tests-results-list", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + res = self.client.get(f"{url}?commit_id={self.test_instances[0].commitid}") + + assert res.status_code == 403 + assert ( + res.data["detail"] == "You do not have permission to perform this action." + ) + + @override_settings(SUPER_API_TOKEN="testaxs3o76rdcdpfzexuccx3uatui2nw73r") + @patch("api.shared.permissions.RepositoryArtifactPermissions.has_permission") + def test_no_result_if_not_super_token_nor_user_token( + self, repository_artifact_permissions_has_permission + ): + repository_artifact_permissions_has_permission.return_value = False + + res = self.client.get( + reverse( + "api-v2-tests-results-detail", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "pk": self.test_instances[0].pk, + }, + ), + HTTP_AUTHORIZATION="Bearer 73c8d301-2e0b-42c0-9ace-95eef6b68e86", + ) + assert res.status_code == 401 + assert res.data["detail"] == "Invalid token." + + @override_settings(SUPER_API_TOKEN="testaxs3o76rdcdpfzexuccx3uatui2nw73r") + @patch("api.shared.permissions.RepositoryArtifactPermissions.has_permission") + def test_no_result_if_super_token_but_no_GET_request( + self, repository_artifact_permissions_has_permission + ): + repository_artifact_permissions_has_permission.return_value = False + res = self.client.post( + reverse( + "api-v2-tests-results-detail", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "pk": self.test_instances[0].pk, + }, + ), + HTTP_AUTHORIZATION="Bearer testaxs3o76rdcdpfzexuccx3uatui2nw73r", + ) + assert res.status_code == 403 + assert ( + res.data["detail"] == "You do not have permission to perform this action." + ) + + @override_settings(SUPER_API_TOKEN="testaxs3o76rdcdpfzexuccx3uatui2nw73r") + @patch("api.shared.permissions.RepositoryArtifactPermissions.has_permission") + def test_result_with_valid_super_token( + self, repository_artifact_permissions_has_permission + ): + repository_artifact_permissions_has_permission.return_value = False + res = self.client.get( + reverse( + "api-v2-tests-results-detail", + kwargs={ + "service": self.org.service, + "owner_username": self.org.username, + "repo_name": self.repo.name, + "pk": self.test_instances[0].pk, + }, + ), + HTTP_AUTHORIZATION="Bearer testaxs3o76rdcdpfzexuccx3uatui2nw73r", + ) + assert res.status_code == 200 + assert res.json() == { + "id": self.test_instances[0].id, + "name": self.test_instances[0].test.name, + "test_id": self.test_instances[0].test_id, + "failure_message": self.test_instances[0].failure_message, + "duration_seconds": self.test_instances[0].duration_seconds, + "commitid": self.test_instances[0].commitid, + "outcome": self.test_instances[0].outcome, + "branch": self.test_instances[0].branch, + "repoid": self.test_instances[0].repoid, + } diff --git a/apps/codecov-api/api/public/v2/tests/test_totals_viewset.py b/apps/codecov-api/api/public/v2/tests/test_totals_viewset.py new file mode 100644 index 0000000000..96eae97a48 --- /dev/null +++ b/apps/codecov-api/api/public/v2/tests/test_totals_viewset.py @@ -0,0 +1,712 @@ +from unittest.mock import call, patch +from urllib.parse import urlencode + +from django.conf import settings +from django.test import TestCase +from rest_framework.reverse import reverse +from shared.django_apps.core.tests.factories import ( + BranchFactory, + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine +from shared.utils.sessions import Session + +from services.components import Component +from utils.test_utils import APIClient + + +def sample_report(): + report = Report() + first_file = ReportFile("foo/file1.py") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("bar/file2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=["flag1", "flag2"])) + return report + + +def flags_report(): + report = Report() + session_a_id, _ = report.add_session(Session(flags=["flag-a"])) + session_b_id, _ = report.add_session(Session(flags=["flag-b"])) + + file_a = ReportFile("foo/file1.py") + file_a.append(1, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + file_a.append(2, ReportLine.create(coverage=0, sessions=[[session_a_id, 0]])) + file_a.append(3, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + file_a.append(5, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + file_a.append(6, ReportLine.create(coverage=0, sessions=[[session_a_id, 0]])) + file_a.append(8, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + file_a.append(9, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + file_a.append(10, ReportLine.create(coverage=0, sessions=[[session_a_id, 0]])) + report.append(file_a) + + file_b = ReportFile("bar/file2.py") + file_b.append(12, ReportLine.create(coverage=1, sessions=[[session_b_id, 1]])) + file_b.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[session_b_id, 2]]) + ) + report.append(file_b) + + return report + + +@patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions") +class TotalsViewSetTestCase(TestCase): + def setUp(self): + self.service = "github" + self.username = "codecov" + self.repo_name = "test-repo" + self.org = OwnerFactory(username=self.username, service=self.service) + self.repo = RepositoryFactory(author=self.org, name=self.repo_name, active=True) + self.current_owner = OwnerFactory( + username="codecov-user", + service="github", + organizations=[self.org.ownerid], + permission=[self.repo.repoid], + ) + # the order in which these commits are created matters + # because the branch head is the one that is created + # later + self.commit1 = CommitFactory( + author=self.org, + repository=self.repo, + ) + self.commit2 = CommitFactory( + author=self.org, + repository=self.repo, + ) + self.branch = BranchFactory(repository=self.repo, name="test-branch") + + self.commit3 = CommitFactory( + author=self.org, + repository=self.repo, + branch=self.branch.name, + ) + self.branch.head = self.commit3.commitid + self.branch.save() + + self.client = APIClient() + self.client.force_login_owner(self.current_owner) + + def _request_report(self, user_token=None, **params): + if user_token: + self.client.logout() + + url = reverse( + "api-v2-totals-detail", + kwargs={ + "service": "github", + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + + qs = urlencode(params) + url = f"{url}?{qs}" + return ( + self.client.get(url, HTTP_AUTHORIZATION=f"Bearer {user_token}") + if user_token + else self.client.get(url) + ) + + def _post_report(self, user_token=None, **params): + if user_token: + self.client.logout() + + url = reverse( + "api-v2-totals-detail", + kwargs={ + "service": "github", + "owner_username": self.org.username, + "repo_name": self.repo.name, + }, + ) + + qs = urlencode(params) + url = f"{url}?{qs}" + return ( + self.client.post(url, HTTP_AUTHORIZATION=f"Bearer {user_token}") + if user_token + else self.client.post(url) + ) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report(self, build_report_from_commit, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = sample_report() + + res = self._request_report() + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 2, + "lines": 10, + "hits": 6, + "misses": 3, + "partials": 1, + "coverage": 60.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "files": [ + { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + }, + { + "name": "bar/file2.py", + "totals": { + "files": 0, + "lines": 2, + "hits": 1, + "misses": 0, + "partials": 1, + "coverage": 50.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + }, + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/tree/", + } + + build_report_from_commit.assert_called_once_with(self.commit1) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_commit_sha(self, build_report_from_commit, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = sample_report() + + res = self._request_report(sha=self.commit2.commitid) + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 2, + "lines": 10, + "hits": 6, + "misses": 3, + "partials": 1, + "coverage": 60.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "files": [ + { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + }, + { + "name": "bar/file2.py", + "totals": { + "files": 0, + "lines": 2, + "hits": 1, + "misses": 0, + "partials": 1, + "coverage": 50.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + }, + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit2.commitid}/tree/", + } + + build_report_from_commit.assert_called_once_with(self.commit2) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_nonexistent_commit_sha( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = sample_report() + + sha = "aSD*FAJ#GVUAJS-random-sha" + res = self._request_report(sha=sha) + assert res.status_code == 404 + assert res.json() == { + "detail": f"The commit {sha} is not in our records. Please specify valid commit." + } + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_branch(self, build_report_from_commit, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = sample_report() + + res = self._request_report(branch="test-branch") + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 2, + "lines": 10, + "hits": 6, + "misses": 3, + "partials": 1, + "coverage": 60.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "files": [ + { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + }, + { + "name": "bar/file2.py", + "totals": { + "files": 0, + "lines": 2, + "hits": 1, + "misses": 0, + "partials": 1, + "coverage": 50.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + }, + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit3.commitid}/tree/", + } + + build_report_from_commit.assert_called_once_with(self.commit3) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_nonexistent_branch( + self, build_report_from_commit, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = sample_report() + + branch = "random-nonexistent-branch-aaa" + res = self._request_report(branch=branch) + assert res.status_code == 404 + assert res.json() == { + "detail": f"The branch '{branch}' in not in our records. Please provide a valid branch name." + } + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_path(self, build_report_from_commit, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = sample_report() + + res = self._request_report(path="bar") + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 1, + "lines": 2, + "hits": 1, + "misses": 0, + "partials": 1, + "coverage": 50.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "files": [ + { + "name": "bar/file2.py", + "totals": { + "files": 0, + "lines": 2, + "hits": 1, + "misses": 0, + "partials": 1, + "coverage": 50.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + } + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/tree/bar", + } + + build_report_from_commit.assert_called_once_with(self.commit1) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_invalid_path(self, build_report_from_commit, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = sample_report() + path = "random-path-that-doesnt-exist-1234" + + res = self._request_report(path=path) + assert res.status_code == 404 + assert res.json() == { + "detail": f"No files or directories found matching path: {path}" + } + + build_report_from_commit.assert_called_once_with(self.commit1) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_flag(self, build_report_from_commit, get_repo_permissions): + get_repo_permissions.return_value = (True, True) + build_report_from_commit.return_value = flags_report() + + res = self._request_report(flag="flag-a") + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 1, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "files": [ + { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + }, + { + "name": "bar/file2.py", + "totals": { + "files": 0, + "lines": 0, + "hits": 0, + "misses": 0, + "partials": 0, + "coverage": 0, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + }, + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/tree/", + } + + build_report_from_commit.assert_called_once_with(self.commit1) + + res = self._request_report(flag="flag-b") + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 1, + "lines": 2, + "hits": 2, + "misses": 0, + "partials": 0, + "coverage": 100.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "files": [ + { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 0, + "hits": 0, + "misses": 0, + "partials": 0, + "coverage": 0, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + }, + { + "name": "bar/file2.py", + "totals": { + "files": 0, + "lines": 2, + "hits": 2, + "misses": 0, + "partials": 0, + "coverage": 100.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + }, + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/tree/", + } + + build_report_from_commit.assert_has_calls( + [call(self.commit1), call(self.commit1)] + ) + + @patch("api.shared.permissions.RepositoryArtifactPermissions.has_permission") + def test_no_report_if_unauthenticated_token_request( + self, + repository_artifact_permissions_has_permission, + _, + ): + repository_artifact_permissions_has_permission.return_value = False + + res = self._request_report() + assert res.status_code == 403 + assert ( + res.data["detail"] + == "Permission denied: some possible reasons for this are (1) the user doesn't have permission to view the specific resource, (2) the organization has a per-user plan or (3) the user is trying to view a private repo but is not activated." + ) + + @patch("api.public.v2.report.views.commit_components") + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_report_component( + self, build_report_from_commit, commit_components, get_repo_permissions + ): + get_repo_permissions.return_value = (True, True) + commit_components.return_value = [ + Component( + component_id="foo", + paths=[r"^foo/.+"], + name="Foo", + flag_regexes=[], + statuses=[], + ), + Component( + component_id="bar", + paths=[r"^bar/.+"], + name="Bar", + flag_regexes=[], + statuses=[], + ), + ] + build_report_from_commit.return_value = sample_report() + + res = self._request_report(component_id="foo") + commit_components.assert_called_once_with(self.commit1, self.org) + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 1, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + "files": [ + { + "name": "foo/file1.py", + "totals": { + "files": 0, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": 62.5, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 10.0, + "complexity_total": 2.0, + "complexity_ratio": 500.0, + "diff": 0, + }, + } + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/tree/", + } + + res = self._request_report(component_id="bar") + assert res.status_code == 200 + assert res.json() == { + "totals": { + "files": 1, + "lines": 2, + "hits": 1, + "misses": 0, + "partials": 1, + "coverage": 50.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + "files": [ + { + "name": "bar/file2.py", + "totals": { + "files": 0, + "lines": 2, + "hits": 1, + "misses": 0, + "partials": 1, + "coverage": 50.0, + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": 0.0, + "complexity_total": 0.0, + "complexity_ratio": 0, + "diff": 0, + }, + } + ], + "commit_file_url": f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.username}/{self.repo_name}/commit/{self.commit1.commitid}/tree/", + } + + res = self._request_report(component_id="invalid") + assert res.status_code == 404 diff --git a/apps/codecov-api/api/public/v2/urls.py b/apps/codecov-api/api/public/v2/urls.py new file mode 100644 index 0000000000..6f95197aab --- /dev/null +++ b/apps/codecov-api/api/public/v2/urls.py @@ -0,0 +1,117 @@ +from django.conf import settings, urls +from django.urls import include, path +from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView +from rest_framework.exceptions import server_error + +from api.shared.error_views import not_found +from utils.routers import OptionalTrailingSlashRouter, RetrieveUpdateDestroyRouter + +from .branch.views import BranchViewSet +from .commit.views import CommitsUploadsViewSet, CommitsViewSet +from .compare.views import CompareViewSet +from .component.views import ComponentViewSet +from .coverage.views import CoverageViewSet, FlagCoverageViewSet +from .flag.views import FlagViewSet +from .owner.views import OwnersViewSet, OwnerViewSet, UserSessionViewSet, UserViewSet +from .pull.views import PullViewSet +from .repo.views import RepositoryConfigView, RepositoryViewSet +from .report.views import FileReportViewSet, ReportViewSet, TotalsViewSet +from .test_results.views import TestResultsView + +urls.handler404 = not_found +urls.handler500 = server_error + +owners_router = OptionalTrailingSlashRouter() +owners_router.register(r"", OwnerViewSet, basename="api-v2-owners") + +owner_artifacts_router = OptionalTrailingSlashRouter() +owner_artifacts_router.register(r"users", UserViewSet, basename="api-v2-users") +owner_artifacts_router.register( + r"user-sessions", UserSessionViewSet, basename="api-v2-user-sessions" +) + +repository_router = OptionalTrailingSlashRouter() +repository_router.register(r"repos", RepositoryViewSet, basename="api-v2-repos") + +repository_artifacts_router = OptionalTrailingSlashRouter() +repository_artifacts_router.register(r"pulls", PullViewSet, basename="api-v2-pulls") +repository_artifacts_router.register( + r"commits", CommitsViewSet, basename="api-v2-commits" +) +repository_artifacts_router.register( + r"branches", BranchViewSet, basename="api-v2-branches" +) +repository_artifacts_router.register(r"flags", FlagViewSet, basename="api-v2-flags") +repository_artifacts_router.register( + r"components", ComponentViewSet, basename="api-v2-components" +) +repository_artifacts_router.register( + r"test-results", TestResultsView, basename="api-v2-tests-results" +) + +compare_router = RetrieveUpdateDestroyRouter() +compare_router.register(r"compare", CompareViewSet, basename="api-v2-compare") + +coverage_router = OptionalTrailingSlashRouter() +coverage_router.register(r"coverage", CoverageViewSet, basename="api-v2-coverage") + +flag_coverage_router = OptionalTrailingSlashRouter() +flag_coverage_router.register( + r"coverage", FlagCoverageViewSet, basename="api-v2-flag-coverage" +) + +totals_router = RetrieveUpdateDestroyRouter() +totals_router.register(r"totals", TotalsViewSet, basename="api-v2-totals") + +report_router = RetrieveUpdateDestroyRouter() +report_router.register(r"report", ReportViewSet, basename="api-v2-report") + +file_report_router = RetrieveUpdateDestroyRouter() +file_report_router.register( + r"file_report/(?P.+)", FileReportViewSet, basename="api-v2-file-report" +) + +service_prefix = "/" +owner_prefix = "//" +repo_prefix = "//repos//" +flag_prefix = repo_prefix + "flags//" +commit_prefix = repo_prefix + "commits//" + +urlpatterns = [ + path(r"schema/", SpectacularAPIView.as_view(), name="api-v2-schema"), + path( + r"docs/", + SpectacularRedocView.as_view(url_name="api-v2-schema"), + name="api-v2-docs", + ), + path( + "/", + OwnersViewSet.as_view({"get": "list"}), + name="api-v2-service-owners", + ), + path(service_prefix, include(owners_router.urls)), + path(owner_prefix, include(owner_artifacts_router.urls)), + path(owner_prefix, include(repository_router.urls)), + path(repo_prefix, include(repository_artifacts_router.urls)), + path( + f"{repo_prefix}config/", + RepositoryConfigView.as_view(), + name="api-v2-repo-config", + ), + path(repo_prefix, include(compare_router.urls)), + path(repo_prefix, include(totals_router.urls)), + path(repo_prefix, include(report_router.urls)), + path(repo_prefix, include(file_report_router.urls)), + path(repo_prefix, include(coverage_router.urls)), + path( + f"{commit_prefix}uploads/", + CommitsUploadsViewSet.as_view({"get": "list"}), + name="api-v2-commits-uploads", + ), +] + +if settings.TIMESERIES_ENABLED: + urlpatterns += [ + path(repo_prefix, include(coverage_router.urls)), + path(flag_prefix, include(flag_coverage_router.urls)), + ] diff --git a/apps/codecov-api/api/shared/branch/__init__.py b/apps/codecov-api/api/shared/branch/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/shared/branch/filters.py b/apps/codecov-api/api/shared/branch/filters.py new file mode 100644 index 0000000000..c1bd4535ac --- /dev/null +++ b/apps/codecov-api/api/shared/branch/filters.py @@ -0,0 +1,8 @@ +import django_filters + + +class BranchFilters(django_filters.FilterSet): + author = django_filters.CharFilter(method="filter_author") + + def filter_author(self, queryset, name, value): + return queryset.filter(authors__contains=[value]) diff --git a/apps/codecov-api/api/shared/branch/mixins.py b/apps/codecov-api/api/shared/branch/mixins.py new file mode 100644 index 0000000000..f4bff840da --- /dev/null +++ b/apps/codecov-api/api/shared/branch/mixins.py @@ -0,0 +1,19 @@ +from django.db.models import F +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import filters, viewsets + +from api.shared.mixins import RepoPropertyMixin +from api.shared.permissions import RepositoryArtifactPermissions + +from .filters import BranchFilters + + +class BranchViewSetMixin(viewsets.GenericViewSet, RepoPropertyMixin): + filter_backends = (DjangoFilterBackend, filters.OrderingFilter) + filterset_class = BranchFilters + ordering_fields = ("updatestamp", "name") + permission_classes = [RepositoryArtifactPermissions] + lookup_field = "name" + + def get_queryset(self): + return self.repo.branches.order_by(F("updatestamp").desc(nulls_last=True)) diff --git a/apps/codecov-api/api/shared/commit/__init__.py b/apps/codecov-api/api/shared/commit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/shared/commit/filters.py b/apps/codecov-api/api/shared/commit/filters.py new file mode 100644 index 0000000000..5b99b686ff --- /dev/null +++ b/apps/codecov-api/api/shared/commit/filters.py @@ -0,0 +1,16 @@ +import django_filters + +from core.models import Commit + + +class CommitFilters(django_filters.FilterSet): + branch = django_filters.CharFilter( + field_name="branch", method="filter_branch", label="branch name" + ) + + def filter_branch(self, queryset, name, value): + return queryset.filter(branch=value) + + class Meta: + model = Commit + fields = ["branch"] diff --git a/apps/codecov-api/api/shared/commit/mixins.py b/apps/codecov-api/api/shared/commit/mixins.py new file mode 100644 index 0000000000..66b4df56f4 --- /dev/null +++ b/apps/codecov-api/api/shared/commit/mixins.py @@ -0,0 +1,24 @@ +from rest_framework import viewsets + +from api.shared.mixins import RepoPropertyMixin +from api.shared.permissions import RepositoryArtifactPermissions + +from .filters import CommitFilters + + +class CommitsViewSetMixin( + viewsets.GenericViewSet, + RepoPropertyMixin, +): + filterset_class = CommitFilters + permission_classes = [RepositoryArtifactPermissions] + lookup_field = "commitid" + + def get_queryset(self): + # We don't use the "report" field in this endpoint since it can be many MBs of JSON. + # Choosing not to fetch it for perf reasons. + return ( + self.repo.commits.defer("_report") + .select_related("author") + .order_by("-timestamp") + ) diff --git a/apps/codecov-api/api/shared/commit/serializers.py b/apps/codecov-api/api/shared/commit/serializers.py new file mode 100644 index 0000000000..35c3ae6ebf --- /dev/null +++ b/apps/codecov-api/api/shared/commit/serializers.py @@ -0,0 +1,116 @@ +from rest_framework import serializers +from shared.reports.resources import Report, ReportFile +from shared.utils.merge import line_type + +from utils import round_decimals_down + + +class BaseTotalsSerializer(serializers.Serializer): + files = serializers.IntegerField() + lines = serializers.IntegerField() + hits = serializers.IntegerField() + misses = serializers.IntegerField() + partials = serializers.IntegerField() + coverage = serializers.SerializerMethodField() + branches = serializers.IntegerField() + methods = serializers.IntegerField() + + def get_coverage(self, totals) -> float: + if totals.coverage is not None: + return round_decimals_down(float(totals.coverage), 2) + return 0 + + +class PatchCoverageSerializer(serializers.Serializer): + hits = serializers.IntegerField() + misses = serializers.IntegerField() + partials = serializers.IntegerField() + coverage = serializers.FloatField() + + +class CommitTotalsSerializer(BaseTotalsSerializer): + files = serializers.IntegerField(source="f") + lines = serializers.IntegerField(source="n") + hits = serializers.IntegerField(source="h") + misses = serializers.IntegerField(source="m") + partials = serializers.IntegerField(source="p") + coverage = serializers.SerializerMethodField() + branches = serializers.IntegerField(source="b") + methods = serializers.IntegerField(source="d") + sessions = serializers.IntegerField(source="s") + complexity = serializers.FloatField(source="C") + complexity_total = serializers.FloatField(source="N") + complexity_ratio = serializers.SerializerMethodField() + diff = serializers.SerializerMethodField( + label="Deprecated: this will always return 0. Please use comparison endpoint for diff totals instead." + ) + + def get_coverage(self, totals) -> float: + if totals.get("c") is None: + return None + else: + return round_decimals_down(float(totals["c"]), 2) + + def get_complexity_ratio(self, totals) -> float: + return ( + round_decimals_down((totals["C"] / totals["N"]) * 100, 2) + if totals["C"] and totals["N"] + else 0 + ) + + def get_diff(self, totals) -> int: + # deprecated + # 0 is used as the default elsewhere in the system so we'll use that here as well (instead of null) + return 0 + + +class ReportTotalsSerializer(BaseTotalsSerializer): + messages = serializers.IntegerField() + sessions = serializers.IntegerField() + complexity = serializers.FloatField() + complexity_total = serializers.FloatField() + complexity_ratio = serializers.SerializerMethodField() + diff = serializers.JSONField() + + def get_complexity_ratio(self, totals) -> float: + return ( + round_decimals_down((totals.complexity / totals.complexity_total) * 100, 2) + if totals.complexity and totals.complexity_total + else 0 + ) + + +class UploadTotalsSerializer(BaseTotalsSerializer): + pass + + +class ReportFileSerializer(serializers.Serializer): + name = serializers.CharField(label="file path") + totals = ReportTotalsSerializer(label="coverage totals") + line_coverage = serializers.SerializerMethodField( + label="line-by-line coverage values" + ) + + def get_line_coverage(self, report_file: ReportFile) -> list: + if self.context.get("include_line_coverage"): + return [ + (ln, line_type(report_line.coverage)) + for ln, report_line in report_file.lines + ] + + def to_representation(self, value): + res = super().to_representation(value) + if not self.context.get("include_line_coverage"): + del res["line_coverage"] + return res + + +class ReportSerializer(serializers.Serializer): + totals = ReportTotalsSerializer(label="coverage totals") + files = serializers.SerializerMethodField(label="file specific coverage totals") + + def get_files(self, report: Report) -> ReportFileSerializer: + return [ + ReportFileSerializer(report.get(file), context=self.context).data + for file in report.files + ] diff --git a/apps/codecov-api/api/shared/compare/__init__.py b/apps/codecov-api/api/shared/compare/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/shared/compare/mixins.py b/apps/codecov-api/api/shared/compare/mixins.py new file mode 100644 index 0000000000..a8e288fc2c --- /dev/null +++ b/apps/codecov-api/api/shared/compare/mixins.py @@ -0,0 +1,156 @@ +import logging + +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.exceptions import NotFound +from rest_framework.response import Response + +from api.shared.mixins import CompareSlugMixin +from api.shared.permissions import RepositoryArtifactPermissions +from compare.models import CommitComparison +from services.comparison import ( + CommitComparisonService, + Comparison, + MissingComparisonCommit, + MissingComparisonReport, + PullRequestComparison, +) +from services.decorators import torngit_safe +from services.task import TaskService + +from .serializers import ( + FileComparisonSerializer, + FlagComparisonSerializer, + ImpactedFilesComparisonSerializer, + ImpactedFileSegmentsSerializer, +) + +log = logging.getLogger(__name__) + + +class CompareViewSetMixin(CompareSlugMixin, viewsets.GenericViewSet): + permission_classes = [RepositoryArtifactPermissions] + + def get_object(self) -> Comparison: + compare_data = self.get_compare_data() + + if "pull" in compare_data: + comparison = PullRequestComparison( + user=self.request.current_owner, + pull=compare_data["pull"], + ) + try: + # make sure we have a base and head commit + comparison.base_commit + comparison.head_commit + except MissingComparisonCommit: + raise NotFound("Sorry, we are missing a commit for that pull request.") + else: + comparison = Comparison( + user=self.request.current_owner, + base_commit=compare_data["base"], + head_commit=compare_data["head"], + ) + + return comparison + + def get_or_create_commit_comparison( + self, comparison: Comparison + ) -> CommitComparison: + """ + Retrieves the pre-computed CommitComparison + if not found will create one and return None + """ + commit_comparison = CommitComparisonService.fetch_precomputed( + comparison.head_commit.repository_id, + [(comparison.base_commit.commitid, comparison.head_commit.commitid)], + ) + + # Can't use pre-computed impacted files from CommitComparison + # first trigger a Celery task to create a comparison for this commit pair for the future + if not commit_comparison: + new_comparison = CommitComparison( + base_commit=comparison.base_commit, + compare_commit=comparison.head_commit, + state=CommitComparison.CommitComparisonStates.PENDING, + ) + new_comparison.save() + TaskService().compute_comparison(new_comparison.pk) + log.info( + "CommitComparison not found, creating and request to compute new entry" + ) + return new_comparison + return commit_comparison[0] + + @torngit_safe + def retrieve(self, request, *args, **kwargs): + comparison = self.get_object() + + # Some checks here for pseudo-comparisons. Basically, when pseudo-comparing, + # we sometimes might need to tweak the base report + if isinstance(comparison, PullRequestComparison): + if comparison.pseudo_diff_adjusts_tracked_lines: + comparison.update_base_report_with_pseudo_diff() + serializer = self.get_serializer(comparison) + + try: + return Response(serializer.data) + except MissingComparisonReport: + raise NotFound("Raw report not found for base or head reference.") + + @action( + detail=False, + methods=["get"], + url_path="file/(?P.+)", + url_name="file", + ) + @torngit_safe + def file(self, request, *args, **kwargs): + comparison = self.get_object() + file_path = file_path = kwargs.get("file_path") + if file_path not in comparison.head_report: + raise NotFound("File not found in head report.") + return Response( + FileComparisonSerializer( + comparison.get_file_comparison( + file_path, with_src=True, bypass_max_diff=True + ) + ).data + ) + + @action(detail=False, methods=["get"]) + @torngit_safe + def flags(self, request, *args, **kwargs): + comparison = self.get_object() + flags = [ + comparison.flag_comparison(flag_name) + for flag_name in comparison.non_carried_forward_flags + ] + return Response(FlagComparisonSerializer(flags, many=True).data) + + @action(detail=False, methods=["get"]) + @torngit_safe + def impacted_files(self, request, *args, **kwargs): + comparison = self.get_object() + return Response( + ImpactedFilesComparisonSerializer( + comparison, + context={ + "commit_comparison": self.get_or_create_commit_comparison( + comparison + ) + }, + ).data + ) + + @action(detail=False, methods=["get"]) + @torngit_safe + def segments(self, request, *args, **kwargs): + file_path = file_path = kwargs.get("file_path") + comparison = self.get_object() + + return Response( + ImpactedFileSegmentsSerializer( + file_path, context={"comparison": comparison} + ).data + ) diff --git a/apps/codecov-api/api/shared/compare/serializers.py b/apps/codecov-api/api/shared/compare/serializers.py new file mode 100644 index 0000000000..162fd32387 --- /dev/null +++ b/apps/codecov-api/api/shared/compare/serializers.py @@ -0,0 +1,220 @@ +import dataclasses +import logging +from typing import List + +from rest_framework import serializers + +from api.internal.commit.serializers import CommitSerializer +from api.shared.commit.serializers import ReportTotalsSerializer +from services.comparison import ( + Comparison, + ComparisonReport, + FileComparison, + ImpactedFile, + Segment, +) + +log = logging.getLogger(__name__) + + +class TotalsComparisonSerializer(serializers.Serializer): + base = ReportTotalsSerializer() + head = ReportTotalsSerializer() + patch = ReportTotalsSerializer(source="diff") + + +class LineComparisonSerializer(serializers.Serializer): + value = serializers.CharField() + number = serializers.JSONField() + coverage = serializers.JSONField() + is_diff = serializers.BooleanField() + added = serializers.BooleanField() + removed = serializers.BooleanField() + sessions = serializers.IntegerField(source="hit_count") + + +class FileComparisonSerializer(serializers.Serializer): + name = serializers.JSONField() + totals = TotalsComparisonSerializer() + has_diff = serializers.BooleanField() + stats = serializers.JSONField() + change_summary = serializers.JSONField() + lines = LineComparisonSerializer(many=True) + + +class ComparisonSerializer(serializers.Serializer): + base_commit = serializers.CharField(source="base_commit.commitid") + head_commit = serializers.CharField(source="head_commit.commitid") + totals = TotalsComparisonSerializer() + commit_uploads = CommitSerializer(many=True, source="upload_commits") + diff = serializers.SerializerMethodField() + files = serializers.SerializerMethodField() + untracked = serializers.SerializerMethodField() + + def get_untracked(self, comparison) -> List[str]: + return [ + f + for f, _ in comparison.git_comparison["diff"]["files"].items() + if f not in (comparison.base_report or []) + and f not in comparison.head_report + ] + + def get_diff(self, comparison) -> dict: + return {"git_commits": comparison.git_commits} + + def get_files(self, comparison: Comparison) -> List[dict]: + return [ + FileComparisonSerializer(file).data + for file in comparison.files + if self._should_include_file(file) + ] + + def _should_include_file(self, file: FileComparison): + if "has_diff" in self.context: + return self.context["has_diff"] == file.has_diff + else: + return True + + +class FlagComparisonSerializer(serializers.Serializer): + name = serializers.CharField(source="flag_name") + base_report_totals = serializers.SerializerMethodField() + head_report_totals = ReportTotalsSerializer(source="head_report.totals") + diff_totals = ReportTotalsSerializer() + + def get_base_report_totals(self, obj): + if obj.base_report: + return ReportTotalsSerializer(obj.base_report.totals).data + + +class ImpactedFileSegmentSerializer(serializers.Serializer): + header = serializers.SerializerMethodField() + has_unintended_changes = serializers.BooleanField() + lines = serializers.SerializerMethodField() + + def get_header(self, segment: Segment) -> serializers.CharField: + ( + base_starting, + base_extracted, + head_starting, + head_extracted, + ) = segment.header + base = f"{base_starting}" + if base_extracted is not None: + base = f"{base},{base_extracted}" + head = f"{head_starting}" + if head_extracted is not None: + head = f"{head},{head_extracted}" + return f"-{base} +{head}" + + def get_lines(self, segment: Segment) -> serializers.ListField: + lines = [] + for line in segment.lines: + value = line.value + if value and line.is_diff: + content = f"{value[0]} {value[1:]}" + else: + content = f" {value}" + + lines.append( + { + "base_number": line.number["base"], + "head_number": line.number["head"], + "base_coverage": line.coverage["base"], + "head_coverage": line.coverage["head"], + "content": content, + } + ) + return lines + + +class ImpactedFileSegmentsSerializer(serializers.Serializer): + segments = serializers.SerializerMethodField() + + def get_segments(self, file_path: str) -> ImpactedFileSegmentSerializer: + file_comparison = self.context["comparison"].get_file_comparison( + file_path, with_src=True, bypass_max_diff=True + ) + return [ + ImpactedFileSegmentSerializer(segment).data + for segment in file_comparison.segments + ] + + +class ImpactedFileSerializer(serializers.Serializer): + file_name = serializers.SerializerMethodField() + base_name = serializers.CharField() + head_name = serializers.CharField() + is_new_file = serializers.SerializerMethodField() + is_renamed_file = serializers.SerializerMethodField() + is_deleted_file = serializers.SerializerMethodField() + base_coverage = serializers.SerializerMethodField() + head_coverage = serializers.SerializerMethodField() + patch_coverage = serializers.SerializerMethodField() + change_coverage = serializers.SerializerMethodField() + misses_count = serializers.SerializerMethodField() + + def get_base_coverage(self, impacted_file: ImpactedFile) -> serializers.JSONField: + if impacted_file.base_coverage: + return dataclasses.asdict(impacted_file.base_coverage) + + def get_head_coverage(self, impacted_file: ImpactedFile) -> serializers.JSONField: + if impacted_file.head_coverage: + return dataclasses.asdict(impacted_file.head_coverage) + + def get_patch_coverage(self, impacted_file: ImpactedFile) -> serializers.JSONField: + if impacted_file.patch_coverage: + return dataclasses.asdict(impacted_file.patch_coverage) + + def get_file_name(self, impacted_file: ImpactedFile) -> serializers.CharField: + return impacted_file.file_name + + def get_is_new_file(self, impacted_file: ImpactedFile) -> serializers.BooleanField: + base_name = impacted_file.base_name + head_name = impacted_file.head_name + return base_name is None and head_name is not None + + def get_is_renamed_file( + self, impacted_file: ImpactedFile + ) -> serializers.BooleanField: + base_name = impacted_file.base_name + head_name = impacted_file.head_name + return ( + base_name is not None and head_name is not None and base_name != head_name + ) + + def get_is_deleted_file( + self, impacted_file: ImpactedFile + ) -> serializers.BooleanField: + base_name = impacted_file.base_name + head_name = impacted_file.head_name + return base_name is not None and head_name is None + + def get_change_coverage( + self, impacted_file: ImpactedFile + ) -> serializers.FloatField: + return impacted_file.change_coverage + + def get_misses_count(self, impacted_file: ImpactedFile) -> serializers.IntegerField: + return impacted_file.misses_count + + +class ImpactedFilesComparisonSerializer(ComparisonSerializer): + files = serializers.SerializerMethodField() + state = serializers.SerializerMethodField() + + def get_state(self, comparison: Comparison) -> str: + return self.context["commit_comparison"].state + + def get_files(self, comparison: Comparison) -> List[dict]: + commit_comparison = self.context["commit_comparison"] + + if not commit_comparison.is_processed: + return [] + + return [ + ImpactedFileSerializer( + impacted_file, context={"comparison": comparison} + ).data + for impacted_file in ComparisonReport(commit_comparison).files + ] diff --git a/apps/codecov-api/api/shared/error_views.py b/apps/codecov-api/api/shared/error_views.py new file mode 100644 index 0000000000..c5ee950b3e --- /dev/null +++ b/apps/codecov-api/api/shared/error_views.py @@ -0,0 +1,7 @@ +from django.http import JsonResponse +from rest_framework import status + + +def not_found(request, *args, **kwargs): + data = {"error": "Page Not Found (404)"} + return JsonResponse(data=data, status=status.HTTP_404_NOT_FOUND) diff --git a/apps/codecov-api/api/shared/mixins.py b/apps/codecov-api/api/shared/mixins.py new file mode 100644 index 0000000000..43af0d7881 --- /dev/null +++ b/apps/codecov-api/api/shared/mixins.py @@ -0,0 +1,129 @@ +from typing import Optional + +from django.conf import settings +from django.http import Http404 +from django.shortcuts import get_object_or_404 +from django.utils.functional import cached_property +from rest_framework.exceptions import NotFound + +from api.shared.serializers import ( + CommitRefQueryParamSerializer, + PullIDQueryParamSerializer, +) +from codecov_auth.authentication import ( + InternalToken, + InternalUser, + SuperToken, + SuperUser, +) +from codecov_auth.models import Owner, Service +from core.models import Commit, Repository +from utils.services import get_long_service_name + + +class OwnerPropertyMixin: + @cached_property + def owner(self): + service = get_long_service_name(self.kwargs.get("service")) + if service not in Service: + raise Http404("Invalid service for Owner.") + + return get_object_or_404( + Owner, username=self.kwargs.get("owner_username"), service=service + ) + + +class RepoPropertyMixin(OwnerPropertyMixin): + @cached_property + def repo(self): + return get_object_or_404( + Repository, name=self.kwargs.get("repo_name"), author=self.owner + ) + + def get_commit(self, commit_sha: Optional[str] = None) -> Commit: + commit_sha = commit_sha or self.request.query_params.get("sha") + if not commit_sha: + branch_name = self.request.query_params.get("branch", self.repo.branch) + branch = self.repo.branches.filter(name=branch_name).first() + if branch is None: + raise NotFound( + f"The branch '{branch_name}' in not in our records. Please provide a valid branch name.", + 404, + ) + + commit_sha = branch.head + + commit = self.repo.commits.filter(commitid=commit_sha).first() + if commit is None: + raise NotFound( + f"The commit {commit_sha} is not in our records. Please specify valid commit.", + 404, + ) + + return commit + + +class RepositoriesMixin: + @cached_property + def repositories(self): + """ + List of repositories passed in through request query parameters. Used when generating chart response data. + """ + service = get_long_service_name(self.kwargs.get("service")) + + return Repository.objects.filter( + name__in=self.request.data.get("repositories", []), + author__username=self.kwargs.get("owner_username"), + author__service=service, + ) + + +class CompareSlugMixin(RepoPropertyMixin): + def _get_query_param_serializer_class(self): + if "pullid" in self.request.query_params: + return PullIDQueryParamSerializer + return CommitRefQueryParamSerializer + + def get_compare_data(self): + serializer = self._get_query_param_serializer_class()( + data=self.request.query_params, context={"repo": self.repo} + ) + serializer.is_valid(raise_exception=True) + validated_data = serializer.validated_data + return validated_data + + +class SuperPermissionsMixin: + def has_super_token_permissions(self, request): + if request.method != "GET": + return False + user = request.user + auth = request.auth + + if not isinstance(request.user, SuperUser) or not isinstance( + request.auth, SuperToken + ): + return False + return ( + user.is_super_user + and auth.is_super_token + and auth.token == settings.SUPER_API_TOKEN + ) + + +class InternalPermissionsMixin: + def has_internal_token_permissions(self, request): + if request.method != "POST": + return False + user = request.user + auth = request.auth + + if not isinstance(request.user, InternalUser) or not isinstance( + request.auth, InternalToken + ): + return False + return ( + user.is_internal_user + and auth.is_internal_token + and auth.token == settings.CODECOV_INTERNAL_TOKEN + ) diff --git a/apps/codecov-api/api/shared/owner/filters.py b/apps/codecov-api/api/shared/owner/filters.py new file mode 100644 index 0000000000..9629cf0b2e --- /dev/null +++ b/apps/codecov-api/api/shared/owner/filters.py @@ -0,0 +1,12 @@ +import django_filters + + +class UserFilters(django_filters.FilterSet): + activated = django_filters.BooleanFilter(method="filter_activated") + is_admin = django_filters.BooleanFilter(method="filter_is_admin") + + def filter_activated(self, queryset, name, value): + return queryset.filter(activated=value) + + def filter_is_admin(self, queryset, name, value): + return queryset.filter(is_admin=value) diff --git a/apps/codecov-api/api/shared/owner/mixins.py b/apps/codecov-api/api/shared/owner/mixins.py new file mode 100644 index 0000000000..bb9a1cc5ae --- /dev/null +++ b/apps/codecov-api/api/shared/owner/mixins.py @@ -0,0 +1,85 @@ +from django.db.models import BooleanField, Case, Max, Q, QuerySet, When +from django.shortcuts import get_object_or_404 +from django.utils import timezone +from django_filters import rest_framework as django_filters +from rest_framework import filters, viewsets +from rest_framework.exceptions import NotFound + +from api.shared.mixins import OwnerPropertyMixin +from api.shared.permissions import MemberOfOrgPermissions, UserIsAdminPermissions +from codecov_auth.models import Owner, Service + +from .filters import UserFilters + + +class OwnerViewSetMixin(viewsets.GenericViewSet): + lookup_field = "owner_username" + lookup_value_regex = "[^/]+" + + def get_queryset(self) -> QuerySet: + service = self.kwargs.get("service") + try: + Service(service) + except ValueError: + raise NotFound(f"Service not found: {service}") + return Owner.objects.filter(service=self.kwargs.get("service")) + + def get_object(self) -> Owner: + return get_object_or_404( + self.get_queryset(), + username=self.kwargs.get("owner_username"), + service=self.kwargs.get("service"), + ) + + +class UserViewSetMixin( + viewsets.GenericViewSet, + OwnerPropertyMixin, +): + filter_backends = ( + django_filters.DjangoFilterBackend, + filters.SearchFilter, + ) + filterset_class = UserFilters + permission_classes = [MemberOfOrgPermissions] + ordering_fields = ("name", "username", "email", "last_pull_timestamp", "activated") + lookup_field = "user_username_or_ownerid" + search_fields = ["name", "username", "email"] + + def get_queryset(self) -> QuerySet: + return ( + Owner.objects.users_of(owner=self.owner) + .annotate_activated_in(owner=self.owner) + .annotate_is_admin_in(owner=self.owner) + ) + + def get_object(self) -> Owner: + username_or_ownerid = self.kwargs.get("user_username_or_ownerid") + try: + ownerid = int(username_or_ownerid) + except ValueError: + ownerid = None + + return get_object_or_404( + self.get_queryset(), + (Q(username=username_or_ownerid) | Q(ownerid=ownerid)), + ) + + +class UserSessionViewSetMixin( + viewsets.GenericViewSet, + OwnerPropertyMixin, +): + permission_classes = [UserIsAdminPermissions] + ordering_fields = ("name", "username") + + def get_queryset(self) -> QuerySet: + return Owner.objects.users_of(owner=self.owner).annotate( + expiry_date=Max("session__login_session__expire_date"), + has_active_session=Case( + When(expiry_date__isnull=True, then=False), + When(expiry_date__gt=timezone.now(), then=True), + default=False, + output_field=BooleanField(), + ), + ) diff --git a/apps/codecov-api/api/shared/pagination.py b/apps/codecov-api/api/shared/pagination.py new file mode 100644 index 0000000000..64ada38b1a --- /dev/null +++ b/apps/codecov-api/api/shared/pagination.py @@ -0,0 +1,37 @@ +from rest_framework.pagination import CursorPagination, PageNumberPagination + + +class CodecovCursorPagination(CursorPagination): + page_size_query_param = "page_size" + + +class StandardPageNumberPagination(PageNumberPagination): + page_size_query_param = "page_size" + + def get_paginated_response(self, data): + response = super(StandardPageNumberPagination, self).get_paginated_response( + data + ) + response.data["total_pages"] = self.page.paginator.num_pages + return response + + +class PaginationMixin: + """ + Allows dynamicly switching between the default page number based pagination (above) + and cursor based pagination. + + Specifying a `cursor` query string parameter will switch to the cursor-based pagination. + """ + + @property + def paginator(self): + if not hasattr(self, "_paginator"): + if self.pagination_class is None: + self._paginator = None + else: + if "cursor" in self.request.query_params: + self._paginator = CodecovCursorPagination() + else: + self._paginator = self.pagination_class() + return self._paginator diff --git a/apps/codecov-api/api/shared/permissions.py b/apps/codecov-api/api/shared/permissions.py new file mode 100644 index 0000000000..3c6cd7a242 --- /dev/null +++ b/apps/codecov-api/api/shared/permissions.py @@ -0,0 +1,205 @@ +import logging +from typing import Any, Tuple + +from asgiref.sync import async_to_sync +from django.conf import settings +from django.http import Http404, HttpRequest +from rest_framework.permissions import ( + SAFE_METHODS, # ['GET', 'HEAD', 'OPTIONS'] + BasePermission, +) + +import services.self_hosted as self_hosted +from api.shared.mixins import InternalPermissionsMixin, SuperPermissionsMixin +from api.shared.repo.repository_accessors import RepoAccessors +from codecov_auth.models import Owner +from core.models import Repository +from services.activation import try_auto_activate +from services.decorators import torngit_safe +from services.repo_providers import get_generic_adapter_params, get_provider + +log = logging.getLogger(__name__) + + +class RepositoryPermissionsService: + @torngit_safe + def _fetch_provider_permissions( + self, owner: Owner, repo: Repository + ) -> Tuple[bool, bool]: + can_view, can_edit = RepoAccessors().get_repo_permissions(owner, repo) + + if can_view: + owner.permission = owner.permission or [] + owner.permission.append(repo.repoid) + owner.save(update_fields=["permission"]) + + return can_view, can_edit + + def has_read_permissions(self, owner: Owner, repo: Repository) -> bool: + return not repo.private or ( + owner is not None + and ( + repo.author.ownerid == owner.ownerid + or owner.permission + and repo.repoid in owner.permission + or self._fetch_provider_permissions(owner, repo)[0] + ) + ) + + def has_write_permissions(self, user: Owner, repo: Repository) -> bool: + return user.is_authenticated and ( + repo.author.ownerid == user.ownerid + or self._fetch_provider_permissions(user, repo)[1] + ) + + def user_is_activated(self, current_owner: Owner, owner: Owner) -> bool: + if not current_owner or not owner: + return False + if current_owner.ownerid == owner.ownerid: + return True + if owner.has_legacy_plan: + return True + if ( + current_owner.organizations is None + or owner.ownerid not in current_owner.organizations + ): + return False + if ( + owner.plan_activated_users + and current_owner.ownerid in owner.plan_activated_users + ): + return True + return try_auto_activate(owner, current_owner) + + +class RepositoryArtifactPermissions(BasePermission): + """ + Permissions class for artifacts of a repository, eg commits, branches, + pulls, comparisons, etc. Requires that the view has a '.repo' + property that returns the repo being worked on. + """ + + permissions_service = RepositoryPermissionsService() + message = ( + "Permission denied: some possible reasons for this are (1) the " + "user doesn't have permission to view the specific resource, " + "(2) the organization has a per-user plan or (3) the user is " + "trying to view a private repo but is not activated." + ) + + def has_permission(self, request: HttpRequest, view: Any) -> bool: + if view.repo.private: + user_activated_permissions = ( + request.user.is_authenticated + and self.permissions_service.user_is_activated( + request.current_owner, view.owner + ) + ) + else: + user_activated_permissions = True + has_read_permissions = ( + request.method in SAFE_METHODS + and self.permissions_service.has_read_permissions( + request.current_owner, view.repo + ) + ) + if has_read_permissions and user_activated_permissions: + return True + if has_read_permissions and not user_activated_permissions: + # user that can access the repo; but are not activated + return False + raise Http404() + + +class SuperTokenPermissions(BasePermission, SuperPermissionsMixin): + def has_permission(self, request: HttpRequest, view: Any) -> bool: + return self.has_super_token_permissions(request) + + +class InternalTokenPermissions(BasePermission, InternalPermissionsMixin): + def has_permission(self, request: HttpRequest, view: Any) -> bool: + return self.has_internal_token_permissions(request) + + +class ChartPermissions(BasePermission): + permissions_service = RepositoryPermissionsService() + + def has_permission(self, request: HttpRequest, view: Any) -> bool: + log.info( + f"Coverage chart has repositories {view.repositories}", + extra=dict(user=request.current_owner), + ) + for repo in view.repositories: + # TODO: this can cause a provider-api request for every repo in the list, + # can we just rely on our stored read permissions? In fact, it seems like + # permissioning is built into api.internal.charts.filter.add_simple_filters + if not self.permissions_service.has_read_permissions( + request.current_owner, repo + ): + raise Http404 + return True + + +class UserIsAdminPermissions(BasePermission): + """ + Permissions class for asserting the user is an admin of the 'owner' + being queried. Requires that the view has a '.owner' property that + returns this owner. + """ + + def has_permission(self, request: HttpRequest, view: Any) -> bool: + if settings.IS_ENTERPRISE: + return request.user.is_authenticated and self_hosted.is_admin_owner( + request.current_owner + ) + else: + return ( + request.user.is_authenticated + and request.current_owner + and ( + view.owner.is_admin(request.current_owner) + or self._is_admin_on_provider(request.current_owner, view.owner) + ) + ) + + @torngit_safe + def _is_admin_on_provider(self, user: Owner, owner: Owner) -> bool: + torngit_provider_adapter = get_provider( + owner.service, + { + **get_generic_adapter_params(user, owner.service), + **{ + "owner": { + "username": owner.username, + "service_id": owner.service_id, + } + }, + }, + ) + + return async_to_sync(torngit_provider_adapter.get_is_admin)( + user={"username": user.username, "service_id": user.service_id} + ) + + +class MemberOfOrgPermissions(BasePermission): + """ + Permissions class for asserting the user is member of the owner. + Requires that the view has a '.owner' property that returns this owner. + """ + + def has_permission(self, request: HttpRequest, view: Any) -> bool: + if not request.user.is_authenticated: + return False + + current_owner = request.current_owner + if not current_owner: + return False + + owner = view.owner + if current_owner == owner: + return True + if owner.ownerid in (current_owner.organizations or []): + return True + else: + raise Http404("No Owner matches the given query.") diff --git a/apps/codecov-api/api/shared/pull/__init__.py b/apps/codecov-api/api/shared/pull/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/shared/pull/mixins.py b/apps/codecov-api/api/shared/pull/mixins.py new file mode 100644 index 0000000000..5f9cd29171 --- /dev/null +++ b/apps/codecov-api/api/shared/pull/mixins.py @@ -0,0 +1,43 @@ +from django.db.models import OuterRef, Subquery +from django.shortcuts import get_object_or_404 +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import filters, viewsets + +from api.shared.mixins import RepoPropertyMixin +from api.shared.permissions import RepositoryArtifactPermissions +from core.models import Commit + + +class PullViewSetMixin( + viewsets.GenericViewSet, + RepoPropertyMixin, +): + filter_backends = [DjangoFilterBackend, filters.OrderingFilter] + filterset_fields = ["state"] + ordering = ["-pullid"] + ordering_fields = ["pullid"] + permission_classes = [RepositoryArtifactPermissions] + lookup_field = "pullid" + + def get_object(self): + pullid = self.kwargs.get("pullid") + return get_object_or_404(self.get_queryset(), pullid=pullid) + + def get_queryset(self): + return self.repo.pull_requests.annotate( + base_totals=Subquery( + Commit.objects.filter( + commitid=OuterRef("compared_to"), repository=OuterRef("repository") + ).values("totals")[:1] + ), + head_totals=Subquery( + Commit.objects.filter( + commitid=OuterRef("head"), repository=OuterRef("repository") + ).values("totals")[:1] + ), + ci_passed=Subquery( + Commit.objects.filter( + commitid=OuterRef("head"), repository=OuterRef("repository") + ).values("ci_passed")[:1] + ), + ) diff --git a/apps/codecov-api/api/shared/repo/filter.py b/apps/codecov-api/api/shared/repo/filter.py new file mode 100644 index 0000000000..705c63ad2b --- /dev/null +++ b/apps/codecov-api/api/shared/repo/filter.py @@ -0,0 +1,49 @@ +from django_filters import BooleanFilter +from django_filters import rest_framework as django_filters + +from core.models import Repository + + +class StringListFilter(django_filters.Filter): + def __init__(self, query_param, *args, **kwargs): + super(StringListFilter, self).__init__(*args, **kwargs) + self.query_param = query_param + + def filter(self, qs, value): + try: + request = self.parent.request + except AttributeError: + return None + + values = request.GET.getlist(self.query_param) + if len(values) > 0: + return qs.filter(**{"%s__%s" % (self.field_name, self.lookup_expr): values}) + + return qs + + +class RepositoryFilters(django_filters.FilterSet): + """Filter for active repositories""" + + active = BooleanFilter( + field_name="active", + method="filter_active", + label="whether the repository has received an upload", + ) + + """Filter for getting multiple repositories by name""" + names = StringListFilter( + query_param="names", + field_name="name", + lookup_expr="in", + label="list of repository names", + ) + + def filter_active(self, queryset, name, value): + # The database currently stores 't' instead of 'true' for active repos, and nothing for inactive + # so if the query param active is set, we return repos with non-null value in active column + return queryset.filter(active=value) + + class Meta: + model = Repository + fields = ["active", "names"] diff --git a/apps/codecov-api/api/shared/repo/mixins.py b/apps/codecov-api/api/shared/repo/mixins.py new file mode 100644 index 0000000000..7a9388dab1 --- /dev/null +++ b/apps/codecov-api/api/shared/repo/mixins.py @@ -0,0 +1,96 @@ +import logging + +from django.http import Http404 +from rest_framework import viewsets +from rest_framework.exceptions import PermissionDenied +from rest_framework.permissions import SAFE_METHODS # ['GET', 'HEAD', 'OPTIONS'] + +from api.shared.mixins import OwnerPropertyMixin +from api.shared.permissions import RepositoryPermissionsService, UserIsAdminPermissions +from core.models import Repository +from services.decorators import torngit_safe + +from .repository_accessors import RepoAccessors + +log = logging.getLogger(__name__) + + +class RepositoryViewSetMixin( + OwnerPropertyMixin, + viewsets.GenericViewSet, +): + lookup_value_regex = r"[\w\.@\:\-~]+" + lookup_field = "repo_name" + accessors = RepoAccessors() + + def _assert_is_admin(self): + admin_permissions = UserIsAdminPermissions() + if not admin_permissions.has_permission(self.request, self): + raise PermissionDenied() + + def get_queryset(self): + return ( + Repository.objects.filter(author=self.owner) + .viewable_repos(self.request.current_owner) + .select_related("author") + ) + + @torngit_safe + def check_object_permissions(self, request, repo): + # Below is some hacking to avoid requesting permissions from API in certain scenarios. + if not request.user.is_authenticated and not repo.private: + # Unauthenticated users only have read-access to public repositories, + # so we avoid this API call here + self.can_view, self.can_edit = True, False + elif not request.user.is_authenticated and repo.private: + raise Http404() + else: + # If the user is authenticated, we can fetch permissions from the provider + # to determine write permissions. + self.can_view, self.can_edit = self.accessors.get_repo_permissions( + self.request.current_owner, repo + ) + + if repo.private and not RepositoryPermissionsService().user_is_activated( + self.request.current_owner, self.owner + ): + log.info( + "An inactive user attempted to access a repo page", + extra=dict( + user=self.request.current_owner.username, + owner=self.owner.username, + repo=repo.name, + ), + ) + raise PermissionDenied("User not activated") + if self.request.method not in SAFE_METHODS and not self.can_edit: + raise PermissionDenied() + if self.request.method == "DELETE": + self._assert_is_admin() + if not self.can_view: + raise Http404() + + @torngit_safe + def get_object(self): + # Get request args and try to find the repo in the DB + repo_name = self.kwargs.get("repo_name") + org_name = self.kwargs.get("owner_username") + service = self.kwargs.get("service") + + repo = self.accessors.get_repo_details( + user=self.request.current_owner, + repo_name=repo_name, + repo_owner_username=org_name, + repo_owner_service=service, + ) + + if repo is None: + repo = self.accessors.fetch_from_git_and_create_repo( + user=self.request.current_owner, + repo_name=repo_name, + repo_owner_username=org_name, + repo_owner_service=service, + ) + + self.check_object_permissions(self.request, repo) + return repo diff --git a/apps/codecov-api/api/shared/repo/repository_accessors.py b/apps/codecov-api/api/shared/repo/repository_accessors.py new file mode 100644 index 0000000000..0cee985c39 --- /dev/null +++ b/apps/codecov-api/api/shared/repo/repository_accessors.py @@ -0,0 +1,79 @@ +import logging + +import sentry_sdk +from asgiref.sync import async_to_sync +from django.core.exceptions import ObjectDoesNotExist +from django.utils import timezone + +from codecov_auth.models import Owner +from core.models import Repository +from services.repo_providers import RepoProviderService + +log = logging.getLogger(__name__) + + +class RepoAccessors: + """ + Easily mockable wrappers for running torngit coroutines. + """ + + def get_repo_permissions(self, user, repo): + """ + Returns repo permissions information from the provider + + :param repo_name: + :param org_name: + :return: + """ + if user == repo.author: + return True, True + return async_to_sync( + RepoProviderService().get_adapter(owner=user, repo=repo).get_authenticated + )() + + @sentry_sdk.trace + def get_repo_details( + self, user, repo_name, repo_owner_username, repo_owner_service + ): + """ + Returns repo from DB, if it exists. + """ + try: + return ( + Repository.objects.all() + .with_recent_coverage() + .get( + name=repo_name, + author__username=repo_owner_username, + author__service=repo_owner_service, + ) + ) + except ObjectDoesNotExist: + repo = None + return repo + + def fetch_from_git_and_create_repo( + self, user, repo_name, repo_owner_username, repo_owner_service + ): + """ + Fetch repository details for the provider and update the DB with new information. + """ + # Try to fetch the repo from the git provider using shared.torngit + adapter = RepoProviderService().get_by_name( + owner=user, + repo_name=repo_name, + repo_owner_username=repo_owner_username, + repo_owner_service=repo_owner_service, + ) + result = async_to_sync(adapter.get_repository)() + + owner, _ = Owner.objects.get_or_create( + service=repo_owner_service, + username=result["owner"]["username"], + service_id=result["owner"]["service_id"], + defaults={"createstamp": timezone.now()}, + ) + + return Repository.objects.get_or_create_from_git_repo( + git_repo=result["repo"], owner=owner + )[0] diff --git a/apps/codecov-api/api/shared/report/__init__.py b/apps/codecov-api/api/shared/report/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/api/shared/report/serializers.py b/apps/codecov-api/api/shared/report/serializers.py new file mode 100644 index 0000000000..e6fe212256 --- /dev/null +++ b/apps/codecov-api/api/shared/report/serializers.py @@ -0,0 +1,31 @@ +import math + +from rest_framework import serializers + +from services.path import Dir + + +class TreeSerializer(serializers.Serializer): + name = serializers.CharField() + full_path = serializers.CharField() + coverage = serializers.FloatField() + lines = serializers.IntegerField() + hits = serializers.IntegerField() + partials = serializers.IntegerField() + misses = serializers.IntegerField() + + def to_representation(self, instance): + depth = self.context.get("depth", 1) + max_depth = self.context.get("max_depth", math.inf) + res = super().to_representation(instance) + if isinstance(instance, Dir): + if depth < max_depth: + res["children"] = TreeSerializer( + instance.children, + many=True, + context={ + "depth": depth + 1, + "max_depth": max_depth, + }, + ).data + return res diff --git a/apps/codecov-api/api/shared/serializers.py b/apps/codecov-api/api/shared/serializers.py new file mode 100644 index 0000000000..4e3b0a1b37 --- /dev/null +++ b/apps/codecov-api/api/shared/serializers.py @@ -0,0 +1,45 @@ +from django.shortcuts import get_object_or_404 +from rest_framework import serializers +from rest_framework.exceptions import NotFound + +from core.models import Branch, Commit, Pull + + +class StringListField(serializers.ListField): + child = serializers.CharField() + + +class CommitRefQueryParamSerializer(serializers.Serializer): + base = serializers.CharField(required=True) + head = serializers.CharField(required=True) + + def _get_commit_or_branch(self, ref): + repo = self.context.get("repo") + commit = Commit.objects.filter(repository_id=repo.repoid, commitid=ref) + if commit.exists(): + return commit.get() + + branch = Branch.objects.filter(repository=repo, name=ref) + if branch.exists(): + head = Commit.objects.filter(repository=repo, commitid=branch.get().head) + if head.exists(): + return head.get() + raise NotFound( + f"Head commit '{branch.get().head}' for branch '{ref}' not found!" + ) + raise NotFound(f"Commit or branch '{ref}' not found!") + + def validate_base(self, base): + return self._get_commit_or_branch(base) + + def validate_head(self, head): + return self._get_commit_or_branch(head) + + +class PullIDQueryParamSerializer(serializers.Serializer): + pullid = serializers.CharField(required=True) + + def validate(self, obj): + repo = self.context.get("repo") + pull = get_object_or_404(Pull, pullid=obj.get("pullid"), repository=repo) + return {"pull": pull} diff --git a/apps/codecov-api/billing/__init__.py b/apps/codecov-api/billing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/billing/apps.py b/apps/codecov-api/billing/apps.py new file mode 100644 index 0000000000..729d58448d --- /dev/null +++ b/apps/codecov-api/billing/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class BillingConfig(AppConfig): + name = "billing" diff --git a/apps/codecov-api/billing/constants.py b/apps/codecov-api/billing/constants.py new file mode 100644 index 0000000000..bc2f5b9116 --- /dev/null +++ b/apps/codecov-api/billing/constants.py @@ -0,0 +1,28 @@ +class StripeHTTPHeaders: + """ + Header-strings associated with Stripe webhook events. + """ + + # https://stripe.com/docs/webhooks/signatures#verify-official-libraries + SIGNATURE = "HTTP_STRIPE_SIGNATURE" + + +class StripeWebhookEvents: + subscribed_events = ( + "checkout.session.completed", + "customer.created", + "customer.subscription.created", + "customer.subscription.updated", + "customer.subscription.deleted", + "customer.updated", + "invoice.payment_failed", + "invoice.payment_succeeded", + "payment_intent.succeeded", + "setup_intent.succeeded", + "subscription_schedule.created", + "subscription_schedule.released", + "subscription_schedule.updated", + ) + + +REMOVED_INVOICE_STATUSES = ["draft", "void"] diff --git a/apps/codecov-api/billing/helpers.py b/apps/codecov-api/billing/helpers.py new file mode 100644 index 0000000000..8bbef4df26 --- /dev/null +++ b/apps/codecov-api/billing/helpers.py @@ -0,0 +1,193 @@ +from django.conf import settings +from django.db.models import QuerySet +from shared.django_apps.codecov_auth.models import BillingRate +from shared.django_apps.codecov_auth.tests.factories import PlanFactory, TierFactory +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName, PlanPrice, TierName + +from codecov_auth.models import Owner, Plan + + +def on_enterprise_plan(owner: Owner) -> bool: + plan = Plan.objects.select_related("tier").get(name=owner.plan) + return settings.IS_ENTERPRISE or (plan.tier.tier_name == TierName.ENTERPRISE.value) + + +def get_all_admins_for_owners(owners: QuerySet[Owner]): + admin_ids = set() + for owner in owners: + if owner.admins: + admin_ids.update(owner.admins) + + # Add the owner's email as well - for user owners, admins is empty. + if owner.email: + admin_ids.add(owner.ownerid) + + admins: QuerySet[Owner] = Owner.objects.filter(pk__in=admin_ids) + return admins + + +def mock_all_plans_and_tiers(): + TierFactory(tier_name=TierName.BASIC.value) + + trial_tier = TierFactory(tier_name=TierName.TRIAL.value) + PlanFactory( + tier=trial_tier, + name=PlanName.TRIAL_PLAN_NAME.value, + paid_plan=False, + marketing_name="Developer", + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + stripe_id="plan_trial", + ) + + pro_tier = TierFactory(tier_name=TierName.PRO.value) + PlanFactory( + name=PlanName.CODECOV_PRO_MONTHLY.value, + tier=pro_tier, + marketing_name="Pro", + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + billing_rate=BillingRate.MONTHLY.value, + base_unit_price=PlanPrice.MONTHLY.value, + paid_plan=True, + stripe_id="plan_pro", + ) + PlanFactory( + name=PlanName.CODECOV_PRO_YEARLY.value, + tier=pro_tier, + marketing_name="Pro", + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + billing_rate=BillingRate.ANNUALLY.value, + base_unit_price=PlanPrice.YEARLY.value, + paid_plan=True, + stripe_id="plan_pro_yearly", + ) + + team_tier = TierFactory(tier_name=TierName.TEAM.value) + PlanFactory( + name=PlanName.TEAM_MONTHLY.value, + tier=team_tier, + marketing_name="Team", + benefits=[ + "Up to 10 users", + "Unlimited repositories", + "2500 private repo uploads", + "Patch coverage analysis", + ], + billing_rate=BillingRate.MONTHLY.value, + base_unit_price=PlanPrice.TEAM_MONTHLY.value, + monthly_uploads_limit=2500, + paid_plan=True, + stripe_id="plan_team_monthly", + ) + PlanFactory( + name=PlanName.TEAM_YEARLY.value, + tier=team_tier, + marketing_name="Team", + benefits=[ + "Up to 10 users", + "Unlimited repositories", + "2500 private repo uploads", + "Patch coverage analysis", + ], + billing_rate=BillingRate.ANNUALLY.value, + base_unit_price=PlanPrice.TEAM_YEARLY.value, + monthly_uploads_limit=2500, + paid_plan=True, + stripe_id="plan_team_yearly", + ) + + sentry_tier = TierFactory(tier_name=TierName.SENTRY.value) + PlanFactory( + name=PlanName.SENTRY_MONTHLY.value, + tier=sentry_tier, + marketing_name="Sentry Pro", + billing_rate=BillingRate.MONTHLY.value, + base_unit_price=PlanPrice.MONTHLY.value, + paid_plan=True, + benefits=[ + "Includes 5 seats", + "$12 per additional seat", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + stripe_id="plan_sentry_monthly", + ) + PlanFactory( + name=PlanName.SENTRY_YEARLY.value, + tier=sentry_tier, + marketing_name="Sentry Pro", + billing_rate=BillingRate.ANNUALLY.value, + base_unit_price=PlanPrice.YEARLY.value, + paid_plan=True, + benefits=[ + "Includes 5 seats", + "$10 per additional seat", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + stripe_id="plan_sentry_yearly", + ) + + enterprise_tier = TierFactory(tier_name=TierName.ENTERPRISE.value) + PlanFactory( + name=PlanName.ENTERPRISE_CLOUD_MONTHLY.value, + tier=enterprise_tier, + marketing_name="Enterprise Cloud", + billing_rate=BillingRate.MONTHLY.value, + base_unit_price=PlanPrice.MONTHLY.value, + paid_plan=True, + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + stripe_id="plan_enterprise_cloud_monthly", + ) + PlanFactory( + name=PlanName.ENTERPRISE_CLOUD_YEARLY.value, + tier=enterprise_tier, + marketing_name="Enterprise Cloud", + billing_rate=BillingRate.ANNUALLY.value, + base_unit_price=PlanPrice.YEARLY.value, + paid_plan=True, + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + stripe_id="plan_enterprise_cloud_yearly", + ) + + PlanFactory( + name=DEFAULT_FREE_PLAN, + tier=team_tier, + marketing_name="Developer", + billing_rate=None, + base_unit_price=0, + paid_plan=False, + monthly_uploads_limit=250, + benefits=[ + "Up to 1 user", + "Unlimited public repositories", + "Unlimited private repositories", + ], + stripe_id="plan_default_free", + ) diff --git a/apps/codecov-api/billing/migrations/0001_initial.py b/apps/codecov-api/billing/migrations/0001_initial.py new file mode 100644 index 0000000000..d25438f6bc --- /dev/null +++ b/apps/codecov-api/billing/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# Generated by Django 3.1.13 on 2021-11-03 18:21 + +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Account", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("stripe_customer_id", models.TextField(null=True)), + ("stripe_subscription_id", models.TextField(null=True)), + ("plan", models.TextField(default="users-free")), + ( + "plan_provider", + models.TextField(choices=[("github", "Github")], null=True), + ), + ("max_activated_user_count", models.SmallIntegerField(default=5)), + ("should_auto_activate_users", models.BooleanField(default=True)), + ], + options={"abstract": False}, + ) + ] diff --git a/apps/codecov-api/billing/migrations/0002_auto_20220118_1232.py b/apps/codecov-api/billing/migrations/0002_auto_20220118_1232.py new file mode 100644 index 0000000000..2d8d72a78a --- /dev/null +++ b/apps/codecov-api/billing/migrations/0002_auto_20220118_1232.py @@ -0,0 +1,21 @@ +# Generated by Django 3.1.13 on 2022-01-18 12:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [("billing", "0001_initial")] + + operations = [ + migrations.RunSQL( + "ALTER TYPE notifications ADD VALUE IF NOT exists 'checks_patch';" + ), + migrations.RunSQL( + "ALTER TYPE notifications ADD VALUE IF NOT exists 'checks_project';" + ), + migrations.RunSQL( + "ALTER TYPE notifications ADD VALUE IF NOT exists 'checks_changes';" + ), + ] diff --git a/apps/codecov-api/billing/migrations/0003_delete_account.py b/apps/codecov-api/billing/migrations/0003_delete_account.py new file mode 100644 index 0000000000..ce40e1bbbf --- /dev/null +++ b/apps/codecov-api/billing/migrations/0003_delete_account.py @@ -0,0 +1,15 @@ +# Generated by Django 4.2.11 on 2024-07-24 00:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("billing", "0002_auto_20220118_1232"), + ] + + operations = [ + migrations.DeleteModel( + name="Account", + ), + ] diff --git a/apps/codecov-api/billing/migrations/__init__.py b/apps/codecov-api/billing/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/billing/tests/__init__.py b/apps/codecov-api/billing/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/billing/tests/test_helpers.py b/apps/codecov-api/billing/tests/test_helpers.py new file mode 100644 index 0000000000..f862b782e1 --- /dev/null +++ b/apps/codecov-api/billing/tests/test_helpers.py @@ -0,0 +1,31 @@ +from django.test import TestCase, override_settings +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory +from shared.plan.constants import PlanName + +from billing.helpers import mock_all_plans_and_tiers, on_enterprise_plan + + +class HelpersTestCase(TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + + @override_settings(IS_ENTERPRISE=True) + def test_on_enterprise_plan_on_prem(self): + owner = OwnerFactory() + assert on_enterprise_plan(owner) == True + + def test_on_enterprise_plan_enterprise_cloud(self): + plan_names = [ + PlanName.ENTERPRISE_CLOUD_MONTHLY.value, + PlanName.ENTERPRISE_CLOUD_YEARLY.value, + ] + + for plan in plan_names: + owner = OwnerFactory(plan=plan) + assert on_enterprise_plan(owner) == True + + def test_on_enterprise_plan_cloud(self): + owner = OwnerFactory() + assert on_enterprise_plan(owner) == False diff --git a/apps/codecov-api/billing/tests/test_views.py b/apps/codecov-api/billing/tests/test_views.py new file mode 100644 index 0000000000..b45d9cc223 --- /dev/null +++ b/apps/codecov-api/billing/tests/test_views.py @@ -0,0 +1,1825 @@ +import time +from datetime import datetime +from unittest.mock import Mock, call, patch + +import stripe +from django.conf import settings +from freezegun import freeze_time +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APIRequestFactory, APITestCase +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName + +from billing.helpers import mock_all_plans_and_tiers +from billing.views import StripeWebhookHandler +from codecov_auth.models import Plan + +from ..constants import StripeHTTPHeaders + + +class MockSubscriptionPlan(object): + def __init__(self, params): + self.id = params["new_plan"] + + +class MockSubscription(object): + def __init__(self, owner, params): + self.metadata = {"obo_organization": owner.ownerid, "obo": 15} + self.plan = MockSubscriptionPlan(params) + self.quantity = params["new_quantity"] + self.customer = "cus_123" + self.id = params["subscription_id"] + self.items = { + "data": [ + { + "quantity": params["new_quantity"], + "plan": {"id": params["new_plan"]}, + } + ] + } + + def __getitem__(self, key): + return getattr(self, key) + + +class MockCard(object): + def __init__(self): + self.brand = "visa" + self.last4 = "1234" + + def __getitem__(self, key): + return getattr(self, key) + + def get(self, key, default=None): + return getattr(self, key, default) + + +class MockPaymentMethod(object): + def __init__(self, noCard=False): + if noCard: + self.card = None + return + + self.card = MockCard() + + def __getitem__(self, key): + return getattr(self, key) + + def get(self, key, default=None): + return getattr(self, key, default) + + +class MockPaymentIntent(object): + def __init__(self, noCard=False): + self.payment_method = MockPaymentMethod(noCard) + self.status = "succeeded" + + def __getitem__(self, key): + return getattr(self, key) + + def get(self, key, default=None): + return getattr(self, key, default) + + +class StripeWebhookHandlerTests(APITestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + + def setUp(self): + self.owner = OwnerFactory( + stripe_customer_id="cus_123", + stripe_subscription_id="sub_123", + ) + + # Creates a second owner that shares billing details with self.owner. + # This is used to test the case where owners are manually set to share a + # subscription in Stripe. + def add_second_owner(self): + self.other_owner = OwnerFactory( + stripe_customer_id="cus_123", + stripe_subscription_id="sub_123", + ) + + def _send_event(self, payload, errorSig=None): + timestamp = time.time_ns() + + request = APIRequestFactory().post( + reverse("stripe-webhook"), data=payload, format="json" + ) + + return self.client.post( + reverse("stripe-webhook"), + **{ + StripeHTTPHeaders.SIGNATURE: errorSig + or "t={},v1={}".format( + timestamp, + stripe.WebhookSignature._compute_signature( + "{}.{}".format(timestamp, request.body.decode("utf-8")), + settings.STRIPE_ENDPOINT_SECRET, + ), + ) + }, + data=payload, + format="json", + ) + + def test_invalid_event_signature(self): + response = self._send_event( + payload={ + "type": "blah", + "data": {}, + }, + errorSig="lol", + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_invoice_payment_succeeded_sets_owner_delinquent_false(self): + self.owner.delinquent = True + self.owner.save() + + response = self._send_event( + payload={ + "type": "invoice.payment_succeeded", + "data": { + "object": { + "customer": self.owner.stripe_customer_id, + "subscription": self.owner.stripe_subscription_id, + "total": 24000, + "hosted_invoice_url": "https://stripe.com", + } + }, + } + ) + + self.owner.refresh_from_db() + assert response.status_code == status.HTTP_204_NO_CONTENT + assert self.owner.delinquent is False + + def test_invoice_payment_succeeded_sets_multiple_owners_delinquent_false(self): + self.add_second_owner() + self.owner.delinquent = True + self.owner.save() + self.other_owner.delinquent = True + self.other_owner.save() + + response = self._send_event( + payload={ + "type": "invoice.payment_succeeded", + "data": { + "object": { + "customer": self.owner.stripe_customer_id, + "subscription": self.owner.stripe_subscription_id, + "total": 24000, + "hosted_invoice_url": "https://stripe.com", + } + }, + } + ) + + self.owner.refresh_from_db() + self.other_owner.refresh_from_db() + assert response.status_code == status.HTTP_204_NO_CONTENT + assert self.owner.delinquent is False + assert self.other_owner.delinquent is False + + @patch("services.task.TaskService.send_email") + def test_invoice_payment_succeeded_emails_only_emails_delinquents( + self, + mocked_send_email, + ): + self.add_second_owner() + self.owner.delinquent = False + self.owner.save() + + response = self._send_event( + payload={ + "type": "invoice.payment_succeeded", + "data": { + "object": { + "customer": self.owner.stripe_customer_id, + "subscription": self.owner.stripe_subscription_id, + "total": 24000, + "hosted_invoice_url": "https://stripe.com", + } + }, + } + ) + + self.owner.refresh_from_db() + self.other_owner.refresh_from_db() + assert response.status_code == status.HTTP_204_NO_CONTENT + assert self.owner.delinquent is False + + mocked_send_email.assert_not_called() + + @patch("services.task.TaskService.send_email") + def test_invoice_payment_succeeded_emails_delinquents(self, mocked_send_email): + non_admin = OwnerFactory(email="non-admin@codecov.io") + admin_1 = OwnerFactory(email="admin1@codecov.io") + admin_2 = OwnerFactory(email="admin2@codecov.io") + self.owner.admins = [admin_1.ownerid, admin_2.ownerid] + self.owner.plan_activated_users = [non_admin.ownerid] + self.owner.email = "owner@codecov.io" + self.owner.delinquent = True + self.owner.save() + self.add_second_owner() + self.other_owner.delinquent = False + self.other_owner.save() + + response = self._send_event( + payload={ + "type": "invoice.payment_succeeded", + "data": { + "object": { + "customer": self.owner.stripe_customer_id, + "subscription": self.owner.stripe_subscription_id, + "total": 24000, + "hosted_invoice_url": "https://stripe.com", + } + }, + } + ) + + self.owner.refresh_from_db() + self.other_owner.refresh_from_db() + assert response.status_code == status.HTTP_204_NO_CONTENT + assert self.owner.delinquent is False + assert self.other_owner.delinquent is False + + expected_calls = [ + call( + to_addr=self.owner.email, + subject="You're all set", + template_name="success-after-failed-payment", + amount=240, + cta_link="https://stripe.com", + date=datetime.now().strftime("%B %-d, %Y"), + ), + call( + to_addr=admin_1.email, + subject="You're all set", + template_name="success-after-failed-payment", + amount=240, + cta_link="https://stripe.com", + date=datetime.now().strftime("%B %-d, %Y"), + ), + call( + to_addr=admin_2.email, + subject="You're all set", + template_name="success-after-failed-payment", + amount=240, + cta_link="https://stripe.com", + date=datetime.now().strftime("%B %-d, %Y"), + ), + ] + + mocked_send_email.assert_has_calls(expected_calls) + + @patch("services.billing.stripe.PaymentIntent.retrieve") + def test_invoice_payment_failed_skips_delinquency_if_payment_intent_requires_action( + self, retrieve_paymentintent_mock + ): + self.owner.delinquent = False + self.owner.save() + + retrieve_paymentintent_mock.return_value = stripe.PaymentIntent.construct_from( + { + "status": "requires_action", + "next_action": {"type": "verify_with_microdeposits"}, + }, + "payment_intent_asdf", + ) + + response = self._send_event( + payload={ + "type": "invoice.payment_failed", + "data": { + "object": { + "customer": self.owner.stripe_customer_id, + "subscription": self.owner.stripe_subscription_id, + "total": 24000, + "hosted_invoice_url": "https://stripe.com", + "payment_intent": "payment_intent_asdf", + "default_payment_method": None, + } + }, + } + ) + + self.owner.refresh_from_db() + assert response.status_code == status.HTTP_204_NO_CONTENT + assert self.owner.delinquent is False + retrieve_paymentintent_mock.assert_called_once_with("payment_intent_asdf") + + @patch("services.billing.stripe.PaymentIntent.retrieve") + def test_invoice_payment_failed_sets_owner_delinquent_true( + self, retrieve_paymentintent_mock + ): + self.owner.delinquent = False + self.owner.save() + + retrieve_paymentintent_mock.return_value = stripe.PaymentIntent.construct_from( + { + "status": "requires_action", + "next_action": {"type": "verify_with_microdeposits"}, + }, + "payment_intent_asdf", + ) + + response = self._send_event( + payload={ + "type": "invoice.payment_failed", + "data": { + "object": { + "customer": self.owner.stripe_customer_id, + "subscription": self.owner.stripe_subscription_id, + "total": 24000, + "hosted_invoice_url": "https://stripe.com", + "payment_intent": "payment_intent_asdf", + "default_payment_method": {}, + } + }, + } + ) + + self.owner.refresh_from_db() + assert response.status_code == status.HTTP_204_NO_CONTENT + assert self.owner.delinquent is True + + @patch("services.billing.stripe.PaymentIntent.retrieve") + def test_invoice_payment_failed_sets_multiple_owners_delinquent_true( + self, retrieve_paymentintent_mock + ): + self.add_second_owner() + self.owner.delinquent = False + self.owner.save() + self.other_owner.delinquent = False + self.other_owner.save() + + retrieve_paymentintent_mock.return_value = MockPaymentIntent() + + response = self._send_event( + payload={ + "type": "invoice.payment_failed", + "data": { + "object": { + "customer": self.owner.stripe_customer_id, + "subscription": self.owner.stripe_subscription_id, + "total": 24000, + "hosted_invoice_url": "https://stripe.com", + "payment_intent": "payment_intent_asdf", + "default_payment_method": {}, + } + }, + } + ) + + self.owner.refresh_from_db() + self.other_owner.refresh_from_db() + assert response.status_code == status.HTTP_204_NO_CONTENT + assert self.owner.delinquent is True + assert self.other_owner.delinquent is True + + @patch("services.task.TaskService.send_email") + @patch("services.billing.stripe.PaymentIntent.retrieve") + def test_invoice_payment_failed_sends_email_to_admins( + self, + retrieve_paymentintent_mock, + mocked_send_email, + ): + non_admin = OwnerFactory(email="non-admin@codecov.io") + admin_1 = OwnerFactory(email="admin1@codecov.io") + admin_2 = OwnerFactory(email="admin2@codecov.io") + self.owner.admins = [admin_1.ownerid, admin_2.ownerid] + self.owner.plan_activated_users = [non_admin.ownerid] + self.owner.email = "owner@codecov.io" + self.owner.save() + + retrieve_paymentintent_mock.return_value = MockPaymentIntent() + + response = self._send_event( + payload={ + "type": "invoice.payment_failed", + "data": { + "object": { + "customer": self.owner.stripe_customer_id, + "subscription": self.owner.stripe_subscription_id, + "total": 24000, + "hosted_invoice_url": "https://stripe.com", + "payment_intent": "payment_intent_asdf", + "default_payment_method": {}, + } + }, + } + ) + + self.owner.refresh_from_db() + assert response.status_code == status.HTTP_204_NO_CONTENT + assert self.owner.delinquent is True + + expected_calls = [ + call( + to_addr=self.owner.email, + subject="Your Codecov payment failed", + template_name="failed-payment", + name=self.owner.username, + amount=240, + card_type="visa", + last_four="1234", + cta_link="https://stripe.com", + date=datetime.now().strftime("%B %-d, %Y"), + ), + call( + to_addr=admin_1.email, + subject="Your Codecov payment failed", + template_name="failed-payment", + name=admin_1.username, + amount=240, + card_type="visa", + last_four="1234", + cta_link="https://stripe.com", + date=datetime.now().strftime("%B %-d, %Y"), + ), + call( + to_addr=admin_2.email, + subject="Your Codecov payment failed", + template_name="failed-payment", + name=admin_2.username, + amount=240, + card_type="visa", + last_four="1234", + cta_link="https://stripe.com", + date=datetime.now().strftime("%B %-d, %Y"), + ), + ] + + mocked_send_email.assert_has_calls(expected_calls) + + @patch("services.task.TaskService.send_email") + @patch("services.billing.stripe.PaymentIntent.retrieve") + def test_invoice_payment_failed_sends_email_to_admins_no_card( + self, + retrieve_paymentintent_mock, + mocked_send_email, + ): + non_admin = OwnerFactory(email="non-admin@codecov.io") + admin_1 = OwnerFactory(email="admin1@codecov.io") + admin_2 = OwnerFactory(email="admin2@codecov.io") + self.owner.admins = [admin_1.ownerid, admin_2.ownerid] + self.owner.plan_activated_users = [non_admin.ownerid] + self.owner.email = "owner@codecov.io" + self.owner.save() + + retrieve_paymentintent_mock.return_value = MockPaymentIntent(noCard=True) + + response = self._send_event( + payload={ + "type": "invoice.payment_failed", + "data": { + "object": { + "customer": self.owner.stripe_customer_id, + "subscription": self.owner.stripe_subscription_id, + "default_payment_method": None, + "total": 24000, + "hosted_invoice_url": "https://stripe.com", + "payment_intent": { + "id": "payment_intent_asdf", + "status": "succeeded", + }, + } + }, + } + ) + + self.owner.refresh_from_db() + assert response.status_code == status.HTTP_204_NO_CONTENT + assert self.owner.delinquent is True + + expected_calls = [ + call( + to_addr=self.owner.email, + subject="Your Codecov payment failed", + template_name="failed-payment", + name=self.owner.username, + amount=240, + card_type=None, + last_four=None, + cta_link="https://stripe.com", + date=datetime.now().strftime("%B %-d, %Y"), + ), + call( + to_addr=admin_1.email, + subject="Your Codecov payment failed", + template_name="failed-payment", + name=admin_1.username, + amount=240, + card_type=None, + last_four=None, + cta_link="https://stripe.com", + date=datetime.now().strftime("%B %-d, %Y"), + ), + call( + to_addr=admin_2.email, + subject="Your Codecov payment failed", + template_name="failed-payment", + name=admin_2.username, + amount=240, + card_type=None, + last_four=None, + cta_link="https://stripe.com", + date=datetime.now().strftime("%B %-d, %Y"), + ), + ] + mocked_send_email.assert_has_calls(expected_calls) + + def test_customer_subscription_deleted_sets_plan_to_free(self): + self.owner.plan = PlanName.CODECOV_PRO_YEARLY.value + self.owner.plan_user_count = 20 + self.owner.save() + + self._send_event( + payload={ + "type": "customer.subscription.deleted", + "data": { + "object": { + "id": self.owner.stripe_subscription_id, + "customer": self.owner.stripe_customer_id, + "plan": {"name": self.owner.plan}, + "status": "active", + } + }, + } + ) + self.owner.refresh_from_db() + + assert self.owner.plan == DEFAULT_FREE_PLAN + assert self.owner.plan_user_count == 1 + assert self.owner.plan_activated_users is None + assert self.owner.stripe_subscription_id is None + + def test_customer_subscription_deleted_sets_plan_to_free_mutliple_owner(self): + self.add_second_owner() + self.owner.plan = PlanName.CODECOV_PRO_YEARLY.value + self.owner.plan_user_count = 20 + self.owner.save() + self.other_owner.plan = PlanName.CODECOV_PRO_YEARLY.value + self.other_owner.plan_user_count = 20 + self.other_owner.save() + + self._send_event( + payload={ + "type": "customer.subscription.deleted", + "data": { + "object": { + "id": self.owner.stripe_subscription_id, + "customer": self.owner.stripe_customer_id, + "plan": {"name": self.owner.plan}, + "status": "active", + } + }, + } + ) + self.owner.refresh_from_db() + self.other_owner.refresh_from_db() + + assert self.owner.plan == DEFAULT_FREE_PLAN + assert self.owner.plan_user_count == 1 + assert self.owner.plan_activated_users is None + assert self.owner.stripe_subscription_id is None + + assert self.other_owner.plan == DEFAULT_FREE_PLAN + assert self.other_owner.plan_user_count == 1 + assert self.other_owner.plan_activated_users is None + assert self.other_owner.stripe_subscription_id is None + + def test_customer_subscription_deleted_deactivates_all_repos(self): + RepositoryFactory(author=self.owner, activated=True, active=True) + RepositoryFactory(author=self.owner, activated=True, active=True) + RepositoryFactory(author=self.owner, activated=True, active=True) + + assert ( + self.owner.repository_set.filter(activated=True, active=True).count() == 3 + ) + + self._send_event( + payload={ + "type": "customer.subscription.deleted", + "data": { + "object": { + "id": self.owner.stripe_subscription_id, + "customer": self.owner.stripe_customer_id, + "plan": {"name": PlanName.CODECOV_PRO_MONTHLY.value}, + "status": "active", + } + }, + } + ) + + assert ( + self.owner.repository_set.filter(activated=False, active=False).count() == 3 + ) + + def test_customer_subscription_deleted_deactivates_all_repos_multiple_owner(self): + self.add_second_owner() + RepositoryFactory(author=self.owner, activated=True, active=True) + RepositoryFactory(author=self.owner, activated=True, active=True) + RepositoryFactory(author=self.owner, activated=True, active=True) + RepositoryFactory(author=self.other_owner, activated=True, active=True) + RepositoryFactory(author=self.other_owner, activated=True, active=True) + RepositoryFactory(author=self.other_owner, activated=True, active=True) + + self.owner.refresh_from_db() + self.other_owner.refresh_from_db() + + assert ( + self.owner.repository_set.filter(activated=True, active=True).count() == 3 + ) + assert ( + self.other_owner.repository_set.filter(activated=True, active=True).count() + == 3 + ) + + self._send_event( + payload={ + "type": "customer.subscription.deleted", + "data": { + "object": { + "id": self.owner.stripe_subscription_id, + "customer": self.owner.stripe_customer_id, + "plan": {"name": PlanName.CODECOV_PRO_MONTHLY.value}, + "status": "active", + } + }, + } + ) + + self.owner.refresh_from_db() + self.other_owner.refresh_from_db() + + assert ( + self.owner.repository_set.filter(activated=False, active=False).count() == 3 + ) + assert ( + self.other_owner.repository_set.filter( + activated=False, active=False + ).count() + == 3 + ) + + @patch("logging.Logger.info") + def test_customer_subscription_deleted_no_customer(self, log_info_mock): + self.owner.plan = PlanName.CODECOV_PRO_MONTHLY.value + self.owner.plan_user_count = 20 + self.owner.save() + + self._send_event( + payload={ + "type": "customer.subscription.deleted", + "data": { + "object": { + "id": "HUH", + "customer": "nah", + "plan": {"name": self.owner.plan}, + "status": "active", + } + }, + } + ) + + log_info_mock.assert_called_with( + "Customer Subscription Deleted - Couldn't find owner, subscription likely already deleted", + extra={ + "stripe_subscription_id": "HUH", + "stripe_customer_id": "nah", + }, + ) + + def test_customer_created_logs_and_doesnt_crash(self): + self._send_event( + payload={ + "type": "customer.created", + "data": {"object": {"id": "FOEKDCDEQ", "email": "test@email.com"}}, + } + ) + + @patch("billing.views.StripeWebhookHandler._has_unverified_initial_payment_method") + def test_customer_subscription_created_early_returns_if_unverified_payment( + self, mock_has_unverified + ): + mock_has_unverified.return_value = True + self.owner.stripe_subscription_id = None + self.owner.stripe_customer_id = None + self.owner.plan = "users-basic" + self.owner.save() + + response = self._send_event( + payload={ + "type": "customer.subscription.created", + "data": { + "object": { + "id": "sub_123", + "customer": "cus_123", + "plan": {"id": "plan_pro_yearly"}, + "metadata": {"obo_organization": self.owner.ownerid}, + "quantity": 20, + } + }, + } + ) + + self.owner.refresh_from_db() + assert response.status_code == status.HTTP_204_NO_CONTENT + # Subscription and customer IDs should be set + assert self.owner.stripe_subscription_id == "sub_123" + assert self.owner.stripe_customer_id == "cus_123" + # But plan should not be updated since payment is unverified + assert self.owner.plan == "users-basic" + mock_has_unverified.assert_called_once() + + def test_customer_subscription_created_does_nothing_if_no_plan_id(self): + self.owner.stripe_subscription_id = None + self.owner.stripe_customer_id = None + self.owner.save() + + self._send_event( + payload={ + "type": "customer.subscription.created", + "data": { + "object": { + "id": "FOEKDCDEQ", + "customer": "sdo050493", + "plan": {"id": None}, + "metadata": {"obo_organization": self.owner.ownerid}, + "quantity": 20, + } + }, + } + ) + + self.owner.refresh_from_db() + assert self.owner.stripe_subscription_id is None + assert self.owner.stripe_customer_id is None + + def test_customer_subscription_created_does_nothing_if_plan_not_paid_user_plan( + self, + ): + self.owner.stripe_subscription_id = None + self.owner.stripe_customer_id = None + self.owner.save() + + self._send_event( + payload={ + "type": "customer.subscription.created", + "data": { + "object": { + "id": "FOEKDCDEQ", + "customer": "sdo050493", + "plan": {"id": "plan_free"}, + "metadata": {"obo_organization": self.owner.ownerid}, + "quantity": 20, + } + }, + } + ) + + self.owner.refresh_from_db() + assert self.owner.stripe_subscription_id is None + assert self.owner.stripe_customer_id is None + + @patch("billing.views.StripeWebhookHandler._has_unverified_initial_payment_method") + def test_customer_subscription_created_sets_plan_info( + self, has_unverified_initial_payment_method_mock + ): + has_unverified_initial_payment_method_mock.return_value = False + self.owner.stripe_subscription_id = None + self.owner.stripe_customer_id = None + self.owner.save() + + stripe_subscription_id = "FOEKDCDEQ" + stripe_customer_id = "sdo050493" + plan_name = PlanName.CODECOV_PRO_YEARLY.value + quantity = 20 + + self._send_event( + payload={ + "type": "customer.subscription.created", + "data": { + "object": { + "id": stripe_subscription_id, + "customer": stripe_customer_id, + "plan": {"id": "plan_pro_yearly"}, + "metadata": {"obo_organization": self.owner.ownerid}, + "quantity": quantity, + "status": "active", + } + }, + } + ) + + self.owner.refresh_from_db() + assert self.owner.stripe_subscription_id == stripe_subscription_id + assert self.owner.stripe_customer_id == stripe_customer_id + assert self.owner.plan_user_count == quantity + assert self.owner.plan_auto_activate is True + assert self.owner.plan == plan_name + + @freeze_time("2023-06-19") + @patch("billing.views.StripeWebhookHandler._has_unverified_initial_payment_method") + @patch("shared.plan.service.PlanService.expire_trial_when_upgrading") + @patch("services.billing.stripe.PaymentMethod.attach") + @patch("services.billing.stripe.Customer.modify") + def test_customer_subscription_created_can_trigger_trial_expiration( + self, + c_mock, + pm_mock, + expire_trial_when_upgrading_mock, + has_unverified_initial_payment_method_mock, + ): + has_unverified_initial_payment_method_mock.return_value = False + stripe_subscription_id = "FOEKDCDEQ" + stripe_customer_id = "sdo050493" + quantity = 20 + + self._send_event( + payload={ + "type": "customer.subscription.created", + "data": { + "object": { + "id": stripe_subscription_id, + "customer": stripe_customer_id, + "plan": {"id": "plan_pro_yearly"}, + "metadata": {"obo_organization": self.owner.ownerid}, + "quantity": quantity, + "default_payment_method": "blabla", + } + }, + } + ) + + expire_trial_when_upgrading_mock.assert_called_once() + + @patch("billing.views.StripeWebhookHandler._has_unverified_initial_payment_method") + @patch("services.billing.stripe.PaymentMethod.attach") + @patch("services.billing.stripe.Customer.modify") + def test_customer_subscription_updated_does_not_change_subscription_if_not_paid_user_plan( + self, + c_mock, + pm_mock, + has_unverified_initial_payment_method_mock, + ): + has_unverified_initial_payment_method_mock.return_value = False + self.owner.plan = DEFAULT_FREE_PLAN + self.owner.plan_user_count = 0 + self.owner.plan_auto_activate = False + self.owner.save() + + self._send_event( + payload={ + "type": "customer.subscription.updated", + "data": { + "object": { + "id": self.owner.stripe_subscription_id, + "customer": self.owner.stripe_customer_id, + "plan": {"id": "plan_free"}, + "metadata": {"obo_organization": self.owner.ownerid}, + "quantity": 20, + "status": "active", + "schedule": None, + "default_payment_method": "pm_1LhiRsGlVGuVgOrkQguJXdeV", + } + }, + } + ) + + self.owner.refresh_from_db() + assert self.owner.plan == DEFAULT_FREE_PLAN + assert self.owner.plan_user_count == 0 + assert self.owner.plan_auto_activate == False + pm_mock.assert_called_once_with( + "pm_1LhiRsGlVGuVgOrkQguJXdeV", customer=self.owner.stripe_customer_id + ) + c_mock.assert_called_once_with( + self.owner.stripe_customer_id, + invoice_settings={"default_payment_method": "pm_1LhiRsGlVGuVgOrkQguJXdeV"}, + ) + + @patch("billing.views.StripeWebhookHandler._has_unverified_initial_payment_method") + @patch("logging.Logger.info") + @patch("services.billing.stripe.PaymentMethod.attach") + @patch("services.billing.stripe.Customer.modify") + def test_customer_subscription_updated_does_not_change_subscription_if_there_is_a_schedule( + self, + c_mock, + pm_mock, + log_info_mock, + has_unverified_initial_payment_method_mock, + ): + has_unverified_initial_payment_method_mock.return_value = False + self.owner.plan = "users-pr-inappy" + self.owner.plan_user_count = 10 + self.owner.plan_auto_activate = False + self.owner.save() + + self._send_event( + payload={ + "type": "customer.subscription.updated", + "data": { + "object": { + "id": self.owner.stripe_subscription_id, + "customer": self.owner.stripe_customer_id, + "plan": {"id": "plan_pro_yearly"}, + "metadata": {"obo_organization": self.owner.ownerid}, + "quantity": 20, + "status": "active", + "schedule": "sub_sched_1K8xfkGlVGuVgOrkxvroyZdH", + "default_payment_method": "pm_1LhiRsGlVGuVgOrkQguJXdeV", + } + }, + } + ) + + self.owner.refresh_from_db() + assert self.owner.plan == "users-pr-inappy" + assert self.owner.plan_user_count == 10 + assert self.owner.plan_auto_activate == False + pm_mock.assert_called_once_with( + "pm_1LhiRsGlVGuVgOrkQguJXdeV", customer=self.owner.stripe_customer_id + ) + c_mock.assert_called_once_with( + self.owner.stripe_customer_id, + invoice_settings={"default_payment_method": "pm_1LhiRsGlVGuVgOrkQguJXdeV"}, + ) + + log_info_mock.assert_called_once_with( + "Stripe webhook event received", + extra={"stripe_webhook_event": "customer.subscription.updated"}, + ) + + @patch("billing.views.StripeWebhookHandler._has_unverified_initial_payment_method") + @patch("services.billing.stripe.PaymentMethod.attach") + @patch("services.billing.stripe.Customer.modify") + def test_customer_subscription_updated_sets_free_and_deactivates_all_repos_if_incomplete_expired( + self, + c_mock, + pm_mock, + has_unverified_initial_payment_method_mock, + ): + has_unverified_initial_payment_method_mock.return_value = False + self.owner.plan = "users-pr-inappy" + self.owner.plan_user_count = 10 + self.owner.plan_auto_activate = False + self.owner.save() + + RepositoryFactory(author=self.owner, activated=True, active=True) + RepositoryFactory(author=self.owner, activated=True, active=True) + RepositoryFactory(author=self.owner, activated=True, active=True) + assert self.owner.repository_set.count() == 3 + + self._send_event( + payload={ + "type": "customer.subscription.updated", + "data": { + "object": { + "id": self.owner.stripe_subscription_id, + "customer": self.owner.stripe_customer_id, + "plan": { + "id": "plan_pro_yearly", + }, + "metadata": {"obo_organization": self.owner.ownerid}, + "quantity": 20, + "status": "incomplete_expired", + "schedule": None, + "default_payment_method": "pm_1LhiRsGlVGuVgOrkQguJXdeV", + } + }, + } + ) + self.owner.refresh_from_db() + + assert self.owner.plan == DEFAULT_FREE_PLAN + assert self.owner.plan_user_count == 1 + assert self.owner.plan_auto_activate == False + assert self.owner.stripe_subscription_id is None + assert ( + self.owner.repository_set.filter(active=True, activated=True).count() == 0 + ) + pm_mock.assert_called_once_with( + "pm_1LhiRsGlVGuVgOrkQguJXdeV", customer=self.owner.stripe_customer_id + ) + c_mock.assert_called_once_with( + self.owner.stripe_customer_id, + invoice_settings={"default_payment_method": "pm_1LhiRsGlVGuVgOrkQguJXdeV"}, + ) + + @patch("billing.views.StripeWebhookHandler._has_unverified_initial_payment_method") + def test_customer_subscription_updated_payment_failed( + self, has_unverified_initial_payment_method_mock + ): + has_unverified_initial_payment_method_mock.return_value = False + self.owner.delinquent = False + self.owner.save() + + self._send_event( + payload={ + "type": "customer.subscription.updated", + "data": { + "object": { + "id": self.owner.stripe_subscription_id, + "customer": self.owner.stripe_customer_id, + "plan": {"id": "?"}, + "metadata": {"obo_organization": self.owner.ownerid}, + "quantity": 20, + "status": "active", + "schedule": None, + "default_payment_method": "pm_1LhiRsGlVGuVgOrkQguJXdeV", + "pending_update": { + "expires_at": 1571194285, + "subscription_items": [ + { + "id": "si_09IkI4u3ZypJUk5onGUZpe8O", + "price": "price_CBb6IXqvTLXp3f", + } + ], + }, + } + }, + } + ) + + self.owner.refresh_from_db() + assert self.owner.delinquent == True + + @patch("billing.views.StripeWebhookHandler._has_unverified_initial_payment_method") + @patch("services.billing.stripe.PaymentMethod.attach") + @patch("services.billing.stripe.Customer.modify") + def test_customer_subscription_updated_sets_free_and_deactivates_all_repos_if_incomplete_expired_multiple_owner( + self, + c_mock, + pm_mock, + has_unverified_initial_payment_method_mock, + ): + has_unverified_initial_payment_method_mock.return_value = False + self.add_second_owner() + self.owner.plan = "users-pr-inappy" + self.owner.plan_user_count = 10 + self.owner.plan_auto_activate = False + self.owner.save() + self.other_owner.plan = "users-pr-inappy" + self.other_owner.plan_user_count = 10 + self.other_owner.plan_auto_activate = False + self.other_owner.save() + + RepositoryFactory(author=self.owner, activated=True, active=True) + RepositoryFactory(author=self.owner, activated=True, active=True) + RepositoryFactory(author=self.owner, activated=True, active=True) + RepositoryFactory(author=self.other_owner, activated=True, active=True) + RepositoryFactory(author=self.other_owner, activated=True, active=True) + RepositoryFactory(author=self.other_owner, activated=True, active=True) + assert self.owner.repository_set.count() == 3 + assert self.other_owner.repository_set.count() == 3 + + self._send_event( + payload={ + "type": "customer.subscription.updated", + "data": { + "object": { + "id": self.owner.stripe_subscription_id, + "customer": self.owner.stripe_customer_id, + "plan": { + "id": "plan_pro_yearly", + }, + "metadata": {"obo_organization": self.owner.ownerid}, + "quantity": 20, + "status": "incomplete_expired", + "schedule": None, + "default_payment_method": "pm_1LhiRsGlVGuVgOrkQguJXdeV", + } + }, + } + ) + self.owner.refresh_from_db() + self.other_owner.refresh_from_db() + + assert self.owner.plan == DEFAULT_FREE_PLAN + assert self.owner.plan_user_count == 1 + assert self.owner.plan_auto_activate == False + assert self.owner.stripe_subscription_id is None + assert ( + self.owner.repository_set.filter(active=True, activated=True).count() == 0 + ) + assert self.other_owner.plan == DEFAULT_FREE_PLAN + assert self.other_owner.plan_user_count == 1 + assert self.other_owner.plan_auto_activate == False + assert self.other_owner.stripe_subscription_id is None + assert ( + self.other_owner.repository_set.filter(active=True, activated=True).count() + == 0 + ) + pm_mock.assert_called_once_with( + "pm_1LhiRsGlVGuVgOrkQguJXdeV", customer=self.owner.stripe_customer_id + ) + c_mock.assert_called_once_with( + self.owner.stripe_customer_id, + invoice_settings={"default_payment_method": "pm_1LhiRsGlVGuVgOrkQguJXdeV"}, + ) + + @patch("billing.views.StripeWebhookHandler._has_unverified_initial_payment_method") + @patch("services.billing.stripe.PaymentMethod.attach") + @patch("services.billing.stripe.Customer.modify") + def test_customer_subscription_updated_sets_fields_on_success( + self, + c_mock, + pm_mock, + has_unverified_initial_payment_method_mock, + ): + has_unverified_initial_payment_method_mock.return_value = False + self.owner.plan = "users-free" + self.owner.plan_user_count = 5 + self.owner.plan_auto_activate = False + + plan_name = "users-pr-inappy" + quantity = 20 + + self._send_event( + payload={ + "type": "customer.subscription.updated", + "data": { + "object": { + "id": self.owner.stripe_subscription_id, + "customer": self.owner.stripe_customer_id, + "plan": {"id": "plan_pro_yearly"}, + "metadata": {"obo_organization": self.owner.ownerid}, + "quantity": quantity, + "status": "active", + "schedule": None, + "default_payment_method": "pm_1LhiRsGlVGuVgOrkQguJXdeV", + } + }, + } + ) + + self.owner.refresh_from_db() + assert self.owner.plan == plan_name + assert self.owner.plan_user_count == quantity + assert self.owner.plan_auto_activate == True + pm_mock.assert_called_once_with( + "pm_1LhiRsGlVGuVgOrkQguJXdeV", customer=self.owner.stripe_customer_id + ) + c_mock.assert_called_once_with( + self.owner.stripe_customer_id, + invoice_settings={"default_payment_method": "pm_1LhiRsGlVGuVgOrkQguJXdeV"}, + ) + + @patch("billing.views.StripeWebhookHandler._has_unverified_initial_payment_method") + @patch("services.billing.stripe.PaymentMethod.attach") + @patch("services.billing.stripe.Customer.modify") + def test_customer_subscription_updated_sets_fields_on_success_multiple_owner( + self, + c_mock, + pm_mock, + has_unverified_initial_payment_method_mock, + ): + has_unverified_initial_payment_method_mock.return_value = False + self.add_second_owner() + self.owner.plan = "users-free" + self.owner.plan_user_count = 5 + self.owner.plan_auto_activate = False + self.other_owner.plan = "users-free" + self.other_owner.plan_user_count = 5 + self.other_owner.plan_auto_activate = False + + plan_name = "users-pr-inappy" + quantity = 20 + + self._send_event( + payload={ + "type": "customer.subscription.updated", + "data": { + "object": { + "id": self.owner.stripe_subscription_id, + "customer": self.owner.stripe_customer_id, + "plan": {"id": "plan_pro_yearly"}, + "metadata": {"obo_organization": self.owner.ownerid}, + "quantity": quantity, + "status": "active", + "schedule": None, + "default_payment_method": "pm_1LhiRsGlVGuVgOrkQguJXdeV", + } + }, + } + ) + + self.owner.refresh_from_db() + self.other_owner.refresh_from_db() + assert self.owner.plan == plan_name + assert self.owner.plan_user_count == quantity + assert self.owner.plan_auto_activate == True + assert self.other_owner.plan == plan_name + assert self.other_owner.plan_user_count == quantity + assert self.other_owner.plan_auto_activate == True + pm_mock.assert_called_once_with( + "pm_1LhiRsGlVGuVgOrkQguJXdeV", customer=self.owner.stripe_customer_id + ) + c_mock.assert_called_once_with( + self.owner.stripe_customer_id, + invoice_settings={"default_payment_method": "pm_1LhiRsGlVGuVgOrkQguJXdeV"}, + ) + + @patch("logging.Logger.error") + def test_customer_subscription_updated_logs_error_if_no_matching_owners( + self, log_error_mock + ): + self._send_event( + payload={ + "type": "customer.subscription.updated", + "data": { + "object": { + "id": "sub_notexist", + "customer": "cus_notexist", + "plan": {"id": "plan_pro_yearly"}, + "metadata": {"obo_organization": 1}, + "quantity": 8, + "status": "active", + "schedule": None, + "default_payment_method": "pm_1LhiRsGlVGuVgOrkQguJXdeV", + } + }, + } + ) + + log_error_mock.assert_called_with( + "Subscription update requested with for plan attached to no owners", + extra={ + "stripe_subscription_id": "sub_notexist", + "stripe_customer_id": "cus_notexist", + "plan_id": "plan_pro_yearly", + }, + ) + + @patch("services.billing.stripe.Subscription.retrieve") + def test_subscription_schedule_released_updates_owner_with_existing_subscription( + self, retrieve_subscription_mock + ): + self.owner.plan = "users-pr-inappy" + self.owner.plan_user_count = 10 + self.owner.save() + + self.new_params = { + "new_plan": "plan_pro_yearly", + "new_quantity": 7, + "subscription_id": "sub_123", + } + + retrieve_subscription_mock.return_value = MockSubscription( + self.owner, self.new_params + ) + + self._send_event( + payload={ + "type": "subscription_schedule.released", + "data": { + "object": { + "released_subscription": "sub_sched_1K8xfkGlVGuVgOrkxvroyZdH" + } + }, + } + ) + + self.owner.refresh_from_db() + plan = Plan.objects.get(stripe_id=self.new_params["new_plan"]) + assert self.owner.plan == plan.name + assert self.owner.plan_user_count == self.new_params["new_quantity"] + + @patch("services.billing.stripe.Subscription.retrieve") + def test_subscription_schedule_released_updates_multiple_owners_with_existing_subscription( + self, retrieve_subscription_mock + ): + self.add_second_owner() + self.owner.plan = "users-pr-inappy" + self.owner.plan_user_count = 10 + self.owner.save() + self.other_owner.plan = "users-pr-inappy" + self.other_owner.plan_user_count = 10 + self.other_owner.save() + + self.new_params = { + "new_plan": "plan_pro_yearly", + "new_quantity": 7, + "subscription_id": "sub_123", + } + + retrieve_subscription_mock.return_value = MockSubscription( + self.owner, self.new_params + ) + + self._send_event( + payload={ + "type": "subscription_schedule.released", + "data": { + "object": { + "released_subscription": "sub_sched_1K8xfkGlVGuVgOrkxvroyZdH" + } + }, + } + ) + + self.owner.refresh_from_db() + self.other_owner.refresh_from_db() + + plan = Plan.objects.get(stripe_id=self.new_params["new_plan"]) + assert self.owner.plan == plan.name + assert self.owner.plan_user_count == self.new_params["new_quantity"] + assert self.other_owner.plan == plan.name + assert self.other_owner.plan_user_count == self.new_params["new_quantity"] + + @patch("logging.Logger.error") + @patch("services.billing.stripe.Subscription.retrieve") + def test_subscription_schedule_released_logs_error_if_owner_does_not_exist( + self, + retrieve_subscription_mock, + log_error_mock, + ): + self.new_params = { + "new_plan": "plan_pro_yearly", + "new_quantity": 7, + "subscription_id": "sub_notexist", + } + + retrieve_subscription_mock.return_value = MockSubscription( + self.owner, self.new_params + ) + + self._send_event( + payload={ + "type": "subscription_schedule.released", + "data": { + "object": { + "released_subscription": "sub_sched_1K8xfkGlVGuVgOrkxvroyZdH" + } + }, + } + ) + + log_error_mock.assert_called_with( + "Subscription schedule released requested with invalid subscription", + extra={ + "stripe_subscription_id": "sub_notexist", + "stripe_customer_id": "cus_123", + "plan_id": "plan_pro_yearly", + }, + ) + + @patch("services.billing.stripe.Subscription.retrieve") + def test_subscription_schedule_created_logs_a_new_schedule( + self, retrieve_subscription_mock + ): + original_plan = "users-pr-inappy" + original_quantity = 10 + subscription_id = "sub_1K8xfkGlVGuVgOrkxvroyZdH" + self.owner.plan = original_plan + self.owner.plan_user_count = original_quantity + self.owner.save() + + self.params = { + "new_plan": "plan_pro_yearly", + "new_quantity": 7, + "subscription_id": subscription_id, + } + + retrieve_subscription_mock.return_value = MockSubscription( + self.owner, self.params + ) + + self._send_event( + payload={ + "type": "subscription_schedule.created", + "data": {"object": {"subscription": subscription_id}}, + } + ) + + self.owner.refresh_from_db() + assert self.owner.plan == original_plan + assert self.owner.plan_user_count == original_quantity + + @patch("services.billing.stripe.Subscription.retrieve") + def test_subscription_schedule_updated_logs_changes_to_schedule( + self, retrieve_subscription_mock + ): + original_plan = "users-pr-inappy" + original_quantity = 10 + subscription_id = "sub_1K8xfkGlVGuVgOrkxvroyZdH" + new_plan = "plan_pro_yearly" + new_quantity = 7 + self.owner.plan = original_plan + self.owner.plan_user_count = original_quantity + self.owner.save() + + self.params = { + "new_plan": new_plan, + "new_quantity": new_quantity, + "subscription_id": subscription_id, + } + + retrieve_subscription_mock.return_value = MockSubscription( + self.owner, self.params + ) + + self._send_event( + payload={ + "type": "subscription_schedule.updated", + "data": { + "object": { + "subscription": subscription_id, + "phases": [ + {}, + {"items": [{"plan": new_plan, "quantity": new_quantity}]}, + ], + } + }, + } + ) + + self.owner.refresh_from_db() + assert self.owner.plan == original_plan + assert self.owner.plan_user_count == original_quantity + + def test_checkout_session_completed_sets_stripe_ids(self): + self.owner.stripe_customer_id = None + self.owner.save() + + expected_customer_id = "cus_1234" + expected_subscription_id = "sub_7890" + + self._send_event( + payload={ + "type": "checkout.session.completed", + "data": { + "object": { + "customer": expected_customer_id, + "client_reference_id": str(self.owner.ownerid), + "subscription": expected_subscription_id, + } + }, + } + ) + + self.owner.refresh_from_db() + assert self.owner.stripe_customer_id == expected_customer_id + assert self.owner.stripe_subscription_id == expected_subscription_id + + @patch("billing.views.stripe.Subscription.modify") + def test_customer_update_but_not_payment_method(self, subscription_modify_mock): + payment_method = "pm_123" + self._send_event( + payload={ + "type": "customer.updated", + "data": { + "object": { + "invoice_settings": {"default_payment_method": None}, + "subscriptions": { + "data": [{"default_payment_method": payment_method}] + }, + } + }, + } + ) + + subscription_modify_mock.assert_not_called() + + @patch("billing.views.stripe.Subscription.modify") + def test_customer_update_but_payment_method_is_same(self, subscription_modify_mock): + payment_method = "pm_123" + self._send_event( + payload={ + "type": "customer.updated", + "data": { + "object": { + "invoice_settings": {"default_payment_method": payment_method}, + "subscriptions": { + "data": [{"default_payment_method": payment_method}] + }, + } + }, + } + ) + + subscription_modify_mock.assert_not_called() + + @patch("billing.views.stripe.Subscription.modify") + def test_customer_update_payment_method(self, subscription_modify_mock): + payment_method = "pm_123" + old_payment_method = "pm_321" + self._send_event( + payload={ + "type": "customer.updated", + "data": { + "object": { + "id": "cus_123", + "invoice_settings": {"default_payment_method": payment_method}, + "subscriptions": { + "data": [ + { + "id": "sub_123", + "default_payment_method": old_payment_method, + } + ] + }, + } + }, + } + ) + + subscription_modify_mock.assert_called_once_with( + "sub_123", default_payment_method=payment_method + ) + + @patch("services.billing.stripe.PaymentIntent.retrieve") + @patch("services.billing.stripe.Invoice.retrieve") + def test_has_unverified_initial_payment_method( + self, invoice_retrieve_mock, payment_intent_retrieve_mock + ): + subscription = Mock() + subscription.latest_invoice = "inv_123" + + class MockPaymentIntent: + status = "requires_action" + + invoice_retrieve_mock.return_value = Mock(payment_intent="pi_123") + payment_intent_retrieve_mock.return_value = stripe.PaymentIntent.construct_from( + { + "status": "requires_action", + "next_action": {"type": "verify_with_microdeposits"}, + }, + "payment_intent_asdf", + ) + + handler = StripeWebhookHandler() + result = handler._has_unverified_initial_payment_method(subscription) + + assert result is True + invoice_retrieve_mock.assert_called_once_with("inv_123") + payment_intent_retrieve_mock.assert_called_once_with("pi_123") + + @patch("services.billing.stripe.PaymentIntent.retrieve") + @patch("services.billing.stripe.Invoice.retrieve") + def test_has_unverified_initial_payment_method_no_payment_intent( + self, invoice_retrieve_mock, payment_intent_retrieve_mock + ): + subscription = Mock() + subscription.latest_invoice = "inv_123" + + invoice_retrieve_mock.return_value = Mock(payment_intent=None) + + handler = StripeWebhookHandler() + result = handler._has_unverified_initial_payment_method(subscription) + + assert result is False + invoice_retrieve_mock.assert_called_once_with("inv_123") + payment_intent_retrieve_mock.assert_not_called() + + @patch("services.billing.stripe.PaymentIntent.retrieve") + @patch("services.billing.stripe.Invoice.retrieve") + def test_has_unverified_initial_payment_method_payment_intent_succeeded( + self, invoice_retrieve_mock, payment_intent_retrieve_mock + ): + subscription = stripe.Subscription.construct_from( + {"latest_invoice": "inv_123"}, "sub_123" + ) + + invoice_retrieve_mock.return_value = stripe.Invoice.construct_from( + {"payment_intent": "pi_123"}, "inv_123" + ) + payment_intent_retrieve_mock.return_value = stripe.PaymentIntent.construct_from( + {"status": "succeeded"}, "payment_intent_asdf" + ) + + handler = StripeWebhookHandler() + result = handler._has_unverified_initial_payment_method(subscription) + + assert result is False + invoice_retrieve_mock.assert_called_once_with("inv_123") + payment_intent_retrieve_mock.assert_called_once_with("pi_123") + + @patch("services.billing.stripe.PaymentMethod.attach") + @patch("services.billing.stripe.Customer.modify") + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.PaymentMethod.retrieve") + def test_check_and_handle_delayed_notification_payment_methods( + self, + payment_method_retrieve_mock, + subscription_modify_mock, + customer_modify_mock, + payment_method_attach_mock, + ): + class MockPaymentMethod: + type = "us_bank_account" + us_bank_account = {} + id = "pm_123" + + payment_method_retrieve_mock.return_value = MockPaymentMethod() + + self.owner.stripe_subscription_id = "sub_123" + self.owner.stripe_customer_id = "cus_123" + self.owner.save() + + handler = StripeWebhookHandler() + handler._check_and_handle_delayed_notification_payment_methods( + "cus_123", "pm_123" + ) + + payment_method_retrieve_mock.assert_called_once_with("pm_123") + payment_method_attach_mock.assert_called_once_with( + payment_method_retrieve_mock.return_value, customer="cus_123" + ) + customer_modify_mock.assert_called_once_with( + "cus_123", + invoice_settings={ + "default_payment_method": payment_method_retrieve_mock.return_value + }, + ) + subscription_modify_mock.assert_called_once_with( + "sub_123", default_payment_method=payment_method_retrieve_mock.return_value + ) + + @patch("logging.Logger.error") + @patch("services.billing.stripe.PaymentMethod.attach") + @patch("services.billing.stripe.Customer.modify") + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.PaymentMethod.retrieve") + def test_check_and_handle_delayed_notification_payment_methods_no_subscription( + self, + payment_method_retrieve_mock, + subscription_modify_mock, + customer_modify_mock, + payment_method_attach_mock, + log_error_mock, + ): + class MockPaymentMethod: + type = "us_bank_account" + us_bank_account = {} + id = "pm_123" + + payment_method_retrieve_mock.return_value = MockPaymentMethod() + + self.owner.stripe_subscription_id = None + self.owner.stripe_customer_id = "cus_123" + self.owner.save() + + handler = StripeWebhookHandler() + handler._check_and_handle_delayed_notification_payment_methods( + "cus_123", "pm_123" + ) + + payment_method_retrieve_mock.assert_called_once_with("pm_123") + payment_method_attach_mock.assert_not_called() + customer_modify_mock.assert_not_called() + subscription_modify_mock.assert_not_called() + + log_error_mock.assert_called_once_with( + "No owners found with that customer_id, something went wrong", + extra=dict(customer_id="cus_123"), + ) + + @patch("logging.Logger.error") + @patch("services.billing.stripe.PaymentMethod.attach") + @patch("services.billing.stripe.Customer.modify") + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.PaymentMethod.retrieve") + def test_check_and_handle_delayed_notification_payment_methods_no_customer( + self, + payment_method_retrieve_mock, + subscription_modify_mock, + customer_modify_mock, + payment_method_attach_mock, + log_error_mock, + ): + class MockPaymentMethod: + type = "us_bank_account" + us_bank_account = {} + id = "pm_123" + + payment_method_retrieve_mock.return_value = MockPaymentMethod() + + handler = StripeWebhookHandler() + handler._check_and_handle_delayed_notification_payment_methods( + "cus_1", "pm_123" + ) + + payment_method_retrieve_mock.assert_called_once_with("pm_123") + payment_method_attach_mock.assert_not_called() + customer_modify_mock.assert_not_called() + subscription_modify_mock.assert_not_called() + + log_error_mock.assert_called_once_with( + "No owners found with that customer_id, something went wrong", + extra=dict(customer_id="cus_1"), + ) + + @patch("services.billing.stripe.PaymentMethod.attach") + @patch("services.billing.stripe.Customer.modify") + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.PaymentMethod.retrieve") + def test_check_and_handle_delayed_notification_payment_methods_multiple_subscriptions( + self, + payment_method_retrieve_mock, + subscription_modify_mock, + customer_modify_mock, + payment_method_attach_mock, + ): + class MockPaymentMethod: + type = "us_bank_account" + us_bank_account = {} + id = "pm_123" + + payment_method_retrieve_mock.return_value = MockPaymentMethod() + + self.owner.stripe_subscription_id = "sub_123" + self.owner.stripe_customer_id = "cus_123" + self.owner.save() + + OwnerFactory(stripe_subscription_id="sub_124", stripe_customer_id="cus_123") + + handler = StripeWebhookHandler() + handler._check_and_handle_delayed_notification_payment_methods( + "cus_123", "pm_123" + ) + + payment_method_retrieve_mock.assert_called_once_with("pm_123") + payment_method_attach_mock.assert_called_once_with( + payment_method_retrieve_mock.return_value, customer="cus_123" + ) + customer_modify_mock.assert_called_once_with( + "cus_123", + invoice_settings={ + "default_payment_method": payment_method_retrieve_mock.return_value + }, + ) + subscription_modify_mock.assert_has_calls( + [ + call( + "sub_123", + default_payment_method=payment_method_retrieve_mock.return_value, + ), + call( + "sub_124", + default_payment_method=payment_method_retrieve_mock.return_value, + ), + ] + ) + + @patch( + "billing.views.StripeWebhookHandler._check_and_handle_delayed_notification_payment_methods" + ) + @patch("logging.Logger.info") + def test_payment_intent_succeeded( + self, log_info_mock, check_and_handle_delayed_notification_mock + ): + class MockPaymentIntent: + id = "pi_123" + customer = "cus_123" + payment_method = "pm_123" + + handler = StripeWebhookHandler() + handler.payment_intent_succeeded(MockPaymentIntent()) + + check_and_handle_delayed_notification_mock.assert_called_once_with( + "cus_123", "pm_123" + ) + log_info_mock.assert_called_once_with( + "Payment intent succeeded", + extra=dict( + stripe_customer_id="cus_123", + payment_intent_id="pi_123", + payment_method_type="pm_123", + ), + ) + + @patch( + "billing.views.StripeWebhookHandler._check_and_handle_delayed_notification_payment_methods" + ) + @patch("logging.Logger.info") + def test_setup_intent_succeeded( + self, log_info_mock, check_and_handle_delayed_notification_mock + ): + class MockSetupIntent: + id = "seti_123" + customer = "cus_123" + payment_method = "pm_123" + + handler = StripeWebhookHandler() + handler.setup_intent_succeeded(MockSetupIntent()) + + check_and_handle_delayed_notification_mock.assert_called_once_with( + "cus_123", "pm_123" + ) + log_info_mock.assert_called_once_with( + "Setup intent succeeded", + extra=dict( + stripe_customer_id="cus_123", + setup_intent_id="seti_123", + payment_method_type="pm_123", + ), + ) diff --git a/apps/codecov-api/billing/urls.py b/apps/codecov-api/billing/urls.py new file mode 100644 index 0000000000..36b1a7ba82 --- /dev/null +++ b/apps/codecov-api/billing/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from .views import StripeWebhookHandler + +urlpatterns = [ + path("stripe/webhooks", StripeWebhookHandler.as_view(), name="stripe-webhook") +] diff --git a/apps/codecov-api/billing/views.py b/apps/codecov-api/billing/views.py new file mode 100644 index 0000000000..19954146bd --- /dev/null +++ b/apps/codecov-api/billing/views.py @@ -0,0 +1,630 @@ +import logging +from datetime import datetime +from typing import Any, List + +import stripe +from django.conf import settings +from django.db.models import QuerySet +from django.http import HttpRequest +from rest_framework import status +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework.views import APIView +from shared.plan.service import PlanService + +from billing.helpers import get_all_admins_for_owners +from codecov_auth.models import Owner, Plan +from services.task.task import TaskService + +from .constants import StripeHTTPHeaders, StripeWebhookEvents + +if settings.STRIPE_API_KEY: + stripe.api_key = settings.STRIPE_API_KEY + stripe.api_version = "2024-12-18.acacia" + +log = logging.getLogger(__name__) + + +class StripeWebhookHandler(APIView): + permission_classes = [AllowAny] + + def _log_updated(self, updated: List[Owner]) -> None: + if len(updated) >= 1: + log.info( + f"Successfully updated info for {len(updated)} owner(s)", + extra=dict(owners=[owner.ownerid for owner in updated]), + ) + + def invoice_payment_succeeded(self, invoice: stripe.Invoice) -> None: + log.info( + "Invoice Payment Succeeded - Setting delinquency status False", + extra=dict( + stripe_customer_id=invoice.customer, + stripe_subscription_id=invoice.subscription, + ), + ) + owners: QuerySet[Owner] = Owner.objects.filter( + stripe_customer_id=invoice.customer, + stripe_subscription_id=invoice.subscription, + delinquent=True, + ) + + if not owners.exists(): + return + + admins = get_all_admins_for_owners(owners) + owners.update(delinquent=False) + self._log_updated(list(owners)) + + # Send a success email to all admins + + task_service = TaskService() + template_vars = { + "amount": invoice.total / 100, + "date": datetime.now().strftime("%B %-d, %Y"), + "cta_link": invoice.hosted_invoice_url, + } + + for admin in admins: + if admin.email: + task_service.send_email( + to_addr=admin.email, + subject="You're all set", + template_name="success-after-failed-payment", + **template_vars, + ) + + def invoice_payment_failed(self, invoice: stripe.Invoice) -> None: + """ + Stripe invoice.payment_failed webhook event is emitted when an invoice payment fails + (initial or recurring). Note that delayed payment methods (including ACH with + microdeposits) may have a failed initial invoice until the account is verified. + """ + if invoice.default_payment_method is None: + if invoice.payment_intent: + payment_intent = stripe.PaymentIntent.retrieve(invoice.payment_intent) + if ( + payment_intent + and payment_intent.get("status") == "requires_action" + and payment_intent.get("next_action", {}).get("type") + == "verify_with_microdeposits" + ): + log.info( + "Invoice payment failed but still awaiting known customer action, skipping Delinquency actions", + extra=dict( + stripe_customer_id=invoice.customer, + stripe_subscription_id=invoice.subscription, + payment_intent_id=invoice.payment_intent, + payment_intent_status=payment_intent.status, + ), + ) + return + + log.info( + "Invoice Payment Failed - Setting Delinquency status True", + extra=dict( + stripe_customer_id=invoice.customer, + stripe_subscription_id=invoice.subscription, + ), + ) + owners: QuerySet[Owner] = Owner.objects.filter( + stripe_customer_id=invoice.customer, + stripe_subscription_id=invoice.subscription, + ) + owners.update(delinquent=True) + self._log_updated(list(owners)) + + # Send failed payment email to all owner admins + admins = get_all_admins_for_owners(owners) + + task_service = TaskService() + payment_intent = stripe.PaymentIntent.retrieve( + invoice["payment_intent"], expand=["payment_method"] + ) + + try: + card = payment_intent.payment_method.card + except AttributeError: + card = None + + template_vars = { + "amount": invoice.total / 100, + "card_type": card.brand if card else None, + "last_four": card.last4 if card else None, + "cta_link": invoice.hosted_invoice_url, + "date": datetime.now().strftime("%B %-d, %Y"), + } + + for admin in admins: + if admin.email: + task_service.send_email( + to_addr=admin.email, + subject="Your Codecov payment failed", + template_name="failed-payment", + name=admin.username, + **template_vars, + ) + + def customer_subscription_deleted(self, subscription: stripe.Subscription) -> None: + """ + Stripe customer.subscription.deleted webhook event is emitted when a subscription is deleted. + This happens when an org goes from paid to free (see payment_service.delete_subscription) + or when cleaning up an incomplete subscription that never activated (e.g., abandoned async + ACH microdeposits verification). + """ + log.info( + "Customer Subscription Deleted - Setting free plan and deactivating repos for stripe customer", + extra=dict( + stripe_subscription_id=subscription.id, + stripe_customer_id=subscription.customer, + previous_subscription_status=subscription.status, + ), + ) + owners: QuerySet[Owner] = Owner.objects.filter( + stripe_customer_id=subscription.customer, + stripe_subscription_id=subscription.id, + ) + if not owners.exists(): + log.info( + "Customer Subscription Deleted - Couldn't find owner, subscription likely already deleted", + extra=dict( + stripe_subscription_id=subscription.id, + stripe_customer_id=subscription.customer, + ), + ) + return + + for owner in owners: + plan_service = PlanService(current_org=owner) + plan_service.set_default_plan_data() + owner.repository_set.update(active=False, activated=False) + + self._log_updated(list(owners)) + + def subscription_schedule_created( + self, schedule: stripe.SubscriptionSchedule + ) -> None: + subscription = stripe.Subscription.retrieve(schedule["subscription"]) + sub_item_plan_id = subscription.plan.id + plan_name = Plan.objects.get(stripe_id=sub_item_plan_id).name + log.info( + "Schedule created for customer", + extra=dict( + stripe_customer_id=subscription.customer, + stripe_subscription_id=subscription.id, + ownerid=subscription.metadata.get("obo_organization"), + plan=plan_name, + quantity=subscription.quantity, + ), + ) + + def subscription_schedule_updated( + self, schedule: stripe.SubscriptionSchedule + ) -> None: + if schedule["subscription"]: + subscription = stripe.Subscription.retrieve(schedule["subscription"]) + scheduled_phase = schedule["phases"][-1] + scheduled_plan = scheduled_phase["items"][0] + plan_id = scheduled_plan["plan"] + plan_name = Plan.objects.get(stripe_id=plan_id).name + quantity = scheduled_plan["quantity"] + log.info( + "Schedule updated for customer", + extra=dict( + stripe_customer_id=subscription.customer, + stripe_subscription_id=subscription.id, + ownerid=subscription.metadata.get("obo_organization"), + plan=plan_name, + quantity=quantity, + ), + ) + + def subscription_schedule_released( + self, schedule: stripe.SubscriptionSchedule + ) -> None: + subscription = stripe.Subscription.retrieve(schedule["released_subscription"]) + owners: QuerySet[Owner] = Owner.objects.filter( + stripe_subscription_id=subscription.id, + stripe_customer_id=subscription.customer, + ) + if not owners.exists(): + log.error( + "Subscription schedule released requested with invalid subscription", + extra=dict( + stripe_subscription_id=subscription.id, + stripe_customer_id=subscription.customer, + plan_id=subscription.plan.id, + ), + ) + return + + sub_item_plan_id = subscription.plan.id + plan_name = Plan.objects.get(stripe_id=sub_item_plan_id).name + for owner in owners: + plan_service = PlanService(current_org=owner) + plan_service.update_plan(name=plan_name, user_count=subscription.quantity) + + requesting_user_id = subscription.metadata.get("obo") + log.info( + "Successfully updated customer plan info", + extra=dict( + stripe_subscription_id=subscription.id, + stripe_customer_id=subscription.customer, + plan=plan_name, + quantity=subscription.quantity, + owners=[owner.ownerid for owner in owners], + requesting_user_id=requesting_user_id, + ), + ) + + def customer_created(self, customer: stripe.Customer) -> None: + # Based on what stripe doesn't gives us (an ownerid!) + # in this event we cannot reliably create a customer, + # so we're just logging that we created the event and + # relying on customer.subscription.created to handle sub creation + log.info("Customer created", extra=dict(stripe_customer_id=customer.id)) + + def customer_subscription_created(self, subscription: stripe.Subscription) -> None: + """ + Stripe customer.subscription.created webhook event is emitted when a subscription is created. + This happens when an owner completes a CheckoutSession for a new subscription. + """ + sub_item_plan_id = subscription.plan.id + + if not sub_item_plan_id: + log.warning( + "Subscription created, but missing plan_id", + extra=dict( + stripe_customer_id=subscription.customer, + ownerid=subscription.metadata.get("obo_organization"), + subscription_plan=subscription.plan, + ), + ) + return + + try: + plan = Plan.objects.get(stripe_id=sub_item_plan_id) + except Plan.DoesNotExist: + log.warning( + "Subscription creation requested for invalid plan", + extra=dict( + stripe_customer_id=subscription.customer, + ownerid=subscription.metadata.get("obo_organization"), + plan_id=sub_item_plan_id, + ), + ) + return + + plan_name = plan.name + + log.info( + "Subscription created for customer", + extra=dict( + stripe_customer_id=subscription.customer, + stripe_subscription_id=subscription.id, + ownerid=subscription.metadata.get("obo_organization"), + plan=plan_name, + quantity=subscription.quantity, + ), + ) + # add the subscription_id and customer_id to the owner + owner = Owner.objects.get(ownerid=subscription.metadata.get("obo_organization")) + owner.stripe_subscription_id = subscription.id + owner.stripe_customer_id = subscription.customer + owner.save() + + # We may reach here if the subscription was created with a payment method + # that is awaiting verification (e.g. ACH microdeposits) + if self._has_unverified_initial_payment_method(subscription): + log.info( + "Subscription has pending initial payment verification - will upgrade plan after initial invoice payment", + extra=dict( + subscription_id=subscription.id, + customer_id=subscription.customer, + ), + ) + return + + plan_service = PlanService(current_org=owner) + plan_service.expire_trial_when_upgrading() + + plan_service.update_plan(name=plan_name, user_count=subscription.quantity) + + log.info( + "Successfully updated customer plan info", + extra=dict( + stripe_subscription_id=subscription.id, + stripe_customer_id=subscription.customer, + plan=plan_name, + quantity=subscription.quantity, + ), + ) + + self._log_updated([owner]) + + def _has_unverified_initial_payment_method( + self, subscription: stripe.Subscription + ) -> bool: + """ + Helper method to check if a subscription's latest invoice has a payment intent + that requires verification (e.g. ACH microdeposits). This indicates that + there is an unverified payment method from the initial CheckoutSession. + """ + latest_invoice = stripe.Invoice.retrieve(subscription.latest_invoice) + if latest_invoice and latest_invoice.payment_intent: + payment_intent = stripe.PaymentIntent.retrieve( + latest_invoice.payment_intent + ) + return ( + payment_intent + and payment_intent.get("status") == "requires_action" + and payment_intent.get("next_action") + and payment_intent.get("next_action", {}).get("type") + == "verify_with_microdeposits" + ) + return False + + def customer_subscription_updated(self, subscription: stripe.Subscription) -> None: + """ + Stripe customer.subscription.updated webhook event is emitted when a subscription is updated. + This can happen when an owner updates the subscription's default payment method using our + update_payment_method api + """ + owners: QuerySet[Owner] = Owner.objects.filter( + stripe_subscription_id=subscription.id, + stripe_customer_id=subscription.customer, + ) + if not owners.exists(): + log.error( + "Subscription update requested with for plan attached to no owners", + extra=dict( + stripe_subscription_id=subscription.id, + stripe_customer_id=subscription.customer, + plan_id=subscription.plan.id, + ), + ) + return + + if self._has_unverified_initial_payment_method(subscription): + log.info( + "Subscription has pending initial payment verification - will upgrade plan after initial invoice payment", + extra=dict( + subscription_id=subscription.id, + customer_id=subscription.customer, + ), + ) + return + + indication_of_payment_failure = getattr(subscription, "pending_update", None) + if indication_of_payment_failure: + # payment failed, raise this to user by setting as delinquent + owners.update(delinquent=True) + log.info( + "Stripe subscription upgrade failed", + extra=dict( + pending_update=indication_of_payment_failure, + stripe_subscription_id=subscription.id, + stripe_customer_id=subscription.customer, + owners=[owner.ownerid for owner in owners], + ), + ) + return + + # Properly attach the payment method on the customer + # This hook will be called after a checkout session completes, + # updating the subscription created with it + default_payment_method = subscription.default_payment_method + if default_payment_method: + stripe.PaymentMethod.attach( + default_payment_method, customer=subscription.customer + ) + stripe.Customer.modify( + subscription.customer, + invoice_settings={"default_payment_method": default_payment_method}, + ) + + try: + plan = Plan.objects.get(stripe_id=subscription.plan.id) + except Plan.DoesNotExist: + log.error( + "Subscription update requested with invalid plan", + extra=dict( + stripe_subscription_id=subscription.id, + stripe_customer_id=subscription.customer, + plan_id=subscription.plan.id, + ), + ) + return + + subscription_schedule_id = subscription.schedule + plan_name = plan.name + incomplete_expired = subscription.status == "incomplete_expired" + + # Only update if there is not a scheduled subscription + if subscription_schedule_id: + return + + owner_ids = [] + for owner in owners: + plan_service = PlanService(current_org=owner) + if incomplete_expired: + plan_service.set_default_plan_data() + owner.repository_set.update(active=False, activated=False) + else: + plan_service.update_plan( + name=plan_name, user_count=subscription.quantity + ) + owner_ids.append(owner.ownerid) + + if incomplete_expired: + log.info( + "Subscription status updated to incomplete_expired, cancelling to free", + extra=dict( + stripe_subscription_id=subscription.id, + stripe_customer_id=subscription.customer, + owners=owner_ids, + ), + ) + return + log.info( + "Successfully updated customer subscription", + extra=dict( + stripe_subscription_id=subscription.id, + stripe_customer_id=subscription.customer, + plan=plan_name, + quantity=subscription.quantity, + owners=owner_ids, + ), + ) + + def customer_updated(self, customer: stripe.Customer) -> None: + new_default_payment_method = customer["invoice_settings"][ + "default_payment_method" + ] + + if new_default_payment_method is None: + return + + for subscription in customer.get("subscriptions", {}).get("data", []): + if new_default_payment_method == subscription["default_payment_method"]: + continue + log.info( + "Customer updated their payment method, updating the subscription payment as well", + extra=dict( + customer_id=customer["id"], subscription_id=subscription["id"] + ), + ) + stripe.Subscription.modify( + subscription["id"], default_payment_method=new_default_payment_method + ) + + def checkout_session_completed( + self, checkout_session: stripe.checkout.Session + ) -> None: + log.info( + "Checkout session completed", + extra=dict( + ownerid=checkout_session.client_reference_id, + stripe_customer_id=checkout_session.customer, + ), + ) + owner = Owner.objects.get(ownerid=checkout_session.client_reference_id) + owner.stripe_customer_id = checkout_session.customer + owner.stripe_subscription_id = checkout_session.subscription + owner.save() + + self._log_updated([owner]) + + def _check_and_handle_delayed_notification_payment_methods( + self, customer_id: str, payment_method_id: str + ): + """ + Helper method to handle payment methods that require delayed verification (like ACH). + When verification succeeds, this attaches the payment method to the customer and sets + it as the default payment method for both the customer and subscription. + """ + payment_method = stripe.PaymentMethod.retrieve(payment_method_id) + + is_us_bank_account = payment_method.type == "us_bank_account" and hasattr( + payment_method, "us_bank_account" + ) + + should_set_as_default = is_us_bank_account + + # attach the payment method + set as default on the invoice and subscription + if should_set_as_default: + # retrieve the number of owners to update + owners = Owner.objects.filter( + stripe_customer_id=customer_id, stripe_subscription_id__isnull=False + ) + + if owners.exists(): + # Even if multiple results are returned, these two stripe calls are + # just for a single customer + stripe.PaymentMethod.attach(payment_method, customer=customer_id) + stripe.Customer.modify( + customer_id, + invoice_settings={"default_payment_method": payment_method}, + ) + + # But this one is for each subscription an owner may have + for owner in owners: + stripe.Subscription.modify( + owner.stripe_subscription_id, + default_payment_method=payment_method, + ) + else: + log.error( + "No owners found with that customer_id, something went wrong", + extra=dict(customer_id=customer_id), + ) + + def payment_intent_succeeded(self, payment_intent: stripe.PaymentIntent) -> None: + """ + Stripe payment_intent.succeeded webhook event is emitted when a + payment intent goes to a success state. + We create a Stripe PaymentIntent for the initial checkout session. + """ + log.info( + "Payment intent succeeded", + extra=dict( + stripe_customer_id=payment_intent.customer, + payment_intent_id=payment_intent.id, + payment_method_type=payment_intent.payment_method, + ), + ) + + self._check_and_handle_delayed_notification_payment_methods( + payment_intent.customer, payment_intent.payment_method + ) + + def setup_intent_succeeded(self, setup_intent: stripe.SetupIntent) -> None: + """ + Stripe setup_intent.succeeded webhook event is emitted when a setup intent + goes to a success state. We create a Stripe SetupIntent for the gazebo UI + PaymentElement to modify payment methods. + """ + log.info( + "Setup intent succeeded", + extra=dict( + stripe_customer_id=setup_intent.customer, + setup_intent_id=setup_intent.id, + payment_method_type=setup_intent.payment_method, + ), + ) + + self._check_and_handle_delayed_notification_payment_methods( + setup_intent.customer, setup_intent.payment_method + ) + + def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Response: + if settings.STRIPE_ENDPOINT_SECRET is None: + log.critical( + "Stripe endpoint secret improperly configured -- webhooks will not be processed." + ) + try: + self.event = stripe.Webhook.construct_event( + self.request.body, + self.request.META.get(StripeHTTPHeaders.SIGNATURE), + settings.STRIPE_ENDPOINT_SECRET, + ) + except stripe.SignatureVerificationError as e: + log.warning(f"Stripe webhook event received with invalid signature -- {e}") + return Response("Invalid signature", status=status.HTTP_400_BAD_REQUEST) + if self.event.type not in StripeWebhookEvents.subscribed_events: + log.warning( + "Unsupported Stripe webhook event received, exiting", + extra=dict(stripe_webhook_event=self.event.type), + ) + return Response("Unsupported event type", status=204) + + log.info( + "Stripe webhook event received", + extra=dict(stripe_webhook_event=self.event.type), + ) + + # Converts event names of the format X.Y.Z into X_Y_Z, and calls + # the relevant method in this class + getattr(self, self.event.type.replace(".", "_"))(self.event.data.object) + + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apps/codecov-api/ci.yml b/apps/codecov-api/ci.yml new file mode 100644 index 0000000000..74b454c2f3 --- /dev/null +++ b/apps/codecov-api/ci.yml @@ -0,0 +1,87 @@ +setup: + codecov_url: https://codecov.io + debug: no + loglvl: INFO + encryption_secret: "zp^P9*i8aR3" + media: + assets: https://codecov-cdn.storage.googleapis.com/4.4.4-fd6aa1e + dependancies: https://codecov-cdn.storage.googleapis.com/4.4.4-fd6aa1e + http: + force_https: yes + cookie_secret: abc123 + timeouts: + connect: 10 + receive: 15 + tasks: + celery: + soft_timelimit: 200 + hard_timelimit: 240 + upload: + queue: uploads + cache: + yaml: 600 # 10 minutes + tree: 600 # 10 minutes + diff: 300 # 5 minutes + chunks: 300 # 5 minutes + uploads: 86400 # 1 day + +services: + database_url: postgres://postgres:@postgres:5432/circle_test + database: + username: postgres + name: circle_test + password: "" + host: postgres + redis_url: redis://redis:@localhost:6379/ + minio: + hash_key: testixik8qdauiab1yiffydimvi72ekq # never change this + access_key_id: codecov-default-key + secret_access_key: codecov-default-secret + verify_ssl: false + +github: + bot: + username: codecov-io + integration: + id: 254 + pem: src/certs/github.pem + +bitbucket: + bot: + username: codecov-io + +gitlab: + bot: + username: codecov-io + +site: + codecov: + require_ci_to_pass: yes + + coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: yes + patch: yes + changes: no + + parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + + javascript: + enable_partials: no + + comment: + layout: "reach, diff, flags, files, footer" + behavior: default + require_changes: no + require_base: no + require_head: yes diff --git a/apps/codecov-api/codecov.yml b/apps/codecov-api/codecov.yml new file mode 100644 index 0000000000..ea8787240f --- /dev/null +++ b/apps/codecov-api/codecov.yml @@ -0,0 +1,8 @@ +ignore: + - "**tests**/test_*.py" + +codecov: + require_ci_to_pass: false + +test_analytics: + flake_detection: true diff --git a/apps/codecov-api/codecov/__init__.py b/apps/codecov-api/codecov/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/codecov/admin.py b/apps/codecov-api/codecov/admin.py new file mode 100644 index 0000000000..4ff06444fa --- /dev/null +++ b/apps/codecov-api/codecov/admin.py @@ -0,0 +1,71 @@ +from django.contrib import admin +from django.urls import reverse +from django.utils.html import format_html +from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin +from shared.django_apps.rollouts.models import FeatureFlag, FeatureFlagVariant + +from codecov.forms import AutocompleteSearchForm + + +class AdminMixin(object): + def save_model(self, request, new_obj, form, change) -> None: + if change: + old_obj = self.model.objects.get(pk=new_obj.pk) + new_obj.changed_fields = dict() + + for changed_field in form.changed_data: + prev_value = getattr(old_obj, changed_field) + new_value = getattr(new_obj, changed_field) + new_obj.changed_fields[changed_field] = ( + f"prev value: {prev_value}, new value: {new_value}" + ) + + return super().save_model(request, new_obj, form, change) + + def log_change(self, request, object, message): + message.append(object.changed_fields) + return super().log_change(request, object, message) + + +class FeatureFlagVariantInline(admin.StackedInline): + model = FeatureFlagVariant + exclude = ["override_repo_ids", "override_owner_ids"] + fields = ["name", "proportion", "value", "view_link"] + readonly_fields = [ + "view_link", + ] + extra = 0 + + def view_link(self, obj): + link = reverse( + "admin:rollouts_featureflagvariant_change", args=[obj.variant_id] + ) + return format_html('View', link) + + view_link.short_description = "More Details" + + +class FeatureFlagAdmin(admin.ModelAdmin): + list_display = ["name", "is_active", "number_of_variants", "proportion_percentage"] + search_fields = ["name"] + inlines = [FeatureFlagVariantInline] + + def number_of_variants(self, obj): + return obj.variants.count() + + number_of_variants.short_description = "# of Variants" + + def proportion_percentage(self, obj): + return str(round(obj.proportion * 100)) + "%" + + proportion_percentage.short_description = "Experiment Proportion" + + +class FeatureFlagVariantAdmin(admin.ModelAdmin, DynamicArrayMixin): + list_display = ["variant_id", "name", "feature_flag"] + search_fields = ["variant_id", "name", "feature_flag__name"] + form = AutocompleteSearchForm + + +admin.site.register(FeatureFlag, FeatureFlagAdmin) +admin.site.register(FeatureFlagVariant, FeatureFlagVariantAdmin) diff --git a/apps/codecov-api/codecov/commands/__init__.py b/apps/codecov-api/codecov/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/codecov/commands/base.py b/apps/codecov-api/codecov/commands/base.py new file mode 100644 index 0000000000..dc40515d2f --- /dev/null +++ b/apps/codecov-api/codecov/commands/base.py @@ -0,0 +1,111 @@ +from django.conf import settings +from django.contrib.auth.models import AnonymousUser + +import services.self_hosted as self_hosted +from codecov.commands.exceptions import ( + MissingService, + Unauthenticated, + Unauthorized, + ValidationError, +) +from codecov_auth.helpers import current_user_part_of_org +from codecov_auth.models import Owner, User +from core.models import Repository + + +class BaseCommand: + def __init__(self, current_owner: Owner, service: str, current_user: User = None): + self.current_user = current_user or AnonymousUser() + self.current_owner = current_owner + self.service = service + self.executor = None + + def get_interactor(self, InteractorKlass): + return InteractorKlass( + current_owner=self.current_owner, + service=self.service, + current_user=self.current_user, + ) + + def get_command(self, namespace): + """ + Allow a command to call another command + """ + if not self.executor: + # local import to avoid circular import; I'm not too happy about + # this pattern yet + from .executor import get_executor_from_command + + self.executor = get_executor_from_command(self) + return self.executor.get_command(namespace) + + +class BaseInteractor: + requires_service = True + + def __init__(self, current_owner: Owner, service: str, current_user: User = None): + self.current_user = current_user or AnonymousUser() + self.current_owner = current_owner + self.service = service + + if not self.service and self.requires_service: + raise MissingService() + + if self.current_owner: + self.current_user = self.current_owner.user + + def ensure_is_admin(self, owner: Owner) -> None: + """ + Ensures that the `current_owner` is an admin of `owner`, + or raise `Unauthorized` otherwise. + """ + + if not current_user_part_of_org(self.current_owner, owner): + raise Unauthorized() + + if settings.IS_ENTERPRISE: + if not self_hosted.is_admin_owner(self.current_owner): + raise Unauthorized() + else: + if not owner.is_admin(self.current_owner): + raise Unauthorized() + + def resolve_owner_and_repo( + self, + owner_username: str, + repo_name: str, + ensure_is_admin: bool = False, + only_viewable: bool = False, + only_active: bool = False, + ) -> tuple[Owner, Repository]: + """ + Resolves the `Owner` and `Repository` based on the passed `owner_username` + and `repo_name` respectively. + + If `ensure_is_admin` is set, this will also ensure that the `current_owner` is an + admin on the resolved `Owner`. + """ + if ensure_is_admin and not self.current_user.is_authenticated: + raise Unauthenticated() + + owner = Owner.objects.filter( + service=self.service, username=owner_username + ).first() + + if not owner: + raise ValidationError("Owner not found") + + if ensure_is_admin: + self.ensure_is_admin(owner) + + repo_query = Repository.objects + if only_viewable: + repo_query = repo_query.viewable_repos(self.current_owner) + if only_active: + repo_query = repo_query.filter(active=True) + + repo = repo_query.filter(author=owner, name=repo_name).first() + if not repo: + raise ValidationError("Repo not found") + + return (owner, repo) diff --git a/apps/codecov-api/codecov/commands/exceptions.py b/apps/codecov-api/codecov/commands/exceptions.py new file mode 100644 index 0000000000..ce980a9e68 --- /dev/null +++ b/apps/codecov-api/codecov/commands/exceptions.py @@ -0,0 +1,32 @@ +from graphql import GraphQLError + + +class BaseException(Exception): + pass + + +class Unauthenticated(BaseException): + message = "You are not authenticated" + + +class ValidationError(BaseException): + @property + def message(self): + return str(self) + + +class Unauthorized(BaseException): + message = "You are not authorized" + + +class NotFound(BaseException): + message = "Cant find the requested resource" + + +class MissingService(BaseException): + message = "Missing required service" + + +class UnauthorizedGuestAccess(GraphQLError): + def __init__(self): + super().__init__("Unauthorized", extensions={"status": 403}) diff --git a/apps/codecov-api/codecov/commands/executor.py b/apps/codecov-api/codecov/commands/executor.py new file mode 100644 index 0000000000..6c797595ea --- /dev/null +++ b/apps/codecov-api/codecov/commands/executor.py @@ -0,0 +1,49 @@ +from codecov_auth.commands.owner import OwnerCommands +from codecov_auth.models import Owner, User +from compare.commands.compare import CompareCommands +from core.commands.branch import BranchCommands +from core.commands.commit import CommitCommands +from core.commands.component import ComponentCommands +from core.commands.flag import FlagCommands +from core.commands.pull import PullCommands +from core.commands.repository import RepositoryCommands +from utils.services import get_long_service_name + +mapping = { + "commit": CommitCommands, + "owner": OwnerCommands, + "repository": RepositoryCommands, + "branch": BranchCommands, + "compare": CompareCommands, + "pull": PullCommands, + "flag": FlagCommands, + "component": ComponentCommands, +} + + +class Executor: + def __init__(self, current_owner: Owner, service: str, current_user: User): + self.current_user = current_user + self.current_owner = current_owner + self.service = service + + def get_command(self, namespace): + KlassCommand = mapping[namespace] + return KlassCommand(self.current_owner, self.service, self.current_user) + + +def get_executor_from_request(request): + service_in_url = request.resolver_match.kwargs["service"] + return Executor( + current_owner=request.current_owner, + service=get_long_service_name(service_in_url), + current_user=request.user, + ) + + +def get_executor_from_command(command): + return Executor( + current_owner=command.current_owner, + service=command.service, + current_user=command.current_user, + ) diff --git a/apps/codecov-api/codecov/commands/tests/__init__.py b/apps/codecov-api/codecov/commands/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/codecov/commands/tests/test_base.py b/apps/codecov-api/codecov/commands/tests/test_base.py new file mode 100644 index 0000000000..92e75858e2 --- /dev/null +++ b/apps/codecov-api/codecov/commands/tests/test_base.py @@ -0,0 +1,29 @@ +import pytest +from django.contrib.auth.models import AnonymousUser + +from codecov.commands.exceptions import MissingService +from core.commands.commit import CommitCommands + +from ..base import BaseCommand, BaseInteractor + + +def test_base_command(): + command = BaseCommand(None, "github") + # test command is properly init + assert command.current_owner is None + assert command.service == "github" + # test get_interactor + interactor = command.get_interactor(BaseInteractor) + assert interactor.current_owner is None + assert interactor.current_user == AnonymousUser() + assert interactor.service == "github" + # test get_command + command_command = command.get_command("commit") + assert isinstance(command_command, CommitCommands) + + +def test_base_interactor_with_missing_required_service(): + with pytest.raises(MissingService) as excinfo: + BaseInteractor(None, None) + + assert excinfo.value.message == "Missing required service" diff --git a/apps/codecov-api/codecov/commands/tests/test_executor.py b/apps/codecov-api/codecov/commands/tests/test_executor.py new file mode 100644 index 0000000000..6c54bfcbae --- /dev/null +++ b/apps/codecov-api/codecov/commands/tests/test_executor.py @@ -0,0 +1,26 @@ +from django.contrib.auth.models import AnonymousUser +from django.test import RequestFactory +from django.urls import ResolverMatch + +from codecov_auth.commands.owner import OwnerCommands + +from ..executor import get_executor_from_command, get_executor_from_request + + +def test_get_executor_from_request(): + request_factory = RequestFactory() + request = request_factory.get("") + request.current_owner = None + match = ResolverMatch(func=lambda: None, args=(), kwargs={"service": "gh"}) + request.resolver_match = match + request.user = AnonymousUser() + executor = get_executor_from_request(request) + assert executor.service == "github" + assert executor.current_owner is None + + +def test_get_executor_from_command(): + command = OwnerCommands(None, "github") + executor = get_executor_from_command(command) + assert executor.service == "github" + assert executor.current_owner is None diff --git a/apps/codecov-api/codecov/db/__init__.py b/apps/codecov-api/codecov/db/__init__.py new file mode 100644 index 0000000000..28a91e3aa4 --- /dev/null +++ b/apps/codecov-api/codecov/db/__init__.py @@ -0,0 +1,69 @@ +import logging + +from django.conf import settings +from django.db.models import Field, Lookup + +log = logging.getLogger(__name__) + + +class DatabaseRouter: + """ + A router to control all database operations on models across multiple databases. + https://docs.djangoproject.com/en/4.0/topics/db/multi-db/#automatic-database-routing + """ + + def db_for_read(self, model, **hints): + if model._meta.app_label == "timeseries": + if settings.TIMESERIES_DATABASE_READ_REPLICA_ENABLED: + return "timeseries_read" + else: + return "timeseries" + else: + if settings.DATABASE_READ_REPLICA_ENABLED: + return "default_read" + else: + return "default" + + def db_for_write(self, model, **hints): + if model._meta.app_label == "timeseries": + return "timeseries" + else: + return "default" + + def allow_migrate(self, db, app_label, model_name=None, **hints): + if ( + db == "timeseries" or db == "timeseries_read" + ) and not settings.TIMESERIES_ENABLED: + log.warning("Skipping timeseries migration") + return False + if db == "default_read" or db == "timeseries_read": + log.warning("Skipping migration of read-only database") + return False + if app_label == "timeseries": + return db == "timeseries" + else: + return db == "default" + + def allow_relation(self, obj1, obj2, **hints): + obj1_app = obj1._meta.app_label + obj2_app = obj2._meta.app_label + + # cannot form relationship across default <-> timeseries dbs + if obj1_app == "timeseries" and obj2_app != "timeseries": + return False + if obj1_app != "timeseries" and obj2_app == "timeseries": + return False + + # otherwise we allow it + return True + + +@Field.register_lookup +class IsNot(Lookup): + lookup_name = "isnot" + + def as_sql(self, compiler, connection): + lhs, lhs_params = self.process_lhs(compiler, connection) + rhs, rhs_params = self.process_rhs(compiler, connection) + params = tuple(lhs_params) + tuple(rhs_params) + return "%s is not %s" % (lhs, rhs), params diff --git a/apps/codecov-api/codecov/forms.py b/apps/codecov-api/codecov/forms.py new file mode 100644 index 0000000000..f2d76d861d --- /dev/null +++ b/apps/codecov-api/codecov/forms.py @@ -0,0 +1,46 @@ +from dal import autocomplete +from django import forms +from shared.django_apps.rollouts.models import FeatureFlagVariant + +from codecov_auth.models import Owner +from core.models import Repository + + +class AutocompleteSearchForm(forms.ModelForm): + repository = forms.ModelChoiceField( + queryset=Repository.objects.all(), + widget=autocomplete.ModelSelect2(url="admin-repository-autocomplete"), + required=False, + label="Add repo override", + help_text="Search for a repo and hit `Save and continue editing` to add it", + ) + + owner = forms.ModelChoiceField( + queryset=Owner.objects.all(), + widget=autocomplete.ModelSelect2(url="admin-owner-autocomplete"), + required=False, + label="Add owner override", + help_text="Search for an owner and hit `Save and continue editing` to add it", + ) + + class Meta: + model = FeatureFlagVariant + fields = "__all__" + + def save(self, commit=True): + instance = super(AutocompleteSearchForm, self).save(commit=False) + + if self.cleaned_data["repository"]: + if instance.override_repo_ids is None: + instance.override_repo_ids = [] + instance.override_repo_ids.append(self.cleaned_data["repository"].repoid) + + if self.cleaned_data["owner"]: + if instance.override_owner_ids is None: + instance.override_repo_ids = [] + instance.override_owner_ids.append(self.cleaned_data["owner"].ownerid) + + if commit: + instance.save() + + return instance diff --git a/apps/codecov-api/codecov/models.py b/apps/codecov-api/codecov/models.py new file mode 100644 index 0000000000..ff7e9b937c --- /dev/null +++ b/apps/codecov-api/codecov/models.py @@ -0,0 +1,13 @@ +import uuid + +from django.db import models + + +class BaseCodecovModel(models.Model): + id = models.BigAutoField(primary_key=True) + external_id = models.UUIDField(default=uuid.uuid4, editable=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True diff --git a/apps/codecov-api/codecov/settings_base.py b/apps/codecov-api/codecov/settings_base.py new file mode 100644 index 0000000000..55dbaba7cf --- /dev/null +++ b/apps/codecov-api/codecov/settings_base.py @@ -0,0 +1,456 @@ +import os + +import sentry_sdk +from corsheaders.defaults import default_headers +from sentry_sdk.integrations.celery import CeleryIntegration +from sentry_sdk.integrations.django import DjangoIntegration +from sentry_sdk.integrations.httpx import HttpxIntegration +from sentry_sdk.integrations.redis import RedisIntegration +from sentry_sdk.scrubber import DEFAULT_DENYLIST, EventScrubber +from shared.django_apps.db_settings import * +from shared.license import startup_license_logging + +from utils.config import SettingsModule, get_config, get_settings_module + +SECRET_KEY = get_config("django", "secret_key", default="*") + +# Application definition + +INSTALLED_APPS = [ + "legacy_migrations", + "dal", + "dal_select2", # needs to be ahead of django.contrib.admin + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.postgres", + "django_filters", + "drf_spectacular", + "drf_spectacular_sidecar", + "ariadne_django", + "corsheaders", + "rest_framework", + "billing", + "codecov_auth", + "api", + "compare", + "core", + "graphql_api", + "labelanalysis", + "reports", + "staticanalysis", + "timeseries", + "django_prometheus", + "psqlextra", + "django_better_admin_arrayfield", + # New Shared Models + "shared.django_apps.rollouts", + "shared.django_apps.user_measurements", + "shared.django_apps.codecov_metrics", + "shared.django_apps.bundle_analysis", +] + +MIDDLEWARE = [ + "core.middleware.AppMetricsBeforeMiddlewareWithUA", + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "codecov_auth.middleware.CorsMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "core.middleware.ServiceMiddleware", + "codecov_auth.middleware.CurrentOwnerMiddleware", + "codecov_auth.middleware.ImpersonationMiddleware", + "core.middleware.AppMetricsAfterMiddlewareWithUA", + "csp.middleware.CSPMiddleware", +] + +ROOT_URLCONF = "codecov.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + "templates", + ], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ] + }, + } +] + +WSGI_APPLICATION = "codecov.wsgi.application" + +# GraphQL + +GRAPHQL_QUERY_COST_THRESHOLD = get_config( + "setup", "graphql", "query_cost_threshold", default=10000 +) + +GRAPHQL_RATE_LIMIT_ENABLED = get_config( + "setup", "graphql", "rate_limit_enabled", default=True +) + +GRAPHQL_RATE_LIMIT_RPM = get_config("setup", "graphql", "rate_limit_rpm", default=300) + +GRAPHQL_INTROSPECTION_ENABLED = False + +GRAPHQL_MAX_DEPTH = get_config("setup", "graphql", "max_depth", default=20) + +GRAPHQL_MAX_ALIASES = get_config("setup", "graphql", "max_aliases", default=10) + +# Database +# https://docs.djangoproject.com/en/2.1/ref/settings/#databases + +DATABASE_ROUTERS = ["codecov.db.DatabaseRouter"] + +# GCS +GCS_BUCKET_NAME = get_config("services", "minio", "bucket", default="codecov") + + +# Password validation +# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" + }, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, +] + +PROMETHEUS_EXPORT_MIGRATIONS = False + +REST_FRAMEWORK = { + "DEFAULT_PERMISSION_CLASSES": ( + "rest_framework.permissions.IsAuthenticatedOrReadOnly", + ), + "DEFAULT_AUTHENTICATION_CLASSES": ( + "codecov_auth.authentication.UserTokenAuthentication", + "rest_framework.authentication.BasicAuthentication", + "codecov_auth.authentication.SessionAuthentication", + ), + "DEFAULT_PAGINATION_CLASS": "api.shared.pagination.StandardPageNumberPagination", + "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",), + "PAGE_SIZE": 20, + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", +} + +# API auto-documentation settings +# https://drf-spectacular.readthedocs.io/en/latest/settings.html +SPECTACULAR_SETTINGS = { + "TITLE": "Codecov API", + "DESCRIPTION": "Public Codecov API", + "VERSION": "2.0.0", + "SERVE_INCLUDE_SCHEMA": False, + "SERVE_URLCONF": "api.public.v2.urls", + "SERVERS": [{"url": "/api/v2"}], + "AUTHENTICATION_WHITELIST": [ + "codecov_auth.authentication.UserTokenAuthentication", + ], + "REDOC_DIST": "SIDECAR", # serve Redoc from Django (not CDN) +} + +# The frame-ancestors directive restricts the URLs which can embed the resource using +# frame, iframe, object, or embed. This configuration denies doing so. +CSP_FRAME_ANCESTORS = "'none'" + +# Allows GraphQL Playground to render +CSP_DEFAULT_SRC = [ + "'self'", + "'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='", + "'sha256-eKdXhLyOdPl2/gp1Ob116rCU2Ox54rseyz1MwCmzb6w='", + "'sha256-a1pELtDJXf8fPX1YL2JiBM91RQBeIAswunzgwMEsvwA='", + "'sha256-cNIcuS0BVLuBVP5rpfeFE42xHz7r5hMyf9YdfknWuCg='", + "'sha256-bmwAzHxhO1mBINfkKkKPopyKEv4ppCHx/z84wQJ9nOY='", + "'sha256-jQoC6QpIonlMBPFbUGlJFRJFFWbbijMl7Z8XqWrb46o='", + "'sha256-8bfsSkoReSu+APs9CT6QDx2VMgtiw9/lrZLMZNUmhc0='", + "'sha256-WoezM4J4TynepdEmsbDslXjZN6zQbvY3M0cX0ujGGUo='", + "'sha256-OdsouMbSygCsanIn5RN0skp0SiM8rjt4PqX+YWPG3d0='", + "https://cdn.jsdelivr.net/npm/graphql-playground-react/build/static/js/middleware.js", + "https://cdn.jsdelivr.net/npm/graphql-playground-react/build/favicon.png", + "https://cdn.jsdelivr.net/npm/graphql-playground-react/build/static/css/index.css", + "blob:", +] + +CSP_WORKER_SRC = ["'self'", "blob:"] + +# Internationalization +# https://docs.djangoproject.com/en/2.1/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.1/howto/static-files/ + +PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) +STATIC_URL = "/static/" +STATIC_ROOT = os.path.join(PROJECT_ROOT, "static") + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "standard": { + "format": "%(message)s %(asctime)s %(name)s %(levelname)s %(lineno)s %(pathname)s %(funcName)s %(threadName)s", + "class": "utils.logging_configuration.CustomLocalJsonFormatter", + }, + "json": { + "format": "%(message)s %(asctime)s %(name)s %(levelname)s %(lineno)s %(pathname)s %(funcName)s %(threadName)s", + "class": "utils.logging_configuration.CustomDatadogJsonFormatter", + }, + "gunicorn_json": { + "class": "utils.logging_configuration.CustomGunicornLogFormatter", + "datefmt": "%Y-%m-%dT%H:%M:%S%z", + "format": '%(h)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"', + }, + }, + "filters": { + "health_check_filter": {"()": "utils.logging_configuration.HealthCheckFilter"} + }, + "root": {"handlers": ["default"], "level": "INFO", "propagate": True}, + "handlers": { + "default": { + "level": "INFO", + "formatter": ( + "standard" + if get_settings_module() == SettingsModule.DEV.value + else "json" + ), + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", # Default is stderr + }, + "json-gunicorn-console": { + "level": "INFO", + "formatter": "gunicorn_json", + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", # Default is stderr + "filters": ["health_check_filter"], + }, + }, + "loggers": { + "gunicorn.access": { + "level": "INFO", + "handlers": ["json-gunicorn-console"], + } + }, +} + +MINIO_ACCESS_KEY = get_config("services", "minio", "access_key_id") +MINIO_SECRET_KEY = get_config("services", "minio", "secret_access_key") +MINIO_LOCATION = "codecov.s3.amazonaws.com" +MINIO_HASH_KEY = get_config("services", "minio", "hash_key") +ARCHIVE_BUCKET_NAME = "codecov" +ENCRYPTION_SECRET = get_config("setup", "encryption_secret") + +COOKIE_SAME_SITE = "Lax" +COOKIE_SECRET = get_config("setup", "http", "cookie_secret") +COOKIES_DOMAIN = get_config("setup", "http", "cookies_domain", default=".codecov.io") +SESSION_COOKIE_DOMAIN = get_config( + "setup", "http", "cookies_domain", default=".codecov.io" +) +SESSION_COOKIE_SECURE = get_config("setup", "secure_cookie", default=True) +# Defaulting to 'not found' as opposed to 'None' to avoid None somehow getting through as a bearer token. Token strings can't have spaces, hence 'not found' can never be forced as a header input value +SUPER_API_TOKEN = os.getenv("SUPER_API_TOKEN", "not found") +CODECOV_INTERNAL_TOKEN = os.getenv("CODECOV_INTERNAL_TOKEN", "not found") + +CIRCLECI_TOKEN = get_config("circleci", "token") + +GITHUB_CLIENT_ID = get_config("github", "client_id") +GITHUB_CLIENT_SECRET = get_config("github", "client_secret") +GITHUB_BOT_KEY = get_config("github", "bot", "key") +GITHUB_TOKENLESS_BOT_KEY = get_config( + "github", "bots", "tokenless", "key", default=GITHUB_BOT_KEY +) +GITHUB_ACTIONS_TOKEN = get_config("github", "actions_token") + +GITHUB_ENTERPRISE_URL = get_config("github_enterprise", "url") +GITHUB_ENTERPRISE_API_URL = get_config("github_enterprise", "api_url") +GITHUB_ENTERPRISE_CLIENT_ID = get_config("github_enterprise", "client_id") +GITHUB_ENTERPRISE_CLIENT_SECRET = get_config("github_enterprise", "client_secret") +GITHUB_ENTERPRISE_BOT_KEY = get_config("github_enterprise", "bot", "key") +GITHUB_ENTERPRISE_TOKENLESS_BOT_KEY = get_config( + "github_enterprise", "bots", "tokenless", "key", default=GITHUB_ENTERPRISE_BOT_KEY +) +GITHUB_ENTERPRISE_ACTIONS_TOKEN = get_config("github_enterprise", "actions_token") + +BITBUCKET_CLIENT_ID = get_config("bitbucket", "client_id") +BITBUCKET_CLIENT_SECRET = get_config("bitbucket", "client_secret") +BITBUCKET_BOT_KEY = get_config("bitbucket", "bot", "key") +BITBUCKET_TOKENLESS_BOT_KEY = get_config( + "bitbucket", "bots", "tokenless", "key", default=BITBUCKET_BOT_KEY +) +BITBUCKET_REDIRECT_URI = get_config( + "bitbucket", "redirect_uri", default="https://codecov.io/login/bitbucket" +) + +BITBUCKET_SERVER_URL = get_config("bitbucket_server", "url") +BITBUCKET_SERVER_CLIENT_ID = get_config("bitbucket_server", "client_id") +BITBUCKET_SERVER_CLIENT_SECRET = get_config("bitbucket_server", "client_secret") +BITBUCKET_SERVER_BOT_KEY = get_config("bitbucket_server", "bot", "key") +BITBUCKET_SERVER_TOKENLESS_BOT_KEY = get_config( + "bitbucket_server", "bots", "tokenless", "key", default=BITBUCKET_SERVER_BOT_KEY +) + +GITLAB_CLIENT_ID = get_config("gitlab", "client_id") +GITLAB_CLIENT_SECRET = get_config("gitlab", "client_secret") +GITLAB_REDIRECT_URI = get_config( + "gitlab", "redirect_uri", default="https://codecov.io/login/gitlab" +) +GITLAB_SCOPE = get_config("gitlab", "scope", default="api") +GITLAB_BOT_KEY = get_config("gitlab", "bot", "key") +GITLAB_TOKENLESS_BOT_KEY = get_config( + "gitlab", "bots", "tokenless", "key", default=GITLAB_BOT_KEY +) + +GITLAB_ENTERPRISE_CLIENT_ID = get_config("gitlab_enterprise", "client_id") +GITLAB_ENTERPRISE_CLIENT_SECRET = get_config("gitlab_enterprise", "client_secret") +GITLAB_ENTERPRISE_REDIRECT_URI = get_config( + "gitlab_enterprise", + "redirect_uri", + default="https://codecov.io/login/gitlab_enterprise", +) +GITLAB_ENTERPRISE_BOT_KEY = get_config("gitlab_enterprise", "bot", "key") +GITLAB_ENTERPRISE_TOKENLESS_BOT_KEY = get_config( + "gitlab_enterprise", "bots", "tokenless", "key", default=GITLAB_ENTERPRISE_BOT_KEY +) +GITLAB_ENTERPRISE_URL = get_config("gitlab_enterprise", "url") +GITLAB_ENTERPRISE_API_URL = get_config("gitlab_enterprise", "api_url") + +CORS_ALLOW_HEADERS = ( + list(default_headers) + + ["token-type"] + + get_config("setup", "api_cors_extra_headers", default=["baggage"]) +) + +SKIP_RISKY_MIGRATION_STEPS = get_config("migrations", "skip_risky_steps", default=False) + +DJANGO_ADMIN_URL = get_config("django", "admin_url", default="admin") + +IS_ENTERPRISE = get_settings_module() == SettingsModule.ENTERPRISE.value +IS_DEV = get_settings_module() == SettingsModule.DEV.value + +DATA_UPLOAD_MAX_MEMORY_SIZE = int( + get_config("setup", "http", "upload_max_memory_size", default=2621440) +) +FILE_UPLOAD_MAX_MEMORY_SIZE = int( + get_config("setup", "http", "file_upload_max_memory_size", default=2621440) +) + +CORS_ALLOWED_ORIGIN_REGEXES = get_config( + "setup", "api_cors_allowed_origin_regexes", default=[] +) +CORS_ALLOWED_ORIGINS: list[str] = [] + +GRAPHQL_PLAYGROUND = get_settings_module() in [ + SettingsModule.DEV.value, + SettingsModule.STAGING.value, + SettingsModule.TESTING.value, +] + +UPLOAD_THROTTLING_ENABLED = get_config( + "setup", "upload_throttling_enabled", default=True +) + +HIDE_ALL_CODECOV_TOKENS = get_config("setup", "hide_all_codecov_tokens", default=False) + +SENTRY_JWT_SHARED_SECRET = get_config( + "sentry", "jwt_shared_secret", default=None +) or get_config("setup", "sentry", "jwt_shared_secret", default=None) +SENTRY_USER_WEBHOOK_URL = get_config( + "sentry", "webhook_url", default=None +) or get_config("setup", "sentry", "webhook_url", default=None) +SENTRY_OAUTH_CLIENT_ID = get_config("sentry", "client_id") or get_config( + "setup", "sentry", "oauth_client_id" +) +SENTRY_OAUTH_CLIENT_SECRET = get_config("sentry", "client_secret") or get_config( + "setup", "sentry", "oauth_client_secret" +) +SENTRY_OIDC_SHARED_SECRET = get_config("sentry", "oidc_shared_secret") or get_config( + "setup", "sentry", "oidc_shared_secret" +) + +OKTA_OAUTH_CLIENT_ID = get_config("setup", "okta", "oauth_client_id") +OKTA_OAUTH_CLIENT_SECRET = get_config("setup", "okta", "oauth_client_secret") +OKTA_OAUTH_REDIRECT_URL = get_config("setup", "okta", "oauth_redirect_url") +OKTA_ISS = get_config("setup", "okta", "iss", default=None) + +DISABLE_GIT_BASED_LOGIN = IS_ENTERPRISE and get_config( + "setup", "disable_git_based_login", default=False +) + +SHELTER_SHARED_SECRET = get_config("setup", "shelter_shared_secret", default=None) +SHELTER_ENABLED = get_config("setup", "shelter_enabled", default=True) + +SENTRY_ENV = os.environ.get("CODECOV_ENV", False) +SENTRY_DSN = os.environ.get("SERVICES__SENTRY__SERVER_DSN", None) +SENTRY_DENY_LIST = DEFAULT_DENYLIST + ["_headers", "token_to_use"] + +if SENTRY_DSN is not None: + SENTRY_SAMPLE_RATE = float(os.environ.get("SERVICES__SENTRY__SAMPLE_RATE", 0.1)) + sentry_sdk.init( + dsn=SENTRY_DSN, + event_scrubber=EventScrubber(denylist=SENTRY_DENY_LIST), + integrations=[ + DjangoIntegration(), + CeleryIntegration(), + RedisIntegration(), + HttpxIntegration(), + ], + environment=SENTRY_ENV, + traces_sample_rate=SENTRY_SAMPLE_RATE, + profiles_sample_rate=float( + os.environ.get("SERVICES__SENTRY__PROFILE_SAMPLE_RATE", 0.01) + ), + ) + if os.getenv("CLUSTER_ENV"): + sentry_sdk.set_tag("cluster", os.getenv("CLUSTER_ENV")) +elif IS_DEV: + sentry_sdk.init( + spotlight=IS_DEV, + event_scrubber=EventScrubber(denylist=SENTRY_DENY_LIST), + ) + +SHELTER_PUBSUB_PROJECT_ID = get_config("setup", "shelter", "pubsub_project_id") +SHELTER_PUBSUB_SYNC_REPO_TOPIC_ID = get_config("setup", "shelter", "sync_repo_topic_id") + +STRIPE_PAYMENT_METHOD_CONFIGURATION_ID = get_config( + "setup", "stripe", "payment_method_configuration_id", default=None +) + +AMPLITUDE_API_KEY = os.environ.get("AMPLITUDE_API_KEY", None) + +# Allows to do migrations from another module +MIGRATION_MODULES = { + "codecov_auth": "shared.django_apps.codecov_auth.migrations", + "compare": "shared.django_apps.compare.migrations", + "core": "shared.django_apps.core.migrations", + "labelanalysis": "shared.django_apps.labelanalysis.migrations", + "legacy_migrations": "shared.django_apps.legacy_migrations.migrations", + "profiling": "shared.django_apps.profiling.migrations", + "reports": "shared.django_apps.reports.migrations", + "staticanalysis": "shared.django_apps.staticanalysis.migrations", + "timeseries": "shared.django_apps.timeseries.migrations", +} + +# to aid in debugging, print out this info on startup. If no license, prints nothing +startup_license_logging() diff --git a/apps/codecov-api/codecov/settings_dev.py b/apps/codecov-api/codecov/settings_dev.py new file mode 100644 index 0000000000..dc882f4ef9 --- /dev/null +++ b/apps/codecov-api/codecov/settings_dev.py @@ -0,0 +1,42 @@ +from .settings_base import * + +# Remove CSP headers from local development build to allow GQL Playground +MIDDLEWARE.remove("csp.middleware.CSPMiddleware") + +DEBUG = True +# for shelter add "host.docker.internal" and make sure to map it to localhost +# in your /etc/hosts +ALLOWED_HOSTS = get_config( + "setup", + "api_allowed_hosts", + default=["localhost", "api.lt.codecov.dev", "host.docker.internal"], +) + +WEBHOOK_URL = "" # NGROK TUNNEL HERE + +STRIPE_API_KEY = get_config("services", "stripe", "api_key", default="default") +STRIPE_ENDPOINT_SECRET = get_config( + "services", "stripe", "endpoint_secret", default="default" +) + +CORS_ALLOW_CREDENTIALS = True + +CODECOV_URL = "localhost" +CODECOV_API_URL = get_config("setup", "codecov_api_url", default=CODECOV_URL) +CODECOV_DASHBOARD_URL = "http://localhost:3000" + +CORS_ALLOWED_ORIGINS = [ + CODECOV_DASHBOARD_URL, + "http://localhost", + "http://localhost:9000", +] + +COOKIES_DOMAIN = "localhost" +SESSION_COOKIE_DOMAIN = "localhost" + +# add for shelter +# SHELTER_SHARED_SECRET = "test-supertoken" + +GUEST_ACCESS = True + +GRAPHQL_INTROSPECTION_ENABLED = True diff --git a/apps/codecov-api/codecov/settings_enterprise.py b/apps/codecov-api/codecov/settings_enterprise.py new file mode 100644 index 0000000000..5fd3991d40 --- /dev/null +++ b/apps/codecov-api/codecov/settings_enterprise.py @@ -0,0 +1,76 @@ +import os + +from utils.config import get_config + +from .settings_base import * + +DEBUG = False +THIS_POD_IP = os.environ.get("THIS_POD_IP") +ALLOWED_HOSTS = get_config("setup", "api_allowed_hosts", default=["*"]) +if THIS_POD_IP: + ALLOWED_HOSTS.append(THIS_POD_IP) +CORS_ALLOW_CREDENTIALS = True +# Setting default to localhost to avoid errors when running compilation steps. +# This is "fine" because the app surely won't be in a working state without a valid url. +CODECOV_URL = get_config("setup", "codecov_url", default="http://localhost") +CODECOV_API_URL = get_config("setup", "codecov_api_url", default=CODECOV_URL) + +DEFAULT_TRUSTED_ORIGIN = None + +# select out CODECOV_URL domain +if CODECOV_URL.startswith("https://"): + DEFAULT_WHITELISTED_DOMAIN = CODECOV_URL[8:] +elif CODECOV_URL.startswith("http://"): + DEFAULT_WHITELISTED_DOMAIN = CODECOV_URL[7:] +# select out CODECOV_API_URL domain +if CODECOV_API_URL.startswith("https://"): + API_DOMAIN = CODECOV_API_URL[8:] + DEFAULT_TRUSTED_ORIGIN = f"https://*.{API_DOMAIN}" +elif CODECOV_API_URL.startswith("http://"): + API_DOMAIN = CODECOV_API_URL[7:] + DEFAULT_TRUSTED_ORIGIN = f"http://*.{API_DOMAIN}" + +CORS_ALLOWED_ORIGINS = get_config( + "setup", "api_cors_allowed_origins", default=[CODECOV_URL] +) +ALLOWED_HOSTS.append(DEFAULT_WHITELISTED_DOMAIN) +# only add api domain if it is different than codecov url +if API_DOMAIN != DEFAULT_WHITELISTED_DOMAIN: + ALLOWED_HOSTS.append(API_DOMAIN) +# Referenced at module level of services/billing.py, so it needs to be defined +STRIPE_API_KEY = None +SILENCED_SYSTEM_CHECKS = ["urls.W002"] +UPLOAD_THROTTLING_ENABLED = False + +BITBUCKET_REDIRECT_URI = get_config( + "bitbucket", "redirect_uri", default=f"{CODECOV_URL}/login/bitbucket" +) +GITLAB_REDIRECT_URI = get_config( + "gitlab", "redirect_uri", default=f"{CODECOV_URL}/login/gitlab" +) + + +GITLAB_ENTERPRISE_REDIRECT_URI = get_config( + "gitlab_enterprise", + "redirect_uri", + default=f"{CODECOV_URL}/login/gle", +) + +CODECOV_DASHBOARD_URL = get_config( + "setup", "codecov_dashboard_url", default=CODECOV_URL +) + +COOKIES_DOMAIN = get_config( + "setup", "http", "cookies_domain", default=f".{DEFAULT_WHITELISTED_DOMAIN}" +) +SESSION_COOKIE_DOMAIN = COOKIES_DOMAIN + +ADMINS_LIST = get_config("setup", "admins", default=[]) + +CSRF_TRUSTED_ORIGINS = [ + get_config("setup", "trusted_origin", default=DEFAULT_TRUSTED_ORIGIN) +] + +GUEST_ACCESS = get_config("setup", "guest_access", default=True) + +SHELTER_ENABLED = get_config("setup", "shelter_enabled", default=False) diff --git a/apps/codecov-api/codecov/settings_prod.py b/apps/codecov-api/codecov/settings_prod.py new file mode 100644 index 0000000000..ca3c2c91d6 --- /dev/null +++ b/apps/codecov-api/codecov/settings_prod.py @@ -0,0 +1,45 @@ +import os + +from .settings_base import * + +DEBUG = False +THIS_POD_IP = os.environ.get("THIS_POD_IP") +ALLOWED_HOSTS = get_config( + "setup", "api_allowed_hosts", default=["codecov.io-shadow", ".codecov.io"] +) +if THIS_POD_IP: + ALLOWED_HOSTS.append(THIS_POD_IP) + +WEBHOOK_URL = get_config("setup", "webhook_url", default="https://codecov.io") + + +STRIPE_API_KEY = os.environ.get("SERVICES__STRIPE__API_KEY", None) +STRIPE_ENDPOINT_SECRET = os.environ.get("SERVICES__STRIPE__ENDPOINT_SECRET", None) + +CORS_ALLOW_HEADERS += ["sentry-trace", "baggage"] +CORS_ALLOW_CREDENTIALS = True +CODECOV_URL = get_config("setup", "codecov_url", default="https://codecov.io") +CODECOV_API_URL = get_config("setup", "codecov_api_url", default=CODECOV_URL) +CODECOV_DASHBOARD_URL = get_config( + "setup", "codecov_dashboard_url", default="https://app.codecov.io" +) +CORS_ALLOWED_ORIGINS = [ + CODECOV_URL, + CODECOV_DASHBOARD_URL, + "https://gazebo.netlify.app", # to access unreleased URL of gazebo +] +# We are also using the CORS settings to verify if the domain is safe to +# Redirect after authentication, update this setting with care +CORS_ALLOWED_ORIGIN_REGEXES = [] + +# 25MB in bytes +DATA_UPLOAD_MAX_MEMORY_SIZE = 26214400 + +SILENCED_SYSTEM_CHECKS = ["urls.W002"] + +# Reinforcing the Cookie SameSite configuration to be sure it's Lax in prod +COOKIE_SAME_SITE = "Lax" + +CSRF_TRUSTED_ORIGINS = [ + get_config("setup", "trusted_origin", default="https://*.codecov.io") +] diff --git a/apps/codecov-api/codecov/settings_staging.py b/apps/codecov-api/codecov/settings_staging.py new file mode 100644 index 0000000000..1049e192de --- /dev/null +++ b/apps/codecov-api/codecov/settings_staging.py @@ -0,0 +1,65 @@ +import os + +from .settings_base import * + +DEBUG = False +THIS_POD_IP = os.environ.get("THIS_POD_IP") +ALLOWED_HOSTS = get_config( + "setup", "api_allowed_hosts", default=["stage-api.codecov.dev"] +) +if THIS_POD_IP: + ALLOWED_HOSTS.append(THIS_POD_IP) + +WEBHOOK_URL = get_config( + "setup", "webhook_url", default="https://stage-api.codecov.dev" +) + +STRIPE_API_KEY = os.environ.get("SERVICES__STRIPE__API_KEY", None) +STRIPE_ENDPOINT_SECRET = os.environ.get("SERVICES__STRIPE__ENDPOINT_SECRET", None) +COOKIES_DOMAIN = ".codecov.dev" +SESSION_COOKIE_DOMAIN = ".codecov.dev" + +CORS_ALLOW_HEADERS += ["sentry-trace", "baggage"] +CORS_ALLOWED_ORIGIN_REGEXES = [ + r"^(https:\/\/)?deploy-preview-\d+--codecov\.netlify\.app$", + r"^(https:\/\/)?deploy-preview-\d+--stage-app\.netlify\.app$", + r"^(https:\/\/)?deploy-preview-\d+--codecov-stage\.netlify\.app$", + r"^(https:\/\/)?deploy-preview-\d+--gazebo\.netlify\.app$", + r"^(https:\/\/)?deploy-preview-\d+--gazebo-staging\.netlify\.app$", + r"^(https:\/\/)?\w+--gazebo\.netlify\.app$", + r"^(https:\/\/)?preview-[\w\d\-]+\.codecov\.dev$", +] +CORS_ALLOW_CREDENTIALS = True + +CODECOV_URL = get_config( + "setup", "codecov_url", default="https://stage-web.codecov.dev" +) +CODECOV_API_URL = get_config("setup", "codecov_api_url", default=CODECOV_URL) +CODECOV_DASHBOARD_URL = get_config( + "setup", "codecov_dashboard_url", default="https://stage-app.codecov.dev" +) +CORS_ALLOWED_ORIGINS = [ + CODECOV_URL, + CODECOV_DASHBOARD_URL, + "https://gazebo.netlify.app", + "https://gazebo-staging.netlify.app", + "http://localhost:3000", +] + +# 25MB in bytes +DATA_UPLOAD_MAX_MEMORY_SIZE = 26214400 + +GRAPHQL_RATE_LIMIT_RPM = get_config("setup", "graphql", "rate_limit_rpm", default=1000) + +# Same site is set to none on Staging as we want to be able to call the API +# From Netlify preview deploy +COOKIE_SAME_SITE = "None" +SESSION_COOKIE_SAMESITE = "None" + +GCS_BUCKET_NAME = get_config("services", "minio", "bucket", default="codecov-staging") + +CSRF_TRUSTED_ORIGINS = [ + get_config("setup", "trusted_origin", default="https://*.codecov.dev") +] + +GRAPHQL_INTROSPECTION_ENABLED = True diff --git a/apps/codecov-api/codecov/settings_test.py b/apps/codecov-api/codecov/settings_test.py new file mode 100644 index 0000000000..2e98ad868a --- /dev/null +++ b/apps/codecov-api/codecov/settings_test.py @@ -0,0 +1,15 @@ +import os + +from .settings_dev import * + +ALLOWED_HOSTS = ["localhost"] +CORS_ALLOWED_ORIGINS = ["http://localhost:9000", "http://localhost"] +SHELTER_ENABLED = True +SHELTER_PUBSUB_PROJECT_ID = "test-project-id" +SHELTER_PUBSUB_SYNC_REPO_TOPIC_ID = "test-topic-id" + +# Mock the Pub/Sub host for testing +# this prevents the pubsub SDK from trying to load credentials +os.environ["PUBSUB_EMULATOR_HOST"] = "localhost" + +GRAPHQL_INTROSPECTION_ENABLED = True diff --git a/apps/codecov-api/codecov/static/__init__.py b/apps/codecov-api/codecov/static/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/codecov/tests/__init__.py b/apps/codecov-api/codecov/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/codecov/tests/base_test.py b/apps/codecov-api/codecov/tests/base_test.py new file mode 100644 index 0000000000..3eca9ab304 --- /dev/null +++ b/apps/codecov-api/codecov/tests/base_test.py @@ -0,0 +1,18 @@ +import json + +from django.conf import settings +from django.test import TestCase + + +class InternalAPITest(TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # internal apis are behind a debug flag currently + # and django/pytest set DEBUG to false by default + # https://docs.djangoproject.com/en/dev/topics/testing/overview/#other-test-conditions + settings.DEBUG = False + + @staticmethod + def json_content(response): + return json.loads(response.content.decode()) diff --git a/apps/codecov-api/codecov/tests/test_urls.py b/apps/codecov-api/codecov/tests/test_urls.py new file mode 100644 index 0000000000..2ec92c2194 --- /dev/null +++ b/apps/codecov-api/codecov/tests/test_urls.py @@ -0,0 +1,9 @@ +from django.test import TestCase +from django.test.client import Client + + +class ViewTest(TestCase): + def test_health(self): + client = Client() + response = client.get("") + assert response.status_code == 200 diff --git a/apps/codecov-api/codecov/tests/test_views.py b/apps/codecov-api/codecov/tests/test_views.py new file mode 100644 index 0000000000..ac5a923dd0 --- /dev/null +++ b/apps/codecov-api/codecov/tests/test_views.py @@ -0,0 +1,137 @@ +from json import loads + +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from codecov_auth.models import Service, User + + +class OwnerAutocompleteSearchTest(TestCase): + def setUp(self): + self.user = User.objects.create(name="staff", is_staff=True) + self.unauthorized_user = User.objects.create(name="nonstaff", is_staff=False) + OwnerFactory(service=Service.GITHUB, service_id=1, username="user1") + OwnerFactory(service=Service.GITLAB, service_id=2, username="user2") + OwnerFactory(service=Service.GITHUB, service_id=3, username="user3") + + def test_unauthorized_access(self): + self.client.force_login(self.unauthorized_user) + response = self.client.get("/admin-owner-autocomplete/", {"q": "github/user1"}) + json_string = response._container[0].decode("utf-8") + data = loads(json_string) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(data["results"]), 0) + + def test_search_by_two_terms(self): + self.client.force_login(self.user) + response = self.client.get("/admin-owner-autocomplete/", {"q": "github/user1"}) + json_string = response._container[0].decode("utf-8") + data = loads(json_string) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(data["results"]), 1) + + def test_search_by_one_term_service(self): + self.client.force_login(self.user) + response = self.client.get("/admin-owner-autocomplete/", {"q": "github"}) + json_string = response._container[0].decode("utf-8") + data = loads(json_string) + + self.assertEqual(response.status_code, 200) + self.assertTrue(len(data["results"]) == 2) + + def test_search_by_one_term_owner(self): + self.client.force_login(self.user) + response = self.client.get("/admin-owner-autocomplete/", {"q": "user1"}) + json_string = response._container[0].decode("utf-8") + data = loads(json_string) + + self.assertEqual(response.status_code, 200) + self.assertTrue(len(data["results"]) == 1) + + +class RepositoryAutocompleteSearchTest(TestCase): + def setUp(self): + self.user = User.objects.create(name="staff", is_staff=True) + self.unauthorized_user = User.objects.create(name="nonstaff", is_staff=False) + + a = OwnerFactory(service=Service.GITHUB, service_id=4, username="user1") + b = OwnerFactory(service=Service.GITHUB, service_id=5, username="user3") + c = OwnerFactory(service=Service.GITLAB, service_id=6, username="user2") + + RepositoryFactory(author=a, name="repo1") + RepositoryFactory(author=a, name="repo2") + RepositoryFactory(author=b, name="repo3") + RepositoryFactory(author=c, name="repo4") + + def test_unauthorized_access(self): + self.client.force_login(self.unauthorized_user) + response = self.client.get( + "/admin-repository-autocomplete/", {"q": Service.GITHUB} + ) + json_string = response._container[0].decode("utf-8") + data = loads(json_string) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(data["results"]), 0) + + def test_search_by_three_terms(self): + self.client.force_login(self.user) + response = self.client.get( + "/admin-repository-autocomplete/", {"q": "github/user1/repo"} + ) + json_string = response._container[0].decode("utf-8") + data = loads(json_string) + + self.assertEqual(response.status_code, 200) + self.assertTrue(len(data["results"]) == 2) + + def test_search_by_three_terms_invalid_service(self): + self.client.force_login(self.user) + response = self.client.get( + "/admin-repository-autocomplete/", {"q": "geehub/user1/repo"} + ) + json_string = response._container[0].decode("utf-8") + data = loads(json_string) + + self.assertEqual(response.status_code, 200) + self.assertTrue(len(data["results"]) == 0) + + def test_search_by_two_terms_service(self): + self.client.force_login(self.user) + response = self.client.get( + "/admin-repository-autocomplete/", {"q": "github/user1"} + ) + json_string = response._container[0].decode("utf-8") + data = loads(json_string) + + self.assertEqual(response.status_code, 200) + self.assertTrue(len(data["results"]) == 2) + + def test_search_by_two_terms_owner(self): + self.client.force_login(self.user) + response = self.client.get("/admin-repository-autocomplete/", {"q": "user2/re"}) + json_string = response._container[0].decode("utf-8") + data = loads(json_string) + + self.assertEqual(response.status_code, 200) + self.assertTrue(len(data["results"]) == 1) + + def test_search_by_one_term_repo(self): + self.client.force_login(self.user) + response = self.client.get("/admin-repository-autocomplete/", {"q": "repo4"}) + json_string = response._container[0].decode("utf-8") + data = loads(json_string) + + self.assertEqual(response.status_code, 200) + self.assertTrue(len(data["results"]) == 1) + + def test_search_by_one_term_service(self): + self.client.force_login(self.user) + response = self.client.get("/admin-repository-autocomplete/", {"q": "github"}) + json_string = response._container[0].decode("utf-8") + data = loads(json_string) + + self.assertEqual(response.status_code, 200) + self.assertTrue(len(data["results"]) == 3) diff --git a/apps/codecov-api/codecov/urls.py b/apps/codecov-api/codecov/urls.py new file mode 100644 index 0000000000..5274507d2f --- /dev/null +++ b/apps/codecov-api/codecov/urls.py @@ -0,0 +1,40 @@ +from django.conf import settings +from django.contrib import admin +from django.urls import include, path, re_path + +from api.internal.constants import INTERNAL_API_PREFIX +from codecov import views + +urlpatterns = [ + path("billing/", include("billing.urls")), + path("api/v2/", include("api.public.v2.urls")), + path("api/v1/", include("api.public.v1.urls")), + path("api/", include("api.public.v1.urls")), # for backwards compat + path(INTERNAL_API_PREFIX, include("api.internal.urls")), + re_path("^validate/?", include("validate.urls")), + path("health/", views.health), + path("api_health/", views.health), + path("", views.health), + path("///", include("graphs.urls")), + path("upload/", include("upload.urls")), + path("webhooks/", include("webhook_handlers.urls")), + path("graphql/", include("graphql_api.urls")), + path("", include("codecov_auth.urls")), + path(f"{settings.DJANGO_ADMIN_URL}/", admin.site.urls), + path("staticanalysis/", include("staticanalysis.urls")), + path("labels/", include("labelanalysis.urls")), + path( + "admin-repository-autocomplete/", + views.RepositoryAutoCompleteSearch.as_view(), + name="admin-repository-autocomplete", + ), + path( + "admin-owner-autocomplete/", + views.OwnerAutoCompleteSearch.as_view(), + name="admin-owner-autocomplete", + ), + # /monitoring/metrics will be a public route unless you take steps at a + # higher level to null-route or redirect it. + path("monitoring/", include("django_prometheus.urls")), + path("gen_ai/", include("api.gen_ai.urls")), +] diff --git a/apps/codecov-api/codecov/views.py b/apps/codecov-api/codecov/views.py new file mode 100644 index 0000000000..08051037fc --- /dev/null +++ b/apps/codecov-api/codecov/views.py @@ -0,0 +1,119 @@ +from dal import autocomplete +from django.db import connection +from django.http import HttpResponse + +from codecov_auth.models import Owner, Service +from core.models import Constants, Repository + +_version = None + + +def _get_version(): + global _version + if _version is None: + _version = Constants.objects.get(key="version") + return _version + + +def health(request): + # will raise if connection cannot be established + connection.ensure_connection() + + version = _get_version() + return HttpResponse("%s is live!" % version.value) + + +SERVICE_CHOICES = dict(Service.choices) + + +class RepositoryAutoCompleteSearch(autocomplete.Select2QuerySetView): + def get_queryset(self): + # must be authorized to query + if not self.request.user.is_staff: + return Repository.objects.none() + + repos = Repository.objects.all() + + terms = self.q.split("/") if self.q else [] + + if len(terms) >= 3: + repos = self.filter_repos_with_three_terms(terms, repos) + elif len(terms) == 2: + repos = self.filter_repos_with_two_terms(terms, repos) + elif len(terms) == 1: + repos = self.filter_repos_with_one_terms(terms, repos) + + return repos + + def filter_repos_with_three_terms(self, terms, repos): + assert len(terms) >= 3 + + service = terms[0] + + if service not in SERVICE_CHOICES: + return Repository.objects.none() + + owner = terms[1] + repo = "/".join(terms[2:]) + return repos.filter( + author__service=service, author__username=owner, name__startswith=repo + ) + + def filter_repos_with_two_terms(self, terms, repos): + assert len(terms) == 2 + + if terms[0] in SERVICE_CHOICES: + service = terms[0] + owner = terms[1] + return repos.filter( + author__service=service, author__username__startswith=owner + ) + else: + owner = terms[0] + repo = terms[1] + return repos.filter(author__username=owner, name__startswith=repo) + + def filter_repos_with_one_terms(self, terms, repos): + assert len(terms) == 1 + + if terms[0] in SERVICE_CHOICES: + service = terms[0] + return repos.filter(author__service=service) + else: + repo = terms[0] + return repos.filter(name__startswith=repo) + + +class OwnerAutoCompleteSearch(autocomplete.Select2QuerySetView): + def get_queryset(self): + # must be authorized to query + if not self.request.user.is_staff: + return Owner.objects.none() + + owners = Owner.objects.all() + + terms = self.q.split("/") if self.q else [] + + if len(terms) >= 2: + owners = self.filter_owners_with_two_terms(terms, owners) + elif len(terms) == 1: + owners = self.filter_owners_with_one_term(terms, owners) + + return owners + + def filter_owners_with_two_terms(self, terms, owners): + assert len(terms) >= 2 + + service = terms[0] + username = "/".join(terms[1:]) + return owners.filter(service=service, username__startswith=username) + + def filter_owners_with_one_term(self, terms, owners): + assert len(terms) == 1 + + if terms[0] in SERVICE_CHOICES: + service = terms[0] + return owners.filter(service=service) + else: + username = terms[0] + return owners.filter(username__startswith=username) diff --git a/apps/codecov-api/codecov/wsgi.py b/apps/codecov-api/codecov/wsgi.py new file mode 100644 index 0000000000..38285feb8a --- /dev/null +++ b/apps/codecov-api/codecov/wsgi.py @@ -0,0 +1,18 @@ +""" +WSGI config for codecov project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +from utils.config import get_settings_module + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", get_settings_module()) + +application = get_wsgi_application() diff --git a/apps/codecov-api/codecov_auth/__init__.py b/apps/codecov-api/codecov_auth/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/codecov_auth/admin.py b/apps/codecov-api/codecov_auth/admin.py new file mode 100644 index 0000000000..4699f8185c --- /dev/null +++ b/apps/codecov-api/codecov_auth/admin.py @@ -0,0 +1,826 @@ +import logging +from datetime import timedelta +from typing import Optional, Sequence + +import django.forms as forms +from django.conf import settings +from django.contrib import admin, messages +from django.contrib.admin.models import LogEntry +from django.db.models import OuterRef, Subquery +from django.db.models.fields import BLANK_CHOICE_DASH +from django.forms import CheckboxInput, Select, Textarea +from django.http import HttpRequest +from django.shortcuts import redirect, render +from django.utils import timezone +from django.utils.html import format_html +from shared.django_apps.codecov_auth.models import ( + Account, + AccountsUsers, + InvoiceBilling, + Plan, + StripeBilling, + Tier, +) +from shared.plan.service import PlanService + +from codecov.admin import AdminMixin +from codecov.commands.exceptions import ValidationError +from codecov_auth.helpers import History +from codecov_auth.models import OrganizationLevelToken, Owner, SentryUser, Session, User +from codecov_auth.services.org_level_token_service import OrgLevelTokenService +from services.task import TaskService +from utils.services import get_short_service_name + +log = logging.getLogger(__name__) + + +class ExtendTrialForm(forms.Form): + end_date = forms.DateTimeField( + label="Trial End Date (YYYY-MM-DD HH:MM:SS):", required=True + ) + + +def extend_trial(self, request, queryset): + if "extend_trial" in request.POST: + form = ExtendTrialForm(request.POST) + if form.is_valid(): + for org in queryset: + plan_service = PlanService(current_org=org) + try: + plan_service.start_trial_manually( + current_owner=request.current_owner, + end_date=form.cleaned_data["end_date"], + ) + except ValidationError as e: + self.message_user( + request, + e.message + f" for {org.username}", + level=messages.ERROR, + ) + else: + self.message_user( + request, f"Successfully started trial for {org.username}" + ) + return + else: + form = ExtendTrialForm() + + return render( + request, + "admin/extend_trial_form.html", + context={ + "form": form, + "datasets": queryset, + }, + ) + + +extend_trial.short_description = "Start and extend trial up to a selected date" + + +def impersonate_owner(self, request, queryset): + if queryset.count() != 1: + self.message_user( + request, "You must impersonate exactly one Owner.", level=messages.ERROR + ) + return + + owner = queryset.first() + response = redirect( + f"{settings.CODECOV_URL}/{get_short_service_name(owner.service)}/" + ) + + # this cookie is read by the `ImpersonationMiddleware` and + # will reset `request.current_owner` to the impersonated owner + max_age = 900 # 15 minutes + response.set_cookie( + "staff_user", + owner.ownerid, + domain=settings.COOKIES_DOMAIN, + samesite=settings.COOKIE_SAME_SITE, + max_age=max_age, + ) + History.log( + Owner.objects.get(ownerid=owner.ownerid), + "Impersonation successful", + request.user, + ) + return response + + +impersonate_owner.short_description = "Impersonate the selected owner" + + +class AccountsUsersInline(admin.TabularInline): + model = AccountsUsers + max_num = 10 + extra = 1 + verbose_name_plural = "Accounts Users (click save to commit changes)" + verbose_name = "Account User" + can_delete = False + can_edit = False + + +class OwnerUserInline(admin.TabularInline): + model = Owner + max_num = 5 + extra = 0 + verbose_name_plural = "Owners (read only)" + verbose_name = "Owner" + exclude = ("oauth_token",) + can_delete = False + + readonly_fields = [ + "name", + "username", + "email", + "service", + "student", + ] + + fields = [] + readonly_fields + + +@admin.register(User) +class UserAdmin(AdminMixin, admin.ModelAdmin): + list_display = ( + "name", + "email", + ) + inlines = [AccountsUsersInline, OwnerUserInline] + search_fields = ( + "name__iregex", + "email__iregex", + ) + + readonly_fields = ( + "id", + "external_id", + ) + + fields = readonly_fields + ( + "name", + "email", + "is_staff", + "terms_agreement", + "terms_agreement_at", + ) + + def get_form(self, request, obj=None, change=False, **kwargs): + form = super().get_form(request, obj, change, **kwargs) + + if not request.user.is_superuser: + form.base_fields["is_staff"].disabled = True + + return form + + def has_add_permission(self, _, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False + + +@admin.register(SentryUser) +class SentryUserAdmin(AdminMixin, admin.ModelAdmin): + list_display = ( + "name", + "email", + ) + search_fields = ( + "name__iregex", + "email__iregex", + ) + readonly_fields = ( + "id", + "external_id", + "sentry_id", + "user", + ) + fields = readonly_fields + ( + "name", + "email", + ) + + def has_add_permission(self, _, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False + + +class OrgUploadTokenInline(admin.TabularInline): + model = OrganizationLevelToken + readonly_fields = ["token", "refresh"] + fields = ["token", "valid_until", "token_type", "refresh"] + extra = 0 + max_num = 1 + verbose_name = "Organization Level Token" + + def refresh(self, obj: OrganizationLevelToken): + # 0 in this case refers to the 0th index of the inline + # But there can only ever be 1 token per org, so it's fine to use that. + return format_html( + '' + ) + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return request.user.is_staff + + def has_add_permission(self, request: HttpRequest, obj: Optional[Owner]) -> bool: + has_token = OrganizationLevelToken.objects.filter(owner=obj).count() > 0 + return (not has_token) and request.user.is_staff + + +class InvoiceBillingInline(admin.StackedInline): + model = InvoiceBilling + extra = 0 + can_delete = False + verbose_name_plural = "Invoice Billing" + verbose_name = "Invoice Billing (click save to commit changes)" + + +@admin.register(InvoiceBilling) +class InvoiceBillingAdmin(AdminMixin, admin.ModelAdmin): + list_display = ("id", "account", "is_active") + search_fields = ( + "account__name", + "account__id__iexact", + "id__iexact", + "account_manager", + ) + search_help_text = ( + "Search by account name, account id (exact), id (exact), or account_manager" + ) + autocomplete_fields = ("account",) + + readonly_fields = [ + "id", + "created_at", + "updated_at", + ] + + fields = readonly_fields + [ + "account", + "account_manager", + "invoice_notes", + "is_active", + ] + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + field = form.base_fields["account"] + field.widget.can_add_related = False + field.widget.can_change_related = False + field.widget.can_delete_related = False + return form + + +class StripeBillingInline(admin.StackedInline): + can_delete = False + extra = 0 + model = StripeBilling + verbose_name_plural = "Stripe Billing" + verbose_name = "Stripe Billing (click save to commit changes)" + + +@admin.register(StripeBilling) +class StripeBillingAdmin(AdminMixin, admin.ModelAdmin): + list_display = ("id", "account", "is_active") + search_fields = ( + "account__name", + "account__id__iexact", + "id__iexact", + "customer_id__iexact", + "subscription_id__iexact", + ) + search_help_text = "Search by account name, account id (exact), id (exact), customer_id (exact), or subscription_id (exact)" + autocomplete_fields = ("account",) + + readonly_fields = [ + "id", + "created_at", + "updated_at", + ] + + fields = readonly_fields + [ + "account", + "customer_id", + "subscription_id", + "is_active", + ] + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + field = form.base_fields["account"] + field.widget.can_add_related = False + field.widget.can_change_related = False + field.widget.can_delete_related = False + return form + + +class OwnerOrgInline(admin.TabularInline): + model = Owner + max_num = 100 + extra = 0 + verbose_name_plural = "Organizations (read only)" + verbose_name = "Organization" + exclude = ("oauth_token",) + can_delete = False + + readonly_fields = [ + "name", + "username", + "plan", + "plan_activated_users", + "service", + ] + + fields = [] + readonly_fields + + +def find_and_remove_stale_users( + orgs: Sequence[Owner], date_threshold: timedelta | None = None +) -> tuple[set[int], set[int]]: + """ + This functions finds all the stale `plan_activated_users` in any of the given `orgs`. + + It then removes all those stale users from the given `orgs`, + returning the set of stale users (`ownerid`), and the set of `orgs` that were updated (`ownerid`). + + A user is considered stale if it had no API or login `Session` or any opened PR within `date_threshold`. + If no `date_threshold` is given, it defaults to *90 days*. + """ + + active_users = set() + for org in orgs: + active_users.update(set(org.plan_activated_users)) + + if not active_users: + return (set(), set()) + + # NOTE: the `annotate_last_pull_timestamp` manager/queryset method does the same `annotate` with `Subquery`. + sessions = Session.objects.filter(owner=OuterRef("pk")).order_by("-lastseen") + resolved_users = list( + Owner.objects.filter(ownerid__in=active_users) + .annotate(latest_session=Subquery(sessions.values("lastseen")[:1])) + .annotate_last_pull_timestamp() + .values_list("ownerid", "latest_session", "last_pull_timestamp", named=True) + ) + + threshold = timezone.now() - (date_threshold or timedelta(days=90)) + + def is_stale(user: dict) -> bool: + return (user.latest_session is None or user.latest_session < threshold) and ( + # NOTE: `last_pull_timestamp` is not timezone-aware, so we explicitly compare without timezones here + user.last_pull_timestamp is None + or user.last_pull_timestamp.replace(tzinfo=None) + < threshold.replace(tzinfo=None) + ) + + stale_users = {user.ownerid for user in resolved_users if is_stale(user)} + affected_orgs = { + org for org in orgs if stale_users.intersection(set(org.plan_activated_users)) + } + + if not affected_orgs: + return (set(), set()) + + # TODO: it might make sense to run all this within a transaction and locking the `affected_orgs` for update, + # as we have a slight chance of races between querying the `orgs` at the very beginning and updating them here: + for org in affected_orgs: + org.plan_activated_users = list( + set(org.plan_activated_users).difference(stale_users) + ) + Owner.objects.bulk_update(affected_orgs, ["plan_activated_users"]) + + return (stale_users, {org.ownerid for org in affected_orgs}) + + +@admin.register(Account) +class AccountAdmin(AdminMixin, admin.ModelAdmin): + list_display = ("name", "is_active", "organizations_count", "all_user_count") + search_fields = ("name__iregex", "id") + search_help_text = "Search by name (can use regex), or id (exact)" + inlines = [OwnerOrgInline, StripeBillingInline, InvoiceBillingInline] + actions = ["seat_check", "link_users_to_account", "deactivate_stale_users"] + + readonly_fields = ["id", "created_at", "updated_at", "users"] + + fields = readonly_fields + [ + "name", + "is_active", + "plan", + "plan_seat_count", + "free_seat_count", + "plan_auto_activate", + "is_delinquent", + ] + + @admin.action(description="Deactivate all stale `plan_activated_users`") + def deactivate_stale_users(self, request, queryset): + orgs = [org for account in queryset for org in account.organizations.all()] + stale_users, updated_orgs = find_and_remove_stale_users(orgs) + + if not stale_users or not updated_orgs: + self.message_user( + request, + "No stale users found in selected accounts / organizations.", + messages.INFO, + ) + else: + self.message_user( + request, + f"Removed {len(stale_users)} stale users from {len(updated_orgs)} affected organizations.", + messages.SUCCESS, + ) + + @admin.action( + description="Count current plan_activated_users across all Organizations" + ) + def seat_check(self, request, queryset): + self.link_users_to_account(request, queryset, dry_run=True) + + @admin.action(description="Link Users to Account") + def link_users_to_account(self, request, queryset, dry_run=False): + for account in queryset: + account_plan_activated_user_ownerids = set() + for org in account.organizations.all(): + account_plan_activated_user_ownerids.update( + set(org.plan_activated_users) + ) + + account_plan_activated_user_owners = Owner.objects.filter( + ownerid__in=account_plan_activated_user_ownerids + ).prefetch_related("user") + + non_student_count = account_plan_activated_user_owners.exclude( + student=True + ).count() + total_seats_for_account = account.plan_seat_count + account.free_seat_count + if non_student_count > total_seats_for_account: + self.message_user( + request, + f"Request failed: Account plan does not have enough seats; " + f"current plan activated users (non-students): {non_student_count}, total seats for account: {total_seats_for_account}", + messages.ERROR, + ) + return + if dry_run: + self.message_user( + request, + f"Request succeeded: Account plan has enough seats! " + f"current plan activated users (non-students): {non_student_count}, total seats for account: {total_seats_for_account}", + messages.SUCCESS, + ) + return + + owners_without_user_objects = account_plan_activated_user_owners.filter( + user__isnull=True + ) + owners_with_new_user_objects = [] + for userless_owner in owners_without_user_objects: + new_user = User.objects.create( + name=userless_owner.name, email=userless_owner.email + ) + userless_owner.user = new_user + owners_with_new_user_objects.append(userless_owner) + total = Owner.objects.bulk_update(owners_with_new_user_objects, ["user"]) + self.message_user( + request, + f"Created a User for {total} Owners", + messages.INFO, + ) + if total > 0: + log.info( + f"Admin operation for {account} - Created a User for {total} Owners", + extra=dict( + owners_with_new_user_objects=[ + str(owner) for owner in owners_with_new_user_objects + ], + account_id=account.id, + ), + ) + + # redo this query to get all Owners and Users + account_plan_activated_user_owners = Owner.objects.filter( + ownerid__in=account_plan_activated_user_ownerids + ).prefetch_related("user") + + already_linked_account_users = AccountsUsers.objects.filter(account=account) + + not_yet_linked_owners = account_plan_activated_user_owners.exclude( + user_id__in=already_linked_account_users.values_list( + "user_id", flat=True + ) + ) + + account_users_that_should_be_unlinked = ( + already_linked_account_users.exclude( + user_id__in=account_plan_activated_user_owners.values_list( + "user_id", flat=True + ) + ) + ) + deleted_ids_for_log = list( + account_users_that_should_be_unlinked.values_list("id", flat=True) + ) + deleted_count, _ = account_users_that_should_be_unlinked.delete() + + new_accounts_users = [] + for owner in not_yet_linked_owners: + new_account_user = AccountsUsers( + user_id=owner.user_id, account_id=account.id + ) + new_accounts_users.append(new_account_user) + total = AccountsUsers.objects.bulk_create(new_accounts_users) + self.message_user( + request, + f"Created {len(total)} AccountsUsers, removed {deleted_count} AccountsUsers", + messages.SUCCESS, + ) + if len(total) > 0 or deleted_count > 0: + log.info( + f"Admin operation for {account} - Created {len(total)} AccountsUsers, removed {deleted_count} AccountsUsers", + extra=dict( + new_accounts_users=total, + removed_accounts_users_ids=deleted_ids_for_log, + account_id=account.id, + ), + ) + + +@admin.register(Owner) +class OwnerAdmin(AdminMixin, admin.ModelAdmin): + exclude = ("oauth_token",) + list_display = ("name", "username", "email", "service") + readonly_fields = [] + search_fields = ("name__iregex", "username__iregex", "email__iregex", "ownerid") + actions = [impersonate_owner, extend_trial] + autocomplete_fields = ("bot", "account") + inlines = [OrgUploadTokenInline] + + readonly_fields = ( + "ownerid", + "username", + "service", + "email", + "business_email", + "name", + "service_id", + "createstamp", + "parent_service_id", + "root_parent_service_id", + "private_access", + "cache", + "free", + "invoice_details", + "yaml", + "updatestamp", + "permission", + "student", + "student_created_at", + "student_updated_at", + "user", + "trial_fired_by", + ) + + fields = readonly_fields + ( + "admins", + "plan_auto_activate", + "onboarding_completed", + "staff", + "plan", + "plan_provider", + "plan_user_count", + "plan_activated_users", + "uses_invoice", + "delinquent", + "integration_id", + "bot", + "stripe_customer_id", + "stripe_subscription_id", + "organizations", + "max_upload_limit", + "account", + "upload_token_required_for_public_repos", + ) + + def get_form(self, request, obj=None, change=False, **kwargs): + form = super().get_form(request, obj, change, **kwargs) + PLANS_CHOICES = [ + (x, x) + for x in Plan.objects.filter(is_active=True).values_list("name", flat=True) + ] + form.base_fields["plan"].widget = Select( + choices=BLANK_CHOICE_DASH + PLANS_CHOICES + ) + form.base_fields["uses_invoice"].widget = CheckboxInput() + + is_superuser = request.user.is_superuser + if not is_superuser: + form.base_fields["staff"].disabled = True + + field = form.base_fields["account"] + field.widget.can_add_related = False + field.widget.can_change_related = False + field.widget.can_delete_related = False + + return form + + def has_add_permission(self, _, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return bool(request.user and request.user.is_superuser) + + def delete_queryset(self, request, queryset) -> None: + for owner in queryset: + TaskService().delete_owner(ownerid=owner.ownerid) + + def delete_model(self, request, obj) -> None: + TaskService().delete_owner(ownerid=obj.ownerid) + + def get_deleted_objects(self, objs, request): + ( + deleted_objects, + model_count, + perms_needed, + protected, + ) = super().get_deleted_objects(objs, request) + + if request.user and request.user.is_superuser: + perms_needed = set() + + deleted_objects = () + return deleted_objects, model_count, perms_needed, protected + + def save_related(self, request: HttpRequest, form, formsets, change: bool) -> None: + if formsets: + token_formset = formsets[0] + token_id = token_formset.data.get("organization_tokens-0-id") + token_refresh = token_formset.data.get("organization_tokens-0-REFRESH") + # token_id only exists if the token already exists (edit operation) + if token_formset.is_valid() and token_id and token_refresh: + OrgLevelTokenService.refresh_token(token_id) + return super().save_related(request, form, formsets, change) + + +@admin.register(LogEntry) +class LogEntryAdmin(admin.ModelAdmin): + readonly_fields = ( + "action_time", + "user", + "content_type", + "object_id", + "object_repr", + "action_flag", + "change_message", + ) + list_display = ["__str__", "action_time", "user", "change_message"] + search_fields = ("object_repr", "change_message") + + # keep only view permission + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False + + +@admin.register(AccountsUsers) +class AccountsUsersAdmin(AdminMixin, admin.ModelAdmin): + list_display = ("id", "user", "account") + search_fields = ( + "account__name", + "account__id__iexact", + "id__iexact", + "user__id__iexact", + "user__name", + "user__email", + ) + search_help_text = "Search by account name, account id (exact), id (exact), user id (exact), user's name or email" + autocomplete_fields = ("account", "user") + + readonly_fields = [ + "id", + "created_at", + "updated_at", + ] + + fields = readonly_fields + ["account", "user"] + + +class PlansInline(admin.TabularInline): + model = Plan + extra = 1 + verbose_name_plural = "Plans (click save to commit changes)" + verbose_name = "Plan" + fields = [ + "name", + "marketing_name", + "base_unit_price", + "billing_rate", + "max_seats", + "monthly_uploads_limit", + "paid_plan", + "is_active", + "stripe_id", + ] + formfield_overrides = { + Plan._meta.get_field("benefits"): {"widget": Textarea(attrs={"rows": 3})}, + } + + +@admin.register(Tier) +class TierAdmin(admin.ModelAdmin): + list_display = ( + "tier_name", + "bundle_analysis", + "test_analytics", + "flaky_test_detection", + "project_coverage", + "private_repo_support", + ) + list_editable = ( + "bundle_analysis", + "test_analytics", + "flaky_test_detection", + "project_coverage", + "private_repo_support", + ) + search_fields = ("tier_name__iregex",) + inlines = [PlansInline] + fields = [ + "tier_name", + "bundle_analysis", + "test_analytics", + "flaky_test_detection", + "project_coverage", + "private_repo_support", + ] + + +class PlanAdminForm(forms.ModelForm): + class Meta: + model = Plan + fields = "__all__" + + def clean_base_unit_price(self) -> int | None: + base_unit_price = self.cleaned_data.get("base_unit_price") + if base_unit_price is not None and base_unit_price < 0: + raise forms.ValidationError("Base unit price cannot be negative.") + return base_unit_price + + def clean_max_seats(self) -> int | None: + max_seats = self.cleaned_data.get("max_seats") + if max_seats is not None and max_seats < 0: + raise forms.ValidationError("Max seats cannot be negative.") + return max_seats + + def clean_monthly_uploads_limit(self) -> int | None: + monthly_uploads_limit = self.cleaned_data.get("monthly_uploads_limit") + if monthly_uploads_limit is not None and monthly_uploads_limit < 0: + raise forms.ValidationError("Monthly uploads limit cannot be negative.") + return monthly_uploads_limit + + +@admin.register(Plan) +class PlanAdmin(admin.ModelAdmin): + form = PlanAdminForm + list_display = ( + "name", + "marketing_name", + "is_active", + "tier", + "paid_plan", + "billing_rate", + "base_unit_price", + "max_seats", + "monthly_uploads_limit", + ) + list_filter = ("is_active", "paid_plan", "billing_rate", "tier") + search_fields = ("name__iregex", "marketing_name__iregex") + fields = [ + "tier", + "name", + "marketing_name", + "base_unit_price", + "benefits", + "billing_rate", + "is_active", + "max_seats", + "monthly_uploads_limit", + "paid_plan", + "stripe_id", + ] + formfield_overrides = { + Plan._meta.get_field("benefits"): {"widget": Textarea(attrs={"rows": 3})}, + } + autocomplete_fields = ["tier"] # a dropdown for selecting related Tiers diff --git a/apps/codecov-api/codecov_auth/apps.py b/apps/codecov-api/codecov_auth/apps.py new file mode 100644 index 0000000000..ee2fabc65d --- /dev/null +++ b/apps/codecov-api/codecov_auth/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class CodecovAuthConfig(AppConfig): + name = "codecov_auth" + + def ready(self): + import codecov_auth.signals # noqa: F401 diff --git a/apps/codecov-api/codecov_auth/authentication/__init__.py b/apps/codecov-api/codecov_auth/authentication/__init__.py new file mode 100644 index 0000000000..3cfc03f763 --- /dev/null +++ b/apps/codecov-api/codecov_auth/authentication/__init__.py @@ -0,0 +1,73 @@ +import logging + +from django.conf import settings +from django.core.exceptions import ValidationError +from django.utils import timezone +from rest_framework import authentication, exceptions + +from codecov_auth.authentication.types import ( + InternalToken, + InternalUser, + SuperToken, + SuperUser, +) +from codecov_auth.models import UserToken + +log = logging.getLogger(__name__) + + +class UserTokenAuthentication(authentication.TokenAuthentication): + keyword = "Bearer" + + def authenticate(self, request): + # we save the request here so that we can set `current_owner` below + self.request = request + res = super().authenticate(request) + self.request = None + return res + + def authenticate_credentials(self, token): + try: + token = UserToken.objects.select_related("owner").get(token=token) + except (UserToken.DoesNotExist, ValidationError): + raise exceptions.AuthenticationFailed("Invalid token.") + + if token.valid_until is not None and token.valid_until <= timezone.now(): + raise exceptions.AuthenticationFailed("Invalid token.") + + if self.request: + # some permissions checking relies on this being available + self.request.current_owner = token.owner + + # NOTE: this is a bit unconventional in that it will result in + # `request.user` being an `Owner` instance instead of a `User`. + # If we returend `token.owner.user` here instead then we'd potentially + # break existing API clients with tokens being used on behalf of owners + # that have not logged into Codecov (and created a corresponding user record). + # i.e. `token.owner.user` could potentially be `None` + return (token.owner, token) + + +class SuperTokenAuthentication(authentication.TokenAuthentication): + keyword = "Bearer" + + def authenticate_credentials(self, key): + if key == settings.SUPER_API_TOKEN: + return (SuperUser(), SuperToken(token=key)) + return None + + +class InternalTokenAuthentication(authentication.TokenAuthentication): + keyword = "Bearer" + + def authenticate_credentials(self, key): + if key == settings.CODECOV_INTERNAL_TOKEN: + return (InternalUser(), InternalToken(token=key)) + + raise exceptions.AuthenticationFailed("Invalid token.") + + +class SessionAuthentication(authentication.SessionAuthentication): + def enforce_csrf(self, request): + # disable CSRF for the REST API + pass diff --git a/apps/codecov-api/codecov_auth/authentication/helpers.py b/apps/codecov-api/codecov_auth/authentication/helpers.py new file mode 100644 index 0000000000..e5ee82aa27 --- /dev/null +++ b/apps/codecov-api/codecov_auth/authentication/helpers.py @@ -0,0 +1,29 @@ +import re +from typing import NamedTuple + +from django.http import HttpRequest + + +class UploadInfo(NamedTuple): + service: str + encoded_slug: str + commitid: str | None + + +def get_upload_info_from_request_path(request: HttpRequest) -> UploadInfo | None: + path_info = request.get_full_path_info() + # The repo part comes from https://stackoverflow.com/a/22312124 + upload_views_prefix_regex = r"\/upload\/(\w+)\/([\w\.@:_/\-~]+)\/(commits|upload-coverage)(?:\/([a-f0-9]{40}))?" + match = re.search(upload_views_prefix_regex, path_info) + + if match is None: + return None + + service = match.group(1) + encoded_slug = match.group(2) + if match.group(3) == "commits": + commitid = match.group(4) + else: + commitid = None + + return UploadInfo(service, encoded_slug, commitid) diff --git a/apps/codecov-api/codecov_auth/authentication/repo_auth.py b/apps/codecov-api/codecov_auth/authentication/repo_auth.py new file mode 100644 index 0000000000..628e084bd1 --- /dev/null +++ b/apps/codecov-api/codecov_auth/authentication/repo_auth.py @@ -0,0 +1,526 @@ +import json +import logging +from typing import Any, Dict, List, Optional, Tuple +from uuid import UUID + +from django.core.exceptions import ObjectDoesNotExist +from django.db.models import QuerySet +from django.http import HttpRequest +from django.utils import timezone +from jwt import PyJWTError +from rest_framework import authentication, exceptions, serializers +from rest_framework.exceptions import NotAuthenticated, ValidationError +from rest_framework.response import Response +from rest_framework.views import exception_handler +from shared.django_apps.codecov_auth.models import Owner + +from codecov_auth.authentication.helpers import get_upload_info_from_request_path +from codecov_auth.authentication.types import RepositoryAsUser, RepositoryAuthInterface +from codecov_auth.models import ( + OrganizationLevelToken, + RepositoryToken, + Service, + TokenTypeChoices, +) +from core.models import Commit, Repository +from upload.helpers import get_global_tokens, get_repo_with_github_actions_oidc_token +from upload.views.helpers import ( + get_repository_and_owner_from_string, + get_repository_from_string, +) +from utils import is_uuid + +log = logging.getLogger(__name__) + + +def repo_auth_custom_exception_handler( + exc: Exception, context: Dict[str, Any] +) -> Response: + """ + User arrives here if they have correctly supplied a Token or the Tokenless Headers, + but their Token has not matched with any of our Authentication methods. The goal is to + give the user something better than "Invalid Token" or "Authentication credentials were not provided." + """ + response = exception_handler(exc, context) + # we were having issues with this block, I made it super cautions. + # Re-evaluate later whether this is overly cautious. + if ( + response is not None + and hasattr(response, "status_code") + and response.status_code == 401 + and hasattr(response, "data") + ): + try: + exc_code = response.data.get("detail").code + except (TypeError, AttributeError): + return response + if exc_code == NotAuthenticated.default_code: + response.data["detail"] = ( + "Failed token authentication, please double-check that your repository token matches in the Codecov UI, " + "or review the docs https://docs.codecov.com/docs/adding-the-codecov-token" + ) + return response + + +class LegacyTokenRepositoryAuth(RepositoryAuthInterface): + def __init__(self, repository: Repository, auth_data: Dict[str, Any]) -> None: + self._auth_data = auth_data + self._repository = repository + + def get_scopes(self) -> List[TokenTypeChoices]: + return [TokenTypeChoices.UPLOAD] + + def get_repositories(self) -> List[Repository]: + return [self._repository] + + def allows_repo(self, repository: Repository) -> bool: + return repository in self.get_repositories() + + +class OIDCTokenRepositoryAuth(LegacyTokenRepositoryAuth): + pass + + +class TableTokenRepositoryAuth(RepositoryAuthInterface): + def __init__(self, repository: Repository, token: RepositoryToken) -> None: + self._token = token + self._repository = repository + + def get_scopes(self) -> List[str]: + return [self._token.token_type] + + def get_repositories(self) -> List[Repository]: + return [self._repository] + + def allows_repo(self, repository: Repository) -> bool: + return repository in self.get_repositories() + + +class OrgLevelTokenRepositoryAuth(RepositoryAuthInterface): + def __init__(self, token: OrganizationLevelToken) -> None: + self._token = token + self._org = token.owner + + def get_scopes(self) -> List[str]: + return [self._token.token_type] + + def allows_repo(self, repository: Repository) -> bool: + return repository.author.ownerid == self._org.ownerid + + def get_repositories_queryset(self) -> QuerySet: + """Returns the QuerySet that generates get_repositories list. + Because QuerySets are lazy you can add further filters on top of it improving performance. + """ + return Repository.objects.filter(author=self._org) + + def get_repositories(self) -> List[Repository]: + # This might be an expensive function depending on the owner in question (thousands of repos) + # Consider using get_repositories_queryset if possible and adding more filters to it + return list(Repository.objects.filter(author=self._org).all()) + + +class TokenlessAuth(RepositoryAuthInterface): + def __init__(self, repository: Repository) -> None: + self._repository = repository + + def get_scopes(self) -> List[TokenTypeChoices]: + return [TokenTypeChoices.UPLOAD] + + def allows_repo(self, repository: Repository) -> bool: + return repository in self.get_repositories() + + def get_repositories(self) -> List[Repository]: + return [self._repository] + + +class RepositoryLegacyQueryTokenAuthentication(authentication.BaseAuthentication): + def authenticate( + self, request: HttpRequest + ) -> Optional[Tuple[RepositoryAsUser, LegacyTokenRepositoryAuth]]: + token = request.GET.get("token") + if not token: + return None + try: + token = UUID(token) + except ValueError: + return None + try: + repository = Repository.objects.get(upload_token=token) + except Repository.DoesNotExist: + return None + return ( + RepositoryAsUser(repository), + LegacyTokenRepositoryAuth(repository, {"token": token}), + ) + + +class RepositoryLegacyTokenAuthentication(authentication.TokenAuthentication): + def authenticate_credentials( + self, token: str + ) -> Optional[Tuple[RepositoryAsUser, LegacyTokenRepositoryAuth]]: + try: + token_uuid = UUID(token) + repository = Repository.objects.get(upload_token=token_uuid) + except (ValueError, TypeError, Repository.DoesNotExist): + return None # continue to next auth class + return ( + RepositoryAsUser(repository), + LegacyTokenRepositoryAuth(repository, {"token": token_uuid}), + ) + + +class RepositoryTokenAuthentication(authentication.TokenAuthentication): + keyword = "Repotoken" + + def authenticate_credentials( + self, key: str + ) -> Optional[Tuple[RepositoryAsUser, TableTokenRepositoryAuth]]: + try: + token = RepositoryToken.objects.select_related("repository").get(key=key) + except RepositoryToken.DoesNotExist: + raise exceptions.AuthenticationFailed("Invalid token.") + + if not token.repository.active: + raise exceptions.AuthenticationFailed("User inactive or deleted.") + if token.valid_until is not None and token.valid_until <= timezone.now(): + raise exceptions.AuthenticationFailed("Invalid token.") + return ( + RepositoryAsUser(token.repository), + TableTokenRepositoryAuth(token.repository, token), + ) + + +class GlobalTokenAuthentication(authentication.TokenAuthentication): + def authenticate( + self, request: HttpRequest + ) -> Optional[Tuple[RepositoryAsUser, LegacyTokenRepositoryAuth]]: + global_tokens = get_global_tokens() + token = self.get_token(request) + using_global_token = token in global_tokens + if not using_global_token: + return None # continue to next auth class + + service = global_tokens.get(token, "") + upload_info = get_upload_info_from_request_path(request) + if upload_info is None: + return None # continue to next auth class + # It's important NOT to use the service returned in upload_info + # To avoid someone uploading with GlobalUploadToken to a different service + # Than what it configured + repository = get_repository_from_string( + Service(service), upload_info.encoded_slug + ) + if repository is None: + raise exceptions.AuthenticationFailed( + "Could not find a repository, try using repo upload token" + ) + return ( + RepositoryAsUser(repository), + LegacyTokenRepositoryAuth(repository, {"token": token}), + ) + + def get_token(self, request: HttpRequest) -> str | None: + auth_header = request.headers.get("Authorization") + if not auth_header: + return None + if " " in auth_header: + _, token = auth_header.split(" ", 1) + return token + return auth_header + + +class OrgLevelTokenAuthentication(authentication.TokenAuthentication): + def authenticate_credentials( + self, key: str + ) -> Optional[Tuple[Owner, OrgLevelTokenRepositoryAuth]]: + if is_uuid(key): # else, continue to next auth class + # Actual verification for org level tokens + token = OrganizationLevelToken.objects.filter(token=key).first() + + if token is None: + return None + if token.valid_until and token.valid_until <= timezone.now(): + raise exceptions.AuthenticationFailed("Token is expired.") + + return ( + token.owner, + OrgLevelTokenRepositoryAuth(token), + ) + + +class GitHubOIDCTokenAuthentication(authentication.TokenAuthentication): + def authenticate_credentials( + self, token: str + ) -> Optional[Tuple[RepositoryAsUser, OIDCTokenRepositoryAuth]]: + if not token or is_uuid(token): + return None # continue to next auth class + + try: + repository = get_repo_with_github_actions_oidc_token(token) + except (ObjectDoesNotExist, PyJWTError, ValidationError): + return None # continue to next auth class + + log.info( + "GitHubOIDCTokenAuthentication Success", + extra=dict(repository=str(repository)), # Repo + ) + + return ( + RepositoryAsUser(repository), + OIDCTokenRepositoryAuth(repository, {"token": token}), + ) + + +class TokenlessAuthentication(authentication.TokenAuthentication): + # TODO: replace this with the message from repo_auth_custom_exception_handler + auth_failed_message = "Not valid tokenless upload" + + def _get_info_from_request_path( + self, request: HttpRequest + ) -> tuple[Repository, str | None]: + upload_info = get_upload_info_from_request_path(request) + + if upload_info is None: + raise exceptions.AuthenticationFailed(self.auth_failed_message) + service, encoded_slug, commitid = upload_info + # Validate provider + try: + service_enum = Service(service) + except ValueError: + raise exceptions.AuthenticationFailed(self.auth_failed_message) + + # Validate that next group exists and decode slug + repo = get_repository_from_string(service_enum, encoded_slug) + if repo is None: + # Purposefully using the generic message so that we don't tell that + # we don't have a certain repo + raise exceptions.AuthenticationFailed(self.auth_failed_message) + + return repo, commitid + + def get_branch( + self, + request: HttpRequest, + repoid: Optional[int] = None, + commitid: Optional[str] = None, + ) -> Optional[str]: + if repoid and commitid: + commit = Commit.objects.filter( + repository_id=repoid, commitid=commitid + ).first() + if not commit: + raise exceptions.AuthenticationFailed(self.auth_failed_message) + return commit.branch + else: + try: + body = json.loads(str(request.body, "utf8")) + except json.JSONDecodeError: + return None + else: + return body.get("branch") + + def authenticate( + self, request: HttpRequest + ) -> Tuple[RepositoryAsUser, TokenlessAuth]: + repository, commitid = self._get_info_from_request_path(request) + + if repository is None or repository.private: + raise exceptions.AuthenticationFailed(self.auth_failed_message) + + branch = self.get_branch(request, repository.repoid, commitid) + + if (branch and ":" in branch) or request.method == "GET": + return ( + RepositoryAsUser(repository), + TokenlessAuth(repository), + ) + else: + raise exceptions.AuthenticationFailed(self.auth_failed_message) + + +class TestAnalyticsTokenlessAuthentication(TokenlessAuthentication): + def _get_info_from_request_path( + self, request: HttpRequest + ) -> tuple[Repository, str | None]: + try: + body = json.loads(str(request.body, "utf8")) + + # Validate provider + service_enum = Service(body.get("service")) + + # Validate that next group exists and decode slug + repo = get_repository_from_string(service_enum, body.get("slug")) + if repo is None: + # Purposefully using the generic message so that we don't tell that + # we don't have a certain repo + raise exceptions.AuthenticationFailed(self.auth_failed_message) + + return repo, body.get("commit") + except json.JSONDecodeError: + # Validate request body format + raise exceptions.AuthenticationFailed(self.auth_failed_message) + except ValueError: + # Validate provider + raise exceptions.AuthenticationFailed(self.auth_failed_message) + + def get_branch( + self, + request: HttpRequest, + repoid: Optional[int] = None, + commitid: Optional[str] = None, + ) -> str: + body = json.loads(str(request.body, "utf8")) + + # If commit is not created yet (ie first upload for this commit), we just validate branch format. + # However, if a commit exists already (ie not the first upload for this commit), we must additionally + # validate the saved commit branch matches what is requested in this upload call. + commit = Commit.objects.filter(repository_id=repoid, commitid=commitid).first() + if commit and commit.branch != body.get("branch"): + raise exceptions.AuthenticationFailed(self.auth_failed_message) + + return body.get("branch") + + +class BundleAnalysisTokenlessAuthentication(TokenlessAuthentication): + def _get_info_from_request_path( + self, request: HttpRequest + ) -> tuple[Repository, str | None]: + try: + body = json.loads(str(request.body, "utf8")) + + # Validate provider + service_enum = Service(body.get("git_service")) + + # Validate that next group exists and decode slug + repo = get_repository_from_string(service_enum, body.get("slug")) + if repo is None: + # Purposefully using the generic message so that we don't tell that + # we don't have a certain repo + raise exceptions.AuthenticationFailed(self.auth_failed_message) + + return repo, body.get("commit") + except json.JSONDecodeError: + # Validate request body format + raise exceptions.AuthenticationFailed(self.auth_failed_message) + except ValueError: + # Validate provider + raise exceptions.AuthenticationFailed(self.auth_failed_message) + + def get_branch( + self, + request: HttpRequest, + repoid: Optional[int] = None, + commitid: Optional[str] = None, + ) -> str: + body = json.loads(str(request.body, "utf8")) + + # If commit is not created yet (ie first upload for this commit), we just validate branch format. + # However, if a commit exists already (ie not the first upload for this commit), we must additionally + # validate the saved commit branch matches what is requested in this upload call. + commit = Commit.objects.filter(repository_id=repoid, commitid=commitid).first() + if commit and commit.branch != body.get("branch"): + raise exceptions.AuthenticationFailed(self.auth_failed_message) + + return body.get("branch") + + +class UploadTokenRequiredAuthenticationCheck(authentication.TokenAuthentication): + """ + If repo is public, OwnerOrg can set upload_token_required_for_public_repos=False + to allow uploads with no token. If this is the case, catch it first and exclude from + other Auth checks. + """ + + def _get_info_from_request_path( + self, request: HttpRequest + ) -> tuple[Repository | None, Owner | None]: + upload_info = get_upload_info_from_request_path(request) + + if upload_info is None: + return None, None # continue to next auth class + service, encoded_slug, _ = upload_info + # Validate provider + try: + service_enum = Service(service) + except ValueError: + return None, None # continue to next auth class + + repository, owner = get_repository_and_owner_from_string( + service_enum, encoded_slug + ) + + return repository, owner + + def get_repository_and_owner( + self, request: HttpRequest + ) -> tuple[Repository | None, Owner | None]: + return self._get_info_from_request_path(request) + + def authenticate( + self, request: HttpRequest + ) -> tuple[RepositoryAsUser, TokenlessAuth] | None: + repository, owner = self.get_repository_and_owner(request) + + if ( + repository is None + or repository.private + or owner is None + or owner.upload_token_required_for_public_repos + ): + return None # continue to next auth class + + return ( + RepositoryAsUser(repository), + TokenlessAuth(repository), + ) + + +class UploadTokenRequiredGetFromBodySerializer(serializers.Serializer): + slug = serializers.CharField(required=True) + service = serializers.CharField(required=False) # git_service from TA + git_service = serializers.CharField(required=False) # git_service from BA + + +class UploadTokenRequiredGetFromBodyAuthenticationCheck( + UploadTokenRequiredAuthenticationCheck +): + """ + Get Repository and Owner from request body instead of path, + then use the same authenticate() as parent class. + """ + + def _get_git(self, validated_data: Dict[str, str]) -> Optional[str]: + """ + BA sends this in as git_service, TA sends this in as service. + Use this function so this Check class can be used by both views. + """ + git_service = validated_data.get("git_service") or validated_data.get("service") + return git_service + + def _get_info_from_request_body( + self, request: HttpRequest + ) -> tuple[Repository | None, Owner | None]: + try: + body = json.loads(str(request.body, "utf8")) + + serializer = UploadTokenRequiredGetFromBodySerializer(data=body) + + if serializer.is_valid(): + git_service = self._get_git(validated_data=serializer.validated_data) + service_enum = Service(git_service) + return get_repository_and_owner_from_string( + service=service_enum, + repo_identifier=serializer.validated_data["slug"], + ) + + except (json.JSONDecodeError, ValueError): + # exceptions raised by json.loads() and Service() + # catch rather than raise to continue to next auth class + pass + + return None, None # continue to next auth class + + def get_repository_and_owner( + self, request: HttpRequest + ) -> tuple[Repository | None, Owner | None]: + return self._get_info_from_request_body(request) diff --git a/apps/codecov-api/codecov_auth/authentication/types.py b/apps/codecov-api/codecov_auth/authentication/types.py new file mode 100644 index 0000000000..bf7889eea7 --- /dev/null +++ b/apps/codecov-api/codecov_auth/authentication/types.py @@ -0,0 +1,95 @@ +from typing import List + +from django.contrib.auth.models import Group, Permission +from django.db.models.manager import EmptyManager + +from core.models import Repository + + +class RepositoryAsUser(object): + def __init__(self, repository): + self._repository = repository + + def is_authenticated(self): + return True + + +class RepositoryAuthInterface(object): + def get_scopes(): + raise NotImplementedError() + + def get_repositories() -> List[Repository]: + raise NotImplementedError() + + def allows_repo(self, repository: Repository) -> bool: + raise NotImplementedError() + + +class DjangoUser(object): + id = None + pk = None + is_staff = False + is_superuser = False + is_active = False + _groups = EmptyManager(Group) + _user_permissions = EmptyManager(Permission) + + @property + def is_anonymous(self): + return False + + @property + def is_authenticated(self): + return False + + @property + def groups(self): + return False + + @property + def user_permissions(self): + return False + + def get_user_permissions(self, obj=None): + return False + + def get_group_permissions(self, obj=None): + return False + + def get_all_permissions(self, obj=None): + return False + + def has_perm(self, perm, obj=None): + return False + + def has_perms(self, perm_list, obj=None): + return False + + +class SuperUser(DjangoUser): + is_super_user = True + + pass + + +class InternalUser(DjangoUser): + is_internal_user = True + + pass + + +class DjangoToken(object): + def __init__(self, token=None): + self.token = token + + +class SuperToken(DjangoToken): + is_super_token = True + + pass + + +class InternalToken(DjangoToken): + is_internal_token = True + + pass diff --git a/apps/codecov-api/codecov_auth/commands/__init__.py b/apps/codecov-api/codecov_auth/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/codecov_auth/commands/owner/__init__.py b/apps/codecov-api/codecov_auth/commands/owner/__init__.py new file mode 100644 index 0000000000..826743c705 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/__init__.py @@ -0,0 +1,3 @@ +from .owner import OwnerCommands + +__all__ = ["OwnerCommands"] diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/__init__.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/cancel_trial.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/cancel_trial.py new file mode 100644 index 0000000000..9834dc418d --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/cancel_trial.py @@ -0,0 +1,29 @@ +from asgiref.sync import sync_to_async +from shared.plan.service import PlanService + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthorized, ValidationError +from codecov_auth.helpers import current_user_part_of_org +from codecov_auth.models import Owner + + +class CancelTrialInteractor(BaseInteractor): + def validate(self, owner: Owner | None): + if not owner: + raise ValidationError("Cannot find owner record in the database") + if not current_user_part_of_org(self.current_owner, owner): + raise Unauthorized() + + def _cancel_trial(self, owner: Owner): + plan_service = PlanService(current_org=owner) + plan_service.cancel_trial() + return + + @sync_to_async + def execute(self, org_username: str) -> None: + owner = Owner.objects.filter( + username=org_username, service=self.service + ).first() + self.validate(owner=owner) + self._cancel_trial(owner=owner) + return diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/create_api_token.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/create_api_token.py new file mode 100644 index 0000000000..e1692d36e0 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/create_api_token.py @@ -0,0 +1,22 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated, ValidationError +from codecov_auth.models import Session + + +class CreateApiTokenInteractor(BaseInteractor): + def validate(self, name): + if not self.current_user.is_authenticated: + raise Unauthenticated() + if len(name) == 0: + raise ValidationError("name cant be empty") + + def create_token(self, name): + type = Session.SessionType.API + return Session.objects.create(name=name, owner=self.current_owner, type=type) + + @sync_to_async + def execute(self, name): + self.validate(name) + return self.create_token(name) diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/create_stripe_setup_intent.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/create_stripe_setup_intent.py new file mode 100644 index 0000000000..f7624c8cfa --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/create_stripe_setup_intent.py @@ -0,0 +1,42 @@ +import logging + +import stripe +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated, Unauthorized, ValidationError +from codecov_auth.helpers import current_user_part_of_org +from codecov_auth.models import Owner +from services.billing import BillingService + +log = logging.getLogger(__name__) + + +class CreateStripeSetupIntentInteractor(BaseInteractor): + def validate(self, owner_obj: Owner) -> None: + if not self.current_user.is_authenticated: + raise Unauthenticated() + if not owner_obj: + raise ValidationError("Owner not found") + if not current_user_part_of_org(self.current_owner, owner_obj): + raise Unauthorized() + + def create_setup_intent(self, owner_obj: Owner) -> stripe.SetupIntent: + try: + billing = BillingService(requesting_user=self.current_owner) + return billing.create_setup_intent(owner_obj) + except Exception as e: + log.error( + "Error getting setup intent", + extra={ + "ownerid": owner_obj.ownerid, + "error": str(e), + }, + ) + raise ValidationError("Unable to create setup intent") + + @sync_to_async + def execute(self, owner: str) -> stripe.SetupIntent: + owner_obj = Owner.objects.filter(username=owner, service=self.service).first() + self.validate(owner_obj) + return self.create_setup_intent(owner_obj) diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/create_user_token.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/create_user_token.py new file mode 100644 index 0000000000..8d60818785 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/create_user_token.py @@ -0,0 +1,30 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated, ValidationError +from codecov_auth.models import UserToken + + +class CreateUserTokenInteractor(BaseInteractor): + def validate(self, name: str, token_type: str): + if not self.current_user.is_authenticated: + raise Unauthenticated() + if len(name) == 0: + raise ValidationError("name cant be empty") + if token_type not in UserToken.TokenType.values: + raise ValidationError(f"invalid token type: {token_type}") + + def create_token(self, name: str, token_type: str) -> UserToken: + return UserToken.objects.create( + name=name, + owner=self.current_owner, + token_type=token_type, + ) + + @sync_to_async + def execute(self, name: str, token_type: str = None) -> UserToken: + if token_type is None: + token_type = UserToken.TokenType.API.value + + self.validate(name, token_type) + return self.create_token(name, token_type) diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/delete_session.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/delete_session.py new file mode 100644 index 0000000000..dd1aae6b05 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/delete_session.py @@ -0,0 +1,30 @@ +from asgiref.sync import sync_to_async +from django.contrib.sessions.models import Session as DjangoSession + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated +from codecov_auth.models import Session + + +class DeleteSessionInteractor(BaseInteractor): + def validate(self) -> None: + if not self.current_user.is_authenticated: + raise Unauthenticated() + + @sync_to_async + def execute(self, sessionid: int) -> None: + self.validate() + + try: + session_to_delete = Session.objects.get(sessionid=sessionid) + django_session_to_delete = DjangoSession.objects.get( + session_key=session_to_delete.login_session_id + ) + user_id_to_delete = int( + django_session_to_delete.get_decoded().get("_auth_user_id", "0") + ) + + if user_id_to_delete == self.current_user.id: + django_session_to_delete.delete() + except (Session.DoesNotExist, DjangoSession.DoesNotExist): + pass diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/fetch_owner.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/fetch_owner.py new file mode 100644 index 0000000000..bb32216fd4 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/fetch_owner.py @@ -0,0 +1,10 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from codecov_auth.models import Owner + + +class FetchOwnerInteractor(BaseInteractor): + @sync_to_async + def execute(self, username): + return Owner.objects.filter(username=username, service=self.service).first() diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/get_is_current_user_an_admin.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/get_is_current_user_an_admin.py new file mode 100644 index 0000000000..46906de909 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/get_is_current_user_an_admin.py @@ -0,0 +1,54 @@ +from asgiref.sync import async_to_sync, sync_to_async +from django.conf import settings + +import services.self_hosted as self_hosted +from codecov.commands.base import BaseInteractor +from services.decorators import torngit_safe +from services.repo_providers import get_generic_adapter_params, get_provider + + +@torngit_safe +@sync_to_async +def _is_admin_on_provider(owner, current_user): + torngit_provider_adapter = get_provider( + owner.service, + { + **get_generic_adapter_params(current_user, owner.service), + **{ + "owner": { + "username": owner.username, + "service_id": owner.service_id, + } + }, + }, + ) + + isAdmin = async_to_sync(torngit_provider_adapter.get_is_admin)( + user={"username": current_user.username, "service_id": current_user.service_id} + ) + return isAdmin + + +class GetIsCurrentUserAnAdminInteractor(BaseInteractor): + @sync_to_async + def execute(self, owner, current_owner): + if settings.IS_ENTERPRISE: + return self_hosted.is_admin_owner(current_owner) + else: + if not current_owner: + return False + admins = owner.admins + if not hasattr(current_owner, "ownerid"): + return False + if owner.ownerid == current_owner.ownerid: + return True + else: + try: + isAdmin = async_to_sync(_is_admin_on_provider)(owner, current_owner) + if isAdmin: + # save admin provider in admins list + owner.add_admin(current_owner) + return isAdmin or (current_owner.ownerid in admins) + except Exception as error: + print("Error Calling Admin Provider " + repr(error)) # noqa: T201 + return False diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/get_org_upload_token.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/get_org_upload_token.py new file mode 100644 index 0000000000..7a07c7d141 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/get_org_upload_token.py @@ -0,0 +1,23 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated, Unauthorized +from codecov_auth.helpers import current_user_part_of_org +from codecov_auth.models import OrganizationLevelToken + + +class GetOrgUploadToken(BaseInteractor): + def validate(self, owner): + if not self.current_user.is_authenticated: + raise Unauthenticated() + + if not current_user_part_of_org(self.current_owner, owner): + raise Unauthorized() + + @sync_to_async + def execute(self, owner): + self.validate(owner) + + org_token = OrganizationLevelToken.objects.filter(owner=owner).first() + if org_token: + return org_token.token diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/get_uploads_number_per_user.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/get_uploads_number_per_user.py new file mode 100644 index 0000000000..3c5e42bc3f --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/get_uploads_number_per_user.py @@ -0,0 +1,20 @@ +from typing import Optional + +from asgiref.sync import sync_to_async +from shared.helpers.redis import get_redis_connection +from shared.plan.service import PlanService +from shared.upload.utils import query_monthly_coverage_measurements + +from codecov.commands.base import BaseInteractor +from codecov_auth.models import Owner + +redis = get_redis_connection() + + +class GetUploadsNumberPerUserInteractor(BaseInteractor): + @sync_to_async + def execute(self, owner: Owner) -> Optional[int]: + plan_service = PlanService(current_org=owner) + monthly_limit = plan_service.monthly_uploads_limit + if monthly_limit is not None: + return query_monthly_coverage_measurements(plan_service=plan_service) diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/is_syncing.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/is_syncing.py new file mode 100644 index 0000000000..94971cb526 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/is_syncing.py @@ -0,0 +1,10 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from services.refresh import RefreshService + + +class IsSyncingInteractor(BaseInteractor): + @sync_to_async + def execute(self): + return RefreshService().is_refreshing(self.current_owner.ownerid) diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/onboard_user.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/onboard_user.py new file mode 100644 index 0000000000..1d72cbacfe --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/onboard_user.py @@ -0,0 +1,49 @@ +from asgiref.sync import sync_to_async +from django import forms + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated, Unauthorized, ValidationError +from codecov_auth.models import OwnerProfile + + +class OnboardForm(forms.Form): + email = forms.EmailField(required=False) + business_email = forms.EmailField(required=False) + other_goal = forms.CharField(required=False) + type_projects = forms.MultipleChoiceField(choices=OwnerProfile.ProjectType.choices) + goals = forms.MultipleChoiceField(choices=OwnerProfile.Goal.choices) + + +class OnboardUserInteractor(BaseInteractor): + def validate(self, params): + if not self.current_user.is_authenticated: + raise Unauthenticated() + if not self.current_owner or self.current_owner.onboarding_completed: + raise Unauthorized() + form = OnboardForm(params) + if not form.is_valid(): + raise ValidationError(form.errors.as_json()) + + def create_profile(self, params): + self.current_owner.onboarding_completed = True + self.current_owner.business_email = params.get("business_email") + self.current_owner.email = params.get("email") + self.current_owner.save() + + OwnerProfile.objects.update_or_create( + owner=self.current_owner, + defaults={ + "type_projects": params.get("type_projects", []), + "goals": params.get("goals", []), + "other_goal": params.get("other_goal"), + }, + ) + + # refresh in case the profile was already preloaded + self.current_owner.profile.refresh_from_db() + + @sync_to_async + def execute(self, params): + self.validate(params) + self.create_profile(params) + return self.current_owner diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/regenerate_org_upload_token.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/regenerate_org_upload_token.py new file mode 100644 index 0000000000..5eb94c566d --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/regenerate_org_upload_token.py @@ -0,0 +1,33 @@ +import uuid + +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated, Unauthorized, ValidationError +from codecov_auth.helpers import current_user_part_of_org +from codecov_auth.models import OrganizationLevelToken, Owner + + +class RegenerateOrgUploadTokenInteractor(BaseInteractor): + def validate(self, owner_obj): + if not self.current_user.is_authenticated: + raise Unauthenticated() + if not owner_obj: + raise ValidationError("Owner not found") + if not current_user_part_of_org(self.current_owner, owner_obj): + raise Unauthorized() + + @sync_to_async + def execute(self, owner): + owner_obj = Owner.objects.filter(username=owner, service=self.service).first() + + self.validate(owner_obj) + + upload_token, created = OrganizationLevelToken.objects.get_or_create( + owner=owner_obj + ) + if not created: + upload_token.token = uuid.uuid4() + upload_token.save() + + return upload_token.token diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/revoke_user_token.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/revoke_user_token.py new file mode 100644 index 0000000000..904f338a65 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/revoke_user_token.py @@ -0,0 +1,16 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated +from codecov_auth.models import UserToken + + +class RevokeUserTokenInteractor(BaseInteractor): + def validate(self): + if not self.current_user.is_authenticated: + raise Unauthenticated() + + @sync_to_async + def execute(self, tokenid): + self.validate() + UserToken.objects.filter(external_id=tokenid, owner=self.current_owner).delete() diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/save_okta_config.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/save_okta_config.py new file mode 100644 index 0000000000..46c9c84904 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/save_okta_config.py @@ -0,0 +1,92 @@ +from dataclasses import dataclass + +from asgiref.sync import sync_to_async +from shared.django_apps.codecov_auth.models import AccountsUsers, User + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated, Unauthorized, ValidationError +from codecov_auth.models import Account, OktaSettings, Owner + + +@dataclass +class SaveOktaConfigInput: + enabled: bool | None + enforced: bool | None + client_id: str | None = None + client_secret: str | None = None + url: str | None = None + org_username: str | None = None + + +class SaveOktaConfigInteractor(BaseInteractor): + def validate(self, owner: Owner) -> None: + if not self.current_user.is_authenticated: + raise Unauthenticated() + if not owner: + raise ValidationError("Cannot find owner record in the database") + if not owner.is_admin(self.current_owner): + raise Unauthorized() + + @sync_to_async + def execute(self, input: dict) -> None: + typed_input = SaveOktaConfigInput( + client_id=input.get("client_id"), + client_secret=input.get("client_secret"), + url=input.get("url"), + enabled=input.get("enabled"), + enforced=input.get("enforced"), + org_username=input.get("org_username"), + ) + + owner = Owner.objects.filter( + username=typed_input.org_username, service=self.service + ).first() + self.validate(owner=owner) + + account = owner.account + if not account: + account = Account.objects.create( + name=owner.username, + plan=owner.plan, + plan_seat_count=owner.plan_user_count, + free_seat_count=owner.free, + plan_auto_activate=owner.plan_auto_activate, + ) + owner.account = account + owner.save() + + # Update the activated users to be added to the account + plan_activated_user_owners: list[int] = owner.plan_activated_users + activated_connections: list[AccountsUsers] = [] + for activated_user_owner in plan_activated_user_owners: + user_owner: Owner = Owner.objects.select_related("user").get( + pk=activated_user_owner + ) + user = user_owner.user + if user is None: + user = User(name=user_owner.name, email=user_owner.email) + user_owner.user = user + user.save() + user_owner.save() + + activated_connections.append(AccountsUsers(account=account, user=user)) + + # Batch the user creation in batches of 50 users + if len(activated_connections) > 50: + AccountsUsers.objects.bulk_create(activated_connections) + activated_connections = [] + + if activated_connections: + AccountsUsers.objects.bulk_create(activated_connections) + + okta_config, created = OktaSettings.objects.get_or_create(account=account) + + for field in ["client_id", "client_secret", "url", "enabled", "enforced"]: + value = getattr(typed_input, field) + if value is not None: + # Strip the URL of any trailing spaces and slashes before saving it + if field == "url": + value = value.strip("/ ") + setattr(okta_config, field, value) + + okta_config.save() diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/save_terms_agreement.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/save_terms_agreement.py new file mode 100644 index 0000000000..acef9c5324 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/save_terms_agreement.py @@ -0,0 +1,92 @@ +from dataclasses import dataclass +from typing import Any, Optional + +from asgiref.sync import sync_to_async +from django.utils import timezone + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated, ValidationError +from services.analytics import AnalyticsService + + +@dataclass +class TermsAgreementInput: + business_email: Optional[str] = None + name: Optional[str] = None + terms_agreement: bool = False + marketing_consent: bool = False + customer_intent: Optional[str] = None + + +class SaveTermsAgreementInteractor(BaseInteractor): + requires_service = False + + def validate_deprecated(self, input: TermsAgreementInput) -> None: + valid_customer_intents = ["Business", "BUSINESS", "Personal", "PERSONAL"] + if ( + input.customer_intent + and input.customer_intent not in valid_customer_intents + ): + raise ValidationError("Invalid customer intent provided") + if not self.current_user.is_authenticated: + raise Unauthenticated() + + def validate(self, input: TermsAgreementInput) -> None: + if not self.current_user.is_authenticated: + raise Unauthenticated() + + def update_terms_agreement_deprecated(self, input: TermsAgreementInput) -> None: + self.current_user.terms_agreement = input.terms_agreement + self.current_user.terms_agreement_at = timezone.now() + self.current_user.customer_intent = input.customer_intent + self.current_user.email_opt_in = input.marketing_consent + self.current_user.save() + + if input.business_email and input.business_email != "": + self.current_user.email = input.business_email + self.current_user.save() + + if input.marketing_consent: + self.send_data_to_marketo() + + def update_terms_agreement(self, input: TermsAgreementInput) -> None: + self.current_user.terms_agreement = input.terms_agreement + self.current_user.terms_agreement_at = timezone.now() + self.current_user.name = input.name + self.current_user.email_opt_in = input.marketing_consent + self.current_user.save() + + if input.business_email and input.business_email != "": + self.current_user.email = input.business_email + self.current_user.save() + + if input.marketing_consent: + self.send_data_to_marketo() + + def send_data_to_marketo(self) -> None: + event_data = { + "email": self.current_user.email, + } + AnalyticsService().opt_in_email(self.current_user.id, event_data) + + @sync_to_async + def execute(self, input: Any) -> None: + if input.get("name"): + typed_input = TermsAgreementInput( + business_email=input.get("business_email"), + terms_agreement=input.get("terms_agreement"), + marketing_consent=input.get("marketing_consent"), + name=input.get("name"), + ) + self.validate(typed_input) + self.update_terms_agreement(typed_input) + # this handles the deprecated inputs + else: + typed_input = TermsAgreementInput( + business_email=input.get("business_email"), + terms_agreement=input.get("terms_agreement"), + marketing_consent=input.get("marketing_consent"), + customer_intent=input.get("customer_intent"), + ) + self.validate_deprecated(typed_input) + self.update_terms_agreement_deprecated(typed_input) diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/set_upload_token_required.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/set_upload_token_required.py new file mode 100644 index 0000000000..10ba28164b --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/set_upload_token_required.py @@ -0,0 +1,48 @@ +from dataclasses import dataclass + +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated, Unauthorized, ValidationError +from codecov_auth.helpers import current_user_part_of_org +from codecov_auth.models import Owner + + +@dataclass +class SetUploadTokenRequiredInput: + upload_token_required: bool + org_username: str + + +class SetUploadTokenRequiredInteractor(BaseInteractor): + def validate(self, owner_obj, upload_token_required): + if not self.current_user.is_authenticated: + raise Unauthenticated() + if not owner_obj: + raise ValidationError("Owner not found") + if not current_user_part_of_org(self.current_owner, owner_obj): + raise Unauthorized() + if not owner_obj.is_admin(self.current_owner): + raise Unauthorized("Admin authorization required") + if upload_token_required is None: + raise ValidationError("upload_token_required must be either True or False") + + @sync_to_async + def execute(self, input: dict[str, bool]): + typed_input = SetUploadTokenRequiredInput( + upload_token_required=input.get("upload_token_required"), + org_username=input.get("org_username"), + ) + + owner_obj = Owner.objects.filter( + username=typed_input.org_username, service=self.service + ).first() + + self.validate(owner_obj, typed_input.upload_token_required) + + owner_obj.upload_token_required_for_public_repos = bool( + typed_input.upload_token_required + ) + owner_obj.save() + + return diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/set_yaml_on_owner.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/set_yaml_on_owner.py new file mode 100644 index 0000000000..e177a564b2 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/set_yaml_on_owner.py @@ -0,0 +1,98 @@ +import html +from typing import Optional + +import yaml +from asgiref.sync import sync_to_async +from shared.django_apps.core.models import Repository +from shared.django_apps.utils.model_utils import get_ownerid_if_member +from shared.validation.exceptions import InvalidYamlException +from shared.yaml.validation import validate_yaml + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import ( + NotFound, + Unauthenticated, + Unauthorized, + ValidationError, +) +from codecov_auth.constants import OWNER_YAML_TO_STRING_KEY +from codecov_auth.helpers import current_user_part_of_org +from codecov_auth.models import Owner + + +class SetYamlOnOwnerInteractor(BaseInteractor): + def validate(self) -> None: + if not self.current_user.is_authenticated: + raise Unauthenticated() + + def authorize(self) -> None: + if not current_user_part_of_org(self.current_owner, self.owner): + raise Unauthorized() + + def get_owner(self, username: str) -> Owner: + try: + return Owner.objects.get(username=username, service=self.service) + except Owner.DoesNotExist: + raise NotFound() + + def convert_yaml_to_dict(self, yaml_input: str) -> Optional[dict]: + yaml_safe = html.escape(yaml_input, quote=False) + try: + yaml_dict = yaml.safe_load(yaml_safe) + except yaml.scanner.ScannerError as e: + line = e.problem_mark.line + column = e.problem_mark.column + message = ( + f"Syntax error at line {line + 1}, column {column + 1}: {e.problem}" + ) + raise ValidationError(message) + if not yaml_dict: + return None + try: + return validate_yaml(yaml_dict, show_secrets_for=None) + except InvalidYamlException as e: + message = f"Error at {str(e.error_location)}: {e.error_message}" + raise ValidationError(message) + + def yaml_side_effects(self, old_yaml: dict | None, new_yaml: dict | None) -> None: + old_yaml_branch = old_yaml and old_yaml.get("codecov", {}).get("branch") + new_yaml_branch = new_yaml and new_yaml.get("codecov", {}).get("branch") + + # Update all repositories from owner if branch is updated in yaml + if new_yaml_branch != old_yaml_branch: + repos = Repository.objects.filter(author_id=self.owner.ownerid) + repos.update( + branch=new_yaml_branch or old_yaml_branch + ) # Keeps old_branch if new_branch is None + + old_yaml_bot = old_yaml and old_yaml.get("codecov", {}).get("bot") + new_yaml_bot = new_yaml and new_yaml.get("codecov", {}).get("bot") + + # Update owner's bot column if bot is updated in yaml or if bot is not configured. + if new_yaml_bot != old_yaml_bot or self.owner.bot is None: + new_bot_id = ( + get_ownerid_if_member( + service=self.owner.service, + owner_username=new_yaml_bot, + owner_id=self.owner.ownerid, + ) + or old_yaml_bot + or None + ) + self.owner.bot_id = new_bot_id + self.owner.save() + + @sync_to_async + def execute(self, username: str, yaml_input: str) -> Owner: + self.validate() + self.owner = self.get_owner(username) + self.authorize() + old_yaml = self.owner.yaml + self.owner.yaml = self.convert_yaml_to_dict(yaml_input) + if self.owner.yaml: + self.owner.yaml[OWNER_YAML_TO_STRING_KEY] = yaml_input + self.owner.save() + + # side effects + self.yaml_side_effects(old_yaml=old_yaml, new_yaml=self.owner.yaml) + return self.owner diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/start_trial.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/start_trial.py new file mode 100644 index 0000000000..5abd36a8a2 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/start_trial.py @@ -0,0 +1,29 @@ +from asgiref.sync import sync_to_async +from shared.plan.service import PlanService + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthorized, ValidationError +from codecov_auth.helpers import current_user_part_of_org +from codecov_auth.models import Owner + + +class StartTrialInteractor(BaseInteractor): + def validate(self, current_org: Owner | None): + if not current_org: + raise ValidationError("Cannot find owner record in the database") + if not current_user_part_of_org(self.current_owner, current_org): + raise Unauthorized() + + def _start_trial(self, current_org: Owner) -> None: + plan_service = PlanService(current_org=current_org) + plan_service.start_trial(current_owner=self.current_owner) + return + + @sync_to_async + def execute(self, org_username: str) -> None: + current_org = Owner.objects.filter( + username=org_username, service=self.service + ).first() + self.validate(current_org=current_org) + self._start_trial(current_org=current_org) + return diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/store_codecov_metric.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/store_codecov_metric.py new file mode 100644 index 0000000000..c52cad8596 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/store_codecov_metric.py @@ -0,0 +1,31 @@ +import json + +from asgiref.sync import sync_to_async +from shared.django_apps.codecov_metrics.service.codecov_metrics import ( + UserOnboardingMetricsService, +) + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import ValidationError +from codecov_auth.models import Owner + + +class StoreCodecovMetricInteractor(BaseInteractor): + @sync_to_async + def execute(self, org_username: str, event: str, json_string: str) -> None: + current_org = Owner.objects.filter( + username=org_username, service=self.service + ).first() + if not current_org: + raise ValidationError("Cannot find owner record in the database") + + try: + payload = json.loads(json_string) + except json.JSONDecodeError: + raise ValidationError("Invalid JSON string") + + UserOnboardingMetricsService.create_user_onboarding_metric( + org_id=current_org.pk, + event=event, + payload=payload, + ) diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/__init__.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_cancel_trial.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_cancel_trial.py new file mode 100644 index 0000000000..fde8a2380d --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_cancel_trial.py @@ -0,0 +1,109 @@ +from datetime import datetime, timedelta + +import pytest +from asgiref.sync import async_to_sync +from django.test import TestCase +from freezegun import freeze_time +from shared.django_apps.codecov.commands.exceptions import ValidationError +from shared.django_apps.codecov_auth.tests.factories import PlanFactory, TierFactory +from shared.django_apps.core.tests.factories import OwnerFactory +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName, TierName, TrialStatus + +from codecov.commands.exceptions import Unauthorized +from codecov.commands.exceptions import ValidationError as CodecovValidationError +from codecov_auth.models import Owner + +from ..cancel_trial import CancelTrialInteractor + + +class CancelTrialInteractorTest(TestCase): + def setUp(self): + self.tier = TierFactory(tier_name=DEFAULT_FREE_PLAN) + self.plan = PlanFactory(tier=self.tier) + + @async_to_sync + def execute(self, current_user, org_username=None): + current_user = current_user + return CancelTrialInteractor(current_user, "github").execute( + org_username=org_username, + ) + + def test_cancel_trial_raises_exception_when_owner_is_not_in_db(self): + current_user = OwnerFactory( + username="random-user-123", + service="github", + plan=self.plan.name, + ) + with pytest.raises(CodecovValidationError): + self.execute(current_user=current_user, org_username="some-other-username") + + def test_cancel_trial_raises_exception_when_current_user_not_part_of_org(self): + current_user = OwnerFactory( + username="random-user-123", + service="github", + plan=self.plan.name, + ) + OwnerFactory( + username="random-user-456", + service="github", + plan=self.plan.name, + ) + with pytest.raises(Unauthorized): + self.execute(current_user=current_user, org_username="random-user-456") + + @freeze_time("2022-01-01T00:00:00") + def test_cancel_trial_raises_exception_when_owners_trial_status_is_not_started( + self, + ): + trial_start_date = None + trial_end_date = None + current_user = OwnerFactory( + username="random-user-123", + service="github", + trial_start_date=trial_start_date, + trial_end_date=trial_end_date, + plan=self.plan.name, + ) + with pytest.raises(ValidationError): + self.execute(current_user=current_user, org_username=current_user.username) + + @freeze_time("2022-01-01T00:00:00") + def test_cancel_trial_raises_exception_when_owners_trial_status_is_expired(self): + now = datetime.now() + trial_start_date = now + timedelta(days=-10) + trial_end_date = now + timedelta(days=-4) + current_user = OwnerFactory( + username="random-user-123", + service="github", + trial_start_date=trial_start_date, + trial_end_date=trial_end_date, + plan=self.plan.name, + ) + with pytest.raises(ValidationError): + self.execute(current_user=current_user, org_username=current_user.username) + + @freeze_time("2022-01-01T00:00:00") + def test_cancel_trial_starts_trial_for_org_that_has_trial_ongoing(self): + now = datetime.now() + trial_start_date = now + trial_end_date = now + timedelta(days=3) + trial_tier = TierFactory(tier_name=TierName.TRIAL.value) + trial_plan = PlanFactory(tier=trial_tier, name=PlanName.TRIAL_PLAN_NAME.value) + current_user: Owner = OwnerFactory( + username="random-user-123", + service="github", + trial_start_date=trial_start_date, + trial_end_date=trial_end_date, + trial_status=TrialStatus.ONGOING.value, + plan=trial_plan.name, + ) + self.execute(current_user=current_user, org_username=current_user.username) + current_user.refresh_from_db() + + now = datetime.now() + assert current_user.trial_end_date == now + assert current_user.trial_status == TrialStatus.EXPIRED.value + assert current_user.plan == DEFAULT_FREE_PLAN + assert current_user.plan_activated_users is None + assert current_user.plan_user_count == 1 + assert current_user.stripe_subscription_id is None diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_create_api_token.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_create_api_token.py new file mode 100644 index 0000000000..f8c3487b60 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_create_api_token.py @@ -0,0 +1,25 @@ +import pytest +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov.commands.exceptions import Unauthenticated, ValidationError + +from ..create_api_token import CreateApiTokenInteractor + + +class CreateApiTokenInteractorTest(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + + async def test_when_unauthenticated_raise(self): + with pytest.raises(Unauthenticated): + await CreateApiTokenInteractor(None, "github").execute("name") + + async def test_when_no_name_raise(self): + with pytest.raises(ValidationError): + await CreateApiTokenInteractor(self.owner, "github").execute("") + + async def test_create_token(self): + session = await CreateApiTokenInteractor(self.owner, "github").execute("name") + assert session is not None + assert session.owner is self.owner diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_create_user_token.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_create_user_token.py new file mode 100644 index 0000000000..ea606a7d16 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_create_user_token.py @@ -0,0 +1,35 @@ +import pytest +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov.commands.exceptions import Unauthenticated, ValidationError +from codecov_auth.models import UserToken + +from ..create_user_token import CreateUserTokenInteractor + + +class CreateUserTokenInteractorTest(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + + async def test_unauthenticated(self): + with pytest.raises(Unauthenticated): + await CreateUserTokenInteractor(None, "github").execute("name") + + async def test_empty_name(self): + with pytest.raises(ValidationError): + await CreateUserTokenInteractor(self.owner, "github").execute("") + + async def test_invalid_type(self): + with pytest.raises(ValidationError): + await CreateUserTokenInteractor(self.owner, "github").execute( + "name", "wrong" + ) + + async def test_create_token(self): + user_token = await CreateUserTokenInteractor(self.owner, "github").execute( + "name" + ) + assert user_token is not None + assert user_token.owner is self.owner + assert user_token.token_type == UserToken.TokenType.API diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_delete_session.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_delete_session.py new file mode 100644 index 0000000000..d5c500045f --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_delete_session.py @@ -0,0 +1,42 @@ +import pytest +from asgiref.sync import sync_to_async +from django.test import TestCase +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory, SessionFactory + +from codecov.commands.exceptions import Unauthenticated +from codecov_auth.models import DjangoSession, Session +from codecov_auth.tests.factories import DjangoSessionFactory + +from ..delete_session import DeleteSessionInteractor + + +@sync_to_async +def get_session(id): + return Session.objects.get(sessionid=id) + + +class DeleteSessionInteractorTest(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + self.django_session = DjangoSessionFactory() + self.session = SessionFactory( + owner=self.owner, login_session=self.django_session + ) + + async def test_when_unauthenticated_raise(self): + with pytest.raises(Unauthenticated): + await DeleteSessionInteractor(None, "github").execute(12) + + async def test_delete_session(self): + await DeleteSessionInteractor(self.owner, "github").execute( + self.session.sessionid + ) + + @sync_to_async + def assert_sessions(): + return ( + len(DjangoSession.objects.all()) == 0 + and len(Session.objects.all()) == 0 + ) + + assert assert_sessions diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_get_is_current_user_an_admin.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_get_is_current_user_an_admin.py new file mode 100644 index 0000000000..be65f3cb33 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_get_is_current_user_an_admin.py @@ -0,0 +1,152 @@ +from unittest.mock import patch + +from asgiref.sync import async_to_sync +from django.test import TestCase, override_settings +from shared.django_apps.codecov_auth.tests.factories import ( + GetAdminProviderAdapter, + OwnerFactory, +) + +from ..get_is_current_user_an_admin import ( + GetIsCurrentUserAnAdminInteractor, + _is_admin_on_provider, +) + + +class GetIsCurrentUserAnAdminInteractorTest(TestCase): + def setUp(self): + self.owner_has_admins = OwnerFactory(ownerid=0, admins=[2]) + self.owner_has_no_admins = OwnerFactory(ownerid=1, admins=[]) + self.owner_null_admins = OwnerFactory(ownerid=9, admins=None) + + def test_user_admin_in_personal_org(self): + current_user = self.owner_has_admins + owner = self.owner_has_admins + isAdmin = async_to_sync( + GetIsCurrentUserAnAdminInteractor(owner, current_user).execute + )(owner, current_user) + assert isAdmin == True + + def test_user_not_admin_in_org(self): + current_user = OwnerFactory(ownerid=3) + owner = self.owner_has_admins + isAdmin = async_to_sync( + GetIsCurrentUserAnAdminInteractor(owner, current_user).execute + )(owner, current_user) + assert isAdmin == False + + def test_user_not_a_provider_admin(self): + current_user = OwnerFactory(ownerid=3) + owner = self.owner_has_no_admins + isAdmin = async_to_sync( + GetIsCurrentUserAnAdminInteractor(owner, current_user).execute + )(owner, current_user) + assert isAdmin == False + + @patch( + "codecov_auth.commands.owner.interactors.get_is_current_user_an_admin.get_provider" + ) + def test_is_admin_on_provider_invokes_torngit_adapter(self, mocked_get_adapter): + current_user = OwnerFactory(ownerid=3) + owner = self.owner_has_no_admins + mocked_get_adapter.return_value = GetAdminProviderAdapter() + async_to_sync(_is_admin_on_provider)(owner, current_user) + assert mocked_get_adapter.return_value.last_call_args == { + "username": current_user.username, + "service_id": current_user.service_id, + } + + @patch( + "codecov_auth.commands.owner.interactors.get_is_current_user_an_admin.get_provider" + ) + def test_is_admin_in_org_not_on_provider(self, mocked_get_adapter): + current_user = OwnerFactory(ownerid=2) + owner = self.owner_has_admins + mocked_get_adapter.return_value = GetAdminProviderAdapter(result=False) + isAdmin = async_to_sync( + GetIsCurrentUserAnAdminInteractor(owner, current_user).execute + )(owner, current_user) + assert isAdmin == True + + @patch( + "codecov_auth.commands.owner.interactors.get_is_current_user_an_admin.get_provider" + ) + def test_is_admin_on_provider(self, mocked_get_adapter): + current_user = OwnerFactory(ownerid=3) + owner = self.owner_has_no_admins + mocked_get_adapter.return_value = GetAdminProviderAdapter(result=True) + isAdmin = async_to_sync( + GetIsCurrentUserAnAdminInteractor(owner, current_user).execute + )(owner, current_user) + assert current_user.ownerid in owner.admins + assert isAdmin == True + + @patch( + "codecov_auth.commands.owner.interactors.get_is_current_user_an_admin.get_provider" + ) + def test_is_admin_on_provider_only_once(self, mocked_get_adapter): + # Ensure duplicate ownerids won't be saved in admins upon multiple calls + current_user = OwnerFactory(ownerid=3) + owner = self.owner_has_no_admins + mocked_get_adapter.return_value = GetAdminProviderAdapter(result=True) + async_to_sync(GetIsCurrentUserAnAdminInteractor(owner, current_user).execute)( + owner, current_user + ) + async_to_sync(GetIsCurrentUserAnAdminInteractor(owner, current_user).execute)( + owner, current_user + ) + assert owner.admins == [3] + assert current_user.ownerid in owner.admins + + @patch( + "codecov_auth.commands.owner.interactors.get_is_current_user_an_admin.get_provider" + ) + def test_admin_on_provider_initially_is_null(self, mocked_get_adapter): + # Owner model defaults admins to null, check can handle first update + current_user = OwnerFactory(ownerid=3) + owner = self.owner_null_admins + mocked_get_adapter.return_value = GetAdminProviderAdapter(result=True) + isAdmin = async_to_sync( + GetIsCurrentUserAnAdminInteractor(owner, current_user).execute + )(owner, current_user) + assert owner.admins == [3] + assert current_user.ownerid in owner.admins + assert isAdmin == True + + @patch( + "codecov_auth.commands.owner.interactors.get_is_current_user_an_admin.get_provider" + ) + def test_is_admin_not_in_org_or_on_provider(self, mocked_get_adapter): + current_user = OwnerFactory(ownerid=3) + owner = self.owner_has_no_admins + mocked_get_adapter.return_value = GetAdminProviderAdapter(result=False) + isAdmin = async_to_sync( + GetIsCurrentUserAnAdminInteractor(owner, current_user).execute + )(owner, current_user) + assert current_user.ownerid not in owner.admins + assert isAdmin == False + + @patch("services.self_hosted.is_admin_owner") + @override_settings(IS_ENTERPRISE=True) + def test_is_admin_self_hosted(self, is_admin_owner): + current_user = OwnerFactory(ownerid=3) + owner = OwnerFactory(ownerid=4) + + is_admin_owner.return_value = False + is_admin = async_to_sync( + GetIsCurrentUserAnAdminInteractor(owner, current_user).execute + )(owner, current_user) + assert is_admin == False + + is_admin_owner.return_value = True + is_admin = async_to_sync( + GetIsCurrentUserAnAdminInteractor(owner, current_user).execute + )(owner, current_user) + assert is_admin == True + + def test_is_admin_no_current_owner(self): + owner = OwnerFactory(ownerid=5) + res = async_to_sync(GetIsCurrentUserAnAdminInteractor(owner, "gitlab").execute)( + owner, None + ) + assert res == False diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_get_org_upload_token.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_get_org_upload_token.py new file mode 100644 index 0000000000..193484648c --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_get_org_upload_token.py @@ -0,0 +1,46 @@ +import pytest +from django.test import TestCase +from shared.django_apps.codecov_auth.tests.factories import ( + OrganizationLevelTokenFactory, + OwnerFactory, +) + +from codecov.commands.exceptions import Unauthenticated, Unauthorized + +from ..get_org_upload_token import GetOrgUploadToken + + +class GetOrgUploadTokenInteractorTest(TestCase): + def setUp(self): + self.owner_with_no_upload_token = OwnerFactory() + self.owner_with_upload_token = OwnerFactory(plan="users-enterprisem") + OrganizationLevelTokenFactory(owner=self.owner_with_upload_token) + + async def test_owner_with_no_org_upload_token(self): + token = await GetOrgUploadToken( + self.owner_with_no_upload_token, "github" + ).execute(self.owner_with_no_upload_token) + assert token is None + + async def test_owner_with_org_upload_token(self): + token = await GetOrgUploadToken(self.owner_with_upload_token, "github").execute( + self.owner_with_upload_token + ) + assert token + assert len(str(token)) == 36 # default uuid + + async def test_owner_with_org_upload_token_and_anonymous_user(self): + with pytest.raises(Unauthenticated): + token = await GetOrgUploadToken(None, "github").execute( + self.owner_with_upload_token + ) + + assert token is None + + async def test_owner_with_org_upload_token_and_unauthorized_user(self): + with pytest.raises(Unauthorized): + token = await GetOrgUploadToken( + self.owner_with_upload_token, "github" + ).execute(self.owner_with_no_upload_token) + + assert token is None diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_get_uploads_number_per_user.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_get_uploads_number_per_user.py new file mode 100644 index 0000000000..4812a831d3 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_get_uploads_number_per_user.py @@ -0,0 +1,122 @@ +from datetime import datetime, timedelta + +from django.test import TestCase +from shared.django_apps.codecov_auth.tests.factories import PlanFactory, TierFactory +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.django_apps.reports.models import ReportType +from shared.plan.constants import PlanName, TierName, TrialStatus +from shared.upload.utils import UploaderType, insert_coverage_measurement + +from reports.tests.factories import CommitReportFactory, UploadFactory + +from ..get_uploads_number_per_user import GetUploadsNumberPerUserInteractor + + +class GetUploadsNumberPerUserInteractorTest(TestCase): + def setUp(self): + self.tier = TierFactory(tier_name=TierName.BASIC.value) + self.plan = PlanFactory(tier=self.tier, monthly_uploads_limit=250) + self.user_with_no_uploads = OwnerFactory(plan=self.plan.name) + self.user_with_uploads = OwnerFactory(plan=self.plan.name) + repo = RepositoryFactory.create(author=self.user_with_uploads, private=True) + commit = CommitFactory.create(repository=repo) + report = CommitReportFactory.create( + commit=commit, report_type=ReportType.COVERAGE.value + ) + + # Reports all created today/within the last 30 days + for i in range(2): + # Explicit add insert_coverage_measurement as we'll do this every time that we make an upload + upload = UploadFactory.create(report=report) + insert_coverage_measurement( + owner_id=self.user_with_uploads.ownerid, + repo_id=repo.repoid, + commit_id=commit.id, + upload_id=upload.id, + uploader_used=UploaderType.CLI.value, + private_repo=repo.private, + report_type=report.report_type, + ) + + report_within_40_days = UploadFactory.create(report=report) + report_within_40_days.created_at += timedelta(days=-40) + report_within_40_days.save() + + # Trial Data + trial_tier = TierFactory(tier_name=TierName.TRIAL.value) + trial_plan = PlanFactory( + tier=trial_tier, + name=PlanName.TRIAL_PLAN_NAME.value, + monthly_uploads_limit=250, + ) + self.trial_owner = OwnerFactory( + trial_status=TrialStatus.EXPIRED.value, + trial_start_date=datetime.now() + timedelta(days=-10), + trial_end_date=datetime.now() + timedelta(days=-2), + plan=trial_plan.name, + ) + trial_repo = RepositoryFactory.create(author=self.trial_owner, private=True) + trial_commit = CommitFactory.create(repository=trial_repo) + trial_report = CommitReportFactory.create( + commit=trial_commit, report_type=ReportType.COVERAGE.value + ) + + report_before_trial = UploadFactory.create(report=trial_report) + report_before_trial.created_at += timedelta(days=-12) + report_before_trial.save() + upload_before_trial = insert_coverage_measurement( + owner_id=self.trial_owner.ownerid, + repo_id=repo.repoid, + commit_id=commit.id, + upload_id=report_before_trial.id, + uploader_used=UploaderType.CLI.value, + private_repo=repo.private, + report_type=report.report_type, + ) + upload_before_trial.created_at += timedelta(days=-12) + upload_before_trial.save() + + report_during_trial = UploadFactory.create(report=trial_report) + report_during_trial.created_at += timedelta(days=-5) + report_during_trial.save() + upload_during_trial = insert_coverage_measurement( + owner_id=self.trial_owner.ownerid, + repo_id=repo.repoid, + commit_id=commit.id, + upload_id=report_during_trial.id, + uploader_used=UploaderType.CLI.value, + private_repo=repo.private, + report_type=report.report_type, + ) + upload_during_trial.created_at += timedelta(days=-5) + upload_during_trial.save() + + report_after_trial = UploadFactory.create(report=trial_report) + insert_coverage_measurement( + owner_id=self.trial_owner.ownerid, + repo_id=repo.repoid, + commit_id=commit.id, + upload_id=report_after_trial.id, + uploader_used=UploaderType.CLI.value, + private_repo=repo.private, + report_type=report.report_type, + ) + + async def test_with_no_uploads(self): + owner = self.user_with_no_uploads + uploads = await GetUploadsNumberPerUserInteractor(None, owner).execute(owner) + assert uploads == 0 + + async def test_with_number_of_uploads(self): + owner = self.user_with_uploads + uploads = await GetUploadsNumberPerUserInteractor(None, owner).execute(owner) + assert uploads == 2 + + async def test_number_of_uploads_with_expired_trial(self): + owner = self.trial_owner + uploads = await GetUploadsNumberPerUserInteractor(None, owner).execute(owner) + assert uploads == 2 diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_is_syncing.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_is_syncing.py new file mode 100644 index 0000000000..4c37daac67 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_is_syncing.py @@ -0,0 +1,20 @@ +from unittest.mock import patch + +from asgiref.sync import async_to_sync +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from ..is_syncing import IsSyncingInteractor + + +class IsSyncingInteractorTest(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + + @patch("services.refresh.RefreshService.is_refreshing") + @async_to_sync + async def test_call_is_refreshing(self, mock_is_refreshing): + mock_is_refreshing.return_value = True + res = await IsSyncingInteractor(self.owner, "github").execute() + assert res is True + mock_is_refreshing.assert_called() diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_onboard_user.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_onboard_user.py new file mode 100644 index 0000000000..ead455d755 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_onboard_user.py @@ -0,0 +1,49 @@ +import pytest +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov.commands.exceptions import Unauthenticated, Unauthorized, ValidationError +from codecov_auth.commands.owner.interactors.onboard_user import OnboardUserInteractor +from codecov_auth.models import OwnerProfile + + +class OnboardUserInteractorTest(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + self.already_onboarded_owner = OwnerFactory( + username="codecov-user", onboarding_completed=True + ) + self.good_params = { + "email": "dev@dev.com", + "business_email": "dev@codecov.io", + "type_projects": [OwnerProfile.ProjectType.PERSONAL], + "goals": [OwnerProfile.Goal.STARTING_WITH_TESTS, OwnerProfile.Goal.OTHER], + "other_goal": "feel confident in my code", + } + + async def test_when_unauthenticated_raise(self): + with pytest.raises(Unauthenticated): + await OnboardUserInteractor(None, "github").execute(self.good_params) + + async def test_when_user_already_completed_onboarding(self): + with pytest.raises(Unauthorized): + await OnboardUserInteractor( + self.already_onboarded_owner, + "github", + ).execute(self.good_params) + + async def test_when_params_arent_good(self): + with pytest.raises(ValidationError): + await OnboardUserInteractor(self.owner, "github").execute( + {**self.good_params, "email": "notgood"} + ) + + async def test_when_everything_is_good(self): + user = await OnboardUserInteractor(self.owner, "github").execute( + self.good_params + ) + assert user.email == self.good_params["email"] + assert user.business_email == self.good_params["business_email"] + assert user.profile.type_projects == self.good_params["type_projects"] + assert user.profile.goals == self.good_params["goals"] + assert user.profile.other_goal == self.good_params["other_goal"] diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_regenerate_org_upload_token.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_regenerate_org_upload_token.py new file mode 100644 index 0000000000..388b4a9d97 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_regenerate_org_upload_token.py @@ -0,0 +1,39 @@ +import pytest +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov.commands.exceptions import Unauthenticated, Unauthorized, ValidationError + +from ..regenerate_org_upload_token import RegenerateOrgUploadTokenInteractor + + +class RegenerateOrgUploadTokenInteractorTest(TestCase): + def setUp(self): + self.random_user = OwnerFactory() + self.owner = OwnerFactory(username="codecovv", plan="users-enterprisem") + self.user_not_part_of_org = OwnerFactory( + username="random", plan="users-enterprisem" + ) + + def execute(self, owner, org_owner=None): + return RegenerateOrgUploadTokenInteractor(owner, "github").execute( + owner=org_owner + ) + + async def test_when_unauthenticated_raise(self): + with pytest.raises(Unauthenticated): + await self.execute(owner="") + + async def test_when_validation_no_owner_found(self): + with pytest.raises(ValidationError): + await self.execute(owner=self.random_user) + + async def test_regenerate_org_upload_token_user_not_part_of_org(self): + with pytest.raises(Unauthorized): + await self.execute( + owner=self.user_not_part_of_org, org_owner=self.owner.username + ) + + async def test_regenerate_org_upload_token(self): + token = await self.execute(owner=self.owner, org_owner=self.owner.username) + assert token is not None diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_revoke_user_token.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_revoke_user_token.py new file mode 100644 index 0000000000..8e6e645a08 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_revoke_user_token.py @@ -0,0 +1,34 @@ +import pytest +from asgiref.sync import sync_to_async +from django.test import TestCase +from shared.django_apps.codecov_auth.tests.factories import ( + OwnerFactory, + UserTokenFactory, +) + +from codecov.commands.exceptions import Unauthenticated +from codecov_auth.models import UserToken + +from ..revoke_user_token import RevokeUserTokenInteractor + + +@sync_to_async +def get_user_token(external_id): + return UserToken.objects.get(external_id=external_id) + + +class RevokeUserTokenInteractorTest(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + self.user_token = UserTokenFactory(owner=self.owner) + + async def test_unauthenticated(self): + with pytest.raises(Unauthenticated): + await RevokeUserTokenInteractor(None, "github").execute(123) + + async def test_revoke_user_token(self): + await RevokeUserTokenInteractor(self.owner, "github").execute( + self.user_token.external_id + ) + with pytest.raises(UserToken.DoesNotExist): + await get_user_token(self.user_token.external_id) diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_save_okta_config.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_save_okta_config.py new file mode 100644 index 0000000000..8d00cce30e --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_save_okta_config.py @@ -0,0 +1,249 @@ +import pytest +from asgiref.sync import async_to_sync +from django.contrib.auth.models import AnonymousUser +from django.test import TestCase +from shared.django_apps.codecov_auth.tests.factories import ( + AccountFactory, + OktaSettingsFactory, + OwnerFactory, +) + +from codecov.commands.exceptions import Unauthenticated, Unauthorized, ValidationError +from codecov_auth.models import OktaSettings + +from ..save_okta_config import SaveOktaConfigInteractor + + +class SaveOktaConfigInteractorTest(TestCase): + def setUp(self): + self.current_user = OwnerFactory(username="codecov-user") + self.service = "github" + user1 = OwnerFactory() + user2 = OwnerFactory() + self.owner = OwnerFactory( + username=self.current_user.username, + service=self.service, + account=AccountFactory(), + ) + + self.owner_with_admins = OwnerFactory( + username=self.current_user.username, + service=self.service, + admins=[self.current_user.ownerid], + plan_activated_users=[user1.ownerid, user2.ownerid], + account=None, + ) + + self.interactor = SaveOktaConfigInteractor( + current_owner=self.owner, + service=self.service, + current_user=self.current_user, + ) + + @async_to_sync + def execute( + self, + interactor: SaveOktaConfigInteractor | None = None, + input: dict | None = None, + ): + if not interactor and self.interactor: + interactor = self.interactor + + if not interactor: + return + return interactor.execute(input) + + def test_user_is_not_authenticated(self): + with pytest.raises(Unauthenticated): + self.execute( + interactor=SaveOktaConfigInteractor( + current_owner=None, + service=self.service, + current_user=AnonymousUser(), + ), + input={ + "client_id": "some-client-id", + "client_secret": "some-client-secret", + "url": "https://okta.example.com", + "enabled": True, + "enforced": True, + "org_username": self.owner.username, + }, + ) + + def test_validation_error_when_owner_not_found(self): + with pytest.raises(ValidationError): + self.execute( + input={ + "client_id": "some-client-id", + "client_secret": "some-client-secret", + "url": "https://okta.example.com", + "enabled": True, + "enforced": True, + "org_username": "non-existent-user", + }, + ) + + def test_unauthorized_error_when_user_is_not_admin(self): + with pytest.raises(Unauthorized): + self.execute( + input={ + "client_id": "some-client-id", + "client_secret": "some-client-secret", + "url": "https://okta.example.com", + "enabled": True, + "enforced": True, + "org_username": self.owner.username, + }, + ) + + def test_create_okta_settings_when_account_does_not_exist(self): + plan_activated_users = [] + for _ in range(100): + user_owner = OwnerFactory(user=None) + plan_activated_users.append(user_owner.ownerid) + + org_with_lots_of_users = OwnerFactory( + service=self.service, + admins=[self.current_user.ownerid], + plan_activated_users=plan_activated_users, + ) + + input_data = { + "client_id": "some-client-id", + "client_secret": "some-client-secret", + "url": "https://okta.example.com", + "enabled": True, + "enforced": True, + "org_username": org_with_lots_of_users.username, + } + + interactor = SaveOktaConfigInteractor( + current_owner=self.current_user, service=self.service + ) + self.execute(interactor=interactor, input=input_data) + + org_with_lots_of_users.refresh_from_db() + account = org_with_lots_of_users.account + + assert account.name == org_with_lots_of_users.username + assert account.plan == org_with_lots_of_users.plan + assert account.plan_seat_count == org_with_lots_of_users.plan_user_count + assert account.free_seat_count == org_with_lots_of_users.free + + assert account.users.count() == 100 + assert account.users.count() == len(org_with_lots_of_users.plan_activated_users) + + okta_config = OktaSettings.objects.get(account=org_with_lots_of_users.account) + + assert okta_config.client_id == input_data["client_id"] + assert okta_config.client_secret == input_data["client_secret"] + assert okta_config.url == input_data["url"] + assert okta_config.enabled == input_data["enabled"] + assert okta_config.enforced == input_data["enforced"] + + def test_update_okta_settings_when_account_exists(self): + input_data = { + "client_id": "some-client-id", + "client_secret": "some-client-secret", + "url": "https://okta.example.com", + "enabled": True, + "enforced": True, + "org_username": self.owner_with_admins.username, + } + + account = AccountFactory() + self.owner_with_admins.account = account + self.owner_with_admins.save() + + interactor = SaveOktaConfigInteractor( + current_owner=self.current_user, service=self.service + ) + self.execute(interactor=interactor, input=input_data) + + okta_config = OktaSettings.objects.get(account=self.owner_with_admins.account) + + assert okta_config.client_id == input_data["client_id"] + assert okta_config.client_secret == input_data["client_secret"] + assert okta_config.url == input_data["url"] + assert okta_config.enabled == input_data["enabled"] + assert okta_config.enforced == input_data["enforced"] + + def test_update_okta_settings_url_remove_trailing_slashes(self): + input_data = { + "client_id": "some-client-id", + "client_secret": "some-client-secret", + "url": "https://okta.example.com/", + "enabled": True, + "enforced": True, + "org_username": self.owner_with_admins.username, + } + + account = AccountFactory() + self.owner_with_admins.account = account + self.owner_with_admins.save() + + interactor = SaveOktaConfigInteractor( + current_owner=self.current_user, service=self.service + ) + self.execute(interactor=interactor, input=input_data) + + okta_config = OktaSettings.objects.get(account=self.owner_with_admins.account) + + assert okta_config.url == "https://okta.example.com" + + def test_update_okta_settings_when_okta_settings_exists(self): + input_data = { + "client_id": "some-client-id", + "client_secret": "some-client-secret", + "url": "https://okta.example.com", + "enabled": True, + "enforced": True, + "org_username": self.owner_with_admins.username, + } + + account = AccountFactory() + OktaSettingsFactory(account=account) + self.owner_with_admins.account = account + self.owner_with_admins.save() + + interactor = SaveOktaConfigInteractor( + current_owner=self.current_user, service=self.service + ) + self.execute(interactor=interactor, input=input_data) + + okta_config = OktaSettings.objects.get(account=self.owner_with_admins.account) + + assert okta_config.client_id == input_data["client_id"] + assert okta_config.client_secret == input_data["client_secret"] + assert okta_config.url == input_data["url"] + assert okta_config.enabled == input_data["enabled"] + assert okta_config.enforced == input_data["enforced"] + + def test_update_okta_settings_when_some_fields_are_none(self): + input_data = { + "client_id": "some-client-id", + "client_secret": None, + "url": None, + "enabled": True, + "enforced": True, + "org_username": self.owner_with_admins.username, + } + + account = AccountFactory() + OktaSettingsFactory(account=account) + self.owner_with_admins.account = account + self.owner_with_admins.save() + + interactor = SaveOktaConfigInteractor( + current_owner=self.current_user, service=self.service + ) + self.execute(interactor=interactor, input=input_data) + + okta_config = OktaSettings.objects.get(account=self.owner_with_admins.account) + + assert okta_config.client_id == input_data["client_id"] + assert okta_config.client_secret is not None + assert okta_config.url is not None + assert okta_config.enabled == input_data["enabled"] + assert okta_config.enforced == input_data["enforced"] diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_save_terms_agreement.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_save_terms_agreement.py new file mode 100644 index 0000000000..07803acc4e --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_save_terms_agreement.py @@ -0,0 +1,256 @@ +from unittest.mock import patch + +import pytest +from asgiref.sync import async_to_sync +from django.contrib.auth.models import AnonymousUser +from django.test import TestCase +from django.utils import timezone +from freezegun import freeze_time +from freezegun.api import FakeDatetime +from shared.django_apps.codecov_auth.tests.factories import UserFactory + +from codecov.commands.exceptions import Unauthenticated, ValidationError + +from ..save_terms_agreement import SaveTermsAgreementInteractor + + +class UpdateSaveTermsAgreementInteractorTest(TestCase): + def setUp(self): + self.current_user = UserFactory(name="codecov-user") + self.updated_at = FakeDatetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + + @async_to_sync + def execute(self, current_user, input=None): + real_input = { + "business_email": None, + "terms_agreement": False, + "marketing_consent": False, + } + if input: + real_input.update(input) + return SaveTermsAgreementInteractor(None, "github", current_user).execute( + input=real_input, + ) + + #### Start of older tests + + @freeze_time("2022-01-01T00:00:00") + def test_deprecated_update_user_when_agreement_is_false(self): + self.execute( + current_user=self.current_user, + input={"terms_agreement": False, "customer_intent": "Business"}, + ) + before_refresh_business_email = self.current_user.email + + assert self.current_user.terms_agreement == False + assert self.current_user.terms_agreement_at == self.updated_at + + self.current_user.refresh_from_db() + assert self.current_user.email == before_refresh_business_email + + @freeze_time("2022-01-01T00:00:00") + def test_deprecated_update_user_when_agreement_is_true(self): + self.execute( + current_user=self.current_user, + input={"terms_agreement": True, "customer_intent": "Business"}, + ) + before_refresh_business_email = self.current_user.email + + assert self.current_user.terms_agreement == True + assert self.current_user.terms_agreement_at == self.updated_at + + self.current_user.refresh_from_db() + assert self.current_user.email == before_refresh_business_email + + @freeze_time("2022-01-01T00:00:00") + def test_deprecated_update_owner_and_user_when_email_is_not_empty(self): + self.execute( + current_user=self.current_user, + input={ + "business_email": "something@email.com", + "terms_agreement": True, + "customer_intent": "Business", + }, + ) + + assert self.current_user.terms_agreement == True + assert self.current_user.terms_agreement_at == self.updated_at + + self.current_user.refresh_from_db() + assert self.current_user.email == "something@email.com" + + def test_deprecated_validation_error_when_customer_intent_invalid(self): + with pytest.raises(ValidationError): + self.execute( + current_user=self.current_user, + input={"terms_agreement": None, "customer_intent": "invalid"}, + ) + + def test_deprecated_user_is_not_authenticated(self): + with pytest.raises(Unauthenticated): + self.execute( + current_user=AnonymousUser(), + input={ + "business_email": "something@email.com", + "terms_agreement": True, + "customer_intent": "Business", + }, + ) + + def test_deprecated_email_opt_in_saved_in_db(self): + self.execute( + current_user=self.current_user, + input={ + "terms_agreement": True, + "marketing_consent": True, + "customer_intent": "Business", + }, + ) + self.current_user.refresh_from_db() + assert self.current_user.email_opt_in == True + + @patch( + "codecov_auth.commands.owner.interactors.save_terms_agreement.SaveTermsAgreementInteractor.send_data_to_marketo" + ) + def test_deprecated_marketo_called_only_with_consent( + self, mock_send_data_to_marketo + ): + self.execute( + current_user=self.current_user, + input={ + "terms_agreement": True, + "marketing_consent": True, + "customer_intent": "Business", + }, + ) + + mock_send_data_to_marketo.assert_called_once() + + @patch( + "codecov_auth.commands.owner.interactors.save_terms_agreement.SaveTermsAgreementInteractor.send_data_to_marketo" + ) + def test_deprecated_marketo_not_called_without_consent( + self, mock_send_data_to_marketo + ): + self.execute( + current_user=self.current_user, + input={ + "terms_agreement": True, + "marketing_consent": False, + "customer_intent": "Business", + }, + ) + + mock_send_data_to_marketo.assert_not_called() + + #### End of older tests + + @freeze_time("2022-01-01T00:00:00") + def test_update_user_when_agreement_is_false(self): + self.execute( + current_user=self.current_user, + input={ + "business_email": "something@email.com", + "name": "codecov-user", + "terms_agreement": False, + }, + ) + before_refresh_business_email = self.current_user.email + + assert self.current_user.terms_agreement == False + assert self.current_user.terms_agreement_at == self.updated_at + + self.current_user.refresh_from_db() + assert self.current_user.email == before_refresh_business_email + + @freeze_time("2022-01-01T00:00:00") + def test_update_user_when_agreement_is_true(self): + self.execute( + current_user=self.current_user, + input={ + "business_email": "something@email.com", + "name": "codecov-user", + "terms_agreement": True, + }, + ) + before_refresh_business_email = self.current_user.email + + assert self.current_user.terms_agreement == True + assert self.current_user.terms_agreement_at == self.updated_at + + self.current_user.refresh_from_db() + assert self.current_user.email == before_refresh_business_email + + @freeze_time("2022-01-01T00:00:00") + def test_update_owner_and_user_when_email_and_name_are_not_empty(self): + self.execute( + current_user=self.current_user, + input={ + "business_email": "something@email.com", + "name": "codecov-user", + "terms_agreement": True, + }, + ) + + assert self.current_user.terms_agreement == True + assert self.current_user.terms_agreement_at == self.updated_at + + self.current_user.refresh_from_db() + assert self.current_user.email == "something@email.com" + assert self.current_user.name == "codecov-user" + + def test_user_is_not_authenticated(self): + with pytest.raises(Unauthenticated): + self.execute( + current_user=AnonymousUser(), + input={ + "business_email": "something@email.com", + "name": "codecov-user", + "terms_agreement": True, + }, + ) + + def test_email_opt_in_saved_in_db(self): + self.execute( + current_user=self.current_user, + input={ + "business_email": "something@email.com", + "name": "codecov-user", + "terms_agreement": True, + "marketing_consent": True, + }, + ) + self.current_user.refresh_from_db() + assert self.current_user.email_opt_in == True + + @patch( + "codecov_auth.commands.owner.interactors.save_terms_agreement.SaveTermsAgreementInteractor.send_data_to_marketo" + ) + def test_marketo_called_only_with_consent(self, mock_send_data_to_marketo): + self.execute( + current_user=self.current_user, + input={ + "business_email": "something@email.com", + "name": "codecov-user", + "terms_agreement": True, + "marketing_consent": True, + }, + ) + + mock_send_data_to_marketo.assert_called_once() + + @patch( + "codecov_auth.commands.owner.interactors.save_terms_agreement.SaveTermsAgreementInteractor.send_data_to_marketo" + ) + def test_marketo_not_called_without_consent(self, mock_send_data_to_marketo): + self.execute( + current_user=self.current_user, + input={ + "business_email": "something@email.com", + "name": "codecov-user", + "terms_agreement": True, + "marketing_consent": False, + }, + ) + + mock_send_data_to_marketo.assert_not_called() diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_set_upload_token_required.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_set_upload_token_required.py new file mode 100644 index 0000000000..205b12af1d --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_set_upload_token_required.py @@ -0,0 +1,92 @@ +import pytest +from asgiref.sync import async_to_sync +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov.commands.exceptions import Unauthenticated, Unauthorized, ValidationError + +from ..set_upload_token_required import SetUploadTokenRequiredInteractor + + +class SetUploadTokenRequiredInteractorTest(TestCase): + def setUp(self): + self.service = "github" + self.current_user = OwnerFactory(username="codecov-user") + self.owner = OwnerFactory( + username="codecov-owner", + service=self.service, + ) + self.owner_with_admins = OwnerFactory( + username="codecov-admin-owner", + service=self.service, + admins=[self.current_user.ownerid], + ) + + @async_to_sync + async def execute( + self, current_user, org_username=None, upload_token_required=True + ): + interactor = SetUploadTokenRequiredInteractor(current_user, self.service) + return await interactor.execute( + { + "upload_token_required": upload_token_required, + "org_username": org_username, + } + ) + + def test_user_is_not_authenticated(self): + with pytest.raises(Unauthenticated): + self.execute( + current_user=None, + org_username=self.owner.username, + ) + + def test_validation_error_when_owner_not_found(self): + with pytest.raises(ValidationError): + self.execute( + current_user=self.current_user, + org_username="non-existent-user", + ) + + def test_unauthorized_error_when_user_is_not_admin(self): + with pytest.raises(Unauthorized): + self.execute( + current_user=self.current_user, + org_username=self.owner.username, + ) + + def test_set_upload_token_required_when_user_is_admin(self): + self.current_user.organizations = [self.owner_with_admins.ownerid] + self.current_user.save() + + self.execute( + current_user=self.current_user, + org_username=self.owner_with_admins.username, + ) + + self.owner_with_admins.refresh_from_db() + assert self.owner_with_admins.upload_token_required_for_public_repos == True + + def test_set_upload_token_required_to_false(self): + self.current_user.organizations = [self.owner_with_admins.ownerid] + self.current_user.save() + + self.execute( + current_user=self.current_user, + org_username=self.owner_with_admins.username, + upload_token_required=False, + ) + + self.owner_with_admins.refresh_from_db() + assert self.owner_with_admins.upload_token_required_for_public_repos == False + + def test_set_upload_token_required_to_null_raises_validation_error(self): + self.current_user.organizations = [self.owner_with_admins.ownerid] + self.current_user.save() + + with pytest.raises(ValidationError): + self.execute( + current_user=self.current_user, + org_username=self.owner_with_admins.username, + upload_token_required=None, + ) diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_set_yaml_on_owner.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_set_yaml_on_owner.py new file mode 100644 index 0000000000..b82ad35232 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_set_yaml_on_owner.py @@ -0,0 +1,182 @@ +import pytest +from asgiref.sync import sync_to_async +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from codecov.commands.exceptions import ( + NotFound, + Unauthenticated, + Unauthorized, + ValidationError, +) + +from ..set_yaml_on_owner import SetYamlOnOwnerInteractor + +good_yaml = """ +codecov: + require_ci_to_pass: yes +""" + +good_yaml_with_quotes = """ +codecov: + bot: 'codecov' +""" + +good_yaml_with_bot_and_branch = """ +codecov: + branch: 'test-1' + bot: 'codecov' +""" + +yaml_with_changed_branch_and_bot = """ +codecov: + branch: 'test-2' + bot: 'codecov-2' +""" + +bad_yaml_not_dict = """ +hey +""" + +bad_yaml_wrong_keys = """ +toto: + tata: titi +""" + +bad_yaml_syntax_error = """ +codecov: + bot: foo: bar +""" + +good_yaml_with_comments = """ +# comment 1 +codecov: # comment 2 + + + bot: 'codecov' +# comment 3 + #comment 4 +""" + + +class SetYamlOnOwnerInteractorTest(TestCase): + def setUp(self): + self.org = OwnerFactory() + self.current_owner = OwnerFactory( + organizations=[self.org.ownerid], service=self.org.service + ) + self.random_owner = OwnerFactory(service=self.org.service) + self.codecov_bot = OwnerFactory( + username="codecov", + organizations=[self.org.ownerid], + private_access=True, + ) + self.codecov2_bot = OwnerFactory( + username="codecov-2", + organizations=[self.org.ownerid], + private_access=True, + ) + + # helper to execute the interactor + def execute(self, owner, *args): + service = owner.service if owner else "github" + return SetYamlOnOwnerInteractor(owner, service).execute(*args) + + async def test_when_unauthenticated_raise(self): + with pytest.raises(Unauthenticated): + await self.execute(None, self.org.username, good_yaml) + + async def test_when_not_path_of_org_raise(self): + with pytest.raises(Unauthorized): + await self.execute(self.random_owner, self.org.username, good_yaml) + + async def test_when_owner_not_found_raise(self): + with pytest.raises(NotFound): + await self.execute( + self.current_owner, "thing that should not exist", good_yaml + ) + + async def test_user_is_part_of_org_and_yaml_is_good(self): + owner_updated = await self.execute( + self.current_owner, self.org.username, good_yaml + ) + # check the interactor returns the right owner + assert owner_updated.ownerid == self.org.ownerid + assert owner_updated.yaml == { + "codecov": { + "require_ci_to_pass": True, + }, + "to_string": "\ncodecov:\n require_ci_to_pass: yes\n", + } + + async def test_user_is_part_of_org_and_yaml_has_quotes(self): + owner_updated = await self.execute( + self.current_owner, self.org.username, good_yaml_with_quotes + ) + # check the interactor returns the right owner + assert owner_updated.ownerid == self.org.ownerid + assert owner_updated.yaml == { + "codecov": { + "bot": "codecov", + }, + "to_string": "\ncodecov:\n bot: 'codecov'\n", + } + + async def test_user_is_part_of_org_and_yaml_is_empty(self): + owner_updated = await self.execute(self.current_owner, self.org.username, "") + assert owner_updated.yaml is None + + async def test_user_is_part_of_org_and_yaml_is_not_dict(self): + with pytest.raises(ValidationError) as e: + await self.execute(self.current_owner, self.org.username, bad_yaml_not_dict) + assert str(e.value) == "Error at []: Yaml needs to be a dict" + + async def test_user_is_part_of_org_and_yaml_is_not_codecov_valid(self): + with pytest.raises(ValidationError): + await self.execute( + self.current_owner, self.org.username, bad_yaml_wrong_keys + ) + + async def test_yaml_syntax_error(self): + with pytest.raises(ValidationError) as e: + await self.execute( + self.current_owner, self.org.username, bad_yaml_syntax_error + ) + assert ( + str(e.value) + == "Syntax error at line 3, column 13: mapping values are not allowed here" + ) + + async def test_yaml_has_comments(self): + owner_updated = await self.execute( + self.current_owner, self.org.username, good_yaml_with_comments + ) + # check the interactor returns the right owner + assert owner_updated.ownerid == self.org.ownerid + assert owner_updated.yaml == { + "codecov": { + "bot": "codecov", + }, + "to_string": "\n" + "# comment 1\n" + "codecov: # comment 2\n" + "\n" + "\n" + " bot: 'codecov'\n" + "# comment 3\n" + " #comment 4\n", + } + + async def test_user_changes_yaml_bot_and_branch(self): + await sync_to_async(RepositoryFactory)(author=self.org, branch="fake-branch") + assert self.current_owner.bot_id is None + owner_updated = await self.execute( + self.current_owner, self.org.username, yaml_with_changed_branch_and_bot + ) + # check the interactor returns the right owner + assert owner_updated.ownerid == self.org.ownerid + assert owner_updated.yaml == { + "codecov": {"branch": "test-2", "bot": "codecov-2"}, + "to_string": "\ncodecov:\n branch: 'test-2'\n bot: 'codecov-2'\n", + } + assert owner_updated.bot_id == self.codecov2_bot.ownerid diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_start_trial.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_start_trial.py new file mode 100644 index 0000000000..fe00a38d90 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_start_trial.py @@ -0,0 +1,133 @@ +from datetime import datetime, timedelta + +import pytest +from asgiref.sync import async_to_sync +from django.test import TestCase +from freezegun import freeze_time +from shared.django_apps.codecov.commands.exceptions import ValidationError +from shared.django_apps.codecov_auth.tests.factories import PlanFactory, TierFactory +from shared.django_apps.core.tests.factories import OwnerFactory +from shared.plan.constants import ( + TRIAL_PLAN_SEATS, + PlanName, + TierName, + TrialDaysAmount, + TrialStatus, +) + +from codecov.commands.exceptions import Unauthorized +from codecov.commands.exceptions import ValidationError as CodecovValidationError +from codecov_auth.models import Owner + +from ..start_trial import StartTrialInteractor + + +class StartTrialInteractorTest(TestCase): + def setUp(self): + self.tier = TierFactory(tier_name=TierName.BASIC.value) + self.plan = PlanFactory(tier=self.tier, is_active=True) + + @async_to_sync + def execute(self, current_user, org_username=None): + current_user = current_user + return StartTrialInteractor(current_user, "github").execute( + org_username=org_username, + ) + + def test_start_trial_raises_exception_when_owner_is_not_in_db(self): + current_user = OwnerFactory( + username="random-user-123", + service="github", + plan=self.plan.name, + ) + with pytest.raises(CodecovValidationError): + self.execute(current_user=current_user, org_username="some-other-username") + + def test_cancel_trial_raises_exception_when_current_user_not_part_of_org(self): + current_user = OwnerFactory( + username="random-user-123", + service="github", + plan=self.plan.name, + ) + OwnerFactory( + username="random-user-456", + service="github", + plan=self.plan.name, + ) + with pytest.raises(Unauthorized): + self.execute(current_user=current_user, org_username="random-user-456") + + @freeze_time("2022-01-01T00:00:00") + def test_start_trial_raises_exception_when_owners_trial_status_is_ongoing(self): + now = datetime.now() + trial_start_date = now + trial_end_date = now + timedelta(days=3) + current_user = OwnerFactory( + username="random-user-123", + service="github", + trial_start_date=trial_start_date, + trial_end_date=trial_end_date, + trial_status=TrialStatus.ONGOING.value, + plan=self.plan.name, + ) + with pytest.raises(ValidationError): + self.execute(current_user=current_user, org_username=current_user.username) + + @freeze_time("2022-01-01T00:00:00") + def test_start_trial_raises_exception_when_owners_trial_status_is_expired(self): + now = datetime.now() + trial_start_date = now + timedelta(days=-10) + trial_end_date = now + timedelta(days=-4) + current_user = OwnerFactory( + username="random-user-123", + service="github", + trial_start_date=trial_start_date, + trial_end_date=trial_end_date, + trial_status=TrialStatus.EXPIRED.value, + plan=self.plan.name, + ) + with pytest.raises(ValidationError): + self.execute(current_user=current_user, org_username=current_user.username) + + @freeze_time("2022-01-01T00:00:00") + def test_start_trial_raises_exception_when_owners_trial_status_cannot_trial( + self, + ): + now = datetime.now() + trial_start_date = now + trial_end_date = now + current_user = OwnerFactory( + username="random-user-123", + service="github", + trial_start_date=trial_start_date, + trial_end_date=trial_end_date, + trial_status=TrialStatus.CANNOT_TRIAL.value, + plan=self.plan.name, + ) + with pytest.raises(ValidationError): + self.execute(current_user=current_user, org_username=current_user.username) + + @freeze_time("2022-01-01T00:00:00") + def test_start_trial_starts_trial_for_org_that_has_not_started_trial_before_and_calls_segment( + self, + ): + current_user: Owner = OwnerFactory( + username="random-user-123", + service="github", + trial_start_date=None, + trial_end_date=None, + trial_status=TrialStatus.NOT_STARTED.value, + plan=self.plan.name, + ) + self.execute(current_user=current_user, org_username=current_user.username) + current_user.refresh_from_db() + + now = datetime.now() + assert current_user.trial_start_date == now + assert current_user.trial_end_date == now + timedelta( + days=TrialDaysAmount.CODECOV_SENTRY.value + ) + assert current_user.trial_status == TrialStatus.ONGOING.value + assert current_user.plan == PlanName.TRIAL_PLAN_NAME.value + assert current_user.plan_user_count == TRIAL_PLAN_SEATS + assert current_user.plan_auto_activate == True diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_trigger_sync.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_trigger_sync.py new file mode 100644 index 0000000000..8004aa6016 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_trigger_sync.py @@ -0,0 +1,28 @@ +from unittest.mock import patch + +import pytest +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov.commands.exceptions import Unauthenticated + +from ..trigger_sync import TriggerSyncInteractor + + +class IsSyncingInteractorTest(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + + async def test_when_unauthenticated_raise(self): + with pytest.raises(Unauthenticated): + await TriggerSyncInteractor(None, "github").execute() + + @patch("services.refresh.RefreshService.trigger_refresh") + async def test_call_is_refreshing(self, mock_trigger_refresh): + await TriggerSyncInteractor(self.owner, "github").execute() + mock_trigger_refresh.assert_called_once_with( + self.owner.ownerid, + self.owner.username, + using_integration=False, + manual_trigger=True, + ) diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_update_default_organization.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_update_default_organization.py new file mode 100644 index 0000000000..e0c310e382 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_update_default_organization.py @@ -0,0 +1,81 @@ +from unittest.mock import patch + +import pytest +from asgiref.sync import async_to_sync +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov.commands.exceptions import Unauthenticated, ValidationError +from codecov_auth.models import OwnerProfile + +from ..update_default_organization import UpdateDefaultOrganizationInteractor + + +class UpdateDefaultOrganizationInteractorTest(TestCase): + def setUp(self): + self.default_organization_username = "sample-default-org-username" + self.default_organization = OwnerFactory( + username=self.default_organization_username, service="github" + ) + self.owner = OwnerFactory( + username="sample-owner", + service="github", + organizations=[self.default_organization.ownerid], + ) + + self.org_not_in_users_organizations = OwnerFactory( + username="imposter", service="github" + ) + + @async_to_sync + def execute(self, owner, username=None): + return UpdateDefaultOrganizationInteractor(owner, "github").execute( + default_org_username=username, + ) + + def test_when_unauthenticated_raise(self): + with pytest.raises(Unauthenticated): + self.execute(owner=None, username="random-name") + + def test_update_org_not_belonging_to_users_organizations(self): + with pytest.raises(ValidationError): + self.execute(owner=self.owner, username="imposter") + + def test_update_org_when_default_org_username_is_none(self): + self.execute(owner=self.owner, username=None) + + owner_profile: OwnerProfile = OwnerProfile.objects.filter( + owner_id=self.owner.ownerid + ).first() + assert owner_profile.default_org is None + + def test_update_owners_default_org(self): + username = self.execute( + owner=self.owner, username=self.default_organization_username + ) + + owner_profile: OwnerProfile = OwnerProfile.objects.filter( + owner_id=self.owner.ownerid + ).first() + assert owner_profile.default_org == self.default_organization + assert username == self.default_organization.username + + @patch( + "codecov_auth.commands.owner.interactors.update_default_organization.try_auto_activate" + ) + def test_attempts_to_auto_activate_user_for_default_org(self, try_auto_activate): + self.execute(owner=self.owner, username=self.default_organization_username) + + try_auto_activate.assert_called_once_with( + self.default_organization, + self.owner, + ) + + def test_update_owners_default_org_when_current_user_is_selected(self): + username = self.execute(owner=self.owner, username=self.owner.username) + + owner_profile: OwnerProfile = OwnerProfile.objects.filter( + owner_id=self.owner.ownerid + ).first() + assert owner_profile.default_org == self.owner + assert username == self.owner.username diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_update_profile.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_update_profile.py new file mode 100644 index 0000000000..95393f9db4 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_update_profile.py @@ -0,0 +1,40 @@ +import pytest +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov.commands.exceptions import Unauthenticated, ValidationError + +from ..update_profile import UpdateProfileInteractor + + +class UpdateProfileInteractorTest(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + + async def test_when_unauthenticated_raise(self): + with pytest.raises(Unauthenticated): + await UpdateProfileInteractor(None, "github").execute(name="hello") + + async def test_when_email_wrong(self): + with pytest.raises(ValidationError): + await UpdateProfileInteractor(self.owner, "github").execute( + email="not-right" + ) + + async def test_update_name(self): + user = await UpdateProfileInteractor(self.owner, "github").execute(name="hello") + assert user.name == "hello" + + async def test_update_email(self): + user = await UpdateProfileInteractor(self.owner, "github").execute( + email="hello@codecov.io" + ) + assert user.email == "hello@codecov.io" + + async def test_update_email_and_name(self): + user = await UpdateProfileInteractor(self.owner, "github").execute( + name="codecov brother", email="brother@codecov.io" + ) + assert user == self.owner + assert user.email == "brother@codecov.io" + assert user.name == "codecov brother" diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_update_self_hosted_settings.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_update_self_hosted_settings.py new file mode 100644 index 0000000000..cb16ec6691 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/tests/test_update_self_hosted_settings.py @@ -0,0 +1,59 @@ +import pytest +from asgiref.sync import async_to_sync +from django.contrib.auth.models import AnonymousUser +from django.test import TestCase, override_settings +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov.commands.exceptions import Unauthenticated, ValidationError +from codecov_auth.commands.owner.interactors.update_self_hosted_settings import ( + UpdateSelfHostedSettingsInteractor, +) + + +class UpdateSelfHostedSettingsInteractorTest(TestCase): + @async_to_sync + def execute( + self, + current_user, + input={ + "should_auto_activate": None, + }, + ): + return UpdateSelfHostedSettingsInteractor(None, "github", current_user).execute( + input=input, + ) + + @override_settings(IS_ENTERPRISE=True) + def test_update_self_hosted_settings_when_auto_activate_is_true(self): + owner = OwnerFactory(plan_auto_activate=False) + self.execute(current_user=owner, input={"should_auto_activate": True}) + owner.refresh_from_db() + assert owner.plan_auto_activate == True + + @override_settings(IS_ENTERPRISE=True) + def test_update_self_hosted_settings_when_auto_activate_is_false(self): + owner = OwnerFactory(plan_auto_activate=True) + self.execute(current_user=owner, input={"should_auto_activate": False}) + owner.refresh_from_db() + assert owner.plan_auto_activate == False + + @override_settings(IS_ENTERPRISE=False) + def test_validation_error_when_not_self_hosted_instance(self): + owner = OwnerFactory(plan_auto_activate=True) + with pytest.raises(ValidationError): + self.execute( + current_user=owner, + input={ + "should_auto_activate": False, + }, + ) + + @override_settings(IS_ENTERPRISE=True) + def test_user_is_not_authenticated(self): + with pytest.raises(Unauthenticated): + self.execute( + current_user=AnonymousUser(), + input={ + "should_auto_activate": False, + }, + ) diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/trigger_sync.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/trigger_sync.py new file mode 100644 index 0000000000..32f6028431 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/trigger_sync.py @@ -0,0 +1,21 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated +from services.refresh import RefreshService + + +class TriggerSyncInteractor(BaseInteractor): + def validate(self) -> None: + if not self.current_user.is_authenticated: + raise Unauthenticated() + + @sync_to_async + def execute(self) -> None: + self.validate() + RefreshService().trigger_refresh( + self.current_owner.ownerid, + self.current_owner.username, + using_integration=False, + manual_trigger=True, + ) diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/update_default_organization.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/update_default_organization.py new file mode 100644 index 0000000000..023e7583b2 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/update_default_organization.py @@ -0,0 +1,49 @@ +from typing import Optional + +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated, ValidationError +from codecov_auth.models import Owner, OwnerProfile +from services.activation import try_auto_activate + + +class UpdateDefaultOrganizationInteractor(BaseInteractor): + def validate( + self, + default_org: Optional[Owner], + ) -> Optional[Owner]: + if not self.current_user.is_authenticated: + raise Unauthenticated() + + if default_org is None: + return + + if ( + default_org.ownerid not in self.current_owner.organizations + and default_org.ownerid != self.current_owner.ownerid + ): + raise ValidationError( + "Organization does not belong in current user's organization list" + ) + + def update_default_organization(self, default_org: Optional[Owner]): + owner_profile, _ = OwnerProfile.objects.get_or_create( + owner_id=self.current_owner.ownerid + ) + owner_profile.default_org = default_org + saved_owner_profile = owner_profile.save() + if default_org: + try_auto_activate(default_org, owner_profile.owner) + return saved_owner_profile + + @sync_to_async + def execute(self, default_org_username: str): + default_org = None + if default_org_username is not None: + default_org = Owner.objects.filter( + username=default_org_username, service=self.service + ).first() + self.validate(default_org) + self.update_default_organization(default_org) + return default_org_username diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/update_profile.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/update_profile.py new file mode 100644 index 0000000000..6ff5666550 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/update_profile.py @@ -0,0 +1,34 @@ +from asgiref.sync import sync_to_async +from django import forms + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated, ValidationError + + +class UpdateProfileForm(forms.Form): + name = forms.CharField(required=False) + email = forms.EmailField(required=False) + + +class UpdateProfileInteractor(BaseInteractor): + def validate(self, **kwargs): + if not self.current_user.is_authenticated: + raise Unauthenticated() + form = UpdateProfileForm(kwargs) + if not form.is_valid(): + # temporary solution to expose form errors until a better abstraction + raise ValidationError(form.errors.as_json()) + + def update_field(self, field_name, **kwargs): + field = kwargs.get(field_name) + if not field: + return + setattr(self.current_owner, field_name, field) + + @sync_to_async + def execute(self, **kwargs): + self.validate(**kwargs) + self.update_field("email", **kwargs) + self.update_field("name", **kwargs) + self.current_owner.save() + return self.current_owner diff --git a/apps/codecov-api/codecov_auth/commands/owner/interactors/update_self_hosted_settings.py b/apps/codecov-api/codecov_auth/commands/owner/interactors/update_self_hosted_settings.py new file mode 100644 index 0000000000..5464a323c7 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/interactors/update_self_hosted_settings.py @@ -0,0 +1,37 @@ +from dataclasses import dataclass + +from asgiref.sync import sync_to_async +from django.conf import settings + +import services.self_hosted as self_hosted +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated, ValidationError + + +@dataclass +class UpdateSelfHostedSettingsInput: + auto_activate_members: bool = False + + +class UpdateSelfHostedSettingsInteractor(BaseInteractor): + def validate(self) -> None: + if not self.current_user.is_authenticated: + raise Unauthenticated() + + if not settings.IS_ENTERPRISE: + raise ValidationError( + "enable_autoactivation and disable_autoactivation are only available in self-hosted environments" + ) + + @sync_to_async + def execute(self, input: UpdateSelfHostedSettingsInput) -> None: + self.validate() + typed_input = UpdateSelfHostedSettingsInput( + auto_activate_members=input.get("should_auto_activate"), + ) + + should_auto_activate = typed_input.auto_activate_members + if should_auto_activate: + self_hosted.enable_autoactivation() + else: + self_hosted.disable_autoactivation() diff --git a/apps/codecov-api/codecov_auth/commands/owner/owner.py b/apps/codecov-api/codecov_auth/commands/owner/owner.py new file mode 100644 index 0000000000..53b21a9577 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/owner.py @@ -0,0 +1,108 @@ +from codecov.commands.base import BaseCommand + +from .interactors.cancel_trial import CancelTrialInteractor +from .interactors.create_api_token import CreateApiTokenInteractor +from .interactors.create_stripe_setup_intent import CreateStripeSetupIntentInteractor +from .interactors.create_user_token import CreateUserTokenInteractor +from .interactors.delete_session import DeleteSessionInteractor +from .interactors.fetch_owner import FetchOwnerInteractor +from .interactors.get_is_current_user_an_admin import GetIsCurrentUserAnAdminInteractor +from .interactors.get_org_upload_token import GetOrgUploadToken +from .interactors.get_uploads_number_per_user import GetUploadsNumberPerUserInteractor +from .interactors.is_syncing import IsSyncingInteractor +from .interactors.onboard_user import OnboardUserInteractor +from .interactors.regenerate_org_upload_token import RegenerateOrgUploadTokenInteractor +from .interactors.revoke_user_token import RevokeUserTokenInteractor +from .interactors.save_okta_config import SaveOktaConfigInteractor +from .interactors.save_terms_agreement import SaveTermsAgreementInteractor +from .interactors.set_upload_token_required import SetUploadTokenRequiredInteractor +from .interactors.set_yaml_on_owner import SetYamlOnOwnerInteractor +from .interactors.start_trial import StartTrialInteractor +from .interactors.store_codecov_metric import StoreCodecovMetricInteractor +from .interactors.trigger_sync import TriggerSyncInteractor +from .interactors.update_default_organization import UpdateDefaultOrganizationInteractor +from .interactors.update_profile import UpdateProfileInteractor +from .interactors.update_self_hosted_settings import UpdateSelfHostedSettingsInteractor + + +class OwnerCommands(BaseCommand): + def create_api_token(self, name): + return self.get_interactor(CreateApiTokenInteractor).execute(name) + + def create_stripe_setup_intent(self, owner): + return self.get_interactor(CreateStripeSetupIntentInteractor).execute(owner) + + def delete_session(self, sessionid: int): + return self.get_interactor(DeleteSessionInteractor).execute(sessionid) + + def create_user_token(self, name, token_type=None): + return self.get_interactor(CreateUserTokenInteractor).execute(name, token_type) + + def revoke_user_token(self, tokenid): + return self.get_interactor(RevokeUserTokenInteractor).execute(tokenid) + + def set_yaml_on_owner(self, username, yaml): + return self.get_interactor(SetYamlOnOwnerInteractor).execute(username, yaml) + + def update_profile(self, **kwargs): + return self.get_interactor(UpdateProfileInteractor).execute(**kwargs) + + def save_terms_agreement(self, input): + return self.get_interactor(SaveTermsAgreementInteractor).execute(input) + + def update_default_organization(self, **kwargs): + return self.get_interactor(UpdateDefaultOrganizationInteractor).execute( + **kwargs + ) + + def fetch_owner(self, username): + return self.get_interactor(FetchOwnerInteractor).execute(username) + + def trigger_sync(self): + return self.get_interactor(TriggerSyncInteractor).execute() + + def is_syncing(self): + return self.get_interactor(IsSyncingInteractor).execute() + + def onboard_user(self, params): + return self.get_interactor(OnboardUserInteractor).execute(params) + + def get_uploads_number_per_user(self, owner): + return self.get_interactor(GetUploadsNumberPerUserInteractor).execute(owner) + + def get_is_current_user_an_admin(self, owner, current_user): + return self.get_interactor(GetIsCurrentUserAnAdminInteractor).execute( + owner, current_user + ) + + def get_org_upload_token(self, owner): + return self.get_interactor(GetOrgUploadToken).execute(owner) + + def regenerate_org_upload_token(self, owner): + return self.get_interactor(RegenerateOrgUploadTokenInteractor).execute(owner) + + def start_trial(self, org_username: str) -> None: + return self.get_interactor(StartTrialInteractor).execute( + org_username=org_username + ) + + def cancel_trial(self, org_username: str) -> None: + return self.get_interactor(CancelTrialInteractor).execute( + org_username=org_username + ) + + def update_self_hosted_settings(self, input) -> None: + return self.get_interactor(UpdateSelfHostedSettingsInteractor).execute(input) + + def store_codecov_metric( + self, org_username: str, event: str, json_string: str + ) -> None: + return self.get_interactor(StoreCodecovMetricInteractor).execute( + org_username, event, json_string + ) + + def save_okta_config(self, input) -> None: + return self.get_interactor(SaveOktaConfigInteractor).execute(input) + + def set_upload_token_required(self, input) -> None: + return self.get_interactor(SetUploadTokenRequiredInteractor).execute(input) diff --git a/apps/codecov-api/codecov_auth/commands/owner/tests/__init__.py b/apps/codecov-api/codecov_auth/commands/owner/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/codecov_auth/commands/owner/tests/test_owner.py b/apps/codecov-api/codecov_auth/commands/owner/tests/test_owner.py new file mode 100644 index 0000000000..cd8456e3f6 --- /dev/null +++ b/apps/codecov-api/codecov_auth/commands/owner/tests/test_owner.py @@ -0,0 +1,144 @@ +from unittest.mock import patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from ..owner import OwnerCommands + + +class OwnerCommandsTest(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + self.command = OwnerCommands(self.owner, "github") + + @patch("codecov_auth.commands.owner.owner.CreateApiTokenInteractor.execute") + def test_create_api_token_delegate_to_interactor(self, interactor_mock): + name = "new api token" + self.command.create_api_token(name) + interactor_mock.assert_called_once_with(name) + + @patch("codecov_auth.commands.owner.owner.DeleteSessionInteractor.execute") + def test_delete_session_delegate_to_interactor(self, interactor_mock): + sessionid = 12 + self.command.delete_session(sessionid) + interactor_mock.assert_called_once_with(sessionid) + + @patch("codecov_auth.commands.owner.owner.CreateUserTokenInteractor.execute") + def test_create_user_token_delegate_to_interactor(self, interactor_mock): + name = "new api token" + self.command.create_user_token(name) + interactor_mock.assert_called_once_with(name, None) + + @patch("codecov_auth.commands.owner.owner.RevokeUserTokenInteractor.execute") + def test_revoke_user_token_delegate_to_interactor(self, interactor_mock): + tokenid = 123 + self.command.revoke_user_token(tokenid) + interactor_mock.assert_called_once_with(tokenid) + + @patch("codecov_auth.commands.owner.owner.SetYamlOnOwnerInteractor.execute") + def test_set_yaml_on_owner_delegate_to_interactor(self, interactor_mock): + username = "codecov" + yaml = "codecov: something" + self.command.set_yaml_on_owner(username, yaml) + interactor_mock.assert_called_once_with(username, yaml) + + @patch("codecov_auth.commands.owner.owner.UpdateProfileInteractor.execute") + def test_update_profile_delegate_to_interactor(self, interactor_mock): + name = "codecov name" + self.command.update_profile(name=name) + interactor_mock.assert_called_once_with(name=name) + + @patch("codecov_auth.commands.owner.owner.SaveTermsAgreementInteractor.execute") + def test_save_terms_agreement_delegate_to_interactor(self, interactor_mock): + input_dict = {"email": "a@a.com", "termsAgreement": False} + self.command.save_terms_agreement(input_dict) + interactor_mock.assert_called_once_with(input_dict) + + @patch("codecov_auth.commands.owner.owner.StartTrialInteractor.execute") + def test_start_trial_delegate_to_interactor(self, interactor_mock): + org_username = "random_org" + self.command.start_trial(org_username=org_username) + interactor_mock.assert_called_once_with(org_username=org_username) + + @patch("codecov_auth.commands.owner.owner.CancelTrialInteractor.execute") + def test_cancel_trial_delegate_to_interactor(self, interactor_mock): + org_username = "random_org" + self.command.cancel_trial(org_username=org_username) + interactor_mock.assert_called_once_with(org_username=org_username) + + @patch( + "codecov_auth.commands.owner.owner.UpdateDefaultOrganizationInteractor.execute" + ) + def test_update_default_organization_delegate_to_interactor(self, interactor_mock): + username = "codecov-user" + self.command.update_default_organization(default_org_username=username) + interactor_mock.assert_called_once_with(default_org_username=username) + + @patch("codecov_auth.commands.owner.owner.TriggerSyncInteractor.execute") + def test_trigger_sync_delegate_to_interactor(self, interactor_mock): + self.command.trigger_sync() + interactor_mock.assert_called_once() + + @patch("codecov_auth.commands.owner.owner.IsSyncingInteractor.execute") + def test_is_syncing_delegate_to_interactor(self, interactor_mock): + self.command.is_syncing() + interactor_mock.assert_called_once() + + @patch("codecov_auth.commands.owner.owner.OnboardUserInteractor.execute") + def test_onboard_user_delegate_to_interactor(self, interactor_mock): + params = {} + self.command.onboard_user(params) + interactor_mock.assert_called_once_with(params) + + @patch( + "codecov_auth.commands.owner.owner.GetUploadsNumberPerUserInteractor.execute" + ) + def test_get_uploads_number_per_user_delegate_to_interactor(self, interactor_mock): + owner = {} + self.command.get_uploads_number_per_user(owner) + interactor_mock.assert_called_once_with(owner) + + @patch( + "codecov_auth.commands.owner.owner.GetIsCurrentUserAnAdminInteractor.execute" + ) + def test_get_is_current_user_an_admin_delegate_to_interactor(self, interactor_mock): + owner = {} + current_user = {} + self.command.get_is_current_user_an_admin(owner, current_user) + interactor_mock.assert_called_once_with(owner, current_user) + + @patch("codecov_auth.commands.owner.owner.GetOrgUploadToken.execute") + def test_get_org_upload_token_delegate_to_interactor(self, interactor_mock): + owner = {} + self.command.get_org_upload_token(owner) + interactor_mock.assert_called_once_with(owner) + + @patch( + "codecov_auth.commands.owner.owner.RegenerateOrgUploadTokenInteractor.execute" + ) + def test_regenerate_org_upload_token_delegate_to_interactor(self, interactor_mock): + owner = {} + self.command.regenerate_org_upload_token(owner) + interactor_mock.assert_called_once_with(owner) + + @patch("codecov_auth.commands.owner.owner.SaveOktaConfigInteractor.execute") + def test_save_okta_config_delegate_to_interactor(self, interactor_mock): + input_dict = { + "client_id": "123", + "client_secret": "123", + "url": "http://example.com", + "enabled": True, + "enforced": False, + "org_username": "codecov-user", + } + self.command.save_okta_config(input_dict) + interactor_mock.assert_called_once_with(input_dict) + + @patch("codecov_auth.commands.owner.owner.SetUploadTokenRequiredInteractor.execute") + def test_set_upload_token_required_delegate_to_interactor(self, interactor_mock): + input_dict = { + "upload_token_required": True, + "org_username": "codecov-user", + } + self.command.set_upload_token_required(input_dict) + interactor_mock.assert_called_once_with(input_dict) diff --git a/apps/codecov-api/codecov_auth/constants.py b/apps/codecov-api/codecov_auth/constants.py new file mode 100644 index 0000000000..a818f2a64e --- /dev/null +++ b/apps/codecov-api/codecov_auth/constants.py @@ -0,0 +1,6 @@ +AVATAR_GITHUB_BASE_URL = "https://avatars0.githubusercontent.com" +BITBUCKET_BASE_URL = "https://bitbucket.org" +GITLAB_BASE_URL = "https://gitlab.com" +GRAVATAR_BASE_URL = "https://www.gravatar.com" +AVATARIO_BASE_URL = "https://avatars.io" +OWNER_YAML_TO_STRING_KEY = "to_string" diff --git a/apps/codecov-api/codecov_auth/helpers.py b/apps/codecov-api/codecov_auth/helpers.py new file mode 100644 index 0000000000..3998d7529f --- /dev/null +++ b/apps/codecov-api/codecov_auth/helpers.py @@ -0,0 +1,71 @@ +from traceback import format_stack + +import requests +from django.contrib.admin.models import CHANGE, LogEntry +from django.contrib.contenttypes.models import ContentType + +from codecov_auth.constants import GITLAB_BASE_URL + +GITLAB_PAYLOAD_AVATAR_URL_KEY = "avatar_url" + + +def get_gitlab_url(email, size): + res = requests.get( + "{}/api/v4/avatar?email={}&size={}".format(GITLAB_BASE_URL, email, size) + ) + url = "" + if res.status_code == 200: + data = res.json() + try: + url = data[GITLAB_PAYLOAD_AVATAR_URL_KEY] + except KeyError: + pass + + return url + + +def current_user_part_of_org(owner, org): + if owner is None: + return False + if owner == org: + return True + # owner is a direct member of the org + orgs_of_user = owner.organizations or [] + return org.ownerid in orgs_of_user + + +# https://stackoverflow.com/questions/7905106/adding-a-log-entry-for-an-action-by-a-user-in-a-django-ap + + +class History: + @staticmethod + def log(objects, message, user, action_flag=None, add_traceback=False): + """ + Log an action in the admin log + :param objects: Objects being operated on + :param message: Message to log + :param user: User performing action + :param action_flag: Type of action being performed + :param add_traceback: Add the stack trace to the message + """ + if action_flag is None: + action_flag = CHANGE + + if not isinstance(objects, list): + objects = [objects] + + if add_traceback: + message = f"{message}: {format_stack()}" + + for obj in objects: + if not obj: + continue + + LogEntry.objects.log_action( + user_id=user.pk, + content_type_id=ContentType.objects.get_for_model(obj).pk, + object_repr=str(obj), + object_id=obj.ownerid, + change_message=message, + action_flag=action_flag, + ) diff --git a/apps/codecov-api/codecov_auth/managers.py b/apps/codecov-api/codecov_auth/managers.py new file mode 100644 index 0000000000..c081d62cb6 --- /dev/null +++ b/apps/codecov-api/codecov_auth/managers.py @@ -0,0 +1,83 @@ +from django.db.models import Exists, Func, Manager, OuterRef, Q, QuerySet, Subquery +from django.db.models.functions import Coalesce + +from core.models import Pull + + +class OwnerQuerySet(QuerySet): + def users_of(self, owner): + """ + Returns users of "owner", which is defined as Owner objects + whose "organizations" field contains the "owner"s ownerid + or is one of the "owner"s "plan_activated_users". + """ + filters = Q(organizations__contains=[owner.ownerid]) + if owner.plan_activated_users: + filters = filters | Q(ownerid__in=owner.plan_activated_users) + + return self.filter(filters) + + def annotate_activated_in(self, owner): + """ + Annotates queryset with "activated" field, which is True + if a given user is activated in organization "owner", false + otherwise. + """ + from codecov_auth.models import Owner + + return self.annotate( + activated=Coalesce( + Exists( + Owner.objects.filter( + ownerid=owner.ownerid, + plan_activated_users__contains=Func( + OuterRef("ownerid"), + function="ARRAY", + template="%(function)s[%(expressions)s]", + ), + ) + ), + False, + ) + ) + + def annotate_is_admin_in(self, owner): + """ + Annotates queryset with "is_admin" field, which is True + if a given user is an admin in organization "owner", and + false otherwise. + """ + from codecov_auth.models import Owner + + return self.annotate( + is_admin=Coalesce( + Exists( + Owner.objects.filter( + ownerid=owner.ownerid, + admins__contains=Func( + OuterRef("ownerid"), + function="ARRAY", + template="%(function)s[%(expressions)s]", + ), + ) + ), + False, + ) + ) + + def annotate_last_pull_timestamp(self): + pulls = Pull.objects.filter(author=OuterRef("pk")).order_by("-updatestamp") + return self.annotate( + last_pull_timestamp=Subquery(pulls.values("updatestamp")[:1]), + ) + + +# We cannot use `QuerySet.as_manager()` since it relies on the `inspect` module and will +# not play nicely with Cython (which we use for self-hosted): +# https://cython.readthedocs.io/en/latest/src/userguide/limitations.html#inspect-support +class OwnerManager(Manager): + def get_queryset(self): + return OwnerQuerySet(self.model, using=self._db) + + def users_of(self, *args, **kwargs): + return self.get_queryset().users_of(*args, **kwargs) diff --git a/apps/codecov-api/codecov_auth/middleware.py b/apps/codecov-api/codecov_auth/middleware.py new file mode 100644 index 0000000000..6664c600bd --- /dev/null +++ b/apps/codecov-api/codecov_auth/middleware.py @@ -0,0 +1,165 @@ +import logging +from typing import Optional +from urllib.parse import urlparse + +from corsheaders.conf import conf as corsconf +from corsheaders.middleware import ( + ACCESS_CONTROL_ALLOW_CREDENTIALS, + ACCESS_CONTROL_ALLOW_ORIGIN, +) +from corsheaders.middleware import CorsMiddleware as BaseCorsMiddleware +from django.http import HttpRequest, HttpResponse +from django.urls import resolve +from django.utils.deprecation import MiddlewareMixin +from rest_framework import exceptions + +from codecov_auth.models import Owner, Service +from utils.services import get_long_service_name + +log = logging.getLogger(__name__) + + +def get_service(request: HttpRequest) -> Optional[str]: + resolver_match = resolve(request.path_info) + service = resolver_match.kwargs.get("service") + if service is not None: + service = get_long_service_name(service.lower()) + try: + Service(service) + return service + except ValueError: + # not a valid service + return None + + +class CurrentOwnerMiddleware(MiddlewareMixin): + """ + The authenticated `User` may have multiple linked `Owners` and we need a way + to load the "currently active" `Owner` for use in this request. + + If there's a `current_owner_id` value in the session then we use that. + If the current owner does not match the request's `service` then we just pick the first + of the user's owners with the matching service. + + This middleware is preferrable to accessing the session directly in views since + we can load the `Owner` once and reuse it anywhere needed (without having to perform + additional database queries). + """ + + def process_request(self, request: HttpRequest) -> None: + if not request.user or request.user.is_anonymous: + request.current_owner = None + return + + current_user = request.user + current_owner = None + + current_owner_id = request.session.get("current_owner_id") + if current_owner_id is not None: + current_owner = current_user.owners.filter(pk=current_owner_id).first() + + service = get_service(request) + if service and (current_owner is None or service != current_owner.service): + # FIXME: this is OK (for now) since we're only allowing a single owner of a given + # service to be linked to any 1 user + current_owner = current_user.owners.filter(service=service).first() + + request.current_owner = current_owner + + +class ImpersonationMiddleware(MiddlewareMixin): + """ + Allows staff users to impersonate other users for debugging. + """ + + def process_request(self, request: HttpRequest) -> None: + """Log and ensure that the impersonating user is authenticated. + The `current user` is the staff user that is impersonating the + user owner at `impersonating_ownerid`. + """ + current_user = request.user + + if current_user and not current_user.is_anonymous: + impersonating_ownerid = request.COOKIES.get("staff_user") + if impersonating_ownerid is None: + request.impersonation = False + return + + log.info( + "Impersonation attempted", + extra=dict( + current_user_id=current_user.pk, + current_user_email=current_user.email, + impersonating_ownerid=impersonating_ownerid, + ), + ) + if not current_user.is_staff: + log.warning( + "Impersonation unsuccessful", + extra=dict( + reason="must be a staff user", + current_user_id=current_user.pk, + current_user_email=current_user.email, + impersonating_ownerid=impersonating_ownerid, + ), + ) + raise exceptions.PermissionDenied() + + request.current_owner = ( + Owner.objects.filter(pk=impersonating_ownerid) + .prefetch_related("user") + .first() + ) + if request.current_owner is None: + log.warning( + "Impersonation unsuccessful", + extra=dict( + reason="no such owner", + current_user_id=current_user.pk, + current_user_email=current_user.email, + impersonating_ownerid=impersonating_ownerid, + ), + ) + raise exceptions.AuthenticationFailed() + + log.info( + "Impersonation successful", + extra=dict( + current_user_id=current_user.pk, + current_user_email=current_user.email, + impersonating_ownerid=impersonating_ownerid, + ), + ) + request.impersonation = True + else: + request.impersonation = False + + +class CorsMiddleware(BaseCorsMiddleware): + def process_response( + self, request: HttpRequest, response: HttpResponse + ) -> HttpResponse: + response = super().process_response(request, response) + if not self.is_enabled(request): + return response + + origin = request.META.get("HTTP_ORIGIN") + if not origin: + return response + + # we only allow credentials with CORS requests if the request + # is coming from one of our explicitly whitelisted domains + # (other domains will only be able to access public resources) + allow_credentials = False + if corsconf.CORS_ALLOW_CREDENTIALS: + url = urlparse(origin) + if self.origin_found_in_white_lists(origin, url): + allow_credentials = True + + response.headers[ACCESS_CONTROL_ALLOW_ORIGIN] = origin + if allow_credentials: + response.headers[ACCESS_CONTROL_ALLOW_CREDENTIALS] = "true" + else: + del response.headers[ACCESS_CONTROL_ALLOW_CREDENTIALS] + + return response diff --git a/apps/codecov-api/codecov_auth/models.py b/apps/codecov-api/codecov_auth/models.py new file mode 100644 index 0000000000..b30e6f5e49 --- /dev/null +++ b/apps/codecov-api/codecov_auth/models.py @@ -0,0 +1 @@ +from shared.django_apps.codecov_auth.models import * diff --git a/apps/codecov-api/codecov_auth/permissions.py b/apps/codecov-api/codecov_auth/permissions.py new file mode 100644 index 0000000000..c8e43437e7 --- /dev/null +++ b/apps/codecov-api/codecov_auth/permissions.py @@ -0,0 +1,8 @@ +from rest_framework.permissions import BasePermission + + +class SpecificScopePermission(BasePermission): + def has_permission(self, request, view): + return request.auth is not None and all( + scope in request.auth.get_scopes() for scope in view.required_scopes + ) diff --git a/apps/codecov-api/codecov_auth/services/org_level_token_service.py b/apps/codecov-api/codecov_auth/services/org_level_token_service.py new file mode 100644 index 0000000000..bb20bfb36d --- /dev/null +++ b/apps/codecov-api/codecov_auth/services/org_level_token_service.py @@ -0,0 +1,70 @@ +import logging +import uuid + +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.forms import ValidationError + +from codecov_auth.models import OrganizationLevelToken, Owner, Plan + +log = logging.getLogger(__name__) + + +class OrgLevelTokenService(object): + """ + Groups some basic CRUD functionality to create and delete OrganizationLevelToken. + Restrictions: + -- only 1 token per Owner + """ + + @classmethod + def org_can_have_upload_token(cls, org: Owner): + return org.plan in Plan.objects.values_list("name", flat=True) + + @classmethod + def get_or_create_org_token(cls, org: Owner): + if not cls.org_can_have_upload_token(org): + raise ValidationError( + "Organization-wide upload tokens are not available for your organization." + ) + token, created = OrganizationLevelToken.objects.get_or_create(owner=org) + if created: + log.info( + "New OrgLevelToken created", + extra=dict( + ownerid=org.ownerid, + valid_until=token.valid_until, + token_type=token.token_type, + ), + ) + return token + + @classmethod + def refresh_token(cls, tokenid: int): + try: + token = OrganizationLevelToken.objects.get(id=tokenid) + token.token = uuid.uuid4() + token.save() + except OrganizationLevelToken.DoesNotExist: + raise ValidationError( + "Token to refresh was not found", params=dict(tokenid=tokenid) + ) + + @classmethod + def delete_org_token_if_exists(cls, org: Owner): + try: + org_token = OrganizationLevelToken.objects.get(owner=org) + org_token.delete() + except OrganizationLevelToken.DoesNotExist: + pass + + +@receiver(post_save, sender=Owner) +def manage_org_tokens_if_owner_plan_changed(sender, instance: Owner, **kwargs): + """ + Gets executed after saving a Owner instance to DB. + Manages OrganizationLevelToken according to Owner plan, either creating or deleting them as necessary + """ + owner_can_have_org_token = OrgLevelTokenService.org_can_have_upload_token(instance) + if not owner_can_have_org_token: + OrgLevelTokenService.delete_org_token_if_exists(instance) diff --git a/apps/codecov-api/codecov_auth/signals.py b/apps/codecov-api/codecov_auth/signals.py new file mode 100644 index 0000000000..5e60d26eb5 --- /dev/null +++ b/apps/codecov-api/codecov_auth/signals.py @@ -0,0 +1,53 @@ +from typing import Any, Dict, Optional, Type, cast + +from django.db.models.signals import post_save +from django.dispatch import receiver + +from codecov_auth.models import OrganizationLevelToken, Owner, OwnerProfile +from utils.shelter import ShelterPubsub + + +@receiver(post_save, sender=Owner) +def create_owner_profile_when_owner_is_created( + sender: Type[Owner], instance: Owner, created: bool, **kwargs: Dict[str, Any] +) -> Optional[OwnerProfile]: + if created: + return OwnerProfile.objects.create(owner_id=instance.ownerid) + + +@receiver( + post_save, sender=OrganizationLevelToken, dispatch_uid="shelter_sync_org_token" +) +def update_org_token( + sender: Type[OrganizationLevelToken], + instance: OrganizationLevelToken, + **kwargs: Dict[str, Any], +) -> None: + data = { + "type": "org_token", + "sync": "one", + "id": instance.id, + } + ShelterPubsub.get_instance().publish(data) + + +@receiver(post_save, sender=Owner, dispatch_uid="shelter_sync_owner") +def update_owner( + sender: Type[Owner], instance: Owner, **kwargs: Dict[str, Any] +) -> None: + """ + Shelter tracks a limited set of Owner fields - only update if those fields have changed. + """ + created: bool = cast(bool, kwargs["created"]) + tracked_fields = [ + "upload_token_required_for_public_repos", + "username", + "service", + ] + if created or any(instance.tracker.has_changed(field) for field in tracked_fields): + data = { + "type": "owner", + "sync": "one", + "id": instance.ownerid, + } + ShelterPubsub.get_instance().publish(data) diff --git a/apps/codecov-api/codecov_auth/templates/admin/extend_trial_form.html b/apps/codecov-api/codecov_auth/templates/admin/extend_trial_form.html new file mode 100644 index 0000000000..24c6bba2bd --- /dev/null +++ b/apps/codecov-api/codecov_auth/templates/admin/extend_trial_form.html @@ -0,0 +1,28 @@ +{% extends "admin/base_site.html" %} + +{% block content %} +
+ {% csrf_token %} + + + + {{ form }} +
+ +
+ +

Extending trial for:

+

+

    + {% for dataset in datasets %} +
  • + {{ dataset.username }} + +
  • + {% endfor %} +
+

+ + +
+{% endblock %} \ No newline at end of file diff --git a/apps/codecov-api/codecov_auth/tests/__init__.py b/apps/codecov-api/codecov_auth/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/codecov_auth/tests/factories.py b/apps/codecov-api/codecov_auth/tests/factories.py new file mode 100644 index 0000000000..5e4f6efb03 --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/factories.py @@ -0,0 +1,13 @@ +import factory +from django.utils import timezone +from factory.django import DjangoModelFactory + +from codecov_auth.models import DjangoSession + + +class DjangoSessionFactory(DjangoModelFactory): + class Meta: + model = DjangoSession + + expire_date = timezone.now() + session_key = factory.Faker("uuid4") diff --git a/apps/codecov-api/codecov_auth/tests/test_admin.py b/apps/codecov-api/codecov_auth/tests/test_admin.py new file mode 100644 index 0000000000..6b36c50027 --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/test_admin.py @@ -0,0 +1,1051 @@ +from datetime import timedelta +from unittest.mock import MagicMock, patch + +import pytest +from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME +from django.contrib.admin.sites import AdminSite +from django.test import RequestFactory, TestCase +from django.urls import reverse +from django.utils import timezone +from shared.django_apps.codecov_auth.models import ( + Account, + AccountsUsers, + InvoiceBilling, + StripeBilling, +) +from shared.django_apps.codecov_auth.tests.factories import ( + AccountFactory, + InvoiceBillingFactory, + OrganizationLevelTokenFactory, + OwnerFactory, + PlanFactory, + SentryUserFactory, + SessionFactory, + StripeBillingFactory, + TierFactory, + UserFactory, +) +from shared.django_apps.core.tests.factories import PullFactory, RepositoryFactory +from shared.plan.constants import ( + DEFAULT_FREE_PLAN, + PlanName, +) + +from billing.helpers import mock_all_plans_and_tiers +from codecov.commands.exceptions import ValidationError +from codecov_auth.admin import ( + AccountAdmin, + InvoiceBillingAdmin, + OrgUploadTokenInline, + OwnerAdmin, + StripeBillingAdmin, + UserAdmin, + find_and_remove_stale_users, +) +from codecov_auth.models import ( + OrganizationLevelToken, + Owner, + Plan, + SentryUser, + Tier, + User, +) +from core.models import Pull + + +class OwnerAdminTest(TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + + def setUp(self): + self.staff_user = UserFactory(is_staff=True) + self.client.force_login(user=self.staff_user) + admin_site = AdminSite() + admin_site.register(OrganizationLevelToken) + self.owner_admin = OwnerAdmin(Owner, admin_site) + + def test_owner_admin_detail_page(self): + owner = OwnerFactory() + response = self.client.get( + reverse("admin:codecov_auth_owner_change", args=[owner.pk]) + ) + self.assertEqual(response.status_code, 200) + + def test_owner_admin_impersonate_owner(self): + owner_to_impersonate = OwnerFactory(service="bitbucket", plan=DEFAULT_FREE_PLAN) + other_owner = OwnerFactory(plan=DEFAULT_FREE_PLAN) + + with self.subTest("more than one user selected"): + response = self.client.post( + reverse("admin:codecov_auth_owner_changelist"), + { + "action": "impersonate_owner", + ACTION_CHECKBOX_NAME: [ + owner_to_impersonate.pk, + other_owner.pk, + ], + }, + follow=True, + ) + self.assertIn( + "You must impersonate exactly one Owner.", str(response.content) + ) + + with self.subTest("one user selected"): + response = self.client.post( + reverse("admin:codecov_auth_owner_changelist"), + { + "action": "impersonate_owner", + ACTION_CHECKBOX_NAME: [owner_to_impersonate.pk], + }, + ) + self.assertIn("/bb/", response.url) + self.assertEqual( + response.cookies.get("staff_user").value, + str(owner_to_impersonate.pk), + ) + + @patch("codecov_auth.admin.TaskService.delete_owner") + def test_delete_queryset(self, delete_mock): + user_to_delete = OwnerFactory(plan=DEFAULT_FREE_PLAN) + ownerid = user_to_delete.ownerid + queryset = MagicMock() + queryset.__iter__.return_value = [user_to_delete] + + self.owner_admin.delete_queryset(MagicMock(), queryset) + + delete_mock.assert_called_once_with(ownerid=ownerid) + + @patch("codecov_auth.admin.TaskService.delete_owner") + def test_delete_model(self, delete_mock): + user_to_delete = OwnerFactory(plan=DEFAULT_FREE_PLAN) + ownerid = user_to_delete.ownerid + self.owner_admin.delete_model(MagicMock(), user_to_delete) + delete_mock.assert_called_once_with(ownerid=ownerid) + + @patch("codecov_auth.admin.admin.ModelAdmin.get_deleted_objects") + def test_confirmation_deleted_objects(self, mocked_deleted_objs): + user_to_delete = OwnerFactory(plan=DEFAULT_FREE_PLAN) + deleted_objs = [ + 'Owner: {};'.format( + user_to_delete.ownerid, user_to_delete + ) + ] + mocked_deleted_objs.return_value = deleted_objs, {"owners": 1}, set(), [] + + ( + deleted_objects, + model_count, + perms_needed, + protected, + ) = self.owner_admin.get_deleted_objects([user_to_delete], MagicMock()) + + mocked_deleted_objs.assert_called_once() + assert deleted_objects == () + + @patch("codecov_auth.admin.admin.ModelAdmin.log_change") + def test_prev_and_new_values_in_log_entry(self, mocked_super_log_change): + owner = OwnerFactory(staff=True, plan=DEFAULT_FREE_PLAN) + owner.save() + owner.staff = False + form = MagicMock() + form.changed_data = ["staff"] + self.owner_admin.save_model( + request=MagicMock, new_obj=owner, form=form, change=True + ) + assert owner.changed_fields["staff"] == "prev value: True, new value: False" + + message = [] + message.append({"changed": {"fields": ["staff"]}}) + self.owner_admin.log_change(MagicMock, owner, message) + mocked_super_log_change.assert_called_once() + assert message == [ + {"changed": {"fields": ["staff"]}}, + {"staff": "prev value: True, new value: False"}, + ] + + def test_inline_orgwide_tokens_display(self): + owner = OwnerFactory(plan=DEFAULT_FREE_PLAN) + request_url = reverse("admin:codecov_auth_owner_change", args=[owner.ownerid]) + request = RequestFactory().get(request_url) + request.user = self.staff_user + inlines = self.owner_admin.get_inline_instances(request, owner) + # Orgs in enterprise cloud have a token created automagically + assert isinstance(inlines[0], OrgUploadTokenInline) + + def test_inline_orgwide_permissions(self): + owner_in_cloud_plan = OwnerFactory(plan=PlanName.ENTERPRISE_CLOUD_YEARLY.value) + org_token = OrganizationLevelTokenFactory(owner=owner_in_cloud_plan) + owner_in_cloud_plan.save() + org_token.save() + request_url = reverse( + "admin:codecov_auth_owner_change", args=[owner_in_cloud_plan.ownerid] + ) + request = RequestFactory().get(request_url) + request.user = self.staff_user + inlines = self.owner_admin.get_inline_instances(request, owner_in_cloud_plan) + inline_instance = inlines[0] + assert ( + inline_instance.has_add_permission(request, owner_in_cloud_plan) == False + ) # Should be false because it already has a token + assert ( + inline_instance.has_delete_permission(request, owner_in_cloud_plan) == True + ) + assert ( + inline_instance.has_change_permission(request, owner_in_cloud_plan) == False + ) + + def test_inline_orgwide_add_token_permission_no_token_and_user_in_enterprise_cloud_plan( + self, + ): + owner = OwnerFactory(plan=DEFAULT_FREE_PLAN) + assert OrganizationLevelToken.objects.filter(owner=owner).count() == 0 + request_url = reverse("admin:codecov_auth_owner_change", args=[owner.ownerid]) + request = RequestFactory().get(request_url) + request.user = self.staff_user + inlines = self.owner_admin.get_inline_instances(request, owner) + inline_instance = inlines[0] + assert inline_instance.has_add_permission(request, owner) == True + + def test_inline_orgwide_add_token_permission_no_token_user_not_in_enterprise_cloud_plan( + self, + ): + owner_in_cloud_plan = OwnerFactory(plan=PlanName.ENTERPRISE_CLOUD_YEARLY.value) + assert ( + OrganizationLevelToken.objects.filter(owner=owner_in_cloud_plan).count() + == 0 + ) + request_url = reverse( + "admin:codecov_auth_owner_change", args=[owner_in_cloud_plan.ownerid] + ) + request = RequestFactory().get(request_url) + request.user = self.staff_user + inlines = self.owner_admin.get_inline_instances(request, owner_in_cloud_plan) + inline_instance = inlines[0] + assert inline_instance.has_add_permission(request, owner_in_cloud_plan) == True + + @patch( + "codecov_auth.services.org_level_token_service.OrgLevelTokenService.refresh_token" + ) + def test_org_token_refresh_request_calls_service_to_refresh_token( + self, mock_refresh + ): + owner_in_cloud_plan = OwnerFactory(plan=PlanName.ENTERPRISE_CLOUD_YEARLY.value) + org_token = OrganizationLevelTokenFactory(owner=owner_in_cloud_plan) + owner_in_cloud_plan.save() + org_token.save() + request_url = reverse( + "admin:codecov_auth_owner_change", args=[owner_in_cloud_plan.ownerid] + ) + fake_data = { + "staff": ["true"], + "plan": ["users-enterprisem"], + "plan_provider": [""], + "plan_user_count": ["5"], + "plan_activated_users": [""], + "integration_id": [""], + "bot": [""], + "stripe_customer_id": [""], + "stripe_subscription_id": [""], + "organizations": [""], + "organization_tokens-TOTAL_FORMS": ["1"], + "organization_tokens-INITIAL_FORMS": ["0"], + "organization_tokens-MIN_NUM_FORMS": ["0"], + "organization_tokens-MAX_NUM_FORMS": ["1"], + "organization_tokens-0-id": [str(org_token.id)], + "organization_tokens-0-owner": [owner_in_cloud_plan.ownerid], + "organization_tokens-0-valid_until_0": ["2023-08-08"], + "organization_tokens-0-valid_until_1": ["17:01:14"], + "organization_tokens-0-token_type": ["upload"], + "organization_tokens-0-REFRESH": "on", + "_continue": ["Save and continue editing"], + } + self.client.post(request_url, data=fake_data) + mock_refresh.assert_called_with(str(org_token.id)) + + @patch( + "codecov_auth.services.org_level_token_service.OrgLevelTokenService.refresh_token" + ) + def test_org_token_request_doesnt_call_service_to_refresh_token(self, mock_refresh): + owner_in_cloud_plan = OwnerFactory(plan=PlanName.ENTERPRISE_CLOUD_YEARLY.value) + org_token = OrganizationLevelTokenFactory(owner=owner_in_cloud_plan) + owner_in_cloud_plan.save() + org_token.save() + request_url = reverse( + "admin:codecov_auth_owner_change", args=[owner_in_cloud_plan.ownerid] + ) + fake_data = { + "staff": ["true"], + "plan": ["users-enterprisem"], + "plan_provider": [""], + "plan_user_count": ["5"], + "plan_activated_users": [""], + "integration_id": [""], + "bot": [""], + "stripe_customer_id": [""], + "stripe_subscription_id": [""], + "organizations": [""], + "organization_tokens-TOTAL_FORMS": ["1"], + "organization_tokens-INITIAL_FORMS": ["0"], + "organization_tokens-MIN_NUM_FORMS": ["0"], + "organization_tokens-MAX_NUM_FORMS": ["1"], + "organization_tokens-0-id": [str(org_token.id)], + "organization_tokens-0-owner": [owner_in_cloud_plan.ownerid], + "organization_tokens-0-valid_until_0": ["2023-08-08"], + "organization_tokens-0-valid_until_1": ["17:01:14"], + "organization_tokens-0-token_type": ["upload"], + "_continue": ["Save and continue editing"], + } + self.client.post(request_url, data=fake_data) + mock_refresh.assert_not_called() + + def test_start_trial_ui_display(self): + owner = OwnerFactory(plan=DEFAULT_FREE_PLAN) + + res = self.client.post( + reverse("admin:codecov_auth_owner_changelist"), + { + "action": "extend_trial", + ACTION_CHECKBOX_NAME: [owner.pk], + }, + ) + assert res.status_code == 200 + assert "Extending trial for:" in str(res.content) + + @patch("shared.plan.service.PlanService.start_trial_manually") + def test_start_trial_action(self, mock_start_trial_service): + mock_start_trial_service.return_value = None + org_to_be_trialed = OwnerFactory(plan=DEFAULT_FREE_PLAN) + + res = self.client.post( + reverse("admin:codecov_auth_owner_changelist"), + { + "action": "extend_trial", + ACTION_CHECKBOX_NAME: [org_to_be_trialed.pk], + "end_date": "2024-01-01 01:02:03", + "extend_trial": True, + }, + ) + assert res.status_code == 302 + assert mock_start_trial_service.called + + @patch("shared.plan.service.PlanService._start_trial_helper") + def test_extend_trial_action(self, mock_start_trial_service): + mock_start_trial_service.return_value = None + org_to_be_trialed = OwnerFactory(plan=DEFAULT_FREE_PLAN) + org_to_be_trialed.plan = PlanName.TRIAL_PLAN_NAME.value + org_to_be_trialed.save() + + res = self.client.post( + reverse("admin:codecov_auth_owner_changelist"), + { + "action": "extend_trial", + ACTION_CHECKBOX_NAME: [org_to_be_trialed.pk], + "end_date": "2024-01-01 01:02:03", + "extend_trial": True, + }, + ) + assert res.status_code == 302 + assert mock_start_trial_service.called + assert mock_start_trial_service.call_args.kwargs == {"is_extension": True} + + @patch("shared.plan.service.PlanService.start_trial_manually") + def test_start_trial_paid_plan(self, mock_start_trial_service): + mock_start_trial_service.side_effect = ValidationError( + "Cannot trial from a paid plan" + ) + + org_to_be_trialed = OwnerFactory(plan=DEFAULT_FREE_PLAN) + + res = self.client.post( + reverse("admin:codecov_auth_owner_changelist"), + { + "action": "extend_trial", + ACTION_CHECKBOX_NAME: [org_to_be_trialed.pk], + "end_date": "2024-01-01 01:02:03", + "extend_trial": True, + }, + ) + assert res.status_code == 302 + assert mock_start_trial_service.called + + def test_account_widget(self): + owner = OwnerFactory( + user=UserFactory(), plan=PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + rf = RequestFactory() + get_request = rf.get(f"/admin/codecov_auth/owner/{owner.ownerid}/change/") + get_request.user = self.staff_user + sample_input = { + "change": True, + "fields": ["account", "plan", "uses_invoice", "staff"], + } + form = self.owner_admin.get_form(request=get_request, obj=owner, **sample_input) + # admin user cannot create, edit, or delete Account objects from the OwnerAdmin + self.assertFalse(form.base_fields["account"].widget.can_add_related) + self.assertFalse(form.base_fields["account"].widget.can_change_related) + self.assertFalse(form.base_fields["account"].widget.can_delete_related) + + +class UserAdminTest(TestCase): + def setUp(self): + self.staff_user = UserFactory(is_staff=True) + self.client.force_login(user=self.staff_user) + admin_site = AdminSite() + admin_site.register(User) + self.owner_admin = UserAdmin(User, admin_site) + + def test_user_admin_list_page(self): + user = UserFactory() + res = self.client.get(reverse("admin:codecov_auth_user_changelist")) + assert res.status_code == 200 + assert user.name in res.content.decode("utf-8") + assert user.email in res.content.decode("utf-8") + + def test_user_admin_detail_page(self): + user = UserFactory() + res = self.client.get(reverse("admin:codecov_auth_user_change", args=[user.pk])) + assert res.status_code == 200 + assert user.name in res.content.decode("utf-8") + assert user.email in res.content.decode("utf-8") + assert str(user.external_id) in res.content.decode("utf-8") + + +class SentryUserAdminTest(TestCase): + def setUp(self) -> None: + self.staff_user = UserFactory(is_staff=True) + self.client.force_login(user=self.staff_user) + admin_site = AdminSite() + admin_site.register(User) + admin_site.register(SentryUser) + self.owner_admin = UserAdmin(User, admin_site) + + def test_user_admin_list_page(self): + sentry_user = SentryUserFactory() + res = self.client.get(reverse("admin:codecov_auth_sentryuser_changelist")) + assert res.status_code == 200 + res.content.decode("utf-8") + assert sentry_user.name in res.content.decode("utf-8") + assert sentry_user.email in res.content.decode("utf-8") + + def test_user_admin_detail_page(self): + sentry_user = SentryUserFactory() + res = self.client.get( + reverse("admin:codecov_auth_sentryuser_change", args=[sentry_user.pk]) + ) + assert res.status_code == 200 + assert sentry_user.name in res.content.decode("utf-8") + assert sentry_user.email in res.content.decode("utf-8") + assert sentry_user.access_token not in res.content.decode("utf-8") + assert sentry_user.refresh_token not in res.content.decode("utf-8") + + +def create_stale_users( + account: Account | None = None, +) -> tuple[list[Owner], list[Owner]]: + org_1 = OwnerFactory(account=account) + org_2 = OwnerFactory(account=account) + + now = timezone.now() + + user_1 = OwnerFactory(user=UserFactory()) # stale, neither session nor PR + + user_2 = OwnerFactory(user=UserFactory()) # semi-stale, semi-old session + SessionFactory(owner=user_2, lastseen=now - timedelta(days=45)) + + user_3 = OwnerFactory(user=UserFactory()) # stale, old session + SessionFactory(owner=user_3, lastseen=now - timedelta(days=120)) + + user_4 = OwnerFactory(user=UserFactory()) # semi-stale, semi-old PR + pull = PullFactory( + repository=RepositoryFactory(), + author=user_4, + ) + pull.updatestamp = now - timedelta(days=45) + super(Pull, pull).save() # `Pull` overrides the `updatestamp` on each save + + user_5 = OwnerFactory(user=UserFactory()) # stale, old PR + pull = PullFactory( + repository=RepositoryFactory(), + author=user_5, + ) + pull.updatestamp = now - timedelta(days=120) + super(Pull, pull).save() # `Pull` overrides the `updatestamp` on each save + + org_1.plan_activated_users = [ + user_1.ownerid, + user_2.ownerid, + ] + org_1.save() + + org_2.plan_activated_users = [ + user_3.ownerid, + user_4.ownerid, + user_5.ownerid, + ] + org_2.save() + + return ([org_1, org_2], [user_1, user_2, user_3, user_4, user_5]) + + +@pytest.mark.django_db() +def test_stale_user_cleanup(): + orgs, users = create_stale_users() + + # remove stale users with default > 90 days + removed_users, affected_orgs = find_and_remove_stale_users(orgs) + assert removed_users == {users[0].ownerid, users[2].ownerid, users[4].ownerid} + assert affected_orgs == {orgs[0].ownerid, orgs[1].ownerid} + + orgs = list( + Owner.objects.filter(ownerid__in=[org.ownerid for org in orgs]) + ) # re-fetch orgs + # all good, nothing to do + removed_users, affected_orgs = find_and_remove_stale_users(orgs) + assert removed_users == set() + assert affected_orgs == set() + + # remove even more stale users + removed_users, affected_orgs = find_and_remove_stale_users(orgs, timedelta(days=30)) + assert removed_users == {users[1].ownerid, users[3].ownerid} + assert affected_orgs == {orgs[0].ownerid, orgs[1].ownerid} + + orgs = list( + Owner.objects.filter(ownerid__in=[org.ownerid for org in orgs]) + ) # re-fetch orgs + # all the users have been deactivated by now + for org in orgs: + assert len(org.plan_activated_users) == 0 + + +class AccountAdminTest(TestCase): + def setUp(self): + staff_user = UserFactory(is_staff=True) + self.client.force_login(user=staff_user) + admin_site = AdminSite() + admin_site.register(Account) + admin_site.register(StripeBilling) + admin_site.register(InvoiceBilling) + admin_site.register(AccountsUsers) + self.account_admin = AccountAdmin(Account, admin_site) + + self.account = AccountFactory(plan_seat_count=4, free_seat_count=2) + self.org_1 = OwnerFactory(account=self.account) + self.org_2 = OwnerFactory(account=self.account) + self.owner_with_user_1 = OwnerFactory(user=UserFactory()) + self.owner_with_user_2 = OwnerFactory(user=UserFactory()) + self.owner_with_user_3 = OwnerFactory(user=UserFactory()) + self.owner_without_user_1 = OwnerFactory(user=None) + self.owner_without_user_2 = OwnerFactory(user=None) + self.student = OwnerFactory(user=UserFactory(), student=True) + self.org_1.plan_activated_users = [ + self.owner_with_user_2.ownerid, + self.owner_with_user_3.ownerid, + self.owner_without_user_1.ownerid, + self.student.ownerid, + self.owner_without_user_2.ownerid, + ] + self.org_2.plan_activated_users = [ + self.owner_with_user_2.ownerid, + self.owner_with_user_3.ownerid, + self.owner_without_user_1.ownerid, + self.student.ownerid, + self.owner_with_user_1.ownerid, + ] + self.org_1.save() + self.org_2.save() + + def test_list_page(self): + res = self.client.get(reverse("admin:codecov_auth_account_changelist")) + self.assertEqual(res.status_code, 200) + decoded_res = res.content.decode("utf-8") + self.assertIn("column-name", decoded_res) + self.assertIn("column-is_active", decoded_res) + self.assertIn( + '', decoded_res + ) + self.assertIn( + '', + decoded_res, + ) + + def test_detail_page(self): + res = self.client.get( + reverse("admin:codecov_auth_account_change", args=[self.account.pk]) + ) + self.assertEqual(res.status_code, 200) + decoded_res = res.content.decode("utf-8") + self.assertIn( + '', + decoded_res, + ) + self.assertIn("Organizations (read only)", decoded_res) + self.assertIn("Stripe Billing (click save to commit changes)", decoded_res) + self.assertIn("Invoice Billing (click save to commit changes)", decoded_res) + + def test_link_users_to_account(self): + self.assertEqual(AccountsUsers.objects.all().count(), 0) + self.assertEqual(self.account.accountsusers_set.all().count(), 0) + + res = self.client.post( + reverse("admin:codecov_auth_account_changelist"), + { + "action": "link_users_to_account", + ACTION_CHECKBOX_NAME: [self.account.pk], + }, + ) + self.assertEqual(res.status_code, 302) + self.assertEqual(res.url, "/admin/codecov_auth/account/") + messages = list(res.wsgi_request._messages) + self.assertEqual(messages[0].message, "Created a User for 2 Owners") + self.assertEqual( + messages[1].message, "Created 6 AccountsUsers, removed 0 AccountsUsers" + ) + + self.assertEqual(AccountsUsers.objects.all().count(), 6) + self.assertEqual( + AccountsUsers.objects.filter(account_id=self.account.id).count(), 6 + ) + + for org in [self.org_1, self.org_2]: + for active_owner_id in org.plan_activated_users: + owner_obj = Owner.objects.get(pk=active_owner_id) + self.assertTrue( + AccountsUsers.objects.filter( + account=self.account, user_id=owner_obj.user_id + ).exists() + ) + + # another user joins + another_owner_with_user = OwnerFactory(user=UserFactory()) + self.org_1.plan_activated_users.append(another_owner_with_user.ownerid) + self.org_1.save() + # rerun action to re-sync + res = self.client.post( + reverse("admin:codecov_auth_account_changelist"), + { + "action": "link_users_to_account", + ACTION_CHECKBOX_NAME: [self.account.pk], + }, + ) + self.assertEqual(res.status_code, 302) + self.assertEqual(res.url, "/admin/codecov_auth/account/") + messages = list(res.wsgi_request._messages) + self.assertEqual(messages[2].message, "Created a User for 0 Owners") + self.assertEqual( + messages[3].message, "Created 1 AccountsUsers, removed 0 AccountsUsers" + ) + + self.assertEqual(AccountsUsers.objects.all().count(), 7) + self.assertEqual( + AccountsUsers.objects.filter(account_id=self.account.id).count(), 7 + ) + self.assertIn( + another_owner_with_user.user_id, + self.account.accountsusers_set.all().values_list("user_id", flat=True), + ) + + def test_link_users_to_account_not_enough_seats(self): + self.assertEqual(AccountsUsers.objects.all().count(), 0) + self.account.plan_seat_count = 1 + self.account.save() + res = self.client.post( + reverse("admin:codecov_auth_account_changelist"), + { + "action": "link_users_to_account", + ACTION_CHECKBOX_NAME: [self.account.pk], + }, + ) + self.assertEqual(res.status_code, 302) + self.assertEqual(res.url, "/admin/codecov_auth/account/") + messages = list(res.wsgi_request._messages) + self.assertEqual( + messages[0].message, + "Request failed: Account plan does not have enough seats; current plan activated users (non-students): 5, total seats for account: 3", + ) + self.assertEqual(AccountsUsers.objects.all().count(), 0) + + def test_seat_check(self): + # edge case: User has multiple Owners, one of which is a Student, but should still count as 1 seat on this Account + user = self.owner_with_user_1.user + OwnerFactory( + service="gitlab", user=user, student=False + ) # another owner on this user + OwnerFactory( + service="bitbucket", user=user, student=True + ) # student owner on this user + + self.assertEqual(AccountsUsers.objects.all().count(), 0) + self.account.plan_seat_count = 1 + self.account.save() + res = self.client.post( + reverse("admin:codecov_auth_account_changelist"), + { + "action": "seat_check", + ACTION_CHECKBOX_NAME: [self.account.pk], + }, + ) + self.assertEqual(res.status_code, 302) + self.assertEqual(res.url, "/admin/codecov_auth/account/") + messages = list(res.wsgi_request._messages) + self.assertEqual( + messages[0].message, + "Request failed: Account plan does not have enough seats; current plan activated users (non-students): 5, total seats for account: 3", + ) + + self.account.plan_seat_count = 10 + self.account.save() + res = self.client.post( + reverse("admin:codecov_auth_account_changelist"), + { + "action": "seat_check", + ACTION_CHECKBOX_NAME: [self.account.pk], + }, + ) + self.assertEqual(res.status_code, 302) + self.assertEqual(res.url, "/admin/codecov_auth/account/") + messages = list(res.wsgi_request._messages) + self.assertEqual( + messages[1].message, + "Request succeeded: Account plan has enough seats! current plan activated users (non-students): 5, total seats for account: 12", + ) + self.assertEqual(AccountsUsers.objects.all().count(), 0) + + def test_link_users_to_account_remove_unneeded_account_users(self): + res = self.client.post( + reverse("admin:codecov_auth_account_changelist"), + { + "action": "link_users_to_account", + ACTION_CHECKBOX_NAME: [self.account.pk], + }, + ) + self.assertEqual(res.status_code, 302) + self.assertEqual(res.url, "/admin/codecov_auth/account/") + messages = list(res.wsgi_request._messages) + self.assertEqual(messages[0].message, "Created a User for 2 Owners") + self.assertEqual( + messages[1].message, "Created 6 AccountsUsers, removed 0 AccountsUsers" + ) + + self.assertEqual(AccountsUsers.objects.all().count(), 6) + self.assertEqual( + AccountsUsers.objects.filter(account_id=self.account.id).count(), 6 + ) + + for org in [self.org_1, self.org_2]: + for active_owner_id in org.plan_activated_users: + owner_obj = Owner.objects.get(pk=active_owner_id) + self.assertTrue( + AccountsUsers.objects.filter( + account=self.account, user_id=owner_obj.user_id + ).exists() + ) + + # disconnect one of the orgs + self.org_2.account = None + self.org_2.save() + + # re-sync to remove Account users from org 2 that are not connected to other account orgs (just owner_with_user_1) + res = self.client.post( + reverse("admin:codecov_auth_account_changelist"), + { + "action": "link_users_to_account", + ACTION_CHECKBOX_NAME: [self.account.pk], + }, + ) + self.assertEqual(res.status_code, 302) + self.assertEqual(res.url, "/admin/codecov_auth/account/") + messages = list(res.wsgi_request._messages) + self.assertEqual(messages[2].message, "Created a User for 0 Owners") + self.assertEqual( + messages[3].message, "Created 0 AccountsUsers, removed 1 AccountsUsers" + ) + + self.assertEqual(AccountsUsers.objects.all().count(), 5) + self.assertEqual( + AccountsUsers.objects.filter(account_id=self.account.id).count(), 5 + ) + still_connected = [ + self.owner_with_user_2, + self.owner_with_user_3, + self.owner_without_user_1, + self.owner_without_user_2, + self.student, + ] + for owner in still_connected: + owner.refresh_from_db() + self.assertTrue( + AccountsUsers.objects.filter( + account=self.account, user_id=owner.user_id + ).exists() + ) + + self.owner_with_user_1.refresh_from_db() # removed user + # no longer connected to account + self.assertFalse( + AccountsUsers.objects.filter( + account=self.account, user_id=self.owner_with_user_1.user_id + ).exists() + ) + # still connected to org + self.assertIn( + self.owner_with_user_1.ownerid, + Owner.objects.get(pk=self.org_2.pk).plan_activated_users, + ) + # user object still exists, with no account connections + self.assertIsNotNone(self.owner_with_user_1.user_id) + self.assertFalse( + AccountsUsers.objects.filter(user=self.owner_with_user_1.user).exists() + ) + + def test_deactivate_stale_users(self): + account = AccountFactory() + orgs, users = create_stale_users(account) + + res = self.client.post( + reverse("admin:codecov_auth_account_changelist"), + { + "action": "deactivate_stale_users", + ACTION_CHECKBOX_NAME: [account.pk], + }, + ) + messages = list(res.wsgi_request._messages) + self.assertEqual( + messages[-1].message, "Removed 3 stale users from 2 affected organizations." + ) + + res = self.client.post( + reverse("admin:codecov_auth_account_changelist"), + { + "action": "deactivate_stale_users", + ACTION_CHECKBOX_NAME: [account.pk], + }, + ) + messages = list(res.wsgi_request._messages) + self.assertEqual( + messages[-1].message, + "No stale users found in selected accounts / organizations.", + ) + + +class StripeBillingAdminTest(TestCase): + def setUp(self): + self.staff_user = UserFactory(is_staff=True) + self.client.force_login(user=self.staff_user) + admin_site = AdminSite() + admin_site.register(StripeBilling) + self.stripe_admin = StripeBillingAdmin(StripeBilling, admin_site) + self.account = AccountFactory() + self.obj = StripeBillingFactory(account=self.account) + + def test_account_widget(self): + rf = RequestFactory() + get_request = rf.get(f"/admin/codecov_auth/stripebilling/{self.obj.id}/change/") + sample_input = { + "change": True, + "fields": [ + "id", + "created_at", + "updated_at", + "account", + "customer_id", + "subscription_id", + "is_active", + ], + } + form = self.stripe_admin.get_form( + request=get_request, obj=self.obj, **sample_input + ) + # admin user cannot create, edit, or delete Account objects from the StripeBillingAdmin + self.assertFalse(form.base_fields["account"].widget.can_add_related) + self.assertFalse(form.base_fields["account"].widget.can_change_related) + self.assertFalse(form.base_fields["account"].widget.can_delete_related) + + +class InvoiceBillingAdminTest(TestCase): + def setUp(self): + self.staff_user = UserFactory(is_staff=True) + self.client.force_login(user=self.staff_user) + admin_site = AdminSite() + admin_site.register(InvoiceBilling) + self.invoice_admin = InvoiceBillingAdmin(InvoiceBilling, admin_site) + self.account = AccountFactory() + self.obj = InvoiceBillingFactory(account=self.account) + + def test_account_widget(self): + rf = RequestFactory() + get_request = rf.get( + f"/admin/codecov_auth/invoicebilling/{self.obj.id}/change/" + ) + sample_input = { + "change": True, + "fields": [ + "id", + "created_at", + "updated_at", + "account", + "account_manager", + "invoice_notes", + "is_active", + ], + } + form = self.invoice_admin.get_form( + request=get_request, obj=self.obj, **sample_input + ) + # admin user cannot create, edit, or delete Account objects from the InvoiceBillingAdmin + self.assertFalse(form.base_fields["account"].widget.can_add_related) + self.assertFalse(form.base_fields["account"].widget.can_change_related) + self.assertFalse(form.base_fields["account"].widget.can_delete_related) + + +class PlanAdminTest(TestCase): + def setUp(self): + self.staff_user = UserFactory(is_staff=True) + self.client.force_login(user=self.staff_user) + admin_site = AdminSite() + admin_site.register(Plan) + + self.tier = TierFactory() + self.plan = PlanFactory(name=DEFAULT_FREE_PLAN, tier=self.tier) + + def test_plan_admin_modal_display(self): + response = self.client.get( + reverse("admin:codecov_auth_plan_change", args=[self.plan.pk]) + ) + self.assertEqual(response.status_code, 200) + self.assertContains(response, self.plan.name) + + def test_plan_modal_tiers_display(self): + response = self.client.get( + reverse("admin:codecov_auth_plan_change", args=[self.plan.pk]) + ) + self.assertEqual(response.status_code, 200) + self.assertContains(response, self.tier.tier_name) + + def test_add_plans_modal_action(self): + data = { + "action": "add_plans", + ACTION_CHECKBOX_NAME: [self.plan.pk], + "tier_id": self.tier.pk, + } + response = self.client.post( + reverse("admin:codecov_auth_plan_changelist"), data=data + ) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/admin/codecov_auth/plan/") + + def test_plan_change_form(self): + response = self.client.get( + reverse("admin:codecov_auth_plan_change", args=[self.plan.pk]) + ) + self.assertEqual(response.status_code, 200) + for field in [ + "tier", + "name", + "marketing_name", + "base_unit_price", + "benefits", + "billing_rate", + "is_active", + "max_seats", + "monthly_uploads_limit", + "paid_plan", + ]: + self.assertContains(response, f"id_{field}") + + def test_plan_change_form_validation(self): + self.plan.base_unit_price = -10 + self.plan.save() + + response = self.client.post( + reverse("admin:codecov_auth_plan_change", args=[self.plan.pk]), + { + "tier": self.tier.pk, + "name": self.plan.name, + "marketing_name": self.plan.marketing_name, + "base_unit_price": -10, + "benefits": self.plan.benefits, + "is_active": self.plan.is_active, + "paid_plan": self.plan.paid_plan, + }, + ) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Base unit price cannot be negative.") + + response = self.client.post( + reverse("admin:codecov_auth_plan_change", args=[self.plan.pk]), + { + "tier": self.tier.pk, + "name": self.plan.name, + "marketing_name": self.plan.marketing_name, + "base_unit_price": self.plan.base_unit_price, + "benefits": self.plan.benefits, + "is_active": self.plan.is_active, + "max_seats": -5, + "paid_plan": self.plan.paid_plan, + }, + ) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Max seats cannot be negative.") + + response = self.client.post( + reverse("admin:codecov_auth_plan_change", args=[self.plan.pk]), + { + "tier": self.tier.pk, + "name": self.plan.name, + "marketing_name": self.plan.marketing_name, + "benefits": self.plan.benefits, + "is_active": self.plan.is_active, + "monthly_uploads_limit": -5, + "paid_plan": self.plan.paid_plan, + }, + ) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Monthly uploads limit cannot be negative.") + + +class TierAdminTest(TestCase): + def setUp(self): + self.staff_user = UserFactory(is_staff=True) + self.client.force_login(user=self.staff_user) + admin_site = AdminSite() + admin_site.register(Tier) + + self.tier = TierFactory() + self.plan = PlanFactory(name=DEFAULT_FREE_PLAN, tier=self.tier) + + def test_tier_modal_plans_display(self): + response = self.client.get( + reverse("admin:codecov_auth_tier_change", args=[self.tier.pk]) + ) + self.assertEqual(response.status_code, 200) + self.assertContains(response, self.tier.tier_name) + + def test_add_plans_modal_action(self): + data = { + "action": "add_plans", + ACTION_CHECKBOX_NAME: [self.plan.pk], + "tier_id": self.tier.pk, + } + response = self.client.post( + reverse("admin:codecov_auth_tier_changelist"), data=data + ) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/admin/codecov_auth/tier/") + + def test_tier_change_form(self): + response = self.client.get( + reverse("admin:codecov_auth_tier_change", args=[self.tier.pk]) + ) + self.assertEqual(response.status_code, 200) + for field in [ + "tier_name", + "bundle_analysis", + "test_analytics", + "flaky_test_detection", + "project_coverage", + "private_repo_support", + ]: + self.assertContains(response, f"id_{field}") diff --git a/apps/codecov-api/codecov_auth/tests/test_migrations.py b/apps/codecov-api/codecov_auth/tests/test_migrations.py new file mode 100644 index 0000000000..adeebbaf2a --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/test_migrations.py @@ -0,0 +1,77 @@ +import pytest + +from utils.test_utils import TestMigrations + + +class Migration0046Test(TestMigrations): + migrate_from = "0045_remove_ownerprofile_terms_agreement" + migrate_to = "0046_dedupe_owner_admin_values" + + def setUpBeforeMigration(self, apps): + owners = apps.get_model("codecov_auth", "Owner") + + self.owner_no_dupes = owners.objects.create( + ownerid=1, + service_id=1, + service="github", + admins=[1, 2, 3], + ) + + self.owner_null_admins = owners.objects.create( + ownerid=2, + service_id=2, + service="github", + admins=None, + ) + + self.owner_no_admins = owners.objects.create( + ownerid=3, + service_id=3, + service="github", + admins=[], + ) + + self.owner_one_dupe = owners.objects.create( + ownerid=4, + service_id=4, + service="github", + admins=[1, 1], + ) + + self.owner_multi_dupe = owners.objects.create( + ownerid=5, + service_id=5, + service="github", + admins=[1, 1, 2, 3, 3, 3, 4], + ) + + self.owner_multi_dupe_ordering = owners.objects.create( + ownerid=6, + service_id=6, + service="github", + admins=[3, 2, 1, 2, 3], + ) + + @pytest.mark.skip( + reason="move to shared" + ) # TODO move this test to live with auth models in shared + def test_admins_deduped(self): + owners = self.apps.get_model("codecov_auth", "Owner") + + owner = owners.objects.get(ownerid=self.owner_no_dupes.ownerid) + assert owner.admins == [1, 2, 3] + + owner = owners.objects.get(ownerid=self.owner_null_admins.ownerid) + assert owner.admins == [] + + owner = owners.objects.get(ownerid=self.owner_no_admins.ownerid) + assert owner.admins == [] + + owner = owners.objects.get(ownerid=self.owner_one_dupe.ownerid) + assert owner.admins == [1] + + owner = owners.objects.get(ownerid=self.owner_multi_dupe.ownerid) + assert owner.admins == [1, 2, 3, 4] + + owner = owners.objects.get(ownerid=self.owner_multi_dupe_ordering.ownerid) + assert owner.admins == [3, 2, 1] diff --git a/apps/codecov-api/codecov_auth/tests/test_signals.py b/apps/codecov-api/codecov_auth/tests/test_signals.py new file mode 100644 index 0000000000..639978b453 --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/test_signals.py @@ -0,0 +1,121 @@ +from unittest import mock +from unittest.mock import call + +import pytest +from django.test import TestCase +from shared.django_apps.codecov_auth.models import Service +from shared.django_apps.codecov_auth.tests.factories import ( + OrganizationLevelTokenFactory, + OwnerFactory, +) + + +@pytest.mark.django_db +def test_shelter_org_token_sync(mocker): + publish = mocker.patch("google.cloud.pubsub_v1.PublisherClient.publish") + + # this triggers the publish via Django signals + OrganizationLevelTokenFactory(id=91728376, owner=OwnerFactory(ownerid=111)) + + publish.assert_has_calls( + [ + call( + "projects/test-project-id/topics/test-topic-id", + b'{"type": "owner", "sync": "one", "id": 111}', + ), + call( + "projects/test-project-id/topics/test-topic-id", + b'{"type": "org_token", "sync": "one", "id": 91728376}', + ), + ] + ) + + +@mock.patch("google.cloud.pubsub_v1.PublisherClient.publish") +class TestCodecovAuthSignals(TestCase): + def test_sync_on_create(self, mock_publish): + OwnerFactory(ownerid=12345) + mock_publish.assert_called_once_with( + "projects/test-project-id/topics/test-topic-id", + b'{"type": "owner", "sync": "one", "id": 12345}', + ) + + def test_sync_on_update_upload_token_required_for_public_repos(self, mock_publish): + owner = OwnerFactory(ownerid=12345, upload_token_required_for_public_repos=True) + owner.upload_token_required_for_public_repos = False + owner.save() + mock_publish.assert_has_calls( + [ + call( + "projects/test-project-id/topics/test-topic-id", + b'{"type": "owner", "sync": "one", "id": 12345}', + ), + call( + "projects/test-project-id/topics/test-topic-id", + b'{"type": "owner", "sync": "one", "id": 12345}', + ), + ] + ) + + def test_sync_on_update_username(self, mock_publish): + owner = OwnerFactory(ownerid=12345, username="hello") + owner.username = "world" + owner.save() + mock_publish.assert_has_calls( + [ + call( + "projects/test-project-id/topics/test-topic-id", + b'{"type": "owner", "sync": "one", "id": 12345}', + ), + call( + "projects/test-project-id/topics/test-topic-id", + b'{"type": "owner", "sync": "one", "id": 12345}', + ), + ] + ) + + def test_sync_on_update_service(self, mock_publish): + owner = OwnerFactory(ownerid=12345, service=Service.GITHUB.value) + owner.service = Service.BITBUCKET.value + owner.save() + mock_publish.assert_has_calls( + [ + call( + "projects/test-project-id/topics/test-topic-id", + b'{"type": "owner", "sync": "one", "id": 12345}', + ), + call( + "projects/test-project-id/topics/test-topic-id", + b'{"type": "owner", "sync": "one", "id": 12345}', + ), + ] + ) + + def test_no_sync_on_update_other_fields(self, mock_publish): + owner = OwnerFactory(ownerid=12345, name="hello") + owner.name = "world" + owner.save() + mock_publish.assert_called_once_with( + "projects/test-project-id/topics/test-topic-id", + b'{"type": "owner", "sync": "one", "id": 12345}', + ) + + @mock.patch("logging.Logger.warning") + def test_sync_error(self, mock_log, mock_publish): + mock_publish.side_effect = Exception("publish error") + + OwnerFactory(ownerid=12345) + + # publish is still called, raises an Exception + mock_publish.assert_called_once_with( + "projects/test-project-id/topics/test-topic-id", + b'{"type": "owner", "sync": "one", "id": 12345}', + ) + + mock_log.assert_called_once_with( + "Failed to publish a message", + extra=dict( + data_to_publish={"type": "owner", "sync": "one", "id": 12345}, + error=mock_publish.side_effect, + ), + ) diff --git a/apps/codecov-api/codecov_auth/tests/unit/__init__.py b/apps/codecov-api/codecov_auth/tests/unit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/codecov_auth/tests/unit/services/test_org_level_token_service.py b/apps/codecov-api/codecov_auth/tests/unit/services/test_org_level_token_service.py new file mode 100644 index 0000000000..5c13134f8a --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/services/test_org_level_token_service.py @@ -0,0 +1,95 @@ +import uuid +from unittest.mock import patch + +import pytest +from django.forms import ValidationError +from django.test import TestCase +from shared.django_apps.codecov_auth.tests.factories import ( + OrganizationLevelTokenFactory, + OwnerFactory, + PlanFactory, + TierFactory, +) +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName, TierName + +from codecov_auth.models import OrganizationLevelToken +from codecov_auth.services.org_level_token_service import OrgLevelTokenService + + +@patch( + "codecov_auth.services.org_level_token_service.OrgLevelTokenService.org_can_have_upload_token" +) +def test_token_is_deleted_when_changing_user_plan(mocked_org_can_have_upload_token, db): + # This should happen because of the signal consumer we have defined in + # codecov_auth/services/org_upload_token_service.py > manage_org_tokens_if_owner_plan_changed + mocked_org_can_have_upload_token.return_value = False + enterprise_tier = TierFactory(tier_name=TierName.ENTERPRISE.value) + enterprise_plan = PlanFactory( + tier=enterprise_tier, name=PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + owner = OwnerFactory(plan=enterprise_plan.name) + org_token = OrganizationLevelTokenFactory(owner=owner) + owner.save() + org_token.save() + assert OrganizationLevelToken.objects.filter(owner=owner).count() == 1 + owner.plan = "users-basic" + owner.save() + assert OrganizationLevelToken.objects.filter(owner=owner).count() == 0 + + +class TestOrgWideUploadTokenService(TestCase): + def setUp(self): + self.enterprise_tier = TierFactory(tier_name=TierName.ENTERPRISE.value) + self.enterprise_plan = PlanFactory( + tier=self.enterprise_tier, + name=PlanName.ENTERPRISE_CLOUD_YEARLY.value, + ) + self.basic_tier = TierFactory(tier_name=TierName.BASIC.value) + self.basic_plan = PlanFactory( + tier=self.basic_tier, + name=DEFAULT_FREE_PLAN, + ) + self.owner = OwnerFactory(plan=self.enterprise_plan.name) + + def test_get_org_token(self): + # Check that if you try to create a token for an org that already has one you get the same token + org_token = OrganizationLevelTokenFactory(owner=self.owner) + self.owner.save() + org_token.save() + assert org_token == OrgLevelTokenService.get_or_create_org_token(self.owner) + + def test_create_org_token(self): + user_in_enterprise_plan = OwnerFactory(plan=self.enterprise_plan.name) + token = OrgLevelTokenService.get_or_create_org_token(user_in_enterprise_plan) + assert isinstance(token.token, uuid.UUID) + assert token.owner == user_in_enterprise_plan + # Check that users not in enterprise plan can create org tokens + user_not_in_enterprise_plan = OwnerFactory(plan=self.basic_plan.name) + token = OrgLevelTokenService.get_or_create_org_token( + user_not_in_enterprise_plan + ) + assert isinstance(token.token, uuid.UUID) + assert token.owner == user_not_in_enterprise_plan + + def test_delete_token(self): + owner = OwnerFactory(plan=self.enterprise_plan.name) + OrgLevelTokenService.delete_org_token_if_exists(owner) + with pytest.raises(OrganizationLevelToken.DoesNotExist): + OrganizationLevelToken.objects.get(owner=owner) + + def test_refresh_token(self): + owner = OwnerFactory(plan=self.enterprise_plan.name) + org_token = OrganizationLevelTokenFactory(owner=owner) + owner.save() + org_token.save() + previous_token_obj = OrganizationLevelToken.objects.get(owner=owner) + previous_token = previous_token_obj.token + OrgLevelTokenService.refresh_token(previous_token_obj.id) + refreshed_token_obj = OrganizationLevelToken.objects.get(owner=owner) + assert previous_token_obj.id == refreshed_token_obj.id + assert previous_token != refreshed_token_obj.token + + def test_refresh_token_error(self): + with pytest.raises(ValidationError): + # Token that doesn't exist + OrgLevelTokenService.refresh_token(1000) diff --git a/apps/codecov-api/codecov_auth/tests/unit/test_authentication.py b/apps/codecov-api/codecov_auth/tests/unit/test_authentication.py new file mode 100644 index 0000000000..138238d720 --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/test_authentication.py @@ -0,0 +1,256 @@ +from datetime import datetime, timedelta +from http.cookies import SimpleCookie +from unittest.mock import AsyncMock, call, patch + +import pytest +from django.conf import settings +from django.test import TestCase, override_settings +from django.urls import ResolverMatch +from rest_framework.exceptions import AuthenticationFailed, PermissionDenied +from rest_framework.test import APIRequestFactory +from shared.django_apps.codecov_auth.tests.factories import ( + OwnerFactory, + UserFactory, + UserTokenFactory, +) +from shared.django_apps.core.tests.factories import RepositoryFactory + +from codecov_auth.authentication import ( + InternalTokenAuthentication, + SuperTokenAuthentication, + UserTokenAuthentication, +) +from codecov_auth.authentication.types import ( + InternalToken, + InternalUser, + SuperToken, + SuperUser, +) + +# Using the standard RequestFactory API to create a form POST request + + +def set_resolver_match(request, kwargs={}): + match = ResolverMatch(func=lambda: None, args=(), kwargs=kwargs) + request.resolver_match = match + + +class UserTokenAuthenticationTests(TestCase): + def test_bearer_token_auth(self): + user_token = UserTokenFactory() + + request_factory = APIRequestFactory() + request = request_factory.get( + "", HTTP_AUTHORIZATION=f"Bearer {user_token.token}" + ) + + authenticator = UserTokenAuthentication() + result = authenticator.authenticate(request) + assert result == (user_token.owner, user_token) + + def test_bearer_token_auth_invalid_token(self): + request_factory = APIRequestFactory() + request = request_factory.get( + "", HTTP_AUTHORIZATION="Bearer 8f9bc6cb-fd14-43bc-bbb5-be1e7c948f34" + ) + + authenticator = UserTokenAuthentication() + with pytest.raises(AuthenticationFailed): + authenticator.authenticate(request) + + def test_token_not_uuid(self): + request_factory = APIRequestFactory() + request = request_factory.get("", HTTP_AUTHORIZATION="Bearer hello_world") + authenticator = UserTokenAuthentication() + with pytest.raises(AuthenticationFailed): + authenticator.authenticate(request) + + def test_bearer_token_auth_expired_token(self): + user_token = UserTokenFactory(valid_until=datetime.now() - timedelta(seconds=1)) + + request_factory = APIRequestFactory() + request = request_factory.get( + "", HTTP_AUTHORIZATION=f"Bearer {user_token.token}" + ) + + authenticator = UserTokenAuthentication() + with pytest.raises(AuthenticationFailed): + authenticator.authenticate(request) + + def test_bearer_token_auth_malformed_header(self): + request_factory = APIRequestFactory() + request = request_factory.get("", HTTP_AUTHORIZATION="wrong") + + authenticator = UserTokenAuthentication() + result = authenticator.authenticate(request) + assert result is None + + def test_bearer_token_auth_no_authorization_header(self): + request_factory = APIRequestFactory() + request = request_factory.get("") + + authenticator = UserTokenAuthentication() + result = authenticator.authenticate(request) + assert result is None + + +class SuperTokenAuthenticationTests(TestCase): + @override_settings(SUPER_API_TOKEN="17603a9e-0463-45e1-883e-d649fccf4ae8") + def test_bearer_token_auth_if_token_is_super_token(self): + super_token = "17603a9e-0463-45e1-883e-d649fccf4ae8" + + request_factory = APIRequestFactory() + request = request_factory.get("", HTTP_AUTHORIZATION=f"Bearer {super_token}") + + authenticator = SuperTokenAuthentication() + result = authenticator.authenticate(request) + assert isinstance(result[0], SuperUser) + assert isinstance(result[1], SuperToken) + assert result[1].token == super_token + + @override_settings(SUPER_API_TOKEN="17603a9e-0463-45e1-883e-d649fccf4ae8") + def test_bearer_token_auth_invalid_super_token(self): + super_token = "0ae68e58-79f8-4341-9531-55aada05a251" + request_factory = APIRequestFactory() + request = request_factory.get("", HTTP_AUTHORIZATION=f"Bearer {super_token}") + + authenticator = SuperTokenAuthentication() + result = authenticator.authenticate(request) + assert result is None + + def test_bearer_token_default_token_envar(self): + super_token = "0ae68e58-79f8-4341-9531-55aada05a251" + request_factory = APIRequestFactory() + request = request_factory.get("", HTTP_AUTHORIZATION=f"Bearer {super_token}") + authenticator = SuperTokenAuthentication() + result = authenticator.authenticate(request) + assert result is None + + def test_bearer_token_default_token_envar_and_same_string_as_header(self): + super_token = settings.SUPER_API_TOKEN + request_factory = APIRequestFactory() + request = request_factory.get("", HTTP_AUTHORIZATION=f"Bearer {super_token}") + authenticator = SuperTokenAuthentication() + with pytest.raises( + AuthenticationFailed, + match="Invalid token header. Token string should not contain spaces.", + ): + authenticator.authenticate(request) + + +class InternalTokenAuthenticationTests(TestCase): + @override_settings(CODECOV_INTERNAL_TOKEN="17603a9e-0463-45e1-883e-d649fccf4ae8") + def test_bearer_token_auth_if_token_is_internal_token(self): + internal_token = "17603a9e-0463-45e1-883e-d649fccf4ae8" + + request_factory = APIRequestFactory() + request = request_factory.get("", HTTP_AUTHORIZATION=f"Bearer {internal_token}") + + authenticator = InternalTokenAuthentication() + result = authenticator.authenticate(request) + assert isinstance(result[0], InternalUser) + assert isinstance(result[1], InternalToken) + assert result[1].token == internal_token + + @override_settings(CODECOV_INTERNAL_TOKEN="17603a9e-0463-45e1-883e-d649fccf4ae8") + def test_bearer_token_auth_if_token_is_not_internal_token(self): + internal_token = "random_token" + + request_factory = APIRequestFactory() + request = request_factory.get("", HTTP_AUTHORIZATION=f"Bearer {internal_token}") + + authenticator = InternalTokenAuthentication() + with pytest.raises( + AuthenticationFailed, + match="Invalid token", + ): + authenticator.authenticate(request) + + def test_bearer_token_default_token_envar_and_same_string_as_header(self): + internal_token = settings.CODECOV_INTERNAL_TOKEN + request_factory = APIRequestFactory() + request = request_factory.get("", HTTP_AUTHORIZATION=f"Bearer {internal_token}") + authenticator = InternalTokenAuthentication() + with pytest.raises( + AuthenticationFailed, + match="Invalid token header. Token string should not contain spaces.", + ): + authenticator.authenticate(request) + + +class ImpersonationTests(TestCase): + def setUp(self): + self.owner_to_impersonate = OwnerFactory( + username="impersonateme", service="github", user=UserFactory(is_staff=False) + ) + self.staff_user = UserFactory(is_staff=True) + self.non_staff_user = UserFactory(is_staff=False) + + self.client.cookies = SimpleCookie({"staff_user": self.owner_to_impersonate.pk}) + + def test_impersonation(self): + self.client.force_login(user=self.staff_user) + res = self.client.post( + "/graphql/gh", + {"query": "{ me { user { username } } }"}, + content_type="application/json", + ) + assert res.json()["data"]["me"] == {"user": {"username": "impersonateme"}} + + @patch("core.commands.repository.repository.RepositoryCommands.fetch_repository") + def test_impersonation_with_okta( + self, mock_call_to_fetch_repository, new_callable=AsyncMock + ): + repo = RepositoryFactory(author=self.owner_to_impersonate, private=True) + query_repositories = """{ owner(username: "%s") { repository(name: "%s") { ... on Repository { name } } } }""" + query = query_repositories % (repo.author.username, repo.name) + + # not impersonating + del self.client.cookies["staff_user"] + self.client.force_login(user=self.owner_to_impersonate.user) + self.client.post( + "/graphql/gh", + {"query": query}, + content_type="application/json", + ) + + # impersonating, same query + self.client.cookies = SimpleCookie({"staff_user": self.owner_to_impersonate.pk}) + self.client.force_login(user=self.staff_user) + self.client.post( + "/graphql/gh", + {"query": query}, + content_type="application/json", + ) + + mock_call_to_fetch_repository.assert_has_calls( + [ + call( + self.owner_to_impersonate, + repo.name, + [], + exclude_okta_enforced_repos=True, + needs_coverage=False, + needs_commits=False, + ), + call( + self.owner_to_impersonate, + repo.name, + [], + exclude_okta_enforced_repos=False, + needs_coverage=False, + needs_commits=False, + ), + ] + ) + + def test_impersonation_non_staff(self): + self.client.force_login(user=self.non_staff_user) + with pytest.raises(PermissionDenied): + self.client.get("/") + + def test_impersonation_invalid_user(self): + self.client.cookies = SimpleCookie({"staff_user": 9999}) + self.client.force_login(user=self.staff_user) + with pytest.raises(AuthenticationFailed): + self.client.get("/") diff --git a/apps/codecov-api/codecov_auth/tests/unit/test_helpers.py b/apps/codecov-api/codecov_auth/tests/unit/test_helpers.py new file mode 100644 index 0000000000..117fd31340 --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/test_helpers.py @@ -0,0 +1,69 @@ +from unittest.mock import patch + +import pytest +from django.contrib.admin.models import LogEntry +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory + +from codecov_auth.helpers import History, current_user_part_of_org + + +@pytest.mark.django_db +def test_current_user_part_of_org_when_user_not_authenticated(): + org = OwnerFactory() + assert current_user_part_of_org(None, org) is False + + +@pytest.mark.django_db +def test_current_user_part_of_org_when_user_is_owner(): + current_user = OwnerFactory() + assert current_user_part_of_org(current_user, current_user) is True + + +@pytest.mark.django_db +def test_current_user_part_of_org_when_user_doesnt_have_org(): + org = OwnerFactory() + current_user = OwnerFactory(organizations=None) + current_user.save() + assert current_user_part_of_org(current_user, org) is False + + +@pytest.mark.django_db +def test_current_user_part_of_org_when_user_has_org(): + org = OwnerFactory() + current_user = OwnerFactory(organizations=[org.ownerid]) + current_user.save() + assert current_user_part_of_org(current_user, current_user) is True + + +@pytest.mark.django_db +@patch("codecov_auth.helpers.format_stack") +def test_log_entry(mocked_format_stack): + mocked_format_stack.return_value = "test" + orig_owner = OwnerFactory() + impersonated_owner = OwnerFactory() + History.log( + impersonated_owner, + "Impersonation successful", + orig_owner.user, + add_traceback=True, + ) + log_entries = LogEntry.objects.all() + assert ( + str(log_entries.first()) + == f"Changed “{str(impersonated_owner)}” — Impersonation successful: test" + ) + + +@pytest.mark.django_db +@patch("codecov_auth.helpers.format_stack") +def test_log_entry_no_object(mocked_format_stack): + mocked_format_stack.return_value = "test" + orig_owner = OwnerFactory() + History.log( + None, + "Impersonation successful", + orig_owner.user, + add_traceback=True, + ) + log_entries = LogEntry.objects.all() + assert log_entries.first() is None diff --git a/apps/codecov-api/codecov_auth/tests/unit/test_managers.py b/apps/codecov-api/codecov_auth/tests/unit/test_managers.py new file mode 100644 index 0000000000..88fe0e8687 --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/test_managers.py @@ -0,0 +1,51 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov_auth.models import Owner + + +class OwnerManagerTests(TestCase): + def setUp(self): + self.owner = OwnerFactory() + + def test_users_of(self): + org = OwnerFactory() + self.owner.organizations = [org.ownerid] + self.owner.save() + + owner_in_org_and_plan_activated_users = OwnerFactory( + organizations=[org.ownerid] + ) + owner_only_in_plan_activated_users = OwnerFactory() + + org.plan_activated_users = [ + owner_in_org_and_plan_activated_users.ownerid, + owner_only_in_plan_activated_users.ownerid, + ] + org.save() + + with self.subTest("returns all users"): + users_of = Owner.objects.users_of(owner=org) + self.assertCountEqual( + [user.ownerid for user in users_of], + [ + self.owner.ownerid, + owner_only_in_plan_activated_users.ownerid, + owner_in_org_and_plan_activated_users.ownerid, + ], + ) + + with self.subTest("no plan_activated_users"): + org.plan_activated_users = [] + org.save() + users_of = Owner.objects.users_of(owner=org) + self.assertCountEqual( + [user.ownerid for user in users_of], + [self.owner.ownerid, owner_in_org_and_plan_activated_users.ownerid], + ) + + with self.subTest("no users"): + self.owner.delete() + owner_in_org_and_plan_activated_users.delete() + users_of = Owner.objects.users_of(owner=org) + self.assertEqual(list(users_of), []) diff --git a/apps/codecov-api/codecov_auth/tests/unit/test_middleware.py b/apps/codecov-api/codecov_auth/tests/unit/test_middleware.py new file mode 100644 index 0000000000..edc5b8179a --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/test_middleware.py @@ -0,0 +1,21 @@ +from django.test import TestCase, override_settings + +from utils.test_utils import Client + + +@override_settings(CORS_ALLOWED_ORIGINS=["http://localhost:3000"]) +class MiddlewareTest(TestCase): + def setUp(self): + self.client = Client() + + def test_whitelisted_origin(self): + res = self.client.get("/health", headers={"Origin": "http://localhost:3000"}) + + assert res.headers["Access-Control-Allow-Origin"] == "http://localhost:3000" + assert res.headers["Access-Control-Allow-Credentials"] == "true" + + def test_non_whitelisted_origin(self): + res = self.client.get("/health", headers={"Origin": "http://example.com"}) + + assert res.headers["Access-Control-Allow-Origin"] == "http://example.com" + assert "Access-Control-Allow-Credentials" not in res.headers diff --git a/apps/codecov-api/codecov_auth/tests/unit/test_repo_authentication.py b/apps/codecov-api/codecov_auth/tests/unit/test_repo_authentication.py new file mode 100644 index 0000000000..28d9241fb9 --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/test_repo_authentication.py @@ -0,0 +1,899 @@ +import uuid +from datetime import datetime, timedelta +from unittest.mock import patch + +import pytest +from django.core.exceptions import ObjectDoesNotExist +from django.db.models import QuerySet +from django.test import override_settings +from django.utils import timezone +from jwt import PyJWTError +from rest_framework import exceptions +from rest_framework.test import APIRequestFactory +from shared.django_apps.codecov_auth.models import Owner, Service +from shared.django_apps.core.models import Repository +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, + RepositoryTokenFactory, +) + +from codecov_auth.authentication.repo_auth import ( + GitHubOIDCTokenAuthentication, + GlobalTokenAuthentication, + OrgLevelTokenAuthentication, + RepositoryLegacyQueryTokenAuthentication, + RepositoryLegacyTokenAuthentication, + RepositoryTokenAuthentication, + TokenlessAuth, + TokenlessAuthentication, + UploadTokenRequiredAuthenticationCheck, + UploadTokenRequiredGetFromBodyAuthenticationCheck, +) +from codecov_auth.models import SERVICE_GITHUB, OrganizationLevelToken, RepositoryToken + + +class TestRepositoryLegacyQueryTokenAuthentication(object): + def test_authenticate_unauthenticated(self): + request = APIRequestFactory().get("/endpoint") + authentication = RepositoryLegacyQueryTokenAuthentication() + res = authentication.authenticate(request) + assert res is None + + def test_authenticate_non_uuid_token(self): + request = APIRequestFactory().get("/endpoint?token=banana") + authentication = RepositoryLegacyQueryTokenAuthentication() + res = authentication.authenticate(request) + assert res is None + + def test_authenticate_uuid_token_no_repo(self, db): + request = APIRequestFactory().get( + "/endpoint?token=testwabzdowkt4kyti9w0hxa33zetsta" + ) + authentication = RepositoryLegacyQueryTokenAuthentication() + res = authentication.authenticate(request) + assert res is None + + def test_authenticate_uuid_token_with_repo(self, db): + repo = RepositoryFactory.create() + other_repo = RepositoryFactory.create() + request = APIRequestFactory().get(f"/endpoint?token={repo.upload_token}") + authentication = RepositoryLegacyQueryTokenAuthentication() + res = authentication.authenticate(request) + assert res is not None + user, auth = res + assert user._repository == repo + assert auth.get_repositories() == [repo] + assert auth.get_scopes() == ["upload"] + assert user.is_authenticated() + assert auth.allows_repo(repo) + assert not auth.allows_repo(other_repo) + + +class TestRepositoryLegacyTokenAuthentication(object): + def test_authenticate_credentials_empty(self, db): + token = None + authentication = RepositoryLegacyTokenAuthentication() + res = authentication.authenticate_credentials(token) + assert res is None + + def test_authenticate_credentials_not_uuid(self, db): + token = "not-a-uuid" + authentication = RepositoryLegacyTokenAuthentication() + res = authentication.authenticate_credentials(token) + assert res is None + + def test_authenticate_credentials_uuid_no_repo(self, db): + token = str(uuid.uuid4()) + authentication = RepositoryLegacyTokenAuthentication() + res = authentication.authenticate_credentials(token) + assert res is None + + def test_authenticate_credentials_uuid_token_with_repo(self, db): + repo = RepositoryFactory.create() + authentication = RepositoryLegacyTokenAuthentication() + res = authentication.authenticate_credentials(repo.upload_token) + assert res is not None + user, auth = res + assert user._repository == repo + assert auth.get_repositories() == [repo] + assert auth.get_scopes() == ["upload"] + + +class TestRepositoryTableTokenAuthentication(object): + def test_authenticate_credentials_empty(self, db): + key = "" + authentication = RepositoryTokenAuthentication() + with pytest.raises(exceptions.AuthenticationFailed): + authentication.authenticate_credentials(key) + + def test_authenticate_credentials_valid_token_no_repo(self, db): + key = RepositoryToken.generate_key() + authentication = RepositoryTokenAuthentication() + with pytest.raises(exceptions.AuthenticationFailed): + authentication.authenticate_credentials(key) + + def test_authenticate_credentials_uuid_token_with_repo(self, db): + token = RepositoryTokenFactory.create( + repository__active=True, token_type="profiling" + ) + authentication = RepositoryTokenAuthentication() + res = authentication.authenticate_credentials(token.key) + assert res is not None + user, auth = res + assert user._repository == token.repository + assert auth.get_repositories() == [token.repository] + assert auth.get_scopes() == ["profiling"] + + def test_authenticate_credentials_uuid_token_with_repo_not_active(self, db): + token = RepositoryTokenFactory.create( + repository__active=False, token_type="profiling" + ) + authentication = RepositoryTokenAuthentication() + with pytest.raises(exceptions.AuthenticationFailed): + authentication.authenticate_credentials(token.key) + + def test_authenticate_credentials_uuid_token_with_repo_valid_until_not_reached( + self, db + ): + token = RepositoryTokenFactory.create( + repository__active=True, + token_type="banana", + valid_until=timezone.now() + timedelta(seconds=1000), + ) + authentication = RepositoryTokenAuthentication() + res = authentication.authenticate_credentials(token.key) + user, auth = res + assert user._repository == token.repository + assert auth.get_repositories() == [token.repository] + assert auth.get_scopes() == ["banana"] + + def test_authenticate_credentials_uuid_token_with_repo_valid_until_already_reached( + self, db + ): + token = RepositoryTokenFactory.create( + repository__active=True, + token_type="banana", + valid_until=timezone.now() - timedelta(seconds=1000), + ) + authentication = RepositoryTokenAuthentication() + with pytest.raises(exceptions.AuthenticationFailed) as exc: + authentication.authenticate_credentials(token.key) + assert exc.value.args == ("Invalid token.",) + + +class TestGlobalTokenAuthentication(object): + def get_mocked_global_tokens(self): + return { + "githubuploadtoken": "github", + "gitlabuploadtoken": "gitlab", + "bitbucketserveruploadtoken": "bitbucket_server", + } + + @patch("codecov_auth.authentication.repo_auth.get_global_tokens") + def test_authentication_no_global_token_available(self, mocked_get_global_tokens): + mocked_get_global_tokens.return_value = {} + authentication = GlobalTokenAuthentication() + request = APIRequestFactory().post("/upload/service/owner::::repo/commits") + res = authentication.authenticate(request) + assert res is None + + @patch("codecov_auth.authentication.repo_auth.get_global_tokens") + def test_authentication_for_enterprise_wrong_token(self, mocked_get_global_tokens): + mocked_get_global_tokens.return_value = self.get_mocked_global_tokens() + authentication = GlobalTokenAuthentication() + request = APIRequestFactory().post( + "/upload/service/owner::::repo/commits", + headers={"Authorization": "token GUT"}, + ) + res = authentication.authenticate(request) + assert res is None + + @patch("codecov_auth.authentication.repo_auth.get_global_tokens") + def test_authentication_for_enterprise_correct_token_repo_not_exists( + self, mocked_get_global_tokens, db + ): + mocked_get_global_tokens.return_value = self.get_mocked_global_tokens() + authentication = GlobalTokenAuthentication() + request = APIRequestFactory().post( + "/upload/service/owner::::repo/commits", + headers={"Authorization": "token githubuploadtoken"}, + ) + with pytest.raises(exceptions.AuthenticationFailed) as exc: + authentication.authenticate(request) + assert exc.value.args == ( + "Could not find a repository, try using repo upload token", + ) + + @pytest.mark.parametrize( + "owner_service, owner_name, token", + [ + pytest.param("github", "username", "githubuploadtoken", id="github"), + pytest.param( + "gitlab", "username", "gitlabuploadtoken", id="gitlab_single_user" + ), + pytest.param( + "gitlab", + "usergroup:username", + "gitlabuploadtoken", + id="gitlab_subgroup_user", + ), + ], + ) + @patch("codecov_auth.authentication.repo_auth.get_global_tokens") + def test_authentication_for_enterprise_correct_token_repo_exists( + self, mocked_get_global_tokens, owner_service, owner_name, token, db + ): + mocked_get_global_tokens.return_value = self.get_mocked_global_tokens() + owner = OwnerFactory.create(service=owner_service, username=owner_name) + owner_name.replace(":", ":::") # encode name to test GL subgroups + repository = RepositoryFactory.create(author=owner) + authentication = GlobalTokenAuthentication() + request = APIRequestFactory().post( + f"/upload/{owner_service}/{owner_name}::::{repository.name}/commits", + headers={"Authorization": f"token {token}"}, + ) + res = authentication.authenticate(request) + assert res is not None + user, auth = res + assert user._repository == repository + assert auth.get_repositories() == [repository] + assert auth.get_scopes() == ["upload"] + + +@patch("codecov_auth.authentication.repo_auth.get_repo_with_github_actions_oidc_token") +class TestGitHubOIDCTokenAuthentication(object): + def test_authenticate_credentials_empty_returns_none( + self, mocked_get_repo_with_token, db + ): + token = None + authentication = GitHubOIDCTokenAuthentication() + res = authentication.authenticate_credentials(token) + assert res is None + + def test_authenticate_credentials_uuid_returns_none( + self, mocked_get_repo_with_token, db + ): + token = uuid.uuid4() + authentication = GitHubOIDCTokenAuthentication() + res = authentication.authenticate_credentials(token) + assert res is None + + def test_authenticate_credentials_no_repo(self, mocked_get_repo_with_token, db): + mocked_get_repo_with_token.side_effect = ObjectDoesNotExist() + token = "the best token" + authentication = GitHubOIDCTokenAuthentication() + res = authentication.authenticate_credentials(token) + assert res is None + + def test_authenticate_credentials_oidc_error(self, mocked_get_repo_with_token, db): + mocked_get_repo_with_token.side_effect = PyJWTError() + token = "the best token" + authentication = GitHubOIDCTokenAuthentication() + res = authentication.authenticate_credentials(token) + assert res is None + + def test_authenticate_credentials_oidc_valid(self, mocked_get_repo_with_token, db): + token = "the best token" + repository = RepositoryFactory() + owner = repository.author + owner.service = SERVICE_GITHUB + owner.save() + mocked_get_repo_with_token.return_value = repository + authentication = GitHubOIDCTokenAuthentication() + res = authentication.authenticate_credentials(token) + assert res is not None + user, auth = res + assert auth.get_repositories() == [repository] + assert auth.allows_repo(repository) + assert user._repository == repository + assert auth.get_scopes() == ["upload"] + + +valid_params_to_test = [ + ("/upload/github/owner::::the_repo/commits", "owner/the_repo", None), + ("/upload/github/owner::::the_repo/commits/", "owner/the_repo", None), + ( + "/upload/github/owner::::the_repo/commits/9652fb7ff577f554588ea83afded9000acd084ee/reports", + "owner/the_repo", + "9652fb7ff577f554588ea83afded9000acd084ee", + ), + ( + "/upload/github/owner::::the_repo/commits/9652fb7ff577f554588ea83afded9000acd084ee/reports/", + "owner/the_repo", + "9652fb7ff577f554588ea83afded9000acd084ee", + ), + ( + "/upload/github/owner::::the_repo/commits/9652fb7ff577f554588ea83afded9000acd084ee/reports/default/uploads", + "owner/the_repo", + "9652fb7ff577f554588ea83afded9000acd084ee", + ), + ( + "/upload/github/owner::::the_repo/commits/9652fb7ff577f554588ea83afded9000acd084ee/reports/default/uploads/", + "owner/the_repo", + "9652fb7ff577f554588ea83afded9000acd084ee", + ), + ( + "/upload/github/owner::::example-repo/commits", + "owner/example-repo", + None, + ), + ( + "/upload/github/owner::::__example-repo__/commits", + "owner/__example-repo__", + None, + ), + ( + "/upload/github/owner::::~example-repo:copy/commits", + "owner/~example-repo:copy", + None, + ), +] + + +class TestOrgLevelTokenAuthentication(object): + @override_settings(IS_ENTERPRISE=True) + def test_enterprise_no_token_return_none(self, db, mocker): + authentication = OrgLevelTokenAuthentication() + token = uuid.uuid4() + res = authentication.authenticate_credentials(token) + assert res is None + + @override_settings(IS_ENTERPRISE=False) + def test_owner_has_no_token_return_none(self, db, mocker): + token = uuid.uuid4() + authentication = OrgLevelTokenAuthentication() + res = authentication.authenticate_credentials(token) + assert res is None + + @override_settings(IS_ENTERPRISE=False) + def test_owner_has_token_but_wrong_one_sent_return_none(self, db, mocker): + owner = OwnerFactory(plan="users-enterprisey") + owner.save() + owner_token, _ = OrganizationLevelToken.objects.get_or_create(owner=owner) + owner_token.save() + # Valid UUID token but doesn't belong to owner + wrong_token = uuid.uuid4() + request = APIRequestFactory().post( + "/endpoint", HTTP_AUTHORIZATION=f"Token {wrong_token}" + ) + authentication = OrgLevelTokenAuthentication() + res = authentication.authenticate(request) + assert res is None + assert OrganizationLevelToken.objects.filter(owner=owner).count() == 1 + + @override_settings(IS_ENTERPRISE=False) + def test_expired_token_raises_exception(self, db, mocker): + owner = OwnerFactory(plan="users-enterprisey") + owner.save() + six_hours_ago = datetime.now() - timedelta(hours=6) + owner_token, _ = OrganizationLevelToken.objects.get_or_create( + owner=owner, valid_until=six_hours_ago + ) + owner_token.save() + + request = APIRequestFactory().post( + "/endpoint", HTTP_AUTHORIZATION=f"Token {owner_token.token}" + ) + authentication = OrgLevelTokenAuthentication() + with pytest.raises(exceptions.AuthenticationFailed) as exp: + authentication.authenticate(request) + + assert exp.match("Token is expired.") + + @override_settings(IS_ENTERPRISE=False) + def test_orgleveltoken_success_auth(self, db, mocker): + owner = OwnerFactory(plan="users-enterprisey") + owner.save() + week_from_now = datetime.now() + timedelta(days=7) + owner_token, _ = OrganizationLevelToken.objects.get_or_create( + owner=owner, valid_until=week_from_now + ) + owner_token.save() + repository = RepositoryFactory(author=owner) + other_repo_from_owner = RepositoryFactory(author=owner) + random_repo = RepositoryFactory() + repository.save() + other_repo_from_owner.save() + random_repo.save() + + request = APIRequestFactory().post( + "/endpoint", HTTP_AUTHORIZATION=f"Token {owner_token.token}" + ) + authentication = OrgLevelTokenAuthentication() + res = authentication.authenticate(request) + + assert res is not None + user, auth = res + assert user == owner + assert auth.get_repositories() == [other_repo_from_owner, repository] + assert auth._org == owner + get_repos_queryset = auth.get_repositories_queryset() + assert isinstance(get_repos_queryset, QuerySet) + # We can apply more filters to it + assert list( + get_repos_queryset.exclude(repoid=other_repo_from_owner.repoid).all() + ) == [repository] + assert auth.allows_repo(repository) + assert auth.allows_repo(other_repo_from_owner) + assert not auth.allows_repo(random_repo) + + @override_settings(IS_ENTERPRISE=True) + def test_orgleveltoken_success_auth_enterprise(self, db, mocker): + owner = OwnerFactory(plan="users-enterprisey") + owner.save() + week_from_now = datetime.now() + timedelta(days=7) + owner_token, _ = OrganizationLevelToken.objects.get_or_create( + owner=owner, valid_until=week_from_now + ) + owner_token.save() + repository = RepositoryFactory(author=owner) + other_repo_from_owner = RepositoryFactory(author=owner) + random_repo = RepositoryFactory() + repository.save() + other_repo_from_owner.save() + random_repo.save() + + request = APIRequestFactory().post( + "/endpoint", HTTP_AUTHORIZATION=f"Token {owner_token.token}" + ) + authentication = OrgLevelTokenAuthentication() + res = authentication.authenticate(request) + + assert res is not None + user, auth = res + assert user == owner + assert auth.get_repositories() == [other_repo_from_owner, repository] + assert auth._org == owner + get_repos_queryset = auth.get_repositories_queryset() + assert isinstance(get_repos_queryset, QuerySet) + # We can apply more filters to it + assert list( + get_repos_queryset.exclude(repoid=other_repo_from_owner.repoid).all() + ) == [repository] + assert auth.allows_repo(repository) + assert auth.allows_repo(other_repo_from_owner) + assert not auth.allows_repo(random_repo) + + def test_token_is_not_uuid(self): + """ + OIDC tokens are not UUID, so if you do token = OrganizationLevelToken.objects.filter(token=key).first(), + you get a ValidationError for trying a non-UUID in a models.UUIDField. Rather than adding a try/except, + check whether the incoming `key` is a UUID - if not, don't try to find it in OrganizationLevelToken. + """ + authentication = OrgLevelTokenAuthentication() + token = "not a uuid" + res = authentication.authenticate_credentials(token) + assert res is None + + +class TestTokenlessAuth(object): + def test_tokenless_bad_path(self): + request = APIRequestFactory().post( + "/endpoint", + headers={}, + ) + authentication = TokenlessAuthentication() + with pytest.raises(exceptions.AuthenticationFailed): + _ = authentication.authenticate(request) + + def test_tokenless_unknown_repository(self, db): + request = APIRequestFactory().post( + "/upload/github/owner::::repo/commits/commit_sha/reports/report_code/uploads", + headers={"X-Tokenless": "user-name/repo-forked", "X-Tokenless-PR": "15"}, + ) + authentication = TokenlessAuthentication() + with pytest.raises(exceptions.AuthenticationFailed): + _ = authentication.authenticate(request) + + @pytest.mark.parametrize("request_uri,repo_slug,commitid", valid_params_to_test) + def test_tokenless_matches_paths(self, request_uri, repo_slug, commitid, db): + author_name, repo_name = repo_slug.split("/") + repo = RepositoryFactory( + name=repo_name, author__username=author_name, private=False + ) + assert repo.service == "github" + request = APIRequestFactory().post( + request_uri, {"branch": "fork:branch"}, format="json" + ) + authentication = TokenlessAuthentication() + assert authentication._get_info_from_request_path(request) == (repo, commitid) + + @pytest.mark.parametrize("private", [False, True]) + @pytest.mark.parametrize("branch", ["branch", "fork:branch"]) + @pytest.mark.parametrize( + "existing_commit,commit_branch", + [(False, None), (True, "branch"), (True, "fork:branch")], + ) + def test_tokenless_success( + self, + db, + mocker, + private, + branch, + existing_commit, + commit_branch, + ): + repo = RepositoryFactory(private=private) + + if existing_commit: + commit = CommitFactory() + commit.branch = commit_branch + commit.repository = repo + commit.save() + + request = APIRequestFactory().post( + f"/upload/github/{repo.author.username}::::{repo.name}/commits/{commit.commitid}/reports/report_code/uploads", + {"branch": branch}, + format="json", + ) + + else: + request = APIRequestFactory().post( + f"/upload/github/{repo.author.username}::::{repo.name}/commits", + {"branch": branch}, + format="json", + ) + + authentication = TokenlessAuthentication() + expected = private is False and ( + (existing_commit is False and ":" in branch) + or (existing_commit is True and ":" in commit_branch) + ) + + if expected: + res = authentication.authenticate(request) + assert res is not None + repo_as_user, auth_class = res + + assert repo_as_user.is_authenticated() is expected + assert isinstance(auth_class, TokenlessAuth) + else: + with pytest.raises(exceptions.AuthenticationFailed): + res = authentication.authenticate(request) + + +class TestUploadTokenRequiredAuthenticationCheck(object): + def test_token_not_required_bad_path(self): + request = APIRequestFactory().post( + "/endpoint", + headers={}, + ) + authentication = UploadTokenRequiredAuthenticationCheck() + res = authentication.authenticate(request) + assert res is None + + def test_token_not_required_bad_service(self): + request = APIRequestFactory().post( + "/upload/other/owner::::repo/commits/commit_sha/reports/report_code/uploads", + headers={"X-Tokenless": "user-name/repo-forked", "X-Tokenless-PR": "15"}, + ) + authentication = UploadTokenRequiredAuthenticationCheck() + res = authentication.authenticate(request) + assert res is None + + def test_token_not_required_unknown_repository(self, db): + an_owner = OwnerFactory(upload_token_required_for_public_repos=False) + # their repo + RepositoryFactory(author=an_owner, private=False) + request_uri = f"/upload/github/{an_owner.username}::::bad/commits" + request = APIRequestFactory().post( + request_uri, {"branch": "branch"}, format="json" + ) + authentication = UploadTokenRequiredAuthenticationCheck() + res = authentication.authenticate(request) + assert res is None + + def test_token_not_required_unknown_owner(self, db): + repo = RepositoryFactory( + private=False, author__upload_token_required_for_public_repos=False + ) + request_uri = f"/upload/github/bad::::{repo.name}/commits" + request = APIRequestFactory().post( + request_uri, {"branch": "branch"}, format="json" + ) + authentication = UploadTokenRequiredAuthenticationCheck() + res = authentication.authenticate(request) + assert res is None + + @pytest.mark.parametrize("request_uri,repo_slug,commitid", valid_params_to_test) + @pytest.mark.parametrize("private", [False, True]) + @pytest.mark.parametrize("token_required", [False, True]) + def test_get_repository_and_owner( + self, request_uri, repo_slug, commitid, private, token_required, db + ): + author_name, repo_name = repo_slug.split("/") + repo = RepositoryFactory( + name=repo_name, + author__username=author_name, + private=private, + author__upload_token_required_for_public_repos=token_required, + ) + assert repo.service == "github" + request = APIRequestFactory().post( + request_uri, {"branch": "fork:branch"}, format="json" + ) + authentication = UploadTokenRequiredAuthenticationCheck() + assert authentication.get_repository_and_owner(request) == ( + repo, + repo.author, + ) + + @pytest.mark.parametrize("private", [False, True]) + @pytest.mark.parametrize("branch", ["branch", "fork:branch"]) + @pytest.mark.parametrize( + "existing_commit,commit_branch", + [(False, None), (True, "branch"), (True, "fork:branch")], + ) + @pytest.mark.parametrize("token_required", [False, True]) + def test_token_not_required_fork_branch_public_private( + self, + db, + mocker, + private, + branch, + existing_commit, + commit_branch, + token_required, + ): + repo = RepositoryFactory( + private=private, + author__upload_token_required_for_public_repos=token_required, + ) + + if existing_commit: + commit = CommitFactory() + commit.branch = commit_branch + commit.repository = repo + commit.save() + + request = APIRequestFactory().post( + f"/upload/github/{repo.author.username}::::{repo.name}/commits/{commit.commitid}/reports/report_code/uploads", + {"branch": branch}, + format="json", + ) + + else: + request = APIRequestFactory().post( + f"/upload/github/{repo.author.username}::::{repo.name}/commits", + {"branch": branch}, + format="json", + ) + + authentication = UploadTokenRequiredAuthenticationCheck() + + if not private and not token_required: + res = authentication.authenticate(request) + assert res is not None + repo_as_user, auth_class = res + + assert repo_as_user.is_authenticated() is True + assert isinstance(auth_class, TokenlessAuth) + else: + res = authentication.authenticate(request) + assert res is None + + +class TestUploadTokenRequiredGetFromBodyAuthenticationCheck(object): + def test_token_not_required_invalid_data(self): + request = APIRequestFactory().post( + "/endpoint", + data={"slug": 123, "git_service": "github"}, + format="json", + ) + authentication = UploadTokenRequiredGetFromBodyAuthenticationCheck() + res = authentication.authenticate(request) + assert res is None + + def test_token_not_required_no_data(self): + request = APIRequestFactory().post( + "/endpoint", + format="json", + ) + authentication = UploadTokenRequiredGetFromBodyAuthenticationCheck() + res = authentication.authenticate(request) + assert res is None + + def test_token_not_required_no_git_service(self, db): + owner = OwnerFactory(upload_token_required_for_public_repos=False) + # their repo + repo = RepositoryFactory(author=owner, private=False) + request_uri = f"/upload/github/{owner.username}::::{repo.name}/commits" + request = APIRequestFactory().post( + request_uri, + data={ + "slug": f"{owner.username}::::{repo.name}", + }, + format="json", + ) + authentication = UploadTokenRequiredGetFromBodyAuthenticationCheck() + assert authentication.get_repository_and_owner(request) == (None, None) + res = authentication.authenticate(request) + assert res is None + + def test_token_not_required_unknown_repository(self, db): + an_owner = OwnerFactory(upload_token_required_for_public_repos=False) + # their repo + RepositoryFactory(author=an_owner, private=False) + request_uri = f"/upload/github/{an_owner.username}::::bad/commits" + request = APIRequestFactory().post( + request_uri, + data={ + "slug": f"{an_owner.username}::::bad", + "service": an_owner.service, + }, + format="json", + ) + authentication = UploadTokenRequiredGetFromBodyAuthenticationCheck() + assert authentication.get_repository_and_owner(request) == (None, None) + res = authentication.authenticate(request) + assert res is None + + def test_token_not_required_unknown_owner(self, db): + repo = RepositoryFactory( + private=False, author__upload_token_required_for_public_repos=False + ) + request_uri = f"/upload/github/bad::::{repo.name}/commits" + request = APIRequestFactory().post( + request_uri, + data={ + "slug": f"bad::::{repo.name}", + "service": "github", + }, + format="json", + ) + authentication = UploadTokenRequiredGetFromBodyAuthenticationCheck() + res = authentication.authenticate(request) + assert res is None + + @pytest.mark.parametrize("request_uri,repo_slug,commitid", valid_params_to_test) + @pytest.mark.parametrize("private", [False, True]) + @pytest.mark.parametrize("token_required", [False, True]) + def test_get_repository_and_owner( + self, request_uri, repo_slug, commitid, private, token_required, db + ): + author_name, repo_name = repo_slug.split("/") + repo = RepositoryFactory( + name=repo_name, + author__username=author_name, + private=private, + author__upload_token_required_for_public_repos=token_required, + ) + assert repo.service == "github" + request = APIRequestFactory().post( + request_uri, + data={"slug": f"{author_name}::::{repo_name}", "service": "github"}, + format="json", + ) + authentication = UploadTokenRequiredGetFromBodyAuthenticationCheck() + + assert authentication.get_repository_and_owner(request) == ( + repo, + repo.author, + ) + + def test_get_repository_and_owner_with_service(self, db): + authentication = UploadTokenRequiredGetFromBodyAuthenticationCheck() + repo_name = "the-repo" + owner_username = "the-author" + for name, _ in Service.choices: + owner = OwnerFactory(service=name, username=owner_username) + RepositoryFactory(name=repo_name, author=owner) + + request = APIRequestFactory().post( + "endpoint/", + data={ + "slug": f"{owner_username}::::{repo_name}", + "git_service": Service.BITBUCKET.value, + }, + format="json", + ) + matching_owner = Owner.objects.get( + service=Service.BITBUCKET.value, username=owner_username + ) + matching_repo = Repository.objects.get( + name=repo_name, author__service=Service.BITBUCKET.value + ) + assert authentication.get_repository_and_owner(request) == ( + matching_repo, + matching_owner, + ) + + request = APIRequestFactory().post( + "endpoint/", + data={ + "slug": f"{owner_username}::::{repo_name}", + "git_service": Service.GITLAB.value, + }, + format="json", + ) + matching_owner = Owner.objects.get( + service=Service.GITLAB.value, username=owner_username + ) + matching_repo = Repository.objects.get( + name=repo_name, author__service=Service.GITLAB.value + ) + assert authentication.get_repository_and_owner(request) == ( + matching_repo, + matching_owner, + ) + + request = APIRequestFactory().post( + "endpoint/", + data={ + "slug": f"{owner_username}::::{repo_name}", + "git_service": Service.GITHUB.value, + }, + format="json", + ) + matching_owner = Owner.objects.get( + service=Service.GITHUB.value, username=owner_username + ) + matching_repo = Repository.objects.get( + name=repo_name, author__service=Service.GITHUB.value + ) + assert authentication.get_repository_and_owner(request) == ( + matching_repo, + matching_owner, + ) + + @pytest.mark.parametrize("private", [False, True]) + @pytest.mark.parametrize("branch", ["branch", "fork:branch"]) + @pytest.mark.parametrize( + "existing_commit,commit_branch", + [(False, None), (True, "branch"), (True, "fork:branch")], + ) + @pytest.mark.parametrize("token_required", [False, True]) + def test_token_not_required_fork_branch_public_private( + self, + db, + mocker, + private, + branch, + existing_commit, + commit_branch, + token_required, + ): + repo = RepositoryFactory( + private=private, + author__upload_token_required_for_public_repos=token_required, + ) + + if existing_commit: + commit = CommitFactory() + commit.branch = commit_branch + commit.repository = repo + commit.save() + + request = APIRequestFactory().post( + f"/upload/github/{repo.author.username}::::{repo.name}/commits/{commit.commitid}/reports/report_code/uploads", + data={ + "slug": f"{repo.author.username}::::{repo.name}", + "git_service": repo.author.service, + }, + format="json", + ) + + else: + request = APIRequestFactory().post( + f"/upload/github/{repo.author.username}::::{repo.name}/commits", + data={ + "slug": f"{repo.author.username}::::{repo.name}", + "git_service": repo.author.service, + }, + format="json", + ) + + authentication = UploadTokenRequiredGetFromBodyAuthenticationCheck() + + if not private and not token_required: + res = authentication.authenticate(request) + assert res is not None + repo_as_user, auth_class = res + + assert repo_as_user.is_authenticated() is True + assert isinstance(auth_class, TokenlessAuth) + else: + res = authentication.authenticate(request) + assert res is None diff --git a/apps/codecov-api/codecov_auth/tests/unit/views/__init__.py b/apps/codecov-api/codecov_auth/tests/unit/views/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/codecov_auth/tests/unit/views/test_base.py b/apps/codecov-api/codecov_auth/tests/unit/views/test_base.py new file mode 100644 index 0000000000..37fb93a9fd --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/views/test_base.py @@ -0,0 +1,896 @@ +from datetime import datetime, timedelta, timezone +from unittest.mock import Mock, call, patch + +import pytest +from django.conf import settings +from django.contrib.sessions.backends.cache import SessionStore +from django.core.exceptions import PermissionDenied +from django.http import HttpResponse +from django.test import RequestFactory, TestCase, override_settings +from freezegun import freeze_time +from shared.django_apps.codecov_auth.tests.factories import ( + OwnerFactory, + SessionFactory, + UserFactory, +) +from shared.license import LicenseInformation + +from codecov_auth.models import DjangoSession, Owner, OwnerProfile, Session +from codecov_auth.tests.factories import DjangoSessionFactory +from codecov_auth.views.base import LoginMixin, StateMixin + + +def set_up_mixin(to=None): + query_string = {"to": to} if to else None + mixin = StateMixin() + mixin.request = RequestFactory().get("", query_string) + mixin.request.session = SessionStore() + mixin.service = "github" + return mixin + + +def test_generate_state_without_redirection_url(mock_redis): + mixin = set_up_mixin() + state = mixin.generate_state() + assert ( + mock_redis.get(f"oauth-state-{state}").decode("utf-8") + == "http://localhost:3000/gh" + ) + + +def test_generate_state_with_path_redirection_url(mock_redis): + mixin = set_up_mixin("/gh/codecov") + state = mixin.generate_state() + assert mock_redis.get(f"oauth-state-{state}").decode("utf-8") == "/gh/codecov" + + +@override_settings(CORS_ALLOWED_ORIGINS=["https://app.codecov.io"]) +def test_generate_state_with_safe_domain_redirection_url(mock_redis): + mixin = set_up_mixin("https://app.codecov.io/gh/codecov") + state = mixin.generate_state() + assert ( + mock_redis.get(f"oauth-state-{state}").decode("utf-8") + == "https://app.codecov.io/gh/codecov" + ) + + +@override_settings(CORS_ALLOWED_ORIGINS=[]) +@override_settings(CORS_ALLOWED_ORIGIN_REGEXES=[r"^(https:\/\/)?(.+)\.codecov\.io$"]) +def test_generate_state_with_safe_domain_regex_redirection_url(mock_redis): + mixin = set_up_mixin("https://app.codecov.io/gh/codecov") + state = mixin.generate_state() + assert ( + mock_redis.get(f"oauth-state-{state}").decode("utf-8") + == "https://app.codecov.io/gh/codecov" + ) + + +@override_settings(CORS_ALLOWED_ORIGINS=[]) +@override_settings(CORS_ALLOWED_ORIGIN_REGEXES=[]) +def test_generate_state_with_unsafe_domain(mock_redis): + mixin = set_up_mixin("http://hacker.com/i-steal-cookie") + state = mixin.generate_state() + assert mock_redis.keys("*") != [] + assert ( + mock_redis.get(f"oauth-state-{state}").decode("utf-8") + == "http://localhost:3000/gh" + ) + + +@override_settings(CORS_ALLOWED_ORIGINS=[]) +@override_settings(CORS_ALLOWED_ORIGIN_REGEXES=[]) +def test_generate_state_when_wrong_url(mock_redis): + mixin = set_up_mixin("http://localhost:]/") + state = mixin.generate_state() + assert mock_redis.keys("*") != [] + assert ( + mock_redis.get(f"oauth-state-{state}").decode("utf-8") + == "http://localhost:3000/gh" + ) + + +def test_get_redirection_url_from_state_without_redis_state(mock_redis): + mixin = set_up_mixin() + assert mixin.get_redirection_url_from_state("not exist") == ( + "http://localhost:3000/gh", + False, + ) + + +def test_get_redirection_url_from_state_without_session_state(mock_redis): + mixin = set_up_mixin() + state = "abc" + mock_redis.set(mixin._get_key_redis(state), "http://localhost/gh/codecov") + assert mixin.get_redirection_url_from_state(state) == ( + "http://localhost:3000", + False, + ) + + +def test_get_redirection_url_from_state_with_session_state_mismatch(mock_redis): + mixin = set_up_mixin() + state = "abc" + mock_redis.set(mixin._get_key_redis(state), "http://localhost/gh/codecov") + mixin.request.session[mixin._session_key()] = "def" + + assert mixin.get_redirection_url_from_state(state) == ( + "http://localhost:3000", + False, + ) + + +def test_get_redirection_url_from_state_give_url(mock_redis): + mixin = set_up_mixin() + state = "abc" + mock_redis.set(mixin._get_key_redis(state), "http://localhost/gh/codecov") + mixin.request.session[mixin._session_key()] = state + + assert mixin.get_redirection_url_from_state("abc") == ( + "http://localhost/gh/codecov", + True, + ) + + +def test_remove_state_with_with_delay(mock_redis): + mixin = set_up_mixin() + mock_redis.set("oauth-state-abc", "http://localhost/gh/codecov") + mixin.remove_state("abc", delay=5) + initial_datetime = datetime.now() + with freeze_time(initial_datetime) as frozen_time: + assert mock_redis.get("oauth-state-abc") is not None + frozen_time.move_to(initial_datetime + timedelta(seconds=4)) + assert mock_redis.get("oauth-state-abc") is not None + frozen_time.move_to(initial_datetime + timedelta(seconds=6)) + assert mock_redis.get("oauth-state-abc") is None + + +def test_remove_state_with_with_no_delay(mock_redis): + mixin = set_up_mixin() + mock_redis.set("oauth-state-abc", "http://localhost/gh/codecov") + mixin.remove_state("abc") + assert mock_redis.get("oauth-state-abc") is None + + +class LoginMixinTests(TestCase): + def setUp(self): + self.mixin_instance = LoginMixin() + self.mixin_instance.service = "github" + self.request = RequestFactory().get("", {}) + self.request.user = None + self.request.current_owner = None + self.request.session = SessionStore() + self.mixin_instance.request = self.request + + @patch("services.analytics.AnalyticsService.user_signed_up") + def test_get_or_create_calls_analytics_user_signed_up_when_owner_created( + self, user_signed_up_mock + ): + self.mixin_instance._get_or_create_owner( + { + "user": {"id": 12345, "key": "4567", "login": "testuser"}, + "has_private_access": False, + }, + self.request, + ) + user_signed_up_mock.assert_called_once() + + @patch("shared.events.amplitude.AmplitudeEventPublisher.publish") + def test_get_or_create_calls_amplitude_user_created_when_owner_created( + self, amplitude_publish_mock + ): + self.mixin_instance._get_or_create_owner( + { + "user": {"id": 12345, "key": "4567", "login": "testuser"}, + "has_private_access": False, + }, + self.request, + ) + + owner = Owner.objects.get(service_id=12345, username="testuser") + + amplitude_publish_mock.assert_has_calls( + [ + call("User Created", {"user_ownerid": owner.ownerid}), + call("set_orgs", {"user_ownerid": owner.ownerid, "org_ids": []}), + ] + ) + + @patch("services.analytics.AnalyticsService.user_signed_in") + def test_get_or_create_calls_analytics_user_signed_in_when_owner_not_created( + self, user_signed_in_mock + ): + owner = OwnerFactory(service_id=89, service="github") + self.mixin_instance._get_or_create_owner( + { + "user": { + "id": owner.service_id, + "key": "02or0sa", + "login": owner.username, + }, + "has_private_access": owner.private_access, + }, + self.request, + ) + user_signed_in_mock.assert_called_once() + + @patch("shared.events.amplitude.AmplitudeEventPublisher.publish") + def test_get_or_create_calls_amplitude_user_logged_in_when_owner_not_created( + self, amplitude_publish_mock + ): + owner = OwnerFactory(service_id=89, service="github", organizations=[1, 2]) + self.mixin_instance._get_or_create_owner( + { + "user": { + "id": owner.service_id, + "key": "02or0sa", + "login": owner.username, + }, + "has_private_access": owner.private_access, + }, + self.request, + ) + + amplitude_publish_mock.assert_has_calls( + [ + call("User Logged in", {"user_ownerid": owner.ownerid}), + call("set_orgs", {"user_ownerid": owner.ownerid, "org_ids": [1, 2]}), + ] + ) + + @override_settings(IS_ENTERPRISE=False) + @patch("services.analytics.AnalyticsService.user_signed_in") + def test_set_marketing_tags_on_cookies(self, user_signed_in_mock): + OwnerFactory(service="github") + self.request = RequestFactory().get( + "", + { + "utm_department": "a", + "utm_campaign": "b", + "utm_medium": "c", + "utm_source": "d", + "utm_content": "e", + "utm_term": "f", + }, + ) + self.mixin_instance.request = self.request + response = HttpResponse() + self.mixin_instance.store_to_cookie_utm_tags(response) + assert ( + response.cookies["_marketing_tags"].value + == "utm_department=a&utm_campaign=b&utm_medium=c&utm_source=d&utm_content=e&utm_term=f" + ) + + @override_settings(IS_ENTERPRISE=True) + def test_get_marketing_tags_on_enterprise(self): + self.request = RequestFactory().get( + "", + { + "utm_department": "a", + "utm_campaign": "b", + "utm_medium": "c", + "utm_source": "d", + "utm_content": "e", + "utm_term": "f", + }, + ) + self.mixin_instance.request = self.request + response = HttpResponse() + self.mixin_instance.store_to_cookie_utm_tags(response) + marketing_tags = self.mixin_instance.retrieve_marketing_tags_from_cookie() + assert marketing_tags == {} + + @patch("services.analytics.AnalyticsService.user_signed_in") + def test_use_marketing_tags_from_cookies(self, user_signed_in_mock): + owner = OwnerFactory(service_id=89, service="github") + self.request.COOKIES["_marketing_tags"] = ( + "utm_department=a&utm_campaign=b&utm_medium=c&utm_source=d&utm_content=e&utm_term=f" + ) + self.mixin_instance._get_or_create_owner( + { + "user": { + "id": owner.service_id, + "key": "02or0sa", + "login": owner.username, + }, + "has_private_access": owner.private_access, + }, + self.request, + ) + user_signed_in_mock.assert_called_once_with( + owner, + **{ + "utm_department": "a", + "utm_campaign": "b", + "utm_medium": "c", + "utm_source": "d", + "utm_content": "e", + "utm_term": "f", + }, + ) + + def mock_get_or_create_owner(self, user_dict, *args): + owner = OwnerFactory( + service_id=user_dict.get("id", 89), + service="github", + ) + owner.organizations = [1, 2] + return owner, True + + @override_settings(IS_ENTERPRISE=True) + @patch( + "codecov_auth.views.base.LoginMixin._get_or_create_owner", + mock_get_or_create_owner, + ) + @patch( + "codecov_auth.views.base.LoginMixin.get_or_create_org", mock_get_or_create_owner + ) + @patch("services.refresh.RefreshService.trigger_refresh", lambda *args: None) + @patch( + "codecov_auth.views.base.LoginMixin._check_user_count_limitations", + lambda *args: True, + ) + @patch("codecov_auth.views.base.get_config") + def test_get_and_modify_user_enterprise_raise_usernotinorganization_error( + self, mock_get_config: Mock + ): + user_dict = dict( + orgs=[], + is_student=False, + ) + mock_get_config.return_value = ["awesome-team", "modest_mice"] + with pytest.raises(PermissionDenied) as exp: + user = self.mixin_instance.get_and_modify_owner(user_dict, self.request) + self.mixin_instance.login_owner(user, self.request, HttpResponse()) + assert exp.status_code == 401 + mock_get_config.assert_called_with("github", "organizations") + + @patch( + "codecov_auth.views.base.LoginMixin._get_or_create_owner", + mock_get_or_create_owner, + ) + @patch( + "codecov_auth.views.base.LoginMixin.get_or_create_org", mock_get_or_create_owner + ) + @patch("services.refresh.RefreshService.trigger_refresh", lambda *args: None) + @patch( + "codecov_auth.views.base.LoginMixin._check_user_count_limitations", + lambda *args: True, + ) + @patch("codecov_auth.views.base.get_config") + @override_settings(IS_ENTERPRISE=True) + def test_get_and_modify_user_enterprise_orgs_passes_if_user_in_org( + self, mock_get_config: Mock + ): + mock_get_config.return_value = ["awesome-team", "modest_mice"] + user_dict = dict( + orgs=[dict(username="awesome-team", id=29)], + is_student=False, + user=dict(id=121), + ) + # This time it should not raise an exception because the user is in one of the orgs + user = self.mixin_instance.get_and_modify_owner(user_dict, self.request) + self.mixin_instance.login_owner(user, self.request, HttpResponse()) + mock_get_config.assert_any_call("github", "organizations") + + @patch( + "codecov_auth.views.base.LoginMixin._get_or_create_owner", + mock_get_or_create_owner, + ) + @patch( + "codecov_auth.views.base.LoginMixin.get_or_create_org", mock_get_or_create_owner + ) + @patch("services.refresh.RefreshService.trigger_refresh", lambda *args: None) + @patch( + "codecov_auth.views.base.LoginMixin._check_user_count_limitations", + lambda *args: True, + ) + @patch("codecov_auth.views.base.get_config") + @override_settings(IS_ENTERPRISE=False) + def test_get_and_modify_user_passes_if_not_enterprise(self, mock_get_config: Mock): + user_dict = dict(orgs=[], is_student=False, user=dict(id=121)) + # This time it should not raise an exception because it's not in enterprise mode + user = self.mixin_instance.get_and_modify_owner(user_dict, self.request) + self.mixin_instance.login_owner(user, self.request, HttpResponse()) + mock_get_config.assert_called_once_with( + "github", "student_disabled", default=False + ) + + @override_settings(IS_ENTERPRISE=False) + @patch("codecov_auth.views.base.get_current_license") + def test_check_user_account_limitations_not_enterprise( + self, mock_get_current_license: Mock + ): + login_data = dict(id=121) + license = LicenseInformation( + is_valid=True, + message=None, + number_allowed_users=2, + ) + mock_get_current_license.return_value = license + self.mixin_instance._check_user_count_limitations(login_data) + mock_get_current_license.assert_not_called() + + def owner_factory_side_effect(self, serivce_id, token): + owner = OwnerFactory(serivce_id=serivce_id, service="github") + owner.oauth_token = token + return owner + + @override_settings(IS_ENTERPRISE=True) + @patch("codecov_auth.models.Owner.objects") + @patch("codecov_auth.views.base.get_current_license") + def test_check_user_account_limitations_enterprise_user_exists_not_pr_billing( + self, mock_get_current_license: Mock, mock_owner_objects: Mock + ): + login_data = dict(id=121) + license = LicenseInformation( + is_valid=True, message=None, number_allowed_users=2, is_pr_billing=False + ) + mock_get_current_license.return_value = license + mock_owner_objects.get.return_value = self.owner_factory_side_effect( + 1200, token="somethingsomething" + ) + self.mixin_instance._check_user_count_limitations(login_data) + mock_get_current_license.assert_called_once() + mock_owner_objects.get.assert_called_once() + + @override_settings(IS_ENTERPRISE=True) + @patch("codecov_auth.views.base.get_current_license") + def test_check_user_account_limitations_enterprise_user_new_not_pr_billing( + self, mock_get_current_license: Mock + ): + login_data = dict(id=121) + license = LicenseInformation( + is_valid=True, message=None, number_allowed_users=1, is_pr_billing=False + ) + mock_get_current_license.return_value = license + # If the number of users is smaller than the limit, no exception is raised + # In this case + self.mixin_instance._check_user_count_limitations(login_data) + mock_get_current_license.assert_called_once() + assert ( + Owner.objects.filter(oauth_token__isnull=False, service="github").count() + == 0 + ) + # If the number of users is larger than the limit, raise error + with pytest.raises(PermissionDenied): + OwnerFactory(service="github", ownerid=12, oauth_token="very-fake-token") + OwnerFactory(service="github", ownerid=13, oauth_token=None) + OwnerFactory(service="github", ownerid=14, oauth_token="very-fake-token") + assert ( + Owner.objects.filter( + oauth_token__isnull=False, service="github" + ).count() + == 2 + ) + self.mixin_instance._check_user_count_limitations(login_data) + mock_get_current_license.assert_called() + + @override_settings(IS_ENTERPRISE=True) + @patch("codecov_auth.views.base.get_current_license") + def test_check_user_account_limitations_enterprise_pr_billing( + self, mock_get_current_license: Mock + ): + license = LicenseInformation( + is_valid=True, message=None, number_allowed_users=1, is_pr_billing=True + ) + mock_get_current_license.return_value = license + # User doesn't exist, and existing users will raise error + with pytest.raises(PermissionDenied): + OwnerFactory(ownerid=1, service="github", plan_activated_users=[1, 2, 3]) + OwnerFactory( + ownerid=2, + service="github", + service_id="batata_frita", + plan_activated_users=[], + ) + OwnerFactory(ownerid=3, service="github", plan_activated_users=None) + assert ( + Owner.objects.exclude(plan_activated_users__len=0) + .exclude(plan_activated_users__isnull=True) + .count() + == 1 + ) + assert Owner.objects.exclude(plan_activated_users__len=0)[ + 0 + ].plan_activated_users == [1, 2, 3] + self.mixin_instance._check_user_count_limitations(dict(id=121)) + mock_get_current_license.assert_called() + # If user exists, don't raise exception + assert ( + Owner.objects.get(service="github", service_id="batata_frita").ownerid == 2 + ) + self.mixin_instance._check_user_count_limitations(dict(id="batata_frita")) + + @override_settings(IS_ENTERPRISE=True) + @patch("services.refresh.RefreshService.trigger_refresh", lambda *args: None) + @patch( + "codecov_auth.views.base.LoginMixin._check_user_count_limitations", + lambda *args: True, + ) + @patch( + "codecov_auth.views.base.LoginMixin._get_or_create_owner", + mock_get_or_create_owner, + ) + @patch("codecov_auth.views.base.get_config") + def test_github_teams_restrictions(self, mock_get_config: Mock): + def side_effect(*args, **kwargs): + if len(args) == 2 and args[0] == "github" and args[1] == "organizations": + return ["my-org"] + if len(args) == 2 and args[0] == "github" and args[1] == "teams": + return ["My Team"] + if len(args) == 2 and args[0] == "github" and args[1] == "student_disabled": + return False + + mock_get_config.side_effect = side_effect + user_dict = dict( + orgs=[dict(username="my-org", id=29)], + is_student=False, + user=dict(id=121, login="something"), + teams=[], + ) + # Raise exception because user is not member of My Team + with pytest.raises(PermissionDenied) as exp: + user = self.mixin_instance.get_and_modify_owner(user_dict, self.request) + self.mixin_instance.login_owner(user, self.request, HttpResponse()) + mock_get_config.assert_any_call("github", "organizations") + mock_get_config.assert_any_call("github", "teams") + assert ( + str(exp) + == "You must be a member of an allowed team in your organization." + ) + assert exp.status_code == 401 + # No exception if user is in My Team + user_dict["teams"] = [dict(name="My Team")] + user = self.mixin_instance.get_and_modify_owner(user_dict, self.request) + self.mixin_instance.login_owner(user, self.request, HttpResponse()) + mock_get_config.assert_any_call("github", "organizations") + mock_get_config.assert_any_call("github", "teams") + mock_get_config.assert_any_call("github", "student_disabled", default=False) + + @override_settings(IS_ENTERPRISE=True) + @patch("services.refresh.RefreshService.trigger_refresh", lambda *args: None) + @patch( + "codecov_auth.views.base.LoginMixin._check_user_count_limitations", + lambda *args: True, + ) + @patch( + "codecov_auth.views.base.LoginMixin._get_or_create_owner", + mock_get_or_create_owner, + ) + @patch("codecov_auth.views.base.get_config") + def test_github_teams_restrictions_no_teams_in_config(self, mock_get_config: Mock): + def side_effect(*args, **kwargs): + if len(args) == 2 and args[0] == "github" and args[1] == "organizations": + return ["my-org"] + if len(args) == 2 and args[0] == "github" and args[1] == "teams": + return [] + if len(args) == 2 and args[0] == "github" and args[1] == "student_disabled": + return False + + mock_get_config.side_effect = side_effect + user_dict = dict( + orgs=[dict(username="my-org", id=29)], + is_student=False, + user=dict(id=121, login="something"), + teams=[dict(name="My Team")], + ) + # Don't raise exception if there's no team in the config + user = self.mixin_instance.get_and_modify_owner(user_dict, self.request) + self.mixin_instance.login_owner(user, self.request, HttpResponse()) + mock_get_config.assert_any_call("github", "organizations") + mock_get_config.assert_any_call("github", "teams") + mock_get_config.assert_any_call("github", "student_disabled", default=False) + + def test_adjust_redirection_url_is_unchanged_if_url_is_different_from_base_url( + self, + ): + provider = "gh" + owner = OwnerFactory( + username="sample-owner", + service="github", + ) + url = f"{settings.CODECOV_DASHBOARD_URL}/{provider}/some/random/path/to/file.py" + + redirect_url = ( + self.mixin_instance.modify_redirection_url_based_on_default_user_org( + url, owner + ) + ) + assert redirect_url == url + + def test_adjust_redirection_url_is_unchanged_if_no_owner_profile(self): + provider = "gh" + owner = OwnerFactory( + username="sample-owner", + service="github", + ) + url = f"{settings.CODECOV_DASHBOARD_URL}/{provider}" + + redirect_url = ( + self.mixin_instance.modify_redirection_url_based_on_default_user_org( + url, owner + ) + ) + assert redirect_url == url + + def test_adjust_redirection_url_is_unchanged_if_no_default_org(self): + provider = "gh" + owner = OwnerFactory( + username="sample-owner-gh", + service="github", + ) + # OwnerProfile implicitly has no default org + url = f"{settings.CODECOV_DASHBOARD_URL}/{provider}" + + redirect_url = ( + self.mixin_instance.modify_redirection_url_based_on_default_user_org( + url, owner + ) + ) + assert redirect_url == url + + def test_adjust_redirection_url_user_has_a_default_org_for_github(self): + provider = "gh" + default_org_username = "sample-org-gh" + organization = OwnerFactory(username=default_org_username, service="github") + owner = OwnerFactory( + username="sample-owner-gh", + service="github", + organizations=[organization.ownerid], + ) + OwnerProfile.objects.filter(owner_id=owner.ownerid).update( + default_org=organization + ) + url = f"{settings.CODECOV_DASHBOARD_URL}/{provider}" + + redirect_url = ( + self.mixin_instance.modify_redirection_url_based_on_default_user_org( + url, owner + ) + ) + assert redirect_url == url + f"/{default_org_username}" + + def test_adjust_redirection_url_user_has_a_default_org_for_gitlab(self): + provider = "gl" + default_org_username = "sample-org-gl" + organization = OwnerFactory(username=default_org_username, service="gitlab") + owner = OwnerFactory( + username="sample-owner-gl", + service="gitlab", + organizations=[organization.ownerid], + ) + OwnerProfile.objects.filter(owner_id=owner.ownerid).update( + default_org=organization + ) + url = f"{settings.CODECOV_DASHBOARD_URL}/{provider}" + + mixin_instance_gitlab = LoginMixin() + mixin_instance_gitlab.service = "gitlab" + + redirect_url = ( + mixin_instance_gitlab.modify_redirection_url_based_on_default_user_org( + url, owner + ) + ) + assert redirect_url == url + f"/{default_org_username}" + + def test_adjust_redirection_url_user_has_a_default_org_for_bitbucket(self): + provider = "bb" + default_org_username = "sample-org-bb" + organization = OwnerFactory(username=default_org_username, service="bitbucket") + owner = OwnerFactory( + username="sample-owner-bb", + service="bitbucket", + organizations=[organization.ownerid], + ) + OwnerProfile.objects.filter(owner_id=owner.ownerid).update( + default_org=organization + ) + url = f"{settings.CODECOV_DASHBOARD_URL}/{provider}" + + mixin_instance_bitbucket = LoginMixin() + mixin_instance_bitbucket.service = "bitbucket" + + redirect_url = ( + mixin_instance_bitbucket.modify_redirection_url_based_on_default_user_org( + url, owner + ) + ) + assert redirect_url == url + f"/{default_org_username}" + + def test_adjust_redirection_url_user_has_a_default_org_for_github_long_org_name( + self, + ): + provider = "github" + default_org_username = "sample-org-gh" + organization = OwnerFactory(username=default_org_username, service="github") + owner = OwnerFactory( + username="sample-owner-gh", + service="github", + organizations=[organization.ownerid], + ) + # OwnerProfiles get created automatically, so we need to fetch and update the entry manually + OwnerProfile.objects.filter(owner_id=owner.ownerid).update( + default_org=organization + ) + + url = f"{settings.CODECOV_DASHBOARD_URL}/{provider}" + + redirect_url = ( + self.mixin_instance.modify_redirection_url_based_on_default_user_org( + url, owner + ) + ) + assert redirect_url == url + f"/{default_org_username}" + + @patch("services.refresh.RefreshService.trigger_refresh", lambda *args: None) + def test_login_unauthenticated_with_claimed_owner(self): + self.request.user = None + owner = OwnerFactory() + self.mixin_instance.login_owner(owner, self.request, HttpResponse()) + assert self.request.user == owner.user + + @patch("services.refresh.RefreshService.trigger_refresh", lambda *args: None) + def test_login_unauthenticated_with_unclaimed_owner(self): + self.request.user = None + owner = OwnerFactory(user=None) + self.mixin_instance.login_owner(owner, self.request, HttpResponse()) + # creates new user + assert self.request.user == owner.user + assert self.request.user.email == owner.email + assert self.request.user.name == owner.name + + @patch("services.refresh.RefreshService.trigger_refresh", lambda *args: None) + def test_login_authenticated_with_unclaimed_owner(self): + user = UserFactory() + owner = OwnerFactory(user=None) + self.request.user = user + self.mixin_instance.login_owner(owner, self.request, HttpResponse()) + owner.refresh_from_db() + assert owner.user == user + + @patch("services.refresh.RefreshService.trigger_refresh", lambda *args: None) + def test_login_authenticated_with_existing_service_owner(self): + user = UserFactory() + OwnerFactory(service="github", user=user) + owner = OwnerFactory(user=None, service="github") + self.request.user = user + self.mixin_instance.login_owner(owner, self.request, HttpResponse()) + owner.refresh_from_db() + + # logs in new user + assert self.request.user is not None + assert self.request.user != user + + # and claims owner w/ that new user + assert owner.user is not None + assert owner.user != user + + @patch("services.refresh.RefreshService.trigger_refresh", lambda *args: None) + def test_login_authenticated_with_claimed_owner(self): + user = UserFactory() + owner = OwnerFactory(service="github") + self.request.user = user + self.mixin_instance.login_owner(owner, self.request, HttpResponse()) + owner.refresh_from_db() + + assert self.request.user == owner.user + + # does not re-claim owner + assert owner.user is not None + assert owner.user != user + + @patch("services.refresh.RefreshService.trigger_refresh", lambda *args: None) + def test_login_owner_with_expired_login_session(self): + user = UserFactory() + owner = OwnerFactory(service="github", user=user) + + another_user = UserFactory() + another_owner = OwnerFactory(service="github", user=another_user) + + now = datetime.now(timezone.utc) + + # Create a session that will be deleted + to_be_deleted_1 = SessionFactory( + owner=owner, + type="login", + name="to_be_deleted", + lastseen="2021-01-01T00:00:00+00:00", + login_session=DjangoSessionFactory(expire_date=now - timedelta(days=1)), + ) + to_be_deleted_1_session_key = to_be_deleted_1.login_session.session_key + + # Create a session that will not be deleted because its not a login session + to_be_kept_1 = SessionFactory( + owner=owner, + type="api", + name="to_be_kept", + lastseen="2021-01-01T00:00:00+00:00", + login_session=DjangoSessionFactory(expire_date=now + timedelta(days=1)), + ) + + # Create a session that will not be deleted because it's not expired + to_be_kept_2 = SessionFactory( + owner=owner, + type="login", + name="to_be_kept", + lastseen="2021-01-01T00:00:00+00:00", + login_session=DjangoSessionFactory(expire_date=now + timedelta(days=1)), + ) + + # Create a session that will not be deleted because it's not the owner's session + to_be_kept_3 = SessionFactory( + owner=another_owner, + type="login", + name="to_be_kept", + lastseen="2021-01-01T00:00:00+00:00", + login_session=DjangoSessionFactory(expire_date=now - timedelta(seconds=1)), + ) + + assert ( + len(DjangoSession.objects.filter(session_key=to_be_deleted_1_session_key)) + == 1 + ) + assert ( + len( + DjangoSession.objects.filter( + session_key=to_be_kept_1.login_session.session_key + ) + ) + == 1 + ) + assert ( + len( + DjangoSession.objects.filter( + session_key=to_be_kept_2.login_session.session_key + ) + ) + == 1 + ) + assert ( + len( + DjangoSession.objects.filter( + session_key=to_be_kept_3.login_session.session_key + ) + ) + == 1 + ) + + self.request.user = user + self.mixin_instance.login_owner(owner, self.request, HttpResponse()) + owner.refresh_from_db() + + new_login_session = Session.objects.filter(name=None) + + assert len(new_login_session) == 1 + assert len(Session.objects.filter(name="to_be_deleted").all()) == 0 + assert len(Session.objects.filter(name="to_be_kept").all()) == 3 + + assert ( + len(DjangoSession.objects.filter(session_key=to_be_deleted_1_session_key)) + == 0 + ) + assert ( + len( + DjangoSession.objects.filter( + session_key=to_be_kept_1.login_session.session_key + ) + ) + == 1 + ) + assert ( + len( + DjangoSession.objects.filter( + session_key=to_be_kept_2.login_session.session_key + ) + ) + == 1 + ) + assert ( + len( + DjangoSession.objects.filter( + session_key=to_be_kept_3.login_session.session_key + ) + ) + == 1 + ) diff --git a/apps/codecov-api/codecov_auth/tests/unit/views/test_bitbucket.py b/apps/codecov-api/codecov_auth/tests/unit/views/test_bitbucket.py new file mode 100644 index 0000000000..a854c86cf0 --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/views/test_bitbucket.py @@ -0,0 +1,217 @@ +from unittest.mock import call, patch + +from django.core import signing +from django.http.cookie import SimpleCookie +from django.test import TestCase +from django.urls import reverse +from shared.torngit.bitbucket import Bitbucket +from shared.torngit.exceptions import TorngitServer5xxCodeError + +from codecov_auth.models import Owner +from codecov_auth.views.bitbucket import BitbucketLoginView +from utils.encryption import encryptor + + +def test_get_bitbucket_redirect(client, settings, mocker): + mocked_get = mocker.patch.object( + Bitbucket, + "generate_request_token", + return_value={ + "oauth_token": "testy6r2of6ajkmrub", + "oauth_token_secret": "testzibw5q01scpl8qeeupzh8u9yu8hz", + }, + ) + settings.BITBUCKET_REDIRECT_URI = "http://localhost" + settings.BITBUCKET_CLIENT_ID = "testqmo19ebdkseoby" + settings.BITBUCKET_CLIENT_SECRET = "testfi8hzehvz453qj8mhv21ca4rf83f" + url = reverse("bitbucket-login") + res = client.get(url, SERVER_NAME="localhost:8000") + assert res.status_code == 302 + + assert "_oauth_request_token" in res.cookies + cookie = res.cookies["_oauth_request_token"] + assert cookie.value + assert cookie.get("domain") == settings.COOKIES_DOMAIN + assert ( + res.url + == "https://bitbucket.org/api/1.0/oauth/authenticate?oauth_token=testy6r2of6ajkmrub" + ) + mocked_get.assert_called_with(settings.BITBUCKET_REDIRECT_URI) + + +def test_get_bitbucket_redirect_bitbucket_unavailable(client, settings, mocker): + mocked_get = mocker.patch.object( + Bitbucket, "generate_request_token", side_effect=TorngitServer5xxCodeError() + ) + settings.BITBUCKET_REDIRECT_URI = "http://localhost" + settings.BITBUCKET_CLIENT_ID = "testqmo19ebdkseoby" + settings.BITBUCKET_CLIENT_SECRET = "testfi8hzehvz453qj8mhv21ca4rf83f" + url = reverse("bitbucket-login") + res = client.get(url, SERVER_NAME="localhost:8000") + assert res.status_code == 302 + assert "_oauth_request_token" not in res.cookies + assert res.url == url + mocked_get.assert_called_with(settings.BITBUCKET_REDIRECT_URI) + + +async def fake_get_authenticated_user(): + return { + "username": "ThiagoCodecov", + "has_2fa_enabled": None, + "display_name": "Thiago Ramos", + "account_id": "5bce04c759d0e84f8c7555e9", + "links": { + "hooks": { + "href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/hooks" + }, + "self": { + "href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D" + }, + "repositories": { + "href": "https://bitbucket.org/!api/2.0/repositories/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D" + }, + "html": { + "href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/" + }, + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png" + }, + "snippets": { + "href": "https://bitbucket.org/!api/2.0/snippets/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D" + }, + }, + "nickname": "thiago", + "created_on": "2018-11-06T12:12:59.588751+00:00", + "is_staff": False, + "location": None, + "account_status": "active", + "type": "user", + "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + } + + +def test_get_bitbucket_already_token(client, settings, mocker, db, mock_redis): + mocker.patch.object( + Bitbucket, "get_authenticated_user", side_effect=fake_get_authenticated_user + ) + + async def fake_list_teams(): + return [] + + mocker.patch.object(Bitbucket, "list_teams", side_effect=fake_list_teams) + mocker.patch( + "services.task.TaskService.refresh", + return_value=mocker.MagicMock( + as_tuple=mocker.MagicMock(return_value=("a", "b")) + ), + ) + mocked_get = mocker.patch.object( + Bitbucket, + "generate_access_token", + return_value={ + "key": "test6tl3evq7c8vuyn", + "secret": "testdm61tppb5x0tam7nae3qajhcepzz", + }, + ) + settings.BITBUCKET_REDIRECT_URI = "http://localhost" + settings.BITBUCKET_CLIENT_ID = "testqmo19ebdkseoby" + settings.BITBUCKET_CLIENT_SECRET = "testfi8hzehvz453qj8mhv21ca4rf83f" + settings.CODECOV_DASHBOARD_URL = "dashboard.value" + settings.COOKIE_SECRET = "aaaaa" + url = reverse("bitbucket-login") + oauth_request_token = ( + "dGVzdDZ0bDNldnE3Yzh2dXlu|dGVzdGRtNjF0cHBiNXgwdGFtN25hZTNxYWpoY2Vweno=" + ) + client.cookies = SimpleCookie( + { + "_oauth_request_token": signing.get_cookie_signer( + salt="_oauth_request_token" + ).sign(encryptor.encode(oauth_request_token).decode()) + } + ) + mock_create_user_onboarding_metric = mocker.patch( + "shared.django_apps.codecov_metrics.service.codecov_metrics.UserOnboardingMetricsService.create_user_onboarding_metric" + ) + + res = client.get( + url, + {"oauth_verifier": 8519288973, "oauth_token": "test1daxl4jnhegoh4"}, + SERVER_NAME="localhost:8000", + ) + assert res.status_code == 302 + assert res.url == "dashboard.value/bb" + assert "_oauth_request_token" in res.cookies + cookie = res.cookies["_oauth_request_token"] + assert cookie.value == "" + assert cookie.get("domain") == settings.COOKIES_DOMAIN + mocked_get.assert_called_with( + "test6tl3evq7c8vuyn", "testdm61tppb5x0tam7nae3qajhcepzz", "8519288973" + ) + owner = Owner.objects.get(username="ThiagoCodecov", service="bitbucket") + expected_call = call( + org_id=owner.ownerid, + event="INSTALLED_APP", + payload={"login": "bitbucket"}, + ) + assert mock_create_user_onboarding_metric.call_args_list == [expected_call] + + assert ( + encryptor.decode(owner.oauth_token) + == "test6tl3evq7c8vuyn:testdm61tppb5x0tam7nae3qajhcepzz" + ) + + +def test_get_bitbucket_already_token_no_cookie( + client, settings, mocker, db, mock_redis +): + mocker.patch( + "services.task.TaskService.refresh", + return_value=mocker.MagicMock( + as_tuple=mocker.MagicMock(return_value=("a", "b")) + ), + ) + mocked_get = mocker.patch.object( + Bitbucket, + "generate_access_token", + return_value={ + "key": "test6tl3evq7c8vuyn", + "secret": "testdm61tppb5x0tam7nae3qajhcepzz", + }, + ) + settings.BITBUCKET_REDIRECT_URI = "http://localhost" + settings.BITBUCKET_CLIENT_ID = "testqmo19ebdkseoby" + settings.BITBUCKET_CLIENT_SECRET = "testfi8hzehvz453qj8mhv21ca4rf83f" + url = reverse("bitbucket-login") + res = client.get( + url, + {"oauth_verifier": 8519288973, "oauth_token": "test1daxl4jnhegoh4"}, + SERVER_NAME="localhost:8000", + ) + assert res.status_code == 302 + assert res.url == "/login/bitbucket" + assert not mocked_get.called + + +class TestBitbucketLoginView(TestCase): + def test_fetch_user_data(self): + async def fake_list_teams(): + return [] + + with patch.object( + Bitbucket, "get_authenticated_user", side_effect=fake_get_authenticated_user + ): + with patch.object(Bitbucket, "list_teams", side_effect=fake_list_teams): + view = BitbucketLoginView() + token = {"key": "aaaa", "secret": "bbbb"} + res = view.fetch_user_data(token) + assert res == { + "has_private_access": True, + "is_student": False, + "orgs": [], + "user": { + "key": "aaaa", + "secret": "bbbb", + "id": "9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645", + "login": "ThiagoCodecov", + }, + } diff --git a/apps/codecov-api/codecov_auth/tests/unit/views/test_bitbucket_server.py b/apps/codecov-api/codecov_auth/tests/unit/views/test_bitbucket_server.py new file mode 100644 index 0000000000..e7eb7cc982 --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/views/test_bitbucket_server.py @@ -0,0 +1,115 @@ +import pytest +from django.core import signing +from django.http.cookie import SimpleCookie +from django.urls import reverse +from shared.torngit.exceptions import TorngitClientGeneralError + +from codecov_auth.models import Owner +from codecov_auth.views.bitbucket_server import ( + BitbucketServer, +) +from utils.encryption import encryptor + + +def test_get_bbs_redirect(client, settings, mocker): + client_request_mock = mocker.patch.object( + BitbucketServer, + "api", + side_effect=lambda *args, **kwargs: dict( + oauth_token="SomeToken", oauth_token_secret="SomeTokenSecret" + ), + ) + settings.BITBUCKET_SERVER_CLIENT_ID = "this-is-the-important-bit" + settings.BITBUCKET_SERVER_URL = "https://my.bitbucketserver.com" + url = reverse("bbs-login") + res = client.get(url, SERVER_NAME="localhost:8000") + + assert res.status_code == 302 + assert ( + res.url + == "https://my.bitbucketserver.com/plugins/servlet/oauth/authorize?oauth_token=SomeToken" + ) + client_request_mock.assert_called_with( + "POST", f"{settings.BITBUCKET_SERVER_URL}/plugins/servlet/oauth/request-token" + ) + + +def test_get_bbs_redirect_bitbucket_fails_to_get_request_token( + client, settings, mocker +): + def faulty_response(*args, **kwargs): + # This is the error class that BitbucketServer.api generates + raise TorngitClientGeneralError(500, "data data", "BBS unavailable") + + mocker.patch.object( + BitbucketServer, + "api", + side_effect=faulty_response, + ) + settings.BITBUCKET_REDIRECT_URI = "http://localhost" + settings.CODECOV_DASHBOARD_URL = "dashboard.value" + settings.BITBUCKET_CLIENT_ID = "testqmo19ebdkseoby" + settings.BITBUCKET_CLIENT_SECRET = "testfi8hzehvz453qj8mhv21ca4rf83f" + with pytest.raises(TorngitClientGeneralError): + client.get(reverse("bbs-login"), SERVER_NAME="localhost:8000") + + +def test_get_bbs_already_token(client, settings, mocker, db, mock_redis): + settings.BITBUCKET_SERVER_CLIENT_ID = "this-is-the-important-bit" + settings.BITBUCKET_SERVER_URL = "https://my.bitbucketserver.com" + settings.BITBUCKET_SERVER_REDIRECT_URI = "http://localhost" + settings.CODECOV_DASHBOARD_URL = "dashboard.value" + settings.COOKIE_SECRET = "aaaaa" + + async def fake_list_teams(): + return [] + + async def fake_api(method, url): + if method == "POST" and ( + url.endswith("/plugins/servlet/oauth/access-token") + or url.endswith("/plugins/servlet/oauth/request-token") + ): + return dict(oauth_token="SomeToken", oauth_token_secret="SomeTokenSecret") + elif method == "GET" and url.endswith("/plugins/servlet/applinks/whoami"): + return "ThiagoCodecov" + elif method == "GET" and ("/users/ThiagoCodecov" in url): + return dict( + name="ThiagoCodecov", id=101, displayName="Thiago Codecov", active=True + ) + + mocker.patch.object(BitbucketServer, "list_teams", side_effect=fake_list_teams) + client_request_mock = mocker.patch.object( + BitbucketServer, "api", side_effect=fake_api + ) + mocker.patch( + "services.task.TaskService.refresh", + return_value=mocker.MagicMock( + as_tuple=mocker.MagicMock(return_value=("a", "b")) + ), + ) + + url = reverse("bbs-login") + oauth_request_token = ( + "dGVzdDZ0bDNldnE3Yzh2dXlu|dGVzdGRtNjF0cHBiNXgwdGFtN25hZTNxYWpoY2Vweno=" + ) + client.cookies = SimpleCookie( + { + "_oauth_request_token": signing.get_cookie_signer( + salt="_oauth_request_token" + ).sign(encryptor.encode(oauth_request_token).decode()) + } + ) + res = client.get( + url, + {"oauth_verifier": 8519288973, "oauth_token": "test1daxl4jnhegoh4"}, + SERVER_NAME="localhost:8000", + ) + client_request_mock.assert_called() + assert res.status_code == 302 + assert res.url == "dashboard.value/bbs" + assert "_oauth_request_token" in res.cookies + cookie = res.cookies["_oauth_request_token"] + assert cookie.value == "" + assert cookie.get("domain") == settings.COOKIES_DOMAIN + owner = Owner.objects.get(username="ThiagoCodecov", service="bitbucket_server") + assert encryptor.decode(owner.oauth_token) == "SomeToken:SomeTokenSecret" diff --git a/apps/codecov-api/codecov_auth/tests/unit/views/test_github.py b/apps/codecov-api/codecov_auth/tests/unit/views/test_github.py new file mode 100644 index 0000000000..958dc3729c --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/views/test_github.py @@ -0,0 +1,505 @@ +import re +from datetime import datetime +from unittest.mock import call + +import pytest +from django.http.cookie import SimpleCookie +from django.test import override_settings +from django.urls import reverse +from django.utils import timezone +from freezegun import freeze_time +from shared.config import ConfigHelper +from shared.django_apps.core.tests.factories import OwnerFactory +from shared.plan.constants import DEFAULT_FREE_PLAN +from shared.torngit import Github +from shared.torngit.exceptions import TorngitClientGeneralError + +from codecov_auth.models import Owner +from codecov_auth.views.github import GithubLoginView + + +def _get_state_from_redis(mock_redis): + key_redis = mock_redis.keys("*")[0].decode() + return key_redis.replace("oauth-state-", "") + + +@override_settings(GITHUB_CLIENT_ID="testclientid") +@pytest.mark.django_db +def test_get_github_redirect(client, mocker, mock_redis, settings): + settings.IS_ENTERPRISE = False + + url = reverse("github-login") + res = client.get(url) + state = _get_state_from_redis(mock_redis) + assert res.status_code == 302 + assert ( + res.url + == f"https://github.com/login/oauth/authorize?response_type=code&scope=user%3Aemail%2Cread%3Aorg%2Crepo%3Astatus%2Cwrite%3Arepo_hook&client_id=testclientid&state={state}" + ) + + +@override_settings(GITHUB_CLIENT_ID="testclientid") +@pytest.mark.django_db +def test_get_github_redirect_host_override(client, mocker, mock_redis, settings): + settings.IS_ENTERPRISE = False + config = ConfigHelper() + gh_data = {"github": {"url": "https://fake.com", "host_override": "extrafake.com"}} + + def fake_config(*path, default=None): + curr = config + for key in path: + if key in gh_data: + curr = gh_data.get(key) + elif key in curr: + curr = curr.get(key) + else: + return default + return curr + + mocker.patch( + "shared.torngit.github.get_config", + side_effect=fake_config, + ) + url = reverse("github-login") + res = client.get(url) + state = _get_state_from_redis(mock_redis) + assert res.status_code == 302 + assert ( + res.url + == f"https://extrafake.com/login/oauth/authorize?response_type=code&scope=user%3Aemail%2Cread%3Aorg%2Crepo%3Astatus%2Cwrite%3Arepo_hook&client_id=testclientid&state={state}" + ) + + +@override_settings(GITHUB_CLIENT_ID="testclientid") +@pytest.mark.django_db +def test_get_github_redirect_with_ghpr_cookie(client, mocker, mock_redis, settings): + settings.COOKIES_DOMAIN = ".simple.site" + settings.COOKIE_SECRET = "secret" + client.cookies = SimpleCookie({"ghpr": "true"}) + + url = reverse("github-login") + res = client.get(url) + state = _get_state_from_redis(mock_redis) + assert res.status_code == 302 + assert ( + res.url + == f"https://github.com/login/oauth/authorize?response_type=code&scope=user%3Aemail%2Cread%3Aorg%2Crepo%3Astatus%2Cwrite%3Arepo_hook%2Crepo&client_id=testclientid&state={state}" + ) + assert "ghpr" in res.cookies + ghpr_cooke = res.cookies["ghpr"] + assert ghpr_cooke.value == "true" + assert ghpr_cooke.get("domain") == ".simple.site" + + +@override_settings(GITHUB_CLIENT_ID="testclientid") +@pytest.mark.django_db +def test_get_github_redirect_with_private_url(client, mocker, mock_redis, settings): + settings.COOKIES_DOMAIN = ".simple.site" + settings.COOKIE_SECRET = "secret" + + url = reverse("github-login") + res = client.get(url, {"private": "true"}) + state = _get_state_from_redis(mock_redis) + assert res.status_code == 302 + assert ( + res.url + == f"https://github.com/login/oauth/authorize?response_type=code&scope=user%3Aemail%2Cread%3Aorg%2Crepo%3Astatus%2Cwrite%3Arepo_hook%2Crepo&client_id=testclientid&state={state}" + ) + assert "ghpr" in res.cookies + ghpr_cooke = res.cookies["ghpr"] + assert ghpr_cooke.value == "true" + assert ghpr_cooke.get("domain") == ".simple.site" + + +def test_get_github_already_with_code(client, mocker, db, mock_redis, settings): + settings.COOKIES_DOMAIN = ".simple.site" + settings.COOKIE_SECRET = "secret" + now = datetime.now() + now_tz = timezone.now() + + async def helper_func(*args, **kwargs): + return { + "login": "ThiagoCodecov", + "id": 44376991, + "node_id": "MDQ6VXNlcjQ0Mzc2OTkx", + "avatar_url": "https://avatars3.githubusercontent.com/u/44376991?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ThiagoCodecov", + "html_url": "https://github.com/ThiagoCodecov", + "followers_url": "https://api.github.com/users/ThiagoCodecov/followers", + "following_url": "https://api.github.com/users/ThiagoCodecov/following{/other_user}", + "gists_url": "https://api.github.com/users/ThiagoCodecov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ThiagoCodecov/subscriptions", + "organizations_url": "https://api.github.com/users/ThiagoCodecov/orgs", + "repos_url": "https://api.github.com/users/ThiagoCodecov/repos", + "events_url": "https://api.github.com/users/ThiagoCodecov/events{/privacy}", + "received_events_url": "https://api.github.com/users/ThiagoCodecov/received_events", + "type": "User", + "site_admin": False, + "name": "Thiago", + "company": "@codecov ", + "blog": "", + "location": None, + "email": None, + "hireable": None, + "bio": None, + "twitter_username": None, + "public_repos": 3, + "public_gists": 0, + "followers": 0, + "following": 0, + "created_at": "2018-10-22T17:51:44Z", + "updated_at": "2020-10-14T17:58:13Z", + "access_token": "test3k5zz19xqwhgr3eitwcm0lis74s9o0dlovnr", + "token_type": "bearer", + "scope": "read:org,repo:status,user:email,write:repo_hook,repo", + } + + async def helper_list_teams_func(*args, **kwargs): + return [ + { + "email": "hello@codecov.io", + "id": "8226205", + "name": "Codecov", + "username": "codecov", + } + ] + + async def is_student(*args, **kwargs): + return False + + mocker.patch.object(Github, "get_authenticated_user", side_effect=helper_func) + mocker.patch.object(Github, "list_teams", side_effect=helper_list_teams_func) + mocker.patch.object(Github, "is_student", side_effect=is_student) + mocker.patch( + "services.task.TaskService.refresh", + return_value=mocker.MagicMock( + as_tuple=mocker.MagicMock(return_value=("a", "b")) + ), + ) + + session = client.session + session["github_oauth_state"] = "abc" + session.save() + + url = reverse("github-login") + mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/gh") + res = client.get(url, {"code": "aaaaaaa", "state": "abc"}) + assert res.status_code == 302 + + owner = Owner.objects.get(pk=client.session["current_owner_id"]) + assert owner.username == "ThiagoCodecov" + assert owner.service_id == "44376991" + assert owner.email is None + assert owner.private_access is True + assert owner.service == "github" + assert owner.name == "Thiago" + assert owner.oauth_token is not None # cannot test exact value + assert owner.stripe_customer_id is None + assert owner.stripe_subscription_id is None + assert owner.createstamp > now_tz + assert owner.service_id == "44376991" + assert owner.parent_service_id is None + assert owner.root_parent_service_id is None + assert not owner.staff + assert owner.cache is None + assert owner.plan == DEFAULT_FREE_PLAN + assert owner.plan_provider is None + assert owner.plan_user_count == 1 + assert owner.plan_auto_activate is True + assert owner.plan_activated_users is None + assert owner.did_trial is None + assert owner.free == 0 + assert owner.invoice_details is None + assert owner.delinquent is None + assert owner.yaml is None + assert owner.updatestamp > now + assert owner.admins is None + assert owner.integration_id is None + assert owner.permission is None + assert owner.bot is None + assert owner.student is False + assert owner.student_created_at is None + assert owner.student_updated_at is None + # testing orgs + assert owner.organizations is not None + assert len(owner.organizations) == 1 + org = Owner.objects.get(ownerid=owner.organizations[0]) + assert org.service_id == "8226205" + assert org.service == "github" + assert res.url == "http://localhost:3000/gh" + + +def test_get_github_already_with_code_github_error( + client, mocker, db, mock_redis, settings +): + settings.COOKIES_DOMAIN = ".simple.site" + settings.COOKIE_SECRET = "secret" + + async def helper_func(*args, **kwargs): + raise TorngitClientGeneralError(403, "response", "message") + + session = client.session + session["github_oauth_state"] = "abc" + session.save() + mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/gh") + + mocker.patch.object(Github, "get_authenticated_user", side_effect=helper_func) + url = reverse("github-login") + res = client.get(url, {"code": "aaaaaaa", "state": "abc"}) + assert res.status_code == 302 + + assert "current_owner_id" not in client.session + assert res.url == "/" + + +def test_state_not_known(client, mocker, db, mock_redis, settings): + url = reverse("github-login") + res = client.get(url, {"code": "aaaaaaa", "state": "doesnt exist"}) + assert res.status_code == 302 + assert "current_owner_id" not in client.session + + +@freeze_time("2023-02-01T00:00:00") +def test_get_github_already_with_code_with_email( + client, mocker, db, mock_redis, settings +): + settings.COOKIES_DOMAIN = ".simple.site" + settings.COOKIE_SECRET = "secret" + + async def helper_func(*args, **kwargs): + return { + "email": "thiago@codecov.io", + "login": "ThiagoCodecov", + "id": 44376991, + "access_token": "testh04ph89fx0nkd3diauxcw75fyiuo3b86fw4j", + "scope": "read:org,repo:status,user:email,write:repo_hook,repo", + } + + async def helper_list_teams_func(*args, **kwargs): + return [ + { + "email": "hello@codecov.io", + "id": "8226205", + "name": "Codecov", + "username": "codecov", + } + ] + + async def is_student(*args, **kwargs): + return True + + mocker.patch.object(Github, "get_authenticated_user", side_effect=helper_func) + mocker.patch.object(Github, "list_teams", side_effect=helper_list_teams_func) + mocker.patch.object(Github, "is_student", side_effect=is_student) + mocker.patch( + "services.task.TaskService.refresh", + return_value=mocker.MagicMock( + as_tuple=mocker.MagicMock(return_value=("a", "b")) + ), + ) + + session = client.session + session["github_oauth_state"] = "abc" + session.save() + mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/gh") + url = reverse("github-login") + res = client.get(url, {"code": "aaaaaaa", "state": "abc"}) + assert res.status_code == 302 + + owner = Owner.objects.get(pk=client.session["current_owner_id"]) + assert owner.username == "ThiagoCodecov" + assert owner.service_id == "44376991" + assert owner.email == "thiago@codecov.io" + assert owner.private_access is True + assert res.url == "http://localhost:3000/gh" + + +@freeze_time("2023-01-01T00:00:00") +def test_get_github_already_with_code_is_student( + client, mocker, db, mock_redis, settings +): + settings.COOKIES_DOMAIN = ".simple.site" + settings.COOKIE_SECRET = "secret" + + async def helper_func(*args, **kwargs): + return { + "login": "ThiagoCodecov", + "id": 44376991, + "access_token": "testh04ph89fx0nkd3diauxcw75fyiuo3b86fw4j", + "scope": "read:org,repo:status,user:email,write:repo_hook,repo", + } + + async def helper_list_teams_func(*args, **kwargs): + return [ + { + "email": "hello@codecov.io", + "id": "8226205", + "name": "Codecov", + "username": "codecov", + } + ] + + async def is_student(*args, **kwargs): + return True + + mocker.patch.object(Github, "get_authenticated_user", side_effect=helper_func) + mocker.patch.object(Github, "list_teams", side_effect=helper_list_teams_func) + mocker.patch.object(Github, "is_student", side_effect=is_student) + mocker.patch( + "services.task.TaskService.refresh", + return_value=mocker.MagicMock( + as_tuple=mocker.MagicMock(return_value=("a", "b")) + ), + ) + mock_create_user_onboarding_metric = mocker.patch( + "shared.django_apps.codecov_metrics.service.codecov_metrics.UserOnboardingMetricsService.create_user_onboarding_metric" + ) + + session = client.session + session["github_oauth_state"] = "abc" + session.save() + mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/gh") + url = reverse("github-login") + res = client.get(url, {"code": "aaaaaaa", "state": "abc"}) + expected_call = call( + org_id=client.session["current_owner_id"], + event="INSTALLED_APP", + payload={"login": "github"}, + ) + assert mock_create_user_onboarding_metric.call_args_list == [expected_call] + + assert res.status_code == 302 + + owner = Owner.objects.get(pk=client.session["current_owner_id"]) + assert owner.username == "ThiagoCodecov" + assert owner.email is None + assert owner.service_id == "44376991" + assert owner.private_access is True + assert res.url == "http://localhost:3000/gh" + assert owner.student is True + + +@freeze_time("2023-01-01T00:00:00") +def test_get_github_already_owner_already_exist( + client, mocker, db, mock_redis, settings +): + the_bot = OwnerFactory.create(service="github") + the_bot.save() + owner = OwnerFactory.create(bot=the_bot, service="github", service_id="44376991") + owner.save() + old_ownerid = owner.ownerid + assert owner.bot is not None + settings.COOKIES_DOMAIN = ".simple.site" + settings.COOKIE_SECRET = "secret" + + async def helper_func(*args, **kwargs): + return { + "login": "ThiagoCodecov", + "id": 44376991, + "access_token": "testh04ph89fx0nkd3diauxcw75fyiuo3b86fw4j", + "scope": "read:org,repo:status,user:email,write:repo_hook,repo", + } + + async def helper_list_teams_func(*args, **kwargs): + return [ + { + "email": "hello@codecov.io", + "id": "8226205", + "name": "Codecov", + "username": "codecov", + } + ] + + async def is_student(*args, **kwargs): + return True + + mocker.patch.object(Github, "get_authenticated_user", side_effect=helper_func) + mocker.patch.object(Github, "list_teams", side_effect=helper_list_teams_func) + mocker.patch.object(Github, "is_student", side_effect=is_student) + mocker.patch( + "services.task.TaskService.refresh", + return_value=mocker.MagicMock( + as_tuple=mocker.MagicMock(return_value=("a", "b")) + ), + ) + + session = client.session + session["github_oauth_state"] = "abc" + session.save() + + url = reverse("github-login") + mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/gh") + res = client.get(url, {"code": "aaaaaaa", "state": "abc"}) + assert res.status_code == 302 + + owner = Owner.objects.get(pk=client.session["current_owner_id"]) + assert owner.username == "ThiagoCodecov" + assert owner.ownerid == old_ownerid + assert owner.bot is None + assert owner.service_id == "44376991" + assert owner.private_access is True + assert res.url == "http://localhost:3000/gh" + + +@pytest.mark.asyncio +@override_settings(IS_ENTERPRISE=True) +async def test__get_teams_info(client, mocker): + github = GithubLoginView() + repo_service = Github() + + async def helper_api(*args): + url: str = args[2] + if url.startswith("/user/teams"): + match = re.search(r"&page=(\d+)", url) + page_number = match.group(1) + if page_number == "1": + return [dict(name="My team")] + elif page_number == "2": + return [dict(name="My team in another page")] + return [] + return None + + mocker.patch.object(Github, "api", side_effect=helper_api) + result = await github._get_teams_data(repo_service) + assert result == [dict(name="My team"), dict(name="My team in another page")] + + +@pytest.mark.asyncio +@override_settings(IS_ENTERPRISE=True) +async def test__get_teams_info_fails(client, mocker): + github = GithubLoginView() + repo_service = Github() + + async def helper_api(*args): + raise TorngitClientGeneralError( + status_code=500, + response_data=dict(error="generic error"), + message="generic error", + ) + + mocker.patch.object(Github, "api", side_effect=helper_api) + + result = await github._get_teams_data(repo_service) + assert result == [] + + +def test_get_github_missing_access_token(client, mocker, db, mock_redis, settings): + settings.COOKIES_DOMAIN = ".simple.site" + settings.COOKIE_SECRET = "secret" + + async def helper_func(*args, **kwargs): + return { + "id": 44376991, + } + + mocker.patch.object(Github, "get_authenticated_user", side_effect=helper_func) + mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/gh") + session = client.session + session["github_oauth_state"] = "abc" + session.save() + url = reverse("github-login") + res = client.get(url, {"code": "aaaaaaa", "state": "abc"}) + assert res.status_code == 302 + assert res.headers["Location"] == "/" diff --git a/apps/codecov-api/codecov_auth/tests/unit/views/test_github_enterprise.py b/apps/codecov-api/codecov_auth/tests/unit/views/test_github_enterprise.py new file mode 100644 index 0000000000..07b8626877 --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/views/test_github_enterprise.py @@ -0,0 +1,383 @@ +from datetime import datetime + +import pytest +from django.http.cookie import SimpleCookie +from django.urls import reverse +from django.utils import timezone +from shared.django_apps.core.tests.factories import OwnerFactory +from shared.plan.constants import DEFAULT_FREE_PLAN +from shared.torngit import GithubEnterprise +from shared.torngit.exceptions import TorngitClientGeneralError + +from codecov_auth.models import Owner + + +def _get_state_from_redis(mock_redis): + key_redis = mock_redis.keys("*")[0].decode() + return key_redis.replace("oauth-state-", "") + + +@pytest.mark.django_db +def test_get_ghe_redirect(client, mocker, mock_redis, settings): + mock_get_config = mocker.patch( + "shared.torngit.github_enterprise.get_config", + side_effect=lambda *args: "https://my.githubenterprise.com", + ) + settings.IS_ENTERPRISE = True + settings.GITHUB_ENTERPRISE_CLIENT_ID = "3d44be0e772666136a13" + url = reverse("ghe-login") + res = client.get(url) + state = _get_state_from_redis(mock_redis) + assert res.status_code == 302 + assert ( + res.url + == f"https://my.githubenterprise.com/login/oauth/authorize?response_type=code&scope=user%3Aemail%2Cread%3Aorg%2Crepo%3Astatus%2Cwrite%3Arepo_hook%2Crepo&client_id=3d44be0e772666136a13&state={state}" + ) + mock_get_config.assert_called_with("github_enterprise", "url") + + +@pytest.mark.django_db +def test_get_ghe_redirect_with_ghpr_cookie(client, mocker, mock_redis, settings): + mocker.patch( + "shared.torngit.github_enterprise.get_config", + side_effect=lambda *args: "https://my.githubenterprise.com", + ) + settings.GITHUB_ENTERPRISE_CLIENT_ID = "3d44be0e772666136a13" + settings.COOKIES_DOMAIN = ".simple.site" + settings.COOKIE_SECRET = "secret" + client.cookies = SimpleCookie({"ghpr": "true"}) + url = reverse("ghe-login") + res = client.get(url) + state = _get_state_from_redis(mock_redis) + assert res.status_code == 302 + assert ( + res.url + == f"https://my.githubenterprise.com/login/oauth/authorize?response_type=code&scope=user%3Aemail%2Cread%3Aorg%2Crepo%3Astatus%2Cwrite%3Arepo_hook%2Crepo&client_id=3d44be0e772666136a13&state={state}" + ) + assert "ghpr" in res.cookies + ghpr_cooke = res.cookies["ghpr"] + assert ghpr_cooke.value == "true" + assert ghpr_cooke.get("domain") == ".simple.site" + + +@pytest.mark.django_db +def test_get_github_redirect_with_private_url(client, mocker, mock_redis, settings): + mocker.patch( + "shared.torngit.github_enterprise.get_config", + side_effect=lambda *args: "https://my.githubenterprise.com", + ) + settings.GITHUB_ENTERPRISE_CLIENT_ID = "3d44be0e772666136a13" + settings.COOKIES_DOMAIN = ".simple.site" + settings.COOKIE_SECRET = "secret" + url = reverse("ghe-login") + res = client.get(url, {"private": "true"}) + state = _get_state_from_redis(mock_redis) + assert res.status_code == 302 + assert ( + res.url + == f"https://my.githubenterprise.com/login/oauth/authorize?response_type=code&scope=user%3Aemail%2Cread%3Aorg%2Crepo%3Astatus%2Cwrite%3Arepo_hook%2Crepo&client_id=3d44be0e772666136a13&state={state}" + ) + assert "ghpr" in res.cookies + ghpr_cooke = res.cookies["ghpr"] + assert ghpr_cooke.value == "true" + assert ghpr_cooke.get("domain") == ".simple.site" + + +def test_get_ghe_already_with_code(client, mocker, db, mock_redis, settings): + mocker.patch( + "shared.torngit.github_enterprise.get_config", + side_effect=lambda *args: "https://my.githubenterprise.com", + ) + settings.GITHUB_ENTERPRISE_CLIENT_ID = "3d44be0e772666136a13" + settings.COOKIES_DOMAIN = ".simple.site" + settings.COOKIE_SECRET = "secret" + now = datetime.now() + now_tz = timezone.now() + + async def helper_func(*args, **kwargs): + return { + "login": "ThiagoCodecov", + "id": 44376991, + "node_id": "MDQ6VXNlcjQ0Mzc2OTkx", + "avatar_url": "https://avatars3.githubusercontent.com/u/44376991?v=4", + "gravatar_id": "", + "url": "https://api.githubenterprise.com/users/ThiagoCodecov", + "html_url": "https://github.com/ThiagoCodecov", + "followers_url": "https://api.githubenterprise.com/users/ThiagoCodecov/followers", + "following_url": "https://api.githubenterprise.com/users/ThiagoCodecov/following{/other_user}", + "gists_url": "https://api.githubenterprise.com/users/ThiagoCodecov/gists{/gist_id}", + "starred_url": "https://api.githubenterprise.com/users/ThiagoCodecov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.githubenterprise.com/users/ThiagoCodecov/subscriptions", + "organizations_url": "https://api.githubenterprise.com/users/ThiagoCodecov/orgs", + "repos_url": "https://api.githubenterprise.com/users/ThiagoCodecov/repos", + "events_url": "https://api.githubenterprise.com/users/ThiagoCodecov/events{/privacy}", + "received_events_url": "https://api.githubenterprise.com/users/ThiagoCodecov/received_events", + "type": "User", + "site_admin": False, + "name": "Thiago", + "company": "@codecov ", + "blog": "", + "location": None, + "email": None, + "hireable": None, + "bio": None, + "twitter_username": None, + "public_repos": 3, + "public_gists": 0, + "followers": 0, + "following": 0, + "created_at": "2018-10-22T17:51:44Z", + "updated_at": "2020-10-14T17:58:13Z", + "access_token": "test3k5zz19xqwhgr3eitwcm0lis74s9o0dlovnr", + "token_type": "bearer", + "scope": "read:org,repo:status,user:email,write:repo_hook,repo", + } + + async def helper_list_teams_func(*args, **kwargs): + return [ + { + "email": "hello@codecov.io", + "id": "8226205", + "name": "Codecov", + "username": "codecov", + } + ] + + async def is_student(*args, **kwargs): + return False + + mocker.patch.object( + GithubEnterprise, "get_authenticated_user", side_effect=helper_func + ) + mocker.patch.object( + GithubEnterprise, "list_teams", side_effect=helper_list_teams_func + ) + mocker.patch.object(GithubEnterprise, "is_student", side_effect=is_student) + mocker.patch( + "services.task.TaskService.refresh", + return_value=mocker.MagicMock( + as_tuple=mocker.MagicMock(return_value=("a", "b")) + ), + ) + + session = client.session + session["github_enterprise_oauth_state"] = "abc" + session.save() + + url = reverse("ghe-login") + mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/ghe") + res = client.get(url, {"code": "aaaaaaa", "state": "abc"}) + assert res.status_code == 302 + + owner = Owner.objects.get(pk=client.session["current_owner_id"]) + assert owner.username == "ThiagoCodecov" + assert owner.service_id == "44376991" + assert owner.email is None + assert owner.private_access is True + assert owner.service == "github_enterprise" + assert owner.name == "Thiago" + assert owner.oauth_token is not None # cannot test exact value + assert owner.stripe_customer_id is None + assert owner.stripe_subscription_id is None + assert owner.createstamp > now_tz + assert owner.service_id == "44376991" + assert owner.parent_service_id is None + assert owner.root_parent_service_id is None + assert not owner.staff + assert owner.cache is None + assert owner.plan == DEFAULT_FREE_PLAN + assert owner.plan_provider is None + assert owner.plan_user_count == 1 + assert owner.plan_auto_activate is True + assert owner.plan_activated_users is None + assert owner.did_trial is None + assert owner.free == 0 + assert owner.invoice_details is None + assert owner.delinquent is None + assert owner.yaml is None + assert owner.updatestamp > now + assert owner.admins is None + assert owner.integration_id is None + assert owner.permission is None + assert owner.bot is None + assert owner.student is False + assert owner.student_created_at is None + assert owner.student_updated_at is None + # testing orgs + assert owner.organizations is not None + assert len(owner.organizations) == 1 + org = Owner.objects.get(ownerid=owner.organizations[0]) + assert org.service_id == "8226205" + assert org.service == "github_enterprise" + assert res.url == "http://localhost:3000/ghe" + + +def test_get_ghe_already_with_code_github_error( + client, mocker, db, mock_redis, settings +): + mocker.patch( + "shared.torngit.github_enterprise.get_config", + side_effect=lambda *args: "https://my.githubenterprise.com", + ) + settings.GITHUB_ENTERPRISE_CLIENT_ID = "3d44be0e772666136a13" + settings.COOKIES_DOMAIN = ".simple.site" + settings.COOKIE_SECRET = "secret" + + async def helper_func(*args, **kwargs): + raise TorngitClientGeneralError(403, "response", "message") + + mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/ghe") + session = client.session + session["github_enterprise_oauth_state"] = "abc" + session.save() + + mocker.patch.object( + GithubEnterprise, "get_authenticated_user", side_effect=helper_func + ) + url = reverse("ghe-login") + res = client.get(url, {"code": "aaaaaaa", "state": "abc"}) + assert res.status_code == 302 + assert "github_enterprise-token" not in res.cookies + assert "github_enterprise-username" not in res.cookies + assert res.url == "/" + + +def test_state_not_known(client, mocker, db, mock_redis, settings): + mocker.patch( + "shared.torngit.github_enterprise.get_config", + side_effect=lambda *args: "https://my.githubenterprise.com", + ) + settings.GITHUB_ENTERPRISE_CLIENT_ID = "3d44be0e772666136a13" + url = reverse("ghe-login") + res = client.get(url, {"code": "aaaaaaa", "state": "doesnt exist"}) + assert res.status_code == 302 + assert "github_enterprise-token" not in res.cookies + assert "github_enterprise-username" not in res.cookies + + +def test_get_ghe_already_with_code_with_email(client, mocker, db, mock_redis, settings): + mocker.patch( + "shared.torngit.github_enterprise.get_config", + side_effect=lambda *args: "https://my.githubenterprise.com", + ) + settings.GITHUB_ENTERPRISE_CLIENT_ID = "3d44be0e772666136a13" + settings.COOKIES_DOMAIN = ".simple.site" + settings.COOKIE_SECRET = "secret" + + async def helper_func(*args, **kwargs): + return { + "email": "thiago@codecov.io", + "login": "ThiagoCodecov", + "id": 44376991, + "access_token": "testh04ph89fx0nkd3diauxcw75fyiuo3b86fw4j", + "scope": "read:org,repo:status,user:email,write:repo_hook,repo", + } + + async def helper_list_teams_func(*args, **kwargs): + return [ + { + "email": "hello@codecov.io", + "id": "8226205", + "name": "Codecov", + "username": "codecov", + } + ] + + async def is_student(*args, **kwargs): + return False + + mocker.patch.object( + GithubEnterprise, "get_authenticated_user", side_effect=helper_func + ) + mocker.patch.object( + GithubEnterprise, "list_teams", side_effect=helper_list_teams_func + ) + mocker.patch.object(GithubEnterprise, "is_student", side_effect=is_student) + mocker.patch( + "services.task.TaskService.refresh", + return_value=mocker.MagicMock( + as_tuple=mocker.MagicMock(return_value=("a", "b")) + ), + ) + session = client.session + session["github_enterprise_oauth_state"] = "abc" + session.save() + mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/ghe") + url = reverse("ghe-login") + res = client.get(url, {"code": "aaaaaaa", "state": "abc"}) + assert res.status_code == 302 + + owner = Owner.objects.get(pk=client.session["current_owner_id"]) + assert owner.username == "ThiagoCodecov" + assert owner.service_id == "44376991" + assert owner.email == "thiago@codecov.io" + assert owner.private_access is True + assert res.url == "http://localhost:3000/ghe" + + +def test_get_ghe_already_owner_already_exist(client, mocker, db, mock_redis, settings): + mocker.patch( + "shared.torngit.github_enterprise.get_config", + side_effect=lambda *args: "https://my.githubenterprise.com", + ) + settings.GITHUB_ENTERPRISE_CLIENT_ID = "3d44be0e772666136a13" + the_bot = OwnerFactory.create(service="github_enterprise") + the_bot.save() + owner = OwnerFactory.create( + bot=the_bot, service="github_enterprise", service_id="44376991" + ) + owner.save() + old_ownerid = owner.ownerid + assert owner.bot is not None + settings.COOKIES_DOMAIN = ".simple.site" + settings.COOKIE_SECRET = "secret" + + async def helper_func(*args, **kwargs): + return { + "login": "ThiagoCodecov", + "id": 44376991, + "access_token": "testh04ph89fx0nkd3diauxcw75fyiuo3b86fw4j", + "scope": "read:org,repo:status,user:email,write:repo_hook,repo", + } + + async def helper_list_teams_func(*args, **kwargs): + return [ + { + "email": "hello@codecov.io", + "id": "8226205", + "name": "Codecov", + "username": "codecov", + } + ] + + async def is_student(*args, **kwargs): + return True + + mocker.patch.object( + GithubEnterprise, "get_authenticated_user", side_effect=helper_func + ) + mocker.patch.object( + GithubEnterprise, "list_teams", side_effect=helper_list_teams_func + ) + mocker.patch.object(GithubEnterprise, "is_student", side_effect=is_student) + mocker.patch( + "services.task.TaskService.refresh", + return_value=mocker.MagicMock( + as_tuple=mocker.MagicMock(return_value=("a", "b")) + ), + ) + url = reverse("ghe-login") + session = client.session + session["github_enterprise_oauth_state"] = "abc" + session.save() + mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/ghe") + res = client.get(url, {"code": "aaaaaaa", "state": "abc"}) + assert res.status_code == 302 + + owner = Owner.objects.get(pk=client.session["current_owner_id"]) + assert owner.username == "ThiagoCodecov" + assert owner.ownerid == old_ownerid + assert owner.bot is None + assert owner.service_id == "44376991" + assert owner.private_access is True + assert res.url == "http://localhost:3000/ghe" diff --git a/apps/codecov-api/codecov_auth/tests/unit/views/test_gitlab.py b/apps/codecov-api/codecov_auth/tests/unit/views/test_gitlab.py new file mode 100644 index 0000000000..c6a0e4e50b --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/views/test_gitlab.py @@ -0,0 +1,179 @@ +from unittest.mock import call +from uuid import UUID + +import pytest +from django.urls import reverse +from shared.torngit import Gitlab +from shared.torngit.exceptions import TorngitClientGeneralError + +from codecov_auth.models import Owner +from utils.encryption import encryptor + + +def _get_state_from_redis(mock_redis): + key_redis = mock_redis.keys("*")[0].decode() + return key_redis.replace("oauth-state-", "") + + +@pytest.mark.django_db +def test_get_gitlab_redirect(client, settings, mock_redis, mocker): + mocker.patch( + "codecov_auth.views.gitlab.uuid4", + return_value=UUID("fbdf86c6c8d64ed1b814e80b33df85c9"), + ) + settings.GITLAB_CLIENT_ID = ( + "testfiuozujcfo5kxgigugr5x3xxx2ukgyandp16x6w566uits7f32crzl4yvmth" + ) + settings.GITLAB_CLIENT_SECRET = ( + "testi1iinnfrhnf2q6htycgexmp04f1z2mrd7w7u8bigskhwq2km6yls8e2mddzh" + ) + settings.GITLAB_REDIRECT_URI = "http://localhost/login/gitlab" + url = reverse("gitlab-login") + res = client.get(url, SERVER_NAME="localhost:8000") + state = _get_state_from_redis(mock_redis) + assert res.status_code == 302 + assert ( + res.url + == f"https://gitlab.com/oauth/authorize?response_type=code&client_id=testfiuozujcfo5kxgigugr5x3xxx2ukgyandp16x6w566uits7f32crzl4yvmth&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Fgitlab&state={state}&scope=api" + ) + + +def test_get_gitlab_already_with_code(client, mocker, db, settings, mock_redis): + settings.GITLAB_CLIENT_ID = ( + "testfiuozujcfo5kxgigugr5x3xxx2ukgyandp16x6w566uits7f32crzl4yvmth" + ) + settings.GITLAB_CLIENT_SECRET = ( + "testi1iinnfrhnf2q6htycgexmp04f1z2mrd7w7u8bigskhwq2km6yls8e2mddzh" + ) + settings.COOKIES_DOMAIN = ".simple.site" + settings.COOKIE_SECRET = "cookie-secret" + + access_token = "testp2twc8gxedplfn91tm4zn4r4ak2xgyr4ug96q86r2gr0re0143f20nuftka8" + refresh_token = "testqyuk6z4s086jcvwoncxz8owl57o30qx1mhxlw3lgqliisujsiakh3ejq91tt" + + async def helper_func(*args, **kwargs): + return { + "id": 3124507, + "name": "Thiago Ramos", + "username": "ThiagoCodecov", + "state": "active", + "access_token": access_token, + "token_type": "Bearer", + "refresh_token": refresh_token, + "scope": "api", + } + + async def helper_list_teams_func(*args, **kwargs): + return [ + { + "email": "hello@codecov.io", + "id": "8226205", + "name": "Codecov", + "username": "codecov", + } + ] + + mocker.patch.object(Gitlab, "get_authenticated_user", side_effect=helper_func) + mocker.patch.object(Gitlab, "list_teams", side_effect=helper_list_teams_func) + mocker.patch( + "services.task.TaskService.refresh", + return_value=mocker.MagicMock( + as_tuple=mocker.MagicMock(return_value=("a", "b")) + ), + ) + + session = client.session + session["gitlab_oauth_state"] = "abc" + session.save() + mock_create_user_onboarding_metric = mocker.patch( + "shared.django_apps.codecov_metrics.service.codecov_metrics.UserOnboardingMetricsService.create_user_onboarding_metric" + ) + url = reverse("gitlab-login") + mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/gl") + res = client.get(url, {"code": "aaaaaaa", "state": "abc"}) + assert res.status_code == 302 + + owner = Owner.objects.get(pk=client.session["current_owner_id"]) + assert owner.username == "ThiagoCodecov" + assert owner.service_id == "3124507" + assert res.url == "http://localhost:3000/gl" + + expected_call = call( + org_id=owner.ownerid, + event="INSTALLED_APP", + payload={"login": "gitlab"}, + ) + assert mock_create_user_onboarding_metric.call_args_list == [expected_call] + + assert encryptor.decode(owner.oauth_token) == f"{access_token}: :{refresh_token}" + + +def test_get_gitlab_already_with_code_no_session( + client, mocker, db, settings, mock_redis +): + settings.GITLAB_CLIENT_ID = ( + "testfiuozujcfo5kxgigugr5x3xxx2ukgyandp16x6w566uits7f32crzl4yvmth" + ) + settings.GITLAB_CLIENT_SECRET = ( + "testi1iinnfrhnf2q6htycgexmp04f1z2mrd7w7u8bigskhwq2km6yls8e2mddzh" + ) + settings.COOKIES_DOMAIN = ".simple.site" + settings.COOKIE_SECRET = "cookie-secret" + + access_token = "testp2twc8gxedplfn91tm4zn4r4ak2xgyr4ug96q86r2gr0re0143f20nuftka8" + refresh_token = "testqyuk6z4s086jcvwoncxz8owl57o30qx1mhxlw3lgqliisujsiakh3ejq91tt" + + async def helper_func(*args, **kwargs): + return { + "id": 3124507, + "name": "Thiago Ramos", + "username": "ThiagoCodecov", + "state": "active", + "access_token": access_token, + "token_type": "Bearer", + "refresh_token": refresh_token, + "scope": "api", + } + + async def helper_list_teams_func(*args, **kwargs): + return [ + { + "email": "hello@codecov.io", + "id": "8226205", + "name": "Codecov", + "username": "codecov", + } + ] + + mocker.patch.object(Gitlab, "get_authenticated_user", side_effect=helper_func) + mocker.patch.object(Gitlab, "list_teams", side_effect=helper_list_teams_func) + mocker.patch( + "services.task.TaskService.refresh", + return_value=mocker.MagicMock( + as_tuple=mocker.MagicMock(return_value=("a", "b")) + ), + ) + url = reverse("gitlab-login") + res = client.get(url, {"code": "aaaaaaa", "state": "abc"}) + assert res.status_code == 302 + assert res.url == "http://localhost:3000/gl" + + assert "current_owner_id" not in client.session + + +def test_get_github_already_with_code_gitlab_error( + client, mocker, db, mock_redis, settings +): + settings.COOKIES_DOMAIN = ".simple.site" + + async def helper_func(*args, **kwargs): + raise TorngitClientGeneralError(403, "response", "message") + + mocker.patch.object(Gitlab, "get_authenticated_user", side_effect=helper_func) + url = reverse("gitlab-login") + mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/gl") + res = client.get(url, {"code": "aaaaaaa", "state": "abc"}) + assert res.status_code == 302 + + assert "current_owner_id" not in client.session + assert res.url == "/" diff --git a/apps/codecov-api/codecov_auth/tests/unit/views/test_gitlab_enterprise.py b/apps/codecov-api/codecov_auth/tests/unit/views/test_gitlab_enterprise.py new file mode 100644 index 0000000000..416afdfe73 --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/views/test_gitlab_enterprise.py @@ -0,0 +1,121 @@ +from uuid import UUID + +import pytest +from django.test import override_settings +from django.urls import reverse +from shared.torngit import GitlabEnterprise +from shared.torngit.exceptions import TorngitClientGeneralError + +from codecov_auth.models import Owner + + +def _get_state_from_redis(mock_redis): + key_redis = mock_redis.keys("*")[0].decode() + return key_redis.replace("oauth-state-", "") + + +@override_settings( + GITLAB_ENTERPRISE_CLIENT_ID="testfiuozujcfo5kxgigugr5x3xxx2ukgyandp16x6w566uits7f32crzl4yvmth" +) +@override_settings( + GITLAB_ENTERPRISE_CLIENT_SECRET="testi1iinnfrhnf2q6htycgexmp04f1z2mrd7w7u8bigskhwq2km6yls8e2mddzh" +) +@override_settings(GITLAB_ENTERPRISE_REDIRECT_URI="http://localhost/login/gle") +@pytest.mark.django_db +def test_get_gle_redirect(client, settings, mock_redis, mocker): + mock_get_config = mocker.patch( + "shared.torngit.gitlab_enterprise.get_config", + side_effect=lambda *args: "https://my.gitlabenterprise.com", + ) + mocker.patch( + "codecov_auth.views.gitlab.uuid4", + return_value=UUID("fbdf86c6c8d64ed1b814e80b33df85c9"), + ) + url = reverse("gle-login") + res = client.get(url, SERVER_NAME="localhost:8000") + state = _get_state_from_redis(mock_redis) + assert res.status_code == 302 + assert ( + res.url + == f"https://my.gitlabenterprise.com/oauth/authorize?response_type=code&client_id=testfiuozujcfo5kxgigugr5x3xxx2ukgyandp16x6w566uits7f32crzl4yvmth&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Fgle&state={state}&scope=api" + ) + mock_get_config.assert_called_with("gitlab_enterprise", "url") + + +@override_settings( + GITLAB_ENTERPRISE_CLIENT_ID="testfiuozujcfo5kxgigugr5x3xxx2ukgyandp16x6w566uits7f32crzl4yvmth" +) +@override_settings( + GITLAB_ENTERPRISE_CLIENT_SECRET="testi1iinnfrhnf2q6htycgexmp04f1z2mrd7w7u8bigskhwq2km6yls8e2mddzh" +) +@override_settings(GITLAB_ENTERPRISE_REDIRECT_URI="http://localhost/login/gle") +def test_get_gle_already_with_code(client, mocker, db, settings, mock_redis): + settings.COOKIE_SECRET = "secret" + settings.COOKIES_DOMAIN = ".simple.site" + + async def helper_func(*args, **kwargs): + return { + "id": 3124507, + "name": "Thiago Ramos", + "username": "ThiagoCodecov", + "state": "active", + "access_token": "testp2twc8gxedplfn91tm4zn4r4ak2xgyr4ug96q86r2gr0re0143f20nuftka8", + "token_type": "Bearer", + "refresh_token": "testqyuk6z4s086jcvwoncxz8owl57o30qx1mhxlw3lgqliisujsiakh3ejq91tt", + "scope": "api", + } + + async def helper_list_teams_func(*args, **kwargs): + return [ + { + "email": "hello@codecov.io", + "id": "8226205", + "name": "Codecov", + "username": "codecov", + } + ] + + mocker.patch.object( + GitlabEnterprise, "get_authenticated_user", side_effect=helper_func + ) + mocker.patch.object( + GitlabEnterprise, "list_teams", side_effect=helper_list_teams_func + ) + mocker.patch( + "services.task.TaskService.refresh", + return_value=mocker.MagicMock( + as_tuple=mocker.MagicMock(return_value=("a", "b")) + ), + ) + url = reverse("gle-login") + mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/gle") + session = client.session + session["gitlab_enterprise_oauth_state"] = "abc" + session.save() + res = client.get(url, {"code": "aaaaaaa", "state": "abc"}) + assert res.status_code == 302 + + owner = Owner.objects.get(pk=client.session["current_owner_id"]) + assert owner.username == "ThiagoCodecov" + assert owner.service_id == "3124507" + assert res.url == "http://localhost:3000/gle" + + +def test_get_gle_already_with_code_github_error( + client, mocker, db, mock_redis, settings +): + settings.COOKIES_DOMAIN = ".simple.site" + + async def helper_func(*args, **kwargs): + raise TorngitClientGeneralError(403, "response", "message") + + mocker.patch.object( + GitlabEnterprise, "get_authenticated_user", side_effect=helper_func + ) + url = reverse("gle-login") + mock_redis.setex("oauth-state-abc", 300, "http://localhost:3000/gle") + res = client.get(url, {"code": "aaaaaaa", "state": "abc"}) + assert res.status_code == 302 + assert "gitlab_enterprise-token" not in res.cookies + assert "gitlab_enterprise-username" not in res.cookies + assert res.url == "/" diff --git a/apps/codecov-api/codecov_auth/tests/unit/views/test_logout.py b/apps/codecov-api/codecov_auth/tests/unit/views/test_logout.py new file mode 100644 index 0000000000..53132a35fb --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/views/test_logout.py @@ -0,0 +1,38 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from utils.test_utils import Client + + +class LogoutViewTest(TestCase): + def _get(self, url): + return self.client.get(url, content_type="application/json") + + def _post(self, url): + return self.client.post(url, content_type="application/json") + + def _is_authenticated(self): + response = self.client.post( + "/graphql/gh", + {"query": "{ me { user { username } } }"}, + content_type="application/json", + ) + return response.json()["data"]["me"] is not None + + def test_logout_when_unauthenticated(self): + res = self._post("/logout") + assert res.status_code == 401 + + def test_logout_when_authenticated(self): + owner = OwnerFactory() + self.client = Client() + self.client.force_login_owner(owner) + + res = self._post("/graphql/gh/") + self.assertEqual(self._is_authenticated(), True) + + res = self._post("/logout") + self.assertEqual(res.status_code, 205) + + res = self._get("/graphql/gh/") + self.assertEqual(self._is_authenticated(), False) diff --git a/apps/codecov-api/codecov_auth/tests/unit/views/test_okta.py b/apps/codecov-api/codecov_auth/tests/unit/views/test_okta.py new file mode 100644 index 0000000000..b4acd80431 --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/views/test_okta.py @@ -0,0 +1,346 @@ +import pytest +from django.conf import settings +from django.contrib import auth +from django.test import override_settings +from django.urls import reverse +from shared.django_apps.codecov_auth.tests.factories import ( + OktaUserFactory, + OwnerFactory, + UserFactory, +) + +from codecov_auth.models import OktaUser +from codecov_auth.views.okta import OKTA_BASIC_AUTH +from codecov_auth.views.okta_mixin import OktaIdTokenPayload + + +@pytest.fixture +def mocked_okta_token_request(mocker): + return mocker.patch( + "codecov_auth.views.okta_mixin.requests.post", + return_value=mocker.MagicMock( + status_code=200, + json=mocker.MagicMock( + return_value={ + "access_token": "test-access-token", + "refresh_token": "test-refresh-token", + "id_token": "test-id-token", + "state": "test-state", + }, + ), + ), + ) + + +@pytest.fixture +def mocked_validate_id_token(mocker): + return mocker.patch( + "codecov_auth.views.okta.validate_id_token", + return_value=OktaIdTokenPayload( + sub="test-id", + email="test@example.com", + name="Some User", + iss="https://example.com", + aud="test-client-id", + ), + ) + + +# random keypair for RS256 JWTs used below +public_key = """-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo +4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u ++qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh +kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ +0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg +cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc +mwIDAQAB +-----END PUBLIC KEY----- +""" + + +@override_settings( + OKTA_OAUTH_CLIENT_ID="test-client-id", + OKTA_OAUTH_REDIRECT_URL="https://localhost:8000/login/okta", +) +def test_okta_redirect_to_authorize(client, db): + res = client.get( + reverse("okta-login"), + data={ + "iss": "https://example.okta.com", + }, + ) + state = client.session["okta_oauth_state"] + + assert res.status_code == 302 + assert ( + res.url + == "https://example.okta.com/oauth2/v1/authorize?response_type=code&client_id=test-client-id&scope=openid+email+profile&redirect_uri=https%3A%2F%2Flocalhost%3A8000%2Flogin%2Fokta&state={}".format( + state + ) + ) + + +@override_settings( + OKTA_OAUTH_CLIENT_ID="test-client-id", + OKTA_OAUTH_REDIRECT_URL="https://localhost:8000/login/okta", + OKTA_ISS=None, +) +def test_okta_redirect_to_authorize_no_iss(client): + res = client.get(reverse("okta-login")) + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/login" + + +@override_settings( + OKTA_OAUTH_CLIENT_ID="test-client-id", + OKTA_OAUTH_REDIRECT_URL="https://localhost:8000/login/okta", + OKTA_ISS="https://non.okta.domain", +) +def test_okta_redirect_to_authorize_invalid_iss(client): + res = client.get(reverse("okta-login")) + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/login" + + +@override_settings( + OKTA_OAUTH_CLIENT_ID="test-client-id", + OKTA_OAUTH_CLIENT_SECRET="test-client-secret", + OKTA_OAUTH_REDIRECT_URL="https://localhost:8000/login/okta", +) +def test_okta_perform_login( + client, mocked_okta_token_request, mocked_validate_id_token, db +): + state = "test-state" + session = client.session + session["okta_oauth_state"] = state + session.save() + + res = client.get( + reverse("okta-login"), + data={ + "code": "test-code", + "state": state, + }, + ) + + mocked_okta_token_request.assert_called_once_with( + "https://example.okta.com/oauth2/v1/token", + auth=OKTA_BASIC_AUTH, + data={ + "grant_type": "authorization_code", + "code": "test-code", + "redirect_uri": "https://localhost:8000/login/okta", + "state": state, + }, + ) + + mocked_validate_id_token.assert_called_once_with( + "https://example.okta.com", "test-id-token", "test-client-id" + ) + + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/sync" + + # creates new user records + okta_user = OktaUser.objects.get(okta_id="test-id") + assert okta_user.access_token == "test-access-token" + assert okta_user.email == "test@example.com" + assert okta_user.name == "Some User" + user = okta_user.user + assert user is not None + assert user.email == okta_user.email + assert user.name == okta_user.name + + # logs in new user + current_user = auth.get_user(client) + assert user == current_user + + +@override_settings( + OKTA_OAUTH_CLIENT_ID="test-client-id", + OKTA_OAUTH_CLIENT_SECRET="test-client-secret", + OKTA_OAUTH_REDIRECT_URL="https://localhost:8000/login/okta", +) +def test_okta_perform_login_authenticated( + client, mocked_okta_token_request, mocked_validate_id_token, db +): + user = UserFactory() + client.force_login(user=user) + + state = "test-state" + session = client.session + session["okta_oauth_state"] = state + session.save() + + res = client.get( + reverse("okta-login"), + data={ + "code": "test-code", + "state": state, + }, + ) + + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/sync" + + # creates new user records + okta_user = OktaUser.objects.get(okta_id="test-id") + assert okta_user.access_token == "test-access-token" + assert okta_user.email == "test@example.com" + assert okta_user.name == "Some User" + assert okta_user.user == user + + # leaves user logged in + current_user = auth.get_user(client) + assert user == current_user + + +@override_settings( + OKTA_OAUTH_CLIENT_ID="test-client-id", + OKTA_OAUTH_CLIENT_SECRET="test-client-secret", + OKTA_OAUTH_REDIRECT_URL="https://localhost:8000/login/okta", +) +def test_okta_perform_login_existing_okta_user( + client, mocked_okta_token_request, mocked_validate_id_token, db +): + okta_user = OktaUserFactory(okta_id="test-id") + + state = "test-state" + session = client.session + session["okta_oauth_state"] = state + session.save() + + res = client.get( + reverse("okta-login"), + data={ + "code": "test-code", + "state": state, + }, + ) + + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/sync" + + # logs in Okta user + current_user = auth.get_user(client) + assert current_user == okta_user.user + + +@override_settings( + OKTA_OAUTH_CLIENT_ID="test-client-id", + OKTA_OAUTH_CLIENT_SECRET="test-client-secret", + OKTA_OAUTH_REDIRECT_URL="https://localhost:8000/login/okta", +) +def test_okta_perform_login_authenticated_existing_okta_user( + client, mocked_okta_token_request, mocked_validate_id_token, db +): + okta_user = OktaUserFactory(okta_id="test-id") + other_okta_user = OktaUserFactory() + + client.force_login(user=other_okta_user.user) + + state = "test-state" + session = client.session + session["okta_oauth_state"] = state + session.save() + + res = client.get( + reverse("okta-login"), + data={ + "code": "test-code", + "state": state, + }, + ) + + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/sync" + + # logs in Okta user + current_user = auth.get_user(client) + assert current_user == okta_user.user + + +@override_settings( + OKTA_OAUTH_CLIENT_ID="test-client-id", + OKTA_OAUTH_CLIENT_SECRET="test-client-secret", + OKTA_OAUTH_REDIRECT_URL="https://localhost:8000/login/okta", +) +@pytest.mark.django_db +def test_okta_perform_login_existing_okta_user_existing_owner( + client, mocked_okta_token_request, mocked_validate_id_token +): + okta_user = OktaUserFactory(okta_id="test-id") + OwnerFactory(service="github", user=okta_user.user) + + state = "test-state" + session = client.session + session["okta_oauth_state"] = state + session.save() + + res = client.get( + reverse("okta-login"), + data={ + "code": "test-code", + "state": state, + }, + ) + + # redirects to service page + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/gh" + + # logs in Okta user + current_user = auth.get_user(client) + assert current_user == okta_user.user + + +@override_settings( + OKTA_OAUTH_CLIENT_ID="test-client-id", + OKTA_OAUTH_CLIENT_SECRET="test-client-secret", + OKTA_OAUTH_REDIRECT_URL="https://localhost:8000/login/okta", +) +def test_okta_perform_login_error(client, mocker, db): + mocker.patch( + "codecov_auth.views.okta_mixin.requests.post", + return_value=mocker.MagicMock( + status_code=401, + ), + ) + state = "test-state" + session = client.session + session["okta_oauth_state"] = state + session.save() + + res = client.get( + reverse("okta-login"), + data={ + "code": "test-code", + "state": state, + }, + ) + + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/login" + + +@override_settings( + OKTA_OAUTH_CLIENT_ID="test-client-id", + OKTA_OAUTH_CLIENT_SECRET="test-client-secret", + OKTA_OAUTH_REDIRECT_URL="https://localhost:8000/login/okta", +) +def test_okta_perform_login_state_mismatch(client, mocker, db): + res = client.get( + reverse("okta-login"), + data={ + "code": "test-code", + "state": "invalid-state", + }, + ) + + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/login" + + # does not login user + current_user = auth.get_user(client) + assert current_user.is_anonymous diff --git a/apps/codecov-api/codecov_auth/tests/unit/views/test_okta_cloud.py b/apps/codecov-api/codecov_auth/tests/unit/views/test_okta_cloud.py new file mode 100644 index 0000000000..5472454a52 --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/views/test_okta_cloud.py @@ -0,0 +1,554 @@ +from logging import LogRecord +from typing import Any +from unittest.mock import ANY +from urllib.parse import unquote, urlparse + +import pytest +from django.test import override_settings +from pytest import LogCaptureFixture +from pytest_mock import MockerFixture +from shared.django_apps.codecov_auth.models import Account, OktaSettings, Owner +from shared.django_apps.codecov_auth.tests.factories import ( + AccountFactory, + OktaSettingsFactory, +) +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov_auth.views.okta_cloud import ( + OKTA_CURRENT_SESSION, + OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY, +) +from codecov_auth.views.okta_mixin import OktaIdTokenPayload +from utils.test_utils import Client as TestClient + + +@pytest.fixture +def signed_in_client() -> TestClient: + new_client = TestClient() + new_client.force_login_owner(OwnerFactory()) + return new_client + + +@pytest.fixture +def okta_org_name() -> str: + return "foo-bar-organization" + + +@pytest.fixture +def okta_org(okta_org_name: str) -> Owner: + org: Owner = OwnerFactory.create(username=okta_org_name, service="github") + org.save() + return org + + +@pytest.fixture +def okta_account(okta_org: Owner): + account = AccountFactory() + okta_org.account = account + okta_org.save() + + okta_settings: OktaSettings = OktaSettingsFactory(account=account) + okta_settings.url = "https://foo-bar.okta.com/" + okta_settings.save() + return account + + +@pytest.fixture +def mocked_okta_token_request(mocker): + return mocker.patch( + "codecov_auth.views.okta_mixin.requests.post", + return_value=mocker.MagicMock( + status_code=200, + json=mocker.MagicMock( + return_value={ + "access_token": "test-access-token", + "refresh_token": "test-refresh-token", + "id_token": "test-id-token", + "state": "test-state", + }, + ), + ), + ) + + +@pytest.fixture +def mocked_validate_id_token(mocker): + return mocker.patch( + "codecov_auth.views.okta_cloud.validate_id_token", + return_value=OktaIdTokenPayload( + sub="test-id", + email="test@example.com", + name="Some User", + iss="https://example.com", + aud="test-client-id", + ), + ) + + +def log_message_exists(message: str, logs: list[LogRecord]) -> bool: + """Helper method to check that a particular log record was emitted""" + for log in logs: + if log.message == message: + return True + return False + + +@pytest.mark.django_db +def test_okta_login_unauthenticated_user( + client: TestClient, + caplog: LogCaptureFixture, +): + res = client.get("/login/okta/github/some-unknown-service") + assert log_message_exists( + "User needs to be signed in before authenticating organization with Okta.", + caplog.records, + ) + assert res.status_code == 403 + + +@pytest.mark.django_db +def test_okta_login_invalid_organization( + signed_in_client: TestClient, + caplog: LogCaptureFixture, +): + res = signed_in_client.get("/login/okta/github/some-unknown-service") + assert log_message_exists("The organization doesn't exist.", caplog.records) + assert res.status_code == 404 + + +@pytest.mark.django_db +def test_okta_login_no_account(signed_in_client: TestClient, caplog: LogCaptureFixture): + org: Owner = OwnerFactory.create(username="org-no-account", service="github") + org.save() + res = signed_in_client.get("/login/okta/github/org-no-account") + assert log_message_exists( + "Okta settings not found. Cannot sign into Okta", caplog.records + ) + assert res.status_code == 404 + + +@pytest.mark.django_db +def test_okta_login_no_okta_settings( + signed_in_client: TestClient, caplog: LogCaptureFixture +): + org: Owner = OwnerFactory.create(username="account-no-okta", service="github") + org.account = AccountFactory() + org.save() + res = signed_in_client.get("/login/okta/github/account-no-okta") + assert log_message_exists( + "Okta settings not found. Cannot sign into Okta", caplog.records + ) + assert res.status_code == 404 + + +@pytest.mark.django_db +def test_okta_login_already_signed_into_okta( + signed_in_client: TestClient, + okta_org_name: str, + okta_account: Account, +): + session = signed_in_client.session + session[OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY] = [okta_account.id] + session.save() + res = signed_in_client.get(f"/login/okta/gh/{okta_org_name}") + assert res.status_code == 302 + assert res.url == f"http://localhost:3000/github/{okta_org_name}" + + +@override_settings( + CODECOV_API_URL="http://localhost:8000", +) +@pytest.mark.django_db +def test_okta_login_redirect_to_okta_issuer( + signed_in_client: TestClient, okta_org_name: str, okta_account: Account +): + res = signed_in_client.get(f"/login/okta/gh/{okta_org_name}") + assert res.status_code == 302 + parsed_url = urlparse(res.url) + assert parsed_url.hostname == "foo-bar.okta.com" + assert parsed_url.path == "/oauth2/v1/authorize" + + parsed_query = parsed_url.query.split("&") + raw_redirect_url = next(x for x in parsed_query if x.startswith("redirect_uri=")) + assert raw_redirect_url + assert ( + unquote(raw_redirect_url.split("=")[1]) + == "http://localhost:8000/login/okta/callback" + ) + + +@pytest.mark.django_db +def test_okta_callback_login_success( + signed_in_client: TestClient, + okta_account: Account, + okta_org: Owner, + mocked_validate_id_token: Any, + mocked_okta_token_request: Any, +): + state = "test-state" + session = signed_in_client.session + assert session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) is None + + session["okta_cloud_oauth_state"] = state + session[OKTA_CURRENT_SESSION] = { + "org_ownerid": okta_org.ownerid, + "okta_settings_id": okta_account.okta_settings.first().id, + } + + session.save() + + res = signed_in_client.get( + "/login/okta/callback", + data={ + "code": "random-code", + "state": state, + }, + ) + + assert res.status_code == 302 + assert res.url == f"http://localhost:3000/github/{okta_org.username}" + + updated_session = signed_in_client.session + assert updated_session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) == [okta_account.id] + + mocked_validate_id_token.assert_called_with("https://foo-bar.okta.com", ANY, ANY) + + +@pytest.mark.django_db +def test_okta_callback_login_success_multiple_accounts( + signed_in_client: TestClient, + okta_account: Account, + okta_org: Owner, + mocked_validate_id_token: Any, + mocked_okta_token_request: Any, +): + state = "test-state" + session = signed_in_client.session + # Put in a random account that's not current okta_account + session[OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY] = [okta_account.id + 1] + + session["okta_cloud_oauth_state"] = state + session[OKTA_CURRENT_SESSION] = { + "org_ownerid": okta_org.ownerid, + "okta_settings_id": okta_account.okta_settings.first().id, + } + + session.save() + + res = signed_in_client.get( + "/login/okta/callback", + data={ + "code": "random-code", + "state": state, + }, + ) + + assert res.status_code == 302 + assert res.url == f"http://localhost:3000/github/{okta_org.username}" + + updated_session = signed_in_client.session + assert updated_session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) == [ + okta_account.id + 1, + okta_account.id, + ] + + mocked_validate_id_token.assert_called_with("https://foo-bar.okta.com", ANY, ANY) + + +@pytest.mark.django_db +def test_okta_callback_missing_session( + signed_in_client: TestClient, + caplog: LogCaptureFixture, + okta_org: Owner, + okta_account: Account, +): + session = signed_in_client.session + state = "test-state" + assert session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) is None + session["okta_cloud_oauth_state"] = state + session.save() + + res = signed_in_client.get( + "/login/okta/callback", + data={ + "code": "random-code", + "state": state, + }, + ) + assert res.status_code == 403 + + assert log_message_exists( + "Trying to sign into Okta with no existing sign-in session.", caplog.records + ) + + updated_session = signed_in_client.session + assert updated_session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) is None + + +@pytest.mark.django_db +def test_okta_callback_missing_user( + client: TestClient, + caplog: LogCaptureFixture, + okta_org: Owner, + okta_account: Account, +): + session = client.session + state = "test-state" + assert session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) is None + session["okta_cloud_oauth_state"] = state + session[OKTA_CURRENT_SESSION] = { + "org_ownerid": okta_org.ownerid, + "okta_settings_id": okta_account.okta_settings.first().id, + } + session.save() + + res = client.get( + "/login/okta/callback", + data={ + "code": "random-code", + "state": state, + }, + ) + assert res.status_code == 403 + + assert log_message_exists("User not logged in for Okta callback.", caplog.records) + + updated_session = client.session + assert updated_session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) is None + + +@pytest.mark.django_db +def test_okta_callback_missing_okta_settings( + signed_in_client: TestClient, + caplog: LogCaptureFixture, + okta_org: Owner, + okta_account: Account, +): + session = signed_in_client.session + state = "test-state" + assert session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) is None + session["okta_cloud_oauth_state"] = state + session[OKTA_CURRENT_SESSION] = { + "org_ownerid": okta_org.ownerid, + "okta_settings_id": 12345, + } + session.save() + + res = signed_in_client.get( + "/login/okta/callback", + data={ + "code": "random-code", + "state": state, + }, + ) + assert res.status_code == 404 + + assert log_message_exists( + "Okta settings not found. Cannot sign into Okta", caplog.records + ) + + updated_session = signed_in_client.session + assert updated_session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) is None + + +@pytest.mark.django_db +def test_okta_callback_no_code( + signed_in_client: TestClient, + caplog: LogCaptureFixture, + okta_org: Owner, + okta_account: Account, +): + session = signed_in_client.session + state = "test-state" + assert session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) is None + session["okta_cloud_oauth_state"] = state + session[OKTA_CURRENT_SESSION] = { + "org_ownerid": okta_org.ownerid, + "okta_settings_id": okta_account.okta_settings.first().id, + } + session.save() + + res = signed_in_client.get( + "/login/okta/callback", + data={ + "state": state, + }, + ) + assert res.status_code == 400 + + assert log_message_exists( + "No code is passed. Invalid callback. Cannot sign into Okta", caplog.records + ) + + updated_session = signed_in_client.session + assert updated_session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) is None + + +@pytest.mark.django_db +def test_okta_callback_perform_login_invalid_state( + signed_in_client: TestClient, + caplog: LogCaptureFixture, + okta_org: Owner, + okta_account: Account, +): + session = signed_in_client.session + assert session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) is None + session["okta_cloud_oauth_state"] = "random-state" + + session[OKTA_CURRENT_SESSION] = { + "org_ownerid": okta_org.ownerid, + "okta_settings_id": okta_account.okta_settings.first().id, + } + session.save() + + res = signed_in_client.get( + "/login/okta/callback", + data={ + "code": "random-code", + "state": "different-state", + }, + ) + assert res.status_code == 302 + assert ( + res.url + == f"http://localhost:3000/github/{okta_org.username}?error=invalid_state" + ) + + assert log_message_exists("Invalid state during Okta login", caplog.records) + + updated_session = signed_in_client.session + assert updated_session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) is None + + +@pytest.mark.django_db +def test_okta_callback_perform_login_no_user_data( + mocker: MockerFixture, + signed_in_client: TestClient, + caplog: LogCaptureFixture, + okta_org: Owner, + okta_account: Account, + mocked_okta_token_request: Any, +): + state = "test-state" + session = signed_in_client.session + assert session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) is None + session["okta_cloud_oauth_state"] = state + + session[OKTA_CURRENT_SESSION] = { + "org_ownerid": okta_org.ownerid, + "okta_settings_id": okta_account.okta_settings.first().id, + } + session.save() + + mocked_okta_token_request.return_value = mocker.MagicMock( + status_code=400, + ) + + res = signed_in_client.get( + "/login/okta/callback", + data={ + "code": "random-code", + "state": state, + }, + ) + assert res.status_code == 302 + assert ( + res.url + == f"http://localhost:3000/github/{okta_org.username}?error=invalid_token_response" + ) + + assert log_message_exists( + "Can't log in. Invalid Okta Token Response", caplog.records + ) + + updated_session = signed_in_client.session + assert updated_session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) is None + + +@pytest.mark.django_db +def test_okta_callback_perform_login_invalid_id_token( + mocker: MockerFixture, + signed_in_client: TestClient, + caplog: LogCaptureFixture, + okta_org: Owner, + okta_account: Account, + mocked_okta_token_request: Any, +): + state = "test-state" + session = signed_in_client.session + assert session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) is None + session["okta_cloud_oauth_state"] = state + + session[OKTA_CURRENT_SESSION] = { + "org_ownerid": okta_org.ownerid, + "okta_settings_id": okta_account.okta_settings.first().id, + } + + session.save() + + mocked_okta_token_request.return_value = mocker.MagicMock( + status_code=200, + json=lambda: {"access_token": "mock_access_token", "id_token": "mock_id_token"}, + ) + + mocker.patch( + "codecov_auth.views.okta_cloud.validate_id_token", + side_effect=Exception("Invalid ID token"), + ) + + res = signed_in_client.get( + "/login/okta/callback", + data={ + "code": "random-code", + "state": state, + }, + ) + assert res.status_code == 302 + assert ( + res.url + == f"http://localhost:3000/github/{okta_org.username}?error=invalid_id_token" + ) + + updated_session = signed_in_client.session + assert updated_session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) is None + + +@pytest.mark.django_db +def test_okta_callback_perform_login_access_denied( + mocker: MockerFixture, + signed_in_client: TestClient, + caplog: LogCaptureFixture, + okta_org: Owner, + okta_account: Account, + mocked_okta_token_request: Any, +): + state = "test-state" + session = signed_in_client.session + assert session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) is None + session["okta_cloud_oauth_state"] = state + + session[OKTA_CURRENT_SESSION] = { + "org_ownerid": okta_org.ownerid, + "okta_settings_id": okta_account.okta_settings.first().id, + } + session.save() + + mocked_okta_token_request.return_value = mocker.MagicMock( + status_code=403, + ) + + res = signed_in_client.get( + "/login/okta/callback", + data={ + "state": state, + "error": "access_denied", + }, + ) + assert res.status_code == 302 + assert ( + res.url + == f"http://localhost:3000/github/{okta_org.username}?error=access_denied" + ) + + updated_session = signed_in_client.session + assert updated_session.get(OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY) is None diff --git a/apps/codecov-api/codecov_auth/tests/unit/views/test_okta_mixin.py b/apps/codecov-api/codecov_auth/tests/unit/views/test_okta_mixin.py new file mode 100644 index 0000000000..32f612901a --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/views/test_okta_mixin.py @@ -0,0 +1,110 @@ +from unittest.mock import MagicMock, patch + +import jwt + +from codecov_auth.views.okta import OKTA_BASIC_AUTH, OktaLoginView +from codecov_auth.views.okta_mixin import validate_id_token + +private_key = """-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj +MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu +NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ +qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg +p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR +ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi +VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV +laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8 +sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H +mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY +dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw +ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ +DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T +N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t +0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv +t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU +AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk +48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL +DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK +xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA +mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh +2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz +et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr +VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD +TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc +dn/RsYEONbwQSjIfMPkvxF+8HQ== +-----END PRIVATE KEY----- +""" + + +def test_validate_id_token(mocker): + data = { + "sub": "test-okta-id", + "name": "Some User", + "email": "test@example.com", + "iss": "https://example.okta.com", + "aud": "test-client-id", + } + id_token = jwt.encode( + data, private_key, algorithm="RS256", headers={"kid": "test-kid"} + ) + + # did this offline so as not to need an additional dependency - here's the code + # if we ever need to regenerate this: + # + # from Crypto.PublicKey import RSA + # import base64 + # pub = RSA.importKey(public_key) + # modulus = base64.b64encode(pub.n.to_bytes(256, "big")).decode("ascii") + # exponent = base64.b64encode(pub.e.to_bytes(3, "big")).decode("ascii") + + exponent = "AQAB" + modulus = "u1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmw==" + + get_keys = mocker.patch( + "codecov_auth.views.okta_mixin.requests.get", + return_value=mocker.MagicMock( + status_code=200, + json=mocker.MagicMock( + return_value={ + "keys": [ + { + "kty": "RSA", + "alg": "RS256", + "kid": "test-kid", + "use": "sig", + "e": exponent, + "n": modulus, + } + ] + } + ), + ), + ) + + id_payload = validate_id_token( + iss="https://example.okta.com", id_token=id_token, client_id="test-client-id" + ) + assert id_payload.sub == "test-okta-id" + assert id_payload.name == "Some User" + assert id_payload.email == "test@example.com" + + get_keys.assert_called_once_with("https://example.okta.com/oauth2/v1/keys") + + +def test_okta_fetch_user_data_invalid_state(client, db): + with patch("codecov_auth.views.okta_mixin.requests.post") as mock_post: + mock_response = MagicMock() + mock_response.status_code = 200 + mock_post.return_value = mock_response + + with patch.object(OktaLoginView, "verify_state", return_value=False): + view = OktaLoginView() + res = view._fetch_user_data( + "https://example.okta.com", + "test-code", + "invalid-state", + "https://localhost:8000/login/okta", + OKTA_BASIC_AUTH, + ) + + assert res is None diff --git a/apps/codecov-api/codecov_auth/tests/unit/views/test_sentry.py b/apps/codecov-api/codecov_auth/tests/unit/views/test_sentry.py new file mode 100644 index 0000000000..265ed5ae79 --- /dev/null +++ b/apps/codecov-api/codecov_auth/tests/unit/views/test_sentry.py @@ -0,0 +1,382 @@ +from unittest.mock import MagicMock, patch + +import jwt +import pytest +from django.conf import settings +from django.contrib import auth +from django.test import override_settings +from django.urls import reverse +from shared.django_apps.codecov_auth.tests.factories import ( + OwnerFactory, + SentryUserFactory, + UserFactory, +) + +from codecov_auth.models import SentryUser +from codecov_auth.views.sentry import SentryLoginView + + +@pytest.fixture +def mocked_sentry_request(mocker): + return mocker.patch( + "codecov_auth.views.sentry.requests.post", + return_value=mocker.MagicMock( + status_code=200, + json=mocker.MagicMock( + return_value={ + "access_token": "test-access-token", + "refresh_token": "test-refresh-token", + "user": { + "id": "test-id", + "email": "test@example.com", + "name": "Some User", + }, + "id_token": jwt.encode( + { + "iss": "https://sentry.io", + "aud": "test-client-id", + }, + key="test-oidc-shared-secret", + ), + } + ), + ), + ) + + +@override_settings(SENTRY_OAUTH_CLIENT_ID="test-client-id") +def test_sentry_redirect_to_consent(client, db): + res = client.get(reverse("sentry-login")) + state_from_session = client.session["sentry_oauth_state"] + assert res.status_code == 302 + assert ( + res.url + == "https://sentry.io/oauth/authorize?response_type=code&client_id=test-client-id&scope=openid+email+profile&state={}".format( + state_from_session + ) + ) + + +@override_settings( + SENTRY_OAUTH_CLIENT_ID="test-client-id", + SENTRY_OIDC_SHARED_SECRET="test-oidc-shared-secret", +) +def test_sentry_perform_login(client, mocked_sentry_request, db): + state = "test-state" + session = client.session + session["sentry_oauth_state"] = state + session.save() + + res = client.get( + reverse("sentry-login"), + data={ + "code": "test-code", + "state": state, + }, + ) + + mocked_sentry_request.assert_called_once_with( + "https://sentry.io/oauth/token/", + data={ + "grant_type": "authorization_code", + "client_id": settings.SENTRY_OAUTH_CLIENT_ID, + "client_secret": settings.SENTRY_OAUTH_CLIENT_SECRET, + "code": "test-code", + "state": state, + }, + ) + + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/sync" + + # creates new user records + sentry_user = SentryUser.objects.get(sentry_id="test-id") + assert sentry_user.access_token == "test-access-token" + assert sentry_user.refresh_token == "test-refresh-token" + assert sentry_user.email == "test@example.com" + assert sentry_user.name == "Some User" + user = sentry_user.user + assert user is not None + assert user.email == sentry_user.email + assert user.name == sentry_user.name + + # logs in new user + current_user = auth.get_user(client) + assert user == current_user + + +@override_settings( + SENTRY_OAUTH_CLIENT_ID="test-client-id", + SENTRY_OIDC_SHARED_SECRET="test-oidc-shared-secret", +) +def test_sentry_perform_login_authenticated(client, mocked_sentry_request, db): + state = "test-state" + session = client.session + session["sentry_oauth_state"] = state + session.save() + + user = UserFactory() + client.force_login(user=user) + + res = client.get( + reverse("sentry-login"), + data={ + "code": "test-code", + "state": state, + }, + ) + + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/sync" + + # creates new user records + sentry_user = SentryUser.objects.get(sentry_id="test-id") + assert sentry_user.access_token == "test-access-token" + assert sentry_user.refresh_token == "test-refresh-token" + assert sentry_user.email == "test@example.com" + assert sentry_user.name == "Some User" + assert sentry_user.user == user + + # leaves user logged in + current_user = auth.get_user(client) + assert user == current_user + + +@override_settings( + SENTRY_OAUTH_CLIENT_ID="test-client-id", + SENTRY_OIDC_SHARED_SECRET="test-oidc-shared-secret", +) +def test_sentry_perform_login_existing_sentry_user(client, mocked_sentry_request, db): + state = "test-state" + session = client.session + session["sentry_oauth_state"] = state + session.save() + + sentry_user = SentryUserFactory(sentry_id="test-id") + + res = client.get( + reverse("sentry-login"), + data={ + "code": "test-code", + "state": state, + }, + ) + + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/sync" + + # logs in sentry user + current_user = auth.get_user(client) + assert current_user == sentry_user.user + + +@override_settings( + SENTRY_OAUTH_CLIENT_ID="test-client-id", + SENTRY_OIDC_SHARED_SECRET="test-oidc-shared-secret", +) +def test_sentry_perform_login_authenticated_existing_sentry_user( + client, mocked_sentry_request, db +): + state = "test-state" + session = client.session + session["sentry_oauth_state"] = state + session.save() + + sentry_user = SentryUserFactory(sentry_id="test-id") + other_sentry_user = SentryUserFactory() + client.force_login(user=other_sentry_user.user) + + res = client.get( + reverse("sentry-login"), + data={ + "code": "test-code", + "state": state, + }, + ) + + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/sync" + + # logs in sentry user + current_user = auth.get_user(client) + assert current_user == sentry_user.user + + +@override_settings( + SENTRY_OAUTH_CLIENT_ID="test-client-id", + SENTRY_OIDC_SHARED_SECRET="test-oidc-shared-secret", +) +def test_sentry_perform_login_existing_sentry_user_existing_owner( + client, mocked_sentry_request, db +): + state = "test-state" + session = client.session + session["sentry_oauth_state"] = state + session.save() + + sentry_user = SentryUserFactory(sentry_id="test-id") + OwnerFactory(service="github", user=sentry_user.user) + + res = client.get( + reverse("sentry-login"), + data={ + "code": "test-code", + "state": state, + }, + ) + + # redirects to service page + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/gh" + + # logs in sentry user + current_user = auth.get_user(client) + assert current_user == sentry_user.user + + +@override_settings( + SENTRY_OAUTH_CLIENT_ID="test-client-id", + SENTRY_OIDC_SHARED_SECRET="test-oidc-shared-secret", +) +def test_sentry_perform_login_error(client, mocker, db): + state = "test-state" + session = client.session + session["sentry_oauth_state"] = state + session.save() + + mocker.patch( + "codecov_auth.views.sentry.requests.post", + return_value=mocker.MagicMock( + status_code=401, + ), + ) + + res = client.get( + reverse("sentry-login"), + data={ + "code": "test-code", + "state": state, + }, + ) + + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/login" + + +@override_settings( + SENTRY_OAUTH_CLIENT_ID="test-client-id", + SENTRY_OIDC_SHARED_SECRET="invalid-oidc-shared-secret", +) +def test_sentry_perform_login_invalid_id_token(client, mocked_sentry_request, db): + state = "test-state" + session = client.session + session["sentry_oauth_state"] = state + session.save() + + res = client.get( + reverse("sentry-login"), + data={ + "code": "test-code", + "state": state, + }, + ) + + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/login" + + # does not login user + current_user = auth.get_user(client) + assert current_user.is_anonymous + + +@override_settings( + SENTRY_OAUTH_CLIENT_ID="test-client-id", + SENTRY_OIDC_SHARED_SECRET="test-oidc-shared-secret", +) +def test_sentry_perform_login_invalid_id_token_issuer(client, mocker, db): + mocker.patch( + "codecov_auth.views.sentry.requests.post", + return_value=mocker.MagicMock( + status_code=200, + json=mocker.MagicMock( + return_value={ + "access_token": "test-access-token", + "refresh_token": "test-refresh-token", + "user": { + "id": "test-id", + "email": "test@example.com", + "name": "Some User", + }, + "id_token": jwt.encode( + { + "iss": "invalid-issuer", + "aud": "test-client-id", + }, + key="test-oidc-shared-secret", + ), + } + ), + ), + ) + + state = "test-state" + session = client.session + session["sentry_oauth_state"] = state + session.save() + + res = client.get( + reverse("sentry-login"), + data={ + "code": "test-code", + "state": state, + }, + ) + + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/login" + + # does not login user + current_user = auth.get_user(client) + assert current_user.is_anonymous + + +@override_settings( + SENTRY_OAUTH_CLIENT_ID="test-client-id", + SENTRY_OIDC_SHARED_SECRET="test-oidc-shared-secret", +) +def test_sentry_perform_login_state_mismatch(client, mocked_sentry_request, db): + res = client.get( + reverse("sentry-login"), + data={ + "code": "test-code", + "state": "invalid-state", + }, + ) + + assert res.status_code == 302 + assert res.url == f"{settings.CODECOV_DASHBOARD_URL}/login" + + # does not login user + current_user = auth.get_user(client) + assert current_user.is_anonymous + + +@override_settings( + OKTA_OAUTH_CLIENT_ID="test-client-id", + OKTA_OAUTH_CLIENT_SECRE="test-client-secret", + OKTA_OAUTH_REDIRECT_URL="https://localhost:8000/login/okta", +) +def test_sentry_fetch_user_data_invalid_state(client, db): + with patch("codecov_auth.views.sentry.requests.post") as mock_post: + mock_response = MagicMock() + mock_response.status_code = 200 + mock_post.return_value = mock_response + + with patch.object(SentryLoginView, "verify_state", return_value=False): + view = SentryLoginView() + res = view._fetch_user_data( + "test-code", + "invalid-state", + ) + + assert res is None diff --git a/apps/codecov-api/codecov_auth/urls.py b/apps/codecov-api/codecov_auth/urls.py new file mode 100644 index 0000000000..47081a2b96 --- /dev/null +++ b/apps/codecov-api/codecov_auth/urls.py @@ -0,0 +1,56 @@ +from django.conf import settings +from django.urls import path, re_path + +from .views.bitbucket import BitbucketLoginView +from .views.bitbucket_server import BitbucketServerLoginView +from .views.github import GithubLoginView +from .views.github_enterprise import GithubEnterpriseLoginView +from .views.gitlab import GitlabLoginView +from .views.gitlab_enterprise import GitlabEnterpriseLoginView +from .views.logout import logout_view +from .views.okta import OktaLoginView +from .views.okta_cloud import OktaCloudCallbackView, OktaCloudLoginView +from .views.sentry import SentryLoginView + +urlpatterns = [ + path("logout", logout_view, name="logout"), + path("logout/", logout_view, name="logout-service"), + path("login/github", GithubLoginView.as_view(), name="github-login"), + path("login/gh", GithubLoginView.as_view(), name="gh-login"), + path("login/gitlab", GitlabLoginView.as_view(), name="gitlab-login"), + path("login/gl", GitlabLoginView.as_view(), name="gl-login"), + path("login/bitbucket", BitbucketLoginView.as_view(), name="bitbucket-login"), + path("login/bb", BitbucketLoginView.as_view(), name="bb-login"), + re_path( + r"login/github[-_]enterprise/?", + GithubEnterpriseLoginView.as_view(), + name="github-enterprise-login", + ), + path("login/ghe", GithubEnterpriseLoginView.as_view(), name="ghe-login"), + re_path( + r"login/gitlab[-_]enterprise/?", + GitlabEnterpriseLoginView.as_view(), + name="gitlab-enterprise-login", + ), + path("login/gle", GitlabEnterpriseLoginView.as_view(), name="gle-login"), + re_path( + r"login/bitbucket[-_]server/?", + BitbucketServerLoginView.as_view(), + name="bitbucket-server-login", + ), + path("login/bbs", BitbucketServerLoginView.as_view(), name="bbs-login"), + path("login/stash", BitbucketServerLoginView.as_view(), name="stash-login"), + path("login/sentry", SentryLoginView.as_view(), name="sentry-login"), + path( + "login/okta//", + OktaCloudLoginView.as_view(), + name="okta-cloud-login", + ), + path( + "login/okta/callback", + OktaCloudCallbackView.as_view(), + name="okta-cloud-callback", + ), +] +if settings.OKTA_ISS is not None: + urlpatterns += [path("login/okta", OktaLoginView.as_view(), name="okta-login")] diff --git a/apps/codecov-api/codecov_auth/views/__init__.py b/apps/codecov-api/codecov_auth/views/__init__.py new file mode 100644 index 0000000000..7992bf6347 --- /dev/null +++ b/apps/codecov-api/codecov_auth/views/__init__.py @@ -0,0 +1,3 @@ +from codecov_auth.views.github import GithubLoginView + +__all__ = ["GithubLoginView"] diff --git a/apps/codecov-api/codecov_auth/views/base.py b/apps/codecov-api/codecov_auth/views/base.py new file mode 100644 index 0000000000..2d4a9483b1 --- /dev/null +++ b/apps/codecov-api/codecov_auth/views/base.py @@ -0,0 +1,491 @@ +import logging +import re +import uuid +from functools import reduce +from typing import Any +from urllib.parse import parse_qs, urlencode, urlparse + +from django.conf import settings +from django.contrib.auth import login, logout +from django.contrib.sessions.models import Session as DjangoSession +from django.core.exceptions import PermissionDenied +from django.db import transaction +from django.http.request import HttpRequest +from django.http.response import HttpResponse +from django.utils import timezone +from django.utils.timezone import now +from shared.encryption.token import encode_token +from shared.events.amplitude import AmplitudeEventPublisher +from shared.helpers.redis import get_redis_connection +from shared.license import LICENSE_ERRORS_MESSAGES, get_current_license + +from codecov_auth.models import Owner, OwnerProfile, Session, User +from services.analytics import AnalyticsService +from services.refresh import RefreshService +from utils.config import get_config +from utils.encryption import encryptor +from utils.services import get_long_service_name, get_short_service_name + +log = logging.getLogger(__name__) + + +class StateMixin(object): + """ + Implement the bevavior described here: https://auth0.com/docs/protocols/state-parameters + + - Generating a random string (called state) and storing it in Redis + - Passing that state as argument to the oauth2 provider (eg github) + - The oauth2 provider redirects to Codecov with the same state + - We can verify if this state is in Redis, meaning Codecov generated when starting the redirection + - Additionnally; we store in redis the redirection url after auth passed by the front-end + - On the request callback; we can fetch the redirection url via the state + - If the state is not in Redis; we raise an exception which will return a 400 error + - Right before returning the response; we need to remove the state from Redis so it cannot be used again + + How to use: + + Mixin for a Django ClassBaseView (must have self.request set) + + To generate the state: + - self.generate_state() + -> Will return a state to give to the oauth2 provider. + -> Will also store the redirect url from request.GET['to'] query param. + + To get the redirect url from state: + - self.get_redirection_url_from_state(state) + -> Will return a safe URL to redirect after authentication + -> raise django.core.exceptions.SuspiciousOperation if no state was found + + To remove the state: + - self.remove_state(state, delay=0) + -> Will remove the state from Redis; must be called at the end of the request + -> The delay parameter is the number of second in which the state will be removed + + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + self.redis = get_redis_connection() + super().__init__(*args, **kwargs) + + def _session_key(self) -> str: + return f"{self.service}_oauth_state" + + def _get_key_redis(self, state: str) -> str: + return f"oauth-state-{state}" + + def _is_matching_cors_domains(self, url_domain: str) -> bool: + # make sure the domain is part of the CORS so that's a safe domain to + # redirect to. + if url_domain in settings.CORS_ALLOWED_ORIGINS: + return True + for domain_pattern in settings.CORS_ALLOWED_ORIGIN_REGEXES: + if re.match(domain_pattern, url_domain): + return True + return False + + def _is_valid_redirection(self, to: str) -> bool: + # make sure the redirect url is from a domain we own + try: + url = urlparse(to) + except ValueError: + return False + # the url is only a path without domain, it's valid + only_path = not url.scheme and not url.netloc and url.path + if only_path: + return True + url_domain = f"{url.scheme}://{url.netloc}" + return self._is_matching_cors_domains(url_domain) + + def _generate_redirection_url(self) -> str: + redirection_url = self.request.GET.get("to") + if redirection_url and self._is_valid_redirection(redirection_url): + return redirection_url + return ( + f"{settings.CODECOV_DASHBOARD_URL}/{get_short_service_name(self.service)}" + ) + + def generate_state(self) -> str: + state = uuid.uuid4().hex + redirection_url = self._generate_redirection_url() + self.redis.setex(self._get_key_redis(state), 500, redirection_url) + + # By saving the state in a session cookie, we can ensure that the user + # following the redirection URL after OAuth authorization is the same + # as the user who initiated it. Otherwise, a trickster could generate + # a final redirect URL to log into their account, send it to some + # victim, and trick the victim into linking their account with the + # trickster's. + self.request.session[self._session_key()] = state + + return state + + def verify_state(self, state: str) -> bool: + state_from_session = self.request.session.get(self._session_key(), None) + return state_from_session and state == state_from_session + + def get_redirection_url_from_state(self, state: str) -> tuple[str, bool]: + cached_url = self.redis.get(self._get_key_redis(state)) + + if not cached_url: + # we come here after an installation event if the setup url is not set correctly, in that case we usually don't + # have the state set, because that only happens when users try to login, therefore we should just ignore + # this case and redirect them to what the setup url should be + return ( + f"{settings.CODECOV_DASHBOARD_URL}/{get_short_service_name(self.service)}", + False, + ) + + # At this point the git provider has redirected the user back to our + # site. If the state that the git provider relayed in that redirect + # matches the state that we have saved in our session cookie, everything + # is fine and we should return the final redirect URL to complete the + # login. If we're missing that cookie, or if its state doesn't match up, + # we want don't to allow the login. + if not self.verify_state(state): + log.warning( + "Warning: login request is missing state or has disagreeing state" + ) + return ( + f"{settings.CODECOV_DASHBOARD_URL}", + False, + ) + + # Return the final redirect URL to complete the login. + return (cached_url.decode("utf-8"), True) + + def remove_state(self, state: str, delay: int = 0) -> None: + redirection_url, _ = self.get_redirection_url_from_state(state) + if delay == 0: + self.redis.delete(self._get_key_redis(state)) + else: + self.redis.setex(self._get_key_redis(state), delay, redirection_url) + + session_state = self.request.session.get(self._session_key(), None) + if session_state and session_state == state: + self.request.session.pop(self._session_key(), None) + + +class LoginMixin(object): + analytics_service = AnalyticsService() + + def modify_redirection_url_based_on_default_user_org( + self, url: str, owner: Owner + ) -> str: + if ( + url + != f"{settings.CODECOV_DASHBOARD_URL}/{get_short_service_name(self.service)}" + and url + != f"{settings.CODECOV_DASHBOARD_URL}/{get_long_service_name(self.service)}" + ): + return url + + owner_profile = None + if owner: + owner_profile = OwnerProfile.objects.filter(owner_id=owner.ownerid).first() + if owner_profile is not None and owner_profile.default_org is not None: + url += f"/{owner_profile.default_org.username}" + return url + + def get_or_create_org(self, single_organization: dict) -> Owner: + owner, was_created = Owner.objects.get_or_create( + service=self.service, + service_id=single_organization["id"], + defaults={"createstamp": timezone.now()}, + ) + return owner + + def login_owner( + self, owner: Owner, request: HttpRequest, response: HttpResponse + ) -> None: + # if there's a currently authenticated user + if request.user is not None and not request.user.is_anonymous: + if owner.user is None: + # TEMPORARY: We have no mechanism in the UI for supporting multiple + # owners of the same service linked to the same user. If the current + # user is already linked to an owner of the same service as this one then + # we'll logout the current user, create a new user and link the owner to + # that new user. This is not ideal since it creates multiple user records + # for the same person that will need to be merged later on. + if request.user.owners.filter(service=owner.service).exists(): + logout(request) + current_user = User.objects.create( + email=owner.email, + name=owner.name, + is_staff=owner.staff, + ) + owner.user = current_user + owner.save() + login(request, current_user) + else: + # assign the owner to the currently authenticated user + owner.user = request.user + owner.save() + log.info( + "User claimed owner", + extra=dict(user_id=request.user.pk, ownerid=owner.ownerid), + ) + elif request.user != owner.user: + log.warning( + "Owner already linked to another user", + extra=dict(user_id=request.user.pk, ownerid=owner.ownerid), + ) + # TEMPORARY: We may want to handle this better in the future by indicating + # the issue to the user and letting them decide how to proceeed. For now + # we'll just logout the current user and login the user that controls the owner + # that just OAuth-ed. + logout(request) + login(request, owner.user) + return + # else we do not have a currently authenticated user + else: + current_user = None + if owner.user is not None: + current_user = owner.user + else: + # no current user and owner has not already been assigned a user + current_user = User.objects.create( + email=owner.email, + name=owner.name, + is_staff=owner.staff, + ) + owner.user = current_user + owner.save() + + login(request, current_user) + log.info( + "User logged in", + extra=dict(user_id=request.user.pk, ownerid=owner.ownerid), + ) + + request.session["current_owner_id"] = owner.pk + RefreshService().trigger_refresh(owner.ownerid, owner.username) + + self.delete_expired_sessions_and_django_sessions(owner) + self.store_login_session(owner) + + def get_and_modify_owner(self, user_dict: dict, request: HttpRequest) -> Owner: + user_orgs = user_dict["orgs"] + formatted_orgs = [ + dict(username=org["username"], id=str(org["id"])) for org in user_orgs + ] + + self._check_enterprise_organizations_membership(user_dict, formatted_orgs) + upserted_orgs = [self.get_or_create_org(org) for org in formatted_orgs] + + self._check_user_count_limitations(user_dict["user"]) + owner, is_new_user = self._get_or_create_owner(user_dict, request) + fields_to_update = [] + if ( + not get_config(self.service, "student_disabled", default=False) + and user_dict.get("is_student") != owner.student + ): + owner.student = user_dict.get("is_student") + if owner.student_created_at is None: + owner.student_created_at = timezone.now() + owner.student_updated_at = timezone.now() + fields_to_update.extend( + ["student", "student_created_at", "student_updated_at"] + ) + + # Updated by the task `SyncTeams` that is called after login. + # We will only set this for the initial "oranizations is none" login. + if owner.organizations is None: + owner.organizations = [o.ownerid for o in upserted_orgs] + fields_to_update.extend(["organizations"]) + + if owner.bot is not None: + log.info( + "Clearing user bot field", + extra=dict(ownerid=owner.ownerid, old_bot=owner.bot), + ) + owner.bot = None + fields_to_update.append("bot") + + if fields_to_update: + owner.save(update_fields=fields_to_update + ["updatestamp"]) + + return owner + + def _check_enterprise_organizations_membership( + self, user_dict: dict, orgs: list[dict] + ) -> None: + """Checks if a user belongs to the restricted organizations (or teams if GitHub) allowed in settings.""" + if settings.IS_ENTERPRISE and get_config(self.service, "organizations"): + orgs_in_settings = set(get_config(self.service, "organizations")) + orgs_in_user = {org["username"] for org in orgs} + if not (orgs_in_settings & orgs_in_user): + raise PermissionDenied( + "You must be a member of an organization listed in the Codecov Enterprise setup." + ) + if get_config(self.service, "teams") and "teams" in user_dict: + teams_in_settings = set(get_config(self.service, "teams")) + teams_in_user = {team["name"] for team in user_dict["teams"]} + if not (teams_in_settings & teams_in_user): + raise PermissionDenied( + "You must be a member of an allowed team in your organization." + ) + + def _check_user_count_limitations(self, login_data: dict) -> None: + if not settings.IS_ENTERPRISE: + return + license = get_current_license() + if not license.is_valid: + return + + try: + user_logging_in_if_exists = Owner.objects.get( + service=f"{self.service}", service_id=login_data["id"] + ) + except Owner.DoesNotExist: + user_logging_in_if_exists = None + + if license.number_allowed_users: + if license.is_pr_billing: + # User is consuming seat if found in _any_ owner's plan_activated_users + is_consuming_seat = user_logging_in_if_exists and Owner.objects.filter( + plan_activated_users__contains=[user_logging_in_if_exists.ownerid] + ) + if not is_consuming_seat: + owners_with_activated_users = Owner.objects.exclude( + plan_activated_users__len=0 + ).exclude(plan_activated_users__isnull=True) + all_distinct_actiaved_users: set[str] = reduce( + lambda acc, curr: set(curr.plan_activated_users) | acc, + owners_with_activated_users, + set(), + ) + if len(all_distinct_actiaved_users) > license.number_allowed_users: + raise PermissionDenied( + LICENSE_ERRORS_MESSAGES["users-exceeded"] + ) + elif not user_logging_in_if_exists or ( + user_logging_in_if_exists and not user_logging_in_if_exists.oauth_token + ): + users_on_service_count = Owner.objects.filter( + oauth_token__isnull=False, service=f"{self.service}" + ).count() + if users_on_service_count > license.number_allowed_users: + raise PermissionDenied(LICENSE_ERRORS_MESSAGES["users-exceeded"]) + + def _get_or_create_owner( + self, user_dict: dict, request: HttpRequest + ) -> tuple[Owner, bool]: + fields_to_update = ["oauth_token", "private_access", "updatestamp"] + login_data = user_dict["user"] + owner, was_created = Owner.objects.get_or_create( + service=f"{self.service}", + service_id=login_data["id"], + defaults={"createstamp": timezone.now()}, + ) + if login_data["login"] != owner.username: + fields_to_update.append("username") + owner.username = login_data["login"] + + owner.oauth_token = encryptor.encode(encode_token(login_data)).decode() + owner.private_access = user_dict["has_private_access"] + if user_dict["user"].get("name"): + owner.name = user_dict["user"]["name"] + fields_to_update.append("name") + + if user_dict["user"].get("email"): + owner.email = user_dict["user"].get("email") + fields_to_update.append("email") + + owner.save(update_fields=fields_to_update) + + marketing_tags = self.retrieve_marketing_tags_from_cookie() + amplitude = AmplitudeEventPublisher() + if was_created: + self.analytics_service.user_signed_up(owner, **marketing_tags) + amplitude.publish("User Created", {"user_ownerid": owner.ownerid}) + else: + self.analytics_service.user_signed_in(owner, **marketing_tags) + amplitude.publish("User Logged in", {"user_ownerid": owner.ownerid}) + orgs = owner.organizations + amplitude.publish( + "set_orgs", + { + "user_ownerid": owner.ownerid, + "org_ids": orgs if orgs is not None else [], + }, + ) + + return (owner, was_created) + + # below are functions to save marketing UTM params to cookie to retrieve them + # on the oauth callback for the tracking functions + def _get_utm_params(self, params: dict) -> dict: + filtered_params = { + "utm_department": params.get("utm_department", None), + "utm_campaign": params.get("utm_campaign", None), + "utm_medium": params.get("utm_medium", None), + "utm_source": params.get("utm_source", None), + "utm_content": params.get("utm_content", None), + "utm_term": params.get("utm_term", None), + } + # remove None values from the dict + return {k: v for k, v in filtered_params.items() if v is not None} + + def store_to_cookie_utm_tags(self, response: HttpResponse) -> None: + if not settings.IS_ENTERPRISE: + data = urlencode(self._get_utm_params(self.request.GET)) + response.set_cookie( + "_marketing_tags", + data, + max_age=86400, # Same as state validatiy + httponly=True, + domain=settings.COOKIES_DOMAIN, + ) + + def retrieve_marketing_tags_from_cookie(self) -> dict: + if not settings.IS_ENTERPRISE: + cookie_data = self.request.COOKIES.get("_marketing_tags", "") + params_as_dict = parse_qs(cookie_data) + filtered_params = self._get_utm_params(params_as_dict) + return {k: v[0] for k, v in filtered_params.items()} + else: + return {} + + def store_login_session(self, owner: Owner) -> None: + # Store user's login session info after logging in + http_x_forwarded_for = self.request.META.get("HTTP_X_FORWARDED_FOR") + if http_x_forwarded_for: + ip = http_x_forwarded_for.split(",")[0] + else: + ip = self.request.META.get("REMOTE_ADDR") + + login_session = DjangoSession.objects.filter( + session_key=self.request.session.session_key + ).first() + + Session.objects.create( + lastseen=timezone.now(), + useragent=self.request.META.get("HTTP_USER_AGENT"), + ip=ip, + login_session=login_session, + type=Session.SessionType.LOGIN, + owner=owner, + ) + + def delete_expired_sessions_and_django_sessions(self, owner: Owner) -> None: + """ + This function deletes expired login sessions for a given owner + """ + with transaction.atomic(): + # Get the primary keys of expired DjangoSessions for the given owner + expired_sessions = Session.objects.filter( + owner=owner, + type="login", + login_session__isnull=False, + login_session__expire_date__lt=now(), + ) + + # Delete the rows in the Session table using sessionid + Session.objects.filter( + sessionid__in=[es.sessionid for es in expired_sessions] + ).delete() + + # Delete the rows in the DjangoSession table using the extracted keys + DjangoSession.objects.filter( + session_key__in=[es.login_session for es in expired_sessions] + ).delete() diff --git a/apps/codecov-api/codecov_auth/views/bitbucket.py b/apps/codecov-api/codecov_auth/views/bitbucket.py new file mode 100644 index 0000000000..a204e237cd --- /dev/null +++ b/apps/codecov-api/codecov_auth/views/bitbucket.py @@ -0,0 +1,126 @@ +import base64 +import logging +from urllib.parse import urlencode + +from asgiref.sync import async_to_sync +from django.conf import settings +from django.shortcuts import redirect +from django.urls import reverse +from django.views import View +from shared.django_apps.codecov_metrics.service.codecov_metrics import ( + UserOnboardingMetricsService, +) +from shared.torngit import Bitbucket +from shared.torngit.exceptions import TorngitServerFailureError + +from codecov_auth.views.base import LoginMixin +from utils.encryption import encryptor + +log = logging.getLogger(__name__) + + +class BitbucketLoginView(View, LoginMixin): + service = "bitbucket" + + @async_to_sync + async def fetch_user_data(self, token): + repo_service = Bitbucket( + oauth_consumer_token=dict( + key=settings.BITBUCKET_CLIENT_ID, + secret=settings.BITBUCKET_CLIENT_SECRET, + ), + token=token, + ) + user_data = await repo_service.get_authenticated_user() + authenticated_user = { + "key": token["key"], + "secret": token["secret"], + "id": user_data["uuid"][1:-1], + "login": user_data.pop("username"), + } + user_orgs = await repo_service.list_teams() + return dict( + user=authenticated_user, + orgs=user_orgs, + is_student=False, + has_private_access=True, + ) + + def redirect_to_bitbucket_step(self, request): + repo_service = Bitbucket( + oauth_consumer_token=dict( + key=settings.BITBUCKET_CLIENT_ID, + secret=settings.BITBUCKET_CLIENT_SECRET, + ) + ) + oauth_token_pair = repo_service.generate_request_token( + settings.BITBUCKET_REDIRECT_URI + ) + oauth_token = oauth_token_pair["oauth_token"] + oauth_token_secret = oauth_token_pair["oauth_token_secret"] + url_params = urlencode({"oauth_token": oauth_token}) + url_to_redirect = f"{Bitbucket._OAUTH_AUTHORIZE_URL}?{url_params}" + response = redirect(url_to_redirect) + data = ( + base64.b64encode(oauth_token.encode()) + + b"|" + + base64.b64encode(oauth_token_secret.encode()) + ).decode() + response.set_signed_cookie( + "_oauth_request_token", + encryptor.encode(data).decode(), + domain=settings.COOKIES_DOMAIN, + ) + self.store_to_cookie_utm_tags(response) + return response + + def actual_login_step(self, request): + repo_service = Bitbucket( + oauth_consumer_token=dict( + key=settings.BITBUCKET_CLIENT_ID, + secret=settings.BITBUCKET_CLIENT_SECRET, + ) + ) + oauth_verifier = request.GET.get("oauth_verifier") + request_cookie = request.get_signed_cookie("_oauth_request_token", default=None) + if not request_cookie: + log.warning( + "Request arrived with proper url params but not the proper cookies" + ) + return redirect(reverse("bitbucket-login")) + request_cookie = encryptor.decode(request_cookie) + cookie_key, cookie_secret = [ + base64.b64decode(i).decode() for i in request_cookie.split("|") + ] + token = repo_service.generate_access_token( + cookie_key, cookie_secret, oauth_verifier + ) + user_dict = self.fetch_user_data(token) + user = self.get_and_modify_owner(user_dict, request) + redirection_url = settings.CODECOV_DASHBOARD_URL + "/bb" + redirection_url = self.modify_redirection_url_based_on_default_user_org( + redirection_url, user + ) + response = redirect(redirection_url) + response.delete_cookie("_oauth_request_token", domain=settings.COOKIES_DOMAIN) + self.login_owner(user, request, response) + log.info("User successfully logged in", extra=dict(ownerid=user.ownerid)) + UserOnboardingMetricsService.create_user_onboarding_metric( + org_id=user.ownerid, event="INSTALLED_APP", payload={"login": "bitbucket"} + ) + return response + + def get(self, request): + if settings.DISABLE_GIT_BASED_LOGIN and request.user.is_anonymous: + return redirect(f"{settings.CODECOV_DASHBOARD_URL}/login") + + try: + if request.GET.get("oauth_verifier"): + log.info("Logging into bitbucket after authorization") + return self.actual_login_step(request) + else: + log.info("Redirecting user to bitbucket for authorization") + return self.redirect_to_bitbucket_step(request) + except TorngitServerFailureError: + log.warning("Bitbucket not available for login") + return redirect(reverse("bitbucket-login")) diff --git a/apps/codecov-api/codecov_auth/views/bitbucket_server.py b/apps/codecov-api/codecov_auth/views/bitbucket_server.py new file mode 100644 index 0000000000..de9e5a4c05 --- /dev/null +++ b/apps/codecov-api/codecov_auth/views/bitbucket_server.py @@ -0,0 +1,151 @@ +import base64 +import logging +import threading +from urllib.parse import urlencode + +from asgiref.sync import async_to_sync +from django.conf import settings +from django.shortcuts import redirect +from django.urls import reverse +from django.views import View +from shared.torngit import BitbucketServer +from shared.torngit.exceptions import TorngitServerFailureError + +from codecov_auth.models import SERVICE_BITBUCKET_SERVER +from codecov_auth.views.base import LoginMixin +from utils.encryption import encryptor + +log = logging.getLogger(__name__) + + +class BitbucketServerLoginView(View, LoginMixin): + service = SERVICE_BITBUCKET_SERVER + + async def fetch_user_data(self, token): + repo_service = BitbucketServer( + oauth_consumer_token=dict( + key=settings.BITBUCKET_SERVER_CLIENT_ID, + secret=settings.BITBUCKET_SERVER_CLIENT_SECRET, + ), + token=token, + ) + # Whoami? Get the user + # https://answers.atlassian.com/questions/9379031/answers/9379803 + whoami_url = f"{settings.BITBUCKET_SERVER_URL}/plugins/servlet/applinks/whoami" + username = await repo_service.api("GET", whoami_url) + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp2649152 + user = await repo_service.api("GET", "/users/%s" % username) + + authenticated_user = { + "key": token["key"], + "secret": token["secret"], + "id": user["id"], + "login": user["name"], + } + user_orgs = await repo_service.list_teams() + return dict( + user=authenticated_user, + orgs=user_orgs, + is_student=False, + has_private_access=True, + ) + + async def redirect_to_bitbucket_server_step(self, request): + # And the consumer needs to have the defined client id. The secret is ignored. + # https://developer.atlassian.com/server/jira/platform/oauth/ + repo_service = BitbucketServer( + oauth_consumer_token=dict( + key=settings.BITBUCKET_SERVER_CLIENT_ID, + secret="", + ) + ) + # In this part we make a request for the unauthorized request token. + # Here the user will be redirected to the authorize page and allow our app to be used. + # At the end of this step client will see a screen saying "you have authorized this application. Return to application and click continue." + request_token_url = ( + f"{settings.BITBUCKET_SERVER_URL}/plugins/servlet/oauth/request-token" + ) + request_token = await repo_service.api("POST", request_token_url) + + auth_token = request_token["oauth_token"] + auth_token_secret = request_token["oauth_token_secret"] + + data = ( + base64.b64encode(auth_token.encode()) + + b"|" + + base64.b64encode(auth_token_secret.encode()) + ).decode() + + url_params = urlencode(dict(oauth_token=auth_token)) + authorize_url = f"{settings.BITBUCKET_SERVER_URL}/plugins/servlet/oauth/authorize?{url_params}" + response = redirect(authorize_url) + response.set_signed_cookie( + "_oauth_request_token", + encryptor.encode(data).decode(), + domain=settings.COOKIES_DOMAIN, + ) + self.store_to_cookie_utm_tags(response) + return response + + async def actual_login_step(self, request): + # Retrieve the authorized request_token and create a new client + # This new client has the same consumer as before, but uses the request token. + # ! Each request_token can only be used once + request_cookie = request.get_signed_cookie("_oauth_request_token", default=None) + if not request_cookie: + log.warning( + "Request arrived with proper url params but not the proper cookies" + ) + return redirect(reverse("bbs-login")) + + request_cookie = encryptor.decode(request_cookie) + cookie_key, cookie_secret = [ + base64.b64decode(i).decode() for i in request_cookie.split("|") + ] + token = {"key": cookie_key, "secret": cookie_secret} + repo_service = BitbucketServer( + oauth_consumer_token=dict( + key=settings.BITBUCKET_SERVER_CLIENT_ID, + secret=settings.BITBUCKET_SERVER_CLIENT_SECRET, + ), + token=token, + ) + # Get the access token from the request token + # The access token can be stored and reused. + response = redirect(settings.CODECOV_DASHBOARD_URL + "/bbs") + response.delete_cookie("_oauth_request_token", domain=settings.COOKIES_DOMAIN) + access_token_url = ( + f"{settings.BITBUCKET_SERVER_URL}/plugins/servlet/oauth/access-token" + ) + access_token = await repo_service.api("POST", access_token_url) + auth_token = access_token["oauth_token"] + auth_token_secret = access_token["oauth_token_secret"] + + user_dict = await self.fetch_user_data( + dict(key=auth_token, secret=auth_token_secret) + ) + + def async_login(): + user = self.get_and_modify_owner(user_dict, request) + self.login_owner(user, request, response) + log.info( + "User (async) successfully logged in", extra=dict(ownerid=user.ownerid) + ) + + force_sync = threading.Thread(target=async_login) + force_sync.start() + force_sync.join() + return response + + @async_to_sync + async def get(self, request): + try: + if request.COOKIES.get("_oauth_request_token"): + log.info("Logging into bitbucket_server after authorization") + return await self.actual_login_step(request) + else: + log.info("Redirecting user to bitbucket_server for authorization") + return await self.redirect_to_bitbucket_server_step(request) + except TorngitServerFailureError: + log.warning("Bitbucket Server not available for login") + return redirect(settings.CODECOV_DASHBOARD_URL + "/bbs") diff --git a/apps/codecov-api/codecov_auth/views/github.py b/apps/codecov-api/codecov_auth/views/github.py new file mode 100644 index 0000000000..cdb39d1a62 --- /dev/null +++ b/apps/codecov-api/codecov_auth/views/github.py @@ -0,0 +1,165 @@ +import logging +from typing import Optional +from urllib.parse import urlencode, urljoin + +from asgiref.sync import async_to_sync +from django.conf import settings +from django.shortcuts import redirect +from django.views import View +from shared.django_apps.codecov_metrics.service.codecov_metrics import ( + UserOnboardingMetricsService, +) +from shared.torngit import Github +from shared.torngit.exceptions import TorngitError + +from codecov_auth.views.base import LoginMixin, StateMixin +from utils.config import get_config + +log = logging.getLogger(__name__) + + +class GithubLoginView(LoginMixin, StateMixin, View): + service = "github" + error_redirection_page = "/" + + @property + def repo_service_instance(self): + return Github( + oauth_consumer_token=dict( + key=settings.GITHUB_CLIENT_ID, secret=settings.GITHUB_CLIENT_SECRET + ) + ) + + @property + def redirect_info(self): + return dict(repo_service=Github(), client_id=settings.GITHUB_CLIENT_ID) + + def get_url_to_redirect_to(self, scope): + redirect_info = self.redirect_info + redirect_host = ( + redirect_info["repo_service"].get_service_url() + if redirect_info["repo_service"].get_host_header() is None + else "https://" + redirect_info["repo_service"].get_host_header() + ) + base_url = urljoin(redirect_host, "login/oauth/authorize") + state = self.generate_state() + query = dict( + response_type="code", + scope=",".join(scope), + client_id=redirect_info["client_id"], + state=state, + ) + query_str = urlencode(query) + return f"{base_url}?{query_str}" + + async def _get_teams_data(self, repo_service): + # https://docs.github.com/en/rest/reference/teams#list-teams-for-the-authenticated-user + teams = [] + if settings.IS_ENTERPRISE: + async with repo_service.get_client() as client: + try: + teams = [] + curr_page = 1 + while True: + curr_teams = await repo_service.api( + client, "get", f"/user/teams?per_page=100&page={curr_page}" + ) + teams.extend(curr_teams) + curr_page += 1 + if len(curr_teams) == 0: + break + except TorngitError as exp: + log.error(f"Failed to get GitHub teams information: {exp}") + return teams + + @async_to_sync + async def fetch_user_data(self, code) -> Optional[dict]: + # https://docs.github.com/en/rest/reference/teams#list-teams-for-the-authenticated-user + # This is specific to GitHub + repo_service = self.repo_service_instance + authenticated_user = await repo_service.get_authenticated_user(code) + if "access_token" not in authenticated_user: + log.warning( + "Missing access_token during GitHub OAuth", + extra=dict( + user_info=authenticated_user, + ), + ) + return None + # Comply to torngit's token encoding + authenticated_user["key"] = authenticated_user["access_token"] + user_orgs = await repo_service.list_teams() + student_disabled = get_config(self.service, "student_disabled", default=False) + if not student_disabled: + is_student = await repo_service.is_student() + else: + is_student = False + has_private_access = "repo" in authenticated_user["scope"].split(",") + + teams = await self._get_teams_data(repo_service) + + return dict( + user=authenticated_user, + orgs=user_orgs, + teams=teams, + is_student=is_student, + has_private_access=has_private_access, + ) + + def actual_login_step(self, request): + code = request.GET.get("code") + state = request.GET.get("state") + redirection_url, is_valid = self.get_redirection_url_from_state(state) + if not is_valid: + return redirect(redirection_url) + try: + user_dict = self.fetch_user_data(code) + if user_dict is None: + return redirect(self.error_redirection_page) + except TorngitError: + log.warning("Unable to log in due to problem on Github", exc_info=True) + return redirect(self.error_redirection_page) + owner = self.get_and_modify_owner(user_dict, request) + redirection_url = self.modify_redirection_url_based_on_default_user_org( + redirection_url, owner + ) + response = redirect(redirection_url) + self.login_owner(owner, request, response) + self.remove_state(state) + UserOnboardingMetricsService.create_user_onboarding_metric( + org_id=owner.ownerid, event="INSTALLED_APP", payload={"login": "github"} + ) + return response + + def get(self, request): + if settings.DISABLE_GIT_BASED_LOGIN and request.user.is_anonymous: + return redirect(f"{settings.CODECOV_DASHBOARD_URL}/login") + + if request.GET.get("code"): + return self.actual_login_step(request) + else: + scope = ["user:email", "read:org", "repo:status", "write:repo_hook"] + if ( + settings.IS_ENTERPRISE + or request.COOKIES.get("ghpr") == "true" + or request.GET.get("private") + ): + log.info("Appending repo to scope") + scope.append("repo") + url_to_redirect_to = self.get_url_to_redirect_to(scope) + response = redirect(url_to_redirect_to) + seconds_in_one_year = 365 * 24 * 60 * 60 + domain_to_use = settings.COOKIES_DOMAIN + response.set_cookie( + "ghpr", + "true", + max_age=seconds_in_one_year, + httponly=True, + domain=domain_to_use, + ) + self.store_to_cookie_utm_tags(response) + return response + url_to_redirect_to = self.get_url_to_redirect_to(scope) + response = redirect(url_to_redirect_to) + self.store_to_cookie_utm_tags(response) + return response diff --git a/apps/codecov-api/codecov_auth/views/github_enterprise.py b/apps/codecov-api/codecov_auth/views/github_enterprise.py new file mode 100644 index 0000000000..521e51359c --- /dev/null +++ b/apps/codecov-api/codecov_auth/views/github_enterprise.py @@ -0,0 +1,29 @@ +import logging + +from django.conf import settings +from shared.torngit import GithubEnterprise + +from .github import GithubLoginView + +log = logging.getLogger(__name__) + + +class GithubEnterpriseLoginView(GithubLoginView): + service = "github_enterprise" + error_redirection_page = "/" + + @property + def repo_service_instance(self): + return GithubEnterprise( + oauth_consumer_token=dict( + key=settings.GITHUB_ENTERPRISE_CLIENT_ID, + secret=settings.GITHUB_ENTERPRISE_CLIENT_SECRET, + ) + ) + + @property + def redirect_info(self): + return dict( + repo_service=GithubEnterprise(), + client_id=settings.GITHUB_ENTERPRISE_CLIENT_ID, + ) diff --git a/apps/codecov-api/codecov_auth/views/gitlab.py b/apps/codecov-api/codecov_auth/views/gitlab.py new file mode 100644 index 0000000000..399e7c5ac6 --- /dev/null +++ b/apps/codecov-api/codecov_auth/views/gitlab.py @@ -0,0 +1,103 @@ +import logging +from urllib.parse import urlencode, urljoin +from uuid import uuid4 # noqa: F401 + +from asgiref.sync import async_to_sync +from django.conf import settings +from django.shortcuts import redirect +from django.views import View +from shared.django_apps.codecov_metrics.service.codecov_metrics import ( + UserOnboardingMetricsService, +) +from shared.torngit import Gitlab +from shared.torngit.exceptions import TorngitError + +from codecov_auth.views.base import LoginMixin, StateMixin + +log = logging.getLogger(__name__) + + +class GitlabLoginView(LoginMixin, StateMixin, View): + service = "gitlab" + error_redirection_page = "/" + + @property + def repo_service_instance(self): + return Gitlab( + oauth_consumer_token=dict( + key=settings.GITLAB_CLIENT_ID, secret=settings.GITLAB_CLIENT_SECRET + ) + ) + + @property + def redirect_info(self): + return dict( + redirect_uri=settings.GITLAB_REDIRECT_URI, + repo_service=Gitlab(), + client_id=settings.GITLAB_CLIENT_ID, + ) + + def get_url_to_redirect_to(self): + redirect_info = self.redirect_info + base_url = urljoin(redirect_info["repo_service"].service_url, "oauth/authorize") + state = self.generate_state() + + scope = settings.GITLAB_SCOPE + log.info(f"Gitlab oauth with scope: '{scope}'") + + query = dict( + response_type="code", + client_id=redirect_info["client_id"], + redirect_uri=redirect_info["redirect_uri"], + state=state, + scope=scope, + ) + query_str = urlencode(query) + return f"{base_url}?{query_str}" + + @async_to_sync + async def fetch_user_data(self, request, code): + repo_service = self.repo_service_instance + user_dict = await repo_service.get_authenticated_user(code) + user_dict["login"] = user_dict["username"] + # Comply to torngit's token encoding + user_dict["key"] = user_dict["access_token"] + user_orgs = await repo_service.list_teams() + return dict( + user=user_dict, orgs=user_orgs, is_student=False, has_private_access=True + ) + + def actual_login_step(self, request): + state = request.GET.get("state") + code = request.GET.get("code") + try: + user_dict = self.fetch_user_data(request, code) + except TorngitError: + log.warning("Unable to log in due to problem on Gitlab", exc_info=True) + return redirect(self.error_redirection_page) + user = self.get_and_modify_owner(user_dict, request) + redirection_url, is_valid = self.get_redirection_url_from_state(state) + if not is_valid: + return redirect(redirection_url) + redirection_url = self.modify_redirection_url_based_on_default_user_org( + redirection_url, user + ) + response = redirect(redirection_url) + self.login_owner(user, request, response) + self.remove_state(state, delay=5) + UserOnboardingMetricsService.create_user_onboarding_metric( + org_id=user.ownerid, event="INSTALLED_APP", payload={"login": "gitlab"} + ) + return response + + def get(self, request): + if settings.DISABLE_GIT_BASED_LOGIN and request.user.is_anonymous: + return redirect(f"{settings.CODECOV_DASHBOARD_URL}/login") + + if request.GET.get("code"): + return self.actual_login_step(request) + else: + url_to_redirect_to = self.get_url_to_redirect_to() + response = redirect(url_to_redirect_to) + self.store_to_cookie_utm_tags(response) + return response diff --git a/apps/codecov-api/codecov_auth/views/gitlab_enterprise.py b/apps/codecov-api/codecov_auth/views/gitlab_enterprise.py new file mode 100644 index 0000000000..906900a0d4 --- /dev/null +++ b/apps/codecov-api/codecov_auth/views/gitlab_enterprise.py @@ -0,0 +1,30 @@ +import logging + +from django.conf import settings +from shared.torngit import GitlabEnterprise + +from .gitlab import GitlabLoginView + +log = logging.getLogger(__name__) + + +class GitlabEnterpriseLoginView(GitlabLoginView): + service = "gitlab_enterprise" + error_redirection_page = "/" + + @property + def repo_service_instance(self): + return GitlabEnterprise( + oauth_consumer_token=dict( + key=settings.GITLAB_ENTERPRISE_CLIENT_ID, + secret=settings.GITLAB_ENTERPRISE_CLIENT_SECRET, + ) + ) + + @property + def redirect_info(self): + return dict( + redirect_uri=settings.GITLAB_ENTERPRISE_REDIRECT_URI, + repo_service=GitlabEnterprise(), + client_id=settings.GITLAB_ENTERPRISE_CLIENT_ID, + ) diff --git a/apps/codecov-api/codecov_auth/views/logout.py b/apps/codecov-api/codecov_auth/views/logout.py new file mode 100644 index 0000000000..d82f6c88fb --- /dev/null +++ b/apps/codecov-api/codecov_auth/views/logout.py @@ -0,0 +1,24 @@ +from django.conf import settings +from django.contrib.auth import logout +from django.http import HttpRequest +from django.shortcuts import HttpResponse +from rest_framework.decorators import api_view +from rest_framework.response import Response + + +@api_view(["POST"]) +def logout_view(request: HttpRequest, **kwargs: str) -> HttpResponse: + logout(request) + + response = Response(status=205) + kwargs_cookie = dict( + domain=settings.COOKIES_DOMAIN, samesite=settings.COOKIE_SAME_SITE + ) + response.delete_cookie("staff_user", **kwargs_cookie) + + # temporary as we use to set cookie to Strict SameSite; but we need Lax + # So we need delete in both samesite Strict / Lax for a little while + kwargs_cookie = dict(domain=settings.COOKIES_DOMAIN, samesite="Strict") + response.delete_cookie("staff_user", **kwargs_cookie) + + return response diff --git a/apps/codecov-api/codecov_auth/views/okta.py b/apps/codecov-api/codecov_auth/views/okta.py new file mode 100644 index 0000000000..76931c75b6 --- /dev/null +++ b/apps/codecov-api/codecov_auth/views/okta.py @@ -0,0 +1,142 @@ +import logging + +from django.conf import settings +from django.contrib.auth import login, logout +from django.http import HttpRequest, HttpResponse +from django.shortcuts import redirect +from django.views import View +from requests.auth import HTTPBasicAuth + +from codecov_auth.models import OktaUser, User +from codecov_auth.views.base import LoginMixin +from codecov_auth.views.okta_mixin import ( + ISS_REGEX, + OktaLoginMixin, + OktaTokenResponse, + validate_id_token, +) +from utils.services import get_short_service_name + +log = logging.getLogger(__name__) + +OKTA_BASIC_AUTH = HTTPBasicAuth( + settings.OKTA_OAUTH_CLIENT_ID, settings.OKTA_OAUTH_CLIENT_SECRET +) + + +class OktaLoginView(LoginMixin, OktaLoginMixin, View): + service = "okta" + + def _perform_login(self, request: HttpRequest, iss: str) -> HttpResponse: + code = request.GET.get("code") + state = request.GET.get("state") + + if not self.verify_state(state): + log.warning("Invalid state during Okta login") + return redirect(f"{settings.CODECOV_DASHBOARD_URL}/login") + + user_data: OktaTokenResponse | None = self._fetch_user_data( + iss, code, state, settings.OKTA_OAUTH_REDIRECT_URL, OKTA_BASIC_AUTH + ) + if user_data is None: + log.warning("Unable to log in due to problem on Okta", exc_info=True) + return redirect(f"{settings.CODECOV_DASHBOARD_URL}/login") + + current_user = self._login_user(request, iss, user_data) + + # TEMPORARY: we're assuming a single owner for the time being since there's + # no supporting UI to select which owner you'd like to view + owner = current_user.owners.first() + self.remove_state(state) + if owner is not None: + service = get_short_service_name(owner.service) + response = redirect(f"{settings.CODECOV_DASHBOARD_URL}/{service}") + else: + # user has not connected any owners yet + response = redirect(f"{settings.CODECOV_DASHBOARD_URL}/sync") + + return response + + def _login_user( + self, request: HttpRequest, iss: str, user_data: OktaTokenResponse + ) -> User: + id_token = user_data.id_token + id_payload = validate_id_token(iss, id_token, settings.OKTA_OAUTH_CLIENT_ID) + + okta_id = id_payload.sub + user_email = id_payload.email + user_name = id_payload.name + + okta_user = OktaUser.objects.filter(okta_id=okta_id).first() + + if request.user is not None and not request.user.is_anonymous: + # we're already authenticated + current_user = request.user + + if okta_user and okta_user.user != request.user: + log.warning( + "Okta account already linked to another user", + extra=dict( + current_user_id=request.user.pk, okta_user_id=okta_user.pk + ), + ) + # Logout the current user and login the user who already + # claimed this Okta account (below) + logout(request) + current_user = okta_user.user + else: + # we're not authenticated + if okta_user: + log.info( + "Existing Okta user logging in", + extra=dict(okta_user_id=okta_user.pk), + ) + current_user = okta_user.user + else: + current_user = User.objects.create( + name=user_name, + email=user_email, + ) + + if okta_user is None: + okta_user = OktaUser.objects.create( + user=current_user, + okta_id=okta_id, + name=user_name, + email=user_email, + access_token=user_data.access_token, + ) + log.info( + "Created Okta user", + extra=dict(okta_user_id=okta_user.pk), + ) + + login(request, current_user) + return current_user + + def validate_issuer(self) -> str | None: + """Checks that the issuer is valid. If not, it returns None.""" + iss = settings.OKTA_ISS + if iss is None: + log.warning("Unable to log in due to missing Okta issuer", exc_info=True) + return None + if not ISS_REGEX.match(iss): + log.warning("Invalid Okta issuer") + return None + return iss + + def get(self, request: HttpRequest) -> HttpResponse: + iss = self.validate_issuer() + if iss is None: + return redirect(f"{settings.CODECOV_DASHBOARD_URL}/login") + + if request.GET.get("code"): + return self._perform_login(request, iss) + else: + response = self._redirect_to_consent( + iss=iss, + client_id=settings.OKTA_OAUTH_CLIENT_ID, + oauth_redirect_url=settings.OKTA_OAUTH_REDIRECT_URL, + ) + self.store_to_cookie_utm_tags(response) + return response diff --git a/apps/codecov-api/codecov_auth/views/okta_cloud.py b/apps/codecov-api/codecov_auth/views/okta_cloud.py new file mode 100644 index 0000000000..d9268b3833 --- /dev/null +++ b/apps/codecov-api/codecov_auth/views/okta_cloud.py @@ -0,0 +1,210 @@ +import logging + +from django.conf import settings +from django.http import HttpRequest, HttpResponse +from django.shortcuts import redirect +from django.views import View +from requests.auth import HTTPBasicAuth +from shared.django_apps.codecov_auth.models import Account, OktaSettings, Owner + +from codecov_auth.views.okta_mixin import ( + OktaLoginMixin, + OktaTokenResponse, + validate_id_token, +) + +# The key for accessing the Okta signed in accounts list in the session +OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY = "okta_signed_in_accounts" + +# The key for the currently signing in session in Okta. +# This is so that the callback can reference the orgs/accounts that we're +# signing in for. +OKTA_CURRENT_SESSION = "okta_current_session" + +log = logging.getLogger(__name__) + + +def get_app_redirect_url(org_username: str, service: str) -> str: + """The Codecov app page we redirect users to.""" + return f"{settings.CODECOV_DASHBOARD_URL}/{service}/{org_username}" + + +def get_oauth_redirect_url() -> str: + """The Okta callback URL for us to finish the authentication.""" + return f"{settings.CODECOV_API_URL}/login/okta/callback" + + +def get_okta_settings(organization: Owner) -> OktaSettings | None: + account: Account | None = organization.account + if account: + okta_settings: OktaSettings | None = account.okta_settings.first() + if okta_settings: + return okta_settings + return None + + +class OktaCloudLoginView(OktaLoginMixin, View): + service = "okta_cloud" + + def get( + self, request: HttpRequest, service: str, org_username: str + ) -> HttpResponse: + log_context: dict = {"service": service, "username": org_username} + if not request.user or request.user.is_anonymous: + log.warning( + "User needs to be signed in before authenticating organization with Okta.", + extra=log_context, + ) + return HttpResponse(status=403) + + try: + organization: Owner = Owner.objects.get( + service=service, username=org_username + ) + except Owner.DoesNotExist: + log.warning("The organization doesn't exist.", extra=log_context) + return HttpResponse(status=404) + + okta_settings = get_okta_settings(organization) + if not okta_settings: + log.warning( + "Okta settings not found. Cannot sign into Okta", extra=log_context + ) + return HttpResponse(status=404) + + app_redirect_url = get_app_redirect_url( + organization.username, organization.service + ) + oauth_redirect_url = get_oauth_redirect_url() + + # User is already logged in, redirect them to the org page + if organization.account.id in request.session.get( + OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY, [] + ): + return redirect(app_redirect_url) + + # Otherwise start the process redirect them to the Issuer page to authenticate + else: + consent = self._redirect_to_consent( + iss=okta_settings.url.strip("/ "), + client_id=okta_settings.client_id, + oauth_redirect_url=oauth_redirect_url, + ) + request.session[OKTA_CURRENT_SESSION] = { + "org_ownerid": organization.ownerid, + "okta_settings_id": okta_settings.id, + } + return consent + + +class OktaCloudCallbackView(OktaLoginMixin, View): + service = "okta_cloud" + + def get(self, request: HttpRequest) -> HttpResponse: + current_okta_session: dict[str, int] | None = request.session.get( + OKTA_CURRENT_SESSION + ) + if not current_okta_session: + log.warning("Trying to sign into Okta with no existing sign-in session.") + return HttpResponse(status=403) + + org_owner = Owner.objects.get(ownerid=current_okta_session["org_ownerid"]) + log_context: dict = { + "service": org_owner.service, + "username": org_owner.username, + } + + if not request.user or request.user.is_anonymous: + log.warning( + "User not logged in for Okta callback.", + extra=log_context, + ) + return HttpResponse(status=403) + + try: + okta_settings = OktaSettings.objects.get( + id=current_okta_session["okta_settings_id"] + ) + except OktaSettings.DoesNotExist: + log.warning( + "Okta settings not found. Cannot sign into Okta", extra=log_context + ) + return HttpResponse(status=404) + + app_redirect_url = get_app_redirect_url(org_owner.username, org_owner.service) + oauth_redirect_url = get_oauth_redirect_url() + + # Check for error in the callback + error = request.GET.get("error") + if error: + log.warning( + f"Okta authentication error: {error}", + extra=log_context, + ) + return redirect(f"{app_redirect_url}?error={error}") + + # Redirect URL, need to validate and mark user as logged in + if request.GET.get("code"): + return self._perform_login( + request, + org_owner, + okta_settings, + app_redirect_url, + oauth_redirect_url, + ) + else: + log.warning( + "No code is passed. Invalid callback. Cannot sign into Okta", + extra=log_context, + ) + + return HttpResponse(status=400) + + def _perform_login( + self, + request: HttpRequest, + organization: Owner, + okta_settings: OktaSettings, + app_redirect_url: str, + oauth_redirect_url: str, + ) -> HttpResponse: + code = request.GET.get("code") + state = request.GET.get("state") + + if not self.verify_state(state): + log.warning("Invalid state during Okta login") + return redirect(f"{app_redirect_url}?error=invalid_state") + + issuer: str = okta_settings.url.strip("/ ") + user_data: OktaTokenResponse | None = self._fetch_user_data( + issuer, + code, + state, + oauth_redirect_url, + HTTPBasicAuth(okta_settings.client_id, okta_settings.client_secret), + ) + + if user_data is None: + log.warning("Can't log in. Invalid Okta Token Response", exc_info=True) + return redirect(f"{app_redirect_url}?error=invalid_token_response") + + try: + _ = validate_id_token(issuer, user_data.id_token, okta_settings.client_id) + except Exception as e: + log.warning(f"Invalid ID token: {str(e)}", exc_info=True) + return redirect(f"{app_redirect_url}?error=invalid_id_token") + + self._login_user(request, organization) + + return redirect(app_redirect_url) + + def _login_user(self, request: HttpRequest, organization: Owner) -> None: + """Logging in the user will just mean adding the account to the user's + okta_logged_in_accounts session. + """ + okta_signed_in_accounts: list[int] = request.session.get( + OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY, [] + ) + okta_signed_in_accounts.append(organization.account.id) + request.session[OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY] = okta_signed_in_accounts + return diff --git a/apps/codecov-api/codecov_auth/views/okta_mixin.py b/apps/codecov-api/codecov_auth/views/okta_mixin.py new file mode 100644 index 0000000000..052dc54f4b --- /dev/null +++ b/apps/codecov-api/codecov_auth/views/okta_mixin.py @@ -0,0 +1,112 @@ +import json +import logging +import re +from urllib.parse import urlencode + +import jwt +import pydantic +import requests +from django.http import HttpResponse +from django.shortcuts import redirect +from requests.auth import HTTPBasicAuth + +from codecov_auth.views.base import StateMixin + +log = logging.getLogger(__name__) + +ISS_REGEX = re.compile(r"https://[\w\d\-\_]+.okta.com/?") + + +class OktaTokenResponse(pydantic.BaseModel): + """This model serializes the response from Okta's oauth/v1/token endpoint. + ref: https://developer.okta.com/docs/reference/api/oidc/#token + + Keeping reference to only the fields that are used. + """ + + access_token: str + id_token: str # this will be present since we requested the `oidc` scope + + +class OktaIdTokenPayload(pydantic.BaseModel): + """Serializes the ID Payload from Okta's id_token deserialization. + ref: https://developer.okta.com/docs/reference/api/oidc/#id-token + """ + + aud: str + iss: str + sub: str + email: str + name: str + + +def validate_id_token(iss: str, id_token: str, client_id: str) -> OktaIdTokenPayload: + res = requests.get(f"{iss}/oauth2/v1/keys") + jwks = res.json() + + public_keys = {} + for jwk in jwks["keys"]: + kid = jwk["kid"] + public_keys[kid] = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk)) + + kid = jwt.get_unverified_header(id_token)["kid"] + key = public_keys[kid] + + id_payload = jwt.decode( + id_token, + key=key, + algorithms=["RS256"], + audience=client_id, + ) + id_token_payload = OktaIdTokenPayload(**id_payload) + assert id_token_payload.iss == iss + assert id_token_payload.aud == client_id + + return id_token_payload + + +class OktaLoginMixin(StateMixin): + def _fetch_user_data( + self, + iss: str, + code: str, + state: str, + redirect_url: str, + auth: HTTPBasicAuth, + ) -> OktaTokenResponse | None: + res = requests.post( + f"{iss}/oauth2/v1/token", + auth=auth, + data={ + "grant_type": "authorization_code", + "code": code, + "redirect_uri": redirect_url, + "state": state, + }, + ) + + if not self.verify_state(state): + log.warning("Invalid state during Okta OAuth") + return None + + if res.status_code >= 400: + return None + + return OktaTokenResponse(**res.json()) + + def _redirect_to_consent( + self, iss: str, client_id: str, oauth_redirect_url: str + ) -> HttpResponse: + state = self.generate_state() + qs = urlencode( + dict( + response_type="code", + client_id=client_id, + scope="openid email profile", + redirect_uri=oauth_redirect_url, + state=state, + ) + ) + redirect_url = f"{iss}/oauth2/v1/authorize?{qs}" + response = redirect(redirect_url) + return response diff --git a/apps/codecov-api/codecov_auth/views/sentry.py b/apps/codecov-api/codecov_auth/views/sentry.py new file mode 100644 index 0000000000..a05a903c1f --- /dev/null +++ b/apps/codecov-api/codecov_auth/views/sentry.py @@ -0,0 +1,184 @@ +import logging +from typing import Dict, Optional +from urllib.parse import urlencode + +import jwt +import requests +from django.conf import settings +from django.contrib.auth import login, logout +from django.http import HttpRequest, HttpResponse +from django.shortcuts import redirect +from django.views import View + +from codecov_auth.models import SentryUser, User +from codecov_auth.views.base import LoginMixin, StateMixin +from utils.services import get_short_service_name + +log = logging.getLogger(__name__) + + +OAUTH_AUTHORIZE_URL = "https://sentry.io/oauth/authorize" +OAUTH_TOKEN_URL = ( + "https://sentry.io/oauth/token/" # seems to require the trailing slash +) + + +class SentryLoginView(LoginMixin, StateMixin, View): + service = "sentry" + + def _fetch_user_data(self, code: str, state: str) -> Optional[Dict]: + res = requests.post( + OAUTH_TOKEN_URL, + data={ + "grant_type": "authorization_code", + "client_id": settings.SENTRY_OAUTH_CLIENT_ID, + "client_secret": settings.SENTRY_OAUTH_CLIENT_SECRET, + "code": code, + "state": state, + }, + ) + + if res.status_code >= 400: + return None + + if not self.verify_state(state): + log.warning("Invalid state during Sentry OAuth") + return None + + return res.json() + + def _redirect_to_consent(self) -> HttpResponse: + state = self.generate_state() + qs = urlencode( + dict( + response_type="code", + client_id=settings.SENTRY_OAUTH_CLIENT_ID, + scope="openid email profile", + state=state, + ) + ) + redirect_url = f"{OAUTH_AUTHORIZE_URL}?{qs}" + response = redirect(redirect_url) + self.store_to_cookie_utm_tags(response) + return response + + def _verify_id_token(self, id_token: str) -> bool: + try: + id_payload = jwt.decode( + id_token, + settings.SENTRY_OIDC_SHARED_SECRET, + algorithms=["HS256"], + audience=settings.SENTRY_OAUTH_CLIENT_ID, + ) + + if id_payload["iss"] != "https://sentry.io": + log.warning( + "Invalid issuer of OIDC ID token", + exc_info=True, + extra=dict( + id_payload=id_payload, + ), + ) + return False + + return True + except jwt.exceptions.InvalidSignatureError: + id_payload = jwt.decode(id_token, options={"verify_signature": False}) + log.warning( + "Unable to verify signature of OIDC ID token", + exc_info=True, + extra=dict( + id_payload=id_payload, + ), + ) + return False + + def _perform_login(self, request: HttpRequest) -> HttpResponse: + code = request.GET.get("code") + state = request.GET.get("state") + + if not self.verify_state(state): + log.warning("Invalid state during Sentry OAuth") + return redirect(f"{settings.CODECOV_DASHBOARD_URL}/login") + + user_data = self._fetch_user_data(code, state) + if user_data is None: + log.warning("Unable to log in due to problem on Sentry", exc_info=True) + return redirect(f"{settings.CODECOV_DASHBOARD_URL}/login") + + if not self._verify_id_token(user_data["id_token"]): + return redirect(f"{settings.CODECOV_DASHBOARD_URL}/login") + + current_user = self._login_user(request, user_data) + + # TEMPORARY: we're assuming a single owner for the time being since there's + # no supporting UI to select which owner you'd like to view + owner = current_user.owners.first() + self.remove_state(state) + + if owner is not None: + service = get_short_service_name(owner.service) + return redirect(f"{settings.CODECOV_DASHBOARD_URL}/{service}") + else: + # user has not connected any owners yet + return redirect(f"{settings.CODECOV_DASHBOARD_URL}/sync") + + def _login_user(self, request: HttpRequest, user_data: dict) -> User: + sentry_id = user_data["user"]["id"] + user_name = user_data["user"].get("name") + user_email = user_data["user"].get("email") + + sentry_user = SentryUser.objects.filter(sentry_id=sentry_id).first() + + current_user = None + if request.user is not None and not request.user.is_anonymous: + # we're already authenticated + current_user = request.user + + if sentry_user and sentry_user.user != request.user: + log.warning( + "Sentry account already linked to another user", + extra=dict( + current_user_id=request.user.pk, sentry_user_id=sentry_user.pk + ), + ) + # Logout the current user and login the user who already + # claimed this Sentry account (below) + logout(request) + current_user = sentry_user.user + else: + # we're not authenticated + if sentry_user: + log.info( + "Existing Sentry user logging in", + extra=dict(sentry_user_id=sentry_user.pk), + ) + current_user = sentry_user.user + else: + current_user = User.objects.create( + name=user_name, + email=user_email, + ) + + if sentry_user is None: + sentry_user = SentryUser.objects.create( + user=current_user, + sentry_id=sentry_id, + name=user_name, + email=user_email, + access_token=user_data["access_token"], + refresh_token=user_data["refresh_token"], + ) + log.info( + "Created Sentry user", + extra=dict(sentry_user_id=sentry_user.pk), + ) + + login(request, current_user) + return current_user + + def get(self, request: HttpRequest) -> HttpResponse: + if request.GET.get("code"): + return self._perform_login(request) + else: + return self._redirect_to_consent() diff --git a/apps/codecov-api/compare/__init__.py b/apps/codecov-api/compare/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/compare/admin.py b/apps/codecov-api/compare/admin.py new file mode 100644 index 0000000000..cbd8d78b37 --- /dev/null +++ b/apps/codecov-api/compare/admin.py @@ -0,0 +1,42 @@ +from django.contrib import admin + +from .models import CommitComparison + + +@admin.register(CommitComparison) +class CommitComparisonAdmin(admin.ModelAdmin): + list_display = ( + "get_base_commit", + "get_compare_commit", + "get_repo_name", + "state", + "created_at", + "updated_at", + ) + + def get_base_commit(self, obj): + return obj.base_commit.commitid + + get_base_commit.short_description = "Base Commit Sha" + + def get_compare_commit(self, obj): + return obj.compare_commit.commitid + + get_compare_commit.short_description = "Compare Commit Sha" + + def get_repo_name(self, obj): + return obj.base_commit.repository.name + + get_repo_name.short_description = "Repository name" + + def get_queryset(self, request): + qs = super().get_queryset(request) + return qs.select_related( + "base_commit", "compare_commit", "base_commit__repository" + ).defer("base_commit___report", "compare_commit___report") + + def has_add_permission(self, *args, **kwargs): + return False + + def has_delete_permission(self, *args, **kwargs): + return True diff --git a/apps/codecov-api/compare/commands/__init__.py b/apps/codecov-api/compare/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/compare/commands/compare/__init__.py b/apps/codecov-api/compare/commands/compare/__init__.py new file mode 100644 index 0000000000..efe8c26c2f --- /dev/null +++ b/apps/codecov-api/compare/commands/compare/__init__.py @@ -0,0 +1,3 @@ +from .compare import CompareCommands + +__all__ = ["CompareCommands"] diff --git a/apps/codecov-api/compare/commands/compare/compare.py b/apps/codecov-api/compare/commands/compare/compare.py new file mode 100644 index 0000000000..9f62947dcf --- /dev/null +++ b/apps/codecov-api/compare/commands/compare/compare.py @@ -0,0 +1,16 @@ +from codecov.commands.base import BaseCommand +from services.comparison import Comparison, ComparisonReport + +from .interactors.fetch_impacted_files import FetchImpactedFiles + + +class CompareCommands(BaseCommand): + def fetch_impacted_files( + self, + comparison_report: ComparisonReport, + comparison: Comparison, + filters, + ): + return self.get_interactor(FetchImpactedFiles).execute( + comparison_report, comparison, filters + ) diff --git a/apps/codecov-api/compare/commands/compare/interactors/__init__.py b/apps/codecov-api/compare/commands/compare/interactors/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/compare/commands/compare/interactors/fetch_impacted_files.py b/apps/codecov-api/compare/commands/compare/interactors/fetch_impacted_files.py new file mode 100644 index 0000000000..472a61a24f --- /dev/null +++ b/apps/codecov-api/compare/commands/compare/interactors/fetch_impacted_files.py @@ -0,0 +1,142 @@ +import enum +from typing import List, Optional + +from shared.utils.match import Matcher + +import services.components as components +from codecov.commands.base import BaseInteractor +from services.comparison import Comparison, ComparisonReport, ImpactedFile +from services.report import files_belonging_to_flags + + +class ImpactedFileParameter(enum.Enum): + FILE_NAME = "file_name" + CHANGE_COVERAGE = "change_coverage" + HEAD_COVERAGE = "head_coverage" + MISSES_COUNT = "misses_count" + PATCH_COVERAGE = "patch_coverage" + + +class FetchImpactedFiles(BaseInteractor): + def _apply_filters( + self, + impacted_files: Optional[List[ImpactedFile]], + comparison: Comparison, + filters, + ): + parameter = filters.get("ordering", {}).get("parameter") + direction = filters.get("ordering", {}).get("direction") + if parameter and direction: + impacted_files = self.sort_impacted_files( + impacted_files, parameter, direction + ) + + if not comparison: + return impacted_files + + flags_filter = filters.get("flags", []) + components_filter = filters.get("components", []) + + components_paths = [] + components_flags = [] + + head_commit_report = comparison.head_report_without_applied_diff + report_flags = head_commit_report and head_commit_report.get_flag_names() + if components_filter: + all_components = components.commit_components( + comparison.head_commit, comparison.user + ) + filtered_components = components.filter_components_by_name_or_id( + all_components, components_filter + ) + for component in filtered_components: + components_paths.extend(component.paths) + components_flags.extend(component.get_matching_flags(report_flags)) + + # Flags & Components intersection + if components_flags: + if flags_filter: + flags_filter = list(set(flags_filter) & set(components_flags)) + else: + flags_filter = components_flags + + if flags_filter: + if set(flags_filter) & set(report_flags): + files = files_belonging_to_flags( + commit_report=head_commit_report, flags=flags_filter + ) + + impacted_files = [ + file for file in impacted_files if file.head_name in files + ] + + res = impacted_files + + if components_paths: + matcher = Matcher(components_paths) + res = [file for file in impacted_files if matcher.match(file.head_name)] + return res + + def get_attribute( + self, impacted_file: ImpactedFile, parameter: ImpactedFileParameter + ): + if parameter == ImpactedFileParameter.FILE_NAME: + return impacted_file.file_name + elif parameter == ImpactedFileParameter.CHANGE_COVERAGE: + return impacted_file.change_coverage + elif parameter == ImpactedFileParameter.HEAD_COVERAGE: + if impacted_file.head_coverage is not None: + return impacted_file.head_coverage.coverage + elif parameter == ImpactedFileParameter.MISSES_COUNT: + if impacted_file.misses_count is not None: + return impacted_file.misses_count + elif parameter == ImpactedFileParameter.PATCH_COVERAGE: + if impacted_file.patch_coverage is not None: + return impacted_file.patch_coverage.coverage + else: + raise ValueError(f"invalid impacted file parameter: {parameter}") + + def sort_impacted_files(self, impacted_files, parameter, direction): + """ + Sorts the impacted files by any provided parameter and slides items with None values to the end + """ + # Separate impacted files with None values for the specified parameter value + files_with_coverage = [] + files_without_coverage = [] + for file in impacted_files: + if self.get_attribute(file, parameter) is not None: + files_with_coverage.append(file) + else: + files_without_coverage.append(file) + + # Sort impacted_files list based on parameter value + is_reversed = direction.value == "descending" + files_with_coverage = sorted( + files_with_coverage, + key=lambda x: self.get_attribute(x, parameter), + reverse=is_reversed, + ) + + # Merge both lists together + return files_with_coverage + files_without_coverage + + def execute( + self, + comparison_report: ComparisonReport, + comparison: Comparison, + filters, + ): + if filters is None: + return comparison_report.impacted_files + + has_unintended_changes = filters.get("has_unintended_changes") + if has_unintended_changes is not None: + impacted_files = ( + comparison_report.impacted_files_with_unintended_changes + if has_unintended_changes + else comparison_report.impacted_files_with_direct_changes + ) + else: + impacted_files = comparison_report.impacted_files + + return self._apply_filters(impacted_files, comparison, filters) diff --git a/apps/codecov-api/compare/commands/compare/interactors/tests/__init__.py b/apps/codecov-api/compare/commands/compare/interactors/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/compare/commands/compare/interactors/tests/test_fetch_impacted_files.py b/apps/codecov-api/compare/commands/compare/interactors/tests/test_fetch_impacted_files.py new file mode 100644 index 0000000000..ec36e936e4 --- /dev/null +++ b/apps/codecov-api/compare/commands/compare/interactors/tests/test_fetch_impacted_files.py @@ -0,0 +1,800 @@ +import enum +from unittest.mock import PropertyMock, patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine +from shared.utils.sessions import Session + +from compare.commands.compare.interactors.fetch_impacted_files import ( + ImpactedFileParameter, +) +from compare.tests.factories import CommitComparisonFactory +from services.comparison import Comparison, ComparisonReport, PullRequestComparison +from services.components import Component + +from ..fetch_impacted_files import FetchImpactedFiles + + +class OrderingDirection(enum.Enum): + ASC = "ascending" + DESC = "descending" + + +mock_data_without_misses = """ +{ + "files": [{ + "head_name": "fileA", + "base_name": "fileA", + "head_coverage": { + "hits": 10, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [], + "unexpected_line_changes": [] + }, + { + "head_name": "fileB", + "base_name": "fileB", + "head_coverage": { + "hits": 12, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [ + [9,"h"], + [10,"m"], + [13,"p"], + [14,"h"], + [15,"h"], + [16,"h"], + [17,"h"] + ], + "unexpected_line_changes": [] + }] +} +""" + + +mock_data_from_archive = """ +{ + "files": [{ + "head_name": "fileA", + "base_name": "fileA", + "head_coverage": { + "hits": 10, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [ + [9,"h"], + [2,"m"], + [3,"m"], + [13,"p"], + [14,"h"], + [15,"h"], + [16,"h"], + [17,"h"] + ], + "unexpected_line_changes": [[[1, "h"], [1, "h"]]] + }, + { + "head_name": "fileB", + "base_name": "fileB", + "head_coverage": { + "hits": 12, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [ + [9,"h"], + [10,"m"], + [13,"p"], + [14,"h"], + [15,"h"], + [16,"h"], + [17,"h"] + ], + "unexpected_line_changes": [[[1, "h"], [1, "m"]], [[2, "h"], [2, "m"]]] + }] +} +""" + +mocked_files_with_direct_and_indirect_changes = """ +{ + "files": [{ + "head_name": "fileA", + "base_name": "fileA", + "head_coverage": { + "hits": 10, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [ + [9,"h"], + [2,"m"], + [3,"m"], + [13,"p"], + [14,"h"], + [15,"h"], + [16,"h"], + [17,"h"] + ], + "unexpected_line_changes": [[[1, "h"], [1, "h"]]] + }, + { + "head_name": "fileB", + "base_name": "fileB", + "head_coverage": { + "hits": 12, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [ + [9,"h"], + [10,"m"], + [13,"p"], + [14,"h"], + [15,"h"], + [16,"h"], + [17,"h"] + ], + "unexpected_line_changes": [] + }, + { + "head_name": "fileC", + "base_name": "fileC", + "head_coverage": { + "hits": 12, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [], + "unexpected_line_changes": [[[1, "h"], [1, "h"]]] + }] +} +""" + + +mocked_component_files_with_direct_and_indirect_changes = """ +{ + "files": [{ + "head_name": "fileA.py", + "base_name": "fileA.py", + "head_coverage": { + "hits": 10, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [ + [9,"h"], + [2,"m"], + [3,"m"], + [13,"p"], + [14,"h"], + [15,"h"], + [16,"h"], + [17,"h"] + ], + "unexpected_line_changes": [[[1, "h"], [1, "h"]]] + }, + { + "head_name": "fileB.js", + "base_name": "fileB.js", + "head_coverage": { + "hits": 12, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [ + [9,"h"], + [10,"m"], + [13,"p"], + [14,"h"], + [15,"h"], + [16,"h"], + [17,"h"] + ], + "unexpected_line_changes": [] + }] +} +""" + + +class FetchImpactedFilesTest(TestCase): + def setUp(self): + self.user = OwnerFactory(username="codecov-user") + self.parent_commit = CommitFactory() + self.commit = CommitFactory( + parent_commit_id=self.parent_commit.commitid, + repository=self.parent_commit.repository, + ) + self.commit_comparison = CommitComparisonFactory( + base_commit=self.parent_commit, + compare_commit=self.commit, + report_storage_path="v4/test.json", + ) + self.comparison_report = ComparisonReport(self.commit_comparison) + + # helper to execute the interactor + def execute(self, owner, *args): + service = owner.service if owner else "github" + return FetchImpactedFiles(owner, service).execute(*args) + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_file_sort_function(self, read_file): + read_file.return_value = mock_data_from_archive + parameter = ImpactedFileParameter.CHANGE_COVERAGE + direction = OrderingDirection.ASC + filters = {"ordering": {"parameter": parameter, "direction": direction}} + comparison = None + sorted_files = self.execute(None, self.comparison_report, comparison, filters) + assert [file.head_name for file in sorted_files] == ["fileA", "fileB"] + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_file_sort_function_no_misses(self, read_file): + read_file.return_value = mock_data_without_misses + parameter = ImpactedFileParameter.MISSES_COUNT + direction = OrderingDirection.ASC + filters = {"ordering": {"parameter": parameter, "direction": direction}} + comparison = None + sorted_files = self.execute(None, self.comparison_report, comparison, filters) + assert [file.head_name for file in sorted_files] == ["fileA", "fileB"] + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_file_sort_function_error(self, read_file): + read_file.return_value = mock_data_from_archive + parameter = "something else" + direction = OrderingDirection.DESC + filters = {"ordering": {"parameter": parameter, "direction": direction}} + comparison = None + + with self.assertRaises(ValueError) as ctx: + self.execute(None, self.comparison_report, comparison, filters) + self.assertEqual( + "invalid impacted file parameter: something else", str(ctx.exception) + ) + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_change_coverage_ascending(self, read_file): + read_file.return_value = mock_data_from_archive + filters = { + "ordering": { + "direction": OrderingDirection.ASC, + "parameter": ImpactedFileParameter.CHANGE_COVERAGE, + } + } + comparison = None + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + assert [file.head_name for file in impacted_files] == ["fileA", "fileB"] + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_change_coverage_descending(self, read_file): + read_file.return_value = mock_data_from_archive + filters = { + "ordering": { + "direction": OrderingDirection.DESC, + "parameter": ImpactedFileParameter.CHANGE_COVERAGE, + } + } + comparison = None + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + assert [file.head_name for file in impacted_files] == ["fileB", "fileA"] + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_head_coverage_ascending(self, read_file): + read_file.return_value = mock_data_from_archive + filters = { + "ordering": { + "direction": OrderingDirection.ASC, + "parameter": ImpactedFileParameter.HEAD_COVERAGE, + } + } + comparison = None + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + assert [file.head_name for file in impacted_files] == ["fileA", "fileB"] + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_patch_coverage_ascending(self, read_file): + read_file.return_value = mock_data_from_archive + filters = { + "ordering": { + "direction": OrderingDirection.ASC, + "parameter": ImpactedFileParameter.PATCH_COVERAGE, + } + } + comparison = None + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + assert [file.head_name for file in impacted_files] == ["fileA", "fileB"] + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_patch_coverage_descending(self, read_file): + read_file.return_value = mock_data_from_archive + filters = { + "ordering": { + "direction": OrderingDirection.DESC, + "parameter": ImpactedFileParameter.PATCH_COVERAGE, + } + } + comparison = None + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + assert [file.head_name for file in impacted_files] == ["fileB", "fileA"] + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_head_coverage_descending(self, read_file): + read_file.return_value = mock_data_from_archive + filters = { + "ordering": { + "direction": OrderingDirection.DESC, + "parameter": ImpactedFileParameter.HEAD_COVERAGE, + } + } + comparison = None + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + assert [file.head_name for file in impacted_files] == ["fileB", "fileA"] + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_head_name_ascending(self, read_file): + read_file.return_value = mock_data_from_archive + filters = { + "ordering": { + "direction": OrderingDirection.ASC, + "parameter": ImpactedFileParameter.FILE_NAME, + } + } + comparison = None + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + assert [file.head_name for file in impacted_files] == ["fileA", "fileB"] + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_head_name_descending(self, read_file): + read_file.return_value = mock_data_from_archive + filters = { + "ordering": { + "direction": OrderingDirection.DESC, + "parameter": ImpactedFileParameter.FILE_NAME, + } + } + comparison = None + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + assert [file.head_name for file in impacted_files] == ["fileB", "fileA"] + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_misses_count_ascending(self, read_file): + read_file.return_value = mock_data_from_archive + filters = { + "ordering": { + "direction": OrderingDirection.ASC, + "parameter": ImpactedFileParameter.MISSES_COUNT, + } + } + comparison = None + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + assert [file.head_name for file in impacted_files] == ["fileA", "fileB"] + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_misses_count_descending(self, read_file): + read_file.return_value = mock_data_from_archive + filters = { + "ordering": { + "direction": OrderingDirection.DESC, + "parameter": ImpactedFileParameter.MISSES_COUNT, + } + } + comparison = None + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + assert [file.head_name for file in impacted_files] == ["fileB", "fileA"] + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_without_filters(self, read_file): + read_file.return_value = mock_data_from_archive + filters = {} + comparison = None + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + assert [file.head_name for file in impacted_files] == ["fileA", "fileB"] + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_unintended_changes(self, read_file): + read_file.return_value = mock_data_from_archive + filters = { + "has_unintended_changes": True, + "ordering": { + "direction": OrderingDirection.ASC, + "parameter": ImpactedFileParameter.FILE_NAME, + }, + } + comparison = None + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + assert [file.head_name for file in impacted_files] == ["fileA", "fileB"] + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_unintended_changes_set_to_false( + self, read_file + ): + read_file.return_value = mocked_files_with_direct_and_indirect_changes + filters = { + "has_unintended_changes": False, + "ordering": { + "direction": OrderingDirection.ASC, + "parameter": ImpactedFileParameter.FILE_NAME, + }, + } + comparison = None + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + assert [file.head_name for file in impacted_files] == ["fileA", "fileB"] + + @patch( + "services.comparison.Comparison.head_report_without_applied_diff", + new_callable=PropertyMock, + ) + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_flags_and_commit_comparison_for_pull( + self, read_file, build_report_from_commit_mock + ): + read_file.return_value = mocked_files_with_direct_and_indirect_changes + + commit_report = Report() + session_a_id, _ = commit_report.add_session(Session(flags=["flag-123"])) + session_b_id, _ = commit_report.add_session(Session(flags=["flag-456"])) + file_a = ReportFile("fileA") + file_a.append(1, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + commit_report.append(file_a) + file_b = ReportFile("fileB") + file_b.append(1, ReportLine.create(coverage=1, sessions=[[session_b_id, 1]])) + commit_report.append(file_b) + build_report_from_commit_mock.return_value = commit_report + + flags = ["flag-123"] + filters = {"flags": flags} + + owner = OwnerFactory() + repo = RepositoryFactory(author=owner) + base, head, compared_to = ( + CommitFactory(repository=repo), + CommitFactory(repository=repo), + CommitFactory(repository=repo), + ) + pull = PullFactory( + pullid=256, + repository=repo, + base=base.commitid, + head=head.commitid, + compared_to=compared_to.commitid, + ) + comparison = PullRequestComparison(user=owner, pull=pull) + + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + assert len(impacted_files) == 1 + assert impacted_files[0].head_name == "fileA" + + @patch( + "services.comparison.Comparison.head_report_without_applied_diff", + new_callable=PropertyMock, + ) + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_flags_and_commit_comparison_for_parent_commit( + self, read_file, build_report_from_commit_mock + ): + read_file.return_value = mocked_files_with_direct_and_indirect_changes + + commit_report = Report() + session_a_id, _ = commit_report.add_session(Session(flags=["flag-123"])) + session_b_id, _ = commit_report.add_session(Session(flags=["flag-456"])) + file_a = ReportFile("fileA") + file_a.append(1, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + commit_report.append(file_a) + file_b = ReportFile("fileB") + file_b.append(1, ReportLine.create(coverage=1, sessions=[[session_b_id, 1]])) + commit_report.append(file_b) + build_report_from_commit_mock.return_value = commit_report + + flags = ["flag-123"] + filters = {"flags": flags} + + owner = OwnerFactory() + repo = RepositoryFactory(author=owner) + base, head = ( + CommitFactory(repository=repo), + CommitFactory(repository=repo), + ) + comparison = Comparison(user=owner, base_commit=base, head_commit=head) + + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + assert len(impacted_files) == 1 + assert impacted_files[0].head_name == "fileA" + + @patch("services.components.commit_components") + @patch( + "services.comparison.Comparison.head_report_without_applied_diff", + new_callable=PropertyMock, + ) + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_components_and_commit_comparison_for_parent_commit( + self, read_file, build_report_from_commit_mock, commit_components_mock + ): + read_file.return_value = mocked_component_files_with_direct_and_indirect_changes + + commit_report = Report() + session_a_id, _ = commit_report.add_session(Session(flags=["flag-123"])) + session_b_id, _ = commit_report.add_session(Session(flags=["flag-456"])) + file_a = ReportFile("fileA.py") + file_a.append(1, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + commit_report.append(file_a) + file_b = ReportFile("fileB.js") + file_b.append(1, ReportLine.create(coverage=1, sessions=[[session_b_id, 1]])) + commit_report.append(file_b) + build_report_from_commit_mock.return_value = commit_report + + filters = {"components": ["PYThon"]} + + owner = OwnerFactory() + repo = RepositoryFactory(author=owner) + base, head = ( + CommitFactory(repository=repo), + CommitFactory(repository=repo), + ) + + # components filter + commit_components_mock.return_value = [ + Component.from_dict( + {"component_id": "python1.1", "paths": [".*/*.py"], "name": "PYThon"} + ), + Component.from_dict( + {"component_id": "golang1.2", "paths": [".*/*.go"], "name": "GOLang"} + ), + ] + comparison = Comparison(user=owner, base_commit=base, head_commit=head) + + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + assert len(impacted_files) == 1 + assert impacted_files[0].head_name == "fileA.py" + + @patch("services.components.commit_components") + @patch( + "services.comparison.Comparison.head_report_without_applied_diff", + new_callable=PropertyMock, + ) + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_components_using_flags( + self, read_file, build_report_from_commit_mock, commit_components_mock + ): + read_file.return_value = mocked_component_files_with_direct_and_indirect_changes + + commit_report = Report() + session_a_id, _ = commit_report.add_session(Session(flags=["flag-123"])) + session_b_id, _ = commit_report.add_session(Session(flags=["flag-456"])) + file_a = ReportFile("fileA.py") + file_a.append(1, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + commit_report.append(file_a) + file_b = ReportFile("fileB.js") + file_b.append(1, ReportLine.create(coverage=1, sessions=[[session_b_id, 1]])) + commit_report.append(file_b) + build_report_from_commit_mock.return_value = commit_report + + filters = {"components": ["javascript"]} + + owner = OwnerFactory() + repo = RepositoryFactory(author=owner) + base, head = ( + CommitFactory(repository=repo), + CommitFactory(repository=repo), + ) + + # components filter + commit_components_mock.return_value = [ + Component.from_dict( + {"component_id": "python1.1", "paths": [".*/*.py"], "name": "PYThon"} + ), + Component.from_dict( + { + "component_id": "javascript1.2", + "name": "javascript", + "flags_regexes": "flag-123", + } + ), + ] + comparison = Comparison(user=owner, base_commit=base, head_commit=head) + + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + + assert len(impacted_files) == 2 + assert impacted_files[0].head_name == "fileA.py" + assert impacted_files[1].head_name == "fileB.js" + + @patch("services.components.commit_components") + @patch( + "services.comparison.Comparison.head_report_without_applied_diff", + new_callable=PropertyMock, + ) + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_components_and_flags_commit_comparison_for_parent_commit( + self, read_file, build_report_from_commit_mock, commit_components_mock + ): + read_file.return_value = mocked_component_files_with_direct_and_indirect_changes + + commit_report = Report() + session_a_id, _ = commit_report.add_session(Session(flags=["flag-123"])) + session_b_id, _ = commit_report.add_session(Session(flags=["flag-456"])) + file_a = ReportFile("fileA.py") + file_a.append(1, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + commit_report.append(file_a) + file_b = ReportFile("fileB.js") + file_b.append(1, ReportLine.create(coverage=1, sessions=[[session_b_id, 1]])) + commit_report.append(file_b) + build_report_from_commit_mock.return_value = commit_report + + filters = {"components": ["PYThon"], "flags": ["flag-123"]} + + owner = OwnerFactory() + repo = RepositoryFactory(author=owner) + base, head = ( + CommitFactory(repository=repo), + CommitFactory(repository=repo), + ) + + # components filter + commit_components_mock.return_value = [ + Component.from_dict( + { + "component_id": "python1.1", + "paths": [".*/*.py"], + "name": "PYThon", + "flag_regexes": "flag-123", + } + ), + Component.from_dict( + { + "component_id": "javascript.2", + "paths": [".*/*.js"], + "name": "javaScript", + "flag_regexes": "flag-456", + } + ), + ] + comparison = Comparison(user=owner, base_commit=base, head_commit=head) + + impacted_files = self.execute(None, self.comparison_report, comparison, filters) + assert len(impacted_files) == 1 + assert impacted_files[0].head_name == "fileA.py" diff --git a/apps/codecov-api/compare/models.py b/apps/codecov-api/compare/models.py new file mode 100644 index 0000000000..1105eb8463 --- /dev/null +++ b/apps/codecov-api/compare/models.py @@ -0,0 +1 @@ +from shared.django_apps.compare.models import * diff --git a/apps/codecov-api/compare/tests/__init__.py b/apps/codecov-api/compare/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/compare/tests/factories.py b/apps/codecov-api/compare/tests/factories.py new file mode 100644 index 0000000000..d953e2ffed --- /dev/null +++ b/apps/codecov-api/compare/tests/factories.py @@ -0,0 +1 @@ +from shared.django_apps.compare.tests.factories import * diff --git a/apps/codecov-api/compare/tests/test_admin.py b/apps/codecov-api/compare/tests/test_admin.py new file mode 100644 index 0000000000..ea4c58c304 --- /dev/null +++ b/apps/codecov-api/compare/tests/test_admin.py @@ -0,0 +1,18 @@ +from django.test import TestCase +from django.urls import reverse +from shared.django_apps.codecov_auth.tests.factories import UserFactory + +from .factories import CommitComparisonFactory + + +class CompareAdminTest(TestCase): + def setUp(self): + # Create a couple of comparison so the list has something to display + CommitComparisonFactory() + CommitComparisonFactory() + self.staff_user = UserFactory(is_staff=True) + self.client.force_login(user=self.staff_user) + + def test_compare_admin_detail_page(self): + response = self.client.get(reverse("admin:compare_commitcomparison_changelist")) + self.assertEqual(response.status_code, 200) diff --git a/apps/codecov-api/conftest.py b/apps/codecov-api/conftest.py new file mode 100644 index 0000000000..d9d03c8396 --- /dev/null +++ b/apps/codecov-api/conftest.py @@ -0,0 +1,77 @@ +from pathlib import Path + +import fakeredis +import pytest +import vcr +from django.conf import settings +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine +from shared.utils.sessions import Session + +# we need to enable this in the test environment since we're often creating +# timeseries data and then asserting something about the aggregates all in +# a single transaction. calling `refresh_continuous_aggregate` doesn't work +# either since it cannot be called in a transaction. +settings.TIMESERIES_REAL_TIME_AGGREGATES = True + + +def pytest_configure(config): + """ + pytest_configure is the canonical way to configure test server for entire testing suite + """ + pass + + +@pytest.fixture +def codecov_vcr(request): + current_path = Path(request.node.fspath) + current_path_name = current_path.name.replace(".py", "") + cassette_path = current_path.parent / "cassetes" / current_path_name + if request.node.cls: + cls_name = request.node.cls.__name__ + cassette_path = cassette_path / cls_name + current_name = request.node.name + cassette_file_path = str(cassette_path / f"{current_name}.yaml") + with vcr.use_cassette( + cassette_file_path, + filter_headers=["authorization"], + match_on=["method", "scheme", "host", "port", "path"], + ) as cassette_maker: + yield cassette_maker + + +@pytest.fixture +def mock_redis(mocker): + m = mocker.patch("shared.helpers.redis._get_redis_instance_from_url") + redis_server = fakeredis.FakeStrictRedis() + m.return_value = redis_server + yield redis_server + + +@pytest.fixture(scope="class") +def sample_report(request): + report = Report() + first_file = ReportFile("foo/file1.py") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("bar/file2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + third_file = ReportFile("file3.py") + third_file.append(1, ReportLine.create(coverage=1, sessions=[[0, 1]])) + report.append(first_file) + report.append(second_file) + report.append(third_file) + report.add_session(Session(flags=["flag1", "flag2"])) + + request.cls.sample_report = report diff --git a/apps/codecov-api/core/__init__.py b/apps/codecov-api/core/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/admin.py b/apps/codecov-api/core/admin.py new file mode 100644 index 0000000000..2a1908e826 --- /dev/null +++ b/apps/codecov-api/core/admin.py @@ -0,0 +1,139 @@ +from django import forms +from django.contrib import admin +from django.core.paginator import Paginator +from django.db import connections +from django.utils.functional import cached_property + +from codecov.admin import AdminMixin +from codecov_auth.models import RepositoryToken +from core.models import Pull, Repository +from services.task.task import TaskService + + +class RepositoryTokenInline(admin.TabularInline): + model = RepositoryToken + readonly_fields = ["key"] + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False + + class Meta: + readonly_fields = ("key",) + + +class EstimatedCountPaginator(Paginator): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.object_list.count = self.count + + @cached_property + def count(self): + # Inspired by https://code.djangoproject.com/ticket/8408 + if self.object_list.query.where: + return self.object_list.count() + + db_table = self.object_list.model._meta.db_table + cursor = connections[self.object_list.db].cursor() + cursor.execute("SELECT reltuples FROM pg_class WHERE relname = %s", (db_table,)) + result = cursor.fetchone() + if not result: + return 0 + return int(result[0]) + + +class RepositoryAdminForm(forms.ModelForm): + # the model field has null=True but not blank=True, so we have to add a workaround + # to be able to clear out this field through the django admin + webhook_secret = forms.CharField(required=False, empty_value=None) + yaml = forms.JSONField(required=False) + using_integration = forms.BooleanField(required=False) + hookid = forms.CharField(required=False, empty_value=None) + + class Meta: + model = Repository + fields = "__all__" + + +@admin.register(Repository) +class RepositoryAdmin(AdminMixin, admin.ModelAdmin): + inlines = [RepositoryTokenInline] + list_display = ("name", "service_id", "author") + search_fields = ("author__username__exact",) + show_full_result_count = False + autocomplete_fields = ("bot",) + form = RepositoryAdminForm + + paginator = EstimatedCountPaginator + + readonly_fields = ( + "name", + "author", + "service_id", + "updatestamp", + "active", + "language", + "fork", + "upload_token", + "yaml", + "image_token", + "hookid", + "activated", + "deleted", + ) + fields = readonly_fields + ( + "bot", + "using_integration", + "branch", + "private", + "webhook_secret", + ) + + def has_add_permission(self, _, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return bool(request.user and request.user.is_superuser) + + def delete_queryset(self, request, queryset) -> None: + for repo in queryset: + TaskService().flush_repo(repository_id=repo.repoid) + + def delete_model(self, request, obj) -> None: + TaskService().flush_repo(repository_id=obj.repoid) + + +@admin.register(Pull) +class PullsAdmin(AdminMixin, admin.ModelAdmin): + list_display = ("pullid", "repository", "author") + show_full_result_count = False + paginator = EstimatedCountPaginator + readonly_fields = ( + "repository", + "id", + "pullid", + "issueid", + "title", + "base", + "head", + "user_provided_base_sha", + "compared_to", + "commentid", + "author", + "updatestamp", + "diff", + "flare", + ) + fields = readonly_fields + ("state",) + + @admin.display(description="flare") + def flare(self, instance): + return instance.flare + + def has_delete_permission(self, request, obj=None): + return False + + def has_add_permission(self, _, obj=None): + return False diff --git a/apps/codecov-api/core/apps.py b/apps/codecov-api/core/apps.py new file mode 100644 index 0000000000..97b655b18b --- /dev/null +++ b/apps/codecov-api/core/apps.py @@ -0,0 +1,20 @@ +import logging + +from django.apps import AppConfig +from shared.helpers.cache import RedisBackend, cache +from shared.helpers.redis import get_redis_connection + +from utils.config import RUN_ENV + +logger = logging.getLogger(__name__) + + +class CoreConfig(AppConfig): + name = "core" + + def ready(self): + import core.signals # noqa: F401 + + if RUN_ENV not in ["DEV", "TESTING"]: + cache_backend = RedisBackend(get_redis_connection()) + cache.configure(cache_backend) diff --git a/apps/codecov-api/core/commands/__init__.py b/apps/codecov-api/core/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/commands/branch/__init__.py b/apps/codecov-api/core/commands/branch/__init__.py new file mode 100644 index 0000000000..62e21450e3 --- /dev/null +++ b/apps/codecov-api/core/commands/branch/__init__.py @@ -0,0 +1,3 @@ +from .branch import BranchCommands + +__all__ = ["BranchCommands"] diff --git a/apps/codecov-api/core/commands/branch/branch.py b/apps/codecov-api/core/commands/branch/branch.py new file mode 100644 index 0000000000..8678fdcdd9 --- /dev/null +++ b/apps/codecov-api/core/commands/branch/branch.py @@ -0,0 +1,16 @@ +from codecov.commands.base import BaseCommand + +from .interactors.fetch_branch import FetchBranchInteractor +from .interactors.fetch_branches import FetchRepoBranchesInteractor + + +class BranchCommands(BaseCommand): + def fetch_branch(self, repository, branch_name): + return self.get_interactor(FetchBranchInteractor).execute( + repository, branch_name + ) + + def fetch_branches(self, repository, filters): + return self.get_interactor(FetchRepoBranchesInteractor).execute( + repository, filters + ) diff --git a/apps/codecov-api/core/commands/branch/interactors/__init__.py b/apps/codecov-api/core/commands/branch/interactors/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/commands/branch/interactors/fetch_branch.py b/apps/codecov-api/core/commands/branch/interactors/fetch_branch.py new file mode 100644 index 0000000000..60410484d9 --- /dev/null +++ b/apps/codecov-api/core/commands/branch/interactors/fetch_branch.py @@ -0,0 +1,9 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor + + +class FetchBranchInteractor(BaseInteractor): + @sync_to_async + def execute(self, repository, branch_name): + return repository.branches.filter(name=branch_name).first() diff --git a/apps/codecov-api/core/commands/branch/interactors/fetch_branches.py b/apps/codecov-api/core/commands/branch/interactors/fetch_branches.py new file mode 100644 index 0000000000..e19e7ae4d7 --- /dev/null +++ b/apps/codecov-api/core/commands/branch/interactors/fetch_branches.py @@ -0,0 +1,39 @@ +from typing import Any + +from asgiref.sync import sync_to_async +from django.db.models import OuterRef, Q, QuerySet, Subquery +from shared.django_apps.core.models import Repository + +from codecov.commands.base import BaseInteractor +from core.models import Commit + + +class FetchRepoBranchesInteractor(BaseInteractor): + @sync_to_async + def execute(self, repository: Repository, filters: dict[str, Any]) -> QuerySet: + queryset = repository.branches.all() + + filters = filters or {} + search_value = filters.get("search_value") + if search_value: + # force use of ILIKE to optimize search; django icontains doesn't work + # see https://github.com/codecov/engineering-team/issues/2537 + queryset = queryset.extra( + where=['"branches"."branch" ILIKE %s'], params=[f"%{search_value}%"] + ) + + merged = filters.get("merged_branches", False) + if not merged: + queryset = queryset.annotate( + merged=Subquery( + Commit.objects.filter( + commitid=OuterRef("head"), + repository_id=OuterRef("repository__repoid"), + ).values("merged")[:1] + ) + ).filter( + Q(merged__isnot=True) # exclude merged branches + | Q(name=repository.branch) # but always include the default branch + ) + + return queryset diff --git a/apps/codecov-api/core/commands/branch/interactors/tests/__init__.py b/apps/codecov-api/core/commands/branch/interactors/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/commands/branch/interactors/tests/test_fetch_branch.py b/apps/codecov-api/core/commands/branch/interactors/tests/test_fetch_branch.py new file mode 100644 index 0000000000..7169ab7a3a --- /dev/null +++ b/apps/codecov-api/core/commands/branch/interactors/tests/test_fetch_branch.py @@ -0,0 +1,24 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import BranchFactory, OwnerFactory + +from ..fetch_branch import FetchBranchInteractor + + +class FetchBranchInteractorTest(TestCase): + def setUp(self): + self.org = OwnerFactory() + self.branch = BranchFactory() + self.repo = self.branch.repository + + # helper to execute the interactor + def execute(self, owner, *args): + service = owner.service if owner else "github" + return FetchBranchInteractor(owner, service).execute(*args) + + async def test_fetch_branch(self): + branch = await self.execute(None, self.repo, self.branch.name) + assert branch == self.branch + + async def test_fetch_branch_doesnt_exist(self): + branch = await self.execute(None, self.repo, "do not exist") + assert branch is None diff --git a/apps/codecov-api/core/commands/branch/interactors/tests/test_fetch_branches.py b/apps/codecov-api/core/commands/branch/interactors/tests/test_fetch_branches.py new file mode 100644 index 0000000000..5b5a3589b7 --- /dev/null +++ b/apps/codecov-api/core/commands/branch/interactors/tests/test_fetch_branches.py @@ -0,0 +1,81 @@ +from typing import Any + +from asgiref.sync import async_to_sync +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + BranchFactory, + CommitFactory, + OwnerFactory, + RepositoryFactory, +) + +from ..fetch_branches import FetchRepoBranchesInteractor + + +class FetchRepoBranchesInteractorTest(TestCase): + def setUp(self) -> None: + self.org = OwnerFactory(username="codecov") + self.repo = RepositoryFactory( + author=self.org, name="gazebo", private=False, branch="main" + ) + self.head = CommitFactory(repository=self.repo) + self.commit = CommitFactory(repository=self.repo) + self.branches = [ + BranchFactory(repository=self.repo, head=self.head.commitid, name="test1"), + BranchFactory(repository=self.repo, head=self.head.commitid, name="test2"), + ] + + def execute(self, owner, repository, filters) -> FetchRepoBranchesInteractor: + service = owner.service if owner else "github" + return FetchRepoBranchesInteractor(owner, service).execute(repository, filters) + + def test_fetch_branches(self) -> None: + repository = self.repo + filters: dict[str, Any] = {} + branches = async_to_sync(self.execute)(None, repository, filters) + assert any(branch.name == "main" for branch in branches) + assert any(branch.name == "test1" for branch in branches) + assert any(branch.name == "test2" for branch in branches) + assert len(branches) == 3 + + def test_fetch_branches_unmerged(self) -> None: + merged = CommitFactory(repository=self.repo, merged=True) + BranchFactory(repository=self.repo, head=merged.commitid, name="merged") + branches = [ + branch.name for branch in async_to_sync(self.execute)(None, self.repo, {}) + ] + assert "merged" not in branches + branches = [ + branch.name + for branch in async_to_sync(self.execute)( + None, self.repo, {"merged_branches": True} + ) + ] + assert "merged" in branches + + def test_fetch_branches_filtered_by_name(self) -> None: + repository = self.repo + filters = {"search_value": "tESt", "merged_branches": True} + branches = async_to_sync(self.execute)(None, repository, filters) + assert not any(branch.name == "main" for branch in branches) + assert any(branch.name == "test1" for branch in branches) + assert any(branch.name == "test2" for branch in branches) + assert len(branches) == 2 + + def test_fetch_branches_filtered_by_name_no_sql_injection(self) -> None: + repository = self.repo + malicious_filters = { + "search_value": "'; DROP TABLE branches; --", + "merged_branches": True, + } + find_branches_sql_injection_attempt = async_to_sync(self.execute)( + None, repository, malicious_filters + ) + assert ( + # assert no branches found with that branch name + len(find_branches_sql_injection_attempt) == 0 + ) + + # confirm data is unaltered after sql injection attempt + find_branches = async_to_sync(self.execute)(None, repository, {}) + assert len(find_branches) == 3 diff --git a/apps/codecov-api/core/commands/branch/tests/__init__.py b/apps/codecov-api/core/commands/branch/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/commands/branch/tests/test_branch.py b/apps/codecov-api/core/commands/branch/tests/test_branch.py new file mode 100644 index 0000000000..c661127fcf --- /dev/null +++ b/apps/codecov-api/core/commands/branch/tests/test_branch.py @@ -0,0 +1,25 @@ +from unittest.mock import patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from ..branch import BranchCommands + + +class BranchCommandsTest(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + self.repository = RepositoryFactory() + self.command = BranchCommands(self.owner, "github") + + @patch("core.commands.branch.branch.FetchBranchInteractor.execute") + def test_fetch_branch_delegate_to_interactor(self, interactor_mock): + branch_name = "main" + self.command.fetch_branch(self.repository, branch_name) + interactor_mock.assert_called_once_with(self.repository, branch_name) + + @patch("core.commands.branch.branch.FetchRepoBranchesInteractor.execute") + def test_fetch_branches_delegate_to_interactor(self, interactor_mock): + filters = {} + self.command.fetch_branches(self.repository, filters) + interactor_mock.assert_called_once_with(self.repository, filters) diff --git a/apps/codecov-api/core/commands/commit/__init__.py b/apps/codecov-api/core/commands/commit/__init__.py new file mode 100644 index 0000000000..38bf294c84 --- /dev/null +++ b/apps/codecov-api/core/commands/commit/__init__.py @@ -0,0 +1,3 @@ +from .commit import CommitCommands + +__all__ = ["CommitCommands"] diff --git a/apps/codecov-api/core/commands/commit/commit.py b/apps/codecov-api/core/commands/commit/commit.py new file mode 100644 index 0000000000..ea88dc8aeb --- /dev/null +++ b/apps/codecov-api/core/commands/commit/commit.py @@ -0,0 +1,30 @@ +from codecov.commands.base import BaseCommand + +from .interactors.fetch_totals import FetchTotalsInteractor +from .interactors.get_commit_errors import GetCommitErrorsInteractor +from .interactors.get_file_content import GetFileContentInteractor +from .interactors.get_final_yaml import GetFinalYamlInteractor +from .interactors.get_latest_upload_error import GetLatestUploadErrorInteractor +from .interactors.get_uploads_number import GetUploadsNumberInteractor + + +class CommitCommands(BaseCommand): + def get_file_content(self, commit, path): + return self.get_interactor(GetFileContentInteractor).execute(commit, path) + + def fetch_totals(self, commit): + return self.get_interactor(FetchTotalsInteractor).execute(commit) + + def get_final_yaml(self, commit): + return self.get_interactor(GetFinalYamlInteractor).execute(commit) + + def get_commit_errors(self, commit, error_type): + return self.get_interactor(GetCommitErrorsInteractor).execute( + commit, error_type + ) + + def get_uploads_number(self, commit): + return self.get_interactor(GetUploadsNumberInteractor).execute(commit) + + def get_latest_upload_error(self, commit): + return self.get_interactor(GetLatestUploadErrorInteractor).execute(commit) diff --git a/apps/codecov-api/core/commands/commit/interactors/__init__.py b/apps/codecov-api/core/commands/commit/interactors/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/commands/commit/interactors/fetch_totals.py b/apps/codecov-api/core/commands/commit/interactors/fetch_totals.py new file mode 100644 index 0000000000..6d46b61bf2 --- /dev/null +++ b/apps/codecov-api/core/commands/commit/interactors/fetch_totals.py @@ -0,0 +1,10 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor + + +class FetchTotalsInteractor(BaseInteractor): + @sync_to_async + def execute(self, commit): + if commit.commitreport and hasattr(commit.commitreport, "reportleveltotals"): + return commit.commitreport.reportleveltotals diff --git a/apps/codecov-api/core/commands/commit/interactors/get_commit_errors.py b/apps/codecov-api/core/commands/commit/interactors/get_commit_errors.py new file mode 100644 index 0000000000..83414615ba --- /dev/null +++ b/apps/codecov-api/core/commands/commit/interactors/get_commit_errors.py @@ -0,0 +1,18 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from graphql_api.types.enums import CommitErrorCode, CommitErrorGeneralType + + +def errors_by_type(commit, error_type_str): + error_type = CommitErrorGeneralType(error_type_str) + error_codes = CommitErrorCode.get_codes_from_type(error_type) + error_codes_strings = [x.db_string for x in error_codes] + errors_by_type = commit.errors.filter(error_code__in=error_codes_strings) + return errors_by_type + + +class GetCommitErrorsInteractor(BaseInteractor): + @sync_to_async + def execute(self, commit, error_type): + return errors_by_type(commit, error_type_str=error_type) diff --git a/apps/codecov-api/core/commands/commit/interactors/get_file_content.py b/apps/codecov-api/core/commands/commit/interactors/get_file_content.py new file mode 100644 index 0000000000..f256dc42c5 --- /dev/null +++ b/apps/codecov-api/core/commands/commit/interactors/get_file_content.py @@ -0,0 +1,38 @@ +import logging +from typing import Any, Coroutine + +from codecov.commands.base import BaseInteractor +from core.models import Commit +from services.repo_providers import RepoProviderService + +log = logging.getLogger(__name__) + + +class GetFileContentInteractor(BaseInteractor): + async def get_file_from_service(self, commit: Commit, path: str) -> str | None: + try: + repository_service = await RepoProviderService().async_get_adapter( + owner=self.current_owner, repo=commit.repository + ) + content = await repository_service.get_source(path, commit.commitid) + + # When a file received from GH that is larger than 1MB the result will be + # pre-decoded and of string type; no need to decode again in that case + if isinstance(content.get("content"), str): + return content.get("content") + return content.get("content").decode("utf-8") + # TODO raise this to the API so we can handle it. + except Exception as e: + log.warning( + "GetFileContentInteractor - exception raised", + extra=dict( + commitid=commit.commitid, + path=path, + error_name=type(e).__name__, + error_message=str(e), + ), + ) + return None + + def execute(self, commit: Commit, path: str) -> Coroutine[Any, Any, str | None]: + return self.get_file_from_service(commit, path) diff --git a/apps/codecov-api/core/commands/commit/interactors/get_final_yaml.py b/apps/codecov-api/core/commands/commit/interactors/get_final_yaml.py new file mode 100644 index 0000000000..059f55f125 --- /dev/null +++ b/apps/codecov-api/core/commands/commit/interactors/get_final_yaml.py @@ -0,0 +1,10 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from services.yaml import final_commit_yaml + + +class GetFinalYamlInteractor(BaseInteractor): + @sync_to_async + def execute(self, commit): + return final_commit_yaml(commit, self.current_owner).to_dict() diff --git a/apps/codecov-api/core/commands/commit/interactors/get_latest_upload_error.py b/apps/codecov-api/core/commands/commit/interactors/get_latest_upload_error.py new file mode 100644 index 0000000000..46482a71c5 --- /dev/null +++ b/apps/codecov-api/core/commands/commit/interactors/get_latest_upload_error.py @@ -0,0 +1,41 @@ +import logging +from typing import Optional + +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from core.models import Commit +from reports.models import CommitReport, UploadError + +log = logging.getLogger(__name__) + + +class GetLatestUploadErrorInteractor(BaseInteractor): + @sync_to_async + def execute(self, commit: Commit) -> Optional[dict]: + try: + return self._get_latest_error(commit) + except Exception as e: + log.error(f"Error fetching upload error: {e}") + return None + + def _get_latest_error(self, commit: Commit) -> Optional[dict]: + latest_error = self._fetch_latest_error(commit) + if not latest_error: + return None + + return { + "error_code": latest_error.error_code, + "error_message": latest_error.error_params.get("error_message"), + } + + def _fetch_latest_error(self, commit: Commit) -> Optional[UploadError]: + return ( + UploadError.objects.filter( + report_session__report__commit=commit, + report_session__report__report_type=CommitReport.ReportType.TEST_RESULTS, + ) + .only("error_code", "error_params") + .order_by("-created_at") + .first() + ) diff --git a/apps/codecov-api/core/commands/commit/interactors/get_uploads_number.py b/apps/codecov-api/core/commands/commit/interactors/get_uploads_number.py new file mode 100644 index 0000000000..f8c0fe8563 --- /dev/null +++ b/apps/codecov-api/core/commands/commit/interactors/get_uploads_number.py @@ -0,0 +1,11 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor + + +class GetUploadsNumberInteractor(BaseInteractor): + @sync_to_async + def execute(self, commit): + if not commit.commitreport: + return 0 + return len(commit.commitreport.sessions.all()) diff --git a/apps/codecov-api/core/commands/commit/interactors/tests/__init__.py b/apps/codecov-api/core/commands/commit/interactors/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/commands/commit/interactors/tests/test_get_commits_errors.py b/apps/codecov-api/core/commands/commit/interactors/tests/test_get_commits_errors.py new file mode 100644 index 0000000000..6d17b3766f --- /dev/null +++ b/apps/codecov-api/core/commands/commit/interactors/tests/test_get_commits_errors.py @@ -0,0 +1,47 @@ +from asgiref.sync import async_to_sync +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + CommitErrorFactory, + CommitFactory, + OwnerFactory, +) + +from graphql_api.types.enums import CommitErrorGeneralType + +from ..get_commit_errors import GetCommitErrorsInteractor + + +class GetCommitErrorsInteractorTest(TestCase): + def setUp(self): + self.owner = OwnerFactory() + self.commit = CommitFactory() + self.yaml_commit_error = CommitErrorFactory( + commit=self.commit, error_code="invalid_yaml" + ) + self.yaml_commit_error_2 = CommitErrorFactory( + commit=self.commit, error_code="yaml_client_error" + ) + self.bot_commit_error = CommitErrorFactory( + commit=self.commit, error_code="repo_bot_invalid" + ) + + # helper to execute the interactor + def execute(self, owner, commit, error_type): + service = owner.service if owner else "github" + return GetCommitErrorsInteractor(owner, service).execute(commit, error_type) + + def test_fetch_yaml_error(self): + errors = async_to_sync(self.execute)( + owner=self.owner, + commit=self.commit, + error_type=CommitErrorGeneralType.yaml_error.value, + ) + assert len(errors) == 2 + + def test_fetch_bot_error(self): + errors = async_to_sync(self.execute)( + owner=self.owner, + commit=self.commit, + error_type=CommitErrorGeneralType.bot_error.value, + ) + assert len(errors) == 1 diff --git a/apps/codecov-api/core/commands/commit/interactors/tests/test_get_file_content.py b/apps/codecov-api/core/commands/commit/interactors/tests/test_get_file_content.py new file mode 100644 index 0000000000..1da49c7b6c --- /dev/null +++ b/apps/codecov-api/core/commands/commit/interactors/tests/test_get_file_content.py @@ -0,0 +1,81 @@ +from unittest.mock import patch + +import pytest +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.torngit.exceptions import TorngitObjectNotFoundError + +from ..get_file_content import GetFileContentInteractor + + +class MockedProviderAdapter: + async def get_source(self, commit, path): + return { + "content": b""" + def function_1: + pass + """ + } + + +class MockedStringProviderAdapter: + async def get_source(self, commit, path): + return { + "content": """ + def function_1: + pass + """ + } + + +class GetFileContentInteractorTest(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + self.repository = RepositoryFactory() + self.commit = CommitFactory() + + # helper to execute the interactor + def execute(self, owner, *args): + service = owner.service if owner else "github" + return GetFileContentInteractor(owner, service).execute(*args) + + @patch("services.repo_providers.RepoProviderService.async_get_adapter") + @pytest.mark.asyncio + async def test_when_path_has_file(self, mock_provider_adapter): + mock_provider_adapter.return_value = MockedProviderAdapter() + + file_content = await self.execute(None, self.commit, "path/to/file") + assert ( + file_content + == """ + def function_1: + pass + """ + ) + + @patch("services.repo_providers.RepoProviderService.async_get_adapter") + @pytest.mark.asyncio + async def test_when_path_has_no_file(self, mock_provider_adapter): + mock_provider_adapter.side_effect = TorngitObjectNotFoundError( + response_data=404, message="not found" + ) + file_content = await self.execute(None, self.commit, "path") + assert file_content is None + + @patch("services.repo_providers.RepoProviderService.async_get_adapter") + @pytest.mark.asyncio + async def test_when_path_has_file_string_response(self, mock_provider_adapter): + mock_provider_adapter.return_value = MockedStringProviderAdapter() + + file_content = await self.execute(None, self.commit, "path/to/file") + assert ( + file_content + == """ + def function_1: + pass + """ + ) diff --git a/apps/codecov-api/core/commands/commit/interactors/tests/test_get_final_yaml.py b/apps/codecov-api/core/commands/commit/interactors/tests/test_get_final_yaml.py new file mode 100644 index 0000000000..3a6b5bfe55 --- /dev/null +++ b/apps/codecov-api/core/commands/commit/interactors/tests/test_get_final_yaml.py @@ -0,0 +1,46 @@ +import asyncio +from unittest.mock import patch + +from asgiref.sync import async_to_sync +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.torngit.exceptions import TorngitObjectNotFoundError + +from ..get_final_yaml import GetFinalYamlInteractor + + +class GetFinalYamlInteractorTest(TestCase): + def setUp(self): + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org, private=False) + self.commit = CommitFactory(repository=self.repo) + asyncio.set_event_loop(asyncio.new_event_loop()) + + # helper to execute the interactor + def execute(self, owner, *args): + service = owner.service if owner else "github" + return GetFinalYamlInteractor(owner, service).execute(*args) + + @patch("services.yaml.fetch_current_yaml_from_provider_via_reference") + @async_to_sync + async def test_when_commit_has_yaml(self, mock_fetch_yaml): + mock_fetch_yaml.return_value = """ + codecov: + notify: + require_ci_to_pass: no + """ + config = await self.execute(None, self.commit) + assert config["codecov"]["require_ci_to_pass"] is False + + @patch("services.yaml.fetch_current_yaml_from_provider_via_reference") + @async_to_sync + async def test_when_commit_has_no_yaml(self, mock_fetch_yaml): + mock_fetch_yaml.side_effect = TorngitObjectNotFoundError( + response_data=404, message="not found" + ) + config = await self.execute(None, self.commit) + assert config["codecov"]["require_ci_to_pass"] is True diff --git a/apps/codecov-api/core/commands/commit/interactors/tests/test_get_latest_upload_error.py b/apps/codecov-api/core/commands/commit/interactors/tests/test_get_latest_upload_error.py new file mode 100644 index 0000000000..401148bb47 --- /dev/null +++ b/apps/codecov-api/core/commands/commit/interactors/tests/test_get_latest_upload_error.py @@ -0,0 +1,94 @@ +from unittest.mock import patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) + +from core.commands.commit.interactors.get_latest_upload_error import ( + GetLatestUploadErrorInteractor, +) +from graphql_api.types.enums import UploadErrorEnum +from reports.models import CommitReport +from reports.tests.factories import ( + CommitReportFactory, + UploadErrorFactory, + UploadFactory, +) + + +class GetLatestUploadErrorInteractorTest(TestCase): + def setUp(self): + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org) + self.commit = self._create_commit_with_errors() + self.commit_with_no_errors = CommitFactory(repository=self.repo) + self.single_error_commit = self._create_commit_with_single_error() + + def _create_commit_with_errors(self): + commit = CommitFactory(repository=self.repo) + report = CommitReportFactory( + commit=commit, report_type=CommitReport.ReportType.TEST_RESULTS + ) + upload = UploadFactory(report=report) + + # Create two errors with different timestamps + UploadErrorFactory( + report_session=upload, + created_at="2024-01-01T10:00:00Z", + error_code=UploadErrorEnum.FILE_NOT_IN_STORAGE, + error_params={"error_message": "First error"}, + ) + UploadErrorFactory( + report_session=upload, + created_at="2024-01-01T11:00:00Z", + error_code=UploadErrorEnum.REPORT_EMPTY, + error_params={"error_message": "Latest error"}, + ) + return commit + + def _create_commit_with_single_error(self): + commit = CommitFactory(repository=self.repo) + report = CommitReportFactory( + commit=commit, + report_type=CommitReport.ReportType.TEST_RESULTS, + ) + upload = UploadFactory(report=report) + UploadErrorFactory( + report_session=upload, + error_code=UploadErrorEnum.UNKNOWN_PROCESSING, + error_params={"error_message": "Some other error"}, + ) + return commit + + def execute(self, commit, owner=None): + service = owner.service if owner else "github" + return GetLatestUploadErrorInteractor(owner, service).execute(commit) + + async def test_when_no_errors_then_returns_none(self): + result = await self.execute(commit=self.commit_with_no_errors, owner=self.org) + assert result is None + + async def test_when_multiple_errors_then_returns_most_recent(self): + result = await self.execute(commit=self.commit, owner=self.org) + assert result == { + "error_code": UploadErrorEnum.REPORT_EMPTY, + "error_message": "Latest error", + } + + async def test_when_single_error_then_returns_error(self): + result = await self.execute(commit=self.single_error_commit, owner=self.org) + assert result == { + "error_code": UploadErrorEnum.UNKNOWN_PROCESSING, + "error_message": "Some other error", + } + + async def test_return_none_on_raised_error(self): + with patch( + "core.commands.commit.interactors.get_latest_upload_error.GetLatestUploadErrorInteractor._get_latest_error", + side_effect=Exception("Test error"), + ): + result = await self.execute(commit=self.commit, owner=self.org) + assert result is None diff --git a/apps/codecov-api/core/commands/commit/interactors/tests/test_get_uploads_number.py b/apps/codecov-api/core/commands/commit/interactors/tests/test_get_uploads_number.py new file mode 100644 index 0000000000..c19fcc5b18 --- /dev/null +++ b/apps/codecov-api/core/commands/commit/interactors/tests/test_get_uploads_number.py @@ -0,0 +1,36 @@ +from asgiref.sync import async_to_sync +from django.test import TestCase +from shared.django_apps.core.tests.factories import CommitFactory + +from reports.tests.factories import UploadFactory + +from ..get_uploads_number import GetUploadsNumberInteractor + + +class GetUploadsNumberInteractorTest(TestCase): + def setUp(self): + self.commit_with_no_upload = CommitFactory() + self.upload_one = UploadFactory() + self.upload_two = UploadFactory(report=self.upload_one.report) + self.commit_with_upload = self.upload_two.report.commit + + # making sure everything is public + self.commit_with_no_upload.repository.private = False + self.commit_with_no_upload.repository.save() + self.commit_with_upload.repository.private = False + self.commit_with_upload.repository.save() + + # helper to execute the interactor + def execute(self, owner, *args): + service = owner.service if owner else "github" + return GetUploadsNumberInteractor(owner, service).execute(*args) + + async def test_fetch_when_no_reports(self): + uploads_number = await self.execute(None, self.commit_with_no_upload) + assert uploads_number == 0 + + def test_fetch_when_reports(self): + # self.execute returns a lazy queryset so we need to wrap it with + # async_to_sync + uploads_number = async_to_sync(self.execute)(None, self.commit_with_upload) + assert uploads_number == 2 diff --git a/apps/codecov-api/core/commands/commit/tests/__init__.py b/apps/codecov-api/core/commands/commit/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/commands/commit/tests/test_commit.py b/apps/codecov-api/core/commands/commit/tests/test_commit.py new file mode 100644 index 0000000000..2e0f11f090 --- /dev/null +++ b/apps/codecov-api/core/commands/commit/tests/test_commit.py @@ -0,0 +1,45 @@ +from unittest.mock import patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) + +from ..commit import CommitCommands + + +class CommitCommandsTest(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + self.repository = RepositoryFactory() + self.commit = CommitFactory() + self.pull = PullFactory(repository_id=self.repository.repoid) + self.command = CommitCommands(self.owner, "github") + + @patch("core.commands.commit.commit.GetFinalYamlInteractor.execute") + def test_get_final_yaml_delegate_to_interactor(self, interactor_mock): + self.command.get_final_yaml(self.commit) + interactor_mock.assert_called_once_with(self.commit) + + @patch("core.commands.commit.commit.GetFileContentInteractor.execute") + def test_get_file_content_delegate_to_interactor(self, interactor_mock): + self.command.get_file_content(self.commit, "path/to/file") + interactor_mock.assert_called_once_with(self.commit, "path/to/file") + + @patch("core.commands.commit.commit.GetCommitErrorsInteractor.execute") + def test_get_commit_errors_delegate_to_interactor(self, interactor_mock): + self.command.get_commit_errors(self.commit, "YAML_ERROR") + interactor_mock.assert_called_once_with(self.commit, "YAML_ERROR") + + @patch("core.commands.commit.commit.GetUploadsNumberInteractor.execute") + def test_get_uploads_number_delegate_to_interactor(self, interactor_mock): + self.command.get_uploads_number(self.commit) + interactor_mock.assert_called_once_with(self.commit) + + @patch("core.commands.commit.commit.GetLatestUploadErrorInteractor.execute") + def test_get_latest_upload_error_delegate_to_interactor(self, interactor_mock): + self.command.get_latest_upload_error(self.commit) + interactor_mock.assert_called_once_with(self.commit) diff --git a/apps/codecov-api/core/commands/component/__init__.py b/apps/codecov-api/core/commands/component/__init__.py new file mode 100644 index 0000000000..f26cd80a77 --- /dev/null +++ b/apps/codecov-api/core/commands/component/__init__.py @@ -0,0 +1,3 @@ +from .component import ComponentCommands + +__all__ = ["ComponentCommands"] diff --git a/apps/codecov-api/core/commands/component/component.py b/apps/codecov-api/core/commands/component/component.py new file mode 100644 index 0000000000..632bc8658a --- /dev/null +++ b/apps/codecov-api/core/commands/component/component.py @@ -0,0 +1,12 @@ +from codecov.commands.base import BaseCommand + +from .interactors.delete_component_measurements import ( + DeleteComponentMeasurementsInteractor, +) + + +class ComponentCommands(BaseCommand): + def delete_component_measurements(self, *args, **kwargs): + return self.get_interactor(DeleteComponentMeasurementsInteractor).execute( + *args, **kwargs + ) diff --git a/apps/codecov-api/core/commands/component/interactors/__init__.py b/apps/codecov-api/core/commands/component/interactors/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/commands/component/interactors/delete_component_measurements.py b/apps/codecov-api/core/commands/component/interactors/delete_component_measurements.py new file mode 100644 index 0000000000..0b48aafefe --- /dev/null +++ b/apps/codecov-api/core/commands/component/interactors/delete_component_measurements.py @@ -0,0 +1,14 @@ +from codecov.commands.base import BaseInteractor +from services.task import TaskService + + +class DeleteComponentMeasurementsInteractor(BaseInteractor): + def execute(self, owner_username: str, repo_name: str, component_id: str): + _owner, repo = self.resolve_owner_and_repo( + owner_username, repo_name, ensure_is_admin=True + ) + + TaskService().delete_component_measurements( + repo.repoid, + component_id, + ) diff --git a/apps/codecov-api/core/commands/component/tests/__init__.py b/apps/codecov-api/core/commands/component/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/commands/component/tests/test_component.py b/apps/codecov-api/core/commands/component/tests/test_component.py new file mode 100644 index 0000000000..c2605dc862 --- /dev/null +++ b/apps/codecov-api/core/commands/component/tests/test_component.py @@ -0,0 +1,114 @@ +from unittest.mock import patch + +from django.test import TestCase, override_settings +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from codecov.commands.exceptions import Unauthenticated, Unauthorized, ValidationError + +from ..component import ComponentCommands + + +class MockSignature: + def apply_async(self): + pass + + +class ComponentCommandsTest(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="test-user") + self.org = OwnerFactory(username="test-org", admins=[self.owner.pk]) + self.owner.organizations = [self.org.pk] + self.repo = RepositoryFactory(author=self.org) + self.command = ComponentCommands(self.owner, "github") + + @patch("services.task.TaskService.delete_component_measurements") + def test_delete_component_measurements(self, mocked_delete_timeseries): + self.command.delete_component_measurements( + owner_username=self.org.username, + repo_name=self.repo.name, + component_id="component1", + ) + + mocked_delete_timeseries.assert_called_once_with(self.repo.pk, "component1") + + def test_delete_component_measurements_unauthenticated(self): + self.command = ComponentCommands(None, "github") + + with self.assertRaises(Unauthenticated): + self.command.delete_component_measurements( + owner_username=self.org.username, + repo_name=self.repo.name, + component_id="component1", + ) + + def test_delete_component_measurements_owner_not_found(self): + with self.assertRaises(ValidationError): + self.command.delete_component_measurements( + owner_username="nonexistent", + repo_name=self.repo.name, + component_id="component1", + ) + + def test_delete_component_measurements_repo_not_found(self): + with self.assertRaises(ValidationError): + self.command.delete_component_measurements( + owner_username=self.org.username, + repo_name="nonexistent", + component_id="component1", + ) + + def test_delete_component_measurements_not_admin(self): + self.org.admins = [] + self.org.save() + + with self.assertRaises(Unauthorized): + self.command.delete_component_measurements( + owner_username=self.org.username, + repo_name=self.repo.name, + component_id="component1", + ) + + @override_settings(IS_ENTERPRISE=True) + @patch("services.self_hosted.get_config") + @patch("services.task.TaskService.delete_component_measurements") + def test_delete_component_measurements_self_hosted_admin( + self, mocked_delete_timeseries, get_config_mock + ): + get_config_mock.return_value = [ + {"service": "github", "username": self.owner.username}, + ] + + self.command.delete_component_measurements( + owner_username=self.org.username, + repo_name=self.repo.name, + component_id="component1", + ) + + mocked_delete_timeseries.assert_called_once_with(self.repo.pk, "component1") + + @override_settings(IS_ENTERPRISE=True) + @patch("services.self_hosted.get_config") + def test_delete_component_measurements_self_hosted_non_admin(self, get_config_mock): + get_config_mock.return_value = [ + {"service": "github", "username": "someone-else"}, + ] + + with self.assertRaises(Unauthorized): + self.command.delete_component_measurements( + owner_username=self.org.username, + repo_name=self.repo.name, + component_id="component1", + ) + + @patch("services.task.TaskService._create_signature") + def test_delete_component_measurements_signature_created( + self, mocked_create_signature + ): + self.command.delete_component_measurements( + owner_username=self.org.username, + repo_name=self.repo.name, + component_id="component1", + ) + + mocked_create_signature.return_value = MockSignature() + mocked_create_signature.assert_called() diff --git a/apps/codecov-api/core/commands/flag/__init__.py b/apps/codecov-api/core/commands/flag/__init__.py new file mode 100644 index 0000000000..56918fd20f --- /dev/null +++ b/apps/codecov-api/core/commands/flag/__init__.py @@ -0,0 +1,3 @@ +from .flag import FlagCommands + +__all__ = ["FlagCommands"] diff --git a/apps/codecov-api/core/commands/flag/flag.py b/apps/codecov-api/core/commands/flag/flag.py new file mode 100644 index 0000000000..3f2703479d --- /dev/null +++ b/apps/codecov-api/core/commands/flag/flag.py @@ -0,0 +1,8 @@ +from codecov.commands.base import BaseCommand + +from .interactors.delete_flag import DeleteFlagInteractor + + +class FlagCommands(BaseCommand): + def delete_flag(self, *args, **kwargs): + return self.get_interactor(DeleteFlagInteractor).execute(*args, **kwargs) diff --git a/apps/codecov-api/core/commands/flag/interactors/__init__.py b/apps/codecov-api/core/commands/flag/interactors/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/commands/flag/interactors/delete_flag.py b/apps/codecov-api/core/commands/flag/interactors/delete_flag.py new file mode 100644 index 0000000000..e029e987c3 --- /dev/null +++ b/apps/codecov-api/core/commands/flag/interactors/delete_flag.py @@ -0,0 +1,21 @@ +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import ( + NotFound, +) +from reports.models import RepositoryFlag + + +class DeleteFlagInteractor(BaseInteractor): + def execute(self, owner_username: str, repo_name: str, flag_name: str): + _owner, repo = self.resolve_owner_and_repo( + owner_username, repo_name, ensure_is_admin=True + ) + + flag = RepositoryFlag.objects.filter( + repository_id=repo.pk, flag_name=flag_name + ).first() + if not flag: + raise NotFound() + + flag.deleted = True + flag.save() diff --git a/apps/codecov-api/core/commands/flag/tests/__init__.py b/apps/codecov-api/core/commands/flag/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/commands/flag/tests/test_flag.py b/apps/codecov-api/core/commands/flag/tests/test_flag.py new file mode 100644 index 0000000000..66f12ac43f --- /dev/null +++ b/apps/codecov-api/core/commands/flag/tests/test_flag.py @@ -0,0 +1,109 @@ +from unittest.mock import patch + +from django.test import TestCase, override_settings +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from codecov.commands.exceptions import ( + NotFound, + Unauthenticated, + Unauthorized, + ValidationError, +) +from reports.tests.factories import RepositoryFlagFactory + +from ..flag import FlagCommands + + +class FlagCommandsTest(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="test-user") + self.org = OwnerFactory(username="test-org", admins=[self.owner.pk]) + self.owner.organizations = [self.org.pk] + self.repo = RepositoryFactory(author=self.org) + self.command = FlagCommands(self.owner, "github") + self.flag = RepositoryFlagFactory(repository=self.repo, flag_name="test-flag") + + def test_delete_flag(self): + self.command.delete_flag( + owner_username=self.org.username, + repo_name=self.repo.name, + flag_name=self.flag.flag_name, + ) + + self.flag.refresh_from_db() + assert self.flag.deleted is True + + def test_delete_flag_unauthenticated(self): + self.command = FlagCommands(None, "github") + + with self.assertRaises(Unauthenticated): + self.command.delete_flag( + owner_username=self.org.username, + repo_name=self.repo.name, + flag_name=self.flag.flag_name, + ) + + def test_delete_flag_owner_not_found(self): + with self.assertRaises(ValidationError): + self.command.delete_flag( + owner_username="nonexistent", + repo_name=self.repo.name, + flag_name=self.flag.flag_name, + ) + + def test_delete_flag_repo_not_found(self): + with self.assertRaises(ValidationError): + self.command.delete_flag( + owner_username=self.org.username, + repo_name="nonexistent", + flag_name=self.flag.flag_name, + ) + + def test_delete_flag_not_admin(self): + self.org.admins = [] + self.org.save() + + with self.assertRaises(Unauthorized): + self.command.delete_flag( + owner_username=self.org.username, + repo_name=self.repo.name, + flag_name=self.flag.flag_name, + ) + + def test_delete_flag_not_found(self): + with self.assertRaises(NotFound): + self.command.delete_flag( + owner_username=self.org.username, + repo_name=self.repo.name, + flag_name="nonexistent", + ) + + @override_settings(IS_ENTERPRISE=True) + @patch("services.self_hosted.get_config") + def test_delete_flag_self_hosted_admin(self, get_config_mock): + get_config_mock.return_value = [ + {"service": "github", "username": self.owner.username}, + ] + + self.command.delete_flag( + owner_username=self.org.username, + repo_name=self.repo.name, + flag_name=self.flag.flag_name, + ) + + self.flag.refresh_from_db() + assert self.flag.deleted is True + + @override_settings(IS_ENTERPRISE=True) + @patch("services.self_hosted.get_config") + def test_delete_flag_self_hosted_non_admin(self, get_config_mock): + get_config_mock.return_value = [ + {"service": "github", "username": "someone-else"}, + ] + + with self.assertRaises(Unauthorized): + self.command.delete_flag( + owner_username=self.org.username, + repo_name=self.repo.name, + flag_name=self.flag.flag_name, + ) diff --git a/apps/codecov-api/core/commands/pull/__init__.py b/apps/codecov-api/core/commands/pull/__init__.py new file mode 100644 index 0000000000..f4c4878ad0 --- /dev/null +++ b/apps/codecov-api/core/commands/pull/__init__.py @@ -0,0 +1,3 @@ +from .pull import PullCommands + +__all__ = ["PullCommands"] diff --git a/apps/codecov-api/core/commands/pull/interactors/__init__.py b/apps/codecov-api/core/commands/pull/interactors/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/commands/pull/interactors/fetch_pull_request.py b/apps/codecov-api/core/commands/pull/interactors/fetch_pull_request.py new file mode 100644 index 0000000000..b60a310f0f --- /dev/null +++ b/apps/codecov-api/core/commands/pull/interactors/fetch_pull_request.py @@ -0,0 +1,25 @@ +from datetime import datetime, timedelta + +from asgiref.sync import sync_to_async +from shared.django_apps.core.models import Pull, Repository + +from codecov.commands.base import BaseInteractor +from services.task.task import TaskService + + +class FetchPullRequestInteractor(BaseInteractor): + def _should_sync_pull(self, pull: Pull | None) -> bool: + return ( + pull is not None + and pull.state == "open" + and pull.updatestamp is not None + and (datetime.now(tz=None) - pull.updatestamp) > timedelta(hours=1) + ) + + @sync_to_async + def execute(self, repository: Repository, id: int) -> Pull: + pull = repository.pull_requests.filter(pullid=id).first() + if self._should_sync_pull(pull): + TaskService().pulls_sync(repository.repoid, id) + + return pull diff --git a/apps/codecov-api/core/commands/pull/interactors/fetch_pull_requests.py b/apps/codecov-api/core/commands/pull/interactors/fetch_pull_requests.py new file mode 100644 index 0000000000..bc67abcf51 --- /dev/null +++ b/apps/codecov-api/core/commands/pull/interactors/fetch_pull_requests.py @@ -0,0 +1,19 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor + + +class FetchPullRequestsInteractor(BaseInteractor): + def apply_filters_to_pulls_queryset(self, queryset, filters): + filters = filters or {} + state = filters.get("state") + if state and len(state) > 0: + state_values = [s.value for s in state] + queryset = queryset.filter(state__in=state_values) + return queryset + + @sync_to_async + def execute(self, repository, filters): + queryset = repository.pull_requests.all() + queryset = self.apply_filters_to_pulls_queryset(queryset, filters) + return queryset diff --git a/apps/codecov-api/core/commands/pull/interactors/tests/__init__.py b/apps/codecov-api/core/commands/pull/interactors/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/commands/pull/interactors/tests/test_fetch_pull_request.py b/apps/codecov-api/core/commands/pull/interactors/tests/test_fetch_pull_request.py new file mode 100644 index 0000000000..18bbb860b7 --- /dev/null +++ b/apps/codecov-api/core/commands/pull/interactors/tests/test_fetch_pull_request.py @@ -0,0 +1,74 @@ +from datetime import datetime + +import pytest +from django.test import TestCase +from freezegun import freeze_time +from shared.django_apps.core.tests.factories import ( + OwnerFactory, + PullFactory, + RepositoryFactory, +) + +from ..fetch_pull_request import FetchPullRequestInteractor + + +class FetchPullRequestInteractorTest(TestCase): + def setUp(self): + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org, private=False) + self.pr = PullFactory(repository_id=self.repo.repoid) + + # helper to execute the interactor + def execute(self, owner, *args): + service = owner.service if owner else "github" + return FetchPullRequestInteractor(owner, service).execute(*args) + + async def test_fetch_when_pull_request_doesnt_exist(self): + pr = await self.execute(None, self.repo, -12) + assert pr is None + + async def test_fetch_pull_request(self): + pr = await self.execute(None, self.repo, self.pr.pullid) + assert pr == self.pr + + +@freeze_time("2024-07-01 12:00:00") +@pytest.mark.parametrize( + "pr_state, updatestamp, expected", + [ + pytest.param( + "open", "2024-07-01 11:50:00", False, id="pr_open_recently_updated" + ), + pytest.param("merged", "2024-07-01 01:00:00", False, id="pr_merged"), + pytest.param( + "closed", "2024-07-01 11:50:00", False, id="pr_closed_recently_updated" + ), + pytest.param( + "open", "2024-07-01 01:00:00", True, id="pr_open_not_recently_updated" + ), + ], +) +def test_fetch_pull_should_sync(pr_state, updatestamp, expected, db): + repo = RepositoryFactory(private=False) + pr = PullFactory(repository_id=repo.repoid, state=pr_state) + repo.save() + pr.save() # This will change the updatestamp, so we need to set it again + pr.updatestamp = datetime.fromisoformat(updatestamp).replace(tzinfo=None) + should_sync = FetchPullRequestInteractor( + repo.author, repo.service + )._should_sync_pull(pr) + assert pr.updatestamp == datetime.fromisoformat(updatestamp).replace(tzinfo=None) + assert should_sync == expected + + +def test_fetch_pull_updatestamp_is_none(db): + repo = RepositoryFactory(private=False) + pr = PullFactory(repository_id=repo.repoid, state="open") + repo.save() + pr.save() + pr.updatestamp = None + should_sync = FetchPullRequestInteractor( + repo.author, repo.service + )._should_sync_pull(pr) + assert pr.updatestamp is None + assert should_sync == False diff --git a/apps/codecov-api/core/commands/pull/interactors/tests/test_fetch_pull_requests.py b/apps/codecov-api/core/commands/pull/interactors/tests/test_fetch_pull_requests.py new file mode 100644 index 0000000000..f511566fbc --- /dev/null +++ b/apps/codecov-api/core/commands/pull/interactors/tests/test_fetch_pull_requests.py @@ -0,0 +1,128 @@ +from asgiref.sync import async_to_sync +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + OwnerFactory, + PullFactory, + RepositoryFactory, +) + +from core.models import PullStates + +from ..fetch_pull_requests import FetchPullRequestsInteractor + + +class FetchPullRequestsInteractorTest(TestCase): + def setUp(self): + self.pull_id = 10 + self.pull_title = "test-open-pr-1" + self.org = OwnerFactory() + self.repository_no_pull_requests = RepositoryFactory( + author=self.org, private=False + ) + self.repository_with_pull_requests = RepositoryFactory( + author=self.org, private=False + ) + PullFactory( + pullid=self.pull_id, + repository_id=self.repository_with_pull_requests.repoid, + title=self.pull_title, + state=PullStates.OPEN.value, + ) + + # helper to execute the interactor + def execute(self, owner, *args): + service = owner.service if owner else "github" + return FetchPullRequestsInteractor(owner, service).execute(*args) + + def test_fetch_when_repository_has_no_pulls(self): + self.filters = None + no_pull = async_to_sync(self.execute)( + None, self.repository_no_pull_requests, self.filters + ) + assert len(no_pull) == 0 + + def test_fetch_when_repository_has_pulls(self): + self.filters = None + pull_request = async_to_sync(self.execute)( + None, self.repository_with_pull_requests, self.filters + ) + assert len(pull_request) == 1 + assert pull_request[0].pullid == self.pull_id + assert pull_request[0].title == self.pull_title + assert ( + pull_request[0].repository_id == self.repository_with_pull_requests.repoid + ) + + def test_fetch_when_repository_has_pulls_with_filters(self): + # Add more pull requests with different states + # 3 open, 2 closed, 1 merged + PullFactory( + pullid=20, + repository_id=self.repository_with_pull_requests.repoid, + title="test-open-pr-2", + state=PullStates.OPEN.value, + ) + PullFactory( + pullid=21, + repository_id=self.repository_with_pull_requests.repoid, + title="test-open-pr-3", + state=PullStates.OPEN.value, + ) + PullFactory( + pullid=30, + repository_id=self.repository_with_pull_requests.repoid, + title="test-closed-pr-1", + state=PullStates.CLOSED.value, + ) + PullFactory( + pullid=31, + repository_id=self.repository_with_pull_requests.repoid, + title="test-closed-pr-2", + state=PullStates.CLOSED.value, + ) + PullFactory( + pullid=40, + repository_id=self.repository_with_pull_requests.repoid, + title="test-merged-pr-1", + state=PullStates.MERGED.value, + ) + # Execute without filters + self.filters = None + pull_request = async_to_sync(self.execute)( + None, self.repository_with_pull_requests, self.filters + ) + assert len(pull_request) == 6 + + # Execute without open filter + self.filters = {"state": [PullStates.OPEN]} + pull_request = async_to_sync(self.execute)( + None, self.repository_with_pull_requests, self.filters + ) + assert len(pull_request) == 3 + for pull in pull_request: + assert pull.state == PullStates.OPEN.value + + # Execute without closed filter + self.filters = {"state": [PullStates.CLOSED]} + pull_request = async_to_sync(self.execute)( + None, self.repository_with_pull_requests, self.filters + ) + assert len(pull_request) == 2 + for pull in pull_request: + assert pull.state == PullStates.CLOSED.value + + # Execute without merged filter + self.filters = {"state": [PullStates.MERGED]} + pull_request = async_to_sync(self.execute)( + None, self.repository_with_pull_requests, self.filters + ) + assert len(pull_request) == 1 + for pull in pull_request: + assert pull.state == PullStates.MERGED.value + + # Execute without merged filter + self.filters = {"state": [PullStates.MERGED, PullStates.OPEN]} + pull_request = async_to_sync(self.execute)( + None, self.repository_with_pull_requests, self.filters + ) + assert len(pull_request) == 4 diff --git a/apps/codecov-api/core/commands/pull/pull.py b/apps/codecov-api/core/commands/pull/pull.py new file mode 100644 index 0000000000..0d22cc9b1b --- /dev/null +++ b/apps/codecov-api/core/commands/pull/pull.py @@ -0,0 +1,14 @@ +from codecov.commands.base import BaseCommand + +from .interactors.fetch_pull_request import FetchPullRequestInteractor +from .interactors.fetch_pull_requests import FetchPullRequestsInteractor + + +class PullCommands(BaseCommand): + def fetch_pull_request(self, repository, id): + return self.get_interactor(FetchPullRequestInteractor).execute(repository, id) + + def fetch_pull_requests(self, repository, filters): + return self.get_interactor(FetchPullRequestsInteractor).execute( + repository, filters + ) diff --git a/apps/codecov-api/core/commands/pull/tests/__init__.py b/apps/codecov-api/core/commands/pull/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/commands/pull/tests/test_pull.py b/apps/codecov-api/core/commands/pull/tests/test_pull.py new file mode 100644 index 0000000000..5caf12873d --- /dev/null +++ b/apps/codecov-api/core/commands/pull/tests/test_pull.py @@ -0,0 +1,26 @@ +from unittest.mock import patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from ..pull import PullCommands + + +class PullCommandsTest(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + self.repository = RepositoryFactory() + self.command = PullCommands(self.owner, "github") + + @patch("core.commands.pull.pull.FetchPullRequestsInteractor.execute") + def test_fetch_pull_requests_delegate_to_interactor(self, interactor_mock): + self.filters = None + repo = RepositoryFactory() + self.command.fetch_pull_requests(repo, self.filters) + interactor_mock.assert_called_once_with(repo, self.filters) + + @patch("core.commands.pull.pull.FetchPullRequestInteractor.execute") + def test_fetch_pull_request_delegate_to_interactor(self, interactor_mock): + repo = RepositoryFactory() + self.command.fetch_pull_request(repo, 12) + interactor_mock.assert_called_once_with(repo, 12) diff --git a/apps/codecov-api/core/commands/repository/__init__.py b/apps/codecov-api/core/commands/repository/__init__.py new file mode 100644 index 0000000000..099c43bceb --- /dev/null +++ b/apps/codecov-api/core/commands/repository/__init__.py @@ -0,0 +1,3 @@ +from .repository import RepositoryCommands + +__all__ = ["RepositoryCommands"] diff --git a/apps/codecov-api/core/commands/repository/interactors/__init__.py b/apps/codecov-api/core/commands/repository/interactors/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/commands/repository/interactors/activate_measurements.py b/apps/codecov-api/core/commands/repository/interactors/activate_measurements.py new file mode 100644 index 0000000000..5049750d9d --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/activate_measurements.py @@ -0,0 +1,28 @@ +from asgiref.sync import sync_to_async +from django.conf import settings + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import ValidationError +from timeseries.helpers import trigger_backfill +from timeseries.models import Dataset, MeasurementName + + +class ActivateMeasurementsInteractor(BaseInteractor): + @sync_to_async + def execute( + self, repo_name: str, owner_name: str, measurement_type: MeasurementName + ) -> Dataset: + if not settings.TIMESERIES_ENABLED: + raise ValidationError("Timeseries storage not enabled") + + _owner, repo = self.resolve_owner_and_repo( + owner_name, repo_name, only_viewable=True, only_active=True + ) + + dataset, created = Dataset.objects.get_or_create( + name=measurement_type.value, repository_id=repo.pk + ) + if created: + trigger_backfill([dataset]) + + return dataset diff --git a/apps/codecov-api/core/commands/repository/interactors/encode_secret_string.py b/apps/codecov-api/core/commands/repository/interactors/encode_secret_string.py new file mode 100644 index 0000000000..94bceed919 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/encode_secret_string.py @@ -0,0 +1,27 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated, ValidationError +from codecov_auth.models import Owner +from core.commands.repository.interactors.utils import encode_secret_string +from core.models import Repository + + +class EncodeSecretStringInteractor(BaseInteractor): + @sync_to_async + def execute(self, owner: Owner, repo_name: str, value: str) -> str: + if not self.current_user.is_authenticated: + raise Unauthenticated() + + repo = Repository.objects.viewable_repos(owner).filter(name=repo_name).first() + if not repo: + raise ValidationError("Repo not found") + to_encode = "/".join( + ( + owner.service, + owner.service_id, + repo.service_id, + value, + ) + ) + return encode_secret_string(to_encode) diff --git a/apps/codecov-api/core/commands/repository/interactors/erase_repository.py b/apps/codecov-api/core/commands/repository/interactors/erase_repository.py new file mode 100644 index 0000000000..804580bed8 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/erase_repository.py @@ -0,0 +1,15 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from services.task.task import TaskService + + +class EraseRepositoryInteractor(BaseInteractor): + @sync_to_async + def execute(self, owner_username: str, repo_name: str) -> None: + _owner, repo = self.resolve_owner_and_repo( + owner_username, repo_name, ensure_is_admin=True + ) + + TaskService().delete_timeseries(repository_id=repo.repoid) + TaskService().flush_repo(repository_id=repo.repoid) diff --git a/apps/codecov-api/core/commands/repository/interactors/fetch_repository.py b/apps/codecov-api/core/commands/repository/interactors/fetch_repository.py new file mode 100644 index 0000000000..37292f7bd0 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/fetch_repository.py @@ -0,0 +1,34 @@ +import sentry_sdk +from asgiref.sync import sync_to_async +from shared.django_apps.codecov_auth.models import Owner +from shared.django_apps.core.models import Repository + +from codecov.commands.base import BaseInteractor + + +class FetchRepositoryInteractor(BaseInteractor): + @sync_to_async + @sentry_sdk.trace + def execute( + self, + owner: Owner, + name: str, + okta_authenticated_accounts: list[int], + exclude_okta_enforced_repos: bool = True, + needs_coverage: bool = True, + needs_commits: bool = True, + ) -> Repository | None: + queryset = Repository.objects.viewable_repos(self.current_owner) + if exclude_okta_enforced_repos: + queryset = queryset.exclude_accounts_enforced_okta( + okta_authenticated_accounts + ) + + if needs_coverage: + queryset = queryset.with_recent_coverage() + if needs_commits: + queryset = queryset.with_oldest_commit_at() + + repo = queryset.filter(author=owner, name=name).select_related("author").first() + + return repo diff --git a/apps/codecov-api/core/commands/repository/interactors/get_repository_token.py b/apps/codecov-api/core/commands/repository/interactors/get_repository_token.py new file mode 100644 index 0000000000..361b83b914 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/get_repository_token.py @@ -0,0 +1,29 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated +from codecov_auth.helpers import current_user_part_of_org +from codecov_auth.models import RepositoryToken + + +class GetRepositoryTokenInteractor(BaseInteractor): + def validate(self, repository): + if not self.current_user.is_authenticated: + raise Unauthenticated() + + @sync_to_async + def execute(self, repository, token_type): + self.validate(repository) + if not repository.active: + return None + + if current_user_part_of_org(self.current_owner, repository.author): + token = RepositoryToken.objects.filter( + repository_id=repository.repoid, token_type=token_type + ).first() + if not token: + token = RepositoryToken( + repository_id=repository.repoid, token_type=token_type + ) + token.save() + return token.key diff --git a/apps/codecov-api/core/commands/repository/interactors/get_upload_token.py b/apps/codecov-api/core/commands/repository/interactors/get_upload_token.py new file mode 100644 index 0000000000..80ef954d4a --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/get_upload_token.py @@ -0,0 +1,9 @@ +from codecov.commands.base import BaseInteractor +from codecov_auth.helpers import current_user_part_of_org + + +class GetUploadTokenInteractor(BaseInteractor): + async def execute(self, repository): + if not current_user_part_of_org(self.current_owner, repository.author): + return None + return repository.upload_token diff --git a/apps/codecov-api/core/commands/repository/interactors/regenerate_repository_token.py b/apps/codecov-api/core/commands/repository/interactors/regenerate_repository_token.py new file mode 100644 index 0000000000..9d02738e2c --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/regenerate_repository_token.py @@ -0,0 +1,20 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from codecov_auth.models import RepositoryToken + + +class RegenerateRepositoryTokenInteractor(BaseInteractor): + @sync_to_async + def execute(self, repo_name: str, owner_username: str, token_type: str): + _owner, repo = self.resolve_owner_and_repo( + owner_username, repo_name, only_viewable=True, only_active=True + ) + + token, created = RepositoryToken.objects.get_or_create( + repository_id=repo.repoid, token_type=token_type + ) + if not created: + token.key = token.generate_key() + token.save() + return token.key diff --git a/apps/codecov-api/core/commands/repository/interactors/regenerate_repository_upload_token.py b/apps/codecov-api/core/commands/repository/interactors/regenerate_repository_upload_token.py new file mode 100644 index 0000000000..885f3955b6 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/regenerate_repository_upload_token.py @@ -0,0 +1,17 @@ +import uuid + +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor + + +class RegenerateRepositoryUploadTokenInteractor(BaseInteractor): + @sync_to_async + def execute(self, repo_name: str, owner_username: str) -> uuid.UUID: + _owner, repo = self.resolve_owner_and_repo( + owner_username, repo_name, only_viewable=True + ) + + repo.upload_token = uuid.uuid4() + repo.save() + return repo.upload_token diff --git a/apps/codecov-api/core/commands/repository/interactors/tests/__init__.py b/apps/codecov-api/core/commands/repository/interactors/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/commands/repository/interactors/tests/test_activate_measurements.py b/apps/codecov-api/core/commands/repository/interactors/tests/test_activate_measurements.py new file mode 100644 index 0000000000..f3eb9b1224 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/tests/test_activate_measurements.py @@ -0,0 +1,116 @@ +from datetime import datetime +from unittest.mock import patch + +import pytest +from asgiref.sync import async_to_sync +from django.conf import settings +from django.test import TestCase, override_settings +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) + +from codecov.commands.exceptions import ValidationError +from timeseries.models import Dataset, MeasurementName + +from ..activate_measurements import ActivateMeasurementsInteractor + + +@pytest.mark.skipif( + not settings.TIMESERIES_ENABLED, reason="requires timeseries data storage" +) +class ActivateMeasurementsInteractorTest(TestCase): + databases = {"default", "timeseries"} + + def setUp(self): + self.org = OwnerFactory(username="test-org") + self.repo = RepositoryFactory(author=self.org, name="test-repo", active=True) + self.user = OwnerFactory(permission=[self.repo.pk]) + + @async_to_sync + def execute(self, owner, repo_name=None, measurement_type=None): + return ActivateMeasurementsInteractor(owner, "github").execute( + repo_name=repo_name or "test-repo", + owner_name="test-org", + measurement_type=measurement_type or MeasurementName.FLAG_COVERAGE, + ) + + def test_repo_not_found(self): + with pytest.raises(ValidationError): + self.execute(owner=self.user, repo_name="wrong") + + @override_settings(TIMESERIES_ENABLED=False) + def test_timeseries_not_enabled(self): + with pytest.raises(ValidationError): + self.execute(owner=self.user) + + @patch("services.task.TaskService.backfill_dataset") + def test_creates_flag_dataset(self, backfill_dataset): + assert not Dataset.objects.filter( + name=MeasurementName.FLAG_COVERAGE.value, + repository_id=self.repo.pk, + ).exists() + + self.execute(owner=self.user) + + assert Dataset.objects.filter( + name=MeasurementName.FLAG_COVERAGE.value, + repository_id=self.repo.pk, + ).exists() + + @patch("services.task.TaskService.backfill_dataset") + def test_creates_component_dataset(self, backfill_dataset): + assert not Dataset.objects.filter( + name=MeasurementName.COMPONENT_COVERAGE.value, + repository_id=self.repo.pk, + ).exists() + + self.execute( + owner=self.user, + repo_name="test-repo", + measurement_type=MeasurementName.COMPONENT_COVERAGE, + ) + + assert Dataset.objects.filter( + name=MeasurementName.COMPONENT_COVERAGE.value, + repository_id=self.repo.pk, + ).exists() + + @patch("services.task.TaskService.backfill_dataset") + def test_creates_coverage_dataset(self, backfill_dataset): + assert not Dataset.objects.filter( + name=MeasurementName.COVERAGE.value, + repository_id=self.repo.pk, + ).exists() + + self.execute( + owner=self.user, + repo_name="test-repo", + measurement_type=MeasurementName.COVERAGE, + ) + + assert Dataset.objects.filter( + name=MeasurementName.COVERAGE.value, + repository_id=self.repo.pk, + ).exists() + + @patch("services.task.TaskService.backfill_dataset") + def test_triggers_task(self, backfill_dataset): + CommitFactory(repository=self.repo, timestamp=datetime(2000, 1, 1, 1, 1, 1)) + CommitFactory(repository=self.repo, timestamp=datetime(2021, 12, 31, 1, 1, 1)) + self.execute(owner=self.user) + dataset = Dataset.objects.filter( + name=MeasurementName.FLAG_COVERAGE.value, + repository_id=self.repo.pk, + ).first() + backfill_dataset.assert_called_once_with( + dataset, + start_date=datetime(2000, 1, 1, 1, 1, 1), + end_date=datetime(2021, 12, 31, 1, 1, 1), + ) + + @patch("services.task.TaskService.backfill_dataset") + def test_no_commits(self, backfill_dataset): + self.execute(owner=self.user) + assert backfill_dataset.call_count == 0 diff --git a/apps/codecov-api/core/commands/repository/interactors/tests/test_encode_secret_string.py b/apps/codecov-api/core/commands/repository/interactors/tests/test_encode_secret_string.py new file mode 100644 index 0000000000..2b5f8e1b63 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/tests/test_encode_secret_string.py @@ -0,0 +1,33 @@ +import pytest +from asgiref.sync import async_to_sync +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory +from shared.encryption.yaml_secret import yaml_secret_encryptor + +from codecov.commands.exceptions import Unauthenticated, ValidationError + +from ..encode_secret_string import EncodeSecretStringInteractor + + +class EncodeSecretStringInteractorTest(TestCase): + @async_to_sync + def execute(self, owner, repo_name, value): + return EncodeSecretStringInteractor(owner, "github").execute( + owner, repo_name, value + ) + + def test_encode_secret_string(self): + owner = OwnerFactory() + RepositoryFactory(author=owner, name="repo-1") + res = self.execute(owner, repo_name="repo-1", value="token-1") + check_encryptor = yaml_secret_encryptor + assert "token-1" in check_encryptor.decode(res[7:]) + + def test_validation_error_when_repo_not_found(self): + owner = OwnerFactory() + with pytest.raises(ValidationError): + self.execute(owner, repo_name=None, value="token-1") + + def test_user_is_not_authenticated(self): + with pytest.raises(Unauthenticated): + self.execute(None, repo_name=None, value="test") diff --git a/apps/codecov-api/core/commands/repository/interactors/tests/test_erase_repository.py b/apps/codecov-api/core/commands/repository/interactors/tests/test_erase_repository.py new file mode 100644 index 0000000000..aed9fde9a8 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/tests/test_erase_repository.py @@ -0,0 +1,32 @@ +import pytest +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov.commands.exceptions import Unauthorized + +from ..erase_repository import EraseRepositoryInteractor + + +class UpdateRepositoryInteractorTest(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov") + self.random_user = OwnerFactory(organizations=[]) + self.non_admin_user = OwnerFactory(organizations=[self.owner.ownerid]) + + def execute_unauthorized_owner(self): + return EraseRepositoryInteractor(self.owner, "github").execute( + self.random_user.username, "repo-1" + ) + + def execute_user_not_admin(self): + return EraseRepositoryInteractor(self.non_admin_user, "github").execute( + self.owner.username, "repo-1" + ) + + async def test_when_validation_error_unauthorized_owner_not_part_of_org(self): + with pytest.raises(Unauthorized): + await self.execute_unauthorized_owner() + + async def test_when_validation_error_unauthorized_owner_not_admin(self): + with pytest.raises(Unauthorized): + await self.execute_user_not_admin() diff --git a/apps/codecov-api/core/commands/repository/interactors/tests/test_fetch_repository.py b/apps/codecov-api/core/commands/repository/interactors/tests/test_fetch_repository.py new file mode 100644 index 0000000000..e250ba98d3 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/tests/test_fetch_repository.py @@ -0,0 +1,87 @@ +from django.test import TestCase +from shared.django_apps.codecov_auth.tests.factories import ( + AccountFactory, + OktaSettingsFactory, +) +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from ..fetch_repository import FetchRepositoryInteractor + + +class FetchRepositoryInteractorTest(TestCase): + def setUp(self): + self.org = OwnerFactory() + + self.okta_account = AccountFactory() + self.okta_settings = OktaSettingsFactory( + account=self.okta_account, enforced=True + ) + self.okta_org = OwnerFactory(account=self.okta_account) + + self.public_repo = RepositoryFactory(author=self.org, private=False) + self.hidden_private_repo = RepositoryFactory(author=self.org, private=True) + self.private_repo = RepositoryFactory(author=self.org, private=True) + self.okta_private_repo = RepositoryFactory(author=self.okta_org, private=True) + self.current_user = OwnerFactory( + permission=[self.private_repo.repoid, self.okta_private_repo.repoid], + organizations=[self.org.ownerid, self.okta_org.ownerid], + ) + + # helper to execute the interactor + def execute(self, owner, *args, **kwargs): + service = owner.service if owner else "github" + return FetchRepositoryInteractor(owner, service).execute(*args, **kwargs) + + async def test_fetch_public_repo_unauthenticated(self): + repo = await self.execute(None, self.org, self.public_repo.name, []) + assert repo == self.public_repo + + async def test_fetch_public_repo_authenticated(self): + repo = await self.execute( + self.current_user, self.org, self.public_repo.name, [] + ) + assert repo == self.public_repo + + async def test_fetch_private_repo_unauthenticated(self): + repo = await self.execute(None, self.org, self.private_repo.name, []) + assert repo is None + + async def test_fetch_private_repo_authenticated_but_no_permissions(self): + repo = await self.execute( + self.current_user, self.org, self.hidden_private_repo.name, [] + ) + assert repo is None + + async def test_fetch_private_repo_authenticated_with_permissions(self): + repo = await self.execute( + self.current_user, self.org, self.private_repo.name, [] + ) + assert repo == self.private_repo + + async def test_fetch_okta_private_repo_authenticated(self): + repo = await self.execute( + self.current_user, + self.okta_org, + self.okta_private_repo.name, + [self.okta_account.id], + ) + assert repo == self.okta_private_repo + + async def test_fetch_okta_private_repo_unauthenticated(self): + repo = await self.execute( + self.current_user, + self.okta_org, + self.okta_private_repo.name, + [], + ) + assert repo is None + + async def test_fetch_okta_private_repo_do_not_exclude_unauthenticated(self): + repo = await self.execute( + self.current_user, + self.okta_org, + self.okta_private_repo.name, + [], + exclude_okta_enforced_repos=False, + ) + assert repo == self.okta_private_repo diff --git a/apps/codecov-api/core/commands/repository/interactors/tests/test_get_repository_token.py b/apps/codecov-api/core/commands/repository/interactors/tests/test_get_repository_token.py new file mode 100644 index 0000000000..f634667dd3 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/tests/test_get_repository_token.py @@ -0,0 +1,52 @@ +import pytest +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + OwnerFactory, + RepositoryFactory, + RepositoryTokenFactory, +) + +from codecov.commands.exceptions import Unauthenticated + +from ..get_repository_token import GetRepositoryTokenInteractor + + +class GetRepositoryTokenInteractorTest(TestCase): + def setUp(self): + self.org = OwnerFactory(name="codecov") + self.active_repo = RepositoryFactory( + author=self.org, name="gazebo", active=True + ) + self.inactive_repo = RepositoryFactory( + author=self.org, name="backend", active=False + ) + self.repo_with_no_token = RepositoryFactory( + author=self.org, name="frontend", active=True + ) + self.user = OwnerFactory(organizations=[self.org.ownerid]) + RepositoryTokenFactory(repository=self.active_repo, key="random") + + def execute(self, owner, repo, token_type="upload"): + return GetRepositoryTokenInteractor(owner, "github").execute( + repository=repo, token_type=token_type + ) + + async def test_when_unauthenticated_raise(self): + with pytest.raises(Unauthenticated): + await self.execute(owner="", repo=self.active_repo) + + async def test_when_repo_inactive(self): + token = await self.execute(owner=self.user, repo=self.inactive_repo) + assert token is None + + async def test_when_repo_has_no_token(self): + token = await self.execute(owner=self.user, repo=self.repo_with_no_token) + assert token is not None + assert len(token) == 40 + + async def test_get_static_analysis_token(self): + token = await self.execute( + owner=self.user, repo=self.active_repo, token_type="static_analysis" + ) + assert token is not None + assert len(token) == 40 diff --git a/apps/codecov-api/core/commands/repository/interactors/tests/test_get_upload_token.py b/apps/codecov-api/core/commands/repository/interactors/tests/test_get_upload_token.py new file mode 100644 index 0000000000..2f165461d6 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/tests/test_get_upload_token.py @@ -0,0 +1,24 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from ..get_upload_token import GetUploadTokenInteractor + + +class GetUploadTokenInteractorTest(TestCase): + def setUp(self): + self.org = OwnerFactory() + self.repo_in_org = RepositoryFactory(author=self.org) + self.random_repo = RepositoryFactory() + self.owner = OwnerFactory(organizations=[self.org.ownerid]) + + # helper to execute the interactor + def execute(self, *args): + return GetUploadTokenInteractor(self.owner, self.owner.service).execute(*args) + + async def test_fetch_upload_token_random_repo(self): + token = await self.execute(self.random_repo) + assert token is None + + async def test_fetch_upload_token_repo_in_my_org(self): + token = await self.execute(self.repo_in_org) + assert token is self.repo_in_org.upload_token diff --git a/apps/codecov-api/core/commands/repository/interactors/tests/test_regenerate_repository_token.py b/apps/codecov-api/core/commands/repository/interactors/tests/test_regenerate_repository_token.py new file mode 100644 index 0000000000..6519e6aaf4 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/tests/test_regenerate_repository_token.py @@ -0,0 +1,46 @@ +import pytest +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + OwnerFactory, + RepositoryFactory, + RepositoryTokenFactory, +) + +from codecov.commands.exceptions import ValidationError + +from ..regenerate_repository_token import RegenerateRepositoryTokenInteractor + + +class RegenerateRepositoryTokenInteractorTest(TestCase): + def setUp(self): + self.org = OwnerFactory(username="codecov") + self.active_repo = RepositoryFactory( + author=self.org, name="gazebo", active=True + ) + self.inactive_repo = RepositoryFactory( + author=self.org, name="backend", active=False + ) + self.repo_with_no_token = RepositoryFactory( + author=self.org, name="frontend", active=True + ) + RepositoryTokenFactory(repository=self.active_repo, key="random") + self.user = OwnerFactory( + organizations=[self.org.ownerid], + permission=[self.active_repo.repoid, self.repo_with_no_token.repoid], + ) + self.random_user = OwnerFactory(organizations=[self.org.ownerid]) + + def execute(self, owner, repo): + return RegenerateRepositoryTokenInteractor(owner, "github").execute( + repo_name=repo.name, + owner_username=self.org.username, + token_type="upload", + ) + + async def test_when_validation_error_repo_not_active(self): + with pytest.raises(ValidationError): + await self.execute(owner=self.random_user, repo=self.inactive_repo) + + async def test_when_validation_error_repo_not_viewable(self): + with pytest.raises(ValidationError): + await self.execute(owner=self.random_user, repo=self.active_repo) diff --git a/apps/codecov-api/core/commands/repository/interactors/tests/test_update_bundle_cache_config.py b/apps/codecov-api/core/commands/repository/interactors/tests/test_update_bundle_cache_config.py new file mode 100644 index 0000000000..50cd14c90e --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/tests/test_update_bundle_cache_config.py @@ -0,0 +1,91 @@ +import pytest +from asgiref.sync import async_to_sync +from django.test import TestCase +from shared.django_apps.bundle_analysis.models import CacheConfig +from shared.django_apps.core.tests.factories import ( + OwnerFactory, + RepositoryFactory, +) + +from codecov.commands.exceptions import ValidationError + +from ..update_bundle_cache_config import UpdateBundleCacheConfigInteractor + + +class UpdateBundleCacheConfigInteractorTest(TestCase): + databases = {"default"} + + def setUp(self): + self.org = OwnerFactory(username="test-org") + self.repo = RepositoryFactory(author=self.org, name="test-repo", active=True) + self.user = OwnerFactory(permission=[self.repo.pk]) + + @async_to_sync + def execute(self, owner, repo_name=None, cache_config=[]): + return UpdateBundleCacheConfigInteractor(owner, "github").execute( + repo_name=repo_name, + owner_username="test-org", + cache_config=cache_config, + ) + + def test_repo_not_found(self): + with pytest.raises(ValidationError): + self.execute(owner=self.user, repo_name="wrong") + + def test_bundle_not_found(self): + with pytest.raises( + ValidationError, match="The following bundle names do not exist: wrong" + ): + self.execute( + owner=self.user, + repo_name="test-repo", + cache_config=[{"bundle_name": "wrong", "toggle_caching": True}], + ) + + def test_some_bundles_not_found(self): + CacheConfig.objects.create( + repo_id=self.repo.pk, bundle_name="bundle1", is_caching=True + ) + with pytest.raises( + ValidationError, match="The following bundle names do not exist: bundle2" + ): + self.execute( + owner=self.user, + repo_name="test-repo", + cache_config=[ + {"bundle_name": "bundle1", "toggle_caching": False}, + {"bundle_name": "bundle2", "toggle_caching": True}, + ], + ) + + def test_update_bundles_successfully(self): + CacheConfig.objects.create( + repo_id=self.repo.pk, bundle_name="bundle1", is_caching=True + ) + CacheConfig.objects.create( + repo_id=self.repo.pk, bundle_name="bundle2", is_caching=True + ) + + res = self.execute( + owner=self.user, + repo_name="test-repo", + cache_config=[ + {"bundle_name": "bundle1", "toggle_caching": False}, + {"bundle_name": "bundle2", "toggle_caching": True}, + ], + ) + + assert res == [ + {"bundle_name": "bundle1", "is_cached": False, "cache_config": False}, + {"bundle_name": "bundle2", "is_cached": True, "cache_config": True}, + ] + + assert len(CacheConfig.objects.all()) == 2 + + query = CacheConfig.objects.filter(repo_id=self.repo.pk, bundle_name="bundle1") + assert len(query) == 1 + assert query[0].is_caching == False + + query = CacheConfig.objects.filter(repo_id=self.repo.pk, bundle_name="bundle2") + assert len(query) == 1 + assert query[0].is_caching == True diff --git a/apps/codecov-api/core/commands/repository/interactors/tests/test_update_repository.py b/apps/codecov-api/core/commands/repository/interactors/tests/test_update_repository.py new file mode 100644 index 0000000000..df8bf7db44 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/tests/test_update_repository.py @@ -0,0 +1,25 @@ +import pytest +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov.commands.exceptions import Unauthorized + +from ..update_repository import UpdateRepositoryInteractor + + +class UpdateRepositoryInteractorTest(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov") + self.random_user = OwnerFactory(organizations=[]) + + def execute_unauthorized_owner(self): + return UpdateRepositoryInteractor(self.owner, "github").execute( + repo_name="repo-1", + owner=self.random_user, + default_branch=None, + activated=None, + ) + + async def test_when_validation_error_unauthorized_owner(self): + with pytest.raises(Unauthorized): + await self.execute_unauthorized_owner() diff --git a/apps/codecov-api/core/commands/repository/interactors/update_bundle_cache_config.py b/apps/codecov-api/core/commands/repository/interactors/update_bundle_cache_config.py new file mode 100644 index 0000000000..812d6ffa32 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/update_bundle_cache_config.py @@ -0,0 +1,63 @@ +from typing import Dict, List + +from asgiref.sync import sync_to_async +from shared.django_apps.bundle_analysis.models import CacheConfig +from shared.django_apps.bundle_analysis.service.bundle_analysis import ( + BundleAnalysisCacheConfigService, +) + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import ValidationError +from core.models import Repository + + +class UpdateBundleCacheConfigInteractor(BaseInteractor): + def validate( + self, repo: Repository, cache_config: List[Dict[str, str | bool]] + ) -> None: + # Find any missing bundle names + bundle_names = [ + bundle["bundle_name"] + for bundle in cache_config + # the value of bundle_name is always a string, just do this check to appease mypy + if isinstance(bundle["bundle_name"], str) + ] + existing_bundle_names = set( + CacheConfig.objects.filter( + repo_id=repo.pk, bundle_name__in=bundle_names + ).values_list("bundle_name", flat=True) + ) + missing_bundles = set(bundle_names) - existing_bundle_names + if missing_bundles: + raise ValidationError( + f"The following bundle names do not exist: {', '.join(missing_bundles)}" + ) + + @sync_to_async + def execute( + self, + owner_username: str, + repo_name: str, + cache_config: List[Dict[str, str | bool]], + ) -> List[Dict[str, str | bool]]: + _owner, repo = self.resolve_owner_and_repo( + owner_username, repo_name, only_viewable=True + ) + + self.validate(repo, cache_config) + + results = [] + for bundle in cache_config: + bundle_name = bundle["bundle_name"] + is_caching = bundle["toggle_caching"] + BundleAnalysisCacheConfigService.update_cache_option( + repo.pk, bundle_name, is_caching + ) + results.append( + { + "bundle_name": bundle_name, + "is_cached": is_caching, + "cache_config": is_caching, + } + ) + return results diff --git a/apps/codecov-api/core/commands/repository/interactors/update_repository.py b/apps/codecov-api/core/commands/repository/interactors/update_repository.py new file mode 100644 index 0000000000..4fbe12d9a0 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/update_repository.py @@ -0,0 +1,43 @@ +from typing import Optional + +from asgiref.sync import sync_to_async + +from codecov.commands.base import BaseInteractor +from codecov.commands.exceptions import Unauthenticated, Unauthorized, ValidationError +from codecov_auth.helpers import current_user_part_of_org +from codecov_auth.models import Owner +from core.models import Repository + + +class UpdateRepositoryInteractor(BaseInteractor): + def validate_owner(self, owner: Owner): + if not self.current_user.is_authenticated: + raise Unauthenticated() + + if not current_user_part_of_org(self.current_owner, owner): + raise Unauthorized() + + @sync_to_async + def execute( + self, + repo_name: str, + owner: Owner, + default_branch: Optional[str], + activated: Optional[bool], + ): + self.validate_owner(owner) + repo = Repository.objects.filter(author_id=owner.pk, name=repo_name).first() + if not repo: + raise ValidationError("Repo not found") + + if default_branch: + branch = repo.branches.filter(name=default_branch).first() + if branch is None: + raise ValidationError( + f"The branch '{default_branch}' is not in our records. Please provide a valid branch name.", + ) + + repo.branch = default_branch + if activated: + repo.activated = activated + repo.save() diff --git a/apps/codecov-api/core/commands/repository/interactors/utils.py b/apps/codecov-api/core/commands/repository/interactors/utils.py new file mode 100644 index 0000000000..2f81abefb9 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/interactors/utils.py @@ -0,0 +1,6 @@ +from shared.encryption.yaml_secret import yaml_secret_encryptor + + +def encode_secret_string(value) -> str: + encryptor = yaml_secret_encryptor + return "secret:%s" % encryptor.encode(value).decode() diff --git a/apps/codecov-api/core/commands/repository/repository.py b/apps/codecov-api/core/commands/repository/repository.py new file mode 100644 index 0000000000..26cd73bde6 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/repository.py @@ -0,0 +1,89 @@ +import uuid +from typing import Awaitable, Dict, List, Optional + +from codecov.commands.base import BaseCommand +from codecov_auth.models import Owner, RepositoryToken +from core.models import Repository +from timeseries.models import Dataset, MeasurementName + +from .interactors.activate_measurements import ActivateMeasurementsInteractor +from .interactors.encode_secret_string import EncodeSecretStringInteractor +from .interactors.erase_repository import EraseRepositoryInteractor +from .interactors.fetch_repository import FetchRepositoryInteractor +from .interactors.get_repository_token import GetRepositoryTokenInteractor +from .interactors.get_upload_token import GetUploadTokenInteractor +from .interactors.regenerate_repository_token import RegenerateRepositoryTokenInteractor +from .interactors.regenerate_repository_upload_token import ( + RegenerateRepositoryUploadTokenInteractor, +) +from .interactors.update_bundle_cache_config import UpdateBundleCacheConfigInteractor +from .interactors.update_repository import UpdateRepositoryInteractor + + +class RepositoryCommands(BaseCommand): + def fetch_repository(self, *args, **kwargs) -> Repository | None: + return self.get_interactor(FetchRepositoryInteractor).execute(*args, **kwargs) + + def regenerate_repository_upload_token( + self, + repo_name: str, + owner_username: str, + ) -> Awaitable[uuid.UUID]: + return self.get_interactor(RegenerateRepositoryUploadTokenInteractor).execute( + repo_name, owner_username + ) + + def update_repository( + self, + repo_name: str, + owner: Owner, + default_branch: Optional[str], + activated: Optional[bool], + ) -> None: + return self.get_interactor(UpdateRepositoryInteractor).execute( + repo_name, owner, default_branch, activated + ) + + def get_upload_token(self, repository: Repository) -> uuid.UUID: + return self.get_interactor(GetUploadTokenInteractor).execute(repository) + + def get_repository_token( + self, repository: Repository, token_type: RepositoryToken.TokenType + ) -> str: + return self.get_interactor(GetRepositoryTokenInteractor).execute( + repository, token_type + ) + + def regenerate_repository_token( + self, repo_name: str, owner_username: str, token_type: RepositoryToken.TokenType + ) -> str: + return self.get_interactor(RegenerateRepositoryTokenInteractor).execute( + repo_name, owner_username, token_type + ) + + def activate_measurements( + self, repo_name: str, owner_name: str, measurement_type: MeasurementName + ) -> Dataset: + return self.get_interactor(ActivateMeasurementsInteractor).execute( + repo_name, owner_name, measurement_type + ) + + def erase_repository(self, owner_username: str, repo_name: str) -> None: + return self.get_interactor(EraseRepositoryInteractor).execute( + owner_username, repo_name + ) + + def encode_secret_string(self, owner: Owner, repo_name: str, value: str) -> str: + return self.get_interactor(EncodeSecretStringInteractor).execute( + owner, repo_name, value + ) + + def update_bundle_cache_config( + self, + owner_username: str, + repo_name: str, + cache_config: List[Dict[str, str | bool]], + ) -> Awaitable[List[Dict[str, str | bool]]]: + return self.get_interactor(UpdateBundleCacheConfigInteractor).execute( + owner_username, repo_name, cache_config + ) diff --git a/apps/codecov-api/core/commands/repository/tests/__init__.py b/apps/codecov-api/core/commands/repository/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/commands/repository/tests/test_repository.py b/apps/codecov-api/core/commands/repository/tests/test_repository.py new file mode 100644 index 0000000000..bd956358a7 --- /dev/null +++ b/apps/codecov-api/core/commands/repository/tests/test_repository.py @@ -0,0 +1,34 @@ +from unittest.mock import patch + +from django.contrib.auth.models import AnonymousUser +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from ..repository import RepositoryCommands + + +class RepositoryCommandsTest(TestCase): + def setUp(self): + self.user = AnonymousUser() + self.org = OwnerFactory(username="codecov") + self.repo = RepositoryFactory(author=self.org) + self.command = RepositoryCommands(None, "github") + + @patch("core.commands.repository.repository.FetchRepositoryInteractor.execute") + def test_fetch_repository_to_interactor(self, interactor_mock): + self.command.fetch_repository(self.org, self.repo.name, []) + interactor_mock.assert_called_once_with(self.org, self.repo.name, []) + + @patch("core.commands.repository.repository.FetchRepositoryInteractor.execute") + def test_fetch_repository_to_interactor_with_enforcing_okta(self, interactor_mock): + self.command.fetch_repository( + self.org, self.repo.name, [], exclude_okta_enforced_repos=False + ) + interactor_mock.assert_called_once_with( + self.org, self.repo.name, [], exclude_okta_enforced_repos=False + ) + + @patch("core.commands.repository.repository.GetUploadTokenInteractor.execute") + def test_get_upload_token_to_interactor(self, interactor_mock): + self.command.get_upload_token(self.repo) + interactor_mock.assert_called_once_with(self.repo) diff --git a/apps/codecov-api/core/encoders.py b/apps/codecov-api/core/encoders.py new file mode 100644 index 0000000000..2665a63418 --- /dev/null +++ b/apps/codecov-api/core/encoders.py @@ -0,0 +1,10 @@ +from dataclasses import astuple, is_dataclass + +from django.core.serializers.json import DjangoJSONEncoder + + +class ReportJSONEncoder(DjangoJSONEncoder): + def default(self, obj): + if is_dataclass(obj): + return astuple(obj) + return super().default(self, obj) diff --git a/apps/codecov-api/core/management/commands/check_for_migration_conflicts.py b/apps/codecov-api/core/management/commands/check_for_migration_conflicts.py new file mode 100644 index 0000000000..29e77ac14f --- /dev/null +++ b/apps/codecov-api/core/management/commands/check_for_migration_conflicts.py @@ -0,0 +1,37 @@ +import os + +from django.apps import apps +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + def handle(self, *args, **options): + conflicts_found = False + for app in apps.get_app_configs(): + try: + migrations = os.listdir(f"{app.name}/migrations") + migrations_by_prefix = {} + for migration in migrations: + # If it doesn't end in .py it's not a migration file + if not migration.endswith(".py"): + continue + + prefix = migration[0:4] + migrations_by_prefix.setdefault(prefix, []).append(migration) + + for prefix, grouped_migrations in migrations_by_prefix.items(): + if len(grouped_migrations) > 1: + conflicts_found = True + print( # noqa: T201 + f"Conflict found in migrations for {app.name} with prefix {prefix}:" + ) + for grouped_migration in grouped_migrations: + print(grouped_migration) # noqa: T201 + # It's expected to not find migration folders for Django/3rd party apps + except FileNotFoundError: + pass + + if conflicts_found: + raise Exception("Found conflicts in migrations.") + else: + print("No conflicts found!") # noqa: T201 diff --git a/apps/codecov-api/core/management/commands/codecovPlans-Jan25.csv b/apps/codecov-api/core/management/commands/codecovPlans-Jan25.csv new file mode 100644 index 0000000000..9e3eae9597 --- /dev/null +++ b/apps/codecov-api/core/management/commands/codecovPlans-Jan25.csv @@ -0,0 +1,14 @@ +"id","created_at","updated_at","base_unit_price","benefits","billing_rate","is_active","marketing_name","max_seats","monthly_uploads_limit","paid_plan","name","tier_id","stripe_id" +10,2025-01-16 04:40:55.162 -0800,2025-01-24 11:33:46.043 -0800,0,"{""Configurable # of users"",""Unlimited public repositories"",""Unlimited private repositories""}",,true,Developer,,,false,users-trial,6, +9,2025-01-16 04:39:59.759 -0800,2025-01-24 11:34:10.038 -0800,10,"{""Configurable # of users"",""Unlimited public repositories"",""Unlimited private repositories"",""Priority Support""}",annually,true,Enterprise Cloud,,,true,users-enterprisey,4,price_1LmjzwGlVGuVgOrkIwlM46EU +8,2025-01-16 04:39:15.877 -0800,2025-01-24 11:34:31.904 -0800,12,"{""Configurable # of users"",""Unlimited public repositories"",""Unlimited private repositories"",""Priority Support""}",monthly,true,Enterprise Cloud,,,true,users-enterprisem,4,price_1LmjypGlVGuVgOrkzKtNqhwW +7,2025-01-16 04:38:12.544 -0800,2025-01-24 11:34:53.935 -0800,4,"{""Up to 10 users"",""Unlimited repositories"",""2500 private repo uploads"",""Patch coverage analysis""}",annually,true,Team,10,2500,true,users-teamy,2,price_1OCM2cGlVGuVgOrkMWUFjPFz +6,2025-01-16 04:37:08.918 -0800,2025-01-24 11:35:15.346 -0800,5,"{""Up to 10 users"",""Unlimited repositories"",""2500 private repo uploads"",""Patch coverage analysis""}",monthly,true,Team,10,2500,true,users-teamm,2,price_1OCM0gGlVGuVgOrkWDYEBtSL +5,2025-01-16 04:35:34.152 -0800,2025-01-24 11:35:42.724 -0800,10,"{""Includes 5 seats"",""$10 per additional seat"",""Unlimited public repositories"",""Unlimited private repositories"",""Priority Support""}",annually,true,Sentry Pro,5,,true,users-sentryy,5,price_1Mj1mMGlVGuVgOrkC0ORc6iW +4,2025-01-16 04:34:33.867 -0800,2025-01-24 11:35:48.218 -0800,12,"{""Includes 5 seats"",""$10 per additional seat"",""Unlimited public repositories"",""Unlimited private repositories"",""Priority Support""}",monthly,true,Sentry Pro,5,,true,users-sentrym,5,price_1Mj1kYGlVGuVgOrk7jucaZAa +3,2025-01-16 04:32:44.655 -0800,2025-01-24 11:36:09.660 -0800,10,"{""Configurable # of users"",""Unlimited public repositories"",""Unlimited private repositories"",""Priority Support""}",annually,true,Pro,,,true,users-pr-inappy,3,plan_H6P16wij3lUuxg +2,2025-01-16 04:30:42.897 -0800,2025-01-24 11:36:14.651 -0800,12,"{""Configurable # of users"",""Unlimited public repositories"",""Unlimited private repositories"",""Priority Support""}",monthly,true,Pro,,,true,users-pr-inappm,3,plan_H6P3KZXwmAbqPS +13,2025-01-23 14:25:04.793 -0800,2025-01-23 14:25:04.793 -0800,12,"{""Configurable # of users"",""Unlimited public repositories"",""Unlimited private repositories""}",,true,Github Marketplace,,,false,users,3, +12,2025-01-16 04:44:51.064 -0800,2025-01-24 11:33:14.405 -0800,0,"{""Up to 1 user"",""Unlimited public repositories"",""Unlimited private repositories""}",,true,Developer,1,250,false,users-developer,2, +11,2025-01-16 04:44:01.249 -0800,2025-01-24 11:33:28.532 -0800,0,"{""Up to 1 user"",""Unlimited public repositories"",""Unlimited private repositories""}",,true,Developer,1,250,false,users-basic,1, +1,2025-01-16 04:26:44.776 -0800,2025-01-24 11:36:32.387 -0800,0,"{""Up to 1 user"",""Unlimited public repositories"",""Unlimited private repositories""}",,false,Developer,1,,false,users-free,1, diff --git a/apps/codecov-api/core/management/commands/codecovTiers-Jan25.csv b/apps/codecov-api/core/management/commands/codecovTiers-Jan25.csv new file mode 100644 index 0000000000..a72a03924b --- /dev/null +++ b/apps/codecov-api/core/management/commands/codecovTiers-Jan25.csv @@ -0,0 +1,7 @@ +"id","created_at","updated_at","tier_name","bundle_analysis","test_analytics","flaky_test_detection","project_coverage","private_repo_support" +2,2025-01-16 04:22:06.838 -0800,2025-01-16 04:22:06.838 -0800,team,true,true,false,false,true +3,2025-01-16 04:22:47.221 -0800,2025-01-16 04:22:47.221 -0800,pro,true,true,true,true,true +4,2025-01-16 04:22:59.652 -0800,2025-01-16 04:22:59.652 -0800,enterprise,true,true,true,true,true +5,2025-01-16 04:23:08.585 -0800,2025-01-16 04:23:08.585 -0800,sentry,true,true,true,true,true +1,2025-01-16 04:21:40.374 -0800,2025-01-16 07:34:49.139 -0800,basic,true,true,false,true,true +6,2025-01-23 14:23:21.504 -0800,2025-01-23 14:23:21.504 -0800,trial,true,true,true,true,true diff --git a/apps/codecov-api/core/management/commands/delete_rate_limit_keys.py b/apps/codecov-api/core/management/commands/delete_rate_limit_keys.py new file mode 100644 index 0000000000..dcdfb4f363 --- /dev/null +++ b/apps/codecov-api/core/management/commands/delete_rate_limit_keys.py @@ -0,0 +1,27 @@ +from django.core.management.base import BaseCommand, CommandParser +from shared.helpers.redis import get_redis_connection + + +class Command(BaseCommand): + help = "This command is meant to delete all rate limit redis keys for either userId or ip." + + def add_arguments(self, parser: CommandParser) -> None: + # This argument switches the command to "anonymous mode" deleting all the ip based keys + parser.add_argument("--ip", type=bool) + + def handle(self, *args, **options): + redis = get_redis_connection() + + path = "rl-user:*" + if options["ip"]: + path = "rl-ip:*" + + try: + for key in redis.scan_iter(path): + # -1 means the key has no expiry + if redis.ttl(key) == -1: + print(f"Deleting key: {key.decode('utf-8')}") # noqa: T201 + redis.delete(key) + except Exception as e: + print("Error occurred when deleting redis keys") # noqa: T201 + print(e) # noqa: T201 diff --git a/apps/codecov-api/core/management/commands/insert_data_to_db_from_csv.py b/apps/codecov-api/core/management/commands/insert_data_to_db_from_csv.py new file mode 100644 index 0000000000..122f714a43 --- /dev/null +++ b/apps/codecov-api/core/management/commands/insert_data_to_db_from_csv.py @@ -0,0 +1,78 @@ +import csv + +from django.core.management.base import BaseCommand +from shared.django_apps.codecov_auth.models import Plan, Tier + + +class Command(BaseCommand): + help = "Insert data from a CSV file into the database for either plans or tiers" + + def add_arguments(self, parser): + parser.add_argument("csv_file", type=str, help="The path to the CSV file") + parser.add_argument( + "--model", + type=str, + choices=["plans", "tiers"], + required=True, + help="Specify the model to insert data into: plans or tiers", + ) + + def handle(self, *args, **kwargs): + csv_file_path = kwargs["csv_file"] + model_choice = kwargs["model"] + + # Determine which model to use + if model_choice == "plans": + Model = Plan + elif model_choice == "tiers": + Model = Tier + else: + self.stdout.write(self.style.ERROR("Invalid model choice")) + return + + with open(csv_file_path, newline="") as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + model_data = { + field: self.convert_value(value) + for field, value in row.items() + if field in [f.name for f in Model._meta.fields] + } + + # Handle ForeignKey for tier + if "tier_id" in row and model_choice == "plans": + try: + model_data["tier"] = Tier.objects.get(id=row["tier_id"]) + except Tier.DoesNotExist: + self.stdout.write( + self.style.ERROR( + f"Tier with id {row['tier_id']} does not exist. Skipping row." + ) + ) + continue + + try: + Model.objects.update_or_create( + defaults=model_data, + id=row.get("id"), + ) + self.stdout.write(self.style.SUCCESS(f"Inserted row: {row}")) + except Exception as e: + self.stdout.write(self.style.ERROR(f"Error inserting row: {e}")) + continue + + self.stdout.write( + self.style.SUCCESS( + f"Successfully inserted all data into {model_choice} from CSV" + ) + ) + + def convert_value(self, value): + """Convert CSV string values to appropriate Python types.""" + if value == "": + return None + if value.lower() == "true": + return True + if value.lower() == "false": + return False + return value diff --git a/apps/codecov-api/core/management/commands/update_gitlab_webhooks.py b/apps/codecov-api/core/management/commands/update_gitlab_webhooks.py new file mode 100644 index 0000000000..c97a919546 --- /dev/null +++ b/apps/codecov-api/core/management/commands/update_gitlab_webhooks.py @@ -0,0 +1,66 @@ +import uuid + +from asgiref.sync import async_to_sync +from django.core.management.base import BaseCommand, CommandParser +from django.db.models import Q +from shared.config import get_config +from shared.torngit.exceptions import TorngitClientError, TorngitRefreshTokenFailedError +from shared.torngit.gitlab import Gitlab + +from core.models import Repository +from services.repo_providers import RepoProviderService +from utils.repos import get_bot_user + + +class Command(BaseCommand): + def add_arguments(self, parser: CommandParser) -> None: + # this can be used to retry if there's an error - restart the command + # from the last ID printed before failure + parser.add_argument("--starting-repoid", type=int) + + def handle(self, *args, **options): + repos = Repository.objects.filter( + Q(author__service="gitlab") & ~Q(hookid=None) & Q(webhook_secret=None), + ).order_by("repoid") + + if options["starting_repoid"]: + repos = repos.filter(pk__gte=options["starting_repoid"]) + + webhook_url = get_config("setup", "webhook_url") or get_config( + "setup", "codecov_url" + ) + + for repo in repos: + user = get_bot_user(repo) + if user is None: + continue + + webhook_secret = str(uuid.uuid4()) + gitlab: Gitlab = RepoProviderService().get_adapter(user, repo) + + try: + async_to_sync(gitlab.edit_webhook)( + hookid=repo.hookid, + name=None, + url=f"{webhook_url}/webhooks/gitlab", + events={ + "push_events": True, + "issues_events": False, + "merge_requests_events": True, + "tag_push_events": False, + "note_events": False, + "job_events": False, + "build_events": True, + "pipeline_events": True, + "wiki_events": False, + }, + secret=webhook_secret, + ) + + repo.webhook_secret = webhook_secret + repo.save() + except TorngitClientError as e: + print("error making GitLab API call") # noqa: T201 + print(e) # noqa: T201 + except TorngitRefreshTokenFailedError: + print("refresh token failed") # noqa: T201 diff --git a/apps/codecov-api/core/middleware.py b/apps/codecov-api/core/middleware.py new file mode 100644 index 0000000000..eb72ed2dab --- /dev/null +++ b/apps/codecov-api/core/middleware.py @@ -0,0 +1,89 @@ +from typing import Optional + +from django.http import HttpRequest +from django.urls import resolve +from django.utils.deprecation import MiddlewareMixin +from django_prometheus.middleware import ( + Metrics, + PrometheusAfterMiddleware, + PrometheusBeforeMiddleware, +) + +from utils.services import get_long_service_name + +# Prometheus metrics that will be annotated with User-Agent http header as label +USER_AGENT_METRICS = [ + "django_http_requests_unknown_latency_including_middlewares_total", + "django_http_requests_latency_seconds_by_view_method", + "django_http_requests_unknown_latency_total", + "django_http_requests_total_by_method", + "django_http_requests_total_by_transport", + "django_http_requests_total_by_view_transport_method", + "django_http_requests_body_total_bytes", + "django_http_responses_total_by_templatename", + "django_http_responses_total_by_status", + "django_http_responses_total_by_status_view_method", + "django_http_responses_body_total_bytes", + "django_http_responses_total_by_charset", + "django_http_responses_streaming_total", + "django_http_exceptions_total_by_type", + "django_http_exceptions_total_by_view", +] + + +def get_service_long_name(request: HttpRequest) -> Optional[str]: + resolver_match = resolve(request.path_info) + service = resolver_match.kwargs.get("service") + if service is not None: + resolver_match.kwargs["service"] = get_long_service_name(service.lower()) + service = get_long_service_name(service.lower()) + return service + return None + + +class ServiceMiddleware(MiddlewareMixin): + def process_view(self, request, view_func, view_args, view_kwargs): + service = get_service_long_name(request) + if service: + view_kwargs["service"] = service + return None + + +class CustomMetricsWithUA(Metrics): + """ + django_prometheus Metrics class but with extra user_agent label for applicable metrics + """ + + def register_metric(self, metric_cls, name, documentation, labelnames=(), **kwargs): + # TODO: Re-enable a cheaper form of user-agent logging + # https://github.com/codecov/engineering-team/issues/1654 + # if name in USER_AGENT_METRICS: + # labelnames = list(labelnames) + ["user_agent"] + return super().register_metric( + metric_cls, name, documentation, labelnames=labelnames, **kwargs + ) + + +class AppMetricsBeforeMiddlewareWithUA(PrometheusBeforeMiddleware): + """ + django_prometheus monitoring middleware using custom Metrics class + """ + + metrics_cls = CustomMetricsWithUA + + +class AppMetricsAfterMiddlewareWithUA(PrometheusAfterMiddleware): + """ + django_prometheus monitoring middleware using custom Metrics class that injects User-Agent label when possible + """ + + metrics_cls = CustomMetricsWithUA + + def label_metric(self, metric, request, response=None, **labels): + new_labels = labels + # TODO: Re-enable a cheaper form of user-agent logging + # https://github.com/codecov/engineering-team/issues/1654 + # if metric._name in USER_AGENT_METRICS: + # new_labels = {"user_agent": request.headers.get("User-Agent", "none")} + # new_labels.update(labels) + return super().label_metric(metric, request, response=response, **new_labels) diff --git a/apps/codecov-api/core/models.py b/apps/codecov-api/core/models.py new file mode 100644 index 0000000000..3d4dfa9e16 --- /dev/null +++ b/apps/codecov-api/core/models.py @@ -0,0 +1 @@ +from shared.django_apps.core.models import * diff --git a/apps/codecov-api/core/permissions.py b/apps/codecov-api/core/permissions.py new file mode 100644 index 0000000000..906d828c7e --- /dev/null +++ b/apps/codecov-api/core/permissions.py @@ -0,0 +1,9 @@ +from rest_framework.permissions import SAFE_METHODS + + +class IsRepoOwner(object): + def is_user_owner_of_repo(self, user, repository): + pass + + def has_permission(self, request, view): + return request.method in SAFE_METHODS diff --git a/apps/codecov-api/core/signals.py b/apps/codecov-api/core/signals.py new file mode 100644 index 0000000000..92743ce072 --- /dev/null +++ b/apps/codecov-api/core/signals.py @@ -0,0 +1,43 @@ +import logging +from typing import Any, Dict, List, Type, cast + +from django.db.models.signals import post_save +from django.dispatch import receiver +from shared.django_apps.core.models import Commit + +from core.models import Repository +from utils.shelter import ShelterPubsub + +log = logging.getLogger(__name__) + + +@receiver(post_save, sender=Repository, dispatch_uid="shelter_sync_repo") +def update_repository( + sender: Type[Repository], instance: Repository, **kwargs: Dict[str, Any] +) -> None: + log.info(f"Signal triggered for repository {instance.repoid}") + created: bool = cast(bool, kwargs["created"]) + changes: Dict[str, Any] = instance.tracker.changed() + tracked_fields: List[str] = ["name", "upload_token", "author_id", "private"] + + if created or any(field in changes for field in tracked_fields): + data = { + "type": "repo", + "sync": "one", + "id": instance.repoid, + } + ShelterPubsub.get_instance().publish(data) + + +@receiver(post_save, sender=Commit, dispatch_uid="shelter_sync_commit") +def update_commit( + sender: Type[Commit], instance: Commit, **kwargs: Dict[str, Any] +) -> None: + branch: str = instance.branch + if branch and ":" in branch: + data = { + "type": "commit", + "sync": "one", + "id": instance.id, + } + ShelterPubsub.get_instance().publish(data) diff --git a/apps/codecov-api/core/tests/__init__.py b/apps/codecov-api/core/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/core/tests/test_admin.py b/apps/codecov-api/core/tests/test_admin.py new file mode 100644 index 0000000000..0dd1b36c45 --- /dev/null +++ b/apps/codecov-api/core/tests/test_admin.py @@ -0,0 +1,81 @@ +import uuid +from unittest.mock import MagicMock, patch + +from django.contrib.admin.sites import AdminSite +from django.test import TestCase +from shared.django_apps.codecov_auth.tests.factories import UserFactory +from shared.django_apps.core.tests.factories import RepositoryFactory + +from core.admin import RepositoryAdmin, RepositoryAdminForm +from core.models import Repository +from utils.test_utils import Client + + +class AdminTest(TestCase): + def setUp(self): + self.user = UserFactory() + self.repo_admin = RepositoryAdmin(Repository, AdminSite) + self.client = Client() + + def test_staff_can_access_admin(self): + self.user.is_staff = True + self.user.save() + + self.client.force_login(self.user) + response = self.client.get("/admin/") + self.assertEqual(response.status_code, 200) + + def test_non_staff_cannot_access_admin(self): + self.client.force_login(self.user) + response = self.client.get("/admin/") + self.assertEqual(response.status_code, 302) + + @patch("core.admin.admin.ModelAdmin.log_change") + def test_prev_and_new_values_in_log_entry(self, mocked_super_log_change): + repo = RepositoryFactory(using_integration=True) + repo.save() + repo.using_integration = False + form = MagicMock() + form.changed_data = ["using_integration"] + self.repo_admin.save_model( + request=MagicMock, new_obj=repo, form=form, change=True + ) + assert ( + repo.changed_fields["using_integration"] + == "prev value: True, new value: False" + ) + + message = [] + message.append({"changed": {"fields": ["using_integration"]}}) + self.repo_admin.log_change(MagicMock, repo, message) + mocked_super_log_change.assert_called_once() + assert message == [ + {"changed": {"fields": ["using_integration"]}}, + {"using_integration": "prev value: True, new value: False"}, + ] + + +class RepositoryAdminTests(AdminTest): + def test_webhook_secret_nullable(self): + repo = RepositoryFactory( + webhook_secret=str(uuid.uuid4()), + ) + self.assertIsNotNone(repo.webhook_secret) + data = { + "webhook_secret": "", + # all the required fields have to be filled out in the form even though they aren't changed + "name": repo.name, + "author": repo.author, + "service_id": repo.service_id, + "upload_token": repo.upload_token, + "image_token": repo.image_token, + "branch": repo.branch, + } + + form = RepositoryAdminForm(data=data, instance=repo) + self.assertTrue(form.is_valid()) + updated_instance = form.save() + self.assertIsNone(updated_instance.webhook_secret) + + repo.refresh_from_db() + self.assertIsNone(repo.webhook_secret) diff --git a/apps/codecov-api/core/tests/test_management_commands.py b/apps/codecov-api/core/tests/test_management_commands.py new file mode 100644 index 0000000000..fa307d97c4 --- /dev/null +++ b/apps/codecov-api/core/tests/test_management_commands.py @@ -0,0 +1,169 @@ +import csv +import os +import tempfile +import unittest.mock as mock +from io import StringIO + +import pytest +from django.core.management import call_command +from shared.config import ConfigHelper +from shared.django_apps.codecov_auth.models import Plan, Tier +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory +from shared.helpers.redis import get_redis_connection + + +@pytest.mark.django_db +def test_update_gitlab_webhook_command(mocker): + edit_webhook = mocker.patch("shared.torngit.gitlab.Gitlab.edit_webhook") + + get_config = mocker.patch("shared.config._get_config_instance") + config_helper = ConfigHelper() + config_helper.set_params( + { + "setup": { + "webhook_url": "http://example.com", + }, + } + ) + get_config.return_value = config_helper + + author = OwnerFactory(service="gitlab") + repo1 = RepositoryFactory(hookid=123, author=author, webhook_secret=None) + repo2 = RepositoryFactory(hookid=234, author=author, webhook_secret=None) + repo3 = RepositoryFactory(hookid=345, author=author, webhook_secret=None) + + call_command( + "update_gitlab_webhooks", + stdout=StringIO(), + stderr=StringIO(), + starting_repoid=repo2.pk, + ) + + repo1.refresh_from_db() + assert repo1.webhook_secret is None + repo2.refresh_from_db() + assert repo2.webhook_secret is not None + repo3.refresh_from_db() + assert repo3.webhook_secret is not None + + assert edit_webhook.mock_calls == [ + mock.call( + hookid="234", + name=None, + url="http://example.com/webhooks/gitlab", + events={ + "push_events": True, + "issues_events": False, + "merge_requests_events": True, + "tag_push_events": False, + "note_events": False, + "job_events": False, + "build_events": True, + "pipeline_events": True, + "wiki_events": False, + }, + secret=repo2.webhook_secret, + ), + mock.call( + hookid="345", + name=None, + url="http://example.com/webhooks/gitlab", + events={ + "push_events": True, + "issues_events": False, + "merge_requests_events": True, + "tag_push_events": False, + "note_events": False, + "job_events": False, + "build_events": True, + "pipeline_events": True, + "wiki_events": False, + }, + secret=repo3.webhook_secret, + ), + ] + + +@pytest.mark.django_db +def test_delete_rate_limit_keys_user_id(): + redis = get_redis_connection() + redis.set("rl-user:1", 1) + redis.set("rl-user:2", 1, ex=5000) + redis.set("rl-ip:1", 1) + + call_command( + "delete_rate_limit_keys", + stdout=StringIO(), + stderr=StringIO(), + ) + + assert redis.get("rl-user:1") is None + assert redis.get("rl-user:2") is not None + assert redis.get("rl-ip:1") is not None + + # Get rid of lingering keys + redis.delete("rl-ip:1") + redis.delete("rl-user:2") + + +@pytest.mark.django_db +def test_delete_rate_limit_keys_ip_option(): + redis = get_redis_connection() + redis.set("rl-ip:1", 1) + redis.set("rl-ip:2", 1, ex=5000) + redis.set("rl-user:1", 1) + + call_command( + "delete_rate_limit_keys", stdout=StringIO(), stderr=StringIO(), ip=True + ) + + assert redis.get("rl-ip:1") is None + assert redis.get("rl-ip:2") is not None + assert redis.get("rl-user:1") is not None + + # Get rid of lingering keys + redis.delete("rl-user:1") + redis.delete("rl-ip:2") + + +@pytest.mark.django_db +def test_insert_data_to_db_from_csv_for_plans_and_tiers(): + with tempfile.NamedTemporaryFile(mode="w", delete=False, newline="") as temp_csv: + writer = csv.writer(temp_csv) + writer.writerow(["id", "tier_name"]) + writer.writerow([1, "Tier 1"]) + writer.writerow([2, "Tier 2"]) + csv_path = temp_csv.name + + out = StringIO() + call_command("insert_data_to_db_from_csv", csv_path, "--model", "tiers", stdout=out) + + # Check the output + assert "Successfully inserted all data into tiers from CSV" in out.getvalue() + + # Verify the data was inserted correctly + assert Tier.objects.filter(tier_name="Tier 1").exists() + assert Tier.objects.filter(tier_name="Tier 2").exists() + + # Create a temporary CSV file + with tempfile.NamedTemporaryFile(mode="w", delete=False, newline="") as temp_csv: + writer = csv.writer(temp_csv) + writer.writerow( + ["name", "marketing_name", "base_unit_price", "tier_id", "is_active"] + ) + writer.writerow(["Plan A", "Marketing A", 100, 1, "true"]) + writer.writerow(["Plan B", "Marketing B", 200, 2, "false"]) + csv_path = temp_csv.name + + out = StringIO() + call_command("insert_data_to_db_from_csv", csv_path, "--model", "plans", stdout=out) + + # Check the output + assert "Successfully inserted all data into plans from CSV" in out.getvalue() + + # Verify the data was inserted correctly + assert Plan.objects.filter(name="Plan A").exists() + assert Plan.objects.filter(name="Plan B").exists() + + # Clean up the temporary file + os.remove(csv_path) diff --git a/apps/codecov-api/core/tests/test_managers.py b/apps/codecov-api/core/tests/test_managers.py new file mode 100644 index 0000000000..da69d5ebd3 --- /dev/null +++ b/apps/codecov-api/core/tests/test_managers.py @@ -0,0 +1,118 @@ +from datetime import datetime + +from django.test import TestCase +from django.utils import timezone +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) + +from core.models import Repository + + +class RepositoryQuerySetTests(TestCase): + def setUp(self): + self.repo1 = RepositoryFactory() + self.repo2 = RepositoryFactory() + + def test_with_latest_commit_totals_before(self): + totals = {"n": 10, "h": 5, "m": 3, "p": 2, "c": 100.0, "C": 80.0} + CommitFactory(totals=totals, repository=self.repo1) + + repo = Repository.objects.filter( + repoid=self.repo1.repoid + ).with_latest_commit_totals_before(datetime.now().isoformat(), None)[0] + assert repo.latest_commit_totals == totals + + def test_with_latest_coverage_change(self): + CommitFactory(totals={"c": 99}, repository=self.repo1) + CommitFactory(totals={"c": 98}, repository=self.repo1) + assert ( + Repository.objects.filter(repoid=self.repo1.repoid) + .with_latest_commit_totals_before(timezone.now().isoformat(), None, True) + .with_latest_coverage_change()[0] + .latest_coverage_change + == -1 + ) + + def test_get_or_create_from_github_repo_data(self): + owner = OwnerFactory() + + with self.subTest("doesnt crash when fork but no parent"): + repo_data = { + "id": 45, + "default_branch": "main", + "private": True, + "name": "test", + "fork": True, + } + + repo, created = Repository.objects.get_or_create_from_git_repo( + repo_data, owner + ) + assert created + assert repo.service_id == 45 + assert repo.branch == "main" + assert repo.private + assert repo.name == "test" + + def test_viewable_repos(self): + private_repo = RepositoryFactory(private=True) + public_repo = RepositoryFactory(private=False) + deleted_repo = RepositoryFactory(deleted=True) + + with self.subTest("when owner permission is none doesnt crash"): + owner = OwnerFactory(permission=None) + owned_repo = RepositoryFactory(author=owner) + + repos = Repository.objects.viewable_repos(owner) + assert repos.count() == 2 + + repoids = repos.values_list("repoid", flat=True) + assert public_repo.repoid in repoids + assert owned_repo.repoid in repoids + assert deleted_repo.repoid not in repoids + + with self.subTest("when repository do not have a name doesnt return it"): + owner = OwnerFactory(permission=None) + RepositoryFactory(author=owner, name=None) + RepositoryFactory(author=owner, name=None) + RepositoryFactory(author=owner, name=None) + + repos = Repository.objects.viewable_repos(owner) + assert repos.count() == 1 + # only public repo created above + repoids = repos.values_list("repoid", flat=True) + assert public_repo.repoid in repoids + assert deleted_repo.repoid not in repoids + + with self.subTest("when owner permission is not none, returns repos"): + owner = OwnerFactory(permission=[private_repo.repoid]) + owned_repo = RepositoryFactory(author=owner) + + repos = Repository.objects.viewable_repos(owner) + assert repos.count() == 3 + + repoids = repos.values_list("repoid", flat=True) + assert public_repo.repoid in repoids + assert owned_repo.repoid in repoids + assert private_repo.repoid in repoids + assert deleted_repo.repoid not in repoids + + with self.subTest("when user not authed, returns only public"): + repos = Repository.objects.viewable_repos(None) + assert repos.count() == 1 + + repoids = repos.values_list("repoid", flat=True) + assert public_repo.repoid in repoids + assert deleted_repo.repoid not in repoids + + with self.subTest("when repository is deleted, don't return it"): + owner = OwnerFactory() + deleted_owned_repo = RepositoryFactory(author=owner, deleted=True) + + repos = Repository.objects.viewable_repos(owner) + assert public_repo.repoid in repoids + assert deleted_repo not in repoids + assert deleted_owned_repo not in repoids diff --git a/apps/codecov-api/core/tests/test_middleware.py b/apps/codecov-api/core/tests/test_middleware.py new file mode 100644 index 0000000000..c8d43d865a --- /dev/null +++ b/apps/codecov-api/core/tests/test_middleware.py @@ -0,0 +1,132 @@ +from prometheus_client import REGISTRY + + +# TODO: consolidate with worker/helpers/tests/unit/test_checkpoint_logger.py into shared repo +class CounterAssertion: + def __init__(self, metric, labels, expected_value): + self.metric = metric + self.labels = labels + self.expected_value = expected_value + + self.before_value = None + self.after_value = None + + def __repr__(self): + return f"" + + +# TODO: consolidate with worker/helpers/tests/unit/test_checkpoint_logger.py into shared repo +class CounterAssertionSet: + def __init__(self, counter_assertions): + self.counter_assertions = counter_assertions + + def __enter__(self): + for assertion in self.counter_assertions: + assertion.before_value = ( + REGISTRY.get_sample_value(assertion.metric, labels=assertion.labels) + or 0 + ) + + def __exit__(self, exc_type, exc_value, exc_tb): + for assertion in self.counter_assertions: + assertion.after_value = ( + REGISTRY.get_sample_value(assertion.metric, labels=assertion.labels) + or 0 + ) + assert ( + assertion.after_value - assertion.before_value + == assertion.expected_value + ) + + +# TODO: Re-enable some cheaper form of user-agent logging +# https://github.com/codecov/engineering-team/issues/1654 +""" +class PrometheusUserAgentLabelTest(TestCase): + def test_user_agent_label_added(self): + user_agent = "iphone" + + counter_assertions = [ + CounterAssertion( + "django_http_requests_latency_seconds_by_view_method_count", + { + "view": "codecov.views.health", + "method": "GET", + "user_agent": user_agent, + }, + 1, + ), + CounterAssertion( + "django_http_requests_total_by_method_total", + {"user_agent": user_agent, "method": "GET"}, + 1, + ), + CounterAssertion( + "django_http_requests_total_by_transport_total", + {"transport": "http", "user_agent": user_agent}, + 1, + ), + CounterAssertion( + "django_http_requests_total_by_view_transport_method_total", + { + "view": "codecov.views.health", + "transport": "http", + "method": "GET", + "user_agent": user_agent, + }, + 1, + ), + CounterAssertion( + "django_http_requests_body_total_bytes_count", + {"user_agent": user_agent}, + 1, + ), + CounterAssertion( + "django_http_requests_total_by_transport_total", + {"transport": "http", "user_agent": user_agent}, + 1, + ), + CounterAssertion( + "django_http_responses_total_by_status_total", + {"status": "200", "user_agent": user_agent}, + 1, + ), + CounterAssertion( + "django_http_responses_total_by_status_view_method_total", + { + "status": "200", + "view": "codecov.views.health", + "method": "GET", + "user_agent": user_agent, + }, + 1, + ), + CounterAssertion( + "django_http_responses_body_total_bytes_count", + {"user_agent": user_agent}, + 1, + ), + CounterAssertion( + "django_http_responses_total_by_charset_total", + {"charset": "utf-8", "user_agent": user_agent}, + 1, + ), + ] + + with CounterAssertionSet(counter_assertions): + self.client.get( + "/", + headers={ + "User-Agent": user_agent, + }, + ) + + for metric in REGISTRY.collect(): + if metric.name in USER_AGENT_METRICS: + for sample in metric.samples: + assert ( + sample.labels["user_agent"] + == "none" # not all requests have User-Agent header defined + or sample.labels["user_agent"] == user_agent + ) +""" diff --git a/apps/codecov-api/core/tests/test_signals.py b/apps/codecov-api/core/tests/test_signals.py new file mode 100644 index 0000000000..b133bbc971 --- /dev/null +++ b/apps/codecov-api/core/tests/test_signals.py @@ -0,0 +1,103 @@ +from unittest.mock import call + +import pytest +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory +from shared.django_apps.core.tests.factories import CommitFactory, RepositoryFactory + + +@pytest.mark.django_db +def test_shelter_repo_sync(mocker): + publish = mocker.patch("google.cloud.pubsub_v1.PublisherClient.publish") + + # this triggers the publish via Django signals + repo = RepositoryFactory( + repoid=91728376, author=OwnerFactory(ownerid=555), private=False + ) + + # triggers publish on create + publish.assert_has_calls( + [ + call( + "projects/test-project-id/topics/test-topic-id", + b'{"type": "owner", "sync": "one", "id": 555}', + ), + call( + "projects/test-project-id/topics/test-topic-id", + b'{"type": "repo", "sync": "one", "id": 91728376}', + ), + ] + ) + + repo.upload_token = "b69cf44c-89d8-48c2-80c9-5508610d3ced" + repo.save() + + publish_calls = publish.call_args_list + assert len(publish_calls) == 3 + + # triggers publish on update + assert publish_calls[2] == call( + "projects/test-project-id/topics/test-topic-id", + b'{"type": "repo", "sync": "one", "id": 91728376}', + ) + + # Does not trigger another publish with untracked field + repo.message = "foo" + repo.save() + + publish_calls = publish.call_args_list + assert len(publish_calls) == 3 + + # Triggers call when owner is changed + repo.author = OwnerFactory(ownerid=888) + repo.save() + + publish_calls = publish.call_args_list + # 1 is for the new owner created + assert len(publish_calls) == 5 + publish.assert_has_calls( + [ + call( + "projects/test-project-id/topics/test-topic-id", + b'{"type": "owner", "sync": "one", "id": 888}', + ), + ] + ) + + # Triggers call when private is changed + repo.private = True + repo.save() + + # publish_calls = publish.call_args_list + assert len(publish_calls) == 6 + + +@pytest.mark.django_db +def test_shelter_commit_sync(mocker): + publish = mocker.patch("google.cloud.pubsub_v1.PublisherClient.publish") + + # this triggers the publish via Django signals - has to have this format + owner = OwnerFactory(ownerid=555) + commit = CommitFactory( + id=167829367, + branch="random:branch", + author=owner, + repository=RepositoryFactory(author=owner), + ) + + publish_calls = publish.call_args_list + # 3x cause there's a signal triggered when the commit factory requires a Repository and Owner + # which can't be null + assert len(publish_calls) == 3 + + # triggers publish on update + assert publish_calls[2] == call( + "projects/test-project-id/topics/test-topic-id", + b'{"type": "commit", "sync": "one", "id": 167829367}', + ) + + commit.branch = "normal-incompatible-branch" + commit.save() + + publish_calls = publish.call_args_list + # does not trigger another publish since unchanged length + assert len(publish_calls) == 3 diff --git a/apps/codecov-api/dev.sh b/apps/codecov-api/dev.sh new file mode 100755 index 0000000000..af6a527afd --- /dev/null +++ b/apps/codecov-api/dev.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# starts the development server using gunicorn +# NEVER run production with the --reload option command +echo "API: Starting gunicorn in dev mode" + +_start_gunicorn() { + if [ -n "$PROMETHEUS_MULTIPROC_DIR" ]; then + rm -r ${PROMETHEUS_MULTIPROC_DIR?}/* 2> /dev/null + mkdir -p "$PROMETHEUS_MULTIPROC_DIR" + fi + + export PYTHONWARNINGS=always + suffix="" + if [[ "$STATSD_HOST" ]]; then + suffix="--statsd-host ${STATSD_HOST}:${STATSD_PORT}" + fi + if [ "$RUN_ENV" == "ENTERPRISE" ] || [ "$RUN_ENV" == "DEV" ]; then + python manage.py migrate + python manage.py migrate --database "timeseries" timeseries + python manage.py pgpartition --yes --skip-delete + python manage.py insert_data_to_db_from_csv core/management/commands/codecovTiers-Jan25.csv --model tiers + python manage.py insert_data_to_db_from_csv core/management/commands/codecovPlans-Jan25.csv --model plans + fi + if [[ "$DEBUGPY" ]]; then + uv add debugpy + uv sync + python -m debugpy --listen 0.0.0.0:12345 -m gunicorn codecov.wsgi:application --reload --bind 0.0.0.0:8000 --access-logfile '-' --timeout "${GUNICORN_TIMEOUT:-600}" $suffix + fi + gunicorn codecov.wsgi:application --reload --bind 0.0.0.0:8000 --access-logfile '-' --timeout "${GUNICORN_TIMEOUT:-600}" $suffix +} + +if [ -z "$1" ]; +then + _start_gunicorn +else + exec "$@" +fi diff --git a/apps/codecov-api/development.yml b/apps/codecov-api/development.yml new file mode 100644 index 0000000000..3fe036145f --- /dev/null +++ b/apps/codecov-api/development.yml @@ -0,0 +1,60 @@ +github: + bot: + username: codecov-io + integration: + id: 254 + pem: src/certs/github.pem + +bitbucket: + bot: + username: codecov-io + +gitlab: + bot: + username: codecov-io + +site: + codecov: + require_ci_to_pass: yes + + coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: yes + patch: yes + changes: no + + parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + + javascript: + enable_partials: no + + comment: + layout: "reach, diff, flags, files, footer" + behavior: default + require_changes: no + require_base: no + require_head: yes + + +services: + minio: + hash_key: testixik8qdauiab1yiffydimvi72ekq + verify_ssl: false + host: 'minio' + port: 9000 + # bucket: + # region: + access_key_id: codecov-default-key + secret_access_key: codecov-default-secret + client_uploads: true + dsn: https://stage-web.codecov.dev diff --git a/apps/codecov-api/docker-compose.yml b/apps/codecov-api/docker-compose.yml new file mode 100644 index 0000000000..7145edf0db --- /dev/null +++ b/apps/codecov-api/docker-compose.yml @@ -0,0 +1,44 @@ +version: "3" + +services: + api: + image: ${API_DOCKER_REPO}:${API_DOCKER_VERSION} + depends_on: + - postgres + - redis + - timescale + volumes: + - ./:/app/apps/codecov-api + - ./docker/test.yml:/config/codecov.yml + entrypoint: + - ./dev.sh + environment: + - RUN_ENV=DEV + # Improves pytest-cov performance in python 3.12 + # https://github.com/nedbat/coveragepy/issues/1665#issuecomment-1937075835 + - COVERAGE_CORE=sysmon + env_file: + - .testenv + + postgres: + image: postgres:14-alpine + environment: + - POSTGRES_USER=postgres + - POSTGRES_HOST_AUTH_METHOD=trust + - POSTGRES_PASSWORD=password + volumes: + - type: tmpfs + target: /var/lib/postgresql/data + tmpfs: + size: 2048M + + timescale: + image: timescale/timescaledb-ha:pg14-latest + environment: + - POSTGRES_USER=postgres + - POSTGRES_HOST_AUTH_METHOD=trust + - POSTGRES_PASSWORD=password + + redis: + image: redis:6-alpine + diff --git a/apps/codecov-api/docker/Dockerfile b/apps/codecov-api/docker/Dockerfile new file mode 100644 index 0000000000..2c8ad34f89 --- /dev/null +++ b/apps/codecov-api/docker/Dockerfile @@ -0,0 +1,64 @@ +# syntax=docker/dockerfile:1.4 +ARG REQUIREMENTS_IMAGE +ARG BUILD_ENV=self-hosted +ARG BERGLAS_VERSION=2.0.6 + +FROM us-docker.pkg.dev/berglas/berglas/berglas:$BERGLAS_VERSION as berglas + +FROM $REQUIREMENTS_IMAGE as app + +COPY . /app/apps/codecov-api +WORKDIR /app/apps/codecov-api +RUN python manage.py collectstatic --no-input + + +FROM app as local + +FROM app as cloud +ARG RELEASE_VERSION +ENV RELEASE_VERSION=$RELEASE_VERSION +COPY --chmod=755 --from=berglas /bin/berglas /usr/local/bin/berglas + +FROM app as self-hosted + +# set settings module +ENV DJANGO_SETTINGS_MODULE="codecov.settings_enterprise" +# Remove the settings dev and enterprise files. +# These should *never* make it to enterprise. +RUN rm /app/apps/codecov-api/codecov/settings_dev.py && \ + rm /app/apps/codecov-api/codecov/settings_prod.py && \ + rm /app/apps/codecov-api/codecov/settings_test.py && \ + rm /app/apps/codecov-api/codecov/settings_staging.py && \ + rm /app/apps/codecov-api/dev.sh && \ + rm /app/apps/codecov-api/migrate.sh && \ + rm /app/apps/codecov-api/prod.sh && \ + rm /app/apps/codecov-api/staging.sh && \ + rm /app/apps/codecov-api/production.yml && \ + rm /app/apps/codecov-api/development.yml +# Remove unneeded folders +RUN rm -rf /app/apps/codecov-api/.github +RUN rm -rf /app/apps/codecov-api/.circleci +# Create the codecov user to run the container as +RUN addgroup --system application \ + && adduser --system codecov --ingroup application --home /home/codecov +RUN mkdir -p /config && chown codecov:application /config +# copy the enterprise settings module. +WORKDIR /app/apps/codecov-api +RUN chmod +x enterprise.sh && \ + chown codecov:application /app/apps/codecov-api +ARG RELEASE_VERSION +ENV RUN_ENV="ENTERPRISE" +ENV RELEASE_VERSION=$RELEASE_VERSION +ENV DJANGO_SETTINGS_MODULE="codecov.settings_enterprise" +ENV CODECOV_API_PORT=8000 +ENTRYPOINT ["./enterprise.sh"] + +FROM self-hosted as self-hosted-runtime +USER root +ARG EXTERNAL_DEPS_FOLDER=./external_deps +RUN mkdir $EXTERNAL_DEPS_FOLDER +RUN pip install --target $EXTERNAL_DEPS_FOLDER psycopg2-binary tlslite-ng +RUN chown codecov:application $EXTERNAL_DEPS_FOLDER +USER codecov + +FROM ${BUILD_ENV} diff --git a/apps/codecov-api/docker/Dockerfile-proxy b/apps/codecov-api/docker/Dockerfile-proxy new file mode 100644 index 0000000000..ba84fca69b --- /dev/null +++ b/apps/codecov-api/docker/Dockerfile-proxy @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:1.3 +FROM python:3.13-slim-bookworm + +ENV FRP_VERSION=v0.51.3 + +# RUN apk add --no-cache curl +RUN apt-get update +RUN apt-get install -y curl + +SHELL ["/bin/bash", "-c"] + +RUN addgroup --system frp \ +&& adduser --debug --system --home /var/frp --shell /sbin/nologin --ingroup frp frp \ +&& curl -fSL https://github.com/fatedier/frp/releases/download/${FRP_VERSION}/frp_${FRP_VERSION:1}_linux_amd64.tar.gz -o frp.tar.gz \ +&& tar -zxv -f frp.tar.gz \ +&& rm -rf frp.tar.gz \ +&& mv frp_*_linux_amd64 /frp \ +&& chown -R frp:frp /frp + +COPY --chown=frp:frp docker/frpc-entrypoint.sh /frp/entrypoint.sh +RUN chmod 755 /frp/entrypoint.sh +USER frp + +WORKDIR /frp +ADD docker/frpc.ini /frp/frpc.ini + +EXPOSE 6000 7000 + +CMD ["/frp/entrypoint.sh"] diff --git a/apps/codecov-api/docker/Dockerfile.requirements b/apps/codecov-api/docker/Dockerfile.requirements new file mode 100644 index 0000000000..2472c848a7 --- /dev/null +++ b/apps/codecov-api/docker/Dockerfile.requirements @@ -0,0 +1,56 @@ +# syntax=docker/dockerfile:1.4 +ARG PYTHON_IMAGE=ghcr.io/astral-sh/uv:python3.13-bookworm-slim + +# BUILD STAGE - Download dependencies from GitHub that require SSH access +FROM $PYTHON_IMAGE as build + +RUN apt-get update +RUN apt-get install -y \ + curl \ + git \ + build-essential \ + libffi-dev \ + libpq-dev + +ENV UV_LINK_MODE=copy \ + UV_COMPILE_BYTECODE=1 \ + UV_PYTHON_DOWNLOADS=never \ + UV_PYTHON=python \ + UV_PROJECT_ENVIRONMENT=/api + +# Then, add the rest of the project source code and install it +# Installing separately from its dependencies allows optimal layer caching +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv export --no-hashes --frozen --format requirements-txt > requirements.txt + + +RUN grep -v '^-e ' requirements.txt > requirements.remote.txt +# build all remote wheels +RUN pip wheel -w wheels --find-links wheels -r requirements.remote.txt + +# build all local packages to wheels +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv build --all-packages --wheel -o wheels + +# RUNTIME STAGE - Copy packages from build stage and install runtime dependencies +FROM $PYTHON_IMAGE + +# Our postgres driver psycopg2 requires libpq-dev as a runtime dependency +RUN apt-get update +RUN apt-get install -y \ + libpq-dev \ + make \ + curl \ + libexpat1 + +COPY --from=build /wheels/ /wheels/ + +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv pip install --no-deps --no-index --find-links=wheels wheels/* --system + diff --git a/apps/codecov-api/docker/codecov.yml b/apps/codecov-api/docker/codecov.yml new file mode 100644 index 0000000000..b4f96582ae --- /dev/null +++ b/apps/codecov-api/docker/codecov.yml @@ -0,0 +1,11 @@ +setup: + loglvl: DEBUG + codecov_url: "http://localhost:9000" + codecov_api_url: "http://localhost:9000" + enterprise_license: "" + http: + cookie_secret: "secret" +services: + database_url: "postgres://postgres:postgres@postgres/postgres" + minio: + hash_key: "testixik8qdauiab1yiffydimvi72ekq" #do not edit diff --git a/apps/codecov-api/docker/frpc-entrypoint.sh b/apps/codecov-api/docker/frpc-entrypoint.sh new file mode 100644 index 0000000000..a6afb3c6d5 --- /dev/null +++ b/apps/codecov-api/docker/frpc-entrypoint.sh @@ -0,0 +1,30 @@ +#!/bin/sh +if [ $SERVER_ADDR ]; then + sed -i "s|server_addr = 127.0.0.1|server_addr = $SERVER_ADDR|g" /frp/frpc.ini +fi +if [ $FRP_USER ]; then + sed -i "1 a user = $FRP_USER" /frp/frpc.ini +fi +if [ $PROXY_NAME ]; then + sed -i "s|ssh|$PROXY_NAME|g" /frp/frpc.ini +fi +if [ $SERVER_PORT ]; then + sed -i "s|server_port = 7000|server_port = $SERVER_PORT|g" /frp/frpc.ini +fi +if [ $PROTO ]; then + sed -i "s|type = tcp|type = $PROTO|g" /frp/frpc.ini +fi +if [ $LOCAL_IP ]; then + sed -i "s|local_ip = 127.0.0.1|local_ip = $LOCAL_IP|g" /frp/frpc.ini +fi +if [ $LOCAL_PORT ]; then + sed -i "s|local_port = 22|local_port = $LOCAL_PORT|g" /frp/frpc.ini +fi +if [ $REMOTE_PORT ]; then + sed -i "s|remote_port = 6000|remote_port = $REMOTE_PORT|g" /frp/frpc.ini +fi +if [ $DOMAIN ]; then + sed -i "s|subdomain = api|subdomain = $DOMAIN|g" /frp/frpc.ini + sed -i "s|\[api\]|\[$DOMAIN\]|g" /frp/frpc.ini +fi +/frp/frpc -c /frp/frpc.ini \ No newline at end of file diff --git a/apps/codecov-api/docker/frpc.ini b/apps/codecov-api/docker/frpc.ini new file mode 100644 index 0000000000..96dd8ee485 --- /dev/null +++ b/apps/codecov-api/docker/frpc.ini @@ -0,0 +1,10 @@ +[common] +server_addr = lt.codecov.dev +server_port = 7000 +authentication_method = token +token = {{ .Envs.FRP_TOKEN }} +[api] +type = http +local_port = 8000 +local_ip = api +subdomain = api \ No newline at end of file diff --git a/apps/codecov-api/docker/test.yml b/apps/codecov-api/docker/test.yml new file mode 100644 index 0000000000..2e0c4481aa --- /dev/null +++ b/apps/codecov-api/docker/test.yml @@ -0,0 +1,72 @@ +setup: + codecov_url: https://codecov.io + debug: no + loglvl: INFO + encryption_secret: "zp^P9*i8aR3" + timeseries: + enabled: true + okta: + iss: "https://example.okta.com" + http: + force_https: yes + cookie_secret: abc123 + timeouts: + connect: 10 + receive: 15 + +services: + database_url: postgres://postgres:password@postgres:5432/postgres + timeseries_database_url: postgres://postgres:password@timescale:5432/postgres + redis_url: redis://redis:6379 + minio: + hash_key: testixik8qdauiab1yiffydimvi72ekq # never change this + access_key_id: codecov-default-key + secret_access_key: codecov-default-secret + verify_ssl: false + +github: + bot: + username: codecov-io + integration: + id: 254 + pem: src/certs/github.pem + +bitbucket: + bot: + username: codecov-io + +gitlab: + bot: + username: codecov-io + +site: + codecov: + require_ci_to_pass: yes + + coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: yes + patch: yes + changes: no + + parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + + javascript: + enable_partials: no + + comment: + layout: "reach, diff, flags, files, footer" + behavior: default + require_changes: no + require_base: no + require_head: yes diff --git a/apps/codecov-api/enterprise.sh b/apps/codecov-api/enterprise.sh new file mode 100755 index 0000000000..c3e78fa813 --- /dev/null +++ b/apps/codecov-api/enterprise.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Starts the enterprise gunicorn server (no --reload) +echo "Starting api" + +GUNICORN_WORKERS=${GUNICORN_WORKERS:-2} +if [ "$GUNICORN_WORKERS" -gt 1 ]; +then + export PROMETHEUS_MULTIPROC_DIR="${PROMETHEUS_MULTIPROC_DIR:-$HOME/.prometheus}" + rm -r ${PROMETHEUS_MULTIPROC_DIR?}/* 2> /dev/null + mkdir -p "$PROMETHEUS_MULTIPROC_DIR" +fi + +if [[ "$CODECOV_WRAPPER" ]]; then +SUB="$CODECOV_WRAPPER" +else +SUB="" +fi +if [[ "$CODECOV_WRAPPER_POST" ]]; then +POST="$CODECOV_WRAPPER_POST" +else +POST="" +fi +statsd="" +if [[ "$STATSD_HOST" ]]; then + statsd="--statsd-host ${STATSD_HOST}:${STATSD_PORT} " +fi +if [[ "$1" = "api" || -z "$1" ]]; +then + # Migrate + python manage.py migrate + python manage.py migrate --database "timeseries" timeseries + # Start api + ${SUB}$prefix gunicorn codecov.wsgi:application --workers=$GUNICORN_WORKERS --threads=${GUNICORN_THREADS:-1} --worker-connections=${GUNICORN_WORKER_CONNECTIONS:-1000} --bind ${CODECOV_API_BIND:-0.0.0.0}:${CODECOV_API_PORT:-8000} --access-logfile '-' ${statsd}--timeout "${GUNICORN_TIMEOUT:-600}"${POST} +elif [[ "$1" = "rti" ]]; +then + # Start api + ${SUB}$prefix gunicorn codecov.wsgi:application --workers=$GUNICORN_WORKERS --bind ${CODECOV_API_BIND:-0.0.0.0}:${CODECOV_API_PORT:-8000} --access-logfile '-' ${statsd}--timeout "${GUNICORN_TIMEOUT:-600}"${POST} +elif [[ "$1" = "migrate" ]]; +then + python manage.py migrate + python manage.py migrate --database "timeseries" timeseries +else + exec "$@" +fi diff --git a/apps/codecov-api/graphql_api/__init__.py b/apps/codecov-api/graphql_api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/graphql_api/actions/commits.py b/apps/codecov-api/graphql_api/actions/commits.py new file mode 100644 index 0000000000..533e905dbf --- /dev/null +++ b/apps/codecov-api/graphql_api/actions/commits.py @@ -0,0 +1,137 @@ +from collections import defaultdict +from typing import Optional + +from django.db.models import Count, Prefetch, Q, QuerySet +from django.db.models.functions import Lower, Substr +from graphql import GraphQLResolveInfo + +from core.models import Commit, Pull, Repository +from graphql_api.types.enums import CommitStatus +from reports.models import CommitReport + + +def pull_commits(pull: Pull) -> QuerySet[Commit]: + subquery = ( + Commit.objects.filter( + pullid=pull.pullid, + repository_id=pull.repository_id, + ) + # Can't use `exclude(deleted=True)` here since that results in a + # `not (deleted and deleted is not null)` condition. + # We need `deleted is not true` in order for the query to use the right index. + .filter(deleted__isnot=True) + ) + + return Commit.objects.filter(id__in=subquery).defer("_report") + + +def load_commit_statuses( + commit_ids: list[int], +) -> dict[int, dict[CommitReport.ReportType, str]]: + qs = ( + CommitReport.objects.filter(commit__in=commit_ids) + .values_list("commit_id", "report_type", "sessions__state") + .annotate(sessions_count=Count("sessions")) + ) + + grouped: dict[tuple[int, CommitReport.ReportType], dict[str, int]] = defaultdict( + dict + ) + for id, report_type, state, count in qs: + # The above query generates a `LEFT OUTER JOIN` with a proper `GROUP BY`. + # However, it is also yielding rows with a `NULL` state in case a report does not have any uploads. + if not report_type or not state: + continue + grouped[(id, report_type)][state] = count + + results: dict[int, dict[CommitReport.ReportType, str]] = { + id: {} for id in commit_ids + } + for (id, report_type), states in grouped.items(): + status = CommitStatus.COMPLETED.value + if states.get("error", 0) > 0: + status = CommitStatus.ERROR.value + elif states.get("uploaded", 0) > 0: + status = CommitStatus.PENDING.value + + results[id][report_type] = status + + return results + + +def commit_status( + info: GraphQLResolveInfo, commit: Commit, report_type: CommitReport.ReportType +) -> str | None: + commit_statuses = info.context.setdefault("commit_statuses", {}) + commit_status = commit_statuses.get(commit.id) + if commit_status is None: + updated_statuses = load_commit_statuses([commit.id]) + commit_statuses.update(updated_statuses) + commit_status = updated_statuses[commit.id] + + return commit_status.get(report_type) + + +def repo_commits( + repository: Repository, filters: Optional[dict] = None +) -> QuerySet[Commit]: + # prefetch the CommitReport with the ReportLevelTotals + prefetch = Prefetch( + "reports", + queryset=CommitReport.objects.coverage_reports() + .filter(code=None) + .select_related("reportleveltotals"), + ) + + # We don't select the `report` column here b/c it can be many MBs of JSON + # and can cause performance issues + queryset = repository.commits.defer("_report").prefetch_related(prefetch).all() + + # queryset filtering + filters = filters or {} + + hide_failed_ci = filters.get("hide_failed_ci") + if hide_failed_ci is True: + queryset = queryset.filter(ci_passed=True) + + branch_name = filters.get("branch_name") + if branch_name: + queryset = queryset.filter(branch=branch_name) + + pull_id = filters.get("pull_id") + if pull_id: + queryset = queryset.filter(pullid=pull_id) + + search = filters.get("search") + if search: + # search against long sha, short sha and commit message substring + queryset = queryset.annotate(short_sha=Substr(Lower("commitid"), 1, 7)).filter( + Q(commitid=search.lower()) + | Q(short_sha=search.lower()) + | Q(message__icontains=search) + ) + + states = filters.get("states") + if states: + queryset = queryset.filter(state__in=states) + + coverage_status = filters.get("coverage_status") + + if coverage_status: + # FIXME(swatinem): + # This filter here is insane, it resolves *all* the results in the unbounded queryset, + # just to check the status, and to then add it as another restricting filter. + # I’m pretty sure this will completely break the server if anyone actually uses this filter, lol. + commit_ids = [commit.id for commit in queryset] + commit_statuses = load_commit_statuses(commit_ids) + + to_be_included = [ + id + for id, statuses in commit_statuses.items() + if statuses.get(CommitReport.ReportType.COVERAGE) in coverage_status + ] + queryset = queryset.filter(id__in=to_be_included) + + # We need `deleted is not true` in order for the query to use the right index. + queryset = queryset.filter(deleted__isnot=True) + return queryset diff --git a/apps/codecov-api/graphql_api/actions/comparison.py b/apps/codecov-api/graphql_api/actions/comparison.py new file mode 100644 index 0000000000..4ad267a7b4 --- /dev/null +++ b/apps/codecov-api/graphql_api/actions/comparison.py @@ -0,0 +1,30 @@ +from typing import Optional, Union + +from compare.models import CommitComparison +from graphql_api.types.comparison.comparison import ( + MissingBaseReport, + MissingComparison, + MissingHeadReport, +) + + +def validate_commit_comparison( + commit_comparison: Optional[CommitComparison], +) -> Union[MissingBaseReport, MissingHeadReport, MissingComparison]: + if not commit_comparison: + return MissingComparison() + + if ( + commit_comparison.error + == CommitComparison.CommitComparisonErrors.MISSING_BASE_REPORT.value + ): + return MissingBaseReport() + + if ( + commit_comparison.error + == CommitComparison.CommitComparisonErrors.MISSING_HEAD_REPORT.value + ): + return MissingHeadReport() + + if commit_comparison.state == CommitComparison.CommitComparisonStates.ERROR: + return MissingComparison() diff --git a/apps/codecov-api/graphql_api/actions/components.py b/apps/codecov-api/graphql_api/actions/components.py new file mode 100644 index 0000000000..096689dba9 --- /dev/null +++ b/apps/codecov-api/graphql_api/actions/components.py @@ -0,0 +1,45 @@ +from datetime import datetime +from typing import Iterable, Mapping, Optional + +from django.db.models import QuerySet + +from core.models import Repository +from graphql_api.actions.measurements import ( + measurements_by_ids, + measurements_last_uploaded_by_ids, +) +from timeseries.models import Interval, MeasurementName + + +def component_measurements( + repository: Repository, + component_ids: Iterable[str], + interval: Interval, + after: datetime, + before: datetime, + branch: Optional[str] = None, +) -> Mapping[int, Iterable[dict]]: + return measurements_by_ids( + repository=repository, + measurable_name=MeasurementName.COMPONENT_COVERAGE.value, + measurable_ids=component_ids, + interval=interval, + after=after, + before=before, + branch=branch, + ) + + +def component_measurements_last_uploaded( + owner_id: int, + repo_id: int, + measurable_ids: str, + branch: Optional[str] = None, +) -> QuerySet: + return measurements_last_uploaded_by_ids( + owner_id=owner_id, + repo_id=repo_id, + measurable_name=MeasurementName.COMPONENT_COVERAGE.value, + measurable_ids=measurable_ids, + branch=branch, + ) diff --git a/apps/codecov-api/graphql_api/actions/flags.py b/apps/codecov-api/graphql_api/actions/flags.py new file mode 100644 index 0000000000..de7f18981e --- /dev/null +++ b/apps/codecov-api/graphql_api/actions/flags.py @@ -0,0 +1,65 @@ +from datetime import datetime +from typing import Iterable, Mapping + +from django.db.models import QuerySet + +from compare.models import CommitComparison, FlagComparison +from core.models import Repository +from graphql_api.actions.measurements import measurements_by_ids +from reports.models import RepositoryFlag +from timeseries.models import Interval, MeasurementName + + +def flags_for_repo(repository: Repository, filters: Mapping = {}) -> QuerySet: + queryset = RepositoryFlag.objects.filter( + repository=repository, + deleted__isnot=True, + ) + queryset = _apply_filters(queryset, filters or {}) + return queryset + + +def _apply_filters(queryset: QuerySet, filters: Mapping) -> QuerySet: + term = filters.get("term") + flags_names = filters.get("flags_names") + if flags_names: + queryset = queryset.filter(flag_name__in=flags_names) + if term: + queryset = queryset.filter(flag_name__contains=term) + + return queryset + + +def get_flag_comparisons( + commit_comparison: CommitComparison, +) -> Iterable[FlagComparison]: + queryset = ( + FlagComparison.objects.select_related("repositoryflag") + .filter(commit_comparison=commit_comparison.id) + .all() + ) + return queryset + + +def flag_measurements( + repository: Repository, + flag_ids: Iterable[int], + interval: Interval, + after: datetime, + before: datetime, +) -> Mapping[int, Iterable[dict]]: + measurements = measurements_by_ids( + repository=repository, + measurable_name=MeasurementName.FLAG_COVERAGE.value, + measurable_ids=[str(flag_id) for flag_id in flag_ids], + interval=interval, + after=after, + before=before, + ) + + # By default the measurable_id is str type, + # however for flags we need to convert it to an int + return { + int(measurable_id): measurement + for (measurable_id, measurement) in measurements.items() + } diff --git a/apps/codecov-api/graphql_api/actions/measurements.py b/apps/codecov-api/graphql_api/actions/measurements.py new file mode 100644 index 0000000000..f53abe9d14 --- /dev/null +++ b/apps/codecov-api/graphql_api/actions/measurements.py @@ -0,0 +1,92 @@ +from datetime import datetime +from typing import Any, Dict, Iterable, List, Optional + +from django.db.models import Max, QuerySet + +from core.models import Repository +from timeseries.helpers import aggregate_measurements, aligned_start_date +from timeseries.models import Interval, Measurement, MeasurementSummary + + +def measurements_by_ids( + repository: Repository, + measurable_name: str, + measurable_ids: Iterable[str], + interval: Interval, + before: datetime, + after: Optional[datetime] = None, + branch: Optional[str] = None, +) -> Dict[Any, List[Dict[str, Any]]]: + queryset = MeasurementSummary.agg_by(interval).filter( + name=measurable_name, + owner_id=repository.author_id, + repo_id=repository.pk, + measurable_id__in=measurable_ids, + timestamp_bin__lte=before, + ) + + if after is not None: + queryset = queryset.filter( + timestamp_bin__gte=aligned_start_date(interval, after) + ) + + if branch: + queryset = queryset.filter(branch=branch) + + queryset = aggregate_measurements( + queryset, ["timestamp_bin", "owner_id", "repo_id", "measurable_id"] + ) + + # group by measurable_id + measurements: Dict[Any, List[Dict[str, Any]]] = {} + for measurement in queryset: + measurable_id = measurement["measurable_id"] + if measurable_id not in measurements: + measurements[measurable_id] = [] + measurements[measurable_id].append(measurement) + + return measurements + + +def measurements_last_uploaded_before_start_date( + owner_id: int, + repo_id: int, + measurable_name: str, + measurable_id: int, + start_date: datetime, + branch: Optional[str] = None, +) -> QuerySet: + queryset = Measurement.objects.filter( + owner_id=owner_id, + repo_id=repo_id, + measurable_id=measurable_id, + name=measurable_name, + timestamp__lt=start_date, + ) + + if branch: + queryset = queryset.filter(branch=branch) + + return queryset.values("measurable_id", "value").annotate( + last_uploaded=Max("timestamp") + ) + + +def measurements_last_uploaded_by_ids( + owner_id: int, + repo_id: int, + measurable_name: str, + measurable_ids: str, + branch: Optional[str] = None, +) -> QuerySet: + queryset = Measurement.objects.filter( + owner_id=owner_id, + repo_id=repo_id, + measurable_id__in=measurable_ids, + name=measurable_name, + ) + + if branch: + queryset = queryset.filter(branch=branch) + + return queryset.values("measurable_id").annotate(last_uploaded=Max("timestamp")) diff --git a/apps/codecov-api/graphql_api/actions/owner.py b/apps/codecov-api/graphql_api/actions/owner.py new file mode 100644 index 0000000000..8f5b7580cd --- /dev/null +++ b/apps/codecov-api/graphql_api/actions/owner.py @@ -0,0 +1,35 @@ +from asgiref.sync import sync_to_async + +from codecov.commands.exceptions import MissingService +from codecov_auth.models import Owner +from utils.services import get_long_service_name + + +def search_my_owners(current_user, filters): + filters = filters if filters else {} + term = filters.get("term") + queryset = current_user.orgs.exclude(username=None) + if term: + queryset = queryset.filter(username__contains=term) + return queryset + + +@sync_to_async +def get_owner(service, username): + if not service: + raise MissingService() + + long_service = get_long_service_name(service) + return ( + Owner.objects.filter(username=username, service=long_service) + .prefetch_related("account") + .first() + ) + + +def get_owner_login_sessions(current_user): + return current_user.session_set.filter(type="login").all() + + +def get_user_tokens(owner: Owner): + return owner.user_tokens.all() diff --git a/apps/codecov-api/graphql_api/actions/path_contents.py b/apps/codecov-api/graphql_api/actions/path_contents.py new file mode 100644 index 0000000000..cda82d435f --- /dev/null +++ b/apps/codecov-api/graphql_api/actions/path_contents.py @@ -0,0 +1,26 @@ +import sentry_sdk + +from graphql_api.types.enums import PathContentDisplayType +from services.path import Dir, File + + +@sentry_sdk.trace +def sort_path_contents(items: list[File | Dir], filters: dict = {}) -> list[File | Dir]: + filter_parameter = filters.get("ordering", {}).get("parameter") + filter_direction = filters.get("ordering", {}).get("direction") + + if filter_parameter and filter_direction: + parameter_value = filter_parameter.value + direction_value = filter_direction.value + items.sort( + key=lambda item: getattr(item, parameter_value), + reverse=direction_value == "descending", + ) + display_type = filters.get("display_type", {}) + if ( + parameter_value == "name" + and display_type is not PathContentDisplayType.LIST + ): + items.sort(key=lambda item: isinstance(item, File)) + + return items diff --git a/apps/codecov-api/graphql_api/actions/repository.py b/apps/codecov-api/graphql_api/actions/repository.py new file mode 100644 index 0000000000..6bd03325e0 --- /dev/null +++ b/apps/codecov-api/graphql_api/actions/repository.py @@ -0,0 +1,98 @@ +import logging +from typing import Any + +from django.db.models import QuerySet +from shared.django_apps.codecov_auth.models import GithubAppInstallation, Owner +from shared.django_apps.core.models import Repository + +from utils.config import get_config + +log = logging.getLogger(__name__) +AI_FEATURES_GH_APP_ID = get_config("github", "ai_features_app_id") + + +def apply_filters_to_queryset( + queryset: QuerySet, filters: dict[str, Any] | None, owner: Owner | None = None +) -> QuerySet: + filters = filters or {} + term = filters.get("term") + active = filters.get("active") + activated = filters.get("activated") + repo_names = filters.get("repo_names") + is_public = filters.get("is_public") + ai_enabled = filters.get("ai_enabled") + + if repo_names: + queryset = queryset.filter(name__in=repo_names) + if term: + queryset = queryset.filter(name__contains=term) + if activated is not None: + queryset = queryset.filter(activated=activated) + if active is not None: + queryset = queryset.filter(active=active) + if is_public is not None: + queryset = queryset.filter(private=not is_public) + if ai_enabled is not None: + queryset = filter_queryset_by_ai_enabled_repos(queryset, owner) + return queryset + + +def filter_queryset_by_ai_enabled_repos(queryset: QuerySet, owner: Owner) -> QuerySet: + ai_features_app_install = GithubAppInstallation.objects.filter( + app_id=AI_FEATURES_GH_APP_ID, owner=owner + ).first() + + if not ai_features_app_install: + return Repository.objects.none() + + if ai_features_app_install.repository_service_ids: + queryset = queryset.filter( + service_id__in=ai_features_app_install.repository_service_ids + ) + + return queryset + + +def list_repository_for_owner( + current_owner: Owner, + owner: Owner, + filters: dict[str, Any] | None, + okta_account_auths: list[int], + exclude_okta_enforced_repos: bool = True, +) -> QuerySet: + queryset = Repository.objects.viewable_repos(current_owner) + filters = filters or {} + ai_enabled_filter = filters.get("ai_enabled") + + if ai_enabled_filter: + return filter_queryset_by_ai_enabled_repos(queryset, owner) + + if exclude_okta_enforced_repos: + queryset = queryset.exclude_accounts_enforced_okta(okta_account_auths) + + queryset = ( + queryset.with_recent_coverage().with_latest_commit_at().filter(author=owner) + ) + queryset = apply_filters_to_queryset(queryset, filters, owner) + return queryset + + +def search_repos( + current_owner: Owner, + filters: dict[str, Any] | None, + okta_account_auths: list[int], + exclude_okta_enforced_repos: bool = True, +) -> QuerySet: + authors_from = [current_owner.ownerid] + (current_owner.organizations or []) + queryset = Repository.objects.viewable_repos(current_owner) + + if exclude_okta_enforced_repos: + queryset = queryset.exclude_accounts_enforced_okta(okta_account_auths) + + queryset = ( + queryset.with_recent_coverage() + .with_latest_commit_at() + .filter(author__ownerid__in=authors_from) + ) + queryset = apply_filters_to_queryset(queryset, filters) + return queryset diff --git a/apps/codecov-api/graphql_api/apps.py b/apps/codecov-api/graphql_api/apps.py new file mode 100644 index 0000000000..de8bb1c341 --- /dev/null +++ b/apps/codecov-api/graphql_api/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class GraphqlApiConfig(AppConfig): + name = "graphql_api" diff --git a/apps/codecov-api/graphql_api/dataloader/bundle_analysis.py b/apps/codecov-api/graphql_api/dataloader/bundle_analysis.py new file mode 100644 index 0000000000..fed5af95f8 --- /dev/null +++ b/apps/codecov-api/graphql_api/dataloader/bundle_analysis.py @@ -0,0 +1,67 @@ +from typing import Union + +from shared.api_archive.archive import ArchiveService +from shared.bundle_analysis import ( + BundleAnalysisReportLoader, + MissingBaseReportError, + MissingHeadReportError, +) +from shared.storage import get_appropriate_storage_service + +from core.models import Commit +from graphql_api.types.comparison.comparison import MissingBaseReport, MissingHeadReport +from reports.models import CommitReport +from services.bundle_analysis import BundleAnalysisComparison, BundleAnalysisReport + + +def load_bundle_analysis_comparison( + base_commit: Commit, head_commit: Commit +) -> Union[BundleAnalysisComparison, MissingHeadReport, MissingBaseReport]: + head_report = CommitReport.objects.filter( + report_type=CommitReport.ReportType.BUNDLE_ANALYSIS, commit=head_commit + ).first() + if head_report is None: + return MissingHeadReport() + + base_report = CommitReport.objects.filter( + report_type=CommitReport.ReportType.BUNDLE_ANALYSIS, commit=base_commit + ).first() + if base_report is None: + return MissingBaseReport() + + loader = BundleAnalysisReportLoader( + storage_service=get_appropriate_storage_service(head_commit.repository.repoid), + repo_key=ArchiveService.get_archive_hash(head_commit.repository), + ) + + try: + return BundleAnalysisComparison( + loader=loader, + base_report_key=base_report.external_id, + head_report_key=head_report.external_id, + repository=head_commit.repository, + ) + except MissingBaseReportError: + return MissingBaseReport() + except MissingHeadReportError: + return MissingHeadReport() + + +def load_bundle_analysis_report( + commit: Commit, +) -> Union[BundleAnalysisReport, MissingHeadReport, MissingBaseReport]: + report = CommitReport.objects.filter( + report_type=CommitReport.ReportType.BUNDLE_ANALYSIS, commit=commit + ).first() + if report is None: + return MissingHeadReport() + + loader = BundleAnalysisReportLoader( + storage_service=get_appropriate_storage_service(commit.repository.repoid), + repo_key=ArchiveService.get_archive_hash(commit.repository), + ) + report = loader.load(report.external_id) + if report is None: + return MissingHeadReport() + + return BundleAnalysisReport(report) diff --git a/apps/codecov-api/graphql_api/dataloader/commit.py b/apps/codecov-api/graphql_api/dataloader/commit.py new file mode 100644 index 0000000000..871031f356 --- /dev/null +++ b/apps/codecov-api/graphql_api/dataloader/commit.py @@ -0,0 +1,34 @@ +from django.db.models import Prefetch + +from core.models import Commit +from reports.models import CommitReport + +from .loader import BaseLoader + + +class CommitLoader(BaseLoader): + @classmethod + def key(cls, commit): + return commit.commitid + + def __init__(self, info, repository_id, *args, **kwargs): + self.repository_id = repository_id + super().__init__(info, *args, **kwargs) + + def batch_queryset(self, keys): + # We don't select the `report` column here b/c then can be + # very large JSON blobs and cause performance issues + + # prefetch the CommitReport with the ReportLevelTotals + prefetch = Prefetch( + "reports", + queryset=CommitReport.objects.coverage_reports() + .filter(code=None) + .select_related("reportleveltotals"), + ) + + return ( + Commit.objects.filter(commitid__in=keys, repository_id=self.repository_id) + .defer("_report") + .prefetch_related(prefetch) + ) diff --git a/apps/codecov-api/graphql_api/dataloader/comparison.py b/apps/codecov-api/graphql_api/dataloader/comparison.py new file mode 100644 index 0000000000..eadfa66f16 --- /dev/null +++ b/apps/codecov-api/graphql_api/dataloader/comparison.py @@ -0,0 +1,137 @@ +import logging + +from asgiref.sync import sync_to_async + +from compare.models import CommitComparison +from core.models import Commit +from services.comparison import CommitComparisonService +from services.task import TaskService + +from .commit import CommitLoader +from .loader import BaseLoader + +log = logging.getLogger(__name__) + +comparison_table = CommitComparison._meta.db_table +commit_table = Commit._meta.db_table + + +class CommitCache: + def __init__(self, commits): + self.commits = [commit for commit in commits if commit] + self._by_pk = {commit.pk: commit for commit in self.commits} + self._by_commitid = {commit.commitid: commit for commit in self.commits} + + def get_by_pk(self, pk): + return self._by_pk.get(pk) + + def get_by_commitid(self, commitid): + return self._by_commitid.get(commitid) + + +class ComparisonLoader(BaseLoader): + @classmethod + def key(cls, commit_comparison): + return (commit_comparison.base_commitid, commit_comparison.compare_commitid) + + def __init__(self, info, repository_id, *args, **kwargs): + self.repository_id = repository_id + super().__init__(info, *args, **kwargs) + + def batch_queryset(self, keys): + return CommitComparisonService.fetch_precomputed(self.repository_id, keys) + + async def batch_load_fn(self, keys): + # flat list of all commits involved in all comparisons + commitids = {commitid for key in keys for commitid in key} + + commit_loader = CommitLoader.loader(self.info, self.repository_id) + commits = await commit_loader.load_many(commitids) + + commit_cache = CommitCache(commits) + + return await self._load_comparisons(keys, commit_cache) + + @sync_to_async + def _load_comparisons(self, keys, commit_cache): + # initial fetch of comparisons (we may be missing some at this point) + queryset = self.batch_queryset(keys) + comparisons = {self.key(record): record for record in queryset} + + # handle missing comparisons + missing_keys = set(keys) - set(comparisons.keys()) + if len(missing_keys) > 0: + # create comparisons for the missing keys + for record in self._create_comparisons(missing_keys, commit_cache): + comparisons[self.key(record)] = record + + # recalculate comparisons if needed + self._refresh_comparisons(comparisons, missing_keys, commit_cache) + + # return comparisons in the same order as `keys` + return [comparisons.get(key) for key in keys] + + def _create_comparisons(self, keys, commit_cache): + """ + Insert new comparisons for the given keys (skipping insert of any duplicates). + """ + comparisons = [ + CommitComparison( + base_commit=commit_cache.get_by_commitid(base_commitid), + compare_commit=commit_cache.get_by_commitid(compare_commitid), + ) + for (base_commitid, compare_commitid) in keys + if base_commitid + and commit_cache.get_by_commitid(base_commitid) + and compare_commitid + and commit_cache.get_by_commitid(compare_commitid) + ] + CommitComparison.objects.bulk_create( + comparisons, + ignore_conflicts=True, + ) + + # refetch missing comparisons (since they cannot be returned from the create call above + # due to the use of `ignore_conflicts`) + results = self.batch_queryset(keys) + + if len(results) != len(comparisons): + # We've been seeing some instances of commit comparisons being created but no + # corresponding compute comparisons task being enqueued. + # Not sure why this would happen but curious to see if we see this line in the logs + log.warning( + "Failed to refetch all commit comparisons", + extra=dict( + created_count=len(comparisons), + fetched_count=len(results), + ), + ) + + return results + + def _refresh_comparisons(self, comparisons, missing_keys, commit_cache): + """ + Recalculate comparisons for newly added or out-of-date comparisons. + """ + comparison_ids = [] + for key, comparison in comparisons.items(): + # we already have these commits fetched so we might as well store them + # on the comparison for the call to `needs_recompute` below + comparison.base_commit = commit_cache.get_by_pk(comparison.base_commit_id) + comparison.compare_commit = commit_cache.get_by_pk( + comparison.compare_commit_id + ) + + commit_comparison_service = CommitComparisonService(comparison) + if key in missing_keys or commit_comparison_service.needs_recompute(): + comparison_ids.append(comparison.pk) + + # optimistically update the state so we don't need to refetch this comparison + # (actual database update happens below) + comparison.state = CommitComparison.CommitComparisonStates.PENDING + + if len(comparison_ids) > 0: + CommitComparison.objects.filter(pk__in=comparison_ids).update( + state=CommitComparison.CommitComparisonStates.PENDING + ) + TaskService().compute_comparisons(comparison_ids) diff --git a/apps/codecov-api/graphql_api/dataloader/loader.py b/apps/codecov-api/graphql_api/dataloader/loader.py new file mode 100644 index 0000000000..26096e3109 --- /dev/null +++ b/apps/codecov-api/graphql_api/dataloader/loader.py @@ -0,0 +1,66 @@ +from typing import Self + +from aiodataloader import DataLoader +from asgiref.sync import sync_to_async +from graphql import GraphQLResolveInfo + + +class BaseLoader(DataLoader): + @classmethod + def loader(cls, info: GraphQLResolveInfo, *args) -> Self: + """ + Creates a new loader for the given `info` (instance of GraphQLResolveInfo) and `args`. + If a loader of this type already exists for the given `args` then that same object will + be returned from the request context. + """ + context_key = f"__dataloader_{cls.__name__}" + if len(args) > 0: + args_key = "_".join([str(arg) for arg in args]) + context_key += f"_{args_key}" + + if context_key not in info.context: + # one loader of a given (type, args) per request + info.context[context_key] = cls(info, *args) + + return info.context[context_key] + + def __init__(self, info, *args, **kwargs): + self.info = info + super().__init__(*args, **kwargs) + + @classmethod + def key(cls, record): + """ + Return the cache key for the given record (defaults to `id`) + """ + return record.id + + def cache(self, record): + """ + Prime the cache with the given record + """ + self.prime(self.key(record), record) + + def batch_queryset(self, keys): + """ + Return an unordered QuerySet that includes a record for every key in `keys`. + (ordering is handled in `batch_load_fn`) + """ + raise NotImplementedError("override batch_queryset in subclass") + + @sync_to_async + def batch_load_fn(self, keys): + """ + This implements the aiodataloader interface to batch load records for an + ordered list of keys. + + Each time we call `load` in the same tick of the event loop, aiodataloader + remembers the load key and defers the results. At the end of the tick we + batch load the records for all those keys. + """ + + queryset = self.batch_queryset(keys) + results = {self.key(record): record for record in queryset} + + # the returned list of records must be in the exact order of `keys` + return [results.get(key) for key in keys] diff --git a/apps/codecov-api/graphql_api/dataloader/owner.py b/apps/codecov-api/graphql_api/dataloader/owner.py new file mode 100644 index 0000000000..9013a46aee --- /dev/null +++ b/apps/codecov-api/graphql_api/dataloader/owner.py @@ -0,0 +1,12 @@ +from codecov_auth.models import Owner + +from .loader import BaseLoader + + +class OwnerLoader(BaseLoader): + @classmethod + def key(cls, owner): + return owner.ownerid + + def batch_queryset(self, keys): + return Owner.objects.filter(ownerid__in=keys) diff --git a/apps/codecov-api/graphql_api/dataloader/tests/test_bundle_analysis.py b/apps/codecov-api/graphql_api/dataloader/tests/test_bundle_analysis.py new file mode 100644 index 0000000000..1f7b27ea23 --- /dev/null +++ b/apps/codecov-api/graphql_api/dataloader/tests/test_bundle_analysis.py @@ -0,0 +1,163 @@ +from unittest.mock import patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import CommitFactory, RepositoryFactory + +from graphql_api.dataloader.bundle_analysis import ( + MissingBaseReportError, + MissingHeadReportError, + load_bundle_analysis_comparison, + load_bundle_analysis_report, +) +from graphql_api.types.comparison.comparison import MissingBaseReport, MissingHeadReport +from reports.models import CommitReport +from reports.tests.factories import CommitReportFactory + + +class MockReportLoader: + def load(self, external_id): + return True + + +class MockReportLoaderTwo: + def load(self, external_id): + return None + + +class MockBundleAnalysisLoaderServiceMissingHeadReport: + """ + During construction of the Comparison the shared code may raise an exception + when accessing head_report if it is not available + """ + + def __init__(self): + raise MissingHeadReportError() + + +class MockBundleAnalysisLoaderServiceMissingBaseReport: + """ + During construction of the Comparison the shared code may raise an exception + when accessing base_report if it is not available + """ + + def __init__(self): + raise MissingBaseReportError() + + +class BundleAnalysisComparisonLoader(TestCase): + def setUp(self): + self.repo = RepositoryFactory() + + self.base_commit = CommitFactory(repository=self.repo) + self.base_commit_report = CommitReportFactory( + commit=self.base_commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + self.head_commit = CommitFactory(repository=self.repo) + self.head_commit_report = CommitReportFactory( + commit=self.head_commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + @patch("graphql_api.dataloader.bundle_analysis.BundleAnalysisComparison") + @patch("graphql_api.dataloader.bundle_analysis.BundleAnalysisReportLoader") + def test_loader(self, mock_loader, mock_comparison): + mock_loader.return_value = None + mock_comparison.return_value = True + loader = load_bundle_analysis_comparison(self.base_commit, self.head_commit) + + assert loader == True + + def test_loader_missing_base_report(self): + base_commit = CommitFactory(repository=self.repo) + CommitReportFactory( + commit=base_commit, report_type=CommitReport.ReportType.COVERAGE + ) + + loader = load_bundle_analysis_comparison( + base_commit, + self.head_commit, + ) + assert loader.message == MissingBaseReport.message + + def test_loader_missing_head_report(self): + head_commit = CommitFactory(repository=self.repo) + CommitReportFactory( + commit=head_commit, report_type=CommitReport.ReportType.COVERAGE + ) + + loader = load_bundle_analysis_comparison( + self.base_commit, + head_commit, + ) + assert loader.message == MissingHeadReport.message + + def test_loader_no_base_report(self): + base_commit = CommitFactory(repository=self.repo) + + loader = load_bundle_analysis_comparison( + base_commit, + self.head_commit, + ) + assert loader.message == MissingBaseReport.message + + def test_loader_no_head_report(self): + head_commit = CommitFactory(repository=self.repo) + + loader = load_bundle_analysis_comparison( + self.base_commit, + head_commit, + ) + assert loader.message == MissingHeadReport.message + + def test_loader_raises_missing_head_report(self): + with patch( + "graphql_api.dataloader.bundle_analysis.BundleAnalysisComparison", + side_effect=MissingHeadReportError(), + ): + loader = load_bundle_analysis_comparison(self.base_commit, self.head_commit) + assert loader.message == MissingHeadReport.message + + def test_loader_raises_missing_base_report(self): + with patch( + "graphql_api.dataloader.bundle_analysis.BundleAnalysisComparison", + side_effect=MissingBaseReportError(), + ): + loader = load_bundle_analysis_comparison(self.base_commit, self.head_commit) + assert loader.message == MissingBaseReport.message + + +class BundleAnalysisReportLoader(TestCase): + def setUp(self): + self.repo = RepositoryFactory() + + self.commit = CommitFactory(repository=self.repo) + self.commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + @patch("graphql_api.dataloader.bundle_analysis.BundleAnalysisReport") + @patch("graphql_api.dataloader.bundle_analysis.BundleAnalysisReportLoader") + def test_loader(self, mock_loader, mock_report): + mock_loader.return_value = MockReportLoader() + mock_report.return_value = True + loader = load_bundle_analysis_report(self.commit) + assert loader == True + + @patch("graphql_api.dataloader.bundle_analysis.BundleAnalysisReportLoader") + def test_loader_missing_head_report_two(self, mock_loader): + mock_loader.return_value = MockReportLoaderTwo() + loader = load_bundle_analysis_report(self.commit) + assert loader.message == MissingHeadReport.message + + def test_loader_missing_head_report(self): + head_commit = CommitFactory(repository=self.repo) + CommitReportFactory( + commit=head_commit, report_type=CommitReport.ReportType.COVERAGE + ) + loader = load_bundle_analysis_report(head_commit) + assert loader.message == MissingHeadReport.message + + def test_loader_no_head_report(self): + head_commit = CommitFactory(repository=self.repo) + loader = load_bundle_analysis_report(head_commit) + assert loader.message == MissingHeadReport.message diff --git a/apps/codecov-api/graphql_api/dataloader/tests/test_commit.py b/apps/codecov-api/graphql_api/dataloader/tests/test_commit.py new file mode 100644 index 0000000000..5e8b7a9485 --- /dev/null +++ b/apps/codecov-api/graphql_api/dataloader/tests/test_commit.py @@ -0,0 +1,100 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + PullFactory, + RepositoryFactory, +) + +from graphql_api.dataloader.commit import CommitLoader + + +class GraphQLResolveInfo: + def __init__(self): + self.context = {} + + +class CommitLoaderTestCase(TestCase): + def setUp(self): + self.repository = RepositoryFactory(name="test-repo-1") + self.pull_1_commit = CommitFactory( + message="pull-1-head", repository=self.repository, commitid="123" + ) + self.pull_3_commits = [ + CommitFactory( + message="pull-3-commit-1", repository=self.repository, commitid="456" + ), + CommitFactory( + message="pull-3-commit-2", repository=self.repository, commitid="231" + ), + CommitFactory( + message="pull-3-head", repository=self.repository, commitid="159" + ), + ] + self.pull_2_commits = [ + CommitFactory( + message="pull-2-commit-1", repository=self.repository, commitid="153" + ), + CommitFactory( + message="pull-2-head", repository=self.repository, commitid="164" + ), + ] + self.base_commit = CommitFactory( + message="base-commit", repository=self.repository, commitid="346" + ) + self.pulls = [ + PullFactory( + pullid=11, + repository=self.repository, + title="test-pull-request-1", + head=self.pull_1_commit.commitid, + base=self.base_commit.commitid, + ), + PullFactory( + pullid=12, + repository=self.repository, + title="test-pull-request-2", + head=self.pull_3_commits[2].commitid, + base=self.base_commit.commitid, + ), + PullFactory( + pullid=13, + repository=self.repository, + title="test-pull-request-3", + head=self.pull_2_commits[1].commitid, + base=self.base_commit.commitid, + ), + ] + self.info = GraphQLResolveInfo() + + async def test_pull_with_one_commit(self): + loader = CommitLoader.loader(self.info, self.pulls[0].repository_id) + commit = await loader.load(self.pulls[0].head) + assert commit == self.pull_1_commit + + async def test_pull_with_many_commit(self): + loader = CommitLoader.loader(self.info, self.pulls[1].repository_id) + commit = await loader.load(self.pulls[1].head) + assert commit == self.pull_3_commits[2] + + async def test_pull_base_commit(self): + loader = CommitLoader.loader(self.info, self.pulls[0].repository_id) + commit = await loader.load(self.pulls[0].base) + assert commit == self.base_commit + + async def test_on_multiple_pulls_commit(self): + loader = CommitLoader.loader(self.info, self.pulls[1].repository_id) + commit = await loader.load(self.pulls[1].base) + assert commit == self.base_commit + + loader = CommitLoader.loader(self.info, self.pulls[2].repository_id) + commit_2 = await loader.load(self.pulls[2].base) + assert commit_2 == self.base_commit + + async def test_repeated_commit_in_(self): + loader = CommitLoader.loader(self.info, self.pulls[1].repository_id) + commit = await loader.load(self.pulls[1].base) + assert commit == self.base_commit + + loader = CommitLoader.loader(self.info, self.pulls[2].repository_id) + commit_2 = await loader.load(self.pulls[2].base) + assert commit_2 == self.base_commit diff --git a/apps/codecov-api/graphql_api/dataloader/tests/test_comparison.py b/apps/codecov-api/graphql_api/dataloader/tests/test_comparison.py new file mode 100644 index 0000000000..c5ea12c433 --- /dev/null +++ b/apps/codecov-api/graphql_api/dataloader/tests/test_comparison.py @@ -0,0 +1,92 @@ +from unittest.mock import patch + +from asgiref.sync import async_to_sync +from django.test import TestCase +from shared.django_apps.core.tests.factories import CommitFactory, RepositoryFactory + +from compare.models import CommitComparison +from compare.tests.factories import CommitComparisonFactory +from graphql_api.dataloader.comparison import ComparisonLoader + + +class GraphQLResolveInfo: + def __init__(self): + self.context = {} + + +async def load_comparisons(repoid, keys): + info = GraphQLResolveInfo() + loader = ComparisonLoader.loader(info, repoid) + return await loader.load_many(keys) + + +@patch("services.task.TaskService.compute_comparisons") +class ComparisonLoaderTestCase(TestCase): + def setUp(self): + self.repository = RepositoryFactory(name="test-repo-1") + + def _load(self, keys): + return async_to_sync(load_comparisons)(self.repository.pk, keys) + + def test_compare_commits_new_comparison(self, compute_comparisons): + commit1 = CommitFactory(repository=self.repository) + commit2 = CommitFactory(repository=self.repository) + + comparison = CommitComparison.objects.filter( + base_commit=commit1, + compare_commit=commit2, + ).first() + assert comparison is None + + (comparison,) = self._load([(commit1.commitid, commit2.commitid)]) + assert comparison is not None + assert comparison.base_commit == commit1 + assert comparison.compare_commit == commit2 + + compute_comparisons.assert_called_once_with([comparison.pk]) + comparison.refresh_from_db() + assert comparison.state == "pending" + + def test_compare_commits_existing_comparison(self, compute_comparisons): + commit1 = CommitFactory(repository=self.repository) + commit2 = CommitFactory(repository=self.repository) + + CommitComparisonFactory( + base_commit=commit1, + compare_commit=commit2, + state="processed", + ) + + (comparison,) = self._load([(commit1.commitid, commit2.commitid)]) + assert comparison is not None + assert comparison.base_commit == commit1 + assert comparison.compare_commit == commit2 + + assert not compute_comparisons.called + comparison.refresh_from_db() + assert comparison.state == "processed" + + def test_compare_commits_multiple_comparisons(self, compute_comparisons): + commit1 = CommitFactory(repository=self.repository) + commit2 = CommitFactory(repository=self.repository) + commit3 = CommitFactory(repository=self.repository) + + CommitComparisonFactory( + base_commit=commit1, + compare_commit=commit2, + ) + + comparison1, comparison2 = self._load( + [ + (commit1.commitid, commit2.commitid), + (commit2.commitid, commit3.commitid), + ] + ) + assert comparison1 is not None + assert comparison1.base_commit == commit1 + assert comparison1.compare_commit == commit2 + assert comparison2 is not None + assert comparison2.base_commit == commit2 + assert comparison2.compare_commit == commit3 + + compute_comparisons.assert_called_once_with([comparison2.pk]) diff --git a/apps/codecov-api/graphql_api/dataloader/tests/test_loader.py b/apps/codecov-api/graphql_api/dataloader/tests/test_loader.py new file mode 100644 index 0000000000..00d6939c04 --- /dev/null +++ b/apps/codecov-api/graphql_api/dataloader/tests/test_loader.py @@ -0,0 +1,27 @@ +import pytest +from django.test import TestCase +from shared.django_apps.core.tests.factories import CommitFactory + +from graphql_api.dataloader.loader import BaseLoader + + +class GraphQLResolveInfo: + def __init__(self): + self.context = {} + + +class BaseLoaderTestCase(TestCase): + def setUp(self): + # record type is irrelevant here + self.record = CommitFactory(message="test commit", commitid="123") + + self.info = GraphQLResolveInfo() + + async def test_unimplemented_load(self): + loader = BaseLoader.loader(self.info) + with pytest.raises(NotImplementedError): + await loader.load(self.record.id) + + async def test_default_key(self): + key = BaseLoader.key(self.record) + assert key == self.record.id diff --git a/apps/codecov-api/graphql_api/dataloader/tests/test_owner.py b/apps/codecov-api/graphql_api/dataloader/tests/test_owner.py new file mode 100644 index 0000000000..3b6478ad7b --- /dev/null +++ b/apps/codecov-api/graphql_api/dataloader/tests/test_owner.py @@ -0,0 +1,46 @@ +import asyncio + +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from graphql_api.dataloader.owner import OwnerLoader + + +class GraphQLResolveInfo: + def __init__(self): + self.context = {} + + +class OnwerLoaderTestCase(TestCase): + def setUp(self): + self.users = [ + OwnerFactory(username="codecov-1"), + OwnerFactory(username="codecov-2"), + OwnerFactory(username="codecov-3"), + OwnerFactory(username="codecov-4"), + OwnerFactory(username="codecov-5"), + ] + self.info = GraphQLResolveInfo() + + async def test_one_user(self): + loader = OwnerLoader.loader(self.info) + user = await loader.load(self.users[2].ownerid) + assert user == self.users[2] + + async def test_a_set_of_users(self): + loader = OwnerLoader.loader(self.info) + users = [ + loader.load(self.users[3].ownerid), + loader.load(self.users[2].ownerid), + loader.load(self.users[4].ownerid), + loader.load(self.users[0].ownerid), + loader.load(self.users[1].ownerid), + ] + users_loaded = await asyncio.gather(*users) + assert users_loaded == [ + self.users[3], + self.users[2], + self.users[4], + self.users[0], + self.users[1], + ] diff --git a/apps/codecov-api/graphql_api/helpers/__init__.py b/apps/codecov-api/graphql_api/helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/graphql_api/helpers/ariadne.py b/apps/codecov-api/graphql_api/helpers/ariadne.py new file mode 100644 index 0000000000..0b14e845e4 --- /dev/null +++ b/apps/codecov-api/graphql_api/helpers/ariadne.py @@ -0,0 +1,13 @@ +import pathlib + +from ariadne import load_schema_from_path + + +def ariadne_load_local_graphql(current_file, graphql_file): + """ + Given the current_file (__file__) of the caller and a graphql file name + import that file and load it with ariadne.load_schema_from_path + """ + current_dir = pathlib.Path(current_file).parent.absolute() + graphql_file_path = current_dir.joinpath(graphql_file) + return load_schema_from_path(graphql_file_path) diff --git a/apps/codecov-api/graphql_api/helpers/connection.py b/apps/codecov-api/graphql_api/helpers/connection.py new file mode 100644 index 0000000000..5eb3222604 --- /dev/null +++ b/apps/codecov-api/graphql_api/helpers/connection.py @@ -0,0 +1,271 @@ +import enum +from base64 import b64decode +from dataclasses import dataclass +from functools import cached_property +from typing import Any, Dict, List, Optional + +from asgiref.sync import sync_to_async +from cursor_pagination import CursorPage, CursorPaginator, InvalidCursor +from django.db.models import QuerySet + +from codecov.commands.exceptions import ValidationError +from graphql_api.types.enums import OrderingDirection + + +def build_connection_graphql(connection_name, type_node): + edge_name = connection_name + "Edge" + return f""" + type {connection_name} {{ + edges: [{edge_name}] + totalCount: Int! + pageInfo: PageInfo! + }} + + type {edge_name} {{ + cursor: String! + node: {type_node} + }} + """ + + +def field_order(field, ordering): + if isinstance(field, enum.Enum): + field = field.value + if ordering == OrderingDirection.DESC: + field = f"-{field}" + return field + + +@dataclass +class Connection: + queryset: QuerySet + paginator: CursorPaginator + page: CursorPage + + @cached_property + def edges(self): + return [ + {"cursor": self.paginator.cursor(self.page[pos]), "node": node} + for pos, node in enumerate(self.page) + ] + + @sync_to_async + def total_count(self, *args, **kwargs): + return self.queryset.count() + + @cached_property + def start_cursor(self): + return self.paginator.cursor(self.page[0]) if len(self.page) > 0 else None + + @cached_property + def end_cursor(self): + return self.paginator.cursor(self.page[-1]) if len(self.page) > 0 else None + + @sync_to_async + def page_info(self, *args, **kwargs): + return { + "has_next_page": self.page.has_next, + "has_previous_page": self.page.has_previous, + "start_cursor": self.start_cursor, + "end_cursor": self.end_cursor, + } + + +class ArrayPaginator: + """Cursor-based paginator for in-memory arrays.""" + + def __init__( + self, + data: List[Any], + first: Optional[int] = None, + last: Optional[int] = None, + after: Optional[str] = None, + before: Optional[str] = None, + ): + self.data = data + self.start_index = 0 + self.end_index = len(data) + + if first and last: + raise ValidationError("Cannot provide both 'first' and 'last'") + + if after is not None: + try: + self.start_index = int(after) + 1 + except ValueError: + raise ValidationError("'after' cursor must be an integer") + + if before is not None: + try: + self.end_index = min(self.end_index, int(before)) + except ValueError: + raise ValidationError("'before' cursor must be an integer") + + # Ensure valid bounds after 'after' and 'before' + self.start_index = max(self.start_index, 0) + self.end_index = min(self.end_index, len(data)) + + if first is not None: + self.end_index = min(self.start_index + first, len(data)) + + if last is not None: + range_length = self.end_index - self.start_index + if range_length > last: + self.start_index = self.end_index - last + + # Ensure bounds remain valid + self.start_index = max(self.start_index, 0) + self.end_index = min(self.end_index, len(data)) + + def cursor(self, position: int) -> str: + """Generate a cursor based on the position (index).""" + return str(position) + + @property + def page(self) -> List[Any]: + """Returns the sliced page of data.""" + return self.data[self.start_index : self.end_index] + + @property + def has_next(self) -> bool: + """Check if there's a next page.""" + return self.end_index < len(self.data) + + @property + def has_previous(self) -> bool: + """Check if there's a previous page.""" + return self.start_index > 0 + + +class ArrayConnection: + """Connection wrapper for array pagination.""" + + def __init__(self, paginator: ArrayPaginator): + self.data = paginator.data + self.paginator = paginator + self.page = paginator.page + + @property + def edges(self) -> List[Dict[str, Any]]: + """Generate edges with cursor and node information""" + return [ + {"cursor": self.paginator.cursor(pos), "node": node} + for pos, node in enumerate(self.page) + ] + + @property + def total_count(self) -> int: + """Total number of items in the original data""" + return len(self.data) + + @property + def start_cursor(self) -> Optional[str]: + """Cursor for the first item in the page""" + return self.paginator.cursor(self.paginator.start_index) if self.page else None + + @property + def end_cursor(self) -> Optional[str]: + """Cursor for the last item in the page""" + return ( + self.paginator.cursor(self.paginator.end_index - 1) if self.page else None + ) + + @property + def page_info(self) -> Dict[str, Any]: + """Pagination information""" + return { + "has_next_page": self.paginator.has_next, + "has_previous_page": self.paginator.has_previous, + "start_cursor": self.start_cursor, + "end_cursor": self.end_cursor, + } + + +class DictCursorPaginator(CursorPaginator): + NULL_VALUE_REPR = "\x1f" + """ + WARNING: DictCursorPaginator does not work for dict objects where a key contains the following string: "__" + TODO: if instance is a dictionary and not an object, don't split the ordering + + ordering = "test__name" + Django object: + -> obj.test.name + + Dict: + -> obj["test"]["name"] X wrong + we want obj["test__name"] + + overrides CursorPaginator's position_from_instance method + because it assumes that instance's fields are attributes on the + instance. This doesn't work with the aggregate_test_results query + because since it uses annotate() and values() the instance is actually + a dict and the fields are keys in that dict. + + So if getattr fails to find the attribute on the instance then we try getting the "attr" + via a dict access + + if the dict access fails then it throws an exception, although it would be a different + """ + + def decode_cursor(self, cursor): + try: + orderings = b64decode(cursor.encode("ascii")).decode("utf8") + orderings = orderings.split(self.delimiter) + return [ + None if ordering == self.NULL_VALUE_REPR else ordering + for ordering in orderings + ] + except (TypeError, ValueError): + raise InvalidCursor(self.invalid_cursor_message) + + def position_from_instance(self, instance): + position = [] + for order in self.ordering: + parts = order.lstrip("-").split("__") + attr = instance + while parts: + try: + attr = getattr(attr, parts[0]) + except AttributeError as attr_err: + try: + attr = attr[parts[0]] + except (KeyError, TypeError): + raise attr_err from None + parts.pop(0) + position.append(self.NULL_VALUE_REPR if attr is None else str(attr)) + return position + + +def queryset_to_connection_sync( + data: QuerySet | list, + *, + ordering=None, + ordering_direction=None, + first=None, + after=None, + last=None, + before=None, +): + """ + A method to take a queryset or an array and return it in paginated order based on the cursor pattern. + Handles both QuerySets (database queries) and arrays (in-memory data). + """ + if not first and not last: + first = 25 + + if isinstance(data, list): + array_paginator = ArrayPaginator( + data, first=first, last=last, after=after, before=before + ) + return ArrayConnection(array_paginator) + + else: + ordering = tuple(field_order(field, ordering_direction) for field in ordering) + paginator = DictCursorPaginator(data, ordering=ordering) + page = paginator.page(first=first, after=after, last=last, before=before) + return Connection(data, paginator, page) + + +@sync_to_async +def queryset_to_connection(*args, **kwargs): + return queryset_to_connection_sync(*args, **kwargs) diff --git a/apps/codecov-api/graphql_api/helpers/lookahead.py b/apps/codecov-api/graphql_api/helpers/lookahead.py new file mode 100644 index 0000000000..6de145bab9 --- /dev/null +++ b/apps/codecov-api/graphql_api/helpers/lookahead.py @@ -0,0 +1,75 @@ +from typing import Iterable, Optional + +from graphql.language.ast import ( + FragmentSpreadNode, + Node, + SelectionSetNode, + VariableNode, +) +from graphql.type.definition import GraphQLResolveInfo + + +class LookaheadNode: + """ + Wrapper around a node in the GraphQL AST that has a set of args + and potentially some child nodes. + """ + + def __init__(self, node: Node, info: GraphQLResolveInfo): + self.node = node + self.info = info + + @property + def args(self) -> dict[str, any]: + """ + Return a dict of this node's arguments as a name -> value mapping + """ + args = {} + for arg in self.node.arguments: + name = arg.name.value + value_node = arg.value + if isinstance(value_node, VariableNode): + variable_name = value_node.name.value + value = self.info.variable_values[variable_name] + else: + value = value_node.value + args[name] = value + return args + + def __getitem__(self, name: str) -> Optional["LookaheadNode"]: + """ + Get a child node by name + """ + if self.node.selection_set: + for selection in self._flatten_selections(self.node.selection_set): + if selection.name.value == name: + return LookaheadNode(selection, self.info) + + def _flatten_selections(self, selection_set: SelectionSetNode) -> Iterable[Node]: + """ + Expand fragments into flat list of selections + """ + selections = [] + for selection in selection_set.selections: + if isinstance(selection, FragmentSpreadNode): + fragment = self.info.fragments[selection.name.value] + for selection in fragment.selection_set.selections: + selections.append(selection) # noqa: PERF402 + else: + selections.append(selection) + return selections + + +def lookahead(info: GraphQLResolveInfo, path: Iterable[str]) -> Optional[LookaheadNode]: + """ + Traverse the GraphQL AST and return the lookahead node at the given `path` + """ + node = LookaheadNode(info.field_nodes[0], info) + + for item in path: + if node: + node = node[item] + else: + return None + + return node diff --git a/apps/codecov-api/graphql_api/helpers/mutation.py b/apps/codecov-api/graphql_api/helpers/mutation.py new file mode 100644 index 0000000000..a9739406ee --- /dev/null +++ b/apps/codecov-api/graphql_api/helpers/mutation.py @@ -0,0 +1,96 @@ +from codecov.commands import exceptions +from codecov_auth.helpers import current_user_part_of_org + + +class WrappedException: + """ + Our own class to wrap a Python exception as the core GraphQL library would + reraise an exception if a resolver returns an error (https://github.com/graphql-python/graphql-core/blob/c602d00b8a8f78bc349a911e0c26d73e1a9bbbac/src/graphql/execution/execute.py#L663-L666) + so we need to wrap it with a class that is not an Exception; so we can pass + it as a value to be returned by the mutation + """ + + exception = None + + def __init__(self, exception): + self.exception = exception + + def get_graphql_type(self): + """ + Map an exception from "codecov.commands.exceptions" to a GraphQL type + """ + error_to_graphql_type = { + exceptions.Unauthenticated: "UnauthenticatedError", + exceptions.Unauthorized: "UnauthorizedError", + exceptions.NotFound: "NotFoundError", + exceptions.ValidationError: "ValidationError", + exceptions.MissingService: "MissingService", + } + type_exception = type(self.exception) + return error_to_graphql_type.get(type_exception, None) + + def __getattr__(self, attr): + """ + Proxy all the attribute to the exception itself + """ + return getattr(self.exception, attr) + + +def wrap_error_handling_mutation(resolver): + async def resolver_with_error_handling(*args, **kwargs): + try: + return await resolver(*args, **kwargs) + except exceptions.BaseException as e: + # Wrap a pure Python exception with our Wrapper to pass as a value + return {"error": WrappedException(e)} + + return resolver_with_error_handling + + +def require_authenticated(resolver): + def authenticated_resolver(instance, info, *args, **kwargs): + current_user = info.context["request"].user + if not current_user.is_authenticated: + raise exceptions.Unauthenticated() + + return resolver(instance, info, *args, **kwargs) + + return authenticated_resolver + + +def require_part_of_org(resolver): + def authenticated_resolver(queried_owner, info, *args, **kwargs): + current_user = info.context["request"].user + current_owner = info.context["request"].current_owner + if ( + not current_user + or not current_user.is_authenticated + or not current_owner + or not current_user_part_of_org(current_owner, queried_owner) + ): + return None + + return resolver(queried_owner, info, *args, **kwargs) + + return authenticated_resolver + + +def require_shared_account_or_part_of_org(resolver): + def authenticated_resolver(queried_owner, info, *args, **kwargs): + current_user = info.context["request"].user + if ( + current_user + and current_user.is_authenticated + and queried_owner + and queried_owner.account + and current_user in queried_owner.account.users.all() + ): + return resolver(queried_owner, info, *args, **kwargs) + + return require_part_of_org(resolver)(queried_owner, info, *args, **kwargs) + + return authenticated_resolver + + +def resolve_union_error_type(error, *_): + return error.get_graphql_type() diff --git a/apps/codecov-api/graphql_api/helpers/requested_fields.py b/apps/codecov-api/graphql_api/helpers/requested_fields.py new file mode 100644 index 0000000000..c6a961e149 --- /dev/null +++ b/apps/codecov-api/graphql_api/helpers/requested_fields.py @@ -0,0 +1,59 @@ +# This was adapted from +from collections.abc import Generator, Iterable + +from graphql import GraphQLResolveInfo +from graphql.language import ( + FieldNode, + FragmentSpreadNode, + InlineFragmentNode, + SelectionNode, +) + + +def selected_fields(info: GraphQLResolveInfo) -> set[str]: + """ + Given a GraphQL "sub-query", this recursively collects all the queried fields. + + For example, if the original GraphQL query looks like `owner { repository { name } }`, + this would resolve to `repository` and `repository.name`. + + This function works by traversing the parts of the GraphQL Query AST which + are exposed to each "resolver". + """ + names: set[str] = set() + for node in info.field_nodes: + if node.selection_set is None: + continue + names.update(_fields_from_selections(info, node.selection_set.selections)) + return names + + +def _fields_from_selections( + info: GraphQLResolveInfo, selections: Iterable[SelectionNode] +) -> Generator[str, None, None]: + for selection in selections: + match selection: + case FieldNode(): + name = selection.name.value + yield name + + if selection.selection_set is not None: + yield from ( + f"{name}.{field}" + for field in _fields_from_selections( + info, selection.selection_set.selections + ) + ) + + case InlineFragmentNode(): + yield from _fields_from_selections( + info, selection.selection_set.selections + ) + case FragmentSpreadNode(): + fragment = info.fragments[selection.name.value] + yield from _fields_from_selections( + info, fragment.selection_set.selections + ) + + case _: + raise NotImplementedError(f"field type {type(selection)} not supported") diff --git a/apps/codecov-api/graphql_api/helpers/tests/__init__.py b/apps/codecov-api/graphql_api/helpers/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/graphql_api/helpers/tests/test_connection.py b/apps/codecov-api/graphql_api/helpers/tests/test_connection.py new file mode 100644 index 0000000000..fea9512cb0 --- /dev/null +++ b/apps/codecov-api/graphql_api/helpers/tests/test_connection.py @@ -0,0 +1,175 @@ +from asgiref.sync import async_to_sync +from django.test import TestCase +from shared.django_apps.core.tests.factories import RepositoryFactory + +from codecov.commands.exceptions import ValidationError +from core.models import Repository +from graphql_api.types.enums import OrderingDirection, RepositoryOrdering + + +class RepositoryQuerySetTests(TestCase): + def test_queryset_to_connection_deterministic_ordering(self): + from graphql_api.helpers.connection import queryset_to_connection + + repo_1 = RepositoryFactory(name="c") + repo_2 = RepositoryFactory(name="b") + repo_3 = RepositoryFactory(name="b") + repo_4 = RepositoryFactory(name="b") + repo_5 = RepositoryFactory(name="a") + + connection = async_to_sync(queryset_to_connection)( + Repository.objects.all(), + ordering=(RepositoryOrdering.NAME.value, RepositoryOrdering.ID.value), + ordering_direction=OrderingDirection.ASC, + ) + repos = [edge["node"] for edge in connection.edges] + + self.assertEqual(repos, [repo_5, repo_2, repo_3, repo_4, repo_1]) + + connection = async_to_sync(queryset_to_connection)( + Repository.objects.all(), + ordering=(RepositoryOrdering.NAME.value, RepositoryOrdering.ID.value), + ordering_direction=OrderingDirection.DESC, + ) + repos = [edge["node"] for edge in connection.edges] + + self.assertEqual(repos, [repo_1, repo_4, repo_3, repo_2, repo_5]) + + def test_queryset_to_connection_accepts_enum_for_ordering(self): + from graphql_api.helpers.connection import queryset_to_connection + + repo_1 = RepositoryFactory(name="a") + repo_2 = RepositoryFactory(name="b") + repo_3 = RepositoryFactory(name="c") + + connection = async_to_sync(queryset_to_connection)( + Repository.objects.all(), + ordering=(RepositoryOrdering.NAME,), + ordering_direction=OrderingDirection.ASC, + ) + repos = [edge["node"] for edge in connection.edges] + + self.assertEqual(repos, [repo_1, repo_2, repo_3]) + + def test_queryset_to_connection_defers_count(self): + from graphql_api.helpers.connection import queryset_to_connection + + RepositoryFactory(name="a") + RepositoryFactory(name="b") + RepositoryFactory(name="c") + + connection = async_to_sync(queryset_to_connection)( + Repository.objects.all(), + ordering=(RepositoryOrdering.NAME,), + ordering_direction=OrderingDirection.ASC, + ) + + count = async_to_sync(connection.total_count)() + assert count == 3 + + def test_array_pagination_first_after(self): + from graphql_api.helpers.connection import queryset_to_connection_sync + + data = [1, 2, 3, 4, 5] + + # Test first parameter + connection = queryset_to_connection_sync(data, first=2) + self.assertEqual([edge["node"] for edge in connection.edges], [1, 2]) + self.assertTrue(connection.page_info["has_next_page"]) + self.assertFalse(connection.page_info["has_previous_page"]) + + # Test after parameter + connection = queryset_to_connection_sync(data, first=2, after="1") + self.assertEqual([edge["node"] for edge in connection.edges], [3, 4]) + self.assertTrue(connection.page_info["has_next_page"]) + self.assertTrue(connection.page_info["has_previous_page"]) + + def test_array_pagination_last_before(self): + from graphql_api.helpers.connection import queryset_to_connection_sync + + data = [1, 2, 3, 4, 5] + + # Test last parameter + connection = queryset_to_connection_sync(data, last=2) + self.assertEqual([edge["node"] for edge in connection.edges], [4, 5]) + self.assertFalse(connection.page_info["has_next_page"]) + self.assertTrue(connection.page_info["has_previous_page"]) + + # Test before parameter + connection = queryset_to_connection_sync(data, last=2, before="4") + self.assertEqual([edge["node"] for edge in connection.edges], [3, 4]) + self.assertTrue(connection.page_info["has_next_page"]) + self.assertTrue(connection.page_info["has_previous_page"]) + + def test_array_pagination_edge_cases(self): + from graphql_api.helpers.connection import queryset_to_connection_sync + + data = [1, 2, 3] + + # Empty array + connection = queryset_to_connection_sync([], first=2) + self.assertEqual(connection.edges, []) + self.assertEqual(connection.total_count, 0) + + # First greater than array length + connection = queryset_to_connection_sync(data, first=5) + self.assertEqual([edge["node"] for edge in connection.edges], [1, 2, 3]) + self.assertEqual(connection.total_count, 3) + + # Last greater than array length + connection = queryset_to_connection_sync(data, last=5) + self.assertEqual([edge["node"] for edge in connection.edges], [1, 2, 3]) + self.assertEqual(connection.total_count, 3) + + def test_array_pagination_edge_cases_with_before_cursor_2(self): + from graphql_api.helpers.connection import queryset_to_connection_sync + + data = [1, 2, 3, 4, 5] + + connection = queryset_to_connection_sync(data, last=3, before="3") + self.assertEqual([edge["node"] for edge in connection.edges], [1, 2, 3]) + + def test_array_pagination_edge_cases_with_before_and_after(self): + from graphql_api.helpers.connection import queryset_to_connection_sync + + data = [1, 2, 3, 4, 5] + + connection = queryset_to_connection_sync(data, last=3, before="3", after="0") + self.assertEqual([edge["node"] for edge in connection.edges], [2, 3]) + + def test_both_first_and_last(self): + from graphql_api.helpers.connection import queryset_to_connection_sync + + data = [1, 2, 3, 4, 5] + + with self.assertRaises(ValidationError): + queryset_to_connection_sync(data, last=3, first=2) + + def test_invalid_cursors(self): + from graphql_api.helpers.connection import queryset_to_connection_sync + + data = [1, 2, 3, 4, 5] + + with self.assertRaises(ValidationError): + queryset_to_connection_sync(data, last=3, before="invalid") + + with self.assertRaises(ValidationError): + queryset_to_connection_sync(data, first=3, after="invalid") + + def test_dict_cursor_paginator_null_encoding(self): + from graphql_api.helpers.connection import DictCursorPaginator, field_order + + repo_1 = RepositoryFactory(name="a", active=None) + repo_2 = RepositoryFactory(name="b", active=True) + repo_3 = RepositoryFactory(name="c", active=False) + r = Repository.objects.all() + + ordering = tuple( + field_order(field, OrderingDirection.ASC) for field in ("active",) + ) + + paginator = DictCursorPaginator(r, ordering=ordering) + + assert paginator.position_from_instance(repo_1) == ["\x1f"] + assert paginator.position_from_instance(repo_2) == ["True"] + assert paginator.position_from_instance(repo_3) == ["False"] diff --git a/apps/codecov-api/graphql_api/helpers/tests/test_mutation.py b/apps/codecov-api/graphql_api/helpers/tests/test_mutation.py new file mode 100644 index 0000000000..b2d656a7c0 --- /dev/null +++ b/apps/codecov-api/graphql_api/helpers/tests/test_mutation.py @@ -0,0 +1,74 @@ +from asgiref.sync import sync_to_async +from django.test import SimpleTestCase + +from codecov.commands.exceptions import ( + NotFound, + Unauthenticated, + Unauthorized, + ValidationError, +) + +from ..mutation import resolve_union_error_type, wrap_error_handling_mutation + + +class HelperMutationTest(SimpleTestCase): + async def test_mutation_when_everything_is_good(self): + @wrap_error_handling_mutation + @sync_to_async + def resolver(): + return "5" + + assert await resolver() == "5" + + async def test_mutation_when_unauthenticated_is_raised(self): + @wrap_error_handling_mutation + @sync_to_async + def resolver(): + raise Unauthenticated() + + resolved_value = await resolver() + assert resolved_value["error"].message == "You are not authenticated" + graphql_type_error = resolve_union_error_type(resolved_value["error"]) + assert graphql_type_error == "UnauthenticatedError" + + async def test_mutation_when_unauthorized_is_raised(self): + @wrap_error_handling_mutation + @sync_to_async + def resolver(): + raise Unauthorized() + + resolved_value = await resolver() + assert resolved_value["error"].message == "You are not authorized" + graphql_type_error = resolve_union_error_type(resolved_value["error"]) + assert graphql_type_error == "UnauthorizedError" + + async def test_mutation_when_validation_is_raised(self): + @wrap_error_handling_mutation + @sync_to_async + def resolver(): + raise ValidationError("wrong data") + + resolved_value = await resolver() + assert resolved_value["error"].message == "wrong data" + graphql_type_error = resolve_union_error_type(resolved_value["error"]) + assert graphql_type_error == "ValidationError" + + async def test_mutation_when_not_found_is_raised(self): + @wrap_error_handling_mutation + @sync_to_async + def resolver(): + raise NotFound() + + resolved_value = await resolver() + assert resolved_value["error"].message == "Cant find the requested resource" + graphql_type_error = resolve_union_error_type(resolved_value["error"]) + assert graphql_type_error == "NotFoundError" + + async def test_mutation_when_random_exception_is_raised_it_reraise(self): + @wrap_error_handling_mutation + @sync_to_async + def resolver(): + raise AttributeError() + + with self.assertRaises(AttributeError): + await resolver() diff --git a/apps/codecov-api/graphql_api/schema.py b/apps/codecov-api/graphql_api/schema.py new file mode 100644 index 0000000000..e37b22cb04 --- /dev/null +++ b/apps/codecov-api/graphql_api/schema.py @@ -0,0 +1,7 @@ +from ariadne import make_executable_schema + +from .types import bindables, types + +# convert_names_case automatically converts the field name from camelCase +# to snake_case. See: https://ariadnegraphql.org/docs/api-reference#optional-arguments-10 +schema = make_executable_schema(types, *bindables, convert_names_case=True) diff --git a/apps/codecov-api/graphql_api/tests/__init__.py b/apps/codecov-api/graphql_api/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/graphql_api/tests/actions/test_commits.py b/apps/codecov-api/graphql_api/tests/actions/test_commits.py new file mode 100644 index 0000000000..742c94960f --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/actions/test_commits.py @@ -0,0 +1,137 @@ +from collections import Counter + +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) + +from graphql_api.actions.commits import repo_commits +from reports.models import CommitReport +from reports.tests.factories import CommitReportFactory, UploadFactory + + +class RepoCommitsTests(TestCase): + def setUp(self): + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org, private=False) + self.repo_with_deleted_commits = RepositoryFactory( + author=self.org, private=False + ) + self.commits = [ + CommitFactory(repository=self.repo, message="Foo Bar", state="complete"), + CommitFactory( + repository=self.repo, branch="test", message="barn", state="error" + ), + CommitFactory( + repository=self.repo, branch="test", message="baz", state="pending" + ), + ] + self.deleted_commits = [ + CommitFactory(repository=self.repo_with_deleted_commits, deleted=True), + CommitFactory( + repository=self.repo_with_deleted_commits, branch="test", deleted=True + ), + CommitFactory( + repository=self.repo_with_deleted_commits, branch="test", deleted=True + ), + ] + self.repo_2 = RepositoryFactory(author=self.org, private=False) + self.commits_2 = [ + CommitFactory(repository=self.repo_2, pullid=179), + CommitFactory(repository=self.repo_2, branch="test2", pullid=179), + CommitFactory(repository=self.repo_2, ci_passed=False, branch="test2"), + ] + + def test_all(self): + commits = repo_commits(self.repo, None) + assert len(list(commits)) == 3 + assert Counter(list(commits)) == Counter(self.commits) + + def test_hide_failed_ci(self): + commits = repo_commits(self.repo_2, {"hide_failed_ci": True}) + commits_with_filter = list( + filter(lambda commit: commit.ci_passed is True, self.commits_2) + ) + assert Counter(list(commits)) == Counter(commits_with_filter) + + def test_pullid(self): + commits = repo_commits(self.repo_2, {"pull_id": 179}) + commits_with_filter = list( + filter(lambda commit: commit.pullid == 179, self.commits_2) + ) + assert Counter(list(commits)) == Counter(commits_with_filter) + + def test_branch_name(self): + commits = repo_commits(self.repo, {"branch_name": "test"}) + commits_with_filter = list( + filter(lambda commit: commit.branch == "test", self.commits) + ) + assert Counter(list(commits)) == Counter(commits_with_filter) + + def test_deleted_commits(self): + commits = repo_commits(self.repo_with_deleted_commits, None) + assert list(commits) == [] + + def test_branch_name_hide_failed_ci(self): + commits = repo_commits( + self.repo_2, {"hide_failed_ci": True, "branch_name": "test2"} + ) + commits_with_filter = list( + filter( + lambda commit: commit.branch == "test2" and commit.ci_passed is True, + self.commits_2, + ) + ) + assert list(commits) == commits_with_filter + + def test_long_sha(self): + commits = repo_commits(self.repo, {"search": self.commits[0].commitid}) + assert list(commits) == [self.commits[0]] + + def test_short_sha(self): + commits = repo_commits(self.repo, {"search": self.commits[0].commitid[0:7]}) + assert list(commits) == [self.commits[0]] + + def test_message(self): + commits = repo_commits(self.repo, {"search": "bar"}) + assert list(commits) == [self.commits[0], self.commits[1]] + + def test_states(self): + commits = repo_commits(self.repo, {"states": ["complete", "pending"]}) + assert list(commits) == [self.commits[0], self.commits[2]] + + def test_coverage_status(self): + report_0 = CommitReportFactory( + commit=self.commits[0], report_type=CommitReport.ReportType.COVERAGE + ) + UploadFactory(report=report_0, state="processed") + report_1 = CommitReportFactory( + commit=self.commits[1], report_type=CommitReport.ReportType.COVERAGE + ) + UploadFactory(report=report_1, state="uploaded") + report_2 = CommitReportFactory( + commit=self.commits[2], report_type=CommitReport.ReportType.COVERAGE + ) + UploadFactory(report=report_2, state="error") + + commits = repo_commits(self.repo, {"coverage_status": ["COMPLETED"]}) + assert list(commits) == [self.commits[0]] + + commits = repo_commits(self.repo, {"coverage_status": ["PENDING"]}) + assert list(commits) == [self.commits[1]] + + commits = repo_commits(self.repo, {"coverage_status": ["ERROR"]}) + assert list(commits) == [self.commits[2]] + + commits = repo_commits(self.repo, {"coverage_status": ["COMPLETED", "PENDING"]}) + assert list(commits) == [self.commits[0], self.commits[1]] + + commits = repo_commits( + self.repo, {"coverage_status": ["COMPLETED", "PENDING", "ERROR"]} + ) + assert list(commits) == [self.commits[0], self.commits[1], self.commits[2]] + + commits = repo_commits(self.repo, {"coverage_status": []}) + assert list(commits) == [self.commits[0], self.commits[1], self.commits[2]] diff --git a/apps/codecov-api/graphql_api/tests/helper.py b/apps/codecov-api/graphql_api/tests/helper.py new file mode 100644 index 0000000000..0a7f9da271 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/helper.py @@ -0,0 +1,48 @@ +from http.cookies import SimpleCookie + +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory, UserFactory + +from codecov_auth.views.okta_cloud import OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY +from utils.test_utils import Client + + +class GraphQLTestHelper: + def gql_request( + self, + query, + provider="gh", + owner=None, + variables=None, + with_errors=False, + okta_signed_in_accounts=[], + impersonate_owner=False, + ): + url = f"/graphql/{provider}" + + if owner: + self.client = Client() + + if impersonate_owner: + staff_owner = OwnerFactory( + name="staff_user", service="github", user=UserFactory(is_staff=True) + ) + self.client.cookies = SimpleCookie({"staff_user": owner.pk}) + self.client.force_login_owner(staff_owner) + else: + self.client.force_login_owner(owner) + + if okta_signed_in_accounts: + session = self.client.session + session[OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY] = okta_signed_in_accounts + session.save() + + response = self.client.post( + url, + {"query": query, "variables": variables or {}}, + content_type="application/json", + ) + return response.json() if with_errors else response.json()["data"] + + +def paginate_connection(connection): + return [edge["node"] for edge in connection["edges"]] diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_activate_measurements.py b/apps/codecov-api/graphql_api/tests/mutation/test_activate_measurements.py new file mode 100644 index 0000000000..ee3a978f7e --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_activate_measurements.py @@ -0,0 +1,54 @@ +from unittest.mock import patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: ActivateMeasurementsInput!) { + activateMeasurements(input: $input) { + error { + __typename + } + } +} +""" + + +class ActivateMeasurementsTestCase(GraphQLTestHelper, TestCase): + def setUp(self): + self.owner = OwnerFactory() + + def test_when_unauthenticated(self): + data = self.gql_request( + query, + variables={ + "input": { + "owner": "codecov", + "repoName": "test-repo", + "measurementType": "FLAG_COVERAGE", + } + }, + ) + assert ( + data["activateMeasurements"]["error"]["__typename"] + == "UnauthenticatedError" + ) + + @patch( + "core.commands.repository.interactors.activate_measurements.ActivateMeasurementsInteractor.execute" + ) + def test_when_authenticated(self, execute): + data = self.gql_request( + query, + owner=self.owner, + variables={ + "input": { + "owner": "codecov", + "repoName": "test-repo", + "measurementType": "FLAG_COVERAGE", + } + }, + ) + assert data == {"activateMeasurements": None} diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_cancel_trial.py b/apps/codecov-api/graphql_api/tests/mutation/test_cancel_trial.py new file mode 100644 index 0000000000..65b09c565d --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_cancel_trial.py @@ -0,0 +1,86 @@ +from django.test import TestCase +from prometheus_client import REGISTRY +from shared.django_apps.codecov_auth.tests.factories import PlanFactory, TierFactory +from shared.django_apps.core.tests.factories import OwnerFactory +from shared.plan.constants import PlanName, TierName, TrialStatus + +from graphql_api.tests.helper import GraphQLTestHelper +from graphql_api.views import GQL_ERROR_COUNTER, GQL_HIT_COUNTER, GQL_REQUEST_LATENCIES + +query = """ + mutation($input: CancelTrialInput!) { + cancelTrial(input: $input) { + error { + __typename + ... on ResolverError { + message + } + } + } + } +""" + + +class CancelTrialMutationTest(GraphQLTestHelper, TestCase): + def _request(self, owner=None, org_username: str = None): + return self.gql_request( + query, + variables={"input": {"orgUsername": org_username}}, + owner=owner, + ) + + def test_unauthenticated(self): + assert self._request() == { + "cancelTrial": { + "error": { + "__typename": "UnauthenticatedError", + "message": "You are not authenticated", + } + } + } + + def test_authenticated(self): + GQL_HIT_COUNTER.labels( + operation_type="mutation", operation_name="CancelTrialInput" + ) + GQL_ERROR_COUNTER.labels( + operation_type="mutation", operation_name="CancelTrialInput" + ) + GQL_REQUEST_LATENCIES.labels( + operation_type="mutation", operation_name="CancelTrialInput" + ) + before = REGISTRY.get_sample_value( + "api_gql_counts_hits_total", + labels={"operation_type": "mutation", "operation_name": "CancelTrialInput"}, + ) + errors_before = REGISTRY.get_sample_value( + "api_gql_counts_errors_total", + labels={"operation_type": "mutation", "operation_name": "CancelTrialInput"}, + ) + timer_before = REGISTRY.get_sample_value( + "api_gql_timers_full_runtime_seconds_count", + labels={"operation_type": "mutation", "operation_name": "CancelTrialInput"}, + ) + trial_status = TrialStatus.ONGOING.value + tier = TierFactory(tier_name=TierName.TRIAL.value) + plan = PlanFactory(name=PlanName.TRIAL_PLAN_NAME.value, tier=tier) + owner = OwnerFactory(trial_status=trial_status, plan=plan.name) + owner.save() + assert self._request(owner=owner, org_username=owner.username) == { + "cancelTrial": None + } + after = REGISTRY.get_sample_value( + "api_gql_counts_hits_total", + labels={"operation_type": "mutation", "operation_name": "CancelTrialInput"}, + ) + errors_after = REGISTRY.get_sample_value( + "api_gql_counts_errors_total", + labels={"operation_type": "mutation", "operation_name": "CancelTrialInput"}, + ) + timer_after = REGISTRY.get_sample_value( + "api_gql_timers_full_runtime_seconds_count", + labels={"operation_type": "mutation", "operation_name": "CancelTrialInput"}, + ) + assert after - before == 1 + assert errors_after - errors_before == 0 + assert timer_after - timer_before == 1 diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_create_api_token.py b/apps/codecov-api/graphql_api/tests/mutation/test_create_api_token.py new file mode 100644 index 0000000000..1b0cfa264f --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_create_api_token.py @@ -0,0 +1,56 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov_auth.models import Session +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: CreateApiTokenInput!) { + createApiToken(input: $input) { + error { + __typename + } + fullToken + session { + ip + lastseen + lastFour + type + name + useragent + } + } +} +""" + + +class CreateApiTokenTestCase(GraphQLTestHelper, TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + + def test_when_unauthenticated(self): + data = self.gql_request(query, variables={"input": {"name": "yo"}}) + assert data["createApiToken"]["error"]["__typename"] == "UnauthenticatedError" + + def test_when_authenticated(self): + name = "yo" + data = self.gql_request( + query, owner=self.owner, variables={"input": {"name": name}} + ) + created_token = self.owner.session_set.filter(name=name).first() + assert data["createApiToken"]["session"] == { + "name": name, + "ip": None, + "lastseen": None, + "useragent": None, + "type": Session.SessionType.API.value, + "lastFour": str(created_token.token)[-4:], + } + + def test_when_authenticated_full_token(self): + name = "yo" + data = self.gql_request( + query, owner=self.owner, variables={"input": {"name": name}} + ) + created_token = self.owner.session_set.filter(name=name).first() + assert data["createApiToken"]["fullToken"] == str(created_token.token) diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_create_stripe_setup_intent.py b/apps/codecov-api/graphql_api/tests/mutation/test_create_stripe_setup_intent.py new file mode 100644 index 0000000000..9143e51374 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_create_stripe_setup_intent.py @@ -0,0 +1,67 @@ +from unittest.mock import patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: CreateStripeSetupIntentInput!) { + createStripeSetupIntent(input: $input) { + error { + __typename + } + clientSecret + } +} +""" + + +class CreateStripeSetupIntentTestCase(GraphQLTestHelper, TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + + def test_when_unauthenticated(self): + data = self.gql_request(query, variables={"input": {"owner": "somename"}}) + assert ( + data["createStripeSetupIntent"]["error"]["__typename"] + == "UnauthenticatedError" + ) + + def test_when_unauthorized(self): + other_owner = OwnerFactory(username="other-user") + data = self.gql_request( + query, + owner=self.owner, + variables={"input": {"owner": other_owner.username}}, + ) + assert ( + data["createStripeSetupIntent"]["error"]["__typename"] + == "UnauthorizedError" + ) + + @patch("services.billing.stripe.SetupIntent.create") + def test_when_validation_error(self, setup_intent_create_mock): + setup_intent_create_mock.side_effect = Exception("Some error") + data = self.gql_request( + query, owner=self.owner, variables={"input": {"owner": self.owner.username}} + ) + assert ( + data["createStripeSetupIntent"]["error"]["__typename"] == "ValidationError" + ) + + def test_when_owner_not_found(self): + data = self.gql_request( + query, owner=self.owner, variables={"input": {"owner": "nonexistent-user"}} + ) + assert ( + data["createStripeSetupIntent"]["error"]["__typename"] == "ValidationError" + ) + + @patch("services.billing.stripe.SetupIntent.create") + def test_success(self, setup_intent_create_mock): + setup_intent_create_mock.return_value = {"client_secret": "test-client-secret"} + data = self.gql_request( + query, owner=self.owner, variables={"input": {"owner": self.owner.username}} + ) + assert data["createStripeSetupIntent"]["clientSecret"] == "test-client-secret" diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_create_user_token.py b/apps/codecov-api/graphql_api/tests/mutation/test_create_user_token.py new file mode 100644 index 0000000000..721ee15f25 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_create_user_token.py @@ -0,0 +1,50 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov_auth.models import UserToken +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: CreateUserTokenInput!) { + createUserToken(input: $input) { + error { + __typename + } + fullToken + token { + id + type + name + lastFour + } + } +} +""" + + +class CreateApiTokenTestCase(GraphQLTestHelper, TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + + def test_unauthenticated(self): + data = self.gql_request(query, variables={"input": {"name": "test"}}) + assert data["createUserToken"]["error"]["__typename"] == "UnauthenticatedError" + + def test_authenticated(self): + name = "test" + data = self.gql_request( + query, owner=self.owner, variables={"input": {"name": name}} + ) + user_token = self.owner.user_tokens.filter(name=name).first() + assert user_token + + assert data["createUserToken"] == { + "token": { + "id": str(user_token.external_id), + "name": name, + "type": UserToken.TokenType.API.value, + "lastFour": str(user_token.token)[-4:], + }, + "fullToken": str(user_token.token), + "error": None, + } diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_delete_component_measurements.py b/apps/codecov-api/graphql_api/tests/mutation/test_delete_component_measurements.py new file mode 100644 index 0000000000..a01417e4f8 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_delete_component_measurements.py @@ -0,0 +1,150 @@ +from unittest.mock import patch + +from django.test import TestCase + +from codecov.commands.exceptions import ( + NotFound, + Unauthenticated, + Unauthorized, + ValidationError, +) +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ + mutation($input: DeleteComponentMeasurementsInput!) { + deleteComponentMeasurements(input: $input) { + error { + __typename + ... on ResolverError { + message + } + } + } + } +""" + + +class DeleteComponentMeasurementsTest(GraphQLTestHelper, TestCase): + @patch( + "core.commands.component.interactors.delete_component_measurements.DeleteComponentMeasurementsInteractor.execute" + ) + def test_delete_component_measurements(self, execute_mock): + data = self.gql_request( + query, + variables={ + "input": { + "ownerUsername": "test-owner", + "repoName": "test-repo", + "componentId": "test-component", + } + }, + ) + + assert data == {"deleteComponentMeasurements": None} + + execute_mock.assert_called_once_with( + owner_username="test-owner", + repo_name="test-repo", + component_id="test-component", + ) + + @patch( + "core.commands.component.interactors.delete_component_measurements.DeleteComponentMeasurementsInteractor.execute" + ) + def test_delete_component_measurements_unauthenticated(self, execute_mock): + execute_mock.side_effect = Unauthenticated() + + data = self.gql_request( + query, + variables={ + "input": { + "ownerUsername": "test-owner", + "repoName": "test-repo", + "componentId": "test-component", + } + }, + ) + + assert data == { + "deleteComponentMeasurements": { + "error": { + "__typename": "UnauthenticatedError", + "message": "You are not authenticated", + } + } + } + + @patch( + "core.commands.component.interactors.delete_component_measurements.DeleteComponentMeasurementsInteractor.execute" + ) + def test_delete_component_measurements_unauthorized(self, execute_mock): + execute_mock.side_effect = Unauthorized() + + data = self.gql_request( + query, + variables={ + "input": { + "ownerUsername": "test-owner", + "repoName": "test-repo", + "componentId": "test-component", + } + }, + ) + + assert data == { + "deleteComponentMeasurements": { + "error": { + "__typename": "UnauthorizedError", + "message": "You are not authorized", + } + } + } + + @patch( + "core.commands.component.interactors.delete_component_measurements.DeleteComponentMeasurementsInteractor.execute" + ) + def test_delete_component_measurements_validation_error(self, execute_mock): + execute_mock.side_effect = ValidationError("test error") + + data = self.gql_request( + query, + variables={ + "input": { + "ownerUsername": "test-owner", + "repoName": "test-repo", + "componentId": "test-component", + } + }, + ) + + assert data == { + "deleteComponentMeasurements": { + "error": {"__typename": "ValidationError", "message": "test error"} + } + } + + @patch( + "core.commands.component.interactors.delete_component_measurements.DeleteComponentMeasurementsInteractor.execute" + ) + def test_delete_component_measurements_not_found(self, execute_mock): + execute_mock.side_effect = NotFound() + + data = self.gql_request( + query, + variables={ + "input": { + "ownerUsername": "test-owner", + "repoName": "test-repo", + "componentId": "test-component", + } + }, + ) + + assert data == { + "deleteComponentMeasurements": { + "error": { + "__typename": "NotFoundError", + "message": "Cant find the requested resource", + } + } + } diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_delete_flag.py b/apps/codecov-api/graphql_api/tests/mutation/test_delete_flag.py new file mode 100644 index 0000000000..a5649c8347 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_delete_flag.py @@ -0,0 +1,140 @@ +from unittest.mock import patch + +from django.test import TestCase + +from codecov.commands.exceptions import ( + NotFound, + Unauthenticated, + Unauthorized, + ValidationError, +) +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ + mutation($input: DeleteFlagInput!) { + deleteFlag(input: $input) { + error { + __typename + ... on ResolverError { + message + } + } + } + } +""" + + +class DeleteFlagTest(GraphQLTestHelper, TestCase): + @patch("core.commands.flag.interactors.delete_flag.DeleteFlagInteractor.execute") + def test_delete_flag(self, execute_mock): + data = self.gql_request( + query, + variables={ + "input": { + "ownerUsername": "test-owner", + "repoName": "test-repo", + "flagName": "test-flag", + } + }, + ) + + assert data == {"deleteFlag": None} + + execute_mock.assert_called_once_with( + owner_username="test-owner", + repo_name="test-repo", + flag_name="test-flag", + ) + + @patch("core.commands.flag.interactors.delete_flag.DeleteFlagInteractor.execute") + def test_delete_flag_unauthenticated(self, execute_mock): + execute_mock.side_effect = Unauthenticated() + + data = self.gql_request( + query, + variables={ + "input": { + "ownerUsername": "test-owner", + "repoName": "test-repo", + "flagName": "test-flag", + } + }, + ) + + assert data == { + "deleteFlag": { + "error": { + "__typename": "UnauthenticatedError", + "message": "You are not authenticated", + } + } + } + + @patch("core.commands.flag.interactors.delete_flag.DeleteFlagInteractor.execute") + def test_delete_flag_unauthorized(self, execute_mock): + execute_mock.side_effect = Unauthorized() + + data = self.gql_request( + query, + variables={ + "input": { + "ownerUsername": "test-owner", + "repoName": "test-repo", + "flagName": "test-flag", + } + }, + ) + + assert data == { + "deleteFlag": { + "error": { + "__typename": "UnauthorizedError", + "message": "You are not authorized", + } + } + } + + @patch("core.commands.flag.interactors.delete_flag.DeleteFlagInteractor.execute") + def test_delete_flag_validation_error(self, execute_mock): + execute_mock.side_effect = ValidationError("test error") + + data = self.gql_request( + query, + variables={ + "input": { + "ownerUsername": "test-owner", + "repoName": "test-repo", + "flagName": "test-flag", + } + }, + ) + + assert data == { + "deleteFlag": { + "error": {"__typename": "ValidationError", "message": "test error"} + } + } + + @patch("core.commands.flag.interactors.delete_flag.DeleteFlagInteractor.execute") + def test_delete_flag_not_found(self, execute_mock): + execute_mock.side_effect = NotFound() + + data = self.gql_request( + query, + variables={ + "input": { + "ownerUsername": "test-owner", + "repoName": "test-repo", + "flagName": "test-flag", + } + }, + ) + + assert data == { + "deleteFlag": { + "error": { + "__typename": "NotFoundError", + "message": "Cant find the requested resource", + } + } + } diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_delete_session.py b/apps/codecov-api/graphql_api/tests/mutation/test_delete_session.py new file mode 100644 index 0000000000..a6e7df746d --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_delete_session.py @@ -0,0 +1,82 @@ +from django.contrib import auth +from django.test import TransactionTestCase +from django.utils import timezone +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory, UserFactory + +from codecov_auth.models import DjangoSession, Session +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: DeleteSessionInput!) { + deleteSession(input: $input) { + error { + __typename + } + } +} +""" + + +class DeleteSessionTestCase(GraphQLTestHelper, TransactionTestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + user = self.user = UserFactory() + self.owner.user = user + self.owner.save() + + # clear pre-existing login sessions, as some testcase seems to leak these + DjangoSession.objects.all().delete() + + def test_when_unauthenticated(self): + data = self.gql_request(query, variables={"input": {"sessionid": 1}}) + assert data["deleteSession"]["error"]["__typename"] == "UnauthenticatedError" + + def test_when_authenticated(self): + login_query = "{ me { user { username }} }" + self.gql_request(login_query, owner=self.owner) + + user = auth.get_user(self.client) + assert user.is_authenticated + + django_session_id = DjangoSession.objects.all() + assert len(django_session_id) == 1 + + django_session_id = django_session_id[0] + + sessionid = Session.objects.create( + lastseen=timezone.now(), + useragent="Firefox", + ip="0.0.0.0", + login_session=django_session_id, + type=Session.SessionType.LOGIN, + owner=self.owner, + ).sessionid + + self.gql_request( + query, owner=self.owner, variables={"input": {"sessionid": sessionid}} + ) + assert len(Session.objects.filter(sessionid=sessionid)) == 0 + + def test_when_authenticated_session_not_valid(self): + login_query = "{ me { user { username }} }" + self.gql_request(login_query, owner=self.owner) + + user = auth.get_user(self.client) + assert user.is_authenticated + + django_session_id = DjangoSession.objects.all() + assert len(django_session_id) == 1 + + django_session_id = django_session_id[0] + + Session.objects.create( + lastseen=timezone.now(), + useragent="Firefox", + ip="0.0.0.0", + login_session=django_session_id, + type=Session.SessionType.LOGIN, + owner=self.owner, + ).sessionid + + self.gql_request(query, owner=self.owner, variables={"input": {"sessionid": 0}}) + assert len(Session.objects.all()) == 1 diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_encode_secret_string.py b/apps/codecov-api/graphql_api/tests/mutation/test_encode_secret_string.py new file mode 100644 index 0000000000..e7f50c4393 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_encode_secret_string.py @@ -0,0 +1,43 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory +from shared.encryption.yaml_secret import yaml_secret_encryptor + +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: EncodeSecretStringInput!) { + encodeSecretString(input: $input) { + value + error { + __typename + ... on ResolverError { + message + } + } + } +} +""" + + +class TestEncodeSecretString(TestCase, GraphQLTestHelper): + def _request(self): + data = self.gql_request( + query, + owner=self.org, + variables={"input": {"repoName": "test-repo", "value": "token-1"}}, + ) + return data["encodeSecretString"]["value"] + + def setUp(self): + self.org = OwnerFactory(username="test-org") + self.repo = RepositoryFactory( + name="test-repo", + author=self.org, + private=True, + ) + self.owner = OwnerFactory(permission=[self.repo.pk]) + + def test_encoded_secret_string(self): + res = self._request() + check_encryptor = yaml_secret_encryptor + assert "token-1" in check_encryptor.decode(res[7:]) diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_erase_repository.py b/apps/codecov-api/graphql_api/tests/mutation/test_erase_repository.py new file mode 100644 index 0000000000..5c69e7d47c --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_erase_repository.py @@ -0,0 +1,126 @@ +from unittest.mock import patch + +from django.test import TestCase, override_settings +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: EraseRepositoryInput!) { + eraseRepository(input: $input) { + error { + __typename + ... on ResolverError { + message + } + } + } +} +""" + + +class EraseRepositoryTests(GraphQLTestHelper, TestCase): + def setUp(self): + self.org = OwnerFactory(username="codecov", service="github") + self.non_admin_user = OwnerFactory(organizations=[self.org.ownerid]) + self.admin_user = OwnerFactory(organizations=[self.org.ownerid]) + self.org.add_admin(self.admin_user) + + self.repo = RepositoryFactory(author=self.org, name="gazebo", active=True) + + def test_when_authenticated(self): + data = self.gql_request( + query, + owner=self.org, + variables={ + "input": { + "repoName": "gazebo", + } + }, + ) + + assert data == {"eraseRepository": None} + + def test_when_validation_error_repo_not_found(self): + data = self.gql_request( + query, + owner=self.org, + variables={ + "input": { + "repoName": "DNE", + } + }, + ) + assert data["eraseRepository"]["error"]["__typename"] == "ValidationError" + + def test_when_unauthenticated(self): + data = self.gql_request( + query, + variables={ + "input": { + "repoName": "DNE", + } + }, + ) + assert data["eraseRepository"]["error"]["__typename"] == "UnauthenticatedError" + + @override_settings(IS_ENTERPRISE=True) + @patch("services.self_hosted.is_admin_owner") + def test_when_not_self_hosted_admin(self, is_admin_owner): + is_admin_owner.return_value = False + data = self.gql_request( + query, + owner=self.org, + variables={ + "input": { + "repoName": "gazebo", + } + }, + ) + + assert data["eraseRepository"]["error"]["__typename"] == "UnauthorizedError" + + @override_settings(IS_ENTERPRISE=True) + @patch("services.self_hosted.is_admin_owner") + def test_when_self_hosted_admin(self, is_admin_owner): + is_admin_owner.return_value = True + + data = self.gql_request( + query, + owner=self.org, + variables={ + "input": { + "repoName": "gazebo", + } + }, + ) + + assert data == {"eraseRepository": None} + + def test_when_other_admin(self): + data = self.gql_request( + query, + owner=self.admin_user, + variables={ + "input": { + "owner": "codecov", + "repoName": "gazebo", + } + }, + ) + + assert data == {"eraseRepository": None} + + def test_when_not_other_admin(self): + data = self.gql_request( + query, + owner=self.non_admin_user, + variables={ + "input": { + "owner": "codecov", + "repoName": "gazebo", + } + }, + ) + + assert data["eraseRepository"]["error"]["__typename"] == "UnauthorizedError" diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_regenerate_repository_token.py b/apps/codecov-api/graphql_api/tests/mutation/test_regenerate_repository_token.py new file mode 100644 index 0000000000..8bfd8f7675 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_regenerate_repository_token.py @@ -0,0 +1,104 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + OwnerFactory, + RepositoryFactory, + RepositoryTokenFactory, +) + +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: RegenerateRepositoryTokenInput!) { + regenerateRepositoryToken(input: $input) { + token + error { + __typename + ... on ResolverError { + message + } + } + } +} +""" + + +class RegeneratRepositoryTokenTests(GraphQLTestHelper, TestCase): + def setUp(self): + self.org = OwnerFactory(username="codecov") + self.repo = RepositoryFactory(author=self.org, name="gazebo", active=True) + + def test_when_unauthenticated(self): + data = self.gql_request( + query, + variables={ + "input": { + "repoName": "gazebo", + "owner": "codecov", + "tokenType": "PROFILING", + } + }, + ) + assert ( + data["regenerateRepositoryToken"]["error"]["__typename"] + == "UnauthenticatedError" + ) + + def test_when_validation_error_repo_not_viewable(self): + random_user = OwnerFactory(organizations=[self.org.ownerid]) + data = self.gql_request( + query, + owner=random_user, + variables={ + "input": { + "repoName": "gazebo", + "owner": "codecov", + "tokenType": "PROFILING", + } + }, + ) + assert ( + data["regenerateRepositoryToken"]["error"]["__typename"] + == "ValidationError" + ) + + def test_when_authenticated_regenerate_staticanalysis_token(self): + user = OwnerFactory( + organizations=[self.org.ownerid], permission=[self.repo.repoid] + ) + RepositoryTokenFactory( + repository=self.repo, key="random", token_type="static_analysis" + ) + data = self.gql_request( + query, + owner=user, + variables={ + "input": { + "owner": "codecov", + "repoName": "gazebo", + "tokenType": "STATIC_ANALYSIS", + } + }, + ) + newToken = data["regenerateRepositoryToken"]["token"] + assert newToken != "random" + assert len(newToken) == 40 + + def test_when_authenticated_regenerate_upload_token(self): + user = OwnerFactory( + organizations=[self.org.ownerid], permission=[self.repo.repoid] + ) + RepositoryTokenFactory(repository=self.repo, key="random", token_type="upload") + data = self.gql_request( + query, + owner=user, + variables={ + "input": { + "owner": "codecov", + "repoName": "gazebo", + "tokenType": "UPLOAD", + } + }, + ) + newToken = data["regenerateRepositoryToken"]["token"] + assert newToken != "random" + assert len(newToken) == 40 diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_regenerate_repository_upload_token.py b/apps/codecov-api/graphql_api/tests/mutation/test_regenerate_repository_upload_token.py new file mode 100644 index 0000000000..b1e74b61b1 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_regenerate_repository_upload_token.py @@ -0,0 +1,54 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: RegenerateRepositoryUploadTokenInput!) { + regenerateRepositoryUploadToken(input: $input) { + token + error { + __typename + ... on ResolverError { + message + } + } + } +} +""" + + +class RegenerateRepositoryUploadTokenTests(GraphQLTestHelper, TestCase): + def setUp(self): + self.org = OwnerFactory(username="codecov") + self.repo = RepositoryFactory(author=self.org, name="gazebo") + self.old_repo_token = self.repo.upload_token + + def test_when_authenticated_updates_token(self): + user = OwnerFactory( + organizations=[self.org.ownerid], permission=[self.repo.repoid] + ) + + data = self.gql_request( + query, + owner=user, + variables={"input": {"repoName": "gazebo", "owner": "codecov"}}, + ) + + assert data["regenerateRepositoryUploadToken"]["token"] != self.old_repo_token + + def test_when_validation_error_repo_not_found(self): + data = self.gql_request( + query, + owner=self.org, + variables={ + "input": { + "repoName": "DNE", + "owner": "codecov", + } + }, + ) + assert ( + data["regenerateRepositoryUploadToken"]["error"]["__typename"] + == "ValidationError" + ) diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_regenrate_org_upload_token.py b/apps/codecov-api/graphql_api/tests/mutation/test_regenrate_org_upload_token.py new file mode 100644 index 0000000000..0819539699 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_regenrate_org_upload_token.py @@ -0,0 +1,49 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: RegenerateOrgUploadTokenInput!) { + regenerateOrgUploadToken(input: $input) { + orgUploadToken + error { + __typename + } + } +} +""" + + +class RegenerateOrgUploadToken(GraphQLTestHelper, TestCase): + def setUp(self): + self.owner = OwnerFactory( + username="codecov", plan="users-enterprisem", service="github" + ) + + def test_when_unauthenticated_error(self): + data = self.gql_request(query, variables={"input": {"owner": "codecov"}}) + assert ( + data["regenerateOrgUploadToken"]["error"]["__typename"] + == "UnauthenticatedError" + ) + + def test_when_validation_error(self): + owner = OwnerFactory(name="rula") + data = self.gql_request( + query, + owner=owner, + variables={"input": {"owner": "random"}}, + ) + assert ( + data["regenerateOrgUploadToken"]["error"]["__typename"] == "ValidationError" + ) + + def test_when_authenticated_regenerate_token(self): + data = self.gql_request( + query, + owner=self.owner, + variables={"input": {"owner": "codecov"}}, + ) + newToken = data["regenerateOrgUploadToken"]["orgUploadToken"] + assert newToken diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_revoke_user_token.py b/apps/codecov-api/graphql_api/tests/mutation/test_revoke_user_token.py new file mode 100644 index 0000000000..0c3d020e60 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_revoke_user_token.py @@ -0,0 +1,36 @@ +from django.test import TestCase +from shared.django_apps.codecov_auth.tests.factories import ( + OwnerFactory, + UserTokenFactory, +) + +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: RevokeUserTokenInput!) { + revokeUserToken(input: $input) { + error { + __typename + } + } +} +""" + + +class RevokeUserTokenTestCase(GraphQLTestHelper, TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + + def test_unauthenticated(self): + data = self.gql_request(query, variables={"input": {"tokenid": "testing"}}) + assert data["revokeUserToken"]["error"]["__typename"] == "UnauthenticatedError" + + def test_authenticated(self): + user_token = UserTokenFactory(owner=self.owner) + tokenid = str(user_token.external_id) + data = self.gql_request( + query, owner=self.owner, variables={"input": {"tokenid": tokenid}} + ) + assert data["revokeUserToken"] is None + deleted_user_token = self.owner.user_tokens.filter(external_id=tokenid).first() + assert deleted_user_token is None diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_save_okta_config.py b/apps/codecov-api/graphql_api/tests/mutation/test_save_okta_config.py new file mode 100644 index 0000000000..7d85232b69 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_save_okta_config.py @@ -0,0 +1,59 @@ +from django.test import TestCase +from shared.django_apps.codecov_auth.tests.factories import AccountFactory, OwnerFactory + +from codecov_auth.models import OktaSettings +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: SaveOktaConfigInput!) { + saveOktaConfig(input: $input) { + error { + __typename + } + } +} +""" + + +class SaveOktaConfigTestCase(GraphQLTestHelper, TestCase): + def setUp(self): + self.current_user = OwnerFactory(username="codecov-user") + self.owner = OwnerFactory( + username="codecov-owner", + admins=[self.current_user.ownerid], + account=AccountFactory(), + ) + + def test_when_unauthenticated(self): + data = self.gql_request( + query, + variables={ + "input": { + "clientId": "some-client-id", + "clientSecret": "some-client-secret", + "url": "https://okta.example.com", + "enabled": True, + "enforced": True, + "orgUsername": self.owner.username, + } + }, + ) + assert data["saveOktaConfig"]["error"]["__typename"] == "UnauthenticatedError" + + def test_when_authenticated(self): + data = self.gql_request( + query, + owner=self.owner, + variables={ + "input": { + "clientId": "some-client-id", + "clientSecret": "some-client-secret", + "url": "https://okta.example.com", + "enabled": True, + "enforced": True, + "orgUsername": self.owner.username, + } + }, + ) + assert OktaSettings.objects.filter(account=self.owner.account).exists() + assert data["saveOktaConfig"] is None diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_save_sentry_state.py b/apps/codecov-api/graphql_api/tests/mutation/test_save_sentry_state.py new file mode 100644 index 0000000000..d7713e7363 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_save_sentry_state.py @@ -0,0 +1,68 @@ +from unittest.mock import patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from graphql_api.tests.helper import GraphQLTestHelper +from services.sentry import SentryInvalidStateError, SentryUserAlreadyExistsError + +query = """ + mutation($input: SaveSentryStateInput!) { + saveSentryState(input: $input) { + error { + __typename + ... on ResolverError { + message + } + } + } + } +""" + + +@patch("services.sentry.save_sentry_state") +class SaveSentryStateMutationTest(GraphQLTestHelper, TestCase): + def _request(self, owner=None): + return self.gql_request( + query, + variables={"input": {"state": "test-state"}}, + owner=owner, + ) + + def test_unauthenticated(self, save_sentry_state): + assert self._request() == { + "saveSentryState": { + "error": { + "__typename": "UnauthenticatedError", + "message": "You are not authenticated", + } + } + } + + def test_invalid_state(self, save_sentry_state): + save_sentry_state.side_effect = SentryInvalidStateError() + assert self._request(owner=OwnerFactory()) == { + "saveSentryState": { + "error": { + "__typename": "ValidationError", + "message": "Invalid state", + } + } + } + + def test_sentry_user_already_exists(self, save_sentry_state): + save_sentry_state.side_effect = SentryUserAlreadyExistsError() + assert self._request(owner=OwnerFactory()) == { + "saveSentryState": { + "error": { + "__typename": "ValidationError", + "message": "Invalid Sentry user", + } + } + } + + def test_authenticated(self, save_sentry_state): + owner = OwnerFactory() + assert self._request(owner=owner) == {"saveSentryState": None} + + save_sentry_state.assert_called_once_with(owner, "test-state") diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_save_terms_agreement.py b/apps/codecov-api/graphql_api/tests/mutation/test_save_terms_agreement.py new file mode 100644 index 0000000000..a4d854066e --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_save_terms_agreement.py @@ -0,0 +1,85 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ + mutation($input: SaveTermsAgreementInput!) { + saveTermsAgreement(input: $input) { + error { + __typename + ... on ResolverError { + message + } + } + } + } +""" + + +class SaveTermsAgreementMutationTest(GraphQLTestHelper, TestCase): + def _request_deprecated(self, owner=None): + return self.gql_request( + query, + variables={"input": {"termsAgreement": True, "customerIntent": "Business"}}, + owner=owner, + ) + + def _request_invalid_customer_intent_deprecated(self, owner=None): + return self.gql_request( + query, + variables={"input": {"termsAgreement": True, "customerIntent": "invalid"}}, + owner=owner, + ) + + def _request(self, owner=None): + return self.gql_request( + query, + variables={ + "input": { + "termsAgreement": True, + "businessEmail": "something@email.com", + "name": "codecov-user", + } + }, + owner=owner, + ) + + def test_invalid_customer_intent_deprecated(self): + owner = OwnerFactory() + assert self._request_invalid_customer_intent_deprecated(owner=owner) == { + "saveTermsAgreement": { + "error": { + "__typename": "ValidationError", + "message": "Invalid customer intent provided", + } + } + } + + def test_unauthenticated_deprecated(self): + assert self._request_deprecated() == { + "saveTermsAgreement": { + "error": { + "__typename": "UnauthenticatedError", + "message": "You are not authenticated", + } + } + } + + def test_authenticated_deprecated(self): + owner = OwnerFactory() + assert self._request_deprecated(owner=owner) == {"saveTermsAgreement": None} + + def test_unauthenticated(self): + assert self._request() == { + "saveTermsAgreement": { + "error": { + "__typename": "UnauthenticatedError", + "message": "You are not authenticated", + } + } + } + + def test_authenticated(self): + owner = OwnerFactory() + assert self._request(owner=owner) == {"saveTermsAgreement": None} diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_set_upload_token_required.py b/apps/codecov-api/graphql_api/tests/mutation/test_set_upload_token_required.py new file mode 100644 index 0000000000..fa729c9507 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_set_upload_token_required.py @@ -0,0 +1,101 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: SetUploadTokenRequiredInput!) { + setUploadTokenRequired(input: $input) { + error { + __typename + ... on ResolverError { + message + } + } + } +} +""" + + +class SetUploadTokenRequiredTests(GraphQLTestHelper, TestCase): + def setUp(self): + self.org = OwnerFactory(username="codecov") + + def test_when_authenticated_updates_upload_token_required(self): + user = OwnerFactory( + organizations=[self.org.ownerid], + ) + self.org.admins = [user.ownerid] + self.org.save() + + data = self.gql_request( + query, + owner=user, + variables={ + "input": {"orgUsername": "codecov", "uploadTokenRequired": True} + }, + ) + + assert data["setUploadTokenRequired"] is None + + def test_when_validation_error_org_not_found(self): + data = self.gql_request( + query, + owner=self.org, + variables={ + "input": { + "orgUsername": "non_existent_org", + "uploadTokenRequired": True, + } + }, + ) + assert ( + data["setUploadTokenRequired"]["error"]["__typename"] == "ValidationError" + ) + + def test_when_unauthorized_non_admin(self): + non_admin_user = OwnerFactory( + organizations=[self.org.ownerid], + ) + + data = self.gql_request( + query, + owner=non_admin_user, + variables={ + "input": {"orgUsername": "codecov", "uploadTokenRequired": True} + }, + ) + + assert ( + data["setUploadTokenRequired"]["error"]["__typename"] == "UnauthorizedError" + ) + + def test_when_unauthenticated(self): + data = self.gql_request( + query, + variables={ + "input": {"orgUsername": "codecov", "uploadTokenRequired": True} + }, + ) + + assert ( + data["setUploadTokenRequired"]["error"]["__typename"] + == "UnauthenticatedError" + ) + + def test_when_not_part_of_org(self): + non_part_of_org_user = OwnerFactory( + organizations=[self.org.ownerid], + ) + + data = self.gql_request( + query, + owner=non_part_of_org_user, + variables={ + "input": {"orgUsername": "codecov", "uploadTokenRequired": True} + }, + ) + + assert ( + data["setUploadTokenRequired"]["error"]["__typename"] == "UnauthorizedError" + ) diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_set_yaml_on_owner.py b/apps/codecov-api/graphql_api/tests/mutation/test_set_yaml_on_owner.py new file mode 100644 index 0000000000..ed4f153b13 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_set_yaml_on_owner.py @@ -0,0 +1,40 @@ +import asyncio +from unittest.mock import patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: SetYamlOnOwnerInput!) { + setYamlOnOwner(input: $input) { + error { + __typename + } + owner { + username + } + } +} +""" + + +class SetYamlOnOwnerMutationTest(GraphQLTestHelper, TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + asyncio.set_event_loop(asyncio.new_event_loop()) + + @patch("codecov_auth.commands.owner.owner.OwnerCommands.set_yaml_on_owner") + def test_mutation_dispatch_to_command(self, command_mock): + # mock the command to return a Future which resolved to the owner + f = asyncio.Future() + f.set_result(self.owner) + command_mock.return_value = f + input = { + "username": self.owner.username, + "yaml": "codecov:\n require_ci_to_pass: true", + } + data = self.gql_request(query, owner=self.owner, variables={"input": input}) + command_mock.assert_called_once_with(input["username"], input["yaml"]) + assert data["setYamlOnOwner"]["owner"]["username"] == self.owner.username diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_start_trial.py b/apps/codecov-api/graphql_api/tests/mutation/test_start_trial.py new file mode 100644 index 0000000000..92e3ba5330 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_start_trial.py @@ -0,0 +1,43 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ + mutation($input: StartTrialInput!) { + startTrial(input: $input) { + error { + __typename + ... on ResolverError { + message + } + } + } + } +""" + + +class StartTrialMutationTest(GraphQLTestHelper, TestCase): + def _request(self, owner=None, org_username: str = None): + return self.gql_request( + query, + variables={"input": {"orgUsername": org_username}}, + owner=owner, + ) + + def test_unauthenticated(self): + assert self._request() == { + "startTrial": { + "error": { + "__typename": "UnauthenticatedError", + "message": "You are not authenticated", + } + } + } + + def test_authenticated(self): + owner = OwnerFactory() + owner.save() + assert self._request(owner=owner, org_username=owner.username) == { + "startTrial": None + } diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_store_codecov_metrics.py b/apps/codecov-api/graphql_api/tests/mutation/test_store_codecov_metrics.py new file mode 100644 index 0000000000..7976d8d70a --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_store_codecov_metrics.py @@ -0,0 +1,108 @@ +from django.test import TestCase +from shared.django_apps.codecov_metrics.models import UserOnboardingLifeCycleMetrics +from shared.django_apps.core.tests.factories import OwnerFactory + +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ + mutation($input: StoreEventMetricsInput!) { + storeEventMetric(input: $input) { + error { + __typename + ... on ResolverError { + message + } + } + } + } +""" + + +class StoreEventMetricMutationTest(GraphQLTestHelper, TestCase): + def _request(self, org_username: str, event: str, json_payload: str, owner=None): + return self.gql_request( + query, + variables={ + "input": { + "orgUsername": org_username, + "eventName": event, + "jsonPayload": json_payload, + } + }, + owner=owner, + ) + + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + + def test_unauthenticated(self): + response = self._request( + org_username="codecov-user", + event="VISITED_PAGE", + json_payload='{"key": "value"}', + ) + assert response == { + "storeEventMetric": { + "error": { + "__typename": "UnauthenticatedError", + "message": "You are not authenticated", + } + } + } + + def test_authenticated_inserts_into_db(self): + self._request( + org_username="codecov-user", + event="VISITED_PAGE", + json_payload='{"some-key": "some-value"}', + owner=self.owner, + ) + metric = UserOnboardingLifeCycleMetrics.objects.filter( + event="VISITED_PAGE" + ).first() + self.assertIsNotNone(metric) + self.assertEqual(metric.additional_data, {"some-key": "some-value"}) + + def test_invalid_org(self): + response = self._request( + org_username="invalid_org", + event="VISITED_PAGE", + json_payload='{"key": "value"}', + owner=self.owner, + ) + assert response == { + "storeEventMetric": { + "error": { + "__typename": "ValidationError", + "message": "Cannot find owner record in the database", + } + } + } + + def test_invalid_event(self): + self._request( + org_username="codecov-user", + event="INVALID_EVENT", + json_payload='{"key": "value"}', + owner=self.owner, + ) + metric = UserOnboardingLifeCycleMetrics.objects.filter( + event="INVALID_EVENT" + ).first() + self.assertIsNone(metric) + + def test_invalid_json_string(self): + response = self._request( + org_username="codecov-user", + event="VISITED_PAGE", + json_payload="invalid-json", + owner=self.owner, + ) + assert response == { + "storeEventMetric": { + "error": { + "__typename": "ValidationError", + "message": "Invalid JSON string", + } + } + } diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_update_bundle_cache_config.py b/apps/codecov-api/graphql_api/tests/mutation/test_update_bundle_cache_config.py new file mode 100644 index 0000000000..53256f40f0 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_update_bundle_cache_config.py @@ -0,0 +1,80 @@ +from unittest.mock import patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: ActivateMeasurementsInput!) { + activateMeasurements(input: $input) { + error { + __typename + } + } +} +""" + + +query = """ +mutation UpdateBundleCacheConfig( + $owner: String! + $repoName: String! + $bundles: [BundleCacheConfigInput!]! +) { + updateBundleCacheConfig(input: { + owner: $owner, + repoName: $repoName, + bundles: $bundles + }) { + results { + bundleName + isCached + cacheConfig + } + error { + __typename + ... on UnauthenticatedError { + message + } + ... on ValidationError { + message + } + } + } +} +""" + + +class UpdateBundleCacheConfigTestCase(GraphQLTestHelper, TestCase): + def setUp(self): + self.owner = OwnerFactory() + + def test_when_unauthenticated(self): + data = self.gql_request( + query, + variables={ + "owner": "codecov", + "repoName": "test-repo", + "bundles": [{"bundleName": "pr_bundle1", "toggleCaching": True}], + }, + ) + assert ( + data["updateBundleCacheConfig"]["error"]["__typename"] + == "UnauthenticatedError" + ) + + @patch( + "core.commands.repository.interactors.update_bundle_cache_config.UpdateBundleCacheConfigInteractor.execute" + ) + def test_when_authenticated(self, execute): + data = self.gql_request( + query, + owner=self.owner, + variables={ + "owner": "codecov", + "repoName": "test-repo", + "bundles": [{"bundleName": "pr_bundle1", "toggleCaching": True}], + }, + ) + assert data == {"updateBundleCacheConfig": {"results": [], "error": None}} diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_update_default_organization.py b/apps/codecov-api/graphql_api/tests/mutation/test_update_default_organization.py new file mode 100644 index 0000000000..caf79cbdaf --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_update_default_organization.py @@ -0,0 +1,39 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from codecov_auth.models import OwnerProfile +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: UpdateDefaultOrganizationInput!) { + updateDefaultOrganization(input: $input) { + error { + __typename + } + } +} +""" + + +class UpdateProfileTestCase(GraphQLTestHelper, TestCase): + def setUp(self): + self.default_organization_username = "sample-default-org-username" + self.default_organization = OwnerFactory( + username=self.default_organization_username, service="github" + ) + self.owner = OwnerFactory( + username="sample-owner", + service="github", + organizations=[self.default_organization.ownerid], + ) + + def test_when_authenticated(self): + self.gql_request( + query, + owner=self.owner, + variables={"input": {"username": self.default_organization_username}}, + ) + owner_profile: OwnerProfile = OwnerProfile.objects.filter( + owner_id=self.owner.ownerid + ).first() + assert owner_profile.default_org == self.default_organization diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_update_profile.py b/apps/codecov-api/graphql_api/tests/mutation/test_update_profile.py new file mode 100644 index 0000000000..615e196e45 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_update_profile.py @@ -0,0 +1,36 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: UpdateProfileInput!) { + updateProfile(input: $input) { + error { + __typename + } + me { + email + user { + name + } + } + } +} +""" + + +class UpdateProfileTestCase(GraphQLTestHelper, TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + + def test_when_unauthenticated(self): + data = self.gql_request(query, variables={"input": {"name": "yo"}}) + assert data["updateProfile"]["error"]["__typename"] == "UnauthenticatedError" + + def test_when_authenticated(self): + name = "yo" + data = self.gql_request( + query, owner=self.owner, variables={"input": {"name": name}} + ) + assert data["updateProfile"]["me"]["user"]["name"] == name diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_update_repository.py b/apps/codecov-api/graphql_api/tests/mutation/test_update_repository.py new file mode 100644 index 0000000000..067879b486 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_update_repository.py @@ -0,0 +1,107 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + BranchFactory, + OwnerFactory, + RepositoryFactory, +) + +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ +mutation($input: UpdateRepositoryInput!) { + updateRepository(input: $input) { + error { + __typename + ... on ResolverError { + message + } + } + } +} +""" + +repo_query = """{ + me { + owner { + repository(name: "gazebo") { + ... on Repository { + activated + defaultBranch + } + } + } + } +} +""" + + +class UpdateRepositoryTests(GraphQLTestHelper, TestCase): + def setUp(self): + self.org = OwnerFactory(username="codecov", service="github") + self.repo = RepositoryFactory(author=self.org, name="gazebo", activated=False) + + def test_when_authenticated_update_activated(self): + data = self.gql_request( + query, + owner=self.org, + variables={"input": {"activated": True, "repoName": "gazebo"}}, + ) + + repo_result = self.gql_request( + repo_query, + owner=self.org, + ) + assert repo_result["me"]["owner"]["repository"]["activated"] == True + + assert data == {"updateRepository": None} + + def test_when_authenticated_update_branch(self): + BranchFactory.create(name="some other branch", repository=self.repo) + data = self.gql_request( + query, + owner=self.org, + variables={"input": {"branch": "some other branch", "repoName": "gazebo"}}, + ) + + repo_result = self.gql_request( + repo_query, + owner=self.org, + ) + assert ( + repo_result["me"]["owner"]["repository"]["defaultBranch"] + == "some other branch" + ) + + assert data == {"updateRepository": None} + + def test_when_authenticated_branch_does_not_exist(self): + data = self.gql_request( + query, + owner=self.org, + variables={"input": {"branch": "Dne", "repoName": "gazebo"}}, + ) + + assert data["updateRepository"]["error"]["__typename"] == "ValidationError" + + def test_when_unauthenticated(self): + data = self.gql_request( + query, + variables={ + "input": { + "repoName": "gazebo", + } + }, + ) + assert data["updateRepository"]["error"]["__typename"] == "UnauthenticatedError" + + def test_when_validation_error_repo_not_found(self): + data = self.gql_request( + query, + owner=self.org, + variables={ + "input": { + "repoName": "DNE", + } + }, + ) + assert data["updateRepository"]["error"]["__typename"] == "ValidationError" diff --git a/apps/codecov-api/graphql_api/tests/mutation/test_update_self_hosted_settings.py b/apps/codecov-api/graphql_api/tests/mutation/test_update_self_hosted_settings.py new file mode 100644 index 0000000000..e2b6b4bfad --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/mutation/test_update_self_hosted_settings.py @@ -0,0 +1,68 @@ +from django.test import TestCase, override_settings +from shared.django_apps.core.tests.factories import OwnerFactory + +from graphql_api.tests.helper import GraphQLTestHelper + +query = """ + mutation($input: UpdateSelfHostedSettingsInput!) { + updateSelfHostedSettings(input: $input) { + error { + __typename + ... on ResolverError { + message + } + } + } + } +""" + + +class UpdateSelfHostedSettingsTest(GraphQLTestHelper, TestCase): + def _request(self, owner=None): + return self.gql_request( + query, + variables={"input": {"shouldAutoActivate": True}}, + owner=owner, + ) + + def _request_deactivate(self, owner=None): + return self.gql_request( + query, + variables={"input": {"shouldAutoActivate": False}}, + owner=owner, + ) + + @override_settings(IS_ENTERPRISE=True) + def test_unauthenticated(self): + assert self._request() == { + "updateSelfHostedSettings": { + "error": { + "__typename": "UnauthenticatedError", + "message": "You are not authenticated", + } + } + } + + @override_settings(IS_ENTERPRISE=True) + def test_authenticated_enable_autoactivation(self): + owner = OwnerFactory() + assert self._request(owner=owner) == {"updateSelfHostedSettings": None} + + @override_settings(IS_ENTERPRISE=True) + def test_authenticate_disable_autoactivation(self): + owner = OwnerFactory() + assert self._request_deactivate(owner=owner) == { + "updateSelfHostedSettings": None + } + + @override_settings(IS_ENTERPRISE=False) + def test_invalid_settings(self): + owner = OwnerFactory() + assert self._request(owner=owner) == { + "updateSelfHostedSettings": { + "error": { + "__typename": "ValidationError", + "message": "enable_autoactivation and disable_autoactivation are only available in self-hosted environments", + } + } + } diff --git a/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_flag_filter__0.json b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_flag_filter__0.json new file mode 100644 index 0000000000..7125723b64 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_flag_filter__0.json @@ -0,0 +1,21 @@ +[ + { + "name": "test1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_term_filter__0.json b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_term_filter__0.json new file mode 100644 index 0000000000..7125723b64 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_term_filter__0.json @@ -0,0 +1,21 @@ +[ + { + "name": "test1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_testsuite_filter__0.json b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_testsuite_filter__0.json new file mode 100644 index 0000000000..7125723b64 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_testsuite_filter__0.json @@ -0,0 +1,21 @@ +[ + { + "name": "test1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results__0.json b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results__0.json new file mode 100644 index 0000000000..b9504a0cb6 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results__0.json @@ -0,0 +1,97 @@ +[ + { + "name": "test5", + "testsuite": [ + "testsuite5" + ], + "flags": [ + "flag5" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-05T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + }, + { + "name": "test4", + "testsuite": [ + "testsuite4" + ], + "flags": [ + "flag4" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-04T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + }, + { + "name": "test3", + "testsuite": [ + "testsuite3" + ], + "flags": [ + "flag3" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-03T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + }, + { + "name": "test2", + "testsuite": [ + "testsuite2" + ], + "flags": [ + "flag2" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-02T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + }, + { + "name": "test1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_asc__0.json b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_asc__0.json new file mode 100644 index 0000000000..71413d9177 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_asc__0.json @@ -0,0 +1,97 @@ +[ + { + "name": "test1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + }, + { + "name": "test2", + "testsuite": [ + "testsuite2" + ], + "flags": [ + "flag2" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-02T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + }, + { + "name": "test3", + "testsuite": [ + "testsuite3" + ], + "flags": [ + "flag3" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-03T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + }, + { + "name": "test4", + "testsuite": [ + "testsuite4" + ], + "flags": [ + "flag4" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-04T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + }, + { + "name": "test5", + "testsuite": [ + "testsuite5" + ], + "flags": [ + "flag5" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-05T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1__0.json b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1__0.json new file mode 100644 index 0000000000..7125723b64 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1__0.json @@ -0,0 +1,21 @@ +[ + { + "name": "test1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1_after__0.json b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1_after__0.json new file mode 100644 index 0000000000..7f325cfb41 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1_after__0.json @@ -0,0 +1,21 @@ +[ + { + "name": "test2", + "testsuite": [ + "testsuite2" + ], + "flags": [ + "flag2" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-02T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1_after_no_next__0.json b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1_after_no_next__0.json new file mode 100644 index 0000000000..4205c87efc --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1_after_no_next__0.json @@ -0,0 +1,21 @@ +[ + { + "name": "test5", + "testsuite": [ + "testsuite5" + ], + "flags": [ + "flag5" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-05T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1__0.json b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1__0.json new file mode 100644 index 0000000000..4205c87efc --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1__0.json @@ -0,0 +1,21 @@ +[ + { + "name": "test5", + "testsuite": [ + "testsuite5" + ], + "flags": [ + "flag5" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-05T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1_before__0.json b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1_before__0.json new file mode 100644 index 0000000000..f40128b02a --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1_before__0.json @@ -0,0 +1,21 @@ +[ + { + "name": "test4", + "testsuite": [ + "testsuite4" + ], + "flags": [ + "flag4" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-04T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1_before_no_previous__0.json b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1_before_no_previous__0.json new file mode 100644 index 0000000000..7125723b64 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1_before_no_previous__0.json @@ -0,0 +1,21 @@ +[ + { + "name": "test1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1__0.json b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1__0.json new file mode 100644 index 0000000000..4205c87efc --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1__0.json @@ -0,0 +1,21 @@ +[ + { + "name": "test5", + "testsuite": [ + "testsuite5" + ], + "flags": [ + "flag5" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-05T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1_after__0.json b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1_after__0.json new file mode 100644 index 0000000000..f40128b02a --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1_after__0.json @@ -0,0 +1,21 @@ +[ + { + "name": "test4", + "testsuite": [ + "testsuite4" + ], + "flags": [ + "flag4" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-04T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1_after_no_next__0.json b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1_after_no_next__0.json new file mode 100644 index 0000000000..7125723b64 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1_after_no_next__0.json @@ -0,0 +1,21 @@ +[ + { + "name": "test1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1__0.json b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1__0.json new file mode 100644 index 0000000000..7125723b64 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1__0.json @@ -0,0 +1,21 @@ +[ + { + "name": "test1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1_before__0.json b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1_before__0.json new file mode 100644 index 0000000000..7f325cfb41 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1_before__0.json @@ -0,0 +1,21 @@ +[ + { + "name": "test2", + "testsuite": [ + "testsuite2" + ], + "flags": [ + "flag2" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-02T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1_before_no_previous__0.json b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1_before_no_previous__0.json new file mode 100644 index 0000000000..4205c87efc --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1_before_no_previous__0.json @@ -0,0 +1,21 @@ +[ + { + "name": "test5", + "testsuite": [ + "testsuite5" + ], + "flags": [ + "flag5" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-05T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/apps/codecov-api/graphql_api/tests/test_account.py b/apps/codecov-api/graphql_api/tests/test_account.py new file mode 100644 index 0000000000..e0ea0f030c --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_account.py @@ -0,0 +1,233 @@ +from django.test import TestCase +from shared.django_apps.codecov_auth.tests.factories import ( + AccountFactory, + AccountsUsersFactory, + OktaSettingsFactory, + OwnerFactory, +) + +from .helper import GraphQLTestHelper + + +class AccountTestCase(GraphQLTestHelper, TestCase): + def setUp(self): + self.account = AccountFactory( + name="Test Account", plan_seat_count=10, free_seat_count=1 + ) + self.owner = OwnerFactory( + username="randomOwner", service="github", account=self.account + ) + self.okta_settings = OktaSettingsFactory( + account=self.account, + client_id="test-client-id", + client_secret="test-client-secret", + ) + + def test_fetch_okta_config(self) -> None: + query = """ + query { + owner(username: "%s"){ + account { + oktaConfig { + clientId + clientSecret + } + } + } + } + """ % (self.owner.username) + + result = self.gql_request(query, owner=self.owner) + + assert "errors" not in result + data = result["owner"]["account"] + assert data["oktaConfig"]["clientId"] == "test-client-id" + assert data["oktaConfig"]["clientSecret"] == "test-client-secret" + + def test_fetch_total_seat_count(self) -> None: + query = """ + query { + owner(username: "%s"){ + account { + totalSeatCount + } + } + } + """ % (self.owner.username) + + result = self.gql_request(query, owner=self.owner) + + assert "errors" not in result + seatCount = result["owner"]["account"]["totalSeatCount"] + assert seatCount == 11 + + def test_fetch_activated_user_count(self) -> None: + for _ in range(7): + AccountsUsersFactory(account=self.account) + + query = """ + query { + owner(username: "%s") { + account { + activatedUserCount + } + } + } + """ % (self.owner.username) + + result = self.gql_request(query, owner=self.owner) + + assert "errors" not in result + activatedUserCount = result["owner"]["account"]["activatedUserCount"] + assert activatedUserCount == 7 + + def test_fetch_organizations(self) -> None: + account = AccountFactory(name="account") + owner = OwnerFactory( + username="owner-0", + plan_activated_users=[], + account=account, + ) + OwnerFactory( + username="owner-1", + plan_activated_users=[0], + account=account, + ) + OwnerFactory( + username="owner-2", + plan_activated_users=[0, 1], + account=account, + ) + + query = """ + query { + owner(username: "%s") { + account { + organizations(first: 20) { + edges { + node { + username + activatedUserCount + } + } + } + } + } + } + """ % (owner.username) + + result = self.gql_request(query, owner=owner) + + assert "errors" not in result + + orgs = [ + node["node"]["username"] + for node in result["owner"]["account"]["organizations"]["edges"] + ] + + assert orgs == ["owner-0", "owner-1", "owner-2"] + + def test_fetch_organizations_desc(self) -> None: + account = AccountFactory(name="account") + owner = OwnerFactory( + username="owner-0", + plan_activated_users=[], + account=account, + ) + OwnerFactory( + username="owner-1", + plan_activated_users=[0], + account=account, + ) + OwnerFactory( + username="owner-2", + plan_activated_users=[0, 1], + account=account, + ) + + query = """ + query { + owner(username: "%s") { + account { + organizations(first: 20, orderingDirection: DESC) { + edges { + node { + username + activatedUserCount + } + } + } + } + } + } + """ % (owner.username) + + result = self.gql_request(query, owner=owner) + + assert "errors" not in result + + orgs = [ + node["node"]["username"] + for node in result["owner"]["account"]["organizations"]["edges"] + ] + + assert orgs == ["owner-2", "owner-1", "owner-0"] + + def test_fetch_organizations_pagination(self) -> None: + account = AccountFactory(name="account") + owner = OwnerFactory( + username="owner-0", + plan_activated_users=[], + account=account, + ) + OwnerFactory( + username="owner-1", + plan_activated_users=[0], + account=account, + ) + OwnerFactory( + username="owner-2", + plan_activated_users=[0, 1], + account=account, + ) + + query = """ + query { + owner(username: "%s") { + account { + organizations(first: 2) { + edges { + node { + username + activatedUserCount + } + } + totalCount + pageInfo { + hasNextPage + } + } + } + } + } + """ % (owner.username) + + result = self.gql_request(query, owner=owner) + + assert "errors" not in result + + totalCount = result["owner"]["account"]["organizations"]["totalCount"] + + assert totalCount == 3 + + orgs = [ + node["node"]["username"] + for node in result["owner"]["account"]["organizations"]["edges"] + ] + + assert orgs == ["owner-0", "owner-1"] + + hasNextPage = result["owner"]["account"]["organizations"]["pageInfo"][ + "hasNextPage" + ] + assert hasNextPage diff --git a/apps/codecov-api/graphql_api/tests/test_billing.py b/apps/codecov-api/graphql_api/tests/test_billing.py new file mode 100644 index 0000000000..5a9602925a --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_billing.py @@ -0,0 +1,73 @@ +from unittest.mock import patch + +from django.test import TestCase +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory +from stripe.api_resources import PaymentIntent, SetupIntent + +from .helper import GraphQLTestHelper + + +class BillingTestCase(GraphQLTestHelper, TestCase): + def setUp(self): + self.owner = OwnerFactory(stripe_customer_id="test-customer-id") + + def test_fetch_unverified_payment_methods(self): + query = """ + query { + owner(username: "%s") { + billing { + unverifiedPaymentMethods { + paymentMethodId + hostedVerificationUrl + } + } + } + } + """ % (self.owner.username) + + payment_intent = PaymentIntent.construct_from( + { + "payment_method": "pm_123", + "next_action": { + "type": "verify_with_microdeposits", + "verify_with_microdeposits": { + "hosted_verification_url": "https://verify.stripe.com/1" + }, + }, + }, + "fake_api_key", + ) + + setup_intent = SetupIntent.construct_from( + { + "payment_method": "pm_456", + "next_action": { + "type": "verify_with_microdeposits", + "verify_with_microdeposits": { + "hosted_verification_url": "https://verify.stripe.com/2" + }, + }, + }, + "fake_api_key", + ) + + with ( + patch( + "services.billing.stripe.PaymentIntent.list" + ) as payment_intent_list_mock, + patch("services.billing.stripe.SetupIntent.list") as setup_intent_list_mock, + ): + payment_intent_list_mock.return_value.data = [payment_intent] + payment_intent_list_mock.return_value.has_more = False + setup_intent_list_mock.return_value.data = [setup_intent] + setup_intent_list_mock.return_value.has_more = False + + result = self.gql_request(query, owner=self.owner) + + assert "errors" not in result + data = result["owner"]["billing"]["unverifiedPaymentMethods"] + assert len(data) == 2 + assert data[0]["paymentMethodId"] == "pm_123" + assert data[0]["hostedVerificationUrl"] == "https://verify.stripe.com/1" + assert data[1]["paymentMethodId"] == "pm_456" + assert data[1]["hostedVerificationUrl"] == "https://verify.stripe.com/2" diff --git a/apps/codecov-api/graphql_api/tests/test_branch.py b/apps/codecov-api/graphql_api/tests/test_branch.py new file mode 100644 index 0000000000..1e8f9405fa --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_branch.py @@ -0,0 +1,1548 @@ +from datetime import datetime, timedelta +from unittest.mock import PropertyMock, patch + +from django.test import TestCase, override_settings +from shared.django_apps.core.tests.factories import ( + BranchFactory, + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.reports.types import ReportTotals + +from services.components import Component + +from .helper import GraphQLTestHelper + +query_branch = """ + query FetchBranch($org: String!, $repo: String!, $branch: String!) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + branch(name: $branch) { + %s + } + } + } + } + } +""" + +query_files = """ + query FetchFiles($org: String!, $repo: String!, $branch: String!, $path: String!, $filters: PathContentsFilters!) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + branch(name: $branch) { + head { + pathContents (path: $path, filters: $filters) { + __typename + ... on PathContents { + results { + __typename + name + path + hits + misses + partials + lines + percentCovered + } + } + ... on MissingHeadReport { + message + } + ... on MissingCoverage { + message + } + ... on UnknownPath { + message + } + ... on UnknownFlags { + message + } + } + } + } + } + } + } + } +""" + +query_files_connection = """ + query FetchFiles($org: String!, $repo: String!, $branch: String!, $path: String!, $filters: PathContentsFilters!, $first: Int, $after: String, $last: Int, $before: String) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + branch(name: $branch) { + head { + deprecatedPathContents (path: $path, filters: $filters, first: $first, after: $after, last: $last, before: $before) { + __typename + ... on PathContentConnection { + edges { + cursor + node { + __typename + name + path + hits + misses + partials + lines + percentCovered + } + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + ... on MissingHeadReport { + message + } + ... on MissingCoverage { + message + } + ... on UnknownPath { + message + } + ... on UnknownFlags { + message + } + } + } + } + } + } + } + } +""" + + +class MockCoverage(object): + def __init__(self, coverage, hits, lines): + self.coverage = coverage + self.hits = hits + self.lines = lines + + +class MockTotals(object): + def __init__(self): + self.totals = ReportTotals.default_totals() + self.totals.hits = 8 + self.totals.lines = 10 + + +class MockFlag(object): + @property + def totals(self): + return MockTotals() + + +class MockSession(object): + pass + + +class MockFile: + def __init__(self, name: str): + self.name = name + + +class MockReport(object): + def __init__(self): + self.sessions = {1: MockSession()} + + def get(self, file): + return MockTotals() + + @property + def files(self): + return [ + "fileA.py", + "fileB.py", + "folder/fileB.py", + "folder/subfolder/fileC.py", + "folder/subfolder/fileD.py", + ] + + def __iter__(self): + for name in self.files: + yield MockFile(name) + + def get_flag_names(self): + return ["flag_a"] + + @property + def flags(self): + return {"flag-a": MockFlag()} + + def get_file_totals(self, path): + return MockTotals().totals + + def filter(self, paths, flags): + return MockFilteredReport() + + def sessions(self): + return [1, 2, 3] + + +class MockFilteredReport(MockReport): + pass + + +class MockNoFlagsReport(object): + def __init__(self): + self.sessions = {1: MockSession()} + + @property + def flags(self): + return None + + def get_flag_names(self): + return [] + + +class TestBranch(GraphQLTestHelper, TestCase): + def setUp(self): + self.org = OwnerFactory(username="codecov") + self.repo = RepositoryFactory(author=self.org, name="gazebo", private=False) + self.head = CommitFactory(repository=self.repo, timestamp=datetime.now()) + self.commit = CommitFactory(repository=self.repo) + self.branch = BranchFactory( + repository=self.repo, + head=self.head.commitid, + name="test1", + updatestamp=(datetime.now() + timedelta(1)), + ) + self.branch_2 = BranchFactory( + repository=self.repo, + head=self.commit.commitid, + name="test2", + updatestamp=(datetime.now() + timedelta(2)), + ) + + def test_fetch_branch(self): + query = query_branch % "name, headSha, head { commitid }" + variables = { + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + } + data = self.gql_request(query, variables=variables) + assert data["owner"]["repository"]["branch"] == { + "name": self.branch.name, + "headSha": self.head.commitid, + "head": { + "commitid": self.head.commitid, + }, + } + + def test_fetch_branch_missing_commit(self): + self.head.delete() + query = query_branch % "name, headSha, head { commitid }" + variables = { + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + } + data = self.gql_request(query, variables=variables) + assert data["owner"]["repository"]["branch"] == { + "name": self.branch.name, + "headSha": self.branch.head, + "head": None, + } + + def test_fetch_branches(self): + query_branches = """{ + owner(username: "%s") { + repository(name: "%s") { + ... on Repository { + branches { + edges { + node { + name + } + } + } + } + } + } + } + """ + variables = {"org": self.org.username, "repo": self.repo.name} + query = query_branches % (self.org.username, self.repo.name) + data = self.gql_request(query, variables=variables) + branches = data["owner"]["repository"]["branches"]["edges"] + assert isinstance(branches, list) + assert len(branches) == 3 + assert branches == [ + {"node": {"name": "test2"}}, + {"node": {"name": "test1"}}, + {"node": {"name": "main"}}, + ] + + def test_fetch_branches_with_filters(self): + query_branches = """{ + owner(username: "%s") { + repository(name: "%s") { + ... on Repository { + branches (filters: {searchValue: "%s"}) { + edges { + node { + name + } + } + } + } + } + } + } + """ + variables = {"org": self.org.username, "repo": self.repo.name} + query = query_branches % (self.org.username, self.repo.name, "test2") + data = self.gql_request(query, variables=variables) + branches = data["owner"]["repository"]["branches"]["edges"] + assert isinstance(branches, list) + assert len(branches) == 1 + assert branches == [ + {"node": {"name": "test2"}}, + ] + + @override_settings(DEBUG=True) + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_path_contents_with_no_report(self, report_mock): + report_mock.return_value = None + commit_without_report = CommitFactory(repository=self.repo) + branch = BranchFactory( + repository=self.repo, + head=commit_without_report.commitid, + name="branch-two", + updatestamp=(datetime.now() + timedelta(1)), + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "branch": branch.name, + "path": "", + "filters": {}, + } + data = self.gql_request(query_files, variables=variables) + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "pathContents": { + "__typename": "MissingHeadReport", + "message": "Missing head report", + } + } + } + } + } + } + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_path_contents_with_files(self, report_mock): + variables = { + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "", + "filters": { + "ordering": { + "direction": "DESC", + "parameter": "NAME", + } + }, + } + report_mock.return_value = MockReport() + + data = self.gql_request(query_files, variables=variables) + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "pathContents": { + "__typename": "PathContents", + "results": [ + { + "__typename": "PathContentDir", + "name": "folder", + "path": "folder", + "hits": 24, + "misses": 0, + "partials": 0, + "lines": 30, + "percentCovered": 80.0, + }, + { + "__typename": "PathContentFile", + "name": "fileB.py", + "path": "fileB.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + { + "__typename": "PathContentFile", + "name": "fileA.py", + "path": "fileA.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + ], + } + } + } + } + } + } + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_path_contents_with_files_and_path_prefix( + self, + report_mock, + ): + variables = { + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "folder", + "filters": { + "ordering": { + "direction": "ASC", + "parameter": "HITS", + } + }, + } + report_mock.return_value = MockReport() + + data = self.gql_request(query_files, variables=variables) + + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "pathContents": { + "__typename": "PathContents", + "results": [ + { + "__typename": "PathContentFile", + "name": "fileB.py", + "path": "folder/fileB.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + { + "__typename": "PathContentDir", + "name": "subfolder", + "path": "folder/subfolder", + "hits": 16, + "misses": 0, + "partials": 0, + "lines": 20, + "percentCovered": 80.0, + }, + ], + } + } + } + } + } + } + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_path_contents_with_files_and_search_value_case_insensitive( + self, + report_mock, + ): + variables = { + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "", + "filters": { + "searchValue": "fileB", + }, + } + report_mock.return_value = MockReport() + + data = self.gql_request(query_files, variables=variables) + + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "pathContents": { + "__typename": "PathContents", + "results": [ + { + "__typename": "PathContentFile", + "name": "fileB.py", + "path": "fileB.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + { + "__typename": "PathContentFile", + "name": "fileB.py", + "path": "folder/fileB.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + ], + } + } + } + } + } + } + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_path_contents_with_files_and_list_display_type(self, report_mock): + variables = { + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "", + "filters": { + "displayType": "LIST", + }, + } + report_mock.return_value = MockReport() + + data = self.gql_request(query_files, variables=variables) + + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "pathContents": { + "__typename": "PathContents", + "results": [ + { + "__typename": "PathContentFile", + "name": "fileA.py", + "path": "fileA.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + { + "__typename": "PathContentFile", + "name": "fileB.py", + "path": "fileB.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + { + "__typename": "PathContentFile", + "name": "fileB.py", + "path": "folder/fileB.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + { + "__typename": "PathContentFile", + "name": "fileC.py", + "path": "folder/subfolder/fileC.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + { + "__typename": "PathContentFile", + "name": "fileD.py", + "path": "folder/subfolder/fileD.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + ], + } + } + } + } + } + } + assert len( + data["owner"]["repository"]["branch"]["head"]["pathContents"]["results"] + ) == len(report_mock.return_value.files) + + @patch("services.path.provider_path_exists") + @patch("services.path.ReportPaths.paths", new_callable=PropertyMock) + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_path_contents_missing_coverage( + self, report_mock, paths_mock, provider_path_exists_mock + ): + report_mock.return_value = MockReport() + paths_mock.return_value = [] + provider_path_exists_mock.return_value = True + + data = self.gql_request( + query_files, + variables={ + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "invalid", + "filters": {}, + }, + ) + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "pathContents": { + "__typename": "MissingCoverage", + "message": "missing coverage for path: invalid", + } + } + } + } + } + } + + @patch("services.path.provider_path_exists") + @patch("services.path.ReportPaths.paths", new_callable=PropertyMock) + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_path_contents_unknown_path( + self, report_mock, paths_mock, provider_path_exists_mock + ): + report_mock.return_value = MockReport() + paths_mock.return_value = [] + provider_path_exists_mock.return_value = False + + data = self.gql_request( + query_files, + variables={ + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "invalid", + "filters": {}, + }, + ) + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "pathContents": { + "__typename": "UnknownPath", + "message": "path does not exist: invalid", + } + } + } + } + } + } + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_path_contents_unknown_flags_no_flags(self, report_mock): + report_mock.return_value = MockNoFlagsReport() + + data = self.gql_request( + query_files, + variables={ + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "", + "filters": {"flags": ["test-123"]}, + }, + ) + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "pathContents": { + "__typename": "UnknownFlags", + "message": "No coverage with chosen flags: ['test-123']", + } + } + } + } + } + } + + @patch("services.components.commit_components") + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_path_contents_component_filter_missing_coverage( + self, report_mock, commit_components_mock + ): + components = ["ComponentThree"] + variables = { + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "", + "filters": {"components": components}, + } + + report_mock.return_value = MockReport() + commit_components_mock.return_value = [ + Component.from_dict( + { + "component_id": "c1", + "name": "ComponentOne", + "paths": ["fileA.py"], + } + ), + Component.from_dict( + { + "component_id": "c2", + "name": "ComponentTwo", + "paths": ["fileB.py"], + } + ), + Component.from_dict( + { + "component_id": "global", + "name": "Global", + "paths": ["(?s:.*/[^\\/]*\\.py.*)\\Z"], + } + ), + ] + + data = self.gql_request(query_files, variables=variables) + + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "pathContents": { + "__typename": "MissingCoverage", + "message": f"missing coverage for report with components: {components}", + } + } + } + } + } + } + + @patch("services.components.component_filtered_report") + @patch("services.components.commit_components") + @patch("shared.reports.api_report_service.build_report_from_commit") + @patch("services.path.ReportPaths.files", new_callable=PropertyMock) + def test_fetch_path_contents_component_filter_has_coverage( + self, files_mock, report_mock, commit_components_mock, filtered_mock + ): + files_mock.return_value = [ + "folder/fileB.py", + "folder/subfolder/fileC.py", + "folder/subfolder/fileD.py", + ] + report_mock.return_value = MockReport() + commit_components_mock.return_value = [ + Component.from_dict( + { + "component_id": "c1", + "name": "ComponentOne", + "paths": ["fileA.py"], + } + ), + Component.from_dict( + { + "component_id": "c2", + "name": "ComponentTwo", + "paths": ["fileB.py"], + } + ), + Component.from_dict( + { + "component_id": "global", + "name": "Global", + "paths": ["(?s:.*/[^\\/]*\\.py.*)\\Z"], + } + ), + ] + filtered_mock.return_value = MockReport() + + components = ["Global"] + variables = { + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "", + "filters": {"components": components}, + } + data = self.gql_request(query_files, variables=variables) + + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "pathContents": { + "__typename": "PathContents", + "results": [ + { + "__typename": "PathContentDir", + "hits": 24, + "lines": 30, + "misses": 0, + "name": "folder", + "partials": 0, + "path": "folder", + "percentCovered": 80.0, + } + ], + } + } + } + } + } + } + + @patch("services.components.component_filtered_report") + @patch("services.components.commit_components") + @patch("shared.reports.api_report_service.build_report_from_commit") + @patch("services.path.ReportPaths.files", new_callable=PropertyMock) + def test_fetch_path_contents_component_and_flag_filters( + self, + files_mock, + report_mock, + commit_components_mock, + filtered_mock, + ): + files_mock.return_value = ["fileA.py"] + report_mock.return_value = MockReport() + commit_components_mock.return_value = [ + Component.from_dict( + { + "component_id": "unit", + "name": "unit", + "paths": ["fileA.py"], + "flag_regexes": "flag-a", + } + ), + Component.from_dict( + { + "component_id": "integration", + "name": "integration", + "paths": ["fileB.py"], + } + ), + Component.from_dict( + { + "component_id": "global", + "name": "Global", + "paths": ["(?s:.*/[^\\/]*\\.py.*)\\Z"], + "flag_regexes": "flag-a", + } + ), + ] + filtered_mock.return_value = MockReport() + + query_files = """ + query FetchFiles($org: String!, $repo: String!, $branch: String!, $path: String!, $filters: PathContentsFilters!) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + branch(name: $branch) { + head { + pathContents (path: $path, filters: $filters) { + __typename + ... on PathContents { + results { + __typename + name + path + } + } + ... on MissingCoverage { + message + } + ... on UnknownFlags { + message + } + } + } + } + } + } + } + } + """ + components, flags = ["unit"], ["flag-a"] + variables = { + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "", + "filters": {"components": components, "flags": flags}, + } + data = self.gql_request(query_files, variables=variables) + + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "pathContents": { + "__typename": "PathContents", + "results": [ + { + "__typename": "PathContentFile", + "name": "fileA.py", + "path": "fileA.py", + } + ], + } + } + } + } + } + } + + @patch("services.components.component_filtered_report") + @patch("services.components.commit_components") + @patch("shared.reports.api_report_service.build_report_from_commit") + @patch("services.path.ReportPaths.files", new_callable=PropertyMock) + def test_fetch_path_contents_component_and_flag_filters_unknown_flags( + self, files_mock, report_mock, commit_components_mock, filtered_mock + ): + files_mock.return_value = ["fileA.py"] + report_mock.return_value = MockNoFlagsReport() + commit_components_mock.return_value = [ + Component.from_dict( + { + "component_id": "unit", + "name": "unit", + "paths": ["fileA.py"], + "flag_regexes": "flag-a", + } + ), + Component.from_dict( + { + "component_id": "integration", + "name": "integration", + "paths": ["fileB.py"], + } + ), + Component.from_dict( + { + "component_id": "global", + "name": "Global", + "paths": ["(?s:.*/[^\\/]*\\.py.*)\\Z"], + "flag_regexes": "flag-a", + } + ), + ] + filtered_mock.return_value = MockNoFlagsReport() + + query_files = """ + query FetchFiles($org: String!, $repo: String!, $branch: String!, $path: String!, $filters: PathContentsFilters!) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + branch(name: $branch) { + head { + pathContents (path: $path, filters: $filters) { + __typename + ... on PathContents { + results { + __typename + name + path + } + } + ... on MissingCoverage { + message + } + ... on UnknownFlags { + message + } + } + } + } + } + } + } + } + """ + components, flags = ["integration"], ["flag-a"] + variables = { + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "", + "filters": {"components": components, "flags": flags}, + } + data = self.gql_request(query_files, variables=variables) + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "pathContents": { + "__typename": "UnknownFlags", + "message": f"No coverage with chosen flags: {flags}", + } + } + } + } + } + } + + @patch("services.components.component_filtered_report") + @patch("services.components.commit_components") + @patch("shared.reports.api_report_service.build_report_from_commit") + @patch("services.path.ReportPaths.files", new_callable=PropertyMock) + def test_fetch_path_contents_component_flags_filters( + self, + files_mock, + report_mock, + commit_components_mock, + filtered_mock, + ): + files_mock.return_value = ["fileA.py"] + report_mock.return_value = MockReport() + commit_components_mock.return_value = [ + Component.from_dict( + { + "component_id": "unit", + "name": "unit", + "paths": ["fileA.py"], + "flag_regexes": "flag-a", + } + ), + Component.from_dict( + { + "component_id": "integration", + "name": "integration", + "paths": ["fileB.py"], + } + ), + Component.from_dict( + { + "component_id": "global", + "name": "Global", + "paths": ["(?s:.*/[^\\/]*\\.py.*)\\Z"], + "flag_regexes": "flag-a", + } + ), + ] + filtered_mock.return_value = MockReport() + + query_files = """ + query FetchFiles($org: String!, $repo: String!, $branch: String!, $path: String!, $filters: PathContentsFilters!) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + branch(name: $branch) { + head { + pathContents (path: $path, filters: $filters) { + __typename + ... on PathContents { + results { + __typename + name + path + } + } + ... on MissingCoverage { + message + } + ... on UnknownFlags { + message + } + } + } + } + } + } + } + } + """ + components = ["unit"] + variables = { + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "", + "filters": {"components": components}, + } + data = self.gql_request(query_files, variables=variables) + + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "pathContents": { + "__typename": "PathContents", + "results": [ + { + "__typename": "PathContentFile", + "name": "fileA.py", + "path": "fileA.py", + } + ], + } + } + } + } + } + } + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_path_contents_deprecated(self, report_mock): + report_mock.return_value = MockReport() + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "", + "filters": {}, + } + + data = self.gql_request(query_files, variables=variables) + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "pathContents": { + "__typename": "PathContents", + "results": [ + { + "__typename": "PathContentFile", + "name": "fileA.py", + "path": "fileA.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + { + "__typename": "PathContentFile", + "name": "fileB.py", + "path": "fileB.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + { + "__typename": "PathContentDir", + "name": "folder", + "path": "folder", + "hits": 24, + "misses": 0, + "partials": 0, + "lines": 30, + "percentCovered": 80.0, + }, + ], + } + } + } + } + } + } + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_path_contents_deprecated_paginated( + self, + report_mock, + ): + report_mock.return_value = MockReport() + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "", + "filters": {}, + "first": 2, + } + + data = self.gql_request(query_files_connection, variables=variables) + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "deprecatedPathContents": { + "__typename": "PathContentConnection", + "edges": [ + { + "cursor": "0", + "node": { + "__typename": "PathContentFile", + "name": "fileA.py", + "path": "fileA.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + }, + { + "cursor": "1", + "node": { + "__typename": "PathContentFile", + "name": "fileB.py", + "path": "fileB.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + }, + ], + "totalCount": 3, + "pageInfo": { + "hasNextPage": True, + "hasPreviousPage": False, + "startCursor": "0", + "endCursor": "1", + }, + } + } + } + } + } + } + + @override_settings(DEBUG=True) + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_path_contents_deprecated_with_no_report(self, report_mock): + report_mock.return_value = None + commit_without_report = CommitFactory(repository=self.repo) + branch = BranchFactory( + repository=self.repo, + head=commit_without_report.commitid, + name="branch-two", + updatestamp=(datetime.now() + timedelta(1)), + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "branch": branch.name, + "path": "", + "filters": {}, + } + data = self.gql_request(query_files_connection, variables=variables) + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "deprecatedPathContents": { + "__typename": "MissingHeadReport", + "message": "Missing head report", + } + } + } + } + } + } + + @override_settings(DEBUG=True) + @patch("services.path.provider_path_exists") + @patch("services.path.ReportPaths.paths", new_callable=PropertyMock) + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_path_contents_deprecated_missing_coverage( + self, report_mock, paths_mock, provider_path_exists_mock + ): + report_mock.return_value = MockReport() + paths_mock.return_value = [] + provider_path_exists_mock.return_value = True + + data = self.gql_request( + query_files_connection, + variables={ + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "invalid", + "filters": {}, + }, + ) + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "deprecatedPathContents": { + "__typename": "MissingCoverage", + "message": "missing coverage for path: invalid", + } + } + } + } + } + } + + @override_settings(DEBUG=True) + @patch("services.path.provider_path_exists") + @patch("services.path.ReportPaths.paths", new_callable=PropertyMock) + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_path_contents_deprecated_unknown_path( + self, report_mock, paths_mock, provider_path_exists_mock + ): + report_mock.return_value = MockReport() + paths_mock.return_value = [] + provider_path_exists_mock.return_value = False + + data = self.gql_request( + query_files_connection, + variables={ + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "invalid", + "filters": {}, + }, + ) + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "deprecatedPathContents": { + "__typename": "UnknownPath", + "message": "path does not exist: invalid", + } + } + } + } + } + } + + @override_settings(DEBUG=True) + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_path_contents_deprecated_unknown_flags_no_flags(self, report_mock): + report_mock.return_value = MockNoFlagsReport() + + data = self.gql_request( + query_files_connection, + variables={ + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "", + "filters": {"flags": ["test-123"]}, + }, + ) + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "deprecatedPathContents": { + "__typename": "UnknownFlags", + "message": "No coverage with chosen flags: ['test-123']", + } + } + } + } + } + } + + @override_settings(DEBUG=True) + @patch("services.components.commit_components") + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_path_contents_deprecated_component_filter_missing_coverage( + self, report_mock, commit_components_mock + ): + components = ["ComponentThree"] + variables = { + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "", + "filters": {"components": components}, + } + + report_mock.return_value = MockReport() + commit_components_mock.return_value = [ + Component.from_dict( + { + "component_id": "c1", + "name": "ComponentOne", + "paths": ["fileA.py"], + } + ), + Component.from_dict( + { + "component_id": "c2", + "name": "ComponentTwo", + "paths": ["fileB.py"], + } + ), + Component.from_dict( + { + "component_id": "global", + "name": "Global", + "paths": ["(?s:.*/[^\\/]*\\.py.*)\\Z"], + } + ), + ] + + data = self.gql_request(query_files_connection, variables=variables) + + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "deprecatedPathContents": { + "__typename": "MissingCoverage", + "message": f"missing coverage for report with components: {components}", + } + } + } + } + } + } + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_path_contents_deprecated_with_files_and_list_display_type( + self, report_mock + ): + variables = { + "org": self.org.username, + "repo": self.repo.name, + "branch": self.branch.name, + "path": "", + "filters": { + "displayType": "LIST", + }, + } + report_mock.return_value = MockReport() + + data = self.gql_request(query_files_connection, variables=variables) + assert data == { + "owner": { + "repository": { + "branch": { + "head": { + "deprecatedPathContents": { + "__typename": "PathContentConnection", + "edges": [ + { + "cursor": "0", + "node": { + "__typename": "PathContentFile", + "name": "fileA.py", + "path": "fileA.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + }, + { + "cursor": "1", + "node": { + "__typename": "PathContentFile", + "name": "fileB.py", + "path": "fileB.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + }, + { + "cursor": "2", + "node": { + "__typename": "PathContentFile", + "name": "fileB.py", + "path": "folder/fileB.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + }, + { + "cursor": "3", + "node": { + "__typename": "PathContentFile", + "name": "fileC.py", + "path": "folder/subfolder/fileC.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + }, + { + "cursor": "4", + "node": { + "__typename": "PathContentFile", + "name": "fileD.py", + "path": "folder/subfolder/fileD.py", + "hits": 8, + "misses": 0, + "partials": 0, + "lines": 10, + "percentCovered": 80.0, + }, + }, + ], + "totalCount": 5, + "pageInfo": { + "hasNextPage": False, + "hasPreviousPage": False, + "startCursor": "0", + "endCursor": "4", + }, + } + } + } + } + } + } diff --git a/apps/codecov-api/graphql_api/tests/test_bundle_analysis_measurements.py b/apps/codecov-api/graphql_api/tests/test_bundle_analysis_measurements.py new file mode 100644 index 0000000000..9b53945c6b --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_bundle_analysis_measurements.py @@ -0,0 +1,3402 @@ +from unittest.mock import patch + +from django.test import TestCase +from shared.api_archive.archive import ArchiveService +from shared.bundle_analysis import StoragePaths +from shared.bundle_analysis.storage import get_bucket_name +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.storage.memory import MemoryStorageService + +from reports.models import CommitReport +from reports.tests.factories import CommitReportFactory +from timeseries.tests.factories import MeasurementFactory + +from .helper import GraphQLTestHelper + + +class TestBundleAnalysisMeasurements(GraphQLTestHelper, TestCase): + databases = {"default", "timeseries"} + + def setUp(self): + self.org = OwnerFactory(username="codecov") + self.repo = RepositoryFactory(author=self.org, name="gazebo", private=False) + self.parent_commit = CommitFactory(repository=self.repo) + self.commit = CommitFactory( + repository=self.repo, + totals={"c": "12", "diff": [0, 0, 0, 0, 0, "14"]}, + parent_commit_id=self.parent_commit.commitid, + ) + self.head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + measurements_data = [ + # 2024-06-10 + ["bundle_analysis_report_size", "super", "2024-06-10T19:07:22", 29927], + ["bundle_analysis_font_size", "super", "2024-06-10T19:07:22", 290], + ["bundle_analysis_image_size", "super", "2024-06-10T19:07:22", 2900], + ["bundle_analysis_stylesheet_size", "super", "2024-06-10T19:07:22", 29], + ["bundle_analysis_javascript_size", "super", "2024-06-10T19:07:22", 26708], + [ + "bundle_analysis_asset_size", + "ca05c27a-74f7-4d0e-a851-537c7b2bcb48", + "2024-06-10T19:07:22", + 14126, + ], + [ + "bundle_analysis_asset_size", + "4e03bec3-1af3-4e58-b1b7-99aa995122a6", + "2024-06-10T19:07:22", + 11421, + ], + # 2024-06-06 + ["bundle_analysis_report_size", "super", "2024-06-06T19:07:22", 6263], + ["bundle_analysis_font_size", "super", "2024-06-06T19:07:22", 50], + ["bundle_analysis_image_size", "super", "2024-06-06T19:07:22", 500], + ["bundle_analysis_stylesheet_size", "super", "2024-06-06T19:07:22", 5], + ["bundle_analysis_javascript_size", "super", "2024-06-06T19:07:22", 5708], + [ + "bundle_analysis_asset_size", + "ca05c27a-74f7-4d0e-a851-537c7b2bcb48", + "2024-06-06T19:07:22", + 4126, + ], + [ + "bundle_analysis_asset_size", + "4e03bec3-1af3-4e58-b1b7-99aa995122a6", + "2024-06-06T19:07:22", + 1421, + ], + ] + + for item in measurements_data: + MeasurementFactory( + name=item[0], + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id=item[1], + commit_sha=self.commit.pk, + timestamp=item[2], + value=item[3], + ) + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_report_measurements(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + with open("./services/tests/samples/bundle_with_uuid.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=self.head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchMeasurements( + $org: String!, + $repo: String!, + $commit: String! + $filters: BundleAnalysisMeasurementsSetFilters + $orderingDirection: OrderingDirection! + $interval: MeasurementInterval! + $before: DateTime! + $after: DateTime! + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "super") { + name + measurements( + filters: $filters + orderingDirection: $orderingDirection + after: $after + interval: $interval + before: $before + ){ + assetType + name + size { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + change { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + measurements { + avg + min + max + timestamp + } + } + } + } + } + } + } + } + } + } + } + """ + + # Test without using asset type filters + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "orderingDirection": "ASC", + "interval": "INTERVAL_1_DAY", + "after": "2024-06-06", + "before": "2024-06-10", + "filters": {}, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "measurements": [ + { + "assetType": "ASSET_SIZE", + "change": { + "loadTime": { + "highSpeed": 2, + "threeG": 106, + }, + "size": { + "gzip": 10, + "uncompress": 10000, + }, + }, + "measurements": [ + { + "avg": 4126.0, + "max": 4126.0, + "min": 4126.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 14126.0, + "max": 14126.0, + "min": 14126.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": "asset-*.js", + "size": { + "loadTime": { + "highSpeed": 3, + "threeG": 150, + }, + "size": { + "gzip": 14, + "uncompress": 14126, + }, + }, + }, + { + "assetType": "ASSET_SIZE", + "change": { + "loadTime": { + "highSpeed": 2, + "threeG": 106, + }, + "size": { + "gzip": 10, + "uncompress": 10000, + }, + }, + "measurements": [ + { + "avg": 1421.0, + "max": 1421.0, + "min": 1421.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 11421.0, + "max": 11421.0, + "min": 11421.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": "asset-*.js", + "size": { + "loadTime": { + "highSpeed": 3, + "threeG": 121, + }, + "size": { + "gzip": 11, + "uncompress": 11421, + }, + }, + }, + { + "assetType": "ASSET_SIZE", + "change": None, + "measurements": [], + "name": "asset-*.js", + "size": None, + }, + { + "assetType": "FONT_SIZE", + "change": { + "loadTime": { + "highSpeed": 0, + "threeG": 2, + }, + "size": { + "gzip": 0, + "uncompress": 240, + }, + }, + "measurements": [ + { + "avg": 50.0, + "max": 50.0, + "min": 50.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 290.0, + "max": 290.0, + "min": 290.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 0, + "threeG": 3, + }, + "size": { + "gzip": 0, + "uncompress": 290, + }, + }, + }, + { + "assetType": "IMAGE_SIZE", + "change": { + "loadTime": { + "highSpeed": 0, + "threeG": 25, + }, + "size": { + "gzip": 2, + "uncompress": 2400, + }, + }, + "measurements": [ + { + "avg": 500.0, + "max": 500.0, + "min": 500.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 2900.0, + "max": 2900.0, + "min": 2900.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 0, + "threeG": 30, + }, + "size": { + "gzip": 2, + "uncompress": 2900, + }, + }, + }, + { + "assetType": "JAVASCRIPT_SIZE", + "change": { + "loadTime": { + "highSpeed": 5, + "threeG": 224, + }, + "size": { + "gzip": 21, + "uncompress": 21000, + }, + }, + "measurements": [ + { + "avg": 5708.0, + "max": 5708.0, + "min": 5708.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 26708.0, + "max": 26708.0, + "min": 26708.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 7, + "threeG": 284, + }, + "size": { + "gzip": 26, + "uncompress": 26708, + }, + }, + }, + { + "assetType": "REPORT_SIZE", + "change": { + "loadTime": { + "highSpeed": 6, + "threeG": 252, + }, + "size": { + "gzip": 23, + "uncompress": 23664, + }, + }, + "measurements": [ + { + "avg": 6263.0, + "max": 6263.0, + "min": 6263.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 29927.0, + "max": 29927.0, + "min": 29927.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 7, + "threeG": 319, + }, + "size": { + "gzip": 29, + "uncompress": 29927, + }, + }, + }, + { + "assetType": "STYLESHEET_SIZE", + "change": { + "loadTime": { + "highSpeed": 0, + "threeG": 0, + }, + "size": { + "gzip": 0, + "uncompress": 24, + }, + }, + "measurements": [ + { + "avg": 5.0, + "max": 5.0, + "min": 5.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 29.0, + "max": 29.0, + "min": 29.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 0, + "threeG": 0, + }, + "size": { + "gzip": 0, + "uncompress": 29, + }, + }, + }, + { + "assetType": "UNKNOWN_SIZE", + "change": { + "loadTime": { + "highSpeed": 0, + "threeG": 0, + }, + "size": { + "gzip": 0, + "uncompress": 0, + }, + }, + "measurements": [ + { + "avg": 0.0, + "max": 0.0, + "min": 0.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 0.0, + "max": 0.0, + "min": 0.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 0, + "threeG": 0, + }, + "size": { + "gzip": 0, + "uncompress": 0, + }, + }, + }, + ], + "name": "super", + }, + } + + # Test with using asset type filters + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "orderingDirection": "ASC", + "interval": "INTERVAL_1_DAY", + "after": "2024-06-06", + "before": "2024-06-10", + "filters": {"assetTypes": "JAVASCRIPT_SIZE"}, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "measurements": [ + { + "assetType": "JAVASCRIPT_SIZE", + "change": { + "loadTime": { + "highSpeed": 5, + "threeG": 224, + }, + "size": { + "gzip": 21, + "uncompress": 21000, + }, + }, + "measurements": [ + { + "avg": 5708.0, + "max": 5708.0, + "min": 5708.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 26708.0, + "max": 26708.0, + "min": 26708.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 7, + "threeG": 284, + }, + "size": { + "gzip": 26, + "uncompress": 26708, + }, + }, + }, + ], + "name": "super", + }, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_asset_measurements(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + with open("./services/tests/samples/bundle_with_uuid.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=self.head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchMeasurements( + $org: String!, + $repo: String!, + $commit: String! + $interval: MeasurementInterval! + $before: DateTime! + $after: DateTime! + $asset: String! + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "super") { + asset(name: $asset){ + name + measurements( + after: $after + interval: $interval + before: $before + ){ + assetType + name + size { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + change { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + measurements { + avg + min + max + timestamp + } + } + } + } + } + } + } + } + } + } + } + } + """ + + # Tests can only fetch JS asset + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "interval": "INTERVAL_1_DAY", + "after": "2024-06-06", + "before": "2024-06-10", + "asset": "asset-same-name-diff-modules.js", + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "asset": { + "measurements": { + "assetType": "JAVASCRIPT_SIZE", + "change": { + "loadTime": { + "highSpeed": 2, + "threeG": 106, + }, + "size": { + "gzip": 10, + "uncompress": 10000, + }, + }, + "measurements": [ + { + "avg": 4126.0, + "max": 4126.0, + "min": 4126.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 14126.0, + "max": 14126.0, + "min": 14126.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": "asset-*.js", + "size": { + "loadTime": { + "highSpeed": 3, + "threeG": 150, + }, + "size": { + "gzip": 14, + "uncompress": 14126, + }, + }, + }, + "name": "asset-same-name-diff-modules.js", + }, + }, + } + + # Tests non-JS asset can't be fetched + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "interval": "INTERVAL_1_DAY", + "after": "2024-06-06", + "before": "2024-06-10", + "asset": "asset-css-A-TWO.css", + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "asset": { + "measurements": None, + "name": "asset-css-A-TWO.css", + }, + }, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_report_measurements_carryovers(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + with open("./services/tests/samples/bundle_with_uuid.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=self.head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchMeasurements( + $org: String!, + $repo: String!, + $commit: String! + $filters: BundleAnalysisMeasurementsSetFilters + $orderingDirection: OrderingDirection! + $interval: MeasurementInterval! + $before: DateTime! + $after: DateTime! + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "super") { + name + measurements( + filters: $filters + orderingDirection: $orderingDirection + after: $after + interval: $interval + before: $before + ){ + assetType + name + size { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + change { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + measurements { + avg + min + max + timestamp + } + } + } + } + } + } + } + } + } + } + } + """ + + # Test without using asset type filters + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "orderingDirection": "ASC", + "interval": "INTERVAL_1_DAY", + "after": "2024-06-07", + "before": "2024-06-10", + "filters": {}, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "measurements": [ + { + "assetType": "ASSET_SIZE", + "change": { + "loadTime": { + "highSpeed": 2, + "threeG": 106, + }, + "size": { + "gzip": 10, + "uncompress": 10000, + }, + }, + "measurements": [ + { + "avg": 4126.0, + "max": 4126.0, + "min": 4126.0, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 14126.0, + "max": 14126.0, + "min": 14126.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": "asset-*.js", + "size": { + "loadTime": { + "highSpeed": 3, + "threeG": 150, + }, + "size": { + "gzip": 14, + "uncompress": 14126, + }, + }, + }, + { + "assetType": "ASSET_SIZE", + "change": { + "loadTime": { + "highSpeed": 2, + "threeG": 106, + }, + "size": { + "gzip": 10, + "uncompress": 10000, + }, + }, + "measurements": [ + { + "avg": 1421.0, + "max": 1421.0, + "min": 1421.0, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 11421.0, + "max": 11421.0, + "min": 11421.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": "asset-*.js", + "size": { + "loadTime": { + "highSpeed": 3, + "threeG": 121, + }, + "size": { + "gzip": 11, + "uncompress": 11421, + }, + }, + }, + { + "assetType": "ASSET_SIZE", + "change": None, + "measurements": [], + "name": "asset-*.js", + "size": None, + }, + { + "assetType": "FONT_SIZE", + "change": { + "loadTime": { + "highSpeed": 0, + "threeG": 2, + }, + "size": { + "gzip": 0, + "uncompress": 240, + }, + }, + "measurements": [ + { + "avg": 50.0, + "max": 50.0, + "min": 50.0, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 290.0, + "max": 290.0, + "min": 290.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 0, + "threeG": 3, + }, + "size": { + "gzip": 0, + "uncompress": 290, + }, + }, + }, + { + "assetType": "IMAGE_SIZE", + "change": { + "loadTime": { + "highSpeed": 0, + "threeG": 25, + }, + "size": { + "gzip": 2, + "uncompress": 2400, + }, + }, + "measurements": [ + { + "avg": 500.0, + "max": 500.0, + "min": 500.0, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 2900.0, + "max": 2900.0, + "min": 2900.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 0, + "threeG": 30, + }, + "size": { + "gzip": 2, + "uncompress": 2900, + }, + }, + }, + { + "assetType": "JAVASCRIPT_SIZE", + "change": { + "loadTime": { + "highSpeed": 5, + "threeG": 224, + }, + "size": { + "gzip": 21, + "uncompress": 21000, + }, + }, + "measurements": [ + { + "avg": 5708.0, + "max": 5708.0, + "min": 5708.0, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 26708.0, + "max": 26708.0, + "min": 26708.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 7, + "threeG": 284, + }, + "size": { + "gzip": 26, + "uncompress": 26708, + }, + }, + }, + { + "assetType": "REPORT_SIZE", + "change": { + "loadTime": { + "highSpeed": 6, + "threeG": 252, + }, + "size": { + "gzip": 23, + "uncompress": 23664, + }, + }, + "measurements": [ + { + "avg": 6263.0, + "max": 6263.0, + "min": 6263.0, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 29927.0, + "max": 29927.0, + "min": 29927.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 7, + "threeG": 319, + }, + "size": { + "gzip": 29, + "uncompress": 29927, + }, + }, + }, + { + "assetType": "STYLESHEET_SIZE", + "change": { + "loadTime": { + "highSpeed": 0, + "threeG": 0, + }, + "size": { + "gzip": 0, + "uncompress": 24, + }, + }, + "measurements": [ + { + "avg": 5.0, + "max": 5.0, + "min": 5.0, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 29.0, + "max": 29.0, + "min": 29.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 0, + "threeG": 0, + }, + "size": { + "gzip": 0, + "uncompress": 29, + }, + }, + }, + { + "assetType": "UNKNOWN_SIZE", + "change": { + "loadTime": { + "highSpeed": 0, + "threeG": 0, + }, + "size": { + "gzip": 0, + "uncompress": 0, + }, + }, + "measurements": [ + { + "avg": 0.0, + "max": 0.0, + "min": 0.0, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 0.0, + "max": 0.0, + "min": 0.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 0, + "threeG": 0, + }, + "size": { + "gzip": 0, + "uncompress": 0, + }, + }, + }, + ], + "name": "super", + }, + } + + # Test with using asset type filters + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "orderingDirection": "ASC", + "interval": "INTERVAL_1_DAY", + "after": "2024-06-07", + "before": "2024-06-10", + "filters": {"assetTypes": "JAVASCRIPT_SIZE"}, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "measurements": [ + { + "assetType": "JAVASCRIPT_SIZE", + "change": { + "loadTime": { + "highSpeed": 5, + "threeG": 224, + }, + "size": { + "gzip": 21, + "uncompress": 21000, + }, + }, + "measurements": [ + { + "avg": 5708.0, + "max": 5708.0, + "min": 5708.0, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 26708.0, + "max": 26708.0, + "min": 26708.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 7, + "threeG": 284, + }, + "size": { + "gzip": 26, + "uncompress": 26708, + }, + }, + }, + ], + "name": "super", + }, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_report_no_carryovers(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + with open("./services/tests/samples/bundle_with_uuid.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=self.head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchMeasurements( + $org: String!, + $repo: String!, + $commit: String! + $filters: BundleAnalysisMeasurementsSetFilters + $orderingDirection: OrderingDirection! + $interval: MeasurementInterval! + $before: DateTime! + $after: DateTime! + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "super") { + name + measurements( + filters: $filters + orderingDirection: $orderingDirection + after: $after + interval: $interval + before: $before + ){ + assetType + name + size { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + change { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + measurements { + avg + min + max + timestamp + } + } + } + } + } + } + } + } + } + } + } + """ + + # Test without using asset type filters + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "orderingDirection": "ASC", + "interval": "INTERVAL_1_DAY", + "after": "2024-06-05", + "before": "2024-06-10", + "filters": {}, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "measurements": [ + { + "assetType": "ASSET_SIZE", + "change": { + "loadTime": { + "highSpeed": 2, + "threeG": 106, + }, + "size": { + "gzip": 10, + "uncompress": 10000, + }, + }, + "measurements": [ + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-05T00:00:00+00:00", + }, + { + "avg": 4126.0, + "max": 4126.0, + "min": 4126.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 14126.0, + "max": 14126.0, + "min": 14126.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": "asset-*.js", + "size": { + "loadTime": { + "highSpeed": 3, + "threeG": 150, + }, + "size": { + "gzip": 14, + "uncompress": 14126, + }, + }, + }, + { + "assetType": "ASSET_SIZE", + "change": { + "loadTime": { + "highSpeed": 2, + "threeG": 106, + }, + "size": { + "gzip": 10, + "uncompress": 10000, + }, + }, + "measurements": [ + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-05T00:00:00+00:00", + }, + { + "avg": 1421.0, + "max": 1421.0, + "min": 1421.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 11421.0, + "max": 11421.0, + "min": 11421.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": "asset-*.js", + "size": { + "loadTime": { + "highSpeed": 3, + "threeG": 121, + }, + "size": { + "gzip": 11, + "uncompress": 11421, + }, + }, + }, + { + "assetType": "ASSET_SIZE", + "change": None, + "measurements": [], + "name": "asset-*.js", + "size": None, + }, + { + "assetType": "FONT_SIZE", + "change": { + "loadTime": { + "highSpeed": 0, + "threeG": 2, + }, + "size": { + "gzip": 0, + "uncompress": 240, + }, + }, + "measurements": [ + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-05T00:00:00+00:00", + }, + { + "avg": 50.0, + "max": 50.0, + "min": 50.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 290.0, + "max": 290.0, + "min": 290.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 0, + "threeG": 3, + }, + "size": { + "gzip": 0, + "uncompress": 290, + }, + }, + }, + { + "assetType": "IMAGE_SIZE", + "change": { + "loadTime": { + "highSpeed": 0, + "threeG": 25, + }, + "size": { + "gzip": 2, + "uncompress": 2400, + }, + }, + "measurements": [ + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-05T00:00:00+00:00", + }, + { + "avg": 500.0, + "max": 500.0, + "min": 500.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 2900.0, + "max": 2900.0, + "min": 2900.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 0, + "threeG": 30, + }, + "size": { + "gzip": 2, + "uncompress": 2900, + }, + }, + }, + { + "assetType": "JAVASCRIPT_SIZE", + "change": { + "loadTime": { + "highSpeed": 5, + "threeG": 224, + }, + "size": { + "gzip": 21, + "uncompress": 21000, + }, + }, + "measurements": [ + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-05T00:00:00+00:00", + }, + { + "avg": 5708.0, + "max": 5708.0, + "min": 5708.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 26708.0, + "max": 26708.0, + "min": 26708.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 7, + "threeG": 284, + }, + "size": { + "gzip": 26, + "uncompress": 26708, + }, + }, + }, + { + "assetType": "REPORT_SIZE", + "change": { + "loadTime": { + "highSpeed": 6, + "threeG": 252, + }, + "size": { + "gzip": 23, + "uncompress": 23664, + }, + }, + "measurements": [ + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-05T00:00:00+00:00", + }, + { + "avg": 6263.0, + "max": 6263.0, + "min": 6263.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 29927.0, + "max": 29927.0, + "min": 29927.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 7, + "threeG": 319, + }, + "size": { + "gzip": 29, + "uncompress": 29927, + }, + }, + }, + { + "assetType": "STYLESHEET_SIZE", + "change": { + "loadTime": { + "highSpeed": 0, + "threeG": 0, + }, + "size": { + "gzip": 0, + "uncompress": 24, + }, + }, + "measurements": [ + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-05T00:00:00+00:00", + }, + { + "avg": 5.0, + "max": 5.0, + "min": 5.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 29.0, + "max": 29.0, + "min": 29.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 0, + "threeG": 0, + }, + "size": { + "gzip": 0, + "uncompress": 29, + }, + }, + }, + { + "assetType": "UNKNOWN_SIZE", + "change": { + "loadTime": { + "highSpeed": 0, + "threeG": 0, + }, + "size": { + "gzip": 0, + "uncompress": 0, + }, + }, + "measurements": [ + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-05T00:00:00+00:00", + }, + { + "avg": 0.0, + "max": 0.0, + "min": 0.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 0.0, + "max": 0.0, + "min": 0.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 0, + "threeG": 0, + }, + "size": { + "gzip": 0, + "uncompress": 0, + }, + }, + }, + ], + "name": "super", + }, + } + + # Test with using asset type filters + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "orderingDirection": "ASC", + "interval": "INTERVAL_1_DAY", + "after": "2024-06-05", + "before": "2024-06-10", + "filters": {"assetTypes": "JAVASCRIPT_SIZE"}, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "measurements": [ + { + "assetType": "JAVASCRIPT_SIZE", + "change": { + "loadTime": { + "highSpeed": 5, + "threeG": 224, + }, + "size": { + "gzip": 21, + "uncompress": 21000, + }, + }, + "measurements": [ + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-05T00:00:00+00:00", + }, + { + "avg": 5708.0, + "max": 5708.0, + "min": 5708.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 26708.0, + "max": 26708.0, + "min": 26708.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 7, + "threeG": 284, + }, + "size": { + "gzip": 26, + "uncompress": 26708, + }, + }, + }, + ], + "name": "super", + }, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_report_branch(self, get_storage_service): + measurements_data = [ + # 2024-06-10 + ["bundle_analysis_report_size", "super", "2024-06-10T19:07:23", 123], + # 2024-06-06 + ["bundle_analysis_report_size", "super", "2024-06-06T19:07:23", 456], + ] + + for item in measurements_data: + MeasurementFactory( + name=item[0], + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="feat", + measurable_id=item[1], + commit_sha=self.commit.pk, + timestamp=item[2], + value=item[3], + ) + + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + with open("./services/tests/samples/bundle_with_uuid.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=self.head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchMeasurements( + $org: String!, + $repo: String!, + $commit: String! + $filters: BundleAnalysisMeasurementsSetFilters + $orderingDirection: OrderingDirection! + $interval: MeasurementInterval! + $before: DateTime! + $after: DateTime! + $branch: String! + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "super") { + name + measurements( + filters: $filters + orderingDirection: $orderingDirection + after: $after + interval: $interval + before: $before + branch: $branch + ){ + assetType + name + size { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + change { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + measurements { + avg + min + max + timestamp + } + } + } + } + } + } + } + } + } + } + } + """ + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "orderingDirection": "ASC", + "interval": "INTERVAL_1_DAY", + "after": "2024-06-07", + "before": "2024-06-10", + "branch": "feat", + "filters": {}, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "name": "super", + "measurements": [ + { + "assetType": "ASSET_SIZE", + "name": "asset-*.js", + "size": None, + "change": None, + "measurements": [], + }, + { + "assetType": "ASSET_SIZE", + "name": "asset-*.js", + "size": None, + "change": None, + "measurements": [], + }, + { + "assetType": "ASSET_SIZE", + "name": "asset-*.js", + "size": None, + "change": None, + "measurements": [], + }, + { + "assetType": "FONT_SIZE", + "name": None, + "size": None, + "change": None, + "measurements": [], + }, + { + "assetType": "IMAGE_SIZE", + "name": None, + "size": None, + "change": None, + "measurements": [], + }, + { + "assetType": "JAVASCRIPT_SIZE", + "name": None, + "size": None, + "change": None, + "measurements": [], + }, + { + "assetType": "REPORT_SIZE", + "name": None, + "size": { + "loadTime": {"threeG": 1, "highSpeed": 0}, + "size": {"gzip": 0, "uncompress": 123}, + }, + "change": { + "loadTime": {"threeG": -3, "highSpeed": 0}, + "size": {"gzip": 0, "uncompress": -333}, + }, + "measurements": [ + { + "avg": 456.0, + "min": 456.0, + "max": 456.0, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "min": None, + "max": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "min": None, + "max": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 123.0, + "min": 123.0, + "max": 123.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + }, + { + "assetType": "STYLESHEET_SIZE", + "name": None, + "size": None, + "change": None, + "measurements": [], + }, + { + "assetType": "UNKNOWN_SIZE", + "change": { + "loadTime": { + "highSpeed": 0, + "threeG": -3, + }, + "size": { + "gzip": 0, + "uncompress": -333, + }, + }, + "measurements": [ + { + "avg": 456.0, + "max": 456.0, + "min": 456.0, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 123.0, + "max": 123.0, + "min": 123.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 0, + "threeG": 1, + }, + "size": { + "gzip": 0, + "uncompress": 123, + }, + }, + }, + ], + }, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_report_no_after(self, get_storage_service): + measurements_data = [ + # 2024-06-10 + ["bundle_analysis_report_size", "super", "2024-06-10T19:07:23", 123], + # 2024-06-06 + ["bundle_analysis_report_size", "super", "2024-06-06T19:07:23", 456], + ] + + for item in measurements_data: + MeasurementFactory( + name=item[0], + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="feat", + measurable_id=item[1], + commit_sha=self.commit.pk, + timestamp=item[2], + value=item[3], + ) + + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + with open("./services/tests/samples/bundle_with_uuid.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=self.head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchMeasurements( + $org: String!, + $repo: String!, + $commit: String! + $filters: BundleAnalysisMeasurementsSetFilters + $orderingDirection: OrderingDirection! + $interval: MeasurementInterval! + $before: DateTime! + $after: DateTime + $branch: String! + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "super") { + name + measurements( + filters: $filters + orderingDirection: $orderingDirection + after: $after + interval: $interval + before: $before + branch: $branch + ){ + assetType + name + size { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + change { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + measurements { + avg + min + max + timestamp + } + } + } + } + } + } + } + } + } + } + } + """ + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "orderingDirection": "ASC", + "interval": "INTERVAL_1_DAY", + "after": None, + "before": "2024-06-10", + "branch": "feat", + "filters": {}, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "name": "super", + "measurements": [ + { + "assetType": "ASSET_SIZE", + "name": "asset-*.js", + "size": None, + "change": None, + "measurements": [], + }, + { + "assetType": "ASSET_SIZE", + "name": "asset-*.js", + "size": None, + "change": None, + "measurements": [], + }, + { + "assetType": "ASSET_SIZE", + "name": "asset-*.js", + "size": None, + "change": None, + "measurements": [], + }, + { + "assetType": "FONT_SIZE", + "name": None, + "size": None, + "change": None, + "measurements": [], + }, + { + "assetType": "IMAGE_SIZE", + "name": None, + "size": None, + "change": None, + "measurements": [], + }, + { + "assetType": "JAVASCRIPT_SIZE", + "name": None, + "size": None, + "change": None, + "measurements": [], + }, + { + "assetType": "REPORT_SIZE", + "name": None, + "size": { + "loadTime": {"threeG": 1, "highSpeed": 0}, + "size": {"gzip": 0, "uncompress": 123}, + }, + "change": { + "loadTime": {"threeG": -3, "highSpeed": 0}, + "size": {"gzip": 0, "uncompress": -333}, + }, + "measurements": [ + { + "avg": 456.0, + "min": 456.0, + "max": 456.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "min": None, + "max": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "min": None, + "max": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "min": None, + "max": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 123.0, + "min": 123.0, + "max": 123.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + }, + { + "assetType": "STYLESHEET_SIZE", + "name": None, + "size": None, + "change": None, + "measurements": [], + }, + { + "assetType": "UNKNOWN_SIZE", + "change": { + "loadTime": { + "highSpeed": 0, + "threeG": -3, + }, + "size": { + "gzip": 0, + "uncompress": -333, + }, + }, + "measurements": [ + { + "avg": 456.0, + "max": 456.0, + "min": 456.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 123.0, + "max": 123.0, + "min": 123.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 0, + "threeG": 1, + }, + "size": { + "gzip": 0, + "uncompress": 123, + }, + }, + }, + ], + }, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_report_bad_data_check(self, get_storage_service): + measurements_data = [ + # 2024-06-10 + ["bundle_analysis_report_size", "super", "2024-06-10T19:07:23", 123], + # 2024-06-06 + ["bundle_analysis_image_size", "super", "2024-06-06T19:07:23", 456], + # 2024-06-05 + ["bundle_analysis_image_size", "super", "2024-06-05T19:07:23", 666], + ] + + for item in measurements_data: + MeasurementFactory( + name=item[0], + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="feat", + measurable_id=item[1], + commit_sha=self.commit.pk, + timestamp=item[2], + value=item[3], + ) + + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + with open("./services/tests/samples/bundle_with_uuid.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=self.head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchMeasurements( + $org: String!, + $repo: String!, + $commit: String! + $filters: BundleAnalysisMeasurementsSetFilters + $orderingDirection: OrderingDirection! + $interval: MeasurementInterval! + $before: DateTime! + $after: DateTime + $branch: String! + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "super") { + name + measurements( + filters: $filters + orderingDirection: $orderingDirection + after: $after + interval: $interval + before: $before + branch: $branch + ){ + assetType + name + size { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + change { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + measurements { + avg + min + max + timestamp + } + } + } + } + } + } + } + } + } + } + } + """ + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "orderingDirection": "ASC", + "interval": "INTERVAL_1_DAY", + "after": None, + "before": "2024-06-10", + "branch": "feat", + "filters": {}, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "measurements": [ + { + "assetType": "ASSET_SIZE", + "change": None, + "measurements": [], + "name": "asset-*.js", + "size": None, + }, + { + "assetType": "ASSET_SIZE", + "change": None, + "measurements": [], + "name": "asset-*.js", + "size": None, + }, + { + "assetType": "ASSET_SIZE", + "change": None, + "measurements": [], + "name": "asset-*.js", + "size": None, + }, + { + "assetType": "FONT_SIZE", + "change": None, + "measurements": [], + "name": None, + "size": None, + }, + { + "assetType": "IMAGE_SIZE", + "change": { + "loadTime": { + "highSpeed": 0, + "threeG": -2, + }, + "size": { + "gzip": 0, + "uncompress": -210, + }, + }, + "measurements": [ + { + "avg": 666.0, + "max": 666.0, + "min": 666.0, + "timestamp": "2024-06-05T00:00:00+00:00", + }, + { + "avg": 456.0, + "max": 456.0, + "min": 456.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 0, + "threeG": 4, + }, + "size": { + "gzip": 0, + "uncompress": 456, + }, + }, + }, + { + "assetType": "JAVASCRIPT_SIZE", + "change": None, + "measurements": [], + "name": None, + "size": None, + }, + { + "assetType": "REPORT_SIZE", + "change": None, + "measurements": [ + { + "avg": 123.0, + "max": 123.0, + "min": 123.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 0, + "threeG": 1, + }, + "size": { + "gzip": 0, + "uncompress": 123, + }, + }, + }, + { + "assetType": "STYLESHEET_SIZE", + "change": None, + "measurements": [], + "name": None, + "size": None, + }, + { + "assetType": "UNKNOWN_SIZE", + "change": None, + "measurements": [], + "name": None, + "size": None, + }, + ], + "name": "super", + }, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_report_measurements_only_unknown(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + with open("./services/tests/samples/bundle_with_uuid.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=self.head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchMeasurements( + $org: String!, + $repo: String!, + $commit: String! + $filters: BundleAnalysisMeasurementsSetFilters + $orderingDirection: OrderingDirection! + $interval: MeasurementInterval! + $before: DateTime! + $after: DateTime! + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "super") { + name + measurements( + filters: $filters + orderingDirection: $orderingDirection + after: $after + interval: $interval + before: $before + ){ + assetType + name + size { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + change { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + measurements { + avg + min + max + timestamp + } + } + } + } + } + } + } + } + } + } + } + """ + + # Test without using asset type filters + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "orderingDirection": "ASC", + "interval": "INTERVAL_1_DAY", + "after": "2024-06-06", + "before": "2024-06-10", + "filters": {"assetTypes": ["UNKNOWN_SIZE"]}, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "measurements": [ + { + "assetType": "UNKNOWN_SIZE", + "change": { + "loadTime": { + "highSpeed": 0, + "threeG": 0, + }, + "size": { + "gzip": 0, + "uncompress": 0, + }, + }, + "measurements": [ + { + "avg": 0.0, + "max": 0.0, + "min": 0.0, + "timestamp": "2024-06-06T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 0.0, + "max": 0.0, + "min": 0.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + "size": { + "loadTime": { + "highSpeed": 0, + "threeG": 0, + }, + "size": { + "gzip": 0, + "uncompress": 0, + }, + }, + }, + ], + "name": "super", + }, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_report_measurements_no_data_in_range(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + with open("./services/tests/samples/bundle_with_uuid.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=self.head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchMeasurements( + $org: String!, + $repo: String!, + $commit: String! + $filters: BundleAnalysisMeasurementsSetFilters + $orderingDirection: OrderingDirection! + $interval: MeasurementInterval! + $before: DateTime! + $after: DateTime! + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "super") { + name + measurements( + filters: $filters + orderingDirection: $orderingDirection + after: $after + interval: $interval + before: $before + ){ + assetType + name + measurements { + avg + min + max + timestamp + } + } + } + } + } + } + } + } + } + } + } + """ + + # Test without using asset type filters + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "orderingDirection": "ASC", + "interval": "INTERVAL_1_DAY", + "after": "2024-06-08", + "before": "2024-06-09", + "filters": {}, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "measurements": [ + { + "assetType": "ASSET_SIZE", + "measurements": [ + { + "avg": 4126.0, + "max": 4126.0, + "min": 4126.0, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + ], + "name": "asset-*.js", + }, + { + "assetType": "ASSET_SIZE", + "measurements": [], + "name": "asset-*.js", + }, + { + "assetType": "ASSET_SIZE", + "measurements": [], + "name": "asset-*.js", + }, + { + "assetType": "FONT_SIZE", + "measurements": [ + { + "avg": 50.0, + "max": 50.0, + "min": 50.0, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + ], + "name": None, + }, + { + "assetType": "IMAGE_SIZE", + "measurements": [ + { + "avg": 500.0, + "max": 500.0, + "min": 500.0, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + ], + "name": None, + }, + { + "assetType": "JAVASCRIPT_SIZE", + "measurements": [ + { + "avg": 5708.0, + "max": 5708.0, + "min": 5708.0, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + ], + "name": None, + }, + { + "assetType": "REPORT_SIZE", + "measurements": [ + { + "avg": 6263.0, + "max": 6263.0, + "min": 6263.0, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + ], + "name": None, + }, + { + "assetType": "STYLESHEET_SIZE", + "measurements": [ + { + "avg": 5.0, + "max": 5.0, + "min": 5.0, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + ], + "name": None, + }, + { + "assetType": "UNKNOWN_SIZE", + "measurements": [ + { + "avg": 0.0, + "max": 0.0, + "min": 0.0, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + ], + "name": None, + }, + ], + "name": "super", + }, + } + + # Test with using asset type filters + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "orderingDirection": "ASC", + "interval": "INTERVAL_1_DAY", + "after": "2024-06-07", + "before": "2024-06-10", + "filters": {"assetTypes": "JAVASCRIPT_SIZE"}, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "measurements": [ + { + "assetType": "JAVASCRIPT_SIZE", + "measurements": [ + { + "avg": 5708.0, + "max": 5708.0, + "min": 5708.0, + "timestamp": "2024-06-07T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-08T00:00:00+00:00", + }, + { + "avg": None, + "max": None, + "min": None, + "timestamp": "2024-06-09T00:00:00+00:00", + }, + { + "avg": 26708.0, + "max": 26708.0, + "min": 26708.0, + "timestamp": "2024-06-10T00:00:00+00:00", + }, + ], + "name": None, + }, + ], + "name": "super", + }, + } diff --git a/apps/codecov-api/graphql_api/tests/test_commit.py b/apps/codecov-api/graphql_api/tests/test_commit.py new file mode 100644 index 0000000000..9f9980d8f1 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_commit.py @@ -0,0 +1,3530 @@ +import asyncio +import hashlib +import os +from datetime import datetime, timedelta +from unittest.mock import AsyncMock, PropertyMock, patch + +import yaml +from django.test import TestCase +from shared.api_archive.archive import ArchiveService +from shared.bundle_analysis import StoragePaths +from shared.bundle_analysis.storage import get_bucket_name +from shared.django_apps.core.tests.factories import ( + CommitErrorFactory, + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.reports.types import LineSession +from shared.storage.memory import MemoryStorageService + +from compare.models import CommitComparison +from compare.tests.factories import CommitComparisonFactory +from graphql_api.types.enums import CommitStatus, UploadErrorEnum, UploadState +from graphql_api.types.enums.enums import UploadType +from reports.models import CommitReport +from reports.tests.factories import ( + CommitReportFactory, + ReportLevelTotalsFactory, + RepositoryFlagFactory, + UploadErrorFactory, + UploadFactory, + UploadFlagMembershipFactory, +) +from services.comparison import MissingComparisonReport +from services.components import Component + +from .helper import GraphQLTestHelper, paginate_connection + +query_commit = """ +query FetchCommit($org: String!, $repo: String!, $commit: String!) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + %s + } + } + } + } +} +""" + +query_commits = """ +query FetchCommits($org: String!, $repo: String!) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commits { + edges { + node { + %s + } + } + } + } + } + } +} +""" + + +class MockCoverage(object): + def __init__(self, cov): + self.coverage = cov + self.sessions = [ + LineSession(0, None), + LineSession(1, None), + LineSession(2, None), + ] + + +class MockLines(object): + def __init__(self): + self.lines = [ + [0, MockCoverage("1/2")], + [1, MockCoverage(1)], + [2, MockCoverage(0)], + ] + self.totals = MockCoverage(83) + + +class MockReport(object): + def get(self, filename): + MockLines() + return MockLines() + + def filter(self, **kwargs): + return self + + def get_flag_names(self): + return ["flag_a", "flag_b"] + + +class EmptyReport(MockReport): + def get(self, filename): + return None + + +class TestCommit(GraphQLTestHelper, TestCase): + def setUp(self): + asyncio.set_event_loop(asyncio.new_event_loop()) + self.org = OwnerFactory(username="codecov") + self.repo = RepositoryFactory(author=self.org, name="gazebo", private=False) + self.author = OwnerFactory() + self.parent_commit = CommitFactory(repository=self.repo) + self.commit = CommitFactory( + repository=self.repo, + totals={"c": "12", "diff": [0, 0, 0, 0, 0, "14"]}, + parent_commit_id=self.parent_commit.commitid, + ) + self.report = CommitReportFactory(commit=self.commit) + + # mock reports for all tests in this class + self.head_report_patcher = patch( + "services.comparison.Comparison.head_report", new_callable=PropertyMock + ) + self.head_report = self.head_report_patcher.start() + self.head_report.return_value = None + self.addCleanup(self.head_report_patcher.stop) + self.base_report_patcher = patch( + "services.comparison.Comparison.base_report", new_callable=PropertyMock + ) + self.base_report = self.base_report_patcher.start() + self.base_report.return_value = None + self.addCleanup(self.base_report_patcher.stop) + + def test_fetch_commit(self): + query = ( + query_commit + % """ + message, + createdAt, + commitid, + state, + author { username } + """ + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["commitid"] == self.commit.commitid + assert commit["message"] == self.commit.message + assert commit["author"]["username"] == self.commit.author.username + assert commit["state"] == self.commit.state + + def test_fetch_commits(self): + query = query_commits % "message,commitid,ciPassed" + self.repo_2 = RepositoryFactory( + author=self.org, name="test-repo", private=False + ) + + CommitFactory( + repository=self.repo_2, + commitid=123, + timestamp=datetime.today() - timedelta(days=3), + ) + CommitFactory( + repository=self.repo_2, + commitid=456, + timestamp=datetime.today() - timedelta(days=1), + ) + CommitFactory( + repository=self.repo_2, + commitid=789, + timestamp=datetime.today() - timedelta(days=2), + ) + + variables = {"org": self.org.username, "repo": self.repo_2.name} + data = self.gql_request(query, variables=variables) + commits = paginate_connection(data["owner"]["repository"]["commits"]) + commits_commitids = [commit["commitid"] for commit in commits] + assert commits_commitids == ["456", "789", "123"] + + def test_fetch_parent_commit(self): + query = query_commit % "parent { commitid } " + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["parent"]["commitid"] == self.parent_commit.commitid + + def test_resolve_commit_without_parent(self): + self.commit_without_parent = CommitFactory( + repository=self.repo, parent_commit_id=None + ) + query = query_commit % "parent { commitid } " + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit_without_parent.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["parent"] is None + + def test_fetch_commit_coverage(self): + ReportLevelTotalsFactory(report=self.report, coverage=12) + query = query_commit % "coverageAnalytics { totals { percentCovered }} " + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["coverageAnalytics"]["totals"]["percentCovered"] == 12 + + def test_fetch_commit_build(self): + session_one = UploadFactory(report=self.report, provider="circleci") + session_two = UploadFactory(report=self.report, provider="travisci") + query = query_commit % "uploads { edges { node { provider } } }" + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + builds = paginate_connection(commit["uploads"]) + assert builds == [ + {"provider": session_one.provider}, + {"provider": session_two.provider}, + ] + + def test_fetch_commit_uploads_state(self): + UploadFactory( + report=self.report, provider="circleci", state=UploadState.PROCESSED.value + ) + UploadFactory( + report=self.report, provider="travisci", state=UploadState.ERROR.value + ) + UploadFactory( + report=self.report, provider="travisci", state=UploadState.COMPLETE.value + ) + UploadFactory( + report=self.report, provider="travisci", state=UploadState.UPLOADED.value + ) + UploadFactory(report=self.report, provider="travisci", state="") + query = ( + query_commit + % """ + uploads { + edges { + node { + state + } + } + } + """ + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + uploads = paginate_connection(commit["uploads"]) + + assert uploads == [ + {"state": UploadState.PROCESSED.name}, + {"state": UploadState.ERROR.name}, + {"state": UploadState.COMPLETE.name}, + {"state": UploadState.UPLOADED.name}, + {"state": UploadState.ERROR.name}, + ] + + def test_fetch_commit_uploads(self): + flag_a = RepositoryFlagFactory(flag_name="flag_a") + flag_b = RepositoryFlagFactory(flag_name="flag_b") + flag_c = RepositoryFlagFactory(flag_name="flag_c") + flag_d = RepositoryFlagFactory(flag_name="flag_d") + + session_a = UploadFactory( + report=self.report, + upload_type=UploadType.UPLOADED.value, + order_number=0, + ) + UploadFlagMembershipFactory(report_session=session_a, flag=flag_a) + UploadFlagMembershipFactory(report_session=session_a, flag=flag_b) + UploadFlagMembershipFactory(report_session=session_a, flag=flag_c) + session_b = UploadFactory( + report=self.report, + upload_type=UploadType.CARRIEDFORWARD.value, + order_number=1, + ) + UploadFlagMembershipFactory(report_session=session_b, flag=flag_a) + + session_c = UploadFactory( + report=self.report, + upload_type=UploadType.CARRIEDFORWARD.value, + order_number=2, + ) + UploadFlagMembershipFactory(report_session=session_c, flag=flag_b) + session_d = UploadFactory( + report=self.report, + upload_type=UploadType.UPLOADED.value, + order_number=3, + ) + UploadFlagMembershipFactory(report_session=session_d, flag=flag_b) + + session_e = UploadFactory( + report=self.report, + upload_type=UploadType.CARRIEDFORWARD.value, + order_number=4, + ) + UploadFlagMembershipFactory(report_session=session_e, flag=flag_d) + UploadFactory( + report=self.report, + upload_type=UploadType.UPLOADED.value, + order_number=5, + ) + + query = ( + query_commit + % """ + uploads { + edges { + node { + id + uploadType + flags + } + } + } + """ + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + uploads = paginate_connection(commit["uploads"]) + + # ordered by upload id, omits uploads with carriedforward flag if another + # upload exists with the same flag name is is not carriedforward + + assert uploads == [ + { + "id": 0, + "uploadType": "UPLOADED", + "flags": ["flag_a", "flag_b", "flag_c"], + }, + # TEMP: we're returning ALL uploads for now while we figure out a more + # performant way to make this query + {"id": 1, "uploadType": "CARRIEDFORWARD", "flags": ["flag_a"]}, + {"id": 2, "uploadType": "CARRIEDFORWARD", "flags": ["flag_b"]}, + {"id": 3, "uploadType": "UPLOADED", "flags": ["flag_b"]}, + {"id": 4, "uploadType": "CARRIEDFORWARD", "flags": ["flag_d"]}, + {"id": 5, "uploadType": "UPLOADED", "flags": []}, + ] + + def test_fetch_commit_uploads_no_report(self): + commit = CommitFactory( + repository=self.repo, + parent_commit_id=self.commit.commitid, + ) + query = ( + query_commit + % """ + uploads { + edges { + node { + uploadType + flags + provider + } + } + } + """ + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + uploads = paginate_connection(commit["uploads"]) + assert uploads == [] + + def test_fetch_commit_uploads_errors(self): + session = UploadFactory( + report=self.report, provider="circleci", state=UploadState.ERROR.value + ) + UploadErrorFactory( + report_session=session, error_code=UploadErrorEnum.REPORT_EXPIRED.value + ) + UploadErrorFactory( + report_session=session, error_code=UploadErrorEnum.FILE_NOT_IN_STORAGE.value + ) + + query = ( + query_commit + % """ + uploads { + edges { + node { + errors { + edges { + node { + errorCode + } + } + } + } + } + } + """ + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + [upload] = paginate_connection(commit["uploads"]) + errors = paginate_connection(upload["errors"]) + + assert errors == [ + {"errorCode": UploadErrorEnum.REPORT_EXPIRED.name}, + {"errorCode": UploadErrorEnum.FILE_NOT_IN_STORAGE.name}, + ] + + def test_yaml_return_default_state_if_default(self): + org = OwnerFactory(username="default_yaml_owner") + repo = RepositoryFactory(author=org, private=False) + commit = CommitFactory(repository=repo) + query = ( + query_commit + % """ + yamlState + """ + ) + variables = { + "org": org.username, + "repo": repo.name, + "commit": commit.commitid, + } + data = self.gql_request(query, variables=variables) + assert data["owner"]["repository"]["commit"]["yamlState"] == "DEFAULT" + + def test_fetch_commit_ci(self): + UploadFactory( + report=self.report, + provider="circleci", + job_code=123, + build_code=456, + ) + query = query_commit % "uploads { edges { node { ciUrl } } }" + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + uploads = paginate_connection(commit["uploads"]) + assert uploads == [ + { + "ciUrl": "https://circleci.com/gh/codecov/gazebo/456#tests/containers/123", + } + ] + + def test_fetch_download_url(self): + upload = UploadFactory( + report=self.report, + storage_path="v4/raw/2022-06-23/942173DE95CBF167C5683F40B7DB34C0/ee3ecad424e67419d6c4531540f1ef5df045ff12/919ccc6d-7972-4895-b289-f2d569683a17.txt", + ) + query = query_commit % "uploads { edges { node { downloadUrl } } }" + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + uploads = paginate_connection(commit["uploads"]) + assert uploads == [ + { + "downloadUrl": f"https://testserver/upload/gh/{self.org.username}/{self.repo.name}/download?path={upload.storage_path}", + } + ] + + @patch( + "core.commands.commit.commit.CommitCommands.get_final_yaml", + new_callable=AsyncMock, + ) + def test_fetch_commit_yaml_call_the_command(self, command_mock): + query = query_commit % "yaml" + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + fake_config = {"codecov": "yes"} + command_mock.return_value = fake_config + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["yaml"] == yaml.dump(fake_config) + + @patch("core.commands.commit.commit.CommitCommands.get_file_content") + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_commit_coverage_file_call_the_command( + self, report_mock, content_mock + ): + query = ( + query_commit + % 'coverageAnalytics { coverageFile(path: "path") { hashedPath, content, coverage { line,coverage }, totals { percentCovered } } }' + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "path": "path", + } + fake_coverage = { + "content": "file content", + "coverage": [ + {"line": 0, "coverage": "P"}, + {"line": 1, "coverage": "H"}, + {"line": 2, "coverage": "M"}, + ], + "totals": {"percentCovered": 83.0}, + } + content_mock.return_value = "file content" + + report_mock.return_value = MockReport() + data = self.gql_request(query, variables=variables) + coverageFile = data["owner"]["repository"]["commit"]["coverageAnalytics"][ + "coverageFile" + ] + assert coverageFile["content"] == fake_coverage["content"] + assert coverageFile["coverage"] == fake_coverage["coverage"] + assert coverageFile["totals"] == fake_coverage["totals"] + assert coverageFile["hashedPath"] == hashlib.md5("path".encode()).hexdigest() + + @patch("services.components.component_filtered_report") + @patch("services.components.commit_components") + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_commit_coverage_file_with_components( + self, report_mock, commit_components_mock, filtered_mock + ): + components = ["Global"] + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "path": "path", + "components": components, + } + + report_mock.return_value = MockReport() + commit_components_mock.return_value = [ + Component.from_dict( + { + "component_id": "c1", + "name": "ComponentOne", + "paths": ["fileA.py"], + } + ), + Component.from_dict( + { + "component_id": "c2", + "name": "ComponentTwo", + "paths": ["fileB.py"], + } + ), + Component.from_dict( + { + "component_id": "global", + "name": "Global", + "paths": ["**/*.py"], + } + ), + ] + filtered_mock.return_value = MockReport() + + query_files = """ + query FetchCommit($org: String!, $repo: String!, $commit: String!, $components: [String!]!) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + coverageAnalytics { + coverageFile(path: "path", components: $components) { + hashedPath, content, coverage { line,coverage }, totals { percentCovered } + } + } + } + } + } + } + } + """ + + data = self.gql_request(query_files, variables=variables) + + assert data == { + "owner": { + "repository": { + "commit": { + "coverageAnalytics": { + "coverageFile": { + "content": None, + "coverage": [ + {"coverage": "P", "line": 0}, + {"coverage": "H", "line": 1}, + {"coverage": "M", "line": 2}, + ], + "hashedPath": "d6fe1d0be6347b8ef2427fa629c04485", + "totals": {"percentCovered": 83.0}, + } + } + } + } + } + } + + @patch("core.commands.commit.commit.CommitCommands.get_file_content") + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_commit_with_no_coverage_data( + self, + report_mock, + content_mock, + ): + query = ( + query_commit + % 'coverageAnalytics { coverageFile(path: "path") { content, coverage { line,coverage }, totals { percentCovered } } }' + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "path": "path", + } + fake_coverage = {"content": "file content", "coverage": [], "totals": None} + content_mock.return_value = "file content" + + report_mock.return_value = EmptyReport() + data = self.gql_request(query, variables=variables) + coverageFile = data["owner"]["repository"]["commit"]["coverageAnalytics"][ + "coverageFile" + ] + assert coverageFile["content"] == fake_coverage["content"] + assert coverageFile["coverage"] == fake_coverage["coverage"] + assert coverageFile["totals"] == fake_coverage["totals"] + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_flag_names(self, report_mock): + query = query_commit % "coverageAnalytics { flagNames }" + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "path": "path", + } + report_mock.return_value = MockReport() + data = self.gql_request(query, variables=variables) + flags = data["owner"]["repository"]["commit"]["coverageAnalytics"]["flagNames"] + assert flags == ["flag_a", "flag_b"] + + def test_fetch_commit_compare_call_the_command(self): + CommitComparisonFactory( + base_commit=self.parent_commit, + compare_commit=self.commit, + ) + query = query_commit % "compareWithParent { ... on Comparison { state } }" + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["compareWithParent"] == {"state": "pending"} + + def test_fetch_commit_compare_no_parent(self): + self.commit.parent_commit_id = None + self.commit.save() + + query = ( + query_commit + % """ + compareWithParent { __typename ... on Comparison { state } } + bundleAnalysis { + bundleAnalysisCompareWithParent { __typename ... on BundleAnalysisComparison { bundleData { size { uncompress } } } } + } + """ + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["compareWithParent"]["__typename"] == "MissingBaseCommit" + assert ( + commit["bundleAnalysis"]["bundleAnalysisCompareWithParent"]["__typename"] + == "MissingBaseCommit" + ) + + def test_compare_with_parent_comparison_missing_when_commit_comparison_state_is_errored( + self, + ): + CommitComparisonFactory( + base_commit=self.parent_commit, + compare_commit=self.commit, + state=CommitComparison.CommitComparisonStates.ERROR, + ) + query = ( + query_commit + % "compareWithParent { __typename ... on Comparison { state } }" + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["compareWithParent"]["__typename"] == "MissingComparison" + + def test_compare_with_parent_change_coverage(self): + CommitComparisonFactory( + base_commit=self.parent_commit, + compare_commit=self.commit, + state=CommitComparison.CommitComparisonStates.PROCESSED, + ) + ReportLevelTotalsFactory( + report=CommitReportFactory(commit=self.parent_commit), + coverage=75.0, + files=0, + lines=0, + hits=0, + misses=0, + partials=0, + branches=0, + methods=0, + ) + ReportLevelTotalsFactory( + report=self.report, + coverage=80.0, + files=0, + lines=0, + hits=0, + misses=0, + partials=0, + branches=0, + methods=0, + ) + + query = ( + query_commit % "compareWithParent { ... on Comparison { changeCoverage } }" + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["compareWithParent"]["changeCoverage"] == 5.0 + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_compare(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + base_commit_report = CommitReportFactory( + commit=self.parent_commit, + report_type=CommitReport.ReportType.BUNDLE_ANALYSIS, + ) + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + with open("./services/tests/samples/base_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=base_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + with open("./services/tests/samples/head_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = ( + query_commit + % """ + bundleAnalysis { + bundleAnalysisCompareWithParent { + __typename + ... on BundleAnalysisComparison { + bundles { + name + changeType + bundleData { + size { + uncompress + } + } + bundleChange { + size { + uncompress + } + } + } + bundleData { + size { + uncompress + } + } + bundleChange { + size { + uncompress + } + } + } + } + } + """ + ) + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["bundleAnalysis"]["bundleAnalysisCompareWithParent"] == { + "__typename": "BundleAnalysisComparison", + "bundles": [ + { + "name": "b1", + "changeType": "changed", + "bundleData": {"size": {"uncompress": 20}}, + "bundleChange": {"size": {"uncompress": 5}}, + }, + { + "name": "b2", + "changeType": "changed", + "bundleData": {"size": {"uncompress": 200}}, + "bundleChange": {"size": {"uncompress": 50}}, + }, + { + "name": "b3", + "changeType": "added", + "bundleData": {"size": {"uncompress": 1500}}, + "bundleChange": {"size": {"uncompress": 1500}}, + }, + { + "name": "b5", + "changeType": "changed", + "bundleData": {"size": {"uncompress": 200000}}, + "bundleChange": {"size": {"uncompress": 50000}}, + }, + { + "name": "b4", + "changeType": "removed", + "bundleData": {"size": {"uncompress": 0}}, + "bundleChange": {"size": {"uncompress": -15000}}, + }, + ], + "bundleData": {"size": {"uncompress": 201720}}, + "bundleChange": {"size": {"uncompress": 36555}}, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_compare_with_compare_sha(self, get_storage_service): + """ + This tests creates 3 commits C1 -> C2 -> C3 + C1 uses Report1, C2 and C3 uses Report2 + Normally when doing a compare of C3, it would select C2 as its parent + then it would show no change, as expected + However the difference is that in C3's Report2 it has the compareSha set to C1.commitid + Now when doing comparison of C3, it would now select C1 as the parent + therefore show correct comparison in numbers between Report1 and Report2 + """ + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + commit_1 = CommitFactory( + repository=self.repo, + commitid="6ca727b0142bf5625bb82af2555d308862063222", + ) + commit_2 = CommitFactory( + repository=self.repo, parent_commit_id=commit_1.commitid + ) + commit_3 = CommitFactory( + repository=self.repo, parent_commit_id=commit_2.commitid + ) + + commit_report_1 = CommitReportFactory( + commit=commit_1, + report_type=CommitReport.ReportType.BUNDLE_ANALYSIS, + ) + + commit_report_2 = CommitReportFactory( + commit=commit_2, + report_type=CommitReport.ReportType.BUNDLE_ANALYSIS, + ) + + commit_report_3 = CommitReportFactory( + commit=commit_3, + report_type=CommitReport.ReportType.BUNDLE_ANALYSIS, + ) + + with open("./services/tests/samples/base_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=commit_report_1.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + with open("./services/tests/samples/head_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=commit_report_2.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + with open( + "./services/tests/samples/head_bundle_report_with_compare_sha_6ca727b0142bf5625bb82af2555d308862063222.sqlite", + "rb", + ) as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=commit_report_3.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = ( + query_commit + % """ + bundleAnalysis { + bundleAnalysisCompareWithParent { + __typename + ... on BundleAnalysisComparison { + bundleData { + size { + uncompress + } + } + bundleChange { + size { + uncompress + } + } + } + } + } + """ + ) + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": commit_report_3.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["bundleAnalysis"]["bundleAnalysisCompareWithParent"] == { + "__typename": "BundleAnalysisComparison", + "bundleData": {"size": {"uncompress": 201720}}, + "bundleChange": {"size": {"uncompress": 36555}}, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_sqlite_file_deleted(self, get_storage_service): + os.system("rm -rf /tmp/bundle_analysis_*") + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + base_commit_report = CommitReportFactory( + commit=self.parent_commit, + report_type=CommitReport.ReportType.BUNDLE_ANALYSIS, + ) + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + with open("./services/tests/samples/base_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=base_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + with open("./services/tests/samples/head_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = ( + query_commit + % """ + bundleAnalysis { + bundleAnalysisCompareWithParent { + __typename + ... on BundleAnalysisComparison { + bundleData { + size { + uncompress + } + } + } + } + } + """ + ) + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + self.gql_request(query, variables=variables) + + for file in os.listdir("/tmp"): + assert not file.startswith("bundle_analysis_") + os.system("rm -rf /tmp/bundle_analysis_*") + + @patch("graphql_api.views.os.unlink") + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_sqlite_file_not_deleted( + self, get_storage_service, os_unlink_mock + ): + os.system("rm -rf /tmp/bundle_analysis_*") + os_unlink_mock.side_effect = Exception("something went wrong") + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + base_commit_report = CommitReportFactory( + commit=self.parent_commit, + report_type=CommitReport.ReportType.BUNDLE_ANALYSIS, + ) + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + with open("./services/tests/samples/base_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=base_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + with open("./services/tests/samples/head_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = ( + query_commit + % """ + bundleAnalysis { + bundleAnalysisCompareWithParent { + __typename + ... on BundleAnalysisComparison { + bundleData { + size { + uncompress + } + } + } + } + } + """ + ) + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + self.gql_request(query, variables=variables) + + found = False + for file in os.listdir("/tmp"): + if file.startswith("bundle_analysis_"): + found = True + break + assert found + os.system("rm -rf /tmp/bundle_analysis_*") + + def test_bundle_analysis_missing_report(self): + query = ( + query_commit + % """ + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on MissingHeadReport { + message + } + } + } + """ + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "MissingHeadReport", + "message": "Missing head report", + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_report(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + with open("./services/tests/samples/head_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchCommit($org: String!, $repo: String!, $commit: String!) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundles { + name + assets { + normalizedName + } + asset(name: "not_exist") { + normalizedName + } + bundleData { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + isCached + cacheConfig + } + bundleData { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + bundle(name: "not_exist") { + name + isCached + } + isCached + } + ... on MissingHeadReport { + message + } + } + } + } + } + } + } + } + """ + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundles": [ + { + "name": "b1", + "assets": [ + {"normalizedName": "assets/index-*.js"}, + {"normalizedName": "assets/index-*.js"}, + {"normalizedName": "assets/LazyComponent-*.js"}, + {"normalizedName": "assets/index-*.css"}, + {"normalizedName": "assets/react-*.svg"}, + ], + "asset": None, + "bundleData": { + "loadTime": { + "threeG": 0, + "highSpeed": 0, + }, + "size": { + "gzip": 0, + "uncompress": 20, + }, + }, + "isCached": False, + "cacheConfig": False, + }, + { + "name": "b2", + "assets": [ + {"normalizedName": "assets/index-*.js"}, + {"normalizedName": "assets/index-*.js"}, + {"normalizedName": "assets/LazyComponent-*.js"}, + {"normalizedName": "assets/index-*.css"}, + {"normalizedName": "assets/react-*.svg"}, + ], + "asset": None, + "bundleData": { + "loadTime": { + "threeG": 2, + "highSpeed": 0, + }, + "size": { + "gzip": 0, + "uncompress": 200, + }, + }, + "isCached": False, + "cacheConfig": False, + }, + { + "name": "b3", + "assets": [ + {"normalizedName": "assets/index-*.js"}, + {"normalizedName": "assets/index-*.js"}, + {"normalizedName": "assets/LazyComponent-*.js"}, + {"normalizedName": "assets/index-*.css"}, + {"normalizedName": "assets/react-*.svg"}, + ], + "asset": None, + "bundleData": { + "loadTime": { + "threeG": 16, + "highSpeed": 0, + }, + "size": { + "gzip": 0, + "uncompress": 1500, + }, + }, + "isCached": False, + "cacheConfig": False, + }, + { + "name": "b5", + "assets": [ + {"normalizedName": "assets/index-*.js"}, + {"normalizedName": "assets/index-*.js"}, + {"normalizedName": "assets/LazyComponent-*.js"}, + {"normalizedName": "assets/index-*.css"}, + {"normalizedName": "assets/react-*.svg"}, + ], + "asset": None, + "bundleData": { + "loadTime": { + "threeG": 2133, + "highSpeed": 53, + }, + "size": { + "gzip": 200, + "uncompress": 200000, + }, + }, + "isCached": False, + "cacheConfig": False, + }, + ], + "bundleData": { + "loadTime": { + "threeG": 2151, + "highSpeed": 53, + }, + "size": { + "gzip": 201, + "uncompress": 201720, + }, + }, + "bundle": None, + "isCached": False, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_report_assets_paginated_first_after( + self, get_storage_service + ): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + with open("./services/tests/samples/head_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchCommit( + $org: String!, + $repo: String!, + $commit: String!, + $ordering: AssetOrdering, + $orderingDirection: OrderingDirection + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "b1") { + assetsPaginated ( + ordering: $ordering, + orderingDirection: $orderingDirection, + first: 2, + after: "5", + ){ + totalCount + edges { + cursor + node { + normalizedName + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } + } + } + } + } + } + } + } + } + """ + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "ordering": "NAME", + "orderingDirection": "ASC", + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "assetsPaginated": { + "totalCount": 5, + "edges": [ + { + "cursor": "4", + "node": { + "normalizedName": "assets/index-*.js", + }, + }, + { + "cursor": "2", + "node": { + "normalizedName": "assets/index-*.css", + }, + }, + ], + "pageInfo": { + "hasNextPage": True, + "hasPreviousPage": False, + "startCursor": "4", + "endCursor": "2", + }, + } + }, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_report_assets_paginated_first_after_non_existing_cursor( + self, get_storage_service + ): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + with open("./services/tests/samples/head_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchCommit( + $org: String!, + $repo: String!, + $commit: String!, + $ordering: AssetOrdering, + $orderingDirection: OrderingDirection + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "b1") { + assetsPaginated ( + ordering: $ordering, + orderingDirection: $orderingDirection, + first: 2, + after: "notanumber", + ){ + totalCount + edges { + cursor + node { + normalizedName + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } + } + } + } + } + } + } + } + } + """ + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "ordering": "NAME", + "orderingDirection": "ASC", + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "assetsPaginated": { + "totalCount": 5, + "edges": [ + { + "cursor": "3", + "node": { + "normalizedName": "assets/LazyComponent-*.js", + }, + }, + { + "cursor": "5", + "node": { + "normalizedName": "assets/index-*.js", + }, + }, + ], + "pageInfo": { + "hasNextPage": True, + "hasPreviousPage": False, + "startCursor": "3", + "endCursor": "5", + }, + } + }, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_report_assets_paginated_last_before( + self, get_storage_service + ): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + with open("./services/tests/samples/head_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchCommit( + $org: String!, + $repo: String!, + $commit: String!, + $ordering: AssetOrdering, + $orderingDirection: OrderingDirection + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "b1") { + assetsPaginated ( + ordering: $ordering, + orderingDirection: $orderingDirection, + last: 2, + before: "1", + ){ + totalCount + edges { + cursor + node { + normalizedName + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } + } + } + } + } + } + } + } + } + """ + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "ordering": "NAME", + "orderingDirection": "ASC", + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "assetsPaginated": { + "totalCount": 5, + "edges": [ + { + "cursor": "4", + "node": { + "normalizedName": "assets/index-*.js", + }, + }, + { + "cursor": "2", + "node": { + "normalizedName": "assets/index-*.css", + }, + }, + ], + "pageInfo": { + "hasNextPage": False, + "hasPreviousPage": True, + "startCursor": "4", + "endCursor": "2", + }, + } + }, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_report_assets_paginated_last_before_non_existing_cursor( + self, get_storage_service + ): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + with open("./services/tests/samples/head_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchCommit( + $org: String!, + $repo: String!, + $commit: String!, + $ordering: AssetOrdering, + $orderingDirection: OrderingDirection + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "b1") { + assetsPaginated ( + ordering: $ordering, + orderingDirection: $orderingDirection, + last: 2, + before: "99999", + ){ + totalCount + edges { + cursor + node { + normalizedName + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } + } + } + } + } + } + } + } + } + """ + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "ordering": "NAME", + "orderingDirection": "ASC", + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "assetsPaginated": { + "totalCount": 5, + "edges": [ + { + "cursor": "2", + "node": { + "normalizedName": "assets/index-*.css", + }, + }, + { + "cursor": "1", + "node": { + "normalizedName": "assets/react-*.svg", + }, + }, + ], + "pageInfo": { + "hasNextPage": False, + "hasPreviousPage": True, + "startCursor": "2", + "endCursor": "1", + }, + } + }, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_report_assets_paginated_before_and_after_error( + self, get_storage_service + ): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + with open("./services/tests/samples/head_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchCommit( + $org: String!, + $repo: String!, + $commit: String!, + $ordering: AssetOrdering, + $orderingDirection: OrderingDirection + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "b1") { + assetsPaginated ( + ordering: $ordering, + orderingDirection: $orderingDirection, + before: "1", + after: "2", + ){ + totalCount + } + } + } + } + } + } + } + } + } + } + """ + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, with_errors=True, variables=variables) + commit = data["data"]["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": {"assetsPaginated": None}, + } + + assert ( + data["errors"][0]["message"] + == "After and before can not be used at the same time" + ) + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_report_assets_paginated_first_and_last_error( + self, get_storage_service + ): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + with open("./services/tests/samples/head_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchCommit( + $org: String!, + $repo: String!, + $commit: String!, + $ordering: AssetOrdering, + $orderingDirection: OrderingDirection + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "b1") { + assetsPaginated ( + ordering: $ordering, + orderingDirection: $orderingDirection, + first: 1, + last: 2, + ){ + totalCount + } + } + } + } + } + } + } + } + } + } + """ + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, with_errors=True, variables=variables) + commit = data["data"]["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": {"assetsPaginated": None}, + } + + assert ( + data["errors"][0]["message"] + == "First and last can not be used at the same time" + ) + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_asset(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + with open( + "./services/tests/samples/bundle_with_assets_and_modules.sqlite", "rb" + ) as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchCommit($org: String!, $repo: String!, $commit: String!) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "b5") { + moduleCount + asset(name: "assets/LazyComponent-fcbb0922.js") { + name + normalizedName + extension + bundleData { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + modules { + name + bundleData { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + } + } + } + } + } + } + } + } + } + } + } + """ + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + bundle_report = commit["bundleAnalysis"]["bundleAnalysisReport"]["bundle"] + asset_report = bundle_report["asset"] + + assert bundle_report is not None + assert bundle_report["moduleCount"] == 33 + + assert asset_report is not None + assert asset_report["name"] == "assets/LazyComponent-fcbb0922.js" + assert asset_report["normalizedName"] == "assets/LazyComponent-*.js" + assert asset_report["extension"] == "js" + assert asset_report["bundleData"] == { + "loadTime": { + "threeG": 320, + "highSpeed": 8, + }, + "size": { + "gzip": 30, + "uncompress": 30000, + }, + } + + modules = sorted(asset_report["modules"], key=lambda m: m["name"]) + + assert modules and len(modules) == 3 + assert modules[0] == { + "name": "./src/LazyComponent/LazyComponent", + "bundleData": { + "loadTime": { + "threeG": 64, + "highSpeed": 1, + }, + "size": { + "gzip": 6, + "uncompress": 6000, + }, + }, + } + assert modules[1] == { + "name": "./src/LazyComponent/LazyComponent.tsx", + "bundleData": { + "loadTime": { + "threeG": 53, + "highSpeed": 1, + }, + "size": { + "gzip": 5, + "uncompress": 5000, + }, + }, + } + assert modules[2] == { + "name": "./src/LazyComponent/LazyComponent.tsx?module", + "bundleData": { + "loadTime": { + "threeG": 53, + "highSpeed": 1, + }, + "size": { + "gzip": 4, + "uncompress": 4970, + }, + }, + } + + @patch("shared.bundle_analysis.BundleReport.asset_reports") + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_asset_filtering( + self, get_storage_service, asset_reports_mock + ): + storage = MemoryStorageService({}) + + get_storage_service.return_value = storage + asset_reports_mock.return_value = [] + + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + with open( + "./services/tests/samples/bundle_with_assets_and_modules.sqlite", "rb" + ) as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchCommit($org: String!, $repo: String!, $commit: String!, $filters: BundleAnalysisReportFilters) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "b5", filters: $filters) { + moduleCount + assets { + name + } + } + } + } + } + } + } + } + } + } + """ + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "filters": {}, + } + + configurations = [ + # No filters + ( + {"loadTypes": None, "reportGroups": None}, + {"asset_types": None, "chunk_entry": None, "chunk_initial": None}, + ), + ({}, {"asset_types": None, "chunk_entry": None, "chunk_initial": None}), + # Just report groups + ( + {"reportGroups": ["JAVASCRIPT", "FONT"]}, + { + "asset_types": ["JAVASCRIPT", "FONT"], + "chunk_entry": None, + "chunk_initial": None, + }, + ), + # Load types -> chunk_entry cancels out + ( + {"loadTypes": ["ENTRY", "INITIAL"]}, + {"asset_types": None, "chunk_entry": None, "chunk_initial": True}, + ), + # Load types -> chunk_entry = True + ( + {"loadTypes": ["ENTRY"]}, + {"asset_types": None, "chunk_entry": True, "chunk_initial": None}, + ), + # Load types -> chunk_lazy = False + ( + {"loadTypes": ["LAZY"]}, + {"asset_types": None, "chunk_entry": False, "chunk_initial": False}, + ), + # Load types -> chunk_initial cancels out + ( + {"loadTypes": ["LAZY", "INITIAL"]}, + {"asset_types": None, "chunk_entry": False, "chunk_initial": None}, + ), + # Load types -> chunk_initial = True + ( + {"loadTypes": ["INITIAL"]}, + {"asset_types": None, "chunk_entry": False, "chunk_initial": True}, + ), + # Load types -> chunk_initial = False + ( + {"loadTypes": ["LAZY"]}, + {"asset_types": None, "chunk_entry": False, "chunk_initial": False}, + ), + ] + + for config in configurations: + input_d, output_d = config + variables["filters"] = input_d + data = self.gql_request(query, variables=variables) + assert ( + data["owner"]["repository"]["commit"]["bundleAnalysis"][ + "bundleAnalysisReport" + ]["bundle"] + is not None + ) + asset_reports_mock.assert_called_with(**output_d) + + def test_compare_with_parent_missing_change_coverage(self): + CommitComparisonFactory( + base_commit=self.parent_commit, + compare_commit=self.commit, + state=CommitComparison.CommitComparisonStates.PROCESSED, + ) + ReportLevelTotalsFactory( + report=CommitReportFactory(commit=self.parent_commit), + coverage=75.0, + files=0, + lines=0, + hits=0, + misses=0, + partials=0, + branches=0, + methods=0, + ) + + query = ( + query_commit % "compareWithParent { ... on Comparison { changeCoverage } }" + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["compareWithParent"]["changeCoverage"] is None + + def test_commit_yaml_errors(self): + CommitErrorFactory(commit=self.commit, error_code="invalid_yaml") + CommitErrorFactory(commit=self.commit, error_code="yaml_client_error") + query = ( + query_commit + % "errors(errorType: YAML_ERROR) { edges { node { errorCode } } }" + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + errors = paginate_connection(commit["errors"]) + assert errors == [ + {"errorCode": "invalid_yaml"}, + {"errorCode": "yaml_client_error"}, + ] + + def test_commit_bot_errors(self): + CommitErrorFactory(commit=self.commit, error_code="repo_bot_invalid") + CommitErrorFactory(commit=self.commit, error_code="repo_bot_invalid") + query = ( + query_commit + % "errors(errorType: BOT_ERROR) { edges { node { errorCode } } }" + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + errors = paginate_connection(commit["errors"]) + assert errors == [ + {"errorCode": "repo_bot_invalid"}, + {"errorCode": "repo_bot_invalid"}, + ] + + def test_fetch_upload_name(self): + UploadFactory( + name="First Upload", + report=self.report, + job_code=123, + build_code=456, + ) + query = query_commit % "uploads { edges { node { name } } }" + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + uploads = paginate_connection(commit["uploads"]) + assert uploads == [ + { + "name": "First Upload", + } + ] + + @patch("services.comparison.Comparison.validate") + def test_has_different_number_of_head_and_base_reports_with_invalid_comparison( + self, mock_compare_validate + ): + CommitComparisonFactory( + base_commit=self.parent_commit, + compare_commit=self.commit, + state=CommitComparison.CommitComparisonStates.PROCESSED, + ) + mock_compare_validate.side_effect = MissingComparisonReport() + query = ( + query_commit + % "compareWithParent { ... on Comparison { hasDifferentNumberOfHeadAndBaseReports } }" + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + assert data == { + "owner": { + "repository": { + "commit": { + "compareWithParent": { + "hasDifferentNumberOfHeadAndBaseReports": False + } + } + } + } + } + + def test_fetch_upload_name_is_none(self): + UploadFactory( + report=self.report, + job_code=123, + build_code=456, + ) + query = query_commit % "uploads { edges { node { name } } }" + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + uploads = paginate_connection(commit["uploads"]) + assert uploads == [ + { + "name": None, + } + ] + + def test_fetch_uploads_number(self): + for i in range(25): + UploadFactory( + report=self.report, + job_code=123, + build_code=456, + ) + query = query_commit % "totalUploads" + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + assert data["owner"]["repository"]["commit"]["totalUploads"] == 25 + + def test_fetch_all_uploads_is_the_default(self): + for i in range(100): + UploadFactory( + report=self.report, + job_code=123, + build_code=456, + ) + query = query_commit % "uploads { edges { node { state } } }" + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + assert len(data["owner"]["repository"]["commit"]["uploads"]["edges"]) == 100 + + def test_fetch_paginated_uploads(self): + for i in range(99): + UploadFactory( + report=self.report, + job_code=123, + build_code=456, + ) + query = ( + query_commit + % "totalUploads, uploads(first: 25) { edges { node { state } } }" + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + assert (data["owner"]["repository"]["commit"]["totalUploads"]) == 99 + assert len(data["owner"]["repository"]["commit"]["uploads"]["edges"]) == 25 + + def test_fetch_commit_status_no_reports(self): + query = ( + query_commit + % """ + coverageStatus + bundleStatus + """ + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["coverageStatus"] is None + assert commit["bundleStatus"] is None + + def test_fetch_commit_status_no_sessions(self): + CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.COVERAGE + ) + CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + query = ( + query_commit + % """ + coverageStatus + bundleStatus + """ + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["coverageStatus"] is None + assert commit["bundleStatus"] is None + + def test_fetch_commit_status_completed(self): + coverage_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.COVERAGE + ) + UploadFactory(report=coverage_report, state="processed") + UploadFactory(report=coverage_report, state="processed") + UploadFactory(report=coverage_report, state="fully_overwritten") + UploadFactory(report=coverage_report, state="partially_overwritten") + + ba_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + UploadFactory(report=ba_report, state="processed") + UploadFactory(report=ba_report, state="processed") + UploadFactory(report=ba_report, state="fully_overwritten") + UploadFactory(report=ba_report, state="partially_overwritten") + + query = ( + query_commit + % """ + coverageStatus + bundleStatus + """ + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["coverageStatus"] == CommitStatus.COMPLETED.value + assert commit["bundleStatus"] == CommitStatus.COMPLETED.value + + def test_fetch_commit_status_error(self): + coverage_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.COVERAGE + ) + UploadFactory(report=coverage_report, state="processed") + UploadFactory(report=coverage_report, state="error") + UploadFactory(report=coverage_report, state="uploaded") + UploadFactory(report=coverage_report, state="partially_overwritten") + + ba_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + UploadFactory(report=ba_report, state="processed") + UploadFactory(report=ba_report, state="error") + UploadFactory(report=ba_report, state="uploaded") + UploadFactory(report=ba_report, state="partially_overwritten") + + query = ( + query_commit + % """ + coverageStatus + bundleStatus + """ + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["coverageStatus"] == CommitStatus.ERROR.value + assert commit["bundleStatus"] == CommitStatus.ERROR.value + + def test_fetch_commit_status_pending(self): + coverage_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.COVERAGE + ) + UploadFactory(report=coverage_report, state="processed") + UploadFactory(report=coverage_report, state="processed") + UploadFactory(report=coverage_report, state="uploaded") + UploadFactory(report=coverage_report, state="partially_overwritten") + + ba_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + UploadFactory(report=ba_report, state="processed") + UploadFactory(report=ba_report, state="processed") + UploadFactory(report=ba_report, state="uploaded") + UploadFactory(report=ba_report, state="partially_overwritten") + + query = ( + query_commit + % """ + coverageStatus + bundleStatus + """ + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["coverageStatus"] == CommitStatus.PENDING.value + assert commit["bundleStatus"] == CommitStatus.PENDING.value + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_report_gzip_size_total(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + with open( + "./services/tests/samples/head_bundle_report_with_gzip_size.sqlite", "rb" + ) as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchCommit($org: String!, $repo: String!, $commit: String!) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundles { + name + bundleData { + size { + gzip + uncompress + } + } + } + } + } + } + } + } + } + } + } + """ + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundles": [ + { + # All assets non compressible + "name": "b1", + "bundleData": { + "size": { + "gzip": 20, + "uncompress": 20, + }, + }, + }, + { + # Some assets non compressible + "name": "b2", + "bundleData": { + "size": { + "gzip": 198, + "uncompress": 200, + }, + }, + }, + { + # All assets non compressible + "name": "b3", + "bundleData": { + "size": { + "gzip": 1495, + "uncompress": 1500, + }, + }, + }, + { + # All assets non compressible + "name": "b5", + "bundleData": { + "size": { + "gzip": 199995, + "uncompress": 200000, + }, + }, + }, + ], + } + + def test_coverage_totals(self): + ReportLevelTotalsFactory(report=self.report, coverage=12) + query = query_commit % "coverageAnalytics { totals { percentCovered } } " + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["coverageAnalytics"]["totals"]["percentCovered"] == 12 + + @patch("core.commands.commit.commit.CommitCommands.get_file_content") + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_commit_coverage_coverage_file( + self, + report_mock, + content_mock, + ): + query = ( + query_commit + % 'coverageAnalytics { coverageFile(path: "path") { hashedPath, content, coverage { line,coverage }, totals { percentCovered } } }' + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "path": "path", + } + fake_coverage = { + "content": "file content", + "coverage": [ + {"line": 0, "coverage": "P"}, + {"line": 1, "coverage": "H"}, + {"line": 2, "coverage": "M"}, + ], + "totals": {"percentCovered": 83.0}, + } + content_mock.return_value = "file content" + + report_mock.return_value = MockReport() + data = self.gql_request(query, variables=variables) + coverageFile = data["owner"]["repository"]["commit"]["coverageAnalytics"][ + "coverageFile" + ] + assert coverageFile["content"] == fake_coverage["content"] + assert coverageFile["coverage"] == fake_coverage["coverage"] + assert coverageFile["totals"] == fake_coverage["totals"] + assert coverageFile["hashedPath"] == hashlib.md5("path".encode()).hexdigest() + + @patch("services.components.component_filtered_report") + @patch("services.components.commit_components") + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_commit_coverage_coverage_file_with_components( + self, report_mock, commit_components_mock, filtered_mock + ): + components = ["Global"] + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "path": "path", + "components": components, + } + + report_mock.return_value = MockReport() + commit_components_mock.return_value = [ + Component.from_dict( + { + "component_id": "c1", + "name": "ComponentOne", + "paths": ["fileA.py"], + } + ), + Component.from_dict( + { + "component_id": "c2", + "name": "ComponentTwo", + "paths": ["fileB.py"], + } + ), + Component.from_dict( + { + "component_id": "global", + "name": "Global", + "paths": ["**/*.py"], + } + ), + ] + filtered_mock.return_value = MockReport() + + query_files = """ + query FetchCommit($org: String!, $repo: String!, $commit: String!, $components: [String!]!) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + coverageAnalytics { + coverageFile(path: "path", components: $components) { + hashedPath, content, coverage { line,coverage }, totals { percentCovered } + } + } + } + } + } + } + } + """ + + data = self.gql_request(query_files, variables=variables) + + assert data == { + "owner": { + "repository": { + "commit": { + "coverageAnalytics": { + "coverageFile": { + "content": None, + "coverage": [ + {"coverage": "P", "line": 0}, + {"coverage": "H", "line": 1}, + {"coverage": "M", "line": 2}, + ], + "hashedPath": "d6fe1d0be6347b8ef2427fa629c04485", + "totals": {"percentCovered": 83.0}, + } + } + } + } + } + } + + @patch("core.commands.commit.commit.CommitCommands.get_file_content") + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_fetch_commit_coverage_with_no_coverage_data( + self, + report_mock, + content_mock, + ): + query = ( + query_commit + % 'coverageAnalytics { coverageFile(path: "path") { content, coverage { line,coverage }, totals { percentCovered } }}' + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "path": "path", + } + fake_coverage = {"content": "file content", "coverage": [], "totals": None} + content_mock.return_value = "file content" + + report_mock.return_value = EmptyReport() + data = self.gql_request(query, variables=variables) + coverageFile = data["owner"]["repository"]["commit"]["coverageAnalytics"][ + "coverageFile" + ] + assert coverageFile["content"] == fake_coverage["content"] + assert coverageFile["coverage"] == fake_coverage["coverage"] + assert coverageFile["totals"] == fake_coverage["totals"] + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_coverage_flag_names(self, report_mock): + query = query_commit % "coverageAnalytics { flagNames }" + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "path": "path", + } + report_mock.return_value = MockReport() + data = self.gql_request(query, variables=variables) + flags = data["owner"]["repository"]["commit"]["coverageAnalytics"]["flagNames"] + assert flags == ["flag_a", "flag_b"] + + def test_coverage_bundle_analysis_missing_report(self): + query = ( + query_commit + % """ + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on MissingHeadReport { + message + } + } + } + """ + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "MissingHeadReport", + "message": "Missing head report", + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_coverage_bundle_analysis_report(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + with open("./services/tests/samples/head_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchCommit($org: String!, $repo: String!, $commit: String!) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundles { + name + assets { + normalizedName + } + asset(name: "not_exist") { + normalizedName + } + bundleData { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + isCached + } + bundleData { + loadTime { + threeG + highSpeed + } + size { + gzip + uncompress + } + } + bundle(name: "not_exist") { + name + isCached + } + isCached + } + ... on MissingHeadReport { + message + } + } + } + } + } + } + } + } + """ + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundles": [ + { + "name": "b1", + "assets": [ + {"normalizedName": "assets/index-*.js"}, + {"normalizedName": "assets/index-*.js"}, + {"normalizedName": "assets/LazyComponent-*.js"}, + {"normalizedName": "assets/index-*.css"}, + {"normalizedName": "assets/react-*.svg"}, + ], + "asset": None, + "bundleData": { + "loadTime": { + "threeG": 0, + "highSpeed": 0, + }, + "size": { + "gzip": 0, + "uncompress": 20, + }, + }, + "isCached": False, + }, + { + "name": "b2", + "assets": [ + {"normalizedName": "assets/index-*.js"}, + {"normalizedName": "assets/index-*.js"}, + {"normalizedName": "assets/LazyComponent-*.js"}, + {"normalizedName": "assets/index-*.css"}, + {"normalizedName": "assets/react-*.svg"}, + ], + "asset": None, + "bundleData": { + "loadTime": { + "threeG": 2, + "highSpeed": 0, + }, + "size": { + "gzip": 0, + "uncompress": 200, + }, + }, + "isCached": False, + }, + { + "name": "b3", + "assets": [ + {"normalizedName": "assets/index-*.js"}, + {"normalizedName": "assets/index-*.js"}, + {"normalizedName": "assets/LazyComponent-*.js"}, + {"normalizedName": "assets/index-*.css"}, + {"normalizedName": "assets/react-*.svg"}, + ], + "asset": None, + "bundleData": { + "loadTime": { + "threeG": 16, + "highSpeed": 0, + }, + "size": { + "gzip": 0, + "uncompress": 1500, + }, + }, + "isCached": False, + }, + { + "name": "b5", + "assets": [ + {"normalizedName": "assets/index-*.js"}, + {"normalizedName": "assets/index-*.js"}, + {"normalizedName": "assets/LazyComponent-*.js"}, + {"normalizedName": "assets/index-*.css"}, + {"normalizedName": "assets/react-*.svg"}, + ], + "asset": None, + "bundleData": { + "loadTime": { + "threeG": 2133, + "highSpeed": 53, + }, + "size": { + "gzip": 200, + "uncompress": 200000, + }, + }, + "isCached": False, + }, + ], + "bundleData": { + "loadTime": { + "threeG": 2151, + "highSpeed": 53, + }, + "size": { + "gzip": 201, + "uncompress": 201720, + }, + }, + "bundle": None, + "isCached": False, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_coverage_bundle_analysis_compare(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + base_commit_report = CommitReportFactory( + commit=self.parent_commit, + report_type=CommitReport.ReportType.BUNDLE_ANALYSIS, + ) + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + with open("./services/tests/samples/base_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=base_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + with open("./services/tests/samples/head_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = ( + query_commit + % """ + bundleAnalysis { + bundleAnalysisCompareWithParent { + __typename + ... on BundleAnalysisComparison { + bundles { + name + changeType + bundleData { + size { + uncompress + } + } + bundleChange { + size { + uncompress + } + } + } + bundleData { + size { + uncompress + } + } + bundleChange { + size { + uncompress + } + } + } + } + } + """ + ) + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["bundleAnalysis"]["bundleAnalysisCompareWithParent"] == { + "__typename": "BundleAnalysisComparison", + "bundles": [ + { + "name": "b1", + "changeType": "changed", + "bundleData": {"size": {"uncompress": 20}}, + "bundleChange": {"size": {"uncompress": 5}}, + }, + { + "name": "b2", + "changeType": "changed", + "bundleData": {"size": {"uncompress": 200}}, + "bundleChange": {"size": {"uncompress": 50}}, + }, + { + "name": "b3", + "changeType": "added", + "bundleData": {"size": {"uncompress": 1500}}, + "bundleChange": {"size": {"uncompress": 1500}}, + }, + { + "name": "b5", + "changeType": "changed", + "bundleData": {"size": {"uncompress": 200000}}, + "bundleChange": {"size": {"uncompress": 50000}}, + }, + { + "name": "b4", + "changeType": "removed", + "bundleData": {"size": {"uncompress": 0}}, + "bundleChange": {"size": {"uncompress": -15000}}, + }, + ], + "bundleData": {"size": {"uncompress": 201720}}, + "bundleChange": {"size": {"uncompress": 36555}}, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_report_size_filtered(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + with open( + "./services/tests/samples/head_bundle_report_with_gzip_size.sqlite", "rb" + ) as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + query = """ + query FetchCommit($org: String!, $repo: String!, $commit: String!, $bundleFilters: BundleReportFilters) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name:"b1") { + name + bundleDataFiltered(filters: $bundleFilters) { + size { + gzip + uncompress + } + } + } + } + } + } + } + } + } + } + } + """ + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "bundleFilters": {"reportGroup": "JAVASCRIPT"}, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "name": "b1", + "bundleDataFiltered": {"size": {"gzip": 6, "uncompress": 6}}, + }, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_report_size_filtered_no_value(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + with open( + "./services/tests/samples/head_bundle_report_with_gzip_size.sqlite", "rb" + ) as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + query = """ + query FetchCommit($org: String!, $repo: String!, $commit: String!, $bundleFilters: BundleReportFilters) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name:"b1") { + name + bundleDataFiltered(filters: $bundleFilters) { + size { + gzip + uncompress + } + } + } + } + } + } + } + } + } + } + } + """ + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "bundleFilters": {}, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + assert commit["bundleAnalysis"]["bundleAnalysisReport"] == { + "__typename": "BundleAnalysisReport", + "bundle": { + "name": "b1", + "bundleDataFiltered": {"size": {"gzip": 20, "uncompress": 20}}, + }, + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_asset_routes(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + with open( + "./services/tests/samples/bundle_with_asset_routes.sqlite", "rb" + ) as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchCommit($org: String!, $repo: String!, $commit: String!) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "@codecov/example-sveltekit-app-client-esm") { + assets { + name + normalizedName + routes + } + } + } + } + } + } + } + } + } + } + """ + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + asset_reports = commit["bundleAnalysis"]["bundleAnalysisReport"]["bundle"][ + "assets" + ] + + assert asset_reports == [ + { + "name": "_app/immutable/assets/svelte-welcome.VNiyy3gC.png", + "normalizedName": "_app/immutable/assets/svelte-welcome.*.png", + "routes": [], + }, + { + "name": "_app/immutable/assets/svelte-welcome.0pIiHnVF.webp", + "normalizedName": "_app/immutable/assets/svelte-welcome.*.webp", + "routes": [], + }, + { + "name": "_app/immutable/assets/fira-mono-all-400-normal.B2mvLtSD.woff", + "normalizedName": "_app/immutable/assets/fira-mono-all-400-normal.*.woff", + "routes": [], + }, + { + "name": "_app/immutable/chunks/entry.BaWB2kHj.js", + "normalizedName": "_app/immutable/chunks/entry.*.js", + "routes": [], + }, + { + "name": "_app/immutable/nodes/4.CcjRtXvw.js", + "normalizedName": "_app/immutable/nodes/4.*.js", + "routes": ["/sverdle"], + }, + { + "name": "_app/immutable/assets/fira-mono-latin-400-normal.DKjLVgQi.woff2", + "normalizedName": "_app/immutable/assets/fira-mono-latin-400-normal.*.woff2", + "routes": [], + }, + { + "name": "_app/immutable/assets/fira-mono-cyrillic-ext-400-normal.B04YIrm4.woff2", + "normalizedName": "_app/immutable/assets/fira-mono-cyrillic-ext-400-normal.*.woff2", + "routes": [], + }, + { + "name": "_app/immutable/assets/fira-mono-latin-ext-400-normal.D6XfiR-_.woff2", + "normalizedName": "_app/immutable/assets/fira-mono-latin-ext-400-normal.D6XfiR-_.woff2", + "routes": [], + }, + { + "name": "_app/immutable/assets/fira-mono-greek-400-normal.C3zng6O6.woff2", + "normalizedName": "_app/immutable/assets/fira-mono-greek-400-normal.*.woff2", + "routes": [], + }, + { + "name": "_app/immutable/assets/fira-mono-cyrillic-400-normal.36-45Uyg.woff2", + "normalizedName": "_app/immutable/assets/fira-mono-cyrillic-400-normal.*.woff2", + "routes": [], + }, + { + "name": "_app/immutable/nodes/0.CL_S-12h.js", + "normalizedName": "_app/immutable/nodes/0.CL_S-12h.js", + "routes": ["/"], + }, + { + "name": "_app/immutable/assets/fira-mono-greek-ext-400-normal.CsqI23CO.woff2", + "normalizedName": "_app/immutable/assets/fira-mono-greek-ext-400-normal.*.woff2", + "routes": [], + }, + { + "name": "_app/immutable/chunks/index.DDRweiI9.js", + "normalizedName": "_app/immutable/chunks/index.*.js", + "routes": [], + }, + { + "name": "_app/immutable/entry/app.Dd9ByE1Q.js", + "normalizedName": "_app/immutable/entry/app.*.js", + "routes": [], + }, + { + "name": "_app/immutable/nodes/2.BMQFqo-e.js", + "normalizedName": "_app/immutable/nodes/2.*.js", + "routes": ["/"], + }, + { + "name": "_app/immutable/assets/0.CT0x_Q5c.css", + "normalizedName": "_app/immutable/assets/0.CT0x_Q5c.css", + "routes": [], + }, + { + "name": "_app/immutable/assets/4.DOkkq0IA.css", + "normalizedName": "_app/immutable/assets/4.*.css", + "routes": [], + }, + { + "name": "_app/immutable/nodes/5.CwxmUzn6.js", + "normalizedName": "_app/immutable/nodes/5.*.js", + "routes": ["/sverdle/how-to-play"], + }, + { + "name": "_app/immutable/chunks/scheduler.Dk-snqIU.js", + "normalizedName": "_app/immutable/chunks/scheduler.*.js", + "routes": [], + }, + { + "name": "_app/immutable/nodes/3.BqQOub2U.js", + "normalizedName": "_app/immutable/nodes/3.*.js", + "routes": ["/about"], + }, + { + "name": "_app/immutable/assets/2.Cs8KR-Bb.css", + "normalizedName": "_app/immutable/assets/2.*.css", + "routes": [], + }, + { + "name": "_app/immutable/nodes/1.stWWSe4n.js", + "normalizedName": "_app/immutable/nodes/1.*.js", + "routes": [], + }, + { + "name": "_app/immutable/assets/5.CU6psp88.css", + "normalizedName": "_app/immutable/assets/5.*.css", + "routes": [], + }, + { + "name": "_app/immutable/chunks/index.Ice1EKvx.js", + "normalizedName": "_app/immutable/chunks/index.*.js", + "routes": [], + }, + { + "name": "_app/immutable/chunks/stores.BrqGIpx3.js", + "normalizedName": "_app/immutable/chunks/stores.*.js", + "routes": [], + }, + { + "name": "_app/immutable/entry/start.B1Q1eB84.js", + "normalizedName": "_app/immutable/entry/start.*.js", + "routes": [], + }, + { + "name": "_app/immutable/chunks/index.R8ovVqwX.js", + "normalizedName": "_app/immutable/chunks/index.*.js", + "routes": [], + }, + { + "name": "_app/version.json", + "normalizedName": "_app/version.json", + "routes": [], + }, + ] + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_report_info(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + head_commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + with open( + "./services/tests/samples/bundle_with_assets_and_modules.sqlite", "rb" + ) as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + query FetchCommit($org: String!, $repo: String!, $commit: String!) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + bundleAnalysis { + bundleAnalysisReport { + __typename + ... on BundleAnalysisReport { + bundle(name: "b5") { + info { + version + pluginName + pluginVersion + builtAt + duration + bundlerName + bundlerVersion + } + } + } + } + } + } + } + } + } + } + """ + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + bundle_info = commit["bundleAnalysis"]["bundleAnalysisReport"]["bundle"]["info"] + + assert bundle_info["version"] == "1" + assert bundle_info["pluginName"] == "codecov-vite-bundle-analysis-plugin" + assert bundle_info["pluginVersion"] == "1.0.0" + assert bundle_info["builtAt"] == "2023-12-01 17:17:28.604000" + assert bundle_info["duration"] == 331 + assert bundle_info["bundlerName"] == "rollup" + assert bundle_info["bundlerVersion"] == "3.29.4" + + def test_latest_upload_error(self): + commit = CommitFactory(repository=self.repo) + report = CommitReportFactory( + commit=commit, report_type=CommitReport.ReportType.TEST_RESULTS + ) + upload = UploadFactory(report=report) + UploadErrorFactory( + report_session=upload, + error_code=UploadErrorEnum.UNKNOWN_PROCESSING, + error_params={"error_message": "Unknown processing error"}, + ) + + query = """ + query FetchCommit($org: String!, $repo: String!, $commit: String!) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + latestUploadError { + errorCode + errorMessage + } + } + } + } + } + } + """ + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": commit.commitid, + } + data = self.gql_request(query, variables=variables) + commit = data["owner"]["repository"]["commit"] + + assert commit["latestUploadError"] == { + "errorCode": "UNKNOWN_PROCESSING", + "errorMessage": "Unknown processing error", + } diff --git a/apps/codecov-api/graphql_api/tests/test_components.py b/apps/codecov-api/graphql_api/tests/test_components.py new file mode 100644 index 0000000000..50c4d777c7 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_components.py @@ -0,0 +1,1494 @@ +from unittest.mock import PropertyMock, patch + +import pytest +from django.conf import settings +from django.test import TestCase, override_settings +from django.utils import timezone +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine +from shared.utils.sessions import Session + +from compare.models import CommitComparison +from compare.tests.factories import CommitComparisonFactory, ComponentComparisonFactory +from services.comparison import MissingComparisonReport +from services.components import Component +from timeseries.models import MeasurementName +from timeseries.tests.factories import DatasetFactory, MeasurementFactory + +from .helper import GraphQLTestHelper + + +def sample_report(): + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=["flag1", "flag2"])) + return report + + +query_commit_components = """ + query CommitComponents( + $org: String! + $repo: String! + $sha: String! + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $sha) { + coverageAnalytics { + components { + id + name + totals { + hitsCount + missesCount + percentCovered + } + } + } + } + } + } + } + } +""" + +query_repo = """ +query Repo( + $org: String! + $repo: String! + $sha: String! +) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + coverageAnalytics { + componentsMeasurementsActive + componentsMeasurementsBackfilled + componentsCount + } + commit(id: $sha) { + coverageAnalytics { + components { + id + name + totals { + hitsCount + missesCount + percentCovered + } + } + } + } + } + } + } +} +""" + +query_commit_coverage_components = """ + query CommitComponents( + $org: String! + $repo: String! + $sha: String! + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $sha) { + coverageAnalytics { + components { + id + name + totals { + hitsCount + missesCount + percentCovered + } + } + } + } + } + } + } + } +""" + + +class TestCommitCoverageComponents(GraphQLTestHelper, TestCase): + def setUp(self): + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org, private=False) + self.commit = CommitFactory(repository=self.repo) + + def test_no_components(self): + variables = { + "org": self.org.username, + "repo": self.repo.name, + "sha": self.commit.commitid, + } + data = self.gql_request( + query_commit_coverage_components, variables=variables, owner=OwnerFactory() + ) + assert data == { + "owner": { + "repository": { + "commit": { + "coverageAnalytics": { + "components": [], + } + } + } + } + } + + @patch("core.models.Commit.full_report", new_callable=PropertyMock) + @patch("services.components.commit_components") + def test_components(self, commit_components_mock, full_report_mock): + commit_components_mock.return_value = [ + Component.from_dict( + { + "component_id": "python", + "paths": [".*/*.py"], + } + ), + Component.from_dict( + { + "component_id": "golang", + "paths": [".*/*.go"], + } + ), + ] + + full_report_mock.return_value = sample_report() + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "sha": self.commit.commitid, + } + data = self.gql_request( + query_commit_coverage_components, + variables=variables, + owner=OwnerFactory(), + ) + assert data == { + "owner": { + "repository": { + "commit": { + "coverageAnalytics": { + "components": [ + { + "id": "python", + "name": "python", + "totals": { + "hitsCount": 1, + "missesCount": 0, + "percentCovered": 50.0, + }, + }, + { + "id": "golang", + "name": "golang", + "totals": { + "hitsCount": 5, + "missesCount": 3, + "percentCovered": 62.5, + }, + }, + ] + } + } + } + } + } + + @patch("core.models.Commit.full_report", new_callable=PropertyMock) + @patch("services.components.commit_components") + def test_components_filtering(self, commit_components_mock, full_report_mock): + commit_components_mock.return_value = [ + Component.from_dict( + { + "component_id": "python1.1", + "name": "Python", + "paths": [".*/*.py"], + } + ), + Component.from_dict( + { + "component_id": "golang1.2", + "name": "Golang", + "paths": [".*/*.go"], + } + ), + ] + + full_report_mock.return_value = sample_report() + + query_commit_coverage_components = """ + query CommitComponents( + $org: String! + $repo: String! + $sha: String! + $filter: ComponentsFilters + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $sha) { + coverageAnalytics { + components (filters: $filter) { + id + name + } + } + } + } + } + } + } + """ + + # Find one item + variables = { + "org": self.org.username, + "repo": self.repo.name, + "sha": self.commit.commitid, + "filter": {"components": ["Python"]}, + } + data = self.gql_request( + query_commit_coverage_components, variables=variables, owner=OwnerFactory() + ) + assert data == { + "owner": { + "repository": { + "commit": { + "coverageAnalytics": { + "components": [ + { + "id": "python1.1", + "name": "Python", + }, + ] + } + } + } + } + } + + # Find no items + variables = { + "org": self.org.username, + "repo": self.repo.name, + "sha": self.commit.commitid, + "filter": {"components": ["C++"]}, + } + data = self.gql_request(query_commit_coverage_components, variables=variables) + assert data == { + "owner": { + "repository": {"commit": {"coverageAnalytics": {"components": []}}} + } + } + + # Find all items + variables = { + "org": self.org.username, + "repo": self.repo.name, + "sha": self.commit.commitid, + "filter": {"components": []}, + } + data = self.gql_request(query_commit_coverage_components, variables=variables) + assert data == { + "owner": { + "repository": { + "commit": { + "coverageAnalytics": { + "components": [ + { + "id": "python1.1", + "name": "Python", + }, + { + "id": "golang1.2", + "name": "Golang", + }, + ] + } + } + } + } + } + + # Find some items + variables = { + "org": self.org.username, + "repo": self.repo.name, + "sha": self.commit.commitid, + "filter": {"components": ["C", "Golang"]}, + } + data = self.gql_request( + query_commit_coverage_components, variables=variables, owner=OwnerFactory() + ) + assert data == { + "owner": { + "repository": { + "commit": { + "coverageAnalytics": { + "components": [ + { + "id": "golang1.2", + "name": "Golang", + }, + ] + } + } + } + } + } + + @patch("core.models.Commit.full_report", new_callable=PropertyMock) + @patch("services.components.commit_components") + def test_components_filtering_case_insensitive( + self, commit_components_mock, full_report_mock + ): + commit_components_mock.return_value = [ + Component.from_dict( + { + "component_id": "cpython", + "name": "PyThOn", + "paths": [".*/*.py"], + } + ), + ] + + full_report_mock.return_value = sample_report() + + query_commit_coverage_components = """ + query CommitComponents( + $org: String! + $repo: String! + $sha: String! + $filter: ComponentsFilters + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $sha) { + coverageAnalytics { + components (filters: $filter) { + id + name + } + } + } + } + } + } + } + """ + variables = { + "org": self.org.username, + "repo": self.repo.name, + "sha": self.commit.commitid, + "filter": {"components": ["pYtHoN"]}, + } + data = self.gql_request( + query_commit_coverage_components, variables=variables, owner=OwnerFactory() + ) + assert data == { + "owner": { + "repository": { + "commit": { + "coverageAnalytics": { + "components": [ + { + "id": "cpython", + "name": "PyThOn", + }, + ] + } + } + } + } + } + + +query_components_comparison = """ + query ComponentsComparison( + $org: String! + $repo: String! + $pullid: Int! + $filters: ComponentsFilters + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + pull(id: $pullid) { + compareWithBase { + __typename + ... on Comparison { + componentComparisonsCount + componentComparisons(filters: $filters) { + id + name + baseTotals { + percentCovered + } + headTotals { + percentCovered + } + patchTotals { + percentCovered + } + } + } + } + } + } + } + } + } +""" + + +class TestComponentsComparison(GraphQLTestHelper, TestCase): + databases = {"default", "timeseries"} + + def setUp(self): + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org, private=False) + self.base = CommitFactory(repository=self.repo) + self.head = CommitFactory( + repository=self.repo, parent_commit_id=self.base.commitid + ) + self.pull = PullFactory( + pullid=2, + repository=self.repo, + base=self.base.commitid, + head=self.head.commitid, + compared_to=self.base.commitid, + ) + self.comparison = CommitComparisonFactory( + base_commit=self.base, + compare_commit=self.head, + state=CommitComparison.CommitComparisonStates.PROCESSED, + ) + self.commit = CommitFactory(repository=self.repo) + + # mock reports + self.head_report_patcher = patch( + "services.comparison.Comparison.head_report", new_callable=PropertyMock + ) + self.head_report = self.head_report_patcher.start() + self.head_report.return_value = sample_report() + self.addCleanup(self.head_report_patcher.stop) + self.base_report_patcher = patch( + "services.comparison.Comparison.base_report", new_callable=PropertyMock + ) + self.base_report = self.base_report_patcher.start() + self.base_report.return_value = sample_report() + self.addCleanup(self.base_report_patcher.stop) + + def test_no_components_in_pull_request(self): + variables = { + "org": self.org.username, + "repo": self.repo.name, + "pullid": self.pull.pullid, + } + data = self.gql_request(query_components_comparison, variables=variables) + assert data == { + "owner": { + "repository": { + "pull": { + "compareWithBase": { + "__typename": "Comparison", + "componentComparisons": [], + "componentComparisonsCount": 0, + } + } + } + } + } + + @patch("services.comparison.Comparison.validate") + def test_components_invalid_comparison_object(self, mock_compare_validate): + mock_compare_validate.side_effect = MissingComparisonReport() + variables = { + "org": self.org.username, + "repo": self.repo.name, + "pullid": self.pull.pullid, + } + data = self.gql_request(query_components_comparison, variables=variables) + assert data == { + "owner": { + "repository": { + "pull": { + "compareWithBase": { + "__typename": "Comparison", + "componentComparisons": [], + "componentComparisonsCount": 0, + } + } + } + } + } + + @patch("services.components.commit_components") + def test_components(self, commit_components_mock): + commit_components_mock.return_value = [ + Component.from_dict( + { + "component_id": "python", + "paths": [".*/*.py"], + } + ), + Component.from_dict( + { + "component_id": "golang", + "paths": [".*/*.go"], + } + ), + ] + + ComponentComparisonFactory( + commit_comparison=self.comparison, + component_id="python", + ) + ComponentComparisonFactory( + commit_comparison=self.comparison, + component_id="golang", + ) + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "pullid": self.pull.pullid, + } + data = self.gql_request(query_components_comparison, variables=variables) + assert data == { + "owner": { + "repository": { + "pull": { + "compareWithBase": { + "__typename": "Comparison", + "componentComparisonsCount": 2, + "componentComparisons": [ + { + "id": "python", + "name": "python", + "baseTotals": {"percentCovered": 72.92638}, + "headTotals": {"percentCovered": 85.71429}, + "patchTotals": {"percentCovered": 28.57143}, + }, + { + "id": "golang", + "name": "golang", + "baseTotals": {"percentCovered": 72.92638}, + "headTotals": {"percentCovered": 85.71429}, + "patchTotals": {"percentCovered": 28.57143}, + }, + ], + } + } + } + } + } + + @patch("services.components.commit_components") + def test_components_filter(self, commit_components_mock): + commit_components_mock.return_value = [ + Component.from_dict( + {"component_id": "python", "paths": [".*/*.py"], "name": "python"} + ), + Component.from_dict( + {"component_id": "golang", "paths": [".*/*.go"], "name": "golang"} + ), + ] + + ComponentComparisonFactory( + commit_comparison=self.comparison, + component_id="python", + ) + ComponentComparisonFactory( + commit_comparison=self.comparison, + component_id="golang", + ) + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "pullid": self.pull.pullid, + "filters": {"components": ["python"]}, + } + data = self.gql_request(query_components_comparison, variables=variables) + assert data == { + "owner": { + "repository": { + "pull": { + "compareWithBase": { + "__typename": "Comparison", + "componentComparisonsCount": 2, + "componentComparisons": [ + { + "id": "python", + "name": "python", + "baseTotals": {"percentCovered": 72.92638}, + "headTotals": {"percentCovered": 85.71429}, + "patchTotals": {"percentCovered": 28.57143}, + }, + ], + } + } + } + } + } + + @patch("services.components.commit_components") + def test_components_multi_filter(self, commit_components_mock): + commit_components_mock.return_value = [ + Component.from_dict( + {"component_id": "python", "paths": [".*/*.py"], "name": "python"} + ), + Component.from_dict( + {"component_id": "golang", "paths": [".*/*.go"], "name": "golang"} + ), + Component.from_dict( + {"component_id": "js", "paths": [".*/*.js"], "name": "javascript"} + ), + ] + + ComponentComparisonFactory( + commit_comparison=self.comparison, + component_id="python", + ) + ComponentComparisonFactory( + commit_comparison=self.comparison, + component_id="golang", + ) + ComponentComparisonFactory( + commit_comparison=self.comparison, + component_id="js", + ) + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "pullid": self.pull.pullid, + "filters": {"components": ["python", "javascript"]}, + } + data = self.gql_request(query_components_comparison, variables=variables) + assert data == { + "owner": { + "repository": { + "pull": { + "compareWithBase": { + "__typename": "Comparison", + "componentComparisonsCount": 3, + "componentComparisons": [ + { + "id": "python", + "name": "python", + "baseTotals": {"percentCovered": 72.92638}, + "headTotals": {"percentCovered": 85.71429}, + "patchTotals": {"percentCovered": 28.57143}, + }, + { + "id": "js", + "name": "javascript", + "baseTotals": {"percentCovered": 72.92638}, + "headTotals": {"percentCovered": 85.71429}, + "patchTotals": {"percentCovered": 28.57143}, + }, + ], + } + } + } + } + } + + @patch("services.components.commit_components") + def test_components_filter_case_insensitive(self, commit_components_mock): + commit_components_mock.return_value = [ + Component.from_dict( + {"component_id": "python1.1", "paths": [".*/*.py"], "name": "PYThon"} + ), + Component.from_dict( + {"component_id": "golang1.2", "paths": [".*/*.go"], "name": "GOLang"} + ), + ] + + ComponentComparisonFactory( + commit_comparison=self.comparison, + component_id="python1.1", + ) + ComponentComparisonFactory( + commit_comparison=self.comparison, + component_id="golang1.2", + ) + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "pullid": self.pull.pullid, + "filters": {"components": ["PYThon", "golANG"]}, + } + data = self.gql_request(query_components_comparison, variables=variables) + assert data == { + "owner": { + "repository": { + "pull": { + "compareWithBase": { + "__typename": "Comparison", + "componentComparisonsCount": 2, + "componentComparisons": [ + { + "id": "python1.1", + "name": "PYThon", + "baseTotals": {"percentCovered": 72.92638}, + "headTotals": {"percentCovered": 85.71429}, + "patchTotals": {"percentCovered": 28.57143}, + }, + { + "id": "golang1.2", + "name": "GOLang", + "baseTotals": {"percentCovered": 72.92638}, + "headTotals": {"percentCovered": 85.71429}, + "patchTotals": {"percentCovered": 28.57143}, + }, + ], + } + } + } + } + } + + def test_repository_components_metadata_inactive(self): + data = self.gql_request( + query_repo, + variables={ + "org": self.org.username, + "repo": self.repo.name, + "sha": self.commit.commitid, + }, + ) + assert ( + data["owner"]["repository"]["coverageAnalytics"][ + "componentsMeasurementsActive" + ] + == False + ) + assert ( + data["owner"]["repository"]["coverageAnalytics"][ + "componentsMeasurementsBackfilled" + ] + == False + ) + + def test_repository_components_metadata_active(self): + DatasetFactory( + name=MeasurementName.COMPONENT_COVERAGE.value, + repository_id=self.repo.pk, + ) + + data = self.gql_request( + query_repo, + variables={ + "org": self.org.username, + "repo": self.repo.name, + "sha": self.commit.commitid, + }, + ) + assert ( + data["owner"]["repository"]["coverageAnalytics"][ + "componentsMeasurementsActive" + ] + == True + ) + assert ( + data["owner"]["repository"]["coverageAnalytics"][ + "componentsMeasurementsBackfilled" + ] + == False + ) + + @patch("timeseries.models.Dataset.is_backfilled") + def test_repository_components_metadata_backfilled_true(self, is_backfilled): + is_backfilled.return_value = True + + DatasetFactory( + name=MeasurementName.COMPONENT_COVERAGE.value, + repository_id=self.repo.pk, + ) + + data = self.gql_request( + query_repo, + variables={ + "org": self.org.username, + "repo": self.repo.name, + "sha": self.commit.commitid, + }, + ) + assert ( + data["owner"]["repository"]["coverageAnalytics"][ + "componentsMeasurementsActive" + ] + == True + ) + assert ( + data["owner"]["repository"]["coverageAnalytics"][ + "componentsMeasurementsBackfilled" + ] + == True + ) + + +query_component_measurements = """ +query ComponentMeasurements( + $name: String! + $repo: String! + $interval: MeasurementInterval! + $after: DateTime! + $before: DateTime! + $branch: String + $filters: ComponentMeasurementsSetFilters + $orderingDirection: OrderingDirection +) { + owner(username: $name) { + repository(name: $repo) { + ... on Repository { + coverageAnalytics { + components(filters: $filters, orderingDirection: $orderingDirection, after: $after, before: $before, branch: $branch, interval: $interval) { + __typename + ... on ComponentMeasurements { + name + percentCovered + percentChange + measurements { + avg + min + max + timestamp + } + lastUploaded + } + } + } + } + } + } +} +""" + + +@pytest.mark.skipif( + not settings.TIMESERIES_ENABLED, reason="requires timeseries data storage" +) +class TestComponentMeasurements(GraphQLTestHelper, TestCase): + databases = {"default", "timeseries"} + + def setUp(self): + self.org = OwnerFactory() + self.repo = RepositoryFactory( + author=self.org, + private=False, + yaml={ + "component_management": { + "default_rules": {}, + "individual_components": [ + { + "component_id": "python", + "name": "pythonName", + "paths": [".*/*.py"], + }, + { + "component_id": "golang", + "paths": [".*/*.go"], + }, + ], + } + }, + ) + self.commit = CommitFactory(repository=self.repo) + + def test_component_measurements_with_measurements(self): + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="python", + commit_sha=self.commit.pk, + timestamp="2022-06-21T00:00:00", + value=75.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="python", + commit_sha=self.commit.pk, + timestamp="2022-06-22T00:00:00", + value=75.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="python", + commit_sha=self.commit.pk, + timestamp="2022-06-22T01:00:00", + value=85.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="golang", + commit_sha=self.commit.pk, + timestamp="2022-06-21T00:00:00", + value=85.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="golang", + commit_sha=self.commit.pk, + timestamp="2022-06-22T00:00:00", + value=95.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="golang", + commit_sha=self.commit.pk, + timestamp="2022-06-22T01:00:00", + value=85.0, + ) + + variables = { + "name": self.org.username, + "repo": self.repo.name, + "interval": "INTERVAL_1_DAY", + "after": timezone.datetime(2022, 6, 20), + "before": timezone.datetime(2022, 6, 23), + } + data = self.gql_request(query_component_measurements, variables=variables) + + assert data == { + "owner": { + "repository": { + "coverageAnalytics": { + "components": [ + { + "__typename": "ComponentMeasurements", + "name": "golang", + "percentCovered": 90.0, + "percentChange": 5.0, + "measurements": [ + { + "avg": None, + "min": None, + "max": None, + "timestamp": "2022-06-20T00:00:00+00:00", + }, + { + "avg": 85.0, + "min": 85.0, + "max": 85.0, + "timestamp": "2022-06-21T00:00:00+00:00", + }, + { + "avg": 90.0, + "min": 85.0, + "max": 95.0, + "timestamp": "2022-06-22T00:00:00+00:00", + }, + { + "avg": None, + "min": None, + "max": None, + "timestamp": "2022-06-23T00:00:00+00:00", + }, + ], + "lastUploaded": "2022-06-22T01:00:00+00:00", + }, + { + "__typename": "ComponentMeasurements", + "name": "pythonName", + "percentCovered": 80.0, + "percentChange": 5.0, + "measurements": [ + { + "avg": None, + "min": None, + "max": None, + "timestamp": "2022-06-20T00:00:00+00:00", + }, + { + "avg": 75.0, + "min": 75.0, + "max": 75.0, + "timestamp": "2022-06-21T00:00:00+00:00", + }, + { + "avg": 80.0, + "min": 75.0, + "max": 85.0, + "timestamp": "2022-06-22T00:00:00+00:00", + }, + { + "avg": None, + "min": None, + "max": None, + "timestamp": "2022-06-23T00:00:00+00:00", + }, + ], + "lastUploaded": "2022-06-22T01:00:00+00:00", + }, + ] + } + } + } + } + + def test_component_measurements_no_measurements(self): + variables = { + "name": self.org.username, + "repo": self.repo.name, + "interval": "INTERVAL_1_DAY", + "after": timezone.datetime(2022, 6, 20), + "before": timezone.datetime(2022, 6, 23), + } + data = self.gql_request(query_component_measurements, variables=variables) + assert data == { + "owner": { + "repository": { + "coverageAnalytics": { + "components": [ + { + "__typename": "ComponentMeasurements", + "name": "golang", + "percentCovered": None, + "percentChange": None, + "measurements": [], + "lastUploaded": None, + }, + { + "__typename": "ComponentMeasurements", + "name": "pythonName", + "percentCovered": None, + "percentChange": None, + "measurements": [], + "lastUploaded": None, + }, + ] + } + } + } + } + + @override_settings(TIMESERIES_ENABLED=False) + def test_component_measurements_timeseries_not_enabled(self): + variables = { + "name": self.org.username, + "repo": self.repo.name, + "interval": "INTERVAL_1_DAY", + "after": timezone.datetime(2022, 6, 20), + "before": timezone.datetime(2022, 6, 23), + } + data = self.gql_request(query_component_measurements, variables=variables) + assert data == { + "owner": {"repository": {"coverageAnalytics": {"components": []}}} + } + + def test_component_measurements_with_filter(self): + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="python", + commit_sha=self.commit.pk, + timestamp="2022-06-21T00:00:00", + value=75.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="python", + commit_sha=self.commit.pk, + timestamp="2022-06-22T00:00:00", + value=75.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="python", + commit_sha=self.commit.pk, + timestamp="2022-06-22T01:00:00", + value=85.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="golang", + commit_sha=self.commit.pk, + timestamp="2022-06-21T00:00:00", + value=85.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="golang", + commit_sha=self.commit.pk, + timestamp="2022-06-22T00:00:00", + value=95.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="golang", + commit_sha=self.commit.pk, + timestamp="2022-06-22T01:00:00", + value=85.0, + ) + + variables = { + "name": self.org.username, + "repo": self.repo.name, + "interval": "INTERVAL_1_DAY", + "after": timezone.datetime(2022, 6, 20), + "before": timezone.datetime(2022, 6, 23), + "filters": {"components": ["python"]}, + } + data = self.gql_request(query_component_measurements, variables=variables) + + assert data == { + "owner": { + "repository": { + "coverageAnalytics": { + "components": [ + { + "__typename": "ComponentMeasurements", + "name": "pythonName", + "percentCovered": 80.0, + "percentChange": 5.0, + "measurements": [ + { + "avg": None, + "min": None, + "max": None, + "timestamp": "2022-06-20T00:00:00+00:00", + }, + { + "avg": 75.0, + "min": 75.0, + "max": 75.0, + "timestamp": "2022-06-21T00:00:00+00:00", + }, + { + "avg": 80.0, + "min": 75.0, + "max": 85.0, + "timestamp": "2022-06-22T00:00:00+00:00", + }, + { + "avg": None, + "min": None, + "max": None, + "timestamp": "2022-06-23T00:00:00+00:00", + }, + ], + "lastUploaded": "2022-06-22T01:00:00+00:00", + }, + ] + } + } + } + } + + def test_component_measurements_with_branch(self): + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="python", + commit_sha=self.commit.pk, + timestamp="2022-06-21T00:00:00", + value=75.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="python", + commit_sha=self.commit.pk, + timestamp="2022-06-22T00:00:00", + value=75.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="python", + commit_sha=self.commit.pk, + timestamp="2022-06-22T01:00:00", + value=85.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="dev", + measurable_id="golang", + commit_sha=self.commit.pk, + timestamp="2022-06-21T00:00:00", + value=85.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="dev", + measurable_id="golang", + commit_sha=self.commit.pk, + timestamp="2022-06-22T00:00:00", + value=95.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="dev", + measurable_id="golang", + commit_sha=self.commit.pk, + timestamp="2022-06-22T01:00:00", + value=85.0, + ) + + variables = { + "name": self.org.username, + "repo": self.repo.name, + "interval": "INTERVAL_1_DAY", + "after": timezone.datetime(2022, 6, 20), + "before": timezone.datetime(2022, 6, 23), + "branch": "dev", + } + data = self.gql_request(query_component_measurements, variables=variables) + + assert data == { + "owner": { + "repository": { + "coverageAnalytics": { + "components": [ + { + "__typename": "ComponentMeasurements", + "name": "golang", + "percentCovered": 90.0, + "percentChange": 5.0, + "measurements": [ + { + "avg": None, + "min": None, + "max": None, + "timestamp": "2022-06-20T00:00:00+00:00", + }, + { + "avg": 85.0, + "min": 85.0, + "max": 85.0, + "timestamp": "2022-06-21T00:00:00+00:00", + }, + { + "avg": 90.0, + "min": 85.0, + "max": 95.0, + "timestamp": "2022-06-22T00:00:00+00:00", + }, + { + "avg": None, + "min": None, + "max": None, + "timestamp": "2022-06-23T00:00:00+00:00", + }, + ], + "lastUploaded": "2022-06-22T01:00:00+00:00", + }, + { + "__typename": "ComponentMeasurements", + "name": "pythonName", + "percentCovered": None, + "percentChange": None, + "measurements": [], + "lastUploaded": None, + }, + ] + } + } + } + } + + def test_component_measurements_id_fallback(self): + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="python", + commit_sha=self.commit.pk, + timestamp="2022-06-21T00:00:00", + value=75.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="python", + commit_sha=self.commit.pk, + timestamp="2022-06-22T00:00:00", + value=75.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id="python", + commit_sha=self.commit.pk, + timestamp="2022-06-22T01:00:00", + value=85.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="dev", + measurable_id="golang", + commit_sha=self.commit.pk, + timestamp="2022-06-21T00:00:00", + value=85.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="dev", + measurable_id="golang", + commit_sha=self.commit.pk, + timestamp="2022-06-22T00:00:00", + value=95.0, + ) + MeasurementFactory( + name="component_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="dev", + measurable_id="golang", + commit_sha=self.commit.pk, + timestamp="2022-06-22T01:00:00", + value=85.0, + ) + + query = """ + query ComponentMeasurements( + $name: String! + $repo: String! + $interval: MeasurementInterval! + $after: DateTime! + $before: DateTime! + $branch: String + $filters: ComponentMeasurementsSetFilters + $orderingDirection: OrderingDirection + ) { + owner(username: $name) { + repository: repository(name: $repo) { + ... on Repository { + coverageAnalytics { + components(filters: $filters, orderingDirection: $orderingDirection, after: $after, before: $before, branch: $branch, interval: $interval) { + __typename + ... on ComponentMeasurements { + name + componentId + } + } + } + } + } + } + } + """ + + variables = { + "name": self.org.username, + "repo": self.repo.name, + "interval": "INTERVAL_1_DAY", + "after": timezone.datetime(2022, 6, 20), + "before": timezone.datetime(2022, 6, 23), + "branch": "dev", + } + data = self.gql_request(query, variables=variables) + + assert data == { + "owner": { + "repository": { + "coverageAnalytics": { + "components": [ + { + "__typename": "ComponentMeasurements", + "name": "golang", + "componentId": "golang", + }, + { + "__typename": "ComponentMeasurements", + "name": "pythonName", + "componentId": "python", + }, + ] + } + } + } + } diff --git a/apps/codecov-api/graphql_api/tests/test_config.py b/apps/codecov-api/graphql_api/tests/test_config.py new file mode 100644 index 0000000000..6aff4341c1 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_config.py @@ -0,0 +1,318 @@ +from datetime import datetime +from unittest.mock import patch + +from django.test import TestCase, override_settings +from shared.license import LicenseInformation + +from .helper import GraphQLTestHelper + + +class TestConfigType(GraphQLTestHelper, TestCase): + @override_settings( + GITHUB_CLIENT_ID="Github", + GITHUB_ENTERPRISE_CLIENT_ID="Github Enterprise", + GITLAB_CLIENT_ID="Gitlab", + GITLAB_ENTERPRISE_CLIENT_ID="Gitlab Enterprise", + BITBUCKET_CLIENT_ID="Bitbucket", + BITBUCKET_SERVER_CLIENT_ID="Bitbucket Server", + OKTA_OAUTH_CLIENT_ID="Okta", + ) + def test_login_providers(self): + data = self.gql_request("query { config { loginProviders }}") + assert data == { + "config": { + "loginProviders": [ + "GITHUB", + "GITHUB_ENTERPRISE", + "GITLAB", + "GITLAB_ENTERPRISE", + "BITBUCKET", + "BITBUCKET_SERVER", + "OKTA", + ], + }, + } + + @override_settings( + GITHUB_CLIENT_ID="Github", + GITHUB_ENTERPRISE_CLIENT_ID="Github Enterprise", + OKTA_OAUTH_CLIENT_ID="Okta", + DISABLE_GIT_BASED_LOGIN=True, + ) + def test_login_providers_no_git(self): + data = self.gql_request("query { config { loginProviders }}") + assert data == { + "config": { + "loginProviders": [ + "OKTA", + ], + }, + } + + def test_seats_used(self): + data = self.gql_request("query { config { seatsUsed }}") + assert data == { + "config": { + "seatsUsed": None, + }, + } + + @override_settings(IS_ENTERPRISE=True) + @patch("services.self_hosted.activated_owners") + def test_seats_used_self_hosted(self, activated_owners): + activated_owners.count.return_value = 1 + data = self.gql_request("query { config { seatsUsed }}") + assert data == { + "config": { + "seatsUsed": 1, + }, + } + + def test_plan_auto_activate(self): + data = self.gql_request("query { config { planAutoActivate }}") + assert data == { + "config": { + "planAutoActivate": None, + }, + } + + @override_settings(IS_ENTERPRISE=True) + @patch("services.self_hosted.is_autoactivation_enabled") + def test_plan_auto_activate_self_hosted(self, is_autoactivation_enabled): + is_autoactivation_enabled.return_value = True + + data = self.gql_request("query { config { planAutoActivate }}") + assert data == { + "config": { + "planAutoActivate": True, + }, + } + + def test_seats_limit(self): + data = self.gql_request("query { config { seatsLimit }}") + assert data == { + "config": { + "seatsLimit": None, + }, + } + + @override_settings(IS_ENTERPRISE=True) + @patch("services.self_hosted.license_seats") + def test_seats_limit_self_hosted(self, license_seats): + license_seats.return_value = 123 + data = self.gql_request("query { config { seatsLimit }}") + assert data == { + "config": { + "seatsLimit": 123, + }, + } + + @override_settings(TIMESERIES_ENABLED=True) + def test_timeseries_enabled(self): + data = self.gql_request("query { config { isTimescaleEnabled }}") + assert data == { + "config": { + "isTimescaleEnabled": True, + }, + } + + @override_settings(TIMESERIES_ENABLED=False) + def test_timeseries_enabled_is_false(self): + data = self.gql_request("query { config { isTimescaleEnabled }}") + assert data == { + "config": { + "isTimescaleEnabled": False, + }, + } + + @override_settings(TIMESERIES_ENABLED="true") + def test_timeseries_enabled_is_true_string(self): + data = self.gql_request("query { config { isTimescaleEnabled }}") + assert data == { + "config": { + "isTimescaleEnabled": True, + }, + } + + @override_settings(TIMESERIES_ENABLED="false") + def test_timeseries_enabled_is_false_string(self): + data = self.gql_request("query { config { isTimescaleEnabled }}") + assert data == { + "config": { + "isTimescaleEnabled": False, + }, + } + + @override_settings(IS_ENTERPRISE=True, ADMINS_LIST=[]) + def test_has_admins_empty_admins_list(self): + data = self.gql_request("query { config { hasAdmins }}") + assert data == { + "config": { + "hasAdmins": False, + }, + } + + @override_settings(IS_ENTERPRISE=False) + def test_has_admins_enterprise_is_false(self): + data = self.gql_request("query { config { hasAdmins }}") + assert data == { + "config": { + "hasAdmins": None, + }, + } + + @override_settings( + IS_ENTERPRISE=True, ADMINS_LIST=[{"service": "github", "username": "Imogen"}] + ) + def test_has_admins_with_enterprise_and_admins(self): + data = self.gql_request("query { config { hasAdmins }}") + assert data == { + "config": { + "hasAdmins": True, + }, + } + + @override_settings( + IS_ENTERPRISE=True, + GITHUB_ENTERPRISE_CLIENT_ID="Github", + GITHUB_ENTERPRISE_URL="https://github.example.com", + ) + def test_resolve_github_enterprise_url(self): + data = self.gql_request("query { config { githubEnterpriseURL }}") + assert data == { + "config": { + "githubEnterpriseURL": "https://github.example.com", + }, + } + + @override_settings( + IS_ENTERPRISE=False, + ) + def test_resolve_null_github_enterprise_url(self): + data = self.gql_request("query { config { githubEnterpriseURL }}") + assert data == { + "config": { + "githubEnterpriseURL": None, + }, + } + + @override_settings( + IS_ENTERPRISE=True, + GITLAB_ENTERPRISE_CLIENT_ID="Gitlab", + GITLAB_ENTERPRISE_URL="https://gitlab.example.com", + ) + def test_resolve_gitlab_enterprise_url(self): + data = self.gql_request("query { config { gitlabEnterpriseURL }}") + assert data == { + "config": { + "gitlabEnterpriseURL": "https://gitlab.example.com", + }, + } + + @override_settings( + IS_ENTERPRISE=False, + ) + def test_resolve_null_gitlab_enterprise_url(self): + data = self.gql_request("query { config { gitlabEnterpriseURL }}") + assert data == { + "config": { + "gitlabEnterpriseURL": None, + }, + } + + @override_settings( + IS_ENTERPRISE=True, + BITBUCKET_SERVER_CLIENT_ID="Bitbucket", + BITBUCKET_SERVER_URL="https://bitbucket.example.com", + ) + def test_resolve_bitbucket_server_url(self): + data = self.gql_request("query { config { bitbucketServerURL }}") + assert data == { + "config": { + "bitbucketServerURL": "https://bitbucket.example.com", + }, + } + + @override_settings( + IS_ENTERPRISE=False, + ) + def test_resolve_null_bitbucket_sever_url(self): + data = self.gql_request("query { config { bitbucketServerURL }}") + assert data == { + "config": { + "bitbucketServerURL": None, + }, + } + + @override_settings(IS_ENTERPRISE=False) + def test_self_hosted_license_returns_null_if_not_enterprise(self): + data = self.gql_request( + "query { config { selfHostedLicense { expirationDate } } }" + ) + assert data == { + "config": { + "selfHostedLicense": None, + }, + } + + @override_settings(IS_ENTERPRISE=True) + @patch("services.self_hosted.get_current_license") + def test_self_hosted_license_returns_null_if_invalid_license(self, license_mock): + license_mock.return_value = LicenseInformation(is_valid=False) + data = self.gql_request( + "query { config { selfHostedLicense { expirationDate } } }" + ) + assert data == { + "config": { + "selfHostedLicense": None, + }, + } + + @override_settings(IS_ENTERPRISE=True) + @patch("services.self_hosted.get_current_license") + def test_self_hosted_license_returns_expiration_date_if_valid_license( + self, license_mock + ): + license_mock.return_value = LicenseInformation( + is_valid=True, + message=None, + url=None, + number_allowed_users=5, + number_allowed_repos=None, + expires=datetime.strptime("2020-05-09 00:00:00", "%Y-%m-%d %H:%M:%S"), + is_trial=True, + is_pr_billing=False, + ) + data = self.gql_request( + "query { config { selfHostedLicense { expirationDate } } }" + ) + assert data == { + "config": { + "selfHostedLicense": {"expirationDate": "2020-05-09T00:00:00"}, + }, + } + + @override_settings( + GITHUB_CLIENT_ID="Github", + GITHUB_ENTERPRISE_CLIENT_ID="Github Enterprise", + GITLAB_CLIENT_ID="Gitlab", + GITLAB_ENTERPRISE_CLIENT_ID="Gitlab Enterprise", + BITBUCKET_CLIENT_ID="Bitbucket", + BITBUCKET_SERVER_CLIENT_ID="Bitbucket Server", + OKTA_OAUTH_CLIENT_ID="Okta", + DISABLE_GIT_BASED_LOGIN=True, + ) + def test_sync_providers(self): + data = self.gql_request("query { config { syncProviders } }") + assert data == { + "config": { + "syncProviders": [ + "GITHUB", + "GITHUB_ENTERPRISE", + "GITLAB", + "GITLAB_ENTERPRISE", + "BITBUCKET", + "BITBUCKET_SERVER", + ], + }, + } diff --git a/apps/codecov-api/graphql_api/tests/test_coverage_analytics.py b/apps/codecov-api/graphql_api/tests/test_coverage_analytics.py new file mode 100644 index 0000000000..bf31fb819e --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_coverage_analytics.py @@ -0,0 +1,526 @@ +import datetime +from typing import Any, Dict, Optional + +from django.test import TestCase, override_settings +from django.utils import timezone +from freezegun import freeze_time +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) + +from billing.helpers import mock_all_plans_and_tiers +from core.models import Commit, Repository +from graphql_api.tests.helper import GraphQLTestHelper +from graphql_api.types.coverage_analytics.coverage_analytics import ( + CoverageAnalyticsProps, + resolve_coverage_analytics_result_type, +) +from graphql_api.types.errors.errors import NotFoundError + + +class TestFetchCoverageAnalytics(GraphQLTestHelper, TestCase): + # SETUP + def setUp(self) -> None: + self.owner = OwnerFactory(username="codecov-user") + self.yaml = {"test": "test"} + + # some field resolvers require access to postgres or timeseries db + databases = {"default", "timeseries"} + + # HELPERS + def run_gql_query(self, query: str, variables: Dict[str, Any]) -> Dict[str, Any]: + owner = self.owner + # Use the gql_request method from the parent class (GraphQLTestHelper) + return super().gql_request(query=query, owner=owner, variables=variables) + + def create_repository(self, name: str) -> Repository: + return RepositoryFactory( + author=self.owner, + active=True, + private=True, + name=name, + yaml=self.yaml, + language="erlang", + languages=[], + ) + + @staticmethod + def create_commit( + repository: Repository, + coverage_totals: Dict[str, int], + timestamp: Optional[datetime.datetime] = None, + ) -> Commit: + if timestamp is None: + timestamp = timezone.now() + return CommitFactory( + repository=repository, totals=coverage_totals, timestamp=timestamp + ) + + query_builder = """ + query Repository($name: String!){ + me { + owner { + repository(name: $name) { + __typename + ... on Repository { + coverageAnalytics { + %s + } + } + ... on ResolverError { + message + } + } + } + } + } + """ + + # TESTS + def test_coverage_analytics_base_fields(self) -> None: + """Test case where to fetch coverage analytics fields""" + + # Create repo, commit, and coverage data + repo = self.create_repository("myname") + hour_ago = timezone.make_aware(datetime.datetime(2020, 12, 31, 23, 0)) + coverage_commit = self.create_commit( + repository=repo, + coverage_totals={"c": 75, "h": 30, "m": 10, "n": 40}, + timestamp=hour_ago, + ) + self.create_commit(repository=repo, coverage_totals={"c": 85}) + repo.updatestamp = timezone.now() + repo.save() + self.assertTrue(repo.pk, "Repository should be saved and have a primary key.") + + # Set up the GraphQL query and run + query = """ + query CoverageAnalytics($owner: String!, $repo: String!) { + owner(username: $owner) { + repository(name: $repo) { + __typename + ... on Repository { + name + coverageAnalytics { + percentCovered + commitSha + hits + misses + lines + } + } + ... on ResolverError { + message + } + } + } + } + """ + variables = {"owner": self.owner.username, "repo": repo.name} + resp = self.run_gql_query(query=query, variables=variables) + + # Assert the response matches + expected_response = { + "__typename": "Repository", + "name": repo.name, + "coverageAnalytics": { + "percentCovered": 75, + "commitSha": coverage_commit.commitid, + "hits": 30, + "misses": 10, + "lines": 40, + }, + } + assert resp["owner"]["repository"] == expected_response + + def test_coverage_analytics_base_fields_partial(self) -> None: + """Test case where the query only expects one of the fields in CoverageAnalytics""" + + # Create repo and a single commit + repo = self.create_repository("testtest") + hour_ago = timezone.make_aware(datetime.datetime(2020, 12, 31, 23, 0)) + self.create_commit( + repository=repo, + coverage_totals={"c": 75, "h": 30, "m": 10, "n": 40}, + timestamp=hour_ago, + ) + repo.updatestamp = timezone.now() + repo.save() + self.assertTrue(repo.pk, "Repository should be saved and have a primary key.") + + # Set up the GraphQL query and run - requests only the `percentCovered` field + query = """ + query CoverageAnalytics($owner: String!, $repo: String!) { + owner(username: $owner) { + repository(name: $repo) { + __typename + ... on Repository { + coverageAnalytics { + percentCovered + } + } + ... on ResolverError { + message + } + } + } + } + """ + variables = {"owner": "codecov-user", "repo": repo.name} + resp = self.run_gql_query(query=query, variables=variables) + + # Assert the response matches the expected percentCovered value + assert resp["owner"]["repository"]["coverageAnalytics"]["percentCovered"] == 75 + + def test_coverage_analytics_no_commit(self) -> None: + """Test case where no commits exist for coverage data""" + + # Create repo without commits + repo = self.create_repository("empty-repo") + repo.updatestamp = timezone.now() + repo.save() + self.assertTrue(repo.pk, "Repository should be saved and have a primary key.") + + # Set up the GraphQL query and run + query = """ + query CoverageAnalytics($owner: String!, $repo: String!) { + owner(username: $owner) { + repository(name: $repo) { + __typename + ... on Repository { + coverageAnalytics { + percentCovered + commitSha + hits + misses + lines + } + } + ... on ResolverError { + message + } + } + } + } + """ + variables = {"owner": "codecov-user", "repo": repo.name} + resp = self.run_gql_query(query=query, variables=variables) + + # Assert the response matches the expected structure with `None` values + assert resp["owner"]["repository"]["coverageAnalytics"] == { + "percentCovered": None, + "commitSha": None, + "hits": None, + "misses": None, + "lines": None, + } + + def test_coverage_analytics_resolves_to_error(self) -> None: + """Test case where the query resolves to an error (e.g., repository not found)""" + + # Set up and run the query to simulate a repository that doesn't exist + query = """ + query CoverageAnalytics($owner: String!, $repo: String!) { + owner(username: $owner) { + repository(name: $repo) { + __typename + ... on Repository { # Use an inline fragment for the Repository type + coverageAnalytics { + percentCovered + } + } + ... on ResolverError { + message + } + } + } + } + """ + variables = {"owner": "codecov-user", "repo": "non-existent-repo"} + coverage_data = self.run_gql_query(query=query, variables=variables) + + # Assert that the response resolves to an error + assert coverage_data["owner"]["repository"]["__typename"] == "NotFoundError" + assert coverage_data["owner"]["repository"]["message"] == "Not found" + + @freeze_time("2022-01-02") + def test_coverage_analytics_with_interval(self): + """Test with interval argument to fetch coverage data in a specific time range""" + + mock_all_plans_and_tiers() + # Create data to populate the timeseries graph + repo = self.create_repository("test-repo") + one_day_ago = timezone.make_aware(datetime.datetime(2022, 1, 1, 0, 0)) + self.create_commit( + repository=repo, + coverage_totals={"c": 65, "h": 20, "m": 5, "n": 25}, + timestamp=one_day_ago, + ) + + two_days_ago = timezone.make_aware(datetime.datetime(2022, 1, 2, 0, 0)) + self.create_commit( + repository=repo, + coverage_totals={"c": 75, "h": 30, "m": 10, "n": 40}, + timestamp=two_days_ago, + ) + + repo.updatestamp = timezone.now() + repo.save() + self.assertTrue(repo.pk, "Repository should be saved and have a primary key.") + + # Set up GraphQL query and run + query = """ + query CoverageAnalytics($owner:String!, $repo: String!, $interval: MeasurementInterval!) { + owner(username:$owner) { + repository(name: $repo) { + __typename + ... on Repository { + name + coverageAnalytics { + measurements(interval: $interval) { + timestamp + avg + min + max + } + } + } + ... on ResolverError { + message + } + } + } + } + """ + variables = { + "owner": "codecov-user", + "repo": repo.name, + "interval": "INTERVAL_1_DAY", + } + resp = self.run_gql_query(query=query, variables=variables) + + expected_response = { + "__typename": "Repository", + "name": repo.name, + "coverageAnalytics": { + "measurements": [ + { + "avg": 65.0, + "max": 65.0, + "min": 65.0, + "timestamp": "2022-01-01T00:00:00+00:00", + }, + { + "avg": 75.0, + "max": 75.0, + "min": 75.0, + "timestamp": "2022-01-02T00:00:00+00:00", + }, + ] + }, + } + + assert resp["owner"]["repository"] == expected_response + + def test_resolve_coverage_analytics_result_type_for_coverage_analytics_props( + self, + ) -> None: + """Test that the resolver returns 'CoverageAnalyticsProps' when passed a CoverageAnalyticsProps object""" + repo = self.create_repository("test") + coverage_analytics_props = CoverageAnalyticsProps(repository=repo) + result_type = resolve_coverage_analytics_result_type(coverage_analytics_props) + self.assertEqual(result_type, "CoverageAnalyticsProps") + + def test_resolve_coverage_analytics_result_type_for_not_found_error(self) -> None: + """Test that the resolver returns 'NotFoundError' when passed a NotFoundError object""" + result_type = resolve_coverage_analytics_result_type(NotFoundError()) + self.assertEqual(result_type, "NotFoundError") + + def test_resolve_coverage_analytics_result_type_for_unexpected_type(self) -> None: + """Test that the resolver returns None when passed an object of an unexpected type""" + unexpected_object = "unexpected_string" + result_type = resolve_coverage_analytics_result_type(unexpected_object) + self.assertIsNone(result_type) + + @override_settings(TIMESERIES_ENABLED=False) + def test_repository_flags_metadata(self): + user = OwnerFactory() + repo = RepositoryFactory(author=user) + data = self.gql_request( + self.query_builder + % """ + flagsMeasurementsActive + flagsMeasurementsBackfilled + """, + owner=user, + variables={"name": repo.name}, + ) + assert ( + data["me"]["owner"]["repository"]["coverageAnalytics"][ + "flagsMeasurementsActive" + ] + == False + ) + assert ( + data["me"]["owner"]["repository"]["coverageAnalytics"][ + "flagsMeasurementsBackfilled" + ] + == False + ) + + @override_settings(TIMESERIES_ENABLED=False) + def test_repository_components_metadata(self): + user = OwnerFactory() + repo = RepositoryFactory(author=user) + data = self.gql_request( + self.query_builder + % """ + componentsMeasurementsActive + componentsMeasurementsBackfilled + """, + owner=user, + variables={"name": repo.name}, + ) + assert ( + data["me"]["owner"]["repository"]["coverageAnalytics"][ + "componentsMeasurementsActive" + ] + == False + ) + assert ( + data["me"]["owner"]["repository"]["coverageAnalytics"][ + "componentsMeasurementsBackfilled" + ] + == False + ) + + def test_repository_has_components_count(self): + repo = RepositoryFactory( + author=self.owner, + active=True, + private=True, + yaml={ + "component_management": { + "default_rules": {}, + "individual_components": [ + {"component_id": "blah", "paths": [r".*\.go"]}, + {"component_id": "cool_rules"}, + ], + } + }, + ) + + data = self.gql_request( + self.query_builder + % """ + componentsCount + """, + owner=self.owner, + variables={"name": repo.name}, + ) + + assert ( + data["me"]["owner"]["repository"]["coverageAnalytics"]["componentsCount"] + == 2 + ) + + def test_repository_no_components_count(self): + repo = RepositoryFactory( + author=self.owner, + active=True, + private=True, + yaml={"component_management": {}}, + ) + + data = self.gql_request( + self.query_builder + % """ + componentsCount + """, + owner=self.owner, + variables={"name": repo.name}, + ) + + assert ( + data["me"]["owner"]["repository"]["coverageAnalytics"]["componentsCount"] + == 0 + ) + + def test_repository_components_select(self): + repo = RepositoryFactory( + author=self.owner, + active=True, + private=True, + yaml={ + "component_management": { + "default_rules": {}, + "individual_components": [ + { + "component_id": "blah", + "paths": [r".*\.go"], + "name": "blah_name", + }, + {"component_id": "cool_rules", "name": "cool_name"}, + ], + } + }, + ) + + data = self.gql_request( + self.query_builder + % """ + componentsYaml(termId: null) { + id + name + } + """, + owner=self.owner, + variables={"name": repo.name}, + ) + + assert data["me"]["owner"]["repository"]["coverageAnalytics"][ + "componentsYaml" + ] == [ + {"id": "blah", "name": "blah_name"}, + {"id": "cool_rules", "name": "cool_name"}, + ] + + def test_repository_components_select_with_search(self): + repo = RepositoryFactory( + author=self.owner, + active=True, + private=True, + yaml={ + "component_management": { + "default_rules": {}, + "individual_components": [ + { + "component_id": "blah", + "paths": [r".*\.go"], + "name": "blah_name", + }, + {"component_id": "cool_rules", "name": "cool_name"}, + ], + } + }, + ) + + data = self.gql_request( + self.query_builder + % """ + componentsYaml(termId: "blah") { + id + name + } + """, + owner=self.owner, + variables={"name": repo.name}, + ) + + assert data["me"]["owner"]["repository"]["coverageAnalytics"][ + "componentsYaml" + ] == [ + {"id": "blah", "name": "blah_name"}, + ] diff --git a/apps/codecov-api/graphql_api/tests/test_coverage_analytics_measurements.py b/apps/codecov-api/graphql_api/tests/test_coverage_analytics_measurements.py new file mode 100644 index 0000000000..dc7abc1a21 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_coverage_analytics_measurements.py @@ -0,0 +1,141 @@ +import datetime +from unittest.mock import patch + +from django.test import TestCase, override_settings +from django.utils import timezone +from shared.django_apps.core.tests.factories import ( + OwnerFactory, + RepositoryFactory, +) + +from timeseries.models import Interval + +from .helper import GraphQLTestHelper + + +@patch("timeseries.helpers.repository_coverage_measurements_with_fallback") +class TestMeasurement(TestCase, GraphQLTestHelper): + def _request(self, variables=None): + query = f""" + query Measurements($branch: String) {{ + owner(username: "{self.org.username}") {{ + repository(name: "{self.repo.name}") {{ + ... on Repository {{ + coverageAnalytics {{ + measurements( + interval: INTERVAL_1_DAY + after: "2022-01-01" + before: "2022-01-03" + branch: $branch + ) {{ + timestamp + avg + min + max + }} + }} + }} + }} + }} + }} + """ + data = self.gql_request(query, owner=self.owner, variables=variables) + return data["owner"]["repository"]["coverageAnalytics"]["measurements"] + + def setUp(self): + self.org = OwnerFactory(username="test-org") + self.repo = RepositoryFactory( + name="test-repo", + author=self.org, + private=True, + ) + self.owner = OwnerFactory(permission=[self.repo.pk]) + + @override_settings(TIMESERIES_ENABLED=True) + def test_measurements_timeseries_enabled( + self, repository_coverage_measurements_with_fallback + ): + repository_coverage_measurements_with_fallback.return_value = [ + { + "timestamp_bin": datetime.datetime(2022, 1, 1), + "min": 1, + "max": 2, + "avg": 1.5, + }, + { + "timestamp_bin": datetime.datetime(2022, 1, 2), + "min": 3, + "max": 4, + "avg": 3.5, + }, + ] + + assert self._request() == [ + {"timestamp": "2022-01-01T00:00:00", "min": 1.0, "max": 2.0, "avg": 1.5}, + {"timestamp": "2022-01-02T00:00:00", "min": 3.0, "max": 4.0, "avg": 3.5}, + { + "timestamp": "2022-01-03T00:00:00+00:00", + "min": None, + "max": None, + "avg": None, + }, + ] + + repository_coverage_measurements_with_fallback.assert_called_once_with( + self.repo, + Interval.INTERVAL_1_DAY, + start_date=datetime.datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + end_date=datetime.datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + branch=None, + ) + + @override_settings(TIMESERIES_ENABLED=False) + def test_measurements_timeseries_not_enabled( + self, repository_coverage_measurements_with_fallback + ): + repository_coverage_measurements_with_fallback.return_value = [ + { + "timestamp_bin": datetime.datetime(2022, 1, 1), + "min": 1, + "max": 2, + "avg": 1.5, + }, + { + "timestamp_bin": datetime.datetime(2022, 1, 2), + "min": 3, + "max": 4, + "avg": 3.5, + }, + ] + + assert self._request() == [ + {"timestamp": "2022-01-01T00:00:00", "min": 1.0, "max": 2.0, "avg": 1.5}, + {"timestamp": "2022-01-02T00:00:00", "min": 3.0, "max": 4.0, "avg": 3.5}, + { + "timestamp": "2022-01-03T00:00:00+00:00", + "min": None, + "max": None, + "avg": None, + }, + ] + + repository_coverage_measurements_with_fallback.assert_called_once_with( + self.repo, + Interval.INTERVAL_1_DAY, + start_date=datetime.datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + end_date=datetime.datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + branch=None, + ) + + @override_settings(TIMESERIES_ENABLED=True) + def test_measurements_branch(self, repository_coverage_measurements_with_fallback): + repository_coverage_measurements_with_fallback.return_value = [] + self._request(variables={"branch": "foo"}) + + repository_coverage_measurements_with_fallback.assert_called_once_with( + self.repo, + Interval.INTERVAL_1_DAY, + start_date=datetime.datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + end_date=datetime.datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + branch="foo", + ) diff --git a/apps/codecov-api/graphql_api/tests/test_current_user_ariadne.py b/apps/codecov-api/graphql_api/tests/test_current_user_ariadne.py new file mode 100644 index 0000000000..d22aa9f3ba --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_current_user_ariadne.py @@ -0,0 +1,579 @@ +import datetime +from http.cookies import SimpleCookie +from unittest.mock import patch + +from django.test import TestCase +from shared.django_apps.codecov_auth.tests.factories import ( + AccountFactory, + OktaSettingsFactory, + UserFactory, +) +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) + +from codecov_auth.models import OwnerProfile + +from .helper import GraphQLTestHelper, paginate_connection + + +class ArianeTestCase(GraphQLTestHelper, TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + random_owner = OwnerFactory(username="random-user") + RepositoryFactory(author=self.owner, active=True, private=True, name="a") + RepositoryFactory(author=self.owner, active=True, private=False, name="b") + RepositoryFactory(author=random_owner, active=True, private=True, name="not") + self.owner.organizations = [ + OwnerFactory(username="codecov").ownerid, + OwnerFactory(username="facebook").ownerid, + OwnerFactory(username="spotify").ownerid, + ] + self.owner.save() + + def test_when_unauthenticated(self): + query = "{ me { user { username }} }" + data = self.gql_request(query) + assert data == {"me": None} + + def test_when_authenticated(self): + query = "{ me { user { username avatarUrl }} }" + data = self.gql_request(query, owner=self.owner) + assert data == { + "me": { + "user": { + "username": self.owner.username, + "avatarUrl": self.owner.avatar_url, + } + } + } + + def test_when_tracking_metadata(self): + query = "{ me { trackingMetadata { ownerid } } }" + data = self.gql_request(query, owner=self.owner) + assert data == {"me": {"trackingMetadata": {"ownerid": self.owner.ownerid}}} + + def test_when_tracking_metadata_profile(self): + query = """ + { + me { + trackingMetadata { + ownerid + profile { goals } + } + } + } + """ + OwnerProfile.objects.filter(owner_id=self.owner.ownerid).update( + goals=["IMPROVE_COVERAGE"] + ) + data = self.gql_request(query, owner=self.owner) + assert data == { + "me": { + "trackingMetadata": { + "ownerid": self.owner.ownerid, + "profile": {"goals": ["IMPROVE_COVERAGE"]}, + } + } + } + + def test_when_tracking_metadata_no_profile(self): + query = """ + { + me { + trackingMetadata { + ownerid + profile { goals } + } + } + } + """ + data = self.gql_request(query, owner=self.owner) + assert data == { + "me": { + "trackingMetadata": { + "ownerid": self.owner.ownerid, + "profile": {"goals": []}, + } + } + } + + # Applies for old users that didn't get their owner profiles created w/ their owner + def test_when_owner_profile_doesnt_exist(self): + query = """ + { + me { + trackingMetadata { + ownerid + profile { goals } + } + } + } + """ + owner = OwnerFactory(username="another-user") + owner.profile.delete() + data = self.gql_request(query, owner=owner) + assert data == { + "me": { + "trackingMetadata": { + "ownerid": owner.ownerid, + "profile": None, + } + } + } + + def test_private_access_when_private_access_field_is_null(self): + current_user = OwnerFactory(private_access=None) + query = """{ + me { + privateAccess + } + } + """ + data = self.gql_request(query, owner=current_user) + assert data == {"me": {"privateAccess": False}} + + def test_private_access_when_private_access_field_is_false(self): + current_user = OwnerFactory(private_access=False) + query = """{ + me { + privateAccess + } + } + """ + data = self.gql_request(query, owner=current_user) + assert data == {"me": {"privateAccess": False}} + + def test_private_access_when_private_access_field_is_true(self): + current_user = OwnerFactory(private_access=True) + query = """{ + me { + privateAccess + } + } + """ + data = self.gql_request(query, owner=current_user) + assert data == {"me": {"privateAccess": True}} + + def test_fetch_terms_agreement_and_business_email_when_owner_profile_and_user_is_not_null( + self, + ): + current_user = OwnerFactory( + private_access=True, business_email="testEmail@gmail.com" + ) + current_user.user.terms_agreement = True + current_user.user.save() + query = """{ + me { + businessEmail + termsAgreement + } + } + """ + data = self.gql_request(query, owner=current_user) + assert data == { + "me": {"businessEmail": "testEmail@gmail.com", "termsAgreement": True} + } + + def test_fetch_terms_agreement_and_business_email_when_owner_profile_is_null(self): + current_user = OwnerFactory(private_access=True, business_email=None) + current_user.profile.delete() + query = """{ + me { + businessEmail + termsAgreement + } + } + """ + data = self.gql_request(query, owner=current_user) + assert data == {"me": {"businessEmail": None, "termsAgreement": False}} + + def test_fetch_null_terms_agreement_for_user_without_owner(self): + # There is an edge where an owner without user can call the "me" endpoint + # via impersonation, in that case return null for terms agreement + owner_to_impersonate = OwnerFactory() + owner_to_impersonate.user.delete() + self.client.cookies = SimpleCookie({"staff_user": owner_to_impersonate.pk}) + self.client.force_login(user=UserFactory(is_staff=True)) + + query = """{ + me { + termsAgreement + } + } + """ + res = self.client.post( + "/graphql/gh", + {"query": query}, + content_type="application/json", + ) + assert res.json()["data"]["me"] == {"termsAgreement": None} + + def test_fetching_viewable_repositories(self): + org_1 = OwnerFactory() + org_2 = OwnerFactory() + + authed_account = AccountFactory() + OktaSettingsFactory(account=authed_account, enforced=True) + okta_enforced_authenticated = OwnerFactory(account=authed_account) + + unauthed_account = AccountFactory() + okta_enforced_org_unauth = OwnerFactory(account=unauthed_account) + OktaSettingsFactory(account=unauthed_account, enforced=True) + + current_user = OwnerFactory( + organizations=[ + org_1.ownerid, + okta_enforced_authenticated.ownerid, + okta_enforced_org_unauth.ownerid, + ] + ) + + repos_in_db = [ + RepositoryFactory(private=True, name="0"), + RepositoryFactory(author=org_1, private=False, name="1"), + RepositoryFactory(author=org_1, private=True, name="2"), + RepositoryFactory(author=org_2, private=False, name="3"), + RepositoryFactory(author=org_2, private=True, name="4"), + RepositoryFactory(private=True, name="5"), + RepositoryFactory(author=current_user, private=True, name="6"), + RepositoryFactory(author=current_user, private=False, name="7"), + RepositoryFactory( + author=okta_enforced_authenticated, + private=True, + name="okta_enforced_repo_authed", + ), + RepositoryFactory( + author=okta_enforced_org_unauth, + private=True, + name="okta_enforced_repo_unauthed", + ), + ] + current_user.permission = [ + repos_in_db[2].repoid, + repos_in_db[8].repoid, + repos_in_db[9].repoid, + ] + current_user.save() + query = """{ + me { + viewableRepositories { + edges { + node { + name + } + } + } + } + } + """ + data = self.gql_request( + query, owner=current_user, okta_signed_in_accounts=[authed_account.id] + ) + repos = paginate_connection(data["me"]["viewableRepositories"]) + repos_name = [repo["name"] for repo in repos] + assert ( + sorted(repos_name) + == [ + "1", # public repo in org of user + "2", # private repo in org of user and in user permission + "6", # personal private repo + "7", # personal public repo + "okta_enforced_repo_authed", # private repo in org with Okta Enforced permissions + ] + ) + + # Test with impersonation + data = self.gql_request(query, owner=current_user, impersonate_owner=True) + repos = paginate_connection(data["me"]["viewableRepositories"]) + repos_name = [repo["name"] for repo in repos] + assert ( + sorted(repos_name) + == [ + "1", # public repo in org of user + "2", # private repo in org of user and in user permission + "6", # personal private repo + "7", # personal public repo + "okta_enforced_repo_authed", # Okta repo should show up for impersonated users + "okta_enforced_repo_unauthed", # Okta repo should show up for impersonated users + ] + ) + + def test_fetching_viewable_repositories_ordering(self): + current_user = OwnerFactory() + query = """ + query MeOrderingRespoitories($orderingDirection: OrderingDirection, $ordering: RepositoryOrdering) { + me { + viewableRepositories(orderingDirection: $orderingDirection, ordering: $ordering) { + edges { + node { + name + } + } + } + } + } + """ + + repo_1 = RepositoryFactory(author=current_user, name="A") + repo_2 = RepositoryFactory(author=current_user, name="B") + RepositoryFactory(author=current_user, name="C") + + with self.subTest("No ordering (defaults to order by repoid)"): + with self.subTest("no ordering Direction"): + data = self.gql_request(query, owner=current_user) + repos = paginate_connection(data["me"]["viewableRepositories"]) + repos_name = [repo["name"] for repo in repos] + self.assertEqual(repos_name, ["A", "B", "C"]) + + with self.subTest("ASC"): + data = self.gql_request( + query, owner=current_user, variables={"orderingDirection": "ASC"} + ) + repos = paginate_connection(data["me"]["viewableRepositories"]) + repos_name = [repo["name"] for repo in repos] + self.assertEqual(repos_name, ["A", "B", "C"]) + + with self.subTest("DESC"): + data = self.gql_request( + query, owner=current_user, variables={"orderingDirection": "DESC"} + ) + repos = paginate_connection(data["me"]["viewableRepositories"]) + repos_name = [repo["name"] for repo in repos] + self.assertEqual(repos_name, ["C", "B", "A"]) + + with self.subTest("NAME"): + with self.subTest("no ordering Direction"): + data = self.gql_request( + query, owner=current_user, variables={"ordering": "NAME"} + ) + repos = paginate_connection(data["me"]["viewableRepositories"]) + repos_name = [repo["name"] for repo in repos] + self.assertEqual(repos_name, ["A", "B", "C"]) + + with self.subTest("ASC"): + data = self.gql_request( + query, + owner=current_user, + variables={"ordering": "NAME", "orderingDirection": "ASC"}, + ) + repos = paginate_connection(data["me"]["viewableRepositories"]) + repos_name = [repo["name"] for repo in repos] + self.assertEqual(repos_name, ["A", "B", "C"]) + + with self.subTest("DESC"): + data = self.gql_request( + query, + owner=current_user, + variables={"ordering": "NAME", "orderingDirection": "DESC"}, + ) + repos = paginate_connection(data["me"]["viewableRepositories"]) + repos_name = [repo["name"] for repo in repos] + self.assertEqual(repos_name, ["C", "B", "A"]) + + with self.subTest("COMMIT_DATE"): + with self.subTest("no ordering Direction"): + data = self.gql_request( + query, owner=current_user, variables={"ordering": "COMMIT_DATE"} + ) + repos = paginate_connection(data["me"]["viewableRepositories"]) + repos_name = [repo["name"] for repo in repos] + self.assertEqual(repos_name, ["A", "B", "C"]) + + with self.subTest("ASC"): + data = self.gql_request( + query, + owner=current_user, + variables={"ordering": "COMMIT_DATE", "orderingDirection": "ASC"}, + ) + repos = paginate_connection(data["me"]["viewableRepositories"]) + repos_name = [repo["name"] for repo in repos] + self.assertEqual(repos_name, ["A", "B", "C"]) + + with self.subTest("DESC"): + data = self.gql_request( + query, + owner=current_user, + variables={"ordering": "COMMIT_DATE", "orderingDirection": "DESC"}, + ) + repos = paginate_connection(data["me"]["viewableRepositories"]) + repos_name = [repo["name"] for repo in repos] + self.assertEqual(repos_name, ["C", "B", "A"]) + + with self.subTest("COVERAGE"): + hour_ago = datetime.datetime.now() - datetime.timedelta(hours=1) + CommitFactory(repository=repo_1, totals={"c": "42"}, timestamp=hour_ago) + CommitFactory(repository=repo_2, totals={"c": "100.2"}, timestamp=hour_ago) + + # too recent, should not be considered + CommitFactory(repository=repo_2, totals={"c": "10"}) + + with self.subTest("no ordering Direction"): + data = self.gql_request( + query, owner=current_user, variables={"ordering": "COVERAGE"} + ) + repos = paginate_connection(data["me"]["viewableRepositories"]) + repos_name = [repo["name"] for repo in repos] + self.assertEqual(repos_name, ["C", "A", "B"]) + + with self.subTest("ASC"): + data = self.gql_request( + query, + owner=current_user, + variables={"ordering": "COVERAGE", "orderingDirection": "ASC"}, + ) + repos = paginate_connection(data["me"]["viewableRepositories"]) + repos_name = [repo["name"] for repo in repos] + self.assertEqual(repos_name, ["C", "A", "B"]) + + with self.subTest("DESC"): + data = self.gql_request( + query, + owner=current_user, + variables={"ordering": "COVERAGE", "orderingDirection": "DESC"}, + ) + repos = paginate_connection(data["me"]["viewableRepositories"]) + repos_name = [repo["name"] for repo in repos] + self.assertEqual(repos_name, ["B", "A", "C"]) + + def test_fetching_viewable_repositories_text_search(self): + query = """{ + me { + viewableRepositories(filters: { term: "a"}) { + edges { + node { + name + } + } + } + } + } + """ + data = self.gql_request(query, owner=self.owner) + repos = paginate_connection(data["me"]["viewableRepositories"]) + assert repos == [{"name": "a"}] + + def test_fetching_viewable_repositories_with_repo_names_search(self): + query = """{ + me { + viewableRepositories (filters: { repoNames: ["a", "b"] }) { + edges { + node { + name + } + } + } + } + } + """ + data = self.gql_request(query, owner=self.owner) + repos = paginate_connection(data["me"]["viewableRepositories"]) + assert repos == [{"name": "a"}, {"name": "b"}] + + def test_fetching_viewable_repositories_with_is_public(self): + query = """{{ + me {{ + viewableRepositories (filters: {{ {0} }}) {{ + edges {{ + node {{ + name + }} + }} + }} + }} + }} + """ + # isPublic=True (only public) -> b + data = self.gql_request(query.format("isPublic: true"), owner=self.owner) + repos = paginate_connection(data["me"]["viewableRepositories"]) + assert repos == [{"name": "b"}] + # isPublic=False (only private) -> a + data = self.gql_request(query.format("isPublic: false"), owner=self.owner) + repos = paginate_connection(data["me"]["viewableRepositories"]) + assert repos == [{"name": "a"}] + # isPublic is not specified (both public and private) -> a,b + data = self.gql_request(query.format(""), owner=self.owner) + repos = paginate_connection(data["me"]["viewableRepositories"]) + assert repos == [{"name": "a"}, {"name": "b"}] + + def test_fetching_my_orgs(self): + query = """{ + me { + myOrganizations { + edges { + node { + username + } + } + } + } + } + """ + data = self.gql_request(query, owner=self.owner) + orgs = paginate_connection(data["me"]["myOrganizations"]) + assert orgs == [ + {"username": "spotify"}, + {"username": "facebook"}, + {"username": "codecov"}, + ] + + def test_fetching_my_orgs_with_search(self): + query = """{ + me { + myOrganizations(filters: { term: "spot"}) { + edges { + node { + username + } + } + } + } + } + """ + data = self.gql_request(query, owner=self.owner) + orgs = paginate_connection(data["me"]["myOrganizations"]) + + assert orgs == [{"username": "spotify"}] + + def test_sync_repo_not_authenticated(self): + mutation = """ + mutation { + syncWithGitProvider { + error { + __typename + } + } + } + """ + mutation_data = self.gql_request(mutation) + assert ( + mutation_data["syncWithGitProvider"]["error"]["__typename"] + == "UnauthenticatedError" + ) + + @patch("codecov_auth.commands.owner.owner.OwnerCommands.is_syncing") + @patch("codecov_auth.commands.owner.owner.TriggerSyncInteractor.execute") + def test_sync_repo(self, mock_trigger_refresh, mock_is_refreshing): + mock_is_refreshing.return_value = True + query = """{ + me { + isSyncingWithGitProvider + } + } + """ + data = self.gql_request(query, owner=self.owner) + assert data["me"]["isSyncingWithGitProvider"] == True + mutation = """ + mutation { + syncWithGitProvider { + error { + __typename + } + } + } + """ + mutation_data = self.gql_request(mutation, owner=self.owner) + assert mutation_data["syncWithGitProvider"]["error"] is None + mock_trigger_refresh.assert_called() diff --git a/apps/codecov-api/graphql_api/tests/test_flags.py b/apps/codecov-api/graphql_api/tests/test_flags.py new file mode 100644 index 0000000000..d8b21d85a2 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_flags.py @@ -0,0 +1,847 @@ +from unittest.mock import patch + +import pytest +from django.conf import settings +from django.test import TestCase, override_settings +from django.utils import timezone +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) + +from reports.tests.factories import RepositoryFlagFactory +from timeseries.models import MeasurementName +from timeseries.tests.factories import DatasetFactory, MeasurementFactory + +from .helper import GraphQLTestHelper + +query_flags = """ +query Flags( + $org: String! + $repo: String! + $measurementsAfter: DateTime! + $measurementsBefore: DateTime! + $measurementsInterval: MeasurementInterval! +) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + coverageAnalytics { + flagsCount + flags { + edges { + node { + ...FlagFragment + } + } + } + } + } + } + } +} + +fragment FlagFragment on Flag { + name + percentCovered + percentChange + measurements( + interval: $measurementsInterval + after: $measurementsAfter + before: $measurementsBefore + ) { + timestamp + avg + min + max + } +} +""" + +query_repo = """ +query Repo( + $org: String! + $repo: String! +) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + coverageAnalytics { + flagsCount + flagsMeasurementsActive + flagsMeasurementsBackfilled + flags { + edges { + node { + measurements( + interval: INTERVAL_1_DAY + after: "2022-01-01", + before: "2022-12-31", + ) { + timestamp + avg + min + max + } + } + } + } + } + } + } + } +} +""" + + +@pytest.mark.skipif( + not settings.TIMESERIES_ENABLED, reason="requires timeseries data storage" +) +class TestFlags(GraphQLTestHelper, TestCase): + databases = {"default", "timeseries"} + + def setUp(self): + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org, private=False) + self.commit = CommitFactory(repository=self.repo) + + def test_fetch_flags_no_measurements(self): + RepositoryFlagFactory(repository=self.repo, flag_name="flag1") + RepositoryFlagFactory(repository=self.repo, flag_name="flag2") + RepositoryFlagFactory(repository=self.repo, flag_name="flag3", deleted=True) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "measurementsAfter": timezone.datetime(2022, 1, 1), + "measurementsBefore": timezone.datetime(2022, 12, 31), + "measurementsInterval": "INTERVAL_1_DAY", + } + data = self.gql_request(query_flags, variables=variables) + assert data == { + "owner": { + "repository": { + "coverageAnalytics": { + "flagsCount": 2, + "flags": { + "edges": [ + { + "node": { + "name": "flag1", + "percentCovered": None, + "percentChange": None, + "measurements": [], + } + }, + { + "node": { + "name": "flag2", + "percentCovered": None, + "percentChange": None, + "measurements": [], + } + }, + ] + }, + } + } + } + } + + @override_settings(TIMESERIES_ENABLED=False) + def test_fetch_flags_timeseries_not_enabled(self): + RepositoryFlagFactory(repository=self.repo, flag_name="flag1") + RepositoryFlagFactory(repository=self.repo, flag_name="flag2") + RepositoryFlagFactory(repository=self.repo, flag_name="flag3", deleted=True) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "measurementsAfter": timezone.datetime(2022, 1, 1), + "measurementsBefore": timezone.datetime(2022, 12, 31), + "measurementsInterval": "INTERVAL_1_DAY", + } + data = self.gql_request(query_flags, variables=variables) + assert data == { + "owner": { + "repository": { + "coverageAnalytics": { + "flagsCount": 2, + "flags": { + "edges": [ + { + "node": { + "name": "flag1", + "percentCovered": None, + "percentChange": None, + "measurements": [], + } + }, + { + "node": { + "name": "flag2", + "percentCovered": None, + "percentChange": None, + "measurements": [], + } + }, + ] + }, + } + } + } + } + + def test_fetch_flags_with_measurements(self): + flag1 = RepositoryFlagFactory(repository=self.repo, flag_name="flag1") + flag2 = RepositoryFlagFactory(repository=self.repo, flag_name="flag2") + RepositoryFlagFactory(repository=self.repo, flag_name="flag3", deleted=True) + MeasurementFactory( + name="flag_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id=str(flag1.pk), + commit_sha=self.commit.pk, + timestamp="2022-06-21T00:00:00", + value=75.0, + ) + MeasurementFactory( + name="flag_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id=str(flag1.pk), + commit_sha=self.commit.pk, + timestamp="2022-06-22T00:00:00", + value=75.0, + ) + MeasurementFactory( + name="flag_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id=str(flag1.pk), + commit_sha=self.commit.pk, + timestamp="2022-06-22T01:00:00", + value=85.0, + ) + MeasurementFactory( + name="flag_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id=str(flag2.pk), + commit_sha=self.commit.pk, + timestamp="2022-06-21T00:00:00", + value=85.0, + ) + MeasurementFactory( + name="flag_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id=str(flag2.pk), + commit_sha=self.commit.pk, + timestamp="2022-06-22T00:00:00", + value=95.0, + ) + MeasurementFactory( + name="flag_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id=str(flag2.pk), + commit_sha=self.commit.pk, + timestamp="2022-06-22T01:00:00", + value=85.0, + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "measurementsAfter": timezone.datetime(2022, 6, 20), + "measurementsBefore": timezone.datetime(2022, 6, 23), + "measurementsInterval": "INTERVAL_1_DAY", + } + data = self.gql_request(query_flags, variables=variables) + assert data == { + "owner": { + "repository": { + "coverageAnalytics": { + "flagsCount": 2, + "flags": { + "edges": [ + { + "node": { + "name": "flag1", + "percentCovered": 80.0, + "percentChange": 5.0, + "measurements": [ + { + "timestamp": "2022-06-20T00:00:00+00:00", + "avg": None, + "min": None, + "max": None, + }, + { + "timestamp": "2022-06-21T00:00:00+00:00", + "avg": 75.0, + "min": 75.0, + "max": 75.0, + }, + { + "timestamp": "2022-06-22T00:00:00+00:00", + "avg": 80.0, + "min": 75.0, + "max": 85.0, + }, + { + "timestamp": "2022-06-23T00:00:00+00:00", + "avg": None, + "min": None, + "max": None, + }, + ], + } + }, + { + "node": { + "name": "flag2", + "percentCovered": 90.0, + "percentChange": 5.0, + "measurements": [ + { + "timestamp": "2022-06-20T00:00:00+00:00", + "avg": None, + "min": None, + "max": None, + }, + { + "timestamp": "2022-06-21T00:00:00+00:00", + "avg": 85.0, + "min": 85.0, + "max": 85.0, + }, + { + "timestamp": "2022-06-22T00:00:00+00:00", + "avg": 90.0, + "min": 85.0, + "max": 95.0, + }, + { + "timestamp": "2022-06-23T00:00:00+00:00", + "avg": None, + "min": None, + "max": None, + }, + ], + } + }, + ] + }, + } + } + } + } + + def test_fetch_flags_with_measurements_day_alignment_30day(self): + flag = RepositoryFlagFactory(repository=self.repo, flag_name="flag1") + MeasurementFactory( + name="flag_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id=str(flag.pk), + commit_sha=self.commit.pk, + timestamp="2021-04-09T00:00:00", + value=75.0, + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "measurementsAfter": "2021-04-10T00:00:00", # this is in the middle of bin 1 + "measurementsBefore": "2021-04-20T00:00:00", # this is in the middle of bin 2 + "measurementsInterval": "INTERVAL_30_DAY", + } + data = self.gql_request(query_flags, variables=variables) + assert data == { + "owner": { + "repository": { + "coverageAnalytics": { + "flagsCount": 1, + "flags": { + "edges": [ + { + "node": { + "name": "flag1", + "percentCovered": 75, + "percentChange": None, + "measurements": [ + { + "timestamp": "2021-03-13T00:00:00+00:00", + "avg": 75.0, + "min": 75.0, + "max": 75.0, + }, + { + "timestamp": "2021-04-12T00:00:00+00:00", + "avg": None, + "min": None, + "max": None, + }, + ], + } + }, + ] + }, + } + } + } + } + + def test_fetch_flags_with_measurements_day_alignment_7day(self): + flag = RepositoryFlagFactory(repository=self.repo, flag_name="flag1") + MeasurementFactory( + name="flag_coverage", + owner_id=self.org.pk, + repo_id=self.repo.pk, + branch="main", + measurable_id=str(flag.pk), + commit_sha=self.commit.pk, + timestamp="2021-04-09T00:00:00", + value=75.0, + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "measurementsAfter": "2021-04-7T00:00:00", # this is in the middle of bin 1 + "measurementsBefore": "2021-04-15T00:00:00", # this is in the middle of bin 2 + "measurementsInterval": "INTERVAL_7_DAY", + } + data = self.gql_request(query_flags, variables=variables) + assert data == { + "owner": { + "repository": { + "coverageAnalytics": { + "flagsCount": 1, + "flags": { + "edges": [ + { + "node": { + "name": "flag1", + "percentCovered": 75, + "percentChange": None, + "measurements": [ + { + "timestamp": "2021-04-05T00:00:00+00:00", + "avg": 75.0, + "min": 75.0, + "max": 75.0, + }, + { + "timestamp": "2021-04-12T00:00:00+00:00", + "avg": None, + "min": None, + "max": None, + }, + ], + } + }, + ] + }, + } + } + } + } + + def test_fetch_flags_without_measurements(self): + query = """ + query Flags( + $org: String! + $repo: String! + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + coverageAnalytics { + flags { + edges { + node { + name + percentCovered + percentChange + } + } + } + } + } + } + } + } + """ + RepositoryFlagFactory(repository=self.repo, flag_name="flag1") + RepositoryFlagFactory(repository=self.repo, flag_name="flag2") + variables = { + "org": self.org.username, + "repo": self.repo.name, + } + data = self.gql_request(query, variables=variables) + assert data == { + "owner": { + "repository": { + "coverageAnalytics": { + "flags": { + "edges": [ + { + "node": { + "name": "flag1", + "percentCovered": None, + "percentChange": None, + } + }, + { + "node": { + "name": "flag2", + "percentCovered": None, + "percentChange": None, + } + }, + ] + } + } + } + } + } + + def test_fetch_flags_term_filter(self): + query = """ + query Flags( + $org: String! + $repo: String! + $filters: FlagSetFilters! + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + coverageAnalytics { + flags(filters: $filters) { + edges { + node { + name + } + } + } + } + } + } + } + } + """ + RepositoryFlagFactory(repository=self.repo, flag_name="flag1") + RepositoryFlagFactory(repository=self.repo, flag_name="flag2") + RepositoryFlagFactory( + repository=self.repo, flag_name="flag1-deleted", deleted=True + ) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "filters": {"term": "ag1"}, + } + data = self.gql_request(query, variables=variables) + assert data == { + "owner": { + "repository": { + "coverageAnalytics": { + "flags": { + "edges": [ + { + "node": { + "name": "flag1", + } + }, + ] + } + } + } + } + } + + def test_fetch_flags_filter_by_flags_names(self): + query = """ + query Flags( + $org: String! + $repo: String! + $filters: FlagSetFilters! + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + coverageAnalytics { + flags(filters: $filters) { + edges { + node { + name + } + } + } + } + } + } + } + } + """ + RepositoryFlagFactory(repository=self.repo, flag_name="flag1") + RepositoryFlagFactory(repository=self.repo, flag_name="flag2") + RepositoryFlagFactory(repository=self.repo, flag_name="flag3") + RepositoryFlagFactory(repository=self.repo, flag_name="flag4", deleted=True) + variables = { + "org": self.org.username, + "repo": self.repo.name, + "filters": {"flagsNames": ["flag1", "flag3", "flag4"]}, + } + data = self.gql_request(query, variables=variables) + assert data == { + "owner": { + "repository": { + "coverageAnalytics": { + "flags": { + "edges": [ + { + "node": { + "name": "flag1", + } + }, + { + "node": { + "name": "flag3", + } + }, + ] + } + } + } + } + } + + def test_fetch_flags_ordering_direction(self): + query = """ + query Flags( + $org: String! + $repo: String! + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + coverageAnalytics { + flags(orderingDirection: DESC) { + edges { + node { + name + } + } + } + } + } + } + } + } + """ + RepositoryFlagFactory(repository=self.repo, flag_name="flag1") + RepositoryFlagFactory(repository=self.repo, flag_name="flag2") + variables = { + "org": self.org.username, + "repo": self.repo.name, + } + data = self.gql_request(query, variables=variables) + assert data == { + "owner": { + "repository": { + "coverageAnalytics": { + "flags": { + "edges": [ + { + "node": { + "name": "flag2", + } + }, + { + "node": { + "name": "flag1", + } + }, + ] + } + } + } + } + } + + def test_fetch_flags_pagination(self): + query = """ + query Flags( + $org: String! + $repo: String! + $after: String + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + coverageAnalytics { + flags(after: $after) { + edges { + node { + name + } + cursor + } + } + } + } + } + } + } + """ + RepositoryFlagFactory(repository=self.repo, flag_name="flag1") + RepositoryFlagFactory(repository=self.repo, flag_name="flag2") + variables = { + "org": self.org.username, + "repo": self.repo.name, + } + data = self.gql_request(query, variables=variables) + assert data == { + "owner": { + "repository": { + "coverageAnalytics": { + "flags": { + "edges": [ + { + "node": { + "name": "flag1", + }, + "cursor": "ZmxhZzE=", + }, + { + "node": { + "name": "flag2", + }, + "cursor": "ZmxhZzI=", + }, + ] + } + } + } + } + } + variables = { + "org": self.org.username, + "repo": self.repo.name, + "after": "ZmxhZzE=", + } + data = self.gql_request(query, variables=variables) + assert data == { + "owner": { + "repository": { + "coverageAnalytics": { + "flags": { + "edges": [ + { + "node": { + "name": "flag2", + }, + "cursor": "ZmxhZzI=", + }, + ] + } + } + } + } + } + + @patch("timeseries.models.MeasurementSummary.agg_by") + def test_fetch_flags_empty_lookahead(self, agg_by): + query = """ + query Flags( + $org: String! + $repo: String! + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + coverageAnalytics { + flags { + __typename + } + } + } + } + } + } + """ + RepositoryFlagFactory(repository=self.repo, flag_name="flag1") + RepositoryFlagFactory(repository=self.repo, flag_name="flag2") + variables = { + "org": self.org.username, + "repo": self.repo.name, + } + self.gql_request(query, variables=variables) + assert agg_by.call_count == 0 + + def test_repository_flags_metadata_inactive(self): + data = self.gql_request( + query_repo, + variables={"org": self.org.username, "repo": self.repo.name}, + ) + assert ( + data["owner"]["repository"]["coverageAnalytics"]["flagsMeasurementsActive"] + == False + ) + assert ( + data["owner"]["repository"]["coverageAnalytics"][ + "flagsMeasurementsBackfilled" + ] + == False + ) + + def test_repository_flags_metadata_active(self): + DatasetFactory( + name=MeasurementName.FLAG_COVERAGE.value, + repository_id=self.repo.pk, + ) + + data = self.gql_request( + query_repo, + variables={"org": self.org.username, "repo": self.repo.name}, + ) + assert ( + data["owner"]["repository"]["coverageAnalytics"]["flagsMeasurementsActive"] + == True + ) + assert ( + data["owner"]["repository"]["coverageAnalytics"][ + "flagsMeasurementsBackfilled" + ] + == False + ) + + @patch("timeseries.models.Dataset.is_backfilled") + def test_repository_flags_metadata_backfilled_true(self, is_backfilled): + is_backfilled.return_value = True + + DatasetFactory( + name=MeasurementName.FLAG_COVERAGE.value, + repository_id=self.repo.pk, + ) + + data = self.gql_request( + query_repo, + variables={"org": self.org.username, "repo": self.repo.name}, + ) + assert ( + data["owner"]["repository"]["coverageAnalytics"]["flagsMeasurementsActive"] + == True + ) + assert ( + data["owner"]["repository"]["coverageAnalytics"][ + "flagsMeasurementsBackfilled" + ] + == True + ) diff --git a/apps/codecov-api/graphql_api/tests/test_impacted_file.py b/apps/codecov-api/graphql_api/tests/test_impacted_file.py new file mode 100644 index 0000000000..f81ca5ac2f --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_impacted_file.py @@ -0,0 +1,860 @@ +import hashlib +from dataclasses import dataclass, field +from typing import Callable +from unittest.mock import PropertyMock, patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine +from shared.torngit.exceptions import ( + TorngitClientGeneralError, + TorngitObjectNotFoundError, +) +from shared.utils.sessions import Session + +from compare.models import CommitComparison +from compare.tests.factories import CommitComparisonFactory +from services.comparison import ComparisonReport, ImpactedFile, MissingComparisonReport + +from .helper import GraphQLTestHelper + +query_impacted_files = """ +query ImpactedFiles( + $org: String! + $repo: String! + $commit: String! +) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + compareWithParent { + ... on Comparison { + impactedFilesCount + indirectChangedFilesCount + impactedFiles { + ... on ImpactedFiles { + results { + fileName + headName + baseName + isNewFile + isRenamedFile + isDeletedFile + baseCoverage { + percentCovered + } + headCoverage { + percentCovered + } + patchCoverage { + percentCovered + } + changeCoverage + missesCount + } + } + } + } + } + } + } + } + } + } +""" + +query_direct_changed_files_count = """ +query ImpactedFiles( + $org: String! + $repo: String! + $commit: String! +) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + compareWithParent { + ... on Comparison { + directChangedFilesCount + } + } + } + } + } + } +} +""" + +query_impacted_file_through_pull = """ +query ImpactedFile( + $org: String! + $repo: String! + $pull: Int! + $path: String! + $filters: SegmentsFilters +) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + pull(id: $pull) { + compareWithBase { + ... on Comparison { + state + impactedFile(path: $path) { + headName + baseName + hashedPath + baseCoverage { + percentCovered + } + headCoverage { + percentCovered + } + patchCoverage { + percentCovered + } + segments(filters: $filters) { + ... on SegmentComparisons { + results { + hasUnintendedChanges + } + } + ... on ResolverError { + message + } + } + } + } + } + } + } + } + } +} +""" +mock_data_from_archive = """ +{ + "files": [{ + "head_name": "fileA", + "base_name": "fileA", + "head_coverage": { + "hits": 12, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [ + [9,"h"], + [10,"m"] + ], + "unexpected_line_changes": [] + }, + { + "head_name": "fileB", + "base_name": "fileB", + "head_coverage": { + "hits": 12, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [ + [9,"h"], + [10,"h"], + [13,"h"], + [14,"h"], + [15,"h"], + [16,"m"], + [17,"h"] + ], + "unexpected_line_changes": [[[1, "h"], [1, "m"]]] + }] +} +""" + + +@dataclass +class MockSegment: + has_diff_changes: bool = False + has_unintended_changes: bool = False + remove_unintended_changes: Callable[[], None] = field(default=lambda: None) + + +class MockFileComparison(object): + def __init__(self): + self.segments = [ + MockSegment(has_unintended_changes=True, has_diff_changes=False), + MockSegment(has_unintended_changes=False, has_diff_changes=True), + MockSegment(has_unintended_changes=True, has_diff_changes=True), + ] + + +def sample_report(): + report = Report(flags={"flag1": True}) + first_file = ReportFile("foo/file1.py") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("bar/file2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + third_file = ReportFile("file3.py") + third_file.append(1, ReportLine.create(coverage=1, sessions=[[0, 1]])) + report.append(first_file) + report.append(second_file) + report.append(third_file) + report.add_session(Session(flags=["flag1"])) + return report + + +class TestImpactedFileFiltering(GraphQLTestHelper, TestCase): + def setUp(self): + self.org = OwnerFactory(username="codecov") + self.repo = RepositoryFactory(author=self.org, name="gazebo", private=False) + self.author = OwnerFactory() + self.parent_commit = CommitFactory(repository=self.repo) + self.commit = CommitFactory( + repository=self.repo, + totals={"c": "12", "diff": [0, 0, 0, 0, 0, "14"]}, + parent_commit_id=self.parent_commit.commitid, + ) + self.pull = PullFactory( + pullid=44, + repository=self.commit.repository, + head=self.commit.commitid, + base=self.parent_commit.commitid, + compared_to=self.parent_commit.commitid, + ) + self.comparison = CommitComparisonFactory( + base_commit=self.parent_commit, + compare_commit=self.commit, + state=CommitComparison.CommitComparisonStates.PROCESSED, + report_storage_path="v4/test.json", + ) + self.comparison_report = ComparisonReport(self.comparison) + + self.query_impacted_files = """ + query ImpactedFiles( + $org: String! + $repo: String! + $commit: String! + $filters: ImpactedFilesFilters + ) { + owner(username: $org) { + repository(name: $repo) { + ... on Repository { + commit(id: $commit) { + compareWithParent { + ... on Comparison { + impactedFiles(filters: $filters) { + ... on ImpactedFiles { + results { + fileName + } + } + ... on UnknownFlags { + message + } + } + } + } + } + } + } + } + } + """ + + @patch("shared.reports.api_report_service.build_report_from_commit") + @patch("shared.api_archive.archive.ArchiveService.read_file") + @patch("services.comparison.Comparison.git_comparison") + def test_filtering_with_successful_flags( + self, git_comparison_mock, read_file, build_report_from_commit + ): + git_comparison_mock.return_value = None + build_report_from_commit.return_value = sample_report() + read_file.return_value = mock_data_from_archive + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "filters": {"flags": ["flag1"]}, + } + + data = self.gql_request(self.query_impacted_files, variables=variables) + assert ( + "results" + in data["owner"]["repository"]["commit"]["compareWithParent"][ + "impactedFiles" + ] + ) + + @patch("shared.reports.api_report_service.build_report_from_commit") + @patch("shared.api_archive.archive.ArchiveService.read_file") + @patch("services.comparison.Comparison.git_comparison") + def test_filtering_with_unknown_flags( + self, git_comparison_mock, read_file, build_report_from_commit + ): + git_comparison_mock.return_value = None + build_report_from_commit.return_value = sample_report() + read_file.return_value = mock_data_from_archive + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + "filters": {"flags": ["fake_flag"]}, + } + + data = self.gql_request(self.query_impacted_files, variables=variables) + assert data["owner"]["repository"]["commit"]["compareWithParent"][ + "impactedFiles" + ] == {"message": "No coverage with chosen flags"} + + +class TestImpactedFile(GraphQLTestHelper, TestCase): + def setUp(self): + self.org = OwnerFactory(username="codecov") + self.repo = RepositoryFactory(author=self.org, name="gazebo", private=False) + self.author = OwnerFactory() + self.parent_commit = CommitFactory(repository=self.repo) + self.commit = CommitFactory( + repository=self.repo, + totals={"c": "12", "diff": [0, 0, 0, 0, 0, "14"]}, + parent_commit_id=self.parent_commit.commitid, + ) + self.pull = PullFactory( + pullid=44, + repository=self.commit.repository, + head=self.commit.commitid, + base=self.parent_commit.commitid, + compared_to=self.parent_commit.commitid, + ) + self.comparison = CommitComparisonFactory( + base_commit=self.parent_commit, + compare_commit=self.commit, + state=CommitComparison.CommitComparisonStates.PROCESSED, + report_storage_path="v4/test.json", + ) + self.comparison_report = ComparisonReport(self.comparison) + + # mock reports for all tests in this class + self.head_report_patcher = patch( + "services.comparison.Comparison.head_report", new_callable=PropertyMock + ) + self.head_report = self.head_report_patcher.start() + self.head_report.return_value = None + self.addCleanup(self.head_report_patcher.stop) + self.base_report_patcher = patch( + "services.comparison.Comparison.base_report", new_callable=PropertyMock + ) + self.base_report = self.base_report_patcher.start() + self.base_report.return_value = None + self.addCleanup(self.base_report_patcher.stop) + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_fetch_impacted_files(self, read_file): + read_file.return_value = mock_data_from_archive + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request(query_impacted_files, variables=variables) + assert data == { + "owner": { + "repository": { + "commit": { + "compareWithParent": { + "impactedFilesCount": 2, + "indirectChangedFilesCount": 1, + "impactedFiles": { + "results": [ + { + "fileName": "fileA", + "headName": "fileA", + "baseName": "fileA", + "isNewFile": False, + "isRenamedFile": False, + "isDeletedFile": False, + "baseCoverage": { + "percentCovered": 41.666666666666664 + }, + "headCoverage": { + "percentCovered": 85.71428571428571 + }, + "patchCoverage": {"percentCovered": 50.0}, + "changeCoverage": 44.047619047619044, + "missesCount": 1, + }, + { + "fileName": "fileB", + "headName": "fileB", + "baseName": "fileB", + "isNewFile": False, + "isRenamedFile": False, + "isDeletedFile": False, + "baseCoverage": { + "percentCovered": 41.666666666666664 + }, + "headCoverage": { + "percentCovered": 85.71428571428571 + }, + "patchCoverage": { + "percentCovered": 85.71428571428571 + }, + "changeCoverage": 44.047619047619044, + "missesCount": 2, + }, + ] + }, + } + } + } + } + } + + @patch("services.task.TaskService.compute_comparisons") + @patch("services.comparison.ComparisonReport.impacted_file") + @patch("services.comparison.Comparison.validate") + @patch("services.comparison.PullRequestComparison.get_file_comparison") + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_fetch_impacted_file_segments_without_comparison_in_context( + self, + read_file, + mock_get_file_comparison, + mock_compare_validate, + mock_impacted_file, + _, + ): + read_file.return_value = mock_data_from_archive + mock_get_file_comparison.return_value = MockFileComparison() + mock_compare_validate.return_value = True + mock_impacted_file.return_value = ImpactedFile( + **{ + "head_name": "fileB", + "base_name": "fileB", + "head_coverage": { + "hits": 12, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5, + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4, + }, + "added_diff_coverage": [ + [9, "h"], + [10, "m"], + [13, "p"], + [14, "h"], + [15, "h"], + [16, "h"], + [17, "h"], + ], + "unexpected_line_changes": [[[1, "h"], [1, "h"]]], + } + ) + self.comparison.delete() + variables = { + "org": self.org.username, + "repo": self.repo.name, + "pull": self.pull.pullid, + "path": "fileB", + } + data = self.gql_request(query_impacted_file_through_pull, variables=variables) + assert data == { + "owner": { + "repository": { + "pull": { + "compareWithBase": { + "state": "pending", + "impactedFile": { + "headName": "fileB", + "baseName": "fileB", + "hashedPath": "eea3f37743bfd3409bec556ab26d4698", + "baseCoverage": {"percentCovered": None}, + "headCoverage": {"percentCovered": None}, + "patchCoverage": {"percentCovered": 71.42857142857143}, + "segments": {"results": []}, + }, + } + } + } + } + } + + @patch("services.comparison.Comparison.validate") + @patch("services.comparison.PullRequestComparison.get_file_comparison") + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_fetch_impacted_file_with_segments( + self, read_file, mock_get_file_comparison, mock_compare_validate + ): + read_file.return_value = mock_data_from_archive + + mock_get_file_comparison.return_value = MockFileComparison() + mock_compare_validate.return_value = True + variables = { + "org": self.org.username, + "repo": self.repo.name, + "pull": self.pull.pullid, + "path": "fileB", + } + data = self.gql_request(query_impacted_file_through_pull, variables=variables) + assert data == { + "owner": { + "repository": { + "pull": { + "compareWithBase": { + "state": "processed", + "impactedFile": { + "headName": "fileB", + "baseName": "fileB", + "hashedPath": hashlib.md5("fileB".encode()).hexdigest(), + "baseCoverage": {"percentCovered": 41.666666666666664}, + "headCoverage": {"percentCovered": 85.71428571428571}, + "patchCoverage": {"percentCovered": 85.71428571428571}, + "segments": { + "results": [ + {"hasUnintendedChanges": True}, + {"hasUnintendedChanges": False}, + {"hasUnintendedChanges": True}, + ], + }, + }, + } + } + } + } + } + + @patch("services.comparison.Comparison.validate") + @patch("services.comparison.PullRequestComparison.get_file_comparison") + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_fetch_impacted_file_segments_with_indirect_and_direct_changes( + self, read_file, mock_get_file_comparison, mock_compare_validate + ): + read_file.return_value = mock_data_from_archive + + mock_get_file_comparison.return_value = MockFileComparison() + mock_compare_validate.return_value = True + variables = { + "org": self.org.username, + "repo": self.repo.name, + "pull": self.pull.pullid, + "path": "fileA", + "filters": {"hasUnintendedChanges": True}, + } + data = self.gql_request(query_impacted_file_through_pull, variables=variables) + assert data == { + "owner": { + "repository": { + "pull": { + "compareWithBase": { + "state": "processed", + "impactedFile": { + "headName": "fileA", + "baseName": "fileA", + "hashedPath": "5e9f0c9689fb7ec181ea0fb09ad3f74e", + "baseCoverage": {"percentCovered": 41.666666666666664}, + "headCoverage": {"percentCovered": 85.71428571428571}, + "patchCoverage": {"percentCovered": 50.0}, + "segments": { + "results": [ + {"hasUnintendedChanges": True}, + {"hasUnintendedChanges": True}, + ] + }, + }, + } + } + } + } + } + + @patch("services.comparison.Comparison.validate") + @patch("services.comparison.PullRequestComparison.get_file_comparison") + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_fetch_impacted_file_with_segments_unknown_path( + self, read_file, mock_get_file_comparison, mock_compare_validate + ): + read_file.return_value = mock_data_from_archive + mock_get_file_comparison.side_effect = TorngitObjectNotFoundError(None, None) + mock_compare_validate.return_value = True + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "pull": self.pull.pullid, + "path": "fileA", + } + data = self.gql_request(query_impacted_file_through_pull, variables=variables) + assert data == { + "owner": { + "repository": { + "pull": { + "compareWithBase": { + "state": "processed", + "impactedFile": { + "headName": "fileA", + "baseName": "fileA", + "hashedPath": hashlib.md5("fileA".encode()).hexdigest(), + "baseCoverage": {"percentCovered": 41.666666666666664}, + "headCoverage": {"percentCovered": 85.71428571428571}, + "patchCoverage": {"percentCovered": 50.0}, + "segments": {"message": "path does not exist: fileA"}, + }, + } + } + } + } + } + + @patch("services.comparison.Comparison.validate") + @patch("services.comparison.PullRequestComparison.get_file_comparison") + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_fetch_impacted_file_with_segments_provider_error( + self, read_file, mock_get_file_comparison, mock_compare_validate + ): + read_file.return_value = mock_data_from_archive + mock_get_file_comparison.side_effect = TorngitClientGeneralError( + 500, None, None + ) + mock_compare_validate.return_value = True + + variables = { + "org": self.org.username, + "repo": self.repo.name, + "pull": self.pull.pullid, + "path": "fileA", + } + data = self.gql_request(query_impacted_file_through_pull, variables=variables) + assert data == { + "owner": { + "repository": { + "pull": { + "compareWithBase": { + "state": "processed", + "impactedFile": { + "headName": "fileA", + "baseName": "fileA", + "hashedPath": hashlib.md5("fileA".encode()).hexdigest(), + "baseCoverage": {"percentCovered": 41.666666666666664}, + "headCoverage": {"percentCovered": 85.71428571428571}, + "patchCoverage": {"percentCovered": 50.0}, + "segments": { + "message": "Error fetching data from the provider" + }, + }, + } + } + } + } + } + + @patch("services.comparison.Comparison.validate") + @patch("services.comparison.PullRequestComparison.get_file_comparison") + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_fetch_impacted_file_with_invalid_comparison( + self, read_file, mock_get_file_comparison, mock_compare_validate + ): + read_file.return_value = mock_data_from_archive + + mock_get_file_comparison.return_value = MockFileComparison() + mock_compare_validate.side_effect = MissingComparisonReport() + variables = { + "org": self.org.username, + "repo": self.repo.name, + "pull": self.pull.pullid, + "path": "fileA", + "filters": {"hasUnintendedChanges": False}, + } + data = self.gql_request(query_impacted_file_through_pull, variables=variables) + assert data == { + "owner": { + "repository": { + "pull": { + "compareWithBase": { + "state": "processed", + "impactedFile": { + "headName": "fileA", + "baseName": "fileA", + "hashedPath": "5e9f0c9689fb7ec181ea0fb09ad3f74e", + "baseCoverage": {"percentCovered": 41.666666666666664}, + "headCoverage": {"percentCovered": 85.71428571428571}, + "patchCoverage": {"percentCovered": 50.0}, + "segments": {"results": []}, + }, + } + } + } + } + } + + @patch("services.comparison.Comparison.validate") + @patch("services.comparison.PullRequestComparison.get_file_comparison") + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_fetch_impacted_file_segments_with_direct_and_indirect_changes( + self, read_file, mock_get_file_comparison, mock_compare_validate + ): + read_file.return_value = mock_data_from_archive + + mock_get_file_comparison.return_value = MockFileComparison() + mock_compare_validate.return_value = True + variables = { + "org": self.org.username, + "repo": self.repo.name, + "pull": self.pull.pullid, + "path": "fileA", + "filters": {"hasUnintendedChanges": False}, + } + data = self.gql_request(query_impacted_file_through_pull, variables=variables) + assert data == { + "owner": { + "repository": { + "pull": { + "compareWithBase": { + "state": "processed", + "impactedFile": { + "headName": "fileA", + "baseName": "fileA", + "hashedPath": "5e9f0c9689fb7ec181ea0fb09ad3f74e", + "baseCoverage": {"percentCovered": 41.666666666666664}, + "headCoverage": {"percentCovered": 85.71428571428571}, + "patchCoverage": {"percentCovered": 50.0}, + "segments": { + "results": [ + {"hasUnintendedChanges": False}, + {"hasUnintendedChanges": True}, + ] + }, + }, + } + } + } + } + } + + @patch("services.comparison.Comparison.validate") + @patch("services.comparison.PullRequestComparison.get_file_comparison") + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_fetch_impacted_file_without_segments_filter( + self, read_file, mock_get_file_comparison, mock_compare_validate + ): + read_file.return_value = mock_data_from_archive + + mock_get_file_comparison.return_value = MockFileComparison() + mock_compare_validate.return_value = True + variables = { + "org": self.org.username, + "repo": self.repo.name, + "pull": self.pull.pullid, + "path": "fileA", + } + data = self.gql_request(query_impacted_file_through_pull, variables=variables) + assert data == { + "owner": { + "repository": { + "pull": { + "compareWithBase": { + "state": "processed", + "impactedFile": { + "headName": "fileA", + "baseName": "fileA", + "hashedPath": "5e9f0c9689fb7ec181ea0fb09ad3f74e", + "baseCoverage": {"percentCovered": 41.666666666666664}, + "headCoverage": {"percentCovered": 85.71428571428571}, + "patchCoverage": {"percentCovered": 50.0}, + "segments": { + "results": [ + {"hasUnintendedChanges": True}, + {"hasUnintendedChanges": False}, + {"hasUnintendedChanges": True}, + ] + }, + }, + } + } + } + } + } + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_fetch_direct_changed_files_count(self, read_file): + read_file.return_value = mock_data_from_archive + variables = { + "org": self.org.username, + "repo": self.repo.name, + "commit": self.commit.commitid, + } + data = self.gql_request( + query_direct_changed_files_count, + variables=variables, + ) + assert data == { + "owner": { + "repository": { + "commit": { + "compareWithParent": { + "directChangedFilesCount": 2, + } + } + } + } + } diff --git a/apps/codecov-api/graphql_api/tests/test_invoice.py b/apps/codecov-api/graphql_api/tests/test_invoice.py new file mode 100644 index 0000000000..f738c9f2c1 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_invoice.py @@ -0,0 +1,262 @@ +import json +from unittest.mock import patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from utils.test_utils import Client + +from .helper import GraphQLTestHelper + + +class TestInvoiceType(GraphQLTestHelper, TestCase): + def setUp(self): + self.service = "gitlab" + self.current_owner = OwnerFactory(stripe_customer_id="1000") + self.client = Client() + self.client.force_login_owner(self.current_owner) + + @patch("services.billing.stripe.Invoice.list") + def test_invoices_returns_100_recent_invoices(self, mock_list_filtered_invoices): + with open("./services/tests/samples/stripe_invoice.json") as f: + stripe_invoice_response = json.load(f) + # make it so there's 100 invoices, which is the max stripe returns + stripe_invoice_response["data"] = stripe_invoice_response["data"] * 100 + mock_list_filtered_invoices.return_value = stripe_invoice_response + + query = """{ + owner(username: "%s") { + invoices { + amountDue + amountPaid + created + currency + customerAddress + customerEmail + customerName + dueDate + footer + id + lineItems { + amount + currency + description + } + number + periodEnd + periodStart + status + subtotal + total + defaultPaymentMethod { + card { + brand + expMonth + expYear + last4 + } + billingDetails { + address { + city + country + line1 + line2 + postalCode + state + } + email + name + phone + } + } + taxIds { + type + value + } + } + } + } + """ % (self.current_owner.username) + + data = self.gql_request(query, owner=self.current_owner) + assert len(data["owner"]["invoices"]) == 100 + assert data["owner"]["invoices"][0] == { + "amountDue": 999, + "amountPaid": 999, + "created": 1489789429, + "currency": "usd", + "customerAddress": "6639 Boulevard Dr, Westwood FL 34202 USA", + "customerEmail": "olivia.williams.03@example.com", + "customerName": "Peer Company", + "dueDate": None, + "footer": None, + "id": "in_19yTU92eZvKYlo2C7uDjvu6v", + "lineItems": [ + { + "description": "(10) users-pr-inappm", + "amount": 120.0, + "currency": "usd", + } + ], + "number": "EF0A41E-0001", + "periodEnd": 1489789420, + "periodStart": 1487370220, + "status": "paid", + "subtotal": 999, + "total": 999, + "defaultPaymentMethod": None, + "taxIds": [], + } + + @patch("services.billing.stripe.Invoice.retrieve") + def test_invoice_returns_invoice_by_id(self, mock_retrieve_invoice): + with open("./services/tests/samples/stripe_invoice.json") as f: + stripe_invoice_response = json.load(f) + invoice = stripe_invoice_response["data"][0] + invoice["customer"] = self.current_owner.stripe_customer_id + mock_retrieve_invoice.return_value = invoice + + query = """{ + owner(username: "%s") { + invoice(invoiceId: "in_19yTU92eZvKYlo2C7uDjvu6v") { + amountDue + amountPaid + created + currency + customerAddress + customerEmail + customerName + dueDate + footer + id + lineItems { + amount + currency + description + } + number + periodEnd + periodStart + status + subtotal + total + defaultPaymentMethod { + card { + brand + expMonth + expYear + last4 + } + billingDetails { + address { + city + country + line1 + line2 + postalCode + state + } + email + name + phone + } + } + taxIds { + type + value + } + } + } + } + """ % (self.current_owner.username) + + data = self.gql_request(query, owner=self.current_owner) + assert data["owner"]["invoice"] is not None + assert data["owner"]["invoice"] == { + "amountDue": 999, + "amountPaid": 999, + "created": 1489789429, + "currency": "usd", + "customerAddress": "6639 Boulevard Dr, Westwood FL 34202 USA", + "customerEmail": "olivia.williams.03@example.com", + "customerName": "Peer Company", + "dueDate": None, + "footer": None, + "id": "in_19yTU92eZvKYlo2C7uDjvu6v", + "lineItems": [ + { + "description": "(10) users-pr-inappm", + "amount": 120.0, + "currency": "usd", + } + ], + "number": "EF0A41E-0001", + "periodEnd": 1489789420, + "periodStart": 1487370220, + "status": "paid", + "subtotal": 999, + "total": 999, + "defaultPaymentMethod": None, + "taxIds": [], + } + + @patch("services.billing.stripe.Invoice.retrieve") + def test_invoice_returns_none_if_no_invoices(self, mock_retrieve_invoice): + mock_retrieve_invoice.return_value = None + + query = """{ + owner(username: "%s") { + invoice(invoiceId: "in_19yTU92eZvKYlo2C7uDjvu6v") { + amountDue + amountPaid + created + currency + customerAddress + customerEmail + customerName + dueDate + footer + id + lineItems { + amount + currency + description + } + number + periodEnd + periodStart + status + subtotal + total + defaultPaymentMethod { + card { + brand + expMonth + expYear + last4 + } + billingDetails { + address { + city + country + line1 + line2 + postalCode + state + } + email + name + phone + } + } + taxIds { + type + value + } + } + } + } + """ % (self.current_owner.username) + + data = self.gql_request(query, owner=self.current_owner) + assert data["owner"]["invoice"] is None diff --git a/apps/codecov-api/graphql_api/tests/test_okta_config.py b/apps/codecov-api/graphql_api/tests/test_okta_config.py new file mode 100644 index 0000000000..e8a6f59899 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_okta_config.py @@ -0,0 +1,181 @@ +from django.test import TestCase +from shared.django_apps.codecov_auth.tests.factories import ( + AccountFactory, + OktaSettingsFactory, + OwnerFactory, +) + +from .helper import GraphQLTestHelper + + +class OktaConfigTestCase(GraphQLTestHelper, TestCase): + def setUp(self): + self.account = AccountFactory(name="Test Account") + self.owner = OwnerFactory( + username="randomOwner", service="github", account=self.account + ) + self.okta_settings = OktaSettingsFactory( + account=self.account, + client_id="test-client-id", + client_secret="test-client-secret", + enabled=True, + enforced=False, + ) + + def test_fetch_enabled_okta_config(self) -> None: + query = """ + query { + owner(username: "%s"){ + account { + oktaConfig { + enabled + } + } + } + } + """ % (self.owner.username) + + result = self.gql_request(query, owner=self.owner) + + assert "errors" not in result + assert result["owner"]["account"]["oktaConfig"]["enabled"] == True + + def test_fetch_disabled_okta_config(self) -> None: + self.okta_settings.enabled = False + self.okta_settings.save() + query = """ + query { + owner(username: "%s"){ + account { + oktaConfig { + enabled + } + } + } + } + """ % (self.owner.username) + + result = self.gql_request(query, owner=self.owner) + + assert "errors" not in result + assert result["owner"]["account"]["oktaConfig"]["enabled"] == False + + def test_fetch_enforced_okta_config(self) -> None: + query = """ + query { + owner(username: "%s"){ + account { + oktaConfig { + enforced + } + } + } + } + """ % (self.owner.username) + + result = self.gql_request(query, owner=self.owner) + + assert "errors" not in result + assert result["owner"]["account"]["oktaConfig"]["enforced"] == False + + def test_fetch_enforced_okta_config_true(self) -> None: + self.okta_settings.enforced = True + self.okta_settings.save() + query = """ + query { + owner(username: "%s"){ + account { + oktaConfig { + enforced + } + } + } + } + """ % (self.owner.username) + + result = self.gql_request(query, owner=self.owner) + + assert "errors" not in result + assert result["owner"]["account"]["oktaConfig"]["enforced"] == True + + def test_fetch_url_okta_config(self) -> None: + query = """ + query { + owner(username: "%s"){ + account{ + oktaConfig { + url + } + } + } + } + """ % (self.owner.username) + + result = self.gql_request(query, owner=self.owner) + + assert "errors" not in result + assert result["owner"]["account"]["oktaConfig"]["url"] == self.okta_settings.url + + def test_fetch_okta_config_client_id(self) -> None: + query = """ + query { + owner(username: "%s"){ + account{ + oktaConfig { + clientId + } + } + } + } + """ % (self.owner.username) + + result = self.gql_request(query, owner=self.owner) + + assert "errors" not in result + assert ( + result["owner"]["account"]["oktaConfig"]["clientId"] + == self.okta_settings.client_id + ) + + def test_fetch_okta_config_client_secret(self) -> None: + query = """ + query { + owner(username: "%s"){ + account{ + oktaConfig { + clientSecret + } + } + } + } + """ % (self.owner.username) + + result = self.gql_request(query, owner=self.owner) + + assert "errors" not in result + assert ( + result["owner"]["account"]["oktaConfig"]["clientSecret"] + == self.okta_settings.client_secret + ) + + def test_fetch_non_existent_okta_config(self) -> None: + self.okta_settings.delete() + + query = """ + query { + owner(username: "%s"){ + account{ + oktaConfig { + clientId + clientSecret + url + } + } + } + } + """ % (self.owner.username) + + result = self.gql_request(query, owner=self.owner) + + assert "errors" not in result + assert result["owner"]["account"]["oktaConfig"] is None diff --git a/apps/codecov-api/graphql_api/tests/test_onboarding.py b/apps/codecov-api/graphql_api/tests/test_onboarding.py new file mode 100644 index 0000000000..c9f6c85360 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_onboarding.py @@ -0,0 +1,54 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from graphql_api.tests.helper import GraphQLTestHelper + + +class OnboardingTest(GraphQLTestHelper, TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + self.params = { + "typeProjects": ["PERSONAL"], + "goals": ["STARTING_WITH_TESTS", "OTHER"], + "otherGoal": "feel confident in my code", + } + + def test_when_not_onboarded(self): + query = "{ me { onboardingCompleted } }" + data = self.gql_request(query, owner=self.owner) + assert data == {"me": {"onboardingCompleted": False}} + + def test_onboarding_mutation(self): + query = """ + mutation onboarding($input: OnboardUserInput!) { + onboardUser(input: $input) { + me { + onboardingCompleted + trackingMetadata { + profile { + otherGoal + goals + typeProjects + } + } + } + } + } + """ + data = self.gql_request( + query, owner=self.owner, variables={"input": self.params} + ) + assert data == { + "onboardUser": { + "me": { + "onboardingCompleted": True, + "trackingMetadata": { + "profile": { + "otherGoal": self.params["otherGoal"], + "goals": self.params["goals"], + "typeProjects": self.params["typeProjects"], + } + }, + } + } + } diff --git a/apps/codecov-api/graphql_api/tests/test_owner.py b/apps/codecov-api/graphql_api/tests/test_owner.py new file mode 100644 index 0000000000..f153dd0e09 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_owner.py @@ -0,0 +1,1268 @@ +import asyncio +from datetime import timedelta +from unittest.mock import patch + +from django.test import TestCase, override_settings +from django.utils import timezone +from freezegun import freeze_time +from graphql import GraphQLError +from prometheus_client import REGISTRY +from shared.django_apps.codecov_auth.tests.factories import ( + AccountFactory, + AccountsUsersFactory, + GetAdminProviderAdapter, + OktaSettingsFactory, + UserFactory, +) +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.django_apps.reports.models import ReportType +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName, TrialStatus +from shared.upload.utils import UploaderType, insert_coverage_measurement + +from billing.helpers import mock_all_plans_and_tiers +from codecov.commands.exceptions import ( + UnauthorizedGuestAccess, +) +from codecov_auth.models import GithubAppInstallation, OwnerProfile +from graphql_api.types.repository.repository import TOKEN_UNAVAILABLE +from reports.tests.factories import CommitReportFactory, UploadFactory + +from .helper import GraphQLTestHelper, paginate_connection + +query_repositories = """{ + owner(username: "%s") { + delinquent + orgUploadToken + ownerid + isCurrentUserPartOfOrg + yaml + repositories%s { + totalCount + edges { + node { + name + } + } + pageInfo { + hasNextPage + %s + } + } + } +} +""" + + +class TestOwnerType(GraphQLTestHelper, TestCase): + def setUp(self): + mock_all_plans_and_tiers() + self.account = AccountFactory() + self.owner = OwnerFactory( + username="codecov-user", service="github", account=self.account + ) + self.okta_settings = OktaSettingsFactory(account=self.account, enforced=True) + random_user = OwnerFactory(username="random-user", service="github") + RepositoryFactory( + author=self.owner, + active=True, + activated=True, + private=True, + name="a", + service_id="repo-1", + ) + RepositoryFactory( + author=self.owner, + active=False, + activated=False, + private=False, + name="b", + service_id="repo-2", + ) + RepositoryFactory( + author=random_user, + active=True, + activated=False, + private=True, + name="not", + service_id="repo-3", + ) + + def test_fetching_repositories(self): + before = REGISTRY.get_sample_value( + "api_gql_counts_hits_total", + labels={"operation_type": "unknown_type", "operation_name": "owner"}, + ) + errors_before = REGISTRY.get_sample_value( + "api_gql_counts_errors_total", + labels={"operation_type": "unknown_type", "operation_name": "owner"}, + ) + timer_before = REGISTRY.get_sample_value( + "api_gql_timers_full_runtime_seconds_count", + labels={"operation_type": "unknown_type", "operation_name": "owner"}, + ) + query = query_repositories % (self.owner.username, "", "") + data = self.gql_request( + query, owner=self.owner, okta_signed_in_accounts=[self.account.id] + ) + assert data == { + "owner": { + "delinquent": None, + "orgUploadToken": None, + "ownerid": self.owner.ownerid, + "isCurrentUserPartOfOrg": True, + "yaml": None, + "repositories": { + "totalCount": 2, + "edges": [{"node": {"name": "a"}}, {"node": {"name": "b"}}], + "pageInfo": {"hasNextPage": False}, + }, + } + } + after = REGISTRY.get_sample_value( + "api_gql_counts_hits_total", + labels={"operation_type": "unknown_type", "operation_name": "owner"}, + ) + errors_after = REGISTRY.get_sample_value( + "api_gql_counts_errors_total", + labels={"operation_type": "unknown_type", "operation_name": "owner"}, + ) + timer_after = REGISTRY.get_sample_value( + "api_gql_timers_full_runtime_seconds_count", + labels={"operation_type": "unknown_type", "operation_name": "owner"}, + ) + assert after - before == 1 + assert errors_after - errors_before == 0 + assert timer_after - timer_before == 1 + + def test_fetching_repositories_with_pagination(self): + query = query_repositories % (self.owner.username, "(first: 1)", "endCursor") + # Check on the first page if we have the repository b + data_page_one = self.gql_request( + query, owner=self.owner, okta_signed_in_accounts=[self.account.id] + ) + connection = data_page_one["owner"]["repositories"] + assert connection["edges"][0]["node"] == {"name": "a"} + pageInfo = connection["pageInfo"] + assert pageInfo["hasNextPage"] == True + next_cursor = pageInfo["endCursor"] + # Check on the second page if we have the other repository, by using the cursor + query = query_repositories % ( + self.owner.username, + f'(first: 1, after: "{next_cursor}")', + "endCursor", + ) + data_page_two = self.gql_request( + query, owner=self.owner, okta_signed_in_accounts=[self.account.id] + ) + connection = data_page_two["owner"]["repositories"] + assert connection["edges"][0]["node"] == {"name": "b"} + pageInfo = connection["pageInfo"] + assert pageInfo["hasNextPage"] == False + + def test_fetching_active_repositories(self): + query = query_repositories % ( + self.owner.username, + "(filters: { active: true })", + "", + ) + data = self.gql_request( + query, owner=self.owner, okta_signed_in_accounts=[self.account.id] + ) + repos = paginate_connection(data["owner"]["repositories"]) + assert repos == [{"name": "a"}] + + def test_fetching_repositories_by_name(self): + query = query_repositories % ( + self.owner.username, + '(filters: { term: "a" })', + "", + ) + data = self.gql_request( + query, owner=self.owner, okta_signed_in_accounts=[self.account.id] + ) + repos = paginate_connection(data["owner"]["repositories"]) + assert repos == [{"name": "a"}] + + def test_fetching_public_repository_when_unauthenticated(self): + query = query_repositories % (self.owner.username, "", "") + data = self.gql_request(query) + repos = paginate_connection(data["owner"]["repositories"]) + assert repos == [{"name": "b"}] + + def test_fetching_repositories_with_ordering(self): + query = query_repositories % ( + self.owner.username, + "(ordering: NAME, orderingDirection: DESC)", + "", + ) + data = self.gql_request( + query, owner=self.owner, okta_signed_in_accounts=[self.account.id] + ) + repos = paginate_connection(data["owner"]["repositories"]) + assert repos == [{"name": "b"}, {"name": "a"}] + + def test_fetching_repositories_inactive_repositories(self): + query = query_repositories % ( + self.owner.username, + "(filters: { active: false })", + "", + ) + data = self.gql_request(query, owner=self.owner) + repos = paginate_connection(data["owner"]["repositories"]) + assert repos == [{"name": "b"}] + + def test_fetch_account(self) -> None: + query = """{ + owner(username: "%s") { + account { + name + } + } + } + """ % (self.owner.username) + data = self.gql_request(query, owner=self.owner) + assert data["owner"]["account"]["name"] == self.account.name + + def test_fetching_repositories_active_repositories(self): + query = query_repositories % ( + self.owner.username, + "(filters: { active: true })", + "", + ) + data = self.gql_request( + query, owner=self.owner, okta_signed_in_accounts=[self.account.id] + ) + repos = paginate_connection(data["owner"]["repositories"]) + assert repos == [{"name": "a"}] + + def test_fetching_repositories_activated_repositories(self): + query = query_repositories % ( + self.owner.username, + "(filters: { activated: true })", + "", + ) + data = self.gql_request( + query, owner=self.owner, okta_signed_in_accounts=[self.account.id] + ) + repos = paginate_connection(data["owner"]["repositories"]) + assert repos == [{"name": "a"}] + + def test_fetching_repositories_deactivated_repositories(self): + query = query_repositories % ( + self.owner.username, + "(filters: { activated: false })", + "", + ) + data = self.gql_request(query, owner=self.owner) + repos = paginate_connection(data["owner"]["repositories"]) + assert repos == [{"name": "b"}] + + def test_fetching_repositories_filter_out_okta_enforced(self): + query = query_repositories % ( + self.owner.username, + '(filters: { term: "a" })', + "", + ) + data = self.gql_request(query, owner=self.owner) + repos = paginate_connection(data["owner"]["repositories"]) + assert repos == [] + + def test_fetching_repositories_impersonation_show_okta_enforced(self): + query = query_repositories % (self.owner.username, "", "") + data = self.gql_request(query, owner=self.owner, impersonate_owner=True) + repos = paginate_connection(data["owner"]["repositories"]) + assert repos == [{"name": "a"}, {"name": "b"}] + + def test_is_part_of_org_when_unauthenticated(self): + query = query_repositories % (self.owner.username, "", "") + data = self.gql_request(query) + assert data["owner"]["isCurrentUserPartOfOrg"] is False + + def test_is_part_of_org_when_authenticated_but_not_part(self): + org = OwnerFactory(username="random_org_test", service="github") + user = OwnerFactory(username="random_org_user", service="github") + query = query_repositories % (org.username, "", "") + data = self.gql_request(query, owner=user) + assert data["owner"]["isCurrentUserPartOfOrg"] is False + + def test_is_part_of_org_when_user_asking_for_themself(self): + query = query_repositories % (self.owner.username, "", "") + data = self.gql_request(query, owner=self.owner) + assert data["owner"]["isCurrentUserPartOfOrg"] is True + + def test_is_part_of_org_when_user_path_of_it(self): + org = OwnerFactory(username="random_org_test", service="github") + user = OwnerFactory( + username="random_org_user", service="github", organizations=[org.ownerid] + ) + query = query_repositories % (org.username, "", "") + data = self.gql_request(query, owner=user) + assert data["owner"]["isCurrentUserPartOfOrg"] is True + + def test_yaml_when_owner_not_have_yaml(self): + org = OwnerFactory(username="no_yaml", yaml=None, service="github") + self.owner.organizations = [org.ownerid] + self.owner.save() + query = query_repositories % (org.username, "", "") + data = self.gql_request(query, owner=self.owner) + assert data["owner"]["yaml"] is None + + def test_yaml_when_current_user_not_part_of_org(self): + yaml = {"test": "test"} + org = OwnerFactory(username="no_yaml", yaml=yaml, service="github") + self.owner.organizations = [] + self.owner.save() + query = query_repositories % (org.username, "", "") + data = self.gql_request(query, owner=self.owner) + assert data["owner"]["yaml"] is None + + def test_yaml_return_data(self): + yaml = {"test": "test"} + org = OwnerFactory(username="no_yaml", yaml=yaml, service="github") + self.owner.organizations = [org.ownerid] + self.owner.save() + query = query_repositories % (org.username, "", "") + data = self.gql_request(query, owner=self.owner) + assert data["owner"]["yaml"] == "test: test\n" + + @patch("codecov_auth.commands.owner.owner.OwnerCommands.set_yaml_on_owner") + def test_repository_dispatch_to_command(self, command_mock): + asyncio.set_event_loop(asyncio.new_event_loop()) + repo = RepositoryFactory(author=self.owner, private=False) + query_repositories = """{ + owner(username: "%s") { + repository(name: "%s") { + ... on Repository { + name + } + } + } + } + """ + command_mock.return_value = repo + query = query_repositories % (repo.author.username, repo.name) + data = self.gql_request(query, owner=self.owner) + assert data["owner"]["repository"]["name"] == repo.name + + def test_resolve_number_of_uploads_per_user(self): + query_uploads_number = """{ + owner(username: "%s") { + numberOfUploads + } + } + """ + repository = RepositoryFactory.create( + author__plan=DEFAULT_FREE_PLAN, author=self.owner + ) + first_commit = CommitFactory.create(repository=repository) + first_report = CommitReportFactory.create( + commit=first_commit, report_type=ReportType.COVERAGE.value + ) + for i in range(150): + upload = UploadFactory.create(report=first_report) + insert_coverage_measurement( + owner_id=self.owner.ownerid, + repo_id=repository.repoid, + commit_id=first_commit.id, + upload_id=upload.id, + uploader_used=UploaderType.CLI.value, + private_repo=repository.private, + report_type=first_report.report_type, + ) + query = query_uploads_number % (repository.author.username) + data = self.gql_request(query, owner=self.owner) + assert data["owner"]["numberOfUploads"] == 150 + + def test_is_current_user_not_an_admin(self): + query_current_user_is_admin = """{ + owner(username: "%s") { + isAdmin + } + } + """ + user = OwnerFactory(username="random_org_user", service="github") + owner = OwnerFactory(username="random_org_test", service="github") + query = query_current_user_is_admin % (owner.username) + data = self.gql_request(query, owner=user, with_errors=True) + assert data["data"]["owner"]["isAdmin"] is None + + @patch( + "codecov_auth.commands.owner.interactors.get_is_current_user_an_admin.get_provider" + ) + def test_is_current_user_an_admin(self, mocked_get_adapter): + query_current_user_is_admin = """{ + owner(username: "%s") { + isAdmin + } + } + """ + user = OwnerFactory(username="random_org_admin", service="github") + owner = OwnerFactory( + username="random_org_test", service="github", admins=[user.ownerid] + ) + user.organizations = [owner.ownerid] + user.save() + mocked_get_adapter.return_value = GetAdminProviderAdapter() + query = query_current_user_is_admin % (owner.username) + data = self.gql_request(query, owner=user) + assert data["owner"]["isAdmin"] is True + + def test_ownerid(self): + query = query_repositories % (self.owner.username, "", "") + data = self.gql_request(query, owner=self.owner) + assert data["owner"]["ownerid"] == self.owner.ownerid + + def test_delinquent(self): + query = query_repositories % (self.owner.username, "", "") + data = self.gql_request(query, owner=self.owner) + assert data["owner"]["delinquent"] == self.owner.delinquent + + @patch("codecov_auth.commands.owner.owner.OwnerCommands.get_org_upload_token") + def test_get_org_upload_token(self, mocker): + mocker.return_value = "upload_token" + query = query_repositories % (self.owner.username, "", "") + data = self.gql_request(query, owner=self.owner) + assert data["owner"]["orgUploadToken"] == "upload_token" + + @override_settings(HIDE_ALL_CODECOV_TOKENS=True) + def test_get_org_upload_token_hide_tokens_setting_owner_not_admin(self): + random_owner = OwnerFactory() + query = """{ + owner(username: "%s") { + orgUploadToken + } + } + """ % (self.owner.username) + random_owner.organizations = [self.owner.ownerid] + random_owner.save() + data = self.gql_request(query, owner=random_owner) + assert data["owner"]["orgUploadToken"] == TOKEN_UNAVAILABLE + + @patch("codecov_auth.commands.owner.owner.OwnerCommands.get_org_upload_token") + @override_settings(HIDE_ALL_CODECOV_TOKENS=True) + def test_get_org_upload_token_hide_tokens_setting_owner_is_admin(self, mocker): + mocker.return_value = "upload_token" + query = query_repositories % (self.owner.username, "", "") + data = self.gql_request(query, owner=self.owner) + assert data["owner"]["orgUploadToken"] == "upload_token" + + # Applies for old users that didn't get their owner profiles created w/ their owner + def test_when_owner_profile_doesnt_exist(self): + owner = OwnerFactory(username="no-profile-user") + owner.profile.delete() + query = """{ + owner(username: "%s") { + defaultOrgUsername + username + } + } + """ % (owner.username) + data = self.gql_request(query, owner=owner) + assert data["owner"]["defaultOrgUsername"] is None + + def test_get_default_org_username_for_owner(self): + organization = OwnerFactory(username="sample-org", service="github") + owner = OwnerFactory( + username="sample-owner", + service="github", + organizations=[organization.ownerid], + ) + OwnerProfile.objects.filter(owner_id=owner.ownerid).update( + default_org=organization + ) + query = """{ + owner(username: "%s") { + defaultOrgUsername + username + } + } + """ % (owner.username) + data = self.gql_request(query, owner=owner) + assert data["owner"]["defaultOrgUsername"] == organization.username + + def test_owner_without_default_org_returns_null(self): + owner = OwnerFactory(username="sample-owner", service="github") + query = """{ + owner(username: "%s") { + defaultOrgUsername + username + } + } + """ % (owner.username) + data = self.gql_request(query, owner=owner) + assert data["owner"]["defaultOrgUsername"] is None + + def test_owner_without_owner_profile_returns_no_default_org(self): + owner = OwnerFactory(username="sample-owner", service="github") + query = """{ + owner(username: "%s") { + defaultOrgUsername + username + } + } + """ % (owner.username) + data = self.gql_request(query, owner=owner) + assert data["owner"]["defaultOrgUsername"] is None + + def test_is_current_user_not_activated(self): + owner = OwnerFactory(username="sample-owner", service="github") + self.owner.organizations = [owner.ownerid] + self.owner.save() + query = """{ + owner(username: "%s") { + isCurrentUserActivated + } + } + """ % (owner.username) + data = self.gql_request(query, owner=self.owner) + assert data["owner"]["isCurrentUserActivated"] == False + + def test_is_current_user_not_activated_no_current_owner(self): + owner = OwnerFactory(username="sample-owner", service="github") + query = """{ + owner(username: "%s") { + isCurrentUserActivated + } + } + """ % (owner.username) + self.client.force_login(user=UserFactory()) + data = self.gql_request(query, owner=None) + assert data["owner"]["isCurrentUserActivated"] == False + + def test_is_current_user_activated(self): + user = OwnerFactory(username="sample-user") + owner = OwnerFactory( + username="sample-owner", plan_activated_users=[user.ownerid] + ) + user.organizations = [owner.ownerid] + user.save() + query = """{ + owner(username: "%s") { + isCurrentUserActivated + } + } + """ % (owner.username) + data = self.gql_request(query, owner=user) + assert data["owner"]["isCurrentUserActivated"] == True + + def test_is_current_user_activated_when_plan_activated_users_is_none(self): + user = OwnerFactory(username="sample-user") + owner = OwnerFactory(username="sample-owner", plan_activated_users=None) + user.organizations = [owner.ownerid] + user.save() + query = """{ + owner(username: "%s") { + isCurrentUserActivated + } + } + """ % (owner.username) + data = self.gql_request(query, owner=user) + assert data["owner"]["isCurrentUserActivated"] == False + + def test_is_current_user_activated_anonymous(self): + owner = OwnerFactory(username="sample-owner") + query = """{ + owner(username: "%s") { + isCurrentUserActivated + } + } + """ % (owner.username) + data = self.gql_request(query) + assert data["owner"]["isCurrentUserActivated"] == False + + def test_is_current_user_activated_admin_activated(self): + owner = OwnerFactory( + username="sample-owner-authorized", + admins=[self.owner.ownerid], + plan_activated_users=[self.owner.ownerid], + ) + self.owner.organizations = [owner.ownerid] + self.owner.save() + query = """{ + owner(username: "%s") { + isCurrentUserActivated + } + } + """ % (owner.username) + data = self.gql_request(query, owner=self.owner) + assert data["owner"]["isCurrentUserActivated"] == True + + def test_is_current_user_activated_admin_not_activated(self): + owner = OwnerFactory( + username="sample-owner-authorized", + admins=[self.owner.ownerid], + plan_activated_users=None, + ) + self.owner.organizations = [owner.ownerid] + self.owner.save() + query = """{ + owner(username: "%s") { + isCurrentUserActivated + } + } + """ % (owner.username) + data = self.gql_request(query, owner=self.owner) + assert data["owner"]["isCurrentUserActivated"] == False + + def test_owner_is_current_user_activated(self): + query = """{ + owner(username: "%s") { + isCurrentUserActivated + } + } + """ % (self.owner.username) + data = self.gql_request(query, owner=self.owner) + assert data["owner"]["isCurrentUserActivated"] == True + + @freeze_time("2023-06-19") + def test_owner_plan_status(self): + current_org = OwnerFactory( + username="random-plan-user", + service="github", + trial_start_date=timezone.now(), + trial_end_date=timezone.now() + timedelta(days=14), + trial_status=TrialStatus.ONGOING.value, + ) + query = """{ + owner(username: "%s") { + plan { + trialStatus + } + } + } + """ % (current_org.username) + data = self.gql_request(query, owner=current_org) + assert data["owner"]["plan"] == { + "trialStatus": "ONGOING", + } + + @freeze_time("2023-06-19") + def test_owner_pretrial_plan_benefits(self): + current_org = OwnerFactory( + username="random-plan-user", + service="github", + trial_start_date=timezone.now(), + trial_end_date=timezone.now() + timedelta(days=14), + trial_status=TrialStatus.ONGOING.value, + plan=PlanName.TRIAL_PLAN_NAME.value, + pretrial_users_count=123, + ) + query = """{ + owner(username: "%s") { + pretrialPlan { + benefits + } + } + } + """ % (current_org.username) + data = self.gql_request(query, owner=current_org) + assert data["owner"]["pretrialPlan"] == { + "benefits": [ + "Up to 123 users", + "Unlimited public repositories", + "Unlimited private repositories", + ], + } + + @freeze_time("2023-06-19") + def test_owner_available_plans(self): + current_org = OwnerFactory( + username="random-plan-user-123", + service="github", + plan=PlanName.CODECOV_PRO_MONTHLY.value, + pretrial_users_count=123, + ) + query = """{ + owner(username: "%s") { + availablePlans { + value + } + } + } + """ % (current_org.username) + data = self.gql_request(query, owner=current_org) + expected_plans = [ + {"value": "users-pr-inappm"}, + {"value": "users-pr-inappy"}, + {"value": "users-teamm"}, + {"value": "users-teamy"}, + {"value": DEFAULT_FREE_PLAN}, + ] + for plan in expected_plans: + self.assertIn(plan, data["owner"]["availablePlans"]) + + def test_owner_query_with_no_service(self): + current_org = OwnerFactory( + username="random-plan-user", + service="github", + ) + query = """{ + owner(username: "%s") { + username + } + } + """ % (current_org.username) + + res = self.gql_request(query, provider="", with_errors=True) + + assert res["data"]["owner"] is None + + def test_owner_query_with_private_repos(self): + current_org = OwnerFactory( + username="random-plan-user", + service="github", + ) + RepositoryFactory(author=current_org, active=True, activated=True, private=True) + query = """{ + owner(username: "%s") { + hasPrivateRepos + hasPublicRepos + hasActiveRepos + } + } + """ % (current_org.username) + + data = self.gql_request(query, owner=current_org) + assert data["owner"]["hasPrivateRepos"] == True + assert data["owner"]["hasPublicRepos"] == False + assert data["owner"]["hasActiveRepos"] == True + + def test_owner_query_with_no_active_repos(self): + current_org = OwnerFactory( + username="random-plan-user", + service="github", + ) + RepositoryFactory( + author=current_org, active=False, activated=False, private=True + ) + query = """{ + owner(username: "%s") { + hasPrivateRepos + hasPublicRepos + hasActiveRepos + } + } + """ % (current_org.username) + data = self.gql_request(query, owner=current_org) + assert data["owner"]["hasPrivateRepos"] == True + assert data["owner"]["hasPublicRepos"] == False + assert data["owner"]["hasActiveRepos"] == False + + def test_owner_query_with_public_repos(self): + current_org = OwnerFactory( + username="random-plan-user", + service="github", + ) + RepositoryFactory( + author=current_org, + active=True, + activated=True, + private=False, + name="test-one", + ) + RepositoryFactory( + author=current_org, + active=True, + activated=True, + private=False, + name="test-two", + ) + query = """{ + owner(username: "%s") { + hasPrivateRepos + hasPublicRepos + hasActiveRepos + } + } + """ % (current_org.username) + + data = self.gql_request(query, owner=current_org) + assert data["owner"]["hasPrivateRepos"] == False + assert data["owner"]["hasPublicRepos"] == True + assert data["owner"]["hasActiveRepos"] == True + + def test_owner_hash_owner_id(self): + user = OwnerFactory(username="sample-user") + owner = OwnerFactory(username="sample-owner", plan_activated_users=None) + user.organizations = [owner.ownerid] + user.save() + query = """{ + owner(username: "%s") { + hashOwnerid + } + } + """ % (owner.username) + data = self.gql_request(query, owner=user) + assert data["owner"]["hashOwnerid"] is not None + + @override_settings(IS_ENTERPRISE=True, GUEST_ACCESS=False) + def test_fetch_owner_on_unauthenticated_enteprise_guest_access(self): + owner = OwnerFactory(username="sample-owner", service="github") + query = """{ + owner(username: "%s") { + username + } + } + """ % (owner.username) + + try: + self.gql_request(query) + + except GraphQLError as e: + assert e.message == UnauthorizedGuestAccess.message + assert e.extensions["code"] == UnauthorizedGuestAccess.code + + @override_settings(IS_ENTERPRISE=True, GUEST_ACCESS=False) + def test_fetch_owner_on_unauthenticated_enteprise_guest_access_not_activated(self): + user = OwnerFactory(username="sample-user") + owner = OwnerFactory(username="sample-owner", plan_activated_users=[123, 456]) + user.organizations = [owner.ownerid] + user.save() + owner.save() + query = """{ + owner(username: "%s") { + isCurrentUserActivated + } + } + """ % (owner.username) + + try: + self.gql_request(query, owner=user) + + except GraphQLError as e: + assert e.message == UnauthorizedGuestAccess.message + assert e.extensions["code"] == UnauthorizedGuestAccess.code + + @override_settings(IS_ENTERPRISE=True, GUEST_ACCESS=False) + def test_fetch_owner_plan_activated_users_is_none(self): + """ + This test is when Enterprise guest access is disabled, and you are + trying to view an org that does not track plan activated users (e.g., historic data) + """ + user = OwnerFactory(username="sample-user") + owner = OwnerFactory(username="sample-owner", plan_activated_users=None) + user.save() + owner.save() + query = """{ + owner(username: "%s") { + username + } + } + """ % (owner.username) + + data = self.gql_request(query, owner=user) + assert data["owner"]["username"] == "sample-owner" + + def test_fetch_current_user_is_okta_authenticated(self): + account = AccountFactory() + owner = OwnerFactory(username="sample-owner", service="github", account=account) + owner.save() + + user = OwnerFactory(username="sample-user") + user.organizations = [owner.ownerid] + user.save() + + query = """{ + owner(username: "%s") { + isUserOktaAuthenticated + } + } + """ % (owner.username) + + data = self.gql_request(query, owner=user, okta_signed_in_accounts=[account.pk]) + assert data["owner"]["isUserOktaAuthenticated"] == True + + def test_fetch_current_user_is_not_okta_authenticated(self): + account = AccountFactory() + owner = OwnerFactory(username="sample-owner", service="github", account=account) + owner.save() + + user = OwnerFactory(username="sample-user") + user.organizations = [owner.ownerid] + user.save() + + query = """{ + owner(username: "%s") { + isUserOktaAuthenticated + } + } + """ % (owner.username) + + data = self.gql_request(query, owner=user, okta_signed_in_accounts=[]) + assert data["owner"]["isUserOktaAuthenticated"] == False + + def test_fetch_current_user_is_not_okta_authenticated_no_account(self): + owner = OwnerFactory(username="sample-owner", service="github") + owner.save() + + user = OwnerFactory(username="sample-user") + user.organizations = [owner.ownerid] + user.save() + + query = """{ + owner(username: "%s") { + isUserOktaAuthenticated + } + } + """ % (owner.username) + + data = self.gql_request(query, owner=user, okta_signed_in_accounts=[]) + assert data["owner"]["isUserOktaAuthenticated"] == False + + @patch("shared.rate_limits.determine_entity_redis_key") + @patch("shared.rate_limits.determine_if_entity_is_rate_limited") + @override_settings(IS_ENTERPRISE=True, GUEST_ACCESS=True) + def test_fetch_is_github_rate_limited( + self, mock_determine_rate_limit, mock_determine_redis_key + ): + current_org = OwnerFactory( + username="random-plan-user", + service="github", + ) + query = """{ + owner(username: "%s") { + isGithubRateLimited + } + } + + """ % (current_org.username) + mock_determine_redis_key.return_value = "test" + mock_determine_rate_limit.return_value = True + + data = self.gql_request(query, owner=current_org) + assert data["owner"]["isGithubRateLimited"] == True + + def test_fetch_is_github_rate_limited_not_on_gh_service(self): + current_org = OwnerFactory( + username="random-plan-user", + service="bitbucket", + ) + query = """{ + owner(username: "%s") { + isGithubRateLimited + } + } + + """ % (current_org.username) + data = self.gql_request(query, owner=current_org, provider="bb") + assert data["owner"]["isGithubRateLimited"] == False + + @patch("services.self_hosted.get_config") + def test_ai_features_enabled(self, get_config_mock): + current_org = OwnerFactory( + username="random-plan-user", + service="github", + ) + + get_config_mock.return_value = [ + {"service": "github", "ai_features_app_id": 12345}, + ] + + ai_app_installation = GithubAppInstallation( + name="ai-features", + owner=current_org, + repository_service_ids=None, + installation_id=12345, + ) + + ai_app_installation.save() + + query = """{ + owner(username: "%s") { + aiFeaturesEnabled + } + } + + """ % (current_org.username) + + data = self.gql_request(query, owner=current_org) + assert data["owner"]["aiFeaturesEnabled"] == True + + @patch("services.self_hosted.get_config") + def test_fetch_repos_ai_features_enabled(self, get_config_mock): + get_config_mock.return_value = [ + {"service": "github", "ai_features_app_id": 12345}, + ] + + ai_app_installation = GithubAppInstallation( + name="ai-features", + owner=self.owner, + repository_service_ids=["repo-1"], + installation_id=12345, + ) + + ai_app_installation.save() + + query = """{ + owner(username: "%s") { + aiEnabledRepos + } + } + + """ % (self.owner.username) + data = self.gql_request(query, owner=self.owner) + assert data["owner"]["aiEnabledRepos"] == ["a"] + + @patch("services.self_hosted.get_config") + def test_fetch_repos_ai_features_enabled_app_not_configured(self, get_config_mock): + current_org = OwnerFactory( + username="random-plan-user", + service="github", + ) + + get_config_mock.return_value = [ + {"service": "github", "ai_features_app_id": 12345}, + ] + + query = """{ + owner(username: "%s") { + aiEnabledRepos + } + } + + """ % (current_org.username) + data = self.gql_request(query, owner=current_org) + assert data["owner"]["aiEnabledRepos"] is None + + @patch("services.self_hosted.get_config") + def test_fetch_repos_ai_features_enabled_all_repos(self, get_config_mock): + get_config_mock.return_value = [ + {"service": "github", "ai_features_app_id": 12345}, + ] + + ai_app_installation = GithubAppInstallation( + name="ai-features", + owner=self.owner, + repository_service_ids=None, + installation_id=12345, + ) + + ai_app_installation.save() + + query = """{ + owner(username: "%s") { + aiEnabledRepos + } + } + + """ % (self.owner.username) + data = self.gql_request(query, owner=self.owner) + assert data["owner"]["aiEnabledRepos"] == ["b", "a"] + + def test_fetch_upload_token_required(self): + owner = OwnerFactory( + username="sample-owner", + service="github", + upload_token_required_for_public_repos=True, + ) + query = """{ + owner(username: "%s") { + uploadTokenRequired + } + } + """ % (owner.username) + data = self.gql_request(query, owner=owner) + assert data["owner"]["uploadTokenRequired"] == True + + def test_fetch_upload_token_not_required(self): + owner = OwnerFactory(username="sample-owner", service="github") + owner.upload_token_required_for_public_repos = False + owner.save() + query = """{ + owner(username: "%s") { + uploadTokenRequired + } + } + """ % (owner.username) + data = self.gql_request(query, owner=owner) + assert data["owner"]["uploadTokenRequired"] == False + + def test_fetch_upload_token_user_not_part_of_org(self): + owner = OwnerFactory(username="sample", service="github") + user = OwnerFactory(username="sample-user", service="github") + query = """{ + owner(username: "%s") { + uploadTokenRequired + } + } + """ % (owner.username) + + data = self.gql_request(query, owner=user) + assert data["owner"]["uploadTokenRequired"] is None + + def test_fetch_activated_user_count(self): + user = OwnerFactory(username="sample-user") + user2 = OwnerFactory(username="sample-user-2") + user3 = OwnerFactory(username="sample-user-3") + owner = OwnerFactory( + username="sample-org", + plan_activated_users=[user.ownerid, user2.ownerid, user3.ownerid], + ) + user.organizations = [owner.ownerid] + user.save() + + query = """{ + owner(username: "%s") { + activatedUserCount + } + } + """ % (owner.username) + data = self.gql_request(query, owner=user) + assert data["owner"]["activatedUserCount"] == 3 + + def test_fetch_activated_user_count_returns_null_if_not_in_org(self): + user = OwnerFactory(username="sample-user") + user2 = OwnerFactory(username="sample-user-2") + user3 = OwnerFactory(username="sample-user-3") + owner = OwnerFactory( + username="sample-org", plan_activated_users=[user2.ownerid, user3.ownerid] + ) + + query = """{ + owner(username: "%s") { + activatedUserCount + } + } + """ % (owner.username) + data = self.gql_request(query, owner=user) + assert data["owner"]["activatedUserCount"] is None + + def test_fetch_activated_user_count_when_not_in_org_but_has_shared_account(self): + owner = OwnerFactory(username="sample-user") + AccountsUsersFactory(user=owner.user, account=self.account) + user2 = OwnerFactory(username="sample-user-2") + user3 = OwnerFactory(username="sample-user-3") + other_owner = OwnerFactory( + username="sample-org", + plan_activated_users=[user2.ownerid, user3.ownerid], + account=self.account, + ) + + query = """{ + owner(username: "%s") { + activatedUserCount + } + } + """ % (other_owner.username) + data = self.gql_request(query, owner=owner) + assert data["owner"]["activatedUserCount"] == 2 + + def test_fetch_available_plans_is_enterprise_plan(self): + current_org = OwnerFactory( + username="random-plan-user", + service="github", + plan=DEFAULT_FREE_PLAN, + ) + query = """{ + owner(username: "%s") { + availablePlans { + value + isEnterprisePlan + isProPlan + isTeamPlan + isSentryPlan + isFreePlan + isTrialPlan + } + } + } + """ % (current_org.username) + data = self.gql_request(query, owner=current_org) + expected_plans = [ + { + "value": "users-pr-inappm", + "isEnterprisePlan": False, + "isProPlan": True, + "isTeamPlan": False, + "isSentryPlan": False, + "isFreePlan": False, + "isTrialPlan": False, + }, + { + "value": "users-pr-inappy", + "isEnterprisePlan": False, + "isProPlan": True, + "isTeamPlan": False, + "isSentryPlan": False, + "isFreePlan": False, + "isTrialPlan": False, + }, + { + "value": "users-teamm", + "isEnterprisePlan": False, + "isProPlan": False, + "isTeamPlan": True, + "isSentryPlan": False, + "isFreePlan": False, + "isTrialPlan": False, + }, + { + "value": "users-teamy", + "isEnterprisePlan": False, + "isProPlan": False, + "isTeamPlan": True, + "isSentryPlan": False, + "isFreePlan": False, + "isTrialPlan": False, + }, + { + "value": DEFAULT_FREE_PLAN, + "isEnterprisePlan": False, + "isProPlan": False, + "isTeamPlan": True, + "isSentryPlan": False, + "isFreePlan": True, + "isTrialPlan": False, + }, + ] + for plan in expected_plans: + self.assertIn(plan, data["owner"]["availablePlans"]) + + def test_fetch_owner_with_no_service(self): + current_org = OwnerFactory( + username="random-plan-user", + service="github", + plan=DEFAULT_FREE_PLAN, + ) + + query = """{ + owner(username: "%s") { + username + } + } + """ % (current_org.username) + data = self.gql_request(query, owner=current_org, provider="", with_errors=True) + assert data == {"data": {"owner": None}} + + def test_fetch_repositories_ai_features_enabled(self): + ai_app_installation = GithubAppInstallation( + name="ai-features", + owner=self.owner, + repository_service_ids=[], + installation_id=12345, + ) + + ai_app_installation.save() + query = query_repositories % ( + self.owner.username, + "(filters: { aiEnabled: true })", + "", + ) + + data = self.gql_request(query, owner=self.owner) + repos = paginate_connection(data["owner"]["repositories"]) + assert repos == [{"name": "a"}, {"name": "b"}] + + def test_fetch_repositories_ai_features_enabled_no_app_install(self): + query = query_repositories % ( + self.owner.username, + "(filters: { aiEnabled: true })", + "", + ) + data = self.gql_request(query, owner=self.owner) + repos = paginate_connection(data["owner"]["repositories"]) + assert repos == [] diff --git a/apps/codecov-api/graphql_api/tests/test_owner_measurements.py b/apps/codecov-api/graphql_api/tests/test_owner_measurements.py new file mode 100644 index 0000000000..c5a67a094e --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_owner_measurements.py @@ -0,0 +1,186 @@ +from datetime import datetime, timezone +from unittest.mock import patch + +from django.test import TestCase, override_settings +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from timeseries.models import Interval + +from .helper import GraphQLTestHelper + + +@patch("timeseries.helpers.owner_coverage_measurements_with_fallback") +class TestOwnerMeasurements(TestCase, GraphQLTestHelper): + def _request(self, variables=None): + query = f""" + query Measurements($repos: [String!]) {{ + owner(username: "{self.org.username}") {{ + measurements( + interval: INTERVAL_1_DAY + after: "2022-01-01" + before: "2022-01-03" + repos: $repos + ) {{ + timestamp + avg + min + max + }} + }} + }} + """ + data = self.gql_request(query, owner=self.owner, variables=variables) + return data["owner"]["measurements"] + + def setUp(self): + self.org = OwnerFactory(username="test-org") + self.repo1 = RepositoryFactory( + name="test-repo1", + author=self.org, + private=True, + ) + self.repo2 = RepositoryFactory( + name="test-repo2", + author=self.org, + private=False, + ) + self.owner = OwnerFactory(permission=[self.repo1.pk, self.repo2.pk]) + + @override_settings(TIMESERIES_ENABLED=True) + def test_measurements_timeseries_enabled( + self, owner_coverage_measurements_with_fallback + ): + owner_coverage_measurements_with_fallback.return_value = [ + {"timestamp_bin": datetime(2022, 1, 1), "min": 1, "max": 2, "avg": 1.5}, + {"timestamp_bin": datetime(2022, 1, 2), "min": 3, "max": 4, "avg": 3.5}, + ] + + assert self._request() == [ + {"timestamp": "2022-01-01T00:00:00", "min": 1.0, "max": 2.0, "avg": 1.5}, + {"timestamp": "2022-01-02T00:00:00", "min": 3.0, "max": 4.0, "avg": 3.5}, + { + "timestamp": "2022-01-03T00:00:00+00:00", + "min": None, + "max": None, + "avg": None, + }, + ] + + owner_coverage_measurements_with_fallback.assert_called_once_with( + self.org, + [self.repo2.pk, self.repo1.pk], + Interval.INTERVAL_1_DAY, + start_date=datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + end_date=datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + ) + + @override_settings(TIMESERIES_ENABLED=True) + def test_measurements_timeseries_enabled_repoids( + self, owner_coverage_measurements_with_fallback + ): + owner_coverage_measurements_with_fallback.return_value = [ + {"timestamp_bin": datetime(2022, 1, 1), "min": 1, "max": 2, "avg": 1.5}, + {"timestamp_bin": datetime(2022, 1, 2), "min": 3, "max": 4, "avg": 3.5}, + ] + + assert self._request(variables={"repos": ["test-repo1"]}) == [ + {"timestamp": "2022-01-01T00:00:00", "min": 1.0, "max": 2.0, "avg": 1.5}, + {"timestamp": "2022-01-02T00:00:00", "min": 3.0, "max": 4.0, "avg": 3.5}, + { + "timestamp": "2022-01-03T00:00:00+00:00", + "min": None, + "max": None, + "avg": None, + }, + ] + + owner_coverage_measurements_with_fallback.assert_called_once_with( + self.org, + [self.repo1.pk], + Interval.INTERVAL_1_DAY, + start_date=datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + end_date=datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + ) + + @override_settings(TIMESERIES_ENABLED=False) + def test_measurements_timeseries_not_enabled( + self, owner_coverage_measurements_with_fallback + ): + owner_coverage_measurements_with_fallback.return_value = [ + {"timestamp_bin": datetime(2022, 1, 1), "min": 1, "max": 2, "avg": 1.5}, + {"timestamp_bin": datetime(2022, 1, 2), "min": 3, "max": 4, "avg": 3.5}, + ] + + assert self._request() == [ + {"timestamp": "2022-01-01T00:00:00", "min": 1.0, "max": 2.0, "avg": 1.5}, + {"timestamp": "2022-01-02T00:00:00", "min": 3.0, "max": 4.0, "avg": 3.5}, + { + "timestamp": "2022-01-03T00:00:00+00:00", + "min": None, + "max": None, + "avg": None, + }, + ] + + owner_coverage_measurements_with_fallback.assert_called_once_with( + self.org, + [self.repo2.pk, self.repo1.pk], + Interval.INTERVAL_1_DAY, + start_date=datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + end_date=datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + ) + + @override_settings(TIMESERIES_ENABLED=True) + def test_repository_filtering_by_public_private( + self, owner_coverage_measurements_with_fallback + ): + owner_coverage_measurements_with_fallback.return_value = [] + query = f""" + query Measurements($isPublic: Boolean) {{ + owner(username: "{self.org.username}") {{ + measurements( + interval: INTERVAL_1_DAY + isPublic: $isPublic + ) {{ + timestamp + }} + }} + }} + """ + + self.gql_request(query, owner=self.owner, variables={"isPublic": False})[ + "owner" + ]["measurements"] + params = owner_coverage_measurements_with_fallback.call_args.args + # Check that the call is using only repo_ids of the private repo + assert params[1] == [self.repo1.pk] + + self.gql_request(query, owner=self.owner, variables={"isPublic": True})[ + "owner" + ]["measurements"] + params = owner_coverage_measurements_with_fallback.call_args.args + # Check that the call is using only repo_ids of the public repo + assert params[1] == [self.repo2.pk] + + self.gql_request(query, owner=self.owner, variables={"isPublic": None})[ + "owner" + ]["measurements"] + params = owner_coverage_measurements_with_fallback.call_args.args + # Check that the call is using both private and public repos + assert set(params[1]) == {self.repo1.pk, self.repo2.pk} + + query = f""" + query Measurements {{ + owner(username: "{self.org.username}") {{ + measurements( + interval: INTERVAL_1_DAY + ) {{ + timestamp + }} + }} + }} + """ + self.gql_request(query, owner=self.owner)["owner"]["measurements"] + params = owner_coverage_measurements_with_fallback.call_args.args + # Check that the call is using both private and public repos + assert set(params[1]) == {self.repo1.pk, self.repo2.pk} diff --git a/apps/codecov-api/graphql_api/tests/test_path_content.py b/apps/codecov-api/graphql_api/tests/test_path_content.py new file mode 100644 index 0000000000..71499934bf --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_path_content.py @@ -0,0 +1,87 @@ +from unittest.mock import Mock, PropertyMock, patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import CommitFactory +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine, ReportTotals +from shared.utils.sessions import Session + +from services.path import Dir, File + +from ..types.commit.commit import resolve_path_contents +from ..types.errors.errors import MissingCoverage, UnknownPath +from ..types.path_contents.path_content import ( + resolve_path_content_type, +) + + +def sample_report() -> Report: + report = Report() + first_file = ReportFile("foo/file1.py") + first_file.append(1, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file = ReportFile("bar/file2.py") + second_file.append(1, ReportLine.create(coverage=1, sessions=[[0, 1]])) + report.append(first_file) + report.append(second_file) + report.add_session(Session()) + return report + + +class MockContext(object): + def __init__(self, context): + self.context = context + + +class MockProfilingSummary: + def __init__(self, critical_filenames): + self.critical_filenames = critical_filenames + + +class TestResolvePathContent: + def test_returns_path_content_file(self): + file = File(full_path="file.py", totals=ReportTotals.default_totals()) + + type = resolve_path_content_type(file) + assert type == "PathContentFile" + + def test_returns_path_content_dir(self): + dir = Dir(full_path="foo/bar", children=[]) + + type = resolve_path_content_type(dir) + assert type == "PathContentDir" + + def test_returns_none(self): + type = resolve_path_content_type("string") + assert type is None + + +class TestPathContents(TestCase): + def setUp(self): + request = Mock() + request.user = Mock() + self.info = MockContext({"request": request}) + self.commit = CommitFactory() + + @patch("shared.reports.api_report_service.build_report_from_commit") + @patch("services.path.provider_path_exists") + @patch("services.path.ReportPaths.paths", new_callable=PropertyMock) + async def test_missing_coverage( + self, paths_mock, provider_path_exists_mock, report_mock + ): + paths_mock.return_value = [] + provider_path_exists_mock.return_value = True + report_mock.return_value = sample_report() + res = await resolve_path_contents(self.commit, self.info, "test/path") + assert isinstance(res, MissingCoverage) + + @patch("shared.reports.api_report_service.build_report_from_commit") + @patch("services.path.provider_path_exists") + @patch("services.path.ReportPaths.paths", new_callable=PropertyMock) + async def test_unknown_path( + self, paths_mock, provider_path_exists_mock, report_mock + ): + paths_mock.return_value = [] + provider_path_exists_mock.return_value = False + report_mock.return_value = sample_report() + res = await resolve_path_contents(self.commit, self.info, "test/path") + assert isinstance(res, UnknownPath) diff --git a/apps/codecov-api/graphql_api/tests/test_plan.py b/apps/codecov-api/graphql_api/tests/test_plan.py new file mode 100644 index 0000000000..2e73e5de4b --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_plan.py @@ -0,0 +1,269 @@ +from datetime import datetime, timedelta +from unittest.mock import patch + +import pytest +from django.test import TestCase +from django.utils import timezone +from freezegun import freeze_time +from shared.django_apps.codecov_auth.tests.factories import AccountFactory +from shared.django_apps.core.tests.factories import OwnerFactory +from shared.license import LicenseInformation +from shared.plan.constants import PlanName, TrialStatus +from shared.utils.test_utils import mock_config_helper + +from billing.helpers import mock_all_plans_and_tiers + +from .helper import GraphQLTestHelper + + +class TestPlanType(GraphQLTestHelper, TestCase): + @pytest.fixture(scope="function", autouse=True) + def inject_mocker(request, mocker): + request.mocker = mocker + + def setUp(self): + mock_all_plans_and_tiers() + self.current_org = OwnerFactory( + username="random-plan-user", + service="github", + trial_start_date=timezone.now(), + trial_end_date=timezone.now() + timedelta(days=14), + ) + + @freeze_time("2023-06-19") + def test_owner_plan_data_when_trialing(self): + now = timezone.now() + later = timezone.now() + timedelta(days=14) + current_org = OwnerFactory( + username="random-plan-user", + service="github", + plan=PlanName.TRIAL_PLAN_NAME.value, + trial_start_date=now, + trial_end_date=later, + trial_status=TrialStatus.ONGOING.value, + pretrial_users_count=234, + plan_user_count=123, + ) + query = """{ + owner(username: "%s") { + plan { + trialStatus + trialEndDate + trialStartDate + trialTotalDays + marketingName + value + tierName + billingRate + baseUnitPrice + benefits + monthlyUploadLimit + pretrialUsersCount + planUserCount + isEnterprisePlan + isFreePlan + isProPlan + isSentryPlan + isTeamPlan + isTrialPlan + } + } + } + """ % (current_org.username) + data = self.gql_request(query, owner=current_org) + assert data["owner"]["plan"] == { + "trialStatus": "ONGOING", + "trialEndDate": "2023-07-03T00:00:00", + "trialStartDate": "2023-06-19T00:00:00", + "trialTotalDays": 14, + "marketingName": "Developer", + "value": "users-trial", + "tierName": "trial", + "billingRate": None, + "baseUnitPrice": 0, + "benefits": [ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + "monthlyUploadLimit": None, + "pretrialUsersCount": 234, + "planUserCount": 123, + "isEnterprisePlan": False, + "isFreePlan": False, + "isProPlan": False, + "isSentryPlan": False, + "isTeamPlan": False, + "isTrialPlan": True, + } + + def test_owner_plan_data_with_account(self): + self.current_org.account = AccountFactory( + plan=PlanName.CODECOV_PRO_YEARLY.value, + plan_seat_count=25, + ) + self.current_org.save() + query = """{ + owner(username: "%s") { + plan { + marketingName + value + tierName + billingRate + baseUnitPrice + planUserCount + isEnterprisePlan + isFreePlan + isProPlan + isSentryPlan + isTeamPlan + isTrialPlan + } + } + } + """ % (self.current_org.username) + data = self.gql_request(query, owner=self.current_org) + assert data["owner"]["plan"] == { + "marketingName": "Pro", + "value": "users-pr-inappy", + "tierName": "pro", + "billingRate": "annually", + "baseUnitPrice": 10, + "planUserCount": 25, + "isEnterprisePlan": False, + "isFreePlan": False, + "isProPlan": True, + "isSentryPlan": False, + "isTeamPlan": False, + "isTrialPlan": False, + } + + def test_owner_plan_data_has_seats_left(self): + current_org = OwnerFactory( + username="random-plan-user", + service="github", + plan=PlanName.TRIAL_PLAN_NAME.value, + trial_status=TrialStatus.ONGOING.value, + plan_user_count=2, + plan_activated_users=[], + ) + query = """{ + owner(username: "%s") { + plan { + hasSeatsLeft + } + } + } + """ % (current_org.username) + data = self.gql_request(query, owner=current_org) + assert data["owner"]["plan"] == {"hasSeatsLeft": True} + + @patch("shared.self_hosted.service.get_current_license") + def test_plan_user_count_for_enterprise_org(self, mocked_license): + """ + If an Org has an enterprise license, number_allowed_users from their license + should be used instead of plan_user_count on the Org object. + """ + mock_enterprise_license = LicenseInformation( + is_valid=True, + message=None, + url="https://codeov.mysite.com", + number_allowed_users=5, + number_allowed_repos=10, + expires=datetime.strptime("2020-05-09 00:00:00", "%Y-%m-%d %H:%M:%S"), + is_trial=False, + is_pr_billing=True, + ) + mocked_license.return_value = mock_enterprise_license + mock_config_helper( + self.mocker, configs={"setup.enterprise_license": mock_enterprise_license} + ) + + enterprise_org = OwnerFactory( + username="random-plan-user", + service="github", + plan=PlanName.CODECOV_PRO_YEARLY.value, + plan_user_count=1, + plan_activated_users=[], + ) + for i in range(4): + new_owner = OwnerFactory() + enterprise_org.plan_activated_users.append(new_owner.ownerid) + enterprise_org.save() + + other_org_in_enterprise = OwnerFactory( + service="github", + plan=PlanName.CODECOV_PRO_YEARLY.value, + plan_user_count=1, + plan_activated_users=[], + ) + for i in range(4): + new_owner = OwnerFactory() + other_org_in_enterprise.plan_activated_users.append(new_owner.ownerid) + other_org_in_enterprise.save() + + query = """{ + owner(username: "%s") { + plan { + planUserCount + hasSeatsLeft + } + } + } + """ % (enterprise_org.username) + data = self.gql_request(query, owner=enterprise_org) + assert data["owner"]["plan"]["planUserCount"] == 5 + assert data["owner"]["plan"]["hasSeatsLeft"] == False + + @patch("shared.self_hosted.service.get_current_license") + def test_plan_user_count_for_enterprise_org_invaild_license(self, mocked_license): + mock_enterprise_license = LicenseInformation( + is_valid=False, + ) + mocked_license.return_value = mock_enterprise_license + mock_config_helper( + self.mocker, configs={"setup.enterprise_license": mock_enterprise_license} + ) + + enterprise_org = OwnerFactory( + username="random-plan-user", + service="github", + plan=PlanName.CODECOV_PRO_YEARLY.value, + plan_user_count=1, + plan_activated_users=[], + ) + query = """{ + owner(username: "%s") { + plan { + planUserCount + hasSeatsLeft + } + } + } + """ % (enterprise_org.username) + data = self.gql_request(query, owner=enterprise_org) + assert data["owner"]["plan"]["planUserCount"] == 0 + assert data["owner"]["plan"]["hasSeatsLeft"] == False + + def test_owner_plan_data_when_trial_status_is_none(self): + now = timezone.now() + later = now + timedelta(days=14) + current_org = OwnerFactory( + username="random-plan-user", + service="github", + plan=PlanName.TRIAL_PLAN_NAME.value, + trial_start_date=now, + trial_end_date=later, + trial_status=None, + ) + query = """{ + owner(username: "%s") { + plan { + trialStatus + } + } + } + """ % (current_org.username) + data = self.gql_request(query, owner=current_org) + assert data["owner"]["plan"]["trialStatus"] == "NOT_STARTED" diff --git a/apps/codecov-api/graphql_api/tests/test_plan_representation.py b/apps/codecov-api/graphql_api/tests/test_plan_representation.py new file mode 100644 index 0000000000..3f8b5d5958 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_plan_representation.py @@ -0,0 +1,63 @@ +from datetime import timedelta + +from django.test import TestCase +from django.utils import timezone +from freezegun import freeze_time +from shared.django_apps.core.tests.factories import OwnerFactory +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName, TrialStatus + +from billing.helpers import mock_all_plans_and_tiers + +from .helper import GraphQLTestHelper + + +class TestPlanRepresentationsType(GraphQLTestHelper, TestCase): + def setUp(self): + mock_all_plans_and_tiers() + self.current_org = OwnerFactory( + username="random-plan-user", + service="github", + trial_start_date=timezone.now(), + trial_end_date=timezone.now() + timedelta(days=14), + plan=PlanName.USERS_DEVELOPER.value, + ) + + @freeze_time("2023-06-19") + def test_owner_pretrial_plan_data_when_trialing(self): + now = timezone.now() + later = timezone.now() + timedelta(days=14) + current_org = OwnerFactory( + username="random-plan-user", + service="github", + plan=PlanName.TRIAL_PLAN_NAME.value, + trial_start_date=now, + trial_end_date=later, + trial_status=TrialStatus.ONGOING.value, + pretrial_users_count=234, + ) + query = """{ + owner(username: "%s") { + pretrialPlan { + marketingName + value + billingRate + baseUnitPrice + benefits + monthlyUploadLimit + } + } + } + """ % (current_org.username) + data = self.gql_request(query, owner=current_org) + assert data["owner"]["pretrialPlan"] == { + "marketingName": "Developer", + "value": DEFAULT_FREE_PLAN, + "billingRate": None, + "baseUnitPrice": 0, + "benefits": [ + "Up to 234 users", + "Unlimited public repositories", + "Unlimited private repositories", + ], + "monthlyUploadLimit": 250, + } diff --git a/apps/codecov-api/graphql_api/tests/test_pull.py b/apps/codecov-api/graphql_api/tests/test_pull.py new file mode 100644 index 0000000000..984806ad64 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_pull.py @@ -0,0 +1,682 @@ +import os +from datetime import datetime, timedelta +from unittest.mock import patch + +from django.test import TestCase +from freezegun import freeze_time +from shared.api_archive.archive import ArchiveService +from shared.bundle_analysis import StoragePaths +from shared.bundle_analysis.storage import get_bucket_name +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) +from shared.storage.memory import MemoryStorageService + +from compare.tests.factories import CommitComparisonFactory +from core.models import Commit +from reports.models import CommitReport +from reports.tests.factories import CommitReportFactory, ReportLevelTotalsFactory + +from .helper import GraphQLTestHelper, paginate_connection + +query_list_pull_request = """{ + me { + owner { + repository(name: "test-repo-for-pull") { + ... on Repository { + name + pulls { + edges { + node { + title + pullId + } + } + } + } + } + } + } +} +""" + +default_pull_request_detail_query = """ + title + state + pullId + updatestamp + author { + username + } + head { + coverageAnalytics { + totals { + coverage + } + } + } + comparedTo { + commitid + } + compareWithBase { + __typename + ... on Comparison { + patchTotals { + coverage + } + } + } + behindBy + behindByCommit +""" + +pull_request_detail_query_with_bundle_analysis = """ + title + state + pullId + updatestamp + author { + username + } + head { + coverageAnalytics { + totals { + coverage + } + } + } + comparedTo { + commitid + } + compareWithBase { + __typename + ... on Comparison { + patchTotals { + coverage + } + } + } + bundleAnalysisCompareWithBase { + __typename + ... on BundleAnalysisComparison { + bundleData { + size { + uncompress + } + } + } + } + behindBy + behindByCommit +""" + +pull_request_bundle_analysis_missing_reports = """ + bundleAnalysisCompareWithBase { + __typename + ... on BundleAnalysisComparison { + bundleData { + size { + uncompress + } + } + } + } +""" + +query_pull_request_detail = """{ + me { + owner { + repository(name: "test-repo-for-pull") { + ... on Repository { + name + pull(id: %s) { + %s + } + } + } + } + } +} +""" + + +class TestPullRequestList(GraphQLTestHelper, TestCase): + def fetch_list_pull_request(self): + data = self.gql_request(query_list_pull_request, owner=self.owner) + return paginate_connection(data["me"]["owner"]["repository"]["pulls"]) + + def fetch_one_pull_request(self, id, query=default_pull_request_detail_query): + data = self.gql_request( + query_pull_request_detail % (id, query), owner=self.owner + ) + return data["me"]["owner"]["repository"]["pull"] + + def setUp(self): + self.owner = OwnerFactory(username="test-pull-user") + self.repository = RepositoryFactory( + author=self.owner, active=True, private=True, name="test-repo-for-pull" + ) + + def test_fetch_list_pull_request(self): + pull_1 = PullFactory(repository=self.repository, title="a") + pull_2 = PullFactory(repository=self.repository, title="b") + pulls = self.fetch_list_pull_request() + pull_titles = [pull["title"] for pull in pulls] + assert pull_1.title in pull_titles + assert pull_2.title in pull_titles + + @freeze_time("2021-02-02 00:00:00") + @patch("core.commands.pull.interactors.fetch_pull_request.TaskService") + def test_when_repository_has_null_compared_to(self, mock_task_service): + my_pull = PullFactory( + repository=self.repository, + title="test-null-base", + author=self.owner, + head=CommitFactory( + repository=self.repository, + author=self.owner, + commitid="5672734ij1n234918231290j12nasdfioasud0f9", + ).commitid, + compared_to=None, + ) + with freeze_time("2021-02-02 06:00:00"): + pull = self.fetch_one_pull_request( + my_pull.pullid, pull_request_detail_query_with_bundle_analysis + ) + assert pull == { + "title": "test-null-base", + "state": "OPEN", + "pullId": my_pull.pullid, + "updatestamp": "2021-02-02T00:00:00", + "author": {"username": "test-pull-user"}, + "head": {"coverageAnalytics": {"totals": None}}, + "comparedTo": None, + "compareWithBase": { + "__typename": "MissingBaseCommit", + }, + "bundleAnalysisCompareWithBase": { + "__typename": "MissingBaseCommit", + }, + "behindBy": None, + "behindByCommit": None, + } + mock_task_service.return_value.pulls_sync.assert_called_with( + my_pull.repository.repoid, my_pull.pullid + ) + + @freeze_time("2021-02-02 00:00:00") + @patch("core.commands.pull.interactors.fetch_pull_request.TaskService") + def test_when_repository_has_null_author(self, mock_task_service): + PullFactory( + repository=self.repository, + title="dummy-first-pr", + ) + second_pr = PullFactory( + repository=self.repository, + title="test-null-author", + author=None, + head=None, + compared_to=None, + ) + pull = self.fetch_one_pull_request( + second_pr.pullid, pull_request_detail_query_with_bundle_analysis + ) + assert pull == { + "title": "test-null-author", + "state": "OPEN", + "pullId": second_pr.pullid, + "updatestamp": "2021-02-02T00:00:00", + "author": None, + "head": None, + "comparedTo": None, + "compareWithBase": { + "__typename": "MissingBaseCommit", + }, + "bundleAnalysisCompareWithBase": { + "__typename": "MissingBaseCommit", + }, + "behindBy": None, + "behindByCommit": None, + } + mock_task_service.return_value.pulls_sync.assert_not_called() + + @freeze_time("2021-02-02") + @patch("core.commands.pull.interactors.fetch_pull_request.TaskService") + def test_when_repository_has_null_head_no_parent_report(self, mock_task_service): + PullFactory( + repository=self.repository, + title="dummy-first-pr", + author=self.owner, + ) + second_pr = PullFactory( + repository=self.repository, + title="test-null-head", + author=self.owner, + head=None, + ) + pull = self.fetch_one_pull_request( + second_pr.pullid, pull_request_detail_query_with_bundle_analysis + ) + assert pull == { + "title": "test-null-head", + "state": "OPEN", + "pullId": second_pr.pullid, + "updatestamp": "2021-02-02T00:00:00", + "author": {"username": "test-pull-user"}, + "head": None, + "comparedTo": None, + "compareWithBase": { + "__typename": "MissingHeadCommit", + }, + "bundleAnalysisCompareWithBase": { + "__typename": "MissingHeadReport", + }, + "behindBy": None, + "behindByCommit": None, + } + mock_task_service.return_value.pulls_sync.assert_not_called() + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_when_repository_has_null_head_has_parent_report(self, get_storage_service): + os.system("rm -rf /tmp/bundle_analysis_*") + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + parent_commit = CommitFactory(repository=self.repository) + + base_commit_report = CommitReportFactory( + commit=parent_commit, + report_type=CommitReport.ReportType.BUNDLE_ANALYSIS, + ) + + my_pull = PullFactory( + repository=self.repository, + title="test-pull-request", + author=self.owner, + head=None, + compared_to=base_commit_report.commit.commitid, + behind_by=23, + behind_by_commit="1089nf898as-jdf09hahs09fgh", + ) + + with open("./services/tests/samples/base_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repository), + report_key=base_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + bundleAnalysisCompareWithBase { + __typename + ... on BundleAnalysisComparison { + bundleData { + size { + uncompress + } + } + bundleChange { + size { + uncompress + } + } + } + } + """ + + pull = self.fetch_one_pull_request(my_pull.pullid, query) + + assert pull == { + "bundleAnalysisCompareWithBase": { + "__typename": "BundleAnalysisComparison", + "bundleData": { + "size": { + "uncompress": 165165, + } + }, + "bundleChange": { + "size": { + "uncompress": 0, + } + }, + } + } + + for file in os.listdir("/tmp"): + assert not file.startswith("bundle_analysis_") + + os.system("rm -rf /tmp/bundle_analysis_*") + + @freeze_time("2021-02-02") + def test_when_pr_is_first_pr_in_repo(self): + first_pr = PullFactory( + repository=self.repository, + title="dummy-first-pr", + author=self.owner, + compared_to=None, + ) + + res = self.fetch_one_pull_request( + first_pr.pullid, pull_request_detail_query_with_bundle_analysis + ) + assert res == { + "title": "dummy-first-pr", + "state": "OPEN", + "pullId": first_pr.pullid, + "updatestamp": "2021-02-02T00:00:00", + "author": {"username": "test-pull-user"}, + "head": None, + "comparedTo": None, + "compareWithBase": { + "__typename": "FirstPullRequest", + }, + "bundleAnalysisCompareWithBase": { + "__typename": "FirstPullRequest", + }, + "behindBy": None, + "behindByCommit": None, + } + + @freeze_time("2021-02-02") + def test_when_repository_has_missing_head_commit(self): + PullFactory( + repository=self.repository, + title="test-missing-head-commit", + author=self.owner, + ) + second_pull = PullFactory( + repository=self.repository, + title="second-pr-so-it-doesn't-fetch-first-pr", + author=self.owner, + ) + Commit.objects.filter( + repository_id=self.repository.pk, + commitid=second_pull.head, + ).delete() + + res = self.fetch_one_pull_request(second_pull.pullid) + assert res == { + "title": "second-pr-so-it-doesn't-fetch-first-pr", + "state": "OPEN", + "pullId": second_pull.pullid, + "updatestamp": "2021-02-02T00:00:00", + "author": {"username": "test-pull-user"}, + "head": None, + "comparedTo": None, + "compareWithBase": { + "__typename": "MissingComparison", + }, + "behindBy": None, + "behindByCommit": None, + } + + @freeze_time("2021-02-02") + def test_with_complete_pull_request(self): + head = CommitFactory( + repository=self.repository, + author=self.owner, + commitid="5672734ij1n234918231290j12nasdfioasud0f9", + totals={"c": "78.38", "diff": [0, 0, 0, 0, 0, "14"]}, + ) + report = CommitReportFactory(commit=head) + ReportLevelTotalsFactory(report=report, coverage=78.38) + compared_to = CommitFactory( + repository=self.repository, + author=self.owner, + commitid="9asd78fa7as8d8fa97s8d7fgagsd8fa9asd8f77s", + ) + CommitComparisonFactory( + base_commit=compared_to, + compare_commit=head, + patch_totals={"coverage": 0.8739}, + ) + my_pull = PullFactory( + repository=self.repository, + title="test-pull-request", + author=self.owner, + head=head.commitid, + compared_to=compared_to.commitid, + behind_by=23, + behind_by_commit="1089nf898as-jdf09hahs09fgh", + ) + pull = self.fetch_one_pull_request(my_pull.pullid) + assert pull == { + "title": "test-pull-request", + "state": "OPEN", + "pullId": my_pull.pullid, + "updatestamp": "2021-02-02T00:00:00", + "author": {"username": "test-pull-user"}, + "head": {"coverageAnalytics": {"totals": {"coverage": 78.38}}}, + "comparedTo": {"commitid": "9asd78fa7as8d8fa97s8d7fgagsd8fa9asd8f77s"}, + "compareWithBase": { + "__typename": "Comparison", + "patchTotals": {"coverage": 87.39}, + }, + "behindBy": 23, + "behindByCommit": "1089nf898as-jdf09hahs09fgh", + } + + def test_compare_bundle_analysis_missing_reports(self): + head = CommitFactory( + repository=self.repository, + author=self.owner, + commitid="cool-commit-id", + totals={"c": "78.38", "diff": [0, 0, 0, 0, 0, "14"]}, + ) + compared_to = CommitFactory( + repository=self.repository, + author=self.owner, + commitid="blah", + ) + + my_pull = PullFactory( + repository=self.repository, + author=self.owner, + head=head.commitid, + compared_to=compared_to.commitid, + ) + + pull = self.fetch_one_pull_request( + my_pull.pullid, pull_request_bundle_analysis_missing_reports + ) + assert pull == { + "bundleAnalysisCompareWithBase": {"__typename": "MissingHeadReport"} + } + + CommitReportFactory( + commit=head, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + pull = self.fetch_one_pull_request( + my_pull.pullid, pull_request_bundle_analysis_missing_reports + ) + assert pull == { + "bundleAnalysisCompareWithBase": {"__typename": "MissingBaseReport"} + } + + @patch("graphql_api.dataloader.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_sqlite_file_deleted(self, get_storage_service): + os.system("rm -rf /tmp/bundle_analysis_*") + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + parent_commit = CommitFactory(repository=self.repository) + commit = CommitFactory( + repository=self.repository, + totals={"c": "12", "diff": [0, 0, 0, 0, 0, "14"]}, + parent_commit_id=parent_commit.commitid, + ) + + base_commit_report = CommitReportFactory( + commit=parent_commit, + report_type=CommitReport.ReportType.BUNDLE_ANALYSIS, + ) + head_commit_report = CommitReportFactory( + commit=commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + my_pull = PullFactory( + repository=self.repository, + title="test-pull-request", + author=self.owner, + head=head_commit_report.commit.commitid, + compared_to=base_commit_report.commit.commitid, + behind_by=23, + behind_by_commit="1089nf898as-jdf09hahs09fgh", + ) + + with open("./services/tests/samples/base_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repository), + report_key=base_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + with open("./services/tests/samples/head_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repository), + report_key=head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + query = """ + bundleAnalysisCompareWithBase { + __typename + ... on BundleAnalysisComparison { + bundleData { + size { + uncompress + } + } + } + } + """ + + pull = self.fetch_one_pull_request(my_pull.pullid, query) + + assert pull == { + "bundleAnalysisCompareWithBase": { + "__typename": "BundleAnalysisComparison", + "bundleData": { + "size": { + "uncompress": 201720, + } + }, + } + } + + for file in os.listdir("/tmp"): + assert not file.startswith("bundle_analysis_") + + os.system("rm -rf /tmp/bundle_analysis_*") + + @freeze_time("2021-02-02") + def test_pull_no_patch_totals(self): + head = CommitFactory( + repository=self.repository, + author=self.owner, + commitid="5672734ij1n234918231290j12nasdfioasud0f9", + totals=None, + ) + report = CommitReportFactory(commit=head) + ReportLevelTotalsFactory(report=report, coverage=78.38) + compared_to = CommitFactory( + repository=self.repository, + author=self.owner, + commitid="9asd78fa7as8d8fa97s8d7fgagsd8fa9asd8f77s", + ) + CommitComparisonFactory( + base_commit=compared_to, compare_commit=head, patch_totals=None + ) + my_pull = PullFactory( + repository=self.repository, + title="test-pull-request", + author=self.owner, + head=head.commitid, + compared_to=compared_to.commitid, + ) + pull = self.fetch_one_pull_request(my_pull.pullid) + assert pull == { + "title": "test-pull-request", + "state": "OPEN", + "pullId": my_pull.pullid, + "updatestamp": "2021-02-02T00:00:00", + "author": {"username": "test-pull-user"}, + "head": {"coverageAnalytics": {"totals": {"coverage": 78.38}}}, + "comparedTo": {"commitid": "9asd78fa7as8d8fa97s8d7fgagsd8fa9asd8f77s"}, + "compareWithBase": { + "__typename": "Comparison", + "patchTotals": None, + }, + "behindBy": None, + "behindByCommit": None, + } + + @freeze_time("2021-02-02") + def test_fetch_commits_request(self): + query = """ + commits { + totalCount + edges { + node { + commitid + } + } + } + """ + my_pull = PullFactory(repository=self.repository) + + CommitFactory( + repository=self.repository, + pullid=my_pull.pullid, + commitid="11111", + timestamp=datetime.today() - timedelta(days=1), + ) + CommitFactory( + repository=self.repository, + pullid=my_pull.pullid, + commitid="22222", + timestamp=datetime.today() - timedelta(days=2), + ) + CommitFactory( + repository=self.repository, + pullid=my_pull.pullid, + commitid="33333", + timestamp=datetime.today() - timedelta(days=3), + ) + CommitFactory( + repository=self.repository, + pullid=my_pull.pullid, + commitid="44444", + timestamp=datetime.today() - timedelta(days=3), + deleted=True, + ) + + pull = self.fetch_one_pull_request(my_pull.pullid, query) + + assert pull == { + "commits": { + "edges": [ + {"node": {"commitid": "11111"}}, + {"node": {"commitid": "22222"}}, + {"node": {"commitid": "33333"}}, + ], + "totalCount": 3, + } + } + + def test_fetch_first_pull(self): + pull1 = PullFactory(repository=self.repository) + assert self.fetch_one_pull_request(pull1.pullid, "firstPull") == { + "firstPull": True + } + pull2 = PullFactory(repository=self.repository) + assert self.fetch_one_pull_request(pull1.pullid, "firstPull") == { + "firstPull": True + } + assert self.fetch_one_pull_request(pull2.pullid, "firstPull") == { + "firstPull": False + } diff --git a/apps/codecov-api/graphql_api/tests/test_pull_comparison.py b/apps/codecov-api/graphql_api/tests/test_pull_comparison.py new file mode 100644 index 0000000000..93421d7c48 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_pull_comparison.py @@ -0,0 +1,1034 @@ +from collections import namedtuple +from unittest.mock import PropertyMock, patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + CommitWithReportFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) +from shared.reports.types import ReportTotals +from shared.utils.merge import LineType + +from compare.models import CommitComparison +from compare.tests.factories import CommitComparisonFactory, FlagComparisonFactory +from reports.tests.factories import RepositoryFlagFactory + +from .helper import GraphQLTestHelper + +base_query = """{ + me { + owner { + repository(name: "%s") { + ... on Repository { + pull(id: %s) { + %s + } + } + } + } + } +} +""" + +MockSegmentComparison = namedtuple( + "MockSegmentComparison", ["header", "lines", "has_unintended_changes"] +) +MockLineComparison = namedtuple( + "MockLineComparison", + ["number", "coverage", "value", "is_diff", "hit_session_ids"], +) + + +class TestPullComparison(TestCase, GraphQLTestHelper): + def _request(self, query): + data = self.gql_request( + base_query % (self.repository.name, self.pull.pullid, query), + owner=self.owner, + ) + return data["me"]["owner"]["repository"]["pull"] + + def setUp(self): + # mock reports for all tests in this class + self.head_report_patcher = patch( + "services.comparison.Comparison.head_report", new_callable=PropertyMock + ) + self.head_report = self.head_report_patcher.start() + self.head_report.return_value = None + self.addCleanup(self.head_report_patcher.stop) + self.head_report_without_diff_patcher = patch( + "services.comparison.Comparison.head_report_without_applied_diff", + new_callable=PropertyMock, + ) + self.head_report_without_diff = self.head_report_without_diff_patcher.start() + self.head_report_without_diff.return_value = None + self.addCleanup(self.head_report_without_diff_patcher.stop) + self.base_report_patcher = patch( + "services.comparison.Comparison.base_report", new_callable=PropertyMock + ) + self.base_report = self.base_report_patcher.start() + self.base_report.return_value = None + self.addCleanup(self.base_report_patcher.stop) + + self.owner = OwnerFactory() + self.repository = RepositoryFactory( + author=self.owner, + active=True, + private=True, + ) + self.base_commit = CommitWithReportFactory( + repository=self.repository, + author=self.owner, + ) + self.head_commit = CommitWithReportFactory( + parent_commit_id=self.base_commit.commitid, + repository=self.repository, + author=self.owner, + ) + self.commit_comparison = CommitComparisonFactory( + base_commit=self.base_commit, + compare_commit=self.head_commit, + state=CommitComparison.CommitComparisonStates.PROCESSED, + ) + self.pull = PullFactory( + pullid=2, + repository=self.repository, + author=self.owner, + head=self.head_commit.commitid, + compared_to=self.base_commit.commitid, + ) + + def test_pull_comparison_totals(self): + base_totals = self.base_commit.reports.first().reportleveltotals + base_totals.coverage = 75.0 + base_totals.files = 1 + base_totals.lines = 6 + base_totals.hits = 3 + base_totals.misses = 2 + base_totals.partials = 1 + base_totals.branches = 0 + base_totals.methods = 0 + base_totals.save() + + head_totals = self.head_commit.reports.first().reportleveltotals + head_totals.coverage = 75.0 + head_totals.files = 1 + head_totals.lines = 6 + head_totals.hits = 3 + head_totals.misses = 2 + head_totals.partials = 1 + head_totals.branches = 0 + head_totals.methods = 0 + head_totals.save() + + query = """ + pullId + compareWithBase { + ... on Comparison { + baseTotals { + percentCovered + fileCount + lineCount + hitsCount + missesCount + partialsCount + } + headTotals { + percentCovered + fileCount + lineCount + hitsCount + missesCount + partialsCount + } + } + } + """ + + res = self._request(query) + totals = { + "percentCovered": 75.0, + "fileCount": 1, + "lineCount": 6, + "hitsCount": 3, + "missesCount": 2, + "partialsCount": 1, + } + assert res == { + "pullId": self.pull.pullid, + "compareWithBase": { + "baseTotals": totals, + "headTotals": totals, + }, + } + + def test_pull_no_flag_comparisons_for_commit_comparison(self): + # Just running this w/ the commit_comparison in setup will yield nothing + query = """ + compareWithBase { + ... on Comparison { + flagComparisons { + name + patchTotals { + percentCovered + } + headTotals { + percentCovered + } + } + } + } + """ + + res = self._request(query) + assert res == {"compareWithBase": {"flagComparisons": []}} + + @patch("services.task.TaskService.compute_comparisons") + def test_pull_different_number_of_head_and_base_reports_without_context(self, _): + # Just running this w/ the commit_comparison in setup will yield nothing + query = """ + compareWithBase { + ... on Comparison { + hasDifferentNumberOfHeadAndBaseReports + } + } + """ + self.commit_comparison.delete() + res = self._request(query) + assert res == { + "compareWithBase": {"hasDifferentNumberOfHeadAndBaseReports": False} + } + + @patch("services.task.TaskService.compute_comparisons") + def test_pull_component_comparison_without_context(self, _): + # Just running this w/ the commit_comparison in setup will yield nothing + query = """ + compareWithBase { + ... on Comparison { + componentComparisons { + name + } + } + } + """ + self.commit_comparison.delete() + res = self._request(query) + assert res == {"compareWithBase": {"componentComparisons": []}} + + def test_pull_flag_comparisons(self): + FlagComparisonFactory( + commit_comparison=self.commit_comparison, + repositoryflag=RepositoryFlagFactory( + repository=self.repository, flag_name="flag_one" + ), + head_totals={"coverage": "85.71429"}, + base_totals={"coverage": "92.2973"}, + patch_totals={"coverage": "29.28364"}, + ) + FlagComparisonFactory( + commit_comparison=self.commit_comparison, + repositoryflag=RepositoryFlagFactory( + repository=self.repository, flag_name="flag_two" + ), + head_totals={"coverage": "75.273820"}, + base_totals={"coverage": "16.293"}, + patch_totals={"coverage": "68.283496"}, + ) + query = """ + compareWithBase { + ... on Comparison { + flagComparisonsCount + flagComparisons { + name + patchTotals { + percentCovered + } + headTotals { + percentCovered + } + baseTotals { + percentCovered + } + } + } + } + """ + + res = self._request(query) + assert res == { + "compareWithBase": { + "flagComparisonsCount": 2, + "flagComparisons": [ + { + "name": "flag_one", + "patchTotals": {"percentCovered": 29.28364}, + "headTotals": {"percentCovered": 85.71429}, + "baseTotals": {"percentCovered": 92.2973}, + }, + { + "name": "flag_two", + "patchTotals": {"percentCovered": 68.283496}, + "headTotals": {"percentCovered": 75.27382}, + "baseTotals": {"percentCovered": 16.293}, + }, + ], + } + } + + def test_pull_flag_comparisons_with_filter(self): + FlagComparisonFactory( + commit_comparison=self.commit_comparison, + repositoryflag=RepositoryFlagFactory( + repository=self.repository, flag_name="flag_one" + ), + head_totals={"coverage": "85.71429"}, + base_totals={"coverage": "92.2973"}, + patch_totals={"coverage": "29.28364"}, + ) + FlagComparisonFactory( + commit_comparison=self.commit_comparison, + repositoryflag=RepositoryFlagFactory( + repository=self.repository, flag_name="flag_two" + ), + head_totals={"coverage": "75.273820"}, + base_totals={"coverage": "16.293"}, + patch_totals={"coverage": "68.283496"}, + ) + query = """ + compareWithBase { + ... on Comparison { + flagComparisons(filters: { term: "flag_two"}) { + name + patchTotals { + percentCovered + } + headTotals { + percentCovered + } + baseTotals { + percentCovered + } + } + } + } + """ + + res = self._request(query) + assert res == { + "compareWithBase": { + "flagComparisons": [ + { + "name": "flag_two", + "patchTotals": {"percentCovered": 68.283496}, + "headTotals": {"percentCovered": 75.27382}, + "baseTotals": {"percentCovered": 16.293}, + }, + ], + } + } + + @patch( + "services.comparison.Comparison.has_different_number_of_head_and_base_sessions", + new_callable=PropertyMock, + ) + def test_compare_with_base_has_different_number_of_reports_on_head_and_base( + self, mock_has_different_number_of_head_and_base_sessions + ): + mock_has_different_number_of_head_and_base_sessions.return_value = True + query = """ + compareWithBase { + ... on Comparison { + hasDifferentNumberOfHeadAndBaseReports + } + } + """ + + res = self._request(query) + assert res == { + "compareWithBase": {"hasDifferentNumberOfHeadAndBaseReports": True} + } + + @patch( + "services.comparison.ComparisonReport.files", + new_callable=PropertyMock, + ) + def test_pull_comparison_impacted_files( + self, + files_mock, + ): + base_report_totals = ReportTotals( + coverage=75.0, + files=1, + lines=6, + hits=3, + misses=2, + partials=1, + ) + head_report_totals = ReportTotals( + coverage=85.0, + files=1, + lines=6, + hits=3, + misses=2, + partials=1, + diff=None, + ) + patch_totals = ReportTotals( + coverage=0.5, + files=1, + lines=2, + hits=1, + misses=1, + partials=0, + ) + + TestImpactedFile = namedtuple( + "TestImpactedFile", + [ + "base_name", + "head_name", + "base_coverage", + "head_coverage", + "patch_coverage", + ], + ) + + files_mock.return_value = [ + TestImpactedFile( + base_name="foo.py", + head_name="bar.py", + base_coverage=base_report_totals, + head_coverage=head_report_totals, + patch_coverage=patch_totals, + ), + TestImpactedFile( + base_name=None, + head_name="baz.py", + base_coverage=base_report_totals, + head_coverage=head_report_totals, + patch_coverage=patch_totals, + ), + ] + + query = """ + pullId + compareWithBase { + ... on Comparison { + impactedFiles(filters:{}) { + ... on ImpactedFiles { + results { + baseName + headName + isNewFile + isRenamedFile + isDeletedFile + baseCoverage { + percentCovered + fileCount + lineCount + hitsCount + missesCount + partialsCount + } + headCoverage { + percentCovered + fileCount + lineCount + hitsCount + missesCount + partialsCount + } + patchCoverage { + percentCovered + fileCount + lineCount + hitsCount + missesCount + partialsCount + } + } + } + ... on UnknownFlags { + message + } + } + } + } + """ + + base_totals = { + "percentCovered": 75.0, + "fileCount": 1, + "lineCount": 6, + "hitsCount": 3, + "missesCount": 2, + "partialsCount": 1, + } + head_totals = { + "percentCovered": 85.0, + "fileCount": 1, + "lineCount": 6, + "hitsCount": 3, + "missesCount": 2, + "partialsCount": 1, + } + patch_totals = { + "percentCovered": 0.5, + "fileCount": 1, + "lineCount": 2, + "hitsCount": 1, + "missesCount": 1, + "partialsCount": 0, + } + + res = self._request(query) + assert res == { + "pullId": self.pull.pullid, + "compareWithBase": { + "impactedFiles": { + "results": [ + { + "baseName": "foo.py", + "headName": "bar.py", + "isNewFile": False, + "isRenamedFile": True, + "isDeletedFile": False, + "baseCoverage": base_totals, + "headCoverage": head_totals, + "patchCoverage": patch_totals, + }, + { + "baseName": None, + "headName": "baz.py", + "isNewFile": True, + "isRenamedFile": False, + "isDeletedFile": False, + "baseCoverage": base_totals, + "headCoverage": head_totals, + "patchCoverage": patch_totals, + }, + ] + }, + }, + } + + @patch( + "services.comparison.PullRequestComparison.get_file_comparison", + ) + @patch( + "services.comparison.PullRequestComparison.files", + new_callable=PropertyMock, + ) + @patch( + "services.comparison.ComparisonReport.files", + new_callable=PropertyMock, + ) + def test_pull_comparison_line_comparisons( + self, comparison_files_mock, files_mock, get_file_comparison + ): + TestFileComparison = namedtuple( + "TestFileComparison", + ["name", "head_name", "base_name", "has_diff", "has_changes", "segments"], + ) + + test_files = [ + TestFileComparison( + name={"head": "file1", "base": "file1"}, + head_name="file1", + base_name="file1", + has_diff=True, + has_changes=False, + segments=[ + MockSegmentComparison( + header=(1, 2, 3, 4), + has_unintended_changes=False, + lines=[ + MockLineComparison( + number={ + "head": "1", + "base": "1", + }, + coverage={ + "base": LineType.hit, + "head": LineType.hit, + }, + value=" line1", + is_diff=False, + hit_session_ids=[0], + ), + MockLineComparison( + number={ + "base": None, + "head": "2", + }, + coverage={ + "base": None, + "head": LineType.hit, + }, + value="+ line2", + is_diff=True, + hit_session_ids=[0, 1], + ), + ], + ), + ], + ), + TestFileComparison( + name={"head": "file2", "base": "file2"}, + head_name="file2", + base_name="file2", + has_diff=False, + has_changes=True, + segments=[ + MockSegmentComparison( + header=(1, None, 1, None), + has_unintended_changes=True, + lines=[ + MockLineComparison( + number={ + "head": "1", + "base": "1", + }, + coverage={ + "base": LineType.miss, + "head": LineType.hit, + }, + value=" line1", + is_diff=True, + hit_session_ids=[0], + ), + ], + ), + ], + ), + ] + + comparison_files_mock.return_value = test_files + files_mock.return_value = test_files + get_file_comparison.side_effect = test_files + + query = """ + pullId + compareWithBase { + ... on Comparison { + impactedFiles(filters: {}){ + ... on ImpactedFiles { + results { + segments { + ... on SegmentComparisons { + results { + header + hasUnintendedChanges + lines { + baseNumber + headNumber + baseCoverage + headCoverage + content + coverageInfo(ignoredUploadIds: [1]) { + hitCount + hitUploadIds + } + } + } + } + } + } + } + ... on UnknownFlags { + message + } + } + } + } + """ + + res = self._request(query) + assert res == { + "pullId": self.pull.pullid, + "compareWithBase": { + "impactedFiles": { + "results": [ + { + "segments": { + "results": [ + { + "header": "-1,2 +3,4", + "hasUnintendedChanges": False, + "lines": [ + { + "baseNumber": "1", + "headNumber": "1", + "baseCoverage": "H", + "headCoverage": "H", + "content": " line1", + "coverageInfo": { + "hitCount": 1, + "hitUploadIds": [0], + }, + }, + { + "baseNumber": None, + "headNumber": "2", + "baseCoverage": None, + "headCoverage": "H", + "content": "+ line2", + "coverageInfo": { + "hitCount": 1, + "hitUploadIds": [0], + }, + }, + ], + } + ] + } + }, + { + "segments": { + "results": [ + { + "header": "-1 +1", + "hasUnintendedChanges": True, + "lines": [ + { + "baseNumber": "1", + "headNumber": "1", + "baseCoverage": "M", + "headCoverage": "H", + "content": " line1", + "coverageInfo": { + "hitCount": 1, + "hitUploadIds": [0], + }, + } + ], + } + ] + } + }, + ] + }, + }, + } + + @patch("services.comparison.PullRequestComparison.get_file_comparison") + @patch( + "services.comparison.PullRequestComparison.files", + new_callable=PropertyMock, + ) + @patch( + "services.comparison.ComparisonReport.files", + new_callable=PropertyMock, + ) + def test_pull_comparison_coverage_changes( + self, comparison_files_mock, files_mock, get_file_comparison_mock + ): + TestFileComparison = namedtuple( + "TestFileComparison", + ["has_diff", "has_changes", "segments", "name", "head_name", "base_name"], + ) + + test_file_comparison = TestFileComparison( + has_diff=False, + has_changes=True, + name={"head": "test", "base": "test"}, + head_name="test", + base_name="test", + segments=[ + MockSegmentComparison( + header=(1, 1, 1, 1), + has_unintended_changes=True, + lines=[ + MockLineComparison( + number={ + "head": "1", + "base": "1", + }, + coverage={ + "base": LineType.miss, + "head": LineType.hit, + }, + value=" line1", + is_diff=True, + hit_session_ids=[0, 1], + ), + ], + ), + ], + ) + + get_file_comparison_mock.return_value = test_file_comparison + + comparison_files_mock.return_value = [test_file_comparison] + files_mock.return_value = [test_file_comparison] + + query = """ + pullId + compareWithBase { + ... on Comparison { + impactedFiles(filters: {}){ + ... on ImpactedFiles { + results { + segments { + ... on SegmentComparisons { + results { + header + hasUnintendedChanges + lines { + baseNumber + headNumber + baseCoverage + headCoverage + content + } + } + } + } + } + } + ... on UnknownFlags { + message + } + } + } + } + """ + + res = self._request(query) + assert res == { + "pullId": self.pull.pullid, + "compareWithBase": { + "impactedFiles": { + "results": [ + { + "segments": { + "results": [ + { + "header": "-1,1 +1,1", + "hasUnintendedChanges": True, + "lines": [ + { + "baseNumber": "1", + "headNumber": "1", + "baseCoverage": "M", + "headCoverage": "H", + "content": " line1", + } + ], + } + ] + } + } + ] + }, + }, + } + + def test_pull_comparison_pending(self): + self.commit_comparison.state = CommitComparison.CommitComparisonStates.PENDING + self.commit_comparison.save() + + # these are created by default in the factory + base_report = self.base_commit.reports.first() + base_report.reportleveltotals.delete() + head_report = self.head_commit.reports.first() + head_report.reportleveltotals.delete() + + query = """ + pullId + compareWithBase { + ... on Comparison { + state + baseTotals { + percentCovered + } + headTotals { + percentCovered + } + impactedFiles(filters: {}) { + ... on ImpactedFiles { + results { + baseName + headName + } + } + ... on UnknownFlags { + message + } + } + } + } + """ + + res = self._request(query) + assert res == { + "pullId": self.pull.pullid, + "compareWithBase": { + "state": "pending", + "baseTotals": None, + "headTotals": None, + "impactedFiles": {"results": []}, + }, + } + + @patch("services.task.TaskService.compute_comparisons") + def test_pull_comparison_no_comparison(self, compute_comparisons_mock): + self.commit_comparison.delete() + + query = """ + pullId + compareWithBase { + ... on Comparison { + state + } + } + """ + + res = self._request(query) + # it regenerates the comparison as needed + assert res["compareWithBase"] is not None + + compute_comparisons_mock.assert_called_once + + def test_pull_comparison_missing_when_commit_comparison_state_is_errored(self): + self.commit_comparison.state = CommitComparison.CommitComparisonStates.ERROR + self.commit_comparison.save() + + query = """ + pullId + compareWithBase { + __typename + } + """ + + res = self._request(query) + assert res == { + "pullId": self.pull.pullid, + "compareWithBase": {"__typename": "MissingComparison"}, + } + + def test_pull_comparison_missing_comparison(self): + self.head_commit.delete() + self.commit_comparison.delete() + + query = """ + pullId + compareWithBase { + __typename + } + """ + + res = self._request(query) + assert res == { + "pullId": self.pull.pullid, + "compareWithBase": {"__typename": "MissingComparison"}, + } + + def test_pull_comparison_missing_base_sha(self): + self.pull.compared_to = None + self.pull.save() + + query = """ + pullId + compareWithBase { + __typename + ... on ResolverError { + message + } + } + """ + + res = self._request(query) + assert res == { + "pullId": self.pull.pullid, + "compareWithBase": { + "__typename": "MissingBaseCommit", + "message": "Invalid base commit", + }, + } + + def test_pull_comparison_missing_head_sha(self): + self.pull.head = None + self.pull.save() + + query = """ + pullId + compareWithBase { + __typename + ... on ResolverError { + message + } + } + """ + + res = self._request(query) + assert res == { + "pullId": self.pull.pullid, + "compareWithBase": { + "__typename": "MissingHeadCommit", + "message": "Invalid head commit", + }, + } + + def test_pull_comparison_missing_base_report(self): + self.commit_comparison.error = "missing_base_report" + self.commit_comparison.save() + + query = """ + pullId + compareWithBase { + __typename + } + """ + + res = self._request(query) + assert res == { + "pullId": self.pull.pullid, + "compareWithBase": { + "__typename": "MissingBaseReport", + }, + } + + def test_pull_comparison_missing_head_report(self): + self.commit_comparison.error = "missing_head_report" + self.commit_comparison.save() + + query = """ + pullId + compareWithBase { + __typename + } + """ + + res = self._request(query) + assert res == { + "pullId": self.pull.pullid, + "compareWithBase": { + "__typename": "MissingHeadReport", + }, + } + + @patch("services.task.TaskService.compute_comparisons") + @patch("services.comparison.CommitComparisonService.needs_recompute") + def test_pull_comparison_needs_recalculation( + self, needs_recompute_mock, compute_comparisons_mock + ): + needs_recompute_mock.return_value = True + + query = """ + pullId + compareWithBase { + ... on Comparison { + state + } + } + """ + + res = self._request(query) + # recalculates comparison + assert res == { + "pullId": self.pull.pullid, + "compareWithBase": {"state": "pending"}, + } + compute_comparisons_mock.assert_called_once diff --git a/apps/codecov-api/graphql_api/tests/test_repository.py b/apps/codecov-api/graphql_api/tests/test_repository.py new file mode 100644 index 0000000000..5eb3d17afb --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_repository.py @@ -0,0 +1,741 @@ +import datetime +from unittest.mock import patch + +from django.test import TestCase, override_settings +from freezegun import freeze_time +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, + RepositoryTokenFactory, +) + +from graphql_api.types.repository.repository import TOKEN_UNAVAILABLE + +from .helper import GraphQLTestHelper + +query_repository = """ +query Repository($name: String!){ + me { + owner { + repository(name: $name) { + __typename + ... on Repository { + %s + } + ... on ResolverError { + message + } + } + } + } +} +""" + +query_repositories = """ +query Repositories($repoNames: [String!]!) { + me { + owner { + repositories(filters: { repoNames: $repoNames }) { + edges { + node { + %s + } + } + } + } + } +} +""" + +default_fields = """ + repoid + name + active + private + updatedAt + latestCommitAt + oldestCommitAt + uploadToken + defaultBranch + author { username } + graphToken + yaml + isATSConfigured + primaryLanguage + languages + bundleAnalysisEnabled + coverageEnabled + bot { username } + testAnalyticsEnabled +""" + + +def mock_get_config_global_upload_tokens(*args): + if args == ("setup", "hide_all_codecov_tokens"): + return True + + +class TestFetchRepository(GraphQLTestHelper, TestCase): + def fetch_repository(self, name, fields=None): + data = self.gql_request( + query_repository % (fields or default_fields), + owner=self.owner, + variables={"name": name}, + ) + return data["me"]["owner"]["repository"] + + def fetch_repositories(self, repo_names, fields=None): + data = self.gql_request( + query_repositories % (fields or default_fields), + owner=self.owner, + variables={"repoNames": repo_names}, + ) + return [edge["node"] for edge in data["me"]["owner"]["repositories"]["edges"]] + + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + self.yaml = {"test": "test"} + + @freeze_time("2021-01-01") + def test_when_repository_has_no_coverage(self): + repo = RepositoryFactory( + repoid=1, + author=self.owner, + active=True, + private=True, + name="a", + yaml=self.yaml, + language="rust", + languages=["python", "rust"], + test_analytics_enabled=True, + ) + graphToken = repo.image_token + assert self.fetch_repository( + repo.name, + default_fields + + "coverageAnalytics { percentCovered commitSha hits misses lines },", + ) == { + "__typename": "Repository", + "repoid": 1, + "name": "a", + "active": True, + "private": True, + "coverageAnalytics": { + "percentCovered": None, + "commitSha": None, + "hits": None, + "misses": None, + "lines": None, + }, + "latestCommitAt": None, + "oldestCommitAt": None, + "updatedAt": "2021-01-01T00:00:00+00:00", + "uploadToken": repo.upload_token, + "defaultBranch": "main", + "author": {"username": "codecov-user"}, + "graphToken": graphToken, + "yaml": "test: test\n", + "isATSConfigured": False, + "primaryLanguage": "rust", + "languages": ["python", "rust"], + "bundleAnalysisEnabled": False, + "coverageEnabled": False, + "bot": None, + "testAnalyticsEnabled": True, + } + + @freeze_time("2021-01-01") + def test_when_repository_has_coverage(self): + repo = RepositoryFactory( + repoid=1, + author=self.owner, + active=True, + private=True, + name="b", + yaml=self.yaml, + language="erlang", + languages=[], + ) + + hour_ago = datetime.datetime.now() - datetime.timedelta(hours=1) + coverage_commit = CommitFactory( + repository=repo, + totals={"c": 75, "h": 30, "m": 10, "n": 40}, + timestamp=hour_ago, + ) + CommitFactory(repository=repo, totals={"c": 85}) + + # trigger in the database is updating `updatestamp` after creating + # associated commits + repo.updatestamp = datetime.datetime.now() + repo.save() + + graphToken = repo.image_token + assert self.fetch_repository( + repo.name, + default_fields + + "coverageAnalytics { percentCovered commitSha hits misses lines },", + ) == { + "__typename": "Repository", + "repoid": 1, + "name": "b", + "active": True, + "latestCommitAt": None, + "oldestCommitAt": "2020-12-31T23:00:00", # hour ago + "private": True, + "coverageAnalytics": { + "percentCovered": 75, + "commitSha": coverage_commit.commitid, + "hits": 30, + "misses": 10, + "lines": 40, + }, + "updatedAt": "2021-01-01T00:00:00+00:00", + "uploadToken": repo.upload_token, + "defaultBranch": "main", + "author": {"username": "codecov-user"}, + "graphToken": graphToken, + "yaml": "test: test\n", + "isATSConfigured": False, + "primaryLanguage": "erlang", + "languages": [], + "bundleAnalysisEnabled": False, + "coverageEnabled": False, + "bot": None, + "testAnalyticsEnabled": False, + } + + @freeze_time("2021-01-01") + def test_repositories_oldest_commit_at(self): + repo = RepositoryFactory(author=self.owner) + + CommitFactory(repository=repo, totals={"c": 75}) + CommitFactory(repository=repo, totals={"c": 85}) + + # oldestCommitAt not loaded for multiple repos + assert self.fetch_repositories([repo.name], fields="oldestCommitAt") == [ + { + "oldestCommitAt": None, + } + ] + + def test_repository_pulls(self): + repo = RepositoryFactory(author=self.owner, active=True, private=True, name="a") + PullFactory(repository=repo, pullid=2) + PullFactory(repository=repo, pullid=3) + + res = self.fetch_repository(repo.name, "pulls { edges { node { pullId } } }") + assert res["pulls"]["edges"][0]["node"]["pullId"] == 3 + assert res["pulls"]["edges"][1]["node"]["pullId"] == 2 + + def test_repository_get_static_analysis_token(self): + user = OwnerFactory() + repo = RepositoryFactory(author=user, name="gazebo", active=True) + RepositoryTokenFactory( + repository=repo, key="random", token_type="static_analysis" + ) + + data = self.gql_request( + query_repository % "staticAnalysisToken", + owner=user, + variables={"name": repo.name}, + ) + assert data["me"]["owner"]["repository"]["staticAnalysisToken"] == "random" + + def test_repository_get_graph_token(self): + user = OwnerFactory() + repo = RepositoryFactory(author=user) + + data = self.gql_request( + query_repository % "graphToken", + owner=user, + variables={"name": repo.name}, + ) + assert data["me"]["owner"]["repository"]["graphToken"] == repo.image_token + + def test_repository_resolve_yaml(self): + user = OwnerFactory() + repo = RepositoryFactory(author=user, name="has_yaml", yaml=self.yaml) + data = self.gql_request( + query_repository % "yaml", + owner=user, + variables={"name": repo.name}, + ) + assert data["me"]["owner"]["repository"]["yaml"] == "test: test\n" + + def test_repository_resolve_yaml_no_yaml(self): + user = OwnerFactory() + repo = RepositoryFactory(author=user, name="no_yaml") + data = self.gql_request( + query_repository % "yaml", + owner=user, + variables={"name": repo.name}, + ) + assert data["me"]["owner"]["repository"]["yaml"] is None + + def test_repository_resolve_bot(self): + user = OwnerFactory() + bot = OwnerFactory(username="random_bot") + repo = RepositoryFactory(author=user, bot=bot) + data = self.gql_request( + query_repository % "bot {username}", + owner=user, + variables={"name": repo.name}, + ) + assert data["me"]["owner"]["repository"]["bot"]["username"] == "random_bot" + + def test_repository_resolve_activated_true(self): + user = OwnerFactory() + repo = RepositoryFactory(author=user, activated=True) + data = self.gql_request( + query_repository % "activated", + owner=user, + variables={"name": repo.name}, + ) + assert data["me"]["owner"]["repository"]["activated"] == True + + @override_settings(TIMESERIES_ENABLED=False) + def test_repository_resolve_activated_false(self): + user = OwnerFactory() + repo = RepositoryFactory(author=user, activated=False) + data = self.gql_request( + query_repository % "activated", + owner=user, + variables={"name": repo.name}, + ) + assert data["me"]["owner"]["repository"]["activated"] == False + + @patch("shared.yaml.user_yaml.UserYaml.get_final_yaml") + def test_repository_repository_config_indication_range(self, mocked_useryaml): + mocked_useryaml.return_value = {"coverage": {"range": [60, 80]}} + + repo = RepositoryFactory( + author=self.owner, + active=True, + private=True, + ) + + data = self.gql_request( + query_repository + % "repositoryConfig { indicationRange { upperRange lowerRange } }", + owner=self.owner, + variables={"name": repo.name}, + ) + + assert ( + data["me"]["owner"]["repository"]["repositoryConfig"]["indicationRange"][ + "lowerRange" + ] + == 60 + ) + assert ( + data["me"]["owner"]["repository"]["repositoryConfig"]["indicationRange"][ + "upperRange" + ] + == 80 + ) + + @patch("shared.yaml.user_yaml.UserYaml.get_final_yaml") + def test_repository_repository_config_indication_range_float(self, mocked_useryaml): + mocked_useryaml.return_value = {"coverage": {"range": [61.1, 82.2]}} + + repo = RepositoryFactory( + author=self.owner, + active=True, + private=True, + ) + + data = self.gql_request( + query_repository + % "repositoryConfig { indicationRange { upperRange lowerRange } }", + owner=self.owner, + variables={"name": repo.name}, + ) + + assert ( + data["me"]["owner"]["repository"]["repositoryConfig"]["indicationRange"][ + "lowerRange" + ] + == 61.1 + ) + assert ( + data["me"]["owner"]["repository"]["repositoryConfig"]["indicationRange"][ + "upperRange" + ] + == 82.2 + ) + + @patch("services.activation.try_auto_activate") + def test_repository_auto_activate(self, try_auto_activate): + repo = RepositoryFactory( + author=self.owner, + active=True, + private=True, + coverage_enabled=True, + bundle_analysis_enabled=True, + ) + + self.gql_request( + query_repository % "name", + owner=self.owner, + variables={"name": repo.name}, + ) + + try_auto_activate.assert_called_once_with( + self.owner, + self.owner, + ) + + @patch("services.activation.is_activated") + @patch("services.activation.try_auto_activate") + def test_resolve_inactive_user_on_unconfigured_repo( + self, try_auto_activate, is_activated + ): + repo = RepositoryFactory( + author=self.owner, + active=False, + activated=False, + private=True, + name="test-one", + coverage_enabled=True, + bundle_analysis_enabled=False, + ) + + is_activated.return_value = False + + data = self.gql_request( + query_repository % "name", + owner=self.owner, + variables={"name": repo.name}, + ) + + assert data["me"]["owner"]["repository"]["name"] == "test-one" + + def test_repository_not_found(self): + data = self.gql_request( + query_repository % "name", + owner=self.owner, + variables={"name": "nonexistent-repo-name"}, + ) + assert data["me"]["owner"]["repository"] == { + "__typename": "NotFoundError", + "message": "Not found", + } + + def test_repository_has_ats_configured(self): + repo = RepositoryFactory( + author=self.owner, + active=True, + private=True, + yaml={ + "flag_management": {"individual_flags": {"carryforward_mode": "labels"}} + }, + ) + + res = self.fetch_repository(repo.name) + assert res["isATSConfigured"] == True + + def test_repository_get_language(self): + repo = RepositoryFactory( + author=self.owner, active=True, private=True, language="python" + ) + + res = self.fetch_repository(repo.name) + assert res["primaryLanguage"] == "python" + + def test_repository_get_bundle_analysis_enabled(self): + repo = RepositoryFactory( + author=self.owner, active=True, private=True, bundle_analysis_enabled=True + ) + res = self.fetch_repository(repo.name) + assert res["bundleAnalysisEnabled"] == True + + def test_repository_get_coverage_enabled(self): + repo = RepositoryFactory( + author=self.owner, active=True, private=True, coverage_enabled=True + ) + res = self.fetch_repository(repo.name) + assert res["coverageEnabled"] == True + + def test_repository_get_test_analytics_enabled(self) -> None: + repo = RepositoryFactory( + author=self.owner, active=True, private=True, test_analytics_enabled=True + ) + res = self.fetch_repository(repo.name) + assert res["testAnalyticsEnabled"] == True + + def test_repository_get_test_analytics_disabled(self) -> None: + repo = RepositoryFactory( + author=self.owner, active=True, private=True, test_analytics_enabled=False + ) + res = self.fetch_repository(repo.name) + assert res["testAnalyticsEnabled"] == False + + def test_repository_get_languages_null(self): + repo = RepositoryFactory( + author=self.owner, active=True, private=True, languages=None + ) + res = self.fetch_repository(repo.name) + assert res["languages"] is None + + def test_repository_get_languages_empty(self): + repo = RepositoryFactory(author=self.owner, active=True, private=True) + res = self.fetch_repository(repo.name) + assert res["languages"] == [] + + def test_repository_get_languages_with_values(self): + repo = RepositoryFactory( + author=self.owner, active=True, private=True, languages=["C", "C++"] + ) + res = self.fetch_repository(repo.name) + assert res["languages"] == ["C", "C++"] + + def test_repository_is_first_pull_request(self) -> None: + repo = RepositoryFactory( + author=self.owner, + active=True, + private=True, + yaml={"component_management": {}}, + ) + + PullFactory(repository=repo, pullid=1, compared_to=None) + + data = self.gql_request( + query_repository + % """ + isFirstPullRequest + """, + owner=self.owner, + variables={"name": repo.name}, + ) + + assert data["me"]["owner"]["repository"]["isFirstPullRequest"] == True + + def test_repository_is_first_pull_request_compared_to_not_none(self) -> None: + repo = RepositoryFactory( + author=self.owner, + active=True, + private=True, + yaml={"component_management": {}}, + ) + + PullFactory(repository=repo, pullid=1, compared_to=1) + + data = self.gql_request( + query_repository + % """ + isFirstPullRequest + """, + owner=self.owner, + variables={"name": repo.name}, + ) + + assert data["me"]["owner"]["repository"]["isFirstPullRequest"] == False + + def test_repository_when_is_first_pull_request_false(self) -> None: + repo = RepositoryFactory( + author=self.owner, + active=True, + private=True, + yaml={"component_management": {}}, + ) + + PullFactory(repository=repo, pullid=1) + PullFactory(repository=repo, pullid=2) + + data = self.gql_request( + query_repository + % """ + isFirstPullRequest + """, + owner=self.owner, + variables={"name": repo.name}, + ) + + assert data["me"]["owner"]["repository"]["isFirstPullRequest"] == False + + @patch("shared.rate_limits.determine_entity_redis_key") + @patch("shared.rate_limits.determine_if_entity_is_rate_limited") + @override_settings(IS_ENTERPRISE=True, GUEST_ACCESS=False) + def test_fetch_is_github_rate_limited( + self, mock_determine_rate_limit, mock_determine_redis_key + ): + repo = RepositoryFactory( + author=self.owner, + active=True, + private=True, + yaml={"component_management": {}}, + ) + + mock_determine_redis_key.return_value = "test" + mock_determine_rate_limit.return_value = True + + data = self.gql_request( + query_repository + % """ + isGithubRateLimited + """, + owner=self.owner, + variables={"name": repo.name}, + ) + + assert data["me"]["owner"]["repository"]["isGithubRateLimited"] == True + + def test_fetch_is_github_rate_limited_not_on_gh_service(self): + owner = OwnerFactory(service="gitlab") + repo = RepositoryFactory( + author=owner, + author__service="gitlab", + service_id=12345, + active=True, + ) + + data = self.gql_request( + query_repository + % """ + isGithubRateLimited + """, + owner=owner, + variables={"name": repo.name}, + provider="gitlab", + ) + + assert data["me"]["owner"]["repository"]["isGithubRateLimited"] == False + + @override_settings(HIDE_ALL_CODECOV_TOKENS=True) + def test_repo_upload_token_not_available_config_setting_owner_not_admin(self): + owner = OwnerFactory(service="gitlab") + + repo = RepositoryFactory( + author=owner, + author__service="gitlab", + service_id=12345, + active=True, + ) + new_owner = OwnerFactory(service="gitlab", organizations=[owner.ownerid]) + new_owner.permission = [repo.repoid] + new_owner.save() + owner.admins = [] + + query = """ + query { + owner(username: "%s") { + repository(name: "%s") { + ... on Repository { + uploadToken + } + } + } + } + """ % ( + owner.username, + repo.name, + ) + + data = self.gql_request( + query, + owner=new_owner, + variables={"name": repo.name}, + provider="gitlab", + ) + + assert data["owner"]["repository"]["uploadToken"] == TOKEN_UNAVAILABLE + + @override_settings(HIDE_ALL_CODECOV_TOKENS=True) + def test_repo_upload_token_not_available_config_setting_owner_is_anonymous(self): + owner = OwnerFactory(service="gitlab") + + repo = RepositoryFactory( + author=owner, + author__service="gitlab", + service_id=12345, + active=True, + private=False, + ) + + query = """ + query { + owner(username: "%s") { + repository(name: "%s") { + ... on Repository { + uploadToken + } + } + } + } + """ % ( + owner.username, + repo.name, + ) + + data = self.gql_request( + query, + variables={"name": repo.name}, + provider="gitlab", + ) + + assert data["owner"]["repository"]["uploadToken"] == TOKEN_UNAVAILABLE + + @override_settings(HIDE_ALL_CODECOV_TOKENS=True) + def test_repo_upload_token_not_available_config_setting_owner_is_admin(self): + owner = OwnerFactory(service="gitlab") + repo = RepositoryFactory( + author=owner, + author__service="gitlab", + service_id=12345, + active=True, + ) + owner.admins = [owner.ownerid] + + data = self.gql_request( + query_repository + % """ + uploadToken + """, + owner=owner, + variables={"name": repo.name}, + provider="gitlab", + ) + + assert data["me"]["owner"]["repository"]["uploadToken"] != TOKEN_UNAVAILABLE + + @patch("shared.rate_limits.determine_entity_redis_key") + @patch("shared.rate_limits.determine_if_entity_is_rate_limited") + @patch("logging.Logger.warning") + @override_settings(IS_ENTERPRISE=True, GUEST_ACCESS=False) + def test_fetch_is_github_rate_limited_but_errors( + self, + mock_log_warning, + mock_determine_rate_limit, + mock_determine_redis_key, + ): + repo = RepositoryFactory( + author=self.owner, + active=True, + private=True, + yaml={"component_management": {}}, + ) + + mock_determine_redis_key.side_effect = Exception("some random error lol") + mock_determine_rate_limit.return_value = True + + data = self.gql_request( + query_repository + % """ + isGithubRateLimited + """, + owner=self.owner, + variables={"name": repo.name}, + ) + + assert data["me"]["owner"]["repository"]["isGithubRateLimited"] is None + + mock_log_warning.assert_any_call( + "Error when checking rate limit", + extra={ + "repo_id": repo.repoid, + "has_owner": True, + }, + ) diff --git a/apps/codecov-api/graphql_api/tests/test_requested_fields.py b/apps/codecov-api/graphql_api/tests/test_requested_fields.py new file mode 100644 index 0000000000..c7ab1b1a54 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_requested_fields.py @@ -0,0 +1,208 @@ +from graphql import GraphQLResolveInfo +from graphql.language import ( + FragmentDefinitionNode, + OperationDefinitionNode, + parse, +) + +from graphql_api.helpers.requested_fields import selected_fields + + +def parse_into_resolveinfo(source: str) -> GraphQLResolveInfo: + document = parse(source) + + operation: OperationDefinitionNode | None = None + fragments: dict[str, FragmentDefinitionNode] = {} + + for definition in document.definitions: + if isinstance(definition, OperationDefinitionNode): + operation = definition + elif isinstance(definition, FragmentDefinitionNode): + fragments[definition.name.value] = definition + + assert operation + root_fields = [operation] + + return GraphQLResolveInfo( + "__root__", + root_fields, # list[FieldNode] + None, + None, + None, + None, + fragments, # dict[str, FragmentDefinitionNode] + None, + None, + None, + None, + None, + ) + + +QUERY_CoverageForFile = """ +query CoverageForFile( + $owner: String! + $repo: String! + $ref: String! + $path: String! + $flags: [String] + $components: [String] +) { + owner(username: $owner) { + repository(name: $repo) { + __typename + ... on Repository { + commit(id: $ref) { + ...CoverageForFile + } + branch(name: $ref) { + name + head { + ...CoverageForFile + } + } + } + ... on NotFoundError { + message + } + ... on OwnerNotActivatedError { + message + } + } + } +} + +fragment CoverageForFile on Commit { + commitid + coverageAnalytics { + flagNames + components { + id + name + } + coverageFile(path: $path, flags: $flags, components: $components) { + hashedPath + content + coverage { + line + coverage + } + totals { + percentCovered # Absolute coverage of the commit + } + } + } +} +""" + +QUERY_GetRepoConfigurationStatus = """ +query GetRepoConfigurationStatus($owner: String!, $repo: String!) { + owner(username: $owner) { + plan { + isTeamPlan + } + repository(name:$repo) { + __typename + ... on Repository { + coverageEnabled + bundleAnalysisEnabled + testAnalyticsEnabled + yaml + languages + coverageAnalytics { + flagsCount + componentsCount + } + } + ... on NotFoundError { + message + } + ... on OwnerNotActivatedError { + message + } + } + } +} +""" + +QUERY_ReposForOwner = """ +query ReposForOwner( + $filters: RepositorySetFilters! + $owner: String! + $ordering: RepositoryOrdering! + $direction: OrderingDirection! + $after: String + $first: Int +) { + owner(username: $owner) { + repositories( + filters: $filters + ordering: $ordering + orderingDirection: $direction + first: $first + after: $after + ) { + edges { + node { + name + active + activated + private + coverageAnalytics { + percentCovered + lines + } + updatedAt + latestCommitAt + author { + username + } + coverageEnabled + bundleAnalysisEnabled + repositoryConfig { + indicationRange { + upperRange + lowerRange + } + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } +} +""" + + +def test_requested_fields(): + info = parse_into_resolveinfo(QUERY_CoverageForFile) + fields = selected_fields(info) + + assert "owner.repository.branch.name" in fields + assert "owner.repository.branch.head.commitid" in fields + assert ( + "owner.repository.branch.head.coverageAnalytics.coverageFile.totals.percentCovered" + in fields + ) + + assert "owner.repository.oldestCommitAt" not in fields + assert "owner.repository.coverageAnalytics.percentCovered" not in fields + assert "owner.repository.coverageAnalytics.commitSha" not in fields + + info = parse_into_resolveinfo(QUERY_GetRepoConfigurationStatus) + fields = selected_fields(info) + + assert "owner.repository.coverageAnalytics" in fields + assert "owner.repository.oldestCommitAt" not in fields + assert "owner.repository.coverageAnalytics.percentCovered" not in fields + + info = parse_into_resolveinfo(QUERY_ReposForOwner) + fields = selected_fields(info) + + assert "owner.repositories.edges.node.latestCommitAt" in fields + assert "owner.repositories.edges.node.oldestCommitAt" not in fields + assert "owner.repositories.edges.node.coverageAnalytics" in fields + assert "owner.repositories.edges.node.coverageAnalytics.percentCovered" in fields diff --git a/apps/codecov-api/graphql_api/tests/test_session.py b/apps/codecov-api/graphql_api/tests/test_session.py new file mode 100644 index 0000000000..c0d3fc15f4 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_session.py @@ -0,0 +1,59 @@ +from django.test import TestCase +from freezegun import freeze_time +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory, SessionFactory + +from .helper import GraphQLTestHelper, paginate_connection + +query = """ +query MySession { + me { + sessions { + edges { + node { + name + ip + lastseen + useragent + type + lastFour + } + } + } + } +} +""" + + +class SessionTestCase(GraphQLTestHelper, TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + self.session = SessionFactory( + owner=self.owner, + type="login", + name="test-123", + lastseen="2021-01-01T00:00:00+00:00", + ) + + @freeze_time("2021-01-01") + def test_fetching_session(self): + data = self.gql_request(query, owner=self.owner) + sessions = paginate_connection(data["me"]["sessions"]) + current_session = self.owner.session_set.filter(type="login").first() + assert sessions == [ + { + "name": current_session.name, + "ip": current_session.ip, + "lastseen": "2021-01-01T00:00:00+00:00", + "useragent": current_session.useragent, + "type": current_session.type, + "lastFour": str(current_session.token)[-4:], + } + ] + + def test_fetching_session_doesnt_include_other_people_session(self): + random_user = OwnerFactory() + for _ in range(5): + SessionFactory(owner=random_user) + data = self.gql_request(query, owner=self.owner) + sessions = paginate_connection(data["me"]["sessions"]) + assert len(sessions) == 1 diff --git a/apps/codecov-api/graphql_api/tests/test_test_analytics.py b/apps/codecov-api/graphql_api/tests/test_test_analytics.py new file mode 100644 index 0000000000..c9b835e483 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_test_analytics.py @@ -0,0 +1,723 @@ +import datetime +from base64 import b64encode +from typing import Any + +import polars as pl +import pytest +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory +from shared.django_apps.core.tests.factories import RepositoryFactory +from shared.helpers.redis import get_redis_connection +from shared.storage.exceptions import BucketAlreadyExistsError +from shared.storage.memory import MemoryStorageService + +from graphql_api.types.enums import ( + OrderingDirection, + TestResultsOrderingParameter, +) +from graphql_api.types.enums.enum_types import MeasurementInterval +from graphql_api.types.test_analytics.test_analytics import ( + TestResultsRow, + encode_cursor, + generate_test_results, + get_results, +) +from utils.test_results import dedup_table + +from .helper import GraphQLTestHelper + + +class RowFactory: + idx = 0 + + def __call__(self, updated_at: datetime.datetime) -> dict[str, Any]: + RowFactory.idx += 1 + return { + "name": f"test{RowFactory.idx}", + "testsuite": f"testsuite{RowFactory.idx}", + "flags": [f"flag{RowFactory.idx}"], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": updated_at, + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1 if RowFactory.idx == 1 else 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0, + } + + +@pytest.fixture +def mock_storage(mocker): + m = mocker.patch("utils.test_results.get_appropriate_storage_service") + storage_server = MemoryStorageService({}) + m.return_value = storage_server + yield storage_server + + +base_gql_query = """ + query { + owner(username: "%s") { + repository(name: "%s") { + ... on Repository { + testAnalytics { + %s + } + } + } + } + } +""" + + +rows = [RowFactory()(datetime.datetime(2024, 1, 1 + i)) for i in range(5)] + + +rows_with_duplicate_names = [ + RowFactory()(datetime.datetime(2024, 1, 1 + i)) for i in range(5) +] +for i in range(0, len(rows_with_duplicate_names) - 1, 2): + rows_with_duplicate_names[i]["name"] = rows_with_duplicate_names[i + 1]["name"] + + +def dedup(rows: list[dict]) -> list[dict]: + by_name = {} + for row in rows: + if row["name"] not in by_name: + by_name[row["name"]] = [] + by_name[row["name"]].append(row) + + result = [] + for name, group in by_name.items(): + if len(group) == 1: + result.append(group[0]) + continue + + weights = [r["total_pass_count"] + r["total_fail_count"] for r in group] + total_weight = sum(weights) + + merged = { + "name": name, + "testsuite": sorted({r["testsuite"] for r in group}), + "flags": sorted({flag for r in group for flag in r["flags"]}), + "failure_rate": sum(r["failure_rate"] * w for r, w in zip(group, weights)) + / total_weight, + "flake_rate": sum(r["flake_rate"] * w for r, w in zip(group, weights)) + / total_weight, + "updated_at": max(r["updated_at"] for r in group), + "avg_duration": sum(r["avg_duration"] * w for r, w in zip(group, weights)) + / total_weight, + "total_fail_count": sum(r["total_fail_count"] for r in group), + "total_flaky_fail_count": sum(r["total_flaky_fail_count"] for r in group), + "total_pass_count": sum(r["total_pass_count"] for r in group), + "total_skip_count": sum(r["total_skip_count"] for r in group), + "commits_where_fail": sum(r["commits_where_fail"] for r in group), + "last_duration": max(r["last_duration"] for r in group), + } + result.append(merged) + + return sorted(result, key=lambda x: x["updated_at"], reverse=True) + + +def row_to_camel_case(row: dict) -> dict: + return { + "commitsFailed" + if key == "commits_where_fail" + else "".join( + part.capitalize() if i > 0 else part.lower() + for i, part in enumerate(key.split("_")) + ): value.isoformat() if key == "updated_at" else value + for key, value in row.items() + if key not in ("testsuite", "flags") + } + + +test_results_table = pl.DataFrame(rows) +test_results_table_with_duplicate_names = pl.DataFrame(rows_with_duplicate_names) + + +def base64_encode_string(x: str) -> str: + return b64encode(x.encode()).decode("utf-8") + + +def cursor(row: dict) -> str: + return encode_cursor(TestResultsRow(**row), TestResultsOrderingParameter.UPDATED_AT) + + +@pytest.fixture(autouse=True) +def repository(mocker, transactional_db): + owner = OwnerFactory(username="codecov-user") + repo = RepositoryFactory(author=owner, name="testRepoName", active=True) + + return repo + + +@pytest.fixture +def store_in_redis(repository): + redis = get_redis_connection() + redis.set( + f"test_results:{repository.repoid}:{repository.branch}:30", + test_results_table.write_ipc(None).getvalue(), + ) + + yield + + redis.delete( + f"test_results:{repository.repoid}:{repository.branch}:30", + ) + + +@pytest.fixture +def store_in_storage(repository, mock_storage): + from django.conf import settings + + try: + mock_storage.create_root_storage(settings.GCS_BUCKET_NAME) + except BucketAlreadyExistsError: + pass + + mock_storage.write_file( + settings.GCS_BUCKET_NAME, + f"test_results/rollups/{repository.repoid}/{repository.branch}/30", + test_results_table.write_ipc(None).getvalue(), + ) + + yield + + mock_storage.delete_file( + settings.GCS_BUCKET_NAME, + f"test_results/rollups/{repository.repoid}/{repository.branch}/30", + ) + + +@pytest.fixture +def store_in_redis_with_duplicate_names(repository): + redis = get_redis_connection() + redis.set( + f"test_results:{repository.repoid}:{repository.branch}:30", + test_results_table_with_duplicate_names.write_ipc(None).getvalue(), + ) + + yield + + redis.delete( + f"test_results:{repository.repoid}:{repository.branch}:30", + ) + + +class TestAnalyticsTestCase( + GraphQLTestHelper, +): + def test_get_test_results( + self, + transactional_db, + repository, + store_in_redis, + store_in_storage, + mock_storage, + ): + results = get_results(repository.repoid, repository.branch, 30) + assert results is not None + + assert results.equals(dedup_table(test_results_table)) + + def test_get_test_results_no_storage( + self, transactional_db, repository, mock_storage + ): + assert get_results(repository.repoid, repository.branch, 30) is None + + def test_get_test_results_no_redis( + self, mocker, transactional_db, repository, store_in_storage, mock_storage + ): + m = mocker.patch("services.task.TaskService.cache_test_results_redis") + results = get_results(repository.repoid, repository.branch, 30) + assert results is not None + assert results.equals(dedup_table(test_results_table)) + + m.assert_called_once_with(repository.repoid, repository.branch) + + def test_test_results( + self, transactional_db, repository, store_in_redis, mock_storage, snapshot + ): + test_results = generate_test_results( + repoid=repository.repoid, + ordering=TestResultsOrderingParameter.UPDATED_AT, + ordering_direction=OrderingDirection.DESC, + measurement_interval=MeasurementInterval.INTERVAL_30_DAY, + ) + assert test_results is not None + assert test_results.total_count == 5 + assert test_results.page_info == { + "has_next_page": False, + "has_previous_page": False, + "start_cursor": cursor(rows[4]), + "end_cursor": cursor(rows[0]), + } + assert snapshot("json") == [ + row["node"].to_dict() + for row in test_results.edges + if isinstance(row["node"], TestResultsRow) + ] + + def test_test_results_asc( + self, transactional_db, repository, store_in_redis, mock_storage, snapshot + ): + test_results = generate_test_results( + repoid=repository.repoid, + ordering=TestResultsOrderingParameter.UPDATED_AT, + ordering_direction=OrderingDirection.ASC, + measurement_interval=MeasurementInterval.INTERVAL_30_DAY, + ) + assert test_results is not None + assert test_results.total_count == 5 + assert test_results.page_info == { + "has_next_page": False, + "has_previous_page": False, + "start_cursor": cursor(rows[0]), + "end_cursor": cursor(rows[4]), + } + assert snapshot("json") == [ + row["node"].to_dict() + for row in test_results.edges + if isinstance(row["node"], TestResultsRow) + ] + + @pytest.mark.parametrize( + "first, after, last, before, has_next_page, has_previous_page, start_cursor, end_cursor, expected_rows", + [ + pytest.param( + 1, + None, + None, + None, + True, + False, + cursor(rows[4]), + cursor(rows[4]), + [rows[4]], + id="first_1", + ), + pytest.param( + 1, + cursor(rows[4]), + None, + None, + True, + False, + cursor(rows[3]), + cursor(rows[3]), + [rows[3]], + id="first_1_after", + ), + pytest.param( + 1, + cursor(rows[1]), + None, + None, + False, + False, + cursor(rows[0]), + cursor(rows[0]), + [rows[0]], + id="first_1_after_no_next", + ), + pytest.param( + None, + None, + 1, + None, + False, + True, + cursor(rows[0]), + cursor(rows[0]), + [rows[0]], + id="last_1", + ), + pytest.param( + None, + None, + 1, + cursor(rows[0]), + False, + True, + cursor(rows[1]), + cursor(rows[1]), + [rows[1]], + id="last_1_before", + ), + pytest.param( + None, + None, + 1, + cursor(rows[3]), + False, + False, + cursor(rows[4]), + cursor(rows[4]), + [rows[4]], + id="last_1_before_no_previous", + ), + ], + ) + def test_test_results_pagination( + self, + first, + after, + before, + last, + has_next_page, + has_previous_page, + expected_rows, + start_cursor, + end_cursor, + repository, + store_in_redis, + mock_storage, + snapshot, + ): + test_results = generate_test_results( + repoid=repository.repoid, + ordering=TestResultsOrderingParameter.UPDATED_AT, + ordering_direction=OrderingDirection.DESC, + measurement_interval=MeasurementInterval.INTERVAL_30_DAY, + first=first, + after=after, + before=before, + last=last, + ) + assert test_results.total_count == 5 + assert test_results.page_info == { + "has_next_page": has_next_page, + "has_previous_page": has_previous_page, + "start_cursor": start_cursor, + "end_cursor": end_cursor, + } + assert snapshot("json") == [ + row["node"].to_dict() + for row in test_results.edges + if isinstance(row["node"], TestResultsRow) + ] + + @pytest.mark.parametrize( + "first, after, last, before, has_next_page, has_previous_page, start_cursor, end_cursor, expected_rows", + [ + pytest.param( + 1, + None, + None, + None, + True, + False, + cursor(rows[0]), + cursor(rows[0]), + [rows[0]], + id="first_1", + ), + pytest.param( + 1, + cursor(rows[0]), + None, + None, + True, + False, + cursor(rows[1]), + cursor(rows[1]), + [rows[1]], + id="first_1_after", + ), + pytest.param( + 1, + cursor(rows[3]), + None, + None, + False, + False, + cursor(rows[4]), + cursor(rows[4]), + [rows[4]], + id="first_1_after_no_next", + ), + pytest.param( + None, + None, + 1, + None, + False, + True, + cursor(rows[4]), + cursor(rows[4]), + [rows[4]], + id="last_1", + ), + pytest.param( + None, + None, + 1, + cursor(rows[4]), + False, + True, + cursor(rows[3]), + cursor(rows[3]), + [rows[3]], + id="last_1_before", + ), + pytest.param( + None, + None, + 1, + cursor(rows[1]), + False, + False, + cursor(rows[0]), + cursor(rows[0]), + [rows[0]], + id="last_1_before_no_previous", + ), + ], + ) + def test_test_results_pagination_asc( + self, + first, + after, + before, + last, + has_next_page, + has_previous_page, + expected_rows, + start_cursor, + end_cursor, + repository, + store_in_redis, + mock_storage, + snapshot, + ): + test_results = generate_test_results( + repoid=repository.repoid, + ordering=TestResultsOrderingParameter.UPDATED_AT, + ordering_direction=OrderingDirection.ASC, + measurement_interval=MeasurementInterval.INTERVAL_30_DAY, + first=first, + after=after, + before=before, + last=last, + ) + assert test_results.total_count == 5 + assert test_results.page_info == { + "has_next_page": has_next_page, + "has_previous_page": has_previous_page, + "start_cursor": start_cursor, + "end_cursor": end_cursor, + } + assert snapshot("json") == [ + row["node"].to_dict() + for row in test_results.edges + if isinstance(row["node"], TestResultsRow) + ] + + def test_test_analytics_term_filter( + self, repository, store_in_redis, mock_storage, snapshot + ): + test_results = generate_test_results( + repoid=repository.repoid, + term=rows[0]["name"][2:], + ordering=TestResultsOrderingParameter.UPDATED_AT, + ordering_direction=OrderingDirection.DESC, + measurement_interval=MeasurementInterval.INTERVAL_30_DAY, + ) + assert test_results is not None + assert test_results.total_count == 1 + assert test_results.page_info == { + "has_next_page": False, + "has_previous_page": False, + "start_cursor": cursor(rows[0]), + "end_cursor": cursor(rows[0]), + } + assert snapshot("json") == [ + row["node"].to_dict() + for row in test_results.edges + if isinstance(row["node"], TestResultsRow) + ] + + def test_test_analytics_testsuite_filter( + self, repository, store_in_redis, snapshot + ): + test_results = generate_test_results( + repoid=repository.repoid, + testsuites=[rows[0]["testsuite"]], + ordering=TestResultsOrderingParameter.UPDATED_AT, + ordering_direction=OrderingDirection.DESC, + measurement_interval=MeasurementInterval.INTERVAL_30_DAY, + ) + assert test_results is not None + assert test_results.total_count == 1 + assert test_results.page_info == { + "has_next_page": False, + "has_previous_page": False, + "start_cursor": cursor(rows[0]), + "end_cursor": cursor(rows[0]), + } + assert snapshot("json") == [ + row["node"].to_dict() + for row in test_results.edges + if isinstance(row["node"], TestResultsRow) + ] + + def test_test_analytics_flag_filter( + self, repository, store_in_redis, mock_storage, snapshot + ): + test_results = generate_test_results( + repoid=repository.repoid, + flags=[rows[0]["flags"][0]], + ordering=TestResultsOrderingParameter.UPDATED_AT, + ordering_direction=OrderingDirection.DESC, + measurement_interval=MeasurementInterval.INTERVAL_30_DAY, + ) + assert test_results is not None + # rows = dedup(rows) + assert test_results.total_count == 1 + assert test_results.page_info == { + "has_next_page": False, + "has_previous_page": False, + "start_cursor": cursor(rows[0]), + "end_cursor": cursor(rows[0]), + } + assert snapshot("json") == [ + row["node"].to_dict() + for row in test_results.edges + if isinstance(row["node"], TestResultsRow) + ] + + def test_gql_query(self, repository, store_in_redis, mock_storage): + query = base_gql_query % ( + repository.author.username, + repository.name, + """ + testResults(ordering: { parameter: UPDATED_AT, direction: DESC } ) { + totalCount + edges { + cursor + node { + name + failureRate + flakeRate + updatedAt + avgDuration + totalFailCount + totalFlakyFailCount + totalPassCount + totalSkipCount + commitsFailed + lastDuration + } + } + } + """, + ) + + result = self.gql_request(query, owner=repository.author) + + assert ( + result["owner"]["repository"]["testAnalytics"]["testResults"]["totalCount"] + == 5 + ) + assert result["owner"]["repository"]["testAnalytics"]["testResults"][ + "edges" + ] == [ + { + "cursor": cursor(row), + "node": row_to_camel_case(row), + } + for row in dedup(rows) + ] + + def test_gql_query_with_duplicate_names( + self, repository, store_in_redis_with_duplicate_names, mock_storage + ): + query = base_gql_query % ( + repository.author.username, + repository.name, + """ + testResults(ordering: { parameter: UPDATED_AT, direction: DESC } ) { + totalCount + edges { + cursor + node { + name + failureRate + flakeRate + updatedAt + avgDuration + totalFailCount + totalFlakyFailCount + totalPassCount + totalSkipCount + commitsFailed + lastDuration + } + } + } + """, + ) + + result = self.gql_request(query, owner=repository.author) + + assert ( + result["owner"]["repository"]["testAnalytics"]["testResults"]["totalCount"] + == 3 + ) + assert result["owner"]["repository"]["testAnalytics"]["testResults"][ + "edges" + ] == [ + { + "cursor": cursor(row), + "node": row_to_camel_case(row), + } + for row in dedup(rows_with_duplicate_names) + ] + + def test_gql_query_aggregates(self, repository, store_in_redis, mock_storage): + query = base_gql_query % ( + repository.author.username, + repository.name, + """ + testResultsAggregates { + totalDuration + slowestTestsDuration + totalFails + totalSkips + totalSlowTests + } + """, + ) + + result = self.gql_request(query, owner=repository.author) + + assert result["owner"]["repository"]["testAnalytics"][ + "testResultsAggregates" + ] == { + "totalDuration": 1000.0, + "slowestTestsDuration": 200.0, + "totalFails": 5, + "totalSkips": 5, + "totalSlowTests": 1, + } + + def test_gql_query_flake_aggregates(self, repository, store_in_redis, mock_storage): + query = base_gql_query % ( + repository.author.username, + repository.name, + """ + flakeAggregates { + flakeRate + flakeCount + } + """, + ) + + result = self.gql_request(query, owner=repository.author) + + assert result["owner"]["repository"]["testAnalytics"]["flakeAggregates"] == { + "flakeRate": 0.1, + "flakeCount": 1, + } diff --git a/apps/codecov-api/graphql_api/tests/test_user.py b/apps/codecov-api/graphql_api/tests/test_user.py new file mode 100644 index 0000000000..aad72fc92a --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_user.py @@ -0,0 +1,88 @@ +from datetime import timedelta + +from django.test import TestCase +from django.utils import timezone +from freezegun import freeze_time +from prometheus_client import REGISTRY +from shared.django_apps.core.tests.factories import OwnerFactory + +from graphql_api.types.user.user import resolve_customer_intent + +from ..views import GQL_ERROR_COUNTER, GQL_HIT_COUNTER, GQL_REQUEST_LATENCIES +from .helper import GraphQLTestHelper + + +@freeze_time("2023-06-19") +class UserTestCase(GraphQLTestHelper, TestCase): + def setUp(self): + self.service_id = 1 + self.user = OwnerFactory( + username="codecov-user", + name="codecov-name", + service="github", + service_id=self.service_id, + student=True, + student_created_at=timezone.now(), + student_updated_at=timezone.now() + timedelta(days=1), + ) + + def test_query_user_resolver(self): + GQL_HIT_COUNTER.labels(operation_type="unknown_type", operation_name="me") + GQL_ERROR_COUNTER.labels(operation_type="unknown_type", operation_name="me") + GQL_REQUEST_LATENCIES.labels(operation_type="unknown_type", operation_name="me") + before = REGISTRY.get_sample_value( + "api_gql_counts_hits_total", + labels={"operation_type": "unknown_type", "operation_name": "me"}, + ) + errors_before = REGISTRY.get_sample_value( + "api_gql_counts_errors_total", + labels={"operation_type": "unknown_type", "operation_name": "me"}, + ) + timer_before = REGISTRY.get_sample_value( + "api_gql_timers_full_runtime_seconds_count", + labels={"operation_type": "unknown_type", "operation_name": "me"}, + ) + query = """{ + me { + user { + username + name + avatarUrl + student + studentCreatedAt + studentUpdatedAt + customerIntent + } + } + } + """ + data = self.gql_request(query, owner=self.user) + assert data["me"]["user"] == { + "username": "codecov-user", + "name": "codecov-name", + "avatarUrl": f"https://avatars0.githubusercontent.com/u/{self.service_id}?v=3&s=55", + "student": True, + "studentCreatedAt": "2023-06-19T00:00:00", + "studentUpdatedAt": "2023-06-20T00:00:00", + "customerIntent": "Business", + } + after = REGISTRY.get_sample_value( + "api_gql_counts_hits_total", + labels={"operation_type": "unknown_type", "operation_name": "me"}, + ) + errors_after = REGISTRY.get_sample_value( + "api_gql_counts_errors_total", + labels={"operation_type": "unknown_type", "operation_name": "me"}, + ) + timer_after = REGISTRY.get_sample_value( + "api_gql_timers_full_runtime_seconds_count", + labels={"operation_type": "unknown_type", "operation_name": "me"}, + ) + assert after - before == 1 + assert errors_after - errors_before == 0 + assert timer_after - timer_before == 1 + + def test_query_null_user_customer_intent_resolver(self): + null_user = OwnerFactory(user=None, service_id=4) + data = resolve_customer_intent(null_user, None) + assert data is None diff --git a/apps/codecov-api/graphql_api/tests/test_user_tokens.py b/apps/codecov-api/graphql_api/tests/test_user_tokens.py new file mode 100644 index 0000000000..ee475d1ef7 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_user_tokens.py @@ -0,0 +1,54 @@ +from django.test import TestCase +from shared.django_apps.codecov_auth.tests.factories import ( + OwnerFactory, + UserTokenFactory, +) + +from .helper import GraphQLTestHelper, paginate_connection + +query = """ +query { + me { + tokens { + edges { + node { + id + name + type + lastFour + expiration + } + } + } + } +} +""" + + +class UserTokensTestCase(GraphQLTestHelper, TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov-user") + + self.token1 = UserTokenFactory(owner=self.owner, name="token1") + self.token2 = UserTokenFactory(owner=self.owner, name="token2") + self.token3 = UserTokenFactory(name="token3") + + def test_user_tokens(self): + data = self.gql_request(query, owner=self.owner) + tokens = paginate_connection(data["me"]["tokens"]) + assert tokens == [ + { + "id": str(self.token2.external_id), + "name": "token2", + "type": "api", + "lastFour": str(self.token2.token)[-4:], + "expiration": None, + }, + { + "id": str(self.token1.external_id), + "name": "token1", + "type": "api", + "lastFour": str(self.token1.token)[-4:], + "expiration": None, + }, + ] diff --git a/apps/codecov-api/graphql_api/tests/test_validation.py b/apps/codecov-api/graphql_api/tests/test_validation.py new file mode 100644 index 0000000000..6c7f7ca118 --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_validation.py @@ -0,0 +1,100 @@ +from graphql import ( + GraphQLField, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, + parse, + validate, +) + +from ..validation import ( + create_max_aliases_rule, + create_max_depth_rule, +) + + +def resolve_field(*args): + return "test" + + +QueryType = GraphQLObjectType( + "Query", {"field": GraphQLField(GraphQLString, resolve=resolve_field)} +) +schema = GraphQLSchema(query=QueryType) + + +def validate_query(query, *rules): + ast = parse(query) + return validate(schema, ast, rules=rules) + + +def test_max_depth_rule_allows_within_depth(): + query = """ + query { + field + } + """ + errors = validate_query(query, create_max_depth_rule(2)) + assert not errors, "Expected no errors for depth within the limit" + + +def test_max_depth_rule_rejects_exceeding_depth(): + query = """ + query { + field { + field { + field + } + } + } + """ + errors = validate_query(query, create_max_depth_rule(2)) + assert errors, "Expected errors for exceeding depth limit" + assert any( + "Query depth exceeds the maximum allowed depth" in str(e) for e in errors + ) + + +def test_max_depth_rule_exact_depth(): + query = """ + query { + field + } + """ + errors = validate_query(query, create_max_depth_rule(2)) + assert not errors, "Expected no errors when query depth matches the limit" + + +def test_max_aliases_rule_allows_within_alias_limit(): + query = """ + query { + alias1: field + alias2: field + } + """ + errors = validate_query(query, create_max_aliases_rule(2)) + assert not errors, "Expected no errors for alias count within the limit" + + +def test_max_aliases_rule_rejects_exceeding_alias_limit(): + query = """ + query { + alias1: field + alias2: field + alias3: field + } + """ + errors = validate_query(query, create_max_aliases_rule(2)) + assert errors, "Expected errors for exceeding alias limit" + assert any("Query uses too many aliases" in str(e) for e in errors) + + +def test_max_aliases_rule_exact_alias_limit(): + query = """ + query { + alias1: field + alias2: field + } + """ + errors = validate_query(query, create_max_aliases_rule(2)) + assert not errors, "Expected no errors when alias count matches the limit" diff --git a/apps/codecov-api/graphql_api/tests/test_views.py b/apps/codecov-api/graphql_api/tests/test_views.py new file mode 100644 index 0000000000..134f9ab86b --- /dev/null +++ b/apps/codecov-api/graphql_api/tests/test_views.py @@ -0,0 +1,331 @@ +import json +from unittest.mock import Mock, patch + +from ariadne import ObjectType, gql, make_executable_schema +from ariadne.validation import cost_directive +from django.test import RequestFactory, TestCase, override_settings +from django.urls import ResolverMatch +from prometheus_client import REGISTRY + +from codecov.commands.exceptions import Unauthorized + +from ..views import AsyncGraphqlView, QueryMetricsExtension +from .helper import GraphQLTestHelper + + +def generate_schema_that_raise_with(exception): + types = """ + type Query { + failing: Boolean + } + """ + query_bindable = ObjectType("Query") + + @query_bindable.field("failing") + def failing_bindable(*_): + raise exception + + return make_executable_schema(types, query_bindable) + + +def generate_cost_test_schema(): + types = """ + type Query { + stuff: String @cost(complexity: 2000) + } + """ + query_bindable = ObjectType("Query") + + return make_executable_schema([types, cost_directive], query_bindable) + + +def generate_schema_with_required_variables(): + types = gql( + """ + type Query { + person_exists(name: String!): Boolean + stuff: String + } + """ + ) + query_bindable = ObjectType("Query") + + # Add a resolver for the `person_exists` field + @query_bindable.field("person_exists") + def resolve_person_exists(_, info, name): + return name is not None # Example resolver logic + + return make_executable_schema(types, query_bindable) + + +class AriadneViewTestCase(GraphQLTestHelper, TestCase): + async def do_query(self, schema, query="{ failing }", variables=None): + view = AsyncGraphqlView.as_view(schema=schema) + data = {"query": query} + if variables is not None: + data["variables"] = variables + + request = RequestFactory().post( + "/graphql/gh", data, content_type="application/json" + ) + match = ResolverMatch(func=lambda: None, args=(), kwargs={"service": "github"}) + + request.resolver_match = match + request.user = None + request.current_owner = None + res = await view(request, service="gh") + return json.loads(res.content) + + @override_settings(DEBUG=True) + @patch("logging.Logger.info") + async def test_when_debug_is_true(self, patched_log): + before = REGISTRY.get_sample_value( + "api_gql_counts_hits_total", + labels={"operation_type": "unknown_type", "operation_name": "unknown_name"}, + ) + errors_before = REGISTRY.get_sample_value( + "api_gql_counts_errors_total", + labels={"operation_type": "unknown_type", "operation_name": "unknown_name"}, + ) + timer_before = REGISTRY.get_sample_value( + "api_gql_timers_full_runtime_seconds_count", + labels={"operation_type": "unknown_type", "operation_name": "unknown_name"}, + ) + schema = generate_schema_that_raise_with(Exception("hello")) + data = await self.do_query(schema) + assert data["errors"] is not None + assert data["errors"][0]["message"] == "hello" + assert data["errors"][0]["extensions"] is not None + after = REGISTRY.get_sample_value( + "api_gql_counts_hits_total", + labels={"operation_type": "unknown_type", "operation_name": "unknown_name"}, + ) + errors_after = REGISTRY.get_sample_value( + "api_gql_counts_errors_total", + labels={"operation_type": "unknown_type", "operation_name": "unknown_name"}, + ) + timer_after = REGISTRY.get_sample_value( + "api_gql_timers_full_runtime_seconds_count", + labels={"operation_type": "unknown_type", "operation_name": "unknown_name"}, + ) + assert after - before == 1 + assert errors_after - errors_before == 1 + assert timer_after - timer_before == 1 + patched_log.assert_called_with( + "Could not match gql query format for logging", + extra=dict( + query_slice="{ failing }", + ), + ) + + @override_settings(DEBUG=False) + async def test_when_debug_is_false_and_random_exception(self): + schema = generate_schema_that_raise_with(Exception("hello")) + data = await self.do_query(schema) + assert data["errors"] is not None + assert data["errors"][0]["message"] == "INTERNAL SERVER ERROR" + assert data["errors"][0]["type"] == "ServerError" + assert data["errors"][0].get("extensions") is None + + @override_settings(DEBUG=False) + async def test_when_debug_is_false_and_exception_we_know(self): + schema = generate_schema_that_raise_with(Unauthorized()) + data = await self.do_query(schema) + assert data["errors"] is not None + assert data["errors"][0]["message"] == "You are not authorized" + assert data["errors"][0]["type"] == "Unauthorized" + assert data["errors"][0].get("extensions") is None + + @override_settings(DEBUG=True) + async def test_when_bad_query(self): + schema = generate_schema_that_raise_with(Unauthorized()) + data = await self.do_query(schema, " { fieldThatDoesntExist }") + assert data["errors"] is not None + assert ( + data["errors"][0]["message"] + == "Cannot query field 'fieldThatDoesntExist' on type 'Query'." + ) + + @override_settings(DEBUG=False) + async def test_when_bad_query_and_anonymous(self): + schema = generate_schema_that_raise_with(Unauthorized()) + data = await self.do_query(schema, " { fieldThatDoesntExist }") + assert data["errors"] is not None + assert data["errors"][0]["message"] == "INTERNAL SERVER ERROR" + + @override_settings(DEBUG=False, GRAPHQL_QUERY_COST_THRESHOLD=1000) + @patch("logging.Logger.error") + async def test_when_costly_query(self, mock_error_logger): + schema = generate_cost_test_schema() + data = await self.do_query(schema, " { stuff }") + + assert data["errors"] is not None + assert data["errors"][0]["extensions"]["cost"]["requestedQueryCost"] == 2000 + assert data["errors"][0]["extensions"]["cost"]["maximumAvailable"] == 1000 + mock_error_logger.assert_called_with( + "Query Cost Exceeded", + extra=dict( + requested_cost=2000, + maximum_cost=1000, + request_body=dict(query="{ stuff }"), + ), + ) + + @patch("logging.Logger.info") + async def test_query_metrics_extension_set_type_and_name(self, patched_log): + extension = QueryMetricsExtension() + sample_named_query = "query MySession { operation body }" + sample_named_mutation = "mutation($input: CancelTrialInput!) { operation body }" + sample_unnamed_query = "{ owner(username: me) { continued operation body } }" + sample_wildcard = "{ failing }" + + assert extension.operation_type is None + assert extension.operation_name is None + + extension.set_type_and_name(query=sample_named_query) + assert extension.operation_type == "query" + assert extension.operation_name == "MySession" + + extension.set_type_and_name(query=sample_named_mutation) + assert extension.operation_type == "mutation" + assert extension.operation_name == "CancelTrialInput" + + extension.set_type_and_name(query=sample_unnamed_query) + assert extension.operation_type == "unknown_type" + assert extension.operation_name == "owner" + + extension.set_type_and_name(query=sample_wildcard) + assert extension.operation_type == "unknown_type" + assert extension.operation_name == "unknown_name" + patched_log.assert_called_with( + "Could not match gql query format for logging", + extra=dict( + query_slice="{ failing }", + ), + ) + + @patch("regex.match") + @patch("logging.Logger.error") + @patch("logging.Logger.info") + async def test_query_metrics_extension_set_type_and_name_timeout( + self, patched_info_log, patched_error_log, patched_regex + ): + patched_regex.side_effect = TimeoutError + extension = QueryMetricsExtension() + sample_named_query = "query MySession { operation body }" + + extension.set_type_and_name(query=sample_named_query) + + patched_info_log.assert_called_with( + "Could not match gql query format for logging", + extra=dict( + query_slice=sample_named_query[:30], + ), + ) + patched_error_log.assert_called_with( + "Regex Timeout Error", + extra=dict( + query_slice=sample_named_query[:30], + ), + ) + assert extension.operation_type == "unknown_type" + assert extension.operation_name == "unknown_name" + + @patch("graphql_api.views.GQL_REQUEST_MADE_COUNTER.labels") + @patch("graphql_api.views.GQL_ERROR_TYPE_COUNTER.labels") + @patch("graphql_api.views.AsyncGraphqlView._check_ratelimit") + @override_settings(DEBUG=False, GRAPHQL_RATE_LIMIT_RPM=1000) + async def test_when_rate_limit_reached( + self, mocked_check_ratelimit, mocked_error_counter, mocked_request_counter + ): + schema = generate_cost_test_schema() + mocked_check_ratelimit.return_value = True + response = await self.do_query(schema, " { stuff }") + + assert response["status"] == 429 + assert ( + response["detail"] + == "It looks like you've hit the rate limit of 1000 req/min. Try again later." + ) + + mocked_error_counter.assert_called_with( + error_type="rate_limit", path="/graphql/gh" + ) + mocked_request_counter.assert_called_with(path="/graphql/gh") + + @override_settings( + DEBUG=False, GRAPHQL_RATE_LIMIT_RPM=0, GRAPHQL_RATE_LIMIT_ENABLED=False + ) + def test_rate_limit_disabled(self): + # rate limit is 0, so any request would cause a rate limit if the GQL rate limit was enabled + view = AsyncGraphqlView() + request = Mock() + + result = view._check_ratelimit(request) + assert result == False + + def test_client_ip_from_x_forwarded_for(self): + view = AsyncGraphqlView() + request = Mock() + request.META = {"HTTP_X_FORWARDED_FOR": "127.0.0.1,blah", "REMOTE_ADDR": "lol"} + + result = view.get_client_ip(request) + assert result == "127.0.0.1" + + def test_client_ip_from_remote_addr(self): + view = AsyncGraphqlView() + request = Mock() + request.META = {"HTTP_X_FORWARDED_FOR": None, "REMOTE_ADDR": "lol"} + + result = view.get_client_ip(request) + assert result == "lol" + + async def test_required_variable_present(self): + schema = generate_schema_with_required_variables() + + query = """ + query ($name: String!) { + person_exists(name: $name) + } + """ + + # Provide the variable + data = await self.do_query(schema, query=query, variables={"name": "Bob"}) + + assert data is not None + assert "data" in data + assert data["data"]["person_exists"] is True + + async def test_required_variable_missing(self): + schema = generate_schema_with_required_variables() + + query = """ + query ($name: String!) { + person_exists(name: $name) + } + """ + + # Don't provide the variable + data = await self.do_query(schema, query=query, variables={}) + + assert data == {"detail": "Missing required variables: name", "status": 400} + + async def test_empty_request_body(self): + schema = generate_schema_with_required_variables() + + request = RequestFactory().post( + "/graphql/gh", "", content_type="application/json" + ) + match = ResolverMatch(func=lambda: None, args=(), kwargs={"service": "github"}) + request.resolver_match = match + request.user = None + request.current_owner = None + + view = AsyncGraphqlView.as_view(schema=schema) + response = await view(request, service="gh") + + assert response.status_code == 400 + assert json.loads(response.content) == { + "status": 400, + "detail": "Invalid JSON response received.", + } diff --git a/apps/codecov-api/graphql_api/types/__init__.py b/apps/codecov-api/graphql_api/types/__init__.py new file mode 100644 index 0000000000..41794229d7 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/__init__.py @@ -0,0 +1,203 @@ +from ariadne.validation import cost_directive +from ariadne_django.scalars import datetime_scalar + +from ..helpers.ariadne import ariadne_load_local_graphql +from .account import account, account_bindable +from .billing import billing, billing_bindable +from .branch import branch, branch_bindable +from .bundle_analysis import ( + bundle_analysis, + bundle_analysis_comparison, + bundle_analysis_comparison_bindable, + bundle_analysis_comparison_result_bindable, + bundle_analysis_report, + bundle_analysis_report_bindable, + bundle_analysis_report_result_bindable, + bundle_asset_bindable, + bundle_comparison_bindable, + bundle_data_bindable, + bundle_module_bindable, + bundle_report_bindable, + bundle_report_info_bindable, +) +from .commit import ( + commit, + commit_bindable, + commit_bundle_analysis_bindable, + commit_coverage_analytics_bindable, +) +from .comparison import comparison, comparison_bindable, comparison_result_bindable +from .component import component, component_bindable +from .component_comparison import component_comparison, component_comparison_bindable +from .config import config, config_bindable +from .coverage_analytics import coverage_analytics, coverage_analytics_bindable +from .coverage_totals import coverage_totals, coverage_totals_bindable +from .enums import enum_types +from .file import commit_file, file_bindable +from .flag import flag, flag_bindable +from .flag_comparison import flag_comparison, flag_comparison_bindable +from .flake_aggregates import flake_aggregates, flake_aggregates_bindable +from .impacted_file import ( + impacted_file, + impacted_file_bindable, + impacted_files_result_bindable, +) +from .invoice import invoice, invoice_bindable +from .line_comparison import line_comparison, line_comparison_bindable +from .me import me, me_bindable, tracking_metadata_bindable +from .measurement import measurement, measurement_bindable +from .mutation import mutation, mutation_resolvers +from .okta_config import okta_config, okta_config_bindable +from .owner import owner, owner_bindable +from .path_contents import ( + deprecated_path_contents_result_bindable, + path_content, + path_content_bindable, + path_content_file_bindable, + path_contents_result_bindable, +) +from .plan import plan, plan_bindable +from .plan_representation import plan_representation, plan_representation_bindable +from .profile import profile, profile_bindable +from .pull import pull, pull_bindable +from .query import query, query_bindable +from .repository import repository, repository_bindable, repository_result_bindable +from .repository_config import ( + indication_range_bindable, + repository_config, + repository_config_bindable, +) +from .segment_comparison import ( + segment_comparison, + segment_comparison_bindable, + segments_result_bindable, +) +from .self_hosted_license import self_hosted_license, self_hosted_license_bindable +from .session import session, session_bindable +from .test_analytics import test_analytics, test_analytics_bindable +from .test_results import test_result_bindable, test_results +from .test_results_aggregates import ( + test_results_aggregates, + test_results_aggregates_bindable, +) +from .upload import upload, upload_bindable, upload_error_bindable +from .user import user, user_bindable +from .user_token import user_token, user_token_bindable + +inputs = ariadne_load_local_graphql(__file__, "./inputs") +enums = ariadne_load_local_graphql(__file__, "./enums") +errors = ariadne_load_local_graphql(__file__, "./errors") +types = [ + billing, + branch, + bundle_analysis_comparison, + bundle_analysis_report, + bundle_analysis, + commit_file, + commit, + comparison, + component_comparison, + component, + config, + cost_directive, + coverage_analytics, + coverage_totals, + enums, + errors, + flag_comparison, + flag, + impacted_file, + inputs, + invoice, + line_comparison, + me, + measurement, + mutation, + owner, + path_content, + plan_representation, + plan, + profile, + pull, + query, + repository_config, + repository, + segment_comparison, + self_hosted_license, + session, + test_analytics, + upload, + user_token, + user, + account, + okta_config, + test_results, + flake_aggregates, + test_results_aggregates, +] + +bindables = [ + *enum_types.enum_types, + *mutation_resolvers, + billing_bindable, + branch_bindable, + bundle_analysis_comparison_bindable, + bundle_analysis_comparison_result_bindable, + bundle_analysis_report_bindable, + bundle_analysis_report_result_bindable, + bundle_asset_bindable, + bundle_comparison_bindable, + bundle_data_bindable, + bundle_module_bindable, + bundle_report_bindable, + bundle_report_info_bindable, + commit_bindable, + commit_bundle_analysis_bindable, + commit_coverage_analytics_bindable, + comparison_bindable, + comparison_result_bindable, + component_bindable, + component_comparison_bindable, + config_bindable, + coverage_analytics_bindable, + coverage_totals_bindable, + datetime_scalar, + file_bindable, + flag_bindable, + flag_comparison_bindable, + impacted_file_bindable, + impacted_files_result_bindable, + indication_range_bindable, + invoice_bindable, + line_comparison_bindable, + me_bindable, + measurement_bindable, + owner_bindable, + path_content_bindable, + path_content_file_bindable, + path_contents_result_bindable, + deprecated_path_contents_result_bindable, + plan_bindable, + plan_representation_bindable, + profile_bindable, + pull_bindable, + query_bindable, + repository_bindable, + repository_config_bindable, + repository_result_bindable, + segment_comparison_bindable, + segments_result_bindable, + self_hosted_license_bindable, + session_bindable, + test_analytics_bindable, + tracking_metadata_bindable, + upload_bindable, + upload_error_bindable, + user_bindable, + user_token_bindable, + account_bindable, + okta_config_bindable, + test_result_bindable, + test_results_aggregates_bindable, + flake_aggregates_bindable, +] diff --git a/apps/codecov-api/graphql_api/types/account/__init__.py b/apps/codecov-api/graphql_api/types/account/__init__.py new file mode 100644 index 0000000000..fd7541cd3c --- /dev/null +++ b/apps/codecov-api/graphql_api/types/account/__init__.py @@ -0,0 +1,3 @@ +from .account import account, account_bindable + +__all__ = ["account_bindable", "account"] diff --git a/apps/codecov-api/graphql_api/types/account/account.graphql b/apps/codecov-api/graphql_api/types/account/account.graphql new file mode 100644 index 0000000000..3af381ea42 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/account/account.graphql @@ -0,0 +1,13 @@ +type Account { + name: String! + oktaConfig: OktaConfig + totalSeatCount: Int! + activatedUserCount: Int! + organizations( + orderingDirection: OrderingDirection + first: Int + after: String + last: Int + before: String + ): AccountOrganizationConnection! @cost(complexity: 25, multipliers: ["first", "last"]) +} diff --git a/apps/codecov-api/graphql_api/types/account/account.py b/apps/codecov-api/graphql_api/types/account/account.py new file mode 100644 index 0000000000..8bcfc17225 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/account/account.py @@ -0,0 +1,56 @@ +from typing import Any, Coroutine, Optional + +from ariadne import ObjectType +from asgiref.sync import sync_to_async +from graphql import GraphQLResolveInfo + +from codecov_auth.models import Account, OktaSettings, Owner +from graphql_api.helpers.ariadne import ariadne_load_local_graphql +from graphql_api.helpers.connection import ( + build_connection_graphql, + queryset_to_connection, +) +from graphql_api.types.enums.enums import OrderingDirection + +account = ariadne_load_local_graphql(__file__, "account.graphql") +account = account + build_connection_graphql("AccountOrganizationConnection", "Owner") +account_bindable = ObjectType("Account") + + +@account_bindable.field("name") +def resolve_name(account: Account, info: GraphQLResolveInfo) -> str: + return account.name + + +@account_bindable.field("oktaConfig") +@sync_to_async +def resolve_okta_config( + account: Account, info: GraphQLResolveInfo +) -> OktaSettings | None: + return OktaSettings.objects.filter(account_id=account.pk).first() + + +@account_bindable.field("totalSeatCount") +def resolve_total_seat_count(account: Account, info: GraphQLResolveInfo) -> int: + return account.total_seat_count + + +@account_bindable.field("activatedUserCount") +@sync_to_async +def resolve_activated_user_count(account: Account, info: GraphQLResolveInfo) -> int: + return account.activated_user_count + + +@account_bindable.field("organizations") +def resolve_organizations( + account: Account, + info: GraphQLResolveInfo, + ordering_direction: Optional[OrderingDirection] = OrderingDirection.ASC, + **kwargs: Any, +) -> Coroutine[Any, Any, Owner]: + return queryset_to_connection( + account.organizations, + ordering=("username",), + ordering_direction=ordering_direction, + **kwargs, + ) diff --git a/apps/codecov-api/graphql_api/types/billing/__init__.py b/apps/codecov-api/graphql_api/types/billing/__init__.py new file mode 100644 index 0000000000..5b0c130147 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/billing/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .billing import billing_bindable + +billing = ariadne_load_local_graphql(__file__, "billing.graphql") + + +__all__ = ["billing_bindable"] diff --git a/apps/codecov-api/graphql_api/types/billing/billing.graphql b/apps/codecov-api/graphql_api/types/billing/billing.graphql new file mode 100644 index 0000000000..972421c7a0 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/billing/billing.graphql @@ -0,0 +1,8 @@ +type Billing { + unverifiedPaymentMethods: [UnverifiedPaymentMethod] +} + +type UnverifiedPaymentMethod { + paymentMethodId: String! + hostedVerificationUrl: String +} diff --git a/apps/codecov-api/graphql_api/types/billing/billing.py b/apps/codecov-api/graphql_api/types/billing/billing.py new file mode 100644 index 0000000000..bffb6d6895 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/billing/billing.py @@ -0,0 +1,14 @@ +from ariadne import ObjectType +from graphql import GraphQLResolveInfo + +from codecov_auth.models import Owner +from services.billing import BillingService + +billing_bindable = ObjectType("Billing") + + +@billing_bindable.field("unverifiedPaymentMethods") +def resolve_unverified_payment_methods( + owner: Owner, info: GraphQLResolveInfo +) -> list[dict]: + return BillingService(requesting_user=owner).get_unverified_payment_methods(owner) diff --git a/apps/codecov-api/graphql_api/types/branch/__init__.py b/apps/codecov-api/graphql_api/types/branch/__init__.py new file mode 100644 index 0000000000..27a3954e89 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/branch/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .branch import branch_bindable + +branch = ariadne_load_local_graphql(__file__, "branch.graphql") + + +__all__ = ["branch_bindable"] diff --git a/apps/codecov-api/graphql_api/types/branch/branch.graphql b/apps/codecov-api/graphql_api/types/branch/branch.graphql new file mode 100644 index 0000000000..162ebe9b9b --- /dev/null +++ b/apps/codecov-api/graphql_api/types/branch/branch.graphql @@ -0,0 +1,5 @@ +type Branch { + name: String! + headSha: String! + head: Commit +} diff --git a/apps/codecov-api/graphql_api/types/branch/branch.py b/apps/codecov-api/graphql_api/types/branch/branch.py new file mode 100644 index 0000000000..c396cdab65 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/branch/branch.py @@ -0,0 +1,25 @@ +from typing import Optional + +from ariadne import ObjectType +from graphql import GraphQLResolveInfo + +from core.models import Branch, Commit +from graphql_api.dataloader.commit import CommitLoader + +branch_bindable = ObjectType("Branch") + + +@branch_bindable.field("headSha") +def resolve_head_sha(branch: Branch, info: GraphQLResolveInfo) -> str: + head = branch.head + return head + + +@branch_bindable.field("head") +async def resolve_head_commit( + branch: Branch, info: GraphQLResolveInfo +) -> Optional[Commit]: + head = branch.head + if head: + loader = CommitLoader.loader(info, branch.repository_id) + return await loader.load(head) diff --git a/apps/codecov-api/graphql_api/types/bundle_analysis/__init__.py b/apps/codecov-api/graphql_api/types/bundle_analysis/__init__.py new file mode 100644 index 0000000000..40cf8873ad --- /dev/null +++ b/apps/codecov-api/graphql_api/types/bundle_analysis/__init__.py @@ -0,0 +1,36 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .base import ( + bundle_asset_bindable, + bundle_data_bindable, + bundle_module_bindable, + bundle_report_bindable, + bundle_report_info_bindable, +) +from .comparison import ( + bundle_analysis_comparison_bindable, + bundle_analysis_comparison_result_bindable, + bundle_comparison_bindable, +) +from .report import ( + bundle_analysis_report_bindable, + bundle_analysis_report_result_bindable, +) + +bundle_analysis = ariadne_load_local_graphql(__file__, "base.graphql") +bundle_analysis_comparison = ariadne_load_local_graphql(__file__, "comparison.graphql") +bundle_analysis_report = ariadne_load_local_graphql(__file__, "report.graphql") + + +__all__ = [ + "bundle_asset_bindable", + "bundle_data_bindable", + "bundle_module_bindable", + "bundle_report_bindable", + "bundle_report_info_bindable", + "bundle_analysis_comparison_bindable", + "bundle_analysis_comparison_result_bindable", + "bundle_comparison_bindable", + "bundle_analysis_report_bindable", + "bundle_analysis_report_result_bindable", +] diff --git a/apps/codecov-api/graphql_api/types/bundle_analysis/base.graphql b/apps/codecov-api/graphql_api/types/bundle_analysis/base.graphql new file mode 100644 index 0000000000..d6102ca91b --- /dev/null +++ b/apps/codecov-api/graphql_api/types/bundle_analysis/base.graphql @@ -0,0 +1,116 @@ +enum BundleAnalysisMeasurementsAssetType { + REPORT_SIZE + JAVASCRIPT_SIZE + STYLESHEET_SIZE + FONT_SIZE + IMAGE_SIZE + ASSET_SIZE + UNKNOWN_SIZE +} + +enum BundleReportGroups { + JAVASCRIPT + STYLESHEET + FONT + IMAGE + UNKNOWN +} + +enum BundleLoadTypes { + ENTRY + INITIAL + LAZY +} + +type BundleSize { + gzip: Int! + uncompress: Int! +} + +type BundleLoadTime { + threeG: Int! + highSpeed: Int! +} + +type BundleData { + loadTime: BundleLoadTime! + size: BundleSize! +} + +type BundleModule { + name: String! + extension: String! + bundleData: BundleData! +} + +type BundleAsset { + name: String! + extension: String! + normalizedName: String! + modules: [BundleModule]! + bundleData: BundleData! + measurements( + interval: MeasurementInterval! + before: DateTime! + after: DateTime + branch: String + ): BundleAnalysisMeasurements + routes: [String!] +} + +type BundleReportInfo { + version: String! + pluginName: String! + pluginVersion: String! + builtAt: String! + duration: Int! + bundlerName: String! + bundlerVersion: String! +} + +type BundleReport { + name: String! + moduleCount: Int! + assets: [BundleAsset]! + asset(name: String!): BundleAsset + bundleData: BundleData! + bundleDataFiltered(filters: BundleReportFilters): BundleData! + measurements( + interval: MeasurementInterval! + before: DateTime! + after: DateTime + branch: String + orderingDirection: OrderingDirection + filters: BundleAnalysisMeasurementsSetFilters + ): [BundleAnalysisMeasurements!] + isCached: Boolean! + assetsPaginated( + ordering: AssetOrdering + orderingDirection: OrderingDirection + first: Int + after: String + last: Int + before: String + ): AssetConnection + info: BundleReportInfo! + cacheConfig: Boolean! +} + +type BundleAnalysisMeasurements{ + assetType: BundleAnalysisMeasurementsAssetType! + name: String + size: BundleData + change: BundleData + measurements: [Measurement!] +} + +type AssetConnection { + edges: [AssetEdge]! + totalCount: Int! + pageInfo: PageInfo! +} + +type AssetEdge { + cursor: String! + node: BundleAsset! +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/bundle_analysis/base.py b/apps/codecov-api/graphql_api/types/bundle_analysis/base.py new file mode 100644 index 0000000000..f086848f4f --- /dev/null +++ b/apps/codecov-api/graphql_api/types/bundle_analysis/base.py @@ -0,0 +1,435 @@ +from copy import deepcopy +from datetime import datetime +from typing import Dict, List, Mapping, Optional, Union + +import sentry_sdk +from ariadne import ObjectType +from asgiref.sync import sync_to_async +from graphql import GraphQLResolveInfo + +from codecov.commands.exceptions import ValidationError +from graphql_api.types.enums import AssetOrdering, OrderingDirection +from services.bundle_analysis import ( + AssetReport, + BundleAnalysisMeasurementData, + BundleAnalysisMeasurementsAssetType, + BundleAnalysisMeasurementsService, + BundleData, + BundleLoadTime, + BundleReport, + BundleReportInfo, + BundleSize, + ModuleReport, +) +from timeseries.models import Interval + +ASSET_TYPE_UNKNOWN = "UNKNOWN_SIZE" + +bundle_data_bindable = ObjectType("BundleData") +bundle_module_bindable = ObjectType("BundleModule") +bundle_asset_bindable = ObjectType("BundleAsset") +bundle_report_bindable = ObjectType("BundleReport") +bundle_report_info_bindable = ObjectType("BundleReportInfo") + + +def _find_index_by_cursor(assets: List, cursor: str) -> int: + try: + for i, asset in enumerate(assets): + if asset.id == int(cursor): + return i + except ValueError: + pass + return -1 + + +def _compute_unknown_asset_size_raw_measurements(fetched_data: dict) -> List[dict]: + """ + Computes measurements for the unknown asset types, some asset types are not in + the predetermined list of types so we must compute those measurements manually. + The heuristic will be to get the measurements of the bundle type (ie total bundle size) + then substract it from all the known asset type measurements, leaving with only the unknown. + """ + unknown_raw_measurements = deepcopy( + fetched_data[BundleAnalysisMeasurementsAssetType.REPORT_SIZE][ + 0 + ].raw_measurements + ) + + for name, measurements in fetched_data.items(): + if name not in ( + BundleAnalysisMeasurementsAssetType.REPORT_SIZE, + BundleAnalysisMeasurementsAssetType.ASSET_SIZE, + ): + raw_measurements = measurements[0].raw_measurements + for i in range(len(raw_measurements)): + if len(unknown_raw_measurements) != len(raw_measurements): + return [] + unknown_raw_measurements[i]["min"] -= raw_measurements[i]["min"] + unknown_raw_measurements[i]["max"] -= raw_measurements[i]["max"] + unknown_raw_measurements[i]["avg"] -= raw_measurements[i]["avg"] + + return unknown_raw_measurements + + +# ============= Bundle Data Bindable ============= + + +@bundle_data_bindable.field("size") +def resolve_bundle_size( + bundle_data: BundleData, info: GraphQLResolveInfo +) -> BundleSize: + return bundle_data.size + + +@bundle_data_bindable.field("loadTime") +def resolve_bundle_load_time( + bundle_data: BundleData, info: GraphQLResolveInfo +) -> BundleLoadTime: + return bundle_data.load_time + + +# ============= Bundle Module Bindable ============= + + +@bundle_module_bindable.field("name") +def resolve_bundle_module_name( + bundle_module: ModuleReport, info: GraphQLResolveInfo +) -> str: + return bundle_module.name + + +@bundle_module_bindable.field("bundleData") +def resolve_bundle_module_bundle_data( + bundle_module: ModuleReport, info: GraphQLResolveInfo +) -> BundleData: + return BundleData(bundle_module.size_total) + + +# ============= Bundle Asset Bindable ============= + + +@bundle_asset_bindable.field("name") +def resolve_bundle_asset_name( + bundle_asset: AssetReport, info: GraphQLResolveInfo +) -> str: + return bundle_asset.name + + +@bundle_asset_bindable.field("normalizedName") +def resolve_normalized_name(bundle_asset: AssetReport, info: GraphQLResolveInfo) -> str: + return bundle_asset.normalized_name + + +@bundle_asset_bindable.field("extension") +def resolve_extension(bundle_asset: AssetReport, info: GraphQLResolveInfo) -> str: + return bundle_asset.extension + + +@bundle_asset_bindable.field("bundleData") +def resolve_bundle_asset_bundle_data( + bundle_asset: AssetReport, info: GraphQLResolveInfo +) -> BundleData: + return BundleData(bundle_asset.size_total, bundle_asset.gzip_size_total) + + +@bundle_asset_bindable.field("modules") +def resolve_modules( + bundle_asset: AssetReport, info: GraphQLResolveInfo +) -> List[ModuleReport]: + return bundle_asset.modules + + +@bundle_asset_bindable.field("measurements") +@sync_to_async +@sentry_sdk.trace +def resolve_asset_report_measurements( + bundle_asset: AssetReport, + info: GraphQLResolveInfo, + interval: Interval, + before: datetime, + after: Optional[datetime] = None, + branch: Optional[str] = None, +) -> Optional[BundleAnalysisMeasurementData]: + bundle_analysis_measurements = BundleAnalysisMeasurementsService( + repository=info.context["commit"].repository, + interval=interval, + before=before, + after=after, + branch=branch, + ) + return bundle_analysis_measurements.compute_asset(bundle_asset) + + +@bundle_asset_bindable.field("routes") +def resolve_routes( + bundle_asset: AssetReport, info: GraphQLResolveInfo +) -> Optional[List[str]]: + return bundle_asset.routes + + +# ============= Bundle Report Bindable ============= + + +@bundle_report_bindable.field("name") +def resolve_name(bundle_report: BundleReport, info: GraphQLResolveInfo) -> str: + return bundle_report.name + + +@bundle_report_bindable.field("moduleCount") +def resolve_module_count(bundle_report: BundleReport, info: GraphQLResolveInfo) -> int: + return bundle_report.module_count + + +@bundle_report_bindable.field("assets") +def resolve_assets( + bundle_report: BundleReport, + info: GraphQLResolveInfo, +) -> List[AssetReport]: + return list(bundle_report.assets()) + + +@bundle_report_bindable.field("assetsPaginated") +@sentry_sdk.trace +def resolve_assets_paginated( + bundle_report: BundleReport, + info: GraphQLResolveInfo, + ordering: AssetOrdering = AssetOrdering.SIZE, + ordering_direction: OrderingDirection = OrderingDirection.DESC, + first: Optional[int] = None, + after: Optional[str] = None, + last: Optional[int] = None, + before: Optional[str] = None, +) -> Union[Dict[str, object], ValidationError]: + if first is not None and last is not None: + return ValidationError("First and last can not be used at the same time") + if after is not None and before is not None: + return ValidationError("After and before can not be used at the same time") + + # All filtered assets before pagination + assets = list( + bundle_report.assets( + ordering=ordering.value, + ordering_desc=ordering_direction.value == OrderingDirection.DESC.value, + ) + ) + + total_count, has_next_page, has_previous_page = len(assets), False, False + start_cursor, end_cursor = None, None + + # Apply cursors to edges + if after is not None: + after_edge = _find_index_by_cursor(assets, after) + if after_edge > -1: + assets = assets[after_edge + 1 :] + + if before is not None: + before_edge = _find_index_by_cursor(assets, before) + if before_edge > -1: + assets = assets[:before_edge] + + # Slice edges by return size + if first is not None and first >= 0: + if len(assets) > first: + assets = assets[:first] + has_next_page = True + + if last is not None and last >= 0: + if len(assets) > last: + assets = assets[len(assets) - last :] + has_previous_page = True + + if assets: + start_cursor, end_cursor = assets[0].id, assets[-1].id + + return { + "edges": [{"cursor": asset.id, "node": asset} for asset in assets], + "total_count": total_count, + "page_info": { + "has_next_page": has_next_page, + "has_previous_page": has_previous_page, + "start_cursor": start_cursor, + "end_cursor": end_cursor, + }, + } + + +@bundle_report_bindable.field("asset") +def resolve_asset( + bundle_report: BundleReport, info: GraphQLResolveInfo, name: str +) -> Optional[AssetReport]: + return bundle_report.asset(name) + + +@bundle_report_bindable.field("bundleData") +def resolve_bundle_data( + bundle_report: BundleReport, info: GraphQLResolveInfo +) -> BundleData: + return BundleData( + bundle_report.size_total, + bundle_report.gzip_size_total, + ) + + +@bundle_report_bindable.field("bundleDataFiltered") +def resolve_bundle_report_filtered( + bundle_report: BundleReport, + info: GraphQLResolveInfo, + filters: dict[str, list[str]] = {}, +) -> BundleData: + group = filters.get("report_group") + return BundleData( + bundle_report.report.total_size(asset_types=[group] if group else None), + bundle_report.report.total_gzip_size(asset_types=[group] if group else None), + ) + + +@bundle_report_bindable.field("measurements") +@sync_to_async +@sentry_sdk.trace +def resolve_bundle_report_measurements( + bundle_report: BundleReport, + info: GraphQLResolveInfo, + interval: Interval, + before: datetime, + after: Optional[datetime] = None, + branch: Optional[str] = None, + filters: Mapping = {}, + ordering_direction: Optional[OrderingDirection] = OrderingDirection.ASC, +) -> List[BundleAnalysisMeasurementData]: + asset_types = list(filters.get("asset_types", [])) + bundle_analysis_measurements = BundleAnalysisMeasurementsService( + repository=info.context["commit"].repository, + interval=interval, + before=before, + after=after, + branch=branch, + ) + + # All measureable names we need to fetch to compute the requested asset types + if not asset_types: + measurables_to_fetch = list(BundleAnalysisMeasurementsAssetType) + elif ASSET_TYPE_UNKNOWN in asset_types: + measurables_to_fetch = [ + BundleAnalysisMeasurementsAssetType.REPORT_SIZE, + BundleAnalysisMeasurementsAssetType.JAVASCRIPT_SIZE, + BundleAnalysisMeasurementsAssetType.STYLESHEET_SIZE, + BundleAnalysisMeasurementsAssetType.FONT_SIZE, + BundleAnalysisMeasurementsAssetType.IMAGE_SIZE, + ] + else: + measurables_to_fetch = [ + BundleAnalysisMeasurementsAssetType[item] for item in asset_types + ] + + # Retrieve all the measurements necessary to compute the requested asset types + fetched_data = {} + for name in measurables_to_fetch: + fetched_data[name] = bundle_analysis_measurements.compute_report( + bundle_report, asset_type=name + ) + + # All measureable name we need to return + if not asset_types: + measurables_to_display = list(BundleAnalysisMeasurementsAssetType) + else: + measurables_to_display = [ + BundleAnalysisMeasurementsAssetType[item] + for item in asset_types + if item != ASSET_TYPE_UNKNOWN + ] + + measurements = [] + for measurable in measurables_to_display: + measurements.extend(fetched_data[measurable]) + + # Compute for unknown asset type size if necessary + if not asset_types or ASSET_TYPE_UNKNOWN in asset_types: + unknown_size_raw_measurements = _compute_unknown_asset_size_raw_measurements( + fetched_data + ) + measurements.append( + BundleAnalysisMeasurementData( + raw_measurements=unknown_size_raw_measurements, + asset_type=ASSET_TYPE_UNKNOWN, + asset_name=None, + interval=interval, + after=after, + before=before, + ) + ) + + return sorted( + measurements, + key=lambda c: c.asset_type, + reverse=ordering_direction == OrderingDirection.DESC, + ) + + +@bundle_report_bindable.field("isCached") +def resolve_bundle_report_is_cached( + bundle_report: BundleReport, info: GraphQLResolveInfo +) -> bool: + return bundle_report.is_cached + + +@bundle_report_bindable.field("cacheConfig") +def resolve_bundle_report_cache_config( + bundle_report: BundleReport, info: GraphQLResolveInfo +) -> bool: + return bundle_report.cache_config(info.context["commit"].repository.pk) + + +@bundle_report_bindable.field("info") +def resolve_bundle_report_info( + bundle_report: BundleReport, info: GraphQLResolveInfo +) -> BundleReportInfo: + return BundleReportInfo(bundle_report.info) + + +@bundle_report_info_bindable.field("version") +def resolve_bundle_report_info_version( + bundle_report_info: BundleReportInfo, info: GraphQLResolveInfo +) -> str: + return bundle_report_info.version + + +@bundle_report_info_bindable.field("pluginName") +def resolve_bundle_report_info_plugin_name( + bundle_report_info: BundleReportInfo, info: GraphQLResolveInfo +) -> str: + return bundle_report_info.plugin_name + + +@bundle_report_info_bindable.field("pluginVersion") +def resolve_bundle_report_info_plugin_version( + bundle_report_info: BundleReportInfo, info: GraphQLResolveInfo +) -> str: + return bundle_report_info.plugin_version + + +@bundle_report_info_bindable.field("builtAt") +def resolve_bundle_report_info_built_at( + bundle_report_info: BundleReportInfo, info: GraphQLResolveInfo +) -> str: + return bundle_report_info.built_at + + +@bundle_report_info_bindable.field("duration") +def resolve_bundle_report_info_duration( + bundle_report_info: BundleReportInfo, info: GraphQLResolveInfo +) -> int: + return bundle_report_info.duration + + +@bundle_report_info_bindable.field("bundlerName") +def resolve_bundle_report_info_bundler_name( + bundle_report_info: BundleReportInfo, info: GraphQLResolveInfo +) -> str: + return bundle_report_info.bundler_name + + +@bundle_report_info_bindable.field("bundlerVersion") +def resolve_bundle_report_info_bundler_version( + bundle_report_info: BundleReportInfo, info: GraphQLResolveInfo +) -> str: + return bundle_report_info.bundler_version diff --git a/apps/codecov-api/graphql_api/types/bundle_analysis/comparison.graphql b/apps/codecov-api/graphql_api/types/bundle_analysis/comparison.graphql new file mode 100644 index 0000000000..c35f5f0a1a --- /dev/null +++ b/apps/codecov-api/graphql_api/types/bundle_analysis/comparison.graphql @@ -0,0 +1,20 @@ +union BundleAnalysisComparisonResult = + BundleAnalysisComparison + | FirstPullRequest + | MissingBaseCommit + | MissingHeadCommit + | MissingHeadReport + | MissingBaseReport + +type BundleAnalysisComparison { + bundles: [BundleComparison]! + bundleData: BundleData! + bundleChange: BundleData! +} + +type BundleComparison { + name: String! + changeType: String! + bundleData: BundleData! + bundleChange: BundleData! +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/bundle_analysis/comparison.py b/apps/codecov-api/graphql_api/types/bundle_analysis/comparison.py new file mode 100644 index 0000000000..cfd1987ea5 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/bundle_analysis/comparison.py @@ -0,0 +1,75 @@ +from ariadne import ObjectType, UnionType + +from graphql_api.types.comparison.comparison import ( + FirstPullRequest, + MissingBaseCommit, + MissingBaseReport, + MissingHeadCommit, + MissingHeadReport, +) +from services.bundle_analysis import ( + BundleAnalysisComparison, + BundleComparison, + BundleData, +) + +bundle_analysis_comparison_result_bindable = UnionType("BundleAnalysisComparisonResult") +bundle_analysis_comparison_bindable = ObjectType("BundleAnalysisComparison") +bundle_comparison_bindable = ObjectType("BundleComparison") + + +@bundle_analysis_comparison_result_bindable.type_resolver +def resolve_bundle_analysis_comparison_result_type(obj, *_): + if isinstance(obj, BundleAnalysisComparison): + return "BundleAnalysisComparison" + elif isinstance(obj, MissingHeadCommit): + return "MissingHeadCommit" + elif isinstance(obj, MissingBaseCommit): + return "MissingBaseCommit" + elif isinstance(obj, FirstPullRequest): + return "FirstPullRequest" + elif isinstance(obj, MissingHeadReport): + return "MissingHeadReport" + elif isinstance(obj, MissingBaseReport): + return "MissingBaseReport" + + +@bundle_analysis_comparison_bindable.field("bundles") +def resolve_ba_comparison_bundles( + bundles_analysis_comparison: BundleAnalysisComparison, info +): + return bundles_analysis_comparison.bundles + + +@bundle_analysis_comparison_bindable.field("bundleData") +def resolve_ba_comparison_bundle_data( + bundles_analysis_comparison: BundleAnalysisComparison, info +) -> BundleData: + return BundleData(bundles_analysis_comparison.size_total) + + +@bundle_analysis_comparison_bindable.field("bundleChange") +def resolve_ba_comparison_bundle_delta( + bundles_analysis_comparison: BundleAnalysisComparison, info +) -> BundleData: + return BundleData(bundles_analysis_comparison.size_delta) + + +@bundle_comparison_bindable.field("name") +def resolve_name(bundle_comparison: BundleComparison, info): + return bundle_comparison.bundle_name + + +@bundle_comparison_bindable.field("changeType") +def resolve_change_type(bundle_comparison: BundleComparison, info): + return bundle_comparison.change_type + + +@bundle_comparison_bindable.field("bundleData") +def resolve_bundle_data(bundle_comparison: BundleComparison, info) -> BundleData: + return BundleData(bundle_comparison.size_total) + + +@bundle_comparison_bindable.field("bundleChange") +def resolve_bundle_delta(bundle_comparison: BundleComparison, info) -> BundleData: + return BundleData(bundle_comparison.size_delta) diff --git a/apps/codecov-api/graphql_api/types/bundle_analysis/report.graphql b/apps/codecov-api/graphql_api/types/bundle_analysis/report.graphql new file mode 100644 index 0000000000..4dcfbc8ee7 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/bundle_analysis/report.graphql @@ -0,0 +1,10 @@ +union BundleAnalysisReportResult = + BundleAnalysisReport + | MissingHeadReport + +type BundleAnalysisReport { + bundles: [BundleReport]! + bundleData: BundleData! + bundle(name: String!, filters: BundleAnalysisReportFilters): BundleReport + isCached: Boolean! +} diff --git a/apps/codecov-api/graphql_api/types/bundle_analysis/report.py b/apps/codecov-api/graphql_api/types/bundle_analysis/report.py new file mode 100644 index 0000000000..a57f5cf4cf --- /dev/null +++ b/apps/codecov-api/graphql_api/types/bundle_analysis/report.py @@ -0,0 +1,85 @@ +from typing import Any, List, Optional, Union + +from ariadne import ObjectType, UnionType +from graphql import GraphQLResolveInfo + +from graphql_api.types.comparison.comparison import MissingHeadReport +from graphql_api.types.enums import BundleLoadTypes +from services.bundle_analysis import BundleAnalysisReport, BundleData, BundleReport + +bundle_analysis_report_result_bindable = UnionType("BundleAnalysisReportResult") +bundle_analysis_report_bindable = ObjectType("BundleAnalysisReport") + + +@bundle_analysis_report_result_bindable.type_resolver +def resolve_bundle_analysis_report_result_type( + obj: Union[BundleAnalysisReport, MissingHeadReport], *_: Any +) -> str: + if isinstance(obj, BundleAnalysisReport): + return "BundleAnalysisReport" + elif isinstance(obj, MissingHeadReport): + return "MissingHeadReport" + + +@bundle_analysis_report_bindable.field("bundles") +def resolve_bundles( + bundles_analysis_report: BundleAnalysisReport, info: GraphQLResolveInfo +) -> List[BundleReport]: + return bundles_analysis_report.bundles + + +@bundle_analysis_report_bindable.field("bundle") +def resolve_bundle( + bundles_analysis_report: BundleAnalysisReport, + info: GraphQLResolveInfo, + name: str, + filters: dict[str, list[str]] = {}, +) -> Optional[BundleReport]: + asset_types = None + if filters.get("report_groups"): + asset_types = filters.get("report_groups") + + chunk_entry, chunk_initial = None, None + if filters.get("load_types"): + load_types = filters.get("load_types") + + # Compute chunk entry boolean + if BundleLoadTypes.ENTRY in load_types and ( + BundleLoadTypes.INITIAL in load_types or BundleLoadTypes.LAZY in load_types + ): + chunk_entry = None + elif BundleLoadTypes.ENTRY in load_types: + chunk_entry = True + elif ( + BundleLoadTypes.INITIAL in load_types or BundleLoadTypes.LAZY in load_types + ): + chunk_entry = False + + # Compute chunk initial boolean + if BundleLoadTypes.INITIAL in load_types and BundleLoadTypes.LAZY in load_types: + chunk_initial = None + elif BundleLoadTypes.INITIAL in load_types: + chunk_initial = True + elif BundleLoadTypes.LAZY in load_types: + chunk_initial = False + + return bundles_analysis_report.bundle( + name, + { + "asset_types": asset_types, + "chunk_entry": chunk_entry, + "chunk_initial": chunk_initial, + }, + ) + + +@bundle_analysis_report_bindable.field("bundleData") +def resolve_bundle_data( + bundles_analysis_report: BundleAnalysisReport, info: GraphQLResolveInfo +) -> BundleData: + return BundleData(bundles_analysis_report.size_total) + + +@bundle_analysis_report_bindable.field("isCached") +def resolve_is_cached(bundle_report: BundleReport, info: GraphQLResolveInfo) -> bool: + return bundle_report.is_cached diff --git a/apps/codecov-api/graphql_api/types/commit/__init__.py b/apps/codecov-api/graphql_api/types/commit/__init__.py new file mode 100644 index 0000000000..b4b6f2583d --- /dev/null +++ b/apps/codecov-api/graphql_api/types/commit/__init__.py @@ -0,0 +1,16 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .commit import ( + commit_bindable, + commit_bundle_analysis_bindable, + commit_coverage_analytics_bindable, +) + +commit = ariadne_load_local_graphql(__file__, "commit.graphql") + + +__all__ = [ + "commit_bindable", + "commit_coverage_analytics_bindable", + "commit_bundle_analysis_bindable", +] diff --git a/apps/codecov-api/graphql_api/types/commit/commit.graphql b/apps/codecov-api/graphql_api/types/commit/commit.graphql new file mode 100644 index 0000000000..d60006302b --- /dev/null +++ b/apps/codecov-api/graphql_api/types/commit/commit.graphql @@ -0,0 +1,74 @@ +type Commit { + state: String + message: String + createdAt: DateTime! + commitid: String! + author: Owner + parent: Commit + pullId: Int + branchName: String + yaml: String + yamlState: YamlStates + ciPassed: Boolean + compareWithParent: ComparisonResult + uploads( + first: Int + after: String + last: Int + before: String + ): UploadConnection @cost(complexity: 10, multipliers: ["first", "last"]) + pathContents(path: String, filters: PathContentsFilters): PathContentsResult + deprecatedPathContents(path: String, filters: PathContentsFilters, first: Int, after: String, last: Int, before: String): DeprecatedPathContentsResult + errors(errorType: CommitErrorType!): CommitErrorsConnection! + totalUploads: Int! + bundleStatus: CommitStatus + coverageStatus: CommitStatus + coverageAnalytics: CommitCoverageAnalytics + bundleAnalysis: CommitBundleAnalysis + latestUploadError: LatestUploadError +} + +type LatestUploadError { + errorCode: UploadErrorEnum + errorMessage: String +} + +"fields related to Codecov's Coverage product offering" +type CommitCoverageAnalytics { + components(filters: ComponentsFilters): [Component!]! + coverageFile(path: String!, flags: [String], components: [String]): File + flagNames: [String] + totals: CoverageTotals +} + +"fields related to Codecov's Bundle Analysis product offering" +type CommitBundleAnalysis { + bundleAnalysisCompareWithParent: BundleAnalysisComparisonResult + bundleAnalysisReport: BundleAnalysisReportResult +} + +type CommitErrorsConnection { + edges: [CommitErrorEdge] + totalCount: Int! + pageInfo: PageInfo! +} + +type CommitErrorEdge { + cursor: String! + node: CommitError! +} + +type CommitError { + errorCode: CommitErrorCode! +} + +type UploadConnection { + edges: [UploadEdge]! + totalCount: Int! + pageInfo: PageInfo! +} + +type UploadEdge { + cursor: String! + node: Upload! +} diff --git a/apps/codecov-api/graphql_api/types/commit/commit.py b/apps/codecov-api/graphql_api/types/commit/commit.py new file mode 100644 index 0000000000..0f1505284b --- /dev/null +++ b/apps/codecov-api/graphql_api/types/commit/commit.py @@ -0,0 +1,447 @@ +import logging +from typing import Any, List, Optional, Union + +import sentry_sdk +import shared.reports.api_report_service as report_service +import yaml +from ariadne import ObjectType +from asgiref.sync import sync_to_async +from graphql import GraphQLResolveInfo +from shared.reports.api_report_service import ReadOnlyReport +from shared.reports.filtered import FilteredReportFile +from shared.reports.resources import ReportFile +from shared.reports.types import ReportTotals + +import services.components as components_service +import services.path as path_service +from codecov_auth.models import Owner +from core.models import Commit +from graphql_api.actions.commits import commit_status +from graphql_api.actions.comparison import validate_commit_comparison +from graphql_api.actions.path_contents import sort_path_contents +from graphql_api.dataloader.bundle_analysis import ( + load_bundle_analysis_comparison, + load_bundle_analysis_report, +) +from graphql_api.dataloader.commit import CommitLoader +from graphql_api.dataloader.comparison import ComparisonLoader +from graphql_api.dataloader.owner import OwnerLoader +from graphql_api.helpers.connection import ( + queryset_to_connection, + queryset_to_connection_sync, +) +from graphql_api.helpers.requested_fields import selected_fields +from graphql_api.types.comparison.comparison import ( + MissingBaseCommit, + MissingHeadReport, +) +from graphql_api.types.enums import ( + OrderingDirection, + PathContentDisplayType, +) +from graphql_api.types.errors import MissingCoverage, UnknownPath +from graphql_api.types.errors.errors import UnknownFlags +from reports.models import CommitReport +from services.bundle_analysis import BundleAnalysisComparison, BundleAnalysisReport +from services.comparison import Comparison, ComparisonReport +from services.components import Component +from services.path import Dir, File, ReportPaths +from services.yaml import ( + YamlStates, + get_yaml_state, +) + +commit_bindable = ObjectType("Commit") +commit_coverage_analytics_bindable = ObjectType("CommitCoverageAnalytics") +commit_bundle_analysis_bindable = ObjectType("CommitBundleAnalysis") + +commit_bindable.set_alias("createdAt", "timestamp") +commit_bindable.set_alias("pullId", "pullid") +commit_bindable.set_alias("branchName", "branch") + +log = logging.getLogger(__name__) + + +@commit_bindable.field("author") +def resolve_author(commit: Commit, info: GraphQLResolveInfo) -> Owner | None: + if commit.author_id: + return OwnerLoader.loader(info).load(commit.author_id) + + +@commit_bindable.field("parent") +def resolve_parent(commit: Commit, info: GraphQLResolveInfo) -> Commit | None: + if commit.parent_commit_id: + return CommitLoader.loader(info, commit.repository_id).load( + commit.parent_commit_id + ) + + +@commit_bindable.field("yaml") +async def resolve_yaml(commit: Commit, info: GraphQLResolveInfo) -> dict: + command = info.context["executor"].get_command("commit") + final_yaml = await command.get_final_yaml(commit) + return yaml.dump(final_yaml) + + +@commit_bindable.field("yamlState") +async def resolve_yaml_state(commit: Commit, info: GraphQLResolveInfo) -> YamlStates: + command = info.context["executor"].get_command("commit") + final_yaml = await command.get_final_yaml(commit) + return get_yaml_state(yaml=final_yaml) + + +@commit_bindable.field("uploads") +@sync_to_async +def resolve_list_uploads(commit: Commit, info: GraphQLResolveInfo, **kwargs): + if not commit.commitreport: + return queryset_to_connection_sync([]) + + queryset = commit.commitreport.sessions + + requested_fields = selected_fields(info) + + # the `requested_fields` here are prefixed with `edges.node`, as this is a `Connection` + # and using `uploads { edges { node { ... } } }` is the way this is queried. + if "edges.node.flags" in requested_fields: + queryset = queryset.prefetch_related("flags") + if "edges.node.errors" in requested_fields: + queryset = queryset.prefetch_related("errors") + + if not kwargs: # temp to override kwargs -> return all current uploads + kwargs["first"] = queryset.count() + + return queryset_to_connection_sync( + queryset, ordering=("id",), ordering_direction=OrderingDirection.ASC, **kwargs + ) + + +@commit_bindable.field("compareWithParent") +@sentry_sdk.trace +async def resolve_compare_with_parent( + commit: Commit, info: GraphQLResolveInfo, **kwargs +): + if not commit.parent_commit_id: + return MissingBaseCommit() + + comparison_loader = ComparisonLoader.loader(info, commit.repository_id) + commit_comparison = await comparison_loader.load( + (commit.parent_commit_id, commit.commitid) + ) + + comparison_error = validate_commit_comparison(commit_comparison=commit_comparison) + + if comparison_error: + return comparison_error + + if commit_comparison and commit_comparison.is_processed: + current_owner = info.context["request"].current_owner + parent_commit = await CommitLoader.loader(info, commit.repository_id).load( + commit.parent_commit_id + ) + comparison = Comparison( + user=current_owner, base_commit=parent_commit, head_commit=commit + ) + info.context["comparison"] = comparison + + if commit_comparison: + return ComparisonReport(commit_comparison) + + +@sentry_sdk.trace +def get_sorted_path_contents( + current_owner: Owner, + commit: Commit, + path: str | None = None, + filters: dict | None = None, +) -> ( + list[File | Dir] | MissingHeadReport | MissingCoverage | UnknownFlags | UnknownPath +): + # TODO: Might need to add reports here filtered by flags in the future + report = report_service.build_report_from_commit( + commit, report_class=ReadOnlyReport + ) + if not report: + return MissingHeadReport() + + if filters is None: + filters = {} + search_value = filters.get("search_value") + display_type = filters.get("display_type") + + flags_filter = filters.get("flags", []) + component_filter = filters.get("components", []) + + component_paths = [] + component_flags = [] + + report_flags = report.get_flag_names() + + if component_filter: + all_components = components_service.commit_components(commit, current_owner) + filtered_components = components_service.filter_components_by_name_or_id( + all_components, component_filter + ) + + if not filtered_components: + return MissingCoverage( + f"missing coverage for report with components: {component_filter}" + ) + + for component in filtered_components: + component_paths.extend(component.paths) + if report_flags: + component_flags.extend(component.get_matching_flags(report_flags)) + + if component_flags: + if flags_filter: + flags_filter = list(set(flags_filter) & set(component_flags)) + else: + flags_filter = component_flags + + if flags_filter and not report_flags: + return UnknownFlags(f"No coverage with chosen flags: {flags_filter}") + + report_paths = ReportPaths( + report=report, + path=path, + search_term=search_value, + filter_flags=flags_filter, + filter_paths=component_paths, + ) + + if len(report_paths.paths) == 0: + # we do not know about this path + + if path_service.provider_path_exists(path, commit, current_owner) is False: + # file doesn't exist + return UnknownPath(f"path does not exist: {path}") + + # we're just missing coverage for the file + return MissingCoverage(f"missing coverage for path: {path}") + + items: list[File | Dir] + if search_value or display_type == PathContentDisplayType.LIST: + items = report_paths.full_filelist() + else: + items = report_paths.single_directory() + return sort_path_contents(items, filters) + + +@commit_bindable.field("pathContents") +@sync_to_async +def resolve_path_contents( + commit: Commit, + info: GraphQLResolveInfo, + path: str | None = None, + filters: dict | None = None, +) -> Any: + """ + The file directory tree is a list of all the files and directories + extracted from the commit report of the latest, head commit. + The is resolver results in a list that represent the tree with files + and nested directories. + """ + current_owner = info.context["request"].current_owner + + contents = get_sorted_path_contents(current_owner, commit, path, filters) + if isinstance(contents, list): + return {"results": contents} + return contents + + +@commit_bindable.field("deprecatedPathContents") +@sync_to_async +def resolve_deprecated_path_contents( + commit: Commit, + info: GraphQLResolveInfo, + path: str | None = None, + filters: dict | None = None, + first: Any = None, + after: Any = None, + last: Any = None, + before: Any = None, +) -> Any: + """ + The file directory tree is a list of all the files and directories + extracted from the commit report of the latest, head commit. + The is resolver results in a list that represent the tree with files + and nested directories. + """ + current_owner = info.context["request"].current_owner + + contents = get_sorted_path_contents(current_owner, commit, path, filters) + if not isinstance(contents, list): + return contents + + return queryset_to_connection_sync( + contents, + ordering_direction=OrderingDirection.ASC, + first=first, + last=last, + before=before, + after=after, + ) + + +@commit_bindable.field("errors") +async def resolve_errors(commit, info, error_type): + command = info.context["executor"].get_command("commit") + queryset = await command.get_commit_errors(commit, error_type=error_type) + return await queryset_to_connection( + queryset, + ordering=("updated_at",), + ordering_direction=OrderingDirection.ASC, + ) + + +@commit_bindable.field("totalUploads") +async def resolve_total_uploads(commit, info): + command = info.context["executor"].get_command("commit") + return await command.get_uploads_number(commit) + + +@commit_bindable.field("bundleStatus") +@sync_to_async +@sentry_sdk.trace +def resolve_bundle_status(commit: Commit, info: GraphQLResolveInfo) -> str | None: + return commit_status(info, commit, CommitReport.ReportType.BUNDLE_ANALYSIS) + + +@commit_bindable.field("coverageStatus") +@sync_to_async +@sentry_sdk.trace +def resolve_coverage_status(commit: Commit, info: GraphQLResolveInfo) -> str | None: + return commit_status(info, commit, CommitReport.ReportType.COVERAGE) + + +@commit_bindable.field("coverageAnalytics") +def resolve_commit_coverage(commit, info): + return commit + + +@commit_bindable.field("bundleAnalysis") +def resolve_commit_bundle_analysis(commit, info): + return commit + + +### Commit Coverage Bindable ### + + +@commit_coverage_analytics_bindable.field("totals") +@sentry_sdk.trace +def resolve_coverage_totals( + commit: Commit, info: GraphQLResolveInfo +) -> Optional[ReportTotals]: + command = info.context["executor"].get_command("commit") + return command.fetch_totals(commit) + + +@commit_coverage_analytics_bindable.field("flagNames") +@sync_to_async +@sentry_sdk.trace +def resolve_coverage_flags(commit: Commit, info: GraphQLResolveInfo) -> list[str]: + return commit.full_report.get_flag_names() if commit.full_report else [] + + +@commit_coverage_analytics_bindable.field("coverageFile") +@sync_to_async +@sentry_sdk.trace +def resolve_coverage_file(commit, info, path, flags=None, components=None): + fallback_file, paths = None, [] + if components: + all_components = components_service.commit_components( + commit, info.context["request"].current_owner + ) + filtered_components = components_service.filter_components_by_name_or_id( + all_components, components + ) + for fc in filtered_components: + paths.extend(fc.paths) + fallback_file = FilteredReportFile(ReportFile(path), []) + + commit_report = commit.full_report.filter(flags=flags, paths=paths) + file_report = commit_report.get(path) or fallback_file + + return { + "commit_report": commit_report, + "file_report": file_report, + "commit": commit, + "path": path, + "flags": flags, + "components": components, + } + + +@commit_coverage_analytics_bindable.field("components") +@sync_to_async +@sentry_sdk.trace +def resolve_coverage_components(commit: Commit, info, filters=None) -> List[Component]: + info.context["component_commit"] = commit + current_owner = info.context["request"].current_owner + all_components = components_service.commit_components(commit, current_owner) + + if filters and filters.get("components"): + return components_service.filter_components_by_name_or_id( + all_components, filters["components"] + ) + + return all_components + + +### Commit Bundle Analysis Bindable ### + + +@commit_bundle_analysis_bindable.field("bundleAnalysisCompareWithParent") +@sentry_sdk.trace +async def resolve_commit_bundle_analysis_compare_with_parent( + commit: Commit, info: GraphQLResolveInfo +) -> Union[BundleAnalysisComparison, Any]: + if not commit.parent_commit_id: + return MissingBaseCommit() + base_commit = await CommitLoader.loader(info, commit.repository_id).load( + commit.parent_commit_id + ) + + bundle_analysis_comparison = await sync_to_async(load_bundle_analysis_comparison)( + base_commit, commit + ) + + # Store the created SQLite DB path in info.context + # when the request is fully handled, have the file deleted + if isinstance(bundle_analysis_comparison, BundleAnalysisComparison): + info.context[ + "request" + ].bundle_analysis_base_report_db_path = ( + bundle_analysis_comparison.comparison.base_report.db_path + ) + info.context[ + "request" + ].bundle_analysis_head_report_db_path = ( + bundle_analysis_comparison.comparison.head_report.db_path + ) + + return bundle_analysis_comparison + + +@commit_bundle_analysis_bindable.field("bundleAnalysisReport") +@sync_to_async +@sentry_sdk.trace +def resolve_commit_bundle_analysis_report(commit: Commit, info) -> BundleAnalysisReport: + bundle_analysis_report = load_bundle_analysis_report(commit) + + # Store the created SQLite DB path in info.context + # when the request is fully handled, have the file deleted + if isinstance(bundle_analysis_report, BundleAnalysisReport): + info.context[ + "request" + ].bundle_analysis_head_report_db_path = bundle_analysis_report.report.db_path + + info.context["commit"] = commit + + return bundle_analysis_report + + +@commit_bindable.field("latestUploadError") +async def resolve_latest_upload_error(commit, info): + command = info.context["executor"].get_command("commit") + return await command.get_latest_upload_error(commit) diff --git a/apps/codecov-api/graphql_api/types/comparison/__init__.py b/apps/codecov-api/graphql_api/types/comparison/__init__.py new file mode 100644 index 0000000000..6344af7cbd --- /dev/null +++ b/apps/codecov-api/graphql_api/types/comparison/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .comparison import comparison_bindable, comparison_result_bindable + +comparison = ariadne_load_local_graphql(__file__, "comparison.graphql") + + +__all__ = ["comparison_bindable", "comparison_result_bindable"] diff --git a/apps/codecov-api/graphql_api/types/comparison/comparison.graphql b/apps/codecov-api/graphql_api/types/comparison/comparison.graphql new file mode 100644 index 0000000000..8ae51b0c22 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/comparison/comparison.graphql @@ -0,0 +1,50 @@ +type Comparison { + state: String! + impactedFile(path: String!): ImpactedFile + impactedFiles(filters: ImpactedFilesFilters): ImpactedFilesResult! + impactedFilesCount: Int! + indirectChangedFilesCount: Int! + patchTotals: CoverageTotals + directChangedFilesCount: Int! + baseTotals: CoverageTotals + headTotals: CoverageTotals + changeCoverage: Float + flagComparisons(filters: FlagComparisonFilters): [FlagComparison] + componentComparisons(filters: ComponentsFilters): [ComponentComparison!] + hasDifferentNumberOfHeadAndBaseReports: Boolean! + flagComparisonsCount: Int! + componentComparisonsCount: Int! +} + +type MissingBaseCommit implements ResolverError { + message: String! +} + +type MissingHeadCommit implements ResolverError { + message: String! +} + +type MissingComparison implements ResolverError { + message: String! +} + +type MissingBaseReport implements ResolverError { + message: String! +} + +type MissingHeadReport implements ResolverError { + message: String! +} + +type FirstPullRequest { + message: String! +} + +union ComparisonResult = + Comparison + | FirstPullRequest + | MissingBaseCommit + | MissingHeadCommit + | MissingComparison + | MissingBaseReport + | MissingHeadReport diff --git a/apps/codecov-api/graphql_api/types/comparison/comparison.py b/apps/codecov-api/graphql_api/types/comparison/comparison.py new file mode 100644 index 0000000000..19e6651fd3 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/comparison/comparison.py @@ -0,0 +1,277 @@ +from asyncio import gather +from typing import List, Optional + +import sentry_sdk +from ariadne import ObjectType, UnionType +from asgiref.sync import sync_to_async +from graphql.type.definition import GraphQLResolveInfo + +import services.components as components_service +from compare.commands.compare.compare import CompareCommands +from compare.models import ComponentComparison, FlagComparison +from graphql_api.actions.flags import get_flag_comparisons +from graphql_api.dataloader.commit import CommitLoader +from graphql_api.types.errors import ( + MissingBaseCommit, + MissingBaseReport, + MissingComparison, + MissingHeadCommit, + MissingHeadReport, +) +from graphql_api.types.errors.errors import UnknownFlags +from reports.models import ReportLevelTotals +from services.comparison import ( + Comparison, + ComparisonReport, + FirstPullRequest, + ImpactedFile, +) + +comparison_bindable = ObjectType("Comparison") + + +@comparison_bindable.field("state") +def resolve_state(comparison: ComparisonReport, info: GraphQLResolveInfo) -> str: + return comparison.commit_comparison.state + + +@comparison_bindable.field("impactedFiles") +@sync_to_async +def resolve_impacted_files( + comparison_report: ComparisonReport, info: GraphQLResolveInfo, filters=None +): + command: CompareCommands = info.context["executor"].get_command("compare") + comparison: Comparison = info.context.get("comparison", None) + + if filters and comparison: + flags = filters.get("flags", []) + if flags and set(flags).isdisjoint( + set(comparison.head_report.get_flag_names()) + ): + return UnknownFlags() + + return { + "results": command.fetch_impacted_files(comparison_report, comparison, filters) + } + + +@comparison_bindable.field("impactedFilesCount") +@sync_to_async +def resolve_impacted_files_count( + comparison: ComparisonReport, info: GraphQLResolveInfo +) -> int: + return len(comparison.impacted_files) + + +@comparison_bindable.field("directChangedFilesCount") +@sync_to_async +def resolve_direct_changed_files_count( + comparison: ComparisonReport, info: GraphQLResolveInfo +) -> int: + return len(comparison.impacted_files_with_direct_changes) + + +@comparison_bindable.field("indirectChangedFilesCount") +@sync_to_async +def resolve_indirect_changed_files_count( + comparison: ComparisonReport, info: GraphQLResolveInfo +) -> int: + return len(comparison.impacted_files_with_unintended_changes) + + +@comparison_bindable.field("impactedFile") +@sync_to_async +def resolve_impacted_file( + comparison: ComparisonReport, info: GraphQLResolveInfo, path +) -> ImpactedFile: + return comparison.impacted_file(path) + + +# TODO: rename `changeCoverage` +@comparison_bindable.field("changeCoverage") +async def resolve_change_coverage( + comparison: ComparisonReport, info: GraphQLResolveInfo +) -> Optional[float]: + repository_id = comparison.commit_comparison.compare_commit.repository_id + loader = CommitLoader.loader(info, repository_id) + + # the loader prefetches everything we need to get the totals + base_commit, head_commit = await gather( + loader.load(comparison.commit_comparison.base_commit.commitid), + loader.load(comparison.commit_comparison.compare_commit.commitid), + ) + + base_totals = None + head_totals = None + if ( + base_commit + and base_commit.commitreport + and hasattr(base_commit.commitreport, "reportleveltotals") + ): + base_totals = base_commit.commitreport.reportleveltotals + if ( + head_commit + and head_commit.commitreport + and hasattr(head_commit.commitreport, "reportleveltotals") + ): + head_totals = head_commit.commitreport.reportleveltotals + + if base_totals and head_totals: + return head_totals.coverage - base_totals.coverage + + +@comparison_bindable.field("baseTotals") +async def resolve_base_totals( + comparison: ComparisonReport, info: GraphQLResolveInfo +) -> Optional[ReportLevelTotals]: + repository_id = comparison.commit_comparison.base_commit.repository_id + loader = CommitLoader.loader(info, repository_id) + + # the loader prefetches everything we need to get the totals + base_commit = await loader.load(comparison.commit_comparison.base_commit.commitid) + if ( + base_commit + and base_commit.commitreport + and hasattr(base_commit.commitreport, "reportleveltotals") + ): + return base_commit.commitreport.reportleveltotals + + +@comparison_bindable.field("headTotals") +async def resolve_head_totals( + comparison: ComparisonReport, info: GraphQLResolveInfo +) -> Optional[ReportLevelTotals]: + repository_id = comparison.commit_comparison.compare_commit.repository_id + loader = CommitLoader.loader(info, repository_id) + + # the loader prefetches everything we need to get the totals + head_commit = await loader.load( + comparison.commit_comparison.compare_commit.commitid + ) + if ( + head_commit + and head_commit.commitreport + and hasattr(head_commit.commitreport, "reportleveltotals") + ): + return head_commit.commitreport.reportleveltotals + + +@comparison_bindable.field("patchTotals") +def resolve_patch_totals( + comparison: ComparisonReport, info: GraphQLResolveInfo +) -> dict | None: + totals = comparison.commit_comparison.patch_totals + if not totals: + return None + + coverage = totals["coverage"] + if coverage is not None: + # we always return `coverage` as a percentage but it's stored + # in the database as 0 <= value <= 1 + coverage *= 100 + + return {**totals, "coverage": coverage} + + +@comparison_bindable.field("flagComparisons") +@sync_to_async +@sentry_sdk.trace +def resolve_flag_comparisons( + comparison: ComparisonReport, info: GraphQLResolveInfo, filters=None +) -> List[FlagComparison]: + all_flags = get_flag_comparisons(comparison.commit_comparison) + + if filters and filters.get("term"): + filtered_flags = [ + flag + for flag in all_flags + if filters["term"] in flag.repositoryflag.flag_name + ] + return filtered_flags + + return list(all_flags) + + +@comparison_bindable.field("componentComparisons") +@sync_to_async +@sentry_sdk.trace +def resolve_component_comparisons( + comparison_report: ComparisonReport, info: GraphQLResolveInfo, filters=None +) -> List[ComponentComparison]: + current_owner = info.context["request"].current_owner + head_commit = comparison_report.commit_comparison.compare_commit + components = components_service.commit_components(head_commit, current_owner) + list_components = comparison_report.commit_comparison.component_comparisons.all() + + if filters and filters.get("components"): + components = components_service.filter_components_by_name_or_id( + components, filters["components"] + ) + + list_components = list_components.filter( + component_id__in=[component.component_id for component in components] + ) + + # store for child resolvers (needed to get the component name, for example) + info.context["components"] = { + component.component_id: component for component in components + } + + return list(list_components) + + +@comparison_bindable.field("componentComparisonsCount") +@sync_to_async +def resolve_component_comparisons_count( + comparison_report: ComparisonReport, info: GraphQLResolveInfo +) -> int: + return comparison_report.commit_comparison.component_comparisons.count() + + +@comparison_bindable.field("flagComparisonsCount") +@sync_to_async +def resolve_flag_comparisons_count( + comparison: ComparisonReport, info: GraphQLResolveInfo +) -> int: + """ + Resolver to return if the head and base of a pull request have + different number of reports on the head and base. This implementation + excludes commits that have carried forward sessions. + """ + return get_flag_comparisons(comparison.commit_comparison).count() + + +@comparison_bindable.field("hasDifferentNumberOfHeadAndBaseReports") +@sync_to_async +@sentry_sdk.trace +def resolve_has_different_number_of_head_and_base_reports( + comparison: ComparisonReport, + info: GraphQLResolveInfo, + **kwargs, # type: ignore +) -> bool: + # TODO: can we remove the need for `info.context["comparison"]` here? + if "comparison" not in info.context: + return False + comparison: Comparison = info.context["comparison"] + return comparison.has_different_number_of_head_and_base_sessions + + +comparison_result_bindable = UnionType("ComparisonResult") + + +@comparison_result_bindable.type_resolver +def resolve_comparison_result_type(obj, *_): + if isinstance(obj, ComparisonReport): + return "Comparison" + elif isinstance(obj, MissingBaseCommit): + return "MissingBaseCommit" + elif isinstance(obj, MissingHeadCommit): + return "MissingHeadCommit" + elif isinstance(obj, MissingComparison): + return "MissingComparison" + elif isinstance(obj, MissingBaseReport): + return "MissingBaseReport" + elif isinstance(obj, MissingHeadReport): + return "MissingHeadReport" + elif isinstance(obj, FirstPullRequest): + return "FirstPullRequest" diff --git a/apps/codecov-api/graphql_api/types/component/__init__.py b/apps/codecov-api/graphql_api/types/component/__init__.py new file mode 100644 index 0000000000..bd81cc7112 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/component/__init__.py @@ -0,0 +1,7 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .component import component_bindable + +component = ariadne_load_local_graphql(__file__, "component.graphql") + +__all__ = ["component_bindable"] diff --git a/apps/codecov-api/graphql_api/types/component/component.graphql b/apps/codecov-api/graphql_api/types/component/component.graphql new file mode 100644 index 0000000000..fa2398d016 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/component/component.graphql @@ -0,0 +1,19 @@ +type Component { + id: String! + name: String! + totals: CoverageTotals +} + +type ComponentMeasurements { + componentId: String! + name: String! + percentCovered: Float + percentChange: Float + measurements: [Measurement!]! + lastUploaded: DateTime +} + +type ComponentsYaml { + id: String! + name: String! +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/component/component.py b/apps/codecov-api/graphql_api/types/component/component.py new file mode 100644 index 0000000000..862751d68b --- /dev/null +++ b/apps/codecov-api/graphql_api/types/component/component.py @@ -0,0 +1,29 @@ +from typing import Optional + +from ariadne import ObjectType +from asgiref.sync import sync_to_async +from shared.reports.types import ReportTotals + +from core.models import Commit +from services.components import Component, component_filtered_report + +component_bindable = ObjectType("Component") + + +@component_bindable.field("id") +def resolve_id(component: Component, info) -> str: + return component.component_id + + +@component_bindable.field("name") +def resolve_name(component: Component, info) -> str: + return component.get_display_name() + + +@component_bindable.field("totals") +@sync_to_async +def resolve_totals(component: Component, info) -> Optional[ReportTotals]: + commit: Commit = info.context["component_commit"] + report = commit.full_report + filtered_report = component_filtered_report(report, [component]) + return filtered_report.totals diff --git a/apps/codecov-api/graphql_api/types/component_comparison/__init__.py b/apps/codecov-api/graphql_api/types/component_comparison/__init__.py new file mode 100644 index 0000000000..43598a549f --- /dev/null +++ b/apps/codecov-api/graphql_api/types/component_comparison/__init__.py @@ -0,0 +1,10 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .component_comparison import component_comparison_bindable + +component_comparison = ariadne_load_local_graphql( + __file__, "component_comparison.graphql" +) + + +__all__ = ["component_comparison_bindable"] diff --git a/apps/codecov-api/graphql_api/types/component_comparison/component_comparison.graphql b/apps/codecov-api/graphql_api/types/component_comparison/component_comparison.graphql new file mode 100644 index 0000000000..2a845288ee --- /dev/null +++ b/apps/codecov-api/graphql_api/types/component_comparison/component_comparison.graphql @@ -0,0 +1,7 @@ +type ComponentComparison { + id: String! + name: String! + baseTotals: CoverageTotals + headTotals: CoverageTotals + patchTotals: CoverageTotals +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/component_comparison/component_comparison.py b/apps/codecov-api/graphql_api/types/component_comparison/component_comparison.py new file mode 100644 index 0000000000..5271bda950 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/component_comparison/component_comparison.py @@ -0,0 +1,47 @@ +from ariadne import ObjectType +from asgiref.sync import sync_to_async +from shared.reports.types import ReportTotals + +from compare.models import ComponentComparison +from services.components import Component + +component_comparison_bindable = ObjectType("ComponentComparison") + + +@component_comparison_bindable.field("id") +def resolve_id(component_comparison: ComponentComparison, info) -> str: + return component_comparison.component_id + + +@component_comparison_bindable.field("name") +def resolve_name(component_comparison: ComponentComparison, info) -> str: + components: dict[str, Component] = info.context["components"] + component = components.get(component_comparison.component_id) + if component: + return component.get_display_name() + else: + # not sure when we would ever get here + # (yaml components out-of-sync with database for some reason) + return component_comparison.component_id + + +@component_comparison_bindable.field("baseTotals") +def resolve_base_totals( + component_comparison: ComponentComparison, info +) -> ReportTotals: + return component_comparison.base_totals + + +@component_comparison_bindable.field("headTotals") +def resolve_head_totals( + component_comparison: ComponentComparison, info +) -> ReportTotals: + return component_comparison.head_totals + + +@component_comparison_bindable.field("patchTotals") +@sync_to_async +def resolve_patch_totals( + component_comparison: ComponentComparison, info +) -> ReportTotals: + return component_comparison.patch_totals diff --git a/apps/codecov-api/graphql_api/types/config/__init__.py b/apps/codecov-api/graphql_api/types/config/__init__.py new file mode 100644 index 0000000000..9efff1aca2 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/config/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .config import config_bindable + +config = ariadne_load_local_graphql(__file__, "config.graphql") + + +__all__ = ["config_bindable"] diff --git a/apps/codecov-api/graphql_api/types/config/config.graphql b/apps/codecov-api/graphql_api/types/config/config.graphql new file mode 100644 index 0000000000..e4d393f33b --- /dev/null +++ b/apps/codecov-api/graphql_api/types/config/config.graphql @@ -0,0 +1,13 @@ +type Config { + loginProviders: [LoginProvider!]! + planAutoActivate: Boolean + seatsUsed: Int + seatsLimit: Int + isTimescaleEnabled: Boolean! + hasAdmins: Boolean + githubEnterpriseURL: String + gitlabEnterpriseURL: String + bitbucketServerURL: String + selfHostedLicense: SelfHostedLicense + syncProviders: [SyncProvider!]! +} diff --git a/apps/codecov-api/graphql_api/types/config/config.py b/apps/codecov-api/graphql_api/types/config/config.py new file mode 100644 index 0000000000..32d97a4df8 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/config/config.py @@ -0,0 +1,149 @@ +from typing import List, Optional + +from ariadne import ObjectType +from asgiref.sync import sync_to_async +from django.conf import settings +from graphql.type.definition import GraphQLResolveInfo + +import services.self_hosted as self_hosted +from graphql_api.types.enums.enums import LoginProvider, SyncProvider +from utils import strtobool + +config_bindable = ObjectType("Config") + + +@config_bindable.field("loginProviders") +def resolve_login_providers(_, info) -> List[str]: + login_providers = [] + + if not settings.DISABLE_GIT_BASED_LOGIN: + if settings.GITHUB_CLIENT_ID: + login_providers.append(LoginProvider("github")) + + if settings.GITHUB_ENTERPRISE_CLIENT_ID: + login_providers.append(LoginProvider("github_enterprise")) + + if settings.GITLAB_CLIENT_ID: + login_providers.append(LoginProvider("gitlab")) + + if settings.GITLAB_ENTERPRISE_CLIENT_ID: + login_providers.append(LoginProvider("gitlab_enterprise")) + + if settings.BITBUCKET_CLIENT_ID: + login_providers.append(LoginProvider("bitbucket")) + + if settings.BITBUCKET_SERVER_CLIENT_ID: + login_providers.append(LoginProvider("bitbucket_server")) + + if settings.OKTA_OAUTH_CLIENT_ID: + login_providers.append(LoginProvider("okta")) + + return login_providers + + +@config_bindable.field("syncProviders") +def resolve_sync_providers(_, info) -> List[str]: + sync_providers = [] + + if settings.GITHUB_CLIENT_ID: + sync_providers.append(SyncProvider("github")) + + if settings.GITHUB_ENTERPRISE_CLIENT_ID: + sync_providers.append(SyncProvider("github_enterprise")) + + if settings.GITLAB_CLIENT_ID: + sync_providers.append(SyncProvider("gitlab")) + + if settings.GITLAB_ENTERPRISE_CLIENT_ID: + sync_providers.append(SyncProvider("gitlab_enterprise")) + + if settings.BITBUCKET_CLIENT_ID: + sync_providers.append(SyncProvider("bitbucket")) + + if settings.BITBUCKET_SERVER_CLIENT_ID: + sync_providers.append(SyncProvider("bitbucket_server")) + + return sync_providers + + +@config_bindable.field("planAutoActivate") +@sync_to_async +def resolve_plan_auto_activate(_, info: GraphQLResolveInfo) -> Optional[bool]: + if not settings.IS_ENTERPRISE: + return None + + return self_hosted.is_autoactivation_enabled() + + +@config_bindable.field("seatsUsed") +@sync_to_async +def resolve_seats_used(_, info): + if not settings.IS_ENTERPRISE: + return None + + return self_hosted.activated_owners().count() + + +@config_bindable.field("seatsLimit") +@sync_to_async +def resolve_seats_limit(_, info): + if not settings.IS_ENTERPRISE: + return None + + return self_hosted.license_seats() + + +@config_bindable.field("isTimescaleEnabled") +@sync_to_async +def resolve_is_timescale_enabled(_, info): + if isinstance(settings.TIMESERIES_ENABLED, str): + return bool(strtobool(settings.TIMESERIES_ENABLED)) + + return settings.TIMESERIES_ENABLED + + +@config_bindable.field("selfHostedLicense") +def resolve_self_hosted_license(_, info): + if not settings.IS_ENTERPRISE: + return None + license = self_hosted.get_current_license() + + if not license.is_valid: + return None + + return license + + +@config_bindable.field("hasAdmins") +def resolve_has_admins(_, info): + if not settings.IS_ENTERPRISE: + return None + + return len(settings.ADMINS_LIST) != 0 + + +@config_bindable.field("githubEnterpriseURL") +def resolve_github_enterprise_url(_, info): + if not settings.IS_ENTERPRISE: + return None + + if settings.GITHUB_ENTERPRISE_CLIENT_ID: + return settings.GITHUB_ENTERPRISE_URL + + +@config_bindable.field("gitlabEnterpriseURL") +def resolve_gitlab_enterprise_url(_, info): + if not settings.IS_ENTERPRISE: + return None + + if settings.GITLAB_ENTERPRISE_CLIENT_ID: + return settings.GITLAB_ENTERPRISE_URL + + +@config_bindable.field("bitbucketServerURL") +def resolve_bitbucket_server_url(_, info): + if not settings.IS_ENTERPRISE: + return None + + if settings.BITBUCKET_SERVER_CLIENT_ID: + return settings.BITBUCKET_SERVER_URL diff --git a/apps/codecov-api/graphql_api/types/coverage_analytics/__init__.py b/apps/codecov-api/graphql_api/types/coverage_analytics/__init__.py new file mode 100644 index 0000000000..0664a6415b --- /dev/null +++ b/apps/codecov-api/graphql_api/types/coverage_analytics/__init__.py @@ -0,0 +1,13 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .coverage_analytics import ( + coverage_analytics_bindable, + coverage_analytics_result_bindable, +) + +coverage_analytics = ariadne_load_local_graphql(__file__, "coverage_analytics.graphql") + +__all__ = [ + "coverage_analytics_bindable", + "coverage_analytics_result_bindable", +] diff --git a/apps/codecov-api/graphql_api/types/coverage_analytics/coverage_analytics.graphql b/apps/codecov-api/graphql_api/types/coverage_analytics/coverage_analytics.graphql new file mode 100644 index 0000000000..1f0cff6268 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/coverage_analytics/coverage_analytics.graphql @@ -0,0 +1,62 @@ +""" +CoverageAnalytics is information related to a repo's test coverage +""" +type CoverageAnalytics { + "Hits is the number of hits in the latest commit's coverage report" + hits: Int # formerly repository.hits + "Misses is the number of misses in the latest commit's coverage report" + misses: Int # formerly repository.misses + "Lines is the number of lines in the latest commit's coverage report" + lines: Int # formerly repository.lines + "Commit sha is the sha hash of the commit in the latest commit's coverage report" + commitSha: String # formerly repository.coverageSha + "PercentCovered is percent of lines covered (e.g., 87.25)" + percentCovered: Float # formerly repository.coverage + + "Measurements are points in the time series for coverage over time" + measurements( + interval: MeasurementInterval! + after: DateTime + before: DateTime + branch: String + ): [Measurement!]! # formerly repository.measurements + + ## Flags ## + "Flags are the measurements by flag for this repository" + flags( + filters: FlagSetFilters + orderingDirection: OrderingDirection + first: Int + after: String + last: Int + before: String + ): FlagConnection! @cost(complexity: 3, multipliers: ["first", "last"]) + "FlagsCount are how many flags for the given repo" + flagsCount: Int! + "FlagsMeasurementsActive is whether the flag measurements are currently getting populated" + flagsMeasurementsActive: Boolean! + "FlagsMeasurementsBackfilled is whether the flag data has been backfilled" + flagsMeasurementsBackfilled: Boolean! + + ## Components ## + "Components are the measurements by component for this repository" + components( + interval: MeasurementInterval! + before: DateTime! + after: DateTime! + branch: String + filters: ComponentMeasurementsSetFilters + orderingDirection: OrderingDirection + ): [ComponentMeasurements!]! + "ComponentsCount is how many components are configured for the given repo" + componentsCount: Int! + "ComponentsMeasurementsActive is whether the components measurements are currently getting populated" + componentsMeasurementsActive: Boolean! + "ComponentsMeasurementsBackfilled is whether the components data has been backfilled" + componentsMeasurementsBackfilled: Boolean! + "ComponentsYaml is the information related to the configuration yaml for Components" + componentsYaml(termId: String): [ComponentsYaml]! +} + +"CoverageAnalyticsResult is CoverageAnalytics or potential error(s)" +union CoverageAnalyticsResult = CoverageAnalytics | NotFoundError diff --git a/apps/codecov-api/graphql_api/types/coverage_analytics/coverage_analytics.py b/apps/codecov-api/graphql_api/types/coverage_analytics/coverage_analytics.py new file mode 100644 index 0000000000..9285f52ef5 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/coverage_analytics/coverage_analytics.py @@ -0,0 +1,342 @@ +import logging +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Iterable, List, Mapping, Optional, Union + +import sentry_sdk +from ariadne import ObjectType, UnionType +from asgiref.sync import sync_to_async +from django.conf import settings +from django.forms.utils import from_current_timezone +from graphql.type.definition import GraphQLResolveInfo +from shared.yaml import UserYaml + +import timeseries.helpers as timeseries_helpers +from core.models import Repository +from graphql_api.actions.components import ( + component_measurements, + component_measurements_last_uploaded, +) +from graphql_api.actions.flags import flag_measurements, flags_for_repo +from graphql_api.helpers.connection import ( + queryset_to_connection_sync, +) +from graphql_api.helpers.lookahead import lookahead +from graphql_api.types.enums import OrderingDirection +from graphql_api.types.errors.errors import NotFoundError +from services.components import ComponentMeasurements +from timeseries.helpers import fill_sparse_measurements +from timeseries.models import Dataset, Interval, MeasurementName, MeasurementSummary + +log = logging.getLogger(__name__) + +# Bindings for GraphQL types +coverage_analytics_bindable: ObjectType = ObjectType("CoverageAnalytics") +coverage_analytics_result_bindable: UnionType = UnionType("CoverageAnalyticsResult") + + +# CoverageAnalyticsProps is information passed from parent resolver (repository) +# to the coverage analytics resolver +@dataclass +class CoverageAnalyticsProps: + repository: Repository + + +@coverage_analytics_result_bindable.type_resolver +def resolve_coverage_analytics_result_type( + obj: Union[CoverageAnalyticsProps, NotFoundError], *_: Any +) -> Optional[str]: + if isinstance(obj, CoverageAnalyticsProps): + return "CoverageAnalyticsProps" + elif isinstance(obj, NotFoundError): + return "NotFoundError" + return None + + +@coverage_analytics_bindable.field("percentCovered") +def resolve_percent_covered( + parent: CoverageAnalyticsProps, info: GraphQLResolveInfo +) -> Optional[float]: + return parent.repository.recent_coverage if parent else None + + +@coverage_analytics_bindable.field("commitSha") +def resolve_commit_sha( + parent: CoverageAnalyticsProps, info: GraphQLResolveInfo +) -> Optional[str]: + return parent.repository.coverage_sha if parent else None + + +@coverage_analytics_bindable.field("hits") +def resolve_hits( + parent: CoverageAnalyticsProps, info: GraphQLResolveInfo +) -> Optional[int]: + return parent.repository.hits if parent else None + + +@coverage_analytics_bindable.field("misses") +def resolve_misses( + parent: CoverageAnalyticsProps, info: GraphQLResolveInfo +) -> Optional[int]: + return parent.repository.misses if parent else None + + +@coverage_analytics_bindable.field("lines") +def resolve_lines( + parent: CoverageAnalyticsProps, info: GraphQLResolveInfo +) -> Optional[int]: + return parent.repository.lines if parent else None + + +@coverage_analytics_bindable.field("measurements") +async def resolve_measurements( + parent: CoverageAnalyticsProps, + info: GraphQLResolveInfo, + interval: Interval, + before: Optional[datetime] = None, + after: Optional[datetime] = None, + branch: Optional[str] = None, +) -> Iterable[MeasurementSummary]: + coverage_data = await sync_to_async( + timeseries_helpers.repository_coverage_measurements_with_fallback + )( + parent.repository, + interval, + start_date=after, + end_date=before, + branch=branch, + ) + + measurements = await sync_to_async(fill_sparse_measurements)( + coverage_data, + interval, + start_date=after, + end_date=before, + ) + + return measurements + + +@coverage_analytics_bindable.field("components") +@sync_to_async +@sentry_sdk.trace +def resolve_components_measurements( + parent: CoverageAnalyticsProps, + info: GraphQLResolveInfo, + interval: Interval, + before: datetime, + after: datetime, + branch: Optional[str] = None, + filters: Optional[Mapping] = None, + ordering_direction: Optional[OrderingDirection] = OrderingDirection.ASC, +): + components = UserYaml.get_final_yaml( + owner_yaml=parent.repository.author.yaml, + repo_yaml=parent.repository.yaml, + ownerid=parent.repository.author.ownerid, + ).get_components() + + if not settings.TIMESERIES_ENABLED or not components: + return [] + + if filters and "components" in filters: + components = [c for c in components if c.component_id in filters["components"]] + + component_ids = [c.component_id for c in components] + all_measurements = component_measurements( + parent.repository, component_ids, interval, after, before, branch + ) + + last_measurements = component_measurements_last_uploaded( + owner_id=parent.repository.author.ownerid, + repo_id=parent.repository.repoid, + measurable_ids=component_ids, + branch=branch, + ) + last_measurements_mapping = { + row["measurable_id"]: row["last_uploaded"] for row in last_measurements + } + + components_mapping = { + component.component_id: component.name for component in components + } + + queried_measurements = [ + ComponentMeasurements( + raw_measurements=all_measurements.get(component_id, []), + component_id=component_id, + interval=interval, + after=after, + before=before, + last_measurement=last_measurements_mapping.get(component_id), + components_mapping=components_mapping, + ) + for component_id in component_ids + ] + + return sorted( + queried_measurements, + key=lambda c: c.name, + reverse=ordering_direction == OrderingDirection.DESC, + ) + + +@coverage_analytics_bindable.field("componentsYaml") +def resolve_components_yaml( + parent: CoverageAnalyticsProps, info: GraphQLResolveInfo, term_id: Optional[str] +) -> List[str]: + components = UserYaml.get_final_yaml( + owner_yaml=parent.repository.author.yaml, + repo_yaml=parent.repository.yaml, + ownerid=parent.repository.author.ownerid, + ).get_components() + + components = [ + { + "id": c.component_id, + "name": c.name, + } + for c in components + ] + + if term_id: + components = filter(lambda c: term_id in c["id"], components) + + return components + + +@coverage_analytics_bindable.field("componentsMeasurementsActive") +@sync_to_async +def resolve_components_measurements_active( + parent: CoverageAnalyticsProps, info: GraphQLResolveInfo +) -> bool: + if not settings.TIMESERIES_ENABLED: + return False + + return Dataset.objects.filter( + name=MeasurementName.COMPONENT_COVERAGE.value, + repository_id=parent.repository.pk, + ).exists() + + +@coverage_analytics_bindable.field("componentsMeasurementsBackfilled") +@sync_to_async +def resolve_components_measurements_backfilled( + parent: CoverageAnalyticsProps, info: GraphQLResolveInfo +) -> bool: + if not settings.TIMESERIES_ENABLED: + return False + + dataset = Dataset.objects.filter( + name=MeasurementName.COMPONENT_COVERAGE.value, + repository_id=parent.repository.pk, + ).first() + + if not dataset: + return False + + return dataset.is_backfilled() + + +@coverage_analytics_bindable.field("componentsCount") +@sync_to_async +def resolve_components_count( + parent: CoverageAnalyticsProps, info: GraphQLResolveInfo +) -> int: + repo_yaml_components = UserYaml.get_final_yaml( + owner_yaml=parent.repository.author.yaml, + repo_yaml=parent.repository.yaml, + ownerid=parent.repository.author.ownerid, + ).get_components() + + return len(repo_yaml_components) + + +@coverage_analytics_bindable.field("flags") +@sync_to_async +@sentry_sdk.trace +def resolve_flags( + parent: CoverageAnalyticsProps, + info: GraphQLResolveInfo, + filters: Mapping = None, + ordering_direction: OrderingDirection = OrderingDirection.ASC, + **kwargs, +): + queryset = flags_for_repo(parent.repository, filters) + connection = queryset_to_connection_sync( + queryset, + ordering=("flag_name",), + ordering_direction=ordering_direction, + **kwargs, + ) + + # We fetch the measurements in this resolver since there are multiple child + # flag resolvers that depend on this data. Additionally, we're able to fetch + # measurements for all the flags being returned at once. + # Use the lookahead to make sure we don't overfetch measurements that we don't + # need. + node = lookahead(info, ("edges", "node", "measurements")) + if node: + if settings.TIMESERIES_ENABLED: + # TODO: is there a way to have these automatically casted at a + # lower level (i.e. based on the schema)? + interval = node.args["interval"] + if isinstance(interval, str): + interval = Interval[interval] + after = node.args["after"] + if isinstance(after, str): + after = from_current_timezone(datetime.fromisoformat(after)) + before = node.args["before"] + if isinstance(before, str): + before = from_current_timezone(datetime.fromisoformat(before)) + + flag_ids = [edge["node"].pk for edge in connection.edges] + + info.context["flag_measurements"] = flag_measurements( + parent.repository, flag_ids, interval, after, before + ) + else: + info.context["flag_measurements"] = {} + + return connection + + +@coverage_analytics_bindable.field("flagsCount") +@sync_to_async +def resolve_flags_count( + parent: CoverageAnalyticsProps, info: GraphQLResolveInfo +) -> int: + return parent.repository.flags.filter(deleted__isnot=True).count() + + +@coverage_analytics_bindable.field("flagsMeasurementsActive") +@sync_to_async +def resolve_flags_measurements_active( + parent: CoverageAnalyticsProps, info: GraphQLResolveInfo +) -> bool: + if not settings.TIMESERIES_ENABLED: + return False + + return Dataset.objects.filter( + name=MeasurementName.FLAG_COVERAGE.value, + repository_id=parent.repository.pk, + ).exists() + + +@coverage_analytics_bindable.field("flagsMeasurementsBackfilled") +@sync_to_async +def resolve_flags_measurements_backfilled( + parent: CoverageAnalyticsProps, info: GraphQLResolveInfo +) -> bool: + if not settings.TIMESERIES_ENABLED: + return False + + dataset = Dataset.objects.filter( + name=MeasurementName.FLAG_COVERAGE.value, + repository_id=parent.repository.pk, + ).first() + + if not dataset: + return False + + return dataset.is_backfilled() diff --git a/apps/codecov-api/graphql_api/types/coverage_totals/__init__.py b/apps/codecov-api/graphql_api/types/coverage_totals/__init__.py new file mode 100644 index 0000000000..4b7d597f06 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/coverage_totals/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .coverage_totals import coverage_totals_bindable + +coverage_totals = ariadne_load_local_graphql(__file__, "coverage_totals.graphql") + + +__all__ = ["coverage_totals_bindable"] diff --git a/apps/codecov-api/graphql_api/types/coverage_totals/coverage_totals.graphql b/apps/codecov-api/graphql_api/types/coverage_totals/coverage_totals.graphql new file mode 100644 index 0000000000..cd75052a93 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/coverage_totals/coverage_totals.graphql @@ -0,0 +1,11 @@ + +type CoverageTotals { + percentCovered: Float + fileCount: Int + lineCount: Int + hitsCount: Int + missesCount: Int + partialsCount: Int + + coverage: Float @deprecated(reason: "Use `percentCovered`") +} diff --git a/apps/codecov-api/graphql_api/types/coverage_totals/coverage_totals.py b/apps/codecov-api/graphql_api/types/coverage_totals/coverage_totals.py new file mode 100644 index 0000000000..f437a293c2 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/coverage_totals/coverage_totals.py @@ -0,0 +1,10 @@ +from ariadne import ObjectType + +coverage_totals_bindable = ObjectType("CoverageTotals") + +coverage_totals_bindable.set_alias("percentCovered", "coverage") +coverage_totals_bindable.set_alias("fileCount", "files") +coverage_totals_bindable.set_alias("lineCount", "lines") +coverage_totals_bindable.set_alias("hitsCount", "hits") +coverage_totals_bindable.set_alias("missesCount", "misses") +coverage_totals_bindable.set_alias("partialsCount", "partials") diff --git a/apps/codecov-api/graphql_api/types/enums/__init__.py b/apps/codecov-api/graphql_api/types/enums/__init__.py new file mode 100644 index 0000000000..3ad148f7ef --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/__init__.py @@ -0,0 +1,45 @@ +from .enums import ( + AssetOrdering, + BundleLoadTypes, + CommitErrorCode, + CommitErrorGeneralType, + CommitStatus, + CoverageLine, + GoalOnboarding, + LoginProvider, + OrderingDirection, + OrderingParameter, + PathContentDisplayType, + PullRequestState, + RepositoryOrdering, + SyncProvider, + TestResultsFilterParameter, + TestResultsOrderingParameter, + TypeProjectOnboarding, + UploadErrorEnum, + UploadState, + UploadType, +) + +__all__ = [ + "AssetOrdering", + "BundleLoadTypes", + "CommitErrorCode", + "CommitErrorGeneralType", + "CommitStatus", + "CoverageLine", + "GoalOnboarding", + "LoginProvider", + "OrderingDirection", + "OrderingParameter", + "PathContentDisplayType", + "PullRequestState", + "RepositoryOrdering", + "SyncProvider", + "TestResultsFilterParameter", + "TestResultsOrderingParameter", + "TypeProjectOnboarding", + "UploadErrorEnum", + "UploadState", + "UploadType", +] diff --git a/apps/codecov-api/graphql_api/types/enums/asset_ordering.graphql b/apps/codecov-api/graphql_api/types/enums/asset_ordering.graphql new file mode 100644 index 0000000000..fb11939dbf --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/asset_ordering.graphql @@ -0,0 +1,5 @@ +enum AssetOrdering { + NAME + SIZE + TYPE +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/enums/commit_error_code.graphql b/apps/codecov-api/graphql_api/types/enums/commit_error_code.graphql new file mode 100644 index 0000000000..9c7c89ea1f --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/commit_error_code.graphql @@ -0,0 +1,6 @@ +enum CommitErrorCode { + repo_bot_invalid + invalid_yaml + yaml_client_error + yaml_unknown_error +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/enums/commit_error_type.graphql b/apps/codecov-api/graphql_api/types/enums/commit_error_type.graphql new file mode 100644 index 0000000000..a9fe85a97f --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/commit_error_type.graphql @@ -0,0 +1,4 @@ +enum CommitErrorType { + YAML_ERROR + BOT_ERROR +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/enums/commit_state.graphql b/apps/codecov-api/graphql_api/types/enums/commit_state.graphql new file mode 100644 index 0000000000..9e5e616c5d --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/commit_state.graphql @@ -0,0 +1,6 @@ +enum CommitState { + COMPLETE + PENDING + ERROR + SKIPPED +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/enums/commit_status.graphql b/apps/codecov-api/graphql_api/types/enums/commit_status.graphql new file mode 100644 index 0000000000..7d060c42f6 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/commit_status.graphql @@ -0,0 +1,5 @@ +enum CommitStatus { + COMPLETED + ERROR + PENDING +} diff --git a/apps/codecov-api/graphql_api/types/enums/coverage_line.graphql b/apps/codecov-api/graphql_api/types/enums/coverage_line.graphql new file mode 100644 index 0000000000..5b6eefbd57 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/coverage_line.graphql @@ -0,0 +1,8 @@ +""" +Possible value for the coverage of a line, using single letter for a more compact response +""" +enum CoverageLine { + H # hit + M # miss + P # partial +} diff --git a/apps/codecov-api/graphql_api/types/enums/enum_types.py b/apps/codecov-api/graphql_api/types/enums/enum_types.py new file mode 100644 index 0000000000..a8033b12ba --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/enum_types.py @@ -0,0 +1,59 @@ +from ariadne import EnumType +from shared.plan.constants import TierName, TrialStatus + +from codecov_auth.models import RepositoryToken +from compare.commands.compare.interactors.fetch_impacted_files import ( + ImpactedFileParameter, +) +from core.models import Commit +from services.yaml import YamlStates +from timeseries.models import Interval as MeasurementInterval +from timeseries.models import MeasurementName + +from .enums import ( + AssetOrdering, + BundleLoadTypes, + CoverageLine, + GoalOnboarding, + LoginProvider, + OrderingDirection, + OrderingParameter, + PathContentDisplayType, + PullRequestState, + RepositoryOrdering, + SyncProvider, + TestResultsFilterParameter, + TestResultsOrderingParameter, + TypeProjectOnboarding, + UploadErrorEnum, + UploadState, + UploadType, +) + +enum_types = [ + EnumType("RepositoryOrdering", RepositoryOrdering), + EnumType("OrderingDirection", OrderingDirection), + EnumType("CoverageLine", CoverageLine), + EnumType("PathContentDisplayType", PathContentDisplayType), + EnumType("TypeProjectOnboarding", TypeProjectOnboarding), + EnumType("GoalOnboarding", GoalOnboarding), + EnumType("OrderingParameter", OrderingParameter), + EnumType("PullRequestState", PullRequestState), + EnumType("UploadState", UploadState), + EnumType("UploadType", UploadType), + EnumType("UploadErrorEnum", UploadErrorEnum), + EnumType("MeasurementInterval", MeasurementInterval), + EnumType("LoginProvider", LoginProvider), + EnumType("ImpactedFileParameter", ImpactedFileParameter), + EnumType("CommitState", Commit.CommitStates), + EnumType("MeasurementType", MeasurementName), + EnumType("RepositoryTokenType", RepositoryToken.TokenType), + EnumType("SyncProvider", SyncProvider), + EnumType("TierName", TierName), + EnumType("TrialStatus", TrialStatus), + EnumType("YamlStates", YamlStates), + EnumType("BundleLoadTypes", BundleLoadTypes), + EnumType("TestResultsOrderingParameter", TestResultsOrderingParameter), + EnumType("TestResultsFilterParameter", TestResultsFilterParameter), + EnumType("AssetOrdering", AssetOrdering), +] diff --git a/apps/codecov-api/graphql_api/types/enums/enums.py b/apps/codecov-api/graphql_api/types/enums/enums.py new file mode 100644 index 0000000000..c143654b2e --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/enums.py @@ -0,0 +1,146 @@ +import enum +from typing import Self + +from shared.upload.constants import UploadErrorCode as SharedUploadErrorCode + + +class OrderingParameter(enum.Enum): + NAME = "name" + COVERAGE = "coverage" + HITS = "hits" + MISSES = "misses" + PARTIALS = "partials" + LINES = "lines" + + +class TestResultsFilterParameter(enum.Enum): + FLAKY_TESTS = "flaky_tests" + FAILED_TESTS = "failed_tests" + SLOWEST_TESTS = "slowest_tests" + SKIPPED_TESTS = "skipped_tests" + + +class TestResultsOrderingParameter(enum.Enum): + LAST_DURATION = "last_duration" + AVG_DURATION = "avg_duration" + FAILURE_RATE = "failure_rate" + FLAKE_RATE = "flake_rate" + COMMITS_WHERE_FAIL = "commits_where_fail" + UPDATED_AT = "updated_at" + + +class PathContentDisplayType(enum.Enum): + TREE = "tree" + LIST = "list" + + +class RepositoryOrdering(enum.Enum): + COMMIT_DATE = "latest_commit_at" + COVERAGE = "coverage" + ID = "repoid" + NAME = "name" + + +class OrderingDirection(enum.Enum): + ASC = "ascending" + DESC = "descending" + + +class CoverageLine(enum.Enum): + H = "hit" + M = "miss" + P = "partial" + + +class TypeProjectOnboarding(enum.Enum): + PERSONAL = "PERSONAL" + YOUR_ORG = "YOUR_ORG" + OPEN_SOURCE = "OPEN_SOURCE" + EDUCATIONAL = "EDUCATIONAL" + + +class GoalOnboarding(enum.Enum): + STARTING_WITH_TESTS = "STARTING_WITH_TESTS" + IMPROVE_COVERAGE = "IMPROVE_COVERAGE" + MAINTAIN_COVERAGE = "MAINTAIN_COVERAGE" + TEAM_REQUIREMENTS = "TEAM_REQUIREMENTS" + OTHER = "OTHER" + + +class PullRequestState(enum.Enum): + OPEN = "open" + CLOSED = "closed" + MERGED = "merged" + + +class UploadState(enum.Enum): + STARTED = "started" + UPLOADED = "uploaded" + PROCESSED = "processed" + ERROR = "error" + COMPLETE = "complete" + + +class UploadType(enum.Enum): + UPLOADED = "uploaded" + CARRIEDFORWARD = "carriedforward" + + +UploadErrorEnum = SharedUploadErrorCode + + +class LoginProvider(enum.Enum): + GITHUB = "github" + GITHUB_ENTERPRISE = "github_enterprise" + GITLAB = "gitlab" + GITLAB_ENTERPRISE = "gitlab_enterprise" + BITBUCKET = "bitbucket" + BITBUCKET_SERVER = "bitbucket_server" + OKTA = "okta" + + +class SyncProvider(enum.Enum): + GITHUB = "github" + GITHUB_ENTERPRISE = "github_enterprise" + GITLAB = "gitlab" + GITLAB_ENTERPRISE = "gitlab_enterprise" + BITBUCKET = "bitbucket" + BITBUCKET_SERVER = "bitbucket_server" + + +class CommitErrorGeneralType(enum.Enum): + yaml_error = "YAML_ERROR" + bot_error = "BOT_ERROR" + + +class CommitErrorCode(enum.Enum): + invalid_yaml = ("invalid_yaml", CommitErrorGeneralType.yaml_error) + yaml_client_error = ("yaml_client_error", CommitErrorGeneralType.yaml_error) + yaml_unknown_error = ("yaml_unknown_error", CommitErrorGeneralType.yaml_error) + repo_bot_invalid = ("repo_bot_invalid", CommitErrorGeneralType.bot_error) + + def __init__(self, db_string: str, error_type: CommitErrorGeneralType): + self.db_string = db_string + self.error_type = error_type + + @classmethod + def get_codes_from_type(cls, error_type: CommitErrorGeneralType) -> list[Self]: + return [item for item in cls if item.error_type == error_type] + + +class CommitStatus(enum.Enum): + COMPLETED = "COMPLETED" + ERROR = "ERROR" + PENDING = "PENDING" + + +class BundleLoadTypes(enum.Enum): + ENTRY = "ENTRY" + INITIAL = "INITIAL" + LAZY = "LAZY" + + +class AssetOrdering(enum.Enum): + NAME = "name" + SIZE = "size" + TYPE = "asset_type" diff --git a/apps/codecov-api/graphql_api/types/enums/goal_onboarding.graphql b/apps/codecov-api/graphql_api/types/enums/goal_onboarding.graphql new file mode 100644 index 0000000000..917b3ad215 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/goal_onboarding.graphql @@ -0,0 +1,7 @@ +enum GoalOnboarding { + STARTING_WITH_TESTS + IMPROVE_COVERAGE + MAINTAIN_COVERAGE + TEAM_REQUIREMENTS + OTHER +} diff --git a/apps/codecov-api/graphql_api/types/enums/impacted_files_parameters.graphql b/apps/codecov-api/graphql_api/types/enums/impacted_files_parameters.graphql new file mode 100644 index 0000000000..1668c74e3a --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/impacted_files_parameters.graphql @@ -0,0 +1,10 @@ +""" +Possible value for the impacted file list +""" +enum ImpactedFileParameter { + FILE_NAME + CHANGE_COVERAGE + HEAD_COVERAGE + MISSES_COUNT + PATCH_COVERAGE +} diff --git a/apps/codecov-api/graphql_api/types/enums/login_provider.graphql b/apps/codecov-api/graphql_api/types/enums/login_provider.graphql new file mode 100644 index 0000000000..68768d5508 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/login_provider.graphql @@ -0,0 +1,9 @@ +enum LoginProvider { + GITHUB + GITHUB_ENTERPRISE + GITLAB + GITLAB_ENTERPRISE + BITBUCKET + BITBUCKET_SERVER + OKTA +} diff --git a/apps/codecov-api/graphql_api/types/enums/measurement_interval.graphql b/apps/codecov-api/graphql_api/types/enums/measurement_interval.graphql new file mode 100644 index 0000000000..b900307963 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/measurement_interval.graphql @@ -0,0 +1,5 @@ +enum MeasurementInterval { + INTERVAL_1_DAY + INTERVAL_7_DAY + INTERVAL_30_DAY +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/enums/measurement_type.graphql b/apps/codecov-api/graphql_api/types/enums/measurement_type.graphql new file mode 100644 index 0000000000..e4aa7b0ed3 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/measurement_type.graphql @@ -0,0 +1,11 @@ +enum MeasurementType { + COVERAGE + FLAG_COVERAGE + COMPONENT_COVERAGE + BUNDLE_ANALYSIS_REPORT_SIZE + BUNDLE_ANALYSIS_JAVASCRIPT_SIZE + BUNDLE_ANALYSIS_STYLESHEET_SIZE + BUNDLE_ANALYSIS_FONT_SIZE + BUNDLE_ANALYSIS_IMAGE_SIZE + BUNDLE_ANALYSIS_ASSET_SIZE +} diff --git a/apps/codecov-api/graphql_api/types/enums/ordering_direction.graphql b/apps/codecov-api/graphql_api/types/enums/ordering_direction.graphql new file mode 100644 index 0000000000..7a3ab869d0 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/ordering_direction.graphql @@ -0,0 +1,4 @@ +enum OrderingDirection { + ASC + DESC +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/enums/path_contents_value.graphql b/apps/codecov-api/graphql_api/types/enums/path_contents_value.graphql new file mode 100644 index 0000000000..45d57c0dcd --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/path_contents_value.graphql @@ -0,0 +1,16 @@ +""" +Possible value for the search values for a path tree of a pull request +""" +enum OrderingParameter{ + NAME + COVERAGE + HITS + MISSES + PARTIALS + LINES +} + +enum PathContentDisplayType{ + LIST + TREE +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/enums/pull_request_state.graphql b/apps/codecov-api/graphql_api/types/enums/pull_request_state.graphql new file mode 100644 index 0000000000..6621136766 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/pull_request_state.graphql @@ -0,0 +1,8 @@ +""" +Possible value for the state of a pull request +""" +enum PullRequestState{ + OPEN + CLOSED + MERGED +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/enums/repository_ordering.graphql b/apps/codecov-api/graphql_api/types/enums/repository_ordering.graphql new file mode 100644 index 0000000000..34bebbc2a9 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/repository_ordering.graphql @@ -0,0 +1,6 @@ +enum RepositoryOrdering { + COMMIT_DATE + COVERAGE + ID + NAME +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/enums/repository_token.graphql b/apps/codecov-api/graphql_api/types/enums/repository_token.graphql new file mode 100644 index 0000000000..59d93e3494 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/repository_token.graphql @@ -0,0 +1,5 @@ +enum RepositoryTokenType { + UPLOAD + PROFILING + STATIC_ANALYSIS +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/enums/sync_provider.graphql b/apps/codecov-api/graphql_api/types/enums/sync_provider.graphql new file mode 100644 index 0000000000..4e65d1732d --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/sync_provider.graphql @@ -0,0 +1,8 @@ +enum SyncProvider { + GITHUB + GITHUB_ENTERPRISE + GITLAB + GITLAB_ENTERPRISE + BITBUCKET + BITBUCKET_SERVER +} diff --git a/apps/codecov-api/graphql_api/types/enums/test_results_filtering_parameter.graphql b/apps/codecov-api/graphql_api/types/enums/test_results_filtering_parameter.graphql new file mode 100644 index 0000000000..9d9207e66d --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/test_results_filtering_parameter.graphql @@ -0,0 +1,6 @@ +enum TestResultsFilterParameter { + FLAKY_TESTS, + FAILED_TESTS, + SLOWEST_TESTS, + SKIPPED_TESTS +} diff --git a/apps/codecov-api/graphql_api/types/enums/test_results_ordering_parameter.graphql b/apps/codecov-api/graphql_api/types/enums/test_results_ordering_parameter.graphql new file mode 100644 index 0000000000..d4d7bb9c46 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/test_results_ordering_parameter.graphql @@ -0,0 +1,8 @@ +enum TestResultsOrderingParameter { + LAST_DURATION + AVG_DURATION + FAILURE_RATE + FLAKE_RATE + COMMITS_WHERE_FAIL + UPDATED_AT +} diff --git a/apps/codecov-api/graphql_api/types/enums/tier_name.graphql b/apps/codecov-api/graphql_api/types/enums/tier_name.graphql new file mode 100644 index 0000000000..aeb825d0d4 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/tier_name.graphql @@ -0,0 +1,8 @@ +enum TierName { + BASIC + TEAM + PRO + ENTERPRISE + SENTRY + TRIAL +} diff --git a/apps/codecov-api/graphql_api/types/enums/trial_status.graphql b/apps/codecov-api/graphql_api/types/enums/trial_status.graphql new file mode 100644 index 0000000000..5583bc69b6 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/trial_status.graphql @@ -0,0 +1,6 @@ +enum TrialStatus { + NOT_STARTED + ONGOING + EXPIRED + CANNOT_TRIAL +} diff --git a/apps/codecov-api/graphql_api/types/enums/type_projects.graphql b/apps/codecov-api/graphql_api/types/enums/type_projects.graphql new file mode 100644 index 0000000000..25c783cd4e --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/type_projects.graphql @@ -0,0 +1,6 @@ +enum TypeProjectOnboarding { + PERSONAL + YOUR_ORG + OPEN_SOURCE + EDUCATIONAL +} diff --git a/apps/codecov-api/graphql_api/types/enums/upload_error_enum.graphql b/apps/codecov-api/graphql_api/types/enums/upload_error_enum.graphql new file mode 100644 index 0000000000..8e07d3d5b2 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/upload_error_enum.graphql @@ -0,0 +1,10 @@ +enum UploadErrorEnum { + FILE_NOT_IN_STORAGE + REPORT_EXPIRED + REPORT_EMPTY + PROCESSING_TIMEOUT + UNSUPPORTED_FILE_FORMAT + + UNKNOWN_PROCESSING + UNKNOWN_STORAGE +} diff --git a/apps/codecov-api/graphql_api/types/enums/upload_state.graphql b/apps/codecov-api/graphql_api/types/enums/upload_state.graphql new file mode 100644 index 0000000000..5254b847b7 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/upload_state.graphql @@ -0,0 +1,7 @@ +enum UploadState { + STARTED + UPLOADED + PROCESSED + ERROR + COMPLETE +} diff --git a/apps/codecov-api/graphql_api/types/enums/upload_type.graphql b/apps/codecov-api/graphql_api/types/enums/upload_type.graphql new file mode 100644 index 0000000000..779bdb964b --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/upload_type.graphql @@ -0,0 +1,4 @@ +enum UploadType { + UPLOADED + CARRIEDFORWARD +} diff --git a/apps/codecov-api/graphql_api/types/enums/yaml_states.graphql b/apps/codecov-api/graphql_api/types/enums/yaml_states.graphql new file mode 100644 index 0000000000..6bd4079cc4 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/enums/yaml_states.graphql @@ -0,0 +1,3 @@ +enum YamlStates { + DEFAULT +} diff --git a/apps/codecov-api/graphql_api/types/errors/__init__.py b/apps/codecov-api/graphql_api/types/errors/__init__.py new file mode 100644 index 0000000000..2351d803c6 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/errors/__init__.py @@ -0,0 +1,21 @@ +from .errors import ( + MissingBaseCommit, + MissingBaseReport, + MissingComparison, + MissingCoverage, + MissingHeadCommit, + MissingHeadReport, + ProviderError, + UnknownPath, +) + +__all__ = [ + "MissingBaseCommit", + "MissingBaseReport", + "MissingComparison", + "MissingCoverage", + "MissingHeadCommit", + "MissingHeadReport", + "ProviderError", + "UnknownPath", +] diff --git a/apps/codecov-api/graphql_api/types/errors/errors.graphql b/apps/codecov-api/graphql_api/types/errors/errors.graphql new file mode 100644 index 0000000000..68b1d84971 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/errors/errors.graphql @@ -0,0 +1,43 @@ +interface ResolverError { + message: String! +} + +type UnauthenticatedError implements ResolverError { + message: String! +} + +type UnauthorizedError implements ResolverError { + message: String! +} + +type NotFoundError implements ResolverError { + message: String! +} + +type ValidationError implements ResolverError { + message: String! +} + +type MissingCoverage implements ResolverError { + message: String! +} + +type UnknownPath implements ResolverError { + message: String! +} + +type UnknownFlags implements ResolverError { + message: String! +} + +type ProviderError implements ResolverError { + message: String! +} + +type OwnerNotActivatedError implements ResolverError { + message: String! +} + +type MissingService implements ResolverError { + message: String! +} diff --git a/apps/codecov-api/graphql_api/types/errors/errors.py b/apps/codecov-api/graphql_api/types/errors/errors.py new file mode 100644 index 0000000000..7df524a99b --- /dev/null +++ b/apps/codecov-api/graphql_api/types/errors/errors.py @@ -0,0 +1,45 @@ +class MissingHeadReport: + message = "Missing head report" + + +class UnknownFlags: + def __init__(self, message="No coverage with chosen flags"): + self.message = message + + +class MissingBaseCommit: + message = "Invalid base commit" + + +class MissingHeadCommit: + message = "Invalid head commit" + + +class MissingComparison: + message = "Missing comparison" + + +class MissingBaseReport: + message = "Missing base report" + + +class MissingCoverage: + def __init__(self, message="Missing coverage"): + self.message = message + + +class UnknownPath: + def __init__(self, message="Unkown path"): + self.message = message + + +class ProviderError: + message = "Error fetching data from the provider" + + +class OwnerNotActivatedError: + message = "You must be activated in the org" + + +class NotFoundError: + message = "Not found" diff --git a/apps/codecov-api/graphql_api/types/file/__init__.py b/apps/codecov-api/graphql_api/types/file/__init__.py new file mode 100644 index 0000000000..09e7e120d9 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/file/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .file import file_bindable + +commit_file = ariadne_load_local_graphql(__file__, "file.graphql") + + +__all__ = ["file_bindable"] diff --git a/apps/codecov-api/graphql_api/types/file/file.graphql b/apps/codecov-api/graphql_api/types/file/file.graphql new file mode 100644 index 0000000000..bc70e71104 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/file/file.graphql @@ -0,0 +1,11 @@ +type File { + content: String + coverage: [CoverageAnnotation] + totals: CoverageTotals + hashedPath: String! +} + +type CoverageAnnotation { + line: Int + coverage: CoverageLine +} diff --git a/apps/codecov-api/graphql_api/types/file/file.py b/apps/codecov-api/graphql_api/types/file/file.py new file mode 100644 index 0000000000..c8289d6b6a --- /dev/null +++ b/apps/codecov-api/graphql_api/types/file/file.py @@ -0,0 +1,53 @@ +import hashlib + +from ariadne import ObjectType +from shared.utils.merge import LineType, line_type + +from graphql_api.types.enums import CoverageLine + +file_bindable = ObjectType("File") + + +@file_bindable.field("content") +def resolve_content(data, info): + command = info.context["executor"].get_command("commit") + return command.get_file_content(data.get("commit"), data.get("path")) + + +def get_coverage_type(line_report): + # Get the coverage type from the line_report + coverage = line_type(line_report.coverage) + # Convert the LineType enum from shared to the GraphQL one + return { + LineType.hit: CoverageLine.H, + LineType.miss: CoverageLine.M, + LineType.partial: CoverageLine.P, + }.get(coverage) + + +@file_bindable.field("coverage") +def resolve_coverage(data, info): + file_report = data.get("file_report") + + if not file_report: + return [] + + return [ + {"line": line_report[0], "coverage": get_coverage_type(line_report[1])} + for line_report in file_report.lines + ] + + +@file_bindable.field("totals") +def resolve_totals(data, info): + file_report = data.get("file_report") + return file_report.totals if file_report else None + + +@file_bindable.field("hashedPath") +def resolve_hashed_path(data, info): + path = data.get("path") + encoded_path = path.encode() + md5_path = hashlib.md5(encoded_path) + + return md5_path.hexdigest() diff --git a/apps/codecov-api/graphql_api/types/flag/__init__.py b/apps/codecov-api/graphql_api/types/flag/__init__.py new file mode 100644 index 0000000000..97313d98c6 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/flag/__init__.py @@ -0,0 +1,10 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql +from graphql_api.helpers.connection import build_connection_graphql + +from .flag import flag_bindable + +flag = ariadne_load_local_graphql(__file__, "flag.graphql") +flag += build_connection_graphql("FlagConnection", "Flag") + + +__all__ = ["flag_bindable"] diff --git a/apps/codecov-api/graphql_api/types/flag/flag.graphql b/apps/codecov-api/graphql_api/types/flag/flag.graphql new file mode 100644 index 0000000000..6acb6afa1b --- /dev/null +++ b/apps/codecov-api/graphql_api/types/flag/flag.graphql @@ -0,0 +1,6 @@ +type Flag { + name: String! + percentCovered: Float + percentChange: Float + measurements(interval: MeasurementInterval!, after: DateTime!, before: DateTime!): [Measurement!]! +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/flag/flag.py b/apps/codecov-api/graphql_api/types/flag/flag.py new file mode 100644 index 0000000000..64427ddf47 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/flag/flag.py @@ -0,0 +1,51 @@ +from datetime import datetime +from typing import Iterable + +from ariadne import ObjectType + +from reports.models import RepositoryFlag +from timeseries.helpers import fill_sparse_measurements +from timeseries.models import Interval, MeasurementSummary + +flag_bindable = ObjectType("Flag") + +# NOTE: measurements are fetched in the parent resolver (repository) and +# placed in the context so that they can be used in multiple resolvers here + + +@flag_bindable.field("name") +def resolve_timestamp(flag: RepositoryFlag, info) -> str: + return flag.flag_name + + +@flag_bindable.field("percentCovered") +def resolve_percent_covered(flag: RepositoryFlag, info) -> float: + if "flag_measurements" not in info.context: + # we rely on measurements for this computed value + return None + + measurements = info.context["flag_measurements"].get(flag.pk, []) + if len(measurements) > 0: + # coverage returned is the most recent measurement average + return measurements[-1]["avg"] + + +@flag_bindable.field("percentChange") +def resolve_percent_change(flag: RepositoryFlag, info) -> float: + if "flag_measurements" not in info.context: + # we rely on measurements for this computed value + return None + + measurements = info.context["flag_measurements"].get(flag.pk, []) + if len(measurements) > 1: + return measurements[-1]["avg"] - measurements[0]["avg"] + + +@flag_bindable.field("measurements") +def resolve_measurements( + flag: RepositoryFlag, info, interval: Interval, after: datetime, before: datetime +) -> Iterable[MeasurementSummary]: + measurements = info.context["flag_measurements"].get(flag.pk, []) + if len(measurements) == 0: + return [] + return fill_sparse_measurements(measurements, interval, after, before) diff --git a/apps/codecov-api/graphql_api/types/flag_comparison/__init__.py b/apps/codecov-api/graphql_api/types/flag_comparison/__init__.py new file mode 100644 index 0000000000..2589d8e9bb --- /dev/null +++ b/apps/codecov-api/graphql_api/types/flag_comparison/__init__.py @@ -0,0 +1,9 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql +from graphql_api.helpers.connection import build_connection_graphql + +from .flag_comparison import flag_comparison_bindable + +flag_comparison = ariadne_load_local_graphql(__file__, "flag_comparison.graphql") + + +__all__ = ["flag_comparison_bindable", "build_connection_graphql"] diff --git a/apps/codecov-api/graphql_api/types/flag_comparison/flag_comparison.graphql b/apps/codecov-api/graphql_api/types/flag_comparison/flag_comparison.graphql new file mode 100644 index 0000000000..2f0492ef64 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/flag_comparison/flag_comparison.graphql @@ -0,0 +1,6 @@ +type FlagComparison { + name: String! + headTotals: CoverageTotals + baseTotals: CoverageTotals + patchTotals: CoverageTotals +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/flag_comparison/flag_comparison.py b/apps/codecov-api/graphql_api/types/flag_comparison/flag_comparison.py new file mode 100644 index 0000000000..8a02571873 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/flag_comparison/flag_comparison.py @@ -0,0 +1,25 @@ +from ariadne import ObjectType + +from compare.models import FlagComparison + +flag_comparison_bindable = ObjectType("FlagComparison") + + +@flag_comparison_bindable.field("name") +def resolve_name(flag_comparison: FlagComparison, info) -> str: + return flag_comparison.repositoryflag.flag_name + + +@flag_comparison_bindable.field("patchTotals") +def resolve_patch_totals(flag_comparison: FlagComparison, info) -> dict: + return flag_comparison.patch_totals + + +@flag_comparison_bindable.field("headTotals") +def resolve_head_totals(flag_comparison: FlagComparison, info) -> dict: + return flag_comparison.head_totals + + +@flag_comparison_bindable.field("baseTotals") +def resolve_base_totals(flag_comparison: FlagComparison, info) -> dict: + return flag_comparison.base_totals diff --git a/apps/codecov-api/graphql_api/types/flake_aggregates/__init__.py b/apps/codecov-api/graphql_api/types/flake_aggregates/__init__.py new file mode 100644 index 0000000000..c8692bbeae --- /dev/null +++ b/apps/codecov-api/graphql_api/types/flake_aggregates/__init__.py @@ -0,0 +1,7 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .flake_aggregates import flake_aggregates_bindable + +flake_aggregates = ariadne_load_local_graphql(__file__, "flake_aggregates.graphql") + +__all__ = ["flake_aggregates_bindable"] diff --git a/apps/codecov-api/graphql_api/types/flake_aggregates/flake_aggregates.graphql b/apps/codecov-api/graphql_api/types/flake_aggregates/flake_aggregates.graphql new file mode 100644 index 0000000000..92149c4f38 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/flake_aggregates/flake_aggregates.graphql @@ -0,0 +1,6 @@ +type FlakeAggregates { + flakeCount: Int! + flakeCountPercentChange: Float + flakeRate: Float! + flakeRatePercentChange: Float +} diff --git a/apps/codecov-api/graphql_api/types/flake_aggregates/flake_aggregates.py b/apps/codecov-api/graphql_api/types/flake_aggregates/flake_aggregates.py new file mode 100644 index 0000000000..4b6d8603e4 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/flake_aggregates/flake_aggregates.py @@ -0,0 +1,97 @@ +from dataclasses import dataclass + +import polars as pl +from ariadne import ObjectType +from graphql import GraphQLResolveInfo +from shared.django_apps.core.models import Repository + +from graphql_api.types.enums.enum_types import MeasurementInterval +from utils.test_results import get_results + + +@dataclass +class FlakeAggregates: + flake_count: int + flake_rate: float + flake_count_percent_change: float | None = None + flake_rate_percent_change: float | None = None + + +def calculate_flake_aggregates(table: pl.DataFrame) -> pl.DataFrame: + return table.select( + (pl.col("total_flaky_fail_count") > 0).sum().alias("flake_count"), + ( + pl.col("total_flaky_fail_count").sum() + / (pl.col("total_fail_count").sum() + pl.col("total_pass_count").sum()) + ).alias("flake_rate"), + ) + + +def flake_aggregates_from_table(table: pl.DataFrame) -> FlakeAggregates: + aggregates = calculate_flake_aggregates(table).row(0, named=True) + return FlakeAggregates(**aggregates) + + +def flake_aggregates_with_percentage( + curr_results: pl.DataFrame, + past_results: pl.DataFrame, +) -> FlakeAggregates: + curr_aggregates = calculate_flake_aggregates(curr_results) + past_aggregates = calculate_flake_aggregates(past_results) + + merged_results: pl.DataFrame = pl.concat([past_aggregates, curr_aggregates]) + + merged_results = merged_results.with_columns( + pl.all() + .pct_change() + .replace([float("inf"), float("-inf")], None) + .fill_nan(0) + .name.suffix("_percent_change") + ) + aggregates = merged_results.row(1, named=True) + + return FlakeAggregates(**aggregates) + + +def generate_flake_aggregates( + repoid: int, interval: MeasurementInterval +) -> FlakeAggregates | None: + repo = Repository.objects.get(repoid=repoid) + + curr_results = get_results(repo.repoid, repo.branch, interval.value) + if curr_results is None: + return None + past_results = get_results( + repo.repoid, repo.branch, interval.value * 2, interval.value + ) + if past_results is None: + return flake_aggregates_from_table(curr_results) + else: + return flake_aggregates_with_percentage(curr_results, past_results) + + +flake_aggregates_bindable = ObjectType("FlakeAggregates") + + +@flake_aggregates_bindable.field("flakeCount") +def resolve_flake_count(obj: FlakeAggregates, _: GraphQLResolveInfo) -> int: + return obj.flake_count + + +@flake_aggregates_bindable.field("flakeCountPercentChange") +def resolve_flake_count_percent_change( + obj: FlakeAggregates, _: GraphQLResolveInfo +) -> float | None: + return obj.flake_count_percent_change + + +@flake_aggregates_bindable.field("flakeRate") +def resolve_flake_rate(obj: FlakeAggregates, _: GraphQLResolveInfo) -> float: + return obj.flake_rate + + +@flake_aggregates_bindable.field("flakeRatePercentChange") +def resolve_flake_rate_percent_change( + obj: FlakeAggregates, _: GraphQLResolveInfo +) -> float | None: + return obj.flake_rate_percent_change diff --git a/apps/codecov-api/graphql_api/types/impacted_file/__init__.py b/apps/codecov-api/graphql_api/types/impacted_file/__init__.py new file mode 100644 index 0000000000..2412e2ff20 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/impacted_file/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .impacted_file import impacted_file_bindable, impacted_files_result_bindable + +impacted_file = ariadne_load_local_graphql(__file__, "impacted_file.graphql") + + +__all__ = ["impacted_file_bindable", "impacted_files_result_bindable"] diff --git a/apps/codecov-api/graphql_api/types/impacted_file/impacted_file.graphql b/apps/codecov-api/graphql_api/types/impacted_file/impacted_file.graphql new file mode 100644 index 0000000000..e9eb681c41 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/impacted_file/impacted_file.graphql @@ -0,0 +1,23 @@ +type ImpactedFile { + fileName: String + baseName: String + headName: String + isNewFile: Boolean! + isRenamedFile: Boolean! + isDeletedFile: Boolean! + baseCoverage: CoverageTotals + headCoverage: CoverageTotals + patchCoverage: CoverageTotals + changeCoverage: Float + missesCount: Int! + hashedPath: String! + segments(filters: SegmentsFilters): SegmentsResult! +} + +type ImpactedFiles { + results: [ImpactedFile] +} + +union ImpactedFilesResult = + ImpactedFiles + | UnknownFlags diff --git a/apps/codecov-api/graphql_api/types/impacted_file/impacted_file.py b/apps/codecov-api/graphql_api/types/impacted_file/impacted_file.py new file mode 100644 index 0000000000..19441eb70c --- /dev/null +++ b/apps/codecov-api/graphql_api/types/impacted_file/impacted_file.py @@ -0,0 +1,144 @@ +import hashlib +from typing import List, Union + +import sentry_sdk +from ariadne import ObjectType, UnionType +from asgiref.sync import sync_to_async +from shared.reports.types import ReportTotals +from shared.torngit.exceptions import TorngitClientError + +from graphql_api.types.errors import ProviderError, UnknownPath +from graphql_api.types.errors.errors import UnknownFlags +from graphql_api.types.segment_comparison.segment_comparison import SegmentComparisons +from services.comparison import ( + Comparison, + ImpactedFile, + MissingComparisonReport, +) + +impacted_file_bindable = ObjectType("ImpactedFile") + + +@impacted_file_bindable.field("fileName") +def resolve_file_name(impacted_file: ImpactedFile, info) -> str: + return impacted_file.file_name + + +@impacted_file_bindable.field("headName") +def resolve_head_name(impacted_file: ImpactedFile, info) -> str: + return impacted_file.head_name + + +@impacted_file_bindable.field("baseName") +def resolve_base_name(impacted_file: ImpactedFile, info) -> str: + return impacted_file.base_name + + +@impacted_file_bindable.field("headCoverage") +def resolve_head_coverage(impacted_file: ImpactedFile, info) -> ReportTotals: + return impacted_file.head_coverage + + +@impacted_file_bindable.field("baseCoverage") +def resolve_base_coverage(impacted_file: ImpactedFile, info) -> ReportTotals: + return impacted_file.base_coverage + + +@impacted_file_bindable.field("patchCoverage") +def resolve_patch_coverage(impacted_file: ImpactedFile, info) -> ReportTotals: + return impacted_file.patch_coverage + + +@impacted_file_bindable.field("changeCoverage") +def resolve_change_coverage(impacted_file: ImpactedFile, info) -> float: + return impacted_file.change_coverage + + +@impacted_file_bindable.field("hashedPath") +def resolve_hashed_path(impacted_file: ImpactedFile, info) -> str: + path = impacted_file.head_name + encoded_path = path.encode() + md5_path = hashlib.md5(encoded_path) + + return md5_path.hexdigest() + + +@impacted_file_bindable.field("segments") +@sync_to_async +@sentry_sdk.trace +def resolve_segments( + impacted_file: ImpactedFile, info, filters=None +) -> Union[UnknownPath, ProviderError, SegmentComparisons]: + if filters is None: + filters = {} + if "comparison" not in info.context: + return SegmentComparisons(results=[]) + + comparison: Comparison = info.context["comparison"] + try: + comparison.validate() + except MissingComparisonReport: + return SegmentComparisons(results=[]) + path = impacted_file.head_name + + try: + file_comparison = comparison.get_file_comparison( + path, with_src=True, bypass_max_diff=True + ) + except TorngitClientError as e: + if e.code == 404: + return UnknownPath(f"path does not exist: {path}") + else: + return ProviderError() + + segments = file_comparison.segments + + if filters.get("has_unintended_changes") is True: + # segments with no diff changes and at least 1 unintended change + segments = [segment for segment in segments if segment.has_unintended_changes] + elif filters.get("has_unintended_changes") is False: + new_segments = [] + for segment in segments: + if segment.has_diff_changes: + segment.remove_unintended_changes() + new_segments.append(segment) + segments = new_segments + + return SegmentComparisons(results=segments) + + +@impacted_file_bindable.field("isNewFile") +def resolve_is_new_file(impacted_file: ImpactedFile, info) -> bool: + base_name = impacted_file.base_name + head_name = impacted_file.head_name + return base_name is None and head_name is not None + + +@impacted_file_bindable.field("isRenamedFile") +def resolve_is_renamed_file(impacted_file: ImpactedFile, info) -> bool: + base_name = impacted_file.base_name + head_name = impacted_file.head_name + return base_name is not None and head_name is not None and base_name != head_name + + +@impacted_file_bindable.field("isDeletedFile") +def resolve_is_deleted_file(impacted_file: ImpactedFile, info) -> bool: + base_name = impacted_file.base_name + head_name = impacted_file.head_name + return base_name is not None and head_name is None + + +@impacted_file_bindable.field("missesCount") +def resolve_misses_count(impacted_file: ImpactedFile, info) -> int: + return impacted_file.misses_count + + +impacted_files_result_bindable = UnionType("ImpactedFilesResult") + + +@impacted_files_result_bindable.type_resolver +def resolve_files_result_type(res, *_): + if isinstance(res, UnknownFlags): + return "UnknownFlags" + elif isinstance(res, type({"results": List})): + return "ImpactedFiles" diff --git a/apps/codecov-api/graphql_api/types/inputs/activate_measurements.graphql b/apps/codecov-api/graphql_api/types/inputs/activate_measurements.graphql new file mode 100644 index 0000000000..54597a9beb --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/activate_measurements.graphql @@ -0,0 +1,5 @@ +input ActivateMeasurementsInput { + owner: String! + repoName: String! + measurementType: MeasurementType! +} diff --git a/apps/codecov-api/graphql_api/types/inputs/branches_set_filters.graphql b/apps/codecov-api/graphql_api/types/inputs/branches_set_filters.graphql new file mode 100644 index 0000000000..7e982bae80 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/branches_set_filters.graphql @@ -0,0 +1,4 @@ +input BranchesSetFilters { + searchValue: String + mergedBranches: Boolean +} diff --git a/apps/codecov-api/graphql_api/types/inputs/bundle_analysis_cache_config.graphql b/apps/codecov-api/graphql_api/types/inputs/bundle_analysis_cache_config.graphql new file mode 100644 index 0000000000..5500da6b1a --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/bundle_analysis_cache_config.graphql @@ -0,0 +1,10 @@ +input BundleCacheConfigInput { + bundleName: String! + toggleCaching: Boolean! +} + +input UpdateBundleCacheConfigInput { + owner: String! + repoName: String! + bundles: [BundleCacheConfigInput!]! +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/inputs/bundle_analysis_filters.graphql b/apps/codecov-api/graphql_api/types/inputs/bundle_analysis_filters.graphql new file mode 100644 index 0000000000..ea79fa2fd4 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/bundle_analysis_filters.graphql @@ -0,0 +1,12 @@ +input BundleAnalysisReportFilters { + reportGroups: [BundleReportGroups!] + loadTypes: [BundleLoadTypes!] +} + +input BundleAnalysisMeasurementsSetFilters { + assetTypes: [BundleAnalysisMeasurementsAssetType!] +} + +input BundleReportFilters { + reportGroup: BundleReportGroups +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/inputs/cancel_trial.graphql b/apps/codecov-api/graphql_api/types/inputs/cancel_trial.graphql new file mode 100644 index 0000000000..82fb15461e --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/cancel_trial.graphql @@ -0,0 +1,3 @@ +input CancelTrialInput { + orgUsername: String +} diff --git a/apps/codecov-api/graphql_api/types/inputs/commit_set_filters.graphql b/apps/codecov-api/graphql_api/types/inputs/commit_set_filters.graphql new file mode 100644 index 0000000000..ce9d88542a --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/commit_set_filters.graphql @@ -0,0 +1,8 @@ +input CommitsSetFilters { + hideFailedCI: Boolean + branchName: String + pullId: Int + search: String + states: [CommitState!] + coverageStatus: [CommitStatus!] +} diff --git a/apps/codecov-api/graphql_api/types/inputs/components_filters.graphql b/apps/codecov-api/graphql_api/types/inputs/components_filters.graphql new file mode 100644 index 0000000000..d89964e8bf --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/components_filters.graphql @@ -0,0 +1,7 @@ +input ComponentsFilters { + components: [String!] +} + +input ComponentMeasurementsSetFilters { + components: [String] +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/inputs/create_api_token.graphql b/apps/codecov-api/graphql_api/types/inputs/create_api_token.graphql new file mode 100644 index 0000000000..60faf99e15 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/create_api_token.graphql @@ -0,0 +1,3 @@ +input CreateApiTokenInput { + name: String! +} diff --git a/apps/codecov-api/graphql_api/types/inputs/create_stripe_setup_intent.graphql b/apps/codecov-api/graphql_api/types/inputs/create_stripe_setup_intent.graphql new file mode 100644 index 0000000000..5fe147ba5b --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/create_stripe_setup_intent.graphql @@ -0,0 +1,3 @@ +input CreateStripeSetupIntentInput { + owner: String! +} diff --git a/apps/codecov-api/graphql_api/types/inputs/create_user_token.graphql b/apps/codecov-api/graphql_api/types/inputs/create_user_token.graphql new file mode 100644 index 0000000000..3af1753fe1 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/create_user_token.graphql @@ -0,0 +1,4 @@ +input CreateUserTokenInput { + name: String! + tokenType: String +} diff --git a/apps/codecov-api/graphql_api/types/inputs/delete_component_measurements.graphql b/apps/codecov-api/graphql_api/types/inputs/delete_component_measurements.graphql new file mode 100644 index 0000000000..81fd122ec1 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/delete_component_measurements.graphql @@ -0,0 +1,5 @@ +input DeleteComponentMeasurementsInput { + ownerUsername: String! + repoName: String! + componentId: String! +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/inputs/delete_flag.graphql b/apps/codecov-api/graphql_api/types/inputs/delete_flag.graphql new file mode 100644 index 0000000000..c0ca94206d --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/delete_flag.graphql @@ -0,0 +1,5 @@ +input DeleteFlagInput { + ownerUsername: String! + repoName: String! + flagName: String! +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/inputs/delete_session.graphql b/apps/codecov-api/graphql_api/types/inputs/delete_session.graphql new file mode 100644 index 0000000000..69cc894179 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/delete_session.graphql @@ -0,0 +1,3 @@ +input DeleteSessionInput { + sessionid: Int! +} diff --git a/apps/codecov-api/graphql_api/types/inputs/encode_secret_string.graphql b/apps/codecov-api/graphql_api/types/inputs/encode_secret_string.graphql new file mode 100644 index 0000000000..2ea2e0910a --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/encode_secret_string.graphql @@ -0,0 +1,4 @@ +input EncodeSecretStringInput { + repoName: String! + value: String! +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/inputs/flag_comparison_filters.graphql b/apps/codecov-api/graphql_api/types/inputs/flag_comparison_filters.graphql new file mode 100644 index 0000000000..1e368014a7 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/flag_comparison_filters.graphql @@ -0,0 +1,3 @@ +input FlagComparisonFilters { + term: String +} diff --git a/apps/codecov-api/graphql_api/types/inputs/flag_set_filters.graphql b/apps/codecov-api/graphql_api/types/inputs/flag_set_filters.graphql new file mode 100644 index 0000000000..6dcc6b2557 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/flag_set_filters.graphql @@ -0,0 +1,4 @@ +input FlagSetFilters { + term: String + flagsNames: [String] +} diff --git a/apps/codecov-api/graphql_api/types/inputs/impacted_files_filters.graphql b/apps/codecov-api/graphql_api/types/inputs/impacted_files_filters.graphql new file mode 100644 index 0000000000..2ca3d2d932 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/impacted_files_filters.graphql @@ -0,0 +1,11 @@ +input ImpactedFilesFilters { + ordering: ImpactedFilesOrdering + hasUnintendedChanges: Boolean + flags: [String!] + components: [String!] +} + +input ImpactedFilesOrdering { + direction: OrderingDirection + parameter: ImpactedFileParameter +} diff --git a/apps/codecov-api/graphql_api/types/inputs/measurement_set_filters.graphql b/apps/codecov-api/graphql_api/types/inputs/measurement_set_filters.graphql new file mode 100644 index 0000000000..ba8bbc8595 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/measurement_set_filters.graphql @@ -0,0 +1,7 @@ +input MeasurementSetFilters { + repoId: Int + branch: String + flagId: Int + after: DateTime! + before: DateTime! +} diff --git a/apps/codecov-api/graphql_api/types/inputs/onboard_user.graphql b/apps/codecov-api/graphql_api/types/inputs/onboard_user.graphql new file mode 100644 index 0000000000..4574144e55 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/onboard_user.graphql @@ -0,0 +1,7 @@ +input OnboardUserInput { + email: String + businessEmail: String + typeProjects: [TypeProjectOnboarding]! + goals: [GoalOnboarding]! + otherGoal: String +} diff --git a/apps/codecov-api/graphql_api/types/inputs/organization_set_filters.graphql b/apps/codecov-api/graphql_api/types/inputs/organization_set_filters.graphql new file mode 100644 index 0000000000..2ce97fd32b --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/organization_set_filters.graphql @@ -0,0 +1,3 @@ +input OrganizationSetFilters { + term: String +} diff --git a/apps/codecov-api/graphql_api/types/inputs/path_contents_filters.graphql b/apps/codecov-api/graphql_api/types/inputs/path_contents_filters.graphql new file mode 100644 index 0000000000..70be00e30e --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/path_contents_filters.graphql @@ -0,0 +1,12 @@ +input PathContentsFilters { + searchValue: String + displayType: PathContentDisplayType + ordering: PathContentsOrdering + flags: [String!] + components: [String!] +} + +input PathContentsOrdering { + direction: OrderingDirection + parameter: OrderingParameter +} diff --git a/apps/codecov-api/graphql_api/types/inputs/pulls_set_filters.graphql b/apps/codecov-api/graphql_api/types/inputs/pulls_set_filters.graphql new file mode 100644 index 0000000000..8cd23efbad --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/pulls_set_filters.graphql @@ -0,0 +1,3 @@ +input PullsSetFilters { + state: [PullRequestState] +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/inputs/regenerate_org_upload_token.graphql b/apps/codecov-api/graphql_api/types/inputs/regenerate_org_upload_token.graphql new file mode 100644 index 0000000000..363c8a6b4f --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/regenerate_org_upload_token.graphql @@ -0,0 +1,3 @@ +input RegenerateOrgUploadTokenInput { + owner: String! +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/inputs/regenerate_repository_token.graphql b/apps/codecov-api/graphql_api/types/inputs/regenerate_repository_token.graphql new file mode 100644 index 0000000000..707ae7084a --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/regenerate_repository_token.graphql @@ -0,0 +1,5 @@ +input RegenerateRepositoryTokenInput { + owner: String! + repoName: String! + tokenType: RepositoryTokenType! +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/inputs/regenerate_repository_upload_token.graphql b/apps/codecov-api/graphql_api/types/inputs/regenerate_repository_upload_token.graphql new file mode 100644 index 0000000000..d0d290508d --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/regenerate_repository_upload_token.graphql @@ -0,0 +1,4 @@ +input RegenerateRepositoryUploadTokenInput { + repoName: String! + owner: String! +} diff --git a/apps/codecov-api/graphql_api/types/inputs/repository_set_filters.graphql b/apps/codecov-api/graphql_api/types/inputs/repository_set_filters.graphql new file mode 100644 index 0000000000..4669688398 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/repository_set_filters.graphql @@ -0,0 +1,8 @@ +input RepositorySetFilters { + term: String + repoNames: [String] + active: Boolean + activated: Boolean + isPublic: Boolean + aiEnabled: Boolean +} diff --git a/apps/codecov-api/graphql_api/types/inputs/revoke_user_token.graphql b/apps/codecov-api/graphql_api/types/inputs/revoke_user_token.graphql new file mode 100644 index 0000000000..9f5b768738 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/revoke_user_token.graphql @@ -0,0 +1,3 @@ +input RevokeUserTokenInput { + tokenid: String! +} diff --git a/apps/codecov-api/graphql_api/types/inputs/save_sentry_state.graphql b/apps/codecov-api/graphql_api/types/inputs/save_sentry_state.graphql new file mode 100644 index 0000000000..ab24c40a89 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/save_sentry_state.graphql @@ -0,0 +1,3 @@ +input SaveSentryStateInput { + state: String! +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/inputs/segments_filter.graphql b/apps/codecov-api/graphql_api/types/inputs/segments_filter.graphql new file mode 100644 index 0000000000..f848e272d1 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/segments_filter.graphql @@ -0,0 +1,3 @@ +input SegmentsFilters { + hasUnintendedChanges: Boolean +} diff --git a/apps/codecov-api/graphql_api/types/inputs/set_yaml_on_owner.graphql b/apps/codecov-api/graphql_api/types/inputs/set_yaml_on_owner.graphql new file mode 100644 index 0000000000..d3d08044ac --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/set_yaml_on_owner.graphql @@ -0,0 +1,4 @@ +input SetYamlOnOwnerInput { + username: String! + yaml: String! +} diff --git a/apps/codecov-api/graphql_api/types/inputs/start_trial.graphql b/apps/codecov-api/graphql_api/types/inputs/start_trial.graphql new file mode 100644 index 0000000000..0d18c044b9 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/start_trial.graphql @@ -0,0 +1,3 @@ +input StartTrialInput { + orgUsername: String +} diff --git a/apps/codecov-api/graphql_api/types/inputs/test_results_filters.graphql b/apps/codecov-api/graphql_api/types/inputs/test_results_filters.graphql new file mode 100644 index 0000000000..5358ccd9ac --- /dev/null +++ b/apps/codecov-api/graphql_api/types/inputs/test_results_filters.graphql @@ -0,0 +1,13 @@ +input TestResultsFilters { + branch: String + parameter: TestResultsFilterParameter + test_suites: [String!] + flags: [String!] + interval: MeasurementInterval + term: String +} + +input TestResultsOrdering { + direction: OrderingDirection + parameter: TestResultsOrderingParameter +} diff --git a/apps/codecov-api/graphql_api/types/invoice/__init__.py b/apps/codecov-api/graphql_api/types/invoice/__init__.py new file mode 100644 index 0000000000..1b1a23413e --- /dev/null +++ b/apps/codecov-api/graphql_api/types/invoice/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .invoice import invoice_bindable + +invoice = ariadne_load_local_graphql(__file__, "invoice.graphql") + + +__all__ = ["invoice_bindable"] diff --git a/apps/codecov-api/graphql_api/types/invoice/invoice.graphql b/apps/codecov-api/graphql_api/types/invoice/invoice.graphql new file mode 100644 index 0000000000..ef11d76607 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/invoice/invoice.graphql @@ -0,0 +1,71 @@ +type Invoice { + amountDue: Float! + amountPaid: Float! + created: Int! + currency: String! + customerAddress: String + customerEmail: String + customerName: String + defaultPaymentMethod: PaymentMethod + dueDate: Int + footer: String + id: String! + lineItems: [LineItem]! + number: String + periodEnd: Int! + periodStart: Int! + status: String + subtotal: Float! + total: Float! + taxIds: [TaxInfo] +} + +type LineItem { + amount: Float! + currency: String! + description: String! +} + +type Period { + end: Int! + start: Int! +} + +type PaymentMethod { + billingDetails: BillingDetails + card: Card + usBankAccount: USBankAccount +} + +type Card { + brand: String + expMonth: Int + expYear: Int + last4: String +} + +type USBankAccount { + bankName: String + last4: String +} + +type BillingDetails { + address: Address + email: String + name: String + phone: String +} + +type Address { + city: String + country: String + line1: String + line2: String + postalCode: String + state: String +} + +type TaxInfo { + type: String! + value: String! +} diff --git a/apps/codecov-api/graphql_api/types/invoice/invoice.py b/apps/codecov-api/graphql_api/types/invoice/invoice.py new file mode 100644 index 0000000000..fedb1ea857 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/invoice/invoice.py @@ -0,0 +1,119 @@ +from ariadne import ObjectType +from graphql import GraphQLResolveInfo +from stripe import ( + Invoice, + InvoiceLineItem, + ListObject, + PaymentMethod, +) + +invoice_bindable = ObjectType("Invoice") + + +@invoice_bindable.field("id") +def resolve_invoice_id(invoice: Invoice, info: GraphQLResolveInfo) -> str | None: + return invoice["id"] + + +@invoice_bindable.field("number") +def resolve_invoice_number(invoice: Invoice, info: GraphQLResolveInfo) -> str | None: + return invoice["number"] + + +@invoice_bindable.field("status") +def resolve_invoice_status(invoice: Invoice, info: GraphQLResolveInfo) -> str | None: + return invoice["status"] + + +@invoice_bindable.field("created") +def resolve_invoice_created(invoice: Invoice, info: GraphQLResolveInfo) -> int: + return invoice["created"] + + +@invoice_bindable.field("periodStart") +def resolve_invoice_period_start(invoice: Invoice, info: GraphQLResolveInfo) -> int: + return invoice["period_start"] + + +@invoice_bindable.field("periodEnd") +def resolve_invoice_period_end(invoice: Invoice, info: GraphQLResolveInfo) -> int: + return invoice["period_end"] + + +@invoice_bindable.field("dueDate") +def resolve_invoice_due_date(invoice: Invoice, info: GraphQLResolveInfo) -> int | None: + return invoice["due_date"] + + +@invoice_bindable.field("customerName") +def resolve_invoice_customer_name( + invoice: Invoice, info: GraphQLResolveInfo +) -> str | None: + return invoice["customer_name"] + + +# NOTE: Not currently used in Gazebo, keep for address updates +@invoice_bindable.field("customerAddress") +def resolve_invoice_customer_address( + invoice: Invoice, info: GraphQLResolveInfo +) -> str | None: + if invoice["customer_address"]: + return str(invoice["customer_address"]) + return None + + +# NOTE: Not currently used in Gazebo, keep for tax id updates +@invoice_bindable.field("currency") +def resolve_invoice_currency(invoice: Invoice, info: GraphQLResolveInfo) -> str: + return invoice["currency"] + + +@invoice_bindable.field("amountPaid") +def resolve_invoice_amount_paid(invoice: Invoice, info: GraphQLResolveInfo) -> int: + return invoice["amount_paid"] + + +@invoice_bindable.field("amountDue") +def resolve_invoice_amount_due(invoice: Invoice, info: GraphQLResolveInfo) -> int: + return invoice["amount_due"] + + +@invoice_bindable.field("total") +def resolve_invoice_total(invoice: Invoice, info: GraphQLResolveInfo) -> int: + return invoice["total"] + + +@invoice_bindable.field("subtotal") +def resolve_invoice_subtotal(invoice: Invoice, info: GraphQLResolveInfo) -> int: + return invoice["subtotal"] + + +@invoice_bindable.field("lineItems") +def resolve_invoice_line_items( + invoice: Invoice, info: GraphQLResolveInfo +) -> ListObject[InvoiceLineItem]: + return invoice["lines"]["data"] + + +@invoice_bindable.field("footer") +def resolve_invoice_footer(invoice: Invoice, info: GraphQLResolveInfo) -> str | None: + return invoice["footer"] + + +@invoice_bindable.field("customerEmail") +def resolve_invoice_customer_email( + invoice: Invoice, info: GraphQLResolveInfo +) -> str | None: + return invoice["customer_email"] + + +@invoice_bindable.field("defaultPaymentMethod") +def resolve_invoice_default_payment_method( + invoice: Invoice, info: GraphQLResolveInfo +) -> PaymentMethod | None: + return invoice["default_payment_method"] + + +@invoice_bindable.field("taxIds") +def resolve_invoice_tax_ids(invoice: Invoice, info: GraphQLResolveInfo) -> list: + return invoice["customer_tax_ids"] diff --git a/apps/codecov-api/graphql_api/types/line_comparison/__init__.py b/apps/codecov-api/graphql_api/types/line_comparison/__init__.py new file mode 100644 index 0000000000..b8f87ab586 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/line_comparison/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .line_comparison import line_comparison_bindable + +line_comparison = ariadne_load_local_graphql(__file__, "line_comparison.graphql") + + +__all__ = ["line_comparison_bindable"] diff --git a/apps/codecov-api/graphql_api/types/line_comparison/line_comparison.graphql b/apps/codecov-api/graphql_api/types/line_comparison/line_comparison.graphql new file mode 100644 index 0000000000..796d64e788 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/line_comparison/line_comparison.graphql @@ -0,0 +1,13 @@ +type CoverageInfo { + hitCount: Int + hitUploadIds: [Int!] +} + +type LineComparison { + baseNumber: String + headNumber: String + baseCoverage: CoverageLine + headCoverage: CoverageLine + content: String + coverageInfo(ignoredUploadIds: [Int!]): CoverageInfo! +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/line_comparison/line_comparison.py b/apps/codecov-api/graphql_api/types/line_comparison/line_comparison.py new file mode 100644 index 0000000000..2692118de5 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/line_comparison/line_comparison.py @@ -0,0 +1,80 @@ +from functools import cached_property +from typing import List, Optional + +from ariadne import ObjectType +from shared.utils.merge import LineType + +from graphql_api.types.enums import CoverageLine +from services.comparison import LineComparison + +line_coverages = { + LineType.hit: CoverageLine.H, + LineType.miss: CoverageLine.M, + LineType.partial: CoverageLine.P, +} + + +class CoverageInfo: + def __init__( + self, + line_comparison: LineComparison, + ignored_upload_ids: Optional[List[int]] = None, + ): + self.line_comparison = line_comparison + self.ignored_upload_ids = set(ignored_upload_ids or []) + + @cached_property + def hit_count(self): + upload_ids = self.hit_upload_ids + if upload_ids is not None: + return len(upload_ids) + + @cached_property + def hit_upload_ids(self): + upload_ids = self.line_comparison.hit_session_ids + if upload_ids is not None: + return set(upload_ids) - self.ignored_upload_ids + + +line_comparison_bindable = ObjectType("LineComparison") + + +@line_comparison_bindable.field("baseNumber") +def resolve_base_number(line_comparison: LineComparison, info) -> Optional[str]: + return line_comparison.number["base"] + + +@line_comparison_bindable.field("headNumber") +def resolve_head_number(line_comparison: LineComparison, info) -> Optional[str]: + return line_comparison.number["head"] + + +@line_comparison_bindable.field("baseCoverage") +def resolve_base_coverage(line_comparison: LineComparison, info) -> Optional[str]: + line_type: LineType = line_comparison.coverage["base"] + if line_type is not None: + return line_coverages.get(line_type) + + +@line_comparison_bindable.field("headCoverage") +def resolve_head_coverage(line_comparison: LineComparison, info) -> Optional[str]: + line_type: LineType = line_comparison.coverage["head"] + if line_type is not None: + return line_coverages.get(line_type) + + +@line_comparison_bindable.field("content") +def resolve_content(line_comparison: LineComparison, info) -> str: + value = line_comparison.value + if value and line_comparison.is_diff: + return f"{value[0]} {value[1:]}" + return f" {value}" + + +@line_comparison_bindable.field("coverageInfo") +def resolve_coverage_info( + line_comparison: LineComparison, + info, + ignored_upload_ids: Optional[List[int]] = None, +) -> CoverageInfo: + return CoverageInfo(line_comparison, ignored_upload_ids=ignored_upload_ids) diff --git a/apps/codecov-api/graphql_api/types/me/__init__.py b/apps/codecov-api/graphql_api/types/me/__init__.py new file mode 100644 index 0000000000..f589aad98d --- /dev/null +++ b/apps/codecov-api/graphql_api/types/me/__init__.py @@ -0,0 +1,3 @@ +from .me import me, me_bindable, tracking_metadata_bindable + +__all__ = ["me", "me_bindable", "tracking_metadata_bindable"] diff --git a/apps/codecov-api/graphql_api/types/me/me.graphql b/apps/codecov-api/graphql_api/types/me/me.graphql new file mode 100644 index 0000000000..aa9e0c2c2a --- /dev/null +++ b/apps/codecov-api/graphql_api/types/me/me.graphql @@ -0,0 +1,60 @@ +type Me { + email: String + businessEmail: String + onboardingCompleted: Boolean! + user: User! + owner: Owner! + sessions( + first: Int + after: String + last: Int + before: String + ): SessionConnection! @cost(complexity: 7, multipliers: ["first", "last"]) + tokens( + first: Int + after: String + last: Int + before: String + ): UserTokenConnection! @cost(complexity: 5, multipliers: ["first", "last"]) + viewableRepositories( + filters: RepositorySetFilters + ordering: RepositoryOrdering + orderingDirection: OrderingDirection + first: Int + after: String + last: Int + before: String + ): ViewableRepositoryConnection! + @cost(complexity: 25, multipliers: ["first", "last"]) + myOrganizations( + filters: OrganizationSetFilters + first: Int + after: String + last: Int + before: String + ): MyOrganizationConnection! + @cost(complexity: 15, multipliers: ["first", "last"]) + isSyncingWithGitProvider: Boolean! + trackingMetadata: trackingMetadata! # temporary solution to expose the user metadata + privateAccess: Boolean + termsAgreement: Boolean +} + +# Temporary type to gather the different attributes for tracking until we have +# a valid place in the graph for those attributes +type trackingMetadata { + ownerid: Int! + serviceId: String! + plan: String + staff: Boolean + hasYaml: Boolean! + service: String! + bot: String + delinquent: Boolean + didTrial: Boolean + planProvider: String + planUserCount: Int + createstamp: DateTime + updatestamp: DateTime + profile: Profile +} diff --git a/apps/codecov-api/graphql_api/types/me/me.py b/apps/codecov-api/graphql_api/types/me/me.py new file mode 100644 index 0000000000..f08b0ee081 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/me/me.py @@ -0,0 +1,147 @@ +from typing import Optional + +import sentry_sdk +from ariadne import ObjectType +from asgiref.sync import sync_to_async +from graphql import GraphQLResolveInfo + +from codecov_auth.models import Owner, OwnerProfile +from codecov_auth.views.okta_cloud import OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY +from graphql_api.actions.owner import ( + get_owner_login_sessions, + get_user_tokens, + search_my_owners, +) +from graphql_api.actions.repository import search_repos +from graphql_api.helpers.ariadne import ariadne_load_local_graphql +from graphql_api.helpers.connection import ( + build_connection_graphql, + queryset_to_connection, +) +from graphql_api.types.enums import OrderingDirection, RepositoryOrdering + +me = ariadne_load_local_graphql(__file__, "me.graphql") +me = me + build_connection_graphql("ViewableRepositoryConnection", "Repository") +me = me + build_connection_graphql("MyOrganizationConnection", "Owner") +me = me + build_connection_graphql("SessionConnection", "Session") +me = me + build_connection_graphql("UserTokenConnection", "UserToken") +me_bindable = ObjectType("Me") + + +@me_bindable.field("user") +def resolve_user(user, _): + return user + + +@me_bindable.field("owner") +def resolve_owner(user, _): + """ + Current user is also an owner in which we can fetch repositories + """ + return user + + +@me_bindable.field("viewableRepositories") +@sentry_sdk.trace +def resolve_viewable_repositories( + current_user, + info: GraphQLResolveInfo, + filters=None, + ordering=RepositoryOrdering.ID, + ordering_direction=OrderingDirection.ASC, + **kwargs, +): + okta_authenticated_accounts: list[int] = info.context["request"].session.get( + OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY, [] + ) + is_impersonation = info.context["request"].impersonation + # If the user is impersonating another user, we want to show all the Okta repos. + # This means we do not want to filter out the Okta enforced repos + exclude_okta_enforced_repos = not is_impersonation + + queryset = search_repos( + current_user, filters, okta_authenticated_accounts, exclude_okta_enforced_repos + ) + return queryset_to_connection( + queryset, + ordering=(ordering, RepositoryOrdering.ID), + ordering_direction=ordering_direction, + **kwargs, + ) + + +@me_bindable.field("myOrganizations") +def resolve_my_organizations(current_user, _, filters=None, **kwargs): + queryset = search_my_owners(current_user, filters) + return queryset_to_connection( + queryset, + ordering=("ownerid",), + ordering_direction=OrderingDirection.DESC, + **kwargs, + ) + + +@me_bindable.field("sessions") +def resolve_sessions(current_user, _, **kwargs): + queryset = get_owner_login_sessions(current_user) + return queryset_to_connection( + queryset, + ordering=("sessionid",), + ordering_direction=OrderingDirection.DESC, + **kwargs, + ) + + +@me_bindable.field("tokens") +def resolve_tokens(current_user, _, **kwargs): + queryset = get_user_tokens(current_user) + return queryset_to_connection( + queryset, + ordering=("created_at",), + ordering_direction=OrderingDirection.DESC, + **kwargs, + ) + + +@me_bindable.field("isSyncingWithGitProvider") +def resolve_is_syncing_with_git_provider(_, info): + command = info.context["executor"].get_command("owner") + return command.is_syncing() + + +@me_bindable.field("trackingMetadata") +def resolve_tracking_data(current_user, _, **kwargs): + return current_user + + +@me_bindable.field("termsAgreement") +@sync_to_async +def resolve_terms_agreement(current_owner: Owner, _, **kwargs) -> Optional[bool]: + if current_owner.user is None: + return None + return current_owner.user.terms_agreement + + +@me_bindable.field("businessEmail") +def resolve_business_email(current_owner: Owner, _, **kwargs) -> Optional[str]: + return current_owner.business_email + + +@me_bindable.field("privateAccess") +@sync_to_async +def resolve_private_access(owner: Owner, info) -> bool: + if owner.private_access is None: + return False + return owner.private_access + + +tracking_metadata_bindable = ObjectType("trackingMetadata") + + +@tracking_metadata_bindable.field("profile") +@sync_to_async +def resolve_profile(owner: Owner, info) -> OwnerProfile: + try: + return owner.profile + except OwnerProfile.DoesNotExist: + return None diff --git a/apps/codecov-api/graphql_api/types/measurement/__init__.py b/apps/codecov-api/graphql_api/types/measurement/__init__.py new file mode 100644 index 0000000000..8ce7990a1b --- /dev/null +++ b/apps/codecov-api/graphql_api/types/measurement/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .measurement import measurement_bindable + +measurement = ariadne_load_local_graphql(__file__, "measurement.graphql") + + +__all__ = ["measurement_bindable"] diff --git a/apps/codecov-api/graphql_api/types/measurement/measurement.graphql b/apps/codecov-api/graphql_api/types/measurement/measurement.graphql new file mode 100644 index 0000000000..0a13b57275 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/measurement/measurement.graphql @@ -0,0 +1,6 @@ +type Measurement { + timestamp: DateTime! + avg: Float + min: Float + max: Float +} diff --git a/apps/codecov-api/graphql_api/types/measurement/measurement.py b/apps/codecov-api/graphql_api/types/measurement/measurement.py new file mode 100644 index 0000000000..198c240125 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/measurement/measurement.py @@ -0,0 +1,26 @@ +from datetime import datetime +from typing import Mapping + +from ariadne import ObjectType + +measurement_bindable = ObjectType("Measurement") + + +@measurement_bindable.field("timestamp") +def resolve_timestamp(measurement: Mapping, info) -> datetime: + return measurement["timestamp_bin"] + + +@measurement_bindable.field("avg") +def resolve_avg(measurement: Mapping, info) -> float: + return measurement["avg"] + + +@measurement_bindable.field("min") +def resolve_min(measurement: Mapping, info) -> float: + return measurement["min"] + + +@measurement_bindable.field("max") +def resolve_max(measurement: Mapping, info) -> float: + return measurement["max"] diff --git a/apps/codecov-api/graphql_api/types/mutation/__init__.py b/apps/codecov-api/graphql_api/types/mutation/__init__.py new file mode 100644 index 0000000000..24ddd033ff --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/__init__.py @@ -0,0 +1,61 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .activate_measurements import gql_activate_measurements +from .cancel_trial import gql_cancel_trial +from .create_api_token import gql_create_api_token +from .create_stripe_setup_intent import gql_create_stripe_setup_intent +from .create_user_token import gql_create_user_token +from .delete_component_measurements import gql_delete_component_measurements +from .delete_flag import gql_delete_flag +from .delete_session import gql_delete_session +from .encode_secret_string import gql_encode_secret_string +from .erase_repository import gql_erase_repository +from .mutation import mutation_resolvers # noqa: F401 +from .onboard_user import gql_onboard_user +from .regenerate_org_upload_token import gql_regenerate_org_upload_token +from .regenerate_repository_token import gql_regenerate_repository_token +from .regenerate_repository_upload_token import gql_regenerate_repository_upload_token +from .revoke_user_token import gql_revoke_user_token +from .save_okta_config import gql_save_okta_config +from .save_sentry_state import gql_save_sentry_state +from .save_terms_agreement import gql_save_terms_agreement +from .set_upload_token_required import gql_set_upload_token_required +from .set_yaml_on_owner import gql_set_yaml_on_owner +from .start_trial import gql_start_trial +from .store_event_metrics import gql_store_event_metrics +from .sync_with_git_provider import gql_sync_with_git_provider +from .update_bundle_cache_config import gql_update_bundle_cache_config +from .update_default_organization import gql_update_default_organization +from .update_profile import gql_update_profile +from .update_repository import gql_update_repository +from .update_self_hosted_settings import gql_update_self_hosted_settings + +mutation = ariadne_load_local_graphql(__file__, "mutation.graphql") +mutation = mutation + gql_create_api_token +mutation = mutation + gql_create_stripe_setup_intent +mutation = mutation + gql_sync_with_git_provider +mutation = mutation + gql_delete_session +mutation = mutation + gql_set_yaml_on_owner +mutation = mutation + gql_update_profile +mutation = mutation + gql_update_default_organization +mutation = mutation + gql_onboard_user +mutation = mutation + gql_regenerate_repository_token +mutation = mutation + gql_activate_measurements +mutation = mutation + gql_regenerate_org_upload_token +mutation = mutation + gql_create_user_token +mutation = mutation + gql_revoke_user_token +mutation = mutation + gql_delete_flag +mutation = mutation + gql_save_sentry_state +mutation = mutation + gql_save_terms_agreement +mutation = mutation + gql_start_trial +mutation = mutation + gql_cancel_trial +mutation = mutation + gql_delete_component_measurements +mutation = mutation + gql_erase_repository +mutation = mutation + gql_update_repository +mutation = mutation + gql_update_self_hosted_settings +mutation = mutation + gql_regenerate_repository_upload_token +mutation = mutation + gql_encode_secret_string +mutation = mutation + gql_store_event_metrics +mutation = mutation + gql_save_okta_config +mutation = mutation + gql_set_upload_token_required +mutation = mutation + gql_update_bundle_cache_config diff --git a/apps/codecov-api/graphql_api/types/mutation/activate_measurements/__init__.py b/apps/codecov-api/graphql_api/types/mutation/activate_measurements/__init__.py new file mode 100644 index 0000000000..104281a26d --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/activate_measurements/__init__.py @@ -0,0 +1,13 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .activate_measurements import ( + error_activate_measurements, + resolve_activate_measurements, +) + +gql_activate_measurements = ariadne_load_local_graphql( + __file__, "activate_measurements.graphql" +) + + +__all__ = ["error_activate_measurements", "resolve_activate_measurements"] diff --git a/apps/codecov-api/graphql_api/types/mutation/activate_measurements/activate_measurements.graphql b/apps/codecov-api/graphql_api/types/mutation/activate_measurements/activate_measurements.graphql new file mode 100644 index 0000000000..7792a87f88 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/activate_measurements/activate_measurements.graphql @@ -0,0 +1,5 @@ +union ActivateMeasurementsError = UnauthenticatedError | ValidationError + +type activateMeasurementsPayload { + error: ActivateMeasurementsError +} diff --git a/apps/codecov-api/graphql_api/types/mutation/activate_measurements/activate_measurements.py b/apps/codecov-api/graphql_api/types/mutation/activate_measurements/activate_measurements.py new file mode 100644 index 0000000000..88aad675b2 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/activate_measurements/activate_measurements.py @@ -0,0 +1,24 @@ +from ariadne import UnionType + +from core.commands.repository import RepositoryCommands +from graphql_api.helpers.mutation import ( + require_authenticated, + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +@require_authenticated +async def resolve_activate_measurements(_, info, input): + command: RepositoryCommands = info.context["executor"].get_command("repository") + await command.activate_measurements( + owner_name=input.get("owner"), + repo_name=input.get("repo_name"), + measurement_type=input.get("measurement_type"), + ) + return None + + +error_activate_measurements = UnionType("ActivateMeasurementsError") +error_activate_measurements.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/cancel_trial/__init__.py b/apps/codecov-api/graphql_api/types/mutation/cancel_trial/__init__.py new file mode 100644 index 0000000000..826a34ff61 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/cancel_trial/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .cancel_trial import error_cancel_trial, resolve_cancel_trial + +gql_cancel_trial = ariadne_load_local_graphql(__file__, "cancel_trial.graphql") + + +__all__ = ["error_cancel_trial", "resolve_cancel_trial"] diff --git a/apps/codecov-api/graphql_api/types/mutation/cancel_trial/cancel_trial.graphql b/apps/codecov-api/graphql_api/types/mutation/cancel_trial/cancel_trial.graphql new file mode 100644 index 0000000000..ec0fc953fa --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/cancel_trial/cancel_trial.graphql @@ -0,0 +1,5 @@ +union CancelTrialError = UnauthenticatedError | ValidationError + +type CancelTrialPayload { + error: CancelTrialError +} diff --git a/apps/codecov-api/graphql_api/types/mutation/cancel_trial/cancel_trial.py b/apps/codecov-api/graphql_api/types/mutation/cancel_trial/cancel_trial.py new file mode 100644 index 0000000000..b6f0f59d36 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/cancel_trial/cancel_trial.py @@ -0,0 +1,20 @@ +from ariadne import UnionType + +from codecov_auth.commands.owner import OwnerCommands +from graphql_api.helpers.mutation import ( + require_authenticated, + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +@require_authenticated +async def resolve_cancel_trial(_, info, input) -> None: + command: OwnerCommands = info.context["executor"].get_command("owner") + await command.cancel_trial(input.get("org_username")) + return None + + +error_cancel_trial = UnionType("CancelTrialError") +error_cancel_trial.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/create_api_token/__init__.py b/apps/codecov-api/graphql_api/types/mutation/create_api_token/__init__.py new file mode 100644 index 0000000000..7b1ff06527 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/create_api_token/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .create_api_token import error_create_api_token, resolve_create_api_token + +gql_create_api_token = ariadne_load_local_graphql(__file__, "create_api_token.graphql") + + +__all__ = ["error_create_api_token", "resolve_create_api_token"] diff --git a/apps/codecov-api/graphql_api/types/mutation/create_api_token/create_api_token.graphql b/apps/codecov-api/graphql_api/types/mutation/create_api_token/create_api_token.graphql new file mode 100644 index 0000000000..7068fcd789 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/create_api_token/create_api_token.graphql @@ -0,0 +1,7 @@ +union CreateApiTokenError = UnauthenticatedError | ValidationError + +type CreateApiTokenPayload { + error: CreateApiTokenError + session: Session + fullToken: String +} diff --git a/apps/codecov-api/graphql_api/types/mutation/create_api_token/create_api_token.py b/apps/codecov-api/graphql_api/types/mutation/create_api_token/create_api_token.py new file mode 100644 index 0000000000..74f7a5a673 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/create_api_token/create_api_token.py @@ -0,0 +1,17 @@ +from ariadne import UnionType + +from graphql_api.helpers.mutation import ( + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +async def resolve_create_api_token(_, info, input): + command = info.context["executor"].get_command("owner") + session = await command.create_api_token(input.get("name")) + return {"session": session, "full_token": session.token} + + +error_create_api_token = UnionType("CreateApiTokenError") +error_create_api_token.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/create_stripe_setup_intent/__init__.py b/apps/codecov-api/graphql_api/types/mutation/create_stripe_setup_intent/__init__.py new file mode 100644 index 0000000000..967ae52579 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/create_stripe_setup_intent/__init__.py @@ -0,0 +1,12 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .create_stripe_setup_intent import ( + error_create_stripe_setup_intent, + resolve_create_stripe_setup_intent, +) + +gql_create_stripe_setup_intent = ariadne_load_local_graphql( + __file__, "create_stripe_setup_intent.graphql" +) + +__all__ = ["error_create_stripe_setup_intent", "resolve_create_stripe_setup_intent"] diff --git a/apps/codecov-api/graphql_api/types/mutation/create_stripe_setup_intent/create_stripe_setup_intent.graphql b/apps/codecov-api/graphql_api/types/mutation/create_stripe_setup_intent/create_stripe_setup_intent.graphql new file mode 100644 index 0000000000..9f33c57626 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/create_stripe_setup_intent/create_stripe_setup_intent.graphql @@ -0,0 +1,6 @@ +union CreateStripeSetupIntentError = UnauthenticatedError | UnauthorizedError | ValidationError + +type CreateStripeSetupIntentPayload { + error: CreateStripeSetupIntentError + clientSecret: String +} diff --git a/apps/codecov-api/graphql_api/types/mutation/create_stripe_setup_intent/create_stripe_setup_intent.py b/apps/codecov-api/graphql_api/types/mutation/create_stripe_setup_intent/create_stripe_setup_intent.py new file mode 100644 index 0000000000..0b1f0590e2 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/create_stripe_setup_intent/create_stripe_setup_intent.py @@ -0,0 +1,24 @@ +from typing import Any, Dict + +from ariadne import UnionType +from ariadne.types import GraphQLResolveInfo + +from graphql_api.helpers.mutation import ( + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +async def resolve_create_stripe_setup_intent( + _: Any, info: GraphQLResolveInfo, input: Dict[str, str] +) -> Dict[str, str]: + command = info.context["executor"].get_command("owner") + resp = await command.create_stripe_setup_intent(input.get("owner")) + return { + "client_secret": resp["client_secret"], + } + + +error_create_stripe_setup_intent = UnionType("CreateStripeSetupIntentError") +error_create_stripe_setup_intent.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/create_user_token/__init__.py b/apps/codecov-api/graphql_api/types/mutation/create_user_token/__init__.py new file mode 100644 index 0000000000..dd8b1a10b2 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/create_user_token/__init__.py @@ -0,0 +1,9 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .create_user_token import error_create_user_token, resolve_create_user_token + +gql_create_user_token = ariadne_load_local_graphql( + __file__, "create_user_token.graphql" +) + +__all__ = ["error_create_user_token", "resolve_create_user_token"] diff --git a/apps/codecov-api/graphql_api/types/mutation/create_user_token/create_user_token.graphql b/apps/codecov-api/graphql_api/types/mutation/create_user_token/create_user_token.graphql new file mode 100644 index 0000000000..463a8394d9 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/create_user_token/create_user_token.graphql @@ -0,0 +1,7 @@ +union CreateUserTokenError = UnauthenticatedError | ValidationError + +type CreateUserTokenPayload { + error: CreateUserTokenError + token: UserToken + fullToken: String +} diff --git a/apps/codecov-api/graphql_api/types/mutation/create_user_token/create_user_token.py b/apps/codecov-api/graphql_api/types/mutation/create_user_token/create_user_token.py new file mode 100644 index 0000000000..b012bd96c8 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/create_user_token/create_user_token.py @@ -0,0 +1,23 @@ +from ariadne import UnionType + +from graphql_api.helpers.mutation import ( + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +async def resolve_create_user_token(_, info, input): + command = info.context["executor"].get_command("owner") + user_token = await command.create_user_token( + name=input.get("name"), + token_type=input.get("token_type"), + ) + return { + "token": user_token, + "full_token": user_token.token, + } + + +error_create_user_token = UnionType("CreateUserTokenError") +error_create_user_token.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/delete_component_measurements/__init__.py b/apps/codecov-api/graphql_api/types/mutation/delete_component_measurements/__init__.py new file mode 100644 index 0000000000..656d8689c0 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/delete_component_measurements/__init__.py @@ -0,0 +1,16 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .delete_component_measurements import ( + error_delete_component_measurements, + resolve_delete_component_measurements, +) + +gql_delete_component_measurements = ariadne_load_local_graphql( + __file__, "delete_component_measurements.graphql" +) + + +__all__ = [ + "error_delete_component_measurements", + "resolve_delete_component_measurements", +] diff --git a/apps/codecov-api/graphql_api/types/mutation/delete_component_measurements/delete_component_measurements.graphql b/apps/codecov-api/graphql_api/types/mutation/delete_component_measurements/delete_component_measurements.graphql new file mode 100644 index 0000000000..ca23d438e3 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/delete_component_measurements/delete_component_measurements.graphql @@ -0,0 +1,5 @@ +union DeleteComponentMeasurementsError = UnauthenticatedError | UnauthorizedError | ValidationError | NotFoundError + +type DeleteComponentMeasurementsPayload { + error: DeleteComponentMeasurementsError +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/mutation/delete_component_measurements/delete_component_measurements.py b/apps/codecov-api/graphql_api/types/mutation/delete_component_measurements/delete_component_measurements.py new file mode 100644 index 0000000000..44aa4cfd3e --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/delete_component_measurements/delete_component_measurements.py @@ -0,0 +1,23 @@ +from ariadne import UnionType +from asgiref.sync import sync_to_async + +from core.commands.component import ComponentCommands +from graphql_api.helpers.mutation import ( + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +@sync_to_async +def resolve_delete_component_measurements(_, info, input): + command: ComponentCommands = info.context["executor"].get_command("component") + command.delete_component_measurements( + owner_username=input["owner_username"], + repo_name=input["repo_name"], + component_id=input["component_id"], + ) + + +error_delete_component_measurements = UnionType("DeleteComponentMeasurementsError") +error_delete_component_measurements.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/delete_flag/__init__.py b/apps/codecov-api/graphql_api/types/mutation/delete_flag/__init__.py new file mode 100644 index 0000000000..b4aead7635 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/delete_flag/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .delete_flag import error_delete_flag, resolve_delete_flag + +gql_delete_flag = ariadne_load_local_graphql(__file__, "delete_flag.graphql") + + +__all__ = ["error_delete_flag", "resolve_delete_flag"] diff --git a/apps/codecov-api/graphql_api/types/mutation/delete_flag/delete_flag.graphql b/apps/codecov-api/graphql_api/types/mutation/delete_flag/delete_flag.graphql new file mode 100644 index 0000000000..04745fe7b4 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/delete_flag/delete_flag.graphql @@ -0,0 +1,5 @@ +union DeleteFlagError = UnauthenticatedError | UnauthorizedError | ValidationError | NotFoundError + +type DeleteFlagPayload { + error: DeleteFlagError +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/mutation/delete_flag/delete_flag.py b/apps/codecov-api/graphql_api/types/mutation/delete_flag/delete_flag.py new file mode 100644 index 0000000000..ed8b72a2b8 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/delete_flag/delete_flag.py @@ -0,0 +1,23 @@ +from ariadne import UnionType +from asgiref.sync import sync_to_async + +from core.commands.flag import FlagCommands +from graphql_api.helpers.mutation import ( + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +@sync_to_async +def resolve_delete_flag(_, info, input): + command: FlagCommands = info.context["executor"].get_command("flag") + command.delete_flag( + owner_username=input["owner_username"], + repo_name=input["repo_name"], + flag_name=input["flag_name"], + ) + + +error_delete_flag = UnionType("DeleteFlagError") +error_delete_flag.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/delete_session/__init__.py b/apps/codecov-api/graphql_api/types/mutation/delete_session/__init__.py new file mode 100644 index 0000000000..a33094e0ca --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/delete_session/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .delete_session import error_delete_session, resolve_delete_session + +gql_delete_session = ariadne_load_local_graphql(__file__, "delete_session.graphql") + + +__all__ = ["error_delete_session", "resolve_delete_session"] diff --git a/apps/codecov-api/graphql_api/types/mutation/delete_session/delete_session.graphql b/apps/codecov-api/graphql_api/types/mutation/delete_session/delete_session.graphql new file mode 100644 index 0000000000..52406ba2a3 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/delete_session/delete_session.graphql @@ -0,0 +1,5 @@ +union DeleteSessionError = UnauthenticatedError + +type DeleteSessionPayload { + error: DeleteSessionError +} diff --git a/apps/codecov-api/graphql_api/types/mutation/delete_session/delete_session.py b/apps/codecov-api/graphql_api/types/mutation/delete_session/delete_session.py new file mode 100644 index 0000000000..167d666b68 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/delete_session/delete_session.py @@ -0,0 +1,17 @@ +from ariadne import UnionType + +from graphql_api.helpers.mutation import ( + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +async def resolve_delete_session(_, info, input): + command = info.context["executor"].get_command("owner") + await command.delete_session(input.get("sessionid")) + return None + + +error_delete_session = UnionType("DeleteSessionError") +error_delete_session.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/encode_secret_string/__init__.py b/apps/codecov-api/graphql_api/types/mutation/encode_secret_string/__init__.py new file mode 100644 index 0000000000..110dd77c94 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/encode_secret_string/__init__.py @@ -0,0 +1,12 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .encode_secret_string import ( + error_encode_secret_string, + resolve_encode_secret_string, +) + +gql_encode_secret_string = ariadne_load_local_graphql( + __file__, "encode_secret_string.graphql" +) + +__all__ = ["error_encode_secret_string", "resolve_encode_secret_string"] diff --git a/apps/codecov-api/graphql_api/types/mutation/encode_secret_string/encode_secret_string.graphql b/apps/codecov-api/graphql_api/types/mutation/encode_secret_string/encode_secret_string.graphql new file mode 100644 index 0000000000..03662a7167 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/encode_secret_string/encode_secret_string.graphql @@ -0,0 +1,6 @@ +union EncodeSecretStringError = ValidationError | UnauthenticatedError + +type EncodeSecretStringPayload { + error: EncodeSecretStringError + value: String +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/mutation/encode_secret_string/encode_secret_string.py b/apps/codecov-api/graphql_api/types/mutation/encode_secret_string/encode_secret_string.py new file mode 100644 index 0000000000..caaa1e5be9 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/encode_secret_string/encode_secret_string.py @@ -0,0 +1,24 @@ +from ariadne import UnionType + +from graphql_api.helpers.mutation import ( + require_authenticated, + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +@require_authenticated +async def resolve_encode_secret_string(_, info, input) -> None: + command = info.context["executor"].get_command("repository") + repo_name = input.get("repo_name") + value = input.get("value") + current_owner = info.context["request"].current_owner + value = command.encode_secret_string( + repo_name=repo_name, owner=current_owner, value=value + ) + return {"value": value} + + +error_encode_secret_string = UnionType("EraseRepositoryError") +error_encode_secret_string.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/erase_repository/__init__.py b/apps/codecov-api/graphql_api/types/mutation/erase_repository/__init__.py new file mode 100644 index 0000000000..1ad6b3e491 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/erase_repository/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .erase_repository import error_erase_repository, resolve_erase_repository + +gql_erase_repository = ariadne_load_local_graphql(__file__, "erase_repository.graphql") + + +__all__ = ["error_erase_repository", "resolve_erase_repository"] diff --git a/apps/codecov-api/graphql_api/types/mutation/erase_repository/erase_repository.graphql b/apps/codecov-api/graphql_api/types/mutation/erase_repository/erase_repository.graphql new file mode 100644 index 0000000000..e7025a58fa --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/erase_repository/erase_repository.graphql @@ -0,0 +1,10 @@ +union EraseRepositoryError = UnauthorizedError | ValidationError | UnauthenticatedError + +type EraseRepositoryPayload { + error: EraseRepositoryError +} + +input EraseRepositoryInput { + owner: String + repoName: String! +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/mutation/erase_repository/erase_repository.py b/apps/codecov-api/graphql_api/types/mutation/erase_repository/erase_repository.py new file mode 100644 index 0000000000..d321ce611f --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/erase_repository/erase_repository.py @@ -0,0 +1,29 @@ +from typing import Any + +from ariadne import UnionType +from graphql import GraphQLResolveInfo + +from graphql_api.helpers.mutation import ( + require_authenticated, + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +@require_authenticated +async def resolve_erase_repository( + _: Any, info: GraphQLResolveInfo, input: dict[str, Any] +) -> None: + command = info.context["executor"].get_command("repository") + current_owner = info.context["request"].current_owner + + owner_username = input.get("owner") or current_owner.username + repo_name = input.get("repo_name") + + await command.erase_repository(owner_username, repo_name) + return None + + +error_erase_repository = UnionType("EraseRepositoryError") +error_erase_repository.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/mutation.graphql b/apps/codecov-api/graphql_api/types/mutation/mutation.graphql new file mode 100644 index 0000000000..0c91bc768c --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/mutation.graphql @@ -0,0 +1,44 @@ +type Mutation { + createApiToken(input: CreateApiTokenInput!): CreateApiTokenPayload + createStripeSetupIntent(input: CreateStripeSetupIntentInput!): CreateStripeSetupIntentPayload + createUserToken(input: CreateUserTokenInput!): CreateUserTokenPayload + revokeUserToken(input: RevokeUserTokenInput!): RevokeUserTokenPayload + setYamlOnOwner(input: SetYamlOnOwnerInput!): SetYamlOnOwnerPayload + startTrial(input: StartTrialInput!): StartTrialPayload + cancelTrial(input: CancelTrialInput!): CancelTrialPayload + syncWithGitProvider: SyncWithGitProviderPayload + updateProfile(input: UpdateProfileInput!): UpdateProfilePayload + updateDefaultOrganization( + input: UpdateDefaultOrganizationInput! + ): UpdateDefaultOrganizationPayload + deleteSession(input: DeleteSessionInput!): DeleteSessionPayload + onboardUser(input: OnboardUserInput!): OnboardUserPayload + regenerateRepositoryToken( + input: RegenerateRepositoryTokenInput! + ): RegenerateRepositoryTokenPayload + activateMeasurements( + input: ActivateMeasurementsInput! + ): activateMeasurementsPayload + regenerateOrgUploadToken( + input: RegenerateOrgUploadTokenInput! + ): RegenerateOrgUploadTokenPayload + deleteFlag(input: DeleteFlagInput!): DeleteFlagPayload + saveSentryState(input: SaveSentryStateInput!): SaveSentryStatePayload + saveTermsAgreement(input: SaveTermsAgreementInput!): SaveTermsAgreementPayload + deleteComponentMeasurements( + input: DeleteComponentMeasurementsInput! + ): DeleteComponentMeasurementsPayload + eraseRepository(input: EraseRepositoryInput!): EraseRepositoryPayload + updateRepository(input: UpdateRepositoryInput!): UpdateRepositoryPayload + updateSelfHostedSettings( + input: UpdateSelfHostedSettingsInput! + ): UpdateSelfHostedSettingsPayload + regenerateRepositoryUploadToken( + input: RegenerateRepositoryUploadTokenInput! + ): RegenerateRepositoryUploadTokenPayload + encodeSecretString(input: EncodeSecretStringInput!): EncodeSecretStringPayload + storeEventMetric(input: StoreEventMetricsInput!): StoreEventMetricsPayload + saveOktaConfig(input: SaveOktaConfigInput!): SaveOktaConfigPayload + setUploadTokenRequired(input: SetUploadTokenRequiredInput!): SetUploadTokenRequiredPayload + updateBundleCacheConfig(input: UpdateBundleCacheConfigInput!): UpdateBundleCacheConfigPayload +} diff --git a/apps/codecov-api/graphql_api/types/mutation/mutation.py b/apps/codecov-api/graphql_api/types/mutation/mutation.py new file mode 100644 index 0000000000..ecf39f05d2 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/mutation.py @@ -0,0 +1,143 @@ +from ariadne import MutationType + +from .activate_measurements import ( + error_activate_measurements, + resolve_activate_measurements, +) +from .cancel_trial import error_cancel_trial, resolve_cancel_trial +from .create_api_token import error_create_api_token, resolve_create_api_token +from .create_stripe_setup_intent import ( + error_create_stripe_setup_intent, + resolve_create_stripe_setup_intent, +) +from .create_user_token import error_create_user_token, resolve_create_user_token +from .delete_component_measurements import ( + error_delete_component_measurements, + resolve_delete_component_measurements, +) +from .delete_flag import error_delete_flag, resolve_delete_flag +from .delete_session import error_delete_session, resolve_delete_session +from .encode_secret_string import ( + error_encode_secret_string, + resolve_encode_secret_string, +) +from .erase_repository import error_erase_repository, resolve_erase_repository +from .onboard_user import error_onboard_user, resolve_onboard_user +from .regenerate_org_upload_token import ( + error_generate_org_upload_token, + resolve_regenerate_org_upload_token, +) +from .regenerate_repository_token import ( + error_regenerate_repository_token, + resolve_regenerate_repository_token, +) +from .regenerate_repository_upload_token import ( + error_regenerate_repository_upload_token, + resolve_regenerate_repository_upload_token, +) +from .revoke_user_token import error_revoke_user_token, resolve_revoke_user_token +from .save_okta_config import error_save_okta_config, resolve_save_okta_config +from .save_sentry_state import error_save_sentry_state, resolve_save_sentry_state +from .save_terms_agreement import ( + error_save_terms_agreement, + resolve_save_terms_agreement, +) +from .set_upload_token_required import ( + error_set_upload_token_required, + resolve_set_upload_token_required, +) +from .set_yaml_on_owner import error_set_yaml_error, resolve_set_yaml_on_owner +from .start_trial import error_start_trial, resolve_start_trial +from .store_event_metrics import error_store_event_metrics, resolve_store_event_metrics +from .sync_with_git_provider import ( + error_sync_with_git_provider, + resolve_sync_with_git_provider, +) +from .update_bundle_cache_config import ( + error_update_bundle_cache_config, + resolve_update_bundle_cache_config, +) +from .update_default_organization import ( + error_update_default_organization, + resolve_update_default_organization, +) +from .update_profile import error_update_profile, resolve_update_profile +from .update_repository import error_update_repository, resolve_update_repository +from .update_self_hosted_settings import ( + error_update_self_hosted_settings, + resolve_update_self_hosted_settings, +) + +mutation_bindable = MutationType() + +# Here, bind the resolvers from each subfolder to the Mutation type +mutation_bindable.field("createApiToken")(resolve_create_api_token) +mutation_bindable.field("createStripeSetupIntent")(resolve_create_stripe_setup_intent) +mutation_bindable.field("createUserToken")(resolve_create_user_token) +mutation_bindable.field("revokeUserToken")(resolve_revoke_user_token) +mutation_bindable.field("setYamlOnOwner")(resolve_set_yaml_on_owner) +mutation_bindable.field("syncWithGitProvider")(resolve_sync_with_git_provider) +mutation_bindable.field("deleteSession")(resolve_delete_session) +mutation_bindable.field("updateProfile")(resolve_update_profile) +mutation_bindable.field("updateDefaultOrganization")( + resolve_update_default_organization +) +mutation_bindable.field("onboardUser")(resolve_onboard_user) +mutation_bindable.field("regenerateRepositoryToken")( + resolve_regenerate_repository_token +) +mutation_bindable.field("activateMeasurements")(resolve_activate_measurements) +mutation_bindable.field("regenerateOrgUploadToken")(resolve_regenerate_org_upload_token) +mutation_bindable.field("deleteFlag")(resolve_delete_flag) +mutation_bindable.field("saveSentryState")(resolve_save_sentry_state) +mutation_bindable.field("saveTermsAgreement")(resolve_save_terms_agreement) +mutation_bindable.field("startTrial")(resolve_start_trial) +mutation_bindable.field("cancelTrial")(resolve_cancel_trial) +mutation_bindable.field("deleteComponentMeasurements")( + resolve_delete_component_measurements +) +mutation_bindable.field("eraseRepository")(resolve_erase_repository) +mutation_bindable.field("updateRepository")(resolve_update_repository) +mutation_bindable.field("updateSelfHostedSettings")(resolve_update_self_hosted_settings) +mutation_bindable.field("regenerateRepositoryUploadToken")( + resolve_regenerate_repository_upload_token +) +mutation_bindable.field("encodeSecretString")(resolve_encode_secret_string) + +mutation_bindable.field("storeEventMetric")(resolve_store_event_metrics) + +mutation_bindable.field("saveOktaConfig")(resolve_save_okta_config) +mutation_bindable.field("setUploadTokenRequired")(resolve_set_upload_token_required) +mutation_bindable.field("updateBundleCacheConfig")(resolve_update_bundle_cache_config) + +mutation_resolvers = [ + mutation_bindable, + error_create_api_token, + error_create_stripe_setup_intent, + error_create_user_token, + error_revoke_user_token, + error_set_yaml_error, + error_sync_with_git_provider, + error_delete_session, + error_update_profile, + error_update_default_organization, + error_onboard_user, + error_regenerate_repository_token, + error_activate_measurements, + error_generate_org_upload_token, + error_delete_component_measurements, + error_delete_flag, + error_save_sentry_state, + error_save_terms_agreement, + error_start_trial, + error_cancel_trial, + error_erase_repository, + error_update_repository, + error_update_self_hosted_settings, + error_regenerate_repository_upload_token, + error_encode_secret_string, + error_store_event_metrics, + error_save_okta_config, + error_set_upload_token_required, + error_update_bundle_cache_config, +] diff --git a/apps/codecov-api/graphql_api/types/mutation/onboard_user/__init__.py b/apps/codecov-api/graphql_api/types/mutation/onboard_user/__init__.py new file mode 100644 index 0000000000..cd6db9f451 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/onboard_user/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .onboard_user import error_onboard_user, resolve_onboard_user + +gql_onboard_user = ariadne_load_local_graphql(__file__, "onboard_user.graphql") + + +__all__ = ["error_onboard_user", "resolve_onboard_user"] diff --git a/apps/codecov-api/graphql_api/types/mutation/onboard_user/onboard_user.graphql b/apps/codecov-api/graphql_api/types/mutation/onboard_user/onboard_user.graphql new file mode 100644 index 0000000000..66b931e77f --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/onboard_user/onboard_user.graphql @@ -0,0 +1,6 @@ +union OnboardUserError = UnauthenticatedError | UnauthorizedError | ValidationError + +type OnboardUserPayload { + error: OnboardUserError + me: Me +} diff --git a/apps/codecov-api/graphql_api/types/mutation/onboard_user/onboard_user.py b/apps/codecov-api/graphql_api/types/mutation/onboard_user/onboard_user.py new file mode 100644 index 0000000000..39ab1aaf1e --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/onboard_user/onboard_user.py @@ -0,0 +1,18 @@ +from ariadne import UnionType + +from graphql_api.helpers.mutation import ( + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +async def resolve_onboard_user(_, info, input): + command = info.context["executor"].get_command("owner") + input["goals"] = [goal.value for goal in input.get("goals", [])] + input["type_projects"] = [goal.value for goal in input.get("type_projects", [])] + return {"me": await command.onboard_user(input)} + + +error_onboard_user = UnionType("OnboardUserError") +error_onboard_user.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/regenerate_org_upload_token/__init__.py b/apps/codecov-api/graphql_api/types/mutation/regenerate_org_upload_token/__init__.py new file mode 100644 index 0000000000..111d89fc80 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/regenerate_org_upload_token/__init__.py @@ -0,0 +1,12 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .regenerate_org_upload_token import ( + error_generate_org_upload_token, + resolve_regenerate_org_upload_token, +) + +gql_regenerate_org_upload_token = ariadne_load_local_graphql( + __file__, "regenerate_org_upload_token.graphql" +) + +__all__ = ["error_generate_org_upload_token", "resolve_regenerate_org_upload_token"] diff --git a/apps/codecov-api/graphql_api/types/mutation/regenerate_org_upload_token/regenerate_org_upload_token.graphql b/apps/codecov-api/graphql_api/types/mutation/regenerate_org_upload_token/regenerate_org_upload_token.graphql new file mode 100644 index 0000000000..31a73026b7 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/regenerate_org_upload_token/regenerate_org_upload_token.graphql @@ -0,0 +1,6 @@ +union RegenerateOrgUploadTokenError = UnauthenticatedError | ValidationError | UnauthorizedError + +type RegenerateOrgUploadTokenPayload { + orgUploadToken: String + error: RegenerateOrgUploadTokenError +} diff --git a/apps/codecov-api/graphql_api/types/mutation/regenerate_org_upload_token/regenerate_org_upload_token.py b/apps/codecov-api/graphql_api/types/mutation/regenerate_org_upload_token/regenerate_org_upload_token.py new file mode 100644 index 0000000000..e5f2523c80 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/regenerate_org_upload_token/regenerate_org_upload_token.py @@ -0,0 +1,17 @@ +from ariadne import UnionType + +from graphql_api.helpers.mutation import ( + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +async def resolve_regenerate_org_upload_token(_, info, input): + command = info.context["executor"].get_command("owner") + orgUploadToken = await command.regenerate_org_upload_token(owner=input.get("owner")) + return {"org_upload_token": orgUploadToken} + + +error_generate_org_upload_token = UnionType("RegenerateOrgUploadTokenError") +error_generate_org_upload_token.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_token/__init__.py b/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_token/__init__.py new file mode 100644 index 0000000000..10cb2852e8 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_token/__init__.py @@ -0,0 +1,13 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .regenerate_repository_token import ( + error_regenerate_repository_token, + resolve_regenerate_repository_token, +) + +gql_regenerate_repository_token = ariadne_load_local_graphql( + __file__, "regenerate_repository_token.graphql" +) + + +__all__ = ["error_regenerate_repository_token", "resolve_regenerate_repository_token"] diff --git a/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_token/regenerate_repository_token.graphql b/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_token/regenerate_repository_token.graphql new file mode 100644 index 0000000000..d6b102d971 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_token/regenerate_repository_token.graphql @@ -0,0 +1,6 @@ +union RegenerateRepositoryTokenError = UnauthenticatedError | ValidationError + +type RegenerateRepositoryTokenPayload { + token: String + error: RegenerateRepositoryTokenError +} diff --git a/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_token/regenerate_repository_token.py b/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_token/regenerate_repository_token.py new file mode 100644 index 0000000000..0b592c2701 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_token/regenerate_repository_token.py @@ -0,0 +1,25 @@ +from ariadne import UnionType + +from graphql_api.helpers.mutation import ( + require_authenticated, + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +@require_authenticated +async def resolve_regenerate_repository_token(_, info, input): + command = info.context["executor"].get_command("repository") + + token = await command.regenerate_repository_token( + repo_name=input.get("repo_name"), + owner_username=input.get("owner"), + token_type=input.get("token_type"), + ) + + return {"token": token} + + +error_regenerate_repository_token = UnionType("RegenerateRepositoryTokenError") +error_regenerate_repository_token.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_upload_token/__init__.py b/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_upload_token/__init__.py new file mode 100644 index 0000000000..93f0511986 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_upload_token/__init__.py @@ -0,0 +1,16 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .regenerate_repository_upload_token import ( + error_regenerate_repository_upload_token, + resolve_regenerate_repository_upload_token, +) + +gql_regenerate_repository_upload_token = ariadne_load_local_graphql( + __file__, "regenerate_repository_upload_token.graphql" +) + + +__all__ = [ + "error_regenerate_repository_upload_token", + "resolve_regenerate_repository_upload_token", +] diff --git a/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_upload_token/regenerate_repository_upload_token.graphql b/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_upload_token/regenerate_repository_upload_token.graphql new file mode 100644 index 0000000000..e4e52b98ff --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_upload_token/regenerate_repository_upload_token.graphql @@ -0,0 +1,6 @@ +union RegenerateRepositoryUploadTokenError = ValidationError + +type RegenerateRepositoryUploadTokenPayload { + token: String + error: ValidationError +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_upload_token/regenerate_repository_upload_token.py b/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_upload_token/regenerate_repository_upload_token.py new file mode 100644 index 0000000000..0d6a6a8e99 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/regenerate_repository_upload_token/regenerate_repository_upload_token.py @@ -0,0 +1,32 @@ +import uuid +from typing import Any, Dict + +from ariadne import UnionType +from graphql import GraphQLResolveInfo + +from core.commands.repository.repository import RepositoryCommands +from graphql_api.helpers.mutation import ( + require_authenticated, + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +@require_authenticated +async def resolve_regenerate_repository_upload_token( + _: Any, info: GraphQLResolveInfo, input: Dict[str, str] +) -> Dict[str, uuid.UUID]: + command: RepositoryCommands = info.context["executor"].get_command("repository") + token = await command.regenerate_repository_upload_token( + repo_name=input.get("repo_name", ""), + owner_username=input.get("owner", ""), + ) + + return {"token": token} + + +error_regenerate_repository_upload_token = UnionType( + "RegenerateRepositoryUploadTokenError" +) +error_regenerate_repository_upload_token.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/revoke_user_token/__init__.py b/apps/codecov-api/graphql_api/types/mutation/revoke_user_token/__init__.py new file mode 100644 index 0000000000..cf6f8fd43c --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/revoke_user_token/__init__.py @@ -0,0 +1,10 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .revoke_user_token import error_revoke_user_token, resolve_revoke_user_token + +gql_revoke_user_token = ariadne_load_local_graphql( + __file__, "revoke_user_token.graphql" +) + + +__all__ = ["error_revoke_user_token", "resolve_revoke_user_token"] diff --git a/apps/codecov-api/graphql_api/types/mutation/revoke_user_token/revoke_user_token.graphql b/apps/codecov-api/graphql_api/types/mutation/revoke_user_token/revoke_user_token.graphql new file mode 100644 index 0000000000..305cb61fd2 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/revoke_user_token/revoke_user_token.graphql @@ -0,0 +1,5 @@ +union RevokeUserTokenError = UnauthenticatedError + +type RevokeUserTokenPayload { + error: RevokeUserTokenError +} diff --git a/apps/codecov-api/graphql_api/types/mutation/revoke_user_token/revoke_user_token.py b/apps/codecov-api/graphql_api/types/mutation/revoke_user_token/revoke_user_token.py new file mode 100644 index 0000000000..e9dd49fe66 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/revoke_user_token/revoke_user_token.py @@ -0,0 +1,17 @@ +from ariadne import UnionType + +from graphql_api.helpers.mutation import ( + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +async def resolve_revoke_user_token(_, info, input): + command = info.context["executor"].get_command("owner") + await command.revoke_user_token(input.get("tokenid")) + return None + + +error_revoke_user_token = UnionType("RevokeUserTokenError") +error_revoke_user_token.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/save_okta_config/__init__.py b/apps/codecov-api/graphql_api/types/mutation/save_okta_config/__init__.py new file mode 100644 index 0000000000..15fd54e5f2 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/save_okta_config/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .save_okta_config import error_save_okta_config, resolve_save_okta_config + +gql_save_okta_config = ariadne_load_local_graphql(__file__, "save_okta_config.graphql") + + +__all__ = ["error_save_okta_config", "resolve_save_okta_config"] diff --git a/apps/codecov-api/graphql_api/types/mutation/save_okta_config/save_okta_config.graphql b/apps/codecov-api/graphql_api/types/mutation/save_okta_config/save_okta_config.graphql new file mode 100644 index 0000000000..2e57fedf59 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/save_okta_config/save_okta_config.graphql @@ -0,0 +1,17 @@ +union SaveOktaConfigError = + UnauthenticatedError + | UnauthorizedError + | ValidationError + +type SaveOktaConfigPayload { + error: SaveOktaConfigError +} + +input SaveOktaConfigInput { + clientId: String + clientSecret: String + url: String + enabled: Boolean + enforced: Boolean + orgUsername: String +} diff --git a/apps/codecov-api/graphql_api/types/mutation/save_okta_config/save_okta_config.py b/apps/codecov-api/graphql_api/types/mutation/save_okta_config/save_okta_config.py new file mode 100644 index 0000000000..8413f40667 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/save_okta_config/save_okta_config.py @@ -0,0 +1,23 @@ +from typing import Any, Dict + +from ariadne import UnionType +from graphql import GraphQLResolveInfo + +from graphql_api.helpers.mutation import ( + require_authenticated, + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +@require_authenticated +async def resolve_save_okta_config( + _: Any, info: GraphQLResolveInfo, input: Dict[str, Any] +) -> None: + command = info.context["executor"].get_command("owner") + return await command.save_okta_config(input) + + +error_save_okta_config = UnionType("SaveOktaConfigError") +error_save_okta_config.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/save_sentry_state/__init__.py b/apps/codecov-api/graphql_api/types/mutation/save_sentry_state/__init__.py new file mode 100644 index 0000000000..0d90e90e4f --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/save_sentry_state/__init__.py @@ -0,0 +1,9 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .save_sentry_state import error_save_sentry_state, resolve_save_sentry_state + +gql_save_sentry_state = ariadne_load_local_graphql( + __file__, "save_sentry_state.graphql" +) + +__all__ = ["error_save_sentry_state", "resolve_save_sentry_state"] diff --git a/apps/codecov-api/graphql_api/types/mutation/save_sentry_state/save_sentry_state.graphql b/apps/codecov-api/graphql_api/types/mutation/save_sentry_state/save_sentry_state.graphql new file mode 100644 index 0000000000..a287f256fd --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/save_sentry_state/save_sentry_state.graphql @@ -0,0 +1,5 @@ +union SaveSentryStateError = UnauthenticatedError | ValidationError + +type SaveSentryStatePayload { + error: SaveSentryStateError +} diff --git a/apps/codecov-api/graphql_api/types/mutation/save_sentry_state/save_sentry_state.py b/apps/codecov-api/graphql_api/types/mutation/save_sentry_state/save_sentry_state.py new file mode 100644 index 0000000000..8db5749479 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/save_sentry_state/save_sentry_state.py @@ -0,0 +1,27 @@ +from ariadne import UnionType +from asgiref.sync import sync_to_async + +import services.sentry as sentry +from codecov.commands.exceptions import ValidationError +from graphql_api.helpers.mutation import ( + require_authenticated, + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +@require_authenticated +@sync_to_async +def resolve_save_sentry_state(_, info, input): + current_owner = info.context["request"].current_owner + try: + sentry.save_sentry_state(current_owner, input.get("state")) + except sentry.SentryInvalidStateError: + raise ValidationError("Invalid state") + except sentry.SentryUserAlreadyExistsError: + raise ValidationError("Invalid Sentry user") + + +error_save_sentry_state = UnionType("SaveSentryStateError") +error_save_sentry_state.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/save_terms_agreement/__init__.py b/apps/codecov-api/graphql_api/types/mutation/save_terms_agreement/__init__.py new file mode 100644 index 0000000000..d65cf7c938 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/save_terms_agreement/__init__.py @@ -0,0 +1,12 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .save_terms_agreement import ( + error_save_terms_agreement, + resolve_save_terms_agreement, +) + +gql_save_terms_agreement = ariadne_load_local_graphql( + __file__, "save_terms_agreement.graphql" +) + +__all__ = ["error_save_terms_agreement", "resolve_save_terms_agreement"] diff --git a/apps/codecov-api/graphql_api/types/mutation/save_terms_agreement/save_terms_agreement.graphql b/apps/codecov-api/graphql_api/types/mutation/save_terms_agreement/save_terms_agreement.graphql new file mode 100644 index 0000000000..b3fdf5027e --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/save_terms_agreement/save_terms_agreement.graphql @@ -0,0 +1,13 @@ +union SaveTermsAgreementError = UnauthenticatedError | ValidationError + +type SaveTermsAgreementPayload { + error: SaveTermsAgreementError +} + +input SaveTermsAgreementInput { + businessEmail: String + termsAgreement: Boolean! + marketingConsent: Boolean + name: String + customerIntent: String +} diff --git a/apps/codecov-api/graphql_api/types/mutation/save_terms_agreement/save_terms_agreement.py b/apps/codecov-api/graphql_api/types/mutation/save_terms_agreement/save_terms_agreement.py new file mode 100644 index 0000000000..413adbdaa7 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/save_terms_agreement/save_terms_agreement.py @@ -0,0 +1,19 @@ +from ariadne import UnionType + +from codecov_auth.commands.owner import OwnerCommands +from graphql_api.helpers.mutation import ( + require_authenticated, + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +@require_authenticated +async def resolve_save_terms_agreement(_, info, input): + command: OwnerCommands = info.context["executor"].get_command("owner") + return await command.save_terms_agreement(input) + + +error_save_terms_agreement = UnionType("SaveTermsAgreementError") +error_save_terms_agreement.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/set_upload_token_required/__init__.py b/apps/codecov-api/graphql_api/types/mutation/set_upload_token_required/__init__.py new file mode 100644 index 0000000000..55723e1e3c --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/set_upload_token_required/__init__.py @@ -0,0 +1,12 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .set_upload_token_required import ( + error_set_upload_token_required, + resolve_set_upload_token_required, +) + +gql_set_upload_token_required = ariadne_load_local_graphql( + __file__, "set_upload_token_required.graphql" +) + +__all__ = ["error_set_upload_token_required", "resolve_set_upload_token_required"] diff --git a/apps/codecov-api/graphql_api/types/mutation/set_upload_token_required/set_upload_token_required.graphql b/apps/codecov-api/graphql_api/types/mutation/set_upload_token_required/set_upload_token_required.graphql new file mode 100644 index 0000000000..df5eec4197 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/set_upload_token_required/set_upload_token_required.graphql @@ -0,0 +1,13 @@ +union SetUploadTokenRequiredError = + UnauthenticatedError + | UnauthorizedError + | ValidationError + +type SetUploadTokenRequiredPayload { + error: SetUploadTokenRequiredError +} + +input SetUploadTokenRequiredInput { + orgUsername: String! + uploadTokenRequired: Boolean! +} diff --git a/apps/codecov-api/graphql_api/types/mutation/set_upload_token_required/set_upload_token_required.py b/apps/codecov-api/graphql_api/types/mutation/set_upload_token_required/set_upload_token_required.py new file mode 100644 index 0000000000..6e686a9ebb --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/set_upload_token_required/set_upload_token_required.py @@ -0,0 +1,19 @@ +from ariadne import UnionType + +from codecov_auth.commands.owner import OwnerCommands +from graphql_api.helpers.mutation import ( + require_authenticated, + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +@require_authenticated +async def resolve_set_upload_token_required(_, info, input): + command: OwnerCommands = info.context["executor"].get_command("owner") + return await command.set_upload_token_required(input) + + +error_set_upload_token_required = UnionType("SetUploadTokenRequiredError") +error_set_upload_token_required.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/set_yaml_on_owner/__init__.py b/apps/codecov-api/graphql_api/types/mutation/set_yaml_on_owner/__init__.py new file mode 100644 index 0000000000..1d5a8aaa10 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/set_yaml_on_owner/__init__.py @@ -0,0 +1,9 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .set_yaml_on_owner import error_set_yaml_error, resolve_set_yaml_on_owner + +gql_set_yaml_on_owner = ariadne_load_local_graphql( + __file__, "set_yaml_on_owner.graphql" +) + +__all__ = ["error_set_yaml_error", "resolve_set_yaml_on_owner"] diff --git a/apps/codecov-api/graphql_api/types/mutation/set_yaml_on_owner/set_yaml_on_owner.graphql b/apps/codecov-api/graphql_api/types/mutation/set_yaml_on_owner/set_yaml_on_owner.graphql new file mode 100644 index 0000000000..ceca786271 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/set_yaml_on_owner/set_yaml_on_owner.graphql @@ -0,0 +1,6 @@ +union SetYamlOnOwnerError = UnauthenticatedError | ValidationError | UnauthorizedError | NotFoundError + +type SetYamlOnOwnerPayload { + error: SetYamlOnOwnerError + owner: Owner +} diff --git a/apps/codecov-api/graphql_api/types/mutation/set_yaml_on_owner/set_yaml_on_owner.py b/apps/codecov-api/graphql_api/types/mutation/set_yaml_on_owner/set_yaml_on_owner.py new file mode 100644 index 0000000000..2f3a6b04b3 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/set_yaml_on_owner/set_yaml_on_owner.py @@ -0,0 +1,19 @@ +from ariadne import UnionType + +from graphql_api.helpers.mutation import ( + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +async def resolve_set_yaml_on_owner(_, info, input): + command = info.context["executor"].get_command("owner") + username_input = input.get("username") + yaml_input = input.get("yaml") + owner = await command.set_yaml_on_owner(username_input, yaml_input) + return {"owner": owner} + + +error_set_yaml_error = UnionType("SetYamlOnOwnerError") +error_set_yaml_error.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/start_trial/__init__.py b/apps/codecov-api/graphql_api/types/mutation/start_trial/__init__.py new file mode 100644 index 0000000000..46192ad9a4 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/start_trial/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .start_trial import error_start_trial, resolve_start_trial + +gql_start_trial = ariadne_load_local_graphql(__file__, "start_trial.graphql") + + +__all__ = ["error_start_trial", "resolve_start_trial"] diff --git a/apps/codecov-api/graphql_api/types/mutation/start_trial/start_trial.graphql b/apps/codecov-api/graphql_api/types/mutation/start_trial/start_trial.graphql new file mode 100644 index 0000000000..a7571c5be4 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/start_trial/start_trial.graphql @@ -0,0 +1,5 @@ +union StartTrialError = UnauthenticatedError | ValidationError + +type StartTrialPayload { + error: StartTrialError +} diff --git a/apps/codecov-api/graphql_api/types/mutation/start_trial/start_trial.py b/apps/codecov-api/graphql_api/types/mutation/start_trial/start_trial.py new file mode 100644 index 0000000000..034aedfd37 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/start_trial/start_trial.py @@ -0,0 +1,20 @@ +from ariadne import UnionType + +from codecov_auth.commands.owner import OwnerCommands +from graphql_api.helpers.mutation import ( + require_authenticated, + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +@require_authenticated +async def resolve_start_trial(_, info, input) -> None: + command: OwnerCommands = info.context["executor"].get_command("owner") + await command.start_trial(input.get("org_username")) + return None + + +error_start_trial = UnionType("StartTrialError") +error_start_trial.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/store_event_metrics/__init__.py b/apps/codecov-api/graphql_api/types/mutation/store_event_metrics/__init__.py new file mode 100644 index 0000000000..74abfb4f84 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/store_event_metrics/__init__.py @@ -0,0 +1,10 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .store_event_metrics import error_store_event_metrics, resolve_store_event_metrics + +gql_store_event_metrics = ariadne_load_local_graphql( + __file__, "store_event_metrics.graphql" +) + + +__all__ = ["error_store_event_metrics", "resolve_store_event_metrics"] diff --git a/apps/codecov-api/graphql_api/types/mutation/store_event_metrics/store_event_metrics.graphql b/apps/codecov-api/graphql_api/types/mutation/store_event_metrics/store_event_metrics.graphql new file mode 100644 index 0000000000..4a0faccc72 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/store_event_metrics/store_event_metrics.graphql @@ -0,0 +1,11 @@ +union StoreEventMetricsError = UnauthenticatedError | ValidationError + +type StoreEventMetricsPayload { + error: StoreEventMetricsError +} + +input StoreEventMetricsInput { + orgUsername: String! + eventName: String! + jsonPayload: String # The input expects a serialized json string +} diff --git a/apps/codecov-api/graphql_api/types/mutation/store_event_metrics/store_event_metrics.py b/apps/codecov-api/graphql_api/types/mutation/store_event_metrics/store_event_metrics.py new file mode 100644 index 0000000000..e7c3d322da --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/store_event_metrics/store_event_metrics.py @@ -0,0 +1,22 @@ +from ariadne import UnionType + +from codecov_auth.commands.owner import OwnerCommands +from graphql_api.helpers.mutation import ( + require_authenticated, + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +@require_authenticated +async def resolve_store_event_metrics(_, info, input) -> None: + command: OwnerCommands = info.context["executor"].get_command("owner") + await command.store_codecov_metric( + input.get("org_username"), input.get("event_name"), input.get("json_payload") + ) + return None + + +error_store_event_metrics = UnionType("StoreEventMetricsError") +error_store_event_metrics.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/sync_with_git_provider/__init__.py b/apps/codecov-api/graphql_api/types/mutation/sync_with_git_provider/__init__.py new file mode 100644 index 0000000000..8f5f43c266 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/sync_with_git_provider/__init__.py @@ -0,0 +1,13 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .sync_with_git_provider import ( + error_sync_with_git_provider, + resolve_sync_with_git_provider, +) + +gql_sync_with_git_provider = ariadne_load_local_graphql( + __file__, "sync_with_git_provider.graphql" +) + + +__all__ = ["error_sync_with_git_provider", "resolve_sync_with_git_provider"] diff --git a/apps/codecov-api/graphql_api/types/mutation/sync_with_git_provider/sync_with_git_provider.graphql b/apps/codecov-api/graphql_api/types/mutation/sync_with_git_provider/sync_with_git_provider.graphql new file mode 100644 index 0000000000..0443383dce --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/sync_with_git_provider/sync_with_git_provider.graphql @@ -0,0 +1,6 @@ +union SyncWithGitProviderError = UnauthenticatedError + +type SyncWithGitProviderPayload { + me: Me + error: SyncWithGitProviderError +} diff --git a/apps/codecov-api/graphql_api/types/mutation/sync_with_git_provider/sync_with_git_provider.py b/apps/codecov-api/graphql_api/types/mutation/sync_with_git_provider/sync_with_git_provider.py new file mode 100644 index 0000000000..7dc999456e --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/sync_with_git_provider/sync_with_git_provider.py @@ -0,0 +1,17 @@ +from ariadne import UnionType + +from graphql_api.helpers.mutation import ( + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +async def resolve_sync_with_git_provider(_, info): + command = info.context["executor"].get_command("owner") + await command.trigger_sync() + return {"me": info.context["request"].current_owner} + + +error_sync_with_git_provider = UnionType("SyncWithGitProviderError") +error_sync_with_git_provider.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/update_bundle_cache_config/__init__.py b/apps/codecov-api/graphql_api/types/mutation/update_bundle_cache_config/__init__.py new file mode 100644 index 0000000000..c434620641 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/update_bundle_cache_config/__init__.py @@ -0,0 +1,15 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .update_bundle_cache_config import ( + error_update_bundle_cache_config, + resolve_update_bundle_cache_config, +) + +gql_update_bundle_cache_config = ariadne_load_local_graphql( + __file__, "update_bundle_cache_config.graphql" +) + +__all__ = [ + "error_update_bundle_cache_config", + "resolve_update_bundle_cache_config", +] diff --git a/apps/codecov-api/graphql_api/types/mutation/update_bundle_cache_config/update_bundle_cache_config.graphql b/apps/codecov-api/graphql_api/types/mutation/update_bundle_cache_config/update_bundle_cache_config.graphql new file mode 100644 index 0000000000..e07c60530a --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/update_bundle_cache_config/update_bundle_cache_config.graphql @@ -0,0 +1,12 @@ +union UpdateBundleCacheConfigError = UnauthenticatedError | ValidationError + +type UpdateBundleCacheConfigResult { + bundleName: String + isCached: Boolean + cacheConfig: Boolean +} + +type UpdateBundleCacheConfigPayload { + results: [UpdateBundleCacheConfigResult!] + error: UpdateBundleCacheConfigError +} diff --git a/apps/codecov-api/graphql_api/types/mutation/update_bundle_cache_config/update_bundle_cache_config.py b/apps/codecov-api/graphql_api/types/mutation/update_bundle_cache_config/update_bundle_cache_config.py new file mode 100644 index 0000000000..ae13c64a25 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/update_bundle_cache_config/update_bundle_cache_config.py @@ -0,0 +1,30 @@ +from typing import Any, Dict, List + +from ariadne import UnionType +from graphql import GraphQLResolveInfo + +from core.commands.repository.repository import RepositoryCommands +from graphql_api.helpers.mutation import ( + require_authenticated, + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +@require_authenticated +async def resolve_update_bundle_cache_config( + _: Any, info: GraphQLResolveInfo, input: Dict[str, Any] +) -> Dict[str, List[Dict[str, str | bool]]]: + command: RepositoryCommands = info.context["executor"].get_command("repository") + + results = await command.update_bundle_cache_config( + repo_name=input.get("repo_name", ""), + owner_username=input.get("owner", ""), + cache_config=input.get("bundles", []), + ) + return {"results": results} + + +error_update_bundle_cache_config = UnionType("UpdateBundleCacheConfigError") +error_update_bundle_cache_config.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/update_default_organization/__init__.py b/apps/codecov-api/graphql_api/types/mutation/update_default_organization/__init__.py new file mode 100644 index 0000000000..b0f7faccf4 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/update_default_organization/__init__.py @@ -0,0 +1,13 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .update_default_organization import ( + error_update_default_organization, + resolve_update_default_organization, +) + +gql_update_default_organization = ariadne_load_local_graphql( + __file__, "update_default_organization.graphql" +) + + +__all__ = ["error_update_default_organization", "resolve_update_default_organization"] diff --git a/apps/codecov-api/graphql_api/types/mutation/update_default_organization/update_default_organization.graphql b/apps/codecov-api/graphql_api/types/mutation/update_default_organization/update_default_organization.graphql new file mode 100644 index 0000000000..2dbe5cf426 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/update_default_organization/update_default_organization.graphql @@ -0,0 +1,10 @@ +union UpdateDefaultOrganizationError = UnauthenticatedError | ValidationError | MissingService + +type UpdateDefaultOrganizationPayload { + error: UpdateDefaultOrganizationError + username: String +} + +input UpdateDefaultOrganizationInput { + username: String +} diff --git a/apps/codecov-api/graphql_api/types/mutation/update_default_organization/update_default_organization.py b/apps/codecov-api/graphql_api/types/mutation/update_default_organization/update_default_organization.py new file mode 100644 index 0000000000..429f622247 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/update_default_organization/update_default_organization.py @@ -0,0 +1,20 @@ +from ariadne import UnionType + +from codecov_auth.commands.owner import OwnerCommands +from graphql_api.helpers.mutation import ( + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +async def resolve_update_default_organization(_, info, input): + command: OwnerCommands = info.context["executor"].get_command("owner") + default_username = await command.update_default_organization( + default_org_username=input.get("username") + ) + return {"username": default_username} + + +error_update_default_organization = UnionType("UpdateDefaultOrganizationError") +error_update_default_organization.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/update_profile/__init__.py b/apps/codecov-api/graphql_api/types/mutation/update_profile/__init__.py new file mode 100644 index 0000000000..86add287c4 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/update_profile/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .update_profile import error_update_profile, resolve_update_profile + +gql_update_profile = ariadne_load_local_graphql(__file__, "update_profile.graphql") + + +__all__ = ["error_update_profile", "resolve_update_profile"] diff --git a/apps/codecov-api/graphql_api/types/mutation/update_profile/update_profile.graphql b/apps/codecov-api/graphql_api/types/mutation/update_profile/update_profile.graphql new file mode 100644 index 0000000000..f26adacada --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/update_profile/update_profile.graphql @@ -0,0 +1,11 @@ +union UpdateProfileError = UnauthenticatedError | ValidationError + +type UpdateProfilePayload { + error: CreateApiTokenError + me: Me +} + +input UpdateProfileInput { + email: String + name: String +} diff --git a/apps/codecov-api/graphql_api/types/mutation/update_profile/update_profile.py b/apps/codecov-api/graphql_api/types/mutation/update_profile/update_profile.py new file mode 100644 index 0000000000..abd79a5a07 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/update_profile/update_profile.py @@ -0,0 +1,17 @@ +from ariadne import UnionType + +from graphql_api.helpers.mutation import ( + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +async def resolve_update_profile(_, info, input): + command = info.context["executor"].get_command("owner") + me = await command.update_profile(email=input.get("email"), name=input.get("name")) + return {"me": me} + + +error_update_profile = UnionType("UpdateProfileError") +error_update_profile.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/update_repository/__init__.py b/apps/codecov-api/graphql_api/types/mutation/update_repository/__init__.py new file mode 100644 index 0000000000..c9f3549acf --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/update_repository/__init__.py @@ -0,0 +1,10 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .update_repository import error_update_repository, resolve_update_repository + +gql_update_repository = ariadne_load_local_graphql( + __file__, "update_repository.graphql" +) + + +__all__ = ["error_update_repository", "resolve_update_repository"] diff --git a/apps/codecov-api/graphql_api/types/mutation/update_repository/update_repository.graphql b/apps/codecov-api/graphql_api/types/mutation/update_repository/update_repository.graphql new file mode 100644 index 0000000000..02127d0d04 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/update_repository/update_repository.graphql @@ -0,0 +1,11 @@ +union UpdateRepositoryError = UnauthenticatedError | ValidationError | UnauthorizedError + +type UpdateRepositoryPayload { + error: UpdateRepositoryError +} + +input UpdateRepositoryInput { + branch: String + activated: Boolean + repoName: String! +} diff --git a/apps/codecov-api/graphql_api/types/mutation/update_repository/update_repository.py b/apps/codecov-api/graphql_api/types/mutation/update_repository/update_repository.py new file mode 100644 index 0000000000..75aa943a3f --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/update_repository/update_repository.py @@ -0,0 +1,25 @@ +from ariadne import UnionType + +from graphql_api.helpers.mutation import ( + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +async def resolve_update_repository(_, info, input): + command = info.context["executor"].get_command("repository") + owner = info.context["request"].current_owner + repo_name = input.get("repo_name") + default_branch = input.get("branch") + activated = input.get("activated") + await command.update_repository( + repo_name, + owner, + default_branch, + activated, + ) + + +error_update_repository = UnionType("UpdateRepositoryError") +error_update_repository.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/mutation/update_self_hosted_settings/__init__.py b/apps/codecov-api/graphql_api/types/mutation/update_self_hosted_settings/__init__.py new file mode 100644 index 0000000000..dbb5299a6e --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/update_self_hosted_settings/__init__.py @@ -0,0 +1,16 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .update_self_hosted_settings import ( + error_update_self_hosted_settings, + resolve_update_self_hosted_settings, +) + +gql_update_self_hosted_settings = ariadne_load_local_graphql( + __file__, "update_self_hosted_settings.graphql" +) + +__all__ = [ + "gql_update_self_hosted_settings", + "error_update_self_hosted_settings", + "resolve_update_self_hosted_settings", +] diff --git a/apps/codecov-api/graphql_api/types/mutation/update_self_hosted_settings/update_self_hosted_settings.graphql b/apps/codecov-api/graphql_api/types/mutation/update_self_hosted_settings/update_self_hosted_settings.graphql new file mode 100644 index 0000000000..e184f904b1 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/update_self_hosted_settings/update_self_hosted_settings.graphql @@ -0,0 +1,9 @@ +union UpdateSelfHostedSettingsError = UnauthenticatedError | ValidationError + +type UpdateSelfHostedSettingsPayload { + error: UpdateSelfHostedSettingsError +} + +input UpdateSelfHostedSettingsInput { + shouldAutoActivate: Boolean! +} diff --git a/apps/codecov-api/graphql_api/types/mutation/update_self_hosted_settings/update_self_hosted_settings.py b/apps/codecov-api/graphql_api/types/mutation/update_self_hosted_settings/update_self_hosted_settings.py new file mode 100644 index 0000000000..eaf23404b4 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/mutation/update_self_hosted_settings/update_self_hosted_settings.py @@ -0,0 +1,19 @@ +from ariadne import UnionType + +from codecov_auth.commands.owner import OwnerCommands +from graphql_api.helpers.mutation import ( + require_authenticated, + resolve_union_error_type, + wrap_error_handling_mutation, +) + + +@wrap_error_handling_mutation +@require_authenticated +async def resolve_update_self_hosted_settings(_, info, input): + command: OwnerCommands = info.context["executor"].get_command("owner") + return await command.update_self_hosted_settings(input) + + +error_update_self_hosted_settings = UnionType("UpdateSelfHostedSettingsError") +error_update_self_hosted_settings.type_resolver(resolve_union_error_type) diff --git a/apps/codecov-api/graphql_api/types/okta_config/__init__.py b/apps/codecov-api/graphql_api/types/okta_config/__init__.py new file mode 100644 index 0000000000..d34a90550d --- /dev/null +++ b/apps/codecov-api/graphql_api/types/okta_config/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .okta_config import okta_config_bindable + +okta_config = ariadne_load_local_graphql(__file__, "okta_config.graphql") + + +__all__ = ["okta_config_bindable"] diff --git a/apps/codecov-api/graphql_api/types/okta_config/okta_config.graphql b/apps/codecov-api/graphql_api/types/okta_config/okta_config.graphql new file mode 100644 index 0000000000..5cb6f4a4bf --- /dev/null +++ b/apps/codecov-api/graphql_api/types/okta_config/okta_config.graphql @@ -0,0 +1,7 @@ +type OktaConfig { + clientId: String! + clientSecret: String! + url: String! + enabled: Boolean! + enforced: Boolean! +} diff --git a/apps/codecov-api/graphql_api/types/okta_config/okta_config.py b/apps/codecov-api/graphql_api/types/okta_config/okta_config.py new file mode 100644 index 0000000000..a9b07ca289 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/okta_config/okta_config.py @@ -0,0 +1,30 @@ +from ariadne import ObjectType + +from codecov_auth.models import OktaSettings + +okta_config_bindable = ObjectType("OktaConfig") + + +@okta_config_bindable.field("clientId") +def resolve_client_id(okta_config: OktaSettings, info) -> str: + return okta_config.client_id + + +@okta_config_bindable.field("clientSecret") +def resolve_client_secret(okta_config: OktaSettings, info) -> str: + return okta_config.client_secret + + +@okta_config_bindable.field("url") +def resolve_url(okta_config: OktaSettings, info) -> str: + return okta_config.url + + +@okta_config_bindable.field("enabled") +def resolve_enabled(okta_config: OktaSettings, info) -> bool: + return okta_config.enabled + + +@okta_config_bindable.field("enforced") +def resolve_enforced(okta_config: OktaSettings, info) -> bool: + return okta_config.enforced diff --git a/apps/codecov-api/graphql_api/types/owner/__init__.py b/apps/codecov-api/graphql_api/types/owner/__init__.py new file mode 100644 index 0000000000..a484f87370 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/owner/__init__.py @@ -0,0 +1,6 @@ +from .owner import owner, owner_bindable + +__all__ = [ + "owner", + "owner_bindable", +] diff --git a/apps/codecov-api/graphql_api/types/owner/owner.graphql b/apps/codecov-api/graphql_api/types/owner/owner.graphql new file mode 100644 index 0000000000..530b4cf050 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/owner/owner.graphql @@ -0,0 +1,47 @@ +type Owner { + account: Account + availablePlans: [PlanRepresentation!] + avatarUrl: String! + billing: Billing + defaultOrgUsername: String + delinquent: Boolean + hashOwnerid: String + hasActiveRepos: Boolean + hasPrivateRepos: Boolean + hasPublicRepos: Boolean + invoice(invoiceId: String!): Invoice + invoices: [Invoice] @cost(complexity: 100) + isAdmin: Boolean + isCurrentUserActivated: Boolean + isCurrentUserPartOfOrg: Boolean! + isGithubRateLimited: Boolean + isUserOktaAuthenticated: Boolean + measurements( + interval: MeasurementInterval! + after: DateTime + before: DateTime + repos: [String!] + isPublic: Boolean + ): [Measurement!] + numberOfUploads: Int + orgUploadToken: String + ownerid: Int + plan: Plan + pretrialPlan: PlanRepresentation + repository(name: String!): RepositoryResult! + repositories( + filters: RepositorySetFilters + ordering: RepositoryOrdering + orderingDirection: OrderingDirection + first: Int + after: String + last: Int + before: String + ): RepositoryConnection! @cost(complexity: 25, multipliers: ["first", "last"]) + username: String + yaml: String + aiFeaturesEnabled: Boolean! + aiEnabledRepos: [String] + uploadTokenRequired: Boolean + activatedUserCount: Int +} diff --git a/apps/codecov-api/graphql_api/types/owner/owner.py b/apps/codecov-api/graphql_api/types/owner/owner.py new file mode 100644 index 0000000000..c9bf8a72ae --- /dev/null +++ b/apps/codecov-api/graphql_api/types/owner/owner.py @@ -0,0 +1,450 @@ +from datetime import datetime +from hashlib import sha1 +from typing import Any, Coroutine, Iterable, List, Optional + +import sentry_sdk +import shared.rate_limits as rate_limits +import stripe +import yaml +from ariadne import ObjectType +from asgiref.sync import sync_to_async +from django.conf import settings +from graphql import GraphQLResolveInfo +from shared.helpers.redis import get_redis_connection +from shared.plan.constants import DEFAULT_FREE_PLAN +from shared.plan.service import PlanService + +import services.activation as activation +import timeseries.helpers as timeseries_helpers +from codecov_auth.constants import OWNER_YAML_TO_STRING_KEY +from codecov_auth.helpers import current_user_part_of_org +from codecov_auth.models import ( + SERVICE_GITHUB, + SERVICE_GITHUB_ENTERPRISE, + Account, + GithubAppInstallation, + Owner, + Plan, +) +from codecov_auth.views.okta_cloud import OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY +from core.models import Repository +from graphql_api.actions.repository import list_repository_for_owner +from graphql_api.helpers.ariadne import ariadne_load_local_graphql +from graphql_api.helpers.connection import ( + Connection, + build_connection_graphql, + queryset_to_connection_sync, +) +from graphql_api.helpers.mutation import ( + require_part_of_org, + require_shared_account_or_part_of_org, +) +from graphql_api.helpers.requested_fields import selected_fields +from graphql_api.types.enums import OrderingDirection, RepositoryOrdering +from graphql_api.types.errors.errors import NotFoundError +from graphql_api.types.repository.repository import TOKEN_UNAVAILABLE +from services.billing import BillingService +from timeseries.helpers import fill_sparse_measurements +from timeseries.models import Interval +from utils.config import get_config + +owner = ariadne_load_local_graphql(__file__, "owner.graphql") +owner = owner + build_connection_graphql("RepositoryConnection", "Repository") +owner_bindable = ObjectType("Owner") +AI_FEATURES_GH_APP_ID = get_config("github", "ai_features_app_id") + + +@owner_bindable.field("repositories") +@sync_to_async +@sentry_sdk.trace +def resolve_repositories( + owner: Owner, + info: GraphQLResolveInfo, + filters: Optional[dict] = None, + ordering: Optional[RepositoryOrdering] = RepositoryOrdering.ID, + ordering_direction: Optional[OrderingDirection] = OrderingDirection.ASC, + **kwargs: Any, +) -> Coroutine[Any, Any, Connection]: + current_owner = info.context["request"].current_owner + okta_account_auths: list[int] = info.context["request"].session.get( + OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY, [] + ) + + is_impersonation = info.context["request"].impersonation + # If the user is impersonating another user, we want to show all the Okta repos. + # This means we do not want to filter out the Okta enforced repos + exclude_okta_enforced_repos = not is_impersonation + + queryset = list_repository_for_owner( + current_owner, owner, filters, okta_account_auths, exclude_okta_enforced_repos + ) + + return queryset_to_connection_sync( + queryset, + ordering=(ordering, RepositoryOrdering.ID), + ordering_direction=ordering_direction, + **kwargs, + ) + + +@owner_bindable.field("isCurrentUserPartOfOrg") +@sync_to_async +def resolve_is_current_user_part_of_org(owner: Owner, info: GraphQLResolveInfo) -> bool: + current_owner = info.context["request"].current_owner + return current_user_part_of_org(current_owner, owner) + + +@owner_bindable.field("yaml") +def resolve_yaml(owner: Owner, info: GraphQLResolveInfo) -> Optional[str]: + if owner.yaml is None: + return None + current_owner = info.context["request"].current_owner + if not current_user_part_of_org(current_owner, owner): + return None + return owner.yaml.get(OWNER_YAML_TO_STRING_KEY, yaml.dump(owner.yaml)) + + +@owner_bindable.field("plan") +@require_part_of_org +@sync_to_async +def resolve_plan(owner: Owner, info: GraphQLResolveInfo) -> PlanService: + return PlanService(current_org=owner) + + +@owner_bindable.field("pretrialPlan") +@require_part_of_org +@sync_to_async +def resolve_plan_representation(owner: Owner, info: GraphQLResolveInfo) -> Plan: + info.context["plan_service"] = PlanService(current_org=owner) + free_plan = Plan.objects.select_related("tier").get(name=DEFAULT_FREE_PLAN) + return free_plan + + +@owner_bindable.field("availablePlans") +@require_part_of_org +@sync_to_async +def resolve_available_plans(owner: Owner, info: GraphQLResolveInfo) -> List[Plan]: + plan_service = PlanService(current_org=owner) + info.context["plan_service"] = plan_service + owner = info.context["request"].current_owner + return plan_service.available_plans(owner=owner) + + +@owner_bindable.field("hasPrivateRepos") +@sync_to_async +@require_part_of_org +def resolve_has_private_repos(owner: Owner, info: GraphQLResolveInfo) -> bool: + return owner.has_private_repos + + +@owner_bindable.field("hasPublicRepos") +@sync_to_async +@require_part_of_org +def resolve_has_public_repos(owner: Owner, info: GraphQLResolveInfo) -> bool: + return owner.has_public_repos + + +@owner_bindable.field("hasActiveRepos") +@sync_to_async +@require_part_of_org +def resolve_has_active_repos(owner: Owner, info: GraphQLResolveInfo) -> bool: + return owner.has_active_repos + + +@owner_bindable.field("ownerid") +@require_part_of_org +def resolve_ownerid(owner: Owner, info: GraphQLResolveInfo) -> int: + return owner.ownerid + + +COVERAGE_FIELDS = { + "coverageAnalytics.percentCovered", + "coverageAnalytics.commitSha", + "coverageAnalytics.hits", + "coverageAnalytics.misses", + "coverageAnalytics.lines", +} +COMMITS_FIELDS = {"oldestCommitAt"} + + +@owner_bindable.field("repository") +async def resolve_repository( + owner: Owner, info: GraphQLResolveInfo, name: str +) -> Repository | NotFoundError: + command = info.context["executor"].get_command("repository") + okta_authenticated_accounts: list[int] = info.context["request"].session.get( + OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY, [] + ) + + is_impersonation = info.context["request"].impersonation + # If the user is impersonating another user, we want to show all the Okta repos. + # This means we do not want to filter out the Okta enforced repos + exclude_okta_enforced_repos = not is_impersonation + + requested_fields = selected_fields(info) + needs_coverage = not requested_fields.isdisjoint(COVERAGE_FIELDS) + needs_commits = not requested_fields.isdisjoint(COMMITS_FIELDS) + + repository: Repository | None = await command.fetch_repository( + owner, + name, + okta_authenticated_accounts, + exclude_okta_enforced_repos=exclude_okta_enforced_repos, + needs_coverage=needs_coverage, + needs_commits=needs_commits, + ) + + if repository is None: + return NotFoundError() + + sentry_sdk.set_tags( + { + "owner_username": repository.author.username, + "owner_service": repository.author.service, + "owner_plan": repository.author.plan, + "owner_id": repository.author.ownerid, + "repo_name": repository.name, + "repo_id": repository.repoid, + } + ) + + current_owner = info.context["request"].current_owner + has_products_enabled = ( + repository.bundle_analysis_enabled + or repository.coverage_enabled + or repository.test_analytics_enabled + ) + + if repository.private and has_products_enabled: + await sync_to_async(activation.try_auto_activate)(owner, current_owner) + + return repository + + +@owner_bindable.field("numberOfUploads") +@require_part_of_org +async def resolve_number_of_uploads( + owner: Owner, info: GraphQLResolveInfo, **kwargs: Any +) -> int: + command = info.context["executor"].get_command("owner") + return await command.get_uploads_number_per_user(owner) + + +@owner_bindable.field("isAdmin") +@require_part_of_org +def resolve_is_current_user_an_admin(owner: Owner, info: GraphQLResolveInfo) -> bool: + current_owner = info.context["request"].current_owner + command = info.context["executor"].get_command("owner") + return command.get_is_current_user_an_admin(owner, current_owner) + + +@owner_bindable.field("hashOwnerid") +@require_part_of_org +def resolve_hash_ownerid(owner: Owner, info: GraphQLResolveInfo) -> str: + hash_ownerid = sha1(str(owner.ownerid).encode()) + return hash_ownerid.hexdigest() + + +@owner_bindable.field("orgUploadToken") +@require_part_of_org +def resolve_org_upload_token( + owner: Owner, info: GraphQLResolveInfo, **kwargs: Any +) -> str: + should_hide_tokens = settings.HIDE_ALL_CODECOV_TOKENS + current_owner = info.context["request"].current_owner + command = info.context["executor"].get_command("owner") + is_owner_admin = current_owner.is_admin(owner) + if should_hide_tokens and not is_owner_admin: + return TOKEN_UNAVAILABLE + + return command.get_org_upload_token(owner) + + +@owner_bindable.field("defaultOrgUsername") +@sync_to_async +@require_part_of_org +def resolve_org_default_org_username( + owner: Owner, info: GraphQLResolveInfo, **kwargs: Any +) -> Optional[str]: + return None if owner.default_org is None else owner.default_org.username + + +@owner_bindable.field("measurements") +@sync_to_async +def resolve_measurements( + owner: Owner, + info: GraphQLResolveInfo, + interval: Interval, + after: Optional[datetime] = None, + before: Optional[datetime] = None, + repos: Optional[List[str]] = None, + is_public: Optional[bool] = None, +) -> Iterable[dict]: + current_owner = info.context["request"].current_owner + + okta_authenticated_accounts: list[int] = info.context["request"].session.get( + OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY, [] + ) + + queryset = ( + Repository.objects.filter(author=owner) + .viewable_repos(current_owner) + .exclude_accounts_enforced_okta(okta_authenticated_accounts) + ) + + if is_public is not None: + queryset = queryset.filter(private=not is_public) + + if repos is None: + repo_ids = queryset.values_list("pk", flat=True) + else: + repo_ids = queryset.filter(name__in=repos).values_list("pk", flat=True) + + return fill_sparse_measurements( + timeseries_helpers.owner_coverage_measurements_with_fallback( + owner, + list(repo_ids), + interval, + start_date=after, + end_date=before, + ), + interval, + start_date=after, + end_date=before, + ) + + +@owner_bindable.field("isCurrentUserActivated") +@sync_to_async +def resolve_is_current_user_activated(owner: Owner, info: GraphQLResolveInfo) -> bool: + current_user = info.context["request"].user + if not current_user.is_authenticated: + return False + + current_owner = info.context["request"].current_owner + if not current_owner: + return False + + if owner.ownerid == current_owner.ownerid: + return True + if owner.plan_activated_users is None: + return False + + return ( + bool(owner.plan_activated_users) + and current_owner.ownerid in owner.plan_activated_users + ) + + +@owner_bindable.field("invoices") +@require_part_of_org +def resolve_owner_invoices(owner: Owner, info: GraphQLResolveInfo) -> list | None: + return BillingService(requesting_user=owner).list_filtered_invoices(owner, 100) + + +@owner_bindable.field("isGithubRateLimited") +@sync_to_async +def resolve_is_github_rate_limited( + owner: Owner, info: GraphQLResolveInfo +) -> bool | None: + if owner.service != SERVICE_GITHUB and owner.service != SERVICE_GITHUB_ENTERPRISE: + return False + redis_connection = get_redis_connection() + rate_limit_redis_key = rate_limits.determine_entity_redis_key( + owner=owner, repository=None + ) + return rate_limits.determine_if_entity_is_rate_limited( + redis_connection, rate_limit_redis_key + ) + + +@owner_bindable.field("invoice") +@require_part_of_org +def resolve_owner_invoice( + owner: Owner, + info: GraphQLResolveInfo, + invoice_id: str, +) -> stripe.Invoice | None: + return BillingService(requesting_user=owner).get_invoice(owner, invoice_id) + + +@owner_bindable.field("account") +@require_part_of_org +@sync_to_async +def resolve_owner_account(owner: Owner, info: GraphQLResolveInfo) -> dict: + account_id = owner.account_id + return Account.objects.filter(pk=account_id).first() + + +@owner_bindable.field("isUserOktaAuthenticated") +@sync_to_async +@require_part_of_org +def resolve_is_user_okta_authenticated(owner: Owner, info: GraphQLResolveInfo) -> bool: + okta_signed_in_accounts = info.context["request"].session.get( + OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY, + [], + ) + if owner.account_id: + return owner.account_id in okta_signed_in_accounts + return False + + +@owner_bindable.field("delinquent") +@require_part_of_org +def resolve_delinquent(owner: Owner, info: GraphQLResolveInfo) -> bool | None: + return owner.delinquent + + +@owner_bindable.field("aiFeaturesEnabled") +@sync_to_async +@require_part_of_org +def resolve_ai_features_enabled(owner: Owner, info: GraphQLResolveInfo) -> bool: + return GithubAppInstallation.objects.filter( + app_id=AI_FEATURES_GH_APP_ID, owner=owner + ).exists() + + +@owner_bindable.field("aiEnabledRepos") +@sync_to_async +@require_part_of_org +def resolve_ai_enabled_repos( + owner: Owner, info: GraphQLResolveInfo +) -> List[str] | None: + ai_features_app_install = GithubAppInstallation.objects.filter( + app_id=AI_FEATURES_GH_APP_ID, owner=owner + ).first() + + if not ai_features_app_install: + return None + + current_owner = info.context["request"].current_owner + queryset = Repository.objects.filter(author=owner).viewable_repos(current_owner) + + if ai_features_app_install.repository_service_ids: + queryset = queryset.filter( + service_id__in=ai_features_app_install.repository_service_ids + ) + + return list(queryset.values_list("name", flat=True)) + + +@owner_bindable.field("uploadTokenRequired") +@require_part_of_org +def resolve_upload_token_required( + owner: Owner, info: GraphQLResolveInfo +) -> bool | None: + return owner.upload_token_required_for_public_repos + + +@owner_bindable.field("activatedUserCount") +@sync_to_async +@require_shared_account_or_part_of_org +def resolve_activated_user_count(owner: Owner, info: GraphQLResolveInfo) -> int: + return owner.activated_user_count + + +@owner_bindable.field("billing") +@sync_to_async +@require_part_of_org +def resolve_billing(owner: Owner, info: GraphQLResolveInfo) -> dict | None: + return owner diff --git a/apps/codecov-api/graphql_api/types/path_contents/__init__.py b/apps/codecov-api/graphql_api/types/path_contents/__init__.py new file mode 100644 index 0000000000..a20cf9d6a0 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/path_contents/__init__.py @@ -0,0 +1,19 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .path_content import ( + deprecated_path_contents_result_bindable, + path_content_bindable, + path_content_file_bindable, + path_contents_result_bindable, +) + +path_content = ariadne_load_local_graphql(__file__, "path_content.graphql") + + +__all__ = [ + "path_content", + "path_content_bindable", + "path_content_file_bindable", + "path_contents_result_bindable", + "deprecated_path_contents_result_bindable", +] diff --git a/apps/codecov-api/graphql_api/types/path_contents/path_content.graphql b/apps/codecov-api/graphql_api/types/path_contents/path_content.graphql new file mode 100644 index 0000000000..88382fa95b --- /dev/null +++ b/apps/codecov-api/graphql_api/types/path_contents/path_content.graphql @@ -0,0 +1,58 @@ +interface PathContent { + name: String! + path: String! + hits: Int! + misses: Int! + partials: Int! + lines: Int! + percentCovered: Float! +} + +type PathContentFile implements PathContent { + name: String! + path: String! + hits: Int! + misses: Int! + partials: Int! + lines: Int! + percentCovered: Float! +} + +type PathContentDir implements PathContent { + name: String! + path: String! + hits: Int! + misses: Int! + partials: Int! + lines: Int! + percentCovered: Float! +} + +type PathContents { + results: [PathContent!]! +} + +type PathContentEdge { + cursor: String! + node: PathContent! +} + +type PathContentConnection { + edges: [PathContentEdge!]! + totalCount: Int! + pageInfo: PageInfo! +} + +union PathContentsResult = + PathContents + | MissingHeadReport + | MissingCoverage + | UnknownPath + | UnknownFlags + +union DeprecatedPathContentsResult = + PathContentConnection + | MissingHeadReport + | MissingCoverage + | UnknownPath + | UnknownFlags diff --git a/apps/codecov-api/graphql_api/types/path_contents/path_content.py b/apps/codecov-api/graphql_api/types/path_contents/path_content.py new file mode 100644 index 0000000000..192fcdf838 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/path_contents/path_content.py @@ -0,0 +1,92 @@ +from typing import List, Union + +from ariadne import InterfaceType, ObjectType, UnionType + +from graphql_api.helpers.connection import ( + ArrayConnection, + Connection, +) +from graphql_api.types.errors import MissingCoverage, MissingHeadReport, UnknownPath +from graphql_api.types.errors.errors import UnknownFlags +from services.path import Dir, File + +path_content_bindable = InterfaceType("PathContent") +path_content_file_bindable = ObjectType("PathContentFile") + + +@path_content_bindable.type_resolver +def resolve_path_content_type(obj, *_): + if isinstance(obj, File): + return "PathContentFile" + if isinstance(obj, Dir): + return "PathContentDir" + return None + + +@path_content_bindable.field("name") +def resolve_name(item: Union[File, Dir], info) -> str: + return item.name + + +@path_content_bindable.field("path") +def resolve_file_path(item: Union[File, Dir], info) -> str: + return item.full_path + + +@path_content_bindable.field("hits") +def resolve_hits(item: Union[File, Dir], info) -> int: + return item.hits + + +@path_content_bindable.field("misses") +def resolve_misses(item: Union[File, Dir], info) -> int: + return item.misses + + +@path_content_bindable.field("partials") +def resolve_partials(item: Union[File, Dir], info) -> int: + return item.partials + + +@path_content_bindable.field("lines") +def resolve_lines(item: Union[File, Dir], info) -> int: + return item.lines + + +@path_content_bindable.field("percentCovered") +def resolve_percent_covered(item: Union[File, Dir], info) -> float: + return item.coverage + + +path_contents_result_bindable = UnionType("PathContentsResult") + + +@path_contents_result_bindable.type_resolver +def resolve_path_contents_result_type(res, *_): + if isinstance(res, MissingHeadReport): + return "MissingHeadReport" + elif isinstance(res, MissingCoverage): + return "MissingCoverage" + elif isinstance(res, UnknownPath): + return "UnknownPath" + elif isinstance(res, UnknownFlags): + return "UnknownFlags" + if isinstance(res, type({"results": List[Union[File, Dir]]})): + return "PathContents" + + +deprecated_path_contents_result_bindable = UnionType("DeprecatedPathContentsResult") + + +@deprecated_path_contents_result_bindable.type_resolver +def resolve_deprecated_path_contents_result_type(res, *_): + if isinstance(res, MissingHeadReport): + return "MissingHeadReport" + elif isinstance(res, MissingCoverage): + return "MissingCoverage" + elif isinstance(res, UnknownPath): + return "UnknownPath" + elif isinstance(res, UnknownFlags): + return "UnknownFlags" + elif isinstance(res, (Connection, ArrayConnection)): + return "PathContentConnection" diff --git a/apps/codecov-api/graphql_api/types/plan/__init__.py b/apps/codecov-api/graphql_api/types/plan/__init__.py new file mode 100644 index 0000000000..b60ae134c6 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/plan/__init__.py @@ -0,0 +1,6 @@ +from .plan import plan, plan_bindable + +__all__ = [ + "plan", + "plan_bindable", +] diff --git a/apps/codecov-api/graphql_api/types/plan/plan.graphql b/apps/codecov-api/graphql_api/types/plan/plan.graphql new file mode 100644 index 0000000000..505610c78a --- /dev/null +++ b/apps/codecov-api/graphql_api/types/plan/plan.graphql @@ -0,0 +1,22 @@ +type Plan { + baseUnitPrice: Int! + benefits: [String!]! + billingRate: String + hasSeatsLeft: Boolean! + isEnterprisePlan: Boolean! + isFreePlan: Boolean! + isProPlan: Boolean! + isSentryPlan: Boolean! + isTeamPlan: Boolean! + isTrialPlan: Boolean! + marketingName: String! + monthlyUploadLimit: Int + planUserCount: Int + pretrialUsersCount: Int + tierName: String! + trialEndDate: DateTime + trialStartDate: DateTime + trialStatus: TrialStatus! + trialTotalDays: Int + value: String! +} diff --git a/apps/codecov-api/graphql_api/types/plan/plan.py b/apps/codecov-api/graphql_api/types/plan/plan.py new file mode 100644 index 0000000000..c790170b77 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/plan/plan.py @@ -0,0 +1,133 @@ +from datetime import datetime +from typing import List, Optional + +from ariadne import ObjectType +from asgiref.sync import sync_to_async +from shared.plan.constants import PlanBillingRate, TrialStatus +from shared.plan.service import PlanService + +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +plan = ariadne_load_local_graphql(__file__, "plan.graphql") +plan_bindable = ObjectType("Plan") + + +@plan_bindable.field("trialStartDate") +def resolve_trial_start_date(plan_service: PlanService, info) -> Optional[datetime]: + return plan_service.trial_start_date + + +@plan_bindable.field("trialTotalDays") +@sync_to_async +def resolve_trial_total_days(plan_service: PlanService, info) -> Optional[int]: + return plan_service.trial_total_days + + +@plan_bindable.field("trialEndDate") +def resolve_trial_end_date(plan_service: PlanService, info) -> Optional[datetime]: + return plan_service.trial_end_date + + +@plan_bindable.field("trialStatus") +def resolve_trial_status(plan_service: PlanService, info) -> TrialStatus: + if not plan_service.trial_status: + return TrialStatus.NOT_STARTED + return TrialStatus(plan_service.trial_status) + + +@plan_bindable.field("marketingName") +@sync_to_async +def resolve_marketing_name(plan_service: PlanService, info) -> str: + return plan_service.marketing_name + + +@plan_bindable.field("value") +@sync_to_async +def resolve_plan_name_as_value(plan_service: PlanService, info) -> str: + return plan_service.plan_name + + +@plan_bindable.field("tierName") +@sync_to_async +def resolve_tier_name(plan_service: PlanService, info) -> str: + return plan_service.tier_name + + +@plan_bindable.field("billingRate") +@sync_to_async +def resolve_billing_rate(plan_service: PlanService, info) -> Optional[PlanBillingRate]: + return plan_service.billing_rate + + +@plan_bindable.field("baseUnitPrice") +@sync_to_async +def resolve_base_unit_price(plan_service: PlanService, info) -> int: + return plan_service.base_unit_price + + +@plan_bindable.field("benefits") +@sync_to_async +def resolve_benefits(plan_service: PlanService, info) -> List[str]: + return plan_service.benefits + + +@plan_bindable.field("pretrialUsersCount") +@sync_to_async +def resolve_pretrial_users_count(plan_service: PlanService, info) -> Optional[int]: + if plan_service.is_org_trialing: + return plan_service.pretrial_users_count + return None + + +@plan_bindable.field("monthlyUploadLimit") +@sync_to_async +def resolve_monthly_uploads_limit(plan_service: PlanService, info) -> Optional[int]: + return plan_service.monthly_uploads_limit + + +@plan_bindable.field("planUserCount") +@sync_to_async +def resolve_plan_user_count(plan_service: PlanService, info) -> int: + return plan_service.plan_user_count + + +@plan_bindable.field("hasSeatsLeft") +@sync_to_async +def resolve_has_seats_left(plan_service: PlanService, info) -> bool: + return plan_service.has_seats_left + + +@plan_bindable.field("isEnterprisePlan") +@sync_to_async +def resolve_is_enterprise_plan(plan_service: PlanService, info) -> bool: + return plan_service.is_enterprise_plan + + +@plan_bindable.field("isFreePlan") +@sync_to_async +def resolve_is_free_plan(plan_service: PlanService, info) -> bool: + return plan_service.is_free_plan + + +@plan_bindable.field("isProPlan") +@sync_to_async +def resolve_is_pro_plan(plan_service: PlanService, info) -> bool: + return plan_service.is_pro_plan + + +@plan_bindable.field("isSentryPlan") +@sync_to_async +def resolve_is_sentry_plan(plan_service: PlanService, info) -> bool: + return plan_service.is_sentry_plan + + +@plan_bindable.field("isTeamPlan") +@sync_to_async +def resolve_is_team_plan(plan_service: PlanService, info) -> bool: + return plan_service.is_team_plan + + +@plan_bindable.field("isTrialPlan") +@sync_to_async +def resolve_is_trial_plan(plan_service: PlanService, info) -> bool: + return plan_service.is_trial_plan diff --git a/apps/codecov-api/graphql_api/types/plan_representation/__init__.py b/apps/codecov-api/graphql_api/types/plan_representation/__init__.py new file mode 100644 index 0000000000..f033ede1a7 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/plan_representation/__init__.py @@ -0,0 +1,6 @@ +from .plan_representation import plan_representation, plan_representation_bindable + +__all__ = [ + "plan_representation", + "plan_representation_bindable", +] diff --git a/apps/codecov-api/graphql_api/types/plan_representation/plan_representation.graphql b/apps/codecov-api/graphql_api/types/plan_representation/plan_representation.graphql new file mode 100644 index 0000000000..33a3948487 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/plan_representation/plan_representation.graphql @@ -0,0 +1,14 @@ +type PlanRepresentation { + baseUnitPrice: Int! + benefits: [String!]! + billingRate: String + isEnterprisePlan: Boolean! + isFreePlan: Boolean! + isProPlan: Boolean! + isTeamPlan: Boolean! + isSentryPlan: Boolean! + isTrialPlan: Boolean! + marketingName: String! + monthlyUploadLimit: Int + value: String! +} diff --git a/apps/codecov-api/graphql_api/types/plan_representation/plan_representation.py b/apps/codecov-api/graphql_api/types/plan_representation/plan_representation.py new file mode 100644 index 0000000000..8ecc2f6434 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/plan_representation/plan_representation.py @@ -0,0 +1,83 @@ +from typing import List, Optional + +from ariadne import ObjectType +from asgiref.sync import sync_to_async +from shared.plan.service import PlanService + +from codecov_auth.models import Plan +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +plan_representation = ariadne_load_local_graphql( + __file__, "plan_representation.graphql" +) +plan_representation_bindable = ObjectType("PlanRepresentation") + + +@plan_representation_bindable.field("marketingName") +def resolve_marketing_name(plan_data: Plan, info) -> str: + return plan_data.marketing_name + + +@plan_representation_bindable.field("value") +def resolve_plan_value(plan_data: Plan, info) -> str: + return plan_data.name + + +@plan_representation_bindable.field("billingRate") +def resolve_billing_rate(plan_data: Plan, info) -> Optional[str]: + return plan_data.billing_rate + + +@plan_representation_bindable.field("baseUnitPrice") +def resolve_base_unit_price(plan_data: Plan, info) -> int: + return plan_data.base_unit_price + + +@plan_representation_bindable.field("benefits") +@sync_to_async +def resolve_benefits(plan_data: Plan, info) -> List[str]: + plan_service: PlanService = info.context["plan_service"] + if plan_service.is_org_trialing: + benefits_with_pretrial_users = [ + benefit.replace( + "Up to 1 user", f"Up to {plan_service.pretrial_users_count} users" + ) + for benefit in plan_data.benefits + ] + return benefits_with_pretrial_users + return plan_data.benefits + + +@plan_representation_bindable.field("monthlyUploadLimit") +def resolve_monthly_uploads_limit(plan_data: Plan, info) -> Optional[int]: + return plan_data.monthly_uploads_limit + + +@plan_representation_bindable.field("isEnterprisePlan") +def resolve_is_enterprise(plan_data: Plan, info) -> bool: + return plan_data.is_enterprise_plan + + +@plan_representation_bindable.field("isFreePlan") +def resolve_is_free(plan_data: Plan, info) -> bool: + return plan_data.is_free_plan + + +@plan_representation_bindable.field("isProPlan") +def resolve_is_pro(plan_data: Plan, info) -> bool: + return plan_data.is_pro_plan + + +@plan_representation_bindable.field("isTeamPlan") +def resolve_is_team(plan_data: Plan, info) -> bool: + return plan_data.is_team_plan + + +@plan_representation_bindable.field("isSentryPlan") +def resolve_is_sentry(plan_data: Plan, info) -> bool: + return plan_data.is_sentry_plan + + +@plan_representation_bindable.field("isTrialPlan") +def resolve_is_trial(plan_data: Plan, info) -> bool: + return plan_data.is_trial_plan diff --git a/apps/codecov-api/graphql_api/types/profile/__init__.py b/apps/codecov-api/graphql_api/types/profile/__init__.py new file mode 100644 index 0000000000..ebf29f3210 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/profile/__init__.py @@ -0,0 +1,11 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .profile import profile_bindable + +profile = ariadne_load_local_graphql(__file__, "profile.graphql") + + +__all__ = [ + "profile", + "profile_bindable", +] diff --git a/apps/codecov-api/graphql_api/types/profile/profile.graphql b/apps/codecov-api/graphql_api/types/profile/profile.graphql new file mode 100644 index 0000000000..bf43274b9a --- /dev/null +++ b/apps/codecov-api/graphql_api/types/profile/profile.graphql @@ -0,0 +1,6 @@ +type Profile { + otherGoal: String + goals: [GoalOnboarding]! + typeProjects: [TypeProjectOnboarding]! + createdAt: DateTime! +} diff --git a/apps/codecov-api/graphql_api/types/profile/profile.py b/apps/codecov-api/graphql_api/types/profile/profile.py new file mode 100644 index 0000000000..2f68945ceb --- /dev/null +++ b/apps/codecov-api/graphql_api/types/profile/profile.py @@ -0,0 +1,15 @@ +from ariadne import ObjectType + +from graphql_api.types.enums.enums import GoalOnboarding, TypeProjectOnboarding + +profile_bindable = ObjectType("Profile") + + +@profile_bindable.field("goals") +def resolve_goals(profile, _): + return [GoalOnboarding(goal) for goal in profile.goals] + + +@profile_bindable.field("typeProjects") +def resolve_type_projects(profile, _): + return [TypeProjectOnboarding(project) for project in profile.type_projects] diff --git a/apps/codecov-api/graphql_api/types/pull/__init__.py b/apps/codecov-api/graphql_api/types/pull/__init__.py new file mode 100644 index 0000000000..6eb770cb08 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/pull/__init__.py @@ -0,0 +1,11 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .pull import pull_bindable + +pull = ariadne_load_local_graphql(__file__, "pull.graphql") + + +__all__ = [ + "pull", + "pull_bindable", +] diff --git a/apps/codecov-api/graphql_api/types/pull/pull.graphql b/apps/codecov-api/graphql_api/types/pull/pull.graphql new file mode 100644 index 0000000000..49f9be6ffd --- /dev/null +++ b/apps/codecov-api/graphql_api/types/pull/pull.graphql @@ -0,0 +1,20 @@ +type Pull { + behindBy: Int + behindByCommit: String + title: String + state: PullRequestState! + pullId: Int! + author: Owner + updatestamp: DateTime + head: Commit + comparedTo: Commit + compareWithBase: ComparisonResult + bundleAnalysisCompareWithBase: BundleAnalysisComparisonResult + commits( + first: Int + after: String + last: Int + before: String + ): CommitConnection @cost(complexity: 10, multipliers: ["first", "last"]) + firstPull: Boolean! +} diff --git a/apps/codecov-api/graphql_api/types/pull/pull.py b/apps/codecov-api/graphql_api/types/pull/pull.py new file mode 100644 index 0000000000..2845e57148 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/pull/pull.py @@ -0,0 +1,166 @@ +from typing import Any, Optional, Union + +import sentry_sdk +from ariadne import ObjectType +from asgiref.sync import sync_to_async +from graphql import GraphQLResolveInfo + +from codecov_auth.models import Owner +from compare.models import CommitComparison +from core.models import Commit, Pull +from graphql_api.actions.commits import pull_commits +from graphql_api.actions.comparison import validate_commit_comparison +from graphql_api.dataloader.bundle_analysis import load_bundle_analysis_comparison +from graphql_api.dataloader.commit import CommitLoader +from graphql_api.dataloader.comparison import ComparisonLoader +from graphql_api.dataloader.owner import OwnerLoader +from graphql_api.helpers.connection import Connection, queryset_to_connection_sync +from graphql_api.types.comparison.comparison import ( + FirstPullRequest, + MissingBaseCommit, + MissingHeadCommit, +) +from graphql_api.types.enums import OrderingDirection, PullRequestState +from services.bundle_analysis import BundleAnalysisComparison +from services.comparison import ComparisonReport, PullRequestComparison + +pull_bindable = ObjectType("Pull") + +pull_bindable.set_alias("pullId", "pullid") + + +@pull_bindable.field("state") +def resolve_state(pull: Pull, info: GraphQLResolveInfo) -> PullRequestState: + return PullRequestState(pull.state) + + +@pull_bindable.field("author") +def resolve_author(pull: Pull, info: GraphQLResolveInfo) -> Optional[Owner]: + if pull.author_id: + return OwnerLoader.loader(info).load(pull.author_id) + + +@pull_bindable.field("head") +def resolve_head(pull: Pull, info: GraphQLResolveInfo) -> Optional[Commit]: + if pull.head is None: + return None + return CommitLoader.loader(info, pull.repository_id).load(pull.head) + + +@pull_bindable.field("comparedTo") +def resolve_base(pull: Pull, info: GraphQLResolveInfo) -> Optional[Commit]: + if pull.compared_to is None: + return None + return CommitLoader.loader(info, pull.repository_id).load(pull.compared_to) + + +@sync_to_async +def is_first_pull_request(pull: Pull) -> bool: + return pull.repository.pull_requests.order_by("id").first() == pull + + +@pull_bindable.field("compareWithBase") +@sentry_sdk.trace +async def resolve_compare_with_base( + pull: Pull, info: GraphQLResolveInfo, **kwargs: Any +) -> Union[CommitComparison, Any]: + if not pull.compared_to: + if await is_first_pull_request(pull): + return FirstPullRequest() + else: + return MissingBaseCommit() + if not pull.head: + return MissingHeadCommit() + + comparison_loader = ComparisonLoader.loader(info, pull.repository_id) + commit_comparison = await comparison_loader.load((pull.compared_to, pull.head)) + + comparison_error = validate_commit_comparison(commit_comparison=commit_comparison) + + if comparison_error: + return comparison_error + + if commit_comparison and commit_comparison.is_processed: + current_owner = info.context["request"].current_owner + comparison = PullRequestComparison(current_owner, pull) + # store the comparison in the context - to be used in the `Comparison` resolvers + info.context["comparison"] = comparison + + if commit_comparison: + return ComparisonReport(commit_comparison) + + +@pull_bindable.field("bundleAnalysisCompareWithBase") +@sync_to_async +@sentry_sdk.trace +def resolve_bundle_analysis_compare_with_base( + pull: Pull, info: GraphQLResolveInfo, **kwargs: Any +) -> Union[BundleAnalysisComparison, Any]: + if not pull.compared_to: + if pull.repository.pull_requests.order_by("id").first() == pull: + return FirstPullRequest() + else: + return MissingBaseCommit() + + # Handles a case where the PR was created without any uploads because all bundles + # from the build are cached. Instead of showing a "no commit error" we will instead + # show the parent bundle report as it implies everything was cached and carried + # over to the head commit + head_commit_sha = pull.head if pull.head else pull.compared_to + + bundle_analysis_comparison = load_bundle_analysis_comparison( + Commit.objects.filter( + commitid=pull.compared_to, repository=pull.repository + ).first(), + Commit.objects.filter( + commitid=head_commit_sha, repository=pull.repository + ).first(), + ) + + # Store the created SQLite DB path in info.context + # when the request is fully handled, have the file deleted + if isinstance(bundle_analysis_comparison, BundleAnalysisComparison): + info.context[ + "request" + ].bundle_analysis_base_report_db_path = ( + bundle_analysis_comparison.comparison.base_report.db_path + ) + info.context[ + "request" + ].bundle_analysis_head_report_db_path = ( + bundle_analysis_comparison.comparison.head_report.db_path + ) + + return bundle_analysis_comparison + + +@pull_bindable.field("commits") +@sync_to_async +def resolve_commits(pull: Pull, info: GraphQLResolveInfo, **kwargs: Any) -> Connection: + queryset = pull_commits(pull) + + return queryset_to_connection_sync( + queryset, + ordering=("timestamp",), + ordering_direction=OrderingDirection.DESC, + **kwargs, + ) + + +@pull_bindable.field("behindBy") +def resolve_behind_by(pull: Pull, info: GraphQLResolveInfo, **kwargs: Any) -> int: + return pull.behind_by + + +@pull_bindable.field("behindByCommit") +def resolve_behind_by_commit( + pull: Pull, info: GraphQLResolveInfo, **kwargs: Any +) -> str: + return pull.behind_by_commit + + +@pull_bindable.field("firstPull") +@sync_to_async +def resolve_first_pull(pull: Pull, info: GraphQLResolveInfo) -> bool: + # returns true if this pull is/was the 1st for a repo + return pull.repository.pull_requests.order_by("id").first() == pull diff --git a/apps/codecov-api/graphql_api/types/query/__init__.py b/apps/codecov-api/graphql_api/types/query/__init__.py new file mode 100644 index 0000000000..66521fb059 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/query/__init__.py @@ -0,0 +1,6 @@ +from .query import query, query_bindable + +__all__ = [ + "query", + "query_bindable", +] diff --git a/apps/codecov-api/graphql_api/types/query/query.graphql b/apps/codecov-api/graphql_api/types/query/query.graphql new file mode 100644 index 0000000000..d008003838 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/query/query.graphql @@ -0,0 +1,14 @@ +scalar DateTime + +type Query { + me: Me + owner(username: String!): Owner + config: Config! +} + +type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String +} diff --git a/apps/codecov-api/graphql_api/types/query/query.py b/apps/codecov-api/graphql_api/types/query/query.py new file mode 100644 index 0000000000..9114aa8ef4 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/query/query.py @@ -0,0 +1,79 @@ +from typing import Any, Optional + +from ariadne import ObjectType +from asgiref.sync import sync_to_async +from django.conf import settings +from graphql import GraphQLResolveInfo +from sentry_sdk import Scope + +from codecov.commands.exceptions import UnauthorizedGuestAccess +from codecov_auth.models import Owner +from graphql_api.actions.owner import get_owner +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +query = ariadne_load_local_graphql(__file__, "query.graphql") +query_bindable = ObjectType("Query") + + +def query_name(info: GraphQLResolveInfo) -> Optional[str]: + if info.operation and info.operation.name: + return info.operation.name.value + + +def configure_sentry_scope(query_name: Optional[str]) -> None: + # this sets the Sentry transaction name to the GraphQL query name which + # should make it easier to search/filter transactions + # we're configuring this here since it's the main entrypoint into GraphQL resolvers + + # https://docs.sentry.io/platforms/python/enriching-events/transaction-name/ + scope = Scope.get_current_scope() + if scope.transaction: + scope.transaction.name = f"GraphQL [{query_name}]" + + +@query_bindable.field("me") +@sync_to_async +def resolve_me(_: Any, info: GraphQLResolveInfo) -> Optional[Owner]: + configure_sentry_scope(query_name(info)) + # will be `None` for anonymous users or users w/ no linked owners + return info.context["request"].current_owner + + +@query_bindable.field("owner") +async def resolve_owner( + _: Any, info: GraphQLResolveInfo, username: str +) -> Optional[Owner]: + configure_sentry_scope(query_name(info)) + + service = info.context["service"] + if not service: + return None + + user = info.context["request"].current_owner or info.context["request"].user + + if settings.IS_ENTERPRISE and settings.GUEST_ACCESS is False: + if not user or not user.is_authenticated: + raise UnauthorizedGuestAccess() + + # if the owner tracks plan activated users, check if the user is in the list + target_owner = await get_owner(service, username) + has_plan_activated_users = ( + target_owner + and target_owner.plan_activated_users is not None + and len(target_owner.plan_activated_users) > 0 + ) + if ( + has_plan_activated_users + and user.ownerid not in target_owner.plan_activated_users + ): + raise UnauthorizedGuestAccess() + + return await get_owner(service, username) + + +@query_bindable.field("config") +def resolve_config(_: Any, info: GraphQLResolveInfo) -> object: + configure_sentry_scope(query_name(info)) + + # we have to return something here just to allow access to the child resolvers + return object() diff --git a/apps/codecov-api/graphql_api/types/repository/__init__.py b/apps/codecov-api/graphql_api/types/repository/__init__.py new file mode 100644 index 0000000000..0f42fc5137 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/repository/__init__.py @@ -0,0 +1,12 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .repository import repository_bindable, repository_result_bindable + +repository = ariadne_load_local_graphql(__file__, "repository.graphql") + + +__all__ = [ + "repository", + "repository_bindable", + "repository_result_bindable", +] diff --git a/apps/codecov-api/graphql_api/types/repository/repository.graphql b/apps/codecov-api/graphql_api/types/repository/repository.graphql new file mode 100644 index 0000000000..69590e47db --- /dev/null +++ b/apps/codecov-api/graphql_api/types/repository/repository.graphql @@ -0,0 +1,95 @@ +""" +Repository is a named collection of files uploaded +""" +type Repository { + isFirstPullRequest: Boolean! + repoid: Int! + name: String! + active: Boolean! + activated: Boolean! + private: Boolean! + oldestCommitAt: DateTime + latestCommitAt: DateTime + updatedAt: DateTime + author: Owner! + uploadToken: String + branch(name: String!): Branch + commit(id: String!): Commit + pull(id: Int!): Pull + pulls( + filters: PullsSetFilters + orderingDirection: OrderingDirection + first: Int + after: String + last: Int + before: String + ): PullConnection @cost(complexity: 10, multipliers: ["first", "last"]) + commits( + filters: CommitsSetFilters + first: Int + after: String + last: Int + before: String + ): CommitConnection @cost(complexity: 10, multipliers: ["first", "last"]) + branches( + filters: BranchesSetFilters + first: Int + after: String + last: Int + before: String + ): BranchConnection @cost(complexity: 3, multipliers: ["first", "last"]) + defaultBranch: String + graphToken: String + yaml: String + bot: Owner + repositoryConfig: RepositoryConfig + staticAnalysisToken: String + isATSConfigured: Boolean + primaryLanguage: String + languages: [String!] + bundleAnalysisEnabled: Boolean + coverageEnabled: Boolean + testAnalyticsEnabled: Boolean + isGithubRateLimited: Boolean + + "CoverageAnalytics are fields related to Codecov's Coverage product offering" + coverageAnalytics: CoverageAnalytics + + "TestAnalytics are fields related to Codecov's Test Analytics product offering" + testAnalytics: TestAnalytics +} + +type PullConnection { + edges: [PullEdge]! + totalCount: Int! + pageInfo: PageInfo! +} + +type PullEdge { + cursor: String! + node: Pull! +} + +type CommitConnection { + edges: [CommitEdge]! + totalCount: Int! + pageInfo: PageInfo! +} + +type CommitEdge { + cursor: String! + node: Commit! +} + +type BranchConnection { + edges: [BranchEdge]! + totalCount: Int! + pageInfo: PageInfo! +} + +type BranchEdge { + cursor: String! + node: Branch! +} + +union RepositoryResult = Repository | NotFoundError | OwnerNotActivatedError diff --git a/apps/codecov-api/graphql_api/types/repository/repository.py b/apps/codecov-api/graphql_api/types/repository/repository.py new file mode 100644 index 0000000000..145579f0b9 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/repository/repository.py @@ -0,0 +1,339 @@ +import logging +from datetime import datetime +from typing import Any, Dict, List, Optional + +import sentry_sdk +import shared.rate_limits as rate_limits +import yaml +from ariadne import ObjectType, UnionType +from asgiref.sync import sync_to_async +from django.conf import settings +from graphql.type.definition import GraphQLResolveInfo +from shared.helpers.redis import get_redis_connection + +from codecov_auth.models import SERVICE_GITHUB, SERVICE_GITHUB_ENTERPRISE, Owner +from core.models import Branch, Commit, Pull, Repository +from graphql_api.actions.commits import load_commit_statuses, repo_commits +from graphql_api.dataloader.commit import CommitLoader +from graphql_api.dataloader.owner import OwnerLoader +from graphql_api.helpers.connection import queryset_to_connection +from graphql_api.helpers.requested_fields import selected_fields +from graphql_api.types.coverage_analytics.coverage_analytics import ( + CoverageAnalyticsProps, +) +from graphql_api.types.enums import OrderingDirection +from graphql_api.types.enums.enum_types import PullRequestState +from graphql_api.types.errors.errors import NotFoundError, OwnerNotActivatedError + +TOKEN_UNAVAILABLE = "Token Unavailable. Please contact your admin." + +log = logging.getLogger(__name__) + +repository_bindable = ObjectType("Repository") + +repository_bindable.set_alias("updatedAt", "updatestamp") + +# latest_commit_at and coverage have their NULL value defaulted to -1/an old date +# so the NULL would end up last in the queryset as we do not have control over +# the order_by call. The true value of is under true_*; which would actually contain NULL +# see with_cache_latest_commit_at() from core/managers.py +repository_bindable.set_alias("latestCommitAt", "true_latest_commit_at") + + +@repository_bindable.field("repoid") +def resolve_repoid(repository: Repository, info: GraphQLResolveInfo) -> int: + return repository.repoid + + +@repository_bindable.field("name") +def resolve_name(repository: Repository, info: GraphQLResolveInfo) -> str: + return repository.name + + +@repository_bindable.field("oldestCommitAt") +def resolve_oldest_commit_at( + repository: Repository, info: GraphQLResolveInfo +) -> Optional[datetime]: + if hasattr(repository, "oldest_commit_at"): + return repository.oldest_commit_at + else: + return None + + +@repository_bindable.field("branch") +def resolve_branch( + repository: Repository, info: GraphQLResolveInfo, name: str +) -> Branch: + command = info.context["executor"].get_command("branch") + return command.fetch_branch(repository, name) + + +@repository_bindable.field("author") +def resolve_author(repository: Repository, info: GraphQLResolveInfo) -> Owner: + return OwnerLoader.loader(info).load(repository.author_id) + + +@repository_bindable.field("commit") +def resolve_commit(repository: Repository, info: GraphQLResolveInfo, id: str) -> Commit: + loader = CommitLoader.loader(info, repository.pk) + commit = loader.load(id) + + if commit: + sentry_sdk.set_tag("commit_sha", id) + + return commit + + +@repository_bindable.field("uploadToken") +def resolve_upload_token(repository: Repository, info: GraphQLResolveInfo) -> str: + should_hide_tokens = settings.HIDE_ALL_CODECOV_TOKENS + + current_owner = info.context["request"].current_owner + if not current_owner: + is_current_user_admin = False + else: + is_current_user_admin = current_owner.is_admin(repository.author) + + if should_hide_tokens and not is_current_user_admin: + return TOKEN_UNAVAILABLE + command = info.context["executor"].get_command("repository") + return command.get_upload_token(repository) + + +@repository_bindable.field("pull") +def resolve_pull(repository: Repository, info: GraphQLResolveInfo, id: int) -> Pull: + command = info.context["executor"].get_command("pull") + return command.fetch_pull_request(repository, id) + + +@repository_bindable.field("pulls") +async def resolve_pulls( + repository: Repository, + info: GraphQLResolveInfo, + filters: Optional[Dict[str, List[PullRequestState]]] = None, + ordering_direction: Optional[OrderingDirection] = OrderingDirection.DESC, + **kwargs: Any, +) -> List[Pull]: + command = info.context["executor"].get_command("pull") + queryset = await command.fetch_pull_requests(repository, filters) + return await queryset_to_connection( + queryset, + ordering=("pullid",), + ordering_direction=ordering_direction, + **kwargs, + ) + + +# the `requested_fields` here are prefixed with `edges.node`, as this is a `Connection` +# and using `commits { edges { node { ... } } }` is the way this is queried. +STATUS_FIELDS = {"edges.node.coverageStatus", "edges.node.bundleStatus"} + + +@repository_bindable.field("commits") +async def resolve_commits( + repository: Repository, + info: GraphQLResolveInfo, + filters: Optional[Dict[str, Any]] = None, + **kwargs: Any, +) -> List[Commit]: + queryset = await sync_to_async(repo_commits)(repository, filters) + connection = await queryset_to_connection( + queryset, + ordering=("timestamp",), + ordering_direction=OrderingDirection.DESC, + **kwargs, + ) + + for edge in connection.edges: + commit = edge["node"] + # cache all resulting commits in dataloader + loader = CommitLoader.loader(info, repository.repoid) + loader.cache(commit) + + requested_fields = selected_fields(info) + should_load_statuses = not requested_fields.isdisjoint(STATUS_FIELDS) + + if should_load_statuses: + commit_ids = [edge["node"].id for edge in connection.edges] + info.context["commit_statuses"] = await sync_to_async(load_commit_statuses)( + commit_ids + ) + + return connection + + +@repository_bindable.field("branches") +async def resolve_branches( + repository: Repository, + info: GraphQLResolveInfo, + filters: Optional[Dict[str, str | bool]] = None, + **kwargs: Any, +) -> List[Branch]: + command = info.context["executor"].get_command("branch") + queryset = await command.fetch_branches(repository, filters) + return await queryset_to_connection( + queryset, + ordering=("updatestamp",), + ordering_direction=OrderingDirection.DESC, + **kwargs, + ) + + +@repository_bindable.field("defaultBranch") +def resolve_default_branch(repository: Repository, info: GraphQLResolveInfo) -> str: + return repository.branch + + +@repository_bindable.field("staticAnalysisToken") +def resolve_static_analysis_token( + repository: Repository, info: GraphQLResolveInfo +) -> str: + command = info.context["executor"].get_command("repository") + return command.get_repository_token(repository, token_type="static_analysis") + + +@repository_bindable.field("graphToken") +def resolve_graph_token(repository: Repository, info: GraphQLResolveInfo) -> str: + return repository.image_token + + +@repository_bindable.field("yaml") +def resolve_repo_yaml( + repository: Repository, info: GraphQLResolveInfo +) -> Optional[str]: + if repository.yaml is None: + return None + return yaml.dump(repository.yaml) + + +@repository_bindable.field("bot") +@sync_to_async +def resolve_repo_bot( + repository: Repository, info: GraphQLResolveInfo +) -> Optional[Owner]: + return repository.bot + + +@repository_bindable.field("active") +def resolve_active(repository: Repository, info: GraphQLResolveInfo) -> bool: + return repository.active or False + + +@repository_bindable.field("isATSConfigured") +def resolve_is_ats_configured(repository: Repository, info: GraphQLResolveInfo) -> bool: + if not repository.yaml or "flag_management" not in repository.yaml: + return False + + # See https://docs.codecov.com/docs/getting-started-with-ats-github-actions on configuring + # flags. To use Automated Test Selection, a flag is required with Carryforward mode "labels". + individual_flags = repository.yaml["flag_management"].get("individual_flags", {}) + return individual_flags.get("carryforward_mode") == "labels" + + +@repository_bindable.field("repositoryConfig") +def resolve_repository_config( + repository: Repository, info: GraphQLResolveInfo +) -> Repository: + return repository + + +@repository_bindable.field("primaryLanguage") +def resolve_language(repository: Repository, info: GraphQLResolveInfo) -> str: + return repository.language + + +@repository_bindable.field("languages") +def resolve_languages(repository: Repository, info: GraphQLResolveInfo) -> List[str]: + return repository.languages + + +@repository_bindable.field("bundleAnalysisEnabled") +def resolve_bundle_analysis_enabled( + repository: Repository, info: GraphQLResolveInfo +) -> Optional[bool]: + return repository.bundle_analysis_enabled + + +@repository_bindable.field("testAnalyticsEnabled") +def resolve_test_analytics_enabled( + repository: Repository, info: GraphQLResolveInfo +) -> Optional[bool]: + return repository.test_analytics_enabled + + +@repository_bindable.field("coverageEnabled") +def resolve_coverage_enabled( + repository: Repository, info: GraphQLResolveInfo +) -> Optional[bool]: + return repository.coverage_enabled + + +repository_result_bindable = UnionType("RepositoryResult") + + +@repository_result_bindable.type_resolver +def resolve_repository_result_type(obj: Any, *_: Any) -> Optional[str]: + if isinstance(obj, Repository): + return "Repository" + elif isinstance(obj, OwnerNotActivatedError): + return "OwnerNotActivatedError" + elif isinstance(obj, NotFoundError): + return "NotFoundError" + + +@repository_bindable.field("isFirstPullRequest") +@sync_to_async +def resolve_is_first_pull_request( + repository: Repository, info: GraphQLResolveInfo +) -> bool: + # Get at most 2 PRs to determine if there's only one + pull_requests = repository.pull_requests.values("id", "compared_to")[:2] + if len(pull_requests) != 1: + return False + # For single PR, check if it's a valid first PR by verifying no compared_to + return pull_requests[0]["compared_to"] is None + + +@repository_bindable.field("isGithubRateLimited") +@sync_to_async +def resolve_is_github_rate_limited( + repository: Repository, info: GraphQLResolveInfo +) -> bool | None: + if ( + repository.service != SERVICE_GITHUB + and repository.service != SERVICE_GITHUB_ENTERPRISE + ): + return False + repo_owner = repository.author + try: + redis_connection = get_redis_connection() + rate_limit_redis_key = rate_limits.determine_entity_redis_key( + owner=repo_owner, repository=repository + ) + return rate_limits.determine_if_entity_is_rate_limited( + redis_connection, rate_limit_redis_key + ) + except Exception: + log.warning( + "Error when checking rate limit", + extra=dict(repo_id=repository.repoid, has_owner=bool(repo_owner)), + ) + return None + + +@repository_bindable.field("coverageAnalytics") +def resolve_coverage_analytics( + repository: Repository, info: GraphQLResolveInfo +) -> CoverageAnalyticsProps: + return CoverageAnalyticsProps(repository=repository) + + +@repository_bindable.field("testAnalytics") +def resolve_test_analytics( + repository: Repository, + info: GraphQLResolveInfo, +) -> Repository: + """ + resolve_test_analytics defines the data that will get passed to the testAnalytics resolvers + """ + return repository diff --git a/apps/codecov-api/graphql_api/types/repository_config/__init__.py b/apps/codecov-api/graphql_api/types/repository_config/__init__.py new file mode 100644 index 0000000000..0520f63a3b --- /dev/null +++ b/apps/codecov-api/graphql_api/types/repository_config/__init__.py @@ -0,0 +1,11 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .repository_config import indication_range_bindable, repository_config_bindable + +repository_config = ariadne_load_local_graphql(__file__, "repository_config.graphql") + +__all__ = [ + "repository_config", + "repository_config_bindable", + "indication_range_bindable", +] diff --git a/apps/codecov-api/graphql_api/types/repository_config/repository_config.graphql b/apps/codecov-api/graphql_api/types/repository_config/repository_config.graphql new file mode 100644 index 0000000000..6a32ad076c --- /dev/null +++ b/apps/codecov-api/graphql_api/types/repository_config/repository_config.graphql @@ -0,0 +1,8 @@ +type RepositoryConfig { + indicationRange: IndicationRange +} + +type IndicationRange { + upperRange: Float! + lowerRange: Float! +} diff --git a/apps/codecov-api/graphql_api/types/repository_config/repository_config.py b/apps/codecov-api/graphql_api/types/repository_config/repository_config.py new file mode 100644 index 0000000000..289ff315ca --- /dev/null +++ b/apps/codecov-api/graphql_api/types/repository_config/repository_config.py @@ -0,0 +1,41 @@ +from typing import TypedDict + +from ariadne import ObjectType +from asgiref.sync import sync_to_async +from shared.yaml.user_yaml import UserYaml + +from core.models import Repository +from graphql_api.dataloader.owner import OwnerLoader + +repository_config_bindable = ObjectType("RepositoryConfig") +indication_range_bindable = ObjectType("IndicationRange") + + +class IndicationRange(TypedDict): + lowerRange: float + upperRange: float + + +@repository_config_bindable.field("indicationRange") +async def resolve_indication_range(repository: Repository, info) -> dict[str, float]: + owner = await OwnerLoader.loader(info).load(repository.author_id) + + yaml = await sync_to_async(UserYaml.get_final_yaml)( + owner_yaml=owner.yaml, repo_yaml=repository.yaml + ) + range: list[float] = yaml.get("coverage", {"range": [60, 80]}).get( + "range", [60, 80] + ) + return {"lowerRange": range[0], "upperRange": range[1]} + + +@indication_range_bindable.field("upperRange") +def resolve_upper_range(indicationRange: IndicationRange, info) -> float: + upperRange = indicationRange.get("upperRange") + return upperRange + + +@indication_range_bindable.field("lowerRange") +def resolve_lower_range(indicationRange: IndicationRange, info) -> float: + lowerRange = indicationRange.get("lowerRange") + return lowerRange diff --git a/apps/codecov-api/graphql_api/types/segment_comparison/__init__.py b/apps/codecov-api/graphql_api/types/segment_comparison/__init__.py new file mode 100644 index 0000000000..20f87a3b48 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/segment_comparison/__init__.py @@ -0,0 +1,12 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .segment_comparison import segment_comparison_bindable, segments_result_bindable + +segment_comparison = ariadne_load_local_graphql(__file__, "segment_comparison.graphql") + + +__all__ = [ + "segment_comparison", + "segment_comparison_bindable", + "segments_result_bindable", +] diff --git a/apps/codecov-api/graphql_api/types/segment_comparison/segment_comparison.graphql b/apps/codecov-api/graphql_api/types/segment_comparison/segment_comparison.graphql new file mode 100644 index 0000000000..9dac085247 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/segment_comparison/segment_comparison.graphql @@ -0,0 +1,11 @@ +type SegmentComparison { + header: String! + hasUnintendedChanges: Boolean! + lines: [LineComparison!]! +} + +type SegmentComparisons { + results: [SegmentComparison!]! +} + +union SegmentsResult = SegmentComparisons | UnknownPath | ProviderError diff --git a/apps/codecov-api/graphql_api/types/segment_comparison/segment_comparison.py b/apps/codecov-api/graphql_api/types/segment_comparison/segment_comparison.py new file mode 100644 index 0000000000..12356678e2 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/segment_comparison/segment_comparison.py @@ -0,0 +1,55 @@ +from dataclasses import dataclass +from typing import List + +from ariadne import ObjectType, UnionType + +from graphql_api.types.errors.errors import ProviderError, UnknownPath +from services.comparison import LineComparison, Segment + + +@dataclass +class SegmentComparisons: + results: List[Segment] + + +segment_comparison_bindable = ObjectType("SegmentComparison") + + +@segment_comparison_bindable.field("header") +def resolve_header(segment: Segment, info) -> str: + ( + base_starting, + base_extracted, + head_starting, + head_extracted, + ) = segment.header + base = f"{base_starting}" + if base_extracted is not None: + base = f"{base},{base_extracted}" + head = f"{head_starting}" + if head_extracted is not None: + head = f"{head},{head_extracted}" + return f"-{base} +{head}" + + +@segment_comparison_bindable.field("lines") +def resolve_lines(segment: Segment, info) -> List[LineComparison]: + return segment.lines + + +@segment_comparison_bindable.field("hasUnintendedChanges") +def resolve_has_unintended_changes(segment: Segment, info) -> bool: + return segment.has_unintended_changes + + +segments_result_bindable = UnionType("SegmentsResult") + + +@segments_result_bindable.type_resolver +def resolve_segments_result_type(res, *_): + if isinstance(res, UnknownPath): + return "UnknownPath" + elif isinstance(res, ProviderError): + return "ProviderError" + elif isinstance(res, SegmentComparisons): + return "SegmentComparisons" diff --git a/apps/codecov-api/graphql_api/types/self_hosted_license/__init__.py b/apps/codecov-api/graphql_api/types/self_hosted_license/__init__.py new file mode 100644 index 0000000000..777259ea74 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/self_hosted_license/__init__.py @@ -0,0 +1,9 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .self_hosted_license import self_hosted_license_bindable + +self_hosted_license = ariadne_load_local_graphql( + __file__, "self_hosted_license.graphql" +) + +__all__ = ["self_hosted_license", "self_hosted_license_bindable"] diff --git a/apps/codecov-api/graphql_api/types/self_hosted_license/self_hosted_license.graphql b/apps/codecov-api/graphql_api/types/self_hosted_license/self_hosted_license.graphql new file mode 100644 index 0000000000..e7e38af3f2 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/self_hosted_license/self_hosted_license.graphql @@ -0,0 +1,3 @@ +type SelfHostedLicense { + expirationDate: DateTime +} diff --git a/apps/codecov-api/graphql_api/types/self_hosted_license/self_hosted_license.py b/apps/codecov-api/graphql_api/types/self_hosted_license/self_hosted_license.py new file mode 100644 index 0000000000..a737ce54db --- /dev/null +++ b/apps/codecov-api/graphql_api/types/self_hosted_license/self_hosted_license.py @@ -0,0 +1,18 @@ +import datetime +from typing import Optional + +from ariadne import ObjectType +from django.conf import settings +from shared.license import LicenseInformation + +self_hosted_license_bindable = ObjectType("SelfHostedLicense") + + +@self_hosted_license_bindable.field("expirationDate") +def resolve_expiration_date( + license: LicenseInformation, info +) -> Optional[datetime.date]: + if not settings.IS_ENTERPRISE: + return None + + return license.expires diff --git a/apps/codecov-api/graphql_api/types/session/__init__.py b/apps/codecov-api/graphql_api/types/session/__init__.py new file mode 100644 index 0000000000..49f9ff9cac --- /dev/null +++ b/apps/codecov-api/graphql_api/types/session/__init__.py @@ -0,0 +1,7 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .session import session_bindable + +session = ariadne_load_local_graphql(__file__, "session.graphql") + +__all__ = ["session", "session_bindable"] diff --git a/apps/codecov-api/graphql_api/types/session/session.graphql b/apps/codecov-api/graphql_api/types/session/session.graphql new file mode 100644 index 0000000000..30e626f0fa --- /dev/null +++ b/apps/codecov-api/graphql_api/types/session/session.graphql @@ -0,0 +1,9 @@ +type Session { + sessionid: Int! + ip: String + lastseen: DateTime + useragent: String + type: String! + name: String + lastFour: String! +} diff --git a/apps/codecov-api/graphql_api/types/session/session.py b/apps/codecov-api/graphql_api/types/session/session.py new file mode 100644 index 0000000000..859f314c06 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/session/session.py @@ -0,0 +1,8 @@ +from ariadne import ObjectType + +session_bindable = ObjectType("Session") + + +@session_bindable.field("lastFour") +def resolve_last_four(session, _) -> str: + return str(session.token)[-4:] diff --git a/apps/codecov-api/graphql_api/types/test_analytics/__init__.py b/apps/codecov-api/graphql_api/types/test_analytics/__init__.py new file mode 100644 index 0000000000..81a4c7cfba --- /dev/null +++ b/apps/codecov-api/graphql_api/types/test_analytics/__init__.py @@ -0,0 +1,11 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .test_analytics import ( + test_analytics_bindable, +) + +test_analytics = ariadne_load_local_graphql(__file__, "test_analytics.graphql") + +__all__ = [ + "test_analytics_bindable", +] diff --git a/apps/codecov-api/graphql_api/types/test_analytics/test_analytics.graphql b/apps/codecov-api/graphql_api/types/test_analytics/test_analytics.graphql new file mode 100644 index 0000000000..efa71bfde0 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/test_analytics/test_analytics.graphql @@ -0,0 +1,36 @@ +""" +TestAnalytics are fields related to Codecov's Test Analytics product offering +""" +type TestAnalytics { + "Test results are analytics data per test" + testResults( + filters: TestResultsFilters + ordering: TestResultsOrdering + first: Int + after: String + last: Int + before: String + ): TestResultConnection! @cost(complexity: 10, multipliers: ["first", "last"]) + + "Test results aggregates are analytics data totals across all tests" + testResultsAggregates(interval: MeasurementInterval): TestResultsAggregates + + "Flake aggregates are flake totals across all tests" + flakeAggregates(interval: MeasurementInterval): FlakeAggregates + + testSuites(term: String): [String!]! + + "Only flag names relevant to Test Analytics" + flags(term: String): [String!]! +} + +type TestResultConnection { + edges: [TestResultEdge]! + totalCount: Int! + pageInfo: PageInfo! +} + +type TestResultEdge { + cursor: String! + node: TestResult! +} diff --git a/apps/codecov-api/graphql_api/types/test_analytics/test_analytics.py b/apps/codecov-api/graphql_api/types/test_analytics/test_analytics.py new file mode 100644 index 0000000000..28f26fd69e --- /dev/null +++ b/apps/codecov-api/graphql_api/types/test_analytics/test_analytics.py @@ -0,0 +1,416 @@ +import datetime as dt +import logging +from base64 import b64decode, b64encode +from dataclasses import dataclass +from typing import Any, TypedDict + +import polars as pl +from ariadne import ObjectType +from asgiref.sync import sync_to_async +from graphql.type.definition import GraphQLResolveInfo +from shared.django_apps.core.models import Repository + +from codecov.commands.exceptions import ValidationError +from graphql_api.types.enums import ( + OrderingDirection, + TestResultsFilterParameter, + TestResultsOrderingParameter, +) +from graphql_api.types.enums.enum_types import MeasurementInterval +from graphql_api.types.flake_aggregates.flake_aggregates import ( + FlakeAggregates, + generate_flake_aggregates, +) +from graphql_api.types.test_results_aggregates.test_results_aggregates import ( + TestResultsAggregates, + generate_test_results_aggregates, +) +from utils.test_results import get_results + +log = logging.getLogger(__name__) + +INTERVAL_30_DAY = 30 +INTERVAL_7_DAY = 7 +INTERVAL_1_DAY = 1 + + +@dataclass +class TestResultsRow: + # the order here must match the order of the fields in the query + name: str + testsuite: str | None + flags: list[str] + failure_rate: float + flake_rate: float + updated_at: dt.datetime + avg_duration: float + total_fail_count: int + total_flaky_fail_count: int + total_pass_count: int + total_skip_count: int + commits_where_fail: int + last_duration: float + + def to_dict(self) -> dict: + return { + "name": self.name, + "testsuite": self.testsuite, + "flags": self.flags, + "failure_rate": self.failure_rate, + "flake_rate": self.flake_rate, + "updated_at": self.updated_at.isoformat(), + "avg_duration": self.avg_duration, + "total_fail_count": self.total_fail_count, + "total_flaky_fail_count": self.total_flaky_fail_count, + "total_pass_count": self.total_pass_count, + "total_skip_count": self.total_skip_count, + "commits_where_fail": self.commits_where_fail, + "last_duration": self.last_duration, + } + + +@dataclass +class TestResultConnection: + edges: list[dict[str, str | TestResultsRow]] + page_info: dict + total_count: int + + +DELIMITER = "|" + + +@dataclass +class CursorValue: + ordered_value: float | int | dt.datetime | str + name: str + + +def decode_cursor( + value: str | None, ordering: TestResultsOrderingParameter +) -> CursorValue | None: + if value is None: + return None + + split_cursor = b64decode(value.encode("ascii")).decode("utf-8").split(DELIMITER) + ordered_value: str = split_cursor[0] + name: str = split_cursor[1] + match ordering: + case ( + TestResultsOrderingParameter.AVG_DURATION + | TestResultsOrderingParameter.FLAKE_RATE + | TestResultsOrderingParameter.FAILURE_RATE + | TestResultsOrderingParameter.LAST_DURATION + ): + return CursorValue(ordered_value=float(ordered_value), name=name) + case TestResultsOrderingParameter.COMMITS_WHERE_FAIL: + return CursorValue(ordered_value=int(ordered_value), name=name) + case TestResultsOrderingParameter.UPDATED_AT: + return CursorValue( + ordered_value=dt.datetime.fromisoformat(ordered_value), name=name + ) + + raise ValueError(f"Invalid ordering field: {ordering}") + + +def encode_cursor(row: TestResultsRow, ordering: TestResultsOrderingParameter) -> str: + return b64encode( + DELIMITER.join([str(getattr(row, ordering.value)), str(row.name)]).encode( + "utf-8" + ) + ).decode("ascii") + + +def validate( + interval: int, + ordering: TestResultsOrderingParameter, + ordering_direction: OrderingDirection, + after: str | None, + before: str | None, + first: int | None, + last: int | None, +) -> None: + if interval not in {INTERVAL_1_DAY, INTERVAL_7_DAY, INTERVAL_30_DAY}: + raise ValidationError(f"Invalid interval: {interval}") + + if not isinstance(ordering_direction, OrderingDirection): + raise ValidationError(f"Invalid ordering direction: {ordering_direction}") + + if not isinstance(ordering, TestResultsOrderingParameter): + raise ValidationError(f"Invalid ordering field: {ordering}") + + if first is not None and last is not None: + raise ValidationError("First and last can not be used at the same time") + + if after is not None and before is not None: + raise ValidationError("After and before can not be used at the same time") + + +def ordering_expression( + ordering: TestResultsOrderingParameter, cursor_value: CursorValue, is_forward: bool +) -> pl.Expr: + if is_forward: + ordering_expression = (pl.col(ordering.value) > cursor_value.ordered_value) | ( + (pl.col(ordering.value) == cursor_value.ordered_value) + & (pl.col("name") > cursor_value.name) + ) + else: + ordering_expression = (pl.col(ordering.value) < cursor_value.ordered_value) | ( + (pl.col(ordering.value) == cursor_value.ordered_value) + & (pl.col("name") > cursor_value.name) + ) + return ordering_expression + + +def generate_test_results( + ordering: TestResultsOrderingParameter, + ordering_direction: OrderingDirection, + repoid: int, + measurement_interval: MeasurementInterval, + *, + first: int | None = None, + after: str | None = None, + last: int | None = None, + before: str | None = None, + branch: str | None = None, + parameter: TestResultsFilterParameter | None = None, + testsuites: list[str] | None = None, + flags: list[str] | None = None, + term: str | None = None, +) -> TestResultConnection: + """ + Function that retrieves aggregated information about all tests in a given repository, for a given time range, optionally filtered by branch name. + The fields it calculates are: the test failure rate, commits where this test failed, last duration and average duration of the test. + + :param repoid: repoid of the repository we want to calculate aggregates for + :param branch: optional name of the branch we want to filter on, if this is provided the aggregates calculated will only take into account + test instances generated on that branch. By default branches will not be filtered and test instances on all branches wil be taken into + account. + :param interval: timedelta for filtering test instances used to calculate the aggregates by time, the test instances used will be + those with a created at larger than now - interval. + :param testsuites: optional list of testsuite names to filter by, this is done via a union + :param flags: optional list of flag names to filter by, this is done via a union so if a user specifies multiple flags, we get all tests with any + of the flags, not tests that have all of the flags + :returns: queryset object containing list of dictionaries of results + + """ + repo = Repository.objects.get(repoid=repoid) + if branch is None: + branch = repo.branch + interval = measurement_interval.value + validate(interval, ordering, ordering_direction, after, before, first, last) + + table = get_results(repoid, branch, interval) + + if table is None: + return TestResultConnection( + edges=[], + total_count=0, + page_info={ + "has_next_page": False, + "has_previous_page": False, + "start_cursor": None, + "end_cursor": None, + }, + ) + + if term: + table = table.filter(pl.col("name").str.contains(term)) + + if testsuites: + table = table.filter( + pl.col("testsuite").is_not_null() + & pl.col("testsuite").list.eval(pl.element().is_in(testsuites)).list.any() + ) + + if flags: + table = table.filter( + pl.col("flags").is_not_null() + & pl.col("flags").list.eval(pl.element().is_in(flags)).list.any() + ) + + match parameter: + case TestResultsFilterParameter.FAILED_TESTS: + table = table.filter(pl.col("total_fail_count") > 0) + case TestResultsFilterParameter.FLAKY_TESTS: + table = table.filter(pl.col("total_flaky_fail_count") > 0) + case TestResultsFilterParameter.SKIPPED_TESTS: + table = table.filter( + (pl.col("total_skip_count") > 0) & (pl.col("total_pass_count") == 0) + ) + case TestResultsFilterParameter.SLOWEST_TESTS: + table = table.filter( + pl.col("avg_duration") >= pl.col("avg_duration").quantile(0.95) + ).top_k( + min(100, max(table.height // 20, 1)), by=pl.col("avg_duration") + ) # the top k operation here is to make sure we don't show too many slowest tests in the case of a low sample size + + total_count = table.height + + if after or before: + comparison_direction = (ordering_direction == OrderingDirection.ASC) == ( + bool(after) + ) + cursor_value = ( + decode_cursor(after, ordering) if after else decode_cursor(before, ordering) + ) + if cursor_value: + table = table.filter( + ordering_expression(ordering, cursor_value, comparison_direction) + ) + + table = table.sort( + [ordering.value, "name"], + descending=[ordering_direction == OrderingDirection.DESC, False], + ) + + if first: + page_elements = table.slice(0, first) + elif last: + page_elements = table.slice(-last, last) + else: + page_elements = table + + rows = [TestResultsRow(**row) for row in page_elements.rows(named=True)] + + page: list[dict[str, str | TestResultsRow]] = [ + {"cursor": encode_cursor(row, ordering), "node": row} for row in rows + ] + + return TestResultConnection( + edges=page, + total_count=total_count, + page_info={ + "has_next_page": True if first and len(table) > first else False, + "has_previous_page": True if last and len(table) > last else False, + "start_cursor": page[0]["cursor"] if page else None, + "end_cursor": page[-1]["cursor"] if page else None, + }, + ) + + +def get_test_suites( + repoid: int, term: str | None = None, interval: int = 30 +) -> list[str]: + repo = Repository.objects.get(repoid=repoid) + + table = get_results(repoid, repo.branch, interval) + if table is None: + return [] + + testsuites = table.select(pl.col("testsuite")).unique() + + if term: + testsuites = testsuites.filter(pl.col("testsuite").str.starts_with(term)) + + return testsuites.to_series().drop_nulls().to_list() or [] + + +def get_flags(repoid: int, term: str | None = None, interval: int = 30) -> list[str]: + repo = Repository.objects.get(repoid=repoid) + + table = get_results(repoid, repo.branch, interval) + if table is None: + return [] + + flags = table.select(pl.col("flags").explode()).unique() + + if term: + flags = flags.filter(pl.col("flags").str.starts_with(term)) + + return flags.to_series().drop_nulls().to_list() or [] + + +class TestResultsOrdering(TypedDict): + parameter: TestResultsOrderingParameter + direction: OrderingDirection + + +class TestResultsFilters(TypedDict): + parameter: TestResultsFilterParameter | None + interval: MeasurementInterval + branch: str | None + test_suites: list[str] | None + flags: list[str] | None + term: str | None + + +# Bindings for GraphQL types +test_analytics_bindable: ObjectType = ObjectType("TestAnalytics") + + +@test_analytics_bindable.field("testResults") +async def resolve_test_results( + repository: Repository, + info: GraphQLResolveInfo, + ordering: TestResultsOrdering | None = None, + filters: TestResultsFilters | None = None, + first: int | None = None, + after: str | None = None, + last: int | None = None, + before: str | None = None, +) -> TestResultConnection: + queryset = await sync_to_async(generate_test_results)( + ordering=ordering.get("parameter", TestResultsOrderingParameter.AVG_DURATION) + if ordering + else TestResultsOrderingParameter.AVG_DURATION, + ordering_direction=ordering.get("direction", OrderingDirection.DESC) + if ordering + else OrderingDirection.DESC, + repoid=repository.repoid, + measurement_interval=filters.get( + "interval", MeasurementInterval.INTERVAL_30_DAY + ) + if filters + else MeasurementInterval.INTERVAL_30_DAY, + first=first, + after=after, + last=last, + before=before, + branch=filters.get("branch") if filters else None, + parameter=filters.get("parameter") if filters else None, + testsuites=filters.get("test_suites") if filters else None, + flags=filters.get("flags") if filters else None, + term=filters.get("term") if filters else None, + ) + + return queryset + + +@test_analytics_bindable.field("testResultsAggregates") +async def resolve_test_results_aggregates( + repository: Repository, + info: GraphQLResolveInfo, + interval: MeasurementInterval | None = None, + **_: Any, +) -> TestResultsAggregates | None: + return await sync_to_async(generate_test_results_aggregates)( + repoid=repository.repoid, + interval=interval if interval else MeasurementInterval.INTERVAL_30_DAY, + ) + + +@test_analytics_bindable.field("flakeAggregates") +async def resolve_flake_aggregates( + repository: Repository, + info: GraphQLResolveInfo, + interval: MeasurementInterval | None = None, + **_: Any, +) -> FlakeAggregates | None: + return await sync_to_async(generate_flake_aggregates)( + repoid=repository.repoid, + interval=interval if interval else MeasurementInterval.INTERVAL_30_DAY, + ) + + +@test_analytics_bindable.field("testSuites") +async def resolve_test_suites( + repository: Repository, info: GraphQLResolveInfo, term: str | None = None, **_: Any +) -> list[str]: + return await sync_to_async(get_test_suites)(repository.repoid, term) + + +@test_analytics_bindable.field("flags") +async def resolve_flags( + repository: Repository, info: GraphQLResolveInfo, term: str | None = None, **_: Any +) -> list[str]: + return await sync_to_async(get_flags)(repository.repoid, term) diff --git a/apps/codecov-api/graphql_api/types/test_results/__init__.py b/apps/codecov-api/graphql_api/types/test_results/__init__.py new file mode 100644 index 0000000000..4f9f9394f4 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/test_results/__init__.py @@ -0,0 +1,7 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .test_results import test_result_bindable + +test_results = ariadne_load_local_graphql(__file__, "test_results.graphql") + +__all__ = ["test_result_bindable"] diff --git a/apps/codecov-api/graphql_api/types/test_results/test_results.graphql b/apps/codecov-api/graphql_api/types/test_results/test_results.graphql new file mode 100644 index 0000000000..7d7ac178d9 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/test_results/test_results.graphql @@ -0,0 +1,13 @@ +type TestResult { + updatedAt: DateTime! + name: String! + commitsFailed: Int! + failureRate: Float! + flakeRate: Float! + avgDuration: Float! + lastDuration: Float! + totalFailCount: Int! + totalFlakyFailCount: Int! + totalSkipCount: Int! + totalPassCount: Int! +} diff --git a/apps/codecov-api/graphql_api/types/test_results/test_results.py b/apps/codecov-api/graphql_api/types/test_results/test_results.py new file mode 100644 index 0000000000..28d839f10a --- /dev/null +++ b/apps/codecov-api/graphql_api/types/test_results/test_results.py @@ -0,0 +1,63 @@ +from datetime import datetime + +from ariadne import ObjectType +from graphql import GraphQLResolveInfo + +from graphql_api.types.test_analytics.test_analytics import TestResultsRow + +test_result_bindable = ObjectType("TestResult") + + +@test_result_bindable.field("name") +def resolve_name(test: TestResultsRow, _: GraphQLResolveInfo) -> str: + return test.name.replace("\x1f", " ") + + +@test_result_bindable.field("updatedAt") +def resolve_updated_at(test: TestResultsRow, _: GraphQLResolveInfo) -> datetime: + return test.updated_at + + +@test_result_bindable.field("commitsFailed") +def resolve_commits_failed(test: TestResultsRow, _: GraphQLResolveInfo) -> int: + return test.commits_where_fail + + +@test_result_bindable.field("failureRate") +def resolve_failure_rate(test: TestResultsRow, _: GraphQLResolveInfo) -> float: + return test.failure_rate + + +@test_result_bindable.field("flakeRate") +def resolve_flake_rate(test: TestResultsRow, _: GraphQLResolveInfo) -> float: + return test.flake_rate + + +@test_result_bindable.field("avgDuration") +def resolve_avg_duration(test: TestResultsRow, _: GraphQLResolveInfo) -> float: + return test.avg_duration + + +@test_result_bindable.field("lastDuration") +def resolve_last_duration(test: TestResultsRow, _: GraphQLResolveInfo) -> float: + return test.last_duration + + +@test_result_bindable.field("totalFailCount") +def resolve_total_fail_count(test: TestResultsRow, _: GraphQLResolveInfo) -> int: + return test.total_fail_count + + +@test_result_bindable.field("totalFlakyFailCount") +def resolve_total_flaky_fail_count(test: TestResultsRow, _: GraphQLResolveInfo) -> int: + return test.total_flaky_fail_count + + +@test_result_bindable.field("totalSkipCount") +def resolve_total_skip_count(test: TestResultsRow, _: GraphQLResolveInfo) -> int: + return test.total_skip_count + + +@test_result_bindable.field("totalPassCount") +def resolve_total_pass_count(test: TestResultsRow, _: GraphQLResolveInfo) -> int: + return test.total_pass_count diff --git a/apps/codecov-api/graphql_api/types/test_results_aggregates/__init__.py b/apps/codecov-api/graphql_api/types/test_results_aggregates/__init__.py new file mode 100644 index 0000000000..33eafe87dd --- /dev/null +++ b/apps/codecov-api/graphql_api/types/test_results_aggregates/__init__.py @@ -0,0 +1,9 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .test_results_aggregates import test_results_aggregates_bindable + +test_results_aggregates = ariadne_load_local_graphql( + __file__, "test_results_aggregates.graphql" +) + +__all__ = ["test_results_aggregates_bindable"] diff --git a/apps/codecov-api/graphql_api/types/test_results_aggregates/test_results_aggregates.graphql b/apps/codecov-api/graphql_api/types/test_results_aggregates/test_results_aggregates.graphql new file mode 100644 index 0000000000..e225bc1ff3 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/test_results_aggregates/test_results_aggregates.graphql @@ -0,0 +1,12 @@ +type TestResultsAggregates { + totalDuration: Float! + totalDurationPercentChange: Float + slowestTestsDuration: Float! + slowestTestsDurationPercentChange: Float + totalFails: Int! + totalFailsPercentChange: Float + totalSkips: Int! + totalSkipsPercentChange: Float + totalSlowTests: Int! + totalSlowTestsPercentChange: Float +} diff --git a/apps/codecov-api/graphql_api/types/test_results_aggregates/test_results_aggregates.py b/apps/codecov-api/graphql_api/types/test_results_aggregates/test_results_aggregates.py new file mode 100644 index 0000000000..da99787e3c --- /dev/null +++ b/apps/codecov-api/graphql_api/types/test_results_aggregates/test_results_aggregates.py @@ -0,0 +1,164 @@ +from dataclasses import dataclass + +import polars as pl +from ariadne import ObjectType +from graphql import GraphQLResolveInfo +from shared.django_apps.core.models import Repository + +from graphql_api.types.enums.enum_types import MeasurementInterval +from utils.test_results import get_results + + +@dataclass +class TestResultsAggregates: + total_duration: float + slowest_tests_duration: float + total_slow_tests: int + fails: int + skips: int + total_duration_percent_change: float | None = None + slowest_tests_duration_percent_change: float | None = None + total_slow_tests_percent_change: float | None = None + fails_percent_change: float | None = None + skips_percent_change: float | None = None + + +def calculate_aggregates(table: pl.DataFrame) -> pl.DataFrame: + return table.select( + ( + pl.col("avg_duration") + * (pl.col("total_pass_count") + pl.col("total_fail_count")) + ) + .sum() + .alias("total_duration"), + ( + pl.when(pl.col("avg_duration") >= pl.col("avg_duration").quantile(0.95)) + .then( + pl.col("avg_duration") + * (pl.col("total_pass_count") + pl.col("total_fail_count")) + ) + .otherwise(0) + .top_k(min(100, max(table.height // 20, 1))) + .sum() + .alias("slowest_tests_duration") + ), + (pl.col("total_skip_count").sum()).alias("skips"), + (pl.col("total_fail_count").sum()).alias("fails"), + ( + (pl.col("avg_duration") >= pl.col("avg_duration").quantile(0.95)) + .top_k(min(100, max(table.height // 20, 1))) + .sum() + ).alias("total_slow_tests"), + ) + + +def test_results_aggregates_from_table( + table: pl.DataFrame, +) -> TestResultsAggregates: + aggregates = calculate_aggregates(table).row(0, named=True) + return TestResultsAggregates(**aggregates) + + +def test_results_aggregates_with_percentage( + curr_results: pl.DataFrame, + past_results: pl.DataFrame, +) -> TestResultsAggregates: + curr_aggregates = calculate_aggregates(curr_results) + past_aggregates = calculate_aggregates(past_results) + + merged_results: pl.DataFrame = pl.concat([past_aggregates, curr_aggregates]) + + # with_columns upserts the new columns, so if the name already exists it get overwritten + # otherwise it's just added + merged_results = merged_results.with_columns( + pl.all() + .pct_change() + .replace([float("inf"), float("-inf")], None) + .fill_nan(0) + .name.suffix("_percent_change") + ) + aggregates = merged_results.row(1, named=True) + + return TestResultsAggregates(**aggregates) + + +def generate_test_results_aggregates( + repoid: int, interval: MeasurementInterval +) -> TestResultsAggregates | None: + repo = Repository.objects.get(repoid=repoid) + + curr_results = get_results(repo.repoid, repo.branch, interval.value) + if curr_results is None: + return None + past_results = get_results( + repo.repoid, repo.branch, interval.value * 2, interval.value + ) + if past_results is None: + return test_results_aggregates_from_table(curr_results) + else: + return test_results_aggregates_with_percentage(curr_results, past_results) + + +test_results_aggregates_bindable = ObjectType("TestResultsAggregates") + + +@test_results_aggregates_bindable.field("totalDuration") +def resolve_total_duration(obj: TestResultsAggregates, _: GraphQLResolveInfo) -> float: + return obj.total_duration + + +@test_results_aggregates_bindable.field("totalDurationPercentChange") +def resolve_total_duration_percent_change( + obj: TestResultsAggregates, _: GraphQLResolveInfo +) -> float | None: + return obj.total_duration_percent_change + + +@test_results_aggregates_bindable.field("slowestTestsDuration") +def resolve_slowest_tests_duration( + obj: TestResultsAggregates, _: GraphQLResolveInfo +) -> float: + return obj.slowest_tests_duration + + +@test_results_aggregates_bindable.field("slowestTestsDurationPercentChange") +def resolve_slowest_tests_duration_percent_change( + obj: TestResultsAggregates, _: GraphQLResolveInfo +) -> float | None: + return obj.slowest_tests_duration_percent_change + + +@test_results_aggregates_bindable.field("totalSlowTests") +def resolve_total_slow_tests(obj: TestResultsAggregates, _: GraphQLResolveInfo) -> int: + return obj.total_slow_tests + + +@test_results_aggregates_bindable.field("totalSlowTestsPercentChange") +def resolve_total_slow_tests_percent_change( + obj: TestResultsAggregates, _: GraphQLResolveInfo +) -> float | None: + return obj.total_slow_tests_percent_change + + +@test_results_aggregates_bindable.field("totalFails") +def resolve_total_fails(obj: TestResultsAggregates, _: GraphQLResolveInfo) -> int: + return obj.fails + + +@test_results_aggregates_bindable.field("totalFailsPercentChange") +def resolve_total_fails_percent_change( + obj: TestResultsAggregates, _: GraphQLResolveInfo +) -> float | None: + return obj.fails_percent_change + + +@test_results_aggregates_bindable.field("totalSkips") +def resolve_total_skips(obj: TestResultsAggregates, _: GraphQLResolveInfo) -> int: + return obj.skips + + +@test_results_aggregates_bindable.field("totalSkipsPercentChange") +def resolve_total_skips_percent_change( + obj: TestResultsAggregates, _: GraphQLResolveInfo +) -> float | None: + return obj.skips_percent_change diff --git a/apps/codecov-api/graphql_api/types/upload/__init__.py b/apps/codecov-api/graphql_api/types/upload/__init__.py new file mode 100644 index 0000000000..59f00b7129 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/upload/__init__.py @@ -0,0 +1,7 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .upload import upload_bindable, upload_error_bindable + +upload = ariadne_load_local_graphql(__file__, "upload.graphql") + +__all__ = ["upload", "upload_bindable", "upload_error_bindable"] diff --git a/apps/codecov-api/graphql_api/types/upload/upload.graphql b/apps/codecov-api/graphql_api/types/upload/upload.graphql new file mode 100644 index 0000000000..910d84281e --- /dev/null +++ b/apps/codecov-api/graphql_api/types/upload/upload.graphql @@ -0,0 +1,30 @@ +type Upload { + state: UploadState! + provider: String + createdAt: DateTime! + updatedAt: DateTime! + flags: [String] + downloadUrl: String! + ciUrl: String + uploadType: UploadType! + jobCode: String + buildCode: String + errors: UploadErrorsConnection + name: String + id: Int +} + +type UploadErrorsConnection { + edges: [UploadErrorsEdge]! + totalCount: Int! + pageInfo: PageInfo! +} + +type UploadErrorsEdge { + cursor: String! + node: UploadError! +} + +type UploadError { + errorCode: UploadErrorEnum +} diff --git a/apps/codecov-api/graphql_api/types/upload/upload.py b/apps/codecov-api/graphql_api/types/upload/upload.py new file mode 100644 index 0000000000..393f228765 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/upload/upload.py @@ -0,0 +1,84 @@ +from typing import Optional + +from ariadne import ObjectType +from asgiref.sync import sync_to_async +from django.urls import reverse +from graphql import GraphQLResolveInfo +from shared.django_apps.utils.services import get_short_service_name + +from graphql_api.helpers.connection import queryset_to_connection_sync +from graphql_api.types.enums import ( + UploadErrorEnum, + UploadState, + UploadType, +) +from reports.models import ReportSession + +upload_bindable = ObjectType("Upload") +upload_bindable.set_alias("flags", "flag_names") + +upload_error_bindable = ObjectType("UploadError") + +""" + Note Uploads are called ReportSession in the model, so I'm keeping the argument + in line with the code vs product name. +""" + + +@upload_bindable.field("state") +def resolve_state(upload: ReportSession, info: GraphQLResolveInfo) -> UploadState: + if not upload.state: + return UploadState.ERROR + return UploadState(upload.state) + + +@upload_bindable.field("id") +def resolve_id(upload: ReportSession, info: GraphQLResolveInfo) -> Optional[int]: + return upload.order_number + + +@upload_bindable.field("uploadType") +def resolve_upload_type(upload: ReportSession, info: GraphQLResolveInfo) -> UploadType: + return UploadType(upload.upload_type) + + +@upload_bindable.field("errors") +@sync_to_async +def resolve_errors(report_session: ReportSession, info: GraphQLResolveInfo, **kwargs): + return queryset_to_connection_sync(list(report_session.errors.all())) + + +@upload_error_bindable.field("errorCode") +def resolve_error_code(error, info: GraphQLResolveInfo) -> UploadErrorEnum: + return UploadErrorEnum(error.error_code) + + +@upload_bindable.field("ciUrl") +@sync_to_async +def resolve_ci_url(upload: ReportSession, info: GraphQLResolveInfo): + return upload.ci_url + + +@upload_bindable.field("downloadUrl") +@sync_to_async +def resolve_download_url(upload: ReportSession, info) -> str: + request = info.context["request"] + repository = upload.report.commit.repository + download_url = ( + reverse( + "upload-download", + kwargs={ + "service": get_short_service_name(repository.author.service), + "owner_username": repository.author.username, + "repo_name": repository.name, + }, + ) + + f"?path={upload.storage_path}" + ) + download_absolute_uri = request.build_absolute_uri(download_url) + return download_absolute_uri.replace("http", "https", 1) + + +@upload_bindable.field("name") +def resolve_name(upload, info): + return upload.name diff --git a/apps/codecov-api/graphql_api/types/user/__init__.py b/apps/codecov-api/graphql_api/types/user/__init__.py new file mode 100644 index 0000000000..94580c9b04 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/user/__init__.py @@ -0,0 +1,3 @@ +from .user import user, user_bindable + +__all__ = ["user", "user_bindable"] diff --git a/apps/codecov-api/graphql_api/types/user/user.graphql b/apps/codecov-api/graphql_api/types/user/user.graphql new file mode 100644 index 0000000000..acfbab5157 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/user/user.graphql @@ -0,0 +1,10 @@ +type User { + username: String! + name: String + avatarUrl: String! + student: Boolean! + studentCreatedAt: DateTime + studentUpdatedAt: DateTime + # this will no longer be updated from the UI with Appless + customerIntent: String +} diff --git a/apps/codecov-api/graphql_api/types/user/user.py b/apps/codecov-api/graphql_api/types/user/user.py new file mode 100644 index 0000000000..aee121b032 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/user/user.py @@ -0,0 +1,55 @@ +from datetime import datetime +from typing import Optional + +from ariadne import ObjectType +from graphql import GraphQLResolveInfo + +from codecov_auth.models import Owner +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +user = ariadne_load_local_graphql(__file__, "user.graphql") + +user_bindable = ObjectType("User") + + +@user_bindable.field("username") +def resolve_username(user: Owner, info: GraphQLResolveInfo) -> str: + return user.username + + +@user_bindable.field("name") +def resolve_name(user: Owner, info: GraphQLResolveInfo) -> Optional[str]: + return user.name + + +@user_bindable.field("avatarUrl") +def resolve_avatar_url(user: Owner, info: GraphQLResolveInfo) -> str: + return user.avatar_url + + +@user_bindable.field("student") +def resolve_student(user: Owner, info: GraphQLResolveInfo) -> bool: + return user.student + + +@user_bindable.field("studentCreatedAt") +def resolve_student_created_at( + user: Owner, info: GraphQLResolveInfo +) -> Optional[datetime]: + return user.student_created_at + + +@user_bindable.field("studentUpdatedAt") +def resolve_student_updated_at( + user: Owner, info: GraphQLResolveInfo +) -> Optional[datetime]: + return user.student_updated_at + + +# this will no longer be updated from the UI +@user_bindable.field("customerIntent") +def resolve_customer_intent(user: Owner, info: GraphQLResolveInfo) -> Optional[str]: + owner = user + if not owner.user: + return None + return owner.user.customer_intent diff --git a/apps/codecov-api/graphql_api/types/user_token/__init__.py b/apps/codecov-api/graphql_api/types/user_token/__init__.py new file mode 100644 index 0000000000..445e55d62f --- /dev/null +++ b/apps/codecov-api/graphql_api/types/user_token/__init__.py @@ -0,0 +1,8 @@ +from graphql_api.helpers.ariadne import ariadne_load_local_graphql + +from .user_token import user_token_bindable + +user_token = ariadne_load_local_graphql(__file__, "user_token.graphql") + + +__all__ = ["user_token", "user_token_bindable"] diff --git a/apps/codecov-api/graphql_api/types/user_token/user_token.graphql b/apps/codecov-api/graphql_api/types/user_token/user_token.graphql new file mode 100644 index 0000000000..7e8ff1942f --- /dev/null +++ b/apps/codecov-api/graphql_api/types/user_token/user_token.graphql @@ -0,0 +1,7 @@ +type UserToken { + id: String! + name: String! + type: String! + lastFour: String! + expiration: DateTime +} \ No newline at end of file diff --git a/apps/codecov-api/graphql_api/types/user_token/user_token.py b/apps/codecov-api/graphql_api/types/user_token/user_token.py new file mode 100644 index 0000000000..c7dd034672 --- /dev/null +++ b/apps/codecov-api/graphql_api/types/user_token/user_token.py @@ -0,0 +1,25 @@ +from ariadne import ObjectType + +from codecov_auth.models import UserToken + +user_token_bindable = ObjectType("UserToken") + + +@user_token_bindable.field("id") +def resolve_id(user_token: UserToken, info): + return user_token.external_id + + +@user_token_bindable.field("type") +def resolve_type(user_token: UserToken, info): + return user_token.token_type + + +@user_token_bindable.field("lastFour") +def resolve_last_four(user_token: UserToken, info): + return str(user_token.token)[-4:] + + +@user_token_bindable.field("expiration") +def resolve_expiration(user_token: UserToken, info): + return user_token.valid_until diff --git a/apps/codecov-api/graphql_api/urls.py b/apps/codecov-api/graphql_api/urls.py new file mode 100644 index 0000000000..a71cce0b4f --- /dev/null +++ b/apps/codecov-api/graphql_api/urls.py @@ -0,0 +1,25 @@ +from django.urls import re_path + +from .views import ariadne_view + +ALLOWED_SERVICES = [ + "gh", + "github", + "gl", + "gitlab", + "bb", + "bitbucket", + "ghe", + "github_enterprise", + "gle", + "gitlab_enterprise", + "bbs", + "bitbucket_server", + "", +] + +service_regex = "|".join(ALLOWED_SERVICES) + +urlpatterns = [ + re_path(r"^(?P({}))$".format(service_regex), ariadne_view, name="graphql"), +] diff --git a/apps/codecov-api/graphql_api/validation.py b/apps/codecov-api/graphql_api/validation.py new file mode 100644 index 0000000000..b9886b0cd2 --- /dev/null +++ b/apps/codecov-api/graphql_api/validation.py @@ -0,0 +1,114 @@ +from typing import Any, Dict, Type + +from graphql import GraphQLError, ValidationRule +from graphql.language.ast import ( + DocumentNode, + FieldNode, + OperationDefinitionNode, + VariableDefinitionNode, +) +from graphql.validation import ValidationContext + + +class MissingVariablesError(Exception): + """ + Custom error class to represent errors where required variables defined in the query does + not have a matching definition in the variables part of the request. Normally when this + scenario occurs it would raise a GraphQLError type but that would cause a uncaught + exception for some reason. The aim of this is to surface the error in the response clearly + and to prevent internal server errors when it occurs. + """ + + pass + + +def create_required_variables_rule(variables: Dict) -> Type[ValidationRule]: + class RequiredVariablesValidationRule(ValidationRule): + def __init__(self, context: ValidationContext) -> None: + super().__init__(context) + self.variables = variables + + def enter_operation_definition( + self, node: OperationDefinitionNode, *_args: Any + ) -> None: + # Get variable definitions + variable_definitions = node.variable_definitions or [] + + # Extract variables marked as Non Null + required_variables = [ + var_def.variable.name.value + for var_def in variable_definitions + if isinstance(var_def, VariableDefinitionNode) + and var_def.type.kind == "non_null_type" + ] + + # Check if these required variables are provided + missing_variables = [ + var for var in required_variables if var not in self.variables + ] + if missing_variables: + raise MissingVariablesError( + f"Missing required variables: {', '.join(missing_variables)}", + ) + + return RequiredVariablesValidationRule + + +def create_max_depth_rule(max_depth: int) -> Type[ValidationRule]: + class MaxDepthRule(ValidationRule): + def __init__(self, context: ValidationContext) -> None: + super().__init__(context) + self.operation_depth: int = 1 + self.max_depth_reached: bool = False + self.max_depth: int = max_depth + + def enter_operation_definition( + self, node: OperationDefinitionNode, *_args: Any + ) -> None: + self.operation_depth = 1 + self.max_depth_reached = False + + def enter_field(self, node: FieldNode, *_args: Any) -> None: + self.operation_depth += 1 + + if self.operation_depth > self.max_depth and not self.max_depth_reached: + self.max_depth_reached = True + self.report_error( + GraphQLError( + "Query depth exceeds the maximum allowed depth", + node, + ) + ) + + def leave_field(self, node: FieldNode, *_args: Any) -> None: + self.operation_depth -= 1 + + return MaxDepthRule + + +def create_max_aliases_rule(max_aliases: int) -> Type[ValidationRule]: + class MaxAliasesRule(ValidationRule): + def __init__(self, context: ValidationContext) -> None: + super().__init__(context) + self.alias_count: int = 0 + self.has_reported_error: bool = False + self.max_aliases: int = max_aliases + + def enter_document(self, node: DocumentNode, *_args: Any) -> None: + self.alias_count = 0 + self.has_reported_error = False + + def enter_field(self, node: FieldNode, *_args: Any) -> None: + if node.alias: + self.alias_count += 1 + + if self.alias_count > self.max_aliases and not self.has_reported_error: + self.has_reported_error = True + self.report_error( + GraphQLError( + "Query uses too many aliases", + node, + ) + ) + + return MaxAliasesRule diff --git a/apps/codecov-api/graphql_api/views.py b/apps/codecov-api/graphql_api/views.py new file mode 100644 index 0000000000..3be0a9e01c --- /dev/null +++ b/apps/codecov-api/graphql_api/views.py @@ -0,0 +1,431 @@ +import json +import logging +import os +import socket +import time +from asyncio import iscoroutine +from typing import Any, Collection, Optional + +import regex +from ariadne import format_error +from ariadne.types import Extension +from ariadne.validation import cost_validator +from ariadne_django.views import GraphQLAsyncView +from asgiref.sync import sync_to_async +from django.conf import settings +from django.core.handlers.wsgi import WSGIRequest +from django.http import ( + HttpResponse, + HttpResponseBadRequest, + HttpResponseNotAllowed, + JsonResponse, +) +from graphql import DocumentNode +from sentry_sdk import capture_exception +from shared.helpers.redis import get_redis_connection +from shared.metrics import Counter, Histogram, inc_counter + +from codecov.commands.exceptions import BaseException +from codecov.commands.executor import get_executor_from_request +from services import ServiceException + +from .schema import schema +from .validation import ( + MissingVariablesError, + create_max_aliases_rule, + create_max_depth_rule, + create_required_variables_rule, +) + +log = logging.getLogger(__name__) + +GQL_HIT_COUNTER = Counter( + "api_gql_counts_hits", + "Number of times API GQL endpoint request starts", + ["operation_type", "operation_name"], +) + +GQL_ERROR_COUNTER = Counter( + "api_gql_counts_errors", + "Number of times API GQL endpoint failed with an exception", + ["operation_type", "operation_name"], +) + +GQL_REQUEST_LATENCIES = Histogram( + "api_gql_timers_full_runtime_seconds", + "Total runtime in seconds of this query", + ["operation_type", "operation_name"], + buckets=[0.05, 0.1, 0.25, 0.5, 0.75, 1, 2, 5, 10, 30], +) + +GQL_REQUEST_MADE_COUNTER = Counter( + "api_gql_requests_made", + "Total API GQL requests made", + ["path"], +) + +GQL_ERROR_TYPE_COUNTER = Counter( + "api_gql_errors", + "Number of times API GQL endpoint failed with an exception by type", + ["error_type", "path"], +) + +# covers named and 3 unnamed operations (see graphql_api/types/query/query.py) +GQL_TYPE_AND_NAME_PATTERN = r"^(query|mutation|subscription)(?:\(\$input:|) (\w+)(?:\(| \(|{| {|!)|^(?:{) (me|owner|config)(?:\(| |{)" + + +class QueryMetricsExtension(Extension): + """ + We have named and unnamed operations, we want to collect metrics on both. + named operations have an operation_type and operation_name, + ex: "query MySession { operation body }" + would be tracked as operation_type = query, operation_name = MySession + we have `me`, `owner`, and `config` as unnamed operations, + ex: "{ owner(username: "%s") { continued operation body } }" + this operation would be tracked as operation_type = unknown_type, operation_name = owner + + """ + + def __init__(self) -> None: + self.start_timestamp: float = 0 + self.end_timestamp: float = 0 + self.operation_type: str | None = None + self.operation_name: str | None = None + + def set_type_and_name(self, query: str) -> None: + operation_type = "unknown_type" # default value + operation_name = "unknown_name" # default value + try: + match_obj = regex.match(GQL_TYPE_AND_NAME_PATTERN, query, timeout=2) + except TimeoutError: + # does not block the rest of the gql request, logs and falls back to default values + query_slice = query[:30] if len(query) > 30 else query + log.error("Regex Timeout Error", extra=dict(query_slice=query_slice)) + match_obj = None + + if match_obj: + if match_obj.group(1) is not None: + operation_type = match_obj.group(1) + + if match_obj.group(2) is not None: + operation_name = match_obj.group(2) + elif match_obj.group(3) is not None: + operation_name = match_obj.group(3) + + self.operation_type = operation_type + self.operation_name = operation_name + if operation_type == "unknown_type" and operation_name == "unknown_name": + query_slice = query[:30] if len(query) > 30 else query + log.info( + "Could not match gql query format for logging", + extra=dict(query_slice=query_slice), + ) + + def request_started(self, context: dict[str, Any]) -> None: + """ + Extension hook executed at request's start. + """ + self.set_type_and_name(query=context["clean_query"]) + self.start_timestamp = time.perf_counter() + inc_counter( + GQL_HIT_COUNTER, + labels=dict( + operation_type=self.operation_type, + operation_name=self.operation_name, + ), + ) + + def request_finished(self, context: dict[str, Any]) -> None: + """ + Extension hook executed at request's end. + """ + self.end_timestamp = time.perf_counter() + latency = self.end_timestamp - self.start_timestamp + GQL_REQUEST_LATENCIES.labels( + operation_type=self.operation_type, operation_name=self.operation_name + ).observe(latency) + + def has_errors(self, errors: list[dict[str, Any]], context: dict[str, Any]) -> None: + """ + Extension hook executed when GraphQL encountered errors. + """ + GQL_ERROR_COUNTER.labels( + operation_type=self.operation_type, operation_name=self.operation_name + ).inc(len(errors)) + + +class RequestFinalizer: + """ + A context manager class used as a teardown step after the GraphQL request is fully handled. + """ + + # List of keys representing files to be deleted during cleanup + TO_BE_DELETED_FILES = [ + "bundle_analysis_head_report_db_path", + "bundle_analysis_base_report_db_path", + ] + + def __init__(self, request: WSGIRequest) -> None: + self.request = request + + def _remove_temp_files(self) -> None: + """ + Some requests cause temporary files to be created in /tmp (eg BundleAnalysis) + This cleanup step clears all contents of the /tmp directory after each request + """ + for key in RequestFinalizer.TO_BE_DELETED_FILES: + if hasattr(self.request, key): + file_path = getattr(self.request, key) + if file_path: + try: + if os.path.isfile(file_path) or os.path.islink(file_path): + os.unlink(file_path) + except Exception as e: + log.info( + "Failed to delete temp file", + extra={"file_path": file_path, "exc": e}, + ) + + def __enter__(self) -> None: + pass + + def __exit__(self, exc_type: Any, exc_value: Any, exc_traceback: Any) -> None: + self._remove_temp_files() + + +class AsyncGraphqlView(GraphQLAsyncView): + schema = schema + extensions = [QueryMetricsExtension] + introspection = settings.GRAPHQL_INTROSPECTION_ENABLED + + def get_validation_rules( + self, + context_value: Optional[Any], + document: DocumentNode, + data: dict, + ) -> Optional[Collection]: + return [ + create_required_variables_rule(variables=data.get("variables", {})), + create_max_aliases_rule(max_aliases=settings.GRAPHQL_MAX_ALIASES), + create_max_depth_rule(max_depth=settings.GRAPHQL_MAX_DEPTH), + cost_validator( + maximum_cost=settings.GRAPHQL_QUERY_COST_THRESHOLD, + default_cost=1, + variables=data.get("variables"), + ), + ] + + validation_rules = get_validation_rules # type: ignore + + def get_clean_query(self, request_body: dict[str, Any]) -> str | None: + # clean up graphql query to remove new lines and extra spaces + if "query" in request_body and isinstance(request_body["query"], str): + clean_query = request_body["query"].replace("\n", " ") + clean_query = clean_query.replace(" ", "").strip() + return clean_query + + async def get(self, *args: Any, **kwargs: Any) -> HttpResponse: + if settings.GRAPHQL_PLAYGROUND: + return await super().get(*args, **kwargs) + # No GraphqlPlayground if no settings.DEBUG + return HttpResponseNotAllowed(["POST"]) + + async def post( + self, request: WSGIRequest, *args: Any, **kwargs: Any + ) -> HttpResponse: + await self._get_user(request) + # get request body information for logging + req_body = json.loads(request.body.decode("utf-8")) if request.body else {} + + # get request path information for logging + req_path = request.get_full_path() + + # clean up graphql query for logging, remove new lines and extra spaces + cleaned_query = self.get_clean_query(req_body) + if cleaned_query: + req_body["query"] = cleaned_query + + # put everything together for log + log_data = { + "server_hostname": socket.gethostname(), + "request_method": request.method, + "request_path": req_path, + "request_body": req_body, + "user": request.user, + } + log.info("GraphQL Request", extra=log_data) + inc_counter(GQL_REQUEST_MADE_COUNTER, labels=dict(path=req_path)) + if self._check_ratelimit(request=request): + inc_counter( + GQL_ERROR_TYPE_COUNTER, + labels=dict(error_type="rate_limit", path=req_path), + ) + return JsonResponse( + data={ + "status": 429, + "detail": f"It looks like you've hit the rate limit of {settings.GRAPHQL_RATE_LIMIT_RPM} req/min. Try again later.", + }, + status=429, + ) + + with RequestFinalizer(request): + try: + response = await super().post(request, *args, **kwargs) + except MissingVariablesError as e: + return JsonResponse( + data={ + "status": 400, + "detail": str(e), + }, + status=400, + ) + + content = response.content.decode("utf-8") + try: + data = json.loads(content) + except json.JSONDecodeError: + log.error( + "Failed to decode JSON response", + extra={"content": content, "request_body": req_body}, + ) + return JsonResponse( + data={ + "status": 400, + "detail": "Invalid JSON response received.", + }, + status=400, + ) + + if "errors" in data: + inc_counter( + GQL_ERROR_TYPE_COUNTER, + labels=dict(error_type="all", path=req_path), + ) + try: + if data["errors"][0]["extensions"]["cost"]: + costs = data["errors"][0]["extensions"]["cost"] + log.error( + "Query Cost Exceeded", + extra=dict( + requested_cost=costs.get("requestedQueryCost"), + maximum_cost=costs.get("maximumAvailable"), + request_body=req_body, + ), + ) + inc_counter( + GQL_ERROR_TYPE_COUNTER, + labels=dict( + error_type="query_cost_exceeded", + path=req_path, + ), + ) + return HttpResponseBadRequest( + JsonResponse("Your query is too costly.") + ) + except Exception: + pass + return response + + def context_value(self, request: WSGIRequest, *_args: Any) -> dict[str, Any]: + request_body = json.loads(request.body.decode("utf-8")) if request.body else {} + self.request = request + + return { + "request": request, + "service": request.resolver_match.kwargs["service"], + "executor": get_executor_from_request(request), + "clean_query": self.get_clean_query(request_body) if request_body else "", + } + + def error_formatter(self, error: Any, debug: bool = False) -> dict[str, Any]: + user = self.request.user + is_anonymous = user.is_anonymous if user else True + # the only way to check for a malformed query + is_bad_query = "Cannot query field" in error.formatted["message"] + if debug or (not is_anonymous and is_bad_query): + return format_error(error, debug) + formatted = error.formatted + formatted["message"] = "INTERNAL SERVER ERROR" + formatted["type"] = "ServerError" + # if this is one of our own command exception, we can tell a bit more + original_error = error.original_error + if isinstance(original_error, BaseException) or isinstance( + original_error, ServiceException + ): + formatted["message"] = original_error.message # type: ignore + formatted["type"] = type(original_error).__name__ + else: + # otherwise it's not supposed to happen, so we log it + log.error("GraphQL internal server error", exc_info=original_error) + capture_exception(original_error) + return formatted + + @sync_to_async + def _get_user(self, request: WSGIRequest) -> None: + # force eager evaluation of `request.user` (a lazy object) + # while we're in a sync context + if request.user: + request.user.pk + + def _check_ratelimit(self, request: WSGIRequest) -> bool: + if not settings.GRAPHQL_RATE_LIMIT_ENABLED: + return False + + redis = get_redis_connection() + + try: + # eagerly try to get user_id from request object + user_id = request.user.pk + except AttributeError: + user_id = None + + if user_id: + key = f"rl-user:{user_id}" + else: + user_ip = self.get_client_ip(request) + key = f"rl-ip:{user_ip}" + + limit = settings.GRAPHQL_RATE_LIMIT_RPM + window = 60 # in seconds + + current_count = redis.get(key) + if current_count is None: + log.info( + "[GQL Rate Limit] - Setting new key", + extra=dict(key=key, user_id=user_id), + ) + redis.set(name=key, ex=window, value=1) + elif int(current_count) >= limit: + log.warning( + "[GQL Rate Limit] - Rate limit reached for key", + extra=dict(key=key, limit=limit, count=current_count, user_id=user_id), + ) + return True + else: + log.warning( + "[GQL Rate Limit] - Incrementing rate limit for key", + extra=dict(key=key, limit=limit, count=current_count, user_id=user_id), + ) + redis.incr(key) + return False + + def get_client_ip(self, request: WSGIRequest) -> str: + x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") + if x_forwarded_for: + ip = x_forwarded_for.split(",")[0] + else: + ip = request.META.get("REMOTE_ADDR") + return ip + + +BaseAriadneView = AsyncGraphqlView.as_view() + + +async def ariadne_view(request: WSGIRequest, service: str) -> HttpResponse: + response = BaseAriadneView(request, service) + if iscoroutine(response): + response = await response + return response + + +ariadne_view.csrf_exempt = True diff --git a/apps/codecov-api/graphs/__init__.py b/apps/codecov-api/graphs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/graphs/badges/badges.py b/apps/codecov-api/graphs/badges/badges.py new file mode 100644 index 0000000000..663bb1f3a7 --- /dev/null +++ b/apps/codecov-api/graphs/badges/badges.py @@ -0,0 +1,99 @@ +large_badge = """ + + + + + + + + + + + + + + codecov + codecov + {1}% + {1}% + + + + + +""" + +medium_badge = """ + + + + + + + + + + + + + + codecov + codecov + {1}% + {1}% + + + + + +""" + +small_badge = """ + + + + + + + + + + + + + + codecov + codecov + {1}% + {1}% + + + + + +""" + +unknown_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + +""" diff --git a/apps/codecov-api/graphs/helpers/__init__.py b/apps/codecov-api/graphs/helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/graphs/helpers/badge.py b/apps/codecov-api/graphs/helpers/badge.py new file mode 100644 index 0000000000..2e24a9e9c4 --- /dev/null +++ b/apps/codecov-api/graphs/helpers/badge.py @@ -0,0 +1,126 @@ +from shared.helpers.color import coverage_to_color + +from graphs.badges.badges import large_badge, medium_badge, small_badge, unknown_badge + + +def get_badge(coverage: str | None, coverage_range: list[int], precision: str): + """ + Returns and SVG string containing coverage badge + + Parameters: + coverage (str): coverage to be displayed in badge + coverage_range (array): array containing two values: + coverage_range[0] (int): coverage low threshold to use red as badge color + coverage_range[1] (int): coverage high threshold to use green as badge color + precision: (str): amount of decimals to be displayed in badge + """ + precision = int(precision) + if coverage is not None: + # Get color for badge + color = coverage_to_color(*coverage_range)(coverage) + # Use medium badge to fit coverage of 100% + if float(coverage) == 100: + badge = medium_badge + # Use badge size based on precision (0 = small, 1 = medium, 2 = large) + elif precision == 0: + badge = small_badge + elif precision == 1: + badge = medium_badge + else: + badge = large_badge + else: + badge = unknown_badge + + return ( + badge.format(color.hex, coverage).strip() if badge != unknown_badge else badge + ) + + +def format_coverage_precision(coverage: float | None, precision: int): + """ + Returns coverage as a string formatted with appropriate precision + + Parameters: + coverage (string): coverage value + precision (string): amount of decimals to be displayed in coverage + """ + if coverage is None: + return None + + precision = int(precision) + coverage = float(coverage) + return ("%%.%sf" % precision) % coverage + + +def get_bundle_badge(bundle_size_bytes: int | None, precision: int): + if bundle_size_bytes is None: + # Returns text 'unknown' instead of bundle size + return """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + +""" + + bundle_size_string = format_bundle_bytes(bundle_size_bytes, precision) + char_width = 7 # approximate, looks good on all reasonable inputs + width_in_pixels = len(bundle_size_string) * char_width + static_width = 57 # width of static elements in the svg (text + margins) + + width = static_width + width_in_pixels + + return f""" + + + + + + + + + + + + + + bundle + bundle + {bundle_size_string} + {bundle_size_string} + + +""" + + +def format_bundle_bytes(bytes: int, precision: int): + precision = min(abs(precision), 2) # allow at most 2 decimal places + kilobyte = 10**3 + megabyte = 10**6 + gigabyte = 10**9 + + def remove_trailing_zeros(n: str): + return (n.rstrip("0") if "." in n else n).rstrip(".") + + if bytes < kilobyte: + return f"{bytes}B" + elif bytes < megabyte: + return f"{remove_trailing_zeros(str(round(bytes / kilobyte, precision)))}KB" + elif bytes < gigabyte: + return f"{remove_trailing_zeros(str(round(bytes / megabyte, precision)))}MB" + else: + return f"{remove_trailing_zeros(str(round(bytes / gigabyte, precision)))}GB" diff --git a/apps/codecov-api/graphs/helpers/graph_utils.py b/apps/codecov-api/graphs/helpers/graph_utils.py new file mode 100644 index 0000000000..ee548eaf87 --- /dev/null +++ b/apps/codecov-api/graphs/helpers/graph_utils.py @@ -0,0 +1,206 @@ +from math import cos, pi, sin + +style_n_defs = """ + + + + + + + + + +""" + + +def _squarify(values, left, top, width, height, **kwargs): + # values should add up to width * height + if len(values) == 0: + return [] + + if len(values) == 1: + return _layout(values, left, top, width, height)[0] + + i = 1 + while i < len(values) and _worst_ratio( + values[:i], left, top, width, height + ) >= _worst_ratio(values[: (i + 1)], left, top, width, height): + i += 1 + + current = values[:i] + remaining = values[i:] + + rectangles, leftover_space = _layout(current, left, top, width, height) + return rectangles + _squarify(remaining, *leftover_space) + + +def _layout(areas, left, top, width, height, **kwargs): + layout_area = sum(areas) + vertical = width >= height + rectangles = [] + if vertical: + layout_width = layout_area / height + rect_top = top + for area in areas: + rect_height = area / layout_width + rectangles.append((left, rect_top, layout_width, rect_height)) + rect_top += rect_height + leftover_space = (left + layout_width, top, width - layout_width, height) + else: + layout_height = layout_area / width + rect_left = left + for area in areas: + rect_width = area / layout_height + rectangles.append((rect_left, top, rect_width, layout_height)) + rect_left += rect_width + leftover_space = (left, top + layout_height, width, height - layout_height) + return rectangles, leftover_space + + +def _worst_ratio(areas, left, top, width, height, **kwargs): + rectangles, leftover = _layout(areas, left, top, width, height) + return max(map(_max_aspect_ratio, rectangles)) + + +def _max_aspect_ratio(rect): + return max( + (rect[2] / rect[3]) if rect[3] > 0 else 0, + (rect[3] / rect[2]) if rect[2] > 0 else 0, + ) + + +def _svg_rect(x, y, width, height, fill, stroke, stroke_width, _class=None, title=None): + """http://www.w3schools.com/svg/svg_rect.asp""" + if title is None: + return ( + ''.format( + x, + y, + width, + height, + fill, + stroke, + stroke_width, + ('class="%s"' % _class if _class else ""), + ) + ) + + return ( + '' + "{7}" + "".format( + x, y, width, height, fill, stroke, stroke_width, title, _class or "" + ) + ) + + +def _make_svg(width, height, elements, viewPortWidth=None, viewPortHeight=None): + return ( + '\n' + "{2}\n" + "{3}\n" + "".format( + width, + height, + style_n_defs, + "\n".join(elements), + viewPortWidth or width, + viewPortHeight or height, + ) + ) + + +def _tree_height(tree): + if not tree: + return 0 + + subtrees = filter(None, (item.get("children", None) for item in tree)) + + if not subtrees: + return 1 + children_map = list(map(_tree_height, subtrees)) + if len(children_map) < 1: + return 1 + + return 1 + max(children_map) + + +def _svg_polar_rect( + cx, cy, inner_radius, outer_radius, start, end, fill, stroke, stroke_width +): + """ + http://www.w3schools.com/svg/svg_circle.asp + http://www.w3schools.com/svg/svg_path.asp + """ + + # special case: circle + if inner_radius == 0 and end - start == 1: + return ''.format( + cx, cy, fill, outer_radius, stroke, stroke_width + ) + + in_angle = 2.0 * pi * start + out_angle = 2.0 * pi * end + + # special case: ring + if end - start == 1: + # make a ring using two circles + # each circle consists of 2 180-degree arcs + # from (cx - r, cy) to (cx + r, cy) and back + + # outer contour + d = "M {x1} {y} A {r} {r} 0 0 0 {x2} {y} A {r} {r} 0 0 0 {x1} {y} z ".format( + x1=cx - outer_radius, x2=cx + outer_radius, r=outer_radius, y=cy + ) + # inner contour + d += "M {x1} {y} A {r} {r} 0 0 0 {x2} {y} A {r} {r} 0 0 0 {x1} {y} z ".format( + x1=cx - inner_radius, x2=cx + inner_radius, r=inner_radius, y=cy + ) + + return ''.format( + d, fill, stroke, stroke_width + ) + + # start points + spx_outer = outer_radius * sin(in_angle) + spy_outer = outer_radius * cos(in_angle) + spx_inner = inner_radius * sin(out_angle) + spy_inner = inner_radius * cos(out_angle) + + # target points + tpx_outer = outer_radius * sin(out_angle) + tpy_outer = outer_radius * cos(out_angle) + tpx_inner = inner_radius * sin(in_angle) + tpy_inner = inner_radius * cos(in_angle) + + large_arc_flag = 1 if end - start > 0.5 else 0 + + path_args = ( + "M {} {} L {} {} A {} {} 0 {} 0 {} {} L {} {} A {} {} 0 {} 1 {} {} z".format( + cx + tpx_inner, + cy + tpy_inner, + cx + spx_outer, + cy + spy_outer, + outer_radius, + outer_radius, + large_arc_flag, + cx + tpx_outer, + cy + tpy_outer, + cx + spx_inner, + cy + spy_inner, + inner_radius, + inner_radius, + large_arc_flag, + cx + tpx_inner, + cy + tpy_inner, + ) + ) + + return ''.format( + path_args, fill, stroke, stroke_width + ) diff --git a/apps/codecov-api/graphs/helpers/graphs.py b/apps/codecov-api/graphs/helpers/graphs.py new file mode 100644 index 0000000000..2eabd3a2d2 --- /dev/null +++ b/apps/codecov-api/graphs/helpers/graphs.py @@ -0,0 +1,181 @@ +from operator import itemgetter + +from graphs.settings import settings + +from .graph_utils import ( + _make_svg, + _squarify, + _svg_polar_rect, + _svg_rect, + _tree_height, +) + + +def tree(parsed_data, href=None, classes=None, **kwargs): + """ + [ + { + "lines": 10, + "color": "#aaaeee", + "children": [], + "name": "path" + } + ] + """ + options = settings["sunburst"]["options"].copy() + options.update(kwargs) + + svg_elements = [] + + def recursively_draw(items, step, left, top, width, height): + values = [item["lines"] for item in items] + _sum_values = sum(values) + if _sum_values > 0: + correction = width * height / _sum_values + sorted_values = sorted( + enumerate(value * correction for value in values), + key=itemgetter(1), + reverse=True, + ) + indices = [x[0] for x in sorted_values] + values = [x[1] for x in sorted_values] + rectangles = _squarify(values, left, top, width, height) + for rect, color, _class, children, name in zip( + rectangles, + (items[index]["color"] for index in indices), + (items[index]["_class"] for index in indices), + (items[index].get("children", None) for index in indices), + (items[index]["name"] for index in indices), + ): + step.append(name) + if children: + recursively_draw(children, step, *rect) + step.pop(-1) + else: + path = "/".join(step[1:]) + rect = _svg_rect( + rect[0], + rect[1], + rect[2], + rect[3], + fill=color, + stroke=options["border_color"], + stroke_width=options["border_size"], + _class=_class, + title=path, + ) + svg_elements.append(rect) + step.pop(-1) + + recursively_draw( + parsed_data, + [], + 0, + 0, + options.get("viewPortWidth") or options["width"], + options.get("viewPortHeight") or options["height"], + ) + + return _make_svg( + options["width"], + options["height"], + svg_elements, + options.get("viewPortWidth"), + options.get("viewPortHeight"), + ) + + +def icicle(parsed_data, **kwargs): + options = settings["icicle"]["options"].copy() + options.update(kwargs) + + drawing_width = options["width"] + drawing_height = options["height"] + + # ensure 5% frame + plot_width = drawing_width * 0.9 + plot_height = drawing_height * 0.9 + + # starting point + sx, sy = drawing_width * 0.05, drawing_height * 0.05 + strip_height = plot_height / _tree_height(parsed_data) + + svg_elements = [] + + def recursively_draw(items, x, y, max_width, prefix_name): + total = sum((item["lines"] for item in items)) + if total > 0: + for item in items: + item_width = item["lines"] / total * max_width + title = prefix_name + "/" + item["name"] + svg_elements.append( + _svg_rect( + x, + y, + item_width, + strip_height, + fill=item["color"], + stroke=options["border_color"], + title=title, + stroke_width=options["border_size"], + ) + ) + if "children" in item.keys(): + recursively_draw( + item["children"], x, y + strip_height, item_width, title + ) + x += item_width + + recursively_draw(parsed_data, sx, sy, plot_width, "") + + return _make_svg(drawing_width, drawing_height, svg_elements) + + +def sunburst(parsed_data, **kwargs): + options = settings["sunburst"]["options"].copy() + options.update(kwargs) + + drawing_width = options["width"] + drawing_height = options["height"] + cx = drawing_width / 2.0 + cy = drawing_height / 2.0 + + # ensure 5% frame + max_diameter = min(drawing_width, drawing_height) * 0.95 + max_radius = max_diameter / 2.0 + + offset_increment = max_radius / _tree_height(parsed_data) + + svg_elements = [] + + def recursively_draw(items, inner_radius, start, end): + total = sum((item["lines"] for item in items)) + if total > 0: + s = start + for item in items: + arc_size = item["lines"] / total * (end - start) + svg_elements.append( + _svg_polar_rect( + cx, + cy, + inner_radius, + inner_radius + offset_increment, + s, + s + arc_size, + item["color"], + options["border_color"], + options["border_size"], + ) + ) + if "children" in item.keys(): + recursively_draw( + item["children"], + inner_radius + offset_increment, + s, + s + arc_size, + ) + s += arc_size + + recursively_draw(parsed_data, 0, 0, 1) + + return _make_svg(drawing_width, drawing_height, svg_elements) diff --git a/apps/codecov-api/graphs/mixins.py b/apps/codecov-api/graphs/mixins.py new file mode 100644 index 0000000000..e1b74563dc --- /dev/null +++ b/apps/codecov-api/graphs/mixins.py @@ -0,0 +1,37 @@ +from typing import Any + +from django.http import HttpResponse +from rest_framework import status +from rest_framework.request import Request +from rest_framework.response import Response + + +class GraphBadgeAPIMixin(object): + def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: + ext = self.kwargs.get("ext") + if ext not in self.extensions: + return Response( + { + "detail": f"File extension should be one of [ {' || '.join(self.extensions)} ]" + }, + status=status.HTTP_404_NOT_FOUND, + ) + + graph = self.get_object( + request, *args, **kwargs + ) # for badge handler this will get the badge, for graph it will get the graph + # do all the header stuff and return the response + + response = HttpResponse(graph) + if self.kwargs.get("ext") == "svg": + response["Content-Disposition"] = ' inline; filename="{}.svg"'.format( + self.filename + ) + response["Content-Type"] = "image/svg+xml" + response["Pragma"] = "no-cache" + response["Expires"] = "0" + response["Access-Control-Expose-Headers"] = ( + "Content-Type, Cache-Control, Expires, Etag, Last-Modified" + ) + response["Cache-Control"] = "no-cache, no-store, must-revalidate, max-age=0" + return response diff --git a/apps/codecov-api/graphs/settings.py b/apps/codecov-api/graphs/settings.py new file mode 100644 index 0000000000..2a8e37baaa --- /dev/null +++ b/apps/codecov-api/graphs/settings.py @@ -0,0 +1,50 @@ +settings = { + "icicle": { + "method": "flare", + "options": { + "width": 750, + "height": 150, + "border_size": 1, + "border_color": "white", + }, + "exports": ["svg"], + "types": ["commit", "pull", "branch"], + }, + "tree": { + "method": "flare", + "options": { + "width": 500, + "height": 500, + "border_size": 1, + "border_color": "white", + }, + "exports": ["svg", "json"], + "types": ["commit", "pull", "branch"], + }, + "sunburst": { + "method": "flare", + "options": { + "width": 300, + "height": 300, + "border_size": 1, + "border_color": "white", + }, + "exports": ["svg", "html"], + "types": ["commit", "pull", "branch"], + }, + "commits": { + "method": "commits", + "options": { + "width": 700, + "height": 100, + "color": "yes", + "legend": "yes", + "yaxis": [0, 100], + "hg": "yes", + "vg": "yes", + "limit": 20, + }, + "exports": ["svg", "json"], + "types": ["pull", "branch"], + }, +} diff --git a/apps/codecov-api/graphs/tests/test_badge_handler.py b/apps/codecov-api/graphs/tests/test_badge_handler.py new file mode 100644 index 0000000000..c8b47ccf28 --- /dev/null +++ b/apps/codecov-api/graphs/tests/test_badge_handler.py @@ -0,0 +1,1691 @@ +from unittest.mock import PropertyMock, patch + +from rest_framework import status +from rest_framework.test import APITestCase +from shared.django_apps.core.tests.factories import ( + BranchFactory, + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.reports.resources import Report, ReportFile, Session, SessionType +from shared.reports.types import ReportLine, ReportTotals +from shared.yaml import UserYaml + + +def sample_report(): + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1], [1, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1], [1, 0]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session( + Session( + flags=["unittests"], + provider="circleci", + session_type=SessionType.uploaded, + build="aycaramba", + totals=ReportTotals(files=2, lines=10, coverage=95.00000), + ) + ) + report.add_session( + Session( + flags=["integration"], + provider="travis", + session_type=SessionType.uploaded, + build="poli", + totals=ReportTotals(files=2, lines=10, coverage=None), + ) + ) + report.add_session( + Session( + flags=["aaa"], + provider="travis", + session_type=SessionType.uploaded, + build="poli", + totals=ReportTotals(files=2, lines=10, coverage=None), + ) + ) + report.add_session( + Session( + flags=["no_coverage"], + provider="travis", + session_type=SessionType.uploaded, + build="poli", + totals=ReportTotals(files=0, lines=0, coverage=None), + ) + ) + return report + + +def sample_report_no_flags(): + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1], [1, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1], [1, 0]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session( + Session( + flags=None, + provider="circleci", + session_type=SessionType.uploaded, + build="aycaramba", + totals=ReportTotals(files=2, lines=10, coverage=95.00000), + ) + ) + return report + + +class TestBadgeHandler(APITestCase): + def _get(self, kwargs={}, data={}): + path = f"/{kwargs.get('service')}/{kwargs.get('owner_username')}/{kwargs.get('repo_name')}/graphs/badge.{kwargs.get('ext')}" + return self.client.get(path, data=data) + + def _get_branch(self, kwargs={}, data={}): + path = f"/{kwargs.get('service')}/{kwargs.get('owner_username')}/{kwargs.get('repo_name')}/branch/{kwargs.get('branch')}/graphs/badge.{kwargs.get('ext')}" + return self.client.get(path, data=data) + + def test_invalid_precision(self): + response = self._get( + kwargs={ + "service": "gh", + "owner_username": "user", + "repo_name": "repo", + "ext": "svg", + }, + data={"precision": "3"}, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert ( + response.data["detail"] + == "Coverage precision should be one of [ 0 || 1 || 2 ]" + ) + + def test_invalid_extension(self): + response = self._get( + kwargs={ + "service": "gh", + "owner_username": "user", + "repo_name": "repo", + "ext": "png", + } + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert ( + response.data["detail"] == "File extension should be one of [ svg || txt ]" + ) + + def test_unknown_badge_incorrect_service(self): + response = self._get( + kwargs={ + "service": "gih", + "owner_username": "user", + "repo_name": "repo", + "ext": "svg", + } + ) + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + + """ + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_unknown_badge_incorrect_owner(self): + response = self._get( + kwargs={ + "service": "gh", + "owner_username": "user1233", + "repo_name": "repo", + "ext": "svg", + } + ) + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + + """ + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_unknown_badge_incorrect_repo(self): + gh_owner = OwnerFactory(service="github") + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo", + "ext": "svg", + } + ) + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + + """ + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_unknown_badge_no_branch(self): + gh_owner = OwnerFactory(service="github") + RepositoryFactory(author=gh_owner, active=True, private=False, name="repo1") + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + } + ) + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + + """ + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_unknown_badge_no_commit(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + BranchFactory(repository=repo, name="master") + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + } + ) + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + + """ + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_unknown_badge_no_totals(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + CommitFactory(repository=repo, author=gh_owner, totals=None) + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + } + ) + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + + """ + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_text_badge(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + CommitFactory(repository=repo, author=gh_owner) + + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "txt", + } + ) + + badge = response.content.decode("utf-8") + assert badge == "85" + assert response.status_code == status.HTTP_200_OK + + # test precision = 1 + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "txt", + }, + data={"precision": "1"}, + ) + + badge = response.content.decode("utf-8") + assert badge == "85.0" + assert response.status_code == status.HTTP_200_OK + + # test precision = 1 + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "txt", + }, + data={"precision": "2"}, + ) + + badge = response.content.decode("utf-8") + assert badge == "85.00" + assert response.status_code == status.HTTP_200_OK + + def test_svg_badge(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + CommitFactory(repository=repo, author=gh_owner) + + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + } + ) + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + 85% + 85% + + + + + """ + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + # test precision = 1 + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + data={"precision": "1"}, + ) + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + 85.0% + 85.0% + + + + + """ + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert response.status_code == status.HTTP_200_OK + + # test precision = 1 + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + data={"precision": "2"}, + ) + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + 85.00% + 85.00% + + + + + """ + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert response.status_code == status.HTTP_200_OK + + def test_private_badge_no_token(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=True, + name="repo1", + image_token="12345678", + ) + CommitFactory(repository=repo, author=gh_owner) + + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + } + ) + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + + """ + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_private_badge(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=True, + name="repo1", + image_token="12345678", + ) + CommitFactory(repository=repo, author=gh_owner) + + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + data={"token": "12345678"}, + ) + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + 85% + 85% + + + + + """ + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_branch_badge(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=True, + name="repo1", + image_token="12345678", + branch="branch1", + ) + CommitFactory(repository=repo, author=gh_owner) + commit_2_totals = { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "95.00000", + "d": 0, + "diff": [1, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + "f": 3, + "h": 17, + "m": 3, + "n": 20, + "p": 0, + "s": 1, + } + commit_2 = CommitFactory( + commitid="b1c2b4fa3ae9ef615c8f740c5cba95d9851f9ae8", + repository=repo, + author=gh_owner, + totals=commit_2_totals, + ) + branch_2 = BranchFactory( + repository=repo, name="branch1", head=commit_2.commitid + ) + + # test default precision + response = self._get_branch( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "branch": "branch1", + }, + data={"token": "12345678"}, + ) + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + 95% + 95% + + + + + """ + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + branch_2.refresh_from_db() + assert branch_2.head == "b1c2b4fa3ae9ef615c8f740c5cba95d9851f9ae8" + + def test_badge_with_100_coverage(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=True, + name="repo1", + image_token="12345678", + branch="branch1", + ) + CommitFactory(repository=repo, author=gh_owner) + commit_2_totals = { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "100.00000", + "d": 0, + "diff": [1, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + "f": 3, + "h": 17, + "m": 3, + "n": 20, + "p": 0, + "s": 1, + } + commit_2 = CommitFactory( + repository=repo, author=gh_owner, totals=commit_2_totals + ) + BranchFactory(repository=repo, name="branch1", head=commit_2.commitid) + # test default precision + response = self._get_branch( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "branch": "branch1", + }, + data={"token": "12345678"}, + ) + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + 100% + 100% + + + + + """ + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_branch_badge_with_slash(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=True, + name="repo1", + image_token="12345678", + branch="branch1", + ) + CommitFactory(repository=repo, author=gh_owner) + commit_2_totals = { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "95.00000", + "d": 0, + "diff": [1, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + "f": 3, + "h": 17, + "m": 3, + "n": 20, + "p": 0, + "s": 1, + } + commit_2 = CommitFactory( + repository=repo, author=gh_owner, totals=commit_2_totals + ) + BranchFactory(repository=repo, name="test/branch1", head=commit_2.commitid) + # test default precision + response = self._get_branch( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "branch": "test%2Fbranch1", + }, + data={"token": "12345678"}, + ) + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + 95% + 95% + + + + + """ + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("core.models.Commit.full_report", new_callable=PropertyMock) + def test_flag_badge(self, full_report_mock): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + CommitFactory(repository=repo, author=gh_owner) + full_report_mock.return_value = sample_report() + + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + data={"flag": "unittests"}, + ) + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + 100% + 100% + + + + + """ + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_none_branch_flag_badge(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=False, + name="repo1", + branch="not-master", + ) + CommitFactory(repository=repo, author=gh_owner) + + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + data={"flag": "unittests"}, + ) + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + + """ + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("core.models.Commit.full_report", new_callable=PropertyMock) + def test_unknown_flag_badge(self, full_report_mock): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + CommitFactory(repository=repo, author=gh_owner) + full_report_mock.return_value = sample_report() + + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + data={"flag": "aaa"}, + ) + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + + """ + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("core.models.Commit.full_report", new_callable=PropertyMock) + def test_unknown_sessions_flag_badge(self, full_report_mock): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + CommitFactory(repository=repo, author=gh_owner) + full_report_mock.return_value = sample_report() + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + data={"flag": "unit"}, + ) + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + + """ + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("core.models.Commit.full_report", new_callable=PropertyMock) + def test_unknown_report_flag_badge(self, full_report_mock): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + CommitFactory(repository=repo, author=gh_owner) + full_report_mock.return_value = sample_report() + + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + data={"flag": "unit"}, + ) + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + + """ + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("core.models.Commit.full_report", new_callable=PropertyMock) + @patch("graphs.views.commit_components") + def test_component_badge(self, commit_components_mock, full_report_mock): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=False, + name="repo1", + yaml={ + "component_management": { + "individual_components": [ + { + "component_id": "file_1", + "name": "File1", + "paths": ["file_1.go"], + } + ] + } + }, + ) + CommitFactory(repository=repo, author=gh_owner) + commit_components_mock.return_value = UserYaml.get_final_yaml( + repo_yaml=repo.yaml + ).get_components() + full_report_mock.return_value = sample_report() + + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + data={"component": "File1"}, + ) + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + 62% + 62% + + + + + """ + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("core.models.Commit.full_report", new_callable=PropertyMock) + @patch("graphs.views.commit_components") + def test_flag_component_badge(self, commit_components_mock, full_report_mock): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=False, + name="repo1", + yaml={ + "component_management": { + "individual_components": [ + { + "component_id": "unittests", + "name": "Unit tests", + "flag_regexes": ["unittests"], + } + ] + } + }, + ) + CommitFactory(repository=repo, author=gh_owner) + commit_components_mock.return_value = UserYaml.get_final_yaml( + repo_yaml=repo.yaml + ).get_components() + full_report_mock.return_value = sample_report() + + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + data={"component": "unittests"}, + ) + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + 100% + 100% + + + + + """ + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("core.models.Commit.full_report", new_callable=PropertyMock) + @patch("graphs.views.commit_components") + def test_unknown_component_badge(self, commit_components_mock, full_report_mock): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=False, + name="repo1", + yaml={ + "component_management": { + "individual_components": [ + { + "component_id": "unittests", + "name": "Unit tests", + "flag_regexes": ["unittests"], + } + ] + } + }, + ) + CommitFactory(repository=repo, author=gh_owner) + commit_components_mock.return_value = UserYaml.get_final_yaml( + repo_yaml=repo.yaml + ).get_components() + full_report_mock.return_value = sample_report() + + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + data={"component": "idk"}, + ) + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + + """ + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("core.models.Commit.full_report", new_callable=PropertyMock) + @patch("graphs.views.commit_components") + def test_component_badge_no_report(self, commit_components_mock, full_report_mock): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=False, + name="repo1", + yaml={ + "component_management": { + "individual_components": [ + { + "component_id": "unittests", + "name": "Unit tests", + "flag_regexes": ["unittests"], + } + ] + } + }, + ) + CommitFactory(repository=repo, author=gh_owner) + commit_components_mock.return_value = UserYaml.get_final_yaml( + repo_yaml=repo.yaml + ).get_components() + full_report_mock.return_value = None + + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + data={"component": "unittests"}, + ) + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + + """ + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("core.models.Commit.full_report", new_callable=PropertyMock) + @patch("graphs.views.commit_components") + def test_component_badge_report_doesnt_have_matching_coverage( + self, commit_components_mock, full_report_mock + ): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=False, + name="repo1", + yaml={ + "component_management": { + "individual_components": [ + { + "component_id": "no_coverage", + "name": "No Coverage", + "flag_regexes": ["no_coverage"], + } + ] + } + }, + ) + CommitFactory(repository=repo, author=gh_owner) + commit_components_mock.return_value = UserYaml.get_final_yaml( + repo_yaml=repo.yaml + ).get_components() + full_report_mock.return_value = sample_report() + + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + data={"component": "no_coverage"}, + ) + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + + """ + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_yaml_range(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=False, + name="repo1", + yaml={"coverage": {"range": [0.0, 0.8]}}, + ) + CommitFactory(repository=repo, author=gh_owner) + + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + } + ) + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + 85% + 85% + + + + + """ + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_yaml_empty_range(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=False, + name="repo1", + yaml={"coverage": {}}, + ) + CommitFactory(repository=repo, author=gh_owner) + + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + } + ) + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + 85% + 85% + + + + + """ + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("core.models.Commit.full_report", new_callable=PropertyMock) + def test_commit_report_null(self, full_report_mock): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + CommitFactory(repository=repo, author=gh_owner) + full_report_mock.return_value = None + + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + data={"flag": "unit"}, + ) + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + + """ + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("core.models.Commit.full_report", new_callable=PropertyMock) + def test_commit_report_no_flags(self, full_report_mock): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + CommitFactory(repository=repo, author=gh_owner) + full_report_mock.return_value = sample_report_no_flags() + + # test default precision + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + data={"flag": "unit"}, + ) + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + + """ + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK diff --git a/apps/codecov-api/graphs/tests/test_bundle_badge_handler.py b/apps/codecov-api/graphs/tests/test_bundle_badge_handler.py new file mode 100644 index 0000000000..0317ab05b2 --- /dev/null +++ b/apps/codecov-api/graphs/tests/test_bundle_badge_handler.py @@ -0,0 +1,540 @@ +from unittest.mock import patch + +from rest_framework import status +from rest_framework.test import APITestCase +from shared.bundle_analysis import BundleAnalysisReport, BundleReport +from shared.django_apps.core.tests.factories import ( + BranchFactory, + CommitFactory, + OwnerFactory, + RepositoryFactory, +) + + +class MockBundleReport(BundleReport): + def __init__(self): + return + + def total_size(self): + return 1234567 + + +class MockBundleAnalysisReport(BundleAnalysisReport): + def bundle_report(self, bundle_name: str): + if bundle_name == "idk": + return None + return MockBundleReport() + + +class TestBundleBadgeHandler(APITestCase): + def _get(self, kwargs={}, data={}): + path = f"/{kwargs.get('service')}/{kwargs.get('owner_username')}/{kwargs.get('repo_name')}/graphs/bundle/{kwargs.get('bundle')}/badge.{kwargs.get('ext')}" + return self.client.get(path, data=data) + + def _get_branch(self, kwargs={}, data={}): + path = f"/{kwargs.get('service')}/{kwargs.get('owner_username')}/{kwargs.get('repo_name')}/branch/{kwargs.get('branch')}/graphs/{kwargs.get('bundle')}/badge.{kwargs.get('ext')}" + return self.client.get(path, data=data) + + def test_invalid_extension(self): + response = self._get( + kwargs={ + "service": "gh", + "owner_username": "user", + "repo_name": "repo", + "ext": "png", + "bundle": "asdf", + } + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert ( + response.data["detail"] == "File extension should be one of [ svg || txt ]" + ) + + def test_unknown_badge_incorrect_service(self): + response = self._get( + kwargs={ + "service": "gih", + "owner_username": "user", + "repo_name": "repo", + "ext": "svg", + "bundle": "asdf", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + +""" + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_unknown_badge_incorrect_owner(self): + response = self._get( + kwargs={ + "service": "gh", + "owner_username": "user1233", + "repo_name": "repo", + "ext": "svg", + "bundle": "asdf", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + +""" + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_unknown_badge_incorrect_repo(self): + gh_owner = OwnerFactory(service="github") + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo", + "ext": "svg", + "bundle": "asdf", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + +""" + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_unknown_badge_private_repo_wrong_token(self): + gh_owner = OwnerFactory(service="github") + RepositoryFactory( + author=gh_owner, active=True, private=True, name="repo1", image_token="asdf" + ) + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "bundle": "asdf", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + +""" + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_unknown_badge_no_branch(self): + gh_owner = OwnerFactory(service="github") + RepositoryFactory(author=gh_owner, active=True, private=False, name="repo1") + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "bundle": "asdf", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + +""" + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + def test_unknown_badge_no_commit(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + branch = BranchFactory(name="main", repository=repo) + repo.branch = branch + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "bundle": "asdf", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + +""" + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("graphs.views.load_report") + def test_unknown_badge_no_report(self, mock_load_report): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + branch = BranchFactory(name="main", repository=repo) + repo.branch = branch + commit = CommitFactory( + repository=repo, commitid=repo.branch.head, branch="main" + ) + branch.head = commit.commitid + + mock_load_report.return_value = None + + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "bundle": "asdf", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + +""" + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("graphs.views.load_report") + def test_unknown_badge_no_bundle(self, mock_load_report): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + branch = BranchFactory(name="main", repository=repo) + repo.branch = branch + commit = CommitFactory( + repository=repo, commitid=repo.branch.head, branch="main" + ) + branch.head = commit.commitid + + mock_load_report.return_value = MockBundleAnalysisReport() + + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "bundle": "idk", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + +""" + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("graphs.views.load_report") + def test_bundle_badge(self, mock_load_report): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + branch = BranchFactory(name="main", repository=repo) + repo.branch = branch + commit = CommitFactory( + repository=repo, commitid=repo.branch.head, branch="main" + ) + branch.head = commit.commitid + + mock_load_report.return_value = MockBundleAnalysisReport() + + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "bundle": "asdf", + } + ) + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + 1.23MB + 1.23MB + + +""" + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("graphs.views.load_report") + def test_bundle_badge_text(self, mock_load_report): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + branch = BranchFactory(name="main", repository=repo) + repo.branch = branch + commit = CommitFactory( + repository=repo, commitid=repo.branch.head, branch="main" + ) + branch.head = commit.commitid + + mock_load_report.return_value = MockBundleAnalysisReport() + + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "txt", + "bundle": "asdf", + } + ) + expected_badge = "1.23MB" + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK + + @patch("graphs.views.load_report") + def test_bundle_badge_unsupported_precision_defaults_to_2(self, mock_load_report): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + branch = BranchFactory(name="main", repository=repo) + repo.branch = branch + commit = CommitFactory( + repository=repo, commitid=repo.branch.head, branch="main" + ) + branch.head = commit.commitid + + mock_load_report.return_value = MockBundleAnalysisReport() + + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "txt", + "bundle": "asdf", + }, + data={"precision": "asdf"}, + ) + expected_badge = "1.23MB" + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + + @patch("graphs.views.load_report") + def test_bundle_badge_private_repo_correct_token(self, mock_load_report): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=True, name="repo1", image_token="asdf" + ) + branch = BranchFactory(name="main", repository=repo) + repo.branch = branch + commit = CommitFactory( + repository=repo, commitid=repo.branch.head, branch="main" + ) + branch.head = commit.commitid + + mock_load_report.return_value = MockBundleAnalysisReport() + + response = self._get( + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "txt", + "bundle": "asdf", + }, + data={"token": "asdf"}, + ) + expected_badge = "1.23MB" + + badge = response.content.decode("utf-8") + badge = [line.strip() for line in badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == badge + assert response.status_code == status.HTTP_200_OK diff --git a/apps/codecov-api/graphs/tests/test_graph_handler.py b/apps/codecov-api/graphs/tests/test_graph_handler.py new file mode 100644 index 0000000000..5d5882d2d2 --- /dev/null +++ b/apps/codecov-api/graphs/tests/test_graph_handler.py @@ -0,0 +1,687 @@ +from unittest.mock import patch + +from rest_framework import status +from rest_framework.test import APITestCase +from shared.django_apps.core.tests.factories import ( + BranchFactory, + CommitFactory, + CommitWithReportFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) + + +@patch("shared.api_archive.archive.ArchiveService.read_chunks", lambda obj, _: "") +class TestGraphHandler(APITestCase): + def _get(self, graph_type, kwargs={}, data={}): + path = f"/{kwargs.get('service')}/{kwargs.get('owner_username')}/{kwargs.get('repo_name')}/graphs/{graph_type}.{kwargs.get('ext')}" + return self.client.get(path, data=data) + + def _get_branch(self, graph_type, kwargs={}, data={}): + path = f"/{kwargs.get('service')}/{kwargs.get('owner_username')}/{kwargs.get('repo_name')}/branch/{kwargs.get('branch')}/graphs/{graph_type}.{kwargs.get('ext')}" + return self.client.get(path, data=data) + + def _get_commit(self, graph_type, kwargs={}, data={}): + path = f"/{kwargs.get('service')}/{kwargs.get('owner_username')}/{kwargs.get('repo_name')}/commit/{kwargs.get('commit')}/graphs/{graph_type}.{kwargs.get('ext')}" + return self.client.get(path, data=data) + + def _get_pull(self, graph_type, kwargs={}, data={}): + path = f"/{kwargs.get('service')}/{kwargs.get('owner_username')}/{kwargs.get('repo_name')}/pull/{kwargs.get('pull')}/graphs/{graph_type}.{kwargs.get('ext')}" + return self.client.get(path, data=data) + + def test_invalid_extension(self): + response = self._get( + "tree", + kwargs={ + "service": "gh", + "owner_username": "user", + "repo_name": "repo", + "ext": "json", + }, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data["detail"] == "File extension should be one of [ svg ]" + + response = self._get( + "icicle", + kwargs={ + "service": "gh", + "owner_username": "user", + "repo_name": "repo", + "ext": "json", + }, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data["detail"] == "File extension should be one of [ svg ]" + + response = self._get( + "sunburst", + kwargs={ + "service": "gh", + "owner_username": "user", + "repo_name": "repo", + "ext": "json", + }, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data["detail"] == "File extension should be one of [ svg ]" + + response = self._get( + "commits", + kwargs={ + "service": "gh", + "owner_username": "user", + "repo_name": "repo", + "ext": "txt", + }, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data["detail"] == "File extension should be one of [ svg ]" + + def test_tree_graph(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + CommitWithReportFactory(repository=repo, author=gh_owner) + + # test default precision + response = self._get( + "tree", + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + ) + + expected_graph = """ + + + + + + + + + + + + tests/test_sample.py + tests/__init__.py + awesome/__init__.py + """ + + graph = response.content.decode("utf-8") + graph = [line.strip() for line in graph.split("\n")] + expected_graph = [line.strip() for line in expected_graph.split("\n")] + assert expected_graph == graph + assert response.status_code == status.HTTP_200_OK + + def test_icicle_graph(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + CommitWithReportFactory(repository=repo, author=gh_owner) + + # test default precision + response = self._get( + "icicle", + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + ) + + expected_graph = """ + + + + + + + + + + + + / + //tests + //tests/__init__.py + //tests/test_sample.py + //awesome + //awesome/__init__.py + """ + + graph = response.content.decode("utf-8") + graph = [line.strip() for line in graph.split("\n")] + expected_graph = [line.strip() for line in expected_graph.split("\n")] + assert expected_graph == graph + assert response.status_code == status.HTTP_200_OK + + def test_sunburst_graph(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, active=True, private=False, name="repo1" + ) + CommitWithReportFactory(repository=repo, author=gh_owner) + + # test default precision + response = self._get( + "sunburst", + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + ) + + expected_graph = """ + + + + + + + + + + + + + + + + + + """ + + graph = response.content.decode("utf-8") + graph = [line.strip() for line in graph.split("\n")] + expected_graph = [line.strip() for line in expected_graph.split("\n")] + assert expected_graph == graph + assert response.status_code == status.HTTP_200_OK + + def test_unkown_owner(self): + response = self._get( + "sunburst", + kwargs={ + "service": "gh", + "owner_username": "gh_owner", + "repo_name": "repo1", + "ext": "svg", + }, + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert ( + response.data["detail"] + == "Not found. Note: private repositories require ?token arguments" + ) + + def test_unkown_repo(self): + gh_owner = OwnerFactory(service="github") + response = self._get( + "sunburst", + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert ( + response.data["detail"] + == "Not found. Note: private repositories require ?token arguments" + ) + + def test_private_repo_no_token(self): + gh_owner = OwnerFactory(service="github") + RepositoryFactory( + author=gh_owner, + active=True, + private=True, + name="repo1", + image_token="12345678", + ) + response = self._get( + "sunburst", + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + data={"token": "123456"}, + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert ( + response.data["detail"] + == "Not found. Note: private repositories require ?token arguments" + ) + + def test_private_repo(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=True, + name="repo1", + image_token="12345678", + ) + CommitWithReportFactory(repository=repo, author=gh_owner) + + response = self._get( + "sunburst", + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + data={"token": "12345678"}, + ) + + expected_graph = """ + + + + + + + + + + + + + + + + + + """ + + graph = response.content.decode("utf-8") + graph = [line.strip() for line in graph.split("\n")] + expected_graph = [line.strip() for line in expected_graph.split("\n")] + assert expected_graph == graph + assert response.status_code == status.HTTP_200_OK + + def test_unkown_branch(self): + gh_owner = OwnerFactory(service="github") + RepositoryFactory(author=gh_owner, active=True, private=False, name="repo1") + + response = self._get( + "sunburst", + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + }, + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert ( + response.data["detail"] + == "Not found. Note: private repositories require ?token arguments" + ) + + def test_branch_graph(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=True, + name="repo1", + image_token="12345678", + branch="branch1", + ) + CommitWithReportFactory(repository=repo, author=gh_owner) + commit_2_totals = { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "95.00000", + "d": 0, + "diff": [1, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + "f": 3, + "h": 17, + "m": 3, + "n": 20, + "p": 0, + "s": 1, + } + commit_2 = CommitWithReportFactory( + repository=repo, author=gh_owner, totals=commit_2_totals + ) + BranchFactory(repository=repo, name="branch1", head=commit_2.commitid) + # test default precision + response = self._get_branch( + "tree", + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "branch": "branch1", + }, + data={"token": "12345678"}, + ) + expected_graph = """ + + + + + + + + + + + + tests/test_sample.py + tests/__init__.py + awesome/__init__.py + """ + graph = response.content.decode("utf-8") + graph = [line.strip() for line in graph.split("\n")] + expected_graph = [line.strip() for line in expected_graph.split("\n")] + assert expected_graph == graph + assert response.status_code == status.HTTP_200_OK + + def test_commit_graph(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=True, + name="repo1", + image_token="12345678", + ) + commit_1 = CommitWithReportFactory(repository=repo, author=gh_owner) + + # make sure commit 2 report is different than commit 1 and + # assert that the expected graph below still pertains to commit_1 + CommitFactory( + repository=repo, + author=gh_owner, + parent_commit_id=commit_1.commitid, + _report={ + "files": { + "different/test_file.py": [ + 2, + [0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0], + [[0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0]], + [0, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + ], + }, + "sessions": { + "0": { + "N": None, + "a": "v4/raw/2019-01-10/4434BC2A2EC4FCA57F77B473D83F928C/abf6d4df662c47e32460020ab14abf9303581429/9ccc55a1-8b41-4bb1-a946-ee7a33a7fb56.txt", + "c": None, + "d": 1547084427, + "e": None, + "f": ["unittests"], + "j": None, + "n": None, + "p": None, + "t": [3, 20, 17, 3, 0, "85.00000", 0, 0, 0, 0, 0, 0, 0], + "": None, + } + }, + }, + ) + + response = self._get_commit( + "tree", + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "commit": commit_1.commitid, + }, + data={"token": "12345678"}, + ) + expected_graph = """ + + + + + + + + + + + + tests/test_sample.py + tests/__init__.py + awesome/__init__.py + """ + graph = response.content.decode("utf-8") + graph = [line.strip() for line in graph.split("\n")] + expected_graph = [line.strip() for line in expected_graph.split("\n")] + assert expected_graph == graph + assert response.status_code == status.HTTP_200_OK + + def test_pull_graph(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=True, + name="repo1", + image_token="12345678", + branch="branch1", + ) + PullFactory( + pullid=10, + repository_id=repo.repoid, + _flare=[ + { + "name": "", + "color": "#e05d44", + "lines": 14, + "_class": None, + "children": [ + { + "name": "tests.py", + "color": "#baaf1b", + "lines": 7, + "_class": None, + "coverage": "85.71429", + } + ], + } + ], + ) + # test default precision + response = self._get_pull( + "tree", + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "pull": 10, + }, + data={"token": "12345678"}, + ) + expected_graph = """ + + + + + + + + + + + + tests.py + """ + graph = response.content.decode("utf-8") + graph = [line.strip() for line in graph.split("\n")] + expected_graph = [line.strip() for line in expected_graph.split("\n")] + assert expected_graph == graph + assert response.status_code == status.HTTP_200_OK + + def test_no_pull_graph(self): + gh_owner = OwnerFactory(service="github") + RepositoryFactory( + author=gh_owner, + active=True, + private=True, + name="repo1", + image_token="12345678", + branch="branch1", + ) + # test default precision + response = self._get_pull( + "tree", + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "pull": 10, + }, + data={"token": "12345678"}, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert ( + response.data["detail"] + == "Not found. Note: private repositories require ?token arguments" + ) + + def test_pull_no_flare_graph(self): + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=True, + name="repo1", + image_token="12345678", + branch="main", + ) + CommitWithReportFactory(repository=repo, author=gh_owner) + PullFactory(pullid=10, repository_id=repo.repoid, _flare=None) + + # test default precision + response = self._get_pull( + "tree", + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "pull": 10, + }, + data={"token": "12345678"}, + ) + expected_graph = """ + + + + + + + + + + + + tests/test_sample.py + tests/__init__.py + awesome/__init__.py + """ + graph = response.content.decode("utf-8") + graph = [line.strip() for line in graph.split("\n")] + expected_graph = [line.strip() for line in expected_graph.split("\n")] + assert response.status_code == status.HTTP_200_OK + assert expected_graph == graph + + def test_pull_no_repo_graph(self): + gh_owner = OwnerFactory(service="github") + + # test default precision + response = self._get_pull( + "tree", + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "pull": 10, + }, + data={"token": "12345678"}, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert ( + response.data["detail"] + == "Not found. Note: private repositories require ?token arguments" + ) + + @patch("shared.reports.api_report_service.build_report_from_commit") + def test_pull_file_not_found_in_storage(self, mocked_build_report): + mocked_build_report.return_value = None + gh_owner = OwnerFactory(service="github") + repo = RepositoryFactory( + author=gh_owner, + active=True, + private=True, + name="repo1", + image_token="12345678", + branch="main", + ) + CommitWithReportFactory(repository=repo, author=gh_owner) + PullFactory(pullid=10, repository_id=repo.repoid, _flare=None) + + response = self._get_pull( + "tree", + kwargs={ + "service": "gh", + "owner_username": gh_owner.username, + "repo_name": "repo1", + "ext": "svg", + "pull": 10, + }, + data={"token": "12345678"}, + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert ( + response.data["detail"] + == "Not found. Note: file for chunks not found in storage" + ) diff --git a/apps/codecov-api/graphs/tests/test_graph_utils.py b/apps/codecov-api/graphs/tests/test_graph_utils.py new file mode 100644 index 0000000000..1655930bde --- /dev/null +++ b/apps/codecov-api/graphs/tests/test_graph_utils.py @@ -0,0 +1,43 @@ +from graphs.helpers.graph_utils import _tree_height + + +class TestGraphsUtils(object): + def test_tree_height(self): + tree = [{"name": "name_0"}] + + height = _tree_height(tree) + assert height == 1 + + tree = [{"name": "name_0", "children": [{"name": "name_2"}]}] + + height = _tree_height(tree) + assert height == 2 + + tree = [ + { + "name": "name_0", + "children": [ + {"name": "name_1", "children": [{"name": "name_2"}]}, + {"name": "name_3"}, + ], + } + ] + height = _tree_height(tree) + assert height == 3 + + tree = [ + { + "name": "name_0", + "children": [ + { + "name": "name_1", + "children": [ + {"name": "name_2", "children": [{"name": "name_4"}]} + ], + }, + {"name": "name_3"}, + ], + } + ] + height = _tree_height(tree) + assert height == 4 diff --git a/apps/codecov-api/graphs/tests/test_helpers.py b/apps/codecov-api/graphs/tests/test_helpers.py new file mode 100644 index 0000000000..c7955af40a --- /dev/null +++ b/apps/codecov-api/graphs/tests/test_helpers.py @@ -0,0 +1,438 @@ +from graphs.helpers.badge import ( + format_bundle_bytes, + format_coverage_precision, + get_badge, + get_bundle_badge, +) + + +class TestGraphsHelpers(object): + def test_format_coverage_precision(self): + coverage = "91.1111" + precision = "1" + + expected_coverage = "91.1" + _coverage = format_coverage_precision(coverage, precision) + assert expected_coverage == _coverage + + precision = "0" + expected_coverage = "91" + _coverage = format_coverage_precision(coverage, precision) + assert expected_coverage == _coverage + + precision = "2" + expected_coverage = "91.11" + _coverage = format_coverage_precision(coverage, precision) + assert expected_coverage == _coverage + + def test_badge(self): + # Test medium badge + coverage = 91.1 + precision = 1 + coverage_range = [70, 100] + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + 91.1% + 91.1% + + + + + """ + + _badge = get_badge(coverage, coverage_range, precision) + _badge = [line.strip() for line in _badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == _badge + + # Test small badge + coverage = 80 + precision = 0 + coverage_range = [70, 100] + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + 80% + 80% + + + + + """ + _badge = get_badge(coverage, coverage_range, precision) + _badge = [line.strip() for line in _badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == _badge + + # Test large badge + + coverage = 60.52 + precision = 2 + coverage_range = [70, 100] + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + 60.52% + 60.52% + + + + + """ + + _badge = get_badge(coverage, coverage_range, precision) + _badge = [line.strip() for line in _badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == _badge + + def test_unknown_badge(self): + coverage = None + precision = 2 + coverage_range = [70, 100] + + expected_badge = """ + + + + + + + + + + + + + + codecov + codecov + unknown + unknown + + + + + + """ + + _badge = get_badge(coverage, coverage_range, precision) + _badge = [line.strip() for line in _badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == _badge + + def test_bundle_badge_small(self): + bundle_size_bytes = 7 + precision = 2 + + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + 7B + 7B + + + """ + + _badge = get_bundle_badge(bundle_size_bytes, precision) + _badge = [line.strip() for line in _badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == _badge + + def test_bundle_badge_medium(self): + bundle_size_bytes = 7777777 + precision = 2 + + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + 7.78MB + 7.78MB + + + """ + + _badge = get_bundle_badge(bundle_size_bytes, precision) + _badge = [line.strip() for line in _badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == _badge + + def test_bundle_badge_large(self): + bundle_size_bytes = 7777777777777 + precision = 2 + + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + 7777.78GB + 7777.78GB + + + """ + + _badge = get_bundle_badge(bundle_size_bytes, precision) + _badge = [line.strip() for line in _badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == _badge + + def test_bundle_badge_unknown(self): + bundle_size_bytes = None + precision = 2 + + expected_badge = """ + + + + + + + + + + + + + + bundle + bundle + unknown + unknown + + + """ + + _badge = get_bundle_badge(bundle_size_bytes, precision) + _badge = [line.strip() for line in _badge.split("\n")] + expected_badge = [line.strip() for line in expected_badge.split("\n")] + assert expected_badge == _badge + + def test_format_bundle_bytes_0_precision(self): + bundle_sizes = [ + 7, + 77, + 777, + 7777, + 77777, + 777777, + 7777777, + 77777777, + 777777777, + 7777777777, + 77777777777, + 777777777777, + 7777777777777, + ] + + expected = [ + "7B", + "77B", + "777B", + "8KB", + "78KB", + "778KB", + "8MB", + "78MB", + "778MB", + "8GB", + "78GB", + "778GB", + "7778GB", + ] + + for i in range(len(bundle_sizes)): + assert format_bundle_bytes(bundle_sizes[i], 0) == expected[i] + + def test_format_bundle_bytes_1_precision(self): + bundle_sizes = [ + 7, + 77, + 777, + 7777, + 77777, + 777777, + 7777777, + 77777777, + 777777777, + 7777777777, + 77777777777, + 777777777777, + 7777777777777, + ] + + expected = [ + "7B", + "77B", + "777B", + "7.8KB", + "77.8KB", + "777.8KB", + "7.8MB", + "77.8MB", + "777.8MB", + "7.8GB", + "77.8GB", + "777.8GB", + "7777.8GB", + ] + + for i in range(len(bundle_sizes)): + assert format_bundle_bytes(bundle_sizes[i], 1) == expected[i] + + def test_format_bundle_bytes_2_precision(self): + bundle_sizes = [ + 7, + 77, + 777, + 7777, + 77777, + 777777, + 7777777, + 77777777, + 777777777, + 7777777777, + 77777777777, + 777777777777, + 7777777777777, + ] + + expected = [ + "7B", + "77B", + "777B", + "7.78KB", + "77.78KB", + "777.78KB", + "7.78MB", + "77.78MB", + "777.78MB", + "7.78GB", + "77.78GB", + "777.78GB", + "7777.78GB", + ] + + for i in range(len(bundle_sizes)): + assert format_bundle_bytes(bundle_sizes[i], 2) == expected[i] + + def test_format_bundle_strips_zeros(self): + bundle_sizes = [ + 0, + 10, + 100, + 1000, + 10000, + 100000, + 1000000, + 10000000, + 100000000, + 1000000000, + 10000000000, + 100000000000, + 1000000000000, + 1100, + 11100, + 111100, + ] + + expected = [ + "0B", + "10B", + "100B", + "1KB", + "10KB", + "100KB", + "1MB", + "10MB", + "100MB", + "1GB", + "10GB", + "100GB", + "1000GB", + "1.1KB", + "11.1KB", + "111.1KB", + ] + + for i in range(len(bundle_sizes)): + assert format_bundle_bytes(bundle_sizes[i], 2) == expected[i] diff --git a/apps/codecov-api/graphs/urls.py b/apps/codecov-api/graphs/urls.py new file mode 100644 index 0000000000..cf069e9a42 --- /dev/null +++ b/apps/codecov-api/graphs/urls.py @@ -0,0 +1,46 @@ +from django.urls import re_path + +from .views import BadgeHandler, BundleBadgeHandler, GraphHandler + +urlpatterns = [ + re_path( + "branch/(?P.+)/(graph|graphs)/badge.(?P[^/]+)", + BadgeHandler.as_view(), + name="branch-badge", + ), + re_path( + "(graph|graphs)/badge.(?P[^/]+)", + BadgeHandler.as_view(), + name="default-badge", + ), + re_path( + "branch/(?P.+)/(graph|graphs)/bundle/(?P.+)/badge.(?P[^/]+)", + BundleBadgeHandler.as_view(), + name="branch-bundle-badge", + ), + re_path( + "(graph|graphs)/bundle/(?P.+)/badge.(?P[^/]+)", + BundleBadgeHandler.as_view(), + name="default-bundle-badge", + ), + re_path( + "pull/(?P[^/]+)/(graph|graphs)/(?Ptree|icicle|sunburst|commits).(?P[^/]+)", + GraphHandler.as_view(), + name="pull-graph", + ), + re_path( + "branch/(?P[^/].+)/(graph|graphs)/(?Ptree|icicle|sunburst|commits).(?P[^/]+)", + GraphHandler.as_view(), + name="branch-graph", + ), + re_path( + "commit/(?P[^/].+)/(graph|graphs)/(?Ptree|icicle|sunburst|commits).(?P[^/]+)", + GraphHandler.as_view(), + name="commit-graph", + ), + re_path( + "(graph|graphs)/(?Ptree|icicle|sunburst|commits).(?P[^/]+)", + GraphHandler.as_view(), + name="default-graph", + ), +] diff --git a/apps/codecov-api/graphs/views.py b/apps/codecov-api/graphs/views.py new file mode 100644 index 0000000000..68f0779aeb --- /dev/null +++ b/apps/codecov-api/graphs/views.py @@ -0,0 +1,444 @@ +import logging + +import shared.reports.api_report_service as report_service +from django.core.exceptions import ObjectDoesNotExist +from django.http import Http404 +from rest_framework import exceptions +from rest_framework.exceptions import NotFound +from rest_framework.negotiation import DefaultContentNegotiation +from rest_framework.permissions import AllowAny +from rest_framework.views import APIView +from shared.django_apps.core.models import Commit +from shared.metrics import Counter, inc_counter + +from api.shared.mixins import RepoPropertyMixin +from core.models import Branch, Pull +from graphs.settings import settings +from services.bundle_analysis import load_report +from services.components import commit_components + +from .helpers.badge import ( + format_bundle_bytes, + format_coverage_precision, + get_badge, + get_bundle_badge, +) +from .helpers.graphs import icicle, sunburst, tree +from .mixins import GraphBadgeAPIMixin + +log = logging.getLogger(__name__) + +FLARE_USE_COUNTER = Counter( + "graph_activity", + "How are graphs and flare being used?", + [ + "position", + ], +) +FLARE_SUCCESS_COUNTER = Counter( + "graph_success", + "How often are graphs successfully generated?", + [ + "graph_type", + ], +) + + +class IgnoreClientContentNegotiation(DefaultContentNegotiation): + def select_parser(self, request, parsers): + """ + Select the first parser in the `.parser_classes` list. + """ + return parsers[0] + + def select_renderer(self, request, renderers, format_suffix): + """ + Select the first renderer in the `.renderer_classes` list. + """ + try: + return super().select_renderer(request, renderers, format_suffix) + except exceptions.NotAcceptable: + log.info( + f"Recieved unsupported HTTP_ACCEPT header: {request.META.get('HTTP_ACCEPT')}" + ) + return (renderers[0], renderers[0].media_type) + + +class BadgeHandler(APIView, RepoPropertyMixin, GraphBadgeAPIMixin): + content_negotiation_class = IgnoreClientContentNegotiation + + permission_classes = [AllowAny] + + extensions = ["svg", "txt"] + precisions = ["0", "1", "2"] + filename = "badge" + + def get_object(self, request, *args, **kwargs): + # Validate coverage precision + precision = self.request.query_params.get("precision", "0") + if precision not in self.precisions: + raise NotFound("Coverage precision should be one of [ 0 || 1 || 2 ]") + + coverage, coverage_range = self.get_coverage() + + # Format coverage according to precision + coverage = format_coverage_precision(coverage, precision) + + if self.kwargs.get("ext") == "txt": + return coverage + + return get_badge(coverage, coverage_range, precision) + + def get_coverage(self): + """ + Note: This endpoint has the behavior of returning a gray badge with the word 'unknown' instead of returning a 404 + when the user enters an invalid service, owner, repo or when coverage is not found for a branch. + + We also need to support service abbreviations for users already using them + """ + coverage_range = [70, 100] + + try: + repo = self.repo + except Http404: + log.warning("Repo not found", extra=dict(repo=self.kwargs.get("repo_name"))) + return None, coverage_range + + if repo.private and repo.image_token != self.request.query_params.get("token"): + log.warning( + "Token provided does not match repo's image token", + extra=dict(repo=repo), + ) + return None, coverage_range + + branch_name = self.kwargs.get("branch") or repo.branch + branch = Branch.objects.filter( + name=branch_name, repository_id=repo.repoid + ).first() + + if branch is None: + log.warning( + "Branch not found", extra=dict(branch_name=branch_name, repo=repo) + ) + return None, coverage_range + try: + commit = repo.commits.filter(commitid=branch.head).first() + except ObjectDoesNotExist: + # if commit does not exist return None coverage + log.warning("Commit not found", extra=dict(commit=branch.head)) + return None, coverage_range + + if repo.yaml and repo.yaml.get("coverage", {}).get("range") is not None: + coverage_range = repo.yaml.get("coverage", {}).get("range") + + flag = self.request.query_params.get("flag") + if flag: + return self.flag_coverage(flag, commit), coverage_range + + component = self.request.query_params.get("component") + if component: + return self.component_coverage(component, commit), coverage_range + + coverage = ( + commit.totals.get("c") + if commit is not None and commit.totals is not None + else None + ) + + return coverage, coverage_range + + def flag_coverage(self, flag_name: str, commit: Commit): + """ + Looks into a commit's report sessions and returns the coverage for a particular flag name. + """ + if commit.full_report is None: + log.warning( + "Commit's report not found", + extra=dict(commit=commit.commitid, flag=flag_name), + ) + return None + flags = commit.full_report.flags + if flags is None: + return None + flag = flags.get(flag_name) + if flag: + return flag.totals.coverage + return None + + def component_coverage(self, component_identifier: str, commit: Commit): + """ + Looks into a commit's report sessions and returns the coverage for a particular component. + """ + report = commit.full_report + if report is None: + log.warning( + "Commit's report not found", + extra=dict(commit=commit.commitid, component=component_identifier), + ) + return None + components = commit_components(commit, None) + + try: + component = next( + c + for c in components + if c.component_id == component_identifier + or c.name == component_identifier + ) + except StopIteration: + # Component not found + return None + + # Gets the flags present in commit's report and reduces to only those + # that match at least one of the component's flag regexes. + component_flags = component.get_matching_flags(report.get_flag_names()) + + # Filters the commit report on the component's flags and paths. + filtered_report = report.filter(flags=component_flags, paths=component.paths) + + return filtered_report.totals.coverage + + +class BundleBadgeHandler(APIView, RepoPropertyMixin, GraphBadgeAPIMixin): + content_negotiation_class = IgnoreClientContentNegotiation + + permission_classes = [AllowAny] + + extensions = ["svg", "txt"] + precisions = ["0", "1", "2"] + filename = "bundle-badge" + + def get_object(self, request, *args, **kwargs): + # Validate precision query param + precision = self.request.query_params.get("precision", "2") + precision = int(precision) if precision in self.precisions else 2 + + bundle_size_bytes = self.get_bundle_size() + + if self.kwargs.get("ext") == "txt": + return ( + "unknown" + if bundle_size_bytes is None + else format_bundle_bytes(bundle_size_bytes, precision) + ) + + return get_bundle_badge(bundle_size_bytes, precision) + + def get_bundle_size(self) -> int | None: + try: + repo = self.repo + except Http404: + log.warning("Repo not found", extra=dict(repo=self.kwargs.get("repo_name"))) + return None + + if repo.private and repo.image_token != self.request.query_params.get("token"): + log.warning( + "Token provided does not match repo's image token", + extra=dict(repo=repo), + ) + return None + + branch_name = self.kwargs.get("branch") or repo.branch + branch = Branch.objects.filter( + name=branch_name, repository_id=repo.repoid + ).first() + + if branch is None: + log.warning( + "Branch not found", extra=dict(branch_name=branch_name, repo=repo) + ) + return None + + commit: Commit = repo.commits.filter(commitid=branch.head).first() + if commit is None: + log.warning("Commit not found", extra=dict(commit=branch.head)) + return None + + commit_bundles = load_report(commit) + + if commit_bundles is None: + log.warning( + "Bundle analysis report not found for commit", + extra=dict(commit=branch.head), + ) + return None + + bundle_name = str(self.kwargs.get("bundle")) + bundle = commit_bundles.bundle_report(bundle_name) + + if bundle is None: + log.warning( + "Bundle with provided name not found for commit", + extra=dict(commit=branch.head), + ) + return None + + return bundle.total_size() + + +class GraphHandler(APIView, RepoPropertyMixin, GraphBadgeAPIMixin): + permission_classes = [AllowAny] + + extensions = ["svg"] + filename = "graph" + + def get_object(self, request, *args, **kwargs): + options = dict() + graph = self.kwargs.get("graph") + + # a flare graph has been requested + inc_counter(FLARE_USE_COUNTER, labels=dict(position=0)) + log.info( + msg="flare graph activity", + extra=dict(position="start", graph_type=graph, kwargs=self.kwargs), + ) + + flare = self.get_flare() + # flare success, will generate and return graph + inc_counter(FLARE_USE_COUNTER, labels=dict(position=20)) + + if graph == "tree": + options["width"] = int( + self.request.query_params.get( + "width", settings["sunburst"]["options"]["width"] or 100 + ) + ) + options["height"] = int( + self.request.query_params.get( + "height", settings["sunburst"]["options"]["height"] or 100 + ) + ) + inc_counter(FLARE_SUCCESS_COUNTER, labels=dict(graph_type=graph)) + log.info( + msg="flare graph activity", + extra=dict(position="success", graph_type=graph, kwargs=self.kwargs), + ) + return tree(flare, None, None, **options) + elif graph == "icicle": + options["width"] = int( + self.request.query_params.get( + "width", settings["icicle"]["options"]["width"] or 100 + ) + ) + options["height"] = int( + self.request.query_params.get( + "height", settings["icicle"]["options"]["height"] or 100 + ) + ) + inc_counter(FLARE_SUCCESS_COUNTER, labels=dict(graph_type=graph)) + log.info( + msg="flare graph activity", + extra=dict(position="success", graph_type=graph, kwargs=self.kwargs), + ) + return icicle(flare, **options) + elif graph == "sunburst": + options["width"] = int( + self.request.query_params.get( + "width", settings["sunburst"]["options"]["width"] or 100 + ) + ) + options["height"] = int( + self.request.query_params.get( + "height", settings["sunburst"]["options"]["height"] or 100 + ) + ) + inc_counter(FLARE_SUCCESS_COUNTER, labels=dict(graph_type=graph)) + log.info( + msg="flare graph activity", + extra=dict(position="success", graph_type=graph, kwargs=self.kwargs), + ) + return sunburst(flare, **options) + + def get_flare(self): + pullid = self.kwargs.get("pullid") + + if not pullid: + # pullid not in kwargs, try to generate flare from commit + inc_counter(FLARE_USE_COUNTER, labels=dict(position=12)) + return self.get_commit_flare() + else: + # pullid was included in the request + inc_counter(FLARE_USE_COUNTER, labels=dict(position=1)) + pull_flare = self.get_pull_flare(pullid) + if pull_flare is None: + # failed to get flare from pull OR commit - graph request failed + inc_counter(FLARE_USE_COUNTER, labels=dict(position=15)) + raise NotFound( + "Not found. Note: private repositories require ?token arguments" + ) + return pull_flare + + def get_commit_flare(self): + commit = self.get_commit() + + if commit is None: + # could not find a commit - graph request failed + inc_counter(FLARE_USE_COUNTER, labels=dict(position=13)) + raise NotFound( + "Not found. Note: private repositories require ?token arguments" + ) + + # will attempt to build a report from a commit + inc_counter(FLARE_USE_COUNTER, labels=dict(position=10)) + report = report_service.build_report_from_commit(commit) + + if report is None: + # report generation failed + inc_counter(FLARE_USE_COUNTER, labels=dict(position=14)) + raise NotFound("Not found. Note: file for chunks not found in storage") + + # report successfully generated + inc_counter(FLARE_USE_COUNTER, labels=dict(position=11)) + return report.flare(None, [70, 100]) + + def get_pull_flare(self, pullid): + try: + repo = self.repo + # repo was included + inc_counter(FLARE_USE_COUNTER, labels=dict(position=2)) + except Http404: + return None + pull = Pull.objects.filter(pullid=pullid, repository_id=repo.repoid).first() + if pull is not None: + # pull found + inc_counter(FLARE_USE_COUNTER, labels=dict(position=3)) + if pull._flare is not None or pull._flare_storage_path is not None: + # pull has flare + inc_counter(FLARE_USE_COUNTER, labels=dict(position=4)) + return pull.flare + # pull not found or pull does not have flare, try to generate flare + inc_counter(FLARE_USE_COUNTER, labels=dict(position=5)) + return self.get_commit_flare() + + def get_commit(self): + try: + repo = self.repo + # repo included in request + inc_counter(FLARE_USE_COUNTER, labels=dict(position=6)) + except Http404: + return None + if repo.private and repo.image_token != self.request.query_params.get("token"): + # failed auth + inc_counter(FLARE_USE_COUNTER, labels=dict(position=7)) + return None + + commitid = self.kwargs.get("commit") + if commitid: + # commitid included on request + inc_counter(FLARE_USE_COUNTER, labels=dict(position=8)) + commit = repo.commits.filter(commitid=commitid).first() + else: + branch_name = self.kwargs.get("branch") or repo.branch + branch = Branch.objects.filter( + name=branch_name, repository_id=repo.repoid + ).first() + if branch is None: + # failed to get a commit + inc_counter(FLARE_USE_COUNTER, labels=dict(position=16)) + return None + + # found a commit by finding a branch + inc_counter(FLARE_USE_COUNTER, labels=dict(position=9)) + commit = repo.commits.filter(commitid=branch.head).first() + + return commit diff --git a/apps/codecov-api/gunicorn.conf.py b/apps/codecov-api/gunicorn.conf.py new file mode 100644 index 0000000000..fc81474cd9 --- /dev/null +++ b/apps/codecov-api/gunicorn.conf.py @@ -0,0 +1,48 @@ +import logging +import os + +from gunicorn.glogging import Logger +from prometheus_client import multiprocess + + +def child_exit(server, worker): + if worker and worker.pid and "PROMETHEUS_MULTIPROC_DIR" in os.environ: + multiprocess.mark_process_dead(worker.pid) + + +class CustomGunicornLogger(Logger): + """ + A custom class for logging gunicorn startup logs, these are for the logging that takes + place before the Django app starts and takes over with its own defined logging formats. + This class ensures the gunicorn minimum log level to be INFO instead of the default ERROR. + """ + + def setup(self, cfg): + super().setup(cfg) + custom_format = "[%(levelname)s] [%(process)d] [%(asctime)s] %(message)s " + date_format = "%Y-%m-%d %H:%M:%S %z" + formatter = logging.Formatter(fmt=custom_format, datefmt=date_format) + + # Update handlers with the custom formatter + for handler in self.error_log.handlers: + handler.setFormatter(formatter) + for handler in self.access_log.handlers: + handler.setFormatter(formatter) + + +logconfig_dict = { + "loggers": { + "gunicorn.error": { + "level": "INFO", + "handlers": ["console"], + "propagate": False, + }, + "gunicorn.access": { + "level": "INFO", + "handlers": ["console"], + "propagate": False, + }, + } +} + +logger_class = CustomGunicornLogger diff --git a/apps/codecov-api/labelanalysis/__init__.py b/apps/codecov-api/labelanalysis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/labelanalysis/admin.py b/apps/codecov-api/labelanalysis/admin.py new file mode 100644 index 0000000000..846f6b4061 --- /dev/null +++ b/apps/codecov-api/labelanalysis/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/apps/codecov-api/labelanalysis/apps.py b/apps/codecov-api/labelanalysis/apps.py new file mode 100644 index 0000000000..4620208fd1 --- /dev/null +++ b/apps/codecov-api/labelanalysis/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class LabelanalysisConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "labelanalysis" diff --git a/apps/codecov-api/labelanalysis/models.py b/apps/codecov-api/labelanalysis/models.py new file mode 100644 index 0000000000..8a0a37e452 --- /dev/null +++ b/apps/codecov-api/labelanalysis/models.py @@ -0,0 +1 @@ +from shared.django_apps.labelanalysis.models import * diff --git a/apps/codecov-api/labelanalysis/serializers.py b/apps/codecov-api/labelanalysis/serializers.py new file mode 100644 index 0000000000..a75237a1c5 --- /dev/null +++ b/apps/codecov-api/labelanalysis/serializers.py @@ -0,0 +1,89 @@ +from rest_framework import exceptions, serializers + +from core.models import Commit +from labelanalysis.models import ( + LabelAnalysisProcessingError, + LabelAnalysisRequest, + LabelAnalysisRequestState, +) + + +class CommitFromShaSerializerField(serializers.Field): + def __init__(self, *args, **kwargs): + self.accepts_fallback = kwargs.pop("accepts_fallback", False) + super().__init__(*args, **kwargs) + + def to_representation(self, commit): + return commit.commitid + + def to_internal_value(self, commit_sha): + commit = Commit.objects.filter( + repository__in=self.context["request"].auth.get_repositories(), + commitid=commit_sha, + ).first() + if commit is None: + raise exceptions.NotFound(f"Commit {commit_sha[:7]} not found.") + if commit.staticanalysissuite_set.exists(): + return commit + if not self.accepts_fallback: + raise serializers.ValidationError("No static analysis found") + attempted_commits = [] + for _ in range(10): + attempted_commits.append(commit.commitid) + commit = commit.parent_commit + if commit is None: + raise serializers.ValidationError( + f"No possible commits have static analysis sent. Attempted commits: {','.join(attempted_commits)}" + ) + if commit.staticanalysissuite_set.exists(): + return commit + raise serializers.ValidationError( + f"No possible commits have static analysis sent. Attempted too many commits: {','.join(attempted_commits)}" + ) + + +class LabelAnalysisProcessingErrorSerializer(serializers.ModelSerializer): + class Meta: + model = LabelAnalysisProcessingError + fields = ("error_code", "error_params") + read_only_fields = ("error_code", "error_params") + + +class ProcessingErrorList(serializers.ListField): + child = LabelAnalysisProcessingErrorSerializer() + + def to_representation(self, data): + data = data.select_related( + "label_analysis_request", + ).all() + return super().to_representation(data) + + +class LabelAnalysisRequestSerializer(serializers.ModelSerializer): + base_commit = CommitFromShaSerializerField(required=True, accepts_fallback=True) + head_commit = CommitFromShaSerializerField(required=True, accepts_fallback=False) + state = serializers.SerializerMethodField() + errors = ProcessingErrorList(required=False) + + def validate(self, data): + if data["base_commit"] == data["head_commit"]: + raise serializers.ValidationError( + {"base_commit": "Base and head must be different commits"} + ) + return data + + class Meta: + model = LabelAnalysisRequest + fields = ( + "base_commit", + "head_commit", + "requested_labels", + "result", + "state", + "external_id", + "errors", + ) + read_only_fields = ("result", "external_id", "errors") + + def get_state(self, obj): + return LabelAnalysisRequestState.enum_from_int(obj.state_id).name.lower() diff --git a/apps/codecov-api/labelanalysis/tests/__init__.py b/apps/codecov-api/labelanalysis/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/labelanalysis/tests/factories.py b/apps/codecov-api/labelanalysis/tests/factories.py new file mode 100644 index 0000000000..2a08cd0b4a --- /dev/null +++ b/apps/codecov-api/labelanalysis/tests/factories.py @@ -0,0 +1 @@ +from shared.django_apps.labelanalysis.tests.factories import * diff --git a/apps/codecov-api/labelanalysis/tests/integration/__init__.py b/apps/codecov-api/labelanalysis/tests/integration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/labelanalysis/tests/integration/test_views.py b/apps/codecov-api/labelanalysis/tests/integration/test_views.py new file mode 100644 index 0000000000..6f153abc79 --- /dev/null +++ b/apps/codecov-api/labelanalysis/tests/integration/test_views.py @@ -0,0 +1,457 @@ +from uuid import uuid4 + +from django.urls import reverse +from rest_framework.test import APIClient +from shared.celery_config import label_analysis_task_name +from shared.django_apps.core.tests.factories import ( + CommitFactory, + RepositoryFactory, + RepositoryTokenFactory, +) + +from labelanalysis.models import ( + LabelAnalysisProcessingError, + LabelAnalysisRequest, + LabelAnalysisRequestState, +) +from labelanalysis.tests.factories import LabelAnalysisRequestFactory +from services.task import TaskService +from staticanalysis.tests.factories import StaticAnalysisSuiteFactory + + +def test_simple_label_analysis_call_flow(db, mocker): + mocked_task_service = mocker.patch.object(TaskService, "schedule_task") + commit = CommitFactory.create(repository__active=True) + StaticAnalysisSuiteFactory.create(commit=commit) + base_commit = CommitFactory.create(repository=commit.repository) + StaticAnalysisSuiteFactory.create(commit=base_commit) + token = RepositoryTokenFactory.create( + repository=commit.repository, token_type="static_analysis" + ) + client = APIClient() + url = reverse("create_label_analysis") + client.credentials(HTTP_AUTHORIZATION="repotoken " + token.key) + payload = { + "base_commit": base_commit.commitid, + "head_commit": commit.commitid, + "requested_labels": None, + } + response = client.post( + url, + payload, + format="json", + ) + assert response.status_code == 201 + assert LabelAnalysisRequest.objects.filter(head_commit=commit).count() == 1 + produced_object = LabelAnalysisRequest.objects.get(head_commit=commit) + assert produced_object + assert produced_object.base_commit == base_commit + assert produced_object.head_commit == commit + assert produced_object.requested_labels is None + assert produced_object.state_id == LabelAnalysisRequestState.CREATED.db_id + assert produced_object.result is None + response_json = response.json() + expected_response_json = { + "base_commit": base_commit.commitid, + "head_commit": commit.commitid, + "requested_labels": None, + "result": None, + "state": "created", + "external_id": str(produced_object.external_id), + "errors": [], + } + assert response_json == expected_response_json + mocked_task_service.assert_called_with( + label_analysis_task_name, + kwargs={"request_id": produced_object.id}, + apply_async_kwargs={}, + ) + get_url = reverse( + "view_label_analysis", kwargs=dict(external_id=produced_object.external_id) + ) + response = client.get( + get_url, + format="json", + ) + assert response.status_code == 200 + assert response.json() == expected_response_json + + +def test_simple_label_analysis_call_flow_same_commit_error(db, mocker): + mocked_task_service = mocker.patch.object(TaskService, "schedule_task") + commit = CommitFactory.create(repository__active=True) + StaticAnalysisSuiteFactory.create(commit=commit) + token = RepositoryTokenFactory.create( + repository=commit.repository, token_type="static_analysis" + ) + client = APIClient() + url = reverse("create_label_analysis") + client.credentials(HTTP_AUTHORIZATION="repotoken " + token.key) + payload = { + "base_commit": commit.commitid, + "head_commit": commit.commitid, + "requested_labels": None, + } + response = client.post( + url, + payload, + format="json", + ) + assert response.status_code == 400 + assert LabelAnalysisRequest.objects.filter(head_commit=commit).count() == 0 + response_json = response.json() + expected_response_json = { + "base_commit": ["Base and head must be different commits"] + } + assert response_json == expected_response_json + assert not mocked_task_service.called + + +def test_simple_label_analysis_call_flow_with_fallback_on_base(db, mocker): + mocked_task_service = mocker.patch.object(TaskService, "schedule_task") + commit = CommitFactory.create(repository__active=True) + StaticAnalysisSuiteFactory.create(commit=commit) + base_commit_parent_parent = CommitFactory.create(repository=commit.repository) + base_commit_parent = CommitFactory.create( + parent_commit_id=base_commit_parent_parent.commitid, + repository=commit.repository, + ) + base_commit = CommitFactory.create( + parent_commit_id=base_commit_parent.commitid, repository=commit.repository + ) + StaticAnalysisSuiteFactory.create(commit=base_commit_parent_parent) + token = RepositoryTokenFactory.create( + repository=commit.repository, token_type="static_analysis" + ) + client = APIClient() + url = reverse("create_label_analysis") + client.credentials(HTTP_AUTHORIZATION="repotoken " + token.key) + payload = { + "base_commit": base_commit.commitid, + "head_commit": commit.commitid, + "requested_labels": None, + } + response = client.post( + url, + payload, + format="json", + ) + assert response.status_code == 201 + assert LabelAnalysisRequest.objects.filter(head_commit=commit).count() == 1 + produced_object = LabelAnalysisRequest.objects.get(head_commit=commit) + assert produced_object + assert produced_object.base_commit == base_commit_parent_parent + assert produced_object.head_commit == commit + assert produced_object.requested_labels is None + assert produced_object.state_id == LabelAnalysisRequestState.CREATED.db_id + assert produced_object.result is None + response_json = response.json() + expected_response_json = { + "base_commit": base_commit_parent_parent.commitid, + "head_commit": commit.commitid, + "requested_labels": None, + "result": None, + "state": "created", + "external_id": str(produced_object.external_id), + "errors": [], + } + assert response_json == expected_response_json + mocked_task_service.assert_called_with( + label_analysis_task_name, + kwargs={"request_id": produced_object.id}, + apply_async_kwargs={}, + ) + get_url = reverse( + "view_label_analysis", kwargs=dict(external_id=produced_object.external_id) + ) + response = client.get( + get_url, + format="json", + ) + assert response.status_code == 200 + assert response.json() == expected_response_json + + +def test_simple_label_analysis_call_flow_with_fallback_on_base_error_too_long( + db, mocker +): + mocked_task_service = mocker.patch.object(TaskService, "schedule_task") + repository = RepositoryFactory.create(active=True) + commit = CommitFactory.create(repository=repository) + StaticAnalysisSuiteFactory.create(commit=commit) + base_commit_root = CommitFactory.create(repository=repository) + StaticAnalysisSuiteFactory.create(commit=base_commit_root) + current = base_commit_root + attempted_commit_list = [base_commit_root.commitid] + for i in range(12): + current = CommitFactory.create( + parent_commit_id=current.commitid, repository=repository + ) + attempted_commit_list.append(current.commitid) + base_commit = current + token = RepositoryTokenFactory.create( + repository=commit.repository, token_type="static_analysis" + ) + client = APIClient() + url = reverse("create_label_analysis") + client.credentials(HTTP_AUTHORIZATION="repotoken " + token.key) + payload = { + "base_commit": base_commit.commitid, + "head_commit": commit.commitid, + "requested_labels": None, + } + response = client.post( + url, + payload, + format="json", + ) + assert response.status_code == 400 + assert LabelAnalysisRequest.objects.filter(head_commit=commit).count() == 0 + response_json = response.json() + # reverse and get 10 first elements, thats how far we look + attempted_commit_list = ",".join(list(reversed(attempted_commit_list))[:10]) + expected_response_json = { + "base_commit": [ + f"No possible commits have static analysis sent. Attempted too many commits: {attempted_commit_list}" + ] + } + assert response_json == expected_response_json + assert not mocked_task_service.called + + +def test_simple_label_analysis_call_flow_with_fallback_on_base_error(db, mocker): + mocked_task_service = mocker.patch.object(TaskService, "schedule_task") + commit = CommitFactory.create(repository__active=True) + StaticAnalysisSuiteFactory.create(commit=commit) + base_commit_parent_parent = CommitFactory.create(repository=commit.repository) + base_commit_parent = CommitFactory.create( + parent_commit_id=base_commit_parent_parent.commitid, + repository=commit.repository, + ) + base_commit = CommitFactory.create( + parent_commit_id=base_commit_parent.commitid, repository=commit.repository + ) + token = RepositoryTokenFactory.create( + repository=commit.repository, token_type="static_analysis" + ) + client = APIClient() + url = reverse("create_label_analysis") + client.credentials(HTTP_AUTHORIZATION="repotoken " + token.key) + payload = { + "base_commit": base_commit.commitid, + "head_commit": commit.commitid, + "requested_labels": None, + } + response = client.post( + url, + payload, + format="json", + ) + assert response.status_code == 400 + assert LabelAnalysisRequest.objects.filter(head_commit=commit).count() == 0 + response_json = response.json() + attempted_commit_list = ",".join( + [ + base_commit.commitid, + base_commit_parent.commitid, + base_commit_parent_parent.commitid, + ] + ) + expected_response_json = { + "base_commit": [ + f"No possible commits have static analysis sent. Attempted commits: {attempted_commit_list}" + ] + } + assert response_json == expected_response_json + assert not mocked_task_service.called + + +def test_simple_label_analysis_call_flow_with_fallback_on_head_error(db, mocker): + mocked_task_service = mocker.patch.object(TaskService, "schedule_task") + repository = RepositoryFactory.create(active=True) + head_commit_parent = CommitFactory.create(repository=repository) + head_commit = CommitFactory.create( + parent_commit_id=head_commit_parent.commitid, repository=repository + ) + base_commit = CommitFactory.create(repository=repository) + StaticAnalysisSuiteFactory.create(commit=base_commit) + StaticAnalysisSuiteFactory.create(commit=head_commit_parent) + token = RepositoryTokenFactory.create( + repository=head_commit.repository, token_type="static_analysis" + ) + client = APIClient() + url = reverse("create_label_analysis") + client.credentials(HTTP_AUTHORIZATION="repotoken " + token.key) + payload = { + "base_commit": base_commit.commitid, + "head_commit": head_commit.commitid, + "requested_labels": None, + } + response = client.post( + url, + payload, + format="json", + ) + assert response.status_code == 400 + assert LabelAnalysisRequest.objects.filter(head_commit=head_commit).count() == 0 + assert ( + LabelAnalysisRequest.objects.filter(head_commit=head_commit_parent).count() == 0 + ) + response_json = response.json() + expected_response_json = {"head_commit": ["No static analysis found"]} + assert response_json == expected_response_json + assert not mocked_task_service.called + + +def test_simple_label_analysis_only_get(db, mocker): + commit = CommitFactory.create(repository__active=True) + base_commit = CommitFactory.create(repository=commit.repository) + token = RepositoryTokenFactory.create( + repository=commit.repository, token_type="static_analysis" + ) + label_analysis = LabelAnalysisRequestFactory.create( + head_commit=commit, + base_commit=base_commit, + state_id=LabelAnalysisRequestState.FINISHED.db_id, + result={"some": ["result"]}, + ) + larq_processing_error = LabelAnalysisProcessingError( + label_analysis_request=label_analysis, + error_code="Missing FileSnapshot", + error_params={"message": "Something is wrong"}, + ) + label_analysis.save() + larq_processing_error.save() + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="repotoken " + token.key) + assert LabelAnalysisRequest.objects.filter(head_commit=commit).count() == 1 + produced_object = LabelAnalysisRequest.objects.get(head_commit=commit) + assert produced_object == label_analysis + expected_response_json = { + "base_commit": base_commit.commitid, + "head_commit": commit.commitid, + "requested_labels": None, + "result": {"some": ["result"]}, + "state": "finished", + "external_id": str(produced_object.external_id), + "errors": [ + { + "error_code": "Missing FileSnapshot", + "error_params": {"message": "Something is wrong"}, + } + ], + } + get_url = reverse( + "view_label_analysis", kwargs=dict(external_id=produced_object.external_id) + ) + response = client.get( + get_url, + format="json", + ) + assert response.status_code == 200 + assert response.json() == expected_response_json + + +def test_simple_label_analysis_get_does_not_exist(db, mocker): + token = RepositoryTokenFactory.create( + repository__active=True, token_type="static_analysis" + ) + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="repotoken " + token.key) + get_url = reverse("view_label_analysis", kwargs=dict(external_id=uuid4())) + response = client.get( + get_url, + format="json", + ) + assert response.status_code == 404 + assert response.json() == {"detail": "No such Label Analysis exists"} + + +def test_simple_label_analysis_put_labels(db, mocker): + mocked_task_service = mocker.patch.object(TaskService, "schedule_task") + commit = CommitFactory.create(repository__active=True) + StaticAnalysisSuiteFactory.create(commit=commit) + base_commit = CommitFactory.create(repository=commit.repository) + StaticAnalysisSuiteFactory.create(commit=base_commit) + token = RepositoryTokenFactory.create( + repository=commit.repository, token_type="static_analysis" + ) + label_analysis = LabelAnalysisRequestFactory.create( + head_commit=commit, + base_commit=base_commit, + state_id=LabelAnalysisRequestState.CREATED.db_id, + result=None, + ) + label_analysis.save() + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="repotoken " + token.key) + assert LabelAnalysisRequest.objects.filter(head_commit=commit).count() == 1 + produced_object = LabelAnalysisRequest.objects.get(head_commit=commit) + assert produced_object == label_analysis + assert produced_object.requested_labels is None + expected_response_json = { + "base_commit": base_commit.commitid, + "head_commit": commit.commitid, + "requested_labels": ["label_1", "label_2", "label_3"], + "result": None, + "state": "created", + "external_id": str(produced_object.external_id), + "errors": [], + } + patch_url = reverse( + "view_label_analysis", kwargs=dict(external_id=produced_object.external_id) + ) + response = client.patch( + patch_url, + format="json", + data={ + "requested_labels": ["label_1", "label_2", "label_3"], + "base_commit": base_commit.commitid, + "head_commit": commit.commitid, + }, + ) + assert response.status_code == 200 + assert response.json() == expected_response_json + mocked_task_service.assert_called_with( + label_analysis_task_name, + kwargs=dict(request_id=label_analysis.id), + apply_async_kwargs=dict(), + ) + + +def test_simple_label_analysis_put_labels_wrong_base_return_404(db, mocker): + mocked_task_service = mocker.patch.object(TaskService, "schedule_task") + commit = CommitFactory.create(repository__active=True) + StaticAnalysisSuiteFactory.create(commit=commit) + base_commit = CommitFactory.create(repository=commit.repository) + StaticAnalysisSuiteFactory.create(commit=base_commit) + token = RepositoryTokenFactory.create( + repository=commit.repository, token_type="static_analysis" + ) + label_analysis = LabelAnalysisRequestFactory.create( + head_commit=commit, + base_commit=base_commit, + state_id=LabelAnalysisRequestState.CREATED.db_id, + result=None, + ) + label_analysis.save() + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="repotoken " + token.key) + assert LabelAnalysisRequest.objects.filter(head_commit=commit).count() == 1 + produced_object = LabelAnalysisRequest.objects.get(head_commit=commit) + assert produced_object == label_analysis + assert produced_object.requested_labels is None + patch_url = reverse( + "view_label_analysis", kwargs=dict(external_id=produced_object.external_id) + ) + response = client.patch( + patch_url, + format="json", + data={ + "requested_labels": ["label_1", "label_2", "label_3"], + "base_commit": "not_base_commit", + "head_commit": commit.commitid, + }, + ) + assert response.status_code == 404 + mocked_task_service.assert_not_called() diff --git a/apps/codecov-api/labelanalysis/urls.py b/apps/codecov-api/labelanalysis/urls.py new file mode 100644 index 0000000000..2ebda9b54e --- /dev/null +++ b/apps/codecov-api/labelanalysis/urls.py @@ -0,0 +1,19 @@ +from django.urls import path + +from labelanalysis.views import ( + LabelAnalysisRequestCreateView, + LabelAnalysisRequestDetailView, +) + +urlpatterns = [ + path( + "labels-analysis", + LabelAnalysisRequestCreateView.as_view(), + name="create_label_analysis", + ), + path( + "labels-analysis/", + LabelAnalysisRequestDetailView.as_view(), + name="view_label_analysis", + ), +] diff --git a/apps/codecov-api/labelanalysis/views.py b/apps/codecov-api/labelanalysis/views.py new file mode 100644 index 0000000000..222ea6be3a --- /dev/null +++ b/apps/codecov-api/labelanalysis/views.py @@ -0,0 +1,58 @@ +from rest_framework.exceptions import NotFound +from rest_framework.generics import CreateAPIView, RetrieveAPIView, UpdateAPIView +from shared.celery_config import label_analysis_task_name + +from codecov_auth.authentication.repo_auth import RepositoryTokenAuthentication +from codecov_auth.permissions import SpecificScopePermission +from labelanalysis.models import LabelAnalysisRequest, LabelAnalysisRequestState +from labelanalysis.serializers import LabelAnalysisRequestSerializer +from services.task import TaskService + + +class LabelAnalysisRequestCreateView(CreateAPIView): + serializer_class = LabelAnalysisRequestSerializer + authentication_classes = [RepositoryTokenAuthentication] + permission_classes = [SpecificScopePermission] + # TODO Consider using a different permission scope + required_scopes = ["static_analysis"] + + def perform_create(self, serializer): + instance = serializer.save(state_id=LabelAnalysisRequestState.CREATED.db_id) + TaskService().schedule_task( + label_analysis_task_name, + kwargs=dict(request_id=instance.id), + apply_async_kwargs=dict(), + ) + return instance + + +class LabelAnalysisRequestDetailView(RetrieveAPIView, UpdateAPIView): + serializer_class = LabelAnalysisRequestSerializer + authentication_classes = [RepositoryTokenAuthentication] + permission_classes = [SpecificScopePermission] + # TODO Consider using a different permission scope + required_scopes = ["static_analysis"] + + def patch(self, request, *args, **kwargs): + # This is called by the CLI to patch the request_labels information after it's collected + # First we let rest_framework validate and update the larq object + response = super().patch(request, *args, **kwargs) + if response.status_code == 200: + # IF the larq update was successful + # we trigger the task again for the same larq to update the result saved + # The result saved is what we use to get metrics + uid = self.kwargs.get("external_id") + larq = LabelAnalysisRequest.objects.get(external_id=uid) + TaskService().schedule_task( + label_analysis_task_name, + kwargs=dict(request_id=larq.id), + apply_async_kwargs=dict(), + ) + return response + + def get_object(self): + uid = self.kwargs.get("external_id") + try: + return LabelAnalysisRequest.objects.get(external_id=uid) + except LabelAnalysisRequest.DoesNotExist: + raise NotFound("No such Label Analysis exists") diff --git a/apps/codecov-api/legacy_migrations/__init__.py b/apps/codecov-api/legacy_migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/legacy_migrations/apps.py b/apps/codecov-api/legacy_migrations/apps.py new file mode 100644 index 0000000000..3516f440e8 --- /dev/null +++ b/apps/codecov-api/legacy_migrations/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class LegacyMigrationsConfig(AppConfig): + name = "legacy_migrations" diff --git a/apps/codecov-api/legacy_migrations/management/commands/__init__.py b/apps/codecov-api/legacy_migrations/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/legacy_migrations/management/commands/migrate.py b/apps/codecov-api/legacy_migrations/management/commands/migrate.py new file mode 100644 index 0000000000..c0d6512820 --- /dev/null +++ b/apps/codecov-api/legacy_migrations/management/commands/migrate.py @@ -0,0 +1,2 @@ +# We inherit the migrate command from Shared +from shared.django_apps.legacy_migrations.management.commands.migrate import * diff --git a/apps/codecov-api/legacy_migrations/models.py b/apps/codecov-api/legacy_migrations/models.py new file mode 100644 index 0000000000..bb6c070473 --- /dev/null +++ b/apps/codecov-api/legacy_migrations/models.py @@ -0,0 +1 @@ +from shared.django_apps.legacy_migrations.models import * diff --git a/apps/codecov-api/legacy_migrations/tests/factories.py b/apps/codecov-api/legacy_migrations/tests/factories.py new file mode 100644 index 0000000000..93d4cf7e52 --- /dev/null +++ b/apps/codecov-api/legacy_migrations/tests/factories.py @@ -0,0 +1,15 @@ +import factory +from django.utils import timezone +from factory.django import DjangoModelFactory + +from legacy_migrations.models import YamlHistory + + +class YamlHistoryFactory(DjangoModelFactory): + class Meta: + model = YamlHistory + + timestamp = factory.LazyFunction(timezone.now) + message = "some_message" + source = "some source" + diff = "some diff between things" diff --git a/apps/codecov-api/legacy_migrations/tests/unit/test_models.py b/apps/codecov-api/legacy_migrations/tests/unit/test_models.py new file mode 100644 index 0000000000..45db86639d --- /dev/null +++ b/apps/codecov-api/legacy_migrations/tests/unit/test_models.py @@ -0,0 +1,18 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory + +from legacy_migrations.models import YamlHistory +from legacy_migrations.tests.factories import YamlHistoryFactory + + +class TestYamlHistory(TestCase): + def test_get_pieces_of_model(self): + owner = OwnerFactory() + author = OwnerFactory() + yaml = YamlHistoryFactory(author=author, ownerid=owner, message="some_message") + + assert yaml.ownerid == owner + assert yaml.author == author + assert yaml.message == "some_message" + + assert YamlHistory.objects.count() == 1 diff --git a/apps/codecov-api/manage.py b/apps/codecov-api/manage.py new file mode 100755 index 0000000000..c9a00e4c99 --- /dev/null +++ b/apps/codecov-api/manage.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +import os +import sys + +from utils.config import get_settings_module + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", get_settings_module()) + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) diff --git a/apps/codecov-api/migrate.sh b/apps/codecov-api/migrate.sh new file mode 100644 index 0000000000..151154f133 --- /dev/null +++ b/apps/codecov-api/migrate.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +# Command ran by k8s to run migrations for api +echo "Running Django migrations" +prefix="" +if [ -f "/usr/local/bin/berglas" ]; then + prefix="berglas exec --" +fi + +$prefix python manage.py migrate +$prefix python manage.py pgpartition --yes --skip-delete \ No newline at end of file diff --git a/apps/codecov-api/mypy.ini b/apps/codecov-api/mypy.ini new file mode 100644 index 0000000000..1494f91bad --- /dev/null +++ b/apps/codecov-api/mypy.ini @@ -0,0 +1,11 @@ +# Global options: + +[mypy] +disallow_untyped_defs = True +ignore_missing_imports = True +disable_error_code = attr-defined,import-untyped,name-defined +follow_imports = silent +warn_no_return = False + +[mypy-*.tests.*] +disallow_untyped_defs = False \ No newline at end of file diff --git a/apps/codecov-api/prod.sh b/apps/codecov-api/prod.sh new file mode 100755 index 0000000000..f87719d145 --- /dev/null +++ b/apps/codecov-api/prod.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Starts the production gunicorn server (no --reload) +echo "Starting gunicorn in production mode" +prefix="" +if [ -f "/usr/local/bin/berglas" ]; then + prefix="berglas exec --" +fi + +GUNICORN_WORKERS=${GUNICORN_WORKERS:-2} +if [ "$GUNICORN_WORKERS" -gt 1 ]; +then + export PROMETHEUS_MULTIPROC_DIR="${PROMETHEUS_MULTIPROC_DIR:-$HOME/.prometheus}" + rm -r ${PROMETHEUS_MULTIPROC_DIR?}/* 2> /dev/null + mkdir -p "$PROMETHEUS_MULTIPROC_DIR" +fi + +$prefix gunicorn codecov.wsgi:application --workers=${GUNICORN_WORKERS} --threads=${GUNICORN_THREADS:-1} --worker-connections=${GUNICORN_WORKER_CONNECTIONS:-1000} --bind 0.0.0.0:8000 --access-logfile '-' --statsd-host ${STATSD_HOST}:${STATSD_PORT} --timeout "${GUNICORN_TIMEOUT:-600}" --disable-redirect-access-to-syslog --max-requests=50000 --max-requests-jitter=300 diff --git a/apps/codecov-api/production.yml b/apps/codecov-api/production.yml new file mode 100644 index 0000000000..d5240709cd --- /dev/null +++ b/apps/codecov-api/production.yml @@ -0,0 +1,39 @@ +setup: + codecov_url: https://codecov.io + debug: no + loglvl: INFO + encryption_secret: "zp^P9*i8aR3" + media: + assets: https://codecov-cdn.storage.googleapis.com/4.4.4-fd6aa1e + dependancies: https://codecov-cdn.storage.googleapis.com/4.4.4-fd6aa1e + http: + force_https: yes + cookie_secret: Z1353^dggqdbc,kp0)661 + timeouts: + connect: 10 + receive: 15 + tasks: + celery: + soft_timelimit: 200 + hard_timelimit: 240 + upload: + queue: uploads + cache: + yaml: 600 # 10 minutes + tree: 600 # 10 minutes + diff: 300 # 5 minutes + chunks: 300 # 5 minutes + uploads: 86400 # 1 day + +services: + redis_url: redis://redis:@localhost:6379/ + database: + username: postgres + name: postgres + password: postgres + host: localhost + minio: + hash_key: testixik8qdauiab1yiffydimvi72ekq # never change this + access_key_id: codecov-default-key + secret_access_key: codecov-default-secret + verify_ssl: false diff --git a/apps/codecov-api/pyproject.toml b/apps/codecov-api/pyproject.toml new file mode 100644 index 0000000000..76fb273e6c --- /dev/null +++ b/apps/codecov-api/pyproject.toml @@ -0,0 +1,77 @@ +[project] +name = "codecov-api" +version = "0.0.1" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "aiodataloader==0.4.0", + "ariadne==0.23.0", + "ariadne-django==0.3.0", + "celery>=5.3.6", + "cerberus==1.3.5", + "certifi>=2024.7.4", + "django-autocomplete-light==3.11.0", + "django-better-admin-arrayfield==1.4.2", + "django-cors-headers==3.7.0", + "django-csp==3.8.0", + "django-cursor-pagination==0.3.0", + "django-filter==2.4.0", + "django-model-utils==4.5.1", + "django-postgres-extra>=2.0.8", + "django-prometheus==2.3.1", + "django>=4.2.16", + "djangorestframework==3.15.2", + "drf-spectacular==0.26.2", + "drf-spectacular-sidecar==2023.3.1", + "google-cloud-pubsub>=2.18.4", + "grpcio>=1.66.2", + "gunicorn>=22.0.0", + "idna>=3.7", + "minio==7.1.13", + "multidict>=6.1.0", + "polars==1.12.0", + "psycopg2-binary>=2.9.10", + "pydantic>=2.9.0", + "pyjwt>=2.4.0", + "python-dateutil==2.9.0.post0", + "python-json-logger==2.0.7", + "python-redis-lock==4.0.0", + "pytz==2022.1", + "redis==4.4.4", + "regex==2023.12.25", + "requests==2.32.3", + "sentry-sdk>=2.13.0", + "sentry-sdk[celery]==2.13.0", + "shared", + "simplejson==3.17.2", + "starlette==0.40.0", + "stripe>=11.4.1", + "whitenoise==5.2.0", +] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +py-modules = [] + +[tool.uv] +dev-dependencies = [ + "factory-boy>=3.2.0", + "fakeredis==2.10.3", + "freezegun>=1.5.1", + "pre-commit>=3.4.0", + "pytest>=8.1.1", + "pytest-cov>=6.0.0", + "pytest-asyncio>=0.14.0", + "pytest-django==4.8.0", + "pytest-insta>=0.3.0", + "pytest-mock==3.14.0", + "urllib3==1.26.19", + "vcrpy>=6.0.1", + "ruff>=0.9.6", +] + +[tool.uv.sources] +shared = { path = "../../libs/shared" } diff --git a/apps/codecov-api/pytest.ini b/apps/codecov-api/pytest.ini new file mode 100644 index 0000000000..ec326ff696 --- /dev/null +++ b/apps/codecov-api/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +DJANGO_SETTINGS_MODULE = codecov.settings_test +addopts = -p no:warnings --ignore=shared --ignore-glob=**/test_results* diff --git a/apps/codecov-api/reports/__init__.py b/apps/codecov-api/reports/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/reports/admin.py b/apps/codecov-api/reports/admin.py new file mode 100644 index 0000000000..17b3e5bda8 --- /dev/null +++ b/apps/codecov-api/reports/admin.py @@ -0,0 +1,22 @@ +from django.contrib import admin + +from codecov.admin import AdminMixin +from reports.models import ReportSession + + +@admin.register(ReportSession) +class ReportSessionAdmin(AdminMixin, admin.ModelAdmin): + list_display = ("id", "external_id") + show_full_result_count = False + readonly_fields = ("external_id", "storage_path", "upload_type") + search_fields = ("external_id",) + fields = readonly_fields + + def has_delete_permission(self, request, obj=None): + return False + + def has_add_permission(self, _, obj=None): + return False + + def has_change_permission(self, _, obj=None): + return False diff --git a/apps/codecov-api/reports/apps.py b/apps/codecov-api/reports/apps.py new file mode 100644 index 0000000000..f5fbe30b47 --- /dev/null +++ b/apps/codecov-api/reports/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ReportsConfig(AppConfig): + name = "reports" diff --git a/apps/codecov-api/reports/managers.py b/apps/codecov-api/reports/managers.py new file mode 100644 index 0000000000..0db5e5097e --- /dev/null +++ b/apps/codecov-api/reports/managers.py @@ -0,0 +1,17 @@ +from django.db.models import Manager, Q, QuerySet + + +class CommitReportQuerySet(QuerySet): + def coverage_reports(self): + """ + Filters queryset such that only coverage reports are included. + """ + return self.filter(Q(report_type=None) | Q(report_type="coverage")) + + +class CommitReportManager(Manager): + def get_queryset(self): + return CommitReportQuerySet(self.model, using=self._db) + + def coverage_reports(self, *args, **kwargs): + return self.get_queryset().coverage_reports(*args, **kwargs) diff --git a/apps/codecov-api/reports/models.py b/apps/codecov-api/reports/models.py new file mode 100644 index 0000000000..c4588d2a97 --- /dev/null +++ b/apps/codecov-api/reports/models.py @@ -0,0 +1 @@ +from shared.django_apps.reports.models import * diff --git a/apps/codecov-api/reports/tests/__init__.py b/apps/codecov-api/reports/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/reports/tests/factories.py b/apps/codecov-api/reports/tests/factories.py new file mode 100644 index 0000000000..3c77e59d71 --- /dev/null +++ b/apps/codecov-api/reports/tests/factories.py @@ -0,0 +1,144 @@ +from datetime import date, datetime + +import factory +from factory.django import DjangoModelFactory +from shared.django_apps.core.tests.factories import CommitFactory, RepositoryFactory + +from graphql_api.types.enums import UploadErrorEnum +from reports import models +from reports.models import ReportResults, TestInstance + + +class CommitReportFactory(DjangoModelFactory): + class Meta: + model = models.CommitReport + + commit = factory.SubFactory(CommitFactory) + + +class UploadFactory(DjangoModelFactory): + class Meta: + model = models.ReportSession + + build_code = factory.Sequence(lambda n: f"{n}") + report = factory.SubFactory(CommitReportFactory) + state = "processed" + + +class RepositoryFlagFactory(DjangoModelFactory): + class Meta: + model = models.RepositoryFlag + + repository = factory.SubFactory(RepositoryFactory) + flag_name = factory.Faker("word") + + +class UploadFlagMembershipFactory(DjangoModelFactory): + class Meta: + model = models.UploadFlagMembership + + flag = factory.SubFactory(RepositoryFlagFactory) + report_session = factory.SubFactory(UploadFactory) + + +class ReportLevelTotalsFactory(DjangoModelFactory): + class Meta: + model = models.ReportLevelTotals + + report = factory.SubFactory(CommitReportFactory) + branches = factory.Faker("pyint") + coverage = factory.Faker("pydecimal", min_value=10, max_value=90, right_digits=2) + hits = factory.Faker("pyint") + lines = factory.Faker("pyint") + methods = factory.Faker("pyint") + misses = factory.Faker("pyint") + partials = factory.Faker("pyint") + files = factory.Faker("pyint") + + +class UploadLevelTotalsFactory(DjangoModelFactory): + class Meta: + model = models.UploadLevelTotals + + report_session = factory.SubFactory(UploadFactory) + + +class UploadErrorFactory(DjangoModelFactory): + class Meta: + model = models.UploadError + + report_session = factory.SubFactory(UploadFactory) + error_code = factory.Iterator( + [ + UploadErrorEnum.FILE_NOT_IN_STORAGE, + UploadErrorEnum.REPORT_EMPTY, + UploadErrorEnum.REPORT_EXPIRED, + ] + ) + + +class ReportResultsFactory(DjangoModelFactory): + class Meta: + model = ReportResults + + report = factory.SubFactory(CommitReportFactory) + state = factory.Iterator( + [ + ReportResults.ReportResultsStates.PENDING, + ReportResults.ReportResultsStates.COMPLETED, + ] + ) + + +class TestFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.Test + + id = factory.Sequence(lambda n: f"{n}") + name = factory.Sequence(lambda n: f"{n}") + repository = factory.SubFactory(RepositoryFactory) + computed_name = None + + +class TestInstanceFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.TestInstance + + test = factory.SubFactory(TestFactory) + duration_seconds = 1.0 + outcome = TestInstance.Outcome.FAILURE.value + failure_message = "Test failed" + branch = "master" + repoid = factory.SelfAttribute("test.repository.repoid") + commitid = "123456" + upload = factory.SubFactory(UploadFactory) + + +class DailyTestRollupFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.DailyTestRollup + + test = factory.SubFactory(TestFactory) + + repoid = factory.SelfAttribute("test.repository.repoid") + branch = "main" + + last_duration_seconds = 1.0 + avg_duration_seconds = 0.5 + pass_count = 1 + skip_count = 2 + fail_count = 3 + flaky_fail_count = 0 + + latest_run = datetime.now() + date = date.today() + + commits_where_fail = ["123", "456", "789"] + + +class TestFlagBridgeFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.TestFlagBridge + + test = factory.SubFactory(TestFactory) + flag = factory.SubFactory(RepositoryFlagFactory) diff --git a/apps/codecov-api/rollouts/__init__.py b/apps/codecov-api/rollouts/__init__.py new file mode 100644 index 0000000000..7f46f49555 --- /dev/null +++ b/apps/codecov-api/rollouts/__init__.py @@ -0,0 +1,13 @@ +from shared.rollouts import Feature + +from codecov_auth.models import Owner + + +def owner_slug(owner: Owner) -> str: + return f"{owner.service}/{owner.username}" + + +__all__ = ["Feature"] + +# By default, features have one variant: +# { "enabled": FeatureVariant(True, 1.0) } diff --git a/apps/codecov-api/ruff.toml b/apps/codecov-api/ruff.toml new file mode 100644 index 0000000000..872bae8c14 --- /dev/null +++ b/apps/codecov-api/ruff.toml @@ -0,0 +1,74 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.13 +target-version = "py313" + +[lint] +# https://docs.astral.sh/ruff/rules/ +select = [ + "ASYNC", # flake8-async - async checks + "C4", # flake8-comprehensions - list/set/dict/generator comprehensions + "E", # pycodestyle - error rules + "F", # pyflakes - general Python errors, undefined names + "I", # isort - import sorting + "PERF", # perflint - performance anti-pattern rules + "PLC", # pylint - convention rules + "PLE", # pylint - error rules + "T20", # flake8-print - print statements + "W", # pycodestyle - warning rules +] +ignore = ["F405", "F403", "E501", "E712", "C408"] + +# Allow fix for all enabled rules (when `--fix`) is provided. +# The preferred method (for now) w.r.t. fixable rules is to manually update the makefile +# with --fix and re-run 'make lint_local' +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" diff --git a/apps/codecov-api/services/__init__.py b/apps/codecov-api/services/__init__.py new file mode 100644 index 0000000000..9061d9ec29 --- /dev/null +++ b/apps/codecov-api/services/__init__.py @@ -0,0 +1,2 @@ +class ServiceException(Exception): + pass diff --git a/apps/codecov-api/services/activation.py b/apps/codecov-api/services/activation.py new file mode 100644 index 0000000000..9c229c7625 --- /dev/null +++ b/apps/codecov-api/services/activation.py @@ -0,0 +1,96 @@ +import abc +import logging + +from django.conf import settings + +import services.self_hosted as self_hosted +from codecov_auth.models import Owner + +log = logging.getLogger(__name__) + + +class BaseActivator(abc.ABC): + def __init__(self, org: Owner, owner: Owner): + self.org = org + self.owner = owner + + @abc.abstractmethod + def is_autoactivation_enabled(self) -> bool: + pass + + @abc.abstractmethod + def can_activate_user(self) -> bool: + pass + + @abc.abstractmethod + def activate_user(self): + pass + + @abc.abstractmethod + def is_activated(self) -> bool: + pass + + +class CloudActivator(BaseActivator): + def is_autoactivation_enabled(self) -> bool: + return self.org.plan_auto_activate + + def can_activate_user(self) -> bool: + return self.org.can_activate_user(self.owner) + + def activate_user(self): + self.org.activate_user(self.owner) + + def is_activated(self) -> bool: + if not self.org.plan_activated_users: + return False + + return self.owner.pk in self.org.plan_activated_users + + +class SelfHostedActivator(BaseActivator): + def is_autoactivation_enabled(self) -> bool: + return self_hosted.is_autoactivation_enabled() + + def can_activate_user(self) -> bool: + return self_hosted.can_activate_owner(self.owner) + + def activate_user(self): + return self_hosted.activate_owner(self.owner) + + def is_activated(self) -> bool: + return self_hosted.is_activated_owner(self.owner) + + +def _get_activator(org: Owner, owner: Owner) -> BaseActivator: + if settings.IS_ENTERPRISE: + return SelfHostedActivator(org, owner) + else: + return CloudActivator(org, owner) + + +def try_auto_activate(org: Owner, owner: Owner) -> bool: + """ + Returns true if the user was able to be activated, false otherwise. + """ + activator = _get_activator(org, owner) + + if activator.is_autoactivation_enabled(): + log.info( + "Attemping to auto-activate user", + extra=dict(owner_id=owner.ownerid, org_id=org.ownerid), + ) + if activator.can_activate_user(): + activator.activate_user() + return True + else: + log.info( + "Auto-activation failed -- not enough seats remaining", + extra=dict(owner_id=owner.ownerid, org_id=org.ownerid), + ) + return False + + +def is_activated(org: Owner, owner: Owner) -> bool: + activator = _get_activator(org, owner) + return activator.is_activated() diff --git a/apps/codecov-api/services/analytics.py b/apps/codecov-api/services/analytics.py new file mode 100644 index 0000000000..0039c28957 --- /dev/null +++ b/apps/codecov-api/services/analytics.py @@ -0,0 +1,210 @@ +import logging +import re + +from django.conf import settings +from shared.analytics_tracking import analytics_manager +from shared.analytics_tracking.events import Events + +log = logging.getLogger(__name__) + + +def inject_analytics_owner(method): + """ + Decorator: promotes type of 'owner' arg to 'AnalyticsOwner'. + """ + + def exec_method(self, owner, **kwargs): + analytics_owner = AnalyticsOwner(owner, cookies=kwargs.get("cookies", {})) + return method(self, analytics_owner, **kwargs) + + return exec_method + + +def inject_analytics_repository(method): + """ + Decorator: promotes type of second parameter (a repository) to AnalyticsRepository + """ + + def exec_method(self, ownerid, repository): + analytics_repository = AnalyticsRepository(repository) + return method(self, ownerid, analytics_repository) + + return exec_method + + +class AnalyticsOwner: + """ + An object wrapper around 'Owner' that provides "user_id", "traits", + and "context" properties. + """ + + def __init__(self, owner, cookies={}, owner_collection_type="users"): + self.owner = owner + self.cookies = cookies + self.owner_collection_type = owner_collection_type + + @property + def user_id(self): + return self.owner.ownerid + + @property + def traits(self): + return { + "service": self.owner.service, + "service_id": self.owner.service_id, + "plan": self.owner.plan, + "email": self.owner.email or "unknown@codecov.io", + "username": self.owner.username or "unknown", + "owner_id": self.owner.ownerid, + "user_id": self.owner.ownerid, + } + + @property + def context(self): + """ + Mostly copied from + https://github.com/codecov/codecov.io/blob/master/app/services/analytics_tracking.py#L107 + """ + context = {"externalIds": []} + + context["externalIds"].append( + { + "id": self.owner.service_id, + "type": f"{self.owner.service}_id", + "collection": self.owner_collection_type, + "encoding": "none", + } + ) + + if self.owner.stripe_customer_id: + context["externalIds"].append( + { + "id": self.owner.stripe_customer_id, + "type": "stripe_customer_id", + "collection": self.owner_collection_type, + "encoding": "none", + } + ) + + if self.cookies and self.owner_collection_type == "users": + marketo_cookie = self.cookies.get("_mkto_trk") + ga_cookie = self.cookies.get("_ga") + if marketo_cookie: + context["externalIds"].append( + { + "id": marketo_cookie, + "type": "marketo_cookie", + "collection": "users", + "encoding": "none", + } + ) + context["Marketo"] = {"marketo_cookie": marketo_cookie} + if ga_cookie: + # id is everything after the "GA.1." prefix + match = re.match(r"^.+\.(.+?\..+?)$", ga_cookie) + if match: + ga_client_id = match.group(1) + context["externalIds"].append( + { + "id": ga_client_id, + "type": "ga_client_id", + "collection": "users", + "encoding": "none", + } + ) + + return context + + +class AnalyticsRepository: + """ + Wrapper object around Repository to provide a similar "traits" field as in AnalyticsOwner above. + """ + + def __init__(self, repo): + self.repo = repo + + @property + def traits(self): + return { + "repoid": self.repo.repoid, + "ownerid": self.repo.author.ownerid, + "service_id": self.repo.service_id, + "name": self.repo.name, + "private": self.repo.private, + "branch": self.repo.branch, + "updatestamp": self.repo.updatestamp, + "language": self.repo.language, + "active": self.repo.active, + "deleted": self.repo.deleted, + "activated": self.repo.activated, + "bot": self.repo.bot, + "using_integration": self.repo.using_integration, + "hookid": self.repo.hookid, + "has_yaml": self.repo.yaml is not None, + } + + +class AnalyticsService: + """ + Various methods for emitting events related to user actions. + """ + + @inject_analytics_owner + def user_signed_up(self, analytics_owner, **kwargs): + analytics_manager.track_event( + Events.USER_SIGNED_UP.value, + is_enterprise=settings.IS_ENTERPRISE, + event_data=analytics_owner.traits, + ) + + @inject_analytics_owner + def user_signed_in(self, analytics_owner, **kwargs): + analytics_manager.track_event( + Events.USER_SIGNED_IN.value, + is_enterprise=settings.IS_ENTERPRISE, + event_data=analytics_owner.traits, + ) + + @inject_analytics_repository + def account_activated_repository(self, current_user_ownerid, analytics_repository): + event_data = { + **analytics_repository.traits, + "user_id": current_user_ownerid, + } + analytics_manager.track_event( + Events.ACCOUNT_ACTIVATED_REPOSITORY.value, + is_enterprise=settings.IS_ENTERPRISE, + event_data=event_data, + context={"groupId": analytics_repository.repo.author.ownerid}, + ) + + @inject_analytics_repository + def account_activated_repository_on_upload(self, org_ownerid, analytics_repository): + event_data = { + **analytics_repository.traits, + "user_id": org_ownerid, + } + analytics_manager.track_event( + Events.ACCOUNT_ACTIVATED_REPOSITORY_ON_UPLOAD.value, + is_enterprise=settings.IS_ENTERPRISE, + event_data=event_data, + context={"groupId": org_ownerid}, + ) + + def account_uploaded_coverage_report(self, org_ownerid, upload_details): + upload_details = {**upload_details, "user_id": org_ownerid} + analytics_manager.track_event( + Events.ACCOUNT_UPLOADED_COVERAGE_REPORT.value, + is_enterprise=settings.IS_ENTERPRISE, + event_data=upload_details, + context={"groupId": org_ownerid}, + ) + + def opt_in_email(self, user_id, data: dict): + data = {**data, "user_id": user_id} + analytics_manager.track_event( + Events.GDPR_OPT_IN.value, + is_enterprise=settings.IS_ENTERPRISE, + event_data=data, + ) diff --git a/apps/codecov-api/services/billing.py b/apps/codecov-api/services/billing.py new file mode 100644 index 0000000000..d945b2e85d --- /dev/null +++ b/apps/codecov-api/services/billing.py @@ -0,0 +1,1076 @@ +import logging +import re +from abc import ABC, abstractmethod +from datetime import datetime, timezone + +import stripe +from dateutil.relativedelta import relativedelta +from django.conf import settings +from shared.plan.constants import PlanBillingRate, TierName +from shared.plan.service import PlanService + +from billing.constants import REMOVED_INVOICE_STATUSES +from codecov_auth.models import Owner, Plan + +log = logging.getLogger(__name__) + +SCHEDULE_RELEASE_OFFSET = 10 + +if settings.STRIPE_API_KEY: + stripe.api_key = settings.STRIPE_API_KEY + stripe.api_version = "2024-12-18.acacia" + + +def _log_stripe_error(method): + def catch_and_raise(*args, **kwargs): + try: + return method(*args, **kwargs) + except stripe.StripeError as e: + log.warning(f"StripeError raised: {e.user_message}") + raise + + return catch_and_raise + + +class AbstractPaymentService(ABC): + @abstractmethod + def get_invoice(self, owner, invoice_id): + pass + + @abstractmethod + def list_filtered_invoices(self, owner, limit=10): + pass + + @abstractmethod + def delete_subscription(self, owner): + pass + + @abstractmethod + def modify_subscription(self, owner, plan): + pass + + @abstractmethod + def create_checkout_session(self, owner, plan): + pass + + @abstractmethod + def get_subscription(self, owner): + pass + + @abstractmethod + def update_payment_method(self, owner, payment_method): + pass + + @abstractmethod + def update_email_address(self, owner, email_address): + pass + + @abstractmethod + def update_billing_address(self, owner, name, billing_address): + pass + + @abstractmethod + def get_schedule(self, owner): + pass + + @abstractmethod + def apply_cancellation_discount(self, owner: Owner): + pass + + @abstractmethod + def create_setup_intent(self, owner): + pass + + +class StripeService(AbstractPaymentService): + def __init__(self, requesting_user): + if settings.STRIPE_API_KEY is None: + log.critical( + "Missing stripe API key configuration -- communication with stripe won't be possible." + ) + if not isinstance(requesting_user, Owner): + raise Exception( + "StripeService requires requesting_user to be Owner instance" + ) + + self.requesting_user = requesting_user + + def _get_checkout_session_and_subscription_metadata(self, owner: Owner): + return { + "service": owner.service, + "obo_organization": owner.ownerid, + "username": owner.username, + "obo_name": self.requesting_user.name, + "obo_email": self.requesting_user.email, + "obo": self.requesting_user.ownerid, + } + + @_log_stripe_error + def get_invoice(self, owner, invoice_id): + log.info( + f"Fetching invoice {invoice_id} from Stripe for ownerid {owner.ownerid}" + ) + try: + invoice = stripe.Invoice.retrieve(invoice_id) + except stripe.InvalidRequestError: + log.info(f"invoice {invoice_id} not found for owner {owner.ownerid}") + return None + if invoice["customer"] != owner.stripe_customer_id: + log.info( + f"customer id ({invoice['customer']}) on invoice does not match the owner customer id ({owner.stripe_customer_id})" + ) + return None + return invoice + + def filter_invoices_by_status(self, invoice): + if invoice["status"] and invoice["status"] not in REMOVED_INVOICE_STATUSES: + return invoice + + def filter_invoices_by_total(self, invoice): + if invoice["total"] and invoice["total"] != 0: + return invoice + + @_log_stripe_error + def list_filtered_invoices(self, owner: Owner, limit=10): + log.info(f"Fetching invoices from Stripe for ownerid {owner.ownerid}") + if owner.stripe_customer_id is None: + log.info("stripe_customer_id is None, not fetching invoices") + return [] + invoices = stripe.Invoice.list(customer=owner.stripe_customer_id, limit=limit)[ + "data" + ] + + invoices_filtered_by_status = filter(self.filter_invoices_by_status, invoices) + invoices_filtered_by_status_and_total = filter( + self.filter_invoices_by_total, invoices_filtered_by_status + ) + return list(invoices_filtered_by_status_and_total) + + def cancel_and_refund( + self, + owner, + current_subscription_datetime, + subscription_plan_interval, + autorefunds_remaining, + ): + # cancels a Stripe customer subscription immediately and attempts to refund their payments for the current period + stripe.Subscription.cancel(owner.stripe_subscription_id) + + start_of_last_period = current_subscription_datetime - relativedelta(months=1) + invoice_grace_period_start = current_subscription_datetime - relativedelta( + days=1 + ) + + if subscription_plan_interval == "year": + start_of_last_period = current_subscription_datetime - relativedelta( + years=1 + ) + invoice_grace_period_start = current_subscription_datetime - relativedelta( + days=3 + ) + + invoices_list = stripe.Invoice.list( + subscription=owner.stripe_subscription_id, + status="paid", + created={ + "gte": int(start_of_last_period.timestamp()), + "lt": int(current_subscription_datetime.timestamp()), + }, + ) + + # we only want to refund the invoices PAID recently for the latest, current period. "invoices_list" gives us any invoice + # created over the last month/year based on what period length they are on but the customer could have possibly + # switched from monthly to yearly recently. + recently_paid_invoices_list = [ + invoice + for invoice in invoices_list["data"] + if invoice["status_transitions"]["paid_at"] is not None + and invoice["status_transitions"]["paid_at"] + >= int(invoice_grace_period_start.timestamp()) + ] + + created_refund = False + # there could be multiple invoices that need to be refunded such as if the user increased seats within the grace period + for invoice in recently_paid_invoices_list: + # refund if the invoice has a charge and it has been fully paid + if invoice["charge"] is not None and invoice["amount_remaining"] == 0: + stripe.Refund.create(invoice["charge"]) + created_refund = True + + if created_refund: + # update the customer's balance back to 0 in accordance to + # https://support.stripe.com/questions/refunding-credit-balance-to-customer-after-subscription-downgrade-or-cancellation + stripe.Customer.modify( + owner.stripe_customer_id, + balance=0, + metadata={"autorefunds_remaining": str(autorefunds_remaining - 1)}, + ) + log.info( + "Grace period cancelled a subscription and autorefunded associated invoices", + extra=dict( + owner_id=owner.ownerid, + user_id=self.requesting_user.ownerid, + subscription_id=owner.stripe_subscription_id, + customer_id=owner.stripe_customer_id, + autorefunds_remaining=autorefunds_remaining - 1, + ), + ) + else: + log.info( + "Grace period cancelled a subscription but did not find any appropriate invoices to autorefund", + extra=dict( + owner_id=owner.ownerid, + user_id=self.requesting_user.ownerid, + subscription_id=owner.stripe_subscription_id, + customer_id=owner.stripe_customer_id, + autorefunds_remaining=autorefunds_remaining, + ), + ) + + @_log_stripe_error + def delete_subscription(self, owner: Owner): + subscription = stripe.Subscription.retrieve(owner.stripe_subscription_id) + subscription_schedule_id = subscription.schedule + + log.info( + f"Downgrade to basic plan from user plan for owner {owner.ownerid} by user #{self.requesting_user.ownerid}", + extra=dict(ownerid=owner.ownerid), + ) + + if subscription_schedule_id: + log.info( + f"Releasing subscription from schedule for owner {owner.ownerid} by user #{self.requesting_user.ownerid}", + extra=dict(ownerid=owner.ownerid), + ) + stripe.SubscriptionSchedule.release(subscription_schedule_id) + + # we give an auto-refund grace period of 24 hours for a monthly subscription or 72 hours for a yearly subscription + current_subscription_datetime = datetime.fromtimestamp( + subscription["current_period_start"], tz=timezone.utc + ) + difference_from_now = datetime.now(timezone.utc) - current_subscription_datetime + + subscription_plan_interval = getattr( + getattr(subscription, "plan", None), "interval", None + ) + within_refund_grace_period = ( + subscription_plan_interval == "month" and difference_from_now.days < 1 + ) or (subscription_plan_interval == "year" and difference_from_now.days < 3) + + if within_refund_grace_period: + customer = stripe.Customer.retrieve(owner.stripe_customer_id) + # we are currently allowing customers 2 autorefund instances + autorefunds_remaining = int( + customer["metadata"].get("autorefunds_remaining", "2") + ) + log.info( + "Deleting subscription with attempted immediate cancellation with autorefund within grace period", + extra=dict( + owner_id=owner.ownerid, + user_id=self.requesting_user.ownerid, + subscription_id=owner.stripe_subscription_id, + customer_id=owner.stripe_customer_id, + autorefunds_remaining=autorefunds_remaining, + ), + ) + if autorefunds_remaining > 0: + return self.cancel_and_refund( + owner, + current_subscription_datetime, + subscription_plan_interval, + autorefunds_remaining, + ) + + # schedule a cancellation at the end of the paid period with no refund + stripe.Subscription.modify( + owner.stripe_subscription_id, + cancel_at_period_end=True, + proration_behavior="none", + ) + + @_log_stripe_error + def get_subscription(self, owner: Owner): + if not owner.stripe_subscription_id: + return None + return stripe.Subscription.retrieve( + owner.stripe_subscription_id, + expand=[ + "latest_invoice", + "customer", + "customer.invoice_settings.default_payment_method", + "customer.tax_ids", + ], + ) + + @_log_stripe_error + def get_schedule(self, owner: Owner): + if not owner.stripe_subscription_id: + return None + + subscription = self.get_subscription(owner) + subscription_schedule_id = subscription.schedule + + if not subscription_schedule_id: + return None + + return stripe.SubscriptionSchedule.retrieve(subscription_schedule_id) + + @_log_stripe_error + def modify_subscription(self, owner: Owner, desired_plan: dict): + desired_plan_info = Plan.objects.filter(name=desired_plan["value"]).first() + if not desired_plan_info: + log.error( + f"Plan {desired_plan['value']} not found", + extra=dict(owner_id=owner.ownerid), + ) + return + + subscription = stripe.Subscription.retrieve(owner.stripe_subscription_id) + proration_behavior = self._get_proration_params( + owner, + desired_plan_info=desired_plan_info, + desired_quantity=desired_plan["quantity"], + ) + subscription_schedule_id = subscription.schedule + + # proration_behavior indicates whether we immediately invoice a user or not. We only immediately + # invoice a user if the user increases the number of seats or if the plan changes from monthly to yearly. + # An increase in seats and/or plan implies the user is upgrading, hence 'is_upgrading' is a consequence + # of proration_behavior providing an invoice, in this case, != "none" + # TODO: change this to "self._is_upgrading_seats(owner, desired_plan) or self._is_extending_term(owner, desired_plan)" + is_upgrading = ( + True + if proration_behavior != "none" and desired_plan_info.stripe_id + else False + ) + + # Divide logic bw immediate updates and scheduled updates + # Immediate updates: when user upgrades seats or plan + # If the user is not in a schedule, update immediately + # If the user is in a schedule, update the existing schedule + # Scheduled updates: when the user decreases seats or plan + # If the user is not in a schedule, create a schedule + # If the user is in a schedule, update the existing schedule + if is_upgrading: + if subscription_schedule_id: + log.info( + f"Releasing Stripe schedule for owner {owner.ownerid} to {desired_plan['value']} with {desired_plan['quantity']} seats by user #{self.requesting_user.ownerid}" + ) + stripe.SubscriptionSchedule.release(subscription_schedule_id) + log.info( + f"Updating Stripe subscription for owner {owner.ownerid} to {desired_plan['value']} by user #{self.requesting_user.ownerid}" + ) + + subscription = stripe.Subscription.modify( + owner.stripe_subscription_id, + cancel_at_period_end=False, + items=[ + { + "id": subscription["items"]["data"][0]["id"], + "plan": desired_plan_info.stripe_id, + "quantity": desired_plan["quantity"], + } + ], + metadata=self._get_checkout_session_and_subscription_metadata(owner), + proration_behavior=proration_behavior, + # TODO: we need to include this arg, but it means we need to remove some of the existing args + # on the .modify() call https://docs.stripe.com/billing/subscriptions/pending-updates-reference + # payment_behavior="pending_if_incomplete", + ) + log.info( + f"Stripe subscription upgrade attempted for owner {owner.ownerid} by user #{self.requesting_user.ownerid}" + ) + indication_of_payment_failure = getattr( + subscription, "pending_update", None + ) + if indication_of_payment_failure: + # payment failed, raise this to user by setting as delinquent + owner.delinquent = True + owner.save() + log.info( + f"Stripe subscription upgrade failed for owner {owner.ownerid} by user #{self.requesting_user.ownerid}", + extra=dict(pending_update=indication_of_payment_failure), + ) + else: + # payment successful + plan_service = PlanService(current_org=owner) + plan_service.update_plan( + name=desired_plan["value"], user_count=desired_plan["quantity"] + ) + log.info( + f"Stripe subscription upgraded successfully for owner {owner.ownerid} by user #{self.requesting_user.ownerid}" + ) + else: + if not subscription_schedule_id: + schedule = stripe.SubscriptionSchedule.create( + from_subscription=owner.stripe_subscription_id + ) + subscription_schedule_id = schedule.id + self._modify_subscription_schedule( + owner, subscription, subscription_schedule_id, desired_plan + ) + + def _modify_subscription_schedule( + self, + owner: Owner, + subscription: stripe.Subscription, + subscription_schedule_id: str, + desired_plan: dict, + ): + current_subscription_start_date = subscription["current_period_start"] + current_subscription_end_date = subscription["current_period_end"] + + subscription_item = subscription["items"]["data"][0] + current_plan = subscription_item["plan"]["id"] + current_quantity = subscription_item["quantity"] + + plan = Plan.objects.filter(name=desired_plan["value"]).first() + if not plan or not plan.stripe_id: + log.error( + f"Plan {desired_plan['value']} not found", + extra=dict(owner_id=owner.ownerid), + ) + return + + stripe.SubscriptionSchedule.modify( + subscription_schedule_id, + end_behavior="release", + phases=[ + { + "start_date": current_subscription_start_date, + "end_date": current_subscription_end_date, + "items": [ + { + "plan": current_plan, + "price": current_plan, + "quantity": current_quantity, + } + ], + "proration_behavior": "none", + }, + { + "start_date": current_subscription_end_date, + "end_date": current_subscription_end_date + SCHEDULE_RELEASE_OFFSET, + "items": [ + { + "plan": plan.stripe_id, + "price": plan.stripe_id, + "quantity": desired_plan["quantity"], + } + ], + "proration_behavior": "none", + }, + ], + metadata=self._get_checkout_session_and_subscription_metadata(owner), + ) + + def _is_upgrading_seats(self, owner: Owner, desired_quantity: int) -> bool: + """ + Returns `True` if purchasing more seats. + """ + return bool(owner.plan_user_count and owner.plan_user_count < desired_quantity) + + def _is_extending_term( + self, current_plan_info: Plan, desired_plan_info: Plan + ) -> bool: + """ + Returns `True` if switching from monthly to yearly plan. + """ + + return bool( + current_plan_info + and current_plan_info.billing_rate == PlanBillingRate.MONTHLY.value + and desired_plan_info + and desired_plan_info.billing_rate == PlanBillingRate.YEARLY.value + ) + + def _is_similar_plan( + self, + owner: Owner, + current_plan_info: Plan, + desired_plan_info: Plan, + desired_quantity: int, + ) -> bool: + """ + Returns `True` if switching to a plan with similar term and seats. + """ + is_same_term = ( + current_plan_info + and desired_plan_info + and current_plan_info.billing_rate == desired_plan_info.billing_rate + ) + + is_same_seats = ( + owner.plan_user_count and owner.plan_user_count == desired_quantity + ) + # If from PRO to TEAM, then not a similar plan + if ( + current_plan_info.tier.tier_name != TierName.TEAM.value + and desired_plan_info.tier.tier_name == TierName.TEAM.value + ): + return False + # If from TEAM to PRO, then considered a similar plan but really is an upgrade + elif ( + current_plan_info.tier.tier_name == TierName.TEAM.value + and desired_plan_info.tier.tier_name != TierName.TEAM.value + ): + return True + + return bool(is_same_term and is_same_seats) + + def _get_proration_params( + self, owner: Owner, desired_plan_info: Plan, desired_quantity: int + ) -> str: + current_plan_info = Plan.objects.select_related("tier").get(name=owner.plan) + if ( + self._is_upgrading_seats(owner=owner, desired_quantity=desired_quantity) + or self._is_extending_term( + current_plan_info=current_plan_info, desired_plan_info=desired_plan_info + ) + or self._is_similar_plan( + owner=owner, + current_plan_info=current_plan_info, + desired_plan_info=desired_plan_info, + desired_quantity=desired_quantity, + ) + ): + return "always_invoice" + else: + return "none" + + def _get_success_and_cancel_url(self, owner: Owner): + short_services = {"github": "gh", "bitbucket": "bb", "gitlab": "gl"} + base_path = f"/plan/{short_services[owner.service]}/{owner.username}" + success_url = f"{settings.CODECOV_DASHBOARD_URL}{base_path}?success" + cancel_url = f"{settings.CODECOV_DASHBOARD_URL}{base_path}?cancel" + return success_url, cancel_url + + @_log_stripe_error + def create_checkout_session(self, owner: Owner, desired_plan: dict): + success_url, cancel_url = self._get_success_and_cancel_url(owner) + log.info( + "Creating Stripe Checkout Session for owner", + extra=dict(owner_id=owner.ownerid), + ) + + plan = Plan.objects.filter(name=desired_plan["value"]).first() + if not plan or not plan.stripe_id: + log.error( + f"Plan {desired_plan['value']} not found", + extra=dict(owner_id=owner.ownerid), + ) + return + + session = stripe.checkout.Session.create( + payment_method_configuration=settings.STRIPE_PAYMENT_METHOD_CONFIGURATION_ID, + billing_address_collection="required", + payment_method_collection="if_required", + client_reference_id=str(owner.ownerid), + success_url=success_url, + cancel_url=cancel_url, + customer=owner.stripe_customer_id, + mode="subscription", + line_items=[ + { + "price": plan.stripe_id, + "quantity": desired_plan["quantity"], + } + ], + subscription_data={ + "metadata": self._get_checkout_session_and_subscription_metadata(owner), + }, + tax_id_collection={"enabled": True}, + customer_update=( + {"name": "auto", "address": "auto"} + if owner.stripe_customer_id + else None + ), + ) + log.info( + f"Stripe Checkout Session created successfully for owner {owner.ownerid} by user #{self.requesting_user.ownerid}" + ) + return session["id"] + + def _is_unverified_payment_method(self, payment_method_id: str) -> bool: + payment_method = stripe.PaymentMethod.retrieve(payment_method_id) + + is_us_bank_account = payment_method.type == "us_bank_account" and hasattr( + payment_method, "us_bank_account" + ) + if is_us_bank_account: + setup_intents = stripe.SetupIntent.list( + payment_method=payment_method_id, limit=1 + ) + + try: + latest_intent = setup_intents.data[0] + if ( + latest_intent.status == "requires_action" + and latest_intent.next_action + and latest_intent.next_action.type == "verify_with_microdeposits" + ): + return True + except Exception as e: + log.error( + "Error retrieving latest setup intent", + payment_method_id=payment_method_id, + extra=dict(error=e), + ) + return False + + return False + + @_log_stripe_error + def update_payment_method(self, owner: Owner, payment_method: str) -> None: + log.info( + "Stripe update payment method for owner", + extra=dict( + owner_id=owner.ownerid, + user_id=self.requesting_user.ownerid, + subscription_id=owner.stripe_subscription_id, + customer_id=owner.stripe_customer_id, + ), + ) + if owner.stripe_subscription_id is None or owner.stripe_customer_id is None: + log.warn( + "Missing subscription or customer id, returning early", + extra=dict( + owner_id=owner.ownerid, + subscription_id=owner.stripe_subscription_id, + customer_id=owner.stripe_customer_id, + ), + ) + return None + + # do not set as default if the new payment method is unverified (e.g., awaiting microdeposits) + should_set_as_default = not self._is_unverified_payment_method(payment_method) + + if should_set_as_default: + stripe.PaymentMethod.attach( + payment_method, customer=owner.stripe_customer_id + ) + stripe.Customer.modify( + owner.stripe_customer_id, + invoice_settings={"default_payment_method": payment_method}, + ) + stripe.Subscription.modify( + owner.stripe_subscription_id, default_payment_method=payment_method + ) + log.info( + f"Successfully updated payment method for owner {owner.ownerid} by user #{self.requesting_user.ownerid}", + extra=dict( + owner_id=owner.ownerid, + user_id=self.requesting_user.ownerid, + subscription_id=owner.stripe_subscription_id, + customer_id=owner.stripe_customer_id, + ), + ) + + @_log_stripe_error + def update_email_address( + self, + owner: Owner, + email_address: str, + apply_to_default_payment_method: bool = False, + ): + if not re.fullmatch(r"[^@]+@[^@]+\.[^@]+", email_address): + return None + + log.info(f"Stripe update email address for owner {owner.ownerid}") + if owner.stripe_subscription_id is None: + log.info( + f"stripe_subscription_id is None, not updating stripe email for owner {owner.ownerid}" + ) + return None + stripe.Customer.modify(owner.stripe_customer_id, email=email_address) + log.info( + f"Stripe successfully updated email address for owner {owner.ownerid} by user #{self.requesting_user.ownerid}" + ) + + if apply_to_default_payment_method: + try: + default_payment_method = stripe.Customer.retrieve( + owner.stripe_customer_id + )["invoice_settings"]["default_payment_method"] + + stripe.PaymentMethod.modify( + default_payment_method, + billing_details={"email": email_address}, + ) + log.info( + "Stripe successfully updated billing email for payment method", + extra=dict( + payment_method=default_payment_method, + stripe_customer_id=owner.stripe_customer_id, + ownerid=owner.ownerid, + ), + ) + except Exception as e: + log.error( + "Unable to update billing email for payment method", + extra=dict( + payment_method=default_payment_method, + stripe_customer_id=owner.stripe_customer_id, + error=str(e), + ownerid=owner.ownerid, + ), + ) + + @_log_stripe_error + def update_billing_address(self, owner: Owner, name, billing_address): + log.info(f"Stripe update billing address for owner {owner.ownerid}") + if owner.stripe_customer_id is None: + log.info( + f"stripe_customer_id is None, cannot update default billing address for owner {owner.ownerid}" + ) + return None + + try: + default_payment_method = stripe.Customer.retrieve( + owner.stripe_customer_id + ).invoice_settings.default_payment_method + + stripe.PaymentMethod.modify( + default_payment_method, + billing_details={"name": name, "address": billing_address}, + ) + + stripe.Customer.modify(owner.stripe_customer_id, address=billing_address) + log.info( + f"Stripe successfully updated billing address for owner {owner.ownerid} by user #{self.requesting_user.ownerid}" + ) + except Exception: + log.error( + "Unable to update billing address for customer", + extra=dict( + customer_id=owner.stripe_customer_id, + subscription_id=owner.stripe_subscription_id, + ), + ) + + @_log_stripe_error + def apply_cancellation_discount(self, owner: Owner): + if owner.stripe_subscription_id is None: + log.info( + f"stripe_subscription_id is None, not applying cancellation coupon for owner {owner.ownerid}" + ) + return + plan_service = PlanService(current_org=owner) + billing_rate = plan_service.billing_rate + + if billing_rate == PlanBillingRate.MONTHLY.value and not owner.stripe_coupon_id: + log.info(f"Creating Stripe cancellation coupon for owner {owner.ownerid}") + coupon = stripe.Coupon.create( + percent_off=30.0, + duration="repeating", + duration_in_months=6, + name="30% off for 6 months", + max_redemptions=1, + metadata={ + "ownerid": owner.ownerid, + "username": owner.username, + "email": owner.email, + "name": owner.name, + }, + ) + + owner.stripe_coupon_id = coupon.id + owner.save() + + log.info( + f"Applying cancellation coupon to Stripe subscription for owner {owner.ownerid}" + ) + stripe.Customer.modify( + owner.stripe_customer_id, + coupon=owner.stripe_coupon_id, + ) + + @_log_stripe_error + def create_setup_intent(self, owner: Owner) -> stripe.SetupIntent: + log.info( + "Stripe create setup intent for owner", + extra=dict( + owner_id=owner.ownerid, + requesting_user_id=self.requesting_user.ownerid, + subscription_id=owner.stripe_subscription_id, + customer_id=owner.stripe_customer_id, + ), + ) + return stripe.SetupIntent.create( + payment_method_configuration=settings.STRIPE_PAYMENT_METHOD_CONFIGURATION_ID, + customer=owner.stripe_customer_id, + ) + + @_log_stripe_error + def get_unverified_payment_methods(self, owner: Owner): + log.info( + "Getting unverified payment methods", + extra=dict( + owner_id=owner.ownerid, stripe_customer_id=owner.stripe_customer_id + ), + ) + if not owner.stripe_customer_id: + return [] + + unverified_payment_methods = [] + + # Check payment intents + has_more = True + starting_after = None + while has_more: + payment_intents = stripe.PaymentIntent.list( + customer=owner.stripe_customer_id, + limit=20, + starting_after=starting_after, + ) + for intent in payment_intents.data or []: + if ( + intent.get("next_action") + and intent.next_action + and intent.next_action.get("type") == "verify_with_microdeposits" + ): + unverified_payment_methods.extend( + [ + { + "payment_method_id": intent.payment_method, + "hosted_verification_url": intent.next_action.verify_with_microdeposits.hosted_verification_url, + } + ] + ) + has_more = payment_intents.has_more + if has_more and payment_intents.data: + starting_after = payment_intents.data[-1].id + + # Check setup intents + has_more = True + starting_after = None + while has_more: + setup_intents = stripe.SetupIntent.list( + customer=owner.stripe_customer_id, + limit=20, + starting_after=starting_after, + ) + for intent in setup_intents.data: + if ( + intent.get("next_action") + and intent.next_action + and intent.next_action.get("type") == "verify_with_microdeposits" + ): + unverified_payment_methods.extend( + [ + { + "payment_method_id": intent.payment_method, + "hosted_verification_url": intent.next_action.verify_with_microdeposits.hosted_verification_url, + } + ] + ) + has_more = setup_intents.has_more + if has_more and setup_intents.data: + starting_after = setup_intents.data[-1].id + + return unverified_payment_methods + + +class EnterprisePaymentService(AbstractPaymentService): + # enterprise has no payments setup so these are all noops + + def get_invoice(self, owner, invoice_id): + pass + + def list_filtered_invoices(self, owner, limit=10): + pass + + def delete_subscription(self, owner): + pass + + def modify_subscription(self, owner, plan): + pass + + def create_checkout_session(self, owner, plan): + pass + + def get_subscription(self, owner): + pass + + def update_payment_method(self, owner, payment_method): + pass + + def update_email_address(self, owner, email_address): + pass + + def update_billing_address(self, owner, name, billing_address): + pass + + def get_schedule(self, owner): + pass + + def apply_cancellation_discount(self, owner: Owner): + pass + + def create_setup_intent(self, owner): + pass + + def get_unverified_payment_methods(self, owner: Owner): + pass + + +class BillingService: + payment_service = None + + def __init__(self, payment_service=None, requesting_user=None): + if payment_service is None: + if settings.IS_ENTERPRISE: + self.payment_service = EnterprisePaymentService() + else: + self.payment_service = StripeService(requesting_user=requesting_user) + else: + self.payment_service = payment_service + + if not issubclass(type(self.payment_service), AbstractPaymentService): + raise Exception( + "self.payment_service must subclass AbstractPaymentService!" + ) + + def get_subscription(self, owner): + return self.payment_service.get_subscription(owner) + + def get_schedule(self, owner): + return self.payment_service.get_schedule(owner) + + def get_invoice(self, owner, invoice_id): + return self.payment_service.get_invoice(owner, invoice_id) + + def list_filtered_invoices(self, owner, limit=10): + return self.payment_service.list_filtered_invoices(owner, limit) + + def get_unverified_payment_methods(self, owner: Owner): + return self.payment_service.get_unverified_payment_methods(owner) + + def update_plan(self, owner, desired_plan): + """ + Takes an owner and desired plan, and updates the owner's plan. Depending + on current state, might create a stripe checkout session and return + the checkout session's ID, which is a string. Otherwise returns None. + """ + try: + plan = Plan.objects.get(name=desired_plan["value"]) + except Plan.DoesNotExist: + log.warning( + f"Unable to find plan {desired_plan['value']} for owner {owner.ownerid}" + ) + return None + + if not plan.is_active: + log.warning( + f"Attempted to transition to non-existent or legacy plan: " + f"owner {owner.ownerid}, plan: {desired_plan}" + ) + return None + + if plan.paid_plan is False: + if owner.stripe_subscription_id is not None: + self.payment_service.delete_subscription(owner) + else: + plan_service = PlanService(current_org=owner) + plan_service.set_default_plan_data() + else: + if owner.stripe_subscription_id is not None: + # if the existing subscription is incomplete, clean it up and create a new checkout session + subscription = self.payment_service.get_subscription(owner) + + if subscription and subscription.status == "incomplete": + self._cleanup_incomplete_subscription(subscription, owner) + return self.payment_service.create_checkout_session( + owner, desired_plan + ) + + # if the existing subscription is complete, modify the plan + self.payment_service.modify_subscription(owner, desired_plan) + else: + # if the owner has no subscription, create a new checkout session + return self.payment_service.create_checkout_session(owner, desired_plan) + + def update_payment_method(self, owner, payment_method): + """ + Takes an owner and a new card. card is an object coming directly from + the front-end; without any validation, as payment service can handle + the card data differently + """ + return self.payment_service.update_payment_method(owner, payment_method) + + def update_email_address( + self, + owner: Owner, + email_address: str, + apply_to_default_payment_method: bool = False, + ): + """ + Takes an owner and a new email. Email is a string coming directly from + the front-end. If the owner has a payment id and if it's a valid email, + the payment service will update the email address in the upstream service. + Otherwise returns None. + """ + return self.payment_service.update_email_address( + owner, email_address, apply_to_default_payment_method + ) + + def update_billing_address(self, owner: Owner, name: str, billing_address): + """ + Takes an owner and a billing address. Try to update the owner's billing address + to the address passed in. Address should be validated via stripe component prior + to hitting this service method. Return None if invalid. + """ + return self.payment_service.update_billing_address(owner, name, billing_address) + + def apply_cancellation_discount(self, owner: Owner): + return self.payment_service.apply_cancellation_discount(owner) + + def create_setup_intent(self, owner: Owner): + """ + Creates a SetupIntent for the given owner to securely collect payment details + See https://docs.stripe.com/api/setup_intents/create + """ + return self.payment_service.create_setup_intent(owner) + + def _cleanup_incomplete_subscription( + self, subscription: stripe.Subscription, owner: Owner + ): + try: + payment_intent_id = subscription.latest_invoice.payment_intent + except Exception as e: + log.error( + "Latest invoice is missing payment intent id", + extra=dict(error=e), + ) + return None + + payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id) + if payment_intent.status == "requires_action": + log.info( + "Subscription has pending payment verification", + extra=dict( + subscription_id=subscription.get("id"), + payment_intent_id=payment_intent.get("id"), + payment_intent_status=payment_intent.get("status"), + ), + ) + try: + # Delete the subscription, which also removes the + # pending payment method and unverified payment intent + stripe.Subscription.delete(subscription) + log.info( + "Deleted incomplete subscription", + extra=dict( + subscription_id=subscription.get("id"), + payment_intent_id=payment_intent.get("id"), + ), + ) + except Exception as e: + log.error( + "Failed to delete subscription", + extra=dict( + subscription_id=subscription.get("id"), + payment_intent_id=payment_intent.get("id"), + error=str(e), + ), + ) diff --git a/apps/codecov-api/services/bundle_analysis.py b/apps/codecov-api/services/bundle_analysis.py new file mode 100644 index 0000000000..a34814d575 --- /dev/null +++ b/apps/codecov-api/services/bundle_analysis.py @@ -0,0 +1,553 @@ +import enum +import os +from dataclasses import dataclass +from datetime import datetime +from decimal import Decimal +from typing import Any, Dict, Iterable, List, Optional, Union + +import sentry_sdk +from asgiref.sync import sync_to_async +from django.utils.functional import cached_property +from shared.api_archive.archive import ArchiveService +from shared.bundle_analysis import AssetReport as SharedAssetReport +from shared.bundle_analysis import ( + BundleAnalysisComparison as SharedBundleAnalysisComparison, +) +from shared.bundle_analysis import BundleAnalysisReport as SharedBundleAnalysisReport +from shared.bundle_analysis import BundleAnalysisReportLoader +from shared.bundle_analysis import BundleChange as SharedBundleChange +from shared.bundle_analysis import BundleReport as SharedBundleReport +from shared.bundle_analysis import ModuleReport as SharedModuleReport +from shared.bundle_analysis.models import AssetType +from shared.django_apps.bundle_analysis.service.bundle_analysis import ( + BundleAnalysisCacheConfigService, +) +from shared.storage import get_appropriate_storage_service + +from core.models import Commit, Repository +from graphql_api.actions.measurements import ( + measurements_by_ids, + measurements_last_uploaded_before_start_date, +) +from reports.models import CommitReport +from timeseries.helpers import fill_sparse_measurements +from timeseries.models import Interval, MeasurementName + + +@sentry_sdk.trace +def load_report( + commit: Commit, report_code: Optional[str] = None +) -> Optional[SharedBundleAnalysisReport]: + storage = get_appropriate_storage_service(commit.repository.repoid) + + commit_report = commit.reports.filter( + report_type=CommitReport.ReportType.BUNDLE_ANALYSIS, + code=report_code, + ).first() + if commit_report is None: + return None + + loader = BundleAnalysisReportLoader( + storage_service=storage, + repo_key=ArchiveService.get_archive_hash(commit.repository), + ) + return loader.load(commit_report.external_id) + + +def get_extension(filename: str) -> str: + """ + Gets the file extension of the file without the dot + """ + # At times file can be something like './index.js + 12 modules', only keep the real filepath + filename = filename.split(" ")[0] + # Retrieve the file extension with the dot + _, file_extension = os.path.splitext(filename) + # Return empty string if file has no extension + if not file_extension or file_extension[0] != ".": + return file_extension + # Remove the dot in the extension + file_extension = file_extension[1:] + # At times file can be something like './index.js?module', remove the ? + if "?" in file_extension: + file_extension = file_extension[: file_extension.rfind("?")] + + return file_extension + + +class BundleAnalysisMeasurementsAssetType(enum.Enum): + REPORT_SIZE = MeasurementName.BUNDLE_ANALYSIS_REPORT_SIZE + JAVASCRIPT_SIZE = MeasurementName.BUNDLE_ANALYSIS_JAVASCRIPT_SIZE + STYLESHEET_SIZE = MeasurementName.BUNDLE_ANALYSIS_STYLESHEET_SIZE + FONT_SIZE = MeasurementName.BUNDLE_ANALYSIS_FONT_SIZE + IMAGE_SIZE = MeasurementName.BUNDLE_ANALYSIS_IMAGE_SIZE + ASSET_SIZE = MeasurementName.BUNDLE_ANALYSIS_ASSET_SIZE + + +class BundleAnalysisMeasurementData(object): + def __init__( + self, + raw_measurements: List[dict], + asset_type: Union[BundleAnalysisMeasurementsAssetType, str], + asset_name: Optional[str], + interval: Interval, + after: Optional[datetime], + before: datetime, + ): + self.raw_measurements = raw_measurements + self.measurement_type = asset_type + self.measurement_name = asset_name + self.interval = interval + self.after = after + self.before = before + + @cached_property + def asset_type(self) -> str: + if isinstance(self.measurement_type, str): + return self.measurement_type + return self.measurement_type.name + + @cached_property + def name(self) -> Optional[str]: + return self.measurement_name + + @cached_property + def size(self) -> Optional["BundleData"]: + if len(self.raw_measurements) > 0: + return BundleData(self.raw_measurements[-1]["avg"]) + + @cached_property + def change(self) -> Optional["BundleData"]: + if len(self.raw_measurements) > 1: + return BundleData( + self.raw_measurements[-1]["avg"] - self.raw_measurements[0]["avg"] + ) + + @cached_property + def measurements(self) -> Iterable[Dict[str, Any]]: + if not self.raw_measurements: + return [] + return fill_sparse_measurements( + self.raw_measurements, self.interval, self.after, self.before + ) + + +@dataclass +class BundleLoadTime: + """ + Value in Milliseconds + Reference for speed estimation: + https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/throttling/index.html + """ + + # Speed of internet in bits per second (as reference above) + THREE_G_SPEED = 750 * 1000 # Equivalent to 750 Kbps + HIGH_SPEED = 30 * 1000 * 1000 # Equivalent to 30 Mbps + + # Computed load time in milliseconds + three_g: int + high_speed: int + + +@dataclass +class BundleSize: + """ + Value in Bytes + """ + + # Compression ratio compared to uncompressed size + GZIP = 0.001 + UNCOMPRESS = 1.0 + + # Computed size in bytes + gzip: int + uncompress: int + + +@dataclass +class BundleData: + def __init__(self, size_in_bytes: int, gzip_size_in_bytes: Optional[int] = None): + self.size_in_bytes = size_in_bytes + self.size_in_bits = size_in_bytes * 8 + self.gzip_size_in_bytes = gzip_size_in_bytes + + @cached_property + def size(self) -> BundleSize: + gzip_size = ( + self.gzip_size_in_bytes + if self.gzip_size_in_bytes is not None + else int(float(self.size_in_bytes) * BundleSize.GZIP) + ) + return BundleSize( + gzip=gzip_size, + uncompress=int(float(self.size_in_bytes) * BundleSize.UNCOMPRESS), + ) + + @cached_property + def load_time(self) -> BundleLoadTime: + return BundleLoadTime( + three_g=int((self.size_in_bits / BundleLoadTime.THREE_G_SPEED) * 1000), + high_speed=int((self.size_in_bits / BundleLoadTime.HIGH_SPEED) * 1000), + ) + + +@dataclass +class ModuleReport(object): + def __init__(self, module: SharedModuleReport): + self.module = module + + @cached_property + def name(self) -> str: + return self.module.name + + @cached_property + def size_total(self) -> int: + return self.module.size + + @cached_property + def extension(self) -> str: + return get_extension(self.name) + + +@dataclass +class AssetReport(object): + def __init__(self, asset: SharedAssetReport): + self.asset = asset + self.all_modules = None + + @cached_property + def id(self) -> int: + return self.asset.id + + @cached_property + def name(self) -> str: + return self.asset.hashed_name + + @cached_property + def normalized_name(self) -> str: + return self.asset.name + + @cached_property + def extension(self) -> str: + return get_extension(self.name) + + @cached_property + def size_total(self) -> int: + return self.asset.size + + @cached_property + def gzip_size_total(self) -> int: + return self.asset.gzip_size + + @cached_property + def modules(self) -> List[ModuleReport]: + return [ModuleReport(module) for module in self.asset.modules()] + + @cached_property + def module_extensions(self) -> List[str]: + return list({module.extension for module in self.modules}) + + @cached_property + def routes(self) -> Optional[List[str]]: + return self.asset.routes() + + +@dataclass +class BundleReport(object): + def __init__(self, report: SharedBundleReport, filters: Dict[str, Any] = {}): + self.report = report + self.filters = filters + + @cached_property + def name(self) -> str: + return self.report.name + + @cached_property + def all_assets(self) -> List[AssetReport]: + return [AssetReport(asset) for asset in self.report.asset_reports()] + + def assets( + self, ordering: Optional[str] = None, ordering_desc: Optional[bool] = None + ) -> List[AssetReport]: + ordering_dict: Dict[str, Any] = {} + if ordering: + ordering_dict["ordering_column"] = ordering + if ordering_desc is not None: + ordering_dict["ordering_desc"] = ordering_desc + return [ + AssetReport(asset) + for asset in self.report.asset_reports(**{**ordering_dict, **self.filters}) + ] + + def asset(self, name: str) -> Optional[AssetReport]: + for asset_report in self.all_assets: + if asset_report.name == name: + return asset_report + + @cached_property + def size_total(self) -> int: + return self.report.total_size(**self.filters) + + @cached_property + def gzip_size_total(self) -> int: + return self.report.total_gzip_size(**self.filters) + + @cached_property + def module_extensions(self) -> List[str]: + extensions = set() + for asset in self.assets(): + extensions.update(asset.module_extensions) + return list(extensions) + + @cached_property + def module_count(self) -> int: + return sum([len(asset.modules) for asset in self.assets()]) + + @cached_property + def is_cached(self) -> bool: + return self.report.is_cached() + + @cached_property + def info(self) -> dict: + return self.report.info() + + @sync_to_async + def cache_config(self, repo_id: int) -> bool: + return BundleAnalysisCacheConfigService.get_cache_option( + repo_id=repo_id, name=self.report.name + ) + + +@dataclass +class BundleReportInfo(object): + def __init__(self, info: dict) -> None: + self.info = info + + @cached_property + def version(self) -> str: + return self.info.get("version", "unknown") + + @cached_property + def plugin_name(self) -> str: + return self.info.get("plugin_name", "unknown") + + @cached_property + def plugin_version(self) -> str: + return self.info.get("plugin_version", "unknown") + + @cached_property + def built_at(self) -> str: + return str(datetime.fromtimestamp(self.info.get("built_at", 0) / 1000)) + + @cached_property + def duration(self) -> int: + return self.info.get("duration", -1) + + @cached_property + def bundler_name(self) -> str: + return self.info.get("bundler_name", "unknown") + + @cached_property + def bundler_version(self) -> str: + return self.info.get("bundler_version", "unknown") + + +@dataclass +class BundleAnalysisReport(object): + def __init__(self, report: SharedBundleAnalysisReport): + self.report = report + + def bundle( + self, name: str, filters: Dict[str, List[str]] + ) -> Optional[BundleReport]: + bundle_report = self.report.bundle_report(name) + if bundle_report: + return BundleReport(bundle_report, filters) + + @cached_property + def bundles(self) -> List[BundleReport]: + return [BundleReport(bundle) for bundle in self.report.bundle_reports()] + + @cached_property + def size_total(self) -> int: + return sum([bundle.size_total for bundle in self.bundles]) + + @cached_property + def is_cached(self) -> bool: + return self.report.is_cached() + + +@dataclass +class BundleAnalysisComparison(object): + def __init__( + self, + loader: BundleAnalysisReportLoader, + base_report_key: str, + head_report_key: str, + repository: Repository, + ): + self.comparison = SharedBundleAnalysisComparison( + loader, + base_report_key, + head_report_key, + repository, + ) + self.head_report = self.comparison.head_report + + @cached_property + def bundles(self) -> List["BundleComparison"]: + bundle_comparisons = [] + for bundle_change in self.comparison.bundle_changes(): + head_bundle_report = self.comparison.head_report.bundle_report( + bundle_change.bundle_name + ) + head_size = head_bundle_report.total_size() if head_bundle_report else 0 + bundle_comparisons.append(BundleComparison(bundle_change, head_size)) + return bundle_comparisons + + @cached_property + def size_delta(self) -> int: + return sum([change.size_delta for change in self.comparison.bundle_changes()]) + + @cached_property + def size_total(self) -> int: + return BundleAnalysisReport(self.head_report).size_total + + +@dataclass +class BundleComparison(object): + def __init__(self, bundle_change: SharedBundleChange, head_bundle_report_size: int): + self.bundle_change = bundle_change + self.head_bundle_report_size = head_bundle_report_size + + @cached_property + def bundle_name(self) -> str: + return self.bundle_change.bundle_name + + @cached_property + def change_type(self) -> str: + return self.bundle_change.change_type.value + + @cached_property + def size_delta(self) -> int: + return self.bundle_change.size_delta + + @cached_property + def size_total(self) -> int: + return self.head_bundle_report_size + + +class BundleAnalysisMeasurementsService(object): + def __init__( + self, + repository: Repository, + interval: Interval, + before: datetime, + after: Optional[datetime] = None, + branch: Optional[str] = None, + ) -> None: + self.repository = repository + self.interval = interval + self.after = after + self.before = before + self.branch = branch + + @sentry_sdk.trace + def _compute_measurements( + self, measurable_name: str, measurable_ids: List[str] + ) -> Dict[str, List[Dict[str, Any]]]: + all_measurements = measurements_by_ids( + repository=self.repository, + measurable_name=measurable_name, + measurable_ids=measurable_ids, + interval=self.interval, + after=self.after, + before=self.before, + branch=self.branch, + ) or {measurable_ids[0]: []} + + # Carry over previous available value for start date if its value is null + for measurable_id, measurements in all_measurements.items(): + if self.after is not None and ( + not measurements or measurements[0]["timestamp_bin"] > self.after + ): + carryover_measurement = measurements_last_uploaded_before_start_date( + owner_id=self.repository.author.ownerid, + repo_id=self.repository.repoid, + measurable_name=measurable_name, + measurable_id=measurable_id, + start_date=self.after, + branch=self.branch, + ) + + # Create a new datapoint in the measurements and prepend it to the existing list + # If there isn't any measurements before the start date range, measurements will be untouched + if carryover_measurement: + value = Decimal(carryover_measurement[0]["value"]) + carryover = dict(measurements[0]) if measurements else {} + carryover["timestamp_bin"] = self.after.replace( + hour=0, minute=0, second=0, microsecond=0 + ) + carryover["min"] = value + carryover["max"] = value + carryover["avg"] = value + all_measurements[measurable_id] = [carryover] + all_measurements[ + measurable_id + ] + + return all_measurements + + @sentry_sdk.trace + def compute_asset( + self, asset_report: AssetReport + ) -> Optional[BundleAnalysisMeasurementData]: + asset = asset_report.asset + if asset.asset_type != AssetType.JAVASCRIPT: + return None + + measurements = self._compute_measurements( + measurable_name=MeasurementName.BUNDLE_ANALYSIS_ASSET_SIZE.value, + measurable_ids=[asset.uuid], + ) + + return BundleAnalysisMeasurementData( + raw_measurements=list(measurements.get(asset.uuid, [])), + asset_type=BundleAnalysisMeasurementsAssetType.JAVASCRIPT_SIZE, + asset_name=asset.name, + interval=self.interval, + after=self.after, + before=self.before, + ) + + @sentry_sdk.trace + def compute_report( + self, + bundle_report: BundleReport, + asset_type: BundleAnalysisMeasurementsAssetType, + ) -> List[BundleAnalysisMeasurementData]: + asset_uuid_to_name_mapping = {} + if asset_type.value == MeasurementName.BUNDLE_ANALYSIS_ASSET_SIZE: + measurable_ids = [] + for asset_report in bundle_report.all_assets: + asset = asset_report.asset + if asset.asset_type == AssetType.JAVASCRIPT: + measurable_ids.append(asset.uuid) + asset_uuid_to_name_mapping[asset.uuid] = asset.name + else: + measurable_ids = [bundle_report.name] + + measurements = self._compute_measurements( + measurable_name=asset_type.value.value, + measurable_ids=measurable_ids, + ) + + return [ + BundleAnalysisMeasurementData( + raw_measurements=list(measurements.get(measurable_id, [])), + asset_type=asset_type, + asset_name=asset_uuid_to_name_mapping.get(measurable_id, None), + interval=self.interval, + after=self.after, + before=self.before, + ) + for measurable_id in measurable_ids + ] diff --git a/apps/codecov-api/services/comparison.py b/apps/codecov-api/services/comparison.py new file mode 100644 index 0000000000..df6ca410c0 --- /dev/null +++ b/apps/codecov-api/services/comparison.py @@ -0,0 +1,1298 @@ +import copy +import functools +import json +import logging +from collections import Counter +from dataclasses import dataclass, field +from datetime import datetime +from typing import List, Optional, Tuple + +import minio +import pytz +import shared.reports.api_report_service as report_service +from asgiref.sync import async_to_sync +from django.db.models import Prefetch, QuerySet +from django.utils.functional import cached_property +from shared.api_archive.archive import ArchiveService +from shared.helpers.redis import get_redis_connection +from shared.helpers.yaml import walk +from shared.reports.types import ReportTotals +from shared.torngit.base import TorngitBaseAdapter +from shared.utils.merge import LineType, line_type + +from compare.models import CommitComparison +from core.models import Commit, Pull +from reports.models import CommitReport +from services import ServiceException +from services.repo_providers import RepoProviderService +from utils.config import get_config + +log = logging.getLogger(__name__) + + +redis = get_redis_connection() + + +MAX_DIFF_SIZE = 170 + + +def _is_added(line_value): + return line_value and line_value[0] == "+" + + +def _is_removed(line_value): + return line_value and line_value[0] == "-" + + +class ComparisonException(ServiceException): + @property + def message(self): + return str(self) + + +class MissingComparisonCommit(ComparisonException): + pass + + +class MissingComparisonReport(ComparisonException): + pass + + +class FirstPullRequest: + message = "This is the first pull request for this repository" + + +class FileComparisonTraverseManager: + """ + The FileComparisonTraverseManager uses the visitor-pattern to execute a series + of arbitrary actions on each line in a FileComparison. The main entrypoint to + this class is the '.apply()' method, which is the only method client code should invoke. + """ + + def __init__(self, head_file_eof=0, base_file_eof=0, segments=[], src=[]): + """ + head_file_eof -- end-line of the head_file we are traversing, plus 1 + base_file_eof -- same as above, for base_file + + ^^ Generally client code should supply both, except in a couple cases: + 1. The file is newly tracked. In this case, there is no base file, so we should + iterate only over the head file lines. + 2. The file is deleted. As of right now (4/2/2020), we don't show deleted files in + comparisons, but if we were to support that, we would not supply a head_file_eof + and instead only iterate over lines in the base file. + + segments -- these come from the provider API response related to the comparison, and + constitute the 'diff' between the base and head references. Each segment takes this form: + + { + "header": [ + base reference offset, + number of lines in file-segment before changes applied, + head reference offset, + number of lines in file-segment after changes applied + ], + "lines": [ # line values for lines in the diff + "+this is an added line", + "-this is a removed line", + "this line is unchanged in the diff", + ... + ] + } + + The segment["header"], also known as the hunk-header (https://en.wikipedia.org/wiki/Diff#Unified_format), + is an array of strings, which is why we have to use the int() builtin function + to compare with self.head_ln and self.base_ln. It is used by this algorithm to + 1. Set initial values for the self.base_ln and self.head_ln line-counters, and + 2. Detect if self.base and/or self.head refer to lines in the diff at any given time + + This algorithm relies on the fact that segments are returned in ascending + order for each file, which means that the "nearest" segment to the current line + being traversed is located at segments[0]. + + src -- this is the source code of the file at the head-reference, where each line + is a cell in the array. If we are not traversing a segment, and src is provided, + the line value passed to the visitors will be the line at src[self.head_ln - 1]. + """ + self.head_file_eof = head_file_eof + self.base_file_eof = base_file_eof + self.segments = copy.deepcopy(segments) + self.src = src + + if self.segments: + # Base offsets can be 0 if files are added or removed + self.base_ln = min(1, int(self.segments[0]["header"][0])) + self.head_ln = min(1, int(self.segments[0]["header"][2])) + else: + self.base_ln, self.head_ln = 1, 1 + + def traverse_finished(self): + if self.segments: + return False + if self.src: + return self.head_ln > len(self.src) + return self.head_ln >= self.head_file_eof and self.base_ln >= self.base_file_eof + + def traversing_diff(self): + if self.segments == []: + return False + + base_ln_within_offset = ( + int(self.segments[0]["header"][0]) + <= self.base_ln + < int(self.segments[0]["header"][0]) + + int(self.segments[0]["header"][1] or 1) + ) + head_ln_within_offset = ( + int(self.segments[0]["header"][2]) + <= self.head_ln + < int(self.segments[0]["header"][2]) + + int(self.segments[0]["header"][3] or 1) + ) + return base_ln_within_offset or head_ln_within_offset + + def pop_line(self): + if self.traversing_diff(): + return self.segments[0]["lines"].pop(0) + + if self.src: + return self.src[self.head_ln - 1] + + def apply(self, visitors): + """ + Traverses the lines in a file comparison while accounting for the diff. + If a line only appears in the base file (removed in head), it is prefixed + with '-', and we only increment self.base_ln. If a line only appears in + the head file, it is newly added and prefixed with '+', and we only + increment self.head_ln. + + visitors -- A list of visitors applied to each line. + """ + while not self.traverse_finished(): + line_value = self.pop_line() + is_diff = self.traversing_diff() + + for visitor in visitors: + visitor( + None if is_diff and _is_added(line_value) else self.base_ln, + None if is_diff and _is_removed(line_value) else self.head_ln, + line_value, + is_diff, # TODO(pierce): remove when upon combining diff + changes tabs in UI + ) + + if is_diff and _is_added(line_value): + self.head_ln += 1 + elif is_diff and _is_removed(line_value): + self.base_ln += 1 + else: + self.head_ln += 1 + self.base_ln += 1 + + if self.segments and not self.segments[0]["lines"]: + # Either the segment has no lines (and is therefore of no use) + # or all lines have been popped and visited, which means we are + # done traversing it + self.segments.pop(0) + + +class FileComparisonVisitor: + """ + Abstract class with a convenience method for getting lines amongst + all the edge cases. + """ + + def _get_line(self, report_file, ln): + """ + Kindof a hacky way to bypass the dataclasses used in `reports` + library, because they are extremely slow. This basically copies + some logic from ReportFile.get and ReportFile._line, which work + together to take an index and turn it into a ReportLine. Here + we do something similar, but just return the underlying array instead. + Not sure if this will be the final solution. + + Note: the underlying array representation cn be seen here: + https://github.com/codecov/shared/blob/master/shared/reports/types.py#L75 + The index in the array representation is 1-1 with the index of the + dataclass attribute for ReportLine. + """ + if report_file is None or ln is None: + return None + + # copied from ReportFile.get + try: + line = report_file._lines[ln - 1] + except IndexError: + return None + + # copied from ReportFile._line, minus dataclass instantiation + if line: + if isinstance(line, list): + return line + else: + # these are old versions + # note:(pierce) ^^ this comment is copied, not sure what it means + return json.loads(line) + + def _get_lines(self, base_ln, head_ln): + base_line = self._get_line(self.base_file, base_ln) + head_line = self._get_line(self.head_file, head_ln) + return base_line, head_line + + def __call__(self, base_ln, head_ln, value, is_diff): + pass + + +class CreateLineComparisonVisitor(FileComparisonVisitor): + """ + A visitor that creates LineComparisons, and stores the + result in self.lines. Only operates on lines that have + code-values derived from segments or src in FileComparisonTraverseManager. + """ + + def __init__(self, base_file, head_file): + self.base_file, self.head_file = base_file, head_file + self.lines = [] + + def __call__(self, base_ln, head_ln, value, is_diff): + if value is None: + return + + base_line, head_line = self._get_lines(base_ln, head_ln) + + self.lines.append( + LineComparison( + base_line=base_line, + head_line=head_line, + base_ln=base_ln, + head_ln=head_ln, + value=value, + is_diff=is_diff, + ) + ) + + +class CreateChangeSummaryVisitor(FileComparisonVisitor): + """ + A visitor for summarizing the "unexpected coverage changes" + to a certain file. We specifically ignore lines that are changed + in the source code, which are prefixed with '+' or '-'. Result + is stored in self.summary. + """ + + def __init__(self, base_file, head_file): + self.base_file, self.head_file = base_file, head_file + self.summary = Counter() + self.coverage_type_map = { + LineType.hit: "hits", + LineType.miss: "misses", + LineType.partial: "partials", + } + + def _update_summary(self, base_line, head_line): + """ + Updates the change summary based on the coverage type (0 + for miss, 1 for hit, 2 for partial) found at index 0 of the + line-array. + """ + self.summary[self.coverage_type_map[line_type(base_line[0])]] -= 1 + self.summary[self.coverage_type_map[line_type(head_line[0])]] += 1 + + def __call__(self, base_ln, head_ln, value, is_diff): + if value and value[0] in ["+", "-"]: + return + + base_line, head_line = self._get_lines(base_ln, head_ln) + if base_line is None or head_line is None: + return + + if line_type(base_line[0]) == line_type(head_line[0]): + return + + self._update_summary(base_line, head_line) + + +class LineComparison: + def __init__(self, base_line, head_line, base_ln, head_ln, value, is_diff): + self.base_line = base_line + self.head_line = head_line + self.head_ln = head_ln + self.base_ln = base_ln + self.value = value + self.is_diff = is_diff + + self.added = is_diff and _is_added(value) + self.removed = is_diff and _is_removed(value) + + @property + def number(self): + return { + "base": self.base_ln if not self.added else None, + "head": self.head_ln if not self.removed else None, + } + + @property + def coverage(self): + return { + "base": None + if self.added or not self.base_line + else line_type(self.base_line[0]), + "head": None + if self.removed or not self.head_line + else line_type(self.head_line[0]), + } + + @cached_property + def head_line_sessions(self) -> Optional[List[tuple]]: + if self.head_line is None: + return None + + # `head_line` is the tuple representation of a `shared.reports.types.ReportLine` + # it has the following shape: + # (coverage, type, sessions, messages, complexity, datapoints) + + # each session is a tuple representation of a `shared.reports.types.LineSession` + # is has the following shape: + # (id, coverage, branches, partials, complexity) + sessions = self.head_line[2] + + return sessions + + @cached_property + def hit_count(self) -> Optional[int]: + if self.head_line_sessions is None: + return None + + hit_count = 0 + for id, coverage, *rest in self.head_line_sessions: + if line_type(coverage) == LineType.hit: + hit_count += 1 + if hit_count > 0: + return hit_count + + @cached_property + def hit_session_ids(self) -> Optional[List[int]]: + if self.head_line_sessions is None: + return None + + ids = [] + for id, coverage, *rest in self.head_line_sessions: + if line_type(coverage) == LineType.hit: + ids.append(id) + if len(ids) > 0: + return ids + + +class Segment: + """ + A segment represents a contiguous subset of lines in a file where either + the coverage has changed or the code has changed (i.e. is part of a diff). + """ + + # additional lines included before and after each segment + padding_lines = 3 + + # max distance between lines with coverage changes in a single segment + line_distance = 6 + + @classmethod + def segments(cls, file_comparison): + lines = file_comparison.lines + + # line numbers of interest (i.e. coverage changed or code changed) + line_numbers = [] + for idx, line in enumerate(lines): + if ( + line.coverage["base"] != line.coverage["head"] + or line.added + or line.removed + ): + line_numbers.append(idx) + + segmented_lines = [] + if len(line_numbers) > 0: + segmented_lines, last = [[]], None + for line_number in line_numbers: + if last is None or line_number - last <= cls.line_distance: + segmented_lines[-1].append(line_number) + else: + segmented_lines.append([line_number]) + last = line_number + + segments = [] + for group in segmented_lines: + # padding lines before first line of interest + start_line_number = group[0] - cls.padding_lines + start_line_number = max(start_line_number, 0) + # padding lines after last line of interest + end_line_number = group[-1] + cls.padding_lines + end_line_number = min(end_line_number, len(lines) - 1) + + segment = cls(lines[start_line_number : end_line_number + 1]) + segments.append(segment) + + return segments + + def __init__(self, lines): + self._lines = lines + + @property + def header(self): + base_start = None + head_start = None + num_removed = 0 + num_added = 0 + num_context = 0 + + for line in self.lines: + if base_start is None and line.number["base"] is not None: + base_start = int(line.number["base"]) + if head_start is None and line.number["head"] is not None: + head_start = int(line.number["head"]) + if line.added: + num_added += 1 + elif line.removed: + num_removed += 1 + else: + num_context += 1 + + return ( + base_start or 0, + num_context + num_removed, + head_start or 0, + num_context + num_added, + ) + + @property + def lines(self): + return self._lines + + @property + def has_diff_changes(self): + for line in self.lines: + if line.added or line.removed: + return True + return False + + @property + def has_unintended_changes(self): + for line in self.lines: + head_coverage = line.coverage["base"] + base_coverage = line.coverage["head"] + if not (line.added or line.removed) and (base_coverage != head_coverage): + return True + return False + + def remove_unintended_changes(self): + filtered = [] + for line in self._lines: + base_cov = line.coverage["base"] + head_cov = line.coverage["head"] + if (line.added or line.removed) or (base_cov == head_cov): + filtered.append(line) + self._lines = filtered + + +class FileComparison: + def __init__( + self, + base_file, + head_file, + diff_data=None, + src=[], + bypass_max_diff=False, + should_search_for_changes=None, + ): + """ + comparison -- the enclosing Comparison object that owns this FileComparison + + base_file -- the ReportFile for this file from the base report + + head_file -- the ReportFile for this file from the head report + + diff_data -- the git-comparison between the base and head references in the instantiation + Comparison object. fields include: + + stats: -- {"added": number of added lines, "removed": number of removed lines} + segments: (described in detail in the FileComparisonTraverseManager docstring) + before: the name of this file in the base reference, if different from name in head ref + + If this file is unchanged in the comparison between base and head, the default will be used. + + src -- The full source of the file in the head reference. Used in FileComparisonTraverseManager + to join src-code with coverage data. Default is used when retrieving full comparison, + whereas full-src is serialized when retrieving individual file comparison. + + bypass_max_diff -- configuration paramater that tells this class to ignore max-diff truncating. + default is used when retrieving full comparison; True is passed when fetching individual + file comparison. + + should_search_for_changes -- flag that indicates if this FileComparison has unexpected coverage changes, + according to a value cached during asynchronous processing. Has three values: + 1. True - indicates this FileComparison has unexpected coverage changes according to worker, + and we should process the lines in this FileComparison using FileComparisonTraverseManager + to calculate a change summary. + 2. False - indicates this FileComparison does not have unexpected coverage changes according to + worker, and we should not traverse this file or calculate a change summary. + 3. None (default) - indicates we do not have information cached from worker to rely on here + (no value in cache), so we need to traverse this FileComparison and calculate a change + summary to find out. + """ + self.base_file = base_file + self.head_file = head_file + self.diff_data = diff_data + self.src = src + + # Some extra fields for truncating large diffs in the initial response + self.total_diff_length = ( + functools.reduce( + lambda a, b: a + b, + [len(segment["lines"]) for segment in self.diff_data["segments"]], + ) + if self.diff_data is not None and self.diff_data.get("segments") + else 0 + ) + + self.bypass_max_diff = bypass_max_diff + self.should_search_for_changes = should_search_for_changes + + @property + def name(self): + return { + "base": self.base_file.name if self.base_file is not None else None, + "head": self.head_file.name if self.head_file is not None else None, + } + + @property + def totals(self): + head_totals = self.head_file.totals if self.head_file is not None else None + + # The call to '.apply_diff()' in 'Comparison.head_report' stores diff totals + # for each file in the diff_data for that file (in a field called 'totals'). + # Here we pass this along to the frontend by assigning the diff totals + # to the head_totals' 'diff' attribute. It is absolutely worth considering + # modifying the behavior of shared.reports to implement something similar. + diff_totals = None + if head_totals and self.diff_data: + diff_totals = self.diff_data.get("totals") + head_totals.diff = diff_totals or 0 + + return { + "base": self.base_file.totals if self.base_file is not None else None, + "head": head_totals, + "diff": diff_totals, + } + + @property + def has_diff(self): + return self.diff_data is not None + + @property + def stats(self): + return self.diff_data["stats"] if self.diff_data else None + + @cached_property + def _calculated_changes_and_lines(self): + """ + Applies visitors to the file to generate response data (line comparison representations + and change summary). Only applies visitors if + + 1. The file has a diff or src, in which case we need to generate response data for it anyway, or + 2. The should_search_for_changes flag is defined (not None) and is True + + This limitation improves performance by limiting searching for changes to only files that + have them. + """ + change_summary_visitor = CreateChangeSummaryVisitor( + self.base_file, self.head_file + ) + create_lines_visitor = CreateLineComparisonVisitor( + self.base_file, self.head_file + ) + + if self.diff_data or self.src or self.should_search_for_changes is not False: + FileComparisonTraverseManager( + head_file_eof=self.head_file.eof if self.head_file is not None else 0, + base_file_eof=self.base_file.eof if self.base_file is not None else 0, + segments=self.diff_data["segments"] + if self.diff_data and "segments" in self.diff_data + else [], + src=self.src, + ).apply([change_summary_visitor, create_lines_visitor]) + + return change_summary_visitor.summary, create_lines_visitor.lines + + @cached_property + def change_summary(self): + return self._calculated_changes_and_lines[0] + + @property + def has_changes(self): + return any(self.change_summary.values()) + + @cached_property + def lines(self): + if self.total_diff_length > MAX_DIFF_SIZE and not self.bypass_max_diff: + return None + return self._calculated_changes_and_lines[1] + + @cached_property + def segments(self): + return Segment.segments(self) + + +class Comparison(object): + def __init__(self, user, base_commit, head_commit): + # TODO: rename to owner + self.user = user + self._base_commit = base_commit + self._head_commit = head_commit + + @cached_property + def _adapter(self) -> TorngitBaseAdapter: + return RepoProviderService().get_adapter( + owner=self.user, repo=self.base_commit.repository + ) + + def validate(self): + # make sure head and base reports exist (will throw an error if not) + self.head_report + self.base_report + + @cached_property + def base_commit(self): + return self._base_commit + + @cached_property + def head_commit(self): + return self._head_commit + + @cached_property + def files(self): + for file_name in self.head_report.files: + yield self.get_file_comparison(file_name) + + def get_file_comparison(self, file_name, with_src=False, bypass_max_diff=False): + head_file = self.head_report.get(file_name) + diff_data = self.git_comparison["diff"]["files"].get(file_name) + + if self.base_report is not None: + base_file = self.base_report.get(file_name) + if base_file is None and diff_data: + base_file = self.base_report.get(diff_data.get("before")) + else: + base_file = None + + if with_src: + file_content = async_to_sync(self._adapter.get_source)( + file_name, self.head_commit.commitid + )["content"] + # make sure the file is str utf-8 + if not isinstance(file_content, str): + file_content = str(file_content, "utf-8") + src = file_content.splitlines() + else: + src = [] + + return FileComparison( + base_file=base_file, + head_file=head_file, + diff_data=diff_data, + src=src, + bypass_max_diff=bypass_max_diff, + ) + + @cached_property + def git_comparison(self) -> dict: + """ + Fetches comparison, and caches the result. + """ + return async_to_sync(self._adapter.get_compare)( + self.base_commit.commitid, self.head_commit.commitid + ) + + @cached_property + def base_report(self): + try: + return report_service.build_report_from_commit(self.base_commit) + except minio.error.S3Error as e: + if e.code == "NoSuchKey": + raise MissingComparisonReport("Missing base report") + else: + raise e + + @cached_property + def head_report(self): + try: + report = report_service.build_report_from_commit(self.head_commit) + except minio.error.S3Error as e: + if e.code == "NoSuchKey": + raise MissingComparisonReport("Missing head report") + else: + raise e + + # Return the old report if the github API call fails for any reason + try: + report.apply_diff(self.git_comparison["diff"]) + except Exception: + pass + return report + + @cached_property + def head_report_without_applied_diff(self): + """ + This is a variant to the head_report property without having an applied diff. + This is created because applying the diff calls the provider, which adds a + diff_totals key to the head_report object as well as adjusting the diff_total + values in each session. This variant should only be used if you are not using + any diff related data, as it saves an unnecessary request to the provider otherwise. + """ + try: + report = report_service.build_report_from_commit(self.head_commit) + except minio.error.S3Error as e: + if e.code == "NoSuchKey": + raise MissingComparisonReport("Missing head report") + else: + raise e + + return report + + @cached_property + def has_different_number_of_head_and_base_sessions(self): + """ + This method checks if the head and the base have different number of sessions. + It makes use of the head_report_without_applied_diff property instead of the + head_report one as it doesn't need diff related data for this computation (see + the description of that property above for more context). + This method should be replaced with a direct call to the report_uploads table instead, + but leaving the implementation the same for now for consistency. + """ + try: + head_sessions = self.head_report_without_applied_diff.sessions + base_sessions = self.base_report.sessions + except Exception: + return False + + # We're treating this case as false since considering CFF's complicates the logic + if self._has_cff_sessions(head_sessions) or self._has_cff_sessions( + base_sessions + ): + return False + return len(head_sessions) != len(base_sessions) + + # I feel this method should belong to the API Report class, but we're thinking of getting rid of that class soon + # In truth, this should be in the shared.Report class + def _has_cff_sessions(self, sessions) -> bool: + for session in sessions.values(): + if session.session_type.value == "carriedforward": + return True + return False + + @property + def totals(self): + return { + "base": self.base_report.totals if self.base_report is not None else None, + "head": self.head_report.totals if self.head_report is not None else None, + "diff": self.git_comparison["diff"].get("totals"), + } + + @property + def git_commits(self): + return self.git_comparison["commits"] + + @property + def upload_commits(self): + """ + Returns the commits that have uploads between base and head. + :return: Queryset of core.models.Commit objects + """ + commit_ids = [commit["commitid"] for commit in self.git_commits] + commits_queryset = Commit.objects.filter( + commitid__in=commit_ids, repository=self.base_commit.repository + ) + commits_queryset.exclude(deleted=True) + return commits_queryset + + def flag_comparison(self, flag_name): + return FlagComparison(self, flag_name) + + @property + def non_carried_forward_flags(self): + flags_dict = self.head_report.flags + return [flag for flag, vals in flags_dict.items() if not vals.carriedforward] + + +class FlagComparison(object): + def __init__(self, comparison, flag_name): + self.comparison = comparison + self.flag_name = flag_name + + @cached_property + def head_report(self): + return self.comparison.head_report.flags.get(self.flag_name) + + @cached_property + def base_report(self): + return self.comparison.base_report.flags.get(self.flag_name) + + @cached_property + def diff_totals(self): + if self.head_report is None: + return None + git_comparison = self.comparison.git_comparison + return self.head_report.apply_diff(git_comparison["diff"]) + + +@dataclass +class ImpactedFile: + @dataclass + class Totals(ReportTotals): + def __post_init__(self): + nb_branches = self.hits + self.misses + self.partials + self.coverage = (100 * self.hits / nb_branches) if nb_branches > 0 else None + + base_name: Optional[str] = None # will be `None` for created files + head_name: Optional[str] = None # will be `None` for deleted files + file_was_added_by_diff: bool = False + file_was_removed_by_diff: bool = False + base_coverage: Optional[Totals] = None # will be `None` for created files + head_coverage: Optional[Totals] = None # will be `None` for deleted files + + # lists of (line number, coverage) tuples + added_diff_coverage: Optional[List[tuple[int, str]]] = None + removed_diff_coverage: Optional[List[tuple[int, str]]] = field(default_factory=list) + unexpected_line_changes: Optional[List[tuple[int, str]]] = field( + default_factory=list + ) + + lines_only_on_base: List[int] = field(default_factory=list) + lines_only_on_head: List[int] = field(default_factory=list) + + @classmethod + def create(cls, **kwargs): + base_coverage = kwargs.pop("base_coverage") + head_coverage = kwargs.pop("head_coverage") + return cls( + **kwargs, + base_coverage=ImpactedFile.Totals(**base_coverage) + if base_coverage + else None, + head_coverage=ImpactedFile.Totals(**head_coverage) + if head_coverage + else None, + ) + + @cached_property + def has_diff(self) -> bool: + """ + Returns `True` if the file has any additions or removals in the diff + """ + return bool( + self.added_diff_coverage + and len(self.added_diff_coverage) > 0 + or self.removed_diff_coverage + and len(self.removed_diff_coverage) > 0 + or self.file_was_added_by_diff + or self.file_was_removed_by_diff + ) + + @cached_property + def has_changes(self) -> bool: + """ + Returns `True` if the file has any unexpected changes + """ + return ( + self.unexpected_line_changes is not None + and len(self.unexpected_line_changes) > 0 + ) + + @cached_property + def misses_count(self) -> int: + total_misses = 0 + total_misses += self._direct_misses_count + total_misses += self._unintended_misses_count + return total_misses + + @cached_property + def _unintended_misses_count(self) -> int: + """ + Returns the misses count for a unintended impacted file + """ + misses = 0 + + unexpected_line_changes = self.unexpected_line_changes or [] + for [ + base, + [head_line_number, head_coverage_value], + ] in unexpected_line_changes: + if head_coverage_value == "m": + misses += 1 + + return misses + + @cached_property + def _direct_misses_count(self) -> int: + """ + Returns the misses count for a direct impacted file + """ + + misses = 0 + + diff_coverage = self.added_diff_coverage or [] + for line_number, line_coverage_value in diff_coverage: + if line_coverage_value == "m": + misses += 1 + + return misses + + @cached_property + def patch_coverage(self) -> Optional[Totals]: + """ + Sums of hits, misses and partials in the diff + """ + if self.added_diff_coverage and len(self.added_diff_coverage) > 0: + hits, misses, partials = (0, 0, 0) + for added_coverage in self.added_diff_coverage: + [_, type_coverage] = added_coverage + if type_coverage == "h": + hits += 1 + if type_coverage == "m": + misses += 1 + if type_coverage == "p": + partials += 1 + + return ImpactedFile.Totals(hits=hits, misses=misses, partials=partials) + + @cached_property + def change_coverage(self) -> Optional[float]: + if ( + self.base_coverage + and self.base_coverage.coverage + and self.head_coverage + and self.head_coverage.coverage + ): + return float( + float(self.head_coverage.coverage or 0) + - float(self.base_coverage.coverage or 0) + ) + + @cached_property + def file_name(self) -> Optional[str]: + if self.head_name: + parts = self.head_name.split("/") + return parts[-1] + + +@dataclass +class ComparisonReport(object): + """ + This is a wrapper around the data computed by the worker's commit comparison task. + The raw data is stored in blob storage and accessible via the `report_storage_path` + on a `CommitComparison` + """ + + commit_comparison: CommitComparison = None + + @cached_property + def files(self) -> List[ImpactedFile]: + if not self.commit_comparison.report_storage_path: + return [] + + comparison_data = self._fetch_raw_comparison_data() + return [ + ImpactedFile.create(**data) for data in comparison_data.get("files", []) + ] + + def impacted_file(self, path: str) -> Optional[ImpactedFile]: + for file in self.files: + if file.head_name == path: + return file + + @cached_property + def impacted_files(self) -> List[ImpactedFile]: + return self.files + + @cached_property + def impacted_files_with_unintended_changes(self) -> List[ImpactedFile]: + return [file for file in self.files if file.has_changes] + + @cached_property + def impacted_files_with_direct_changes(self) -> List[ImpactedFile]: + return [file for file in self.files if file.has_diff or not file.has_changes] + + def _fetch_raw_comparison_data(self) -> dict: + """ + Fetches the raw comparison data from storage + """ + repository = self.commit_comparison.compare_commit.repository + archive_service = ArchiveService(repository) + try: + data = archive_service.read_file(self.commit_comparison.report_storage_path) + return json.loads(data) + except Exception: + log.error( + "ComparisonReport - couldn't fetch data from storage", exc_info=True + ) + return {} + + +class PullRequestComparison(Comparison): + """ + A Comparison instantiated with a Pull. Contains relevant additional processing + required for Pulls, including caching of files-with-changes and support for + 'pseudo-comparisons'. + """ + + def __init__(self, user, pull): + self.pull = pull + super().__init__( + user=user, + # these are lazy loaded in the property methods below + base_commit=None, + head_commit=None, + ) + + # TODO: try using the dataloader to fetch the commits before you create this class, and pass those commits + # to the constructor + @cached_property + def base_commit(self): + try: + return Commit.objects.defer("_report").get( + repository=self.pull.repository, + commitid=self.pull.compared_to + if self.is_pseudo_comparison + else self.pull.base, + ) + except Commit.DoesNotExist: + raise MissingComparisonCommit("Missing base commit") + + @cached_property + def head_commit(self): + try: + return Commit.objects.defer("_report").get( + repository=self.pull.repository, commitid=self.pull.head + ) + except Commit.DoesNotExist: + raise MissingComparisonCommit("Missing head commit") + + @cached_property + def _files_with_changes_hash_key(self): + return "/".join( + ( + "compare-changed-files", + self.pull.repository.author.service, + self.pull.repository.author.username, + self.pull.repository.name, + f"{self.pull.pullid}", + ) + ) + + @cached_property + def _files_with_changes(self): + try: + key = self._files_with_changes_hash_key + changes = json.loads(redis.get(key) or json.dumps(None)) + log.info( + f"Found {len(changes) if changes else 0} files with changes in cache.", + extra=dict(repoid=self.pull.repository.repoid, pullid=self.pull.pullid), + ) + return changes + except OSError as e: + log.warning( + f"Error connecting to redis: {e}", + extra=dict(repoid=self.pull.repository.repoid, pullid=self.pull.pullid), + ) + + def _set_files_with_changes_in_cache(self, files_with_changes): + redis.set( + self._files_with_changes_hash_key, + json.dumps(files_with_changes), + ex=86400, # 1 day in seconds + ) + log.info( + f"Stored {len(files_with_changes)} files with changes in cache", + extra=dict(repoid=self.pull.repository.repoid, pullid=self.pull.pullid), + ) + + @cached_property + def files(self): + """ + Overrides the 'files' property to do additional caching of + 'files_with_changes', for future performance improvements. + """ + files_with_changes = [] + for file_comparison in super().files: + if file_comparison.change_summary: + files_with_changes.append(file_comparison.name["head"]) + yield file_comparison + self._set_files_with_changes_in_cache(files_with_changes) + + def get_file_comparison(self, file_name, with_src=False, bypass_max_diff=False): + """ + Overrides the 'get_file_comparison' method to set the "should_search_for_changes" + field. + """ + file_comparison = super().get_file_comparison( + file_name, with_src=with_src, bypass_max_diff=bypass_max_diff + ) + file_comparison.should_search_for_changes = ( + file_name in self._files_with_changes + if self._files_with_changes is not None + else None + ) + return file_comparison + + @cached_property + def is_pseudo_comparison(self): + """ + Returns True if this comparison is a pseudo-comparison, False if not. + + Depends on + 1) The repository yaml or app yaml settings allow pseudo_comparisons + 2) the pull request's 'compared_to' field is defined + """ + return walk( + _dict=self.pull.repository.yaml, + keys=("codecov", "allow_pseudo_compare"), + _else=get_config(("site", "codecov", "allow_pseudo_compare"), default=True), + ) and bool(self.pull.compared_to) + + @cached_property + def pseudo_diff(self): + """ + Returns the diff between the 'self.pull.compared_to' field and the + 'self.pull.base' field. + """ + adapter = RepoProviderService().get_adapter(self.user, self.pull.repository) + return async_to_sync(adapter.get_compare)( + self.pull.compared_to, self.pull.base + )["diff"] + + @cached_property + def pseudo_diff_adjusts_tracked_lines(self): + """ + Returns True if we are doing a pull request pseudo-comparison, and tracked + lines have changed between the pull's 'base' and 'compared_to' fields. This + signifies an error-condition for the comparison, I think because if tracked lines + have been adjusted between the 'base' and 'compared_to' commits, the 'compared_to' + report can't be substituted for the 'base' report, since it will throw off the + unexpected coverage change results. If `self.allow_coverage_offests` is True, + client code can adjust the lines in the base report according to the diff + with `self.update_base_report_with_pseudo_diff'. + + Ported from the block at: https://github.com/codecov/codecov.io/blob/master/app/handlers/compare.py#L137 + """ + if ( + self.is_pseudo_comparison + and self.pull.base != self.pull.compared_to + and self.base_report is not None + and self.head_report is not None + ): + if self.pseudo_diff and self.pseudo_diff.get("files"): + return self.base_report.does_diff_adjust_tracked_lines( + self.pseudo_diff, + future_report=self.head_report, + future_diff=self.git_comparison["diff"], + ) + return False + + def update_base_report_with_pseudo_diff(self): + self.base_report.shift_lines_by_diff(self.pseudo_diff, forward=True) + + +class CommitComparisonService: + """ + Utilities for determining whether a commit comparison needs to be recomputed + (and enqueueing that recompute when necessary), and fetching associated comparisons + for pulls + """ + + def __init__(self, commit_comparison: CommitComparison): + self.commit_comparison = commit_comparison + + @cached_property + def base_commit(self): + if "base_commit" not in self.commit_comparison._state.fields_cache: + # base_commit is already preloaded + self.commit_comparison.base_commit = self._load_commit( + self.commit_comparison.base_commit_id + ) + return self.commit_comparison.base_commit + + @cached_property + def compare_commit(self): + if "compare_commit" not in self.commit_comparison._state.fields_cache: + # compare_commit is already preloaded + self.commit_comparison.compare_commit = self._load_commit( + self.commit_comparison.compare_commit_id + ) + return self.commit_comparison.compare_commit + + def needs_recompute(self) -> bool: + if self._last_updated_before(self.compare_commit.updatestamp): + return True + + if self._last_updated_before(self.base_commit.updatestamp): + return True + + return False + + def _last_updated_before(self, timestamp: datetime) -> bool: + """ + Returns true if the given timestamp occurred after the commit comparison's last update + """ + timezone = pytz.utc + if not timestamp: + return False + + if timestamp.tzinfo is None: + timestamp = timezone.localize(timestamp) + else: + timestamp = timezone.normalize(timestamp) + + return timezone.normalize(self.commit_comparison.updated_at) < timestamp + + def _load_commit(self, commit_id: int) -> Optional[Commit]: + prefetch = Prefetch( + "reports", + queryset=CommitReport.objects.coverage_reports().filter(code=None), + ) + return ( + Commit.objects.filter(pk=commit_id) + .prefetch_related(prefetch) + .defer("_report") + .first() + ) + + @staticmethod + def get_commit_comparison_for_pull(obj: Pull) -> Optional[CommitComparison]: + comparison_qs = CommitComparison.objects.filter( + base_commit__commitid=obj.compared_to, + compare_commit__commitid=obj.head, + base_commit__repository_id=obj.repository_id, + compare_commit__repository_id=obj.repository_id, + ).select_related("compare_commit", "base_commit") + return comparison_qs.first() + + @classmethod + def fetch_precomputed(self, repo_id: int, keys: List[Tuple]) -> QuerySet: + comparison_table = CommitComparison._meta.db_table + commit_table = Commit._meta.db_table + queryset = CommitComparison.objects.raw( + f""" + select + {comparison_table}.*, + base_commit.commitid as base_commitid, + compare_commit.commitid as compare_commitid + from {comparison_table} + inner join {commit_table} base_commit + on base_commit.id = {comparison_table}.base_commit_id and base_commit.repoid = {repo_id} + inner join {commit_table} compare_commit + on compare_commit.id = {comparison_table}.compare_commit_id and compare_commit.repoid = {repo_id} + where (base_commit.commitid, compare_commit.commitid) in %s + """, + [tuple(keys)], + ) + + # we need to make sure we're performing the query against the primary database + # (and not the read replica) since we may have just inserted new comparisons + # that we'd like to ensure are returned here + return queryset.using("default") diff --git a/apps/codecov-api/services/components.py b/apps/codecov-api/services/components.py new file mode 100644 index 0000000000..afd0be6e4d --- /dev/null +++ b/apps/codecov-api/services/components.py @@ -0,0 +1,135 @@ +from datetime import datetime +from typing import Any, Dict, Iterable, List, Optional + +from django.utils.functional import cached_property +from shared.components import Component +from shared.reports.filtered import FilteredReport +from shared.reports.resources import Report +from shared.reports.types import ReportTotals + +from codecov_auth.models import Owner +from core.models import Commit +from services.comparison import Comparison +from services.yaml import final_commit_yaml +from timeseries.helpers import fill_sparse_measurements +from timeseries.models import Interval + + +def commit_components(commit: Commit, owner: Owner | None) -> List[Component]: + """ + Get the list of components for a commit. + A request is made to the provider on behalf of the given `owner` + to fetch the commit YAML (from which component config is parsed). + """ + yaml = final_commit_yaml(commit, owner) + return yaml.get_components() + + +def component_filtered_report( + report: Report, components: List[Component] +) -> FilteredReport: + """ + Filter a report such that the totals, etc. are only pertaining to the given component. + """ + flags, paths = [], [] + report_flags = report.get_flag_names() if report else [] + for component in components: + flags.extend(component.get_matching_flags(report_flags)) + paths.extend(component.paths) + filtered_report = report.filter(flags=flags, paths=paths) + return filtered_report + + +def filter_components_by_name_or_id( + components: List[Component], terms: List[str] +) -> List[Component]: + """ + Given a list of Components and a list of strings (terms), + return a new list of Components only including Components with names in terms (case insensitive) + OR component_id in terms (case insensitive) + """ + terms = [v.lower() for v in terms] + return [ + component + for component in components + if component.name.lower() in terms or component.component_id.lower() in terms + ] + + +class ComponentComparison: + def __init__(self, comparison: Comparison, component: Component): + self.comparison = comparison + self.component = component + + @cached_property + def base_report(self) -> FilteredReport: + return component_filtered_report(self.comparison.base_report, [self.component]) + + @cached_property + def head_report(self) -> FilteredReport: + return component_filtered_report(self.comparison.head_report, [self.component]) + + @cached_property + def base_totals(self) -> ReportTotals: + return self.base_report.totals + + @cached_property + def head_totals(self) -> ReportTotals: + return self.head_report.totals + + @cached_property + def patch_totals(self) -> ReportTotals: + git_comparison = self.comparison.git_comparison + return self.head_report.apply_diff(git_comparison["diff"]) + + +class ComponentMeasurements: + def __init__( + self, + raw_measurements: List[dict], + component_id: str, + interval: Interval, + after: datetime, + before: datetime, + last_measurement: datetime, + components_mapping: Dict[str, str], + ): + self.raw_measurements = raw_measurements + self.component_id = component_id + self.interval = interval + self.after = after + self.before = before + self.last_measurement = last_measurement + self.components_mapping = components_mapping + + @cached_property + def name(self) -> str: + if self.components_mapping.get(self.component_id): + return self.components_mapping[self.component_id] + return self.component_id + + @cached_property + def component_id(self) -> str: + return self.component_id + + @cached_property + def percent_covered(self) -> Optional[float]: + if len(self.raw_measurements) > 0: + return self.raw_measurements[-1]["avg"] + + @cached_property + def percent_change(self) -> Optional[float]: + if len(self.raw_measurements) > 1: + return self.raw_measurements[-1]["avg"] - self.raw_measurements[0]["avg"] + + @cached_property + def measurements(self) -> Iterable[Dict[str, Any]]: + if not self.raw_measurements: + return [] + return fill_sparse_measurements( + self.raw_measurements, self.interval, self.after, self.before + ) + + @cached_property + def last_uploaded(self) -> datetime: + return self.last_measurement diff --git a/apps/codecov-api/services/decorators.py b/apps/codecov-api/services/decorators.py new file mode 100644 index 0000000000..47244ae2f9 --- /dev/null +++ b/apps/codecov-api/services/decorators.py @@ -0,0 +1,39 @@ +import stripe +from rest_framework.exceptions import APIException +from shared.torngit.exceptions import TorngitClientError + + +def torngit_safe(method): + """ + Translatess torngit exceptions into DRF APIExceptions. + For use in DRF views. + """ + + def exec_method(*args, **kwargs): + try: + return method(*args, **kwargs) + except TorngitClientError as e: + exception = APIException(detail=e.message) + exception.status_code = e.code + raise exception + + # This is needed to decorate custom DRF viewset actions + exec_method.__name__ = method.__name__ + return exec_method + + +def stripe_safe(method): + """ + Translates stripe-api errors into DRF APIExceptions. + """ + + def exec_method(*args, **kwargs): + try: + return method(*args, **kwargs) + except stripe.StripeError as e: + exception = APIException(detail=e.user_message) + exception.status_code = e.http_status + raise exception + + exec_method.__name__ = method.__name__ + return exec_method diff --git a/apps/codecov-api/services/path.py b/apps/codecov-api/services/path.py new file mode 100644 index 0000000000..ea9295464f --- /dev/null +++ b/apps/codecov-api/services/path.py @@ -0,0 +1,276 @@ +from __future__ import annotations + +from collections import defaultdict +from dataclasses import dataclass +from functools import cached_property +from typing import Iterable + +import sentry_sdk +from asgiref.sync import async_to_sync +from django.conf import settings +from shared.reports.filtered import FilteredReport, FilteredReportFile +from shared.reports.resources import Report +from shared.reports.types import ReportTotals +from shared.torngit.exceptions import TorngitClientError + +from codecov_auth.models import Owner +from core.models import Commit +from services.repo_providers import RepoProviderService + + +class PathNode: + """ + Generic node in a file/directory tree that has coverage totals. + Expects a `totals: ReportTotals` attribute to be set. + """ + + @property + def name(self) -> str: + return self.full_path.split("/")[-1] + + @property + def lines(self) -> int: + return self.totals.lines or 0 + + @property + def hits(self) -> int: + return self.totals.hits or 0 + + @property + def partials(self) -> int: + return self.totals.partials or 0 + + @property + def misses(self) -> int: + return self.totals.misses or 0 + + @property + def coverage(self) -> float: + if self.lines > 0: + return float(self.hits / self.lines) * 100 + else: + return 0.0 + + +@dataclass +class File(PathNode): + """ + File node in a file/directory tree. + """ + + full_path: str + totals: ReportTotals + + +@dataclass +class Dir(PathNode): + """ + Directory node in a file/directory tree. + """ + + full_path: str + children: list[File | Dir] + + @cached_property + def totals(self) -> ReportTotals: + # A dir's totals are sum of its children's totals + totals = ReportTotals.default_totals() + for child in self.children: + totals.lines += child.lines + totals.hits += child.hits + totals.partials += child.partials + totals.misses += child.misses + return totals + + +@dataclass +class PrefixedPath: + full_path: str + prefix: str + + @property + def relative_path(self) -> str: + """ + The path relative to the `prefix`. For example, if `full_path` + is `a/b/c/d.txt` and `prefix` is `a/b` then this method would return `c/d.txt`. + """ + if not self.prefix: + return self.full_path + else: + return self.full_path.removeprefix(f"{self.prefix}/") + + @property + def is_file(self) -> bool: + parts = self.relative_path.split("/") + return len(parts) == 1 + + @property + def basename(self) -> str: + """ + The base path name (including the prefix). For example, if `full_path` + is `a/b/c/d.txt` and `prefix` is `a/b` then this method would return `a/b/c`. + """ + name = self.relative_path.split("/", 1)[0] + if self.prefix: + return f"{self.prefix}/{name}" + else: + return name + + +def is_subpath(full_path: str, subpath: str) -> bool: + if not subpath: + return True + return full_path.startswith(f"{subpath}/") or full_path == subpath + + +def dashboard_commit_file_url( + path: str | None, + service: str, + owner: str, + repo: str, + commit: Commit, +) -> str: + if path is None: + path = "" + report = commit.full_report + is_file = report and path in report.files + commit_path = f"blob/{path}" if is_file else f"tree/{path}" + return f"{settings.CODECOV_DASHBOARD_URL}/{service}/{owner}/{repo}/commit/{commit.commitid}/{commit_path}" + + +class ReportPaths: + """ + Contains methods for getting path information out of a single report. + """ + + @sentry_sdk.trace + def __init__( + self, + report: Report, + path: str | None = None, + search_term: str | None = None, + filter_flags: list[str] = None, + filter_paths: list[str] = None, + ): + self.report: Report | FilteredReport = report + self.filter_flags = filter_flags or [] + self.filter_paths = filter_paths or [] + self.prefix = path or "" + + # Filter report if flags or paths exist + if self.filter_flags or self.filter_paths: + self.report = self.report.filter( + paths=self.filter_paths, flags=self.filter_flags + ) + + self._paths = [ + PrefixedPath(full_path=full_path, prefix=self.prefix) + for full_path in self.files + if is_subpath(full_path, self.prefix) + ] + + if search_term: + search_term = search_term.lower() + self._paths = [ + path + for path in self._paths + if search_term in path.relative_path.lower() + ] + + @cached_property + def files(self) -> list[str]: + # No flags filtering, just return (path-filtered) files in Report + if not self.filter_flags: + return self.report.files + + # When there is a flag filter, `FilteredReport` currently yields + # `FilteredReportFile`s without actually checking whether they match the sessions. + # Before that bug is fixed, lets do the filtering manually here. Once that bug is fixed, + # this should just forward to `self.report.files` like above. + files = [] + for file in self.report: + if isinstance(file, FilteredReportFile): + found = False + for _ln, line in file.lines: + if line and any(s.id in file.session_ids for s in line.sessions): + found = True + break + if not found: + continue + + files.append(file.name) + return files + + @property + def paths(self) -> list[PrefixedPath]: + return self._paths + + @sentry_sdk.trace + def full_filelist(self) -> list[File | Dir]: + """ + Return a flat file list of all files under the specified `path` prefix/directory. + """ + return [ + File(full_path=path.full_path, totals=self._totals(path)) + for path in self.paths + ] + + @sentry_sdk.trace + def single_directory(self) -> list[File | Dir]: + """ + Return a single directory (specified by `path`) of mixed file/directory results. + """ + return self._single_directory_recursive(self.paths) + + def _totals(self, path: PrefixedPath) -> ReportTotals: + """ + Returns the report totals for a given prefixed path. + """ + # Fixes an issue when filtering by flags does not work in the case where + # one flag covers half of the file and another flag covers another half. + # Using get_file_totals will return the totals for coverage of all flags + # applied to the file instead of just the filter flags being queried + if self.filter_flags: + return self.report.get(path.full_path).totals + else: + return self.report.get_file_totals(path.full_path) + + def _single_directory_recursive( + self, paths: Iterable[PrefixedPath] + ) -> list[File | Dir]: + grouped = defaultdict(list) + for path in paths: + grouped[path.basename].append(path) + + results: list[File | Dir] = [] + + for basename, paths in grouped.items(): + if len(paths) == 1 and paths[0].is_file: + path = paths[0] + results.append( + File(full_path=path.full_path, totals=self._totals(path)) + ) + else: + children = self._single_directory_recursive( + PrefixedPath(full_path=path.full_path, prefix=basename) + for path in paths + ) + results.append(Dir(full_path=basename, children=children)) + + return results + + +def provider_path_exists(path: str, commit: Commit, owner: Owner) -> bool | None: + """ + Check whether the given path exists on the provider. + """ + try: + adapter = RepoProviderService().get_adapter(owner, commit.repository) + async_to_sync(adapter.list_files)(commit.commitid, path) + return True + except TorngitClientError as e: + if e.code == 404: + return False + else: + # more generic error from provider + return None diff --git a/apps/codecov-api/services/refresh.py b/apps/codecov-api/services/refresh.py new file mode 100644 index 0000000000..c2fc920875 --- /dev/null +++ b/apps/codecov-api/services/refresh.py @@ -0,0 +1,49 @@ +from json import dumps, loads + +from celery.result import result_from_tuple +from shared.helpers.redis import get_redis_connection + +from services.task import TaskService, celery_app + + +class RefreshService(object): + def __init__(self): + self.task_service = TaskService() + self.redis = get_redis_connection() + + def _get_key_name(self, ownerid): + return f"refresh_{ownerid}" + + def is_refreshing(self, ownerid): + key_name = self._get_key_name(ownerid) + data_task = self.redis.get(key_name) + if not data_task: + return False + try: + res = result_from_tuple(loads(data_task), app=celery_app) + except ValueError: + self.redis.delete(key_name) + return False + has_failed = res.failed() or (res.parent and res.parent.failed()) + if res.successful() or has_failed: + self.redis.delete(key_name) + return False + # task is not success, nor failed, so probably pending or in progress + return True + + def trigger_refresh( + self, + ownerid, + username, + sync_teams=True, + sync_repos=True, + using_integration=False, + manual_trigger=False, + ): + if self.is_refreshing(ownerid): + return + resp = self.task_service.refresh( + ownerid, username, sync_repos, sync_teams, using_integration, manual_trigger + ) + # store in redis the task data to be used for `is_refreshing` logic + self.redis.setex(self._get_key_name(ownerid), 900, dumps(resp.as_tuple())) diff --git a/apps/codecov-api/services/repo_providers.py b/apps/codecov-api/services/repo_providers.py new file mode 100644 index 0000000000..da25e7cf33 --- /dev/null +++ b/apps/codecov-api/services/repo_providers.py @@ -0,0 +1,200 @@ +import logging +from os import getenv +from typing import Callable, Dict, Optional + +from asgiref.sync import sync_to_async +from django.conf import settings +from shared.encryption.token import encode_token +from shared.torngit import get + +from codecov_auth.models import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, + Owner, + Service, +) +from core.models import Repository +from utils.config import get_config +from utils.encryption import encryptor + +log = logging.getLogger(__name__) + + +class TorngitInitializationFailed(Exception): + """ + Exception when initializing the torngit provider object. + """ + + pass + + +def get_token_refresh_callback( + owner: Optional[Owner], service: Service +) -> Callable[[Dict], None]: + """ + Produces a callback function that will encode and update the oauth token of an owner. + This callback is passed to the TorngitAdapter for the service. + """ + if owner is None: + return None + if service == Service.BITBUCKET or service == Service.BITBUCKET_SERVER: + return None + + @sync_to_async + def callback(new_token: Dict) -> None: + log.info( + "Saving new token after refresh", + extra=dict(owner=owner.username, ownerid=owner.ownerid), + ) + string_to_save = encode_token(new_token) + owner.oauth_token = encryptor.encode(string_to_save).decode() + owner.save() + + return callback + + +def get_generic_adapter_params( + owner: Optional[Owner], service, use_ssl=False, token=None +): + if use_ssl: + verify_ssl = ( + get_config(service, "ssl_pem") + if get_config(service, "verify_ssl") is not False + else getenv("REQUESTS_CA_BUNDLE") + ) + else: + verify_ssl = None + + if token is None: + if owner is not None and owner.oauth_token is not None: + token = encryptor.decrypt_token(owner.oauth_token) + token["username"] = owner.username + else: + token = {"key": getattr(settings, f"{service.upper()}_BOT_KEY")} + return dict( + verify_ssl=verify_ssl, + token=token, + timeouts=(5, 15), + oauth_consumer_token=dict( + key=getattr(settings, f"{service.upper()}_CLIENT_ID", "unknown"), + secret=getattr(settings, f"{service.upper()}_CLIENT_SECRET", "unknown"), + ), + # By checking the "username" in token we can know if the token belongs to an Owner + # We only try to refresh user-to-server tokens (e.g. belongs to owner) + on_token_refresh=( + get_token_refresh_callback(owner, service) if "username" in token else None + ), + ) + + +def get_provider(service, adapter_params): + provider = get(service, **adapter_params) + if provider: + return provider + else: + raise TorngitInitializationFailed() + + +def get_ghapp_default_installation( + owner: Optional[Owner], +) -> Optional[GithubAppInstallation]: + if owner is None or owner.service not in [ + Service.GITHUB.value, + Service.GITHUB_ENTERPRISE.value, + ]: + return None + return owner.github_app_installations.filter( + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME + ).first() + + +async def async_get_ghapp_default_installation( + owner: Optional[Owner], +) -> Optional[GithubAppInstallation]: + if owner is None or owner.service not in [ + Service.GITHUB.value, + Service.GITHUB_ENTERPRISE.value, + ]: + return None + return await owner.github_app_installations.filter( + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME + ).afirst() + + +class RepoProviderService(object): + def _is_using_integration( + self, ghapp_installation: GithubAppInstallation, repo: Repository + ) -> bool: + if ghapp_installation: + return ghapp_installation.is_repo_covered_by_integration(repo) + return repo.using_integration + + async def async_get_adapter( + self, owner: Optional[Owner], repo: Repository, use_ssl=False, token=None + ): + ghapp = await async_get_ghapp_default_installation(owner) + return self._get_adapter(owner, repo, ghapp=ghapp) + + def get_adapter( + self, owner: Optional[Owner], repo: Repository, use_ssl=False, token=None + ): + ghapp = get_ghapp_default_installation(owner) + return self._get_adapter(owner, repo, ghapp=ghapp, token=token) + + def _get_adapter( + self, + owner: Optional[Owner], + repo: Repository, + use_ssl=False, + token=None, + ghapp=None, + ): + """ + Return the corresponding implementation for calling the repository provider + + :param owner: :class:`codecov_auth.models.Owner` + :param repo: :class:`core.models.Repository` + :return: + :raises: TorngitInitializationFailed + """ + generic_adapter_params = get_generic_adapter_params( + owner, repo.author.service, use_ssl, token + ) + + owner_and_repo_params = { + "repo": { + "name": repo.name, + "using_integration": self._is_using_integration(ghapp, repo), + "service_id": repo.service_id, + "private": repo.private, + "repoid": repo.repoid, + }, + "owner": { + "username": repo.author.username, + "service_id": repo.author.service_id, + }, + } + + return get_provider( + repo.author.service, {**generic_adapter_params, **owner_and_repo_params} + ) + + def get_by_name(self, owner, repo_name, repo_owner_username, repo_owner_service): + """ + Return the corresponding implementation for calling the repository provider + + :param owner: Owner object of the user + :param repo_name: string, name of the repo + :param owner: Owner, owner of the repo in question + :repo_owner_service: 'github', 'gitlab' etc + :return: + :raises: TorngitInitializationFailed + """ + generic_adapter_params = get_generic_adapter_params(owner, repo_owner_service) + owner_and_repo_params = { + "repo": {"name": repo_name}, + "owner": {"username": repo_owner_username}, + } + return get_provider( + repo_owner_service, {**generic_adapter_params, **owner_and_repo_params} + ) diff --git a/apps/codecov-api/services/report.py b/apps/codecov-api/services/report.py new file mode 100644 index 0000000000..4e768c0646 --- /dev/null +++ b/apps/codecov-api/services/report.py @@ -0,0 +1,31 @@ +from shared.reports.resources import Report + + +def files_belonging_to_flags(commit_report: Report, flags: list[str]) -> list[str]: + flags_set = set(flags) + session_ids = { + sid + for sid, session in commit_report.sessions.items() + if session.flags and not set(session.flags).isdisjoint(flags_set) + } + files_in_specific_sessions = files_in_sessions( + commit_report=commit_report, session_ids=session_ids + ) + return files_in_specific_sessions + + +def files_in_sessions(commit_report: Report, session_ids: set[int]) -> list[str]: + files = [] + for file in commit_report: + found = False + for line in file: + if line: + for session in line.sessions: + if session.id in session_ids: + found = True + break + if found: + break + if found: + files.append(file.name) + return files diff --git a/apps/codecov-api/services/self_hosted.py b/apps/codecov-api/services/self_hosted.py new file mode 100644 index 0000000000..e2b5273873 --- /dev/null +++ b/apps/codecov-api/services/self_hosted.py @@ -0,0 +1,180 @@ +import operator +from functools import reduce +from typing import Optional + +from django.conf import settings +from django.db import transaction +from django.db.models import F, Func, Q, QuerySet +from shared.license import get_current_license + +from codecov_auth.models import Owner +from services import ServiceException +from utils.config import get_config + + +class LicenseException(ServiceException): + def __init__(self, message: str): + self.message = message + super().__init__(message) + + +def admin_owners() -> QuerySet: + """ + Returns a queryset of admin owners based on the YAML config: + + setup: + admins: + - service: + username: + - ... + """ + admins = get_config("setup", "admins", default=[]) + + filters = [ + Q(service=admin["service"], username=admin["username"]) + for admin in admins + if "service" in admin and "username" in admin + ] + + if len(filters) == 0: + return Owner.objects.none() + else: + return Owner.objects.filter(reduce(operator.or_, filters)) + + +def is_admin_owner(owner: Optional[Owner]) -> bool: + """ + Returns true if the given owner is an admin. + """ + return owner is not None and admin_owners().filter(pk=owner.pk).exists() + + +def activated_owners() -> QuerySet: + """ + Returns all owners that are activated in ANY org's `plan_activated_users` + across the entire instance. + """ + owner_ids = ( + Owner.objects.annotate( + plan_activated_owner_ids=Func( + F("plan_activated_users"), + function="unnest", + ) + ) + .values_list("plan_activated_owner_ids", flat=True) + .distinct() + ) + return Owner.objects.filter(pk__in=owner_ids) + + +def is_activated_owner(owner: Owner) -> bool: + """ + Returns true if the given owner is activated in this instance. + """ + return activated_owners().filter(pk=owner.pk).exists() + + +def license_seats() -> int: + """ + Max number of seats allowed by the current license. + """ + enterprise_license = get_current_license() + if not enterprise_license.is_valid: + return 0 + return enterprise_license.number_allowed_users or 0 + + +def enterprise_has_seats_left() -> bool: + """ + The activated_owner_query is heavy, so check the license first, only proceed if they have a valid license. + """ + license_seat_count = license_seats() + if license_seat_count == 0: + return False + owners = activated_owners() + count = owners.count() + return count < license_seat_count + + +def can_activate_owner(owner: Owner) -> bool: + """ + Returns true if there are available seats left for activation. + """ + if is_activated_owner(owner): + # user is already activated in at least 1 org + return True + else: + return activated_owners().count() < license_seats() + + +@transaction.atomic +def activate_owner(owner: Owner): + """ + Activate the given owner in ALL orgs that the owner is a part of. + """ + if not settings.IS_ENTERPRISE: + raise Exception("activate_owner is only available in self-hosted environments") + + if not can_activate_owner(owner): + raise LicenseException( + "No seats remaining. Please contact Codecov support or deactivate users." + ) + + Owner.objects.filter(pk__in=owner.organizations).update( + plan_activated_users=Func( + owner.pk, + function="array_append_unique", + template="%(function)s(plan_activated_users, %(expressions)s)", + ) + ) + + +def deactivate_owner(owner: Owner): + """ + Deactivate the given owner across ALL orgs. + """ + if not settings.IS_ENTERPRISE: + raise Exception( + "deactivate_owner is only available in self-hosted environments" + ) + + Owner.objects.filter( + plan_activated_users__contains=Func( + owner.pk, + function="array", + template="%(function)s[%(expressions)s]", + ) + ).update( + plan_activated_users=Func( + owner.pk, + function="array_remove", + template="%(function)s(plan_activated_users, %(expressions)s)", + ) + ) + + +def enable_autoactivation(): + """ + Enable auto-activation for the entire instance. + + There's no good place to store this instance-wide so we're just saving this + for all owners. + """ + Owner.objects.all().update(plan_auto_activate=True) + + +def disable_autoactivation(): + """ + Disable auto-activation for the entire instance. + + There's no good place to store this instance-wide so we're just saving this + for all owners. + """ + Owner.objects.all().update(plan_auto_activate=False) + + +def is_autoactivation_enabled(): + """ + Returns true if ANY org has auto-activation enabled. + """ + return Owner.objects.filter(plan_auto_activate=True).exists() diff --git a/apps/codecov-api/services/sentry.py b/apps/codecov-api/services/sentry.py new file mode 100644 index 0000000000..f78d3e7b1b --- /dev/null +++ b/apps/codecov-api/services/sentry.py @@ -0,0 +1,139 @@ +import json +import logging +from typing import Optional + +import jwt +from django.conf import settings +from django.db.utils import IntegrityError + +from codecov_auth.models import Owner +from services.task import TaskService + +log = logging.getLogger(__name__) + + +class SentryError(Exception): + pass + + +class SentryInvalidStateError(SentryError): + """ + The Sentry `state` JWT was malformed or not signed properly. + """ + + pass + + +class SentryUserAlreadyExistsError(SentryError): + """ + The Sentry `user_id` was already claimed by a Codecov owner. + """ + + pass + + +class SentryState: + def __init__(self, data: dict): + self.data = data + + @property + def user_id(self) -> Optional[str]: + return self.data.get("user_id") + + +def decode_state(state: str) -> Optional[SentryState]: + """ + Decode the given state (a JWT) using our shared secret. + Returns `None` if the state could not be decoded. + """ + secret = settings.SENTRY_JWT_SHARED_SECRET + try: + data = jwt.decode(state, secret, algorithms=["HS256"]) + return SentryState(data) + except jwt.exceptions.InvalidSignatureError: + # signed with a different secret + log.error( + "Sentry state has invalid signature", + extra=dict(sentry_state=state), + ) + return None + except jwt.exceptions.DecodeError: + # malformed JWT + log.error( + "Sentry state is malformed", + extra=dict(sentry_state=state), + ) + return None + + +def save_sentry_state(owner: Owner, encoded_state: str): + """ + If the given state decodes successfully then save it with the owner. + """ + decoded_state = decode_state(encoded_state) + if decoded_state is None: + log.error( + "Invalid Sentry state", + extra=dict(owner_id=owner.pk, sentry_state=encoded_state), + ) + raise SentryInvalidStateError() + + if decoded_state.user_id is not None: + owner.sentry_user_id = decoded_state.user_id + owner.sentry_user_data = decoded_state.data + + try: + owner.save() + except IntegrityError: + log.error( + "Sentry user already exists", + extra=dict( + owner_id=owner.pk, + sentry_user_id=decoded_state.user_id, + sentry_user_data=decoded_state.data, + ), + ) + raise SentryUserAlreadyExistsError() + + +def is_sentry_user(owner: Owner) -> bool: + """ + Returns true if the given owner has been linked with a Sentry user. + """ + return owner.sentry_user_id is not None + + +def send_user_webhook(user: Owner, org: Owner): + """ + Sends data back to Sentry about the Sentry <-> Codecov user link. + """ + assert is_sentry_user(user) + + webhook_url = settings.SENTRY_USER_WEBHOOK_URL + if webhook_url is None: + log.warning("No Sentry webhook URL is configured") + return + + state = { + "user_id": user.sentry_user_id, + "org_id": (user.sentry_user_data or {}).get("org_id"), + "codecov_owner_id": user.pk, + "codecov_organization_id": org.pk, + "service": org.service, + "service_id": org.service_id, + } + + secret = settings.SENTRY_JWT_SHARED_SECRET + encoded_state = jwt.encode(state, secret, algorithm="HS256") + + payload = json.dumps({"state": encoded_state}) + + TaskService().http_request( + url=webhook_url, + method="POST", + headers={ + "Content-Type": "application/json", + "User-Agent": "Codecov", + }, + data=payload, + ) diff --git a/apps/codecov-api/services/task/__init__.py b/apps/codecov-api/services/task/__init__.py new file mode 100644 index 0000000000..47396c6744 --- /dev/null +++ b/apps/codecov-api/services/task/__init__.py @@ -0,0 +1 @@ +from services.task.task import * diff --git a/apps/codecov-api/services/task/task.py b/apps/codecov-api/services/task/task.py new file mode 100644 index 0000000000..0c201bb4a2 --- /dev/null +++ b/apps/codecov-api/services/task/task.py @@ -0,0 +1,424 @@ +import logging +from datetime import datetime, timedelta +from typing import Iterable, List, Optional, Tuple + +import celery +from celery import Celery, chain, group, signature +from celery.canvas import Signature +from django.conf import settings +from sentry_sdk import set_tag +from sentry_sdk.integrations.celery import _wrap_apply_async +from shared import celery_config + +from core.models import Repository +from services.task.task_router import route_task +from timeseries.models import Dataset, MeasurementName + +celery_app = Celery("tasks") +celery_app.config_from_object("shared.celery_config:BaseCeleryConfig") + +log = logging.getLogger(__name__) + +if settings.SENTRY_ENV: + celery.group.apply_async = _wrap_apply_async(celery.group.apply_async) + celery.chunks.apply_async = _wrap_apply_async(celery.chunks.apply_async) + celery.canvas._chain.apply_async = _wrap_apply_async( + celery.canvas._chain.apply_async + ) + celery.canvas._chord.apply_async = _wrap_apply_async( + celery.canvas._chord.apply_async + ) + Signature.apply_async = _wrap_apply_async(Signature.apply_async) + + +class TaskService(object): + def _create_signature(self, name, args=None, kwargs=None, immutable=False): + """ + Create Celery signature + """ + queue_and_config = route_task(name, args=args, kwargs=kwargs) + queue_name = queue_and_config["queue"] + extra_config = queue_and_config.get("extra_config", {}) + celery_compatible_config = { + "time_limit": extra_config.get("hard_timelimit", None), + "soft_time_limit": extra_config.get("soft_timelimit", None), + } + headers = dict(created_timestamp=datetime.now().isoformat()) + set_tag("celery.queue", queue_name) + return signature( + name, + args=args, + kwargs=kwargs, + app=celery_app, + queue=queue_name, + headers=headers, + immutable=immutable, + **celery_compatible_config, + ) + + def schedule_task(self, task_name, *, kwargs, apply_async_kwargs): + return self._create_signature( + task_name, + kwargs=kwargs, + ).apply_async(**apply_async_kwargs) + + def compute_comparison(self, comparison_id): + self._create_signature( + celery_config.compute_comparison_task_name, + kwargs=dict(comparison_id=comparison_id), + ).apply_async() + + def compute_comparisons(self, comparison_ids: List[int]): + """ + Enqueue a batch of comparison tasks using a Celery group + """ + if len(comparison_ids) > 0: + queue_and_config = route_task( + celery_config.compute_comparison_task_name, + args=None, + kwargs=dict(comparison_id=comparison_ids[0]), + ) + celery_compatible_config = { + "queue": queue_and_config["queue"], + "time_limit": queue_and_config.get("extra_config", {}).get( + "hard_timelimit", None + ), + "soft_time_limit": queue_and_config.get("extra_config", {}).get( + "soft_timelimit", None + ), + } + signatures = [ + signature( + celery_config.compute_comparison_task_name, + args=None, + kwargs=dict(comparison_id=comparison_id), + app=celery_app, + **celery_compatible_config, + ) + for comparison_id in comparison_ids + ] + for comparison_id in comparison_ids: + # log each separately so it can be filtered easily in the logs + log.info( + "Triggering compute comparison task", + extra=dict(comparison_id=comparison_id), + ) + group(signatures).apply_async() + + def status_set_pending(self, repoid, commitid, branch, on_a_pull_request): + self._create_signature( + "app.tasks.status.SetPending", + kwargs=dict( + repoid=repoid, + commitid=commitid, + branch=branch, + on_a_pull_request=on_a_pull_request, + ), + ).apply_async() + + def upload_signature( + self, + repoid, + commitid, + report_type=None, + report_code=None, + arguments=None, + debug=False, + rebuild=False, + immutable=False, + ): + return self._create_signature( + "app.tasks.upload.Upload", + kwargs=dict( + repoid=repoid, + commitid=commitid, + report_type=report_type, + report_code=report_code, + arguments=arguments, + debug=debug, + rebuild=rebuild, + ), + immutable=immutable, + ) + + def upload( + self, + repoid, + commitid, + report_type=None, + report_code=None, + arguments=None, + countdown=0, + debug=False, + rebuild=False, + ): + return self.upload_signature( + repoid, + commitid, + report_type=report_type, + report_code=report_code, + arguments=arguments, + debug=debug, + rebuild=rebuild, + ).apply_async(countdown=countdown) + + def notify_signature(self, repoid, commitid, current_yaml=None, empty_upload=None): + return self._create_signature( + "app.tasks.notify.Notify", + kwargs=dict( + repoid=repoid, + commitid=commitid, + current_yaml=current_yaml, + empty_upload=empty_upload, + ), + ) + + def notify(self, repoid, commitid, current_yaml=None, empty_upload=None): + self.notify_signature( + repoid, commitid, current_yaml=current_yaml, empty_upload=empty_upload + ).apply_async() + + def pulls_sync(self, repoid, pullid): + self._create_signature( + "app.tasks.pulls.Sync", kwargs=dict(repoid=repoid, pullid=pullid) + ).apply_async() + + def refresh( + self, + ownerid, + username, + sync_teams=True, + sync_repos=True, + using_integration=False, + manual_trigger=False, + repos_affected: Optional[List[Tuple[str, str]]] = None, + ): + """ + Send sync_teams and/or sync_repos task message + If running both tasks on new worker, we create a chain with sync_teams to run + first so that when sync_repos starts it has the most up to date teams/groups + data for the user. Otherwise, we may miss some repos. + """ + chain_to_call = [] + if sync_teams: + chain_to_call.append( + self._create_signature( + "app.tasks.sync_teams.SyncTeams", + kwargs=dict( + ownerid=ownerid, + username=username, + using_integration=using_integration, + ), + ) + ) + + if sync_repos: + chain_to_call.append( + self._create_signature( + "app.tasks.sync_repos.SyncRepos", + kwargs=dict( + ownerid=ownerid, + username=username, + using_integration=using_integration, + manual_trigger=manual_trigger, + repository_service_ids=repos_affected, + ), + ) + ) + + return chain(*chain_to_call).apply_async() + + def sync_plans(self, sender=None, account=None, action=None): + self._create_signature( + celery_config.ghm_sync_plans_task_name, + kwargs=dict(sender=sender, account=account, action=action), + ).apply_async() + + def delete_owner(self, ownerid): + log.info(f"Triggering delete_owner task for owner: {ownerid}") + self._create_signature( + "app.tasks.delete_owner.DeleteOwner", kwargs=dict(ownerid=ownerid) + ).apply_async() + + def backfill_repo( + self, + repository: Repository, + start_date: datetime, + end_date: datetime, + dataset_names: Iterable[str] = None, + ): + log.info( + "Triggering timeseries backfill tasks for repo", + extra=dict( + repoid=repository.pk, + start_date=start_date.isoformat(), + end_date=end_date.isoformat(), + dataset_names=dataset_names, + ), + ) + + # This controls the batch size for the task - we'll backfill + # measurements 10 days at a time in this case. I picked this + # somewhat arbitrarily - we might need to tweak to see what's + # most appropriate. + delta = timedelta(days=10) + + signatures = [] + + task_end_date = end_date + while task_end_date > start_date: + task_start_date = task_end_date - delta + if task_start_date < start_date: + task_start_date = start_date + + kwargs = dict( + repoid=repository.pk, + start_date=task_start_date.isoformat(), + end_date=task_end_date.isoformat(), + ) + if dataset_names is not None: + kwargs["dataset_names"] = dataset_names + + signatures.append( + self._create_signature( + celery_config.timeseries_backfill_task_name, + kwargs=kwargs, + ) + ) + + task_end_date = task_start_date + + group(signatures).apply_async() + + def backfill_dataset( + self, + dataset: Dataset, + start_date: datetime, + end_date: datetime, + ): + log.info( + "Triggering dataset backfill", + extra=dict( + dataset_id=dataset.pk, + start_date=start_date.isoformat(), + end_date=end_date.isoformat(), + ), + ) + + self._create_signature( + "app.tasks.timeseries.backfill_dataset", + kwargs=dict( + dataset_id=dataset.pk, + start_date=start_date.isoformat(), + end_date=end_date.isoformat(), + ), + ).apply_async() + + def delete_timeseries(self, repository_id: int): + log.info( + "Delete repository timeseries data", + extra=dict(repository_id=repository_id), + ) + self._create_signature( + celery_config.timeseries_delete_task_name, + kwargs=dict(repository_id=repository_id), + ).apply_async() + + def update_commit(self, commitid, repoid): + self._create_signature( + "app.tasks.commit_update.CommitUpdate", + kwargs=dict(commitid=commitid, repoid=repoid), + ).apply_async() + + def create_report_results(self, commitid, repoid, report_code, current_yaml=None): + self._create_signature( + "app.tasks.reports.save_report_results", + kwargs=dict( + commitid=commitid, + repoid=repoid, + report_code=report_code, + current_yaml=current_yaml, + ), + ).apply_async() + + def http_request(self, url, method="POST", headers=None, data=None, timeout=None): + self._create_signature( + "app.tasks.http_request.HTTPRequest", + kwargs=dict( + url=url, + method=method, + headers=headers, + data=data, + timeout=timeout, + ), + ).apply_async() + + def flush_repo(self, repository_id: int): + self._create_signature( + "app.tasks.flush_repo.FlushRepo", + kwargs=dict(repoid=repository_id), + ).apply_async() + + def manual_upload_completion_trigger( + self, repoid, commitid, report_code=None, current_yaml=None + ): + self._create_signature( + "app.tasks.upload.ManualUploadCompletionTrigger", + kwargs=dict( + commitid=commitid, + repoid=repoid, + report_code=report_code, + current_yaml=current_yaml, + ), + ).apply_async() + + def preprocess_upload(self, repoid, commitid, report_code): + self._create_signature( + "app.tasks.upload.PreProcessUpload", + kwargs=dict( + repoid=repoid, + commitid=commitid, + report_code=report_code, + ), + ).apply_async() + + def send_email( + self, + to_addr: str, + subject: str, + template_name: str, + from_addr: str | None = None, + **kwargs, + ): + # Templates can be found in worker/templates + self._create_signature( + "app.tasks.send_email.SendEmail", + kwargs=dict( + to_addr=to_addr, + subject=subject, + template_name=template_name, + from_addr=from_addr, + **kwargs, + ), + ).apply_async() + + def delete_component_measurements(self, repoid: int, component_id: str) -> None: + log.info( + "Delete component measurements data", + extra=dict(repository_id=repoid, component_id=component_id), + ) + self._create_signature( + celery_config.timeseries_delete_task_name, + kwargs=dict( + repository_id=repoid, + measurement_only=True, + measurement_type=MeasurementName.COMPONENT_COVERAGE.value, + measurement_id=component_id, + ), + ).apply_async() + + def cache_test_results_redis(self, repoid: int, branch: str) -> None: + self._create_signature( + celery_config.cache_test_rollups_redis_task_name, + kwargs=dict(repoid=repoid, branch=branch), + ).apply_async() diff --git a/apps/codecov-api/services/task/task_router.py b/apps/codecov-api/services/task/task_router.py new file mode 100644 index 0000000000..154f11956e --- /dev/null +++ b/apps/codecov-api/services/task/task_router.py @@ -0,0 +1,104 @@ +import shared.celery_config as shared_celery_config +from shared.celery_router import route_tasks_based_on_user_plan +from shared.plan.constants import DEFAULT_FREE_PLAN + +from codecov_auth.models import Owner +from compare.models import CommitComparison +from core.models import Repository +from labelanalysis.models import LabelAnalysisRequest +from staticanalysis.models import StaticAnalysisSuite + + +def _get_user_plan_from_ownerid(ownerid, *args, **kwargs) -> str: + owner = Owner.objects.filter(ownerid=ownerid).first() + if owner: + return owner.plan + return DEFAULT_FREE_PLAN + + +def _get_user_plan_from_repoid(repoid, *args, **kwargs) -> str: + repo = Repository.objects.filter(repoid=repoid).first() + if repo and repo.author: + return repo.author.plan + return DEFAULT_FREE_PLAN + + +def _get_user_plan_from_comparison_id(comparison_id, *args, **kwargs) -> str: + compare_commit = ( + CommitComparison.objects.filter(id=comparison_id) + .select_related("compare_commit__repository__author") + .first() + ) + if ( + compare_commit + and compare_commit.compare_commit + and compare_commit.compare_commit.repository + and compare_commit.compare_commit.repository.author + ): + return compare_commit.compare_commit.repository.author.plan + return DEFAULT_FREE_PLAN + + +def _get_user_plan_from_label_request_id(request_id, *args, **kwargs) -> str: + label_analysis_request = ( + LabelAnalysisRequest.objects.filter(id=request_id) + .select_related("head_commit__repository__author") + .first() + ) + if ( + label_analysis_request + and label_analysis_request.head_commit + and label_analysis_request.head_commit.repository + and label_analysis_request.head_commit.repository.author + ): + return label_analysis_request.head_commit.repository.author.plan + return DEFAULT_FREE_PLAN + + +def _get_user_plan_from_suite_id(suite_id, *args, **kwargs) -> str: + static_analysis_suite = ( + StaticAnalysisSuite.objects.filter(id=suite_id) + .select_related("commit__repository__author") + .first() + ) + if ( + static_analysis_suite + and static_analysis_suite.commit + and static_analysis_suite.commit.repository + and static_analysis_suite.commit.repository.author + ): + return static_analysis_suite.commit.repository.author.plan + return DEFAULT_FREE_PLAN + + +def _get_user_plan_from_task(task_name: str, task_kwargs: dict) -> str: + owner_plan_lookup_funcs = { + # from ownerid + shared_celery_config.delete_owner_task_name: _get_user_plan_from_ownerid, + shared_celery_config.sync_repos_task_name: _get_user_plan_from_ownerid, + shared_celery_config.sync_teams_task_name: _get_user_plan_from_ownerid, + # from repoid + shared_celery_config.upload_task_name: _get_user_plan_from_repoid, + shared_celery_config.notify_task_name: _get_user_plan_from_repoid, + shared_celery_config.status_set_error_task_name: _get_user_plan_from_repoid, + shared_celery_config.status_set_pending_task_name: _get_user_plan_from_repoid, + shared_celery_config.pulls_task_name: _get_user_plan_from_repoid, + # from comparison_id + shared_celery_config.compute_comparison_task_name: _get_user_plan_from_comparison_id, + # from label_request_id + shared_celery_config.label_analysis_task_name: _get_user_plan_from_label_request_id, + # from suite_id + shared_celery_config.static_analysis_task_name: _get_user_plan_from_suite_id, + } + func_to_use = owner_plan_lookup_funcs.get( + task_name, lambda *args, **kwargs: DEFAULT_FREE_PLAN + ) + return func_to_use(**task_kwargs) + + +def route_task(name, args, kwargs, options={}, task=None, **kw): + """Function to dynamically route tasks to the proper queue. + Docs: https://docs.celeryq.dev/en/stable/userguide/routing.html#routers + """ + user_plan = _get_user_plan_from_task(name, kwargs) + return route_tasks_based_on_user_plan(name, user_plan) diff --git a/apps/codecov-api/services/tests/__init__.py b/apps/codecov-api/services/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/services/tests/cassetes/test_archive/TestReport/test_create_raw_upload_presigned_get.yaml b/apps/codecov-api/services/tests/cassetes/test_archive/TestReport/test_create_raw_upload_presigned_get.yaml new file mode 100644 index 0000000000..e21aab3876 --- /dev/null +++ b/apps/codecov-api/services/tests/cassetes/test_archive/TestReport/test_create_raw_upload_presigned_get.yaml @@ -0,0 +1,78 @@ +interactions: +- request: + body: null + headers: + host: + - minio:9000 + x-amz-content-sha256: + - e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + x-amz-date: + - 20220622T121514Z + method: GET + uri: http://minio:9000/archive?location= + response: + body: + string: ' + + ' + headers: + Accept-Ranges: + - bytes + Content-Security-Policy: + - block-all-mixed-content + Content-Type: + - application/xml + Date: + - Wed, 22 Jun 2022 12:15:14 GMT + Server: + - Minio/RELEASE.2019-04-09T01-22-30Z + Transfer-Encoding: + - chunked + Vary: + - Origin + X-Amz-Request-Id: + - 16FAF06173122EC0 + X-Minio-Deployment-Id: + - eac53635-8dc9-48b8-a84e-755e2779a894 + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: null + headers: + host: + - minio:9000 + user-agent: + - MinIO (Linux; aarch64) minio-py/6.0.2 + x-amz-content-sha256: + - e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 + x-amz-date: + - 20220622T121514Z + method: HEAD + uri: http://minio:9000/archive/ + response: + body: + string: '' + headers: + Accept-Ranges: + - bytes + Content-Security-Policy: + - block-all-mixed-content + Date: + - Wed, 22 Jun 2022 12:15:14 GMT + Server: + - Minio/RELEASE.2019-04-09T01-22-30Z + Vary: + - Origin + X-Amz-Request-Id: + - 16FAF06173BDDBF8 + X-Minio-Deployment-Id: + - eac53635-8dc9-48b8-a84e-755e2779a894 + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/apps/codecov-api/services/tests/cassetes/test_archive/TestReport/test_create_raw_upload_presigned_put.yaml b/apps/codecov-api/services/tests/cassetes/test_archive/TestReport/test_create_raw_upload_presigned_put.yaml new file mode 100644 index 0000000000..080ddd632d --- /dev/null +++ b/apps/codecov-api/services/tests/cassetes/test_archive/TestReport/test_create_raw_upload_presigned_put.yaml @@ -0,0 +1,47 @@ +interactions: +- request: + body: null + headers: + host: ['minio:9000'] + x-amz-content-sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + x-amz-date: [20210122T212511Z] + method: GET + uri: http://minio:9000/archive?location= + response: + body: {string: ' + + '} + headers: + Accept-Ranges: [bytes] + Content-Security-Policy: [block-all-mixed-content] + Content-Type: [application/xml] + Date: ['Fri, 22 Jan 2021 21:25:11 GMT'] + Server: [Minio/RELEASE.2019-04-09T01-22-30Z] + Transfer-Encoding: [chunked] + Vary: [Origin] + X-Amz-Request-Id: [165CAAEF6815ED00] + X-Minio-Deployment-Id: [65c76aa0-0170-423e-bf67-f8b33dd9bba8] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} +- request: + body: null + headers: + host: ['minio:9000'] + user-agent: [MinIO (Linux; aarch64) minio-py/6.0.0] + x-amz-content-sha256: [e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855] + x-amz-date: [20210122T212511Z] + method: HEAD + uri: http://minio:9000/archive/ + response: + body: {string: ''} + headers: + Accept-Ranges: [bytes] + Content-Security-Policy: [block-all-mixed-content] + Date: ['Fri, 22 Jan 2021 21:25:11 GMT'] + Server: [Minio/RELEASE.2019-04-09T01-22-30Z] + Vary: [Origin] + X-Amz-Request-Id: [165CAAEF684CEB20] + X-Minio-Deployment-Id: [65c76aa0-0170-423e-bf67-f8b33dd9bba8] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} +version: 1 diff --git a/apps/codecov-api/services/tests/samples/base_bundle_report.sqlite b/apps/codecov-api/services/tests/samples/base_bundle_report.sqlite new file mode 100644 index 0000000000..75ceaa0c3f Binary files /dev/null and b/apps/codecov-api/services/tests/samples/base_bundle_report.sqlite differ diff --git a/apps/codecov-api/services/tests/samples/bundle_report.sqlite b/apps/codecov-api/services/tests/samples/bundle_report.sqlite new file mode 100644 index 0000000000..dfaf104b00 Binary files /dev/null and b/apps/codecov-api/services/tests/samples/bundle_report.sqlite differ diff --git a/apps/codecov-api/services/tests/samples/bundle_with_asset_routes.sqlite b/apps/codecov-api/services/tests/samples/bundle_with_asset_routes.sqlite new file mode 100644 index 0000000000..3ca73ee44d Binary files /dev/null and b/apps/codecov-api/services/tests/samples/bundle_with_asset_routes.sqlite differ diff --git a/apps/codecov-api/services/tests/samples/bundle_with_assets_and_modules.sqlite b/apps/codecov-api/services/tests/samples/bundle_with_assets_and_modules.sqlite new file mode 100644 index 0000000000..f7e1467ac4 Binary files /dev/null and b/apps/codecov-api/services/tests/samples/bundle_with_assets_and_modules.sqlite differ diff --git a/apps/codecov-api/services/tests/samples/bundle_with_uuid.sqlite b/apps/codecov-api/services/tests/samples/bundle_with_uuid.sqlite new file mode 100644 index 0000000000..26c8b7285c Binary files /dev/null and b/apps/codecov-api/services/tests/samples/bundle_with_uuid.sqlite differ diff --git a/apps/codecov-api/services/tests/samples/chunks.txt b/apps/codecov-api/services/tests/samples/chunks.txt new file mode 100644 index 0000000000..3ad04429d0 --- /dev/null +++ b/apps/codecov-api/services/tests/samples/chunks.txt @@ -0,0 +1,39 @@ +{} +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[0, null, [[0, 0], [1, 0]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 1]]] +[1, null, [[0, 1], [1, 1]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 1]]] +[1, null, [[0, 0], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[0, null, [[0, 0], [1, 0]]] diff --git a/apps/codecov-api/services/tests/samples/head_bundle_report.sqlite b/apps/codecov-api/services/tests/samples/head_bundle_report.sqlite new file mode 100644 index 0000000000..787dc78853 Binary files /dev/null and b/apps/codecov-api/services/tests/samples/head_bundle_report.sqlite differ diff --git a/apps/codecov-api/services/tests/samples/head_bundle_report_with_compare_sha_6ca727b0142bf5625bb82af2555d308862063222.sqlite b/apps/codecov-api/services/tests/samples/head_bundle_report_with_compare_sha_6ca727b0142bf5625bb82af2555d308862063222.sqlite new file mode 100644 index 0000000000..d17d6789bf Binary files /dev/null and b/apps/codecov-api/services/tests/samples/head_bundle_report_with_compare_sha_6ca727b0142bf5625bb82af2555d308862063222.sqlite differ diff --git a/apps/codecov-api/services/tests/samples/head_bundle_report_with_gzip_size.sqlite b/apps/codecov-api/services/tests/samples/head_bundle_report_with_gzip_size.sqlite new file mode 100644 index 0000000000..1c28a8a4f3 Binary files /dev/null and b/apps/codecov-api/services/tests/samples/head_bundle_report_with_gzip_size.sqlite differ diff --git a/apps/codecov-api/services/tests/samples/stripe_invoice.json b/apps/codecov-api/services/tests/samples/stripe_invoice.json new file mode 100644 index 0000000000..289f59d5e3 --- /dev/null +++ b/apps/codecov-api/services/tests/samples/stripe_invoice.json @@ -0,0 +1,401 @@ +{ + "object": "list", + "url": "/v1/invoices", + "has_more": false, + "data": [ + { + "id": "in_19yTU92eZvKYlo2C7uDjvu6v", + "object": "invoice", + "account_country": "US", + "account_name": "Stripe.com", + "amount_due": 999, + "amount_paid": 999, + "amount_remaining": 0, + "application_fee_amount": null, + "attempt_count": 1, + "attempted": true, + "auto_advance": false, + "billing_reason": null, + "charge": "ch_19yUQN2eZvKYlo2CQf7aWpSX", + "collection_method": "charge_automatically", + "created": 1489789429, + "currency": "usd", + "custom_fields": null, + "customer": "cus_HF6p8Zx7JdRS7A", + "customer_address": "6639 Boulevard Dr, Westwood FL 34202 USA", + "customer_email": "olivia.williams.03@example.com", + "customer_name": "Peer Company", + "customer_phone": null, + "customer_shipping": null, + "customer_tax_exempt": "none", + "customer_tax_ids": [], + "default_payment_method": null, + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "due_date": null, + "ending_balance": 0, + "footer": null, + "hosted_invoice_url": "https://pay.stripe.com/invoice/acct_1032D82eZvKYlo2C/invst_a7KV10HpLw2QxrihgVyuOkOjMZ", + "invoice_pdf": "https://pay.stripe.com/invoice/acct_1032D82eZvKYlo2C/invst_a7KV10HpLw2QxrihgVyuOkOjMZ/pdf", + "lines": { + "data": [ + { + "id": "il_tmp_06bab3ae5b3624", + "object": "line_item", + "amount": 120, + "currency": "usd", + "description": "(10) users-pr-inappm", + "discountable": true, + "livemode": false, + "metadata": {}, + "period": { + "end": 1521326190, + "start": 1518906990 + }, + "plan": { + "id": "ivory-freelance-040", + "name": "users-pr-inappm", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 999, + "amount_decimal": "999", + "billing_scheme": "per_unit", + "created": 1466202980, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "nickname": null, + "product": "prod_BUthVRQ7KdFfa7", + "tiers": null, + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "proration": false, + "quantity": 1, + "subscription": "sub_8epEF0PuRhmltU", + "subscription_item": "si_18NVZi2eZvKYlo2CUtBNGL9x", + "tax_amounts": [], + "tax_rates": [], + "type": "subscription" + } + ], + "has_more": false, + "object": "list", + "url": "/v1/invoices/in_19yTU92eZvKYlo2C7uDjvu6v/lines" + }, + "livemode": false, + "metadata": { + "order_id": "6735" + }, + "next_payment_attempt": null, + "number": "EF0A41E-0001", + "paid": true, + "payment_intent": { + "id": "pi_3P4567890123456789012345", + "status": "completed" + }, + "period_end": 1489789420, + "period_start": 1487370220, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "receipt_number": "2277-9887", + "starting_balance": 0, + "statement_descriptor": null, + "status": "paid", + "status_transitions": { + "finalized_at": 1489793039, + "marked_uncollectible_at": null, + "paid_at": 1489793039, + "voided_at": null + }, + "subscription": "sub_9lNL2lSXI8nYEQ", + "subtotal": 999, + "tax": null, + "tax_percent": null, + "total": 999, + "total_tax_amounts": [], + "webhooks_delivered_at": 1489789437 + }, + { + "id": "in_19yTU92eZvKYlo2C7uDjvu69", + "object": "invoice", + "account_country": "US", + "account_name": "Stripe.com", + "amount_due": 999, + "amount_paid": 999, + "amount_remaining": 0, + "application_fee_amount": null, + "attempt_count": 1, + "attempted": true, + "auto_advance": false, + "billing_reason": null, + "charge": "ch_19yUQN2eZvKYlo2CQf7aWpSX", + "collection_method": "charge_automatically", + "created": 1489789429, + "currency": "usd", + "custom_fields": null, + "customer": "cus_HF6p8Zx7JdRS7A", + "customer_address": "6639 Boulevard Dr, Westwood FL 34202 USA", + "customer_email": "olivia.williams.03@example.com", + "customer_name": "Peer Company", + "customer_phone": null, + "customer_shipping": null, + "customer_tax_exempt": "none", + "customer_tax_ids": [], + "default_payment_method": null, + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "due_date": null, + "ending_balance": 0, + "footer": null, + "hosted_invoice_url": "https://pay.stripe.com/invoice/acct_1032D82eZvKYlo2C/invst_a7KV10HpLw2QxrihgVyuOkOjMZ", + "invoice_pdf": "https://pay.stripe.com/invoice/acct_1032D82eZvKYlo2C/invst_a7KV10HpLw2QxrihgVyuOkOjMZ/pdf", + "lines": { + "data": [ + { + "id": "il_tmp_06bab3ae5b3624", + "object": "line_item", + "amount": 120, + "currency": "usd", + "description": "(10) users-pr-inappm", + "discountable": true, + "livemode": false, + "metadata": {}, + "period": { + "end": 1521326190, + "start": 1518906990 + }, + "plan": { + "id": "ivory-freelance-040", + "name": "users-pr-inappm", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 999, + "amount_decimal": "999", + "billing_scheme": "per_unit", + "created": 1466202980, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "nickname": null, + "product": "prod_BUthVRQ7KdFfa7", + "tiers": null, + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "proration": false, + "quantity": 1, + "subscription": "sub_8epEF0PuRhmltU", + "subscription_item": "si_18NVZi2eZvKYlo2CUtBNGL9x", + "tax_amounts": [], + "tax_rates": [], + "type": "subscription" + } + ], + "has_more": false, + "object": "list", + "url": "/v1/invoices/in_19yTU92eZvKYlo2C7uDjvu6v/lines" + }, + "livemode": false, + "metadata": { + "order_id": "6735" + }, + "next_payment_attempt": null, + "number": "EF0A41E-0001", + "paid": true, + "payment_intent": { + "id": "pi_3P4567890123456789012345", + "status": "completed" + }, + "period_end": 1489789420, + "period_start": 1487370220, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "receipt_number": "2277-9887", + "starting_balance": 0, + "statement_descriptor": null, + "status": "paid", + "status_transitions": { + "finalized_at": 1489793039, + "marked_uncollectible_at": null, + "paid_at": 1489793039, + "voided_at": null + }, + "subscription": "sub_9lNL2lSXI8nYEQ", + "subtotal": 999, + "tax": null, + "tax_percent": null, + "total": 0, + "total_tax_amounts": [], + "webhooks_delivered_at": 1489789437 + }, + { + "id": "in_1MCimz2eZvKYlo2CvRbpaFFM", + "object": "invoice", + "account_country": "US", + "account_name": "Stripe.com", + "account_tax_ids": null, + "amount_due": 11000, + "amount_paid": 0, + "amount_remaining": 11000, + "application": null, + "application_fee_amount": null, + "attempt_count": 0, + "attempted": false, + "auto_advance": false, + "automatic_tax": { + "enabled": false, + "status": null + }, + "billing_reason": "manual", + "charge": null, + "collection_method": "charge_automatically", + "created": 1670500881, + "currency": "usd", + "custom_fields": null, + "customer": "cus_4QFOF3xrvBT2nU", + "customer_address": null, + "customer_email": null, + "customer_name": null, + "customer_phone": null, + "customer_shipping": null, + "customer_tax_exempt": "none", + "customer_tax_ids": [ + { + "type": "eu_vat", + "value": "DE123456789" + } + ], + "default_payment_method": null, + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "discounts": [], + "due_date": null, + "ending_balance": null, + "footer": null, + "from_invoice": null, + "hosted_invoice_url": null, + "invoice_pdf": null, + "last_finalization_error": null, + "latest_revision": null, + "lines": { + "object": "list", + "data": [ + { + "id": "il_1MCimy2eZvKYlo2CNV9viA8Z", + "object": "line_item", + "amount": 11000, + "amount_excluding_tax": 11000, + "currency": "usd", + "description": "My First Invoice Item (created for API docs)", + "discount_amounts": [], + "discountable": true, + "discounts": [], + "invoice_item": "ii_1MCimy2eZvKYlo2CXHo2fIwW", + "livemode": false, + "metadata": {}, + "period": { + "end": 1670500880, + "start": 1670500880 + }, + "price": { + "id": "price_1MCicx2eZvKYlo2CXoWVQnO4", + "object": "price", + "active": true, + "billing_scheme": "per_unit", + "created": 1670500259, + "currency": "usd", + "custom_unit_amount": null, + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_MwbqfRCnGFA9As", + "recurring": null, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "one_time", + "unit_amount": 11000, + "unit_amount_decimal": "11000" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 1, + "subscription": null, + "tax_amounts": [], + "tax_rates": [], + "type": "invoiceitem", + "unit_amount_excluding_tax": "11000" + } + ], + "has_more": false, + "url": "/v1/invoices/in_1MCimz2eZvKYlo2CvRbpaFFM/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": 1670504481, + "number": null, + "on_behalf_of": null, + "paid": false, + "paid_out_of_band": false, + "payment_intent": { + "id": "pi_3P4567890123456789012345", + "status": "completed" + }, + "payment_settings": { + "default_mandate": null, + "payment_method_options": null, + "payment_method_types": null + }, + "period_end": 1673097820, + "period_start": 1670419420, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "quote": null, + "receipt_number": null, + "redaction": null, + "rendering_options": null, + "starting_balance": 0, + "statement_descriptor": null, + "status": "void", + "status_transitions": { + "finalized_at": null, + "marked_uncollectible_at": null, + "paid_at": null, + "voided_at": null + }, + "subscription": null, + "subtotal": 11000, + "subtotal_excluding_tax": 11000, + "tax": null, + "test_clock": null, + "total": 0, + "total_discount_amounts": [], + "total_excluding_tax": 11000, + "total_tax_amounts": [], + "transfer_data": null, + "webhooks_delivered_at": null, + "closed": true, + "forgiven": false + } + ] +} diff --git a/apps/codecov-api/services/tests/test_activation.py b/apps/codecov-api/services/tests/test_activation.py new file mode 100644 index 0000000000..f2a2a83c06 --- /dev/null +++ b/apps/codecov-api/services/tests/test_activation.py @@ -0,0 +1,32 @@ +import pytest +from shared.django_apps.core.tests.factories import OwnerFactory + +from services.activation import _get_activator + + +@pytest.mark.django_db +def test_get_activator(): + org = OwnerFactory() + owner = OwnerFactory() + org.plan_activated_users = [owner.pk] + activator = _get_activator(org, owner) + + assert activator.org == org + assert activator.owner == owner + assert activator.is_autoactivation_enabled() == True + assert activator.can_activate_user() == False + assert activator.is_activated() == True + + +@pytest.mark.django_db +def test_get_activator_no_activated_users(): + org = OwnerFactory() + org.plan_activated_users = None + owner = OwnerFactory() + activator = _get_activator(org, owner) + + assert activator.org == org + assert activator.owner == owner + assert activator.is_autoactivation_enabled() == True + assert activator.can_activate_user() == True + assert activator.is_activated() == False diff --git a/apps/codecov-api/services/tests/test_analytics.py b/apps/codecov-api/services/tests/test_analytics.py new file mode 100644 index 0000000000..6fb37586af --- /dev/null +++ b/apps/codecov-api/services/tests/test_analytics.py @@ -0,0 +1,222 @@ +from datetime import datetime, timedelta +from unittest.mock import patch + +import pytest +from django.test import TestCase +from django.utils import timezone +from shared.analytics_tracking.events import Events +from shared.django_apps.codecov_auth.tests.factories import UserFactory +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from codecov_auth.models import PlanProviders +from services.analytics import AnalyticsOwner, AnalyticsRepository, AnalyticsService + + +class AnalyticsOwnerTests(TestCase): + def setUp(self): + self.analytics_owner = AnalyticsOwner( + OwnerFactory( + private_access=True, + plan_provider=PlanProviders.GITHUB.value, + plan_user_count=10, + delinquent=True, + trial_start_date=timezone.now(), + trial_end_date=timezone.now() + timedelta(days=14), + student=True, + bot=OwnerFactory(), + email="user@codecov.io", + student_created_at=datetime(2017, 1, 1, 12, 0, 0, 5000), + student_updated_at=datetime(2018, 1, 1, 12, 0, 0, 5000), + ) + ) + + def test_traits(self): + expected_traits = { + "email": self.analytics_owner.owner.email, + "username": self.analytics_owner.owner.username, + "service": self.analytics_owner.owner.service, + "service_id": self.analytics_owner.owner.service_id, + "plan": self.analytics_owner.owner.plan, + "owner_id": self.analytics_owner.owner.ownerid, + "user_id": self.analytics_owner.owner.ownerid, + } + + assert self.analytics_owner.traits == expected_traits + + @pytest.mark.skip + def test_traits_defaults(self): + analytics_owner_missing_traits = AnalyticsOwner( + OwnerFactory( + email=None, + name=None, + username=None, + private_access=None, + plan_provider=None, + plan_user_count=None, + delinquent=None, + trial_start_date=None, + trial_end_date=None, + student=0, + bot=None, + student_created_at=None, + student_updated_at=None, + ) + ) + + expected_traits = { + "email": "unknown@codecov.io", + "name": "unknown", + "username": "unknown", + "avatar": analytics_owner_missing_traits.owner.avatar_url, + "createdAt": datetime(2014, 1, 1, 12, 0, 0), + "updatedAt": self.analytics_owner.owner.updatestamp.replace(microsecond=0), + "service": analytics_owner_missing_traits.owner.service, + "service_id": analytics_owner_missing_traits.owner.service_id, + "private_access": False, + "plan": analytics_owner_missing_traits.owner.plan, + "plan_provider": "", + "plan_user_count": 5, + "delinquent": False, + "trial_start_date": None, + "trial_end_date": None, + "student": False, + "student_created_at": datetime(2014, 1, 1, 12, 0, 0), + "student_updated_at": datetime(2014, 1, 1, 12, 0, 0), + "staff": analytics_owner_missing_traits.owner.staff, + "bot": False, + "has_yaml": analytics_owner_missing_traits.owner.yaml is not None, + } + + assert analytics_owner_missing_traits.traits == expected_traits + + def test_context(self): + marketo_cookie, ga_cookie = "foo", "GA1.2.1429057651.1605972584" + cookies = {"_mkto_trk": marketo_cookie, "_ga": ga_cookie} + + self.analytics_owner = AnalyticsOwner( + OwnerFactory(stripe_customer_id=684), cookies=cookies + ) + + expected_context = { + "externalIds": [ + { + "id": self.analytics_owner.owner.service_id, + "type": f"{self.analytics_owner.owner.service}_id", + "collection": "users", + "encoding": "none", + }, + { + "id": self.analytics_owner.owner.stripe_customer_id, + "type": "stripe_customer_id", + "collection": "users", + "encoding": "none", + }, + { + "id": marketo_cookie, + "type": "marketo_cookie", + "collection": "users", + "encoding": "none", + }, + { + "id": "1429057651.1605972584", + "type": "ga_client_id", + "collection": "users", + "encoding": "none", + }, + ], + "Marketo": {"marketo_cookie": marketo_cookie}, + } + + assert self.analytics_owner.context == expected_context + + +class AnalyticsServiceTests(TestCase): + def setUp(self): + self.analytics_service = AnalyticsService() + self.owner = OwnerFactory() + self.analytics_owner = AnalyticsOwner(self.owner) + + @patch("shared.analytics_tracking.analytics_manager.track_event") + def test_user_signed_up(self, track_mock): + with self.settings(IS_ENTERPRISE=True): + self.analytics_service.user_signed_up(self.owner) + track_mock.assert_called_once_with( + Events.USER_SIGNED_UP.value, + is_enterprise=True, + event_data=self.analytics_owner.traits, + ) + + @patch("shared.analytics_tracking.analytics_manager.track_event") + def test_user_signed_in(self, track_mock): + with self.settings(IS_ENTERPRISE=False): + self.analytics_service.user_signed_in(self.owner) + track_mock.assert_called_once_with( + Events.USER_SIGNED_IN.value, + is_enterprise=False, + event_data=self.analytics_owner.traits, + ) + + @patch("shared.analytics_tracking.analytics_manager.track_event") + def test_account_activated_repository(self, track_mock): + owner = OwnerFactory() + repo = RepositoryFactory(author=owner) + with self.settings(IS_ENTERPRISE=False): + self.analytics_service.account_activated_repository(owner.ownerid, repo) + event_data = { + **AnalyticsRepository(repo).traits, + "user_id": owner.ownerid, + } + track_mock.assert_called_once_with( + Events.ACCOUNT_ACTIVATED_REPOSITORY.value, + is_enterprise=False, + event_data=event_data, + context={"groupId": repo.author.ownerid}, + ) + + @patch("shared.analytics_tracking.analytics_manager.track_event") + def test_account_activated_repository_on_upload(self, track_mock): + owner = OwnerFactory() + repo = RepositoryFactory(author=owner) + with self.settings(IS_ENTERPRISE=False): + self.analytics_service.account_activated_repository_on_upload( + owner.ownerid, repo + ) + track_mock.assert_called_once_with( + Events.ACCOUNT_ACTIVATED_REPOSITORY_ON_UPLOAD.value, + is_enterprise=False, + event_data={ + **AnalyticsRepository(repo).traits, + "user_id": owner.ownerid, + }, + context={"groupId": repo.author.ownerid}, + ) + + @patch("shared.analytics_tracking.analytics_manager.track_event") + def test_account_uploaded_coverage_report(self, track_mock): + owner = OwnerFactory() + upload_details = {"some": "dict", "user_id": owner.ownerid} + with self.settings(IS_ENTERPRISE=False): + self.analytics_service.account_uploaded_coverage_report( + owner.ownerid, upload_details + ) + track_mock.assert_called_once_with( + Events.ACCOUNT_UPLOADED_COVERAGE_REPORT.value, + is_enterprise=False, + event_data=upload_details, + context={"groupId": owner.ownerid}, + ) + + @patch("shared.analytics_tracking.analytics_manager.track_event") + def test_opt_in_email(self, track_mock): + user = UserFactory() + data = { + "email": user.email, + } + + with self.settings(IS_ENTERPRISE=False): + self.analytics_service.opt_in_email(user.id, data) + track_mock.assert_called_once_with( + Events.GDPR_OPT_IN.value, + is_enterprise=False, + event_data={**data, "user_id": user.id}, + ) diff --git a/apps/codecov-api/services/tests/test_billing.py b/apps/codecov-api/services/tests/test_billing.py new file mode 100644 index 0000000000..be880a0fba --- /dev/null +++ b/apps/codecov-api/services/tests/test_billing.py @@ -0,0 +1,2537 @@ +import json +from unittest.mock import MagicMock, call, patch + +import requests +import stripe +from django.conf import settings +from django.test import TestCase +from freezegun import freeze_time +from shared.django_apps.core.tests.factories import OwnerFactory +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName +from stripe import InvalidRequestError +from stripe.api_resources import PaymentIntent, SetupIntent + +from billing.helpers import mock_all_plans_and_tiers +from codecov_auth.models import Plan, Service +from services.billing import AbstractPaymentService, BillingService, StripeService + +SCHEDULE_RELEASE_OFFSET = 10 + +expected_invoices = [ + { + "id": "in_19yTU92eZvKYlo2C7uDjvu6v", + "object": "invoice", + "account_country": "US", + "account_name": "Stripe.com", + "amount_due": 999, + "amount_paid": 999, + "amount_remaining": 0, + "application_fee_amount": None, + "attempt_count": 1, + "attempted": True, + "auto_advance": False, + "billing_reason": None, + "charge": "ch_19yUQN2eZvKYlo2CQf7aWpSX", + "collection_method": "charge_automatically", + "created": 1489789429, + "currency": "usd", + "custom_fields": None, + "customer": "cus_HF6p8Zx7JdRS7A", + "customer_address": "6639 Boulevard Dr, Westwood FL 34202 USA", + "customer_email": "olivia.williams.03@example.com", + "customer_name": "Peer Company", + "customer_phone": None, + "customer_shipping": None, + "customer_tax_exempt": "none", + "customer_tax_ids": [], + "default_payment_method": None, + "default_source": None, + "default_tax_rates": [], + "description": None, + "discount": None, + "due_date": None, + "ending_balance": 0, + "footer": None, + "hosted_invoice_url": "https://pay.stripe.com/invoice/acct_1032D82eZvKYlo2C/invst_a7KV10HpLw2QxrihgVyuOkOjMZ", + "invoice_pdf": "https://pay.stripe.com/invoice/acct_1032D82eZvKYlo2C/invst_a7KV10HpLw2QxrihgVyuOkOjMZ/pdf", + "lines": { + "data": [ + { + "id": "il_tmp_06bab3ae5b3624", + "object": "line_item", + "amount": 120, + "currency": "usd", + "description": "(10) users-pr-inappm", + "discountable": True, + "livemode": False, + "metadata": {}, + "period": {"end": 1521326190, "start": 1518906990}, + "plan": { + "id": "ivory-freelance-040", + "name": "users-pr-inappm", + "object": "plan", + "active": True, + "aggregate_usage": None, + "amount": 999, + "amount_decimal": "999", + "billing_scheme": "per_unit", + "created": 1466202980, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": False, + "metadata": {}, + "nickname": None, + "product": "prod_BUthVRQ7KdFfa7", + "tiers": None, + "tiers_mode": None, + "transform_usage": None, + "trial_period_days": None, + "usage_type": "licensed", + }, + "proration": False, + "quantity": 1, + "subscription": "sub_8epEF0PuRhmltU", + "subscription_item": "si_18NVZi2eZvKYlo2CUtBNGL9x", + "tax_amounts": [], + "tax_rates": [], + "type": "subscription", + } + ], + "has_more": False, + "object": "list", + "url": "/v1/invoices/in_19yTU92eZvKYlo2C7uDjvu6v/lines", + }, + "livemode": False, + "metadata": {"order_id": "6735"}, + "next_payment_attempt": None, + "number": "EF0A41E-0001", + "paid": True, + "payment_intent": {"id": "pi_3P4567890123456789012345", "status": "completed"}, + "period_end": 1489789420, + "period_start": 1487370220, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "receipt_number": "2277-9887", + "starting_balance": 0, + "statement_descriptor": None, + "status": "paid", + "status_transitions": { + "finalized_at": 1489793039, + "marked_uncollectible_at": None, + "paid_at": 1489793039, + "voided_at": None, + }, + "subscription": "sub_9lNL2lSXI8nYEQ", + "subtotal": 999, + "tax": None, + "tax_percent": None, + "total": 999, + "total_tax_amounts": [], + "webhooks_delivered_at": 1489789437, + } +] + + +class MockSubscriptionPlan(object): + def __init__(self, params): + self.id = params["new_plan"] + self.interval = "year" + + +class MockSubscription(object): + def __init__(self, subscription_params): + self.schedule = subscription_params["schedule_id"] + self.current_period_start = subscription_params["start_date"] + self.current_period_end = subscription_params["end_date"] + self.plan = ( + MockSubscriptionPlan(subscription_params["plan"]) + if subscription_params.get("plan") is not None + else None + ) + self.items = { + "data": [ + { + "quantity": subscription_params["quantity"], + "id": subscription_params["id"], + "plan": { + "id": subscription_params["name"], + "interval": subscription_params.get("plan", {}).get( + "interval", "month" + ), + }, + } + ] + } + + def __getitem__(self, key): + return getattr(self, key) + + +class MockFailedSubscriptionUpgrade(object): + def __init__(self, subscription_params): + self.id = subscription_params["id"] + self.object = subscription_params["object"] + self.pending_update = subscription_params["pending_update"] + + def __getitem__(self, key): + return getattr(self, key) + + +class StripeServiceTests(TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + + def setUp(self): + self.user = OwnerFactory() + self.stripe = StripeService(requesting_user=self.user) + + def test_stripe_service_requires_requesting_user_to_be_owner_instance(self): + with self.assertRaises(Exception): + StripeService(None) + + def _assert_subscription_modify( + self, subscription_modify_mock, owner, subscription_params, desired_plan + ): + plan = Plan.objects.get(name=desired_plan["value"]) + subscription_modify_mock.assert_called_once_with( + owner.stripe_subscription_id, + cancel_at_period_end=False, + items=[ + { + "id": subscription_params["id"], + "plan": plan.stripe_id, + "quantity": desired_plan["quantity"], + } + ], + metadata={ + "service": owner.service, + "obo_organization": owner.ownerid, + "username": owner.username, + "obo_name": self.user.name, + "obo_email": self.user.email, + "obo": self.user.ownerid, + }, + proration_behavior="always_invoice", + # payment_behavior="pending_if_incomplete", + ) + + def _assert_schedule_modify( + self, + schedule_modify_mock, + owner, + subscription_params, + desired_plan, + schedule_id, + ): + plan = Plan.objects.get(name=desired_plan["value"]) + schedule_modify_mock.assert_called_once_with( + schedule_id, + end_behavior="release", + phases=[ + { + "start_date": subscription_params["start_date"], + "end_date": subscription_params["end_date"], + "items": [ + { + "plan": subscription_params["name"], + "price": subscription_params["name"], + "quantity": subscription_params["quantity"], + } + ], + "proration_behavior": "none", + }, + { + "start_date": subscription_params["end_date"], + "end_date": subscription_params["end_date"] + + SCHEDULE_RELEASE_OFFSET, + "items": [ + { + "plan": plan.stripe_id, + "price": plan.stripe_id, + "quantity": desired_plan["quantity"], + } + ], + "proration_behavior": "none", + }, + ], + metadata={ + "service": owner.service, + "obo_organization": owner.ownerid, + "username": owner.username, + "obo_name": self.user.name, + "obo_email": self.user.email, + "obo": self.user.ownerid, + }, + ) + + @patch("services.billing.stripe.Invoice.list") + def test_list_filtered_invoices_calls_stripe_invoice_list_with_customer_stripe_id( + self, invoice_list_mock + ): + owner = OwnerFactory(stripe_customer_id=-1) + self.stripe.list_filtered_invoices(owner) + invoice_list_mock.assert_called_once_with( + customer=owner.stripe_customer_id, limit=10 + ) + + @patch("services.billing.stripe.Invoice.list") + def test_list_filtered_invoices_returns_expected_invoices(self, invoice_list_mock): + with open("./services/tests/samples/stripe_invoice.json") as f: + stripe_invoice_response = json.load(f) + invoice_list_mock.return_value = stripe_invoice_response + owner = OwnerFactory(stripe_customer_id=-1) + invoices = self.stripe.list_filtered_invoices(owner) + assert invoices == expected_invoices + assert len(invoices) == 1 + + @patch("stripe.Invoice.list") + def test_list_filtered_invoices_returns_emptylist_if_stripe_customer_id_is_None( + self, invoice_list_mock + ): + owner = OwnerFactory() + invoices = self.stripe.list_filtered_invoices(owner) + + invoice_list_mock.assert_not_called() + assert invoices == [] + + @patch("services.billing.stripe.Customer.retrieve") + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.Subscription.modify") + def test_delete_subscription_without_schedule_modifies_subscription_to_delete_at_end_of_billing_cycle_if_valid_plan( + self, modify_mock, retrieve_subscription_mock, retrieve_customer_mock + ): + plan = PlanName.CODECOV_PRO_YEARLY.value + stripe_subscription_id = "sub_1K77Y5GlVGuVgOrkJrLjRnne" + stripe_schedule_id = None + customer_id = "cus_HF6p8Zx7JdRS7A" + owner = OwnerFactory( + stripe_subscription_id=stripe_subscription_id, + plan=plan, + plan_activated_users=[4, 6, 3], + plan_user_count=9, + stripe_customer_id=customer_id, + ) + subscription_params = { + "schedule_id": stripe_schedule_id, + "start_date": 1489799420, + "end_date": 1492477820, + "quantity": 10, + "name": plan, + "id": 215, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + retrieve_customer_mock.return_value = { + "id": "cus_123456789", + "email": "test@example.com", + "name": "Test User", + "metadata": {}, + } + self.stripe.delete_subscription(owner) + modify_mock.assert_called_once_with( + stripe_subscription_id, + cancel_at_period_end=True, + proration_behavior="none", + ) + owner.refresh_from_db() + assert owner.stripe_subscription_id == stripe_subscription_id + assert owner.plan == plan + assert owner.plan_activated_users == [4, 6, 3] + assert owner.plan_user_count == 9 + + @freeze_time("2017-03-22T00:00:00") + @patch("services.billing.stripe.Customer.retrieve") + @patch("services.billing.stripe.Refund.create") + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.SubscriptionSchedule.release") + def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscription_at_end_of_billing_cycle_if_valid_plan( + self, + schedule_release_mock, + retrieve_subscription_mock, + modify_mock, + create_refund_mock, + retrieve_customer_mock, + ): + plan = PlanName.CODECOV_PRO_YEARLY.value + stripe_subscription_id = "sub_1K77Y5GlVGuVgOrkJrLjRnne" + stripe_schedule_id = "sub_sched_sch1K77Y5GlVGuVgOrkJrLjRnne" + # customer_id = "cus_HF6p8Zx7JdRS7A" + owner = OwnerFactory( + stripe_subscription_id=stripe_subscription_id, + plan=plan, + plan_activated_users=[4, 6, 3], + plan_user_count=9, + # stripe_customer_id=customer_id + ) + subscription_params = { + "schedule_id": stripe_schedule_id, + "start_date": 1489799420, + "end_date": 1492477820, + "quantity": 10, + "name": plan, + "id": 215, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + retrieve_customer_mock.return_value = { + "id": "cus_123456789", + "email": "test@example.com", + "name": "Test User", + "metadata": {}, + } + self.stripe.delete_subscription(owner) + schedule_release_mock.assert_called_once_with(stripe_schedule_id) + modify_mock.assert_called_once_with( + stripe_subscription_id, + cancel_at_period_end=True, + proration_behavior="none", + ) + create_refund_mock.assert_not_called() + owner.refresh_from_db() + assert owner.stripe_subscription_id == stripe_subscription_id + assert owner.plan == plan + assert owner.plan_activated_users == [4, 6, 3] + assert owner.plan_user_count == 9 + + @freeze_time("2017-03-18T00:00:00") + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.Customer.modify") + @patch("services.billing.stripe.Refund.create") + @patch("services.billing.stripe.Invoice.list") + @patch("services.billing.stripe.Subscription.cancel") + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.SubscriptionSchedule.release") + @patch("services.billing.stripe.Customer.retrieve") + def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscription_with_grace_month_refund_if_valid_plan( + self, + retrieve_customer_mock, + schedule_release_mock, + retrieve_subscription_mock, + cancel_sub_mock, + list_invoice_mock, + create_refund_mock, + modify_customer_mock, + modify_sub_mock, + ): + with open("./services/tests/samples/stripe_invoice.json") as f: + stripe_invoice_response = json.load(f) + list_invoice_mock.return_value = stripe_invoice_response + plan = PlanName.CODECOV_PRO_YEARLY.value + stripe_subscription_id = "sub_1K77Y5GlVGuVgOrkJrLjRnne" + stripe_schedule_id = "sub_sched_sch1K77Y5GlVGuVgOrkJrLjRnne" + customer_id = "cus_HF6p8Zx7JdRS7A" + owner = OwnerFactory( + stripe_subscription_id=stripe_subscription_id, + plan=plan, + plan_activated_users=[4, 6, 3], + plan_user_count=9, + stripe_customer_id=customer_id, + ) + subscription_params = { + "schedule_id": stripe_schedule_id, + "start_date": 1489799420, + "end_date": 1492477820, + "quantity": 10, + "name": plan, + "id": 215, + "plan": { + "new_plan": "plan_pro_yearly", + "new_quantity": 7, + "subscription_id": "sub_123", + "interval": "month", + }, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + retrieve_customer_mock.return_value = { + "id": "cus_HF6p8Zx7JdRS7A", + "metadata": {}, + } + self.stripe.delete_subscription(owner) + schedule_release_mock.assert_called_once_with(stripe_schedule_id) + retrieve_customer_mock.assert_called_once_with(owner.stripe_customer_id) + cancel_sub_mock.assert_called_once_with(stripe_subscription_id) + list_invoice_mock.assert_called_once_with( + subscription=stripe_subscription_id, + status="paid", + created={"gte": 1458263420, "lt": 1489799420}, + ) + self.assertEqual(create_refund_mock.call_count, 2) + modify_customer_mock.assert_called_once_with( + owner.stripe_customer_id, balance=0, metadata={"autorefunds_remaining": "1"} + ) + modify_sub_mock.assert_not_called() + + owner.refresh_from_db() + assert owner.stripe_subscription_id == stripe_subscription_id + assert owner.plan == plan + assert owner.plan_activated_users == [4, 6, 3] + assert owner.plan_user_count == 9 + + @freeze_time("2017-03-19T00:00:00") + @patch("services.billing.stripe.Customer.retrieve") + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.Customer.modify") + @patch("services.billing.stripe.Refund.create") + @patch("services.billing.stripe.Invoice.list") + @patch("services.billing.stripe.Subscription.cancel") + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.SubscriptionSchedule.release") + def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscription_with_grace_year_refund_if_valid_plan( + self, + schedule_release_mock, + retrieve_subscription_mock, + cancel_sub_mock, + list_invoice_mock, + create_refund_mock, + modify_customer_mock, + modify_sub_mock, + retrieve_customer_mock, + ): + with open("./services/tests/samples/stripe_invoice.json") as f: + stripe_invoice_response = json.load(f) + list_invoice_mock.return_value = stripe_invoice_response + plan = PlanName.CODECOV_PRO_YEARLY.value + stripe_subscription_id = "sub_1K77Y5GlVGuVgOrkJrLjRnne" + stripe_schedule_id = "sub_sched_sch1K77Y5GlVGuVgOrkJrLjRnne" + customer_id = "cus_HF6p8Zx7JdRS7A" + owner = OwnerFactory( + stripe_subscription_id=stripe_subscription_id, + plan=plan, + plan_activated_users=[4, 6, 3], + plan_user_count=9, + stripe_customer_id=customer_id, + ) + subscription_params = { + "schedule_id": stripe_schedule_id, + "start_date": 1489799420, + "end_date": 1492477820, + "quantity": 10, + "name": plan, + "id": 215, + "plan": { + "new_plan": "plan_pro_yearly", + "new_quantity": 7, + "subscription_id": "sub_123", + "interval": "year", + }, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + retrieve_customer_mock.return_value = { + "id": "cus_HF6p8Zx7JdRS7A", + "metadata": {"autorefunds_remaining": "1"}, + } + self.stripe.delete_subscription(owner) + schedule_release_mock.assert_called_once_with(stripe_schedule_id) + retrieve_customer_mock.assert_called_once_with(owner.stripe_customer_id) + cancel_sub_mock.assert_called_once_with(stripe_subscription_id) + list_invoice_mock.assert_called_once_with( + subscription=stripe_subscription_id, + status="paid", + created={"gte": 1458263420, "lt": 1489799420}, + ) + self.assertEqual(create_refund_mock.call_count, 2) + modify_customer_mock.assert_called_once_with( + owner.stripe_customer_id, balance=0, metadata={"autorefunds_remaining": "0"} + ) + modify_sub_mock.assert_not_called() + + owner.refresh_from_db() + assert owner.stripe_subscription_id == stripe_subscription_id + assert owner.plan == plan + assert owner.plan_activated_users == [4, 6, 3] + assert owner.plan_user_count == 9 + + @freeze_time("2017-03-19T00:00:00") + @patch("services.billing.stripe.Customer.retrieve") + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.Customer.modify") + @patch("services.billing.stripe.Refund.create") + @patch("services.billing.stripe.Invoice.list") + @patch("services.billing.stripe.Subscription.cancel") + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.SubscriptionSchedule.release") + def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscription_immediately_with_grace_year_but_no_invoices_to_refund( + self, + schedule_release_mock, + retrieve_subscription_mock, + cancel_sub_mock, + list_invoice_mock, + create_refund_mock, + modify_customer_mock, + modify_sub_mock, + retrieve_customer_mock, + ): + with open("./services/tests/samples/stripe_invoice.json") as f: + stripe_invoice_response = json.load(f) + for invoice in stripe_invoice_response["data"]: + invoice["charge"] = None + list_invoice_mock.return_value = stripe_invoice_response + plan = PlanName.CODECOV_PRO_YEARLY.value + stripe_subscription_id = "sub_1K77Y5GlVGuVgOrkJrLjRnne" + stripe_schedule_id = "sub_sched_sch1K77Y5GlVGuVgOrkJrLjRnne" + customer_id = "cus_HF6p8Zx7JdRS7A" + owner = OwnerFactory( + stripe_subscription_id=stripe_subscription_id, + plan=plan, + plan_activated_users=[4, 6, 3], + plan_user_count=9, + stripe_customer_id=customer_id, + ) + subscription_params = { + "schedule_id": stripe_schedule_id, + "start_date": 1489799420, + "end_date": 1492477820, + "quantity": 10, + "name": plan, + "id": 215, + "plan": { + "new_plan": "plan_pro_yearly", + "new_quantity": 7, + "subscription_id": "sub_123", + "interval": "year", + }, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + retrieve_customer_mock.return_value = { + "id": "cus_HF6p8Zx7JdRS7A", + "metadata": {"autorefunds_remaining": "1"}, + } + self.stripe.delete_subscription(owner) + schedule_release_mock.assert_called_once_with(stripe_schedule_id) + retrieve_customer_mock.assert_called_once_with(owner.stripe_customer_id) + cancel_sub_mock.assert_called_once_with(stripe_subscription_id) + list_invoice_mock.assert_called_once_with( + subscription=stripe_subscription_id, + status="paid", + created={"gte": 1458263420, "lt": 1489799420}, + ) + create_refund_mock.assert_not_called() + modify_customer_mock.assert_not_called() + modify_sub_mock.assert_not_called() + + owner.refresh_from_db() + assert owner.stripe_subscription_id == stripe_subscription_id + assert owner.plan == plan + assert owner.plan_activated_users == [4, 6, 3] + assert owner.plan_user_count == 9 + + @freeze_time("2017-03-19T00:00:00") + @patch("services.billing.stripe.Customer.retrieve") + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.Customer.modify") + @patch("services.billing.stripe.Refund.create") + @patch("services.billing.stripe.Invoice.list") + @patch("services.billing.stripe.Subscription.cancel") + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.SubscriptionSchedule.release") + def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscription_at_end_of_billing_cycle_as_no_more_autorefunds_available( + self, + schedule_release_mock, + retrieve_subscription_mock, + cancel_sub_mock, + list_invoice_mock, + create_refund_mock, + modify_customer_mock, + modify_sub_mock, + retrieve_customer_mock, + ): + with open("./services/tests/samples/stripe_invoice.json") as f: + stripe_invoice_response = json.load(f) + list_invoice_mock.return_value = stripe_invoice_response + plan = PlanName.CODECOV_PRO_YEARLY.value + stripe_subscription_id = "sub_1K77Y5GlVGuVgOrkJrLjRnne" + stripe_schedule_id = "sub_sched_sch1K77Y5GlVGuVgOrkJrLjRnne" + customer_id = "cus_HF6p8Zx7JdRS7A" + owner = OwnerFactory( + stripe_subscription_id=stripe_subscription_id, + plan=plan, + plan_activated_users=[4, 6, 3], + plan_user_count=9, + stripe_customer_id=customer_id, + ) + subscription_params = { + "schedule_id": stripe_schedule_id, + "start_date": 1489799420, + "end_date": 1492477820, + "quantity": 10, + "name": plan, + "id": 215, + "plan": { + "new_plan": "plan_pro_yearly", + "new_quantity": 7, + "subscription_id": "sub_123", + "interval": "year", + }, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + retrieve_customer_mock.return_value = { + "id": "cus_HF6p8Zx7JdRS7A", + "metadata": {"autorefunds_remaining": "0"}, + } + self.stripe.delete_subscription(owner) + schedule_release_mock.assert_called_once_with(stripe_schedule_id) + retrieve_customer_mock.assert_called_once_with(owner.stripe_customer_id) + cancel_sub_mock.assert_not_called() + create_refund_mock.assert_not_called() + modify_customer_mock.assert_not_called() + modify_sub_mock.assert_called_once_with( + stripe_subscription_id, + cancel_at_period_end=True, + proration_behavior="none", + ) + + owner.refresh_from_db() + assert owner.stripe_subscription_id == stripe_subscription_id + assert owner.plan == plan + assert owner.plan_activated_users == [4, 6, 3] + assert owner.plan_user_count == 9 + + @patch("logging.Logger.error") + def test_modify_subscription_no_plan_found( + self, + log_error_mock, + ): + original_plan = PlanName.CODECOV_PRO_MONTHLY.value + original_user_count = 10 + owner = OwnerFactory( + plan=original_plan, + plan_user_count=original_user_count, + stripe_subscription_id="33043sdf", + ) + + desired_plan_name = "invalid plan" + desired_user_count = 10 + desired_plan = {"value": desired_plan_name, "quantity": desired_user_count} + self.stripe.modify_subscription(owner, desired_plan) + + owner.refresh_from_db() + assert owner.plan == original_plan + assert owner.plan_user_count == original_user_count + log_error_mock.assert_has_calls( + [ + call( + f"Plan {desired_plan_name} not found", + extra=dict(owner_id=owner.ownerid), + ), + ] + ) + + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.Subscription.retrieve") + def test_modify_subscription_without_schedule_increases_user_count_immediately( + self, + retrieve_subscription_mock, + subscription_modify_mock, + ): + original_user_count = 10 + original_plan = PlanName.CODECOV_PRO_YEARLY.value + owner = OwnerFactory( + plan=original_plan, + plan_user_count=original_user_count, + stripe_subscription_id="33043sdf", + ) + + schedule_id = None + current_subscription_start_date = 1639628096 + current_subscription_end_date = 1644107871 + subscription_params = { + "schedule_id": schedule_id, + "start_date": current_subscription_start_date, + "end_date": current_subscription_end_date, + "quantity": original_user_count, + "name": original_plan, + "id": 105, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + subscription_modify_mock.return_value = MockSubscription(subscription_params) + + desired_plan_name = PlanName.CODECOV_PRO_YEARLY.value + desired_user_count = 20 + desired_plan = {"value": desired_plan_name, "quantity": desired_user_count} + self.stripe.modify_subscription(owner, desired_plan) + + self._assert_subscription_modify( + subscription_modify_mock, owner, subscription_params, desired_plan + ) + + owner.refresh_from_db() + assert owner.plan == desired_plan_name + assert owner.plan_user_count == desired_user_count + + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.Subscription.retrieve") + def test_modify_subscription_without_schedule_upgrades_plan_immediately( + self, + retrieve_subscription_mock, + subscription_modify_mock, + ): + original_plan = PlanName.CODECOV_PRO_MONTHLY.value + original_user_count = 10 + owner = OwnerFactory( + plan=original_plan, + plan_user_count=original_user_count, + stripe_subscription_id="33043sdf", + ) + + schedule_id = None + current_subscription_start_date = 1639628096 + current_subscription_end_date = 1644107871 + subscription_params = { + "schedule_id": schedule_id, + "start_date": current_subscription_start_date, + "end_date": current_subscription_end_date, + "quantity": original_user_count, + "name": original_plan, + "id": 101, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + subscription_modify_mock.return_value = MockSubscription(subscription_params) + + desired_plan_name = PlanName.CODECOV_PRO_YEARLY.value + desired_user_count = 10 + desired_plan = {"value": desired_plan_name, "quantity": desired_user_count} + self.stripe.modify_subscription(owner, desired_plan) + + self._assert_subscription_modify( + subscription_modify_mock, owner, subscription_params, desired_plan + ) + + owner.refresh_from_db() + assert owner.plan == desired_plan_name + assert owner.plan_user_count == desired_user_count + + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.Subscription.retrieve") + def test_modify_subscription_payment_failure( + self, + retrieve_subscription_mock, + subscription_modify_mock, + ): + original_user_count = 10 + original_plan = PlanName.CODECOV_PRO_YEARLY.value + owner = OwnerFactory( + plan=original_plan, + plan_user_count=original_user_count, + stripe_subscription_id="33043sdf", + delinquent=False, + ) + schedule_id = None + current_subscription_start_date = 1639628096 + current_subscription_end_date = 1644107871 + subscription_params = { + "schedule_id": schedule_id, + "start_date": current_subscription_start_date, + "end_date": current_subscription_end_date, + "quantity": original_user_count, + "name": original_plan, + "id": 105, + } + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + + subscription_response = { + "id": 105, + "object": "subscription", + "application_fee_percent": None, + "pending_update": { + "expires_at": 1571194285, + "subscription_items": [ + { + "id": "si_09IkI4u3ZypJUk5onGUZpe8O", + "price": "price_CBb6IXqvTLXp3f", + } + ], + }, + } + subscription_modify_mock.return_value = MockFailedSubscriptionUpgrade( + subscription_response + ) + + desired_plan_name = PlanName.CODECOV_PRO_YEARLY.value + desired_user_count = 20 + desired_plan = {"value": desired_plan_name, "quantity": desired_user_count} + self.stripe.modify_subscription(owner, desired_plan) + + self._assert_subscription_modify( + subscription_modify_mock, owner, subscription_params, desired_plan + ) + + # changes to plan are rejected, owner becomes delinquent + owner.refresh_from_db() + assert owner.plan == desired_plan_name + assert owner.plan_user_count == original_user_count + assert owner.delinquent == True + + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.Subscription.retrieve") + def test_modify_subscription_payment_no_false_positives( + self, + retrieve_subscription_mock, + subscription_modify_mock, + ): + original_user_count = 10 + original_plan = PlanName.CODECOV_PRO_YEARLY.value + owner = OwnerFactory( + plan=original_plan, + plan_user_count=original_user_count, + stripe_subscription_id="33043sdf", + delinquent=False, + ) + schedule_id = None + current_subscription_start_date = 1639628096 + current_subscription_end_date = 1644107871 + subscription_params = { + "schedule_id": schedule_id, + "start_date": current_subscription_start_date, + "end_date": current_subscription_end_date, + "quantity": original_user_count, + "name": original_plan, + "id": 105, + } + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + + subscription_response = { + "id": 105, + "object": "subscription", + "application_fee_percent": None, + "pending_update": {}, + } + subscription_modify_mock.return_value = MockFailedSubscriptionUpgrade( + subscription_response + ) + + desired_plan_name = PlanName.CODECOV_PRO_YEARLY.value + desired_user_count = 20 + desired_plan = {"value": desired_plan_name, "quantity": desired_user_count} + self.stripe.modify_subscription(owner, desired_plan) + + self._assert_subscription_modify( + subscription_modify_mock, owner, subscription_params, desired_plan + ) + + # plan is updated, owner is not delinquent + owner.refresh_from_db() + assert owner.plan == desired_plan_name + assert owner.plan_user_count == desired_user_count + assert owner.delinquent == False + + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.Subscription.retrieve") + def test_modify_subscription_but_stripe_is_broken( + self, retrieve_subscription_mock, subscription_modify_mock + ): + owner = OwnerFactory( + plan=PlanName.CODECOV_PRO_YEARLY.value, + plan_user_count=10, + stripe_subscription_id="33043sdf", + delinquent=False, + ) + subscription_params = { + "schedule_id": None, + "start_date": 1639628096, + "end_date": 1644107871, + "quantity": 10, + "name": PlanName.CODECOV_PRO_YEARLY.value, + "id": 105, + } + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + + subscription_modify_mock.side_effect = requests.exceptions.Timeout + + desired_plan = {"value": PlanName.CODECOV_PRO_YEARLY.value, "quantity": 100} + with self.assertRaises(requests.exceptions.Timeout): + # if stripe is erroring, it will pop up on sentry + self.stripe.modify_subscription(owner, desired_plan) + + owner.refresh_from_db() + assert owner.plan == PlanName.CODECOV_PRO_YEARLY.value + assert owner.plan_user_count == 10 + assert owner.delinquent == False + + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.Subscription.retrieve") + def test_modify_subscription_without_schedule_upgrades_plan_and_users_immediately( + self, + retrieve_subscription_mock, + subscription_modify_mock, + ): + original_user_count = 10 + original_plan = PlanName.CODECOV_PRO_MONTHLY.value + owner = OwnerFactory( + plan=original_plan, + plan_user_count=original_user_count, + stripe_subscription_id="33043sdf", + ) + + schedule_id = None + current_subscription_start_date = 1639628096 + current_subscription_end_date = 1644107871 + subscription_params = { + "schedule_id": schedule_id, + "start_date": current_subscription_start_date, + "end_date": current_subscription_end_date, + "quantity": original_user_count, + "name": original_plan, + "id": 102, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + subscription_modify_mock.return_value = MockSubscription(subscription_params) + + desired_plan_name = PlanName.CODECOV_PRO_YEARLY.value + desired_user_count = 15 + desired_plan = {"value": desired_plan_name, "quantity": desired_user_count} + self.stripe.modify_subscription(owner, desired_plan) + + self._assert_subscription_modify( + subscription_modify_mock, owner, subscription_params, desired_plan + ) + + owner.refresh_from_db() + assert owner.plan == desired_plan_name + assert owner.plan_user_count == desired_user_count + + @patch("services.billing.stripe.SubscriptionSchedule.create") + @patch("services.billing.stripe.SubscriptionSchedule.modify") + @patch("services.billing.stripe.Subscription.retrieve") + def test_modify_subscription_without_schedule_adds_schedule_when_user_count_decreases( + self, retrieve_subscription_mock, schedule_modify_mock, create_mock + ): + original_user_count = 14 + original_plan = PlanName.CODECOV_PRO_MONTHLY.value + stripe_subscription_id = "33043sdf" + owner = OwnerFactory( + plan=original_plan, + plan_user_count=original_user_count, + stripe_subscription_id=stripe_subscription_id, + ) + + schedule_id = None + current_subscription_start_date = 1639628096 + current_subscription_end_date = 1644107871 + subscription_params = { + "schedule_id": schedule_id, + "start_date": current_subscription_start_date, + "end_date": current_subscription_end_date, + "quantity": original_user_count, + "name": original_plan, + "id": 104, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + + desired_plan_name = PlanName.CODECOV_PRO_MONTHLY.value + desired_user_count = 8 + desired_plan = {"value": desired_plan_name, "quantity": desired_user_count} + + self.stripe.modify_subscription(owner, desired_plan) + + create_mock.assert_called_once_with(from_subscription=stripe_subscription_id) + schedule_id = create_mock.return_value._mock_children["id"] + + self._assert_schedule_modify( + schedule_modify_mock, owner, subscription_params, desired_plan, schedule_id + ) + + owner.refresh_from_db() + assert owner.plan == original_plan + assert owner.plan_user_count == original_user_count + + @patch("services.billing.stripe.SubscriptionSchedule.create") + @patch("services.billing.stripe.SubscriptionSchedule.modify") + @patch("services.billing.stripe.Subscription.retrieve") + def test_modify_subscription_without_schedule_adds_schedule_when_plan_downgrades( + self, retrieve_subscription_mock, schedule_modify_mock, create_mock + ): + original_user_count = 20 + original_plan = PlanName.CODECOV_PRO_YEARLY.value + stripe_subscription_id = "33043sdf" + owner = OwnerFactory( + plan=original_plan, + plan_user_count=original_user_count, + stripe_subscription_id=stripe_subscription_id, + ) + + schedule_id = None + current_subscription_start_date = 1639628096 + current_subscription_end_date = 1644107871 + subscription_params = { + "schedule_id": schedule_id, + "start_date": current_subscription_start_date, + "end_date": current_subscription_end_date, + "quantity": original_user_count, + "name": original_plan, + "id": 106, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + + desired_plan_name = PlanName.CODECOV_PRO_MONTHLY.value + desired_user_count = 20 + desired_plan = {"value": desired_plan_name, "quantity": desired_user_count} + + self.stripe.modify_subscription(owner, desired_plan) + + create_mock.assert_called_once_with(from_subscription=stripe_subscription_id) + schedule_id = create_mock.return_value._mock_children["id"] + + self._assert_schedule_modify( + schedule_modify_mock, owner, subscription_params, desired_plan, schedule_id + ) + + owner.refresh_from_db() + assert owner.plan == original_plan + assert owner.plan_user_count == original_user_count + + @patch("services.billing.stripe.SubscriptionSchedule.create") + @patch("services.billing.stripe.SubscriptionSchedule.modify") + @patch("services.billing.stripe.Subscription.retrieve") + def test_modify_subscription_without_schedule_adds_schedule_when_plan_and_count_downgrades( + self, retrieve_subscription_mock, schedule_modify_mock, create_mock + ): + original_user_count = 16 + original_plan = PlanName.CODECOV_PRO_YEARLY.value + stripe_subscription_id = "33043sdf" + owner = OwnerFactory( + plan=original_plan, + plan_user_count=original_user_count, + stripe_subscription_id=stripe_subscription_id, + ) + + schedule_id = None + current_subscription_start_date = 1639628096 + current_subscription_end_date = 1644107871 + subscription_params = { + "schedule_id": schedule_id, + "start_date": current_subscription_start_date, + "end_date": current_subscription_end_date, + "quantity": original_user_count, + "name": original_plan, + "id": 107, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + + desired_plan_name = PlanName.CODECOV_PRO_MONTHLY.value + desired_user_count = 7 + desired_plan = {"value": desired_plan_name, "quantity": desired_user_count} + + self.stripe.modify_subscription(owner, desired_plan) + + create_mock.assert_called_once_with(from_subscription=stripe_subscription_id) + schedule_id = create_mock.return_value._mock_children["id"] + + self._assert_schedule_modify( + schedule_modify_mock, owner, subscription_params, desired_plan, schedule_id + ) + + owner.refresh_from_db() + assert owner.plan == original_plan + assert owner.plan_user_count == original_user_count + + @patch("services.billing.stripe.SubscriptionSchedule.modify") + @patch("services.billing.stripe.Subscription.retrieve") + def test_modify_subscription_with_schedule_modifies_schedule_when_user_count_decreases( + self, retrieve_subscription_mock, schedule_modify_mock + ): + original_user_count = 13 + original_plan = PlanName.CODECOV_PRO_MONTHLY.value + stripe_subscription_id = "33043sdf" + owner = OwnerFactory( + plan=original_plan, + plan_user_count=original_user_count, + stripe_subscription_id=stripe_subscription_id, + ) + + schedule_id = "sub_sched_1K77Y5GlVGuVgOrkJrLjRnne" + current_subscription_start_date = 1639628096 + current_subscription_end_date = 1644107871 + subscription_params = { + "schedule_id": schedule_id, + "start_date": current_subscription_start_date, + "end_date": current_subscription_end_date, + "quantity": original_user_count, + "name": original_plan, + "id": 108, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + + desired_plan_name = PlanName.CODECOV_PRO_MONTHLY.value + desired_user_count = 9 + desired_plan = {"value": desired_plan_name, "quantity": desired_user_count} + + self.stripe.modify_subscription(owner, desired_plan) + + self._assert_schedule_modify( + schedule_modify_mock, owner, subscription_params, desired_plan, schedule_id + ) + + owner.refresh_from_db() + assert owner.plan == original_plan + assert owner.plan_user_count == original_user_count + + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.SubscriptionSchedule.release") + def test_modify_subscription_with_schedule_modifies_schedule_when_user_count_increases( + self, + schedule_release_mock, + retrieve_subscription_mock, + subscription_modify_mock, + ): + original_user_count = 17 + original_plan = PlanName.CODECOV_PRO_MONTHLY.value + stripe_subscription_id = "33043sdf" + owner = OwnerFactory( + plan=original_plan, + plan_user_count=original_user_count, + stripe_subscription_id=stripe_subscription_id, + ) + + desired_plan_name = PlanName.CODECOV_PRO_MONTHLY.value + desired_user_count = 26 + + desired_plan = {"value": desired_plan_name, "quantity": desired_user_count} + schedule_id = "sub_sched_1K77Y5GlVGuVgOrkJrLjRnne" + current_subscription_start_date = 1639628096 + current_subscription_end_date = 1644107871 + subscription_params = { + "schedule_id": schedule_id, + "start_date": current_subscription_start_date, + "end_date": current_subscription_end_date, + "quantity": original_user_count, + "name": original_plan, + "id": 109, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + subscription_modify_mock.return_value = MockSubscription(subscription_params) + + desired_plan_name = PlanName.CODECOV_PRO_MONTHLY.value + desired_user_count = 26 + desired_plan = {"value": desired_plan_name, "quantity": desired_user_count} + self.stripe.modify_subscription(owner, desired_plan) + schedule_release_mock.assert_called_once_with(schedule_id) + self._assert_subscription_modify( + subscription_modify_mock, owner, subscription_params, desired_plan + ) + + owner.refresh_from_db() + assert owner.plan == desired_plan_name + assert owner.plan_user_count == desired_user_count + + @patch("services.billing.stripe.SubscriptionSchedule.modify") + @patch("services.billing.stripe.Subscription.retrieve") + def test_modify_subscription_with_schedule_modifies_schedule_when_plan_downgrades( + self, retrieve_subscription_mock, schedule_modify_mock + ): + original_user_count = 15 + original_plan = PlanName.CODECOV_PRO_YEARLY.value + stripe_subscription_id = "33043sdf" + owner = OwnerFactory( + plan=original_plan, + plan_user_count=original_user_count, + stripe_subscription_id=stripe_subscription_id, + ) + + schedule_id = "sub_sched_1K77Y5GlVGuVgOrkJrLjRn2e" + current_subscription_start_date = 1639628096 + current_subscription_end_date = 1644107871 + subscription_params = { + "schedule_id": schedule_id, + "start_date": current_subscription_start_date, + "end_date": current_subscription_end_date, + "quantity": original_user_count, + "name": Plan.objects.get(name=original_plan).stripe_id, + "id": 110, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + + desired_plan_name = PlanName.CODECOV_PRO_MONTHLY.value + desired_user_count = 15 + desired_plan = {"value": desired_plan_name, "quantity": desired_user_count} + + self.stripe.modify_subscription(owner, desired_plan) + + self._assert_schedule_modify( + schedule_modify_mock, owner, subscription_params, desired_plan, schedule_id + ) + + owner.refresh_from_db() + assert owner.plan == original_plan + assert owner.plan_user_count == original_user_count + + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.SubscriptionSchedule.release") + def test_modify_subscription_with_schedule_releases_schedule_when_plan_upgrades( + self, + schedule_release_mock, + retrieve_subscription_mock, + subscription_modify_mock, + ): + original_user_count = 15 + original_plan = PlanName.CODECOV_PRO_MONTHLY.value + stripe_subscription_id = "33043sdf" + owner = OwnerFactory( + plan=original_plan, + plan_user_count=original_user_count, + stripe_subscription_id=stripe_subscription_id, + ) + + schedule_id = "sub_sched_1K77Y5GlVGuVgOrkJrLjRn2e" + current_subscription_start_date = 1639628096 + current_subscription_end_date = 1644107871 + subscription_params = { + "schedule_id": schedule_id, + "start_date": current_subscription_start_date, + "end_date": current_subscription_end_date, + "quantity": original_user_count, + "name": original_plan, + "id": 111, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + subscription_modify_mock.return_value = MockSubscription(subscription_params) + + desired_plan_name = PlanName.CODECOV_PRO_YEARLY.value + desired_user_count = 15 + desired_plan = {"value": desired_plan_name, "quantity": desired_user_count} + + self.stripe.modify_subscription(owner, desired_plan) + schedule_release_mock.assert_called_once_with(schedule_id) + self._assert_subscription_modify( + subscription_modify_mock, owner, subscription_params, desired_plan + ) + + owner.refresh_from_db() + assert owner.plan == desired_plan_name + assert owner.plan_user_count == desired_user_count + + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.SubscriptionSchedule.release") + def test_modify_subscription_with_schedule_releases_schedule_when_plan_upgrades_and_count_decreases( + self, + schedule_release_mock, + retrieve_subscription_mock, + subscription_modify_mock, + ): + original_user_count = 15 + original_plan = PlanName.CODECOV_PRO_MONTHLY.value + stripe_subscription_id = "33043sdf" + owner = OwnerFactory( + plan=original_plan, + plan_user_count=original_user_count, + stripe_subscription_id=stripe_subscription_id, + ) + + schedule_id = "sub_sched_1K77Y5GlVGuVgOrkJrLjRn2e" + current_subscription_start_date = 1639628096 + current_subscription_end_date = 1644107871 + subscription_params = { + "schedule_id": schedule_id, + "start_date": current_subscription_start_date, + "end_date": current_subscription_end_date, + "quantity": original_user_count, + "name": original_plan, + "id": 112, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + subscription_modify_mock.return_value = MockSubscription(subscription_params) + + desired_plan_name = PlanName.CODECOV_PRO_YEARLY.value + desired_user_count = 10 + desired_plan = {"value": desired_plan_name, "quantity": desired_user_count} + self.stripe.modify_subscription(owner, desired_plan) + schedule_release_mock.assert_called_once_with(schedule_id) + self._assert_subscription_modify( + subscription_modify_mock, owner, subscription_params, desired_plan + ) + + owner.refresh_from_db() + assert owner.plan == desired_plan_name + assert owner.plan_user_count == desired_user_count + + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.billing.stripe.SubscriptionSchedule.release") + def test_modify_subscription_with_schedule_releases_schedule_when_plan_downgrades_and_count_increases( + self, + schedule_release_mock, + retrieve_subscription_mock, + subscription_modify_mock, + ): + original_user_count = 15 + original_plan = PlanName.CODECOV_PRO_YEARLY.value + stripe_subscription_id = "33043sdf" + owner = OwnerFactory( + plan=original_plan, + plan_user_count=original_user_count, + stripe_subscription_id=stripe_subscription_id, + ) + + schedule_id = "sub_sched_1K77Y5GlVGuVgOrkJrLjRn2e" + current_subscription_start_date = 1639628096 + current_subscription_end_date = 1644107871 + subscription_params = { + "schedule_id": schedule_id, + "start_date": current_subscription_start_date, + "end_date": current_subscription_end_date, + "quantity": original_user_count, + "name": original_plan, + "id": 113, + } + + retrieve_subscription_mock.return_value = MockSubscription(subscription_params) + subscription_modify_mock.return_value = MockSubscription(subscription_params) + + desired_plan_name = PlanName.CODECOV_PRO_MONTHLY.value + desired_user_count = 20 + desired_plan = {"value": desired_plan_name, "quantity": desired_user_count} + + self.stripe.modify_subscription(owner, desired_plan) + schedule_release_mock.assert_called_once_with(schedule_id) + self._assert_subscription_modify( + subscription_modify_mock, owner, subscription_params, desired_plan + ) + + owner.refresh_from_db() + assert owner.plan == desired_plan_name + assert owner.plan_user_count == desired_user_count + + def test_get_proration_params(self): + # Test same plan, increased users + owner = OwnerFactory(plan=PlanName.CODECOV_PRO_YEARLY.value, plan_user_count=10) + plan = Plan.objects.get(name=PlanName.CODECOV_PRO_YEARLY.value) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=14 + ) + == "always_invoice" + ) + + # Test same plan, decrease users + owner = OwnerFactory(plan=PlanName.CODECOV_PRO_YEARLY.value, plan_user_count=20) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=14 + ) + == "none" + ) + + # Test going from monthly to yearly + owner = OwnerFactory( + plan=PlanName.CODECOV_PRO_MONTHLY.value, plan_user_count=20 + ) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=14 + ) + == "always_invoice" + ) + + # monthly to Sentry monthly plan + owner = OwnerFactory( + plan=PlanName.CODECOV_PRO_MONTHLY.value, plan_user_count=20 + ) + plan = Plan.objects.get(name=PlanName.SENTRY_MONTHLY.value) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=19 + ) + == "none" + ) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=20 + ) + == "always_invoice" + ) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=21 + ) + == "always_invoice" + ) + + # yearly to Sentry monthly plan + owner = OwnerFactory(plan=PlanName.CODECOV_PRO_YEARLY.value, plan_user_count=20) + plan = Plan.objects.get(name=PlanName.SENTRY_MONTHLY.value) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=19 + ) + == "none" + ) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=20 + ) + == "none" + ) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=21 + ) + == "always_invoice" + ) + + # monthly to Sentry monthly plan + owner = OwnerFactory( + plan=PlanName.CODECOV_PRO_MONTHLY.value, plan_user_count=20 + ) + plan = Plan.objects.get(name=PlanName.SENTRY_MONTHLY.value) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=19 + ) + == "none" + ) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=20 + ) + == "always_invoice" + ) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=21 + ) + == "always_invoice" + ) + + # yearly to Sentry yearly plan + owner = OwnerFactory(plan=PlanName.CODECOV_PRO_YEARLY.value, plan_user_count=20) + plan = Plan.objects.get(name=PlanName.SENTRY_YEARLY.value) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=19 + ) + == "none" + ) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=20 + ) + == "always_invoice" + ) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=21 + ) + == "always_invoice" + ) + + # monthly to Sentry yearly plan + owner = OwnerFactory( + plan=PlanName.CODECOV_PRO_MONTHLY.value, plan_user_count=20 + ) + plan = Plan.objects.get(name=PlanName.SENTRY_YEARLY.value) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=19 + ) + == "always_invoice" + ) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=20 + ) + == "always_invoice" + ) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=21 + ) + == "always_invoice" + ) + + # Team to Sentry + owner = OwnerFactory(plan=PlanName.TEAM_MONTHLY.value, plan_user_count=10) + plan = Plan.objects.get(name=PlanName.SENTRY_MONTHLY.value) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=10 + ) + == "always_invoice" + ) + + # Team to Pro + owner = OwnerFactory(plan=PlanName.TEAM_MONTHLY.value, plan_user_count=10) + plan = Plan.objects.get(name=PlanName.CODECOV_PRO_MONTHLY.value) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=10 + ) + == "always_invoice" + ) + + # Sentry to Team + owner = OwnerFactory(plan=PlanName.SENTRY_MONTHLY.value, plan_user_count=10) + plan = Plan.objects.get(name=PlanName.TEAM_MONTHLY.value) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=10 + ) + == "none" + ) + + # Sentry to Pro + owner = OwnerFactory( + plan=PlanName.CODECOV_PRO_MONTHLY.value, plan_user_count=10 + ) + plan = Plan.objects.get(name=PlanName.TEAM_MONTHLY.value) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=10 + ) + == "none" + ) + + @patch("services.billing.stripe.checkout.Session.create") + def test_create_checkout_session_with_no_stripe_customer_id( + self, create_checkout_session_mock + ): + stripe_customer_id = None + owner = OwnerFactory( + service=Service.GITHUB.value, + stripe_customer_id=stripe_customer_id, + ) + expected_id = "fkkgosd" + create_checkout_session_mock.return_value = {"id": expected_id} + desired_quantity = 25 + desired_plan = { + "value": PlanName.CODECOV_PRO_MONTHLY.value, + "quantity": desired_quantity, + } + plan = Plan.objects.get(name=desired_plan["value"]) + + assert self.stripe.create_checkout_session(owner, desired_plan) == expected_id + + create_checkout_session_mock.assert_called_once_with( + billing_address_collection="required", + payment_method_configuration=settings.STRIPE_PAYMENT_METHOD_CONFIGURATION_ID, + payment_method_collection="if_required", + client_reference_id=str(owner.ownerid), + customer=None, + success_url=f"{settings.CODECOV_DASHBOARD_URL}/plan/gh/{owner.username}?success", + cancel_url=f"{settings.CODECOV_DASHBOARD_URL}/plan/gh/{owner.username}?cancel", + mode="subscription", + line_items=[ + { + "price": plan.stripe_id, + "quantity": desired_quantity, + } + ], + subscription_data={ + "metadata": { + "service": owner.service, + "obo_organization": owner.ownerid, + "username": owner.username, + "obo_name": self.user.name, + "obo_email": self.user.email, + "obo": self.user.ownerid, + }, + }, + tax_id_collection={"enabled": True}, + customer_update=None, + ) + + @patch("services.billing.stripe.checkout.Session.create") + def test_create_checkout_session_with_stripe_customer_id( + self, create_checkout_session_mock + ): + stripe_customer_id = "test-cusa78723hb4@" + owner = OwnerFactory( + service=Service.GITHUB.value, + stripe_customer_id=stripe_customer_id, + ) + expected_id = "fkkgosd" + create_checkout_session_mock.return_value = {"id": expected_id} + desired_quantity = 25 + desired_plan = { + "value": PlanName.CODECOV_PRO_MONTHLY.value, + "quantity": desired_quantity, + } + + assert self.stripe.create_checkout_session(owner, desired_plan) == expected_id + + plan = Plan.objects.get(name=desired_plan["value"]) + + create_checkout_session_mock.assert_called_once_with( + billing_address_collection="required", + payment_method_configuration=settings.STRIPE_PAYMENT_METHOD_CONFIGURATION_ID, + payment_method_collection="if_required", + client_reference_id=str(owner.ownerid), + customer=owner.stripe_customer_id, + success_url=f"{settings.CODECOV_DASHBOARD_URL}/plan/gh/{owner.username}?success", + cancel_url=f"{settings.CODECOV_DASHBOARD_URL}/plan/gh/{owner.username}?cancel", + mode="subscription", + line_items=[ + { + "price": plan.stripe_id, + "quantity": desired_quantity, + } + ], + subscription_data={ + "metadata": { + "service": owner.service, + "obo_organization": owner.ownerid, + "username": owner.username, + "obo_name": self.user.name, + "obo_email": self.user.email, + "obo": self.user.ownerid, + }, + }, + tax_id_collection={"enabled": True}, + customer_update={"name": "auto", "address": "auto"}, + ) + + @patch("logging.Logger.error") + @patch("services.billing.stripe.checkout.Session.create") + def test_create_checkout_session_with_invalid_plan( + self, create_checkout_session_mock, logger_error_mock + ): + stripe_customer_id = "test-cusa78723hb4@" + owner = OwnerFactory( + service=Service.GITHUB.value, + stripe_customer_id=stripe_customer_id, + ) + desired_quantity = 25 + desired_plan = { + "value": "invalid_plan", + "quantity": desired_quantity, + } + + self.stripe.create_checkout_session(owner, desired_plan) + + create_checkout_session_mock.assert_not_called() + logger_error_mock.assert_called_once_with( + f"Plan {desired_plan['value']} not found", + extra=dict( + owner_id=owner.ownerid, + ), + ) + + def test_get_subscription_when_no_subscription(self): + owner = OwnerFactory(stripe_subscription_id=None) + assert self.stripe.get_subscription(owner) is None + + @patch("services.billing.stripe.Subscription.retrieve") + def test_get_subscription_returns_stripe_data(self, subscription_retrieve_mock): + owner = OwnerFactory(stripe_subscription_id="abc") + # only including fields relevant to implementation + stripe_data_subscription = {"doesnt": "matter"} + subscription_retrieve_mock.return_value = stripe_data_subscription + assert self.stripe.get_subscription(owner) == stripe_data_subscription + subscription_retrieve_mock.assert_called_once_with( + owner.stripe_subscription_id, + expand=[ + "latest_invoice", + "customer", + "customer.invoice_settings.default_payment_method", + "customer.tax_ids", + ], + ) + + def test_update_payment_method_when_no_subscription(self): + owner = OwnerFactory(stripe_subscription_id=None) + assert self.stripe.update_payment_method(owner, "abc") is None + + @patch("services.billing.stripe.PaymentMethod.attach") + @patch("services.billing.stripe.Customer.modify") + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.StripeService._is_unverified_payment_method") + def test_update_payment_method( + self, + is_unverified_mock, + modify_sub_mock, + modify_customer_mock, + attach_payment_mock, + ): + payment_method_id = "pm_1234567" + subscription_id = "sub_abc" + customer_id = "cus_abc" + owner = OwnerFactory( + stripe_subscription_id=subscription_id, stripe_customer_id=customer_id + ) + is_unverified_mock.return_value = False + self.stripe.update_payment_method(owner, payment_method_id) + attach_payment_mock.assert_called_once_with( + payment_method_id, customer=customer_id + ) + modify_customer_mock.assert_called_once_with( + customer_id, invoice_settings={"default_payment_method": payment_method_id} + ) + + modify_sub_mock.assert_called_once_with( + subscription_id, default_payment_method=payment_method_id + ) + + @patch("services.billing.stripe.PaymentMethod.attach") + @patch("services.billing.stripe.Customer.modify") + @patch("services.billing.stripe.Subscription.modify") + @patch("services.billing.stripe.PaymentMethod.retrieve") + @patch("services.billing.stripe.SetupIntent.list") + def test_update_payment_method_with_unverified_payment_method( + self, + setup_intent_list_mock, + payment_method_retrieve_mock, + modify_sub_mock, + modify_customer_mock, + attach_payment_mock, + ): + # Define the mock return values + setup_intent_list_mock.return_value = MagicMock( + data=[ + MagicMock( + status="requires_action", + next_action=MagicMock( + type="verify_with_microdeposits", + verify_with_microdeposits=MagicMock( + hosted_verification_url="https://verify.stripe.com/1" + ), + ), + ) + ] + ) + payment_method_retrieve_mock.return_value = MagicMock( + type="us_bank_account", + us_bank_account=MagicMock( + status="requires_action", + next_action=MagicMock( + type="verify_with_microdeposits", + verify_with_microdeposits=MagicMock( + hosted_verification_url="https://verify.stripe.com/1" + ), + ), + ), + ) + modify_sub_mock.return_value = MagicMock() + modify_customer_mock.return_value = MagicMock() + attach_payment_mock.return_value = MagicMock() + + # Create a mock owner object + subscription_id = "sub_abc" + customer_id = "cus_abc" + owner = OwnerFactory( + stripe_subscription_id=subscription_id, stripe_customer_id=customer_id + ) + + result = self.stripe.update_payment_method(owner, "abc") + + assert result is None + assert payment_method_retrieve_mock.called + assert setup_intent_list_mock.called + assert not attach_payment_mock.called + assert not modify_customer_mock.called + assert not modify_sub_mock.called + + def test_update_email_address_with_invalid_email(self): + owner = OwnerFactory(stripe_subscription_id=None) + assert self.stripe.update_email_address(owner, "not-an-email") is None + + def test_update_email_address_when_no_subscription(self): + owner = OwnerFactory(stripe_subscription_id=None) + assert self.stripe.update_email_address(owner, "test@gmail.com") is None + + @patch("services.billing.stripe.Customer.modify") + def test_update_email_address(self, modify_customer_mock): + subscription_id = "sub_abc" + customer_id = "cus_abc" + email = "test@gmail.com" + owner = OwnerFactory( + stripe_subscription_id=subscription_id, stripe_customer_id=customer_id + ) + self.stripe.update_email_address(owner, "test@gmail.com") + modify_customer_mock.assert_called_once_with(customer_id, email=email) + + @patch("logging.Logger.error") + def test_update_billing_address_with_invalid_address(self, log_error_mock): + owner = OwnerFactory(stripe_customer_id="123", stripe_subscription_id="123") + assert self.stripe.update_billing_address(owner, "John Doe", "gabagool") is None + log_error_mock.assert_called_with( + "Unable to update billing address for customer", + extra={ + "customer_id": "123", + "subscription_id": "123", + }, + ) + + def test_update_billing_address_when_no_customer_id(self): + owner = OwnerFactory(stripe_customer_id=None) + assert ( + self.stripe.update_billing_address( + owner, + name="John Doe", + billing_address={ + "line1": "45 Fremont St.", + "line2": "", + "city": "San Francisco", + "state": "CA", + "country": "US", + "postal_code": "94105", + }, + ) + is None + ) + + @patch("services.billing.stripe.Customer.retrieve") + @patch("services.billing.stripe.PaymentMethod.modify") + @patch("services.billing.stripe.Customer.modify") + def test_update_billing_address( + self, modify_customer_mock, modify_payment_mock, retrieve_customer_mock + ): + subscription_id = "sub_abc" + customer_id = "cus_abc" + owner = OwnerFactory( + stripe_subscription_id=subscription_id, stripe_customer_id=customer_id + ) + billing_address = { + "line1": "45 Fremont St.", + "line2": "", + "city": "San Francisco", + "state": "CA", + "country": "US", + "postal_code": "94105", + } + self.stripe.update_billing_address( + owner, + name="John Doe", + billing_address=billing_address, + ) + + retrieve_customer_mock.assert_called_once() + modify_payment_mock.assert_called_once() + modify_customer_mock.assert_called_once_with( + customer_id, address=billing_address + ) + + @patch("services.billing.stripe.Invoice.retrieve") + def test_get_invoice_not_found(self, retrieve_invoice_mock): + invoice_id = "abc" + retrieve_invoice_mock.side_effect = InvalidRequestError( + message="not found", param=invoice_id + ) + assert self.stripe.get_invoice(OwnerFactory(), invoice_id) is None + retrieve_invoice_mock.assert_called_once_with(invoice_id) + + @patch("services.billing.stripe.Invoice.retrieve") + def test_get_invoice_customer_dont_match(self, retrieve_invoice_mock): + owner = OwnerFactory(stripe_customer_id="something_very_very_random_cus_abc") + invoice_id = "abc" + invoice = {"invoice_id": "abc", "customer": "cus_abc"} + retrieve_invoice_mock.return_value = invoice + assert self.stripe.get_invoice(owner, invoice_id) is None + retrieve_invoice_mock.assert_called_once_with(invoice_id) + + @patch("services.billing.stripe.Invoice.retrieve") + def test_get_invoice(self, retrieve_invoice_mock): + customer_id = "cus_abc" + owner = OwnerFactory(stripe_customer_id=customer_id) + invoice_id = "abc" + invoice = {"invoice_id": "abc", "customer": customer_id} + retrieve_invoice_mock.return_value = invoice + assert self.stripe.get_invoice(owner, invoice_id) == invoice + retrieve_invoice_mock.assert_called_once_with(invoice_id) + + @patch("services.billing.stripe.Coupon.create") + @patch("services.billing.stripe.Customer.modify") + def test_apply_cancellation_discount( + self, customer_modify_mock, coupon_create_mock + ): + coupon_create_mock.return_value = MagicMock(id="test-coupon-id") + + owner = OwnerFactory( + stripe_subscription_id="test-subscription-id", + stripe_customer_id="test-customer-id", + plan="users-pr-inappm", + ) + self.stripe.apply_cancellation_discount(owner) + + coupon_create_mock.assert_called_once_with( + percent_off=30.0, + duration="repeating", + duration_in_months=6, + name="30% off for 6 months", + max_redemptions=1, + metadata={ + "ownerid": owner.ownerid, + "username": owner.username, + "email": owner.email, + "name": owner.name, + }, + ) + customer_modify_mock.assert_called_once_with( + "test-customer-id", + coupon="test-coupon-id", + ) + + owner.refresh_from_db() + assert owner.stripe_coupon_id == "test-coupon-id" + + @patch("services.billing.stripe.Coupon.create") + @patch("services.billing.stripe.Customer.modify") + def test_apply_cancellation_discount_yearly( + self, customer_modify_mock, coupon_create_mock + ): + owner = OwnerFactory( + stripe_customer_id="test-customer-id", + stripe_subscription_id=None, + plan="users-inappy", + ) + self.stripe.apply_cancellation_discount(owner) + + assert not customer_modify_mock.called + assert not coupon_create_mock.called + assert owner.stripe_coupon_id is None + + @patch("services.billing.stripe.Coupon.create") + @patch("services.billing.stripe.Customer.modify") + def test_apply_cancellation_discount_no_subscription( + self, customer_modify_mock, coupon_create_mock + ): + owner = OwnerFactory( + stripe_customer_id="test-customer-id", + stripe_subscription_id=None, + ) + self.stripe.apply_cancellation_discount(owner) + + assert not customer_modify_mock.called + assert not coupon_create_mock.called + assert owner.stripe_coupon_id is None + + @patch("services.billing.stripe.Coupon.create") + @patch("services.billing.stripe.Customer.modify") + def test_apply_cancellation_discount_existing_coupon( + self, customer_modify_mock, coupon_create_mock + ): + owner = OwnerFactory( + stripe_customer_id="test-customer-id", + stripe_subscription_id="test-subscription-id", + stripe_coupon_id="test-coupon-id", + ) + self.stripe.apply_cancellation_discount(owner) + + assert not customer_modify_mock.called + assert not coupon_create_mock.called + + @patch("services.billing.stripe.SetupIntent.create") + def test_create_setup_intent(self, setup_intent_create_mock): + owner = OwnerFactory(stripe_customer_id="test-customer-id") + setup_intent_create_mock.return_value = {"client_secret": "test-client-secret"} + resp = self.stripe.create_setup_intent(owner) + assert resp["client_secret"] == "test-client-secret" + + @patch("services.billing.stripe.PaymentIntent.list") + @patch("services.billing.stripe.SetupIntent.list") + def test_get_unverified_payment_methods( + self, setup_intent_list_mock, payment_intent_list_mock + ): + owner = OwnerFactory(stripe_customer_id="test-customer-id") + payment_intent = PaymentIntent.construct_from( + { + "id": "pi_123", + "payment_method": "pm_123", + "next_action": { + "type": "verify_with_microdeposits", + "verify_with_microdeposits": { + "hosted_verification_url": "https://verify.stripe.com/1" + }, + }, + }, + "fake_api_key", + ) + + setup_intent = SetupIntent.construct_from( + { + "id": "si_123", + "payment_method": "pm_456", + "next_action": { + "type": "verify_with_microdeposits", + "verify_with_microdeposits": { + "hosted_verification_url": "https://verify.stripe.com/2" + }, + }, + }, + "fake_api_key", + ) + + payment_intent_list_mock.return_value.data = [payment_intent] + payment_intent_list_mock.return_value.has_more = False + setup_intent_list_mock.return_value.data = [setup_intent] + setup_intent_list_mock.return_value.has_more = False + + expected = [ + { + "payment_method_id": "pm_123", + "hosted_verification_url": "https://verify.stripe.com/1", + }, + { + "payment_method_id": "pm_456", + "hosted_verification_url": "https://verify.stripe.com/2", + }, + ] + assert self.stripe.get_unverified_payment_methods(owner) == expected + + @patch("services.billing.stripe.PaymentIntent.list") + @patch("services.billing.stripe.SetupIntent.list") + def test_get_unverified_payment_methods_pagination( + self, setup_intent_list_mock, payment_intent_list_mock + ): + owner = OwnerFactory(stripe_customer_id="test-customer-id") + + # Create 42 payment intents with only 2 having microdeposits verification + payment_intents = [] + for i in range(42): + next_action = None + if i in [0, 41]: # First and last have verification + next_action = { + "type": "verify_with_microdeposits", + "verify_with_microdeposits": { + "hosted_verification_url": f"https://verify.stripe.com/pi_{i}" + }, + } + payment_intents.append( + PaymentIntent.construct_from( + { + "id": f"pi_{i}", + "payment_method": f"pm_pi_{i}", + "next_action": next_action, + }, + "fake_api_key", + ) + ) + + # Create 42 setup intents with only 2 having microdeposits verification + setup_intents = [] + for i in range(42): + next_action = None + if i in [0, 41]: # First and last have verification + next_action = { + "type": "verify_with_microdeposits", + "verify_with_microdeposits": { + "hosted_verification_url": f"https://verify.stripe.com/si_{i}" + }, + } + setup_intents.append( + SetupIntent.construct_from( + { + "id": f"si_{i}", + "payment_method": f"pm_si_{i}", + "next_action": next_action, + }, + "fake_api_key", + ) + ) + + # Split into pages of 20 + payment_intent_pages = [ + type( + "obj", + (object,), + { + "data": payment_intents[i : i + 20], + "has_more": i + 20 < len(payment_intents), + }, + ) + for i in range(0, len(payment_intents), 20) + ] + + setup_intent_pages = [ + type( + "obj", + (object,), + { + "data": setup_intents[i : i + 20], + "has_more": i + 20 < len(setup_intents), + }, + ) + for i in range(0, len(setup_intents), 20) + ] + + payment_intent_list_mock.side_effect = payment_intent_pages + setup_intent_list_mock.side_effect = setup_intent_pages + + expected = [ + { + "payment_method_id": "pm_pi_0", + "hosted_verification_url": "https://verify.stripe.com/pi_0", + }, + { + "payment_method_id": "pm_pi_41", + "hosted_verification_url": "https://verify.stripe.com/pi_41", + }, + { + "payment_method_id": "pm_si_0", + "hosted_verification_url": "https://verify.stripe.com/si_0", + }, + { + "payment_method_id": "pm_si_41", + "hosted_verification_url": "https://verify.stripe.com/si_41", + }, + ] + + result = self.stripe.get_unverified_payment_methods(owner) + assert result == expected + assert len(result) == 4 # Verify we got exactly 4 results + + # Verify pagination calls + payment_intent_calls = [ + call(customer="test-customer-id", limit=20, starting_after=None), + call(customer="test-customer-id", limit=20, starting_after="pi_19"), + call(customer="test-customer-id", limit=20, starting_after="pi_39"), + ] + setup_intent_calls = [ + call(customer="test-customer-id", limit=20, starting_after=None), + call(customer="test-customer-id", limit=20, starting_after="si_19"), + call(customer="test-customer-id", limit=20, starting_after="si_39"), + ] + + payment_intent_list_mock.assert_has_calls(payment_intent_calls) + setup_intent_list_mock.assert_has_calls(setup_intent_calls) + + +class MockPaymentService(AbstractPaymentService): + def list_filtered_invoices(self, owner, limit=10): + return f"{owner.ownerid} {limit}" + + def get_invoice(self, owner, id): + pass + + def delete_subscription(self, owner): + pass + + def modify_subscription(self, owner, plan): + pass + + def create_checkout_session(self, owner, plan): + pass + + def get_subscription(self, owner, plan): + pass + + def update_payment_method(self, owner, plan): + pass + + def update_email_address(self, owner, email_address): + pass + + def update_billing_address(self, owner, name, billing_address): + pass + + def get_schedule(self, owner): + pass + + def apply_cancellation_discount(self, owner): + pass + + def create_setup_intent(self, owner): + pass + + +class BillingServiceTests(TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + + def setUp(self): + self.mock_payment_service = MockPaymentService() + self.billing_service = BillingService(payment_service=self.mock_payment_service) + + def test_default_payment_service_is_stripe(self): + requesting_user = OwnerFactory() + assert isinstance( + BillingService(requesting_user=requesting_user).payment_service, + StripeService, + ) + + def test_list_filtered_invoices_calls_payment_service_list_filtered_invoices_with_limit( + self, + ): + owner = OwnerFactory() + assert self.billing_service.list_filtered_invoices( + owner + ) == self.mock_payment_service.list_filtered_invoices(owner) + + @patch("services.tests.test_billing.MockPaymentService.delete_subscription") + def test_update_plan_to_users_developer_deletes_subscription_if_user_has_stripe_subscription( + self, delete_subscription_mock + ): + owner = OwnerFactory(stripe_subscription_id="tor_dsoe") + self.billing_service.update_plan(owner, {"value": DEFAULT_FREE_PLAN}) + delete_subscription_mock.assert_called_once_with(owner) + + @patch("shared.plan.service.PlanService.set_default_plan_data") + @patch("services.tests.test_billing.MockPaymentService.create_checkout_session") + @patch("services.tests.test_billing.MockPaymentService.modify_subscription") + @patch("services.tests.test_billing.MockPaymentService.delete_subscription") + def test_update_plan_to_users_developer_sets_plan_if_no_subscription_id( + self, + delete_subscription_mock, + modify_subscription_mock, + create_checkout_session_mock, + set_default_plan_data, + ): + owner = OwnerFactory() + self.billing_service.update_plan(owner, {"value": DEFAULT_FREE_PLAN}) + + set_default_plan_data.assert_called_once() + + delete_subscription_mock.assert_not_called() + modify_subscription_mock.assert_not_called() + create_checkout_session_mock.assert_not_called() + + @patch("shared.plan.service.PlanService.set_default_plan_data") + @patch("services.tests.test_billing.MockPaymentService.create_checkout_session") + @patch("services.tests.test_billing.MockPaymentService.modify_subscription") + @patch("services.tests.test_billing.MockPaymentService.delete_subscription") + @patch("services.tests.test_billing.MockPaymentService.get_subscription") + def test_update_plan_modifies_subscription_if_user_plan_and_subscription_exists( + self, + get_subscription_mock, + delete_subscription_mock, + modify_subscription_mock, + create_checkout_session_mock, + set_default_plan_data, + ): + owner = OwnerFactory(stripe_subscription_id=10) + desired_plan = {"value": PlanName.CODECOV_PRO_YEARLY.value, "quantity": 10} + + get_subscription_mock.return_value = stripe.util.convert_to_stripe_object( + { + "schedule": None, + "current_period_start": 1489799420, + "current_period_end": 1492477820, + "quantity": 10, + "name": PlanName.CODECOV_PRO_YEARLY.value, + "id": 215, + "status": "active", + } + ) + + self.billing_service.update_plan(owner, desired_plan) + modify_subscription_mock.assert_called_once_with(owner, desired_plan) + + set_default_plan_data.assert_not_called() + delete_subscription_mock.assert_not_called() + create_checkout_session_mock.assert_not_called() + + @patch("shared.plan.service.PlanService.set_default_plan_data") + @patch("services.tests.test_billing.MockPaymentService.create_checkout_session") + @patch("services.tests.test_billing.MockPaymentService.modify_subscription") + @patch("services.tests.test_billing.MockPaymentService.delete_subscription") + def test_update_plan_creates_checkout_session_if_user_plan_and_no_subscription( + self, + delete_subscription_mock, + modify_subscription_mock, + create_checkout_session_mock, + set_default_plan_data, + ): + owner = OwnerFactory(stripe_subscription_id=None) + desired_plan = {"value": PlanName.CODECOV_PRO_YEARLY.value, "quantity": 10} + self.billing_service.update_plan(owner, desired_plan) + + create_checkout_session_mock.assert_called_once_with(owner, desired_plan) + + set_default_plan_data.assert_not_called() + delete_subscription_mock.assert_not_called() + modify_subscription_mock.assert_not_called() + + @patch("services.tests.test_billing.MockPaymentService.get_subscription") + @patch("services.tests.test_billing.MockPaymentService.create_checkout_session") + @patch("services.tests.test_billing.MockPaymentService.modify_subscription") + @patch("services.billing.BillingService._cleanup_incomplete_subscription") + def test_update_plan_cleans_up_incomplete_subscription_and_creates_new_checkout( + self, + cleanup_incomplete_mock, + modify_subscription_mock, + create_checkout_session_mock, + get_subscription_mock, + ): + owner = OwnerFactory(stripe_subscription_id="sub_123") + desired_plan = {"value": PlanName.CODECOV_PRO_YEARLY.value, "quantity": 10} + + subscription = stripe.Subscription.construct_from( + {"status": "incomplete"}, "fake_api_key" + ) + get_subscription_mock.return_value = subscription + + self.billing_service.update_plan(owner, desired_plan) + + cleanup_incomplete_mock.assert_called_once_with(subscription, owner) + create_checkout_session_mock.assert_called_once_with(owner, desired_plan) + modify_subscription_mock.assert_not_called() + + @patch("services.billing.stripe.PaymentIntent.retrieve") + @patch("services.billing.stripe.Subscription.delete") + def test_cleanup_incomplete_subscription(self, delete_mock, retrieve_mock): + owner = OwnerFactory(stripe_subscription_id="sub_123") + + payment_intent = stripe.PaymentIntent.construct_from( + {"id": "pi_123", "status": "requires_action"}, "fake_api_key" + ) + subscription = stripe.Subscription.construct_from( + {"id": "abcd", "latest_invoice": {"payment_intent": "pi_123"}}, + "fake_api_key", + ) + retrieve_mock.return_value = payment_intent + + self.billing_service._cleanup_incomplete_subscription(subscription, owner) + + retrieve_mock.assert_called_once_with("pi_123") + delete_mock.assert_called_once_with(subscription) + + @patch("services.billing.stripe.PaymentIntent.retrieve") + @patch("services.billing.stripe.Subscription.delete") + def test_cleanup_incomplete_subscription_no_latest_invoice( + self, delete_mock, retrieve_mock + ): + owner = OwnerFactory(stripe_subscription_id="sub_123") + + subscription = stripe.Subscription.construct_from( + {"id": "sub_123"}, "fake_api_key" + ) + + result = self.billing_service._cleanup_incomplete_subscription( + subscription, owner + ) + + assert result is None + delete_mock.assert_not_called() + retrieve_mock.assert_not_called() + assert owner.stripe_subscription_id == "sub_123" + + @patch("services.billing.stripe.PaymentIntent.retrieve") + @patch("services.billing.stripe.Subscription.delete") + def test_cleanup_incomplete_subscription_no_payment_intent( + self, delete_mock, retrieve_mock + ): + owner = OwnerFactory(stripe_subscription_id="sub_123") + + class MockSubscription: + id = "sub_123" + + def get(self, key): + if key == "latest_invoice": + return {"payment_intent": None} + return None + + subscription = MockSubscription() + + result = self.billing_service._cleanup_incomplete_subscription( + subscription, owner + ) + + assert result is None + delete_mock.assert_not_called() + retrieve_mock.assert_not_called() + assert owner.stripe_subscription_id == "sub_123" + + @patch("services.billing.stripe.PaymentIntent.retrieve") + @patch("services.billing.stripe.Subscription.delete") + def test_cleanup_incomplete_subscription_delete_fails( + self, delete_mock, retrieve_mock + ): + owner = OwnerFactory(stripe_subscription_id="sub_123") + + payment_intent = stripe.PaymentIntent.construct_from( + {"id": "pi_123", "status": "requires_action"}, "fake_api_key" + ) + subscription = stripe.Subscription.construct_from( + {"id": "abcd", "latest_invoice": {"payment_intent": "pi_123"}}, + "fake_api_key", + ) + retrieve_mock.return_value = payment_intent + delete_mock.side_effect = Exception("Delete failed") + + result = self.billing_service._cleanup_incomplete_subscription( + subscription, owner + ) + + assert result is None + retrieve_mock.assert_called_once_with("pi_123") + delete_mock.assert_called_once_with(subscription) + assert owner.stripe_subscription_id == "sub_123" + + @patch("shared.plan.service.PlanService.set_default_plan_data") + @patch("services.tests.test_billing.MockPaymentService.create_checkout_session") + @patch("services.tests.test_billing.MockPaymentService.modify_subscription") + @patch("services.tests.test_billing.MockPaymentService.delete_subscription") + def test_update_plan_does_nothing_if_not_switching_to_user_plan( + self, + delete_subscription_mock, + modify_subscription_mock, + create_checkout_session_mock, + set_default_plan_data, + ): + owner = OwnerFactory() + desired_plan = {"value": "v4-50m"} + self.billing_service.update_plan(owner, desired_plan) + + set_default_plan_data.assert_not_called() + delete_subscription_mock.assert_not_called() + modify_subscription_mock.assert_not_called() + create_checkout_session_mock.assert_not_called() + + @patch("services.tests.test_billing.MockPaymentService.create_checkout_session") + @patch("services.tests.test_billing.MockPaymentService.modify_subscription") + def test_update_plan_sentry_user_sentrym( + self, modify_subscription_mock, create_checkout_session_mock + ): + owner = OwnerFactory(sentry_user_id="sentry-user") + desired_plan = {"value": PlanName.SENTRY_MONTHLY.value} + self.billing_service.update_plan(owner, desired_plan) + + modify_subscription_mock.assert_not_called() + create_checkout_session_mock.assert_called_once_with(owner, desired_plan) + + @patch("services.tests.test_billing.MockPaymentService.create_checkout_session") + @patch("services.tests.test_billing.MockPaymentService.modify_subscription") + def test_update_plan_sentry_user_sentryy( + self, modify_subscription_mock, create_checkout_session_mock + ): + owner = OwnerFactory(sentry_user_id="sentry-user") + desired_plan = {"value": PlanName.SENTRY_YEARLY.value} + self.billing_service.update_plan(owner, desired_plan) + + modify_subscription_mock.assert_not_called() + create_checkout_session_mock.assert_called_once_with(owner, desired_plan) + + @patch("services.tests.test_billing.MockPaymentService.get_subscription") + def test_get_subscription(self, get_subscription_mock): + owner = OwnerFactory() + self.billing_service.get_subscription(owner) + get_subscription_mock.assert_called_once_with(owner) + + @patch("services.tests.test_billing.MockPaymentService.update_payment_method") + def test_update_payment_method(self, get_subscription_mock): + owner = OwnerFactory() + self.billing_service.update_payment_method(owner, "abc") + get_subscription_mock.assert_called_once_with(owner, "abc") + + @patch("services.tests.test_billing.MockPaymentService.update_email_address") + def test_email_address(self, get_subscription_mock): + owner = OwnerFactory() + self.billing_service.update_email_address(owner, "test@gmail.com", False) + get_subscription_mock.assert_called_once_with(owner, "test@gmail.com", False) + + @patch("services.tests.test_billing.MockPaymentService.get_invoice") + def test_get_invoice(self, get_invoice_mock): + owner = OwnerFactory() + self.billing_service.get_invoice(owner, "abc") + get_invoice_mock.assert_called_once_with(owner, "abc") diff --git a/apps/codecov-api/services/tests/test_bundle_analysis.py b/apps/codecov-api/services/tests/test_bundle_analysis.py new file mode 100644 index 0000000000..7240b09880 --- /dev/null +++ b/apps/codecov-api/services/tests/test_bundle_analysis.py @@ -0,0 +1,178 @@ +from unittest.mock import patch + +import pytest +from django.test import TestCase +from shared.api_archive.archive import ArchiveService +from shared.bundle_analysis import BundleAnalysisReport as SharedBundleAnalysisReport +from shared.bundle_analysis import ( + BundleAnalysisReportLoader, + BundleChange, + StoragePaths, +) +from shared.bundle_analysis.storage import get_bucket_name +from shared.django_apps.core.tests.factories import CommitFactory, RepositoryFactory +from shared.storage.memory import MemoryStorageService + +from reports.models import CommitReport +from reports.tests.factories import CommitReportFactory +from services.bundle_analysis import ( + BundleAnalysisComparison, + BundleAnalysisReport, + BundleComparison, + BundleReport, + load_report, +) + + +@pytest.mark.django_db +@patch("services.bundle_analysis.get_appropriate_storage_service") +def test_load_report(get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + repo = RepositoryFactory() + commit = CommitFactory(repository=repo) + + # no commit report record + assert load_report(commit) is None + + commit_report = CommitReportFactory( + commit=commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(repo), + report_key=commit_report.external_id, + ) + + # nothing in storage + assert load_report(commit) is None + + with open("./services/tests/samples/bundle_report.sqlite", "rb") as f: + storage.write_file(get_bucket_name(), storage_path, f) + + report = load_report(commit) + assert report is not None + assert isinstance(report, SharedBundleAnalysisReport) + + +class TestBundleComparison(TestCase): + @patch("services.bundle_analysis.SharedBundleChange") + def test_bundle_comparison(self, mock_shared_bundle_change): + mock_shared_bundle_change = BundleChange( + bundle_name="bundle1", + change_type=BundleChange.ChangeType.ADDED, + size_delta=1000000, + percentage_delta=0.0, + ) + + bundle_comparison = BundleComparison( + mock_shared_bundle_change, + 7654321, + ) + + assert bundle_comparison.bundle_name == "bundle1" + assert bundle_comparison.change_type == "added" + assert bundle_comparison.size_delta == 1000000 + assert bundle_comparison.size_total == 7654321 + + +class TestBundleAnalysisComparison(TestCase): + def setUp(self): + self.repo = RepositoryFactory() + + self.base_commit = CommitFactory(repository=self.repo) + self.base_commit_report = CommitReportFactory( + commit=self.base_commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + self.head_commit = CommitFactory(repository=self.repo) + self.head_commit_report = CommitReportFactory( + commit=self.head_commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + @patch("services.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_comparison(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + with open("./services/tests/samples/base_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=self.base_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + with open("./services/tests/samples/head_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=self.head_commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + loader = BundleAnalysisReportLoader( + storage_service=storage, + repo_key=ArchiveService.get_archive_hash(self.head_commit.repository), + ) + + bac = BundleAnalysisComparison( + loader, + self.base_commit_report.external_id, + self.head_commit_report.external_id, + self.repo, + ) + + assert len(bac.bundles) == 5 + assert bac.size_delta == 36555 + assert bac.size_total == 201720 + + +class TestBundleReport(TestCase): + def test_bundle_comparison(self): + class MockSharedBundleReport: + def __init__(self, db_path, bundle_name): + self.bundle_name = bundle_name + + def total_size(self): + return 7654321 + + @property + def name(self): + return self.bundle_name + + bundle_comparison = BundleReport(MockSharedBundleReport("123abc", "bundle1")) + + assert bundle_comparison.name == "bundle1" + assert bundle_comparison.size_total == 7654321 + + +class TestBundleAnalysisReport(TestCase): + def setUp(self): + self.repo = RepositoryFactory() + + self.commit = CommitFactory(repository=self.repo) + self.commit_report = CommitReportFactory( + commit=self.commit, report_type=CommitReport.ReportType.BUNDLE_ANALYSIS + ) + + @patch("services.bundle_analysis.get_appropriate_storage_service") + def test_bundle_analysis_report(self, get_storage_service): + storage = MemoryStorageService({}) + get_storage_service.return_value = storage + + with open("./services/tests/samples/head_bundle_report.sqlite", "rb") as f: + storage_path = StoragePaths.bundle_report.path( + repo_key=ArchiveService.get_archive_hash(self.repo), + report_key=self.commit_report.external_id, + ) + storage.write_file(get_bucket_name(), storage_path, f) + + loader = BundleAnalysisReportLoader( + storage_service=storage, + repo_key=ArchiveService.get_archive_hash(self.commit.repository), + ) + + bar = BundleAnalysisReport(loader.load(self.commit_report.external_id)) + + assert len(bar.bundles) == 4 + assert bar.size_total == 201720 diff --git a/apps/codecov-api/services/tests/test_comparison.py b/apps/codecov-api/services/tests/test_comparison.py new file mode 100644 index 0000000000..de037ab55e --- /dev/null +++ b/apps/codecov-api/services/tests/test_comparison.py @@ -0,0 +1,1854 @@ +import asyncio +import enum +import json +from collections import Counter +from datetime import datetime +from unittest.mock import PropertyMock, patch + +import minio +import pytest +import pytz +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) +from shared.reports.api_report_service import SerializableReport +from shared.reports.resources import ReportFile +from shared.reports.types import ReportTotals +from shared.utils.merge import LineType + +from compare.models import CommitComparison +from compare.tests.factories import CommitComparisonFactory +from core.models import Commit +from reports.tests.factories import CommitReportFactory +from services.comparison import ( + CommitComparisonService, + Comparison, + ComparisonReport, + CreateChangeSummaryVisitor, + CreateLineComparisonVisitor, + FileComparison, + FileComparisonTraverseManager, + ImpactedFile, + LineComparison, + MissingComparisonReport, + PullRequestComparison, + Segment, +) + +# Pulled from shared.django_apps.core.tests.factories.CommitFactory files. +# Contents don't actually matter, it's just for providing a format +# compatible with what SerializableReport expects. Used in +# ComparisonTests. +file_data = [ + 2, + [0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0], + [[0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0]], + [0, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], +] + + +class MockOrderValue(object): + def __init__(self, value): + self.value = value + + def __getitem__(self, key): + return getattr(self, key) + + +class OrderingDirection(enum.Enum): + ASC = "ascending" + DESC = "descending" + + +class LineNumberCollector: + """ + A visitor for testing line traversal. + """ + + def __init__(self): + self.line_numbers = [] + + def __call__(self, base_ln, head_ln, value, is_diff): + self.line_numbers.append((base_ln, head_ln)) + + +class FileComparisonTraverseManagerTests(TestCase): + def test_no_diff_results_in_no_line_number_adjustments(self): + manager = FileComparisonTraverseManager(head_file_eof=3, base_file_eof=3) + + expected_result = [(1, 1), (2, 2)] + + visitor = LineNumberCollector() + manager.apply(visitors=[visitor]) + + assert visitor.line_numbers == expected_result + + def test_diff_with_added_lines_adjusts_lines(self): + # A line added at line 1 -- note header is string values, that's how + # torngit returns it + segments = [{"header": ["1", "1", "1", "2"], "lines": ["+"]}] + + manager = FileComparisonTraverseManager( + head_file_eof=4, base_file_eof=3, segments=segments + ) + + expected_result = [(None, 1), (1, 2), (2, 3)] + + visitor = LineNumberCollector() + manager.apply(visitors=[visitor]) + + assert visitor.line_numbers == expected_result + + def test_diff_with_removed_lines_adjusts_lines(self): + # A line removed at line 1 + segments = [{"header": ["1", "1", "1", "2"], "lines": ["-"]}] + manager = FileComparisonTraverseManager( + head_file_eof=3, base_file_eof=4, segments=segments + ) + + expected_result = [(1, None), (2, 1), (3, 2)] + + visitor = LineNumberCollector() + manager.apply(visitors=[visitor]) + + assert visitor.line_numbers == expected_result + + def test_diff_with_1_line_added_file_adjusts_lines(self): + segments = [{"header": ["0", "0", "1", ""], "lines": ["+"]}] + manager = FileComparisonTraverseManager( + head_file_eof=2, base_file_eof=0, segments=segments + ) + + expected_result = [(None, 1)] + + visitor = LineNumberCollector() + manager.apply(visitors=[visitor]) + + assert visitor.line_numbers == expected_result + + def test_diff_with_1_line_removed_file_adjusts_lines(self): + segments = [{"header": ["1", "1", "0", ""], "lines": ["-"]}] + manager = FileComparisonTraverseManager( + head_file_eof=0, base_file_eof=2, segments=segments + ) + + expected_result = [(1, None)] + + visitor = LineNumberCollector() + manager.apply(visitors=[visitor]) + + assert visitor.line_numbers == expected_result + + def test_pop_line_returns_none_if_no_diff_or_src(self): + manager = FileComparisonTraverseManager() + assert manager.pop_line() is None + + def test_pop_line_pops_first_line_in_segment_if_traversing_that_segment(self): + expected_line_value = "+this is a line!" + segments = [ + { + "header": [1, 2, 1, 3], + "lines": [expected_line_value, "this is another line"], + } + ] + manager = FileComparisonTraverseManager(segments=segments) + assert manager.pop_line() == expected_line_value + + def test_pop_line_returns_line_at_head_ln_index_in_src_if_not_in_segment(self): + expected_line_value = "a line from src!" + manager = FileComparisonTraverseManager( + head_file_eof=2, src=[expected_line_value] + ) + assert manager.pop_line() == expected_line_value + + def test_traversing_diff_returns_true_if_head_ln_within_segment_at_position_0(self): + manager = FileComparisonTraverseManager( + segments=[{"header": ["1", "6", "12", "4"]}] + ) + manager.head_ln = 14 + manager.base_ln = 1000 + assert manager.traversing_diff() is True + + manager.head_ln = 11 + assert manager.traversing_diff() is False + + def test_traversing_diff_handles_added_one_line_file_segment_header(self): + segment = {"header": ["0", "0", "1", ""], "lines": ["+"]} + manager = FileComparisonTraverseManager(segments=[segment]) + + assert manager.traversing_diff() is True + + def test_traversing_diff_handles_removed_one_line_file_segment_header(self): + segment = {"header": ["1", "1", "0", ""], "lines": ["-"]} + manager = FileComparisonTraverseManager(segments=[segment]) + + assert manager.traversing_diff() is True + + def test_traversing_diff_returns_true_if_base_ln_within_segment_at_position_0(self): + manager = FileComparisonTraverseManager( + segments=[{"header": ["4", "43", "4", "3"]}] + ) + manager.head_ln = 7 + manager.base_ln = 44 + assert manager.traversing_diff() is True + + def test_traverse_finished_returns_false_even_both_line_counters_at_eof_and_traversing_diff( + self, + ): + # This accounts for an edge case wherein you remove a multi-line expression + # (which codecov counts as a single line, coverage-wise) at the + # end of a file. The git-diff counts these as multiple lines, so + # in order to not truncate the diff we need to continue popping + # lines off the segment even of line counters are technically both + # at EOF. + manager = FileComparisonTraverseManager( + head_file_eof=7, + base_file_eof=43, + segments=[{"header": ["4", "43", "4", "3"]}], + ) + + manager.base_ln = 45 # higher than eof, but still traversing_diff + manager.head_ln = 7 # highest it can go in this segment + + assert manager.traverse_finished() is False + + def test_no_indexerror_if_basefile_longer_than_headfile_and_src_provided(self): + manager = FileComparisonTraverseManager( + head_file_eof=3, + base_file_eof=4, + src=["hey"] * 2, # head file eof minus 1, which is the typical case + ) + + # No indexerror should occur + manager.apply([lambda a, b, c, d: None]) + + def test_can_traverse_diff_with_line_numbers_greater_than_file_eof(self): + # This can happen when we have a file ending in a large multi-line + # expression, and a diff is made somewhere within that expression, + # but the start of the diff occurs after the start of the expression. + # The previous implementation of "traverse_finished" would end the traverse + # on account of not "traversing_diff", and having the line indices be + # greater than the respective files' EOF. The fix for this bug is stronger + # than that of the above comment and should handle both cases. + segments = [ + { + "header": ["3", "4", "3", "4"], + "lines": ["-Pro Team (billed monthly)", "+Pro Team"], + } + ] + manager = FileComparisonTraverseManager( + head_file_eof=2, base_file_eof=2, segments=segments + ) + + visitor = LineNumberCollector() + manager.apply([visitor]) + assert visitor.line_numbers == [(1, 1), (2, 2), (3, None), (None, 3)] + + def test_can_traverse_diff_with_diff_like_lines(self): + src = [ + "- line 1", # not part of diff + "- line 2", # not part of diff + "line 3", + ] + + # this is the diff + segments = [ + { + "header": ["3", "4", "3", "4"], + "lines": ["-line 3", "+line 3"], + } + ] + manager = FileComparisonTraverseManager( + head_file_eof=3, base_file_eof=3, segments=segments, src=src + ) + + visitor = LineNumberCollector() + manager.apply([visitor]) + assert visitor.line_numbers == [(1, 1), (2, 2), (3, None), (None, 3)] + + +class CreateLineComparisonVisitorTests(TestCase): + def setUp(self): + self.head_file = ReportFile( + "file1", lines=[[0, "", [], 0, 0], None, [0, "", [], 0, 0]] + ) + self.base_file = ReportFile( + "file1", lines=[None, [0, "", [], 0, 0], None, [0, "", [], 0, 0]] + ) + + def test_skips_if_line_value_is_none(self): + visitor = CreateLineComparisonVisitor(self.base_file, self.head_file) + visitor(0, 0, None, False) + assert visitor.lines == [] + + def test_appends_line_comparison_with_relevant_fields_if_line_value_not_none(self): + base_ln = 2 + head_ln = 1 + base_line = self.base_file._lines[base_ln - 1] + head_line = self.head_file._lines[head_ln - 1] + value = "sup dood" + is_diff = True + + visitor = CreateLineComparisonVisitor(self.base_file, self.head_file) + visitor(base_ln, head_ln, value, is_diff) + + line = visitor.lines[0] + assert line.head_ln == head_ln + assert line.base_ln == base_ln + assert line.head_line == head_line + assert line.base_line == base_line + assert line.value == value + assert line.is_diff == is_diff + + def test_appends_line_comparison_with_no_base_line_if_no_base_file_or_line_not_in_base_file( + self, + ): + visitor = CreateLineComparisonVisitor(self.base_file, self.head_file) + visitor(100, 1, "", False) # 100 is not a line in the base file + assert visitor.lines[0].base_line is None + + visitor.base_file = None + visitor( + 2, 1, "", False + ) # all valid line numbers, but still expect none without base_file + assert visitor.lines[1].base_line is None + + def test_appends_line_comparison_with_no_head_line_if_no_head_file_or_line_not_in_head_file( + self, + ): + visitor = CreateLineComparisonVisitor(self.base_file, self.head_file) + visitor(2, 100, "", False) + assert visitor.lines[0].head_line is None + + visitor.head_file = None + visitor(1, 2, "", False) + assert visitor.lines[1].head_line is None + + +class CreateChangeSummaryVisitorTests(TestCase): + def setUp(self): + self.head_file = ReportFile("file1", lines=[[1, "", [], 0, 0]]) + self.base_file = ReportFile("file1", lines=[[0, "", [], 0, 0]]) + + def test_changed_lines_in_diff_do_not_affect_change_summary(self): + visitor = CreateChangeSummaryVisitor(self.base_file, self.head_file) + visitor(1, 1, "+", False) + assert visitor.summary == {} + + visitor(1, 1, "-", False) + assert visitor.summary == {} + + def test_summary_with_one_less_miss_and_one_more_hit(self): + visitor = CreateChangeSummaryVisitor(self.base_file, self.head_file) + visitor(1, 1, "", True) + assert visitor.summary == {"misses": -1, "hits": 1} + + def test_summary_with_one_less_hit_and_one_more_partial(self): + self.base_file._lines[0][0] = 1 + self.head_file._lines[0][0] = "1/2" + visitor = CreateChangeSummaryVisitor(self.base_file, self.head_file) + visitor(1, 1, "", True) + assert visitor.summary == {"hits": -1, "partials": 1} + + +class LineComparisonTests(TestCase): + def test_number_shows_number_from_base_and_head(self): + base_ln = 3 + head_ln = 4 + lc = LineComparison( + [0, "", [], 0, 0], [0, "", [], 0, 0], base_ln, head_ln, "", False + ) + assert lc.number == {"base": base_ln, "head": head_ln} + + def test_number_shows_none_for_base_if_added(self): + head_ln = 4 + lc = LineComparison(None, [0, "", [], 0, 0], 0, head_ln, "+", True) + assert lc.number == {"base": None, "head": head_ln} + + def test_number_shows_none_for_base_if_plus_not_part_of_diff(self): + base_ln = 3 + head_ln = 4 + lc = LineComparison(None, [0, "", [], 0, 0], base_ln, head_ln, "+", False) + assert lc.number == {"base": base_ln, "head": head_ln} + + def test_number_shows_none_for_base_if_minus_not_part_of_diff(self): + base_ln = 3 + head_ln = 4 + lc = LineComparison(None, [0, "", [], 0, 0], base_ln, head_ln, "-", False) + assert lc.number == {"base": base_ln, "head": head_ln} + + def test_number_shows_none_for_head_if_removed(self): + base_ln = 3 + lc = LineComparison([0, "", [], 0, 0], None, base_ln, 0, "-", True) + assert lc.number == {"base": base_ln, "head": None} + + def test_coverage_shows_coverage_for_base_and_head(self): + base_cov, head_cov = 0, 1 + lc = LineComparison( + [base_cov, "", [], 0, 0], [head_cov, "", [], 0, 0], 0, 0, "", False + ) + assert lc.coverage == {"base": LineType.miss, "head": LineType.hit} + + def test_coverage_shows_none_for_base_if_added(self): + head_cov = 1 + lc = LineComparison(None, [head_cov, "", [], 0, 0], 0, 0, "+", False) + assert lc.coverage == {"base": None, "head": LineType.hit} + + def test_coverage_shows_none_for_head_if_removed(self): + base_cov = 0 + lc = LineComparison([base_cov, "", [], 0, 0], None, 0, 0, "-", False) + assert lc.coverage == {"base": LineType.miss, "head": None} + + def test_hit_count_returns_sessions_hit_in_head(self): + lc = LineComparison( + None, + [ + 1, + "", + [ + [0, 1, 0, 0, 0], + [1, 2, 0, 0, 0], + [2, 0, 0, 0, 0], + [3, "2/2", 0, 0, 0], + ], + 0, + 0, + ], + 0, + 0, + "", + False, + ) + + assert lc.hit_count == 3 + + def test_hit_count_returns_none_if_no_coverage(self): + lc = LineComparison(None, [0, "", [[0, 0, 0, 0, 0]], 0, 0], 0, 0, "", False) + assert lc.hit_count is None + + def test_hit_session_ids(self): + lc = LineComparison( + None, + [ + 1, + "", + [ + [0, 1, 0, 0, 0], + [1, 2, 0, 0, 0], + [2, 0, 0, 0, 0], + [3, "2/2", 0, 0, 0], + ], + 0, + 0, + ], + 0, + 0, + "", + False, + ) + + assert lc.hit_session_ids == [0, 1, 3] + + def test_hit_session_ids_no_coverage(self): + lc = LineComparison(None, [0, "", [[0, 0, 0, 0, 0]], 0, 0], 0, 0, "", False) + assert lc.hit_session_ids is None + + def test_hit_session_ids_no_head_line(self): + lc = LineComparison(None, None, 0, 0, "", False) + assert lc.hit_session_ids is None + + +class FileComparisonConstructorTests(TestCase): + def test_constructor_no_keyError_if_diff_data_segements_is_missing(self): + FileComparison( + head_file=ReportFile("file1"), base_file=ReportFile("file1"), diff_data={} + ) + + +class FileComparisonTests(TestCase): + def setUp(self): + self.file_comparison = FileComparison( + head_file=ReportFile("file1"), base_file=ReportFile("file1") + ) + + def test_name_shows_name_for_base_and_head(self): + assert self.file_comparison.name == { + "base": self.file_comparison.base_file.name, + "head": self.file_comparison.head_file.name, + } + + def test_name_none_if_base_or_head_if_files_none(self): + self.file_comparison.head_file = None + assert self.file_comparison.name == { + "base": self.file_comparison.base_file.name, + "head": None, + } + + self.file_comparison.base_file = None + assert self.file_comparison.name == {"base": None, "head": None} + + def test_totals_shows_totals_for_base_and_head(self): + assert self.file_comparison.totals == { + "base": self.file_comparison.base_file.totals, + "head": self.file_comparison.head_file.totals, + "diff": None, + } + + def test_totals_shows_totals_for_base_head_and_diff(self): + diff_totals = ReportTotals.default_totals() + self.file_comparison.diff_data = { + "totals": diff_totals, + } + + assert self.file_comparison.totals == { + "base": self.file_comparison.base_file.totals, + "head": self.file_comparison.head_file.totals, + "diff": diff_totals, + } + + def test_totals_base_is_none_if_missing_basefile(self): + self.file_comparison.base_file = None + assert self.file_comparison.totals == { + "base": None, + "head": self.file_comparison.head_file.totals, + "diff": None, + } + + def test_totals_head_is_none_if_missing_headfile(self): + self.file_comparison.head_file = None + assert self.file_comparison.totals == { + "base": self.file_comparison.base_file.totals, + "head": None, + "diff": None, + } + + def test_totals_includes_diff_totals_if_diff(self): + totals = "these are the totals" + self.file_comparison.diff_data = {"totals": totals} + assert self.file_comparison.totals["head"].diff == totals + + def test_has_diff_returns_true_iff_diff_data_not_none(self): + assert self.file_comparison.has_diff is False + + self.file_comparison.diff_data = {} + assert self.file_comparison.has_diff is True + + def test_stats_returns_none_if_no_diff_data(self): + assert self.file_comparison.has_diff is False + assert self.file_comparison.stats is None + + def test_stats_returns_diff_stats_if_diff_data(self): + expected_stats = "yep" + self.file_comparison.diff_data = {"stats": expected_stats} + assert self.file_comparison.stats == expected_stats + + def test_lines_returns_empty_list_if_no_diff_or_src(self): + assert self.file_comparison.lines == [] + + # essentially a smoke/integration test + def test_lines(self): + head_lines = [ + [1, "", [], 0, None], + ["1/2", "", [], 0, None], + [1, "", [], 0, None], + ] + base_lines = [[0, "", [], 0, None], [1, "", [], 0, None], [0, "", [], 0, None]] + + first_line_val = "unchanged line from src" + second_line_val = "+this is an added line" + third_line_val = "-this is a removed line" + last_line_val = "this is the third line" + + segment = { + "header": ["2", "2", "2", "2"], + "lines": [second_line_val, third_line_val], + } + src = [first_line_val, "this is an added line", last_line_val] + + self.file_comparison.head_file._parsed_lines = head_lines + self.file_comparison.base_file._parsed_lines = base_lines + self.file_comparison.diff_data = {"segments": [segment]} + self.file_comparison.src = src + + assert self.file_comparison.lines[0].value == first_line_val + assert self.file_comparison.lines[0].number == {"base": 1, "head": 1} + assert self.file_comparison.lines[0].coverage == { + "base": LineType.miss, + "head": LineType.hit, + } + + assert self.file_comparison.lines[1].value == second_line_val + assert self.file_comparison.lines[1].number == {"base": None, "head": 2} + assert self.file_comparison.lines[1].coverage == { + "base": None, + "head": LineType.partial, + } + + assert self.file_comparison.lines[2].value == third_line_val + assert self.file_comparison.lines[2].number == {"base": 2, "head": None} + assert self.file_comparison.lines[2].coverage == { + "base": LineType.hit, + "head": None, + } + + assert self.file_comparison.lines[3].value == last_line_val + assert self.file_comparison.lines[3].number == {"base": 3, "head": 3} + assert self.file_comparison.lines[3].coverage == { + "base": LineType.miss, + "head": LineType.hit, + } + + @patch("services.comparison.FileComparison.lines", new_callable=PropertyMock) + def test_segments_diff_only(self, lines): + lines.return_value = [ + LineComparison([1], [1], 1, 1, "first line", False), + LineComparison(None, [1], None, 2, "+this is an added line", True), + LineComparison([1], None, 2, None, "-this is a removed line", True), + LineComparison([1], [1], 3, 3, "last line", False), + ] + + segments = self.file_comparison.segments + + assert len(segments) == 1 + assert segments[0].lines == self.file_comparison.lines + assert segments[0].header == (1, 3, 1, 3) + assert segments[0].has_diff_changes == True + assert segments[0].has_unintended_changes == False + + @patch("services.comparison.FileComparison.lines", new_callable=PropertyMock) + def test_segments_changes_only(self, lines): + lines.return_value = [ + LineComparison([1], [1], 1, 1, "first line", False), + LineComparison([0], [1], 2, 2, "middle line", False), # coverage added + LineComparison([1], [1], 3, 3, "last line", False), + ] + + segments = self.file_comparison.segments + + assert len(segments) == 1 + assert segments[0].lines == self.file_comparison.lines + assert segments[0].header == (1, 3, 1, 3) + assert segments[0].has_diff_changes == False + assert segments[0].has_unintended_changes + + @patch("services.comparison.FileComparison.lines", new_callable=PropertyMock) + def test_segments_no_changes_no_diff(self, lines): + lines.return_value = [ + LineComparison([1], [1], 1, 1, "first line", False), + LineComparison([1], [1], 2, 2, "middle line", False), + LineComparison([1], [1], 3, 3, "last line", False), + ] + + segments = self.file_comparison.segments + assert len(segments) == 0 + + def test_change_summary(self): + head_lines = [ + [1, "", [], 0, None], + ["3/4", "", [], 0, None], + [1, "", [], 0, None], + ] + base_lines = [[0, "", [], 0, None], [1, "", [], 0, None], [0, "", [], 0, None]] + + first_line_val = "unchanged line from src" + second_line_val = "+this is an added line" + third_line_val = "-this is a removed line" + last_line_val = "this is the third line" + + segment = { + "header": ["2", "2", "2", "2"], + "lines": [second_line_val, third_line_val], + } + src = [first_line_val, "this is an added line", last_line_val] + + self.file_comparison.head_file._parsed_lines = head_lines + self.file_comparison.base_file._parsed_lines = base_lines + self.file_comparison.diff_data = {"segments": [segment]} + self.file_comparison.src = src + + assert self.file_comparison.change_summary == {"hits": 2, "misses": -2} + + @patch( + "services.comparison.FileComparison.change_summary", new_callable=PropertyMock + ) + def test_has_changes(self, change_summary_mock): + change_summary_mock.return_value = Counter() + assert self.file_comparison.has_changes == False + + change_summary_mock.return_value = Counter({"hits": 0, "misses": 0}) + assert self.file_comparison.has_changes == False + + change_summary_mock.return_value = Counter({"hits": 1, "misses": -1}) + assert self.file_comparison.has_changes == True + + @patch("services.comparison.FileComparisonTraverseManager.apply") + def test_does_not_calculate_changes_if_no_diff_and_should_search_for_changes_is_False( + self, mocked_apply_traverse + ): + self.file_comparison.should_search_for_changes = False + self.file_comparison._calculated_changes_and_lines + mocked_apply_traverse.assert_not_called() + + @patch("services.comparison.FileComparisonTraverseManager.apply") + def test_calculates_changes_if_no_diff_and_should_search_for_changes_is_None( + self, mocked_apply_traverse + ): + self.file_comparison.should_search_for_changes = None + self.file_comparison._calculated_changes_and_lines + mocked_apply_traverse.assert_called_once() + + @patch("services.comparison.FileComparisonTraverseManager.apply") + def test_calculates_changes_should_search_for_changes_is_True( + self, mocked_apply_traverse + ): + self.file_comparison.should_search_for_changes = True + self.file_comparison._calculated_changes_and_lines + mocked_apply_traverse.assert_called_once() + + @patch("services.comparison.FileComparisonTraverseManager.apply") + def test_calculates_changes_if_traversing_src(self, mocked_apply_traverse): + self.file_comparison.should_search_for_changes = False + self.file_comparison.src = ["a truthy list"] + self.file_comparison._calculated_changes_and_lines + mocked_apply_traverse.assert_called_once() + + +@patch("services.comparison.Comparison.git_comparison", new_callable=PropertyMock) +@patch("services.comparison.Comparison.head_report", new_callable=PropertyMock) +@patch("services.comparison.Comparison.base_report", new_callable=PropertyMock) +class ComparisonTests(TestCase): + def setUp(self): + owner = OwnerFactory() + base, head = CommitFactory(author=owner), CommitFactory(author=owner) + self.comparison = Comparison(user=owner, base_commit=base, head_commit=head) + + def test_files_gets_file_comparison_for_each_file_in_head_report( + self, base_report_mock, head_report_mock, git_comparison_mock + ): + head_report_files = {"file1": file_data, "file2": file_data} + head_report_mock.return_value = SerializableReport(files=head_report_files) + base_report_mock.return_value = SerializableReport(files={}) + git_comparison_mock.return_value = {"diff": {"files": {}}} + + assert sum(1 for x in self.comparison.files) == 2 + for fc in self.comparison.files: + assert isinstance(fc, FileComparison) + assert fc.head_file.name in head_report_files + assert fc.base_file is None + + def test_get_file_comparison_adds_in_file_from_base_report_if_exists( + self, base_report_mock, head_report_mock, git_comparison_mock + ): + git_comparison_mock.return_value = {"diff": {"files": {}}} + + files = {"both.py": file_data} + base_report_mock.return_value = SerializableReport(files=files) + head_report_mock.return_value = SerializableReport(files=files) + + fc = self.comparison.get_file_comparison("both.py") + assert fc.head_file.name == "both.py" + assert fc.base_file.name == "both.py" + + def test_get_file_comparison_accounts_for_renamed_files( + self, base_report_mock, head_report_mock, git_comparison_mock + ): + file_name = "myfile.py" + previous_name = "previous.py" + + base_report_files = {previous_name: file_data} + base_report_mock.return_value = SerializableReport(files=base_report_files) + + head_report_files = {file_name: file_data} + head_report_mock.return_value = SerializableReport(files=head_report_files) + + git_comparison_mock.return_value = { + "diff": {"files": {file_name: {"before": previous_name, "segments": []}}}, + "commits": [], + } + + fc = self.comparison.get_file_comparison(file_name) + assert fc.head_file.name == file_name + assert fc.base_file.name == previous_name + + def test_get_file_comparison_includes_diff_data_if_exists( + self, base_report_mock, head_report_mock, git_comparison_mock + ): + file_name = "f" + diff_data = { + "segments": [{"header": ["4", "6", "7", "3"], "lines": []}], + "stats": {"added": 3, "removed": 2}, + } + + base_report_mock.return_value = SerializableReport(files={}) + + head_report_files = {file_name: file_data} + head_report_mock.return_value = SerializableReport(files=head_report_files) + + git_comparison_mock.return_value = {"diff": {"files": {file_name: diff_data}}} + + fc = self.comparison.get_file_comparison(file_name) + assert fc.diff_data == diff_data + + @patch("services.repo_providers.RepoProviderService.get_adapter") + def test_get_file_comparison_includes_src_if_with_src_is_true( + self, + mocked_comparison_adapter, + base_report_mock, + head_report_mock, + git_comparison_mock, + ): + from api.internal.tests.views.test_compare_viewset import ( + MockedComparisonAdapter, + ) + + src = b"two\nlines" + git_comparison_mock.return_value = {"diff": {"files": {}}} + mocked_comparison_adapter.return_value = MockedComparisonAdapter( + {"diff": {"files": {}}}, test_lines=src + ) + + file_name = "f" + + base_report_mock.return_value = SerializableReport(files={}) + head_report_files = {file_name: file_data} + head_report_mock.return_value = SerializableReport(files=head_report_files) + + fc = self.comparison.get_file_comparison(file_name, with_src=True) + assert fc.src == ["two", "lines"] + + @patch("services.repo_providers.RepoProviderService.get_adapter") + def test_get_file_comparison_can_parse_string_src( + self, + mocked_comparison_adapter, + base_report_mock, + head_report_mock, + git_comparison_mock, + ): + from api.internal.tests.views.test_compare_viewset import ( + MockedComparisonAdapter, + ) + + src = "two\nlines" + git_comparison_mock.return_value = {"diff": {"files": {}}} + mocked_comparison_adapter.return_value = MockedComparisonAdapter( + {"diff": {"files": {}}}, test_lines=src + ) + + file_name = "f" + + base_report_mock.return_value = SerializableReport(files={}) + head_report_files = {file_name: file_data} + head_report_mock.return_value = SerializableReport(files=head_report_files) + + fc = self.comparison.get_file_comparison(file_name, with_src=True) + assert fc.src == ["two", "lines"] + + def test_get_file_comparison_with_no_base_report_doesnt_crash( + self, base_report_mock, head_report_mock, git_comparison_mock + ): + git_comparison_mock.return_value = {"diff": {"files": {}}} + + files = {"both.py": file_data} + base_report_mock.return_value = None + head_report_mock.return_value = SerializableReport(files=files) + + fc = self.comparison.get_file_comparison("both.py") + assert fc.head_file.name == "both.py" + + @pytest.mark.xfail # TODO(pierce): investigate this feature + def test_files_adds_deleted_files_that_were_tracked_in_base_report( + self, base_report_mock, head_report_mock, git_comparison_mock + ): + deleted_file_name = "deleted.py" + base_report_files = {deleted_file_name: file_data} + base_report_mock.return_value = SerializableReport(files=base_report_files) + + head_report_files = {} + head_report_mock.return_value = SerializableReport(files=head_report_files) + + git_comparison_mock.return_value = { + "diff": {"files": {deleted_file_name: {"type": "deleted"}}}, + "commits": [], + } + + assert self.comparison.files[0].base_file.name == deleted_file_name + assert self.comparison.files[0].head_file is None + assert self.comparison.files[0].diff_data == {"type": "deleted"} + + def test_totals_returns_head_totals_if_exists( + self, base_report_mock, head_report_mock, git_comparison_mock + ): + base_report_mock.return_value = None + head_report_mock.return_value = SerializableReport() + + assert self.comparison.totals["head"] == head_report_mock.return_value.totals + assert self.comparison.totals["base"] is None + + def test_totals_returns_base_totals_if_exists( + self, base_report_mock, head_report_mock, git_comparison_mock + ): + head_report_mock.return_value = None + base_report_mock.return_value = SerializableReport() + + assert self.comparison.totals["base"] == base_report_mock.return_value.totals + assert self.comparison.totals["head"] is None + + def test_totals_returns_diff_totals_if_exists( + self, base_report_mock, head_report_mock, git_comparison_mock + ): + head_report = SerializableReport() + head_report_mock.return_value = head_report + base_report_mock.return_value = None + + diff_totals = ReportTotals.default_totals() + git_comparison_mock.return_value = {"diff": {"totals": diff_totals}} + + assert self.comparison.totals["base"] is None + assert self.comparison.totals["head"] == head_report.totals + assert self.comparison.totals["diff"] is diff_totals + + def test_head_and_base_reports_have_cff_sessions( + self, base_report_mock, head_report_mock, _ + ): + # Only relevant files keys to the session object + head_report_sessions = {"0": {"st": "carriedforward"}} + head_report = SerializableReport(sessions=head_report_sessions) + head_report_mock.return_value = head_report + base_report_sessions = {"0": {"st": "carriedforward"}} + base_report = SerializableReport(sessions=base_report_sessions) + base_report_mock.return_value = base_report + + fc = self.comparison.has_different_number_of_head_and_base_sessions + assert fc == False + + @patch( + "services.comparison.Comparison.head_report_without_applied_diff", + new_callable=PropertyMock, + ) + def test_head_and_base_reports_have_different_number_of_reports( + self, head_report_no_diff_mock, base_report_mock, head_report_mock, _ + ): + # Only relevant files keys to the session object + head_report_sessions = {"0": {"st": "uploaded"}, "1": {"st": "uploaded"}} + head_report = SerializableReport(sessions=head_report_sessions) + head_report_no_diff_mock.return_value = head_report + base_report_sessions = {"0": {"st": "uploaded"}} + base_report = SerializableReport(sessions=base_report_sessions) + base_report_mock.return_value = base_report + + fc = self.comparison.has_different_number_of_head_and_base_sessions + assert fc == True + + def test_head_and_base_reports_have_same_number_of_reports( + self, base_report_mock, head_report_mock, _ + ): + # Only relevant files keys to the session object + head_report_sessions = {"0": {"st": "uploaded"}} + head_report = SerializableReport(sessions=head_report_sessions) + head_report_mock.return_value = head_report + base_report_sessions = {"0": {"st": "uploaded"}} + base_report = SerializableReport(sessions=base_report_sessions) + base_report_mock.return_value = base_report + + fc = self.comparison.has_different_number_of_head_and_base_sessions + assert fc == False + + +class PullRequestComparisonTests(TestCase): + def setUp(self): + owner = OwnerFactory() + repo = RepositoryFactory(author=owner) + base, head, compared_to = ( + CommitFactory(repository=repo), + CommitFactory(repository=repo), + CommitFactory(repository=repo), + ) + + self.pull = PullFactory( + repository=repo, + base=base.commitid, + head=head.commitid, + compared_to=compared_to.commitid, + ) + self.comparison = PullRequestComparison(user=owner, pull=self.pull) + + def test_files_with_changes_hash_key(self): + assert self.comparison._files_with_changes_hash_key == "/".join( + ( + "compare-changed-files", + self.pull.repository.author.service, + self.pull.repository.author.username, + self.pull.repository.name, + str(self.pull.pullid), + ) + ) + + @patch("redis.Redis.get") + def test_files_with_changes_retrieves_from_redis(self, mocked_get): + filename = "something.py" + mocked_get.return_value = json.dumps([filename]) + assert self.comparison._files_with_changes == [filename] + + @patch("redis.Redis.get") + def test_files_with_changes_returns_none_if_no_files_with_changes(self, mocked_get): + mocked_get.return_value = None + assert self.comparison._files_with_changes is None + + @patch("redis.Redis.get") + def test_files_with_changes_doesnt_crash_if_redis_connection_problem( + self, mocked_get + ): + def raise_oserror(*args, **kwargs): + raise OSError + + mocked_get.side_effect = raise_oserror + self.comparison._files_with_changes + + @patch("redis.Redis.set") + def test_set_files_with_changes_in_cache_stores_in_redis(self, mocked_set): + files_with_changes = ["file1", "file2"] + self.comparison._set_files_with_changes_in_cache(files_with_changes) + mocked_set.assert_called_once_with( + self.comparison._files_with_changes_hash_key, + json.dumps(files_with_changes), + ex=86400, # 1 day in seconds + ) + + @patch("services.comparison.Comparison.git_comparison", new_callable=PropertyMock) + @patch("services.comparison.Comparison.head_report", new_callable=PropertyMock) + @patch("services.comparison.Comparison.base_report", new_callable=PropertyMock) + @patch("redis.Redis.set") + @patch("redis.Redis.get") + @patch( + "services.comparison.FileComparison.change_summary", new_callable=PropertyMock + ) + def test_files_populates_files_with_changes_in_redis( + self, + mocked_change_summary, + mocked_get, + mocked_set, + base_report_mock, + head_report_mock, + git_comparison_mock, + ): + mocked_get.return_value = None + mocked_change_summary.return_value = {"hits": 1, "misses": -1} + head_report_files = {"file1": file_data, "file2": file_data} + head_report_mock.return_value = SerializableReport(files=head_report_files) + base_report_mock.return_value = SerializableReport(files={}) + git_comparison_mock.return_value = {"diff": {"files": {}}} + + list(self.comparison.files) + + mocked_set.assert_called_once_with( + self.comparison._files_with_changes_hash_key, + json.dumps(["file1", "file2"]), + ex=86400, # 1 day in seconds + ) + + @patch("services.comparison.Comparison.git_comparison", new_callable=PropertyMock) + @patch("services.comparison.Comparison.head_report", new_callable=PropertyMock) + @patch("services.comparison.Comparison.base_report", new_callable=PropertyMock) + @patch( + "services.comparison.PullRequestComparison._files_with_changes", + new_callable=PropertyMock, + ) + def test_get_file_comparison_sets_should_search_for_changes_correctly( + self, + files_with_changes_mock, + base_report_mock, + head_report_mock, + git_comparison_mock, + ): + head_report_files = {"file1": file_data, "file2": file_data} + head_report_mock.return_value = SerializableReport(files=head_report_files) + base_report_mock.return_value = SerializableReport(files={}) + git_comparison_mock.return_value = {"diff": {"files": {}}} + + with self.subTest("it's None when nothing found in cache"): + files_with_changes_mock.return_value = None + fc = self.comparison.get_file_comparison("file1") + assert fc.should_search_for_changes is None + + with self.subTest("it's True when file found in cached list"): + files_with_changes_mock.return_value = ["file1"] + fc = self.comparison.get_file_comparison("file1") + assert fc.should_search_for_changes is True + + with self.subTest("it's False when file not found in list"): + files_with_changes_mock.return_value = ["file2"] + fc = self.comparison.get_file_comparison("file1") + assert fc.should_search_for_changes is False + + @patch("services.comparison.get_config") + def test_is_pseudo_comparison(self, get_config_mock): + owner = OwnerFactory() + repository = RepositoryFactory(author=owner) + pull = PullFactory( + pullid=44, + repository=repository, + compared_to=CommitFactory(repository=repository).commitid, + head=CommitFactory(repository=repository).commitid, + base=CommitFactory(repository=repository).commitid, + ) + + with self.subTest("returns the result in the repo yaml if exists"): + repository.yaml = {"codecov": {"allow_pseudo_compare": True}} + repository.save() + comparison = PullRequestComparison(owner, pull) + assert comparison.is_pseudo_comparison is True + + repository.yaml = {"codecov": {"allow_pseudo_compare": False}} + repository.save() + comparison = PullRequestComparison(owner, pull) + assert comparison.is_pseudo_comparison is False + + with self.subTest( + "returns the result in app settings if repo yaml doesn't exist" + ): + repository.yaml = None + repository.save() + get_config_mock.return_value = True + comparison = PullRequestComparison(owner, pull) + assert comparison.is_pseudo_comparison is True + + with self.subTest( + "returns the result in app settings if repo yaml doesn't exist" + ): + repository.yaml = None + repository.save() + get_config_mock.return_value = False + comparison = PullRequestComparison(owner, pull) + assert comparison.is_pseudo_comparison is False + + with self.subTest("depends on the truthiness of the 'compared_to' commit"): + repository.yaml = {"codecov": {"allow_pseudo_compare": True}} + repository.save() + pull.compared_to = None + pull.save() + comparison = PullRequestComparison(owner, pull) + assert comparison.is_pseudo_comparison is False + + @patch("services.repo_providers.RepoProviderService.get_adapter") + def test_pseudo_diff_returns_diff_between_base_and_compared_to( + self, get_adapter_mock + ): + expected_diff = "expected_diff" + + class PseudoCompareAdapter: + async def get_compare(self, base, head): + self.base, self.head = base, head + return {"diff": expected_diff} + + get_compare_adapter = PseudoCompareAdapter() + get_adapter_mock.return_value = get_compare_adapter + + assert self.comparison.pseudo_diff == expected_diff + assert ( + get_compare_adapter.base == self.pull.compared_to + and get_compare_adapter.head == self.pull.base + ) + + @patch("services.comparison.Comparison.head_report", new_callable=PropertyMock) + @patch("services.comparison.Comparison.base_report", new_callable=PropertyMock) + @patch("services.comparison.Comparison.git_comparison", new_callable=PropertyMock) + @patch( + "services.comparison.PullRequestComparison.pseudo_diff", + new_callable=PropertyMock, + ) + @patch("shared.reports.resources.Report.does_diff_adjust_tracked_lines") + @patch( + "services.comparison.PullRequestComparison.is_pseudo_comparison", + new_callable=PropertyMock, + ) + def test_pseudo_diff_adjusts_tracked_lines( + self, + is_pseudo_comparison_mock, + does_diff_adjust_mock, + pseudo_diff_mock, + git_comparison_mock, + base_report_mock, + head_report_mock, + ): + owner = OwnerFactory() + repository = RepositoryFactory(author=owner) + pull = PullFactory( + pullid=44, + repository=repository, + compared_to=CommitFactory(repository=repository).commitid, + head=CommitFactory(repository=repository).commitid, + base=CommitFactory(repository=repository).commitid, + ) + + with self.subTest( + "returns True if reports exist and there is a diff that adjusts tracked lines" + ): + is_pseudo_comparison_mock.return_value = True + head_report_files = {"file1": file_data, "file2": file_data} + head_report_mock.return_value = SerializableReport(files=head_report_files) + base_report_mock.return_value = SerializableReport(files={}) + git_comparison_mock.return_value = {"diff": {"files": {}}} + pseudo_diff_mock.return_value = {"files": {"file1": {}}} + does_diff_adjust_mock.return_value = True + comparison = PullRequestComparison(owner, pull) + assert comparison.pseudo_diff_adjusts_tracked_lines is True + + with self.subTest("returns False if reports don't exist"): + head_report_mock.return_value = None + comparison = PullRequestComparison(owner, pull) + assert self.comparison.pseudo_diff_adjusts_tracked_lines is False + + head_report_mock.return_value = SerializableReport(files=head_report_files) + base_report_mock.return_value = None + comparison = PullRequestComparison(owner, pull) + assert self.comparison.pseudo_diff_adjusts_tracked_lines is False + + with self.subTest("returns False if compared to is same as base"): + self.comparison.pull.compared_to = self.comparison.pull.base + self.comparison.pull.save() + comparison = PullRequestComparison(owner, pull) + assert self.comparison.pseudo_diff_adjusts_tracked_lines is False + + with self.subTest("returns False for non-pseudo comparisons"): + is_pseudo_comparison_mock.return_value = True + comparison = PullRequestComparison(owner, pull) + assert self.comparison.pseudo_diff_adjusts_tracked_lines is False + + @patch( + "services.comparison.PullRequestComparison.pseudo_diff", + new_callable=PropertyMock, + ) + @patch("services.comparison.Comparison.base_report", new_callable=PropertyMock) + @patch("shared.reports.resources.Report.shift_lines_by_diff") + def test_update_base_report_with_pseudo_diff( + self, shift_lines_by_diff_mock, base_report_mock, pseudo_diff_mock + ): + pseudo_diff_mock.return_value = {"files": {}} + base_report_files = {"file1": file_data, "file2": file_data} + base_report_mock.return_value = SerializableReport(files=base_report_files) + self.comparison.update_base_report_with_pseudo_diff() + shift_lines_by_diff_mock.assert_called_once_with({"files": {}}, forward=True) + + +@patch("services.comparison.Comparison.git_comparison", new_callable=PropertyMock) +@patch("shared.reports.api_report_service.build_report_from_commit") +class ComparisonHeadReportTests(TestCase): + def setUp(self): + owner = OwnerFactory() + base, head = CommitFactory(author=owner), CommitFactory(author=owner) + self.comparison = Comparison(base, head, owner) + + @patch("shared.reports.api_report_service.SerializableReport.apply_diff") + def test_head_report_calls_apply_diff( + self, apply_diff_mock, build_report_from_commit_mock, git_comparison_mock + ): + build_report_from_commit_mock.return_value = SerializableReport( + files={"f": file_data} + ) + git_comparison_mock.return_value = {"diff": {"files": {}}} + + # should be called when invoking this property + self.comparison.head_report + + apply_diff_mock.assert_called_once_with( + git_comparison_mock.return_value["diff"] + ) + + def test_head_report_and_base_report_translates_nosuchkey_into_missingcomparisonreport( + self, build_report_from_commit_mock, git_comparison_mock + ): + build_report_from_commit_mock.side_effect = minio.error.S3Error( + code="NoSuchKey", + message=None, + resource=None, + request_id=None, + host_id=None, + response=None, + ) + with self.assertRaises(MissingComparisonReport): + self.comparison.head_report + + with self.assertRaises(MissingComparisonReport): + self.comparison.base_report + + +@patch("services.repo_providers.RepoProviderService.get_adapter") +class ComparisonHasUnmergedBaseCommitsTests(TestCase): + class MockFetchDiffCoro: + def __init__(self, commits): + self.commits = commits + + async def get_compare(self, base, head): + return {"commits": self.commits} + + def setUp(self): + owner = OwnerFactory() + base, head = CommitFactory(author=owner), CommitFactory(author=owner) + self.comparison = Comparison(user=owner, base_commit=base, head_commit=head) + asyncio.set_event_loop(asyncio.new_event_loop()) + + +class SegmentTests(TestCase): + def _report_lines(self, hits): + return [ + # from shared.reports.types.ReportLine + # values are [coverage, type, sessions, messages, complexity] + [hit, "", [], 0, None] + for hit in hits + ] + + def _src(self, n): + return [f"line{i + 1}" for i in range(n)] + + def setUp(self): + self.file_comparison = FileComparison( + base_file=ReportFile("file1"), head_file=ReportFile("file1") + ) + + def test_single_segment(self): + self.file_comparison.src = self._src(12) + self.file_comparison.head_file._parsed_lines = self._report_lines( + [1 for _ in range(12)] + ) + self.file_comparison.base_file._parsed_lines = self._report_lines( + [ + 1, + 1, # first line of segment + 1, + 1, + 0, # coverage changed + 0, # coverage changed + 1, + 0, # coverage changed + 1, + 1, + 1, # last line of segment + 1, + ] + ) + + segments = self.file_comparison.segments + assert len(segments) == 1 + + segment_lines = segments[0].lines + assert segment_lines[0].value == "line2" + assert segment_lines[-1].value == "line11" + assert len(segment_lines) == 10 + + def test_multiple_segments(self): + self.file_comparison.src = self._src(25) + self.file_comparison.head_file._parsed_lines = self._report_lines( + [1 for _ in range(25)] + ) + self.file_comparison.base_file._parsed_lines = self._report_lines( + [ + 1, + 1, # first line of segment 1 + 1, + 1, + 0, # coverage changed + 0, # coverage changed + 1, + 0, # coverage changed + 1, + 1, + 1, # last line of segment 1 + 1, + 1, + 1, + 1, + 1, # first line of segment 2 + 1, + 1, + 0, # coverage changed + 1, + 1, + 1, # last line of segment 2 + 1, + 1, + 1, + ] + ) + + segments = self.file_comparison.segments + assert len(segments) == 2 + + assert segments[0].lines[0].value == "line2" + assert segments[0].lines[-1].value == "line11" + assert len(segments[0].lines) == 10 + assert segments[0].header == (2, 10, 2, 10) + assert segments[0].has_unintended_changes + + assert segments[1].lines[0].value == "line16" + assert segments[1].lines[-1].value == "line22" + assert len(segments[1].lines) == 7 + assert segments[1].header == (16, 7, 16, 7) + assert segments[1].has_unintended_changes + + @patch("services.comparison.FileComparison.lines", new_callable=PropertyMock) + def test_header_new_file(self, lines): + lines.return_value = [ + LineComparison(None, [1], None, 1, "+line1", True), + LineComparison(None, [1], None, 2, "+line2", True), + LineComparison(None, [1], None, 3, "+line3", True), + ] + + file_comparison = FileComparison(base_file=None, head_file=ReportFile("file1")) + + segments = file_comparison.segments + assert len(segments) == 1 + assert segments[0].header == (0, 0, 1, 3) + + @patch("services.comparison.FileComparison.lines", new_callable=PropertyMock) + def test_header_deleted_file(self, lines): + lines.return_value = [ + LineComparison([1], None, 1, None, "-line1", True), + LineComparison([1], None, 2, None, "-line2", True), + LineComparison([1], None, 3, None, "-line3", True), + ] + + file_comparison = FileComparison(base_file=ReportFile("file1"), head_file=None) + + segments = file_comparison.segments + assert len(segments) == 1 + assert segments[0].header == (1, 3, 0, 0) + + +mock_data_from_archive = """ +{ + "files": [{ + "head_name": "fileA", + "base_name": "fileA", + "head_coverage": { + "hits": 10, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [ + [9,"h"], + [2,"m"], + [3,"m"], + [13,"p"], + [14,"h"], + [15,"h"], + [16,"h"], + [17,"h"] + ], + "unexpected_line_changes": [[[1, "h"], [1, "h"]]] + }, + { + "head_name": "fileB", + "base_name": "fileB", + "head_coverage": { + "hits": 12, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [ + [9,"h"], + [10,"m"], + [13,"p"], + [14,"h"], + [15,"h"], + [16,"h"], + [17,"h"] + ], + "unexpected_line_changes": [] + }] +} +""" + +mocked_files_with_direct_and_indirect_changes = """ +{ + "files": [{ + "head_name": "fileA", + "base_name": "fileA", + "head_coverage": { + "hits": 10, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [ + [9,"h"], + [2,"m"], + [3,"m"], + [13,"p"], + [14,"h"], + [15,"h"], + [16,"h"], + [17,"h"] + ], + "unexpected_line_changes": [[[1, "h"], [1, "h"]]] + }, + { + "head_name": "fileB", + "base_name": "fileB", + "head_coverage": { + "hits": 12, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [ + [9,"h"], + [10,"m"], + [13,"p"], + [14,"h"], + [15,"h"], + [16,"h"], + [17,"h"] + ], + "unexpected_line_changes": [] + }, + { + "head_name": "fileC", + "base_name": "fileC", + "head_coverage": { + "hits": 12, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [], + "unexpected_line_changes": [[[1, "h"], [1, "h"]]] + }, + { + "head_name": "fileD", + "base_name": "fileD", + "head_coverage": { + "hits": 12, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5 + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4 + }, + "added_diff_coverage": [], + "unexpected_line_changes": [] + }] +} +""" + + +class ComparisonReportTest(TestCase): + def setUp(self): + self.user = OwnerFactory(username="codecov-user") + self.parent_commit = CommitFactory() + self.commit = CommitFactory( + parent_commit_id=self.parent_commit.commitid, + repository=self.parent_commit.repository, + ) + self.comparison = CommitComparisonFactory( + base_commit=self.parent_commit, + compare_commit=self.commit, + report_storage_path="v4/test.json", + ) + self.comparison_without_storage = CommitComparisonFactory() + self.comparison_report = ComparisonReport(self.comparison) + self.comparison_report_without_storage = ComparisonReport( + self.comparison_without_storage + ) + + def test_empty_impacted_files(self): + impacted_files = self.comparison_report_without_storage.impacted_files + assert impacted_files == [] + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_error_when_failing_to_get_file_from_storage( + self, mock_read_file + ): + mock_read_file.side_effect = Exception() + impacted_files = self.comparison_report.impacted_files + assert impacted_files == [] + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_file(self, read_file): + read_file.return_value = mock_data_from_archive + impacted_file = self.comparison_report.impacted_file("fileB") + assert impacted_file.head_name == "fileB" + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_indirect_changes(self, read_file): + read_file.return_value = mock_data_from_archive + impacted_files = self.comparison_report.impacted_files_with_unintended_changes + assert [file.head_name for file in impacted_files] == ["fileA"] + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_impacted_files_filtered_by_direct_changes(self, read_file): + read_file.return_value = mocked_files_with_direct_and_indirect_changes + impacted_files = self.comparison_report.impacted_files_with_direct_changes + assert [file.head_name for file in impacted_files] == [ + "fileA", + "fileB", + "fileD", + ] + + def test_file_has_diff(self): + file = ImpactedFile( + **{ + "head_name": "fileB", + "base_name": "fileB", + "head_coverage": { + "hits": 12, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5, + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4, + }, + "added_diff_coverage": [ + [9, "h"], + [10, "m"], + [13, "p"], + [14, "h"], + [15, "h"], + [16, "h"], + [17, "h"], + ], + "unexpected_line_changes": [], + } + ) + assert file.has_diff is True + + def test_file_has_diff_with_indirect_changes(self): + file = ImpactedFile( + **{ + "head_name": "fileB", + "base_name": "fileB", + "head_coverage": { + "hits": 12, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5, + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4, + }, + "added_diff_coverage": [ + [9, "h"], + [10, "m"], + [13, "p"], + [14, "h"], + [15, "h"], + [16, "h"], + [17, "h"], + ], + "unexpected_line_changes": [[[1, "h"], [1, "h"]]], + } + ) + assert file.has_diff is True + + def test_file_has_changes(self): + file = ImpactedFile( + **{ + "head_name": "fileB", + "base_name": "fileB", + "head_coverage": { + "hits": 12, + "misses": 1, + "partials": 1, + "branches": 3, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 5, + }, + "base_coverage": { + "hits": 5, + "misses": 6, + "partials": 1, + "branches": 2, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 4, + }, + "added_diff_coverage": [ + [9, "h"], + [10, "m"], + [13, "p"], + [14, "h"], + [15, "h"], + [16, "h"], + [17, "h"], + ], + "unexpected_line_changes": [[[1, "h"], [1, "h"]]], + } + ) + assert file.has_changes is True + + def test_remove_unintended_changes(self): + lines = [ + LineComparison([1, "", [], 0, 0], [1, "", [], 0, 0], 1, 1, "line1", False), + LineComparison([1, "", [], 0, 0], [0, "", [], 0, 0], 2, 2, "line2", False), + LineComparison(None, [0, "", [], 0, 0], None, 3, "+line3", True), + LineComparison([0, "", [], 0, 0], None, 4, None, "-line4", True), + LineComparison([1, "", [], 0, 0], [0, "", [], 0, 0], 5, 5, "line5", False), + ] + + segment = Segment(lines) + segment.remove_unintended_changes() + + assert len(segment.lines) == 3 + assert [line.value for line in segment.lines] == ["line1", "+line3", "-line4"] + + +class CommitComparisonTests(TestCase): + def setUp(self): + self.base_commit = CommitFactory(updatestamp=datetime(2023, 1, 1)) + self.compare_commit = CommitFactory(updatestamp=datetime(2023, 1, 1)) + self.base_commit_report = CommitReportFactory(commit=self.base_commit) + self.compare_commit_report = CommitReportFactory(commit=self.compare_commit) + self.commit_comparison = CommitComparisonFactory( + base_commit=self.base_commit, + compare_commit=self.compare_commit, + ) + + def test_needs_recompute(self): + commit_comparison = CommitComparison.objects.get(pk=self.commit_comparison.pk) + service = CommitComparisonService(commit_comparison) + + assert service.needs_recompute() == False + + def test_needs_recompute_missing_timestamp(self): + Commit.objects.filter(pk=self.base_commit.id).update(updatestamp=None) + commit_comparison = CommitComparison.objects.get(pk=self.commit_comparison.pk) + service = CommitComparisonService(commit_comparison) + + assert service.needs_recompute() == False + + def test_stale_base_commit(self): + # base_commit was updated after this comparison was made + self.commit_comparison.updated_at = datetime(2021, 1, 1, tzinfo=pytz.utc) + self.commit_comparison.save() + self.base_commit.updatestamp = datetime(2023, 1, 2) + self.base_commit.save() + + commit_comparison = CommitComparison.objects.get(pk=self.commit_comparison.pk) + service = CommitComparisonService(commit_comparison) + assert service.needs_recompute() == True + + def test_stale_compare_commit(self): + # compare_commit was updated after this comparison was made + self.commit_comparison.updated_at = datetime(2021, 1, 1, tzinfo=pytz.utc) + self.commit_comparison.save() + + self.compare_commit.updatestamp = datetime(2023, 1, 2) + self.compare_commit.save() + + commit_comparison = CommitComparison.objects.get(pk=self.commit_comparison.pk) + service = CommitComparisonService(commit_comparison) + + assert service.needs_recompute() == True diff --git a/apps/codecov-api/services/tests/test_components.py b/apps/codecov-api/services/tests/test_components.py new file mode 100644 index 0000000000..0daf54caab --- /dev/null +++ b/apps/codecov-api/services/tests/test_components.py @@ -0,0 +1,285 @@ +from unittest.mock import PropertyMock, patch + +from django.contrib.auth.models import AnonymousUser +from django.test import TestCase +from shared.components import Component +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine +from shared.utils.sessions import Session +from shared.yaml.user_yaml import UserYaml + +from services.comparison import Comparison +from services.components import ( + ComponentComparison, + commit_components, + component_filtered_report, + filter_components_by_name_or_id, +) + + +def sample_report(): + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=["flag1", "flag2"])) + return report + + +class ComponentServiceTest(TestCase): + def setUp(self): + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org, private=False) + self.commit = CommitFactory(repository=self.repo) + + @patch("services.components.final_commit_yaml") + def test_commit_components(self, mock_final_yaml): + mock_final_yaml.return_value = UserYaml( + { + "component_management": { + "default_rules": { + "paths": [r".*\.py"], + "flag_regexes": [r"flag.*"], + }, + "individual_components": [ + {"component_id": "go_files", "paths": [r".*\.go"]}, + {"component_id": "rules_from_default"}, + { + "component_id": "I have my flags", + "flag_regexes": [r"python-.*"], + }, + { + "component_id": "required", + "name": "display", + "flag_regexes": [], + "paths": [r"src/.*"], + }, + ], + } + } + ) + + user = AnonymousUser() + components = commit_components(self.commit, user) + assert components == [ + Component( + component_id="go_files", + paths=[r".*\.go"], + name="", + flag_regexes=[r"flag.*"], + statuses=[], + ), + Component( + component_id="rules_from_default", + paths=[r".*\.py"], + name="", + flag_regexes=[r"flag.*"], + statuses=[], + ), + Component( + component_id="I have my flags", + paths=[r".*\.py"], + name="", + flag_regexes=[r"python-.*"], + statuses=[], + ), + Component( + component_id="required", + name="display", + paths=[r"src/.*"], + flag_regexes=[], + statuses=[], + ), + ] + + mock_final_yaml.assert_called_once_with(self.commit, user) + + def test_component_filtered_report(self): + report = sample_report() + + component_go = Component.from_dict( + { + "component_id": "golang", + "paths": [".*/*.go"], + } + ) + report_go = component_filtered_report(report, [component_go]) + assert report_go.files == ["file_1.go"] + assert report_go.totals.coverage == report.get("file_1.go").totals.coverage + + component_py = Component.from_dict( + { + "component_id": "python", + "paths": [".*/*.py"], + } + ) + report_py = component_filtered_report(report, [component_py]) + assert report_py.files == ["file_2.py"] + assert report_py.totals.coverage == report.get("file_2.py").totals.coverage + + +class ComponentComparisonTest(TestCase): + def setUp(self): + self.user = OwnerFactory() + self.repo = RepositoryFactory(author=self.user) + self.base_commit = CommitFactory(repository=self.repo) + self.head_commit = CommitFactory(repository=self.repo) + self.comparison = Comparison(self.user, self.base_commit, self.head_commit) + + @patch("services.comparison.Comparison.base_report", new_callable=PropertyMock) + def test_base_report(self, base_report_mock): + report = sample_report() + base_report_mock.return_value = report + + component_go = Component.from_dict( + { + "component_id": "golang", + "paths": [".*/*.go"], + } + ) + component_comparison = ComponentComparison(self.comparison, component_go) + assert component_comparison.base_report.files == ["file_1.go"] + assert ( + component_comparison.base_report.totals.coverage + == report.get("file_1.go").totals.coverage + ) + + @patch("services.comparison.Comparison.head_report", new_callable=PropertyMock) + def test_head_report(self, head_report_mock): + report = sample_report() + head_report_mock.return_value = report + + component_go = Component.from_dict( + { + "component_id": "golang", + "paths": [".*/*.go"], + } + ) + component_comparison = ComponentComparison(self.comparison, component_go) + assert component_comparison.head_report.files == ["file_1.go"] + assert ( + component_comparison.head_report.totals.coverage + == report.get("file_1.go").totals.coverage + ) + + @patch("services.comparison.Comparison.git_comparison", new_callable=PropertyMock) + @patch("services.comparison.Comparison.head_report", new_callable=PropertyMock) + def test_patch_totals(self, head_report_mock, git_comparison_mock): + report = sample_report() + head_report_mock.return_value = report + + git_comparison_mock.return_value = { + "diff": { + "files": { + "file_1.go": { + "type": "modified", + "segments": [ + { + "header": ["1", "2", "1", "1"], + "lines": ["-line", "+line", "+another line"], + } + ], + }, + "file_2.py": { + "type": "modified", + "segments": [ + { + "header": ["1", "1", "1", "1"], + "lines": ["-line", "+line"], + } + ], + }, + } + } + } + + component_go = Component.from_dict( + { + "component_id": "golang", + "paths": [".*/*.go"], + } + ) + component_comparison = ComponentComparison(self.comparison, component_go) + + # removed 1 tested line, added 1 tested and 1 untested line + assert component_comparison.patch_totals.coverage == "50.00000" + + def test_filter_components_by_name_or_id(self): + components = [ + Component( + name="ComponentA", + component_id="123", + paths=[], + flag_regexes=[], + statuses=[], + ), + Component( + name="ComponentB", + component_id="456", + paths=[], + flag_regexes=[], + statuses=[], + ), + Component( + name="ComponentC", + component_id="789", + paths=[], + flag_regexes=[], + statuses=[], + ), + ] + terms = ["comPOnentA", "123", "456"] + + filtered = filter_components_by_name_or_id(components, terms) + self.assertEqual(len(filtered), 2) + self.assertEqual(filtered[0].name, "ComponentA") + self.assertEqual(filtered[1].component_id, "456") + + def test_filter_components_by_name_or_id_no_matches(self): + components = [ + Component( + name="ComponentA", + component_id="123", + paths=[], + flag_regexes=[], + statuses=[], + ), + Component( + name="ComponentB", + component_id="456", + paths=[], + flag_regexes=[], + statuses=[], + ), + Component( + name="ComponentC", + component_id="789", + paths=[], + flag_regexes=[], + statuses=[], + ), + ] + terms = ["nonexistent", "000"] + + filtered = filter_components_by_name_or_id(components, terms) + self.assertEqual(len(filtered), 0) diff --git a/apps/codecov-api/services/tests/test_path.py b/apps/codecov-api/services/tests/test_path.py new file mode 100644 index 0000000000..9e3e3552cb --- /dev/null +++ b/apps/codecov-api/services/tests/test_path.py @@ -0,0 +1,382 @@ +from unittest.mock import MagicMock, patch + +import pytest +from django.conf import settings +from django.test import TestCase +from shared.django_apps.core.tests.factories import CommitFactory, OwnerFactory +from shared.reports.api_report_service import SerializableReport +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine, ReportTotals +from shared.torngit.exceptions import TorngitClientGeneralError +from shared.utils.sessions import Session + +from services.path import ( + Dir, + File, + PrefixedPath, + ReportPaths, + dashboard_commit_file_url, + provider_path_exists, +) + +# mock data + +file_data1 = [ + 2, + [0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0], + [[0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0]], + [0, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], +] + +file_data2 = [ + 2, + [0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0], + [[0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0]], + [0, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], +] + +file_data3 = [ + 2, + [0, 10, 3, 2, 0, "30.00000", 0, 0, 0, 0, 0, 0, 0], + [[0, 10, 3, 2, 0, "30.00000", 0, 0, 0, 0, 0, 0, 0]], + [0, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], +] + +totals1 = ReportTotals( + files=0, + lines=10, + hits=8, + misses=2, + partials=0, + coverage="80.00000", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, +) + +totals2 = ReportTotals( + files=0, + lines=10, + hits=8, + misses=2, + partials=0, + coverage="80.00000", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, +) + +totals3 = ReportTotals( + files=0, + lines=10, + hits=3, + misses=2, + partials=0, + coverage="30.00000", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, +) + +# tests + + +class TestPathNode(TestCase): + def setUp(self): + self.dir = Dir( + full_path="dir/subdir", + children=[ + File(full_path="dir/subdir/file2.py", totals=totals1), + File(full_path="dir/subdir/file3.py", totals=totals1), + ], + ) + + def test_lines(self): + # 2 files, 10 lins each + assert self.dir.lines == 20 + + def test_hits(self): + # 2 files, 8 hits each + assert self.dir.hits == 16 + + def test_misses(self): + # 2 files, 2 misses each + assert self.dir.misses == 4 + + def test_partials(self): + assert self.dir.partials == 0 + + def test_coverage(self): + assert self.dir.coverage == 80.0 + + file = File(full_path="file1.py", totals=ReportTotals.default_totals()) + assert file.coverage == 0 + + def test_name(self): + assert self.dir.name == "subdir" + + +class TestPrefixedPath(TestCase): + def test_relative_path(self): + path = PrefixedPath("dir/file1.py", "") + assert path.relative_path == "dir/file1.py" + + path = PrefixedPath("dir/file1.py", "dir") + assert path.relative_path == "file1.py" + + def test_basename(self): + path = PrefixedPath("dir/file1.py", "") + assert path.basename == "dir" + + path = PrefixedPath("file1.py", "") + assert path.basename == "file1.py" + + path = PrefixedPath("dir/subdir/file1.py", "dir") + assert path.basename == "dir/subdir" + + path = PrefixedPath("dir/subdir/file1.py", "dir/subdir") + assert path.basename == "dir/subdir/file1.py" + + +class TestReportPaths(TestCase): + def setUp(self): + files = { + "dir/file1.py": file_data1, + "dir/subdir/file2.py": file_data2, + "dir/subdir/file3.py": file_data3, + } + self.report = SerializableReport(files=files) + + def test_default_paths(self): + report_paths = ReportPaths(self.report) + assert report_paths.paths == [ + PrefixedPath("dir/file1.py", ""), + PrefixedPath("dir/subdir/file2.py", ""), + PrefixedPath("dir/subdir/file3.py", ""), + ] + + def test_prefix_paths(self): + report_paths = ReportPaths(self.report, path="dir") + assert report_paths.paths == [ + PrefixedPath("dir/file1.py", "dir"), + PrefixedPath("dir/subdir/file2.py", "dir"), + PrefixedPath("dir/subdir/file3.py", "dir"), + ] + + report_paths = ReportPaths(self.report, path="dir/subdir") + assert report_paths.paths == [ + PrefixedPath("dir/subdir/file2.py", "dir/subdir"), + PrefixedPath("dir/subdir/file3.py", "dir/subdir"), + ] + + def test_search_paths(self): + report_paths = ReportPaths(self.report, search_term="file") + assert report_paths.paths == [ + PrefixedPath("dir/file1.py", ""), + PrefixedPath("dir/subdir/file2.py", ""), + PrefixedPath("dir/subdir/file3.py", ""), + ] + + report_paths = ReportPaths(self.report, search_term="ile2") + assert report_paths.paths == [ + PrefixedPath("dir/subdir/file2.py", ""), + ] + + def test_full_filelist(self): + report_paths = ReportPaths(self.report) + assert report_paths.full_filelist() == [ + File(full_path="dir/file1.py", totals=totals1), + File(full_path="dir/subdir/file2.py", totals=totals2), + File(full_path="dir/subdir/file3.py", totals=totals3), + ] + + def test_single_directory(self): + report_paths = ReportPaths(self.report, path="dir") + assert report_paths.single_directory() == [ + File(full_path="dir/file1.py", totals=totals1), + Dir( + full_path="dir/subdir", + children=[ + File(full_path="dir/subdir/file2.py", totals=totals2), + File(full_path="dir/subdir/file3.py", totals=totals3), + ], + ), + ] + + def test_invalid_path(self): + report_paths = ReportPaths(self.report, path="wrong") + assert report_paths.paths == [] + + def test_files(self): + flags = ["flag-123"] + report = Report() + session_a_id, _ = report.add_session(Session(flags=["flag-123"])) + + file_a = ReportFile("foo/file1.py") + file_a.append(1, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + report.append(file_a) + + report_paths = ReportPaths(report=report, filter_flags=flags) + assert report_paths.files == ["foo/file1.py"] + + +class TestReportPathsNested(TestCase): + def setUp(self): + files = { + "dir/file1.py": file_data1, + "dir/subdir/file2.py": file_data2, + "dir/subdir/dir1/file3.py": file_data3, + "dir/subdir/dir2/file3.py": file_data3, + "src/ui/A/A.js": file_data3, + "src/ui/Avatar/A.js": file_data3, + } + self.report = SerializableReport(files=files) + + def test_single_directory(self): + report_paths = ReportPaths(self.report, path="dir") + assert report_paths.single_directory() == [ + File(full_path="dir/file1.py", totals=totals1), + Dir( + full_path="dir/subdir", + children=[ + File(full_path="dir/subdir/file2.py", totals=totals2), + Dir( + full_path="dir/subdir/dir1", + children=[ + File(full_path="dir/subdir/dir1/file3.py", totals=totals3), + ], + ), + Dir( + full_path="dir/subdir/dir2", + children=[ + File(full_path="dir/subdir/dir2/file3.py", totals=totals3), + ], + ), + ], + ), + ] + + report_paths = ReportPaths(self.report, path="src/ui/A") + assert report_paths.single_directory() == [ + File(full_path="src/ui/A/A.js", totals=totals3), + ] + + +class MockedProviderAdapter: + async def list_files(self, *args, **kwargs): + return [] + + +class TestProviderPath(TestCase): + def setUp(self): + self.commit = CommitFactory() + self.owner = OwnerFactory() + + @patch("services.repo_providers.RepoProviderService.get_adapter") + def test_provider_path(self, mock_provider_adapter): + mock_provider_adapter.return_value = MockedProviderAdapter() + assert provider_path_exists("foo/bar", self.commit, self.owner) == True + + @patch("services.repo_providers.RepoProviderService.get_adapter") + def test_provider_path_not_found(self, mock_provider_adapter): + mock_provider_adapter.side_effect = TorngitClientGeneralError(404, None, None) + assert provider_path_exists("foo/bar", self.commit, self.owner) == False + + @patch("services.repo_providers.RepoProviderService.get_adapter") + def test_provider_path_other_error(self, mock_provider_adapter): + mock_provider_adapter.side_effect = TorngitClientGeneralError(500, None, None) + assert provider_path_exists("foo/bar", self.commit, self.owner) is None + + +@pytest.mark.usefixtures("sample_report") +class TestPathMisc(TestCase): + def setUp(self): + self.service = "gh" + self.owner = "marquet" + self.repo = "yios" + self.commit_sha = "540feb1e8c5d39b714c43874d0aa9da02ad257b7" + self.commit = MagicMock( + commitid=self.commit_sha, full_report=self.sample_report + ) + + def test_dashboard_commit_file_url_path_none(self): + path = None + commit_file_url = dashboard_commit_file_url( + path=path, + service=self.service, + owner=self.owner, + repo=self.repo, + commit=self.commit, + ) + assert ( + commit_file_url + == f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.owner}/{self.repo}/commit/{self.commit_sha}/tree/" + ) + + def test_dashboard_commit_file_url_empty_path(self): + path = "" + commit_file_url = dashboard_commit_file_url( + path=path, + service=self.service, + owner=self.owner, + repo=self.repo, + commit=self.commit, + ) + assert ( + commit_file_url + == f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.owner}/{self.repo}/commit/{self.commit_sha}/tree/" + ) + + def test_dashboard_commit_file_nonexistent_path(self): + path = "path/not/in/report" + commit_file_url = dashboard_commit_file_url( + path=path, + service=self.service, + owner=self.owner, + repo=self.repo, + commit=self.commit, + ) + assert ( + commit_file_url + == f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.owner}/{self.repo}/commit/{self.commit_sha}/tree/{path}" + ) + + def test_dashboard_commit_file_not_in_report(self): + path = "foo/file2.py" + commit_file_url = dashboard_commit_file_url( + path=path, + service=self.service, + owner=self.owner, + repo=self.repo, + commit=self.commit, + ) + assert ( + commit_file_url + == f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.owner}/{self.repo}/commit/{self.commit_sha}/tree/{path}" + ) + + def test_dashboard_commit_file_in_report(self): + path = "foo/file1.py" + commit_file_url = dashboard_commit_file_url( + path=path, + service=self.service, + owner=self.owner, + repo=self.repo, + commit=self.commit, + ) + assert ( + commit_file_url + == f"{settings.CODECOV_DASHBOARD_URL}/{self.service}/{self.owner}/{self.repo}/commit/{self.commit_sha}/blob/{path}" + ) diff --git a/apps/codecov-api/services/tests/test_redis_configuration.py b/apps/codecov-api/services/tests/test_redis_configuration.py new file mode 100644 index 0000000000..1b48fbf37c --- /dev/null +++ b/apps/codecov-api/services/tests/test_redis_configuration.py @@ -0,0 +1,9 @@ +from shared.helpers.redis import get_redis_connection + + +def test_get_redis_connection(mocker): + mocker.patch("shared.helpers.redis.get_config", return_value=None) + mocked = mocker.patch("shared.helpers.redis.Redis.from_url") + res = get_redis_connection() + assert res is not None + mocked.assert_called_with("redis://redis:6379") diff --git a/apps/codecov-api/services/tests/test_refresh.py b/apps/codecov-api/services/tests/test_refresh.py new file mode 100644 index 0000000000..bfc21a7666 --- /dev/null +++ b/apps/codecov-api/services/tests/test_refresh.py @@ -0,0 +1,107 @@ +import pytest + +from services.refresh import RefreshService + +celery_task_data = {"random": "data"} + + +class AsyncReturnMock: + parent = None + + def as_tuple(self): + return celery_task_data + + def successful(self): + return False + + def failed(self): + return False + + +class AsyncReturnMockSuccessful(AsyncReturnMock): + def successful(self): + return True + + +class AsyncReturnMockFailed(AsyncReturnMock): + def failed(self): + return True + + +class AsyncReturnMockParentFailed(AsyncReturnMock): + @property + def parent(self): + return AsyncReturnMockFailed() + + +@pytest.fixture +def mock_refresh(mocker): + mock_task_service = mocker.patch("services.task.TaskService.refresh") + mock_task_service.return_value = AsyncReturnMock() + yield mock_task_service + + +@pytest.fixture +def mock_result_from_tuple(mocker): + mock_result_from_tuple = mocker.patch("services.refresh.result_from_tuple") + mock_result_from_tuple.return_value = AsyncReturnMock() + yield mock_result_from_tuple + + +def test_is_refreshing_true_after_trigger( + mock_result_from_tuple, mock_refresh, mock_redis +): + assert RefreshService().is_refreshing(5) is False + RefreshService().trigger_refresh(5, "codecov") + assert RefreshService().is_refreshing(5) is True + + +def test_refresh_makes_proper_redis_calls( + mock_result_from_tuple, mock_refresh, mock_redis +): + RefreshService().trigger_refresh(5, "codecov") + assert mock_redis.get("refresh_5") == b'{"random": "data"}' + + +def test_dont_refresh_is_already_refreshing( + mock_result_from_tuple, mock_refresh, mock_redis +): + RefreshService().trigger_refresh(5, "codecov") + mock_refresh.assert_called() + mock_refresh.reset_mock() + RefreshService().trigger_refresh(5, "codecov") + mock_refresh.assert_not_called() + + +def test_is_refreshing_false_when_task_is_successful( + mock_result_from_tuple, mock_refresh, mock_redis +): + RefreshService().trigger_refresh(5, "codecov") + assert mock_redis.get("refresh_5") == b'{"random": "data"}' + mock_result_from_tuple.return_value = AsyncReturnMockSuccessful() + assert RefreshService().is_refreshing(5) is False + assert mock_redis.get("refresh_5") is None + + +def test_is_refreshing_false_when_task_is_failed( + mock_result_from_tuple, mock_refresh, mock_redis +): + RefreshService().trigger_refresh(5, "codecov") + mock_result_from_tuple.return_value = AsyncReturnMockFailed() + assert RefreshService().is_refreshing(5) is False + + +def test_is_refreshing_false_when_parent_task_is_failed( + mock_result_from_tuple, mock_refresh, mock_redis +): + RefreshService().trigger_refresh(5, "codecov") + mock_result_from_tuple.return_value = AsyncReturnMockParentFailed() + assert RefreshService().is_refreshing(5) is False + + +def test_is_refreshing_false_when_result_from_tuple_raise( + mock_result_from_tuple, mock_refresh, mock_redis +): + RefreshService().trigger_refresh(5, "codecov") + mock_result_from_tuple.side_effect = ValueError + assert RefreshService().is_refreshing(5) is False diff --git a/apps/codecov-api/services/tests/test_repo_providers.py b/apps/codecov-api/services/tests/test_repo_providers.py new file mode 100644 index 0000000000..5e301abb2f --- /dev/null +++ b/apps/codecov-api/services/tests/test_repo_providers.py @@ -0,0 +1,378 @@ +import inspect +from unittest.mock import patch + +import pytest +from asgiref.sync import sync_to_async +from django.conf import settings +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory +from shared.torngit import Bitbucket, Github, Gitlab + +from codecov_auth.models import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, + Owner, + Service, +) +from services.repo_providers import RepoProviderService, get_token_refresh_callback +from utils.encryption import encryptor + + +def mock_get_config_verify_ssl_true(*args): + if args == ("github", "verify_ssl"): + return True + if args == ("github", "ssl_pem"): + return "ssl_pem" + + +def mock_get_config_verify_ssl_false(*args): + if args == ("github", "verify_ssl"): + return False + if args == ("github", "ssl_pem"): + return "ssl_pem" + + +def mock_get_env_ca_bundle(*args): + if args == ("REQUESTS_CA_BUNDLE",): + return "REQUESTS_CA_BUNDLE" + + +@pytest.mark.parametrize("using_integration", [True, False]) +def test__is_using_integration_deprecated_flow(using_integration, db): + repo = RepositoryFactory.create(using_integration=using_integration) + assert RepoProviderService()._is_using_integration(None, repo) == using_integration + + +def test__is_using_integration_ghapp_covers_all_repos(db): + owner = OwnerFactory.create(service="github") + repo = RepositoryFactory.create(author=owner) + other_repo_same_owner = RepositoryFactory.create(author=owner) + repo_different_owner = RepositoryFactory.create() + assert repo.author != repo_different_owner.author + ghapp_installation = GithubAppInstallation( + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + owner=owner, + repository_service_ids=None, + installation_id=12345, + ) + ghapp_installation.save() + assert RepoProviderService()._is_using_integration(ghapp_installation, repo) == True + assert ( + RepoProviderService()._is_using_integration( + ghapp_installation, other_repo_same_owner + ) + == True + ) + assert ( + RepoProviderService()._is_using_integration( + ghapp_installation, repo_different_owner + ) + == False + ) + + +def test__is_using_integration_ghapp_covers_some_repos(db): + owner = OwnerFactory.create(service="github") + repo = RepositoryFactory.create(author=owner) + other_repo_same_owner = RepositoryFactory.create(author=owner) + repo_different_owner = RepositoryFactory.create() + assert repo.author != repo_different_owner.author + ghapp_installation = GithubAppInstallation( + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + owner=owner, + repository_service_ids=[repo.service_id], + installation_id=12345, + ) + ghapp_installation.save() + assert RepoProviderService()._is_using_integration(ghapp_installation, repo) == True + assert ( + RepoProviderService()._is_using_integration( + ghapp_installation, other_repo_same_owner + ) + == False + ) + assert ( + RepoProviderService()._is_using_integration( + ghapp_installation, repo_different_owner + ) + == False + ) + + +@pytest.mark.parametrize( + "should_have_owner,service", + [ + (False, Service.GITHUB.value), + (True, Service.BITBUCKET.value), + (True, Service.BITBUCKET_SERVER.value), + ], +) +def test_token_refresh_callback_none_cases(should_have_owner, service, db): + owner = None + if should_have_owner: + owner = OwnerFactory(service=service) + assert get_token_refresh_callback(owner, service) is None + + +class TestRepoProviderService(TestCase): + def setUp(self): + self.repo_gh = RepositoryFactory.create( + author__unencrypted_oauth_token="testaaft3ituvli790m1yajovjv5eg0r4j0264iw", + author__username="ThiagoCodecov", + author__service="github", + ) + self.repo_gl = RepositoryFactory.create( + author__unencrypted_oauth_token="testaaft3ituvli790m1yajovjv5eg0r4j0264iw", + author__username="ThiagoCodecov", + author__service="gitlab", + ) + + @sync_to_async + def get_owner_gl(self): + return Owner.objects.filter(ownerid=self.repo_gl.author.ownerid).first() + + @sync_to_async + def get_owner_gh(self): + return Owner.objects.filter(ownerid=self.repo_gh.author.ownerid).first() + + def test_get_torngit_with_names_github(self): + provider = RepoProviderService().get_by_name( + self.repo_gh.author, + self.repo_gh.name, + self.repo_gh.author, + self.repo_gh.author.service, + ) + assert isinstance(Github(), type(provider)) + + def test_get_torngit_with_names_gitlab(self): + provider = RepoProviderService().get_by_name( + self.repo_gl.author, + self.repo_gl.name, + self.repo_gl.author, + self.repo_gl.author.service, + ) + assert isinstance(provider, Gitlab) + assert provider._on_token_refresh is not None + + @pytest.mark.asyncio + async def test_refresh_callback(self): + provider = RepoProviderService().get_by_name( + self.repo_gl.author, + self.repo_gl.name, + self.repo_gl.author, + self.repo_gl.author.service, + ) + assert isinstance(Gitlab(), type(provider)) + assert provider._on_token_refresh is not None + assert inspect.isawaitable(provider._on_token_refresh()) + owner = await self.get_owner_gl() + saved_token = encryptor.decrypt_token(owner.oauth_token) + assert saved_token["key"] == "testaaft3ituvli790m1yajovjv5eg0r4j0264iw" + assert "refresh_token" not in saved_token + + new_token = {"key": "00001023102301", "refresh_token": "20349230952"} + await provider._on_token_refresh(new_token) + owner = await self.get_owner_gl() + assert owner.username == "ThiagoCodecov" + saved_token = encryptor.decrypt_token(owner.oauth_token) + assert saved_token["key"] == "00001023102301" + assert saved_token["refresh_token"] == "20349230952" + + @pytest.mark.asyncio + async def test_refresh_callback_github(self): + provider = RepoProviderService().get_by_name( + self.repo_gh.author, + self.repo_gh.name, + self.repo_gh.author, + self.repo_gh.author.service, + ) + assert isinstance(Github(), type(provider)) + assert provider._on_token_refresh is not None + assert inspect.isawaitable(provider._on_token_refresh()) + owner = await self.get_owner_gh() + saved_token = encryptor.decrypt_token(owner.oauth_token) + assert saved_token["key"] == "testaaft3ituvli790m1yajovjv5eg0r4j0264iw" + assert "refresh_token" not in saved_token + + new_token = {"key": "00001023102301", "refresh_token": "20349230952"} + await provider._on_token_refresh(new_token) + owner = await self.get_owner_gh() + assert owner.username == "ThiagoCodecov" + saved_token = encryptor.decrypt_token(owner.oauth_token) + assert saved_token["key"] == "00001023102301" + assert saved_token["refresh_token"] == "20349230952" + + def test_get_adapter_returns_adapter_for_repo_authors_service(self): + some_other_user = OwnerFactory(service="github") + repo = RepositoryFactory.create( + author__username="ThiagoCodecov", author__service="bitbucket" + ) + provider = RepoProviderService().get_adapter(some_other_user, repo) + assert isinstance(Bitbucket(), type(provider)) + + def test_get_by_name_returns_adapter_for_repo_owner_service(self): + some_other_user = OwnerFactory(service="bitbucket") + repo_name = "gl-repo" + repo_owner_username = "me" + repo_owner_service = "gitlab" + + provider = RepoProviderService().get_by_name( + owner=some_other_user, + repo_name=repo_name, + repo_owner_username=repo_owner_username, + repo_owner_service=repo_owner_service, + ) + + assert isinstance(Gitlab(), type(provider)) + + def test_get_by_name_submits_consumer_oauth_token(self): + user = OwnerFactory(service="bitbucket") + repo_name = "bb-repo" + repo_owner_username = "me" + repo_owner_service = "bitbucket" + + provider = RepoProviderService().get_by_name( + owner=user, + repo_name=repo_name, + repo_owner_username=repo_owner_username, + repo_owner_service=repo_owner_service, + ) + + assert provider._oauth_consumer_token() is not None + + @patch("services.repo_providers.get_provider") + @patch("services.repo_providers.get_config") + def test_get_adapter_verify_ssl_true(self, mock_get_config, mock_get_provider): + mock_get_config.side_effect = mock_get_config_verify_ssl_true + bot = OwnerFactory() + user = OwnerFactory(service="github") + repo = RepositoryFactory.create(author=user, bot=bot) + + RepoProviderService().get_adapter( + user, repo, use_ssl=True, token=repo.bot.oauth_token + ) + mock_get_provider.call_args == ( + ( + "github", + dict( + repo=dict( + name=repo.name, + using_integration=repo.using_integration, + service_id=repo.service_id, + private=repo.private, + ), + owner=dict(username=repo.author.username), + token=encryptor.decrypt_token(repo.bot.oauth_token), + verify_ssl="ssl_pem", + oauth_consumer_token=dict( + key=getattr(settings, "GITHUB_CLIENT_ID", "unknown"), + secret=getattr(settings, "GITHUB_CLIENT_SECRET", "unknown"), + ), + ), + ), + ) + + @patch("services.repo_providers.get_provider") + @patch("services.repo_providers.get_config") + @patch("services.repo_providers.getenv") + def test_get_adapter_for_uploads_verify_ssl_false( + self, mock_get_env, mock_get_config, mock_get_provider + ): + mock_get_config.side_effect = mock_get_config_verify_ssl_false + mock_get_env.side_effect = mock_get_env_ca_bundle + bot = OwnerFactory() + user = OwnerFactory(service="github") + repo = RepositoryFactory.create(author=user, bot=bot) + + RepoProviderService().get_adapter( + user, repo, use_ssl=True, token=repo.bot.oauth_token + ) + mock_get_provider.call_args == ( + ( + "github", + dict( + repo=dict( + name=repo.name, + using_integration=repo.using_integration, + service_id=repo.service_id, + private=repo.private, + ), + owner=dict(username=repo.author.username), + token=encryptor.decrypt_token(repo.bot.oauth_token), + verify_ssl="REQUESTS_CA_BUNDLE", + oauth_consumer_token=dict( + key=getattr(settings, "GITHUB_CLIENT_ID", "unknown"), + secret=getattr(settings, "GITHUB_CLIENT_SECRET", "unknown"), + ), + ), + ), + ) + + def test_get_adapter_sets_token_to_bot_when_user_not_authenticated(self): + repo_owner = OwnerFactory(service="github") + repo = RepositoryFactory(author=repo_owner) + adapter = RepoProviderService().get_adapter(None, repo) + assert adapter.token["key"] == settings.GITHUB_BOT_KEY + + def test_get_by_name_sets_token_to_bot_when_user_not_authenticated(self): + repo_name = "gh-repo" + repo_owner_username = "me" + repo_owner_service = "github" + + adapter = RepoProviderService().get_by_name( + owner=None, + repo_name=repo_name, + repo_owner_username=repo_owner_username, + repo_owner_service=repo_owner_service, + ) + + assert adapter.token["key"] == settings.GITHUB_BOT_KEY + + def test_get_adapter_sets_owner_service_id(self): + owner = OwnerFactory() + repo = RepositoryFactory(author=owner) + user = OwnerFactory() + adapter = RepoProviderService().get_adapter(owner=user, repo=repo) + assert adapter.data["owner"]["service_id"] == owner.service_id + + @pytest.mark.asyncio + @patch( + "services.repo_providers.RepoProviderService._get_adapter", + return_value="torngit_adapter", + ) + async def test_async_get_adapter(self, mock__get_adapter): + owner = await self.get_owner_gh() + ghapp_installation = GithubAppInstallation( + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + installation_id=1234, + owner=owner, + repository_service_ids=None, + ) + await ghapp_installation.asave() + fetched = await RepoProviderService().async_get_adapter(owner, self.repo_gh) + assert fetched == "torngit_adapter" + mock__get_adapter.assert_called_with( + owner, self.repo_gh, ghapp=ghapp_installation + ) + + @pytest.mark.asyncio + @patch( + "services.repo_providers.RepoProviderService._get_adapter", + return_value="torngit_adapter", + ) + async def test_async_get_adapter_owner_not_github(self, mock__get_adapter): + owner = await self.get_owner_gl() + fetched = await RepoProviderService().async_get_adapter(owner, self.repo_gl) + assert fetched == "torngit_adapter" + mock__get_adapter.assert_called_with(owner, self.repo_gl, ghapp=None) + + @pytest.mark.asyncio + @patch( + "services.repo_providers.RepoProviderService._get_adapter", + return_value="torngit_adapter", + ) + async def test_async_get_adapter_no_installation(self, mock__get_adapter): + owner = await self.get_owner_gh() + fetched = await RepoProviderService().async_get_adapter(owner, self.repo_gh) + assert fetched == "torngit_adapter" + mock__get_adapter.assert_called_with(owner, self.repo_gh, ghapp=None) diff --git a/apps/codecov-api/services/tests/test_report.py b/apps/codecov-api/services/tests/test_report.py new file mode 100644 index 0000000000..e93fca7bf5 --- /dev/null +++ b/apps/codecov-api/services/tests/test_report.py @@ -0,0 +1,333 @@ +from pathlib import Path +from unittest.mock import patch + +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + CommitWithReportFactory, +) +from shared.reports.api_report_service import build_report_from_commit +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine +from shared.storage.exceptions import FileNotInStorageError +from shared.utils.sessions import Session + +from reports.tests.factories import UploadFactory, UploadFlagMembershipFactory +from services.report import files_belonging_to_flags + +current_file = Path(__file__) + + +def flags_report(): + report = Report() + session_a_id, _ = report.add_session(Session(flags=["flag-a"])) + session_b_id, _ = report.add_session(Session(flags=["flag-b"])) + session_c_id, _ = report.add_session(Session(flags=["flag-c"])) + + file_a = ReportFile("foo/file1.py") + file_a.append(1, ReportLine.create(coverage=1, sessions=[[session_a_id, 1]])) + report.append(file_a) + + file_b = ReportFile("bar/file2.py") + file_b.append(12, ReportLine.create(coverage=1, sessions=[[session_b_id, 1]])) + report.append(file_b) + + file_c = ReportFile("another/file3.py") + file_c.append(12, ReportLine.create(coverage=1, sessions=[[session_c_id, 1]])) + report.append(file_c) + + return report + + +def sorted_files(report: Report) -> list[ReportFile]: + files = [report.get(file) for file in report.files] + return sorted(files, key=lambda x: x.name) + + +class ReportServiceTest(TestCase): + @patch("shared.api_archive.archive.ArchiveService.read_chunks") + def test_build_report_from_commit(self, read_chunks_mock): + f = open(current_file.parent / "samples" / "chunks.txt", "r") + read_chunks_mock.return_value = f.read() + commit = CommitWithReportFactory.create(message="aaaaa", commitid="abf6d4d") + commit_report = commit.reports.first() + + # this will be ignored + UploadFactory( + report=commit_report, + order_number=None, + storage_path="v4/raw/2019-01-10/4434BC2A2EC4FCA57F77B473D83F928C/abf6d4df662c47e32460020ab14abf9303581429/9ccc55a1-8b41-4bb1-a946-ee7a33a7fb56.txt", + state="error", + ) + + res = build_report_from_commit(commit) + assert len(res.files) == 3 + file_1, file_2, file_3 = sorted_files(res) + assert file_1.name == "awesome/__init__.py" + assert tuple(file_1.totals) == (0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0) + assert file_2.name == "tests/__init__.py" + assert tuple(file_2.totals) == (0, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0) + assert file_3.name == "tests/test_sample.py" + assert tuple(file_3.totals) == (0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0) + read_chunks_mock.assert_called_with("abf6d4d") + assert list(res.totals) == [ + 3, + 20, + 17, + 3, + 0, + "85.00000", + 0, + 0, + 0, + 1, + 0, + 0, + [1, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + ] + + @patch("shared.api_archive.archive.ArchiveService.read_chunks") + def test_build_report_from_commit_file_not_in_storage(self, read_chunks_mock): + read_chunks_mock.side_effect = FileNotInStorageError() + commit = CommitWithReportFactory.create(message="aaaaa", commitid="abf6d4d") + assert build_report_from_commit(commit) is None + + @patch("shared.api_archive.archive.ArchiveService.read_chunks") + def test_build_report_from_commit_cff_and_direct_uploads(self, read_chunks_mock): + f = open(current_file.parent / "samples" / "chunks.txt", "r") + read_chunks_mock.return_value = f.read() + + commit = CommitWithReportFactory.create() + commit_report = commit.reports.first() + + # this upload will be ignored since there's another direct + # upload with the same flag + upload = UploadFactory( + report=commit_report, + order_number=2, + storage_path="v4/raw/2019-01-10/4434BC2A2EC4FCA57F77B473D83F928C/abf6d4df662c47e32460020ab14abf9303581429/9ccc55a1-8b41-4bb1-a946-ee7a33a7fb56.txt", + upload_type="carriedforward", + ) + UploadFlagMembershipFactory( + report_session=upload, + flag=commit.repository.flags.get(flag_name="unittests"), + ) + + res = build_report_from_commit(commit) + assert len(res.sessions) == 2 + + @patch("shared.api_archive.archive.ArchiveService.read_chunks") + def test_build_report_from_commit_null_session_totals(self, read_chunks_mock): + f = open(current_file.parent / "samples" / "chunks.txt", "r") + read_chunks_mock.return_value = f.read() + commit = CommitWithReportFactory.create(message="aaaaa", commitid="abf6d4d") + upload = commit.reports.first().sessions.first() + upload.uploadleveltotals.delete() + res = build_report_from_commit(commit) + assert len(res.files) == 3 + file_1, file_2, file_3 = sorted_files(res) + assert file_1.name == "awesome/__init__.py" + assert tuple(file_1.totals) == (0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0) + assert file_2.name == "tests/__init__.py" + assert tuple(file_2.totals) == (0, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0) + assert file_3.name == "tests/test_sample.py" + assert tuple(file_3.totals) == (0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0) + read_chunks_mock.assert_called_with("abf6d4d") + assert list(res.totals) == [ + 3, + 20, + 17, + 3, + 0, + "85.00000", + 0, + 0, + 0, + 1, + 0, + 0, + [1, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + ] + + @patch("shared.api_archive.archive.ArchiveService.read_chunks") + def test_build_report_from_commit_with_flags(self, read_chunks_mock): + f = open(current_file.parent / "samples" / "chunks.txt", "r") + read_chunks_mock.return_value = f.read() + commit = CommitWithReportFactory.create(message="aaaaa", commitid="abf6d4d") + report = build_report_from_commit(commit) + res = report.flags["integrations"].report + assert len(res.files) == 3 + file_1, file_2, file_3 = sorted_files(res) + assert file_1.name == "awesome/__init__.py" + assert tuple(file_1.totals) == (0, 10, 1, 9, 0, "10.00000", 0, 0, 0, 0, 0, 0, 0) + assert file_2.name == "tests/__init__.py" + assert tuple(file_2.totals) == (0, 3, 0, 3, 0, "0", 0, 0, 0, 0, 0, 0, 0) + assert file_3.name == "tests/test_sample.py" + assert tuple(file_3.totals) == (0, 7, 2, 5, 0, "28.57143", 0, 0, 0, 0, 0, 0, 0) + read_chunks_mock.assert_called_with("abf6d4d") + assert list(res.totals) == [3, 20, 3, 17, 0, "15.00000", 0, 0, 0, 1, 0, 0, 0] + + @patch("shared.api_archive.archive.ArchiveService.read_chunks") + def test_build_report_from_commit_with_non_carried_forward_flags( + self, read_chunks_mock + ): + f = open(current_file.parent / "samples" / "chunks.txt", "r") + read_chunks_mock.return_value = f.read() + commit = CommitWithReportFactory.create( + message="another test", + commitid="asdfbhasdf89", + ) + + report = commit._report + report["sessions"]["1"].update( + st="carriedforward", + se={"carriedforward_from": "56e05fced214c44a37759efa2dfc25a65d8ae98d"}, + ) + commit.save() + + commit_report = commit.reports.first() + session = commit_report.sessions.filter(order_number=1).first() + session.upload_type = "carriedforward" + session.upload_extras = { + "carriedforward_from": "56e05fced214c44a37759efa2dfc25a65d8ae98d" + } + session.save() + + report = build_report_from_commit(commit) + res = report.flags["integrations"].report + assert len(res.files) == 3 + file_1, file_2, file_3 = sorted_files(res) + assert file_1.name == "awesome/__init__.py" + assert tuple(file_1.totals) == (0, 10, 1, 9, 0, "10.00000", 0, 0, 0, 0, 0, 0, 0) + assert file_2.name == "tests/__init__.py" + assert tuple(file_2.totals) == (0, 3, 0, 3, 0, "0", 0, 0, 0, 0, 0, 0, 0) + assert file_3.name == "tests/test_sample.py" + assert tuple(file_3.totals) == (0, 7, 2, 5, 0, "28.57143", 0, 0, 0, 0, 0, 0, 0) + read_chunks_mock.assert_called_with("asdfbhasdf89") + assert list(res.totals) == [3, 20, 3, 17, 0, "15.00000", 0, 0, 0, 1, 0, 0, 0] + cff_session = res.report.sessions[1] + assert cff_session.session_type.value == "carriedforward" + assert ( + cff_session.session_extras["carriedforward_from"] + == "56e05fced214c44a37759efa2dfc25a65d8ae98d" + ) + + def test_build_report_from_commit_no_report(self): + commit = CommitFactory() + report = build_report_from_commit(commit) + assert report is None + + @patch("shared.api_archive.archive.ArchiveService.read_chunks") + def test_build_report_from_commit_fallback(self, read_chunks_mock): + f = open(current_file.parent / "samples" / "chunks.txt", "r") + read_chunks_mock.return_value = f.read() + + report = { + "files": { + "awesome/__init__.py": [ + 2, + [0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0], + [[0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0]], + [0, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + ], + "tests/__init__.py": [ + 0, + [0, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0], + [[0, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "tests/test_sample.py": [ + 1, + [0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + [[0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + }, + "sessions": { + "0": { + "N": None, + "a": "v4/raw/2019-01-10/4434BC2A2EC4FCA57F77B473D83F928C/abf6d4df662c47e32460020ab14abf9303581429/9ccc55a1-8b41-4bb1-a946-ee7a33a7fb56.txt", + "c": None, + "d": 1547084427, + "e": None, + "f": ["unittests"], + "j": None, + "n": None, + "p": None, + "t": [3, 20, 17, 3, 0, "85.00000", 0, 0, 0, 0, 0, 0, 0], + "": None, + }, + "1": { + "N": None, + "a": "v4/raw/2019-01-10/4434BC2A2EC4FCA57F77B473D83F928C/abf6d4df662c47e32460020ab14abf9303581429/9ccc55a1-8b41-4bb1-a946-ee7a33a7fb56.txt", + "c": None, + "d": 1547084427, + "e": None, + "f": ["integrations"], + "j": None, + "n": None, + "p": None, + "t": [3, 20, 17, 3, 0, "85.00000", 0, 0, 0, 0, 0, 0, 0], + "": None, + }, + }, + } + + # there are no associated `reports_*` records but we have `commits.report` populated + commit = CommitFactory.create( + message="aaaaa", commitid="abf6d4d", _report=report + ) + res = build_report_from_commit(commit) + + assert len(res.files) == 3 + file_1, file_2, file_3 = sorted_files(res) + assert file_1.name == "awesome/__init__.py" + assert tuple(file_1.totals) == (0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0) + assert file_2.name == "tests/__init__.py" + assert tuple(file_2.totals) == (0, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0) + assert file_3.name == "tests/test_sample.py" + assert tuple(file_3.totals) == (0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0) + read_chunks_mock.assert_called_with("abf6d4d") + assert list(res.totals) == [ + 3, + 20, + 17, + 3, + 0, + "85.00000", + 0, + 0, + 0, + 1, + 0, + 0, + [1, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + ] + + def test_files_belonging_to_flags_with_one_flag(self): + commit_report = flags_report() + flags = ["flag-a"] + files = files_belonging_to_flags(commit_report=commit_report, flags=flags) + assert len(files) == 1 + assert files == ["foo/file1.py"] + + def test_files_belonging_to_flags_with_all_flags(self): + commit_report = flags_report() + flags = ["flag-a", "flag-b", "flag-c"] + files = files_belonging_to_flags(commit_report=commit_report, flags=flags) + assert len(files) == 3 + assert files == ["foo/file1.py", "bar/file2.py", "another/file3.py"] + + def test_files_belonging_to_flags_with_known_and_unknown_flag(self): + commit_report = flags_report() + flags = ["flag-a", "flag-b", "random-value-123"] + files = files_belonging_to_flags(commit_report=commit_report, flags=flags) + assert len(files) == 2 + assert files == ["foo/file1.py", "bar/file2.py"] + + def test_files_belonging_to_flags_with_only_unknown_flag(self): + commit_report = flags_report() + flags = ["random-value-123"] + files = files_belonging_to_flags(commit_report=commit_report, flags=flags) + assert len(files) == 0 + assert files == [] diff --git a/apps/codecov-api/services/tests/test_self_hosted.py b/apps/codecov-api/services/tests/test_self_hosted.py new file mode 100644 index 0000000000..d555e4db9d --- /dev/null +++ b/apps/codecov-api/services/tests/test_self_hosted.py @@ -0,0 +1,241 @@ +from unittest.mock import patch + +from django.test import TestCase, override_settings +from shared.django_apps.core.tests.factories import OwnerFactory +from shared.license import LicenseInformation + +from codecov_auth.models import Owner +from services.self_hosted import ( + LicenseException, + activate_owner, + activated_owners, + admin_owners, + can_activate_owner, + deactivate_owner, + disable_autoactivation, + enable_autoactivation, + is_activated_owner, + is_admin_owner, + is_autoactivation_enabled, + license_seats, +) + + +@override_settings(IS_ENTERPRISE=True) +class SelfHostedTestCase(TestCase): + @patch("services.self_hosted.get_config") + def test_admin_owners(self, get_config): + owner1 = OwnerFactory(service="github", username="foo") + OwnerFactory(service="github", username="bar") + owner3 = OwnerFactory(service="gitlab", username="foo") + + get_config.return_value = [ + {"service": "github", "username": "foo"}, + {"service": "gitlab", "username": "foo"}, + ] + + owners = admin_owners() + assert list(owners) == [owner1, owner3] + + get_config.assert_called_once_with("setup", "admins", default=[]) + + def test_admin_owners_empty(self): + OwnerFactory(service="github", username="foo") + OwnerFactory(service="github", username="bar") + OwnerFactory(service="gitlab", username="foo") + + owners = admin_owners() + assert list(owners) == [] + + @patch("services.self_hosted.admin_owners") + def test_is_admin_owner(self, admin_owners): + owner1 = OwnerFactory(service="github", username="foo") + owner2 = OwnerFactory(service="github", username="bar") + owner3 = OwnerFactory(service="gitlab", username="foo") + + admin_owners.return_value = Owner.objects.filter(pk__in=[owner1.pk, owner2.pk]) + + assert is_admin_owner(owner1) == True + assert is_admin_owner(owner2) == True + assert is_admin_owner(owner3) == False + assert is_admin_owner(None) == False + + def test_activated_owners(self): + user1 = OwnerFactory() + user2 = OwnerFactory() + user3 = OwnerFactory() + OwnerFactory() + OwnerFactory(plan_activated_users=[user1.pk]) + OwnerFactory(plan_activated_users=[user2.pk, user3.pk]) + + owners = activated_owners() + assert list(owners) == [user1, user2, user3] + + @patch("services.self_hosted.activated_owners") + def test_is_activated_owner(self, activated_owners): + owner1 = OwnerFactory(service="github", username="foo") + owner2 = OwnerFactory(service="github", username="bar") + owner3 = OwnerFactory(service="gitlab", username="foo") + + activated_owners.return_value = Owner.objects.filter( + pk__in=[owner1.pk, owner2.pk] + ) + + assert is_activated_owner(owner1) == True + assert is_activated_owner(owner2) == True + assert is_activated_owner(owner3) == False + + @patch("services.self_hosted.get_current_license") + def test_license_seats(self, get_current_license): + get_current_license.return_value = LicenseInformation( + number_allowed_users=123, is_valid=True + ) + assert license_seats() == 123 + + @patch("services.self_hosted.get_current_license") + def test_license_seats_not_specified(self, get_current_license): + get_current_license.return_value = LicenseInformation(is_valid=True) + assert license_seats() == 0 + + @patch("services.self_hosted.activated_owners") + @patch("services.self_hosted.license_seats") + def test_can_activate_owner(self, license_seats, activated_owners): + license_seats.return_value = 1 + + owner1 = OwnerFactory(service="github", username="foo") + owner2 = OwnerFactory(service="github", username="bar") + owner3 = OwnerFactory(service="gitlab", username="foo") + + activated_owners.return_value = Owner.objects.filter( + pk__in=[owner1.pk, owner2.pk] + ) + + assert can_activate_owner(owner1) == True + assert can_activate_owner(owner2) == True + assert can_activate_owner(owner3) == False + + license_seats.return_value = 5 + + assert can_activate_owner(owner1) == True + assert can_activate_owner(owner2) == True + assert can_activate_owner(owner3) == True + + @patch("services.self_hosted.can_activate_owner") + def test_activate_owner(self, can_activate_owner): + can_activate_owner.return_value = True + + other_owner = OwnerFactory() + org1 = OwnerFactory(plan_activated_users=[other_owner.pk]) + org2 = OwnerFactory(plan_activated_users=[]) + org3 = OwnerFactory(plan_activated_users=[other_owner.pk]) + owner = OwnerFactory(organizations=[org1.pk, org2.pk]) + + activate_owner(owner) + + org1.refresh_from_db() + assert org1.plan_activated_users == [other_owner.pk, owner.pk] + org2.refresh_from_db() + assert org2.plan_activated_users == [owner.pk] + org3.refresh_from_db() + assert org3.plan_activated_users == [other_owner.pk] + + activate_owner(owner) + + # does not add duplicate entry + org1.refresh_from_db() + assert org1.plan_activated_users == [other_owner.pk, owner.pk] + org2.refresh_from_db() + assert org2.plan_activated_users == [owner.pk] + org3.refresh_from_db() + assert org3.plan_activated_users == [other_owner.pk] + + @patch("services.self_hosted.can_activate_owner") + def test_activate_owner_cannot_activate(self, can_activate_owner): + can_activate_owner.return_value = False + + other_owner = OwnerFactory() + org1 = OwnerFactory(plan_activated_users=[other_owner.pk]) + org2 = OwnerFactory(plan_activated_users=[]) + owner = OwnerFactory(organizations=[org2.pk]) + + with self.assertRaises(LicenseException) as e: + activate_owner(owner) + assert e.message == "no more seats available" + + org1.refresh_from_db() + assert org1.plan_activated_users == [other_owner.pk] + org2.refresh_from_db() + assert org2.plan_activated_users == [] + + def test_deactivate_owner(self): + owner1 = OwnerFactory() + owner2 = OwnerFactory() + org1 = OwnerFactory(plan_activated_users=[owner1.pk, owner2.pk]) + org2 = OwnerFactory(plan_activated_users=[owner1.pk]) + org3 = OwnerFactory(plan_activated_users=[owner2.pk]) + + deactivate_owner(owner1) + + org1.refresh_from_db() + assert org1.plan_activated_users == [owner2.pk] + org2.refresh_from_db() + assert org2.plan_activated_users == [] + org3.refresh_from_db() + assert org3.plan_activated_users == [owner2.pk] + + def test_autoactivation(self): + disable_autoactivation() + + owner1 = OwnerFactory(plan_auto_activate=False) + owner2 = OwnerFactory(plan_auto_activate=False) + assert is_autoactivation_enabled() == False + + owner1.plan_auto_activate = True + owner1.save() + assert is_autoactivation_enabled() == True + + owner2.plan_auto_activate = True + owner2.save() + assert is_autoactivation_enabled() == True + + def test_enable_autoactivation(self): + owner = OwnerFactory(plan_auto_activate=False) + enable_autoactivation() + owner.refresh_from_db() + assert owner.plan_auto_activate == True + + def test_disable_autoactivation(self): + owner = OwnerFactory(plan_auto_activate=True) + disable_autoactivation() + owner.refresh_from_db() + assert owner.plan_auto_activate == False + + +@override_settings(IS_ENTERPRISE=False) +class SelfHostedNonEnterpriseTestCase(TestCase): + def test_activate_owner(self): + org = OwnerFactory(plan_activated_users=[]) + owner = OwnerFactory(organizations=[org.pk]) + + with self.assertRaises(Exception): + activate_owner(owner) + + org.refresh_from_db() + assert org.plan_activated_users == [] + + def test_deactivate_owner(self): + owner1 = OwnerFactory() + owner2 = OwnerFactory() + org1 = OwnerFactory(plan_activated_users=[owner1.pk, owner2.pk]) + org2 = OwnerFactory(plan_activated_users=[owner1.pk]) + org3 = OwnerFactory(plan_activated_users=[owner2.pk]) + + with self.assertRaises(Exception): + deactivate_owner(owner1) + + org1.refresh_from_db() + assert org1.plan_activated_users == [owner1.pk, owner2.pk] + org2.refresh_from_db() + assert org2.plan_activated_users == [owner1.pk] + org3.refresh_from_db() + assert org3.plan_activated_users == [owner2.pk] diff --git a/apps/codecov-api/services/tests/test_sentry.py b/apps/codecov-api/services/tests/test_sentry.py new file mode 100644 index 0000000000..3ffd494002 --- /dev/null +++ b/apps/codecov-api/services/tests/test_sentry.py @@ -0,0 +1,143 @@ +import json +from unittest.mock import MagicMock, patch + +import jwt +from django.test import TestCase, TransactionTestCase, override_settings +from shared.django_apps.core.tests.factories import OwnerFactory + +from services.sentry import ( + SentryInvalidStateError, + SentryState, + SentryUserAlreadyExistsError, + decode_state, + is_sentry_user, + save_sentry_state, + send_user_webhook, +) + + +@override_settings(SENTRY_JWT_SHARED_SECRET="secret") +class DecodeStateTests(TestCase): + def setUp(self): + self.decoded_state = {"user_id": "sentry-user-id", "org_id": "sentry-org-id"} + self.state = jwt.encode(self.decoded_state, "secret", algorithm="HS256") + + def test_decode_state(self): + res = decode_state(self.state) + assert res.data == self.decoded_state + + @override_settings(SENTRY_JWT_SHARED_SECRET="wrong") + def test_decode_state_wrong_secret(self): + res = decode_state(self.state) + assert res is None + + def test_decode_state_malformed(self): + res = decode_state("malformed") + assert res is None + + +class SaveSentryStateTests(TransactionTestCase): + def setUp(self): + self.owner = OwnerFactory() + + self.decode_state_patcher = patch("services.sentry.decode_state") + self.decode_state = self.decode_state_patcher.start() + self.decode_state.return_value = SentryState( + { + "user_id": "sentry-user-id", + "org_id": "sentry-org-id", + } + ) + self.addCleanup(self.decode_state_patcher.stop) + + def test_save_sentry_state(self): + state_mock = MagicMock() + save_sentry_state(self.owner, state_mock) + + self.decode_state.assert_called_once_with(state_mock) + + self.owner.refresh_from_db() + assert self.owner.sentry_user_id == "sentry-user-id" + assert self.owner.sentry_user_data == { + "user_id": "sentry-user-id", + "org_id": "sentry-org-id", + } + + def test_save_sentry_state_invalid_state(self): + self.decode_state.return_value = None + + with self.assertRaises(SentryInvalidStateError): + save_sentry_state(self.owner, MagicMock()) + + self.owner.refresh_from_db() + assert self.owner.sentry_user_id is None + assert self.owner.sentry_user_data is None + + def test_save_sentry_state_duplicate_user_id(self): + OwnerFactory(sentry_user_id="sentry-user-id") + with self.assertRaises(SentryUserAlreadyExistsError): + save_sentry_state(self.owner, MagicMock()) + + self.owner.refresh_from_db() + assert self.owner.sentry_user_id is None + assert self.owner.sentry_user_data is None + + +class IsSentryUserTests(TestCase): + def setUp(self): + self.owner = OwnerFactory() + + def test_owner_has_sentry_user_id(self): + self.owner.sentry_user_id = "testing" + assert is_sentry_user(self.owner) == True + + def test_owner_missing_sentry_user_id(self): + self.owner.sentry_user_id = None + assert is_sentry_user(self.owner) == False + + +@patch("services.task.TaskService.http_request") +class SendWebhookTests(TestCase): + def setUp(self): + self.user = OwnerFactory( + sentry_user_id="sentry-user-id", + sentry_user_data={"user_id": "sentry-user-id", "org_id": "sentry-org-id"}, + ) + self.org = OwnerFactory( + service="github", + service_id="org-service-id", + ) + + @override_settings( + SENTRY_USER_WEBHOOK_URL="https://example.com", SENTRY_JWT_SHARED_SECRET="secret" + ) + def test_webhook(self, http_request): + send_user_webhook(self.user, self.org) + + encoded_state = jwt.encode( + { + "user_id": self.user.sentry_user_id, + "org_id": self.user.sentry_user_data.get("org_id"), + "codecov_owner_id": self.user.pk, + "codecov_organization_id": self.org.pk, + "service": self.org.service, + "service_id": self.org.service_id, + }, + "secret", + algorithm="HS256", + ) + + http_request.assert_called_once_with( + url="https://example.com", + method="POST", + headers={ + "Content-Type": "application/json", + "User-Agent": "Codecov", + }, + data=json.dumps({"state": f"{encoded_state}"}), + ) + + @override_settings(SENTRY_USER_WEBHOOK_URL=None) + def test_webhook_no_url(self, http_request): + send_user_webhook(self.user, self.org) + assert not http_request.called diff --git a/apps/codecov-api/services/tests/test_task.py b/apps/codecov-api/services/tests/test_task.py new file mode 100644 index 0000000000..6f564d8348 --- /dev/null +++ b/apps/codecov-api/services/tests/test_task.py @@ -0,0 +1,324 @@ +from datetime import datetime +from unittest.mock import MagicMock + +import pytest +from django.conf import settings +from freezegun import freeze_time +from shared import celery_config +from shared.django_apps.core.tests.factories import RepositoryFactory + +from services.task import TaskService, celery_app +from timeseries.tests.factories import DatasetFactory + + +def test_refresh_task(mocker): + chain_mock = mocker.patch("services.task.task.chain") + mock_route_task = mocker.patch( + "services.task.task.route_task", return_value={"queue": "my_queue"} + ) + TaskService().refresh(5, "codecov") + chain_mock.assert_called() + mock_route_task.assert_called() + + +@freeze_time("2023-06-13T10:01:01.000123") +def test_compute_comparison_task(mocker): + signature_mock = mocker.patch("services.task.task.signature") + mock_route_task = mocker.patch( + "services.task.task.route_task", return_value={"queue": "my_queue"} + ) + TaskService().compute_comparison(5) + mock_route_task.assert_called_with( + celery_config.compute_comparison_task_name, + args=None, + kwargs=dict(comparison_id=5), + ) + signature_mock.assert_called_with( + celery_config.compute_comparison_task_name, + args=None, + kwargs=dict(comparison_id=5), + app=celery_app, + queue="my_queue", + soft_time_limit=None, + time_limit=None, + headers=dict(created_timestamp="2023-06-13T10:01:01.000123"), + immutable=False, + ) + + +def test_compute_comparisons_task(mocker): + signature_mock = mocker.patch("services.task.task.signature") + mock_route_task = mocker.patch( + "services.task.task.route_task", return_value={"queue": "my_queue"} + ) + apply_async_mock = mocker.patch("celery.group.apply_async") + TaskService().compute_comparisons([5, 10]) + assert mock_route_task.call_count == 1 + mock_route_task.assert_called_with( + celery_config.compute_comparison_task_name, + args=None, + kwargs=dict(comparison_id=5), + ) + assert signature_mock.call_count == 2 + signature_mock.assert_any_call( + celery_config.compute_comparison_task_name, + args=None, + kwargs=dict(comparison_id=10), + app=celery_app, + queue="my_queue", + soft_time_limit=None, + time_limit=None, + ) + signature_mock.assert_any_call( + celery_config.compute_comparison_task_name, + args=None, + kwargs=dict(comparison_id=5), + app=celery_app, + queue="my_queue", + soft_time_limit=None, + time_limit=None, + ) + apply_async_mock.assert_called_once_with() + + +@pytest.mark.skipif( + not settings.TIMESERIES_ENABLED, reason="requires timeseries data storage" +) +@pytest.mark.django_db(databases={"default", "timeseries"}) +@freeze_time("2023-06-13T10:01:01.000123") +def test_backfill_repo(mocker): + signature_mock = mocker.patch("services.task.task.signature") + mock_route_task = mocker.patch( + "services.task.task.route_task", return_value={"queue": "celery"} + ) + apply_async_mock = mocker.patch("celery.group.apply_async") + + repo = RepositoryFactory() + TaskService().backfill_repo( + repo, + start_date=datetime(2022, 1, 1), + end_date=datetime(2022, 1, 25), + dataset_names=["testing"], + ) + + assert signature_mock.call_count == 3 + assert mock_route_task.call_count == 3 + mock_route_task.assert_any_call( + celery_config.timeseries_backfill_task_name, + args=None, + kwargs=dict( + repoid=repo.pk, + start_date="2022-01-15T00:00:00", + end_date="2022-01-25T00:00:00", + dataset_names=["testing"], + ), + ) + + signature_mock.assert_any_call( + celery_config.timeseries_backfill_task_name, + args=None, + kwargs=dict( + repoid=repo.pk, + start_date="2022-01-15T00:00:00", + end_date="2022-01-25T00:00:00", + dataset_names=["testing"], + ), + app=celery_app, + queue="celery", + soft_time_limit=None, + time_limit=None, + headers=dict(created_timestamp="2023-06-13T10:01:01.000123"), + immutable=False, + ) + signature_mock.assert_any_call( + celery_config.timeseries_backfill_task_name, + args=None, + kwargs=dict( + repoid=repo.pk, + start_date="2022-01-05T00:00:00", + end_date="2022-01-15T00:00:00", + dataset_names=["testing"], + ), + app=celery_app, + queue="celery", + soft_time_limit=None, + time_limit=None, + headers=dict(created_timestamp="2023-06-13T10:01:01.000123"), + immutable=False, + ) + signature_mock.assert_any_call( + celery_config.timeseries_backfill_task_name, + args=None, + kwargs=dict( + repoid=repo.pk, + start_date="2022-01-01T00:00:00", + end_date="2022-01-05T00:00:00", + dataset_names=["testing"], + ), + app=celery_app, + queue="celery", + soft_time_limit=None, + time_limit=None, + headers=dict(created_timestamp="2023-06-13T10:01:01.000123"), + immutable=False, + ) + + apply_async_mock.assert_called_once_with() + + +@pytest.mark.skipif( + not settings.TIMESERIES_ENABLED, reason="requires timeseries data storage" +) +@pytest.mark.django_db(databases={"default", "timeseries"}) +@freeze_time("2023-06-13T10:01:01.000123") +def test_backfill_dataset(mocker): + signature_mock = mocker.patch("services.task.task.signature") + mocker.patch("services.task.task.route_task", return_value={"queue": "celery"}) + signature = MagicMock() + signature_mock.return_value = signature + + dataset = DatasetFactory() + TaskService().backfill_dataset( + dataset, + start_date=datetime(2022, 1, 1), + end_date=datetime(2022, 8, 9), + ) + + signature_mock.assert_called_with( + "app.tasks.timeseries.backfill_dataset", + args=None, + kwargs=dict( + dataset_id=dataset.pk, + start_date="2022-01-01T00:00:00", + end_date="2022-08-09T00:00:00", + ), + app=celery_app, + queue="celery", + soft_time_limit=None, + time_limit=None, + headers=dict(created_timestamp="2023-06-13T10:01:01.000123"), + immutable=False, + ) + signature.apply_async.assert_called_once_with() + + +@freeze_time("2023-06-13T10:01:01.000123") +def test_timeseries_delete(mocker): + signature_mock = mocker.patch("services.task.task.signature") + mock_route_task = mocker.patch( + "services.task.task.route_task", return_value={"queue": "celery"} + ) + TaskService().delete_timeseries(repository_id=12345) + mock_route_task.assert_called_with( + celery_config.timeseries_delete_task_name, + args=None, + kwargs=dict(repository_id=12345), + ) + signature_mock.assert_called_with( + celery_config.timeseries_delete_task_name, + args=None, + kwargs=dict(repository_id=12345), + app=celery_app, + queue="celery", + soft_time_limit=None, + time_limit=None, + headers=dict(created_timestamp="2023-06-13T10:01:01.000123"), + immutable=False, + ) + + +@freeze_time("2023-06-13T10:01:01.000123") +def test_flush_repo(mocker): + signature_mock = mocker.patch("services.task.task.signature") + mock_route_task = mocker.patch( + "services.task.task.route_task", return_value={"queue": "celery"} + ) + TaskService().flush_repo(repository_id=12345) + mock_route_task.assert_called_with( + "app.tasks.flush_repo.FlushRepo", + args=None, + kwargs=dict(repoid=12345), + ) + signature_mock.assert_called_with( + "app.tasks.flush_repo.FlushRepo", + args=None, + kwargs=dict(repoid=12345), + app=celery_app, + queue="celery", + soft_time_limit=None, + time_limit=None, + headers=dict(created_timestamp="2023-06-13T10:01:01.000123"), + immutable=False, + ) + + +@freeze_time("2023-06-13T10:01:01.000123") +def test_update_commit_task(mocker): + signature_mock = mocker.patch("services.task.task.signature") + mock_route_task = mocker.patch( + "services.task.task.route_task", + return_value={ + "queue": "celery", + "extra_config": {"soft_timelimit": 300, "hard_timelimit": 400}, + }, + ) + TaskService().update_commit(1, 2) + mock_route_task.assert_called_with( + celery_config.commit_update_task_name, + args=None, + kwargs=dict(commitid=1, repoid=2), + ) + signature_mock.assert_called_with( + celery_config.commit_update_task_name, + args=None, + kwargs=dict(commitid=1, repoid=2), + app=celery_app, + queue="celery", + soft_time_limit=300, + time_limit=400, + headers=dict(created_timestamp="2023-06-13T10:01:01.000123"), + immutable=False, + ) + + +@freeze_time("2023-06-13T10:01:01.000123") +def test_make_http_request_task(mocker): + signature_mock = mocker.patch("services.task.task.signature") + mock_route_task = mocker.patch( + "services.task.task.route_task", return_value={"queue": "celery"} + ) + TaskService().http_request( + url="http://example.com", + method="POST", + headers={"Content-Type": "text/plain"}, + data="test body", + timeout=10, + ) + mock_route_task.assert_called_with( + "app.tasks.http_request.HTTPRequest", + args=None, + kwargs=dict( + url="http://example.com", + method="POST", + headers={"Content-Type": "text/plain"}, + data="test body", + timeout=10, + ), + ) + signature_mock.assert_called_with( + "app.tasks.http_request.HTTPRequest", + args=None, + kwargs=dict( + url="http://example.com", + method="POST", + headers={"Content-Type": "text/plain"}, + data="test body", + timeout=10, + ), + app=celery_app, + queue="celery", + soft_time_limit=None, + time_limit=None, + headers=dict(created_timestamp="2023-06-13T10:01:01.000123"), + immutable=False, + ) diff --git a/apps/codecov-api/services/tests/test_task_router.py b/apps/codecov-api/services/tests/test_task_router.py new file mode 100644 index 0000000000..df4accab36 --- /dev/null +++ b/apps/codecov-api/services/tests/test_task_router.py @@ -0,0 +1,200 @@ +import pytest +import shared.celery_config as shared_celery_config +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName + +from compare.tests.factories import CommitComparisonFactory +from labelanalysis.tests.factories import LabelAnalysisRequestFactory +from services.task.task_router import ( + _get_user_plan_from_comparison_id, + _get_user_plan_from_label_request_id, + _get_user_plan_from_ownerid, + _get_user_plan_from_repoid, + _get_user_plan_from_suite_id, + _get_user_plan_from_task, + route_task, +) +from staticanalysis.tests.factories import StaticAnalysisSuiteFactory + + +@pytest.fixture +def fake_owners(db): + owner = OwnerFactory.create(plan=PlanName.CODECOV_PRO_MONTHLY.value) + owner_enterprise_cloud = OwnerFactory.create( + plan=PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + owner.save() + owner_enterprise_cloud.save() + return (owner, owner_enterprise_cloud) + + +@pytest.fixture +def fake_repos(db, fake_owners): + (owner, owner_enterprise_cloud) = fake_owners + + repo = RepositoryFactory(author=owner) + repo_enterprise = RepositoryFactory(author=owner_enterprise_cloud) + repo.save() + repo_enterprise.save() + return (repo, repo_enterprise) + + +@pytest.fixture +def fake_compare_commit(db, fake_repos): + (repo, repo_enterprise) = fake_repos + compare_commit = CommitComparisonFactory(compare_commit__repository=repo) + compare_commit_enterprise = CommitComparisonFactory( + compare_commit__repository=repo_enterprise + ) + compare_commit.save() + compare_commit_enterprise.save() + return (compare_commit, compare_commit_enterprise) + + +@pytest.fixture +def fake_label_analysis_request(db, fake_repos): + (repo, repo_enterprise_cloud) = fake_repos + label_analysis_request = LabelAnalysisRequestFactory(head_commit__repository=repo) + label_analysis_request_enterprise = LabelAnalysisRequestFactory( + head_commit__repository=repo_enterprise_cloud + ) + label_analysis_request.save() + label_analysis_request_enterprise.save() + return (label_analysis_request, label_analysis_request_enterprise) + + +@pytest.fixture +def fake_static_analysis_suite(db, fake_repos): + (repo, repo_enterprise_cloud) = fake_repos + static_analysis_suite = StaticAnalysisSuiteFactory(commit__repository=repo) + static_analysis_suite_enterprise = StaticAnalysisSuiteFactory( + commit__repository=repo_enterprise_cloud + ) + static_analysis_suite.save() + static_analysis_suite_enterprise.save() + return (static_analysis_suite, static_analysis_suite_enterprise) + + +def test_get_owner_plan_from_ownerid(fake_owners): + (owner, owner_enterprise_cloud) = fake_owners + assert ( + _get_user_plan_from_ownerid(owner.ownerid) == PlanName.CODECOV_PRO_MONTHLY.value + ) + assert ( + _get_user_plan_from_ownerid(owner_enterprise_cloud.ownerid) + == PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + assert _get_user_plan_from_ownerid(10000000) == DEFAULT_FREE_PLAN + + +def test_get_owner_plan_from_repoid(fake_repos): + (repo, repo_enterprise) = fake_repos + assert _get_user_plan_from_repoid(repo.repoid) == PlanName.CODECOV_PRO_MONTHLY.value + assert ( + _get_user_plan_from_repoid(repo_enterprise.repoid) + == PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + assert _get_user_plan_from_repoid(10000000) == DEFAULT_FREE_PLAN + + +def test_get_user_plan_from_comparison_id(fake_compare_commit): + (compare_commit, compare_commit_enterprise) = fake_compare_commit + assert ( + _get_user_plan_from_comparison_id(compare_commit.id) + == PlanName.CODECOV_PRO_MONTHLY.value + ) + assert ( + _get_user_plan_from_comparison_id(compare_commit_enterprise.id) + == PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + assert _get_user_plan_from_comparison_id(10000000) == DEFAULT_FREE_PLAN + + +def test_get_user_plan_from_label_request_id(fake_label_analysis_request): + ( + label_analysis_request, + label_analysis_request_enterprise, + ) = fake_label_analysis_request + assert ( + _get_user_plan_from_label_request_id(request_id=label_analysis_request.id) + == PlanName.CODECOV_PRO_MONTHLY.value + ) + assert ( + _get_user_plan_from_label_request_id( + request_id=label_analysis_request_enterprise.id + ) + == PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + assert _get_user_plan_from_label_request_id(10000000) == DEFAULT_FREE_PLAN + + +def test_get_user_plan_from_static_analysis_suite(fake_static_analysis_suite): + ( + static_analysis_suite, + static_analysis_suite_enterprise, + ) = fake_static_analysis_suite + assert ( + _get_user_plan_from_suite_id(suite_id=static_analysis_suite.id) + == PlanName.CODECOV_PRO_MONTHLY.value + ) + assert ( + _get_user_plan_from_suite_id(suite_id=static_analysis_suite_enterprise.id) + == PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + assert _get_user_plan_from_suite_id(10000000) == DEFAULT_FREE_PLAN + + +def test_get_user_plan_from_task( + fake_repos, + fake_compare_commit, +): + (repo, repo_enterprise_cloud) = fake_repos + compare_commit = fake_compare_commit[0] + task_kwargs = dict(repoid=repo.repoid, commitid=0, debug=False, rebuild=False) + assert ( + _get_user_plan_from_task(shared_celery_config.upload_task_name, task_kwargs) + == PlanName.CODECOV_PRO_MONTHLY.value + ) + + task_kwargs = dict( + repoid=repo_enterprise_cloud.repoid, commitid=0, debug=False, rebuild=False + ) + assert ( + _get_user_plan_from_task(shared_celery_config.upload_task_name, task_kwargs) + == PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + + task_kwargs = dict(ownerid=repo.author.ownerid) + assert ( + _get_user_plan_from_task( + shared_celery_config.delete_owner_task_name, task_kwargs + ) + == PlanName.CODECOV_PRO_MONTHLY.value + ) + + task_kwargs = dict(comparison_id=compare_commit.id) + assert ( + _get_user_plan_from_task( + shared_celery_config.compute_comparison_task_name, task_kwargs + ) + == PlanName.CODECOV_PRO_MONTHLY.value + ) + + task_kwargs = dict( + repoid=repo_enterprise_cloud.repoid, commitid=0, debug=False, rebuild=False + ) + assert _get_user_plan_from_task("unknown task", task_kwargs) == DEFAULT_FREE_PLAN + + +def test_route_task(mocker, fake_repos): + mock_route_tasks_shared = mocker.patch( + "services.task.task_router.route_tasks_based_on_user_plan" + ) + mock_route_tasks_shared.return_value = {"queue": "correct queue"} + repo = fake_repos[0] + task_kwargs = dict(repoid=repo.repoid, commitid=0, debug=False, rebuild=False) + response = route_task(shared_celery_config.upload_task_name, [], task_kwargs, {}) + assert response == {"queue": "correct queue"} + mock_route_tasks_shared.assert_called_with( + shared_celery_config.upload_task_name, PlanName.CODECOV_PRO_MONTHLY.value + ) diff --git a/apps/codecov-api/services/tests/test_yaml.py b/apps/codecov-api/services/tests/test_yaml.py new file mode 100644 index 0000000000..4bdf184cd5 --- /dev/null +++ b/apps/codecov-api/services/tests/test_yaml.py @@ -0,0 +1,73 @@ +from unittest.mock import patch + +import pytest +from django.test import TestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.torngit.exceptions import TorngitObjectNotFoundError + +import services.yaml as yaml + + +class YamlServiceTest(TestCase): + def setUp(self): + self.org = OwnerFactory() + self.repo = RepositoryFactory(author=self.org, private=False) + self.commit = CommitFactory(repository=self.repo) + + @patch("services.yaml.fetch_current_yaml_from_provider_via_reference") + def test_when_commit_has_yaml(self, mock_fetch_yaml): + mock_fetch_yaml.return_value = """ + codecov: + notify: + require_ci_to_pass: no + """ + config = yaml.final_commit_yaml(self.commit, None) + assert config["codecov"]["require_ci_to_pass"] is False + + @patch("services.yaml.fetch_current_yaml_from_provider_via_reference") + def test_when_commit_has_no_yaml(self, mock_fetch_yaml): + mock_fetch_yaml.side_effect = TorngitObjectNotFoundError( + response_data=404, message="not found" + ) + config = yaml.final_commit_yaml(self.commit, None) + assert config["codecov"]["require_ci_to_pass"] is True + + @patch("services.yaml.fetch_current_yaml_from_provider_via_reference") + def test_when_commit_has_yaml_with_wrongly_typed_owner_arg(self, mock_fetch_yaml): + mock_fetch_yaml.return_value = """ + codecov: + notify: + require_ci_to_pass: no + """ + with pytest.raises(TypeError) as exc_info: + yaml.final_commit_yaml(self.commit, "something else") + assert ( + str(exc_info.value) + == "fetch_commit_yaml owner arg must be Owner or None. Provided: " + ) + + @patch("services.yaml.fetch_current_yaml_from_provider_via_reference") + def test_when_commit_has_yaml_with_owner(self, mock_fetch_yaml): + mock_fetch_yaml.return_value = """ + codecov: + notify: + require_ci_to_pass: no + """ + config = yaml.final_commit_yaml(self.commit, self.org) + assert config["codecov"]["require_ci_to_pass"] is False + + @patch("services.yaml.fetch_current_yaml_from_provider_via_reference") + def test_when_commit_has_reserved_to_string_key(self, mock_fetch_yaml): + mock_fetch_yaml.return_value = """ + codecov: + notify: + require_ci_to_pass: no + to_string: hello + """ + config = yaml.final_commit_yaml(self.commit, self.org) + assert config.get("to_string") is None + assert "to_string" not in config.to_dict() diff --git a/apps/codecov-api/services/yaml.py b/apps/codecov-api/services/yaml.py new file mode 100644 index 0000000000..1bb458444c --- /dev/null +++ b/apps/codecov-api/services/yaml.py @@ -0,0 +1,77 @@ +import enum +import logging +from functools import lru_cache + +import sentry_sdk +from asgiref.sync import async_to_sync +from shared.helpers.cache import cache +from shared.yaml import UserYaml, fetch_current_yaml_from_provider_via_reference +from shared.yaml.validation import validate_yaml +from yaml import safe_load + +from codecov_auth.models import Owner, get_config +from core.models import Commit +from services.repo_providers import RepoProviderService + + +class YamlStates(enum.Enum): + DEFAULT = "default" + + +log = logging.getLogger(__name__) + + +@cache.cache_function(ttl=60 * 60) +def fetch_commit_yaml(commit: Commit, owner: Owner | None) -> dict | None: + """ + Fetches the codecov.yaml file for a particular commit from the service provider. + Service provider API request is made on behalf of the given `owner`. + """ + + # There's been a lot of instances where this function is called where the owner arg is not Owner type + # Previously this issue is masked by the catch all exception handling. Most badly typed calls have + # been addressed, but there might still be some entrypoints unaccounted for, will fix new discoveries + # from this assertion being raised. + if owner is not None and not isinstance(owner, Owner): + raise TypeError( + f"fetch_commit_yaml owner arg must be Owner or None. Provided: {type(owner)}" + ) + + try: + repository_service = RepoProviderService().get_adapter( + owner=owner, repo=commit.repository + ) + yaml_str = async_to_sync(fetch_current_yaml_from_provider_via_reference)( + commit.commitid, repository_service + ) + yaml_dict = safe_load(yaml_str) + return validate_yaml(yaml_dict, show_secrets_for=None) + except Exception as e: + # fetching, parsing, validating the yaml inside the commit can + # have various exceptions, which we do not care about to get the final + # yaml used for a commit, as any error here, the codecov.yaml would not + # be used, so we return None here + + log.warning( + f"Was not able to fetch yaml file for commit. Ignoring error and returning None. Exception: {e}", + extra={ + "commit_id": commit.commitid, + "owner_arg": type(owner), + }, + ) + return None + + +@lru_cache() +@sentry_sdk.trace +def final_commit_yaml(commit: Commit, owner: Owner | None) -> UserYaml: + return UserYaml.get_final_yaml( + owner_yaml=commit.repository.author.yaml, + repo_yaml=commit.repository.yaml, + commit_yaml=fetch_commit_yaml(commit, owner), + ) + + +def get_yaml_state(yaml: UserYaml) -> YamlStates | None: + if yaml == get_config("site", default={}): + return YamlStates.DEFAULT diff --git a/apps/codecov-api/staging.sh b/apps/codecov-api/staging.sh new file mode 100644 index 0000000000..cef17032e6 --- /dev/null +++ b/apps/codecov-api/staging.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# starts the development server using gunicorn +# NEVER run production with the --reload option command +echo "Starting gunicorn in dev mode" +export PYTHONWARNINGS=always +prefix="" +if [ -f "/usr/local/bin/berglas" ]; then + prefix="berglas exec --" +fi + +if [ "$GUNICORN_WORKERS" -gt 1 ]; +then + export PROMETHEUS_MULTIPROC_DIR="${PROMETHEUS_MULTIPROC_DIR:-$HOME/.prometheus}" + rm -r ${PROMETHEUS_MULTIPROC_DIR?}/* 2> /dev/null + mkdir -p "$PROMETHEUS_MULTIPROC_DIR" +fi + +$prefix gunicorn codecov.wsgi:application --reload --workers=${GUNICORN_WORKERS:-2} --threads=${GUNICORN_THREADS:-1} --worker-connections=${GUNICORN_WORKER_CONNECTIONS:-1000} --bind 0.0.0.0:8000 --access-logfile '-' --timeout "${GUNICORN_TIMEOUT:-600}" --disable-redirect-access-to-syslog --config=gunicorn.conf.py diff --git a/apps/codecov-api/staticanalysis/__init__.py b/apps/codecov-api/staticanalysis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/staticanalysis/admin.py b/apps/codecov-api/staticanalysis/admin.py new file mode 100644 index 0000000000..846f6b4061 --- /dev/null +++ b/apps/codecov-api/staticanalysis/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/apps/codecov-api/staticanalysis/apps.py b/apps/codecov-api/staticanalysis/apps.py new file mode 100644 index 0000000000..338c1f7f04 --- /dev/null +++ b/apps/codecov-api/staticanalysis/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class StaticanalysisConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "staticanalysis" diff --git a/apps/codecov-api/staticanalysis/models.py b/apps/codecov-api/staticanalysis/models.py new file mode 100644 index 0000000000..2fc17ec3bb --- /dev/null +++ b/apps/codecov-api/staticanalysis/models.py @@ -0,0 +1 @@ +from shared.django_apps.staticanalysis.models import * diff --git a/apps/codecov-api/staticanalysis/serializers.py b/apps/codecov-api/staticanalysis/serializers.py new file mode 100644 index 0000000000..d7056ebaee --- /dev/null +++ b/apps/codecov-api/staticanalysis/serializers.py @@ -0,0 +1,156 @@ +import logging +import math + +from rest_framework import exceptions, serializers +from shared.api_archive.archive import ArchiveService, MinioEndpoints + +from core.models import Commit +from staticanalysis.models import ( + StaticAnalysisSingleFileSnapshot, + StaticAnalysisSingleFileSnapshotState, + StaticAnalysisSuite, + StaticAnalysisSuiteFilepath, +) + +log = logging.getLogger(__name__) + + +class CommitFromShaSerializerField(serializers.Field): + def to_representation(self, commit): + return commit.commitid + + def to_internal_value(self, commit_sha): + # TODO: Change this query when we change how we fetch URLs + commit = Commit.objects.filter( + repository__in=self.context["request"].auth.get_repositories(), + commitid=commit_sha, + ).first() + if commit is None: + raise exceptions.NotFound("Commit not found.") + return commit + + +def _dict_to_suite_filepath( + analysis_suite, + repository, + archive_service, + existing_file_snapshots_mapping, + file_dict, +): + if file_dict["file_hash"] in existing_file_snapshots_mapping: + db_element = existing_file_snapshots_mapping[file_dict["file_hash"]] + was_created = False + else: + path = MinioEndpoints.static_analysis_single_file.get_path( + version="v4", + repo_hash=archive_service.storage_hash, + location=f"{file_dict['file_hash']}.json", + ) + # Using get or create in the case the object was already + # created somewhere else first, but also because get_or_create + # is internally get_or_create_or_get, so Django handles the conflicts + # that can arise on race conditions on the create step + # We might choose to change it if the number of extra GETs become too much + ( + db_element, + was_created, + ) = StaticAnalysisSingleFileSnapshot.objects.get_or_create( + file_hash=file_dict["file_hash"], + repository=repository, + defaults=dict( + state_id=StaticAnalysisSingleFileSnapshotState.CREATED.db_id, + content_location=path, + ), + ) + if was_created: + log.debug( + "Created new snapshot for repository", + extra=dict(repoid=repository.repoid, snapshot_id=db_element.id), + ) + return StaticAnalysisSuiteFilepath( + filepath=file_dict["filepath"], + file_snapshot=db_element, + analysis_suite=analysis_suite, + ) + + +class StaticAnalysisSuiteFilepathField(serializers.ModelSerializer): + file_hash = serializers.UUIDField() + raw_upload_location = serializers.SerializerMethodField() + state = serializers.SerializerMethodField() + + class Meta: + model = StaticAnalysisSuiteFilepath + fields = [ + "filepath", + "file_hash", + "raw_upload_location", + "state", + ] + + def get_state(self, obj): + return StaticAnalysisSingleFileSnapshotState.enum_from_int( + obj.file_snapshot.state_id + ).name + + def get_raw_upload_location(self, obj): + # TODO: This has a built-in ttl of 10 seconds. + # We have to consider changing it in case customers are doing a few + # thousand uploads on the first time + return self.context["archive_service"].create_presigned_put( + obj.file_snapshot.content_location + ) + + +class FilepathListField(serializers.ListField): + child = StaticAnalysisSuiteFilepathField() + + def to_representation(self, data): + data = data.select_related( + "file_snapshot", + ).all() + return super().to_representation(data) + + +class StaticAnalysisSuiteSerializer(serializers.ModelSerializer): + commit = CommitFromShaSerializerField(required=True) + filepaths = FilepathListField() + + class Meta: + model = StaticAnalysisSuite + fields = ["external_id", "commit", "filepaths"] + read_only_fields = ["raw_upload_location", "external_id"] + + def create(self, validated_data): + file_metadata_array = validated_data.pop("filepaths") + # `validated_data` only contains `commit` after pop + obj = StaticAnalysisSuite.objects.create(**validated_data) + request = self.context["request"] + repository = request.auth.get_repositories()[0] + archive_service = ArchiveService(repository) + # allow 1s per 10 uploads + ttl = max(math.ceil(len(file_metadata_array) / 10) + 5, 10) + self.context["archive_service"] = ArchiveService(repository, ttl=ttl) + all_hashes = [val["file_hash"] for val in file_metadata_array] + existing_values = StaticAnalysisSingleFileSnapshot.objects.filter( + repository=repository, file_hash__in=all_hashes + ) + existing_values_mapping = {val.file_hash: val for val in existing_values} + created_filepaths = [ + _dict_to_suite_filepath( + obj, + repository, + archive_service, + existing_values_mapping, + file_dict, + ) + for file_dict in file_metadata_array + ] + StaticAnalysisSuiteFilepath.objects.bulk_create(created_filepaths) + log.info( + "Created static analysis filepaths", + extra=dict( + created_ids=[f.id for f in created_filepaths], repoid=repository.repoid + ), + ) + return obj diff --git a/apps/codecov-api/staticanalysis/tests.py b/apps/codecov-api/staticanalysis/tests.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/staticanalysis/tests/__init__.py b/apps/codecov-api/staticanalysis/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/staticanalysis/tests/factories.py b/apps/codecov-api/staticanalysis/tests/factories.py new file mode 100644 index 0000000000..b1ebc3645b --- /dev/null +++ b/apps/codecov-api/staticanalysis/tests/factories.py @@ -0,0 +1 @@ +from shared.django_apps.staticanalysis.tests.factories import * diff --git a/apps/codecov-api/staticanalysis/tests/test_views.py b/apps/codecov-api/staticanalysis/tests/test_views.py new file mode 100644 index 0000000000..7e04738df1 --- /dev/null +++ b/apps/codecov-api/staticanalysis/tests/test_views.py @@ -0,0 +1,101 @@ +from uuid import uuid4 + +from django.urls import reverse +from rest_framework.test import APIClient +from shared.celery_config import static_analysis_task_name +from shared.django_apps.core.tests.factories import ( + CommitFactory, + RepositoryTokenFactory, +) + +from services.task import TaskService +from staticanalysis.models import StaticAnalysisSuite +from staticanalysis.tests.factories import StaticAnalysisSuiteFactory + + +def test_simple_static_analysis_call_no_uploads_yet(db, mocker): + mocked_task_service = mocker.patch.object(TaskService, "schedule_task") + mocked_presigned_put = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="banana.txt", + ) + commit = CommitFactory.create(repository__active=True) + token = RepositoryTokenFactory.create( + repository=commit.repository, token_type="static_analysis" + ) + client = APIClient() + url = reverse("staticanalyses-list") + client.credentials(HTTP_AUTHORIZATION="repotoken " + token.key) + some_uuid, second_uuid = uuid4(), uuid4() + response = client.post( + url, + { + "commit": commit.commitid, + "filepaths": [ + { + "filepath": "path/to/a.py", + "file_hash": some_uuid, + }, + { + "filepath": "banana.cpp", + "file_hash": second_uuid, + }, + ], + }, + format="json", + ) + assert response.status_code == 201 + assert StaticAnalysisSuite.objects.filter(commit=commit).count() == 1 + produced_object = StaticAnalysisSuite.objects.filter(commit=commit).get() + response_json = response.json() + assert "filepaths" in response_json + # Popping and sorting because the order doesn't matter, as long as all are there + assert sorted(response_json.pop("filepaths"), key=lambda x: x["filepath"]) == [ + { + "filepath": "banana.cpp", + "file_hash": str(second_uuid), + "raw_upload_location": "banana.txt", + "state": "CREATED", + }, + { + "filepath": "path/to/a.py", + "file_hash": str(some_uuid), + "raw_upload_location": "banana.txt", + "state": "CREATED", + }, + ] + # Now asserting the remaining of the response + assert response_json == { + "external_id": str(produced_object.external_id), + "commit": commit.commitid, + } + mocked_task_service.assert_called_with( + static_analysis_task_name, + kwargs={"suite_id": produced_object.id}, + apply_async_kwargs={"countdown": 10}, + ) + mocked_presigned_put.assert_called_with( + "archive", + mocker.ANY, + 10, + ) + + +def test_static_analysis_finish(db, mocker): + mocked_task_service = mocker.patch.object(TaskService, "schedule_task") + commit = CommitFactory.create(repository__active=True) + suite = StaticAnalysisSuiteFactory(commit=commit) + token = RepositoryTokenFactory.create( + repository=commit.repository, token_type="static_analysis" + ) + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="repotoken " + token.key) + response = client.post( + reverse("staticanalyses-finish", kwargs={"external_id": suite.external_id}) + ) + assert response.status_code == 204 + mocked_task_service.assert_called_with( + static_analysis_task_name, + kwargs={"suite_id": suite.id}, + apply_async_kwargs={}, + ) diff --git a/apps/codecov-api/staticanalysis/tests/unit/__init__.py b/apps/codecov-api/staticanalysis/tests/unit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/staticanalysis/tests/unit/test_serializers.py b/apps/codecov-api/staticanalysis/tests/unit/test_serializers.py new file mode 100644 index 0000000000..4676ec8a89 --- /dev/null +++ b/apps/codecov-api/staticanalysis/tests/unit/test_serializers.py @@ -0,0 +1,331 @@ +import re +from uuid import UUID, uuid4 + +import pytest +from rest_framework.exceptions import NotFound, ValidationError +from shared.api_archive.archive import ArchiveService +from shared.django_apps.core.tests.factories import CommitFactory, RepositoryFactory + +from staticanalysis.models import ( + StaticAnalysisSingleFileSnapshotState, + StaticAnalysisSuite, + StaticAnalysisSuiteFilepath, +) +from staticanalysis.serializers import ( + CommitFromShaSerializerField, + StaticAnalysisSuiteFilepathField, + StaticAnalysisSuiteSerializer, +) +from staticanalysis.tests.factories import ( + StaticAnalysisSingleFileSnapshotFactory, + StaticAnalysisSuiteFilepathFactory, +) + +expected_location_regex = re.compile( + "v4/repos/[A-F0-9]{32}/static_analysis/files/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}.json" +) + + +def test_commit_from_sha_serializer_field_to_internal_value(mocker, db): + commit = CommitFactory.create() + # notice this second commit has a different repo by default + second_commit = CommitFactory.create() + commit.save() + second_commit.save() + fake_request = mocker.MagicMock( + auth=mocker.MagicMock( + get_repositories=mocker.MagicMock(return_value=[commit.repository]) + ) + ) + # silly workaround to not have to manually bind serializers + mocker.patch.object( + CommitFromShaSerializerField, "context", {"request": fake_request} + ) + serializer_field = CommitFromShaSerializerField() + with pytest.raises(NotFound): + assert serializer_field.to_internal_value("abcde" * 8) + with pytest.raises(NotFound): + assert serializer_field.to_internal_value(second_commit.commitid) + assert serializer_field.to_internal_value(commit.commitid) == commit + + +def test_filepath_field(db, mocker): + sasfs = StaticAnalysisSingleFileSnapshotFactory.create( + state_id=StaticAnalysisSingleFileSnapshotState.VALID.db_id + ) + sasfs.save() + fp = StaticAnalysisSuiteFilepathFactory.create( + filepath="ohoooo", file_snapshot=sasfs + ) + fp.analysis_suite.save() + fp.save() + fake_archive_service = mocker.MagicMock( + create_presigned_put=mocker.MagicMock(return_value="some_url_stuff") + ) + serializer_field = StaticAnalysisSuiteFilepathField( + context={ + "archive_service": fake_archive_service, + } + ) + assert dict(serializer_field.to_representation(fp)) == { + "file_hash": sasfs.file_hash, + "filepath": "ohoooo", + "raw_upload_location": "some_url_stuff", + "state": "VALID", + } + + +class TestStaticAnalysisSuiteSerializer(object): + def test_to_internal_value_missing_filepaths(self, mocker, db): + commit = CommitFactory.create() + commit.save() + input_data = {"commit": commit.commitid} + fake_request = mocker.MagicMock( + auth=mocker.MagicMock( + get_repositories=mocker.MagicMock(return_value=[commit.repository]) + ) + ) + serializer = StaticAnalysisSuiteSerializer(context={"request": fake_request}) + with pytest.raises(ValidationError) as exc: + serializer.to_internal_value(input_data) + assert exc.value.detail == {"filepaths": ["This field is required."]} + + def test_to_internal_value_complete(self, mocker, db): + commit = CommitFactory.create() + commit.save() + input_data = { + "commit": commit.commitid, + "filepaths": [ + { + "filepath": "path/to/a.py", + "file_hash": "c8c23bea-c383-4abf-8a7e-6b9cadbeb5b2", + }, + { + "filepath": "anothersomething", + "file_hash": "3998813e-60db-4686-be84-1a0efa7d9b9f", + }, + ], + } + fake_request = mocker.MagicMock( + auth=mocker.MagicMock( + get_repositories=mocker.MagicMock(return_value=[commit.repository]) + ) + ) + serializer = StaticAnalysisSuiteSerializer(context={"request": fake_request}) + res = serializer.to_internal_value(input_data) + assert res == { + "commit": commit, + "filepaths": [ + { + "filepath": "path/to/a.py", + "file_hash": UUID("c8c23bea-c383-4abf-8a7e-6b9cadbeb5b2"), + }, + { + "filepath": "anothersomething", + "file_hash": UUID("3998813e-60db-4686-be84-1a0efa7d9b9f"), + }, + ], + } + + def test_create_no_data_previously_exists(self, mocker, db): + first_repository = RepositoryFactory.create() + commit = CommitFactory.create(repository=first_repository) + commit.save() + validated_data = { + "commit": commit, + "filepaths": [ + { + "filepath": "path/to/a.py", + "file_hash": UUID("c8c23bea-c383-4abf-8a7e-6b9cadbeb5b2"), + }, + { + "filepath": "anothersomething", + "file_hash": UUID("3998813e-60db-4686-be84-1a0efa7d9b9f"), + }, + ], + } + fake_request = mocker.MagicMock( + auth=mocker.MagicMock( + get_repositories=mocker.MagicMock(return_value=[commit.repository]) + ) + ) + serializer = StaticAnalysisSuiteSerializer(context={"request": fake_request}) + res = serializer.create(validated_data) + assert isinstance(res, StaticAnalysisSuite) + assert res.commit == commit + assert res.filepaths.count() == 2 + first_filepath, second_filepath = sorted( + res.filepaths.all(), key=lambda x: x.filepath + ) + assert isinstance(first_filepath, StaticAnalysisSuiteFilepath) + assert first_filepath.filepath == "anothersomething" + assert first_filepath.file_snapshot is not None + archive_hash = ArchiveService.get_archive_hash(commit.repository) + assert first_filepath.file_snapshot.repository == commit.repository + assert first_filepath.file_snapshot.file_hash == UUID( + "3998813e-60db-4686-be84-1a0efa7d9b9f" + ) + assert archive_hash in first_filepath.file_snapshot.content_location + assert ( + expected_location_regex.match(first_filepath.file_snapshot.content_location) + is not None + ) + assert ( + first_filepath.file_snapshot.state_id + == StaticAnalysisSingleFileSnapshotState.CREATED.db_id + ) + + def test_create_some_data_previously_exists(self, mocker, db): + first_repository = RepositoryFactory.create() + second_repository = RepositoryFactory.create() + commit = CommitFactory.create(repository=first_repository) + first_repo_first_snapshot = StaticAnalysisSingleFileSnapshotFactory.create( + file_hash=UUID("c8c23bea-c383-4abf-8a7e-6b9cadbeb5b2"), + repository=first_repository, + state_id=StaticAnalysisSingleFileSnapshotState.VALID.db_id, + content_location="first_repo_first_snapshot", + ) + second_repo_first_snapshot = StaticAnalysisSingleFileSnapshotFactory.create( + file_hash=UUID("c8c23bea-c383-4abf-8a7e-6b9cadbeb5b2"), + repository=second_repository, + state_id=StaticAnalysisSingleFileSnapshotState.VALID.db_id, + content_location="second_repo_first_snapshot", + ) + second_repo_second_snapshot = StaticAnalysisSingleFileSnapshotFactory.create( + file_hash=UUID("3998813e-60db-4686-be84-1a0efa7d9b9f"), + repository=second_repository, + state_id=StaticAnalysisSingleFileSnapshotState.VALID.db_id, + content_location="second_repo_second_snapshot", + ) + first_repo_separate_snapshot = StaticAnalysisSingleFileSnapshotFactory.create( + file_hash=uuid4(), + repository=first_repository, + state_id=StaticAnalysisSingleFileSnapshotState.VALID.db_id, + content_location="first_repo_separate_snapshot", + ) + second_repo_separate_snapshot = StaticAnalysisSingleFileSnapshotFactory.create( + file_hash=uuid4(), + repository=second_repository, + state_id=StaticAnalysisSingleFileSnapshotState.VALID.db_id, + content_location="second_repo_separate_snapshot", + ) + first_repo_exists_but_not_valid_yet = ( + StaticAnalysisSingleFileSnapshotFactory.create( + file_hash=UUID("31803149-8bd7-4c2b-9a80-71f259360c72"), + repository=first_repository, + state_id=StaticAnalysisSingleFileSnapshotState.CREATED.db_id, + content_location="first_repo_exists_but_not_valid_yet", + ) + ) + first_repo_first_snapshot.save() + second_repo_first_snapshot.save() + second_repo_second_snapshot.save() + first_repo_separate_snapshot.save() + second_repo_separate_snapshot.save() + first_repo_exists_but_not_valid_yet.save() + + validated_data = { + "commit": commit, + "filepaths": [ + { + "filepath": "path/to/a.py", + "file_hash": UUID("c8c23bea-c383-4abf-8a7e-6b9cadbeb5b2"), + }, + { + "filepath": "anothersomething", + "file_hash": UUID("3998813e-60db-4686-be84-1a0efa7d9b9f"), + }, + { + "filepath": "oooaaa.rb", + "file_hash": UUID("60228df6-4d11-44d4-a048-ec2fa1ea2c32"), + }, + { + "filepath": "awert.qt", + "file_hash": UUID("31803149-8bd7-4c2b-9a80-71f259360c72"), + }, + ], + } + fake_request = mocker.MagicMock( + auth=mocker.MagicMock( + get_repositories=mocker.MagicMock(return_value=[commit.repository]) + ) + ) + serializer = StaticAnalysisSuiteSerializer(context={"request": fake_request}) + res = serializer.create(validated_data) + assert isinstance(res, StaticAnalysisSuite) + assert res.commit == commit + assert res.filepaths.count() == 4 + first_filepath, second_filepath, third_filepath, fourth_filepath = sorted( + res.filepaths.all(), key=lambda x: x.filepath + ) + archive_hash = ArchiveService.get_archive_hash(first_repository) + # first one + assert isinstance(first_filepath, StaticAnalysisSuiteFilepath) + assert first_filepath.filepath == "anothersomething" + assert first_filepath.file_snapshot is not None + assert first_filepath.file_snapshot.repository == first_repository + assert first_filepath.file_snapshot.file_hash == UUID( + "3998813e-60db-4686-be84-1a0efa7d9b9f" + ) + assert archive_hash in first_filepath.file_snapshot.content_location + assert ( + expected_location_regex.match(first_filepath.file_snapshot.content_location) + is not None + ) + assert ( + first_filepath.file_snapshot.state_id + == StaticAnalysisSingleFileSnapshotState.CREATED.db_id + ) + # second one + assert isinstance(second_filepath, StaticAnalysisSuiteFilepath) + assert second_filepath.filepath == "awert.qt" + assert second_filepath.file_snapshot == first_repo_exists_but_not_valid_yet + assert second_filepath.file_snapshot.repository == first_repository + assert second_filepath.file_snapshot.file_hash == UUID( + "31803149-8bd7-4c2b-9a80-71f259360c72" + ) + # content location was already there, so nothing is created + # asserting the old value is still there + assert ( + second_filepath.file_snapshot.content_location + == "first_repo_exists_but_not_valid_yet" + ) + assert ( + second_filepath.file_snapshot.state_id + == StaticAnalysisSingleFileSnapshotState.CREATED.db_id + ) + # third one + assert isinstance(third_filepath, StaticAnalysisSuiteFilepath) + assert third_filepath.filepath == "oooaaa.rb" + assert third_filepath.file_snapshot is not None + assert third_filepath.file_snapshot.repository == first_repository + assert third_filepath.file_snapshot.file_hash == UUID( + "60228df6-4d11-44d4-a048-ec2fa1ea2c32" + ) + assert archive_hash in third_filepath.file_snapshot.content_location + assert ( + expected_location_regex.match(third_filepath.file_snapshot.content_location) + is not None + ) + assert ( + third_filepath.file_snapshot.state_id + == StaticAnalysisSingleFileSnapshotState.CREATED.db_id + ) + # fourth one + assert isinstance(fourth_filepath, StaticAnalysisSuiteFilepath) + assert fourth_filepath.filepath == "path/to/a.py" + assert fourth_filepath.file_snapshot == first_repo_first_snapshot + assert fourth_filepath.file_snapshot.repository == first_repository + assert fourth_filepath.file_snapshot.file_hash == UUID( + "c8c23bea-c383-4abf-8a7e-6b9cadbeb5b2" + ) + # content location was already there, so nothing is created + # asserting the old value is still there + assert ( + fourth_filepath.file_snapshot.content_location + == "first_repo_first_snapshot" + ) + assert ( + fourth_filepath.file_snapshot.state_id + == StaticAnalysisSingleFileSnapshotState.VALID.db_id + ) diff --git a/apps/codecov-api/staticanalysis/urls.py b/apps/codecov-api/staticanalysis/urls.py new file mode 100644 index 0000000000..6a2a5510b8 --- /dev/null +++ b/apps/codecov-api/staticanalysis/urls.py @@ -0,0 +1,7 @@ +from staticanalysis.views import StaticAnalysisSuiteViewSet +from utils.routers import OptionalTrailingSlashRouter + +router = OptionalTrailingSlashRouter() +router.register("analyses", StaticAnalysisSuiteViewSet, basename="staticanalyses") + +urlpatterns = router.urls diff --git a/apps/codecov-api/staticanalysis/views.py b/apps/codecov-api/staticanalysis/views.py new file mode 100644 index 0000000000..7045e63020 --- /dev/null +++ b/apps/codecov-api/staticanalysis/views.py @@ -0,0 +1,46 @@ +import logging + +from django.http import HttpResponse +from rest_framework import mixins, viewsets +from rest_framework.decorators import action +from shared.celery_config import static_analysis_task_name + +from codecov_auth.authentication.repo_auth import RepositoryTokenAuthentication +from codecov_auth.permissions import SpecificScopePermission +from services.task import TaskService +from staticanalysis.models import StaticAnalysisSuite +from staticanalysis.serializers import StaticAnalysisSuiteSerializer + +log = logging.getLogger(__name__) + + +class StaticAnalysisSuiteViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): + serializer_class = StaticAnalysisSuiteSerializer + authentication_classes = [RepositoryTokenAuthentication] + permission_classes = [SpecificScopePermission] + required_scopes = ["static_analysis"] + lookup_field = "external_id" + + def get_queryset(self): + repository = self.request.auth.get_repositories()[0] + return StaticAnalysisSuite.objects.filter(commit__repository=repository) + + def perform_create(self, serializer): + instance = serializer.save() + # TODO: remove this once the CLI is calling the `finish` endpoint + TaskService().schedule_task( + static_analysis_task_name, + kwargs=dict(suite_id=instance.id), + apply_async_kwargs=dict(countdown=10), + ) + return instance + + @action(detail=True, methods=["post"]) + def finish(self, request, *args, **kwargs): + suite = self.get_object() + TaskService().schedule_task( + static_analysis_task_name, + kwargs=dict(suite_id=suite.pk), + apply_async_kwargs={}, + ) + return HttpResponse(status=204) diff --git a/apps/codecov-api/templates/admin/base_site.html b/apps/codecov-api/templates/admin/base_site.html new file mode 100644 index 0000000000..11c7f81767 --- /dev/null +++ b/apps/codecov-api/templates/admin/base_site.html @@ -0,0 +1,50 @@ +{% extends "admin/base_site.html" %} +{% load i18n %} + +{% block extrahead %} +{{ block.super }} + + +{% endblock %} diff --git a/apps/codecov-api/timeseries/__init__.py b/apps/codecov-api/timeseries/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/timeseries/admin.py b/apps/codecov-api/timeseries/admin.py new file mode 100644 index 0000000000..5e493dffd4 --- /dev/null +++ b/apps/codecov-api/timeseries/admin.py @@ -0,0 +1,79 @@ +from datetime import datetime + +import django.forms as forms +from django.conf import settings +from django.contrib import admin, messages +from django.db.models import QuerySet +from django.shortcuts import render + +from codecov.admin import AdminMixin +from core.models import Repository +from services.task import TaskService +from timeseries.models import Dataset + + +def enqueue_tasks(datasets: QuerySet, start_date: datetime, end_date: datetime): + count = datasets.update(backfilled=False) + + for dataset in datasets: + TaskService().backfill_dataset( + dataset, + start_date=start_date, + end_date=end_date, + ) + + return count + + +class BackfillForm(forms.Form): + start_date = forms.DateTimeField(required=True) + end_date = forms.DateTimeField(required=True) + + +class DatasetAdmin(AdminMixin, admin.ModelAdmin): + list_display = ("name", "repository", "backfilled") + actions = ["backfill"] + + def get_queryset(self, request): + queryset = super().get_queryset(request) + + # this prevents an N+1 query from the `repository` method below + repo_ids = list(queryset.values_list("repository_id", flat=True)) + self._repositories = { + repository.pk: repository + for repository in Repository.objects.filter(pk__in=repo_ids) + } + + return queryset + + def repository(self, dataset): + return self._repositories[dataset.repository_id] + + def backfill(self, request, queryset): + if "backfill" in request.POST: + form = BackfillForm(request.POST) + if form.is_valid(): + count = enqueue_tasks( + queryset, + start_date=form.cleaned_data["start_date"], + end_date=form.cleaned_data["end_date"], + ) + messages.success( + request, f"Enqueued backfill tasks for {count} datasets" + ) + return + else: + form = BackfillForm() + + return render( + request, + "admin/backfill.html", + context={ + "form": form, + "datasets": queryset, + }, + ) + + +if settings.TIMESERIES_ENABLED: + admin.site.register(Dataset, DatasetAdmin) diff --git a/apps/codecov-api/timeseries/helpers.py b/apps/codecov-api/timeseries/helpers.py new file mode 100644 index 0000000000..8ce735e2a8 --- /dev/null +++ b/apps/codecov-api/timeseries/helpers.py @@ -0,0 +1,427 @@ +import math +from datetime import datetime, timedelta +from typing import Iterable, List, Optional + +import sentry_sdk +from django.conf import settings +from django.db import connections +from django.db.models import ( + Avg, + DateTimeField, + DecimalField, + F, + FloatField, + Func, + Max, + Min, + QuerySet, + Sum, + Value, +) +from django.db.models.fields.json import KeyTextTransform +from django.db.models.functions import Cast +from django.utils import timezone + +from codecov_auth.models import Owner +from core.models import Commit, Repository +from services.task import TaskService +from timeseries.models import ( + Dataset, + Interval, + MeasurementName, + MeasurementSummary, +) + +interval_deltas = { + Interval.INTERVAL_1_DAY: timedelta(days=1), + Interval.INTERVAL_7_DAY: timedelta(days=7), + Interval.INTERVAL_30_DAY: timedelta(days=30), +} + + +@sentry_sdk.trace +def refresh_measurement_summaries(start_date: datetime, end_date: datetime) -> None: + """ + Refresh the measurement summaries for the given time range. + This calls a TimescaleDB provided SQL function for each of the continuous aggregates + to refresh the aggregate data in the provided time range. + """ + continuous_aggregates = [ + "timeseries_measurement_summary_1day", + "timeseries_measurement_summary_7day", + "timeseries_measurement_summary_30day", + ] + with connections["timeseries"].cursor() as cursor: + for cagg in continuous_aggregates: + sql = f"CALL refresh_continuous_aggregate('{cagg}', '{start_date.isoformat()}', '{end_date.isoformat()}')" + cursor.execute(sql) + + +@sentry_sdk.trace +def aggregate_measurements( + queryset: QuerySet, group_by: Iterable[str] = None +) -> QuerySet: + """ + The given queryset is a set of measurement summaries. These are already + pre-aggregated by (timestamp, owner_id, repo_id, measurable_id, branch) via TimescaleDB's + continuous aggregates. If we want to further aggregate over any of those columns + then we need to perform additional aggregation in SQL. That is what this function + does to the given queryset. + """ + if not group_by: + group_by = ["timestamp_bin"] + + return ( + queryset.values(*group_by) + .annotate( + min=Min("value_min"), + max=Max("value_max"), + avg=Cast( + Sum(F("value_avg") * F("value_count")) / Sum(F("value_count")), + # this is equivalent to Postgres' numeric(1000, 5) type + # 1000 is the max precision + # (used to avoid floating point error) + DecimalField(max_digits=1000, decimal_places=5), + ), + ) + .order_by("timestamp_bin") + ) + + +def _filter_repos( + queryset: QuerySet, repos: Optional[List[Repository]], column_name: str = "repo_id" +) -> QuerySet: + """ + Filter the given generic queryset by a set of (repoid, branch) tuples. + """ + if repos: + queryset = queryset.extra( + where=[f"({column_name}, branch) in %s"], + params=[tuple((repo.repoid, repo.branch) for repo in repos)], + ) + return queryset + + +@sentry_sdk.trace +def coverage_measurements( + interval: Interval, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + repos: Optional[List[Repository]] = None, + **filters, +): + timestamp_filters = {} + if start_date is not None: + timestamp_filters["timestamp_bin__gte"] = start_date + if end_date is not None: + timestamp_filters["timestamp_bin__lte"] = end_date + + queryset = ( + MeasurementSummary.agg_by(interval) + .filter(name=MeasurementName.COVERAGE.value, **timestamp_filters) + .filter(**filters) + ) + + queryset = _filter_repos(queryset, repos) + + if start_date: + # The first measurement of the specified range (`start_date` through `end_date`) + # may be missing the first datapoint. In order for consumers of this API to have + # usable data to show we can carry an older datapoint forward to the first time bin. + # Including this older datapoint in the result set makes that possible. + older = ( + MeasurementSummary.agg_by(interval) + .filter( + name=MeasurementName.COVERAGE.value, + timestamp_bin__lt=start_date, + ) + .filter(**filters) + ) + older = _filter_repos(older, repos) + older = aggregate_measurements(older).order_by("-timestamp_bin")[:1] + + return older.union(aggregate_measurements(queryset)).order_by("timestamp_bin") + else: + return aggregate_measurements(queryset).order_by("timestamp_bin") + + +def trigger_backfill(datasets: list[Dataset]): + """ + Triggers a backfill for the full timespan of the dataset's repo's commits. + """ + repo_ids = {d.repository_id for d in datasets} + timeranges = ( + Commit.objects.filter(repository_id__in=repo_ids) + .values_list("repository_id") + .annotate(start_date=Min("timestamp"), end_date=Max("timestamp")) + ) + + timerange_by_repo = { + repo_id: (start_date, end_date) for repo_id, start_date, end_date in timeranges + } + + for dataset in datasets: + if dataset.repository_id not in timerange_by_repo: + continue # there are no commits, and thus nothing to backfill + start_date, end_date = timerange_by_repo[dataset.repository_id] + TaskService().backfill_dataset( + dataset, start_date=start_date, end_date=end_date + ) + + +def aligned_start_date(interval: Interval, date: datetime) -> datetime: + """ + Finds the aligned start date for the given timedelta and date. + TimescaleDB aligns time buckets starting on 2000-01-03 so this function will + return the date of the start of the bin containing the given `date`. + The return value will be <= the given date. + """ + delta = interval_deltas[interval] + + # TimescaleDB aligns time buckets starting on 2000-01-03) + aligning_date = datetime(2000, 1, 3, tzinfo=timezone.utc) + + # number of full intervals between aligning date and the given date + intervals_before = math.floor((date - aligning_date) / delta) + + # starting date of time bucket that contains the given date + return aligning_date + (intervals_before * delta) + + +@sentry_sdk.trace +def fill_sparse_measurements( + measurements: Iterable[dict], + interval: Interval, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, +) -> Iterable[dict]: + """ + Fill in sparse array of measurements with values such that we + have an entry for every interval within the requested time range. + Those placeholder entries will have empty measurement values. + """ + by_timestamp = { + measurement["timestamp_bin"].replace(tzinfo=timezone.utc): measurement + for measurement in measurements + } + timestamps = sorted(by_timestamp.keys()) + if len(timestamps) == 0: + return [] + + delta = interval_deltas[interval] + + if start_date is None: + start_date = timestamps[0] + start_date = aligned_start_date(interval, start_date) + + if end_date is None: + end_date = timezone.now() + + intervals = [] + + current_date = start_date + while current_date <= end_date: + if current_date in by_timestamp: + intervals.append(by_timestamp[current_date]) + else: + # interval not found + intervals.append( + { + "timestamp_bin": current_date, + "avg": None, + "min": None, + "max": None, + } + ) + current_date += delta + + if len(timestamps) > 0: + oldest_date = timestamps[0] + if ( + oldest_date <= start_date + and len(intervals) > 0 + and intervals[0]["avg"] is None + ): + # we're missing the first datapoint but we can carry forward + # and older measurement that was selected + measurement = by_timestamp[oldest_date] + intervals[0] = { + **measurement, + "timestamp_bin": start_date, + } + + return intervals + + +@sentry_sdk.trace +def coverage_fallback_query( + interval: Interval, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + repos: Optional[List[Repository]] = None, + **filters, +): + """ + Query for coverage timeseries directly from the database + """ + timestamp_filters = {} + if start_date is not None: + timestamp_filters["timestamp__gte"] = start_date + if end_date is not None: + timestamp_filters["timestamp__lte"] = end_date + commits = Commit.objects.filter(**timestamp_filters).filter(**filters) + commits = _filter_repos(commits, repos, column_name="repoid") + commits = _commits_coverage(commits, interval) + + if start_date: + # The first measurement of the specified range (`start_date` through `end_date`) + # may be missing the first datapoint. In order for consumers of this API to have + # usable data to show we can carry an older datapoint forward to the first time bin. + # Including this older datapoint in the result set makes that possible. + older = Commit.objects.filter( + timestamp__lt=start_date, + ).filter(**filters) + older = _filter_repos(older, repos, column_name="repoid") + older = _commits_coverage(older, interval).order_by("-timestamp_bin")[:1] + + return older.union(commits).order_by("timestamp_bin") + else: + return commits.order_by("timestamp_bin") + + +def _commits_coverage( + commits_queryset: QuerySet[Commit], interval: Interval +) -> QuerySet[Commit]: + intervals = { + Interval.INTERVAL_1_DAY: "1 day", + Interval.INTERVAL_7_DAY: "7 days", + Interval.INTERVAL_30_DAY: "30 days", + } + + return ( + commits_queryset.annotate( + timestamp_bin=Func( + Value(intervals[interval]), + F("timestamp"), + Value("2000-01-03"), # mimic how Timescale aligns bins + function="date_bin", + template="%(function)s(%(expressions)s) at time zone 'utc'", + output_field=DateTimeField(), + ), + coverage=Cast(KeyTextTransform("c", "totals"), output_field=FloatField()), + ) + .filter(coverage__isnull=False) + .values("timestamp_bin") + .annotate( + min=Min("coverage"), + max=Max("coverage"), + avg=Avg("coverage"), + ) + .order_by("timestamp_bin") + ) + + +@sentry_sdk.trace +def repository_coverage_measurements_with_fallback( + repository: Repository, + interval: Interval, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + branch: str = None, +): + """ + Tries to return repository coverage measurements from Timescale. + If those are not available then we trigger a backfill and return computed results + directly from the primary database (much slower to query). + """ + if settings.TIMESERIES_ENABLED: + dataset = Dataset.objects.filter( + name=MeasurementName.COVERAGE.value, + repository_id=repository.pk, + ).first() + + if dataset and dataset.is_backfilled(): + # timeseries data is ready + return coverage_measurements( + interval, + start_date=start_date, + end_date=end_date, + owner_id=repository.author_id, + repo_id=repository.pk, + measurable_id=str(repository.pk), + branch=branch or repository.branch, + ) + + if not dataset: + # we need to backfill + dataset, created = Dataset.objects.get_or_create( + name=MeasurementName.COVERAGE.value, + repository_id=repository.pk, + ) + if created: + trigger_backfill([dataset]) + + # we're still backfilling or timeseries is disabled + return coverage_fallback_query( + interval, + start_date=start_date, + end_date=end_date, + repository_id=repository.pk, + branch=branch or repository.branch, + ) + + +@sentry_sdk.trace +def owner_coverage_measurements_with_fallback( + owner: Owner, + repo_ids: Iterable[str], + interval: Interval, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, +): + """ + Tries to return owner coverage measurements from Timescale. + If those are not available then we trigger a backfill and return computed results + directly from the primary database (much slower to query). + """ + # we can't join across databases so we need to load all this into memory. + # select just the needed columns to keep this manageable + repos = Repository.objects.filter(repoid__in=repo_ids).only("repoid", "branch") + + if settings.TIMESERIES_ENABLED: + datasets = Dataset.objects.filter( + name=MeasurementName.COVERAGE.value, + repository_id__in=repo_ids, + ) + all_backfilled = len(datasets) == len(repo_ids) and all( + dataset.is_backfilled() for dataset in datasets + ) + + if all_backfilled: + # timeseries data is ready + return coverage_measurements( + interval, + start_date=start_date, + end_date=end_date, + owner_id=owner.pk, + repos=repos, + ) + + # we need to backfill some datasets + dataset_repo_ids = {dataset.repository_id for dataset in datasets} + missing_dataset_repo_ids = set(repo_ids) - dataset_repo_ids + created_datasets = Dataset.objects.bulk_create( + [ + Dataset(name=MeasurementName.COVERAGE.value, repository_id=repo_id) + for repo_id in missing_dataset_repo_ids + ] + ) + trigger_backfill(created_datasets) + + # we're still backfilling or timeseries is disabled + return coverage_fallback_query( + interval, + start_date=start_date, + end_date=end_date, + repos=repos, + ) diff --git a/apps/codecov-api/timeseries/models.py b/apps/codecov-api/timeseries/models.py new file mode 100644 index 0000000000..b4f7267db5 --- /dev/null +++ b/apps/codecov-api/timeseries/models.py @@ -0,0 +1 @@ +from shared.django_apps.timeseries.models import * diff --git a/apps/codecov-api/timeseries/templates/admin/backfill.html b/apps/codecov-api/timeseries/templates/admin/backfill.html new file mode 100644 index 0000000000..24bde6f2e0 --- /dev/null +++ b/apps/codecov-api/timeseries/templates/admin/backfill.html @@ -0,0 +1,28 @@ +{% extends "admin/base_site.html" %} + +{% block content %} +
+ {% csrf_token %} + + + + {{ form }} +
+ +
+ +

Backfill will be performed for the following datasets:

+

+

    + {% for dataset in datasets %} +
  • + {{ dataset.name }} (repo={{dataset.repository_id}}) + +
  • + {% endfor %} +
+

+ + +
+{% endblock %} \ No newline at end of file diff --git a/apps/codecov-api/timeseries/tests/__init__.py b/apps/codecov-api/timeseries/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/timeseries/tests/factories.py b/apps/codecov-api/timeseries/tests/factories.py new file mode 100644 index 0000000000..7e4b78ffcd --- /dev/null +++ b/apps/codecov-api/timeseries/tests/factories.py @@ -0,0 +1 @@ +from shared.django_apps.timeseries.tests.factories import * diff --git a/apps/codecov-api/timeseries/tests/test_admin.py b/apps/codecov-api/timeseries/tests/test_admin.py new file mode 100644 index 0000000000..6f3c15eb16 --- /dev/null +++ b/apps/codecov-api/timeseries/tests/test_admin.py @@ -0,0 +1,81 @@ +from unittest.mock import patch + +import pytest +from django.conf import settings +from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME +from django.test import TestCase +from django.urls import reverse +from django.utils import timezone +from shared.django_apps.codecov_auth.tests.factories import UserFactory +from shared.django_apps.core.tests.factories import RepositoryFactory +from shared.django_apps.timeseries.tests.factories import DatasetFactory + + +@pytest.mark.skipif( + not settings.TIMESERIES_ENABLED, reason="requires timeseries data storage" +) +class DatasetAdminTest(TestCase): + databases = {"default", "timeseries"} + + def setUp(self): + self.user = UserFactory(is_staff=True) + self.client.force_login(user=self.user) + + self.repo1 = RepositoryFactory() + self.repo2 = RepositoryFactory() + self.dataset1 = DatasetFactory(repository_id=self.repo1.pk, backfilled=True) + self.dataset2 = DatasetFactory(repository_id=self.repo2.pk, backfilled=True) + + def test_list_page(self): + res = self.client.get(reverse("admin:timeseries_dataset_changelist")) + assert res.status_code == 200 + + def test_backfill_page(self): + res = self.client.post( + reverse("admin:timeseries_dataset_changelist"), + { + "action": "backfill", + ACTION_CHECKBOX_NAME: [ + self.dataset1.pk, + self.dataset2.pk, + ], + }, + ) + assert res.status_code == 200 + assert "Backfill will be performed for the following datasets" in str( + res.content + ) + + @patch("services.task.TaskService.backfill_dataset") + def test_perform_backfill(self, backfill_dataset): + res = self.client.post( + reverse("admin:timeseries_dataset_changelist"), + { + "action": "backfill", + ACTION_CHECKBOX_NAME: [ + self.dataset1.pk, + self.dataset2.pk, + ], + "start_date": "2000-01-01", + "end_date": "2022-01-01", + "backfill": True, + }, + ) + assert res.status_code == 302 + + assert backfill_dataset.call_count == 2 + backfill_dataset.assert_any_call( + self.dataset1, + start_date=timezone.datetime(2000, 1, 1, tzinfo=timezone.utc), + end_date=timezone.datetime(2022, 1, 1, tzinfo=timezone.utc), + ) + backfill_dataset.assert_any_call( + self.dataset2, + start_date=timezone.datetime(2000, 1, 1, tzinfo=timezone.utc), + end_date=timezone.datetime(2022, 1, 1, tzinfo=timezone.utc), + ) + + self.dataset1.refresh_from_db() + assert self.dataset1.backfilled == False + self.dataset2.refresh_from_db() + assert self.dataset2.backfilled == False diff --git a/apps/codecov-api/timeseries/tests/test_helpers.py b/apps/codecov-api/timeseries/tests/test_helpers.py new file mode 100644 index 0000000000..1c507a245c --- /dev/null +++ b/apps/codecov-api/timeseries/tests/test_helpers.py @@ -0,0 +1,1230 @@ +from datetime import datetime, timezone +from unittest.mock import patch + +import pytest +from django.conf import settings +from django.test import TestCase +from freezegun import freeze_time +from freezegun.api import FakeDatetime +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.django_apps.timeseries.tests.factories import ( + DatasetFactory, + MeasurementFactory, +) +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine +from shared.utils.sessions import Session + +from timeseries.helpers import ( + coverage_measurements, + fill_sparse_measurements, + owner_coverage_measurements_with_fallback, + refresh_measurement_summaries, + repository_coverage_measurements_with_fallback, +) +from timeseries.models import Dataset, Interval, MeasurementName + + +def sample_report(): + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=["flag1", "flag2"])) + return report + + +@pytest.mark.skipif( + not settings.TIMESERIES_ENABLED, reason="requires timeseries data storage" +) +class RefreshMeasurementSummariesTest(TestCase): + databases = {"timeseries"} + + @patch("django.db.backends.utils.CursorWrapper.execute") + def test_refresh_measurement_summaries(self, execute): + refresh_measurement_summaries( + start_date=datetime(2022, 1, 1, 0, 0, 0), + end_date=datetime(2022, 1, 2, 0, 0, 0), + ) + + assert execute.call_count == 3 + sql_statements = [call[0][0] for call in execute.call_args_list] + assert sql_statements == [ + "CALL refresh_continuous_aggregate('timeseries_measurement_summary_1day', '2022-01-01T00:00:00', '2022-01-02T00:00:00')", + "CALL refresh_continuous_aggregate('timeseries_measurement_summary_7day', '2022-01-01T00:00:00', '2022-01-02T00:00:00')", + "CALL refresh_continuous_aggregate('timeseries_measurement_summary_30day', '2022-01-01T00:00:00', '2022-01-02T00:00:00')", + ] + + +@pytest.mark.skipif( + not settings.TIMESERIES_ENABLED, reason="requires timeseries data storage" +) +class RepositoryCoverageMeasurementsTest(TestCase): + databases = {"default", "timeseries"} + + def setUp(self): + self.repo = RepositoryFactory() + + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2022, 1, 1, 1, 0, 0), + value=80.0, + branch="main", + commit_sha="commit1", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2022, 1, 1, 2, 0, 0), + value=85.0, + branch="main", + commit_sha="commit2", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2022, 1, 1, 3, 0, 0), + value=90.0, + branch="other", + commit_sha="commit3", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2022, 1, 2, 1, 0, 0), + value=80.0, + branch="main", + commit_sha="commit4", + ) + + def test_coverage_measurements(self): + res = coverage_measurements( + Interval.INTERVAL_1_DAY, + start_date=datetime(2021, 12, 30, 0, 0, 0), + end_date=datetime(2022, 1, 4, 0, 0, 0), + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + branch=self.repo.branch, + ) + assert list(res) == [ + { + # aggregates over 2 measurements on main branch (commit1, commit2) + "timestamp_bin": datetime(2022, 1, 1, 0, 0, tzinfo=timezone.utc), + "avg": 82.5, + "min": 80.0, + "max": 85.0, + }, + { + # aggregates over 1 measurement (commit4) + "timestamp_bin": datetime(2022, 1, 2, 0, 0, tzinfo=timezone.utc), + "avg": 80.0, + "min": 80.0, + "max": 80.0, + }, + ] + + +@pytest.mark.skipif( + not settings.TIMESERIES_ENABLED, reason="requires timeseries data storage" +) +class FillSparseMeasurementsTest(TestCase): + databases = {"default", "timeseries"} + + def setUp(self): + self.repo = RepositoryFactory() + + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2022, 1, 1, 1, 0, 0), + value=80.0, + branch="main", + commit_sha="commit1", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2022, 1, 1, 2, 0, 0), + value=85.0, + branch="main", + commit_sha="commit2", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2022, 1, 1, 3, 0, 0), + value=90.0, + branch="other", + commit_sha="commit3", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2022, 1, 2, 1, 0, 0), + value=80.0, + branch="main", + commit_sha="commit4", + ) + + def test_fill_sparse_measurements(self): + start_date = datetime(2021, 12, 31, 0, 0, 0, tzinfo=timezone.utc) + end_date = datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc) + measurements = coverage_measurements( + Interval.INTERVAL_1_DAY, + start_date=start_date, + end_date=end_date, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + branch="main", + ) + assert fill_sparse_measurements( + measurements, Interval.INTERVAL_1_DAY, start_date, end_date + ) == [ + { + "timestamp_bin": datetime(2021, 12, 31, 0, 0, tzinfo=timezone.utc), + "avg": None, + "min": None, + "max": None, + }, + { + # aggregates over 2 measurements on main branch (commit1, commit2) + "timestamp_bin": datetime(2022, 1, 1, 0, 0, tzinfo=timezone.utc), + "avg": 82.5, + "min": 80.0, + "max": 85.0, + }, + { + # aggregates over 1 measurement (commit4) + "timestamp_bin": datetime(2022, 1, 2, 0, 0, tzinfo=timezone.utc), + "avg": 80.0, + "min": 80.0, + "max": 80.0, + }, + { + "timestamp_bin": datetime(2022, 1, 3, 0, 0, tzinfo=timezone.utc), + "avg": None, + "min": None, + "max": None, + }, + ] + + def test_fill_sparse_measurements_no_start_date(self): + end_date = datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc) + measurements = coverage_measurements( + Interval.INTERVAL_1_DAY, + end_date=end_date, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + branch="main", + ) + assert fill_sparse_measurements( + measurements, Interval.INTERVAL_1_DAY, start_date=None, end_date=end_date + ) == [ + { + # aggregates over 2 measurements on main branch (commit1, commit2) + "timestamp_bin": datetime(2022, 1, 1, 0, 0, tzinfo=timezone.utc), + "avg": 82.5, + "min": 80.0, + "max": 85.0, + }, + { + # aggregates over 1 measurement (commit4) + "timestamp_bin": datetime(2022, 1, 2, 0, 0, tzinfo=timezone.utc), + "avg": 80.0, + "min": 80.0, + "max": 80.0, + }, + { + "timestamp_bin": datetime(2022, 1, 3, 0, 0, tzinfo=timezone.utc), + "avg": None, + "min": None, + "max": None, + }, + ] + + @freeze_time("2022-01-03T00:00:00") + def test_fill_sparse_measurements_no_end_date(self): + start_date = datetime(2021, 12, 31, 0, 0, 0, tzinfo=timezone.utc) + measurements = coverage_measurements( + Interval.INTERVAL_1_DAY, + start_date=start_date, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + branch="main", + ) + assert fill_sparse_measurements( + measurements, + Interval.INTERVAL_1_DAY, + start_date=start_date, + ) == [ + { + "timestamp_bin": FakeDatetime(2021, 12, 31, 0, 0, tzinfo=timezone.utc), + "avg": None, + "min": None, + "max": None, + }, + { + # aggregates over 2 measurements on main branch (commit1, commit2) + "timestamp_bin": datetime(2022, 1, 1, 0, 0, tzinfo=timezone.utc), + "avg": 82.5, + "min": 80.0, + "max": 85.0, + }, + { + # aggregates over 1 measurement (commit4) + "timestamp_bin": datetime(2022, 1, 2, 0, 0, tzinfo=timezone.utc), + "avg": 80.0, + "min": 80.0, + "max": 80.0, + }, + { + "timestamp_bin": FakeDatetime(2022, 1, 3, 0, 0, tzinfo=timezone.utc), + "avg": None, + "min": None, + "max": None, + }, + ] + + def test_fill_sparse_measurements_first_datapoint(self): + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2021, 12, 1, 1, 0, 0), + value=80.0, + branch="main", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2021, 12, 1, 1, 0, 0), + value=90.0, + branch="main", + ) + + start_date = datetime(2021, 12, 31, 0, 0, 0, tzinfo=timezone.utc) + end_date = datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc) + measurements = coverage_measurements( + Interval.INTERVAL_1_DAY, + start_date=start_date, + end_date=end_date, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + branch="main", + ) + assert fill_sparse_measurements( + measurements, Interval.INTERVAL_1_DAY, start_date, end_date + ) == [ + { + # this bin is carried forward from the last datapoint before `start_date` + "timestamp_bin": datetime(2021, 12, 31, 0, 0, tzinfo=timezone.utc), + "avg": 85.0, + "min": 80.0, + "max": 90.0, + }, + { + # aggregates over 2 measurements on main branch (commit1, commit2) + "timestamp_bin": datetime(2022, 1, 1, 0, 0, tzinfo=timezone.utc), + "avg": 82.5, + "min": 80.0, + "max": 85.0, + }, + { + # aggregates over 1 measurement (commit4) + "timestamp_bin": datetime(2022, 1, 2, 0, 0, tzinfo=timezone.utc), + "avg": 80.0, + "min": 80.0, + "max": 80.0, + }, + { + "timestamp_bin": datetime(2022, 1, 3, 0, 0, tzinfo=timezone.utc), + "avg": None, + "min": None, + "max": None, + }, + ] + + def test_fill_sparse_measurements_no_measurements(self): + assert fill_sparse_measurements([], Interval.INTERVAL_1_DAY, None, None) == [] + + +@pytest.mark.skipif( + not settings.TIMESERIES_ENABLED, reason="requires timeseries data storage" +) +class RepositoryCoverageMeasurementsWithFallbackTest(TestCase): + databases = {"default", "timeseries"} + + def setUp(self): + self.repo = RepositoryFactory() + + @patch("timeseries.models.Dataset.is_backfilled") + def test_backfilled_dataset(self, is_backfilled): + is_backfilled.return_value = True + + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2022, 1, 1, 1, 0, 0), + value=80.0, + branch="main", + commit_sha="commit1", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2022, 1, 1, 2, 0, 0), + value=85.0, + branch="main", + commit_sha="commit2", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2022, 1, 1, 3, 0, 0), + value=90.0, + branch="other", + commit_sha="commit3", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2022, 1, 2, 1, 0, 0), + value=80.0, + branch="main", + commit_sha="commit4", + ) + + DatasetFactory( + name=MeasurementName.COVERAGE.value, + repository_id=self.repo.pk, + ) + + res = repository_coverage_measurements_with_fallback( + self.repo, + Interval.INTERVAL_1_DAY, + start_date=datetime(2021, 12, 31, 0, 0, 0, tzinfo=timezone.utc), + end_date=datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + ) + assert list(res) == [ + { + # aggregates over 2 measurements on main branch (commit1, commit2) + "timestamp_bin": datetime(2022, 1, 1, 0, 0, tzinfo=timezone.utc), + "avg": 82.5, + "min": 80.0, + "max": 85.0, + }, + { + # aggregates over 1 measurement (commit4) + "timestamp_bin": datetime(2022, 1, 2, 0, 0, tzinfo=timezone.utc), + "avg": 80.0, + "min": 80.0, + "max": 80.0, + }, + ] + + @patch("timeseries.models.Dataset.is_backfilled") + def test_backfilled_dataset_no_start_end_dates(self, is_backfilled): + is_backfilled.return_value = True + + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2022, 1, 1, 1, 0, 0), + value=80.0, + branch="main", + commit_sha="commit1", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2022, 1, 1, 2, 0, 0), + value=85.0, + branch="main", + commit_sha="commit2", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2021, 1, 1, 3, 0, 0), + value=90.0, + branch="other", + commit_sha="commit3", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo.author_id, + repo_id=self.repo.pk, + measurable_id=str(self.repo.pk), + timestamp=datetime(2022, 1, 2, 1, 0, 0), + value=80.0, + branch="main", + commit_sha="commit4", + ) + + DatasetFactory( + name=MeasurementName.COVERAGE.value, + repository_id=self.repo.pk, + ) + + res = repository_coverage_measurements_with_fallback( + self.repo, + Interval.INTERVAL_1_DAY, + ) + assert list(res) == [ + { + # aggregates over 2 measurements on main branch (commit1, commit2) + "timestamp_bin": datetime(2022, 1, 1, 0, 0, tzinfo=timezone.utc), + "avg": 82.5, + "min": 80.0, + "max": 85.0, + }, + { + # aggregates over 1 measurement (commit4) + "timestamp_bin": datetime(2022, 1, 2, 0, 0, tzinfo=timezone.utc), + "avg": 80.0, + "min": 80.0, + "max": 80.0, + }, + ] + + @patch("timeseries.models.Dataset.is_backfilled") + def test_unbackfilled_dataset(self, is_backfilled): + is_backfilled.return_value = False + + CommitFactory( + commitid="commit1", + repository_id=self.repo.pk, + branch="main", + timestamp=datetime(2022, 1, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + totals={ + "c": "80.00", + }, + ) + CommitFactory( + commitid="commit2", + repository_id=self.repo.pk, + branch="main", + timestamp=datetime(2022, 1, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + totals={"c": "85.00"}, + ) + CommitFactory( + commitid="commit3", + repository_id=self.repo.pk, + branch="other", + timestamp=datetime(2022, 1, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + totals={"c": "90.00"}, + ) + CommitFactory( + commitid="commit4", + repository_id=self.repo.pk, + branch="main", + timestamp=datetime(2022, 1, 2, 1, 0, 0, 0, tzinfo=timezone.utc), + totals={ + "c": "80.00", + }, + ) + + DatasetFactory( + name=MeasurementName.COVERAGE.value, + repository_id=self.repo.pk, + ) + + res = repository_coverage_measurements_with_fallback( + self.repo, + Interval.INTERVAL_1_DAY, + start_date=datetime(2021, 12, 31, 0, 0, 0, tzinfo=timezone.utc), + end_date=datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + ) + assert list(res) == [ + { + # aggregates over 2 measurements on main branch (commit1, commit2) + "timestamp_bin": datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "avg": 82.5, + "min": 80.0, + "max": 85.0, + }, + { + # aggregates over 1 measurement (commit4) + "timestamp_bin": datetime(2022, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + "avg": 80.0, + "min": 80.0, + "max": 80.0, + }, + ] + + @patch("timeseries.models.Dataset.is_backfilled") + def test_unbackfilled_dataset_no_start_end_dates(self, is_backfilled): + is_backfilled.return_value = False + + CommitFactory( + commitid="commit1", + repository_id=self.repo.pk, + branch="main", + timestamp=datetime(2022, 1, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + totals={ + "c": "80.00", + }, + ) + CommitFactory( + commitid="commit2", + repository_id=self.repo.pk, + branch="main", + timestamp=datetime(2022, 1, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + totals={"c": "85.00"}, + ) + CommitFactory( + commitid="commit3", + repository_id=self.repo.pk, + branch="other", + timestamp=datetime(2022, 1, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + totals={"c": "90.00"}, + ) + CommitFactory( + commitid="commit4", + repository_id=self.repo.pk, + branch="main", + timestamp=datetime(2022, 1, 2, 1, 0, 0, 0, tzinfo=timezone.utc), + totals={ + "c": "80.00", + }, + ) + + DatasetFactory( + name=MeasurementName.COVERAGE.value, + repository_id=self.repo.pk, + ) + + res = repository_coverage_measurements_with_fallback( + self.repo, + Interval.INTERVAL_1_DAY, + ) + assert list(res) == [ + { + # aggregates over 2 measurements on main branch (commit1, commit2) + "timestamp_bin": datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "avg": 82.5, + "min": 80.0, + "max": 85.0, + }, + { + # aggregates over 1 measurement (commit4) + "timestamp_bin": datetime(2022, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + "avg": 80.0, + "min": 80.0, + "max": 80.0, + }, + ] + + @patch("timeseries.helpers.trigger_backfill") + def test_no_dataset(self, trigger_backfill): + CommitFactory( + commitid="commit1", + repository_id=self.repo.pk, + branch="main", + timestamp=datetime(2022, 1, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + totals={ + "c": "80.00", + }, + ) + CommitFactory( + commitid="commit2", + repository_id=self.repo.pk, + branch="main", + timestamp=datetime(2022, 1, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + totals={"c": "85.00"}, + ) + CommitFactory( + commitid="commit3", + repository_id=self.repo.pk, + branch="other", + timestamp=datetime(2022, 1, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + totals={"c": "90.00"}, + ) + CommitFactory( + commitid="commit4", + repository_id=self.repo.pk, + branch="main", + timestamp=datetime(2022, 1, 2, 1, 0, 0, 0, tzinfo=timezone.utc), + totals={ + "c": "80.00", + }, + ) + + res = repository_coverage_measurements_with_fallback( + self.repo, + Interval.INTERVAL_1_DAY, + start_date=datetime(2021, 12, 31, 0, 0, 0, tzinfo=timezone.utc), + end_date=datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + ) + assert list(res) == [ + { + # aggregates over 2 measurements on main branch (commit1, commit2) + "timestamp_bin": datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "avg": 82.5, + "min": 80.0, + "max": 85.0, + }, + { + # aggregates over 1 measurement (commit4) + "timestamp_bin": datetime(2022, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + "avg": 80.0, + "min": 80.0, + "max": 80.0, + }, + ] + + dataset = Dataset.objects.filter( + name=MeasurementName.COVERAGE.value, + repository_id=self.repo.pk, + ).first() + assert dataset + trigger_backfill.assert_called_once_with([dataset]) + + @patch("timeseries.models.Dataset.is_backfilled") + @patch("timeseries.helpers.trigger_backfill") + @patch("timeseries.models.Dataset.objects.get_or_create") + def test_backfill_trigger_on_dataset_creation( + self, mock_get_or_create, mock_trigger_backfill, mock_is_backfilled + ): + mock_is_backfilled.return_value = False + mock_get_or_create.return_value = (Dataset(), True) + + CommitFactory( + commitid="commit1", + repository_id=self.repo.pk, + branch="main", + timestamp=datetime(2022, 1, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + totals={"c": "80.00"}, + ) + + # Invoke the logic + repository_coverage_measurements_with_fallback( + self.repo, + Interval.INTERVAL_1_DAY, + start_date=datetime(2021, 12, 31, 0, 0, 0, tzinfo=timezone.utc), + end_date=datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + ) + + # Ensure get_or_create was called with the expected arguments + mock_get_or_create.assert_called_once_with( + name=MeasurementName.COVERAGE.value, + repository_id=self.repo.pk, + ) + + # Ensure trigger_backfill was called when a new Dataset was created + mock_trigger_backfill.assert_called_once_with( + [mock_get_or_create.return_value[0]] + ) + + @patch("timeseries.models.Dataset.is_backfilled") + @patch("timeseries.helpers.trigger_backfill") + @patch("timeseries.models.Dataset.objects.get_or_create") + def test_backfill_not_triggered_if_no_dataset_creation( + self, mock_get_or_create, mock_trigger_backfill, mock_is_backfilled + ): + mock_is_backfilled.return_value = False + mock_get_or_create.return_value = (Dataset(), False) + + CommitFactory( + commitid="commit1", + repository_id=self.repo.pk, + branch="main", + timestamp=datetime(2022, 1, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + totals={"c": "80.00"}, + ) + + repository_coverage_measurements_with_fallback( + self.repo, + Interval.INTERVAL_1_DAY, + start_date=datetime(2021, 12, 31, 0, 0, 0, tzinfo=timezone.utc), + end_date=datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + ) + + mock_get_or_create.assert_called_once_with( + name=MeasurementName.COVERAGE.value, + repository_id=self.repo.pk, + ) + + mock_trigger_backfill.assert_not_called() + + +@pytest.mark.skipif( + not settings.TIMESERIES_ENABLED, reason="requires timeseries data storage" +) +class OwnerCoverageMeasurementsWithFallbackTest(TestCase): + databases = {"default", "timeseries"} + + def setUp(self): + self.owner = OwnerFactory() + self.repo1 = RepositoryFactory(author=self.owner) + self.repo2 = RepositoryFactory(author=self.owner) + + @patch("timeseries.models.Dataset.is_backfilled") + def test_backfilled_datasets(self, is_backfilled): + is_backfilled.return_value = True + + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo1.author_id, + repo_id=self.repo1.pk, + measurable_id=str(self.repo1.pk), + timestamp=datetime(2022, 1, 1, 1, 0, 0), + value=80.0, + branch="main", + commit_sha="commit1", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo1.author_id, + repo_id=self.repo1.pk, + measurable_id=str(self.repo1.pk), + timestamp=datetime(2022, 1, 1, 2, 0, 0), + value=85.0, + branch="main", + commit_sha="commit2", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo1.author_id, + repo_id=self.repo1.pk, + measurable_id=str(self.repo1.pk), + timestamp=datetime(2022, 1, 1, 3, 0, 0), + value=90.0, + branch="other", + commit_sha="commit3", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo1.author_id, + repo_id=self.repo1.pk, + measurable_id=str(self.repo1.pk), + timestamp=datetime(2022, 1, 2, 1, 0, 0), + value=80.0, + branch="main", + commit_sha="commit4", + ) + + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo2.author_id, + repo_id=self.repo2.pk, + measurable_id=str(self.repo2.pk), + timestamp=datetime(2022, 1, 1, 1, 0, 0), + value=80.0, + branch="main", + commit_sha="commit1", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo2.author_id, + repo_id=self.repo2.pk, + measurable_id=str(self.repo2.pk), + timestamp=datetime(2022, 1, 1, 2, 0, 0), + value=85.0, + branch="main", + commit_sha="commit2", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo2.author_id, + repo_id=self.repo2.pk, + measurable_id=str(self.repo2.pk), + timestamp=datetime(2022, 1, 1, 3, 0, 0), + value=90.0, + branch="other", + commit_sha="commit3", + ) + MeasurementFactory( + name=MeasurementName.COVERAGE.value, + owner_id=self.repo2.author_id, + repo_id=self.repo2.pk, + measurable_id=str(self.repo2.pk), + timestamp=datetime(2022, 1, 2, 1, 0, 0), + value=90.0, + branch="main", + commit_sha="commit4", + ) + + DatasetFactory( + name=MeasurementName.COVERAGE.value, + repository_id=self.repo1.pk, + ) + DatasetFactory( + name=MeasurementName.COVERAGE.value, + repository_id=self.repo2.pk, + ) + + res = owner_coverage_measurements_with_fallback( + owner=self.owner, + repo_ids=[self.repo1.pk, self.repo2.pk], + interval=Interval.INTERVAL_1_DAY, + start_date=datetime(2021, 12, 31, 0, 0, 0, tzinfo=timezone.utc), + end_date=datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + ) + assert list(res) == [ + { + # aggregates over 2 measurements on main branch + "timestamp_bin": datetime(2022, 1, 1, 0, 0, tzinfo=timezone.utc), + "avg": 82.5, + "min": 80.0, + "max": 85.0, + }, + { + # aggregates over 1 measurement (commit4) + "timestamp_bin": datetime(2022, 1, 2, 0, 0, tzinfo=timezone.utc), + "avg": 85.0, + "min": 80.0, + "max": 90.0, + }, + ] + res = owner_coverage_measurements_with_fallback( + owner=self.owner, + repo_ids=[self.repo1.pk], + interval=Interval.INTERVAL_1_DAY, + start_date=datetime(2021, 12, 31, 0, 0, 0, tzinfo=timezone.utc), + end_date=datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + ) + assert list(res) == [ + { + # aggregates over 2 measurements on main branch + "timestamp_bin": datetime(2022, 1, 1, 0, 0, tzinfo=timezone.utc), + "avg": 82.5, + "min": 80.0, + "max": 85.0, + }, + { + # aggregates over 1 measurement (commit4) + "timestamp_bin": datetime(2022, 1, 2, 0, 0, tzinfo=timezone.utc), + "avg": 80.0, + "min": 80.0, + "max": 80.0, + }, + ] + + @patch("timeseries.models.Dataset.is_backfilled") + def test_unbackfilled_dataset(self, is_backfilled): + is_backfilled.return_value = False + + CommitFactory( + commitid="commit1", + repository_id=self.repo1.pk, + branch="main", + timestamp=datetime(2022, 1, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + totals={ + "c": "80.00", + }, + ) + CommitFactory( + commitid="commit2", + repository_id=self.repo1.pk, + branch="main", + timestamp=datetime(2022, 1, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + totals={"c": "85.00"}, + ) + CommitFactory( + commitid="commit3", + repository_id=self.repo1.pk, + branch="other", + timestamp=datetime(2022, 1, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + totals={"c": "90.00"}, + ) + CommitFactory( + commitid="commit4", + repository_id=self.repo1.pk, + branch="main", + timestamp=datetime(2022, 1, 2, 1, 0, 0, 0, tzinfo=timezone.utc), + totals={ + "c": "80.00", + }, + ) + + CommitFactory( + commitid="commit1", + repository_id=self.repo2.pk, + branch="main", + timestamp=datetime(2022, 1, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + totals={ + "c": "80.00", + }, + ) + CommitFactory( + commitid="commit2", + repository_id=self.repo2.pk, + branch="main", + timestamp=datetime(2022, 1, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + totals={"c": "85.00"}, + ) + CommitFactory( + commitid="commit3", + repository_id=self.repo2.pk, + branch="other", + timestamp=datetime(2022, 1, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + totals={"c": "90.00"}, + ) + CommitFactory( + commitid="commit4", + repository_id=self.repo2.pk, + branch="main", + timestamp=datetime(2022, 1, 2, 1, 0, 0, 0, tzinfo=timezone.utc), + totals={ + "c": "90.00", + }, + ) + + DatasetFactory( + name=MeasurementName.COVERAGE.value, + repository_id=self.repo1.pk, + ) + DatasetFactory( + name=MeasurementName.COVERAGE.value, + repository_id=self.repo2.pk, + ) + + res = owner_coverage_measurements_with_fallback( + owner=self.owner, + repo_ids=[self.repo1.pk, self.repo2.pk], + interval=Interval.INTERVAL_1_DAY, + start_date=datetime(2021, 12, 31, 0, 0, 0, tzinfo=timezone.utc), + end_date=datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + ) + assert list(res) == [ + { + # aggregates over 2 commits on main branch + "timestamp_bin": datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "avg": 82.5, + "min": 80.0, + "max": 85.0, + }, + { + # aggregates over 1 commit (commit4) + "timestamp_bin": datetime(2022, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + "avg": 85.0, + "min": 80.0, + "max": 90.0, + }, + ] + res = owner_coverage_measurements_with_fallback( + owner=self.owner, + repo_ids=[self.repo1.pk], + interval=Interval.INTERVAL_1_DAY, + start_date=datetime(2021, 12, 31, 0, 0, 0, tzinfo=timezone.utc), + end_date=datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + ) + assert list(res) == [ + { + # aggregates over 2 commits on main branch + "timestamp_bin": datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "avg": 82.5, + "min": 80.0, + "max": 85.0, + }, + { + # aggregates over 1 commit (commit4) + "timestamp_bin": datetime(2022, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + "avg": 80.0, + "min": 80.0, + "max": 80.0, + }, + ] + + @patch("timeseries.helpers.trigger_backfill") + def test_no_dataset(self, trigger_backfill): + CommitFactory( + commitid="commit1", + repository_id=self.repo1.pk, + branch="main", + timestamp=datetime(2022, 1, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + totals={ + "c": "80.00", + }, + ) + CommitFactory( + commitid="commit2", + repository_id=self.repo1.pk, + branch="main", + timestamp=datetime(2022, 1, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + totals={"c": "85.00"}, + ) + CommitFactory( + commitid="commit3", + repository_id=self.repo1.pk, + branch="other", + timestamp=datetime(2022, 1, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + totals={"c": "90.00"}, + ) + CommitFactory( + commitid="commit4", + repository_id=self.repo1.pk, + branch="main", + timestamp=datetime(2022, 1, 2, 1, 0, 0, 0, tzinfo=timezone.utc), + totals={ + "c": "80.00", + }, + ) + + CommitFactory( + commitid="commit1", + repository_id=self.repo2.pk, + branch="main", + timestamp=datetime(2022, 1, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + totals={ + "c": "80.00", + }, + ) + CommitFactory( + commitid="commit2", + repository_id=self.repo2.pk, + branch="main", + timestamp=datetime(2022, 1, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + totals={"c": "85.00"}, + ) + CommitFactory( + commitid="commit3", + repository_id=self.repo2.pk, + branch="other", + timestamp=datetime(2022, 1, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + totals={"c": "90.00"}, + ) + CommitFactory( + commitid="commit4", + repository_id=self.repo2.pk, + branch="main", + timestamp=datetime(2022, 1, 2, 1, 0, 0, 0, tzinfo=timezone.utc), + totals={ + "c": "90.00", + }, + ) + + res = owner_coverage_measurements_with_fallback( + owner=self.owner, + repo_ids=[self.repo1.pk, self.repo2.pk], + interval=Interval.INTERVAL_1_DAY, + start_date=datetime(2021, 12, 31, 0, 0, 0, tzinfo=timezone.utc), + end_date=datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + ) + assert list(res) == [ + { + # aggregates over 2 commits on main branch + "timestamp_bin": datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "avg": 82.5, + "min": 80.0, + "max": 85.0, + }, + { + # aggregates over 1 measurement (commit4) + "timestamp_bin": datetime(2022, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + "avg": 85.0, + "min": 80.0, + "max": 90.0, + }, + ] + + datasets = list( + Dataset.objects.filter( + name=MeasurementName.COVERAGE.value, + repository_id__in=[self.repo1.pk, self.repo2.pk], + ) + ) + assert len(datasets) == 2 + created_datasets = [ + ( + ds.repository_id, + ds.name, + ds.is_backfilled(), + ) + for ds in datasets + ] + + assert ( + self.repo1.pk, + MeasurementName.COVERAGE.value, + False, + ) in created_datasets + assert ( + self.repo2.pk, + MeasurementName.COVERAGE.value, + False, + ) in created_datasets + + try: + trigger_backfill.assert_called_once_with(datasets) + except AssertionError: + datasets.reverse() + trigger_backfill.assert_called_once_with(datasets) + + res = owner_coverage_measurements_with_fallback( + owner=self.owner, + repo_ids=[self.repo1.pk], + interval=Interval.INTERVAL_1_DAY, + start_date=datetime(2021, 12, 31, 0, 0, 0, tzinfo=timezone.utc), + end_date=datetime(2022, 1, 3, 0, 0, 0, tzinfo=timezone.utc), + ) + assert list(res) == [ + { + # aggregates over 2 commits on main branch + "timestamp_bin": datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "avg": 82.5, + "min": 80.0, + "max": 85.0, + }, + { + # aggregates over 1 measurement (commit4) + "timestamp_bin": datetime(2022, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + "avg": 80.0, + "min": 80.0, + "max": 80.0, + }, + ] diff --git a/apps/codecov-api/upload/__init__.py b/apps/codecov-api/upload/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/upload/constants.py b/apps/codecov-api/upload/constants.py new file mode 100644 index 0000000000..1d6808b1c8 --- /dev/null +++ b/apps/codecov-api/upload/constants.py @@ -0,0 +1,222 @@ +ci = { + "travis": { + "title": "Travis-CI", + "icon": "travis", + "require_token_when_public": False, + "instructions": "travis", + "build_url": "https://travis-ci.com/{owner.username}/{repo.name}/jobs/{upload.job_code}", + }, + "azure_pipelines": { + "title": "Azure", + "icon": "azure_pipelines", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "docker": { + "title": "Docker", + "icon": "custom", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "buildbot": { + "title": "Buildbot", + "icon": "buildbot", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "cirrus-ci": { + "title": "Cirrus CI", + "icon": "cirrus-ci", + "require_token_when_public": False, + "instructions": "generic", + "build_url": "https://cirrus-ci.com/build/{upload.build_code}", + }, + "codebuild": { + "title": "AWS Codebuild", + "icon": "codebuild", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "codefresh": { + "title": "Codefresh", + "icon": "custom", + "require_token_when_public": True, + "instructions": "generic", + "build_url": "https://g.codefresh.io/repositories/{owner.username}/{repo.name}/builds/{upload.build_code}", + }, + "bitbucket": { + "title": "Bitbucket Pipelines", + "icon": "bitbucket", + "require_token_when_public": False, + "instructions": "generic", + "build_url": "https://bitbucket.org/{owner.username}/{repo.name}/addon/pipelines/home#!/results/{upload.job_code}", + }, + "circleci": { + "title": "CircleCI", + "icon": "circleci", + "require_token_when_public": False, + "instructions": "circleci", + "build_url": "https://circleci.com/{service_short}/{owner.username}/{repo.name}/{upload.build_code}#tests/containers/{upload.job_code}", + }, + "buddybuild": { + "title": "buddybuild", + "icon": "custom", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "buddy": { + "title": "buddy", + "icon": "custom", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "github-actions": { + "title": "GitHub Actions", + "icon": "github-actions", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "solano": { + "title": "Solano", + "icon": "custom", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "teamcity": { + "title": "TeamCity", + "icon": "teamcity", + "require_token_when_public": True, + "instructions": "teamcity", + "build_url": None, + }, + "appveyor": { + "title": "AppVeyor", + "icon": "appveyor", + "require_token_when_public": False, + "instructions": "appveyor", + "build_url": None, + }, + "wercker": { + "title": "Wercker", + "icon": "wercker", + "require_token_when_public": True, + "instructions": "generic", + "build_url": "https://app.wercker.com/#build/{upload.build_code}", + }, + "shippable": { + "title": "Shippable", + "icon": "shippable", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, # provided in upload, + }, + "codeship": { + "title": "Codeship", + "icon": "codeship", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, # provided in upload, + }, + "drone.io": { + "title": "Drone.io", + "icon": "drone.io", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, # provided in upload, + }, + "jenkins": { + "title": "Jenkins", + "icon": "jenkins", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, # provided in upload, + }, + "semaphore": { + "title": "Semaphore", + "icon": "semaphore", + "require_token_when_public": True, + "instructions": "generic", + "build_url": "https://semaphoreapp.com/{owner.username}/{repo.name}/branches/{commit.branch}/builds/{upload.build_code}", + }, + "gitlab": { + "title": "GitLab CI", + "icon": "gitlab", + "require_token_when_public": True, + "instructions": "generic", + "build_url": "https://gitlab.com/{owner.username}/{repo.name}/builds/{upload.build_code}", + }, + "bamboo": { + "title": "Bamboo", + "icon": "bamboo", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "buildkite": { + "title": "BuildKite", + "icon": "buildkite", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, # provided in upload, + }, + "bitrise": { + "title": "Bitrise", + "icon": "bitrise", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, # provided in upload, + }, + "greenhouse": { + "title": "Greenhouse", + "icon": "greenhouse", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "heroku": { + "title": "Heroku", + "icon": "heroku", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "woodpecker": { + "title": "WoodpeckerCI", + "icon": "custom", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, # provided in upload, + }, + "custom": { + "title": "Custom", + "icon": "custom", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, # provided in upload, + }, +} + +errors = { + "travis": { + "tokenless-general-error": "\nERROR: Tokenless uploads are only supported for public repositories on Travis that can be verified through the Travis API. Please use an upload token if your repository is private and specify it via the -t flag. You can find the token for this repository at the url below on codecov.io (login required):\n\nRepo token: {}\nDocumentation: https://docs.codecov.io/docs/about-the-codecov-bash-uploader#section-upload-token", + "tokenless-stale-build": "\nERROR: The coverage upload was rejected because the build is out of date. Please make sure the build is not stale for uploads to process correctly.", + "tokenless-bad-status": "\nERROR: The build status does not indicate that the current build is in progress. Please make sure the build is in progress or was finished within the past 4 minutes to ensure reports upload properly.", + } +} + +global_upload_token_providers = [ + "github", + "github_enterprise", + "gitlab", + "gitlab_enterprise", + "bitbucket", + "bitbucket_server", +] diff --git a/apps/codecov-api/upload/helpers.py b/apps/codecov-api/upload/helpers.py new file mode 100644 index 0000000000..380b17cc1a --- /dev/null +++ b/apps/codecov-api/upload/helpers.py @@ -0,0 +1,841 @@ +import logging +import re +from json import dumps +from typing import Any, Dict, Optional +from urllib.parse import urlparse + +import jwt +from asgiref.sync import async_to_sync +from cerberus import Validator +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from django.db.models import Q +from django.http import HttpRequest +from django.utils import timezone +from jwt import PyJWKClient, PyJWTError +from redis import Redis +from rest_framework.exceptions import NotFound, Throttled, ValidationError +from shared.github import InvalidInstallationError, get_github_integration_token +from shared.helpers.redis import get_redis_connection +from shared.plan.service import PlanService +from shared.reports.enums import UploadType +from shared.torngit.base import TorngitBaseAdapter +from shared.torngit.exceptions import TorngitClientError, TorngitObjectNotFoundError +from shared.typings.oauth_token_types import OauthConsumerToken +from shared.upload.utils import query_monthly_coverage_measurements + +from codecov_auth.models import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + SERVICE_GITHUB, + SERVICE_GITHUB_ENTERPRISE, + GithubAppInstallation, + Owner, + Plan, +) +from core.models import Commit, Repository +from reports.models import CommitReport, ReportSession +from services.analytics import AnalyticsService +from services.repo_providers import RepoProviderService +from services.task import TaskService +from upload.tokenless.tokenless import TokenlessUploadHandler +from utils import is_uuid +from utils.config import get_config +from utils.encryption import encryptor + +from .constants import ci, global_upload_token_providers + +is_pull_noted_in_branch = re.compile(r".*(pull|pr)\/(\d+).*") + +# Valid values are `https://dev.azure.com/username/` or `https://username.visualstudio.com/` +# May be URL-encoded, so ':' can be '%3A' and '/' can be '%2F' +# Username is alphanumeric with '_' and '-' +_valid_azure_server_uri = r"^https?(?:://|%3A%2F%2F)(?:dev.azure.com(?:/|%2F)[a-zA-Z0-9_-]+(?:/|%2F)|[a-zA-Z0-9_-]+.visualstudio.com(?:/|%2F))$" + +log = logging.getLogger(__name__) +redis = get_redis_connection() + + +def parse_params(data: Dict[str, Any]) -> Dict[str, Any]: + """ + This function will validate the input request parameters and do some additional parsing/tranformation of the params. + """ + + # filter out empty values from the data; this makes parsing and setting defaults a bit easier + non_empty_data = { + key: value for key, value in data.items() if value not in [None, ""] + } + + global_tokens = get_global_tokens() + params_schema = { + # --- The following parameters are populated in the code based on request data, settings, etc. + "owner": { # owner username, we set this by splitting the value of "slug" on "/" if provided + "type": "string", + "nullable": True, + "default_setter": ( + lambda document: ( + document.get("slug") + .rsplit("/", 1)[0] + .replace( + "/", ":" + ) # we use ':' as separator for gitlab subgroups internally + if document.get("slug") + and len(document.get("slug").rsplit("/", 1)) == 2 + else None + ) + ), + }, + # repo name, we set this by parsing the value of "slug" if provided + "repo": { + "type": "string", + "nullable": True, + "default_setter": ( + lambda document: ( + document.get("slug").rsplit("/", 1)[1] + if document.get("slug") + and len(document.get("slug").rsplit("/", 1)) == 2 + else None + ) + ), + }, + # indicates whether the token provided is a global upload token rather than a repository upload token + # note: this needs to go before the "service" field in the schema so we can use is when determining the service value to use + "using_global_token": { + "type": "boolean", + "default_setter": ( + lambda document: ( + True + if document.get("token") and document.get("token") in global_tokens + else False + ) + ), + }, + # --- The following parameters are expected to be provided in the upload request. + "version": {"type": "string", "required": True, "allowed": ["v2", "v4"]}, + # commit SHA + "commit": { + "type": "string", + "required": True, + "regex": r"^\d+:\w{12}|\w{40}$", + "coerce": lambda value: value.lower(), + }, + # if this is true, then we won't do any merge commit parsing + "_did_change_merge_commit": {"type": "boolean"}, + "slug": {"type": "string", "regex": r"^[\w\-\.\~\/]+\/[\w\-\.]{1,255}$"}, + # repository upload token + "token": { + "type": "string", + "anyof": [ + {"regex": r"^[0-9a-f]{8}(-?[0-9a-f]{4}){3}-?[0-9a-f]{12}$"}, # UUID + {"regex": r"(^[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*$)"}, # JWT + {"allowed": list(global_tokens.keys())}, + ], + }, + # name of the CI service used, must be a name in the list of CI services we support + "service": { + "type": "string", + "nullable": True, + "allowed": list(ci.keys()) + list(global_tokens.values()), + "coerce": ( + lambda value: "travis" if value == "travis-org" else value, + ), # if "travis-org" was passed as the service rename it to "travis" before validating + "default_setter": ( + lambda document: ( + global_tokens[document.get("token")] + if document.get("using_global_token") + else None + ) + ), + }, + # pull request number + # if a value is passed to the "pull_request" field and not to "pr", we'll use that to set the value of this field + "pr": { + "type": "string", + "regex": r"^(\d+|false|null|undefined|true)$", + "nullable": True, + "default_setter": (lambda document: document.get("pull_request")), + "coerce": ( + lambda value: None if value in ["false", "null", "undefined"] else value + ), + }, + # pull request number + # "deprecated" in the sense that if a value is passed to this field, we'll use it to set "pr" and use that field instead + "pull_request": { # pull request number + "type": "string", + "regex": r"^(\d+|false|null|undefined|true)$", + "nullable": True, + "coerce": ( + lambda value: ( + None if value in ["false", "null", "undefined", "true"] else value + ) + ), + }, + "build_url": {"type": "string", "regex": r"^https?\:\/\/(.{,200})"}, + "flags": {"type": "string", "regex": r"^[\w\.\-\,]+$"}, + "branch": { + "type": "string", + "nullable": True, + "coerce": ( + lambda value: ( + None + if value == "HEAD" + # if prefixed with "origin/" or "refs/heads", the prefix will be removed + else ( + value[7:] + if value[:7] == "origin/" + else value[11:] + if value[:11] == "refs/heads/" + else value + ) + ), + ), + }, + "tag": {"type": "string"}, + # if a value is passed to "travis_job_id" and not to "job", we'll use that to set the value of this field + "job": { + "type": "string", + "nullable": True, + "default_setter": (lambda document: document.get("travis_job_id")), + }, + # "deprecated" in the sense that if a value is passed to this field, we'll use it to set "job" and use that field instead + "travis_job_id": {"type": "string", "nullable": True, "empty": True}, + "build": { + "type": "string", + "nullable": True, + "coerce": ( + lambda value: ( + None if value in ["null", "undefined", "none", "nil"] else value + ) + ), + }, + "name": {"type": "string"}, + "package": {"type": "string"}, + "s3": {"type": "integer"}, + "yaml": { + "type": "string" + }, # file path to custom location of codecov.yml in repo + "url": {"type": "string"}, # custom location where report is found + "parent": {"type": "string"}, + "project": {"type": "string"}, + "server_uri": { + "type": "string", + "regex": _valid_azure_server_uri, + }, + "root": {"type": "string"}, # deprecated + "storage_path": {"type": "string"}, + } + + v = Validator(params_schema, allow_unknown=True) + if not v.validate(non_empty_data): + raise ValidationError(v.errors) + # override service to the one from the global token if global token is in use + if v.document.get("using_global_token"): + v.document["service"] = global_tokens[v.document.get("token")] + # return validated data, including coerced values + return v.document + + +def get_repo_with_github_actions_oidc_token(token: str) -> Repository: + unverified_contents = jwt.decode(token, options={"verify_signature": False}) + token_issuer = str(unverified_contents.get("iss")) + parsed_url = urlparse(token_issuer) + if parsed_url.hostname == "token.actions.githubusercontent.com": + service = "github" + jwks_url = "https://token.actions.githubusercontent.com/.well-known/jwks" + else: + service = "github_enterprise" + github_enterprise_url = get_config("github_enterprise", "url") + if not github_enterprise_url: + raise ValidationError("GitHub Enterprise URL configuration is not set") + # remove trailing slashes if present + github_enterprise_url = re.sub(r"/+$", "", github_enterprise_url) + jwks_url = f"{github_enterprise_url}/_services/token/.well-known/jwks" + jwks_client = PyJWKClient(jwks_url) + signing_key = jwks_client.get_signing_key_from_jwt(token) + data = jwt.decode( + token, + signing_key.key, + algorithms=["RS256"], + audience=[settings.CODECOV_API_URL, settings.CODECOV_URL], + ) + repo = str(data.get("repository")).split("/")[-1] + repository = Repository.objects.get( + author__service=service, + name=repo, + author__username=data.get("repository_owner"), + ) + return repository + + +def determine_repo_for_upload(upload_params: Dict[str, Any]) -> Repository: + token = upload_params.get("token") + using_global_token = upload_params.get("using_global_token") + service = upload_params.get("service") + + if token and not using_global_token: + if is_uuid(token): + try: + repository = Repository.objects.get(upload_token=token) + except ObjectDoesNotExist: + raise NotFound( + f"Could not find a repository associated with upload token {token}" + ) + elif service == "github-actions": + try: + repository = get_repo_with_github_actions_oidc_token(token) + except PyJWTError: + raise ValidationError( + "Could not validate upload request using Github token" + ) + elif service: + if using_global_token: + git_service = service + else: + git_service = TokenlessUploadHandler(service, upload_params).verify_upload() + try: + repository = Repository.objects.get( + author__service=git_service, + name=upload_params.get("repo"), + author__username=upload_params.get("owner"), + ) + except ObjectDoesNotExist: + raise NotFound("Could not find a repository, try using repo upload token") + else: + raise ValidationError( + "Need either a token or service to determine target repository" + ) + + return repository + + """ + TODO: add CI verification and repo retrieval from CI + elif service: + if not using_global_token: + # verify CI TODO + + # Get repo info from CI TODO + """ + + +def determine_upload_branch_to_use( + upload_params: Dict[str, Any], repo_default_branch: str +) -> str | None: + """ + Do processing on the upload request parameters to determine which branch to use for the upload: + - If no branch or PR were provided, use the default branch for the repository. + - If a branch was provided and the branch name contains "pull" or "pr" followed by digits, don't use the branch name. + In "determine_upload_pr_to_use" we'll extract the digits from the branch name and use that as the pr number. + - Otherwise, use the value provided in the request parameters. + """ + upload_params_branch = upload_params.get("branch") + upload_params_pr = upload_params.get("pr") + + if not upload_params_branch and not upload_params_pr: + return repo_default_branch + elif upload_params_branch and not is_pull_noted_in_branch.match( + upload_params_branch + ): + return upload_params_branch + else: + return None + + +def determine_upload_pr_to_use(upload_params: Dict[str, Any]) -> str | None: + """ + Do processing on the upload request parameters to determine which PR to use for the upload: + - If a branch was provided and the branch name contains "pull" or "pr" followed by digits, extract the digits and use that as the PR number. + - Otherwise, use the value provided in the request parameters. + """ + pullid = is_pull_noted_in_branch.match(upload_params.get("branch") or "") + if pullid: + return pullid.groups()[1] + # The value of pr can be "true" and we use that info when determining upload branch, however we don't want to save that value to the db + elif upload_params.get("pr") == "true": + return None + else: + return upload_params.get("pr") + + +def ghapp_installation_id_to_use(repository: Repository) -> Optional[str]: + if ( + repository.service != SERVICE_GITHUB + and repository.service != SERVICE_GITHUB_ENTERPRISE + ): + return None + + gh_app_default_installation: GithubAppInstallation = ( + repository.author.github_app_installations.filter( + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME + ).first() + ) + if ( + gh_app_default_installation + and gh_app_default_installation.is_repo_covered_by_integration(repository) + ): + return gh_app_default_installation.installation_id + elif repository.using_integration and repository.author.integration_id: + # THIS FLOW IS DEPRECATED + # it will (hopefully) be removed after the ghapp installation work is complete + # and the data is backfilles appropriately + return repository.author.integration_id + + +def try_to_get_best_possible_bot_token( + repository: Repository, +) -> OauthConsumerToken | Dict: + ghapp_installation_id = ghapp_installation_id_to_use(repository) + if ghapp_installation_id is not None: + try: + github_token = get_github_integration_token( + repository.author.service, + integration_id=ghapp_installation_id, + ) + return dict(key=github_token) + except InvalidInstallationError: + log.warning( + "Invalid installation error", + extra=dict( + service=repository.author.service, + integration_id=ghapp_installation_id, + ), + ) + # now we'll fallback to trying an OAuth token + service = repository.author.service + if repository.bot is not None and repository.bot.oauth_token is not None: + log.info( + "Repo has specific bot", + extra=dict(repoid=repository.repoid, botid=repository.bot.ownerid), + ) + return encryptor.decrypt_token(repository.bot.oauth_token) + if ( + repository.author.bot is not None + and repository.author.bot.oauth_token is not None + ): + log.info( + "Repo Owner has specific bot", + extra=dict( + repoid=repository.repoid, + botid=repository.author.bot.ownerid, + ownerid=repository.author.ownerid, + ), + ) + return encryptor.decrypt_token(repository.author.bot.oauth_token) + if repository.author.oauth_token is not None: + log.info( + "Using repository owner as bot fallback", + extra=dict(repoid=repository.repoid, ownerid=repository.author.ownerid), + ) + return encryptor.decrypt_token(repository.author.oauth_token) + if not repository.private: + log.info( + "Using tokenless bot as bot fallback", + extra=dict(repoid=repository.repoid, ownerid=repository.author.ownerid), + ) + return get_config(service, "bots", "tokenless") + return None + + +@async_to_sync +async def _get_git_commit_data( + adapter: TorngitBaseAdapter, commit: str, token: Optional[OauthConsumerToken | Dict] +) -> Dict[str, Any]: + return await adapter.get_commit(commit, token) + + +def determine_upload_commit_to_use( + upload_params: Dict[str, Any], repository: Repository +) -> str: + """ + Do processing on the upload request parameters to determine which commit to use for the upload: + - If this is a merge commit on github, use the first commit SHA in the merge commit message. + - Otherwise, use the value provided in the request parameters. + """ + # Check if this is a merge commit and, if so, use the commitid of the commit being merged into per the merge commit message. + # See https://docs.codecov.io/docs/merge-commits for more context. + service = repository.author.service + commitid = upload_params.get("commit", "") + if service.startswith("github") and not upload_params.get( + "_did_change_merge_commit" + ): + token = try_to_get_best_possible_bot_token(repository) + if token is None: + return commitid + # Get the commit message from the git provider and check if it's structured like a merge commit message + try: + adapter = RepoProviderService().get_adapter( + repository.author, repository, use_ssl=True, token=token + ) + git_commit_data = _get_git_commit_data(adapter, commitid, token) + except TorngitObjectNotFoundError: + log.warning( + "Unable to fetch commit. Not found", + extra=dict(commit=commitid), + ) + return commitid + except TorngitClientError: + log.warning("Unable to fetch commit", extra=dict(commit=commitid)) + return commitid + + git_commit_message = git_commit_data.get("message", "").strip() + is_merge_commit = re.match(r"^Merge\s\w{40}\sinto\s\w{40}$", git_commit_message) + + if is_merge_commit: + # If the commit message says "Merge A into B", we'll extract A and use that as the commitid for this upload + new_commit_id = git_commit_message.split(" ")[1] + log.info( + "Upload is for a merge commit, updating commit id for upload", + extra=dict( + commit=commitid, + commit_message=git_commit_message, + new_commit=new_commit_id, + ), + ) + return new_commit_id + + # If it's not a merge commit we'll just use the commitid provided in the upload parameters + return commitid + + +def insert_commit( + commitid: str, + branch: str, + pr: int, + repository: Repository, + owner: Owner, + parent_commit_id: Optional[str] = None, +) -> Commit: + commit, was_created = Commit.objects.defer("_report").get_or_create( + commitid=commitid, + repository=repository, + defaults={ + "branch": branch, + "pullid": pr, + "merged": False if pr is not None else None, + "parent_commit_id": parent_commit_id, + "state": "pending", + }, + ) + + edited = False + if parent_commit_id and commit.parent_commit_id is None: + commit.parent_commit_id = parent_commit_id + edited = True + if branch and commit.branch != branch: + # A branch head may have been moved; this allows commits to be "moved" + commit.branch = branch + edited = True + if edited: + commit.save(update_fields=["parent_commit_id", "branch"]) + return commit + + +def get_global_tokens() -> Dict[str | None, Any]: + """ + Enterprise only: check the config to see if global tokens were set for this organization's uploads. + + Returns dict with structure {: } + """ + tokens = { + get_config(service, "global_upload_token"): service + for service in global_upload_token_providers + if get_config(service, "global_upload_token") + } # should be empty if we're not in enterprise + return tokens + + +def check_commit_upload_constraints(commit: Commit) -> None: + if settings.UPLOAD_THROTTLING_ENABLED and commit.repository.private: + owner = _determine_responsible_owner(commit.repository) + plan_service = PlanService(current_org=owner) + limit = plan_service.monthly_uploads_limit + if limit is not None: + did_commit_uploads_start_already = ReportSession.objects.filter( + report__commit=commit + ).exists() + if not did_commit_uploads_start_already: + if ( + query_monthly_coverage_measurements(plan_service=plan_service) + >= limit + ): + log.warning( + "User exceeded its limits for usage", + extra=dict(ownerid=owner.ownerid, repoid=commit.repository_id), + ) + message = "Request was throttled. Throttled due to limit on private repository coverage uploads to Codecov on a free plan. Please upgrade your plan if you require additional uploads this month." + raise Throttled(detail=message) + + +def validate_upload( + upload_params: Dict[str, Any], repository: Repository, redis: Redis +) -> None: + """ + Make sure the upload can proceed and, if so, activate the repository if needed. + """ + + validate_activated_repo(repository) + # Make sure repo hasn't moved + if not repository.name: + raise ValidationError( + "This repository has moved or was deleted. Please login to Codecov to retrieve a new upload token." + ) + + # Check if there are already too many sessions associated with this commit + try: + commit = Commit.objects.get( + commitid=upload_params.get("commit"), repository=repository + ) + new_session_count = ReportSession.objects.filter( + ~Q(state="error"), + ~Q(upload_type=UploadType.CARRIEDFORWARD.db_name), + report__commit=commit, + ).count() + session_count = (commit.totals.get("s") if commit.totals else 0) or 0 + current_upload_limit = get_config("setup", "max_sessions") or 150 + if new_session_count > current_upload_limit: + if session_count <= current_upload_limit: + log.info( + "Old session count would not have blocked this upload", + extra=dict( + commit=upload_params.get("commit"), + session_count=session_count, + repoid=repository.repoid, + old_session_count=session_count, + new_session_count=new_session_count, + ), + ) + log.warning( + "Too many uploads to this commit", + extra=dict( + commit=upload_params.get("commit"), + session_count=session_count, + repoid=repository.repoid, + ), + ) + raise ValidationError("Too many uploads to this commit.") + elif session_count > current_upload_limit: + log.info( + "Old session count would block this upload", + extra=dict( + commit=upload_params.get("commit"), + session_count=session_count, + repoid=repository.repoid, + old_session_count=session_count, + new_session_count=new_session_count, + ), + ) + except Commit.DoesNotExist: + pass + + # Check if this repository is blacklisted and not allowed to upload + if redis.sismember("flags.disable_tasks", repository.repoid): + raise ValidationError( + "Uploads rejected for this project. Please contact Codecov staff for more details. Sorry for the inconvenience." + ) + + # Make sure the repository author has enough repo credits to upload reports + if ( + repository.private + and not repository.activated + and not bool(get_config("setup", "enterprise_license", default=False)) + ): + owner = _determine_responsible_owner(repository) + + # If author is on per repo billing, check their repo credits + if ( + owner.plan not in Plan.objects.values_list("name", flat=True) + and owner.repo_credits <= 0 + ): + raise ValidationError( + "Sorry, but this team has no private repository credits left." + ) + + if not repository.activated: + AnalyticsService().account_activated_repository_on_upload( + repository.author.ownerid, repository + ) + + if ( + not repository.activated + or not repository.active + or repository.deleted + or not repository.coverage_enabled + ): + # Activate the repository + repository.activated = True + repository.active = True + repository.deleted = False + repository.coverage_enabled = True + repository.save( + update_fields=["activated", "active", "deleted", "coverage_enabled"] + ) + + +def _determine_responsible_owner(repository: Repository) -> Owner: + owner = repository.author + + if owner.service == "gitlab": + # Gitlab authors have a "subgroup" structure, so find the parent group before checking repo credits + while owner.parent_service_id is not None: + owner = Owner.objects.get( + service_id=owner.parent_service_id, service=owner.service + ) + return owner + + +def parse_headers( + headers: Dict[str, Any], upload_params: Dict[str, Any] +) -> Dict[str, Any]: + version = upload_params.get("version") + + # Content disposition header + if headers.get("Content_Disposition") not in (None, "inline"): + raise ValidationError("Setting Content-Disposition is not supported") + + # Content type + if version == "v2": + content_type = "application/x-gzip" + reduced_redundancy = False + else: + content_type = ( + "text/plain" + if headers.get("X_Content_Type", "") in ("", "text/html") + else headers.get("X_Content_Type", "") + ) + reduced_redundancy = ( + False + if "node" in upload_params.get("package", "") + else headers.get("X_Reduced_Redundancy") in ("true", None) + ) + + if content_type not in ("text/plain", "application/x-gzip", "plain/text"): + # Prevent customers from setting headers that could result in a XSS attack + content_type = "text/plain" + + return {"content_type": content_type, "reduced_redundancy": reduced_redundancy} + + +def dispatch_upload_task( + task_arguments: Dict[str, Any], + repository: Repository, + redis: Redis, + report_type: Optional[CommitReport.ReportType] = CommitReport.ReportType.COVERAGE, +) -> None: + # Store task arguments in redis + cache_uploads_eta = get_config(("setup", "cache", "uploads"), default=86400) + if report_type == CommitReport.ReportType.COVERAGE: + repo_queue_key = f"uploads/{repository.repoid}/{task_arguments.get('commit')}" + else: + repo_queue_key = ( + f"uploads/{repository.repoid}/{task_arguments.get('commit')}/{report_type}" + ) + + countdown = 0 + if task_arguments.get("version") == "v4": + countdown = 4 + if ( + report_type == CommitReport.ReportType.BUNDLE_ANALYSIS + or CommitReport.ReportType.TEST_RESULTS + ): + countdown = 4 + + redis.rpush(repo_queue_key, dumps(task_arguments)) + redis.expire( + repo_queue_key, cache_uploads_eta if cache_uploads_eta is not True else 86400 + ) + + if report_type == CommitReport.ReportType.COVERAGE: + latest_upload_key = ( + f"latest_upload/{repository.repoid}/{task_arguments.get('commit')}" + ) + else: + latest_upload_key = f"latest_upload/{repository.repoid}/{task_arguments.get('commit')}/{report_type}" + redis.setex( + latest_upload_key, + 3600, + timezone.now().timestamp(), + ) + commitid = task_arguments.get("commit") + + TaskService().upload( + repoid=repository.repoid, + commitid=commitid, + report_type=str(report_type), + report_code=task_arguments.get("report_code"), + arguments=task_arguments, + countdown=max( + countdown, int(get_config("setup", "upload_processing_delay") or 0) + ), + ) + + +def validate_activated_repo(repository: Repository) -> None: + if repository.active and not repository.activated: + config_url = f"{settings.CODECOV_DASHBOARD_URL}/{repository.author.service}/{repository.author.username}/{repository.name}/config/general" + raise ValidationError( + f"This repository is deactivated. To resume uploading to it, please activate the repository in the codecov UI: {config_url}" + ) + + +# headers["User-Agent"] should look something like this: codecov-cli/0.4.7 or codecov-uploader/0.7.1 +def get_agent_from_headers(headers: Dict[str, Any]) -> str: + try: + return headers["User-Agent"].split("/")[0].split("-")[1] + except Exception as e: + log.warning( + "Error getting agent from user agent header", + extra=dict( + err=str(e), + ), + ) + return "unknown-user-agent" + + +def get_version_from_headers(headers: Dict[str, Any]) -> str: + try: + return headers["User-Agent"].split("/")[1] + except Exception as e: + log.warning( + "Error getting version from user agent header", + extra=dict( + err=str(e), + ), + ) + return "unknown-user-agent" + + +def generate_upload_prometheus_metrics_labels( + action: str, + request: HttpRequest, + is_shelter_request: bool, + endpoint: Optional[str] = None, + repository: Optional[Repository] = None, + position: Optional[str] = None, + upload_version: Optional[str] = None, + include_empty_labels: bool = True, +) -> Dict[str, Any]: + metrics_tags = dict( + agent=get_agent_from_headers(request.headers), + version=get_version_from_headers(request.headers), + action=action, + endpoint=endpoint, + is_using_shelter="yes" if is_shelter_request else "no", + ) + + repo_visibility = None + if repository: + repo_visibility = "private" if repository.private else "public" + + optional_fields = { + "repo_visibility": repo_visibility, + "position": position, + "upload_version": upload_version, + } + + metrics_tags.update( + { + field: value + for field, value in optional_fields.items() + if value or include_empty_labels + } + ) + + return metrics_tags diff --git a/apps/codecov-api/upload/metrics.py b/apps/codecov-api/upload/metrics.py new file mode 100644 index 0000000000..378459e54a --- /dev/null +++ b/apps/codecov-api/upload/metrics.py @@ -0,0 +1,16 @@ +from shared.metrics import Counter + +API_UPLOAD_COUNTER = Counter( + "api_upload", + "Total API upload endpoint requests", + [ + "agent", + "version", + "action", + "endpoint", + "is_using_shelter", + "repo_visibility", + "position", + "upload_version", + ], +) diff --git a/apps/codecov-api/upload/serializers.py b/apps/codecov-api/upload/serializers.py new file mode 100644 index 0000000000..f092c6f7d5 --- /dev/null +++ b/apps/codecov-api/upload/serializers.py @@ -0,0 +1,196 @@ +from typing import Any, Dict, List + +from django.conf import settings +from django.db.models import QuerySet +from rest_framework import serializers +from shared.api_archive.archive import ArchiveService + +from codecov_auth.models import Owner +from core.models import Commit, Repository +from reports.models import CommitReport, ReportResults, ReportSession, RepositoryFlag +from services.task import TaskService + + +class FlagListField(serializers.ListField): + child = serializers.CharField() + + def to_representation(self, data: QuerySet) -> List[str | None]: + return [item.flag_name if item is not None else None for item in data.all()] + + +class UploadSerializer(serializers.ModelSerializer): + flags = FlagListField(required=False) + ci_url = serializers.CharField(source="build_url", required=False, allow_null=True) + version = serializers.CharField(write_only=True, required=False) + url = serializers.SerializerMethodField() + storage_path = serializers.CharField(write_only=True, required=False) + ci_service = serializers.CharField(write_only=True, required=False) + + class Meta: + read_only_fields = ( + "external_id", + "created_at", + "raw_upload_location", + "state", + "provider", + "upload_type", + "url", + ) + fields = read_only_fields + ( + "ci_url", + "flags", + "env", + "name", + "job_code", + "version", + "storage_path", + "ci_service", + ) + model = ReportSession + + raw_upload_location = serializers.SerializerMethodField() + + def get_raw_upload_location(self, obj: ReportSession) -> str: + repo = obj.report.commit.repository + archive_service = ArchiveService(repo) + return archive_service.create_presigned_put(obj.storage_path) + + def get_url(self, obj: ReportSession) -> str: + repository = obj.report.commit.repository + commit = obj.report.commit + return f"{settings.CODECOV_DASHBOARD_URL}/{repository.author.service}/{repository.author.username}/{repository.name}/commit/{commit.commitid}" + + def _create_existing_flags_map(self, repoid: int) -> dict: + existing_flags = RepositoryFlag.objects.filter(repository_id=repoid).all() + existing_flags_map = {} + for flag_obj in existing_flags: + existing_flags_map[flag_obj.flag_name] = flag_obj + return existing_flags_map + + def create(self, validated_data: Dict[str, Any]) -> ReportSession | None: + flag_names = ( + validated_data.pop("flags") if "flags" in validated_data.keys() else [] + ) + repoid = validated_data.pop("repo_id", None) + + # default is necessary here, or else if the key is not in the dict + # the below will throw a KeyError + validated_data.pop("version", None) + validated_data.pop("ci_service", None) + + upload = ReportSession.objects.create(**validated_data) + flags = [] + + if upload: + existing_flags_map = self._create_existing_flags_map(repoid) + for individual_flag in flag_names: + flag_obj = existing_flags_map.get(individual_flag, None) + if flag_obj is None: + flag_obj = RepositoryFlag.objects.create( + repository_id=repoid, flag_name=individual_flag + ) + flags.append(flag_obj) + upload.flags.set(flags) + return upload + + +class OwnerSerializer(serializers.ModelSerializer): + class Meta: + model = Owner + fields = ( + "avatar_url", + "service", + "username", + "name", + "ownerid", + ) + read_only_fields = fields + + +class RepositorySerializer(serializers.ModelSerializer): + is_private = serializers.BooleanField(source="private") + + class Meta: + model = Repository + fields = ("name", "is_private", "active", "language", "yaml") + read_only_fields = fields + + +class CommitSerializer(serializers.ModelSerializer): + author = OwnerSerializer(read_only=True) + repository = RepositorySerializer(read_only=True) + + class Meta: + model = Commit + read_only_fields = ( + "message", + "timestamp", + "ci_passed", + "state", + "repository", + "author", + ) + fields = read_only_fields + ( + "commitid", + "parent_commit_id", + "pullid", + "branch", + ) + + def create(self, validated_data: Dict[str, Any]) -> Commit: + repo = validated_data.pop("repository", None) + commitid = validated_data.pop("commitid", None) + commit, created = Commit.objects.get_or_create( + repository=repo, commitid=commitid, defaults=validated_data + ) + + if created: + TaskService().update_commit( + commitid=commit.commitid, repoid=commit.repository.repoid + ) + + return commit + + +class CommitReportSerializer(serializers.ModelSerializer): + commit_sha = serializers.CharField(source="commit.commitid", read_only=True) + + class Meta: + model = CommitReport + read_only_fields = ( + "external_id", + "created_at", + "commit_sha", + ) + fields = read_only_fields + ("code",) + + def create(self, validated_data: Dict[str, Any]) -> tuple[CommitReport, bool]: + report = ( + CommitReport.objects.coverage_reports() + .filter( + code=validated_data.get("code"), + commit_id=validated_data.get("commit_id"), + ) + .first() + ) + if report: + if report.report_type is None: + report.report_type = CommitReport.ReportType.COVERAGE + report.save() + return report, False + return super().create(validated_data), True + + +class ReportResultsSerializer(serializers.ModelSerializer): + report = CommitReportSerializer(read_only=True) + + class Meta: + model = ReportResults + read_only_fields = ( + "external_id", + "report", + "state", + "result", + "completed_at", + ) + fields = read_only_fields diff --git a/apps/codecov-api/upload/tests/__init__.py b/apps/codecov-api/upload/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/upload/tests/cassetes/test_helpers/test_determine_repo_for_upload_github_actions.yaml b/apps/codecov-api/upload/tests/cassetes/test_helpers/test_determine_repo_for_upload_github_actions.yaml new file mode 100644 index 0000000000..7611f58e45 --- /dev/null +++ b/apps/codecov-api/upload/tests/cassetes/test_helpers/test_determine_repo_for_upload_github_actions.yaml @@ -0,0 +1,48 @@ +interactions: +- request: + body: null + headers: + Connection: + - close + Host: + - token.actions.githubusercontent.com + User-Agent: + - Python-urllib/3.9 + method: GET + uri: https://token.actions.githubusercontent.com/.well-known/jwks + response: + body: + string: '{"keys":[{"n":"u1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmw==","kty":"RSA","kid":"78167F727DEC5D801DD1C8784C704A1C880EC0E1","alg":"RS256","e":"AQAB","use":"sig"}]}' + headers: + ActivityId: + - 4183b59f-ea2d-4bc5-ae32-dc091fb49be2 + Cache-Control: + - no-store,no-cache + Connection: + - close + Content-Length: + - '3522' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 04 Oct 2023 14:34:40 GMT + Pragma: + - no-cache + Strict-Transport-Security: + - max-age=2592000 + X-Cache: + - CONFIG_NOCACHE + X-MSEdge-Ref: + - 'Ref A: F7396FF450344B50A669BB5F69F76217 Ref B: CHGEDGE1206 Ref C: 2023-10-04T14:34:40Z' + X-TFS-ProcessId: + - f0fc6568-a025-4825-be20-f73b4d89a942 + X-TFS-Session: + - 4183b59f-ea2d-4bc5-ae32-dc091fb49be2 + X-VSS-E2EID: + - 4183b59f-ea2d-4bc5-ae32-dc091fb49be2 + X-VSS-SenderDeploymentId: + - 84ff2294-8721-ab49-db5a-db17b0f6c2de + status: + code: 200 + message: OK +version: 1 diff --git a/apps/codecov-api/upload/tests/test_helpers.py b/apps/codecov-api/upload/tests/test_helpers.py new file mode 100644 index 0000000000..b0540466e3 --- /dev/null +++ b/apps/codecov-api/upload/tests/test_helpers.py @@ -0,0 +1,348 @@ +from contextlib import nullcontext +from unittest.mock import patch + +import jwt +import pytest +from django.conf import settings +from django.test import TestCase +from rest_framework.exceptions import Throttled, ValidationError +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.django_apps.reports.models import ReportType +from shared.plan.constants import DEFAULT_FREE_PLAN +from shared.upload.utils import UploaderType, insert_coverage_measurement + +from billing.helpers import mock_all_plans_and_tiers +from codecov_auth.models import GithubAppInstallation, Service +from reports.tests.factories import CommitReportFactory, UploadFactory +from upload.helpers import ( + check_commit_upload_constraints, + determine_repo_for_upload, + ghapp_installation_id_to_use, + try_to_get_best_possible_bot_token, + validate_activated_repo, + validate_upload, +) + + +class TestGithubAppInstallationUsage(TestCase): + def test_not_github_provider(self): + repo = RepositoryFactory(author__service=Service.GITLAB.value) + assert ghapp_installation_id_to_use(repo) is None + + def test_github_app_installation_flow(self): + owner = OwnerFactory(service=Service.GITHUB.value, integration_id=None) + covered_repo = RepositoryFactory(author=owner) + not_covered_repo = RepositoryFactory(author=owner) + ghapp_installation = GithubAppInstallation( + owner=owner, + repository_service_ids=[covered_repo.service_id], + installation_id=200, + ) + ghapp_installation.save() + assert ghapp_installation_id_to_use(covered_repo) == 200 + assert ghapp_installation_id_to_use(not_covered_repo) is None + + +def test_try_to_get_best_possible_bot_token_no_repobot_no_ownerbot(db): + owner = OwnerFactory.create(unencrypted_oauth_token="super") + owner.save() + repository = RepositoryFactory.create(author=owner) + repository.save() + assert try_to_get_best_possible_bot_token(repository) == { + "key": "super", + "secret": None, + } + + +def test_try_to_get_best_possible_bot_token_no_repobot_yes_ownerbot(db): + bot = OwnerFactory.create(unencrypted_oauth_token="bornana") + bot.save() + owner = OwnerFactory.create(unencrypted_oauth_token="super", bot=bot) + owner.save() + repository = RepositoryFactory.create(author=owner) + repository.save() + assert try_to_get_best_possible_bot_token(repository) == { + "key": "bornana", + "secret": None, + } + + +def test_try_to_get_best_possible_bot_token_yes_repobot(db): + bot = OwnerFactory.create(unencrypted_oauth_token="bornana") + bot.save() + another_bot = OwnerFactory.create(unencrypted_oauth_token="anotha_one") + another_bot.save() + owner = OwnerFactory.create(unencrypted_oauth_token="super", bot=bot) + owner.save() + repository = RepositoryFactory.create(author=owner, bot=another_bot) + repository.save() + assert try_to_get_best_possible_bot_token(repository) == { + "key": "anotha_one", + "secret": None, + } + + +@patch("upload.helpers.get_github_integration_token") +@pytest.mark.django_db +def test_try_to_get_best_possible_bot_token_using_integration( + get_github_integration_token, +): + get_github_integration_token.return_value = "test-token" + owner = OwnerFactory.create(integration_id=12345) + owner.save() + repository = RepositoryFactory.create(author=owner, using_integration=True) + repository.save() + assert try_to_get_best_possible_bot_token(repository) == { + "key": "test-token", + } + get_github_integration_token.assert_called_once_with("github", integration_id=12345) + + +@patch("upload.helpers.get_github_integration_token") +@pytest.mark.django_db +def test_try_to_get_best_possible_bot_token_using_invalid_integration( + get_github_integration_token, +): + from shared.github import InvalidInstallationError # circular imports + + get_github_integration_token.side_effect = InvalidInstallationError( + error_cause="installation_not_found" + ) + bot = OwnerFactory.create(unencrypted_oauth_token="bornana") + bot.save() + owner = OwnerFactory.create(integration_id=12345, bot=bot) + owner.save() + repository = RepositoryFactory.create(author=owner, using_integration=True) + repository.save() + # falls back to bot token + assert try_to_get_best_possible_bot_token(repository) == { + "key": "bornana", + "secret": None, + } + get_github_integration_token.assert_called_once_with("github", integration_id=12345) + + +def test_try_to_get_best_possible_nothing_and_is_private(db): + owner = OwnerFactory.create(oauth_token=None) + owner.save() + repository = RepositoryFactory.create(author=owner, bot=None, private=True) + repository.save() + assert try_to_get_best_possible_bot_token(repository) is None + + +def test_try_to_get_best_possible_nothing_and_not_private(db, mocker): + something = mocker.MagicMock() + mock_get_config = mocker.patch("upload.helpers.get_config", return_value=something) + owner = OwnerFactory.create(service="github", oauth_token=None) + owner.save() + repository = RepositoryFactory.create(author=owner, bot=None, private=False) + repository.save() + assert try_to_get_best_possible_bot_token(repository) is something + mock_get_config.assert_called_with("github", "bots", "tokenless") + + +def test_check_commit_constraints_settings_disabled(db, settings): + settings.UPLOAD_THROTTLING_ENABLED = False + repository = RepositoryFactory.create(author__plan=DEFAULT_FREE_PLAN, private=True) + first_commit = CommitFactory.create(repository=repository) + second_commit = CommitFactory.create(repository=repository) + third_commit = CommitFactory.create(repository__author=repository.author) + unrelated_commit = CommitFactory.create() + report = CommitReportFactory.create(commit=first_commit) + for i in range(300): + UploadFactory.create(report=report) + # no commit should be throttled + check_commit_upload_constraints(first_commit) + check_commit_upload_constraints(unrelated_commit) + check_commit_upload_constraints(second_commit) + check_commit_upload_constraints(third_commit) + + +def test_check_commit_constraints_settings_enabled(db, settings, mocker): + settings.UPLOAD_THROTTLING_ENABLED = True + mock_all_plans_and_tiers() + author = OwnerFactory.create(plan=DEFAULT_FREE_PLAN) + repository = RepositoryFactory.create(author=author, private=True) + public_repository = RepositoryFactory.create(author=author, private=False) + first_commit = CommitFactory.create(repository=repository) + second_commit = CommitFactory.create(repository=repository) + third_commit = CommitFactory.create(repository__author=repository.author) + fourth_commit = CommitFactory.create(repository=repository) + public_repository_commit = CommitFactory.create(repository=public_repository) + unrelated_commit = CommitFactory.create() + first_report = CommitReportFactory.create( + commit=first_commit, report_type=ReportType.COVERAGE.value + ) + fourth_report = CommitReportFactory.create( + commit=fourth_commit, report_type=ReportType.COVERAGE.value + ) + check_commit_upload_constraints(second_commit) + for i in range(300): + UploadFactory.create(report__commit__repository=public_repository) + first_upload = UploadFactory(report=first_report) + insert_coverage_measurement( + owner_id=author.ownerid, + repo_id=public_repository.repoid, + commit_id=public_repository_commit.id, + upload_id=first_upload.id, + uploader_used=UploaderType.CLI.value, + private_repo=public_repository.private, + report_type=first_report.report_type, + ) + # ensuring public repos counts don't count towards the quota + check_commit_upload_constraints(second_commit) + for i in range(150): + another_first_upload = UploadFactory.create(report=first_report) + insert_coverage_measurement( + owner_id=author.ownerid, + repo_id=repository.repoid, + commit_id=first_commit.id, + upload_id=another_first_upload.id, + uploader_used=UploaderType.CLI.value, + private_repo=repository.private, + report_type=first_report.report_type, + ) + fourth_upload = UploadFactory.create(report=fourth_report) + insert_coverage_measurement( + owner_id=author.ownerid, + repo_id=repository.repoid, + commit_id=fourth_commit.id, + upload_id=fourth_upload.id, + uploader_used=UploaderType.CLI.value, + private_repo=repository.private, + report_type=fourth_report.report_type, + ) + # first and fourth commit already has uploads made, we won't block uploads to them + check_commit_upload_constraints(first_commit) + check_commit_upload_constraints(fourth_commit) + # unrelated commit belongs to a different user. Ensuring we don't block it + check_commit_upload_constraints(unrelated_commit) + # public repositories commit should never be throttled + check_commit_upload_constraints(public_repository_commit) + with pytest.raises(Throttled): + # second commit does not have uploads made, so we block it + check_commit_upload_constraints(second_commit) + with pytest.raises(Throttled) as excinfo: + # third commit belongs to a different repo, but same user + check_commit_upload_constraints(third_commit) + assert ( + "Throttled due to limit on private repository coverage uploads" + in excinfo.value.detail + ) + + +@pytest.mark.parametrize( + "totals_column_count, rows_count, should_raise", + [(151, 0, False), (151, 151, True), (0, 0, False), (0, 200, True)], +) +def test_validate_upload_too_many_uploads_for_commit( + db, totals_column_count, rows_count, should_raise, mocker +): + redis = mocker.MagicMock(sismember=mocker.MagicMock(return_value=False)) + owner = OwnerFactory.create(plan="users-free") + repo = RepositoryFactory.create(author=owner) + commit = CommitFactory.create(totals={"s": totals_column_count}, repository=repo) + report = CommitReportFactory.create(commit=commit) + for i in range(rows_count): + UploadFactory.create(report=report) + with pytest.raises(ValidationError) if should_raise else nullcontext(): + validate_upload({"commit": commit.commitid}, repo, redis) + + +def test_deactivated_repo(db, mocker): + repository = RepositoryFactory.create(active=True, activated=False) + config_url = f"{settings.CODECOV_DASHBOARD_URL}/{repository.author.service}/{repository.author.username}/{repository.name}/config/general" + + with pytest.raises(ValidationError) as exp: + validate_activated_repo(repository) + assert exp.match( + f"This repository is deactivated. To resume uploading to it, please activate the repository in the codecov UI: {config_url}" + ) + + +@pytest.mark.django_db +def test_determine_repo_for_upload_token(): + token = "80cd8016-5d26-40e5-8c71-f1c44e04aba0" + repository = RepositoryFactory.create(upload_token=token) + assert determine_repo_for_upload({"token": token}) == repository + + +# random keypair for RS256 JWTs used below +public_key = """-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo +4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u ++qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh +kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ +0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg +cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc +mwIDAQAB +-----END PUBLIC KEY-----""" +private_key = """-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj +MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu +NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ +qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg +p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR +ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi +VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV +laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8 +sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H +mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY +dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw +ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ +DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T +N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t +0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv +t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU +AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk +48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL +DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK +xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA +mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh +2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz +et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr +VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD +TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc +dn/RsYEONbwQSjIfMPkvxF+8HQ== +-----END PRIVATE KEY-----""" + + +@pytest.mark.django_db +def test_determine_repo_for_upload_github_actions(codecov_vcr): + # This test recorded a VCR cassette while making a request to + # https://token.actions.githubusercontent.com/.well-known/jwks + # + # I modified this request to include the modulus and exponent corresponding + # to the random keypair I generated above. + # + # I did this offline so as not to need an additional dependency - here's the code + # if we ever need to regenerate these: + # + # from Crypto.PublicKey import RSA + # import base64 + # pub = RSA.importKey(public_key) + # modulus = base64.b64encode(pub.n.to_bytes(256, "big")).decode("ascii") + # exponent = base64.b64encode(pub.e.to_bytes(3, "big")).decode("ascii") + + repository = RepositoryFactory.create() + token = jwt.encode( + { + "iss": "https://token.actions.githubusercontent.com/abcdefg", + "aud": [f"{settings.CODECOV_API_URL}"], + "repository": f"{repository.author.username}/{repository.name}", + "repository_owner": repository.author.username, + }, + private_key, + algorithm="RS256", + headers={ + "kid": "78167F727DEC5D801DD1C8784C704A1C880EC0E1" + }, # from the JWKS response + ) + assert ( + determine_repo_for_upload({"token": token, "service": "github-actions"}) + == repository + ) diff --git a/apps/codecov-api/upload/tests/test_serializers.py b/apps/codecov-api/upload/tests/test_serializers.py new file mode 100644 index 0000000000..8d58606153 --- /dev/null +++ b/apps/codecov-api/upload/tests/test_serializers.py @@ -0,0 +1,259 @@ +from django.conf import settings +from rest_framework.exceptions import ErrorDetail +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) + +from billing.helpers import mock_all_plans_and_tiers +from reports.tests.factories import ( + CommitReportFactory, + ReportResultsFactory, + RepositoryFlagFactory, + UploadFactory, +) +from upload.serializers import ( + CommitReportSerializer, + CommitSerializer, + ReportResultsSerializer, + UploadSerializer, +) + + +def get_fake_upload(): + OwnerFactory() + user_with_uploads = OwnerFactory() + repo = RepositoryFactory.create(author=user_with_uploads, private=True) + RepositoryFactory.create(author=user_with_uploads, private=False) + commit = CommitFactory.create(repository=repo) + report = CommitReportFactory.create(commit=commit) + + return UploadFactory.create(report=report) + + +def get_fake_upload_with_flags(): + upload = get_fake_upload() + flag1 = RepositoryFlagFactory( + repository=upload.report.commit.repository, flag_name="flag1" + ) + flag2 = RepositoryFlagFactory( + repository=upload.report.commit.repository, flag_name="flag2" + ) + upload.flags.set([flag1, flag2]) + return upload + + +def test_serialize_upload(transactional_db, mocker): + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="presigned put", + ) + fake_upload = get_fake_upload() + serializer = UploadSerializer(instance=fake_upload) + assert ( + "upload_type" in serializer.data + and serializer.data["upload_type"] == "uploaded" + ) + new_data = {"env": {"some_var": "some_value"}, "name": "upload name...?"} + res = serializer.update(fake_upload, new_data) + assert res == fake_upload + assert fake_upload.name == "upload name...?" + + +def test_upload_serializer_contains_expected_fields_no_flags(transactional_db, mocker): + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="presigned put", + ) + upload = get_fake_upload() + serializer = UploadSerializer(instance=upload) + repo = upload.report.commit.repository + expected_data = { + "external_id": str(upload.external_id), + "created_at": upload.created_at.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "raw_upload_location": "presigned put", + "state": upload.state, + "provider": upload.provider, + "upload_type": upload.upload_type, + "ci_url": upload.build_url, + "flags": [], + "job_code": upload.job_code, + "env": upload.env, + "name": upload.name, + "url": f"{settings.CODECOV_DASHBOARD_URL}/{repo.author.service}/{repo.author.username}/{repo.name}/commit/{upload.report.commit.commitid}", + } + assert serializer.data == expected_data + + +def test_upload_serializer_contains_expected_fields_with_flags( + transactional_db, mocker +): + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="presigned put", + ) + upload = get_fake_upload_with_flags() + serializer = UploadSerializer(instance=upload) + repo = upload.report.commit.repository + expected_data = { + "external_id": str(upload.external_id), + "created_at": upload.created_at.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "raw_upload_location": "presigned put", + "state": upload.state, + "provider": upload.provider, + "upload_type": upload.upload_type, + "ci_url": upload.build_url, + "flags": ["flag1", "flag2"], + "job_code": upload.job_code, + "env": upload.env, + "name": upload.name, + "url": f"{settings.CODECOV_DASHBOARD_URL}/{repo.author.service}/{repo.author.username}/{repo.name}/commit/{upload.report.commit.commitid}", + } + assert serializer.data == expected_data + + +def test_upload_serializer_null_build_url_empty_flags(transactional_db, mocker): + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="presigned put", + ) + data = { + "ci_url": None, + "flags": [], + "env": "env", + "name": "name", + "job_code": "job_code", + } + + serializer = UploadSerializer(data=data) + assert serializer.is_valid() + + +def test__create_existing_flags_map(transactional_db, mocker): + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="presigned put", + ) + upload = get_fake_upload_with_flags() + serializer = UploadSerializer(instance=upload) + flags_map = serializer._create_existing_flags_map( + upload.report.commit.repository.repoid + ) + upload_flags = upload.flags.all() + flag1 = list(filter(lambda flag: flag.flag_name == "flag1", upload_flags))[0] + flag2 = list(filter(lambda flag: flag.flag_name == "flag2", upload_flags))[0] + assert flags_map == { + "flag1": flag1, + "flag2": flag2, + } + + +def test_commit_serializer_contains_expected_fields(transactional_db, mocker): + commit = CommitFactory.create() + serializer = CommitSerializer(commit) + expected_data = { + "message": commit.message, + "ci_passed": commit.ci_passed, + "state": commit.state, + "repository": { + "name": commit.repository.name, + "is_private": commit.repository.private, + "active": commit.repository.active, + "language": commit.repository.language, + "yaml": commit.repository.yaml, + }, + "author": { + "avatar_url": commit.author.avatar_url, + "service": commit.author.service, + "username": commit.author.username, + "name": commit.author.name, + "ownerid": commit.author.ownerid, + }, + "commitid": commit.commitid, + "parent_commit_id": commit.parent_commit_id, + "pullid": commit.pullid, + "branch": commit.branch, + "timestamp": commit.timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + } + assert serializer.data == expected_data + + +def test_commit_serializer_does_not_duplicate(transactional_db, mocker): + mock_all_plans_and_tiers() + repository = RepositoryFactory() + serializer = CommitSerializer() + + saved_commit1 = serializer.create( + { + "repository": repository, + "commitid": "1234567", + "parent_commit_id": "2345678", + "pullid": 1, + "branch": "test_branch", + } + ) + + saved_commit2 = serializer.create( + { + "repository": repository, + "commitid": "1234567", + "parent_commit_id": "2345678", + "pullid": 1, + "branch": "test_branch", + } + ) + + assert saved_commit1 == saved_commit2 + + +def test_invalid_update_data(transactional_db, mocker): + commit = CommitFactory.create() + new_data = {"pullid": "1"} + serializer = CommitSerializer(commit, new_data) + assert not serializer.is_valid() + assert serializer.errors == { + "commitid": [ErrorDetail(string="This field is required.", code="required")] + } + + +def test_valid_update_data(transactional_db, mocker): + commit = CommitFactory.create(pullid=1) + new_data = {"pullid": "20", "commitid": "abc"} + serializer = CommitSerializer(commit) + res = serializer.update(commit, new_data) + assert commit.pullid == "20" + assert commit.commitid == "abc" + assert commit == res + + +def test_commit_report_serializer(transactional_db, mocker): + report = CommitReportFactory.create() + serializer = CommitReportSerializer(report) + expected_data = { + "commit_sha": report.commit.commitid, + "external_id": str(report.external_id), + "created_at": report.created_at.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "code": report.code, + } + assert serializer.data == expected_data + + +def test_report_results_serializer(transactional_db, mocker): + report_result = ReportResultsFactory.create() + serializer = ReportResultsSerializer(report_result) + expected_data = { + "external_id": str(report_result.external_id), + "report": { + "external_id": str(report_result.report.external_id), + "created_at": report_result.report.created_at.strftime( + "%Y-%m-%dT%H:%M:%S.%fZ" + ), + "commit_sha": report_result.report.commit.commitid, + "code": report_result.report.code, + }, + "state": report_result.state, + "result": report_result.result, + "completed_at": report_result.completed_at, + } + assert serializer.data == expected_data diff --git a/apps/codecov-api/upload/tests/test_throttles.py b/apps/codecov-api/upload/tests/test_throttles.py new file mode 100644 index 0000000000..ea1b85d124 --- /dev/null +++ b/apps/codecov-api/upload/tests/test_throttles.py @@ -0,0 +1,202 @@ +from unittest.mock import MagicMock, Mock + +from django.test import override_settings +from rest_framework.test import APITestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.django_apps.reports.models import ReportType +from shared.helpers.redis import get_redis_connection +from shared.plan.constants import DEFAULT_FREE_PLAN +from shared.upload.utils import UploaderType, insert_coverage_measurement + +from billing.helpers import mock_all_plans_and_tiers +from reports.tests.factories import CommitReportFactory, UploadFactory +from upload.throttles import UploadsPerCommitThrottle, UploadsPerWindowThrottle + + +class ThrottlesUnitTests(APITestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + cls.owner = OwnerFactory(plan=DEFAULT_FREE_PLAN, max_upload_limit=150) + + def request_should_not_throttle(self, commit): + self.uploads_per_window_not_throttled(commit) + self.uploads_per_commit_not_throttled(commit) + + def set_view_obj(self, commit): + view = MagicMock() + view.get_repo.return_value = commit.repository + view.get_commit.return_value = commit + return view + + def uploads_per_commit_throttled(self, commit): + throttle_class = UploadsPerCommitThrottle() + view = self.set_view_obj(commit) + assert not throttle_class.allow_request(Mock(), view) + + def uploads_per_window_throttled(self, commit): + throttle_class = UploadsPerWindowThrottle() + view = self.set_view_obj(commit) + assert not throttle_class.allow_request(Mock(), view) + + def uploads_per_commit_not_throttled(self, commit): + throttle_class = UploadsPerCommitThrottle() + view = self.set_view_obj(commit) + assert throttle_class.allow_request(Mock(), view) + + def uploads_per_window_not_throttled(self, commit): + throttle_class = UploadsPerWindowThrottle() + view = self.set_view_obj(commit) + assert throttle_class.allow_request(Mock(), view) + + @override_settings(UPLOAD_THROTTLING_ENABLED=False) + def test_check_commit_constraints_settings_disabled(self): + repository = RepositoryFactory( + author__plan=DEFAULT_FREE_PLAN, + private=True, + author=self.owner, + ) + first_commit = CommitFactory(repository=repository) + second_commit = CommitFactory(repository=repository) + third_commit = CommitFactory(repository__author=repository.author) + unrelated_commit = CommitFactory() + + first_report = CommitReportFactory( + commit=first_commit, report_type=ReportType.COVERAGE.value + ) + sec_report = CommitReportFactory( + commit=second_commit, report_type=ReportType.COVERAGE.value + ) + + for i in range(150): + first_upload = UploadFactory(report=first_report) + insert_coverage_measurement( + owner_id=self.owner.ownerid, + repo_id=repository.repoid, + commit_id=first_commit.id, + upload_id=first_upload.id, + uploader_used=UploaderType.CLI.value, + private_repo=repository.private, + report_type=first_report.report_type, + ) + second_upload = UploadFactory(report=sec_report) + insert_coverage_measurement( + owner_id=self.owner.ownerid, + repo_id=repository.repoid, + commit_id=second_commit.id, + upload_id=second_upload.id, + uploader_used=UploaderType.CLI.value, + private_repo=repository.private, + report_type=sec_report.report_type, + ) + + # no commit should be throttled + self.request_should_not_throttle(first_commit) + self.request_should_not_throttle(second_commit) + self.request_should_not_throttle(third_commit) + self.request_should_not_throttle(unrelated_commit) + + @override_settings(UPLOAD_THROTTLING_ENABLED=True) + def test_throttle_check_commit_constraints_settings_enabled(self): + author = self.owner + first_commit = CommitFactory.create(repository__author=author) + + repository = RepositoryFactory.create(author=author, private=True) + second_commit = CommitFactory.create(repository=repository) + third_commit = CommitFactory.create(repository=repository) + fourth_commit = CommitFactory.create(repository=repository) + + public_repository = RepositoryFactory.create(author=author, private=False) + public_repository_commit = CommitFactory.create(repository=public_repository) + + unrelated_commit = CommitFactory.create() + + second_report = CommitReportFactory.create( + commit=second_commit, report_type=ReportType.COVERAGE.value + ) + fourth_report = CommitReportFactory.create( + commit=fourth_commit, report_type=ReportType.COVERAGE.value + ) + self.request_should_not_throttle(third_commit) + + for i in range(300): + upload = UploadFactory.create(report__commit__repository=public_repository) + insert_coverage_measurement( + owner_id=author.ownerid, + repo_id=public_repository.repoid, + commit_id=second_commit.id, + upload_id=upload.id, + uploader_used=UploaderType.CLI.value, + private_repo=public_repository.private, + report_type=second_report.report_type, + ) + # ensuring public repos counts don't count towards the quota + self.request_should_not_throttle(third_commit) + + for i in range(150): + second_upload = UploadFactory.create(report=second_report) + insert_coverage_measurement( + owner_id=author.ownerid, + repo_id=repository.repoid, + commit_id=second_commit.id, + upload_id=second_upload.id, + uploader_used=UploaderType.CLI.value, + private_repo=repository.private, + report_type=second_report.report_type, + ) + fourth_upload = UploadFactory.create(report=fourth_report) + insert_coverage_measurement( + owner_id=author.ownerid, + repo_id=repository.repoid, + commit_id=fourth_commit.id, + upload_id=fourth_upload.id, + uploader_used=UploaderType.CLI.value, + private_repo=repository.private, + report_type=fourth_report.report_type, + ) + # second and fourth commit already has uploads made, we won't block uploads to them + self.request_should_not_throttle(second_commit) + self.request_should_not_throttle(fourth_commit) + + # unrelated commit belongs to a different user. Ensuring we don't block it + self.request_should_not_throttle(unrelated_commit) + # public repositories commit should never be throttled + self.request_should_not_throttle(public_repository_commit) + + # third commit does not have uploads made, so we block it + self.uploads_per_window_throttled(third_commit) + # first commit belongs to a different repo, but same user + self.uploads_per_window_throttled(first_commit) + + def test_validate_upload_too_many_uploads_for_commit(self): + par = [(151, 0, False), (151, 151, True), (0, 0, False), (0, 200, True)] + for totals_column_count, rows_count, should_raise in par: + owner = self.owner + repo = RepositoryFactory.create(author=owner) + commit = CommitFactory.create( + totals={"s": totals_column_count}, repository=repo + ) + report = CommitReportFactory.create(commit=commit) + for i in range(rows_count): + UploadFactory.create(report=report) + + if should_raise: + self.uploads_per_commit_throttled(commit) + else: + self.request_should_not_throttle(commit) + + def test_validate_redis_counter(self): + redis = get_redis_connection() + owner = self.owner + cache_key = f"monthly_upload_usage_{owner.ownerid}" + redis.set(cache_key, 1, ex=259200) + repo = RepositoryFactory.create(author=owner) + commit = CommitFactory.create(totals={}, repository=repo) + self.request_should_not_throttle(commit) + assert redis.get(cache_key) == b"1" + redis.delete(cache_key) diff --git a/apps/codecov-api/upload/tests/test_tokenless_azure.py b/apps/codecov-api/upload/tests/test_tokenless_azure.py new file mode 100644 index 0000000000..536e591f4c --- /dev/null +++ b/apps/codecov-api/upload/tests/test_tokenless_azure.py @@ -0,0 +1,90 @@ +from datetime import datetime, timedelta +from unittest.mock import patch + +import pytest +from rest_framework.exceptions import NotFound + +from upload.tokenless.azure import TokenlessAzureHandler + + +@pytest.fixture +def upload_params(): + return { + "job": "899861", + "project": "public", + "server_uri": "https://dev.azure.com/dnceng-public/", + "build": "20241219.14", + "commit": "0f6e31fec5876be932f9e52f739ce1a2e04f11e3", + } + + +def test_verify_handles_nanosecond_timestamp(upload_params): + """ + Test that the handler correctly processes timestamps with nanosecond precision + from the Azure DevOps API. + """ + handler = TokenlessAzureHandler(upload_params) + + # Mock a response with nanosecond precision timestamp (7 digits after decimal) + current_time = datetime.now() + timestamp = current_time.strftime("%Y-%m-%dT%H:%M:%S.1234567Z") + + mock_build_response = { + "status": "completed", + "finishTime": timestamp, + "buildNumber": "20241219.14", + "sourceVersion": "0f6e31fec5876be932f9e52f739ce1a2e04f11e3", + "repository": {"type": "GitHub"}, + } + + with patch.object(handler, "get_build", return_value=mock_build_response): + service = handler.verify() + assert service == "github" + + +def test_verify_handles_microsecond_timestamp(upload_params): + """ + Test that the handler still works correctly with regular microsecond precision + timestamps. + """ + handler = TokenlessAzureHandler(upload_params) + + # Mock a response with microsecond precision (6 digits after decimal) + current_time = datetime.now() + timestamp = current_time.strftime("%Y-%m-%dT%H:%M:%S.123456Z") + + mock_build_response = { + "status": "completed", + "finishTime": timestamp, + "buildNumber": "20241219.14", + "sourceVersion": "0f6e31fec5876be932f9e52f739ce1a2e04f11e3", + "repository": {"type": "GitHub"}, + } + + with patch.object(handler, "get_build", return_value=mock_build_response): + service = handler.verify() + assert service == "github" + + +def test_verify_rejects_old_timestamp(upload_params): + """ + Test that the handler correctly rejects timestamps older than 4 minutes, + even with nanosecond precision. + """ + handler = TokenlessAzureHandler(upload_params) + + # Create a timestamp that's more than 4 minutes old + old_time = datetime.now() - timedelta(minutes=5) + timestamp = old_time.strftime("%Y-%m-%dT%H:%M:%S.1234567Z") + + mock_build_response = { + "status": "completed", + "finishTime": timestamp, + "buildNumber": "20241219.14", + "sourceVersion": "0f6e31fec5876be932f9e52f739ce1a2e04f11e3", + "repository": {"type": "GitHub"}, + } + + with patch.object(handler, "get_build", return_value=mock_build_response): + with pytest.raises(NotFound, match="Azure build has already finished"): + handler.verify() diff --git a/apps/codecov-api/upload/tests/test_upload.py b/apps/codecov-api/upload/tests/test_upload.py new file mode 100644 index 0000000000..5c6e3a9eb1 --- /dev/null +++ b/apps/codecov-api/upload/tests/test_upload.py @@ -0,0 +1,3226 @@ +import time +from datetime import datetime, timedelta +from json import dumps, loads +from unittest.mock import ANY, PropertyMock, patch +from urllib.parse import urlencode + +import pytest +import requests +import rest_framework +from django.core.exceptions import MultipleObjectsReturned +from django.test import TestCase, override_settings +from django.utils import timezone +from freezegun import freeze_time +from rest_framework import status +from rest_framework.exceptions import NotFound, ValidationError +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.api_archive.archive import ArchiveService +from shared.django_apps.codecov_auth.tests.factories import PlanFactory, TierFactory +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.plan.constants import TierName +from shared.torngit.exceptions import ( + TorngitClientGeneralError, + TorngitObjectNotFoundError, + TorngitRateLimitError, +) +from simplejson import JSONDecodeError + +from core.models import Commit +from reports.tests.factories import CommitReportFactory, UploadFactory +from upload.helpers import ( + determine_repo_for_upload, + determine_upload_branch_to_use, + determine_upload_commit_to_use, + determine_upload_pr_to_use, + dispatch_upload_task, + get_global_tokens, + insert_commit, + parse_headers, + parse_params, + validate_upload, +) +from upload.tokenless.tokenless import TokenlessUploadHandler +from utils.encryption import encryptor + + +def mock_get_config_global_upload_tokens(*args): + if args == ("github", "global_upload_token"): + return "githubuploadtoken" + if args == ("gitlab", "global_upload_token"): + return "gitlabuploadtoken" + if args == ("bitbucket_server", "global_upload_token"): + return "bitbucketserveruploadtoken" + + +class MockRedis: + def __init__(self, blacklisted=False, *args, **kwargs): + self.blacklisted = blacklisted + self.expected_task_key = kwargs.get("expected_task_key") + self.expected_task_arguments = kwargs.get("expected_task_arguments") + self.expected_expire_time = kwargs.get("expected_expire_time") + + def rpush(self, key, value): + assert key == self.expected_task_key + assert value == dumps(self.expected_task_arguments) + + def expire(self, key, expire_time): + assert key == self.expected_task_key + assert expire_time == self.expected_expire_time + + def sismember(self, key, repoid): + return self.blacklisted + + def get(self, key): + return 10 + + def setex(self, redis_key, expire_time, report): + return + + def set(self, redis_key, value, **kwargs): + # This is only used when setting the cache key for the number of uploads. Will need to be refactored if we use it for something else. + assert self.get(redis_key) + 1 == value + + +class UploadHandlerHelpersTest(TestCase): + def test_parse_params_validates_valid_input(self): + request_params = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "slug": "codecov/codecov-api", + "token": "4a24929b-9276-4784-8e85-a7a008a32037", + "service": "circleci", + "pull_request": "undefined", + "flags": "this-is-a-flag,this-is-another-flag", + "name": "", + "branch": "HEAD", + "param_doesn't_exist_but_still_should_not_error": True, + "s3": 123, + "build_url": "https://thisisabuildurl.com", + "_did_change_merge_commit": False, + "parent": "123abc", + } + + expected_result = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "slug": "codecov/codecov-api", + "owner": "codecov", + "repo": "codecov-api", + "token": "4a24929b-9276-4784-8e85-a7a008a32037", + "service": "circleci", + "pr": None, + "pull_request": None, + "flags": "this-is-a-flag,this-is-another-flag", + "param_doesn't_exist_but_still_should_not_error": True, + "s3": 123, + "build_url": "https://thisisabuildurl.com", + "job": None, + "using_global_token": False, + "branch": None, + "_did_change_merge_commit": False, + "parent": "123abc", + } + + parsed_params = parse_params(request_params) + assert expected_result == parsed_params + + def test_parse_params_errors_for_invalid_input(self): + request_params = { + "version": "v5", + "slug": "not-a-valid-slug", + "token": "4a24929b-9276-4784-8e85-a7a008a32037", + "service": "circleci", + "pr": 123, + "flags": "not_a_valid_flag!!?!", + "s3": "this should be an integer", + "build_url": "not a valid url!", + "_did_change_merge_commit": "yup", + "parent": 123, + } + + with self.assertRaises(ValidationError) as err: + parse_params(request_params) + + assert len(err.exception.detail) == 9 + + def test_parse_params_transforms_input(self): + request_params = { + "version": "v4", + "commit": "3BE5C52BD748C508a7e96993c02cf3518c816e84", + "slug": "codecov/subgroup/codecov-api", + "service": "travis-org", + "pull_request": "439", + "pr": "", + "branch": "origin/test-branch", + "travis_job_id": "travis-jobID", + "build": "nil", + } + + expected_result = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", # converted to lower case + "slug": "codecov/subgroup/codecov-api", + "owner": "codecov:subgroup", # extracted from slug + "repo": "codecov-api", # extracted from slug + "service": "travis", # "travis-org" converted to "travis" + "pr": "439", # populated from "pull_request" field since none was provided + "pull_request": "439", + "branch": "test-branch", # "origin/" removed from name + "job": "travis-jobID", # populated from "travis_job_id" since none was provided + "travis_job_id": "travis-jobID", + "build": None, # "nil" coerced to None + "using_global_token": False, + } + + parsed_params = parse_params(request_params) + assert expected_result == parsed_params + + request_params = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "pr": "967", + "pull_request": "null", + "branch": "refs/heads/another-test-branch", + "job": "jobID", + "travis_job_id": "travis-jobID", + } + + expected_result = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "owner": None, # set to "None" since slug wasn't provided + "repo": None, # set to "None" since slug wasn't provided + "pr": "967", # not populated from "pull_request" + "pull_request": None, # "null" coerced to None + "branch": "another-test-branch", # "refs/heads" removed + "job": "jobID", # not populated from "travis_job_id" + "travis_job_id": "travis-jobID", + "using_global_token": False, + "service": None, # defaulted to None if not provided and not using global upload token + } + + parsed_params = parse_params(request_params) + assert expected_result == parsed_params + + request_params = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "slug": "", + "pr": "", + "pull_request": "156", + "job": None, + "travis_job_id": "travis-jobID", + } + + expected_result = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "owner": None, # set to "None" since slug wasn't provided + "repo": None, # set to "None" since slug wasn't provided + "pr": "156", # populated from "pull_request" + "pull_request": "156", + "job": "travis-jobID", # populated from "travis_job_id" + "travis_job_id": "travis-jobID", + "service": None, # defaulted to None if not provided and not using global upload token + "using_global_token": False, + } + + parsed_params = parse_params(request_params) + assert expected_result == parsed_params + + @patch("upload.helpers.get_config") + def test_parse_params_recognizes_global_token(self, mock_get_config): + mock_get_config.side_effect = mock_get_config_global_upload_tokens + + request_params = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "token": "bitbucketserveruploadtoken", + } + + expected_result = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "token": "bitbucketserveruploadtoken", + "using_global_token": True, + "service": "bitbucket_server", + "job": None, + "owner": None, + "pr": None, + "repo": None, + } + + parsed_params = parse_params(request_params) + assert expected_result == parsed_params + + @patch("upload.helpers.get_config") + def test_parse_params_recognizes_global_token_overrides_service( + self, mock_get_config + ): + mock_get_config.side_effect = mock_get_config_global_upload_tokens + + request_params = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "token": "bitbucketserveruploadtoken", + "service": "jenkins", + } + + expected_result = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "token": "bitbucketserveruploadtoken", + "using_global_token": True, + "service": "bitbucket_server", + "job": None, + "owner": None, + "pr": None, + "repo": None, + } + + parsed_params = parse_params(request_params) + assert expected_result == parsed_params + + @patch("upload.helpers.get_config") + def test_get_global_tokens(self, mock_get_config): + mock_get_config.side_effect = mock_get_config_global_upload_tokens + + expected_result = { + "githubuploadtoken": "github", + "gitlabuploadtoken": "gitlab", + "bitbucketserveruploadtoken": "bitbucket_server", + } + + global_tokens = get_global_tokens() + assert expected_result == global_tokens + + def test_determine_repo_upload(self): + with self.subTest("token found"): + org = OwnerFactory() + repo = RepositoryFactory(author=org) + + params = { + "version": "v4", + "using_global_token": False, + "token": repo.upload_token, + } + + assert repo == determine_repo_for_upload(params) + + with self.subTest("token not found"): + org = OwnerFactory() + repo = RepositoryFactory(author=org) + + params = { + "version": "v4", + "using_global_token": False, + "token": "4a24929b-9276-4784-8e85-a7a008a32037", + } + + with self.assertRaises(NotFound): + determine_repo_for_upload(params) + + with self.subTest("missing token or service"): + params = {"version": "v4", "using_global_token": False, "service": None} + + with self.assertRaises(ValidationError): + determine_repo_for_upload(params) + + @patch.object(requests, "get") + def test_determine_repo_upload_tokenless(self, mock_get): + org = OwnerFactory(username="codecov", service="github") + repo = RepositoryFactory(author=org) + expected_response = { + "id": 732059764, + "finishTime": f"{datetime.now()}", + "status": "inProgress", + "sourceVersion": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "buildNumber": "732059764", + "allow_failure": None, + "number": "498.1", + "state": "passed", + "started_at": "2020-10-01T20:02:55Z", + "finished_at": f"{datetime.now()}".split(".")[0], + "project": {"visibility": "public", "repositoryType": "github"}, + "triggerInfo": {"pr.sourceSha": "3be5c52bd748c508a7e96993c02cf3518c816e84"}, + "build": { + "@type": "build", + "@href": "/build/732059763", + "@representation": "minimal", + "id": 732059763, + "number": "498", + "state": "passed", + "duration": 84, + "event_type": "push", + "previous_state": "passed", + "pull_request_title": None, + "pull_request_number": None, + "started_at": "2020-10-01T20:01:31Z", + "finished_at": "2020-10-01T20:02:55Z", + "private": False, + "priority": False, + "jobs": [{"jobId": "732059764"}], + }, + "queue": "builds.gce", + "repository": { + "@type": "repository", + "@href": "/repo/25205338", + "@representation": "minimal", + "id": 25205338, + "type": "GitHub", + "name": "python-standard", + "slug": f"{org.username}/{repo.name}", + }, + "commit": { + "@type": "commit", + "@representation": "minimal", + "id": 226208830, + "sha": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "ref": "refs/heads/master", + "message": "New Build: 10/01/20 20:00:54", + "compare_url": "https://github.com/codecov/python-standard/compare/28392734979c...2485b28f9862", + "committed_at": "2020-10-01T20:00:55Z", + }, + } + + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "slug": f"{org.username}/{repo.name}", + "owner": org.username, + "repo": repo.name, + "service": "travis", + "pr": None, + "pull_request": None, + "flags": "this-is-a-flag,this-is-another-flag", + "param_doesn't_exist_but_still_should_not_error": True, + "s3": 123, + "build_url": "https://thisisabuildurl.com", + "job": "732059764", + "build": "732059764", + "using_global_token": False, + "branch": None, + "project": "p12", + "server_uri": "https://dev.azure.com/example/", + "_did_change_merge_commit": False, + "parent": "123abc", + } + + assert repo == determine_repo_for_upload(params) + + params["service"] = "appveyor" + + assert repo == determine_repo_for_upload(params) + + params["service"] = "azure_pipelines" + + assert repo == determine_repo_for_upload(params) + + def test_determine_upload_branch_to_use(self): + with self.subTest("no branch and no pr provided"): + upload_params = {"branch": None, "pr": None} + repo_default_branch = "defaultbranch" + + expected_value = "defaultbranch" + assert expected_value == determine_upload_branch_to_use( + upload_params, repo_default_branch + ) + + with self.subTest("pullid in branch name"): + upload_params = {"branch": "pr/123", "pr": None} + repo_default_branch = "defaultbranch" + + expected_value = None + assert expected_value == determine_upload_branch_to_use( + upload_params, repo_default_branch + ) + + with self.subTest("branch and no pr provided"): + upload_params = {"branch": "uploadbranch", "pr": None} + repo_default_branch = "defaultbranch" + + expected_value = "uploadbranch" + assert expected_value == determine_upload_branch_to_use( + upload_params, repo_default_branch + ) + + with self.subTest("branch and pr provided"): + upload_params = {"branch": "uploadbranch", "pr": "123"} + repo_default_branch = "defaultbranch" + + expected_value = "uploadbranch" + assert expected_value == determine_upload_branch_to_use( + upload_params, repo_default_branch + ) + + def test_determine_upload_pr_to_use(self): + with self.subTest("pullid in branch"): + upload_params = {"branch": "pr/123", "pr": "456"} + + expected_value = "123" + assert expected_value == determine_upload_pr_to_use(upload_params) + + with self.subTest("pullid in arguments, no pullid in branch"): + upload_params = {"branch": "uploadbranch", "pr": "456"} + + expected_value = "456" + assert expected_value == determine_upload_pr_to_use(upload_params) + + with self.subTest("pullid not provided"): + upload_params = {"branch": "uploadbranch", "pr": None} + + expected_value = None + assert expected_value == determine_upload_pr_to_use(upload_params) + + with self.subTest("pullid not provided, branch not provided"): + upload_params = {"branch": None, "pr": None} + + expected_value = None + assert expected_value == determine_upload_pr_to_use(upload_params) + + with self.subTest("pullid set to true"): + upload_params = {"branch": None, "pr": "true"} + + expected_value = None + assert expected_value == determine_upload_pr_to_use(upload_params) + + @patch("upload.helpers.RepoProviderService") + @patch("upload.helpers._get_git_commit_data") + def test_determine_upload_commit_to_use( + self, mock_repo_provider_service, mock_async + ): + mock_repo_provider_service.return_value = { + "message": "Merge 1c78206f1a46dc6db8412a491fc770eb7d0f8a47 into 261aa931e8e3801ad95a31bbc3529de2bba436c8" + } + + with self.subTest("not a github commit"): + org = OwnerFactory( + service="bitbucket", + oauth_token=encryptor.encode("hahahahaha").decode(), + ) + repo = RepositoryFactory(author=org) + upload_params = { + "service": "bitbucket", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + } + assert ( + "3be5c52bd748c508a7e96993c02cf3518c816e84" + == determine_upload_commit_to_use(upload_params, repo) + ) + + with self.subTest("merge commit"): + org = OwnerFactory( + service="github", + oauth_token=encryptor.encode("hahahahaha").decode(), + ) + repo = RepositoryFactory(author=org) + upload_params = { + "service": "github", + "commit": "3084886b7ff869dcf327ad1d28a8b7d34adc7584", + } + # Should use id from merge commit message, not from params + assert ( + "1c78206f1a46dc6db8412a491fc770eb7d0f8a47" + == determine_upload_commit_to_use(upload_params, repo) + ) + + with self.subTest("just no bot available"): + org = OwnerFactory(service="github", oauth_token=None) + repo = RepositoryFactory(author=org, private=True) + upload_params = { + "service": "github", + "commit": "3084886b7ff869dcf327ad1d28a8b7d34adc7584", + } + # Should use id from merge commit message, not from params + assert ( + "3084886b7ff869dcf327ad1d28a8b7d34adc7584" + == determine_upload_commit_to_use(upload_params, repo) + ) + + with self.subTest("merge commit with did_change_merge_commit argument"): + org = OwnerFactory( + service="github", + oauth_token=encryptor.encode("hahahahaha").decode(), + ) + repo = RepositoryFactory(author=org) + upload_params = { + "service": "github", + "commit": "3084886b7ff869dcf327ad1d28a8b7d34adc7584", + "_did_change_merge_commit": True, + } + # Should use the commit id provided in params, not the one from the commit message + assert ( + "3084886b7ff869dcf327ad1d28a8b7d34adc7584" + == determine_upload_commit_to_use(upload_params, repo) + ) + + with self.subTest("use repo bot token when available"): + bot = OwnerFactory() + org = OwnerFactory(service="github") + repo = RepositoryFactory(author=org, bot=bot) + + upload_params = { + "service": "github", + "commit": "3084886b7ff869dcf327ad1d28a8b7d34adc7584", + } + + determine_upload_commit_to_use(upload_params, repo) + + assert ( + "1c78206f1a46dc6db8412a491fc770eb7d0f8a47" + == determine_upload_commit_to_use(upload_params, repo) + ) + + mock_async.side_effect = [TorngitClientGeneralError(500, None, None)] + + with self.subTest("HTTP error"): + org = OwnerFactory(service="github") + repo = RepositoryFactory(author=org) + upload_params = { + "service": "github", + "commit": "3084886b7ff869dcf327ad1d28a8b7d34adc7584", + "_did_change_merge_commit": False, + } + assert ( + "3084886b7ff869dcf327ad1d28a8b7d34adc7584" + == determine_upload_commit_to_use(upload_params, repo) + ) + + mock_async.side_effect = [TorngitObjectNotFoundError(500, None)] + + with self.subTest("HTTP error"): + org = OwnerFactory(service="github") + repo = RepositoryFactory(author=org) + upload_params = { + "service": "github", + "commit": "3084886b7ff869dcf327ad1d28a8b7d34adc7584", + "_did_change_merge_commit": False, + } + assert ( + "3084886b7ff869dcf327ad1d28a8b7d34adc7584" + == determine_upload_commit_to_use(upload_params, repo) + ) + + def test_insert_commit(self): + org = OwnerFactory() + repo = RepositoryFactory(author=org) + + with self.subTest("newly created"): + insert_commit( + "3084886b7ff869dcf327ad1d28a8b7d34adc7584", "test", "123", repo, org + ) + + commit = Commit.objects.get( + commitid="3084886b7ff869dcf327ad1d28a8b7d34adc7584" + ) + assert commit.repository == repo + assert commit.state == "pending" + assert commit.branch == "test" + assert commit.pullid == 123 + assert commit.merged == False + assert commit.parent_commit_id is None + + with self.subTest("commit already in database"): + CommitFactory( + commitid="1c78206f1a46dc6db8412a491fc770eb7d0f8a47", + branch="apples", + pullid="456", + repository=repo, + parent_commit_id=None, + ) + # parent_commit_id and branch should be updated + insert_commit( + "1c78206f1a46dc6db8412a491fc770eb7d0f8a47", + "oranges", + "123", + repo, + org, + parent_commit_id="different_parent_commit", + ) + + commit = Commit.objects.get( + commitid="1c78206f1a46dc6db8412a491fc770eb7d0f8a47" + ) + assert commit.repository == repo + assert commit.branch == "oranges" + assert commit.pullid == 456 + assert commit.merged is None + assert commit.parent_commit_id == "different_parent_commit" + + with self.subTest("parent provided"): + parent = CommitFactory() + insert_commit( + "8458a8c72aafb5fb4c5cd58f467a2f71298f1b61", + "test", + None, + repo, + org, + parent_commit_id=parent.commitid, + ) + + commit = Commit.objects.get( + commitid="8458a8c72aafb5fb4c5cd58f467a2f71298f1b61" + ) + assert commit.repository == repo + assert commit.branch == "test" + assert commit.pullid is None + assert commit.merged is None + assert commit.parent_commit_id == parent.commitid + + def test_parse_request_headers(self): + with self.subTest("Invalid content disposition"): + with self.assertRaises(ValidationError): + parse_headers({"Content_Disposition": "not inline"}, {"version": "v2"}) + + with self.subTest("v2"): + assert parse_headers( + {"Content-Disposition": "inline"}, {"version": "v2"} + ) == {"content_type": "application/x-gzip", "reduced_redundancy": False} + + with self.subTest("v4"): + assert parse_headers( + {"X_Content_Type": "text/html", "X_Reduced_Redundancy": "false"}, + {"version": "v4"}, + ) == {"content_type": "text/plain", "reduced_redundancy": False} + + assert parse_headers( + {"X_Content_Type": "plain/text", "X_Reduced_Redundancy": "true"}, + {"version": "v4"}, + ) == {"content_type": "plain/text", "reduced_redundancy": True} + + assert parse_headers( + { + "X_Content_Type": "application/x-gzip", + "X_Reduced_Redundancy": "true", + }, + {"version": "v4", "package": "node"}, + ) == {"content_type": "application/x-gzip", "reduced_redundancy": False} + + with self.subTest("Unsafe content type"): + assert parse_headers( + { + "Content_Disposition": None, + "X_Content_Type": "multipart/form-data", + }, + {"version": "v4"}, + ) == {"content_type": "text/plain", "reduced_redundancy": True} + + def test_validate_upload_repository_moved(self): + redis = MockRedis() + owner = OwnerFactory(plan="users-free") + repo = RepositoryFactory(author=owner, name="") + commit = CommitFactory() + + with self.assertRaises(ValidationError) as err: + validate_upload({"commit": commit.commitid}, repo, redis) + + assert ( + err.exception.detail[0] + == "This repository has moved or was deleted. Please login to Codecov to retrieve a new upload token." + ) + + def test_validate_upload_empty_totals(self): + redis = MockRedis() + owner = OwnerFactory(plan="5m") + repo = RepositoryFactory(author=owner) + commit = CommitFactory(totals=None, repository=repo) + + validate_upload({"commit": commit.commitid}, repo, redis) + repo.refresh_from_db() + assert repo.activated == True + assert repo.active == True + assert repo.deleted == False + + def test_validate_upload_too_many_uploads_for_commit(self): + redis = MockRedis() + owner = OwnerFactory(plan="users-free") + repo = RepositoryFactory(author=owner) + commit = CommitFactory(totals={"s": 151}, repository=repo) + report = CommitReportFactory.create(commit=commit) + for i in range(151): + UploadFactory.create(report=report) + + with self.assertRaises(ValidationError) as err: + validate_upload({"commit": commit.commitid}, repo, redis) + assert err.exception.detail[0] == "Too many uploads to this commit." + + def test_validate_upload_repository_blacklisted(self): + redis = MockRedis(blacklisted=True) + owner = OwnerFactory(plan="users-free") + repo = RepositoryFactory(author=owner) + commit = CommitFactory() + + with self.assertRaises(ValidationError) as err: + validate_upload({"commit": commit.commitid}, repo, redis) + assert ( + err.exception.detail[0] + == "Uploads rejected for this project. Please contact Codecov staff for more details. Sorry for the inconvenience." + ) + + def test_validate_upload_per_repo_billing_invalid(self): + redis = MockRedis() + owner = OwnerFactory(plan="1m") + RepositoryFactory(author=owner, private=True, activated=True, active=True) + repo = RepositoryFactory( + author=owner, private=True, activated=False, active=False + ) + commit = CommitFactory() + + with self.assertRaises(ValidationError) as err: + validate_upload({"commit": commit.commitid}, repo, redis) + assert ( + err.exception.detail[0] + == "Sorry, but this team has no private repository credits left." + ) + + def test_validate_upload_gitlab_subgroups(self): + redis = MockRedis() + parent_group = OwnerFactory(plan="1m", parent_service_id=None, service="gitlab") + top_subgroup = OwnerFactory( + plan="1m", + parent_service_id=parent_group.service_id, + service="gitlab", + ) + bottom_subgroup = OwnerFactory( + plan="1m", + parent_service_id=top_subgroup.service_id, + service="gitlab", + ) + RepositoryFactory( + author=parent_group, private=True, activated=True, active=True + ) + repo = RepositoryFactory(author=bottom_subgroup, private=True, activated=False) + commit = CommitFactory() + + with self.assertRaises(ValidationError) as err: + validate_upload({"commit": commit.commitid}, repo, redis) + assert ( + err.exception.detail[0] + == "Sorry, but this team has no private repository credits left." + ) + + def test_validate_upload_valid_upload_repo_not_activated(self): + redis = MockRedis() + owner = OwnerFactory(plan="users-free") + repo = RepositoryFactory( + author=owner, + private=True, + activated=False, + deleted=False, + active=False, + ) + commit = CommitFactory() + + with patch( + "services.analytics.AnalyticsService.account_activated_repository_on_upload" + ) as mock_segment_event: + validate_upload({"commit": commit.commitid}, repo, redis) + assert mock_segment_event.called + + repo.refresh_from_db() + assert repo.activated == True + assert repo.active == True + assert repo.deleted == False + + def test_validate_upload_valid_upload_repo_activated(self): + redis = MockRedis() + owner = OwnerFactory(plan="5m") + repo = RepositoryFactory(author=owner, private=True, activated=True) + commit = CommitFactory() + + with patch( + "services.analytics.AnalyticsService.account_activated_repository_on_upload" + ) as mock_segment_event: + validate_upload({"commit": commit.commitid}, repo, redis) + assert not mock_segment_event.called + + repo.refresh_from_db() + assert repo.activated == True + assert repo.active == True + assert repo.deleted == False + + @freeze_time("2023-01-01T00:00:00") + @patch("services.task.TaskService.upload") + def test_dispatch_upload_task(self, upload): + repo = RepositoryFactory() + task_arguments = { + "commit": "commit123", + "version": "v4", + "report_code": "local_report", + } + + expected_key = f"uploads/{repo.repoid}/commit123" + + redis = MockRedis( + expected_task_key=expected_key, + expected_task_arguments=task_arguments, + expected_expire_time=86400, + ) + + dispatch_upload_task(task_arguments, repo, redis) + upload.assert_called_once_with( + repoid=repo.repoid, + commitid=task_arguments.get("commit"), + report_type="coverage", + report_code="local_report", + arguments=task_arguments, + countdown=4, + ) + + +class UploadHandlerRouteTest(APITestCase): + @pytest.fixture(scope="function", autouse=True) + def inject_mocker(self, mocker): + self.mocker = mocker + + # Wrap client calls + def _get(self, kwargs=None): + return self.client.get(reverse("upload-handler", kwargs=kwargs)) + + def _options(self, kwargs=None, data=None): + return self.client.options(reverse("upload-handler", kwargs=kwargs)) + + def _post( + self, + kwargs=None, + data=None, + query=None, + content_type="application/json", + headers=None, + ): + headers = headers or {} + query_string = f"?{urlencode(query)}" if query else "" + url = reverse("upload-handler", kwargs=kwargs) + query_string + return self.client.post(url, data=data, content_type=content_type, **headers) + + def _post_slash( + self, + kwargs=None, + data=None, + query=None, + content_type="application/json", + headers=None, + ): + headers = headers or {} + query_string = f"?{urlencode(query)}" if query else "" + url = "/upload/v2/" + query_string + return self.client.post(url, data=data, content_type=content_type, **headers) + + def setUp(self): + tier = TierFactory(tier_name=TierName.BASIC.value) + plan = PlanFactory(tier=tier, is_active=True) + self.org = OwnerFactory( + plan=plan.name, username="codecovtest", service="github" + ) + self.repo = RepositoryFactory( + author=self.org, + name="upload-test-repo", + upload_token="a03e5d02-9495-4413-b0d8-05651bb2e842", + ) + + def test_get_request_returns_405(self): + response = self._get(kwargs={"version": "v4"}) + + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + + # Test headers + def test_options_headers(self): + response = self._options(kwargs={"version": "v2"}) + + headers = response.headers + + assert headers["accept"] == "text/*" + assert headers["access-control-allow-origin"] == "*" + assert headers["access-control-allow-method"] == "POST" + assert ( + headers["access-control-allow-headers"] + == "Origin, Content-Type, Accept, X-User-Agent" + ) + + def test_invalid_request_params(self): + query_params = {"pr": 9838, "flags": "flags!!!", "package": "codecov-cli/0.0.0"} + + response = self._post(kwargs={"version": "v5"}, query=query_params) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_invalid_request_params_uploader_package(self): + query_params = {"pr": 9838, "flags": "flags!!!", "package": "uploader-0.0.0"} + + response = self._post(kwargs={"version": "v5"}, query=query_params) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_invalid_request_params_invalid_package(self): + query_params = {"pr": 9838, "flags": "flags!!!", "package": ""} + + response = self._post(kwargs={"version": "v5"}, query=query_params) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @patch("shared.api_archive.archive.ArchiveService.write_file") + @patch("upload.views.legacy.get_redis_connection") + @patch("upload.views.legacy.uuid4") + @patch("upload.views.legacy.dispatch_upload_task") + @patch("services.repo_providers.RepoProviderService.get_adapter") + @override_settings(CODECOV_DASHBOARD_URL="https://app.codecov.io") + def test_successful_upload_v2( + self, + mock_repo_provider_service, + mock_dispatch_upload, + mock_uuid4, + mock_get_redis, + mock_write_file, + ): + class MockRepoProviderAdapter: + async def get_commit(self, commit, token): + return {"message": "This is not a merge commit"} + + mock_get_redis.return_value = MockRedis() + mock_repo_provider_service.return_value = MockRepoProviderAdapter() + mock_uuid4.return_value = ( + "dec1f00b-1883-40d0-afd6-6dcb876510be" # this will be the reportid + ) + + query_params = { + "commit": "b521e55aef79b101f48e2544837ca99a7fa3bf6b", + "token": "a03e5d02-9495-4413-b0d8-05651bb2e842", + "pr": "456", + "branch": "", + "flags": "", + "build_url": "", + "package": "", + } + + response = self._post( + kwargs={"version": "v2"}, query=query_params, data="coverage report" + ) + + assert response.status_code == 200 + + headers = response.headers + + assert headers["access-control-allow-origin"] == "*" + assert ( + headers["access-control-allow-headers"] + == "Origin, Content-Type, Accept, X-User-Agent" + ) + assert headers["content-type"] != "text/plain" + + archive_service = ArchiveService(self.repo) + datetime = timezone.now().strftime("%Y-%m-%d") + repo_hash = archive_service.get_archive_hash(self.repo) + expected_url = f"v4/raw/{datetime}/{repo_hash}/b521e55aef79b101f48e2544837ca99a7fa3bf6b/dec1f00b-1883-40d0-afd6-6dcb876510be.txt" + + mock_write_file.assert_called_with( + expected_url, b"coverage report", is_already_gzipped=False + ) + assert mock_dispatch_upload.call_args[0][0] == { + "commit": "b521e55aef79b101f48e2544837ca99a7fa3bf6b", + "token": "a03e5d02-9495-4413-b0d8-05651bb2e842", + "pr": "456", + "version": "v2", + "service": None, + "owner": None, + "repo": None, + "using_global_token": False, + "build_url": None, + "branch": None, + "reportid": "dec1f00b-1883-40d0-afd6-6dcb876510be", + "url": expected_url, + "job": None, + } + + result = loads(response.content) + assert result["message"] == "Coverage reports upload successfully" + assert result["uploaded"] == True + assert result["queued"] == True + assert result["id"] == "dec1f00b-1883-40d0-afd6-6dcb876510be" + assert ( + result["url"] + == "https://app.codecov.io/github/codecovtest/upload-test-repo/commit/b521e55aef79b101f48e2544837ca99a7fa3bf6b" + ) + + @patch("shared.api_archive.archive.ArchiveService.write_file") + @patch("upload.views.legacy.get_redis_connection") + @patch("upload.views.legacy.uuid4") + @patch("upload.views.legacy.dispatch_upload_task") + @patch("services.repo_providers.RepoProviderService.get_adapter") + @override_settings(CODECOV_DASHBOARD_URL="https://app.codecov.io") + def test_successful_upload_v2_slash( + self, + mock_repo_provider_service, + mock_dispatch_upload, + mock_uuid4, + mock_get_redis, + mock_write_file, + ): + class MockRepoProviderAdapter: + async def get_commit(self, commit, token): + return {"message": "This is not a merge commit"} + + mock_get_redis.return_value = MockRedis() + mock_repo_provider_service.return_value = MockRepoProviderAdapter() + mock_uuid4.return_value = ( + "dec1f00b-1883-40d0-afd6-6dcb876510be" # this will be the reportid + ) + + query_params = { + "commit": "b521e55aef79b101f48e2544837ca99a7fa3bf6b", + "token": "a03e5d02-9495-4413-b0d8-05651bb2e842", + "pr": "456", + "branch": "", + "flags": "", + "build_url": "", + "package": "", + } + + response = self._post_slash( + kwargs={"version": "v2"}, query=query_params, data="coverage report" + ) + + assert response.status_code == 200 + + headers = response.headers + + assert headers["access-control-allow-origin"] == "*" + assert ( + headers["access-control-allow-headers"] + == "Origin, Content-Type, Accept, X-User-Agent" + ) + assert headers["content-type"] != "text/plain" + + archive_service = ArchiveService(self.repo) + datetime = timezone.now().strftime("%Y-%m-%d") + repo_hash = archive_service.get_archive_hash(self.repo) + expected_url = f"v4/raw/{datetime}/{repo_hash}/b521e55aef79b101f48e2544837ca99a7fa3bf6b/dec1f00b-1883-40d0-afd6-6dcb876510be.txt" + + mock_write_file.assert_called_with( + expected_url, b"coverage report", is_already_gzipped=False + ) + assert mock_dispatch_upload.call_args[0][0] == { + "commit": "b521e55aef79b101f48e2544837ca99a7fa3bf6b", + "token": "a03e5d02-9495-4413-b0d8-05651bb2e842", + "pr": "456", + "version": "v2", + "service": None, + "owner": None, + "repo": None, + "using_global_token": False, + "build_url": None, + "branch": None, + "reportid": "dec1f00b-1883-40d0-afd6-6dcb876510be", + "url": expected_url, + "job": None, + } + + result = loads(response.content) + assert result["message"] == "Coverage reports upload successfully" + assert result["uploaded"] == True + assert result["queued"] == True + assert result["id"] == "dec1f00b-1883-40d0-afd6-6dcb876510be" + assert ( + result["url"] + == "https://app.codecov.io/github/codecovtest/upload-test-repo/commit/b521e55aef79b101f48e2544837ca99a7fa3bf6b" + ) + + @patch("upload.views.legacy.get_redis_connection") + @patch("upload.views.legacy.uuid4") + @patch("upload.views.legacy.determine_repo_for_upload") + @patch("services.repo_providers.RepoProviderService.get_adapter") + @override_settings(CODECOV_DASHBOARD_URL="https://app.codecov.io") + def test_repo_validation_error_v2( + self, + mock_repo_provider_service, + mock_determine_repo_for_upload, + mock_uuid4, + mock_get_redis, + ): + class MockRepoProviderAdapter: + async def get_commit(self, commit, token): + return {"message": "This is not a merge commit"} + + mock_get_redis.return_value = MockRedis() + mock_repo_provider_service.return_value = MockRepoProviderAdapter() + mock_uuid4.return_value = ( + "dec1f00b-1883-40d0-afd6-6dcb876510be" # this will be the reportid + ) + mock_determine_repo_for_upload.side_effect = ValidationError( + "Unable to determine repo and owner" + ) + + query_params = { + "commit": "b521e55aef79b101f48e2544837ca99a7fa3bf6b", + "token": "a03e5d02-9495-4413-b0d8-05651bb2e842", + "pr": "456", + "branch": "", + "flags": "", + "build_url": "", + "package": "", + } + + response = self._post_slash( + kwargs={"version": "v2"}, query=query_params, data="coverage report" + ) + + assert response.status_code == 400 + + headers = response.headers + + assert headers["access-control-allow-origin"] == "*" + assert ( + headers["access-control-allow-headers"] + == "Origin, Content-Type, Accept, X-User-Agent" + ) + assert headers["content-type"] != "text/plain" + + assert response.content == b"Could not determine repo and owner" + + @patch("upload.views.legacy.get_redis_connection") + @patch("upload.views.legacy.uuid4") + @patch("upload.views.legacy.determine_repo_for_upload") + @patch("services.repo_providers.RepoProviderService.get_adapter") + @override_settings(CODECOV_DASHBOARD_URL="https://app.codecov.io") + def test_too_many_repos_found_v2( + self, + mock_repo_provider_service, + mock_determine_repo_for_upload, + mock_uuid4, + mock_get_redis, + ): + class MockRepoProviderAdapter: + async def get_commit(self, commit, token): + return {"message": "This is not a merge commit"} + + mock_get_redis.return_value = MockRedis() + mock_repo_provider_service.return_value = MockRepoProviderAdapter() + mock_uuid4.return_value = ( + "dec1f00b-1883-40d0-afd6-6dcb876510be" # this will be the reportid + ) + mock_determine_repo_for_upload.side_effect = MultipleObjectsReturned( + "Found too many repos" + ) + + query_params = { + "commit": "b521e55aef79b101f48e2544837ca99a7fa3bf6b", + "token": "a03e5d02-9495-4413-b0d8-05651bb2e842", + "pr": "456", + "branch": "", + "flags": "", + "build_url": "", + "package": "", + } + + response = self._post_slash( + kwargs={"version": "v2"}, query=query_params, data="coverage report" + ) + + assert response.status_code == 400 + + headers = response.headers + + assert headers["access-control-allow-origin"] == "*" + assert ( + headers["access-control-allow-headers"] + == "Origin, Content-Type, Accept, X-User-Agent" + ) + assert headers["content-type"] != "text/plain" + + assert response.content == b"Found too many repos" + + @patch("shared.api_archive.archive.ArchiveService.create_presigned_put") + @patch("shared.api_archive.archive.ArchiveService.get_archive_hash") + @patch("upload.views.legacy.get_redis_connection") + @patch("upload.views.legacy.uuid4") + @patch("upload.views.legacy.dispatch_upload_task") + @patch("services.repo_providers.RepoProviderService.get_adapter") + def test_upload_v4( + self, + mock_repo_provider_service, + mock_dispatch_upload, + mock_uuid4, + mock_get_redis, + mock_hash, + mock_storage_put, + ): + class MockRepoProviderAdapter: + async def get_commit(self, commit, token): + return {"message": "This is not a merge commit"} + + path = "/".join( + ( + "v4/raw", + timezone.now().strftime("%Y-%m-%d"), + "awawaw", + "b521e55aef79b101f48e2544837ca99a7fa3bf6b", + ) + ) + + mock_storage_put.return_value = path + "?AWS=PARAMS" + mock_get_redis.return_value = MockRedis() + mock_repo_provider_service.return_value = MockRepoProviderAdapter() + mock_uuid4.return_value = ( + "dec1f00b-1883-40d0-afd6-6dcb876510be" # this will be the reportid + ) + mock_hash.return_value = "awawaw" + query_params = { + "commit": "b521e55aef79b101f48e2544837ca99a7fa3bf6b", + "token": "a03e5d02-9495-4413-b0d8-05651bb2e842", + "pr": "456", + "branch": "", + "flags": "", + "build_url": "", + "package": "", + } + + response = self._post( + kwargs={"version": "v4"}, query=query_params, data="coverage report" + ) + + assert response.status_code == 200 + + @patch("shared.api_archive.archive.ArchiveService.create_presigned_put") + @patch("shared.api_archive.archive.ArchiveService.get_archive_hash") + @patch("upload.views.legacy.get_redis_connection") + @patch("upload.views.legacy.uuid4") + @patch("upload.views.legacy.dispatch_upload_task") + @patch("services.repo_providers.RepoProviderService.get_adapter") + def test_upload_v4_with_upload_token_header( + self, + mock_repo_provider_service, + mock_dispatch_upload, + mock_uuid4, + mock_get_redis, + mock_hash, + mock_storage_put, + ): + class MockRepoProviderAdapter: + async def get_commit(self, commit, token): + return {"message": "This is not a merge commit"} + + path = "/".join( + ( + "v4/raw", + timezone.now().strftime("%Y-%m-%d"), + "awawaw", + "b521e55aef79b101f48e2544837ca99a7fa3bf6b", + ) + ) + + mock_storage_put.return_value = path + "?AWS=PARAMS" + mock_get_redis.return_value = MockRedis() + mock_repo_provider_service.return_value = MockRepoProviderAdapter() + mock_uuid4.return_value = ( + "dec1f00b-1883-40d0-afd6-6dcb876510be" # this will be the reportid + ) + mock_hash.return_value = "awawaw" + query_params = { + "commit": "b521e55aef79b101f48e2544837ca99a7fa3bf6b", + "pr": "456", + "branch": "", + "flags": "", + "build_url": "", + "package": "", + } + + response = self._post( + kwargs={"version": "v4"}, + query=query_params, + data="coverage report", + headers={"HTTP_X_UPLOAD_TOKEN": "a03e5d02-9495-4413-b0d8-05651bb2e842"}, + ) + + assert response.status_code == 200 + + @patch("shared.api_archive.archive.ArchiveService.create_presigned_put") + @patch("shared.api_archive.archive.ArchiveService.get_archive_hash") + @patch("upload.views.legacy.get_redis_connection") + @patch("upload.views.legacy.uuid4") + @patch("upload.views.legacy.dispatch_upload_task") + @patch("services.repo_providers.RepoProviderService.get_adapter") + @patch("upload.views.legacy.determine_repo_for_upload") + def test_repo_validation_error_v4( + self, + mock_determine_repo_for_upload, + mock_repo_provider_service, + mock_dispatch_upload, + mock_uuid4, + mock_get_redis, + mock_hash, + mock_storage_put, + ): + mock_determine_repo_for_upload.side_effect = ValidationError( + "Unable to determine repo and owner" + ) + + class MockRepoProviderAdapter: + async def get_commit(self, commit, token): + return {"message": "This is not a merge commit"} + + path = "/".join( + ( + "v4/raw", + timezone.now().strftime("%Y-%m-%d"), + "awawaw", + "b521e55aef79b101f48e2544837ca99a7fa3bf6b", + ) + ) + + mock_storage_put.return_value = path + "?AWS=PARAMS" + mock_get_redis.return_value = MockRedis() + mock_repo_provider_service.return_value = MockRepoProviderAdapter() + mock_uuid4.return_value = ( + "dec1f00b-1883-40d0-afd6-6dcb876510be" # this will be the reportid + ) + mock_hash.return_value = "awawaw" + query_params = { + "commit": "b521e55aef79b101f48e2544837ca99a7fa3bf6b", + "token": "a03e5d02-9495-4413-b0d8-05651bb2e842", + "pr": "456", + "branch": "", + "flags": "", + "build_url": "", + "package": "", + } + + response = self._post( + kwargs={"version": "v4"}, query=query_params, data="coverage report" + ) + + assert response.status_code == 400 + + headers = response.headers + + assert headers["access-control-allow-origin"] == "*" + assert ( + headers["access-control-allow-headers"] + == "Origin, Content-Type, Accept, X-User-Agent" + ) + assert headers["content-type"] != "text/plain" + + assert response.content == b"Could not determine repo and owner" + + @patch("shared.api_archive.archive.ArchiveService.create_presigned_put") + @patch("shared.api_archive.archive.ArchiveService.get_archive_hash") + @patch("upload.views.legacy.get_redis_connection") + @patch("upload.views.legacy.uuid4") + @patch("upload.views.legacy.dispatch_upload_task") + @patch("services.repo_providers.RepoProviderService.get_adapter") + @patch("upload.views.legacy.determine_repo_for_upload") + def test_too_many_repos_found_v4( + self, + mock_determine_repo_for_upload, + mock_repo_provider_service, + mock_dispatch_upload, + mock_uuid4, + mock_get_redis, + mock_hash, + mock_storage_put, + ): + mock_determine_repo_for_upload.side_effect = MultipleObjectsReturned( + "Found too many repos" + ) + + class MockRepoProviderAdapter: + async def get_commit(self, commit, token): + return {"message": "This is not a merge commit"} + + path = "/".join( + ( + "v4/raw", + timezone.now().strftime("%Y-%m-%d"), + "awawaw", + "b521e55aef79b101f48e2544837ca99a7fa3bf6b", + ) + ) + + mock_storage_put.return_value = path + "?AWS=PARAMS" + mock_get_redis.return_value = MockRedis() + mock_repo_provider_service.return_value = MockRepoProviderAdapter() + mock_uuid4.return_value = ( + "dec1f00b-1883-40d0-afd6-6dcb876510be" # this will be the reportid + ) + mock_hash.return_value = "awawaw" + query_params = { + "commit": "b521e55aef79b101f48e2544837ca99a7fa3bf6b", + "token": "a03e5d02-9495-4413-b0d8-05651bb2e842", + "pr": "456", + "branch": "", + "flags": "", + "build_url": "", + "package": "", + } + + response = self._post( + kwargs={"version": "v4"}, query=query_params, data="coverage report" + ) + + assert response.status_code == 400 + + headers = response.headers + + assert headers["access-control-allow-origin"] == "*" + assert ( + headers["access-control-allow-headers"] + == "Origin, Content-Type, Accept, X-User-Agent" + ) + assert headers["content-type"] != "text/plain" + + assert response.content == b"Found too many repos" + + +class UploadHandlerTravisTokenlessTest(TestCase): + @patch.object(requests, "get") + def test_travis_no_slug_match(self, mock_get): + expected_response = { + "id": 732059764, + "allow_failure": None, + "number": "498.1", + "state": "passed", + "started_at": "2020-10-01T20:01:31Z", + "finished_at": "2020-10-01T20:02:55Z", + "build": { + "@type": "build", + "@href": "/build/732059763", + "@representation": "minimal", + "id": 732059763, + "number": "498", + "state": "passed", + "duration": 84, + "event_type": "push", + "previous_state": "passed", + "pull_request_title": None, + "pull_request_number": None, + "started_at": "2020-10-01T20:01:31Z", + "finished_at": "2020-10-01T20:02:55Z", + "private": False, + "priority": False, + }, + "queue": "builds.gce", + "repository": { + "@type": "repository", + "@href": "/repo/25205338", + "@representation": "minimal", + "id": 25205338, + "name": "python-standard", + "slug": "codecov/python-standard", + }, + "commit": { + "@type": "commit", + "@representation": "minimal", + "id": 226208830, + "sha": "2485b28f9862e98bcee576f02d8b37e6433f8c30", + "ref": "refs/heads/master", + "message": "New Build: 10/01/20 20:00:54", + "compare_url": "https://github.com/codecov/python-standard/compare/28392734979c...2485b28f9862", + "committed_at": "2020-10-01T20:00:55Z", + }, + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "slug": "codecov/codecov-api", + "owner": "codecov", + "repo": "codecov-api", + "token": "4a24929b-9276-4784-8e85-a7a008a32037", + "service": "circleci", + "pr": None, + "pull_request": None, + "flags": "this-is-a-flag,this-is-another-flag", + "param_doesn't_exist_but_still_should_not_error": True, + "s3": 123, + "build_url": "https://thisisabuildurl.com", + "job": 732059764, + "using_global_token": False, + "branch": None, + "_did_change_merge_commit": False, + "parent": "123abc", + } + + expected_error = """ + ERROR: Tokenless uploads are only supported for public repositories on Travis that can be verified through the Travis API. Please use an upload token if your repository is private and specify it via the -t flag. You can find the token for this repository at the url below on codecov.io (login required): + + Repo token: https://codecov.io/gh/codecov/codecov-api/settings + Documentation: https://docs.codecov.io/docs/about-the-codecov-bash-uploader#section-upload-token""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("travis", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("something", params).verify_upload() + + @patch.object(requests, "get") + def test_travis_no_sha_match(self, mock_get): + expected_response = { + "id": 732059764, + "allow_failure": None, + "number": "498.1", + "state": "passed", + "started_at": "2020-10-01T20:01:31Z", + "finished_at": "2020-10-01T20:02:55Z", + "build": { + "@type": "build", + "@href": "/build/732059763", + "@representation": "minimal", + "id": 732059763, + "number": "498", + "state": "passed", + "duration": 84, + "event_type": "push", + "previous_state": "passed", + "pull_request_title": None, + "pull_request_number": None, + "started_at": "2020-10-01T20:01:31Z", + "finished_at": "2020-10-01T20:02:55Z", + "private": False, + "priority": False, + }, + "queue": "builds.gce", + "repository": { + "@type": "repository", + "@href": "/repo/25205338", + "@representation": "minimal", + "id": 25205338, + "name": "python-standard", + "slug": "codecov/codecov-api", + }, + "commit": { + "@type": "commit", + "@representation": "minimal", + "id": 226208830, + "sha": "2485b28f9862e98bcee576f02d8b37e6433f8c30", + "ref": "refs/heads/master", + "message": "New Build: 10/01/20 20:00:54", + "compare_url": "https://github.com/codecov/python-standard/compare/28392734979c...2485b28f9862", + "committed_at": "2020-10-01T20:00:55Z", + }, + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "slug": "codecov/codecov-api", + "owner": "codecov", + "repo": "codecov-api", + "token": "4a24929b-9276-4784-8e85-a7a008a32037", + "service": "circleci", + "pr": None, + "pull_request": None, + "flags": "this-is-a-flag,this-is-another-flag", + "param_doesn't_exist_but_still_should_not_error": True, + "s3": 123, + "build_url": "https://thisisabuildurl.com", + "job": 732059764, + "using_global_token": False, + "branch": None, + "_did_change_merge_commit": False, + "parent": "123abc", + } + + expected_error = """ + ERROR: Tokenless uploads are only supported for public repositories on Travis that can be verified through the Travis API. Please use an upload token if your repository is private and specify it via the -t flag. You can find the token for this repository at the url below on codecov.io (login required): + + Repo token: https://codecov.io/gh/codecov/codecov-api/settings + Documentation: https://docs.codecov.io/docs/about-the-codecov-bash-uploader#section-upload-token""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("travis", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_travis_no_event_match(self, mock_get): + expected_response = { + "id": 732059764, + "allow_failure": None, + "number": "498.1", + "state": "passed", + "started_at": "2020-10-01T20:01:31Z", + "finished_at": "2020-10-01T20:02:55Z", + "build": { + "@type": "build", + "@href": "/build/732059763", + "@representation": "minimal", + "id": 732059763, + "number": "498", + "state": "passed", + "duration": 84, + "event_type": "push", + "previous_state": "passed", + "pull_request_title": None, + "pull_request_number": None, + "started_at": "2020-10-01T20:01:31Z", + "finished_at": "2020-10-01T20:02:55Z", + "private": False, + "priority": False, + }, + "queue": "builds.gce", + "repository": { + "@type": "repository", + "@href": "/repo/25205338", + "@representation": "minimal", + "id": 25205338, + "name": "python-standard", + "slug": "codecov/codecov-api", + }, + "commit": { + "@type": "commit", + "@representation": "minimal", + "id": 226208830, + "sha": "2485b28f9862e98bcee576f02d8b37e6433f8c30", + "ref": "refs/heads/master", + "message": "New Build: 10/01/20 20:00:54", + "compare_url": "https://github.com/codecov/python-standard/compare/28392734979c...2485b28f9862", + "committed_at": "2020-10-01T20:00:55Z", + }, + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "slug": "codecov/codecov-api", + "owner": "codecov", + "repo": "codecov-api", + "token": "4a24929b-9276-4784-8e85-a7a008a32037", + "service": "circleci", + "pr": None, + "pull_request": None, + "flags": "this-is-a-flag,this-is-another-flag", + "param_doesn't_exist_but_still_should_not_error": True, + "s3": 123, + "build_url": "https://thisisabuildurl.com", + "job": 732059764, + "using_global_token": False, + "branch": None, + "_did_change_merge_commit": False, + "parent": "123abc", + } + + expected_error = """ + ERROR: Tokenless uploads are only supported for public repositories on Travis that can be verified through the Travis API. Please use an upload token if your repository is private and specify it via the -t flag. You can find the token for this repository at the url below on codecov.io (login required): + + Repo token: https://codecov.io/gh/codecov/codecov-api/settings + Documentation: https://docs.codecov.io/docs/about-the-codecov-bash-uploader#section-upload-token""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("travis", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_travis_failed_requests(self, mock_get): + mock_get.side_effect = [ + requests.exceptions.ConnectionError("Not found"), + requests.exceptions.ConnectionError("Not found"), + ] + params = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "slug": "codecov/codecov-api", + "owner": "codecov", + "repo": "codecov-api", + "token": "4a24929b-9276-4784-8e85-a7a008a32037", + "service": "circleci", + "pr": None, + "pull_request": None, + "flags": "this-is-a-flag,this-is-another-flag", + "param_doesn't_exist_but_still_should_not_error": True, + "s3": 123, + "build_url": "https://thisisabuildurl.com", + "job": 732059764, + "using_global_token": False, + "branch": None, + "_did_change_merge_commit": False, + "parent": "123abc", + } + + expected_error = """ + ERROR: Tokenless uploads are only supported for public repositories on Travis that can be verified through the Travis API. Please use an upload token if your repository is private and specify it via the -t flag. You can find the token for this repository at the url below on codecov.io (login required): + + Repo token: https://codecov.io/gh/codecov/codecov-api/settings + Documentation: https://docs.codecov.io/docs/about-the-codecov-bash-uploader#section-upload-token""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("travis", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_travis_failed_requests_connection_error(self, mock_get): + mock_get.side_effect = [ + requests.exceptions.HTTPError("Not found"), + requests.exceptions.HTTPError("Not found"), + ] + params = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "slug": "codecov/codecov-api", + "owner": "codecov", + "repo": "codecov-api", + "token": "4a24929b-9276-4784-8e85-a7a008a32037", + "service": "circleci", + "pr": None, + "pull_request": None, + "flags": "this-is-a-flag,this-is-another-flag", + "param_doesn't_exist_but_still_should_not_error": True, + "s3": 123, + "build_url": "https://thisisabuildurl.com", + "job": 732059764, + "using_global_token": False, + "branch": None, + "_did_change_merge_commit": False, + "parent": "123abc", + } + + expected_error = """ + ERROR: Tokenless uploads are only supported for public repositories on Travis that can be verified through the Travis API. Please use an upload token if your repository is private and specify it via the -t flag. You can find the token for this repository at the url below on codecov.io (login required): + + Repo token: https://codecov.io/gh/codecov/codecov-api/settings + Documentation: https://docs.codecov.io/docs/about-the-codecov-bash-uploader#section-upload-token""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("travis", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_travis_failed_requests_connection_error_ex(self, mock_get): + mock_get.side_effect = [ + Exception("Not found"), + requests.exceptions.HTTPError("Not found"), + ] + params = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "slug": "codecov/codecov-api", + "owner": "codecov", + "repo": "codecov-api", + "token": "4a24929b-9276-4784-8e85-a7a008a32037", + "service": "circleci", + "pr": None, + "pull_request": None, + "flags": "this-is-a-flag,this-is-another-flag", + "param_doesn't_exist_but_still_should_not_error": True, + "s3": 123, + "build_url": "https://thisisabuildurl.com", + "job": 732059764, + "using_global_token": False, + "branch": None, + "_did_change_merge_commit": False, + "parent": "123abc", + } + + expected_error = """ + ERROR: Tokenless uploads are only supported for public repositories on Travis that can be verified through the Travis API. Please use an upload token if your repository is private and specify it via the -t flag. You can find the token for this repository at the url below on codecov.io (login required): + + Repo token: https://codecov.io/gh/codecov/codecov-api/settings + Documentation: https://docs.codecov.io/docs/about-the-codecov-bash-uploader#section-upload-token""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("travis", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_build_not_in_progress(self, mock_get): + expected_response = { + "id": 732059764, + "allow_failure": None, + "number": "498.1", + "state": "passed", + "started_at": "2020-10-01T20:01:31Z", + "finished_at": None, + "build": { + "@type": "build", + "@href": "/build/732059763", + "@representation": "minimal", + "id": 732059763, + "number": "498", + "state": "passed", + "duration": 84, + "event_type": "push", + "previous_state": "passed", + "pull_request_title": None, + "pull_request_number": None, + "started_at": "2020-10-01T20:01:31Z", + "finished_at": "2020-10-01T20:02:55Z", + "private": False, + "priority": False, + }, + "queue": "builds.gce", + "repository": { + "@type": "repository", + "@href": "/repo/25205338", + "@representation": "minimal", + "id": 25205338, + "name": "python-standard", + "slug": "codecov/codecov-api", + }, + "commit": { + "@type": "commit", + "@representation": "minimal", + "id": 226208830, + "sha": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "ref": "refs/heads/master", + "message": "New Build: 10/01/20 20:00:54", + "compare_url": "https://github.com/codecov/python-standard/compare/28392734979c...2485b28f9862", + "committed_at": "2020-10-01T20:00:55Z", + }, + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "slug": "codecov/codecov-api", + "owner": "codecov", + "repo": "codecov-api", + "token": "4a24929b-9276-4784-8e85-a7a008a32037", + "service": "circleci", + "pr": None, + "pull_request": None, + "flags": "this-is-a-flag,this-is-another-flag", + "param_doesn't_exist_but_still_should_not_error": True, + "s3": 123, + "build_url": "https://thisisabuildurl.com", + "job": 732059764, + "using_global_token": False, + "branch": None, + "_did_change_merge_commit": False, + "parent": "123abc", + } + + expected_error = """ + ERROR: The build status does not indicate that the current build is in progress. Please make sure the build is in progress or was finished within the past 4 minutes to ensure reports upload properly.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("travis", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_travis_no_job(self, mock_get): + mock_get.side_effect = [requests.exceptions.HTTPError("Not found"), None] + params = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "slug": "codecov/codecov-api", + "owner": "codecov", + "repo": "codecov-api", + "token": "4a24929b-9276-4784-8e85-a7a008a32037", + "service": "circleci", + "pr": None, + "pull_request": None, + "flags": "this-is-a-flag,this-is-another-flag", + "param_doesn't_exist_but_still_should_not_error": True, + "s3": 123, + "build_url": "https://thisisabuildurl.com", + "job": 732059764, + "using_global_token": False, + "branch": None, + "_did_change_merge_commit": False, + "parent": "123abc", + } + + expected_error = """ + ERROR: Tokenless uploads are only supported for public repositories on Travis that can be verified through the Travis API. Please use an upload token if your repository is private and specify it via the -t flag. You can find the token for this repository at the url below on codecov.io (login required): + + Repo token: https://codecov.io/gh/codecov/codecov-api/settings + Documentation: https://docs.codecov.io/docs/about-the-codecov-bash-uploader#section-upload-token""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("travis", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_success(self, mock_get): + expected_response = { + "id": 732059764, + "allow_failure": None, + "number": "498.1", + "state": "passed", + "started_at": "2020-10-01T20:02:55Z", + "finished_at": f"{datetime.now()}".split(".")[0], + "build": { + "@type": "build", + "@href": "/build/732059763", + "@representation": "minimal", + "id": 732059763, + "number": "498", + "state": "passed", + "duration": 84, + "event_type": "push", + "previous_state": "passed", + "pull_request_title": None, + "pull_request_number": None, + "started_at": "2020-10-01T20:01:31Z", + "finished_at": "2020-10-01T20:02:55Z", + "private": False, + "priority": False, + }, + "queue": "builds.gce", + "repository": { + "@type": "repository", + "@href": "/repo/25205338", + "@representation": "minimal", + "id": 25205338, + "name": "python-standard", + "slug": "codecov/codecov-api", + }, + "commit": { + "@type": "commit", + "@representation": "minimal", + "id": 226208830, + "sha": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "ref": "refs/heads/master", + "message": "New Build: 10/01/20 20:00:54", + "compare_url": "https://github.com/codecov/python-standard/compare/28392734979c...2485b28f9862", + "committed_at": "2020-10-01T20:00:55Z", + }, + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "slug": "codecov/codecov-api", + "owner": "codecov", + "repo": "codecov-api", + "token": "4a24929b-9276-4784-8e85-a7a008a32037", + "service": "circleci", + "pr": None, + "pull_request": None, + "flags": "this-is-a-flag,this-is-another-flag", + "param_doesn't_exist_but_still_should_not_error": True, + "s3": 123, + "build_url": "https://thisisabuildurl.com", + "job": 732059764, + "using_global_token": False, + "branch": None, + "_did_change_merge_commit": False, + "parent": "123abc", + } + + res = TokenlessUploadHandler("travis", params).verify_upload() + + assert res == "github" + + @patch.object(requests, "get") + def test_expired_build(self, mock_get): + expected_response = { + "id": 732059764, + "allow_failure": None, + "number": "498.1", + "state": "passed", + "started_at": "2020-10-01T20:02:55Z", + "finished_at": "2020-10-01T20:02:55Z", + "build": { + "@type": "build", + "@href": "/build/732059763", + "@representation": "minimal", + "id": 732059763, + "number": "498", + "state": "passed", + "duration": 84, + "event_type": "push", + "previous_state": "passed", + "pull_request_title": None, + "pull_request_number": None, + "started_at": "2020-10-01T20:01:31Z", + "finished_at": "2020-10-01T20:02:55Z", + "private": False, + "priority": False, + }, + "queue": "builds.gce", + "repository": { + "@type": "repository", + "@href": "/repo/25205338", + "@representation": "minimal", + "id": 25205338, + "name": "python-standard", + "slug": "codecov/codecov-api", + }, + "commit": { + "@type": "commit", + "@representation": "minimal", + "id": 226208830, + "sha": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "ref": "refs/heads/master", + "message": "New Build: 10/01/20 20:00:54", + "compare_url": "https://github.com/codecov/python-standard/compare/28392734979c...2485b28f9862", + "committed_at": "2020-10-01T20:00:55Z", + }, + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "version": "v4", + "commit": "3be5c52bd748c508a7e96993c02cf3518c816e84", + "slug": "codecov/codecov-api", + "owner": "codecov", + "repo": "codecov-api", + "token": "4a24929b-9276-4784-8e85-a7a008a32037", + "service": "circleci", + "pr": None, + "pull_request": None, + "flags": "this-is-a-flag,this-is-another-flag", + "param_doesn't_exist_but_still_should_not_error": True, + "s3": 123, + "build_url": "https://thisisabuildurl.com", + "job": 732059764, + "using_global_token": False, + "branch": None, + "_did_change_merge_commit": False, + "parent": "123abc", + } + + expected_error = """ + ERROR: The coverage upload was rejected because the build is out of date. Please make sure the build is not stale for uploads to process correctly.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("travis", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + +class UploadHandlerAzureTokenlessTest(TestCase): + def test_azure_no_job(self): + params = {} + + expected_error = """Missing "job" argument. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("azure_pipelines", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + def test_azure_no_project(self): + params = {"job": 732059764} + + expected_error = """Missing "project" argument. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("azure_pipelines", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + def test_azure_no_server_uri(self): + params = {"project": "project123", "job": 732059764} + + expected_error = """Missing "server_uri" argument. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("azure_pipelines", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + def test_azure_invalid_server_uri(self): + expected_error = """Unable to locate build via Azure API. Please upload with the Codecov repository upload token to resolve issue.""" + + params = { + "project": "project123", + "job": 732059764, + "server_uri": "https://dev.azure.com/missing_trailing_slash", + } + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("azure_pipelines", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + params["server_uri"] = "https://missing_trailing_slash.visualstudio.com" + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("azure_pipelines", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + params["server_uri"] = "https://example.visualstudio.com.attacker.com/" + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("azure_pipelines", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + params["server_uri"] = "https://dev.azure.com.attacker.com/example" + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("azure_pipelines", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + params["server_uri"] = "https://dev.azure.attacker.com/example" + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("azure_pipelines", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_azure_http_error(self, mock_get): + mock_get.side_effect = [requests.exceptions.HTTPError("Not found")] + + params = { + "project": "project123", + "job": 732059764, + "server_uri": "https://dev.azure.com/example/", + } + + expected_error = """Unable to locate build via Azure API. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("azure_pipelines", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_azure_connection_error(self, mock_get): + mock_get.side_effect = [requests.exceptions.ConnectionError("Not found")] + + params = { + "project": "project123", + "job": 732059764, + "server_uri": "https://dev.azure.com/example/", + } + + expected_error = """Unable to locate build via Azure API. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("azure_pipelines", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_azure_no_errors(self, mock_get): + expected_response = { + "finishTime": "NOW", + "buildNumber": "20190725.8", + "status": "inProgress", + "sourceVersion": "c739768fcac68144a3a6d82305b9c4106934d31a", + "project": {"visibility": "public"}, + "repository": {"type": "GitHub"}, + "triggerInfo": {"pr.sourceSha": "c739768fcac68144a3a6d82305b9c4106934d31a"}, + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "project": "project123", + "job": 732059764, + "server_uri": "https://dev.azure.com/example/", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + "build": "20190725.8", + } + + res = TokenlessUploadHandler("azure_pipelines", params).verify_upload() + + assert res == "github" + + @patch.object(requests, "get") + def test_azure_wrong_build_number(self, mock_get): + expected_response = { + "finishTime": f"{datetime.now()}", + "buildNumber": "BADBUILDNUM", + "status": "completed", + "sourceVersion": "c739768fcac68144a3a6d82305b9c4106934d31a", + "project": {"visibility": "public"}, + "repository": {"type": "GitHub"}, + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "project": "project123", + "job": 732059764, + "server_uri": "https://dev.azure.com/example/", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + "build": "20190725.8", + } + + expected_error = """Build numbers do not match. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("azure_pipelines", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_azure_expired_build(self, mock_get): + expected_response = { + "finishTime": f"{datetime.now() - timedelta(minutes=4)}", + "buildNumber": "20190725.8", + "status": "completed", + "sourceVersion": "c739768fcac68144a3a6d82305b9c4106934d31a", + "project": {"visibility": "public"}, + "repository": {"type": "GitHub"}, + } + + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "project": "project123", + "job": 732059764, + "server_uri": "https://dev.azure.com/example/", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + "build": "20190725.8", + } + + expected_error = """Azure build has already finished. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("azure_pipelines", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_azure_invalid_status(self, mock_get): + expected_response = { + "finishTime": f"{datetime.now()}", + "buildNumber": "20190725.8", + "status": "BADSTATUS", + "sourceVersion": "c739768fcac68144a3a6d82305b9c4106934d31a", + "project": {"visibility": "public"}, + "repository": {"type": "GitHub"}, + } + + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "project": "project123", + "job": 732059764, + "server_uri": "https://dev.azure.com/example/", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + "build": "20190725.8", + } + + expected_error = """Azure build has already finished. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("azure_pipelines", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_azure_wrong_commit(self, mock_get): + expected_response = { + "finishTime": "NOW", + "buildNumber": "20190725.8", + "status": "inProgress", + "sourceVersion": "BADSHA", + "project": {"visibility": "public"}, + "repository": {"type": "GitHub"}, + } + + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "project": "project123", + "job": 732059764, + "server_uri": "https://dev.azure.com/example/", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + "build": "20190725.8", + } + + expected_error = """Commit sha does not match Azure build. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("azure_pipelines", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_azure_not_public(self, mock_get): + expected_response = """ + + + Azure DevOps Services | Sign In + + """ + + mock_get.return_value.status_code.return_value = 203 + mock_get.return_value.json.side_effect = JSONDecodeError( + "Expecting value: line 1 column 1", expected_response, 0 + ) + + params = { + "project": "project123", + "job": 732059764, + "server_uri": "https://dev.azure.com/example/", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + "build": "20190725.8", + } + + expected_error = """Unable to locate build via Azure API. Project is likely private, please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("azure_pipelines", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_azure_wrong_service_type(self, mock_get): + expected_response = { + "finishTime": "NOW", + "buildNumber": "20190725.8", + "status": "inProgress", + "sourceVersion": "c739768fcac68144a3a6d82305b9c4106934d31a", + "project": {"visibility": "public"}, + "repository": {"type": "BADREPOTYPE"}, + } + + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "project": "project123", + "job": 732059764, + "server_uri": "https://dev.azure.com/example/", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + "build": "20190725.8", + } + + expected_error = """Sorry this service is not supported. Codecov currently only works with GitHub, GitLab, and BitBucket repositories""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("azure_pipelines", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + +class UploadHandlerAppveyorTokenlessTest(TestCase): + def test_appveyor_no_job(self): + params = {} + + expected_error = """Missing "job" argument. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("appveyor", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_appveyor_http_error(self, mock_get): + mock_get.side_effect = [requests.exceptions.HTTPError("Not found")] + + params = { + "project": "project123", + "job": "732059764", + "server_uri": "https://dev.azure.com/example/", + } + + expected_error = """Unable to locate build via Appveyor API. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("appveyor", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_appveyor_connection_error(self, mock_get): + mock_get.side_effect = [requests.exceptions.ConnectionError("Not found")] + + params = { + "project": "project123", + "job": "something/else/732059764", + "server_uri": "https://dev.azure.com/example/", + } + + expected_error = """Unable to locate build via Appveyor API. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("appveyor", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_appveyor_finished_build(self, mock_get): + expected_response = { + "build": {"jobs": [{"jobId": "732059764"}]}, + "finishTime": "NOW", + "buildNumber": "20190725.8", + "status": "inProgress", + "sourceVersion": "c739768fcac68144a3a6d82305b9c4106934d31a", + "project": {"visibility": "public", "repositoryType": "github"}, + "repository": {"type": "GitHub"}, + "triggerInfo": {"pr.sourceSha": "c739768fcac68144a3a6d82305b9c4106934d31a"}, + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "project": "project123", + "job": "732059764", + "server_uri": "https://dev.azure.com/example/", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + "build": "20190725.8", + } + + expected_error = """Build already finished, unable to accept new reports. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("appveyor", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_appveyor_no_errors(self, mock_get): + expected_response = { + "build": {"jobs": [{"jobId": "732059764"}]}, + "finishTime": "NOW", + "buildNumber": "20190725.8", + "status": "inProgress", + "sourceVersion": "c739768fcac68144a3a6d82305b9c4106934d31a", + "project": {"visibility": "public", "repositoryType": "github"}, + "repository": {"type": "GitHub"}, + "triggerInfo": {"pr.sourceSha": "c739768fcac68144a3a6d82305b9c4106934d31a"}, + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "project": "project123", + "job": "732059764", + "server_uri": "https://dev.azure.com/example/", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + "build": "732059764", + } + + res = TokenlessUploadHandler("appveyor", params).verify_upload() + + assert res == "github" + + @patch.object(requests, "get") + def test_appveyor_invalid_service(self, mock_get): + expected_response = { + "build": {"jobs": [{"jobId": "732059764"}]}, + "finishTime": "NOW", + "buildNumber": "20190725.8", + "status": "inProgress", + "sourceVersion": "c739768fcac68144a3a6d82305b9c4106934d31a", + "project": {"visibility": "public", "repositoryType": "gitthub"}, + "repository": {"type": "GittHub"}, + "triggerInfo": {"pr.sourceSha": "c739768fcac68144a3a6d82305b9c4106934d31a"}, + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "project": "project123", + "job": "732059764", + "server_uri": "https://dev.azure.com/example/", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + "build": "732059764", + } + expected_error = """Sorry this service is not supported. Codecov currently only works with GitHub, GitLab, and BitBucket repositories""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("appveyor", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + +class UploadHandlerCircleciTokenlessTest(TestCase): + def test_circleci_no_build(self): + params = {} + + expected_error = """Missing "build" argument. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("circleci", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + def test_circleci_no_owner(self): + params = {"build": 1234} + + expected_error = """Missing "owner" argument. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("circleci", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + def test_circleci_no_repo(self): + params = {"build": "12.34", "owner": "owner"} + + expected_error = """Missing "repo" argument. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("circleci", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_circleci_http_error(self, mock_get): + mock_get.side_effect = [requests.exceptions.HTTPError("Not found")] + + params = {"build": "12.34", "owner": "owner", "repo": "repo"} + + expected_error = """Unable to locate build via CircleCI API. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("circleci", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_circleci_connection_error(self, mock_get): + mock_get.side_effect = [requests.exceptions.ConnectionError("Not found")] + + params = {"build": "12.34", "owner": "owner", "repo": "repo"} + + expected_error = """Unable to locate build via CircleCI API. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("circleci", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_circleci_invalid_commit(self, mock_get): + expected_response = {"vcs_revision": "739768fcac68144a3a6d82305b9c4106934d31a"} + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "build": "12.34", + "owner": "owner", + "repo": "repo", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + } + expected_error = """Commit sha does not match Circle build. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("circleci", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_circleci_invalid_stop_time(self, mock_get): + expected_response = { + "vcs_revision": "c739768fcac68144a3a6d82305b9c4106934d31a", + "stop_time": "", + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "build": "12.34", + "owner": "owner", + "repo": "repo", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + } + expected_error = """Build has already finished, uploads rejected.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("circleci", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch.object(requests, "get") + def test_circleci_invalid_stop_time_gh(self, mock_get): + expected_response = { + "vcs_revision": "c739768fcac68144a3a6d82305b9c4106934d31a", + "vcs_type": "github", + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.json.return_value = expected_response + + params = { + "build": "12.34", + "owner": "owner", + "repo": "repo", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + } + + assert TokenlessUploadHandler("circleci", params).verify_upload() == "github" + + +class UploadHandlerGithubActionsTokenlessTest(TestCase): + @patch( + "upload.tokenless.github_actions.TokenlessGithubActionsHandler.get_build", + new_callable=PropertyMock, + ) + def test_underscore_replace(self, mock_get): + expected_response = { + "commit_sha": "c739768fcac68144a3a6d82305b9c4106934d31a", + "slug": "owner/repo", + "public": True, + "finish_time": f"{datetime.now()}".split(".")[0], + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.return_value = expected_response + + params = { + "build": "12.34", + "owner": "owner", + "repo": "repo", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + } + + assert ( + TokenlessUploadHandler("github-actions", params).verify_upload() == "github" + ) + + def test_github_actions_no_owner(self): + params = {} + + expected_error = """Missing "owner" argument. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("github_actions", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + def test_github_actions_no_repo(self): + params = {"owner": "owner"} + + expected_error = """Missing "repo" argument. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("github_actions", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch("upload.tokenless.github_actions.get", new_callable=PropertyMock) + def test_github_actions_client_error(self, mock_get_torngit): + mock_get = mock_get_torngit.return_value.get_workflow_run + mock_get.side_effect = [TorngitClientGeneralError(500, None, None)] + + params = {"build": "12.34", "owner": "owner", "repo": "repo"} + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("github_actions", params).verify_upload() + assert ( + e.value.args[0] + == "Unable to locate build via Github Actions API. Please upload with the Codecov repository upload token to resolve issue." + ) + mock_get_torngit.assert_called_with( + "github", + token={"key": ANY}, + repo={"name": "repo"}, + owner={"username": "owner"}, + oauth_consumer_token={"key": ANY, "secret": ANY}, + ) + mock_get.assert_called_with("12.34") + mock_get.reset_mock() + mock_get.side_effect = [Exception("Not Found")] + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("github_actions", params).verify_upload() + assert ( + e.value.args[0] + == "Unable to locate build via Github Actions API. Please upload with the Codecov repository upload token to resolve issue." + ) + mock_get.assert_called_with("12.34") + + @freeze_time("2024-04-25T00:00:00") + @patch("upload.tokenless.github_actions.get", new_callable=PropertyMock) + def test_github_actions_rate_limit_error(self, mock_get_torngit): + mock_get = mock_get_torngit.return_value.get_workflow_run + in_10_s = datetime.now() + timedelta(seconds=10) + mock_get.side_effect = [ + TorngitRateLimitError("error", "err msg", in_10_s.timestamp(), 20) + ] + + params = {"build": "12.34", "owner": "owner", "repo": "repo"} + + with self.assertRaises(rest_framework.exceptions.Throttled) as e: + TokenlessUploadHandler("github_actions", params).verify_upload() + self.assertEqual( + str(e.exception.detail), + "Rate limit reached. Please upload with the Codecov repository upload token to resolve issue. Expected time to availability: 10s.", + ) + + mock_get.reset_mock() + mock_get.side_effect = [TorngitRateLimitError("error", "err msg", None, 20)] + + with self.assertRaises(rest_framework.exceptions.Throttled) as e: + TokenlessUploadHandler("github_actions", params).verify_upload() + self.assertEqual( + str(e.exception.detail), + "Rate limit reached. Please upload with the Codecov repository upload token to resolve issue. Expected time to availability: 20s.", + ) + + mock_get.reset_mock() + mock_get.side_effect = [TorngitRateLimitError("error", "err msg", None, None)] + + with self.assertRaises(rest_framework.exceptions.Throttled) as e: + TokenlessUploadHandler("github_actions", params).verify_upload() + self.assertEqual( + str(e.exception.detail), + "Rate limit reached. Please upload with the Codecov repository upload token to resolve issue.", + ) + + @patch( + "upload.tokenless.github_actions.TokenlessGithubActionsHandler.get_build", + new_callable=PropertyMock, + ) + def test_github_actions_non_public(self, mock_get): + expected_response = {"public": False, "slug": "slug", "commit_sha": "abc"} + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.return_value = expected_response + + params = { + "build": "12.34", + "owner": "owner", + "repo": "repo", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + } + + expected_error = """Repository slug or commit sha do not match Github actions build. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("github_actions", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch( + "upload.tokenless.github_actions.TokenlessGithubActionsHandler.get_build", + new_callable=PropertyMock, + ) + def test_github_actions_wrong_slug(self, mock_get): + expected_response = {"slug": "slug", "public": True, "commit_sha": "abc"} + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.return_value = expected_response + + params = { + "build": "12.34", + "owner": "owner", + "repo": "repo", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + } + + expected_error = """Repository slug or commit sha do not match Github actions build. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("github_actions", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch( + "upload.tokenless.github_actions.TokenlessGithubActionsHandler.get_build", + new_callable=PropertyMock, + ) + def test_github_actions_wrong_commit(self, mock_get): + expected_response = {"commit_sha": "abc", "slug": "owner/repo", "public": True} + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.return_value = expected_response + + params = { + "build": "12.34", + "owner": "owner", + "repo": "repo", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + } + + expected_error = """Repository slug or commit sha do not match Github actions build. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("github_actions", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch( + "upload.tokenless.github_actions.TokenlessGithubActionsHandler.get_build", + new_callable=PropertyMock, + ) + def test_github_actions_no_build_status(self, mock_get): + expected_response = { + "commit_sha": "c739768fcac68144a3a6d82305b9c4106934d31a", + "slug": "owner/repo", + "public": True, + "finish_time": f"{datetime.now() - timedelta(minutes=10)}".split(".")[0], + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.return_value = expected_response + + params = { + "build": "12.34", + "owner": "owner", + "repo": "repo", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + } + + expected_error = """Actions workflow run is stale""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("github_actions", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch( + "upload.tokenless.github_actions.TokenlessGithubActionsHandler.get_build", + new_callable=PropertyMock, + ) + def test_github_actions(self, mock_get): + expected_response = { + "commit_sha": "c739768fcac68144a3a6d82305b9c4106934d31a", + "slug": "owner/repo", + "public": True, + "finish_time": f"{datetime.now()}".split(".")[0], + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.return_value = expected_response + + params = { + "build": "12.34", + "owner": "owner", + "repo": "repo", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + } + + assert ( + TokenlessUploadHandler("github_actions", params).verify_upload() == "github" + ) + + @patch( + "upload.tokenless.github_actions.TokenlessGithubActionsHandler.get_build", + new_callable=PropertyMock, + ) + def test_github_actions_in_progress(self, mock_get): + expected_response = { + "commit_sha": "c739768fcac68144a3a6d82305b9c4106934d31a", + "slug": "owner/repo", + "public": True, + "status": "in_progress", + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.return_value = expected_response + + params = { + "build": "12.34", + "owner": "owner", + "repo": "repo", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + } + assert ( + TokenlessUploadHandler("github_actions", params).verify_upload() == "github" + ) + + @patch( + "upload.tokenless.github_actions.TokenlessGithubActionsHandler.get_build", + new_callable=PropertyMock, + ) + def test_github_actions_queued(self, mock_get): + expected_response = { + "commit_sha": "c739768fcac68144a3a6d82305b9c4106934d31a", + "slug": "owner/repo", + "public": True, + "status": "queued", + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.return_value = expected_response + + params = { + "build": "12.34", + "owner": "owner", + "repo": "repo", + "commit": "c739768fcac68144a3a6d82305b9c4106934d31a", + } + assert ( + TokenlessUploadHandler("github_actions", params).verify_upload() == "github" + ) + + @patch( + "upload.tokenless.cirrus.TokenlessCirrusHandler.get_build", + new_callable=PropertyMock, + ) + def test_cirrus_ci(self, mock_get): + expected_response = { + "data": { + "build": { + "changeIdInRepo": "bbeefc070d847ff1ed526d412b7f97c5e743b1c1", + "repository": {"name": "mtail", "owner": "google"}, + "status": "COMPLETED", + "buildCreatedTimestamp": time.time() - 90, + "durationInSeconds": 90, + } + } + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.return_value = expected_response + + params = { + "build": "5699563004624896", + "owner": "google", + "repo": "mtail", + "commit": "bbeefc070d847ff1ed526d412b7f97c5e743b1c1", + } + + assert TokenlessUploadHandler("cirrus_ci", params).verify_upload() == "github" + + @patch( + "upload.tokenless.cirrus.TokenlessCirrusHandler.get_build", + new_callable=PropertyMock, + ) + def test_cirrus_ci_executing(self, mock_get): + expected_response = { + "data": { + "build": { + "changeIdInRepo": "bbeefc070d847ff1ed526d412b7f97c5e743b1c1", + "repository": {"name": "mtail", "owner": "google"}, + "status": "EXECUTING", + } + } + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.return_value = expected_response + + params = { + "build": "5699563004624896", + "owner": "google", + "repo": "mtail", + "commit": "bbeefc070d847ff1ed526d412b7f97c5e743b1c1", + } + + assert TokenlessUploadHandler("cirrus_ci", params).verify_upload() == "github" + + @patch( + "upload.tokenless.cirrus.TokenlessCirrusHandler.get_build", + new_callable=PropertyMock, + ) + def test_cirrus_ci_no_owner(self, mock_get): + expected_response = { + "data": { + "build": { + "changeIdInRepo": "bbeefc070d847ff1ed526d412b7f97c5e743b1c1", + "repository": {"owner": "google", "name": "mtail"}, + "status": "EXECUTING", + } + } + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.return_value = expected_response + + params = { + "build": "5699563004624896", + "repo": "mtail", + "commit": "bbeefc070d847ff1ed526d412b7f97c5e743b1c1", + } + + expected_error = """Missing "owner" argument. Please upload with the Codecov repository upload token to resolve this issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("cirrus_ci", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch( + "upload.tokenless.cirrus.TokenlessCirrusHandler.get_build", + new_callable=PropertyMock, + ) + def test_cirrus_ci_no_repo(self, mock_get): + expected_response = { + "data": { + "build": { + "changeIdInRepo": "bbeefc070d847ff1ed526d412b7f97c5e743b1c1", + "repository": {"owner": "google", "name": "mtail"}, + "status": "EXECUTING", + } + } + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.return_value = expected_response + + params = { + "build": "5699563004624896", + "owner": "google", + "commit": "bbeefc070d847ff1ed526d412b7f97c5e743b1c1", + } + + expected_error = """Missing "repo" argument. Please upload with the Codecov repository upload token to resolve this issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("cirrus_ci", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch( + "upload.tokenless.cirrus.TokenlessCirrusHandler.get_build", + new_callable=PropertyMock, + ) + def test_cirrus_ci_no_commit(self, mock_get): + expected_response = { + "data": { + "build": { + "changeIdInRepo": "bbeefc070d847ff1ed526d412b7f97c5e743b1c1", + "repository": {"owner": "google", "name": "mtail"}, + "status": "EXECUTING", + } + } + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.return_value = expected_response + + params = {"build": "5699563004624896", "owner": "google", "repo": "mtail"} + + expected_error = """Missing "commit" argument. Please upload with the Codecov repository upload token to resolve this issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("cirrus_ci", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch( + "upload.tokenless.cirrus.TokenlessCirrusHandler.get_build", + new_callable=PropertyMock, + ) + def test_cirrus_ci_wrong_repository(self, mock_get): + expected_response = { + "data": { + "build": { + "changeIdInRepo": "bbeefc070d847ff1ed526d412b7f97c5e743b1c1", + "repository": {"owner": "test", "name": "test"}, + "status": "EXECUTING", + } + } + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.return_value = expected_response + + params = { + "build": "5699563004624896", + "owner": "google", + "repo": "mtail", + "commit": "bbeefc070d847ff1ed526d412b7f97c5e743b1c1", + } + + expected_error = """Repository slug does not match Cirrus CI build. Please upload with the Codecov repository upload token to resolve this issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("cirrus_ci", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch( + "upload.tokenless.cirrus.TokenlessCirrusHandler.get_build", + new_callable=PropertyMock, + ) + def test_cirrus_ci_wrong_commit(self, mock_get): + expected_response = { + "data": { + "build": { + "changeIdInRepo": "testtesttesttest", + "repository": {"owner": "google", "name": "mtail"}, + "status": "EXECUTING", + } + } + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.return_value = expected_response + + params = { + "build": "5699563004624896", + "owner": "google", + "repo": "mtail", + "commit": "bbeefc070d847ff1ed526d412b7f97c5e743b1c1", + } + + expected_error = """Commit sha does not match Cirrus CI build. Please upload with the Codecov repository upload token to resolve issue.""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("cirrus_ci", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] + + @patch( + "upload.tokenless.cirrus.TokenlessCirrusHandler.get_build", + new_callable=PropertyMock, + ) + def test_cirrus_ci_stale(self, mock_get): + expected_response = { + "data": { + "build": { + "changeIdInRepo": "bbeefc070d847ff1ed526d412b7f97c5e743b1c1", + "repository": {"name": "mtail", "owner": "google"}, + "status": "COMPLETED", + "buildCreatedTimestamp": time.time() - 100000, + "durationInSeconds": 1, + } + } + } + mock_get.return_value.status_code.return_value = 200 + mock_get.return_value.return_value = expected_response + + params = { + "build": "5699563004624896", + "owner": "google", + "repo": "mtail", + "commit": "bbeefc070d847ff1ed526d412b7f97c5e743b1c1", + } + + expected_error = """Cirrus run is stale""" + + with pytest.raises(NotFound) as e: + TokenlessUploadHandler("cirrus_ci", params).verify_upload() + assert [line.strip() for line in e.value.args[0].split("\n")] == [ + line.strip() for line in expected_error.split("\n") + ] diff --git a/apps/codecov-api/upload/tests/test_upload_download.py b/apps/codecov-api/upload/tests/test_upload_download.py new file mode 100644 index 0000000000..1bb66674e9 --- /dev/null +++ b/apps/codecov-api/upload/tests/test_upload_download.py @@ -0,0 +1,149 @@ +from unittest.mock import patch + +import minio +from rest_framework.test import APITestCase +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory +from shared.django_apps.core.tests.factories import RepositoryFactory + +from utils.test_utils import Client + + +class UploadDownloadHelperTest(APITestCase): + def _get(self, kwargs={}, data={}): + path = f"/upload/{kwargs.get('service')}/{kwargs.get('owner_username')}/{kwargs.get('repo_name')}/download" + return self.client.get(path, data=data) + + def setUp(self): + self.org = OwnerFactory(username="codecovtest", service="github") + self.repo = RepositoryFactory( + author=self.org, + name="upload-test-repo", + upload_token="a03e5d02-9495-4413-b0d8-05651bb2e842", + ) + self.repo = RepositoryFactory( + author=self.org, name="private-upload-test-repo", private=True + ) + self.client = Client() + self.client.force_login_owner(self.org) + + def test_no_path_param(self): + response = self._get( + kwargs={ + "service": "gh", + "owner_username": "codecovtest", + "repo_name": "invalid", + }, + ) + assert response.status_code == 404 + + def test_invalid_path_param(self): + response = self._get( + kwargs={ + "service": "gh", + "owner_username": "codecovtest", + "repo_name": "invalid", + }, + data={"path": "v2"}, + ) + assert response.status_code == 404 + + def test_invalid_owner(self): + response = self._get( + kwargs={ + "service": "gh", + "owner_username": "invalid", + "repo_name": "invalid", + }, + data={"path": "v4/raw"}, + ) + assert response.status_code == 404 + + def test_invalid_repo(self): + response = self._get( + kwargs={ + "service": "gh", + "owner_username": "codecovtest", + "repo_name": "invalid", + }, + data={"path": "v4/raw"}, + ) + assert response.status_code == 404 + + @patch("shared.api_archive.archive.ArchiveService.get_archive_hash") + @patch("shared.storage.MinioStorageService.create_presigned_get") + def test_invalid_archive_path(self, create_presigned_get, get_archive_hash): + create_presigned_get.side_effect = [ + minio.error.S3Error( + code="NoSuchKey", + message=None, + resource=None, + request_id=None, + host_id=None, + response=None, + ) + ] + get_archive_hash.return_value = "path" + response = self._get( + kwargs={ + "service": "gh", + "owner_username": "codecovtest", + "repo_name": "upload-test-repo", + }, + data={"path": "v4/raw/path"}, + ) + assert response.status_code == 404 + + @patch("shared.api_archive.archive.ArchiveService.get_archive_hash") + @patch("shared.storage.MinioStorageService.create_presigned_get") + def test_valid_repo_archive_path(self, create_presigned_get, get_archive_hash): + create_presigned_get.return_value = "presigned-url" + get_archive_hash.return_value = "hasssshhh" + response = self._get( + kwargs={ + "service": "gh", + "owner_username": "codecovtest", + "repo_name": "upload-test-repo", + }, + data={"path": "v4/raw/hasssshhh"}, + ) + assert response.status_code == 302 + headers = response.headers + assert headers["location"] == "presigned-url" + create_presigned_get.assert_called_once_with( + "archive", "v4/raw/hasssshhh", expires=30 + ) + + @patch("shared.api_archive.archive.ArchiveService.read_file") + def test_invalid_repo_archive_path(self, mock_read_file): + mock_read_file.return_value = "Report!" + response = self._get( + kwargs={ + "service": "gh", + "owner_username": "codecovtest", + "repo_name": "upload-test-repo", + }, + data={"path": "v4/raw"}, + ) + assert response.status_code == 404 + + def test_private_valid_archive_path(self): + response = self._get( + kwargs={ + "service": "gh", + "owner_username": "codecovtest", + "repo_name": "private-upload-test-repo", + }, + data={"path": "v4/raw"}, + ) + assert response.status_code == 404 + + def test_invalid_shelter_path(self): + response = self._get( + kwargs={ + "service": "gh", + "owner_username": "codecovtest", + "repo_name": "upload-test-repo", + }, + data={"path": "shelter/github/codecovtest::::some-other-repo"}, + ) + assert response.status_code == 404 diff --git a/apps/codecov-api/upload/tests/views/__init__.py b/apps/codecov-api/upload/tests/views/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/upload/tests/views/test_base.py b/apps/codecov-api/upload/tests/views/test_base.py new file mode 100644 index 0000000000..8e8e71519e --- /dev/null +++ b/apps/codecov-api/upload/tests/views/test_base.py @@ -0,0 +1,83 @@ +import pytest +from rest_framework.exceptions import ValidationError +from shared.django_apps.core.tests.factories import CommitFactory, RepositoryFactory + +from reports.models import CommitReport +from upload.views.base import GetterMixin + + +def test_get_repo(db): + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + repository.save() + generic_class = GetterMixin() + generic_class.kwargs = dict(repo="codecov::::the_repo", service="github") + recovered_repo = generic_class.get_repo() + assert recovered_repo == repository + + +def test_get_repo_with_invalid_service(db): + generic_class = GetterMixin() + generic_class.kwargs = dict(repo="repo", service="wrong service") + with pytest.raises(ValidationError) as exp: + generic_class.get_repo() + assert exp.match("Service not found: wrong service") + + +def test_get_repo_not_found(db): + generic_class = GetterMixin() + generic_class.kwargs = dict(repo="repo", service="github") + with pytest.raises(ValidationError) as exp: + generic_class.get_repo() + assert exp.match("Repository not found") + + +def test_get_commit(db): + repository = RepositoryFactory(name="the_repo", author__username="codecov") + commit = CommitFactory(repository=repository) + repository.save() + commit.save() + generic_class = GetterMixin() + generic_class.kwargs = dict(repo=repository.name, commit_sha=commit.commitid) + recovered_commit = generic_class.get_commit(repository) + assert recovered_commit == commit + + +def test_get_commit_error(db): + repository = RepositoryFactory(name="the_repo", author__username="codecov") + repository.save() + generic_class = GetterMixin() + generic_class.kwargs = dict(repo=repository.name, commit_sha="missing_commit") + with pytest.raises(ValidationError) as exp: + generic_class.get_commit(repository) + assert exp.match("Commit SHA not found") + + +def test_get_report(db): + repository = RepositoryFactory(name="the_repo", author__username="codecov") + commit = CommitFactory(repository=repository) + report = CommitReport(commit=commit) + repository.save() + commit.save() + report.save() + generic_class = GetterMixin() + generic_class.kwargs = dict( + repo=repository.name, commit_sha=commit.commitid, report_code=report.code + ) + recovered_report = generic_class.get_report(commit) + assert recovered_report == report + + +def test_get_report_error(db): + repository = RepositoryFactory(name="the_repo", author__username="codecov") + commit = CommitFactory(repository=repository) + repository.save() + commit.save() + generic_class = GetterMixin() + generic_class.kwargs = dict( + repo=repository.name, commit_sha=commit.commitid, report_code="random_code" + ) + with pytest.raises(ValidationError) as exp: + generic_class.get_report(commit) + assert exp.match("Report not found") diff --git a/apps/codecov-api/upload/tests/views/test_bundle_analysis.py b/apps/codecov-api/upload/tests/views/test_bundle_analysis.py new file mode 100644 index 0000000000..20510fb16c --- /dev/null +++ b/apps/codecov-api/upload/tests/views/test_bundle_analysis.py @@ -0,0 +1,853 @@ +import json +import re +from unittest.mock import ANY, patch + +import pytest +from django.test import override_settings +from django.urls import reverse +from rest_framework.test import APIClient +from shared.django_apps.codecov_auth.tests.factories import ( + OrganizationLevelTokenFactory, +) +from shared.django_apps.core.tests.factories import CommitFactory, RepositoryFactory +from shared.events.amplitude import UNKNOWN_USER_OWNERID +from shared.helpers.redis import get_redis_connection + +from core.models import Commit +from services.task import TaskService +from timeseries.models import Dataset, MeasurementName + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_upload_bundle_analysis_success(db, client, mocker, mock_redis): + upload = mocker.patch.object(TaskService, "upload") + mock_metrics = mocker.patch( + "upload.views.bundle_analysis.BUNDLE_ANALYSIS_UPLOAD_VIEWS_COUNTER.labels" + ) + create_presigned_put = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + mock_amplitude = mocker.patch( + "shared.events.amplitude.AmplitudeEventPublisher.publish" + ) + + repository = RepositoryFactory.create() + commit_sha = "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef" + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {repository.upload_token}") + + res = client.post( + reverse("upload-bundle-analysis"), + { + "commit": commit_sha, + "slug": f"{repository.author.username}::::{repository.name}", + "build": "test-build", + "buildURL": "test-build-url", + "job": "test-job", + "service": "test-service", + "compareSha": "6fd5b89357fc8cdf34d6197549ac7c6d7e5aaaaa", + }, + format="json", + headers={"User-Agent": "codecov-cli/0.4.7"}, + ) + assert res.status_code == 201 + + # returns presigned storage URL + assert res.json() == {"url": "test-presigned-put"} + + create_presigned_put.assert_called_once_with("bundle-analysis", ANY, 30) + call = create_presigned_put.mock_calls[0] + _, storage_path, _ = call.args + match = re.match(r"v1/uploads/([\d\w\-]+)\.json", storage_path) + assert match + (reportid,) = match.groups() + + # creates commit + commit = Commit.objects.get(commitid=commit_sha) + assert commit + + # saves args in Redis + redis = get_redis_connection() + args = redis.rpop(f"uploads/{repository.repoid}/{commit_sha}/bundle_analysis") + assert json.loads(args) == { + "reportid": reportid, + "build": "test-build", + "build_url": "test-build-url", + "job": "test-job", + "service": "test-service", + "url": f"v1/uploads/{reportid}.json", + "commit": commit_sha, + "report_code": None, + "bundle_analysis_compare_sha": "6fd5b89357fc8cdf34d6197549ac7c6d7e5aaaaa", + } + + # sets latest upload timestamp + ts = redis.get(f"latest_upload/{repository.repoid}/{commit_sha}/bundle_analysis") + assert ts + + # triggers upload task + upload.assert_called_with( + commitid=commit_sha, + repoid=repository.repoid, + report_code=None, + report_type="bundle_analysis", + arguments=ANY, + countdown=4, + ) + mock_metrics.assert_called_with( + **{ + "agent": "cli", + "version": "0.4.7", + "action": "bundle_analysis", + "endpoint": "bundle_analysis", + "is_using_shelter": "no", + "position": "end", + "result": "success", + }, + ) + + # emits Amplitude event + mock_amplitude.assert_called_with( + "Upload Received", + { + "user_ownerid": UNKNOWN_USER_OWNERID, + "ownerid": commit.repository.author.ownerid, + "repoid": commit.repository.repoid, + "commitid": commit.id, + "pullid": commit.pullid, + "upload_type": "Bundle", + }, + ) + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +@override_settings(SHELTER_SHARED_SECRET="shelter-shared-secret") +def test_upload_bundle_analysis_success_shelter(db, client, mocker, mock_redis): + upload = mocker.patch.object(TaskService, "upload") + mock_metrics = mocker.patch( + "upload.views.bundle_analysis.BUNDLE_ANALYSIS_UPLOAD_VIEWS_COUNTER.labels" + ) + create_presigned_put = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + + repository = RepositoryFactory.create() + commit_sha = "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef" + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {repository.upload_token}") + + res = client.post( + reverse("upload-bundle-analysis"), + { + "commit": commit_sha, + "slug": f"{repository.author.username}::::{repository.name}", + "build": "test-build", + "buildURL": "test-build-url", + "job": "test-job", + "service": "test-service", + "compareSha": "6fd5b89357fc8cdf34d6197549ac7c6d7e5aaaaa", + "storage_path": "shelter/test/path.txt", + "upload_external_id": "test-47078f85-2cee-4511-b38d-183c334ef43b", + }, + format="json", + headers={"User-Agent": "codecov-cli/0.4.7"}, + ) + assert res.status_code == 201 + + # returns presigned storage URL + assert res.json() == {"url": "test-presigned-put"} + + create_presigned_put.assert_called_once_with("bundle-analysis", ANY, 30) + call = create_presigned_put.mock_calls[0] + _, storage_path, _ = call.args + match = re.match(r"v1/uploads/([\d\w\-]+)\.json", storage_path) + assert match + (reportid,) = match.groups() + + # creates commit + commit = Commit.objects.get(commitid=commit_sha) + assert commit + + # saves args in Redis + redis = get_redis_connection() + args = redis.rpop(f"uploads/{repository.repoid}/{commit_sha}/bundle_analysis") + assert json.loads(args) == { + "reportid": reportid, + "build": "test-build", + "build_url": "test-build-url", + "job": "test-job", + "service": "test-service", + "url": f"v1/uploads/{reportid}.json", + "commit": commit_sha, + "report_code": None, + "bundle_analysis_compare_sha": "6fd5b89357fc8cdf34d6197549ac7c6d7e5aaaaa", + } + + # sets latest upload timestamp + ts = redis.get(f"latest_upload/{repository.repoid}/{commit_sha}/bundle_analysis") + assert ts + + # triggers upload task + upload.assert_called_with( + commitid=commit_sha, + repoid=repository.repoid, + report_code=None, + report_type="bundle_analysis", + arguments=ANY, + countdown=4, + ) + mock_metrics.assert_called_with( + **{ + "agent": "cli", + "version": "0.4.7", + "action": "bundle_analysis", + "endpoint": "bundle_analysis", + "is_using_shelter": "no", + "position": "end", + "result": "success", + }, + ) + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_upload_bundle_analysis_org_token(db, client, mocker, mock_redis): + mocker.patch.object(TaskService, "upload") + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + mock_metrics = mocker.patch( + "upload.views.bundle_analysis.BUNDLE_ANALYSIS_UPLOAD_VIEWS_COUNTER.labels" + ) + + repository = RepositoryFactory.create() + org_token = OrganizationLevelTokenFactory.create(owner=repository.author) + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {org_token.token}") + + res = client.post( + reverse("upload-bundle-analysis"), + { + "commit": "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef", + "slug": f"{repository.author.username}::::{repository.name}", + }, + format="json", + ) + assert res.status_code == 201 + mock_metrics.assert_called_with( + **{ + "agent": "unknown-user-agent", + "version": "unknown-user-agent", + "action": "bundle_analysis", + "endpoint": "bundle_analysis", + "is_using_shelter": "no", + "position": "end", + "result": "success", + }, + ) + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_upload_bundle_analysis_existing_commit(db, client, mocker, mock_redis): + upload = mocker.patch.object(TaskService, "upload") + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + mock_metrics = mocker.patch( + "upload.views.bundle_analysis.BUNDLE_ANALYSIS_UPLOAD_VIEWS_COUNTER.labels" + ) + + repository = RepositoryFactory.create() + commit = CommitFactory.create(repository=repository) + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {repository.upload_token}") + + res = client.post( + reverse("upload-bundle-analysis"), + { + "commit": commit.commitid, + "slug": f"{repository.author.username}::::{repository.name}", + }, + format="json", + ) + assert res.status_code == 201 + + upload.assert_called_with( + commitid=commit.commitid, + repoid=repository.repoid, + report_code=None, + report_type="bundle_analysis", + arguments=ANY, + countdown=4, + ) + mock_metrics.assert_called_with( + **{ + "agent": "unknown-user-agent", + "version": "unknown-user-agent", + "action": "bundle_analysis", + "endpoint": "bundle_analysis", + "is_using_shelter": "no", + "position": "end", + "result": "success", + }, + ) + + +def test_upload_bundle_analysis_missing_args(db, client, mocker, mock_redis): + upload = mocker.patch.object(TaskService, "upload") + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + mock_metrics = mocker.patch( + "upload.views.bundle_analysis.BUNDLE_ANALYSIS_UPLOAD_VIEWS_COUNTER.labels" + ) + + repository = RepositoryFactory.create() + commit = CommitFactory.create(repository=repository) + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {repository.upload_token}") + + res = client.post( + reverse("upload-bundle-analysis"), + { + "commit": commit.commitid, + }, + format="json", + ) + assert res.status_code == 400 + assert res.json() == {"slug": ["This field is required."]} + assert not upload.called + + res = client.post( + reverse("upload-bundle-analysis"), + { + "slug": f"{repository.author.username}::::{repository.name}", + }, + format="json", + ) + assert res.status_code == 400 + assert res.json() == {"commit": ["This field is required."]} + assert not upload.called + mock_metrics.assert_called_with( + **{ + "agent": "unknown-user-agent", + "version": "unknown-user-agent", + "action": "bundle_analysis", + "endpoint": "bundle_analysis", + "is_using_shelter": "no", + "position": "end", + "result": "bad_request", + }, + ) + + +def test_upload_bundle_analysis_invalid_token(db, client, mocker, mock_redis): + upload = mocker.patch.object(TaskService, "upload") + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + + repository = RepositoryFactory.create() + commit = CommitFactory.create(repository=repository) + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="token 2a869881-9c0f-4754-b790-3f5920be3605") + + res = client.post( + reverse("upload-bundle-analysis"), + { + "commit": commit.commitid, + }, + format="json", + ) + assert res.status_code == 401 + assert res.json() == {"detail": "Not valid tokenless upload"} + assert not upload.called + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +@patch("upload.helpers.jwt.decode") +@patch("upload.helpers.PyJWKClient") +def test_upload_bundle_analysis_github_oidc_auth( + mock_jwks_client, mock_jwt_decode, db, mocker +): + mocker.patch.object(TaskService, "upload") + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + mock_metrics = mocker.patch( + "upload.views.bundle_analysis.BUNDLE_ANALYSIS_UPLOAD_VIEWS_COUNTER.labels" + ) + repository = RepositoryFactory() + mock_jwt_decode.return_value = { + "repository": f"url/{repository.name}", + "repository_owner": repository.author.username, + "iss": "https://token.actions.githubusercontent.com", + } + token = "ThisValueDoesNotMatterBecauseOf_mock_jwt_decode" + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {token}") + + res = client.post( + reverse("upload-bundle-analysis"), + { + "commit": "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef", + "slug": f"{repository.author.username}::::{repository.name}", + }, + format="json", + ) + assert res.status_code == 201 + mock_metrics.assert_called_with( + **{ + "agent": "unknown-user-agent", + "version": "unknown-user-agent", + "action": "bundle_analysis", + "endpoint": "bundle_analysis", + "is_using_shelter": "no", + "position": "end", + "result": "success", + }, + ) + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_upload_bundle_analysis_measurement_datasets_created( + db, client, mocker, mock_redis +): + mocker.patch.object(TaskService, "upload") + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + mock_metrics = mocker.patch( + "upload.views.bundle_analysis.BUNDLE_ANALYSIS_UPLOAD_VIEWS_COUNTER.labels" + ) + + repository = RepositoryFactory.create() + commit_sha = "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef" + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {repository.upload_token}") + + res = client.post( + reverse("upload-bundle-analysis"), + { + "commit": commit_sha, + "slug": f"{repository.author.username}::::{repository.name}", + "build": "test-build", + "buildURL": "test-build-url", + "job": "test-job", + "service": "test-service", + }, + format="json", + headers={"User-Agent": "codecov-cli/0.4.7"}, + ) + assert res.status_code == 201 + + supported_bundle_analysis_measurement_types = [ + MeasurementName.BUNDLE_ANALYSIS_ASSET_SIZE, + MeasurementName.BUNDLE_ANALYSIS_FONT_SIZE, + MeasurementName.BUNDLE_ANALYSIS_IMAGE_SIZE, + MeasurementName.BUNDLE_ANALYSIS_JAVASCRIPT_SIZE, + MeasurementName.BUNDLE_ANALYSIS_REPORT_SIZE, + MeasurementName.BUNDLE_ANALYSIS_STYLESHEET_SIZE, + ] + for measurement_type in supported_bundle_analysis_measurement_types: + assert Dataset.objects.filter( + name=measurement_type.value, + repository_id=repository.pk, + ).exists() + + mock_metrics.assert_called_with( + **{ + "agent": "cli", + "version": "0.4.7", + "action": "bundle_analysis", + "endpoint": "bundle_analysis", + "is_using_shelter": "no", + "position": "end", + "result": "success", + }, + ) + + +@override_settings(TIMESERIES_ENABLED=False) +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_upload_bundle_analysis_measurement_timeseries_disabled( + db, client, mocker, mock_redis +): + mocker.patch.object(TaskService, "upload") + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + mock_metrics = mocker.patch( + "upload.views.bundle_analysis.BUNDLE_ANALYSIS_UPLOAD_VIEWS_COUNTER.labels" + ) + + repository = RepositoryFactory.create() + commit_sha = "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef" + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {repository.upload_token}") + + res = client.post( + reverse("upload-bundle-analysis"), + { + "commit": commit_sha, + "slug": f"{repository.author.username}::::{repository.name}", + "build": "test-build", + "buildURL": "test-build-url", + "job": "test-job", + "service": "test-service", + }, + format="json", + headers={"User-Agent": "codecov-cli/0.4.7"}, + ) + assert res.status_code == 201 + + supported_bundle_analysis_measurement_types = [ + MeasurementName.BUNDLE_ANALYSIS_ASSET_SIZE, + MeasurementName.BUNDLE_ANALYSIS_FONT_SIZE, + MeasurementName.BUNDLE_ANALYSIS_IMAGE_SIZE, + MeasurementName.BUNDLE_ANALYSIS_JAVASCRIPT_SIZE, + MeasurementName.BUNDLE_ANALYSIS_REPORT_SIZE, + MeasurementName.BUNDLE_ANALYSIS_STYLESHEET_SIZE, + ] + for measurement_type in supported_bundle_analysis_measurement_types: + assert not Dataset.objects.filter( + name=measurement_type.value, + repository_id=repository.pk, + ).exists() + + mock_metrics.assert_called_with( + **{ + "agent": "cli", + "version": "0.4.7", + "action": "bundle_analysis", + "endpoint": "bundle_analysis", + "is_using_shelter": "no", + "position": "end", + "result": "success", + }, + ) + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_upload_bundle_analysis_no_repo(db, client, mocker, mock_redis): + upload = mocker.patch.object(TaskService, "upload") + mocker.patch.object(TaskService, "upload") + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + mock_metrics = mocker.patch( + "upload.views.bundle_analysis.BUNDLE_ANALYSIS_UPLOAD_VIEWS_COUNTER.labels" + ) + + repository = RepositoryFactory.create() + org_token = OrganizationLevelTokenFactory.create(owner=repository.author) + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {org_token.token}") + + res = client.post( + reverse("upload-bundle-analysis"), + { + "commit": "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef", + "slug": "FakeUser::::NonExistentName", + }, + format="json", + ) + assert res.status_code == 404 + assert res.json() == {"detail": "Repository not found."} + assert not upload.called + + mock_metrics.assert_called_with( + **{ + "agent": "unknown-user-agent", + "version": "unknown-user-agent", + "action": "bundle_analysis", + "endpoint": "bundle_analysis", + "is_using_shelter": "no", + "position": "end", + "result": "error", + }, + ) + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_upload_bundle_analysis_tokenless_success(db, client, mocker, mock_redis): + upload = mocker.patch.object(TaskService, "upload") + mock_metrics = mocker.patch( + "upload.views.bundle_analysis.BUNDLE_ANALYSIS_UPLOAD_VIEWS_COUNTER.labels" + ) + + create_presigned_put = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + + repository = RepositoryFactory.create(private=False) + commit_sha = "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef" + + client = APIClient() + + res = client.post( + reverse("upload-bundle-analysis"), + { + "commit": commit_sha, + "slug": f"{repository.author.username}::::{repository.name}", + "build": "test-build", + "buildURL": "test-build-url", + "job": "test-job", + "service": "test-service", + "compareSha": "6fd5b89357fc8cdf34d6197549ac7c6d7e5aaaaa", + "branch": "f1:main", + "git_service": "github", + }, + format="json", + headers={"User-Agent": "codecov-cli/0.4.7"}, + ) + + assert res.status_code == 201 + + # returns presigned storage URL + assert res.json() == {"url": "test-presigned-put"} + + assert upload.called + create_presigned_put.assert_called_once_with("bundle-analysis", ANY, 30) + + mock_metrics.assert_called_with( + **{ + "agent": "cli", + "version": "0.4.7", + "action": "bundle_analysis", + "endpoint": "bundle_analysis", + "is_using_shelter": "no", + "position": "end", + "result": "success", + }, + ) + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_upload_bundle_analysis_true_tokenless_success(db, client, mocker, mock_redis): + upload = mocker.patch.object(TaskService, "upload") + + create_presigned_put = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + + repository = RepositoryFactory.create( + private=False, + author__upload_token_required_for_public_repos=False, + author__service="github", + ) + client = APIClient() + + res = client.post( + reverse("upload-bundle-analysis"), + { + "commit": "any", + "slug": f"{repository.author.username}::::{repository.name}", + "build": "test-build", + "buildURL": "test-build-url", + "job": "test-job", + "service": "test-service", + "compareSha": "6fd5b89357fc8cdf34d6197549ac7c6d7e5aaaaa", + "branch": "f1:main", + "git_service": "github", + }, + format="json", + headers={"User-Agent": "codecov-cli/0.4.7"}, + ) + + assert res.status_code == 201 + + # returns presigned storage URL + assert res.json() == {"url": "test-presigned-put"} + + assert upload.called + create_presigned_put.assert_called_once_with("bundle-analysis", ANY, 30) + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_upload_bundle_analysis_tokenless_no_repo(db, client, mocker, mock_redis): + upload = mocker.patch.object(TaskService, "upload") + + repository = RepositoryFactory.create(private=False) + commit_sha = "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef" + + client = APIClient() + + res = client.post( + reverse("upload-bundle-analysis"), + { + "commit": commit_sha, + "slug": f"fakerepo::::{repository.name}", + "build": "test-build", + "buildURL": "test-build-url", + "job": "test-job", + "service": "test-service", + "compareSha": "6fd5b89357fc8cdf34d6197549ac7c6d7e5aaaaa", + "branch": "f1:main", + "git_service": "github", + }, + format="json", + headers={"User-Agent": "codecov-cli/0.4.7"}, + ) + + assert res.status_code == 401 + assert res.json() == {"detail": "Not valid tokenless upload"} + assert not upload.called + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_upload_bundle_analysis_tokenless_no_git_service( + db, client, mocker, mock_redis +): + upload = mocker.patch.object(TaskService, "upload") + + repository = RepositoryFactory.create(private=False) + commit_sha = "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef" + + client = APIClient() + + res = client.post( + reverse("upload-bundle-analysis"), + { + "commit": commit_sha, + "slug": f"{repository.author.username}::::{repository.name}", + "build": "test-build", + "buildURL": "test-build-url", + "job": "test-job", + "service": "test-service", + "compareSha": "6fd5b89357fc8cdf34d6197549ac7c6d7e5aaaaa", + "branch": "f1:main", + "git_service": "fakegitservice", + }, + format="json", + headers={"User-Agent": "codecov-cli/0.4.7"}, + ) + + assert res.status_code == 401 + assert res.json() == {"detail": "Not valid tokenless upload"} + assert not upload.called + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_upload_bundle_analysis_tokenless_bad_json(db, client, mocker, mock_redis): + upload = mocker.patch.object(TaskService, "upload") + + repository = RepositoryFactory.create(private=False) + commit_sha = "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef" + + from json import JSONDecodeError + + with patch( + "codecov_auth.authentication.repo_auth.json.loads", + side_effect=JSONDecodeError("mocked error", doc="doc", pos=0), + ): + client = APIClient() + + res = client.post( + reverse("upload-bundle-analysis"), + { + "commit": commit_sha, + "slug": f"{repository.author.username}::::{repository.name}", + "build": "test-build", + "buildURL": "test-build-url", + "job": "test-job", + "service": "test-service", + "compareSha": "6fd5b89357fc8cdf34d6197549ac7c6d7e5aaaaa", + "branch": "f1:main", + "git_service": "github", + }, + format="json", + headers={"User-Agent": "codecov-cli/0.4.7"}, + ) + + assert res.status_code == 401 + assert not upload.called + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_upload_bundle_analysis_tokenless_mismatched_branch( + db, client, mocker, mock_redis +): + upload = mocker.patch.object(TaskService, "upload") + + commit_sha = "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef" + repository = RepositoryFactory.create( + private=False, + author__upload_token_required_for_public_repos=True, + ) + CommitFactory.create(repository=repository, commitid=commit_sha, branch="main") + + client = APIClient() + + res = client.post( + reverse("upload-bundle-analysis"), + { + "commit": commit_sha, + "slug": f"{repository.author.username}::::{repository.name}", + "build": "test-build", + "buildURL": "test-build-url", + "job": "test-job", + "service": "test-service", + "compareSha": "6fd5b89357fc8cdf34d6197549ac7c6d7e5aaaaa", + "branch": "f1:main", + "git_service": "github", + }, + format="json", + headers={"User-Agent": "codecov-cli/0.4.7"}, + ) + + assert res.status_code == 401 + assert res.json() == {"detail": "Not valid tokenless upload"} + assert not upload.called + + +def test_upload_bundle_analysis_view_exception_handling(db, client, mocker, mock_redis): + try: + with patch( + "upload.views.bundle_analysis.BundleAnalysisView._handle_upload", + side_effect=Exception("Test Exception"), + ): + client = APIClient() + repository = RepositoryFactory.create() + client.credentials(HTTP_AUTHORIZATION=f"token {repository.upload_token}") + + mock_inc_counter = mocker.patch("upload.views.bundle_analysis.inc_counter") + + client.post( + reverse("upload-bundle-analysis"), + { + "commit": "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef", + "slug": f"{repository.author.username}::::{repository.name}", + }, + format="json", + ) + except Exception as e: + # Check that the Test Exception was raised and the inc_counter went up + assert str(e) == "Test Exception" + mock_inc_counter.assert_any_call( + ANY, + labels=mocker.ANY, + ) + labels = mock_inc_counter.call_args[1]["labels"] + assert labels["result"] == "error" diff --git a/apps/codecov-api/upload/tests/views/test_commits.py b/apps/codecov-api/upload/tests/views/test_commits.py new file mode 100644 index 0000000000..b433ba8081 --- /dev/null +++ b/apps/codecov-api/upload/tests/views/test_commits.py @@ -0,0 +1,421 @@ +from unittest.mock import patch + +import pytest +from django.conf import settings +from django.urls import reverse +from rest_framework.exceptions import ValidationError +from rest_framework.test import APIClient +from shared.django_apps.core.tests.factories import CommitFactory, RepositoryFactory + +from core.models import Commit +from services.task import TaskService +from upload.views.commits import CommitViews + + +def test_get_repo(db): + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + repository.save() + upload_views = CommitViews() + upload_views.kwargs = dict(repo="codecov::::the_repo", service="github") + recovered_repo = upload_views.get_repo() + assert recovered_repo == repository + + +def test_get_repo_with_invalid_service(): + upload_views = CommitViews() + upload_views.kwargs = dict(repo="repo", service="wrong service") + with pytest.raises(ValidationError) as exp: + upload_views.get_repo() + assert exp.match("Service not found: wrong service") + + +def test_get_repo_not_found(db): + # Making sure that owner has different repos and getting none when the name of the repo isn't correct + RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + upload_views = CommitViews() + upload_views.kwargs = dict(repo="codecov::::wrong-repo-name", service="github") + with pytest.raises(ValidationError) as exp: + upload_views.get_repo() + assert exp.match("Repository not found") + + +def test_deactivated_repo(db): + repo = RepositoryFactory( + name="the_repo", + author__username="codecov", + author__service="github", + active=True, + activated=False, + ) + repo.save() + repo_slug = f"{repo.author.username}::::{repo.name}" + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="token " + repo.upload_token) + url = reverse( + "new_upload.commits", + args=[repo.author.service, repo_slug], + ) + response = client.post( + url, + {"commitid": "commit_sha"}, + format="json", + ) + response_json = response.json() + assert response.status_code == 400 + assert response_json == [ + f"This repository is deactivated. To resume uploading to it, please activate the repository in the codecov UI: {settings.CODECOV_DASHBOARD_URL}/github/codecov/the_repo/config/general" + ] + + +def test_get_queryset(db): + target_repo = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + random_repo = RepositoryFactory() + target_commit_1 = CommitFactory(repository=target_repo) + target_commit_2 = CommitFactory(repository=target_repo) + random_commit = CommitFactory(repository=random_repo) + upload_views = CommitViews() + upload_views.kwargs = dict(repo="codecov::::the_repo", service="github") + recovered_commits = upload_views.get_queryset() + assert target_commit_1 in recovered_commits + assert target_commit_2 in recovered_commits + assert random_commit not in recovered_commits + + +def test_commits_get(client, db): + repo = RepositoryFactory(name="the-repo") + commit_1 = CommitFactory(repository=repo) + commit_2 = CommitFactory(repository=repo) + # Some other commit in the DB that doens't belong to repo + # It should not be returned in the response + CommitFactory() + repo_slug = f"{repo.author.username}::::{repo.name}" + url = reverse("new_upload.commits", args=[repo.author.service, repo_slug]) + assert url == f"/upload/{repo.author.service}/{repo_slug}/commits" + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="token " + repo.upload_token) + res = client.get(url, format="json") + assert res.status_code == 200 + content = res.json() + assert content.get("count") == 2 + # Test that we get the correct commits back, regardless of order + assert content.get("results")[0].get("commitid") in [ + commit_1.commitid, + commit_2.commitid, + ] + assert content.get("results")[1].get("commitid") in [ + commit_1.commitid, + commit_2.commitid, + ] + assert content.get("results")[0].get("commitid") != content.get("results")[1].get( + "commitid" + ) + + +@pytest.mark.parametrize( + "repo_privacy,status_code,detail", + [ + ( + True, + 401, + "Not valid tokenless upload", + ), + ( + False, + 200, + None, + ), + ], +) +def test_commits_get_no_auth(client, db, repo_privacy, status_code, detail): + repo = RepositoryFactory(name="the-repo") + repo.private = repo_privacy + repo.save() + CommitFactory(repository=repo) + CommitFactory(repository=repo) + repo_slug = f"{repo.author.username}::::{repo.name}" + url = reverse("new_upload.commits", args=[repo.author.service, repo_slug]) + assert url == f"/upload/{repo.author.service}/{repo_slug}/commits" + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="token BAD") + res = client.get(url, format="json") + assert res.status_code == status_code + assert res.json().get("detail") == detail + + +def test_commit_post_empty(db, client, mocker): + mocked_call = mocker.patch.object(TaskService, "update_commit") + repository = RepositoryFactory.create() + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="token " + repository.upload_token) + repo_slug = f"{repository.author.username}::::{repository.name}" + url = reverse( + "new_upload.commits", + args=[repository.author.service, repo_slug], + ) + response = client.post( + url, + {"commitid": "commit_sha", "pullid": "4", "branch": "abc"}, + format="json", + ) + response_json = response.json() + commit = Commit.objects.get(commitid="commit_sha") + expected_response = { + "author": None, + "branch": "abc", + "ci_passed": None, + "commitid": "commit_sha", + "message": None, + "parent_commit_id": None, + "repository": { + "name": repository.name, + "is_private": repository.private, + "active": repository.active, + "language": repository.language, + "yaml": repository.yaml, + }, + "pullid": 4, + "state": None, + "timestamp": commit.timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + } + assert response.status_code == 201 + assert expected_response == response_json + mocked_call.assert_called_with(commitid="commit_sha", repoid=repository.repoid) + + +def test_create_commit_already_exists(db, client, mocker): + mocked_call = mocker.patch.object(TaskService, "update_commit") + repository = RepositoryFactory.create() + commit = CommitFactory(repository=repository, author=None) + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="token " + repository.upload_token) + repo_slug = f"{repository.author.username}::::{repository.name}" + url = reverse( + "new_upload.commits", + args=[repository.author.service, repo_slug], + ) + response = client.post( + url, + {"commitid": commit.commitid, "pullid": "4", "branch": "abc"}, + format="json", + ) + response_json = response.json() + expected_response = { + "author": None, + "branch": commit.branch, + "ci_passed": commit.ci_passed, + "commitid": commit.commitid, + "message": commit.message, + "parent_commit_id": commit.parent_commit_id, + "repository": { + "name": repository.name, + "is_private": repository.private, + "active": repository.active, + "language": repository.language, + "yaml": repository.yaml, + }, + "pullid": commit.pullid, + "state": commit.state, + "timestamp": commit.timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + } + assert response.status_code == 201 + assert expected_response == response_json + mocked_call.assert_not_called() + + +@pytest.mark.parametrize("branch", ["main", "someone:main", "someone/fork:main"]) +@pytest.mark.parametrize("private", [True, False]) +def test_commit_tokenless(db, client, mocker, branch, private): + repository = RepositoryFactory.create( + private=private, + author__username="codecov", + name="the_repo", + author__upload_token_required_for_public_repos=True, + ) + mocked_call = mocker.patch.object(TaskService, "update_commit") + + client = APIClient() + repo_slug = f"{repository.author.username}::::{repository.name}" + url = reverse( + "new_upload.commits", + args=[repository.author.service, repo_slug], + ) + response = client.post( + url, + { + "commitid": "commit_sha", + "pullid": "4", + "branch": branch, + }, + format="json", + ) + + if ":" in branch and private == False: + assert response.status_code == 201 + response_json = response.json() + commit = Commit.objects.get(commitid="commit_sha") + expected_response = { + "author": None, + "branch": f"{branch}", + "ci_passed": None, + "commitid": "commit_sha", + "message": None, + "parent_commit_id": None, + "repository": { + "name": repository.name, + "is_private": repository.private, + "active": repository.active, + "language": repository.language, + "yaml": repository.yaml, + }, + "pullid": 4, + "state": None, + "timestamp": commit.timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + } + assert expected_response == response_json + mocked_call.assert_called_with(commitid="commit_sha", repoid=repository.repoid) + else: + assert response.status_code == 401 + commit = Commit.objects.filter(commitid="commit_sha").first() + assert commit is None + + +@pytest.mark.parametrize("branch", ["main", "someone:main", "someone/fork:main"]) +@pytest.mark.parametrize("private", [True, False]) +@pytest.mark.parametrize("upload_token_required_for_public_repos", [True, False]) +def test_commit_upload_token_required_auth_check( + db, client, mocker, branch, private, upload_token_required_for_public_repos +): + repository = RepositoryFactory( + private=private, + author__username="codecov", + name="the_repo", + author__upload_token_required_for_public_repos=upload_token_required_for_public_repos, + ) + mocked_call = mocker.patch.object(TaskService, "update_commit") + + client = APIClient() + repo_slug = f"{repository.author.username}::::{repository.name}" + url = reverse( + "new_upload.commits", + args=[repository.author.service, repo_slug], + ) + response = client.post( + url, + { + "commitid": "commit_sha", + "pullid": "4", + "branch": branch, + }, + format="json", + ) + + # when TokenlessAuthentication is removed, this test should use `if private == False and upload_token_required_for_public_repos == False:` + # but TokenlessAuthentication lets some additional uploads through. + authorized_by_tokenless_auth_class = ":" in branch + + if private == False and ( + upload_token_required_for_public_repos == False + or authorized_by_tokenless_auth_class + ): + assert response.status_code == 201 + response_json = response.json() + commit = Commit.objects.get(commitid="commit_sha") + expected_response = { + "author": None, + "branch": f"{branch}", + "ci_passed": None, + "commitid": "commit_sha", + "message": None, + "parent_commit_id": None, + "repository": { + "name": repository.name, + "is_private": repository.private, + "active": repository.active, + "language": repository.language, + "yaml": repository.yaml, + }, + "pullid": 4, + "state": None, + "timestamp": commit.timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + } + assert expected_response == response_json + mocked_call.assert_called_with(commitid="commit_sha", repoid=repository.repoid) + else: + assert response.status_code == 401 + commit = Commit.objects.filter(commitid="commit_sha").first() + assert commit is None + + +@patch("upload.helpers.jwt.decode") +@patch("upload.helpers.PyJWKClient") +def test_commit_github_oidc_auth(mock_jwks_client, mock_jwt_decode, db, mocker): + repository = RepositoryFactory.create( + private=False, author__username="codecov", name="the_repo" + ) + mocked_call = mocker.patch.object(TaskService, "update_commit") + mock_prometheus_metrics = mocker.patch("upload.metrics.API_UPLOAD_COUNTER.labels") + mock_jwt_decode.return_value = { + "repository": f"url/{repository.name}", + "repository_owner": repository.author.username, + "iss": "https://token.actions.githubusercontent.com", + } + token = "ThisValueDoesNotMatterBecauseOf_mock_jwt_decode" + + client = APIClient() + repo_slug = f"{repository.author.username}::::{repository.name}" + url = reverse( + "new_upload.commits", + args=[repository.author.service, repo_slug], + ) + response = client.post( + url, + { + "commitid": "commit_sha", + "pullid": "4", + }, + format="json", + headers={"Authorization": f"token {token}", "User-Agent": "codecov-cli/0.4.7"}, + ) + assert response.status_code == 201 + response_json = response.json() + commit = Commit.objects.get(commitid="commit_sha") + expected_response = { + "author": None, + "branch": None, + "ci_passed": None, + "commitid": "commit_sha", + "message": None, + "parent_commit_id": None, + "repository": { + "name": repository.name, + "is_private": repository.private, + "active": repository.active, + "language": repository.language, + "yaml": repository.yaml, + }, + "pullid": 4, + "state": None, + "timestamp": commit.timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + } + assert expected_response == response_json + mocked_call.assert_called_with(commitid="commit_sha", repoid=repository.repoid) + mock_prometheus_metrics.assert_called_with( + **{ + "agent": "cli", + "version": "0.4.7", + "action": "coverage", + "endpoint": "create_commit", + "repo_visibility": "public", + "is_using_shelter": "no", + "position": "end", + "upload_version": None, + }, + ) diff --git a/apps/codecov-api/upload/tests/views/test_empty_upload.py b/apps/codecov-api/upload/tests/views/test_empty_upload.py new file mode 100644 index 0000000000..7ad51f3a46 --- /dev/null +++ b/apps/codecov-api/upload/tests/views/test_empty_upload.py @@ -0,0 +1,584 @@ +from unittest.mock import patch + +from django.urls import reverse +from rest_framework.test import APIClient +from shared.django_apps.codecov_auth.tests.factories import ( + OrganizationLevelTokenFactory, +) +from shared.django_apps.core.tests.factories import CommitFactory, RepositoryFactory +from shared.yaml.user_yaml import UserYaml + +from upload.views.uploads import CanDoCoverageUploadsPermission + + +class MockedProviderAdapter: + def __init__(self, changed_files) -> None: + self.changed_files = changed_files + + async def find_pull_request(self, commit): + # Random value + return "5" + + async def get_pull_request_files(self, pull_id): + return self.changed_files + + +def test_uploads_get_not_allowed(client, db, mocker): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + repository = RepositoryFactory( + name="the-repo", author__username="codecov", author__service="github" + ) + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.empty_upload", + args=["github", "codecov::::the-repo", "commit-sha"], + ) + assert url == "/upload/github/codecov::::the-repo/commits/commit-sha/empty-upload" + res = client.get(url) + assert res.status_code == 405 + + +@patch("services.task.TaskService.notify") +@patch("upload.views.empty_upload.final_commit_yaml") +@patch("services.repo_providers.RepoProviderService.get_adapter") +def test_empty_upload_with_yaml_ignored_files( + mock_repo_provider_service, mock_final_yaml, notify_mock, db, mocker +): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + mock_prometheus_metrics = mocker.patch("upload.metrics.API_UPLOAD_COUNTER.labels") + mock_final_yaml.return_value = UserYaml( + { + "ignore": [ + "file.py", + "another_file.py", + ] + } + ) + mock_repo_provider_service.return_value = MockedProviderAdapter( + [ + "file.py", + ] + ) + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + repository.save() + commit.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.empty_upload", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post(url, headers={"User-Agent": "codecov-cli/0.4.7"}) + response_json = response.json() + assert response.status_code == 200 + assert ( + response_json.get("result") + == "All changed files are ignored. Triggering passing notifications." + ) + notify_mock.assert_called_once_with( + repoid=repository.repoid, commitid=commit.commitid, empty_upload="pass" + ) + mock_prometheus_metrics.assert_called_with( + **{ + "agent": "cli", + "version": "0.4.7", + "action": "coverage", + "endpoint": "empty_upload", + "repo_visibility": "private", + "is_using_shelter": "no", + "position": "end", + "upload_version": None, + }, + ) + + +@patch("services.task.TaskService.notify") +@patch("upload.views.empty_upload.final_commit_yaml") +@patch("services.repo_providers.RepoProviderService.get_adapter") +def test_empty_upload_non_testable_files( + mock_repo_provider_service, mock_final_yaml, notify_mock, db, mocker +): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + mock_final_yaml.return_value = UserYaml( + { + "ignore": [ + "file.py", + "another_file.py", + ] + } + ) + mock_repo_provider_service.return_value = MockedProviderAdapter( + [ + "README.md", + "codecov.yml", + "template.txt", + "dir/sub-dir/codecov.yml", + ".circleci/config.yml", + ] + ) + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + repository.save() + commit.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.empty_upload", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post( + url, + ) + response_json = response.json() + assert response.status_code == 200 + assert ( + response_json.get("result") + == "All changed files are ignored. Triggering passing notifications." + ) + assert response_json.get("non_ignored_files") == [] + notify_mock.assert_called_once_with( + repoid=repository.repoid, commitid=commit.commitid, empty_upload="pass" + ) + + +@patch("services.task.TaskService.notify") +@patch("upload.views.empty_upload.final_commit_yaml") +@patch("services.repo_providers.RepoProviderService.get_adapter") +def test_empty_upload_with_testable_file( + mock_repo_provider_service, mock_final_yaml, notify_mock, db, mocker +): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + mock_final_yaml.return_value = UserYaml( + {"ignore": ["file.py", "another_file.py", "README.md"]} + ) + mock_repo_provider_service.return_value = MockedProviderAdapter( + ["README.md", "codecov.yml", "template.txt", "base.py"] + ) + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + repository.save() + commit.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.empty_upload", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post( + url, + ) + response_json = response.json() + assert response.status_code == 200 + assert ( + response_json.get("result") + == "Some files cannot be ignored. Triggering failing notifications." + ) + assert response_json.get("non_ignored_files") == ["base.py"] + notify_mock.assert_called_once_with( + repoid=repository.repoid, commitid=commit.commitid, empty_upload="fail" + ) + + +@patch("services.task.TaskService.notify") +@patch("upload.views.empty_upload.final_commit_yaml") +@patch("services.repo_providers.RepoProviderService.get_adapter") +def test_empty_upload_with_testable_file_with_force( + mock_repo_provider_service, mock_final_yaml, notify_mock, db, mocker +): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + mock_final_yaml.return_value = UserYaml( + {"ignore": ["file.py", "another_file.py", "README.md"]} + ) + mock_repo_provider_service.return_value = MockedProviderAdapter( + ["README.md", "codecov.yml", "template.txt", "base.py"] + ) + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + repository.save() + commit.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.empty_upload", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post(url, data={"should_force": True}) + response_json = response.json() + assert response.status_code == 200 + assert ( + response_json.get("result") + == "Force option was enabled. Triggering passing notifications." + ) + assert response_json.get("non_ignored_files") == [] + notify_mock.assert_called_once_with( + repoid=repository.repoid, commitid=commit.commitid, empty_upload="pass" + ) + + +@patch("services.task.TaskService.notify") +@patch("upload.views.empty_upload.final_commit_yaml") +@patch("services.repo_providers.RepoProviderService.get_adapter") +def test_empty_upload_with_testable_file_invalid_serializer( + mock_repo_provider_service, mock_final_yaml, notify_mock, db, mocker +): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + mock_final_yaml.return_value = UserYaml( + {"ignore": ["file.py", "another_file.py", "README.md"]} + ) + mock_repo_provider_service.return_value = MockedProviderAdapter( + ["README.md", "codecov.yml", "template.txt", "base.py"] + ) + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + repository.save() + commit.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.empty_upload", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post(url, data={"should_force": "hello world"}) + assert response.status_code == 400 + + +@patch("services.task.TaskService.notify") +@patch("upload.views.empty_upload.final_commit_yaml") +@patch("services.repo_providers.RepoProviderService.get_adapter") +def test_empty_upload_no_changed_files_in_pr( + mock_repo_provider_service, mock_final_yaml, notify_mock, db, mocker +): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + mock_final_yaml.return_value = UserYaml( + { + "ignore": [ + "file.py", + "another_file.py", + ] + } + ) + mock_repo_provider_service.return_value = MockedProviderAdapter([]) + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + repository.save() + commit.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.empty_upload", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post( + url, + ) + response_json = response.json() + assert response.status_code == 200 + assert ( + response_json.get("result") + == "All changed files are ignored. Triggering passing notifications." + ) + assert response_json.get("non_ignored_files") == [] + notify_mock.assert_called_once_with( + repoid=repository.repoid, commitid=commit.commitid, empty_upload="pass" + ) + + +@patch("services.task.TaskService.notify") +@patch("upload.views.empty_upload.final_commit_yaml") +@patch("services.repo_providers.RepoProviderService.get_adapter") +@patch("upload.helpers.jwt.decode") +@patch("upload.helpers.PyJWKClient") +def test_empty_upload_no_changed_files_in_pr_github_oidc_auth( + mock_jwks_client, + mock_jwt_decode, + mock_repo_provider_service, + mock_final_yaml, + notify_mock, + db, + mocker, +): + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + mock_jwt_decode.return_value = { + "repository": f"url/{repository.name}", + "repository_owner": repository.author.username, + "iss": "https://token.actions.githubusercontent.com", + } + token = "ThisValueDoesNotMatterBecauseOf_mock_jwt_decode" + mock_final_yaml.return_value = UserYaml( + { + "ignore": [ + "file.py", + "another_file.py", + ] + } + ) + mock_repo_provider_service.return_value = MockedProviderAdapter([]) + + client = APIClient() + url = reverse( + "new_upload.empty_upload", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post( + url, + headers={"Authorization": f"token {token}"}, + ) + response_json = response.json() + assert response.status_code == 200 + assert ( + response_json.get("result") + == "All changed files are ignored. Triggering passing notifications." + ) + assert response_json.get("non_ignored_files") == [] + notify_mock.assert_called_once_with( + repoid=repository.repoid, commitid=commit.commitid, empty_upload="pass" + ) + + +@patch("services.task.TaskService.notify") +@patch("upload.views.empty_upload.final_commit_yaml") +@patch("services.repo_providers.RepoProviderService.get_adapter") +def test_empty_upload_no_commit_pr_id( + mock_repo_provider_service, mock_final_yaml, notify_mock, db, mocker +): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + mock_final_yaml.return_value = UserYaml( + { + "ignore": [ + "file.py", + "another_file.py", + ] + } + ) + mock_repo_provider_service.return_value = MockedProviderAdapter([]) + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository, pullid=None) + repository.save() + commit.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.empty_upload", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post( + url, + ) + response_json = response.json() + assert response.status_code == 200 + assert ( + response_json.get("result") + == "All changed files are ignored. Triggering passing notifications." + ) + assert response_json.get("non_ignored_files") == [] + notify_mock.assert_called_once_with( + repoid=repository.repoid, commitid=commit.commitid, empty_upload="pass" + ) + + +def test_empty_upload_no_auth(db, mocker): + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + token = "BAD" + client = APIClient() + url = reverse( + "new_upload.empty_upload", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post( + url, + headers={"Authorization": f"token {token}"}, + ) + response_json = response.json() + assert response.status_code == 401 + assert ( + response_json.get("detail") + == "Failed token authentication, please double-check that your repository token matches in the Codecov UI, " + "or review the docs https://docs.codecov.com/docs/adding-the-codecov-token" + ) + + +@patch("services.yaml.fetch_commit_yaml") +@patch("services.task.TaskService.notify") +@patch("services.repo_providers.RepoProviderService.get_adapter") +def test_empty_upload_commit_yaml_org_token( + mock_repo_provider_service, notify_mock, fetch_yaml_mock, db, mocker +): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + mock_repo_provider_service.return_value = MockedProviderAdapter( + ["README.md", "codecov.yml", "template.txt", "base.py"] + ) + fetch_yaml_mock.return_value = None + + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + org_token = OrganizationLevelTokenFactory.create(owner=repository.author) + repository.save() + commit.save() + org_token.save() + + client = APIClient() + url = reverse( + "new_upload.empty_upload", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post( + url, + headers={"Authorization": f"token {org_token.token}"}, + ) + response_json = response.json() + assert response.status_code == 200 + assert ( + response_json.get("result") + == "Some files cannot be ignored. Triggering failing notifications." + ) + assert response_json.get("non_ignored_files") == ["base.py"] + notify_mock.assert_called_once_with( + repoid=repository.repoid, commitid=commit.commitid, empty_upload="fail" + ) + + fetch_yaml_mock.assert_called_once_with(commit, repository.author) + + +@patch("services.yaml.fetch_commit_yaml") +@patch("services.task.TaskService.notify") +@patch("services.repo_providers.RepoProviderService.get_adapter") +def test_empty_upload_ommit_yaml_repo_token( + mock_repo_provider_service, notify_mock, fetch_yaml_mock, db, mocker +): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + mock_repo_provider_service.return_value = MockedProviderAdapter( + ["README.md", "codecov.yml", "template.txt", "base.py"] + ) + fetch_yaml_mock.return_value = None + + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + repository.save() + commit.save() + + client = APIClient() + url = reverse( + "new_upload.empty_upload", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post( + url, + headers={"Authorization": f"token {repository.upload_token}"}, + ) + response_json = response.json() + assert response.status_code == 200 + assert ( + response_json.get("result") + == "Some files cannot be ignored. Triggering failing notifications." + ) + assert response_json.get("non_ignored_files") == ["base.py"] + notify_mock.assert_called_once_with( + repoid=repository.repoid, commitid=commit.commitid, empty_upload="fail" + ) + + fetch_yaml_mock.assert_called_once_with(commit, repository.author) diff --git a/apps/codecov-api/upload/tests/views/test_helpers.py b/apps/codecov-api/upload/tests/views/test_helpers.py new file mode 100644 index 0000000000..4950ea9b25 --- /dev/null +++ b/apps/codecov-api/upload/tests/views/test_helpers.py @@ -0,0 +1,151 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from codecov_auth.models import Service +from upload.views.helpers import ( + get_repository_and_owner_from_string, + get_repository_from_string, +) + + +class ViewHelpersTest(TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.gh = Service.GITHUB + cls.bb = Service.BITBUCKET + cls.gl = Service.GITLAB + cls.uname = "simpleusername" + cls.repo_1_name = "simplerepo" + cls.repo_2_name = "anotherrepo" + cls.group = "somegroup" + cls.subgroup = "somesubgroup" + cls.sub_subgroup = "subsub" + owners_to_repos_mapping = { + cls.gh: {cls.uname: [cls.repo_1_name, cls.repo_2_name]}, + cls.bb: {cls.uname: [cls.repo_1_name, cls.repo_2_name]}, + cls.gl: { + cls.uname: [cls.repo_1_name, cls.repo_2_name], + cls.group: [cls.repo_1_name], + f"{cls.group}:{cls.subgroup}": [cls.repo_1_name], + f"{cls.group}:{cls.subgroup}:{cls.sub_subgroup}": [ + cls.repo_1_name, + cls.repo_2_name, + ], + }, + } + for service, owner_data in owners_to_repos_mapping.items(): + for username, repo_names in owner_data.items(): + RepositoryFactory() + o = OwnerFactory(service=service, username=username) + for r_name in repo_names: + RepositoryFactory(author=o, name=r_name) + + def test_get_repository_from_string(self): + first_result = get_repository_from_string( + self.gh, f"{self.uname}::::{self.repo_1_name}" + ) + assert first_result.name == self.repo_1_name + assert first_result.author.username == self.uname + assert first_result.author.service == self.gh + second_result = get_repository_from_string( + self.bb, f"{self.uname}::::{self.repo_1_name}" + ) + assert second_result.name == self.repo_1_name + assert second_result.author.username == self.uname + assert second_result.author.service == self.bb + assert first_result != second_result + assert ( + get_repository_from_string( + self.gh, f"somerandomlalala::::{self.repo_1_name}" + ) + is None + ) + assert ( + get_repository_from_string( + self.gh, f"{self.uname}::::{self.repo_1_name}wrongname" + ) + is None + ) + assert ( + get_repository_from_string( + "badgithub", f"{self.uname}::::{self.repo_1_name}" + ) + is None + ) + first_gitlab_result = get_repository_from_string( + self.gl, f"{self.group}::::{self.repo_1_name}" + ) + assert first_gitlab_result.name == self.repo_1_name + assert first_gitlab_result.author.username == self.group + assert first_gitlab_result.author.service == self.gl + second_gitlab_result = get_repository_from_string( + self.gl, f"{self.group}:::{self.subgroup}::::{self.repo_1_name}" + ) + assert second_gitlab_result.name == self.repo_1_name + assert second_gitlab_result.author.username == f"{self.group}:{self.subgroup}" + assert second_gitlab_result.author.service == self.gl + assert ( + get_repository_from_string( + self.gl, f"{self.group}:::somebadsubgroup::::{self.repo_1_name}" + ) + is None + ) + + def test_get_repository_and_owner_from_string(self): + first_result_repository, first_result_owner = ( + get_repository_and_owner_from_string( + self.gh, f"{self.uname}::::{self.repo_1_name}" + ) + ) + assert first_result_repository.name == self.repo_1_name + assert first_result_repository.author.username == self.uname + assert first_result_repository.author.service == self.gh + assert first_result_repository.author == first_result_owner + + second_result_repository, second_result_owner = ( + get_repository_and_owner_from_string( + self.bb, f"{self.uname}::::{self.repo_1_name}" + ) + ) + assert second_result_repository.name == self.repo_1_name + assert second_result_repository.author.username == self.uname + assert second_result_repository.author.service == self.bb + assert first_result_repository != second_result_repository + assert second_result_repository.author == second_result_owner + + assert get_repository_and_owner_from_string( + self.gh, f"somerandomlalala::::{self.repo_1_name}" + ) == (None, None) + assert get_repository_and_owner_from_string( + self.gh, f"{self.uname}::::{self.repo_1_name}wrongname" + ) == (None, None) + assert get_repository_and_owner_from_string( + "badgithub", f"{self.uname}::::{self.repo_1_name}" + ) == (None, None) + + first_gitlab_result_repository, first_gitlab_result_owner = ( + get_repository_and_owner_from_string( + self.gl, f"{self.group}::::{self.repo_1_name}" + ) + ) + assert first_gitlab_result_repository.name == self.repo_1_name + assert first_gitlab_result_repository.author.username == self.group + assert first_gitlab_result_repository.author.service == self.gl + assert first_gitlab_result_repository.author == first_gitlab_result_owner + + second_gitlab_result_repository, second_gitlab_result_owner = ( + get_repository_and_owner_from_string( + self.gl, f"{self.group}:::{self.subgroup}::::{self.repo_1_name}" + ) + ) + assert second_gitlab_result_repository.name == self.repo_1_name + assert ( + second_gitlab_result_repository.author.username + == f"{self.group}:{self.subgroup}" + ) + assert second_gitlab_result_repository.author.service == self.gl + assert second_gitlab_result_owner == second_gitlab_result_repository.author + assert get_repository_and_owner_from_string( + self.gl, f"{self.group}:::somebadsubgroup::::{self.repo_1_name}" + ) == (None, None) diff --git a/apps/codecov-api/upload/tests/views/test_reports.py b/apps/codecov-api/upload/tests/views/test_reports.py new file mode 100644 index 0000000000..22f1f25c34 --- /dev/null +++ b/apps/codecov-api/upload/tests/views/test_reports.py @@ -0,0 +1,609 @@ +from unittest.mock import patch + +import pytest +from django.conf import settings +from django.urls import reverse +from rest_framework.test import APIClient +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) + +from reports.models import CommitReport, ReportResults +from reports.tests.factories import ReportResultsFactory +from services.task.task import TaskService +from upload.views.uploads import CanDoCoverageUploadsPermission + + +def test_reports_get_not_allowed(client, mocker, db): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + owner = OwnerFactory(service="github") + repo = RepositoryFactory(name="the_repo", private=False, author=owner) + commit = CommitFactory(repository=repo) + commit.branch = "someone:branch" + owner.save() + repo.save() + commit.save() + headers = {} + url = reverse( + "new_upload.reports", + args=["github", f"{owner.username}::::the_repo", commit.commitid], + ) + assert ( + url + == f"/upload/github/{owner.username}::::the_repo/commits/{commit.commitid}/reports" + ) + res = client.get(url, **headers) + assert res.status_code == 405 + + +def test_deactivated_repo(db): + repo = RepositoryFactory( + name="the_repo", + author__username="codecov", + author__service="github", + active=True, + activated=False, + ) + commit = CommitFactory(repository=repo) + repo.save() + commit.save() + repo_slug = f"{repo.author.username}::::{repo.name}" + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="token " + repo.upload_token) + url = reverse( + "new_upload.reports", + args=["github", repo_slug, commit.commitid], + ) + response = client.post( + url, data={"code": "code1"}, headers={"User-Agent": "codecov-cli/0.4.7"} + ) + response_json = response.json() + assert response.status_code == 400 + assert response_json == [ + f"This repository is deactivated. To resume uploading to it, please activate the repository in the codecov UI: {settings.CODECOV_DASHBOARD_URL}/github/codecov/the_repo/config/general" + ] + + +def test_reports_post(client, db, mocker): + mocked_call = mocker.patch.object(TaskService, "preprocess_upload") + mock_prometheus_metrics = mocker.patch("upload.metrics.API_UPLOAD_COUNTER.labels") + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + repository.save() + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="token " + repository.upload_token) + url = reverse( + "new_upload.reports", + args=["github", "codecov::::the_repo", commit.commitid], + ) + response = client.post( + url, data={"code": "code1"}, headers={"User-Agent": "codecov-cli/0.4.7"} + ) + + assert ( + url == f"/upload/github/codecov::::the_repo/commits/{commit.commitid}/reports" + ) + assert response.status_code == 201 + assert CommitReport.objects.filter( + commit_id=commit.id, code="code1", report_type=CommitReport.ReportType.COVERAGE + ).exists() + mocked_call.assert_called_with(repository.repoid, commit.commitid, "code1") + mock_prometheus_metrics.assert_called_with( + **{ + "agent": "cli", + "version": "0.4.7", + "action": "coverage", + "endpoint": "create_report", + "repo_visibility": "private", + "is_using_shelter": "no", + "position": "end", + "upload_version": None, + }, + ) + + +@patch("upload.helpers.jwt.decode") +@patch("upload.helpers.PyJWKClient") +def test_reports_post_github_oidc_auth( + mock_jwks_client, mock_jwt_decode, client, db, mocker +): + mocked_call = mocker.patch.object(TaskService, "preprocess_upload") + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + mock_jwt_decode.return_value = { + "repository": f"url/{repository.name}", + "repository_owner": repository.author.username, + "iss": "https://token.actions.githubusercontent.com", + } + token = "ThisValueDoesNotMatterBecauseOf_mock_jwt_decode" + commit = CommitFactory(repository=repository) + repository.save() + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="token " + token) + url = reverse( + "new_upload.reports", + args=["github", "codecov::::the_repo", commit.commitid], + ) + response = client.post(url, data={"code": "code1"}) + + assert ( + url == f"/upload/github/codecov::::the_repo/commits/{commit.commitid}/reports" + ) + assert response.status_code == 201 + assert CommitReport.objects.filter( + commit_id=commit.id, code="code1", report_type=CommitReport.ReportType.COVERAGE + ).exists() + mocked_call.assert_called_with(repository.repoid, commit.commitid, "code1") + + +@pytest.mark.parametrize("private", [False, True]) +@pytest.mark.parametrize("branch", ["main", "fork:branch", "someone/fork:branch"]) +@pytest.mark.parametrize( + "branch_sent", + [ + None, + "branch", + "fork:branch", + "someone/fork:branch", + ], +) +def test_reports_post_tokenless(client, db, mocker, private, branch, branch_sent): + mocked_call = mocker.patch.object(TaskService, "preprocess_upload") + repository = RepositoryFactory( + name="the_repo", + author__username="codecov", + author__service="github", + author__upload_token_required_for_public_repos=True, + private=private, + ) + commit = CommitFactory(repository=repository) + commit.branch = branch + repository.save() + commit.save() + + client = APIClient() + url = reverse( + "new_upload.reports", + args=["github", "codecov::::the_repo", commit.commitid], + ) + + data = {"code": "code1"} + if branch_sent: + data["branch"] = branch_sent + response = client.post( + url, + data=data, + headers={}, + ) + + assert ( + url == f"/upload/github/codecov::::the_repo/commits/{commit.commitid}/reports" + ) + if private is False and ":" in branch: + assert response.status_code == 201 + assert CommitReport.objects.filter( + commit_id=commit.id, + code="code1", + report_type=CommitReport.ReportType.COVERAGE, + ).exists() + mocked_call.assert_called_with(repository.repoid, commit.commitid, "code1") + else: + assert response.status_code == 401 + assert not CommitReport.objects.filter( + commit_id=commit.id, + code="code1", + report_type=CommitReport.ReportType.COVERAGE, + ).exists() + assert response.json().get("detail") == "Not valid tokenless upload" + + +@pytest.mark.parametrize("private", [False, True]) +@pytest.mark.parametrize("branch", ["main", "fork:branch", "someone/fork:branch"]) +@pytest.mark.parametrize( + "branch_sent", + [ + None, + "branch", + "fork:branch", + "someone/fork:branch", + ], +) +@pytest.mark.parametrize("upload_token_required_for_public_repos", [True, False]) +def test_reports_post_upload_token_required_auth_check( + client, + db, + mocker, + private, + branch, + branch_sent, + upload_token_required_for_public_repos, +): + mocked_call = mocker.patch.object(TaskService, "preprocess_upload") + repository = RepositoryFactory( + name="the_repo", + author__username="codecov", + author__service="github", + private=private, + author__upload_token_required_for_public_repos=upload_token_required_for_public_repos, + ) + commit = CommitFactory(repository=repository) + commit.branch = branch + repository.save() + commit.save() + + client = APIClient() + url = reverse( + "new_upload.reports", + args=["github", "codecov::::the_repo", commit.commitid], + ) + + data = {"code": "code1"} + if branch_sent: + data["branch"] = branch_sent + response = client.post( + url, + data=data, + headers={}, + ) + + assert ( + url == f"/upload/github/codecov::::the_repo/commits/{commit.commitid}/reports" + ) + + # when TokenlessAuthentication is removed, this test should use `if private == False and upload_token_required_for_public_repos == False:` + # but TokenlessAuthentication lets some additional uploads through. + authorized_by_tokenless_auth_class = ":" in branch + + if private == False and ( + upload_token_required_for_public_repos == False + or authorized_by_tokenless_auth_class + ): + assert response.status_code == 201 + assert CommitReport.objects.filter( + commit_id=commit.id, + code="code1", + report_type=CommitReport.ReportType.COVERAGE, + ).exists() + mocked_call.assert_called_with(repository.repoid, commit.commitid, "code1") + else: + assert response.status_code == 401 + assert not CommitReport.objects.filter( + commit_id=commit.id, + code="code1", + report_type=CommitReport.ReportType.COVERAGE, + ).exists() + assert response.json().get("detail") == "Not valid tokenless upload" + + +def test_create_report_already_exists(client, db, mocker): + mocked_call = mocker.patch.object(TaskService, "preprocess_upload") + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + CommitReport.objects.create(commit=commit, code="code") + + repository.save() + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="token " + repository.upload_token) + url = reverse( + "new_upload.reports", + args=["github", "codecov::::the_repo", commit.commitid], + ) + response = client.post(url, data={"code": "code"}) + + assert ( + url == f"/upload/github/codecov::::the_repo/commits/{commit.commitid}/reports" + ) + assert response.status_code == 201 + assert CommitReport.objects.filter( + commit_id=commit.id, code="code", report_type=CommitReport.ReportType.COVERAGE + ).exists() + mocked_call.assert_not_called() + + +def test_reports_post_code_as_default(client, db, mocker): + mocked_call = mocker.patch.object(TaskService, "preprocess_upload") + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + repository.save() + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="token " + repository.upload_token) + url = reverse( + "new_upload.reports", + args=["github", "codecov::::the_repo", commit.commitid], + ) + response = client.post(url, data={"code": "default"}) + + assert ( + url == f"/upload/github/codecov::::the_repo/commits/{commit.commitid}/reports" + ) + assert response.status_code == 201 + assert CommitReport.objects.filter( + commit_id=commit.id, code=None, report_type=CommitReport.ReportType.COVERAGE + ).exists() + mocked_call.assert_called_once() + + +def test_reports_results_post_successful(client, db, mocker): + mocked_task = mocker.patch("services.task.TaskService.create_report_results") + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + commit_report = CommitReport.objects.create(commit=commit, code="code") + repository.save() + commit_report.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.reports_results", + args=["github", "codecov::::the_repo", commit.commitid, "code"], + ) + response = client.post(url, content_type="application/json", data={}) + + assert ( + url + == f"/upload/github/codecov::::the_repo/commits/{commit.commitid}/reports/code/results" + ) + assert response.status_code == 201 + assert ReportResults.objects.filter( + report_id=commit_report.id, + ).exists() + mocked_task.assert_called_once() + + +@patch("upload.helpers.jwt.decode") +@patch("upload.helpers.PyJWKClient") +def test_reports_results_post_successful_github_oidc_auth( + mock_jwks_client, mock_jwt_decode, client, db, mocker +): + mocked_task = mocker.patch("services.task.TaskService.create_report_results") + mock_prometheus_metrics = mocker.patch("upload.metrics.API_UPLOAD_COUNTER.labels") + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + mock_jwt_decode.return_value = { + "repository": f"url/{repository.name}", + "repository_owner": repository.author.username, + "iss": "https://token.actions.githubusercontent.com", + } + token = "ThisValueDoesNotMatterBecauseOf_mock_jwt_decode" + commit = CommitFactory(repository=repository) + commit_report = CommitReport.objects.create(commit=commit, code="code") + repository.save() + commit_report.save() + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {token}") + url = reverse( + "new_upload.reports_results", + args=["github", "codecov::::the_repo", commit.commitid, "code"], + ) + response = client.post( + url, + content_type="application/json", + data={}, + headers={"User-Agent": "codecov-cli/0.4.7"}, + ) + + assert ( + url + == f"/upload/github/codecov::::the_repo/commits/{commit.commitid}/reports/code/results" + ) + assert response.status_code == 201 + assert ReportResults.objects.filter( + report_id=commit_report.id, + ).exists() + mocked_task.assert_called_once() + mock_prometheus_metrics.assert_called_with( + **{ + "agent": "cli", + "version": "0.4.7", + "action": "coverage", + "endpoint": "create_report_results", + "repo_visibility": "private", + "is_using_shelter": "no", + "position": "end", + "upload_version": None, + }, + ) + + +@pytest.mark.parametrize("private", [False, True]) +@pytest.mark.parametrize("branch", ["main", "fork:branch", "someone/fork:branch"]) +@pytest.mark.parametrize( + "branch_sent", + [ + None, + "branch", + "fork:branch", + "someone/fork:branch", + ], +) +@pytest.mark.parametrize("upload_token_required_for_public_repos", [True, False]) +def test_reports_results_post_upload_token_required_auth_check( + client, + db, + mocker, + private, + branch, + branch_sent, + upload_token_required_for_public_repos, +): + mocked_task = mocker.patch("services.task.TaskService.create_report_results") + repository = RepositoryFactory( + name="the_repo", + author__username="codecov", + author__service="github", + private=private, + author__upload_token_required_for_public_repos=upload_token_required_for_public_repos, + ) + commit = CommitFactory(repository=repository) + commit_report = CommitReport.objects.create(commit=commit, code="code") + commit.branch = branch + repository.save() + commit.save() + + client = APIClient() + url = reverse( + "new_upload.reports_results", + args=["github", "codecov::::the_repo", commit.commitid, "code"], + ) + + data = {"code": "code1"} + if branch_sent: + data["branch"] = branch_sent + response = client.post( + url, + data=data, + headers={}, + ) + + assert ( + url + == f"/upload/github/codecov::::the_repo/commits/{commit.commitid}/reports/code/results" + ) + + # when TokenlessAuthentication is removed, this test should use `if private == False and upload_token_required_for_public_repos == False:` + # but TokenlessAuthentication lets some additional uploads through. + authorized_by_tokenless_auth_class = ":" in branch + + if private == False and ( + upload_token_required_for_public_repos == False + or authorized_by_tokenless_auth_class + ): + assert response.status_code == 201 + assert ReportResults.objects.filter( + report_id=commit_report.id, + ).exists() + mocked_task.assert_called_once() + else: + assert response.status_code == 401 + assert not ReportResults.objects.filter( + report_id=commit_report.id, + ).exists() + assert response.json().get("detail") == "Not valid tokenless upload" + + +def test_reports_results_already_exists_post_successful(client, db, mocker): + mocked_task = mocker.patch("services.task.TaskService.create_report_results") + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + commit_report = CommitReport.objects.create(commit=commit, code="code") + report_results = ReportResults.objects.create( + report=commit_report, state=ReportResults.ReportResultsStates.COMPLETED + ) + repository.save() + commit_report.save() + report_results.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.reports_results", + args=["github", "codecov::::the_repo", commit.commitid, "code"], + ) + response = client.post(url, content_type="application/json", data={}) + + assert ( + url + == f"/upload/github/codecov::::the_repo/commits/{commit.commitid}/reports/code/results" + ) + assert response.status_code == 201 + assert ReportResults.objects.filter( + report_id=commit_report.id, state=ReportResults.ReportResultsStates.PENDING + ).exists() + mocked_task.assert_called_once() + + +def test_report_results_get_successful(client, db, mocker): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + commit_report = CommitReport.objects.create(commit=commit, code="code") + commit_report_results = ReportResultsFactory(report=commit_report) + repository.save() + commit_report_results.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.reports_results", + args=["github", "codecov::::the_repo", commit.commitid, "code"], + ) + response = client.get(url, content_type="application/json", data={}) + + assert ( + url + == f"/upload/github/codecov::::the_repo/commits/{commit.commitid}/reports/code/results" + ) + assert response.status_code == 200 + assert response.json() == { + "external_id": str(commit_report_results.external_id), + "report": { + "external_id": str(commit_report.external_id), + "created_at": commit_report.created_at.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "commit_sha": commit_report.commit.commitid, + "code": commit_report.code, + }, + "state": commit_report_results.state, + "result": {}, + "completed_at": None, + } + + +def test_report_results_get_unsuccessful(client, db, mocker): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + CommitReport.objects.create(commit=commit, code="code") + repository.save() + + client = APIClient() + client.force_authenticate(user=OwnerFactory()) + url = reverse( + "new_upload.reports_results", + args=["github", "codecov::::the_repo", commit.commitid, "code"], + ) + response = client.get(url, content_type="application/json", data={}) + + assert ( + url + == f"/upload/github/codecov::::the_repo/commits/{commit.commitid}/reports/code/results" + ) + assert response.status_code == 400 + assert response.json() == ["Report Results not found"] diff --git a/apps/codecov-api/upload/tests/views/test_test_results.py b/apps/codecov-api/upload/tests/views/test_test_results.py new file mode 100644 index 0000000000..85449fcea5 --- /dev/null +++ b/apps/codecov-api/upload/tests/views/test_test_results.py @@ -0,0 +1,620 @@ +import json +import re +from unittest.mock import ANY, patch + +from django.urls import reverse +from rest_framework.test import APIClient +from shared.django_apps.codecov_auth.tests.factories import ( + OrganizationLevelTokenFactory, +) +from shared.django_apps.core.models import Commit +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.events.amplitude import UNKNOWN_USER_OWNERID +from shared.helpers.redis import get_redis_connection + +from services.task import TaskService + + +def test_upload_test_results(db, client, mocker, mock_redis): + upload = mocker.patch.object(TaskService, "upload") + mock_prometheus_metrics = mocker.patch("upload.metrics.API_UPLOAD_COUNTER.labels") + create_presigned_put = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + mock_amplitude = mocker.patch( + "shared.events.amplitude.AmplitudeEventPublisher.publish" + ) + + owner = OwnerFactory(service="github", username="codecov") + repository = RepositoryFactory.create(author=owner) + commit_sha = "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef" + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {repository.upload_token}") + + res = client.post( + reverse("upload-test-results"), + { + "commit": commit_sha, + "slug": f"{repository.author.username}::::{repository.name}", + "build": "test-build", + "buildURL": "test-build-url", + "job": "test-job", + "service": "github-actions", + "branch": "aaaaaa", + }, + format="json", + headers={"User-Agent": "codecov-cli/0.4.7"}, + ) + assert res.status_code == 201 + + # returns presigned storage URL + assert res.json() == {"raw_upload_location": "test-presigned-put"} + + create_presigned_put.assert_called_once_with("archive", ANY, 10) + call = create_presigned_put.mock_calls[0] + _, storage_path, _ = call.args + match = re.match( + r"test_results/v1/raw/([\d\w\-]+)/([\d\w\-]+)/([\d\w\-]+)/([\d\w\-]+)\.txt", + storage_path, + ) + assert match + ( + date, + repo_hash, + commit_sha, + reportid, + ) = match.groups() + + # creates commit + commit = Commit.objects.get(commitid=commit_sha) + assert commit + assert commit.branch is not None + + # saves args in Redis + redis = get_redis_connection() + args = json.loads( + redis.rpop(f"uploads/{repository.repoid}/{commit_sha}/test_results") + ) + assert args == { + "reportid": reportid, + "build": "test-build", + "build_url": "test-build-url", + "job": "test-job", + "service": "github-actions", + "url": f"test_results/v1/raw/{date}/{repo_hash}/{commit_sha}/{reportid}.txt", + "commit": commit_sha, + "report_code": None, + "flags": None, + } + + # sets latest upload timestamp + ts = redis.get(f"latest_upload/{repository.repoid}/{commit_sha}/test_results") + assert ts + + # triggers upload task + upload.assert_called_with( + commitid=commit_sha, + repoid=repository.repoid, + report_code=None, + report_type="test_results", + arguments=args, + countdown=4, + ) + mock_prometheus_metrics.assert_called_with( + **{ + "agent": "cli", + "version": "0.4.7", + "action": "test_results", + "endpoint": "test_results", + "repo_visibility": "private", + "is_using_shelter": "no", + "position": "end", + "upload_version": None, + }, + ) + + # emits Amplitude event + mock_amplitude.assert_called_with( + "Upload Received", + { + "user_ownerid": UNKNOWN_USER_OWNERID, + "ownerid": commit.repository.author.ownerid, + "repoid": commit.repository.repoid, + "commitid": commit.id, + "pullid": commit.pullid, + "upload_type": "Test results", + }, + ) + + +def test_test_results_org_token(db, client, mocker, mock_redis): + mocker.patch.object(TaskService, "upload") + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + + owner = OwnerFactory(service="github", username="codecov") + repository = RepositoryFactory.create(author=owner) + org_token = OrganizationLevelTokenFactory.create(owner=repository.author) + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {org_token.token}") + + res = client.post( + reverse("upload-test-results"), + { + "commit": "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef", + "slug": f"{repository.author.username}::::{repository.name}", + "branch": "aaaaaa", + }, + format="json", + ) + assert res.status_code == 201 + + +@patch("upload.helpers.jwt.decode") +@patch("upload.helpers.PyJWKClient") +def test_test_results_github_oidc_token( + mock_jwks_client, mock_jwt_decode, db, client, mocker, mock_redis +): + mocker.patch.object(TaskService, "upload") + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + + owner = OwnerFactory(service="github", username="codecov") + repository = RepositoryFactory.create(author=owner) + mock_jwt_decode.return_value = { + "repository": f"url/{repository.name}", + "repository_owner": repository.author.username, + "iss": "https://token.actions.githubusercontent.com", + } + token = "ThisValueDoesNotMatterBecauseOf_mock_jwt_decode" + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {token}") + + res = client.post( + reverse("upload-test-results"), + { + "commit": "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef", + "slug": f"{repository.author.username}::::{repository.name}", + "branch": "aaaaaa", + }, + format="json", + ) + assert res.status_code == 201 + + +def test_test_results_upload_token_not_required(db, client, mocker, mock_redis): + mocker.patch.object(TaskService, "upload") + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + + owner = OwnerFactory( + service="github", + username="codecov", + upload_token_required_for_public_repos=False, + ) + repository = RepositoryFactory.create(author=owner, private=False) + + client = APIClient() + + res = client.post( + reverse("upload-test-results"), + { + "commit": "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef", + "slug": f"{repository.author.username}::::{repository.name}", + "branch": "aaaaaa", + "service": owner.service, + }, + format="json", + ) + assert res.status_code == 201 + + +def test_test_results_no_auth(db, client, mocker, mock_redis): + owner = OwnerFactory(service="github", username="codecov") + repository = RepositoryFactory.create(author=owner) + token = "BAD" + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {token}") + + res = client.post( + reverse("upload-test-results"), + { + "commit": "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef", + "slug": f"{repository.author.username}::::{repository.name}", + }, + format="json", + ) + assert res.status_code == 401 + assert res.json().get("detail") == "Not valid tokenless upload" + + +def test_upload_test_results_no_repo(db, client, mocker, mock_redis): + upload = mocker.patch.object(TaskService, "upload") + mocker.patch.object(TaskService, "upload") + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + + repository = RepositoryFactory.create() + org_token = OrganizationLevelTokenFactory.create(owner=repository.author) + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {org_token.token}") + + res = client.post( + reverse("upload-test-results"), + { + "commit": "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef", + "slug": "FakeUser::::NonExistentName", + }, + format="json", + ) + assert res.status_code == 404 + assert res.json() == {"detail": "Repository not found."} + assert not upload.called + + +def test_upload_test_results_missing_args(db, client, mocker, mock_redis): + upload = mocker.patch.object(TaskService, "upload") + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + + repository = RepositoryFactory.create() + commit = CommitFactory.create(repository=repository) + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {repository.upload_token}") + + res = client.post( + reverse("upload-test-results"), + { + "commit": commit.commitid, + }, + format="json", + ) + assert res.status_code == 400 + assert res.json() == {"slug": ["This field is required."]} + assert not upload.called + + res = client.post( + reverse("upload-test-results"), + { + "slug": f"{repository.author.username}::::{repository.name}", + }, + format="json", + ) + assert res.status_code == 400 + assert res.json() == {"commit": ["This field is required."]} + assert not upload.called + + +def test_upload_test_results_missing_branch_no_commit(db, client, mocker, mock_redis): + upload = mocker.patch.object(TaskService, "upload") + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + + repository = RepositoryFactory.create() + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {repository.upload_token}") + + commit_sha = "aaaaaa" + res = client.post( + reverse("upload-test-results"), + { + "commit": "aaaaaa", + "slug": f"{repository.author.username}::::{repository.name}", + }, + format="json", + ) + assert res.status_code == 201 + + assert upload.called + + commit = Commit.objects.get(commitid=commit_sha) + assert commit.branch is not None + + +def test_upload_test_results_branch_none_no_commit(db, client, mocker, mock_redis): + upload = mocker.patch.object(TaskService, "upload") + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + + repository = RepositoryFactory.create() + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {repository.upload_token}") + + commit_sha = "aaaaaa" + res = client.post( + reverse("upload-test-results"), + { + "commit": "aaaaaa", + "slug": f"{repository.author.username}::::{repository.name}", + "branch": None, + }, + format="json", + ) + assert res.status_code == 201 + + assert upload.called + + commit = Commit.objects.get(commitid=commit_sha) + assert commit.branch is not None + + +def test_update_repo_fields_when_upload_is_triggered( + db, client, mocker, mock_redis +) -> None: + mocker.patch.object(TaskService, "upload") + mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + + repository = RepositoryFactory.create(active=False, activated=False) + commit = CommitFactory.create(repository=repository) + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {repository.upload_token}") + + res = client.post( + reverse("upload-test-results"), + { + "commit": commit.commitid, + "slug": f"{repository.author.username}::::{repository.name}", + }, + format="json", + ) + assert res.status_code == 201 + + repository.refresh_from_db() + assert repository.active is True + assert repository.activated is True + assert repository.test_analytics_enabled is True + + +def test_upload_test_results_file_not_found(db, client, mocker, mock_redis): + upload = mocker.patch.object(TaskService, "upload") + create_presigned_put = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + + owner = OwnerFactory(service="github", username="codecov") + repository = RepositoryFactory.create(author=owner) + commit_sha = "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef" + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {repository.upload_token}") + + res = client.post( + reverse("upload-test-results"), + { + "commit": commit_sha, + "slug": f"{repository.author.username}::::{repository.name}", + "build": "test-build", + "buildURL": "test-build-url", + "job": "test-job", + "service": "github-actions", + "branch": "aaaaaa", + "file_not_found": True, + }, + format="json", + headers={"User-Agent": "codecov-cli/0.4.7"}, + ) + assert res.status_code == 201 + + assert res.data is None + + create_presigned_put.assert_not_called() + + commit = Commit.objects.get(commitid=commit_sha) + assert commit + assert commit.branch is not None + + redis = get_redis_connection() + args = json.loads( + redis.rpop(f"uploads/{repository.repoid}/{commit_sha}/test_results") + ) + assert args == { + "reportid": mocker.ANY, + "build": "test-build", + "build_url": "test-build-url", + "job": "test-job", + "service": "github-actions", + "url": None, + "commit": commit_sha, + "report_code": None, + "flags": None, + } + + # sets latest upload timestamp + ts = redis.get(f"latest_upload/{repository.repoid}/{commit_sha}/test_results") + assert ts + + # triggers upload task + upload.assert_called_with( + commitid=commit_sha, + repoid=repository.repoid, + report_code=None, + report_type="test_results", + arguments=args, + countdown=4, + ) + + +def test_upload_test_results_tokenless_authentication(db, client, mocker, mock_redis): + upload = mocker.patch.object(TaskService, "upload") + _ = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + + owner = OwnerFactory(service="github", username="codecov") + repository = RepositoryFactory.create(author=owner, private=False) + commit_sha = "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef" + + client = APIClient() + + res = client.post( + reverse("upload-test-results"), + { + "commit": commit_sha, + "slug": f"{repository.author.username}::::{repository.name}", + "build": "test-build", + "buildURL": "test-build-url", + "job": "test-job", + "service": "github", + "branch": "fork:branch", + }, + format="json", + headers={"User-Agent": "codecov-cli/0.4.7"}, + ) + assert res.status_code == 201 + + assert res.data == {"raw_upload_location": "test-presigned-put"} + + commit = Commit.objects.get(commitid=commit_sha) + assert commit + assert commit.branch is not None + + # triggers upload task + upload.assert_called_with( + commitid=commit_sha, + repoid=repository.repoid, + report_code=None, + report_type="test_results", + arguments=mocker.ANY, + countdown=4, + ) + + +def test_upload_test_results_tokenless_authentication_private_repo( + db, client, mocker, mock_redis +): + _ = mocker.patch.object(TaskService, "upload") + _ = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + + owner = OwnerFactory(service="github", username="codecov") + repository = RepositoryFactory.create(author=owner, private=True) + commit_sha = "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef" + + client = APIClient() + + res = client.post( + reverse("upload-test-results"), + { + "commit": commit_sha, + "slug": f"{repository.author.username}::::{repository.name}", + "build": "test-build", + "buildURL": "test-build-url", + "job": "test-job", + "service": "github", + "branch": "fork:branch", + }, + format="json", + headers={"User-Agent": "codecov-cli/0.4.7"}, + ) + assert res.status_code == 401 + assert res.json().get("detail") == "Not valid tokenless upload" + + +def test_upload_test_results_tokenless_authentication_invalid_branch( + db, client, mocker, mock_redis +): + _ = mocker.patch.object(TaskService, "upload") + _ = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + + owner = OwnerFactory(service="github", username="codecov") + owner.upload_token_required_for_public_repos = True + owner.save() + + repository = RepositoryFactory.create(author=owner, private=False) + commit_sha = "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef" + + client = APIClient() + + res = client.post( + reverse("upload-test-results"), + { + "commit": commit_sha, + "slug": f"{repository.author.username}::::{repository.name}", + "build": "test-build", + "buildURL": "test-build-url", + "job": "test-job", + "service": "github", + "branch": "branch", + }, + format="json", + headers={"User-Agent": "codecov-cli/0.4.7"}, + ) + assert res.status_code == 401 + assert res.json().get("detail") == "Not valid tokenless upload" + + +def test_upload_test_results_tokenless_authentication_invalid_branch_existing_commits( + db, client, mocker, mock_redis +): + _ = mocker.patch.object(TaskService, "upload") + _ = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="test-presigned-put", + ) + + owner = OwnerFactory(service="github", username="codecov") + owner.upload_token_required_for_public_repos = True + owner.save() + + repository = RepositoryFactory.create(author=owner, private=False) + commit_sha = "6fd5b89357fc8cdf34d6197549ac7c6d7e5977ef" + CommitFactory.create(repository=repository, commitid=commit_sha, branch="branch") + client = APIClient() + + res = client.post( + reverse("upload-test-results"), + { + "commit": commit_sha, + "slug": f"{repository.author.username}::::{repository.name}", + "build": "test-build", + "buildURL": "test-build-url", + "job": "test-job", + "service": "github", + "branch": "branch", + }, + format="json", + headers={"User-Agent": "codecov-cli/0.4.7"}, + ) + assert res.status_code == 401 + assert res.json().get("detail") == "Not valid tokenless upload" + commit = Commit.objects.get(commitid=commit_sha) + assert commit + assert commit.branch == "branch" diff --git a/apps/codecov-api/upload/tests/views/test_upload_completion.py b/apps/codecov-api/upload/tests/views/test_upload_completion.py new file mode 100644 index 0000000000..9c6b5f44e0 --- /dev/null +++ b/apps/codecov-api/upload/tests/views/test_upload_completion.py @@ -0,0 +1,367 @@ +from unittest.mock import patch + +from django.http import HttpResponse +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient +from shared.django_apps.core.tests.factories import CommitFactory, RepositoryFactory + +from reports.tests.factories import CommitReportFactory, UploadFactory +from upload.views.uploads import CanDoCoverageUploadsPermission + + +def test_upload_completion_view_no_uploads(db, mocker): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + repository.save() + commit.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.upload-complete", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post( + url, + ) + response_json = response.json() + assert response.status_code == 404 + assert response_json == { + "uploads_total": 0, + "uploads_success": 0, + "uploads_processing": 0, + "uploads_error": 0, + } + + +@patch("services.task.TaskService.manual_upload_completion_trigger") +def test_upload_completion_view_processed_uploads(mocked_manual_trigger, db, mocker): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + mock_prometheus_metrics = mocker.patch("upload.metrics.API_UPLOAD_COUNTER.labels") + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + report = CommitReportFactory(commit=commit) + upload1 = UploadFactory(report=report) + upload2 = UploadFactory(report=report) + repository.save() + commit.save() + report.save() + upload1.save() + upload2.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.upload-complete", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post(url, headers={"User-Agent": "codecov-cli/0.4.7"}) + response_json = response.json() + assert response.status_code == 200 + assert response_json == { + "uploads_total": 2, + "uploads_success": 2, + "uploads_processing": 0, + "uploads_error": 0, + } + mocked_manual_trigger.assert_called_once_with(repository.repoid, commit.commitid) + mock_prometheus_metrics.assert_called_with( + **{ + "agent": "cli", + "version": "0.4.7", + "action": "coverage", + "endpoint": "upload_complete", + "repo_visibility": "private", + "is_using_shelter": "no", + "position": "end", + "upload_version": None, + }, + ) + + +@patch("services.task.TaskService.manual_upload_completion_trigger") +@patch("upload.helpers.jwt.decode") +@patch("upload.helpers.PyJWKClient") +def test_upload_completion_view_processed_uploads_github_oidc_auth( + mock_jwks_client, mock_jwt_decode, mocked_manual_trigger, db, mocker +): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + mock_jwt_decode.return_value = { + "repository": f"url/{repository.name}", + "repository_owner": repository.author.username, + "iss": "https://token.actions.githubusercontent.com", + } + token = "ThisValueDoesNotMatterBecauseOf_mock_jwt_decode" + commit = CommitFactory(repository=repository) + report = CommitReportFactory(commit=commit) + upload1 = UploadFactory(report=report) + upload2 = UploadFactory(report=report) + repository.save() + commit.save() + report.save() + upload1.save() + upload2.save() + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {token}") + url = reverse( + "new_upload.upload-complete", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post( + url, + ) + response_json = response.json() + assert response.status_code == 200 + assert response_json == { + "uploads_total": 2, + "uploads_success": 2, + "uploads_processing": 0, + "uploads_error": 0, + } + mocked_manual_trigger.assert_called_once_with(repository.repoid, commit.commitid) + + +def test_upload_completion_view_no_auth(db, mocker): + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + token = "BAD" + commit = CommitFactory(repository=repository) + report = CommitReportFactory(commit=commit) + upload1 = UploadFactory(report=report) + upload2 = UploadFactory(report=report) + repository.save() + commit.save() + report.save() + upload1.save() + upload2.save() + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {token}") + url = reverse( + "new_upload.upload-complete", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post( + url, + ) + response_json = response.json() + assert response.status_code == 401 + assert ( + response_json.get("detail") + == "Failed token authentication, please double-check that your repository token matches in the Codecov UI, " + "or review the docs https://docs.codecov.com/docs/adding-the-codecov-token" + ) + + +@patch("codecov_auth.authentication.repo_auth.exception_handler") +def test_upload_completion_view_repo_auth_custom_exception_handler_error( + customized_error, db, mocker +): + mocked_response = HttpResponse( + "No content posted.", + status=status.HTTP_401_UNAUTHORIZED, + content_type="application/json", + ) + mocked_response.data = "invalid" + customized_error.return_value = mocked_response + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + token = "BAD" + commit = CommitFactory(repository=repository) + report = CommitReportFactory(commit=commit) + upload1 = UploadFactory(report=report) + upload2 = UploadFactory(report=report) + repository.save() + commit.save() + report.save() + upload1.save() + upload2.save() + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"token {token}") + url = reverse( + "new_upload.upload-complete", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post( + url, + ) + assert response.status_code == 401 + assert response == mocked_response + + +@patch("services.task.TaskService.manual_upload_completion_trigger") +def test_upload_completion_view_still_processing_uploads( + mocked_manual_trigger, db, mocker +): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + report = CommitReportFactory(commit=commit) + upload1 = UploadFactory(report=report) + upload2 = UploadFactory(report=report, state="") + repository.save() + commit.save() + report.save() + upload1.save() + upload2.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.upload-complete", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post( + url, + ) + response_json = response.json() + assert response.status_code == 200 + assert response_json == { + "uploads_total": 2, + "uploads_success": 1, + "uploads_processing": 1, + "uploads_error": 0, + } + mocked_manual_trigger.assert_called_once_with(repository.repoid, commit.commitid) + + +@patch("services.task.TaskService.manual_upload_completion_trigger") +def test_upload_completion_view_errored_uploads(mocked_manual_trigger, db, mocker): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + report = CommitReportFactory(commit=commit) + upload1 = UploadFactory(report=report) + upload2 = UploadFactory(report=report, state="error") + repository.save() + commit.save() + report.save() + upload1.save() + upload2.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.upload-complete", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post( + url, + ) + response_json = response.json() + assert response.status_code == 200 + assert response_json == { + "uploads_total": 2, + "uploads_success": 1, + "uploads_processing": 0, + "uploads_error": 1, + } + mocked_manual_trigger.assert_called_once_with(repository.repoid, commit.commitid) + + +@patch("services.task.TaskService.manual_upload_completion_trigger") +def test_upload_completion_view_errored_and_processing_uploads( + mocked_manual_trigger, db, mocker +): + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + report = CommitReportFactory(commit=commit) + upload1 = UploadFactory(report=report, state="") + upload2 = UploadFactory(report=report, state="error") + repository.save() + commit.save() + report.save() + upload1.save() + upload2.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.upload-complete", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + ], + ) + response = client.post( + url, + ) + response_json = response.json() + assert response.status_code == 200 + assert response_json == { + "uploads_total": 2, + "uploads_success": 0, + "uploads_processing": 1, + "uploads_error": 1, + } + mocked_manual_trigger.assert_called_once_with(repository.repoid, commit.commitid) diff --git a/apps/codecov-api/upload/tests/views/test_upload_coverage.py b/apps/codecov-api/upload/tests/views/test_upload_coverage.py new file mode 100644 index 0000000000..ec03e1e940 --- /dev/null +++ b/apps/codecov-api/upload/tests/views/test_upload_coverage.py @@ -0,0 +1,300 @@ +from unittest.mock import patch + +from django.conf import settings +from django.test import override_settings +from django.urls import reverse +from rest_framework.test import APIClient +from shared.api_archive.archive import ArchiveService, MinioEndpoints +from shared.django_apps.core.tests.factories import CommitFactory, RepositoryFactory + +from billing.helpers import mock_all_plans_and_tiers +from reports.models import ReportSession, RepositoryFlag, UploadFlagMembership +from upload.views.upload_coverage import CanDoCoverageUploadsPermission + + +def test_get_repo(db): + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + repository.save() + repo_slug = f"{repository.author.username}::::{repository.name}" + url = reverse( + "new_upload.upload_coverage", + args=[repository.author.service, repo_slug], + ) + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="token " + repository.upload_token) + response = client.post(url, {}, format="json") + assert response.status_code == 400 # Bad request due to missing required fields + assert "commitid" in response.json() + + +@patch("services.task.TaskService.upload") +def test_get_repo_not_found(upload, db): + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + repo_slug = "codecov::::wrong-repo-name" + url = reverse( + "new_upload.upload_coverage", + args=[repository.author.service, repo_slug], + ) + client = APIClient() + response = client.post(url, {}, format="json") + assert response.status_code == 401 + assert response.json() == {"detail": "Not valid tokenless upload"} + assert not upload.called + + +def test_deactivated_repo(db): + repository = RepositoryFactory( + name="the_repo", + author__username="codecov", + author__service="github", + active=True, + activated=False, + ) + repository.save() + repo_slug = f"{repository.author.username}::::{repository.name}" + url = reverse( + "new_upload.upload_coverage", + args=[repository.author.service, repo_slug], + ) + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="token " + repository.upload_token) + response = client.post(url, {"commitid": "abc123"}, format="json") + assert response.status_code == 400 + assert "This repository is deactivated" in str(response.json()) + + +def test_upload_coverage_with_errors(db): + mock_all_plans_and_tiers() + repository = RepositoryFactory() + repo_slug = f"{repository.author.username}::::{repository.name}" + url = reverse( + "new_upload.upload_coverage", + args=[repository.author.service, repo_slug], + ) + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="token " + repository.upload_token) + + # Missing required fields + response = client.post(url, {}, format="json") + assert response.status_code == 400 + assert "commitid" in response.json() + + # Invalid flag format + response = client.post( + url, {"commitid": "abc123", "flags": "not-a-list"}, format="json" + ) + assert response.status_code == 400 + assert "flags" in response.json() + + +def test_upload_coverage_post(db, mocker): + mock_all_plans_and_tiers() + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + presigned_put_mock = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="presigned put", + ) + upload_task_mock = mocker.patch( + "upload.views.uploads.trigger_upload_task", return_value=True + ) + amplitude_mock = mocker.patch( + "shared.events.amplitude.AmplitudeEventPublisher.publish" + ) + + repository = RepositoryFactory( + name="the_repo1", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + repository.save() + commit.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + repo_slug = f"{repository.author.username}::::{repository.name}" + url = reverse( + "new_upload.upload_coverage", + args=[repository.author.service, repo_slug], + ) + response = client.post( + url, + { + "branch": "branch", + "ci_service": "ci_service", + "ci_url": "ci_url", + "code": "code", + "commitid": commit.commitid, + "flags": ["flag1", "flag2"], + "job_code": "job_code", + "version": "version", + }, + format="json", + ) + response_json = response.json() + upload = ReportSession.objects.filter( + report__commit=commit, + report__code="code", + upload_extras={"format_version": "v1"}, + ).first() + assert response.status_code == 201 + assert all( + ( + x in response_json.keys() + for x in ["external_id", "created_at", "raw_upload_location", "url"] + ) + ) + assert ( + response_json.get("url") + == f"{settings.CODECOV_DASHBOARD_URL}/{repository.author.service}/{repository.author.username}/{repository.name}/commit/{commit.commitid}" + ) + amplitude_mock.assert_called_with( + "Upload Received", + { + "user_ownerid": commit.author.ownerid, + "ownerid": commit.repository.author.ownerid, + "repoid": commit.repository.repoid, + "commitid": commit.id, + "pullid": commit.pullid, + "upload_type": "Coverage report", + }, + ) + + assert ReportSession.objects.filter( + report__commit=commit, + report__code="code", + upload_extras={"format_version": "v1"}, + ).exists() + assert RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag1" + ).exists() + assert RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag2" + ).exists() + flag1 = RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag1" + ).first() + flag2 = RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag2" + ).first() + assert UploadFlagMembership.objects.filter( + report_session_id=upload.id, flag_id=flag1.id + ).exists() + assert UploadFlagMembership.objects.filter( + report_session_id=upload.id, flag_id=flag2.id + ).exists() + assert list(upload.flags.all()) == [flag1, flag2] + + archive_service = ArchiveService(repository) + assert upload.storage_path == MinioEndpoints.raw_with_upload_id.get_path( + version="v4", + date=upload.created_at.strftime("%Y-%m-%d"), + repo_hash=archive_service.storage_hash, + commit_sha=commit.commitid, + reportid=upload.report.external_id, + uploadid=upload.external_id, + ) + presigned_put_mock.assert_called_with("archive", upload.storage_path, 10) + upload_task_mock.assert_called() + + +@override_settings(SHELTER_SHARED_SECRET="shelter-shared-secret") +def test_upload_coverage_post_shelter(db, mocker): + mock_all_plans_and_tiers() + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + presigned_put_mock = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="presigned put", + ) + upload_task_mock = mocker.patch( + "upload.views.uploads.trigger_upload_task", return_value=True + ) + + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + repository.save() + commit.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + repo_slug = f"{repository.author.username}::::{repository.name}" + url = reverse( + "new_upload.upload_coverage", + args=[repository.author.service, repo_slug], + ) + response = client.post( + url, + { + "branch": "branch", + "ci_service": "ci_service", + "ci_url": "ci_url", + "code": "code", + "commitid": commit.commitid, + "flags": ["flag1", "flag2"], + "job_code": "job_code", + "storage_path": "shelter/test/path.txt", + "version": "version", + }, + headers={ + "X-Shelter-Token": "shelter-shared-secret", + "User-Agent": "codecov-cli/0.4.7", + }, + format="json", + ) + response_json = response.json() + upload = ReportSession.objects.filter( + report__commit=commit, + report__code="code", + upload_extras={"format_version": "v1"}, + ).first() + assert response.status_code == 201 + assert all( + ( + x in response_json.keys() + for x in ["external_id", "created_at", "raw_upload_location", "url"] + ) + ) + assert ( + response_json.get("url") + == f"{settings.CODECOV_DASHBOARD_URL}/{repository.author.service}/{repository.author.username}/{repository.name}/commit/{commit.commitid}" + ) + + assert ReportSession.objects.filter( + report__commit=commit, + report__code="code", + upload_extras={"format_version": "v1"}, + ).exists() + assert RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag1" + ).exists() + assert RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag2" + ).exists() + flag1 = RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag1" + ).first() + flag2 = RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag2" + ).first() + assert UploadFlagMembership.objects.filter( + report_session_id=upload.id, flag_id=flag1.id + ).exists() + assert UploadFlagMembership.objects.filter( + report_session_id=upload.id, flag_id=flag2.id + ).exists() + assert list(upload.flags.all()) == [flag1, flag2] + + assert upload.storage_path == "shelter/test/path.txt" + presigned_put_mock.assert_called_with("archive", upload.storage_path, 10) + upload_task_mock.assert_called() diff --git a/apps/codecov-api/upload/tests/views/test_uploads.py b/apps/codecov-api/upload/tests/views/test_uploads.py new file mode 100644 index 0000000000..9a9d822cfc --- /dev/null +++ b/apps/codecov-api/upload/tests/views/test_uploads.py @@ -0,0 +1,1001 @@ +from unittest.mock import MagicMock, patch + +import pytest +from django.conf import settings +from django.test import override_settings +from django.urls import reverse +from rest_framework.exceptions import ValidationError +from rest_framework.test import APIClient, APITestCase +from shared.api_archive.archive import ArchiveService, MinioEndpoints +from shared.django_apps.codecov_auth.tests.factories import PlanFactory, TierFactory +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from shared.plan.constants import PlanName, TierName +from shared.utils.test_utils import mock_config_helper + +from billing.helpers import mock_all_plans_and_tiers +from codecov_auth.authentication.repo_auth import OrgLevelTokenRepositoryAuth +from codecov_auth.services.org_level_token_service import OrgLevelTokenService +from reports.models import ( + CommitReport, + ReportSession, + RepositoryFlag, + UploadFlagMembership, +) +from reports.tests.factories import CommitReportFactory, UploadFactory +from upload.views.uploads import ( + CanDoCoverageUploadsPermission, + UploadViews, + activate_repo, + trigger_upload_task, +) + + +def test_upload_permission_class_pass(db, mocker): + request_mocked = MagicMock(auth=MagicMock()) + request_mocked.auth.get_scopes.return_value = ["upload"] + permission = CanDoCoverageUploadsPermission() + assert permission.has_permission(request_mocked, MagicMock()) + request_mocked.auth.get_scopes.assert_called_once() + + +def test_upload_permission_orglevel_token(db, mocker): + tier = TierFactory(tier_name=TierName.ENTERPRISE.value) + plan = PlanFactory(name=PlanName.ENTERPRISE_CLOUD_MONTHLY.value, tier=tier) + owner = OwnerFactory(plan=plan.name) + owner.save() + repo = RepositoryFactory(author=owner) + repo.save() + token = OrgLevelTokenService.get_or_create_org_token(owner) + + request_mocked = MagicMock(auth=OrgLevelTokenRepositoryAuth(token)) + mocked_view = MagicMock() + mocked_view.get_repo = MagicMock(return_value=repo) + permission = CanDoCoverageUploadsPermission() + assert permission.has_permission(request_mocked, mocked_view) + mocked_view.get_repo.assert_called_once() + + +def test_upload_permission_class_fail(db, mocker): + request_mocked = MagicMock(auth=MagicMock()) + request_mocked.auth.get_scopes.return_value = ["wrong_scope"] + permission = CanDoCoverageUploadsPermission() + assert not permission.has_permission(request_mocked, MagicMock()) + request_mocked.auth.get_scopes.assert_called_once() + + +def test_upload_permission_orglevel_fail(db, mocker): + tier = TierFactory(tier_name=TierName.ENTERPRISE.value) + plan = PlanFactory(name=PlanName.ENTERPRISE_CLOUD_MONTHLY.value, tier=tier) + owner = OwnerFactory(plan=plan.name) + owner.save() + repo = RepositoryFactory() # Not the same owner of the token + repo.save() + token = OrgLevelTokenService.get_or_create_org_token(owner) + + request_mocked = MagicMock(auth=OrgLevelTokenRepositoryAuth(token)) + mocked_view = MagicMock() + mocked_view.get_repo = MagicMock(return_value=repo) + permission = CanDoCoverageUploadsPermission() + assert not permission.has_permission(request_mocked, mocked_view) + mocked_view.get_repo.assert_called_once() + + +def test_uploads_get_not_allowed(client, db, mocker): + mock_all_plans_and_tiers() + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + repository = RepositoryFactory( + name="the-repo", author__username="codecov", author__service="github" + ) + CommitFactory(repository=repository, commitid="commit-sha") + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.uploads", + args=["github", "codecov::::the-repo", "commit-sha", "report-code"], + ) + assert ( + url + == "/upload/github/codecov::::the-repo/commits/commit-sha/reports/report-code/uploads" + ) + res = client.get(url) + assert res.status_code == 405 + + +def test_get_repo(db): + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + repository.save() + upload_views = UploadViews() + upload_views.kwargs = dict(repo="codecov::::the_repo", service="github") + recovered_repo = upload_views.get_repo() + assert recovered_repo == repository + + +def test_get_repo_with_invalid_service(db): + upload_views = UploadViews() + upload_views.kwargs = dict(repo="repo", service="wrong service") + with pytest.raises(ValidationError) as exp: + upload_views.get_repo() + assert exp.match("Service not found: wrong service") + + +def test_get_repo_not_found(db): + upload_views = UploadViews() + upload_views.kwargs = dict(repo="repo", service="github") + with pytest.raises(ValidationError) as exp: + upload_views.get_repo() + assert exp.match("Repository not found") + + +def test_get_commit(db): + repository = RepositoryFactory(name="the_repo", author__username="codecov") + commit = CommitFactory(repository=repository) + repository.save() + commit.save() + upload_views = UploadViews() + upload_views.kwargs = dict(repo=repository.name, commit_sha=commit.commitid) + recovered_commit = upload_views.get_commit(repository) + assert recovered_commit == commit + + +def test_get_commit_error(db): + repository = RepositoryFactory(name="the_repo", author__username="codecov") + repository.save() + upload_views = UploadViews() + upload_views.kwargs = dict(repo=repository.name, commit_sha="missing_commit") + with pytest.raises(ValidationError) as exp: + upload_views.get_commit(repository) + assert exp.match("Commit SHA not found") + + +def test_get_report(db): + repository = RepositoryFactory(name="the_repo", author__username="codecov") + commit = CommitFactory(repository=repository) + report = CommitReport(commit=commit) + repository.save() + commit.save() + report.save() + upload_views = UploadViews() + upload_views.kwargs = dict( + repo=repository.name, commit_sha=commit.commitid, report_code=report.code + ) + recovered_report = upload_views.get_report(commit) + assert recovered_report == report + + +def test_get_default_report(db): + repository = RepositoryFactory(name="the_repo", author__username="codecov") + commit = CommitFactory(repository=repository) + report = CommitReport(commit=commit) + repository.save() + commit.save() + report.save() + upload_views = UploadViews() + upload_views.kwargs = dict( + repo=repository.name, commit_sha=commit.commitid, report_code="default" + ) + recovered_report = upload_views.get_report(commit) + assert recovered_report == report + + +def test_get_report_error(db): + repository = RepositoryFactory(name="the_repo", author__username="codecov") + commit = CommitFactory(repository=repository) + repository.save() + commit.save() + upload_views = UploadViews() + upload_views.kwargs = dict( + repo=repository.name, commit_sha=commit.commitid, report_code="random_code" + ) + with pytest.raises(ValidationError) as exp: + upload_views.get_report(commit) + assert exp.match("Report not found") + + +def test_uploads_post(db, mocker, mock_redis): + mock_all_plans_and_tiers() + # TODO remove the mock object and test the flow with the permissions + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + amplitude_mock = mocker.patch( + "shared.events.amplitude.AmplitudeEventPublisher.publish" + ) + presigned_put_mock = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="presigned put", + ) + upload_task_mock = mocker.patch( + "upload.views.uploads.trigger_upload_task", return_value=True + ) + + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + commit_report = CommitReport.objects.create(commit=commit, code="code") + repository.save() + commit_report.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.uploads", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + commit_report.code, + ], + ) + response = client.post( + url, + { + "state": "uploaded", + "flags": ["flag1", "flag2"], + "version": "version", + # this cannot be passed in by default + "storage_path": "this/path/should/be/ingored.txt", + }, + ) + response_json = response.json() + upload = ReportSession.objects.filter( + report_id=commit_report.id, + upload_extras={"format_version": "v1"}, + state="started", + ).first() + assert response.status_code == 201 + assert all( + ( + x in response_json.keys() + for x in ["external_id", "created_at", "raw_upload_location", "url"] + ) + ) + assert ( + response_json.get("url") + == f"{settings.CODECOV_DASHBOARD_URL}/{repository.author.service}/{repository.author.username}/{repository.name}/commit/{commit.commitid}" + ) + + amplitude_mock.assert_called_with( + "Upload Received", + { + "user_ownerid": commit.author.ownerid, + "ownerid": repository.author.ownerid, + "repoid": repository.repoid, + "commitid": commit.id, + "pullid": commit.pullid, + "upload_type": "Coverage report", + }, + ) + assert ReportSession.objects.filter( + report_id=commit_report.id, + upload_extras={"format_version": "v1"}, + state="started", + ).exists() + assert RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag1" + ).exists() + assert RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag2" + ).exists() + flag1 = RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag1" + ).first() + flag2 = RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag2" + ).first() + assert UploadFlagMembership.objects.filter( + report_session_id=upload.id, flag_id=flag1.id + ).exists() + assert UploadFlagMembership.objects.filter( + report_session_id=upload.id, flag_id=flag2.id + ).exists() + assert list(upload.flags.all()) == [flag1, flag2] + + archive_service = ArchiveService(repository) + assert upload.storage_path == MinioEndpoints.raw_with_upload_id.get_path( + version="v4", + date=upload.created_at.strftime("%Y-%m-%d"), + repo_hash=archive_service.storage_hash, + commit_sha=commit.commitid, + reportid=commit_report.external_id, + uploadid=upload.external_id, + ) + presigned_put_mock.assert_called_with("archive", upload.storage_path, 10) + upload_task_mock.assert_called() + + +@pytest.mark.parametrize("private", [False, True]) +@pytest.mark.parametrize("branch", ["branch", "fork:branch", "someone/fork:branch"]) +@pytest.mark.parametrize( + "branch_sent", [None, "branch", "fork:branch", "someone/fork:branch"] +) +def test_uploads_post_tokenless(db, mocker, mock_redis, private, branch, branch_sent): + presigned_put_mock = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="presigned put", + ) + upload_task_mock = mocker.patch( + "upload.views.uploads.trigger_upload_task", return_value=True + ) + analytics_service_mock = mocker.patch("upload.views.uploads.AnalyticsService") + + repository = RepositoryFactory( + name="the_repo", + author__username="codecov", + author__service="github", + private=private, + author__upload_token_required_for_public_repos=True, + ) + commit = CommitFactory(repository=repository) + commit.branch = branch + commit_report = CommitReport.objects.create(commit=commit, code="code") + repository.save() + commit_report.save() + commit.save() + + client = APIClient() + url = reverse( + "new_upload.uploads", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + commit_report.code, + ], + ) + if branch_sent is not None: + data = { + "state": "uploaded", + "flags": ["flag1", "flag2"], + "version": "version", + "branch": branch_sent, + } + else: + data = { + "state": "uploaded", + "flags": ["flag1", "flag2"], + "version": "version", + } + response = client.post( + url, + data, + ) + + if private is False and ":" in branch: + assert response.status_code == 201 + response_json = response.json() + upload = ReportSession.objects.filter( + report_id=commit_report.id, + upload_extras={"format_version": "v1"}, + state="started", + ).first() + assert all( + ( + x in response_json.keys() + for x in ["external_id", "created_at", "raw_upload_location", "url"] + ) + ) + assert ( + response_json.get("url") + == f"{settings.CODECOV_DASHBOARD_URL}/{repository.author.service}/{repository.author.username}/{repository.name}/commit/{commit.commitid}" + ) + + assert ReportSession.objects.filter( + report_id=commit_report.id, + upload_extras={"format_version": "v1"}, + state="started", + ).exists() + assert RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag1" + ).exists() + assert RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag2" + ).exists() + flag1 = RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag1" + ).first() + flag2 = RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag2" + ).first() + assert UploadFlagMembership.objects.filter( + report_session_id=upload.id, flag_id=flag1.id + ).exists() + assert UploadFlagMembership.objects.filter( + report_session_id=upload.id, flag_id=flag2.id + ).exists() + assert list(upload.flags.all()) == [flag1, flag2] + + archive_service = ArchiveService(repository) + assert upload.storage_path == MinioEndpoints.raw_with_upload_id.get_path( + version="v4", + date=upload.created_at.strftime("%Y-%m-%d"), + repo_hash=archive_service.storage_hash, + commit_sha=commit.commitid, + reportid=commit_report.external_id, + uploadid=upload.external_id, + ) + presigned_put_mock.assert_called_with("archive", upload.storage_path, 10) + upload_task_mock.assert_called() + analytics_service_mock.return_value.account_uploaded_coverage_report.assert_called_with( + commit.repository.author.ownerid, + { + "commit": commit.commitid, + "branch": commit.branch, + "pr": commit.pullid, + "repo": commit.repository.name, + "repository_name": commit.repository.name, + "repository_id": commit.repository.repoid, + "service": commit.repository.service, + "build": upload.build_code, + "build_url": upload.build_url, + "flags": "", + "owner": commit.repository.author.ownerid, + "token": "tokenless_upload", + "version": "version", + "uploader_type": "CLI", + }, + ) + else: + assert response.status_code == 401 + assert response.json().get("detail") == "Not valid tokenless upload" + + +@pytest.mark.parametrize("private", [False, True]) +@pytest.mark.parametrize("branch", ["branch", "fork:branch", "someone/fork:branch"]) +@pytest.mark.parametrize( + "branch_sent", [None, "branch", "fork:branch", "someone/fork:branch"] +) +@pytest.mark.parametrize("upload_token_required_for_public_repos", [True, False]) +def test_uploads_post_token_required_auth_check( + db, + mocker, + mock_redis, + private, + branch, + branch_sent, + upload_token_required_for_public_repos, +): + presigned_put_mock = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="presigned put", + ) + upload_task_mock = mocker.patch( + "upload.views.uploads.trigger_upload_task", return_value=True + ) + analytics_service_mock = mocker.patch("upload.views.uploads.AnalyticsService") + + repository = RepositoryFactory( + name="the_repo", + author__username="codecov", + author__service="github", + private=private, + author__upload_token_required_for_public_repos=upload_token_required_for_public_repos, + ) + commit = CommitFactory(repository=repository) + commit.branch = branch + commit_report = CommitReport.objects.create(commit=commit, code="code") + repository.save() + commit_report.save() + commit.save() + + client = APIClient() + url = reverse( + "new_upload.uploads", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + commit_report.code, + ], + ) + if branch_sent is not None: + data = { + "state": "uploaded", + "flags": ["flag1", "flag2"], + "version": "version", + "branch": branch_sent, + } + else: + data = { + "state": "uploaded", + "flags": ["flag1", "flag2"], + "version": "version", + } + response = client.post( + url, + data, + ) + + # when TokenlessAuthentication is removed, this test should use `if private == False and upload_token_required_for_public_repos == False:` + # but TokenlessAuthentication lets some additional uploads through. + authorized_by_tokenless_auth_class = ":" in branch + + if private == False and ( + upload_token_required_for_public_repos == False + or authorized_by_tokenless_auth_class + ): + assert response.status_code == 201 + response_json = response.json() + upload = ReportSession.objects.filter( + report_id=commit_report.id, + upload_extras={"format_version": "v1"}, + state="started", + ).first() + assert all( + ( + x in response_json.keys() + for x in ["external_id", "created_at", "raw_upload_location", "url"] + ) + ) + assert ( + response_json.get("url") + == f"{settings.CODECOV_DASHBOARD_URL}/{repository.author.service}/{repository.author.username}/{repository.name}/commit/{commit.commitid}" + ) + + assert ReportSession.objects.filter( + report_id=commit_report.id, + upload_extras={"format_version": "v1"}, + state="started", + ).exists() + assert RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag1" + ).exists() + assert RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag2" + ).exists() + flag1 = RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag1" + ).first() + flag2 = RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag2" + ).first() + assert UploadFlagMembership.objects.filter( + report_session_id=upload.id, flag_id=flag1.id + ).exists() + assert UploadFlagMembership.objects.filter( + report_session_id=upload.id, flag_id=flag2.id + ).exists() + assert list(upload.flags.all()) == [flag1, flag2] + + archive_service = ArchiveService(repository) + assert upload.storage_path == MinioEndpoints.raw_with_upload_id.get_path( + version="v4", + date=upload.created_at.strftime("%Y-%m-%d"), + repo_hash=archive_service.storage_hash, + commit_sha=commit.commitid, + reportid=commit_report.external_id, + uploadid=upload.external_id, + ) + presigned_put_mock.assert_called_with("archive", upload.storage_path, 10) + upload_task_mock.assert_called() + analytics_service_mock.return_value.account_uploaded_coverage_report.assert_called_with( + commit.repository.author.ownerid, + { + "commit": commit.commitid, + "branch": commit.branch, + "pr": commit.pullid, + "repo": commit.repository.name, + "repository_name": commit.repository.name, + "repository_id": commit.repository.repoid, + "service": commit.repository.service, + "build": upload.build_code, + "build_url": upload.build_url, + "flags": "", + "owner": commit.repository.author.ownerid, + "token": "tokenless_upload", + "version": "version", + "uploader_type": "CLI", + }, + ) + else: + assert response.status_code == 401 + assert response.json().get("detail") == "Not valid tokenless upload" + + +@patch("upload.views.uploads.AnalyticsService") +@patch("upload.helpers.jwt.decode") +@patch("upload.helpers.PyJWKClient") +def test_uploads_post_github_oidc_auth( + mock_jwks_client, + mock_jwt_decode, + analytics_service_mock, + db, + mocker, + mock_redis, +): + presigned_put_mock = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="presigned put", + ) + upload_task_mock = mocker.patch( + "upload.views.uploads.trigger_upload_task", return_value=True + ) + + repository = RepositoryFactory( + name="the_repo", + author__username="codecov", + author__service="github", + author__upload_token_required_for_public_repos=True, + private=False, + ) + mock_jwt_decode.return_value = { + "repository": f"url/{repository.name}", + "repository_owner": repository.author.username, + "iss": "https://token.actions.githubusercontent.com", + "audience": [settings.CODECOV_API_URL], + } + token = "ThisValueDoesNotMatterBecauseOf_mock_jwt_decode" + + commit = CommitFactory(repository=repository) + commit_report = CommitReport.objects.create(commit=commit, code="code") + + client = APIClient() + url = reverse( + "new_upload.uploads", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + commit_report.code, + ], + ) + response = client.post( + url, + { + "state": "uploaded", + "flags": ["flag1", "flag2"], + "version": "version", + }, + headers={"Authorization": f"token {token}"}, + ) + assert response.status_code == 201 + response_json = response.json() + upload = ReportSession.objects.filter( + report_id=commit_report.id, + upload_extras={"format_version": "v1"}, + state="started", + ).first() + assert all( + ( + x in response_json.keys() + for x in ["external_id", "created_at", "raw_upload_location", "url"] + ) + ) + assert ( + response_json.get("url") + == f"{settings.CODECOV_DASHBOARD_URL}/{repository.author.service}/{repository.author.username}/{repository.name}/commit/{commit.commitid}" + ) + + assert ReportSession.objects.filter( + report_id=commit_report.id, + upload_extras={"format_version": "v1"}, + state="started", + ).exists() + assert RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag1" + ).exists() + assert RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag2" + ).exists() + flag1 = RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag1" + ).first() + flag2 = RepositoryFlag.objects.filter( + repository_id=repository.repoid, flag_name="flag2" + ).first() + assert UploadFlagMembership.objects.filter( + report_session_id=upload.id, flag_id=flag1.id + ).exists() + assert UploadFlagMembership.objects.filter( + report_session_id=upload.id, flag_id=flag2.id + ).exists() + assert list(upload.flags.all()) == [flag1, flag2] + + archive_service = ArchiveService(repository) + assert upload.storage_path == MinioEndpoints.raw_with_upload_id.get_path( + version="v4", + date=upload.created_at.strftime("%Y-%m-%d"), + repo_hash=archive_service.storage_hash, + commit_sha=commit.commitid, + reportid=commit_report.external_id, + uploadid=upload.external_id, + ) + presigned_put_mock.assert_called_with("archive", upload.storage_path, 10) + upload_task_mock.assert_called() + analytics_service_mock.return_value.account_uploaded_coverage_report.assert_called_with( + commit.repository.author.ownerid, + { + "commit": commit.commitid, + "branch": commit.branch, + "pr": commit.pullid, + "repo": commit.repository.name, + "repository_name": commit.repository.name, + "repository_id": commit.repository.repoid, + "service": commit.repository.service, + "build": upload.build_code, + "build_url": upload.build_url, + "flags": "", + "owner": commit.repository.author.ownerid, + "token": "oidc_token_upload", + "version": "version", + "uploader_type": "CLI", + }, + ) + + +@override_settings(SHELTER_SHARED_SECRET="shelter-shared-secret") +def test_uploads_post_shelter(db, mocker, mock_redis): + mock_all_plans_and_tiers() + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + presigned_put_mock = mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="presigned put", + ) + mocker.patch("upload.views.uploads.trigger_upload_task", return_value=True) + mock_prometheus_metrics = mocker.patch("upload.metrics.API_UPLOAD_COUNTER.labels") + + repository = RepositoryFactory( + name="the_repo", author__username="codecov", author__service="github" + ) + commit = CommitFactory(repository=repository) + commit_report = CommitReport.objects.create(commit=commit, code="code") + repository.save() + commit_report.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.uploads", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + commit_report.code, + ], + ) + response = client.post( + url, + { + "state": "uploaded", + "flags": ["flag1", "flag2"], + "version": "version", + "storage_path": "shelter/test/path.txt", + }, + headers={ + "X-Shelter-Token": "shelter-shared-secret", + "User-Agent": "codecov-cli/0.4.7", + }, + ) + + mock_prometheus_metrics.assert_called_with( + **{ + "agent": "cli", + "version": "0.4.7", + "action": "coverage", + "endpoint": "create_upload", + "repo_visibility": "private", + "is_using_shelter": "yes", + "position": "end", + "upload_version": None, + }, + ) + + upload = ReportSession.objects.filter( + report_id=commit_report.id, + upload_extras={"format_version": "v1"}, + state="started", + ).first() + assert response.status_code == 201 + ArchiveService(repository) + assert upload.storage_path == "shelter/test/path.txt" + presigned_put_mock.assert_called_with("archive", upload.storage_path, 10) + + +def test_deactivated_repo(db, mocker, mock_redis): + mock_all_plans_and_tiers() + mocker.patch.object( + CanDoCoverageUploadsPermission, "has_permission", return_value=True + ) + repository = RepositoryFactory( + name="the_repo", + author__username="codecov", + author__service="github", + active=True, + activated=False, + ) + commit = CommitFactory(repository=repository) + commit_report = CommitReport.objects.create(commit=commit, code="code") + repository.save() + commit_report.save() + + owner = repository.author + client = APIClient() + client.force_authenticate(user=owner) + url = reverse( + "new_upload.uploads", + args=[ + "github", + "codecov::::the_repo", + commit.commitid, + commit_report.code, + ], + ) + response = client.post( + url, + {"state": "uploaded"}, + format="json", + ) + response_json = response.json() + assert response.status_code == 400 + assert response_json == [ + f"This repository is deactivated. To resume uploading to it, please activate the repository in the codecov UI: {settings.CODECOV_DASHBOARD_URL}/github/codecov/the_repo/config/general" + ] + + +def test_trigger_upload_task(db, mocker): + repo = RepositoryFactory.create() + upload = UploadFactory.create() + report = CommitReportFactory.create() + commitid = "commit id" + mocked_redis = mocker.patch("upload.views.uploads.get_redis_connection") + mocked_dispatched_task = mocker.patch("upload.views.uploads.dispatch_upload_task") + trigger_upload_task(repo, commitid, upload, report) + mocked_redis.assert_called() + mocked_dispatched_task.assert_called() + + +def test_activate_repo(db): + repo = RepositoryFactory( + active=False, deleted=True, activated=False, coverage_enabled=False + ) + activate_repo(repo) + assert repo.active + assert repo.activated + assert not repo.deleted + assert repo.coverage_enabled + + +def test_activate_already_activated_repo(db): + repo = RepositoryFactory( + active=True, activated=True, deleted=False, coverage_enabled=True + ) + activate_repo(repo) + assert repo.active + + +class TestGitlabEnterpriseOIDC(APITestCase): + @pytest.fixture(scope="function", autouse=True) + def inject_mocker(request, mocker): + request.mocker = mocker + + @pytest.fixture(autouse=True) + def mock_config(self, mocker): + mock_config_helper( + mocker, configs={"github_enterprise.url": "https://example.com/"} + ) + + @patch("upload.views.uploads.AnalyticsService") + @patch("upload.helpers.jwt.decode") + @patch("upload.helpers.PyJWKClient") + def test_uploads_post_github_enterprise_oidc_auth_jwks_url( + self, + mock_jwks_client, + mock_jwt_decode, + analytics_service_mock, + ): + self.mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="presigned put", + ) + self.mocker.patch("upload.views.uploads.trigger_upload_task", return_value=True) + + repository = RepositoryFactory( + name="the_repo", + author__username="codecov", + author__service="github_enterprise", + author__upload_token_required_for_public_repos=True, + private=False, + ) + mock_jwt_decode.return_value = { + "repository": f"url/{repository.name}", + "repository_owner": repository.author.username, + "iss": "https://enterprise-client.actions.githubusercontent.com", + "audience": [settings.CODECOV_API_URL], + } + token = "ThisValueDoesNotMatterBecauseOf_mock_jwt_decode" + + commit = CommitFactory(repository=repository) + commit_report = CommitReport.objects.create(commit=commit, code="code") + + client = APIClient() + url = reverse( + "new_upload.uploads", + args=[ + "github_enterprise", + "codecov::::the_repo", + commit.commitid, + commit_report.code, + ], + ) + response = client.post( + url, + { + "state": "uploaded", + "flags": ["flag1", "flag2"], + "version": "version", + }, + headers={"Authorization": f"token {token}"}, + ) + assert response.status_code == 201 + mock_jwks_client.assert_called_with( + "https://example.com/_services/token/.well-known/jwks" + ) + + @patch("upload.views.uploads.AnalyticsService") + @patch("upload.helpers.jwt.decode") + @patch("upload.helpers.PyJWKClient") + def test_uploads_post_github_enterprise_oidc_auth_no_url( + self, + mock_jwks_client, + mock_jwt_decode, + analytics_service_mock, + ): + mock_config_helper(self.mocker, configs={"github_enterprise.url": None}) + self.mocker.patch( + "shared.storage.MinioStorageService.create_presigned_put", + return_value="presigned put", + ) + self.mocker.patch("upload.views.uploads.trigger_upload_task", return_value=True) + + repository = RepositoryFactory( + name="the_repo", + author__username="codecov", + author__service="github_enterprise", + author__upload_token_required_for_public_repos=True, + private=False, + ) + mock_jwt_decode.return_value = { + "repository": f"url/{repository.name}", + "repository_owner": repository.author.username, + "iss": "https://enterprise-client.actions.githubusercontent.com", + "audience": [settings.CODECOV_API_URL], + } + token = "ThisValueDoesNotMatterBecauseOf_mock_jwt_decode" + + commit = CommitFactory(repository=repository) + commit_report = CommitReport.objects.create(commit=commit, code="code") + + client = APIClient() + url = reverse( + "new_upload.uploads", + args=[ + "github_enterprise", + "codecov::::the_repo", + commit.commitid, + commit_report.code, + ], + ) + response = client.post( + url, + { + "state": "uploaded", + "flags": ["flag1", "flag2"], + "version": "version", + }, + headers={"Authorization": f"token {token}"}, + ) + assert response.status_code == 401 + assert response.json().get("detail") == "Not valid tokenless upload" diff --git a/apps/codecov-api/upload/throttles.py b/apps/codecov-api/upload/throttles.py new file mode 100644 index 0000000000..42bc19ab80 --- /dev/null +++ b/apps/codecov-api/upload/throttles.py @@ -0,0 +1,78 @@ +import logging + +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from django.db.models import Q +from django.http import HttpRequest +from rest_framework.exceptions import ValidationError +from rest_framework.throttling import BaseThrottle +from rest_framework.views import APIView +from shared.helpers.redis import get_redis_connection +from shared.plan.service import PlanService +from shared.reports.enums import UploadType +from shared.upload.utils import query_monthly_coverage_measurements + +from reports.models import ReportSession +from upload.helpers import _determine_responsible_owner + +log = logging.getLogger(__name__) + +redis = get_redis_connection() + + +class UploadsPerCommitThrottle(BaseThrottle): + def allow_request(self, request: HttpRequest, view: APIView) -> bool: + try: + repository = view.get_repo() + commit = view.get_commit(repository) + new_session_count = ReportSession.objects.filter( + ~Q(state="error"), + ~Q(upload_type=UploadType.CARRIEDFORWARD.db_name), + report__commit=commit, + ).count() + max_upload_limit = repository.author.max_upload_limit or 150 + if new_session_count > max_upload_limit: + log.warning( + "Too many uploads to this commit", + extra=dict( + commit=commit.commitid, + repoid=repository.repoid, + ), + ) + return False + return True + except (ObjectDoesNotExist, ValidationError): + return True + + +class UploadsPerWindowThrottle(BaseThrottle): + def allow_request(self, request: HttpRequest, view: APIView) -> bool: + try: + repository = view.get_repo() + commit = view.get_commit(repository) + + if settings.UPLOAD_THROTTLING_ENABLED and repository.private: + owner = _determine_responsible_owner(repository) + plan_service = PlanService(current_org=owner) + limit = plan_service.monthly_uploads_limit + if limit is not None: + did_commit_uploads_start_already = ReportSession.objects.filter( + report__commit=commit + ).exists() + if not did_commit_uploads_start_already: + if ( + query_monthly_coverage_measurements( + plan_service=plan_service + ) + >= limit + ): + log.warning( + "User exceeded its limits for usage", + extra=dict( + ownerid=owner.ownerid, repoid=commit.repository_id + ), + ) + return False + return True + except (ObjectDoesNotExist, ValidationError): + return True diff --git a/apps/codecov-api/upload/tokenless/__init__.py b/apps/codecov-api/upload/tokenless/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/upload/tokenless/appveyor.py b/apps/codecov-api/upload/tokenless/appveyor.py new file mode 100644 index 0000000000..9b7d56855f --- /dev/null +++ b/apps/codecov-api/upload/tokenless/appveyor.py @@ -0,0 +1,75 @@ +import logging +from typing import Any, Dict + +import requests +from requests.exceptions import ConnectionError, HTTPError +from rest_framework.exceptions import NotFound + +from upload.tokenless.base import BaseTokenlessUploadHandler + +log = logging.getLogger(__name__) + + +class TokenlessAppveyorHandler(BaseTokenlessUploadHandler): + def get_build(self) -> Dict[str, Any]: + try: + build = requests.get( + "https://ci.appveyor.com/api/projects/{}/{}/build/{}".format( + *self.job.split("/", 2) + ), + headers={"Accept": "application/json", "User-Agent": "Codecov"}, + ) + except (ConnectionError, HTTPError) as e: + log.warning( + f"HTTP error {e}", + extra=dict( + commit=self.upload_params.get("commit"), + repo_name=self.upload_params.get("repo"), + job=self.upload_params.get("job"), + owner=self.upload_params.get("owner"), + ), + ) + raise NotFound( + "Unable to locate build via Appveyor API. Please upload with the Codecov repository upload token to resolve issue." + ) + + if not build: + raise NotFound( + "Unable to locate build via Appveyor API. Please upload with the Codecov repository upload token to resolve issue." + ) + + return build.json() + + def verify(self) -> str: + if not self.upload_params.get("job"): + raise NotFound( + 'Missing "job" argument. Please upload with the Codecov repository upload token to resolve issue.' + ) + + self.job = ( + self.upload_params.get("job") + if "/" in self.upload_params.get("job") + else ( + f"{self.upload_params.get('owner')}/{self.upload_params.get('repo')}/{self.upload_params.get('job')}" + ) + ) + + self.job = self.job.replace("+", "%20").replace(" ", "%20") + + build = self.get_build() + + # validate build + if not any( + filter( + lambda j: j["jobId"] == self.upload_params.get("build", "") # type: ignore + and j.get("finished") is None, + build["build"]["jobs"], + ) + ): + raise NotFound( + "Build already finished, unable to accept new reports. Please upload with the Codecov repository upload token to resolve issue." + ) + + service = self.check_repository_type(build["project"]["repositoryType"]) + + return service diff --git a/apps/codecov-api/upload/tokenless/azure.py b/apps/codecov-api/upload/tokenless/azure.py new file mode 100644 index 0000000000..4e871b505b --- /dev/null +++ b/apps/codecov-api/upload/tokenless/azure.py @@ -0,0 +1,141 @@ +import logging +from datetime import datetime, timedelta +from typing import Any, Dict + +import requests +from requests.exceptions import ConnectionError, HTTPError +from rest_framework.exceptions import NotFound +from simplejson import JSONDecodeError + +from upload.tokenless.base import BaseTokenlessUploadHandler + +log = logging.getLogger(__name__) + + +class TokenlessAzureHandler(BaseTokenlessUploadHandler): + def get_build(self) -> Dict[str, Any]: + try: + response = requests.get( + f"{self.server_uri}{self.project}/_apis/build/builds/{self.job}?api-version=5.0", + headers={"Accept": "application/json", "User-Agent": "Codecov"}, + ) + except (ConnectionError, HTTPError) as e: + log.warning( + f"Request error {e}", + extra=dict( + commit=self.upload_params.get("commit"), + repo_name=self.upload_params.get("repo"), + job=self.upload_params.get("job"), + owner=self.upload_params.get("owner"), + ), + ) + raise NotFound( + "Unable to locate build via Azure API. Please upload with the Codecov repository upload token to resolve issue." + ) + + if not response: + raise NotFound( + "Unable to locate build via Azure API. Please upload with the Codecov repository upload token to resolve issue." + ) + try: + build = response.json() + except JSONDecodeError as e: + log.warning( + f"Expected JSON in Azure response, got error {e} instead", + extra=dict( + commit=self.upload_params.get("commit"), + repo_name=self.upload_params.get("repo"), + job=self.upload_params.get("job"), + owner=self.upload_params.get("owner"), + response=response, + ), + ) + raise NotFound( + "Unable to locate build via Azure API. Project is likely private, please upload with the Codecov repository upload token to resolve issue." + ) + return build + + def verify(self) -> None: + if not self.upload_params.get("job"): + raise NotFound( + 'Missing "job" argument. Please upload with the Codecov repository upload token to resolve issue.' + ) + self.job = self.upload_params.get("job") + + if not self.upload_params.get("project"): + raise NotFound( + 'Missing "project" argument. Please upload with the Codecov repository upload token to resolve issue.' + ) + self.project = self.upload_params.get("project") + + if not self.upload_params.get("server_uri"): + raise NotFound( + 'Missing "server_uri" argument. Please upload with the Codecov repository upload token to resolve issue.' + ) + self.server_uri = self.upload_params.get("server_uri") + + build = self.get_build() + + # Build should have finished within the last 4 mins OR should have an 'inProgress' flag + if build["status"] == "completed": + finishTimestamp = build["finishTime"].replace("T", " ").replace("Z", "") + # Azure DevOps API returns nanosecond precision (7 digits), but Python only supports + # microsecond precision (6 digits). Truncate to 6 digits after decimal. + if "." in finishTimestamp: + base, fraction = finishTimestamp.rsplit(".", 1) + finishTimestamp = f"{base}.{fraction[:6]}" + buildFinishDateObj = datetime.strptime( + finishTimestamp, "%Y-%m-%d %H:%M:%S.%f" + ) + finishTimeWithBuffer = buildFinishDateObj + timedelta(minutes=4) + now = datetime.now() + if not now <= finishTimeWithBuffer: + raise NotFound( + "Azure build has already finished. Please upload with the Codecov repository upload token to resolve issue." + ) + else: + if build["status"].lower() != "inprogress": + raise NotFound( + "Azure build has already finished. Please upload with the Codecov repository upload token to resolve issue." + ) + + # Check build ID + build["buildNumber"] = build["buildNumber"].replace("+", " ") + self.upload_params["build"] = self.upload_params.get("build").replace("+", " ") + if build["buildNumber"] != self.upload_params.get("build"): + log.warning( + f"Azure build numbers do not match. Upload build number: {self.upload_params.get('build')}, Azure build number: {self.upload_params.get('buildNumber')}", + extra=dict( + commit=self.upload_params.get("commit"), + repo_name=self.upload_params.get("repo"), + job=self.upload_params.get("job"), + owner=self.upload_params.get("owner"), + ), + ) + raise NotFound( + "Build numbers do not match. Please upload with the Codecov repository upload token to resolve issue." + ) + + # Make sure commit sha matches + if build["sourceVersion"] != self.upload_params.get("commit") and ( + build.get("triggerInfo", {}).get("pr.sourceSha") + != self.upload_params.get("commit") + ): + log.warning( + "Commit sha does not match Azure build", + extra=dict( + commit=self.upload_params.get("commit"), + repo_name=self.upload_params.get("repo"), + job=self.upload_params.get("job"), + owner=self.upload_params.get("owner"), + ), + ) + raise NotFound( + "Commit sha does not match Azure build. Please upload with the Codecov repository upload token to resolve issue." + ) + + # Azure supports various repo types, ensure current repo type is supported on Codecov + service = self.check_repository_type(build["repository"]["type"]) + + # Validation step is complete, return repo type + return service diff --git a/apps/codecov-api/upload/tokenless/base.py b/apps/codecov-api/upload/tokenless/base.py new file mode 100644 index 0000000000..cf93e4033b --- /dev/null +++ b/apps/codecov-api/upload/tokenless/base.py @@ -0,0 +1,19 @@ +from rest_framework.exceptions import NotFound + + +class BaseTokenlessUploadHandler(object): + def __init__(self, upload_params): + self.upload_params = upload_params + + def check_repository_type(self, repository_type): + if repository_type.lower() not in ("github", "gitlab", "bitbucket"): + raise NotFound( + "Sorry this service is not supported. Codecov currently only works with GitHub, GitLab, and BitBucket repositories" + ) + return repository_type.lower() + + def get_build(self): + raise NotImplementedError() + + def verify(self): + raise NotImplementedError() diff --git a/apps/codecov-api/upload/tokenless/circleci.py b/apps/codecov-api/upload/tokenless/circleci.py new file mode 100644 index 0000000000..1aea3cb27e --- /dev/null +++ b/apps/codecov-api/upload/tokenless/circleci.py @@ -0,0 +1,77 @@ +import logging + +import requests +from django.conf import settings +from requests.exceptions import ConnectionError, HTTPError +from rest_framework.exceptions import NotFound + +from upload.tokenless.base import BaseTokenlessUploadHandler + +log = logging.getLogger(__name__) + + +class TokenlessCircleciHandler(BaseTokenlessUploadHandler): + circleci_token = settings.CIRCLECI_TOKEN + + def get_build(self): + build_num = self.build.split(".")[0] + try: + build = requests.get( + f"https://circleci.com/api/v1/project/{self.owner}/{self.repo}/{build_num}?circle-token={self.circleci_token}", + headers={"Accept": "application/json", "User-Agent": "Codecov"}, + ) + return build.json() + except (ConnectionError, HTTPError) as e: + log.warning( + f"Request error {e}", + extra=dict( + commit=self.upload_params.get("commit"), + repo_name=self.upload_params.get("repo"), + job=self.upload_params.get("job"), + owner=self.upload_params.get("owner"), + ), + ) + raise NotFound( + "Unable to locate build via CircleCI API. Please upload with the Codecov repository upload token to resolve issue." + ) + + def verify(self): + if not self.upload_params.get("build"): + raise NotFound( + 'Missing "build" argument. Please upload with the Codecov repository upload token to resolve issue.' + ) + self.build = self.upload_params.get("build") + + if not self.upload_params.get("owner"): + raise NotFound( + 'Missing "owner" argument. Please upload with the Codecov repository upload token to resolve issue.' + ) + self.owner = self.upload_params.get("owner") + + if not self.upload_params.get("repo"): + raise NotFound( + 'Missing "repo" argument. Please upload with the Codecov repository upload token to resolve issue.' + ) + self.repo = self.upload_params.get("repo") + + build = self.get_build() + + if build.get("vcs_revision", "") != self.upload_params.get("commit"): + log.warning( + "Failed to fetch commit from CircleCI", + extra=dict( + commit=self.upload_params.get("commit"), + vcs_revision=build.get("vcs_revision", ""), + repo_name=self.upload_params.get("repo"), + build_num=self.build.split(".")[0], + owner=self.upload_params.get("owner"), + ), + ) + raise NotFound( + "Commit sha does not match Circle build. Please upload with the Codecov repository upload token to resolve issue." + ) + + if build.get("stop_time") is not None: + raise NotFound("Build has already finished, uploads rejected.") + + return build["vcs_type"] diff --git a/apps/codecov-api/upload/tokenless/cirrus.py b/apps/codecov-api/upload/tokenless/cirrus.py new file mode 100644 index 0000000000..9dc5350225 --- /dev/null +++ b/apps/codecov-api/upload/tokenless/cirrus.py @@ -0,0 +1,163 @@ +import logging +import time +from typing import Any, Dict + +import requests +from requests.exceptions import ConnectionError, HTTPError +from rest_framework.exceptions import NotFound + +from upload.tokenless.base import BaseTokenlessUploadHandler + +log = logging.getLogger(__name__) + + +class TokenlessCirrusHandler(BaseTokenlessUploadHandler): + def get_build(self) -> Dict[str, Any]: + query = f"""{{ + "query": "query ($buildId: ID!) {{ + build(id: $buildId) {{ + buildCreatedTimestamp, + changeIdInRepo, + durationInSeconds, + repository {{ + name, + owner + }}, + status + }} + }}", + "variables": {{ + "buildId": {self.upload_params.get("build")} + }} + }}""" + + try: + response = requests.post( + "https://api.cirrus-ci.com/graphql", + data=query, + headers={"Content-Type": "application/json", "User-Agent": "Codecov"}, + ) + except (ConnectionError, HTTPError) as e: + log.warning( + f"Request error {e}", + extra=dict( + build=self.upload_params["build"], + commit=self.upload_params["commit"], + job=self.upload_params["job"], + owner=self.upload_params["owner"], + repo_name=self.upload_params["repo"], + ), + ) + raise NotFound( + "Unable to locate build via Cirrus CI API. Please upload with the Codecov repository upload token to resolve this issue." + ) + + build = response.json() + log.info( + "Cirrus CI build response found.", + extra=dict(build=build, upload_params=self.upload_params), + ) + if "errors" in build or build.get("data") is None: + log.warning( + "Build Error", + extra=dict( + build=self.upload_params["build"], + commit=self.upload_params["commit"], + error=build["errors"], + job=self.upload_params["job"], + owner=self.upload_params["owner"], + repo_name=self.upload_params["repo"], + ), + ) + raise NotFound( + "Could not retrieve build via Cirrus CI API. Please upload with the Codecov repository upload token to resolve this issue." + ) + + return build + + def verify(self) -> str: + if not self.upload_params.get("owner"): + raise NotFound( + 'Missing "owner" argument. Please upload with the Codecov repository upload token to resolve this issue.' + ) + owner = self.upload_params.get("owner") + + if not self.upload_params.get("repo"): + raise NotFound( + 'Missing "repo" argument. Please upload with the Codecov repository upload token to resolve this issue.' + ) + repo = self.upload_params.get("repo") + + if not self.upload_params.get("commit"): + raise NotFound( + 'Missing "commit" argument. Please upload with the Codecov repository upload token to resolve this issue.' + ) + commit = self.upload_params.get("commit") + + raw_build = self.get_build() + build = raw_build["data"]["build"] + + # Check repository + if build["repository"]["owner"] != owner or build["repository"]["name"] != repo: + log.warning( + "Repository slug does not match Cirrus arguments", + extra=dict( + build_info=build, + commit=commit, + job=self.upload_params.get("job"), + owner=owner, + repo_name=repo, + ), + ) + raise NotFound( + "Repository slug does not match Cirrus CI build. Please upload with the Codecov repository upload token to resolve this issue." + ) + + # Check commit SHA + if build["changeIdInRepo"] != commit: + log.warning( + "Commit sha does not match Github actions arguments", + extra=dict( + build_info=build, + commit=commit, + job=self.upload_params.get("job"), + owner=owner, + repo_name=repo, + ), + ) + raise NotFound( + "Commit sha does not match Cirrus CI build. Please upload with the Codecov repository upload token to resolve issue." + ) + + # Check if current status is correct + if build.get("status") != "EXECUTING": + finishTimestamp = ( + build.get("buildCreatedTimestamp") + + build.get("durationInSeconds") + + (4 * 60) # Add 4 minutes buffer + ) + now = time.time() + if now > finishTimestamp: + log.warning( + "Cirrus run is stale", + extra=dict( + build_info=build, + commit=commit, + job=self.upload_params.get("job"), + owner=owner, + repo_name=repo, + ), + ) + log.warning( + "Cirrus run is stale", + extra=dict( + build_info=build, + commit=commit, + job=self.upload_params.get("job"), + owner=owner, + repo_name=repo, + ), + ) + raise NotFound("Cirrus run is stale") + + return "github" diff --git a/apps/codecov-api/upload/tokenless/github_actions.py b/apps/codecov-api/upload/tokenless/github_actions.py new file mode 100644 index 0000000000..b508351544 --- /dev/null +++ b/apps/codecov-api/upload/tokenless/github_actions.py @@ -0,0 +1,137 @@ +import logging +from datetime import datetime, timedelta +from typing import Any, Dict + +from asgiref.sync import async_to_sync +from django.conf import settings +from rest_framework import exceptions +from rest_framework.exceptions import NotFound +from shared.torngit import get +from shared.torngit.exceptions import TorngitClientError, TorngitRateLimitError + +from upload.tokenless.base import BaseTokenlessUploadHandler + +log = logging.getLogger(__name__) + + +class TokenlessGithubActionsHandler(BaseTokenlessUploadHandler): + actions_token = settings.GITHUB_ACTIONS_TOKEN + client_id = settings.GITHUB_CLIENT_ID + client_secret = settings.GITHUB_CLIENT_SECRET + + def log_warning(self, message: str) -> None: + log.warning( + message, + extra=dict( + commit=self.upload_params.get("commit"), + repo_name=self.upload_params.get("repo"), + job=self.upload_params.get("job"), + owner=self.upload_params.get("owner"), + ), + ) + + def get_build(self) -> Dict[str, Any]: + git = get( + "github", + token=dict(key=self.actions_token), + repo=dict(name=self.upload_params.get("repo")), + owner=dict(username=self.upload_params.get("owner")), + oauth_consumer_token=dict(key=self.client_id, secret=self.client_secret), + ) + + try: + actions_response = async_to_sync(git.get_workflow_run)( + self.upload_params.get("build") + ) + except TorngitRateLimitError as e: + self.log_warning(message=f"Rate limit error {e}") + if e.reset: + now_timestamp = datetime.now().timestamp() + retry_after = int(e.reset) - int(now_timestamp) + elif e.retry_after: + retry_after = int(e.retry_after) + else: + retry_after = None + time_to_available_str = "" + if retry_after is not None: + time_to_available_str = ( + f" Expected time to availability: {retry_after}s." + ) + raise exceptions.Throttled( + wait=None, + detail=f"Rate limit reached. Please upload with the Codecov repository upload token to resolve issue.{time_to_available_str}", + ) + except TorngitClientError as e: + self.log_warning(message=f"Request client error {e}") + raise NotFound( + "Unable to locate build via Github Actions API. Please upload with the Codecov repository upload token to resolve issue." + ) + except Exception as e: + self.log_warning(message=f"Request error {e}") + raise NotFound( + "Unable to locate build via Github Actions API. Please upload with the Codecov repository upload token to resolve issue." + ) + + return actions_response + + def verify(self) -> str: + if not self.upload_params.get("owner"): + raise NotFound( + 'Missing "owner" argument. Please upload with the Codecov repository upload token to resolve issue.' + ) + owner = self.upload_params.get("owner") + + if not self.upload_params.get("repo"): + raise NotFound( + 'Missing "repo" argument. Please upload with the Codecov repository upload token to resolve issue.' + ) + repo = self.upload_params.get("repo") + + build = self.get_build() + + if ( + build["public"] != True + or build["slug"] != f"{owner}/{repo}" + or ( + build["commit_sha"] != self.upload_params.get("commit") + and self.upload_params.get("pr") is None + ) + ): + self.log_warning( + message="Repository slug or commit sha do not match Github actions arguments" + ) + raise NotFound( + "Repository slug or commit sha do not match Github actions build. Please upload with the Codecov repository upload token to resolve issue." + ) + + # Check if current status is correct (not stale or in progress) + if build.get("status") not in ["in_progress", "queued"]: + # Verify workflow finished within the last 4 minutes because it's not in-progress + try: + build_finish_date_obj = datetime.strptime( + build["finish_time"], "%Y-%m-%dT%H:%M:%SZ" + ) + except ValueError: + build_finish_date_obj = datetime.strptime( + build["finish_time"], "%Y-%m-%d %H:%M:%S" + ) + + finish_time_with_buffer = build_finish_date_obj + timedelta(minutes=10) + now = datetime.now() + if not now <= finish_time_with_buffer: + log.warning( + "Actions workflow run is stale", + extra=dict( + build=build, + commit=self.upload_params.get("commit"), + finish_time_with_buffer=finish_time_with_buffer, + job=self.upload_params.get("job"), + now=now, + owner=self.upload_params.get("owner"), + repo_name=self.upload_params.get("repo"), + time_diff=now - finish_time_with_buffer, + ), + ) + raise NotFound("Actions workflow run is stale") + + return "github" diff --git a/apps/codecov-api/upload/tokenless/tokenless.py b/apps/codecov-api/upload/tokenless/tokenless.py new file mode 100644 index 0000000000..54861e4048 --- /dev/null +++ b/apps/codecov-api/upload/tokenless/tokenless.py @@ -0,0 +1,45 @@ +import logging + +from rest_framework.exceptions import NotFound + +from upload.tokenless.appveyor import TokenlessAppveyorHandler +from upload.tokenless.azure import TokenlessAzureHandler +from upload.tokenless.circleci import TokenlessCircleciHandler +from upload.tokenless.cirrus import TokenlessCirrusHandler +from upload.tokenless.github_actions import TokenlessGithubActionsHandler +from upload.tokenless.travis import TokenlessTravisHandler + +log = logging.getLogger(__name__) + + +class TokenlessUploadHandler(object): + ci_verifiers = { + "appveyor": TokenlessAppveyorHandler, + "azure_pipelines": TokenlessAzureHandler, + "circleci": TokenlessCircleciHandler, + "cirrus_ci": TokenlessCirrusHandler, + "github_actions": TokenlessGithubActionsHandler, + "travis": TokenlessTravisHandler, + } + + def __init__(self, ci_type, upload_params): + self.verifier = self.ci_verifiers.get(ci_type.replace("-", "_"), None) + self.upload_params = upload_params + self.ci_type = ci_type + + def verify_upload(self): + log.info( + f"Started {self.ci_type} tokenless upload", + extra=dict( + commit=self.upload_params.get("commit"), + repo_name=self.upload_params.get("repo"), + job=self.upload_params.get("job"), + owner=self.upload_params.get("owner"), + ), + ) + try: + return self.verifier(self.upload_params).verify() + except TypeError: + raise NotFound( + "Your CI provider is not compatible with tokenless uploads, please upload using your repository token to resolve this." + ) diff --git a/apps/codecov-api/upload/tokenless/travis.py b/apps/codecov-api/upload/tokenless/travis.py new file mode 100644 index 0000000000..0054f89a59 --- /dev/null +++ b/apps/codecov-api/upload/tokenless/travis.py @@ -0,0 +1,160 @@ +import logging +from datetime import datetime, timedelta +from typing import Any, Dict + +import requests +from requests.exceptions import ConnectionError, HTTPError +from rest_framework.exceptions import NotFound + +from upload.constants import errors +from upload.tokenless.base import BaseTokenlessUploadHandler + +log = logging.getLogger(__name__) + + +class TokenlessTravisHandler(BaseTokenlessUploadHandler): + def get_build(self) -> Dict[str, Any]: + travis_dot_com = False + + try: + build = requests.get( + "https://api.travis-ci.com/job/{}".format(self.upload_params["job"]), + headers={"Travis-API-Version": "3", "User-Agent": "Codecov"}, + ) + travis_dot_com = ( + build.json()["repository"]["slug"] + == f"{self.upload_params['owner']}/{self.upload_params['repo']}" + ) + except (ConnectionError, HTTPError) as e: + log.warning( + f"Request error {e}", + extra=dict( + commit=self.upload_params["commit"], + repo_name=self.upload_params["repo"], + job=self.upload_params["job"], + owner=self.upload_params["owner"], + ), + ) + pass + except Exception as e: + log.warning( + f"Error {e}", + extra=dict( + commit=self.upload_params["commit"], + repo_name=self.upload_params["repo"], + job=self.upload_params["job"], + owner=self.upload_params["owner"], + ), + ) + + # if job not found in travis.com try travis.org + if not travis_dot_com: + log.info( + "Unable to verify using travis.com, trying travis.org", + extra=dict( + commit=self.upload_params["commit"], + repo_name=self.upload_params["repo"], + job=self.upload_params["job"], + owner=self.upload_params["owner"], + ), + ) + try: + build = requests.get( + "https://api.travis-ci.org/job/{}".format( + self.upload_params["job"] + ), + headers={"Travis-API-Version": "3", "User-Agent": "Codecov"}, + ) + except (ConnectionError, HTTPError) as e: + log.warning( + f"Request error {e}", + extra=dict( + commit=self.upload_params["commit"], + repo_name=self.upload_params["repo"], + job=self.upload_params["job"], + owner=self.upload_params["owner"], + ), + ) + raise NotFound( + errors["travis"]["tokenless-general-error"].format( + f"https://codecov.io/gh/{self.upload_params['owner']}/{self.upload_params['repo']}/settings" + ) + ) + if not build: + raise NotFound( + errors["travis"]["tokenless-general-error"].format( + f"https://codecov.io/gh/{self.upload_params['owner']}/{self.upload_params['repo']}/settings" + ) + ) + + return build.json() + + def verify(self) -> str: + # find repo in travis.com + job = self.get_build() + + slug = f"{self.upload_params['owner']}/{self.upload_params['repo']}" + + codecovUrl = f"https://codecov.io/gh/{self.upload_params['owner']}/{self.upload_params['repo']}/settings" + + # Check repo slug and commit sha + # We check commit sha only for a push event since sha in arguments will not match if event type = pull request + if ( + job["repository"]["slug"] != slug + or job["commit"]["sha"] != self.upload_params["commit"] + and job["build"]["event_type"] != "pull_request" + ): + log.warning( + f"Repository slug: {slug} or commit sha: {self.upload_params['commit']} do not match travis arguments", + extra=dict( + commit=self.upload_params["commit"], + repo_name=self.upload_params["repo"], + job=self.upload_params["job"], + owner=self.upload_params["owner"], + ), + ) + raise NotFound( + errors["travis"]["tokenless-general-error"].format(codecovUrl) + ) + + # Verify job finished within the last 4 minutes or is still in progress + if job["finished_at"] is not None: + finishTimestamp = job["finished_at"].replace("T", " ").replace("Z", "") + buildFinishDateObj = datetime.strptime(finishTimestamp, "%Y-%m-%d %H:%M:%S") + finishTimeWithBuffer = buildFinishDateObj + timedelta(minutes=4) + now = datetime.now() + if not now <= finishTimeWithBuffer: + log.warning( + "Cancelling upload: 4 mins since build", + extra=dict( + commit=self.upload_params["commit"], + repo_name=self.upload_params["repo"], + job=self.upload_params["job"], + owner=self.upload_params["owner"], + ), + ) + raise NotFound(errors["travis"]["tokenless-stale-build"]) + else: + # check if current state is correct (i.e not finished) + if job["state"] != "started": + log.warning( + "Cancelling upload: job state does not indicate that build is in progress", + extra=dict( + commit=self.upload_params["commit"], + repo_name=self.upload_params["repo"], + job=self.upload_params["job"], + owner=self.upload_params["owner"], + ), + ) + raise NotFound(errors["travis"]["tokenless-bad-status"]) + + log.info( + "Finished travis tokenless upload", + extra=dict( + commit=self.upload_params["commit"], + repo_name=self.upload_params["repo"], + job=self.upload_params["job"], + owner=self.upload_params["owner"], + ), + ) + return "github" diff --git a/apps/codecov-api/upload/urls.py b/apps/codecov-api/upload/urls.py new file mode 100644 index 0000000000..5194d09c3c --- /dev/null +++ b/apps/codecov-api/upload/urls.py @@ -0,0 +1,68 @@ +from django.urls import path, re_path + +from upload.views.bundle_analysis import BundleAnalysisView +from upload.views.commits import CommitViews +from upload.views.empty_upload import EmptyUploadView +from upload.views.legacy import UploadDownloadHandler, UploadHandler +from upload.views.reports import ReportResultsView, ReportViews +from upload.views.test_results import TestResultsView +from upload.views.upload_completion import UploadCompletionView +from upload.views.upload_coverage import UploadCoverageView +from upload.views.uploads import UploadViews + +urlpatterns = [ + path( + "test_results/v1", + TestResultsView.as_view(), + name="upload-test-results", + ), + path( + "bundle_analysis/v1", + BundleAnalysisView.as_view(), + name="upload-bundle-analysis", + ), + # use regex to make trailing slash optional + path( + "///download", + UploadDownloadHandler.as_view(), + name="upload-download", + ), + # Empty routes that will become the new upload endpoint eventually + path( + "//commits//reports//uploads", + UploadViews.as_view(), + name="new_upload.uploads", + ), + path( + "//commits//reports//results", + ReportResultsView.as_view(), + name="new_upload.reports_results", + ), + path( + "//commits//reports", + ReportViews.as_view(), + name="new_upload.reports", + ), + path( + "//commits//empty-upload", + EmptyUploadView.as_view(), + name="new_upload.empty_upload", + ), + path( + "//commits//upload-complete", + UploadCompletionView.as_view(), + name="new_upload.upload-complete", + ), + path( + "//commits", + CommitViews.as_view(), + name="new_upload.commits", + ), + path( + "//upload-coverage", + UploadCoverageView.as_view(), + name="new_upload.upload_coverage", + ), + # This was getting in the way of the new endpoints, so I moved to the end + re_path(r"(?P\w+)/?", UploadHandler.as_view(), name="upload-handler"), +] diff --git a/apps/codecov-api/upload/views/base.py b/apps/codecov-api/upload/views/base.py new file mode 100644 index 0000000000..f250379f32 --- /dev/null +++ b/apps/codecov-api/upload/views/base.py @@ -0,0 +1,88 @@ +import logging +from typing import Optional + +from django.conf import settings +from rest_framework.exceptions import ValidationError + +from codecov_auth.models import Service +from core.models import Commit, Repository +from reports.models import CommitReport +from upload.views.helpers import get_repository_from_string + +log = logging.getLogger(__name__) + + +class ShelterMixin: + def is_shelter_request(self) -> bool: + """ + Returns true when the incoming request originated from a Shelter. + Shelter adds an `X-Shelter-Token` header which contains a shared secret. + Use of that shared secret allows certain priviledged functionality that normal + uploads cannot access. + """ + shelter_token = self.request.META.get("HTTP_X_SHELTER_TOKEN") + return shelter_token and shelter_token == settings.SHELTER_SHARED_SECRET + + +class GetterMixin(ShelterMixin): + def get_repo(self) -> Repository: + service = self.kwargs.get("service") + repo_slug = self.kwargs.get("repo") + try: + service_enum = Service(service) + except ValueError: + log.warning( + f"Service not found: {service}", extra=dict(repo_slug=repo_slug) + ) + raise ValidationError(f"Service not found: {service}") + + repository = get_repository_from_string(service_enum, repo_slug) + + if not repository: + log.warning( + "Repository not found", + extra=dict(repo_slug=repo_slug), + ) + raise ValidationError("Repository not found") + return repository + + def get_commit(self, repo: Repository) -> Commit: + commit_sha = self.kwargs.get("commit_sha") + try: + commit = Commit.objects.get( + commitid=commit_sha, repository__repoid=repo.repoid + ) + return commit + except Commit.DoesNotExist: + log.warning( + "Commit SHA not found", + extra=dict(repo=repo.name, commit_sha=commit_sha), + ) + raise ValidationError("Commit SHA not found") + + def get_report( + self, + commit: Commit, + report_type: Optional[ + CommitReport.ReportType + ] = CommitReport.ReportType.COVERAGE, + ) -> CommitReport: + report_code = self.kwargs.get("report_code") + if report_code == "default": + report_code = None + queryset = CommitReport.objects.filter(code=report_code, commit=commit) + if report_type == CommitReport.ReportType.COVERAGE: + queryset = queryset.coverage_reports() + else: + queryset = queryset.filter(report_type=report_type) + report = queryset.first() + if report is None: + log.warning( + "Report not found", + extra=dict(commit_sha=commit.commitid, report_code=report_code), + ) + raise ValidationError("Report not found") + if report.report_type is None: + report.report_type = CommitReport.ReportType.COVERAGE + report.save() + return report diff --git a/apps/codecov-api/upload/views/bundle_analysis.py b/apps/codecov-api/upload/views/bundle_analysis.py new file mode 100644 index 0000000000..950d68d728 --- /dev/null +++ b/apps/codecov-api/upload/views/bundle_analysis.py @@ -0,0 +1,253 @@ +import logging +import uuid +from typing import Any, Callable, Tuple + +from django.conf import settings +from django.http import HttpRequest +from rest_framework import serializers, status +from rest_framework.exceptions import NotAuthenticated, NotFound +from rest_framework.permissions import BasePermission +from rest_framework.response import Response +from rest_framework.views import APIView +from shared.api_archive.archive import ArchiveService +from shared.bundle_analysis.storage import StoragePaths, get_bucket_name +from shared.events.amplitude import UNKNOWN_USER_OWNERID, AmplitudeEventPublisher +from shared.helpers.redis import get_redis_connection +from shared.metrics import Counter, inc_counter + +from codecov_auth.authentication.repo_auth import ( + BundleAnalysisTokenlessAuthentication, + GitHubOIDCTokenAuthentication, + OrgLevelTokenAuthentication, + RepositoryLegacyTokenAuthentication, + UploadTokenRequiredGetFromBodyAuthenticationCheck, + repo_auth_custom_exception_handler, +) +from codecov_auth.authentication.types import RepositoryAsUser +from codecov_auth.models import Owner, Service +from core.models import Commit +from reports.models import CommitReport +from timeseries.models import Dataset, MeasurementName +from upload.helpers import ( + dispatch_upload_task, + generate_upload_prometheus_metrics_labels, +) +from upload.views.base import ShelterMixin +from upload.views.helpers import get_repository_from_string + +log = logging.getLogger(__name__) + + +BUNDLE_ANALYSIS_UPLOAD_VIEWS_COUNTER = Counter( + "bundle_analysis_upload_views_runs", + "Number of times a BA upload was run and with what result", + [ + "agent", + "version", + "action", + "endpoint", + "is_using_shelter", + "position", + "result", + ], +) + + +class UploadBundleAnalysisPermission(BasePermission): + def has_permission(self, request: HttpRequest, view: Any) -> bool: + return request.auth is not None and "upload" in request.auth.get_scopes() + + +class UploadSerializer(serializers.Serializer): + commit = serializers.CharField(required=True) + slug = serializers.CharField(required=True) + build = serializers.CharField(required=False, allow_null=True) + buildURL = serializers.CharField(required=False, allow_null=True) + job = serializers.CharField(required=False, allow_null=True) + pr = serializers.CharField(required=False, allow_null=True) + service = serializers.CharField(required=False, allow_null=True) + branch = serializers.CharField(required=False, allow_null=True) + compareSha = serializers.CharField(required=False, allow_null=True) + git_service = serializers.CharField(required=False, allow_null=True) + storage_path = serializers.CharField(required=False, allow_null=True) + upload_external_id = serializers.CharField(required=False, allow_null=True) + + +class BundleAnalysisView(APIView, ShelterMixin): + permission_classes = [UploadBundleAnalysisPermission] + authentication_classes = [ + UploadTokenRequiredGetFromBodyAuthenticationCheck, + OrgLevelTokenAuthentication, + GitHubOIDCTokenAuthentication, + RepositoryLegacyTokenAuthentication, + BundleAnalysisTokenlessAuthentication, + ] + + def get_exception_handler(self) -> Callable: + return repo_auth_custom_exception_handler + + def _handle_upload(self, request: HttpRequest) -> Tuple[str, Response]: + serializer = UploadSerializer(data=request.data) + if not serializer.is_valid(): + return ( + "bad_request", + Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST), + ) + data = serializer.validated_data + + if isinstance(request.user, Owner): + # using org token + owner = request.user + repo = get_repository_from_string(Service(owner.service), data["slug"]) + elif isinstance(request.user, RepositoryAsUser): + # repository token + repo = request.user._repository + else: + raise NotAuthenticated() + + if repo is None: + raise NotFound("Repository not found.") + + update_fields = [] + if not repo.active or not repo.activated: + repo.active = True + repo.activated = True + update_fields += ["active", "activated"] + + if not repo.bundle_analysis_enabled: + repo.bundle_analysis_enabled = True + update_fields += ["bundle_analysis_enabled"] + + if update_fields: + repo.save(update_fields=update_fields) + + commit, _ = Commit.objects.get_or_create( + commitid=data["commit"], + repository=repo, + defaults={ + "branch": data.get("branch"), + "pullid": data.get("pr"), + "merged": False if data.get("pr") is not None else None, + "state": "pending", + }, + ) + + AmplitudeEventPublisher().publish( + "Upload Received", + { + "user_ownerid": commit.author.ownerid + if commit.author + else UNKNOWN_USER_OWNERID, + "ownerid": repo.author.ownerid, + "repoid": repo.repoid, + "commitid": commit.id, # Not commit.commitid, we do not want a commit SHA here! + "pullid": commit.pullid, + "upload_type": "Bundle", + }, + ) + + storage_path = data.get("storage_path", None) + upload_external_id = data.get("upload_external_id", None) + url = None + if not self.is_shelter_request(): + upload_external_id = str(uuid.uuid4()) + storage_path = StoragePaths.upload.path(upload_key=upload_external_id) + archive_service = ArchiveService(repo) + url = archive_service.storage.create_presigned_put( + get_bucket_name(), storage_path, 30 + ) + + task_arguments = { + # these are used in the upload task when saving an upload record + # and use some unfortunately named and confusing keys + # (eventual reports_upload columns indicated by comments) + "reportid": upload_external_id, # external_id + "build": data.get("build"), # build_code + "build_url": data.get("buildURL"), # build_url + "job": data.get("job"), # job_code + "service": data.get("service"), # provider + "url": storage_path, # storage_path + # these are used for dispatching the task below + "commit": commit.commitid, + "report_code": None, + # custom comparison sha for the current uploaded commit sha + "bundle_analysis_compare_sha": data.get("compareSha"), + } + + log.info( + "Dispatching bundle analysis upload to worker", + extra=dict( + commit=commit.commitid, + repoid=repo.repoid, + task_arguments=task_arguments, + ), + ) + + dispatch_upload_task( + task_arguments, + repo, + get_redis_connection(), + report_type=CommitReport.ReportType.BUNDLE_ANALYSIS, + ) + + if settings.TIMESERIES_ENABLED: + supported_bundle_analysis_measurement_types = [ + MeasurementName.BUNDLE_ANALYSIS_ASSET_SIZE, + MeasurementName.BUNDLE_ANALYSIS_FONT_SIZE, + MeasurementName.BUNDLE_ANALYSIS_IMAGE_SIZE, + MeasurementName.BUNDLE_ANALYSIS_JAVASCRIPT_SIZE, + MeasurementName.BUNDLE_ANALYSIS_REPORT_SIZE, + MeasurementName.BUNDLE_ANALYSIS_STYLESHEET_SIZE, + ] + for measurement_type in supported_bundle_analysis_measurement_types: + _, created = Dataset.objects.get_or_create( + name=measurement_type.value, + repository_id=repo.pk, + ) + if created: + log.info( + "Created new timescale dataset for bundle analysis", + extra=dict( + commit=commit.commitid, + repoid=repo.repoid, + measurement_type=measurement_type, + ), + ) + + return ("success", Response({"url": url}, status=201)) + + def post(self, request: HttpRequest) -> Response: + labels = generate_upload_prometheus_metrics_labels( + action="bundle_analysis", + endpoint="bundle_analysis", + request=self.request, + is_shelter_request=self.is_shelter_request(), + position="start", + include_empty_labels=False, + ) + labels["result"] = "pending" + inc_counter( + BUNDLE_ANALYSIS_UPLOAD_VIEWS_COUNTER, + labels=labels, + ) + + try: + upload_result, response = self._handle_upload(request) + return response + except Exception as e: + log.error( + "Error handling bundle analysis upload", + extra=dict( + error=e, + ), + exc_info=True, + ) + upload_result = "error" + raise + finally: + labels["position"] = "end" + labels["result"] = upload_result + inc_counter( + BUNDLE_ANALYSIS_UPLOAD_VIEWS_COUNTER, + labels=labels, + ) diff --git a/apps/codecov-api/upload/views/commits.py b/apps/codecov-api/upload/views/commits.py new file mode 100644 index 0000000000..2ed1aa9a5c --- /dev/null +++ b/apps/codecov-api/upload/views/commits.py @@ -0,0 +1,103 @@ +import logging +from typing import Any, Callable, Dict + +from django.db.models import QuerySet +from django.http import HttpRequest +from rest_framework import serializers +from rest_framework.exceptions import NotAuthenticated +from rest_framework.generics import ListCreateAPIView +from rest_framework.response import Response +from shared.metrics import inc_counter + +from codecov_auth.authentication.repo_auth import ( + GitHubOIDCTokenAuthentication, + GlobalTokenAuthentication, + OrgLevelTokenAuthentication, + RepositoryLegacyTokenAuthentication, + TokenlessAuthentication, + UploadTokenRequiredAuthenticationCheck, + repo_auth_custom_exception_handler, +) +from core.models import Commit, Repository +from upload.helpers import ( + generate_upload_prometheus_metrics_labels, + validate_activated_repo, +) +from upload.metrics import API_UPLOAD_COUNTER +from upload.serializers import CommitSerializer +from upload.views.base import GetterMixin +from upload.views.uploads import CanDoCoverageUploadsPermission + +log = logging.getLogger(__name__) + + +def create_commit( + serializer: serializers.ModelSerializer, repository: Repository +) -> Commit: + validate_activated_repo(repository) + commit = serializer.save(repository=repository) + return commit + + +class CommitViews(ListCreateAPIView, GetterMixin): + serializer_class = CommitSerializer + permission_classes = [CanDoCoverageUploadsPermission] + authentication_classes = [ + UploadTokenRequiredAuthenticationCheck, + GlobalTokenAuthentication, + OrgLevelTokenAuthentication, + GitHubOIDCTokenAuthentication, + RepositoryLegacyTokenAuthentication, + TokenlessAuthentication, + ] + + def get_exception_handler(self) -> Callable[[Exception, Dict[str, Any]], Response]: + return repo_auth_custom_exception_handler + + def get_queryset(self) -> QuerySet: + repository = self.get_repo() + return Commit.objects.filter(repository=repository) + + def list(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Response: + repository = self.get_repo() + if repository.private and isinstance( + self.request.auth, TokenlessAuthentication + ): + raise NotAuthenticated() + return super().list(request, *args, **kwargs) + + def create(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Response: + return super().create(request, *args, **kwargs) + + def perform_create(self, serializer: CommitSerializer) -> Commit: + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="coverage", + endpoint="create_commit", + request=self.request, + is_shelter_request=self.is_shelter_request(), + position="start", + ), + ) + repository = self.get_repo() + commit = create_commit(serializer, repository) + + log.info( + "Request to create new commit", + extra=dict(repo=repository.name, commit=commit.commitid), + ) + + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="coverage", + endpoint="create_commit", + request=self.request, + repository=repository, + is_shelter_request=self.is_shelter_request(), + position="end", + ), + ) + + return commit diff --git a/apps/codecov-api/upload/views/empty_upload.py b/apps/codecov-api/upload/views/empty_upload.py new file mode 100644 index 0000000000..d8bb9ea1f6 --- /dev/null +++ b/apps/codecov-api/upload/views/empty_upload.py @@ -0,0 +1,229 @@ +import fnmatch +import logging +from typing import Any, Callable, List, Optional + +import regex +from asgiref.sync import async_to_sync +from django.http import HttpRequest +from rest_framework import serializers, status +from rest_framework.exceptions import NotFound +from rest_framework.generics import CreateAPIView +from rest_framework.response import Response +from shared.metrics import inc_counter +from shared.torngit.base import TorngitBaseAdapter +from shared.torngit.exceptions import TorngitClientError, TorngitClientGeneralError + +from codecov_auth.authentication.repo_auth import ( + GitHubOIDCTokenAuthentication, + GlobalTokenAuthentication, + OrgLevelTokenAuthentication, + RepositoryLegacyTokenAuthentication, + UploadTokenRequiredAuthenticationCheck, + repo_auth_custom_exception_handler, +) +from codecov_auth.authentication.types import RepositoryAsUser +from core.models import Commit +from services.repo_providers import RepoProviderService +from services.task import TaskService +from services.yaml import final_commit_yaml +from upload.helpers import ( + generate_upload_prometheus_metrics_labels, + try_to_get_best_possible_bot_token, +) +from upload.metrics import API_UPLOAD_COUNTER +from upload.views.base import GetterMixin +from upload.views.uploads import CanDoCoverageUploadsPermission + +log = logging.getLogger(__name__) + +GLOB_NON_TESTABLE_FILES = [ + "*.cfg", + "*.conf", + "*.css", + "*.csv", + "*.db", + "*.doc", + "*.egg", + "*.env", + "*.git", + "*.html", + "*.htmlypertext", + "*.ini", + "*.jar*", + "*.jpeg", + "*.jpg", + "*.jsonipt", + "*.mak*", + "*.md", + "*.pdf", + "*.png", + "*.ppt", + "*.svg", + "*.tar.tz", + "*.template", + "*.txt", + "*.whl", + "*.xls", + "*.xml", + "*.yaml", + "*.yml", +] + + +class EmptyUploadSerializer(serializers.Serializer): + should_force = serializers.BooleanField(required=False) + + +class EmptyUploadView(CreateAPIView, GetterMixin): + permission_classes = [CanDoCoverageUploadsPermission] + authentication_classes = [ + UploadTokenRequiredAuthenticationCheck, + GlobalTokenAuthentication, + OrgLevelTokenAuthentication, + GitHubOIDCTokenAuthentication, + RepositoryLegacyTokenAuthentication, + ] + + def get_exception_handler(self) -> Callable[[Exception, dict[str, Any]], Response]: + return repo_auth_custom_exception_handler + + def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Response: + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="coverage", + endpoint="empty_upload", + request=self.request, + is_shelter_request=self.is_shelter_request(), + position="start", + ), + ) + serializer = EmptyUploadSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + data = serializer.validated_data + should_force = data.get("should_force", False) + + repo = self.get_repo() + commit = self.get_commit(repo) + + if should_force is True: + TaskService().notify( + repoid=repo.repoid, commitid=commit.commitid, empty_upload="pass" + ) + return Response( + data={ + "result": "Force option was enabled. Triggering passing notifications.", + "non_ignored_files": [], + }, + status=status.HTTP_200_OK, + ) + + # Depending on the authentication class used, the request.user may need to be converted to a Owner + owner = request.user + if isinstance(request.user, RepositoryAsUser): + owner = request.user._repository.author + + yaml = final_commit_yaml(commit, owner).to_dict() + token = try_to_get_best_possible_bot_token(repo) + provider = RepoProviderService().get_adapter(repo.author, repo, token=token) + pull_id = commit.pullid + if pull_id is None: + pull_id = self.get_pull_request_id(commit, provider, pull_id) + + changed_files: List[str] = self.get_changed_files_from_provider( + commit, provider, pull_id + ) + + ignored_files = yaml.get("ignore", []) + + regex_non_testable_files = [ + fnmatch.translate(path) for path in GLOB_NON_TESTABLE_FILES + ] + + compiled_files_to_ignore = [ + regex.compile(path) for path in (regex_non_testable_files + ignored_files) + ] + + ignored_changed_files = [ + file + for file in changed_files + if any( + ( + regex.match(regex_patt, file, timeout=2) + for regex_patt in compiled_files_to_ignore + ) + ) + ] + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="coverage", + endpoint="empty_upload", + request=self.request, + repository=repo, + is_shelter_request=self.is_shelter_request(), + position="end", + ), + ) + if set(changed_files) == set(ignored_changed_files): + TaskService().notify( + repoid=repo.repoid, commitid=commit.commitid, empty_upload="pass" + ) + return Response( + data={ + "result": "All changed files are ignored. Triggering passing notifications.", + "non_ignored_files": [], + }, + status=status.HTTP_200_OK, + ) + + non_ignored_files = set(changed_files) - set(ignored_changed_files) + TaskService().notify( + repoid=repo.repoid, commitid=commit.commitid, empty_upload="fail" + ) + + return Response( + data={ + "result": "Some files cannot be ignored. Triggering failing notifications.", + "non_ignored_files": non_ignored_files, + }, + status=status.HTTP_200_OK, + ) + + def get_changed_files_from_provider( + self, commit: Commit, provider: TorngitBaseAdapter, pull_id: int + ) -> List[str]: + try: + changed_files = async_to_sync(provider.get_pull_request_files)(pull_id) + except TorngitClientError: + log.warning( + "Request client error", + extra=dict( + commit=commit.commitid, + repoid=commit.repository.repoid, + ), + exc_info=True, + ) + raise NotFound("Unable to get pull request's files.") + return changed_files + + def get_pull_request_id( + self, commit: Commit, provider: TorngitBaseAdapter, pull_id: Optional[int] + ) -> int: + try: + if pull_id is None: + pull_id = async_to_sync(provider.find_pull_request)( + commit=commit.commitid + ) + except TorngitClientGeneralError: + log.warning( + "Request client error", + extra=dict( + commit=commit.commitid, + repoid=commit.repository.repoid, + ), + exc_info=True, + ) + raise NotFound(f"Unable to get pull request for commit: {commit.commitid}") + return pull_id diff --git a/apps/codecov-api/upload/views/helpers.py b/apps/codecov-api/upload/views/helpers.py new file mode 100644 index 0000000000..cd61677680 --- /dev/null +++ b/apps/codecov-api/upload/views/helpers.py @@ -0,0 +1,47 @@ +import typing + +from codecov_auth.models import Owner, Service +from core.models import Repository + + +def get_repository_and_owner_from_string( + service: Service, repo_identifier: str +) -> tuple[Repository | None, Owner | None]: + if not isinstance(service, Service): + # if we pass this value to the db, it just raises DataError + # No need for that + return None, None + + if "::::" not in repo_identifier: + return None, None + + owner_identifier, repo_name_identifier = repo_identifier.rsplit("::::", 1) + owner = _get_owner_from_string(service, owner_identifier) + if not owner: + return None, None + try: + repository = Repository.objects.get(author=owner, name=repo_name_identifier) + except Repository.DoesNotExist: + return None, None + + return repository, owner + + +def _get_owner_from_string( + service: Service, owner_identifier: str +) -> typing.Optional[Owner]: + if ":::" in owner_identifier: + owner_identifier = owner_identifier.replace(":::", ":") + try: + return Owner.objects.get(service=service, username=owner_identifier) + except Owner.DoesNotExist: + return None + + +def get_repository_from_string( + service: Service, repo_identifier: str +) -> Repository | None: + repository, _ = get_repository_and_owner_from_string( + service=service, repo_identifier=repo_identifier + ) + return repository diff --git a/apps/codecov-api/upload/views/legacy.py b/apps/codecov-api/upload/views/legacy.py new file mode 100644 index 0000000000..42287d57f3 --- /dev/null +++ b/apps/codecov-api/upload/views/legacy.py @@ -0,0 +1,450 @@ +import asyncio +import logging +import re +from json import dumps +from uuid import uuid4 + +import minio +from asgiref.sync import sync_to_async +from django.conf import settings +from django.core.exceptions import MultipleObjectsReturned +from django.http import Http404, HttpResponse, HttpResponseServerError +from django.utils import timezone +from django.utils.decorators import classonlymethod +from django.utils.encoding import smart_str +from django.views import View +from rest_framework import renderers, status +from rest_framework.exceptions import ValidationError +from rest_framework.permissions import AllowAny +from rest_framework.views import APIView +from shared.api_archive.archive import ArchiveService +from shared.helpers.redis import get_redis_connection +from shared.metrics import inc_counter + +from codecov_auth.commands.owner import OwnerCommands +from core.commands.repository import RepositoryCommands +from services.analytics import AnalyticsService +from upload.helpers import ( + check_commit_upload_constraints, + determine_repo_for_upload, + determine_upload_branch_to_use, + determine_upload_commit_to_use, + determine_upload_pr_to_use, + dispatch_upload_task, + generate_upload_prometheus_metrics_labels, + insert_commit, + parse_headers, + parse_params, + validate_upload, +) +from upload.metrics import API_UPLOAD_COUNTER +from upload.views.base import ShelterMixin +from utils.config import get_config +from utils.services import get_long_service_name + +log = logging.getLogger(__name__) + + +class PlainTextRenderer(renderers.BaseRenderer): + media_type = "text/plain" + format = "txt" + + def render(self, data, media_type=None, renderer_context=None): + return smart_str(data, encoding=self.charset) + + +class UploadHandler(APIView, ShelterMixin): + permission_classes = [AllowAny] + renderer_classes = [PlainTextRenderer, renderers.JSONRenderer] + + def get(self, request, *args, **kwargs): + return HttpResponse(status=status.HTTP_405_METHOD_NOT_ALLOWED) + + def options(self, request, *args, **kwargs): + response = HttpResponse() + response["Accept"] = "text/*" + response["Access-Control-Allow-Origin"] = "*" + response["Access-Control-Allow-Method"] = "POST" + response["Access-Control-Allow-Headers"] = ( + "Origin, Content-Type, Accept, X-User-Agent" + ) + + return response + + def post(self, request, *args, **kwargs): + # Extract the version + version = self.kwargs["version"] + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="coverage", + endpoint="legacy_upload", + request=self.request, + is_shelter_request=self.is_shelter_request(), + position="start", + upload_version=version, + ), + ) + + log.info( + f"Received upload request {version}", + extra=dict( + version=version, + query_params=self.request.query_params, + commit=self.request.query_params.get("commit"), + ), + ) + + # Set response headers + response = HttpResponse() + response["Access-Control-Allow-Origin"] = "*" + response["Access-Control-Allow-Headers"] = ( + "Origin, Content-Type, Accept, X-User-Agent" + ) + + # Parse request parameters + request_params = { + **self.request.query_params.dict(), # query_params is a QueryDict, need to convert to dict to process it properly + **self.kwargs, + } + request_params["token"] = request_params.get("token") or request.META.get( + "HTTP_X_UPLOAD_TOKEN" + ) + + package = request_params.get("package") + if package is not None: + package_format = r"((codecov-cli/)|((.+-)?uploader-))(\d+.\d+.\d+)" + match = re.fullmatch(package_format, package) + if not match: + log.warning( + "Package query parameter failed to match CLI or uploader format", + extra=dict(package=package), + ) + try: + # note: try to avoid mutating upload_params past this point, to make it easier to reason about the state of this variable + upload_params = parse_params(request_params) + except ValidationError as e: + log.warning( + "Failed to parse upload request params", + extra=dict(request_params=request_params, errors=str(e)), + ) + response.status_code = status.HTTP_400_BAD_REQUEST + response.content = "Invalid request parameters" + return response + + # Try to determine the repository associated with the upload based on the params provided + try: + repository = determine_repo_for_upload(upload_params) + owner = repository.author + except ValidationError: + response.status_code = status.HTTP_400_BAD_REQUEST + response.content = "Could not determine repo and owner" + return response + except MultipleObjectsReturned: + response.status_code = status.HTTP_400_BAD_REQUEST + response.content = "Found too many repos" + return response + + log.info( + "Found repository for upload request", + extra=dict( + version=version, + upload_params=upload_params, + repo_name=repository.name, + owner_username=owner.username, + commit=upload_params.get("commit"), + ), + ) + + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="coverage", + endpoint="legacy_upload", + request=self.request, + repository=repository, + is_shelter_request=self.is_shelter_request(), + position="end", + upload_version=version, + ), + ) + + # Validate the upload to make sure the org has enough repo credits and is allowed to upload for this commit + redis = get_redis_connection() + validate_upload(upload_params, repository, redis) + log.info( + "Upload was determined to be valid", extra=dict(repoid=repository.repoid) + ) + # Do some processing to handle special cases for branch, pr, and commit values, and determine which values to use + # note that these values may be different from the values provided in the upload_params + branch = determine_upload_branch_to_use(upload_params, repository.branch) + pr = determine_upload_pr_to_use(upload_params) + commitid = determine_upload_commit_to_use(upload_params, repository) + + # Save (or update, if it exists already) the commit in the database + log.info( + "Saving commit in database", + extra=dict( + commit=commitid, + pr=pr, + branch=branch, + version=version, + upload_params=upload_params, + ), + ) + commit = insert_commit( + commitid, branch, pr, repository, owner, upload_params.get("parent") + ) + check_commit_upload_constraints(commit) + + # --------- Handle the actual upload + + reportid = str(uuid4()) + # populated later for `v4` uploads when generating presigned PUT url, + # or by `v2` uploads when storing the report directly + path = None + + # Get the url where the commit details can be found on the Codecov site, we'll return this in the response + destination_url = f"{settings.CODECOV_DASHBOARD_URL}/{owner.service}/{owner.username}/{repository.name}/commit/{commitid}" + + archive_service = ArchiveService(repository) + datetime = timezone.now().strftime("%Y-%m-%d") + repo_hash = archive_service.get_archive_hash(repository) + default_path = f"v4/raw/{datetime}/{repo_hash}/{commitid}/{reportid}.txt" + + # v2 - store request body directly + if version == "v2": + log.info( + "Started V2 upload", + extra=dict( + commit=commitid, + pr=pr, + branch=branch, + version=version, + upload_params=upload_params, + ), + ) + + path = default_path + encoding = request.META.get("HTTP_X_CONTENT_ENCODING") or request.META.get( + "HTTP_CONTENT_ENCODING" + ) + archive_service.write_file( + path, request.body, is_already_gzipped=(encoding == "gzip") + ) + + log.info( + "Stored coverage report", + extra=dict( + commit=commitid, + upload_params=upload_params, + reportid=reportid, + path=path, + repoid=repository.repoid, + ), + ) + + response.write( + dumps( + dict( + message="Coverage reports upload successfully", + uploaded=True, + queued=True, + id=reportid, + url=destination_url, + ) + ) + ) + + # v4 - generate presigned PUT url + minio = get_config("services", "minio") or {} + if minio and version == "v4": + log.info( + "Started V4 upload", + extra=dict( + commit=commitid, + pr=pr, + branch=branch, + version=version, + upload_params=upload_params, + ), + ) + + parse_headers(request.META, upload_params) + + # only Shelter requests are allowed to set their own `storage_path` + path = upload_params.get("storage_path") + if path is None or not self.is_shelter_request(): + path = default_path + + try: + # When using shelter (`is_shelter_request`), the returned `upload_url` is being + # ignored, as shelter is handling the creation of a "presigned put" matching the + # `storage_path`. + # This code runs here just for backwards compatibility reasons: + upload_url = archive_service.create_presigned_put(default_path) + except Exception as e: + log.warning( + f"Error generating minio presign put {e}", + extra=dict( + commit=commitid, + pr=pr, + branch=branch, + version=version, + upload_params=upload_params, + ), + ) + return HttpResponseServerError("Unknown error, please try again later") + log.info( + "Returning presign put", + extra=dict( + commit=commitid, repoid=repository.repoid, upload_url=upload_url + ), + ) + response["Content-Type"] = "text/plain" + response.write(f"{destination_url}\n{upload_url}") + + # Get build url + if ( + repository.service == "gitlab_enterprise" + and not upload_params.get("build_url") + and upload_params.get("build") + ): + # if gitlab ci - change domain based by referer + build_url = f"{get_config((repository.service, 'url'))}/{owner.username}/{repository.name}/{upload_params.get('build')}" + else: + build_url = upload_params.get("build_url") + queue_params = upload_params.copy() + if upload_params.get("using_global_token"): + queue_params["service"] = request_params.get("service") + # Define the task arguments to send when dispatching upload task to worker + task_arguments = { + **queue_params, + "build_url": build_url, + "reportid": reportid, + "url": ( + path + if path # If a path was generated for an upload, pass that to the 'url' field, potentially overwriting it + else upload_params.get("url") + ), + # These values below might be different from the initial request parameters, so overwrite them here to ensure they're up-to-date + "commit": commitid, + "branch": branch, + "pr": pr, + } + + log.info( + "Dispatching upload to worker (new upload)", + extra=dict( + commit=commitid, task_arguments=task_arguments, repoid=repository.repoid + ), + ) + + # Send task to worker + dispatch_upload_task(task_arguments, repository, redis) + + # Analytics Tracking + analytics_upload_data = upload_params.copy() + analytics_upload_data["repository_id"] = repository.repoid + analytics_upload_data["repository_name"] = repository.name + analytics_upload_data["version"] = version + analytics_upload_data["userid_type"] = "org" + analytics_upload_data["uploader_type"] = "node uploader" + AnalyticsService().account_uploaded_coverage_report( + owner.ownerid, analytics_upload_data + ) + + if version == "v4": + response["Content-Type"] = "text/plain" + request.META["HTTP_ACCEPT"] = "text/plain" + if version == "v2": + response["Content-Type"] = "application/json" + + response.status_code = status.HTTP_200_OK + return response + + +class UploadDownloadHandler(View): + @classonlymethod + def as_view(_, **initkwargs): + view = super().as_view(**initkwargs) + view._is_coroutine = asyncio.coroutines._is_coroutine + return view + + async def get_repo(self): + owner = await OwnerCommands( + self.request.current_owner, self.service + ).fetch_owner(self.owner_username) + if owner is None: + raise Http404("Requested report could not be found") + repo = await RepositoryCommands( + self.request.current_owner, + self.service, + ).fetch_repository( + owner, self.repo_name, [], exclude_okta_enforced_repos=False + ) # Okta sign-in is only enforced on the UI for now. + + if repo is None: + raise Http404("Requested report could not be found") + return repo + + @sync_to_async + def validate_path(self, repo): + msg = "Requested report could not be found" + if not self.path: + raise Http404(msg) + + if self.path.startswith("v4/raw"): + # direct API upload + + # Verify that the repo hash in the path matches the repo in the URL by generating the repo hash + archive_service = ArchiveService(repo) + if archive_service.storage_hash not in self.path: + raise Http404(msg) + elif self.path.startswith("shelter/"): + # Shelter upload + if not self.path.startswith( + f"shelter/{self.service}/{self.owner_username}::::{self.repo_name}" + ): + raise Http404(msg) + else: + # unexpected path structure + raise Http404(msg) + + def read_params(self): + self.path = self.request.GET.get("path") + self.service = get_long_service_name(self.kwargs.get("service")) + self.repo_name = self.kwargs.get("repo_name") + self.owner_username = self.kwargs.get("owner_username") + + @sync_to_async + def get_presigned_url(self, repo): + archive_service = ArchiveService(repo) + + try: + return archive_service.storage.create_presigned_get( + archive_service.root, self.path, expires=30 + ) + except minio.error.S3Error as e: + if e.code == "NoSuchKey": + raise Http404("Requested report could not be found") + else: + raise + + async def get(self, request, *args, **kwargs): + await self._get_user(request) + + self.read_params() + repo = await self.get_repo() + await self.validate_path(repo) + + response = HttpResponse(status=302) + response["Location"] = await self.get_presigned_url(repo) + return response + + @sync_to_async + def _get_user(self, request): + # force eager evaluation of `request.user` (a lazy object) + # while we're in a sync context + if request.user: + request.user.pk diff --git a/apps/codecov-api/upload/views/reports.py b/apps/codecov-api/upload/views/reports.py new file mode 100644 index 0000000000..a6140d5a91 --- /dev/null +++ b/apps/codecov-api/upload/views/reports.py @@ -0,0 +1,179 @@ +import logging +from typing import Any, Callable + +from django.http import HttpRequest, HttpResponseNotAllowed +from rest_framework.exceptions import ValidationError +from rest_framework.generics import CreateAPIView, ListCreateAPIView, RetrieveAPIView +from rest_framework.response import Response +from shared.metrics import inc_counter + +from codecov_auth.authentication.repo_auth import ( + GitHubOIDCTokenAuthentication, + GlobalTokenAuthentication, + OrgLevelTokenAuthentication, + RepositoryLegacyTokenAuthentication, + TokenlessAuthentication, + UploadTokenRequiredAuthenticationCheck, + repo_auth_custom_exception_handler, +) +from core.models import Commit, Repository +from reports.models import CommitReport, ReportResults +from services.task import TaskService +from upload.helpers import ( + generate_upload_prometheus_metrics_labels, + validate_activated_repo, +) +from upload.metrics import API_UPLOAD_COUNTER +from upload.serializers import CommitReportSerializer, ReportResultsSerializer +from upload.views.base import GetterMixin +from upload.views.uploads import CanDoCoverageUploadsPermission + +log = logging.getLogger(__name__) + + +def create_report( + serializer: CommitReportSerializer, repository: Repository, commit: Commit +) -> CommitReport: + code = serializer.validated_data.get("code") + if code == "default": + serializer.validated_data["code"] = None + instance, was_created = serializer.save( + commit_id=commit.id, + report_type=CommitReport.ReportType.COVERAGE, + ) + if was_created: + TaskService().preprocess_upload( + repository.repoid, commit.commitid, instance.code + ) + return instance + + +class ReportViews(ListCreateAPIView, GetterMixin): + serializer_class = CommitReportSerializer + permission_classes = [CanDoCoverageUploadsPermission] + authentication_classes = [ + UploadTokenRequiredAuthenticationCheck, + GlobalTokenAuthentication, + OrgLevelTokenAuthentication, + GitHubOIDCTokenAuthentication, + RepositoryLegacyTokenAuthentication, + TokenlessAuthentication, + ] + + def get_exception_handler(self) -> Callable[[Exception, dict[str, Any]], Response]: + return repo_auth_custom_exception_handler + + def perform_create(self, serializer: CommitReportSerializer) -> CommitReport: + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="coverage", + endpoint="create_report", + request=self.request, + is_shelter_request=self.is_shelter_request(), + position="start", + ), + ) + repository = self.get_repo() + validate_activated_repo(repository) + commit = self.get_commit(repository) + log.info( + "Request to create new report", + extra=dict(repo=repository.name, commit=commit.commitid), + ) + instance = create_report(serializer, repository, commit) + + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="coverage", + endpoint="create_report", + request=self.request, + repository=repository, + is_shelter_request=self.is_shelter_request(), + position="end", + ), + ) + return instance + + def list( + self, request: HttpRequest, service: str, repo: str, commit_sha: str + ) -> HttpResponseNotAllowed: + return HttpResponseNotAllowed(permitted_methods=["POST"]) + + +class ReportResultsView( + CreateAPIView, + RetrieveAPIView, + GetterMixin, +): + serializer_class = ReportResultsSerializer + permission_classes = [CanDoCoverageUploadsPermission] + authentication_classes = [ + UploadTokenRequiredAuthenticationCheck, + GlobalTokenAuthentication, + OrgLevelTokenAuthentication, + GitHubOIDCTokenAuthentication, + RepositoryLegacyTokenAuthentication, + TokenlessAuthentication, + ] + + def get_exception_handler(self) -> Callable[[Exception, dict[str, Any]], Response]: + return repo_auth_custom_exception_handler + + def perform_create(self, serializer: ReportResultsSerializer) -> ReportResults: + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="coverage", + endpoint="create_report_results", + request=self.request, + is_shelter_request=self.is_shelter_request(), + position="start", + ), + ) + repository = self.get_repo() + commit = self.get_commit(repository) + report = self.get_report(commit) + instance = ReportResults.objects.filter(report=report).first() + if not instance: + instance = serializer.save( + report=report, state=ReportResults.ReportResultsStates.PENDING + ) + else: + instance.state = ReportResults.ReportResultsStates.PENDING + instance.save() + TaskService().create_report_results( + commitid=commit.commitid, + repoid=repository.repoid, + report_code=report.code, + ) + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="coverage", + endpoint="create_report_results", + request=self.request, + repository=repository, + is_shelter_request=self.is_shelter_request(), + position="end", + ), + ) + return instance + + def get_object(self) -> ReportResults: + repository = self.get_repo() + commit = self.get_commit(repository) + report = self.get_report(commit) + try: + report_results = ReportResults.objects.get(report=report) + except ReportResults.DoesNotExist: + log.info( + "Report Results not found", + extra=dict( + commit_sha=commit.commitid, + report_code=self.kwargs.get("report_code"), + ), + ) + raise ValidationError("Report Results not found") + return report_results diff --git a/apps/codecov-api/upload/views/test_results.py b/apps/codecov-api/upload/views/test_results.py new file mode 100644 index 0000000000..7e975d0436 --- /dev/null +++ b/apps/codecov-api/upload/views/test_results.py @@ -0,0 +1,208 @@ +import logging +import uuid + +from django.utils import timezone +from rest_framework import serializers, status +from rest_framework.exceptions import NotAuthenticated, NotFound +from rest_framework.permissions import BasePermission +from rest_framework.response import Response +from rest_framework.views import APIView +from shared.api_archive.archive import ArchiveService, MinioEndpoints +from shared.events.amplitude import UNKNOWN_USER_OWNERID, AmplitudeEventPublisher +from shared.helpers.redis import get_redis_connection +from shared.metrics import inc_counter + +from codecov_auth.authentication.repo_auth import ( + GitHubOIDCTokenAuthentication, + OrgLevelTokenAuthentication, + RepositoryLegacyTokenAuthentication, + TestAnalyticsTokenlessAuthentication, + UploadTokenRequiredGetFromBodyAuthenticationCheck, + repo_auth_custom_exception_handler, +) +from codecov_auth.authentication.types import RepositoryAsUser +from codecov_auth.models import Owner, Service +from core.models import Commit +from reports.models import CommitReport +from upload.helpers import ( + dispatch_upload_task, + generate_upload_prometheus_metrics_labels, +) +from upload.metrics import API_UPLOAD_COUNTER +from upload.serializers import FlagListField +from upload.views.base import ShelterMixin +from upload.views.helpers import get_repository_from_string + +log = logging.getLogger(__name__) + + +class UploadTestResultsPermission(BasePermission): + def has_permission(self, request, view): + return request.auth is not None and "upload" in request.auth.get_scopes() + + +class UploadSerializer(serializers.Serializer): + commit = serializers.CharField(required=True) + slug = serializers.CharField(required=True) + service = serializers.CharField(required=False) # git_service + build = serializers.CharField(required=False) + buildURL = serializers.CharField(required=False) + job = serializers.CharField(required=False) + flags = FlagListField(required=False) + pr = serializers.CharField(required=False) + branch = serializers.CharField(required=False, allow_null=True) + storage_path = serializers.CharField(required=False) + file_not_found = serializers.BooleanField(required=False) + + +class TestResultsView( + APIView, + ShelterMixin, +): + permission_classes = [UploadTestResultsPermission] + authentication_classes = [ + UploadTokenRequiredGetFromBodyAuthenticationCheck, + OrgLevelTokenAuthentication, + GitHubOIDCTokenAuthentication, + RepositoryLegacyTokenAuthentication, + TestAnalyticsTokenlessAuthentication, + ] + + def get_exception_handler(self): + return repo_auth_custom_exception_handler + + def post(self, request): + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="test_results", + endpoint="test_results", + request=request, + is_shelter_request=self.is_shelter_request(), + position="start", + ), + ) + serializer = UploadSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + data = serializer.validated_data + + if isinstance(request.user, Owner): + # using org token + owner = request.user + repo = get_repository_from_string(Service(owner.service), data["slug"]) + elif isinstance(request.user, RepositoryAsUser): + # repository token + repo = request.user._repository + else: + raise NotAuthenticated() + + if repo is None: + raise NotFound("Repository not found.") + + update_fields = [] + if not repo.active or not repo.activated: + repo.active = True + repo.activated = True + update_fields += ["active", "activated"] + + if not repo.test_analytics_enabled: + repo.test_analytics_enabled = True + update_fields += ["test_analytics_enabled"] + + if update_fields: + repo.save(update_fields=update_fields) + + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="test_results", + endpoint="test_results", + request=request, + repository=repo, + is_shelter_request=self.is_shelter_request(), + position="end", + ), + ) + + commit, _ = Commit.objects.get_or_create( + commitid=data["commit"], + repository=repo, + defaults={ + "branch": data.get("branch") or repo.branch, + "pullid": data.get("pr"), + "merged": False if data.get("pr") is not None else None, + "state": "pending", + }, + ) + + AmplitudeEventPublisher().publish( + "Upload Received", + { + "user_ownerid": commit.author.ownerid + if commit.author + else UNKNOWN_USER_OWNERID, + "ownerid": repo.author.ownerid, + "repoid": repo.repoid, + "commitid": commit.id, # Not commit.commitid, we do not want a commit SHA here! + "pullid": commit.pullid, + "upload_type": "Test results", + }, + ) + + upload_external_id = str(uuid.uuid4()) + + url = None + file_not_found = data.get("file_not_found", False) + if file_not_found: + storage_path = None + else: + archive_service = ArchiveService(repo) + + storage_path = data.get("storage_path", None) + if storage_path is None or not self.is_shelter_request(): + storage_path = MinioEndpoints.test_results.get_path( + date=timezone.now().strftime("%Y-%m-%d"), + repo_hash=archive_service.get_archive_hash(repo), + commit_sha=data["commit"], + uploadid=upload_external_id, + ) + + url = archive_service.create_presigned_put(storage_path) + + task_arguments = { + # these are used in the upload task when saving an upload record + # and use some unfortunately named and confusing keys + # (eventual reports_upload columns indicated by comments) + "reportid": upload_external_id, # external_id + "build": data.get("build"), # build_code + "build_url": data.get("buildURL"), # build_url + "job": data.get("job"), # job_code + "flags": data.get("flags"), + "service": data.get("service"), # git provider + "url": storage_path, # storage_path + # these are used for dispatching the task below + "commit": commit.commitid, + "report_code": None, + } + + log.info( + "Dispatching test results upload to worker", + extra=dict( + commit=commit.commitid, + repoid=repo.repoid, + task_arguments=task_arguments, + ), + ) + + dispatch_upload_task( + task_arguments, + repo, + get_redis_connection(), + report_type=CommitReport.ReportType.TEST_RESULTS, + ) + + if url is None: + return Response(status=201) + else: + return Response({"raw_upload_location": url}, status=201) diff --git a/apps/codecov-api/upload/views/upload_completion.py b/apps/codecov-api/upload/views/upload_completion.py new file mode 100644 index 0000000000..f9b82ace1c --- /dev/null +++ b/apps/codecov-api/upload/views/upload_completion.py @@ -0,0 +1,107 @@ +import logging +from typing import Any, Callable, Dict + +from django.http import HttpRequest +from rest_framework import status +from rest_framework.generics import CreateAPIView +from rest_framework.response import Response +from shared.metrics import inc_counter + +from codecov_auth.authentication.repo_auth import ( + GitHubOIDCTokenAuthentication, + GlobalTokenAuthentication, + OrgLevelTokenAuthentication, + RepositoryLegacyTokenAuthentication, + UploadTokenRequiredAuthenticationCheck, + repo_auth_custom_exception_handler, +) +from reports.models import ReportSession +from services.task import TaskService +from upload.helpers import generate_upload_prometheus_metrics_labels +from upload.metrics import API_UPLOAD_COUNTER +from upload.views.base import GetterMixin +from upload.views.uploads import CanDoCoverageUploadsPermission + +log = logging.getLogger(__name__) + + +class UploadCompletionView(CreateAPIView, GetterMixin): + permission_classes = [CanDoCoverageUploadsPermission] + authentication_classes = [ + UploadTokenRequiredAuthenticationCheck, + GlobalTokenAuthentication, + OrgLevelTokenAuthentication, + GitHubOIDCTokenAuthentication, + RepositoryLegacyTokenAuthentication, + ] + + def get_exception_handler(self) -> Callable[[Exception, Dict[str, Any]], Response]: + return repo_auth_custom_exception_handler + + def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Response: + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="coverage", + endpoint="upload_complete", + request=self.request, + is_shelter_request=self.is_shelter_request(), + position="start", + ), + ) + repo = self.get_repo() + commit = self.get_commit(repo) + uploads_queryset = ReportSession.objects.filter( + report__commit=commit, + report__code=None, + ) + uploads_count = uploads_queryset.count() + if not uploads_queryset or uploads_count == 0: + log.info( + "Cannot trigger notifications as we didn't find any uploads for the provided commit", + extra=dict( + repo=repo.name, commit=commit.commitid, pullid=commit.pullid + ), + ) + return Response( + data={ + "uploads_total": 0, + "uploads_success": 0, + "uploads_processing": 0, + "uploads_error": 0, + }, + status=status.HTTP_404_NOT_FOUND, + ) + + in_progress_uploads = 0 + errored_uploads = 0 + for upload in uploads_queryset: + # upload is still processing + if not upload.state: + in_progress_uploads += 1 + elif upload.state == "error": + errored_uploads += 1 + + TaskService().manual_upload_completion_trigger(repo.repoid, commit.commitid) + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="coverage", + endpoint="upload_complete", + request=self.request, + repository=repo, + is_shelter_request=self.is_shelter_request(), + position="end", + ), + ) + return Response( + data={ + "uploads_total": uploads_count, + "uploads_success": uploads_count + - in_progress_uploads + - errored_uploads, + "uploads_processing": in_progress_uploads, + "uploads_error": errored_uploads, + }, + status=status.HTTP_200_OK, + ) diff --git a/apps/codecov-api/upload/views/upload_coverage.py b/apps/codecov-api/upload/views/upload_coverage.py new file mode 100644 index 0000000000..ecea6b92a2 --- /dev/null +++ b/apps/codecov-api/upload/views/upload_coverage.py @@ -0,0 +1,158 @@ +import logging + +from django.conf import settings +from django.http import HttpRequest +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView +from shared.api_archive.archive import ArchiveService +from shared.metrics import inc_counter + +from codecov_auth.authentication.repo_auth import ( + GitHubOIDCTokenAuthentication, + GlobalTokenAuthentication, + OrgLevelTokenAuthentication, + RepositoryLegacyTokenAuthentication, + TokenlessAuthentication, + UploadTokenRequiredAuthenticationCheck, + repo_auth_custom_exception_handler, +) +from upload.helpers import generate_upload_prometheus_metrics_labels +from upload.metrics import API_UPLOAD_COUNTER +from upload.serializers import ( + CommitReportSerializer, + CommitSerializer, + UploadSerializer, +) +from upload.throttles import UploadsPerCommitThrottle, UploadsPerWindowThrottle +from upload.views.base import GetterMixin +from upload.views.commits import create_commit +from upload.views.reports import create_report +from upload.views.uploads import ( + CanDoCoverageUploadsPermission, + create_upload, + get_token_for_analytics, +) + +log = logging.getLogger(__name__) + + +class UploadCoverageView(APIView, GetterMixin): + permission_classes = [CanDoCoverageUploadsPermission] + authentication_classes = [ + UploadTokenRequiredAuthenticationCheck, + GlobalTokenAuthentication, + OrgLevelTokenAuthentication, + GitHubOIDCTokenAuthentication, + RepositoryLegacyTokenAuthentication, + TokenlessAuthentication, + ] + throttle_classes = [UploadsPerCommitThrottle, UploadsPerWindowThrottle] + + def get_exception_handler(self): + return repo_auth_custom_exception_handler + + def emit_metrics(self, position: str) -> None: + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="coverage", + endpoint="upload_coverage", + request=self.request, + is_shelter_request=self.is_shelter_request(), + position=position, + ), + ) + + def post(self, request: HttpRequest, *args, **kwargs) -> Response: + self.emit_metrics(position="start") + + # Create commit + create_commit_data = dict( + branch=request.data.get("branch"), + commitid=request.data.get("commitid"), + parent_commit_id=request.data.get("parent_commit_id"), + pullid=request.data.get("pullid"), + ) + commit_serializer = CommitSerializer(data=create_commit_data) + if not commit_serializer.is_valid(): + return Response( + commit_serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + + repository = self.get_repo() + self.emit_metrics(position="create_commit") + commit = create_commit(commit_serializer, repository) + + log.info( + "Request to create new coverage upload", + extra=dict( + repo=repository.name, + commit=commit.commitid, + ), + ) + + # Create report + commit_report_data = dict( + code=request.data.get("code"), + ) + commit_report_serializer = CommitReportSerializer(data=commit_report_data) + if not commit_report_serializer.is_valid(): + return Response( + commit_report_serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + + self.emit_metrics(position="create_report") + report = create_report(commit_report_serializer, repository, commit) + + # Do upload + upload_data = dict( + ci_service=request.data.get("ci_service"), + ci_url=request.data.get("ci_url"), + env=request.data.get("env"), + flags=request.data.get("flags"), + job_code=request.data.get("job_code"), + name=request.data.get("name"), + version=request.data.get("version"), + ) + + if self.is_shelter_request(): + upload_data["storage_path"] = request.data.get("storage_path") + + upload_serializer = UploadSerializer(data=upload_data) + if not upload_serializer.is_valid(): + return Response( + upload_serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + + self.emit_metrics(position="create_upload") + upload = create_upload( + upload_serializer, + repository, + commit, + report, + self.is_shelter_request(), + get_token_for_analytics(commit, self.request), + ) + + self.emit_metrics(position="end") + + if not upload: + return Response( + upload_serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + + commitid = upload.report.commit.commitid + upload_repository = upload.report.commit.repository + url = f"{settings.CODECOV_DASHBOARD_URL}/{upload_repository.author.service}/{upload_repository.author.username}/{upload_repository.name}/commit/{commitid}" + archive_service = ArchiveService(upload_repository) + raw_upload_location = archive_service.create_presigned_put(upload.storage_path) + return Response( + { + "external_id": str(upload.external_id), + "created_at": upload.created_at.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "raw_upload_location": raw_upload_location, + "url": url, + }, + status=status.HTTP_201_CREATED, + ) diff --git a/apps/codecov-api/upload/views/uploads.py b/apps/codecov-api/upload/views/uploads.py new file mode 100644 index 0000000000..a430943dd3 --- /dev/null +++ b/apps/codecov-api/upload/views/uploads.py @@ -0,0 +1,309 @@ +import logging +import uuid +from typing import Any, Callable, Dict + +from django.http import HttpRequest, HttpResponseNotAllowed +from django.utils import timezone +from rest_framework.exceptions import ValidationError +from rest_framework.generics import ListCreateAPIView +from rest_framework.permissions import BasePermission +from rest_framework.response import Response +from rest_framework.views import APIView +from shared.api_archive.archive import ArchiveService, MinioEndpoints +from shared.events.amplitude import UNKNOWN_USER_OWNERID, AmplitudeEventPublisher +from shared.helpers.redis import get_redis_connection +from shared.metrics import inc_counter +from shared.upload.utils import UploaderType, insert_coverage_measurement + +from codecov_auth.authentication.repo_auth import ( + GitHubOIDCTokenAuthentication, + GlobalTokenAuthentication, + OIDCTokenRepositoryAuth, + OrgLevelTokenAuthentication, + OrgLevelTokenRepositoryAuth, + RepositoryLegacyTokenAuthentication, + TokenlessAuth, + TokenlessAuthentication, + UploadTokenRequiredAuthenticationCheck, + repo_auth_custom_exception_handler, +) +from codecov_auth.models import OrganizationLevelToken +from core.models import Commit, Repository +from reports.models import CommitReport, ReportSession +from services.analytics import AnalyticsService +from upload.helpers import ( + dispatch_upload_task, + generate_upload_prometheus_metrics_labels, + validate_activated_repo, +) +from upload.metrics import API_UPLOAD_COUNTER +from upload.serializers import UploadSerializer +from upload.throttles import UploadsPerCommitThrottle, UploadsPerWindowThrottle +from upload.views.base import GetterMixin + +log = logging.getLogger(__name__) + + +def create_upload( + serializer: UploadSerializer, + repository: Repository, + commit: Commit, + report: CommitReport, + is_shelter_request: bool, + analytics_token: str, +) -> ReportSession: + AmplitudeEventPublisher().publish( + "Upload Received", + { + "user_ownerid": commit.author.ownerid + if commit.author + else UNKNOWN_USER_OWNERID, + "ownerid": repository.author.ownerid, + "repoid": repository.repoid, + "commitid": commit.id, # Not commit.commitid, we do not want a commit SHA here. + "pullid": commit.pullid, + "upload_type": "Coverage report", + }, + ) + + version = ( + serializer.validated_data["version"] + if "version" in serializer.validated_data + else None + ) + archive_service = ArchiveService(repository) + # only Shelter requests are allowed to set their own `storage_path` + + if not serializer.validated_data.get("storage_path") or not is_shelter_request: + serializer.validated_data["external_id"] = uuid.uuid4() + path = MinioEndpoints.raw_with_upload_id.get_path( + version="v4", + date=timezone.now().strftime("%Y-%m-%d"), + repo_hash=archive_service.storage_hash, + commit_sha=commit.commitid, + reportid=report.external_id, + uploadid=serializer.validated_data["external_id"], + ) + serializer.validated_data["storage_path"] = path + # Create upload record + instance: ReportSession = serializer.save( + repo_id=repository.repoid, + report_id=report.id, + upload_extras={"format_version": "v1"}, + state="started", + ) + + # Inserts mirror upload record into measurements table. CLI hits this endpoint + insert_coverage_measurement( + owner_id=repository.author.ownerid, + repo_id=repository.repoid, + commit_id=commit.id, + upload_id=instance.id, + uploader_used=UploaderType.CLI.value, + private_repo=repository.private, + report_type=report.report_type, + ) + + trigger_upload_task(repository, commit.commitid, instance, report) + activate_repo(repository) + send_analytics_data(commit, instance, version, analytics_token) + return instance + + +def trigger_upload_task( + repository: Repository, commit_sha: str, upload: ReportSession, report: CommitReport +) -> None: + log.info( + "Triggering upload task", + extra=dict( + repo=repository.name, + commit=commit_sha, + upload_id=upload.id, + report_code=report.code, + ), + ) + redis = get_redis_connection() + task_arguments = { + "commit": commit_sha, + "upload_id": upload.id, + "version": "v4", + "report_code": report.code, + "reportid": str(report.external_id), + } + dispatch_upload_task(task_arguments, repository, redis) + + +def activate_repo(repository: Repository) -> None: + # Only update the fields if needed + if ( + repository.activated + and repository.active + and not repository.deleted + and repository.coverage_enabled + ): + return + repository.activated = True + repository.active = True + repository.deleted = False + repository.coverage_enabled = True + repository.save( + update_fields=[ + "activated", + "active", + "deleted", + "coverage_enabled", + "updatestamp", + ] + ) + + +def send_analytics_data( + commit: Commit, upload: ReportSession, version: str, analytics_token: str +) -> None: + analytics_upload_data = { + "commit": commit.commitid, + "branch": commit.branch, + "pr": commit.pullid, + "repo": commit.repository.name, + "repository_name": commit.repository.name, + "repository_id": commit.repository.repoid, + "service": commit.repository.service, + "build": upload.build_code, + "build_url": upload.build_url, + # we were previously using upload.flag_names here, and this query might not be optimized + # we weren't doing it in the legacy endpoint, but in the new one we are, and it may be causing problems + # therefore we are removing this for now to see if it is the source of the issue + "flags": "", + "owner": commit.repository.author.ownerid, + "token": str(analytics_token), + "version": version, + "uploader_type": "CLI", + } + AnalyticsService().account_uploaded_coverage_report( + commit.repository.author.ownerid, analytics_upload_data + ) + + +def get_token_for_analytics(commit: Commit, request: HttpRequest) -> str: + repo = commit.repository + if isinstance(request.auth, TokenlessAuth): + analytics_token = "tokenless_upload" + elif isinstance(request.auth, OrgLevelTokenRepositoryAuth): + analytics_token = ( + OrganizationLevelToken.objects.filter(owner=repo.author).first().token + ) + elif isinstance(request.auth, OIDCTokenRepositoryAuth): + analytics_token = "oidc_token_upload" + else: + analytics_token = repo.upload_token + return analytics_token + + +class CanDoCoverageUploadsPermission(BasePermission): + def has_permission(self, request: HttpRequest, view: APIView) -> bool: + repository = view.get_repo() + return ( + request.auth is not None + and "upload" in request.auth.get_scopes() + and request.auth.allows_repo(repository) + ) + + +class UploadViews(ListCreateAPIView, GetterMixin): + serializer_class = UploadSerializer + permission_classes = [ + CanDoCoverageUploadsPermission, + ] + authentication_classes = [ + UploadTokenRequiredAuthenticationCheck, + GlobalTokenAuthentication, + OrgLevelTokenAuthentication, + GitHubOIDCTokenAuthentication, + RepositoryLegacyTokenAuthentication, + TokenlessAuthentication, + ] + + throttle_classes = [UploadsPerCommitThrottle, UploadsPerWindowThrottle] + + def get_exception_handler(self) -> Callable[[Exception, Dict[str, Any]], Response]: + return repo_auth_custom_exception_handler + + def perform_create(self, serializer: UploadSerializer) -> ReportSession: + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="coverage", + endpoint="create_upload", + request=self.request, + is_shelter_request=self.is_shelter_request(), + position="start", + ), + ) + repository: Repository = self.get_repo() + validate_activated_repo(repository) + commit: Commit = self.get_commit(repository) + report: CommitReport = self.get_report(commit) + + log.info( + "Request to create new upload", + extra=dict( + repo=repository.name, + commit=commit.commitid, + cli_version=serializer.validated_data["version"] + if "version" in serializer.validated_data + else None, + ), + ) + + instance = create_upload( + serializer, + repository, + commit, + report, + self.is_shelter_request(), + get_token_for_analytics(commit, self.request), + ) + inc_counter( + API_UPLOAD_COUNTER, + labels=generate_upload_prometheus_metrics_labels( + action="coverage", + endpoint="create_upload", + request=self.request, + repository=repository, + is_shelter_request=self.is_shelter_request(), + position="end", + ), + ) + + return instance + + def list( + self, + request: HttpRequest, + service: str, + repo: str, + commit_sha: str, + report_code: str, + ) -> HttpResponseNotAllowed: + return HttpResponseNotAllowed(permitted_methods=["POST"]) + + def get_repo(self) -> Repository: + try: + repo = super().get_repo() + return repo + except ValidationError as exception: + raise exception + + def get_commit(self, repo: Repository) -> Commit: + try: + commit = super().get_commit(repo) + return commit + except ValidationError as excpetion: + raise excpetion + + def get_report(self, commit: Commit, _: Any = None) -> CommitReport: + try: + report = super().get_report(commit) + return report + except ValidationError as exception: + raise exception diff --git a/apps/codecov-api/utils/__init__.py b/apps/codecov-api/utils/__init__.py new file mode 100644 index 0000000000..2da0214932 --- /dev/null +++ b/apps/codecov-api/utils/__init__.py @@ -0,0 +1,43 @@ +import math +import uuid +from typing import Any + + +def is_uuid(value: Any) -> bool: + try: + uuid.UUID(str(value)) + return True + except ValueError: + return False + + +def round_decimals_down(number: float, decimals: int = 2) -> float: + """ + Returns a value rounded down to a specific number of decimal places. + """ + if not isinstance(decimals, int): + raise TypeError("decimal places must be an integer") + elif decimals < 0: + raise ValueError("decimal places has to be 0 or more") + elif decimals == 0: + return math.floor(number) + + factor = 10**decimals + return math.floor(number * factor) / factor + + +# Copied from +def strtobool(val: Any) -> int: + """Convert a string representation of truth to true (1) or false (0). + + True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values + are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if + 'val' is anything else. + """ + val = val.lower() + if val in ("y", "yes", "t", "true", "on", "1"): + return 1 + elif val in ("n", "no", "f", "false", "off", "0"): + return 0 + else: + raise ValueError("invalid truth value %r" % (val,)) diff --git a/apps/codecov-api/utils/config.py b/apps/codecov-api/utils/config.py new file mode 100644 index 0000000000..577b0e9904 --- /dev/null +++ b/apps/codecov-api/utils/config.py @@ -0,0 +1,64 @@ +import logging +import os +from enum import Enum + +from shared.config import get_config as shared_get_config + + +class SettingsModule(Enum): + DEV = "codecov.settings_dev" + STAGING = "codecov.settings_staging" + TESTING = "codecov.settings_test" + ENTERPRISE = "codecov.settings_enterprise" + PRODUCTION = "codecov.settings_prod" + + +RUN_ENV = os.environ.get("RUN_ENV", "PRODUCTION") + + +if RUN_ENV == "DEV": + settings_module = SettingsModule.DEV.value +elif RUN_ENV == "STAGING": + settings_module = SettingsModule.STAGING.value +elif RUN_ENV == "TESTING": + settings_module = SettingsModule.TESTING.value +elif RUN_ENV == "ENTERPRISE": + settings_module = SettingsModule.ENTERPRISE.value +else: + settings_module = SettingsModule.PRODUCTION.value + + +def get_settings_module(): + return settings_module + + +class MissingConfigException(Exception): + pass + + +log = logging.getLogger(__name__) + + +def get_config(*path, default=None): + return shared_get_config(*path, default=default) + + +def should_write_data_to_storage_config_check( + master_switch_key: str, is_codecov_repo: bool, repoid: int +) -> bool: + master_write_switch = get_config( + "setup", + "save_report_data_in_storage", + master_switch_key, + default=False, + ) + if master_write_switch == "restricted_access": + allowed_repo_ids = get_config( + "setup", "save_report_data_in_storage", "repo_ids", default=[] + ) + is_in_allowed_repoids = repoid in allowed_repo_ids + elif master_write_switch == "general_access": + is_in_allowed_repoids = True + else: + is_in_allowed_repoids = False + return master_write_switch and (is_codecov_repo or is_in_allowed_repoids) diff --git a/apps/codecov-api/utils/encryption.py b/apps/codecov-api/utils/encryption.py new file mode 100644 index 0000000000..8fc320865e --- /dev/null +++ b/apps/codecov-api/utils/encryption.py @@ -0,0 +1,3 @@ +from shared.encryption.oauth import get_encryptor_from_configuration + +encryptor = get_encryptor_from_configuration() diff --git a/apps/codecov-api/utils/logging_configuration.py b/apps/codecov-api/utils/logging_configuration.py new file mode 100644 index 0000000000..27455746fd --- /dev/null +++ b/apps/codecov-api/utils/logging_configuration.py @@ -0,0 +1,89 @@ +import json +from datetime import datetime +from logging import Filter + +from pythonjsonlogger.jsonlogger import JsonFormatter +from sentry_sdk import get_current_span + + +class BaseLogger(JsonFormatter): + def add_fields(self, log_record, record, message_dict): + super(BaseLogger, self).add_fields(log_record, record, message_dict) + + asctime_format = "%Y-%m-%d %H:%M:%S,%f" + asctime = datetime.strptime(log_record.get("asctime"), asctime_format) + + log_record["utctime"] = asctime.isoformat() + + def format_json_on_new_lines(self, json_str): + # Parse the input JSON string + data = json.loads(json_str) + # Convert the parsed JSON data back to a formatted JSON string + formatted_json = json.dumps(data, indent=4) + return formatted_json + + +class CustomLocalJsonFormatter(BaseLogger): + def jsonify_log_record(self, log_record): + """Returns a json string of the log record.""" + levelname = log_record.pop("levelname") + message = log_record.pop("message") + exc_info = log_record.pop("exc_info", "") + content = super().jsonify_log_record(log_record) + formatted = super().format_json_on_new_lines(content) if content else None + if exc_info: + return f"{levelname}: {message} \n {formatted}\n{exc_info}" + return f"{levelname}: {message} \n {formatted}" + + +class CustomDatadogJsonFormatter(BaseLogger): + def add_fields(self, log_record, record, message_dict): + super(CustomDatadogJsonFormatter, self).add_fields( + log_record, record, message_dict + ) + if not log_record.get("logger.name") and log_record.get("name"): + log_record["logger.name"] = log_record.get("name") + if not log_record.get("logger.thread_name") and log_record.get("threadName"): + log_record["logger.thread_name"] = log_record.get("threadName") + if log_record.get("level"): + log_record["level"] = log_record["level"].upper() + else: + log_record["level"] = record.levelname + + span = get_current_span() + if span and span.trace_id: + log_record["sentry_trace_id"] = span.trace_id + + +class CustomGunicornLogFormatter(JsonFormatter): + rename_fields = { + "levelname": "level", + "r": "request", + "a": "useragent", + "f": "referrer", + "b": "response_length", + "h": "remote_address", + "t": "request_time", + "s": "status_code", + } + + def add_fields(self, log_record, record, message_dict): + super(CustomGunicornLogFormatter, self).add_fields( + log_record, record, message_dict + ) + for field in self._required_fields: + if field in self.rename_fields: + log_record[self.rename_fields[field]] = record.args.get(field) + del log_record[field] + else: + log_record[field] = record.args.get(field) + + +class HealthCheckFilter(Filter): + def filter(self, record): + # Ignore /health/ requests, unless it's not a 200. + return ( + ("GET /health/" not in record.getMessage()) + if record.args.get("s") == "200" + else True + ) diff --git a/apps/codecov-api/utils/repos.py b/apps/codecov-api/utils/repos.py new file mode 100644 index 0000000000..a44c2806bc --- /dev/null +++ b/apps/codecov-api/utils/repos.py @@ -0,0 +1,13 @@ +from typing import Optional + +from codecov_auth.models import Owner +from core.models import Repository + + +def get_bot_user(repo: Repository) -> Optional[Owner]: + if repo.bot and repo.bot.oauth_token: + return repo.bot + if repo.author.bot and repo.author.bot.oauth_token: + return repo.author.bot + if repo.author.oauth_token: + return repo.author diff --git a/apps/codecov-api/utils/rollouts.py b/apps/codecov-api/utils/rollouts.py new file mode 100644 index 0000000000..a9ffa1aa57 --- /dev/null +++ b/apps/codecov-api/utils/rollouts.py @@ -0,0 +1,5 @@ +from codecov_auth.models import Owner + + +def owner_slug(owner: Owner) -> str: + return f"{owner.service}/{owner.username}" diff --git a/apps/codecov-api/utils/routers.py b/apps/codecov-api/utils/routers.py new file mode 100644 index 0000000000..94ebc65f89 --- /dev/null +++ b/apps/codecov-api/utils/routers.py @@ -0,0 +1,40 @@ +from rest_framework.routers import DefaultRouter, DynamicRoute, Route + + +class OptionalTrailingSlashRouter(DefaultRouter): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.trailing_slash = "/?" + + +class RetrieveUpdateDestroyRouter(OptionalTrailingSlashRouter): + """ + A router that maps GET /resource/ -> retrieve, as opposed to list. + + Also maps + + PUT /resource/ -> to update + PATCH /resource/ -> to partial_update + DELETE /resource/ -> to destroy + """ + + routes = [ + Route( + url=r"^{prefix}{trailing_slash}$", + mapping={ + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + }, + name="{basename}-detail", + detail=False, + initkwargs={"suffix": "Retrieve"}, + ), + DynamicRoute( + url=r"^{prefix}/{url_path}$", + name="{basename}-{url_name}", + detail=False, + initkwargs={}, + ), + ] diff --git a/apps/codecov-api/utils/services.py b/apps/codecov-api/utils/services.py new file mode 100644 index 0000000000..1c6edb2983 --- /dev/null +++ b/apps/codecov-api/utils/services.py @@ -0,0 +1,21 @@ +short_services = { + "gh": "github", + "bb": "bitbucket", + "gl": "gitlab", + "ghe": "github_enterprise", + "gle": "gitlab_enterprise", + "bbs": "bitbucket_server", +} +long_services = {value: key for (key, value) in short_services.items()} + + +def get_long_service_name(service): + if service in short_services: + return short_services[service] + return service + + +def get_short_service_name(service): + if service in long_services: + return long_services[service] + return service diff --git a/apps/codecov-api/utils/shelter.py b/apps/codecov-api/utils/shelter.py new file mode 100644 index 0000000000..f34a847b8b --- /dev/null +++ b/apps/codecov-api/utils/shelter.py @@ -0,0 +1,50 @@ +import json +import logging +from typing import Any, Dict + +from django.conf import settings +from google.cloud import pubsub_v1 + +log = logging.getLogger(__name__) + + +class ShelterPubsub: + pubsub_publisher = None + _instance = None + + @classmethod + def get_instance(cls) -> "ShelterPubsub": + """ + This class needs the Django settings to be fully loaded before it can be instantiated, + therefore use this method to get an instance rather than instantiating directly. + """ + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def __init__(self) -> None: + if not settings.SHELTER_ENABLED: + return + + if not self.pubsub_publisher: + self.pubsub_publisher = pubsub_v1.PublisherClient() + pubsub_project_id: str = settings.SHELTER_PUBSUB_PROJECT_ID + + # topic_id has REPO in the name but it is used for all types of objects + topic_id: str = settings.SHELTER_PUBSUB_SYNC_REPO_TOPIC_ID + self.topic_path = self.pubsub_publisher.topic_path(pubsub_project_id, topic_id) + + def publish(self, data: Dict[str, Any]) -> None: + if not settings.SHELTER_ENABLED: + return + + try: + self.pubsub_publisher.publish( + self.topic_path, + json.dumps(data).encode("utf-8"), + ) + except Exception as e: + log.warning( + "Failed to publish a message", + extra=dict(data_to_publish=data, error=e), + ) diff --git a/apps/codecov-api/utils/test_results.py b/apps/codecov-api/utils/test_results.py new file mode 100644 index 0000000000..1d5e043871 --- /dev/null +++ b/apps/codecov-api/utils/test_results.py @@ -0,0 +1,116 @@ +import polars as pl +from django.conf import settings +from shared.helpers.redis import get_redis_connection +from shared.storage import get_appropriate_storage_service +from shared.storage.exceptions import FileNotInStorageError + +from services.task import TaskService + + +def redis_key( + repoid: int, + branch: str, + interval_start: int, + interval_end: int | None = None, +) -> str: + key = f"test_results:{repoid}:{branch}:{interval_start}" + + if interval_end is not None: + key = f"{key}:{interval_end}" + + return key + + +def storage_key( + repoid: int, branch: str, interval_start: int, interval_end: int | None = None +) -> str: + key = f"test_results/rollups/{repoid}/{branch}/{interval_start}" + + if interval_end is not None: + key = f"{key}_{interval_end}" + + return key + + +def dedup_table(table: pl.DataFrame) -> pl.DataFrame: + failure_rate_expr = ( + pl.col("failure_rate") + * (pl.col("total_fail_count") + pl.col("total_pass_count")) + ).sum() / (pl.col("total_fail_count") + pl.col("total_pass_count")).sum() + + flake_rate_expr = ( + pl.col("flake_rate") * (pl.col("total_fail_count") + pl.col("total_pass_count")) + ).sum() / (pl.col("total_fail_count") + pl.col("total_pass_count")).sum() + + avg_duration_expr = ( + pl.col("avg_duration") + * (pl.col("total_pass_count") + pl.col("total_fail_count")) + ).sum() / (pl.col("total_pass_count") + pl.col("total_fail_count")).sum() + + # dedup + table = ( + table.group_by("name") + .agg( + pl.col("testsuite").alias("testsuite"), + pl.col("flags").explode().unique().alias("flags"), + failure_rate_expr.fill_nan(0).alias("failure_rate"), + flake_rate_expr.fill_nan(0).alias("flake_rate"), + pl.col("updated_at").max().alias("updated_at"), + avg_duration_expr.fill_nan(0).alias("avg_duration"), + pl.col("total_fail_count").sum().alias("total_fail_count"), + pl.col("total_flaky_fail_count").sum().alias("total_flaky_fail_count"), + pl.col("total_pass_count").sum().alias("total_pass_count"), + pl.col("total_skip_count").sum().alias("total_skip_count"), + pl.col("commits_where_fail").sum().alias("commits_where_fail"), + pl.col("last_duration").max().alias("last_duration"), + ) + .sort("name") + ) + + return table + + +def get_results( + repoid: int, + branch: str, + interval_start: int, + interval_end: int | None = None, +) -> pl.DataFrame | None: + """ + try redis + if redis is empty + try storage + if storage is empty + return None + else + cache to redis + deserialize + """ + # try redis + redis_conn = get_redis_connection() + key = redis_key(repoid, branch, interval_start, interval_end) + result: bytes | None = redis_conn.get(key) + + if result is None: + # try storage + storage_service = get_appropriate_storage_service(repoid) + key = storage_key(repoid, branch, interval_start, interval_end) + try: + result = storage_service.read_file( + bucket_name=settings.GCS_BUCKET_NAME, path=key + ) + # cache to redis + TaskService().cache_test_results_redis(repoid, branch) + except FileNotInStorageError: + # give up + return None + + # deserialize + table = pl.read_ipc(result) + + if table.height == 0: + return None + + table = dedup_table(table) + + return table diff --git a/apps/codecov-api/utils/test_utils.py b/apps/codecov-api/utils/test_utils.py new file mode 100644 index 0000000000..6cac27e04e --- /dev/null +++ b/apps/codecov-api/utils/test_utils.py @@ -0,0 +1,71 @@ +from typing import Any + +from django.apps import apps +from django.db import connection +from django.db.migrations.executor import MigrationExecutor +from django.test import TestCase +from django.test.client import Client as DjangoClient +from rest_framework.test import APIClient as DjangoAPIClient + +from codecov_auth.models import Owner + + +class BaseTestCase(object): + pass + + +class ClientMixin: + def force_login_owner(self, owner: Owner) -> None: + self.force_login(user=owner.user) + session = self.session + session["current_owner_id"] = owner.pk + session.save() + + def logout(self) -> None: + session = self.session + session["current_owner_id"] = None + session.save() + super().logout() # type: ignore + + +class Client(ClientMixin, DjangoClient): + pass + + +class APIClient(ClientMixin, DjangoAPIClient): + pass + + +class TestMigrations(TestCase): + @property + def app(self) -> str: + return apps.get_containing_app_config(type(self).__module__).name + + migrate_from = None + migrate_to = None + + def setUp(self) -> None: + assert self.migrate_from and self.migrate_to, ( + "TestCase '{}' must define migrate_from and migrate_to properties".format( + type(self).__name__ + ) + ) + self.migrate_from = [(self.app, self.migrate_from)] + self.migrate_to = [(self.app, self.migrate_to)] + executor = MigrationExecutor(connection) + old_apps = executor.loader.project_state(self.migrate_from).apps + + # Reverse to the original migration + executor.migrate(self.migrate_from) + + self.setUpBeforeMigration(old_apps) + + # Run the migration to test + executor = MigrationExecutor(connection) + executor.loader.build_graph() # reload. + executor.migrate(self.migrate_to) + + self.apps = executor.loader.project_state(self.migrate_to).apps + + def setUpBeforeMigration(self, apps: Any) -> None: + pass diff --git a/apps/codecov-api/utils/tests/unit/test_config.py b/apps/codecov-api/utils/tests/unit/test_config.py new file mode 100644 index 0000000000..41aa901e7c --- /dev/null +++ b/apps/codecov-api/utils/tests/unit/test_config.py @@ -0,0 +1,107 @@ +import pytest + +from utils.config import should_write_data_to_storage_config_check + + +@pytest.mark.parametrize( + "inner_config, func_args, result", + [ + ( + { + "repo_ids": [], + "report_details_files_array": True, + "commit_report": False, + }, + ("report_details_files_array", False, 1), + False, + ), + ( + { + "repo_ids": [], + "report_details_files_array": True, + "commit_report": False, + }, + ("report_details_files_array", True, 1), + True, + ), + ( + { + "repo_ids": [], + "report_details_files_array": True, + "commit_report": False, + }, + ("commit_report", True, 1), + False, + ), + ( + { + "repo_ids": [1], + "report_details_files_array": True, + "commit_report": False, + }, + ("commit_report", False, 1), + False, + ), + ( + { + "repo_ids": [1], + "report_details_files_array": True, # True is the same as "codecov_access" + "commit_report": False, + }, + ("report_details_files_array", False, 1), + False, + ), + ( + { + "repo_ids": [1], + "report_details_files_array": "codecov_access", + "commit_report": False, + }, + ("report_details_files_array", False, 1), + False, + ), + ( + { + "repo_ids": [1], + "report_details_files_array": "codecov_access", + "commit_report": False, + }, + ("report_details_files_array", True, 1), + True, + ), + ( + { + "repo_ids": [], + "report_details_files_array": "general_access", + "commit_report": False, + }, + ("report_details_files_array", False, 1), + True, + ), + ( + { + "repo_ids": [1], + "report_details_files_array": "restricted_access", + "commit_report": False, + }, + ("report_details_files_array", True, 1), + True, + ), + ], +) +def test_should_write_data_to_storage_config_check( + inner_config, func_args, result, mocker +): + config = {"setup": {"save_report_data_in_storage": inner_config}} + + def fake_config(*path, default=None): + curr = config + for key in path: + if key in curr: + curr = curr.get(key) + else: + return default + return curr + + mocker.patch("utils.config.get_config", side_effect=fake_config) + assert should_write_data_to_storage_config_check(*func_args) == result diff --git a/apps/codecov-api/utils/tests/unit/test_logging.py b/apps/codecov-api/utils/tests/unit/test_logging.py new file mode 100644 index 0000000000..940430f49a --- /dev/null +++ b/apps/codecov-api/utils/tests/unit/test_logging.py @@ -0,0 +1,19 @@ +from utils.logging_configuration import CustomLocalJsonFormatter + + +class TestLoggingConfig(object): + def test_local_formatter(self): + log_record = {"levelname": "weird_level", "message": "This is a message"} + cljf = CustomLocalJsonFormatter() + res = cljf.jsonify_log_record(log_record) + assert "weird_level: This is a message \n {}" == res + + def test_local_formatter_with_exc_info(self): + log_record = { + "levelname": "weird_level", + "message": "This is a message", + "exc_info": "Line\nWith\nbreaks", + } + cljf = CustomLocalJsonFormatter() + res = cljf.jsonify_log_record(log_record) + assert "weird_level: This is a message \n {}\nLine\nWith\nbreaks" == res diff --git a/apps/codecov-api/utils/tests/unit/test_repos.py b/apps/codecov-api/utils/tests/unit/test_repos.py new file mode 100644 index 0000000000..f2597ea0cc --- /dev/null +++ b/apps/codecov-api/utils/tests/unit/test_repos.py @@ -0,0 +1,22 @@ +from django.test import TestCase +from shared.django_apps.core.tests.factories import OwnerFactory, RepositoryFactory + +from utils.repos import get_bot_user + + +class RepoUtilsTests(TestCase): + def test_repo_bot_user_bot(self): + bot = OwnerFactory() + repo = RepositoryFactory(bot=bot) + assert get_bot_user(repo) == bot + + def test_repo_bot_user_author_bot(self): + bot = OwnerFactory() + author = OwnerFactory(bot=bot) + repo = RepositoryFactory(author=author) + assert get_bot_user(repo) == bot + + def test_repo_bot_user_author(self): + author = OwnerFactory() + repo = RepositoryFactory(author=author) + assert get_bot_user(repo) == author diff --git a/apps/codecov-api/utils/tests/unit/test_services.py b/apps/codecov-api/utils/tests/unit/test_services.py new file mode 100644 index 0000000000..125384027a --- /dev/null +++ b/apps/codecov-api/utils/tests/unit/test_services.py @@ -0,0 +1,42 @@ +from utils.services import get_long_service_name, get_short_service_name + + +class TestServices(object): + def test_gitlab(self): + service = get_long_service_name("gl") + assert service == "gitlab" + + service = get_long_service_name("gitlab") + assert service == "gitlab" + + service = get_short_service_name("gitlab") + assert service == "gl" + + service = get_short_service_name("gl") + assert service == "gl" + + def test_bb(self): + service = get_long_service_name("bb") + assert service == "bitbucket" + + service = get_long_service_name("bitbucket") + assert service == "bitbucket" + + service = get_short_service_name("bitbucket") + assert service == "bb" + + service = get_short_service_name("bb") + assert service == "bb" + + def test_gh(self): + service = get_long_service_name("gh") + assert service == "github" + + service = get_long_service_name("github") + assert service == "github" + + service = get_short_service_name("github") + assert service == "gh" + + service = get_short_service_name("gh") + assert service == "gh" diff --git a/apps/codecov-api/utils/version.py b/apps/codecov-api/utils/version.py new file mode 100644 index 0000000000..3775778725 --- /dev/null +++ b/apps/codecov-api/utils/version.py @@ -0,0 +1,5 @@ +import os + + +def get_current_version() -> str: + return os.getenv("RELEASE_VERSION", "NO_VERSION") diff --git a/apps/codecov-api/uv.lock b/apps/codecov-api/uv.lock new file mode 100644 index 0000000000..14591c73fa --- /dev/null +++ b/apps/codecov-api/uv.lock @@ -0,0 +1,1900 @@ +version = 1 +requires-python = ">=3.13" + +[[package]] +name = "aiodataloader" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/dd/e92b21d0c0d09bad4d41ce39def76deef63248c65e4ddc7ac2245ae0b2cc/aiodataloader-0.4.0.tar.gz", hash = "sha256:6de9ca0eb75c4eef686754679981ebd45a933391b40473f92419b8a747504169", size = 1629645 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/11/72e151269095f4ca73416526b017b2b7ddbcbe5181ca7b0176e3ffd09db3/aiodataloader-0.4.0-py3-none-any.whl", hash = "sha256:2775d8607e1b68ded82efc93c839846d0ae9d9e0421085e444f3c1c541f3c2b6", size = 10937 }, +] + +[[package]] +name = "amplitude-analytics" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/d8/4c35f6e844e4d967c35e130570f3f663acc09db5430eab78b9fddb48d5fb/amplitude_analytics-1.1.5.tar.gz", hash = "sha256:2836fcf88d75506a1f266a5582253fdf592e14fafaf5783017baeec2118369d4", size = 21787 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/c6/39cea4c4b5791312c81b7935ef2065d3e86ff050008c069a042f77f5c583/amplitude_analytics-1.1.5-py3-none-any.whl", hash = "sha256:da7db1985087d2b9ef0456115d116525888f416e174ef79b53e1532be7a0b3bd", size = 24138 }, +] + +[[package]] +name = "amqp" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/2c/6eb09fbdeb3c060b37bd33f8873832897a83e7a428afe01aad333fc405ec/amqp-5.2.0.tar.gz", hash = "sha256:a1ecff425ad063ad42a486c902807d1482311481c8ad95a72694b2975e75f7fd", size = 128754 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/f0/8e5be5d5e0653d9e1d02b1144efa33ff7d2963dfad07049e02c0fa9b2e8d/amqp-5.2.0-py3-none-any.whl", hash = "sha256:827cb12fb0baa892aad844fd95258143bce4027fdac4fccddbc43330fd281637", size = 50917 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/c4/fd50bbb2fb72532a4b778562e28ba581da15067cfb2537dbd3a2e64689c1/anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b", size = 140240 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/22/4cba7e1b4f45ffbefd2ca817a6800ba1c671c26f288d7705f20289872012/anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be", size = 80617 }, +] + +[[package]] +name = "ariadne" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "graphql-core" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/74/dc0232bc311a705c3cc642007bbbea817832c7fedb9655c870240804ad05/ariadne-0.23.0.tar.gz", hash = "sha256:84c46ae27fad98e1a6553c8930e6fc8e755f8dcd54a959d33caf04ecfe026a93", size = 75112 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/14/e2e13d6d2937f8fca9d220234f775e56119bce3cc3d6a807324faf5c80df/ariadne-0.23.0-py2.py3-none-any.whl", hash = "sha256:594206e4fc20fa05d6872f0ccd6e3aafe0575b9da36945211762445126b28bcb", size = 108265 }, +] + +[[package]] +name = "ariadne-django" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ariadne" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/f8/700f521eedc144978297bfbd8cfb67c2acfbe5b25f31a97f34f12ed8baf3/ariadne_django-0.3.0.tar.gz", hash = "sha256:19aca29d35398564ea59749a9c28b79cf11cf79f6fbb3d017d7e15d56157ae96", size = 15652 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/55/a57ab018b1c685cba22d8ca468f870cdbe299e4eb660e762947eaed5875d/ariadne_django-0.3.0-py3-none-any.whl", hash = "sha256:3c638993e2cf1d2f579ec8735a7dfdbb57d5420c8eb2ae09b93880746e2764c6", size = 18556 }, +] + +[[package]] +name = "asgiref" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/2d/797c0537426266d6c9377a2ed6a4ac61e50c2d5b1ab4da101a4b9bfe26e2/asgiref-3.6.0.tar.gz", hash = "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506", size = 32748 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/29/38d10a47b322a77b2d12c2b79c789f52956f733cb701d4d5157c76b5f238/asgiref-3.6.0-py3-none-any.whl", hash = "sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac", size = 23105 }, +] + +[[package]] +name = "async-timeout" +version = "4.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/6e/9678f7b2993537452710ffb1750c62d2c26df438aa621ad5fa9d1507a43a/async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15", size = 8221 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/c1/8991e7c5385b897b8c020cdaad718c5b087a6626d1d11a23e1ea87e325a7/async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c", size = 5763 }, +] + +[[package]] +name = "attrs" +version = "20.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/cb/80a4a274df7da7b8baf083249b0890a0579374c3d74b5ac0ee9291f912dc/attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700", size = 164523 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/aa/cb45262569fcc047bf070b5de61813724d6726db83259222cd7b4c79821a/attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", size = 49337 }, +] + +[[package]] +name = "billiard" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/52/f10d74fd56e73b430c37417658158ad8386202b069b70ff97d945c3ab67a/billiard-4.2.0.tar.gz", hash = "sha256:9a3c3184cb275aa17a732f93f65b20c525d3d9f253722d26a82194803ade5a2c", size = 154665 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/8d/6e9fdeeab04d803abc5a715175f87e88893934d5590595eacff23ca12b07/billiard-4.2.0-py3-none-any.whl", hash = "sha256:07aa978b308f334ff8282bd4a746e681b3513db5c9a514cbdd810cbbdc19714d", size = 86720 }, +] + +[[package]] +name = "boto3" +version = "1.37.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/3f/135ec0771e6d0e1af2ad7023a15df6677d96112072838d948c9b5075efe1/boto3-1.37.3.tar.gz", hash = "sha256:21f3ce0ef111297e63a6eb998a25197b8c10982970c320d4c6e8db08be2157be", size = 111160 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8c/213511a505af2239a673de4de145d013379275c569185187922f93dbdf14/boto3-1.37.3-py3-none-any.whl", hash = "sha256:2063b40af99fd02f6228ff52397b552ff3353831edaf8d25cc04801827ab9794", size = 139344 }, +] + +[[package]] +name = "botocore" +version = "1.37.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/fb/b243ab806d2e1e6b8a475b731cc59a1f1e4709eded4884b988a27bbc996b/botocore-1.37.3.tar.gz", hash = "sha256:fe8403eb55a88faf9b0f9da6615e5bee7be056d75e17af66c3c8f0a3b0648da4", size = 13574648 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/54/772118f15b5990173aa5264946cc8c9ff70c8f02d72ee6d63167a985188c/botocore-1.37.3-py3-none-any.whl", hash = "sha256:d01bd3bf4c80e61fa88d636ad9f5c9f60a551d71549b481386c6b4efe0bb2b2e", size = 13342066 }, +] + +[[package]] +name = "cachetools" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/c8/0b52cf3132b4b85c9e83faa3e4d375575afeb3a1710c40b2b2cd2a3e5635/cachetools-4.1.1.tar.gz", hash = "sha256:bbaa39c3dede00175df2dc2b03d0cf18dd2d32a7de7beb68072d13043c9edb20", size = 23574 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/5c/f3aa86b6d5482f3051b433c7616668a9b96fbe49a622210e2c9781938a5c/cachetools-4.1.1-py3-none-any.whl", hash = "sha256:513d4ff98dd27f85743a8dc0e92f55ddb1b49e060c2d5961512855cda2c01a98", size = 10960 }, +] + +[[package]] +name = "celery" +version = "5.3.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "billiard" }, + { name = "click" }, + { name = "click-didyoumean" }, + { name = "click-plugins" }, + { name = "click-repl" }, + { name = "kombu" }, + { name = "python-dateutil" }, + { name = "tzdata" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/72/45a2d2f9b45ccc6e80e2168ce169d17bf06a98711c192d7b53d5a8accf77/celery-5.3.6.tar.gz", hash = "sha256:870cc71d737c0200c397290d730344cc991d13a057534353d124c9380267aab9", size = 1544498 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/c2/4c8a67a4d98a6fcd55dbdd79b641f945d7f59637c3e885c4abbda3c431f6/celery-5.3.6-py3-none-any.whl", hash = "sha256:9da4ea0118d232ce97dff5ed4974587fb1c0ff5c10042eb15278487cdd27d1af", size = 422035 }, +] + +[[package]] +name = "cerberus" +version = "1.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/20/3ec65289ab0ccf5f41d38d103343f5b609263944238e299598aeba684a82/Cerberus-1.3.5.tar.gz", hash = "sha256:81011e10266ef71b6ec6d50e60171258a5b134d69f8fb387d16e4936d0d47642", size = 29898 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/17/335e0a4daf5475ada5eaa74735f765cc088ace306fbfdd2f4c2285320cc3/Cerberus-1.3.5-py3-none-any.whl", hash = "sha256:7649a5815024d18eb7c6aa5e7a95355c649a53aacfc9b050e9d0bf6bfa2af372", size = 30779 }, +] + +[[package]] +name = "certifi" +version = "2024.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/02/a95f2b11e207f68bc64d7aae9666fed2e2b3f307748d5123dffb72a1bbea/certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", size = 164065 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/d5/c84e1a17bf61d4df64ca866a1c9a913874b4e9bdc131ec689a0ad013fb36/certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90", size = 162960 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "cfgv" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/75/c80804e4a5eccc9acf767faf4591bb7ab289485ba236dfee542467dc7c9b/cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1", size = 7851 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/cd/3878c9248e59e5e2ebd0dc741ab984b18d86e7283ae9b127b05fc287d239/cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", size = 7277 }, +] + +[[package]] +name = "charset-normalizer" +version = "2.0.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/e4/e014e7360fc6d1ccc507fe0b563b4646d00e0d4f9beec4975026dd15850b/charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c", size = 75753 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/84/b06f6729fac8108c5fa3e13cde19b0b3de66ba5538c325496dbe39f5ff8e/charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", size = 39257 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[package]] +name = "click-didyoumean" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/a7/822fbc659be70dcb75a91fb91fec718b653326697d0e9907f4f90114b34f/click-didyoumean-0.3.0.tar.gz", hash = "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035", size = 2405 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/36/4599267417fc78b587b1588e0647a468c60b36c02bb723d450d050738fa8/click_didyoumean-0.3.0-py3-none-any.whl", hash = "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667", size = 2730 }, +] + +[[package]] +name = "click-plugins" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/1d/45434f64ed749540af821fd7e42b8e4d23ac04b1eda7c26613288d6cd8a8/click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", size = 8164 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/da/824b92d9942f4e472702488857914bdd50f73021efea15b4cad9aca8ecef/click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8", size = 7497 }, +] + +[[package]] +name = "click-repl" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "prompt-toolkit" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/30/11d3f09eff5ae3627bca79563855035e8d241444520500a3c7914eae6a74/click-repl-0.2.0.tar.gz", hash = "sha256:cd12f68d745bf6151210790540b4cb064c7b13e571bc64b6957d98d120dacfd8", size = 5743 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/33/15f401400cc0cf2470aa777d225e772f83a68541495e015d2fa5c77d33d0/click_repl-0.2.0-py3-none-any.whl", hash = "sha256:94b3fbbc9406a236f176e0506524b2937e4b23b6f4c0c0b2a0a83f8a64e9194b", size = 5163 }, +] + +[[package]] +name = "codecov-api" +version = "0.0.1" +source = { editable = "." } +dependencies = [ + { name = "aiodataloader" }, + { name = "ariadne" }, + { name = "ariadne-django" }, + { name = "celery" }, + { name = "cerberus" }, + { name = "certifi" }, + { name = "django" }, + { name = "django-autocomplete-light" }, + { name = "django-better-admin-arrayfield" }, + { name = "django-cors-headers" }, + { name = "django-csp" }, + { name = "django-cursor-pagination" }, + { name = "django-filter" }, + { name = "django-model-utils" }, + { name = "django-postgres-extra" }, + { name = "django-prometheus" }, + { name = "djangorestframework" }, + { name = "drf-spectacular" }, + { name = "drf-spectacular-sidecar" }, + { name = "google-cloud-pubsub" }, + { name = "grpcio" }, + { name = "gunicorn" }, + { name = "idna" }, + { name = "minio" }, + { name = "multidict" }, + { name = "polars" }, + { name = "psycopg2-binary" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "python-dateutil" }, + { name = "python-json-logger" }, + { name = "python-redis-lock" }, + { name = "pytz" }, + { name = "redis" }, + { name = "regex" }, + { name = "requests" }, + { name = "sentry-sdk", extra = ["celery"] }, + { name = "shared" }, + { name = "simplejson" }, + { name = "starlette" }, + { name = "stripe" }, + { name = "whitenoise" }, +] + +[package.dev-dependencies] +dev = [ + { name = "factory-boy" }, + { name = "fakeredis" }, + { name = "freezegun" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "pytest-insta" }, + { name = "pytest-mock" }, + { name = "ruff" }, + { name = "urllib3" }, + { name = "vcrpy" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiodataloader", specifier = "==0.4.0" }, + { name = "ariadne", specifier = "==0.23.0" }, + { name = "ariadne-django", specifier = "==0.3.0" }, + { name = "celery", specifier = ">=5.3.6" }, + { name = "cerberus", specifier = "==1.3.5" }, + { name = "certifi", specifier = ">=2024.7.4" }, + { name = "django", specifier = ">=4.2.16" }, + { name = "django-autocomplete-light", specifier = "==3.11.0" }, + { name = "django-better-admin-arrayfield", specifier = "==1.4.2" }, + { name = "django-cors-headers", specifier = "==3.7.0" }, + { name = "django-csp", specifier = "==3.8.0" }, + { name = "django-cursor-pagination", specifier = "==0.3.0" }, + { name = "django-filter", specifier = "==2.4.0" }, + { name = "django-model-utils", specifier = "==4.5.1" }, + { name = "django-postgres-extra", specifier = ">=2.0.8" }, + { name = "django-prometheus", specifier = "==2.3.1" }, + { name = "djangorestframework", specifier = "==3.15.2" }, + { name = "drf-spectacular", specifier = "==0.26.2" }, + { name = "drf-spectacular-sidecar", specifier = "==2023.3.1" }, + { name = "google-cloud-pubsub", specifier = ">=2.18.4" }, + { name = "grpcio", specifier = ">=1.66.2" }, + { name = "gunicorn", specifier = ">=22.0.0" }, + { name = "idna", specifier = ">=3.7" }, + { name = "minio", specifier = "==7.1.13" }, + { name = "multidict", specifier = ">=6.1.0" }, + { name = "polars", specifier = "==1.12.0" }, + { name = "psycopg2-binary", specifier = ">=2.9.10" }, + { name = "pydantic", specifier = ">=2.9.0" }, + { name = "pyjwt", specifier = ">=2.4.0" }, + { name = "python-dateutil", specifier = "==2.9.0.post0" }, + { name = "python-json-logger", specifier = "==2.0.7" }, + { name = "python-redis-lock", specifier = "==4.0.0" }, + { name = "pytz", specifier = "==2022.1" }, + { name = "redis", specifier = "==4.4.4" }, + { name = "regex", specifier = "==2023.12.25" }, + { name = "requests", specifier = "==2.32.3" }, + { name = "sentry-sdk", specifier = ">=2.13.0" }, + { name = "sentry-sdk", extras = ["celery"], specifier = "==2.13.0" }, + { name = "shared", directory = "../../libs/shared" }, + { name = "simplejson", specifier = "==3.17.2" }, + { name = "starlette", specifier = "==0.40.0" }, + { name = "stripe", specifier = ">=11.4.1" }, + { name = "whitenoise", specifier = "==5.2.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "factory-boy", specifier = ">=3.2.0" }, + { name = "fakeredis", specifier = "==2.10.3" }, + { name = "freezegun", specifier = ">=1.5.1" }, + { name = "pre-commit", specifier = ">=3.4.0" }, + { name = "pytest", specifier = ">=8.1.1" }, + { name = "pytest-asyncio", specifier = ">=0.14.0" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "pytest-django", specifier = "==4.8.0" }, + { name = "pytest-insta", specifier = ">=0.3.0" }, + { name = "pytest-mock", specifier = "==3.14.0" }, + { name = "ruff", specifier = ">=0.9.6" }, + { name = "urllib3", specifier = "==1.26.19" }, + { name = "vcrpy", specifier = ">=6.0.1" }, +] + +[[package]] +name = "codecov-ribs" +version = "0.1.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/0c/da1d2367c86ccebbcd60d77a697a816b9b226031528b92c5e751a82b498d/codecov_ribs-0.1.18.tar.gz", hash = "sha256:004590ee88515aa2ee3e1ad0a05ed616a6caffcedd9effe3454efe9d927c2a46", size = 31935 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/bc/e39377099e929bd4da67e9dde23e9e4b1f765e80174a4fcbd76066ab85bc/codecov_ribs-0.1.18-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:00fe34c01f72cfb967d9abfa0a19ac65485564f1cdfc7ea634362f0f8031a57a", size = 449013 }, + { url = "https://files.pythonhosted.org/packages/cd/26/27b1404572ece21c3cfcaebcacc77accdef87fd7f3799ae75ec9a3a986a6/codecov_ribs-0.1.18-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:509c3b5ffedb75590e5efff9fe44fa38fb9a330f3cfd2cfdbffe03ba9baf2c6f", size = 435904 }, + { url = "https://files.pythonhosted.org/packages/90/e4/aac82889bf1233e65c05d3182f0f009f9fec7c3938143a627082a4f470dd/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:94676137c0a84fe434b6fa9d2e51e8301839b704f0561642e3104def7fdd186a", size = 1277625 }, + { url = "https://files.pythonhosted.org/packages/46/b3/5b9677fa973a25942538c143bb1184bfd13ee838ade1f62836bae1ab4f8e/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:078a3c5b23852142ce0c362e5e61dc3e76be28820000cf8d62f0dfa02e0b9fd1", size = 1230640 }, + { url = "https://files.pythonhosted.org/packages/48/c6/85c6d4ad866d3ed860ec4d371eaa4d55a3937dd21315d7258d6968623748/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ba82a37648c42b568513c5b8c4044c45f1dc4240a9c53259687cc2e869f415b", size = 1243114 }, + { url = "https://files.pythonhosted.org/packages/15/30/919304e23d9599674887ae112d362e6a0b96cdb120a1766c2c0ea30bff17/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb7aeb1fce52ae4e61ad4fa1f16d9d6c5429b77c037800eec8a58586020a3b1", size = 1340437 }, + { url = "https://files.pythonhosted.org/packages/53/55/d71a0ebf5363af6eacaf60cbf2db33d4c2e7962c8734172e6f37e378b153/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7f87052b56e5346ab2d89cad8cb801074faaca4cb9d351097e5a7a54a2af424", size = 1432376 }, + { url = "https://files.pythonhosted.org/packages/44/26/ee24c4f22de09d04d242e21dfb4877469735a6a7d55c052b3bbcb782e541/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08db4118a7350f76c53fc0ba04110b606a3178fdd1acb6946cd1c0b9fea777b5", size = 1242811 }, + { url = "https://files.pythonhosted.org/packages/00/ad/e86b5967ee91e93604554a5c9ea35175330d84e52f781a8d1295d63150e9/codecov_ribs-0.1.18-cp38-abi3-win32.whl", hash = "sha256:a7db0d0afa593f155e34ae722703b8b21840c85a94cc90e8550173610d346d0f", size = 290807 }, + { url = "https://files.pythonhosted.org/packages/d1/e3/b1e2b06b1d7e047318608fb2b7268d594da0798b166eb6b38ae7d8c3f41f/codecov_ribs-0.1.18-cp38-abi3-win_amd64.whl", hash = "sha256:dd7a799c1eea76caebf59f4a9a3a5cda0fff7999d49452235c1db5f1d654d4b8", size = 305137 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "colour" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/d4/5911a7618acddc3f594ddf144ecd8a03c29074a540f4494670ad8f153efe/colour-0.1.5.tar.gz", hash = "sha256:af20120fefd2afede8b001fbef2ea9da70ad7d49fafdb6489025dae8745c3aee", size = 24776 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/46/e81907704ab203206769dee1385dc77e1407576ff8f50a0681d0a6b541be/colour-0.1.5-py2.py3-none-any.whl", hash = "sha256:33f6db9d564fadc16e59921a56999b79571160ce09916303d35346dddc17978c", size = 23772 }, +] + +[[package]] +name = "coverage" +version = "7.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/1d/3e426526df9f320f0a08dd08434dc7b94cd621543f73f96d8a4faf216ec7/coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c", size = 784825 } + +[[package]] +name = "cryptography" +version = "44.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/67/545c79fe50f7af51dbad56d16b23fe33f63ee6a5d956b3cb68ea110cbe64/cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14", size = 710819 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/27/5e3524053b4c8889da65cf7814a9d0d8514a05194a25e1e34f46852ee6eb/cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009", size = 6642022 }, + { url = "https://files.pythonhosted.org/packages/34/b9/4d1fa8d73ae6ec350012f89c3abfbff19fc95fe5420cf972e12a8d182986/cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f", size = 3943865 }, + { url = "https://files.pythonhosted.org/packages/6e/57/371a9f3f3a4500807b5fcd29fec77f418ba27ffc629d88597d0d1049696e/cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2", size = 4162562 }, + { url = "https://files.pythonhosted.org/packages/c5/1d/5b77815e7d9cf1e3166988647f336f87d5634a5ccecec2ffbe08ef8dd481/cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911", size = 3951923 }, + { url = "https://files.pythonhosted.org/packages/28/01/604508cd34a4024467cd4105887cf27da128cba3edd435b54e2395064bfb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69", size = 3685194 }, + { url = "https://files.pythonhosted.org/packages/c6/3d/d3c55d4f1d24580a236a6753902ef6d8aafd04da942a1ee9efb9dc8fd0cb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026", size = 4187790 }, + { url = "https://files.pythonhosted.org/packages/ea/a6/44d63950c8588bfa8594fd234d3d46e93c3841b8e84a066649c566afb972/cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd", size = 3951343 }, + { url = "https://files.pythonhosted.org/packages/c1/17/f5282661b57301204cbf188254c1a0267dbd8b18f76337f0a7ce1038888c/cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0", size = 4187127 }, + { url = "https://files.pythonhosted.org/packages/f3/68/abbae29ed4f9d96596687f3ceea8e233f65c9645fbbec68adb7c756bb85a/cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf", size = 4070666 }, + { url = "https://files.pythonhosted.org/packages/0f/10/cf91691064a9e0a88ae27e31779200b1505d3aee877dbe1e4e0d73b4f155/cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864", size = 4288811 }, + { url = "https://files.pythonhosted.org/packages/38/78/74ea9eb547d13c34e984e07ec8a473eb55b19c1451fe7fc8077c6a4b0548/cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a", size = 2771882 }, + { url = "https://files.pythonhosted.org/packages/cf/6c/3907271ee485679e15c9f5e93eac6aa318f859b0aed8d369afd636fafa87/cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00", size = 3206989 }, + { url = "https://files.pythonhosted.org/packages/9f/f1/676e69c56a9be9fd1bffa9bc3492366901f6e1f8f4079428b05f1414e65c/cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008", size = 6643714 }, + { url = "https://files.pythonhosted.org/packages/ba/9f/1775600eb69e72d8f9931a104120f2667107a0ee478f6ad4fe4001559345/cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862", size = 3943269 }, + { url = "https://files.pythonhosted.org/packages/25/ba/e00d5ad6b58183829615be7f11f55a7b6baa5a06910faabdc9961527ba44/cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3", size = 4166461 }, + { url = "https://files.pythonhosted.org/packages/b3/45/690a02c748d719a95ab08b6e4decb9d81e0ec1bac510358f61624c86e8a3/cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7", size = 3950314 }, + { url = "https://files.pythonhosted.org/packages/e6/50/bf8d090911347f9b75adc20f6f6569ed6ca9b9bff552e6e390f53c2a1233/cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a", size = 3686675 }, + { url = "https://files.pythonhosted.org/packages/e1/e7/cfb18011821cc5f9b21efb3f94f3241e3a658d267a3bf3a0f45543858ed8/cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c", size = 4190429 }, + { url = "https://files.pythonhosted.org/packages/07/ef/77c74d94a8bfc1a8a47b3cafe54af3db537f081742ee7a8a9bd982b62774/cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62", size = 3950039 }, + { url = "https://files.pythonhosted.org/packages/6d/b9/8be0ff57c4592382b77406269b1e15650c9f1a167f9e34941b8515b97159/cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41", size = 4189713 }, + { url = "https://files.pythonhosted.org/packages/78/e1/4b6ac5f4100545513b0847a4d276fe3c7ce0eacfa73e3b5ebd31776816ee/cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b", size = 4071193 }, + { url = "https://files.pythonhosted.org/packages/3d/cb/afff48ceaed15531eab70445abe500f07f8f96af2bb35d98af6bfa89ebd4/cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7", size = 4289566 }, + { url = "https://files.pythonhosted.org/packages/30/6f/4eca9e2e0f13ae459acd1ca7d9f0257ab86e68f44304847610afcb813dc9/cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9", size = 2772371 }, + { url = "https://files.pythonhosted.org/packages/d2/05/5533d30f53f10239616a357f080892026db2d550a40c393d0a8a7af834a9/cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f", size = 3207303 }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + +[[package]] +name = "django" +version = "4.2.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/d8/a607ee443b54a4db4ad28902328b906ae6218aa556fb9b3ac45c0bcb313d/Django-4.2.16.tar.gz", hash = "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad", size = 10436023 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/2c/6b6c7e493d5ea789416918658ebfa16be7a64c77610307497ed09a93c8c4/Django-4.2.16-py3-none-any.whl", hash = "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898", size = 7992936 }, +] + +[[package]] +name = "django-autocomplete-light" +version = "3.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/32/004073fb1bc55770bc9111bac42f17f481e17d4d20bbab2b5a8b921b95a6/django-autocomplete-light-3.11.0.tar.gz", hash = "sha256:212576a17e3308ef7ca77e280b86684167916d2091d4b73640f38845d9516328", size = 173786 } + +[[package]] +name = "django-better-admin-arrayfield" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/4f/e8bdf86d5bba2622d585bec983a0b59f373f4ed00eb948a6907ba35e5585/django-better-admin-arrayfield-1.4.2.tar.gz", hash = "sha256:b45423e51bbc0aa31ef658248c058ca8b533a541be4dee9fb8bcd059f8a10a58", size = 13857 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/75/8fc6cfc73c456283c89a80bd8319df61aabcd273cfa37697bc31a00e387b/django_better_admin_arrayfield-1.4.2-py2.py3-none-any.whl", hash = "sha256:bfeaa0fa8210a7ea95ee996a6caaa59ecd0c923269f573e6d8319c28dcac5c88", size = 13931 }, +] + +[[package]] +name = "django-cors-headers" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/69/83ac503117bca6ca013b5301c3d402fb24fcc356b4f915f9d7897ff10c48/django-cors-headers-3.7.0.tar.gz", hash = "sha256:96069c4aaacace786a34ee7894ff680780ec2644e4268b31181044410fecd12e", size = 88367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/72/5a8907328d6b508a2c7f90a9f3d5d17f368cb5a180169078d7bf7514919e/django_cors_headers-3.7.0-py3-none-any.whl", hash = "sha256:1ac2b1213de75a251e2ba04448da15f99bcfcbe164288ae6b5ff929dc49b372f", size = 12402 }, +] + +[[package]] +name = "django-csp" +version = "3.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/16/c3c65ad59997284402e54d00797c7aca96572df911aede3e1f2cc2e029f8/django_csp-3.8.tar.gz", hash = "sha256:ef0f1a9f7d8da68ae6e169c02e9ac661c0ecf04db70e0d1d85640512a68471c0", size = 13341 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/ff/2c7a4b6706125a17bd0071802e4894c28772cfcdea20a086a2be3c5fafda/django_csp-3.8-py3-none-any.whl", hash = "sha256:19b2978b03fcd73517d7d67acbc04fbbcaec0facc3e83baa502965892d1e0719", size = 17410 }, +] + +[[package]] +name = "django-cursor-pagination" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/91/73adf878757d3f10c8882e532cbcf1feda4a788bfe8088a03475486ff97c/django_cursor_pagination-0.3.0.tar.gz", hash = "sha256:b09293ea9aa93cd0f3a9f4197e1f11f09283678e6c991cf4d4517a0fe90244c1", size = 7907 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ae/da7b25f23c97cdc05f97f83555653b5e4b69dee20ed0da5e9cce136d4e00/django_cursor_pagination-0.3.0-py3-none-any.whl", hash = "sha256:ce88147adc1e41c58427217cf54d7cbb4d04d5cf0a3a2c794d81602ad347658e", size = 6822 }, +] + +[[package]] +name = "django-filter" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/cf/adae3e55995ea27e1dceb493e0226557d4207d8819ddb99591df5204a471/django-filter-2.4.0.tar.gz", hash = "sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06", size = 146904 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/2b/b2fe483c3095b6222725dd05f9ad9e6ed6cb7347c154fdbd80238d36f1a8/django_filter-2.4.0-py3-none-any.whl", hash = "sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1", size = 73156 }, +] + +[[package]] +name = "django-model-utils" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/f3/77be195e7518b8a652c052cdc42e60135b56a9a460aa3bd8cb08d751c322/django_model_utils-4.5.1.tar.gz", hash = "sha256:1220f22d9a467d53a1e0f4cda4857df0b2f757edf9a29955c42461988caa648a", size = 75980 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/c5/5d14bf2d55cb843e556b6283a2fbd6ea531cf7d2fa7b8d4f6d3abfada672/django_model_utils-4.5.1-py3-none-any.whl", hash = "sha256:f1141fc71796242edeffed5ad53a8cc57f00d345eb5a3a63e3f69401cd562ee2", size = 39341 }, +] + +[[package]] +name = "django-postgres-extra" +version = "2.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/8a/2bf9f570e73058569e48cc3208f007fd7ff45cb28e178ea1da2b188f14e2/django-postgres-extra-2.0.8.tar.gz", hash = "sha256:9efa08c6f18ed34460af41c6f679bb375b93d12544b1105aa348b787a30b46eb", size = 48572 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/22/559559d7894648bd7f2bbfdeedd5ccba4f8d9de68855ced522011d608af5/django_postgres_extra-2.0.8-py3-none-any.whl", hash = "sha256:447d5a971759943ee63a9d4cef9c6c1fa290e518611ea521a38b6732681d2f3a", size = 75205 }, +] + +[[package]] +name = "django-prometheus" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prometheus-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/51/485b4122e00f2b8efec8a6d718ef4ce6b150231e49398e554ce1151f65c3/django-prometheus-2.3.1.tar.gz", hash = "sha256:f9c8b6c780c9419ea01043c63a437d79db2c33353451347894408184ad9c3e1e", size = 24718 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/75/fb3d4f056f9ed4f8848817d5afd7a1d949632ab117452ccd179e3839cfc4/django_prometheus-2.3.1-py2.py3-none-any.whl", hash = "sha256:cf9b26f7ba2e4568f08f8f91480a2882023f5908579681bcf06a4d2465f12168", size = 29081 }, +] + +[[package]] +name = "djangorestframework" +version = "3.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/ce/31482eb688bdb4e271027076199e1aa8d02507e530b6d272ab8b4481557c/djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad", size = 1067420 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/b6/fa99d8f05eff3a9310286ae84c4059b08c301ae4ab33ae32e46e8ef76491/djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20", size = 1071235 }, +] + +[[package]] +name = "drf-spectacular" +version = "0.26.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "djangorestframework" }, + { name = "inflection" }, + { name = "jsonschema" }, + { name = "pyyaml" }, + { name = "uritemplate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/8b/cf2b8770d1eb563bb91b566c8494b2d5fdeac5991ddd7e654eb21f3671f7/drf-spectacular-0.26.2.tar.gz", hash = "sha256:005623d6bb9de37d2d0ec24ccd59c636e4a42f9af252f1470129ac32ccab38cb", size = 216281 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/6c/064c318c78a9ac36c802bd284612a77721301c8d2866fb10a35194d9cb28/drf_spectacular-0.26.2-py3-none-any.whl", hash = "sha256:e80eba58d9579bf6c3380ffd6d6a9b466c4bc35b23da0ba76dfcc96de1e907d7", size = 92913 }, +] + +[[package]] +name = "drf-spectacular-sidecar" +version = "2023.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/91/054e90af66573d2795480811b0766b253c68c26978a214251069af9f88bd/drf-spectacular-sidecar-2023.3.1.tar.gz", hash = "sha256:ceb78fd59971bb79e90de38bf89afa50a60b043953f72b3cdeb4ca6a34623f92", size = 2472749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b2/5f3e768a183e0927b893b8010b02798c6d3cdc8d8ae40a480fbe5367bfb4/drf_spectacular_sidecar-2023.3.1-py3-none-any.whl", hash = "sha256:2b5ea98d976a4ba023d03cd9dfc2506892e0f26e2ba2869b58ddf344ab69f40f", size = 2494623 }, +] + +[[package]] +name = "factory-boy" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "faker" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/37/b046abce36a5fc19827e3662fd6c63ae3489b93f96dbd099cd412bbff6c3/factory_boy-3.2.0.tar.gz", hash = "sha256:401cc00ff339a022f84d64a4339503d1689e8263a4478d876e58a3295b155c5b", size = 153844 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/62/41a5a7ed8a474072d491521562ffbeb18a869c090c21506f890298433fab/factory_boy-3.2.0-py2.py3-none-any.whl", hash = "sha256:1d3db4b44b8c8c54cdd8b83ae4bdb9aeb121e464400035f1f03ae0e1eade56a4", size = 35313 }, +] + +[[package]] +name = "faker" +version = "4.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/1f/c7d5429347075b799576b9509926ff0fd72cffae2c34d1191ee8342c7b68/Faker-4.1.3.tar.gz", hash = "sha256:075a95ac4c95765370919d787dcd958acfaea635005ad5af4d926cb0973800db", size = 1000270 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/3c/4fc4a53a24c0ae040616815eb18e73b00832d2eb9275da3837c8345c68a6/Faker-4.1.3-py3-none-any.whl", hash = "sha256:80bab8d46035a7393de827210c5d39c17109d3346d131946bde622137120c496", size = 1039477 }, +] + +[[package]] +name = "fakeredis" +version = "2.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/16/981975de0d325255f9b05b6244a53e7a95b19c5104f09315068039263efc/fakeredis-2.10.3.tar.gz", hash = "sha256:c5dcb070ef3219226e1d6db8836ddad47da1fc821270f6e89cfeb5da1f7f2e38", size = 94025 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/7f/e4fdf244f46f08f2846334867dc2304ace61c768863283a503721bae3f67/fakeredis-2.10.3-py3-none-any.whl", hash = "sha256:078ad729fe7cbcc84c9ff6f25c0e503fd4e19db6956f78049f9991b10c5271ba", size = 57471 }, +] + +[[package]] +name = "filelock" +version = "3.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e", size = 18027 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 }, +] + +[[package]] +name = "freezegun" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/ef/722b8d71ddf4d48f25f6d78aa2533d505bf3eec000a7cacb8ccc8de61f2f/freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9", size = 33697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/0b/0d7fee5919bccc1fdc1c2a7528b98f65c6f69b223a3fd8f809918c142c36/freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1", size = 17569 }, +] + +[[package]] +name = "google-api-core" +version = "2.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/6b/b98553c2061c4e2186f5bbfb1aa1a6ef13fc0775c096d18595d3c99ba023/google_api_core-2.23.0.tar.gz", hash = "sha256:2ceb087315e6af43f256704b871d99326b1f12a9d6ce99beaedec99ba26a0ace", size = 160094 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/a4/c26886d57d90032c5f74c2e80aefdc38ec58551fc46bd4ce79fb2c9389fa/google_api_core-2.23.0-py3-none-any.whl", hash = "sha256:c20100d4c4c41070cf365f1d8ddf5365915291b5eb11b83829fbd1c999b5122f", size = 156554 }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-auth" +version = "2.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/71/4c5387d8a3e46e3526a8190ae396659484377a73b33030614dd3b28e7ded/google_auth-2.36.0.tar.gz", hash = "sha256:545e9618f2df0bcbb7dcbc45a546485b1212624716975a1ea5ae8149ce769ab1", size = 268336 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3d5087d27865c2f0431b942b5c4500b7d1b744dd3262fdc973a4c39d099e/google_auth-2.36.0-py2.py3-none-any.whl", hash = "sha256:51a15d47028b66fd36e5c64a82d2d57480075bccc7da37cde257fc94177a61fb", size = 209519 }, +] + +[[package]] +name = "google-cloud-core" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/96/16cc0a34f75899ace6a42bb4ef242ac4aa263089b018d1c18c007d1fd8f2/google_cloud_core-2.4.2.tar.gz", hash = "sha256:a4fcb0e2fcfd4bfe963837fad6d10943754fd79c1a50097d68540b6eb3d67f35", size = 35854 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/0f/76e813cee7568ac467d929f4f0da7ab349596e7fc4ee837b990611e07d99/google_cloud_core-2.4.2-py2.py3-none-any.whl", hash = "sha256:7459c3e83de7cb8b9ecfec9babc910efb4314030c56dd798eaad12c426f7d180", size = 29343 }, +] + +[[package]] +name = "google-cloud-pubsub" +version = "2.18.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "grpcio-status" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/21/c934b995208027434c15df104e342b41407bb7aeb23b19d4f73bc93a1961/google-cloud-pubsub-2.18.4.tar.gz", hash = "sha256:32eb61fd4c1dc6c842f594d69d9afa80544e3b327aa640a164eb6fb0201eaf2d", size = 309745 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/58/fbdedd07206514f09f029458d95d391829b09399c1459d8892e6e20022d1/google_cloud_pubsub-2.18.4-py2.py3-none-any.whl", hash = "sha256:f32144ad9ed32331a80a2f8379a3ca7526bbc01e7bd76de2e8ab52e492d21f50", size = 265939 }, +] + +[[package]] +name = "google-cloud-storage" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "google-resumable-media" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/d7/dfa74049c4faa3b4d68fa1a10a7eab5a76c57d0788b47c27f927bedc606d/google_cloud_storage-3.0.0.tar.gz", hash = "sha256:2accb3e828e584888beff1165e5f3ac61aa9088965eb0165794a82d8c7f95297", size = 7665253 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/ae/1a50f07161301e40a30b2e40744a7b85ffab7add16e044417925eccf9bbf/google_cloud_storage-3.0.0-py2.py3-none-any.whl", hash = "sha256:f85fd059650d2dbb0ac158a9a6b304b66143b35ed2419afec2905ca522eb2c6a", size = 173860 }, +] + +[[package]] +name = "google-crc32c" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/72/c3298da1a3773102359c5a78f20dae8925f5ea876e37354415f68594a6fb/google_crc32c-1.6.0.tar.gz", hash = "sha256:6eceb6ad197656a1ff49ebfbbfa870678c75be4344feb35ac1edf694309413dc", size = 14472 } + +[[package]] +name = "google-resumable-media" +version = "2.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-crc32c" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/5a/0efdc02665dca14e0837b62c8a1a93132c264bd02054a15abb2218afe0ae/google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0", size = 2163099 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/35/b8d3baf8c46695858cb9d8835a53baa1eeb9906ddaf2f728a5f5b640fd1e/google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa", size = 81251 }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.59.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/9b/ea531afe585da044686ab13351c99dfbb2ca02b96c396874946d52d0e127/googleapis-common-protos-1.59.1.tar.gz", hash = "sha256:b35d530fe825fb4227857bc47ad84c33c809ac96f312e13182bdeaa2abe1178a", size = 118520 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/b7/bbaa556e9ff0580f408c64ccf4db0c1414eec79e7151d33a10bc209ffb6d/googleapis_common_protos-1.59.1-py2.py3-none-any.whl", hash = "sha256:0cbedb6fb68f1c07e18eb4c48256320777707e7d0c55063ae56c15db3224a61e", size = 224487 }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, +] + +[[package]] +name = "graphql-core" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/a6/94df9045ca1bac404c7b394094cd06713f63f49c7a4d54d99b773ae81737/graphql-core-3.2.3.tar.gz", hash = "sha256:06d2aad0ac723e35b1cb47885d3e5c45e956a53bc1b209a9fc5369007fe46676", size = 529552 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/39/e5143e7ec70939d2076c1165ae9d4a3815597019c4d797b7f959cf778600/graphql_core-3.2.3-py3-none-any.whl", hash = "sha256:5766780452bd5ec8ba133f8bf287dc92713e3868ddd83aee4faab9fc3e303dc3", size = 202921 }, +] + +[[package]] +name = "greenlet" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 }, + { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 }, + { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 }, + { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 }, + { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 }, + { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 }, + { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 }, + { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 }, + { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 }, + { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 }, + { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 }, + { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 }, + { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 }, + { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 }, + { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 }, + { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, +] + +[[package]] +name = "grpc-google-iam-v1" +version = "0.12.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", extra = ["grpc"] }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/92/aee864f03f47c672a31d128e49981b01ca629d81541dcc9904c652dbab5b/grpc-google-iam-v1-0.12.6.tar.gz", hash = "sha256:2bc4b8fdf22115a65d751c9317329322602c39b7c86a289c9b72d228d960ef5f", size = 17760 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/72/c84e54991d452942c5a85474693c8433169104a596e9dd23b05c5f091894/grpc_google_iam_v1-0.12.6-py2.py3-none-any.whl", hash = "sha256:5c10f3d8dc2d88678ab1a9b0cb5482735c5efee71e6c0cd59f872eef22913f5c", size = 26266 }, +] + +[[package]] +name = "grpcio" +version = "1.70.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/e1/4b21b5017c33f3600dcc32b802bb48fe44a4d36d6c066f52650c7c2690fa/grpcio-1.70.0.tar.gz", hash = "sha256:8d1584a68d5922330025881e63a6c1b54cc8117291d382e4fa69339b6d914c56", size = 12788932 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/38/66d0f32f88feaf7d83f8559cd87d899c970f91b1b8a8819b58226de0a496/grpcio-1.70.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:aa573896aeb7d7ce10b1fa425ba263e8dddd83d71530d1322fd3a16f31257b4a", size = 5199218 }, + { url = "https://files.pythonhosted.org/packages/c1/96/947df763a0b18efb5cc6c2ae348e56d97ca520dc5300c01617b234410173/grpcio-1.70.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:d405b005018fd516c9ac529f4b4122342f60ec1cee181788249372524e6db429", size = 11445983 }, + { url = "https://files.pythonhosted.org/packages/fd/5b/f3d4b063e51b2454bedb828e41f3485800889a3609c49e60f2296cc8b8e5/grpcio-1.70.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f32090238b720eb585248654db8e3afc87b48d26ac423c8dde8334a232ff53c9", size = 5663954 }, + { url = "https://files.pythonhosted.org/packages/bd/0b/dab54365fcedf63e9f358c1431885478e77d6f190d65668936b12dd38057/grpcio-1.70.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfa089a734f24ee5f6880c83d043e4f46bf812fcea5181dcb3a572db1e79e01c", size = 6304323 }, + { url = "https://files.pythonhosted.org/packages/76/a8/8f965a7171ddd336ce32946e22954aa1bbc6f23f095e15dadaa70604ba20/grpcio-1.70.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f19375f0300b96c0117aca118d400e76fede6db6e91f3c34b7b035822e06c35f", size = 5910939 }, + { url = "https://files.pythonhosted.org/packages/1b/05/0bbf68be8b17d1ed6f178435a3c0c12e665a1e6054470a64ce3cb7896596/grpcio-1.70.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:7c73c42102e4a5ec76608d9b60227d917cea46dff4d11d372f64cbeb56d259d0", size = 6631405 }, + { url = "https://files.pythonhosted.org/packages/79/6a/5df64b6df405a1ed1482cb6c10044b06ec47fd28e87c2232dbcf435ecb33/grpcio-1.70.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:0a5c78d5198a1f0aa60006cd6eb1c912b4a1520b6a3968e677dbcba215fabb40", size = 6190982 }, + { url = "https://files.pythonhosted.org/packages/42/aa/aeaac87737e6d25d1048c53b8ec408c056d3ed0c922e7c5efad65384250c/grpcio-1.70.0-cp313-cp313-win32.whl", hash = "sha256:fe9dbd916df3b60e865258a8c72ac98f3ac9e2a9542dcb72b7a34d236242a5ce", size = 3598359 }, + { url = "https://files.pythonhosted.org/packages/1f/79/8edd2442d2de1431b4a3de84ef91c37002f12de0f9b577fb07b452989dbc/grpcio-1.70.0-cp313-cp313-win_amd64.whl", hash = "sha256:4119fed8abb7ff6c32e3d2255301e59c316c22d31ab812b3fbcbaf3d0d87cc68", size = 4293938 }, +] + +[[package]] +name = "grpcio-status" +version = "1.58.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/75/4126cf49ecb4991de5e90f3801151eb24a1dcf37cf30720324f19a081e9e/grpcio-status-1.58.0.tar.gz", hash = "sha256:0b42e70c0405a66a82d9e9867fa255fe59e618964a6099b20568c31dd9099766", size = 13524 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/48/2bcf11bc2df159564eac099ea38d80663d291a56fa2f2f561d08bf083dfa/grpcio_status-1.58.0-py3-none-any.whl", hash = "sha256:36d46072b71a00147709ebce49344ac59b4b8960942acf0f813a8a7d6c1c28e0", size = 14444 }, +] + +[[package]] +name = "gunicorn" +version = "22.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/88/e2f93c5738a4c1f56a458fc7a5b1676fc31dcdbb182bef6b40a141c17d66/gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63", size = 3639760 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/97/6d610ae77b5633d24b69c2ff1ac3044e0e565ecbd1ec188f02c45073054c/gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9", size = 84443 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "identify" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/d5/f31f5b47b4ea076644e1311b89b9b0efe2f989085151080d72aa5d4b31e2/identify-2.2.2.tar.gz", hash = "sha256:43cb1965e84cdd247e875dec6d13332ef5be355ddc16776396d98089b9053d87", size = 98841 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/35/fac586ad912cb341d159b29ee8961dc6de54986e8e4d01eb728bdb2ce1d3/identify-2.2.2-py2.py3-none-any.whl", hash = "sha256:c7c0f590526008911ccc5ceee6ed7b085cbc92f7b6591d0ee5913a130ad64034", size = 98095 }, +] + +[[package]] +name = "idna" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/ed/f86a79a07470cb07819390452f178b3bef1d375f2ec021ecfc709fc7cf07/idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", size = 189575 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0", size = 66836 }, +] + +[[package]] +name = "ijson" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/83/28e9e93a3a61913e334e3a2e78ea9924bb9f9b1ac45898977f9d9dd6133f/ijson-3.3.0.tar.gz", hash = "sha256:7f172e6ba1bee0d4c8f8ebd639577bfe429dee0f3f96775a067b8bae4492d8a0", size = 60079 } + +[[package]] +name = "inflection" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454 }, +] + +[[package]] +name = "iniconfig" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/a2/97899f6bd0e873fed3a7e67ae8d3a08b21799430fb4da15cfedf10d6e2c2/iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32", size = 8104 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", size = 4990 }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, +] + +[[package]] +name = "jsonschema" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "pyrsistent" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/ee/889aee424a43066a06ac68e499335877775eac9b4409f7860f6af94c6688/jsonschema-4.14.0.tar.gz", hash = "sha256:15062f4cc6f591400cd528d2c355f2cfa6a57e44c820dc783aee5e23d36a831f", size = 288641 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/fe/e45dbe1cff8bbc7c84845ac4cd7c1380393d8479eb68b2392ed2ed01e1bd/jsonschema-4.14.0-py3-none-any.whl", hash = "sha256:9892b8d630a82990521a9ca630d3446bd316b5ad54dbe981338802787f3e0d2d", size = 82407 }, +] + +[[package]] +name = "kombu" +version = "5.3.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "amqp" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/d9/86edccdc0f6868936deddf5892d6687481dcf047727f73214e61d31b2515/kombu-5.3.6.tar.gz", hash = "sha256:f3da5b570a147a5da8280180aa80b03807283d63ea5081fcdb510d18242431d9", size = 439311 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/e8/bade4ea794047c94bb267c08aad9d5270c803c18296e876b5d97bad28daf/kombu-5.3.6-py3-none-any.whl", hash = "sha256:49f1e62b12369045de2662f62cc584e7df83481a513db83b01f87b5b9785e378", size = 200188 }, +] + +[[package]] +name = "minio" +version = "7.1.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/25/6764ec7542c6f1537334feaa2c9b1d25f0c7639a105d436b1769638f9159/minio-7.1.13.tar.gz", hash = "sha256:8828615a20cde82df79c5a52005252ad29bb022cde25177a4a43952a04c3222c", size = 115571 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/d6/2c5ea778822ece1493cfe784cc48a5cdfdcc88bf59dacbf8509f748e12b1/minio-7.1.13-py3-none-any.whl", hash = "sha256:462aebd79000d5b923b2a728352014f76292bbd81a9d00e3ed25aa6ec4dc41f2", size = 76155 }, +] + +[[package]] +name = "mmh3" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/1b/1fc6888c74cbd8abad1292dde2ddfcf8fc059e114c97dd6bf16d12f36293/mmh3-5.1.0.tar.gz", hash = "sha256:136e1e670500f177f49ec106a4ebf0adf20d18d96990cc36ea492c651d2b406c", size = 33728 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/06/a098a42870db16c0a54a82c56a5bdc873de3165218cd5b3ca59dbc0d31a7/mmh3-5.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a523899ca29cfb8a5239618474a435f3d892b22004b91779fcb83504c0d5b8c", size = 56165 }, + { url = "https://files.pythonhosted.org/packages/5a/65/eaada79a67fde1f43e1156d9630e2fb70655e1d3f4e8f33d7ffa31eeacfd/mmh3-5.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:17cef2c3a6ca2391ca7171a35ed574b5dab8398163129a3e3a4c05ab85a4ff40", size = 40569 }, + { url = "https://files.pythonhosted.org/packages/36/7e/2b6c43ed48be583acd68e34d16f19209a9f210e4669421b0321e326d8554/mmh3-5.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52e12895b30110f3d89dae59a888683cc886ed0472dd2eca77497edef6161997", size = 40104 }, + { url = "https://files.pythonhosted.org/packages/11/2b/1f9e962fdde8e41b0f43d22c8ba719588de8952f9376df7d73a434827590/mmh3-5.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d6719045cda75c3f40397fc24ab67b18e0cb8f69d3429ab4c39763c4c608dd", size = 102497 }, + { url = "https://files.pythonhosted.org/packages/46/94/d6c5c3465387ba077cccdc028ab3eec0d86eed1eebe60dcf4d15294056be/mmh3-5.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d19fa07d303a91f8858982c37e6939834cb11893cb3ff20e6ee6fa2a7563826a", size = 108834 }, + { url = "https://files.pythonhosted.org/packages/34/1e/92c212bb81796b69dddfd50a8a8f4b26ab0d38fdaf1d3e8628a67850543b/mmh3-5.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31b47a620d622fbde8ca1ca0435c5d25de0ac57ab507209245e918128e38e676", size = 106936 }, + { url = "https://files.pythonhosted.org/packages/f4/41/f2f494bbff3aad5ffd2085506255049de76cde51ddac84058e32768acc79/mmh3-5.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00f810647c22c179b6821079f7aa306d51953ac893587ee09cf1afb35adf87cb", size = 93709 }, + { url = "https://files.pythonhosted.org/packages/9e/a9/a2cc4a756d73d9edf4fb85c76e16fd56b0300f8120fd760c76b28f457730/mmh3-5.1.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6128b610b577eed1e89ac7177ab0c33d06ade2aba93f5c89306032306b5f1c6", size = 101623 }, + { url = "https://files.pythonhosted.org/packages/5e/6f/b9d735533b6a56b2d56333ff89be6a55ac08ba7ff33465feb131992e33eb/mmh3-5.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1e550a45d2ff87a1c11b42015107f1778c93f4c6f8e731bf1b8fa770321b8cc4", size = 98521 }, + { url = "https://files.pythonhosted.org/packages/99/47/dff2b54fac0d421c1e6ecbd2d9c85b2d0e6f6ee0d10b115d9364116a511e/mmh3-5.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:785ae09276342f79fd8092633e2d52c0f7c44d56e8cfda8274ccc9b76612dba2", size = 96696 }, + { url = "https://files.pythonhosted.org/packages/be/43/9e205310f47c43ddf1575bb3a1769c36688f30f1ac105e0f0c878a29d2cd/mmh3-5.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0f4be3703a867ef976434afd3661a33884abe73ceb4ee436cac49d3b4c2aaa7b", size = 105234 }, + { url = "https://files.pythonhosted.org/packages/6b/44/90b11fd2b67dcb513f5bfe9b476eb6ca2d5a221c79b49884dc859100905e/mmh3-5.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e513983830c4ff1f205ab97152a0050cf7164f1b4783d702256d39c637b9d107", size = 98449 }, + { url = "https://files.pythonhosted.org/packages/f0/d0/25c4b0c7b8e49836541059b28e034a4cccd0936202800d43a1cc48495ecb/mmh3-5.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9135c300535c828c0bae311b659f33a31c941572eae278568d1a953c4a57b59", size = 97796 }, + { url = "https://files.pythonhosted.org/packages/23/fa/cbbb7fcd0e287a715f1cd28a10de94c0535bd94164e38b852abc18da28c6/mmh3-5.1.0-cp313-cp313-win32.whl", hash = "sha256:c65dbd12885a5598b70140d24de5839551af5a99b29f9804bb2484b29ef07692", size = 40828 }, + { url = "https://files.pythonhosted.org/packages/09/33/9fb90ef822f7b734955a63851907cf72f8a3f9d8eb3c5706bfa6772a2a77/mmh3-5.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:10db7765201fc65003fa998faa067417ef6283eb5f9bba8f323c48fd9c33e91f", size = 41504 }, + { url = "https://files.pythonhosted.org/packages/16/71/4ad9a42f2772793a03cb698f0fc42499f04e6e8d2560ba2f7da0fb059a8e/mmh3-5.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:b22fe2e54be81f6c07dcb36b96fa250fb72effe08aa52fbb83eade6e1e2d5fd7", size = 38890 }, +] + +[[package]] +name = "multidict" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, + { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, + { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, + { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, + { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, + { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, + { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, + { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, + { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, + { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, + { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, + { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, + { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, + { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, + { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +] + +[[package]] +name = "nodeenv" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/15/d1eb0d2664e57da61622a815efe7a88db68c7a593fb86bd7cc629fc31c76/nodeenv-1.5.0.tar.gz", hash = "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c", size = 33862 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/d0/efdf54539948315cc76e5a66b709212963101d002822c3b54369dbf9b5e0/nodeenv-1.5.0-py2.py3-none-any.whl", hash = "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9", size = 21388 }, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688 }, +] + +[[package]] +name = "orjson" +version = "3.10.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/5dea21763eeff8c1590076918a446ea3d6140743e0e36f58f369928ed0f4/orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e", size = 5282482 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/10/fe7d60b8da538e8d3d3721f08c1b7bff0491e8fa4dd3bf11a17e34f4730e/orjson-3.10.15-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bae0e6ec2b7ba6895198cd981b7cca95d1487d0147c8ed751e5632ad16f031a6", size = 249399 }, + { url = "https://files.pythonhosted.org/packages/6b/83/52c356fd3a61abd829ae7e4366a6fe8e8863c825a60d7ac5156067516edf/orjson-3.10.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93ce145b2db1252dd86af37d4165b6faa83072b46e3995ecc95d4b2301b725a", size = 125044 }, + { url = "https://files.pythonhosted.org/packages/55/b2/d06d5901408e7ded1a74c7c20d70e3a127057a6d21355f50c90c0f337913/orjson-3.10.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c203f6f969210128af3acae0ef9ea6aab9782939f45f6fe02d05958fe761ef9", size = 150066 }, + { url = "https://files.pythonhosted.org/packages/75/8c/60c3106e08dc593a861755781c7c675a566445cc39558677d505878d879f/orjson-3.10.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8918719572d662e18b8af66aef699d8c21072e54b6c82a3f8f6404c1f5ccd5e0", size = 139737 }, + { url = "https://files.pythonhosted.org/packages/6a/8c/ae00d7d0ab8a4490b1efeb01ad4ab2f1982e69cc82490bf8093407718ff5/orjson-3.10.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f71eae9651465dff70aa80db92586ad5b92df46a9373ee55252109bb6b703307", size = 154804 }, + { url = "https://files.pythonhosted.org/packages/22/86/65dc69bd88b6dd254535310e97bc518aa50a39ef9c5a2a5d518e7a223710/orjson-3.10.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e117eb299a35f2634e25ed120c37c641398826c2f5a3d3cc39f5993b96171b9e", size = 130583 }, + { url = "https://files.pythonhosted.org/packages/bb/00/6fe01ededb05d52be42fabb13d93a36e51f1fd9be173bd95707d11a8a860/orjson-3.10.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13242f12d295e83c2955756a574ddd6741c81e5b99f2bef8ed8d53e47a01e4b7", size = 138465 }, + { url = "https://files.pythonhosted.org/packages/db/2f/4cc151c4b471b0cdc8cb29d3eadbce5007eb0475d26fa26ed123dca93b33/orjson-3.10.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7946922ada8f3e0b7b958cc3eb22cfcf6c0df83d1fe5521b4a100103e3fa84c8", size = 130742 }, + { url = "https://files.pythonhosted.org/packages/9f/13/8a6109e4b477c518498ca37963d9c0eb1508b259725553fb53d53b20e2ea/orjson-3.10.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b7155eb1623347f0f22c38c9abdd738b287e39b9982e1da227503387b81b34ca", size = 414669 }, + { url = "https://files.pythonhosted.org/packages/22/7b/1d229d6d24644ed4d0a803de1b0e2df832032d5beda7346831c78191b5b2/orjson-3.10.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:208beedfa807c922da4e81061dafa9c8489c6328934ca2a562efa707e049e561", size = 141043 }, + { url = "https://files.pythonhosted.org/packages/cc/d3/6dc91156cf12ed86bed383bcb942d84d23304a1e57b7ab030bf60ea130d6/orjson-3.10.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eca81f83b1b8c07449e1d6ff7074e82e3fd6777e588f1a6632127f286a968825", size = 129826 }, + { url = "https://files.pythonhosted.org/packages/b3/38/c47c25b86f6996f1343be721b6ea4367bc1c8bc0fc3f6bbcd995d18cb19d/orjson-3.10.15-cp313-cp313-win32.whl", hash = "sha256:c03cd6eea1bd3b949d0d007c8d57049aa2b39bd49f58b4b2af571a5d3833d890", size = 142542 }, + { url = "https://files.pythonhosted.org/packages/27/f1/1d7ec15b20f8ce9300bc850de1e059132b88990e46cd0ccac29cbf11e4f9/orjson-3.10.15-cp313-cp313-win_amd64.whl", hash = "sha256:fd56a26a04f6ba5fb2045b0acc487a63162a958ed837648c5781e1fe3316cfbf", size = 133444 }, +] + +[[package]] +name = "packaging" +version = "24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "polars" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/df/55127a3099e990b45ce3a29ab6789a083451e76e7109fb754aad5525360b/polars-1.12.0.tar.gz", hash = "sha256:fb5c92de1a8f7d0a3f923fe48ea89eb518bdf55315ae917012350fa072bd64f4", size = 4090738 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/ae/77c7ec395d9361ae2086693af1947c9a2b21346ba3faf092bb154b735227/polars-1.12.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f3c4e4e423c373dda07b4c8a7ff12aa02094b524767d0ca306b1eba67f2d99e", size = 32923786 }, + { url = "https://files.pythonhosted.org/packages/97/1c/60736d5588309eb528c52538e116593cb275310bab82ba28702cd87a76d1/polars-1.12.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:aa6f9862f0cec6353243920d9b8d858c21ec8f25f91af203dea6ff91980e140d", size = 28887255 }, + { url = "https://files.pythonhosted.org/packages/5a/3e/31257118e7e087fa27c230b8fadf8ff15d521140bf58558dc889ee0c9c5e/polars-1.12.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afb03647b5160737d2119532ee8ffe825de1d19d87f81bbbb005131786f7d59b", size = 34126501 }, + { url = "https://files.pythonhosted.org/packages/ad/e6/d03053e6064d262f2ec41172a5092b08fc20d10c059dda6c9460371cfd7e/polars-1.12.0-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:ea96aba5eb3dab8f0e6abf05ab3fc2136b329261860ef8661d20f5456a2d78e0", size = 30479546 }, + { url = "https://files.pythonhosted.org/packages/d5/28/3d44ddf56a5c95272b202ce8aa0e9b818a1310e83525c4c29176b538ae7c/polars-1.12.0-cp39-abi3-win_amd64.whl", hash = "sha256:a228a4b320a36d03a9ec9dfe7241b6d80a2f119b2dceb1da953166655e4cf43c", size = 33790337 }, +] + +[[package]] +name = "pre-commit" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/13/b62d075317d8686071eb843f0bb1f195eb332f48869d3c31a4c6f1e063ac/pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4", size = 193330 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/b3/df14c580d82b9627d173ceea305ba898dca135feb360b6d84019d0803d3b/pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b", size = 220560 }, +] + +[[package]] +name = "prometheus-client" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/05/aee33352594522c56eb4a4382b5acd9a706a030db9ba2fc3dc38a283e75c/prometheus_client-0.17.1.tar.gz", hash = "sha256:21e674f39831ae3f8acde238afd9a27a37d0d2fb5a28ea094f0ce25d2cbf2091", size = 90360 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/b3/6e18c89bf6bd120590ea538a62cae16dc763ff2745b18377c4be5495c4aa/prometheus_client-0.17.1-py3-none-any.whl", hash = "sha256:e537f37160f6807b8202a6fc4764cdd19bac5480ddd3e0d463c3002b34462101", size = 60601 }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.28" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/34/c34c376882305c5051ed7f086daf07e68563d284015839bfb74d6e61d402/prompt_toolkit-3.0.28.tar.gz", hash = "sha256:9f1cd16b1e86c2968f2519d7fb31dd9d669916f515612c269d14e9ed52b51650", size = 3057388 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/e4/beb2fad0bec554a011b321d0f6b99981f9171c0b05d03a6948e45ac8a5be/prompt_toolkit-3.0.28-py3-none-any.whl", hash = "sha256:30129d870dcb0b3b6a53efdc9d0a83ea96162ffd28ffe077e94215b233dc670c", size = 380236 }, +] + +[[package]] +name = "proto-plus" +version = "1.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/05/74417b2061e1bf1b82776037cad97094228fa1c1b6e82d08a78d3fb6ddb6/proto_plus-1.25.0.tar.gz", hash = "sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91", size = 56124 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/25/0b7cc838ae3d76d46539020ec39fc92bfc9acc29367e58fe912702c2a79e/proto_plus-1.25.0-py3-none-any.whl", hash = "sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961", size = 50126 }, +] + +[[package]] +name = "protobuf" +version = "4.24.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/30/98dc7297ce8c3f0182800d2879703f0196e76d6a28e53ecaafc3901f8118/protobuf-4.24.3.tar.gz", hash = "sha256:12e9ad2ec079b833176d2921be2cb24281fa591f0b119b208b788adc48c2561d", size = 383885 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/1c/8cf83f294cd8abef37ca2e2251feda7413d8bde7643587e113f847cebb6b/protobuf-4.24.3-cp310-abi3-win32.whl", hash = "sha256:20651f11b6adc70c0f29efbe8f4a94a74caf61b6200472a9aea6e19898f9fcf4", size = 409991 }, + { url = "https://files.pythonhosted.org/packages/5e/46/5b9674a33cbf690ffdd79ab1863767a66461cd06ea7aeb9f90e4e50be7a5/protobuf-4.24.3-cp310-abi3-win_amd64.whl", hash = "sha256:3d42e9e4796a811478c783ef63dc85b5a104b44aaaca85d4864d5b886e4b05e3", size = 430521 }, + { url = "https://files.pythonhosted.org/packages/fe/f3/957db80e5b9f7fd7df97e5554fdc57919dfad24e89291223fd04a0e3c84f/protobuf-4.24.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:6e514e8af0045be2b56e56ae1bb14f43ce7ffa0f68b1c793670ccbe2c4fc7d2b", size = 409444 }, + { url = "https://files.pythonhosted.org/packages/90/76/4303d1f01e799ed0243a55ec81fb16a731faed0d7c95eaba809f4c7f408d/protobuf-4.24.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:ba53c2f04798a326774f0e53b9c759eaef4f6a568ea7072ec6629851c8435959", size = 310612 }, + { url = "https://files.pythonhosted.org/packages/bb/c3/6a06208ecf0934ecaf509b51c52a6cf688586f54ae81ac65c56124571494/protobuf-4.24.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:f6ccbcf027761a2978c1406070c3788f6de4a4b2cc20800cc03d52df716ad675", size = 311586 }, + { url = "https://files.pythonhosted.org/packages/fa/8a/e9c6b48b8f4651df1b1a9d46fe94a74ed99881141b4660aa855a798c7c53/protobuf-4.24.3-py3-none-any.whl", hash = "sha256:f6f8dc65625dadaad0c8545319c2e2f0424fede988368893ca3844261342c11a", size = 175704 }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699 }, + { url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245 }, + { url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631 }, + { url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140 }, + { url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762 }, + { url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967 }, + { url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326 }, + { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712 }, + { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155 }, + { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356 }, + { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 }, +] + +[[package]] +name = "pyasn1" +version = "0.4.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/db/fffec68299e6d7bad3d504147f9094830b704527a7fc098b721d38cc7fa7/pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", size = 146820 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/1e/a94a8d635fa3ce4cfc7f506003548d0a2447ae76fd5ca53932970fe3053f/pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", size = 77145 }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/87/72eb9ccf8a58021c542de2588a867dbefc7556e14b2866d1e40e9e2b587e/pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", size = 242864 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/de/214830a981892a3e286c3794f41ae67a4495df1108c3da8a9f62159b9a9d/pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", size = 155269 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pydantic" +version = "2.10.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/c7/ca334c2ef6f2e046b1144fe4bb2a5da8a4c574e7f2ebf7e16b34a6a2fa92/pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff", size = 761287 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/26/82663c79010b28eddf29dcdd0ea723439535fa917fce5905885c0e9ba562/pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53", size = 431426 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, +] + +[[package]] +name = "pyjwt" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/72/8259b2bccfe4673330cea843ab23f86858a419d8f1493f66d413a76c7e3b/PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de", size = 78313 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/4f/e04a8067c7c96c364cef7ef73906504e2f40d690811c021e1a1901473a19/PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320", size = 22591 }, +] + +[[package]] +name = "pyparsing" +version = "2.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/47/dfc9c342c9842bbe0036c7f763d2d6686bcf5eb1808ba3e170afdb282210/pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", size = 649718 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/bb/488841f56197b13700afd5658fc279a2025a39e22449b7cf29864669b15d/pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b", size = 67842 }, +] + +[[package]] +name = "pyrsistent" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/42/ac/455fdc7294acc4d4154b904e80d964cc9aae75b087bbf486be04df9f2abd/pyrsistent-0.18.1.tar.gz", hash = "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96", size = 100522 } + +[[package]] +name = "pytest" +version = "8.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/b7/7d44bbc04c531dcc753056920e0988032e5871ac674b5a84cb979de6e7af/pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044", size = 1409703 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/7e/c79cecfdb6aa85c6c2e3cf63afc56d0f165f24f5c66c03c695c4d9b84756/pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7", size = 337359 }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.23.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/ef/80107b9e939875ad613c705d99d91e4510dcf5fed29613ac9aecbcba0a8d/pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f", size = 46203 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/c9/de22c040d4c821c6c797ca1d720f1f4b2f4293d5757e811c62ae544496c4/pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a", size = 17552 }, +] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + +[[package]] +name = "pytest-django" +version = "4.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/cf/44510ac5479f281d6663a08dff0d93f56b21f4ee091980ea4d4b64491ad6/pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90", size = 83291 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/5b/29555191e903881d05e1f7184205ec534c7021e0ee077d1e6a1ee8f1b1eb/pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7", size = 23432 }, +] + +[[package]] +name = "pytest-insta" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/5b/6ca4baca60c3f8361415501668cde3abd94dbad44293325833fd89d1a7c1/pytest_insta-0.3.0.tar.gz", hash = "sha256:9e6e1c70a021f68ccc4643360b2c2f8326cf3befba85f942c1da17b9caf713f7", size = 14960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/d5/1459b2861cf703cf49d96b6f29731ee74f4ac7e34b0c60b0ff75bdd318bc/pytest_insta-0.3.0-py3-none-any.whl", hash = "sha256:93a105e3850f2887b120a581923b10bb313d722e00d369377a1d91aa535df704", size = 13660 }, +] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-json-logger" +version = "2.0.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/da/95963cebfc578dabd323d7263958dfb68898617912bb09327dd30e9c8d13/python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c", size = 10508 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/a6/145655273568ee78a581e734cf35beb9e33a370b29c5d3c8fee3744de29f/python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd", size = 8067 }, +] + +[[package]] +name = "python-redis-lock" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/d7/a2a97c73d39e68aacce02667885b9e0b575eb9082866a04fbf098b4c4d99/python-redis-lock-4.0.0.tar.gz", hash = "sha256:4abd0bcf49136acad66727bf5486dd2494078ca55e49efa693f794077319091a", size = 162533 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/70/c5dfaec2085d9be10792704f108543ba1802e228bf040632c673066d8e78/python_redis_lock-4.0.0-py3-none-any.whl", hash = "sha256:ff786e587569415f31e64ca9337fce47c4206e832776e9e42b83bfb9ee1af4bd", size = 12165 }, +] + +[[package]] +name = "pytz" +version = "2022.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/5f/a0f653311adff905bbcaa6d3dfaf97edcf4d26138393c6ccd37a484851fb/pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7", size = 320473 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/2e/dec1cc18c51b8df33c7c4d0a321b084cf38e1733b98f9d15018880fb4970/pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c", size = 503520 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/af35f7ea75cf72f2cd079c95ee16797de7cd71f29ea7c68ae5ce7be1eda0/PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", size = 125201 } + +[[package]] +name = "redis" +version = "4.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/af/f791fa76162d1e7655a34bca58876b43388209912c6a771756fc0c8f96da/redis-4.4.4.tar.gz", hash = "sha256:68226f7ede928db8302f29ab088a157f41061fa946b7ae865452b6d7838bbffb", size = 4549578 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/68/da435219cd46d416bf5b59d66dbe36ae4aaf05b91b7be6e8241a87d4c9b1/redis-4.4.4-py3-none-any.whl", hash = "sha256:da92a39fec86438d3f1e2a1db33c312985806954fe860120b582a8430e231d8f", size = 238045 }, +] + +[[package]] +name = "regex" +version = "2023.12.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/39/31626e7e75b187fae7f121af3c538a991e725c744ac893cc2cfd70ce2853/regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5", size = 394706 } + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "rsa" +version = "4.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/b5/475c45a58650b0580421746504b680cd2db4e81bc941e94ca53785250269/rsa-4.7.2.tar.gz", hash = "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9", size = 39711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/93/0c0f002031f18b53af7a6166103c02b9c0667be528944137cc954ec921b3/rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2", size = 34505 }, +] + +[[package]] +name = "ruff" +version = "0.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/e1/e265aba384343dd8ddd3083f5e33536cd17e1566c41453a5517b5dd443be/ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9", size = 3639454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/e3/3d2c022e687e18cf5d93d6bfa2722d46afc64eaa438c7fbbdd603b3597be/ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba", size = 11714128 }, + { url = "https://files.pythonhosted.org/packages/e1/22/aff073b70f95c052e5c58153cba735748c9e70107a77d03420d7850710a0/ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504", size = 11682539 }, + { url = "https://files.pythonhosted.org/packages/75/a7/f5b7390afd98a7918582a3d256cd3e78ba0a26165a467c1820084587cbf9/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83", size = 11132512 }, + { url = "https://files.pythonhosted.org/packages/a6/e3/45de13ef65047fea2e33f7e573d848206e15c715e5cd56095589a7733d04/ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc", size = 11929275 }, + { url = "https://files.pythonhosted.org/packages/7d/f2/23d04cd6c43b2e641ab961ade8d0b5edb212ecebd112506188c91f2a6e6c/ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b", size = 11466502 }, + { url = "https://files.pythonhosted.org/packages/b5/6f/3a8cf166f2d7f1627dd2201e6cbc4cb81f8b7d58099348f0c1ff7b733792/ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e", size = 12676364 }, + { url = "https://files.pythonhosted.org/packages/f5/c4/db52e2189983c70114ff2b7e3997e48c8318af44fe83e1ce9517570a50c6/ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666", size = 13335518 }, + { url = "https://files.pythonhosted.org/packages/66/44/545f8a4d136830f08f4d24324e7db957c5374bf3a3f7a6c0bc7be4623a37/ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5", size = 12823287 }, + { url = "https://files.pythonhosted.org/packages/c5/26/8208ef9ee7431032c143649a9967c3ae1aae4257d95e6f8519f07309aa66/ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5", size = 14592374 }, + { url = "https://files.pythonhosted.org/packages/31/70/e917781e55ff39c5b5208bda384fd397ffd76605e68544d71a7e40944945/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217", size = 12500173 }, + { url = "https://files.pythonhosted.org/packages/84/f5/e4ddee07660f5a9622a9c2b639afd8f3104988dc4f6ba0b73ffacffa9a8c/ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6", size = 11906555 }, + { url = "https://files.pythonhosted.org/packages/f1/2b/6ff2fe383667075eef8656b9892e73dd9b119b5e3add51298628b87f6429/ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897", size = 11538958 }, + { url = "https://files.pythonhosted.org/packages/3c/db/98e59e90de45d1eb46649151c10a062d5707b5b7f76f64eb1e29edf6ebb1/ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08", size = 12117247 }, + { url = "https://files.pythonhosted.org/packages/ec/bc/54e38f6d219013a9204a5a2015c09e7a8c36cedcd50a4b01ac69a550b9d9/ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656", size = 12554647 }, + { url = "https://files.pythonhosted.org/packages/a5/7d/7b461ab0e2404293c0627125bb70ac642c2e8d55bf590f6fce85f508f1b2/ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d", size = 9949214 }, + { url = "https://files.pythonhosted.org/packages/ee/30/c3cee10f915ed75a5c29c1e57311282d1a15855551a64795c1b2bbe5cf37/ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa", size = 10999914 }, + { url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 }, +] + +[[package]] +name = "s3transfer" +version = "0.11.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/24/1390172471d569e281fcfd29b92f2f73774e95972c965d14b6c802ff2352/s3transfer-0.11.3.tar.gz", hash = "sha256:edae4977e3a122445660c7c114bba949f9d191bae3b34a096f18a1c8c354527a", size = 148042 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/81/48c41b554a54d75d4407740abb60e3a102ae416284df04d1dbdcbe3dbf24/s3transfer-0.11.3-py3-none-any.whl", hash = "sha256:ca855bdeb885174b5ffa95b9913622459d4ad8e331fc98eb01e6d5eb6a30655d", size = 84246 }, +] + +[[package]] +name = "sentry-sdk" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/41/97f673384dae5ed81cc2a568cc5c28e76deee85f8ba50def862e86150a5a/sentry_sdk-2.13.0.tar.gz", hash = "sha256:8d4a576f7a98eb2fdb40e13106e41f330e5c79d72a68be1316e7852cf4995260", size = 279937 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/7e/e9ca09f24a6c334286631a2d32c267cdc5edad5ac03fd9d20a01a82f1c35/sentry_sdk-2.13.0-py2.py3-none-any.whl", hash = "sha256:6beede8fc2ab4043da7f69d95534e320944690680dd9a963178a49de71d726c6", size = 309078 }, +] + +[package.optional-dependencies] +celery = [ + { name = "celery" }, +] + +[[package]] +name = "shared" +version = "0.1.0" +source = { directory = "../../libs/shared" } +dependencies = [ + { name = "amplitude-analytics" }, + { name = "boto3" }, + { name = "cachetools" }, + { name = "cerberus" }, + { name = "codecov-ribs" }, + { name = "colour" }, + { name = "cryptography" }, + { name = "django" }, + { name = "django-better-admin-arrayfield" }, + { name = "django-model-utils" }, + { name = "django-postgres-extra" }, + { name = "django-prometheus" }, + { name = "google-auth" }, + { name = "google-cloud-pubsub" }, + { name = "google-cloud-storage" }, + { name = "httpx" }, + { name = "ijson" }, + { name = "minio" }, + { name = "mmh3" }, + { name = "oauthlib" }, + { name = "orjson" }, + { name = "prometheus-client" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "pyparsing" }, + { name = "python-redis-lock" }, + { name = "pyyaml" }, + { name = "redis" }, + { name = "requests" }, + { name = "sentry-sdk" }, + { name = "sqlalchemy" }, + { name = "zstandard" }, +] + +[package.metadata] +requires-dist = [ + { name = "amplitude-analytics", specifier = ">=1.1.4" }, + { name = "boto3", specifier = ">=1.20.25" }, + { name = "cachetools", specifier = ">=4.1.1" }, + { name = "cerberus", specifier = ">=1.3.5" }, + { name = "codecov-ribs", specifier = ">=0.1.18" }, + { name = "colour", specifier = ">=0.1.5" }, + { name = "cryptography", specifier = ">=43.0.1" }, + { name = "django", specifier = "<5" }, + { name = "django-better-admin-arrayfield", specifier = ">=1.4.2" }, + { name = "django-model-utils", specifier = ">=4.5.1" }, + { name = "django-postgres-extra", specifier = ">=2.0.8" }, + { name = "django-prometheus", specifier = ">=2.3.1" }, + { name = "google-auth", specifier = ">=2.21.0" }, + { name = "google-cloud-pubsub", specifier = ">=2.18.4" }, + { name = "google-cloud-storage", specifier = ">=2.18.2" }, + { name = "httpx", specifier = ">=0.23.0" }, + { name = "ijson", specifier = ">=3.2.3" }, + { name = "minio", specifier = ">=7.1.13" }, + { name = "mmh3", specifier = ">=4.0.1" }, + { name = "oauthlib", specifier = ">=3.1.0" }, + { name = "orjson", specifier = ">=3.10.9" }, + { name = "prometheus-client", specifier = ">=0.17.1" }, + { name = "pydantic", specifier = ">=2.10.4" }, + { name = "pyjwt", specifier = ">=2.8.0" }, + { name = "pyparsing", specifier = ">=2.4.7" }, + { name = "python-redis-lock", specifier = ">=4.0.0" }, + { name = "pyyaml", specifier = ">=6.0.1" }, + { name = "redis", specifier = ">=4.4.4" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "sentry-sdk", specifier = ">=2.13.0" }, + { name = "sqlalchemy", specifier = "<2" }, + { name = "zstandard", specifier = ">=0.23.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "factory-boy", specifier = ">=3.2.0" }, + { name = "freezegun", specifier = ">=1.1.0" }, + { name = "mock", specifier = ">=4.0.3" }, + { name = "mypy", specifier = ">=1.13.0" }, + { name = "pre-commit", specifier = ">=2.11.1" }, + { name = "psycopg2-binary", specifier = ">=2.9.2" }, + { name = "pytest", specifier = ">=8.1.1" }, + { name = "pytest-asyncio", specifier = ">=0.14.0" }, + { name = "pytest-codspeed", specifier = ">=3.2.0" }, + { name = "pytest-cov", specifier = ">=5.0.0" }, + { name = "pytest-django", specifier = ">=4.7.0" }, + { name = "pytest-mock", specifier = ">=1.13.0" }, + { name = "respx", specifier = ">=0.20.2" }, + { name = "ruff", specifier = ">=0.9.0" }, + { name = "types-mock", specifier = ">=5.1.0.20240425" }, + { name = "types-requests", specifier = ">=2.31.0.6" }, + { name = "urllib3", specifier = "==1.26.19" }, + { name = "vcrpy", specifier = ">=4.1.1" }, +] + +[[package]] +name = "simplejson" +version = "3.17.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/45/a16db4f0fa383aaf0676fb7e3c660304fe390415c243f41a77c7f917d59b/simplejson-3.17.2.tar.gz", hash = "sha256:75ecc79f26d99222a084fbdd1ce5aad3ac3a8bd535cd9059528452da38b68841", size = 83210 } + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, +] + +[[package]] +name = "sniffio" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/44ed7978bcb1f6337a3e2bef19c941de750d73243fc9389140d62853b686/sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de", size = 17132 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/b0/7b2e028b63d092804b6794595871f936aafa5e9322dcaaad50ebf67445b3/sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", size = 10033 }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, +] + +[[package]] +name = "sqlalchemy" +version = "1.4.54" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/af/20290b55d469e873cba9d41c0206ab5461ff49d759989b3fe65010f9d265/sqlalchemy-1.4.54.tar.gz", hash = "sha256:4470fbed088c35dc20b78a39aaf4ae54fe81790c783b3264872a0224f437c31a", size = 8470350 } + +[[package]] +name = "sqlparse" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/26/5da251cd090ccd580f5cfaa7d36cdd8b2471e49fffce60ed520afc27f4bc/sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93", size = 83475 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/5d/a0fdd88fd486b39ae1fd1a75ff75b4e29a0df96c0304d462fd407b82efe0/sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663", size = 43971 }, +] + +[[package]] +name = "starlette" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/cb/244daf0d7be4508099ad5bca3cdfe8b8b5538acd719c5f397f614e569fff/starlette-0.40.0.tar.gz", hash = "sha256:1a3139688fb298ce5e2d661d37046a66ad996ce94be4d4983be019a23a04ea35", size = 2573611 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/0f/64baf7a06492e8c12f5c4b49db286787a7255195df496fc21f5fd9eecffa/starlette-0.40.0-py3-none-any.whl", hash = "sha256:c494a22fae73805376ea6bf88439783ecfba9aac88a43911b48c653437e784c4", size = 73303 }, +] + +[[package]] +name = "stripe" +version = "11.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/84/b72eeda51c6033e6a88c71de74de56e3a529a60728159e200b42220b21a9/stripe-11.4.1.tar.gz", hash = "sha256:7ddd251b622d490fe57d78487855dc9f4d95b1bb113607e81fd377037a133d5a", size = 1379118 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/a0/e416502214c2da3f78015956127e6e44ef789cfcfa7a78f6e564fc05d2c6/stripe-11.4.1-py2.py3-none-any.whl", hash = "sha256:8aa47a241de0355c383c916c4ef7273ab666f096a44ee7081e357db4a36f0cce", size = 1627969 }, +] + +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "tzdata" +version = "2024.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/5b/e025d02cb3b66b7b76093404392d4b44343c69101cc85f4d180dd5784717/tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", size = 190559 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/58/f9c9e6be752e9fcb8b6a0ee9fb87e6e7a1f6bcab2cdc73f02bb7ba91ada0/tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252", size = 345370 }, +] + +[[package]] +name = "uritemplate" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/5a/4742fdba39cd02a56226815abfa72fe0aa81c33bed16ed045647d6000eba/uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0", size = 273898 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c0/7461b49cd25aeece13766f02ee576d1db528f1c37ce69aee300e075b485b/uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e", size = 10356 }, +] + +[[package]] +name = "urllib3" +version = "1.26.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/93/65e479b023bbc46dab3e092bda6b0005424ea3217d711964ccdede3f9b1b/urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429", size = 306068 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/6a/99eaaeae8becaa17a29aeb334a18e5d582d873b6f084c11f02581b8d7f7f/urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3", size = 143933 }, +] + +[[package]] +name = "vcrpy" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "urllib3", marker = "platform_python_implementation == 'PyPy'" }, + { name = "wrapt" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/59/9fe85bf7af469bdb0ab8416c76cde630cdff6d1790ecb87e5a58f259c89c/vcrpy-6.0.1.tar.gz", hash = "sha256:9e023fee7f892baa0bbda2f7da7c8ac51165c1c6e38ff8688683a12a4bde9278", size = 84836 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/eb/922cfd27d6593363c3e50b7808bcc234ec996128813fd34341685bb307b7/vcrpy-6.0.1-py2.py3-none-any.whl", hash = "sha256:621c3fb2d6bd8aa9f87532c688e4575bcbbde0c0afeb5ebdb7e14cac409edfdd", size = 41880 }, +] + +[[package]] +name = "vine" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636 }, +] + +[[package]] +name = "virtualenv" +version = "20.29.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/88/dacc875dd54a8acadb4bcbfd4e3e86df8be75527116c91d8f9784f5e9cab/virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728", size = 4320272 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/fa/849483d56773ae29740ae70043ad88e068f98a6401aa819b5d6bee604683/virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a", size = 4301478 }, +] + +[[package]] +name = "wcwidth" +version = "0.2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/38/459b727c381504f361832b9e5ace19966de1a235d73cdbdea91c771a1155/wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83", size = 34755 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/7c/e39aca596badaf1b78e8f547c807b04dae603a433d3e7a7e04d67f2ef3e5/wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", size = 30763 }, +] + +[[package]] +name = "whitenoise" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/8a/cd6346ffd78f5ff9aa9ce750f3e8d75cde7c60fbe197ac10bdea49d61cff/whitenoise-5.2.0.tar.gz", hash = "sha256:05ce0be39ad85740a78750c86a93485c40f08ad8c62a6006de0233765996e5c7", size = 45096 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/83/5d91949e370e52578a99ef6391c3b3e19f9fd1f5b4f58d5cbd6e2862d4a8/whitenoise-5.2.0-py2.py3-none-any.whl", hash = "sha256:05d00198c777028d72d8b0bbd234db605ef6d60e9410125124002518a48e515d", size = 19775 }, +] + +[[package]] +name = "wrapt" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/4c/063a912e20bcef7124e0df97282a8af3ff3e4b603ce84c481d6d7346be0a/wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", size = 53972 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/21/abdedb4cdf6ff41ebf01a74087740a709e2edb146490e4d9beea054b0b7a/wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", size = 23362 }, +] + +[[package]] +name = "yarl" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/ad/bedcdccbcbf91363fd425a948994f3340924145c2bc8ccb296f4a1e52c28/yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf", size = 141869 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/05/4d79198ae568a92159de0f89e710a8d19e3fa267b719a236582eee921f4a/yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad", size = 31638 }, +] + +[[package]] +name = "zstandard" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation == 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975 }, + { url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448 }, + { url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269 }, + { url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228 }, + { url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891 }, + { url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310 }, + { url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912 }, + { url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946 }, + { url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994 }, + { url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681 }, + { url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239 }, + { url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149 }, + { url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392 }, + { url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299 }, + { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862 }, + { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578 }, +] diff --git a/apps/codecov-api/validate/__init__.py b/apps/codecov-api/validate/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/validate/tests/test_validate.py b/apps/codecov-api/validate/tests/test_validate.py new file mode 100644 index 0000000000..acca2f22c2 --- /dev/null +++ b/apps/codecov-api/validate/tests/test_validate.py @@ -0,0 +1,100 @@ +from json import dumps +from unittest.mock import patch + +from django.conf import settings +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from yaml import YAMLError + + +class TestValidateYamlHandler(APITestCase): + # Wrap get and post client calls + + def _get(self): + return self.client.get(reverse("validate-yaml")) + + def _post(self, data=None): + return self.client.post(reverse("validate-yaml"), data=data, format="json") + + # Unit tests + + def test_get(self): + response = self._get() + + assert response.status_code == status.HTTP_200_OK + + expected_result = f"Usage:\n\ncurl -X POST --data-binary @codecov.yml {settings.CODECOV_URL}/validate\n" + assert response.content.decode() == expected_result + + def test_post_no_data(self): + response = self._post() + assert response.status_code == status.HTTP_400_BAD_REQUEST + + expected_result = "No content posted." + assert response.content.decode() == expected_result + + @patch("validate.views.safe_load") + def test_post_malformed_yaml(self, mock_safe_load): + mock_safe_load.side_effect = YAMLError("Can't parse YAML") + + response = self._post(data="malformed yaml") + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + expected_result = "Can't parse YAML\n" + assert response.content.decode() == expected_result + + def test_post_valid_yaml(self): + yaml = { + "ignore": ["Pods/.*"], + "coverage": { + "round": "down", + "precision": 2, + "range": [70.0, 100.0], + "status": {"project": {"default": {"base": "auto"}}}, + "notify": { + "slack": { + "default": { + "url": "secret:c/nCgqn5v1HY5VFIs9i4W3UY6eleB2rTBdBKK/ilhPR7Ch4N0FE1aO6SRfAxp3Zlm4tLNusaPY7ettH6dTYj/YhiRohxiNqJMJ4L9YQmESo=" + } + } + }, + }, + } + response = self._post(data=yaml) + + assert response.status_code == status.HTTP_200_OK + expected_result = f"Valid!\n\n{dumps(yaml, indent=2)}\n" + assert response.content.decode() == expected_result + + def test_post_invalid_yaml(self): + yaml = { + "ignore": ["Pods/.*"], + "coverage": { + "round": "down", + "precision": 2, + "range": [70.0, 100.0], + "status": {"project": {"default": {"base": "auto"}}, "patch": "nope"}, + }, + } + + response = self._post(data=yaml) + assert response.status_code == status.HTTP_400_BAD_REQUEST + expected_result = "Error at ['coverage', 'status', 'patch']: \nmust be of ['dict', 'boolean'] type\n" + assert response.content.decode() == expected_result + + def test_request_body_not_parsable_as_dict(self): + # String + response = self._post(data="codecov.yml") + assert response.status_code == status.HTTP_400_BAD_REQUEST + + expected_result = "No file posted." + assert response.content.decode() == expected_result + + # Number + response = self._post(data=123) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + expected_result = "No file posted." + assert response.content.decode() == expected_result diff --git a/apps/codecov-api/validate/tests/test_validate_v2.py b/apps/codecov-api/validate/tests/test_validate_v2.py new file mode 100644 index 0000000000..d1dbd22fa9 --- /dev/null +++ b/apps/codecov-api/validate/tests/test_validate_v2.py @@ -0,0 +1,73 @@ +from unittest.mock import patch + +from django.test import Client, TestCase +from rest_framework.reverse import reverse + + +@patch("validate.views.API_VALIDATE_V2_COUNTER.labels") +class TestValidateYamlV2Handler(TestCase): + def _post(self, data, query_source=""): + client = Client() + if query_source: + query_source = f"?source={query_source}" + return client.post( + reverse("validate-yaml-v2") + query_source, + data=data, + content_type="text/plain", + ) + + def test_no_data(self, mock_metrics): + res = self._post("") + assert res.status_code == 400 + assert res.json() == {"valid": False, "message": "YAML is empty"} + mock_metrics.assert_called_once_with(**{"source": "unknown"}) + + def test_list_type(self, mock_metrics): + res = self._post("- testing: 123") + assert res.status_code == 400 + assert res.json() == { + "valid": False, + "message": "YAML must be a dictionary type", + } + mock_metrics.assert_called_once_with(**{"source": "unknown"}) + + def test_parse_error(self, mock_metrics): + res = self._post("foo: - 123") + assert res.status_code == 400 + assert res.json() == { + "valid": False, + "message": "YAML could not be parsed", + "parse_error": { + "line": 1, + "column": 6, + "problem": "sequence entries are not allowed here", + }, + } + mock_metrics.assert_called_once_with(**{"source": "unknown"}) + + def test_parse_invalid(self, mock_metrics): + res = self._post("comment: 123") + assert res.status_code == 400 + assert res.json() == { + "valid": False, + "message": "YAML does not match the accepted schema", + "validation_error": {"comment": ["must be of ['dict', 'boolean'] type"]}, + } + mock_metrics.assert_called_once_with(**{"source": "unknown"}) + + def test_parse_valid(self, mock_metrics): + res = self._post("comment: true") + assert res.status_code == 200 + assert res.json() == { + "valid": True, + "message": "YAML is valid", + "validated_yaml": { + "comment": True, + }, + } + mock_metrics.assert_called_once_with(**{"source": "unknown"}) + + def test_query_source_metric(self, mock_metrics): + self._post("comment: true", query_source="vscode") + mock_metrics.assert_called() + mock_metrics.assert_called_with(**{"source": "vscode"}) diff --git a/apps/codecov-api/validate/urls.py b/apps/codecov-api/validate/urls.py new file mode 100644 index 0000000000..08ca6a1e57 --- /dev/null +++ b/apps/codecov-api/validate/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from .views import V1ValidateYamlHandler, V2ValidateYamlHandler + +urlpatterns = [ + path("", V1ValidateYamlHandler.as_view(), name="validate-yaml"), + path("v1", V1ValidateYamlHandler.as_view(), name="validate-yaml-v1"), + path("v2", V2ValidateYamlHandler.as_view(), name="validate-yaml-v2"), +] diff --git a/apps/codecov-api/validate/views.py b/apps/codecov-api/validate/views.py new file mode 100644 index 0000000000..5c3fe6aae2 --- /dev/null +++ b/apps/codecov-api/validate/views.py @@ -0,0 +1,154 @@ +import logging +from json import dumps +from typing import Any + +from django.conf import settings +from django.http import HttpRequest, HttpResponse +from rest_framework import status +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework.views import APIView +from shared.metrics import Counter, inc_counter +from shared.validation.exceptions import InvalidYamlException +from shared.yaml.validation import validate_yaml +from yaml import YAMLError, safe_load + +log = logging.getLogger(__name__) + +API_VALIDATE_V2_COUNTER = Counter( + "api_validate_v2", + "Number of times the validate v2 endpoint has been hit", +) + + +class V1ValidateYamlHandler(APIView): + permission_classes = [AllowAny] + + def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: + return HttpResponse( + f"Usage:\n\ncurl -X POST --data-binary @codecov.yml {settings.CODECOV_URL}/validate\n", + status=status.HTTP_200_OK, + content_type="text/plain", + ) + + def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: + if not self.request.body: + return HttpResponse( + "No content posted.", + status=status.HTTP_400_BAD_REQUEST, + content_type="text/plain", + ) + + # Parse the yaml from the request body + try: + yaml_dict = safe_load(self.request.body) + + if not isinstance(yaml_dict, dict): + log.warning( + "yaml_dict result from loading validate request body is not a dict", + extra=dict( + yaml_dict=yaml_dict, request_body=str(self.request.body) + ), + ) + return HttpResponse( + "No file posted.", + status=status.HTTP_400_BAD_REQUEST, + content_type="text/plain", + ) + + except YAMLError: + return HttpResponse( + "Can't parse YAML\n", + status=status.HTTP_400_BAD_REQUEST, + content_type="text/plain", + ) + + # Validate the parsed yaml + try: + validated_yaml = validate_yaml(yaml_dict) + return HttpResponse( + f"Valid!\n\n{dumps(validated_yaml, indent=2)}\n", + status=status.HTTP_200_OK, + content_type="text/plain", + ) + + except InvalidYamlException as e: + return HttpResponse( + f"Error at {str(e.error_location)}: \n{e.error_message}\n", + status=status.HTTP_400_BAD_REQUEST, + content_type="text/plain", + ) + + +class V2ValidateYamlHandler(V1ValidateYamlHandler): + permission_classes = [AllowAny] + + def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: + source = self.request.query_params.get("source", "unknown") + inc_counter( + API_VALIDATE_V2_COUNTER, + labels=dict(source=source), + ) + + if not self.request.body: + return Response( + { + "valid": False, + "message": "YAML is empty", + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Parse the yaml from the request body + try: + yaml_dict = safe_load(self.request.body) + if not isinstance(yaml_dict, dict): + log.warning( + "yaml_dict result from loading validate request body is not a dict", + extra=dict( + yaml_dict=yaml_dict, request_body=str(self.request.body) + ), + ) + return Response( + { + "valid": False, + "message": "YAML must be a dictionary type", + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + except YAMLError as e: + mark = e.problem_mark + return Response( + { + "valid": False, + "message": "YAML could not be parsed", + "parse_error": { + "problem": e.problem, + "line": mark.line + 1, + "column": mark.column + 1, + }, + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Validate the parsed yaml + try: + validated_yaml = validate_yaml(yaml_dict) + return Response( + { + "valid": True, + "message": "YAML is valid", + "validated_yaml": validated_yaml, + } + ) + + except InvalidYamlException as e: + return Response( + { + "valid": False, + "message": "YAML does not match the accepted schema", + "validation_error": e.error_dict, + }, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apps/codecov-api/webhook_handlers/__init__.py b/apps/codecov-api/webhook_handlers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/webhook_handlers/constants.py b/apps/codecov-api/webhook_handlers/constants.py new file mode 100644 index 0000000000..189e7520f1 --- /dev/null +++ b/apps/codecov-api/webhook_handlers/constants.py @@ -0,0 +1,104 @@ +class GitHubHTTPHeaders: + EVENT = "HTTP_X_GITHUB_EVENT" + DELIVERY_TOKEN = "HTTP_X_GITHUB_DELIVERY" + SIGNATURE = "HTTP_X_HUB_SIGNATURE" + SIGNATURE_256 = "HTTP_X_HUB_SIGNATURE_256" + + +class GitHubWebhookEvents: + PULL_REQUEST = "pull_request" + DELETE = "delete" + PUSH = "push" + PUBLIC = "public" + STATUS = "status" + REPOSITORY = "repository" + PING = "ping" + INSTALLATION = "installation" + INSTALLATION_REPOSITORIES = "installation_repositories" + ORGANIZATION = "organization" + MARKETPLACE_PURCHASE = "marketplace_purchase" + MEMBER = "member" + + repository_events = [PULL_REQUEST, DELETE, PUSH, PUBLIC, STATUS, REPOSITORY, MEMBER] + + +class BitbucketHTTPHeaders: + EVENT = "HTTP_X_EVENT_KEY" + UUID = "HTTP_X_HOOK_UUID" + + +class BitbucketServerHTTPHeaders: + EVENT = "X-Event-Key" + UUID = "X-Request-Id" + + +class BitbucketWebhookEvents: + PULL_REQUEST_CREATED = "pullrequest:created" + PULL_REQUEST_UPDATED = "pullrequest:updated" + PULL_REQUEST_REJECTED = "pullrequest:rejected" + PULL_REQUEST_FULFILLED = "pullrequest:fulfilled" + REPO_PUSH = "repo:push" + REPO_COMMIT_STATUS_CREATED = "repo:commit_status_created" + REPO_COMMIT_STATUS_UPDATED = "repo:commit_status_updated" + + subscribed_events = [ + PULL_REQUEST_CREATED, + PULL_REQUEST_UPDATED, + PULL_REQUEST_FULFILLED, + REPO_PUSH, + REPO_COMMIT_STATUS_CREATED, + REPO_COMMIT_STATUS_UPDATED, + ] + + +class BitbucketServerWebhookEvents: + REPO_MODIFIED = "repo:modified" + REPO_REFS_CHANGED = "repo:refs_changed" + PULL_REQUEST_CREATED = "pr:opened" + PULL_REQUEST_MERGED = "pr:merged" + PULL_REQUEST_REJECTED = "pr:declined" + PULL_REQUEST_DELETED = "pr:deleted" + + subscribed_events = [ + REPO_MODIFIED, + REPO_REFS_CHANGED, + PULL_REQUEST_CREATED, + PULL_REQUEST_MERGED, + PULL_REQUEST_REJECTED, + PULL_REQUEST_DELETED, + ] + + +class GitLabHTTPHeaders: + EVENT = "HTTP_X_GITLAB_EVENT" + TOKEN = "HTTP_X_GITLAB_TOKEN" + + +class GitLabWebhookEvents: + MERGE_REQUEST = "Merge Request Hook" + SYSTEM = "System Hook" + PUSH = "Push Hook" + JOB = "Job Hook" + + subscribed_events = { + "push_events": True, + "issues_events": False, + "merge_requests_events": True, + "tag_push_events": False, + "note_events": False, + "job_events": False, + "build_events": True, + "pipeline_events": True, + "wiki_events": False, + } + + +class WebhookHandlerErrorMessages: + LICENSE_EXPIRED = "License expired/invalid. Webhook rejected." + INVALID_SIGNATURE = "Invalid signature" + UNSUPPORTED_EVENT = "Unsupported event" + SKIP_CODECOV_STATUS = "Ok. Skip Codecov status updates." + SKIP_NOT_ACTIVE = "OK. Skip because repo is not active." + SKIP_PROCESSING = "OK. Skip because commit not found or is processing." + SKIP_PENDING_STATUSES = "Ok. Skip because status is pending." + SKIP_WEBHOOK_IGNORED = "Ok. Skip because config says to ignore this webhook" diff --git a/apps/codecov-api/webhook_handlers/tests/__init__.py b/apps/codecov-api/webhook_handlers/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/codecov-api/webhook_handlers/tests/test_bitbucket.py b/apps/codecov-api/webhook_handlers/tests/test_bitbucket.py new file mode 100644 index 0000000000..1aaeeb59c1 --- /dev/null +++ b/apps/codecov-api/webhook_handlers/tests/test_bitbucket.py @@ -0,0 +1,349 @@ +from unittest.mock import patch + +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.django_apps.core.tests.factories import ( + BranchFactory, + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) + +from core.models import Branch, Commit, PullStates +from webhook_handlers.constants import ( + BitbucketHTTPHeaders, + BitbucketWebhookEvents, + WebhookHandlerErrorMessages, +) + + +class TestBitbucketWebhookHandler(APITestCase): + def _post_event_data( + self, event, data={}, hookid="f2e634c1-63db-44ac-b119-019fa6a71a2c" + ): + return self.client.post( + reverse("bitbucket-webhook"), + **{BitbucketHTTPHeaders.EVENT: event, BitbucketHTTPHeaders.UUID: hookid}, + data=data, + format="json", + ) + + def setUp(self): + self.repo = RepositoryFactory( + author=OwnerFactory(service="bitbucket"), + service_id="673a6070-3421-46c9-9d48-90745f7bfe8e", + active=True, + hookid="f2e634c1-63db-44ac-b119-019fa6a71a2c", + ) + self.pull = PullFactory( + author=self.repo.author, + repository=self.repo, + pullid=1, + state=PullStates.OPEN, + ) + + def test_unknown_repo(self): + response = self._post_event_data( + event=BitbucketWebhookEvents.REPO_PUSH, + data={"repository": {"uuid": "{94f4c9b4-254f-46cf-a39e-97ce03fe58af}"}}, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_inactive_repo(self): + self.repo.active = False + self.repo.save() + response = self._post_event_data( + event=BitbucketWebhookEvents.REPO_PUSH, + data={"repository": {"uuid": "{673a6070-3421-46c9-9d48-90745f7bfe8e}"}}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_NOT_ACTIVE + + @patch("services.task.TaskService.pulls_sync") + def test_pull_request_created(self, pulls_sync_mock): + pullid = 1 + response = self._post_event_data( + event=BitbucketWebhookEvents.PULL_REQUEST_CREATED, + data={ + "repository": {"uuid": "{673a6070-3421-46c9-9d48-90745f7bfe8e}"}, + "pullrequest": {"id": pullid}, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Opening pull request in Codecov" + + pulls_sync_mock.assert_called_once_with(repoid=self.repo.repoid, pullid=pullid) + + def test_pull_request_fulfilled(self): + pullid = 1 + response = self._post_event_data( + event=BitbucketWebhookEvents.PULL_REQUEST_FULFILLED, + data={ + "repository": {"uuid": "{673a6070-3421-46c9-9d48-90745f7bfe8e}"}, + "pullrequest": {"id": pullid}, + }, + ) + assert response.status_code == status.HTTP_200_OK + self.pull.refresh_from_db() + assert self.pull.state == PullStates.MERGED + + def test_pull_request_rejected(self): + pullid = 1 + response = self._post_event_data( + event=BitbucketWebhookEvents.PULL_REQUEST_REJECTED, + data={ + "repository": {"uuid": "{673a6070-3421-46c9-9d48-90745f7bfe8e}"}, + "pullrequest": {"id": pullid}, + }, + ) + assert response.status_code == status.HTTP_200_OK + self.pull.refresh_from_db() + assert self.pull.state == PullStates.CLOSED + + def test_repo_push_branch_deleted(self): + BranchFactory(repository=self.repo, name="name-of-branch") + response = self._post_event_data( + event=BitbucketWebhookEvents.REPO_PUSH, + data={ + "repository": {"uuid": "{673a6070-3421-46c9-9d48-90745f7bfe8e}"}, + "push": { + "changes": [ + { + "new": None, + "old": { + "type": "branch", + "name": "name-of-branch", + "target": {}, + }, + "links": {}, + "created": False, + "forced": False, + "closed": False, + "commits": [ + { + "hash": "03f4a7270240708834de475bcf21532d6134777e", + "type": "commit", + "message": "commit message\n", + "author": {}, + "links": {}, + } + ], + "truncated": False, + } + ] + }, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data is None + assert not Branch.objects.filter( + repository=self.repo, name="name-of-branch" + ).exists() + + def test_repo_push_new_branch_sync_yaml_skipped(self): + response = self._post_event_data( + event=BitbucketWebhookEvents.REPO_PUSH, + data={ + "repository": {"uuid": "{673a6070-3421-46c9-9d48-90745f7bfe8e}"}, + "push": { + "changes": [ + { + "new": { + "type": "branch", + "name": "name-of-branch", + "target": {}, + }, + "old": { + "type": "branch", + "name": "name-of-branch", + "target": {}, + }, + "links": {}, + "created": False, + "forced": False, + "closed": False, + "commits": [ + { + "hash": "03f4a7270240708834de475bcf21532d6134777e", + "type": "commit", + "message": "commit message\n", + "author": {}, + "links": {}, + } + ], + "truncated": False, + } + ] + }, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Synchronize codecov.yml skipped" + + def test_repo_push_new_branch_sync_yaml(self): + response = self._post_event_data( + event=BitbucketWebhookEvents.REPO_PUSH, + data={ + "repository": {"uuid": "{673a6070-3421-46c9-9d48-90745f7bfe8e}"}, + "push": { + "changes": [ + { + "new": { + "type": "branch", + "name": "name-of-branch", + "target": {}, + }, + "old": { + "type": "branch", + "name": "name-of-branch", + "target": {}, + }, + "links": {}, + "created": False, + "forced": False, + "closed": False, + "commits": [ + { + "hash": "03f4a7270240708834de475bcf21532d6134777e", + "type": "commit", + "message": "commit message\n", + "author": {}, + "links": {}, + } + ], + "truncated": False, + } + ] + }, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Synchronize codecov.yml skipped" + + def test_repo_commit_status_change_wrong_context(self): + response = self._post_event_data( + event=BitbucketWebhookEvents.REPO_COMMIT_STATUS_CREATED, + data={ + "repository": {"uuid": "{673a6070-3421-46c9-9d48-90745f7bfe8e}"}, + "commit_status": { + "name": "Unit Tests (Python)", + "description": "Build started", + "state": "INPROGRESS", + "key": "codecov", + "url": "https://my-build-tool.com/builds/MY-PROJECT/BUILD-777", + "type": "build", + "created_on": "2015-11-19T20:37:35.547563+00:00", + "updated_on": "2015-11-19T20:37:35.547563+00:00", + "links": { + "commit": { + "href": "http://api.bitbucket.org/2.0/repositories/tk/test/commit/9fec847784abb10b2fa567ee63b85bd238955d0e" + }, + "self": { + "href": "http://api.bitbucket.org/2.0/repositories/tk/test/commit/9fec847784abb10b2fa567ee63b85bd238955d0e/statuses/build/mybuildtool" + }, + }, + }, + }, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_CODECOV_STATUS + + def test_repo_commit_status_change_in_progress(self): + response = self._post_event_data( + event=BitbucketWebhookEvents.REPO_COMMIT_STATUS_CREATED, + data={ + "repository": {"uuid": "{673a6070-3421-46c9-9d48-90745f7bfe8e}"}, + "commit_status": { + "name": "Unit Tests (Python)", + "description": "Build started", + "state": "INPROGRESS", + "key": "not_codecov_context", + "url": "https://my-build-tool.com/builds/MY-PROJECT/BUILD-777", + "type": "build", + "created_on": "2015-11-19T20:37:35.547563+00:00", + "updated_on": "2015-11-19T20:37:35.547563+00:00", + "links": { + "commit": { + "href": "http://api.bitbucket.org/2.0/repositories/tk/test/commit/9fec847784abb10b2fa567ee63b85bd238955d0e" + }, + "self": { + "href": "http://api.bitbucket.org/2.0/repositories/tk/test/commit/9fec847784abb10b2fa567ee63b85bd238955d0e/statuses/build/mybuildtool" + }, + }, + }, + }, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_PENDING_STATUSES + + def test_repo_commit_status_change_commit_skip_processing(self): + commitid = "9fec847784abb10b2fa567ee63b85bd238955d0e" + CommitFactory( + commitid=commitid, repository=self.repo, state=Commit.CommitStates.PENDING + ) + response = self._post_event_data( + event=BitbucketWebhookEvents.REPO_COMMIT_STATUS_CREATED, + data={ + "repository": {"uuid": "{673a6070-3421-46c9-9d48-90745f7bfe8e}"}, + "commit_status": { + "name": "Unit Tests (Python)", + "description": "Build started", + "state": "SUCCESSFUL", + "key": "not_codecov_context", + "url": "https://my-build-tool.com/builds/MY-PROJECT/BUILD-777", + "type": "build", + "created_on": "2015-11-19T20:37:35.547563+00:00", + "updated_on": "2015-11-19T20:37:35.547563+00:00", + "links": { + "commit": { + "href": f"http://api.bitbucket.org/2.0/repositories/tk/test/commit/{commitid}" + }, + "self": { + "href": f"http://api.bitbucket.org/2.0/repositories/tk/test/commit/{commitid}/statuses/build/mybuildtool" + }, + }, + }, + }, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_PROCESSING + + @patch("services.task.TaskService.notify") + def test_repo_commit_status_change_commit_notifies(self, notify_mock): + commitid = "9fec847784abb10b2fa567ee63b85bd238955d0e" + CommitFactory( + commitid=commitid, repository=self.repo, state=Commit.CommitStates.COMPLETE + ) + response = self._post_event_data( + event=BitbucketWebhookEvents.REPO_COMMIT_STATUS_CREATED, + data={ + "repository": {"uuid": "{673a6070-3421-46c9-9d48-90745f7bfe8e}"}, + "commit_status": { + "name": "Unit Tests (Python)", + "description": "Build started", + "state": "SUCCESSFUL", + "key": "not_codecov_context", + "url": "https://my-build-tool.com/builds/MY-PROJECT/BUILD-777", + "type": "build", + "created_on": "2015-11-19T20:37:35.547563+00:00", + "updated_on": "2015-11-19T20:37:35.547563+00:00", + "links": { + "commit": { + "href": f"http://api.bitbucket.org/2.0/repositories/tk/test/commit/{commitid}" + }, + "self": { + "href": f"http://api.bitbucket.org/2.0/repositories/tk/test/commit/{commitid}/statuses/build/mybuildtool" + }, + }, + }, + }, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == "Notify queued" + notify_mock.assert_called_once_with(repoid=self.repo.repoid, commitid=commitid) diff --git a/apps/codecov-api/webhook_handlers/tests/test_bitbucket_server.py b/apps/codecov-api/webhook_handlers/tests/test_bitbucket_server.py new file mode 100644 index 0000000000..7c37a614e5 --- /dev/null +++ b/apps/codecov-api/webhook_handlers/tests/test_bitbucket_server.py @@ -0,0 +1,243 @@ +from unittest.mock import patch + +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.django_apps.core.tests.factories import ( + BranchFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) + +from core.models import Branch, PullStates +from webhook_handlers.constants import ( + BitbucketServerHTTPHeaders, + BitbucketServerWebhookEvents, + WebhookHandlerErrorMessages, +) + + +class TestBitbucketServerWebhookHandler(APITestCase): + def _post_event_data( + self, event, data={}, hookid="f2e634c1-63db-44ac-b119-019fa6a71a2c" + ): + return self.client.post( + reverse("bitbucket-server-webhook"), + **{ + BitbucketServerHTTPHeaders.EVENT: event, + BitbucketServerHTTPHeaders.UUID: hookid, + }, + data=data, + format="json", + ) + + def setUp(self): + self.repo = RepositoryFactory( + author=OwnerFactory(service="bitbucket_server"), + service_id="673a6070-3421-46c9-9d48-90745f7bfe8e", + active=True, + hookid="f2e634c1-63db-44ac-b119-019fa6a71a2c", + ) + self.pull = PullFactory( + author=self.repo.author, + repository=self.repo, + pullid=1, + state=PullStates.OPEN, + ) + + def test_unknown_repo(self): + pullid = 1 + response = self._post_event_data( + event=BitbucketServerWebhookEvents.PULL_REQUEST_CREATED, + data={ + "pullRequest": { + "id": pullid, + "toRef": {"repository": {"id": "some-unknown-value"}}, + } + }, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_inactive_repo(self): + self.repo.active = False + self.repo.save() + response = self._post_event_data( + event=BitbucketServerWebhookEvents.PULL_REQUEST_CREATED, + data={ + "pullRequest": { + "toRef": { + "repository": {"id": "673a6070-3421-46c9-9d48-90745f7bfe8e"} + } + } + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_NOT_ACTIVE + + @patch("services.task.TaskService.pulls_sync") + def test_pull_request_created(self, pulls_sync_mock): + pullid = 1 + response = self._post_event_data( + event=BitbucketServerWebhookEvents.PULL_REQUEST_CREATED, + data={ + "pullRequest": { + "id": pullid, + "toRef": { + "repository": {"id": "673a6070-3421-46c9-9d48-90745f7bfe8e"} + }, + } + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Opening pull request in Codecov" + + pulls_sync_mock.assert_called_once_with(repoid=self.repo.repoid, pullid=pullid) + + def test_pull_request_fulfilled(self): + pullid = 1 + response = self._post_event_data( + event=BitbucketServerWebhookEvents.PULL_REQUEST_MERGED, + data={ + "pullRequest": { + "id": pullid, + "toRef": { + "repository": {"id": "673a6070-3421-46c9-9d48-90745f7bfe8e"} + }, + } + }, + ) + assert response.status_code == status.HTTP_200_OK + self.pull.refresh_from_db() + assert self.pull.state == PullStates.MERGED + + def test_pull_request_rejected(self): + pullid = 1 + response = self._post_event_data( + event=BitbucketServerWebhookEvents.PULL_REQUEST_REJECTED, + data={ + "pullRequest": { + "id": pullid, + "toRef": { + "repository": {"id": "673a6070-3421-46c9-9d48-90745f7bfe8e"} + }, + } + }, + ) + assert response.status_code == status.HTTP_200_OK + self.pull.refresh_from_db() + assert self.pull.state == PullStates.CLOSED + + def test_repo_push_branch_deleted(self): + BranchFactory(repository=self.repo, name="name-of-branch") + response = self._post_event_data( + event=BitbucketServerWebhookEvents.REPO_REFS_CHANGED, + data={ + "repository": {"id": "673a6070-3421-46c9-9d48-90745f7bfe8e"}, + "push": { + "changes": { + "new": None, + "old": { + "type": "branch", + "name": "name-of-branch", + "target": {}, + }, + "links": {}, + "created": False, + "forced": False, + "closed": False, + "commits": [ + { + "hash": "03f4a7270240708834de475bcf21532d6134777e", + "type": "commit", + "message": "commit message\n", + "author": {}, + "links": {}, + } + ], + "truncated": False, + } + }, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data is None + assert not Branch.objects.filter( + repository=self.repo, name="name-of-branch" + ).exists() + + def test_repo_push_new_branch_sync_yaml_skipped(self): + response = self._post_event_data( + event=BitbucketServerWebhookEvents.REPO_REFS_CHANGED, + data={ + "repository": {"id": "673a6070-3421-46c9-9d48-90745f7bfe8e"}, + "push": { + "changes": { + "new": { + "type": "branch", + "name": "name-of-branch", + "target": {}, + }, + "old": { + "type": "branch", + "name": "name-of-branch", + "target": {}, + }, + "links": {}, + "created": False, + "forced": False, + "closed": False, + "commits": [ + { + "hash": "03f4a7270240708834de475bcf21532d6134777e", + "type": "commit", + "message": "commit message\n", + "author": {}, + "links": {}, + } + ], + "truncated": False, + } + }, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Synchronize codecov.yml skipped" + + def test_repo_push_new_branch_sync_yaml(self): + response = self._post_event_data( + event=BitbucketServerWebhookEvents.REPO_REFS_CHANGED, + data={ + "repository": {"id": "673a6070-3421-46c9-9d48-90745f7bfe8e"}, + "push": { + "changes": { + "new": { + "type": "branch", + "name": "name-of-branch", + "target": {}, + }, + "old": { + "type": "branch", + "name": "name-of-branch", + "target": {}, + }, + "links": {}, + "created": False, + "forced": False, + "closed": False, + "commits": [ + { + "hash": "03f4a7270240708834de475bcf21532d6134777e", + "type": "commit", + "message": "commit message\n", + "author": {}, + "links": {}, + } + ], + "truncated": False, + } + }, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Synchronize codecov.yml skipped" diff --git a/apps/codecov-api/webhook_handlers/tests/test_github.py b/apps/codecov-api/webhook_handlers/tests/test_github.py new file mode 100644 index 0000000000..9e11cc7e5b --- /dev/null +++ b/apps/codecov-api/webhook_handlers/tests/test_github.py @@ -0,0 +1,1422 @@ +import hmac +import json +import uuid +from hashlib import sha1, sha256 +from unittest.mock import call, patch + +import pytest +from freezegun import freeze_time +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.django_apps.core.tests.factories import ( + BranchFactory, + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) +from shared.plan.constants import PlanName +from shared.utils.test_utils import mock_config_helper + +from billing.helpers import mock_all_plans_and_tiers +from codecov_auth.models import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, + Owner, + Service, +) +from webhook_handlers.constants import ( + GitHubHTTPHeaders, + GitHubWebhookEvents, + WebhookHandlerErrorMessages, +) + + +class MockedSubscription(object): + def __init__(self, status, plan_name, quantity): + self.status = status + self.plan = { + "name": plan_name, + } + self.quantity = quantity + + def __getitem__(self, key): + return getattr(self, key) + + +WEBHOOK_SECRET = b"testixik8qdauiab1yiffydimvi72ekq" +DEFAULT_APP_ID = 1234 + + +class GithubWebhookHandlerTests(APITestCase): + @pytest.fixture(scope="function", autouse=True) + def inject_mocker(request, mocker): + request.mocker = mocker + + @pytest.fixture(autouse=True) + def mock_webhook_secret(self, mocker): + mock_config_helper(mocker, configs={"github.webhook_secret": WEBHOOK_SECRET}) + + @pytest.fixture(autouse=True) + def mock_default_app_id(self, mocker): + mock_config_helper(mocker, configs={"github.integration.id": DEFAULT_APP_ID}) + + def _post_event_data(self, event, data={}): + return self.client.post( + reverse("github-webhook"), + **{ + GitHubHTTPHeaders.EVENT: event, + GitHubHTTPHeaders.DELIVERY_TOKEN: uuid.UUID(int=5), + GitHubHTTPHeaders.SIGNATURE_256: "sha256=" + + hmac.new( + WEBHOOK_SECRET, + json.dumps(data, separators=(",", ":")).encode("utf-8"), + digestmod=sha256, + ).hexdigest(), + }, + data=data, + format="json", + ) + + def setUp(self): + self.repo = RepositoryFactory( + author=OwnerFactory(service=Service.GITHUB.value), + service_id=12345, + active=True, + ) + + def test_get_repo_paths_dont_crash(self): + with self.subTest("with ownerid success"): + self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={ + "action": "publicized", + "repository": { + "id": self.repo.service_id, + "owner": {"id": self.repo.author.service_id}, + }, + }, + ) + + with self.subTest("with not found owner"): + self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={ + "action": "publicized", + "repository": { + "id": self.repo.service_id, + "owner": {"id": -239450}, + }, + }, + ) + + with self.subTest("with not found owner and not found repo"): + self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={ + "action": "publicized", + "repository": {"id": -1948503, "owner": {"id": -239450}}, + }, + ) + + with self.subTest("with owner and not found repo"): + self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={ + "action": "publicized", + "repository": { + "id": -1948503, + "owner": {"id": self.repo.author.service_id}, + }, + }, + ) + + def test_ping_returns_pong_and_200(self): + response = self._post_event_data(event=GitHubWebhookEvents.PING) + assert response.status_code == status.HTTP_200_OK + + def test_repository_publicized_sets_activated_false_and_private_false(self): + self.repo.private = True + self.repo.activated = True + + self.repo.save() + + response = self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={"action": "publicized", "repository": {"id": self.repo.service_id}}, + ) + + assert response.status_code == status.HTTP_200_OK + + self.repo.refresh_from_db() + + assert self.repo.private == False + assert self.repo.activated == False + + def test_repository_privatized_sets_private_true(self): + self.repo.private = False + self.repo.save() + + response = self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={"action": "privatized", "repository": {"id": self.repo.service_id}}, + ) + + assert response.status_code == status.HTTP_200_OK + + self.repo.refresh_from_db() + + assert self.repo.private == True + + def test_repository_deleted_sets_deleted_activated_and_active(self): + response = self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={"action": "deleted", "repository": {"id": self.repo.service_id}}, + ) + + assert response.status_code == status.HTTP_200_OK + self.repo.refresh_from_db() + assert self.repo.deleted is True + assert self.repo.active is False + assert self.repo.activated is False + + def test_repository_delete_renames_repo(self): + self.repo.name = "testing" + self.repo.save() + assert self.repo.deleted == False + + other_repo = RepositoryFactory( + name="testing", + author=OwnerFactory(service=Service.GITHUB.value), + service_id=67890, + active=True, + ) + + response = self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={"action": "deleted", "repository": {"id": self.repo.service_id}}, + ) + + assert response.status_code == status.HTTP_200_OK + self.repo.refresh_from_db() + assert self.repo.deleted is True + assert self.repo.name == "testing-deleted" + + # renaming the deleted repo allows the other repo to potentially be moved to a + # new owner (uniqueness constraints would have prevented this otherwise) + other_repo.author = self.repo.author + other_repo.save() + + def test_delete_event_deletes_branch(self): + branch = BranchFactory(repository=self.repo) + + response = self._post_event_data( + event=GitHubWebhookEvents.DELETE, + data={ + "ref": "refs/heads/" + branch.name, + "ref_type": "branch", + "repository": {"id": self.repo.service_id}, + }, + ) + + assert response.status_code == status.HTTP_200_OK + assert not self.repo.branches.filter(name=branch.name).exists() + + def test_public_sets_repo_private_false_and_activated_false(self): + self.repo.private = True + self.repo.activated = True + self.repo.save() + + response = self._post_event_data( + event=GitHubWebhookEvents.PUBLIC, + data={"repository": {"id": self.repo.service_id}}, + ) + + assert response.status_code == status.HTTP_200_OK + self.repo.refresh_from_db() + assert not self.repo.private + assert not self.repo.activated + + @patch("redis.Redis.sismember", lambda x, y, z: False) + def test_push_updates_only_unmerged_commits_with_branch_name(self): + commit1 = CommitFactory(merged=False, repository=self.repo) + commit2 = CommitFactory(merged=False, repository=self.repo) + + merged_branch_name = "merged" + unmerged_branch_name = "unmerged" + + merged_commit = CommitFactory( + merged=True, repository=self.repo, branch=merged_branch_name + ) + + self._post_event_data( + event=GitHubWebhookEvents.PUSH, + data={ + "ref": "refs/heads/" + unmerged_branch_name, + "repository": {"id": self.repo.service_id}, + "commits": [ + {"id": commit1.commitid, "message": commit1.message}, + {"id": commit2.commitid, "message": commit2.message}, + {"id": merged_commit.commitid, "message": merged_commit.message}, + ], + }, + ) + + commit1.refresh_from_db() + commit2.refresh_from_db() + merged_commit.refresh_from_db() + + assert not commit1.merged + assert not commit2.merged + + assert merged_commit.branch == merged_branch_name + + @patch("redis.Redis.sismember", lambda x, y, z: False) + def test_push_updates_commit_on_default_branch(self): + commit1 = CommitFactory( + merged=False, repository=self.repo, branch="feature-branch" + ) + commit2 = CommitFactory( + merged=False, repository=self.repo, branch="feature-branch" + ) + + merged_branch_name = "merged" + repo_branch = self.repo.branch + + merged_commit = CommitFactory( + merged=True, repository=self.repo, branch=merged_branch_name + ) + + self._post_event_data( + event=GitHubWebhookEvents.PUSH, + data={ + "ref": "refs/heads/" + repo_branch, + "repository": {"id": self.repo.service_id}, + "commits": [ + {"id": commit1.commitid, "message": commit1.message}, + {"id": commit2.commitid, "message": commit2.message}, + {"id": merged_commit.commitid, "message": merged_commit.message}, + ], + }, + ) + + commit1.refresh_from_db() + commit2.refresh_from_db() + merged_commit.refresh_from_db() + + assert commit1.branch == repo_branch + assert commit2.branch == repo_branch + assert commit1.merged + assert commit2.merged + + assert merged_commit.branch == merged_branch_name + + def test_push_exits_early_with_200_if_repo_not_active(self): + self.repo.active = False + self.repo.save() + unmerged_commit = CommitFactory(repository=self.repo, merged=False) + branch_name = "new-branch-name" + + response = self._post_event_data( + event=GitHubWebhookEvents.PUSH, + data={ + "ref": "refs/heads/" + branch_name, + "repository": {"id": self.repo.service_id}, + "commits": [ + {"id": unmerged_commit.commitid, "message": unmerged_commit.message} + ], + }, + ) + + assert response.status_code == status.HTTP_200_OK + + unmerged_commit.refresh_from_db() + assert unmerged_commit.branch != branch_name + + @patch("webhook_handlers.views.github.get_config") + def test_push_exits_early_with_200_if_repo_name_is_ignored(self, get_config_mock): + get_config_mock.side_effect = [WEBHOOK_SECRET.decode("utf-8"), [self.repo.name]] + + self.repo.save() + unmerged_commit = CommitFactory(repository=self.repo, merged=False) + branch_name = "new-branch-name" + + response = self._post_event_data( + event=GitHubWebhookEvents.PUSH, + data={ + "ref": "refs/heads/" + branch_name, + "repository": {"id": self.repo.service_id}, + "commits": [ + {"id": unmerged_commit.commitid, "message": unmerged_commit.message} + ], + }, + ) + + assert response.status_code == status.HTTP_200_OK + + unmerged_commit.refresh_from_db() + + assert unmerged_commit.branch != branch_name + + @patch("redis.Redis.sismember", lambda x, y, z: True) + @patch("services.task.TaskService.status_set_pending") + def test_push_triggers_set_pending_task_on_most_recent_commit( + self, set_pending_mock + ): + commit1 = CommitFactory(merged=False, repository=self.repo) + commit2 = CommitFactory(merged=False, repository=self.repo) + unmerged_branch_name = "unmerged" + + self._post_event_data( + event=GitHubWebhookEvents.PUSH, + data={ + "ref": "refs/heads/" + unmerged_branch_name, + "repository": {"id": self.repo.service_id}, + "commits": [ + {"id": commit1.commitid, "message": commit1.message}, + {"id": commit2.commitid, "message": commit2.message}, + ], + }, + ) + + set_pending_mock.assert_called_once_with( + repoid=self.repo.repoid, + commitid=commit2.commitid, + branch=unmerged_branch_name, + on_a_pull_request=False, + ) + + @patch("redis.Redis.sismember", lambda x, y, z: False) + @patch("services.task.TaskService.status_set_pending") + def test_push_doesnt_trigger_task_if_repo_not_part_of_beta_set( + self, set_pending_mock + ): + commit1 = CommitFactory(merged=False, repository=self.repo) + + self._post_event_data( + event=GitHubWebhookEvents.PUSH, + data={ + "ref": "refs/heads/" + "derp", + "repository": {"id": self.repo.service_id}, + "commits": [{"id": commit1.commitid, "message": commit1.message}], + }, + ) + + set_pending_mock.assert_not_called() + + @patch("redis.Redis.sismember", lambda x, y, z: True) + @patch("services.task.TaskService.status_set_pending") + def test_push_doesnt_trigger_task_if_ci_skipped(self, set_pending_mock): + commit1 = CommitFactory(merged=False, repository=self.repo, message="[ci skip]") + + response = self._post_event_data( + event=GitHubWebhookEvents.PUSH, + data={ + "ref": "refs/heads/" + "derp", + "repository": {"id": self.repo.service_id}, + "commits": [{"id": commit1.commitid, "message": commit1.message}], + }, + ) + + assert response.data == "CI Skipped" + set_pending_mock.assert_not_called() + + def test_status_exits_early_if_repo_not_active(self): + self.repo.active = False + self.repo.save() + + response = self._post_event_data( + event=GitHubWebhookEvents.STATUS, + data={"repository": {"id": self.repo.service_id}}, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_NOT_ACTIVE + + def test_status_exits_early_for_codecov_statuses(self): + response = self._post_event_data( + event=GitHubWebhookEvents.STATUS, + data={"context": "codecov/", "repository": {"id": self.repo.service_id}}, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_CODECOV_STATUS + + def test_status_exits_early_for_pending_statuses(self): + response = self._post_event_data( + event=GitHubWebhookEvents.STATUS, + data={"state": "pending", "repository": {"id": self.repo.service_id}}, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_PENDING_STATUSES + + def test_status_exits_early_if_commit_not_complete(self): + response = self._post_event_data( + event=GitHubWebhookEvents.STATUS, + data={ + "repository": {"id": self.repo.service_id}, + "sha": CommitFactory(repository=self.repo, state="pending").commitid, + }, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_PROCESSING + + @patch("services.task.TaskService.notify") + def test_status_triggers_notify_task(self, notify_mock): + commit = CommitFactory(repository=self.repo) + response = self._post_event_data( + event=GitHubWebhookEvents.STATUS, + data={"repository": {"id": self.repo.service_id}, "sha": commit.commitid}, + ) + + assert response.status_code == status.HTTP_200_OK + notify_mock.assert_called_once_with( + repoid=self.repo.repoid, commitid=commit.commitid + ) + + def test_pull_request_exits_early_if_repo_not_active(self): + self.repo.active = False + self.repo.save() + + response = self._post_event_data( + event=GitHubWebhookEvents.PULL_REQUEST, + data={"repository": {"id": self.repo.service_id}}, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_NOT_ACTIVE + + @patch("services.task.TaskService.pulls_sync") + def test_pull_request_triggers_pulls_sync_task_for_valid_actions( + self, pulls_sync_mock + ): + pull = PullFactory(repository=self.repo) + + valid_actions = ["opened", "closed", "reopened", "synchronize"] + + for action in valid_actions: + self._post_event_data( + event=GitHubWebhookEvents.PULL_REQUEST, + data={ + "repository": {"id": self.repo.service_id}, + "action": action, + "number": pull.pullid, + }, + ) + + pulls_sync_mock.assert_has_calls( + [call(repoid=self.repo.repoid, pullid=pull.pullid)] * len(valid_actions) + ) + + def test_pull_request_updates_title_if_edited(self): + pull = PullFactory(repository=self.repo) + new_title = "brand new dang title" + response = self._post_event_data( + event=GitHubWebhookEvents.PULL_REQUEST, + data={ + "repository": {"id": self.repo.service_id}, + "action": "edited", + "number": pull.pullid, + "pull_request": {"title": new_title}, + }, + ) + + assert response.status_code == status.HTTP_200_OK + + pull.refresh_from_db() + assert pull.title == new_title + + @freeze_time("2024-03-28T00:00:00") + @patch("services.task.TaskService.refresh") + def test_installation_creates_new_owner_if_dne_default_app(self, mock_refresh): + username, service_id = "newuser", 123456 + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION, + data={ + "installation": { + "id": 4, + "repository_selection": "selected", + "account": {"id": service_id, "login": username}, + "app_id": DEFAULT_APP_ID, + }, + "repositories": [ + {"id": "12321", "node_id": "R_kgDOG2tZYQ"}, + {"id": "12343", "node_id": "R_kgDOG2tABC"}, + ], + "sender": {"type": "User"}, + }, + ) + + owner_set = Owner.objects.filter( + service="github", service_id=service_id, username=username + ) + + assert owner_set.exists() + + owner = owner_set.first() + assert owner.createstamp.isoformat() == "2024-03-28T00:00:00+00:00" + + ghapp_installations_set = GithubAppInstallation.objects.filter( + owner_id=owner.ownerid + ) + assert ghapp_installations_set.count() == 1 + installation = ghapp_installations_set.first() + assert installation.installation_id == 4 + assert installation.app_id == DEFAULT_APP_ID + assert installation.name == GITHUB_APP_INSTALLATION_DEFAULT_NAME + assert installation.repository_service_ids == ["12321", "12343"] + + assert mock_refresh.call_count == 1 + _, kwargs = mock_refresh.call_args_list[0] + # Because we throw these into a set we need to order them here + # In practive it doesn't matter, but for the test it does. + kwargs["repos_affected"].sort() + assert kwargs == dict( + ownerid=owner.ownerid, + username=username, + sync_teams=False, + sync_repos=True, + using_integration=True, + repos_affected=[("12321", "R_kgDOG2tZYQ"), ("12343", "R_kgDOG2tABC")], + ) + + @patch("shared.events.amplitude.AmplitudeEventPublisher.publish") + @patch("services.task.TaskService.refresh") + def test_installation_publishes_amplitude_event_without_installer( + self, mock_refresh, mock_amplitude_publish + ): + username, service_id = "newuser", 123456 + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION, + data={ + "installation": { + "id": 4, + "repository_selection": "selected", + "account": {"id": service_id, "login": username}, + "app_id": DEFAULT_APP_ID, + }, + "repositories": [ + {"id": "12321", "node_id": "R_kgDOG2tZYQ"}, + {"id": "12343", "node_id": "R_kgDOG2tABC"}, + ], + "sender": {"type": "User"}, + }, + ) + + owner_set = Owner.objects.filter( + service="github", service_id=service_id, username=username + ) + assert owner_set.exists() + owner = owner_set.first() + + mock_amplitude_publish.assert_called_with( + "App Installed", + { + "user_ownerid": owner.ownerid, + "ownerid": owner.ownerid, + }, + ) + + @patch("shared.events.amplitude.AmplitudeEventPublisher.publish") + @patch("services.task.TaskService.refresh") + def test_installation_publishes_amplitude_event_with_installer( + self, mock_refresh, mock_amplitude_publish + ): + installer = OwnerFactory(service="github", username="installer_username") + + username, service_id = "newuser", 123456 + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION, + data={ + "installation": { + "id": 4, + "repository_selection": "selected", + "account": {"id": service_id, "login": username}, + "app_id": DEFAULT_APP_ID, + }, + "repositories": [ + {"id": "12321", "node_id": "R_kgDOG2tZYQ"}, + {"id": "12343", "node_id": "R_kgDOG2tABC"}, + ], + "sender": {"type": "User", "login": "installer_username"}, + }, + ) + + owner_set = Owner.objects.filter( + service="github", service_id=service_id, username=username + ) + assert owner_set.exists() + owner = owner_set.first() + + mock_amplitude_publish.assert_called_with( + "App Installed", + { + "user_ownerid": installer.ownerid, + "ownerid": owner.ownerid, + }, + ) + + @patch( + "services.task.TaskService.refresh", + lambda self, + ownerid, + username, + sync_teams, + sync_repos, + using_integration, + repos_affected: None, + ) + def test_installation_creates_new_owner_if_dne_all_repos_non_default_app(self): + username, service_id = "newuser", 123456 + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION, + data={ + "installation": { + "id": 4, + "repository_selection": "all", + "account": {"id": service_id, "login": username}, + "app_id": 15, + }, + "repositories": [ + {"id": "12321", "node_id": "R_kgDOG2tZYQ"}, + {"id": "12343", "node_id": "R_kgDOG2tABC"}, + ], + "sender": {"type": "User"}, + }, + ) + + owner_set = Owner.objects.filter( + service="github", service_id=service_id, username=username + ) + + assert owner_set.exists() + + owner = owner_set.first() + + ghapp_installations_set = GithubAppInstallation.objects.filter( + owner_id=owner.ownerid + ) + assert ghapp_installations_set.count() == 1 + installation = ghapp_installations_set.first() + assert installation.installation_id == 4 + assert installation.app_id == 15 + assert installation.name == "unconfigured_app" + assert installation.repository_service_ids is None + + @patch( + "services.task.TaskService.refresh", + lambda self, + ownerid, + username, + sync_teams, + sync_repos, + using_integration, + repos_affected: None, + ) + def test_installation_repositories_creates_new_owner_if_dne(self): + username, service_id = "newuser", 123456 + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION_REPOSITORIES, + data={ + "installation": { + "id": 4, + "repository_selection": "all", + "account": {"id": service_id, "login": username}, + "app_id": 15, + }, + "repository_selection": "all", + "sender": {"type": "User"}, + }, + ) + + owner_set = Owner.objects.filter(service="github", service_id=service_id) + + assert owner_set.exists() + + owner = owner_set.first() + + ghapp_installations_set = GithubAppInstallation.objects.filter( + owner_id=owner.ownerid + ) + assert ghapp_installations_set.count() == 1 + installation = ghapp_installations_set.first() + assert installation.installation_id == 4 + assert installation.app_id == 15 + assert installation.name == "unconfigured_app" + assert installation.repository_service_ids is None + + @patch( + "services.task.TaskService.refresh", + lambda self, + ownerid, + username, + sync_teams, + sync_repos, + using_integration, + repos_affected: None, + ) + def test_installation_update_repos_existing_ghapp_installation(self): + owner = OwnerFactory(service=Service.GITHUB.value) + owner.save() + installation = GithubAppInstallation( + owner=owner, + repository_service_ids=["repo1", "repo2"], + installation_id=4, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + ) + installation.save() + assert owner.github_app_installations.count() == 1 + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION, + data={ + "installation": { + "id": 4, + "repository_selection": "selected", + "account": {"id": owner.service_id, "login": owner.username}, + "app_id": 15, + }, + "repositories": [ + {"id": "repo1", "node_id": "R_node1"}, + {"id": "repo2", "node_id": "R_node2"}, + {"id": "repo3", "node_id": "R_node3"}, + ], + "sender": {"type": "User"}, + }, + ) + + owner.refresh_from_db() + installation.refresh_from_db() + + assert ( + owner.github_app_installations.count() == 1 + ) # no new installations created + installation = owner.github_app_installations.first() + assert installation.installation_id == 4 + # This installation changed names because it's not configured + # AND doesn't have the default app id + assert installation.name == "unconfigured_app" + assert installation.repository_service_ids == ["repo1", "repo2", "repo3"] + + def test_installation_with_deleted_action_nulls_values(self): + # Should set integration_id to null for owner, + # and set using_integration=False and bot=null for repos + owner = OwnerFactory(service=Service.GITHUB.value) + repo1 = RepositoryFactory(author=owner) + repo2 = RepositoryFactory(author=owner) + + owner.integration_id = 12 + owner.save() + + repo1.using_integration, repo2.using_integration = True, True + repo1.bot, repo2.bot = owner, owner + + repo1.save() + repo2.save() + + ghapp_installation = GithubAppInstallation( + installation_id=25, + repository_service_ids=[repo1.service_id, repo2.service_id], + owner=owner, + ) + ghapp_installation.save() + + assert owner.github_app_installations.exists() + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION, + data={ + "installation": { + "id": 25, + "repository_selection": "selected", + "account": {"id": owner.service_id, "login": owner.username}, + "app_id": 15, + }, + "repositories": [ + {"id": "12321", "node_id": "R_kgDOG2tZYQ"}, + {"id": "12343", "node_id": "R_kgDOG2tABC"}, + ], + "action": "deleted", + "sender": {"type": "User"}, + }, + ) + + owner.refresh_from_db() + repo1.refresh_from_db() + repo2.refresh_from_db() + + assert owner.integration_id is None + assert repo1.using_integration == False + assert repo2.using_integration == False + + assert repo1.bot is None + assert repo2.bot is None + + assert not owner.github_app_installations.exists() + + @patch( + "services.task.TaskService.refresh", + lambda self, + ownerid, + username, + sync_teams, + sync_repos, + using_integration, + repos_affected: None, + ) + def test_installation_repositories_update_existing_ghapp(self): + # Should set integration_id to null for owner, + # and set using_integration=False and bot=null for repos + owner = OwnerFactory(service=Service.GITHUB.value) + repo1 = RepositoryFactory(author=owner) + repo2 = RepositoryFactory(author=owner) + installation = GithubAppInstallation( + owner=owner, + repository_service_ids=[repo1.service_id], + installation_id=12, + app_id=2500, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + pem_path="some_path", + ) + owner.save() + repo1.save() + repo2.save() + installation.save() + + assert owner.github_app_installations.exists() + assert installation.is_repo_covered_by_integration(repo2) is False + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION_REPOSITORIES, + data={ + "installation": { + "id": installation.installation_id, + "repository_selection": "selected", + "account": {"id": owner.service_id, "login": owner.username}, + "app_id": 15, + }, + "repositories_added": [ + {"id": repo2.service_id, "node_id": "R_xDOGxCAT"} + ], + "repositories_removed": [ + {"id": repo1.service_id, "node_id": "R_xCATxCAT"} + ], + "repository_selection": "selected", + "action": "added", + "sender": {"type": "User"}, + }, + ) + + installation.refresh_from_db() + assert installation.installation_id == 12 + # This app is not the default app, but it's configured + # So it should keep it's name + assert installation.app_id != DEFAULT_APP_ID + assert installation.name == GITHUB_APP_INSTALLATION_DEFAULT_NAME + assert installation.repository_service_ids == [repo2.service_id] + assert installation.is_repo_covered_by_integration(repo2) is True + + @patch( + "services.task.TaskService.refresh", + lambda self, ownerid, username, sync_teams, sync_repos, using_integration: None, + ) + def test_installation_repositories_update_existing_ghapp_all_repos(self): + # Should set integration_id to null for owner, + # and set using_integration=False and bot=null for repos + owner = OwnerFactory(service=Service.GITHUB.value) + repo1 = RepositoryFactory(author=owner) + repo2 = RepositoryFactory(author=owner) + installation = GithubAppInstallation( + owner=owner, repository_service_ids=[repo1.service_id], installation_id=12 + ) + + owner.save() + repo1.save() + repo2.save() + installation.save() + + assert owner.github_app_installations.exists() + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION_REPOSITORIES, + data={ + "installation": { + "id": 12, + "repository_selection": "all", + "account": {"id": owner.service_id, "login": owner.username}, + "app_id": 15, + }, + "repositories_added": [{"id": repo2.service_id}], + "repositories_removed": [], + "repository_selection": "all", + "action": "deleted", + "sender": {"type": "User"}, + }, + ) + + installation.refresh_from_db() + assert installation.installation_id == 12 + assert installation.repository_service_ids is None + + @patch( + "services.task.TaskService.refresh", + lambda self, + ownerid, + username, + sync_teams, + sync_repos, + using_integration, + repos_affected: None, + ) + def test_installation_with_other_actions_sets_owner_integration_id_if_none( + self, + ): + installation_id = 44 + owner = OwnerFactory(service=Service.GITHUB.value) + + owner.integration_id = None + owner.save() + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION, + data={ + "installation": { + "id": installation_id, + "repository_selection": "selected", + "account": {"id": owner.service_id, "login": owner.username}, + "app_id": DEFAULT_APP_ID, + }, + "repositories": [ + {"id": "12321", "node_id": "R_kgDOG2tZYQ"}, + {"id": "12343", "node_id": "R_kgDOG2tABC"}, + ], + "action": "suspend", + "sender": {"type": "User"}, + }, + ) + + owner.refresh_from_db() + + assert owner.integration_id == installation_id + + ghapp_installations_set = GithubAppInstallation.objects.filter( + owner_id=owner.ownerid + ) + assert ghapp_installations_set.count() == 1 + installation = ghapp_installations_set.first() + assert installation.installation_id == installation_id + assert installation.app_id == DEFAULT_APP_ID + assert installation.name == GITHUB_APP_INSTALLATION_DEFAULT_NAME + assert installation.is_suspended == True + assert installation.repository_service_ids == ["12321", "12343"] + + @patch( + "services.task.TaskService.refresh", + lambda self, + ownerid, + username, + sync_teams, + sync_repos, + using_integration, + repos_affected: None, + ) + def test_installation_repositories_with_other_actions_sets_owner_itegration_id_if_none( + self, + ): + installation_id = 44 + owner = OwnerFactory(service=Service.GITHUB.value) + + owner.integration_id = None + owner.save() + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION_REPOSITORIES, + data={ + "installation": { + "id": installation_id, + "repository_selection": "all", + "account": {"id": owner.service_id, "login": owner.username}, + "app_id": 15, + }, + "repository_selection": "all", + "action": "added", + "sender": {"type": "User"}, + }, + ) + + owner.refresh_from_db() + + assert owner.integration_id == installation_id + + ghapp_installations_set = GithubAppInstallation.objects.filter( + owner_id=owner.ownerid + ) + assert ghapp_installations_set.count() == 1 + installation = ghapp_installations_set.first() + assert installation.installation_id == installation_id + assert installation.repository_service_ids is None + + @patch("services.task.TaskService.refresh") + def test_installation_trigger_refresh_with_other_actions(self, refresh_mock): + owner = OwnerFactory(service=Service.GITHUB.value) + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION, + data={ + "installation": { + "id": 11, + "repository_selection": "selected", + "account": {"id": owner.service_id, "login": owner.username}, + "app_id": 15, + }, + "action": "added", + "sender": {"type": "User"}, + "repositories": [ + {"id": "12321", "node_id": "R_kgDOG2tZYQ"}, + {"id": "12343", "node_id": "R_kgDOG2tABC"}, + ], + }, + ) + + assert refresh_mock.call_count == 1 + _, kwargs = refresh_mock.call_args_list[0] + # Because we throw these into a set we need to order them here + # In practive it doesn't matter, but for the test it does. + kwargs["repos_affected"].sort() + assert kwargs == dict( + ownerid=owner.ownerid, + username=owner.username, + sync_teams=False, + sync_repos=True, + using_integration=True, + repos_affected=[("12321", "R_kgDOG2tZYQ"), ("12343", "R_kgDOG2tABC")], + ) + + @patch("services.task.TaskService.refresh") + def test_organization_with_removed_action_removes_user_from_org_and_activated_user_list( + self, + mock_refresh, + ): + org = OwnerFactory(service_id="4321", service=Service.GITHUB.value) + user = OwnerFactory( + organizations=[org.ownerid], service_id="12", service=Service.GITHUB.value + ) + org.plan_activated_users = [user.ownerid] + org.save() + + self._post_event_data( + event=GitHubWebhookEvents.ORGANIZATION, + data={ + "action": "member_removed", + "membership": {"user": {"id": user.service_id}}, + "organization": {"id": org.service_id}, + }, + ) + + user.refresh_from_db() + org.refresh_from_db() + + mock_refresh.assert_called_with( + ownerid=user.ownerid, + username=user.username, + sync_teams=True, + sync_repos=True, + using_integration=False, + ) + assert user.ownerid not in org.plan_activated_users + + def test_organization_member_removed_with_nonexistent_org_doesnt_crash(self): + user = OwnerFactory(service_id="12", service=Service.GITHUB.value) + + response = self._post_event_data( + event=GitHubWebhookEvents.ORGANIZATION, + data={ + "action": "member_removed", + "membership": {"user": {"id": user.service_id}}, + "organization": {"id": 65000}, + }, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_organization_member_removed_with_nonexistent_or_nonactivated_member(self): + mock_all_plans_and_tiers() + org = OwnerFactory( + service_id="4321", + plan_activated_users=[50392], + service=Service.GITHUB.value, + ) + user = OwnerFactory( + service_id="12", organizations=[60798], service=Service.GITHUB.value + ) + + response = self._post_event_data( + event=GitHubWebhookEvents.ORGANIZATION, + data={ + "action": "member_removed", + "membership": {"user": {"id": user.service_id}}, + "organization": {"id": org.service_id}, + }, + ) + + assert response.status_code == status.HTTP_200_OK + + def test_organization_member_removed_with_nonexistent_member_doesnt_crash(self): + org = OwnerFactory(service_id="4321", service=Service.GITHUB.value) + + response = self._post_event_data( + event=GitHubWebhookEvents.ORGANIZATION, + data={ + "action": "member_removed", + "membership": {"user": {"id": 101010}}, + "organization": {"id": org.service_id}, + }, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.task.TaskService.sync_plans") + def test_marketplace_purchase_triggers_sync_plans_task( + self, sync_plans_mock, subscription_retrieve_mock + ): + sender = {"id": 545, "login": "buddy@guy.com"} + action = "purchased" + account = {"type": "Organization", "id": 54678, "login": "username"} + subscription_retrieve_mock.return_value = None + self._post_event_data( + event=GitHubWebhookEvents.MARKETPLACE_PURCHASE, + data={ + "action": action, + "sender": sender, + "marketplace_purchase": {"account": account}, + }, + ) + + sync_plans_mock.assert_called_once_with( + sender=sender, account=account, action=action + ) + + @patch("logging.Logger.warning") + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.task.TaskService.sync_plans") + def test_marketplace_purchase_but_user_has_stripe_subscription( + self, sync_plans_mock, subscription_retrieve_mock, log_warning_mock + ): + sender = {"id": 545, "login": "buddy@guy.com"} + action = "purchased" + account = {"type": "Organization", "id": 54678, "login": "username"} + OwnerFactory( + username=account["login"], service="github", stripe_subscription_id="abc" + ) + quantity = 14 + plan = PlanName.CODECOV_PRO_MONTHLY.value + subscription_retrieve_mock.return_value = MockedSubscription( + "active", plan, quantity + ) + self._post_event_data( + event=GitHubWebhookEvents.MARKETPLACE_PURCHASE, + data={ + "action": action, + "sender": sender, + "marketplace_purchase": { + "account": account, + "plan": {"name": "gh-marketplace"}, + "unit_count": 200, + }, + }, + ) + + log_warning_mock.assert_called_with( + "GHM webhook - user purchasing but has a Stripe Subscription", + extra={ + "username": "username", + "old_plan_name": plan, + "old_plan_seats": quantity, + "new_plan_name": "gh-marketplace", + "new_plan_seats": 200, + }, + ) + + sync_plans_mock.assert_called_once_with( + sender=sender, account=account, action=action + ) + + def test_signature_validation(self): + response = self.client.post( + reverse("github-webhook"), + **{ + GitHubHTTPHeaders.EVENT: "", + GitHubHTTPHeaders.DELIVERY_TOKEN: uuid.UUID(int=5), + GitHubHTTPHeaders.SIGNATURE: "", + }, + data={}, + format="json", + ) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + response = self.client.post( + reverse("github-webhook"), + **{ + GitHubHTTPHeaders.EVENT: "", + GitHubHTTPHeaders.DELIVERY_TOKEN: uuid.UUID(int=5), + GitHubHTTPHeaders.SIGNATURE_256: "sha256=" + + hmac.new( + WEBHOOK_SECRET, + json.dumps({}, separators=(",", ":")).encode("utf-8"), + digestmod=sha256, + ).hexdigest(), + }, + data={}, + format="json", + ) + + assert response.status_code == status.HTTP_200_OK + + response = self.client.post( + reverse("github-webhook"), + **{ + GitHubHTTPHeaders.EVENT: "", + GitHubHTTPHeaders.DELIVERY_TOKEN: uuid.UUID(int=5), + GitHubHTTPHeaders.SIGNATURE: "sha1=" + + hmac.new( + WEBHOOK_SECRET, + json.dumps({}, separators=(",", ":")).encode("utf-8"), + digestmod=sha1, + ).hexdigest(), + }, + data={}, + format="json", + ) + + assert response.status_code == status.HTTP_200_OK + + @patch("webhook_handlers.views.github.get_config") + def test_signature_validation_with_string_key(self, get_config_mock): + # The hmac function requires a bytestring, and we're creating hmacs + # throughout these tests so we've been using bytestrings. However, + # `get_config` normally returns a UTF-8 string; make sure that still + # works. + get_config_mock.return_value = WEBHOOK_SECRET.decode("utf-8") + response = self._post_event_data(event="", data={}) + assert response.status_code == status.HTTP_200_OK + + def test_member_removes_repo_permissions_if_member_removed(self): + member = OwnerFactory( + permission=[self.repo.repoid], service_id=6098, service=Service.GITHUB.value + ) + self._post_event_data( + event=GitHubWebhookEvents.MEMBER, + data={ + "action": "removed", + "member": {"id": member.service_id}, + "repository": {"id": self.repo.service_id}, + }, + ) + + member.refresh_from_db() + assert self.repo.repoid not in member.permission + + def test_member_doesnt_crash_if_member_permission_array_is_None(self): + member = OwnerFactory( + permission=None, service_id=6098, service=Service.GITHUB.value + ) + self._post_event_data( + event=GitHubWebhookEvents.MEMBER, + data={ + "action": "removed", + "member": {"id": member.service_id}, + "repository": {"id": self.repo.service_id}, + }, + ) + + def test_member_doesnt_crash_if_member_didnt_have_permission(self): + member = OwnerFactory( + permission=[self.repo.service_id + 1], + service_id=6098, + service=Service.GITHUB.value, + ) + self._post_event_data( + event=GitHubWebhookEvents.MEMBER, + data={ + "action": "removed", + "member": {"id": member.service_id}, + "repository": {"id": self.repo.service_id}, + }, + ) + + def test_member_doesnt_crash_if_member_dne(self): + response = self._post_event_data( + event=GitHubWebhookEvents.MEMBER, + data={ + "action": "removed", + "member": {"id": 604945829}, # some random number + "repository": {"id": self.repo.service_id}, + }, + ) + + assert response.status_code == 404 + + def test_returns_404_if_repo_not_found(self): + response = self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={"action": "publicized", "repository": {"id": -29384}}, + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_repo_not_found_when_owner_has_integration_creates_repo(self): + owner = OwnerFactory( + integration_id=4850403, service_id=97968493, service=Service.GITHUB.value + ) + self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={ + "action": "publicized", + "repository": { + "id": 506003, + "name": "testrepo", + "private": False, + "default_branch": "master", + "owner": {"id": owner.service_id}, + }, + }, + ) + + assert owner.repository_set.filter(name="testrepo").exists() + + def test_repo_creation_doesnt_crash_for_forked_repo(self): + owner = OwnerFactory( + integration_id=4850403, service_id=97968493, service=Service.GITHUB.value + ) + self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={ + "action": "publicized", + "repository": { + "id": 506003, + "name": "testrepo", + "private": False, + "default_branch": "master", + "owner": {"id": owner.service_id}, + "fork": True, + "parent": { + "name": "mainrepo", + "language": "python", + "id": 7940284, + "private": False, + "default_branch": "master", + "owner": {"id": 8495712939, "login": "alogin"}, + }, + }, + }, + ) + + assert owner.repository_set.filter(name="testrepo").exists() diff --git a/apps/codecov-api/webhook_handlers/tests/test_github_enterprise.py b/apps/codecov-api/webhook_handlers/tests/test_github_enterprise.py new file mode 100644 index 0000000000..523074c6da --- /dev/null +++ b/apps/codecov-api/webhook_handlers/tests/test_github_enterprise.py @@ -0,0 +1,1197 @@ +import hmac +import json +import uuid +from hashlib import sha256 +from unittest.mock import call, patch + +import pytest +from freezegun import freeze_time +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.django_apps.core.tests.factories import ( + BranchFactory, + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) +from shared.plan.constants import PlanName + +from billing.helpers import mock_all_plans_and_tiers +from codecov_auth.models import GithubAppInstallation, Owner, Service +from utils.config import get_config +from webhook_handlers.constants import ( + GitHubHTTPHeaders, + GitHubWebhookEvents, + WebhookHandlerErrorMessages, +) +from webhook_handlers.tests.test_github import MockedSubscription + +WEBHOOK_SECRET = b"testixik8qdauiab1yiffydimvi72ekq" + + +class GithubEnterpriseWebhookHandlerTests(APITestCase): + @pytest.fixture(autouse=True) + def mock_webhook_secret(self, mocker): + orig_get_config = get_config + + def override(*args, default=None): + if args[0] == "github" and args[1] == "webhook_secret": + return WEBHOOK_SECRET + + return orig_get_config(*args, default=default) + + mock_get_config = mocker.patch("webhook_handlers.views.github.get_config") + mock_get_config.side_effect = override + + def _post_event_data(self, event, data={}): + return self.client.post( + reverse("github_enterprise-webhook"), + **{ + GitHubHTTPHeaders.EVENT: event, + GitHubHTTPHeaders.DELIVERY_TOKEN: uuid.UUID(int=5), + GitHubHTTPHeaders.SIGNATURE_256: "sha256=" + + hmac.new( + WEBHOOK_SECRET, + json.dumps(data, separators=(",", ":")).encode("utf-8"), + digestmod=sha256, + ).hexdigest(), + }, + data=data, + format="json", + ) + + def setUp(self): + self.repo = RepositoryFactory( + author=OwnerFactory(service=Service.GITHUB_ENTERPRISE.value), + service_id=12345, + active=True, + ) + + def test_get_repo_paths_dont_crash(self): + with self.subTest("with ownerid success"): + self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={ + "action": "publicized", + "repository": { + "id": self.repo.service_id, + "owner": {"id": self.repo.author.service_id}, + }, + }, + ) + + with self.subTest("with not found owner"): + self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={ + "action": "publicized", + "repository": { + "id": self.repo.service_id, + "owner": {"id": -239450}, + }, + }, + ) + + with self.subTest("with not found owner and not found repo"): + self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={ + "action": "publicized", + "repository": {"id": -1948503, "owner": {"id": -239450}}, + }, + ) + + with self.subTest("with owner and not found repo"): + self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={ + "action": "publicized", + "repository": { + "id": -1948503, + "owner": {"id": self.repo.author.service_id}, + }, + }, + ) + + def test_ping_returns_pong_and_200(self): + response = self._post_event_data(event=GitHubWebhookEvents.PING) + assert response.status_code == status.HTTP_200_OK + + def test_repository_publicized_sets_activated_false_and_private_false(self): + self.repo.private = True + self.repo.activated = True + + self.repo.save() + + response = self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={"action": "publicized", "repository": {"id": self.repo.service_id}}, + ) + + assert response.status_code == status.HTTP_200_OK + + self.repo.refresh_from_db() + + assert self.repo.private == False + assert self.repo.activated == False + + def test_repository_privatized_sets_private_true(self): + self.repo.private = False + self.repo.save() + + response = self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={"action": "privatized", "repository": {"id": self.repo.service_id}}, + ) + + assert response.status_code == status.HTTP_200_OK + + self.repo.refresh_from_db() + + assert self.repo.private == True + + def test_repository_deleted_sets_deleted_activated_and_active(self): + response = self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={"action": "deleted", "repository": {"id": self.repo.service_id}}, + ) + + assert response.status_code == status.HTTP_200_OK + self.repo.refresh_from_db() + assert self.repo.deleted is True + assert self.repo.active is False + assert self.repo.activated is False + + def test_delete_event_deletes_branch(self): + branch = BranchFactory(repository=self.repo) + + response = self._post_event_data( + event=GitHubWebhookEvents.DELETE, + data={ + "ref": "refs/heads/" + branch.name, + "ref_type": "branch", + "repository": {"id": self.repo.service_id}, + }, + ) + + assert response.status_code == status.HTTP_200_OK + assert not self.repo.branches.filter(name=branch.name).exists() + + def test_public_sets_repo_private_false_and_activated_false(self): + self.repo.private = True + self.repo.activated = True + self.repo.save() + + response = self._post_event_data( + event=GitHubWebhookEvents.PUBLIC, + data={"repository": {"id": self.repo.service_id}}, + ) + + assert response.status_code == status.HTTP_200_OK + self.repo.refresh_from_db() + assert not self.repo.private + assert not self.repo.activated + + @patch("redis.Redis.sismember", lambda x, y, z: False) + def test_push_updates_only_unmerged_commits_with_branch_name(self): + commit1 = CommitFactory(merged=False, repository=self.repo) + commit2 = CommitFactory(merged=False, repository=self.repo) + + merged_branch_name = "merged" + unmerged_branch_name = "unmerged" + + merged_commit = CommitFactory( + merged=True, repository=self.repo, branch=merged_branch_name + ) + + self._post_event_data( + event=GitHubWebhookEvents.PUSH, + data={ + "ref": "refs/heads/" + unmerged_branch_name, + "repository": {"id": self.repo.service_id}, + "commits": [ + {"id": commit1.commitid, "message": commit1.message}, + {"id": commit2.commitid, "message": commit2.message}, + {"id": merged_commit.commitid, "message": merged_commit.message}, + ], + }, + ) + + commit1.refresh_from_db() + commit2.refresh_from_db() + merged_commit.refresh_from_db() + + assert not commit1.merged + assert not commit2.merged + + assert merged_commit.branch == merged_branch_name + + @patch("redis.Redis.sismember", lambda x, y, z: False) + def test_push_updates_commit_on_default_branch(self): + commit1 = CommitFactory( + merged=False, repository=self.repo, branch="feature-branch" + ) + commit2 = CommitFactory( + merged=False, repository=self.repo, branch="feature-branch" + ) + + merged_branch_name = "merged" + repo_branch = self.repo.branch + + merged_commit = CommitFactory( + merged=True, repository=self.repo, branch=merged_branch_name + ) + + self._post_event_data( + event=GitHubWebhookEvents.PUSH, + data={ + "ref": "refs/heads/" + repo_branch, + "repository": {"id": self.repo.service_id}, + "commits": [ + {"id": commit1.commitid, "message": commit1.message}, + {"id": commit2.commitid, "message": commit2.message}, + {"id": merged_commit.commitid, "message": merged_commit.message}, + ], + }, + ) + + commit1.refresh_from_db() + commit2.refresh_from_db() + merged_commit.refresh_from_db() + + assert commit1.branch == repo_branch + assert commit2.branch == repo_branch + assert commit1.merged + assert commit2.merged + + assert merged_commit.branch == merged_branch_name + + def test_push_exits_early_with_200_if_repo_not_active(self): + self.repo.active = False + self.repo.save() + unmerged_commit = CommitFactory(repository=self.repo, merged=False) + branch_name = "new-branch-name" + + response = self._post_event_data( + event=GitHubWebhookEvents.PUSH, + data={ + "ref": "refs/heads/" + branch_name, + "repository": {"id": self.repo.service_id}, + "commits": [ + {"id": unmerged_commit.commitid, "message": unmerged_commit.message} + ], + }, + ) + + assert response.status_code == status.HTTP_200_OK + + unmerged_commit.refresh_from_db() + assert unmerged_commit.branch != branch_name + + @patch("redis.Redis.sismember", lambda x, y, z: True) + @patch("services.task.TaskService.status_set_pending") + def test_push_triggers_set_pending_task_on_most_recent_commit( + self, set_pending_mock + ): + commit1 = CommitFactory(merged=False, repository=self.repo) + commit2 = CommitFactory(merged=False, repository=self.repo) + unmerged_branch_name = "unmerged" + + self._post_event_data( + event=GitHubWebhookEvents.PUSH, + data={ + "ref": "refs/heads/" + unmerged_branch_name, + "repository": {"id": self.repo.service_id}, + "commits": [ + {"id": commit1.commitid, "message": commit1.message}, + {"id": commit2.commitid, "message": commit2.message}, + ], + }, + ) + + set_pending_mock.assert_called_once_with( + repoid=self.repo.repoid, + commitid=commit2.commitid, + branch=unmerged_branch_name, + on_a_pull_request=False, + ) + + @patch("redis.Redis.sismember", lambda x, y, z: False) + @patch("services.task.TaskService.status_set_pending") + def test_push_doesnt_trigger_task_if_repo_not_part_of_beta_set( + self, set_pending_mock + ): + commit1 = CommitFactory(merged=False, repository=self.repo) + + self._post_event_data( + event=GitHubWebhookEvents.PUSH, + data={ + "ref": "refs/heads/" + "derp", + "repository": {"id": self.repo.service_id}, + "commits": [{"id": commit1.commitid, "message": commit1.message}], + }, + ) + + set_pending_mock.assert_not_called() + + @patch("redis.Redis.sismember", lambda x, y, z: True) + @patch("services.task.TaskService.status_set_pending") + def test_push_doesnt_trigger_task_if_ci_skipped(self, set_pending_mock): + commit1 = CommitFactory(merged=False, repository=self.repo, message="[ci skip]") + + response = self._post_event_data( + event=GitHubWebhookEvents.PUSH, + data={ + "ref": "refs/heads/" + "derp", + "repository": {"id": self.repo.service_id}, + "commits": [{"id": commit1.commitid, "message": commit1.message}], + }, + ) + + assert response.data == "CI Skipped" + set_pending_mock.assert_not_called() + + def test_status_exits_early_if_repo_not_active(self): + self.repo.active = False + self.repo.save() + + response = self._post_event_data( + event=GitHubWebhookEvents.STATUS, + data={"repository": {"id": self.repo.service_id}}, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_NOT_ACTIVE + + def test_status_exits_early_for_codecov_statuses(self): + response = self._post_event_data( + event=GitHubWebhookEvents.STATUS, + data={"context": "codecov/", "repository": {"id": self.repo.service_id}}, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_CODECOV_STATUS + + def test_status_exits_early_for_pending_statuses(self): + response = self._post_event_data( + event=GitHubWebhookEvents.STATUS, + data={"state": "pending", "repository": {"id": self.repo.service_id}}, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_PENDING_STATUSES + + def test_status_exits_early_if_commit_not_complete(self): + response = self._post_event_data( + event=GitHubWebhookEvents.STATUS, + data={ + "repository": {"id": self.repo.service_id}, + "sha": CommitFactory(repository=self.repo, state="pending").commitid, + }, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_PROCESSING + + @patch("services.task.TaskService.notify") + def test_status_triggers_notify_task(self, notify_mock): + commit = CommitFactory(repository=self.repo) + response = self._post_event_data( + event=GitHubWebhookEvents.STATUS, + data={"repository": {"id": self.repo.service_id}, "sha": commit.commitid}, + ) + + assert response.status_code == status.HTTP_200_OK + notify_mock.assert_called_once_with( + repoid=self.repo.repoid, commitid=commit.commitid + ) + + def test_pull_request_exits_early_if_repo_not_active(self): + self.repo.active = False + self.repo.save() + + response = self._post_event_data( + event=GitHubWebhookEvents.PULL_REQUEST, + data={"repository": {"id": self.repo.service_id}}, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_NOT_ACTIVE + + @patch("services.task.TaskService.pulls_sync") + def test_pull_request_triggers_pulls_sync_task_for_valid_actions( + self, pulls_sync_mock + ): + pull = PullFactory(repository=self.repo) + + valid_actions = ["opened", "closed", "reopened", "synchronize"] + + for action in valid_actions: + self._post_event_data( + event=GitHubWebhookEvents.PULL_REQUEST, + data={ + "repository": {"id": self.repo.service_id}, + "action": action, + "number": pull.pullid, + }, + ) + + pulls_sync_mock.assert_has_calls( + [call(repoid=self.repo.repoid, pullid=pull.pullid)] * len(valid_actions) + ) + + def test_pull_request_updates_title_if_edited(self): + pull = PullFactory(repository=self.repo) + new_title = "brand new dang title" + response = self._post_event_data( + event=GitHubWebhookEvents.PULL_REQUEST, + data={ + "repository": {"id": self.repo.service_id}, + "action": "edited", + "number": pull.pullid, + "pull_request": {"title": new_title}, + }, + ) + + assert response.status_code == status.HTTP_200_OK + + pull.refresh_from_db() + assert pull.title == new_title + + @freeze_time("2024-03-28T00:00:00") + @patch( + "services.task.TaskService.refresh", + lambda self, + ownerid, + username, + sync_teams, + sync_repos, + using_integration, + repos_affected: None, + ) + def test_installation_creates_new_owner_if_dne(self): + username, service_id = "newuser", 123456 + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION, + data={ + "installation": { + "id": 4, + "repository_selection": "selected", + "account": {"id": service_id, "login": username}, + "app_id": 15, + }, + "repositories": [ + {"id": "12321", "node_id": "R_kgDOG2tZYQ"}, + {"id": "12343", "node_id": "R_kgDOG2tABC"}, + ], + "sender": {"type": "User"}, + }, + ) + + owner_set = Owner.objects.filter( + service=Service.GITHUB_ENTERPRISE.value, + service_id=service_id, + username=username, + ) + + assert owner_set.exists() + + owner = owner_set.first() + assert owner.createstamp.isoformat() == "2024-03-28T00:00:00+00:00" + + ghapp_installations_set = GithubAppInstallation.objects.filter( + owner_id=owner.ownerid + ) + assert ghapp_installations_set.count() == 1 + installation = ghapp_installations_set.first() + assert installation.installation_id == 4 + assert installation.repository_service_ids == ["12321", "12343"] + + @patch( + "services.task.TaskService.refresh", + lambda self, + ownerid, + username, + sync_teams, + sync_repos, + using_integration, + repos_affected: None, + ) + def test_installation_creates_new_owner_if_dne_all_repos(self): + username, service_id = "newuser", 123456 + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION, + data={ + "installation": { + "id": 4, + "repository_selection": "all", + "account": {"id": service_id, "login": username}, + "app_id": 15, + }, + "repositories": [ + {"id": "12321", "node_id": "R_kgDOG2tZYQ"}, + {"id": "12343", "node_id": "R_kgDOG2tABC"}, + ], + "sender": {"type": "User"}, + }, + ) + + owner_set = Owner.objects.filter( + service=Service.GITHUB_ENTERPRISE.value, + service_id=service_id, + username=username, + ) + + assert owner_set.exists() + + owner = owner_set.first() + + ghapp_installations_set = GithubAppInstallation.objects.filter( + owner_id=owner.ownerid + ) + assert ghapp_installations_set.count() == 1 + installation = ghapp_installations_set.first() + assert installation.installation_id == 4 + assert installation.repository_service_ids is None + + @freeze_time("2024-03-28T00:00:00") + @patch( + "services.task.TaskService.refresh", + lambda self, + ownerid, + username, + sync_teams, + sync_repos, + using_integration, + repos_affected: None, + ) + def test_installation_repositories_creates_new_owner_if_dne(self): + username, service_id = "newuser", 123456 + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION_REPOSITORIES, + data={ + "installation": { + "id": 4, + "repository_selection": "all", + "account": {"id": service_id, "login": username}, + "app_id": 15, + }, + "repository_selection": "all", + "sender": {"type": "User"}, + }, + ) + + owner_set = Owner.objects.filter( + service=Service.GITHUB_ENTERPRISE.value, + service_id=service_id, + username=username, + ) + + assert owner_set.exists() + + owner = owner_set.first() + assert owner.createstamp.isoformat() == "2024-03-28T00:00:00+00:00" + + ghapp_installations_set = GithubAppInstallation.objects.filter( + owner_id=owner.ownerid + ) + assert ghapp_installations_set.count() == 1 + installation = ghapp_installations_set.first() + assert installation.installation_id == 4 + assert installation.repository_service_ids is None + + def test_installation_with_deleted_action_nulls_values(self): + # Should set integration_id to null for owner, + # and set using_integration=False and bot=null for repos + owner = OwnerFactory(service=Service.GITHUB_ENTERPRISE.value) + repo1 = RepositoryFactory(author=owner) + repo2 = RepositoryFactory(author=owner) + + owner.integration_id = 12 + owner.save() + + repo1.using_integration, repo2.using_integration = True, True + repo1.bot, repo2.bot = owner, owner + + repo1.save() + repo2.save() + + ghapp_installation = GithubAppInstallation( + installation_id=25, + repository_service_ids=[repo1.service_id, repo2.service_id], + owner=owner, + ) + ghapp_installation.save() + + assert owner.github_app_installations.exists() + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION, + data={ + "installation": { + "id": 25, + "repository_selection": "selected", + "account": {"id": owner.service_id, "login": owner.username}, + "app_id": 15, + }, + "repositories": [ + {"id": "12321", "node_id": "R_kgDOG2tZYQ"}, + {"id": "12343", "node_id": "R_kgDOG2tABC"}, + ], + "action": "deleted", + "sender": {"type": "User"}, + }, + ) + + owner.refresh_from_db() + repo1.refresh_from_db() + repo2.refresh_from_db() + + assert owner.integration_id is None + assert repo1.using_integration == False + assert repo2.using_integration == False + + assert repo1.bot is None + assert repo2.bot is None + + assert not owner.github_app_installations.exists() + + @patch( + "services.task.TaskService.refresh", + lambda self, + ownerid, + username, + sync_teams, + sync_repos, + using_integration, + repos_affected: None, + ) + def test_installation_repositories_update_existing_ghapp(self): + # Should set integration_id to null for owner, + # and set using_integration=False and bot=null for repos + owner = OwnerFactory(service=Service.GITHUB_ENTERPRISE.value) + repo1 = RepositoryFactory(author=owner) + repo2 = RepositoryFactory(author=owner) + installation = GithubAppInstallation( + owner=owner, repository_service_ids=[repo1.service_id], installation_id=12 + ) + + owner.integration_id = 12 + owner.save() + + repo1.using_integration, repo2.using_integration = True, True + repo1.bot, repo2.bot = owner, owner + + repo1.save() + repo2.save() + + installation.save() + + assert owner.github_app_installations.exists() + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION_REPOSITORIES, + data={ + "installation": { + "id": 12, + "repository_selection": "selected", + "account": {"id": owner.service_id, "login": owner.username}, + "app_id": 15, + }, + "repositories_added": [{"id": repo2.service_id, "node_id": "R_repo2"}], + "repositories_removed": [ + {"id": repo1.service_id, "node_id": "R_repo1"} + ], + "repository_selection": "selected", + "action": "added", + "sender": {"type": "User"}, + }, + ) + + installation.refresh_from_db() + assert installation.installation_id == 12 + assert installation.repository_service_ids == [repo2.service_id] + + @patch( + "services.task.TaskService.refresh", + lambda self, + ownerid, + username, + sync_teams, + sync_repos, + using_integration, + repos_affected: None, + ) + def test_installation_repositories_update_existing_ghapp_all_repos(self): + # Should set integration_id to null for owner, + # and set using_integration=False and bot=null for repos + owner = OwnerFactory(service=Service.GITHUB_ENTERPRISE.value) + repo1 = RepositoryFactory(author=owner) + repo2 = RepositoryFactory(author=owner) + installation = GithubAppInstallation( + owner=owner, repository_service_ids=[repo1.service_id], installation_id=12 + ) + + owner.integration_id = 12 + owner.save() + + repo1.using_integration, repo2.using_integration = True, True + repo1.bot, repo2.bot = owner, owner + + repo1.save() + repo2.save() + + installation.save() + + assert owner.github_app_installations.exists() + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION_REPOSITORIES, + data={ + "installation": { + "id": 12, + "repository_selection": "all", + "account": {"id": owner.service_id, "login": owner.username}, + "app_id": 15, + }, + "repositories_added": [{"id": repo2.service_id, "node_id": "R_repo2"}], + "repositories_removed": [], + "repository_selection": "all", + "action": "deleted", + "sender": {"type": "User"}, + }, + ) + + installation.refresh_from_db() + assert installation.installation_id == 12 + assert installation.repository_service_ids is None + + @patch( + "services.task.TaskService.refresh", + lambda self, + ownerid, + username, + sync_teams, + sync_repos, + using_integration, + repos_affected: None, + ) + def test_installation_with_other_actions_sets_owner_itegration_id_if_none( + self, + ): + installation_id = 44 + owner = OwnerFactory(service=Service.GITHUB_ENTERPRISE.value) + + owner.integration_id = None + owner.save() + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION, + data={ + "installation": { + "id": installation_id, + "repository_selection": "selected", + "account": {"id": owner.service_id, "login": owner.username}, + "app_id": 15, + }, + "repositories": [ + {"id": "12321", "node_id": "R_12321CAT"}, + {"id": "12343", "node_id": "R_12343DOG"}, + ], + "action": "added", + "sender": {"type": "User"}, + }, + ) + + owner.refresh_from_db() + + assert owner.integration_id == installation_id + + ghapp_installations_set = GithubAppInstallation.objects.filter( + owner_id=owner.ownerid + ) + assert ghapp_installations_set.count() == 1 + installation = ghapp_installations_set.first() + assert installation.installation_id == installation_id + assert installation.repository_service_ids == ["12321", "12343"] + + @patch( + "services.task.TaskService.refresh", + lambda self, + ownerid, + username, + sync_teams, + sync_repos, + using_integration, + repos_affected: None, + ) + def test_installation_repositories_with_other_actions_sets_owner_itegration_id_if_none( + self, + ): + installation_id = 44 + owner = OwnerFactory(service=Service.GITHUB_ENTERPRISE.value) + + owner.integration_id = None + owner.save() + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION_REPOSITORIES, + data={ + "installation": { + "id": installation_id, + "repository_selection": "all", + "account": {"id": owner.service_id, "login": owner.username}, + "app_id": 15, + }, + "repository_selection": "all", + "action": "added", + "sender": {"type": "User"}, + }, + ) + + owner.refresh_from_db() + + assert owner.integration_id == installation_id + + ghapp_installations_set = GithubAppInstallation.objects.filter( + owner_id=owner.ownerid + ) + assert ghapp_installations_set.count() == 1 + installation = ghapp_installations_set.first() + assert installation.installation_id == installation_id + assert installation.repository_service_ids is None + + @patch("services.task.TaskService.refresh") + def test_installation_trigger_refresh_with_other_actions(self, refresh_mock): + owner = OwnerFactory(service=Service.GITHUB_ENTERPRISE.value) + + self._post_event_data( + event=GitHubWebhookEvents.INSTALLATION, + data={ + "installation": { + "id": 11, + "repository_selection": "selected", + "account": {"id": owner.service_id, "login": owner.username}, + "app_id": 15, + }, + "action": "added", + "sender": {"type": "User"}, + "repositories": [ + {"id": "12321", "node_id": "R_12321CAT"}, + {"id": "12343", "node_id": "R_12343DOG"}, + ], + }, + ) + + assert refresh_mock.call_count == 1 + _, kwargs = refresh_mock.call_args_list[0] + # Because we throw these into a set we need to order them here + # In practive it doesn't matter, but for the test it does. + kwargs["repos_affected"].sort() + assert kwargs == dict( + ownerid=owner.ownerid, + username=owner.username, + sync_teams=False, + sync_repos=True, + using_integration=True, + repos_affected=[("12321", "R_12321CAT"), ("12343", "R_12343DOG")], + ) + + @patch("services.task.TaskService.refresh") + def test_organization_with_removed_action_removes_user_from_org_and_activated_user_list( + self, + mock_refresh, + ): + org = OwnerFactory(service_id="4321", service=Service.GITHUB_ENTERPRISE.value) + user = OwnerFactory( + organizations=[org.ownerid], + service_id="12", + service=Service.GITHUB_ENTERPRISE.value, + ) + org.plan_activated_users = [user.ownerid] + org.save() + + self._post_event_data( + event=GitHubWebhookEvents.ORGANIZATION, + data={ + "action": "member_removed", + "membership": {"user": {"id": user.service_id}}, + "organization": {"id": org.service_id}, + }, + ) + + user.refresh_from_db() + org.refresh_from_db() + + mock_refresh.assert_called_with( + ownerid=user.ownerid, + username=user.username, + sync_teams=True, + sync_repos=True, + using_integration=False, + ) + assert user.ownerid not in org.plan_activated_users + + def test_organization_member_removed_with_nonexistent_org_doesnt_crash(self): + user = OwnerFactory(service_id="12", service=Service.GITHUB_ENTERPRISE.value) + + response = self._post_event_data( + event=GitHubWebhookEvents.ORGANIZATION, + data={ + "action": "member_removed", + "membership": {"user": {"id": user.service_id}}, + "organization": {"id": 65000}, + }, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_organization_member_removed_with_nonexistent_or_nonactivated_member(self): + mock_all_plans_and_tiers() + org = OwnerFactory( + service_id="4321", + plan_activated_users=[50392], + service=Service.GITHUB_ENTERPRISE.value, + ) + user = OwnerFactory( + service_id="12", + organizations=[60798], + service=Service.GITHUB_ENTERPRISE.value, + ) + + response = self._post_event_data( + event=GitHubWebhookEvents.ORGANIZATION, + data={ + "action": "member_removed", + "membership": {"user": {"id": user.service_id}}, + "organization": {"id": org.service_id}, + }, + ) + + assert response.status_code == status.HTTP_200_OK + + def test_organization_member_removed_with_nonexistent_member_doesnt_crash(self): + org = OwnerFactory(service_id="4321", service=Service.GITHUB_ENTERPRISE.value) + + response = self._post_event_data( + event=GitHubWebhookEvents.ORGANIZATION, + data={ + "action": "member_removed", + "membership": {"user": {"id": 101010}}, + "organization": {"id": org.service_id}, + }, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.task.TaskService.sync_plans") + def test_marketplace_purchase_triggers_sync_plans_task( + self, sync_plans_mock, subscription_retrieve_mock + ): + sender = {"id": 545, "login": "buddy@guy.com"} + action = "purchased" + account = {"type": "Organization", "id": 54678, "login": "username"} + subscription_retrieve_mock.return_value = None + self._post_event_data( + event=GitHubWebhookEvents.MARKETPLACE_PURCHASE, + data={ + "action": action, + "sender": sender, + "marketplace_purchase": {"account": account}, + }, + ) + + sync_plans_mock.assert_called_once_with( + sender=sender, account=account, action=action + ) + + @patch("logging.Logger.warning") + @patch("services.billing.stripe.Subscription.retrieve") + @patch("services.task.TaskService.sync_plans") + def test_marketplace_purchase_but_user_has_stripe_subscription( + self, sync_plans_mock, subscription_retrieve_mock, log_warning_mock + ): + sender = {"id": 545, "login": "buddy@guy.com"} + action = "purchased" + account = {"type": "Organization", "id": 54678, "login": "username"} + OwnerFactory( + username=account["login"], + service="github_enterprise", + stripe_subscription_id="abc", + ) + quantity = 14 + plan = PlanName.CODECOV_PRO_MONTHLY.value + subscription_retrieve_mock.return_value = MockedSubscription( + "active", plan, quantity + ) + self._post_event_data( + event=GitHubWebhookEvents.MARKETPLACE_PURCHASE, + data={ + "action": action, + "sender": sender, + "marketplace_purchase": { + "account": account, + "plan": {"name": "gh-marketplace"}, + "unit_count": 14, + }, + }, + ) + + log_warning_mock.assert_called_with( + "GHM webhook - user purchasing but has a Stripe Subscription", + extra={ + "username": "username", + "old_plan_name": plan, + "old_plan_seats": quantity, + "new_plan_name": "gh-marketplace", + "new_plan_seats": 14, + }, + ) + + sync_plans_mock.assert_called_once_with( + sender=sender, account=account, action=action + ) + + def test_signature_validation(self): + response = self.client.post( + reverse("github-webhook"), + **{ + GitHubHTTPHeaders.EVENT: "", + GitHubHTTPHeaders.DELIVERY_TOKEN: uuid.UUID(int=5), + GitHubHTTPHeaders.SIGNATURE: "", + }, + data={}, + format="json", + ) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + @patch("webhook_handlers.views.github.get_config") + def test_signature_validation_with_string_key(self, get_config_mock): + # make get_config return string + get_config_mock.return_value = "testixik8qdauiab1yiffydimvi72ekq" + response = self._post_event_data(event="", data={}) + assert response.status_code == status.HTTP_200_OK + + def test_member_removes_repo_permissions_if_member_removed(self): + member = OwnerFactory( + permission=[self.repo.repoid], + service_id=6098, + service=Service.GITHUB_ENTERPRISE.value, + ) + self._post_event_data( + event=GitHubWebhookEvents.MEMBER, + data={ + "action": "removed", + "member": {"id": member.service_id}, + "repository": {"id": self.repo.service_id}, + }, + ) + + member.refresh_from_db() + assert self.repo.repoid not in member.permission + + def test_member_doesnt_crash_if_member_permission_array_is_None(self): + member = OwnerFactory( + permission=None, service_id=6098, service=Service.GITHUB_ENTERPRISE.value + ) + self._post_event_data( + event=GitHubWebhookEvents.MEMBER, + data={ + "action": "removed", + "member": {"id": member.service_id}, + "repository": {"id": self.repo.service_id}, + }, + ) + + def test_member_doesnt_crash_if_member_didnt_have_permission(self): + member = OwnerFactory( + permission=[self.repo.service_id + 1], + service_id=6098, + service=Service.GITHUB_ENTERPRISE.value, + ) + self._post_event_data( + event=GitHubWebhookEvents.MEMBER, + data={ + "action": "removed", + "member": {"id": member.service_id}, + "repository": {"id": self.repo.service_id}, + }, + ) + + def test_member_doesnt_crash_if_member_dne(self): + response = self._post_event_data( + event=GitHubWebhookEvents.MEMBER, + data={ + "action": "removed", + "member": {"id": 604945829}, # some random number + "repository": {"id": self.repo.service_id}, + }, + ) + + assert response.status_code == 404 + + def test_returns_404_if_repo_not_found(self): + response = self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={"action": "publicized", "repository": {"id": -29384}}, + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_repo_not_found_when_owner_has_integration_creates_repo(self): + owner = OwnerFactory( + integration_id=4850403, + service_id=97968493, + service=Service.GITHUB_ENTERPRISE.value, + ) + self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={ + "action": "publicized", + "repository": { + "id": 506003, + "name": "testrepo", + "private": False, + "default_branch": "master", + "owner": {"id": owner.service_id}, + }, + }, + ) + + assert owner.repository_set.filter(name="testrepo").exists() + + def test_repo_creation_doesnt_crash_for_forked_repo(self): + owner = OwnerFactory( + integration_id=4850403, + service_id=97968493, + service=Service.GITHUB_ENTERPRISE.value, + ) + self._post_event_data( + event=GitHubWebhookEvents.REPOSITORY, + data={ + "action": "publicized", + "repository": { + "id": 506003, + "name": "testrepo", + "private": False, + "default_branch": "master", + "owner": {"id": owner.service_id}, + "fork": True, + "parent": { + "name": "mainrepo", + "language": "python", + "id": 7940284, + "private": False, + "default_branch": "master", + "owner": {"id": 8495712939, "login": "alogin"}, + }, + }, + }, + ) + + assert owner.repository_set.filter(name="testrepo").exists() diff --git a/apps/codecov-api/webhook_handlers/tests/test_gitlab.py b/apps/codecov-api/webhook_handlers/tests/test_gitlab.py new file mode 100644 index 0000000000..8ee14074f9 --- /dev/null +++ b/apps/codecov-api/webhook_handlers/tests/test_gitlab.py @@ -0,0 +1,313 @@ +import uuid +from unittest.mock import patch + +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) + +from core.models import Commit, PullStates +from webhook_handlers.constants import ( + GitLabHTTPHeaders, + GitLabWebhookEvents, + WebhookHandlerErrorMessages, +) + + +def get_config_mock(*args, **kwargs): + if args == ("setup", "enterprise_license"): + return False + elif args == ("gitlab", "webhook_validation"): + return False + else: + return kwargs.get("default") + + +class TestGitlabWebhookHandler(APITestCase): + def _post_event_data(self, event, data): + return self.client.post( + reverse("gitlab-webhook"), + data=data, + format="json", + **{ + GitLabHTTPHeaders.EVENT: event, + }, + ) + + def setUp(self): + self.get_config_patcher = patch("webhook_handlers.views.gitlab.get_config") + self.get_config_mock = self.get_config_patcher.start() + self.get_config_mock.side_effect = get_config_mock + + self.repo = RepositoryFactory( + author=OwnerFactory(service="gitlab"), service_id=123, active=True + ) + + def tearDown(self): + self.get_config_patcher.stop() + + def test_unknown_repo(self): + response = self._post_event_data( + event=GitLabWebhookEvents.PUSH, data={"project_id": 1404} + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_push_event_no_yaml_cached(self): + response = self._post_event_data( + event=GitLabWebhookEvents.PUSH, + data={"object_kind": "push", "project_id": self.repo.service_id}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "No yaml cached yet." + + def test_push_event_yaml_cached(self): + response = self._post_event_data( + event=GitLabWebhookEvents.PUSH, + data={"object_kind": "push", "project_id": self.repo.service_id}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "No yaml cached yet." + + def test_job_event_build_pending(self): + response = self._post_event_data( + event=GitLabWebhookEvents.JOB, + data={ + "object_kind": "build", + "project_id": self.repo.service_id, + "build_status": "pending", + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_PENDING_STATUSES + + def test_job_event_repo_not_active(self): + self.repo.active = False + self.repo.save() + + response = self._post_event_data( + event=GitLabWebhookEvents.JOB, + data={ + "object_kind": "build", + "project_id": self.repo.service_id, + "build_status": "success", + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_PROCESSING + + def test_job_event_commit_not_found(self): + response = self._post_event_data( + event=GitLabWebhookEvents.JOB, + data={ + "object_kind": "build", + "project_id": self.repo.service_id, + "build_status": "success", + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_PROCESSING + + def test_job_event_commit_not_complete(self): + commit_sha = "2293ada6b400935a1378653304eaf6221e0fdb8f" + CommitFactory( + author=self.repo.author, + repository=self.repo, + commitid=commit_sha, + state=Commit.CommitStates.PENDING, + ) + + response = self._post_event_data( + event=GitLabWebhookEvents.JOB, + data={ + "object_kind": "build", + "project_id": self.repo.service_id, + "build_status": "success", + "sha": commit_sha, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_PROCESSING + + @patch("services.task.TaskService.notify") + def test_job_event_triggers_notify(self, notify_mock): + commit_sha = "2293ada6b400935a1378653304eaf6221e0fdb8f" + commit = CommitFactory( + author=self.repo.author, + repository=self.repo, + commitid=commit_sha, + state=Commit.CommitStates.COMPLETE, + ) + + response = self._post_event_data( + event=GitLabWebhookEvents.JOB, + data={ + "object_kind": "build", + "project_id": self.repo.service_id, + "build_status": "success", + "sha": commit_sha, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Notify queued." + notify_mock.assert_called_once_with( + repoid=self.repo.repoid, commitid=commit.commitid + ) + + def test_merge_request_event_repo_not_found(self): + response = self._post_event_data( + event=GitLabWebhookEvents.MERGE_REQUEST, + data={ + "object_kind": "merge_request", + "object_attributes": {"target_project_id": 1404}, + }, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + @patch("services.task.TaskService.pulls_sync") + def test_merge_request_event_action_open(self, pulls_sync_mock): + pullid = 2 + response = self._post_event_data( + event=GitLabWebhookEvents.MERGE_REQUEST, + data={ + "object_kind": "merge_request", + "object_attributes": { + "action": "open", + "target_project_id": self.repo.service_id, + "iid": pullid, + }, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Opening pull request in Codecov" + + pulls_sync_mock.assert_called_once_with(repoid=self.repo.repoid, pullid=pullid) + + def test_merge_request_event_action_close(self): + pull = PullFactory( + author=self.repo.author, + repository=self.repo, + pullid=1, + state=PullStates.OPEN, + ) + + response = self._post_event_data( + event=GitLabWebhookEvents.MERGE_REQUEST, + data={ + "object_kind": "merge_request", + "object_attributes": { + "action": "close", + "target_project_id": self.repo.service_id, + "iid": pull.pullid, + }, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Pull request closed" + + pull.refresh_from_db() + assert pull.state == PullStates.CLOSED + + @patch("services.task.TaskService.pulls_sync") + def test_merge_request_event_action_merge(self, pulls_sync_mock): + pullid = 2 + response = self._post_event_data( + event=GitLabWebhookEvents.MERGE_REQUEST, + data={ + "object_kind": "merge_request", + "object_attributes": { + "action": "merge", + "target_project_id": self.repo.service_id, + "iid": pullid, + }, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Pull request merged" + + pulls_sync_mock.assert_called_once_with(repoid=self.repo.repoid, pullid=pullid) + + @patch("services.task.TaskService.pulls_sync") + def test_merge_request_event_action_update(self, pulls_sync_mock): + pullid = 2 + response = self._post_event_data( + event=GitLabWebhookEvents.MERGE_REQUEST, + data={ + "object_kind": "merge_request", + "object_attributes": { + "action": "update", + "target_project_id": self.repo.service_id, + "iid": pullid, + }, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Pull request synchronize queued" + + pulls_sync_mock.assert_called_once_with(repoid=self.repo.repoid, pullid=pullid) + + def test_handle_system_hook_when_not_enterprise(self): + owner = OwnerFactory(service="gitlab") + repo = RepositoryFactory(author=owner) + + system_hook_events = [ + "project_create", + "project_destroy", + "project_rename", + "project_transfer", + "user_add_to_team", + "user_remove_from_team", + ] + + event_data = { + "event": GitLabWebhookEvents.SYSTEM, + "data": { + "event_name": "project_create", + "project_id": repo.service_id, + }, + } + + for event in system_hook_events: + event_data["data"]["event_name"] = event + response = self._post_event_data(**event_data) + assert response.status_code == status.HTTP_403_FORBIDDEN + + def test_secret_validation(self): + owner = OwnerFactory(service="gitlab") + repo = RepositoryFactory( + author=owner, + service_id=uuid.uuid4(), + webhook_secret=uuid.uuid4(), # if repo has webhook secret, requires validation + ) + owner.permission = [repo.repoid] + owner.save() + + response = self.client.post( + reverse("gitlab-webhook"), + **{ + GitLabHTTPHeaders.EVENT: "", + GitLabHTTPHeaders.TOKEN: "", + }, + data={ + "project_id": repo.service_id, + }, + format="json", + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + + response = self.client.post( + reverse("gitlab-webhook"), + **{ + GitLabHTTPHeaders.EVENT: "", + GitLabHTTPHeaders.TOKEN: repo.webhook_secret, + }, + data={ + "project_id": repo.service_id, + }, + format="json", + ) + assert response.status_code == status.HTTP_200_OK diff --git a/apps/codecov-api/webhook_handlers/tests/test_gitlab_enterprise.py b/apps/codecov-api/webhook_handlers/tests/test_gitlab_enterprise.py new file mode 100644 index 0000000000..d166ced9fb --- /dev/null +++ b/apps/codecov-api/webhook_handlers/tests/test_gitlab_enterprise.py @@ -0,0 +1,892 @@ +import uuid +from unittest.mock import patch + +import pytest +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase +from shared.django_apps.core.tests.factories import ( + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) +from shared.utils.test_utils import mock_config_helper + +from core.models import Commit, PullStates, Repository +from webhook_handlers.constants import ( + GitLabHTTPHeaders, + GitLabWebhookEvents, + WebhookHandlerErrorMessages, +) + + +class TestGitlabEnterpriseWebhookHandler(APITestCase): + @pytest.fixture(scope="function", autouse=True) + def inject_mocker(request, mocker): + request.mocker = mocker + + @pytest.fixture(autouse=True) + def mock_config(self, mocker): + mock_config_helper( + mocker, + configs={ + "setup.enterprise_license": True, + "gitlab_enterprise.webhook_validation": False, + }, + ) + + def _post_event_data(self, event, data, token=None): + return self.client.post( + reverse("gitlab_enterprise-webhook"), + **{ + GitLabHTTPHeaders.EVENT: event, + GitLabHTTPHeaders.TOKEN: token, + }, + data=data, + format="json", + ) + + def setUp(self): + self.repo = RepositoryFactory( + author=OwnerFactory(service="gitlab_enterprise"), + service_id=123, + active=True, + ) + + def test_unknown_repo(self): + response = self._post_event_data( + event=GitLabWebhookEvents.PUSH, data={"project_id": 1404} + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_push_event_no_yaml_cached(self): + response = self._post_event_data( + event=GitLabWebhookEvents.PUSH, + data={"object_kind": "push", "project_id": self.repo.service_id}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "No yaml cached yet." + + def test_push_event_yaml_cached(self): + response = self._post_event_data( + event=GitLabWebhookEvents.PUSH, + data={"object_kind": "push", "project_id": self.repo.service_id}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "No yaml cached yet." + + def test_job_event_build_pending(self): + response = self._post_event_data( + event=GitLabWebhookEvents.JOB, + data={ + "object_kind": "build", + "project_id": self.repo.service_id, + "build_status": "pending", + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_PENDING_STATUSES + + def test_job_event_repo_not_active(self): + self.repo.active = False + self.repo.save() + + response = self._post_event_data( + event=GitLabWebhookEvents.JOB, + data={ + "object_kind": "build", + "project_id": self.repo.service_id, + "build_status": "success", + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_PROCESSING + + def test_job_event_commit_not_found(self): + response = self._post_event_data( + event=GitLabWebhookEvents.JOB, + data={ + "object_kind": "build", + "project_id": self.repo.service_id, + "build_status": "success", + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_PROCESSING + + def test_job_event_commit_not_complete(self): + commit_sha = "2293ada6b400935a1378653304eaf6221e0fdb8f" + CommitFactory( + author=self.repo.author, + repository=self.repo, + commitid=commit_sha, + state=Commit.CommitStates.PENDING, + ) + + response = self._post_event_data( + event=GitLabWebhookEvents.JOB, + data={ + "object_kind": "build", + "project_id": self.repo.service_id, + "build_status": "success", + "sha": commit_sha, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == WebhookHandlerErrorMessages.SKIP_PROCESSING + + @patch("services.task.TaskService.notify") + def test_job_event_triggers_notify(self, notify_mock): + commit_sha = "2293ada6b400935a1378653304eaf6221e0fdb8f" + commit = CommitFactory( + author=self.repo.author, + repository=self.repo, + commitid=commit_sha, + state=Commit.CommitStates.COMPLETE, + ) + + response = self._post_event_data( + event=GitLabWebhookEvents.JOB, + data={ + "object_kind": "build", + "project_id": self.repo.service_id, + "build_status": "success", + "sha": commit_sha, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Notify queued." + notify_mock.assert_called_once_with( + repoid=self.repo.repoid, commitid=commit.commitid + ) + + def test_merge_request_event_repo_not_found(self): + response = self._post_event_data( + event=GitLabWebhookEvents.MERGE_REQUEST, + data={ + "object_kind": "merge_request", + "object_attributes": {"target_project_id": 1404}, + }, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + @patch("services.task.TaskService.pulls_sync") + def test_merge_request_event_action_open(self, pulls_sync_mock): + pullid = 2 + response = self._post_event_data( + event=GitLabWebhookEvents.MERGE_REQUEST, + data={ + "object_kind": "merge_request", + "object_attributes": { + "action": "open", + "target_project_id": self.repo.service_id, + "iid": pullid, + }, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Opening pull request in Codecov" + + pulls_sync_mock.assert_called_once_with(repoid=self.repo.repoid, pullid=pullid) + + def test_merge_request_event_action_close(self): + pull = PullFactory( + author=self.repo.author, + repository=self.repo, + pullid=1, + state=PullStates.OPEN, + ) + + response = self._post_event_data( + event=GitLabWebhookEvents.MERGE_REQUEST, + data={ + "object_kind": "merge_request", + "object_attributes": { + "action": "close", + "target_project_id": self.repo.service_id, + "iid": pull.pullid, + }, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Pull request closed" + + pull.refresh_from_db() + assert pull.state == PullStates.CLOSED + + @patch("services.task.TaskService.pulls_sync") + def test_merge_request_event_action_merge(self, pulls_sync_mock): + pullid = 2 + response = self._post_event_data( + event=GitLabWebhookEvents.MERGE_REQUEST, + data={ + "object_kind": "merge_request", + "object_attributes": { + "action": "merge", + "target_project_id": self.repo.service_id, + "iid": pullid, + }, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Pull request merged" + + pulls_sync_mock.assert_called_once_with(repoid=self.repo.repoid, pullid=pullid) + + @patch("services.task.TaskService.pulls_sync") + def test_merge_request_event_action_update(self, pulls_sync_mock): + pullid = 2 + response = self._post_event_data( + event=GitLabWebhookEvents.MERGE_REQUEST, + data={ + "object_kind": "merge_request", + "object_attributes": { + "action": "update", + "target_project_id": self.repo.service_id, + "iid": pullid, + }, + }, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Pull request synchronize queued" + + pulls_sync_mock.assert_called_once_with(repoid=self.repo.repoid, pullid=pullid) + + def test_handle_system_hook_not_enterprise(self): + mock_config_helper(self.mocker, configs={"setup.enterprise_license": None}) + owner = OwnerFactory(service="gitlab_enterprise", username="jsmith") + + response = self._post_event_data( + event=GitLabWebhookEvents.SYSTEM, + data={ + "created_at": "2020-01-21T07:30:54Z", + "updated_at": "2020-01-21T07:38:22Z", + "event_name": "project_create", + "name": "StoreCloud", + "owner_email": "johnsmith@gmail.com", + "owner_name": "John Smith", + "path": "storecloud", + "path_with_namespace": f"{owner.username}/storecloud", + "project_id": 74, + "project_visibility": "private", + }, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + + new_repo = Repository.objects.filter( + author__ownerid=owner.ownerid, service_id=74 + ).first() + assert new_repo is None + + @patch("services.refresh.RefreshService.trigger_refresh") + def test_handle_system_hook_project_create(self, mock_refresh_task): + sample_payload_from_gitlab_docs = { + "created_at": "2012-07-21T07:30:54Z", + "updated_at": "2012-07-21T07:38:22Z", + "event_name": "project_create", + "name": "StoreCloud", + "owner_email": "johnsmith@example.com", + "owner_name": "John Smith", + "owners": [{"name": "John", "email": "user1@example.com"}], + "path": "storecloud", + "path_with_namespace": "jsmith/storecloud", + "project_id": 74, + "project_visibility": "private", + } + + owner = OwnerFactory( + service="gitlab_enterprise", + username="jsmith", + name=sample_payload_from_gitlab_docs["owner_name"], + email=sample_payload_from_gitlab_docs["owner_email"], + oauth_token="123", + ) + + response = self._post_event_data( + event=GitLabWebhookEvents.SYSTEM, + data=sample_payload_from_gitlab_docs, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Sync initiated" + + mock_refresh_task.assert_called_once_with( + ownerid=owner.ownerid, + username=owner.username, + using_integration=False, + manual_trigger=False, + ) + + @patch("services.refresh.RefreshService.trigger_refresh") + def test_handle_system_hook_project_destroy(self, mock_refresh_task): + sample_payload_from_gitlab_docs = { + "created_at": "2012-07-21T07:30:58Z", + "updated_at": "2012-07-21T07:38:22Z", + "event_name": "project_destroy", + "name": "Underscore", + "owner_email": "johnsmith@example.com", + "owner_name": "John Smith", + "owners": [{"name": "John", "email": "user1@example.com"}], + "path": "underscore", + "path_with_namespace": "jsmith/underscore", + "project_id": 73, + "project_visibility": "internal", + } + + OwnerFactory( + service="gitlab_enterprise", + username="jsmith", + name=sample_payload_from_gitlab_docs["owner_name"], + email=sample_payload_from_gitlab_docs["owner_email"], + oauth_token="123", + ) + + owner_org = OwnerFactory( + service="gitlab_enterprise", + oauth_token=None, + ) + + repo = RepositoryFactory( + author=owner_org, + service_id=sample_payload_from_gitlab_docs["project_id"], + active=True, + activated=True, + deleted=False, + ) + + response = self._post_event_data( + event=GitLabWebhookEvents.SYSTEM, + data=sample_payload_from_gitlab_docs, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Repository deleted" + + mock_refresh_task.assert_not_called() + + repo.refresh_from_db() + assert repo.active is False + assert repo.activated is False + assert repo.deleted is True + + @patch("services.refresh.RefreshService.trigger_refresh") + def test_handle_system_hook_project_rename(self, mock_refresh_task): + # testing get owner by namespace in payload + sample_payload_from_gitlab_docs = { + "created_at": "2012-07-21T07:30:58Z", + "updated_at": "2012-07-21T07:38:22Z", + "event_name": "project_rename", + "name": "Underscore", + "path": "underscore", + "path_with_namespace": "jsmith/underscore", + "project_id": 73, + "owner_name": "John Smith", + "owner_email": "johnsmith@example.com", + "owners": [{"name": "John", "email": "user1@example.com"}], + "project_visibility": "internal", + "old_path_with_namespace": "jsmith/overscore", + } + + OwnerFactory( + service="gitlab_enterprise", + oauth_token="123", + username="jsmith", + ) + + owner_bot = OwnerFactory( + service="gitlab_enterprise", + oauth_token="123", + ) + + owner_org = OwnerFactory( + service="gitlab_enterprise", + oauth_token=None, + ) + + RepositoryFactory( + author=owner_org, + service_id=sample_payload_from_gitlab_docs["project_id"], + active=True, + activated=True, + deleted=False, + bot=owner_bot, + ) + + response = self._post_event_data( + event=GitLabWebhookEvents.SYSTEM, + data=sample_payload_from_gitlab_docs, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Sync initiated" + + mock_refresh_task.assert_called_once_with( + ownerid=owner_bot.ownerid, + username=owner_bot.username, + using_integration=False, + manual_trigger=False, + ) + + @patch("services.refresh.RefreshService.trigger_refresh") + def test_handle_system_hook_project_transfer(self, mock_refresh_task): + # moving this repo from one namespace to another + sample_payload_from_gitlab_docs = { + "created_at": "2012-07-21T07:30:58Z", + "updated_at": "2012-07-21T07:38:22Z", + "event_name": "project_transfer", + "name": "Underscore", + "path": "underscore", + "path_with_namespace": "scores/underscore", + "project_id": 73, + "owner_name": "John Smith", + "owner_email": "johnsmith@example.com", + "owners": [{"name": "John", "email": "user1@example.com"}], + "project_visibility": "internal", + "old_path_with_namespace": "jsmith/overscore", + } + + owner_user = OwnerFactory( + service="gitlab_enterprise", + name=sample_payload_from_gitlab_docs["owner_name"], + email=sample_payload_from_gitlab_docs["owner_email"], + oauth_token="123", + ) + + non_usable_bot = OwnerFactory( + service="gitlab_enterprise", + oauth_token=None, + ) + + owner_org = OwnerFactory( + service="gitlab_enterprise", + oauth_token=None, + username="jsmith", + ) + + RepositoryFactory( + author=owner_org, + service_id=sample_payload_from_gitlab_docs["project_id"], + active=True, + activated=True, + deleted=False, + bot=non_usable_bot, + ) + + response = self._post_event_data( + event=GitLabWebhookEvents.SYSTEM, + data=sample_payload_from_gitlab_docs, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Sync initiated" + + mock_refresh_task.assert_called_once_with( + ownerid=owner_user.ownerid, + username=owner_user.username, + using_integration=False, + manual_trigger=False, + ) + + @patch("services.refresh.RefreshService.trigger_refresh") + def test_handle_system_hook_user_create(self, mock_refresh_task): + sample_payload_from_gitlab_docs = { + "created_at": "2012-07-21T07:44:07Z", + "updated_at": "2012-07-21T07:38:22Z", + "email": "js@gitlabhq.com", + "event_name": "user_create", + "name": "John Smith", + "username": "js", + "user_id": 41, + } + response = self._post_event_data( + event=GitLabWebhookEvents.SYSTEM, + data=sample_payload_from_gitlab_docs, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + mock_refresh_task.assert_not_called() + + @patch("services.refresh.RefreshService.trigger_refresh") + def test_handle_system_hook_user_add_to_team(self, mock_refresh_task): + sample_payload_from_gitlab_docs = { + "created_at": "2012-07-21T07:30:56Z", + "updated_at": "2012-07-21T07:38:22Z", + "event_name": "user_add_to_team", + "access_level": "Maintainer", + "project_id": 74, + "project_name": "StoreCloud", + "project_path": "storecloud", + "project_path_with_namespace": "jsmith/storecloud", + "user_email": "johnsmith@example.com", + "user_name": "John Smith", + "user_username": "johnsmith", + "user_id": 41, + "project_visibility": "private", + } + + owner_user = OwnerFactory( + service="gitlab_enterprise", + name=sample_payload_from_gitlab_docs["user_name"], + email=sample_payload_from_gitlab_docs["user_email"], + oauth_token="123", + username=sample_payload_from_gitlab_docs["user_username"], + service_id=sample_payload_from_gitlab_docs["user_id"], + ) + + owner_org = OwnerFactory( + service="gitlab_enterprise", + oauth_token=None, + username="jsmith", + ) + + RepositoryFactory( + author=owner_org, + service_id=sample_payload_from_gitlab_docs["project_id"], + active=True, + activated=True, + deleted=False, + ) + + response = self._post_event_data( + event=GitLabWebhookEvents.SYSTEM, + data=sample_payload_from_gitlab_docs, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Sync initiated" + + mock_refresh_task.assert_called_once_with( + ownerid=owner_user.ownerid, + username=owner_user.username, + using_integration=False, + manual_trigger=False, + ) + + @patch("services.refresh.RefreshService.trigger_refresh") + def test_handle_system_hook_user_add_to_team_repo_public(self, mock_refresh_task): + sample_payload_from_gitlab_docs = { + "created_at": "2012-07-21T07:30:56Z", + "updated_at": "2012-07-21T07:38:22Z", + "event_name": "user_add_to_team", + "access_level": "Maintainer", + "project_id": 74, + "project_name": "StoreCloud", + "project_path": "storecloud", + "project_path_with_namespace": "jsmith/storecloud", + "user_email": "johnsmith@example.com", + "user_name": "John Smith", + "user_username": "johnsmith", + "user_id": 41, + "project_visibility": "public", + } + + OwnerFactory( + service="gitlab_enterprise", + name=sample_payload_from_gitlab_docs["user_name"], + email=sample_payload_from_gitlab_docs["user_email"], + oauth_token="123", + username=sample_payload_from_gitlab_docs["user_username"], + service_id=sample_payload_from_gitlab_docs["user_id"], + ) + + owner_org = OwnerFactory( + service="gitlab_enterprise", + oauth_token=None, + username="jsmith", + ) + + RepositoryFactory( + author=owner_org, + service_id=sample_payload_from_gitlab_docs["project_id"], + active=True, + activated=True, + deleted=False, + ) + + response = self._post_event_data( + event=GitLabWebhookEvents.SYSTEM, + data=sample_payload_from_gitlab_docs, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data is None + + mock_refresh_task.assert_not_called() + + @patch("services.refresh.RefreshService.trigger_refresh") + def test_handle_system_hook_user_remove_from_team(self, mock_refresh_task): + sample_payload_from_gitlab_docs = { + "created_at": "2012-07-21T07:30:56Z", + "updated_at": "2012-07-21T07:38:22Z", + "event_name": "user_remove_from_team", + "access_level": "Maintainer", + "project_id": 74, + "project_name": "StoreCloud", + "project_path": "storecloud", + "project_path_with_namespace": "jsmith/storecloud", + "user_email": "johnsmith@example.com", + "user_name": "John Smith", + "user_username": "johnsmith", + "user_id": 41, + "project_visibility": "private", + } + + owner_user = OwnerFactory( + service="gitlab_enterprise", + name=sample_payload_from_gitlab_docs["user_name"], + email=sample_payload_from_gitlab_docs["user_email"], + oauth_token="123", + username=sample_payload_from_gitlab_docs["user_username"], + service_id=sample_payload_from_gitlab_docs["user_id"], + ) + + owner_org = OwnerFactory( + service="gitlab_enterprise", + oauth_token=None, + username="jsmith", + ) + + RepositoryFactory( + author=owner_org, + service_id=sample_payload_from_gitlab_docs["project_id"], + active=True, + activated=True, + deleted=False, + ) + + response = self._post_event_data( + event=GitLabWebhookEvents.SYSTEM, + data=sample_payload_from_gitlab_docs, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Sync initiated" + + mock_refresh_task.assert_called_once_with( + ownerid=owner_user.ownerid, + username=owner_user.username, + using_integration=False, + manual_trigger=False, + ) + + @patch("services.refresh.RefreshService.trigger_refresh") + def test_handle_system_hook_unknown_repo(self, mock_refresh_task): + sample_payload_from_gitlab_docs = { + "created_at": "2012-07-21T07:30:56Z", + "updated_at": "2012-07-21T07:38:22Z", + "event_name": "user_add_to_team", + "access_level": "Maintainer", + "project_id": 74, + "project_name": "StoreCloud", + "project_path": "storecloud", + "project_path_with_namespace": "jsmith/storecloud", + "user_email": "johnsmith@example.com", + "user_name": "John Smith", + "user_username": "johnsmith", + "user_id": 41, + "project_visibility": "private", + } + + OwnerFactory( + service="gitlab_enterprise", + name=sample_payload_from_gitlab_docs["user_name"], + email=sample_payload_from_gitlab_docs["user_email"], + oauth_token="123", + username=sample_payload_from_gitlab_docs["user_username"], + service_id=sample_payload_from_gitlab_docs["user_id"], + ) + + owner_org = OwnerFactory( + service="gitlab_enterprise", + oauth_token=None, + username="jsmith", + ) + + RepositoryFactory( + author=owner_org, + service_id=sample_payload_from_gitlab_docs["project_id"] + 1, + active=True, + activated=True, + deleted=False, + ) + + response = self._post_event_data( + event=GitLabWebhookEvents.SYSTEM, + data=sample_payload_from_gitlab_docs, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + @patch("services.refresh.RefreshService.trigger_refresh") + def test_handle_system_hook_user_add_to_team_unknown_user(self, mock_refresh_task): + sample_payload_from_gitlab_docs = { + "created_at": "2012-07-21T07:30:56Z", + "updated_at": "2012-07-21T07:38:22Z", + "event_name": "user_add_to_team", + "access_level": "Maintainer", + "project_id": 74, + "project_name": "StoreCloud", + "project_path": "storecloud", + "project_path_with_namespace": "jsmith/storecloud", + "user_email": "johnsmith@example.com", + "user_name": "John Smith", + "user_username": "johnsmith", + "user_id": 41, + "project_visibility": "private", + } + + owner_org = OwnerFactory( + service="gitlab_enterprise", + oauth_token=None, + username="jsmith", + ) + + RepositoryFactory( + author=owner_org, + service_id=sample_payload_from_gitlab_docs["project_id"], + active=True, + activated=True, + deleted=False, + ) + + response = self._post_event_data( + event=GitLabWebhookEvents.SYSTEM, + data=sample_payload_from_gitlab_docs, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Sync initiated" + + mock_refresh_task.assert_not_called() + + @patch("services.refresh.RefreshService.trigger_refresh") + def test_handle_system_hook_no_bot_or_user_match(self, mock_refresh_task): + sample_payload_from_gitlab_docs = { + "created_at": "2012-07-21T07:30:58Z", + "updated_at": "2012-07-21T07:38:22Z", + "event_name": "project_rename", + "name": "Underscore", + "path": "underscore", + "path_with_namespace": "jsmith/underscore", + "project_id": 73, + "owner_name": "John Smith", + "owner_email": "johnsmith@example.com", + "owners": [{"name": "John", "email": "user1@example.com"}], + "project_visibility": "internal", + "old_path_with_namespace": "jsmith/overscore", + } + + OwnerFactory( + service="gitlab_enterprise", + name=sample_payload_from_gitlab_docs["owner_name"], + oauth_token="123", + ) + + owner_org = OwnerFactory( + service="gitlab_enterprise", + oauth_token=None, + username="jsmith", + ) + + RepositoryFactory( + author=owner_org, + service_id=sample_payload_from_gitlab_docs["project_id"], + active=True, + activated=True, + deleted=False, + ) + + response = self._post_event_data( + event=GitLabWebhookEvents.SYSTEM, + data=sample_payload_from_gitlab_docs, + ) + assert response.status_code == status.HTTP_200_OK + assert response.data == "Sync initiated" + + mock_refresh_task.assert_not_called() + + def test_secret_validation(self): + owner = OwnerFactory(service="gitlab_enterprise") + repo = RepositoryFactory( + author=owner, + service_id=uuid.uuid4(), + webhook_secret=uuid.uuid4(), # if repo has webhook secret, requires validation + ) + owner.permission = [repo.repoid] + owner.save() + + response = self.client.post( + reverse("gitlab_enterprise-webhook"), + **{ + GitLabHTTPHeaders.EVENT: "", + GitLabHTTPHeaders.TOKEN: "", + }, + data={ + "project_id": repo.service_id, + }, + format="json", + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + + response = self.client.post( + reverse("gitlab_enterprise-webhook"), + **{ + GitLabHTTPHeaders.EVENT: "", + GitLabHTTPHeaders.TOKEN: repo.webhook_secret, + }, + data={ + "project_id": repo.service_id, + }, + format="json", + ) + assert response.status_code == status.HTTP_200_OK + + def test_secret_validation_required_by_config(self): + webhook_secret = uuid.uuid4() + # if repo has webhook_validation config set to True, requires validation + mock_config_helper( + self.mocker, + configs={ + "gitlab_enterprise.webhook_validation": True, + }, + ) + owner = OwnerFactory(service="gitlab_enterprise") + repo = RepositoryFactory( + author=owner, + service_id=uuid.uuid4(), + webhook_secret=None, + ) + owner.permission = [repo.repoid] + owner.save() + + response = self.client.post( + reverse("gitlab_enterprise-webhook"), + **{ + GitLabHTTPHeaders.EVENT: "", + GitLabHTTPHeaders.TOKEN: "", + }, + data={ + "project_id": repo.service_id, + }, + format="json", + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + + response = self.client.post( + reverse("gitlab_enterprise-webhook"), + **{ + GitLabHTTPHeaders.EVENT: "", + GitLabHTTPHeaders.TOKEN: webhook_secret, + }, + data={ + "project_id": repo.service_id, + }, + format="json", + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + + repo.webhook_secret = webhook_secret + repo.save() + response = self.client.post( + reverse("gitlab_enterprise-webhook"), + **{ + GitLabHTTPHeaders.EVENT: "", + GitLabHTTPHeaders.TOKEN: webhook_secret, + }, + data={ + "project_id": repo.service_id, + }, + format="json", + ) + assert response.status_code == status.HTTP_200_OK diff --git a/apps/codecov-api/webhook_handlers/urls.py b/apps/codecov-api/webhook_handlers/urls.py new file mode 100644 index 0000000000..ff73bb7a64 --- /dev/null +++ b/apps/codecov-api/webhook_handlers/urls.py @@ -0,0 +1,31 @@ +from django.urls import path + +# to remove when in production we send the webhooks to /billing/stripe/webhooks +from billing.views import StripeWebhookHandler + +from .views.bitbucket import BitbucketWebhookHandler +from .views.bitbucket_server import BitbucketServerWebhookHandler +from .views.github import GithubEnterpriseWebhookHandler, GithubWebhookHandler +from .views.gitlab import GitLabEnterpriseWebhookHandler, GitLabWebhookHandler + +urlpatterns = [ + path("github", GithubWebhookHandler.as_view(), name="github-webhook"), + path( + "github_enterprise", + GithubEnterpriseWebhookHandler.as_view(), + name="github_enterprise-webhook", + ), + path("bitbucket", BitbucketWebhookHandler.as_view(), name="bitbucket-webhook"), + path("gitlab", GitLabWebhookHandler.as_view(), name="gitlab-webhook"), + path( + "gitlab_enterprise", + GitLabEnterpriseWebhookHandler.as_view(), + name="gitlab_enterprise-webhook", + ), + path( + "bitbucket_server", + BitbucketServerWebhookHandler.as_view(), + name="bitbucket-server-webhook", + ), + path("stripe", StripeWebhookHandler.as_view(), name="old-stripe-webhook"), +] diff --git a/apps/codecov-api/webhook_handlers/views/__init__.py b/apps/codecov-api/webhook_handlers/views/__init__.py new file mode 100644 index 0000000000..93c20fabd6 --- /dev/null +++ b/apps/codecov-api/webhook_handlers/views/__init__.py @@ -0,0 +1,22 @@ +from shared.metrics import Counter + +WEBHOOKS_RECEIVED = Counter( + "api_webhooks_received", + "Incoming webhooks, broken down by service, event type, and action", + [ + "service", + "event", + "action", + ], +) + +WEBHOOKS_ERRORED = Counter( + "api_webhooks_errored", + "Webhooks that cannot be processed, broken down by service and error reason", + [ + "service", + "event", + "action", + "error_reason", + ], +) diff --git a/apps/codecov-api/webhook_handlers/views/bitbucket.py b/apps/codecov-api/webhook_handlers/views/bitbucket.py new file mode 100644 index 0000000000..5ea4655173 --- /dev/null +++ b/apps/codecov-api/webhook_handlers/views/bitbucket.py @@ -0,0 +1,138 @@ +import logging + +from django.shortcuts import get_object_or_404 +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework.views import APIView +from shared.helpers.yaml import walk + +from core.models import Branch, Commit, Pull, PullStates, Repository +from services.task import TaskService +from webhook_handlers.constants import ( + BitbucketHTTPHeaders, + BitbucketWebhookEvents, + WebhookHandlerErrorMessages, +) + +from . import WEBHOOKS_ERRORED, WEBHOOKS_RECEIVED + +log = logging.getLogger(__name__) + + +class BitbucketWebhookHandler(APIView): + permission_classes = [AllowAny] + service_name = "bitbucket" + + def _inc_recv(self): + event, _, action = self.event.partition(":") + WEBHOOKS_RECEIVED.labels( + service=self.service_name, event=event, action=action + ).inc() + + def _inc_err(self, reason: str): + event, _, action = self.event.partition(":") + WEBHOOKS_ERRORED.labels( + service=self.service_name, + event=event, + action=action, + error_reason=reason, + ).inc() + + def post(self, request, *args, **kwargs): + self.event = self.request.META.get(BitbucketHTTPHeaders.EVENT) + event_hook_id = self.request.META.get(BitbucketHTTPHeaders.UUID) + + try: + repo = get_object_or_404( + Repository, + author__service="bitbucket", + service_id=self.request.data["repository"]["uuid"][1:-1], + hookid=event_hook_id, + ) + except Exception as e: + self._inc_err("repo_not_found") + raise e + + if not repo.active: + self._inc_err("repo_not_active") + return Response(data=WebhookHandlerErrorMessages.SKIP_NOT_ACTIVE) + + log.info( + "Bitbucket webhook message received", + extra=dict(event=self.event, hookid=event_hook_id, repoid=repo.repoid), + ) + + if self.event == BitbucketWebhookEvents.PULL_REQUEST_CREATED: + self._inc_recv() + return self._handle_pull_request_created_event(repo) + elif self.event in ( + BitbucketWebhookEvents.PULL_REQUEST_FULFILLED, + BitbucketWebhookEvents.PULL_REQUEST_REJECTED, + ): + self._inc_recv() + return self._handle_pull_request_state_change(repo) + elif self.event == BitbucketWebhookEvents.REPO_PUSH: + self._inc_recv() + return self._handle_repo_push_event(repo) + elif self.event in ( + BitbucketWebhookEvents.REPO_COMMIT_STATUS_CREATED, + BitbucketWebhookEvents.REPO_COMMIT_STATUS_UPDATED, + ): + self._inc_recv() + return self._handle_repo_commit_status_change(repo) + + self._inc_err("unhandled_event") + return Response() + + def _handle_pull_request_created_event(self, repo): + TaskService().pulls_sync( + repoid=repo.repoid, pullid=self.request.data["pullrequest"]["id"] + ) + return Response(data="Opening pull request in Codecov") + + def _handle_pull_request_state_change(self, repo): + state = { + BitbucketWebhookEvents.PULL_REQUEST_FULFILLED: PullStates.MERGED, + BitbucketWebhookEvents.PULL_REQUEST_REJECTED: PullStates.CLOSED, + }.get(self.event) + + Pull.objects.filter( + repository__repoid=repo.repoid, + pullid=self.request.data["pullrequest"]["id"], + ).update(state=state) + + return Response() + + def _handle_repo_push_event(self, repo): + for change in self.request.data["push"]["changes"]: + if walk(change, ("old", "type")) == "branch" and change["new"] is None: + # when a branch is deleted, new is null + branch_name = change["old"]["name"] + Branch.objects.filter(repository=repo, name=branch_name).delete() + + for change in self.request.data["push"]["changes"]: + if change["new"]: + return Response(data="Synchronize codecov.yml skipped") + + return Response() + + def _handle_repo_commit_status_change(self, repo): + if self.request.data["commit_status"]["key"].startswith("codecov"): + # a codecov/* context + return Response(data=WebhookHandlerErrorMessages.SKIP_CODECOV_STATUS) + + if self.request.data["commit_status"]["state"] == "INPROGRESS": + # skip pending + return Response(data=WebhookHandlerErrorMessages.SKIP_PENDING_STATUSES) + + commitid = self.request.data["commit_status"]["links"]["commit"]["href"].split( + "/" + )[-1] + + if not Commit.objects.filter( + repository=repo, commitid=commitid, state=Commit.CommitStates.COMPLETE + ).exists(): + return Response(data=WebhookHandlerErrorMessages.SKIP_PROCESSING) + + TaskService().notify(repoid=repo.repoid, commitid=commitid) + return Response(data="Notify queued") diff --git a/apps/codecov-api/webhook_handlers/views/bitbucket_server.py b/apps/codecov-api/webhook_handlers/views/bitbucket_server.py new file mode 100644 index 0000000000..99b5423683 --- /dev/null +++ b/apps/codecov-api/webhook_handlers/views/bitbucket_server.py @@ -0,0 +1,114 @@ +import logging + +from django.shortcuts import get_object_or_404 +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework.views import APIView + +from core.models import Branch, Pull, PullStates, Repository +from services.task import TaskService +from webhook_handlers.constants import ( + BitbucketServerHTTPHeaders, + BitbucketServerWebhookEvents, + WebhookHandlerErrorMessages, +) + +from . import WEBHOOKS_ERRORED, WEBHOOKS_RECEIVED + +log = logging.getLogger(__name__) + + +class BitbucketServerWebhookHandler(APIView): + # https://confluence.atlassian.com/bitbucketserver/event-payload-938025882.html + permission_classes = [AllowAny] + service_name = "bitbucket_server" + + def _inc_recv(self): + event, _, action = self.event.partition(":") + WEBHOOKS_RECEIVED.labels( + service=self.service_name, event=event, action=action + ).inc() + + def _inc_err(self, reason: str): + event, _, action = self.event.partition(":") + WEBHOOKS_ERRORED.labels( + service=self.service_name, + event=event, + action=action, + error_reason=reason, + ).inc() + + def _get_repo(self, event, body): + if event.startswith("repo:"): + repo_id = body["repository"]["id"] + elif event.startswith("pr:"): + repo_id = body["pullRequest"]["toRef"]["repository"]["id"] + + try: + return get_object_or_404( + Repository, author__service="bitbucket_server", service_id=repo_id + ) + except Exception as e: + self._inc_err("repo_not_found") + raise e + + def post(self, request, *args, **kwargs): + self.event = self.request.META.get(BitbucketServerHTTPHeaders.EVENT) + event_hook_id = self.request.META.get(BitbucketServerHTTPHeaders.UUID) + + repo = self._get_repo(self.event, self.request.data) + if not repo.active: + self._inc_err("repo_not_active") + return Response(data=WebhookHandlerErrorMessages.SKIP_NOT_ACTIVE) + + log.info( + "BitbucketServer webhook message received", + extra=dict(event=self.event, hookid=event_hook_id, repoid=repo.repoid), + ) + + if self.event == BitbucketServerWebhookEvents.PULL_REQUEST_CREATED: + self._inc_recv() + return self._handle_pull_request_created_event(repo) + elif self.event in ( + BitbucketServerWebhookEvents.PULL_REQUEST_MERGED, + BitbucketServerWebhookEvents.PULL_REQUEST_REJECTED, + ): + self._inc_recv() + return self._handle_pull_request_state_change(repo) + elif self.event == BitbucketServerWebhookEvents.REPO_REFS_CHANGED: + self._inc_recv() + return self._handle_repo_refs_change(repo) + + self._inc_err("unhandled_event") + return Response() + + def _handle_pull_request_created_event(self, repo): + TaskService().pulls_sync( + repoid=repo.repoid, pullid=self.request.data["pullRequest"]["id"] + ) + return Response(data="Opening pull request in Codecov") + + def _handle_pull_request_state_change(self, repo): + state = { + BitbucketServerWebhookEvents.PULL_REQUEST_MERGED: PullStates.MERGED, + BitbucketServerWebhookEvents.PULL_REQUEST_DELETED: PullStates.CLOSED, + BitbucketServerWebhookEvents.PULL_REQUEST_REJECTED: PullStates.CLOSED, + }.get(self.event) + + Pull.objects.filter( + repository__repoid=repo.repoid, + pullid=self.request.data["pullRequest"]["id"], + ).update(state=state) + + return Response() + + def _handle_repo_refs_change(self, repo): + ref_type = self.request.data["push"]["changes"]["old"]["type"] + if ref_type == "branch": + Branch.objects.filter( + repository=repo, + name=self.request.data["push"]["changes"]["old"]["name"], + ).delete() + if self.request.data["push"]["changes"]["new"]: + return Response(data="Synchronize codecov.yml skipped") + return Response() diff --git a/apps/codecov-api/webhook_handlers/views/github.py b/apps/codecov-api/webhook_handlers/views/github.py new file mode 100644 index 0000000000..b3d3a56112 --- /dev/null +++ b/apps/codecov-api/webhook_handlers/views/github.py @@ -0,0 +1,766 @@ +import hmac +import logging +import re +from contextlib import suppress +from hashlib import sha1, sha256 +from typing import Optional + +from django.db.models import Q +from django.utils import timezone +from django.utils.crypto import constant_time_compare +from rest_framework import status +from rest_framework.exceptions import NotFound, PermissionDenied +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework.views import APIView +from shared.events.amplitude import AmplitudeEventPublisher +from shared.helpers.redis import get_redis_connection + +from codecov_auth.models import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, + Owner, +) +from core.models import Branch, Commit, Pull, Repository +from services.billing import BillingService +from services.task import TaskService +from utils.config import get_config +from webhook_handlers.constants import ( + GitHubHTTPHeaders, + GitHubWebhookEvents, + WebhookHandlerErrorMessages, +) + +from . import WEBHOOKS_ERRORED, WEBHOOKS_RECEIVED + +log = logging.getLogger(__name__) + + +# This should probably go somewhere where it can be easily shared +regexp_ci_skip = re.compile(r"\[(ci|skip| |-){3,}\]").search + + +class GithubWebhookHandler(APIView): + """ + GitHub Webhook Handler. Method names correspond to events as defined in + + webhook_handlers.constants.GitHubWebhookEvents + """ + + permission_classes = [AllowAny] + redis = get_redis_connection() + + service_name = "github" + + def _inc_recv(self): + action = self.request.data.get("action", "") + WEBHOOKS_RECEIVED.labels( + service=self.service_name, event=self.event, action=action + ).inc() + + def _inc_err(self, reason: str): + action = self.request.data.get("action", "") + WEBHOOKS_ERRORED.labels( + service=self.service_name, + event=self.event, + action=action, + error_reason=reason, + ).inc() + + def validate_signature(self, request): + key = get_config( + self.service_name, + "webhook_secret", + default=b"testixik8qdauiab1yiffydimvi72ekq", + ) + if isinstance(key, str): + # If "key" comes from k8s secret, it is of type str, so + # must convert to bytearray for use with hmac + key = bytes(key, "utf-8") + + expected_sig = None + computed_sig = None + if GitHubHTTPHeaders.SIGNATURE_256 in request.META: + expected_sig = request.META.get(GitHubHTTPHeaders.SIGNATURE_256) + computed_sig = ( + "sha256=" + hmac.new(key, request.body, digestmod=sha256).hexdigest() + ) + elif GitHubHTTPHeaders.SIGNATURE in request.META: + expected_sig = request.META.get(GitHubHTTPHeaders.SIGNATURE) + computed_sig = ( + "sha1=" + hmac.new(key, request.body, digestmod=sha1).hexdigest() + ) + + if ( + computed_sig is None + or expected_sig is None + or len(computed_sig) != len(expected_sig) + or not constant_time_compare(computed_sig, expected_sig) + ): + self._inc_err("validation_failed") + raise PermissionDenied() + + def unhandled_webhook_event(self, request, *args, **kwargs): + return Response(data=WebhookHandlerErrorMessages.UNSUPPORTED_EVENT) + + def _get_repo(self, request): + """ + Attempts to fetch the repo first via the index on o(wnerid, service_id), + then naively on service, service_id if that fails. + """ + repo_data = self.request.data.get("repository", {}) + repo_service_id = repo_data.get("id") + owner_service_id = repo_data.get("owner", {}).get("id") + repo_slug = repo_data.get("full_name") + + try: + owner = Owner.objects.get( + service=self.service_name, service_id=owner_service_id + ) + except Owner.DoesNotExist: + log.info( + f"Error fetching owner with service_id {owner_service_id}, " + f"using repository service id to get repo", + extra=dict(repo_service_id=repo_service_id, repo_slug=repo_slug), + ) + try: + log.info( + "Unable to find repository owner, fetching repo with service, service_id", + extra=dict(repo_service_id=repo_service_id, repo_slug=repo_slug), + ) + return Repository.objects.get( + author__service=self.service_name, service_id=repo_service_id + ) + except Repository.DoesNotExist: + log.info( + "Received event for non-existent repository", + extra=dict(repo_service_id=repo_service_id, repo_slug=repo_slug), + ) + self._inc_err("repo_not_found") + raise NotFound("Repository does not exist") + else: + try: + log.debug( + "Found repository owner, fetching repo with ownerid, service_id", + extra=dict(repo_service_id=repo_service_id, repo_slug=repo_slug), + ) + return Repository.objects.get( + author__ownerid=owner.ownerid, service_id=repo_service_id + ) + except Repository.DoesNotExist: + default_ghapp_installation = owner.github_app_installations.filter( + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME + ).first() + if default_ghapp_installation or owner.integration_id: + log.info( + "Repository no found but owner is using integration, creating repository" + ) + return Repository.objects.get_or_create_from_git_repo( + repo_data, owner + )[0] + log.info( + "Received event for non-existent repository", + extra=dict(repo_service_id=repo_service_id, repo_slug=repo_slug), + ) + self._inc_err("repo_not_found") + raise NotFound("Repository does not exist") + + def ping(self, request, *args, **kwargs): + return Response(data="pong") + + def repository(self, request, *args, **kwargs): + action, repo = self.request.data.get("action"), self._get_repo(request) + if action == "publicized": + repo.private, repo.activated = False, False + repo.save() + log.info( + "Repository publicized", + extra=dict(repoid=repo.repoid, github_webhook_event=self.event), + ) + elif action == "privatized": + repo.private = True + repo.save() + log.info( + "Repository privatized", + extra=dict(repoid=repo.repoid, github_webhook_event=self.event), + ) + elif action == "deleted": + log.info(f"Request to delete repository: {repo.repoid}") + repo.deleted = True + repo.activated = False + repo.active = False + repo.name = f"{repo.name}-deleted" + repo.save(update_fields=["deleted", "activated", "active", "name"]) + log.info( + "Repository soft-deleted", + extra=dict(repoid=repo.repoid, github_webhook_event=self.event), + ) + else: + log.warning( + f"Unknown repository action: {action}", extra=dict(repoid=repo.repoid) + ) + return Response() + + def delete(self, request, *args, **kwargs): + ref_type = request.data.get("ref_type", "") + repo = self._get_repo(request) + if ref_type != "branch": + log.info( + f"Unsupported ref type: {ref_type}, exiting", + extra=dict(repoid=repo.repoid, github_webhook_event=self.event), + ) + return Response("Unsupported ref type") + branch_name = self.request.data.get("ref")[11:] + Branch.objects.filter( + repository=self._get_repo(request), name=branch_name + ).delete() + log.info( + f"Branch '{branch_name}' deleted", + extra=dict(repoid=repo.repoid, github_webhook_event=self.event), + ) + return Response() + + def public(self, request, *args, **kwargs): + repo = self._get_repo(request) + repo.private, repo.activated = False, False + repo.save() + log.info( + "Repository publicized", + extra=dict(repoid=repo.repoid, github_webhook_event=self.event), + ) + return Response() + + def push(self, request, *args, **kwargs): + ref_type = "branch" if request.data.get("ref", "")[5:10] == "heads" else "tag" + repo = self._get_repo(request) + if ref_type != "branch": + log.debug( + "Ref is tag, not branch, ignoring push event", + extra=dict(repoid=repo.repoid, github_webhook_event=self.event), + ) + return Response("Unsupported ref type") + + if not repo.active: + log.debug( + "Repository is not active, ignoring push event", + extra=dict(repoid=repo.repoid, github_webhook_event=self.event), + ) + return Response(data=WebhookHandlerErrorMessages.SKIP_NOT_ACTIVE) + + push_webhook_ignore_repos = get_config( + "setup", "push_webhook_ignore_repo_names", default=[] + ) + if repo.name in push_webhook_ignore_repos: + log.debug( + "Codecov is configured to ignore this repository name", + extra=dict( + repoid=repo.repoid, + github_webhook_event=self.event, + repo_name=repo.name, + ), + ) + return Response(data=WebhookHandlerErrorMessages.SKIP_WEBHOOK_IGNORED) + + pushed_to_branch_name = self.request.data.get("ref")[11:] + commits = self.request.data.get("commits", []) + + if not commits: + log.debug( + f"No commits in webhook payload for branch {pushed_to_branch_name}", + extra=dict(repoid=repo.repoid, github_webhook_event=self.event), + ) + return Response() + + if pushed_to_branch_name == repo.branch: + commits_queryset = Commit.objects.filter( + ~Q(branch=pushed_to_branch_name), + repository=repo, + commitid__in=[commit.get("id") for commit in commits], + merged=False, + ) + commits_queryset.update(branch=pushed_to_branch_name, merged=True) + log.info( + f"Branch name updated for commits to {pushed_to_branch_name}; setting merged to True", + extra=dict( + repoid=repo.repoid, + github_webhook_event=self.event, + commits=[commit.get("id") for commit in commits], + ), + ) + + most_recent_commit = commits[-1] + + if regexp_ci_skip(most_recent_commit.get("message")): + log.info( + "CI skip tag on head commit, not setting status", + extra=dict( + repoid=repo.repoid, + commit=most_recent_commit.get("id"), + github_webhook_event=self.event, + ), + ) + return Response(data="CI Skipped") + + if self.redis.sismember("beta.pending", repo.repoid): + log.info( + "Triggering status set pending task", + extra=dict( + repoid=repo.repoid, + commit=most_recent_commit.get("id"), + github_webhook_event=self.event, + ), + ) + TaskService().status_set_pending( + repoid=repo.repoid, + commitid=most_recent_commit.get("id"), + branch=pushed_to_branch_name, + on_a_pull_request=False, + ) + + return Response() + + def status(self, request, *args, **kwargs): + repo = self._get_repo(request) + commitid = request.data.get("sha") + + if not repo.active: + log.debug( + "Repository is not active, ignoring status event", + extra=dict( + repoid=repo.repoid, commit=commitid, github_webhook_event=self.event + ), + ) + return Response(data=WebhookHandlerErrorMessages.SKIP_NOT_ACTIVE) + if request.data.get("context", "")[:8] == "codecov/": + log.debug( + "Recieved a web hook for a Codecov status from GitHub. We ignore these, skipping.", + extra=dict( + repoid=repo.repoid, commit=commitid, github_webhook_event=self.event + ), + ) + return Response(data=WebhookHandlerErrorMessages.SKIP_CODECOV_STATUS) + if request.data.get("state") == "pending": + log.debug( + "Recieved a web hook for a `pending` status from GitHub. We ignore these, skipping.", + extra=dict( + repoid=repo.repoid, commit=commitid, github_webhook_event=self.event + ), + ) + return Response(data=WebhookHandlerErrorMessages.SKIP_PENDING_STATUSES) + + if not Commit.objects.filter( + repository=repo, commitid=commitid, state="complete" + ).exists(): + return Response(data=WebhookHandlerErrorMessages.SKIP_PROCESSING) + + log.info( + "Triggering notify task", + extra=dict( + repoid=repo.repoid, commit=commitid, github_webhook_event=self.event + ), + ) + + TaskService().notify(repoid=repo.repoid, commitid=commitid) + + return Response() + + def pull_request(self, request, *args, **kwargs): + repo = self._get_repo(request) + + if not repo.active: + log.info( + "Repository is not active, ignoring pull request event", + extra=dict(repoid=repo.repoid, github_webhook_event=self.event), + ) + return Response(data=WebhookHandlerErrorMessages.SKIP_NOT_ACTIVE) + + action, pullid = request.data.get("action"), request.data.get("number") + + if action in ["opened", "closed", "reopened", "synchronize", "labeled"]: + log.info( + f"Pull request action is '{action}', triggering pulls_sync task", + extra=dict( + repoid=repo.repoid, github_webhook_event=self.event, pullid=pullid + ), + ) + TaskService().pulls_sync(repoid=repo.repoid, pullid=pullid) + elif action == "edited": + log.info( + f"Pull request action is 'edited', updating pull title to " + f"'{request.data.get('pull_request', {}).get('title')}'", + extra=dict( + repoid=repo.repoid, github_webhook_event=self.event, pullid=pullid + ), + ) + Pull.objects.filter(repository=repo, pullid=pullid).update( + title=request.data.get("pull_request", {}).get("title") + ) + + return Response() + + def _decide_app_name(self, ghapp: GithubAppInstallation) -> str: + """Possibly updated the name of a GithubAppInstallation that has been fetched from DB or created. + Only the real default installation maybe use the name `GITHUB_APP_INSTALLATION_DEFAULT_NAME` + (otherwise we break the app) + We check that apps: + * already were given a custom name (do nothing); + * app_id match the configured default app app_id (use default name); + * none of the above (use 'unconfigured_app'); + + Returns the app name that should be used + """ + if ghapp.is_configured(): + return ghapp.name + log.warning( + "Github installation is unconfigured. Changing name to 'unconfigured_app'", + extra=dict(installation=ghapp.external_id, previous_name=ghapp.name), + ) + return "unconfigured_app" + + def _handle_installation_repository_events(self, request, *args, **kwargs): + # https://docs.github.com/en/webhooks/webhook-events-and-payloads#installation_repositories + service_id = request.data["installation"]["account"]["id"] + username = request.data["installation"]["account"]["login"] + owner, _ = Owner.objects.get_or_create( + service=self.service_name, + service_id=service_id, + defaults={ + "username": username, + "createstamp": timezone.now(), + }, + ) + + installation_id = request.data["installation"]["id"] + + ghapp_installation, _ = GithubAppInstallation.objects.get_or_create( + installation_id=installation_id, owner=owner + ) + app_id = request.data["installation"]["app_id"] + # Either update or set + # But this value shouldn't change for the installation, so doesn't matter + ghapp_installation.app_id = app_id + ghapp_installation.name = self._decide_app_name(ghapp_installation) + + all_repos_affected = request.data.get("repository_selection") == "all" + if all_repos_affected: + ghapp_installation.repository_service_ids = None + else: + repo_list_to_save = set(ghapp_installation.repository_service_ids or []) + repositories_added_service_ids = { + obj["id"] for obj in request.data.get("repositories_added", []) + } + repositories_removed_service_ids = { + obj["id"] for obj in request.data.get("repositories_removed", []) + } + repo_list_to_save = repo_list_to_save.union( + repositories_added_service_ids + ).difference(repositories_removed_service_ids) + ghapp_installation.repository_service_ids = list(repo_list_to_save) + ghapp_installation.save() + + def _handle_installation_events( + self, request, *args, event=GitHubWebhookEvents.INSTALLATION, **kwargs + ): + service_id = request.data["installation"]["account"]["id"] + username = request.data["installation"]["account"]["login"] + action = request.data.get("action") + + owner, _ = Owner.objects.get_or_create( + service=self.service_name, + service_id=service_id, + defaults={ + "username": username, + "createstamp": timezone.now(), + }, + ) + + installation_id = request.data["installation"]["id"] + + # https://docs.github.com/en/webhooks/webhook-events-and-payloads#installation + if action == "deleted": + if event == GitHubWebhookEvents.INSTALLATION: + ghapp_installation: Optional[GithubAppInstallation] = ( + owner.github_app_installations.filter( + installation_id=installation_id + ).first() + ) + if ghapp_installation is not None: + ghapp_installation.delete() + # Deprecated flow - BEGIN + owner.integration_id = None + owner.save() + owner.repository_set.all().update(using_integration=False, bot=None) + # Deprecated flow - END + log.info( + "Owner deleted app integration", + extra=dict(ownerid=owner.ownerid, github_webhook_event=self.event), + ) + else: + # GithubWebhookEvents.INSTALLTION_REPOSITORIES also execute this code + # because of deprecated flow. But the GithubAppInstallation shouldn't be changed + if event == GitHubWebhookEvents.INSTALLATION: + ghapp_installation, was_created = ( + GithubAppInstallation.objects.get_or_create( + installation_id=installation_id, owner=owner + ) + ) + if was_created: + installer_username = request.data.get("sender", {}).get( + "login", None + ) + installer = ( + Owner.objects.filter( + service=self.service_name, + username=installer_username, + ).first() + if installer_username + else None + ) + # If installer does not exist, just attribute the action to the org owner. + AmplitudeEventPublisher().publish( + "App Installed", + { + "user_ownerid": installer.ownerid + if installer is not None + else owner.ownerid, + "ownerid": owner.ownerid, + }, + ) + + app_id = request.data["installation"]["app_id"] + # Either update or set + # But this value shouldn't change for the installation, so doesn't matter + ghapp_installation.app_id = app_id + ghapp_installation.name = self._decide_app_name(ghapp_installation) + + affects_all_repositories = ( + request.data["installation"]["repository_selection"] == "all" + ) + if affects_all_repositories: + ghapp_installation.repository_service_ids = None + else: + repositories_service_ids = [ + obj["id"] for obj in request.data.get("repositories", []) + ] + ghapp_installation.repository_service_ids = repositories_service_ids + + if action in ["suspend", "unsuspend"]: + log.info( + "Request to suspend/unsuspend App", + extra=dict( + action=action, + is_currently_suspended=ghapp_installation.is_suspended, + ownerid=owner.ownerid, + installation_id=request.data["installation"]["id"], + ), + ) + ghapp_installation.is_suspended = action == "suspend" + + ghapp_installation.save() + + # This flow is deprecated and should be removed once the + # work on github app integration model is complete and backfilled + # Deprecated flow - BEGIN + if owner.integration_id is None: + owner.integration_id = request.data["installation"]["id"] + owner.save() + # Deprecated flow - END + + log.info( + "Triggering refresh task to sync repos", + extra=dict(ownerid=owner.ownerid, github_webhook_event=self.event), + ) + + repos_affected = ( + request.data.get("repositories", []) + + request.data.get("repositories_added", []) + + request.data.get("repositories_removed", []) + ) + repos_affected_clean = { + (obj["id"], obj["node_id"]) for obj in repos_affected + } + + TaskService().refresh( + ownerid=owner.ownerid, + username=username, + sync_teams=False, + sync_repos=True, + using_integration=True, + repos_affected=list(repos_affected_clean), + ) + + return Response(data="Integration webhook received") + + def installation(self, request, *args, **kwargs): + return self._handle_installation_events( + request, *args, **kwargs, event=GitHubWebhookEvents.INSTALLATION + ) + + def installation_repositories(self, request, *args, **kwargs): + self._handle_installation_repository_events(request, *args, **kwargs) + # This is kept to preserve the logic for deprecated usage of owner.installation_id + # It can be removed once the move to codecov_auth.GithubAppInstallation is complete + return self._handle_installation_events( + request, + *args, + **kwargs, + event=GitHubWebhookEvents.INSTALLATION_REPOSITORIES, + ) + + def organization(self, request, *args, **kwargs): + action = request.data.get("action") + if action == "member_removed": + log.info( + f"Removing user with service-id {request.data['membership']['user']['id']} " + f"from organization with service-id {request.data['organization']['id']}", + extra=dict(github_webhook_event=self.event), + ) + + try: + org = Owner.objects.get( + service=self.service_name, + service_id=request.data["organization"]["id"], + ) + except Owner.DoesNotExist: + log.info("Organization does not exist, exiting") + return Response( + status=status.HTTP_400_BAD_REQUEST, + data="Attempted to remove member from non-Codecov org failed", + ) + + try: + member = Owner.objects.get( + service=self.service_name, + service_id=request.data["membership"]["user"]["id"], + ) + except Owner.DoesNotExist: + log.info( + f"Member with service-id {request.data['membership']['user']['id']} " + f"does not exist, exiting", + extra=dict(ownerid=org.ownerid, github_webhook_event=self.event), + ) + return Response( + status=status.HTTP_400_BAD_REQUEST, + data="Attempted to remove non Codecov user from Codecov org failed", + ) + + # Force a sync for the removed member to remove their access to the + # org and its private repositories. + TaskService().refresh( + ownerid=member.ownerid, + username=member.username, + sync_teams=True, + sync_repos=True, + using_integration=False, + ) + + try: + if org.plan_activated_users: + org.plan_activated_users.remove(member.ownerid) + org.save(update_fields=["plan_activated_users"]) + except ValueError: + pass + + log.info( + f"User removal of {member.ownerid}, success", + extra=dict(ownerid=org.ownerid, github_webhook_event=self.event), + ) + + return Response() + + def _handle_marketplace_events(self, request, *args, **kwargs): + log.info( + "Triggering sync_plans task", extra=dict(github_webhook_event=self.event) + ) + with suppress(Exception): + # log if users purchase GHM plans while having a stripe plan + username = request.data["marketplace_purchase"]["account"]["login"] + new_plan_seats = request.data["marketplace_purchase"]["unit_count"] + new_plan_name = request.data["marketplace_purchase"]["plan"]["name"] + owner = Owner.objects.get(service=self.service_name, username=username) + subscription = BillingService(requesting_user=owner).get_subscription(owner) + if subscription.status == "active": + log.warning( + "GHM webhook - user purchasing but has a Stripe Subscription", + extra=dict( + username=username, + old_plan_name=subscription.plan.get("name", None), + old_plan_seats=subscription.quantity, + new_plan_name=new_plan_name, + new_plan_seats=new_plan_seats, + ), + ) + TaskService().sync_plans( + sender=request.data["sender"], + account=request.data["marketplace_purchase"]["account"], + action=request.data["action"], + ) + return Response() + + def marketplace_purchase(self, request, *args, **kwargs): + return self._handle_marketplace_events(request, *args, **kwargs) + + def member(self, request, *args, **kwargs): + action = request.data["action"] + if action == "removed": + repo = self._get_repo(request) + log.info( + "Request to remove read permissions for user", + extra=dict(repoid=repo.repoid, github_webhook_event=self.event), + ) + try: + member = Owner.objects.get( + service=self.service_name, service_id=request.data["member"]["id"] + ) + except Owner.DoesNotExist: + log.info( + "Repository permissions unchanged -- owner doesn't exist", + extra=dict(repoid=repo.repoid, github_webhook_event=self.event), + ) + return Response(status=status.HTTP_404_NOT_FOUND) + + try: + member.permission.remove(repo.repoid) + member.save(update_fields=["permission"]) + log.info( + "Successfully updated read permissions for repository", + extra=dict( + repoid=repo.repoid, + ownerid=member.ownerid, + github_webhook_event=self.event, + ), + ) + except (ValueError, AttributeError): + log.info( + "Member didn't have read permissions, didn't update", + extra=dict( + repoid=repo.repoid, + ownerid=member.ownerid, + github_webhook_event=self.event, + ), + ) + + return Response() + + def post(self, request, *args, **kwargs): + self.event = self.request.META.get(GitHubHTTPHeaders.EVENT) + log.info( + "GitHub Webhook Handler invoked", + extra=dict( + github_webhook_event=self.event, + delivery=self.request.META.get(GitHubHTTPHeaders.DELIVERY_TOKEN), + ), + ) + + self.validate_signature(request) + + if handler := getattr(self, self.event, None): + self._inc_recv() + return handler(request, *args, **kwargs) + else: + self._inc_err("unhandled_event") + return self.unhandled_webhook_event(request, *args, **kwargs) + + +class GithubEnterpriseWebhookHandler(GithubWebhookHandler): + service_name = "github_enterprise" diff --git a/apps/codecov-api/webhook_handlers/views/gitlab.py b/apps/codecov-api/webhook_handlers/views/gitlab.py new file mode 100644 index 0000000000..f9a79c6dc6 --- /dev/null +++ b/apps/codecov-api/webhook_handlers/views/gitlab.py @@ -0,0 +1,296 @@ +import logging + +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.utils.crypto import constant_time_compare +from rest_framework.exceptions import PermissionDenied +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework.views import APIView + +from codecov_auth.models import Owner +from core.models import Commit, Pull, PullStates, Repository +from services.refresh import RefreshService +from services.task import TaskService +from utils.config import get_config +from webhook_handlers.constants import ( + GitLabHTTPHeaders, + GitLabWebhookEvents, + WebhookHandlerErrorMessages, +) + +from . import WEBHOOKS_ERRORED, WEBHOOKS_RECEIVED + +log = logging.getLogger(__name__) + + +class GitLabWebhookHandler(APIView): + permission_classes = [AllowAny] + service_name = "gitlab" + + def _inc_recv(self): + event_name = self.request.data.get("event_name") + if not event_name: + event_name = self.request.data.get("object_kind") + action = self.request.data.get("object_attributes", {}).get("action", "") + + WEBHOOKS_RECEIVED.labels( + service=self.service_name, event=event_name, action=action + ).inc() + + def _inc_err(self, reason: str): + event_name = self.request.data.get("event_name") + if not event_name: + event_name = self.request.data.get("object_kind") + action = self.request.data.get("object_attributes", {}).get("action", "") + + WEBHOOKS_ERRORED.labels( + service=self.service_name, + event=event_name, + action=action, + error_reason=reason, + ).inc() + + def post(self, request, *args, **kwargs): + """ + Helpful docs for working with GitLab webhooks + https://docs.gitlab.com/ee/user/project/integrations/webhooks.html#webhook-receiver-requirements + for those special system hooks: https://docs.gitlab.com/ee/administration/system_hooks.html#hooks-request-example + all the other hooks: https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html + """ + event = self.request.META.get(GitLabHTTPHeaders.EVENT) + + log.info("GitLab webhook message received", extra=dict(event=event)) + + project_id = request.data.get("project_id") or request.data.get( + "object_attributes", {} + ).get("target_project_id") + + event_name = self.request.data.get( + "event_name", self.request.data.get("object_kind") + ) + + is_enterprise = True if get_config("setup", "enterprise_license") else False + + # special case - only event that doesn't have a repo yet + if event_name == "project_create": + if event == GitLabWebhookEvents.SYSTEM and is_enterprise: + self._inc_recv() + return self._handle_system_project_create_hook_event() + else: + self._inc_err("permission_denied") + raise PermissionDenied() + + try: + # all other events should correspond to a repo in the db + repo = get_object_or_404( + Repository, author__service=self.service_name, service_id=project_id + ) + except Exception as e: + self._inc_err("repo_not_found") + raise e + + webhook_validation = bool( + get_config( + self.service_name, "webhook_validation", default=False + ) # TODO: backfill migration then switch to True + ) + if webhook_validation or repo.webhook_secret: + self._validate_secret(request, repo.webhook_secret) + + if event == GitLabWebhookEvents.PUSH: + self._inc_recv() + return self._handle_push_event(repo) + elif event == GitLabWebhookEvents.JOB: + self._inc_recv() + return self._handle_job_event(repo) + elif event == GitLabWebhookEvents.MERGE_REQUEST: + self._inc_recv() + return self._handle_merge_request_event(repo) + elif event == GitLabWebhookEvents.SYSTEM: + # SYSTEM events have always been gated behind is_enterprise, requires an enterprise_license + if not is_enterprise: + self._inc_err("permission_denied") + raise PermissionDenied() + self._inc_recv() + return self._handle_system_hook_event(repo, event_name) + + self._inc_err("unhandled_event") + return Response() + + def _handle_push_event(self, repo): + """ + Triggered when pushing to the repository except when pushing tags. + + https://docs.gitlab.com/ce/user/project/integrations/webhooks.html#push-events + """ + message = "No yaml cached yet." + return Response(data=message) + + def _handle_job_event(self, repo): + """ + Triggered on status change of a job. + + This is equivalent to legacy "Build Hook" handling (https://gitlab.com/gitlab-org/gitlab-foss/issues/28226) + + https://docs.gitlab.com/ee/user/project/integrations/webhooks.html#job-events + """ + if self.request.data.get("build_status") == "pending": + return Response(data=WebhookHandlerErrorMessages.SKIP_PENDING_STATUSES) + + if repo.active is not True: + return Response(data=WebhookHandlerErrorMessages.SKIP_PROCESSING) + + commitid = self.request.data.get("sha") + + try: + commit = repo.commits.get( + commitid=commitid, state=Commit.CommitStates.COMPLETE + ) + except Commit.DoesNotExist: + return Response(data=WebhookHandlerErrorMessages.SKIP_PROCESSING) + + TaskService().notify(repoid=commit.repository.repoid, commitid=commitid) + return Response(data="Notify queued.") + + def _handle_merge_request_event(self, repo): + """ + Triggered when a new merge request is created, an existing merge request was updated/merged/closed or + a commit is added in the source branch. + + https://docs.gitlab.com/ce/user/project/integrations/webhooks.html#merge-request-events + """ + repoid = repo.repoid + + pull = self.request.data.get("object_attributes", {}) + action = pull.get("action") + message = None + if action == "open": + TaskService().pulls_sync(repoid=repoid, pullid=pull["iid"]) + message = "Opening pull request in Codecov" + + elif action == "close": + Pull.objects.filter(repository__repoid=repoid, pullid=pull["iid"]).update( + state=PullStates.CLOSED + ) + message = "Pull request closed" + + elif action == "merge": + TaskService().pulls_sync(repoid=repoid, pullid=pull["iid"]) + message = "Pull request merged" + + elif action == "update": + TaskService().pulls_sync(repoid=repoid, pullid=pull["iid"]) + message = "Pull request synchronize queued" + + else: + log.warning( + "Unhandled Gitlab webhook merge_request action", + extra=dict(action=action), + ) + + return Response(data=message) + + def _initiate_sync_for_owner(self, owner): + """ + default: will sync_teams and sync_repos for owner + sync_teams to update owner.organizations list (expired memberships are removed and new memberships are added), + and username, name, email, and avatar of each Org in owner.organizations. + sync_repos to update owner.permission list (private repo access), + and name, language, private, repoid, and deleted=False for each repo the owner has access to. + """ + RefreshService().trigger_refresh( + ownerid=owner.ownerid, + username=owner.username, + using_integration=False, + manual_trigger=False, + ) + + def _try_initiate_sync_for_owner(self): + owner_email = self.request.data.get("owner_email") + + # email is a strong identifier (GL users must have a unique email) + try: + owner = Owner.objects.get( + service=self.service_name, + oauth_token__isnull=False, + email=owner_email, + ) + except (Owner.DoesNotExist, Owner.MultipleObjectsReturned): + # could be the username of the OwnerUser or OwnerOrg. Sync only works with an OwnerUser. + owner_username_best_guess = self.request.data.get( + "path_with_namespace", "" + ).split("/")[0] + try: + owner = Owner.objects.get( + service=self.service_name, + oauth_token__isnull=False, + username=owner_username_best_guess, + ) + except (Owner.DoesNotExist, Owner.MultipleObjectsReturned): + return + + self._initiate_sync_for_owner(owner) + + def _handle_system_project_create_hook_event(self): + self._try_initiate_sync_for_owner() + return Response(data="Sync initiated") + + def _try_initiate_sync_for_repo(self, repo): + # most GL repos have bots - try to sync with bot as Owner + if repo.bot: + bot_owner = Owner.objects.filter( + service=self.service_name, + ownerid=repo.bot.ownerid, + oauth_token__isnull=False, + ).first() + if bot_owner: + return self._initiate_sync_for_owner(owner=bot_owner) + self._try_initiate_sync_for_owner() + + def _handle_system_hook_event(self, repo, event_name): + """ + GitLab Enterprise instance can send system hooks for changes on user, group, project, etc + """ + message = None + + if event_name == "project_destroy": + repo.deleted = True + repo.activated = False + repo.active = False + repo.name = f"{repo.name}-deleted" + repo.save(update_fields=["deleted", "activated", "active", "name"]) + message = "Repository deleted" + + elif event_name in ("project_rename", "project_transfer"): + self._try_initiate_sync_for_repo(repo=repo) + message = "Sync initiated" + + elif ( + event_name in ("user_add_to_team", "user_remove_from_team") + and self.request.data.get("project_visibility") == "private" + ): + # the payload from these hooks includes the ownerid + ownerid = self.request.data.get("user_id") + user = Owner.objects.filter( + service=self.service_name, + service_id=ownerid, + oauth_token__isnull=False, + ).first() + message = "Sync initiated" + if user: + self._initiate_sync_for_owner(owner=user) + + return Response(data=message) + + def _validate_secret(self, request: HttpRequest, webhook_secret: str): + token = request.META.get(GitLabHTTPHeaders.TOKEN) + if token and webhook_secret: + if constant_time_compare(webhook_secret, token): + return + self._inc_err("validation_failed") + raise PermissionDenied() + + +class GitLabEnterpriseWebhookHandler(GitLabWebhookHandler): + service_name = "gitlab_enterprise" diff --git a/apps/worker b/apps/worker deleted file mode 160000 index ad52223de9..0000000000 --- a/apps/worker +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ad52223de9313e23dc140a3bf6248dab4646beb7 diff --git a/apps/worker/.dockerignore b/apps/worker/.dockerignore new file mode 100644 index 0000000000..968559ca43 --- /dev/null +++ b/apps/worker/.dockerignore @@ -0,0 +1,2 @@ +service.json +gha-creds-*.json diff --git a/apps/worker/.envrc b/apps/worker/.envrc new file mode 100644 index 0000000000..5647e8722a --- /dev/null +++ b/apps/worker/.envrc @@ -0,0 +1,2 @@ +uv sync +source .venv/bin/activate \ No newline at end of file diff --git a/apps/worker/.github/ISSUE_TEMPLATE/bug_report.md b/apps/worker/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..86c0fb8633 --- /dev/null +++ b/apps/worker/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,16 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + + + +# 🐛 Bugs + +Do you want to file a bug report? [Please use our feedback repo instead](https://github.com/codecov/feedback/issues). diff --git a/apps/worker/.github/ISSUE_TEMPLATE/feature_request.md b/apps/worker/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..03313301fe --- /dev/null +++ b/apps/worker/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,16 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + + + +# 📣 Feedback / 🐛 Bugs + +Do you want to file a bug report and/or feature request for Codecov? [Please use our feedback repo instead](https://github.com/codecov/feedback/issues). diff --git a/apps/worker/.github/pull_request_template.md b/apps/worker/.github/pull_request_template.md new file mode 100644 index 0000000000..9fcafcedc1 --- /dev/null +++ b/apps/worker/.github/pull_request_template.md @@ -0,0 +1,13 @@ + + + + + + +### Legal Boilerplate + +Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. In 2022 this entity acquired Codecov and as result Sentry is going to need some rights from me in order to utilize my contributions in this PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms. \ No newline at end of file diff --git a/apps/worker/.github/workflows/cache_cleanup.yml b/apps/worker/.github/workflows/cache_cleanup.yml new file mode 100644 index 0000000000..a8da8d01a7 --- /dev/null +++ b/apps/worker/.github/workflows/cache_cleanup.yml @@ -0,0 +1,33 @@ +name: cleanup caches by a branch +on: + pull_request: + types: + - closed + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Cleanup + run: | + gh extension install actions/gh-actions-cache + + REPO=${{ github.repository }} + BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge" + + echo "Fetching list of cache key" + cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 ) + + ## Setting this to not fail the workflow while deleting cache keys. + set +e + echo "Deleting caches..." + for cacheKey in $cacheKeysForPR + do + gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm + done + echo "Done" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/apps/worker/.github/workflows/ci.yml b/apps/worker/.github/workflows/ci.yml new file mode 100644 index 0000000000..95fc3b0db6 --- /dev/null +++ b/apps/worker/.github/workflows/ci.yml @@ -0,0 +1,86 @@ +name: Worker CI + +on: + push: + tags: + - prod-* + branches: + - main + - staging + pull_request: + merge_group: + +permissions: + contents: "read" + id-token: "write" + issues: "write" + pull-requests: "write" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + lint: + name: Run Lint + + uses: codecov/gha-workflows/.github/workflows/lint.yml@v1.2.33 + + build: + name: Build Worker + uses: codecov/gha-workflows/.github/workflows/build-app.yml@v1.2.33 + + secrets: inherit + with: + repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-worker' }} + cache_file: "uv.lock" + + test: + name: Test + needs: [build] + uses: codecov/gha-workflows/.github/workflows/run-tests.yml@v1.2.33 + + secrets: inherit + with: + repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-worker' }} + + build-self-hosted: + name: Build Self Hosted Worker + needs: [build, test] + + uses: codecov/gha-workflows/.github/workflows/self-hosted.yml@v1.2.33 + secrets: inherit + with: + repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-worker' }} + cache_file: "uv.lock" + + staging: + name: Push Staging Image + needs: [build, test] + if: ${{ github.event_name == 'push' && (github.event.ref == 'refs/heads/main' || github.event.ref == 'refs/heads/staging') && github.repository_owner == 'codecov' }} + uses: codecov/gha-workflows/.github/workflows/push-env.yml@v1.2.33 + secrets: inherit + with: + environment: staging + repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-worker' }} + + production: + name: Push Production Image + needs: [build, test] + if: ${{ github.event_name == 'push' && github.event.ref == 'refs/heads/main' && github.repository_owner == 'codecov' }} + uses: codecov/gha-workflows/.github/workflows/push-env.yml@v1.2.33 + secrets: inherit + with: + environment: production + repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-worker' }} + + self-hosted: + name: Push Self Hosted Image + needs: [build-self-hosted, test] + secrets: inherit + if: ${{ github.event_name == 'push' && github.event.ref == 'refs/heads/main' && github.repository_owner == 'codecov' }} + uses: codecov/gha-workflows/.github/workflows/self-hosted.yml@v1.2.33 + with: + push_rolling: true + repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-worker' }} + cache_file: "uv.lock" diff --git a/apps/worker/.github/workflows/enforce-license-compliance.yml b/apps/worker/.github/workflows/enforce-license-compliance.yml new file mode 100644 index 0000000000..86be74100e --- /dev/null +++ b/apps/worker/.github/workflows/enforce-license-compliance.yml @@ -0,0 +1,14 @@ +name: Enforce License Compliance + +on: + pull_request: + branches: [main, master] + +jobs: + enforce-license-compliance: + runs-on: ubuntu-latest + steps: + - name: 'Enforce License Compliance' + uses: getsentry/action-enforce-license-compliance@57ba820387a1a9315a46115ee276b2968da51f3d # main + with: + fossa_api_key: ${{ secrets.FOSSA_API_KEY }} diff --git a/apps/worker/.github/workflows/mypy.yml b/apps/worker/.github/workflows/mypy.yml new file mode 100644 index 0000000000..d51e19b1e5 --- /dev/null +++ b/apps/worker/.github/workflows/mypy.yml @@ -0,0 +1,14 @@ +name: "Patch typing check" + +on: + push: + branches: + - main + - staging + pull_request: + merge_group: + +jobs: + patch-typing-check: + name: Run Patch Type Check + uses: codecov/gha-workflows/.github/workflows/mypy.yml@v1.2.33 diff --git a/apps/worker/.github/workflows/pr_detect_shared_changes.yml b/apps/worker/.github/workflows/pr_detect_shared_changes.yml new file mode 100644 index 0000000000..15d4e96d9f --- /dev/null +++ b/apps/worker/.github/workflows/pr_detect_shared_changes.yml @@ -0,0 +1,14 @@ +name: Detect dep version changes + +on: + pull_request: + +permissions: + pull-requests: "write" + +jobs: + shared-change-checker: + name: See if shared changed + uses: codecov/gha-workflows/.github/workflows/diff-dep.yml@main + with: + dep: 'shared' diff --git a/apps/worker/.github/workflows/self-hosted-release-pr.yml b/apps/worker/.github/workflows/self-hosted-release-pr.yml new file mode 100644 index 0000000000..aa094c802d --- /dev/null +++ b/apps/worker/.github/workflows/self-hosted-release-pr.yml @@ -0,0 +1,14 @@ +name: Create Self Hosted Release PR + +on: + workflow_dispatch: + inputs: + versionName: + description: "Name of version (ie 23.9.5)" + required: true + +jobs: + create-release-pr: + name: Create PR for Release ${{ github.event.inputs.versionName }} + uses: codecov/gha-workflows/.github/workflows/create-release-pr.yml@v1.2.33 + secrets: inherit diff --git a/apps/worker/.github/workflows/self-hosted-release.yml b/apps/worker/.github/workflows/self-hosted-release.yml new file mode 100644 index 0000000000..03d6eb7141 --- /dev/null +++ b/apps/worker/.github/workflows/self-hosted-release.yml @@ -0,0 +1,30 @@ +name: Create Self Hosted Release + +on: + pull_request: + branches: + - main + types: [closed] + +permissions: + contents: "read" + id-token: "write" + +jobs: + create-release: + name: Tag Release ${{ github.head_ref }} and Push Docker image to Docker Hub + if: ${{ github.event.pull_request.merged == true && startsWith(github.head_ref, 'release/') && github.repository_owner == 'codecov' }} + uses: codecov/gha-workflows/.github/workflows/create-release.yml@v1.2.33 + with: + tag_to_prepend: self-hosted- + secrets: inherit + + push-image: + needs: [create-release] + if: ${{ github.event.pull_request.merged == true && startsWith(github.head_ref, 'release/') && github.repository_owner == 'codecov' }} + uses: codecov/gha-workflows/.github/workflows/self-hosted.yml@v1.2.33 + secrets: inherit + with: + push_release: true + repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-worker' }} + cache_file: "uv.lock" diff --git a/apps/worker/.github/workflows/upload-overwatch.yml b/apps/worker/.github/workflows/upload-overwatch.yml new file mode 100644 index 0000000000..6c60a079a3 --- /dev/null +++ b/apps/worker/.github/workflows/upload-overwatch.yml @@ -0,0 +1,37 @@ +name: Upload Overwatch + +on: + pull_request: + types: + - opened + - synchronize + +jobs: + upload-overwatch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - name: Install UV + run: pip install uv + - name: Install Project Dependencies + run: | + uv export --format requirements-txt > requirements.txt + uv pip install -r requirements.txt --system + - name: Install Static Analysis Tools + run: | + pip install mypy==1.15.0 + pip install ruff==0.9.8 + - name: Install Overwatch CLI + run: | + curl -o overwatch-cli https://overwatch.codecov.dev/linux/cli + chmod +x overwatch-cli + - name: Run Overwatch CLI + run: | + ./overwatch-cli \ + --auth-token ${{ secrets.SENTRY_AUTH_TOKEN }} \ + --organization-slug codecov \ + python \ + --python-path $(which python3) \ No newline at end of file diff --git a/apps/worker/.gitignore b/apps/worker/.gitignore new file mode 100644 index 0000000000..54e9959b0f --- /dev/null +++ b/apps/worker/.gitignore @@ -0,0 +1,137 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +.mutmut-cache +gha-creds-*.json + +*.pem + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +*coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ +*.junit.xml +junit.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.testenv + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +background_app/.fdk/metrics +background_app/.report.json + +.pytype + +# Vim temporary files +*.swp +*.swo + +# Pycharm/Intellij +.idea/ + +# VCS +.sl +.git + +# MacOS +.DS_Store \ No newline at end of file diff --git a/apps/worker/.pre-commit-config.yaml b/apps/worker/.pre-commit-config.yaml new file mode 100644 index 0000000000..f9cebd057e --- /dev/null +++ b/apps/worker/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +repos: + - repo: local + hooks: + - id: lint + name: lint + description: "Lint and sort" + entry: make lint.local + pass_filenames: false + require_serial: true + language: system + - repo: https://github.com/pre-commit/mirrors-mypy + rev: "v1.10.0" + hooks: + - id: mypy + verbose: true + entry: bash -c 'mypy "$@" --explicit-package-bases || true' -- diff --git a/apps/worker/.python-version b/apps/worker/.python-version new file mode 100644 index 0000000000..3a4f41ef34 --- /dev/null +++ b/apps/worker/.python-version @@ -0,0 +1 @@ +3.13 \ No newline at end of file diff --git a/apps/worker/.vscode/README.md b/apps/worker/.vscode/README.md new file mode 100644 index 0000000000..2c266ab448 --- /dev/null +++ b/apps/worker/.vscode/README.md @@ -0,0 +1,11 @@ +As everyone will have their personal vscode preferences, +and might want to override settings inside of the workspace, +here is how you can make changes locally, without git prompting you to commit +your changes to these settings files: + +``` +git update-index --assume-unchanged .vscode/settings.json +``` + +This way, git will ignore your local changes, and you are free to change these +files as you see fit. diff --git a/apps/worker/.vscode/extensions.json b/apps/worker/.vscode/extensions.json new file mode 100644 index 0000000000..1fc832ae26 --- /dev/null +++ b/apps/worker/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "ms-python.python", + "ms-python.mypy-type-checker", + "charliermarsh.ruff" + ] +} diff --git a/apps/worker/.vscode/settings.json b/apps/worker/.vscode/settings.json new file mode 100644 index 0000000000..21a2e64474 --- /dev/null +++ b/apps/worker/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "[python]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + } + } +} diff --git a/apps/worker/CHANGELOG.md b/apps/worker/CHANGELOG.md new file mode 100644 index 0000000000..bdef09d6ad --- /dev/null +++ b/apps/worker/CHANGELOG.md @@ -0,0 +1,43 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [Unreleased] + +### Added + +### Changed + +### Deprecated + +### Removed +- Removed safeguard protecting against small downtimes when `Upload` objects were not created yet + +### Fixed + +### Security + +## [v4.5.10] + +### Added +- Added `flag_management` field and subfields to user YAML +- Added support for PR billing licenses on enterprise cases + +### Changed + +### Deprecated + +### Removed +- Retired Gitlab v3 specific support + +### Fixed +- Fixed visibility timeout issues, that in some cases could have caused the same task to be rerun more than once. + +### Security + + +[unreleased]: https://github.com/codecov/worker/compare/v4.5.9...HEAD +[v4.5.10]: https://github.com/codecov/worker/compare/v4.5.9...v4.5.10 + diff --git a/apps/worker/LICENSE.md b/apps/worker/LICENSE.md new file mode 100644 index 0000000000..630ce64ac6 --- /dev/null +++ b/apps/worker/LICENSE.md @@ -0,0 +1,105 @@ +# Functional Source License, Version 1.1, Apache 2.0 Future License + +## Abbreviation + +FSL-1.1-Apache-2.0 + +## Notice + +Copyright 2018-2024 Functional Software, Inc. dba Sentry + +## Terms and Conditions + +### Licensor ("We") + +The party offering the Software under these Terms and Conditions. + +### The Software + +The "Software" is each version of the software that we make available under +these Terms and Conditions, as indicated by our inclusion of these Terms and +Conditions with the Software. + +### License Grant + +Subject to your compliance with this License Grant and the Patents, +Redistribution and Trademark clauses below, we hereby grant you the right to +use, copy, modify, create derivative works, publicly perform, publicly display +and redistribute the Software for any Permitted Purpose identified below. + +### Permitted Purpose + +A Permitted Purpose is any purpose other than a Competing Use. A Competing Use +means making the Software available to others in a commercial product or +service that: + +1. substitutes for the Software; + +2. substitutes for any other product or service we offer using the Software + that exists as of the date we make the Software available; or + +3. offers the same or substantially similar functionality as the Software. + +Permitted Purposes specifically include using the Software: + +1. for your internal use and access; + +2. for non-commercial education; + +3. for non-commercial research; and + +4. in connection with professional services that you provide to a licensee + using the Software in accordance with these Terms and Conditions. + +### Patents + +To the extent your use for a Permitted Purpose would necessarily infringe our +patents, the license grant above includes a license under our patents. If you +make a claim against any party that the Software infringes or contributes to +the infringement of any patent, then your patent license to the Software ends +immediately. + +### Redistribution + +The Terms and Conditions apply to all copies, modifications and derivatives of +the Software. + +If you redistribute any copies, modifications or derivatives of the Software, +you must include a copy of or a link to these Terms and Conditions and not +remove any copyright notices provided in or with the Software. + +### Disclaimer + +THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR +PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT. + +IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE +SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, +EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE. + +### Trademarks + +Except for displaying the License Details and identifying us as the origin of +the Software, you have no right under these Terms and Conditions to use our +trademarks, trade names, service marks or product names. + +## Grant of Future License + +We hereby irrevocably grant you an additional license to use the Software under +the Apache License, Version 2.0 that is effective on the second anniversary of +the date we make the Software available. On or after that date, you may use the +Software under the Apache License, Version 2.0, in which case the following +will apply: + +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. diff --git a/apps/worker/Makefile b/apps/worker/Makefile new file mode 100644 index 0000000000..43d83e72ae --- /dev/null +++ b/apps/worker/Makefile @@ -0,0 +1,255 @@ +sha ?= $(shell git rev-parse --short=7 HEAD) +full_sha ?= $(shell git rev-parse HEAD) +release_version = `cat VERSION` +_gcr := ${CODECOV_WORKER_GCR_REPO_BASE} +merge_sha ?= $(shell git merge-base HEAD^ origin/main) +build_date ?= $(shell git show -s --date=iso8601-strict --pretty=format:%cd $$sha) +name ?= worker +branch ?= $(shell git branch | grep \* | cut -f2 -d' ') +gh_access_token := $(shell echo ${GH_ACCESS_TOKEN}) +epoch ?= $(shell date +"%s") + +AR_REPO ?= codecov/worker +DOCKERHUB_REPO ?= codecov/self-hosted-worker +VERSION ?= release-${sha} +CODECOV_UPLOAD_TOKEN ?= "notset" +CODECOV_STATIC_TOKEN ?= "notset" +CODECOV_URL ?= "https://api.codecov.io" + +DEFAULT_REQS_TAG := requirements-v1-$(shell sha1sum uv.lock | cut -d ' ' -f 1)-$(shell sha1sum docker/Dockerfile.requirements | cut -d ' ' -f 1) +REQUIREMENTS_TAG ?= ${DEFAULT_REQS_TAG} + +# We allow this to be overridden so that we can run `pytest` from this directory +# but have the junit file use paths relative to a parent directory. This will +# help us move to a monorepo. +PYTEST_ROOTDIR ?= "." + +export DOCKER_BUILDKIT=1 +export WORKER_DOCKER_REPO=${AR_REPO} +export WORKER_DOCKER_VERSION=${VERSION} +export CODECOV_TOKEN=${CODECOV_UPLOAD_TOKEN} + +# Codecov CLI version to use +CODECOV_CLI_VERSION := 0.5.1 + +build: + $(MAKE) build.requirements + $(MAKE) build.local + + +# for portable builds to dockerhub, for use with local development and +# acceptance testing. +build.portable: + docker build -f dockerscripts/Dockerfile . -t codecov/$(name)-portable \ + --label "org.label-schema.build-date"="$(build_date)" \ + --label "org.label-schema.name"="$(name)" \ + --label "org.label-schema.vcs-ref"="$(sha)" \ + --label "org.label-schema.vendor"="Codecov" \ + --label "org.label-schema.version"="${release_version}-${sha}" \ + --label "org.vcs-branch"="$(branch)" \ + --build-arg GH_ACCESS_TOKEN=${gh_access_token} \ + --build-arg COMMIT_SHA="${sha}" \ + --build-arg RELEASE_VERSION="${release_version}" + +test: + COVERAGE_CORE=sysmon pytest --cov=./ --junitxml=junit.xml -o junit_family=legacy -c pytest.ini --rootdir=${PYTEST_ROOTDIR} + +test.unit: + COVERAGE_CORE=sysmon pytest --cov=./ -m "not integration" --cov-report=xml:unit.coverage.xml --junitxml=unit.junit.xml -o junit_family=legacy -c pytest.ini --rootdir=${PYTEST_ROOTDIR} + +test.integration: + COVERAGE_CORE=sysmon pytest --cov=./ -m "integration" --cov-report=xml:integration.coverage.xml --junitxml=integration.junit.xml -o junit_family=legacy -c pytest.ini --rootdir=${PYTEST_ROOTDIR} + +lint: + make lint.install + make lint.run + +# used for CI +lint.install: + echo "Installing..." + pip install -Iv ruff + +lint.local: + make lint.install.local + make lint.run + +lint.install.local: + echo "Installing..." + uv add --dev ruff + +# The preferred method (for now) w.r.t. fixable rules is to manually update the makefile +# with --fix and re-run 'make lint.' Since ruff is constantly adding rules this is a slight +# amount of "needed" friction imo. +lint.run: + ruff check + ruff format + +lint.check: + echo "Linting..." + ruff check + echo "Formatting..." + ruff format --check + +build.requirements: + # If make was given a different requirements tag, we assume a suitable image + # was already built (e.g. by umbrella) and don't want to build this one. + ifneq (${REQUIREMENTS_TAG},${DEFAULT_REQS_TAG}) + echo "Error: building worker reqs image despite another being provided" + exit 1 + endif + # if docker pull succeeds, we have already build this version of + # requirements.txt. Otherwise, build and push a version tagged + # with the hash of this requirements.txt + docker pull ${AR_REPO}:${REQUIREMENTS_TAG} || docker build \ + -f docker/Dockerfile.requirements . \ + -t ${AR_REPO}:${REQUIREMENTS_TAG} \ + -t codecov/worker-ci-requirements:${REQUIREMENTS_TAG} + +build.local: + docker build -f docker/Dockerfile . \ + -t ${AR_REPO}:latest \ + -t ${AR_REPO}:${VERSION} \ + --build-arg REQUIREMENTS_IMAGE=${AR_REPO}:${REQUIREMENTS_TAG} \ + --build-arg BUILD_ENV=local + +build.app: + docker build -f docker/Dockerfile . \ + -t ${AR_REPO}:latest \ + -t ${AR_REPO}:${VERSION} \ + --label "org.label-schema.vendor"="Codecov" \ + --label "org.label-schema.version"="${release_version}-${sha}" \ + --label "org.opencontainers.image.revision"="$(full_sha)" \ + --label "org.opencontainers.image.source"="github.com/codecov/worker" \ + --build-arg REQUIREMENTS_IMAGE=${AR_REPO}:${REQUIREMENTS_TAG} \ + --build-arg RELEASE_VERSION=${VERSION} \ + --build-arg BUILD_ENV=cloud + +build.self-hosted: + make build.self-hosted-base + make build.self-hosted-runtime + +build.self-hosted-base: + docker build -f docker/Dockerfile . \ + -t ${DOCKERHUB_REPO}:latest-no-dependencies \ + -t ${DOCKERHUB_REPO}:${VERSION}-no-dependencies \ + --build-arg REQUIREMENTS_IMAGE=${AR_REPO}:${REQUIREMENTS_TAG} \ + --build-arg RELEASE_VERSION=${VERSION} \ + --build-arg BUILD_ENV=self-hosted + +build.self-hosted-runtime: + docker build -f docker/Dockerfile . \ + -t ${DOCKERHUB_REPO}:latest \ + -t ${DOCKERHUB_REPO}:${VERSION} \ + --label "org.label-schema.vendor"="Codecov" \ + --label "org.label-schema.version"="${release_version}-${sha}" \ + --build-arg REQUIREMENTS_IMAGE=${AR_REPO}:${REQUIREMENTS_TAG} \ + --build-arg RELEASE_VERSION=${VERSION} \ + --build-arg BUILD_ENV=self-hosted-runtime + +tag.latest: + docker tag ${AR_REPO}:${VERSION} ${AR_REPO}:latest + +tag.staging: + docker tag ${AR_REPO}:${VERSION} ${AR_REPO}:staging-${VERSION} + +tag.production: + docker tag ${AR_REPO}:${VERSION} ${AR_REPO}:production-${VERSION} + +tag.self-hosted-rolling: + docker tag ${DOCKERHUB_REPO}:${VERSION}-no-dependencies ${DOCKERHUB_REPO}:rolling_no_dependencies + docker tag ${DOCKERHUB_REPO}:${VERSION} ${DOCKERHUB_REPO}:rolling + +tag.self-hosted-release: + docker tag ${DOCKERHUB_REPO}:${VERSION}-no-dependencies ${DOCKERHUB_REPO}:${release_version}_no_dependencies + docker tag ${DOCKERHUB_REPO}:${VERSION}-no-dependencies ${DOCKERHUB_REPO}:latest_calver_no_dependencies + docker tag ${DOCKERHUB_REPO}:${VERSION}-no-dependencies ${DOCKERHUB_REPO}:latest_stable_no_dependencies + docker tag ${DOCKERHUB_REPO}:${VERSION} ${DOCKERHUB_REPO}:${release_version} + docker tag ${DOCKERHUB_REPO}:${VERSION} ${DOCKERHUB_REPO}:latest-stable + docker tag ${DOCKERHUB_REPO}:${VERSION} ${DOCKERHUB_REPO}:latest-calver + +load.requirements: + docker load --input requirements.tar + docker tag codecov/worker-ci-requirements:${REQUIREMENTS_TAG} ${AR_REPO}:${REQUIREMENTS_TAG} + +load.self-hosted: + docker load --input self-hosted-runtime.tar + docker load --input self-hosted.tar + +save.app: + docker save -o app.tar ${AR_REPO}:${VERSION} + +save.requirements: + docker tag ${AR_REPO}:${REQUIREMENTS_TAG} codecov/worker-ci-requirements:${REQUIREMENTS_TAG} + docker save -o requirements.tar codecov/worker-ci-requirements:${REQUIREMENTS_TAG} + +save.self-hosted: + make save.self-hosted-base + make save.self-hosted-runtime + +save.self-hosted-base: + docker save -o self-hosted.tar ${DOCKERHUB_REPO}:${VERSION}-no-dependencies + +save.self-hosted-runtime: + docker save -o self-hosted-runtime.tar ${DOCKERHUB_REPO}:${VERSION} + +push.latest: + docker push ${AR_REPO}:latest + +push.staging: + docker push ${AR_REPO}:staging-${VERSION} + +push.production: + docker push ${AR_REPO}:production-${VERSION} + +push.requirements: + docker push ${AR_REPO}:${REQUIREMENTS_TAG} + +push.self-hosted-release: + docker push ${DOCKERHUB_REPO}:${release_version}_no_dependencies + docker push ${DOCKERHUB_REPO}:latest_calver_no_dependencies + docker push ${DOCKERHUB_REPO}:latest_stable_no_dependencies + docker push ${DOCKERHUB_REPO}:${release_version} + docker push ${DOCKERHUB_REPO}:latest-stable + docker push ${DOCKERHUB_REPO}:latest-calver + +push.self-hosted-rolling: + docker push ${DOCKERHUB_REPO}:rolling_no_dependencies + docker push ${DOCKERHUB_REPO}:rolling + +shell: + docker-compose exec worker bash + +test_env.up: + env | grep GITHUB > .testenv; true + docker-compose up -d + +test_env.prepare: + docker-compose exec worker make test_env.container_prepare + +test_env.check_db: + docker-compose exec worker make test_env.container_check_db + +test_env.install_cli: + pip install --no-cache-dir codecov-cli==$(CODECOV_CLI_VERSION) + +test_env.container_prepare: + apt-get update + apt-get install -y git build-essential netcat-traditional + git config --global --add safe.directory /apps/app/worker || true + +test_env.container_check_db: + while ! nc -vz postgres 5432; do sleep 1; echo "waiting for postgres"; done + while ! nc -vz timescale 5432; do sleep 1; echo "waiting for timescale"; done + +test_env.run_unit: + docker-compose exec worker make test.unit PYTEST_ROOTDIR=${PYTEST_ROOTDIR} + +test_env.run_integration: + docker-compose exec worker make test.integration PYTEST_ROOTDIR=${PYTEST_ROOTDIR} + +test_env: + make test_env.up + make test_env.prepare + make test_env.check_db + make test_env.run_unit + make test_env.run_integration diff --git a/apps/worker/README.md b/apps/worker/README.md new file mode 100644 index 0000000000..2fb023af75 --- /dev/null +++ b/apps/worker/README.md @@ -0,0 +1,119 @@ +# worker + +![Actions](https://github.com/codecov/worker/actions/workflows/ci.yml/badge.svg) +[![worker](https://codecov.io/github/codecov/worker/coverage.svg?branch=master&token=BWTOrjBaE5)](https://codecov.io/github/codecov/worker) + +> We believe that everyone should have access to quality software (like Sentry), that’s why we have always offered Codecov for free to open source maintainers. +> +> By making our code public, we’re not only joining the community that’s supported us from the start — but also want to make sure that every developer can contribute to and build on the Codecov experience. + +Code for Background Workers of Codecov. This is built on top of the `celery` async framework + +## Quickstart + +### Setting Virtual Env + +This repo is set up with `direnv`, which will automatically +create a virtualenv in `.venv` and activate the environment +when you `cd` into the directory in your console. + +Please take a look at [direnv](https://github.com/direnv/direnv/blob/master/docs/installation.md) +for more installation information. + +### Installing dependencies + +Make sure to: + +- Install rust. See https://www.rust-lang.org/tools/install +- Have access to any private codecov repos listed in the requirements.txt file. See [here](https://codecovio.atlassian.net/wiki/spaces/ENG/pages/1270743045/Setup) for help on getting that set up. + +To install the dependencies, run + +``` +uv sync +``` + +### Environment variables + +In order to successfully run `make push`, you'll need to define the `CODECOV_WORKER_GCR_REPO_BASE` variable. See its use in [`Makefile`](Makefile) to understand what it's used for. An example is `gcr.io/your-project-here/codecov`. Codecov internal users, see [the env setup documentation](https://www.notion.so/sentry/Environment-variables-for-building-pushing-Docker-images-locally-3159e90c5e6f4db4bfbde8800cdad2c0?pvs=4) for our canonical defaults. + +### Running Tests Locally + +Simply: + +``` +make test_env +``` + +What this does is start a few dependent databases with `docker-compose`, +then it runs the tests in the `worker` docker container. + +### Linting and Import Sorts + +Install/run `black` and `isort` using + +``` +make lint +``` + +### Getting into docker + +To build this into a docker image: + +``` +make build.base +make build +``` + +To run this as part of the whole infrasctructure, you will be better served by getting the main codebase and running `docker-compose up` from there + +### Getting into enterprise mode + +To generate an enterprise build, do + +``` +make build.enterprise +``` + +## Versioning + +The source of truth on which version we use is in the file `VERSION`. Every script that tags things with versions will consult that file to see what version it is. + +That file is manually updated. We use semantic versioning. + +If you are unsure whether you need to change that version at a given moment, the answer is that you probaby don't then. We have multiple deploys on the same version, and only change it when we want to cut a version to enterprise. + +## Upgrading Dependencies + +This repository uses `uv` to manage dependencies. +Make your dependency updates to `pyproject.toml` then run: + +``` +uv sync +``` + +to update the `uv.lock` file. + +### After deploying + +If you are deploying or helping with a deploy, make sure to: + +1. Watch logs (on datadog and sentry) +2. Monitor error rates and timing graphs on the dashboards we have set up + +As the deployer, it is your responsibility to make sure the system is working as expected post-deploy. If not, you might need to do a rollback. + +## Code Structure + +Before getting into changing the code, try to use the following structure (feel free to suggest changes.Some bits of it are based on our experience) + +- `helpers` - Those are the "low" level pieces of code, that don't depend on database models or any other heavy business logic. Those shouldn't depend on anything else on the codebase, preferrably +- `database` - Those contain database models. They can use logic from `helpers` and other models, but nothing else. Try to avoid any heavy logic in this code. +- `services` - Those are heavier pieces of logic, that don't talk to the external world. They can use `helpers` and `database` logic, and among themselves. But make sure that if a service _bravo_ depends on service _alpha_, then _alpha_ should not depend on any part of _bravo_ +- `tasks` - Those are the parts of the code that talk to the external world: it has the tasks that are triggered by external containers. They can depend on `helpers`, `models` and `services`, but NEVER depend on another task (except to schedule them). If some code is common to two tasks, try to put it in a `service` or somewhere else. + +You will also notice some usage of the package https://github.com/codecov/shared for various things. The logic that is there is used by both here and `codecov/api` codebase. So feel free to make changes there, but dont do anything that will break compatibility too hard. + +## Contributing + +This repository, like all of Codecov's repositories, strives to follow our general [Contributing guidlines](https://github.com/codecov/contributing). If you're considering making a contribution to this repository, we encourage review of our Contributing guidelines first. diff --git a/apps/worker/VERSION b/apps/worker/VERSION new file mode 100644 index 0000000000..86a8f0262a --- /dev/null +++ b/apps/worker/VERSION @@ -0,0 +1 @@ +25.4.1 \ No newline at end of file diff --git a/apps/worker/__init__.py b/apps/worker/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/app.py b/apps/worker/app.py new file mode 100644 index 0000000000..2dccd4a0a0 --- /dev/null +++ b/apps/worker/app.py @@ -0,0 +1,22 @@ +import logging +import logging.config + +from celery import Celery, signals + +from helpers.logging_config import get_logging_config_dict +from helpers.sentry import initialize_sentry, is_sentry_enabled + +log = logging.getLogger(__name__) + +_config_dict = get_logging_config_dict() +logging.config.dictConfig(_config_dict) + + +celery_app = Celery("tasks") +celery_app.config_from_object("celery_config:CeleryWorkerConfig") + + +@signals.celeryd_init.connect +def init_sentry(**_kwargs): + if is_sentry_enabled(): + initialize_sentry() diff --git a/apps/worker/celery_config.py b/apps/worker/celery_config.py new file mode 100644 index 0000000000..6814334bd8 --- /dev/null +++ b/apps/worker/celery_config.py @@ -0,0 +1,146 @@ +# http://docs.celeryq.org/en/latest/configuration.html#configuration +import gc +import logging +import logging.config +from datetime import timedelta + +from celery import signals +from celery.beat import BeatLazyFunc +from celery.schedules import crontab +from shared.celery_config import ( + BaseCeleryConfig, + brolly_stats_rollup_task_name, + # flare_cleanup_task_name, + gh_app_webhook_check_task_name, + health_check_task_name, +) +from shared.config import get_config +from shared.helpers.cache import RedisBackend, cache +from shared.helpers.redis import get_redis_connection + +from celery_task_router import route_task +from helpers.clock import get_utc_now_as_iso_format +from helpers.health_check import get_health_check_interval_seconds + +log = logging.getLogger(__name__) + + +@signals.worker_before_create_process.connect +def prefork_gc_freeze(**kwargs) -> None: + # This comes from https://github.com/getsentry/sentry/pull/63001 + # More info https://www.youtube.com/watch?v=Hgw_RlCaIds + # The idea is to save memory in the worker subprocesses + # By freezing all the stuff we can just read from + gc.freeze() + + +@signals.setup_logging.connect +def initialize_logging(loglevel=logging.INFO, **kwargs): + celery_logger = logging.getLogger("celery") + celery_logger.setLevel(loglevel) + log.info("Initialized celery logging") + return celery_logger + + +@signals.worker_process_init.connect +def initialize_cache(**kwargs): + log.info("Initialized cache") + redis_cache_backend = RedisBackend(get_redis_connection()) + cache.configure(redis_cache_backend) + + +hourly_check_task_name = "app.cron.hourly_check.HourlyCheckTask" +daily_plan_manager_task_name = "app.cron.daily.PlanManagerTask" + +notify_error_task_name = "app.tasks.notify.NotifyErrorTask" + +# Backfill GH Apps +backfill_existing_gh_app_installations_name = "app.tasks.backfill_existing_gh_app_installations.BackfillExistingGHAppInstallationsTask" +backfill_existing_individual_gh_app_installation_name = "app.tasks.backfill_existing_individual_gh_app_installation.BackfillExistingIndividualGHAppInstallationTask" +backfill_owners_without_gh_app_installations_name = "app.tasks.backfill_owners_without_gh_app_installations.BackfillOwnersWithoutGHAppInstallationsTask" +backfill_owners_without_gh_app_installation_individual_name = "app.tasks.backfill_owners_without_gh_app_installation_individual.BackfillOwnersWithoutGHAppInstallationIndividualTask" + +trial_expiration_task_name = "app.tasks.plan.TrialExpirationTask" +trial_expiration_cron_task_name = "app.cron.plan.TrialExpirationCronTask" + +update_branches_task_name = "app.cron.branches.UpdateBranchesTask" +update_branches_task_name = "app.cron.test_instances.BackfillTestInstancesTask" + +cache_rollup_cron_task_name = "app.cron.cache_rollup.CacheRollupTask" + +regular_cleanup_cron_task_name = "app.cron.cleanup.RegularCleanup" + + +def _beat_schedule(): + beat_schedule = { + "hourly_check": { + "task": hourly_check_task_name, + "schedule": crontab(minute="0"), + "kwargs": { + "cron_task_generation_time_iso": BeatLazyFunc(get_utc_now_as_iso_format) + }, + }, + "github_app_webhooks_task": { + "task": gh_app_webhook_check_task_name, + "schedule": crontab(minute="0", hour="0,6,12,18"), + "kwargs": { + "cron_task_generation_time_iso": BeatLazyFunc(get_utc_now_as_iso_format) + }, + }, + "trial_expiration_cron": { + "task": trial_expiration_cron_task_name, + "schedule": crontab(minute="0", hour="4"), # 4 UTC is 12am EDT + "kwargs": { + "cron_task_generation_time_iso": BeatLazyFunc(get_utc_now_as_iso_format) + }, + }, + "regular_cleanup": { + "task": regular_cleanup_cron_task_name, + "schedule": crontab(minute="0", hour="4"), + "kwargs": { + "cron_task_generation_time_iso": BeatLazyFunc(get_utc_now_as_iso_format) + }, + }, + # "flare_cleanup": { + # "task": flare_cleanup_task_name, + # "schedule": crontab(minute="0", hour="5"), # every day, 5am UTC (10pm PDT) + # "kwargs": { + # "cron_task_generation_time_iso": BeatLazyFunc(get_utc_now_as_iso_format) + # }, + # }, + } + + if get_config("setup", "health_check", "enabled", default=False): + beat_schedule["health_check_task"] = { + "task": health_check_task_name, + "schedule": timedelta(seconds=get_health_check_interval_seconds()), + "kwargs": { + "cron_task_generation_time_iso": BeatLazyFunc(get_utc_now_as_iso_format) + }, + } + + if get_config("setup", "telemetry", "enabled", default=True): + beat_schedule["brolly_stats_rollup"] = { + "task": brolly_stats_rollup_task_name, + "schedule": crontab(minute="0", hour="2"), + "kwargs": { + "cron_task_generation_time_iso": BeatLazyFunc(get_utc_now_as_iso_format) + }, + } + + if get_config("setup", "cache_rollup", "enabled", default=False): + beat_schedule["cache_rollup"] = { + "task": cache_rollup_cron_task_name, + "schedule": crontab(minute="0", hour="2"), + "kwargs": { + "cron_task_generation_time_iso": BeatLazyFunc(get_utc_now_as_iso_format) + }, + } + + return beat_schedule + + +class CeleryWorkerConfig(BaseCeleryConfig): + beat_schedule = _beat_schedule() + + task_routes = route_task diff --git a/apps/worker/celery_task_router.py b/apps/worker/celery_task_router.py new file mode 100644 index 0000000000..1d33b59584 --- /dev/null +++ b/apps/worker/celery_task_router.py @@ -0,0 +1,119 @@ +import shared.celery_config as shared_celery_config +from shared.celery_router import route_tasks_based_on_user_plan +from shared.plan.constants import DEFAULT_FREE_PLAN + +from database.engine import get_db_session +from database.models.core import Commit, CompareCommit, Owner, Repository +from database.models.labelanalysis import LabelAnalysisRequest +from database.models.staticanalysis import StaticAnalysisSuite + + +def _get_user_plan_from_ownerid(db_session, ownerid, *args, **kwargs) -> str: + result = db_session.query(Owner.plan).filter(Owner.ownerid == ownerid).first() + if result: + return result.plan + return DEFAULT_FREE_PLAN + + +def _get_user_plan_from_repoid(db_session, repoid, *args, **kwargs) -> str: + result = ( + db_session.query(Owner.plan) + .join(Repository.owner) + .filter(Repository.repoid == repoid) + .first() + ) + if result: + return result.plan + return DEFAULT_FREE_PLAN + + +def _get_user_plan_from_org_ownerid(dbsession, org_ownerid, *args, **kwargs) -> str: + return _get_user_plan_from_ownerid(dbsession, ownerid=org_ownerid) + + +def _get_user_plan_from_comparison_id(dbsession, comparison_id, *args, **kwargs) -> str: + result = ( + dbsession.query(Owner.plan) + .join(CompareCommit.compare_commit) + .join(Commit.repository) + .join(Repository.owner) + .filter(CompareCommit.id_ == comparison_id) + .first() + ) + if result: + return result.plan + return DEFAULT_FREE_PLAN + + +def _get_user_plan_from_label_request_id(dbsession, request_id, *args, **kwargs) -> str: + result = ( + dbsession.query(Owner.plan) + .join(LabelAnalysisRequest.head_commit) + .join(Commit.repository) + .join(Repository.owner) + .filter(LabelAnalysisRequest.id_ == request_id) + .first() + ) + if result: + return result.plan + return DEFAULT_FREE_PLAN + + +def _get_user_plan_from_suite_id(dbsession, suite_id, *args, **kwargs) -> str: + result = ( + dbsession.query(Owner.plan) + .join(StaticAnalysisSuite.commit) + .join(Commit.repository) + .join(Repository.owner) + .filter(StaticAnalysisSuite.id_ == suite_id) + .first() + ) + if result: + return result.plan + return DEFAULT_FREE_PLAN + + +def _get_user_plan_from_task(dbsession, task_name: str, task_kwargs: dict) -> str: + owner_plan_lookup_funcs = { + # from ownerid + shared_celery_config.delete_owner_task_name: _get_user_plan_from_ownerid, + shared_celery_config.send_email_task_name: _get_user_plan_from_ownerid, + shared_celery_config.sync_repos_task_name: _get_user_plan_from_ownerid, + shared_celery_config.sync_teams_task_name: _get_user_plan_from_ownerid, + # from org_ownerid + shared_celery_config.new_user_activated_task_name: _get_user_plan_from_org_ownerid, + # from repoid + shared_celery_config.pre_process_upload_task_name: _get_user_plan_from_repoid, + shared_celery_config.upload_task_name: _get_user_plan_from_repoid, + shared_celery_config.upload_processor_task_name: _get_user_plan_from_repoid, + shared_celery_config.notify_task_name: _get_user_plan_from_repoid, + shared_celery_config.commit_update_task_name: _get_user_plan_from_repoid, + shared_celery_config.flush_repo_task_name: _get_user_plan_from_repoid, + shared_celery_config.status_set_error_task_name: _get_user_plan_from_repoid, + shared_celery_config.status_set_pending_task_name: _get_user_plan_from_repoid, + shared_celery_config.pulls_task_name: _get_user_plan_from_repoid, + shared_celery_config.upload_finisher_task_name: _get_user_plan_from_repoid, # didn't want to directly import the task module + shared_celery_config.manual_upload_completion_trigger_task_name: _get_user_plan_from_repoid, + # from comparison_id + shared_celery_config.compute_comparison_task_name: _get_user_plan_from_comparison_id, + # from label_request_id + shared_celery_config.label_analysis_task_name: _get_user_plan_from_label_request_id, + # from suite_id + shared_celery_config.static_analysis_task_name: _get_user_plan_from_suite_id, + } + func_to_use = owner_plan_lookup_funcs.get( + task_name, lambda *args, **kwargs: DEFAULT_FREE_PLAN + ) + return func_to_use(dbsession, **task_kwargs) + + +def route_task(name, args, kwargs, options, task=None, **kw): + """Function to dynamically route tasks to the proper queue. + Docs: https://docs.celeryq.dev/en/stable/userguide/routing.html#routers + """ + + user_plan = options.get("user_plan") + if user_plan is None: + db_session = get_db_session() + user_plan = _get_user_plan_from_task(db_session, name, kwargs) + return route_tasks_based_on_user_plan(name, user_plan) diff --git a/apps/worker/codecov.yml b/apps/worker/codecov.yml new file mode 100644 index 0000000000..41ac1a3c18 --- /dev/null +++ b/apps/worker/codecov.yml @@ -0,0 +1,27 @@ +codecov: + require_ci_to_pass: false + notify: + wait_for_ci: false + +component_management: + default_rules: + statuses: + - type: project + target: auto + individual_components: + - component_id: actual_code + name: NonTestCode + paths: + - "!conftest.py" + - "!**/conftest.py" + - "!**tests**/test_*.py" + - "!database/tests/factories/**" + - component_id: no_tasks + name: OutsideTasks + paths: + - "!tasks/**" + flag_regexes: + - "unit" + +test_analytics: + flake_detection: true diff --git a/apps/worker/conftest.py b/apps/worker/conftest.py new file mode 100644 index 0000000000..eb4b358e8e --- /dev/null +++ b/apps/worker/conftest.py @@ -0,0 +1,424 @@ +import logging +import os +from pathlib import Path + +import mock +import pytest +import vcr +from shared.config import ConfigHelper +from shared.storage.memory import MemoryStorageService +from shared.torngit import Github as GithubHandler +from sqlalchemy import event +from sqlalchemy.orm import Session +from sqlalchemy_utils import database_exists + +from celery_config import initialize_logging +from database.base import Base +from database.engine import json_dumps +from helpers.environment import _get_cached_current_env + + +# @pytest.hookimpl(tryfirst=True) +def pytest_configure(config): + """ + Allows plugins and conftest files to perform initial configuration. + This hook is called for every plugin and initial conftest + file after command line options have been parsed. + """ + os.environ["CURRENT_ENVIRONMENT"] = "local" + os.environ["RUN_ENV"] = "DEV" + _get_cached_current_env.cache_clear() + initialize_logging() + + +def pytest_itemcollected(item): + """logic that runs on the test collection step""" + if "codecov_vcr" in item.fixturenames: + # Tests with codecov_vcr fixtures are automatically 'integration' + item.add_marker("integration") + + +@pytest.fixture(scope="session") +def engine(request, sqlalchemy_db, sqlalchemy_connect_url, app_config): + """Engine configuration. + See http://docs.sqlalchemy.org/en/latest/core/engines.html + for more details. + :sqlalchemy_connect_url: Connection URL to the database. E.g + postgresql://scott:tiger@localhost:5432/mydatabase + :app_config: Path to a ini config file containing the sqlalchemy.url + config variable in the DEFAULT section. + :returns: Engine instance + """ + if app_config: + from sqlalchemy import engine_from_config + + engine = engine_from_config(app_config) + elif sqlalchemy_connect_url: + from sqlalchemy.engine import create_engine + + engine = create_engine(sqlalchemy_connect_url, json_serializer=json_dumps) + else: + raise RuntimeError("Can not establish a connection to the database") + + # Put a suffix like _gw0, _gw1 etc on xdist processes + xdist_suffix = getattr(request.config, "slaveinput", {}).get("slaveid") + if engine.url.database != ":memory:" and xdist_suffix is not None: + engine.url.database = "{}_{}".format(engine.url.database, xdist_suffix) + engine = create_engine(engine.url) # override engine + + # Check that the DB exist and migrate the unmigrated SQLALchemy models as a stop-gap + database_url = sqlalchemy_connect_url + if not database_exists(database_url): + raise RuntimeError(f"SQLAlchemy cannot connect to DB at {database_url}") + + Base.metadata.tables["timeseries_measurement"].create(bind=engine, checkfirst=True) + Base.metadata.tables["timeseries_dataset"].create(bind=engine, checkfirst=True) + + Base.metadata.tables["compare_commitcomparison"].create( + bind=engine, checkfirst=True + ) + Base.metadata.tables["compare_flagcomparison"].create(bind=engine, checkfirst=True) + Base.metadata.tables["compare_componentcomparison"].create( + bind=engine, checkfirst=True + ) + + Base.metadata.tables["labelanalysis_labelanalysisrequest"].create( + bind=engine, checkfirst=True + ) + Base.metadata.tables["labelanalysis_labelanalysisprocessingerror"].create( + bind=engine, checkfirst=True + ) + + Base.metadata.tables["staticanalysis_staticanalysissuite"].create( + bind=engine, checkfirst=True + ) + Base.metadata.tables["staticanalysis_staticanalysissinglefilesnapshot"].create( + bind=engine, checkfirst=True + ) + Base.metadata.tables["staticanalysis_staticanalysissuitefilepath"].create( + bind=engine, checkfirst=True + ) + + yield engine + + print("Disposing engine") # noqa: T201 + engine.dispose() + + +@pytest.fixture(scope="session") +def sqlalchemy_db(request: pytest.FixtureRequest, django_db_blocker, django_db_setup): + # Bootstrap the DB by running the Django bootstrap version. + from django.conf import settings + from django.db import connections + from django.test.utils import setup_databases, teardown_databases + + keepdb = request.config.getvalue("reuse_db", False) and not request.config.getvalue( + "create_db", False + ) + + with django_db_blocker.unblock(): + # Temporarily reset the database to the SQLAlchemy DBs to run the migrations. + original_db_name = settings.DATABASES["default"]["NAME"] + original_test_name = settings.DATABASES["default"]["TEST"]["NAME"] + settings.DATABASES["default"]["NAME"] = "sqlalchemy" + settings.DATABASES["default"]["TEST"]["NAME"] = "test_postgres_sqlalchemy" + for connection in connections: + if "timeseries" in connection: + with connections[connection].cursor() as cursor: + cursor.execute( + "SELECT _timescaledb_internal.stop_background_workers();" + ) + db_cfg = setup_databases( + verbosity=request.config.option.verbose, + interactive=False, + keepdb=keepdb, + ) + for connection in connections: + if "timeseries" in connection: + with connections[connection].cursor() as cursor: + cursor.execute( + "SELECT _timescaledb_internal.start_background_workers();" + ) + settings.DATABASES["default"]["NAME"] = original_db_name + settings.DATABASES["default"]["TEST"]["NAME"] = original_test_name + + # Hack to get the default connection for the test database to _actually_ be the + # Django database that the django_db should actually use. It was set to the SQLAlchemy database, + # but this makes sure that the default Django DB connection goes to the Django database. + # Since the database was already created and migrated in the django_db_setup fixture, + # we set keepdb=True to avoid recreating the database and rerunning the migrations. + connections.configure_settings(settings.DATABASES) + connections["default"].creation.create_test_db( + verbosity=request.config.option.verbose, + autoclobber=True, + keepdb=True, + ) + + yield + + if not keepdb: + try: + with django_db_blocker.unblock(): + # Need to set `test_postgres_sqlalchemy` as the main db name to tear down properly. + settings.DATABASES["default"]["NAME"] = "test_postgres_sqlalchemy" + teardown_databases(db_cfg, verbosity=request.config.option.verbose) + settings.DATABASES["default"]["NAME"] = original_db_name + except Exception as exc: # noqa: BLE001 + request.node.warn( + pytest.PytestWarning( + f"Error when trying to teardown test databases: {exc!r}" + ) + ) + + +@pytest.fixture +def dbsession(sqlalchemy_db, engine): + """Sets up the SQLAlchemy dbsession.""" + connection = engine.connect() + + connection_transaction = connection.begin() + + # bind an individual Session to the connection + session = Session(bind=connection) + + # start the session in a SAVEPOINT... + session.begin_nested() + + # then each time that SAVEPOINT ends, reopen it + @event.listens_for(session, "after_transaction_end") + def restart_savepoint(session, transaction): + if transaction.nested and not transaction._parent.nested: + # ensure that state is expired the way + # session.commit() at the top level normally does + # (optional step) + session.expire_all() + + session.begin_nested() + + yield session + + session.close() + connection_transaction.rollback() + connection.close() + + +@pytest.fixture +def mock_configuration(mocker): + m = mocker.patch("shared.config._get_config_instance") + mock_config = ConfigHelper() + m.return_value = mock_config + our_config = { + "bitbucket": {"bot": {"username": "codecov-io"}}, + "services": { + "minio": { + "access_key_id": "codecov-default-key", + "bucket": "archive", + "hash_key": "88f572f4726e4971827415efa8867978", + "secret_access_key": "codecov-default-secret", + "verify_ssl": False, + "host": "minio", + "port": 9000, + }, + "smtp": { + "host": "mailhog", + "port": 1025, + "username": "username", + "password": "password", + }, + }, + "setup": { + "codecov_url": "https://codecov.io", + "encryption_secret": "zp^P9*i8aR3", + "telemetry": { + "endpoint_override": "abcde", + }, + }, + } + mock_config.set_params(our_config) + return mock_config + + +@pytest.fixture +def empty_configuration(mocker): + m = mocker.patch("shared.config._get_config_instance") + mock_config = ConfigHelper() + m.return_value = mock_config + return mock_config + + +@pytest.fixture +def codecov_vcr(request): + vcr_log = logging.getLogger("vcr") + vcr_log.setLevel(logging.ERROR) + + current_path = Path(request.node.fspath) + current_path_name = current_path.name.replace(".py", "") + cassete_path = current_path.parent / "cassetes" / current_path_name + if request.node.cls: + cls_name = request.node.cls.__name__ + cassete_path = cassete_path / cls_name + current_name = request.node.name + casset_file_path = str(cassete_path / f"{current_name}.yaml") + with vcr.use_cassette( + casset_file_path, + record_mode="once", + filter_headers=["authorization"], + match_on=["method", "scheme", "host", "port", "path"], + ) as cassete_maker: + yield cassete_maker + + +@pytest.fixture +def mock_redis(mocker): + m = mocker.patch("shared.helpers.redis._get_redis_instance_from_url") + redis_server = mocker.MagicMock() + m.return_value = redis_server + yield redis_server + + +@pytest.fixture +def mock_storage(mocker): + m = mocker.patch("shared.storage.get_appropriate_storage_service") + storage_server = MemoryStorageService({}) + m.return_value = storage_server + return storage_server + + +@pytest.fixture +def mock_archive_storage(mocker): + mocker.patch( + "shared.django_apps.core.models.should_write_data_to_storage_config_check", + return_value=True, + ) + storage_server = MemoryStorageService({}) + mocker.patch( + "shared.storage.get_appropriate_storage_service", return_value=storage_server + ) + return storage_server + + +@pytest.fixture +def mock_smtp(mocker): + m = mocker.patch("services.smtp.SMTPService") + smtp_server = mocker.MagicMock() + m.return_value = smtp_server + yield smtp_server + + +@pytest.fixture +def mock_repo_provider(mocker): + m = mocker.patch("services.repository._get_repo_provider_service_instance") + provider_instance = mocker.MagicMock( + GithubHandler, + data={}, + get_commit_diff=mock.AsyncMock(return_value={}), + get_distance_in_commits=mock.AsyncMock( + return_value={"behind_by": 0, "behind_by_commit": None} + ), + ) + m.return_value = provider_instance + yield provider_instance + + +@pytest.fixture +def mock_owner_provider(mocker): + provider_instance = mocker.MagicMock(GithubHandler) + + def side_effect(*args, **kwargs): + provider_instance.data = {**kwargs} + return provider_instance + + m = mocker.patch("services.owner._get_owner_provider_service_instance") + m.side_effect = side_effect + yield provider_instance + + +@pytest.fixture +def with_sql_functions(dbsession): + dbsession.execute( + """CREATE OR REPLACE FUNCTION array_append_unique(anyarray, anyelement) RETURNS anyarray + LANGUAGE sql IMMUTABLE + AS $_$ + select case when $2 is null + then $1 + else array_remove($1, $2) || array[$2] + end; + $_$;""" + ) + dbsession.execute( + """create or replace function try_to_auto_activate(int, int) returns boolean as $$ + update owners + set plan_activated_users = ( + case when coalesce(array_length(plan_activated_users, 1), 0) < plan_user_count -- we have credits + then array_append_unique(plan_activated_users, $2) -- add user + else plan_activated_users + end) + where ownerid=$1 + returning (plan_activated_users @> array[$2]); + $$ language sql volatile strict;""" + ) + dbsession.execute( + """create or replace function get_gitlab_root_group(int) returns jsonb as $$ + /* get root group by following parent_service_id to highest level */ + with recursive tree as ( + select o.service_id, + o.parent_service_id, + o.ownerid, + 1 as depth + from owners o + where o.ownerid = $1 + and o.service = 'gitlab' + and o.parent_service_id is not null + + union all + + select o.service_id, + o.parent_service_id, + o.ownerid, + depth + 1 as depth + from tree t + join owners o + on o.service_id = t.parent_service_id + /* avoid infinite loop in case of cycling (2 > 5 > 3 > 2 > 5...) up to Gitlab max subgroup depth of 20 */ + where depth <= 20 + ), data as ( + select t.ownerid, + t.service_id + from tree t + where t.parent_service_id is null + ) + select to_jsonb(data) from data limit 1; + $$ language sql stable strict;""" + ) + dbsession.flush() + + +# We don't want any tests submitting checkpoint logs to Sentry for real +@pytest.fixture(autouse=True) +def mock_checkpoint_submit(mocker, request): + # We mock sentry differently in the tests for CheckpointLogger + if request.node.get_closest_marker("real_checkpoint_logger"): + return + + def mock_submit_fn(metric, start, end, data={}): + pass + + mock_submit = mocker.Mock() + mock_submit.side_effect = mock_submit_fn + + return mocker.patch( + "helpers.checkpoint_logger.BaseFlow.submit_subflow", mock_submit + ) + + +@pytest.fixture(autouse=True) +def mock_feature(mocker, request): + if request.node.get_closest_marker("real_feature"): + return + + from shared.rollouts import Feature + + def check_value(self, identifier, default=False): + return default + + return mocker.patch.object(Feature, "check_value", check_value) diff --git a/apps/worker/database/__init__.py b/apps/worker/database/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/database/base.py b/apps/worker/database/base.py new file mode 100644 index 0000000000..a9aa736bb5 --- /dev/null +++ b/apps/worker/database/base.py @@ -0,0 +1,44 @@ +import uuid + +from sqlalchemy import Column, types +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import Session + +from helpers.clock import get_utc_now + +Base = declarative_base() + + +class CodecovBaseModel(Base): + __abstract__ = True + + def get_db_session(self): + return Session.object_session(self) + + +class MixinBaseClass(object): + id_ = Column("id", types.BigInteger, primary_key=True) + external_id = Column( + UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False + ) + created_at = Column(types.DateTime(timezone=True), default=get_utc_now) + updated_at = Column( + types.DateTime(timezone=True), onupdate=get_utc_now, default=get_utc_now + ) + + @property + def id(self): + return self.id_ + + +class MixinBaseClassNoExternalID(object): + id_ = Column("id", types.BigInteger, primary_key=True) + created_at = Column(types.DateTime(timezone=True), default=get_utc_now) + updated_at = Column( + types.DateTime(timezone=True), onupdate=get_utc_now, default=get_utc_now + ) + + @property + def id(self): + return self.id_ diff --git a/apps/worker/database/engine.py b/apps/worker/database/engine.py new file mode 100644 index 0000000000..69a6cb8400 --- /dev/null +++ b/apps/worker/database/engine.py @@ -0,0 +1,92 @@ +import dataclasses +import json +from decimal import Decimal + +from shared.config import get_config +from shared.timeseries.helpers import is_timeseries_enabled +from shared.utils.ReportEncoder import ReportEncoder +from sqlalchemy import create_engine +from sqlalchemy.orm import Session, scoped_session, sessionmaker + +import database.events # noqa: F401 +from database.models.timeseries import TimeseriesBaseModel + +from .base import Base + + +def create_all(engine): + Base.metadata.create_all(engine) + + +class DatabaseEncoder(ReportEncoder): + def default(self, obj): + if dataclasses.is_dataclass(obj): + return dataclasses.astuple(obj) + if isinstance(obj, Decimal): + return str(obj) + return super().default(obj) + + +def json_dumps(d): + return json.dumps(d, cls=DatabaseEncoder) + + +class SessionFactory: + def __init__(self, database_url, timeseries_database_url=None): + self.database_url = database_url + self.timeseries_database_url = timeseries_database_url + self.main_engine = None + self.timeseries_engine = None + + def create_session(self): + self.main_engine = create_engine( + self.database_url, + json_serializer=json_dumps, + ) + + if is_timeseries_enabled(): + self.timeseries_engine = create_engine( + self.timeseries_database_url, + json_serializer=json_dumps, + ) + + main_engine = self.main_engine + timeseries_engine = self.timeseries_engine + + class RoutingSession(Session): + def get_bind(self, mapper=None, clause=None): + if mapper is not None and issubclass( + mapper.class_, TimeseriesBaseModel + ): + return timeseries_engine + if ( + clause is not None + and hasattr(clause, "table") + and clause.table.name.startswith("timeseries_") + ): + return timeseries_engine + return main_engine + + session_factory = sessionmaker(class_=RoutingSession) + else: + session_factory = sessionmaker(bind=self.main_engine) + + return scoped_session(session_factory) + + +session_factory = SessionFactory( + database_url=get_config( + "services", + "database_url", + default="postgres://postgres:@postgres:5432/postgres", + ), + timeseries_database_url=get_config( + "services", + "timeseries_database_url", + default="postgres://postgres:@timescale:5432/postgres", + ), +) + +session = session_factory.create_session() + +get_db_session = session diff --git a/apps/worker/database/enums.py b/apps/worker/database/enums.py new file mode 100644 index 0000000000..42b0b92601 --- /dev/null +++ b/apps/worker/database/enums.py @@ -0,0 +1,80 @@ +from enum import Enum + + +class Decoration(Enum): + standard = "standard" + upgrade = "upgrade" + upload_limit = "upload_limit" + passing_empty_upload = "passing_empty_upload" + failing_empty_upload = "failing_empty_upload" + processing_upload = "processing_upload" + + +class Notification(Enum): + comment = "comment" + status_changes = "status_changes" + status_patch = "status_patch" + status_project = "status_project" + checks_changes = "checks_changes" + checks_patch = "checks_patch" + checks_project = "checks_project" + slack = "slack" + webhook = "webhook" + gitter = "gitter" + irc = "irc" + hipchat = "hipchat" + codecov_slack_app = "codecov_slack_app" + + +notification_type_status_or_checks = { + Notification.status_changes, + Notification.status_patch, + Notification.status_project, + Notification.checks_changes, + Notification.checks_patch, + Notification.checks_project, +} + + +class NotificationState(Enum): + pending = "pending" + success = "success" + error = "error" + + +class CompareCommitState(Enum): + pending = "pending" + processed = "processed" + error = "error" + + +class CompareCommitError(Enum): + missing_base_report = "missing_base_report" + missing_head_report = "missing_head_report" + provider_client_error = "provider_client_error" + + +class CommitErrorTypes(Enum): + INVALID_YAML = "invalid_yaml" + YAML_CLIENT_ERROR = "yaml_client_error" + YAML_UNKNOWN_ERROR = "yaml_unknown_error" + REPO_BOT_INVALID = "repo_bot_invalid" + + +class TrialStatus(Enum): + NOT_STARTED = "not_started" + ONGOING = "ongoing" + EXPIRED = "expired" + CANNOT_TRIAL = "cannot_trial" + + +class ReportType(Enum): + COVERAGE = "coverage" + TEST_RESULTS = "test_results" + BUNDLE_ANALYSIS = "bundle_analysis" + + +class FlakeSymptomType(Enum): + FAILED_IN_DEFAULT_BRANCH = "failed_in_default_branch" + CONSECUTIVE_DIFF_OUTCOMES = "consecutive_diff_outcomes" + UNRELATED_MATCHING_FAILURES = "unrelated_matching_failures" diff --git a/apps/worker/database/events.py b/apps/worker/database/events.py new file mode 100644 index 0000000000..80ce322031 --- /dev/null +++ b/apps/worker/database/events.py @@ -0,0 +1,85 @@ +import json +import logging + +from google.cloud import pubsub_v1 +from shared.config import get_config +from sqlalchemy import event, inspect + +from database.models.core import Repository +from helpers.environment import is_enterprise + +_pubsub_publisher = None + +log = logging.getLogger(__name__) + + +def _is_shelter_enabled(): + return get_config( + "setup", "shelter", "enabled", default=False if is_enterprise() else True + ) + + +def _get_pubsub_publisher(): + global _pubsub_publisher + if not _pubsub_publisher: + _pubsub_publisher = pubsub_v1.PublisherClient() + return _pubsub_publisher + + +def _sync_repo(repository: Repository): + log.info(f"Signal triggered for repository {repository.repoid}") + try: + pubsub_project_id = get_config("setup", "shelter", "pubsub_project_id") + pubsub_topic_id = get_config("setup", "shelter", "sync_repo_topic_id") + + if pubsub_project_id and pubsub_topic_id: + publisher = _get_pubsub_publisher() + topic_path = publisher.topic_path(pubsub_project_id, pubsub_topic_id) + publisher.publish( + topic_path, + json.dumps( + { + "type": "repo", + "sync": "one", + "id": repository.repoid, + } + ).encode("utf-8"), + ) + log.info(f"Message published for repository {repository.repoid}") + except Exception as e: + log.warning(f"Failed to publish message for repo {repository.repoid}: {e}") + + +@event.listens_for(Repository, "after_insert") +def after_insert_repo(mapper, connection, target: Repository): + if not _is_shelter_enabled(): + log.debug("Shelter is not enabled, skipping after_insert signal") + return + + # Send to shelter service + log.info("After insert signal", extra=dict(repoid=target.repoid)) + _sync_repo(target) + + +@event.listens_for(Repository, "after_update") +def after_update_repo(mapper, connection, target: Repository): + if not _is_shelter_enabled(): + log.debug("Shelter is not enabled, skipping after_update signal") + return + + # Send to shelter service + state = inspect(target) + + for attr in state.attrs: + if attr.key in ["name", "upload_token", "ownerid", "private"]: + history = attr.history + # Detects if there are changes and if said changes are different. + # has_changes() is True when you update the an entry with the same value, + # so we must ensure those values are different to trigger the signal + if history.has_changes() and history.deleted and history.added: + old_value = history.deleted[0] + new_value = history.added[0] + if old_value != new_value: + log.info("After update signal", extra=dict(repoid=target.repoid)) + _sync_repo(target) + break diff --git a/apps/worker/database/models/__init__.py b/apps/worker/database/models/__init__.py new file mode 100644 index 0000000000..ea11f1a3d3 --- /dev/null +++ b/apps/worker/database/models/__init__.py @@ -0,0 +1,5 @@ +from database.models.core import * +from database.models.labelanalysis import * +from database.models.reports import * +from database.models.staticanalysis import * +from database.models.timeseries import * diff --git a/apps/worker/database/models/core.py b/apps/worker/database/models/core.py new file mode 100644 index 0000000000..4c86664b6e --- /dev/null +++ b/apps/worker/database/models/core.py @@ -0,0 +1,673 @@ +import random +import string +import uuid +from datetime import datetime +from functools import cached_property +from typing import Optional + +from shared.plan.constants import DEFAULT_FREE_PLAN +from sqlalchemy import Column, ForeignKey, Index, UniqueConstraint, types +from sqlalchemy.dialects import postgresql +from sqlalchemy.orm import Session, backref, relationship, validates +from sqlalchemy.schema import FetchedValue + +import database.models +from database.base import CodecovBaseModel, MixinBaseClass, MixinBaseClassNoExternalID +from database.enums import Decoration, Notification, NotificationState, ReportType +from database.utils import ArchiveField +from helpers.config import should_write_data_to_storage_config_check + + +class AccountsUsers(CodecovBaseModel, MixinBaseClassNoExternalID): + __tablename__ = "codecov_auth_accountsusers" + user_id = Column("user_id", ForeignKey("users.id"), primary_key=True) + account_id = Column( + "account_id", ForeignKey("codecov_auth_account.id"), primary_key=True + ) + + +class User(CodecovBaseModel, MixinBaseClass): + __tablename__ = "users" + id_ = Column("id", types.BigInteger, primary_key=True) + + # This field is case-insensitive but we don't have a way to represent that + # here. Options to address: + # - Upgrade sqlalchemy and use `postgresql.CITEXT(100)` as the field type + # - Add a case-insensitive collation to postgres[1] and use it here + in `codecov-api` + # + # [1] https://www.postgresql.org/docs/current/collation.html + email = Column(types.String(100), nullable=True) + + name = Column(types.String(100), nullable=True) + is_staff = Column(types.Boolean, default=False) + is_superuser = Column(types.Boolean, default=False) + external_id = Column(postgresql.UUID(as_uuid=True), unique=True, default=uuid.uuid4) + email_opt_in = Column(types.Boolean, default=False) + + accounts = relationship( + "Account", secondary="codecov_auth_accountsusers", back_populates="users" + ) + + @validates("external_id") + def validate_external_id(self, key, value): + if self.external_id: + raise ValueError("`external_id` cannot be modified") + return value + + +class Account(CodecovBaseModel, MixinBaseClassNoExternalID): + __tablename__ = "codecov_auth_account" + name = Column(types.String(100), nullable=False, unique=True) + is_active = Column(types.Boolean, nullable=False, default=True) + plan = Column( + types.String(50), + nullable=False, + default=DEFAULT_FREE_PLAN, + ) + plan_seat_count = Column(types.SmallInteger, nullable=False, default=1) + free_seat_count = Column(types.SmallInteger, nullable=False, default=0) + plan_auto_activate = Column(types.Boolean, nullable=False, default=True) + is_delinquent = Column(types.Boolean, nullable=False, default=False) + + users = relationship( + "User", secondary="codecov_auth_accountsusers", back_populates="accounts" + ) + organizations = relationship("Owner", back_populates="account") + + +class Owner(CodecovBaseModel): + __tablename__ = "owners" + ownerid = Column(types.Integer, primary_key=True) + service = Column(types.String(100), nullable=False, server_default=FetchedValue()) + service_id = Column(types.Text, nullable=False, server_default=FetchedValue()) + + name = Column(types.String(100), server_default=FetchedValue()) + email = Column(types.String(300), server_default=FetchedValue()) + username = Column(types.String(100), server_default=FetchedValue()) + plan_activated_users = Column( + postgresql.ARRAY(types.Integer), server_default=FetchedValue() + ) + + createstamp = Column(types.DateTime, server_default=FetchedValue()) + admins = Column(postgresql.ARRAY(types.Integer), server_default=FetchedValue()) + permission = Column(postgresql.ARRAY(types.Integer), server_default=FetchedValue()) + organizations = Column( + postgresql.ARRAY(types.Integer), server_default=FetchedValue() + ) + free = Column( + types.Integer, nullable=False, default=0, server_default=FetchedValue() + ) + + account_id = Column(types.BigInteger, ForeignKey("codecov_auth_account.id")) + account = relationship("Account", back_populates="organizations") + + # DEPRECATED - Prefer GithubAppInstallation + integration_id = Column(types.Integer, server_default=FetchedValue()) + yaml = Column(postgresql.JSON, server_default=FetchedValue()) + oauth_token = Column(types.Text, server_default=FetchedValue()) + avatar_url = Column(types.Text, server_default=FetchedValue()) + updatestamp = Column(types.DateTime, server_default=FetchedValue()) + parent_service_id = Column(types.Text, server_default=FetchedValue()) + root_parent_service_id = Column(types.Text, nullable=True) + plan_provider = Column(types.Text, server_default=FetchedValue()) + trial_start_date = Column(types.DateTime, server_default=FetchedValue()) + trial_end_date = Column(types.DateTime, server_default=FetchedValue()) + trial_status = Column(types.Text, server_default=FetchedValue()) + trial_fired_by = Column(types.Integer, server_default=FetchedValue()) + plan = Column(types.Text, default=DEFAULT_FREE_PLAN) + plan_user_count = Column(types.SmallInteger, server_default=FetchedValue()) + pretrial_users_count = Column(types.SmallInteger, server_default=FetchedValue()) + plan_auto_activate = Column(types.Boolean, server_default=FetchedValue()) + stripe_customer_id = Column(types.Text, server_default=FetchedValue()) + stripe_subscription_id = Column(types.Text, server_default=FetchedValue()) + onboarding_completed = Column(types.Boolean, default=False) + upload_token_required_for_public_repos = Column( + types.Boolean, default=False, nullable=False + ) + + bot_id = Column( + "bot", + types.Integer, + ForeignKey("owners.ownerid"), + server_default=FetchedValue(), + ) + + bot = relationship("Owner", remote_side=[ownerid]) + repositories = relationship( + "Repository", + back_populates="owner", + foreign_keys="Repository.ownerid", + cascade="all, delete", + passive_deletes=True, + ) + github_app_installations = relationship( + "GithubAppInstallation", + back_populates="owner", + foreign_keys="GithubAppInstallation.ownerid", + cascade="all, delete", + passive_deletes=True, + ) + + # TODO: Create association between `User` and `Owner` mirroring `codecov-api` + # https://github.com/codecov/codecov-api/blob/204f7fd7e37896efe0259e4bc91aad20601087e0/codecov_auth/models.py#L196-L202 + + __table_args__ = ( + Index("owner_service_ids", "service", "service_id", unique=True), + Index("owner_service_username", "service", "username", unique=True), + ) + + @property + def slug(self: "Owner") -> str: + return self.username + + @property + def root_organization(self: "Owner") -> Optional["Owner"]: + """ + Find the root organization of Gitlab OwnerOrg, by using the root_parent_service_id + if it exists, otherwise iterating through the parents and cache it in root_parent_service_id + """ + db_session = self.get_db_session() + if self.root_parent_service_id: + return self._get_owner_by_service_id( + db_session, self.root_parent_service_id + ) + + root = None + if self.service == "gitlab" and self.parent_service_id: + root = self + while root.parent_service_id is not None: + root = self._get_owner_by_service_id(db_session, root.parent_service_id) + self.root_parent_service_id = root.service_id + db_session.commit() + return root + + def _get_owner_by_service_id( + self: "Owner", db_session: Session, service_id: str + ) -> "Owner": + """ + Helper method to fetch an Owner by service_id. + """ + return ( + db_session.query(Owner) + .filter_by(service_id=service_id, service=self.service) + .one() + ) + + def __repr__(self: "Owner") -> str: + return f"Owner<{self.ownerid}@service<{self.service}>>" + + +class Repository(CodecovBaseModel): + __tablename__ = "repos" + + repoid = Column(types.Integer, primary_key=True) + ownerid = Column(types.Integer, ForeignKey("owners.ownerid")) + bot_id = Column("bot", types.Integer, ForeignKey("owners.ownerid")) + service_id = Column(types.Text) + name = Column(types.Text) + private = Column(types.Boolean) + updatestamp = Column(types.DateTime) + yaml = Column(postgresql.JSON) + deleted = Column(types.Boolean, nullable=False, default=False) + branch = Column(types.Text, default="main") + image_token = Column( + types.Text, + default=lambda: "".join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(10) + ), + ) + language = Column(types.Text) + languages = Column(postgresql.ARRAY(types.String), nullable=True, default=[]) + languages_last_updated = Column(types.DateTime, server_default=FetchedValue()) + hookid = Column(types.Text) + webhook_secret = Column(types.Text) + activated = Column(types.Boolean, default=False) + bundle_analysis_enabled = Column(types.Boolean, default=False) + test_analytics_enabled = Column(types.Boolean, default=False) + upload_token = Column(postgresql.UUID, server_default=FetchedValue()) + + # DEPRECATED - prefer GithubAppInstallation.is_repo_covered_by_integration + using_integration = Column(types.Boolean) + + owner = relationship(Owner, foreign_keys=[ownerid], back_populates="repositories") + bot = relationship(Owner, foreign_keys=[bot_id]) + + __table_args__ = ( + Index("repos_slug", "ownerid", "name", unique=True), + Index("repos_service_ids", "ownerid", "service_id", unique=True), + ) + + @property + def slug(self): + return f"{self.owner.slug}/{self.name}" + + @property + def service(self): + return self.owner.service + + def __repr__(self): + return f"Repo<{self.repoid}>" + + +GITHUB_APP_INSTALLATION_DEFAULT_NAME = "codecov_app_installation" + + +class GithubAppInstallation(CodecovBaseModel, MixinBaseClass): + __tablename__ = "codecov_auth_githubappinstallation" + + # replacement for owner.integration_id + # installation id GitHub sends us in the installation-related webhook events + installation_id = Column(types.Integer, server_default=FetchedValue()) + name = Column(types.Text, default=GITHUB_APP_INSTALLATION_DEFAULT_NAME) + # if null, all repos are covered by this installation + # otherwise, it's a list of repo.id values + repository_service_ids = Column( + postgresql.ARRAY(types.Text), server_default=FetchedValue() + ) + # Data required to get a token from gh + app_id = Column(types.Text, server_default=FetchedValue()) + pem_path = Column(types.Text, server_default=FetchedValue()) + + ownerid = Column("owner_id", types.Integer, ForeignKey("owners.ownerid")) + owner = relationship( + Owner, foreign_keys=[ownerid], back_populates="github_app_installations" + ) + + is_suspended = Column(types.Boolean, default=False) + + def repository_queryset(self, dbsession: Session): + """Returns a query set of repositories covered by this installation""" + if self.repository_service_ids is None: + # All repos covered + return dbsession.query(Repository).filter( + Repository.ownerid == self.ownerid + ) + # Some repos covered + return dbsession.query(Repository).filter( + Repository.service_id.in_(self.repository_service_ids), + Repository.ownerid == self.ownerid, + ) + + def covers_all_repos(self) -> bool: + return self.repository_service_ids is None + + def is_repo_covered_by_integration(self, repo: Repository) -> bool: + if self.covers_all_repos(): + return repo.ownerid == self.ownerid + return repo.service_id in self.repository_service_ids + + def is_configured(self) -> bool: + """Returns whether this installation is properly configured and can be used""" + if self.name == GITHUB_APP_INSTALLATION_DEFAULT_NAME: + # The default app is configured in the installation YAML + return True + return self.app_id is not None and self.pem_path is not None + + +class OwnerInstallationNameToUseForTask(CodecovBaseModel, MixinBaseClass): + __tablename__ = "codecov_auth_ownerinstallationnametousefortask" + + ownerid = Column("owner_id", types.Integer, ForeignKey("owners.ownerid")) + owner = relationship( + Owner, + foreign_keys=[ownerid], + ) + installation_name = Column(types.Text, server_default=FetchedValue()) + task_name = Column(types.Text, server_default=FetchedValue()) + + __table_args__ = ( + UniqueConstraint( + "owner_id", + "task_name", + name="single_task_name_per_owner", + ), + ) + + +class Commit(CodecovBaseModel): + __tablename__ = "commits" + + id_ = Column("id", types.BigInteger, primary_key=True) + author_id = Column("author", types.Integer, ForeignKey("owners.ownerid")) + branch = Column(types.Text) + ci_passed = Column(types.Boolean) + commitid = Column(types.Text) + deleted = Column(types.Boolean) + message = Column(types.Text) + notified = Column(types.Boolean) + merged = Column(types.Boolean) + parent_commit_id = Column("parent", types.Text) + pullid = Column(types.Integer) + repoid = Column(types.Integer, ForeignKey("repos.repoid")) + state = Column(types.String(256)) + timestamp = Column(types.DateTime, nullable=False) + updatestamp = Column(types.DateTime, nullable=True) + totals = Column(postgresql.JSON) + + author = relationship(Owner) + repository = relationship(Repository, backref=backref("commits", cascade="delete")) + notifications = relationship( + "CommitNotification", + backref=backref("commits"), + cascade="all, delete", + passive_deletes=True, + ) + reports_list = relationship( + "CommitReport", + back_populates="commit", + cascade="all, delete", + passive_deletes=True, + ) + + def __repr__(self): + return f"Commit<{self.commitid}@repo<{self.repoid}>>" + + def get_parent_commit(self): + db_session = self.get_db_session() + return ( + db_session.query(Commit) + .filter_by(repoid=self.repoid, commitid=self.parent_commit_id) + .first() + ) + + @property + def report(self): + db_session = self.get_db_session() + CommitReport = database.models.reports.CommitReport + return ( + db_session.query(CommitReport) + .filter( + (CommitReport.commit_id == self.id_) + & (CommitReport.code == None) # noqa: E711 + & ( + (CommitReport.report_type == None) # noqa: E711 + | (CommitReport.report_type == ReportType.COVERAGE.value) + ) + ) + .first() + ) + + def commit_report(self, report_type: ReportType): + db_session = self.get_db_session() + CommitReport = database.models.reports.CommitReport + if report_type == ReportType.COVERAGE: + return self.report + else: + return ( + db_session.query(CommitReport) + .filter( + (CommitReport.commit_id == self.id_) + & (CommitReport.code == None) # noqa: E711 + & (CommitReport.report_type == report_type.value) + ) + .first() + ) + + @property + def id(self): + return self.id_ + + @property + def external_id(self): + return self.commitid + + def get_repository(self): + return self.repository + + def get_commitid(self): + return self.commitid + + def should_write_to_storage(self: object) -> bool: + if self.repository is None or self.repository.owner is None: + return False + is_codecov_repo = self.repository.owner.username == "codecov" + return should_write_data_to_storage_config_check( + "commit_report", is_codecov_repo, self.repository.repoid + ) + + # Use custom JSON to properly serialize custom data classes on reports + _report_json = Column("report", postgresql.JSON) + _report_json_storage_path = Column("report_storage_path", types.Text, nullable=True) + report_json = ArchiveField( + should_write_to_storage_fn=should_write_to_storage, + default_value_class=dict, + ) + + +class Branch(CodecovBaseModel): + __tablename__ = "branches" + + repoid = Column(types.Integer, ForeignKey("repos.repoid"), primary_key=True) + updatestamp = Column(types.DateTime, default=datetime.now) + branch = Column(types.Text, nullable=False, primary_key=True) + base = Column(types.Text) + head = Column(types.Text, nullable=False) + authors = Column(postgresql.ARRAY(types.Integer)) + + repository = relationship(Repository, backref=backref("branches", cascade="delete")) + + __table_args__ = (Index("branches_repoid_branch", "repoid", "branch", unique=True),) + + def __repr__(self): + return f"Branch<{self.branch}@repo<{self.repoid}>>" + + +class LoginSession(CodecovBaseModel): + __tablename__ = "sessions" + + sessionid = Column(types.Integer, primary_key=True) + token = Column(postgresql.UUID(as_uuid=True)) + name = Column(types.Text) + ownerid = Column(types.Integer, ForeignKey("owners.ownerid")) + session_type = Column("type", types.Text) + lastseen = Column(types.DateTime(timezone=True)) + useragent = Column(types.Text) + ip = Column(types.Text) + + +class Pull(CodecovBaseModel): + __tablename__ = "pulls" + + id_ = Column("id", types.BigInteger, primary_key=True) + repoid = Column(types.Integer, ForeignKey("repos.repoid")) + pullid = Column(types.Integer, nullable=False) + issueid = Column(types.Integer) + updatestamp = Column( + types.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow + ) + state = Column(types.Text, nullable=False, default="open") + title = Column(types.Text) + base = Column(types.Text) + user_provided_base_sha = Column(types.Text) + compared_to = Column(types.Text) + head = Column(types.Text) + commentid = Column(types.Text) + bundle_analysis_commentid = Column(types.Text) + diff = Column(postgresql.JSON) + author_id = Column("author", types.Integer, ForeignKey("owners.ownerid")) + behind_by = Column(types.Integer) + behind_by_commit = Column(types.Text) + + author = relationship(Owner) + repository = relationship( + Repository, backref=backref("pulls", cascade="delete", lazy="dynamic") + ) + + def should_write_to_storage(self: object) -> bool: + if self.repository is None or self.repository.owner is None: + return False + is_codecov_repo = self.repository.owner.username == "codecov" + return should_write_data_to_storage_config_check( + master_switch_key="pull_flare", + is_codecov_repo=is_codecov_repo, + repoid=self.repository.repoid, + ) + + _flare = Column("flare", postgresql.JSON) + _flare_storage_path = Column("flare_storage_path", types.Text, nullable=True) + flare = ArchiveField( + should_write_to_storage_fn=should_write_to_storage, default_value_class=dict + ) + + __table_args__ = (Index("pulls_repoid_pullid", "repoid", "pullid", unique=True),) + + def __repr__(self): + return f"Pull<{self.pullid}@repo<{self.repoid}>>" + + def get_head_commit(self): + return ( + self.get_db_session() + .query(Commit) + .filter_by(repoid=self.repoid, commitid=self.head) + .first() + ) + + def get_comparedto_commit(self): + return ( + self.get_db_session() + .query(Commit) + .filter_by(repoid=self.repoid, commitid=self.compared_to) + .first() + ) + + def get_head_commit_notifications(self): + head_commit = self.get_head_commit() + if head_commit: + return ( + self.get_db_session() + .query(CommitNotification) + .filter_by(commit_id=head_commit.id_) + .all() + ) + return [] + + def get_repository(self): + return self.repository + + def get_commitid(self): + return None + + @property + def external_id(self): + return self.pullid + + @property + def id(self): + return self.id_ + + @cached_property + def is_first_coverage_pull(self): + """ + This method determines if this is the first PR that has coverage. + + It fetches the first record that has a commentid, which implies having a coverage comment. + If it doesn't exist, this is the first record ever, hence returning true. Else, we evaluate if + the record found is the same as the current one. + """ + + first_pull_with_coverage = ( + self.repository.pulls.with_entities( + Pull.id_, Pull.commentid, Pull.bundle_analysis_commentid + ) + .filter(Pull.commentid != None) # noqa: E711 + .order_by(Pull.id_) + .first() + ) + + if first_pull_with_coverage: + return first_pull_with_coverage.id_ == self.id_ + return True + + +class CommitNotification(CodecovBaseModel): + __tablename__ = "commit_notifications" + + id_ = Column("id", types.BigInteger, primary_key=True) + commit_id = Column(types.BigInteger, ForeignKey("commits.id")) + gh_app_id = Column( + types.BigInteger, + ForeignKey("codecov_auth_githubappinstallation.id"), + nullable=True, + ) + notification_type = Column( + postgresql.ENUM(Notification, values_callable=lambda x: [e.value for e in x]), + nullable=False, + ) + decoration_type = Column( + postgresql.ENUM(Decoration, values_callable=lambda x: [e.value for e in x]) + ) + created_at = Column(types.DateTime, default=datetime.now()) + updated_at = Column(types.DateTime, default=datetime.now(), onupdate=datetime.now()) + state = Column( + postgresql.ENUM( + NotificationState, values_callable=lambda x: [e.value for e in x] + ) + ) + + commit = relationship(Commit, foreign_keys=[commit_id]) + + __table_args__ = ( + Index("notifications_commit_id", "commit_id"), + UniqueConstraint( + "commit_id", + "notification_type", + name="commit_notifications_commit_id_notification_type", + ), + ) + + def __repr__(self): + return f"Notification<{self.notification_type}@commit<{self.commit_id}>>" + + +class CompareCommit(MixinBaseClass, CodecovBaseModel): + __tablename__ = "compare_commitcomparison" + + base_commit_id = Column(types.BigInteger, ForeignKey("commits.id")) + base_commit = relationship(Commit, foreign_keys=[base_commit_id]) + compare_commit_id = Column(types.BigInteger, ForeignKey("commits.id")) + compare_commit = relationship(Commit, foreign_keys=[compare_commit_id]) + report_storage_path = Column(types.String(150)) + patch_totals = Column(postgresql.JSON) + state = Column(types.Text) + error = Column(types.Text) + + __table_args__ = ( + Index("compare_commitcomparison_base_commit_id_cf53c1d9", "base_commit_id"), + Index( + "compare_commitcomparison_compare_commit_id_3ea19610", "compare_commit_id" + ), + UniqueConstraint( + "base_commit_id", + "compare_commit_id", + name="unique_comparison_between_commit", + ), + ) + + def __repr__(self): + return f"CompareCommit<{self.base_commit_id}...{self.compare_commit_id}>" + + +class CommitError(CodecovBaseModel, MixinBaseClass): + __tablename__ = "core_commiterror" + + commit_id = Column(types.BigInteger, ForeignKey("commits.id")) + commit = relationship(Commit, foreign_keys=[commit_id], backref="errors") + error_code = Column(types.String(100), nullable=True) + error_params = Column(postgresql.JSON, default=dict) + + +class OrganizationLevelToken(MixinBaseClass, CodecovBaseModel): + __tablename__ = "codecov_auth_organizationleveltoken" + + ownerid = Column(types.Integer, ForeignKey("owners.ownerid")) + owner = relationship(Owner, foreign_keys=[ownerid]) + token = Column(postgresql.UUID) + valid_until = Column(types.DateTime) + token_type = Column(types.String) + + +class Constants(CodecovBaseModel): + __tablename__ = "constants" + + key = Column(types.String, primary_key=True) + value = Column(types.String) diff --git a/apps/worker/database/models/labelanalysis.py b/apps/worker/database/models/labelanalysis.py new file mode 100644 index 0000000000..d0382028f1 --- /dev/null +++ b/apps/worker/database/models/labelanalysis.py @@ -0,0 +1,40 @@ +from enum import Enum + +from sqlalchemy import Column, ForeignKey, types +from sqlalchemy.dialects import postgresql +from sqlalchemy.orm import relationship + +from database.base import CodecovBaseModel, MixinBaseClass + + +class LabelAnalysisProcessingErrorCode(Enum): + NOT_FOUND = "not found" + FAILED = "failed" + MISSING_DATA = "missing data" + + +class LabelAnalysisRequest(CodecovBaseModel, MixinBaseClass): + __tablename__ = "labelanalysis_labelanalysisrequest" + base_commit_id = Column(types.BigInteger, ForeignKey("commits.id"), nullable=False) + head_commit_id = Column(types.BigInteger, ForeignKey("commits.id"), nullable=False) + requested_labels = Column(postgresql.ARRAY(types.Text), nullable=True) + state_id = Column(types.Integer, nullable=False) + result = Column(postgresql.JSON, nullable=True) + # relationships + base_commit = relationship("Commit", foreign_keys=[base_commit_id]) + head_commit = relationship("Commit", foreign_keys=[head_commit_id]) + + +class LabelAnalysisProcessingError(CodecovBaseModel, MixinBaseClass): + __tablename__ = "labelanalysis_labelanalysisprocessingerror" + label_analysis_request = relationship(LabelAnalysisRequest, backref="errors") + label_analysis_request_id = Column( + "label_analysis_request_id", + types.BigInteger, + ForeignKey("labelanalysis_labelanalysisrequest.id"), + ) + error_code = Column(types.String(100), nullable=False) + error_params = Column(postgresql.JSON, nullable=True) + + def to_representation(self): + return dict(error_code=self.error_code, error_params=self.error_params) diff --git a/apps/worker/database/models/reports.py b/apps/worker/database/models/reports.py new file mode 100644 index 0000000000..0a13a2efb7 --- /dev/null +++ b/apps/worker/database/models/reports.py @@ -0,0 +1,353 @@ +import logging +import uuid +from decimal import Decimal +from functools import cached_property + +from sqlalchemy import Column, ForeignKey, Table, UniqueConstraint, types +from sqlalchemy.dialects import postgresql +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import backref, relationship + +from database.base import CodecovBaseModel, MixinBaseClass, MixinBaseClassNoExternalID +from database.models.core import Commit, CompareCommit, Repository +from helpers.clock import get_utc_now +from helpers.number import precise_round + +log = logging.getLogger(__name__) + + +class RepositoryFlag(CodecovBaseModel, MixinBaseClass): + __tablename__ = "reports_repositoryflag" + repository_id = Column(types.Integer, ForeignKey("repos.repoid")) + repository = relationship(Repository, backref=backref("flags")) + flag_name = Column(types.String(1024), nullable=False) + deleted = Column(types.Boolean, nullable=True) + + +class CommitReport(CodecovBaseModel, MixinBaseClass): + __tablename__ = "reports_commitreport" + commit_id = Column(types.BigInteger, ForeignKey("commits.id")) + code = Column(types.String(100), nullable=True) + report_type = Column(types.String(100), nullable=True) + commit: Commit = relationship( + "Commit", + foreign_keys=[commit_id], + back_populates="reports_list", + cascade="all, delete", + ) + totals = relationship( + "ReportLevelTotals", + back_populates="report", + uselist=False, + cascade="all, delete", + passive_deletes=True, + ) + uploads = relationship( + "Upload", back_populates="report", cascade="all, delete", passive_deletes=True + ) + patch_results = relationship( + "ReportResults", + uselist=False, + back_populates="report", + cascade="all, delete", + passive_deletes=True, + ) + test_result_totals = relationship( + "TestResultReportTotals", + back_populates="report", + uselist=False, + cascade="all, delete", + passive_deletes=True, + ) + + +uploadflagmembership = Table( + "reports_uploadflagmembership", + CodecovBaseModel.metadata, + Column("upload_id", types.BigInteger, ForeignKey("reports_upload.id")), + Column("flag_id", types.BigInteger, ForeignKey("reports_repositoryflag.id")), +) + + +class ReportResults(MixinBaseClass, CodecovBaseModel): + __tablename__ = "reports_reportresults" + state = Column(types.Text) + completed_at = Column(types.DateTime(timezone=True), nullable=True) + result = Column(postgresql.JSON) + report_id = Column(types.BigInteger, ForeignKey("reports_commitreport.id")) + report = relationship("CommitReport", foreign_keys=[report_id]) + + +class Upload(CodecovBaseModel, MixinBaseClass): + __tablename__ = "reports_upload" + build_code = Column(types.Text) + build_url = Column(types.Text) + env = Column(postgresql.JSON) + job_code = Column(types.Text) + name = Column(types.String(100)) + provider = Column(types.String(50)) + report_id = Column(types.BigInteger, ForeignKey("reports_commitreport.id")) + report: CommitReport = relationship( + "CommitReport", foreign_keys=[report_id], back_populates="uploads" + ) + state = Column(types.String(100), nullable=False) + storage_path = Column(types.Text, nullable=False) + order_number = Column(types.Integer) + flags = relationship(RepositoryFlag, secondary=uploadflagmembership) + totals = relationship( + "UploadLevelTotals", + back_populates="upload", + uselist=False, + cascade="all, delete", + passive_deletes=True, + ) + upload_extras = Column(postgresql.JSON, nullable=False) + upload_type = Column(types.String(100), nullable=False) + state_id = Column(types.Integer) + upload_type_id = Column(types.Integer) + + @cached_property + def flag_names(self) -> list[str]: + return [f.flag_name for f in self.flags] + + +class UploadError(CodecovBaseModel, MixinBaseClass): + __tablename__ = "reports_uploaderror" + report_upload = relationship(Upload, backref="errors") + upload_id = Column("upload_id", types.BigInteger, ForeignKey("reports_upload.id")) + error_code = Column(types.String(100), nullable=False) + error_params = Column(postgresql.JSON, default=dict) + + +class AbstractTotals(MixinBaseClass): + branches = Column(types.Integer) + coverage = Column(types.Numeric(precision=8, scale=5)) + hits = Column(types.Integer) + lines = Column(types.Integer) + methods = Column(types.Integer) + misses = Column(types.Integer) + partials = Column(types.Integer) + files = Column(types.Integer) + + def update_from_totals(self, totals, precision=2, rounding="down"): + self.branches = totals.branches + if totals.coverage is not None: + coverage: Decimal = Decimal(totals.coverage) + self.coverage = precise_round( + coverage, precision=precision, rounding=rounding + ) + # Temporary until the table starts accepting NULLs + else: + self.coverage = 0 + self.hits = totals.hits + self.lines = totals.lines + self.methods = totals.methods + self.misses = totals.misses + self.partials = totals.partials + self.files = totals.files + + class Meta: + abstract = True + + +class ReportLevelTotals(CodecovBaseModel, AbstractTotals): + __tablename__ = "reports_reportleveltotals" + report_id = Column(types.BigInteger, ForeignKey("reports_commitreport.id")) + report = relationship("CommitReport", foreign_keys=[report_id]) + + +class UploadLevelTotals(CodecovBaseModel, AbstractTotals): + __tablename__ = "reports_uploadleveltotals" + upload_id = Column("upload_id", types.BigInteger, ForeignKey("reports_upload.id")) + upload = relationship("Upload", foreign_keys=[upload_id]) + + +class CompareFlag(MixinBaseClass, CodecovBaseModel): + __tablename__ = "compare_flagcomparison" + + commit_comparison_id = Column( + types.BigInteger, ForeignKey("compare_commitcomparison.id") + ) + repositoryflag_id = Column( + types.BigInteger, ForeignKey("reports_repositoryflag.id") + ) + head_totals = Column(postgresql.JSON) + base_totals = Column(postgresql.JSON) + patch_totals = Column(postgresql.JSON) + + commit_comparison = relationship(CompareCommit, foreign_keys=[commit_comparison_id]) + repositoryflag = relationship(RepositoryFlag, foreign_keys=[repositoryflag_id]) + + +class CompareComponent(MixinBaseClass, CodecovBaseModel): + __tablename__ = "compare_componentcomparison" + + commit_comparison_id = Column( + types.BigInteger, ForeignKey("compare_commitcomparison.id") + ) + component_id = Column(types.String(100), nullable=False) + head_totals = Column(postgresql.JSON) + base_totals = Column(postgresql.JSON) + patch_totals = Column(postgresql.JSON) + + commit_comparison = relationship(CompareCommit, foreign_keys=[commit_comparison_id]) + + +class Test(CodecovBaseModel): + __tablename__ = "reports_test" + # the reason we aren't using the regular primary key + # in this case is because we want to be able to compute/predict + # the primary key of a Test object ourselves in the processor + # so we can easily do concurrent writes to the database + # this is a hash of the repoid, name, testsuite and env + id_ = Column("id", types.Text, primary_key=True) + external_id = Column( + UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False + ) + created_at = Column(types.DateTime(timezone=True), default=get_utc_now) + updated_at = Column( + types.DateTime(timezone=True), onupdate=get_utc_now, default=get_utc_now + ) + + @property + def id(self): + return self.id_ + + repoid = Column(types.Integer, ForeignKey("repos.repoid")) + repository = relationship("Repository", backref=backref("tests")) + name = Column(types.String(256), nullable=False) + testsuite = Column(types.String(256), nullable=False) + # this is a hash of the flags associated with this test + # users will use flags to distinguish the same test being run + # in a different environment + # for example: the same test being run on windows vs. mac + flags_hash = Column(types.String(256), nullable=False) + + framework = Column(types.String(100), nullable=True) + + computed_name = Column(types.Text, nullable=True) + filename = Column(types.Text, nullable=True) + + __table_args__ = ( + UniqueConstraint( + "repoid", + "name", + "testsuite", + "flags_hash", + name="reports_test_repoid_name_testsuite_flags_hash", + ), + ) + + +class TestInstance(CodecovBaseModel, MixinBaseClass): + __tablename__ = "reports_testinstance" + test_id = Column(types.Text, ForeignKey("reports_test.id")) + test = relationship(Test, backref=backref("testinstances")) + duration_seconds = Column(types.Float, nullable=False) + outcome = Column(types.String(100), nullable=False) + upload_id = Column(types.BigInteger, ForeignKey("reports_upload.id")) + upload = relationship("Upload", backref=backref("testinstances")) + failure_message = Column(types.Text) + branch = Column(types.Text, nullable=True) + commitid = Column(types.Text, nullable=True) + repoid = Column(types.Integer, nullable=True) + + reduced_error_id = Column( + types.BigInteger, ForeignKey("reports_reducederror.id"), nullable=True + ) + reduced_error = relationship("ReducedError", backref=backref("testinstances")) + + +class TestResultReportTotals(CodecovBaseModel, MixinBaseClass): + __tablename__ = "reports_testresultreporttotals" + report_id = Column(types.BigInteger, ForeignKey("reports_commitreport.id")) + report = relationship("CommitReport", foreign_keys=[report_id]) + passed = Column(types.Integer) + skipped = Column(types.Integer) + failed = Column(types.Integer) + + # this field is no longer used in the new ta_finisher task + # TODO: thus, it will be removed in the future + error = Column(types.String(100), nullable=True) + + +class ReducedError(CodecovBaseModel): + __tablename__ = "reports_reducederror" + id_ = Column("id", types.BigInteger, primary_key=True) + message = Column(types.Text) + created_at = Column(types.DateTime(timezone=True), default=get_utc_now) + updated_at = Column( + types.DateTime(timezone=True), onupdate=get_utc_now, default=get_utc_now + ) + + @property + def id(self): + return self.id_ + + +class Flake(CodecovBaseModel): + __tablename__ = "reports_flake" + id_ = Column("id", types.BigInteger, primary_key=True) + repoid = Column(types.Integer, ForeignKey("repos.repoid")) + repository = relationship("Repository", backref=backref("flakes")) + + testid = Column(types.Text, ForeignKey("reports_test.id")) + test = relationship(Test, backref=backref("flakes")) + + reduced_error_id = Column( + types.BigInteger, ForeignKey("reports_reducederror.id"), nullable=True + ) + reduced_error = relationship(ReducedError, backref=backref("flakes")) + + recent_passes_count = Column(types.Integer) + count = Column(types.Integer) + fail_count = Column(types.Integer) + start_date = Column(types.DateTime) + end_date = Column(types.DateTime, nullable=True) + + @property + def id(self): + return self.id_ + + +class DailyTestRollup(CodecovBaseModel, MixinBaseClassNoExternalID): + __tablename__ = "reports_dailytestrollups" + + test_id = Column(types.Text, ForeignKey("reports_test.id")) + test = relationship(Test, backref=backref("dailytestrollups")) + date = Column(types.Date) + repoid = Column(types.Integer) + branch = Column(types.Text) + + fail_count = Column(types.Integer) + flaky_fail_count = Column(types.Integer) + skip_count = Column(types.Integer) + pass_count = Column(types.Integer) + last_duration_seconds = Column(types.Float) + avg_duration_seconds = Column(types.Float) + latest_run = Column(types.DateTime) + commits_where_fail = Column(types.ARRAY(types.Text)) + + __table_args__ = ( + UniqueConstraint( + "repoid", + "date", + "branch", + "test_id", + name="reports_dailytestrollups_repoid_date_branch_test", + ), + ) + + +class TestFlagBridge(CodecovBaseModel): + __tablename__ = "reports_test_results_flag_bridge" + + id_ = Column("id", types.BigInteger, primary_key=True) + + test_id = Column(types.Text, ForeignKey("reports_test.id")) + test = relationship(Test, backref=backref("test_flag_bridges")) + + flag_id = Column( + "flag_id", types.BigInteger, ForeignKey("reports_repositoryflag.id") + ) + flag = relationship("RepositoryFlag", backref=backref("test_flag_bridges")) diff --git a/apps/worker/database/models/staticanalysis.py b/apps/worker/database/models/staticanalysis.py new file mode 100644 index 0000000000..22f3bd5110 --- /dev/null +++ b/apps/worker/database/models/staticanalysis.py @@ -0,0 +1,37 @@ +from sqlalchemy import Column, ForeignKey, types +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship + +from database.base import CodecovBaseModel, MixinBaseClass + + +class StaticAnalysisSuite(CodecovBaseModel, MixinBaseClass): + __tablename__ = "staticanalysis_staticanalysissuite" + commit_id = Column(types.BigInteger, ForeignKey("commits.id")) + # relationships + commit = relationship("Commit") + + +class StaticAnalysisSingleFileSnapshot(CodecovBaseModel, MixinBaseClass): + __tablename__ = "staticanalysis_staticanalysissinglefilesnapshot" + repository_id = Column(types.Integer, ForeignKey("repos.repoid")) + file_hash = Column(UUID, nullable=False) + content_location = Column(types.Text, nullable=False) + state_id = Column(types.Integer, nullable=False) + # relationships + repository = relationship("Repository") + + +class StaticAnalysisSuiteFilepath(CodecovBaseModel, MixinBaseClass): + __tablename__ = "staticanalysis_staticanalysissuitefilepath" + analysis_suite_id = Column( + types.BigInteger, ForeignKey("staticanalysis_staticanalysissuite.id") + ) + file_snapshot_id = Column( + types.BigInteger, + ForeignKey("staticanalysis_staticanalysissinglefilesnapshot.id"), + ) + filepath = Column(types.Text, nullable=False) + # relationships + file_snapshot = relationship(StaticAnalysisSingleFileSnapshot) + analysis_suite = relationship(StaticAnalysisSuite) diff --git a/apps/worker/database/models/timeseries.py b/apps/worker/database/models/timeseries.py new file mode 100644 index 0000000000..f96b0a8a4c --- /dev/null +++ b/apps/worker/database/models/timeseries.py @@ -0,0 +1,90 @@ +from enum import Enum + +from sqlalchemy import Column, types +from sqlalchemy.schema import Index + +from database.base import CodecovBaseModel + + +class TimeseriesBaseModel(CodecovBaseModel): + __abstract__ = True + + +class MeasurementName(Enum): + coverage = "coverage" + flag_coverage = "flag_coverage" + component_coverage = "component_coverage" + # For tracking the entire size of a bundle report by its name + bundle_analysis_report_size = "bundle_analysis_report_size" + # For tracking the size of a category of assets of a bundle report by its name + bundle_analysis_javascript_size = "bundle_analysis_javascript_size" + bundle_analysis_stylesheet_size = "bundle_analysis_stylesheet_size" + bundle_analysis_font_size = "bundle_analysis_font_size" + bundle_analysis_image_size = "bundle_analysis_image_size" + # For tracking individual asset size via its UUID + bundle_analysis_asset_size = "bundle_analysis_asset_size" + + +class Measurement(TimeseriesBaseModel): + """ + This model is defined here in order to describe the available columns and + indexes on the table. The table does not have a primary key and so you'll + likely run into issues if you try to use the model in an ORM-style of loading + and saving single records based on the primary key. The primary key is defined + below to appease SQLAlchemy only and is not intended to be used. + + See `services/timeseries.py` for an example of inserting/updating measurements. + """ + + __tablename__ = "timeseries_measurement" + + timestamp = Column(types.DateTime(timezone=True), nullable=False) + owner_id = Column(types.BigInteger, nullable=False) + repo_id = Column(types.BigInteger, nullable=False) + measurable_id = Column(types.String(256)) + branch = Column(types.String(256)) + commit_sha = Column(types.String(256), nullable=False) + name = Column(types.String(256), nullable=False) + value = Column(types.Float, nullable=False) + + __table_args__ = ( + Index( + "timeseries_measurement_measurable_unique", + timestamp, + owner_id, + repo_id, + measurable_id, + commit_sha, + name, + unique=True, + ), + ) + + __mapper_args__ = { + "primary_key": [ + timestamp, + owner_id, + repo_id, + measurable_id, + commit_sha, + name, + ] + } + + +class Dataset(TimeseriesBaseModel): + __tablename__ = "timeseries_dataset" + + id_ = Column("id", types.BigInteger, primary_key=True) + name = Column(types.String(256), nullable=False) + repository_id = Column(types.BigInteger, nullable=False) + backfilled = Column(types.Boolean, nullable=False, default=False) + + __table_args__ = ( + Index( + "timeseries_dataset_name_repo_unique", + name, + repository_id, + unique=True, + ), + ) diff --git a/apps/worker/database/tests/__init__.py b/apps/worker/database/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/database/tests/factories/__init__.py b/apps/worker/database/tests/factories/__init__.py new file mode 100644 index 0000000000..2cd2c564a1 --- /dev/null +++ b/apps/worker/database/tests/factories/__init__.py @@ -0,0 +1 @@ +from database.tests.factories.core import * diff --git a/apps/worker/database/tests/factories/core.py b/apps/worker/database/tests/factories/core.py new file mode 100644 index 0000000000..92ac521bd1 --- /dev/null +++ b/apps/worker/database/tests/factories/core.py @@ -0,0 +1,331 @@ +from datetime import datetime, timezone +from hashlib import sha1 +from uuid import uuid4 + +import factory +from factory import Factory +from shared.django_apps.codecov_auth.models import Plan, Tier +from shared.plan.constants import DEFAULT_FREE_PLAN, TierName + +from database import enums, models +from services.encryption import encryptor + + +def encrypt_oauth_token(val): + if val is None: + return None + return encryptor.encode(val) + + +class UserFactory(Factory): + class Meta: + model = models.User + + id_ = factory.Sequence(lambda n: n) + + name = factory.Faker("name") + email = factory.Faker("email") + is_staff = False + is_superuser = False + external_id = factory.LazyFunction(lambda: uuid4()) + + +class OwnerFactory(Factory): + class Meta: + model = models.Owner + exclude = ("unencrypted_oauth_token",) + + name = factory.Faker("name") + email = factory.Faker("email") + username = factory.Faker("user_name") + plan_activated_users = [] + service_id = factory.Sequence(lambda n: "user%d" % n) + admins = [] + permission = [] + organizations = [] + service = factory.Iterator(["gitlab", "github", "bitbucket"]) + free = 0 + unencrypted_oauth_token = factory.LazyFunction(lambda: uuid4().hex) + trial_start_date = datetime.now() + trial_end_date = datetime.now() + trial_status = enums.TrialStatus.NOT_STARTED.value + trial_fired_by = None + upload_token_required_for_public_repos = False + plan = DEFAULT_FREE_PLAN + + oauth_token = factory.LazyAttribute( + lambda o: encrypt_oauth_token(o.unencrypted_oauth_token) + ) + + @classmethod + def create_from_test_request(cls, request, *args, **kwargs): + if "username" not in kwargs: + kwargs["username"] = request.node.name[-100:] + return cls(*args, **kwargs) + + +class RepositoryFactory(Factory): + class Meta: + model = models.Repository + + private = True + name = factory.Faker("slug") + using_integration = False + service_id = factory.Sequence(lambda n: "id_%d" % n) + + owner = factory.SubFactory(OwnerFactory) + bot = None + updatestamp = factory.LazyAttribute(lambda o: datetime.now(tz=timezone.utc)) + languages = [] + languages_last_updated = factory.LazyAttribute( + lambda o: datetime.now(tz=timezone.utc) + ) + bundle_analysis_enabled = False + test_analytics_enabled = True + + +class BranchFactory(Factory): + class Meta: + model = models.Branch + + branch = factory.Faker("slug") + head = factory.LazyAttribute(lambda o: sha1(o.branch.encode("utf-8")).hexdigest()) + authors = [] + + repository = factory.SubFactory(RepositoryFactory) + + +class PullFactory(Factory): + class Meta: + model = models.Pull + + pullid = factory.Sequence(lambda n: 10 + (7 * n) % 90) + state = "open" + + repository = factory.SubFactory(RepositoryFactory) + author = factory.SubFactory(OwnerFactory) + + +class CommitFactory(Factory): + class Meta: + model = models.Commit + + message = factory.Faker("sentence") + + id_ = factory.Sequence(lambda n: n) + commitid = factory.LazyAttribute( + lambda o: sha1( + (o.message if o.message is not None else "nomessage").encode("utf-8") + ).hexdigest() + ) + ci_passed = True + pullid = None + timestamp = datetime(2019, 2, 1, 17, 59, 47, tzinfo=timezone.utc) + author = factory.SubFactory(OwnerFactory) + repository = factory.SubFactory(RepositoryFactory) + totals = factory.LazyFunction( + lambda: { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "85.00000", + "d": 0, + "diff": [1, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + "f": 3, + "h": 17, + "m": 3, + "n": 20, + "p": 0, + "s": 1, + } + ) + _report_json_storage_path = None + _report_json = factory.LazyFunction( + lambda: { + "files": { + "awesome/__init__.py": [ + 2, + [0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0], + [[0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0]], + [0, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + ], + "tests/__init__.py": [ + 0, + [0, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0], + [[0, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "tests/test_sample.py": [ + 1, + [0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + [[0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + }, + "sessions": { + "0": { + "N": None, + "a": "v4/raw/2019-01-10/4434BC2A2EC4FCA57F77B473D83F928C/abf6d4df662c47e32460020ab14abf9303581429/9ccc55a1-8b41-4bb1-a946-ee7a33a7fb56.txt", + "c": None, + "d": 1547084427, + "e": None, + "f": ["unit"], + "j": None, + "n": None, + "p": None, + "t": [3, 20, 17, 3, 0, "85.00000", 0, 0, 0, 0, 0, 0, 0], + "": None, + } + }, + } + ) + parent_commit_id = factory.LazyAttribute( + lambda o: sha1( + (o.message if o.message is not None else "nomessage" + "parent").encode( + "utf-8" + ) + ).hexdigest() + ) + state = "complete" + + +class ReportFactory(Factory): + class Meta: + model = models.CommitReport + + commit = factory.SubFactory(CommitFactory) + + +class ReportLevelTotalsFactory(Factory): + class Meta: + model = models.ReportLevelTotals + + report = factory.SubFactory(ReportFactory) + branches = 0 + coverage = 0.00 + hits = 0 + lines = 0 + methods = 0 + misses = 0 + partials = 0 + files = 0 + + +class ReportResultsFactory(Factory): + class Meta: + model = models.ReportResults + + report = factory.SubFactory(ReportFactory) + state = "success" + result = {"state": "success", "message": "somemessage"} + + +class UploadFactory(Factory): + class Meta: + model = models.Upload + + report = factory.SubFactory(ReportFactory) + state = "complete" + upload_extras = {} + upload_type = "uploaded" + storage_path = "storage/path.txt" + created_at = datetime.now() + + +class UploadLevelTotalsFactory(Factory): + class Meta: + model = models.UploadLevelTotals + + upload = factory.SubFactory(UploadFactory) + branches = 0 + coverage = 0.00 + hits = 0 + lines = 0 + methods = 0 + misses = 0 + partials = 0 + files = 0 + + +class RepositoryFlagFactory(Factory): + class Meta: + model = models.RepositoryFlag + + repository = factory.SubFactory(RepositoryFactory) + flag_name = "test_flag" + + +class CommitNotificationFactory(Factory): + class Meta: + model = models.CommitNotification + + notification_type = enums.Notification.comment + decoration_type = enums.Decoration.standard + state = enums.NotificationState.pending + + commit = factory.SubFactory(CommitFactory) + + +class CompareCommitFactory(Factory): + class Meta: + model = models.CompareCommit + + state = enums.CompareCommitState.pending.value + base_commit = factory.SubFactory(CommitFactory) + compare_commit = factory.SubFactory(CommitFactory) + + +class OrgLevelTokenFactory(Factory): + class Meta: + model = models.OrganizationLevelToken + + token = factory.LazyFunction(lambda: uuid4().hex) + token_type = "upload" + owner = factory.SubFactory(OwnerFactory) + + +class ConstantsFactory(Factory): + class Meta: + model = models.Constants + + key = "" + value = "" + + +class TierFactory(Factory): + class Meta: + model = Tier + + tier_name = TierName.BASIC.value + bundle_analysis = False + test_analytics = False + flaky_test_detection = False + project_coverage = False + private_repo_support = False + + +class PlanFactory(Factory): + class Meta: + model = Plan + + tier = factory.SubFactory(TierFactory) + base_unit_price = 0 + benefits = factory.LazyFunction(lambda: ["Benefit 1", "Benefit 2", "Benefit 3"]) + billing_rate = None + is_active = True + marketing_name = factory.Faker("catch_phrase") + max_seats = 1 + monthly_uploads_limit = None + name = DEFAULT_FREE_PLAN + paid_plan = False + stripe_id = None + + +class UploadErrorFactory(Factory): + class Meta: + model = models.UploadError + + report_upload = factory.SubFactory(UploadFactory) + error_code = "error" + error_params = {"error_message": "error message"} diff --git a/apps/worker/database/tests/factories/labelanalysis.py b/apps/worker/database/tests/factories/labelanalysis.py new file mode 100644 index 0000000000..08e4ee031c --- /dev/null +++ b/apps/worker/database/tests/factories/labelanalysis.py @@ -0,0 +1,13 @@ +import factory + +from database.models import LabelAnalysisRequest +from database.tests.factories.core import CommitFactory + + +class LabelAnalysisRequestFactory(factory.Factory): + class Meta: + model = LabelAnalysisRequest + + base_commit = factory.SubFactory(CommitFactory) + head_commit = factory.SubFactory(CommitFactory) + state_id = 1 diff --git a/apps/worker/database/tests/factories/reports.py b/apps/worker/database/tests/factories/reports.py new file mode 100644 index 0000000000..b5859dd0f6 --- /dev/null +++ b/apps/worker/database/tests/factories/reports.py @@ -0,0 +1,69 @@ +import datetime as dt + +import factory + +from database.models.reports import ( + CompareFlag, + Flake, + RepositoryFlag, + Test, + TestResultReportTotals, +) +from database.tests.factories.core import ( + CompareCommitFactory, + ReportFactory, + RepositoryFactory, +) + + +class RepositoryFlagFactory(factory.Factory): + repository = factory.SubFactory(RepositoryFactory) + flag_name = factory.Sequence(lambda n: f"flag{n}") + + class Meta: + model = RepositoryFlag + + +class CompareFlagFactory(factory.Factory): + class Meta: + model = CompareFlag + + commit_comparison = factory.SubFactory(CompareCommitFactory) + repositoryflag = factory.SubFactory(RepositoryFlagFactory) + + +class TestFactory(factory.Factory): + class Meta: + model = Test + + name = factory.Sequence(lambda n: f"test_{n}") + testsuite = "testsuite" + flags_hash = "flags_hash" + id_ = factory.Sequence(lambda n: f"id_{n}") + repository = factory.SubFactory(RepositoryFactory) + + +class FlakeFactory(factory.Factory): + class Meta: + model = Flake + + test = factory.SubFactory(TestFactory) + repository = factory.SelfAttribute("test.repository") + reduced_error = None + + count = 0 + fail_count = 0 + recent_passes_count = 0 + + start_date = dt.datetime.now() + end_date = None + + +class TestResultReportTotalsFactory(factory.Factory): + class Meta: + model = TestResultReportTotals + + report = factory.SubFactory(ReportFactory) + passed = 0 + skipped = 0 + failed = 0 diff --git a/apps/worker/database/tests/factories/staticanalysis.py b/apps/worker/database/tests/factories/staticanalysis.py new file mode 100644 index 0000000000..4b74fd8be4 --- /dev/null +++ b/apps/worker/database/tests/factories/staticanalysis.py @@ -0,0 +1,37 @@ +from uuid import uuid4 + +import factory +from shared.labelanalysis import LabelAnalysisRequestState + +from database.models.staticanalysis import ( + StaticAnalysisSingleFileSnapshot, + StaticAnalysisSuite, + StaticAnalysisSuiteFilepath, +) +from database.tests.factories.core import CommitFactory, RepositoryFactory + + +class StaticAnalysisSuiteFactory(factory.Factory): + class Meta: + model = StaticAnalysisSuite + + commit = factory.SubFactory(CommitFactory) + + +class StaticAnalysisSingleFileSnapshotFactory(factory.Factory): + class Meta: + model = StaticAnalysisSingleFileSnapshot + + repository = factory.SubFactory(RepositoryFactory) + file_hash = factory.LazyFunction(lambda: uuid4().hex) + content_location = factory.Faker("file_path", depth=3) + state_id = LabelAnalysisRequestState.CREATED.db_id + + +class StaticAnalysisSuiteFilepathFactory(factory.Factory): + class Meta: + model = StaticAnalysisSuiteFilepath + + filepath = factory.Faker("file_name") + file_snapshot = factory.SubFactory(StaticAnalysisSingleFileSnapshotFactory) + analysis_suite = factory.SubFactory(StaticAnalysisSuiteFactory) diff --git a/apps/worker/database/tests/factories/timeseries.py b/apps/worker/database/tests/factories/timeseries.py new file mode 100644 index 0000000000..12b0df3c13 --- /dev/null +++ b/apps/worker/database/tests/factories/timeseries.py @@ -0,0 +1,27 @@ +import random +from datetime import datetime + +import factory + +from database.models.timeseries import Dataset, Measurement + + +class MeasurementFactory(factory.Factory): + owner_id = 1 + repo_id = 1 + name = "testing" + branch = "master" + value = factory.LazyAttribute(lambda: random.random() * 1000) + timestamp = factory.LazyAttribute(lambda: datetime.now()) + + class Meta: + model = Measurement + + +class DatasetFactory(factory.Factory): + repository_id = 1 + name = "testing" + backfilled = False + + class Meta: + model = Dataset diff --git a/apps/worker/database/tests/unit/test_engine.py b/apps/worker/database/tests/unit/test_engine.py new file mode 100644 index 0000000000..50760a1b40 --- /dev/null +++ b/apps/worker/database/tests/unit/test_engine.py @@ -0,0 +1,58 @@ +from sqlalchemy.dialects.postgresql import insert +from sqlalchemy_utils import get_mapper + +from database.engine import SessionFactory +from database.models import Commit +from database.models.timeseries import Measurement + + +class TestDatabaseEngine: + def test_session_get_bind_timeseries_disabled(self, sqlalchemy_connect_url, mocker): + mocker.patch("database.engine.is_timeseries_enabled", return_value=False) + + session_factory = SessionFactory( + database_url=sqlalchemy_connect_url, + timeseries_database_url=sqlalchemy_connect_url, + ) + session = session_factory.create_session() + assert session_factory.main_engine is not None + assert session_factory.timeseries_engine is None + + engine = session.get_bind(mapper=get_mapper(Commit)) + assert engine == session_factory.main_engine + + clause = insert(Commit.__table__) + engine = session.get_bind(clause=clause) + assert engine == session_factory.main_engine + + engine = session.get_bind(mapper=get_mapper(Measurement)) + assert engine == session_factory.main_engine + + clause = insert(Measurement.__table__) + engine = session.get_bind(clause=clause) + assert engine == session_factory.main_engine + + def test_session_get_bind_timeseries_enabled(self, sqlalchemy_connect_url, mocker): + mocker.patch("database.engine.is_timeseries_enabled", return_value=True) + + session_factory = SessionFactory( + database_url=sqlalchemy_connect_url, + timeseries_database_url=sqlalchemy_connect_url, + ) + session = session_factory.create_session() + assert session_factory.main_engine is not None + assert session_factory.timeseries_engine is not None + + engine = session.get_bind(mapper=get_mapper(Commit)) + assert engine == session_factory.main_engine + + clause = insert(Commit.__table__) + engine = session.get_bind(clause=clause) + assert engine == session_factory.main_engine + + engine = session.get_bind(mapper=get_mapper(Measurement)) + assert engine == session_factory.timeseries_engine + + clause = insert(Measurement.__table__) + engine = session.get_bind(clause=clause) + assert engine == session_factory.timeseries_engine diff --git a/apps/worker/database/tests/unit/test_events.py b/apps/worker/database/tests/unit/test_events.py new file mode 100644 index 0000000000..36994afd10 --- /dev/null +++ b/apps/worker/database/tests/unit/test_events.py @@ -0,0 +1,89 @@ +import os + +import database.events # noqa: F401 +from database.tests.factories import OwnerFactory, RepositoryFactory + + +def test_shelter_repo_sync(dbsession, mock_configuration, mocker): + # this prevents the pubsub SDK from trying to load credentials + os.environ["PUBSUB_EMULATOR_HOST"] = "localhost" + publish = mocker.patch("google.cloud.pubsub_v1.PublisherClient.publish") + + mock_configuration.set_params( + { + "setup": { + "shelter": { + "pubsub_project_id": "test-project-id", + "sync_repo_topic_id": "test-topic-id", + "enabled": True, + } + } + } + ) + + # this triggers the publish via SQLAlchemy events (after_insert) + repo = RepositoryFactory( + repoid=91728376, name="test-123", owner=OwnerFactory(ownerid=123), private=False + ) + dbsession.add(repo) + dbsession.commit() + + publish.assert_called_once_with( + "projects/test-project-id/topics/test-topic-id", + b'{"type": "repo", "sync": "one", "id": 91728376}', + ) + publish_calls = publish.call_args_list + + # Synchronize object flush for history.deleted to be perceived by sqlalchemy + dbsession.refresh(repo) + + # this triggers the publish via SQLAlchemy events (after_update) + repo.name = "test-456" + dbsession.commit() + dbsession.refresh(repo) + assert len(publish_calls) == 2 + + # Does not trigger another publish with untracked field + repo.message = "foo" + dbsession.commit() + dbsession.refresh(repo) + assert len(publish_calls) == 2 + + # Triggers call when owner is changed + repo.owner = OwnerFactory(ownerid=456) + dbsession.commit() + dbsession.refresh(repo) + assert len(publish_calls) == 3 + + # Triggers call when private is changed + repo.private = True + dbsession.commit() + dbsession.refresh(repo) + assert len(publish_calls) == 4 + + +def test_repo_sync_when_shelter_disabled(dbsession, mock_configuration, mocker): + # this prevents the pubsub SDK from trying to load credentials + os.environ["PUBSUB_EMULATOR_HOST"] = "localhost" + + mock_configuration.set_params( + { + "setup": { + "shelter": { + "pubsub_project_id": "test-project-id", + "sync_repo_topic_id": "test-topic-id", + "enabled": False, + } + } + } + ) + + publish = mocker.patch("google.cloud.pubsub_v1.PublisherClient.publish") + + # Create new repo with shelter disabled + repo = RepositoryFactory(repoid=91728377, name="test-789") + dbsession.add(repo) + dbsession.commit() + + # Verify no publish was called when shelter is disabled + publish.assert_not_called() diff --git a/apps/worker/database/tests/unit/test_model_utils.py b/apps/worker/database/tests/unit/test_model_utils.py new file mode 100644 index 0000000000..9105ef10bf --- /dev/null +++ b/apps/worker/database/tests/unit/test_model_utils.py @@ -0,0 +1,139 @@ +import json +from unittest.mock import PropertyMock + +from shared.storage.exceptions import FileNotInStorageError + +from database.models.core import Commit +from database.tests.factories.core import CommitFactory +from database.utils import ArchiveField, ArchiveFieldInterface + + +class TestArchiveField(object): + class ClassWithArchiveField(object): + commit: Commit + id = 1 + external_id = "external_id" + __tablename__ = "test_table" + + _archive_field = "db_field" + _archive_field_storage_path = "archive_field_path" + + def should_write_to_storage(self): + return self.should_write_to_gcs + + def get_repository(self): + return self.commit.repository + + def get_commitid(self): + return self.commit.commitid + + def __init__( + self, commit, db_value, archive_value, should_write_to_gcs=False + ) -> None: + self.commit = commit + self._archive_field = db_value + self._archive_field_storage_path = archive_value + self.should_write_to_gcs = should_write_to_gcs + + archive_field = ArchiveField( + should_write_to_storage_fn=should_write_to_storage, + read_timeout=0.1, + ) + + class ClassWithArchiveFieldMissingMethods: + commit: Commit + id = 1 + external_id = "external_id" + + def test_subclass_validation(self, mocker): + assert issubclass( + self.ClassWithArchiveField( + mocker.MagicMock(), mocker.MagicMock(), mocker.MagicMock() + ), + ArchiveFieldInterface, + ) + assert not issubclass( + self.ClassWithArchiveFieldMissingMethods, ArchiveFieldInterface + ) + + def test_archive_getter_db_field_set(self, sqlalchemy_db): + commit = CommitFactory() + test_class = self.ClassWithArchiveField(commit, "db_value", "gcs_path") + assert test_class._archive_field == "db_value" + assert test_class._archive_field_storage_path == "gcs_path" + assert test_class.archive_field == "db_value" + + def test_archive_getter_archive_field_set(self, sqlalchemy_db, mocker): + some_json = {"some": "data"} + mock_read_file = mocker.MagicMock(return_value=json.dumps(some_json)) + mock_archive_service = mocker.patch("database.utils.ArchiveService") + mock_archive_service.return_value.read_file = mock_read_file + commit = CommitFactory() + test_class = self.ClassWithArchiveField(commit, None, "gcs_path") + + assert test_class._archive_field is None + assert test_class._archive_field_storage_path == "gcs_path" + assert test_class.archive_field == some_json + mock_read_file.assert_called_with("gcs_path") + mock_archive_service.assert_called_with(repository=commit.repository) + assert mock_read_file.call_count == 1 + # Test that caching also works + assert test_class.archive_field == some_json + assert mock_read_file.call_count == 1 + + def test_archive_getter_file_not_in_storage(self, sqlalchemy_db, mocker): + mocker.patch( + "database.utils.ArchiveField.read_timeout", + new_callable=PropertyMock, + return_value=0.1, + ) + mock_read_file = mocker.MagicMock(side_effect=FileNotInStorageError()) + mock_archive_service = mocker.patch("database.utils.ArchiveService") + mock_archive_service.return_value.read_file = mock_read_file + commit = CommitFactory() + test_class = self.ClassWithArchiveField(commit, None, "gcs_path") + + assert test_class._archive_field is None + assert test_class._archive_field_storage_path == "gcs_path" + assert test_class.archive_field is None + mock_read_file.assert_called_with("gcs_path") + mock_archive_service.assert_called_with(repository=commit.repository) + + def test_archive_setter_db_field(self, sqlalchemy_db, mocker): + commit = CommitFactory() + test_class = self.ClassWithArchiveField(commit, "db_value", "gcs_path", False) + assert test_class._archive_field == "db_value" + assert test_class._archive_field_storage_path == "gcs_path" + assert test_class.archive_field == "db_value" + mock_archive_service = mocker.patch("database.utils.ArchiveService") + test_class.archive_field = "batata frita" + mock_archive_service.assert_not_called() + assert test_class._archive_field == "batata frita" + assert test_class.archive_field == "batata frita" + + def test_archive_setter_archive_field(self, sqlalchemy_db, mocker): + commit = CommitFactory() + test_class = self.ClassWithArchiveField(commit, "db_value", None, True) + some_json = {"some": "data"} + mock_read_file = mocker.MagicMock(return_value=json.dumps(some_json)) + mock_write_file = mocker.MagicMock(return_value="path/to/written/object") + mock_archive_service = mocker.patch("database.utils.ArchiveService") + mock_archive_service.return_value.read_file = mock_read_file + mock_archive_service.return_value.write_json_data_to_storage = mock_write_file + + assert test_class._archive_field == "db_value" + assert test_class._archive_field_storage_path is None + assert test_class.archive_field == "db_value" + assert mock_read_file.call_count == 0 + + # Pretend there was something in the path. + # This should happen, but it will help us test the deletion of old data saved + test_class._archive_field_storage_path = "path/to/old/data" + + # Now we write to the property + test_class.archive_field = some_json + assert test_class._archive_field is None + assert test_class._archive_field_storage_path == "path/to/written/object" + assert test_class.archive_field == some_json + # Cache is updated on write + assert mock_read_file.call_count == 0 diff --git a/apps/worker/database/tests/unit/test_models.py b/apps/worker/database/tests/unit/test_models.py new file mode 100644 index 0000000000..735706d92b --- /dev/null +++ b/apps/worker/database/tests/unit/test_models.py @@ -0,0 +1,492 @@ +import json +from unittest.mock import PropertyMock + +from mock import MagicMock, patch +from shared.plan.constants import DEFAULT_FREE_PLAN +from shared.storage.exceptions import FileNotInStorageError +from sqlalchemy.orm import Session + +from database.models import ( + Account, + Branch, + Commit, + CommitNotification, + Owner, + Pull, + Repository, +) +from database.models.core import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + AccountsUsers, + GithubAppInstallation, +) +from database.tests.factories import ( + BranchFactory, + CommitFactory, + CommitNotificationFactory, + CompareCommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) +from database.tests.factories.core import UserFactory + + +class TestReprModels(object): + def test_owner_repr(self, dbsession): + simple_owner = Owner() + assert "Owner>" == repr(simple_owner) + factoried_owner = OwnerFactory.create(service="github") + assert "Owner>" == repr(factoried_owner) + dbsession.add(factoried_owner) + dbsession.flush() + dbsession.refresh(factoried_owner) + assert f"Owner<{factoried_owner.ownerid}@service>" == repr( + factoried_owner + ) + + def test_repo_repr(self, dbsession): + simple_repo = Repository() + assert "Repo" == repr(simple_repo) + factoried_repo = RepositoryFactory.create() + assert "Repo" == repr(factoried_repo) + dbsession.add(factoried_repo) + dbsession.flush() + dbsession.refresh(factoried_repo) + assert f"Repo<{factoried_repo.repoid}>" == repr(factoried_repo) + + def test_commit_repr(self, dbsession): + simple_commit = Commit() + assert "Commit>" == repr(simple_commit) + factoried_commit = CommitFactory.create( + commitid="327993f5d81eda4bac19ea6090fe68c8eb313066" + ) + assert "Commit<327993f5d81eda4bac19ea6090fe68c8eb313066@repo>" == repr( + factoried_commit + ) + dbsession.add(factoried_commit) + dbsession.flush() + dbsession.refresh(factoried_commit) + assert ( + f"Commit<327993f5d81eda4bac19ea6090fe68c8eb313066@repo<{factoried_commit.repoid}>>" + == repr(factoried_commit) + ) + + def test_branch_repr(self, dbsession): + simple_branch = Branch() + assert "Branch>" == repr(simple_branch) + factoried_branch = BranchFactory.create(branch="thisoakbranch") + assert "Branch>" == repr(factoried_branch) + dbsession.add(factoried_branch) + dbsession.flush() + dbsession.refresh(factoried_branch) + assert f"Branch>" == repr( + factoried_branch + ) + + def test_pull_repr(self, dbsession): + simple_pull = Pull() + assert "Pull>" == repr(simple_pull) + factoried_pull = PullFactory.create() + assert f"Pull<{factoried_pull.pullid}@repo>" == repr(factoried_pull) + dbsession.add(factoried_pull) + dbsession.flush() + dbsession.refresh(factoried_pull) + assert f"Pull<{factoried_pull.pullid}@repo<{factoried_pull.repoid}>>" == repr( + factoried_pull + ) + + def test_notification_repr(self, dbsession): + simple_notification = CommitNotification() + assert "Notification>" == repr(simple_notification) + factoried_notification = CommitNotificationFactory.create() + assert ( + f"Notification<{factoried_notification.notification_type}@commit<{factoried_notification.commit_id}>>" + == repr(factoried_notification) + ) + dbsession.add(factoried_notification) + dbsession.flush() + dbsession.refresh(factoried_notification) + assert ( + f"Notification<{factoried_notification.notification_type}@commit<{factoried_notification.commit_id}>>" + == repr(factoried_notification) + ) + + def test_commit_compare_repr(self, dbsession): + compare_commit = CompareCommitFactory() + assert "CompareCommit" == repr(compare_commit) + + def test_commit_notified(self, dbsession): + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + assert commit.notified is None + commit.notified = True + dbsession.flush() + dbsession.refresh(commit) + assert commit.notified is True + + +class TestPullModel(object): + def test_updatestamp_update(self, dbsession): + factoried_pull = PullFactory.create(updatestamp=None) + assert factoried_pull.updatestamp is None + dbsession.add(factoried_pull) + dbsession.flush() + assert factoried_pull.updatestamp is not None + val = factoried_pull.updatestamp + factoried_pull.title = "Super Mario Bros" + dbsession.flush() + assert factoried_pull.updatestamp is not None + assert factoried_pull.updatestamp > val + + +class TestOwnerModel(object): + def test_upload_token_required_for_public_repos(self, dbsession): + # Create an owner with upload_token_required_for_public_repos specified + tokens_required_owner = Owner( + name="Token Owner", + email="token_owner@example.com", + username="tokenuser", + upload_token_required_for_public_repos=True, + service="github", + service_id="abc", + ) + dbsession.add(tokens_required_owner) + dbsession.commit() + + # Refresh from the database to verify persistence + dbsession.refresh(tokens_required_owner) + assert tokens_required_owner.upload_token_required_for_public_repos is True + + # Update other field, upload_token_required_for_public_repos is unchanged + assert tokens_required_owner.onboarding_completed is False + tokens_required_owner.onboarding_completed = True + dbsession.commit() + dbsession.refresh(tokens_required_owner) + assert tokens_required_owner.onboarding_completed is True + assert tokens_required_owner.upload_token_required_for_public_repos is True + + # Create an owner without upload_token_required_for_public_repos specified + tokens_not_required_owner = Owner( + name="Tokenless Owner", + email="tokenless_owner@example.com", + username="tokenlessuser", + service="github", + service_id="defg", + ) + dbsession.add(tokens_not_required_owner) + dbsession.commit() + dbsession.refresh(tokens_not_required_owner) + assert tokens_not_required_owner.upload_token_required_for_public_repos is False + + # Update other field, upload_token_required_for_public_repos is unchanged + assert tokens_not_required_owner.onboarding_completed is False + tokens_not_required_owner.onboarding_completed = True + dbsession.commit() + dbsession.refresh(tokens_not_required_owner) + assert tokens_not_required_owner.onboarding_completed is True + assert tokens_not_required_owner.upload_token_required_for_public_repos is False + + def test_root_organization(self, dbsession): + gitlab_root_group = OwnerFactory.create( + username="root_group", + service="gitlab", + plan="users-pr-inappm", + ) + dbsession.add(gitlab_root_group) + gitlab_middle_group = OwnerFactory.create( + username="mid_group", + service="gitlab", + parent_service_id=gitlab_root_group.service_id, + root_parent_service_id=None, + ) + dbsession.add(gitlab_middle_group) + gitlab_subgroup = OwnerFactory.create( + username="subgroup", + service="gitlab", + parent_service_id=gitlab_middle_group.service_id, + root_parent_service_id=None, + ) + dbsession.add(gitlab_subgroup) + github_org = OwnerFactory.create( + username="gh", + service="github", + ) + dbsession.add(github_org) + dbsession.flush() + + assert gitlab_root_group.root_organization is None + assert gitlab_root_group.root_parent_service_id is None + + assert gitlab_middle_group.root_organization == gitlab_root_group + assert ( + gitlab_middle_group.root_parent_service_id == gitlab_root_group.service_id + ) + + assert gitlab_subgroup.root_organization == gitlab_root_group + assert gitlab_subgroup.root_parent_service_id == gitlab_root_group.service_id + + assert github_org.root_organization is None + assert github_org.root_parent_service_id is None + + +class TestAccountModels(object): + def test_create_account(self, dbsession): + account = Account( + name="test_name", + ) + dbsession.add(account) + dbsession.commit() + dbsession.refresh(account) + assert account.name == "test_name" + assert account.is_active is True + assert account.plan == DEFAULT_FREE_PLAN + assert account.plan_seat_count == 1 + assert account.free_seat_count == 0 + assert account.plan_auto_activate is True + assert account.is_delinquent is False + assert account.users == [] + assert account.organizations == [] + + def test_account_fks(self, dbsession): + user = UserFactory() + owner_person = OwnerFactory() + owner_org = OwnerFactory() + account = Account( + name="test_name", + ) + dbsession.add_all([user, owner_person, owner_org, account]) + dbsession.commit() + + # this is the evaluation from shared that was breaking + assert owner_org.account is None + has_account = owner_org.account is not None + assert has_account is False + + owner_person.user = user + account.users.append(user) + account.organizations.append(owner_org) + dbsession.add_all([owner_person, account]) + dbsession.commit() + + dbsession.refresh(user) + dbsession.refresh(owner_person) + dbsession.refresh(owner_org) + dbsession.refresh(account) + + assert user.accounts == [account] + assert owner_person.account is None + assert owner_org.account == account + assert account.users == [user] + assert account.organizations == [owner_org] + # this is the evaluation from shared that was breaking + has_account = owner_org.account is not None + assert has_account is True + + through_table_obj = dbsession.query(AccountsUsers).first() + assert through_table_obj.user_id == user.id + assert through_table_obj.account_id == account.id + + +class TestCommitModel(object): + sample_report = { + "files": { + "different/test_file.py": [ + 2, + [0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0], + [[0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0]], + [0, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + ], + }, + "sessions": { + "0": { + "N": None, + "a": "v4/raw/2019-01-10/4434BC2A2EC4FCA57F77B473D83F928C/abf6d4df662c47e32460020ab14abf9303581429/9ccc55a1-8b41-4bb1-a946-ee7a33a7fb56.txt", + "c": None, + "d": 1547084427, + "e": None, + "f": ["unittests"], + "j": None, + "n": None, + "p": None, + "t": [3, 20, 17, 3, 0, "85.00000", 0, 0, 0, 0, 0, 0, 0], + "": None, + } + }, + } + + @patch("database.utils.ArchiveService") + def test_get_report_from_db(self, mock_archive, dbsession): + commit = CommitFactory() + mock_read_file = MagicMock() + mock_archive.return_value.read_file = mock_read_file + commit._report_json = self.sample_report + dbsession.add(commit) + dbsession.flush() + + fetched = dbsession.query(Commit).get(commit.id_) + assert fetched.report_json == self.sample_report + mock_archive.assert_not_called() + mock_read_file.assert_not_called() + + @patch("database.utils.ArchiveService") + def test_get_report_from_storage(self, mock_archive, dbsession): + commit = CommitFactory() + storage_path = "https://storage/path/report.json" + mock_read_file = MagicMock(return_value=json.dumps(self.sample_report)) + mock_archive.return_value.read_file = mock_read_file + commit._report_json = None + commit._report_json_storage_path = storage_path + dbsession.add(commit) + dbsession.flush() + + fetched = dbsession.query(Commit).get(commit.id_) + assert fetched.report_json == self.sample_report + mock_archive.assert_called() + mock_read_file.assert_called_with(storage_path) + # Calls it again to test caching + assert fetched.report_json == self.sample_report + assert mock_archive.call_count == 1 + assert mock_read_file.call_count == 1 + # This one to help us understand caching across different instances + # different instances if they are the same + assert commit.report_json == self.sample_report + assert mock_archive.call_count == 1 + assert mock_read_file.call_count == 1 + # Let's see for objects with different IDs + diff_commit = CommitFactory() + storage_path = "https://storage/path/files_array.json" + diff_commit._report_json = None + diff_commit._report_json_storage_path = storage_path + dbsession.add(diff_commit) + dbsession.flush() + assert diff_commit.report_json == self.sample_report + assert mock_archive.call_count == 2 + assert mock_read_file.call_count == 2 + + @patch("database.utils.ArchiveService") + def test_get_report_from_storage_file_not_found( + self, mock_archive, dbsession, mocker + ): + mocker.patch( + "database.utils.ArchiveField.read_timeout", + new_callable=PropertyMock, + return_value=0.1, + ) + commit = CommitFactory() + storage_path = "https://storage/path/files_array.json" + + def side_effect(*args, **kwargs): + raise FileNotInStorageError() + + mock_read_file = MagicMock(side_effect=side_effect) + mock_archive.return_value.read_file = mock_read_file + commit._report_json = None + commit._report_json_storage_path = storage_path + dbsession.add(commit) + dbsession.flush() + + fetched = dbsession.query(Commit).get(commit.id_) + assert fetched._report_json_storage_path == storage_path + assert fetched.report_json == {} + mock_archive.assert_called() + mock_read_file.assert_called_with(storage_path) + + +class TestGithubAppInstallationModel(object): + def test_covers_all_repos(self, dbsession: Session): + owner = OwnerFactory.create() + other_owner = OwnerFactory.create() + repo1 = RepositoryFactory.create(owner=owner) + repo2 = RepositoryFactory.create(owner=owner) + repo3 = RepositoryFactory.create(owner=owner) + other_repo_different_owner = RepositoryFactory.create(owner=other_owner) + installation_obj = GithubAppInstallation( + owner=owner, + repository_service_ids=None, + installation_id=100, + # name would be set by the API + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + ) + dbsession.add_all([owner, other_owner, repo1, repo2, repo3, installation_obj]) + dbsession.flush() + assert installation_obj.covers_all_repos() == True + assert installation_obj.is_repo_covered_by_integration(repo1) == True + assert other_repo_different_owner.ownerid != repo1.ownerid + assert ( + installation_obj.is_repo_covered_by_integration(other_repo_different_owner) + == False + ) + assert owner.github_app_installations == [installation_obj] + assert installation_obj.repository_queryset(dbsession).count() == 3 + assert set(installation_obj.repository_queryset(dbsession).all()) == set( + [repo1, repo2, repo3] + ) + + def test_covers_some_repos(self, dbsession: Session): + owner = OwnerFactory() + repo = RepositoryFactory(owner=owner) + same_owner_other_repo = RepositoryFactory(owner=owner) + other_repo_different_owner = RepositoryFactory() + installation_obj = GithubAppInstallation( + owner=owner, + repository_service_ids=[repo.service_id], + installation_id=100, + ) + dbsession.add_all( + [ + owner, + repo, + same_owner_other_repo, + other_repo_different_owner, + installation_obj, + ] + ) + dbsession.flush() + assert installation_obj.covers_all_repos() == False + assert installation_obj.is_repo_covered_by_integration(repo) == True + assert ( + installation_obj.is_repo_covered_by_integration(other_repo_different_owner) + == False + ) + assert ( + installation_obj.is_repo_covered_by_integration(same_owner_other_repo) + == False + ) + assert owner.github_app_installations == [installation_obj] + assert installation_obj.repository_queryset(dbsession).count() == 1 + assert list(installation_obj.repository_queryset(dbsession).all()) == [repo] + + def test_is_configured(self, dbsession: Session): + owner = OwnerFactory() + installation_obj_default = GithubAppInstallation( + owner=owner, + repository_service_ids=None, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + installation_id=100, + ) + installation_obj_configured = GithubAppInstallation( + owner=owner, + repository_service_ids=None, + name="my_installation", + installation_id=100, + app_id=10, + pem_path="some_path", + ) + installation_obj_not_configured = GithubAppInstallation( + owner=owner, + repository_service_ids=None, + installation_id=100, + name="my_installation", + ) + dbsession.add_all( + [ + installation_obj_default, + installation_obj_configured, + installation_obj_not_configured, + ] + ) + assert installation_obj_default.is_configured() == True + assert installation_obj_configured.is_configured() == True + assert installation_obj_not_configured.is_configured() == False diff --git a/apps/worker/database/utils.py b/apps/worker/database/utils.py new file mode 100644 index 0000000000..34bd47dec5 --- /dev/null +++ b/apps/worker/database/utils.py @@ -0,0 +1,183 @@ +import logging +import time +from typing import Any, Callable, Optional + +import orjson +from shared.storage.exceptions import FileNotInStorageError +from shared.utils.ReportEncoder import ReportEncoder + +from services.archive import ArchiveService + +log = logging.getLogger(__name__) + + +class ArchiveFieldInterfaceMeta(type): + def __subclasscheck__(cls, subclass): + return ( + hasattr(subclass, "get_repository") + and callable(subclass.get_repository) + and hasattr(subclass, "get_commitid") + and callable(subclass.get_commitid) + and hasattr(subclass, "external_id") + ) + + +class ArchiveFieldInterface(metaclass=ArchiveFieldInterfaceMeta): + """Any class that uses ArchiveField must implement this interface""" + + external_id: str + + def get_repository(self): + """Returns the repository object associated with self""" + raise NotImplementedError() + + def get_commitid(self) -> Optional[str]: + """Returns the commitid associated with self. + If no commitid is associated return None. + """ + raise NotImplementedError() + + +class ArchiveField: + """This is a helper class that transparently handles models' fields that are saved in storage. + Classes that use the ArchiveField MUST implement ArchiveFieldInterface. It ill throw an error otherwise. + It uses the Descriptor pattern: https://docs.python.org/3/howto/descriptor.html + + Arguments: + should_write_to_storage_fn: Callable function that decides if data should be written to storage. + It should take 1 argument: the object instance. + + rehydrate_fn: Callable function to allow you to decode your saved data into internal representations. + The default value does nothing. + Data retrieved both from DB and storage pass through this function to guarantee consistency. + It should take 2 arguments: the object instance and the encoded data. + + default_value: Any value that will be returned if we can't save the data for whatever reason + + Example: + archive_field = ArchiveField( + should_write_to_storage_fn=should_write_data, + rehydrate_fn=rehidrate_data, + default_value='default' + ) + For a full example check utils/tests/unit/test_model_utils.py + """ + + def __init__( + self, + should_write_to_storage_fn: Callable[[object], bool], + rehydrate_fn: Callable[[object, object], Any] = lambda self, x: x, + json_encoder=ReportEncoder, + default_value_class=lambda: None, + read_timeout=5, + ): + self.default_value_class = default_value_class + self.rehydrate_fn = rehydrate_fn + self.should_write_to_storage_fn = should_write_to_storage_fn + self.json_encoder = json_encoder + self._read_timeout = read_timeout + + @property + def read_timeout(self): + return self._read_timeout + + def __set_name__(self, owner, name): + # Validate that the owner class has the methods we need + assert issubclass(owner, ArchiveFieldInterface), ( + "Missing some required methods to use AchiveField" + ) + self.public_name = name + self.db_field_name = "_" + name + self.archive_field_name = "_" + name + "_storage_path" + self.cached_value_property_name = f"__{self.public_name}_cached_value" + + def _get_value_from_archive(self, obj): + repository = obj.get_repository() + archive_service = ArchiveService(repository=repository) + archive_field = getattr(obj, self.archive_field_name) + if archive_field: + start_time = time.time() + error = False + while time.time() < start_time + self.read_timeout: + # we're within the timeout window + try: + file_str = archive_service.read_file(archive_field) + result = self.rehydrate_fn(obj, orjson.loads(file_str)) + if error: + # we previously errored and now it succeeded + log.info( + "Archive enabled field found in storage after delay", + extra=dict( + storage_path=archive_field, + object_id=obj.id, + commit=obj.get_commitid(), + delay_seconds=time.time() - start_time, + ), + ) + return result + except FileNotInStorageError: + log.warning( + "Archive enabled not found, retrying soon", + extra=dict( + storage_path=archive_field, + object_id=obj.id, + commit=obj.get_commitid(), + ), + ) + error = True + # sleep a little but so we're not hammering the archive service + # in a tight loop + time.sleep(self.read_timeout / 10) + + log.error( + "Archive enabled field not in storage", + extra=dict( + storage_path=archive_field, + object_id=obj.id, + commit=obj.get_commitid(), + ), + ) + else: + log.debug( + "Both db_field and archive_field are None", + extra=dict( + object_id=obj.id, + commit=obj.get_commitid(), + ), + ) + return self.default_value_class() + + def __get__(self, obj, objtype=None): + cached_value = getattr(obj, self.cached_value_property_name, None) + if cached_value: + return cached_value + db_field = getattr(obj, self.db_field_name) + if db_field is not None: + value = self.rehydrate_fn(obj, db_field) + else: + value = self._get_value_from_archive(obj) + setattr(obj, self.cached_value_property_name, value) + return value + + def __set__(self, obj, value): + # Set the new value + if self.should_write_to_storage_fn(obj): + repository = obj.get_repository() + archive_service = ArchiveService(repository=repository) + old_file_path = getattr(obj, self.archive_field_name) + table_name = obj.__tablename__ + path = archive_service.write_json_data_to_storage( + commit_id=obj.get_commitid(), + table=table_name, + field=self.public_name, + external_id=obj.external_id, + data=value, + encoder=self.json_encoder, + ) + if old_file_path is not None and path != old_file_path: + archive_service.delete_file(old_file_path) + setattr(obj, self.archive_field_name, path) + setattr(obj, self.db_field_name, None) + else: + setattr(obj, self.db_field_name, value) + setattr(obj, self.cached_value_property_name, value) diff --git a/apps/worker/django_scaffold/__init__.py b/apps/worker/django_scaffold/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/django_scaffold/settings.py b/apps/worker/django_scaffold/settings.py new file mode 100644 index 0000000000..6f85e6cd07 --- /dev/null +++ b/apps/worker/django_scaffold/settings.py @@ -0,0 +1,86 @@ +import os +from pathlib import Path + +from shared.django_apps.db_settings import * + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +ALLOWED_HOSTS = [] + +IS_DEV = os.getenv("RUN_ENV") == "DEV" +IS_ENTERPRISE = os.getenv("RUN_ENV") == "ENTERPRISE" + +GCS_BUCKET_NAME = get_config("services", "minio", "bucket", default="archive") + +# Application definition +INSTALLED_APPS = [ + # dependencies + "psqlextra", + # Needed to install legacy migrations + "django.contrib.admin", + "django.contrib.contenttypes", + "django.contrib.auth", + "django.contrib.messages", + "django.contrib.sessions", + # Shared apps: + "shared.django_apps.legacy_migrations", + "shared.django_apps.pg_telemetry", + "shared.django_apps.rollouts", + "shared.django_apps.user_measurements", + "shared.django_apps.bundle_analysis", + "shared.django_apps.codecov_auth", + "shared.django_apps.compare", + "shared.django_apps.core", + "shared.django_apps.labelanalysis", + "shared.django_apps.reports", + "shared.django_apps.staticanalysis", + "shared.django_apps.ta_timeseries", + "shared.django_apps.test_analytics", +] + +TELEMETRY_VANILLA_DB = "default" +TELEMETRY_TIMESCALE_DB = "timeseries" + +SKIP_RISKY_MIGRATION_STEPS = get_config("migrations", "skip_risky_steps", default=False) + +MIDDLEWARE = [ + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", +] + +BUNDLE_ANALYSIS_NOTIFY_MESSAGE_TEMPLATES = ( + BASE_DIR / "services" / "bundle_analysis" / "notify" / "messages" / "templates" +) +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BUNDLE_ANALYSIS_NOTIFY_MESSAGE_TEMPLATES], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.request", + "django.contrib.messages.context_processors.messages", + ] + }, + } +] + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators +AUTH_PASSWORD_VALIDATORS = [] + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True diff --git a/apps/worker/django_scaffold/tests_settings.py b/apps/worker/django_scaffold/tests_settings.py new file mode 100644 index 0000000000..b17ab895ac --- /dev/null +++ b/apps/worker/django_scaffold/tests_settings.py @@ -0,0 +1,27 @@ +from pathlib import Path + +from shared.django_apps.dummy_settings import * + +BASE_DIR = Path(__file__).resolve().parent.parent + +BUNDLE_ANALYSIS_NOTIFY_MESSAGE_TEMPLATES = ( + BASE_DIR / "services" / "bundle_analysis" / "notify" / "messages" / "templates" +) +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BUNDLE_ANALYSIS_NOTIFY_MESSAGE_TEMPLATES], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.request", + "django.contrib.messages.context_processors.messages", + ] + }, + } +] + +DATABASES["default"]["TEST"] = {"NAME": "test_postgres_django"} +IS_ENTERPRISE = False diff --git a/apps/worker/docker-compose.yml b/apps/worker/docker-compose.yml new file mode 100644 index 0000000000..7333514769 --- /dev/null +++ b/apps/worker/docker-compose.yml @@ -0,0 +1,62 @@ +version: "3" + +services: + worker: + image: ${WORKER_DOCKER_REPO}:${WORKER_DOCKER_VERSION} + depends_on: + - postgres + - redis + - timescale + - minio + volumes: + - ./:/app/apps/worker + - ./docker/test_codecov_config.yml:/config/codecov.yml + environment: + # Improves pytest-cov performance in python 3.12 + # https://github.com/nedbat/coveragepy/issues/1665#issuecomment-1937075835 + - COVERAGE_CORE=sysmon + env_file: + - .testenv + command: + - sleep + - infinity + + postgres: + image: postgres:14-alpine + environment: + - POSTGRES_USER=postgres + - POSTGRES_HOST_AUTH_METHOD=trust + - POSTGRES_PASSWORD=password + volumes: + - type: tmpfs + target: /var/lib/postgresql/data + tmpfs: + size: 2048M + + timescale: + image: timescale/timescaledb-ha:pg14-latest + environment: + - POSTGRES_USER=postgres + - POSTGRES_HOST_AUTH_METHOD=trust + - POSTGRES_PASSWORD=password + volumes: + - ./docker/init_db.sql:/docker-entrypoint-initdb.d/init_db.sql + + minio: + image: minio/minio:latest + command: server /export + ports: + - "${MINIO_PORT:-9000}:9000" + environment: + - MINIO_ACCESS_KEY=codecov-default-key + - MINIO_SECRET_KEY=codecov-default-secret + volumes: + - type: tmpfs + target: /export + tmpfs: + size: 256M + redis: + image: redis:6-alpine + + mailhog: + image: mailhog/mailhog:latest diff --git a/apps/worker/docker/Dockerfile b/apps/worker/docker/Dockerfile new file mode 100644 index 0000000000..a91ce0ab69 --- /dev/null +++ b/apps/worker/docker/Dockerfile @@ -0,0 +1,34 @@ +# syntax=docker/dockerfile:1.4 +ARG REQUIREMENTS_IMAGE +ARG BUILD_ENV=self-hosted +ARG BERGLAS_VERSION=2.0.6 + +FROM us-docker.pkg.dev/berglas/berglas/berglas:$BERGLAS_VERSION as berglas + +FROM $REQUIREMENTS_IMAGE as app +WORKDIR /app/apps/worker +ADD . /app/apps/worker +RUN chmod +x worker.sh +ARG RELEASE_VERSION +ENV RELEASE_VERSION=$RELEASE_VERSION +ENTRYPOINT ["./worker.sh"] + +FROM app as local + +FROM app as cloud +COPY --chmod=755 --from=berglas /bin/berglas /usr/local/bin/berglas + +FROM app as self-hosted +ENV RUN_ENV="ENTERPRISE" + + +FROM self-hosted as self-hosted-runtime +USER root +ARG EXTERNAL_DEPS_FOLDER=./external_deps +RUN mkdir $EXTERNAL_DEPS_FOLDER +RUN pip install --target $EXTERNAL_DEPS_FOLDER psycopg2-binary tlslite-ng +RUN chown codecov:application $EXTERNAL_DEPS_FOLDER +USER codecov + + +FROM ${BUILD_ENV} diff --git a/apps/worker/docker/Dockerfile.requirements b/apps/worker/docker/Dockerfile.requirements new file mode 100644 index 0000000000..c3f710a2a5 --- /dev/null +++ b/apps/worker/docker/Dockerfile.requirements @@ -0,0 +1,66 @@ +# syntax=docker/dockerfile:1.4 +ARG PYTHON_IMAGE=ghcr.io/astral-sh/uv:python3.13-bookworm-slim +# BUILD STAGE +FROM $PYTHON_IMAGE as build + +RUN apt-get update +RUN apt-get install -y \ + build-essential \ + curl \ + git \ + libffi-dev \ + libpq-dev \ + libxml2-dev \ + libxslt-dev + +# Install Rust +ARG RUST_VERSION=stable +ENV RUST_VERSION=${RUST_VERSION} + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | bash -s -- -y --profile minimal --default-toolchain $RUST_VERSION +ENV PATH="/root/.cargo/bin:$PATH" + +ENV UV_LINK_MODE=copy \ + UV_COMPILE_BYTECODE=1 \ + UV_PYTHON_DOWNLOADS=never \ + UV_PYTHON=python \ + UV_PROJECT_ENVIRONMENT=/worker + +# Then, add the rest of the project source code and install it +# Installing separately from its dependencies allows optimal layer caching +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv export --no-hashes --frozen --format requirements-txt > requirements.txt + +RUN grep -v '^-e ' requirements.txt > requirements.remote.txt + +# build all remote wheels +RUN pip wheel -w wheels --find-links wheels -r requirements.remote.txt + +# build all local packages to wheels +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv build --all-packages --wheel -o wheels + + +# RUNTIME STAGE - Copy packages from build stage and install runtime dependencies +FROM $PYTHON_IMAGE + +RUN apt-get update +RUN apt-get install -y \ + libxml2-dev \ + libxslt-dev \ + make + +COPY --from=build /wheels/ /wheels/ + +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv pip install --no-deps --no-index --find-links=wheels wheels/* --system + +RUN addgroup --system application \ + && adduser --system codecov --ingroup application --home /home/codecov diff --git a/apps/worker/docker/init_db.sql b/apps/worker/docker/init_db.sql new file mode 100644 index 0000000000..92eca2ee49 --- /dev/null +++ b/apps/worker/docker/init_db.sql @@ -0,0 +1 @@ +CREATE DATABASE test_analytics; \ No newline at end of file diff --git a/apps/worker/docker/test_codecov_config.yml b/apps/worker/docker/test_codecov_config.yml new file mode 100644 index 0000000000..81487a89ff --- /dev/null +++ b/apps/worker/docker/test_codecov_config.yml @@ -0,0 +1,72 @@ +setup: + codecov_url: https://codecov.io + debug: no + loglvl: INFO + encryption_secret: "zp^P9*i8aR3" + timeseries: + enabled: true + ta_timeseries: + enabled: true + +services: + database_url: postgres://postgres:password@postgres:5432/postgres + timeseries_database_url: postgres://postgres:password@timescale:5432/postgres + redis_url: redis://redis:6379 + minio: + hash_key: testixik8qdauiab1yiffydimvi72ekq # never change this + bucket: archive + access_key_id: codecov-default-key + secret_access_key: codecov-default-secret + verify_ssl: false + port: 9000 + host: minio + smtp: + host: mailhog + port: 1025 + +github: + bot: + username: codecov-io + integration: + id: 254 + pem: src/certs/github.pem + +bitbucket: + bot: + username: codecov-io + +gitlab: + bot: + username: codecov-io + +site: + codecov: + require_ci_to_pass: yes + + coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: yes + patch: yes + changes: no + + parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + + javascript: + enable_partials: no + + comment: + layout: "reach, diff, flags, files, footer" + behavior: default + require_changes: no + require_base: no + require_head: yes diff --git a/apps/worker/enterprise/ldd b/apps/worker/enterprise/ldd new file mode 100644 index 0000000000..d7d1f21209 --- /dev/null +++ b/apps/worker/enterprise/ldd @@ -0,0 +1,13 @@ +#!/bin/sh + +# From http://wiki.musl-libc.org/wiki/FAQ#Q:_where_is_ldd_.3F +# +# Musl's dynlinker comes with ldd functionality built in. just create a +# symlink from ld-musl-$ARCH.so to /bin/ldd. If the dynlinker was started +# as "ldd", it will detect that and print the appropriate DSO information. +# +# Instead, this string replaced "ldd" with the package so that pyinstaller +# can find the actual lib. +exec /usr/bin/ldd "$@" | \ + sed -r 's/([^[:space:]]+) => ldd/\1 => \/lib\/\1/g' | \ + sed -r 's/ldd \(.*\)//g' \ No newline at end of file diff --git a/apps/worker/enterprise/package.sh b/apps/worker/enterprise/package.sh new file mode 100644 index 0000000000..18a780e0c7 --- /dev/null +++ b/apps/worker/enterprise/package.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Simple wrapper around pyinstaller + +set -e +set -x + +# Generate a random key for encryption +random_key=$(pwgen -s 16 1) +pyinstaller_args="${@/--random-key/--key $random_key}" + +# Use the hacked ldd to fix libc.musl-x86_64.so.1 location +PATH="/pyinstaller:$PATH" + +hiddenimport=$(python -c " +from glob import glob +import celery + +base = celery.__file__.rsplit('/', 1)[0] +print( + ' '.join( + [ + '--hiddenimport celery' + + file.replace(base, '').replace('.py', '').replace('/', '.') + for file in (glob(base + '/*.py') + glob(base + '/**/*.py')) + ] + ) +) +print(' --hiddenimport pkg_resources.py2_warn') +print(' --hiddenimport kombu.transport.pyamqp') +print(' --hiddenimport celery.worker.consumer') +print(' --hiddenimport sqlalchemy.ext.baked') +print(' --hiddenimport tasks') +print(' --hiddenimport tornado.curl_httpclient') +print(' --hiddenimport asyncore') +print(' --hiddenimport imaplib') +print(' --hiddenimport poplib') +print(' --hiddenimport smtplib') +print(' --hiddenimport xmlrpc.server') +") + +mkdir src +echo 'true' > src/is_enterprise + +# Exclude pycrypto and PyInstaller from built packages +pyinstaller -F \ + --exclude-module pycrypto \ + --exclude-module PyInstaller \ + --exclude-module psycopg2 \ + --exclude-module tlslite \ + --additional-hooks-dir /pyinstaller/hooks \ + ${hiddenimport} \ + ${pyinstaller_args} \ + --paths /worker \ + /worker/enterprise.py + +# cat enterprise.spec + +# Clean up +mv /worker/dist/enterprise / +cd / +rm -rf /home/* +rm -rf /worker +mv /enterprise /home +rm -rf /pyinstaller +rm -rf /usr/local/lib/python3.8/site-packages diff --git a/apps/worker/generated_proto/testrun/__init__.py b/apps/worker/generated_proto/testrun/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/generated_proto/testrun/ta_testrun_pb2.py b/apps/worker/generated_proto/testrun/ta_testrun_pb2.py new file mode 100644 index 0000000000..04e4f6247e --- /dev/null +++ b/apps/worker/generated_proto/testrun/ta_testrun_pb2.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: ta_testrun.proto +# Protobuf Python Version: 5.29.2 +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, 5, 29, 2, "", "ta_testrun.proto" +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x10ta_testrun.proto"\xa4\x03\n\x07TestRun\x12\x11\n\ttimestamp\x18\x01 \x01(\x03\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x11\n\tclassname\x18\x03 \x01(\t\x12\x11\n\ttestsuite\x18\x04 \x01(\t\x12\x15\n\rcomputed_name\x18\x05 \x01(\t\x12!\n\x07outcome\x18\x06 \x01(\x0e\x32\x10.TestRun.Outcome\x12\x17\n\x0f\x66\x61ilure_message\x18\x07 \x01(\t\x12\x18\n\x10\x64uration_seconds\x18\x08 \x01(\x02\x12\x0e\n\x06repoid\x18\n \x01(\x03\x12\x12\n\ncommit_sha\x18\x0b \x01(\t\x12\x13\n\x0b\x62ranch_name\x18\x0c \x01(\t\x12\r\n\x05\x66lags\x18\r \x03(\t\x12\x10\n\x08\x66ilename\x18\x0e \x01(\t\x12\x11\n\tframework\x18\x0f \x01(\t\x12\x11\n\tupload_id\x18\x10 \x01(\x03\x12\x12\n\nflags_hash\x18\x11 \x01(\x0c\x12\x0f\n\x07test_id\x18\x12 \x01(\x0c"@\n\x07Outcome\x12\n\n\x06PASSED\x10\x00\x12\n\n\x06\x46\x41ILED\x10\x01\x12\x0b\n\x07SKIPPED\x10\x02\x12\x10\n\x0c\x46LAKY_FAILED\x10\x03' +) + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "ta_testrun_pb2", _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals["_TESTRUN"]._serialized_start = 21 + _globals["_TESTRUN"]._serialized_end = 441 + _globals["_TESTRUN_OUTCOME"]._serialized_start = 377 + _globals["_TESTRUN_OUTCOME"]._serialized_end = 441 +# @@protoc_insertion_point(module_scope) diff --git a/apps/worker/helpers/__init__.py b/apps/worker/helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/helpers/backfills.py b/apps/worker/helpers/backfills.py new file mode 100644 index 0000000000..34313ee208 --- /dev/null +++ b/apps/worker/helpers/backfills.py @@ -0,0 +1,69 @@ +import logging + +import shared.torngit as torngit +from asgiref.sync import async_to_sync +from sqlalchemy.orm.session import Session + +from database.models.core import GithubAppInstallation, Repository + +log = logging.getLogger(__name__) + + +# GH App Backfills +# Looping and adding all repositories in the installation app +def add_repos_service_ids_from_provider( + db_session: Session, + ownerid: int, + owner_service: torngit.base.TorngitBaseAdapter, + gh_app_installation: GithubAppInstallation, +): + # TODO: Convert this to the generator function + repos = async_to_sync(owner_service.list_repos_using_installation)() + + if repos: + # Fetching all repos service ids we have for that owner in the DB + repo_service_ids_in_db = [ + repo.service_id + for repo in db_session.query(Repository.service_id) + .filter_by(ownerid=ownerid) + .all() + ] + + # Add service ids from provider that we have DB records for to a list + new_repo_service_ids = [] + for repo in repos: + repo_data = repo["repo"] + service_id = repo_data["service_id"] + if service_id and service_id in repo_service_ids_in_db: + new_repo_service_ids.append(service_id) + log.info( + "Added the following repo service ids to this gh app installation", + extra=dict( + ownerid=ownerid, + installation_id=gh_app_installation.installation_id, + new_repo_service_ids=new_repo_service_ids, + ), + ) + gh_app_installation.repository_service_ids = new_repo_service_ids + db_session.commit() + + +# Check if gh selection is set to all and act accordingly +def maybe_set_installation_to_all_repos( + db_session: Session, + owner_service, + gh_app_installation: GithubAppInstallation, +): + remote_gh_app_installation = async_to_sync(owner_service.get_gh_app_installation)( + installation_id=gh_app_installation.installation_id + ) + repository_selection = remote_gh_app_installation.get("repository_selection", "") + if repository_selection == "all": + gh_app_installation.repository_service_ids = None + db_session.commit() + log.info( + "Selection is set to all, no installation is needed", + extra=dict(ownerid=gh_app_installation.ownerid), + ) + return True + return False diff --git a/apps/worker/helpers/checkpoint_logger/__init__.py b/apps/worker/helpers/checkpoint_logger/__init__.py new file mode 100644 index 0000000000..1d426d2fdd --- /dev/null +++ b/apps/worker/helpers/checkpoint_logger/__init__.py @@ -0,0 +1,520 @@ +""" +`checkpoint_logger` is a module that tracks latencies/reliabilities for higher-level +"flows" that don't map well to auto-instrumented tracing. It serializes its data +between tasks allowing you to begin a flow on one host and log its completion on +another (as long as clock drift is marginal). + +See `UploadFlow` for an example of defining a flow. It's recommended that you +define your flow with the decorators in this file: +- `@success_events()`, `@failure_events()`: designate some events as terminal + success/fail states of your flow. +- `@subflows()`: pre-define subflows that get submitted automatically; implicitly + define a flow from the first event to each success or failure event +- `@reliability_counters`: increment event, start, finish, success, failure counters + for defining reliability metrics for your flow + +It is expected that `@subflows()` and `@reliability_counters` be invoked **after** +`@success_events()` and/or `@failure_events`. + + # Simple usage + UploadFlow.log(UploadFlow.BEGIN) + ... + # each function returns the flow so you can chain `log` calls. + UploadFlow + .log(UploadFlow.PROCESSING_COMPLETE) + .log(UploadFlow.SKIPPING_NOTIFICATION) + + + # More complicated usage + # - Loads data from a previous task that was passed in `kwargs` + # - Logs `UploadFlow.BEGIN` directly into `kwargs` to pass to the next task + # - Ignores if `UploadFlow.BEGIN` was already logged (i.e. if this is a task retry) + from_kwargs([UploadFlow], kwargs) + UploadFlow.log(UploadFlow.BEGIN, kwargs=kwargs, ignore_repeat=True) + next_task(kwargs) + ... + # when using `@failure_events()` and `@subflows()`, an auto-created subflow + # is automatically submitted because `UploadFlow.TOO_MANY_RETRIES` is an error + from_kwargs(UploadFlow, kwargs) + .log(UploadFlow.TOO_MANY_RETRIES) +""" + +import functools +import itertools +import logging +import time +from enum import Enum +from typing import ( + Any, + Callable, + ClassVar, + Iterable, + Mapping, + MutableMapping, + Optional, + TypeAlias, + TypeVar, +) + +import sentry_sdk + +from helpers.checkpoint_logger.prometheus import PROMETHEUS_HANDLER +from helpers.log_context import get_log_context, set_log_context + +logger = logging.getLogger(__name__) + +T = TypeVar("T", bound="BaseFlow") +TSubflows: TypeAlias = Mapping[T, Iterable[tuple[str, T]]] + + +def _error(msg, flow, strict=False): + # When a new version of worker rolls out, it will pick up tasks that + # may have been enqueued by the old worker and be missing checkpoints + # data. At least for that reason, we want to allow failing softly. + PROMETHEUS_HANDLER.log_errors(flow=flow.__name__) + if strict: + raise ValueError(msg) + else: + logger.warning(msg) + + +class BaseFlow(str, Enum): + """ + Base class for a flow. Defines optional functions which are added by the + @success_events, @failure_events, @subflows, and @reliability_counters + decorators to (mostly) appease mypy. + + Inherits from `str` so a dictionary of checkpoints data can be serialized + between worker tasks. It overrides sort order functions so that it follows + enum declaration order instead of lexicographic order. + """ + + _subflows: Callable[[], TSubflows] + _success_events: Callable[[], Iterable[T]] + _failure_events: Callable[[], Iterable[T]] + is_success: ClassVar[Callable[[T], bool]] + is_failure: ClassVar[Callable[[T], bool]] + log_counters: ClassVar[Callable[[T], None]] + + def _generate_next_value_( + name: str, start: int, count: int, last_values: list[Any] + ): # type: ignore[override] + """ + This powers `enum.auto()`. It sets the value of "MyEnum.A" to "A". + """ + return name + + def __eq__(self, other: object) -> bool: + if isinstance(other, self.__class__): + return self.__class__._member_names_.index( + self.name + ) == self.__class__._member_names_.index(other.name) + return NotImplemented + + def __gt__(self, other: object) -> bool: + if isinstance(other, self.__class__): + return self.__class__._member_names_.index( + self.name + ) > self.__class__._member_names_.index(other.name) + return NotImplemented + + def __ge__(self, other: object) -> bool: + if isinstance(other, self.__class__): + return self == other or self > other + return NotImplemented + + def __lt__(self, other: object) -> bool: + if isinstance(other, self.__class__): + return (not self == other) and (not self > other) + return NotImplemented + + def __le__(self, other: object) -> bool: + if isinstance(other, self.__class__): + return self == other or self < other + return NotImplemented + + def __hash__(self): + return hash(self.name) + + @classmethod + def beginning(cls: type[T]) -> T: + return next(iter(cls.__members__.values())) + + @classmethod + def has_begun(cls: type[T]) -> bool: + return cls.beginning() in cls._data_from_log_context() + + @classmethod + def has_ended(cls: type[T]) -> bool: + return cls.final() in cls._data_from_log_context() + + @classmethod + def final(cls: type[T]) -> T: + *_, final = iter(cls.__members__.values()) + return final + + def is_beginning(self): + return self == self.beginning() + + @classmethod + def _validate_checkpoint(cls: type[T], checkpoint: T) -> None: + if checkpoint.__class__ != cls: + # This error is not ignored when `strict==False` because it's definitely + # a code mistake + raise ValueError( + f"Checkpoint {checkpoint} not part of flow `{cls.__name__}`" + ) + + @classmethod + def _data_from_log_context(cls: type[T]) -> Mapping[T, int]: + return get_log_context().checkpoints_data.get(_kwargs_key(cls), {}) + + @classmethod + def _save_to_log_context(cls: type[T], data: Mapping[T, int]): + log_context = get_log_context() + log_context.checkpoints_data[_kwargs_key(cls)] = data + set_log_context(log_context) + + @classmethod + def save_to_kwargs(cls: type[T], kwargs: dict): + data = cls._data_from_log_context() + if data: + kwargs[_kwargs_key(cls)] = data + return kwargs + + @classmethod + def log( + cls: type[T], + checkpoint: T, + ignore_repeat: bool = False, + kwargs: Optional[MutableMapping[str, Any]] = None, + strict: bool = False, + ) -> type[T]: + cls._validate_checkpoint(checkpoint) + + if not cls.has_begun() and not checkpoint.is_beginning(): + _error( + f"Tried to log checkpoint {checkpoint} before the flow began", + cls, + strict=strict, + ) + return cls + + is_failure = hasattr(checkpoint, "is_failure") and checkpoint.is_failure() + is_success = hasattr(checkpoint, "is_success") and checkpoint.is_success() + is_terminal = is_failure or is_success + if is_terminal and cls.has_ended(): + _error( + f"Tried to log terminal checkpoint {checkpoint} after the flow ended", + cls, + strict=strict, + ) + return cls + + data = cls._data_from_log_context() + if checkpoint in data: + if not ignore_repeat: + _error(f"Already recorded checkpoint {checkpoint}", cls, strict=strict) + return cls + + timestamp = _get_milli_timestamp() + data[checkpoint] = timestamp + if is_terminal: + data[cls.final()] = timestamp + cls._save_to_log_context(data) + + if kwargs is not None: + cls.save_to_kwargs(kwargs) + + # `cls._subflows()` comes from the `@subflows` decorator + # If the flow has pre-defined subflows, we can automatically submit + # any of them that end with the checkpoint we just logged. + if hasattr(cls, "_subflows"): + for metric, beginning in cls._subflows().get(checkpoint, []): # type: ignore[operator] + cls.submit_subflow(metric, beginning, checkpoint, data=data) + + # `checkpoint.log_counters()` comes from the `@reliability_counters` + # decorator + # Increment event, start, finish, success, failure counters + if hasattr(checkpoint, "log_counters"): + checkpoint.log_counters() + + return cls + + @classmethod + def _subflow_duration( + cls: type[T], start: T, end: T, data: Mapping[T, int], strict=False + ) -> Optional[int]: + cls._validate_checkpoint(start) + cls._validate_checkpoint(end) + if start not in data: + _error( + f"Cannot compute duration; missing start checkpoint {start}", + cls, + strict=strict, + ) + return None + elif end not in data: + _error( + f"Cannot compute duration; missing end checkpoint {end}", + cls, + strict=strict, + ) + return None + elif end <= start: + # This error is not ignored when `self.strict==False` because it's definitely + # a code mistake + raise ValueError( + f"Cannot compute duration; end {end} is not after start {start}" + ) + + return data[end] - data[start] + + @classmethod + def submit_subflow( + cls: type[T], metric: str, start: T, end: T, data: Mapping[T, int], strict=False + ) -> type[T]: + duration = cls._subflow_duration(start, end, data, strict) + if duration: + sentry_sdk.set_measurement(metric, duration, "milliseconds") + duration_in_seconds = duration / 1000 + PROMETHEUS_HANDLER.log_subflow( + flow=cls.__name__, subflow=metric, duration=duration_in_seconds + ) + + return cls + + +TClassDecorator: TypeAlias = Callable[[type[T]], type[T]] + + +def failure_events(*args: str) -> TClassDecorator: + """ + Class decorator that designates some events as terminal failure conditions. + + @failure_events('ERROR') + class MyEnum(str, Enum): + BEGIN: auto() + CHECKPOINT: auto() + ERROR: auto() + FINISHED: auto() + assert MyEnum.ERROR.is_failure() + """ + + def class_decorator(klass: type[T]) -> type[T]: + def _failure_events() -> Iterable[T]: + return {v for k, v in klass.__members__.items() if k in args} + + def is_failure(obj: T) -> bool: + return obj in _failure_events() + + # `_failure_events` is a cached function rather than a data member so + # that it is not processed as if it's a value from the enum. + klass._failure_events = functools.lru_cache(maxsize=1)(_failure_events) + klass.is_failure = is_failure + + return klass + + return class_decorator + + +def success_events(*args: str) -> TClassDecorator: + """ + Class decorator that designates some events as terminal success conditions. + + @success_events('FINISHED') + class MyEnum(str, Enum): + BEGIN: auto() + CHECKPOINT: auto() + ERROR: auto() + FINISHED: auto() + assert MyEnum.FINISHED.is_success() + """ + + def class_decorator(klass: type[T]) -> type[T]: + def _success_events() -> Iterable[T]: + return {v for k, v in klass.__members__.items() if k in args} + + def is_success(obj: T) -> bool: + return obj in _success_events() + + # `_success_events` is a cached function rather than a data member so + # that it is not processed as if it's a value from the enum. + klass._success_events = functools.lru_cache(maxsize=1)(_success_events) + klass.is_success = is_success + + return klass + + return class_decorator + + +def subflows(*args: tuple[str, str, str]) -> TClassDecorator: + """ + Class decorator that defines a set of interesting subflows which should be + logged as well as the name each should be logged with. It is expected that + you invoke this **after** @success_events() and/or @failure_events(). + + @success_events('FINISH') + @subflows( + ('first_subflow', 'BEGIN', 'CHECKPOINT_A'), + ('second_subflow', 'CHECKPOINT_A', 'FINISH') + ) + class MyEnum(str, Enum): + BEGIN: auto() + CHECKPOINT: auto() + ERROR: auto() + FINISHED: auto() + + A subflow from the first event to each terminal event (success and failure) is + created implicitly with names like 'MyEnum_BEGIN_to_FINISHED'. This name can be + overridden by defining the subflow explicitly. + """ + + def class_decorator(klass: type[T]) -> type[T]: + def _subflows() -> TSubflows: + # We get our subflows in the form: [(metric, begin, end)] + # We want them in the form: {end: [(metric, begin)]} + # The first step of munging is to group by end + def key_on_end(x): + return x[2] + + sorted_by_end = sorted(args, key=key_on_end) + grouped_by_end = itertools.groupby(args, key=key_on_end) + + enum_vals = klass.__members__ + + subflows = {} + for end, group in grouped_by_end: + # grouped_by_end is not a simple dict so we create our own. + # `begin` and `end` are still strings at this point so we also want to convert + # them to enum values. + subflows[enum_vals[end]] = list( + ((metric, enum_vals[begin]) for metric, begin, _ in group) + ) + + # The first enum value is the beginning of the flow, no matter what + # branches it takes. We want to automatically define a subflow from + # this beginning to each terminal checkpoint (failures/successes) + # unless the user provided one already. + flow_begin = next(iter(enum_vals.values())) + + # `klass._failure_events` comes from the `@failure_events` decorator + if hasattr(klass, "_failure_events"): + # mypy thinks klass._failure_events == klass + for end in klass._failure_events(): # type: ignore[operator] + flows_ending_here = subflows.setdefault( + end, [] + ) # [(metric, begin)] + if not any((x[1] == flow_begin for x in flows_ending_here)): + flows_ending_here.append( + ( + f"{klass.__name__}_{flow_begin.name}_to_{end.name}", + flow_begin, + ) + ) + + # `klass._success_events` comes from the `@success_events` decorator + if hasattr(klass, "_success_events"): + # mypy thinks klass._success_events == klass + for end in klass._success_events(): # type: ignore[operator] + flows_ending_here = subflows.setdefault( + end, [] + ) # [(metric, begin)] + if not any((x[1] == flow_begin for x in flows_ending_here)): + flows_ending_here.append( + ( + f"{klass.__name__}_{flow_begin.name}_to_{end.name}", + flow_begin, + ) + ) + + return subflows + + klass._subflows = functools.lru_cache(maxsize=1)(_subflows) + return klass + + return class_decorator + + +def reliability_counters(klass: type[T]) -> type[T]: + """ + Class decorator that enables computing success/failure rates for a flow. It + is expected that you invoke this **after** @success_events and/or + @failure_events. + + @success_events('FINISHED') + @failure_events('ERROR') + @reliability_counters + class MyEnum(str, Enum): + BEGIN: auto() + CHECKPOINT: auto() + ERROR: auto() + FINISHED: auto() + MyEnum.BEGIN.log_counters() # increments "MyEnum.begun" counter + MyEnum.ERROR.log_counters() # increments "MyEnum.failed" counter + MyEnum.FINISHED.log_counters() # increments "MyEnum.succeeded" counter + + A "MyEnum.ended" counter is incremented for both success and failure events. + This counter can be compared to "MyEnum.begun" to detect if any branches + aren't instrumented. + """ + + def log_counters(obj: T) -> None: + PROMETHEUS_HANDLER.log_checkpoints(flow=klass.__name__, checkpoint=obj.name) + + # If this is the first checkpoint, increment the number of flows we've begun + if obj == next(iter(klass.__members__.values())): + PROMETHEUS_HANDLER.log_begun(flow=klass.__name__) + return + + is_failure = hasattr(obj, "is_failure") and obj.is_failure() + is_success = hasattr(obj, "is_success") and obj.is_success() + is_terminal = is_failure or is_success + + if is_failure: + PROMETHEUS_HANDLER.log_failure(flow=klass.__name__) + elif is_success: + PROMETHEUS_HANDLER.log_success(flow=klass.__name__) + + if is_terminal: + PROMETHEUS_HANDLER.log_total_ended(flow=klass.__name__) + + klass.log_counters = log_counters + return klass + + +def _get_milli_timestamp() -> int: + return time.time_ns() // 1000000 + + +def _kwargs_key(cls: type[T]) -> str: + return f"checkpoints_{cls.__name__}" + + +def from_kwargs( + flows: list[type[T]], kwargs: MutableMapping[str, Any], strict: bool = False +): + checkpoints_data = {} + for cls in flows: + kwargs_key = _kwargs_key(cls) + data = kwargs.get(kwargs_key, {}) + + # kwargs has been deserialized into a Python dictionary, but our enum values + # are deserialized as simple strings. We need to ensure the strings are all + # proper enum values as best we can, and then downcast to enum instances. + checkpoints_data[kwargs_key] = {} + for checkpoint, timestamp in data.items(): + try: + checkpoints_data[kwargs_key][cls(checkpoint)] = timestamp + except ValueError: + _error( + f"Checkpoint {checkpoint} not part of flow `{cls.__name__}`", + cls, + strict, + ) + checkpoints_data[kwargs_key] = {} + break + + log_context = get_log_context() + log_context.checkpoints_data = checkpoints_data + set_log_context(log_context) diff --git a/apps/worker/helpers/checkpoint_logger/flows.py b/apps/worker/helpers/checkpoint_logger/flows.py new file mode 100644 index 0000000000..9d4b78ec7a --- /dev/null +++ b/apps/worker/helpers/checkpoint_logger/flows.py @@ -0,0 +1,103 @@ +from enum import auto + +from helpers.checkpoint_logger import ( + BaseFlow, + failure_events, + reliability_counters, + subflows, + success_events, +) + + +@failure_events( + "TOO_MANY_RETRIES", + "FINISHER_LOCK_ERROR", + "NOTIF_LOCK_ERROR", + "NOTIF_NO_VALID_INTEGRATION", + "NOTIF_GIT_CLIENT_ERROR", + "NOTIF_GIT_SERVICE_ERROR", + "NOTIF_TOO_MANY_RETRIES", + "NOTIF_NO_APP_INSTALLATION", + "NOTIF_ERROR_NO_REPORT", + "NOTIFIED_ERROR", + "ERROR_NOTIFYING_ERROR", + "UNCAUGHT_RETRY_EXCEPTION", + "CELERY_FAILURE", + "CELERY_TIMEOUT", +) +@success_events( + "SKIPPING_NOTIFICATION", + "NOTIFIED", + "NO_PENDING_JOBS", + "NOTIF_STALE_HEAD", + "NO_REPORTS_FOUND", +) +@subflows( + ("time_before_processing", "UPLOAD_TASK_BEGIN", "PROCESSING_BEGIN"), + ("initial_processing_duration", "PROCESSING_BEGIN", "INITIAL_PROCESSING_COMPLETE"), + ( + "batch_processing_duration", + "INITIAL_PROCESSING_COMPLETE", + "BATCH_PROCESSING_COMPLETE", + ), + ("total_processing_duration", "PROCESSING_BEGIN", "PROCESSING_COMPLETE"), + ("notification_latency", "UPLOAD_TASK_BEGIN", "NOTIFIED"), + ("error_notification_latency", "UPLOAD_TASK_BEGIN", "NOTIFIED_ERROR"), +) +@reliability_counters +class UploadFlow(BaseFlow): + UPLOAD_TASK_BEGIN = auto() + NO_PENDING_JOBS = auto() + NO_REPORTS_FOUND = auto() + TOO_MANY_RETRIES = auto() + PROCESSING_BEGIN = auto() + INITIAL_PROCESSING_COMPLETE = auto() + BATCH_PROCESSING_COMPLETE = auto() + PROCESSING_COMPLETE = auto() + SKIPPING_NOTIFICATION = auto() + NOTIFIED = auto() + NOTIFIED_ERROR = auto() + ERROR_NOTIFYING_ERROR = auto() + FINISHER_LOCK_ERROR = auto() + NOTIF_LOCK_ERROR = auto() + NOTIF_NO_VALID_INTEGRATION = auto() + NOTIF_GIT_CLIENT_ERROR = auto() + NOTIF_GIT_SERVICE_ERROR = auto() + NOTIF_TOO_MANY_RETRIES = auto() + NOTIF_STALE_HEAD = auto() + NOTIF_NO_APP_INSTALLATION = auto() + NOTIF_ERROR_NO_REPORT = auto() + UNCAUGHT_RETRY_EXCEPTION = auto() + CELERY_FAILURE = auto() + CELERY_TIMEOUT = auto() + + # Sentinel checkpoint - not directly used + FINAL = auto() + + +@failure_events( + "TEST_RESULTS_ERROR", "UNCAUGHT_RETRY_EXCEPTION", "CELERY_FAILURE", "CELERY_TIMEOUT" +) +@success_events("TEST_RESULTS_NOTIFY") +@subflows( + ("test_results_notification_latency", "TEST_RESULTS_BEGIN", "TEST_RESULTS_NOTIFY"), + ("flake_notification_latency", "TEST_RESULTS_BEGIN", "FLAKE_DETECTION_NOTIFY"), + ( + "test_results_processing_time", + "TEST_RESULTS_BEGIN", + "TEST_RESULTS_FINISHER_BEGIN", + ), +) +@reliability_counters +class TestResultsFlow(BaseFlow): + TEST_RESULTS_BEGIN = auto() + TEST_RESULTS_NOTIFY = auto() + FLAKE_DETECTION_NOTIFY = auto() + TEST_RESULTS_ERROR = auto() + TEST_RESULTS_FINISHER_BEGIN = auto() + UNCAUGHT_RETRY_EXCEPTION = auto() + CELERY_FAILURE = auto() + CELERY_TIMEOUT = auto() + + # Sentinel checkpoint - not directly used + FINAL = auto() diff --git a/apps/worker/helpers/checkpoint_logger/prometheus.py b/apps/worker/helpers/checkpoint_logger/prometheus.py new file mode 100644 index 0000000000..15bf5b8ca9 --- /dev/null +++ b/apps/worker/helpers/checkpoint_logger/prometheus.py @@ -0,0 +1,174 @@ +from shared.metrics import Counter, Histogram + +from helpers.log_context import get_log_context +from rollouts import CHECKPOINT_ENABLED_REPOSITORIES + +_subflow_buckets = [ + 0.05, + 0.1, + 0.5, + 1, + 2, + 5, + 10, + 30, + 60, + 120, + 180, + 300, + 600, + 900, + 1200, + 1800, + 2400, + 3600, +] + +# Main Counter +CHECKPOINTS_TOTAL_BEGUN = Counter( + "worker_checkpoints_begun", + "Total number of times a flow's first checkpoint was logged.", + ["flow"], +) +CHECKPOINTS_TOTAL_SUCCEEDED = Counter( + "worker_checkpoints_succeeded", + "Total number of times one of a flow's success checkpoints was logged.", + ["flow"], +) +CHECKPOINTS_TOTAL_FAILED = Counter( + "worker_checkpoints_failed", + "Total number of times one of a flow's failure checkpoints was logged.", + ["flow"], +) +CHECKPOINTS_TOTAL_ENDED = Counter( + "worker_checkpoints_ended", + "Total number of times one of a flow's terminal checkpoints (success or failure) was logged.", + ["flow"], +) +CHECKPOINTS_ERRORS = Counter( + "worker_checkpoints_errors", + "Total number of errors while trying to log checkpoints", + ["flow"], +) +CHECKPOINTS_EVENTS = Counter( + "worker_checkpoints_events", + "Total number of checkpoints logged.", + ["flow", "checkpoint"], +) +CHECKPOINTS_SUBFLOW_DURATION = Histogram( + "worker_checkpoints_subflow_duration_seconds", + "Duration of subflows in seconds.", + ["flow", "subflow"], + buckets=_subflow_buckets, +) + +# Repo Counters +REPO_CHECKPOINTS_TOTAL_BEGUN = Counter( + "worker_repo_checkpoints_begun", + "Total number of times a flow's first checkpoint was logged. Labeled with a repo id, but only used for select repos.", + ["flow", "repoid"], +) +REPO_CHECKPOINTS_TOTAL_SUCCEEDED = Counter( + "worker_repo_checkpoints_succeeded", + "Total number of times one of a flow's success checkpoints was logged. Labeled with a repo id, but only used for select repos.", + ["flow", "repoid"], +) +REPO_CHECKPOINTS_TOTAL_FAILED = Counter( + "worker_repo_checkpoints_failed", + "Total number of times one of a flow's failure checkpoints was logged. Labeled with a repo id, but only used for select repos.", + ["flow", "repoid"], +) +REPO_CHECKPOINTS_TOTAL_ENDED = Counter( + "worker_repo_checkpoints_ended", + "Total number of times one of a flow's terminal checkpoints (success or failure) was logged. Labeled with a repo id, but only used for select repos.", + ["flow", "repoid"], +) +REPO_CHECKPOINTS_ERRORS = Counter( + "worker_repo_checkpoints_errors", + "Total number of errors while trying to log checkpoints. Labeled with a repo id, but only used for select repos.", + ["flow", "repoid"], +) +REPO_CHECKPOINTS_EVENTS = Counter( + "worker_repo_checkpoints_events", + "Total number of checkpoints logged. Labeled with a repo id, but only used for select repos.", + ["flow", "checkpoint", "repoid"], +) +REPO_CHECKPOINTS_SUBFLOW_DURATION = Histogram( + "worker_repo_checkpoints_subflow_duration_seconds", + "Duration of subflows in seconds. Labeled with a repo id, but only used for select repos.", + ["flow", "subflow", "repoid"], + buckets=_subflow_buckets, +) + + +class PrometheusCheckpointLoggerHandler: + """ + PrometheusCheckpointLoggerHandler is a class that is responsible for all + Prometheus related logs. This checkpoint logic is responsible for logging + metrics to any checkpoints we define. This class is made with the intent + of extending different checkpoints for metrics for different needs. The + methods in this class are mainly used by the CheckpointLogger class. + """ + + def log_begun(self, flow: str): + CHECKPOINTS_TOTAL_BEGUN.labels(flow=flow).inc() + context = get_log_context() + repoid = context and context.repo_id + if repoid and CHECKPOINT_ENABLED_REPOSITORIES.check_value(identifier=repoid): + REPO_CHECKPOINTS_TOTAL_BEGUN.labels(flow=flow, repoid=repoid).inc() + + def log_failure(self, flow: str): + CHECKPOINTS_TOTAL_FAILED.labels(flow=flow).inc() + context = get_log_context() + repoid = context and context.repo_id + if repoid and CHECKPOINT_ENABLED_REPOSITORIES.check_value(identifier=repoid): + REPO_CHECKPOINTS_TOTAL_FAILED.labels(flow=flow, repoid=repoid).inc() + + def log_success(self, flow: str): + CHECKPOINTS_TOTAL_SUCCEEDED.labels(flow=flow).inc() + context = get_log_context() + repoid = context and context.repo_id + if repoid and CHECKPOINT_ENABLED_REPOSITORIES.check_value(identifier=repoid): + REPO_CHECKPOINTS_TOTAL_SUCCEEDED.labels(flow=flow, repoid=repoid).inc() + + def log_total_ended(self, flow: str): + CHECKPOINTS_TOTAL_ENDED.labels(flow=flow).inc() + context = get_log_context() + repoid = context and context.repo_id + if repoid and CHECKPOINT_ENABLED_REPOSITORIES.check_value(identifier=repoid): + REPO_CHECKPOINTS_TOTAL_ENDED.labels(flow=flow, repoid=repoid).inc() + + def log_checkpoints(self, flow: str, checkpoint: str): + CHECKPOINTS_EVENTS.labels(flow=flow, checkpoint=checkpoint).inc() + context = get_log_context() + repoid = context and context.repo_id + if repoid and CHECKPOINT_ENABLED_REPOSITORIES.check_value(identifier=repoid): + REPO_CHECKPOINTS_EVENTS.labels( + flow=flow, checkpoint=checkpoint, repoid=repoid + ).inc() + + def log_errors(self, flow: str): + CHECKPOINTS_ERRORS.labels(flow=flow).inc() + context = get_log_context() + repoid = context and context.repo_id + if repoid and CHECKPOINT_ENABLED_REPOSITORIES.check_value(identifier=repoid): + REPO_CHECKPOINTS_ERRORS.labels(flow=flow, repoid=repoid).inc() + + def log_subflow( + self, + flow: str, + subflow: str, + duration: int, + ): + CHECKPOINTS_SUBFLOW_DURATION.labels(flow=flow, subflow=subflow).observe( + duration + ) + context = get_log_context() + repoid = context and context.repo_id + if repoid and CHECKPOINT_ENABLED_REPOSITORIES.check_value(identifier=repoid): + REPO_CHECKPOINTS_SUBFLOW_DURATION.labels( + flow=flow, subflow=subflow, repoid=repoid + ).observe(duration) + + +PROMETHEUS_HANDLER = PrometheusCheckpointLoggerHandler() diff --git a/apps/worker/helpers/clock.py b/apps/worker/helpers/clock.py new file mode 100644 index 0000000000..e1dbf69f96 --- /dev/null +++ b/apps/worker/helpers/clock.py @@ -0,0 +1,15 @@ +from datetime import datetime, timezone + + +def get_utc_now() -> datetime: + return datetime.now(timezone.utc) + + +def get_utc_now_as_iso_format() -> str: + return get_utc_now().isoformat() + + +def get_seconds_to_next_hour() -> int: + now = datetime.now(timezone.utc) + current_seconds = (now.minute * 60) + now.second + return 3600 - current_seconds diff --git a/apps/worker/helpers/comparison.py b/apps/worker/helpers/comparison.py new file mode 100644 index 0000000000..6b112d7d78 --- /dev/null +++ b/apps/worker/helpers/comparison.py @@ -0,0 +1,21 @@ +from shared.reports.types import ReportTotals + + +def minimal_totals(totals: ReportTotals | None) -> dict: + if totals is None: + return { + "hits": 0, + "misses": 0, + "partials": 0, + "coverage": None, + } + return { + "hits": totals.hits, + "misses": totals.misses, + "partials": totals.partials, + # ReportTotals has coverage as a string, we want float in the DB + # Also the coverage from ReportTotals is 0-100, while in the DB it's 0-1 + "coverage": ( + (float(totals.coverage) / 100) if totals.coverage is not None else None + ), + } diff --git a/apps/worker/helpers/components.py b/apps/worker/helpers/components.py new file mode 100644 index 0000000000..bea35cdc72 --- /dev/null +++ b/apps/worker/helpers/components.py @@ -0,0 +1,42 @@ +import re +from dataclasses import dataclass +from typing import List + + +@dataclass +class Component: + """ + Virtual representation of components defined in the user_schema yaml. + Definition: https://github.com/codecov/shared/pull/312/commits/c7bd48173da914bb16137526015791cb5a3c931c + """ + + component_id: str + name: str + flag_regexes: List[str] + paths: List[str] + statuses: List[dict] + + @classmethod + def from_dict(cls, component_dict): + return Component( + component_id=component_dict.get("component_id", ""), + name=component_dict.get("name", ""), + flag_regexes=component_dict.get("flag_regexes", []), + paths=component_dict.get("paths", []), + statuses=component_dict.get("statuses", []), + ) + + def get_display_name(self) -> str: + return self.name or self.component_id or "default_component" + + def get_matching_flags(self, current_flags: List[str]) -> List[str]: + ans = set() + compiled_regexes = map( + lambda flag_regex: re.compile(flag_regex), self.flag_regexes + ) + for regex_to_match in compiled_regexes: + matches_to_this_regex = filter( + lambda flag: regex_to_match.match(flag), current_flags + ) + ans.update(matches_to_this_regex) + return list(ans) diff --git a/apps/worker/helpers/config.py b/apps/worker/helpers/config.py new file mode 100644 index 0000000000..976eab1d41 --- /dev/null +++ b/apps/worker/helpers/config.py @@ -0,0 +1,22 @@ +from shared.config import get_config + + +def should_write_data_to_storage_config_check( + master_switch_key: str, is_codecov_repo: bool, repoid: int +) -> bool: + master_write_switch = get_config( + "setup", + "save_report_data_in_storage", + master_switch_key, + default=False, + ) + if master_write_switch == "restricted_access": + allowed_repo_ids = get_config( + "setup", "save_report_data_in_storage", "repo_ids", default=[] + ) + is_in_allowed_repoids = repoid in allowed_repo_ids + elif master_write_switch == "general_access": + is_in_allowed_repoids = True + else: + is_in_allowed_repoids = False + return master_write_switch and (is_codecov_repo or is_in_allowed_repoids) diff --git a/apps/worker/helpers/email.py b/apps/worker/helpers/email.py new file mode 100644 index 0000000000..b6053efcf2 --- /dev/null +++ b/apps/worker/helpers/email.py @@ -0,0 +1,14 @@ +from email.message import EmailMessage + + +class Email: + def __init__( + self, to_addr=None, from_addr=None, subject=None, text=None, html=None + ): + self.message = EmailMessage() + self.message["To"] = to_addr + self.message["From"] = from_addr + self.message["Subject"] = subject + self.message.set_content(text) + if html: + self.message.add_alternative(html, "html") diff --git a/apps/worker/helpers/environment.py b/apps/worker/helpers/environment.py new file mode 100644 index 0000000000..7cd43abf4c --- /dev/null +++ b/apps/worker/helpers/environment.py @@ -0,0 +1,65 @@ +import os +import sys +from enum import Enum +from functools import lru_cache +from pathlib import Path + +from shared.config import get_config + + +class Environment(Enum): + production = "production" + local = "local" + enterprise = "enterprise" + + +def get_current_env() -> Environment: + """ + Gets the current environment of the system + + Returns: + Environment: The current environment + """ + return _get_cached_current_env() + + +def is_enterprise() -> bool: + """Tells whether the current environment is enterprise or not + + Returns: + bool: True if the current environment is enterprise, else False + """ + return get_current_env() == Environment.enterprise + + +@lru_cache() +def _get_cached_current_env() -> Environment: + return _calculate_current_env() + + +def _get_current_folder() -> str: + return getattr(sys, "_MEIPASS", os.getcwd()) + + +def _calculate_current_env() -> Environment: + run_env = os.getenv("RUN_ENV") + if run_env is not None: + if run_env == "ENTERPRISE": + return Environment.enterprise + elif run_env == "DEV": + return Environment.local + else: + return Environment.production + os.environ["CODECOV_HOME"] = _get_current_folder() + some_dir = Path(os.getenv("CODECOV_HOME")) + if os.path.exists(some_dir / "src/is_enterprise"): + return Environment.enterprise + if os.getenv("CURRENT_ENVIRONMENT", "production") == "local": + return Environment.local + return Environment.production + + +def get_external_dependencies_folder(): + return get_config( + "services", "external_dependencies_folder", default="./external_deps" + ) diff --git a/apps/worker/helpers/exceptions.py b/apps/worker/helpers/exceptions.py new file mode 100644 index 0000000000..ad1e71236f --- /dev/null +++ b/apps/worker/helpers/exceptions.py @@ -0,0 +1,44 @@ +import shared.bots.exceptions + +RepositoryWithoutValidBotError = shared.bots.exceptions.RepositoryWithoutValidBotError +OwnerWithoutValidBotError = shared.bots.exceptions.OwnerWithoutValidBotError +RequestedGithubAppNotFound = shared.bots.exceptions.RequestedGithubAppNotFound +NoConfiguredAppsAvailable = shared.bots.exceptions.NoConfiguredAppsAvailable + + +class ReportExpiredException(Exception): + def __init__(self, message=None, filename=None) -> None: + super().__init__(message) + self.filename = filename + + +class ReportEmptyError(Exception): + pass + + +class CorruptRawReportError(Exception): + """Error indicated that report is somehow different than it should be + + Notice that this error should not be used to replace `matches_content` logic on each processor. + For header/top-level or even deeper checks that are quick and O(1), the method + `matches_content` should be used. Its purpose is to quickly look at the file and try to + determine which processor can handle it. + + This error is meant for when the report header/top-level information truly indicated the file + format was X and could be read by processorX, and then something deep down the file did not + properly match this file expected structure, and it this could not be checked beforehand + without doing some parsing as complete as the actual processing of the file + + The an example of such logic, see `VOneProcessor`. It is impractical there to check every + file dict to see if any of them do not have the proper format + + Attributes: + corruption_error (str): A short description of the unexpected issue + expected_format (str): What format the file was expcted to have. Can be an actual format + name, or some identifier for people to understand what is the right structure to follow + """ + + def __init__(self, expected_format: str, corruption_error: str): + super().__init__(expected_format, corruption_error) + self.expected_format = expected_format + self.corruption_error = corruption_error diff --git a/apps/worker/helpers/github_installation.py b/apps/worker/helpers/github_installation.py new file mode 100644 index 0000000000..202ede12df --- /dev/null +++ b/apps/worker/helpers/github_installation.py @@ -0,0 +1,33 @@ +import logging + +from database.models.core import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + Owner, + OwnerInstallationNameToUseForTask, +) + +log = logging.getLogger(__file__) + + +def get_installation_name_for_owner_for_task(task_name: str, owner: Owner) -> str: + if owner.service not in ["github", "github_enterprise"]: + # The `installation` concept only exists in GitHub. + # We still return a default here, primarily to satisfy types. + return GITHUB_APP_INSTALLATION_DEFAULT_NAME + + dbsession = owner.get_db_session() + config_for_owner = ( + dbsession.query(OwnerInstallationNameToUseForTask) + .filter( + OwnerInstallationNameToUseForTask.task_name == task_name, + OwnerInstallationNameToUseForTask.ownerid == owner.ownerid, + ) + .first() + ) + if config_for_owner: + log.info( + "Owner has dedicated app for this task", + extra=dict(this_task=task_name, ownerid=owner.ownerid), + ) + return config_for_owner.installation_name + return GITHUB_APP_INSTALLATION_DEFAULT_NAME diff --git a/apps/worker/helpers/health_check.py b/apps/worker/helpers/health_check.py new file mode 100644 index 0000000000..c762dfe31e --- /dev/null +++ b/apps/worker/helpers/health_check.py @@ -0,0 +1,27 @@ +import logging + +from shared.config import get_config + +HEALTH_CHECK_DEFAULT_INTERVAL_SECONDS = 10000 + +logger = logging.getLogger(__name__) + + +def get_health_check_interval_seconds(): + try: + interval_config = int( + get_config( + "setup", + "tasks", + "healthcheck", + "interval_seconds", + default=HEALTH_CHECK_DEFAULT_INTERVAL_SECONDS, + ) + ) + if interval_config >= 1: + return interval_config + except (ValueError, TypeError): + logger.warning( + "Invalid configuration for healthcheck interval. Using default value of 10s" + ) + return HEALTH_CHECK_DEFAULT_INTERVAL_SECONDS diff --git a/apps/worker/helpers/labels.py b/apps/worker/helpers/labels.py new file mode 100644 index 0000000000..0784409951 --- /dev/null +++ b/apps/worker/helpers/labels.py @@ -0,0 +1,73 @@ +from enum import Enum + +from shared.reports.resources import Report + +# The SpecialLabelsEnum enum is place to hold sentinels for labels with special +# meanings +# One example is CODECOV_ALL_LABELS_PLACEHOLDER: it's a sentinel for +# "all the labels from this report apply here" +# Imagine a suite, with many tests, that import a particular file +# The imports all happen before any of the tests is executed +# So on this imported file, there might be some global code +# Because it's global, it runs during this import phase +# So it's not attached directly to any test, because it ran +# outside of any tests +# But it is responsible for those tests in a way, since this global variable +# is used in the functions themselves (which run during the tests). At least +# there is no simple way to guarantee the global variable didn't affect +# anything that imported that file +# So, from the coverage perspective, this global-level line was an indirect +# part of every test. So, in the end, if we see this constant in the report +# datapoints, we will replace it with all the labels (tests) that we saw in +# that report +# This is what CODECOV_ALL_LABELS_PLACEHOLDER is + + +class SpecialLabelsEnum(Enum): + CODECOV_ALL_LABELS_PLACEHOLDER = ("Th2dMtk4M_codecov", 0) + + def __init__(self, label, index): + self.corresponding_label = label + self.corresponding_index = index + + +def get_labels_per_session(report: Report, sess_id: int) -> set[str | int]: + """Returns a Set with the labels present in a session from report, EXCLUDING the SpecialLabel. + + The return value can either be a set of strings (the labels themselves) OR + a set of ints (the label indexes). The label can be looked up from the index + using Report.lookup_label_by_id(label_id) (assuming Report._labels_idx is set) + """ + all_labels: set[str | int] = set() + + for file in report: + for _, line in file.lines: + if line.datapoints: + for datapoint in line.datapoints: + if datapoint.sessionid == sess_id: + all_labels.update(datapoint.label_ids or []) + + return all_labels - { + SpecialLabelsEnum.CODECOV_ALL_LABELS_PLACEHOLDER.corresponding_label, + SpecialLabelsEnum.CODECOV_ALL_LABELS_PLACEHOLDER.corresponding_index, + } + + +def get_all_report_labels(report: Report) -> set[str | int]: + """Returns a Set with the labels present in report EXCLUDING the SpecialLabel. + + The return value can either be a set of strings (the labels themselves) OR + a set of ints (the label indexes). The label can be looked up from the index + using Report.lookup_label_by_id(label_id) (assuming Report._labels_idx is set) + """ + all_labels: set[str | int] = set() + for file in report: + for _, line in file.lines: + if line.datapoints: + for datapoint in line.datapoints: + all_labels.update(datapoint.label_ids or []) + + return all_labels - { + SpecialLabelsEnum.CODECOV_ALL_LABELS_PLACEHOLDER.corresponding_label, + SpecialLabelsEnum.CODECOV_ALL_LABELS_PLACEHOLDER.corresponding_index, + } diff --git a/apps/worker/helpers/log_context.py b/apps/worker/helpers/log_context.py new file mode 100644 index 0000000000..2a04c5586e --- /dev/null +++ b/apps/worker/helpers/log_context.py @@ -0,0 +1,144 @@ +import contextvars +import logging +from dataclasses import asdict, dataclass, field, replace + +import sentry_sdk +from sentry_sdk import get_current_span +from shared.config import get_config + +from database.models.core import Owner, Repository + +log = logging.getLogger("log_context") + + +@dataclass +class LogContext: + """ + Class containing all the information we may want to add in logs and metrics. + """ + + task_name: str = "???" + task_id: str = "???" + + _populated_from_db = False + owner_username: str | None = None + owner_service: str | None = None + owner_plan: str | None = None + owner_id: int | None = None + repo_name: str | None = None + repo_id: int | None = None + commit_sha: str | None = None + + # TODO: begin populating this again or remove it + # we can populate this again if we reduce query load by passing the Commit, + # Owner, and Repository models we use to populate the log context into the + # various task implementations so they don't fetch the same models again + commit_id: int | None = None + + checkpoints_data: dict = field(default_factory=lambda: {}) + + @property + def sentry_trace_id(self): + if span := get_current_span(): + return span.trace_id + return None + + def as_dict(self): + d = asdict(self) + d.pop("_populated_from_db", None) + d["sentry_trace_id"] = self.sentry_trace_id + return d + + def populate_from_sqlalchemy(self, dbsession): + """ + Attempt to use the information we have to fill in other context fields. For + example, if we have `self.repo_id` but not `self.owner_id`, we can look up + the latter in the database. + + Ignore exceptions; no need to fail a task for a missing context field. + """ + if self._populated_from_db or not get_config( + "setup", "log_context", "populate", default=True + ): + return + + try: + # - commit_id (or commit_sha + repo_id) is enough to get everything else + # - however, this slams the commit table and we rarely really need the + # DB PK for a commit, so we don't do this. + # - repo_id is enough to get repo and owner + # - owner_id is just enough to get owner + + if self.repo_id: + query = ( + dbsession.query( + Repository.name, + Owner.ownerid, + Owner.username, + Owner.service, + Owner.plan, + ) + .join(Repository.owner) + .filter(Repository.repoid == self.repo_id) + ) + + ( + self.repo_name, + self.owner_id, + self.owner_username, + self.owner_service, + self.owner_plan, + ) = query.first() + + elif self.owner_id: + query = dbsession.query( + Owner.username, Owner.service, Owner.plan + ).filter(Owner.ownerid == self.owner_id) + + (self.owner_username, self.owner_service, self.owner_plan) = ( + query.first() + ) + + except Exception: + log.exception("Failed to populate log context") + + self._populated_from_db = True + + def add_to_log_record(self, log_record: dict): + d = self.as_dict() + d.pop("checkpoints_data", None) + log_record["context"] = d + + def add_to_sentry(self): + d = self.as_dict() + d.pop("sentry_trace_id", None) + d.pop("checkpoints_data", None) + sentry_sdk.set_tags(d) + + +_log_context = contextvars.ContextVar("log_context", default=LogContext()) + + +def set_log_context(context: LogContext): + """ + Overwrite whatever is currently in the log context. Also sets tags in the + Sentry SDK appropriately. + """ + _log_context.set(context) + context.add_to_sentry() + + +def update_log_context(context: dict): + """ + Add new fields to the log context without removing old ones. + """ + current_context: LogContext = _log_context.get() + new_context = replace(current_context, **context) + set_log_context(new_context) + + +def get_log_context() -> LogContext: + """ + Access the log context. + """ + return _log_context.get() diff --git a/apps/worker/helpers/logging_config.py b/apps/worker/helpers/logging_config.py new file mode 100644 index 0000000000..e61785036f --- /dev/null +++ b/apps/worker/helpers/logging_config.py @@ -0,0 +1,93 @@ +import json +from copy import deepcopy + +from pythonjsonlogger.jsonlogger import JsonFormatter + +from helpers.environment import Environment, get_current_env +from helpers.log_context import get_log_context + + +class BaseLogger(JsonFormatter): + def add_fields(self, log_record, record, message_dict) -> None: + super(BaseLogger, self).add_fields(log_record, record, message_dict) + + log_context = get_log_context() + log_context.add_to_log_record(log_record) + + def format_json_on_new_lines(self, json_str): + # Parse the input JSON string + data = json.loads(json_str) + + for key, value in data.items(): + if isinstance(value, list) and len(value) > 10: + # If more than 10 elements in a list, concat to single line + data[key] = ", ".join(map(str, value)) + + # Convert the parsed JSON data back to a formatted JSON string + formatted_json = json.dumps(data, indent=4) + return formatted_json + + +class CustomLocalJsonFormatter(BaseLogger): + def jsonify_log_record(self, log_record) -> str: + """Returns a json string of the log record.""" + levelname = log_record.pop("levelname") + message = log_record.pop("message") + exc_info = log_record.pop("exc_info", "") + content = super().jsonify_log_record(log_record) + formatted = super().format_json_on_new_lines(content) if content else None + if exc_info: + return f"{levelname}: {message} \n {formatted}\n{exc_info}" + return f"{levelname}: {message} \n {formatted}" + + +class CustomDatadogJsonFormatter(BaseLogger): + def add_fields(self, log_record, record, message_dict): + super(CustomDatadogJsonFormatter, self).add_fields( + log_record, record, message_dict + ) + if not log_record.get("logger.name") and log_record.get("name"): + log_record["logger.name"] = log_record.get("name") + if not log_record.get("logger.thread_name") and log_record.get("threadName"): + log_record["logger.thread_name"] = log_record.get("threadName") + if log_record.get("level"): + log_record["level"] = log_record["level"].upper() + else: + log_record["level"] = record.levelname + + +config_dict = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "standard": { + "format": "%(message)s %(asctime)s %(name)s %(levelname)s %(lineno)s %(pathname)s %(funcName)s %(threadName)s", + "class": "helpers.logging_config.CustomLocalJsonFormatter", + }, + "json": { + "format": "%(message)s %(asctime)s %(name)s %(levelname)s %(lineno)s %(pathname)s %(funcName)s %(threadName)s", + "class": "helpers.logging_config.CustomDatadogJsonFormatter", + }, + }, + "root": { # root logger + "handlers": ["default"], + "level": "INFO", + "propagate": True, + }, + "handlers": { + "default": { + "level": "INFO", + "formatter": "json", + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", # Default is stderr + } + }, + "loggers": {}, +} + + +def get_logging_config_dict() -> dict: + res = deepcopy(config_dict) + if get_current_env() == Environment.local: + res["handlers"]["default"]["formatter"] = "standard" + return res diff --git a/apps/worker/helpers/match.py b/apps/worker/helpers/match.py new file mode 100644 index 0000000000..4d94c59a26 --- /dev/null +++ b/apps/worker/helpers/match.py @@ -0,0 +1,30 @@ +import re +from typing import List, Optional + + +def match(patterns: Optional[List[str]], string: str) -> bool: + if patterns is None or string in patterns: + return True + + patterns = set(filter(None, patterns)) + negatives = set(filter(lambda a: a.startswith(("^!", "!")), patterns)) + positives = patterns - negatives + + # must not match + for pattern in negatives: + # matched a negative search + if re.match(pattern.replace("!", ""), string): + return False + + if positives: + for pattern in positives: + # match was found + if re.match(pattern, string): + return True + + # did not match any required paths + return False + + else: + # no positives: everyting else is ok + return True diff --git a/apps/worker/helpers/metrics.py b/apps/worker/helpers/metrics.py new file mode 100644 index 0000000000..44caad0fde --- /dev/null +++ b/apps/worker/helpers/metrics.py @@ -0,0 +1,9 @@ +from statsd.defaults.env import statsd + +KiB = 1024 +MiB = KiB * KiB + +metrics = statsd + +# enumerates all the powers-of-two, from 8K all the way to 1G: +BYTE_SIZE_BUCKETS = [2**power for power in range(13, 31)] diff --git a/apps/worker/helpers/notifier.py b/apps/worker/helpers/notifier.py new file mode 100644 index 0000000000..fa4a43b8f2 --- /dev/null +++ b/apps/worker/helpers/notifier.py @@ -0,0 +1,94 @@ +import logging +from dataclasses import dataclass +from enum import Enum +from typing import Literal + +from asgiref.sync import async_to_sync +from shared.torngit.base import TorngitBaseAdapter +from shared.torngit.exceptions import TorngitClientError + +from database.models import Commit +from services.repository import ( + EnrichedPull, + fetch_and_update_pull_request_information_from_commit, + get_repo_provider_service, +) +from services.yaml import UserYaml + +log = logging.getLogger(__name__) + + +class NotifierResult(Enum): + COMMENT_POSTED = "comment_posted" + TORNGIT_ERROR = "torngit_error" + NO_PULL = "no_pull" + NO_COMMENT = "no_comment" + + +@dataclass +class BaseNotifier: + commit: Commit + commit_yaml: UserYaml | None + _pull: EnrichedPull | None | Literal[False] = False + _repo_service: TorngitBaseAdapter | None = None + + def get_pull(self, do_log=True) -> EnrichedPull | None: + repo_service = self.get_repo_service() + + if self._pull is False: + self._pull = async_to_sync( + fetch_and_update_pull_request_information_from_commit + )(repo_service, self.commit, self.commit_yaml) + + if self._pull is None and do_log: + log.info( + "Not notifying since there is no pull request associated with this commit", + extra=dict(commitid=self.commit.commitid), + ) + + return self._pull + + def get_repo_service(self) -> TorngitBaseAdapter: + if self._repo_service is None: + self._repo_service = get_repo_provider_service(self.commit.repository) + + return self._repo_service + + def send_to_provider(self, pull: EnrichedPull, message: str) -> bool: + repo_service = self.get_repo_service() + assert repo_service + + pullid = pull.database_pull.pullid + try: + comment_id = pull.database_pull.commentid + if comment_id: + async_to_sync(repo_service.edit_comment)(pullid, comment_id, message) + else: + res = async_to_sync(repo_service.post_comment)(pullid, message) + pull.database_pull.commentid = res["id"] + return True + except TorngitClientError: + log.error( + "Error creating/updating PR comment", + extra=dict( + commitid=self.commit.commitid, + pullid=pullid, + ), + ) + return False + + def build_message(self) -> str: + raise NotImplementedError + + def notify(self) -> NotifierResult: + pull = self.get_pull() + if pull is None: + return NotifierResult.NO_PULL + + message = self.build_message() + + sent_to_provider = self.send_to_provider(pull, message) + if sent_to_provider == False: + return NotifierResult.TORNGIT_ERROR + + return NotifierResult.COMMENT_POSTED diff --git a/apps/worker/helpers/number.py b/apps/worker/helpers/number.py new file mode 100644 index 0000000000..36ce841bcc --- /dev/null +++ b/apps/worker/helpers/number.py @@ -0,0 +1,19 @@ +from decimal import ROUND_CEILING, ROUND_FLOOR, ROUND_HALF_EVEN, Decimal + + +def precise_round( + number: Decimal, precision: int = 2, rounding: str = "down" +) -> Decimal: + """ + Helper function to do more precise rounding given a precision and rounding strategy. + :param number: Number to round + :param precision: The number of decimal places to round to + :param rounding: Rounding strategy to use, which can be "down", "up" or "nearest" + :return: The rounded number as a Decimal object + """ + quantizer = Decimal("0.1") ** precision + if rounding == "up": + return number.quantize(quantizer, rounding=ROUND_CEILING) + if rounding == "down": + return number.quantize(quantizer, rounding=ROUND_FLOOR) + return number.quantize(quantizer, rounding=ROUND_HALF_EVEN) diff --git a/apps/worker/helpers/pathmap.py b/apps/worker/helpers/pathmap.py new file mode 100644 index 0000000000..24077620e0 --- /dev/null +++ b/apps/worker/helpers/pathmap.py @@ -0,0 +1,183 @@ +from difflib import SequenceMatcher +from os.path import relpath +from typing import Sequence + + +def _clean_path(path): + path = relpath( + path.strip() + .replace("**/", "") + .replace("\r", "") + .replace("\\ ", " ") + .replace("\\", "/") + ) + return path + + +def _check_ancestors(path, match, ancestors): + """ + Require N ancestors to be in common with original path and matched path + """ + pl = path.lower() + ml = match.lower() + if pl == ml: + return True + if len(ml.split("/")) < len(pl.split("/")) and pl.endswith(ml): + return True + return ml.endswith("/".join(pl.split("/")[(ancestors + 1) * -1 :])) + + +def _get_best_match(path: str, possibilities: list[str]) -> str: + """ + Given a `path`, return the most similar one out of `possibilities`. + """ + + best_match = (-1.0, "") + for possibility in possibilities: + match = SequenceMatcher(None, path, possibility).ratio() + if match > best_match[0]: + best_match = (match, possibility) + + return best_match[1] + + +class Node: + full_paths: list[str] + """ + The full paths terminating in this node. + """ + + children: dict[str, "Node"] + """ + Child nodes, keyed by path component. + """ + + def __init__(self) -> None: + self.full_paths = [] + self.children = {} + + +class Tree: + """ + This tree maintains a list of files and allows matching on them. + + It internally organizes the list of files (called `paths`) as a tree of `Node`s. + The paths are split into path components in reverse order. + Lookups in the tree also happen in reverse path-component order. + + For example, the following list of files: + - `src/foo/mod.rs` + - `src/foo/bar/mod.rs` + + ... are organized in a tree that looks like this: + - mod.rs + - foo + - src => src/foo/mod.rs + - bar + - foo + - src => src/foo/bar/mod.rs + + Using this tree, it is possible to look up paths like: + - `C:\\Users\\ci\\repo\\src\\foo\\mod.rs` + + Matching / lookup again happens in reverse path-component order, from right to left. + In this particular case, the tree traversal would walk the tree `Node`s `mod.rs`, `foo`, `src` + before it hits the `src/foo/mod.rs` "full_path", which is the result of the lookup. + """ + + def __init__(self, paths: Sequence[str]): + self.root = Node() + for path in paths: + self.insert(path) + + def insert(self, path: str): + # the path components, in reverse order + components = reversed(path.split("/")) + + node = self.root + for component in components: + component = component.lower() + if component not in node.children: + node.children[component] = Node() + node = node.children[component] + + node.full_paths.append(path) + + def resolve_path(self, path: str, ancestors: int | None = None) -> str | None: + path = _clean_path(path) + new_path = self.lookup(path, ancestors) + + if new_path: + if ancestors and not _check_ancestors(path, new_path, ancestors): + # path ancestor count is not valid + return None + + return new_path + + # path was not resolved + return None + + def _drill(self, node: Node) -> list[str] | None: + """ + "Drill down" a straight branch of a tree, returning the first `full_paths`. + """ + while len(node.children) == 1: + node = next(iter(node.children.values())) + if len(node.full_paths): + return node.full_paths + + return None + + def _recursive_lookup( + self, + node: Node, + components: list[str], + results: list[str], + i=0, + end=False, + match=False, + ) -> list[str]: + """ + Performs a lookup in tree recursively + + :bool: end - Indicates if last lookup was the end of a sequence + :bool: match - Indicates if filename has any match in tree + """ + + child_node = ( + node.children.get(components[i].lower()) if i < len(components) else None + ) + if child_node: + is_end = len(child_node.full_paths) > 0 + if is_end: + results = child_node.full_paths + return self._recursive_lookup( + child_node, components, results, i + 1, is_end, True + ) + else: + if not end and match: + next_path = self._drill(node) + if next_path: + results.extend(next_path) + return results + + def lookup(self, path: str, ancestors=None) -> str | None: + """ + Lookup a path in the tree, returning the closest matching path + in the tree if found. + """ + path_hit = None + components = list(reversed(path.split("/"))) + results = self._recursive_lookup(self.root, components, []) + if not results: + return None + if len(results) == 1: + path_hit = results[0] + else: + if path.replace(".", "").startswith("/") and ancestors: + path_lengths = list(map(lambda x: len(x), results)) + closest_length = min(path_lengths, key=lambda x: abs(x - ancestors)) + path_hit = next(x for x in results if len(x) == closest_length) + else: + path_hit = _get_best_match(path, list(reversed(results))) + return path_hit diff --git a/apps/worker/helpers/reports.py b/apps/worker/helpers/reports.py new file mode 100644 index 0000000000..7bbb3ec25a --- /dev/null +++ b/apps/worker/helpers/reports.py @@ -0,0 +1,17 @@ +from shared.config import get_config +from shared.yaml import UserYaml + +from services.yaml.reader import read_yaml_field + + +def get_totals_from_file_in_reports(report, path): + file = report.get(path) + return file.totals if file else None + + +def delete_archive_setting(commit_yaml: UserYaml | dict) -> bool: + if get_config("services", "minio", "expire_raw_after_n_days"): + return True + return not read_yaml_field( + commit_yaml, ("codecov", "archive", "uploads"), _else=True + ) diff --git a/apps/worker/helpers/save_commit_error.py b/apps/worker/helpers/save_commit_error.py new file mode 100644 index 0000000000..5943355d0f --- /dev/null +++ b/apps/worker/helpers/save_commit_error.py @@ -0,0 +1,23 @@ +from database.models import Commit +from database.models.core import CommitError + + +def save_commit_error(commit: Commit, error_code, error_params=None): + db_session = commit.get_db_session() + error_exist = ( + db_session.query(CommitError) + .filter_by(commit=commit, error_code=error_code) + .first() + ) + + if error_params is None: + error_params = {} + + if not error_exist: + err = CommitError( + commit=commit, + error_code=error_code, + error_params=error_params, + ) + db_session.add(err) + db_session.flush() diff --git a/apps/worker/helpers/sentry.py b/apps/worker/helpers/sentry.py new file mode 100644 index 0000000000..1bf9cee3fb --- /dev/null +++ b/apps/worker/helpers/sentry.py @@ -0,0 +1,40 @@ +import os + +import sentry_sdk +from sentry_sdk.integrations.celery import CeleryIntegration +from sentry_sdk.integrations.django import DjangoIntegration +from sentry_sdk.integrations.httpx import HttpxIntegration +from sentry_sdk.integrations.redis import RedisIntegration +from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration +from shared.config import get_config + +from helpers.version import get_current_version + + +def is_sentry_enabled() -> bool: + return bool(get_config("services", "sentry", "server_dsn")) + + +def initialize_sentry() -> None: + version = get_current_version() + version_str = f"worker-{version}" + sentry_dsn = get_config("services", "sentry", "server_dsn") + sentry_sdk.init( + sentry_dsn, + sample_rate=float(os.getenv("SENTRY_PERCENTAGE", 1.0)), + environment=os.getenv("DD_ENV", "production"), + traces_sample_rate=float(os.environ.get("SERVICES__SENTRY__SAMPLE_RATE", 1)), + profiles_sample_rate=float( + os.environ.get("SERVICES__SENTRY__PROFILES_SAMPLE_RATE", 1) + ), + integrations=[ + CeleryIntegration(monitor_beat_tasks=True), + DjangoIntegration(signals_spans=False), + SqlalchemyIntegration(), + RedisIntegration(cache_prefixes=["cache:"]), + HttpxIntegration(), + ], + release=os.getenv("SENTRY_RELEASE", version_str), + ) + if os.getenv("CLUSTER_ENV"): + sentry_sdk.set_tag("cluster", os.getenv("CLUSTER_ENV")) diff --git a/apps/worker/helpers/string.py b/apps/worker/helpers/string.py new file mode 100644 index 0000000000..f699e1fd70 --- /dev/null +++ b/apps/worker/helpers/string.py @@ -0,0 +1,120 @@ +from dataclasses import dataclass +from enum import Enum +from typing import List + +import regex + + +class EscapeEnum(Enum): + APPEND = "append" + PREPEND = "prepend" + REPLACE = "replace" + + +@dataclass +class Replacement: + strings: List[str] + output: str + method: EscapeEnum + + +class StringEscaper: + """ + Class to use to escape strings using format defined + through a dict. + + Args: + escape_def: list of Replacement that defines how to escape + characters + + string is escaped by applying method in each Replacement + to each char in Replacement.chars using the char in output + + for example: + escape_def = [ + Replacement(["1"], "2", EscapeEnum.APPEND), + Replacement(["3"], "4", EscapeEnum.PREPEND), + Replacement(["5", "6"], "6", EscapeEnum.REPLACE), + ] + + escaper = StringEscaper(escape_def) + + escaper.replace("123456") + + will give: "12243466" + """ + + def __init__(self, escape_def: List[Replacement]): + self.escape_def = escape_def + + def replace(self, replacement_target): + for replacement in self.escape_def: + for string in replacement.strings: + if replacement.method == EscapeEnum.PREPEND: + replacement_target = replacement_target.replace( + string, f"{replacement.output}{string}" + ) + elif replacement.method == EscapeEnum.APPEND: + replacement_target = replacement_target.replace( + string, f"{string}{replacement.output}" + ) + elif replacement.method == EscapeEnum.REPLACE: + replacement_target = replacement_target.replace( + string, replacement.output + ) + return replacement_target + + +MAX_PATH_COMPONENTS = 3 + + +# matches file paths with an optional line number and column at the end: +# /Users/josephsawaya/dev/test-result-action/demo/calculator/calculator.test.ts:10:31 +# /Users/josephsawaya/dev/test-result-action/demo/calculator/calculator.test.ts +# Users/josephsawaya/dev/test-result-action/demo/calculator/calculator.test.ts +file_path_regex = regex.compile( + r"((\/*[\w\-]+\/)+([\w\.]+)(:\d+:\d+)*)", +) + + +def shorten_file_paths(string): + """ + This function takes in a string and returns it with all the paths + it contains longer than 3 components shortened to 3 components + + Example: + string = ''' + Expected: 1 + Received: -1 + at Object.<anonymous> (/Users/josephsawaya/dev/test-result-action/demo/calculator/calculator.test.ts:10:31) + at Promise.then.completed (/Users/josephsawaya/dev/test-result-action/node_modules/jest-circus/build/utils.js:298:28) + ''' + shortened_string = shorten_file_paths(string) + print(shortened_string) + + will print: + Expected: 1 + Received: -1 + at Object.<anonymous> (.../demo/calculator/calculator.test.ts:10:31) + at Promise.then.completed (.../jest-circus/build/utils.js:298:28) + """ + + matches = file_path_regex.findall(string) + for match_tuple in matches: + file_path = match_tuple[0] + split_file_path = file_path.split("/") + + # if the file_path has more than 3 components we should shorten it + if len(split_file_path) > MAX_PATH_COMPONENTS: + last_path_components = split_file_path[-MAX_PATH_COMPONENTS:] + no_dots_shortened_file_path = "/".join(last_path_components) + + # possibly remove leading / because we're adding it with the dots + if no_dots_shortened_file_path.startswith("/"): + no_dots_shortened_file_path = no_dots_shortened_file_path[1:] + + shortened_path = ".../" + no_dots_shortened_file_path + + string = string.replace(file_path, shortened_path) + + return string diff --git a/apps/worker/helpers/tests/pathmap/__init__.py b/apps/worker/helpers/tests/pathmap/__init__.py new file mode 100644 index 0000000000..40a96afc6f --- /dev/null +++ b/apps/worker/helpers/tests/pathmap/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/apps/worker/helpers/tests/pathmap/test_pathmap.py b/apps/worker/helpers/tests/pathmap/test_pathmap.py new file mode 100644 index 0000000000..6e8625d06b --- /dev/null +++ b/apps/worker/helpers/tests/pathmap/test_pathmap.py @@ -0,0 +1,144 @@ +from helpers.pathmap import Tree, _check_ancestors, _clean_path + + +def test_clean_path(): + path = "**/some/directory" + assert _clean_path(path) == "some/directory" + path = "some/path\r/with/tabs\r" + assert _clean_path(path) == "some/path/with/tabs" + path = "some\\ very_long/directory\\ name" + assert _clean_path(path) == "some very_long/directory name" + path = "ms\\style\\directory" + assert _clean_path(path) == "ms/style/directory" + + +def test_resolve_path(): + tree = Tree(["src/components/login.js"]) + + assert tree.resolve_path("Src/components/login.js") == "src/components/login.js" + + +def test_resolve_case(): + tree = Tree(["Aa/Bb/cc", "Aa/Bb/Cc"]) + assert tree.resolve_path("aa/bb/cc") == "Aa/Bb/cc" + assert tree.resolve_path("aa/bb/Cc") == "Aa/Bb/Cc" + + +def test_resolve_paths(): + before = [ + "not/found.py", + "/Users/user/owner/repo/src/components/login.js", + "site-packages/package/__init__.py", + "path.py", + "a/b/../Path With\\ Space", + ] + + after = [ + None, + "src/components/login.js", + "package/__init__.py", + "path.py", + "a/Path With Space", + ] + + tree = Tree([path for path in after if path]) + for path, expected in zip(before, after): + assert tree.resolve_path(path) == expected + + +def test_resolve_path_when_to_short(): + tree = Tree(["a/b/c"]) + assert tree.resolve_path("b/c", 0) == "a/b/c" + assert tree.resolve_path("b/c", 1) == "a/b/c" + + +def test_resolve_path_when_to_long(): + tree = Tree(["a/b/c"]) + assert tree.resolve_path("z/y/b/c", 1) == "a/b/c" + + +def test_check_ancestors(): + assert _check_ancestors("a", "a", 1) is True, "matches" + assert _check_ancestors("A", "a", 1) is True, "matches case insensative" + assert _check_ancestors("a/B", "a/B", 1) is True, "matches" + assert _check_ancestors("A/B", "a/b", 1) is True, "matches case insensative" + assert _check_ancestors("b/b", "a/b", 1) is False, "does not match first ancestor" + assert _check_ancestors("a/b/c", "x/b/c", 1) is True + assert _check_ancestors("a/b/c", "x/b/c", 2) is False + assert _check_ancestors("a/b/c/d", "X/B/C/D", 2) is True + assert _check_ancestors("a", "b/a", 2) is True, "original was missing ancestors" + assert _check_ancestors("a/b", "z/a/b", 2) is True + assert _check_ancestors("b", "a/b", 1) is True + + +def test_resolve_paths_with_ancestors(): + tree = Tree(["x/y/z"]) + + # default, no ancestors ============================ + paths = ["z", "R/z", "R/y/z", "x/y/z", "w/x/y/z"] + expected = ["x/y/z", "x/y/z", "x/y/z", "x/y/z", "x/y/z"] + resolved = [tree.resolve_path(path) for path in paths] + assert resolved == expected + + # one ancestors ==================================== + paths = ["z", "R/z", "R/y/z", "x/y/z", "w/x/y/z"] + expected = [None, None, "x/y/z", "x/y/z", "x/y/z"] + resolved = [tree.resolve_path(path, 1) for path in paths] + assert set(resolved) == set(expected) + + # two ancestors ==================================== + paths = ["z", "R/z", "R/y/z", "x/y/z", "w/x/y/z"] + expected = [None, None, None, "x/y/z", "x/y/z"] + resolved = [tree.resolve_path(path, 2) for path in paths] + assert set(resolved) == set(expected) + + +def test_resolving(): + tree = Tree(["a/b/c", "a/r/c", "c"]) + assert tree.resolve_path("r/c", 1) == "a/r/c" + assert tree.resolve_path("r/c") == "a/r/c" + + tree = Tree(["a/b", "a/b/c/d", "x/y"]) + assert tree.resolve_path("c/d", 1) == "a/b/c/d" + + +def test_with_plus(): + tree = Tree(["b+c"]) + assert tree.resolve_path("b+c") == "b+c" + + tree = Tree(["a/b+c"]) + assert tree.resolve_path("b+c") == "a/b+c" + + +def test_case_sensitive_ancestors(): + tree = Tree(["src/HeapDump/GCHeapDump.cs"]) + path = "C:/projects/perfview/src/heapDump/GCHeapDump.cs" + new_path = tree.resolve_path(path, 1) + assert new_path == "src/HeapDump/GCHeapDump.cs" + + +def test_path_should_not_resolve(): + tree = Tree(["four/six/three.py"]) + assert tree.resolve_path("four/six/seven.py") is None + + +def test_path_should_not_resolve_case_insensative(): + tree = Tree(["a/b/C"]) + assert tree.resolve_path("a/B/c") == "a/b/C" + + +def test_ancestors_original_missing(): + tree = Tree(["shorter.h"]) + assert tree.resolve_path("a/long/path/shorter.h", 1) == "shorter.h" + + +def test_ancestors_absolute_path(): + tree = Tree( + [ + "examples/ChurchNumerals.scala", + "tests/src/test/scala/at/logic/gapt/examples/ChurchNumerals.scala", + ] + ) + path = "/home/travis/build/gapt/gapt/examples/ChurchNumerals.scala" + + assert tree.resolve_path(path, 1) == "examples/ChurchNumerals.scala" diff --git a/apps/worker/helpers/tests/pathmap/test_tree.py b/apps/worker/helpers/tests/pathmap/test_tree.py new file mode 100644 index 0000000000..ec22b88adc --- /dev/null +++ b/apps/worker/helpers/tests/pathmap/test_tree.py @@ -0,0 +1,43 @@ +from helpers.pathmap import Tree, _get_best_match + + +def test_get_best_match(): + path = "a/bB.py" + possibilities = ["c/bB.py", "d/Bb.py"] + + assert _get_best_match(path, possibilities) == "c/bB.py" + + +def test_drill(): + tree = Tree(["a/b/c"]) + assert tree._drill(tree.root) == ["a/b/c"] + + +def test_drill_multiple_possible_paths(): + tree = Tree(["src/list.rs", "benches/list.rs"]) + + branch = tree.root.children.get("list.rs") + assert tree._drill(branch) is None + + +def test_recursive_lookup(): + path = "one/two/three.py" + + tree = Tree([path]) + + path_split = list(reversed(path.split("/"))) + match = tree._recursive_lookup(tree.root, path_split, []) + + assert match == ["one/two/three.py"] + + path = "four/five/three.py" + path_split = list(reversed(path.split("/"))) + match = tree._recursive_lookup(tree.root, path_split, []) + + assert match == ["one/two/three.py"] + + +def test_lookup(): + tree = Tree(["one/two/three.py"]) + + assert tree.lookup("two/one/three.py") == "one/two/three.py" diff --git a/apps/worker/helpers/tests/unit/test_checkpoint_logger.py b/apps/worker/helpers/tests/unit/test_checkpoint_logger.py new file mode 100644 index 0000000000..1b95f19f71 --- /dev/null +++ b/apps/worker/helpers/tests/unit/test_checkpoint_logger.py @@ -0,0 +1,671 @@ +import json +import unittest +from enum import auto +from unittest.mock import patch + +import pytest +from prometheus_client import REGISTRY + +from helpers.checkpoint_logger import ( + BaseFlow, + _get_milli_timestamp, + failure_events, + from_kwargs, + reliability_counters, + subflows, + success_events, +) +from helpers.log_context import LogContext, set_log_context +from rollouts import CHECKPOINT_ENABLED_REPOSITORIES + + +class CounterAssertion: + def __init__(self, metric, labels, expected_value): + self.metric = metric + self.labels = labels + self.expected_value = expected_value + + self.before_value = None + self.after_value = None + + def __repr__(self): + return f"" + + +class CounterAssertionSet: + def __init__(self, counter_assertions): + self.counter_assertions = counter_assertions + + def __enter__(self): + for assertion in self.counter_assertions: + assertion.before_value = ( + REGISTRY.get_sample_value(assertion.metric, labels=assertion.labels) + or 0 + ) + + def __exit__(self, exc_type, exc_value, exc_tb): + for assertion in self.counter_assertions: + assertion.after_value = ( + REGISTRY.get_sample_value(assertion.metric, labels=assertion.labels) + or 0 + ) + assert ( + assertion.after_value - assertion.before_value + == assertion.expected_value + ) + + +@failure_events("BRANCH_1_FAIL") +@success_events("BRANCH_1_SUCCESS", "BRANCH_2_SUCCESS") +@subflows( + ("first_checkpoint", "BEGIN", "CHECKPOINT"), + ("branch_1_to_finish", "BRANCH_1", "BRANCH_1_SUCCESS"), + ("total_branch_1_time", "BEGIN", "BRANCH_1_SUCCESS"), + ("total_branch_1_fail_time", "BEGIN", "BRANCH_1_FAIL"), +) +@reliability_counters +class DecoratedEnum(BaseFlow): + BEGIN = auto() + CHECKPOINT = auto() + BRANCH_1 = auto() + BRANCH_1_FAIL = auto() + BRANCH_1_SUCCESS = auto() + BRANCH_2 = auto() + BRANCH_2_SUCCESS = auto() + + +class TestEnum1(BaseFlow): + A = auto() + B = auto() + C = auto() + + +class TestEnum2(BaseFlow): + D = auto() + E = auto() + F = auto() + + +class SortOrderEnum(BaseFlow): + C = auto() + B = auto() + A = auto() + + +class TestCheckpointLogger(unittest.TestCase): + @pytest.fixture(scope="function", autouse=True) + def inject_mocker(request, mocker): + request.mocker = mocker + + def setUp(self): + set_log_context(LogContext()) + + @patch("time.time_ns", return_value=123456789) + def test_get_milli_timestamp(self, mocker): + expected_ms = 123456789 // 1000000 + self.assertEqual(_get_milli_timestamp(), expected_ms) + + @patch("helpers.checkpoint_logger._get_milli_timestamp", return_value=1337) + def test_log_checkpoint(self, mocker): + TestEnum1.log(TestEnum1.A) + + self.assertEqual(TestEnum1._data_from_log_context()[TestEnum1.A], 1337) + + @patch( + "helpers.checkpoint_logger._get_milli_timestamp", + side_effect=[1337, 9001, 100000], + ) + def test_log_multiple_checkpoints(self, mocker): + TestEnum1.log(TestEnum1.A) + TestEnum1.log(TestEnum1.B) + TestEnum1.log(TestEnum1.C) + + data = TestEnum1._data_from_log_context() + self.assertEqual(data[TestEnum1.A], 1337) + self.assertEqual(data[TestEnum1.B], 9001) + self.assertEqual(data[TestEnum1.C], 100000) + + def test_log_checkpoint_twice_throws(self): + TestEnum1.log(TestEnum1.A) + + with self.assertRaises(ValueError): + TestEnum1.log(TestEnum1.A, strict=True) + + def test_log_checkpoint_wrong_enum_throws(self) -> None: + with self.assertRaises(ValueError): + TestEnum1.log(TestEnum2.D, strict=True) # type: ignore[arg-type] + + @patch("helpers.checkpoint_logger._get_milli_timestamp", side_effect=[1337, 9001]) + def test_subflow_duration(self, mocker): + TestEnum1.log(TestEnum1.A) + TestEnum1.log(TestEnum1.B) + + data = TestEnum1._data_from_log_context() + duration = TestEnum1._subflow_duration(TestEnum1.A, TestEnum1.B, data=data) + self.assertEqual(duration, 9001 - 1337) + + @patch("helpers.checkpoint_logger._get_milli_timestamp", side_effect=[1337, 9001]) + def test_subflow_duration_missing_checkpoints(self, mocker): + TestEnum1.log(TestEnum1.A) + TestEnum1.log(TestEnum1.C) + data = TestEnum1._data_from_log_context() + + # Missing end checkpoint + with self.assertRaises(ValueError): + TestEnum1._subflow_duration( + TestEnum1.A, TestEnum1.B, data=data, strict=True + ) + + # Missing start checkpoint + with self.assertRaises(ValueError): + TestEnum1._subflow_duration( + TestEnum1.B, TestEnum1.C, data=data, strict=True + ) + + @patch("helpers.checkpoint_logger._get_milli_timestamp", side_effect=[1337, 9001]) + def test_subflow_duration_wrong_order(self, mocker): + TestEnum1.log(TestEnum1.A) + TestEnum1.log(TestEnum1.B) + data = TestEnum1._data_from_log_context() + + # End < start + with self.assertRaises(ValueError): + TestEnum1._subflow_duration( + TestEnum1.B, TestEnum1.A, data=data, strict=True + ) + + # End == start + with self.assertRaises(ValueError): + TestEnum1._subflow_duration( + TestEnum1.A, TestEnum1.A, data=data, strict=True + ) + + @patch("helpers.checkpoint_logger._get_milli_timestamp", return_value=1337) + def test_subflow_duration_wrong_enum(self, mocker): + TestEnum1.log(TestEnum1.A) + data = TestEnum1._data_from_log_context() + + # Wrong enum for start checkpoint + with self.assertRaises(ValueError): + TestEnum1._subflow_duration( + TestEnum2.D, TestEnum1.A, data=data, strict=True + ) + + # Wrong enum for end checkpoint + with self.assertRaises(ValueError): + TestEnum1._subflow_duration( + TestEnum1.A, TestEnum2.D, data=data, strict=True + ) + + @pytest.mark.real_checkpoint_logger + @patch("helpers.checkpoint_logger._get_milli_timestamp", side_effect=[1337, 9001]) + @patch("sentry_sdk.set_measurement") + def test_submit_subflow(self, mock_sentry, mock_timestamp): + TestEnum1.log(TestEnum1.A) + TestEnum1.log(TestEnum1.B) + data = TestEnum1._data_from_log_context() + + expected_duration = 9001 - 1337 + TestEnum1.submit_subflow("metricname", TestEnum1.A, TestEnum1.B, data=data) + mock_sentry.assert_called_with("metricname", expected_duration, "milliseconds") + assert ( + REGISTRY.get_sample_value( + "worker_checkpoints_subflow_duration_seconds_sum", + labels={"flow": "TestEnum1", "subflow": "metricname"}, + ) + == expected_duration / 1000 + ) + + @patch("helpers.checkpoint_logger._get_milli_timestamp", side_effect=[1337]) + def test_log_ignore_repeat(self, mock_timestamp): + TestEnum1.log(TestEnum1.A) + time = TestEnum1._data_from_log_context()[TestEnum1.A] + + TestEnum1.log(TestEnum1.A, ignore_repeat=True) + assert TestEnum1._data_from_log_context()[TestEnum1.A] == time + + def test_create_from_kwargs(self): + good_data = { + TestEnum1.A: 1337, + TestEnum1.B: 9001, + } + deserialized_good_data = json.loads(json.dumps(good_data)) + good_kwargs = { + "checkpoints_TestEnum1": deserialized_good_data, + } + from_kwargs([TestEnum1], good_kwargs, strict=True) + assert TestEnum1._data_from_log_context() == good_data + + # Data is from TestEnum2 but we expected TestEnum1 + bad_data = { + TestEnum2.D: 1337, + TestEnum2.E: 9001, + } + deserialized_bad_data = json.loads(json.dumps(bad_data)) + bad_kwargs = { + "checkpoints_TestEnum1": deserialized_bad_data, + } + with self.assertRaises(ValueError): + checkpoints = from_kwargs([TestEnum1], bad_kwargs, strict=True) + + from_kwargs([TestEnum1], bad_kwargs, strict=False) + assert TestEnum1._data_from_log_context() == {} + + @patch("helpers.checkpoint_logger._get_milli_timestamp", side_effect=[1337, 9001]) + def test_log_to_kwargs(self, mock_timestamp): + kwargs = {} + + TestEnum1.log(TestEnum1.A, kwargs=kwargs) + assert "checkpoints_TestEnum1" in kwargs + assert kwargs["checkpoints_TestEnum1"][TestEnum1.A] == 1337 + assert TestEnum1.B not in kwargs["checkpoints_TestEnum1"] + + TestEnum1.log(TestEnum1.B, kwargs=kwargs) + assert "checkpoints_TestEnum1" in kwargs + assert kwargs["checkpoints_TestEnum1"][TestEnum1.A] == 1337 + assert kwargs["checkpoints_TestEnum1"][TestEnum1.B] == 9001 + + @pytest.mark.real_checkpoint_logger + @patch("sentry_sdk.set_measurement") + @patch("helpers.checkpoint_logger._get_milli_timestamp", side_effect=[9001]) + def test_create_log_oneliner(self, mock_timestamp, mock_sentry): + kwargs = { + "checkpoints_TestEnum1": { + TestEnum1.A: 1337, + }, + } + + expected_duration = 9001 - 1337 + + from_kwargs([TestEnum1], kwargs, strict=True) + TestEnum1.log(TestEnum1.B, kwargs=kwargs) + data = TestEnum1._data_from_log_context() + TestEnum1.submit_subflow("x", TestEnum1.A, TestEnum1.B, data=data) + + mock_sentry.assert_called_with("x", expected_duration, "milliseconds") + assert kwargs["checkpoints_TestEnum1"][TestEnum1.A] == 1337 + assert kwargs["checkpoints_TestEnum1"][TestEnum1.B] == 9001 + + def test_log_before_beginning_throws(self): + with self.assertRaises(ValueError): + DecoratedEnum.log(DecoratedEnum.BRANCH_1_SUCCESS, strict=True) + + def test_log_after_end_throws(self): + DecoratedEnum.log(DecoratedEnum.BEGIN) + DecoratedEnum.log(DecoratedEnum.BRANCH_1_SUCCESS) + + with self.assertRaises(ValueError): + DecoratedEnum.log(DecoratedEnum.BRANCH_1_FAIL, strict=True) + + def test_success_failure_decorators(self): + for val in DecoratedEnum.__members__.values(): + if val in [DecoratedEnum.BRANCH_1_SUCCESS, DecoratedEnum.BRANCH_2_SUCCESS]: + assert val.is_success() + else: + assert not val.is_success() + + if val in [DecoratedEnum.BRANCH_1_FAIL]: + assert val.is_failure() + else: + assert not val.is_failure() + + def test_subflows_decorator(self): + subflows = DecoratedEnum._subflows() + + # No subflows end at these checkpoints + assert DecoratedEnum.BEGIN not in subflows + assert DecoratedEnum.BRANCH_1 not in subflows + assert DecoratedEnum.BRANCH_2 not in subflows + + # `DecoratedEnum.CHECKPOINT` is not a terminal event, but we explicitly + # defined a subflow ending there. + checkpoint_subflows = subflows.get(DecoratedEnum.CHECKPOINT) + assert checkpoint_subflows is not None + assert len(checkpoint_subflows) == 1 + assert checkpoint_subflows[0] == ("first_checkpoint", DecoratedEnum.BEGIN) + + # All terminal events should have a subflow defined for them which + # begins at the flow's first event. `BRANCH_1_FAIL` has had this + # subflow provided by the user, so we should use the user's name. + branch_1_fail_subflows = subflows.get(DecoratedEnum.BRANCH_1_FAIL) + assert branch_1_fail_subflows is not None + assert len(branch_1_fail_subflows) == 1 + assert branch_1_fail_subflows[0] == ( + "total_branch_1_fail_time", + DecoratedEnum.BEGIN, + ) + + # All terminal events should have a subflow defined for them which + # begins at the flow's first event. `BRANCH_1_SUCCESS` has had this + # subflow provided by the user, so we should use the user's name. + # Also, `BRANCH_1_SUCCESS` is the end of a second subflow. Ensure that + # both subflows are present. + branch_1_success_subflows = subflows.get(DecoratedEnum.BRANCH_1_SUCCESS) + assert branch_1_success_subflows is not None + assert len(branch_1_success_subflows) == 2 + assert ("total_branch_1_time", DecoratedEnum.BEGIN) in branch_1_success_subflows + assert ( + "branch_1_to_finish", + DecoratedEnum.BRANCH_1, + ) in branch_1_success_subflows + + # All terminal events should have a subflow defined for them which + # begins at the flow's first event. `BRANCH_2_SUCCESS` has not had this + # subflow provided by the user, so we should use the default name. + branch_2_success_subflows = subflows.get(DecoratedEnum.BRANCH_2_SUCCESS) + assert branch_2_success_subflows is not None + assert len(branch_2_success_subflows) == 1 + assert branch_2_success_subflows[0] == ( + "DecoratedEnum_BEGIN_to_BRANCH_2_SUCCESS", + DecoratedEnum.BEGIN, + ) + + @patch.object(CHECKPOINT_ENABLED_REPOSITORIES, "check_value", return_value=123) + def test_reliability_counters_with_context(self, mock_object): + repoid = 123 + mock_object.return_value = repoid + set_log_context(LogContext(repo_id=repoid)) + + counter_assertions = [ + CounterAssertion( + "worker_checkpoints_begun_total", {"flow": "DecoratedEnum"}, 1 + ), + CounterAssertion( + "worker_repo_checkpoints_begun_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 1, + ), + CounterAssertion( + "worker_checkpoints_events_total", + {"flow": "DecoratedEnum", "checkpoint": "BEGIN"}, + 1, + ), + CounterAssertion( + "worker_repo_checkpoints_events_total", + {"flow": "DecoratedEnum", "checkpoint": "BEGIN", "repoid": f"{repoid}"}, + 1, + ), + CounterAssertion( + "worker_checkpoints_succeeded_total", {"flow": "DecoratedEnum"}, 0 + ), + CounterAssertion( + "worker_repo_checkpoints_succeeded_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 0, + ), + CounterAssertion( + "worker_checkpoints_failed_total", {"flow": "DecoratedEnum"}, 0 + ), + CounterAssertion( + "worker_repo_checkpoints_failed_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 0, + ), + CounterAssertion( + "worker_checkpoints_ended_total", {"flow": "DecoratedEnum"}, 0 + ), + CounterAssertion( + "worker_repo_checkpoints_ended_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 0, + ), + ] + with CounterAssertionSet(counter_assertions) as a: + DecoratedEnum.log(DecoratedEnum.BEGIN) + + # Nothing special about `CHECKPOINT` - no counters should change + counter_assertions = [ + CounterAssertion( + "worker_checkpoints_begun_total", {"flow": "DecoratedEnum"}, 0 + ), + CounterAssertion( + "worker_repo_checkpoints_begun_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 0, + ), + CounterAssertion( + "worker_checkpoints_events_total", + {"flow": "DecoratedEnum", "checkpoint": "BEGIN"}, + 0, + ), + CounterAssertion( + "worker_repo_checkpoints_events_total", + {"flow": "DecoratedEnum", "checkpoint": "BEGIN", "repoid": f"{repoid}"}, + 0, + ), + CounterAssertion( + "worker_checkpoints_events_total", + {"flow": "DecoratedEnum", "checkpoint": "CHECKPOINT"}, + 1, + ), + CounterAssertion( + "worker_repo_checkpoints_events_total", + { + "flow": "DecoratedEnum", + "checkpoint": "CHECKPOINT", + "repoid": f"{repoid}", + }, + 1, + ), + CounterAssertion( + "worker_checkpoints_succeeded_total", {"flow": "DecoratedEnum"}, 0 + ), + CounterAssertion( + "worker_repo_checkpoints_succeeded_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 0, + ), + CounterAssertion( + "worker_checkpoints_failed_total", {"flow": "DecoratedEnum"}, 0 + ), + CounterAssertion( + "worker_repo_checkpoints_failed_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 0, + ), + CounterAssertion( + "worker_checkpoints_ended_total", {"flow": "DecoratedEnum"}, 0 + ), + CounterAssertion( + "worker_reopo_checkpoints_ended_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 0, + ), + ] + with CounterAssertionSet(counter_assertions): + DecoratedEnum.log(DecoratedEnum.CHECKPOINT) + + # Failures should increment both `failed` and `ended` + counter_assertions = [ + CounterAssertion( + "worker_checkpoints_begun_total", {"flow": "DecoratedEnum"}, 0 + ), + CounterAssertion( + "worker_repo_checkpoints_begun_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 0, + ), + CounterAssertion( + "worker_checkpoints_succeeded_total", {"flow": "DecoratedEnum"}, 0 + ), + CounterAssertion( + "worker_repo_checkpoints_succeeded_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 0, + ), + CounterAssertion( + "worker_checkpoints_failed_total", {"flow": "DecoratedEnum"}, 1 + ), + CounterAssertion( + "worker_repo_checkpoints_failed_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 1, + ), + CounterAssertion( + "worker_checkpoints_ended_total", {"flow": "DecoratedEnum"}, 1 + ), + CounterAssertion( + "worker_repo_checkpoints_ended_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 1, + ), + CounterAssertion( + "worker_checkpoints_events_total", + {"flow": "DecoratedEnum", "checkpoint": "BRANCH_1_FAIL"}, + 1, + ), + CounterAssertion( + "worker_repo_checkpoints_events_total", + { + "flow": "DecoratedEnum", + "checkpoint": "BRANCH_1_FAIL", + "repoid": f"{repoid}", + }, + 1, + ), + ] + with CounterAssertionSet(counter_assertions): + DecoratedEnum.log(DecoratedEnum.BRANCH_1_FAIL) + + # Because we've logged a terminal event, we have to restart the flow + set_log_context(LogContext(repo_id=repoid)) + DecoratedEnum.log(DecoratedEnum.BEGIN) + + # Successes should increment both `succeeded` and `ended` + counter_assertions = [ + CounterAssertion( + "worker_checkpoints_begun_total", {"flow": "DecoratedEnum"}, 0 + ), + CounterAssertion( + "worker_repo_checkpoints_begun_total", {"flow": "DecoratedEnum"}, 0 + ), + CounterAssertion( + "worker_checkpoints_succeeded_total", {"flow": "DecoratedEnum"}, 1 + ), + CounterAssertion( + "worker_repo_checkpoints_succeeded_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 1, + ), + CounterAssertion( + "worker_checkpoints_failed_total", {"flow": "DecoratedEnum"}, 0 + ), + CounterAssertion( + "worker_repo_checkpoints_failed_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 0, + ), + CounterAssertion( + "worker_checkpoints_ended_total", {"flow": "DecoratedEnum"}, 1 + ), + CounterAssertion( + "worker_repo_checkpoints_ended_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 1, + ), + CounterAssertion( + "worker_checkpoints_events_total", + {"flow": "DecoratedEnum", "checkpoint": "BRANCH_1_SUCCESS"}, + 1, + ), + CounterAssertion( + "worker_repo_checkpoints_events_total", + { + "flow": "DecoratedEnum", + "checkpoint": "BRANCH_1_SUCCESS", + "repoid": f"{repoid}", + }, + 1, + ), + ] + with CounterAssertionSet(counter_assertions): + DecoratedEnum.log(DecoratedEnum.BRANCH_1_SUCCESS) + + # Because we've logged a terminal event, we have to restart the flow + set_log_context(LogContext(repo_id=repoid)) + DecoratedEnum.log(DecoratedEnum.BEGIN) + + # A different success path should also increment `succeeded` and `ended` + counter_assertions = [ + CounterAssertion( + "worker_checkpoints_begun_total", {"flow": "DecoratedEnum"}, 0 + ), + CounterAssertion( + "worker_repo_checkpoints_begun_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 0, + ), + CounterAssertion( + "worker_checkpoints_succeeded_total", {"flow": "DecoratedEnum"}, 1 + ), + CounterAssertion( + "worker_repo_checkpoints_succeeded_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 1, + ), + CounterAssertion( + "worker_checkpoints_failed_total", {"flow": "DecoratedEnum"}, 0 + ), + CounterAssertion( + "worker_repo_checkpoints_failed_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 0, + ), + CounterAssertion( + "worker_checkpoints_ended_total", {"flow": "DecoratedEnum"}, 1 + ), + CounterAssertion( + "worker_repo_checkpoints_ended_total", + {"flow": "DecoratedEnum", "repoid": f"{repoid}"}, + 1, + ), + CounterAssertion( + "worker_checkpoints_events_total", + {"flow": "DecoratedEnum", "checkpoint": "BRANCH_2_SUCCESS"}, + 1, + ), + CounterAssertion( + "worker_repo_checkpoints_events_total", + { + "flow": "DecoratedEnum", + "checkpoint": "BRANCH_2_SUCCESS", + "repoid": f"{repoid}", + }, + 1, + ), + ] + with CounterAssertionSet(counter_assertions): + DecoratedEnum.log(DecoratedEnum.BRANCH_2_SUCCESS) + + def test_serialize_between_tasks(self): + """ + BaseFlow must inherit from str in order for our checkpoints dict to be + readily serializable to JSON for passing between celery tasks. + + We encode the flow name and checkpoint name in the string value so that + we can validate when we deserialize. Make sure that all works. + """ + original = { + TestEnum1.A: 1337, + TestEnum1.B: 9001, + } + serialized = json.dumps(original) + deserialized = json.loads(serialized) + + assert serialized == '{"A": 1337, "B": 9001}' + assert deserialized == { + "A": 1337, + "B": 9001, + } + + def test_sort_order(self): + assert TestEnum1.A == TestEnum1.A + assert TestEnum1.A < TestEnum1.B + assert TestEnum1.C > TestEnum1.B + assert SortOrderEnum.C < SortOrderEnum.B + assert SortOrderEnum.A > SortOrderEnum.B + + SortOrderEnum.log(SortOrderEnum.C) + SortOrderEnum.log(SortOrderEnum.B) + data = SortOrderEnum._data_from_log_context() + SortOrderEnum._subflow_duration(SortOrderEnum.C, SortOrderEnum.B, data=data) diff --git a/apps/worker/helpers/tests/unit/test_clock.py b/apps/worker/helpers/tests/unit/test_clock.py new file mode 100644 index 0000000000..280299f442 --- /dev/null +++ b/apps/worker/helpers/tests/unit/test_clock.py @@ -0,0 +1,36 @@ +from datetime import datetime, timezone + +import pytest +from freezegun import freeze_time + +from helpers.clock import ( + get_seconds_to_next_hour, + get_utc_now, + get_utc_now_as_iso_format, +) + + +def test_get_utc_now(): + res = get_utc_now() + assert isinstance(res, datetime) + assert res.tzinfo == timezone.utc + + +def test_get_utc_now_as_iso_format(): + res = get_utc_now_as_iso_format() + assert isinstance(res, str) + assert isinstance(datetime.fromisoformat(res), datetime) + + +@pytest.mark.parametrize( + "timestamp, expected", + [ + ("2024-04-22T10:22:00", 38 * 60), + ("2024-04-22T10:22:59", 38 * 60 - 59), + ("2024-04-22T10:59:59", 1), + ("2024-04-22T10:59:00", 60), + ], +) +def test_get_seconds_to_next_hour(timestamp, expected): + with freeze_time(timestamp): + assert get_seconds_to_next_hour() == expected diff --git a/apps/worker/helpers/tests/unit/test_commit_error.py b/apps/worker/helpers/tests/unit/test_commit_error.py new file mode 100644 index 0000000000..954cc8c77f --- /dev/null +++ b/apps/worker/helpers/tests/unit/test_commit_error.py @@ -0,0 +1,23 @@ +from database.enums import CommitErrorTypes +from database.tests.factories import CommitFactory +from helpers.save_commit_error import save_commit_error + + +class TestSaveCommitError(object): + def test_save_commit_error(self, mocker, dbsession): + commit = CommitFactory.create() + dbsession.add(commit) + + save_commit_error(commit, error_code=CommitErrorTypes.REPO_BOT_INVALID.value) + + assert commit.errors + assert len(commit.errors) == 1 + + def test_save_commit_error_already_saved(self, mocker, dbsession): + commit = CommitFactory.create() + dbsession.add(commit) + + save_commit_error(commit, error_code=CommitErrorTypes.REPO_BOT_INVALID.value) + save_commit_error(commit, error_code=CommitErrorTypes.REPO_BOT_INVALID.value) + + assert len(commit.errors) == 1 diff --git a/apps/worker/helpers/tests/unit/test_components.py b/apps/worker/helpers/tests/unit/test_components.py new file mode 100644 index 0000000000..8ee4c81f8b --- /dev/null +++ b/apps/worker/helpers/tests/unit/test_components.py @@ -0,0 +1,26 @@ +from helpers.components import Component + + +def test_from_dict(): + default_component: Component = Component.from_dict({}) + assert default_component.name == "" + assert default_component.component_id == "" + assert default_component.statuses == [] + assert default_component.paths == [] + assert default_component.flag_regexes == [] + assert default_component.get_display_name() == "default_component" + + +def test_get_display_name(): + component: Component = Component.from_dict({"component_id": "myID"}) + assert component.get_display_name() == "myID" + component.name = "myName" + assert component.get_display_name() == "myName" + + +def test_get_matching_flags(): + component: Component = Component.from_dict({"flag_regexes": ["teamA.*", "batata"]}) + matched_flags = component.get_matching_flags( + ["teamA/unit", "teamB/unit", "teamA/core", "batata", "random"] + ) + assert sorted(matched_flags) == ["batata", "teamA/core", "teamA/unit"] diff --git a/apps/worker/helpers/tests/unit/test_config.py b/apps/worker/helpers/tests/unit/test_config.py new file mode 100644 index 0000000000..664ebf8873 --- /dev/null +++ b/apps/worker/helpers/tests/unit/test_config.py @@ -0,0 +1,107 @@ +import pytest + +from helpers.config import should_write_data_to_storage_config_check + + +@pytest.mark.parametrize( + "inner_config, func_args, result", + [ + ( + { + "repo_ids": [], + "report_details_files_array": True, + "commit_report": False, + }, + ("report_details_files_array", False, 1), + False, + ), + ( + { + "repo_ids": [], + "report_details_files_array": True, + "commit_report": False, + }, + ("report_details_files_array", True, 1), + True, + ), + ( + { + "repo_ids": [], + "report_details_files_array": True, + "commit_report": False, + }, + ("commit_report", True, 1), + False, + ), + ( + { + "repo_ids": [1], + "report_details_files_array": True, + "commit_report": False, + }, + ("commit_report", False, 1), + False, + ), + ( + { + "repo_ids": [1], + "report_details_files_array": True, # True is the same as "codecov_access" + "commit_report": False, + }, + ("report_details_files_array", False, 1), + False, + ), + ( + { + "repo_ids": [1], + "report_details_files_array": "codecov_access", + "commit_report": False, + }, + ("report_details_files_array", False, 1), + False, + ), + ( + { + "repo_ids": [1], + "report_details_files_array": "codecov_access", + "commit_report": False, + }, + ("report_details_files_array", True, 1), + True, + ), + ( + { + "repo_ids": [], + "report_details_files_array": "general_access", + "commit_report": False, + }, + ("report_details_files_array", False, 1), + True, + ), + ( + { + "repo_ids": [1], + "report_details_files_array": "restricted_access", + "commit_report": False, + }, + ("report_details_files_array", True, 1), + True, + ), + ], +) +def test_should_write_data_to_storage_config_check( + inner_config, func_args, result, mocker +): + config = {"setup": {"save_report_data_in_storage": inner_config}} + + def fake_config(*path, default=None): + curr = config + for key in path: + if key in curr: + curr = curr.get(key) + else: + return default + return curr + + mocker.patch("helpers.config.get_config", side_effect=fake_config) + assert should_write_data_to_storage_config_check(*func_args) == result diff --git a/apps/worker/helpers/tests/unit/test_environment.py b/apps/worker/helpers/tests/unit/test_environment.py new file mode 100644 index 0000000000..6f55fe74c6 --- /dev/null +++ b/apps/worker/helpers/tests/unit/test_environment.py @@ -0,0 +1,53 @@ +import os +from pathlib import PosixPath + +from helpers.environment import ( + Environment, + _calculate_current_env, + get_external_dependencies_folder, +) + + +class TestEnvironment(object): + def test_get_current_env(self, mocker): + # CURRENT_ENVIRONMENT is a fallback when RUN_ENV is not supplied + # have to clear out RUN_ENV to test CURRENT_ENVIRONMENT + mocker.patch.dict(os.environ, {}, clear=True) + mocker.patch.dict(os.environ, {"CURRENT_ENVIRONMENT": ""}) + assert _calculate_current_env() == Environment.production + + def test_get_current_env_local(self, mocker): + mocker.patch.dict(os.environ, {}, clear=True) + mocker.patch.dict(os.environ, {"CURRENT_ENVIRONMENT": "local"}) + assert _calculate_current_env() == Environment.local + + def test_get_current_env_enterprise(self, mocker): + mocker.patch.dict(os.environ, {}, clear=True) + mocker.patch.dict(os.environ, {"CURRENT_ENVIRONMENT": "local"}) + mock_path_exists = mocker.patch( + "helpers.environment.os.path.exists", return_value=True + ) + mocker.patch( + "helpers.environment._get_current_folder", return_value="/home/path" + ) + assert _calculate_current_env() == Environment.enterprise + mock_path_exists.assert_called_with(PosixPath("/home/path/src/is_enterprise")) + + def test_get_external_dependencies_folder(self, mock_configuration): + assert get_external_dependencies_folder() == "./external_deps" + mock_configuration.set_params( + {"services": {"external_dependencies_folder": "some/folder"}} + ) + assert get_external_dependencies_folder() == "some/folder" + + def test_get_current_env_run_env(self, mocker): + mocker.patch.dict(os.environ, {"RUN_ENV": "ENTERPRISE"}) + assert _calculate_current_env() == Environment.enterprise + mocker.patch.dict(os.environ, {"RUN_ENV": "DEV"}) + assert _calculate_current_env() == Environment.local + mocker.patch.dict(os.environ, {"RUN_ENV": "STAGING"}) + assert _calculate_current_env() == Environment.production + mocker.patch.dict(os.environ, {"RUN_ENV": "TESTING"}) + assert _calculate_current_env() == Environment.production + mocker.patch.dict(os.environ, {"RUN_ENV": "PRODUCTION"}) + assert _calculate_current_env() == Environment.production diff --git a/apps/worker/helpers/tests/unit/test_github_installation.py b/apps/worker/helpers/tests/unit/test_github_installation.py new file mode 100644 index 0000000000..6707810852 --- /dev/null +++ b/apps/worker/helpers/tests/unit/test_github_installation.py @@ -0,0 +1,33 @@ +from sqlalchemy.orm import Session + +from database.models.core import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + OwnerInstallationNameToUseForTask, +) +from database.tests.factories.core import OwnerFactory +from helpers.github_installation import get_installation_name_for_owner_for_task + + +def test_get_installation_name_for_owner_for_task(dbsession: Session): + owner = OwnerFactory(service="github") + other_owner = OwnerFactory() + task_name = "app.tasks.notify.Notify" + installation_task_config = OwnerInstallationNameToUseForTask( + owner=owner, + ownerid=owner.ownerid, + installation_name="my_installation", + task_name=task_name, + ) + dbsession.add_all([owner, other_owner, installation_task_config]) + dbsession.flush() + assert ( + get_installation_name_for_owner_for_task(task_name, owner) == "my_installation" + ) + assert ( + get_installation_name_for_owner_for_task(task_name, other_owner) + == GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + assert ( + get_installation_name_for_owner_for_task("other_task", owner) + == GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) diff --git a/apps/worker/helpers/tests/unit/test_health_check.py b/apps/worker/helpers/tests/unit/test_health_check.py new file mode 100644 index 0000000000..66acdab7b5 --- /dev/null +++ b/apps/worker/helpers/tests/unit/test_health_check.py @@ -0,0 +1,25 @@ +import pytest + +from helpers.health_check import ( + HEALTH_CHECK_DEFAULT_INTERVAL_SECONDS, + get_health_check_interval_seconds, +) + + +@pytest.mark.parametrize( + "input,expected", + [ + (-10, HEALTH_CHECK_DEFAULT_INTERVAL_SECONDS), + (0, HEALTH_CHECK_DEFAULT_INTERVAL_SECONDS), + (None, HEALTH_CHECK_DEFAULT_INTERVAL_SECONDS), + ("batata", HEALTH_CHECK_DEFAULT_INTERVAL_SECONDS), + (20, 20), + ("5", 5), + ], +) +def test_get_interval_seconds(mock_configuration, input, expected): + mock_configuration.set_params( + {"setup": {"tasks": {"healthcheck": {"interval_seconds": input}}}} + ) + interval = get_health_check_interval_seconds() + assert interval == expected diff --git a/apps/worker/helpers/tests/unit/test_log_context.py b/apps/worker/helpers/tests/unit/test_log_context.py new file mode 100644 index 0000000000..27bc58dd64 --- /dev/null +++ b/apps/worker/helpers/tests/unit/test_log_context.py @@ -0,0 +1,207 @@ +import asyncio + +from asgiref.sync import async_to_sync +from sqlalchemy.exc import IntegrityError + +from database.tests.factories.core import CommitFactory, OwnerFactory, RepositoryFactory +from helpers.log_context import LogContext, get_log_context, set_log_context + + +def create_db_records(dbsession): + owner = OwnerFactory.create( + service="github", + username="codecove2e", + unencrypted_oauth_token="test76zow6xgh7modd88noxr245j2z25t4ustoff", + plan="users-basic", + ) + dbsession.add(owner) + + repo = RepositoryFactory.create( + owner=owner, + yaml={"codecov": {"max_report_age": "1y ago"}}, + name="example-python", + ) + dbsession.add(repo) + + commit = CommitFactory.create( + message="", + commitid="c5b67303452bbff57cc1f49984339cde39eb1db5", + repository=repo, + ) + dbsession.add(commit) + + dbsession.commit() + dbsession.expire(owner) + dbsession.expire(repo) + dbsession.expire(commit) + + return owner, repo, commit + + +def test_populate_just_owner(dbsession): + owner, _repo, _commit = create_db_records(dbsession) + log_context = LogContext(owner_id=owner.ownerid) + log_context.populate_from_sqlalchemy(dbsession) + + assert log_context == LogContext( + owner_id=owner.ownerid, + owner_username="codecove2e", + owner_service="github", + owner_plan="users-basic", + ) + + +def test_populate_just_repo(dbsession): + owner, repo, _commit = create_db_records(dbsession) + log_context = LogContext(repo_id=repo.repoid) + log_context.populate_from_sqlalchemy(dbsession) + + assert log_context == LogContext( + repo_id=repo.repoid, + repo_name="example-python", + owner_id=owner.ownerid, + owner_username="codecove2e", + owner_service="github", + owner_plan="users-basic", + ) + + +def test_populate_just_commit_sha(dbsession): + _owner, _repo, commit = create_db_records(dbsession) + log_context = LogContext(commit_sha=commit.commitid) + log_context.populate_from_sqlalchemy(dbsession) + + assert log_context == LogContext(commit_sha=commit.commitid) + + +def test_populate_just_commit_id(dbsession): + _owner, _repo, commit = create_db_records(dbsession) + log_context = LogContext(commit_id=commit.id_) + log_context.populate_from_sqlalchemy(dbsession) + + assert log_context == LogContext( + commit_id=commit.id_, + ) + + +def test_populate_repo_and_commit_sha(dbsession): + owner, repo, commit = create_db_records(dbsession) + log_context = LogContext(repo_id=repo.repoid, commit_sha=commit.commitid) + log_context.populate_from_sqlalchemy(dbsession) + + assert log_context == LogContext( + repo_id=repo.repoid, + repo_name="example-python", + owner_id=owner.ownerid, + owner_username="codecove2e", + owner_service="github", + owner_plan="users-basic", + commit_sha=commit.commitid, + ) + + +def test_populate_ignores_db_exceptions(dbsession, mocker): + owner, repo, commit = create_db_records(dbsession) + log_context = LogContext(repo_id=repo.repoid, commit_sha=commit.commitid) + mocker.patch.object(dbsession, "query", side_effect=IntegrityError("", {}, None)) + + # If this succeeds, the exception thrown by dbsession was ignored + log_context.populate_from_sqlalchemy(dbsession) + + +def test_set_and_get_log_context(dbsession): + log_context = LogContext(repo_id=1, commit_sha="abcde", commit_id=2, owner_id=3) + set_log_context(log_context) + + assert get_log_context() == log_context + + async def check_context_in_coroutine(): + coro_log_context = get_log_context() + assert coro_log_context == log_context + + # Check that the ContextVar is propagated through multiple ways of running + # async functions + asyncio.run(check_context_in_coroutine()) + async_to_sync(check_context_in_coroutine)() + + +def test_as_dict(dbsession, mocker): + owner, repo, commit = create_db_records(dbsession) + log_context = LogContext( + commit_sha=commit.commitid, repo_id=repo.repoid, task_name="foo", task_id="bar" + ) + log_context.populate_from_sqlalchemy(dbsession) + + mock_span = mocker.Mock() + mock_span.trace_id = 123 + mocker.patch("helpers.log_context.get_current_span", return_value=mock_span) + + # `_populated_from_db` is a dataclass field that we want to strip + # `sentry_trace_id` is a property that we want to include + assert log_context.as_dict() == { + "task_name": "foo", + "task_id": "bar", + "commit_id": None, + "commit_sha": commit.commitid, + "repo_id": repo.repoid, + "repo_name": repo.name, + "owner_id": owner.ownerid, + "owner_username": owner.username, + "owner_service": owner.service, + "owner_plan": owner.plan, + "sentry_trace_id": 123, + "checkpoints_data": {}, + } + + +def test_add_to_log_record(dbsession): + owner, repo, commit = create_db_records(dbsession) + log_context = LogContext(commit_id=commit.id_, task_name="foo", task_id="bar") + log_context.populate_from_sqlalchemy(dbsession) + + log_record = {} + log_context.add_to_log_record(log_record) + + expected_dict = log_context.as_dict() + expected_dict.pop("checkpoints_data", None) + assert log_record["context"] == expected_dict + + +def test_add_to_sentry(dbsession, mocker): + mock_set_tags = mocker.patch("sentry_sdk.set_tags") + + owner, repo, commit = create_db_records(dbsession) + log_context = LogContext(commit_id=commit.id_, task_name="foo", task_id="bar") + log_context.populate_from_sqlalchemy(dbsession) + + # Calls `log_context.set_to_sentry()` + set_log_context(log_context) + + expected_sentry_fields = log_context.as_dict() + expected_sentry_fields.pop("sentry_trace_id") + expected_sentry_fields.pop("checkpoints_data") + mock_set_tags.assert_called_with(expected_sentry_fields) + + +def test_log_context_populate_from_sqlalchemy_is_disabled( + mock_configuration, dbsession +): + mock_configuration.set_params({"setup": {"log_context": {"populate": False}}}) + log_context = LogContext(task_name="foo", task_id="bar") + log_context.populate_from_sqlalchemy(dbsession) + + assert log_context == LogContext(task_name="foo", task_id="bar") + assert log_context.as_dict() == { + "task_name": "foo", + "task_id": "bar", + "commit_id": None, + "commit_sha": None, + "repo_id": None, + "repo_name": None, + "owner_id": None, + "owner_username": None, + "owner_service": None, + "sentry_trace_id": None, + "checkpoints_data": {}, + "owner_plan": None, + } diff --git a/apps/worker/helpers/tests/unit/test_logging_config.py b/apps/worker/helpers/tests/unit/test_logging_config.py new file mode 100644 index 0000000000..01ca878033 --- /dev/null +++ b/apps/worker/helpers/tests/unit/test_logging_config.py @@ -0,0 +1,60 @@ +from helpers.environment import Environment +from helpers.log_context import LogContext, set_log_context +from helpers.logging_config import ( + CustomLocalJsonFormatter, + config_dict, + get_logging_config_dict, +) + + +class TestLoggingConfig(object): + def test_local_formatter(self): + log_record = {"levelname": "weird_level", "message": "This is a message"} + cljf = CustomLocalJsonFormatter() + res = cljf.jsonify_log_record(log_record) + assert "weird_level: This is a message \n {}" == res + + def test_local_formatter_with_exc_info(self): + log_record = { + "levelname": "weird_level", + "message": "This is a message", + "exc_info": "Line\nWith\nbreaks", + } + cljf = CustomLocalJsonFormatter() + res = cljf.jsonify_log_record(log_record) + assert "weird_level: This is a message \n {}\nLine\nWith\nbreaks" == res + + def test_get_logging_config_dict(self, mocker): + get_current_env = mocker.patch("helpers.logging_config.get_current_env") + get_current_env.return_value = Environment.production + assert get_logging_config_dict() == config_dict + + def test_add_fields_empty_log_context(self, mocker): + set_log_context(LogContext()) + log_record, record, message_dict = {}, mocker.MagicMock(), {"message": "aaa"} + log_formatter = CustomLocalJsonFormatter() + log_formatter.add_fields(log_record, record, message_dict) + + expected_log_record = { + "message": "aaa", + "method_calls": [], + } + LogContext().add_to_log_record(expected_log_record) + assert log_record == expected_log_record + + def test_add_fields_populated_log_context(self, mocker): + log_context = LogContext( + task_name="lkjhg", task_id="abcdef", repo_id=5, owner_id=3 + ) + set_log_context(log_context) + + log_record, record, message_dict = {}, mocker.MagicMock(), {"message": "aaa"} + log_formatter = CustomLocalJsonFormatter() + log_formatter.add_fields(log_record, record, message_dict) + + expected_log_record = { + "message": "aaa", + "method_calls": [], + } + log_context.add_to_log_record(expected_log_record) + assert log_record == expected_log_record diff --git a/apps/worker/helpers/tests/unit/test_match.py b/apps/worker/helpers/tests/unit/test_match.py new file mode 100644 index 0000000000..82b8314bb4 --- /dev/null +++ b/apps/worker/helpers/tests/unit/test_match.py @@ -0,0 +1,10 @@ +from helpers.match import match + + +def test_match(): + assert match(["old.*"], "new_branch") is False + assert match(["new.*"], "new_branch") is True + assert match(["old.*"], "new_branch") is False + # Negative matches return False + assert match(["!new.*"], "new_branch") is False + assert match(["!new_branch"], "new_branch") is False diff --git a/apps/worker/helpers/tests/unit/test_number.py b/apps/worker/helpers/tests/unit/test_number.py new file mode 100644 index 0000000000..91da08a837 --- /dev/null +++ b/apps/worker/helpers/tests/unit/test_number.py @@ -0,0 +1,32 @@ +from decimal import Decimal + +import pytest + +from helpers.number import precise_round + + +@pytest.mark.parametrize( + "number,precision,rounding,expected_rounding", + [ + (Decimal("1.129"), 2, "down", Decimal("1.12")), + (Decimal("1.121"), 2, "up", Decimal("1.13")), + (Decimal("1.125"), 1, "nearest", Decimal("1.1")), + (Decimal("1.18"), 1, "nearest", Decimal("1.2")), + (Decimal("1.15"), 1, "nearest", Decimal("1.2")), + (Decimal("1.25"), 1, "nearest", Decimal("1.2")), + ], + ids=[ + "number rounds down", + "number rounds up", + "number rounds nearest (down)", + "number rounds nearest (up)", + "number rounds half-even (up)", + "number rounds half-even (down)", + ], +) +def test_precise_round( + number: Decimal, precision: int, rounding: str, expected_rounding: Decimal +): + assert expected_rounding == precise_round( + number, precision=precision, rounding=rounding + ) diff --git a/apps/worker/helpers/tests/unit/test_sentry.py b/apps/worker/helpers/tests/unit/test_sentry.py new file mode 100644 index 0000000000..6941cbc1fc --- /dev/null +++ b/apps/worker/helpers/tests/unit/test_sentry.py @@ -0,0 +1,26 @@ +import os + +from helpers.sentry import initialize_sentry + + +class TestSentry(object): + def test_initialize_sentry(self, mocker, mock_configuration): + mock_configuration._params["services"] = {"sentry": {"server_dsn": "this_dsn"}} + cluster = "test_env" + mocker.patch.dict( + os.environ, + {"RELEASE_VERSION": "FAKE_VERSION_FOR_YOU", "CLUSTER_ENV": cluster}, + ) + mocked_init = mocker.patch("helpers.sentry.sentry_sdk.init") + mocked_set_tag = mocker.patch("helpers.sentry.sentry_sdk.set_tag") + assert initialize_sentry() is None + mocked_init.assert_called_with( + "this_dsn", + release="worker-FAKE_VERSION_FOR_YOU", + sample_rate=1.0, + traces_sample_rate=1.0, + profiles_sample_rate=1.0, + environment="production", + integrations=mocker.ANY, + ) + mocked_set_tag.assert_called_with("cluster", cluster) diff --git a/apps/worker/helpers/tests/unit/test_string.py b/apps/worker/helpers/tests/unit/test_string.py new file mode 100644 index 0000000000..50a430fe79 --- /dev/null +++ b/apps/worker/helpers/tests/unit/test_string.py @@ -0,0 +1,35 @@ +from helpers.string import EscapeEnum, Replacement, StringEscaper, shorten_file_paths + + +def test_string_escaper(): + escape_def = [ + Replacement(["1"], "2", EscapeEnum.APPEND), + Replacement(["3"], "4", EscapeEnum.PREPEND), + Replacement(["5"], "6", EscapeEnum.REPLACE), + ] + + escaper = StringEscaper(escape_def) + + assert escaper.replace("123456") == "12243466" + + +def test_shorten_file_paths(): + string = """Error: expect(received).toBe(expected) // Object.is equality + +Expected: 1 +Received: -1 + at Object.<anonymous> (/Users/users/dir/repo/demo/calculator/calculator.test.ts:10:31) + at Promise.then.completed (/Users/users/dir/repo/node_modules/jest-circus/build/utils.js:298:28) + at Promise.then.completed (build/utils.js:298:28) + +""" + expected = """Error: expect(received).toBe(expected) // Object.is equality + +Expected: 1 +Received: -1 + at Object.<anonymous> (.../demo/calculator/calculator.test.ts:10:31) + at Promise.then.completed (.../jest-circus/build/utils.js:298:28) + at Promise.then.completed (build/utils.js:298:28) + +""" + assert expected == shorten_file_paths(string) diff --git a/apps/worker/helpers/tests/unit/test_version.py b/apps/worker/helpers/tests/unit/test_version.py new file mode 100644 index 0000000000..f139f73047 --- /dev/null +++ b/apps/worker/helpers/tests/unit/test_version.py @@ -0,0 +1,13 @@ +import os + +from helpers.version import get_current_version + + +class TestVersion(object): + def test_get_current_version(self, mocker): + mocker.patch.dict(os.environ, {"RELEASE_VERSION": "HAHA"}) + assert get_current_version() == "HAHA" + + def test_get_current_version_no_set_version(self, mocker): + mocker.patch.dict(os.environ, {"nada": "nada"}, clear=True) + assert get_current_version() == "NO_VERSION" diff --git a/apps/worker/helpers/timeseries.py b/apps/worker/helpers/timeseries.py new file mode 100644 index 0000000000..89dbdc47a5 --- /dev/null +++ b/apps/worker/helpers/timeseries.py @@ -0,0 +1,5 @@ +from shared.config import get_config + + +def backfill_max_batch_size() -> int: + return get_config("setup", "timeseries", "backfill_max_batch_size", default=500) diff --git a/apps/worker/helpers/token_refresh.py b/apps/worker/helpers/token_refresh.py new file mode 100644 index 0000000000..dcb380f722 --- /dev/null +++ b/apps/worker/helpers/token_refresh.py @@ -0,0 +1,35 @@ +import logging +from typing import Callable, Dict + +from shared.encryption.token import encode_token + +from database.models.core import Owner +from services.encryption import encryptor + +log = logging.getLogger(__name__) + + +def get_token_refresh_callback(owner: Owner) -> Callable[[Dict], None]: + """ + Produces a callback function that will encode and update the oauth token of a user. + This callback is passed to the TorngitAdapter for the service. + """ + # Some tokens don't have to be refreshed (GH integration, default bots) + # They don't belong to any owners. + if owner is None: + return None + + service = owner.service + if service == "bitbucket" or service == "bitbucket_server": + return None + + async def callback(new_token: Dict) -> None: + log.info( + "Saving new token after refresh", + extra=dict(owner=owner.username, ownerid=owner.ownerid), + ) + string_to_save = encode_token(new_token) + oauth_token = encryptor.encode(string_to_save).decode() + owner.oauth_token = oauth_token + + return callback diff --git a/apps/worker/helpers/version.py b/apps/worker/helpers/version.py new file mode 100644 index 0000000000..3775778725 --- /dev/null +++ b/apps/worker/helpers/version.py @@ -0,0 +1,5 @@ +import os + + +def get_current_version() -> str: + return os.getenv("RELEASE_VERSION", "NO_VERSION") diff --git a/apps/worker/json/ci.json b/apps/worker/json/ci.json new file mode 100644 index 0000000000..02777e53a3 --- /dev/null +++ b/apps/worker/json/ci.json @@ -0,0 +1,177 @@ +{ + "travis": { + "title": "Travis-CI", + "icon": "travis", + "require_token_when_public": false, + "instructions": "travis", + "build_url": "https://travis-ci.{tld}/{owner[username]}/{repo[name]}/jobs/{session.job}" + }, + "azure-pipelines": { + "title": "Azure", + "icon": "custom", + "require_token_when_public": true, + "instructions": "generic", + "build_url": null + }, + "azure_pipelines": { + "title": "Azure", + "icon": "custom", + "require_token_when_public": true, + "instructions": "generic", + "build_url": null + }, + "azure-devops": { + "title": "Azure", + "icon": "custom", + "require_token_when_public": true, + "instructions": "generic", + "build_url": null + }, + "docker": { + "title": "Docker", + "icon": "custom", + "require_token_when_public": true, + "instructions": "generic", + "build_url": null + }, + "buildbot": { + "title": "Buildbot", + "icon": "buildbot", + "require_token_when_public": true, + "instructions": "generic", + "build_url": null + }, + "codefresh": { + "title": "Codefresh", + "icon": "custom", + "require_token_when_public": true, + "instructions": "generic", + "build_url": "https://g.codefresh.io/repositories/{owner[username]}/{repo[name]}/builds/{session.build}" + }, + "circleci": { + "title": "CircleCI", + "icon": "circleci", + "require_token_when_public": false, + "instructions": "circleci", + "build_url": "https://circleci.com/gh/{owner[username]}/{repo[name]}/{session.build}#tests/containers/{session.job}" + }, + "buddybuild": { + "title": "buddybuild", + "icon": "custom", + "require_token_when_public": true, + "instructions": "generic", + "build_url": null + }, + "solano": { + "title": "Solano", + "icon": "custom", + "require_token_when_public": true, + "instructions": "generic", + "build_url": null + }, + "teamcity": { + "title": "TeamCity", + "icon": "teamcity", + "require_token_when_public": true, + "instructions": "teamcity", + "build_url": null + }, + "appveyor": { + "title": "AppVeyor", + "icon": "appveyor", + "require_token_when_public": false, + "instructions": "appveyor", + "build_url": "https://ci.appveyor.com/project/{owner[username]}/{repo[name]}/build/job/{session.build}" + }, + "wercker": { + "title": "Wercker", + "icon": "wercker", + "require_token_when_public": true, + "instructions": "generic", + "build_url": "https://app.wercker.com/#build/{session.build}" + }, + "shippable": { + "title": "Shippable", + "icon": "shippable", + "require_token_when_public": true, + "instructions": "generic", + "build_url": null + }, + "codeship": { + "title": "Codeship", + "icon": "codeship", + "require_token_when_public": true, + "instructions": "generic", + "build_url": null + }, + "drone.io": { + "title": "Drone.io", + "icon": "drone.io", + "require_token_when_public": true, + "instructions": "generic", + "build_url": null + }, + "jenkins": { + "title": "Jenkins", + "icon": "jenkins", + "require_token_when_public": true, + "instructions": "generic", + "build_url": null + }, + "semaphore": { + "title": "Semaphore", + "icon": "semaphore", + "require_token_when_public": true, + "instructions": "generic", + "build_url": "https://semaphoreapp.com/{owner[username]}/{repo[name]}/branches/{commit[branch]}/builds/{session.build}" + }, + "gitlab": { + "title": "GitLab CI", + "icon": "gitlab", + "require_token_when_public": true, + "instructions": "generic", + "build_url": "https://gitlab.com/{owner[username]}/{repo[name]}/builds/{session.build}" + }, + "bamboo": { + "title": "Bamboo", + "icon": "bamboo", + "require_token_when_public": true, + "instructions": "generic", + "build_url": null + }, + "buildkite": { + "title": "BuildKite", + "icon": "buildkite", + "require_token_when_public": true, + "instructions": "generic", + "build_url": null + }, + "bitrise": { + "title": "Bitrise", + "icon": "bitrise", + "require_token_when_public": true, + "instructions": "generic", + "build_url": null + }, + "greenhouse": { + "title": "Greenhouse", + "icon": "greenhouse", + "require_token_when_public": true, + "instructions": "generic", + "build_url": null + }, + "heroku": { + "title": "Heroku", + "icon": "heroku", + "require_token_when_public": true, + "instructions": "generic", + "build_url": null + }, + "custom": { + "title": "Custom", + "icon": "custom", + "require_token_when_public": true, + "instructions": "generic", + "build_url": null + } +} diff --git a/apps/worker/main.py b/apps/worker/main.py new file mode 100644 index 0000000000..aac995974c --- /dev/null +++ b/apps/worker/main.py @@ -0,0 +1,154 @@ +import os + +import django + +# we're moving this before we create the Celery object +# so that celery can detect Django is being used +# using the Django fixup will help fix some database issues +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_scaffold.settings") +django.setup() + +import logging # noqa: E402 +import sys # noqa: E402 + +import click # noqa: E402 +import shared.storage # noqa: E402 +from celery.signals import worker_process_shutdown # noqa: E402 +from prometheus_client import REGISTRY, CollectorRegistry, multiprocess # noqa: E402 +from shared.celery_config import BaseCeleryConfig # noqa: E402 +from shared.config import get_config # noqa: E402 +from shared.license import startup_license_logging # noqa: E402 +from shared.metrics import start_prometheus # noqa: E402 +from shared.storage.exceptions import BucketAlreadyExistsError # noqa: E402 + +import app # noqa: E402 +from helpers.environment import get_external_dependencies_folder # noqa: E402 +from helpers.version import get_current_version # noqa: E402 + +log = logging.getLogger(__name__) + +initialization_text = """ + _____ _ + / ____| | | +| | ___ __| | ___ ___ _____ __ +| | / _ \\ / _` |/ _ \\/ __/ _ \\ \\ / / +| |___| (_) | (_| | __/ (_| (_) \\ V / + \\_____\\___/ \\__,_|\\___|\\___\\___/ \\_/ + {version} + +""" + + +@click.group() +@click.pass_context +def cli(ctx: click.Context): + pass + + +@cli.command() +def test(): + raise click.ClickException("System not suitable to run TEST mode") + + +@cli.command() +def web(): + raise click.ClickException("System not suitable to run WEB mode") + + +@worker_process_shutdown.connect +def mark_process_dead(pid, exitcode, **kwargs): + multiprocess.mark_process_dead(pid) + + +def setup_worker(): + print(initialization_text.format(version=get_current_version())) # noqa: T201 + + if getattr(sys, "frozen", False): + # Only for enterprise builds + external_deps_folder = get_external_dependencies_folder() + log.info(f"External dependencies folder configured to {external_deps_folder}") + sys.path.append(external_deps_folder) + + registry = REGISTRY + if "PROMETHEUS_MULTIPROC_DIR" in os.environ: + registry = CollectorRegistry() + multiprocess.MultiProcessCollector(registry) + + start_prometheus(9996, registry=registry) # 9996 is an arbitrary port number + + minio_config = get_config("services", "minio", default={}) + auto_create_bucket = minio_config.get("auto_create_bucket", False) + if auto_create_bucket: + try: + bucket_name = minio_config.get("bucket", "archive") + region = minio_config.get("region", "us-east-1") + + # note that this is a departure from the old default behavior. + # This is intended as the bucket will exist in most cases where IAC or manual setup is used + log.info("Initializing bucket %s", bucket_name) + + # this storage client is only used to create the bucket so it doesn't need to be + # aware of the repoid + storage_client = shared.storage.get_appropriate_storage_service() + storage_client.create_root_storage(bucket_name, region) + except BucketAlreadyExistsError: + pass + + startup_license_logging() + + +@cli.command() +@click.option("--name", envvar="HOSTNAME", default="worker", help="Node name") +@click.option( + "--concurrency", type=int, default=2, help="Number for celery concurrency" +) +@click.option("--debug", is_flag=True, default=False, help="Enable celery debug mode") +@click.option( + "--queue", + multiple=True, + default=["celery"], + help="Queues to listen to for this worker", +) +def worker(name: str, concurrency: int, debug: bool, queue: list[str]): + setup_worker() + args = [ + "worker", + "-n", + name, + "-c", + concurrency, + "-l", + ("debug" if debug else "info"), + ] + if get_config("setup", "celery_queues_enabled", default=True): + actual_queues = _get_queues_param_from_queue_input(queue) + args += ["-Q", actual_queues] + + if get_config("setup", "celery_beat_enabled", default=True): + args += ["-B", "-s", "/home/codecov/celerybeat-schedule"] + + return app.celery_app.worker_main(argv=args) + + +def _get_queues_param_from_queue_input(queues: list[str]) -> str: + # We always run the health_check queue to make sure the healthcheck is performed + # And also to avoid that queue fillign up with no workers to consume from it + + # Support passing comma separated values, as those will be split again: + joined_queues = ",".join(queues) + enterprise_queues = ["enterprise_" + q for q in joined_queues.split(",")] + all_queues = [ + joined_queues, + *enterprise_queues, + BaseCeleryConfig.health_check_default_queue, + ] + + return ",".join(all_queues) + + +def main(): + cli(obj={}) + + +if __name__ == "__main__": + main() diff --git a/apps/worker/manage.py b/apps/worker/manage.py new file mode 100755 index 0000000000..48bcbb58a5 --- /dev/null +++ b/apps/worker/manage.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" + +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_scaffold.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/apps/worker/migrate-timeseries.sh b/apps/worker/migrate-timeseries.sh new file mode 100644 index 0000000000..1f8ebbfa85 --- /dev/null +++ b/apps/worker/migrate-timeseries.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +echo "Running Timeseries Django migrations" +prefix="" +if [ -f "/usr/local/bin/berglas" ]; then + prefix="berglas exec --" +fi + +$prefix python migrate_timeseries.py diff --git a/apps/worker/migrate.sh b/apps/worker/migrate.sh new file mode 100644 index 0000000000..2d57bd4af8 --- /dev/null +++ b/apps/worker/migrate.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +# Command ran by k8s to run migrations for worker +echo "Running Django migrations" +prefix="" +if [ -f "/usr/local/bin/berglas" ]; then + prefix="berglas exec --" +fi + +$prefix python manage.py migrate +$prefix python manage.py pgpartition --yes --skip-delete diff --git a/apps/worker/migrate_timeseries.py b/apps/worker/migrate_timeseries.py new file mode 100644 index 0000000000..b40fbe33a9 --- /dev/null +++ b/apps/worker/migrate_timeseries.py @@ -0,0 +1,35 @@ +import logging +import os + +import django +from django.core.management import call_command + +# Setup Django environment +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_scaffold.settings") +django.setup() + +from django.conf import settings # noqa: E402 + +logger = logging.getLogger(__name__) + + +def run_migrate_commands(): + try: + if settings.TA_TIMESERIES_ENABLED: + logger.info("Running ta_timeseries migrations") + call_command( + "migrate", + database="ta_timeseries", + app_label="ta_timeseries", + settings="django_scaffold.settings", + verbosity=1, + ) + else: + logger.info("Skipping ta_timeseries migrations") + + except Exception as e: + logger.error(f"An error occurred: {e}") + + +if __name__ == "__main__": + run_migrate_commands() diff --git a/apps/worker/mypy.ini b/apps/worker/mypy.ini new file mode 100644 index 0000000000..e551d98949 --- /dev/null +++ b/apps/worker/mypy.ini @@ -0,0 +1,17 @@ +[mypy] +explicit_package_bases = True +; disable type checks in tests (for now): +exclude = tests + +; TODO: we eventually want these errors to be enabled: +disable_error_code = attr-defined,import-untyped,name-defined + +; TODO: and we would also want these additional checks to be enabled +; as they are defined in Sentry as well: +; check_untyped_defs = True +; no_implicit_reexport = True +; warn_unreachable = True +; warn_unused_configs = True +; warn_unused_ignores = True +; warn_redundant_casts = True +; enable_error_code = ignore-without-code,redundant-self diff --git a/apps/worker/protobuf/ta_testrun.proto b/apps/worker/protobuf/ta_testrun.proto new file mode 100644 index 0000000000..7b460c43ae --- /dev/null +++ b/apps/worker/protobuf/ta_testrun.proto @@ -0,0 +1,35 @@ +syntax = "proto2"; + +message TestRun { + optional int64 timestamp = 1; + optional string name = 2; + optional string classname = 3; + optional string testsuite = 4; + optional string computed_name = 5; + + enum Outcome { + PASSED = 0; + FAILED = 1; + SKIPPED = 2; + FLAKY_FAILED = 3; + } + + optional Outcome outcome = 6; + + optional string failure_message = 7; + optional float duration_seconds = 8; + + optional int64 repoid = 10; + optional string commit_sha = 11; + + optional string branch_name = 12; + + repeated string flags = 13; + + optional string filename = 14; + optional string framework = 15; + + optional int64 upload_id = 16; + optional bytes flags_hash = 17; + optional bytes test_id = 18; +} diff --git a/apps/worker/pyproject.toml b/apps/worker/pyproject.toml new file mode 100644 index 0000000000..16a2edb482 --- /dev/null +++ b/apps/worker/pyproject.toml @@ -0,0 +1,86 @@ +[project] +name = "worker" +version = "0.1.0" +description = "The codecov worker" +readme = "README.md" +requires-python = "==3.13.*" +dependencies = [ + "asgiref>=3.7.2", + "analytics-python==1.3.0b1", + "billiard>=4.2.1", + "boto3>=1.34", + "celery>=5.3.6", + "click>=8.1.7", + "codecov-ribs==0.1.18", + "django>=4.2.16", + "django-postgres-extra>=2.0.8", + "google-cloud-pubsub>=2.27.1", + "google-cloud-storage>=2.10.0", + "grpcio>=1.66.2", + "httpx>0.23.1", + "jinja2>=3.1.3", + "lxml>=5.3.0", + "mmh3>=5.0.1", + "multidict>=6.1.0", + "openai>=1.2.4", + "orjson>=3.10.11", + "polars==1.12.0", + "proto-plus>=1.25.0", + "psycopg2-binary>=2.9.10", + "protobuf>=5.29.2", + "pydantic>=2.9.0", + "pyjwt>=2.4.0", + "python-dateutil>=2.9.0.post0", + "python-json-logger>=0.1.11", + "python-redis-lock>=4.0.0", + "pyyaml>=6.0.1", + "redis>=4.4.4", + "regex>=2023.12.25", + "requests>=2.32.0", + "sentry-sdk>=2.13.0", + "shared", + "sqlalchemy==1.3.*", + "sqlparse==0.5.0", + "statsd>=3.3.0", + "stripe>=11.4.1", + "test-results-parser", + "timestring", + "zstandard>=0.23.0", +] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +py-modules = [] + +[tool.uv] +dev-dependencies = [ + "coverage>=7.5.0", + "factory-boy>=3.2.0", + "mock>=4.0.3", + "pre-commit>=3.4.0", + "pytest>=8.1.1", + "pytest-asyncio>=0.14.0", + "pytest-celery>=0.0.0", + "pytest-cov>=6.0.0", + "pytest-django>=4.7.0", + "pytest-freezegun>=0.4.2", + "pytest-insta>=0.3.0", + "pytest-mock>=1.13.0", + "pytest-sqlalchemy>=0.2.1", + "respx>=0.20.2", + "ruff>=0.9.8", + "sqlalchemy-utils>=0.41.2", + "time-machine>=2.16.0", + # NOTE: some weird interaction between existing `vcrpy` snapshots and the way + # `oauth2` / `minio` deal with requests forces us to downgrade `urllib3`: + "urllib3==1.26.19", + "vcrpy>=6.0.0", +] + +[tool.uv.sources] +timestring = { git = "https://github.com/codecov/timestring", rev = "d37ceacc5954dff3b5bd2f887936a98a668dda42" } +test-results-parser = { git = "https://github.com/codecov/test-results-parser", rev = "190bbc8a911099749928e13d5fe57f6027ca1e74" } +shared = { path = "../../libs/shared" } diff --git a/apps/worker/pytest.ini b/apps/worker/pytest.ini new file mode 100644 index 0000000000..cfb5b69e25 --- /dev/null +++ b/apps/worker/pytest.ini @@ -0,0 +1,7 @@ +[pytest] +DJANGO_SETTINGS_MODULE = django_scaffold.tests_settings +addopts = --sqlalchemy-connect-url="postgresql://postgres@postgres:5432/test_postgres_sqlalchemy" --ignore-glob=**/test_results* +markers= + integration: integration tests (includes tests with vcrs) + real_checkpoint_logger: prevents use of stubbed CheckpointLogger + real_feature: prevents use of stubbed Feature diff --git a/apps/worker/rollouts/__init__.py b/apps/worker/rollouts/__init__.py new file mode 100644 index 0000000000..6d63131d0b --- /dev/null +++ b/apps/worker/rollouts/__init__.py @@ -0,0 +1,14 @@ +from shared.rollouts import Feature + +# Declare the feature variants and parameters via Django Admin +CARRYFORWARD_BASE_SEARCH_RANGE_BY_OWNER = Feature("carryforward_base_search_range") + +SYNC_PULL_USE_MERGE_COMMIT_SHA = Feature("sync_pull_use_merge_commit_sha") + +CHECKPOINT_ENABLED_REPOSITORIES = Feature("checkpoint_enabled_repositories") + +NEW_TA_TASKS = Feature("new_ta_tasks") + +PARALLEL_COMPONENT_COMPARISON = Feature("parallel_component_comparison") + +TA_TIMESERIES = Feature("ta_timeseries") diff --git a/apps/worker/ruff.toml b/apps/worker/ruff.toml new file mode 100644 index 0000000000..e8523a3576 --- /dev/null +++ b/apps/worker/ruff.toml @@ -0,0 +1,64 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.13 +target-version = "py313" + +[lint] +# Currently only enabled for most F (Pyflakes), Pycodestyle (Error), PERF (Perflint), +# PyLint (Convention, Error), I (isort), and T20 (flake8-print) rules: https://docs.astral.sh/ruff/rules/ +select = ["F", "E", "I", "PLC", "PLE", "PERF", "T20"] +ignore = ["F841", "F405", "F403", "E501", "E712"] + +# Allow fix for all enabled rules (when `--fix`) is provided. +# The preferred method (for now) w.r.t. fixable rules is to manually update the makefile +# with --fix and re-run 'make lint' +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" \ No newline at end of file diff --git a/apps/worker/services/__init__.py b/apps/worker/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/activation.py b/apps/worker/services/activation.py new file mode 100644 index 0000000000..2a87b15c5d --- /dev/null +++ b/apps/worker/services/activation.py @@ -0,0 +1,121 @@ +import logging + +from shared.celery_config import ( + activate_account_user_task_name, + new_user_activated_task_name, +) +from sqlalchemy import func +from sqlalchemy.sql import text + +from app import celery_app +from services.license import ( + calculate_reason_for_not_being_valid, + get_current_license, + get_installation_plan_activated_users, + requires_license, +) + +log = logging.getLogger(__name__) + + +def activate_user(db_session, org_ownerid: int, user_ownerid: int) -> bool: + """ + Attempt to activate the user for the given org + + Returns: + bool: was the user successfully activated + """ + if requires_license(): + # we will not activate if the license is invalid for any reason. + license_status = calculate_reason_for_not_being_valid(db_session) + if license_status is None: + # check if you have an available seat with which to activate. + seat_query = get_installation_plan_activated_users(db_session) + + this_license = get_current_license() + can_activate = all( + result[0] < this_license.number_allowed_users for result in seat_query + ) + # add user_ownerid to orgs, plan activated users. + if can_activate: + query_string = text( + """ + UPDATE owners + set plan_activated_users = array_append_unique(plan_activated_users, :user_ownerid) + where ownerid=:org_ownerid + returning ownerid, + plan_activated_users, + username, + plan_activated_users @> array[:user_ownerid]::int[] as has_access;""" + ) + (activation_success,) = db_session.execute( + query_string, + {"user_ownerid": user_ownerid, "org_ownerid": org_ownerid}, + ).fetchall() + + log.info( + "PR Auto activation attempted", + extra=dict( + org_ownerid=org_ownerid, + author_ownerid=user_ownerid, + activation_success=activation_success, + ), + ) + + return True + else: + log.info( + "Auto activation failed due to no seats remaining", + extra=dict( + org_ownerid=org_ownerid, + author_ownerid=user_ownerid, + activation_success=False, + license_status=license_status, + ), + ) + return False + + else: + log.info( + "Auto activation failed due to invalid license", + extra=dict( + org_ownerid=org_ownerid, + author_ownerid=user_ownerid, + activation_success=False, + license_status=license_status, + ), + ) + return False + + # TODO: we need to decide the best way for this logic to be shared across + # worker and codecov-api - ideally moving logic from database to application layer + (activation_success,) = db_session.query( + func.public.try_to_auto_activate(org_ownerid, user_ownerid) + ).first() + + log.info( + "Auto activation attempted", + extra=dict( + org_ownerid=org_ownerid, + author_ownerid=user_ownerid, + activation_success=activation_success, + ), + ) + return activation_success + + +def schedule_new_user_activated_task(org_ownerid, user_ownerid): + celery_app.send_task( + new_user_activated_task_name, + args=None, + kwargs=dict(org_ownerid=org_ownerid, user_ownerid=user_ownerid), + ) + # Activate the account user if it exists. + celery_app.send_task( + activate_account_user_task_name, + args=None, + kwargs=dict( + user_ownerid=user_ownerid, + org_ownerid=org_ownerid, + ), + ) diff --git a/apps/worker/services/ai_pr_review.py b/apps/worker/services/ai_pr_review.py new file mode 100644 index 0000000000..268848ac7f --- /dev/null +++ b/apps/worker/services/ai_pr_review.py @@ -0,0 +1,378 @@ +import json +import logging +import re +from dataclasses import dataclass +from functools import cached_property +from typing import Dict, List, Optional + +from openai import AsyncOpenAI +from shared.config import get_config +from shared.storage.exceptions import FileNotInStorageError +from shared.torngit.base import TokenType, TorngitBaseAdapter + +from database.models.core import Repository +from helpers.metrics import metrics +from services.archive import ArchiveService +from services.repository import get_repo_provider_service + +log = logging.getLogger(__name__) + +FILE_REGEX = re.compile(r"diff --git a/(.+) b/(.+)") + + +def build_prompt(diff_text: str) -> str: + return f""" + Your purpose is to act as a highly experienced software engineer and provide a thorough + review of code changes and suggest improvements. Do not comment on minor style issues, + missing comments or documentation. Identify and resolve significant concerns to improve + overall code quality. + + You will receive a Git diff where each line has been prefixed with a unique identifer in + square brackets. When referencing lines in this diff use that identifier. + + Format your output as JSON such that there is 1 top-level comment that summarizes your review + and multiple additional comments addressing specific lines in the code with the changes you + deem appropriate. + + The output should have this JSON form: + + {{ + "body": "This is the summary comment", + "comments": [ + {{ + "line_id": 123, + "body": "This is a comment about the code with line ID 123", + }} + ] + }} + + Limit the number of comments to 10 at most. + + Here is the Git diff on which you should base your review: + + {diff_text} + """ + + +async def fetch_openai_completion(prompt: str): + client = AsyncOpenAI(api_key=get_config("services", "openai", "api_key")) + completion = await client.chat.completions.create( + messages=[ + { + "role": "user", + "content": prompt, + } + ], + model="gpt-4", + ) + + output = completion.choices[0].message.content + return output + + +@dataclass(frozen=True) +class LineInfo: + file_path: str + position: int + + +class Diff: + def __init__(self, diff): + self._diff = diff + self._index = {} + self._build_index() + + @cached_property + def preprocessed(self) -> str: + """ + This returns the full diff but with each line prefixed by: + [line_id] (where line_id is just a unique integer) + """ + return "\n".join( + [f"[{i + 1}] {line}" for i, line in enumerate(self._diff.split("\n"))] + ) + + def line_info(self, line_id: int) -> LineInfo: + return self._index[line_id] + + def _build_index(self): + file_path = None + position = None + + for idx, line in enumerate(self._diff.split("\n")): + line_id = idx + 1 + match = FILE_REGEX.match(line) + if match: + # start of a new file, extract the name and reset the position + file_path = match.groups()[-1] + position = None + elif line.startswith("@@"): + # start of new hunk + if position is None: + # 1st hunk of file, start tracking position + position = 0 + else: + # new hunk same file, just ignore but keep tracking position + pass + elif position is not None: + # code line, increment position and keep track of it in the index + position += 1 + self._index[line_id] = LineInfo(file_path=file_path, position=position) + + +@dataclass +class Comment: + body: str + comment_id: Optional[int] = None + + +@dataclass +class ReviewComments: + # top-level comment + body: str + + # line-based code comments + comments: Dict[LineInfo, Comment] + + +class PullWrapper: + def __init__(self, torngit: TorngitBaseAdapter, pullid: int): + self.torngit = torngit + self.pullid = pullid + self._head_sha = None + + @property + def token(self): + return self.torngit.get_token_by_type_if_none(None, TokenType.read) + + async def fetch_diff(self) -> str: + async with self.torngit.get_client() as client: + diff = await self.torngit.api( + client, + "get", + f"/repos/{self.torngit.slug}/pulls/{self.pullid}", + token=self.token, + headers={"Accept": "application/vnd.github.v3.diff"}, + ) + return diff + + async def fetch_head_sha(self) -> str: + if self._head_sha is not None: + return self._head_sha + + async with self.torngit.get_client() as client: + res = await self.torngit.api( + client, + "get", + f"/repos/{self.torngit.slug}/pulls/{self.pullid}", + token=self.token, + ) + self._head_sha = res["head"]["sha"] + return self._head_sha + + async def fetch_review_comments(self): + async with self.torngit.get_client() as client: + page = 1 + while True: + res = await self.torngit.api( + client, + "get", + f"/repos/{self.torngit.slug}/pulls/{self.pullid}/comments?per_page=100&page={page}", + token=self.token, + ) + if len(res) == 0: + break + for item in res: + yield item + page += 1 + + async def create_review(self, commit_sha: str, review_comments: ReviewComments): + body = dict( + commit_id=commit_sha, + body=review_comments.body, + event="COMMENT", + comments=[ + { + "path": line_info.file_path, + "position": line_info.position, + "body": comment.body, + } + for line_info, comment in review_comments.comments.items() + ], + ) + log.info( + "Creating AI PR review", + extra=body, + ) + + async with self.torngit.get_client() as client: + res = await self.torngit.api( + client, + "post", + f"/repos/{self.torngit.slug}/pulls/{self.pullid}/reviews", + token=self.token, + body=body, + ) + return res + + async def update_comment(self, comment: Comment): + log.info( + "Updating comment", + extra=dict( + comment_id=comment.comment_id, + body=comment.body, + ), + ) + async with self.torngit.get_client() as client: + await self.torngit.api( + client, + "patch", + f"/repos/{self.torngit.slug}/pulls/comments/{comment.comment_id}", + token=self.token, + body=dict(body=comment.body), + ) + + +class Review: + def __init__( + self, pull_wrapper: PullWrapper, review_ids: Optional[List[int]] = None + ): + self.pull_wrapper = pull_wrapper + self.review_ids = review_ids or [] + self.diff = None + + async def perform(self) -> Optional[int]: + raw_diff = await self.pull_wrapper.fetch_diff() + self.diff = Diff(raw_diff) + + prompt = build_prompt(self.diff.preprocessed) + log.debug( + "OpenAI prompt", + extra=dict( + prompt=prompt, + ), + ) + + res = await fetch_openai_completion(prompt) + log.debug( + "OpenAI response", + extra=dict( + res=res, + ), + ) + + try: + data = json.loads(res) + except json.decoder.JSONDecodeError: + metrics.incr("ai_pr_review.non_json_completion") + log.error( + "OpenAI completion was expected to be JSON but wasn't", + extra=dict(res=res), + exc_info=True, + ) + return + + try: + review_comments = ReviewComments( + body=data["body"], + comments={ + self.diff.line_info(comment["line_id"]): Comment( + body=comment["body"] + ) + for comment in data["comments"] + }, + ) + except KeyError: + metrics.incr("ai_pr_review.malformed_completion") + log.error( + "OpenAI completion JSON was not formed as expected", + extra=dict(data=data), + exc_info=True, + ) + return + + if len(self.review_ids) > 0: + comments_to_update = [] + async for comment in self.pull_wrapper.fetch_review_comments(): + if comment["pull_request_review_id"] not in self.review_ids: + continue + + line_info = LineInfo( + file_path=comment["path"], + position=comment["position"], + ) + if line_info in review_comments.comments: + # we have an existing comment on this line that we'll need + # to update instead of create + line_comment = review_comments.comments[line_info] + + # we'll update this existing comment + line_comment.comment_id = comment["id"] + comments_to_update.append(line_comment) + + # remove it from the current review comments since those will + # be posted as new comments + del review_comments.comments[line_info] + + for comment in comments_to_update: + await self.pull_wrapper.update_comment(comment) + + if len(review_comments.comments) > 0: + head_commit_sha = await self.pull_wrapper.fetch_head_sha() + if len(self.review_ids) > 0: + # we already made a top-level comment w/ summary of suggestions, + # this is more of a placeholder since we're obligated to pass a top-level + # comment when creating a new review + review_comments.body = ( + f"CodecovAI submitted a new review for {head_commit_sha}" + ) + res = await self.pull_wrapper.create_review( + head_commit_sha, review_comments + ) + return res["id"], head_commit_sha + + +async def perform_review(repository: Repository, pullid: int): + repository_service = get_repo_provider_service(repository) + pull_wrapper = PullWrapper(repository_service, pullid) + + archive = ArchiveService(repository) + archive_path = f"ai_pr_review/{archive.storage_hash}/pull_{pullid}.json" + + archive_data = None + try: + archive_data = archive.read_file(archive_path) + archive_data = json.loads(archive_data) + except FileNotInStorageError: + pass + + commit_sha = None + review_ids = [] + if archive_data is not None: + commit_sha = archive_data.get("commit_sha") + review_ids = archive_data.get("review_ids", []) + + head_sha = await pull_wrapper.fetch_head_sha() + if head_sha == commit_sha: + log.info( + "Review already performed on SHA", + extra=dict(sha=head_sha), + ) + return + + review = Review(pull_wrapper, review_ids=review_ids) + res = await review.perform() + if res is not None: + # we created a new review + review_id, commit_sha = res + review_ids.append(review_id) + + archive.write_file( + archive_path, + json.dumps( + { + "commit_sha": commit_sha, + "review_ids": review_ids, + } + ), + ) diff --git a/apps/worker/services/archive.py b/apps/worker/services/archive.py new file mode 100644 index 0000000000..c54be2c818 --- /dev/null +++ b/apps/worker/services/archive.py @@ -0,0 +1,181 @@ +import json +import logging +from base64 import b16encode +from datetime import datetime +from enum import Enum +from hashlib import md5 + +import sentry_sdk +import shared.storage +from shared.config import get_config +from shared.utils.ReportEncoder import ReportEncoder + +from helpers.metrics import metrics + +log = logging.getLogger(__name__) + + +class MinioEndpoints(Enum): + chunks = "{version}/repos/{repo_hash}/commits/{commitid}/{chunks_file_name}.txt" + json_data = "{version}/repos/{repo_hash}/commits/{commitid}/json_data/{table}/{field}/{external_id}.json" + json_data_no_commit = ( + "{version}/repos/{repo_hash}/json_data/{table}/{field}/{external_id}.json" + ) + raw = "v4/raw/{date}/{repo_hash}/{commit_sha}/{reportid}.txt" + computed_comparison = "{version}/repos/{repo_hash}/comparisons/{comparison_id}.json" + + def get_path(self, **kwaargs) -> str: + return self.value.format(**kwaargs) + + +# Service class for performing archive operations. Meant to work against the +# underlying StorageService +class ArchiveService(object): + root: str + """ + The root level of the archive. In s3 terms, + this would be the name of the bucket + """ + + storage_hash: str + """ + A hash key of the repo for internal storage + """ + + def __init__(self, repository, bucket=None) -> None: + if bucket is None: + self.root = get_config("services", "minio", "bucket", default="archive") + else: + self.root = bucket + self.storage = shared.storage.get_appropriate_storage_service(repository.repoid) + log.debug("Getting archive hash") + self.storage_hash = self.get_archive_hash(repository) + + def get_now(self) -> datetime: + return datetime.now() + + @classmethod + def get_archive_hash(cls, repository) -> str: + """ + Generates a hash key from repo specific information. + Provides slight obfuscation of data in minio storage + """ + _hash = md5() + hash_key = get_config("services", "minio", "hash_key") + val = "".join( + map( + str, + ( + repository.repoid, + repository.service, + repository.service_id, + hash_key, + ), + ) + ).encode() + _hash.update(val) + return b16encode(_hash.digest()).decode() + + @sentry_sdk.trace + def write_file( + self, path, data, reduced_redundancy=False, *, is_already_gzipped=False + ) -> None: + """ + Writes a generic file to the archive -- it's typically recommended to + not use this in lieu of the convenience method `write_chunks` + """ + self.storage.write_file( + self.root, + path, + data, + reduced_redundancy=reduced_redundancy, + is_already_gzipped=is_already_gzipped, + ) + + def write_computed_comparison(self, comparison, data) -> str: + path = MinioEndpoints.computed_comparison.get_path( + version="v4", repo_hash=self.storage_hash, comparison_id=comparison.id + ) + self.write_file(path, json.dumps(data)) + return path + + def write_json_data_to_storage( + self, + commit_id, + table: str, + field: str, + external_id: str, + data: dict, + *, + encoder=ReportEncoder, + ): + if commit_id is None: + # Some classes don't have a commit associated with them + # For example Pull belongs to multiple commits. + path = MinioEndpoints.json_data_no_commit.get_path( + version="v4", + repo_hash=self.storage_hash, + table=table, + field=field, + external_id=external_id, + ) + else: + path = MinioEndpoints.json_data.get_path( + version="v4", + repo_hash=self.storage_hash, + commitid=commit_id, + table=table, + field=field, + external_id=external_id, + ) + stringified_data = json.dumps(data, cls=encoder) + self.write_file(path, stringified_data) + return path + + def write_chunks(self, commit_sha, data, report_code=None) -> str: + """ + Convenience method to write a chunks.txt file to storage. + """ + chunks_file_name = report_code if report_code is not None else "chunks" + path = MinioEndpoints.chunks.get_path( + version="v4", + repo_hash=self.storage_hash, + commitid=commit_sha, + chunks_file_name=chunks_file_name, + ) + + self.write_file(path, data) + return path + + @sentry_sdk.trace + def read_file(self, path: str) -> bytes: + """ + Generic method to read a file from the archive + """ + with metrics.timer("services.archive.read_file") as t: + contents = self.storage.read_file(self.root, path) + log.debug( + "Downloaded file", extra=dict(timing_ms=t.ms, content_len=len(contents)) + ) + return contents + + @sentry_sdk.trace + def delete_file(self, path) -> None: + """ + Generic method to delete a file from the archive. + """ + self.storage.delete_file(self.root, path) + + def read_chunks(self, commit_sha, report_code=None) -> str: + """ + Convenience method to read a chunks file from the archive. + """ + chunks_file_name = report_code if report_code is not None else "chunks" + path = MinioEndpoints.chunks.get_path( + version="v4", + repo_hash=self.storage_hash, + commitid=commit_sha, + chunks_file_name=chunks_file_name, + ) + + return self.read_file(path).decode(errors="replace") diff --git a/apps/worker/services/bundle_analysis/__init__.py b/apps/worker/services/bundle_analysis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/bundle_analysis/comparison.py b/apps/worker/services/bundle_analysis/comparison.py new file mode 100644 index 0000000000..c6fa10f157 --- /dev/null +++ b/apps/worker/services/bundle_analysis/comparison.py @@ -0,0 +1,83 @@ +from functools import cached_property + +import shared.storage +from shared.bundle_analysis import ( + BundleAnalysisComparison, + BundleAnalysisReportLoader, +) + +from database.enums import ReportType +from database.models.core import Commit, Repository +from database.models.reports import CommitReport +from services.archive import ArchiveService +from services.bundle_analysis.exceptions import ( + MissingBaseCommit, + MissingBaseReport, + MissingHeadCommit, + MissingHeadReport, +) +from services.repository import EnrichedPull + + +class ComparisonLoader: + def __init__(self, base_commit: Commit | None, head_commit: Commit | None): + self._base_commit: Commit | None = base_commit + self._head_commit: Commit | None = head_commit + + @classmethod + def from_EnrichedPull(cls, pull: EnrichedPull) -> "ComparisonLoader": + return cls( + base_commit=pull.database_pull.get_comparedto_commit(), + head_commit=pull.database_pull.get_head_commit(), + ) + + @cached_property + def repository(self) -> Repository: + return self.head_commit.repository + + @cached_property + def base_commit(self) -> Commit: + commit = self._base_commit + if commit is None: + raise MissingBaseCommit() + return commit + + @cached_property + def head_commit(self) -> Commit: + commit = self._head_commit + if commit is None: + raise MissingHeadCommit() + return commit + + @cached_property + def base_commit_report(self) -> CommitReport: + commit_report = self.base_commit.commit_report( + report_type=ReportType.BUNDLE_ANALYSIS + ) + if commit_report is None: + raise MissingBaseReport() + return commit_report + + @cached_property + def head_commit_report(self) -> CommitReport: + commit_report = self.head_commit.commit_report( + report_type=ReportType.BUNDLE_ANALYSIS + ) + if commit_report is None: + raise MissingHeadReport() + return commit_report + + def get_comparison(self) -> BundleAnalysisComparison: + loader = BundleAnalysisReportLoader( + storage_service=shared.storage.get_appropriate_storage_service( + self.repository.repoid + ), + repo_key=ArchiveService.get_archive_hash(self.repository), + ) + + return BundleAnalysisComparison( + loader=loader, + base_report_key=self.base_commit_report.external_id, + head_report_key=self.head_commit_report.external_id, + repository=self.repository.repoid, + ) diff --git a/apps/worker/services/bundle_analysis/exceptions.py b/apps/worker/services/bundle_analysis/exceptions.py new file mode 100644 index 0000000000..edbc6b4311 --- /dev/null +++ b/apps/worker/services/bundle_analysis/exceptions.py @@ -0,0 +1,18 @@ +class ComparisonError(Exception): + pass + + +class MissingBaseCommit(ComparisonError): + pass + + +class MissingBaseReport(ComparisonError): + pass + + +class MissingHeadCommit(ComparisonError): + pass + + +class MissingHeadReport(ComparisonError): + pass diff --git a/apps/worker/services/bundle_analysis/notify/__init__.py b/apps/worker/services/bundle_analysis/notify/__init__.py new file mode 100644 index 0000000000..93fa982e8f --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/__init__.py @@ -0,0 +1,204 @@ +import logging +from typing import NamedTuple, Never + +import sentry_sdk +from shared.yaml import UserYaml + +from database.models.core import GITHUB_APP_INSTALLATION_DEFAULT_NAME, Commit, Owner +from services.bundle_analysis.notify.contexts import ( + BaseBundleAnalysisNotificationContext, + NotificationContextBuilder, + NotificationContextBuildError, +) +from services.bundle_analysis.notify.contexts.comment import ( + BundleAnalysisPRCommentContextBuilder, +) +from services.bundle_analysis.notify.contexts.commit_status import ( + CommitStatusNotificationContextBuilder, +) +from services.bundle_analysis.notify.helpers import get_notification_types_configured +from services.bundle_analysis.notify.messages import MessageStrategyInterface +from services.bundle_analysis.notify.messages.comment import ( + BundleAnalysisCommentMarkdownStrategy, +) +from services.bundle_analysis.notify.messages.commit_status import ( + CommitStatusMessageStrategy, +) +from services.bundle_analysis.notify.types import NotificationSuccess, NotificationType + +log = logging.getLogger(__name__) + + +class NotificationFullContext(NamedTuple): + notification_context: BaseBundleAnalysisNotificationContext + message_strategy: MessageStrategyInterface + + +class BundleAnalysisNotifyReturn(NamedTuple): + notifications_configured: tuple[NotificationType, ...] + notifications_attempted: tuple[NotificationType, ...] | tuple[Never] + notifications_successful: tuple[NotificationType, ...] | tuple[Never] + + def to_NotificationSuccess(self) -> NotificationSuccess: + notification_configured_count = len(self.notifications_configured) + notifications_successful_count = len(self.notifications_successful) + if notification_configured_count == 0: + return NotificationSuccess.NOTHING_TO_NOTIFY + if notification_configured_count == notifications_successful_count: + return NotificationSuccess.FULL_SUCCESS + return NotificationSuccess.PARTIAL_SUCCESS + + +class BundleAnalysisNotifyService: + def __init__( + self, + commit: Commit, + current_yaml: UserYaml, + gh_app_installation_name: str | None = None, + ): + self.commit = commit + self.current_yaml = current_yaml + self.gh_app_installation_name = ( + gh_app_installation_name or GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + + @property + def owner(self) -> Owner: + return self.commit.repository.owner + + @sentry_sdk.trace + def build_base_context(self) -> BaseBundleAnalysisNotificationContext | None: + try: + return ( + NotificationContextBuilder() + .initialize( + self.commit, self.current_yaml, self.gh_app_installation_name + ) + .build_context() + .get_result() + ) + except NotificationContextBuildError as exp: + log.warning( + "Failed to build NotificationContext", + extra=dict( + notification_type="base_context", failed_step=exp.failed_step + ), + ) + return None + + def create_context_for_notification( + self, + base_context: BaseBundleAnalysisNotificationContext, + notification_type: NotificationType, + ) -> NotificationFullContext | None: + """Builds the NotificationContext for the given notification_type + If the NotificationContext failed to build we can't send this notification. + + Each NotificationType is paired with a ContextBuilder and MessageStrategyInterface. + The MessageStrategy is later used to build and send the message based on the NotificationContext + """ + notifier_lookup: dict[ + NotificationType, + tuple[NotificationContextBuilder, MessageStrategyInterface], + ] = { + NotificationType.PR_COMMENT: ( + BundleAnalysisPRCommentContextBuilder(), + BundleAnalysisCommentMarkdownStrategy(), + ), + NotificationType.COMMIT_STATUS: ( + CommitStatusNotificationContextBuilder(), + CommitStatusMessageStrategy(), + ), + # The commit-check API is more powerful than COMMIT_STATUS + # but we currently don't differentiate between them + NotificationType.GITHUB_COMMIT_CHECK: ( + CommitStatusNotificationContextBuilder(), + CommitStatusMessageStrategy(), + ), + } + notifier_strategy = notifier_lookup.get(notification_type) + if notifier_strategy is None: + msg = f"No context builder for {notification_type}. Skipping" + log.error(msg) + return None + builder_instance, message_strategy_instance = notifier_strategy + try: + builder = builder_instance.initialize_from_context( + self.current_yaml, base_context + ) + return NotificationFullContext( + builder.build_context().get_result(), + message_strategy_instance, + ) + except NotificationContextBuildError as exp: + log.warning( + "Failed to build NotificationContext", + extra=dict( + notification_type=notification_type, failed_step=exp.failed_step + ), + ) + return None + + def get_notification_contexts( + self, + base_context: BaseBundleAnalysisNotificationContext, + notification_types: tuple[NotificationType, ...], + ) -> list[NotificationFullContext]: + previous_context = base_context + specific_contexts = [] + for notification_type in notification_types: + full_context = self.create_context_for_notification( + previous_context, notification_type + ) + if full_context: + previous_context = full_context.notification_context + specific_contexts.append(full_context) + return specific_contexts + + def notify(self) -> BundleAnalysisNotifyReturn: + """Entrypoint for BundleAnalysis notifications. This function does the following: + 1. Gets the configured notifications. Those are the ones we must send; + 2. Attempts to build a BaseContext with necessary info for all notifications; + 3. Attempts to build a Context for each notification to be sent; + 4. For each notification with a context, build and send the message. + + Returns: BundleAnalysisNotifyReturn - tuple with notifications configured and the ones + that we successfully notified. + """ + notification_types = get_notification_types_configured( + self.current_yaml, self.owner + ) + base_context = self.build_base_context() + if base_context is None: + log.warning("Skipping ALL notifications because there's no base context") + return BundleAnalysisNotifyReturn( + notifications_configured=notification_types, + notifications_attempted=tuple(), + notifications_successful=tuple(), + ) + + notification_full_contexts = self.get_notification_contexts( + base_context, notification_types + ) + notifications_sent = [] + notifications_successful = [] + for notification_context, message_strategy in notification_full_contexts: + message = message_strategy.build_message(notification_context) + result = message_strategy.send_message(notification_context, message) + if result.notification_attempted: + notifications_sent.append(notification_context.notification_type) + if result.notification_successful: + notifications_successful.append(notification_context.notification_type) + log.info( + "Notification done", + extra=dict( + notification_type=notification_context.notification_type, + notification_result=result, + ), + ) + + return BundleAnalysisNotifyReturn( + notifications_configured=notification_types, + notifications_attempted=tuple(notifications_sent), + notifications_successful=tuple(notifications_successful), + ) diff --git a/apps/worker/services/bundle_analysis/notify/conftest.py b/apps/worker/services/bundle_analysis/notify/conftest.py new file mode 100644 index 0000000000..bef75d1359 --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/conftest.py @@ -0,0 +1,81 @@ +from pathlib import Path + +from shared.bundle_analysis.storage import get_bucket_name + +from database.enums import ReportType +from database.models.core import Commit, Repository +from database.models.reports import CommitReport +from database.tests.factories.core import CommitFactory, PullFactory +from services.archive import ArchiveService +from services.repository import EnrichedPull + +SAMPLE_FOLDER_PATH = Path(__file__).resolve().parent / "tests" / "samples" + + +def get_commit_pair(dbsession) -> tuple[Commit, Commit]: + base_commit = CommitFactory(repository__owner__service="github") + head_commit = CommitFactory(repository=base_commit.repository) + dbsession.add_all([base_commit, head_commit]) + dbsession.commit() + return (head_commit, base_commit) + + +def get_report_pair(dbsession, commit_pair) -> tuple[CommitReport, CommitReport]: + head_commit, base_commit = commit_pair + base_commit_report = CommitReport( + commit=base_commit, report_type=ReportType.BUNDLE_ANALYSIS.value + ) + head_commit_report = CommitReport( + commit=head_commit, report_type=ReportType.BUNDLE_ANALYSIS.value + ) + dbsession.add_all([base_commit_report, head_commit_report]) + dbsession.commit() + return (head_commit_report, base_commit_report) + + +def get_enriched_pull_setting_up_mocks(dbsession, mocker, commit_pair) -> EnrichedPull: + head_commit, base_commit = commit_pair + pull = PullFactory( + repository=base_commit.repository, + head=head_commit.commitid, + base=base_commit.commitid, + compared_to=base_commit.commitid, + ) + dbsession.add(pull) + dbsession.commit() + enriched_pull = EnrichedPull( + database_pull=pull, + provider_pull={}, + ) + fake_pull_patches_to_apply = [ + "services.bundle_analysis.notify.contexts.comment.fetch_and_update_pull_request_information_from_commit", + "services.bundle_analysis.notify.contexts.commit_status.fetch_and_update_pull_request_information_from_commit", + ] + for patch_to_apply in fake_pull_patches_to_apply: + mocker.patch( + patch_to_apply, + return_value=enriched_pull, + ) + fake_repo_service = mocker.MagicMock(name="fake_repo_service") + mocker.patch( + "services.bundle_analysis.notify.contexts.get_repo_provider_service", + return_value=fake_repo_service, + ) + + return enriched_pull + + +def save_mock_bundle_analysis_report( + repository: Repository, + commit_report: CommitReport, + mock_storage, + sample_report_number, +) -> None: + repo_key = ArchiveService.get_archive_hash(repository) + sample_path = SAMPLE_FOLDER_PATH / f"sample_{sample_report_number}.sqlite" + sample_contents = sample_path.read_bytes() + mock_storage.write_file( + get_bucket_name(), + f"v1/repos/{repo_key}/{commit_report.external_id}/bundle_report.sqlite", + sample_contents, + ) diff --git a/apps/worker/services/bundle_analysis/notify/contexts/__init__.py b/apps/worker/services/bundle_analysis/notify/contexts/__init__.py new file mode 100644 index 0000000000..139813558b --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/contexts/__init__.py @@ -0,0 +1,234 @@ +from enum import Enum, auto +from functools import cached_property +from typing import Generic, Literal, Self, TypeVar + +import sentry_sdk +import shared.storage +from shared.bundle_analysis import BundleAnalysisReport, BundleAnalysisReportLoader +from shared.torngit.base import TorngitBaseAdapter +from shared.typings.torngit import AdditionalData, UploadType +from shared.validation.types import BundleThreshold +from shared.yaml import UserYaml + +from database.enums import ReportType +from database.models.core import Commit, Repository +from database.models.reports import CommitReport +from services.archive import ArchiveService +from services.bundle_analysis.notify.helpers import to_BundleThreshold +from services.bundle_analysis.notify.types import ( + NotificationType, + NotificationUserConfig, +) +from services.repository import get_repo_provider_service + +T = TypeVar("T") + + +class ContextNotLoadedError(Exception): + pass + + +class CommitStatusLevel(Enum): + INFO = auto() + WARNING = auto() + ERROR = auto() + + def to_str(self) -> Literal["success"] | Literal["failure"]: + if self.value == "ERROR": + return "failure" + return "success" + + +class NotificationContextField(Generic[T]): + """NotificationContextField is a descriptor akin to a Django model field. + If you create one as a class member named `foo`, it will define the behavior to get and set an instance member named `foo`. + It is also similar to @property + """ + + def __set_name__(self, owner, name) -> None: + self._name = name + + def __get__(self, instance, owner) -> T: + if self._name not in instance.__dict__: + msg = f"Property {self._name} is not loaded. Make sure to build the context before using it." + raise ContextNotLoadedError(msg) + return instance.__dict__[self._name] + + def __set__(self, instance, value: T) -> None: + instance.__dict__[self._name] = value + + +class BaseBundleAnalysisNotificationContext: + """Base NotificationContext for bundle analysis notifications. + It includes basic information that all bundle analysis notifications need. + Use NotificationContextBuilder to populate the context. + + Example: + builder = NotificationContextBuilder(commit, current_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME) + notification_context = builder.build_context().get_result() + """ + + notification_type: NotificationType + + def __init__(self, commit: Commit, gh_app_installation_name: str) -> None: + self.commit = commit + self.gh_app_installation_name = gh_app_installation_name + + @cached_property + def repository(self) -> Repository: + return self.commit.repository + + @cached_property + def repository_service(self) -> TorngitBaseAdapter: + additional_data: AdditionalData = {"upload_type": UploadType.BUNDLE_ANALYSIS} + return get_repo_provider_service( + self.repository, + installation_name_to_use=self.gh_app_installation_name, + additional_data=additional_data, + ) + + commit_report = NotificationContextField[CommitReport]() + bundle_analysis_report = NotificationContextField[BundleAnalysisReport]() + user_config = NotificationContextField[NotificationUserConfig]() + + +class NotificationContextBuildError(Exception): + def __init__(self, failed_step: str, detail: str | None = None) -> None: + super().__init__(failed_step, detail) + self.failed_step = failed_step + self.detail = detail + + +class WrongContextBuilderError(Exception): + pass + + +class NotificationContextBuilder: + """Creates the BaseBundleAnalysisNotificationContext one step at a time, in the correct order.""" + + current_yaml: UserYaml + """ Used with `initialize_from_context` method. Declare the fields the class wants to copy over when initializing from another context.""" + fields_of_interest: tuple[str, ...] = ( + "commit_report", + "bundle_analysis_report", + "user_config", + ) + + def initialize( + self, commit: Commit, current_yaml: UserYaml, gh_app_installation_name: str + ) -> "NotificationContextBuilder": + self.current_yaml = current_yaml + self._notification_context = BaseBundleAnalysisNotificationContext( + commit=commit, + gh_app_installation_name=gh_app_installation_name, + ) + return self + + def initialize_from_context( + self, current_yaml: UserYaml, context: BaseBundleAnalysisNotificationContext + ) -> Self: + self.initialize( + commit=context.commit, + current_yaml=current_yaml, + gh_app_installation_name=context.gh_app_installation_name, + ) + + for field_name in self.fields_of_interest: + if field_name in context.__dict__: + self._notification_context.__dict__[field_name] = context.__dict__[ + field_name + ] + return self + + def is_field_loaded(self, field_name: str): + return field_name in self._notification_context.__dict__ + + def load_commit_report(self) -> "NotificationContextBuilder": + """Loads the CommitReport into the NotificationContext + Raises: + NotificationContextBuildError: no CommitReport exist for the commit + """ + if self.is_field_loaded("commit_report"): + return self + commit_report = self._notification_context.commit.commit_report( + report_type=ReportType.BUNDLE_ANALYSIS + ) + if commit_report is None: + raise NotificationContextBuildError("load_commit_report") + self._notification_context.commit_report = commit_report + return self + + @sentry_sdk.trace + def load_bundle_analysis_report(self) -> "NotificationContextBuilder": + """Loads the BundleAnalysisReport into the NotificationContext + BundleAnalysisReport is an SQLite report generated by processing uploads + Raises: + NotificationContextBuildError: no BundleAnalysisReport exists for the commit. + """ + if self.is_field_loaded("bundle_analysis_report"): + return self + repo_hash = ArchiveService.get_archive_hash( + self._notification_context.repository + ) + storage_service = shared.storage.get_appropriate_storage_service( + self._notification_context.repository.repoid + ) + analysis_report_loader = BundleAnalysisReportLoader(storage_service, repo_hash) + bundle_analysis_report = analysis_report_loader.load( + self._notification_context.commit_report.external_id + ) + if bundle_analysis_report is None: + raise NotificationContextBuildError("load_bundle_analysis_report") + self._notification_context.bundle_analysis_report = bundle_analysis_report + return self + + def load_user_config(self) -> "NotificationContextBuilder": + """Parses the configuration from the `current_yaml` related to bundle analysis notification + into a NotificationUserConfig object for the context. + + This allows all notifiers to access configuration for any notifier and already have the defaults + """ + comment_config: bool | dict = self.current_yaml.read_yaml_field("comment") + required_changes: bool | Literal["bundle_increase"] + if isinstance(comment_config, bool): + required_changes = comment_config + required_changes_threshold = BundleThreshold("absolute", 0) + else: + required_changes = comment_config.get("require_bundle_changes", False) + required_changes_threshold = comment_config.get( + "bundle_change_threshold", + BundleThreshold("absolute", 0), + ) + warning_threshold: int | float = self.current_yaml.read_yaml_field( + "bundle_analysis", + "warning_threshold", + _else=BundleThreshold("percentage", 5.0), + ) + status_level: bool | Literal["informational"] = ( + self.current_yaml.read_yaml_field( + "bundle_analysis", "status", _else="informational" + ) + ) + self._notification_context.user_config = NotificationUserConfig( + required_changes=required_changes, + warning_threshold=to_BundleThreshold(warning_threshold), + status_level=status_level, + required_changes_threshold=to_BundleThreshold(required_changes_threshold), + ) + return self + + def build_context(self) -> "NotificationContextBuilder": + """Calls all the steps necessary to fully load the NotificationContext + Raises: + NotificationContextBuildError: if any of the steps fail + """ + self.load_user_config() + self.load_commit_report() + self.load_bundle_analysis_report() + return self + + def get_result(self) -> BaseBundleAnalysisNotificationContext: + """Returns the NotificationContext. + Should be called after `build_context`, or you get an empty context back. + """ + return self._notification_context diff --git a/apps/worker/services/bundle_analysis/notify/contexts/comment.py b/apps/worker/services/bundle_analysis/notify/contexts/comment.py new file mode 100644 index 0000000000..c50532bcf7 --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/contexts/comment.py @@ -0,0 +1,213 @@ +import logging +from typing import Self + +import sentry_sdk +from asgiref.sync import async_to_sync +from shared.bundle_analysis import ( + BundleAnalysisComparison, +) +from shared.yaml import UserYaml + +from database.models.core import Commit +from services.activation import activate_user, schedule_new_user_activated_task +from services.bundle_analysis.comparison import ComparisonLoader +from services.bundle_analysis.exceptions import ( + MissingBaseCommit, + MissingBaseReport, + MissingHeadCommit, + MissingHeadReport, +) +from services.bundle_analysis.notify.contexts import ( + BaseBundleAnalysisNotificationContext, + CommitStatusLevel, + NotificationContextBuilder, + NotificationContextBuildError, + NotificationContextField, +) +from services.bundle_analysis.notify.helpers import ( + is_bundle_comparison_change_within_configured_threshold, +) +from services.bundle_analysis.notify.types import NotificationType +from services.repository import ( + EnrichedPull, + fetch_and_update_pull_request_information_from_commit, +) +from services.seats import ShouldActivateSeat, determine_seat_activation + +log = logging.getLogger(__name__) + + +class BundleAnalysisPRCommentNotificationContext(BaseBundleAnalysisNotificationContext): + """Context for the Bundle Analysis PR Comment. Extends BaseBundleAnalysisNotificationContext.""" + + notification_type = NotificationType.PR_COMMENT + + pull = NotificationContextField[EnrichedPull]() + bundle_analysis_comparison = NotificationContextField[BundleAnalysisComparison]() + commit_status_level = NotificationContextField[CommitStatusLevel]() + should_use_upgrade_comment = NotificationContextField[bool]() + + +class BundleAnalysisPRCommentContextBuilder(NotificationContextBuilder): + fields_of_interest: tuple[str, ...] = ( + "commit_report", + "bundle_analysis_report", + "user_config", + "pull", + "bundle_analysis_comparison", + "should_use_upgrade_comment", + ) + + def initialize( + self, commit: Commit, current_yaml: UserYaml, gh_app_installation_name: str + ) -> "BundleAnalysisPRCommentContextBuilder": + self.current_yaml = current_yaml + self._notification_context = BundleAnalysisPRCommentNotificationContext( + commit=commit, + gh_app_installation_name=gh_app_installation_name, + ) + return self + + @sentry_sdk.trace + async def load_enriched_pull(self) -> Self: + """Loads the EnrichedPull into the NotificationContext. + EnrichedPull includes updated info from the git provider and info saved in the database. + Raises: + NotificationContextBuildError: failed to get EnrichedPull. + This can be because there's no Pull saved in the database, + or because we couldn't update the pull's info from the git provider. + """ + if self.is_field_loaded("pull"): + return self + pull: ( + EnrichedPull | None + ) = await fetch_and_update_pull_request_information_from_commit( + self._notification_context.repository_service, + self._notification_context.commit, + self.current_yaml, + ) + if pull is None: + raise NotificationContextBuildError("load_enriched_pull") + self._notification_context.pull = pull + return self + + @sentry_sdk.trace + def load_bundle_comparison(self) -> Self: + """Loads the BundleAnalysisComparison into the NotificationContext. + BundleAnalysisComparison is the diff between 2 BundleAnalysisReports, + respectively the one for the pull's base and one for the pull's head. + Raises: + NotificationContextBuildError: missing some information necessary to create + the BundleAnalysisComparison. + """ + if self.is_field_loaded("bundle_analysis_comparison"): + return self + pull = self._notification_context.pull + try: + comparison = ComparisonLoader.from_EnrichedPull(pull).get_comparison() + self._notification_context.bundle_analysis_comparison = comparison + return self + except ( + MissingBaseCommit, + MissingHeadCommit, + MissingBaseReport, + MissingHeadReport, + ) as exp: + raise NotificationContextBuildError( + "load_bundle_comparison", detail=exp.__class__.__name__ + ) + + def evaluate_has_enough_changes(self) -> Self: + """Evaluates if the NotificationContext includes enough changes to send the notification. + Configuration is done via UserYaml. + If a comment was previously made for this PR the required changes are bypassed so that we + update the existing comment with the latest information. + Raises: + NotificationContextBuildError: required changes are not met. + """ + pull = self._notification_context.pull + required_changes_threshold = ( + self._notification_context.user_config.required_changes_threshold + ) + required_changes = self._notification_context.user_config.required_changes + if pull.database_pull.bundle_analysis_commentid: + log.info( + "Skipping required_changes verification because comment already exists", + extra=dict( + pullid=pull.database_pull.id, + commitid=self._notification_context.commit.commitid, + ), + ) + return self + comparison = self._notification_context.bundle_analysis_comparison + should_continue = { + False: True, + True: not is_bundle_comparison_change_within_configured_threshold( + comparison, + required_changes_threshold, + compare_non_negative_numbers=True, + ), + "bundle_increase": ( + comparison.total_size_delta > 0 + and not is_bundle_comparison_change_within_configured_threshold( + comparison, + required_changes_threshold, + compare_non_negative_numbers=True, + ) + ), + }.get(required_changes, True) + if not should_continue: + raise NotificationContextBuildError("evaluate_has_enough_changes") + return self + + @sentry_sdk.trace + def evaluate_should_use_upgrade_message(self) -> Self: + activate_seat_info = determine_seat_activation(self._notification_context.pull) + match activate_seat_info.should_activate_seat: + case ShouldActivateSeat.AUTO_ACTIVATE: + successful_activation = activate_user( + db_session=self._notification_context.commit.get_db_session(), + org_ownerid=activate_seat_info.owner_id, + user_ownerid=activate_seat_info.author_id, + ) + if successful_activation: + schedule_new_user_activated_task( + activate_seat_info.owner_id, activate_seat_info.author_id + ) + self._notification_context.should_use_upgrade_comment = False + else: + self._notification_context.should_use_upgrade_comment = True + case ShouldActivateSeat.MANUAL_ACTIVATE: + self._notification_context.should_use_upgrade_comment = True + case ShouldActivateSeat.NO_ACTIVATE: + self._notification_context.should_use_upgrade_comment = False + return self + + def load_commit_status_level(self) -> Self: + bundle_analysis_comparison = ( + self._notification_context.bundle_analysis_comparison + ) + user_config = self._notification_context.user_config + + if is_bundle_comparison_change_within_configured_threshold( + bundle_analysis_comparison, user_config.warning_threshold + ): + self._notification_context.commit_status_level = CommitStatusLevel.INFO + elif user_config.status_level == "informational": + self._notification_context.commit_status_level = CommitStatusLevel.WARNING + else: + self._notification_context.commit_status_level = CommitStatusLevel.ERROR + return self + + def build_context(self) -> Self: + super().build_context() + async_to_sync(self.load_enriched_pull)() + return ( + self.load_bundle_comparison() + .evaluate_has_enough_changes() + .evaluate_should_use_upgrade_message() + .load_commit_status_level() + ) + + def get_result(self) -> BundleAnalysisPRCommentNotificationContext: + return self._notification_context # type: ignore diff --git a/apps/worker/services/bundle_analysis/notify/contexts/commit_status.py b/apps/worker/services/bundle_analysis/notify/contexts/commit_status.py new file mode 100644 index 0000000000..5b7bfbf4d5 --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/contexts/commit_status.py @@ -0,0 +1,206 @@ +from typing import Self + +import sentry_sdk +from asgiref.sync import async_to_sync +from shared.bundle_analysis import ( + BundleAnalysisComparison, +) +from shared.config import get_config +from shared.yaml import UserYaml + +from database.models.core import Commit +from services.activation import activate_user, schedule_new_user_activated_task +from services.bundle_analysis.comparison import ComparisonLoader +from services.bundle_analysis.exceptions import ( + MissingBaseCommit, + MissingBaseReport, + MissingHeadCommit, + MissingHeadReport, +) +from services.bundle_analysis.notify.contexts import ( + BaseBundleAnalysisNotificationContext, + CommitStatusLevel, + NotificationContextBuilder, + NotificationContextBuildError, + NotificationContextField, +) +from services.bundle_analysis.notify.helpers import ( + is_bundle_comparison_change_within_configured_threshold, +) +from services.bundle_analysis.notify.types import NotificationType +from services.repository import ( + EnrichedPull, + fetch_and_update_pull_request_information_from_commit, +) +from services.seats import ShouldActivateSeat, determine_seat_activation +from services.urls import get_bundle_analysis_pull_url, get_commit_url + + +class CommitStatusNotificationContext(BaseBundleAnalysisNotificationContext): + notification_type = NotificationType.COMMIT_STATUS + + pull = NotificationContextField[EnrichedPull | None]() + bundle_analysis_comparison = NotificationContextField[BundleAnalysisComparison]() + commit_status_level = NotificationContextField[CommitStatusLevel]() + commit_status_url = NotificationContextField[str]() + cache_ttl = NotificationContextField[int]() + should_use_upgrade_comment = NotificationContextField[bool]() + + @property + def base_commit(self) -> Commit: + if self.pull: + return self.pull.database_pull.get_comparedto_commit() + return self.commit.get_parent_commit() + + +class CommitStatusNotificationContextBuilder(NotificationContextBuilder): + fields_of_interest: tuple[str, ...] = ( + "commit_report", + "bundle_analysis_report", + "user_config", + "pull", + "bundle_analysis_comparison", + "should_use_upgrade_comment", + ) + + def initialize( + self, commit: Commit, current_yaml: UserYaml, gh_app_installation_name: str + ) -> Self: + self.current_yaml = current_yaml + self._notification_context = CommitStatusNotificationContext( + commit=commit, + gh_app_installation_name=gh_app_installation_name, + ) + return self + + @sentry_sdk.trace + async def load_optional_enriched_pull( + self, + ) -> Self: + """Loads an optional EnrichedPull into the NotificationContext. + EnrichedPull includes updated info from the git provider and info saved in the database. + If the value is None it's because the commit is not in a Pull Request + """ + if self.is_field_loaded("pull"): + return self + optional_pull: ( + EnrichedPull | None + ) = await fetch_and_update_pull_request_information_from_commit( + self._notification_context.repository_service, + self._notification_context.commit, + self.current_yaml, + ) + self._notification_context.pull = optional_pull + return self + + @sentry_sdk.trace + def load_bundle_comparison( + self, + ) -> Self: + """Loads the BundleAnalysisComparison into the NotificationContext. + BundleAnalysisComparison is the diff between 2 BundleAnalysisReports. + IF pull is not None, comparison is pull's BASE vs HEAD + ELSE comparison is HEAD vs HEAD's parent + + Raises: + NotificationContextBuildError: missing some information necessary to create + the BundleAnalysisComparison. + """ + if self.is_field_loaded("bundle_analysis_comparison"): + return self + pull = self._notification_context.pull + try: + if pull is None: + comparison = ComparisonLoader( + base_commit=self._notification_context.commit.get_parent_commit(), + head_commit=self._notification_context.commit, + ).get_comparison() + else: + comparison = ComparisonLoader.from_EnrichedPull(pull).get_comparison() + self._notification_context.bundle_analysis_comparison = comparison + return self + except ( + MissingBaseCommit, + MissingHeadCommit, + MissingBaseReport, + MissingHeadReport, + ) as exp: + raise NotificationContextBuildError( + "load_bundle_comparison", detail=exp.__class__.__name__ + ) + + def load_commit_status_level(self) -> Self: + bundle_analysis_comparison = ( + self._notification_context.bundle_analysis_comparison + ) + user_config = self._notification_context.user_config + + if is_bundle_comparison_change_within_configured_threshold( + bundle_analysis_comparison, user_config.warning_threshold + ): + self._notification_context.commit_status_level = CommitStatusLevel.INFO + elif user_config.status_level == "informational": + self._notification_context.commit_status_level = CommitStatusLevel.WARNING + else: + self._notification_context.commit_status_level = CommitStatusLevel.ERROR + return self + + def load_commit_status_url(self) -> Self: + if self._notification_context.pull: + self._notification_context.commit_status_url = get_bundle_analysis_pull_url( + self._notification_context.pull.database_pull + ) + else: + self._notification_context.commit_status_url = get_commit_url( + self._notification_context.commit + ) + return self + + def load_cache_ttl(self) -> Self: + self._notification_context.cache_ttl = int( + # using `get_config` instead of `current_yaml` because + # `current_yaml` does not include the install configuration + get_config("setup", "cache", "send_status_notification", default=600) + ) # 10 min default + return self + + @sentry_sdk.trace + def evaluate_should_use_upgrade_message(self) -> Self: + if self._notification_context.pull is None: + self._notification_context.should_use_upgrade_comment = False + return self + activate_seat_info = determine_seat_activation(self._notification_context.pull) + match activate_seat_info.should_activate_seat: + case ShouldActivateSeat.AUTO_ACTIVATE: + successful_activation = activate_user( + db_session=self._notification_context.commit.get_db_session(), + org_ownerid=activate_seat_info.owner_id, + user_ownerid=activate_seat_info.author_id, + ) + if successful_activation: + schedule_new_user_activated_task( + activate_seat_info.owner_id, + activate_seat_info.author_id, + ) + self._notification_context.should_use_upgrade_comment = False + else: + self._notification_context.should_use_upgrade_comment = True + case ShouldActivateSeat.MANUAL_ACTIVATE: + self._notification_context.should_use_upgrade_comment = True + case ShouldActivateSeat.NO_ACTIVATE: + self._notification_context.should_use_upgrade_comment = False + return self + + def build_context(self) -> Self: + super().build_context() + async_to_sync(self.load_optional_enriched_pull)() + return ( + self.load_bundle_comparison() + .load_commit_status_level() + .evaluate_should_use_upgrade_message() + .load_commit_status_url() + .load_cache_ttl() + ) + + def get_result(self) -> CommitStatusNotificationContext: + return self._notification_context diff --git a/apps/worker/services/bundle_analysis/notify/contexts/tests/__init__.py b/apps/worker/services/bundle_analysis/notify/contexts/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/bundle_analysis/notify/contexts/tests/test_comment_context.py b/apps/worker/services/bundle_analysis/notify/contexts/tests/test_comment_context.py new file mode 100644 index 0000000000..67247e09db --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/contexts/tests/test_comment_context.py @@ -0,0 +1,360 @@ +from unittest.mock import MagicMock + +import pytest +from shared.config import PATCH_CENTRIC_DEFAULT_CONFIG +from shared.yaml import UserYaml + +from database.models.core import GITHUB_APP_INSTALLATION_DEFAULT_NAME +from database.tests.factories.core import CommitFactory, PullFactory +from services.bundle_analysis.comparison import ComparisonLoader +from services.bundle_analysis.notify.conftest import ( + get_commit_pair, + get_enriched_pull_setting_up_mocks, + get_report_pair, + save_mock_bundle_analysis_report, +) +from services.bundle_analysis.notify.contexts import ( + ContextNotLoadedError, + NotificationContextBuildError, +) +from services.bundle_analysis.notify.contexts.comment import ( + BundleAnalysisPRCommentContextBuilder, +) +from services.repository import EnrichedPull +from services.seats import SeatActivationInfo, ShouldActivateSeat +from tests.helpers import mock_all_plans_and_tiers + + +class TestBundleAnalysisPRCommentNotificationContext: + @pytest.mark.asyncio + async def test_load_pull_not_found(self, dbsession, mocker): + head_commit, _ = get_commit_pair(dbsession) + user_yaml = UserYaml.from_dict({}) + fake_repo_service = mocker.MagicMock(name="fake_repo_service") + mock_fetch_pr = mocker.patch( + "services.bundle_analysis.notify.contexts.comment.fetch_and_update_pull_request_information_from_commit", + return_value=None, + ) + mocker.patch( + "services.bundle_analysis.notify.contexts.get_repo_provider_service", + return_value=fake_repo_service, + ) + builder = BundleAnalysisPRCommentContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + with pytest.raises(NotificationContextBuildError) as exp: + await builder.load_enriched_pull() + assert exp.value.failed_step == "load_enriched_pull" + mock_fetch_pr.assert_called_with(fake_repo_service, head_commit, user_yaml) + + @pytest.mark.parametrize( + "expected_missing_detail", + [ + pytest.param("MissingBaseCommit"), + pytest.param("MissingHeadCommit"), + pytest.param("MissingBaseReport"), + pytest.param("MissingHeadReport"), + ], + ) + @pytest.mark.asyncio + async def test_load_bundle_comparison_missing_some_info( + self, expected_missing_detail, dbsession, mocker + ): + head_commit, base_commit = get_commit_pair(dbsession) + sink_commit = CommitFactory(repository=head_commit.repository) + dbsession.add(sink_commit) + get_report_pair(dbsession, (head_commit, base_commit)) + enriched_pull = get_enriched_pull_setting_up_mocks( + dbsession, mocker, (head_commit, base_commit) + ) + user_yaml = UserYaml.from_dict({}) + builder = BundleAnalysisPRCommentContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + builder.load_commit_report() + match expected_missing_detail: + case "MissingBaseCommit": + enriched_pull.database_pull.compared_to = None + case "MissingHeadCommit": + enriched_pull.database_pull.head = None + case "MissingBaseReport": + # Deleting the BaseReport also deleted base_commit + # So instead we point the Pull object to a commit that doesn't have reports + enriched_pull.database_pull.compared_to = sink_commit.commitid + case "MissingHeadReport": + enriched_pull.database_pull.head = sink_commit.commitid + + with pytest.raises(NotificationContextBuildError) as exp: + await builder.load_enriched_pull() + builder.load_bundle_comparison() + assert exp.value.failed_step == "load_bundle_comparison" + assert exp.value.detail == expected_missing_detail + + @pytest.mark.parametrize( + "config, total_size_delta", + [ + pytest.param( + { + "comment": { + "require_bundle_changes": "bundle_increase", + "bundle_change_threshold": 1000000, + } + }, + 100, + id="required_increase_with_big_threshold", + ), + pytest.param( + { + "comment": { + "require_bundle_changes": "bundle_increase", + "bundle_change_threshold": 10, + } + }, + -100, + id="required_increase_but_decreased", + ), + pytest.param( + { + "comment": { + "require_bundle_changes": True, + "bundle_change_threshold": 1000000, + } + }, + 100, + id="required_changes_with_big_threshold", + ), + ], + ) + def test_evaluate_changes_fail(self, config, total_size_delta, dbsession, mocker): + head_commit, _ = get_commit_pair(dbsession) + user_yaml = UserYaml.from_dict(config) + builder = BundleAnalysisPRCommentContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + builder.load_user_config() + mock_pull = MagicMock( + name="fake_pull", + database_pull=MagicMock(bundle_analysis_commentid=None, id=12), + ) + builder._notification_context.pull = mock_pull + mock_comparison = MagicMock( + name="fake_bundle_analysis_comparison", total_size_delta=total_size_delta + ) + builder._notification_context.bundle_analysis_comparison = mock_comparison + with pytest.raises(NotificationContextBuildError) as exp: + builder.evaluate_has_enough_changes() + assert exp.value.failed_step == "evaluate_has_enough_changes" + + @pytest.mark.parametrize( + "config, total_size_delta", + [ + pytest.param( + PATCH_CENTRIC_DEFAULT_CONFIG, + 100, + id="default_config", + ), + pytest.param( + {"comment": {"require_bundle_changes": False}}, + 100, + id="no_required_changes", + ), + pytest.param( + {"comment": {"require_bundle_changes": True}}, + 100, + id="required_changes_increase", + ), + pytest.param( + {"comment": {"require_bundle_changes": True}}, + -100, + id="required_changes_decrease", + ), + pytest.param( + {"comment": {"require_bundle_changes": "bundle_increase"}}, + 100, + id="required_increase", + ), + pytest.param( + { + "comment": { + "require_bundle_changes": "bundle_increase", + "bundle_change_threshold": 1000, + } + }, + 1001, + id="required_increase_with_small_threshold", + ), + ], + ) + def test_evaluate_changes_success(self, config, total_size_delta, dbsession): + head_commit, _ = get_commit_pair(dbsession) + user_yaml = UserYaml.from_dict(config) + builder = BundleAnalysisPRCommentContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + builder.load_user_config() + mock_pull = MagicMock( + name="fake_pull", + database_pull=MagicMock(bundle_analysis_commentid=None, id=12), + ) + builder._notification_context.pull = mock_pull + mock_comparison = MagicMock( + name="fake_bundle_analysis_comparison", total_size_delta=total_size_delta + ) + builder._notification_context.bundle_analysis_comparison = mock_comparison + result = builder.evaluate_has_enough_changes() + assert result == builder + + def test_evaluate_changes_comment_exists(self, dbsession): + head_commit, _ = get_commit_pair(dbsession) + user_yaml = UserYaml.from_dict( + { + "comment": { + "require_bundle_changes": "bundle_increase", + "bundle_change_threshold": 1000000, + } + } + ) + builder = BundleAnalysisPRCommentContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + builder.load_user_config() + mock_pull = MagicMock( + name="fake_pull", + database_pull=MagicMock(bundle_analysis_commentid=12345, id=12), + ) + builder._notification_context.pull = mock_pull + mock_comparison = MagicMock( + name="fake_bundle_analysis_comparison", total_size_delta=100 + ) + builder._notification_context.bundle_analysis_comparison = mock_comparison + result = builder.evaluate_has_enough_changes() + assert result == builder + + @pytest.mark.parametrize( + "activation_result, auto_activate_succeeds, expected", + [ + (ShouldActivateSeat.AUTO_ACTIVATE, True, False), + (ShouldActivateSeat.AUTO_ACTIVATE, False, True), + (ShouldActivateSeat.MANUAL_ACTIVATE, False, True), + (ShouldActivateSeat.NO_ACTIVATE, False, False), + ], + ) + def test_evaluate_should_use_upgrade_message( + self, activation_result, dbsession, auto_activate_succeeds, expected, mocker + ): + activation_result = SeatActivationInfo( + should_activate_seat=activation_result, + owner_id=1, + author_id=10, + reason="mocked", + ) + mocker.patch( + "services.bundle_analysis.notify.contexts.comment.determine_seat_activation", + return_value=activation_result, + ) + mocker.patch( + "services.bundle_analysis.notify.contexts.comment.activate_user", + return_value=auto_activate_succeeds, + ) + mocker.patch( + "services.bundle_analysis.notify.contexts.comment.schedule_new_user_activated_task", + return_value=auto_activate_succeeds, + ) + head_commit, _ = get_commit_pair(dbsession) + user_yaml = UserYaml.from_dict({}) + builder = BundleAnalysisPRCommentContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + mock_pull = MagicMock( + name="fake_pull", + database_pull=MagicMock(bundle_analysis_commentid=12345, id=12), + ) + builder._notification_context.pull = mock_pull + builder.evaluate_should_use_upgrade_message() + assert builder._notification_context.should_use_upgrade_comment == expected + + @pytest.mark.django_db + def test_build_context(self, dbsession, mocker, mock_storage): + mock_all_plans_and_tiers() + head_commit, base_commit = get_commit_pair(dbsession) + repository = head_commit.repository + head_commit_report, base_commit_report = get_report_pair( + dbsession, (head_commit, base_commit) + ) + save_mock_bundle_analysis_report( + repository, head_commit_report, mock_storage, sample_report_number=2 + ) + save_mock_bundle_analysis_report( + repository, base_commit_report, mock_storage, sample_report_number=1 + ) + enriched_pull = get_enriched_pull_setting_up_mocks( + dbsession, mocker, (head_commit, base_commit) + ) + user_yaml = UserYaml.from_dict(PATCH_CENTRIC_DEFAULT_CONFIG) + builder = BundleAnalysisPRCommentContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + + context = builder.build_context().get_result() + assert context.commit_report == head_commit_report + assert context.bundle_analysis_report.session_count() == 18 + assert context.pull == enriched_pull + assert ( + context.bundle_analysis_comparison.base_report_key + == base_commit_report.external_id + ) + assert ( + context.bundle_analysis_comparison.head_report_key + == head_commit_report.external_id + ) + + @pytest.mark.django_db + def test_initialize_from_context(self, dbsession, mocker): + mock_all_plans_and_tiers() + head_commit, base_commit = get_commit_pair(dbsession) + user_yaml = UserYaml.from_dict(PATCH_CENTRIC_DEFAULT_CONFIG) + builder = BundleAnalysisPRCommentContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + context = builder.get_result() + context.commit_report = MagicMock(name="fake_commit_report") + context.bundle_analysis_report = MagicMock(name="fake_bundle_analysis_report") + + pull = PullFactory( + repository=base_commit.repository, + head=head_commit.commitid, + base=base_commit.commitid, + compared_to=base_commit.commitid, + ) + dbsession.add(pull) + dbsession.commit() + context.pull = EnrichedPull( + database_pull=pull, + provider_pull={}, + ) + + other_builder = BundleAnalysisPRCommentContextBuilder().initialize_from_context( + user_yaml, context + ) + other_context = other_builder.get_result() + + assert context.commit == other_context.commit + assert context.commit_report == other_context.commit_report + assert context.bundle_analysis_report == other_context.bundle_analysis_report + assert context.pull == other_context.pull + with pytest.raises(ContextNotLoadedError): + other_context.bundle_analysis_comparison + + fake_comparison = MagicMock( + name="fake_comparison", percentage_delta=10.0, total_size_delta=10.0 + ) + mocker.patch.object( + ComparisonLoader, "get_comparison", return_value=fake_comparison + ) + other_context = other_builder.build_context().get_result() + + assert context.commit == other_context.commit + assert context.commit_report == other_context.commit_report + assert context.bundle_analysis_report == other_context.bundle_analysis_report + assert context.pull == other_context.pull + assert other_context.bundle_analysis_comparison == fake_comparison diff --git a/apps/worker/services/bundle_analysis/notify/contexts/tests/test_commit_status_context.py b/apps/worker/services/bundle_analysis/notify/contexts/tests/test_commit_status_context.py new file mode 100644 index 0000000000..dc9dd06eee --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/contexts/tests/test_commit_status_context.py @@ -0,0 +1,376 @@ +from unittest.mock import MagicMock + +import pytest +from shared.config import PATCH_CENTRIC_DEFAULT_CONFIG +from shared.validation.types import BundleThreshold +from shared.yaml import UserYaml + +from database.models.core import GITHUB_APP_INSTALLATION_DEFAULT_NAME +from database.tests.factories.core import CommitFactory, PullFactory +from services.bundle_analysis.comparison import ComparisonLoader +from services.bundle_analysis.notify.conftest import ( + get_commit_pair, + get_enriched_pull_setting_up_mocks, + get_report_pair, + save_mock_bundle_analysis_report, +) +from services.bundle_analysis.notify.contexts import ( + ContextNotLoadedError, + NotificationContextBuildError, +) +from services.bundle_analysis.notify.contexts.commit_status import ( + CommitStatusLevel, + CommitStatusNotificationContextBuilder, +) +from services.bundle_analysis.notify.types import NotificationUserConfig +from services.repository import EnrichedPull +from services.seats import SeatActivationInfo, ShouldActivateSeat +from tests.helpers import mock_all_plans_and_tiers + + +class TestBundleAnalysisPRCommentNotificationContext: + @pytest.mark.asyncio + async def test_load_pull_not_found(self, dbsession, mocker): + head_commit, _ = get_commit_pair(dbsession) + user_yaml = UserYaml.from_dict({}) + fake_repo_service = mocker.MagicMock(name="fake_repo_service") + mock_fetch_pr = mocker.patch( + "services.bundle_analysis.notify.contexts.commit_status.fetch_and_update_pull_request_information_from_commit", + return_value=None, + ) + mocker.patch( + "services.bundle_analysis.notify.contexts.get_repo_provider_service", + return_value=fake_repo_service, + ) + builder = CommitStatusNotificationContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + await builder.load_optional_enriched_pull() + assert builder._notification_context.pull is None + mock_fetch_pr.assert_called_with(fake_repo_service, head_commit, user_yaml) + + @pytest.mark.parametrize( + "expected_missing_detail", + [ + pytest.param("MissingBaseCommit"), + pytest.param("MissingHeadCommit"), + pytest.param("MissingBaseReport"), + pytest.param("MissingHeadReport"), + ], + ) + @pytest.mark.asyncio + async def test_load_bundle_comparison_missing_some_info( + self, expected_missing_detail, dbsession, mocker + ): + head_commit, base_commit = get_commit_pair(dbsession) + sink_commit = CommitFactory(repository=head_commit.repository) + dbsession.add(sink_commit) + get_report_pair(dbsession, (head_commit, base_commit)) + enriched_pull = get_enriched_pull_setting_up_mocks( + dbsession, mocker, (head_commit, base_commit) + ) + user_yaml = UserYaml.from_dict({}) + builder = CommitStatusNotificationContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + builder.load_commit_report() + match expected_missing_detail: + case "MissingBaseCommit": + enriched_pull.database_pull.compared_to = None + case "MissingHeadCommit": + enriched_pull.database_pull.head = None + case "MissingBaseReport": + # Deleting the BaseReport also deleted base_commit + # So instead we point the Pull object to a commit that doesn't have reports + enriched_pull.database_pull.compared_to = sink_commit.commitid + case "MissingHeadReport": + enriched_pull.database_pull.head = sink_commit.commitid + + with pytest.raises(NotificationContextBuildError) as exp: + await builder.load_optional_enriched_pull() + builder.load_bundle_comparison() + assert builder._notification_context.pull is not None + assert exp.value.failed_step == "load_bundle_comparison" + assert exp.value.detail == expected_missing_detail + + @pytest.mark.asyncio + async def test_load_bundle_comparison_when_pull_is_none( + self, dbsession, mocker, mock_storage + ): + head_commit, base_commit = get_commit_pair(dbsession) + repository = head_commit.repository + head_commit.parent_commit_id = base_commit.commitid + head_report, base_report = get_report_pair( + dbsession, (head_commit, base_commit) + ) + mocker.patch( + "services.bundle_analysis.notify.contexts.commit_status.fetch_and_update_pull_request_information_from_commit", + return_value=None, + ) + mocker.patch( + "services.bundle_analysis.notify.contexts.get_repo_provider_service", + return_value=mocker.MagicMock(name="fake_repo_service"), + ) + + mock_check_compare_sha = mocker.patch( + "shared.bundle_analysis.comparison.BundleAnalysisComparison._check_compare_sha", + return_value=None, + ) + + save_mock_bundle_analysis_report( + repository, head_report, mock_storage, sample_report_number=2 + ) + save_mock_bundle_analysis_report( + repository, base_report, mock_storage, sample_report_number=1 + ) + user_yaml = UserYaml.from_dict({}) + builder = CommitStatusNotificationContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + builder.load_commit_report() + await builder.load_optional_enriched_pull() + builder.load_bundle_comparison() + context = builder.get_result() + assert context.pull is None + assert context.bundle_analysis_comparison is not None + assert ( + context.bundle_analysis_comparison.base_report_key + == base_report.external_id + ) + assert ( + context.bundle_analysis_comparison.head_report_key + == head_report.external_id + ) + + @pytest.mark.parametrize( + "yaml_dict, percent_change, absolute_change, expected", + [ + pytest.param( + PATCH_CENTRIC_DEFAULT_CONFIG, + 1.0, + 10000, + CommitStatusLevel.INFO, + id="default_config_within_5%_change", + ), + pytest.param( + PATCH_CENTRIC_DEFAULT_CONFIG, + 5.5, + 10000, + CommitStatusLevel.WARNING, + id="default_config_outside_5%_change", + ), + pytest.param( + { + **PATCH_CENTRIC_DEFAULT_CONFIG, + "bundle_analysis": {"warning_threshold": 10000}, + }, + 1.0, + 10001, + CommitStatusLevel.WARNING, + id="informational_outside_range", + ), + pytest.param( + { + **PATCH_CENTRIC_DEFAULT_CONFIG, + "bundle_analysis": {"warning_threshold": 10000}, + }, + 1.0, + 10000, + CommitStatusLevel.INFO, + id="informational_within_range", + ), + pytest.param( + { + **PATCH_CENTRIC_DEFAULT_CONFIG, + "bundle_analysis": {"warning_threshold": 10000, "status": True}, + }, + 1.0, + 10001, + CommitStatusLevel.ERROR, + id="fail_outside_range_absolute", + ), + pytest.param( + {**PATCH_CENTRIC_DEFAULT_CONFIG, "bundle_analysis": {"status": True}}, + 5.1, + 10000, + CommitStatusLevel.ERROR, + id="fail_outside_range_percentage", + ), + ], + ) + def test_load_commit_status_level( + self, yaml_dict, percent_change, absolute_change, expected, dbsession, mocker + ): + head_commit = CommitFactory() + dbsession.add(head_commit) + yaml_dict.update(PATCH_CENTRIC_DEFAULT_CONFIG) + user_yaml = UserYaml.from_dict(yaml_dict) + builder = CommitStatusNotificationContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + builder.load_user_config() + builder._notification_context.bundle_analysis_comparison = mocker.MagicMock( + name="fake_comparison", + total_size_delta=absolute_change, + percentage_delta=percent_change, + ) + builder.load_commit_status_level() + context = builder.get_result() + assert context.commit_status_level == expected + + @pytest.mark.django_db + def test_build_context(self, dbsession, mocker, mock_storage): + mock_all_plans_and_tiers() + head_commit, base_commit = get_commit_pair(dbsession) + repository = head_commit.repository + head_commit_report, base_commit_report = get_report_pair( + dbsession, (head_commit, base_commit) + ) + save_mock_bundle_analysis_report( + repository, head_commit_report, mock_storage, sample_report_number=2 + ) + save_mock_bundle_analysis_report( + repository, base_commit_report, mock_storage, sample_report_number=1 + ) + enriched_pull = get_enriched_pull_setting_up_mocks( + dbsession, mocker, (head_commit, base_commit) + ) + user_yaml = UserYaml.from_dict(PATCH_CENTRIC_DEFAULT_CONFIG) + builder = CommitStatusNotificationContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + + context = builder.build_context().get_result() + assert context.commit_report == head_commit_report + assert context.bundle_analysis_report.session_count() == 18 + assert context.pull == enriched_pull + assert ( + context.bundle_analysis_comparison.base_report_key + == base_commit_report.external_id + ) + assert ( + context.bundle_analysis_comparison.head_report_key + == head_commit_report.external_id + ) + assert context.user_config == NotificationUserConfig( + required_changes=False, + warning_threshold=BundleThreshold("percentage", 5.0), + status_level="informational", + required_changes_threshold=BundleThreshold("absolute", 0), + ) + assert context.commit_status_level == CommitStatusLevel.INFO + assert context.cache_ttl == 600 + assert context.commit_status_url is not None + + @pytest.mark.django_db + def test_initialize_from_context(self, dbsession, mocker): + mock_all_plans_and_tiers() + head_commit, base_commit = get_commit_pair(dbsession) + user_yaml = UserYaml.from_dict(PATCH_CENTRIC_DEFAULT_CONFIG) + builder = CommitStatusNotificationContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + context = builder.get_result() + context.commit_report = MagicMock(name="fake_commit_report") + context.bundle_analysis_report = MagicMock(name="fake_bundle_analysis_report") + pull = PullFactory( + repository=base_commit.repository, + head=head_commit.commitid, + base=base_commit.commitid, + compared_to=base_commit.commitid, + ) + dbsession.add(pull) + dbsession.commit() + context.pull = EnrichedPull( + database_pull=pull, + provider_pull={}, + ) + + other_builder = ( + CommitStatusNotificationContextBuilder().initialize_from_context( + user_yaml, context + ) + ) + other_context = other_builder.get_result() + + assert context.commit == other_context.commit + assert context.commit_report == other_context.commit_report + assert context.bundle_analysis_report == other_context.bundle_analysis_report + assert context.pull == other_context.pull + with pytest.raises(ContextNotLoadedError): + other_context.bundle_analysis_comparison + + fake_comparison = MagicMock( + name="fake_comparison", total_size_delta=1000, percentage_delta=1 + ) + mocker.patch.object( + ComparisonLoader, "get_comparison", return_value=fake_comparison + ) + other_context = other_builder.build_context().get_result() + + assert context.commit == other_context.commit + assert context.commit_report == other_context.commit_report + assert context.bundle_analysis_report == other_context.bundle_analysis_report + assert context.pull == other_context.pull + assert other_context.bundle_analysis_comparison == fake_comparison + + def test_evaluate_should_use_upgrade_message_no_pull(self, dbsession, mocker): + head_commit, _ = get_commit_pair(dbsession) + user_yaml = UserYaml.from_dict({}) + builder = CommitStatusNotificationContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + builder._notification_context.pull = None + builder.evaluate_should_use_upgrade_message() + assert builder._notification_context.should_use_upgrade_comment is False + + @pytest.mark.parametrize( + "activation_result, auto_activate_succeeds, expected", + [ + (ShouldActivateSeat.AUTO_ACTIVATE, True, False), + (ShouldActivateSeat.AUTO_ACTIVATE, False, True), + (ShouldActivateSeat.MANUAL_ACTIVATE, False, True), + (ShouldActivateSeat.NO_ACTIVATE, False, False), + ], + ) + def test_evaluate_should_use_upgrade_message( + self, activation_result, dbsession, auto_activate_succeeds, expected, mocker + ): + activation_result = SeatActivationInfo( + should_activate_seat=activation_result, + owner_id=1, + author_id=10, + reason="mocked", + ) + mocker.patch( + "services.bundle_analysis.notify.contexts.commit_status.determine_seat_activation", + return_value=activation_result, + ) + mocker.patch( + "services.bundle_analysis.notify.contexts.commit_status.activate_user", + return_value=auto_activate_succeeds, + ) + mocker.patch( + "services.bundle_analysis.notify.contexts.commit_status.schedule_new_user_activated_task", + return_value=auto_activate_succeeds, + ) + head_commit, _ = get_commit_pair(dbsession) + user_yaml = UserYaml.from_dict( + { + "comment": { + "layout": "reach,diff,flags,tree,reach", + "behavior": "default", + "show_carryforward_flags": False, + } + } + ) + builder = CommitStatusNotificationContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + mock_pull = MagicMock( + name="fake_pull", + database_pull=MagicMock(bundle_analysis_commentid=12345, id=12), + ) + builder._notification_context.pull = mock_pull + builder.evaluate_should_use_upgrade_message() + assert builder._notification_context.should_use_upgrade_comment == expected diff --git a/apps/worker/services/bundle_analysis/notify/contexts/tests/test_contexts.py b/apps/worker/services/bundle_analysis/notify/contexts/tests/test_contexts.py new file mode 100644 index 0000000000..c2f6e2ab6a --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/contexts/tests/test_contexts.py @@ -0,0 +1,146 @@ +from unittest.mock import MagicMock + +import pytest +from shared.validation.types import BundleThreshold +from shared.yaml import UserYaml + +from database.models.core import GITHUB_APP_INSTALLATION_DEFAULT_NAME +from services.bundle_analysis.notify.conftest import ( + get_commit_pair, + get_report_pair, + save_mock_bundle_analysis_report, +) +from services.bundle_analysis.notify.contexts import ( + ContextNotLoadedError, + NotificationContextBuilder, + NotificationContextBuildError, +) +from services.bundle_analysis.notify.types import NotificationUserConfig + + +class TestBaseBundleAnalysisNotificationContextBuild: + def test_access_not_loaded_field_raises(self, dbsession): + head_commit, _ = get_commit_pair(dbsession) + builder = NotificationContextBuilder().initialize( + head_commit, UserYaml.from_dict({}), GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + with pytest.raises(ContextNotLoadedError) as exp: + builder._notification_context.commit_report + assert ( + str(exp.value) + == "Property commit_report is not loaded. Make sure to build the context before using it." + ) + + @pytest.mark.parametrize( + "field_name, expected", + [ + ("commit_report", True), + ("bundle_analysis_report", False), + ("field_doesnt_exist", False), + ], + ) + def test_is_field_loaded(self, field_name, expected, dbsession): + head_commit, _ = get_commit_pair(dbsession) + builder = NotificationContextBuilder().initialize( + head_commit, UserYaml.from_dict({}), GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + builder._notification_context.commit_report = MagicMock( + name="fake_commit_report" + ) + assert builder.is_field_loaded(field_name) == expected + + def test_load_commit_report_no_report(self, dbsession): + head_commit, _ = get_commit_pair(dbsession) + builder = NotificationContextBuilder().initialize( + head_commit, UserYaml.from_dict({}), GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + with pytest.raises(NotificationContextBuildError) as exp: + builder.load_commit_report() + assert exp.value.failed_step == "load_commit_report" + + def test_load_bundle_analysis_report_no_report(self, dbsession, mock_storage): + head_commit, base_commit = get_commit_pair(dbsession) + get_report_pair(dbsession, (head_commit, base_commit)) + builder = NotificationContextBuilder().initialize( + head_commit, UserYaml.from_dict({}), GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + with pytest.raises(NotificationContextBuildError) as exp: + builder.load_commit_report() + builder.load_bundle_analysis_report() + assert exp.value.failed_step == "load_bundle_analysis_report" + + @pytest.mark.parametrize( + "config, expected_user_config", + [ + pytest.param( + { + "comment": { + "layout": "reach,diff,flags,tree,reach", + "behavior": "default", + "show_carryforward_flags": False, + } + }, + NotificationUserConfig( + required_changes=False, + warning_threshold=BundleThreshold("percentage", 5.0), + status_level="informational", + required_changes_threshold=BundleThreshold("absolute", 0), + ), + id="default_site_config", + ), + pytest.param( + {"comment": False}, + NotificationUserConfig( + required_changes=False, + warning_threshold=BundleThreshold("percentage", 5.0), + status_level="informational", + required_changes_threshold=BundleThreshold("absolute", 0), + ), + id="comment_is_bool", + ), + ], + ) + def test_build_context( + self, dbsession, mock_storage, mocker, config, expected_user_config + ): + head_commit, base_commit = get_commit_pair(dbsession) + head_commit_report, _ = get_report_pair(dbsession, (head_commit, base_commit)) + save_mock_bundle_analysis_report( + head_commit.repository, + head_commit_report, + mock_storage, + sample_report_number=1, + ) + builder = NotificationContextBuilder().initialize( + head_commit, + UserYaml.from_dict(config), + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + ) + context = builder.build_context().get_result() + assert context.commit_report == head_commit_report + assert context.bundle_analysis_report.session_count() == 19 + assert context.user_config == expected_user_config + assert [ + bundle_report.name + for bundle_report in context.bundle_analysis_report.bundle_reports() + ] == [ + "@codecov/sveltekit-plugin-cjs", + "@codecov/webpack-plugin-cjs", + "@codecov/webpack-plugin-esm", + "@codecov/vite-plugin-esm", + "@codecov/bundler-plugin-core-esm", + "@codecov/remix-vite-plugin-esm", + "@codecov/nuxt-plugin-esm", + "@codecov/rollup-plugin-esm", + "@codecov/example-webpack-app-array-push", + "@codecov/remix-vite-plugin-cjs", + "@codecov/nuxt-plugin-cjs", + "@codecov/example-vite-app-esm", + "@codecov/example-next-app-edge-server-array-push", + "@codecov/rollup-plugin-cjs", + "@codecov/sveltekit-plugin-esm", + "@codecov/vite-plugin-cjs", + "@codecov/bundler-plugin-core-cjs", + "@codecov/example-rollup-app-iife", + "@codecov/example-next-app-server-cjs", + ] diff --git a/apps/worker/services/bundle_analysis/notify/helpers.py b/apps/worker/services/bundle_analysis/notify/helpers.py new file mode 100644 index 0000000000..a40eab87f6 --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/helpers.py @@ -0,0 +1,117 @@ +import numbers +from typing import Literal, Optional + +from shared.bundle_analysis import BundleAnalysisComparison, BundleChange +from shared.django_apps.codecov_auth.models import Service +from shared.torngit.base import TorngitBaseAdapter +from shared.validation.types import BundleThreshold +from shared.yaml import UserYaml + +from database.models.core import Owner +from services.bundle_analysis.notify.types import NotificationType + + +def is_commit_status_configured( + yaml: UserYaml, owner: Owner +) -> None | NotificationType: + """Verifies if we should attempt to send bundle analysis commit status based on given YAML. + Config field is `bundle_analysis.status` (default: "informational") + + If the user is from GitHub and has an app we can send NotificationType.GITHUB_COMMIT_CHECK. + """ + is_status_configured: bool | Literal["informational"] = yaml.read_yaml_field( + "bundle_analysis", "status", _else="informational" + ) + is_github = Service(owner.service) in (Service.GITHUB, Service.GITHUB_ENTERPRISE) + owner_has_app = owner.github_app_installations != [] + if is_status_configured: + if is_github and owner_has_app: + return NotificationType.GITHUB_COMMIT_CHECK + return NotificationType.COMMIT_STATUS + return None + + +def is_comment_configured(yaml: UserYaml, owner: Owner) -> None | NotificationType: + """Verifies if we should attempt to send bundle analysis PR comment based on given YAML. + Config field is `comment` (default: see shared.config) + """ + is_comment_configured: dict | bool = yaml.read_yaml_field("comment") is not False + if is_comment_configured: + return NotificationType.PR_COMMENT + return None + + +def get_notification_types_configured( + yaml: UserYaml, owner: Owner +) -> tuple[NotificationType, ...]: + """Gets a tuple with all the different bundle analysis notifications that we should attempt to send, + based on the given YAML""" + notification_types = [ + is_comment_configured(yaml, owner), + is_commit_status_configured(yaml, owner), + ] + return tuple(filter(None, notification_types)) + + +def get_github_app_used(torngit: TorngitBaseAdapter | None) -> int | None: + if torngit is None: + return None + torngit_installation = torngit.data.get("installation") + selected_installation_id = ( + torngit_installation.get("id") if torngit_installation else None + ) + return selected_installation_id + + +def bytes_readable(bytes: int, show_negative: Optional[bool] = True) -> str: + """Converts bytes into human-readable string (up to GB)""" + is_negative = bytes < 0 + value: float = abs(bytes) + exponent_index = 0 + + while value >= 1000 and exponent_index < 3: + value /= 1000 + exponent_index += 1 + + exponent_str = [" bytes", "kB", "MB", "GB"][exponent_index] + rounded_value = round(value, 2) + + if is_negative and show_negative: + return f"-{rounded_value}{exponent_str}" + else: + return f"{rounded_value}{exponent_str}" + + +def to_BundleThreshold(value: int | float | BundleThreshold) -> BundleThreshold: + if isinstance(value, (list, tuple)) and value[0] in ["absolute", "percentage"]: + return BundleThreshold(*value) + if isinstance(value, numbers.Integral): + return BundleThreshold("absolute", value) + elif isinstance(value, numbers.Number): + return BundleThreshold("percentage", value) + raise TypeError(f"Can't parse {value} into BundleThreshold") + + +def is_bundle_comparison_change_within_configured_threshold( + comparison: BundleAnalysisComparison, + threshold: BundleThreshold, + compare_non_negative_numbers: bool = False, +) -> bool: + if threshold.type == "absolute": + total_size_delta = ( + abs(comparison.total_size_delta) + if compare_non_negative_numbers + else comparison.total_size_delta + ) + return total_size_delta <= threshold.threshold + else: + return comparison.percentage_delta <= threshold.threshold + + +def is_bundle_change_within_configured_threshold( + bundle_change: BundleChange, threshold: BundleThreshold +) -> bool: + if threshold.type == "absolute": + return bundle_change.size_delta <= threshold.threshold + else: + return bundle_change.percentage_delta <= threshold.threshold diff --git a/apps/worker/services/bundle_analysis/notify/messages/__init__.py b/apps/worker/services/bundle_analysis/notify/messages/__init__.py new file mode 100644 index 0000000000..4678f43c62 --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/messages/__init__.py @@ -0,0 +1,21 @@ +from abc import ABC, abstractmethod + +from services.bundle_analysis.notify.contexts import ( + BaseBundleAnalysisNotificationContext, +) +from services.notification.notifiers.base import NotificationResult + + +class MessageStrategyInterface(ABC): + @abstractmethod + def build_message( + self, context: BaseBundleAnalysisNotificationContext + ) -> str | bytes: + """Builds the message to be sent using the `context` information""" + pass + + @abstractmethod + def send_message( + self, context: BaseBundleAnalysisNotificationContext, message: str | bytes + ) -> NotificationResult: + pass diff --git a/apps/worker/services/bundle_analysis/notify/messages/comment.py b/apps/worker/services/bundle_analysis/notify/messages/comment.py new file mode 100644 index 0000000000..3fdabcf56d --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/messages/comment.py @@ -0,0 +1,385 @@ +import logging +from typing import List, Literal, TypedDict + +import sentry_sdk +from asgiref.sync import async_to_sync +from django.template import loader +from shared.bundle_analysis import ( + BundleAnalysisComparison, + BundleChange, + MissingBundleError, +) +from shared.bundle_analysis.comparison import AssetChange, RouteChange +from shared.torngit.exceptions import TorngitClientError +from shared.validation.types import BundleThreshold + +from services.bundle_analysis.notify.contexts.comment import ( + BundleAnalysisPRCommentNotificationContext, +) +from services.bundle_analysis.notify.helpers import ( + bytes_readable, + get_github_app_used, + is_bundle_change_within_configured_threshold, +) +from services.bundle_analysis.notify.messages import MessageStrategyInterface +from services.license import requires_license +from services.notification.notifiers.base import NotificationResult +from services.urls import get_bundle_analysis_pull_url, get_members_url + +log = logging.getLogger(__name__) + + +class BundleRow(TypedDict): + bundle_name: str + bundle_size: str + change_size_readable: str + percentage_change_readable: str + change_icon: str + has_cached: bool + is_change_outside_threshold: bool + + +class BundleRouteRow(TypedDict): + route_name: str + change_size_readable: str + percentage_change_readable: str + change_icon: str + route_size: str + + +class ModuleData(TypedDict): + module_name: str + change_size_readable: str + + +class AssetData(TypedDict): + asset_display_name_1: str + asset_display_name_2: str + change_size_readable: str + percentage_change_readable: str + change_icon: str + asset_size_readable: str + module_data: List[ModuleData] + + +class IndividualBundleData(TypedDict): + bundle_name: str + asset_data: List[AssetData] + app_routes_data: List[BundleRouteRow] + + +class BundleCommentTemplateContext(TypedDict): + pull_url: str + total_size_delta: int + total_size_readable: str + total_percentage: str + status_level: Literal["INFO"] | Literal["WARNING"] | Literal["ERROR"] + warning_threshold_readable: str + bundle_rows: list[BundleRow] + has_cached_bundles: bool + individual_bundle_data: dict[str, IndividualBundleData] + + +class UpgradeCommentTemplateContext(TypedDict): + author_username: str + is_saas: bool + activation_link: str + + +class BundleAnalysisCommentMarkdownStrategy(MessageStrategyInterface): + def build_message( + self, context: BundleAnalysisPRCommentNotificationContext + ) -> str | bytes: + if context.should_use_upgrade_comment: + return self.build_upgrade_message(context) + else: + return self.build_default_message(context) + + @sentry_sdk.trace + def build_default_message( + self, context: BundleAnalysisPRCommentNotificationContext + ) -> str: + try: + pull = context.pull.database_pull + repository_service = context.repository_service + changed_files = async_to_sync(repository_service.get_pull_request_files)( + pull.pullid + ) + except Exception: + changed_files = None + log.error( + "Unable to retrieve PR files", + extra=dict( + commit=context.commit.commitid, + report_key=context.commit_report.external_id, + pullid=pull.pullid, + ), + exc_info=True, + ) + + template = loader.get_template("bundle_analysis_notify/bundle_comment.md") + total_size_delta = context.bundle_analysis_comparison.total_size_delta + warning_threshold = context.user_config.warning_threshold + bundle_rows = self._create_bundle_rows( + context.bundle_analysis_comparison, warning_threshold + ) + + individual_bundle_data = self._create_individual_bundle_data( + context.bundle_analysis_comparison, + changed_files, + warning_threshold, + ) + + if warning_threshold.type == "absolute": + warning_threshold_readable = bytes_readable(warning_threshold.threshold) + else: + warning_threshold_readable = str(round(warning_threshold.threshold)) + "%" + context = BundleCommentTemplateContext( + has_cached=any(row["is_cached"] for row in bundle_rows), + bundle_rows=bundle_rows, + pull_url=get_bundle_analysis_pull_url(pull=context.pull.database_pull), + total_size_delta=total_size_delta, + status_level=context.commit_status_level.name, + total_percentage=str( + round(context.bundle_analysis_comparison.percentage_delta, 2) + ) + + "%", + total_size_readable=bytes_readable(total_size_delta, show_negative=False), + warning_threshold_readable=warning_threshold_readable, + individual_bundle_data=individual_bundle_data, + ) + return template.render(context) + + @sentry_sdk.trace + def build_upgrade_message( + self, context: BundleAnalysisPRCommentNotificationContext + ) -> str: + template = loader.get_template("bundle_analysis_notify/upgrade_comment.md") + context = UpgradeCommentTemplateContext( + activation_link=get_members_url(context.pull.database_pull), + is_saas=not requires_license(), + author_username=context.pull.provider_pull["author"].get("username"), + ) + return template.render(context) + + @sentry_sdk.trace + def send_message( + self, context: BundleAnalysisPRCommentNotificationContext, message: str + ) -> NotificationResult: + pull = context.pull.database_pull + repository_service = context.repository_service + try: + comment_id = pull.bundle_analysis_commentid + if comment_id: + async_to_sync(repository_service.edit_comment)( + pull.pullid, comment_id, message + ) + else: + res = async_to_sync(repository_service.post_comment)( + pull.pullid, message + ) + pull.bundle_analysis_commentid = res["id"] + return NotificationResult( + notification_attempted=True, + notification_successful=True, + github_app_used=get_github_app_used(repository_service), + ) + except TorngitClientError: + log.error( + "Error creating/updating PR comment", + extra=dict( + commit=context.commit.commitid, + report_key=context.commit_report.external_id, + pullid=pull.pullid, + ), + ) + return NotificationResult( + notification_attempted=True, + notification_successful=False, + explanation="TorngitClientError", + ) + + def _create_bundle_rows( + self, + comparison: BundleAnalysisComparison, + configured_threshold: BundleThreshold, + ) -> list[BundleRow]: + bundle_rows = [] + bundle_changes = comparison.bundle_changes() + # Calculate bundle change data in one loop since bundle_changes is a generator + for bundle_change in bundle_changes: + # Define row table data + bundle_name = bundle_change.bundle_name + if bundle_change.change_type == BundleChange.ChangeType.REMOVED: + size = "(removed)" + is_cached = False + else: + head_bundle_report = comparison.head_report.bundle_report(bundle_name) + size = bytes_readable(head_bundle_report.total_size()) + is_cached = head_bundle_report.is_cached() + + change_size = bundle_change.size_delta + if change_size == 0: + # Don't include bundles that were not changed in the table + continue + icon = "" + if change_size > 0: + icon = ":arrow_up:" + elif change_size < 0: + icon = ":arrow_down:" + + bundle_rows.append( + BundleRow( + bundle_name=bundle_name, + bundle_size=size, + change_size_readable=bytes_readable(change_size), + change_icon=icon, + is_cached=is_cached, + percentage_change_readable=f"{bundle_change.percentage_delta}%", + is_change_outside_threshold=( + not is_bundle_change_within_configured_threshold( + bundle_change, configured_threshold + ) + ), + ) + ) + + return bundle_rows + + def _create_bundle_route_data( + self, + comparison: BundleAnalysisComparison, + warning_threshold: BundleThreshold, + ) -> dict[str, list[BundleRouteRow]]: + """ + Translate BundleRouteComparison dict data into a template compatible dict data + """ + bundle_route_data = {} + changes_dict = comparison.bundle_routes_changes() + + for bundle_name, route_changes in changes_dict.items(): + rows = [] + for route_change in route_changes: + change_size, size = ( + route_change.size_delta, + bytes_readable(route_change.size_head), + ) + + if change_size == 0: + continue + + exceeds_threshold = ( + warning_threshold.type == "percentage" + and route_change.percentage_delta > warning_threshold.threshold + ) + bundle_display_name, icon = route_change.route_name, "" + if route_change.change_type == RouteChange.ChangeType.ADDED: + icon = ":rocket:" + bundle_display_name = f"**{route_change.route_name}** _(New)_" + elif route_change.change_type == RouteChange.ChangeType.REMOVED: + icon = ":wastebasket:" + bundle_display_name = ( + f"~~**{route_change.route_name}**~~ _(Deleted)_" + ) + elif exceeds_threshold: + icon = ":warning:" + + rows.append( + BundleRouteRow( + route_name=bundle_display_name, + change_size_readable=bytes_readable(change_size), + percentage_change_readable=f"{route_change.percentage_delta}%", + change_icon=icon, + route_size=size, + ) + ) + bundle_route_data[bundle_name] = rows + return bundle_route_data + + def _create_asset_data( + self, + comparison: BundleAnalysisComparison, + bundle_name: str, + changed_files: List[str] | None, + warning_threshold: BundleThreshold, + ) -> List[AssetData]: + try: + asset_data = [] + asset_comparisons = comparison.bundle_comparison( + bundle_name + ).asset_comparisons() + for asset_comparison in asset_comparisons: + asset_change = asset_comparison.asset_change() + + # If not change in size for the asset then we don't show it + if asset_change.size_delta == 0: + continue + + # Determine what asset name styling and change icon to use + asset_display_name_1 = f"```{asset_change.asset_name}```" + asset_display_name_2 = asset_display_name_2 = ( + f"**```{asset_change.asset_name}```**" + ) + exceeds_threshold = ( + warning_threshold.type == "percentage" + and asset_change.percentage_delta > warning_threshold.threshold + ) + if asset_change.change_type == AssetChange.ChangeType.ADDED: + asset_display_name_1 = f"**{asset_display_name_1}** _(New)_" + change_icon = ":rocket:" + elif asset_change.change_type == AssetChange.ChangeType.REMOVED: + asset_display_name_1 = f"~~**{asset_display_name_1}**~~ _(Deleted)_" + change_icon = ":wastebasket:" + elif exceeds_threshold: + change_icon = ":warning:" + else: + change_icon = "" + + modules = asset_comparison.contributing_modules( + pr_changed_files=changed_files + ) + asset_data.append( + AssetData( + asset_display_name_1=asset_display_name_1, + asset_display_name_2=asset_display_name_2, + change_size_readable=bytes_readable(asset_change.size_delta), + percentage_change_readable=f"{asset_change.percentage_delta}%", + change_icon=change_icon, + asset_size_readable=bytes_readable(asset_change.size_head), + module_data=[ + ModuleData( + module_name=f"```{module.name}```", + change_size_readable=bytes_readable(module.size), + ) + for module in modules + ], + ) + ) + return asset_data + except MissingBundleError: + # Won't have assets changed comparisons if either head or base report doesn't have the bundle + return [] + + def _create_individual_bundle_data( + self, + comparison: BundleAnalysisComparison, + changed_files: List[str] | None, + warning_threshold: BundleThreshold, + ) -> dict[str, IndividualBundleData]: + data = {} + bundle_route_data = self._create_bundle_route_data( + comparison, warning_threshold + ) + for bundle_name in bundle_route_data.keys(): + asset_data = self._create_asset_data( + comparison, bundle_name, changed_files, warning_threshold + ) + + # Only create an entry for this bundle if the there's either app routes or asset changes + if asset_data or bundle_route_data.get(bundle_name): + data[bundle_name] = IndividualBundleData( + bundle_name=bundle_name, + asset_data=asset_data, + app_routes_data=bundle_route_data[bundle_name], + ) + return data diff --git a/apps/worker/services/bundle_analysis/notify/messages/commit_status.py b/apps/worker/services/bundle_analysis/notify/messages/commit_status.py new file mode 100644 index 0000000000..5eee45344a --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/messages/commit_status.py @@ -0,0 +1,129 @@ +import logging +from typing import TypedDict + +import sentry_sdk +from asgiref.sync import async_to_sync +from django.template import loader +from shared.helpers.cache import cache, make_hash_sha256 +from shared.torngit.exceptions import TorngitClientError + +from services.bundle_analysis.notify.contexts.commit_status import ( + CommitStatusLevel, + CommitStatusNotificationContext, +) +from services.bundle_analysis.notify.helpers import bytes_readable, get_github_app_used +from services.bundle_analysis.notify.messages import MessageStrategyInterface +from services.notification.notifiers.base import NotificationResult + +log = logging.getLogger(__name__) + + +class BundleCommentTemplateContext(TypedDict): + prefix_message: str + change_readable: str + warning_threshold_readable: str + + +class CommitStatusMessageStrategy(MessageStrategyInterface): + def build_message(self, context: CommitStatusNotificationContext) -> str | bytes: + if context.should_use_upgrade_comment: + return self.build_upgrade_message(context) + else: + return self.build_default_message(context) + + @sentry_sdk.trace + def build_default_message( + self, context: CommitStatusNotificationContext + ) -> str | bytes: + template = loader.get_template( + "bundle_analysis_notify/commit_status_summary.md" + ) + # Prefix message is based on the commit status level + prefix_message = { + CommitStatusLevel.INFO: "", + CommitStatusLevel.WARNING: "Passed with Warnings - ", + CommitStatusLevel.ERROR: "Failed - ", + }.get(context.commit_status_level) + + warning_threshold = context.user_config.warning_threshold + if warning_threshold.type == "absolute": + warning_threshold_readable = bytes_readable(warning_threshold.threshold) + absolute_change = context.bundle_analysis_comparison.total_size_delta + change_readable = bytes_readable(absolute_change) + else: + warning_threshold_readable = str(warning_threshold.threshold) + "%" + change_readable = ( + str(context.bundle_analysis_comparison.percentage_delta) + "%" + ) + + context = BundleCommentTemplateContext( + prefix_message=prefix_message, + change_readable=change_readable, + warning_threshold_readable=warning_threshold_readable, + ) + return template.render(context) + + @sentry_sdk.trace + def build_upgrade_message(self, context: CommitStatusNotificationContext) -> str: + author_username = context.pull.provider_pull["author"].get("username") + return ( + f"Please activate user {author_username} to display a detailed status check" + ) + + def _cache_key(self, context: CommitStatusNotificationContext) -> str: + return "cache:" + make_hash_sha256( + dict( + type="status_check_notification", + repoid=context.repository.repoid, + base_commitid=context.base_commit.commitid, + head_commitid=context.commit.commitid, + notifier_name="bundle_analysis_commit_status", + notifier_title="codecov/bundles", + ) + ) + + @sentry_sdk.trace + def send_message( + self, context: CommitStatusNotificationContext, message: str | bytes + ) -> NotificationResult: + repository_service = context.repository_service + cache_key = self._cache_key(context) + last_payload = cache.get_backend().get(cache_key) + if message == last_payload: + return NotificationResult( + notification_attempted=False, + notification_successful=False, + explanation="payload_unchanged", + ) + try: + async_to_sync(repository_service.set_commit_status)( + context.commit.commitid, + context.commit_status_level.to_str(), + "codecov/bundles", + message, + context.commit_status_url, + ) + # Update the recently-sent messages cache + cache.get_backend().set( + cache_key, + context.cache_ttl, + message, + ) + return NotificationResult( + notification_attempted=True, + notification_successful=True, + github_app_used=get_github_app_used(repository_service), + ) + except TorngitClientError: + log.error( + "Failed to set commit status", + extra=dict( + commit=context.commit.commitid, + report_key=context.commit_report.external_id, + ), + ) + return NotificationResult( + notification_attempted=True, + notification_successful=False, + explanation="TorngitClientError", + ) diff --git a/apps/worker/services/bundle_analysis/notify/messages/templates/bundle_analysis_notify/bundle_comment.md b/apps/worker/services/bundle_analysis/notify/messages/templates/bundle_analysis_notify/bundle_comment.md new file mode 100644 index 0000000000..1eeceef544 --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/messages/templates/bundle_analysis_notify/bundle_comment.md @@ -0,0 +1,11 @@ +## [Bundle]({{pull_url}}) Report +{% if total_size_delta == 0 %} +Bundle size has no change :white_check_mark: +{% else %} +{% if status_level == "ERROR" %}:x: Check failed: c{% else %}C{% endif %}hanges will {% if total_size_delta > 0 %}increase{% else %}decrease{% endif %} total bundle size by {{total_size_readable}} ({{total_percentage}}) {% if total_size_delta > 0 %}:arrow_up:{% else %}:arrow_down:{% endif %}{% if status_level == "WARNING" %}:warning:, exceeding the [configured](https://docs.codecov.com/docs/javascript-bundle-analysis#main-features) threshold of {{warning_threshold_readable}}.{% elif status_level == "ERROR" %}, **exceeding** the [configured](https://docs.codecov.com/docs/javascript-bundle-analysis#main-features) threshold of {{warning_threshold_readable}}.{% else %}. This is within the [configured](https://docs.codecov.com/docs/javascript-bundle-analysis#main-features) threshold :white_check_mark:{% endif %} +{% endif %} +{% if bundle_rows %}{% include "bundle_analysis_notify/bundle_table.md" %}{% if has_cached %} + +ℹ️ *Bundle size includes cached data from a previous commit +{%endif%}{% endif %} +{% if individual_bundle_data %}{% include "bundle_analysis_notify/individual_bundle_data.md" %}{% endif %} \ No newline at end of file diff --git a/apps/worker/services/bundle_analysis/notify/messages/templates/bundle_analysis_notify/bundle_table.md b/apps/worker/services/bundle_analysis/notify/messages/templates/bundle_analysis_notify/bundle_table.md new file mode 100644 index 0000000000..bc270594cf --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/messages/templates/bundle_analysis_notify/bundle_table.md @@ -0,0 +1,7 @@ +{% if status_level == "INFO" %}
Detailed changes + +{% endif %}| Bundle name | Size | Change | +| ----------- | ---- | ------ |{% for bundle_row in bundle_rows %} +| {{bundle_row.bundle_name}}{% if bundle_row.is_cached %}*{% endif %} | {{bundle_row.bundle_size}} | {{bundle_row.change_size_readable}} ({{bundle_row.percentage_change_readable}}) {{bundle_row.change_icon}}{% if bundle_row.is_change_outside_threshold and status_level == "WARNING" %}:warning:{% elif bundle_row.is_change_outside_threshold and status_level == "ERROR"%}:x:{% endif %}{{bundle_row.}} |{% endfor %}{% if status_level == "INFO" %} + +
{% endif %} \ No newline at end of file diff --git a/apps/worker/services/bundle_analysis/notify/messages/templates/bundle_analysis_notify/commit_status_summary.md b/apps/worker/services/bundle_analysis/notify/messages/templates/bundle_analysis_notify/commit_status_summary.md new file mode 100644 index 0000000000..10203eda8b --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/messages/templates/bundle_analysis_notify/commit_status_summary.md @@ -0,0 +1 @@ +{{prefix_message}}Bundle change: {{change_readable}} (Threshold: {{warning_threshold_readable}}) \ No newline at end of file diff --git a/apps/worker/services/bundle_analysis/notify/messages/templates/bundle_analysis_notify/individual_bundle_data.md b/apps/worker/services/bundle_analysis/notify/messages/templates/bundle_analysis_notify/individual_bundle_data.md new file mode 100644 index 0000000000..1f76a2f68b --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/messages/templates/bundle_analysis_notify/individual_bundle_data.md @@ -0,0 +1,28 @@ + +### Affected Assets, Files, and Routes: +{% for bundle_name, bundle_data in individual_bundle_data.items %} +
+view changes for bundle: {{ bundle_name }} + +{% if bundle_data.asset_data %}#### **Assets Changed:** +| Asset Name | Size Change | Total Size | Change (%) | +| ---------- | ----------- | ---------- | ---------- |{% for row in bundle_data.asset_data %} +| {{row.asset_display_name_1}} | {{row.change_size_readable}} | {{row.asset_size_readable}} | {{row.percentage_change_readable}} {{row.change_icon}} |{% endfor %} + +{% for row in bundle_data.asset_data %} +{% if row.module_data %}**Files in** {{row.asset_display_name_2}}: +{% for module in row.module_data %} +- {{module.module_name}} → Total Size: **{{module.change_size_readable}}** +{% endfor %} +{% endif %} +{% endfor %} +{% endif %} +{% if bundle_data.app_routes_data %}#### App Routes Affected: + +| App Route | Size Change | Total Size | Change (%) | +| --------- | ----------- | ---------- | ---------- |{% for row in bundle_data.app_routes_data %} +| {{row.route_name}} | {{row.change_size_readable}} | {{row.route_size}} | {{row.percentage_change_readable}} {{row.change_icon}} |{% endfor %} + +{% endif %} +
+{% endfor %} \ No newline at end of file diff --git a/apps/worker/services/bundle_analysis/notify/messages/templates/bundle_analysis_notify/upgrade_comment.md b/apps/worker/services/bundle_analysis/notify/messages/templates/bundle_analysis_notify/upgrade_comment.md new file mode 100644 index 0000000000..bfb8aa52bc --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/messages/templates/bundle_analysis_notify/upgrade_comment.md @@ -0,0 +1,8 @@ +The author of this PR, {{author_username}}, {% if is_saas %}is not an activated member of this organization on Codecov.{% else %}is not activated in your Codecov Self-Hosted installation.{% endif %} +Please [activate this user]({{activation_link}}) to display this PR comment. +Bundle data is still being uploaded to {{ is_saas|yesno:"Codecov,your instance of Codecov"}} for purposes of overall calculations. +{% if is_saas %} +Please don't hesitate to email us at support@codecov.io with any questions. +{% else %} +Please contact your Codecov On-Premises installation administrator with any questions. +{% endif %} \ No newline at end of file diff --git a/apps/worker/services/bundle_analysis/notify/messages/tests/__init__.py b/apps/worker/services/bundle_analysis/notify/messages/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/bundle_analysis/notify/messages/tests/test_comment.py b/apps/worker/services/bundle_analysis/notify/messages/tests/test_comment.py new file mode 100644 index 0000000000..8a54a54dab --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/messages/tests/test_comment.py @@ -0,0 +1,259 @@ +from textwrap import dedent +from unittest.mock import MagicMock + +import pytest +from mock import AsyncMock +from shared.config import PATCH_CENTRIC_DEFAULT_CONFIG +from shared.torngit.exceptions import TorngitClientError +from shared.typings.torngit import TorngitInstanceData +from shared.validation.types import BundleThreshold +from shared.yaml import UserYaml + +from database.models.core import GITHUB_APP_INSTALLATION_DEFAULT_NAME +from database.tests.factories.core import PullFactory +from services.bundle_analysis.notify.conftest import ( + get_commit_pair, + get_enriched_pull_setting_up_mocks, + get_report_pair, + save_mock_bundle_analysis_report, +) +from services.bundle_analysis.notify.contexts.comment import ( + BundleAnalysisPRCommentContextBuilder, + BundleAnalysisPRCommentNotificationContext, +) +from services.bundle_analysis.notify.messages.comment import ( + BundleAnalysisCommentMarkdownStrategy, +) +from services.bundle_analysis.notify.types import NotificationUserConfig +from services.notification.notifiers.base import NotificationResult +from tests.helpers import mock_all_plans_and_tiers + + +class TestCommentMesage: + @pytest.mark.django_db + def test_build_message_from_samples(self, dbsession, mocker, mock_storage): + mock_all_plans_and_tiers() + head_commit, base_commit = get_commit_pair(dbsession) + repository = head_commit.repository + head_commit_report, base_commit_report = get_report_pair( + dbsession, (head_commit, base_commit) + ) + save_mock_bundle_analysis_report( + repository, head_commit_report, mock_storage, sample_report_number=2 + ) + save_mock_bundle_analysis_report( + repository, base_commit_report, mock_storage, sample_report_number=1 + ) + enriched_pull = get_enriched_pull_setting_up_mocks( + dbsession, mocker, (head_commit, base_commit) + ) + user_yaml = UserYaml.from_dict(PATCH_CENTRIC_DEFAULT_CONFIG) + builder = BundleAnalysisPRCommentContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + + context = builder.build_context().get_result() + message = BundleAnalysisCommentMarkdownStrategy().build_message(context) + assert message.startswith( + dedent("""\ + ## [Bundle](https://app.codecov.io/gh/{owner}/{repo}/pull/{pullid}?dropdown=bundle) Report + + Changes will decrease total bundle size by 372.56kB (-48.89%) :arrow_down:. This is within the [configured](https://docs.codecov.com/docs/javascript-bundle-analysis#main-features) threshold :white_check_mark: + +
Detailed changes + + | Bundle name | Size | Change | + | ----------- | ---- | ------ | + | @codecov/sveltekit-plugin-esm | 1.1kB | 188 bytes (20.68%) :arrow_up: | + | @codecov/rollup-plugin-esm | 1.32kB | -1.01kB (-43.37%) :arrow_down: | + | @codecov/bundler-plugin-core-esm | 8.2kB | -30.02kB (-78.55%) :arrow_down: | + | @codecov/bundler-plugin-core-cjs | 43.32kB | 611 bytes (1.43%) :arrow_up: | + | @codecov/example-next-app-server-cjs | (removed) | -342.32kB (-100.0%) :arrow_down: | + +
+ """).format( + pullid=enriched_pull.database_pull.pullid, + owner=head_commit.repository.owner.username, + repo=head_commit.repository.name, + ) + ) + + def _setup_send_message_tests( + self, dbsession, mocker, torngit_ghapp_data, bundle_analysis_commentid + ): + fake_repo_provider = MagicMock( + name="fake_repo_provider", + data=TorngitInstanceData(installation=torngit_ghapp_data), + post_comment=AsyncMock(), + edit_comment=AsyncMock(), + ) + fake_repo_provider.post_comment.return_value = {"id": 1000} + fake_repo_provider.edit_comment.return_value = {"id": 1000} + mocker.patch( + "services.bundle_analysis.notify.contexts.get_repo_provider_service", + return_value=fake_repo_provider, + ) + head_commit, _ = get_commit_pair(dbsession) + context = BundleAnalysisPRCommentNotificationContext( + head_commit, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + mock_pull = MagicMock( + name="fake_pull", + database_pull=MagicMock( + name="fake_database_pull", + bundle_analysis_commentid=bundle_analysis_commentid, + pullid=12, + ), + ) + context.__dict__["pull"] = mock_pull + context.__dict__["user_config"] = NotificationUserConfig( + warning_threshold=BundleThreshold("absolute", 0), + required_changes_threshold=BundleThreshold("absolute", 0), + required_changes=False, + status_level="informational", + ) + mock_comparison = MagicMock(name="fake_bundle_analysis_comparison") + context.__dict__["bundle_analysis_comparison"] = mock_comparison + message = "carefully crafted message" + return (fake_repo_provider, mock_pull, context, message) + + @pytest.mark.parametrize( + "torngit_ghapp_data", + [ + pytest.param(None, id="no_app_used"), + pytest.param( + { + "installation_id": 123, + "id": 12, + "app_id": 12300, + "pem_path": "some_path", + }, + id="some_app_used", + ), + ], + ) + def test_send_message_no_exising_comment( + self, dbsession, mocker, torngit_ghapp_data + ): + fake_repo_provider, mock_pull, context, message = ( + self._setup_send_message_tests( + dbsession, mocker, torngit_ghapp_data, bundle_analysis_commentid=None + ) + ) + strategy = BundleAnalysisCommentMarkdownStrategy() + result = strategy.send_message(context, message) + expected_app = torngit_ghapp_data.get("id") if torngit_ghapp_data else None + assert result == NotificationResult( + notification_attempted=True, + notification_successful=True, + github_app_used=expected_app, + ) + fake_repo_provider.post_comment.assert_called_with(12, message) + fake_repo_provider.edit_message.assert_not_called() + assert mock_pull.database_pull.bundle_analysis_commentid == 1000 + + @pytest.mark.parametrize( + "torngit_ghapp_data", + [ + pytest.param(None, id="no_app_used"), + pytest.param( + { + "installation_id": 123, + "id": 12, + "app_id": 12300, + "pem_path": "some_path", + }, + id="some_app_used", + ), + ], + ) + def test_send_message_exising_comment(self, dbsession, mocker, torngit_ghapp_data): + fake_repo_provider, _, context, message = self._setup_send_message_tests( + dbsession, mocker, torngit_ghapp_data, bundle_analysis_commentid=1000 + ) + strategy = BundleAnalysisCommentMarkdownStrategy() + result = strategy.send_message(context, message) + expected_app = torngit_ghapp_data.get("id") if torngit_ghapp_data else None + assert result == NotificationResult( + notification_attempted=True, + notification_successful=True, + github_app_used=expected_app, + ) + fake_repo_provider.edit_comment.assert_called_with(12, 1000, message) + fake_repo_provider.post_comment.assert_not_called() + + def test_send_message_fail(self, dbsession, mocker): + fake_repo_provider, _, context, message = self._setup_send_message_tests( + dbsession, mocker, None, bundle_analysis_commentid=None + ) + fake_repo_provider.post_comment.side_effect = TorngitClientError() + context.__dict__["commit_report"] = MagicMock( + name="fake_commit_report", external_id="some_UUID4" + ) + strategy = BundleAnalysisCommentMarkdownStrategy() + result = strategy.send_message(context, message) + assert result == NotificationResult( + notification_attempted=True, + notification_successful=False, + explanation="TorngitClientError", + ) + + +class TestUpgradeMessage: + def test_build_upgrade_message_cloud(self, dbsession, mocker): + head_commit, _ = get_commit_pair(dbsession) + context = BundleAnalysisPRCommentNotificationContext( + head_commit, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + context.should_use_upgrade_comment = True + context.pull = MagicMock( + name="fake_pull", + database_pull=PullFactory(), + provider_pull={"author": {"username": "PR_author_username"}}, + ) + mocker.patch( + "services.bundle_analysis.notify.messages.comment.requires_license", + return_value=False, + ) + mocker.patch( + "services.bundle_analysis.notify.messages.comment.get_members_url", + return_value="http://members_url", + ) + strategy = BundleAnalysisCommentMarkdownStrategy() + result = strategy.build_message(context) + assert result == dedent("""\ + The author of this PR, PR_author_username, is not an activated member of this organization on Codecov. + Please [activate this user](http://members_url) to display this PR comment. + Bundle data is still being uploaded to Codecov for purposes of overall calculations. + + Please don't hesitate to email us at support@codecov.io with any questions. + """) + + def test_build_upgrade_message_self_hosted(self, dbsession, mocker): + head_commit, _ = get_commit_pair(dbsession) + context = BundleAnalysisPRCommentNotificationContext( + head_commit, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + context.should_use_upgrade_comment = True + context.pull = MagicMock( + name="fake_pull", + database_pull=PullFactory(), + provider_pull={"author": {"username": "PR_author_username"}}, + ) + mocker.patch( + "services.bundle_analysis.notify.messages.comment.requires_license", + return_value=True, + ) + mocker.patch( + "services.bundle_analysis.notify.messages.comment.get_members_url", + return_value="http://members_url", + ) + strategy = BundleAnalysisCommentMarkdownStrategy() + result = strategy.build_message(context) + assert result == dedent("""\ + The author of this PR, PR_author_username, is not activated in your Codecov Self-Hosted installation. + Please [activate this user](http://members_url) to display this PR comment. + Bundle data is still being uploaded to your instance of Codecov for purposes of overall calculations. + + Please contact your Codecov On-Premises installation administrator with any questions. + """) diff --git a/apps/worker/services/bundle_analysis/notify/messages/tests/test_commit_status.py b/apps/worker/services/bundle_analysis/notify/messages/tests/test_commit_status.py new file mode 100644 index 0000000000..e1799b5a73 --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/messages/tests/test_commit_status.py @@ -0,0 +1,310 @@ +from unittest.mock import MagicMock + +import pytest +from mock import AsyncMock +from shared.config import PATCH_CENTRIC_DEFAULT_CONFIG +from shared.torngit.exceptions import TorngitClientError +from shared.typings.torngit import TorngitInstanceData +from shared.yaml import UserYaml + +from database.models.core import GITHUB_APP_INSTALLATION_DEFAULT_NAME +from database.tests.factories.core import PullFactory +from services.bundle_analysis.notify.conftest import ( + get_commit_pair, + get_enriched_pull_setting_up_mocks, + get_report_pair, + save_mock_bundle_analysis_report, +) +from services.bundle_analysis.notify.contexts.commit_status import ( + CommitStatusNotificationContext, + CommitStatusNotificationContextBuilder, +) +from services.bundle_analysis.notify.messages.commit_status import ( + CommitStatusMessageStrategy, +) +from services.notification.notifiers.base import NotificationResult +from services.seats import SeatActivationInfo, ShouldActivateSeat +from tests.helpers import mock_all_plans_and_tiers + + +class FakeRedis(object): + """ + This is a fake, very rudimentary redis implementation to ease the managing + of mocking `set`, `get`. + """ + + def __init__(self) -> None: + self.inner_dict = {} + + def set(self, key, ttl, value): + self.inner_dict[key] = value + + def get(self, key): + if key in self.inner_dict: + return self.inner_dict[key] + return None + + +@pytest.fixture +def mock_cache(mocker): + fake_cache = mocker.MagicMock(name="fake_cache") + fake_cache.get_backend.return_value = FakeRedis() + mocker.patch( + "services.bundle_analysis.notify.messages.commit_status.cache", fake_cache + ) + return fake_cache + + +class TestCommitStatusMessage: + @pytest.fixture(autouse=True) + def setup(self): + mock_all_plans_and_tiers() + + @pytest.mark.parametrize( + "user_config, expected", + [ + pytest.param( + PATCH_CENTRIC_DEFAULT_CONFIG, + "Bundle change: -48.89% (Threshold: 5.0%)", + id="default_config", + ), + pytest.param( + { + **PATCH_CENTRIC_DEFAULT_CONFIG, + "bundle_analysis": {"warning_threshold": 500000}, + }, + "Bundle change: -372.56kB (Threshold: 500.0kB)", + id="success_absolute_threshold", + ), + pytest.param( + { + **PATCH_CENTRIC_DEFAULT_CONFIG, + "bundle_analysis": {"warning_threshold": 300000}, + }, + "Bundle change: -372.56kB (Threshold: 300.0kB)", + id="warning_absolute_threshold", + ), + ], + ) + @pytest.mark.django_db + def test_build_message_from_samples_negative_changes( + self, user_config, expected, dbsession, mocker, mock_storage + ): + head_commit, base_commit = get_commit_pair(dbsession) + repository = head_commit.repository + head_commit_report, base_commit_report = get_report_pair( + dbsession, (head_commit, base_commit) + ) + save_mock_bundle_analysis_report( + repository, head_commit_report, mock_storage, sample_report_number=2 + ) + save_mock_bundle_analysis_report( + repository, base_commit_report, mock_storage, sample_report_number=1 + ) + enriched_pull = get_enriched_pull_setting_up_mocks( + dbsession, mocker, (head_commit, base_commit) + ) + user_yaml = UserYaml.from_dict(user_config) + builder = CommitStatusNotificationContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + + context = builder.build_context().get_result() + message = CommitStatusMessageStrategy().build_message(context) + assert message == expected + + @pytest.mark.parametrize( + "user_config, expected", + [ + pytest.param( + PATCH_CENTRIC_DEFAULT_CONFIG, + "Passed with Warnings - Bundle change: 95.64% (Threshold: 5.0%)", + id="default_config", + ), + pytest.param( + { + **PATCH_CENTRIC_DEFAULT_CONFIG, + "bundle_analysis": {"warning_threshold": 500000}, + }, + "Bundle change: 372.56kB (Threshold: 500.0kB)", + id="success_absolute_threshold", + ), + pytest.param( + { + **PATCH_CENTRIC_DEFAULT_CONFIG, + "bundle_analysis": {"warning_threshold": 300000}, + }, + "Passed with Warnings - Bundle change: 372.56kB (Threshold: 300.0kB)", + id="warning_absolute_threshold", + ), + ], + ) + @pytest.mark.django_db + def test_build_message_from_samples( + self, user_config, expected, dbsession, mocker, mock_storage + ): + head_commit, base_commit = get_commit_pair(dbsession) + repository = head_commit.repository + head_commit_report, base_commit_report = get_report_pair( + dbsession, (head_commit, base_commit) + ) + save_mock_bundle_analysis_report( + repository, head_commit_report, mock_storage, sample_report_number=1 + ) + save_mock_bundle_analysis_report( + repository, base_commit_report, mock_storage, sample_report_number=2 + ) + enriched_pull = get_enriched_pull_setting_up_mocks( + dbsession, mocker, (head_commit, base_commit) + ) + user_yaml = UserYaml.from_dict(user_config) + builder = CommitStatusNotificationContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + + context = builder.build_context().get_result() + message = CommitStatusMessageStrategy().build_message(context) + assert message == expected + + @pytest.mark.django_db + def _setup_send_message_tests( + self, dbsession, mocker, torngit_ghapp_data, mock_storage + ): + fake_repo_provider = MagicMock( + name="fake_repo_provider", + data=TorngitInstanceData(installation=torngit_ghapp_data), + set_commit_status=AsyncMock(), + ) + fake_repo_provider.set_commit_status.return_value = {"id": 1000} + mocker.patch( + "services.bundle_analysis.notify.contexts.get_repo_provider_service", + return_value=fake_repo_provider, + ) + + head_commit, base_commit = get_commit_pair(dbsession) + head_commit.parent_commit_id = base_commit.commitid + dbsession.add_all([head_commit, base_commit]) + repository = head_commit.repository + head_commit_report, base_commit_report = get_report_pair( + dbsession, (head_commit, base_commit) + ) + dbsession.add_all([head_commit_report, base_commit_report]) + save_mock_bundle_analysis_report( + repository, head_commit_report, mock_storage, sample_report_number=1 + ) + save_mock_bundle_analysis_report( + repository, base_commit_report, mock_storage, sample_report_number=2 + ) + mocker.patch( + "services.bundle_analysis.notify.contexts.commit_status.fetch_and_update_pull_request_information_from_commit", + return_value=None, + ) + mocker.patch( + "services.bundle_analysis.notify.contexts.commit_status.determine_seat_activation", + return_value=SeatActivationInfo( + should_activate_seat=ShouldActivateSeat.NO_ACTIVATE + ), + ) + user_yaml = UserYaml.from_dict(PATCH_CENTRIC_DEFAULT_CONFIG) + builder = CommitStatusNotificationContextBuilder().initialize( + head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + return ( + fake_repo_provider, + builder.build_context().get_result(), + "Passed with Warnings - Bundle change: 95.64% (Threshold: 5.0%)", + ) + + @pytest.mark.django_db + @pytest.mark.parametrize( + "torngit_ghapp_data", + [ + pytest.param(None, id="no_app_used"), + pytest.param( + { + "installation_id": 123, + "id": 12, + "app_id": 12300, + "pem_path": "some_path", + }, + id="some_app_used", + ), + ], + ) + def test_send_message_success( + self, dbsession, mocker, torngit_ghapp_data, mock_storage, mock_cache + ): + fake_repo_provider, context, message = self._setup_send_message_tests( + dbsession, mocker, torngit_ghapp_data, mock_storage + ) + strategy = CommitStatusMessageStrategy() + result = strategy.send_message(context, message) + fake_repo_provider.set_commit_status.assert_called_with( + context.commit.commitid, + "success", + "codecov/bundles", + message, + context.commit_status_url, + ) + expected_app = torngit_ghapp_data.get("id") if torngit_ghapp_data else None + assert result == NotificationResult( + notification_attempted=True, + notification_successful=True, + github_app_used=expected_app, + ) + # Side effect of sending message is updating the cache + assert mock_cache.get_backend().get(strategy._cache_key(context)) == message + + @pytest.mark.django_db + def test_send_message_fail(self, dbsession, mocker, mock_storage): + fake_repo_provider, context, message = self._setup_send_message_tests( + dbsession, mocker, None, mock_storage + ) + fake_repo_provider.set_commit_status.side_effect = TorngitClientError() + strategy = CommitStatusMessageStrategy() + result = strategy.send_message(context, message) + assert result == NotificationResult( + notification_attempted=True, + notification_successful=False, + explanation="TorngitClientError", + ) + + @pytest.mark.django_db + def test_skip_payload_unchanged(self, dbsession, mocker, mock_storage, mock_cache): + fake_repo_provider, context, message = self._setup_send_message_tests( + dbsession, mocker, None, mock_storage + ) + strategy = CommitStatusMessageStrategy() + mock_cache.get_backend().set(strategy._cache_key(context), 600, message) + result = strategy.send_message(context, message) + fake_repo_provider.set_commit_status.assert_not_called() + assert result == NotificationResult( + notification_attempted=False, + notification_successful=False, + explanation="payload_unchanged", + ) + # Side effect of sending message is updating the cache + assert strategy.send_message(context, message) == NotificationResult( + notification_attempted=False, + notification_successful=False, + explanation="payload_unchanged", + ) + + +class TestCommitStatusUpgradeMessage: + def test_build_upgrade_message(self, dbsession): + head_commit, _ = get_commit_pair(dbsession) + context = CommitStatusNotificationContext( + head_commit, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + context.should_use_upgrade_comment = True + context.pull = MagicMock( + name="fake_pull", + database_pull=PullFactory(), + provider_pull={"author": {"username": "PR_author_username"}}, + ) + message = CommitStatusMessageStrategy().build_message(context) + assert ( + message + == "Please activate user PR_author_username to display a detailed status check" + ) diff --git a/apps/worker/services/bundle_analysis/notify/tests/__init__.py b/apps/worker/services/bundle_analysis/notify/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/bundle_analysis/notify/tests/samples/sample_1.sqlite b/apps/worker/services/bundle_analysis/notify/tests/samples/sample_1.sqlite new file mode 100644 index 0000000000..c67ee5d00c Binary files /dev/null and b/apps/worker/services/bundle_analysis/notify/tests/samples/sample_1.sqlite differ diff --git a/apps/worker/services/bundle_analysis/notify/tests/samples/sample_2.sqlite b/apps/worker/services/bundle_analysis/notify/tests/samples/sample_2.sqlite new file mode 100644 index 0000000000..a36d35b0fd Binary files /dev/null and b/apps/worker/services/bundle_analysis/notify/tests/samples/sample_2.sqlite differ diff --git a/apps/worker/services/bundle_analysis/notify/tests/test_helpers.py b/apps/worker/services/bundle_analysis/notify/tests/test_helpers.py new file mode 100644 index 0000000000..64a132f109 --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/tests/test_helpers.py @@ -0,0 +1,182 @@ +from unittest.mock import MagicMock + +import pytest +from shared.validation.types import BundleThreshold +from shared.yaml import UserYaml + +from database.models.core import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, + Owner, +) +from database.tests.factories.core import OwnerFactory +from services.bundle_analysis.notify.helpers import ( + bytes_readable, + get_github_app_used, + get_notification_types_configured, + is_bundle_comparison_change_within_configured_threshold, + to_BundleThreshold, +) +from services.bundle_analysis.notify.types import NotificationType + + +@pytest.mark.parametrize( + "input, expected", + [ + pytest.param(0, "0 bytes"), + pytest.param(123, "123 bytes"), + pytest.param(1000, "1.0kB"), + pytest.param(1500, "1.5kB"), + pytest.param(1000000, "1.0MB"), + pytest.param(1500010, "1.5MB"), + pytest.param(1e9, "1.0GB"), + pytest.param(1230000000, "1.23GB"), + ], +) +def test_bytes_readable(input, expected): + assert bytes_readable(input) == expected + + +@pytest.fixture +def github_owner_no_apps(dbsession) -> Owner: + owner = OwnerFactory(service="github") + dbsession.add(owner) + dbsession.commit() + assert owner.github_app_installations == [] + return owner + + +@pytest.fixture +def github_owner_with_apps(dbsession) -> Owner: + owner = OwnerFactory(service="github") + ghapp = GithubAppInstallation( + ownerid=owner.ownerid, + owner=owner, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + installation_id=1234, + ) + dbsession.add_all([owner, ghapp]) + dbsession.commit() + assert owner.github_app_installations == [ghapp] + return owner + + +@pytest.fixture +def gitlab_owner(dbsession) -> Owner: + owner = OwnerFactory(service="gitlab") + dbsession.add(owner) + dbsession.commit() + return owner + + +@pytest.mark.parametrize( + "config, owner_fixture, expected", + [ + pytest.param( + {"comment": False, "bundle_analysis": {"status": False}}, + "github_owner_no_apps", + (), + id="no_notification_configured", + ), + # The default site configuration puts the `comment` as a dict + pytest.param( + {"comment": {"require_bundle_changes": False}}, + "github_owner_no_apps", + (NotificationType.PR_COMMENT, NotificationType.COMMIT_STATUS), + id="default_values_github_no_apps", + ), + pytest.param( + {"comment": {"require_bundle_changes": False}}, + "github_owner_with_apps", + (NotificationType.PR_COMMENT, NotificationType.GITHUB_COMMIT_CHECK), + id="default_values_github_with_apps", + ), + pytest.param( + {"comment": {"require_bundle_changes": False}}, + "gitlab_owner", + (NotificationType.PR_COMMENT, NotificationType.COMMIT_STATUS), + id="default_values_gitlab", + ), + pytest.param( + {"comment": False, "bundle_analysis": {"status": True}}, + "gitlab_owner", + (NotificationType.COMMIT_STATUS,), + id="just_commit_status", + ), + pytest.param( + { + "comment": {"require_bundle_changes": False}, + "bundle_analysis": {"status": False}, + }, + "gitlab_owner", + (NotificationType.PR_COMMENT,), + id="just_pr_comment", + ), + ], +) +def test_get_configuration_types_configured(config, owner_fixture, expected, request): + owner = request.getfixturevalue(owner_fixture) + yaml = UserYaml.from_dict(config) + assert get_notification_types_configured(yaml, owner) == expected + + +@pytest.mark.parametrize( + "torngit, expected", + [ + pytest.param(None, None, id="no_torngit"), + pytest.param( + MagicMock(data={"installation": None}), None, id="torngit_no_installation" + ), + pytest.param( + MagicMock(data={"installation": {"id": 12}}), + 12, + id="torngit_with_installation", + ), + ], +) +def test_get_github_app_used(torngit, expected): + assert get_github_app_used(torngit) == expected + + +@pytest.mark.parametrize( + "value, expected", + [ + (100, BundleThreshold("absolute", 100)), + (0, BundleThreshold("absolute", 0)), + (14.5, BundleThreshold("percentage", 14.5)), + (["percentage", 14.5], BundleThreshold("percentage", 14.5)), + (["absolute", 1000], BundleThreshold("absolute", 1000)), + (BundleThreshold("absolute", 1000), BundleThreshold("absolute", 1000)), + (("absolute", 1000), BundleThreshold("absolute", 1000)), + ], +) +def test_to_BundleThreshold(value, expected): + assert to_BundleThreshold(value) == expected + + +@pytest.mark.parametrize("value", ["value", [1, 2, 3], None]) +def test_to_BundleThreshold_raises(value): + with pytest.raises(TypeError): + to_BundleThreshold(value) + + +@pytest.mark.parametrize( + "threshold, expected", + [ + (BundleThreshold("absolute", 10001), True), + (BundleThreshold("absolute", 10000), True), + (BundleThreshold("absolute", 9999), False), + (BundleThreshold("percentage", 13.0), True), + (BundleThreshold("percentage", 12.5), True), + (BundleThreshold("percentage", 12.0), False), + ], +) +def test_is_bundle_change_within_bundle_threshold(threshold, expected): + comparison = MagicMock( + name="fake_comparison", total_size_delta=10000, percentage_delta=12.5 + ) + assert comparison.total_size_delta == 10000 + assert ( + is_bundle_comparison_change_within_configured_threshold(comparison, threshold) + == expected + ) diff --git a/apps/worker/services/bundle_analysis/notify/tests/test_notify_service.py b/apps/worker/services/bundle_analysis/notify/tests/test_notify_service.py new file mode 100644 index 0000000000..6a1155e116 --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/tests/test_notify_service.py @@ -0,0 +1,311 @@ +from unittest.mock import MagicMock + +import pytest +from shared.config import PATCH_CENTRIC_DEFAULT_CONFIG +from shared.yaml import UserYaml + +from database.models.core import GITHUB_APP_INSTALLATION_DEFAULT_NAME +from database.tests.factories.core import CommitFactory +from services.bundle_analysis.notify import ( + BundleAnalysisNotifyReturn, + BundleAnalysisNotifyService, + NotificationSuccess, +) +from services.bundle_analysis.notify.conftest import ( + get_commit_pair, + get_enriched_pull_setting_up_mocks, + get_report_pair, + save_mock_bundle_analysis_report, +) +from services.bundle_analysis.notify.contexts import ( + BaseBundleAnalysisNotificationContext, + NotificationContextBuildError, +) +from services.bundle_analysis.notify.contexts.comment import ( + BundleAnalysisPRCommentNotificationContext, +) +from services.bundle_analysis.notify.messages.comment import ( + BundleAnalysisCommentMarkdownStrategy, +) +from services.bundle_analysis.notify.types import NotificationType +from services.notification.notifiers.base import NotificationResult +from tests.helpers import mock_all_plans_and_tiers + + +def override_comment_builder_and_message_strategy(mocker): + mock_comment_builder = MagicMock(name="fake_builder") + mock_comment_builder.get_result.return_value = "D. Context" + mock_comment_builder.build_context.return_value = mock_comment_builder + mock_comment_builder.initialize_from_context.return_value = mock_comment_builder + mock_comment_builder = mocker.patch( + "services.bundle_analysis.notify.BundleAnalysisPRCommentContextBuilder", + return_value=mock_comment_builder, + ) + mock_markdown_strategy = MagicMock(name="fake_markdown_strategy") + mock_markdown_strategy = mocker.patch( + "services.bundle_analysis.notify.BundleAnalysisCommentMarkdownStrategy", + return_value=mock_markdown_strategy, + ) + mock_comment_builder.return_value.get_result.return_value = MagicMock( + name="fake_context", notification_type=NotificationType.PR_COMMENT + ) + mock_markdown_strategy.build_message.return_value = "D. Message" + mock_markdown_strategy.send_message.return_value = NotificationResult( + notification_attempted=True, + notification_successful=True, + github_app_used=None, + ) + return (mock_comment_builder, mock_markdown_strategy) + + +def override_commit_status_builder_and_message_strategy(mocker): + mock_commit_status_builder = MagicMock(name="fake_commit_status_builder") + mock_commit_status_builder.get_result.return_value = "D. Context" + mock_commit_status_builder.build_context.return_value = mock_commit_status_builder + mock_commit_status_builder.initialize_from_context.return_value = ( + mock_commit_status_builder + ) + mock_commit_status_builder = mocker.patch( + "services.bundle_analysis.notify.CommitStatusNotificationContextBuilder", + return_value=mock_commit_status_builder, + ) + commit_status_message_strategy = MagicMock(name="fake_markdown_strategy") + commit_status_message_strategy = mocker.patch( + "services.bundle_analysis.notify.CommitStatusMessageStrategy", + return_value=commit_status_message_strategy, + ) + mock_commit_status_builder.return_value.get_result.return_value = MagicMock( + name="fake_context", notification_type=NotificationType.COMMIT_STATUS + ) + commit_status_message_strategy.build_message.return_value = "D. Message" + commit_status_message_strategy.send_message.return_value = NotificationResult( + notification_attempted=True, + notification_successful=True, + github_app_used=None, + ) + return (mock_commit_status_builder, commit_status_message_strategy) + + +@pytest.fixture +def mock_base_context(): + context_requirements = ( + CommitFactory(), + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + ) + context = BaseBundleAnalysisNotificationContext(*context_requirements) + context.commit_report = MagicMock(name="fake_CommitReport") + context.bundle_analysis_report = MagicMock(name="fake_BundleAnalysisReport") + return context + + +class TestCreateContextForNotification: + def test_build_base_context(self, mocker, dbsession, mock_storage): + head_commit, base_commit = get_commit_pair(dbsession) + head_commit_report, _ = get_report_pair(dbsession, (head_commit, base_commit)) + save_mock_bundle_analysis_report( + head_commit.repository, + head_commit_report, + mock_storage, + sample_report_number=1, + ) + service = BundleAnalysisNotifyService( + head_commit, UserYaml.from_dict(PATCH_CENTRIC_DEFAULT_CONFIG) + ) + base_context = service.build_base_context() + assert base_context.commit_report == head_commit_report + assert base_context.bundle_analysis_report.session_count() == 19 + + @pytest.mark.django_db + def test_create_context_success(self, dbsession, mock_storage, mocker): + mock_all_plans_and_tiers() + current_yaml = UserYaml.from_dict(PATCH_CENTRIC_DEFAULT_CONFIG) + head_commit, base_commit = get_commit_pair(dbsession) + head_commit_report, base_commit_report = get_report_pair( + dbsession, (head_commit, base_commit) + ) + save_mock_bundle_analysis_report( + head_commit.repository, + head_commit_report, + mock_storage, + sample_report_number=1, + ) + save_mock_bundle_analysis_report( + head_commit.repository, + base_commit_report, + mock_storage, + sample_report_number=2, + ) + enriched_pull = get_enriched_pull_setting_up_mocks( + dbsession, mocker, (head_commit, base_commit) + ) + service = BundleAnalysisNotifyService(head_commit, current_yaml) + result = service.create_context_for_notification( + BaseBundleAnalysisNotificationContext( + head_commit, GITHUB_APP_INSTALLATION_DEFAULT_NAME + ), + NotificationType.PR_COMMENT, + ) + assert result is not None + assert isinstance( + result.notification_context, BundleAnalysisPRCommentNotificationContext + ) + assert isinstance( + result.message_strategy, BundleAnalysisCommentMarkdownStrategy + ) + context = result.notification_context + assert context.commit_report == head_commit_report + assert context.bundle_analysis_report.session_count() == 19 + assert context.pull == enriched_pull + assert ( + context.bundle_analysis_comparison.base_report_key + == base_commit_report.external_id + ) + assert ( + context.bundle_analysis_comparison.head_report_key + == head_commit_report.external_id + ) + + def test_create_contexts_unknown_notification(self, mock_base_context): + current_yaml = UserYaml.from_dict({}) + service = BundleAnalysisNotifyService(mock_base_context.commit, current_yaml) + assert ( + service.create_context_for_notification( + mock_base_context, "unknown_notification_type" + ) + is None + ) + + def test_create_context_for_notification_build_fails( + self, mocker, mock_base_context + ): + mock_comment_builder = MagicMock(name="fake_builder") + mock_comment_builder.initialize_from_context.return_value = mock_comment_builder + mock_comment_builder.build_context.side_effect = NotificationContextBuildError( + "mock_failed_step" + ) + current_yaml = UserYaml.from_dict({}) + mock_comment_builder = mocker.patch( + "services.bundle_analysis.notify.BundleAnalysisPRCommentContextBuilder", + return_value=mock_comment_builder, + ) + service = BundleAnalysisNotifyService(mock_base_context.commit, current_yaml) + assert ( + service.create_context_for_notification( + mock_base_context, NotificationType.PR_COMMENT + ) + is None + ) + + +class TestBundleAnalysisNotifyService: + def test_skip_all_notification_base_context_failed( + self, mocker, dbsession, mock_storage, caplog + ): + head_commit, _ = get_commit_pair(dbsession) + service = BundleAnalysisNotifyService( + head_commit, + UserYaml.from_dict({"comment": {"require_bundle_changes": False}}), + ) + result = service.notify() + warning_logs = [ + record for record in caplog.records if record.levelname == "WARNING" + ] + assert any( + warning.message == "Failed to build NotificationContext" + for warning in warning_logs + ) + assert any( + warning.message + == "Skipping ALL notifications because there's no base context" + for warning in warning_logs + ) + assert result == BundleAnalysisNotifyReturn( + notifications_configured=( + NotificationType.PR_COMMENT, + NotificationType.COMMIT_STATUS, + ), + notifications_attempted=tuple(), + notifications_successful=tuple(), + ) + + @pytest.mark.parametrize( + "current_yaml, expected_configured_count, expected_success_count", + [ + pytest.param( + { + "comment": {"require_bundle_changes": False}, + "bundle_analysis": {"status": "informational"}, + }, + 2, + 2, + id="comment_and_status", + ), + pytest.param( + { + "comment": {"require_bundle_changes": False}, + "bundle_analysis": {"status": False}, + }, + 1, + 1, + id="only_comment_sent", + ), + pytest.param( + { + "comment": False, + }, + 1, + 1, + id="only_commit_status", + ), + ], + ) + def test_notify( + self, + current_yaml, + expected_configured_count, + expected_success_count, + mocker, + mock_base_context, + ): + override_comment_builder_and_message_strategy(mocker) + override_commit_status_builder_and_message_strategy(mocker) + + mocker.patch.object( + BundleAnalysisNotifyService, + "build_base_context", + return_value=mock_base_context, + ) + current_yaml = UserYaml.from_dict(current_yaml) + mock_base_context.current_yaml = current_yaml + service = BundleAnalysisNotifyService(mock_base_context.commit, current_yaml) + result = service.notify() + assert len(result.notifications_configured) == expected_configured_count + assert len(result.notifications_successful) == expected_success_count + + @pytest.mark.parametrize( + "result, success_value", + [ + ( + BundleAnalysisNotifyReturn([], [], []), + NotificationSuccess.NOTHING_TO_NOTIFY, + ), + ( + BundleAnalysisNotifyReturn( + [NotificationType.COMMIT_STATUS], + [NotificationType.COMMIT_STATUS], + [NotificationType.COMMIT_STATUS], + ), + NotificationSuccess.FULL_SUCCESS, + ), + ( + BundleAnalysisNotifyReturn( + [NotificationType.COMMIT_STATUS, NotificationType.PR_COMMENT], + [NotificationType.COMMIT_STATUS, NotificationType.PR_COMMENT], + [NotificationType.COMMIT_STATUS], + ), + NotificationSuccess.PARTIAL_SUCCESS, + ), + ], + ) + def test_to_NotificationSuccess(self, result, success_value): + assert result.to_NotificationSuccess() == success_value diff --git a/apps/worker/services/bundle_analysis/notify/types.py b/apps/worker/services/bundle_analysis/notify/types.py new file mode 100644 index 0000000000..ac7de4ef12 --- /dev/null +++ b/apps/worker/services/bundle_analysis/notify/types.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass +from enum import Enum +from typing import Literal + +from shared.validation.types import BundleThreshold + + +class NotificationType(Enum): + PR_COMMENT = "pr_comment" + COMMIT_STATUS = "commit_status" + # See docs on the difference between COMMIT_STATUS and GITHUB_COMMIT_CHECK + # https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks#types-of-status-checks-on-github + GITHUB_COMMIT_CHECK = "github_commit_check" + + +class NotificationSuccess(Enum): + ALL_ERRORED = "all_processing_results_errored" + NOTHING_TO_NOTIFY = "nothing_to_notify" + FULL_SUCCESS = "full_success" + PARTIAL_SUCCESS = "partial_success" + + +@dataclass +class NotificationUserConfig: + warning_threshold: BundleThreshold + status_level: bool | Literal["informational"] + required_changes: bool | Literal["bundle_increase"] + required_changes_threshold: BundleThreshold diff --git a/apps/worker/services/bundle_analysis/report.py b/apps/worker/services/bundle_analysis/report.py new file mode 100644 index 0000000000..3766da7aa7 --- /dev/null +++ b/apps/worker/services/bundle_analysis/report.py @@ -0,0 +1,475 @@ +import logging +import os +import tempfile +from dataclasses import dataclass +from typing import Any, Dict, Optional + +import sentry_sdk +import shared.storage +from shared.bundle_analysis import BundleAnalysisReport, BundleAnalysisReportLoader +from shared.bundle_analysis.models import AssetType, MetadataKey +from shared.bundle_analysis.storage import get_bucket_name +from shared.django_apps.bundle_analysis.models import CacheConfig +from shared.django_apps.bundle_analysis.service.bundle_analysis import ( + BundleAnalysisCacheConfigService, +) +from shared.metrics import Counter +from shared.reports.enums import UploadState, UploadType +from shared.storage.exceptions import FileNotInStorageError, PutRequestRateLimitError +from shared.utils.sessions import SessionType +from sqlalchemy.dialects import postgresql +from sqlalchemy.orm import Session + +from database.enums import ReportType +from database.models.core import Commit +from database.models.reports import CommitReport, Upload, UploadError +from database.models.timeseries import Measurement, MeasurementName +from services.archive import ArchiveService +from services.report import BaseReportService +from services.timeseries import repository_datasets_query + +log = logging.getLogger(__name__) + + +BUNDLE_ANALYSIS_REPORT_PROCESSOR_COUNTER = Counter( + "bundle_analysis_report_processor_runs", + "Number of times a BA report processor was run and with what result", + [ + "result", + "plugin_name", + "error_type", + ], +) + + +@dataclass +class ProcessingError: + code: str + params: Dict[str, Any] + is_retryable: bool = False + + def as_dict(self): + return {"code": self.code, "params": self.params} + + +@dataclass +class ProcessingResult: + upload: Upload + commit: Commit + bundle_report: Optional[BundleAnalysisReport] = None + previous_bundle_report: Optional[BundleAnalysisReport] = None + session_id: Optional[int] = None + bundle_name: Optional[str] = None + error: Optional[ProcessingError] = None + + def as_dict(self): + return { + "upload_id": self.upload.id_, + "session_id": self.session_id, + "bundle_name": self.bundle_name, + "error": self.error.as_dict() if self.error else None, + } + + def update_upload(self, carriedforward: Optional[bool] = False) -> None: + """ + Updates this result's `Upload` record with information from + this result. + """ + db_session = self.upload.get_db_session() + + if self.error: + self.commit.state = "error" + self.upload.state = "error" + self.upload.state_id = UploadState.ERROR.db_id + + upload_error = UploadError( + upload_id=self.upload.id_, + error_code=self.error.code, + error_params=self.error.params, + ) + db_session.add(upload_error) + else: + assert self.bundle_report is not None + self.commit.state = "complete" + self.upload.state = "processed" + self.upload.state_id = UploadState.PROCESSED.db_id + self.upload.order_number = self.session_id + + if carriedforward: + self.upload.upload_type = SessionType.carriedforward.value + self.upload_type_id = UploadType.CARRIEDFORWARD.db_id + + BUNDLE_ANALYSIS_REPORT_PROCESSOR_COUNTER.labels( + result="upload_error" if self.error else "processed", + plugin_name="n/a", + error_type=self.error.code if self.error else "n/a", + ).inc() + db_session.flush() + + +class BundleAnalysisReportService(BaseReportService): + def initialize_and_save_report( + self, commit: Commit, report_code: str | None = None + ) -> CommitReport: + db_session = commit.get_db_session() + + commit_report = ( + db_session.query(CommitReport) + .filter_by( + commit_id=commit.id_, + code=report_code, + report_type=ReportType.BUNDLE_ANALYSIS.value, + ) + .first() + ) + if not commit_report: + commit_report = CommitReport( + commit_id=commit.id_, + code=report_code, + report_type=ReportType.BUNDLE_ANALYSIS.value, + ) + db_session.add(commit_report) + db_session.flush() + return commit_report + + def _get_parent_commit( + self, + db_session: Session, + head_commit: Commit, + head_bundle_report: Optional[BundleAnalysisReport], + ) -> Optional[Commit]: + """ + There's two ways to retrieve parent commit of the head commit (in order of priority): + 1. Get the commitSha from head commit bundle report (stored in Metadata during ingestion) + 2. Get the head commit.parent from the DB + """ + commitid = ( + head_bundle_report + and head_bundle_report.metadata().get(MetadataKey.COMPARE_SHA) + ) or head_commit.parent_commit_id + + return ( + db_session.query(Commit) + .filter_by( + commitid=commitid, + repository=head_commit.repository, + ) + .first() + ) + + @sentry_sdk.trace + def _previous_bundle_analysis_report( + self, + bundle_loader: BundleAnalysisReportLoader, + commit: Commit, + head_bundle_report: BundleAnalysisReport | None, + ) -> BundleAnalysisReport | None: + """ + Helper function to fetch the parent commit's BAR for the purpose of matching previous bundle's + Assets to the current one being parsed. + """ + db_session = commit.get_db_session() + + parent_commit = self._get_parent_commit( + db_session=db_session, + head_commit=commit, + head_bundle_report=head_bundle_report, + ) + if parent_commit is None: + return None + + parent_commit_report = ( + db_session.query(CommitReport) + .filter_by( + commit_id=parent_commit.id_, + report_type=ReportType.BUNDLE_ANALYSIS.value, + ) + .first() + ) + if parent_commit_report is None: + return None + + return bundle_loader.load(parent_commit_report.external_id) + + @sentry_sdk.trace + def _attempt_init_from_previous_report( + self, + commit: Commit, + bundle_loader: BundleAnalysisReportLoader, + ) -> BundleAnalysisReport: + """Attempts to carry over parent bundle analysis report if current commit doesn't have a report. + Fallback to creating a fresh bundle analysis report if there is no previous report to carry over. + """ + # load a new copy of the previous bundle report into temp file + bundle_report = self._previous_bundle_analysis_report( + bundle_loader, commit, head_bundle_report=None + ) + if bundle_report: + # query which bundle names has caching turned on + bundles_to_be_cached = CacheConfig.objects.filter( + is_caching=True, + repo_id=commit.repoid, + ).values_list("bundle_name", flat=True) + + # For each bundle: + # if caching is on then update bundle.is_cached property to true + # if caching is off then delete that bundle from the report + update_fields = {} + for bundle in bundle_report.bundle_reports(): + if bundle.name in bundles_to_be_cached: + update_fields[bundle.name] = True + else: + bundle_report.delete_bundle_by_name(bundle.name) + if update_fields: + bundle_report.update_is_cached(update_fields) + return bundle_report + # fallback to create a fresh bundle analysis report if there is no previous report to carry over + return BundleAnalysisReport() + + @sentry_sdk.trace + def process_upload( + self, commit: Commit, upload: Upload, compare_sha: str | None = None + ) -> ProcessingResult: + """ + Download and parse the data associated with the given upload and + merge the results into a bundle report. + """ + commit_report: CommitReport = upload.report + repo_hash = ArchiveService.get_archive_hash(commit_report.commit.repository) + storage_service = shared.storage.get_appropriate_storage_service(commit.repoid) + bundle_loader = BundleAnalysisReportLoader(storage_service, repo_hash) + + # fetch existing bundle report from storage + bundle_report = bundle_loader.load(commit_report.external_id) + if bundle_report is None: + bundle_report = self._attempt_init_from_previous_report( + commit, bundle_loader + ) + + # download raw upload data to local tempfile + _, local_path = tempfile.mkstemp() + try: + session_id, prev_bar, bundle_name = None, None, None + if upload.storage_path != "": + with open(local_path, "wb") as f: + storage_service.read_file( + get_bucket_name(), upload.storage_path, file_obj=f + ) + + # load the downloaded data into the bundle report + session_id, bundle_name = bundle_report.ingest(local_path, compare_sha) + + # Retrieve previous commit's BAR and associate past Assets + prev_bar = self._previous_bundle_analysis_report( + bundle_loader, commit, head_bundle_report=bundle_report + ) + if prev_bar: + bundle_report.associate_previous_assets(prev_bar) + + # Turn on caching option by default for all new bundles only for default branch + if commit.branch == commit.repository.branch: + for bundle in bundle_report.bundle_reports(): + BundleAnalysisCacheConfigService.create_if_not_exists( + commit.repoid, bundle.name + ) + + # save the bundle report back to storage + bundle_loader.save(bundle_report, commit_report.external_id) + except FileNotInStorageError as e: + BUNDLE_ANALYSIS_REPORT_PROCESSOR_COUNTER.labels( + result="file_not_in_storage", + plugin_name="n/a", + error_type=type(e).__name__, + ).inc() + return ProcessingResult( + upload=upload, + commit=commit, + error=ProcessingError( + code="file_not_in_storage", + params={"location": upload.storage_path}, + is_retryable=True, + ), + ) + except PutRequestRateLimitError as e: + plugin_name = getattr(e, "bundle_analysis_plugin_name", "unknown") + BUNDLE_ANALYSIS_REPORT_PROCESSOR_COUNTER.labels( + result="rate_limit_error", + plugin_name=plugin_name, + error_type=type(e).__name__, + ).inc() + return ProcessingResult( + upload=upload, + commit=commit, + error=ProcessingError( + code="rate_limit_error", + params={"location": upload.storage_path}, + is_retryable=True, + ), + ) + except Exception as e: + # Metrics to count number of parsing errors of bundle files by plugins + plugin_name = getattr(e, "bundle_analysis_plugin_name", "unknown") + error_type = type(e).__name__ + BUNDLE_ANALYSIS_REPORT_PROCESSOR_COUNTER.labels( + result="parser_error", + plugin_name=plugin_name, + error_type=error_type, + ).inc() + log.error( + "Unable to parse upload for bundle analysis", + exc_info=True, + extra=dict( + repoid=commit.repoid, + commit=commit.commitid, + error_type=error_type, + ), + ) + return ProcessingResult( + upload=upload, + commit=commit, + error=ProcessingError( + code="parser_error", + params={ + "location": upload.storage_path, + "plugin_name": plugin_name, + }, + is_retryable=False, + ), + ) + finally: + os.remove(local_path) + + return ProcessingResult( + upload=upload, + commit=commit, + bundle_report=bundle_report, + previous_bundle_report=prev_bar, + session_id=session_id, + bundle_name=bundle_name, + ) + + def _save_to_timeseries( + self, + db_session: Session, + commit: Commit, + name: str, + measurable_id: str, + value: float, + ): + command = postgresql.insert(Measurement.__table__).values( + name=name, + owner_id=commit.repository.ownerid, + repo_id=commit.repoid, + measurable_id=measurable_id, + branch=commit.branch, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + value=value, + ) + command = command.on_conflict_do_update( + index_elements=[ + Measurement.name, + Measurement.owner_id, + Measurement.repo_id, + Measurement.measurable_id, + Measurement.commit_sha, + Measurement.timestamp, + ], + set_=dict( + branch=command.excluded.branch, + value=command.excluded.value, + ), + ) + db_session.execute(command) + db_session.flush() + + @sentry_sdk.trace + def save_measurements( + self, commit: Commit, upload: Upload, bundle_name: str + ) -> ProcessingResult: + """ + Save timeseries measurements for this bundle analysis report + """ + try: + commit_report: CommitReport = upload.report + repo_hash = ArchiveService.get_archive_hash(commit_report.commit.repository) + storage_service = shared.storage.get_appropriate_storage_service( + commit.repoid + ) + bundle_loader = BundleAnalysisReportLoader(storage_service, repo_hash) + + # fetch existing bundle report from storage + bundle_analysis_report = bundle_loader.load(commit_report.external_id) + + dataset_names = [ + dataset.name for dataset in repository_datasets_query(commit.repository) + ] + + db_session = commit.get_db_session() + bundle_report = bundle_analysis_report.bundle_report(bundle_name) + if bundle_report: + # For overall bundle size + if MeasurementName.bundle_analysis_report_size.value in dataset_names: + self._save_to_timeseries( + db_session, + commit, + MeasurementName.bundle_analysis_report_size.value, + bundle_report.name, + bundle_report.total_size(), + ) + + # For individual javascript associated assets using UUID + if MeasurementName.bundle_analysis_asset_size.value in dataset_names: + for asset in bundle_report.asset_reports(): + if asset.asset_type == AssetType.JAVASCRIPT: + self._save_to_timeseries( + db_session, + commit, + MeasurementName.bundle_analysis_asset_size.value, + asset.uuid, + asset.size, + ) + + # For asset types sizes + asset_type_map = { + MeasurementName.bundle_analysis_font_size: AssetType.FONT, + MeasurementName.bundle_analysis_image_size: AssetType.IMAGE, + MeasurementName.bundle_analysis_stylesheet_size: AssetType.STYLESHEET, + MeasurementName.bundle_analysis_javascript_size: AssetType.JAVASCRIPT, + } + for measurement_name, asset_type in asset_type_map.items(): + if measurement_name.value in dataset_names: + total_size = 0 + for asset in bundle_report.asset_reports(): + if asset.asset_type == asset_type: + total_size += asset.size + self._save_to_timeseries( + db_session, + commit, + measurement_name.value, + bundle_report.name, + total_size, + ) + + return ProcessingResult( + upload=upload, + commit=commit, + ) + except Exception as e: + BUNDLE_ANALYSIS_REPORT_PROCESSOR_COUNTER.labels( + result="parser_error", + plugin_name="n/a", + error_type=type(e).__name__, + ).inc() + return ProcessingResult( + upload=upload, + commit=commit, + error=ProcessingError( + code="measurement_save_error", + params={ + "location": upload.storage_path, + "repository": commit.repository.repoid, + }, + is_retryable=False, + ), + ) diff --git a/apps/worker/services/bundle_analysis/tests/__init__.py b/apps/worker/services/bundle_analysis/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/bundle_analysis/tests/test_bundle_analysis.py b/apps/worker/services/bundle_analysis/tests/test_bundle_analysis.py new file mode 100644 index 0000000000..d98349c1f1 --- /dev/null +++ b/apps/worker/services/bundle_analysis/tests/test_bundle_analysis.py @@ -0,0 +1,1594 @@ +from textwrap import dedent +from typing import Dict, List +from unittest.mock import PropertyMock + +import pytest +from shared.bundle_analysis.comparison import ( + AssetChange, + AssetComparison, + BundleChange, + RouteChange, +) +from shared.bundle_analysis.models import AssetType +from shared.bundle_analysis.storage import get_bucket_name +from shared.config import PATCH_CENTRIC_DEFAULT_CONFIG +from shared.yaml import UserYaml + +from database.enums import ReportType +from database.models import CommitReport, MeasurementName +from database.tests.factories import CommitFactory, PullFactory, UploadFactory +from database.tests.factories.timeseries import DatasetFactory, Measurement +from services.archive import ArchiveService +from services.bundle_analysis.notify import ( + BundleAnalysisNotifyReturn, + BundleAnalysisNotifyService, +) +from services.bundle_analysis.notify.types import NotificationType +from services.bundle_analysis.report import ( + BundleAnalysisReportService, + ProcessingResult, +) +from services.repository import EnrichedPull +from services.urls import get_bundle_analysis_pull_url +from tests.helpers import mock_all_plans_and_tiers + + +class MockBundleReport: + def __init__(self, name): + self.name = name + + def total_size(self): + return 123456 + + def is_cached(self): + return self.name.startswith("cached") + + def asset_reports(self): + return [] + + +def hook_mock_repo_provider(mocker, mock_repo_provider): + USING_GET_REPO_PROVIDER = [ + "services.bundle_analysis.notify.contexts.get_repo_provider_service", + ] + for usage in USING_GET_REPO_PROVIDER: + mocker.patch( + usage, + return_value=mock_repo_provider, + ) + + +def hook_mock_pull(mocker, mock_pull): + USING_MOCK_PULL = [ + "services.bundle_analysis.notify.contexts.comment.fetch_and_update_pull_request_information_from_commit", + "services.bundle_analysis.notify.contexts.commit_status.fetch_and_update_pull_request_information_from_commit", + ] + for usage in USING_MOCK_PULL: + mocker.patch(usage, return_value=mock_pull) + + +@pytest.mark.asyncio +async def test_bundle_analysis_save_measurements_report_size( + dbsession, mocker, mock_storage +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + commit = CommitFactory() + dbsession.add(commit) + dbsession.commit() + + commit_report = CommitReport( + commit=commit, report_type=ReportType.BUNDLE_ANALYSIS.value + ) + dbsession.add(commit_report) + dbsession.commit() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.commit() + + dataset = DatasetFactory.create( + name=MeasurementName.bundle_analysis_report_size.value, + repository_id=commit.repository.repoid, + ) + dbsession.add(dataset) + dbsession.commit() + + class MockBundleReport: + def __init__(self, bundle_name, size): + self.bundle_name = bundle_name + self.size = size + + @property + def name(self): + return self.bundle_name + + def total_size(self): + return self.size + + class MockBundleAnalysisReport: + def bundle_report(self, bundle_name): + return MockBundleReport("BundleA", 1111) + + mocker.patch( + "shared.bundle_analysis.BundleAnalysisReportLoader.load", + return_value=MockBundleAnalysisReport(), + ) + + report_service = BundleAnalysisReportService(UserYaml.from_dict({})) + result: ProcessingResult = report_service.save_measurements( + commit, upload, "BundleA" + ) + + assert result.error is None + + measurements = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.bundle_analysis_report_size.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id="BundleA", + ) + .all() + ) + + assert len(measurements) == 1 + assert measurements[0].value == 1111 + + measurements = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.bundle_analysis_report_size.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id="BundleB", + ) + .all() + ) + + assert len(measurements) == 0 + + +@pytest.mark.asyncio +async def test_bundle_analysis_save_measurements_asset_size( + dbsession, mocker, mock_storage +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + commit = CommitFactory() + dbsession.add(commit) + dbsession.commit() + + commit_report = CommitReport( + commit=commit, report_type=ReportType.BUNDLE_ANALYSIS.value + ) + dbsession.add(commit_report) + dbsession.commit() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.commit() + + dataset = DatasetFactory.create( + name=MeasurementName.bundle_analysis_asset_size.value, + repository_id=commit.repository.repoid, + ) + dbsession.add(dataset) + dbsession.commit() + + class MockAssetReport: + def __init__(self, mock_uuid, mock_size, mock_type): + self.mock_uuid = mock_uuid + self.mock_size = mock_size + self.mock_type = mock_type + + @property + def uuid(self): + return self.mock_uuid + + @property + def size(self): + return self.mock_size + + @property + def asset_type(self): + return self.mock_type + + class MockBundleReport: + def __init__(self, bundle_name, size): + self.bundle_name = bundle_name + self.size = size + + @property + def name(self): + return self.bundle_name + + def total_size(self): + return self.size + + def asset_reports(self): + return [ + MockAssetReport("UUID1", 123, AssetType.JAVASCRIPT), + MockAssetReport("UUID2", 321, AssetType.JAVASCRIPT), + ] + + class MockBundleAnalysisReport: + def bundle_report(self, bundle_name): + return MockBundleReport("BundleA", 1111) + + mocker.patch( + "shared.bundle_analysis.BundleAnalysisReportLoader.load", + return_value=MockBundleAnalysisReport(), + ) + + report_service = BundleAnalysisReportService(UserYaml.from_dict({})) + result: ProcessingResult = report_service.save_measurements( + commit, upload, "BundleA" + ) + + assert result.error is None + + measurements = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.bundle_analysis_asset_size.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id="UUID1", + ) + .all() + ) + + assert len(measurements) == 1 + assert measurements[0].value == 123 + + measurements = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.bundle_analysis_asset_size.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id="UUID2", + ) + .all() + ) + + assert len(measurements) == 1 + assert measurements[0].value == 321 + + +@pytest.mark.asyncio +async def test_bundle_analysis_save_measurements_asset_type_sizes( + dbsession, mocker, mock_storage +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + commit = CommitFactory() + dbsession.add(commit) + dbsession.commit() + + commit_report = CommitReport( + commit=commit, report_type=ReportType.BUNDLE_ANALYSIS.value + ) + dbsession.add(commit_report) + dbsession.commit() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.commit() + + measurements_datasets = [ + MeasurementName.bundle_analysis_stylesheet_size, + MeasurementName.bundle_analysis_font_size, + MeasurementName.bundle_analysis_image_size, + MeasurementName.bundle_analysis_javascript_size, + ] + for measurement in measurements_datasets: + dataset = DatasetFactory.create( + name=measurement.value, + repository_id=commit.repository.repoid, + ) + dbsession.add(dataset) + dbsession.commit() + + class MockAssetReport: + def __init__(self, mock_uuid, mock_size, mock_type): + self.mock_uuid = mock_uuid + self.mock_size = mock_size + self.mock_type = mock_type + + @property + def uuid(self): + return self.mock_uuid + + @property + def size(self): + return self.mock_size + + @property + def asset_type(self): + return self.mock_type + + class MockBundleReport: + def __init__(self, bundle_name, size): + self.bundle_name = bundle_name + self.size = size + + @property + def name(self): + return self.bundle_name + + def total_size(self): + return self.size + + def asset_reports(self): + return [ + MockAssetReport("UUID1", 111, AssetType.JAVASCRIPT), + MockAssetReport("UUID2", 222, AssetType.FONT), + MockAssetReport("UUID3", 333, AssetType.IMAGE), + MockAssetReport("UUID4", 444, AssetType.STYLESHEET), + ] + + class MockBundleAnalysisReport: + def bundle_report(self, bundle_name): + return MockBundleReport("BundleA", 1111) + + mocker.patch( + "shared.bundle_analysis.BundleAnalysisReportLoader.load", + return_value=MockBundleAnalysisReport(), + ) + + report_service = BundleAnalysisReportService(UserYaml.from_dict({})) + result: ProcessingResult = report_service.save_measurements( + commit, upload, "BundleA" + ) + + assert result.error is None + + measurements = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.bundle_analysis_javascript_size.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id="BundleA", + ) + .all() + ) + + assert len(measurements) == 1 + assert measurements[0].value == 111 + + measurements = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.bundle_analysis_font_size.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id="BundleA", + ) + .all() + ) + + assert len(measurements) == 1 + assert measurements[0].value == 222 + + measurements = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.bundle_analysis_image_size.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id="BundleA", + ) + .all() + ) + + assert len(measurements) == 1 + assert measurements[0].value == 333 + + measurements = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.bundle_analysis_stylesheet_size.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id="BundleA", + ) + .all() + ) + + assert len(measurements) == 1 + assert measurements[0].value == 444 + + +@pytest.mark.asyncio +async def test_bundle_analysis_save_measurements_error(dbsession, mocker, mock_storage): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + commit = CommitFactory() + dbsession.add(commit) + dbsession.commit() + + commit_report = CommitReport( + commit=commit, report_type=ReportType.BUNDLE_ANALYSIS.value + ) + dbsession.add(commit_report) + dbsession.commit() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.commit() + + dataset = DatasetFactory.create( + name=MeasurementName.bundle_analysis_asset_size.value, + repository_id=commit.repository.repoid, + ) + dbsession.add(dataset) + dbsession.commit() + + mocker.patch( + "shared.bundle_analysis.BundleAnalysisReportLoader.load", + return_value=None, + ) + + report_service = BundleAnalysisReportService(UserYaml.from_dict({})) + result: ProcessingResult = report_service.save_measurements( + commit, upload, "BundleA" + ) + + assert result.error is not None + + +@pytest.mark.parametrize( + "bundle_changes, percent_change, user_config, expected_message", + [ + pytest.param( + [ + BundleChange( + bundle_name="added-bundle", + change_type=BundleChange.ChangeType.ADDED, + size_delta=12345, + percentage_delta=5.56, + ), + BundleChange( + bundle_name="changed-bundle", + change_type=BundleChange.ChangeType.CHANGED, + size_delta=3456, + percentage_delta=0.35, + ), + BundleChange( + bundle_name="removed-bundle", + change_type=BundleChange.ChangeType.REMOVED, + size_delta=-1234, + percentage_delta=-1.23, + ), + ], + 5.56, + { + **PATCH_CENTRIC_DEFAULT_CONFIG, + "bundle_analysis": { + "status": "informational", + "warning_threshold": ["percentage", 5.0], + }, + }, + dedent("""\ + ## [Bundle](URL) Report + + Changes will increase total bundle size by 14.57kB (5.56%) :arrow_up::warning:, exceeding the [configured](https://docs.codecov.com/docs/javascript-bundle-analysis#main-features) threshold of 5%. + + | Bundle name | Size | Change | + | ----------- | ---- | ------ | + | added-bundle | 123.46kB | 12.35kB (5.56%) :arrow_up::warning: | + | changed-bundle | 123.46kB | 3.46kB (0.35%) :arrow_up: | + | removed-bundle | (removed) | -1.23kB (-1.23%) :arrow_down: | + """), + id="comment_increase_size_warning", + ), + pytest.param( + [ + BundleChange( + bundle_name="added-bundle", + change_type=BundleChange.ChangeType.ADDED, + size_delta=12345, + percentage_delta=5.56, + ), + BundleChange( + bundle_name="changed-bundle", + change_type=BundleChange.ChangeType.CHANGED, + size_delta=3456, + percentage_delta=2.56, + ), + BundleChange( + bundle_name="removed-bundle", + change_type=BundleChange.ChangeType.REMOVED, + size_delta=-1234, + percentage_delta=-100.0, + ), + ], + 5.56, + { + **PATCH_CENTRIC_DEFAULT_CONFIG, + "bundle_analysis": { + "status": True, + "warning_threshold": ["absolute", 10000], + }, + }, + dedent("""\ + ## [Bundle](URL) Report + + :x: Check failed: changes will increase total bundle size by 14.57kB (5.56%) :arrow_up:, **exceeding** the [configured](https://docs.codecov.com/docs/javascript-bundle-analysis#main-features) threshold of 10.0kB. + + | Bundle name | Size | Change | + | ----------- | ---- | ------ | + | added-bundle | 123.46kB | 12.35kB (5.56%) :arrow_up::x: | + | changed-bundle | 123.46kB | 3.46kB (2.56%) :arrow_up: | + | removed-bundle | (removed) | -1.23kB (-100.0%) :arrow_down: | + """), + id="comment_increase_size_error", + ), + pytest.param( + [ + BundleChange( + bundle_name="added-bundle", + change_type=BundleChange.ChangeType.ADDED, + size_delta=12345, + percentage_delta=2.56, + ), + BundleChange( + bundle_name="cached-bundle", + change_type=BundleChange.ChangeType.CHANGED, + size_delta=3456, + percentage_delta=2.56, + ), + BundleChange( + bundle_name="removed-bundle", + change_type=BundleChange.ChangeType.REMOVED, + size_delta=-1234, + percentage_delta=2.56, + ), + ], + 3.46, + { + **PATCH_CENTRIC_DEFAULT_CONFIG, + "bundle_analysis": { + "status": "informational", + "warning_threshold": ["percentage", 5.0], + }, + }, + dedent("""\ + ## [Bundle](URL) Report + + Changes will increase total bundle size by 14.57kB (3.46%) :arrow_up:. This is within the [configured](https://docs.codecov.com/docs/javascript-bundle-analysis#main-features) threshold :white_check_mark: + +
Detailed changes + + | Bundle name | Size | Change | + | ----------- | ---- | ------ | + | added-bundle | 123.46kB | 12.35kB (2.56%) :arrow_up: | + | cached-bundle* | 123.46kB | 3.46kB (2.56%) :arrow_up: | + | removed-bundle | (removed) | -1.23kB (2.56%) :arrow_down: | + +
+ + ℹ️ *Bundle size includes cached data from a previous commit + + """), + id="comment_increase_size_cached_values", + ), + pytest.param( + [ + BundleChange( + bundle_name="test-bundle", + change_type=BundleChange.ChangeType.CHANGED, + size_delta=-3456, + percentage_delta=-2.56, + ), + ], + -0.52, + { + **PATCH_CENTRIC_DEFAULT_CONFIG, + "bundle_analysis": { + "status": "informational", + "warning_threshold": ["percentage", 5.0], + }, + }, + dedent("""\ + ## [Bundle](URL) Report + + Changes will decrease total bundle size by 3.46kB (-0.52%) :arrow_down:. This is within the [configured](https://docs.codecov.com/docs/javascript-bundle-analysis#main-features) threshold :white_check_mark: + +
Detailed changes + + | Bundle name | Size | Change | + | ----------- | ---- | ------ | + | test-bundle | 123.46kB | -3.46kB (-2.56%) :arrow_down: | + +
+ """), + id="comment_decrease_size", + ), + pytest.param( + [ + BundleChange( + bundle_name="test-bundle", + change_type=BundleChange.ChangeType.CHANGED, + size_delta=0, + percentage_delta=0.0, + ), + ], + 0, + { + **PATCH_CENTRIC_DEFAULT_CONFIG, + "bundle_analysis": { + "status": "informational", + "warning_threshold": ["percentage", 5.0], + }, + }, + dedent("""\ + ## [Bundle](URL) Report + + Bundle size has no change :white_check_mark: + + + """), + id="comment_no_change", + ), + pytest.param( + [ + BundleChange( + bundle_name="added-bundle", + change_type=BundleChange.ChangeType.ADDED, + size_delta=12345, + percentage_delta=5.56, + ), + BundleChange( + bundle_name="changed-bundle", + change_type=BundleChange.ChangeType.CHANGED, + size_delta=3456, + percentage_delta=0.35, + ), + BundleChange( + bundle_name="removed-bundle", + change_type=BundleChange.ChangeType.REMOVED, + size_delta=-1234, + percentage_delta=-1.23, + ), + ], + 5.56, + { + **PATCH_CENTRIC_DEFAULT_CONFIG, + "bundle_analysis": { + "status": "informational", + "warning_threshold": ["percentage", 5.0], + }, + }, + dedent("""\ + ## [Bundle](URL) Report + + Changes will increase total bundle size by 14.57kB (5.56%) :arrow_up::warning:, exceeding the [configured](https://docs.codecov.com/docs/javascript-bundle-analysis#main-features) threshold of 5%. + + | Bundle name | Size | Change | + | ----------- | ---- | ------ | + | added-bundle | 123.46kB | 12.35kB (5.56%) :arrow_up::warning: | + | changed-bundle | 123.46kB | 3.46kB (0.35%) :arrow_up: | + | removed-bundle | (removed) | -1.23kB (-1.23%) :arrow_down: | + """), + id="comparison_by_file_path", + ), + ], +) +@pytest.mark.django_db +def test_bundle_analysis_notify_bundle_summary( + bundle_changes: list[BundleChange], + percent_change: float, + user_config: dict, + expected_message: str, + dbsession, + mocker, + mock_storage, + mock_repo_provider, +): + mock_all_plans_and_tiers() + hook_mock_repo_provider(mocker, mock_repo_provider) + base_commit = CommitFactory() + dbsession.add(base_commit) + base_commit_report = CommitReport( + commit=base_commit, report_type=ReportType.BUNDLE_ANALYSIS.value + ) + dbsession.add(base_commit_report) + + head_commit = CommitFactory(repository=base_commit.repository) + dbsession.add(head_commit) + head_commit_report = CommitReport( + commit=head_commit, report_type=ReportType.BUNDLE_ANALYSIS.value + ) + dbsession.add(head_commit_report) + + pull = PullFactory( + repository=base_commit.repository, + head=head_commit.commitid, + base=base_commit.commitid, + compared_to=base_commit.commitid, + ) + dbsession.add(pull) + dbsession.commit() + + notifier = BundleAnalysisNotifyService(head_commit, UserYaml.from_dict(user_config)) + + repo_key = ArchiveService.get_archive_hash(base_commit.repository) + mock_storage.write_file( + get_bucket_name(), + f"v1/repos/{repo_key}/{base_commit_report.external_id}/bundle_report.sqlite", + "test-content", + ) + mock_storage.write_file( + get_bucket_name(), + f"v1/repos/{repo_key}/{head_commit_report.external_id}/bundle_report.sqlite", + "test-content", + ) + + mocker.patch("shared.bundle_analysis.report.BundleAnalysisReport._setup") + + mocker.patch( + "shared.bundle_analysis.comparison.BundleAnalysisComparison.bundle_changes", + return_value=bundle_changes, + ) + mocker.patch( + "shared.bundle_analysis.comparison.BundleAnalysisComparison.bundle_routes_changes", + return_value={}, + ) + mock_percentage = mocker.patch( + "shared.bundle_analysis.comparison.BundleAnalysisComparison.percentage_delta", + new_callable=PropertyMock, + ) + mock_percentage.return_value = percent_change + + mocker.patch( + "shared.bundle_analysis.report.BundleAnalysisReport.bundle_report", + side_effect=lambda name: MockBundleReport(name), + ) + + hook_mock_pull( + mocker, + EnrichedPull( + database_pull=pull, + provider_pull={}, + ), + ) + + mock_check_compare_sha = mocker.patch( + "shared.bundle_analysis.comparison.BundleAnalysisComparison._check_compare_sha" + ) + mock_check_compare_sha.return_value = None + + expected_message = expected_message.replace( + "URL", get_bundle_analysis_pull_url(pull=pull) + ) + + mock_repo_provider.post_comment.return_value = {"id": "test-comment-id"} + + success = notifier.notify() + assert success == BundleAnalysisNotifyReturn( + notifications_configured=( + NotificationType.PR_COMMENT, + NotificationType.COMMIT_STATUS, + ), + notifications_attempted=( + NotificationType.PR_COMMENT, + NotificationType.COMMIT_STATUS, + ), + notifications_successful=( + NotificationType.PR_COMMENT, + NotificationType.COMMIT_STATUS, + ), + ) + + assert pull.bundle_analysis_commentid is not None + mock_repo_provider.post_comment.assert_called_once_with( + pull.pullid, expected_message + ) + + success = notifier.notify() + assert success == BundleAnalysisNotifyReturn( + notifications_configured=( + NotificationType.PR_COMMENT, + NotificationType.COMMIT_STATUS, + ), + notifications_attempted=( + NotificationType.PR_COMMENT, + NotificationType.COMMIT_STATUS, + ), + notifications_successful=( + NotificationType.PR_COMMENT, + NotificationType.COMMIT_STATUS, + ), + ) + + assert pull.bundle_analysis_commentid is not None + mock_repo_provider.edit_comment.assert_called_once_with( + pull.pullid, "test-comment-id", expected_message + ) + + +class MockModuleReport: + def __init__(self, a, b): + self._name = a + self._size = b + + @property + def name(self) -> str: + return self._name + + @property + def size(self) -> int: + return self._size + + +class MockAssetComparison: + def __init__(self, a, b) -> None: + self.asset = a + self.modules = [MockModuleReport(item[0], item[1]) for item in b] + + def asset_change(self): + return self.asset + + def contributing_modules(self, pr_changed_files): + return self.modules + + +@pytest.mark.parametrize( + "bundle_changes, route_changes, asset_comparisons, expected_message", + [ + pytest.param( + # Bundle Changes + [], + # Route Changes + {}, + # Asset Comparisons + [], + # Expected message + dedent("""\ + ## [Bundle](URL) Report + + Bundle size has no change :white_check_mark: + + + """), + id="no_bundle_change_at_all", + ), + pytest.param( + # Bundle Changes + [ + BundleChange( + bundle_name="test-no-change", + change_type=BundleChange.ChangeType.CHANGED, + size_delta=0, + percentage_delta=0.0, + ), + BundleChange( + bundle_name="test-with-change", + change_type=BundleChange.ChangeType.CHANGED, + size_delta=100, + percentage_delta=1.0, + ), + ], + # Route Changes + { + "test-with-change": [], + }, + # Asset Comparisons + [ + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_base=1000, + size_head=1200, + size_delta=200, + asset_name="this-is-a-warning", + percentage_delta=20.0, + ), + [], + ), + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.ADDED, + size_base=0, + size_head=10000, + size_delta=10000, + asset_name="this-is-added", + percentage_delta=100.0, + ), + [], + ), + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.REMOVED, + size_base=20000, + size_head=0, + size_delta=-20000, + asset_name="this-is-removed", + percentage_delta=-100.0, + ), + [], + ), + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_base=100000, + size_head=101000, + size_delta=1000, + asset_name="this-is-a-small-change", + percentage_delta=1.0, + ), + [], + ), + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_base=5000, + size_head=5000, + size_delta=0, + asset_name="this-is-no-change", + percentage_delta=0.0, + ), + [], + ), + ], + dedent("""\ + ## [Bundle](URL) Report + + Changes will increase total bundle size by 100 bytes (5.56%) :arrow_up::warning:, exceeding the [configured](https://docs.codecov.com/docs/javascript-bundle-analysis#main-features) threshold of 5%. + + | Bundle name | Size | Change | + | ----------- | ---- | ------ | + | test-with-change | 123.46kB | 100 bytes (1.0%) :arrow_up: | + + ### Affected Assets, Files, and Routes: + +
+ view changes for bundle: test-with-change + + #### **Assets Changed:** + | Asset Name | Size Change | Total Size | Change (%) | + | ---------- | ----------- | ---------- | ---------- | + | ```this-is-a-warning``` | 200 bytes | 1.2kB | 20.0% :warning: | + | **```this-is-added```** _(New)_ | 10.0kB | 10.0kB | 100.0% :rocket: | + | ~~**```this-is-removed```**~~ _(Deleted)_ | -20.0kB | 0 bytes | -100.0% :wastebasket: | + | ```this-is-a-small-change``` | 1.0kB | 101.0kB | 1.0% | + + + + + + + + + + + + +
+ """), + id="bundle_with_assets_change_only", + ), + pytest.param( + # Bundle Changes + [ + BundleChange( + bundle_name="test-no-change", + change_type=BundleChange.ChangeType.CHANGED, + size_delta=0, + percentage_delta=0.0, + ), + BundleChange( + bundle_name="test-with-change", + change_type=BundleChange.ChangeType.CHANGED, + size_delta=100, + percentage_delta=1.0, + ), + ], + # Route Changes + { + "test-with-change": [], + }, + # Asset Comparisons + [ + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_base=1000, + size_head=1200, + size_delta=200, + asset_name="this-is-a-warning", + percentage_delta=20.0, + ), + [], + ), + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.ADDED, + size_base=0, + size_head=10000, + size_delta=10000, + asset_name="this-is-added", + percentage_delta=100.0, + ), + [("abc/def/ghi/file1.ts", 1000)], + ), + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.REMOVED, + size_base=20000, + size_head=0, + size_delta=-20000, + asset_name="this-is-removed", + percentage_delta=-100.0, + ), + [("abc/def/ghi/file1.ts", 1000), ("abc/def/ghi/file2.ts", 2000)], + ), + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_base=100000, + size_head=101000, + size_delta=1000, + asset_name="this-is-a-small-change", + percentage_delta=1.0, + ), + [ + ("abc/def/ghi/file1.ts", 1000), + ("abc/def/ghi/file2.ts", 2000), + ("abc/def/ghi/file3.ts", 3000), + ], + ), + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_base=5000, + size_head=5000, + size_delta=0, + asset_name="this-is-no-change", + percentage_delta=0.0, + ), + [ + ("abc/def/ghi/file1.ts", 1000), + ("abc/def/ghi/file2.ts", 2000), + ("abc/def/ghi/file3.ts", 3000), + ("abc/def/ghi/file4.ts", 4000), + ], + ), + ], + dedent("""\ + ## [Bundle](URL) Report + + Changes will increase total bundle size by 100 bytes (5.56%) :arrow_up::warning:, exceeding the [configured](https://docs.codecov.com/docs/javascript-bundle-analysis#main-features) threshold of 5%. + + | Bundle name | Size | Change | + | ----------- | ---- | ------ | + | test-with-change | 123.46kB | 100 bytes (1.0%) :arrow_up: | + + ### Affected Assets, Files, and Routes: + +
+ view changes for bundle: test-with-change + + #### **Assets Changed:** + | Asset Name | Size Change | Total Size | Change (%) | + | ---------- | ----------- | ---------- | ---------- | + | ```this-is-a-warning``` | 200 bytes | 1.2kB | 20.0% :warning: | + | **```this-is-added```** _(New)_ | 10.0kB | 10.0kB | 100.0% :rocket: | + | ~~**```this-is-removed```**~~ _(Deleted)_ | -20.0kB | 0 bytes | -100.0% :wastebasket: | + | ```this-is-a-small-change``` | 1.0kB | 101.0kB | 1.0% | + + + + + **Files in** **```this-is-added```**: + + - ```abc/def/ghi/file1.ts``` → Total Size: **1.0kB** + + + + **Files in** **```this-is-removed```**: + + - ```abc/def/ghi/file1.ts``` → Total Size: **1.0kB** + + - ```abc/def/ghi/file2.ts``` → Total Size: **2.0kB** + + + + **Files in** **```this-is-a-small-change```**: + + - ```abc/def/ghi/file1.ts``` → Total Size: **1.0kB** + + - ```abc/def/ghi/file2.ts``` → Total Size: **2.0kB** + + - ```abc/def/ghi/file3.ts``` → Total Size: **3.0kB** + + + + + +
+ """), + id="bundle_with_assets_change_and_module_list", + ), + pytest.param( + # Bundle Changes + [ + BundleChange( + bundle_name="test-no-change", + change_type=BundleChange.ChangeType.CHANGED, + size_delta=0, + percentage_delta=0.0, + ), + BundleChange( + bundle_name="test-with-change", + change_type=BundleChange.ChangeType.CHANGED, + size_delta=100, + percentage_delta=1.0, + ), + ], + # Route Changes + { + "test-with-change": [ + RouteChange( + route_name="/users", + change_type=RouteChange.ChangeType.ADDED, + size_delta=1000, + size_base=0, + size_head=1000, + percentage_delta=100, + ), + RouteChange( + route_name="/faq", + change_type=RouteChange.ChangeType.REMOVED, + size_delta=-5000, + size_base=5000, + size_head=0, + percentage_delta=-100.0, + ), + RouteChange( + route_name="/big-change", + change_type=RouteChange.ChangeType.CHANGED, + size_delta=10000, + size_base=20000, + size_head=30000, + percentage_delta=50.0, + ), + RouteChange( + route_name="/no-change", + change_type=RouteChange.ChangeType.CHANGED, + size_delta=0, + size_base=999999, + size_head=999999, + percentage_delta=0, + ), + RouteChange( + route_name="/small-change", + change_type=RouteChange.ChangeType.CHANGED, + size_delta=1000, + size_base=100000, + size_head=101000, + percentage_delta=1.0, + ), + ], + }, + # Asset Comparisons + [ + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_base=1000, + size_head=1200, + size_delta=200, + asset_name="this-is-a-warning", + percentage_delta=20.0, + ), + [], + ), + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.ADDED, + size_base=0, + size_head=10000, + size_delta=10000, + asset_name="this-is-added", + percentage_delta=100.0, + ), + [], + ), + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.REMOVED, + size_base=20000, + size_head=0, + size_delta=-20000, + asset_name="this-is-removed", + percentage_delta=-100.0, + ), + [], + ), + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_base=100000, + size_head=101000, + size_delta=1000, + asset_name="this-is-a-small-change", + percentage_delta=1.0, + ), + [], + ), + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_base=5000, + size_head=5000, + size_delta=0, + asset_name="this-is-no-change", + percentage_delta=0.0, + ), + [], + ), + ], + dedent("""\ + ## [Bundle](URL) Report + + Changes will increase total bundle size by 100 bytes (5.56%) :arrow_up::warning:, exceeding the [configured](https://docs.codecov.com/docs/javascript-bundle-analysis#main-features) threshold of 5%. + + | Bundle name | Size | Change | + | ----------- | ---- | ------ | + | test-with-change | 123.46kB | 100 bytes (1.0%) :arrow_up: | + + ### Affected Assets, Files, and Routes: + +
+ view changes for bundle: test-with-change + + #### **Assets Changed:** + | Asset Name | Size Change | Total Size | Change (%) | + | ---------- | ----------- | ---------- | ---------- | + | ```this-is-a-warning``` | 200 bytes | 1.2kB | 20.0% :warning: | + | **```this-is-added```** _(New)_ | 10.0kB | 10.0kB | 100.0% :rocket: | + | ~~**```this-is-removed```**~~ _(Deleted)_ | -20.0kB | 0 bytes | -100.0% :wastebasket: | + | ```this-is-a-small-change``` | 1.0kB | 101.0kB | 1.0% | + + + + + + + + + + + + #### App Routes Affected: + + | App Route | Size Change | Total Size | Change (%) | + | --------- | ----------- | ---------- | ---------- | + | **/users** _(New)_ | 1.0kB | 1.0kB | 100% :rocket: | + | ~~**/faq**~~ _(Deleted)_ | -5.0kB | 0 bytes | -100.0% :wastebasket: | + | /big-change | 10.0kB | 30.0kB | 50.0% :warning: | + | /small-change | 1.0kB | 101.0kB | 1.0% | + + +
+ """), + id="bundle_with_assets_change_and_routes", + ), + pytest.param( + # Bundle Changes + [ + BundleChange( + bundle_name="test-no-change", + change_type=BundleChange.ChangeType.CHANGED, + size_delta=0, + percentage_delta=0.0, + ), + BundleChange( + bundle_name="test-with-change", + change_type=BundleChange.ChangeType.CHANGED, + size_delta=100, + percentage_delta=1.0, + ), + ], + # Route Changes + { + "test-with-change": [ + RouteChange( + route_name="/users", + change_type=RouteChange.ChangeType.ADDED, + size_delta=1000, + size_base=0, + size_head=1000, + percentage_delta=100, + ), + RouteChange( + route_name="/faq", + change_type=RouteChange.ChangeType.REMOVED, + size_delta=-5000, + size_base=5000, + size_head=0, + percentage_delta=-100.0, + ), + RouteChange( + route_name="/big-change", + change_type=RouteChange.ChangeType.CHANGED, + size_delta=10000, + size_base=20000, + size_head=30000, + percentage_delta=50.0, + ), + RouteChange( + route_name="/no-change", + change_type=RouteChange.ChangeType.CHANGED, + size_delta=0, + size_base=999999, + size_head=999999, + percentage_delta=0, + ), + RouteChange( + route_name="/small-change", + change_type=RouteChange.ChangeType.CHANGED, + size_delta=1000, + size_base=100000, + size_head=101000, + percentage_delta=1.0, + ), + ], + }, + # Asset Comparisons + [ + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_base=1000, + size_head=1200, + size_delta=200, + asset_name="this-is-a-warning", + percentage_delta=20.0, + ), + [], + ), + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.ADDED, + size_base=0, + size_head=10000, + size_delta=10000, + asset_name="this-is-added", + percentage_delta=100.0, + ), + [("abc/def/ghi/file1.ts", 1000)], + ), + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.REMOVED, + size_base=20000, + size_head=0, + size_delta=-20000, + asset_name="this-is-removed", + percentage_delta=-100.0, + ), + [("abc/def/ghi/file1.ts", 1000), ("abc/def/ghi/file2.ts", 2000)], + ), + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_base=100000, + size_head=101000, + size_delta=1000, + asset_name="this-is-a-small-change", + percentage_delta=1.0, + ), + [ + ("abc/def/ghi/file1.ts", 1000), + ("abc/def/ghi/file2.ts", 2000), + ("abc/def/ghi/file3.ts", 3000), + ], + ), + MockAssetComparison( + AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_base=5000, + size_head=5000, + size_delta=0, + asset_name="this-is-no-change", + percentage_delta=0.0, + ), + [ + ("abc/def/ghi/file1.ts", 1000), + ("abc/def/ghi/file2.ts", 2000), + ("abc/def/ghi/file3.ts", 3000), + ("abc/def/ghi/file4.ts", 4000), + ], + ), + ], + dedent("""\ + ## [Bundle](URL) Report + + Changes will increase total bundle size by 100 bytes (5.56%) :arrow_up::warning:, exceeding the [configured](https://docs.codecov.com/docs/javascript-bundle-analysis#main-features) threshold of 5%. + + | Bundle name | Size | Change | + | ----------- | ---- | ------ | + | test-with-change | 123.46kB | 100 bytes (1.0%) :arrow_up: | + + ### Affected Assets, Files, and Routes: + +
+ view changes for bundle: test-with-change + + #### **Assets Changed:** + | Asset Name | Size Change | Total Size | Change (%) | + | ---------- | ----------- | ---------- | ---------- | + | ```this-is-a-warning``` | 200 bytes | 1.2kB | 20.0% :warning: | + | **```this-is-added```** _(New)_ | 10.0kB | 10.0kB | 100.0% :rocket: | + | ~~**```this-is-removed```**~~ _(Deleted)_ | -20.0kB | 0 bytes | -100.0% :wastebasket: | + | ```this-is-a-small-change``` | 1.0kB | 101.0kB | 1.0% | + + + + + **Files in** **```this-is-added```**: + + - ```abc/def/ghi/file1.ts``` → Total Size: **1.0kB** + + + + **Files in** **```this-is-removed```**: + + - ```abc/def/ghi/file1.ts``` → Total Size: **1.0kB** + + - ```abc/def/ghi/file2.ts``` → Total Size: **2.0kB** + + + + **Files in** **```this-is-a-small-change```**: + + - ```abc/def/ghi/file1.ts``` → Total Size: **1.0kB** + + - ```abc/def/ghi/file2.ts``` → Total Size: **2.0kB** + + - ```abc/def/ghi/file3.ts``` → Total Size: **3.0kB** + + + + + #### App Routes Affected: + + | App Route | Size Change | Total Size | Change (%) | + | --------- | ----------- | ---------- | ---------- | + | **/users** _(New)_ | 1.0kB | 1.0kB | 100% :rocket: | + | ~~**/faq**~~ _(Deleted)_ | -5.0kB | 0 bytes | -100.0% :wastebasket: | + | /big-change | 10.0kB | 30.0kB | 50.0% :warning: | + | /small-change | 1.0kB | 101.0kB | 1.0% | + + +
+ """), + id="bundle_with_assets_change_and_module_list_and_routes", + ), + ], +) +@pytest.mark.django_db +def test_bundle_analysis_notify_individual_bundle_data( + bundle_changes: list[BundleChange], + route_changes: Dict[str, List[RouteChange]], + asset_comparisons: List[AssetComparison], + expected_message: str, + dbsession, + mocker, + mock_storage, + mock_repo_provider, +): + mock_all_plans_and_tiers() + percent_change = 5.56 + user_config = { + **PATCH_CENTRIC_DEFAULT_CONFIG, + "bundle_analysis": { + "status": "informational", + "warning_threshold": ["percentage", 5.0], + }, + } + hook_mock_repo_provider(mocker, mock_repo_provider) + base_commit = CommitFactory() + dbsession.add(base_commit) + base_commit_report = CommitReport( + commit=base_commit, report_type=ReportType.BUNDLE_ANALYSIS.value + ) + dbsession.add(base_commit_report) + + head_commit = CommitFactory(repository=base_commit.repository) + dbsession.add(head_commit) + head_commit_report = CommitReport( + commit=head_commit, report_type=ReportType.BUNDLE_ANALYSIS.value + ) + dbsession.add(head_commit_report) + + pull = PullFactory( + repository=base_commit.repository, + head=head_commit.commitid, + base=base_commit.commitid, + compared_to=base_commit.commitid, + ) + dbsession.add(pull) + dbsession.commit() + + notifier = BundleAnalysisNotifyService(head_commit, UserYaml.from_dict(user_config)) + + repo_key = ArchiveService.get_archive_hash(base_commit.repository) + mock_storage.write_file( + get_bucket_name(), + f"v1/repos/{repo_key}/{base_commit_report.external_id}/bundle_report.sqlite", + "test-content", + ) + mock_storage.write_file( + get_bucket_name(), + f"v1/repos/{repo_key}/{head_commit_report.external_id}/bundle_report.sqlite", + "test-content", + ) + + mocker.patch("shared.bundle_analysis.report.BundleAnalysisReport._setup") + + mocker.patch( + "shared.bundle_analysis.comparison.BundleAnalysisComparison.bundle_changes", + return_value=bundle_changes, + ) + mocker.patch( + "shared.bundle_analysis.comparison.BundleAnalysisComparison.bundle_routes_changes", + return_value=route_changes, + ) + mocker.patch( + "shared.bundle_analysis.comparison.BundleComparison.asset_comparisons", + return_value=asset_comparisons, + ) + mock_percentage = mocker.patch( + "shared.bundle_analysis.comparison.BundleAnalysisComparison.percentage_delta", + new_callable=PropertyMock, + ) + mock_percentage.return_value = percent_change + + mocker.patch( + "shared.bundle_analysis.report.BundleAnalysisReport.bundle_report", + side_effect=lambda name: MockBundleReport(name), + ) + + hook_mock_pull( + mocker, + EnrichedPull( + database_pull=pull, + provider_pull={}, + ), + ) + + mock_check_compare_sha = mocker.patch( + "shared.bundle_analysis.comparison.BundleAnalysisComparison._check_compare_sha" + ) + mock_check_compare_sha.return_value = None + + expected_message = expected_message.replace( + "URL", get_bundle_analysis_pull_url(pull=pull) + ) + + mock_repo_provider.post_comment.return_value = {"id": "test-comment-id"} + notifier.notify() + + mock_repo_provider.post_comment.assert_called_once_with( + pull.pullid, expected_message + ) diff --git a/apps/worker/services/cleanup/__init__.py b/apps/worker/services/cleanup/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/cleanup/cleanup.py b/apps/worker/services/cleanup/cleanup.py new file mode 100644 index 0000000000..546303b4b7 --- /dev/null +++ b/apps/worker/services/cleanup/cleanup.py @@ -0,0 +1,71 @@ +import contextlib +import logging + +from django.db.models.query import QuerySet + +from services.cleanup.models import MANUAL_CLEANUP +from services.cleanup.relations import build_relation_graph +from services.cleanup.utils import ( + CleanupContext, + CleanupResult, + CleanupSummary, + cleanup_context, +) + +log = logging.getLogger(__name__) + + +def run_cleanup( + query: QuerySet, + context: CleanupContext | None = None, +) -> CleanupSummary: + """ + Cleans up all the models and storage files reachable from the given `QuerySet`. + + This deletes all database models in topological sort order, and also removes + all the files in storage for any of the models in the relationship graph. + + Returns the number of models and files being cleaned up in total, and per-Model. + """ + models_to_cleanup = build_relation_graph(query) + + summary = {} + cleaned_models = 0 + cleaned_files = 0 + + cm = contextlib.nullcontext(context) if context else cleanup_context() + with cm as context: + for relation in models_to_cleanup: + model = relation.model + result = CleanupResult(0) + + for query in relation.querysets: + # This is needed so that the correct connection is chosen for the + # `_raw_delete` queries, as otherwise it might chose a readonly connection. + query._for_write = True + + manual_cleanup = MANUAL_CLEANUP.get(model) + if manual_cleanup is not None: + query_result = manual_cleanup(context, query) + else: + query_result = CleanupResult(query._raw_delete(query.db)) + + result.cleaned_models += query_result.cleaned_models + result.cleaned_files += query_result.cleaned_files + + if result.cleaned_models > 0 or result.cleaned_files > 0: + summary[model] = result + + log.info( + f"Finished cleaning up `{model.__name__}`", + extra={ + "cleaned_models": result.cleaned_models, + "cleaned_files": result.cleaned_files, + }, + ) + + cleaned_models += result.cleaned_models + cleaned_files += result.cleaned_files + + totals = CleanupResult(cleaned_models, cleaned_files) + return CleanupSummary(totals, summary) diff --git a/apps/worker/services/cleanup/models.py b/apps/worker/services/cleanup/models.py new file mode 100644 index 0000000000..dcbd9233c6 --- /dev/null +++ b/apps/worker/services/cleanup/models.py @@ -0,0 +1,257 @@ +import dataclasses +from collections import defaultdict +from collections.abc import Callable +from functools import partial + +import sentry_sdk +from django.db.models import Model, Q, QuerySet +from shared.bundle_analysis import StoragePaths +from shared.django_apps.compare.models import CommitComparison +from shared.django_apps.core.models import Commit, Pull, Repository +from shared.django_apps.reports.models import ( + CommitReport, + DailyTestRollup, + TestInstance, +) +from shared.django_apps.reports.models import ReportSession as Upload +from shared.django_apps.staticanalysis.models import StaticAnalysisSingleFileSnapshot +from shared.django_apps.timeseries.models import Dataset, Measurement +from shared.storage.exceptions import FileNotInStorageError +from shared.timeseries.helpers import is_timeseries_enabled +from shared.utils.sessions import SessionType + +from services.archive import ArchiveService, MinioEndpoints +from services.cleanup.relations import reverse_filter +from services.cleanup.utils import CleanupContext, CleanupResult + +MANUAL_QUERY_CHUNKSIZE = 1_000 +DELETE_FILES_BATCHSIZE = 50 + + +@sentry_sdk.trace +def cleanup_files_batched( + context: CleanupContext, buckets_paths: dict[str, list[str]] +) -> int: + def delete_file(bucket_path: tuple[str, str]) -> bool: + try: + return context.storage.delete_file(bucket_path[0], bucket_path[1]) + except FileNotInStorageError: + return False + except Exception as e: + sentry_sdk.capture_exception(e) + return False + + iter = ((bucket, path) for bucket, paths in buckets_paths.items() for path in paths) + results = context.threadpool.map(delete_file, iter) + return sum(results) + + +@sentry_sdk.trace +def cleanup_with_storage_field( + path_field: str, + context: CleanupContext, + query: QuerySet, +) -> CleanupResult: + cleaned_files = 0 + + # delete `None` `path_field`s right away + cleaned_models = query.filter(**{f"{path_field}__isnull": True})._raw_delete( + query.db + ) + + # delete all those files from storage in chunks + storage_query = query.filter(**{f"{path_field}__isnull": False}).values_list( + "pk", path_field + ) + while True: + storage_results = dict(storage_query[:MANUAL_QUERY_CHUNKSIZE]) + if len(storage_results) == 0: + break + + cleaned_files += cleanup_files_batched( + context, {context.default_bucket: list(storage_results.values())} + ) + # go through `query.object` here, to avoid some duplicated subqueries + cleaned_models += query.model.objects.filter( + pk__in=storage_results.keys() + )._raw_delete(query.db) + + return CleanupResult(cleaned_models, cleaned_files) + + +def cleanup_archivefield( + field_name: str, context: CleanupContext, query: QuerySet +) -> CleanupResult: + model_field_name = f"_{field_name}_storage_path" + + return cleanup_with_storage_field(model_field_name, context, query) + + +# This has all the `Repository` fields needed by `get_archive_hash` +@dataclasses.dataclass +class FakeRepository: + repoid: int + service: str + service_id: str + + +@sentry_sdk.trace +def cleanup_commitreport(context: CleanupContext, query: QuerySet) -> CleanupResult: + coverage_reports = query.values_list( + "pk", + "report_type", + "code", + "external_id", + "commit__commitid", + "commit__repository__repoid", + "commit__repository__author__service", + "commit__repository__service_id", + ) + + cleaned_models = 0 + cleaned_files = 0 + repo_hashes: dict[int, str] = {} + + while True: + reports = list(coverage_reports[:MANUAL_QUERY_CHUNKSIZE]) + if len(reports) == 0: + break + + buckets_paths: dict[str, list[str]] = defaultdict(list) + for ( + _pk, + report_type, + report_code, + external_id, + commit_sha, + repoid, + repo_service, + repo_service_id, + ) in reports: + if repoid not in repo_hashes: + fake_repo = FakeRepository( + repoid=repoid, service=repo_service, service_id=repo_service_id + ) + repo_hashes[repoid] = ArchiveService.get_archive_hash(fake_repo) + repo_hash = repo_hashes[repoid] + + # depending on the `report_type`, we have: + # - a `chunks` file for coverage + # - a `bundle_report.sqlite` for BA + if report_type == "bundle_analysis": + path = StoragePaths.bundle_report.path( + repo_key=repo_hash, report_key=external_id + ) + buckets_paths[context.bundleanalysis_bucket].append(path) + elif report_type == "test_results": + # TA has cached rollups, but those are based on `Branch` + pass + else: + chunks_file_name = report_code if report_code is not None else "chunks" + path = MinioEndpoints.chunks.get_path( + version="v4", + repo_hash=repo_hash, + commitid=commit_sha, + chunks_file_name=chunks_file_name, + ) + buckets_paths[context.default_bucket].append(path) + + cleaned_files += cleanup_files_batched(context, buckets_paths) + cleaned_models += query.model.objects.filter( + pk__in=(r[0] for r in reports) + )._raw_delete(query.db) + + return CleanupResult(cleaned_models, cleaned_files) + + +@sentry_sdk.trace +def cleanup_upload(context: CleanupContext, query: QuerySet) -> CleanupResult: + cleaned_files = 0 + + # delete `None` `storage_path`s or carryforwarded right away, + # as those duplicate their parents `storage_path`. + cleaned_models = query.filter( + Q(storage_path__isnull=True) | Q(upload_type=SessionType.carriedforward.value) + )._raw_delete(query.db) + + # delete all those files from storage in chunks + upload_query = query.filter(storage_path__isnull=False).values_list( + "pk", "report__report_type", "storage_path" + ) + while True: + uploads = list(upload_query[:MANUAL_QUERY_CHUNKSIZE]) + if len(uploads) == 0: + break + + buckets_paths: dict[str, list[str]] = defaultdict(list) + for _pk, report_type, storage_path in uploads: + if report_type == "bundle_analysis": + buckets_paths[context.bundleanalysis_bucket].append(storage_path) + else: + buckets_paths[context.default_bucket].append(storage_path) + + cleaned_files += cleanup_files_batched(context, buckets_paths) + cleaned_models += query.model.objects.filter( + pk__in=(u[0] for u in uploads) + )._raw_delete(query.db) + + return CleanupResult(cleaned_models, cleaned_files) + + +def cleanup_repository(context: CleanupContext, query: QuerySet) -> CleanupResult: + # The equivalent of `SET NULL`: + Repository.objects.filter(fork__in=query).update(fork=None) + + # Cleans up all the `timeseries` stuff: + if is_timeseries_enabled(): + by_owner: dict[int, list[int]] = defaultdict(list) + all_repo_ids: list[int] = [] + for owner_id, repo_id in query.values_list("author_id", "repoid"): + by_owner[owner_id].append(repo_id) + all_repo_ids.append(repo_id) + + datasets = Dataset.objects.filter(repository_id__in=all_repo_ids) + datasets._for_write = True + datasets._raw_delete(datasets.db) + for owner_id, repo_ids in by_owner.items(): + measurements = Measurement.objects.filter( + owner_id=owner_id, + repo_id__in=repo_ids, + ) + measurements._for_write = True + measurements._raw_delete(measurements.db) + + return CleanupResult(query._raw_delete(query.db)) + + +def unroll_subquery(context: CleanupContext, query: QuerySet) -> CleanupResult: + reversed_query = reverse_filter(query) + if not reversed_query: + return CleanupResult(query._raw_delete(query.db)) + field, subquery = reversed_query + + cleaned_models = 0 + for parent in subquery: + cleaned_models += query.model.objects.filter(**{field: parent.pk})._raw_delete( + query.db + ) + + return CleanupResult(cleaned_models) + + +# All the models that need custom python code for deletions so a bulk `DELETE` query does not work. +MANUAL_CLEANUP: dict[ + type[Model], Callable[[CleanupContext, QuerySet], CleanupResult] +] = { + Commit: partial(cleanup_archivefield, "report"), + Pull: partial(cleanup_archivefield, "flare"), + CommitReport: cleanup_commitreport, + Upload: cleanup_upload, + CommitComparison: partial(cleanup_with_storage_field, "report_storage_path"), + StaticAnalysisSingleFileSnapshot: partial( + cleanup_with_storage_field, "content_location" + ), + Repository: cleanup_repository, + TestInstance: unroll_subquery, + DailyTestRollup: unroll_subquery, +} diff --git a/apps/worker/services/cleanup/owner.py b/apps/worker/services/cleanup/owner.py new file mode 100644 index 0000000000..1620685e90 --- /dev/null +++ b/apps/worker/services/cleanup/owner.py @@ -0,0 +1,58 @@ +import logging + +from django.db import transaction +from django.db.models import Q +from shared.django_apps.codecov_auth.models import Owner, OwnerProfile +from shared.django_apps.core.models import Commit, Pull, Repository + +from services.cleanup.cleanup import run_cleanup +from services.cleanup.utils import CleanupSummary + +log = logging.getLogger(__name__) + +CLEAR_ARRAY_FIELDS = ["plan_activated_users", "organizations", "admins"] + + +def cleanup_owner(owner_id: int) -> CleanupSummary: + log.info("Started/Continuing Owner cleanup") + + clear_owner_references(owner_id) + owner_query = Owner.objects.filter(ownerid=owner_id) + summary = run_cleanup(owner_query) + + log.info("Owner cleanup finished", extra={"summary": summary}) + return summary + + +# TODO: maybe turn this into a `MANUAL_CLEANUP`? +def clear_owner_references(owner_id: int): + """ + This clears the `ownerid` from various DB arrays where it is being referenced. + """ + + OwnerProfile.objects.filter(default_org=owner_id).update(default_org=None) + Owner.objects.filter(bot=owner_id).update(bot=None) + Repository.objects.filter(bot=owner_id).update(bot=None) + Commit.objects.filter(author=owner_id).update(author=None) + Pull.objects.filter(author=owner_id).update(author=None) + + # This uses a transaction / `select_for_update` to ensure consistency when + # modifying these `ArrayField`s in python. + # I don’t think we have such consistency anyplace else in the codebase, so + # if this is causing lock contention issues, its also fair to avoid this. + with transaction.atomic(): + filter = Q() + for field in CLEAR_ARRAY_FIELDS: + filter = filter | Q(**{f"{field}__contains": [owner_id]}) + + owners_with_reference = Owner.objects.select_for_update().filter(filter) + for owner in owners_with_reference: + updated_fields = set() + for field in CLEAR_ARRAY_FIELDS: + array = getattr(owner, field) + if array: + updated_fields.add(field) + setattr(owner, field, [x for x in array if x != owner_id]) + + if updated_fields: + owner.save(update_fields=updated_fields) diff --git a/apps/worker/services/cleanup/regular.py b/apps/worker/services/cleanup/regular.py new file mode 100644 index 0000000000..37454c4220 --- /dev/null +++ b/apps/worker/services/cleanup/regular.py @@ -0,0 +1,35 @@ +import logging +import random + +from services.cleanup.cleanup import run_cleanup +from services.cleanup.utils import CleanupResult, CleanupSummary, cleanup_context + +log = logging.getLogger(__name__) + + +def run_regular_cleanup() -> CleanupSummary: + log.info("Starting regular cleanup job") + complete_summary = CleanupSummary(CleanupResult(0), summary={}) + + # Usage of these model was removed, and we should clean up all its data before dropping the table for good. + cleanups_to_run = [] + + # as we expect this job to have frequent retries, and cleanup to take a long time, + # lets shuffle the various cleanups so that each one of those makes a little progress. + random.shuffle(cleanups_to_run) + + with cleanup_context() as context: + for query in cleanups_to_run: + name = query.model.__name__ + log.info(f"Cleaning up `{name}`") + summary = run_cleanup(query, context=context) + log.info(f"Cleaned up `{name}`", extra={"summary": summary}) + complete_summary.add(summary) + + # TODO: + # - cleanup old `ReportSession`s (aka `Upload`s) + # - cleanup `Commit`s that are `deleted` + # - figure out a way how we can first mark, and then fully delete `Branch`es + + log.info("Regular cleanup finished") + return complete_summary diff --git a/apps/worker/services/cleanup/relations.py b/apps/worker/services/cleanup/relations.py new file mode 100644 index 0000000000..4e03a006aa --- /dev/null +++ b/apps/worker/services/cleanup/relations.py @@ -0,0 +1,219 @@ +import dataclasses +from collections import defaultdict +from graphlib import TopologicalSorter + +from django.db.models import Model +from django.db.models.expressions import Col, Expression +from django.db.models.fields import Field +from django.db.models.lookups import Exact, In +from django.db.models.query import QuerySet +from shared.django_apps.bundle_analysis.models import CacheConfig +from shared.django_apps.codecov_auth.models import Owner, OwnerProfile +from shared.django_apps.core.models import Commit, Pull, Repository +from shared.django_apps.reports.models import DailyTestRollup, TestInstance +from shared.django_apps.user_measurements.models import UserMeasurement + +# Relations referencing 0 through field 1 of model 2: +IGNORE_RELATIONS: set[tuple[type[Model], str, type[Model]]] = { + (Owner, "default_org", OwnerProfile), + (Owner, "bot", Owner), + (Owner, "bot", Repository), + (Owner, "author", Commit), + (Owner, "author", Pull), + (Repository, "forkid", Repository), +} + +# Relations which have no proper foreign key: +UNDOCUMENTED_RELATIONS: list[tuple[type[Model], str, type[Model]]] = [ + (Repository, "repoid", TestInstance), + (Repository, "repoid", DailyTestRollup), + (Commit, "commit_id", UserMeasurement), + (Owner, "owner_id", UserMeasurement), + (Repository, "repo_id", UserMeasurement), + (Repository, "repo_id", CacheConfig), + # TODO: `UserMeasurement` also has `upload_id`, should we register that as well? + # TODO: should we also include `SimpleMetric` here? +] + + +@dataclasses.dataclass +class Node: + edges: dict[type[Model], list[str]] = dataclasses.field( + default_factory=lambda: defaultdict(list) + ) + querysets: list[QuerySet] = dataclasses.field(default_factory=list) + depth: int = 9999 + + +@dataclasses.dataclass +class ModelQueries: + model: type[Model] + querysets: list[QuerySet] + + +def build_relation_graph(query: QuerySet) -> list[ModelQueries]: + """ + This takes as input a django `QuerySet`, like `Repository.objects.filter(repoid=123)`. + + It then walks the django relation graph, resolving all the models that have a relationship **to** the input model, + returning those models along with a `QuerySet` that allows either querying or deleting those models. + + The returned list is in topological sorting order, so related models are always sorted before models they depend on. + """ + nodes: dict[type[Model], Node] = defaultdict(Node) + graph: TopologicalSorter[type[Model]] = TopologicalSorter() + + def process_relation( + model: type[Model], related_model_field: str, related_model: type[Model] + ): + if (model, related_model_field, related_model) in IGNORE_RELATIONS: + return + + graph.add(model, related_model) + nodes[model].edges[related_model].append(related_model_field) + + if related_model not in nodes: + process_model(related_model) + + def process_model(model: type[Model]): + for ( + referenced_model, + related_model_field, + related_model, + ) in UNDOCUMENTED_RELATIONS: + if referenced_model == model: + process_relation(model, related_model_field, related_model) + + if not (meta := model._meta): + return + + for field in meta.get_fields(include_hidden=True): + if not field.is_relation: + continue + + if field.one_to_many or field.one_to_one: + # Most likely the reverse of a `ForeignKey` + # + + if not hasattr(field, "field"): + # I believe this is the actual *forward* definition of a `OneToOne` + continue + + # this should be the actual `ForeignKey` definition: + actual_field = field.field + if actual_field.model == model: + # this field goes from *this* model to another, but we are interested in the reverse actually + continue + + related_model = actual_field.model + related_model_field = actual_field.name + process_relation(model, related_model_field, related_model) + + elif field.many_to_many: + if not hasattr(field, "through"): + # we want to delete all related records on the join table + continue + + related_model = field.through + join_meta = related_model._meta + for field in join_meta.get_fields(include_hidden=True): + if not field.is_relation or field.model != model: + continue + + related_model_field = actual_field.name + process_relation(model, related_model_field, related_model) + + graph.add(query.model) + process_model(query.model) + + # the topological sort yields models in the order we want to run deletions + sorted_models = list(graph.static_order()) + + # but for actually building the querysets, we prefer the order from root to leafs + nodes[query.model].querysets = [query] + nodes[query.model].depth = 0 + for model in reversed(sorted_models): + node = nodes[model] + depth = node.depth + 1 + in_filters = [simplified_lookup(qs) for qs in node.querysets] + + for related_model, related_fields in node.edges.items(): + related_node = nodes[related_model] + + if depth < related_node.depth: + related_node.depth = depth + queries_to_build = ( + (field, in_filter) + for field in related_fields + for in_filter in in_filters + ) + related_node.querysets = [ + related_model.objects.filter(**{f"{field}__in": in_filter}) + for field, in_filter in queries_to_build + ] + + return [ModelQueries(model, nodes[model].querysets) for model in sorted_models] + + +def simplified_lookup(queryset: QuerySet) -> QuerySet | list[int]: + """ + This potentially simplifies simple primary key lookups. + + The whole point of the `build_relation_graph` is to begin with a `QuerySet`, + and most of the time, those are simple lookups by primary key. + + When chaining those to related objects, and we can detect that this is indeed + a simple primary key lookup, we can eliminate one level of subqueries by + returning the simplified lookup value. + + This is hopefully slightly faster, as the DB will still do an index scan for + a subquery like `foreign_pk IN (SELECT pk FROM table WHERE pk=123)`. + In that case, the expression will be simplified to `foreign_pk IN (123)`. + """ + if queryset.query.is_sliced: + return queryset + + where = queryset.query.where + if len(where.children) != 1: + return queryset + + condition = where.children[0] + if not isinstance(condition, Expression) or not isinstance(condition.lhs, Col): + return queryset + + column = condition.lhs.target + if column.model == queryset.model and column.primary_key: + if isinstance(condition, Exact) and condition.rhs_is_direct_value(): + return [condition.rhs] + + # In theory, this does not necessarily need to be a "direct value", + # but it can also be a subquery. But lets be conservative here. + if isinstance(condition, In) and condition.rhs_is_direct_value(): + return condition.rhs + + return queryset + + +def reverse_filter(queryset: QuerySet) -> None | tuple[str, QuerySet]: + if queryset.query.is_sliced: + return None + + where = queryset.query.where + if len(where.children) != 1: + return None + + condition = where.children[0] + if ( + not isinstance(condition, In) + or not isinstance(condition.lhs, Col) + or not isinstance(condition.lhs.target, Field) + or condition.rhs_is_direct_value() + ): + return None + + column = condition.lhs.target.name + + query = condition.rhs + model = query.model + + return (column, QuerySet(model=model, query=query)) diff --git a/apps/worker/services/cleanup/repository.py b/apps/worker/services/cleanup/repository.py new file mode 100644 index 0000000000..c73eef5323 --- /dev/null +++ b/apps/worker/services/cleanup/repository.py @@ -0,0 +1,102 @@ +import logging +from uuid import uuid4 + +from django.db import DatabaseError, IntegrityError, transaction +from shared.django_apps.codecov_auth.models import Owner +from shared.django_apps.core.models import Repository + +from services.cleanup.cleanup import run_cleanup +from services.cleanup.utils import CleanupResult, CleanupSummary + +log = logging.getLogger(__name__) + + +def cleanup_repo(repo_id: int) -> CleanupSummary: + try: + cleanup_started, owner_id = start_repo_cleanup(repo_id) + except Repository.DoesNotExist: + log.warning("Repository does not exist / was already cleaned up") + return CleanupSummary(CleanupResult(0), {}) + except (DatabaseError, IntegrityError) as _: + # `DatabaseError` means that the `SELECT FOR UPDATE NOWAIT` is currently locked. + # `IntegrityError` means that the `Owner.create` has a duplicated `service_id`. + log.warning("Cleanup could not be started because of conflicting job") + return CleanupSummary(CleanupResult(0), {}) + + if cleanup_started: + log.info("Started Repository cleanup") + else: + log.info("Continuing Repository cleanup") + + repo_query = Repository.objects.filter(repoid=repo_id) + summary = run_cleanup(repo_query) + + # Delete the created "Shadow Owner" using a `raw_delete`, as otherwise this + # would run some very slow and expensive `SET NULL` / `CASCADE` queries, + # which are unnecessary as this shadow owner does not have any relations + owner_query = Owner.objects.filter(ownerid=owner_id) + owner_query._for_write = True # Select the correct writable DB connection + owner_query._raw_delete(owner_query.db) + + log.info("Repository cleanup finished", extra={"summary": summary}) + return summary + + +def start_repo_cleanup(repo_id: int) -> tuple[bool, int]: + """ + Starts Repository deletion by marking the repository as `deleted`, and moving + it to a newly created "shadow Owner". + + This newly created `Owner` only has a valid `service` and `service_id`, + which are the only required non-NULL fields without defaults, and is otherwise + completely empty. + + The `ownerid` of this newly created owner is being returned along with a flag + indicating whether the repo cleanup was just started, or whether it is already + marked for deletion, and this function is being retried. + It is expected that repo cleanup is a slow process and might be done in more steps. + """ + # Runs in a transaction as we do not want to leave leftover shadow owners in + # case anything goes wrong here. + with transaction.atomic(): + ( + repo_deleted, + owner_id, + owner_name, + owner_username, + owner_service, + owner_service_id, + ) = ( + Repository.objects.select_for_update(nowait=True) + .values_list( + "deleted", + "author__ownerid", + "author__name", + "author__username", + "author__service", + "author__service_id", + ) + .get(repoid=repo_id) + ) + + if repo_deleted and not owner_name and not owner_username: + return (False, owner_id) + + # We mark the repository as "scheduled for deletion" by setting the `deleted` + # flag, moving it to a new shadow owner, and clearing some tokens. + shadow_owner = Owner.objects.create( + # `Owner` is unique across service/id, and both are non-NULL, + # so we cannot duplicate the values just like that, so lets change up the `service_id` + # a bit. We need the `Repository.service_id` for further `ArchiveService` deletions. + service=owner_service, + service_id=f"☠️{owner_service_id}☠️", + ) + new_token = uuid4().hex + Repository.objects.filter(repoid=repo_id).update( + deleted=True, + author=shadow_owner, + upload_token=new_token, + image_token=new_token, + ) + + return (True, shadow_owner.ownerid) diff --git a/apps/worker/services/cleanup/tests/__init__.py b/apps/worker/services/cleanup/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/cleanup/tests/snapshots/relations__builds_delete_queries__owner.txt b/apps/worker/services/cleanup/tests/snapshots/relations__builds_delete_queries__owner.txt new file mode 100644 index 0000000000..95d38267a2 --- /dev/null +++ b/apps/worker/services/cleanup/tests/snapshots/relations__builds_delete_queries__owner.txt @@ -0,0 +1,463 @@ +-- UserMeasurement +DELETE +FROM "user_measurements" +WHERE "user_measurements"."owner_id" IN (%s); + + +-- YamlHistory +DELETE +FROM "yaml_history" +WHERE "yaml_history"."ownerid" IN (%s); +DELETE +FROM "yaml_history" +WHERE "yaml_history"."author" IN (%s); + + +-- CommitNotification +DELETE +FROM "commit_notifications" +WHERE "commit_notifications"."gh_app_id" IN + (SELECT U0."id" + FROM "codecov_auth_githubappinstallation" U0 + WHERE U0."owner_id" IN (%s)); + + +-- OwnerInstallationNameToUseForTask +DELETE +FROM "codecov_auth_ownerinstallationnametousefortask" +WHERE "codecov_auth_ownerinstallationnametousefortask"."owner_id" IN (%s); + + +-- OrganizationLevelToken +DELETE +FROM "codecov_auth_organizationleveltoken" +WHERE "codecov_auth_organizationleveltoken"."ownerid" IN (%s); + + +-- OwnerProfile +DELETE +FROM "codecov_auth_ownerprofile" +WHERE "codecov_auth_ownerprofile"."owner_id" IN (%s); + + +-- Session +DELETE +FROM "sessions" +WHERE "sessions"."ownerid" IN (%s); + + +-- UserToken +DELETE +FROM "codecov_auth_usertoken" +WHERE "codecov_auth_usertoken"."ownerid" IN (%s); + + +-- TestInstance +DELETE +FROM "reports_testinstance" +WHERE "reports_testinstance"."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)); + + +-- DailyTestRollup +DELETE +FROM "reports_dailytestrollups" +WHERE "reports_dailytestrollups"."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)); + + +-- CacheConfig +DELETE +FROM "bundle_analysis_cacheconfig" +WHERE "bundle_analysis_cacheconfig"."repo_id" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)); + + +-- RepositoryToken +DELETE +FROM "codecov_auth_repositorytoken" +WHERE "codecov_auth_repositorytoken"."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)); + + +-- Branch +DELETE +FROM "branches" +WHERE "branches"."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)); + + +-- FlagComparison +DELETE +FROM "compare_flagcomparison" +WHERE "compare_flagcomparison"."repositoryflag_id" IN + (SELECT V0."id" + FROM "reports_repositoryflag" V0 + WHERE V0."repository_id" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s))); + + +-- ComponentComparison +DELETE +FROM "compare_componentcomparison" +WHERE "compare_componentcomparison"."commit_comparison_id" IN + (SELECT W0."id" + FROM "compare_commitcomparison" W0 + WHERE W0."base_commit_id" IN + (SELECT V0."id" + FROM "commits" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)))); +DELETE +FROM "compare_componentcomparison" +WHERE "compare_componentcomparison"."commit_comparison_id" IN + (SELECT W0."id" + FROM "compare_commitcomparison" W0 + WHERE W0."compare_commit_id" IN + (SELECT V0."id" + FROM "commits" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)))); + + +-- CommitError +DELETE +FROM "core_commiterror" +WHERE "core_commiterror"."commit_id" IN + (SELECT V0."id" + FROM "commits" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s))); + + +-- LabelAnalysisProcessingError +DELETE +FROM "labelanalysis_labelanalysisprocessingerror" +WHERE "labelanalysis_labelanalysisprocessingerror"."label_analysis_request_id" IN + (SELECT W0."id" + FROM "labelanalysis_labelanalysisrequest" W0 + WHERE W0."base_commit_id" IN + (SELECT V0."id" + FROM "commits" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)))); +DELETE +FROM "labelanalysis_labelanalysisprocessingerror" +WHERE "labelanalysis_labelanalysisprocessingerror"."label_analysis_request_id" IN + (SELECT W0."id" + FROM "labelanalysis_labelanalysisrequest" W0 + WHERE W0."head_commit_id" IN + (SELECT V0."id" + FROM "commits" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)))); + + +-- ReportResults +DELETE +FROM "reports_reportresults" +WHERE "reports_reportresults"."report_id" IN + (SELECT W0."id" + FROM "reports_commitreport" W0 + WHERE W0."commit_id" IN + (SELECT V0."id" + FROM "commits" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)))); + + +-- ReportLevelTotals +DELETE +FROM "reports_reportleveltotals" +WHERE "reports_reportleveltotals"."report_id" IN + (SELECT W0."id" + FROM "reports_commitreport" W0 + WHERE W0."commit_id" IN + (SELECT V0."id" + FROM "commits" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)))); + + +-- UploadError +DELETE +FROM "reports_uploaderror" +WHERE "reports_uploaderror"."upload_id" IN + (SELECT X0."id" + FROM "reports_upload" X0 + WHERE X0."report_id" IN + (SELECT W0."id" + FROM "reports_commitreport" W0 + WHERE W0."commit_id" IN + (SELECT V0."id" + FROM "commits" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s))))); + + +-- UploadFlagMembership +DELETE +FROM "reports_uploadflagmembership" +WHERE "reports_uploadflagmembership"."flag_id" IN + (SELECT V0."id" + FROM "reports_repositoryflag" V0 + WHERE V0."repository_id" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s))); + + +-- UploadLevelTotals +DELETE +FROM "reports_uploadleveltotals" +WHERE "reports_uploadleveltotals"."upload_id" IN + (SELECT X0."id" + FROM "reports_upload" X0 + WHERE X0."report_id" IN + (SELECT W0."id" + FROM "reports_commitreport" W0 + WHERE W0."commit_id" IN + (SELECT V0."id" + FROM "commits" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s))))); + + +-- TestResultReportTotals +DELETE +FROM "reports_testresultreporttotals" +WHERE "reports_testresultreporttotals"."report_id" IN + (SELECT W0."id" + FROM "reports_commitreport" W0 + WHERE W0."commit_id" IN + (SELECT V0."id" + FROM "commits" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)))); + + +-- StaticAnalysisSuiteFilepath +DELETE +FROM "staticanalysis_staticanalysissuitefilepath" +WHERE "staticanalysis_staticanalysissuitefilepath"."file_snapshot_id" IN + (SELECT V0."id" + FROM "staticanalysis_staticanalysissinglefilesnapshot" V0 + WHERE V0."repository_id" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s))); + + +-- Pull +DELETE +FROM "pulls" +WHERE "pulls"."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)); + + +-- TestFlagBridge +DELETE +FROM "reports_test_results_flag_bridge" +WHERE "reports_test_results_flag_bridge"."test_id" IN + (SELECT V0."id" + FROM "reports_test" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s))); + + +-- Flake +DELETE +FROM "reports_flake" +WHERE "reports_flake"."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)); + + +-- LastCacheRollupDate +DELETE +FROM "reports_lastrollupdate" +WHERE "reports_lastrollupdate"."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)); + + +-- GithubAppInstallation +DELETE +FROM "codecov_auth_githubappinstallation" +WHERE "codecov_auth_githubappinstallation"."owner_id" IN (%s); + + +-- CommitComparison +DELETE +FROM "compare_commitcomparison" +WHERE "compare_commitcomparison"."base_commit_id" IN + (SELECT V0."id" + FROM "commits" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s))); +DELETE +FROM "compare_commitcomparison" +WHERE "compare_commitcomparison"."compare_commit_id" IN + (SELECT V0."id" + FROM "commits" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s))); + + +-- LabelAnalysisRequest +DELETE +FROM "labelanalysis_labelanalysisrequest" +WHERE "labelanalysis_labelanalysisrequest"."base_commit_id" IN + (SELECT V0."id" + FROM "commits" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s))); +DELETE +FROM "labelanalysis_labelanalysisrequest" +WHERE "labelanalysis_labelanalysisrequest"."head_commit_id" IN + (SELECT V0."id" + FROM "commits" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s))); + + +-- ReportSession +DELETE +FROM "reports_upload" +WHERE "reports_upload"."report_id" IN + (SELECT W0."id" + FROM "reports_commitreport" W0 + WHERE W0."commit_id" IN + (SELECT V0."id" + FROM "commits" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)))); + + +-- StaticAnalysisSuite +DELETE +FROM "staticanalysis_staticanalysissuite" +WHERE "staticanalysis_staticanalysissuite"."commit_id" IN + (SELECT V0."id" + FROM "commits" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s))); + + +-- StaticAnalysisSingleFileSnapshot +DELETE +FROM "staticanalysis_staticanalysissinglefilesnapshot" +WHERE "staticanalysis_staticanalysissinglefilesnapshot"."repository_id" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)); + + +-- RepositoryFlag +DELETE +FROM "reports_repositoryflag" +WHERE "reports_repositoryflag"."repository_id" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)); + + +-- Test +DELETE +FROM "reports_test" +WHERE "reports_test"."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)); + + +-- ReducedError +DELETE +FROM "reports_reducederror" +WHERE "reports_reducederror"."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)); + + +-- CommitReport +DELETE +FROM "reports_commitreport" +WHERE "reports_commitreport"."commit_id" IN + (SELECT V0."id" + FROM "commits" V0 + WHERE V0."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s))); + + +-- Commit +DELETE +FROM "commits" +WHERE "commits"."repoid" IN + (SELECT U0."repoid" + FROM "repos" U0 + WHERE U0."ownerid" IN (%s)); + + +-- Repository +DELETE +FROM "repos" +WHERE "repos"."ownerid" IN (%s); + + +-- Owner +DELETE +FROM "owners" +WHERE "owners"."ownerid" = %s; diff --git a/apps/worker/services/cleanup/tests/snapshots/relations__builds_delete_queries__repository.txt b/apps/worker/services/cleanup/tests/snapshots/relations__builds_delete_queries__repository.txt new file mode 100644 index 0000000000..4ee2d7b146 --- /dev/null +++ b/apps/worker/services/cleanup/tests/snapshots/relations__builds_delete_queries__repository.txt @@ -0,0 +1,310 @@ +-- TestInstance +DELETE +FROM "reports_testinstance" +WHERE "reports_testinstance"."repoid" IN (%s); + + +-- DailyTestRollup +DELETE +FROM "reports_dailytestrollups" +WHERE "reports_dailytestrollups"."repoid" IN (%s); + + +-- UserMeasurement +DELETE +FROM "user_measurements" +WHERE "user_measurements"."repo_id" IN (%s); + + +-- CacheConfig +DELETE +FROM "bundle_analysis_cacheconfig" +WHERE "bundle_analysis_cacheconfig"."repo_id" IN (%s); + + +-- RepositoryToken +DELETE +FROM "codecov_auth_repositorytoken" +WHERE "codecov_auth_repositorytoken"."repoid" IN (%s); + + +-- Branch +DELETE +FROM "branches" +WHERE "branches"."repoid" IN (%s); + + +-- FlagComparison +DELETE +FROM "compare_flagcomparison" +WHERE "compare_flagcomparison"."repositoryflag_id" IN + (SELECT U0."id" + FROM "reports_repositoryflag" U0 + WHERE U0."repository_id" IN (%s)); + + +-- ComponentComparison +DELETE +FROM "compare_componentcomparison" +WHERE "compare_componentcomparison"."commit_comparison_id" IN + (SELECT V0."id" + FROM "compare_commitcomparison" V0 + WHERE V0."base_commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s))); +DELETE +FROM "compare_componentcomparison" +WHERE "compare_componentcomparison"."commit_comparison_id" IN + (SELECT V0."id" + FROM "compare_commitcomparison" V0 + WHERE V0."compare_commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s))); + + +-- CommitNotification +DELETE +FROM "commit_notifications" +WHERE "commit_notifications"."commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s)); + + +-- CommitError +DELETE +FROM "core_commiterror" +WHERE "core_commiterror"."commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s)); + + +-- LabelAnalysisProcessingError +DELETE +FROM "labelanalysis_labelanalysisprocessingerror" +WHERE "labelanalysis_labelanalysisprocessingerror"."label_analysis_request_id" IN + (SELECT V0."id" + FROM "labelanalysis_labelanalysisrequest" V0 + WHERE V0."base_commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s))); +DELETE +FROM "labelanalysis_labelanalysisprocessingerror" +WHERE "labelanalysis_labelanalysisprocessingerror"."label_analysis_request_id" IN + (SELECT V0."id" + FROM "labelanalysis_labelanalysisrequest" V0 + WHERE V0."head_commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s))); + + +-- ReportResults +DELETE +FROM "reports_reportresults" +WHERE "reports_reportresults"."report_id" IN + (SELECT V0."id" + FROM "reports_commitreport" V0 + WHERE V0."commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s))); + + +-- ReportLevelTotals +DELETE +FROM "reports_reportleveltotals" +WHERE "reports_reportleveltotals"."report_id" IN + (SELECT V0."id" + FROM "reports_commitreport" V0 + WHERE V0."commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s))); + + +-- UploadError +DELETE +FROM "reports_uploaderror" +WHERE "reports_uploaderror"."upload_id" IN + (SELECT W0."id" + FROM "reports_upload" W0 + WHERE W0."report_id" IN + (SELECT V0."id" + FROM "reports_commitreport" V0 + WHERE V0."commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s)))); + + +-- UploadFlagMembership +DELETE +FROM "reports_uploadflagmembership" +WHERE "reports_uploadflagmembership"."flag_id" IN + (SELECT U0."id" + FROM "reports_repositoryflag" U0 + WHERE U0."repository_id" IN (%s)); + + +-- UploadLevelTotals +DELETE +FROM "reports_uploadleveltotals" +WHERE "reports_uploadleveltotals"."upload_id" IN + (SELECT W0."id" + FROM "reports_upload" W0 + WHERE W0."report_id" IN + (SELECT V0."id" + FROM "reports_commitreport" V0 + WHERE V0."commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s)))); + + +-- TestResultReportTotals +DELETE +FROM "reports_testresultreporttotals" +WHERE "reports_testresultreporttotals"."report_id" IN + (SELECT V0."id" + FROM "reports_commitreport" V0 + WHERE V0."commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s))); + + +-- StaticAnalysisSuiteFilepath +DELETE +FROM "staticanalysis_staticanalysissuitefilepath" +WHERE "staticanalysis_staticanalysissuitefilepath"."file_snapshot_id" IN + (SELECT U0."id" + FROM "staticanalysis_staticanalysissinglefilesnapshot" U0 + WHERE U0."repository_id" IN (%s)); + + +-- Pull +DELETE +FROM "pulls" +WHERE "pulls"."repoid" IN (%s); + + +-- TestFlagBridge +DELETE +FROM "reports_test_results_flag_bridge" +WHERE "reports_test_results_flag_bridge"."test_id" IN + (SELECT U0."id" + FROM "reports_test" U0 + WHERE U0."repoid" IN (%s)); + + +-- Flake +DELETE +FROM "reports_flake" +WHERE "reports_flake"."repoid" IN (%s); + + +-- LastCacheRollupDate +DELETE +FROM "reports_lastrollupdate" +WHERE "reports_lastrollupdate"."repoid" IN (%s); + + +-- CommitComparison +DELETE +FROM "compare_commitcomparison" +WHERE "compare_commitcomparison"."base_commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s)); +DELETE +FROM "compare_commitcomparison" +WHERE "compare_commitcomparison"."compare_commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s)); + + +-- LabelAnalysisRequest +DELETE +FROM "labelanalysis_labelanalysisrequest" +WHERE "labelanalysis_labelanalysisrequest"."base_commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s)); +DELETE +FROM "labelanalysis_labelanalysisrequest" +WHERE "labelanalysis_labelanalysisrequest"."head_commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s)); + + +-- ReportSession +DELETE +FROM "reports_upload" +WHERE "reports_upload"."report_id" IN + (SELECT V0."id" + FROM "reports_commitreport" V0 + WHERE V0."commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s))); + + +-- StaticAnalysisSuite +DELETE +FROM "staticanalysis_staticanalysissuite" +WHERE "staticanalysis_staticanalysissuite"."commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s)); + + +-- StaticAnalysisSingleFileSnapshot +DELETE +FROM "staticanalysis_staticanalysissinglefilesnapshot" +WHERE "staticanalysis_staticanalysissinglefilesnapshot"."repository_id" IN (%s); + + +-- RepositoryFlag +DELETE +FROM "reports_repositoryflag" +WHERE "reports_repositoryflag"."repository_id" IN (%s); + + +-- Test +DELETE +FROM "reports_test" +WHERE "reports_test"."repoid" IN (%s); + + +-- ReducedError +DELETE +FROM "reports_reducederror" +WHERE "reports_reducederror"."repoid" IN (%s); + + +-- CommitReport +DELETE +FROM "reports_commitreport" +WHERE "reports_commitreport"."commit_id" IN + (SELECT U0."id" + FROM "commits" U0 + WHERE U0."repoid" IN (%s)); + + +-- Commit +DELETE +FROM "commits" +WHERE "commits"."repoid" IN (%s); + + +-- Repository +DELETE +FROM "repos" +WHERE "repos"."repoid" = %s; diff --git a/apps/worker/services/cleanup/tests/snapshots/relations__leaf_table__leaf.txt b/apps/worker/services/cleanup/tests/snapshots/relations__leaf_table__leaf.txt new file mode 100644 index 0000000000..2b323e9217 --- /dev/null +++ b/apps/worker/services/cleanup/tests/snapshots/relations__leaf_table__leaf.txt @@ -0,0 +1,3 @@ +-- UploadLevelTotals +DELETE +FROM "reports_uploadleveltotals"; diff --git a/apps/worker/services/cleanup/tests/test_regular_cleanup.py b/apps/worker/services/cleanup/tests/test_regular_cleanup.py new file mode 100644 index 0000000000..da52c30a26 --- /dev/null +++ b/apps/worker/services/cleanup/tests/test_regular_cleanup.py @@ -0,0 +1,11 @@ +import pytest + +from services.cleanup.regular import run_regular_cleanup +from services.cleanup.utils import CleanupResult, CleanupSummary + + +@pytest.mark.django_db +def test_runs_regular_cleanup(): + summary = run_regular_cleanup() + + assert summary == CleanupSummary(CleanupResult(0, 0), {}) diff --git a/apps/worker/services/cleanup/tests/test_relations.py b/apps/worker/services/cleanup/tests/test_relations.py new file mode 100644 index 0000000000..865df7bff3 --- /dev/null +++ b/apps/worker/services/cleanup/tests/test_relations.py @@ -0,0 +1,99 @@ +import pytest +import sqlparse +from django.db.models.query import QuerySet +from django.db.models.sql.subqueries import DeleteQuery +from shared.django_apps.codecov_auth.models import Owner +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory +from shared.django_apps.core.models import Repository +from shared.django_apps.core.tests.factories import RepositoryFactory +from shared.django_apps.reports.models import TestInstance, UploadLevelTotals + +from services.cleanup.relations import ( + build_relation_graph, + reverse_filter, + simplified_lookup, +) + + +def dump_delete_queries(queryset: QuerySet) -> str: + relations = build_relation_graph(queryset) + + queries = "" + for relation in relations: + if queries: + queries += "\n\n" + queries += f"-- {relation.model.__name__}\n" + + for query in relation.querysets: + compiler = query.query.chain(DeleteQuery).get_compiler(query.db) + sql, _params = compiler.as_sql() + sql = sqlparse.format(sql, reindent=True, keyword_case="upper") + queries += sql + ";\n" + + return queries + + +@pytest.mark.django_db +def test_builds_delete_queries(snapshot): + repo = Repository.objects.filter(repoid=123) + org = Owner.objects.filter(ownerid=123) + + # if you change any of the model relations, this snapshot will most likely change. + # in that case, feel free to update this using `pytest --insta update`. + assert dump_delete_queries(repo) == snapshot("repository.txt") + assert dump_delete_queries(org) == snapshot("owner.txt") + + +@pytest.mark.django_db +def test_can_simplify_queries(): + repo = Repository.objects.filter(repoid=123) + assert simplified_lookup(repo) == [123] + + repo = Repository.objects.filter(repoid__in=[123, 456]) + assert simplified_lookup(repo) == [123, 456] + + repo = Repository.objects.filter(fork=123) + assert simplified_lookup(repo) == repo + + owner_repos = Repository.objects.filter(author=123) + repo = Repository.objects.filter(repoid__in=owner_repos) + # In theory, we could simplify this to forward directly to the `owner_repo` + # subquery, but that would open too many opportunities to properly test. + assert simplified_lookup(repo) == repo + + +@pytest.mark.django_db +def test_leaf_table(snapshot): + query = UploadLevelTotals.objects.all() + assert dump_delete_queries(query) == snapshot("leaf.txt") + + +@pytest.mark.django_db +def test_can_reverse_filter(): + query = TestInstance.objects.filter(repoid=123) + assert reverse_filter(query) is None + + query = TestInstance.objects.filter(repoid__in=[123, 234]) + assert reverse_filter(query) is None + + query = TestInstance.objects.filter( + repoid__in=Repository.objects.filter(author=123), branch="foo" + ) + assert reverse_filter(query) is None + + owner = OwnerFactory() + r1 = RepositoryFactory(author=owner) + r2 = RepositoryFactory(author=owner) + r3 = RepositoryFactory(author=owner) + + filtered_qs = Repository.objects.filter(author=owner.ownerid) + query = TestInstance.objects.filter(repoid__in=filtered_qs) + + column, reversed_query = reverse_filter(query) + + assert column == "repoid" + # ideally, we would assert that the `QuerySet` itself is the same, + # but that is not really doable. but in essense we only care that it yields + # the same results, which it does: + assert set(reversed_query) == set(filtered_qs) + assert set(reversed_query) == {r1, r2, r3} diff --git a/apps/worker/services/cleanup/utils.py b/apps/worker/services/cleanup/utils.py new file mode 100644 index 0000000000..66192a32f9 --- /dev/null +++ b/apps/worker/services/cleanup/utils.py @@ -0,0 +1,58 @@ +import dataclasses +from concurrent.futures import ThreadPoolExecutor +from contextlib import contextmanager + +import shared.storage +from django.db.models import Model +from shared.config import get_config +from shared.storage.base import BaseStorageService + + +class CleanupContext: + threadpool: ThreadPoolExecutor + storage: BaseStorageService + default_bucket: str + bundleanalysis_bucket: str + + def __init__(self): + self.threadpool = ThreadPoolExecutor() + self.storage = shared.storage.get_appropriate_storage_service() + self.default_bucket = get_config( + "services", "minio", "bucket", default="archive" + ) + self.bundleanalysis_bucket = get_config( + "bundle_analysis", "bucket_name", default="bundle-analysis" + ) + + +@contextmanager +def cleanup_context(): + context = CleanupContext() + try: + yield context + finally: + context.threadpool.shutdown() + + +@dataclasses.dataclass +class CleanupResult: + cleaned_models: int + cleaned_files: int = 0 + + +@dataclasses.dataclass +class CleanupSummary: + totals: CleanupResult + summary: dict[type[Model], CleanupResult] + + def add(self, other: "CleanupSummary"): + self.totals.cleaned_models += other.totals.cleaned_models + self.totals.cleaned_files += other.totals.cleaned_files + + for model, other_result in other.summary.items(): + if model not in self.summary: + self.summary[model] = CleanupResult(0) + result = self.summary[model] + + result.cleaned_models += other_result.cleaned_models + result.cleaned_files += other_result.cleaned_files diff --git a/apps/worker/services/commit_status.py b/apps/worker/services/commit_status.py new file mode 100644 index 0000000000..003ce10cb9 --- /dev/null +++ b/apps/worker/services/commit_status.py @@ -0,0 +1,81 @@ +from typing import List + +from shared.config import get_config + +from services.yaml import read_yaml_field + + +def _ci_providers() -> List[str]: + providers = get_config("services", "ci_providers") + if not providers: + return [] + elif isinstance(providers, list): + return providers + else: + return map(lambda p: p.strip(), providers.split(",")) + + +ENTERPRISE_DEFAULTS = set(filter(None, _ci_providers())) + +CI_CONTEXTS = set( + ( + "ci", + "Codefresh", + "wercker", + "semaphoreci", + "pull request validator (cloudbees)", + "Taskcluster (pull_request)", + "continuous-integration", + "buildkite", + ) +) + +CI_DOMAINS = set( + ("jenkins", "codefresh", "bitbucket", "teamcity", "buildkite", "taskcluster") +) + +CI_EXCLUDE = set(("styleci",)) + + +class RepositoryCIFilter(object): + def __init__(self, commit_yaml) -> None: + ci = read_yaml_field(commit_yaml, ("codecov", "ci")) or [] + ci = set(ci) | ENTERPRISE_DEFAULTS + self.exclude = ( + set(map(lambda a: a[1:], filter(lambda ci: ci[0] == "!", ci)) if ci else []) + | CI_EXCLUDE + ) + self.include = ( + set(filter(lambda ci: ci[0] != "!", ci) if ci else []) | CI_DOMAINS + ) + + def __call__(self, status) -> bool: + return self._filter(status) + + def _filter(self, status) -> bool: + domain = ((status["url"] or "").split("/") + ["", "", ""])[2] + if domain: + # ignore.com in ('ignore.com',) || skip.domain.com in ('skip',) + if domain in self.exclude or set(domain.split(".")) & self.exclude: + return False + + elif domain in self.include or set(domain.split(".")) & self.include: + return True + + if status["context"]: + contexts = set(status["context"].split("/")) + if status["context"] in self.exclude or contexts & self.exclude: + return False + + elif status["context"] in self.include or contexts & ( + self.include | CI_CONTEXTS + ): + return True + + elif ( + "jenkins" in status["context"].lower() and "jenkins" not in self.exclude + ): + # url="", context="Jenkins2 - Build" + return True + + return False diff --git a/apps/worker/services/comparison/__init__.py b/apps/worker/services/comparison/__init__.py new file mode 100644 index 0000000000..d7ece4dbd9 --- /dev/null +++ b/apps/worker/services/comparison/__init__.py @@ -0,0 +1,415 @@ +import logging +from dataclasses import dataclass +from typing import Any + +import sentry_sdk +from asgiref.sync import async_to_sync +from shared.reports.changes import run_comparison_using_rust +from shared.reports.types import Change, ReportTotals +from shared.torngit.base import TorngitBaseAdapter +from shared.torngit.exceptions import TorngitClientGeneralError +from shared.utils.sessions import SessionType + +from database.enums import CompareCommitState +from database.models import CompareCommit +from services.archive import ArchiveService +from services.comparison.changes import get_changes +from services.comparison.types import Comparison, FullCommit, ReportUploadedCount +from services.repository import get_repo_provider_service + +log = logging.getLogger(__name__) + + +@dataclass +class ComparisonContext(object): + """Extra information not necessarily related to coverage that may affect notifications""" + + repository_service: TorngitBaseAdapter | None = None + all_tests_passed: bool | None = None + test_results_error: str | None = None + gh_app_installation_name: str | None = None + gh_is_using_codecov_commenter: bool = False + # GitLab has a "merge results pipeline" (see https://docs.gitlab.com/ee/ci/pipelines/merged_results_pipelines.html) + # This runs on an "internal" commit that is the merge from the PR HEAD and the target branch. This commit only exists in GitLab. + # We need to send commit statuses to this other commit, to guarantee that the check is not ignored. + # See https://docs.gitlab.com/ee/ci/pipelines/merged_results_pipelines.html#successful-merged-results-pipeline-overrides-a-failed-branch-pipeline + gitlab_extra_shas: set[str] | None = None + + +NOT_RESOLVED: Any = object() + + +class ComparisonProxy(object): + """The idea of this class is to produce a wrapper around Comparison with functionalities that + are useful to the notifications context. + + What ComparisonProxy aims to do is to provide a bunch of common calculations (like + get_changes and get_diff) with a specific set of assumptions very fit for the + notification use-cases (like the one that we should use the head commit repository + to fetch data, or that there is even a repository we can fetch data from, + and that whatever information is fetched at first would not change). + + This is not really meant for other places where a comparison might be used ( + like when one really only needs the actual in-database and in-report information). + A pure Comparison should be used for those cases + + Attributes: + comparison (Comparison): The original comparison we want to wrap and proxy + context (ComparisonContext | None): Other information not coverage-related that may affect notifications + """ + + def __init__( + self, comparison: Comparison, context: ComparisonContext | None = None + ): + self.comparison = comparison + self._repository_service = None + self._adjusted_base_diff = NOT_RESOLVED + self._original_base_diff = NOT_RESOLVED + self._patch_totals = NOT_RESOLVED + self._changes = NOT_RESOLVED + self._existing_statuses = None + self._behind_by = None + self._branch = None + self._archive_service = None + self.context = context or ComparisonContext() + self._cached_reports_uploaded_per_flag: list[ReportUploadedCount] | None = None + + def get_archive_service(self): + if self._archive_service is None: + self._archive_service = ArchiveService( + self.comparison.project_coverage_base.commit.repository + ) + return self._archive_service + + def get_filtered_comparison(self, flags, path_patterns): + if not flags and not path_patterns: + return self + return FilteredComparison(self, flags=flags, path_patterns=path_patterns) + + @property + def repository_service(self): + if self._repository_service is None: + if self.context.repository_service is not None: + self._repository_service = self.context.repository_service + else: + self._repository_service = get_repo_provider_service( + self.comparison.head.commit.repository, + installation_name_to_use=self.context.gh_app_installation_name, + ) + return self._repository_service + + def has_project_coverage_base_report(self): + return self.comparison.has_project_coverage_base_report() + + def has_head_report(self): + return self.comparison.has_head_report() + + @property + def head(self): + return self.comparison.head + + @property + def project_coverage_base(self): + return self.comparison.project_coverage_base + + @property + def enriched_pull(self): + return self.comparison.enriched_pull + + @property + def pull(self): + return self.comparison.pull + + def get_diff(self, use_original_base=False) -> dict | None: + head = self.comparison.head.commit + base = self.comparison.project_coverage_base.commit + patch_coverage_base_commitid = self.comparison.patch_coverage_base_commitid + + # If the original and adjusted bases are the same commit, then if we + # already fetched the diff for one we can return it for the other. + bases_match = patch_coverage_base_commitid == (base.commitid if base else "") + + populate_original_base_diff = use_original_base and ( + self._original_base_diff is NOT_RESOLVED + ) + populate_adjusted_base_diff = (not use_original_base) and ( + self._adjusted_base_diff is NOT_RESOLVED + ) + if populate_original_base_diff: + if bases_match and self._adjusted_base_diff is not NOT_RESOLVED: + self._original_base_diff = self._adjusted_base_diff + elif patch_coverage_base_commitid is not None: + pull_diff = async_to_sync(self.repository_service.get_compare)( + patch_coverage_base_commitid, head.commitid, with_commits=False + ) + self._original_base_diff = pull_diff["diff"] + else: + return None + elif populate_adjusted_base_diff: + if bases_match and self._original_base_diff is not NOT_RESOLVED: + self._adjusted_base_diff = self._original_base_diff + elif base is not None: + pull_diff = async_to_sync(self.repository_service.get_compare)( + base.commitid, head.commitid, with_commits=False + ) + self._adjusted_base_diff = pull_diff["diff"] + else: + return None + + if use_original_base: + return self._original_base_diff + else: + return self._adjusted_base_diff + + def get_changes(self) -> list[Change] | None: + if self._changes is NOT_RESOLVED: + diff = self.get_diff() + self._changes = get_changes( + self.comparison.project_coverage_base.report, + self.comparison.head.report, + diff, + ) + + return self._changes + + @sentry_sdk.trace + def get_patch_totals(self) -> ReportTotals | None: + """Returns the patch coverage for the comparison. + + Patch coverage refers to looking at the coverage in HEAD report filtered by the git diff HEAD..BASE. + """ + if self._patch_totals is NOT_RESOLVED: + diff = self.get_diff(use_original_base=True) + self._patch_totals = self.head.report.apply_diff(diff) + + return self._patch_totals + + def get_behind_by(self): + if self._behind_by is None: + if not getattr( + self.comparison.project_coverage_base.commit, "commitid", None + ): + log.info( + "Comparison base commit does not have commitid, unable to get behind_by" + ) + return None + + provider_pull = self.comparison.enriched_pull.provider_pull + if provider_pull is None: + log.info( + "Comparison does not have provider pull request information, unable to get behind_by" + ) + return None + branch_to_get = provider_pull["base"]["branch"] + if self._branch is None: + try: + branch_response = async_to_sync(self.repository_service.get_branch)( + branch_to_get + ) + except TorngitClientGeneralError: + log.warning( + "Unable to fetch base branch from Git provider", + extra=dict( + branch=branch_to_get, + ), + ) + return None + except KeyError: + log.warning( + "Error fetching base branch from Git provider", + extra=dict( + branch=branch_to_get, + ), + ) + return None + self._branch = branch_response + + distance = async_to_sync(self.repository_service.get_distance_in_commits)( + self._branch["sha"], + self.comparison.project_coverage_base.commit.commitid, + with_commits=False, + ) + self._behind_by = distance["behind_by"] + self.enriched_pull.database_pull.behind_by = distance["behind_by"] + self.enriched_pull.database_pull.behind_by_commit = distance[ + "behind_by_commit" + ] + return self._behind_by + + def all_tests_passed(self) -> bool: + if self.context: + return self.context.all_tests_passed or False + + return False + + def test_results_error(self) -> str | None: + if self.context: + return self.context.test_results_error + + return None + + def get_existing_statuses(self): + if self._existing_statuses is None: + self._existing_statuses = async_to_sync( + self.repository_service.get_commit_statuses + )(self.head.commit.commitid) + return self._existing_statuses + + @sentry_sdk.trace + def get_impacted_files(self) -> dict: + files_in_diff = self.get_diff() + return run_comparison_using_rust( + self.comparison.project_coverage_base.report, + self.comparison.head.report, + files_in_diff, + ) + + def get_reports_uploaded_count_per_flag(self) -> list[ReportUploadedCount]: + """This function counts how many reports (by flag) the BASE and HEAD commit have.""" + if self._cached_reports_uploaded_per_flag: + # Reports may have many sessions, so it's useful to memoize this function + return self._cached_reports_uploaded_per_flag + if not self.has_head_report() or not self.has_project_coverage_base_report(): + log.warning( + "Can't calculate diff in uploads. Missing some report", + extra=dict( + has_head_report=self.has_head_report(), + has_project_base_report=self.has_project_coverage_base_report(), + ), + ) + return [] + per_flag_dict: dict[str, ReportUploadedCount] = dict() + base_report = self.comparison.project_coverage_base.report + head_report = self.comparison.head.report + ops = [(base_report, "base_count"), (head_report, "head_count")] + for curr_report, curr_counter in ops: + for session in curr_report.sessions.values(): + # We ignore carryforward sessions + # Because not all commits would upload all flags (potentially) + # But they are still carried forward + if session.session_type != SessionType.carriedforward: + # It's possible that an upload is done without flags. + # In this case we still want to count it in the count, but there's no flag associated to it + session_flags = session.flags or [""] + for flag in session_flags: + dict_value = per_flag_dict.get(flag) + if dict_value is None: + dict_value = ReportUploadedCount( + flag=flag, base_count=0, head_count=0 + ) + dict_value[curr_counter] += 1 + per_flag_dict[flag] = dict_value + self._cached_reports_uploaded_per_flag = list(per_flag_dict.values()) + return self._cached_reports_uploaded_per_flag + + def get_reports_uploaded_count_per_flag_diff(self) -> list[ReportUploadedCount]: + """ + Returns the difference, per flag, or reports uploaded in BASE and HEAD + + ❗️ For a difference to be considered there must be at least 1 "uploaded" upload in both + BASE and HEAD (that is, if all reports for a flag are "carryforward" it's not considered a diff) + """ + reports_per_flag = self.get_reports_uploaded_count_per_flag() + + def is_valid_diff(obj: ReportUploadedCount): + if self.comparison.current_yaml: + flag_config = self.comparison.current_yaml.get_flag_configuration( + obj["flag"] + ) + is_flag_carryforward = ( + flag_config.get("carryforward", False) + if flag_config is not None + else False + ) + else: + is_flag_carryforward = False + head_and_base_have_uploads = obj["base_count"] > 0 and obj["head_count"] > 0 + head_or_base_have_uploads = obj["base_count"] > 0 or obj["head_count"] > 0 + return ( + (is_flag_carryforward and head_and_base_have_uploads) + or (not is_flag_carryforward and head_or_base_have_uploads) + ) and obj["base_count"] > obj["head_count"] + + per_flag_diff = list(filter(is_valid_diff, reports_per_flag)) + self._cached_reports_uploaded_per_flag = per_flag_diff + return per_flag_diff + + +class FilteredComparison(object): + def __init__(self, real_comparison: ComparisonProxy, *, flags, path_patterns): + self.flags = flags + self.path_patterns = path_patterns + self.real_comparison = real_comparison + self._patch_totals = None + self._changes = None + self.project_coverage_base = FullCommit( + commit=real_comparison.project_coverage_base.commit, + report=( + real_comparison.project_coverage_base.report.filter( + flags=flags, paths=path_patterns + ) + if self.has_project_coverage_base_report() + else None + ), + ) + self.head = FullCommit( + commit=real_comparison.head.commit, + report=real_comparison.head.report.filter(flags=flags, paths=path_patterns), + ) + + def get_impacted_files(self) -> dict: + return self.real_comparison.get_impacted_files() + + def get_diff(self, use_original_base=False): + return self.real_comparison.get_diff(use_original_base=use_original_base) + + @sentry_sdk.trace + def get_patch_totals(self) -> ReportTotals | None: + """Returns the patch coverage for the comparison. + + Patch coverage refers to looking at the coverage in HEAD report filtered by the git diff HEAD..BASE. + """ + if self._patch_totals: + return self._patch_totals + diff = self.get_diff(use_original_base=True) + self._patch_totals = self.head.report.apply_diff(diff) + return self._patch_totals + + def get_existing_statuses(self): + return self.real_comparison.get_existing_statuses() + + def has_project_coverage_base_report(self): + return self.real_comparison.has_project_coverage_base_report() + + @property + def enriched_pull(self): + return self.real_comparison.enriched_pull + + def get_changes(self) -> list[Change] | None: + if self._changes is None: + diff = self.get_diff() + self._changes = get_changes( + self.project_coverage_base.report, self.head.report, diff + ) + return self._changes + + @property + def pull(self): + return self.real_comparison.pull + + +def get_or_create_comparison(db_session, base_commit, compare_commit): + comparison = ( + db_session.query(CompareCommit) + .filter_by(base_commit=base_commit, compare_commit=compare_commit) + .one_or_none() + ) + if comparison is None: + comparison = CompareCommit( + base_commit=base_commit, + compare_commit=compare_commit, + state=CompareCommitState.pending.value, + ) + db_session.add(comparison) + db_session.flush() + return comparison diff --git a/apps/worker/services/comparison/changes.py b/apps/worker/services/comparison/changes.py new file mode 100644 index 0000000000..c1232d2d6c --- /dev/null +++ b/apps/worker/services/comparison/changes.py @@ -0,0 +1,308 @@ +import dataclasses +import logging +from collections import defaultdict +from typing import Any, Iterator, Tuple, Union + +import sentry_sdk +from shared.helpers.numeric import ratio +from shared.reports.resources import Report +from shared.reports.types import Change, ReportTotals +from shared.utils.merge import line_type + +log = logging.getLogger(__name__) + + +def diff_totals(base, head, absolute=None) -> Union[bool, None, ReportTotals]: + if head is None: + return False # file deleted + + elif base is None: + return True # new file + + elif base == head: + return None # same same + + head_tuple = dataclasses.astuple(head) + base_tuple = dataclasses.astuple(base) + + diff_tuple = [ + (int(float(head_tuple[i] or 0)) - int(float(base_tuple[i] or 0))) + for i in (0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11) + ] + diff = ReportTotals(*diff_tuple) + if absolute and absolute.coverage is not None: + # ratio(before.hits + changed.hits, before.lines, changed.lines) - coverage before + # = actual coveage change + hits = absolute.hits + diff.hits + diff.coverage = float( + ratio( + hits, + ( + hits + + absolute.misses + + diff.misses + + absolute.partials + + diff.partials + ), + ) + ) - float(absolute.coverage) + else: + diff.coverage = float(head.coverage) - float(base.coverage) + return ReportTotals(*diff) + + +def get_segment_offsets(segments) -> tuple[dict[int, Any], list[int], list[int]]: + offsets: dict[int, int] = defaultdict(int) + additions = [] + removals = [] + # loop through the segments + for seg in segments: + # get the starting line number + start = int(seg["header"][2]) or 1 + offset_l = 0 # used to offset the segment line number (not real line numbers) + offset_r = 0 # used to offset the segment line number (not real line numbers) + base_start = ( + int(seg["header"][0] if seg["header"][0] and seg["header"][0] != " " else 1) + or 1 + ) + starting_diff = start - base_start + # loop through all the lines + for ln, line in enumerate(seg["lines"], start=start): + l0 = line[0] + if l0 == "-": + removals.append(ln + offset_l - starting_diff) + offsets[ln + offset_r] += 1 + offset_r -= 1 + + elif l0 == "+": + additions.append(ln + offset_r) + offsets[ln + offset_r] -= 1 + offset_l -= 1 + return dict([(k, v) for k, v in offsets.items() if v != 0]), additions, removals + + +@sentry_sdk.trace +def get_changes( + base_report: Report, head_report: Report, diff_json: dict[str, Any] | None +) -> list[Change] | None: + """ + + Please bear with me because I didnt write the function, so what I know is from using it + and trying to unit testing it. + + What this function does is calculate the "unexpected" changes on coverage between two reports. + Unexpected changes are changes that do NOT arise from the diff. + That means, for example, that: + - If you delete the file between BASE and HEAD, it is expected that the coverage from + that file will vanish on HEAD, so it does not show up on changes list + - Added files are also ignored on the changes list. + - The coverage changes that happen inside the git diff are also expected, so + they dont show up here + - Files that are not in the diff will show up here + - Files that are in the diff, but had the change happen outside the diff will show up + here + - Renaming the file will also be properly handled here such that, if a file is renamed, + we compare the `original_name` ReportFile in the base report and + the `new_name` ReportFile in the head report + + For a better understanding of it, see the unit tests covering this function + + Args: + base_report (Report): The report for the base commit + head_report (Report): The report for the head commit + diff_json (Mapping[str, Any]): The diff between the base and head commit as returned by torngit + + Returns: + List[Change]: A list of unexpected changes between base_report and head_report + """ + if base_report is None or head_report is None: + return None + + changes = [] + base_files = set(base_report.files) + head_files = set(head_report.files) + diff_keys = set(diff_json["files"].keys()) if diff_json else set() + + # moved files + moved_files = ( + set([d["before"] for k, d in diff_json["files"].items() if d.get("before")]) + if diff_json + else set() + ) + # deleted files + missing_files = base_files - head_files - diff_keys - moved_files + # added files + new_files = head_files - base_files - diff_keys - moved_files + + # find modified !diff files + for _file in head_report: + filename = _file.name + # skip [new] + [missing] + if filename in missing_files or filename in new_files: + continue + + diff = diff_json["files"].get(filename) if diff_json is not None else None + base_report_file = base_report.get( + (diff.get("before") or filename) if diff else filename + ) + if not base_report_file: + if diff is None: + # Seems to only happen when there is a 'moved file' in a weird situation + log.info( + "File not in the diff, not in base, but still not a 'new_file'", + extra=dict( + diff_keys=sorted(diff_keys), + missing_filename=filename, + base_is_none=base_report_file is None, + moved_files=sorted(moved_files), + ), + ) + new_files.add(filename) + continue + if diff.get("type") == "new": + # File is lacking at base, present at head + # Diff says it's because it's new + # This is expected + continue + r = get_segment_offsets(diff["segments"]) + additions: set[int] = set(r[1]) + if any(ln not in additions for ln, _ in _file.lines): + # file has new coverage lines that are not accounted by the diff + new_files.add(filename) + continue + + lines = list( + iter_changed_lines( + base_report_file=base_report_file, + head_report_file=_file, + diff=diff, + yield_line_numbers=False, + ) + ) + + if any(lines): + # only if there are any lines that changed + lines = zip(*lines) + changes.append( + Change( + path=filename, + in_diff=bool(diff), + old_path=diff.get("before") if diff else None, + totals=diff_totals( + get_totals_from_list(next(lines)), + get_totals_from_list(next(lines)), + base_report_file.totals, + ), + ) + ) + if diff_json: + vanished_base_files = { + d.get("before") or k: (k, d) + for (k, d) in diff_json["files"].items() + if head_report.get(k) is None + and base_report.get(d.get("before") or k) is not None + } + for possibly_deleted_filename, data in vanished_base_files.items(): + head_name, diff = data + # these are files that are present on base, not present on head + # and are possibly accounted by the diff + # But to know that for sure we need to know that every line lost is accounted + # by the diff + if diff.get("type") != "deleted": + base_report_file = base_report.get(possibly_deleted_filename) + present_lines_on_base = set(x[0] for x in base_report_file.lines) + _, _, line_removals = get_segment_offsets(diff["segments"]) + lines_unnaccounted_for = present_lines_on_base - set(line_removals) + if lines_unnaccounted_for: + changes.append(Change(path=head_name, deleted=True)) + + # [deleted] [~~diff~~] == missing reports + # left over deleted files + # this one is "bad" because coverage reports are missing entirely. + if missing_files: + changes.extend([Change(path=path, deleted=True) for path in missing_files]) + + # [new] [~~diff~~] == new reports + if new_files: + changes.extend([Change(path=path, new=True) for path in new_files]) + + return changes + + +def get_totals_from_list(lst) -> ReportTotals: + """ + takes list of coverage values and returns a + on the list + IN [1,0,"1/2"] => OUT ReportTotals(hits=1, misses=1, partials=1) + """ + lst = list(map(line_type, lst)) + return ReportTotals(hits=lst.count(0), misses=lst.count(1), partials=lst.count(2)) + + +def iter_changed_lines( + base_report_file, head_report_file, diff=None, yield_line_numbers=True +) -> Iterator[Union[int, Tuple[Any, Any]]]: + """ + streams line numbers that changed as integers > 0 + """ + if not diff or diff["type"] == "modified": + offsets, skip_lines, removed_lines = ( + get_segment_offsets(diff["segments"]) if diff else (None, None, None) + ) + base_ln = 0 + base_report_file_eof = ( + base_report_file.eof if base_report_file is not None else 0 + ) + for ln in range( + 1, + max( + ( + base_report_file_eof, + base_report_file_eof + + len(skip_lines or []) + - len(removed_lines or []), + head_report_file.eof, + ) + ) + + 1, + ): + if offsets: + base_ln += 1 + _offset = offsets.get(ln) + if _offset is not None: + base_ln += _offset + + if not skip_lines or ln not in skip_lines: + base_line = ( + base_report_file.get(base_ln or ln) + if base_report_file is not None + else None + ) + head_line = head_report_file.get(ln) + # if a base line exist we can compare against + if base_line: + if head_line: + # we have a head line + if line_has_changed(base_line, head_line): + # unexpected: coverage data changed + yield ( + ln + if yield_line_numbers + else ( + base_line.coverage, + head_line.coverage, + ) + ) + # coverage data remains the same + else: + # unexpected: coverage data disappeared + yield ln if yield_line_numbers else (base_line.coverage, None) + + elif head_line: + # unexpected: new coverage data + yield ln if yield_line_numbers else (None, head_line.coverage) + + +def line_has_changed(before, after) -> bool: + # coverage changed + return line_type(before.coverage) != line_type(after.coverage) diff --git a/apps/worker/services/comparison/conftest.py b/apps/worker/services/comparison/conftest.py new file mode 100644 index 0000000000..c3d8d53e89 --- /dev/null +++ b/apps/worker/services/comparison/conftest.py @@ -0,0 +1,107 @@ +# TODO: Clean this + +import pytest +from shared.reports.readonly import ReadOnlyReport +from shared.reports.reportfile import ReportFile +from shared.reports.resources import Report +from shared.reports.types import ReportLine +from shared.utils.sessions import Session + +from database.tests.factories import CommitFactory, PullFactory, RepositoryFactory +from services.comparison import ComparisonProxy +from services.comparison.types import Comparison, FullCommit +from services.repository import EnrichedPull + + +def get_small_report(flags=None): + if flags is None: + flags = ["integration"] + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(11, 20)) + ) + first_file.append(3, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append(51, ReportLine.create(coverage=0, sessions=[[0, 1]])) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=flags)) + return report + + +@pytest.fixture +def sample_report(): + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=["unit"])) + return report + + +@pytest.fixture +def sample_comparison(dbsession, request, sample_report): + repository = RepositoryFactory.create( + owner__username=request.node.name, owner__service="github" + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository, author__service="github") + head_commit = CommitFactory.create( + repository=repository, branch="new_branch", author__service="github" + ) + pull = PullFactory.create( + repository=repository, base=base_commit.commitid, head=head_commit.commitid + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + repository = base_commit.repository + base_full_commit = FullCommit( + commit=base_commit, report=ReadOnlyReport.create_from_report(get_small_report()) + ) + head_full_commit = FullCommit( + commit=head_commit, report=ReadOnlyReport.create_from_report(sample_report) + ) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={ + "author": {"id": "12345", "username": "codecov-test-user"}, + "base": {"branch": "master", "commitid": base_commit.commitid}, + "head": { + "branch": "reason/some-testing", + "commitid": head_commit.commitid, + }, + "number": str(pull.pullid), + "id": str(pull.pullid), + "state": "open", + "title": "Creating new code for reasons no one knows", + }, + ), + ) + ) diff --git a/apps/worker/services/comparison/tests/unit/__init__.py b/apps/worker/services/comparison/tests/unit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/comparison/tests/unit/test_behind_by.py b/apps/worker/services/comparison/tests/unit/test_behind_by.py new file mode 100644 index 0000000000..d910da388e --- /dev/null +++ b/apps/worker/services/comparison/tests/unit/test_behind_by.py @@ -0,0 +1,46 @@ +from shared.torngit.exceptions import TorngitClientGeneralError + +from services.comparison import ComparisonProxy + + +class TestGetBehindBy(object): + def test_get_behind_by(self, mocker, mock_repo_provider): + comparison = ComparisonProxy(mocker.MagicMock()) + comparison.comparison.enriched_pull.provider_pull = {"base": {"branch": "a"}} + mock_repo_provider.get_branches.return_value = [("a", "1")] + mock_repo_provider.get_distance_in_commits.return_value = { + "behind_by": 3, + "behind_by_commit": 123456, + } + mocker.patch( + "services.comparison.get_repo_provider_service", + return_value=mock_repo_provider, + ) + res = comparison.get_behind_by() + assert res == 3 + + def test_get_behind_by_no_base_commit(self, mocker): + comparison = ComparisonProxy(mocker.MagicMock()) + del comparison.comparison.project_coverage_base.commit.commitid + res = comparison.get_behind_by() + assert res is None + + def test_get_behind_by_no_provider_pull(self, mocker): + comparison = ComparisonProxy(mocker.MagicMock()) + comparison.comparison.enriched_pull.provider_pull = None + res = comparison.get_behind_by() + assert res is None + + def test_get_behind_by_no_matching_branches(self, mocker, mock_repo_provider): + mock_repo_provider.get_branch.side_effect = TorngitClientGeneralError( + 404, + None, + "Branch not found", + ) + mocker.patch( + "services.comparison.get_repo_provider_service", + return_value=mock_repo_provider, + ) + comparison = ComparisonProxy(mocker.MagicMock()) + res = comparison.get_behind_by() + assert res is None diff --git a/apps/worker/services/comparison/tests/unit/test_changes.py b/apps/worker/services/comparison/tests/unit/test_changes.py new file mode 100644 index 0000000000..d7db7fefa1 --- /dev/null +++ b/apps/worker/services/comparison/tests/unit/test_changes.py @@ -0,0 +1,459 @@ +import pytest +from shared.reports.reportfile import ReportFile +from shared.reports.resources import Report +from shared.reports.types import ReportLine, ReportTotals + +from services.comparison.changes import ( + Change, + diff_totals, + get_changes, + get_segment_offsets, +) + + +class TestDiffTotals(object): + @pytest.mark.parametrize( + "base, head, absolute, res", + [ + (ReportTotals(lines=1), None, None, False), + (ReportTotals(lines=1), ReportTotals(lines=1), None, None), + (None, ReportTotals(lines=1), None, True), + ( + ReportTotals(lines=1, coverage=1), + ReportTotals(lines=2, coverage=2), + None, + ReportTotals(lines=1, coverage=1), + ), + ( + ReportTotals(lines=2, coverage=3), + ReportTotals(lines=1, coverage=4), + None, + ReportTotals(lines=-1, coverage=1), + ), + ( + ReportTotals(lines=2, coverage=5), + ReportTotals(files=1, coverage=6), + None, + ReportTotals(files=1, lines=-2, coverage=1), + ), + ( + ReportTotals(coverage=15), + ReportTotals(coverage=14), + None, + ReportTotals(coverage=-1), + ), + ( + ReportTotals(coverage=15), + ReportTotals(coverage=14), + ReportTotals(coverage=None), + ReportTotals(coverage=-1), + ), + ], + ) + def test_diff_totals(self, base, head, absolute, res): + assert diff_totals(base, head, absolute) == res + + +@pytest.mark.parametrize( + "segments, result", + [ + ([(["1", "0", "1", "0"], "-+ ")], ({}, [1], [1])), + ([(["1", "0", "1", "0"], "++ ")], ({1: -1, 2: -1}, [1, 2], [])), + ([(["1", "0", "1", "0"], "-- ")], ({1: 2}, [], [1, 2])), + ( + [(["1", "0", "1", "0"], " ++ --+ ")], + ({2: -1, 3: -1, 5: 1}, [2, 3, 5], [3, 4]), + ), + ([(["5", "0", "7", "0"], " -+ + ")], ({10: -1}, [8, 10], [6])), + ([(["0", "0", "0", "0"], "--++")], ({1: 1, 2: -1}, [1, 2], [1, 2])), + ([([" ", "0", "0", "0"], "--++")], ({1: 1, 2: -1}, [1, 2], [1, 2])), + ([(["1", "0", "1", "0"], "-----+ ")], ({1: 4}, [1], [1, 2, 3, 4, 5])), + ( + [(["1", "0", "1", "0"], "-+ -+ -+ -----++++ ")], + ( + {8: -1, 9: -1, 10: -1, 7: 4}, + [1, 3, 5, 7, 8, 9, 10], + [1, 3, 5, 7, 8, 9, 10, 11], + ), + ), + ( + [(["5", "0", "1", "0"], " - - - + - ")], + ({2: 1, 3: 1, 4: 1, 5: -1, 7: 1}, [5], [6, 8, 10, 13]), + ), + ], +) +def test_get_segment_offsets(segments, result): + assert ( + get_segment_offsets( + [dict(header=seg[0], lines=list(seg[1])) for seg in segments] + ) + == result + ) + + +class TestChanges(object): + def test_get_changes_eof_case(self): + json_diff = { + "files": { + "filename.py": { + "before": None, + "segments": [ + {"header": ["1", "1", "1", "20"], "lines": ["-"] + ["+"] * 20} + ], + "stats": {"added": 20, "removed": 1}, + "type": "modified", + } + } + } + first_report = Report() + second_report = Report() + first_file = ReportFile("filename.py") + second_file = ReportFile("filename.py") + first_file.append(2, ReportLine.create(coverage=0)) + second_file.append(2, ReportLine.create(coverage=0)) + first_report.append(first_file) + second_report.append(second_file) + expected_result = [ + Change( + "filename.py", + new=False, + deleted=False, + in_diff=True, + old_path=None, + totals=ReportTotals( + files=0, + lines=0, + hits=0, + misses=-1, + partials=0, + coverage=100.0, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ) + ] + res = get_changes(first_report, second_report, json_diff) + assert expected_result == res + + def test_get_changes(self): + json_diff = { + "files": { + "modified.py": { + "before": None, + "segments": [ + { + "header": ["20", "8", "20", "8"], + "lines": [ + " return k * k", + " ", + " ", + "-def k(l):", + "- return 2 * l", + "+def k(var):", + "+ return 2 * var", + " ", + " ", + " def sample_function():", + ], + } + ], + "stats": {"added": 2, "removed": 2}, + "type": "modified", + }, + "renamed.py": { + "before": "old_renamed.py", + "segments": [], + "stats": {"added": 0, "removed": 0}, + "type": "modified", + }, + "renamed_and_missing.py": { + "before": "old_renamed_and_missing.py", + "segments": [ + { + "header": ["1", "1", "1", "1"], + "lines": ["- return 2 * l", "+ return 2 * var"], + } + ], + "stats": {"added": 0, "removed": 0}, + "type": "modified", + }, + "renamed_with_changes.py": { + "before": "old_renamed_with_changes.py", + "segments": [], + "stats": {"added": 0, "removed": 0}, + "type": "modified", + }, + "removed_some_covered_lines.py": { + "before": None, + "segments": [ + { + "header": ["10", "3", "10", "1"], + "lines": ["-removed", "-r", " h"], + }, + {"header": ["35", "3", "33", "1"], "lines": [" b", "-r", " a"]}, + ], + "stats": {"added": 0, "removed": 0}, + "type": "modified", + }, + "removed_all_covered_lines.py": { + "before": None, + "segments": [ + {"header": ["10", "1", "10", "0"], "lines": ["-removed"]} + ], + "stats": {"added": 0, "removed": 0}, + "type": "modified", + }, + "added.py": { + "before": None, + "segments": [ + { + "header": ["0", "0", "1", ""], + "lines": ["+This is an explanation"], + } + ], + "stats": {"added": 1, "removed": 0}, + "type": "new", + }, + "added_unnacounted.py": { + "before": None, + "segments": [ + { + "header": ["50", "0", "50", "1"], + "lines": ["+This is an explanation"], + } + ], + "stats": {"added": 1, "removed": 0}, + "type": "modified", + }, + "deleted.py": { + "before": None, + "stats": {"added": 0, "removed": 0}, + "type": "deleted", + }, + } + } + + first_report = Report() + second_report = Report() + # DELETED FILE + first_deleted_file = ReportFile("deleted.py") + first_deleted_file.append(10, ReportLine.create(coverage=1)) + first_deleted_file.append(12, ReportLine.create(coverage=0)) + first_report.append(first_deleted_file) + # removed_some_covered_lines.py + second_deleted_file = ReportFile("removed_some_covered_lines.py") + second_deleted_file.append(10, ReportLine.create(coverage=0)) + second_deleted_file.append(11, ReportLine.create(coverage=0)) + second_deleted_file.append(12, ReportLine.create(coverage=0)) + first_report.append(second_deleted_file) + # removed_all_covered_lines.py + third_deleted_file = ReportFile("removed_all_covered_lines.py") + third_deleted_file.append(10, ReportLine.create(coverage=0)) + first_report.append(third_deleted_file) + # ADDED FILE + second_added_file = ReportFile("added.py") + second_added_file.append(99, ReportLine.create(coverage=1)) + second_added_file.append(101, ReportLine.create(coverage=0)) + second_report.append(second_added_file) + # ADDED FILE BUT UNNACOUNTED FOR + second_added_file_unnaccounted_for = ReportFile("added_unnacounted.py") + second_added_file_unnaccounted_for.append(99, ReportLine.create(coverage=1)) + second_added_file_unnaccounted_for.append(101, ReportLine.create(coverage=0)) + second_report.append(second_added_file_unnaccounted_for) + # MODIFIED FILE + first_modified_file = ReportFile("modified.py") + first_modified_file.append(17, ReportLine.create(coverage=1)) + first_modified_file.append(18, ReportLine.create(coverage=1)) + first_modified_file.append(19, ReportLine.create(coverage=1)) + first_modified_file.append(20, ReportLine.create(coverage=0)) + first_modified_file.append(21, ReportLine.create(coverage=1)) + first_modified_file.append(22, ReportLine.create(coverage=1)) + first_modified_file.append(23, ReportLine.create(coverage=1)) + first_report.append(first_modified_file) + second_modified_file = ReportFile("modified.py") + second_modified_file.append(18, ReportLine.create(coverage=1)) + second_modified_file.append(19, ReportLine.create(coverage=0)) + second_modified_file.append(20, ReportLine.create(coverage=0)) + second_modified_file.append(21, ReportLine.create(coverage=1)) + second_modified_file.append(22, ReportLine.create(coverage=0)) + second_modified_file.append(23, ReportLine.create(coverage=0)) + second_report.append(second_modified_file) + # RENAMED WITHOUT CHANGES + first_renamed_without_changes_file = ReportFile("old_renamed.py") + first_renamed_without_changes_file.append(1, ReportLine.create(coverage=1)) + first_renamed_without_changes_file.append(2, ReportLine.create(coverage=1)) + first_renamed_without_changes_file.append(3, ReportLine.create(coverage=0)) + first_renamed_without_changes_file.append(4, ReportLine.create(coverage=1)) + first_renamed_without_changes_file.append(5, ReportLine.create(coverage=0)) + first_report.append(first_renamed_without_changes_file) + second_renamed_without_changes_file = ReportFile("renamed.py") + second_renamed_without_changes_file.append(1, ReportLine.create(coverage=1)) + second_renamed_without_changes_file.append(2, ReportLine.create(coverage=1)) + second_renamed_without_changes_file.append(3, ReportLine.create(coverage=0)) + second_renamed_without_changes_file.append(4, ReportLine.create(coverage=1)) + second_renamed_without_changes_file.append(5, ReportLine.create(coverage=0)) + second_report.append(second_renamed_without_changes_file) + # RENAMED WITH COVERAGE CHANGES FILE + first_renamed_file = ReportFile("old_renamed_with_changes.py") + first_renamed_file.append(2, ReportLine.create(coverage=1)) + first_renamed_file.append(3, ReportLine.create(coverage=1)) + first_renamed_file.append(5, ReportLine.create(coverage=0)) + first_renamed_file.append(8, ReportLine.create(coverage=1)) + first_renamed_file.append(13, ReportLine.create(coverage=1)) + first_report.append(first_renamed_file) + second_renamed_file = ReportFile("renamed_with_changes.py") + second_renamed_file.append(5, ReportLine.create(coverage=1)) + second_renamed_file.append(8, ReportLine.create(coverage=0)) + second_renamed_file.append(13, ReportLine.create(coverage=1)) + second_renamed_file.append(21, ReportLine.create(coverage=1)) + second_renamed_file.append(34, ReportLine.create(coverage=0)) + second_report.append(second_renamed_file) + # UNRELATED FILE + first_unrelated_file = ReportFile("unrelated.py") + first_unrelated_file.append(1, ReportLine.create(coverage=1)) + first_unrelated_file.append(2, ReportLine.create(coverage=1)) + first_unrelated_file.append(4, ReportLine.create(coverage=1)) + first_unrelated_file.append(16, ReportLine.create(coverage=0)) + first_unrelated_file.append(256, ReportLine.create(coverage=1)) + first_unrelated_file.append(65556, ReportLine.create(coverage=1)) + first_report.append(first_unrelated_file) + second_unrelated_file = ReportFile("unrelated.py") + second_unrelated_file.append(2, ReportLine.create(coverage=1)) + second_unrelated_file.append(4, ReportLine.create(coverage=0)) + second_unrelated_file.append(8, ReportLine.create(coverage=0)) + second_unrelated_file.append(16, ReportLine.create(coverage=1)) + second_unrelated_file.append(32, ReportLine.create(coverage=0)) + second_report.append(second_unrelated_file) + # JUST VANISHED + first_old_renamed_and_missing = ReportFile("old_renamed_and_missing.py") + first_old_renamed_and_missing.append(1, ReportLine.create(coverage=1)) + first_old_renamed_and_missing.append(2, ReportLine.create(coverage=1)) + first_old_renamed_and_missing.append(3, ReportLine.create(coverage=0)) + first_old_renamed_and_missing.append(8, ReportLine.create(coverage=0)) + first_report.append(first_old_renamed_and_missing) + res = get_changes(first_report, second_report, json_diff) + expected_result = [ + Change( + path="modified.py", + new=False, + deleted=False, + in_diff=True, + old_path=None, + totals=ReportTotals( + files=0, + lines=0, + hits=-3, + misses=2, + partials=0, + coverage=-35.714290000000005, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ), + Change( + path="renamed_with_changes.py", + new=False, + deleted=False, + in_diff=True, + old_path="old_renamed_with_changes.py", + totals=ReportTotals( + files=0, + lines=0, + hits=-1, + misses=1, + partials=0, + coverage=-20.0, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ), + Change( + path="unrelated.py", + new=False, + deleted=False, + in_diff=False, + old_path=None, + totals=ReportTotals( + files=0, + lines=0, + hits=-3, + misses=2, + partials=0, + coverage=-43.333330000000004, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ), + Change( + path="added_unnacounted.py", + new=True, + deleted=False, + in_diff=None, + old_path=None, + totals=None, + ), + Change(path="removed_some_covered_lines.py", deleted=True), + Change( + path="renamed_and_missing.py", + new=False, + deleted=True, + in_diff=None, + old_path=None, + totals=None, + ), + ] + assert set([x.path for x in res]) == set([x.path for x in expected_result]) + for individual_result, individual_expected_result in zip( + sorted(res, key=lambda x: x.path), + sorted(expected_result, key=lambda x: x.path), + ): + assert individual_result == individual_expected_result + assert sorted(res, key=lambda x: x.path) == sorted( + expected_result, key=lambda x: x.path + ) + + def test_get_changes_missing_file(self): + json_diff = {"files": {"a": {"before": "missing_file"}}} + first_report_file = ReportFile("missing_file") + first_report_file.append(18, ReportLine.create(coverage=1)) + first_report = Report() + second_report = Report() + second_report.append(first_report_file) + res = get_changes(first_report, second_report, json_diff) + assert res == [ + Change( + path="missing_file", + new=True, + deleted=False, + in_diff=None, + old_path=None, + totals=None, + ) + ] + + def test_get_changes_diff_with_no_before(self): + json_diff = {"files": {"file_on_base": {"type": "binary"}}} + first_report = Report() + second_report = Report() + res = get_changes(first_report, second_report, json_diff) + assert res == [] diff --git a/apps/worker/services/comparison/tests/unit/test_comparison_proxy.py b/apps/worker/services/comparison/tests/unit/test_comparison_proxy.py new file mode 100644 index 0000000000..a9821ca6ed --- /dev/null +++ b/apps/worker/services/comparison/tests/unit/test_comparison_proxy.py @@ -0,0 +1,142 @@ +from mock import call, patch + +from database.tests.factories import CommitFactory, PullFactory, RepositoryFactory +from services.comparison import NOT_RESOLVED, ComparisonProxy +from services.comparison.types import Comparison, FullCommit +from services.repository import EnrichedPull + + +def make_sample_comparison(adjusted_base=False): + repo = RepositoryFactory.create(owner__service="github") + + head_commit = CommitFactory.create(repository=repo) + adjusted_base_commit = CommitFactory.create(repository=repo) + + if adjusted_base: + # Just getting a random commitid, doesn't need to be in the db + patch_coverage_base_commitid = CommitFactory.create(repository=repo).commitid + else: + patch_coverage_base_commitid = adjusted_base_commit.commitid + + pull = PullFactory.create( + repository=repo, + head=head_commit.commitid, + base=patch_coverage_base_commitid, + compared_to=adjusted_base_commit.commitid, + ) + + base_full_commit = FullCommit(commit=adjusted_base_commit, report=None) + head_full_commit = FullCommit(commit=head_commit, report=None) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=patch_coverage_base_commitid, + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={}, + ), + ), + ) + + +class TestComparisonProxy(object): + compare_url = "https://api.github.com/repos/{}/compare/{}...{}" + + @patch("shared.torngit.github.Github.get_compare") + def test_get_diff_adjusted_base(self, mock_get_compare): + comparison = make_sample_comparison(adjusted_base=True) + mock_get_compare.return_value = {"diff": "magic string"} + result = comparison.get_diff(use_original_base=False) + + assert result == "magic string" + assert comparison._adjusted_base_diff == "magic string" + assert comparison._original_base_diff is NOT_RESOLVED + assert ( + comparison.comparison.patch_coverage_base_commitid + != comparison.project_coverage_base.commit.commitid + ) + + assert mock_get_compare.call_args_list == [ + call( + comparison.project_coverage_base.commit.commitid, + comparison.head.commit.commitid, + with_commits=False, + ), + ] + + @patch("shared.torngit.github.Github.get_compare") + def test_get_diff_original_base(self, mock_get_compare): + comparison = make_sample_comparison(adjusted_base=True) + mock_get_compare.return_value = {"diff": "magic string"} + result = comparison.get_diff(use_original_base=True) + + assert result == "magic string" + assert comparison._original_base_diff == "magic string" + assert comparison._adjusted_base_diff is NOT_RESOLVED + assert ( + comparison.comparison.patch_coverage_base_commitid + != comparison.project_coverage_base.commit.commitid + ) + + assert mock_get_compare.call_args_list == [ + call( + comparison.comparison.patch_coverage_base_commitid, + comparison.head.commit.commitid, + with_commits=False, + ), + ] + + @patch("shared.torngit.github.Github.get_compare") + def test_get_diff_bases_match_original_base(self, mock_get_compare): + comparison = make_sample_comparison(adjusted_base=False) + mock_get_compare.return_value = {"diff": "magic string"} + result = comparison.get_diff(use_original_base=True) + + assert result == "magic string" + assert comparison._original_base_diff == "magic string" + assert ( + comparison.comparison.patch_coverage_base_commitid + == comparison.project_coverage_base.commit.commitid + ) + + # In this test case, the adjusted and original base commits are the + # same. If we get one, we should set the cache for the other. + adjusted_base_result = comparison.get_diff(use_original_base=False) + assert comparison._adjusted_base_diff == "magic string" + + # Make sure we only called the Git provider API once + assert mock_get_compare.call_args_list == [ + call( + comparison.comparison.patch_coverage_base_commitid, + comparison.head.commit.commitid, + with_commits=False, + ), + ] + + @patch("shared.torngit.github.Github.get_compare") + def test_get_diff_bases_match_adjusted_base(self, mock_get_compare): + comparison = make_sample_comparison(adjusted_base=False) + mock_get_compare.return_value = {"diff": "magic string"} + result = comparison.get_diff(use_original_base=False) + + assert result == "magic string" + assert comparison._adjusted_base_diff == "magic string" + assert ( + comparison.comparison.patch_coverage_base_commitid + == comparison.project_coverage_base.commit.commitid + ) + + # In this test case, the adjusted and original base commits are the + # same. If we get one, we should set the cache for the other. + adjusted_base_result = comparison.get_diff(use_original_base=True) + assert comparison._adjusted_base_diff == "magic string" + + # Make sure we only called the Git provider API once + assert mock_get_compare.call_args_list == [ + call( + comparison.comparison.patch_coverage_base_commitid, + comparison.head.commit.commitid, + with_commits=False, + ), + ] diff --git a/apps/worker/services/comparison/tests/unit/test_get_or_create_comparison.py b/apps/worker/services/comparison/tests/unit/test_get_or_create_comparison.py new file mode 100644 index 0000000000..ba1f0571d6 --- /dev/null +++ b/apps/worker/services/comparison/tests/unit/test_get_or_create_comparison.py @@ -0,0 +1,29 @@ +from database.enums import CompareCommitState +from database.tests.factories.core import CommitFactory, CompareCommitFactory +from services.comparison import get_or_create_comparison + + +class TestGetOrCreateComparison(object): + def test_get_or_create_existing_comparison(self, dbsession): + existing_comparison = CompareCommitFactory.create() + dbsession.add(existing_comparison) + dbsession.flush() + + comparison = get_or_create_comparison( + dbsession, + existing_comparison.base_commit, + existing_comparison.compare_commit, + ) + assert comparison == existing_comparison + assert comparison.state == CompareCommitState.pending.value + assert comparison.error is None + + def test_get_or_create_new_comparison(self, dbsession): + base_commit = CommitFactory() + commit = CommitFactory() + dbsession.commit() + comparison = get_or_create_comparison(dbsession, base_commit, commit) + dbsession.flush() + assert comparison.state == CompareCommitState.pending.value + assert comparison.base_commit == base_commit + assert comparison.compare_commit == commit diff --git a/apps/worker/services/comparison/tests/unit/test_reports_uploaded_count_diff.py b/apps/worker/services/comparison/tests/unit/test_reports_uploaded_count_diff.py new file mode 100644 index 0000000000..a38b181f55 --- /dev/null +++ b/apps/worker/services/comparison/tests/unit/test_reports_uploaded_count_diff.py @@ -0,0 +1,143 @@ +from unittest.mock import MagicMock + +import pytest +from shared.reports.resources import Report +from shared.utils.sessions import Session, SessionType +from shared.yaml import UserYaml + +from services.comparison import ComparisonProxy +from services.comparison.types import Comparison, FullCommit, ReportUploadedCount + + +@pytest.mark.parametrize( + "head_sessions, base_sessions, expected_count, expected_diff", + [ + ( + { + 0: Session( + flags=["unit", "local"], session_type=SessionType.carriedforward + ), + 1: Session(flags=["integration"], session_type=SessionType.uploaded), + 2: Session(flags=["unit"], session_type=SessionType.uploaded), + 3: Session(flags=["unit"], session_type=SessionType.uploaded), + 4: Session(flags=["integration"], session_type=SessionType.uploaded), + 5: Session(flags=[], session_type=SessionType.uploaded), + }, + { + 0: Session( + flags=["unit", "local"], session_type=SessionType.carriedforward + ), + 1: Session( + flags=["integration"], session_type=SessionType.carriedforward + ), + 2: Session(flags=["unit"], session_type=SessionType.uploaded), + 3: Session(flags=["unit"], session_type=SessionType.uploaded), + }, + [ + ReportUploadedCount(flag="unit", base_count=2, head_count=2), + ReportUploadedCount(flag="integration", base_count=0, head_count=2), + ReportUploadedCount(flag="", base_count=0, head_count=1), + ], + [], + ), + ( + { + 0: Session( + flags=["unit", "local"], session_type=SessionType.carriedforward + ), + 1: Session(flags=["integration"], session_type=SessionType.uploaded), + 2: Session(flags=["unit"], session_type=SessionType.uploaded), + 3: Session(flags=["unit"], session_type=SessionType.uploaded), + 4: Session(flags=["integration"], session_type=SessionType.uploaded), + 5: Session(flags=[""], session_type=SessionType.uploaded), + }, + { + 0: Session(flags=["unit", "local"], session_type=SessionType.uploaded), + 1: Session(flags=["integration"], session_type=SessionType.uploaded), + 2: Session(flags=["unit"], session_type=SessionType.uploaded), + 3: Session(flags=["unit"], session_type=SessionType.uploaded), + 4: Session(flags=["obscure_flag"], session_type=SessionType.uploaded), + }, + [ + ReportUploadedCount(flag="unit", base_count=3, head_count=2), + ReportUploadedCount(flag="local", base_count=1, head_count=0), + ReportUploadedCount(flag="integration", base_count=1, head_count=2), + ReportUploadedCount(flag="obscure_flag", base_count=1, head_count=0), + ReportUploadedCount(flag="", base_count=0, head_count=1), + ], + [ + ReportUploadedCount(flag="unit", base_count=3, head_count=2), + ReportUploadedCount(flag="obscure_flag", base_count=1, head_count=0), + ], + ), + ( + {0: Session(flags=[], session_type=SessionType.uploaded)}, + { + 0: Session(flags=[], session_type=SessionType.uploaded), + 1: Session(flags=[], session_type=SessionType.uploaded), + }, + [ReportUploadedCount(flag="", base_count=2, head_count=1)], + [ReportUploadedCount(flag="", base_count=2, head_count=1)], + ), + ], + ids=[ + "flag_counts_no_diff", + "flag_count_yes_diff", + "diff_from_session_with_no_flags", + ], +) +def test_get_reports_uploaded_count_per_flag( + head_sessions, base_sessions, expected_count, expected_diff, mock_configuration +): + head_report = Report() + head_report.sessions = head_sessions + base_report = Report() + base_report.sessions = base_sessions + comparison_proxy = ComparisonProxy( + comparison=Comparison( + head=FullCommit(report=head_report, commit=None), + project_coverage_base=FullCommit(report=base_report, commit=None), + patch_coverage_base_commitid=None, + enriched_pull=None, + current_yaml=UserYaml( + { + "flag_management": { + "default_rules": {"carryforward": True}, + "individual_flags": [ + {"name": "obscure_flag", "carryforward": False} + ], + } + } + ), + ) + ) + # Python Dicts preserve order, so we can actually test this equality + # See more https://stackoverflow.com/a/39537308 + assert comparison_proxy.get_reports_uploaded_count_per_flag() == expected_count + assert comparison_proxy.get_reports_uploaded_count_per_flag_diff() == expected_diff + + +def test_get_reports_uploaded_count_per_flag_cached(): + comparison_proxy = ComparisonProxy(comparison=MagicMock(name="fake_comparison")) + comparison_proxy._cached_reports_uploaded_per_flag = ( + "object_that_doesnt_have_this_shape" + ) + assert ( + comparison_proxy.get_reports_uploaded_count_per_flag() + == "object_that_doesnt_have_this_shape" + ) + + +def test_get_reports_uploaded_count_per_flag_diff_missing_report(): + head_report = None + base_report = Report() + base_report.sessions = None + comparison_proxy = ComparisonProxy( + comparison=Comparison( + head=FullCommit(report=head_report, commit=None), + project_coverage_base=FullCommit(report=base_report, commit=None), + patch_coverage_base_commitid=None, + enriched_pull=None, + ) + ) + assert comparison_proxy.get_reports_uploaded_count_per_flag_diff() == [] diff --git a/apps/worker/services/comparison/types.py b/apps/worker/services/comparison/types.py new file mode 100644 index 0000000000..7f3afefcd3 --- /dev/null +++ b/apps/worker/services/comparison/types.py @@ -0,0 +1,61 @@ +from dataclasses import dataclass +from typing import Optional, TypedDict + +from shared.reports.readonly import ReadOnlyReport +from shared.yaml import UserYaml + +from database.models import Commit +from services.repository import EnrichedPull + + +@dataclass +class FullCommit(object): + commit: Commit + report: ReadOnlyReport + + +class ReportUploadedCount(TypedDict): + flag: str + base_count: int + head_count: int + + +@dataclass +class Comparison(object): + head: FullCommit + + # To see how a patch changes project coverage, we compare the branch head's + # report against the base's report, or if the base isn't in our database, + # the next-oldest commit that is. Be aware that this base commit may not be + # the true base that, for example, a PR is based on. + project_coverage_base: FullCommit + + # Computing patch coverage doesn't require an old report to compare against, + # so doing the "next-oldest" adjustment described above is unnecessary and + # makes the results less correct. All it requires is a head report and the + # patch diff, and the original base's commit SHA is enough to get that. + patch_coverage_base_commitid: str + + enriched_pull: EnrichedPull + current_yaml: Optional[UserYaml] = None + + # FIXME: The functions down below would not make sense given that we assume + # a `FullCommit` and its `report` to always exist. + # Which they don't, so these checks do make sense, contrary to declared types. + # Similarly, we also expect an `EnrichedPull` to exist, which does not match + # the reality. + + def has_project_coverage_base_report(self): + return bool( + self.project_coverage_base is not None + and self.project_coverage_base.report is not None + ) + + def has_head_report(self): + return bool(self.head is not None and self.head.report is not None) + + @property + def pull(self): + if self.enriched_pull is None: + return None + return self.enriched_pull.database_pull diff --git a/apps/worker/services/comparison_utils.py b/apps/worker/services/comparison_utils.py new file mode 100644 index 0000000000..52d592f1a3 --- /dev/null +++ b/apps/worker/services/comparison_utils.py @@ -0,0 +1,37 @@ +import sentry_sdk +from shared.reports.readonly import ReadOnlyReport + +from database.models import CompareCommit +from services.comparison import ComparisonContext, ComparisonProxy +from services.comparison.types import Comparison, FullCommit +from services.report import ReportService + + +@sentry_sdk.trace +def get_comparison_proxy( + comparison: CompareCommit, + report_service: ReportService, +): + compare_commit = comparison.compare_commit + base_commit = comparison.base_commit + + base_report = report_service.get_existing_report_for_commit( + base_commit, report_class=ReadOnlyReport + ) + compare_report = report_service.get_existing_report_for_commit( + compare_commit, report_class=ReadOnlyReport + ) + # No access to the PR so we have to assume the base commit did not need + # to be adjusted. + patch_coverage_base_commitid = base_commit.commitid + return ComparisonProxy( + Comparison( + head=FullCommit(commit=compare_commit, report=compare_report), + project_coverage_base=FullCommit(commit=base_commit, report=base_report), + patch_coverage_base_commitid=patch_coverage_base_commitid, + enriched_pull=None, + ), + context=ComparisonContext( + gh_app_installation_name=report_service.gh_app_installation_name + ), + ) diff --git a/apps/worker/services/decoration.py b/apps/worker/services/decoration.py new file mode 100644 index 0000000000..4aea8b79f3 --- /dev/null +++ b/apps/worker/services/decoration.py @@ -0,0 +1,170 @@ +import logging +from dataclasses import dataclass + +from shared.config import get_config +from shared.plan.service import PlanService +from shared.upload.utils import query_monthly_coverage_measurements + +from database.enums import Decoration +from database.models import Owner +from services.license import requires_license +from services.repository import EnrichedPull + +log = logging.getLogger(__name__) + +# For more context on PR decorations, see here: +# https://codecovio.atlassian.net/wiki/spaces/ENG/pages/34603058/PR+based+Billing+Refactor + + +BOT_USER_EMAILS = [ + "dependabot[bot]@users.noreply.github.com", + "29139614+renovate[bot]@users.noreply.github.com", + "157164994+sentry-autofix[bot]@users.noreply.github.com", +] +BOT_USER_IDS = ["29139614", "157164994"] # renovate[bot] github, sentry-autofix[bot] +USER_BASIC_LIMIT_UPLOAD = 250 + + +@dataclass +class DecorationDetails(object): + decoration_type: Decoration + reason: str + should_attempt_author_auto_activation: bool = False + activation_org_ownerid: int | None = None + activation_author_ownerid: int | None = None + + +def _is_bot_account(author: Owner) -> bool: + return author.email in BOT_USER_EMAILS or author.service_id in BOT_USER_IDS + + +def determine_uploads_used(plan_service: PlanService) -> int: + # This query takes an absurdly long time to run and in some environments we + # would like to disable it + if not get_config("setup", "upload_throttling_enabled", default=True): + return 0 + + return query_monthly_coverage_measurements(plan_service=plan_service) + + +def determine_decoration_details( + enriched_pull: EnrichedPull, empty_upload=None +) -> DecorationDetails: + """ + Determine the decoration details from pull information. We also check if the pull author needs to be activated + + Returns: + DecorationDetails: the decoration type and reason along with whether auto-activation of the author should be attempted + """ + if enriched_pull: + db_pull = enriched_pull.database_pull + provider_pull = enriched_pull.provider_pull + + if not provider_pull: + return DecorationDetails( + decoration_type=Decoration.standard, + reason="Can't determine PR author - no pull info from provider", + ) + if empty_upload == "pass": + return DecorationDetails( + decoration_type=Decoration.passing_empty_upload, + reason="Non testable files got changed.", + ) + + if empty_upload == "fail": + return DecorationDetails( + decoration_type=Decoration.failing_empty_upload, + reason="Testable files got changed.", + ) + + if empty_upload == "processing": + return DecorationDetails( + decoration_type=Decoration.processing_upload, + reason="Upload is still processing.", + ) + + if db_pull.repository.private is False: + # public repo or repo we aren't certain is private should be standard + return DecorationDetails( + decoration_type=Decoration.standard, reason="Public repo" + ) + + org = db_pull.repository.owner + + db_session = db_pull.get_db_session() + + # do not access plan directly - only through PlanService + org_plan = PlanService(current_org=org) + # use the org that has the plan - for GL this is the root_org rather than the repository.owner org + org = org_plan.current_org + + if not org_plan.is_pr_billing_plan: + return DecorationDetails( + decoration_type=Decoration.standard, reason="Org not on PR plan" + ) + + pr_author = ( + db_session.query(Owner) + .filter( + Owner.service == org.service, + Owner.service_id == provider_pull["author"]["id"], + ) + .first() + ) + + if not pr_author: + log.info( + "PR author not found in database", + extra=dict( + author_service=org.service, + author_service_id=provider_pull["author"]["id"], + author_username=provider_pull["author"]["username"], + ), + ) + return DecorationDetails( + decoration_type=Decoration.upgrade, + reason="PR author not found in database", + ) + + monthly_limit = org_plan.monthly_uploads_limit + if monthly_limit is not None: + uploads_used = determine_uploads_used(plan_service=org_plan) + + if ( + uploads_used >= org_plan.monthly_uploads_limit + and not requires_license() + ): + return DecorationDetails( + decoration_type=Decoration.upload_limit, + reason="Org has exceeded the upload limit", + ) + + if ( + org.plan_activated_users is not None + and pr_author.ownerid in org.plan_activated_users + ): + return DecorationDetails( + decoration_type=Decoration.standard, + reason="User is currently activated", + ) + + if _is_bot_account(pr_author): + return DecorationDetails( + decoration_type=Decoration.standard, + reason="Bot user detected (does not need to be activated)", + ) + + if not org.plan_auto_activate: + return DecorationDetails( + decoration_type=Decoration.upgrade, + reason="User must be manually activated", + ) + else: + return DecorationDetails( + decoration_type=Decoration.upgrade, + reason="User must be activated", + should_attempt_author_auto_activation=True, + activation_org_ownerid=org.ownerid, + activation_author_ownerid=pr_author.ownerid, + ) + return DecorationDetails(decoration_type=Decoration.standard, reason="No pull") diff --git a/apps/worker/services/encryption.py b/apps/worker/services/encryption.py new file mode 100644 index 0000000000..8fc320865e --- /dev/null +++ b/apps/worker/services/encryption.py @@ -0,0 +1,3 @@ +from shared.encryption.oauth import get_encryptor_from_configuration + +encryptor = get_encryptor_from_configuration() diff --git a/apps/worker/services/failure_normalizer.py b/apps/worker/services/failure_normalizer.py new file mode 100644 index 0000000000..3a92288440 --- /dev/null +++ b/apps/worker/services/failure_normalizer.py @@ -0,0 +1,112 @@ +from typing import List, Optional + +import regex +import sentry_sdk + +predefined_dict_of_regexes_to_match = { + "UUID": [ + r"[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}" + ], + "DATETIME": [ + r"(?:19|20)[0-9]{2}-(?:(?:0?[0-9])|(?:1[012]))-(?:(?:0?[0-9])|1[0-9]|2[0-9]|3[01])T(?:0[0-9]|1[0-9]|2[0-3]):(?:[0-5][0-9]):(?:[0-5][0-9])(?:Z|(?:-(?:0[0-9]:[03]0)))?", + r"(?:19|20)[0-9]{2}(?:(?:0?[0-9])|(?:1[012]))(?:(?:0?[0-9])|1[0-9]|2[0-9]|3[01])T(?:0[0-9]|1[0-9]|2[0-3])(?:[0-5][0-9])(?:[0-5][0-9])(?:Z|(?:-(?:0[0-9]:[03]0)))?", + ], + "DATE": [ + r"(?:19|20)[0-9]{2}-(?:(?:0?[0-9])|(?:1[012]))-(?:(?:0?[0-9])\b|1[0-9]|2[0-9]|3[01])" + ], + "TIME": [ + r"(?:0[0-9]|1[0-9]|2[0-3]):(?:[0-5][0-9]):(?:[0-5][0-9])Z?", + r"T(?:0[0-9]|1[0-9]|2[0-3])(?:[0-5][0-9])(?:[0-5][0-9])Z?", + ], + "URL": [ + r"[a-z]+:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)" + ], + "FILEPATH": [r"\/?[a-zA-Z0-9-_]+(\/[a-zA-Z0-9-_]+)+(?=\/([a-zA-Z0-9-_]+\/){2})"], + "LINENO": [r":\d+:\d*"], + "HEXNUMBER": [r"0?x[A-Fa-f0-9]+\b"], + "HASH": [r"[0-9a-fA-F\-]{30}[0-9a-fA-F\-]*"], + "NO": [r"[+-]?\d+(\.\d+)?\b"], +} + + +class FailureNormalizer: + """ + Class for normalizing a failure message + + Takes a dict of strings to strings where the key is the replacement string and the value is + the regex we want to match and replace occurences of and a boolean that toggles whether we + should ignore the predefined list of patterns in the constructor + + The normalize_failure_message method on the class takes a string as a parameter + it will replace all occurences of a match of the regexes specified in the list + with the keys that map to that regex, in that string + + If the users + + Usage: + + dict_of_regex_strings = [ + "DATE": r"(\\d{4}-\\d{2}-\\d{2})", + ] + + f = FailureNormalizer(dict_of_regex_strings) + s = ''' + abcdefAB-1234-1234-1234-abcdef123456 test_1 abcdefAB-1234-1234-1234-abcdef123456 test_2 2024-03-09 + test_3 abcdefAB-5678-5678-5678-abcdef123456 2024-03-10 test_4 + 2024-03-10 + ''' + + f.normalize_failure_message(s) + + will give: + + ''' + UUID test_1 UUID test_2 DATE + test_3 UUID DATE test_4 + DATE + ''' + """ + + def __init__( + self, + user_dict_of_regex_strings: dict[str, list[str]], + ignore_predefined=False, + override_predefined=False, + *, + key_analysis_order: Optional[List[str]] = None, + ): + flags = regex.MULTILINE + + self.dict_of_regex = dict() + self.key_analysis_order = key_analysis_order + + if not ignore_predefined: + dict_of_list_of_regex_string = dict(predefined_dict_of_regexes_to_match) + for key, user_regex_string in user_dict_of_regex_strings.items(): + if not override_predefined and key in dict_of_list_of_regex_string: + dict_of_list_of_regex_string[key] = ( + user_regex_string + dict_of_list_of_regex_string[key] + ) + else: + dict_of_list_of_regex_string[key] = user_regex_string + else: + dict_of_list_of_regex_string = dict(user_dict_of_regex_strings) + + for key, list_of_regex_string in dict_of_list_of_regex_string.items(): + self.dict_of_regex[key] = [ + regex.compile(regex_string, flags=flags) + for regex_string in list_of_regex_string + ] + + @sentry_sdk.trace + def normalize_failure_message(self, failure_message: str): + key_ordering = self.key_analysis_order or self.dict_of_regex.keys() + for key in key_ordering: + list_of_compiled_regex = self.dict_of_regex[key] + for compiled_regex in list_of_compiled_regex: + for match_obj in compiled_regex.finditer(failure_message): + actual_match = match_obj.group() + # Limit number of replaces to 1 so one match doesn't interfere + # With future matches + failure_message = failure_message.replace(actual_match, key, 1) + return failure_message diff --git a/apps/worker/services/github.py b/apps/worker/services/github.py new file mode 100644 index 0000000000..e6cd431eeb --- /dev/null +++ b/apps/worker/services/github.py @@ -0,0 +1,79 @@ +import logging +from typing import Optional + +from redis import RedisError +from shared.github import InvalidInstallationError +from shared.github import get_github_integration_token as _get_github_integration_token +from shared.helpers.redis import get_redis_connection + +from database.models.core import Commit +from helpers.exceptions import RepositoryWithoutValidBotError + +log = logging.getLogger(__name__) + + +def get_github_integration_token( + service: str, + installation_id: int = None, + app_id: Optional[str] = None, + pem_path: Optional[str] = None, +): + try: + return _get_github_integration_token( + service, integration_id=installation_id, app_id=app_id, pem_path=pem_path + ) + except InvalidInstallationError: + log.warning("Failed to get installation token") + raise RepositoryWithoutValidBotError() + + +def COMMIT_GHAPP_KEY_NAME(commit_id): + return f"app_to_use_for_commit_{commit_id}" + + +GHAPP_KEY_EXPIRY_SECONDS = 60 * 60 * 2 # 2h + + +def set_github_app_for_commit( + installation_id: str | int | None, commit: Commit +) -> bool: + """Sets a GithubAppInstallation.id in Redis as the installation to use for a commit. + Keys live in redis for GHAPP_KEY_EXPIRY_SECONDS before being expired. + + Args: + installation_id (str | int | None) - the ID to save. + None -- there was actually no installation ID. Do nothing. + int -- value comes from the Database + str -- value comes from Redis (i.e. the installation was already cached) + commit (Commit) - the commit to attach installation_id to + """ + if installation_id is None: + return False + redis = get_redis_connection() + try: + redis.set( + COMMIT_GHAPP_KEY_NAME(commit.id), + str(installation_id), + ex=GHAPP_KEY_EXPIRY_SECONDS, + ) + return True + except RedisError: + log.exception( + "Failed to set app for commit", extra=dict(commit=commit.commitid) + ) + return False + + +def get_github_app_for_commit(commit: Commit) -> str | None: + if commit.repository.service not in ["github", "github_enterprise"]: + # Because this feature is GitHub-exclusive we can skip checking for other providers + return None + redis = get_redis_connection() + try: + value = redis.get(COMMIT_GHAPP_KEY_NAME(commit.id)) + return value if value is None else value.decode() + except RedisError: + log.exception( + "Failed to get app for commit", extra=dict(commit=commit.commitid) + ) + return None diff --git a/apps/worker/services/github_marketplace.py b/apps/worker/services/github_marketplace.py new file mode 100644 index 0000000000..f83f85c6e0 --- /dev/null +++ b/apps/worker/services/github_marketplace.py @@ -0,0 +1,120 @@ +import logging + +import requests +import shared.torngit as torngit +from shared.config import get_config + +from services.github import get_github_integration_token + +log = logging.getLogger(__name__) + + +class GitHubMarketplaceService(object): + """ + Static ids for each of Codecov's plans on GitHub marketplace + """ + + LEGACY_PLAN_ID = 18 + CURRENT_PLAN_ID = 2147 + PER_USER_PLAN_ID = 3267 + + def __init__(self): + self._token = None + self.use_stubbed = get_config( + "services", "github_marketplace", "use_stubbed", default=False + ) + + def api( + self, + method, + url, + body=None, + headers=None, + params=None, + auth_with_integration_token=True, + **args, + ): + _headers = { + "Accept": "application/vnd.github.valkyrie-preview+json", + "User-Agent": "Codecov", + } + if auth_with_integration_token: + _headers["Authorization"] = f"Bearer {self.get_integration_token()}" + _headers.update(headers or {}) + + method = (method or "GET").upper() + + if url.startswith("/"): + base_url = torngit.Github.get_api_url() + url = base_url + url + + if self.use_stubbed: + # use stubbed endpoints for testing + # https://developer.github.com/v3/apps/marketplace/#testing-with-stubbed-endpoints + url = url.replace("marketplace_listing/", "marketplace_listing/stubbed/", 1) + + res = requests.request(method, url, headers=_headers, params=params) + try: + res.raise_for_status() + except requests.exceptions.HTTPError: + log.exception( + "Github Marketplace Service Error", + extra=dict(code=res.status_code, text=res.text), + ) + raise + return res.json() + + def get_integration_token(self): + """ + Get GitHub app token + """ + if not self._token: + self._token = get_github_integration_token("github") + + return self._token + + @property + def plan_ids(self): + return [self.LEGACY_PLAN_ID, self.CURRENT_PLAN_ID, self.PER_USER_PLAN_ID] + + def get_account_plans(self, account_id): + """ + Check if a GitHub account is associated with any Marketplace listing. + + Shows whether the user or organization account actively subscribes to a + Codecov plan. When someone submits a plan change that won't be processed until + the end of their billing cycle, you will also see the upcoming pending change. + """ + return self.api("get", "/marketplace_listing/accounts/{}".format(account_id)) + + def get_codecov_plans(self): + """ + List all plans for Codecov Marketplace listing + """ + return self.api("get", "/marketplace_listing/plans") + + def get_plan_accounts(self, page, plan_id): + """ + List all GitHub accounts (user or organization) on a specific plan. + """ + params = dict(page=page) + return self.api( + "get", + "/marketplace_listing/plans/{}/accounts".format(plan_id), + params=params, + ) + + def get_user(self, service_id): + """ + Get GitHub user details + """ + params = dict( + client_id=get_config("github", "client_id"), + client_secret=get_config("github", "client_secret"), + ) + return self.api( + "get", + "/user/{}".format(service_id), + params=params, + auth_with_integration_token=False, + ) diff --git a/apps/worker/services/license.py b/apps/worker/services/license.py new file mode 100644 index 0000000000..f2bdf65581 --- /dev/null +++ b/apps/worker/services/license.py @@ -0,0 +1,114 @@ +import logging +from datetime import datetime +from enum import Enum, auto +from functools import lru_cache +from typing import Optional + +from shared.config import get_config +from shared.license import get_current_license +from sqlalchemy import func +from sqlalchemy.sql import text + +from database.models import Owner, Repository +from helpers.environment import is_enterprise + +log = logging.getLogger(__name__) + + +class InvalidLicenseReason(Enum): + invalid = auto() + no_license = auto() + unknown = auto() + expired = auto() + demo_mode = auto() + url_mismatch = auto() + users_exceeded = auto() + repos_exceeded = auto() + + +def is_properly_licensed(db_session) -> bool: + return not requires_license() or has_valid_license(db_session) + + +def requires_license() -> bool: + return is_enterprise() + + +def _get_now() -> datetime: + return datetime.now() + + +def has_valid_license(db_session) -> bool: + return reason_for_not_being_valid(db_session) is None + + +def reason_for_not_being_valid(db_session) -> Optional[InvalidLicenseReason]: + return cached_reason_for_not_being_valid(db_session) + + +@lru_cache() +def cached_reason_for_not_being_valid(db_session) -> Optional[InvalidLicenseReason]: + return calculate_reason_for_not_being_valid(db_session) + + +def get_installation_plan_activated_users(db_session) -> list: + query_string = text( + """ + WITH all_plan_activated_users AS ( + SELECT DISTINCT + UNNEST(o.plan_activated_users) AS activated_owner_id + FROM owners o + ) SELECT count(*) as count + FROM all_plan_activated_users""" + ) + return db_session.execute(query_string).fetchall() + + +def calculate_reason_for_not_being_valid(db_session) -> Optional[InvalidLicenseReason]: + current_license = get_current_license() + if not current_license.is_valid: + return InvalidLicenseReason.invalid + if current_license.url: + if get_config("setup", "codecov_url") != current_license.url: + return InvalidLicenseReason.url_mismatch + + if current_license.number_allowed_users: + if current_license.is_pr_billing: + # PR Billing must count _all_ plan_activated_users in db + query = get_installation_plan_activated_users(db_session) + else: + # non PR billing must count all owners with oauth_token != None. + query = ( + db_session.query(func.count(), Owner.service) + .filter(Owner.oauth_token.isnot(None)) + .group_by(Owner.service) + .all() + ) + for result in query: + if result[0] > current_license.number_allowed_users: + return InvalidLicenseReason.users_exceeded + elif result[0] > (0.9 * current_license.number_allowed_users): + log.warning( + "Number of users is approaching license limit of %d/%d", + result[0], + current_license.number_allowed_users, + ) + if current_license.number_allowed_repos: + repos = ( + db_session.query(func.count()) + .select_from(Repository) + .filter(Repository.updatestamp.isnot(None)) + .first()[0] + ) + if repos > current_license.number_allowed_repos: + return InvalidLicenseReason.repos_exceeded + elif repos > (current_license.number_allowed_repos * 0.85): + log.warning( + "Number of repositories is approaching license limit of %d/%d", + repos, + current_license.number_allowed_repos, + ) + if current_license.expires: + if current_license.expires < _get_now(): + return InvalidLicenseReason.expired + return None diff --git a/apps/worker/services/lock_manager.py b/apps/worker/services/lock_manager.py new file mode 100644 index 0000000000..189ccea181 --- /dev/null +++ b/apps/worker/services/lock_manager.py @@ -0,0 +1,102 @@ +import logging +import random +from contextlib import contextmanager +from enum import Enum +from typing import Optional + +from redis import Redis +from redis.exceptions import LockError +from shared.helpers.redis import get_redis_connection + +from database.enums import ReportType + +log = logging.getLogger(__name__) + + +class LockType(Enum): + BUNDLE_ANALYSIS_PROCESSING = "bundle_analysis_processing" + BUNDLE_ANALYSIS_NOTIFY = "bundle_analysis_notify" + NOTIFICATION = "notify" + # TODO: port existing task locking to use `LockManager` + + +class LockRetry(Exception): + def __init__(self, countdown: int): + self.countdown = countdown + + +class LockManager: + def __init__( + self, + repoid: int, + commitid: str, + report_type=ReportType.COVERAGE, + lock_timeout=300, # 5 min + redis_connection: Optional[Redis] = None, + ): + self.repoid = repoid + self.commitid = commitid + self.report_type = report_type + self.lock_timeout = lock_timeout + self.redis_connection = redis_connection or get_redis_connection() + + def lock_name(self, lock_type: LockType): + if self.report_type == ReportType.COVERAGE: + # for backward compat this does not include the report type + return f"{lock_type.value}_lock_{self.repoid}_{self.commitid}" + else: + return f"{lock_type.value}_lock_{self.repoid}_{self.commitid}_{self.report_type.value}" + + def is_locked(self, lock_type: LockType) -> bool: + lock_name = self.lock_name(lock_type) + if self.redis_connection.get(lock_name): + return True + return False + + @contextmanager + def locked(self, lock_type: LockType, retry_num=0): + lock_name = self.lock_name(lock_type) + try: + log.info( + "Acquiring lock", + extra=dict( + repoid=self.repoid, + commitid=self.commitid, + lock_name=lock_name, + ), + ) + with self.redis_connection.lock( + lock_name, timeout=self.lock_timeout, blocking_timeout=5 + ): + log.info( + "Acquired lock", + extra=dict( + repoid=self.repoid, + commitid=self.commitid, + lock_name=lock_name, + ), + ) + yield + log.info( + "Releasing lock", + extra=dict( + repoid=self.repoid, + commitid=self.commitid, + lock_name=lock_name, + ), + ) + except LockError: + max_retry = 200 * 3**retry_num + countdown = min(random.randint(max_retry // 2, max_retry), 60 * 60 * 5) + + log.warning( + "Unable to acquire lock", + extra=dict( + repoid=self.repoid, + commitid=self.commitid, + lock_name=lock_name, + countdown=countdown, + retry_num=retry_num, + ), + ) + raise LockRetry(countdown) diff --git a/apps/worker/services/notification/__init__.py b/apps/worker/services/notification/__init__.py new file mode 100644 index 0000000000..853ec3d003 --- /dev/null +++ b/apps/worker/services/notification/__init__.py @@ -0,0 +1,377 @@ +"""Notification system + +This packages uses the following services: + - comparison + +""" + +import logging +from typing import Iterator, List, Optional, TypedDict + +from celery.exceptions import CeleryError, SoftTimeLimitExceeded +from shared.config import get_config +from shared.django_apps.codecov_auth.models import Plan +from shared.helpers.yaml import default_if_true +from shared.plan.constants import TierName +from shared.torngit.base import TorngitBaseAdapter +from shared.yaml import UserYaml + +from database.enums import notification_type_status_or_checks +from database.models.core import GITHUB_APP_INSTALLATION_DEFAULT_NAME, Owner, Repository +from services.comparison import ComparisonProxy +from services.decoration import Decoration +from services.license import is_properly_licensed +from services.notification.commit_notifications import ( + create_or_update_commit_notification_from_notification_result, +) +from services.notification.notifiers import ( + StatusType, + get_all_notifier_classes_mapping, + get_pull_request_notifiers, + get_status_notifier_class, +) +from services.notification.notifiers.base import ( + AbstractBaseNotifier, + NotificationResult, +) +from services.notification.notifiers.checks.checks_with_fallback import ( + ChecksWithFallback, +) +from services.notification.notifiers.codecov_slack_app import CodecovSlackAppNotifier +from services.notification.notifiers.mixins.status import StatusState +from services.yaml import read_yaml_field +from services.yaml.reader import get_components_from_yaml + +log = logging.getLogger(__name__) + + +class IndividualResult(TypedDict): + notifier: str + title: str + result: NotificationResult | None + + +class NotificationService(object): + def __init__( + self, + repository: Repository, + current_yaml: UserYaml, + repository_service: TorngitBaseAdapter, + decoration_type=Decoration.standard, + gh_installation_name_to_use: str = GITHUB_APP_INSTALLATION_DEFAULT_NAME, + ) -> None: + self.repository = repository + self.current_yaml = current_yaml + self.decoration_type = decoration_type + self.repository_service = repository_service + self.gh_installation_name_to_use = gh_installation_name_to_use + self.plan = None # used for caching the plan / tier information + + def _should_use_status_notifier(self, status_type: StatusType) -> bool: + owner: Owner = self.repository.owner + + if not self.plan: + self.plan = Plan.objects.select_related("tier").get(name=owner.plan) + + if ( + self.plan.tier.tier_name == TierName.TEAM.value + and status_type != StatusType.PATCH.value + ): + return False + + return True + + def _should_use_checks_notifier(self, status_type: StatusType) -> bool: + checks_yaml_field = read_yaml_field(self.current_yaml, ("github_checks",)) + if checks_yaml_field is False: + return False + + owner: Owner = self.repository.owner + if owner.service not in ["github", "github_enterprise"]: + return False + + if not self.plan: + self.plan = Plan.objects.select_related("tier").get(name=owner.plan) + + if ( + self.plan.tier.tier_name == TierName.TEAM.value + and status_type != StatusType.PATCH.value + ): + return False + + app_installation_filter = filter( + lambda obj: ( + obj.name == self.gh_installation_name_to_use and obj.is_configured() + ), + owner.github_app_installations or [], + ) + # filter is an Iterator, so we need to scan matches + for app_installation in app_installation_filter: + if app_installation.is_repo_covered_by_integration(self.repository): + return True + # DEPRECATED FLOW + return ( + self.repository.using_integration + and self.repository.owner.integration_id + and (self.repository.owner.service in ["github", "github_enterprise"]) + ) + + def _use_status_and_possibly_checks_notifiers( + self, + key: StatusType, + title: str, + status_config: dict, + ) -> AbstractBaseNotifier: + status_notifier_class = get_status_notifier_class(key, "status") + if self._should_use_checks_notifier(status_type=key): + checks_notifier = get_status_notifier_class(key, "checks") + return ChecksWithFallback( + checks_notifier=checks_notifier( + repository=self.repository, + title=title, + notifier_yaml_settings=status_config, + notifier_site_settings={}, + current_yaml=self.current_yaml, + repository_service=self.repository_service, + decoration_type=self.decoration_type, + ), + status_notifier=status_notifier_class( + repository=self.repository, + title=title, + notifier_yaml_settings=status_config, + notifier_site_settings={}, + current_yaml=self.current_yaml, + repository_service=self.repository_service, + decoration_type=self.decoration_type, + ), + ) + else: + return status_notifier_class( + repository=self.repository, + title=title, + notifier_yaml_settings=status_config, + notifier_site_settings={}, + current_yaml=self.current_yaml, + repository_service=self.repository_service, + decoration_type=self.decoration_type, + ) + + def get_notifiers_instances(self) -> Iterator[AbstractBaseNotifier]: + mapping = get_all_notifier_classes_mapping() + yaml_field = read_yaml_field(self.current_yaml, ("coverage", "notify")) + if yaml_field is not None: + for instance_type, instance_configs in yaml_field.items(): + class_to_use = mapping.get(instance_type) + if not class_to_use: + continue + for title, individual_config in instance_configs.items(): + yield class_to_use( + repository=self.repository, + title=title, + notifier_yaml_settings=individual_config, + notifier_site_settings=get_config( + "services", "notifications", instance_type, default=True + ), + current_yaml=self.current_yaml, + repository_service=self.repository_service, + decoration_type=self.decoration_type, + ) + + current_flags = [rf.flag_name for rf in self.repository.flags if not rf.deleted] + for key, title, status_config in self.get_statuses(current_flags): + if self._should_use_status_notifier(status_type=key): + yield self._use_status_and_possibly_checks_notifiers( + key=key, + title=title, + status_config=status_config, + ) + + # yield notifier if slack_app field is True, nonexistent, or a non-empty dict + slack_app_yaml_field = get_config( + "setup", "slack_app", default=True + ) and read_yaml_field(self.current_yaml, ("slack_app",), True) + if slack_app_yaml_field: + yield CodecovSlackAppNotifier( + repository=self.repository, + title="codecov-slack-app", + notifier_yaml_settings=slack_app_yaml_field, + notifier_site_settings={}, + current_yaml=self.current_yaml, + repository_service=self.repository_service, + decoration_type=self.decoration_type, + ) + + comment_yaml_field = read_yaml_field(self.current_yaml, ("comment",)) + if comment_yaml_field: + for pull_notifier_class in get_pull_request_notifiers(): + yield pull_notifier_class( + repository=self.repository, + title="comment", + notifier_yaml_settings=comment_yaml_field, + notifier_site_settings={}, + current_yaml=self.current_yaml, + repository_service=self.repository_service, + decoration_type=self.decoration_type, + ) + + def _get_component_statuses(self, current_flags: List[str]): + all_components = get_components_from_yaml(self.current_yaml) + for component in all_components: + for status in component.statuses: + if not status.get( + "enabled", True + ): # All defined statuses enabled by default + continue + n_st = { + "flags": component.get_matching_flags(current_flags), + "paths": component.paths, + **status, + } + yield ( + status["type"], + f"{status.get('name_prefix', '')}{component.get_display_name()}", + n_st, + ) + + def get_statuses(self, current_flags: List[str]): + status_fields = read_yaml_field(self.current_yaml, ("coverage", "status")) + # Default statuses + if status_fields: + for key, value in status_fields.items(): + if key in ["patch", "project", "changes"]: + for title, status_config in default_if_true(value): + yield (key, title, status_config) + # Flag based statuses + for f_name in current_flags: + flag_configuration = self.current_yaml.get_flag_configuration(f_name) + if flag_configuration and flag_configuration.get("enabled", True): + for st in flag_configuration.get("statuses", []): + n_st = {"flags": [f_name], **st} + yield (st["type"], f"{st.get('name_prefix', '')}{f_name}", n_st) + # Component based statuses + for component_status in self._get_component_statuses(current_flags): + yield component_status + + def notify(self, comparison: ComparisonProxy) -> list[IndividualResult]: + if not is_properly_licensed(comparison.head.commit.get_db_session()): + log.warning( + "Not sending notifications because the system is not properly licensed" + ) + return [] + log.debug( + f"Notifying with decoration type {self.decoration_type}", + extra=dict( + head_commit=comparison.head.commit.commitid, + base_commit=( + comparison.project_coverage_base.commit.commitid + if comparison.project_coverage_base.commit is not None + else "NO_BASE" + ), + repoid=comparison.head.commit.repoid, + ), + ) + + status_or_checks_notifiers, all_other_notifiers = split_notifiers( + notifier + for notifier in self.get_notifiers_instances() + if notifier.is_enabled() + ) + + results = [ + self.notify_individual_notifier(notifier, comparison) + for notifier in status_or_checks_notifiers + ] + + status_or_checks_helper_text = {} + if results and all_other_notifiers: + # if the status/check fails, sometimes we want to add helper text to the message of the other notifications, + # to better surface that the status/check failed. + # so if there are status_and_checks_notifiers and all_other_notifiers, do the status_and_checks_notifiers first, + # look at the results of the checks, if any failed AND they are the type we have helper text for, + # add that text onto the other notifiers messages through status_or_checks_helper_text. + for _notifier, result in results: + if result is not None and result.data_sent is not None: + if ( + result.data_sent.get("state") == StatusState.failure.value + ) and result.data_sent.get("included_helper_text"): + status_or_checks_helper_text.update( + result.data_sent["included_helper_text"] + ) + + results.extend( + self.notify_individual_notifier( + notifier, + comparison, + status_or_checks_helper_text=status_or_checks_helper_text, + ) + for notifier in all_other_notifiers + ) + + return [ + IndividualResult( + notifier=notifier.name, title=notifier.title, result=result + ) + for notifier, result in results + ] + + def notify_individual_notifier( + self, + notifier: AbstractBaseNotifier, + comparison: ComparisonProxy, + status_or_checks_helper_text: Optional[dict[str, str]] = None, + ) -> tuple[AbstractBaseNotifier, NotificationResult | None]: + commit = comparison.head.commit + base_commit = comparison.project_coverage_base.commit + log_extra = { + "commit": commit.commitid, + "base_commit": base_commit.commitid + if base_commit is not None + else "NO_BASE", + "repoid": commit.repoid, + "notifier": notifier.name, + "notifier_title": notifier.title, + } + + log.info("Attempting individual notification", extra=log_extra) + res: NotificationResult | None = None + try: + res = notifier.notify( + comparison, status_or_checks_helper_text=status_or_checks_helper_text + ) + log_extra["result"] = res + + # TODO: The `CommentNotifier` is the only one implementing this method, + # so maybe we should just move it there + notifier.store_results(comparison, res) + + log.info("Individual notification done", extra=log_extra) + return notifier, res + except (CeleryError, SoftTimeLimitExceeded): + raise + except Exception: + log.exception("Individual notifier failed", extra=log_extra) + return notifier, res + finally: + if res is None or res.notification_attempted: + # only running if there is no result (indicating some exception) + # or there was an actual attempt + create_or_update_commit_notification_from_notification_result( + comparison, notifier, res + ) + + +def split_notifiers( + notifiers: Iterator[AbstractBaseNotifier], +) -> tuple[list[AbstractBaseNotifier], list[AbstractBaseNotifier]]: + "Splits the input notifiers based on whether they are a status/check, or other notifier." + + status_or_checks_notifiers = [] + all_other_notifiers = [] + + for notifier in notifiers: + if notifier.notification_type in notification_type_status_or_checks: + status_or_checks_notifiers.append(notifier) + else: + all_other_notifiers.append(notifier) + + return status_or_checks_notifiers, all_other_notifiers diff --git a/apps/worker/services/notification/commit_notifications.py b/apps/worker/services/notification/commit_notifications.py new file mode 100644 index 0000000000..75efc3adcd --- /dev/null +++ b/apps/worker/services/notification/commit_notifications.py @@ -0,0 +1,76 @@ +import logging + +from sqlalchemy.orm.session import Session + +from database.enums import NotificationState +from database.models import CommitNotification, Pull +from services.comparison import ComparisonProxy +from services.notification.notifiers.base import ( + AbstractBaseNotifier, + NotificationResult, +) + +log = logging.getLogger(__name__) + + +def create_or_update_commit_notification_from_notification_result( + comparison: ComparisonProxy, + notifier: AbstractBaseNotifier, + notification_result: NotificationResult | None, +) -> CommitNotification | None: + """Saves a CommitNotification entry in the database. + We save an entry in the following scenarios: + - We save all notification attempts for commits that are part of a PullRequest + - We save _successful_ notification attempt _with_ a github app + """ + pull: Pull | None = comparison.pull + not_pull = pull is None + not_head_commit = comparison.head is None or comparison.head.commit is None + not_github_app_info = ( + notification_result is None or notification_result.github_app_used is None + ) + failed = ( + notification_result is None + or notification_result.notification_successful == False + ) + if not_pull and (not_head_commit or not_github_app_info or failed): + return None + + commit = pull.get_head_commit() if pull else comparison.head.commit + if not commit: + log.warning("Head commit not found for pull", extra=dict(pull=pull)) + return None + + db_session: Session = commit.get_db_session() + + commit_notification = ( + db_session.query(CommitNotification) + .filter( + CommitNotification.commit_id == commit.id_, + CommitNotification.notification_type == notifier.notification_type, + ) + .first() + ) + + notification_state = ( + NotificationState.error if failed else NotificationState.success + ) + github_app_used = ( + notification_result.github_app_used if notification_result else None + ) + + if not commit_notification: + commit_notification = CommitNotification( + commit_id=commit.id_, + notification_type=notifier.notification_type, + decoration_type=notifier.decoration_type, + gh_app_id=github_app_used, + state=notification_state, + ) + db_session.add(commit_notification) + db_session.flush() + return commit_notification + + commit_notification.decoration_type = notifier.decoration_type + commit_notification.state = notification_state + return commit_notification diff --git a/apps/worker/services/notification/notifiers/__init__.py b/apps/worker/services/notification/notifiers/__init__.py new file mode 100644 index 0000000000..ecc2e932b3 --- /dev/null +++ b/apps/worker/services/notification/notifiers/__init__.py @@ -0,0 +1,57 @@ +from enum import Enum +from typing import Dict, List, Type + +from services.notification.notifiers.base import AbstractBaseNotifier +from services.notification.notifiers.checks import ( + ChangesChecksNotifier, + PatchChecksNotifier, + ProjectChecksNotifier, +) +from services.notification.notifiers.comment import CommentNotifier +from services.notification.notifiers.gitter import GitterNotifier +from services.notification.notifiers.hipchat import HipchatNotifier +from services.notification.notifiers.irc import IRCNotifier +from services.notification.notifiers.slack import SlackNotifier +from services.notification.notifiers.status import ( + ChangesStatusNotifier, + PatchStatusNotifier, + ProjectStatusNotifier, +) +from services.notification.notifiers.webhook import WebhookNotifier + + +def get_all_notifier_classes_mapping() -> Dict[str, Type[AbstractBaseNotifier]]: + return { + "gitter": GitterNotifier, + "hipchat": HipchatNotifier, + "irc": IRCNotifier, + "slack": SlackNotifier, + "webhook": WebhookNotifier, + } + + +class StatusType(Enum): + PATCH = "patch" + PROJECT = "project" + CHANGES = "changes" + + +def get_status_notifier_class( + status_type: str, class_type: str = "status" +) -> Type[AbstractBaseNotifier]: + if status_type == StatusType.PATCH.value and class_type == "checks": + return PatchChecksNotifier + if status_type == StatusType.PROJECT.value and class_type == "checks": + return ProjectChecksNotifier + if status_type == StatusType.CHANGES.value and class_type == "checks": + return ChangesChecksNotifier + if status_type == StatusType.PATCH.value and class_type == "status": + return PatchStatusNotifier + if status_type == StatusType.PROJECT.value and class_type == "status": + return ProjectStatusNotifier + if status_type == StatusType.CHANGES.value and class_type == "status": + return ChangesStatusNotifier + + +def get_pull_request_notifiers() -> List[Type[AbstractBaseNotifier]]: + return [CommentNotifier] diff --git a/apps/worker/services/notification/notifiers/base.py b/apps/worker/services/notification/notifiers/base.py new file mode 100644 index 0000000000..19af03a070 --- /dev/null +++ b/apps/worker/services/notification/notifiers/base.py @@ -0,0 +1,119 @@ +import logging +from dataclasses import dataclass +from typing import Any, Mapping, Optional + +from shared.torngit.base import TorngitBaseAdapter +from shared.yaml import UserYaml + +from database.models import Repository +from services.comparison import ComparisonProxy +from services.decoration import Decoration + +log = logging.getLogger(__name__) + + +@dataclass +class NotificationResult(object): + notification_attempted: bool = False + notification_successful: bool = False + explanation: str | None = None + data_sent: Mapping[str, Any] | None = None + data_received: Mapping[str, Any] | None = None + github_app_used: int | None = None + + def merge(self, other: "NotificationResult") -> "NotificationResult": + ans = NotificationResult() + ans.notification_attempted = bool( + self.notification_attempted or other.notification_attempted + ) + ans.notification_successful = bool( + self.notification_successful or other.notification_successful + ) + ans.explanation = self.explanation or other.explanation + ans.data_sent = self.data_sent or other.data_sent + ans.data_received = self.data_received or other.data_received + return ans + + +class AbstractBaseNotifier(object): + """ + Base Notifier, abstract class that should not be used + + This class has the core ideas of a notifier that has the structure: + + notifications: + : + ... + + """ + + def __init__( + self, + repository: Repository, + title: str, + notifier_yaml_settings: Mapping[str, Any], + notifier_site_settings: Mapping[str, Any], + current_yaml: UserYaml, + repository_service: TorngitBaseAdapter, + decoration_type: Decoration | None = None, + ): + """ + :param repository: The repository notifications are being sent to. + :param title: The project name for this notification, if applicable. For more info see https://docs.codecov.io/docs/commit-status#splitting-up-projects-example + :param notifier_yaml_settings: Contains the codecov yaml fields, if any, for this particular notification. + example: status -> patch -> custom_project_name -> + :param notifier_site_settings: Contains the codecov yaml fields under the "notify" header + :param current_yaml: The complete codecov yaml used for this notification. + :param decoration_type: Indicates whether the user needs to upgrade their account before they can see the notification + """ + self.repository = repository + self.title = title + self.notifier_yaml_settings = notifier_yaml_settings + self.site_settings = notifier_site_settings + self.current_yaml = current_yaml + self.decoration_type = decoration_type + self.repository_service = repository_service + + @property + def name(self) -> str: + raise NotImplementedError() + + def notify( + self, + comparison: ComparisonProxy, + status_or_checks_helper_text: Optional[dict[str, str]] = None, + ) -> NotificationResult: + raise NotImplementedError() + + def is_enabled(self) -> bool: + raise NotImplementedError() + + def store_results(self, comparison: ComparisonProxy, result: NotificationResult): + """ + This function stores the result in the notification wherever it needs to be saved + This is the only function in this class allowed to have side-effects in the database + + Args: + comparison (Comparison): The comparison with which this notify ran + result (NotificationResult): The results of the notification + """ + raise NotImplementedError() + + def should_use_upgrade_decoration(self) -> bool: + return self.decoration_type == Decoration.upgrade + + def should_use_upload_limit_decoration(self) -> bool: + return self.decoration_type == Decoration.upload_limit + + def is_passing_empty_upload(self) -> bool: + return self.decoration_type == Decoration.passing_empty_upload + + def is_failing_empty_upload(self) -> bool: + return self.decoration_type == Decoration.failing_empty_upload + + def is_empty_upload(self) -> bool: + return self.is_passing_empty_upload() or self.is_failing_empty_upload() + + def is_processing_upload(self) -> bool: + return self.decoration_type == Decoration.processing_upload diff --git a/apps/worker/services/notification/notifiers/checks/__init__.py b/apps/worker/services/notification/notifiers/checks/__init__.py new file mode 100644 index 0000000000..71d313d2b6 --- /dev/null +++ b/apps/worker/services/notification/notifiers/checks/__init__.py @@ -0,0 +1,4 @@ +# ruff: noqa: F401 +from services.notification.notifiers.checks.changes import ChangesChecksNotifier +from services.notification.notifiers.checks.patch import PatchChecksNotifier +from services.notification.notifiers.checks.project import ProjectChecksNotifier diff --git a/apps/worker/services/notification/notifiers/checks/base.py b/apps/worker/services/notification/notifiers/checks/base.py new file mode 100644 index 0000000000..08ccd0b96f --- /dev/null +++ b/apps/worker/services/notification/notifiers/checks/base.py @@ -0,0 +1,469 @@ +import logging +from contextlib import nullcontext +from typing import Any, Optional, TypedDict + +import sentry_sdk +from asgiref.sync import async_to_sync +from shared.torngit.exceptions import TorngitClientError, TorngitError + +from services.comparison import ComparisonProxy, FilteredComparison +from services.notification.notifiers.base import NotificationResult +from services.notification.notifiers.mixins.status import StatusState +from services.notification.notifiers.status.base import StatusNotifier +from services.urls import ( + append_tracking_params_to_urls, + get_commit_url, + get_members_url, + get_pull_url, +) +from services.yaml.reader import get_paths_from_flags + +log = logging.getLogger(__name__) + + +class CheckOutput(TypedDict): + title: str + summary: str + annotations: list[Any] + text: Optional[str] + + +class CheckResult(TypedDict): + state: StatusState + output: CheckOutput + included_helper_text: dict[str, str] + + +class ChecksNotifier(StatusNotifier): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._repository_service = None + + ANNOTATIONS_PER_REQUEST = 50 + + def is_enabled(self) -> bool: + return True + + def store_results(self, comparison: ComparisonProxy, result: NotificationResult): + pass + + @property + def name(self): + return f"checks-{self.context}" + + def get_notifier_filters(self) -> dict: + flag_list = self.notifier_yaml_settings.get("flags") or [] + return dict( + path_patterns=set( + get_paths_from_flags(self.current_yaml, flag_list) + + (self.notifier_yaml_settings.get("paths") or []) + ), + flags=flag_list, + ) + + def get_upgrade_message( + self, comparison: ComparisonProxy | FilteredComparison + ) -> str: + db_pull = comparison.enriched_pull.database_pull + links = {"members_url": get_members_url(db_pull)} + author_username = comparison.enriched_pull.provider_pull["author"].get( + "username" + ) + return "\n".join( + [ + f"The author of this PR, {author_username}, is not an activated member of this organization on Codecov.", + f"Please [activate this user on Codecov]({links['members_url']}) to display a detailed status check.", + "Coverage data is still being uploaded to Codecov.io for purposes of overall coverage calculations.", + "Please don't hesitate to email us at support@codecov.io with any questions.", + ] + ) + + def paginate_annotations(self, annotations): + for i in range(0, len(annotations), self.ANNOTATIONS_PER_REQUEST): + yield annotations[i : i + self.ANNOTATIONS_PER_REQUEST] + + def build_payload(self, comparison: ComparisonProxy | FilteredComparison) -> dict: + raise NotImplementedError() + + def get_status_external_name(self) -> str: + status_piece = f"/{self.title}" if self.title != "default" else "" + return f"codecov/{self.context}{status_piece}" + + @sentry_sdk.trace + def notify( + self, + comparison: ComparisonProxy, + status_or_checks_helper_text: Optional[dict[str, str]] = None, + ) -> NotificationResult: + if comparison.pull is None or (): + log.debug( + "Falling back to commit_status: Not a pull request", + extra=dict( + notifier=self.name, + repoid=comparison.head.commit.repoid, + notifier_title=self.title, + commit=comparison.head.commit, + ), + ) + return NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="no_pull_request", + data_sent=None, + data_received=None, + ) + if ( + comparison.enriched_pull is None + or comparison.enriched_pull.provider_pull is None + ): + log.debug( + "Falling back to commit_status: Pull request not in provider", + extra=dict( + notifier=self.name, + repoid=comparison.head.commit.repoid, + notifier_title=self.title, + commit=comparison.head.commit, + ), + ) + return NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="pull_request_not_in_provider", + data_sent=None, + data_received=None, + ) + if comparison.pull.state != "open": + log.debug( + "Falling back to commit_status: Pull request closed", + extra=dict( + notifier=self.name, + repoid=comparison.head.commit.repoid, + notifier_title=self.title, + commit=comparison.head.commit, + ), + ) + return NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="pull_request_closed", + data_sent=None, + data_received=None, + ) + # Check branches config for this status before sending the check + if not self.can_we_set_this_status(comparison): + return NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="not_fit_criteria", + data_sent=None, + ) + if not self.required_builds(comparison): + return NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="need_more_builds", + data_sent=None, + data_received=None, + ) + # Check for existing statuses for this commit. If so, retain + # statuses and don't do a check as well + statuses = comparison.get_existing_statuses() + status_title = self.get_status_external_name() + if statuses and statuses.get(status_title): + log.debug( + "Falling back to commit_status: Status already exists for this commit", + extra=dict( + notifier=self.name, + repoid=comparison.head.commit.repoid, + notifier_title=self.title, + status_title=status_title, + commit=comparison.head.commit, + ), + ) + return NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="preexisting_commit_status", + data_sent=None, + data_received=None, + ) + payload = None + try: + with nullcontext(): + # If flag coverage wasn't uploaded, apply the appropriate behavior + flag_coverage_not_uploaded_behavior = ( + self.determine_status_check_behavior_to_apply( + comparison, "flag_coverage_not_uploaded_behavior" + ) + ) + if not comparison.has_head_report(): + payload = self.build_payload(comparison) + elif ( + flag_coverage_not_uploaded_behavior == "exclude" + and not self.flag_coverage_was_uploaded(comparison) + ): + return NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="exclude_flag_coverage_not_uploaded_checks", + data_sent=None, + data_received=None, + ) + elif ( + flag_coverage_not_uploaded_behavior == "pass" + and not self.flag_coverage_was_uploaded(comparison) + ): + filtered_comparison = comparison.get_filtered_comparison( + **self.get_notifier_filters() + ) + payload = self.build_payload(filtered_comparison) + payload["state"] = "success" + payload["output"]["summary"] = ( + payload.get("output", {}).get("summary", "") + + " [Auto passed due to carriedforward or missing coverage]" + ) + else: + filtered_comparison = comparison.get_filtered_comparison( + **self.get_notifier_filters() + ) + payload = self.build_payload(filtered_comparison) + if comparison.pull: + payload["url"] = get_pull_url(comparison.pull) + else: + payload["url"] = get_commit_url(comparison.head.commit) + return self.maybe_send_notification(comparison, payload) + except TorngitClientError as e: + if e.code == 403: + raise e + log.warning( + "Unable to send checks notification to user due to a client-side error", + exc_info=True, + extra=dict( + repoid=comparison.head.commit.repoid, + commit=comparison.head.commit.commitid, + notifier_name=self.name, + ), + ) + return NotificationResult( + notification_attempted=True, + notification_successful=False, + explanation="client_side_error_provider", + data_sent=payload, + ) + except TorngitError: + log.warning( + "Unable to send checks notification to user due to an unexpected error", + exc_info=True, + extra=dict( + repoid=comparison.head.commit.repoid, + commit=comparison.head.commit.commitid, + notifier_name=self.name, + ), + ) + return NotificationResult( + notification_attempted=True, + notification_successful=False, + explanation="server_side_error_provider", + data_sent=payload, + ) + + def get_line_diff(self, file_diff): + """ + This method traverses a git file diff and returns the lines (as line numbers) that where chnaged + Note: For now it only looks for line additions on diff, we can quickly add functionality to handle + line deletions if needed + + Parameters: + file_diff: file diff returned by repository.get_compare method. + structure: + { + "type": (str), + "path": (str), + "segments": [ + { + "header": [ + base reference offset, + number of lines in file-segment before changes applied, + head reference offset, + number of lines in file-segment after changes applied + ], + "lines": [ # line values for lines in the diff + "+this is an added line", + "-this is a removed line", + "this line is unchanged in the diff", + ... + ] + } + ] + } + + """ + segments = file_diff["segments"] + if len(segments) <= 0: + return None + + lines_diff = [] + for segment in segments: + header = segment["header"].copy() + lines = segment["lines"].copy() + base_ln = int(header[0]) + head_ln = int(header[2]) + while not len(lines) == 0: + line_value = lines.pop(0) + if line_value and line_value[0] == "+": + lines_diff.append({"head_line": head_ln}) + head_ln += 1 + elif line_value and line_value[0] == "-": + base_ln += 1 + else: + head_ln += 1 + base_ln += 1 + file_diff["additions"] = lines_diff + return file_diff + + def get_codecov_pr_link(self, comparison: ComparisonProxy | FilteredComparison): + return f"[View this Pull Request on Codecov]({get_pull_url(comparison.pull)}?dropdown=coverage&src=pr&el=h1)" + + def get_lines_to_annotate(self, comparison: ComparisonProxy, files_with_change): + lines_diff = [] + for _file in files_with_change: + if _file is None: + continue + head_file_report = comparison.head.report.get(_file["path"]) + for head_line in head_file_report.lines: + line_addition = next( + ( + line + for line in _file["additions"] + if head_line[0] == line["head_line"] + ), + None, + ) + if line_addition and head_line[1].coverage == 0: + lines_diff.append( + { + "type": "new_line", + "line": head_line[0], + "coverage": head_line[1].coverage, + "path": _file["path"], + } + ) + line_headers = [] + previous_line = {} + for index, line in enumerate(lines_diff): + if index == 0: + line_headers.append(line) + elif line["line"] != previous_line["line"] + 1: + line_headers[len(line_headers) - 1]["end_line"] = previous_line["line"] + line_headers.append(line) + if index == len(lines_diff) - 1: + line_headers[len(line_headers) - 1]["end_line"] = line["line"] + previous_line = line + return line_headers + + def create_annotations( + self, comparison: ComparisonProxy | FilteredComparison, diff + ): + files_with_change = [ + {"type": _diff["type"], "path": path, "segments": _diff["segments"]} + for path, _diff in (diff["files"] if diff else {}).items() + if _diff.get("totals") + ] + file_additions = [self.get_line_diff(_file) for _file in files_with_change] + lines_to_annotate = self.get_lines_to_annotate(comparison, file_additions) + annotations = [] + for line in lines_to_annotate: + annotation = { + "path": line["path"], + "start_line": line["line"], + "end_line": line["end_line"], + "annotation_level": "warning", + "message": ( + "Added lines #L{} - L{} were not covered by tests".format( + line["line"], line["end_line"] + ) + if line["line"] != line["end_line"] + else "Added line #L{} was not covered by tests".format(line["line"]) + ), + } + annotations.append(annotation) + return annotations + + def send_notification(self, comparison: ComparisonProxy, payload): + repository_service = self.repository_service + title = self.get_status_external_name() + head = comparison.head.commit + state = payload["state"] + state = "success" if self.notifier_yaml_settings.get("informational") else state + + # Append tracking parameters to any codecov urls in the title or summary + output = payload.get("output", {}) + if output.get("title"): + output["title"] = append_tracking_params_to_urls( + output["title"], + service=self.repository.service, + notification_type="checks", + org_name=self.repository.owner.name, + ) + if output.get("summary"): + output["summary"] = append_tracking_params_to_urls( + output["summary"], + service=self.repository.service, + notification_type="checks", + org_name=self.repository.owner.name, + ) + if output.get("text"): + output["text"] = append_tracking_params_to_urls( + output["text"], + service=self.repository.service, + notification_type="checks", + org_name=self.repository.owner.name, + ) + if payload.get("url"): + payload["url"] = append_tracking_params_to_urls( + payload["url"], + service=self.repository.service, + notification_type="checks", + org_name=self.repository.owner.name, + ) + + # We need to first create the check run, get that id and update the status + check_id = async_to_sync(repository_service.create_check_run)( + check_name=title, head_sha=head.commitid + ) + + if len(output.get("annotations", [])) > self.ANNOTATIONS_PER_REQUEST: + annotation_pages = list( + self.paginate_annotations(output.get("annotations")) + ) + log.info( + "Paginating annotations", + extra=dict( + number_pages=len(annotation_pages), + number_annotations=len(output.get("annotations")), + ), + ) + for annotation_page in annotation_pages: + async_to_sync(repository_service.update_check_run)( + check_id, + state, + output={ + "title": output.get("title"), + "summary": output.get("summary"), + "annotations": annotation_page, + }, + url=payload.get("url"), + ) + + else: + async_to_sync(repository_service.update_check_run)( + check_id, state, output=output, url=payload.get("url") + ) + + return NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent=payload, + github_app_used=self.get_github_app_used(), + ) diff --git a/apps/worker/services/notification/notifiers/checks/changes.py b/apps/worker/services/notification/notifiers/checks/changes.py new file mode 100644 index 0000000000..0c5ebde45a --- /dev/null +++ b/apps/worker/services/notification/notifiers/checks/changes.py @@ -0,0 +1,55 @@ +from database.enums import Notification +from services.comparison import ComparisonProxy, FilteredComparison +from services.notification.notifiers.checks.base import ( + CheckOutput, + CheckResult, + ChecksNotifier, +) +from services.notification.notifiers.mixins.status import StatusChangesMixin + + +class ChangesChecksNotifier(StatusChangesMixin, ChecksNotifier): + context = "changes" + notification_type_display_name = "check" + + @property + def notification_type(self) -> Notification: + return Notification.checks_changes + + def build_payload( + self, comparison: ComparisonProxy | FilteredComparison + ) -> CheckResult: + if self.is_empty_upload(): + state, message = self.get_status_check_for_empty_upload() + return CheckResult( + state=state, + output=CheckOutput( + title="Empty Upload", + summary=message, + annotations=[], + ), + included_helper_text={}, + ) + + status_result = self.get_changes_status( + comparison, notification_type=self.notification_type_display_name + ) + codecov_link = self.get_codecov_pr_link(comparison) + + title = status_result["message"] + message = status_result["message"] + + should_use_upgrade = self.should_use_upgrade_decoration() + if should_use_upgrade: + message = self.get_upgrade_message(comparison) + title = "Codecov Report" + + return CheckResult( + state=status_result["state"], + output=CheckOutput( + title=title, + summary="\n\n".join([codecov_link, message]), + annotations=[], + ), + included_helper_text={}, + ) diff --git a/apps/worker/services/notification/notifiers/checks/checks_with_fallback.py b/apps/worker/services/notification/notifiers/checks/checks_with_fallback.py new file mode 100644 index 0000000000..571b39fb8b --- /dev/null +++ b/apps/worker/services/notification/notifiers/checks/checks_with_fallback.py @@ -0,0 +1,102 @@ +import logging +from typing import Optional + +from shared.torngit.exceptions import TorngitClientError + +from services.comparison import ComparisonProxy +from services.notification.notifiers.base import ( + AbstractBaseNotifier, + NotificationResult, +) + +log = logging.getLogger(__name__) + + +class ChecksWithFallback(AbstractBaseNotifier): + """ + This class attempts to notify using Github Checks and has the ability of falling back + to commit_status in the event of users not having enough permissons to use checks. + + Note: This class is not meant to store results. + """ + + def __init__( + self, + checks_notifier: AbstractBaseNotifier, + status_notifier: AbstractBaseNotifier, + ): + self._checks_notifier = checks_notifier + self._status_notifier = status_notifier + self._decoration_type = checks_notifier.decoration_type + self._title = checks_notifier.title + self._name = f"{checks_notifier.name}-with-fallback" + self._notification_type = checks_notifier.notification_type + + def is_enabled(self): + return self._checks_notifier.is_enabled() or self._status_notifier.is_enabled() + + @property + def name(self): + return self._name + + @property + def title(self): + return self._title + + @property + def notification_type(self): + return self._notification_type + + @property + def decoration_type(self): + return self._decoration_type + + def store_results(self, comparison: ComparisonProxy, result: NotificationResult): + pass + + def notify( + self, + comparison: ComparisonProxy, + status_or_checks_helper_text: Optional[dict[str, str]] = None, + ) -> NotificationResult: + try: + res = self._checks_notifier.notify( + comparison, status_or_checks_helper_text=status_or_checks_helper_text + ) + if not res.notification_successful and ( + res.explanation == "no_pull_request" + or res.explanation == "pull_request_not_in_provider" + or res.explanation == "pull_request_closed" + or res.explanation == "preexisting_commit_status" + ): + log.info( + "Couldn't use checks notifier, falling back to status notifiers", + extra=dict( + notifier=self._checks_notifier.name, + repoid=comparison.head.commit.repoid, + notifier_title=self._checks_notifier.title, + commit=comparison.head.commit, + explanation=res.explanation, + ), + ) + res = self._status_notifier.notify( + comparison, + status_or_checks_helper_text=status_or_checks_helper_text, + ) + return res + except TorngitClientError as e: + if e.code == 403: + log.info( + "Checks notifier failed due to torngit error, falling back to status notifiers", + extra=dict( + notifier=self._checks_notifier.name, + repoid=comparison.head.commit.repoid, + notifier_title=self._checks_notifier.title, + commit=comparison.head.commit, + ), + ) + return self._status_notifier.notify( + comparison, + status_or_checks_helper_text=status_or_checks_helper_text, + ) + raise e diff --git a/apps/worker/services/notification/notifiers/checks/patch.py b/apps/worker/services/notification/notifiers/checks/patch.py new file mode 100644 index 0000000000..d2e069aed2 --- /dev/null +++ b/apps/worker/services/notification/notifiers/checks/patch.py @@ -0,0 +1,93 @@ +from database.enums import Notification +from services.comparison import ComparisonProxy, FilteredComparison +from services.notification.notifiers.checks.base import ( + CheckOutput, + CheckResult, + ChecksNotifier, +) +from services.notification.notifiers.mixins.status import StatusPatchMixin +from services.yaml import read_yaml_field + + +class PatchChecksNotifier(StatusPatchMixin, ChecksNotifier): + context = "patch" + notification_type_display_name = "check" + + @property + def notification_type(self) -> Notification: + return Notification.checks_patch + + def build_payload( + self, comparison: ComparisonProxy | FilteredComparison + ) -> CheckResult: + """ + This method build the paylod of the patch github checks. + + We only add annotaions to the top-level patch check of a project. + We do not add annotations on checks that are used with paths/flags + """ + if self.is_empty_upload(): + state, message = self.get_status_check_for_empty_upload() + result = CheckResult( + state=state, + output=CheckOutput( + title="Empty Upload", + summary=message, + annotations=[], + ), + included_helper_text={}, + ) + return result + status_result = self.get_patch_status( + comparison, notification_type=self.notification_type_display_name + ) + codecov_link = self.get_codecov_pr_link(comparison) + + title = status_result["message"] + message = status_result["message"] + + should_use_upgrade = self.should_use_upgrade_decoration() + if should_use_upgrade: + message = self.get_upgrade_message(comparison) + title = "Codecov Report" + + checks_yaml_field = read_yaml_field(self.current_yaml, ("github_checks",)) + try: + # checks_yaml_field can be dict, bool, None + # should_annotate defaults to False as of Jan 30 2025 + should_annotate = checks_yaml_field.get("annotations", False) + except AttributeError: + should_annotate = False + + flags = self.notifier_yaml_settings.get("flags") + paths = self.notifier_yaml_settings.get("paths") + if ( + flags is not None + or paths is not None + or should_use_upgrade + or should_annotate is False + ): + result = CheckResult( + state=status_result["state"], + output=CheckOutput( + title=title, + summary="\n\n".join([codecov_link, message]), + annotations=[], + ), + included_helper_text=status_result["included_helper_text"], + ) + return result + diff = comparison.get_diff(use_original_base=True) + # TODO: Look into why the apply diff in get_patch_status is not saving state at this point + comparison.head.report.apply_diff(diff) + annotations = self.create_annotations(comparison, diff) + result = CheckResult( + state=status_result["state"], + output=CheckOutput( + title=title, + summary="\n\n".join([codecov_link, message]), + annotations=annotations, + ), + included_helper_text=status_result["included_helper_text"], + ) + return result diff --git a/apps/worker/services/notification/notifiers/checks/project.py b/apps/worker/services/notification/notifiers/checks/project.py new file mode 100644 index 0000000000..6dcca70c83 --- /dev/null +++ b/apps/worker/services/notification/notifiers/checks/project.py @@ -0,0 +1,114 @@ +from typing import Optional + +from database.enums import Notification +from services.comparison import ComparisonProxy, FilteredComparison +from services.notification.notifiers.checks.base import ( + CheckOutput, + CheckResult, + ChecksNotifier, +) +from services.notification.notifiers.mixins.message import MessageMixin +from services.notification.notifiers.mixins.status import StatusProjectMixin +from services.yaml.reader import read_yaml_field + + +class ProjectChecksNotifier(MessageMixin, StatusProjectMixin, ChecksNotifier): + context = "project" + notification_type_display_name = "check" + + @property + def notification_type(self) -> Notification: + return Notification.checks_project + + def get_message( + self, + comparison: ComparisonProxy | FilteredComparison, + yaml_comment_settings, + status_or_checks_helper_text: Optional[dict[str, str]] = None, + ): + pull_dict = comparison.enriched_pull.provider_pull + return self.create_message( + comparison, + pull_dict, + yaml_comment_settings, + status_or_checks_helper_text=status_or_checks_helper_text, + ) + + def build_payload( + self, comparison: ComparisonProxy | FilteredComparison + ) -> CheckResult: + """ + This method build the paylod of the project github checks. + + We only show/add the comment message to the top-level check of a project. + We do not show/add the message on checks that are used with paths/flags. + """ + if self.is_empty_upload(): + state, message = self.get_status_check_for_empty_upload() + result = CheckResult( + state=state, + output=CheckOutput( + title="Empty Upload", summary=message, annotations=[] + ), + included_helper_text={}, + ) + return result + + status_result = self.get_project_status( + comparison, notification_type=self.notification_type_display_name + ) + codecov_link = self.get_codecov_pr_link(comparison) + + title = status_result["message"] + summary = status_result["message"] + + should_use_upgrade = self.should_use_upgrade_decoration() + if should_use_upgrade: + title = "Codecov Report" + summary = self.get_upgrade_message(comparison) + + flags = self.notifier_yaml_settings.get("flags") + paths = self.notifier_yaml_settings.get("paths") + yaml_comment_settings = read_yaml_field(self.current_yaml, ("comment",)) or {} + if yaml_comment_settings is True: + yaml_comment_settings = self.site_settings.get("comment", {}) + # copying to a new variable because we will be modifying that + settings_to_be_used = dict(yaml_comment_settings) + if "flag" in settings_to_be_used.get("layout", ""): + old_flags_list = settings_to_be_used.get("layout", "").split(",") + new_flags_list = [x for x in old_flags_list if "flag" not in x] + settings_to_be_used["layout"] = ",".join(new_flags_list) + + if ( + flags is not None + or paths is not None + or should_use_upgrade + or not settings_to_be_used + ): + result = CheckResult( + state=status_result["state"], + output=CheckOutput( + title=title, + summary="\n\n".join([codecov_link, summary]), + annotations=[], + ), + included_helper_text=status_result["included_helper_text"], + ) + return result + + message = self.get_message( + comparison, + settings_to_be_used, + status_or_checks_helper_text=status_result["included_helper_text"], + ) + result = CheckResult( + state=status_result["state"], + output=CheckOutput( + title=title, + summary="\n\n".join([codecov_link, summary]), + annotations=[], + text="\n".join(message), + ), + included_helper_text=status_result["included_helper_text"], + ) + return result diff --git a/apps/worker/services/notification/notifiers/codecov_slack_app.py b/apps/worker/services/notification/notifiers/codecov_slack_app.py new file mode 100644 index 0000000000..151f327b9f --- /dev/null +++ b/apps/worker/services/notification/notifiers/codecov_slack_app.py @@ -0,0 +1,141 @@ +import json +import os +from decimal import Decimal +from typing import Optional + +import requests + +from database.enums import Notification +from database.models import Commit +from services.comparison import ComparisonProxy +from services.notification.notifiers.base import ( + AbstractBaseNotifier, + NotificationResult, +) +from services.notification.notifiers.generics import EnhancedJSONEncoder +from services.urls import get_commit_url, get_pull_url +from services.yaml.reader import round_number + +CODECOV_INTERNAL_TOKEN = os.environ.get("CODECOV_INTERNAL_TOKEN") +CODECOV_SLACK_APP_URL = os.environ.get("CODECOV_SLACK_APP_URL") + + +class CodecovSlackAppNotifier(AbstractBaseNotifier): + name = "codecov-slack-app" + + @property + def notification_type(self) -> Notification: + return Notification.codecov_slack_app + + def is_enabled(self) -> bool: + # if yaml settings are a dict, then check the enabled key and return that + # the enabled field should always exist if the yaml settings are a dict because otherwise it would fail the validation + + # else if the yaml settings is a boolean then just return that + + # in any case, self.notifier_yaml_settings should either be a bool or a dict always and should never be None + if isinstance(self.notifier_yaml_settings, dict): + return self.notifier_yaml_settings.get("enabled", False) + elif isinstance(self.notifier_yaml_settings, bool): + return self.notifier_yaml_settings + + def store_results(self, comparison: ComparisonProxy, result: NotificationResult): + pass + + def serialize_commit(self, commit: Commit): + if not commit: + return None + return { + "commitid": commit.commitid, + "branch": commit.branch, + "message": commit.message, + "author": commit.author.username if commit.author else None, + "timestamp": commit.timestamp.isoformat() if commit.timestamp else None, + "ci_passed": commit.ci_passed, + "totals": commit.totals, + "pull": commit.pullid, + } + + def build_payload(self, comparison: ComparisonProxy) -> dict: + head_full_commit = comparison.head + base_full_commit = comparison.project_coverage_base + if comparison.has_project_coverage_base_report(): + difference = Decimal(0) + head_coverage = head_full_commit.report.totals.coverage + base_coverage = base_full_commit.report.totals.coverage + if head_coverage is not None and base_coverage is not None: + difference = Decimal(head_coverage) - Decimal(base_coverage) + + message = ( + "no change" + if difference == 0 + else "increased" + if difference > 0 + else "decreased" + ) + notation = "" if difference == 0 else "+" if difference > 0 else "-" + comparison_url = ( + get_pull_url(comparison.pull) + if comparison.pull + else get_commit_url(comparison.head.commit) + ) + else: + difference = None + message = "unknown" + notation = "" + comparison_url = None + head_report = comparison.head.report + return { + "url": comparison_url, + "message": message, + "coverage": str(round_number(self.current_yaml, difference)) + if difference is not None + else None, + "notation": notation, + "head_commit": self.serialize_commit( + comparison.head.commit if comparison.head else None + ), + "base_commit": self.serialize_commit( + comparison.project_coverage_base.commit + if comparison.project_coverage_base + else None + ), + "head_totals_c": str(head_report.totals.coverage) if head_report else "0", + } + + def notify( + self, + comparison: ComparisonProxy, + status_or_checks_helper_text: Optional[dict[str, str]] = None, + ) -> NotificationResult: + request_url = f"{CODECOV_SLACK_APP_URL}/notify" + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {CODECOV_INTERNAL_TOKEN}", + } + + compare_dict = self.build_payload(comparison) + data = { + "repository": self.repository.name, + "owner": self.repository.owner.username, + "comparison": compare_dict, + } + response = requests.post( + request_url, headers=headers, data=json.dumps(data, cls=EnhancedJSONEncoder) + ) + + if response.status_code == 200: + return NotificationResult( + data_sent=data, + notification_attempted=True, + notification_successful=True, + explanation="Successfully notified slack app", + ) + else: + return NotificationResult( + data_sent=data, + notification_attempted=True, + notification_successful=False, + explanation=f"Failed to notify slack app\nError {response.status_code}: {response.reason}.", + ) diff --git a/apps/worker/services/notification/notifiers/comment/__init__.py b/apps/worker/services/notification/notifiers/comment/__init__.py new file mode 100644 index 0000000000..6366786365 --- /dev/null +++ b/apps/worker/services/notification/notifiers/comment/__init__.py @@ -0,0 +1,444 @@ +import logging +from datetime import datetime, timezone +from typing import Any, Mapping, Optional + +import sentry_sdk +from asgiref.sync import async_to_sync +from shared.metrics import Counter, inc_counter +from shared.plan.constants import PlanName +from shared.torngit.exceptions import ( + TorngitClientError, + TorngitObjectNotFoundError, + TorngitServerFailureError, +) + +from database.enums import Notification +from services.comparison import ComparisonProxy +from services.comparison.types import Comparison +from services.license import requires_license +from services.notification.notifiers.base import ( + AbstractBaseNotifier, + NotificationResult, +) +from services.notification.notifiers.comment.conditions import ( + ComparisonHasPull, + HasEnoughBuilds, + HasEnoughRequiredChanges, + NoAutoActivateMessageIfAutoActivateIsOff, + NotifyCondition, + PullHeadMatchesComparisonHead, + PullRequestInProvider, +) +from services.notification.notifiers.mixins.message import MessageMixin +from services.urls import append_tracking_params_to_urls, get_members_url, get_plan_url + +log = logging.getLogger(__name__) + +COMMENT_NOTIFIER_COUNTER = Counter( + "notifiers_comment_pull_closed_notifying_anyways", + "Number of comment notifier runs when pull is closed", + ["repo_using_integration"], +) + + +class CommentNotifier(MessageMixin, AbstractBaseNotifier): + notify_conditions: list[type[NotifyCondition]] = [ + ComparisonHasPull, + PullRequestInProvider, + PullHeadMatchesComparisonHead, + HasEnoughBuilds, + HasEnoughRequiredChanges, + NoAutoActivateMessageIfAutoActivateIsOff, + ] + + def store_results(self, comparison: ComparisonProxy, result: NotificationResult): + pull = comparison.pull + if not result.notification_attempted or not result.notification_successful: + return + data_received = result.data_received + if data_received: + if data_received.get("id"): + pull.commentid = data_received.get("id") + elif data_received.get("deleted_comment"): + pull.commentid = None + + @property + def name(self) -> str: + return "comment" + + @property + def notification_type(self) -> Notification: + return Notification.comment + + def get_diff(self, comparison: Comparison): + return comparison.get_diff() + + @sentry_sdk.trace + def notify( + self, + comparison: ComparisonProxy, + status_or_checks_helper_text: Optional[dict[str, str]] = None, + ) -> NotificationResult: + # TODO: remove this when we don't need it anymore + # this line is measuring how often we try to comment on a PR that is closed + if comparison.pull is not None and comparison.pull.state != "open": + inc_counter( + COMMENT_NOTIFIER_COUNTER, + labels=dict( + repo_using_integration="true" + if self.repository_service.data["repo"]["using_integration"] + else "false", + ), + ) + + for condition in self.notify_conditions: + condition_result = condition.check_condition( + notifier=self, comparison=comparison + ) + if condition_result == False: + side_effect_result = condition.on_failure_side_effect(self, comparison) + default_result = NotificationResult( + notification_attempted=False, + explanation=condition.failure_explanation, + data_sent=None, + data_received=None, + ) + return default_result.merge(side_effect_result) + pull = comparison.pull + try: + message = self.build_message( + comparison, status_or_checks_helper_text=status_or_checks_helper_text + ) + except TorngitClientError: + log.warning( + "Unable to fetch enough information to build message for comment", + extra=dict( + commit=comparison.head.commit.commitid, + pullid=comparison.pull.pullid, + ), + exc_info=True, + ) + return NotificationResult( + notification_attempted=False, + explanation="unable_build_message", + data_sent=None, + data_received=None, + ) + data = {"message": message, "commentid": pull.commentid, "pullid": pull.pullid} + try: + return self.send_actual_notification(data) + except TorngitServerFailureError: + log.warning( + "Unable to send comments because the provider server was not reachable or errored", + extra=dict(git_service=self.repository.service), + exc_info=True, + ) + return NotificationResult( + notification_attempted=True, + notification_successful=False, + explanation="provider_issue", + data_sent=data, + data_received=None, + ) + + def send_actual_notification(self, data: Mapping[str, Any]): + message = "\n".join(data["message"]) + + # Append tracking parameters to any codecov urls in the message + message = append_tracking_params_to_urls( + message, + service=self.repository.service, + notification_type="comment", + org_name=self.repository.owner.name, + ) + + behavior = self.notifier_yaml_settings.get("behavior", "default") + if behavior == "default": + res = self.send_comment_default_behavior( + data["pullid"], data["commentid"], message + ) + elif behavior == "once": + res = self.send_comment_once_behavior( + data["pullid"], data["commentid"], message + ) + elif behavior == "new": + res = self.send_comment_new_behavior( + data["pullid"], data["commentid"], message + ) + elif behavior == "spammy": + res = self.send_comment_spammy_behavior( + data["pullid"], data["commentid"], message + ) + return NotificationResult( + notification_attempted=res["notification_attempted"], + notification_successful=res["notification_successful"], + explanation=res["explanation"], + data_sent=data, + data_received=res["data_received"], + ) + + def send_comment_default_behavior(self, pullid, commentid, message): + if commentid: + try: + res = async_to_sync(self.repository_service.edit_comment)( + pullid, commentid, message + ) + return { + "notification_attempted": True, + "notification_successful": True, + "explanation": None, + "data_received": {"id": res["id"]}, + } + except TorngitObjectNotFoundError: + log.warning("Comment was not found to be edited") + except TorngitClientError: + log.warning( + "Comment could not be edited due to client permissions", + exc_info=True, + extra=dict(pullid=pullid, commentid=commentid), + ) + try: + res = async_to_sync(self.repository_service.post_comment)(pullid, message) + return { + "notification_attempted": True, + "notification_successful": True, + "explanation": None, + "data_received": {"id": res["id"]}, + } + except TorngitClientError: + log.warning( + "Comment could not be posted due to client permissions", + exc_info=True, + extra=dict(pullid=pullid, commentid=commentid), + ) + return { + "notification_attempted": True, + "notification_successful": False, + "explanation": "comment_posting_permissions", + "data_received": None, + } + + def send_comment_once_behavior(self, pullid, commentid, message): + if commentid: + try: + res = async_to_sync(self.repository_service.edit_comment)( + pullid, commentid, message + ) + return { + "notification_attempted": True, + "notification_successful": True, + "explanation": None, + "data_received": {"id": res["id"]}, + } + except TorngitObjectNotFoundError: + log.warning("Comment was not found to be edited") + return { + "notification_attempted": False, + "notification_successful": None, + "explanation": "comment_deleted", + "data_received": None, + } + except TorngitClientError: + log.warning( + "Comment could not be edited due to client permissions", + exc_info=True, + ) + return { + "notification_attempted": True, + "notification_successful": False, + "explanation": "no_permissions", + "data_received": None, + } + res = async_to_sync(self.repository_service.post_comment)(pullid, message) + return { + "notification_attempted": True, + "notification_successful": True, + "explanation": None, + "data_received": {"id": res["id"]}, + } + + def send_comment_new_behavior(self, pullid, commentid, message): + if commentid: + try: + async_to_sync(self.repository_service.delete_comment)(pullid, commentid) + except TorngitObjectNotFoundError: + log.info("Comment was already deleted") + except TorngitClientError: + log.warning( + "Comment could not be deleted due to client permissions", + exc_info=True, + extra=dict( + repoid=self.repository.repoid, + pullid=pullid, + commentid=commentid, + ), + ) + return { + "notification_attempted": True, + "notification_successful": False, + "explanation": "no_permissions", + "data_received": None, + } + try: + res = async_to_sync(self.repository_service.post_comment)(pullid, message) + return { + "notification_attempted": True, + "notification_successful": True, + "explanation": None, + "data_received": {"id": res["id"]}, + } + except TorngitClientError: + log.warning( + "Comment could not be posted due to client permissions", + exc_info=True, + extra=dict( + repoid=self.repository.repoid, pullid=pullid, commentid=commentid + ), + ) + return { + "notification_attempted": True, + "notification_successful": False, + "explanation": "comment_posting_permissions", + "data_received": None, + } + + def send_comment_spammy_behavior(self, pullid, commentid, message): + res = async_to_sync(self.repository_service.post_comment)(pullid, message) + return { + "notification_attempted": True, + "notification_successful": True, + "explanation": None, + "data_received": {"id": res["id"]}, + } + + def is_enabled(self) -> bool: + return bool(self.notifier_yaml_settings) and isinstance( + self.notifier_yaml_settings, dict + ) + + def build_message( + self, + comparison: ComparisonProxy, + status_or_checks_helper_text: Optional[dict[str, str]] = None, + ) -> list[str]: + if self.should_use_upgrade_decoration(): + return self._create_upgrade_message(comparison) + if self.is_processing_upload(): + return self._create_processing_upload_message() + if self.is_empty_upload(): + return self._create_empty_upload_message() + if self.should_use_upload_limit_decoration(): + return self._create_reached_upload_limit_message(comparison) + if comparison.pull.is_first_coverage_pull: + return self._create_welcome_message() + pull_dict = comparison.enriched_pull.provider_pull + return self.create_message( + comparison, + pull_dict, + self.notifier_yaml_settings, + status_or_checks_helper_text=status_or_checks_helper_text, + ) + + def should_see_project_coverage_cta(self): + """ + Why was this check added? We changed our default behavior on 5/1/2024. + Change explained on issue 1078 + """ + introduction_date = datetime(2024, 5, 1, 0, 0, 0).replace(tzinfo=timezone.utc) + + if ( + not self.repository.private + and self.repository.owner.createstamp + and self.repository.owner.createstamp > introduction_date + ): + # public repos, only if they signed up after introduction date + return True + + if ( + not ( + self.repository.owner.plan == PlanName.TEAM_MONTHLY.value + or self.repository.owner.plan == PlanName.TEAM_YEARLY.value + ) + and self.repository.owner.createstamp + and self.repository.owner.createstamp > introduction_date + ): + # private repos excluding those on team plans, only if they signed up after introduction date + return True + + return False + + def _create_welcome_message(self): + welcome_message = [ + "## Welcome to [Codecov](https://codecov.io) :tada:", + "", + "Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.", + "", + "Thanks for integrating Codecov - We've got you covered :open_umbrella:", + ] + project_coverage_cta = [ + ":information_source: You can also turn on [project coverage checks](https://docs.codecov.com/docs/common-recipe-list#set-project-coverage-checks-on-a-pull-request) " + "and [project coverage reporting on Pull Request comment](https://docs.codecov.com/docs/common-recipe-list#show-project-coverage-changes-on-the-pull-request-comment)", + "", + ] + + if self.should_see_project_coverage_cta(): + welcome_message_with_project_coverage_cta = ( + welcome_message[0:4] + project_coverage_cta + welcome_message[4:] + ) + return welcome_message_with_project_coverage_cta + + return welcome_message + + def _create_empty_upload_message(self): + if self.is_passing_empty_upload(): + return [ + "## Codecov Report", + ":heavy_check_mark: **No coverage data to report**, because files changed do not require tests or are set to [ignore](https://docs.codecov.com/docs/ignoring-paths#:~:text=You%20can%20use%20the%20top,will%20be%20skipped%20during%20processing.) ", + ] + if self.is_failing_empty_upload(): + return [ + "## Codecov Report", + "This is an empty upload", + "Files changed in this PR are testable or aren't ignored by Codecov, please run your tests and upload coverage. If you wish to ignore these files, please visit our [ignoring paths docs](https://docs.codecov.com/docs/ignoring-paths).", + ] + + def _create_reached_upload_limit_message(self, comparison: ComparisonProxy): + db_pull = comparison.enriched_pull.database_pull + links = {"plan_url": get_plan_url(db_pull)} + return [ + f"## [Codecov]({links['plan_url']}) upload limit reached :warning:", + f"This org is currently on the free Basic Plan; which includes 250 free private repo uploads each rolling month.\ + This limit has been reached and additional reports cannot be generated. For unlimited uploads,\ + upgrade to our [pro plan]({links['plan_url']}).", + "", + "**Do you have questions or need help?** Connect with our sales team today at ` sales@codecov.io `", + ] + + def _create_upgrade_message(self, comparison: ComparisonProxy): + db_pull = comparison.enriched_pull.database_pull + links = { + "members_url_cloud": get_members_url(db_pull), + "members_url_self_hosted": get_members_url(db_pull), + } + author_username = comparison.enriched_pull.provider_pull["author"].get( + "username" + ) + if not requires_license(): + return [ + f"The author of this PR, {author_username}, is not an activated member of this organization on Codecov.", + f"Please [activate this user on Codecov]({links['members_url_cloud']}) to display this PR comment.", + "Coverage data is still being uploaded to Codecov.io for purposes of overall coverage calculations.", + "Please don't hesitate to email us at support@codecov.io with any questions.", + ] + else: + return [ + f"The author of this PR, {author_username}, is not activated in your Codecov Self-Hosted installation.", + f"Please [activate this user]({links['members_url_self_hosted']}) to display this PR comment.", + "Coverage data is still being uploaded to Codecov Self-Hosted for the purposes of overall coverage calculations.", + "Please contact your Codecov On-Premises installation administrator with any questions.", + ] + + def _create_processing_upload_message(self): + return [ + "We're currently processing your upload. This comment will be updated when the results are available.", + ] diff --git a/apps/worker/services/notification/notifiers/comment/conditions.py b/apps/worker/services/notification/notifiers/comment/conditions.py new file mode 100644 index 0000000000..017e934fde --- /dev/null +++ b/apps/worker/services/notification/notifiers/comment/conditions.py @@ -0,0 +1,226 @@ +import logging +from abc import ABC, abstractmethod +from decimal import Decimal + +from shared.validation.types import ( + CoverageCommentRequiredChanges, + CoverageCommentRequiredChangesORGroup, +) + +from services.comparison import ComparisonProxy +from services.notification.notifiers.base import ( + AbstractBaseNotifier, + NotificationResult, +) +from services.yaml import read_yaml_field + +log = logging.getLogger(__name__) + + +class NotifyCondition(ABC): + """Abstract class that defines the basis of a NotifyCondition. + + NotifyCondition specifies the conditions that need to be met in order for a notification to be sent + from Codecov to a git provider. + NotifyCondition can have a side effect that is called when the condition fails. + """ + + failure_explanation: str + + @staticmethod + @abstractmethod + def check_condition( + notifier: AbstractBaseNotifier, comparison: ComparisonProxy + ) -> bool: + return True + + @staticmethod + def on_failure_side_effect( + notifier: AbstractBaseNotifier, comparison: ComparisonProxy + ) -> NotificationResult: + return NotificationResult() + + +class ComparisonHasPull(NotifyCondition): + failure_explanation = "no_pull_request" + + @staticmethod + def check_condition( + notifier: AbstractBaseNotifier, comparison: ComparisonProxy + ) -> bool: + return comparison.pull is not None + + +class PullRequestInProvider(NotifyCondition): + failure_explanation = "pull_request_not_in_provider" + + @staticmethod + def check_condition( + notifier: AbstractBaseNotifier, comparison: ComparisonProxy + ) -> bool: + return ( + comparison.enriched_pull is not None + and comparison.enriched_pull.provider_pull is not None + ) + + +class PullRequestOpen(NotifyCondition): + failure_explanation = "pull_request_closed" + + @staticmethod + def check_condition( + notifier: AbstractBaseNotifier, comparison: ComparisonProxy + ) -> bool: + return comparison.pull.state == "open" + + +class PullHeadMatchesComparisonHead(NotifyCondition): + failure_explanation = "pull_head_does_not_match" + + @staticmethod + def check_condition( + notifier: AbstractBaseNotifier, comparison: ComparisonProxy + ) -> bool: + return comparison.pull.head == comparison.head.commit.commitid + + +class HasEnoughBuilds(NotifyCondition): + failure_explanation = "not_enough_builds" + + @staticmethod + def check_condition( + notifier: AbstractBaseNotifier, comparison: ComparisonProxy + ) -> bool: + expected_builds = notifier.notifier_yaml_settings.get("after_n_builds", 0) + present_builds = len(comparison.head.report.sessions) + return present_builds >= expected_builds + + +class HasEnoughRequiredChanges(NotifyCondition): + failure_explanation = "changes_required" + + @staticmethod + def _check_unexpected_changes(comparison: ComparisonProxy) -> bool: + """Returns a bool that indicates wether there are unexpected changes""" + return bool(comparison.get_changes()) + + @staticmethod + def _check_coverage_change(comparison: ComparisonProxy) -> bool: + """Returns a bool that indicates wether there is any change in coverage""" + diff = comparison.get_diff() + res = None if diff is None else comparison.head.report.calculate_diff(diff) + return res is not None and res["general"].lines > 0 + + @staticmethod + def _check_any_change(comparison: ComparisonProxy) -> bool: + unexpected_changes = HasEnoughRequiredChanges._check_unexpected_changes( + comparison + ) + coverage_changes = HasEnoughRequiredChanges._check_coverage_change(comparison) + return unexpected_changes or coverage_changes + + @staticmethod + def _check_coverage_drop(comparison: ComparisonProxy) -> bool: + no_head_coverage = comparison.head.report.totals.coverage is None + no_base_report = comparison.project_coverage_base.report is None + no_base_coverage = ( + no_base_report + or comparison.project_coverage_base.report.totals.coverage is None + ) + if no_head_coverage or no_base_coverage: + # We don't know if there was a coverage drop, because we can't compare BASE and HEAD (missing some info) + # But we default to showing the comment. It might have info for the user about _what_ info we are missing + return True + head_coverage = Decimal(comparison.head.report.totals.coverage).quantize( + Decimal("0.00000") + ) + project_status_config = read_yaml_field( + comparison.comparison.current_yaml, ("coverage", "status", "project"), {} + ) + threshold = 0 + if isinstance(project_status_config, dict): + # Project status can also be a bool value, so check is needed + threshold = Decimal(project_status_config.get("threshold", 0)) + target_coverage = Decimal( + comparison.project_coverage_base.report.totals.coverage + ).quantize(Decimal("0.00000")) + diff = head_coverage - target_coverage + # Need to take the project threshold into consideration + return diff < 0 and abs(diff) >= (threshold + Decimal(0.01)) + + @staticmethod + def _check_uncovered_patch(comparison: ComparisonProxy) -> bool: + diff = comparison.get_diff(use_original_base=True) + totals = None if diff is None else comparison.head.report.apply_diff(diff) + coverage_not_affected_by_patch = totals and totals.lines == 0 + if totals is None or coverage_not_affected_by_patch: + # The patch doesn't affect coverage. So we don't show a comment. + # The patch is probably not related to testable files + return False + coverage = Decimal(totals.coverage).quantize(Decimal("0.00000")) + return abs(coverage - Decimal(100).quantize(Decimal("0.00000"))) >= Decimal( + 0.01 + ) + + @staticmethod + def check_condition_OR_group( + condition_group: CoverageCommentRequiredChangesORGroup, + comparison: ComparisonProxy, + ) -> bool: + if condition_group == CoverageCommentRequiredChanges.no_requirements.value: + return True + cache_results = { + individual_condition: None + for individual_condition in CoverageCommentRequiredChanges + } + functions_lookup = { + CoverageCommentRequiredChanges.any_change: HasEnoughRequiredChanges._check_any_change, + CoverageCommentRequiredChanges.coverage_drop: HasEnoughRequiredChanges._check_coverage_drop, + CoverageCommentRequiredChanges.uncovered_patch: HasEnoughRequiredChanges._check_uncovered_patch, + } + final_result = False + for individual_condition in CoverageCommentRequiredChanges: + if condition_group & individual_condition.value: + if cache_results[individual_condition] is None: + function_to_call = functions_lookup[individual_condition] + cache_results[individual_condition] = function_to_call(comparison) + final_result |= cache_results[individual_condition] + return final_result + + @staticmethod + def check_condition( + notifier: AbstractBaseNotifier, comparison: ComparisonProxy + ) -> bool: + if comparison.pull and comparison.pull.commentid: + log.info( + "Comment already exists. Skipping required_changes verification to update comment", + extra=dict(pull=comparison.pull.pullid, commit=comparison.pull.head), + ) + return True + required_changes = notifier.notifier_yaml_settings.get( + "require_changes", [CoverageCommentRequiredChanges.no_requirements.value] + ) + # backwards compatibility (can be removed after full rollout) + if isinstance(required_changes, bool): + # True --> 1 (any_change) + # False --> 0 (no_requirements) + required_changes = [int(required_changes)] + return all( + HasEnoughRequiredChanges.check_condition_OR_group(or_group, comparison) + for or_group in required_changes + ) + + +class NoAutoActivateMessageIfAutoActivateIsOff(NotifyCondition): + failure_explanation = "auto_activate_message_but_auto_activate_is_off" + + @staticmethod + def check_condition( + notifier: AbstractBaseNotifier, comparison: ComparisonProxy + ) -> bool: + owner = notifier.repository.owner + # Return False ONLY if (owner.plan_auto_activate is False) and should_use_upgrade_message + # Checking if owner.plan_auto_activate is False so None will pass (tests) + return (owner.plan_auto_activate != False) or ( + not notifier.should_use_upgrade_decoration() + ) diff --git a/apps/worker/services/notification/notifiers/generics.py b/apps/worker/services/notification/notifiers/generics.py new file mode 100644 index 0000000000..fef7d7f3a2 --- /dev/null +++ b/apps/worker/services/notification/notifiers/generics.py @@ -0,0 +1,262 @@ +import json +import logging +from decimal import Decimal +from typing import Any, Mapping, Optional +from urllib.parse import urlparse + +import httpx +import sentry_sdk +from shared.config import get_config + +from helpers.match import match +from helpers.metrics import metrics +from services.comparison import ComparisonProxy +from services.comparison.types import Comparison +from services.notification.notifiers.base import ( + AbstractBaseNotifier, + NotificationResult, +) +from services.urls import get_commit_url, get_pull_url +from services.yaml.reader import get_paths_from_flags, round_number + +log = logging.getLogger(__name__) + + +class StandardNotifier(AbstractBaseNotifier): + """ + This class is our standard notifier. It assumes and does the following: + + - Ensure that the notifier has a valid `url` to be used + - Ensure that the `url` base is enabled on site-wide settings + - Check that the current branch is inside the list of enabled branches + - Filters the reports according to the given paths and flags + - Check that the threshold of the webhook is satisfied on this comparison + """ + + def store_results(self, comparison: ComparisonProxy, result: NotificationResult): + pass + + @property + def name(self): + return self.__class__.__name__ + + def is_enabled(self) -> bool: + if not bool(self.site_settings): + log.info( + "Not notifying on %s, because it is not enabled on site-level settings", + self.name, + ) + return False + if not self.notifier_yaml_settings.get("url"): + log.warning("Not notifying because webhook had no url") + return False + parsed_url = urlparse(self.notifier_yaml_settings.get("url")) + if ( + isinstance(self.site_settings, list) + and parsed_url.netloc not in self.site_settings + ): + log.warning("Not notifying because url not permitted by site settings") + return False + return True + + def should_notify_comparison(self, comparison: Comparison) -> bool: + head_full_commit = comparison.head + if not match( + self.notifier_yaml_settings.get("branches"), head_full_commit.commit.branch + ): + log.warning( + "Not notifying because branch not in expected branches", + extra=dict( + commit=head_full_commit.commit.commitid, + repoid=head_full_commit.commit.repoid, + current_branch=head_full_commit.commit.branch, + branch_patterns=self.notifier_yaml_settings.get("branches"), + ), + ) + return False + if not self.is_above_threshold(comparison): + return False + return True + + @sentry_sdk.trace + def notify( + self, + comparison: ComparisonProxy, + status_or_checks_helper_text: Optional[dict[str, str]] = None, + ) -> NotificationResult: + filtered_comparison = comparison.get_filtered_comparison( + **self.get_notifier_filters() + ) + if self.should_notify_comparison(filtered_comparison): + result = self.do_notify(filtered_comparison) + else: + result = NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="Did not fit criteria", + data_sent=None, + ) + return result + + def get_notifier_filters(self) -> dict: + flag_list = self.notifier_yaml_settings.get("flags") or [] + return dict( + path_patterns=set( + get_paths_from_flags(self.current_yaml, flag_list) + + (self.notifier_yaml_settings.get("paths") or []) + ), + flags=flag_list, + ) + + def do_notify(self, comparison: Comparison) -> NotificationResult: + data = self.build_payload(comparison) + result = self.send_actual_notification(data) + return NotificationResult( + notification_attempted=result["notification_attempted"], + notification_successful=result["notification_successful"], + explanation=result["explanation"], + data_sent=data, + ) + + def is_above_threshold(self, comparison: Comparison): + head_full_commit = comparison.head + base_full_commit = comparison.project_coverage_base + threshold = self.notifier_yaml_settings.get("threshold") + if threshold is None: + return True + if not comparison.has_project_coverage_base_report(): + log.info( + "Cannot compare commits because base commit does not have a report", + extra=dict( + commit=head_full_commit.commit.commitid, + base_commit=base_full_commit.commit.commitid + if base_full_commit.commit + else None, + ), + ) + return False + if ( + base_full_commit.report.totals.coverage is None + or head_full_commit.report.totals.coverage is None + ): + log.info( + "Cannot compare commits because either base or head commit has no coverage information", + extra=dict( + commit=head_full_commit.commit.commitid, + base_commit=base_full_commit.commit.commitid + if base_full_commit.commit + else None, + ), + ) + return False + diff_coverage = Decimal(head_full_commit.report.totals.coverage) - Decimal( + base_full_commit.report.totals.coverage + ) + rounded_coverage = round_number(self.current_yaml, diff_coverage) + return rounded_coverage >= threshold + + def generate_compare_dict(self, comparison: Comparison): + head_full_commit = comparison.head + base_full_commit = comparison.project_coverage_base + if comparison.has_project_coverage_base_report(): + difference = Decimal(head_full_commit.report.totals.coverage) - Decimal( + base_full_commit.report.totals.coverage + ) + message = ( + "no change" + if difference == 0 + else "increased" + if difference > 0 + else "decreased" + ) + notation = "" if difference == 0 else "+" if difference > 0 else "-" + comparison_url = ( + get_pull_url(comparison.pull) + if comparison.pull + else get_commit_url(comparison.head.commit) + ) + else: + difference = None + message = "unknown" + notation = "" + comparison_url = None + return { + "url": comparison_url, + "message": message, + "coverage": round_number(self.current_yaml, difference) + if difference is not None + else None, + "notation": notation, + } + + def generate_message(self, comparison: Comparison): + if self.notifier_yaml_settings.get("message"): + return self.notifier_yaml_settings.get("message") + commit = comparison.head.commit + comparison_string = "" + if comparison.has_project_coverage_base_report(): + compare = self.generate_compare_dict(comparison) + comparison_string = self.COMPARISON_STRING.format( + compare_message=compare["message"], + compare_url=compare["url"], + compare_notation=compare["notation"], + compare_coverage=compare["coverage"], + ) + return self.BASE_MESSAGE.format( + head_url=get_commit_url(commit), + owner_username=commit.repository.owner.username, + repo_name=commit.repository.name, + comparison_string=comparison_string, + head_branch=commit.branch, + head_totals_c=comparison.head.report.totals.coverage, + head_short_commitid=commit.commitid[:7], + ) + + +class EnhancedJSONEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, Decimal): + return str(o) + return super().default(o) + + +class RequestsYamlBasedNotifier(StandardNotifier): + """ + This class is a small implementation detail for using `requests` package to communicate with + the server we want to notify + """ + + json_headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": "Codecov", + } + + def send_actual_notification(self, data: Mapping[str, Any]): + _timeouts = get_config("setup", "http", "timeouts", "external", default=10) + kwargs = dict(timeout=_timeouts, headers=self.json_headers) + try: + with metrics.timer( + f"worker.services.notifications.notifiers.{self.name}.actual_connection" + ): + with httpx.Client() as client: + res = client.post( + url=self.notifier_yaml_settings["url"], + data=json.dumps(data, cls=EnhancedJSONEncoder), + **kwargs, + ) + return { + "notification_attempted": True, + "notification_successful": res.status_code < 400, + "explanation": None if res.status_code else res.message, + } + except httpx.HTTPError: + log.warning( + "Unable to send notification to server due to a connection error", + exc_info=True, + ) + return { + "notification_attempted": True, + "notification_successful": False, + "explanation": "connection_issue", + } diff --git a/apps/worker/services/notification/notifiers/gitter.py b/apps/worker/services/notification/notifiers/gitter.py new file mode 100644 index 0000000000..78030cc3bb --- /dev/null +++ b/apps/worker/services/notification/notifiers/gitter.py @@ -0,0 +1,43 @@ +from shared.torngit.enums import Endpoints + +from database.enums import Notification +from services.notification.notifiers.generics import ( + Comparison, + RequestsYamlBasedNotifier, +) +from services.urls import get_commit_url + + +class GitterNotifier(RequestsYamlBasedNotifier): + # TODO (Thiago): Fix base message + BASE_MESSAGE = " ".join( + [ + "Coverage {comparison_string}on `{head_branch}` is `{head_totals_c}%`", + "via {head_url}", + ] + ) + + COMPARISON_STRING = "*{compare_message}* {compare_notation}{compare_coverage}% " + + @property + def notification_type(self) -> Notification: + return Notification.gitter + + def build_payload(self, comparison: Comparison) -> dict: + compare_dict = self.generate_compare_dict(comparison) + message = self.generate_message(comparison) + head_commit = comparison.head.commit + return { + "message": message, + "branch": head_commit.branch, + "pr": comparison.pull.pullid if comparison.pull else None, + "commit": head_commit.commitid, + "commit_short": head_commit.commitid[:7], + "text": compare_dict["message"], + "commit_url": self.repository_service.get_href( + Endpoints.commit_detail, commitid=head_commit.commitid + ), + "codecov_url": get_commit_url(head_commit), + "coverage": comparison.head.report.totals.coverage, + "coverage_change": compare_dict["coverage"], + } diff --git a/apps/worker/services/notification/notifiers/hipchat.py b/apps/worker/services/notification/notifiers/hipchat.py new file mode 100644 index 0000000000..e8cb8fd595 --- /dev/null +++ b/apps/worker/services/notification/notifiers/hipchat.py @@ -0,0 +1,101 @@ +from decimal import Decimal + +from database.enums import Notification +from services.notification.notifiers.generics import ( + Comparison, + RequestsYamlBasedNotifier, +) +from services.urls import get_commit_url, get_graph_url +from services.yaml.reader import round_number + + +class HipchatNotifier(RequestsYamlBasedNotifier): + # TODO (Thiago): Fix base message + BASE_MESSAGE = " ".join( + [ + 'Coverage for
{owner_username}/{repo_name}', + "{comparison_string}on {head_branch} is {head_totals_c}%", + '# via {head_short_commitid}', + ] + ) + + COMPARISON_STRING = "{compare_message} {compare_notation}{compare_coverage}% " + + @property + def notification_type(self) -> Notification: + return Notification.hipchat + + def build_payload(self, comparison: Comparison) -> dict: + card = None + head_commit = comparison.head.commit + commitid = head_commit.commitid + repository = comparison.head.commit.repository + head_url = get_commit_url(comparison.head.commit) + comparison_dict = self.generate_compare_dict(comparison) + if self.notifier_yaml_settings.get("card"): + compare = [] + if comparison.project_coverage_base.report is not None: + compare = [ + { + "label": "Compare", + "value": { + "style": {"+": "lozenge-success", "-": "lozenge-error"}.get( + comparison_dict["notation"], "lozenge-current" + ), + "label": "{0}{1}%".format( + comparison_dict["notation"], + round_number( + self.current_yaml, comparison_dict["coverage"] + ), + ), + }, + } + ] + card = { + "id": commitid, + "title": f"Codecov \u22c5 {repository.slug} on {head_commit.branch}", + "style": "application", + "format": "compact", + "url": head_url, + "icon": { + "url": get_graph_url( + comparison.head.commit, "sunburst.svg", size=100 + ) + }, + "attributes": [ + { + "label": "Author", + "value": { + "url": head_url, + "label": comparison.head.commit.author.username, + }, + }, + { + "label": "Commit", + "value": {"url": head_url, "label": commitid[:7]}, + }, + ] + + compare, + "description": { + "value": "Coverage for {0} on {1} is now {2}%".format( + repository.slug, + head_commit.branch, + round_number( + self.current_yaml, + Decimal(comparison.head.report.totals.coverage), + ), + ), + "format": "html", + }, + } + message = self.generate_message(comparison) + return { + "from": "Codecov", + "card": card, + "message": message, + "color": {"+": "green", "-": "red"}.get( + comparison_dict["notation"], "gray" + ), + "notify": self.notifier_yaml_settings.get("notify", False), + "message_format": "html", + } diff --git a/apps/worker/services/notification/notifiers/irc.py b/apps/worker/services/notification/notifiers/irc.py new file mode 100644 index 0000000000..99836dc75b --- /dev/null +++ b/apps/worker/services/notification/notifiers/irc.py @@ -0,0 +1,91 @@ +import logging +import socket +from io import BytesIO +from typing import Any, List, Mapping + +from database.enums import Notification +from services.notification.notifiers.generics import Comparison, StandardNotifier + +log = logging.getLogger(__name__) + + +class IRCClient(object): + def __init__(self, server) -> None: + self.server = server + server = tuple(self.server.split(":")) + (6667,) + self.con = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.con.settimeout(1) + self.con.connect((server[0], int(server[1]))) + + def send(self, message: str): + message = message + "\r\n" + return self.con.send(message.encode()) + + def close(self) -> None: + self.con.close() + + def receive_everything(self) -> List[str]: + received_messages = BytesIO() + try: + while 1: + data = self.con.recv(1024) + received_messages.write(data) + if not data: + break + except socket.timeout: + log.debug("Message sent") + final_data = received_messages.getvalue().decode().split("\r\n") + for line in final_data: + if line.startswith("PING"): + message_to_respond = line.split(" ")[1] + self.send(f"PONG {message_to_respond}") + return final_data + + +class IRCNotifier(StandardNotifier): + BASE_MESSAGE = " ".join( + [ + "Coverage for {owner_username}/{repo_name}", + "{comparison_string}on `{head_branch}` is `{head_totals_c}%`", + "via `{head_short_commitid}`", + ] + ) + + COMPARISON_STRING = "*{compare_message}* `{compare_notation}{compare_coverage}%` " + + @property + def notification_type(self) -> Notification: + return Notification.irc + + def send_actual_notification(self, data: Mapping[str, Any]) -> dict: + # https://github.com/travis-ci/travis-tasks/blob/94c97165d7ecf89d986609614667ae86dff7e9ce/lib/travis/addons/irc/client.rb + message = data["message"] + con = IRCClient(self.notifier_yaml_settings["server"]) + if self.notifier_yaml_settings.get("password"): + command_to_send = "PASS %s" % self.notifier_yaml_settings["password"] + con.send(command_to_send) + con.send("USER codecov codecov codecov :codecov") + con.send("NICK codecov") + con.receive_everything() + if self.notifier_yaml_settings.get("nickserv_password"): + nickserv_password = self.notifier_yaml_settings["nickserv_password"] + command_to_send = f"PRIVMSG NickServ :IDENTIFY {nickserv_password}" + con.send(command_to_send) + con.receive_everything() + if self.notifier_yaml_settings["channel"][0] == "#": + command = "JOIN %s \n" % self.notifier_yaml_settings["channel"] + con.send(command) + con.receive_everything() + + status = ( + "NOTICE" if self.notifier_yaml_settings.get("notice", True) else "PRIVMSG" + ) + channel = self.notifier_yaml_settings["channel"] + message_to_send = f"{status} {channel} :{message} " + con.send(message_to_send) + con.receive_everything() + con.close() + return {"successful": True, "reason": None} + + def build_payload(self, comparison: Comparison) -> dict: + return {"message": self.generate_message(comparison)} diff --git a/apps/worker/services/notification/notifiers/mixins/__init__.py b/apps/worker/services/notification/notifiers/mixins/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/notification/notifiers/mixins/message/__init__.py b/apps/worker/services/notification/notifiers/mixins/message/__init__.py new file mode 100644 index 0000000000..106d88c3a2 --- /dev/null +++ b/apps/worker/services/notification/notifiers/mixins/message/__init__.py @@ -0,0 +1,227 @@ +import logging +from typing import Any, Callable, Mapping, Optional + +from shared.django_apps.core.models import Repository +from shared.plan.constants import TierName +from shared.plan.service import PlanService +from shared.reports.resources import ReportTotals + +from database.models.core import Owner +from helpers.environment import is_enterprise +from services.comparison import ComparisonProxy, FilteredComparison +from services.notification.notifiers.mixins.message.helpers import ( + should_message_be_compact, +) +from services.notification.notifiers.mixins.message.sections import get_message_layout +from services.notification.notifiers.mixins.message.writers import TeamPlanWriter +from services.urls import get_commit_url, get_pull_url +from services.yaml.reader import read_yaml_field + +log = logging.getLogger(__name__) + + +class MessageMixin(object): + def create_message( + self, + comparison: ComparisonProxy | FilteredComparison, + pull_dict: Optional[Mapping[str, Any]], + yaml_settings: dict, + status_or_checks_helper_text: Optional[dict[str, str]] = None, + ): + """ + Assemble the various components of the PR comments message in accordance with their YAML configuration. + See https://docs.codecov.io/docs/pull-request-comments for more context on the different parts of a PR comment. + + Returns the PR comment message as a list of strings, where each item in the list corresponds to a line in the comment. + + Parameters: + yaml_settings: YAML settings for notifier + + Note: Github Checks Notifiers are initialized with "status" YAML settings. + Thus, the comment block of the codecov YAML is passed as the "yaml_settings" parameter for these Notifiers. + + """ + changes = comparison.get_changes() + diff = comparison.get_diff(use_original_base=True) + behind_by = comparison.get_behind_by() + base_report = comparison.project_coverage_base.report + head_report = comparison.head.report + pull = comparison.pull + + settings = yaml_settings + current_yaml = self.current_yaml + + links = { + "pull": get_pull_url(pull), + "base": ( + get_commit_url(comparison.project_coverage_base.commit) + if comparison.project_coverage_base.commit is not None + else None + ), + "head": get_commit_url(comparison.head.commit), + } + + # bool: show complexity + if read_yaml_field(current_yaml, ("codecov", "ui", "hide_complexity")): + show_complexity = False + else: + show_complexity = bool( + (base_report.totals if base_report else ReportTotals()).complexity + or (head_report.totals if head_report else ReportTotals()).complexity + ) + + message: list[str] = [] + # note: since we're using append, calling write("") will add a newline to the message + write = message.append + + self._possibly_write_install_app(comparison, write) + + # Write Header + write(f"## [Codecov]({links['pull']}?dropdown=coverage&src=pr&el=h1) Report") + + repo = comparison.head.commit.repository + owner: Owner = repo.owner + + # Separate PR comment based on plan that can't/won't be tweaked by codecov.yml settings + owner_plan = PlanService(owner) + if owner_plan.tier_name == TierName.TEAM.value: + return self._team_plan_notification( + comparison=comparison, + message=message, + diff=diff, + settings=settings, + links=links, + current_yaml=current_yaml, + ) + + sections = get_message_layout(settings, status_or_checks_helper_text) + + for upper_section_name, section_writer_class in sections.upper: + section_writer = section_writer_class( + self.repository, + upper_section_name, + show_complexity, + settings, + current_yaml, + status_or_checks_helper_text, + ) + self.write_section_to_msg( + comparison, changes, diff, links, write, section_writer, behind_by + ) + + if head_report: + is_compact_message = sections.middle and should_message_be_compact( + comparison, settings + ) + if is_compact_message: + write( + "
Additional details and impacted files\n" + ) + write("") + + for layout, section_writer_class in sections.middle: + section_writer = section_writer_class( + self.repository, layout, show_complexity, settings, current_yaml + ) + self.write_section_to_msg( + comparison, changes, diff, links, write, section_writer + ) + + if is_compact_message: + write("
") + + for lower_section_name, section_writer_class in sections.lower: + section_writer = section_writer_class( + self.repository, + lower_section_name, + show_complexity, + settings, + current_yaml, + ) + self.write_section_to_msg( + comparison, changes, diff, links, write, section_writer + ) + + # TODO(swatinem): should this rather be part of the `announcements` section + self.write_cross_pollination_message(write=write) + + return [m for m in message if m is not None] + + def _possibly_write_install_app( + self, comparison: ComparisonProxy, write: Callable + ) -> None: + """Write a message if the user does not have any GH installations + and will be writing with a Codecov Commenter Account. + """ + repo: Repository = comparison.head.commit.repository + repo_owner: Owner = repo.owner + if ( + repo_owner.service == "github" + and not is_enterprise() + and repo_owner.github_app_installations == [] + and comparison.context.gh_is_using_codecov_commenter + ): + message_to_display = ":warning: Please install the !['codecov app svg image'](https://github.com/codecov/engineering-team/assets/152432831/e90313f4-9d3a-4b63-8b54-cfe14e7ec20d) to ensure uploads and comments are reliably processed by Codecov." + write(message_to_display) + write("") + + def _team_plan_notification( + self, + comparison: ComparisonProxy, + message: list[str], + diff, + settings, + links, + current_yaml, + ) -> list[str]: + writer_class = TeamPlanWriter() + + # Settings here enable failed tests results for now as a new product + message.extend( + writer_class.header_lines( + comparison=comparison, diff=diff, settings=settings + ) + ) + message.extend( + writer_class.middle_lines( + comparison=comparison, diff=diff, links=links, current_yaml=current_yaml + ) + ) + message.extend(writer_class.footer_lines(comparison)) + + return message + + def write_section_to_msg( + self, comparison, changes, diff, links, write, section_writer, behind_by=None + ): + wrote_something = False + for line in section_writer.write_section( + comparison, diff, changes, links, behind_by=behind_by + ): + wrote_something |= line is not None + write(line) + if wrote_something: + write("") + + def write_cross_pollination_message(self, write: Callable): + extra_message = [] + + ta_message = "- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems." + ba_message = "- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges." + + if not self.repository.test_analytics_enabled: + extra_message.append(ta_message) + + if not self.repository.bundle_analysis_enabled and set( + {"javascript", "typescript"} + ).intersection(self.repository.languages or {}): + extra_message.append(ba_message) + + if extra_message: + for i in [ + "
:rocket: New features to boost your workflow: ", + "", + *extra_message, + "
", + ]: + write(i) diff --git a/apps/worker/services/notification/notifiers/mixins/message/helpers.py b/apps/worker/services/notification/notifiers/mixins/message/helpers.py new file mode 100644 index 0000000000..81ab6f166a --- /dev/null +++ b/apps/worker/services/notification/notifiers/mixins/message/helpers.py @@ -0,0 +1,392 @@ +import re +from decimal import Decimal +from typing import List, Sequence + +from shared.reports.resources import ReportTotals +from shared.yaml.user_yaml import UserYaml + +from services.comparison import ComparisonProxy +from services.comparison.changes import Change +from services.yaml import read_yaml_field +from services.yaml.reader import get_minimum_precision, round_number + +zero_change_regex = re.compile("0.0+%?") + + +def has_project_status(yaml: UserYaml) -> bool: + project_status_details = read_yaml_field( + yaml, ("coverage", "status", "project"), False + ) + if isinstance(project_status_details, bool): + return project_status_details + # If it's not a bool, it has to be a dict + if isinstance(project_status_details, dict): + if "enabled" in project_status_details: + return project_status_details["enabled"] + return True + # The config is not according to what we expect + # So it's an invalid status definition + return False + + +def is_coverage_drop_significant(comparison: ComparisonProxy) -> bool: + head_coverage = ( + comparison.head.report.totals.coverage if comparison.has_head_report() else None + ) + base_coverage = ( + comparison.project_coverage_base.report.totals.coverage + if comparison.has_project_coverage_base_report() + else None + ) + if head_coverage is None or base_coverage is None: + # No change to be significant + return False + diff = Decimal(head_coverage) - Decimal(base_coverage) + change_is_positive = diff >= 0 + # head_coverage is the percent of the project covered in HEAD + # base_coverage is the percent of the project covered in BASE + # So "change_is_significant" is checking if the diff between them is more than 5% + change_is_significant = abs(diff) >= Decimal(5) + return not change_is_positive and change_is_significant + + +def make_metrics(before, after, relative, show_complexity, yaml, pull_url=None): + coverage_good = None + icon = " |" + if after is None: + # e.g. missing flags + coverage = " `?` |" + complexity = " `?` |" if show_complexity else "" + + elif after is False: + # e.g. file deleted + coverage = " |" + complexity = " |" if show_complexity else "" + + else: + if isinstance(before, list): + before = ReportTotals(*before) + if isinstance(after, list): + after = ReportTotals(*after) + + layout = " `{absolute} <{relative}> ({impact})` |" + + if ( + before + and before.coverage is not None + and after + and after.coverage is not None + ): + coverage_change = float(after.coverage) - float(before.coverage) + else: + coverage_change = None + coverage_good = (coverage_change > 0) if coverage_change is not None else None + coverage = layout.format( + absolute=format_number_to_str( + yaml, after.coverage, style="{0}%", if_null="\u2205" + ), + relative=format_number_to_str( + yaml, relative.coverage if relative else 0, style="{0}%", if_null="\xf8" + ), + impact=( + format_number_to_str( + yaml, + coverage_change, + style="{0}%", + if_zero="\xf8", + if_null="\u2205", + plus=True, + ) + if before + else "?" + if before is None + else "\xf8" + ), + ) + + if show_complexity: + is_string = isinstance(relative.complexity if relative else "", str) + style = "{0}%" if is_string else "{0}" + complexity_change = ( + Decimal(after.complexity) - Decimal(before.complexity) + if before + else None + ) + complexity_good = (complexity_change < 0) if before else None + complexity = layout.format( + absolute=style.format(format_number_to_str(yaml, after.complexity)), + relative=style.format( + format_number_to_str( + yaml, relative.complexity if relative else 0, if_null="\xf8" + ) + ), + impact=style.format( + format_number_to_str( + yaml, + complexity_change, + if_zero="\xf8", + if_null="\xf8", + plus=True, + ) + if before + else "?" + ), + ) + + show_up_arrow = coverage_good and complexity_good + show_down_arrow = (coverage_good is False and coverage_change != 0) and ( + complexity_good is False and complexity_change != 0 + ) + icon = ( + " :arrow_up: |" + if show_up_arrow + else " :arrow_down: |" + if show_down_arrow + else " |" + ) + + else: + complexity = "" + icon = ( + " :arrow_up: |" + if coverage_good + else ( + " :arrow_down: |" + if coverage_good is False and coverage_change != 0 + else " |" + ) + ) + + return "".join(("|", coverage, complexity, icon)) + + +def make_patch_only_metrics(before, after, relative, show_complexity, yaml, pull_url): + if after is None: + # e.g. missing flags + coverage = " `?` |" + missing_line_str = " `?` |" + + elif after is False: + # e.g. file deleted + coverage = " |" + missing_line_str = " |" + + else: + patch_cov = format_number_to_str( + yaml, relative.coverage if relative else 0, style="{0}%", if_null="\xf8" + ) + coverage = f" {patch_cov} |" + missing_lines = relative.misses if relative else 0 + partials = relative.partials if relative else 0 + s = "s" if partials > 1 else "" + partials_str = "{n} partial{s}".format( + n=partials, + s=s, + ) + missing_line_str = ( + " [{m} Missing {partials}:warning: ]({pull_url}?src=pr&el=tree) |".format( + m=missing_lines, + partials=f"and {partials_str} " if partials else "", + pull_url=pull_url, + ) + ) + return "".join(("|", coverage, missing_line_str)) + + +def get_table_header(show_complexity): + return ( + "| Coverage \u0394 |" + + (" Complexity \u0394 |" if show_complexity else "") + + " |" + ) + + +def get_table_layout(show_complexity): + return "|---|---|---|" + ("---|" if show_complexity else "") + + +def format_number_to_str( + yml, value, if_zero=None, if_null=None, plus=False, style="{0}" +) -> str: + if value is None: + return if_null + precision = get_minimum_precision(yml) + value = Decimal(value) + res = round_number(yml, value) + + if if_zero and value == 0: + return if_zero + + if res == 0 and value != 0: + # <.01 + return style.format( + "%s<%s" + % ("+" if plus and value > 0 else "" if value > 0 else "-", precision) + ) + + if plus and res > Decimal("0"): + res = "+" + str(res) + return style.format(res) + + +def add_plus_sign(value: str) -> str: + if value in ("", "0", "0%") or zero_change_regex.fullmatch(value): + return "" + elif value[0] != "-": + return "+%s" % value + else: + return value + + +def list_to_text_table(rows, padding=0) -> List[str]: + """ + Assumes align left. + + list_to_text_table( + [ + ('|##', 'master|', 'stable|', '+/-|', '##|'), + ('+', '1|', '2|', '+1', ''), + ], 2) == ['## master stable +/- ##', + '+ 1 2 +1 '] + + """ + # (2, 6, 6, 3, 2) + column_w = list( + map( + max, + zip(*map(lambda row: map(lambda cell: len(cell.strip("|")), row), rows)), + ) + ) + + def _fill(a): + w, cell = a + return "{text:{fill}{align}{width}}".format( + text=cell.strip("|"), + fill=" ", + align=(("^" if cell[:1] == "|" else ">") if cell[-1:] == "|" else "<"), + width=w, + ) + + # now they are filled with spaces + spacing = (" " * padding).join + return list(map(lambda row: spacing(map(_fill, zip(column_w, row))), rows)) + + +def diff_to_string(current_yaml, base_title, base, head_title, head) -> List[str]: + """ + ('master', {}, + 'stable', {}, + ('ui', before, after), ...}) + """ + + def F(value): + if value is None: + return "?" + elif isinstance(value, str): + return "%s%%" % round_number(current_yaml, Decimal(value)) + else: + return value + + def _row(title, c1, c2, plus="+", minus="-", neutral=" "): + if c1 == c2 == 0: + return ("", "", "", "", "") + else: + # TODO if coverage format to smallest string or precision + if c1 is None or c2 is None: + change = "" + elif isinstance(c2, str) or isinstance(c1, str): + change = F(str(float(c2) - float(c1))) + else: + change = str(c2 - c1) + change_is_zero = change in ("0", "0%", "") or zero_change_regex.fullmatch( + change + ) + sign = neutral if change_is_zero else plus if change[0] != "-" else minus + return ( + "%s %s" % (sign, title), + "%s|" % F(c1), + "%s|" % F(c2), + "%s|" % add_plus_sign(change), + "", + ) + + c = int(isinstance(base.complexity, str)) if base else 0 + # create a spaced table with data + table = list_to_text_table( + [ + ("|##", "%s|" % base_title, "%s|" % head_title, "+/-|", "##|"), + _row("Coverage", base.coverage if base else None, head.coverage, "+", "-"), + _row( + "Complexity", + base.complexity if base else None, + head.complexity, + "-+"[c], + "+-"[c], + ), + _row("Files", base.files if base else None, head.files, " ", " "), + _row("Lines", base.lines if base else None, head.lines, " ", " "), + _row("Branches", base.branches if base else None, head.branches, " ", " "), + _row("Hits", base.hits if base else None, head.hits, "+", "-"), + _row("Misses", base.misses if base else None, head.misses, "-", "+"), + _row("Partials", base.partials if base else None, head.partials, "-", "+"), + ], + 3, + ) + row_w = len(table[0]) + + spacer = ["=" * row_w] + + title = "@@%s@@" % "{text:{fill}{align}{width}}".format( + text="Coverage Diff", + fill=" ", + align="^", + width=row_w - 4, + ) + + table = ( + [title, table[0]] + + spacer + + table[1:3] + + spacer # coverage, complexity + + table[3:6] + + spacer # files, lines, branches + + table[6:9] # hits, misses, partials + ) + + # no complexity included + if head.complexity in (None, 0): + table.pop(4) + + return "\n".join(filter(lambda row: row.strip(" "), table)).strip("=").split("\n") + + +def sort_by_importance(changes: Sequence[Change]) -> List[Change]: + return sorted( + changes or [], + key=lambda c: (float((c.totals or ReportTotals()).coverage), c.new, c.deleted), + ) + + +def ellipsis(text, length, cut_from="left") -> str: + if cut_from == "right": + return (text[:length] + "...") if len(text) > length else text + elif cut_from is None: + return ( + (text[: (length / 2)] + "..." + text[(length / -2) :]) + if len(text) > length + else text + ) + else: + return ("..." + text[len(text) - length :]) if len(text) > length else text + + +def escape_markdown(value: str) -> str: + return value.replace("`", "\\`").replace("*", "\\*").replace("_", "\\_") + + +def should_message_be_compact(comparison, settings): + # bitbucket doesnt support
+ supported_services = ("github", "gitlab") + if comparison.repository_service.service not in supported_services: + return False + return settings.get("hide_comment_details", False) diff --git a/apps/worker/services/notification/notifiers/mixins/message/sections.py b/apps/worker/services/notification/notifiers/mixins/message/sections.py new file mode 100644 index 0000000000..bf41f332f2 --- /dev/null +++ b/apps/worker/services/notification/notifiers/mixins/message/sections.py @@ -0,0 +1,773 @@ +import logging +import random +from base64 import b64encode +from dataclasses import dataclass +from decimal import Decimal +from enum import Enum, auto +from itertools import starmap +from urllib.parse import urlencode + +from shared.helpers.yaml import walk +from shared.reports.resources import Report +from shared.validation.helpers import LayoutStructure + +from helpers.environment import is_enterprise +from helpers.reports import get_totals_from_file_in_reports +from services.comparison import ComparisonProxy +from services.comparison.types import ReportUploadedCount +from services.notification.notifiers.mixins.message.helpers import ( + diff_to_string, + ellipsis, + escape_markdown, + get_table_header, + get_table_layout, + has_project_status, + is_coverage_drop_significant, + make_metrics, + make_patch_only_metrics, +) +from services.urls import get_commit_url_from_commit_sha, get_pull_graph_url +from services.yaml.reader import get_components_from_yaml, round_number + +log = logging.getLogger(__name__) + +ALL_TESTS_PASSED_MSG = ":white_check_mark: All tests successful. No failed tests found." + +SectionList = list[tuple[str, type["BaseSectionWriter"]]] + + +@dataclass +class MessageLayout: + upper: SectionList + middle: SectionList + lower: SectionList + + +HEADER_SECTIONS = {"header", "newheader", "condensed_header"} + + +def get_message_layout( + settings, status_or_checks_helper_text: dict[str, str] | None +) -> MessageLayout: + sections = [s.strip() for s in settings.get("layout", "").split(",")] + + # force at least the `condensed_header` if no header has been configured + if not any(s in HEADER_SECTIONS for s in sections): + sections.insert(0, "condensed_header") + + # append the `newfiles` section if there is a `files` or `tree` section + if "files" in sections or "tree" in sections: + sections.append("newfiles") + + upper: SectionList = [] + middle: SectionList = [] + lower: SectionList = [] + + for section in sections: + if section in HEADER_SECTIONS: + upper.append((section, HeaderSectionWriter)) + elif section == "newfiles" or section == "condensed_files": + upper.append((section, NewFilesSectionWriter)) + + elif section.startswith("flag"): + middle.append((section, FlagSectionWriter)) + elif section == "diff": + middle.append((section, DiffSectionWriter)) + elif section.startswith(("files", "tree")): + middle.append((section, FileSectionWriter)) + elif section == "reach": + middle.append((section, ReachSectionWriter)) + elif section == "announcements": + middle.append((section, AnnouncementSectionWriter)) + elif section.startswith("component"): + middle.append((section, ComponentsSectionWriter)) + elif section == "footer": + middle.append((section, FooterSectionWriter)) + + elif section == "newfooter" or section == "condensed_footer": + lower.append(("newfooter", NewFooterSectionWriter)) + + elif section not in LayoutStructure.acceptable_objects: + log.warning("Improper layout name", extra={"layout": section}) + + # append the helper text and messages to user to the upper section + if status_or_checks_helper_text: + upper.append(("status_or_checks_helper_text", HelperTextSectionWriter)) + upper.append(("messages_to_user", MessagesToUserSectionWriter)) + + return MessageLayout(upper, middle, lower) + + +class BaseSectionWriter(object): + def __init__( + self, + repository, + layout, + show_complexity, + settings, + current_yaml, + status_or_checks_helper_text=None, + ): + self.repository = repository + self.layout = layout + self.show_complexity = show_complexity + self.settings = settings + self.current_yaml = current_yaml + self.status_or_checks_helper_text = status_or_checks_helper_text + + @property + def name(self): + return self.__class__.__name__ + + def write_section(self, *args, **kwargs) -> list[str]: + return list(self.do_write_section(*args, **kwargs)) + + +class NewFooterSectionWriter(BaseSectionWriter): + def do_write_section(self, comparison, diff, changes, links, behind_by=None): + hide_project_coverage = self.settings.get("hide_project_coverage", False) + if hide_project_coverage: + yield "" + yield ":loudspeaker: Thoughts on this report? [Let us know!]({0})".format( + "https://about.codecov.io/pull-request-comment-report/" + ) + + else: + repo_service = comparison.repository_service.service + yield "" + yield "[:umbrella: View full report in Codecov by Sentry]({0}?dropdown=coverage&src=pr&el=continue). ".format( + links["pull"] + ) + yield ":loudspeaker: Have feedback on the report? [Share it here]({0}).".format( + "https://about.codecov.io/codecov-pr-comment-feedback/" + if repo_service == "github" + else "https://gitlab.com/codecov-open-source/codecov-user-feedback/-/issues/4" + ) + + +class HeaderSectionWriter(BaseSectionWriter): + def _possibly_include_test_result_setup_confirmation(self, comparison): + if ta_error_msg := comparison.test_results_error(): + yield "" + yield ta_error_msg + elif comparison.all_tests_passed(): + yield "" + yield ALL_TESTS_PASSED_MSG + + def do_write_section(self, comparison, diff, changes, links, behind_by=None): + yaml = self.current_yaml + base_report = comparison.project_coverage_base.report + head_report = comparison.head.report + pull_dict = comparison.enriched_pull.provider_pull + + diff_totals = head_report.apply_diff(diff) + if diff_totals: + misses_and_partials = diff_totals.misses + diff_totals.partials + patch_coverage = diff_totals.coverage + else: + misses_and_partials = None + patch_coverage = None + if misses_and_partials: + ln_text = "lines" if misses_and_partials > 1 else "line" + yield ( + f"Attention: Patch coverage is `{patch_coverage}%` with `{misses_and_partials} {ln_text}` in your changes missing coverage. Please review." + ) + else: + yield "All modified and coverable lines are covered by tests :white_check_mark:" + + hide_project_coverage = self.settings.get("hide_project_coverage", False) + if hide_project_coverage: + for msg in self._possibly_include_test_result_setup_confirmation( + comparison + ): + yield msg + return + + if base_report and head_report: + yield ( + "> Project coverage is {head_cov}%. Comparing base [(`{commitid_base}`)]({links[base]}?dropdown=coverage&el=desc) to head [(`{commitid_head}`)]({links[head]}?dropdown=coverage&el=desc).".format( + commitid_head=comparison.head.commit.commitid[:7], + commitid_base=comparison.project_coverage_base.commit.commitid[:7], + links=links, + head_cov=round_number(yaml, Decimal(head_report.totals.coverage)), + ) + ) + else: + # This doesn't actually emit a message if the _head_ report is missing + # Because we don't notify if the _head_ report is missing + # But it's still used if the base report is missing. + # Why didn't you change the condition and the code then? Idk... maybe I'm wrong in my assumptions :P + what = "BASE" if not base_report else "HEAD" + branch = pull_dict["base" if not base_report else "head"]["branch"] + commit = pull_dict["base" if not base_report else "head"]["commitid"][:7] + yield ( + f"> Please [upload](https://docs.codecov.com/docs/codecov-uploader) report for {what} (`{branch}@{commit}`). [Learn more](https://docs.codecov.io/docs/error-reference#section-missing-{what.lower()}-commit) about missing {what} report." + ) + + if behind_by: + yield ( + f"> Report is {behind_by} commits behind head on {pull_dict['base']['branch']}." + ) + if ( + comparison.enriched_pull.provider_pull is not None + and comparison.head.commit.commitid + != comparison.enriched_pull.provider_pull["head"]["commitid"] + ): + # Temporary log so we understand when this happens + log.info( + "Notifying user that current head and pull head differ", + extra=dict( + repoid=comparison.head.commit.repoid, + commit=comparison.head.commit.commitid, + pull_head=comparison.enriched_pull.provider_pull["head"][ + "commitid" + ], + ), + ) + yield "" + pull_head = comparison.enriched_pull.provider_pull["head"]["commitid"][:7] + current_head = comparison.head.commit.commitid[:7] + yield ( + f"> :exclamation: **Current head {current_head} differs from pull request most recent head {pull_head}**" + ) + yield "> " + yield f"> Please [upload](https://docs.codecov.com/docs/codecov-uploader) reports for the commit {pull_head} to get more accurate results." + + for msg in self._possibly_include_test_result_setup_confirmation(comparison): + yield msg + + +class AnnouncementSectionWriter(BaseSectionWriter): + current_active_messages = [ + "Codecov offers a browser extension for seamless coverage viewing on GitHub. Try it in [Chrome](https://chrome.google.com/webstore/detail/codecov/gedikamndpbemklijjkncpnolildpbgo) or [Firefox](https://addons.mozilla.org/en-US/firefox/addon/codecov/) today!" + # "Codecov can now indicate which changes are the most critical in Pull Requests. [Learn more](https://about.codecov.io/product/feature/runtime-insights/)" # This is disabled as of CODE-1885. But we might bring it back later. + ] + + def do_write_section(self, comparison: ComparisonProxy, *args, **kwargs): + # This allows us to shift through active messages while respecting the annoucement limit. + message_to_display = random.choice( + AnnouncementSectionWriter.current_active_messages + ) + + yield f":mega: {message_to_display}" + + +class FooterSectionWriter(BaseSectionWriter): + def do_write_section(self, comparison, diff, changes, links, behind_by=None): + pull_dict = comparison.enriched_pull.provider_pull + yield "------" + yield "" + yield ( + "[Continue to review full report in Codecov by Sentry]({0}?dropdown=coverage&src=pr&el=continue).".format( + links["pull"] + ) + ) + yield "> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)" + yield "> `\u0394 = absolute (impact)`, `\xf8 = not affected`, `? = missing data`" + + yield ( + "> Powered by [Codecov]({pull}?dropdown=coverage&src=pr&el=footer). Last update [{base}...{head}]({pull}?dropdown=coverage&src=pr&el=lastupdated). Read the [comment docs]({comment}).".format( + pull=links["pull"], + base=pull_dict["base"]["commitid"][:7], + head=pull_dict["head"]["commitid"][:7], + comment="https://docs.codecov.io/docs/pull-request-comments", + ) + ) + + +class ReachSectionWriter(BaseSectionWriter): + def do_write_section(self, comparison, diff, changes, links, behind_by=None): + pull = comparison.enriched_pull.database_pull + yield ( + "[![Impacted file tree graph]({})]({}?src=pr&el=tree)".format( + get_pull_graph_url( + pull, + "tree.svg", + width=650, + height=150, + src="pr", + token=pull.repository.image_token, + ), + links["pull"], + ) + ) + + +class DiffSectionWriter(BaseSectionWriter): + def do_write_section(self, comparison, diff, changes, links, behind_by=None): + base_report = comparison.project_coverage_base.report + head_report = comparison.head.report + if base_report is None: + base_report = Report() + pull_dict = comparison.enriched_pull.provider_pull + pull = comparison.enriched_pull.database_pull + yield "```diff" + lines = diff_to_string( + self.current_yaml, + pull_dict["base"]["branch"], # important because base may be null + base_report.totals if base_report else None, + "#%s" % pull.pullid, + head_report.totals, + ) + yield from lines + yield "```" + + +def _get_tree_cell(typ, path, metrics, compare, is_critical): + return "| {rm}[{path}]({compare}?src=pr&el=tree&{path_as_query_param}#diff-{hash}){rm}{file_tags} {metrics}".format( + rm="~~" if typ == "deleted" else "", + path=escape_markdown(ellipsis(path, 50, False)), + path_as_query_param=urlencode({"filepath": path}), + compare=compare, + hash=b64encode(path.encode()).decode(), + metrics=metrics, + file_tags=" **Critical**" if is_critical else "", + ) + + +class NewFilesSectionWriter(BaseSectionWriter): + def do_write_section(self, comparison, diff, changes, links, behind_by=None): + # create list of files changed in diff + base_report = comparison.project_coverage_base.report + head_report = comparison.head.report + if base_report is None: + base_report = Report() + files_in_diff = [ + ( + _diff["type"], + path, + make_patch_only_metrics( + get_totals_from_file_in_reports(base_report, path) or False, + get_totals_from_file_in_reports(head_report, path) or False, + _diff["totals"], + self.show_complexity, + self.current_yaml, + links["pull"], + ), + int(_diff["totals"].misses + _diff["totals"].partials), + ) + for path, _diff in (diff["files"] if diff else {}).items() + if _diff.get("totals") + ] + + all_files = set(f[1] for f in files_in_diff or []) | set( + c.path for c in changes or [] + ) + if files_in_diff: + table_header = "| Patch % | Lines |" + table_layout = "|---|---|---|" + + # get limit of results to show + limit = int(self.layout.split(":")[1] if ":" in self.layout else 10) + mentioned = [] + files_in_critical = set() + + def tree_cell(typ, path, metrics, _=None): + if path not in mentioned: + # mentioned: for files that are in diff and changes + mentioned.append(path) + return _get_tree_cell( + typ=typ, + path=path, + metrics=metrics, + compare=links["pull"], + is_critical=path in files_in_critical, + ) + + remaining_files = 0 + printed_files = 0 + changed_files = sorted( + files_in_diff, key=lambda a: a[3] or Decimal("0"), reverse=True + ) + changed_files_with_missing_lines = [f for f in changed_files if f[3] > 0] + if changed_files_with_missing_lines: + yield ( + "| [Files with missing lines]({0}?dropdown=coverage&src=pr&el=tree) {1}".format( + links["pull"], table_header + ) + ) + yield table_layout + for file in changed_files_with_missing_lines: + if printed_files == limit: + remaining_files += 1 + else: + printed_files += 1 + yield tree_cell(file[0], file[1], file[2]) + if remaining_files: + yield ( + "| ... and [{n} more]({href}?src=pr&el=tree-more) | |".format( + n=remaining_files, href=links["pull"] + ) + ) + + +class FileSectionWriter(BaseSectionWriter): + def do_write_section(self, comparison, diff, changes, links, behind_by=None): + # create list of files changed in diff + base_report = comparison.project_coverage_base.report + head_report = comparison.head.report + if base_report is None: + base_report = Report() + files_in_diff = [ + ( + _diff["type"], + path, + make_metrics( + get_totals_from_file_in_reports(base_report, path) or False, + get_totals_from_file_in_reports(head_report, path) or False, + _diff["totals"], + self.show_complexity, + self.current_yaml, + links["pull"], + ), + int(_diff["totals"].misses + _diff["totals"].partials), + ) + for path, _diff in (diff["files"] if diff else {}).items() + if _diff.get("totals") + ] + + all_files = set(f[1] for f in files_in_diff or []) | set( + c.path for c in changes or [] + ) + if files_in_diff: + table_header = get_table_header(self.show_complexity) + table_layout = get_table_layout(self.show_complexity) + + # get limit of results to show + limit = int(self.layout.split(":")[1] if ":" in self.layout else 10) + mentioned = [] + files_in_critical = set() + + def tree_cell(typ, path, metrics, _=None): + if path not in mentioned: + # mentioned: for files that are in diff and changes + mentioned.append(path) + return _get_tree_cell( + typ=typ, + path=path, + metrics=metrics, + compare=links["pull"], + is_critical=path in files_in_critical, + ) + + yield ( + "| [Files with missing lines]({0}?dropdown=coverage&src=pr&el=tree) {1}".format( + links["pull"], table_header + ) + ) + yield table_layout + yield from starmap( + tree_cell, + sorted(files_in_diff, key=lambda a: a[3] or Decimal("0"))[:limit], + ) + remaining = len(files_in_diff) - limit + if remaining > 0: + yield ( + "| ... and [{n} more]({href}?src=pr&el=tree-more) | |".format( + n=remaining, href=links["pull"] + ) + ) + + if changes: + len_changes_not_in_diff = len(all_files or []) - len(files_in_diff or []) + if files_in_diff and len_changes_not_in_diff > 0: + yield "" + yield ( + "... and [{n} file{s} with indirect coverage changes]({href}/indirect-changes?src=pr&el=tree-more)".format( + n=len_changes_not_in_diff, + href=links["pull"], + s="s" if len_changes_not_in_diff > 1 else "", + ) + ) + elif len_changes_not_in_diff > 0: + yield ( + "[see {n} file{s} with indirect coverage changes]({href}/indirect-changes?src=pr&el=tree-more)".format( + n=len_changes_not_in_diff, + href=links["pull"], + s="s" if len_changes_not_in_diff > 1 else "", + ) + ) + + +class FlagSectionWriter(BaseSectionWriter): + def do_write_section(self, comparison, diff, changes, links, behind_by=None): + # flags + base_report = comparison.project_coverage_base.report + head_report = comparison.head.report + if base_report is None: + base_report = Report() + base_flags = base_report.flags if base_report else {} + head_flags = head_report.flags if head_report else {} + missing_flags = set(base_flags.keys()) - set(head_flags.keys()) + flags = [] + + show_carriedforward_flags = self.settings.get("show_carryforward_flags", False) + for name, flag in head_flags.items(): + if (show_carriedforward_flags is True) or ( # Include all flags + show_carriedforward_flags is False + and flag.carriedforward + is False # Only include flags without carriedforward coverage + ): + flags.append( + { + "name": name, + "before": get_totals_from_file_in_reports(base_flags, name), + "after": flag.totals, + "diff": ( + flag.apply_diff(diff) if walk(diff, ("files",)) else None + ), + "carriedforward": flag.carriedforward, + "carriedforward_from": flag.carriedforward_from, + } + ) + + flags.extend( + { + "name": flag, + "before": base_flags[flag], + "after": None, + "diff": None, + "carriedforward": False, + "carriedforward_from": None, + } + for flag in missing_flags + ) + + # TODO: get icons working + # flag_icon_url = "" + # carriedforward_flag_icon_url = "" + + if flags: + # Even if "show_carryforward_flags" is true, we don't want to show that column if there isn't actually carriedforward coverage, + # so figure out if we actually have any carriedforward coverage to show + has_carriedforward_flags = any( + flag["carriedforward"] is True + for flag in flags # If "show_carryforward_flags" yaml setting is set to false there won't be any flags in this list with carriedforward coverage. + ) + + table_header = ( + "| Coverage \u0394 |" + + (" Complexity \u0394 |" if self.show_complexity else "") + + " |" + + (" *Carryforward flag |" if has_carriedforward_flags else "") + ) + table_layout = ( + "|---|---|---|" + + ("---|" if self.show_complexity else "") + + ("---|" if has_carriedforward_flags else "") + ) + + yield ( + "| [Flag]({href}/flags?src=pr&el=flags) ".format(href=links["pull"]) + + table_header + ) + yield table_layout + for flag in sorted(flags, key=lambda f: f["name"]): + carriedforward, carriedforward_from = ( + flag["carriedforward"], + flag["carriedforward_from"], + ) + # Format the message for the "carriedforward" column, if the flag was carried forward + if carriedforward is True: + # The "from " text will only appear if we actually know which commit we carried forward from + carriedforward_from_url = ( + get_commit_url_from_commit_sha( + self.repository, carriedforward_from + ) + if carriedforward_from + else "" + ) + + carriedforward_message = ( + " Carriedforward" + + ( + f" from [{carriedforward_from[:7]}]({carriedforward_from_url})" + if carriedforward_from and carriedforward_from_url + else "" + ) + + " |" + ) + else: + carriedforward_message = " |" if has_carriedforward_flags else "" + + yield ( + "| {name} {metrics}{cf}".format( + name="[{flag_name}]({href}/flags?src=pr&el=flag)".format( + flag_name=flag["name"], + href=links["pull"], + ), + metrics=make_metrics( + flag["before"], + flag["after"], + flag["diff"], + self.show_complexity, + self.current_yaml, + ), + cf=carriedforward_message, + ) + ) + + if has_carriedforward_flags and show_carriedforward_flags: + yield "" + yield ( + "*This pull request uses carry forward flags. [Click here](https://docs.codecov.io/docs/carryforward-flags) to find out more." + ) + elif not show_carriedforward_flags: + yield "" + yield ( + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more." + ) + + +class ComponentsSectionWriter(BaseSectionWriter): + def _get_table_data_for_components( + self, all_components, comparison: ComparisonProxy + ) -> list[dict]: + component_data = [] + for component in all_components: + flags = component.get_matching_flags(comparison.head.report.flags.keys()) + filtered_comparison = comparison.get_filtered_comparison( + flags, component.paths + ) + diff = filtered_comparison.get_diff() + component_data.append( + { + "name": component.get_display_name(), + "before": ( + filtered_comparison.project_coverage_base.report.totals + if filtered_comparison.project_coverage_base.report is not None + else None + ), + "after": filtered_comparison.head.report.totals, + "diff": filtered_comparison.head.report.apply_diff( + diff, _save=False + ), + } + ) + return component_data + + def do_write_section( + self, comparison: ComparisonProxy, diff, changes, links, behind_by=None + ): + all_components = get_components_from_yaml(self.current_yaml) + if all_components == []: + return # fast return if there's noting to process + + component_data_to_show = self._get_table_data_for_components( + all_components, comparison + ) + + # Table header and layout + yield "| [Components]({href}/components?src=pr&el=components) | Coverage \u0394 | |".format( + href=links["pull"], + ) + yield "|---|---|---|" + # The interesting part + for component_data in component_data_to_show: + yield ( + "| {name} {metrics}".format( + name="[{component_name}]({href}/components?src=pr&el=component)".format( + component_name=component_data["name"], + href=links["pull"], + ), + metrics=make_metrics( + component_data["before"], + component_data["after"], + component_data["diff"], + show_complexity=False, + yaml=self.current_yaml, + ), + ) + ) + + +class HelperTextSectionWriter(BaseSectionWriter): + def do_write_section(self, comparison: ComparisonProxy, *args, **kwargs): + helper_template = ":x: {helper_text}" + sorted_helper_text_keys = sorted(self.status_or_checks_helper_text.keys()) + for helper_text_key in sorted_helper_text_keys: + yield helper_template.format( + helper_text=self.status_or_checks_helper_text[helper_text_key] + ) + + +class MessagesToUserSectionWriter(BaseSectionWriter): + class Messages(Enum): + INSTALL_GITHUB_APP_WARNING = auto() + DIFFERENT_UPLOAD_COUNT_WARNING = auto() + + def _write_different_upload_count_warning(self, comparison: ComparisonProxy) -> str: + upload_diff = comparison.get_reports_uploaded_count_per_flag_diff() + is_commit_complete = comparison.head.commit.state == "complete" + if ( + is_commit_complete + and upload_diff + and has_project_status(self.current_yaml) + and is_coverage_drop_significant(comparison) + ): + template = ( + "> :exclamation: There is a different number of reports uploaded between BASE ({base_short_sha}) and HEAD ({head_short_sha}). Click for more details." + + "\n> " + + "\n>
HEAD has {aggregated_upload_diff} upload{plural} {more_or_less} than BASE" + # There needs to be a padding line between the and the table or it won't be rendered correctly + + "\n>" + + "\n>| Flag | BASE ({base_short_sha}) | HEAD ({head_short_sha}) |" + + "\n>|------|------|------|" + + "{per_flag_diff_lines}" + + "\n>
" + ) + + def get_line_for_flag_diff(info: ReportUploadedCount) -> str: + line_template = "\n>|{flag_name}|{base_count}|{head_count}|" + return line_template.format( + flag_name=info["flag"], + base_count=info["base_count"], + head_count=info["head_count"], + ) + + aggregated_upload_diff = sum( + map(lambda diff: diff["head_count"] - diff["base_count"], upload_diff) + ) + context = dict( + aggregated_upload_diff=abs(aggregated_upload_diff), + more_or_less="more" if aggregated_upload_diff > 0 else "less", + plural="s" if abs(aggregated_upload_diff) > 1 else "", + base_short_sha=comparison.project_coverage_base.commit.commitid[:7], + head_short_sha=comparison.head.commit.commitid[:7], + per_flag_diff_lines="".join( + [get_line_for_flag_diff(info) for info in upload_diff] + ), + ) + + return template.format(**context) + return "" + + def _write_install_github_app_warning(self, comparison: ComparisonProxy) -> str: + """Writes a warning message to GitHub owners that have not yet installed the Codecov App to their account.""" + repo = comparison.head.commit.repository + owner = repo.owner + is_user_in_github = owner.service == "github" + owner_is_using_app = ( + owner.integration_id is not None or owner.github_app_installations != [] + ) + if is_user_in_github and not is_enterprise() and not owner_is_using_app: + return ":exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality." + return "" + + def do_write_section(self, comparison: ComparisonProxy, *args, **kwargs): + messages_ordering = [ + self.Messages.INSTALL_GITHUB_APP_WARNING, + self.Messages.DIFFERENT_UPLOAD_COUNT_WARNING, + ] + messages_content = { + self.Messages.INSTALL_GITHUB_APP_WARNING: self._write_install_github_app_warning( + comparison + ), + self.Messages.DIFFERENT_UPLOAD_COUNT_WARNING: self._write_different_upload_count_warning( + comparison + ), + } + for message in messages_ordering: + message_content = messages_content[message] + if message_content != "": + yield message_content diff --git a/apps/worker/services/notification/notifiers/mixins/message/tests/__init__.py b/apps/worker/services/notification/notifiers/mixins/message/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/notification/notifiers/mixins/message/tests/test_helpers.py b/apps/worker/services/notification/notifiers/mixins/message/tests/test_helpers.py new file mode 100644 index 0000000000..329b6ecd97 --- /dev/null +++ b/apps/worker/services/notification/notifiers/mixins/message/tests/test_helpers.py @@ -0,0 +1,77 @@ +import pytest +from shared.reports.resources import Report, ReportTotals + +from services.comparison import ComparisonProxy +from services.notification.notifiers.mixins.message.helpers import ( + has_project_status, + is_coverage_drop_significant, +) + + +@pytest.mark.parametrize( + "yaml, expected", + [ + pytest.param( + {"coverage": {"status": {"project": False}}}, + False, + id="project_coverage_boolean_disabled", + ), + pytest.param( + {"coverage": {"status": {"project": True}}}, + True, + id="project_coverage_boolean_enabled", + ), + pytest.param( + {"coverage": {"status": {"project": {"enabled": True}}}}, + True, + id="project_coverage_dict_enabled", + ), + pytest.param( + {"coverage": {"status": {"project": {"enabled": False}}}}, + False, + id="project_coverage_dict_disabled", + ), + pytest.param( + { + "coverage": { + "status": {"project": {"only_pulls": True, "informational": True}} + } + }, + True, + id="project_coverage_dict_no_explicit_enabled_value", + ), + pytest.param( + {"coverage": {"status": {"project": "enabled"}}}, + False, + id="project_coverage_invalid_value", + ), + ], +) +def test_has_project_status(yaml: dict, expected: bool): + assert has_project_status(yaml) == expected + + +@pytest.mark.parametrize( + "head_coverage, base_coverage, expected", + [ + pytest.param(None, None, False, id="no_head_no_base_not_significant_drop"), + pytest.param(85.0, None, False, id="no_base_not_significant_drop"), + pytest.param(None, 85.0, False, id="no_head_not_significant_drop"), + pytest.param(85.0, 85.0, False, id="no_change"), + pytest.param(91.0, 85.0, False, id="change_is_significant_but_positive"), + pytest.param(86.0, 85.0, False, id="change_not_significant"), + pytest.param(80.0, 85.0, True, id="change_is_significant"), + ], +) +def test_is_coverage_drop_significant( + head_coverage: float, base_coverage: float, expected, mocker +): + head_report = Report() + head_report._totals = ReportTotals(coverage=head_coverage) + base_report = Report() + base_report._totals = ReportTotals(coverage=base_coverage) + mock_head = mocker.MagicMock(report=head_report) + mock_base = mocker.MagicMock(report=base_report) + mock_comparison = mocker.MagicMock(head=mock_head, project_coverage_base=mock_base) + fake_comparison = ComparisonProxy(comparison=mock_comparison) + assert is_coverage_drop_significant(fake_comparison) == expected diff --git a/apps/worker/services/notification/notifiers/mixins/message/writers.py b/apps/worker/services/notification/notifiers/mixins/message/writers.py new file mode 100644 index 0000000000..85e768a7c0 --- /dev/null +++ b/apps/worker/services/notification/notifiers/mixins/message/writers.py @@ -0,0 +1,151 @@ +import logging +from base64 import b64encode +from decimal import Decimal +from typing import List + +from shared.reports.resources import Report + +from database.models import Repository +from helpers.reports import get_totals_from_file_in_reports +from services.comparison import ComparisonProxy +from services.notification.notifiers.mixins.message.helpers import ( + ellipsis, + escape_markdown, + make_patch_only_metrics, +) +from services.notification.notifiers.mixins.message.sections import ( + ALL_TESTS_PASSED_MSG, +) + +log = logging.getLogger(__name__) + + +# Unlike sections.py, this is an alternative take for creating messages based functionality. +# This is a plan specific section, so it doesn't adhere to the settings/yaml configurations +# like other writers do, hence the new file +class TeamPlanWriter: + @property + def name(self): + return self.__class__.__name__ + + def header_lines(self, comparison: ComparisonProxy, diff, settings) -> List[str]: + lines = [] + + head_report = comparison.head.report + diff_totals = head_report.apply_diff(diff) + + if diff_totals: + misses_and_partials = diff_totals.misses + diff_totals.partials + patch_coverage = diff_totals.coverage + else: + misses_and_partials = None + patch_coverage = None + if misses_and_partials: + ln_text = "lines" if misses_and_partials > 1 else "line" + lines.append( + f"Attention: Patch coverage is `{patch_coverage}%` with `{misses_and_partials} {ln_text}` in your changes missing coverage. Please review." + ) + else: + lines.append( + "All modified and coverable lines are covered by tests :white_check_mark:" + ) + + hide_project_coverage = settings.get("hide_project_coverage", False) + if hide_project_coverage: + if ta_error_msg := comparison.test_results_error(): + lines.append("") + lines.append(ta_error_msg) + elif comparison.all_tests_passed(): + lines.append("") + lines.append(ALL_TESTS_PASSED_MSG) + return lines + + def middle_lines( + self, comparison: ComparisonProxy, diff, links, current_yaml + ) -> List[str]: + lines = [] + + # create list of files changed in diff + base_report = comparison.project_coverage_base.report + head_report = comparison.head.report + if base_report is None: + base_report = Report() + files_in_diff = [ + ( + _diff["type"], + path, + make_patch_only_metrics( + get_totals_from_file_in_reports(base_report, path) or False, + get_totals_from_file_in_reports(head_report, path) or False, + _diff["totals"], + # show_complexity defaulted to none + None, + current_yaml, + links["pull"], + ), + int(_diff["totals"].misses + _diff["totals"].partials), + ) + for path, _diff in (diff["files"] if diff else {}).items() + if _diff.get("totals") + ] + + if files_in_diff: + table_header = "| Patch % | Lines |" + table_layout = "|---|---|---|" + + # get limit of results to show + limit = 10 + mentioned = [] + + def tree_cell(typ, path, metrics, _=None): + if path not in mentioned: + # mentioned: for files that are in diff and changes + mentioned.append(path) + return "| {rm}[{path}]({compare}?src=pr&el=tree#diff-{hash}){rm} {metrics}".format( + rm="~~" if typ == "deleted" else "", + path=escape_markdown(ellipsis(path, 50, False)), + compare=links["pull"], + hash=b64encode(path.encode()).decode(), + metrics=metrics, + ) + + remaining_files = 0 + printed_files = 0 + changed_files = sorted( + files_in_diff, key=lambda a: a[3] or Decimal("0"), reverse=True + ) + changed_files_with_missing_lines = [f for f in changed_files if f[3] > 0] + if changed_files_with_missing_lines: + lines.append( + "| [Files with missing lines]({0}?dropdown=coverage&src=pr&el=tree) {1}".format( + links["pull"], table_header + ) + ) + lines.append(table_layout) + for file in changed_files_with_missing_lines: + if printed_files == limit: + remaining_files += 1 + else: + printed_files += 1 + lines.append(tree_cell(file[0], file[1], file[2])) + if remaining_files: + lines.append( + "| ... and [{n} more]({href}?src=pr&el=tree-more) | |".format( + n=remaining_files, href=links["pull"] + ) + ) + + return lines + + def footer_lines(self, comparison: ComparisonProxy) -> List[str]: + lines = [] + lines.append("") + lines.append( + ":loudspeaker: Thoughts on this report? [Let us know!]({0})".format( + "https://github.com/codecov/feedback/issues/255" + ) + ) + # CAN BE REMOVED AFTER January 31st 2025 + # Will only be shown to orgs specified in the feature flag override. + repo: Repository = comparison.head.commit.repository + return lines diff --git a/apps/worker/services/notification/notifiers/mixins/status.py b/apps/worker/services/notification/notifiers/mixins/status.py new file mode 100644 index 0000000000..7ae5706780 --- /dev/null +++ b/apps/worker/services/notification/notifiers/mixins/status.py @@ -0,0 +1,610 @@ +import logging +from decimal import Decimal, InvalidOperation +from enum import Enum +from typing import Literal, TypedDict + +from services.comparison import ComparisonProxy, FilteredComparison +from services.yaml.reader import round_number + +log = logging.getLogger(__name__) + + +class StatusState(Enum): + success = "success" + failure = "failure" + + +class StatusResult(TypedDict): + """ + The mixins in this file do the calculations and decide the SuccessState for all Status and Checks Notifiers. + Checks have different fields than Statuses, so Checks are converted to the CheckResult type later. + """ + + state: Literal["success", "failure"] # StatusState values + message: str + included_helper_text: dict[str, str] + + +class HelperTextKey(str, Enum): + CUSTOM_TARGET_PATCH = "custom_target_helper_text_patch" + CUSTOM_TARGET_PROJECT = "custom_target_helper_text_project" + RCB_INDIRECT_CHANGES = "rcb_indirect_changes_helper_text" + INDIRECT_CHANGES_KEY = "indirect_changes_helper_text" + RCB_ADJUST_BASE = "rcb_adjust_base_helper_text" + + +class HelperTextTemplate(str, Enum): + CUSTOM_TARGET = ( + "Your {context} {notification_type} has failed because the {point_of_comparison} coverage ({coverage}%) is below the target coverage ({target}%). " + "You can increase the {point_of_comparison} coverage or adjust the " + "[target](https://docs.codecov.com/docs/commit-status#target) coverage." + ) + INDIRECT_CHANGES = ( + "Your {context} {notification_type} has failed because you have indirect coverage changes. " + "Learn more about [Unexpected Coverage Changes](https://docs.codecov.com/docs/unexpected-coverage-changes) " + "and [reasons for indirect coverage changes](https://docs.codecov.com/docs/unexpected-coverage-changes#reasons-for-indirect-changes)." + ) + RCB_ADJUST_BASE = ( + "Your project {notification_type} has failed because the head coverage ({coverage}%) " + "is below the [adjusted base coverage](https://docs.codecov.com/docs/removed-code-behavior#option-3-default-adjust_base) ({adjusted_base_cov}%). " + "You can increase the head coverage or adjust the " + "[Removed Code Behavior](https://docs.codecov.com/docs/removed-code-behavior)." + ) + + +HELPER_TEXT_MAP = { + HelperTextKey.CUSTOM_TARGET_PATCH: HelperTextTemplate.CUSTOM_TARGET, + HelperTextKey.CUSTOM_TARGET_PROJECT: HelperTextTemplate.CUSTOM_TARGET, + HelperTextKey.RCB_INDIRECT_CHANGES: HelperTextTemplate.INDIRECT_CHANGES, + HelperTextKey.INDIRECT_CHANGES_KEY: HelperTextTemplate.INDIRECT_CHANGES, + HelperTextKey.RCB_ADJUST_BASE: HelperTextTemplate.RCB_ADJUST_BASE, +} + + +class StatusPatchMixin(object): + context = "patch" + + def _get_threshold(self) -> Decimal: + """ + Threshold can be configured by user, default is 0.0 + """ + threshold = self.notifier_yaml_settings.get("threshold", "0.0") + + try: + # check if user has erroneously added a % to this input and fix + threshold = Decimal(str(threshold).replace("%", "")) + except (InvalidOperation, TypeError, AttributeError): + threshold = Decimal("0.0") + return threshold + + def _get_target( + self, comparison: ComparisonProxy | FilteredComparison + ) -> tuple[Decimal | None, bool]: + """ + Target can be configured by user, default is auto, which is the coverage level from the base report. + Target will be None if no report is found to compare against. + """ + if self.notifier_yaml_settings.get("target") not in ("auto", None): + # check if user has erroneously added a % to this input and fix + target_coverage = Decimal( + str(self.notifier_yaml_settings.get("target")).replace("%", "") + ) + is_custom_target = True + else: + target_coverage = ( + Decimal(comparison.project_coverage_base.report.totals.coverage) + if comparison.has_project_coverage_base_report() + and comparison.project_coverage_base.report.totals.coverage is not None + else None + ) + is_custom_target = False + return target_coverage, is_custom_target + + def get_patch_status( + self, comparison: ComparisonProxy | FilteredComparison, notification_type: str + ) -> StatusResult: + threshold = self._get_threshold() + target_coverage, is_custom_target = self._get_target(comparison) + totals = comparison.get_patch_totals() + included_helper_text = {} + + # coverage affected + if totals and totals.lines > 0: + coverage = Decimal(totals.coverage) + if target_coverage is None: + state = self.notifier_yaml_settings.get( + "if_not_found", StatusState.success.value + ) + message = "No report found to compare against" + else: + state = ( + StatusState.success.value + if coverage >= target_coverage + else StatusState.failure.value + ) + # use rounded numbers for messages + coverage_rounded = round_number(self.current_yaml, coverage) + target_rounded = round_number(self.current_yaml, target_coverage) + if state == StatusState.failure.value and coverage >= ( + target_coverage - threshold + ): + threshold_rounded = round_number(self.current_yaml, threshold) + state = StatusState.success.value + message = f"{coverage_rounded}% of diff hit (within {threshold_rounded}% threshold of {target_rounded}%)" + else: + message = ( + f"{coverage_rounded}% of diff hit (target {target_rounded}%)" + ) + if state == StatusState.failure.value and is_custom_target: + helper_text = HELPER_TEXT_MAP[ + HelperTextKey.CUSTOM_TARGET_PATCH + ].value.format( + context=self.context, + notification_type=notification_type, + point_of_comparison=self.context, + coverage=coverage_rounded, + target=target_rounded, + ) + included_helper_text[HelperTextKey.CUSTOM_TARGET_PATCH.value] = ( + helper_text + ) + return StatusResult( + state=state, message=message, included_helper_text=included_helper_text + ) + + # coverage not affected + if comparison.project_coverage_base.commit: + description = "Coverage not affected when comparing {0}...{1}".format( + comparison.project_coverage_base.commit.commitid[:7], + comparison.head.commit.commitid[:7], + ) + else: + description = "Coverage not affected" + return StatusResult( + state=StatusState.success.value, + message=description, + included_helper_text=included_helper_text, + ) + + +class StatusChangesMixin(object): + context = "changes" + + def is_a_change_worth_noting(self, change) -> bool: + if not change.new and not change.deleted: + # has totals and not -10m => 10h + t = change.totals + if t: + # new missed||partial lines + return (t.misses + t.partials) > 0 + return False + + def get_changes_status( + self, comparison: ComparisonProxy | FilteredComparison, notification_type: str + ) -> StatusResult: + included_helper_text = {} + pull = comparison.pull + if self.notifier_yaml_settings.get("base") in ("auto", None, "pr") and pull: + if not comparison.has_project_coverage_base_report(): + description = ( + "Unable to determine changes, no report found at pull request base" + ) + return StatusResult( + state=StatusState.success.value, + message=description, + included_helper_text=included_helper_text, + ) + + # filter changes + changes = comparison.get_changes() + if changes: + changes = list(filter(self.is_a_change_worth_noting, changes)) + + # remove new additions + if changes: + lpc = len(changes) + eng = "files have" if lpc > 1 else "file has" + description = ( + "{0} {1} indirect coverage changes not visible in diff".format(lpc, eng) + ) + state = ( + StatusState.success.value + if self.notifier_yaml_settings.get("informational") + else StatusState.failure.value + ) + if state == StatusState.failure.value: + # their comparison failed because of unexpected/indirect changes, give them helper text about it + included_helper_text[HelperTextKey.INDIRECT_CHANGES_KEY.value] = ( + HELPER_TEXT_MAP[HelperTextKey.INDIRECT_CHANGES_KEY].value.format( + context=self.context, + notification_type=notification_type, + ) + ) + return StatusResult( + state=state, + message=description, + included_helper_text=included_helper_text, + ) + + description = "No indirect coverage changes found" + return StatusResult( + state=StatusState.success.value, + message=description, + included_helper_text=included_helper_text, + ) + + +class StatusProjectMixin(object): + DEFAULT_REMOVED_CODE_BEHAVIOR = "adjust_base" + context = "project" + point_of_comparison = "head" + + def _apply_removals_only_behavior( + self, comparison: ComparisonProxy | FilteredComparison + ) -> tuple[str, str] | None: + """ + Rule for passing project status on removals_only behavior: + Pass if code was _only removed_ (i.e. no addition, no unexpected changes) + """ + log.info( + "Applying removals_only behavior to project status", + extra=dict(commit=comparison.head.commit.commitid), + ) + impacted_files = comparison.get_impacted_files().get("files", []) + + no_added_no_unexpected_change = all( + not file.get("added_diff_coverage") + and not file.get("unexpected_line_changes") + for file in impacted_files + ) + some_removed = any(file.get("removed_diff_coverage") for file in impacted_files) + + if no_added_no_unexpected_change and some_removed: + return ( + StatusState.success.value, + ", passed because this change only removed code", + ) + return None + + def _apply_adjust_base_behavior( + self, + comparison: ComparisonProxy | FilteredComparison, + notification_type: str, + ) -> tuple[tuple[str, str] | None, dict]: + """ + Rule for passing project status on adjust_base behavior: + We adjust the BASE of the comparison by removing from it lines that were removed in HEAD + And then re-calculate BASE coverage and compare it to HEAD coverage. + """ + helper_text = {} + log.info( + "Applying adjust_base behavior to project status", + extra=dict(commit=comparison.head.commit.commitid), + ) + # If the user defined a target value for project coverage + # Adjusting the base won't make HEAD change in relation to the target value + # So we skip the calculation entirely + if self.notifier_yaml_settings.get("target") not in ("auto", None): + log.info( + "Notifier settings specify target value. Skipping adjust_base.", + extra=dict(commit=comparison.head.commit.commitid), + ) + return None, helper_text + + impacted_files = comparison.get_impacted_files().get("files", []) + + hits_removed = 0 + misses_removed = 0 + partials_removed = 0 + + for file_dict in impacted_files: + removed_diff_coverage_list = file_dict.get("removed_diff_coverage") + if removed_diff_coverage_list: + hits_removed += sum( + 1 if item[1] == "h" else 0 for item in removed_diff_coverage_list + ) + misses_removed += sum( + 1 if item[1] == "m" else 0 for item in removed_diff_coverage_list + ) + partials_removed += sum( + 1 if item[1] == "p" else 0 for item in removed_diff_coverage_list + ) + + base_totals = comparison.project_coverage_base.report.totals + base_adjusted_hits = base_totals.hits - hits_removed + base_adjusted_misses = base_totals.misses - misses_removed + base_adjusted_partials = base_totals.partials - partials_removed + base_adjusted_totals = ( + base_adjusted_hits + base_adjusted_misses + base_adjusted_partials + ) + + if not base_adjusted_totals: + return None, helper_text + + # The coverage info is in percentage, so multiply by 100 + base_adjusted_coverage = ( + Decimal(base_adjusted_hits / base_adjusted_totals) * 100 + ) + threshold = self._get_threshold() + + head_coverage = Decimal(comparison.head.report.totals.coverage) + log.info( + "Adjust base applied to project status", + extra=dict( + commit=comparison.head.commit.commitid, + base_adjusted_coverage=base_adjusted_coverage, + threshold=threshold, + head_coverage=head_coverage, + hits_removed=hits_removed, + misses_removed=misses_removed, + partials_removed=partials_removed, + ), + ) + base_adjusted_coverage = base_adjusted_coverage - threshold + + # the head coverage is rounded to five digits after the dot, using shared.helpers.numeric.ratio + # so we should round the base adjusted coverage to the same amount of digits after the dot + # Decimal.quantize: https://docs.python.org/3/library/decimal.html#decimal.Decimal.quantize + quantized_base_adjusted_coverage = base_adjusted_coverage.quantize( + Decimal("0.00000") + ) + rounded_base_adjusted_coverage = round_number( + self.current_yaml, base_adjusted_coverage + ) + + if quantized_base_adjusted_coverage - head_coverage < Decimal("0.005"): + rounded_difference = max( + 0, + round_number(self.current_yaml, head_coverage - base_adjusted_coverage), + ) + message = f", passed because coverage increased by {rounded_difference}% when compared to adjusted base ({rounded_base_adjusted_coverage}%)" + return ( + ( + StatusState.success.value, + message, + ), + helper_text, + ) + + # use rounded numbers for messages + coverage_rounded = round_number(self.current_yaml, head_coverage) + + # their comparison failed despite the adjusted base, give them helper text about it + helper_text[HelperTextKey.RCB_ADJUST_BASE.value] = HELPER_TEXT_MAP[ + HelperTextKey.RCB_ADJUST_BASE + ].value.format( + notification_type=notification_type, + coverage=coverage_rounded, + adjusted_base_cov=rounded_base_adjusted_coverage, + ) + return None, helper_text + + def _apply_fully_covered_patch_behavior( + self, + comparison: ComparisonProxy | FilteredComparison, + notification_type: str, + ) -> tuple[tuple[str, str] | None, dict]: + """ + Rule for passing project status on fully_covered_patch behavior: + Pass if patch coverage is 100% and there are no indirect changes + """ + helper_text = {} + log.info( + "Applying fully_covered_patch behavior to project status", + extra=dict(commit=comparison.head.commit.commitid), + ) + impacted_files = comparison.get_impacted_files().get("files", []) + + no_unexpected_changes = all( + not file.get("unexpected_line_changes") for file in impacted_files + ) + + if not no_unexpected_changes: + log.info( + "Unexpected changes when applying patch_100 behavior", + extra=dict(commit=comparison.head.commit.commitid), + ) + + # their comparison failed because of unexpected/indirect changes, give them helper text about it + helper_text[HelperTextKey.RCB_INDIRECT_CHANGES] = HELPER_TEXT_MAP[ + HelperTextKey.RCB_INDIRECT_CHANGES + ].format( + context=self.context, + notification_type=notification_type, + ) + return None, helper_text + + diff = comparison.get_diff(use_original_base=True) + patch_totals = comparison.head.report.apply_diff(diff) + if patch_totals is None or patch_totals.lines == 0: + # Coverage was not changed by patch + return ( + ( + StatusState.success.value, + ", passed because coverage was not affected by patch", + ), + helper_text, + ) + coverage = Decimal(patch_totals.coverage) + if coverage == 100.0: + return ( + ( + StatusState.success.value, + ", passed because patch was fully covered by tests, and no indirect coverage changes", + ), + helper_text, + ) + return None, helper_text + + def get_project_status( + self, comparison: ComparisonProxy | FilteredComparison, notification_type: str + ) -> StatusResult: + result = self._get_project_status( + comparison, notification_type=notification_type + ) + if result["state"] == StatusState.success.value: + return result + + # Possibly pass the status check via removed_code_behavior + # The removed code behavior can change the `state` from `failure` to `success` and add to the `message`. + # We need both reports to be able to get the diff and apply the removed_code behavior + if comparison.project_coverage_base.report and comparison.head.report: + is_custom_rcb = True + removed_code_behavior = self.notifier_yaml_settings.get( + "removed_code_behavior", None + ) + if removed_code_behavior is None: + is_custom_rcb = False + removed_code_behavior = self.DEFAULT_REMOVED_CODE_BEHAVIOR + + # Apply removed_code_behavior + removed_code_result = None + if removed_code_behavior == "removals_only": + removed_code_result = self._apply_removals_only_behavior(comparison) + elif removed_code_behavior == "adjust_base": + removed_code_result, helper_text = self._apply_adjust_base_behavior( + comparison, + notification_type=notification_type, + ) + if is_custom_rcb: + # if user set this in their yaml, give them helper text related to it + result["included_helper_text"].update(helper_text) + elif removed_code_behavior == "fully_covered_patch": + removed_code_result, helper_text = ( + self._apply_fully_covered_patch_behavior( + comparison, + notification_type=notification_type, + ) + ) + # if user set this in their yaml, give them helper text related to it + result["included_helper_text"].update(helper_text) + else: + if removed_code_behavior not in [False, "off"]: + log.warning( + "Unknown removed_code_behavior", + extra=dict( + removed_code_behavior=removed_code_behavior, + commit_id=comparison.head.commit.commitid, + ), + ) + # Possibly change status + if removed_code_result: + removed_code_state, removed_code_message = removed_code_result + if removed_code_state == StatusState.success.value: + # the status was failure, has been changed to success through RCB settings + # since the status is no longer failing, remove any included_helper_text + result["included_helper_text"] = {} + result["state"] = removed_code_state + result["message"] = result["message"] + removed_code_message + return result + + def _get_threshold(self) -> Decimal: + """ + Threshold can be configured by user, default is 0.0 + """ + threshold = self.notifier_yaml_settings.get("threshold", "0.0") + + try: + # check if user has erroneously added a % to this input and fix + threshold = Decimal(str(threshold).replace("%", "")) + except (InvalidOperation, TypeError, AttributeError): + threshold = Decimal("0.0") + return threshold + + def _get_target( + self, base_report_totals: ComparisonProxy | FilteredComparison + ) -> tuple[Decimal, bool]: + """ + Target can be configured by user, default is auto, which is the coverage level from the base report. + """ + if self.notifier_yaml_settings.get("target") not in ("auto", None): + # check if user has erroneously added a % to this input and fix + target_coverage = Decimal( + str(self.notifier_yaml_settings.get("target")).replace("%", "") + ) + is_custom_target = True + else: + target_coverage = Decimal(base_report_totals.coverage) + is_custom_target = False + return target_coverage, is_custom_target + + def _get_project_status( + self, comparison: ComparisonProxy | FilteredComparison, notification_type: str + ) -> StatusResult: + included_helper_text = {} + if ( + not comparison.head.report + or (head_report_totals := comparison.head.report.totals) is None + or head_report_totals.coverage is None + ): + state = self.notifier_yaml_settings.get( + "if_not_found", StatusState.success.value + ) + message = "No coverage information found on head" + return StatusResult( + state=state, message=message, included_helper_text=included_helper_text + ) + + base_report = comparison.project_coverage_base.report + if base_report is None: + # No base report - can't pass by offset coverage + state = self.notifier_yaml_settings.get( + "if_not_found", StatusState.success.value + ) + message = "No report found to compare against" + return StatusResult( + state=state, message=message, included_helper_text=included_helper_text + ) + + base_report_totals = base_report.totals + if base_report_totals.coverage is None: + # Base report, no coverage on base report - can't pass by offset coverage + state = self.notifier_yaml_settings.get( + "if_not_found", StatusState.success.value + ) + message = "No coverage information found on base report" + return StatusResult( + state=state, message=message, included_helper_text=included_helper_text + ) + + # Proper comparison head vs base report + threshold = self._get_threshold() + target_coverage, is_custom_target = self._get_target(base_report_totals) + head_coverage = Decimal(head_report_totals.coverage) + head_coverage_rounded = round_number(self.current_yaml, head_coverage) + + # threshold is used to determine success/failure, but is not included in messaging + state = ( + StatusState.success.value + if head_coverage >= (target_coverage - threshold) + else StatusState.failure.value + ) + + if is_custom_target: + # Explicit target coverage defined in YAML + # use rounded numbers for messages + target_rounded = round_number(self.current_yaml, target_coverage) + message = f"{head_coverage_rounded}% (target {target_rounded}%)" + if state == StatusState.failure.value: + helper_text = HELPER_TEXT_MAP[ + HelperTextKey.CUSTOM_TARGET_PROJECT + ].value.format( + context=self.context, + notification_type=notification_type, + point_of_comparison=self.point_of_comparison, + coverage=head_coverage_rounded, + target=target_rounded, + ) + included_helper_text[HelperTextKey.CUSTOM_TARGET_PROJECT] = helper_text + return StatusResult( + state=state, message=message, included_helper_text=included_helper_text + ) + + # use rounded numbers for messages + change_coverage_rounded = round_number( + self.current_yaml, head_coverage - target_coverage + ) + message = f"{head_coverage_rounded}% ({change_coverage_rounded:+}%) compared to {comparison.project_coverage_base.commit.commitid[:7]}" + return StatusResult( + state=state, message=message, included_helper_text=included_helper_text + ) diff --git a/apps/worker/services/notification/notifiers/slack.py b/apps/worker/services/notification/notifiers/slack.py new file mode 100644 index 0000000000..fbf2c3c7c2 --- /dev/null +++ b/apps/worker/services/notification/notifiers/slack.py @@ -0,0 +1,48 @@ +from database.enums import Notification +from services.notification.notifiers.generics import ( + Comparison, + RequestsYamlBasedNotifier, +) +from services.urls import get_commit_url, get_graph_url + + +class SlackNotifier(RequestsYamlBasedNotifier): + BASE_MESSAGE = " ".join( + [ + "Coverage for <{head_url}|{owner_username}/{repo_name}>", + "{comparison_string}on `{head_branch}` is `{head_totals_c}%`", + "via `<{head_url}|{head_short_commitid}>`", + ] + ) + + COMPARISON_STRING = ( + "*{compare_message}* `<{compare_url}|{compare_notation}{compare_coverage}%>` " + ) + + @property + def notification_type(self) -> Notification: + return Notification.slack + + def build_payload(self, comparison: Comparison) -> dict: + message = self.generate_message(comparison) + compare_dict = self.generate_compare_dict(comparison) + color = "good" if compare_dict["notation"] in ("", "+") else "bad" + attachments = [ + { + "fallback": "Commit sunburst attachment", + "color": color, + "title": "Commit Sunburst", + "title_link": get_commit_url(comparison.head.commit), + "image_url": get_graph_url( + comparison.head.commit, "sunburst.svg", size=100 + ), + } + for attachment_type in self.notifier_yaml_settings.get("attachments", []) + if attachment_type == "sunburst" + ] + return { + "text": message, + "author_name": "Codecov", + "author_link": get_commit_url(comparison.head.commit), + "attachments": attachments, + } diff --git a/apps/worker/services/notification/notifiers/status/__init__.py b/apps/worker/services/notification/notifiers/status/__init__.py new file mode 100644 index 0000000000..c4c636fadf --- /dev/null +++ b/apps/worker/services/notification/notifiers/status/__init__.py @@ -0,0 +1,4 @@ +# ruff: noqa: F401 +from services.notification.notifiers.status.changes import ChangesStatusNotifier +from services.notification.notifiers.status.patch import PatchStatusNotifier +from services.notification.notifiers.status.project import ProjectStatusNotifier diff --git a/apps/worker/services/notification/notifiers/status/base.py b/apps/worker/services/notification/notifiers/status/base.py new file mode 100644 index 0000000000..eeaebadca9 --- /dev/null +++ b/apps/worker/services/notification/notifiers/status/base.py @@ -0,0 +1,390 @@ +import logging +from typing import Optional + +import sentry_sdk +from asgiref.sync import async_to_sync +from shared.config import get_config +from shared.helpers.cache import NO_VALUE, cache, make_hash_sha256 +from shared.torngit.exceptions import TorngitClientError, TorngitError + +from helpers.match import match +from services.comparison import ComparisonProxy, FilteredComparison +from services.notification.notifiers.base import ( + AbstractBaseNotifier, + NotificationResult, +) +from services.urls import get_commit_url, get_pull_url +from services.yaml import read_yaml_field +from services.yaml.reader import get_paths_from_flags + +log = logging.getLogger(__name__) + + +class StatusNotifier(AbstractBaseNotifier): + def is_enabled(self) -> bool: + return True + + def store_results(self, comparison: ComparisonProxy, result: NotificationResult): + pass + + @property + def name(self): + return f"status-{self.context}" + + def build_payload(self, comparison: ComparisonProxy | FilteredComparison) -> dict: + raise NotImplementedError() + + def get_upgrade_message(self) -> str: + # TODO: this is the message in the PR author billing spec but maybe we should add the actual username? + return "Please activate this user to display a detailed status check" + + def get_status_check_for_empty_upload(self): + if self.is_passing_empty_upload(): + return ("success", "Non-testable files changed.") + + if self.is_failing_empty_upload(): + return ("failure", "Testable files changed") + + def can_we_set_this_status(self, comparison: ComparisonProxy) -> bool: + head = comparison.head.commit + pull = comparison.pull + if ( + self.notifier_yaml_settings.get("only_pulls") + or self.notifier_yaml_settings.get("base") == "pr" + ) and not pull: + return False + if not match(self.notifier_yaml_settings.get("branches"), head.branch): + return False + return True + + def determine_status_check_behavior_to_apply( + self, comparison: ComparisonProxy, field_name + ) -> str | None: + """ + Used for fields that can be set at the global level for all checks in "default_rules", or at the component level for an individual check. + For more context, see https://docs.codecov.io/docs/commit-status#default_rules + """ + # Get the component level setting, if one is specified + component_behavior = self.notifier_yaml_settings.get(field_name) + # Get the value set at the global level via the default_rules key. This can be 'None' if no value was provided. + # If provided, this is populated either by the YAML file directly or by the defaults set in 'shared'. + default_rules_behavior = read_yaml_field( + self.current_yaml, ("coverage", "status", "default_rules", field_name) + ) + + behavior_to_apply = ( + component_behavior + if component_behavior is not None + else default_rules_behavior + ) + + return behavior_to_apply + + def flag_coverage_was_uploaded(self, comparison: ComparisonProxy) -> bool: + """ + Indicates whether coverage was uploaded for any of the flags on this status check. + If there are no flags on the status check, this will return true. + If there are multiple flags on the status check, this will return true if at least one of them has uploaded coverage. + """ + + flags_included_in_status_check = set( + self.notifier_yaml_settings.get("flags") or [] + ) + if not flags_included_in_status_check: + return True + report_uploaded_flags = comparison.head.report.get_uploaded_flags() + return ( + len(report_uploaded_flags.intersection(flags_included_in_status_check)) > 0 + ) + + def get_notifier_filters(self) -> dict: + flag_list = self.notifier_yaml_settings.get("flags") or [] + return dict( + path_patterns=set( + get_paths_from_flags(self.current_yaml, flag_list) + + (self.notifier_yaml_settings.get("paths") or []) + ), + flags=flag_list, + ) + + def required_builds(self, comparison: ComparisonProxy) -> bool: + flags = self.notifier_yaml_settings.get("flags") or [] + head_report = comparison.head.report + + for flag in flags: + flag_configuration = self.current_yaml.get_flag_configuration(flag) + if flag_configuration and head_report and head_report.sessions: + number_of_occ = 0 + for session in head_report.sessions.values(): + if session.flags and flag in session.flags: + number_of_occ += 1 + needed_builds = flag_configuration.get("after_n_builds", 0) + if number_of_occ < needed_builds: + log.info( + "Flag needs more builds to send status check", + extra=dict( + flag=flag, + needed_builds=needed_builds, + number_of_occ=number_of_occ, + repoid=comparison.head.commit.repoid, + commit=comparison.head.commit.commitid, + ), + ) + return False + return True + + def get_github_app_used(self) -> int | None: + torngit = self.repository_service + if torngit is None: + return None + torngit_installation = torngit.data.get("installation") + selected_installation_id = ( + torngit_installation.get("id") if torngit_installation else None + ) + return selected_installation_id + + @sentry_sdk.trace + def notify( + self, + comparison: ComparisonProxy, + status_or_checks_helper_text: Optional[dict[str, str]] = None, + ) -> NotificationResult: + payload = None + if not self.can_we_set_this_status(comparison): + return NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="not_fit_criteria", + data_sent=None, + ) + if not self.required_builds(comparison): + return NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="need_more_builds", + data_sent=None, + ) + # Filter the coverage report based on fields in this notification's YAML settings + # e.g. if "paths" is specified, exclude the coverage not on those paths + try: + # If flag coverage wasn't uploaded, apply the appropriate behavior + flag_coverage_not_uploaded_behavior = ( + self.determine_status_check_behavior_to_apply( + comparison, "flag_coverage_not_uploaded_behavior" + ) + ) + if not comparison.has_head_report(): + payload = self.build_payload(comparison) + elif ( + flag_coverage_not_uploaded_behavior == "exclude" + and not self.flag_coverage_was_uploaded(comparison) + ): + return NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="exclude_flag_coverage_not_uploaded_checks", + data_sent=None, + data_received=None, + ) + elif ( + flag_coverage_not_uploaded_behavior == "pass" + and not self.flag_coverage_was_uploaded(comparison) + ): + filtered_comparison = comparison.get_filtered_comparison( + **self.get_notifier_filters() + ) + payload = self.build_payload(filtered_comparison) + payload["state"] = "success" + payload["message"] = ( + payload["message"] + + " [Auto passed due to carriedforward or missing coverage]" + ) + else: + filtered_comparison = comparison.get_filtered_comparison( + **self.get_notifier_filters() + ) + payload = self.build_payload(filtered_comparison) + if comparison.pull: + payload["url"] = get_pull_url(comparison.pull) + else: + payload["url"] = get_commit_url(comparison.head.commit) + + return self.maybe_send_notification(comparison, payload) + except TorngitClientError: + log.warning( + "Unable to send status notification to user due to a client-side error", + exc_info=True, + extra=dict( + repoid=comparison.head.commit.repoid, + commit=comparison.head.commit.commitid, + notifier_name=self.name, + ), + ) + return NotificationResult( + notification_attempted=True, + notification_successful=False, + explanation="client_side_error_provider", + data_sent=payload, + ) + except TorngitError: + log.warning( + "Unable to send status notification to user due to an unexpected error", + exc_info=True, + extra=dict( + repoid=comparison.head.commit.repoid, + commit=comparison.head.commit.commitid, + notifier_name=self.name, + ), + ) + return NotificationResult( + notification_attempted=True, + notification_successful=False, + explanation="server_side_error_provider", + data_sent=payload, + ) + + def status_already_exists( + self, comparison: ComparisonProxy, title, state, description + ) -> bool: + statuses = comparison.get_existing_statuses() + if statuses: + exists = statuses.get(title) + return ( + exists + and exists["state"] == state + and exists["description"] == description + ) + return False + + def get_status_external_name(self) -> str: + status_piece = f"/{self.title}" if self.title != "default" else "" + return f"codecov/{self.context}{status_piece}" + + def maybe_send_notification( + self, comparison: ComparisonProxy, payload: dict + ) -> NotificationResult: + base_commit = ( + comparison.project_coverage_base.commit + if comparison.project_coverage_base + else None + ) + head_commit = comparison.head.commit if comparison.head else None + + cache_key = "cache:" + make_hash_sha256( + dict( + type="status_check_notification", + repoid=head_commit.repoid, + base_commitid=base_commit.commitid if base_commit else None, + head_commitid=head_commit.commitid if head_commit else None, + notifier_name=self.name, + notifier_title=self.title, + ) + ) + + last_payload = cache.get_backend().get(cache_key) + if last_payload is NO_VALUE or last_payload != payload: + ttl = int( + get_config("setup", "cache", "send_status_notification", default=600) + ) # 10 min default + cache.get_backend().set(cache_key, ttl, payload) + return self.send_notification(comparison, payload) + else: + log.info( + "Notification payload unchanged. Skipping notification.", + extra=dict( + repoid=head_commit.repoid, + base_commitid=base_commit.commitid if base_commit else None, + head_commitid=head_commit.commitid if head_commit else None, + notifier_name=self.name, + notifier_title=self.title, + ), + ) + return NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="payload_unchanged", + data_sent=None, + ) + + def send_notification(self, comparison: ComparisonProxy, payload): + repository_service = self.repository_service + title = self.get_status_external_name() + head_commit_sha = comparison.head.commit.commitid + head_report = comparison.head.report + state = payload["state"] + message = payload["message"] + url = payload["url"] + if self.status_already_exists(comparison, title, state, message): + log.info( + "Status already set", + extra=dict(context=title, description=message, state=state), + ) + return NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="already_done", + data_sent={"title": title, "state": state, "message": message}, + ) + + state = "success" if self.notifier_yaml_settings.get("informational") else state + + notification_result_data_sent = { + "title": title, + "state": state, + "message": message, + } + + if payload.get("included_helper_text"): + notification_result_data_sent["included_helper_text"] = payload[ + "included_helper_text" + ] + + all_shas_to_notify = [head_commit_sha] + list( + comparison.context.gitlab_extra_shas or set() + ) + if len(all_shas_to_notify) > 1: + log.info( + "Notifying multiple SHAs", + extra=dict(all_shas=all_shas_to_notify, commit=head_commit_sha), + ) + + try: + _set_commit_status = async_to_sync(repository_service.set_commit_status) + head_coverage = head_report and head_report.totals.coverage + all_results = [ + _set_commit_status( + commitid, + state, + title, + description=message, + url=url, + coverage=(float(head_coverage) if head_coverage else 0), + ) + for commitid in all_shas_to_notify + ] + res = all_results[0] + + except TorngitClientError: + log.warning( + "Status not posted because this user can see but not set statuses on this repo", + extra=dict( + data_sent=notification_result_data_sent, + commit=head_commit_sha, + repoid=comparison.head.commit.repoid, + ), + ) + return NotificationResult( + notification_attempted=True, + notification_successful=False, + explanation="no_write_permission", + data_sent=notification_result_data_sent, + data_received=None, + ) + return NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent=notification_result_data_sent, + data_received={"id": res.get("id", "NO_ID")}, + github_app_used=self.get_github_app_used(), + ) diff --git a/apps/worker/services/notification/notifiers/status/changes.py b/apps/worker/services/notification/notifiers/status/changes.py new file mode 100644 index 0000000000..08eac62a82 --- /dev/null +++ b/apps/worker/services/notification/notifiers/status/changes.py @@ -0,0 +1,46 @@ +import logging + +from database.enums import Notification +from services.comparison import ComparisonProxy, FilteredComparison +from services.notification.notifiers.mixins.status import ( + StatusChangesMixin, + StatusResult, +) +from services.notification.notifiers.status.base import StatusNotifier + +log = logging.getLogger(__name__) + + +class ChangesStatusNotifier(StatusChangesMixin, StatusNotifier): + """This status analyzes the "unexpected changes" (see services/notification/changes.py + for a better description) and covered lines within it + + Attributes: + context (str): The context + + Possible results + - 'No unexpected coverage changes found.' + - {0} {1} unexpected coverage changes not visible in diff + - Unable to determine changes, no report found at pull request base + """ + + context = "changes" + notification_type_display_name = "status" + + @property + def notification_type(self) -> Notification: + return Notification.status_changes + + def build_payload( + self, comparison: ComparisonProxy | FilteredComparison + ) -> StatusResult: + if self.is_empty_upload(): + state, message = self.get_status_check_for_empty_upload() + return StatusResult(state=state, message=message, included_helper_text={}) + result = self.get_changes_status( + comparison, notification_type=self.notification_type_display_name + ) + if self.should_use_upgrade_decoration(): + result["message"] = self.get_upgrade_message() + + return result diff --git a/apps/worker/services/notification/notifiers/status/patch.py b/apps/worker/services/notification/notifiers/status/patch.py new file mode 100644 index 0000000000..10c52ba4e2 --- /dev/null +++ b/apps/worker/services/notification/notifiers/status/patch.py @@ -0,0 +1,40 @@ +from database.enums import Notification +from services.comparison import ComparisonProxy, FilteredComparison +from services.notification.notifiers.mixins.status import StatusPatchMixin, StatusResult +from services.notification.notifiers.status.base import StatusNotifier + + +class PatchStatusNotifier(StatusPatchMixin, StatusNotifier): + """This status analyzes the git patch and sees covered lines within it + + Attributes: + context (str): The context + + Possible results + - No report found to compare against + - f'{coverage_str}% of diff hit (within {threshold_str}% threshold of {target_str}%)' + - {coverage_str}% of diff hit (target {target_str}%) + """ + + context = "patch" + notification_type_display_name = "status" + + @property + def notification_type(self) -> Notification: + return Notification.status_patch + + def build_payload( + self, comparison: ComparisonProxy | FilteredComparison + ) -> StatusResult: + if self.is_empty_upload(): + state, message = self.get_status_check_for_empty_upload() + result = StatusResult(state=state, message=message, included_helper_text={}) + return result + + result = self.get_patch_status( + comparison, notification_type=self.notification_type_display_name + ) + if self.should_use_upgrade_decoration(): + result["message"] = self.get_upgrade_message() + + return result diff --git a/apps/worker/services/notification/notifiers/status/project.py b/apps/worker/services/notification/notifiers/status/project.py new file mode 100644 index 0000000000..702ed4bfab --- /dev/null +++ b/apps/worker/services/notification/notifiers/status/project.py @@ -0,0 +1,49 @@ +import logging + +from database.enums import Notification +from services.comparison import ComparisonProxy, FilteredComparison +from services.notification.notifiers.mixins.status import ( + StatusProjectMixin, + StatusResult, +) +from services.notification.notifiers.status.base import StatusNotifier + +log = logging.getLogger(__name__) + + +class ProjectStatusNotifier(StatusProjectMixin, StatusNotifier): + """ + + Attributes: + context (str): The context + + Possible results + - 100% remains the same compared to 29320f9 + - 57.42% (+<.01%) compared to 559fe9e + - 85.65% (target 87%) + - No report found to compare against + + Not implemented results (yet): + - Absolute coverage decreased by -{0}% but relative coverage ... + """ + + context = "project" + notification_type_display_name = "status" + + @property + def notification_type(self) -> Notification: + return Notification.status_project + + def build_payload( + self, comparison: ComparisonProxy | FilteredComparison + ) -> StatusResult: + if self.is_empty_upload(): + state, message = self.get_status_check_for_empty_upload() + return StatusResult(state=state, message=message, included_helper_text={}) + + result = self.get_project_status( + comparison, notification_type=self.notification_type_display_name + ) + if self.should_use_upgrade_decoration(): + result["message"] = self.get_upgrade_message() + return result diff --git a/apps/worker/services/notification/notifiers/tests/__init__.py b/apps/worker/services/notification/notifiers/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/notification/notifiers/tests/conftest.py b/apps/worker/services/notification/notifiers/tests/conftest.py new file mode 100644 index 0000000000..fca2dce59d --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/conftest.py @@ -0,0 +1,836 @@ +from datetime import datetime, timezone + +import pytest +from shared.reports.readonly import ReadOnlyReport +from shared.reports.reportfile import ReportFile +from shared.reports.resources import Report +from shared.reports.types import ReportLine +from shared.utils.sessions import Session + +from database.tests.factories import ( + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) +from services.archive import ArchiveService +from services.comparison import ComparisonProxy +from services.comparison.types import Comparison, FullCommit +from services.report import ReportService +from services.repository import EnrichedPull + + +def get_small_report(flags=None): + if flags is None: + flags = ["integration"] + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(11, 20)) + ) + first_file.append(3, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append(51, ReportLine.create(coverage=0, sessions=[[0, 1]])) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=flags)) + return report + + +@pytest.fixture +def small_report(): + return get_small_report() + + +@pytest.fixture +def sample_report_without_flags(): + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=None)) + return report + + +@pytest.fixture +def sample_report(): + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=["unit"])) + return report + + +@pytest.fixture +def sample_commit_with_report_already_carriedforward(dbsession, mock_storage): + sessions_dict = { + "0": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": [], + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "t": None, + "u": None, + }, + "1": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["unit"], + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "t": None, + "u": None, + }, + "2": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["enterprise"], + "j": None, + "n": None, + "p": None, + "st": "carriedforward", + "t": None, + "u": None, + "se": {"carriedforward_from": "123456789sha"}, + }, + "3": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["integration"], + "j": None, + "n": None, + "p": None, + "st": "carriedforward", + "t": None, + "u": None, + }, + } + file_headers = { + "file_00.py": [ + 0, + [0, 14, 12, 0, 2, "85.71429", 0, 0, 0, 0, 0, 0, 0], + [None, None, None, [0, 14, 12, 0, 2, "85.71429", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "file_01.py": [ + 1, + [0, 11, 8, 0, 3, "72.72727", 0, 0, 0, 0, 0, 0, 0], + [None, None, None, [0, 11, 8, 0, 3, "72.72727", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "file_10.py": [ + 10, + [0, 10, 6, 1, 3, "60.00000", 0, 0, 0, 0, 0, 0, 0], + [None, None, None, [0, 10, 6, 1, 3, "60.00000", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "file_11.py": [ + 11, + [0, 23, 15, 1, 7, "65.21739", 0, 0, 0, 0, 0, 0, 0], + [None, None, None, [0, 23, 15, 1, 7, "65.21739", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "file_12.py": [ + 12, + [0, 14, 8, 0, 6, "57.14286", 0, 0, 0, 0, 0, 0, 0], + [None, None, None, [0, 14, 8, 0, 6, "57.14286", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "file_13.py": [ + 13, + [0, 15, 9, 0, 6, "60.00000", 0, 0, 0, 0, 0, 0, 0], + [None, None, None, [0, 15, 9, 0, 6, "60.00000", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "file_14.py": [ + 14, + [0, 23, 13, 0, 10, "56.52174", 0, 0, 0, 0, 0, 0, 0], + [None, None, None, [0, 23, 13, 0, 10, "56.52174", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "file_02.py": [ + 2, + [0, 13, 9, 0, 4, "69.23077", 0, 0, 0, 0, 0, 0, 0], + [None, None, None, [0, 13, 9, 0, 4, "69.23077", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "file_03.py": [ + 3, + [0, 16, 8, 0, 8, "50.00000", 0, 0, 0, 0, 0, 0, 0], + [None, None, None, [0, 16, 8, 0, 8, "50.00000", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "file_04.py": [ + 4, + [0, 10, 6, 0, 4, "60.00000", 0, 0, 0, 0, 0, 0, 0], + [None, None, None, [0, 10, 6, 0, 4, "60.00000", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "file_05.py": [ + 5, + [0, 14, 10, 0, 4, "71.42857", 0, 0, 0, 0, 0, 0, 0], + [None, None, None, [0, 14, 10, 0, 4, "71.42857", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "file_06.py": [ + 6, + [0, 9, 7, 1, 1, "77.77778", 0, 0, 0, 0, 0, 0, 0], + [None, None, None, [0, 9, 7, 1, 1, "77.77778", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "file_07.py": [ + 7, + [0, 11, 9, 0, 2, "81.81818", 0, 0, 0, 0, 0, 0, 0], + [None, None, None, [0, 11, 9, 0, 2, "81.81818", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "file_08.py": [ + 8, + [0, 11, 6, 0, 5, "54.54545", 0, 0, 0, 0, 0, 0, 0], + [None, None, None, [0, 11, 6, 0, 5, "54.54545", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "file_09.py": [ + 9, + [0, 14, 10, 1, 3, "71.42857", 0, 0, 0, 0, 0, 0, 0], + [None, None, None, [0, 14, 10, 1, 3, "71.42857", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + } + commit = CommitFactory.create( + _report_json={"sessions": sessions_dict, "files": file_headers}, + repository__owner__service="github", + repository__owner__integration_id="10000", + repository__using_integration=True, + ) + dbsession.add(commit) + dbsession.flush() + + with open("tasks/tests/samples/sample_chunks_4_sessions.txt") as f: + content = f.read().encode() + archive_hash = ArchiveService.get_archive_hash(commit.repository) + chunks_url = f"v4/repos/{archive_hash}/commits/{commit.commitid}/chunks.txt" + mock_storage.write_file("archive", chunks_url, content) + + return commit + + +@pytest.fixture +def create_sample_comparison(dbsession, request, sample_report, mocker): + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + return_value="github-integration-token", + ) + + def _comparison(service="github", username="codecov-test"): + repository = RepositoryFactory.create( + owner__username=username, + owner__service=service, + owner__integration_id="10000", + using_integration=True, + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository) + head_commit = CommitFactory.create(repository=repository, branch="new_branch") + pull = PullFactory.create( + repository=repository, base=base_commit.commitid, head=head_commit.commitid + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + repository = base_commit.repository + base_full_commit = FullCommit(commit=base_commit, report=get_small_report()) + head_full_commit = FullCommit(commit=head_commit, report=sample_report) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull(database_pull=pull, provider_pull={}), + ) + ) + + return _comparison + + +@pytest.fixture +def sample_comparison(dbsession, request, sample_report, mocker): + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + return_value="github-integration-token", + ) + repository = RepositoryFactory.create( + owner__username=request.node.name, + owner__service="github", + owner__integration_id="10000", + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + owner__createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + using_integration=True, + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository, author__service="github") + head_commit = CommitFactory.create( + repository=repository, branch="new_branch", author__service="github" + ) + pull = PullFactory.create( + repository=repository, + base=base_commit.commitid, + head=head_commit.commitid, + author=OwnerFactory(username="codecov-test-user"), + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + base_full_commit = FullCommit( + commit=base_commit, report=ReadOnlyReport.create_from_report(get_small_report()) + ) + head_full_commit = FullCommit( + commit=head_commit, report=ReadOnlyReport.create_from_report(sample_report) + ) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={ + "author": {"id": "12345", "username": "codecov-test-user"}, + "base": {"branch": "master", "commitid": base_commit.commitid}, + "head": { + "branch": "reason/some-testing", + "commitid": head_commit.commitid, + }, + "number": str(pull.pullid), + "id": str(pull.pullid), + "state": "open", + "title": "Creating new code for reasons no one knows", + }, + ), + ) + ) + + +@pytest.fixture +def sample_comparison_coverage_carriedforward( + dbsession, request, sample_commit_with_report_already_carriedforward, mocker +): + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + return_value="github-integration-token", + ) + head_commit = sample_commit_with_report_already_carriedforward + base_commit = CommitFactory.create(repository=head_commit.repository) + + repository = head_commit.repository + dbsession.add(repository) + dbsession.flush() + pull = PullFactory.create( + repository=repository, base=base_commit.commitid, head=head_commit.commitid + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + + yaml_dict = {"flags": {"enterprise": {"carryforward": True}}} + report = ReportService(yaml_dict).get_existing_report_for_commit(head_commit) + report._totals = ( + None # need to reset the report to get it to recalculate totals correctly + ) + base_full_commit = FullCommit( + commit=base_commit, report=ReadOnlyReport.create_from_report(report) + ) + head_full_commit = FullCommit( + commit=head_commit, report=ReadOnlyReport.create_from_report(report) + ) + + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={ + "author": {"id": "12345", "username": "codecov-test-user"}, + "base": {"branch": "master", "commitid": base_commit.commitid}, + "head": { + "branch": "reason/some-testing", + "commitid": head_commit.commitid, + }, + "number": str(pull.pullid), + "id": str(pull.pullid), + "state": "open", + "title": "Creating new code for reasons no one knows", + }, + ), + ) + ) + + +@pytest.fixture +def sample_comparison_negative_change(dbsession, request, sample_report, mocker): + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + return_value="github-integration-token", + ) + repository = RepositoryFactory.create( + owner__username=request.node.name, + owner__service="github", + owner__integration_id="10000", + using_integration=True, + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository) + head_commit = CommitFactory.create(repository=repository, branch="new_branch") + pull = PullFactory.create( + repository=repository, base=base_commit.commitid, head=head_commit.commitid + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + repository = base_commit.repository + base_full_commit = FullCommit( + commit=base_commit, report=ReadOnlyReport.create_from_report(sample_report) + ) + head_full_commit = FullCommit( + commit=head_commit, report=ReadOnlyReport.create_from_report(get_small_report()) + ) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={ + "author": {"id": "12345", "username": "codecov-test-user"}, + "base": {"branch": "master", "commitid": base_commit.commitid}, + "head": { + "branch": "reason/some-testing", + "commitid": head_commit.commitid, + }, + "number": str(pull.pullid), + "id": str(pull.pullid), + "state": "open", + "title": "Creating new code for reasons no one knows", + }, + ), + ) + ) + + +@pytest.fixture +def sample_comparison_no_change(dbsession, request, sample_report, mocker): + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + return_value="github-integration-token", + ) + repository = RepositoryFactory.create( + owner__username=request.node.name, + owner__service="github", + owner__integration_id="10000", + using_integration=True, + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository) + head_commit = CommitFactory.create(repository=repository, branch="new_branch") + pull = PullFactory.create( + repository=repository, base=base_commit.commitid, head=head_commit.commitid + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + repository = base_commit.repository + base_full_commit = FullCommit( + commit=base_commit, report=ReadOnlyReport.create_from_report(sample_report) + ) + head_full_commit = FullCommit( + commit=head_commit, report=ReadOnlyReport.create_from_report(sample_report) + ) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={ + "author": {"id": "12345", "username": "codecov-test-user"}, + "base": {"branch": "master", "commitid": base_commit.commitid}, + "head": { + "branch": "reason/some-testing", + "commitid": head_commit.commitid, + }, + "number": str(pull.pullid), + "id": str(pull.pullid), + "state": "open", + "title": "Creating new code for reasons no one knows", + }, + ), + ) + ) + + +@pytest.fixture +def sample_comparison_without_pull(dbsession, request, sample_report, mocker): + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + return_value="github-integration-token", + ) + repository = RepositoryFactory.create( + owner__username=request.node.name, + owner__service="github", + owner__integration_id="10000", + using_integration=True, + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository, author__service="github") + head_commit = CommitFactory.create( + repository=repository, branch="new_branch", author__service="github" + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.flush() + repository = base_commit.repository + base_full_commit = FullCommit( + commit=base_commit, report=ReadOnlyReport.create_from_report(get_small_report()) + ) + head_full_commit = FullCommit( + commit=head_commit, report=ReadOnlyReport.create_from_report(sample_report) + ) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull(database_pull=None, provider_pull=None), + ) + ) + + +@pytest.fixture +def sample_comparison_database_pull_without_provider( + dbsession, request, sample_report, mocker +): + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + return_value="github-integration-token", + ) + repository = RepositoryFactory.create( + owner__username=request.node.name, + owner__integration_id="10000", + using_integration=True, + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository) + head_commit = CommitFactory.create(repository=repository, branch="new_branch") + pull = PullFactory.create( + repository=repository, base=base_commit.commitid, head=head_commit.commitid + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + repository = base_commit.repository + base_full_commit = FullCommit( + commit=base_commit, report=ReadOnlyReport.create_from_report(get_small_report()) + ) + head_full_commit = FullCommit( + commit=head_commit, report=ReadOnlyReport.create_from_report(sample_report) + ) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull(database_pull=pull, provider_pull=None), + ) + ) + + +def generate_sample_comparison(username, dbsession, base_report, head_report): + repository = RepositoryFactory.create( + owner__username=username, + owner__service="github", + owner__integration_id="10000", + using_integration=True, + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository, author__service="github") + head_commit = CommitFactory.create( + repository=repository, branch="new_branch", author__service="github" + ) + pull = PullFactory.create( + repository=repository, base=base_commit.commitid, head=head_commit.commitid + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + repository = base_commit.repository + base_full_commit = FullCommit(commit=base_commit, report=base_report) + head_full_commit = FullCommit(commit=head_commit, report=head_report) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={ + "author": {"id": "12345", "username": "codecov-test-user"}, + "base": {"branch": "master", "commitid": base_commit.commitid}, + "head": { + "branch": "reason/some-testing", + "commitid": head_commit.commitid, + }, + "number": str(pull.pullid), + "id": str(pull.pullid), + "state": "open", + "title": "Creating new code for reasons no one knows", + }, + ), + ) + ) + + +@pytest.fixture +def sample_comparison_without_base_report(dbsession, request, sample_report, mocker): + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + return_value="github-integration-token", + ) + repository = RepositoryFactory.create( + owner__username=request.node.name, + owner__service="github", + owner__integration_id="10000", + using_integration=True, + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository, author__service="github") + head_commit = CommitFactory.create( + repository=repository, branch="new_branch", author__service="github" + ) + pull = PullFactory.create( + repository=repository, base=base_commit.commitid, head=head_commit.commitid + ) + dbsession.add(head_commit) + dbsession.add(base_commit) + dbsession.add(pull) + dbsession.flush() + head_full_commit = FullCommit( + commit=head_commit, report=ReadOnlyReport.create_from_report(sample_report) + ) + base_full_commit = FullCommit(commit=base_commit, report=None) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={ + "author": {"id": "12345", "username": "codecov-test-user"}, + "base": {"branch": "master", "commitid": base_commit.commitid}, + "head": { + "branch": "reason/some-testing", + "commitid": head_commit.commitid, + }, + "number": str(pull.pullid), + "id": str(pull.pullid), + "state": "open", + "title": "Creating new code for reasons no one knows", + }, + ), + ) + ) + + +@pytest.fixture +def sample_comparison_matching_flags(dbsession, request, sample_report): + base_report = ReadOnlyReport.create_from_report(get_small_report(flags=["unit"])) + head_report = ReadOnlyReport.create_from_report(sample_report) + return generate_sample_comparison( + request.node.name, dbsession, base_report, head_report + ) + + +@pytest.fixture +def sample_comparison_without_base_with_pull(dbsession, request, sample_report, mocker): + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + return_value="github-integration-token", + ) + repository = RepositoryFactory.create( + owner__username=request.node.name, + owner__service="github", + owner__integration_id="10000", + using_integration=True, + ) + dbsession.add(repository) + dbsession.flush() + head_commit = CommitFactory.create(repository=repository, branch="new_branch") + pull = PullFactory.create( + repository=repository, base="base_commitid", head=head_commit.commitid + ) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + head_full_commit = FullCommit( + commit=head_commit, report=ReadOnlyReport.create_from_report(sample_report) + ) + base_full_commit = FullCommit(commit=None, report=None) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid="cdf9aa4bd2c6bcd8a662864097cb62a85a2fd55b", + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={ + "author": {"id": "12345", "username": "codecov-test-user"}, + "base": { + "branch": "master", + "commitid": "cdf9aa4bd2c6bcd8a662864097cb62a85a2fd55b", + }, + "head": { + "branch": "reason/some-testing", + "commitid": head_commit.commitid, + }, + "number": str(pull.pullid), + "id": str(pull.pullid), + "state": "open", + "title": "Creating new code for reasons no one knows", + }, + ), + ) + ) + + +@pytest.fixture +def sample_comparison_head_and_pull_head_differ( + dbsession, request, sample_report, mocker +): + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + return_value="github-integration-token", + ) + repository = RepositoryFactory.create( + owner__service="github", + owner__username="ThiagoCodecov", + name="example-python", + owner__unencrypted_oauth_token="testtlxuu2kfef3km1fbecdlmnb2nvpikvmoadi3", + image_token="abcdefghij", + owner__integration_id="10000", + using_integration=True, + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository, author__service="github") + head_commit = CommitFactory.create( + repository=repository, branch="new_branch", author__service="github" + ) + random_commit = CommitFactory.create( + repository=repository, author__service="github" + ) + pull = PullFactory.create( + repository=repository, base=base_commit.commitid, head=head_commit.commitid + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + repository = base_commit.repository + base_full_commit = FullCommit( + commit=base_commit, report=ReadOnlyReport.create_from_report(get_small_report()) + ) + head_full_commit = FullCommit( + commit=head_commit, report=ReadOnlyReport.create_from_report(sample_report) + ) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={ + "author": {"id": "12345", "username": "codecov-test-user"}, + "base": {"branch": "master", "commitid": base_commit.commitid}, + "head": { + "branch": "reason/some-testing", + "commitid": random_commit.commitid, + }, + "number": str(pull.pullid), + "id": str(pull.pullid), + "state": "open", + "title": "Creating new code for reasons no one knows", + }, + ), + ) + ) diff --git a/apps/worker/services/notification/notifiers/tests/integration/__init__.py b/apps/worker/services/notification/notifiers/tests/integration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify.yaml b/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify.yaml new file mode 100644 index 0000000000..8a4421905f --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify.yaml @@ -0,0 +1,960 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/compare/4535be18e90467d6d9a99c0ce651becec7f7eba6...2e2600aa09525e2e1e1d98b09de61454d29c94bb + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/4535be18e90467d6d9a99c0ce651becec7f7eba6...2e2600aa09525e2e1e1d98b09de61454d29c94bb","html_url":"https://github.com/ThiagoCodecov/example-python/compare/4535be18e90467d6d9a99c0ce651becec7f7eba6...2e2600aa09525e2e1e1d98b09de61454d29c94bb","permalink_url":"https://github.com/ThiagoCodecov/example-python/compare/ThiagoCodecov:4535be1...ThiagoCodecov:2e2600a","diff_url":"https://github.com/ThiagoCodecov/example-python/compare/4535be18e90467d6d9a99c0ce651becec7f7eba6...2e2600aa09525e2e1e1d98b09de61454d29c94bb.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/compare/4535be18e90467d6d9a99c0ce651becec7f7eba6...2e2600aa09525e2e1e1d98b09de61454d29c94bb.patch","base_commit":{"sha":"4535be18e90467d6d9a99c0ce651becec7f7eba6","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjQ1MzViZTE4ZTkwNDY3ZDZkOWE5OWMwY2U2NTFiZWNlYzdmN2ViYTY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-31T03:23:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-31T03:23:55Z"},"message":"20EE4D09-F49E-474B-B7AF-4E4916EF82FA","tree":{"sha":"7da71bf52d31d24403d4208835b5d1333ba54695","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7da71bf52d31d24403d4208835b5d1333ba54695"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/4535be18e90467d6d9a99c0ce651becec7f7eba6","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"66567ec66e07440e3170910df61cf458812323fc","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/66567ec66e07440e3170910df61cf458812323fc","html_url":"https://github.com/ThiagoCodecov/example-python/commit/66567ec66e07440e3170910df61cf458812323fc"}]},"merge_base_commit":{"sha":"30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjMwY2MxZWQ3NTFhNTlmYTllN2FkOGU3OWZmZjQxYTZmZTExZWY1ZGQ=","commit":{"author":{"name":"Thiago","email":"44376991+ThiagoCodecov@users.noreply.github.com","date":"2019-12-09T12:02:11Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2019-12-09T12:02:11Z"},"message":"Update + README.md","tree":{"sha":"c901b3cb75fd85aebf03f254494d5ad06b86777e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c901b3cb75fd85aebf03f254494d5ad06b86777e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJd7jfDCRBK7hj4Ov3rIwAAdHIIAF8TrpEbraVNLuNObBiQefch\nVHGpc6MNxOnnRgGPZZl/egrosAxCJ2og+ArrjQG+D/7eYUhWeb73YPR8lWvVf6YN\nIkNSwv1o/2bmH3pma9DR4wDXnKeO/w/UPhNZaGC1w3dcvsHSJReSRkl/ipLxtPep\n3XpmC8RdIwFNU1hG8Dd5iKqJz3d3iA3mdkb3vrvYmKq2imQigSbXpOvL1o/Du9DN\nxE29+ZeGqTZf0Z5QHgJT7THw8P1bf/UJUado7nn6cIon/slDbP/ZirN3rgEJWXU6\neH9/bdS+HMcCwK5TZwJBXgRkbtzRIgc7mj5cTWOSX8XFuUnOS/QCyfkAIvajseU=\n=3PXD\n-----END + PGP SIGNATURE-----\n","payload":"tree c901b3cb75fd85aebf03f254494d5ad06b86777e\nparent + 2dfb36626ca60a8aa6d2bc624ad0f54b473bb436\nauthor Thiago <44376991+ThiagoCodecov@users.noreply.github.com> + 1575892931 -0300\ncommitter GitHub 1575892931 -0300\n\nUpdate + README.md"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars3.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"2dfb36626ca60a8aa6d2bc624ad0f54b473bb436","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2dfb36626ca60a8aa6d2bc624ad0f54b473bb436","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2dfb36626ca60a8aa6d2bc624ad0f54b473bb436"}]},"status":"diverged","ahead_by":3,"behind_by":11,"total_commits":3,"commits":[{"sha":"e022f588e40269f8ab1c568080dc4a7662ff2335","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmUwMjJmNTg4ZTQwMjY5ZjhhYjFjNTY4MDgwZGM0YTc2NjJmZjIzMzU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:44Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:44Z"},"message":"69E76A1D-FD86-4A90-A6CE-6A66BEEE75CA","tree":{"sha":"6188b6c086ebf6c63626abca649d2596b145c84c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6188b6c086ebf6c63626abca649d2596b145c84c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e022f588e40269f8ab1c568080dc4a7662ff2335","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd"}]},{"sha":"c2a05aa15ecad5bec37e29b9fe51ef30120f8642","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmMyYTA1YWExNWVjYWQ1YmVjMzdlMjliOWZlNTFlZjMwMTIwZjg2NDI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:47Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:47Z"},"message":"81905186-E567-4E4E-A5CB-3E5BFB820E57","tree":{"sha":"dd944d7739d5d429f264709345fe481c1e602be8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/dd944d7739d5d429f264709345fe481c1e602be8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e022f588e40269f8ab1c568080dc4a7662ff2335","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e022f588e40269f8ab1c568080dc4a7662ff2335"}]},{"sha":"2e2600aa09525e2e1e1d98b09de61454d29c94bb","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJlMjYwMGFhMDk1MjVlMmUxZTFkOThiMDlkZTYxNDU0ZDI5Yzk0YmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:49Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:49Z"},"message":"AE0A9904-3B05-405A-B8F7-A5DD3B38C92D","tree":{"sha":"f64d89541a0899881da6764081991e70ed43bfea","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f64d89541a0899881da6764081991e70ed43bfea"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2e2600aa09525e2e1e1d98b09de61454d29c94bb","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"c2a05aa15ecad5bec37e29b9fe51ef30120f8642","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c2a05aa15ecad5bec37e29b9fe51ef30120f8642"}]}],"files":[{"sha":"657d4c0ca181f2df83261d3ab1922d6ec47ece1e","filename":"README.md","status":"modified","additions":8,"deletions":0,"changes":8,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/2e2600aa09525e2e1e1d98b09de61454d29c94bb/README.md","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/2e2600aa09525e2e1e1d98b09de61454d29c94bb/README.md","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.md?ref=2e2600aa09525e2e1e1d98b09de61454d29c94bb","patch":"@@ + -1 +1,9 @@\n Now this\n+\n+\n+\n+80EC0808-DD33-42DF-8873-B2A605A82432\n+63FE7DEF-8367-47BD-B864-D86DE9F94358\n+69E76A1D-FD86-4A90-A6CE-6A66BEEE75CA\n+81905186-E567-4E4E-A5CB-3E5BFB820E57\n+AE0A9904-3B05-405A-B8F7-A5DD3B38C92D"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 03 Jan 2020 20:01:18 GMT + Etag: + - W/"44cd18200d235728506114fd6b930cca" + Last-Modified: + - Tue, 31 Dec 2019 03:23:55 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - D081:309E:10C87AE:17A2BD6:5E0F9D8E + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4995' + X-Ratelimit-Reset: + - '1578085278' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/compare/4535be18e90467d6d9a99c0ce651becec7f7eba6...2e2600aa09525e2e1e1d98b09de61454d29c94bb +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15","id":350683791,"node_id":"MDExOlB1bGxSZXF1ZXN0MzUwNjgzNzkx","html_url":"https://github.com/ThiagoCodecov/example-python/pull/15","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/15.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/15.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/15","number":15,"state":"open","locked":false,"title":"Thiago/test + 1","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"","created_at":"2019-12-09T12:19:01Z","updated_at":"2019-12-09T12:19:01Z","closed_at":null,"merged_at":null,"merge_commit_sha":null,"assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/15/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/2e2600aa09525e2e1e1d98b09de61454d29c94bb","head":{"label":"ThiagoCodecov:thiago/test-1","ref":"thiago/test-1","sha":"2e2600aa09525e2e1e1d98b09de61454d29c94bb","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2019-12-31T03:24:04Z","pushed_at":"2019-12-31T03:24:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":161,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":4,"license":null,"forks":0,"open_issues":4,"watchers":0,"default_branch":"master"}},"base":{"label":"ThiagoCodecov:master","ref":"master","sha":"d723f5cb5c9c9f48c47f2df97c47de20457d3fdc","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2019-12-31T03:24:04Z","pushed_at":"2019-12-31T03:24:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":161,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":4,"license":null,"forks":0,"open_issues":4,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/15"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/15"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/15/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/2e2600aa09525e2e1e1d98b09de61454d29c94bb"}},"author_association":"OWNER","merged":false,"mergeable":false,"rebaseable":false,"mergeable_state":"dirty","merged_by":null,"comments":0,"review_comments":0,"maintainer_can_modify":false,"commits":3,"additions":8,"deletions":0,"changed_files":1}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 03 Jan 2020 20:01:19 GMT + Etag: + - W/"80de2d6223eb160b0cd153534733b75d" + Last-Modified: + - Fri, 03 Jan 2020 19:56:45 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - D082:50AC:E05476:13C1D91:5E0F9D8E + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4994' + X-Ratelimit-Reset: + - '1578085278' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15 +- request: + body: '{"body": "## [Codecov](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=h1) + Report\n> Merging [#15](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=desc) + into [master](None/gh/ThiagoCodecov/example-python/commit/4535be18e90467d6d9a99c0ce651becec7f7eba6&el=desc) + will **increase** coverage by `10.00%`.\n> The diff coverage is `n/a`.\n\n[![Impacted + file tree graph](None/gh/ThiagoCodecov/example-python/pull/15/graphs/tree.svg?width=650&height=150&src=pr&token=CJ00P0THAO)](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree)\n\n```diff\n@@ Coverage + Diff @@\n## master #15 +/- ##\n===========================================\n+ + Coverage 50.00% 60.00% +10.00% \n===========================================\n Files 2 2 \n Lines 6 10 +4 \n Branches 0 1 +1 \n===========================================\n+ + Hits 3 6 +3 \n Misses 3 3 \n- + Partials 0 1 +1 \n```\n\n| Flag | Coverage \u0394 | + |\n|---|---|---|\n| #unit | `100.00% <\u00f8> (?)` | |\n\n| [Impacted Files](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree) + | Coverage \u0394 | |\n|---|---|---|\n| [file\\_2.py](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree#diff-ZmlsZV8yLnB5) + | `50.00% <0.00%> (\u00f8)` | :arrow_up: |\n| [file\\_1.go](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree#diff-ZmlsZV8xLmdv) + | `62.50% <0.00%> (+12.50%)` | :arrow_up: |\n\n------\n\n[Continue to review + full report at Codecov](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=continue).\n> + **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)\n> + `\u0394 = absolute (impact)`, `\u00f8 = not affected`, `? = missing + data`\n> Powered by [Codecov](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=footer). + Last update [d723f5c...2e2600a](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=lastupdated). + Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).\n"}' + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/issues/15/comments + response: + content: "{\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments/570682170\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/pull/15#issuecomment-570682170\"\ + ,\"issue_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/issues/15\"\ + ,\"id\":570682170,\"node_id\":\"MDEyOklzc3VlQ29tbWVudDU3MDY4MjE3MA==\",\"user\"\ + :{\"login\":\"ThiagoCodecov\",\"id\":44376991,\"node_id\":\"MDQ6VXNlcjQ0Mzc2OTkx\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/44376991?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/ThiagoCodecov\",\"\ + html_url\":\"https://github.com/ThiagoCodecov\",\"followers_url\":\"https://api.github.com/users/ThiagoCodecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/ThiagoCodecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}\"\ + ,\"starred_url\":\"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/ThiagoCodecov/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/ThiagoCodecov/orgs\",\"\ + repos_url\":\"https://api.github.com/users/ThiagoCodecov/repos\",\"events_url\"\ + :\"https://api.github.com/users/ThiagoCodecov/events{/privacy}\",\"received_events_url\"\ + :\"https://api.github.com/users/ThiagoCodecov/received_events\",\"type\":\"\ + User\",\"site_admin\":false},\"created_at\":\"2020-01-03T20:01:19Z\",\"updated_at\"\ + :\"2020-01-03T20:01:19Z\",\"author_association\":\"OWNER\",\"body\":\"# [Codecov](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=h1)\ + \ Report\\n> Merging [#15](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=desc)\ + \ into [master](None/gh/ThiagoCodecov/example-python/commit/4535be18e90467d6d9a99c0ce651becec7f7eba6&el=desc)\ + \ will **increase** coverage by `10.00%`.\\n> The diff coverage is `n/a`.\\\ + n\\n[![Impacted file tree graph](None/gh/ThiagoCodecov/example-python/pull/15/graphs/tree.svg?width=650&height=150&src=pr&token=CJ00P0THAO)](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree)\\\ + n\\n```diff\\n@@ Coverage Diff @@\\n## master\ + \ #15 +/- ##\\n===========================================\\n+\ + \ Coverage 50.00% 60.00% +10.00% \\n===========================================\\\ + n Files 2 2 \\n Lines 6 10\ + \ +4 \\n Branches 0 1 +1 \\n===========================================\\\ + n+ Hits 3 6 +3 \\n Misses 3 3\ + \ \\n- Partials 0 1 +1 \\n```\\n\\n|\ + \ Flag | Coverage \u0394 | |\\n|---|---|---|\\n| #unit | `100.00% <\xF8> (?)`\ + \ | |\\n\\n| [Impacted Files](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree)\ + \ | Coverage \u0394 | |\\n|---|---|---|\\n| [file\\\\_2.py](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree#diff-ZmlsZV8yLnB5)\ + \ | `50.00% <0.00%> (\xF8)` | :arrow_up: |\\n| [file\\\\_1.go](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree#diff-ZmlsZV8xLmdv)\ + \ | `62.50% <0.00%> (+12.50%)` | :arrow_up: |\\n\\n------\\n\\n[Continue to\ + \ review full report at Codecov](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=continue).\\\ + n> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)\\\ + n> `\u0394 = absolute (impact)`, `\xF8 = not affected`, `? = missing\ + \ data`\\n> Powered by [Codecov](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=footer).\ + \ Last update [d723f5c...2e2600a](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=lastupdated).\ + \ Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).\\\ + n\"}" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Length: + - '3545' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 03 Jan 2020 20:01:20 GMT + Etag: + - '"b9894b79f2e1d3509dad975b48cb566b"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments/570682170 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - D083:50AA:26C744:38893C:5E0F9D8F + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4993' + X-Ratelimit-Reset: + - '1578085278' + X-Xss-Protection: + - 1; mode=block + status: + code: 201 + message: Created + status_code: 201 + url: https://api.github.com/repos/ThiagoCodecov/example-python/issues/15/comments +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/commits + response: + content: '[{"sha":"e022f588e40269f8ab1c568080dc4a7662ff2335","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmUwMjJmNTg4ZTQwMjY5ZjhhYjFjNTY4MDgwZGM0YTc2NjJmZjIzMzU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:44Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:44Z"},"message":"69E76A1D-FD86-4A90-A6CE-6A66BEEE75CA","tree":{"sha":"6188b6c086ebf6c63626abca649d2596b145c84c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6188b6c086ebf6c63626abca649d2596b145c84c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e022f588e40269f8ab1c568080dc4a7662ff2335","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd"}]},{"sha":"c2a05aa15ecad5bec37e29b9fe51ef30120f8642","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmMyYTA1YWExNWVjYWQ1YmVjMzdlMjliOWZlNTFlZjMwMTIwZjg2NDI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:47Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:47Z"},"message":"81905186-E567-4E4E-A5CB-3E5BFB820E57","tree":{"sha":"dd944d7739d5d429f264709345fe481c1e602be8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/dd944d7739d5d429f264709345fe481c1e602be8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e022f588e40269f8ab1c568080dc4a7662ff2335","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e022f588e40269f8ab1c568080dc4a7662ff2335"}]},{"sha":"2e2600aa09525e2e1e1d98b09de61454d29c94bb","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJlMjYwMGFhMDk1MjVlMmUxZTFkOThiMDlkZTYxNDU0ZDI5Yzk0YmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:49Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:49Z"},"message":"AE0A9904-3B05-405A-B8F7-A5DD3B38C92D","tree":{"sha":"f64d89541a0899881da6764081991e70ed43bfea","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f64d89541a0899881da6764081991e70ed43bfea"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2e2600aa09525e2e1e1d98b09de61454d29c94bb","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"c2a05aa15ecad5bec37e29b9fe51ef30120f8642","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c2a05aa15ecad5bec37e29b9fe51ef30120f8642"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 17 Jan 2020 00:10:36 GMT + Etag: + - W/"ccf882511531ad012f6664fd8f73c441" + Last-Modified: + - Fri, 17 Jan 2020 00:10:01 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 132D:1D4D:20EF65:2DD4BC:5E20FB7C + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4994' + X-Ratelimit-Reset: + - '1579222789' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/commits +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/branches?per_page=100&page=1 + response: + content: '[{"name":"master","commit":{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654"},"protected":false},{"name":"random-branch","commit":{"sha":"b12b8ba6046c1738c5c89325b8a63a6f51bc4ff6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b12b8ba6046c1738c5c89325b8a63a6f51bc4ff6"},"protected":false},{"name":"thiago/base-no-base","commit":{"sha":"620b8042c2f846fcc6dda1c732dedd15cdbe76db","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/620b8042c2f846fcc6dda1c732dedd15cdbe76db"},"protected":false},{"name":"thiago/f/big-pt","commit":{"sha":"d55dc4ef748fd11537e50c9abed4ab1864fa1d94","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d55dc4ef748fd11537e50c9abed4ab1864fa1d94"},"protected":false},{"name":"thiago/f/cool-branch","commit":{"sha":"89f3d20f2be3fb2d098e544814f8ce3636dc78c4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/89f3d20f2be3fb2d098e544814f8ce3636dc78c4"},"protected":false},{"name":"thiago/f/something","commit":{"sha":"c9fb9262268f11b53c6b0682d4f9acac89bdeee5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c9fb9262268f11b53c6b0682d4f9acac89bdeee5"},"protected":false},{"name":"thiago/test-1","commit":{"sha":"2e2600aa09525e2e1e1d98b09de61454d29c94bb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb"},"protected":false}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 03 Jul 2023 04:24:01 GMT + ETag: + - W/"dab6303e47e5d12f9bb596bcb1e3fd8a7e482a11e1e543c0b1e05e279ea55fe3" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C46C:0919:18CCE0:32A952:64A24D61 + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4997' + X-RateLimit-Reset: + - '1688361672' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '3' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-08-02 04:17:36 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...4535be18e90467d6d9a99c0ce651becec7f7eba6 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...4535be18e90467d6d9a99c0ce651becec7f7eba6","html_url":"https://github.com/ThiagoCodecov/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...4535be18e90467d6d9a99c0ce651becec7f7eba6","permalink_url":"https://github.com/ThiagoCodecov/example-python/compare/ThiagoCodecov:93189ce...ThiagoCodecov:4535be1","diff_url":"https://github.com/ThiagoCodecov/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...4535be18e90467d6d9a99c0ce651becec7f7eba6.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...4535be18e90467d6d9a99c0ce651becec7f7eba6.patch","base_commit":{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjkzMTg5Y2U1MGYyMjQyOTZkNjQxMmUyODg0YjkzZGNjM2M3Yzg2NTQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"message":"Adding + some encoding","tree":{"sha":"966e4dc888b0828c5dea75f21e82eb2daa9a4485","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/966e4dc888b0828c5dea75f21e82eb2daa9a4485"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","html_url":"https://github.com/ThiagoCodecov/example-python/commit/93189ce50f224296d6412e2884b93dcc3c7c8654","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67"}]},"merge_base_commit":{"sha":"4535be18e90467d6d9a99c0ce651becec7f7eba6","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjQ1MzViZTE4ZTkwNDY3ZDZkOWE5OWMwY2U2NTFiZWNlYzdmN2ViYTY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-31T03:23:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-31T03:23:55Z"},"message":"20EE4D09-F49E-474B-B7AF-4E4916EF82FA","tree":{"sha":"7da71bf52d31d24403d4208835b5d1333ba54695","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7da71bf52d31d24403d4208835b5d1333ba54695"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/4535be18e90467d6d9a99c0ce651becec7f7eba6","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"66567ec66e07440e3170910df61cf458812323fc","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/66567ec66e07440e3170910df61cf458812323fc","html_url":"https://github.com/ThiagoCodecov/example-python/commit/66567ec66e07440e3170910df61cf458812323fc"}]},"status":"behind","ahead_by":0,"behind_by":52,"total_commits":0,"commits":[],"files":[]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 03 Jul 2023 04:24:01 GMT + ETag: + - W/"c625aec9cfb15b0fc585ff2dbcdaeef7a4e39d1881112104a691b9fb0f748c49" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C46D:8600:1BB361:387DFB:64A24D61 + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4996' + X-RateLimit-Reset: + - '1688361672' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '4' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-08-02 04:17:36 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d + response: + content: '{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d","html_url":"https://github.com/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d","permalink_url":"https://github.com/joseph-sentry/codecov-demo/compare/joseph-sentry:5b174c2...joseph-sentry:5601846","diff_url":"https://github.com/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d.diff","patch_url":"https://github.com/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d.patch","base_commit":{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","node_id":"C_kwDOJu7MkNoAKDViMTc0YzJiNDBkNTAxYTcwYzQ3OWU5MTAyNWQ1MTA5YjFhZDVjMWI","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"message":"make + change on main","tree":{"sha":"a9d6c66e50012a6d42f3f4c4e6d6de845230e73b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/a9d6c66e50012a6d42f3f4c4e6d6de845230e73b"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQCSXJE75ylD7PwNrXf6ITnsvh9gIP5b/JASjGlKqB7f37XPt9wLvT2YaV3HcSyTRyt\nCY9zBOZT+tELiyZcpqKwY=\n-----END + SSH SIGNATURE-----","payload":"tree a9d6c66e50012a6d42f3f4c4e6d6de845230e73b\nparent + 3fe10c352faae434e0cca7c1c5984c1c98000288\nauthor joseph-sentry + 1692026348 -0400\ncommitter joseph-sentry 1692026348 + -0400\n\nmake change on main\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"3fe10c352faae434e0cca7c1c5984c1c98000288","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/3fe10c352faae434e0cca7c1c5984c1c98000288","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/3fe10c352faae434e0cca7c1c5984c1c98000288"}]},"merge_base_commit":{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","node_id":"C_kwDOJu7MkNoAKDViMTc0YzJiNDBkNTAxYTcwYzQ3OWU5MTAyNWQ1MTA5YjFhZDVjMWI","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"message":"make + change on main","tree":{"sha":"a9d6c66e50012a6d42f3f4c4e6d6de845230e73b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/a9d6c66e50012a6d42f3f4c4e6d6de845230e73b"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQCSXJE75ylD7PwNrXf6ITnsvh9gIP5b/JASjGlKqB7f37XPt9wLvT2YaV3HcSyTRyt\nCY9zBOZT+tELiyZcpqKwY=\n-----END + SSH SIGNATURE-----","payload":"tree a9d6c66e50012a6d42f3f4c4e6d6de845230e73b\nparent + 3fe10c352faae434e0cca7c1c5984c1c98000288\nauthor joseph-sentry + 1692026348 -0400\ncommitter joseph-sentry 1692026348 + -0400\n\nmake change on main\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"3fe10c352faae434e0cca7c1c5984c1c98000288","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/3fe10c352faae434e0cca7c1c5984c1c98000288","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/3fe10c352faae434e0cca7c1c5984c1c98000288"}]},"status":"ahead","ahead_by":1,"behind_by":0,"total_commits":1,"commits":[{"sha":"5601846871b8142ab0df1e0b8774756c658bcc7d","node_id":"C_kwDOJu7MkNoAKDU2MDE4NDY4NzFiODE0MmFiMGRmMWUwYjg3NzQ3NTZjNjU4YmNjN2Q","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:16:17Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:23:50Z"},"message":"make + change","tree":{"sha":"3ca6632aeb641e579bea4391294d9ebe55c062c7","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/3ca6632aeb641e579bea4391294d9ebe55c062c7"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5601846871b8142ab0df1e0b8774756c658bcc7d","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQAmeUxSTgmd3FlcaYtRmVpuCh0n6L0/2Oy7wnsoGF6zjLoBO+aDz22vOG+bywNp4bc\nlmOl+Tu5Jk9woOhH2Olg8=\n-----END + SSH SIGNATURE-----","payload":"tree 3ca6632aeb641e579bea4391294d9ebe55c062c7\nparent + 5b174c2b40d501a70c479e91025d5109b1ad5c1b\nauthor joseph-sentry + 1692026177 -0400\ncommitter joseph-sentry 1692026630 + -0400\n\nmake change\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5601846871b8142ab0df1e0b8774756c658bcc7d","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b"}]}],"files":[{"sha":"8664ca881cd0536ab32eac8dfee923a2b304167d","filename":"api/calculator/calculator.py","status":"modified","additions":1,"deletions":3,"changes":4,"blob_url":"https://github.com/joseph-sentry/codecov-demo/blob/5601846871b8142ab0df1e0b8774756c658bcc7d/api%2Fcalculator%2Fcalculator.py","raw_url":"https://github.com/joseph-sentry/codecov-demo/raw/5601846871b8142ab0df1e0b8774756c658bcc7d/api%2Fcalculator%2Fcalculator.py","contents_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/contents/api%2Fcalculator%2Fcalculator.py?ref=5601846871b8142ab0df1e0b8774756c658bcc7d","patch":"@@ + -9,6 +9,4 @@ def multiply(x, y):\n return x * y\n \n def divide(x, + y):\n- if y == 0:\n- return ''Cannot divide by 0''\n- return + x * 1.0 / y\n\\ No newline at end of file\n+ return x * 1.0 / y"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:42 GMT + ETag: + - W/"3bcbc91ba2aac4e70b8b74075bc06a0b7436e42a61c15cd11f8c28af48d74080" + Last-Modified: + - Mon, 14 Aug 2023 15:23:50 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E726:5597:D48B2:1B1274:64EF8E1A + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4987' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '13' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:36:05 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/branches/main + response: + content: '{"name":"main","commit":{"sha":"38c2d0214f2a48c9212a140f5311977059a15b35","node_id":"C_kwDOJu7MkNoAKDM4YzJkMDIxNGYyYTQ4YzkyMTJhMTQwZjUzMTE5NzcwNTlhMTViMzU","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"message":"make + change to app\n\nSigned-off-by: joseph-sentry ","tree":{"sha":"9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/38c2d0214f2a48c9212a140f5311977059a15b35","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQJe3OYXkY7fA1+RZ130Zrab06lH36l2XC9HMqjHDAW8iCW5VUbvNhOxNpKSokDItdd\nroqDRnazyFj3+oWVCvxgM=\n-----END + SSH SIGNATURE-----","payload":"tree 9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043\nparent + 1713006782821e36912c23baa98901c7361ee83c\nauthor joseph-sentry + 1692377290 -0400\ncommitter joseph-sentry 1692377290 + -0400\n\nmake change to app\n\nSigned-off-by: joseph-sentry \n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/38c2d0214f2a48c9212a140f5311977059a15b35","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"1713006782821e36912c23baa98901c7361ee83c","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/1713006782821e36912c23baa98901c7361ee83c","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/1713006782821e36912c23baa98901c7361ee83c"}]},"_links":{"self":"https://api.github.com/repos/joseph-sentry/codecov-demo/branches/main","html":"https://github.com/joseph-sentry/codecov-demo/tree/main"},"protected":false,"protection":{"enabled":false,"required_status_checks":{"enforcement_level":"off","contexts":[],"checks":[]}},"protection_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/branches/main/protection"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:42 GMT + ETag: + - W/"28690f5aa55bcb187fc062a1ab6ad5901a1ccbe0225d6f96dacf712da8e22306" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E727:0526:134901:270CDD:64EF8E1A + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4986' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '14' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:36:05 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b + response: + content: '{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b","permalink_url":"https://github.com/joseph-sentry/codecov-demo/compare/joseph-sentry:38c2d02...joseph-sentry:5b174c2","diff_url":"https://github.com/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b.diff","patch_url":"https://github.com/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b.patch","base_commit":{"sha":"38c2d0214f2a48c9212a140f5311977059a15b35","node_id":"C_kwDOJu7MkNoAKDM4YzJkMDIxNGYyYTQ4YzkyMTJhMTQwZjUzMTE5NzcwNTlhMTViMzU","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"message":"make + change to app\n\nSigned-off-by: joseph-sentry ","tree":{"sha":"9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/38c2d0214f2a48c9212a140f5311977059a15b35","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQJe3OYXkY7fA1+RZ130Zrab06lH36l2XC9HMqjHDAW8iCW5VUbvNhOxNpKSokDItdd\nroqDRnazyFj3+oWVCvxgM=\n-----END + SSH SIGNATURE-----","payload":"tree 9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043\nparent + 1713006782821e36912c23baa98901c7361ee83c\nauthor joseph-sentry + 1692377290 -0400\ncommitter joseph-sentry 1692377290 + -0400\n\nmake change to app\n\nSigned-off-by: joseph-sentry \n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/38c2d0214f2a48c9212a140f5311977059a15b35","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"1713006782821e36912c23baa98901c7361ee83c","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/1713006782821e36912c23baa98901c7361ee83c","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/1713006782821e36912c23baa98901c7361ee83c"}]},"merge_base_commit":{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","node_id":"C_kwDOJu7MkNoAKDViMTc0YzJiNDBkNTAxYTcwYzQ3OWU5MTAyNWQ1MTA5YjFhZDVjMWI","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"message":"make + change on main","tree":{"sha":"a9d6c66e50012a6d42f3f4c4e6d6de845230e73b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/a9d6c66e50012a6d42f3f4c4e6d6de845230e73b"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQCSXJE75ylD7PwNrXf6ITnsvh9gIP5b/JASjGlKqB7f37XPt9wLvT2YaV3HcSyTRyt\nCY9zBOZT+tELiyZcpqKwY=\n-----END + SSH SIGNATURE-----","payload":"tree a9d6c66e50012a6d42f3f4c4e6d6de845230e73b\nparent + 3fe10c352faae434e0cca7c1c5984c1c98000288\nauthor joseph-sentry + 1692026348 -0400\ncommitter joseph-sentry 1692026348 + -0400\n\nmake change on main\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"3fe10c352faae434e0cca7c1c5984c1c98000288","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/3fe10c352faae434e0cca7c1c5984c1c98000288","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/3fe10c352faae434e0cca7c1c5984c1c98000288"}]},"status":"behind","ahead_by":0,"behind_by":2,"total_commits":0,"commits":[],"files":[]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:42 GMT + ETag: + - W/"fb71dcba3dddf292e08598b556f7bcc763019abc6e1678ed3355d3d8ec57454f" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E728:44A9:1226D9:24CF0C:64EF8E1A + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4985' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '15' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:36:05 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"body": "## [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell) + Report\n> Merging [#9](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell) + (5601846) into [main](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell) + (5b174c2) will **increase** coverage by `10.00%`.\n> Report is 2 commits behind + head on main.\n> The diff coverage is `n/a`.\n\n:exclamation: Your organization + is not using the GitHub App Integration. As a result you may experience degraded + service beginning May 15th. Please [install the GitHub App Integration](https://github.com/apps/codecov) + for your organization. [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell).\n\n[![Impacted + file tree graph](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\n\n```diff\n@@ Coverage + Diff @@\n## main #9 +/- ##\n=============================================\n+ + Coverage 50.00% 60.00% +10.00% \n+ Complexity 11 10 -1 \n=============================================\n Files 2 2 \n Lines 6 10 +4 \n Branches 0 1 +1 \n=============================================\n+ + Hits 3 6 +3 \n Misses 3 3 \n- + Partials 0 1 +1 \n```\n\n| [Flag](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell) + | Coverage \u0394 | Complexity \u0394 | |\n|---|---|---|---|\n| [integration](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell) + | `?` | `?` | |\n| [unit](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell) + | `100.00% <\u00f8> (?)` | `0.00 <\u00f8> (?)` | |\n\nFlags with carried forward + coverage won''t be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell#carryforward-flags-in-the-pull-request-comment) + to find out more.\n\n[see 2 files with indirect coverage changes](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\n\n------\n\n[Continue + to review full report in Codecov by Sentry](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell).\n> + **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\n> + `\u0394 = absolute (impact)`, `\u00f8 = not affected`, `? = missing + data`\n> Powered by [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell). + Last update [5b174c2...5601846](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell). + Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell).\n"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '4719' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/issues/9/comments + response: + content: "{\"url\":\"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments/1699669247\"\ + ,\"html_url\":\"https://github.com/joseph-sentry/codecov-demo/pull/9#issuecomment-1699669247\"\ + ,\"issue_url\":\"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/9\"\ + ,\"id\":1699669247,\"node_id\":\"IC_kwDOJu7MkM5lTuT_\",\"user\":{\"login\":\"\ + joseph-sentry\",\"id\":136376984,\"node_id\":\"U_kgDOCCDymA\",\"avatar_url\"\ + :\"https://avatars.githubusercontent.com/u/136376984?u=8154f2067e6b4966acba9c27358d6e49cfbbf45d&v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/joseph-sentry\"\ + ,\"html_url\":\"https://github.com/joseph-sentry\",\"followers_url\":\"https://api.github.com/users/joseph-sentry/followers\"\ + ,\"following_url\":\"https://api.github.com/users/joseph-sentry/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/joseph-sentry/gists{/gist_id}\"\ + ,\"starred_url\":\"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/joseph-sentry/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/joseph-sentry/orgs\",\"\ + repos_url\":\"https://api.github.com/users/joseph-sentry/repos\",\"events_url\"\ + :\"https://api.github.com/users/joseph-sentry/events{/privacy}\",\"received_events_url\"\ + :\"https://api.github.com/users/joseph-sentry/received_events\",\"type\":\"\ + User\",\"site_admin\":false},\"created_at\":\"2023-08-30T18:44:43Z\",\"updated_at\"\ + :\"2023-08-30T18:44:43Z\",\"author_association\":\"OWNER\",\"body\":\"## [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\ + \ Report\\n> Merging [#9](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\ + \ (5601846) into [main](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\ + \ (5b174c2) will **increase** coverage by `10.00%`.\\n> Report is 2 commits\ + \ behind head on main.\\n> The diff coverage is `n/a`.\\n\\n:exclamation: Your\ + \ organization is not using the GitHub App Integration. As a result you may\ + \ experience degraded service beginning May 15th. Please [install the GitHub\ + \ App Integration](https://github.com/apps/codecov) for your organization. [Read\ + \ more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell).\\\ + n\\n[![Impacted file tree graph](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\\\ + n\\n```diff\\n@@ Coverage Diff @@\\n## \ + \ main #9 +/- ##\\n=============================================\\\ + n+ Coverage 50.00% 60.00% +10.00% \\n+ Complexity 11 \ + \ 10 -1 \\n=============================================\\n Files\ + \ 2 2 \\n Lines 6 10 \ + \ +4 \\n Branches 0 1 +1 \\n=============================================\\\ + n+ Hits 3 6 +3 \\n Misses 3 \ + \ 3 \\n- Partials 0 1 +1 \\n```\\\ + n\\n| [Flag](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\ + \ | Coverage \u0394 | Complexity \u0394 | |\\n|---|---|---|---|\\n| [integration](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\ + \ | `?` | `?` | |\\n| [unit](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\ + \ | `100.00% <\xF8> (?)` | `0.00 <\xF8> (?)` | |\\n\\nFlags with carried forward\ + \ coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell#carryforward-flags-in-the-pull-request-comment)\ + \ to find out more.\\n\\n[see 2 files with indirect coverage changes](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\\\ + n\\n------\\n\\n[Continue to review full report in Codecov by Sentry](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell).\\\ + n> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\\\ + n> `\u0394 = absolute (impact)`, `\xF8 = not affected`, `? = missing\ + \ data`\\n> Powered by [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell).\ + \ Last update [5b174c2...5601846](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell).\ + \ Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell).\\\ + n\",\"reactions\":{\"url\":\"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments/1699669247/reactions\"\ + ,\"total_count\":0,\"+1\":0,\"-1\":0,\"laugh\":0,\"hooray\":0,\"confused\":0,\"\ + heart\":0,\"rocket\":0,\"eyes\":0},\"performed_via_github_app\":null}" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '6356' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:43 GMT + ETag: + - '"a3267b6fdb96428f156fb276116e0b4c918da9a371be840fc5573f132fbe1dbe"' + Location: + - https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments/1699669247 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E72A:35CC:109BE2:21B617:64EF8E1B + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4984' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '16' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:36:05 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 201 +version: 1 diff --git a/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_gitlab.yaml b/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_gitlab.yaml new file mode 100644 index 0000000000..713498b998 --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_gitlab.yaml @@ -0,0 +1,379 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - gitlab.com + user-agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/47404140/repository/compare/?from=0fc784af11c401449e56b24a174bae7b9af86c98&to=0b6a213fc300cd328c0625f38f30432ee6e066e5 + response: + content: '{"commit":{"id":"0b6a213fc300cd328c0625f38f30432ee6e066e5","short_id":"0b6a213f","created_at":"2023-06-27T12:30:01.000-04:00","parent_ids":["0c11f75d3f08db4bdf9a9f077e9e513990cd7a18"],"title":"5","message":"5\n","author_name":"joseph-sentry","author_email":"joseph.sawaya@sentry.io","authored_date":"2023-06-27T11:14:35.000-04:00","committer_name":"joseph-sentry","committer_email":"joseph.sawaya@sentry.io","committed_date":"2023-06-27T12:30:01.000-04:00","trailers":{},"web_url":"https://gitlab.com/joseph-sentry/example-python/-/commit/0b6a213fc300cd328c0625f38f30432ee6e066e5"},"commits":[{"id":"0c11f75d3f08db4bdf9a9f077e9e513990cd7a18","short_id":"0c11f75d","created_at":"2023-06-27T12:30:00.000-04:00","parent_ids":["5ad8e6ce797491865e815667384f93c87b1c7245"],"title":"4","message":"4\n","author_name":"joseph-sentry","author_email":"joseph.sawaya@sentry.io","authored_date":"2023-06-27T11:13:52.000-04:00","committer_name":"joseph-sentry","committer_email":"joseph.sawaya@sentry.io","committed_date":"2023-06-27T12:30:00.000-04:00","trailers":{},"web_url":"https://gitlab.com/joseph-sentry/example-python/-/commit/0c11f75d3f08db4bdf9a9f077e9e513990cd7a18"},{"id":"0b6a213fc300cd328c0625f38f30432ee6e066e5","short_id":"0b6a213f","created_at":"2023-06-27T12:30:01.000-04:00","parent_ids":["0c11f75d3f08db4bdf9a9f077e9e513990cd7a18"],"title":"5","message":"5\n","author_name":"joseph-sentry","author_email":"joseph.sawaya@sentry.io","authored_date":"2023-06-27T11:14:35.000-04:00","committer_name":"joseph-sentry","committer_email":"joseph.sawaya@sentry.io","committed_date":"2023-06-27T12:30:01.000-04:00","trailers":{},"web_url":"https://gitlab.com/joseph-sentry/example-python/-/commit/0b6a213fc300cd328c0625f38f30432ee6e066e5"}],"diffs":[{"diff":"","new_path":"4.txt","old_path":"4.txt","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false},{"diff":"@@ + -24,7 +24,4 @@ def test_multiply():\n assert Calculator.multiply(-4, 2.0) + == -8.0\n \n def test_divide():\n- assert Calculator.divide(1, 2) == 0.5\n- assert + Calculator.divide(1.0, 2.0) == 0.5\n- assert Calculator.divide(0, 2.0) == + 0\n- assert Calculator.divide(-4, 2.0) == -2.0\n\\ No newline at end of file\n+ assert + Calculator.divide(1, 2) == 0.5\n\\ No newline at end of file\n","new_path":"api/calculator/test_calculator.py","old_path":"api/calculator/test_calculator.py","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false}],"compare_timeout":false,"compare_same_ref":false,"web_url":"https://gitlab.com/joseph-sentry/example-python/-/compare/5ad8e6ce797491865e815667384f93c87b1c7245...0b6a213fc300cd328c0625f38f30432ee6e066e5"}' + headers: + CF-Cache-Status: + - MISS + CF-RAY: + - 7e193b306ca136d8-YYZ + Cache-Control: + - max-age=0, private, must-revalidate + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json + Date: + - Tue, 04 Jul 2023 17:54:03 GMT + Etag: + - W/"bb9ffde5bcafb3a779105cac464ca1f3" + GitLab-LB: + - fe-09-lb-gprd + GitLab-SV: + - api-gke-us-east1-b + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Referrer-Policy: + - strict-origin-when-cross-origin + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=JC6qR2pHaLG0QU%2B32UiLFIzlAktwMU3phdIK9YHtb%2Fqwb9VQUDg5NhBlrT0w%2BNxE4bI3xYU5sLUX4A6p9xGLx%2FqhSUOIbv6k6QbBDZSXrzl00VpN79NU90RWuvJdan8yD%2BTkrjaFAc0%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - _cfuvid=3EG7MrO_vdP3KgZ_aLy9NpP3MSknAnAoW5OFYhLqsAQ-1688493243130-0-604800000; + path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000 + Transfer-Encoding: + - chunked + Vary: + - Origin, Accept-Encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Gitlab-Meta: + - '{"correlation_id":"6a5cbc836eac371c289f44dbc2313eb3","version":"1"}' + X-Request-Id: + - 6a5cbc836eac371c289f44dbc2313eb3 + X-Runtime: + - '0.090368' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - gitlab.com + user-agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/47404140/repository/branches + response: + content: '[{"name":"behind","commit":{"id":"0b6a213fc300cd328c0625f38f30432ee6e066e5","short_id":"0b6a213f","created_at":"2023-06-27T12:30:01.000-04:00","parent_ids":["0c11f75d3f08db4bdf9a9f077e9e513990cd7a18"],"title":"5","message":"5\n","author_name":"joseph-sentry","author_email":"joseph.sawaya@sentry.io","authored_date":"2023-06-27T11:14:35.000-04:00","committer_name":"joseph-sentry","committer_email":"joseph.sawaya@sentry.io","committed_date":"2023-06-27T12:30:01.000-04:00","trailers":{},"web_url":"https://gitlab.com/joseph-sentry/example-python/-/commit/0b6a213fc300cd328c0625f38f30432ee6e066e5"},"merged":false,"protected":false,"developers_can_push":false,"developers_can_merge":false,"can_push":true,"default":false,"web_url":"https://gitlab.com/joseph-sentry/example-python/-/tree/behind"},{"name":"main","commit":{"id":"0fc784af11c401449e56b24a174bae7b9af86c98","short_id":"0fc784af","created_at":"2023-06-27T11:13:34.000-04:00","parent_ids":["b64b9b4c25c6c088781ead5eae424f156ee98d8c"],"title":"3","message":"3\n","author_name":"joseph-sentry","author_email":"joseph.sawaya@sentry.io","authored_date":"2023-06-27T11:13:34.000-04:00","committer_name":"joseph-sentry","committer_email":"joseph.sawaya@sentry.io","committed_date":"2023-06-27T11:13:34.000-04:00","trailers":{},"web_url":"https://gitlab.com/joseph-sentry/example-python/-/commit/0fc784af11c401449e56b24a174bae7b9af86c98"},"merged":false,"protected":true,"developers_can_push":false,"developers_can_merge":false,"can_push":true,"default":true,"web_url":"https://gitlab.com/joseph-sentry/example-python/-/tree/main"},{"name":"step3","commit":{"id":"61089a89aa216c556a17f5648bd89ef17cb54f63","short_id":"61089a89","created_at":"2023-06-13T12:17:26.000-04:00","parent_ids":["50e2fabc371fa2c8ba66295862df53deec038ac8"],"title":"use + token?","message":"use token?\n","author_name":"joseph-sentry","author_email":"joseph.sawaya@sentry.io","authored_date":"2023-06-13T12:17:26.000-04:00","committer_name":"joseph-sentry","committer_email":"joseph.sawaya@sentry.io","committed_date":"2023-06-13T12:17:26.000-04:00","trailers":{},"web_url":"https://gitlab.com/joseph-sentry/example-python/-/commit/61089a89aa216c556a17f5648bd89ef17cb54f63"},"merged":false,"protected":false,"developers_can_push":false,"developers_can_merge":false,"can_push":true,"default":false,"web_url":"https://gitlab.com/joseph-sentry/example-python/-/tree/step3"}]' + headers: + CF-Cache-Status: + - MISS + CF-RAY: + - 7e193b32190236d5-YYZ + Cache-Control: + - max-age=0, private, must-revalidate + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json + Date: + - Tue, 04 Jul 2023 17:54:03 GMT + Etag: + - W/"ed688f50c71ff83cb17dadf6e336e69a" + GitLab-LB: + - fe-12-lb-gprd + GitLab-SV: + - api-gke-us-east1-b + Link: + - ; + rel="first", ; + rel="last" + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Referrer-Policy: + - strict-origin-when-cross-origin + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=TDqA%2Fh0ao1DGnX2YxEo3WW0wN13XAfEDbtjDd87%2BsNBpIUZULCm47m9y887VfLIIo1jDoPDoK3OQrs5d%2F2oSBAoU3%2F6JCkaJPlCssfBTfIoxt%2Ffr5RxsTf9TOJtqWSyu50Pi6fYBQ%2BU%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - _cfuvid=lQx4LdgHz2oZpgnFLpvobY2Y.1L_u3FR11V.jRGMEY0-1688493243431-0-604800000; + path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000 + Transfer-Encoding: + - chunked + Vary: + - Origin, Accept-Encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Gitlab-Meta: + - '{"correlation_id":"4353ac9cc0ef373e12b71a68f1612cb6","version":"1"}' + X-Next-Page: + - '' + X-Page: + - '1' + X-Per-Page: + - '20' + X-Prev-Page: + - '' + X-Request-Id: + - 4353ac9cc0ef373e12b71a68f1612cb6 + X-Runtime: + - '0.113054' + X-Total: + - '3' + X-Total-Pages: + - '1' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"body": "## [Codecov](https://app.codecov.io/gl/joseph-sentry/example-python/pull/1?src=pr&el=h1&utm_medium=referral&utm_source=gitlab&utm_content=comment&utm_campaign=pr+comments&utm_term=Gina+French) + Report\n> Merging [#1](https://app.codecov.io/gl/joseph-sentry/example-python/pull/1?src=pr&el=desc&utm_medium=referral&utm_source=gitlab&utm_content=comment&utm_campaign=pr+comments&utm_term=Gina+French) + (0b6a213) into [main](https://app.codecov.io/gl/joseph-sentry/example-python/commit/0fc784af11c401449e56b24a174bae7b9af86c98?el=desc&utm_medium=referral&utm_source=gitlab&utm_content=comment&utm_campaign=pr+comments&utm_term=Gina+French) + (0fc784a) will **increase** coverage by `10.00%`.\n> The diff coverage is `n/a`.\n\n[![Impacted + file tree graph](https://app.codecov.io/gl/joseph-sentry/example-python/pull/1/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij&utm_medium=referral&utm_source=gitlab&utm_content=comment&utm_campaign=pr+comments&utm_term=Gina+French)](https://app.codecov.io/gl/joseph-sentry/example-python/pull/1?src=pr&el=tree&utm_medium=referral&utm_source=gitlab&utm_content=comment&utm_campaign=pr+comments&utm_term=Gina+French)\n\n```diff\n@@ Coverage + Diff @@\n## main #1 +/- ##\n=============================================\n+ + Coverage 50.00% 60.00% +10.00% \n+ Complexity 11 10 -1 \n=============================================\n Files 2 2 \n Lines 6 10 +4 \n Branches 0 1 +1 \n=============================================\n+ + Hits 3 6 +3 \n Misses 3 3 \n- + Partials 0 1 +1 \n```\n\n| Flag | Coverage \u0394 + | Complexity \u0394 | |\n|---|---|---|---|\n| integration | `?` | `?` | |\n| + unit | `100.00% <\u00f8> (?)` | `0.00 <\u00f8> (?)` | |\n\nFlags with carried + forward coverage won''t be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=gitlab&utm_content=comment&utm_campaign=pr+comments&utm_term=Gina+French#carryforward-flags-in-the-pull-request-comment) + to find out more.\n\n[see 2 files with indirect coverage changes](https://app.codecov.io/gl/joseph-sentry/example-python/pull/1/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=gitlab&utm_content=comment&utm_campaign=pr+comments&utm_term=Gina+French)\n\n------\n\n[Continue + to review full report in Codecov by Sentry](https://app.codecov.io/gl/joseph-sentry/example-python/pull/1?src=pr&el=continue&utm_medium=referral&utm_source=gitlab&utm_content=comment&utm_campaign=pr+comments&utm_term=Gina+French).\n> + **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=gitlab&utm_content=comment&utm_campaign=pr+comments&utm_term=Gina+French)\n> + `\u0394 = absolute (impact)`, `\u00f8 = not affected`, `? = missing + data`\n> Powered by [Codecov](https://app.codecov.io/gl/joseph-sentry/example-python/pull/1?src=pr&el=footer&utm_medium=referral&utm_source=gitlab&utm_content=comment&utm_campaign=pr+comments&utm_term=Gina+French). + Last update [0fc784a...0b6a213](https://app.codecov.io/gl/joseph-sentry/example-python/pull/1?src=pr&el=lastupdated&utm_medium=referral&utm_source=gitlab&utm_content=comment&utm_campaign=pr+comments&utm_term=Gina+French). + Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=gitlab&utm_content=comment&utm_campaign=pr+comments&utm_term=Gina+French).\n"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '3644' + content-type: + - application/json + host: + - gitlab.com + user-agent: + - Default + method: POST + uri: https://gitlab.com/api/v4/projects/47404140/merge_requests/5/notes + response: + content: "{\"id\":1457135397,\"type\":null,\"body\":\"## [Codecov](https://app.codecov.io/gl/joseph-sentry/example-python/pull/1?src=pr\\\ + u0026el=h1\\u0026utm_medium=referral\\u0026utm_source=gitlab\\u0026utm_content=comment\\\ + u0026utm_campaign=pr+comments\\u0026utm_term=Gina+French) Report\\n\\u003e Merging\ + \ [#1](https://app.codecov.io/gl/joseph-sentry/example-python/pull/1?src=pr\\\ + u0026el=desc\\u0026utm_medium=referral\\u0026utm_source=gitlab\\u0026utm_content=comment\\\ + u0026utm_campaign=pr+comments\\u0026utm_term=Gina+French) (0b6a213) into [main](https://app.codecov.io/gl/joseph-sentry/example-python/commit/0fc784af11c401449e56b24a174bae7b9af86c98?el=desc\\\ + u0026utm_medium=referral\\u0026utm_source=gitlab\\u0026utm_content=comment\\\ + u0026utm_campaign=pr+comments\\u0026utm_term=Gina+French) (0fc784a) will **increase**\ + \ coverage by `10.00%`.\\n\\u003e The diff coverage is `n/a`.\\n\\n[![Impacted\ + \ file tree graph](https://app.codecov.io/gl/joseph-sentry/example-python/pull/1/graphs/tree.svg?width=650\\\ + u0026height=150\\u0026src=pr\\u0026token=abcdefghij\\u0026utm_medium=referral\\\ + u0026utm_source=gitlab\\u0026utm_content=comment\\u0026utm_campaign=pr+comments\\\ + u0026utm_term=Gina+French)](https://app.codecov.io/gl/joseph-sentry/example-python/pull/1?src=pr\\\ + u0026el=tree\\u0026utm_medium=referral\\u0026utm_source=gitlab\\u0026utm_content=comment\\\ + u0026utm_campaign=pr+comments\\u0026utm_term=Gina+French)\\n\\n```diff\\n@@\ + \ Coverage Diff @@\\n## main #1\ + \ +/- ##\\n=============================================\\n+ Coverage\ + \ 50.00% 60.00% +10.00% \\n+ Complexity 11 10 \ + \ -1 \\n=============================================\\n Files \ + \ 2 2 \\n Lines 6 10 +4\ + \ \\n Branches 0 1 +1 \\n=============================================\\\ + n+ Hits 3 6 +3 \\n Misses 3 \ + \ 3 \\n- Partials 0 1 +1 \\n```\\\ + n\\n| Flag | Coverage \u0394 | Complexity \u0394 | |\\n|---|---|---|---|\\n|\ + \ integration | `?` | `?` | |\\n| unit | `100.00% \\u003c\xF8\\u003e (?)` |\ + \ `0.00 \\u003c\xF8\\u003e (?)` | |\\n\\nFlags with carried forward coverage\ + \ won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral\\\ + u0026utm_source=gitlab\\u0026utm_content=comment\\u0026utm_campaign=pr+comments\\\ + u0026utm_term=Gina+French#carryforward-flags-in-the-pull-request-comment) to\ + \ find out more.\\n\\n[see 2 files with indirect coverage changes](https://app.codecov.io/gl/joseph-sentry/example-python/pull/1/indirect-changes?src=pr\\\ + u0026el=tree-more\\u0026utm_medium=referral\\u0026utm_source=gitlab\\u0026utm_content=comment\\\ + u0026utm_campaign=pr+comments\\u0026utm_term=Gina+French)\\n\\n------\\n\\n[Continue\ + \ to review full report in Codecov by Sentry](https://app.codecov.io/gl/joseph-sentry/example-python/pull/1?src=pr\\\ + u0026el=continue\\u0026utm_medium=referral\\u0026utm_source=gitlab\\u0026utm_content=comment\\\ + u0026utm_campaign=pr+comments\\u0026utm_term=Gina+French).\\n\\u003e **Legend**\ + \ - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral\\\ + u0026utm_source=gitlab\\u0026utm_content=comment\\u0026utm_campaign=pr+comments\\\ + u0026utm_term=Gina+French)\\n\\u003e `\u0394 = absolute \\u003crelative\\u003e\ + \ (impact)`, `\xF8 = not affected`, `? = missing data`\\n\\u003e Powered by\ + \ [Codecov](https://app.codecov.io/gl/joseph-sentry/example-python/pull/1?src=pr\\\ + u0026el=footer\\u0026utm_medium=referral\\u0026utm_source=gitlab\\u0026utm_content=comment\\\ + u0026utm_campaign=pr+comments\\u0026utm_term=Gina+French). Last update [0fc784a...0b6a213](https://app.codecov.io/gl/joseph-sentry/example-python/pull/1?src=pr\\\ + u0026el=lastupdated\\u0026utm_medium=referral\\u0026utm_source=gitlab\\u0026utm_content=comment\\\ + u0026utm_campaign=pr+comments\\u0026utm_term=Gina+French). Read the [comment\ + \ docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral\\\ + u0026utm_source=gitlab\\u0026utm_content=comment\\u0026utm_campaign=pr+comments\\\ + u0026utm_term=Gina+French).\",\"attachment\":null,\"author\":{\"id\":15014576,\"\ + username\":\"joseph-sentry\",\"name\":\"joseph-sentry\",\"state\":\"active\"\ + ,\"avatar_url\":\"https://secure.gravatar.com/avatar/b8db468edebbae1ffa228d3095c230c5?s=80\\\ + u0026d=identicon\",\"web_url\":\"https://gitlab.com/joseph-sentry\"},\"created_at\"\ + :\"2023-07-04T17:54:03.693Z\",\"updated_at\":\"2023-07-04T17:54:03.693Z\",\"\ + system\":false,\"noteable_id\":234535703,\"noteable_type\":\"MergeRequest\"\ + ,\"project_id\":47404140,\"resolvable\":false,\"confidential\":false,\"internal\"\ + :false,\"noteable_iid\":1,\"commands_changes\":{}}" + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 7e193b34483fa1d8-YYZ + Cache-Control: + - max-age=0, private, must-revalidate + Connection: + - keep-alive + Content-Length: + - '4563' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json + Date: + - Tue, 04 Jul 2023 17:54:03 GMT + Etag: + - W/"c385bbb79f9cd8c8ef5c8c20956b1d23" + GitLab-LB: + - fe-13-lb-gprd + GitLab-SV: + - gke-cny-api + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Referrer-Policy: + - strict-origin-when-cross-origin + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=ddX79d3f6rJGXEtRolagciIjbg9zjjbYpg65sv%2FAUNyXUtdEJIuthFhXPuYZVDi8byimBpfkor6ypIrXF07bWt331SxRnOdeKU6Imreqkfkite2Nx8%2FqRVRiMrH9r8vBFGe167NFSmc%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - _cfuvid=YKIxWaDzvazpjHge8Q9AjvELoaYcqF5jWIN.5ym_YyQ-1688493243878-0-604800000; + path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Gitlab-Meta: + - '{"correlation_id":"a6cddfff67cb7349491082b18e233049","version":"1"}' + X-Request-Id: + - a6cddfff67cb7349491082b18e233049 + X-Runtime: + - '0.228506' + http_version: HTTP/1.1 + status_code: 201 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - gitlab.com + user-agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/47404140/repository/branches/main + response: + content: '{"message":"401 Unauthorized"}' + headers: + CF-Cache-Status: + - MISS + CF-RAY: + - 7fef2fd0182436fe-YYZ + Cache-Control: + - no-cache + Connection: + - keep-alive + Content-Length: + - '30' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json + Date: + - Wed, 30 Aug 2023 18:44:44 GMT + GitLab-LB: + - fe-05-lb-gprd + GitLab-SV: + - localhost + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + RateLimit-Limit: + - '2000' + RateLimit-Observed: + - '2' + RateLimit-Remaining: + - '1998' + RateLimit-Reset: + - '1693421144' + RateLimit-ResetTime: + - Wed, 30 Aug 2023 18:45:44 GMT + Referrer-Policy: + - strict-origin-when-cross-origin + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=lBgieih4yF%2BSFqCVeTidu8Y1w18hb7BV63ktECkxzQPrULx2APCFXvtqQDV6ZmRnfTxppDY%2F15MRyjs064ja6G4RylNKxJlsQ8C7GUpx67j3H%2BfnNW7ugAHn574%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - _cfuvid=0RVizv.CnCUUI1sQDhOigxGHiJY1v6BbpkmTQn7RUNM-1693421084336-0-604800000; + path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Origin, Accept-Encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Gitlab-Meta: + - '{"correlation_id":"0a55ef3f31f3613df4edf90f82a78c20","version":"1"}' + X-Request-Id: + - 0a55ef3f31f3613df4edf90f82a78c20 + X-Runtime: + - '0.034387' + http_version: HTTP/1.1 + status_code: 401 +version: 1 diff --git a/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_new_layout.yaml b/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_new_layout.yaml new file mode 100644 index 0000000000..f3fc010da8 --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_new_layout.yaml @@ -0,0 +1,821 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/compare/4535be18e90467d6d9a99c0ce651becec7f7eba6...2e2600aa09525e2e1e1d98b09de61454d29c94bb + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/4535be18e90467d6d9a99c0ce651becec7f7eba6...2e2600aa09525e2e1e1d98b09de61454d29c94bb","html_url":"https://github.com/ThiagoCodecov/example-python/compare/4535be18e90467d6d9a99c0ce651becec7f7eba6...2e2600aa09525e2e1e1d98b09de61454d29c94bb","permalink_url":"https://github.com/ThiagoCodecov/example-python/compare/ThiagoCodecov:4535be1...ThiagoCodecov:2e2600a","diff_url":"https://github.com/ThiagoCodecov/example-python/compare/4535be18e90467d6d9a99c0ce651becec7f7eba6...2e2600aa09525e2e1e1d98b09de61454d29c94bb.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/compare/4535be18e90467d6d9a99c0ce651becec7f7eba6...2e2600aa09525e2e1e1d98b09de61454d29c94bb.patch","base_commit":{"sha":"4535be18e90467d6d9a99c0ce651becec7f7eba6","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjQ1MzViZTE4ZTkwNDY3ZDZkOWE5OWMwY2U2NTFiZWNlYzdmN2ViYTY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-31T03:23:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-31T03:23:55Z"},"message":"20EE4D09-F49E-474B-B7AF-4E4916EF82FA","tree":{"sha":"7da71bf52d31d24403d4208835b5d1333ba54695","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7da71bf52d31d24403d4208835b5d1333ba54695"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/4535be18e90467d6d9a99c0ce651becec7f7eba6","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"66567ec66e07440e3170910df61cf458812323fc","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/66567ec66e07440e3170910df61cf458812323fc","html_url":"https://github.com/ThiagoCodecov/example-python/commit/66567ec66e07440e3170910df61cf458812323fc"}]},"merge_base_commit":{"sha":"30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjMwY2MxZWQ3NTFhNTlmYTllN2FkOGU3OWZmZjQxYTZmZTExZWY1ZGQ=","commit":{"author":{"name":"Thiago","email":"44376991+ThiagoCodecov@users.noreply.github.com","date":"2019-12-09T12:02:11Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2019-12-09T12:02:11Z"},"message":"Update + README.md","tree":{"sha":"c901b3cb75fd85aebf03f254494d5ad06b86777e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c901b3cb75fd85aebf03f254494d5ad06b86777e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJd7jfDCRBK7hj4Ov3rIwAAdHIIAF8TrpEbraVNLuNObBiQefch\nVHGpc6MNxOnnRgGPZZl/egrosAxCJ2og+ArrjQG+D/7eYUhWeb73YPR8lWvVf6YN\nIkNSwv1o/2bmH3pma9DR4wDXnKeO/w/UPhNZaGC1w3dcvsHSJReSRkl/ipLxtPep\n3XpmC8RdIwFNU1hG8Dd5iKqJz3d3iA3mdkb3vrvYmKq2imQigSbXpOvL1o/Du9DN\nxE29+ZeGqTZf0Z5QHgJT7THw8P1bf/UJUado7nn6cIon/slDbP/ZirN3rgEJWXU6\neH9/bdS+HMcCwK5TZwJBXgRkbtzRIgc7mj5cTWOSX8XFuUnOS/QCyfkAIvajseU=\n=3PXD\n-----END + PGP SIGNATURE-----\n","payload":"tree c901b3cb75fd85aebf03f254494d5ad06b86777e\nparent + 2dfb36626ca60a8aa6d2bc624ad0f54b473bb436\nauthor Thiago <44376991+ThiagoCodecov@users.noreply.github.com> + 1575892931 -0300\ncommitter GitHub 1575892931 -0300\n\nUpdate + README.md"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"2dfb36626ca60a8aa6d2bc624ad0f54b473bb436","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2dfb36626ca60a8aa6d2bc624ad0f54b473bb436","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2dfb36626ca60a8aa6d2bc624ad0f54b473bb436"}]},"status":"diverged","ahead_by":3,"behind_by":11,"total_commits":3,"commits":[{"sha":"e022f588e40269f8ab1c568080dc4a7662ff2335","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmUwMjJmNTg4ZTQwMjY5ZjhhYjFjNTY4MDgwZGM0YTc2NjJmZjIzMzU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:44Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:44Z"},"message":"69E76A1D-FD86-4A90-A6CE-6A66BEEE75CA","tree":{"sha":"6188b6c086ebf6c63626abca649d2596b145c84c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6188b6c086ebf6c63626abca649d2596b145c84c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e022f588e40269f8ab1c568080dc4a7662ff2335","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd"}]},{"sha":"c2a05aa15ecad5bec37e29b9fe51ef30120f8642","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmMyYTA1YWExNWVjYWQ1YmVjMzdlMjliOWZlNTFlZjMwMTIwZjg2NDI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:47Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:47Z"},"message":"81905186-E567-4E4E-A5CB-3E5BFB820E57","tree":{"sha":"dd944d7739d5d429f264709345fe481c1e602be8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/dd944d7739d5d429f264709345fe481c1e602be8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e022f588e40269f8ab1c568080dc4a7662ff2335","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e022f588e40269f8ab1c568080dc4a7662ff2335"}]},{"sha":"2e2600aa09525e2e1e1d98b09de61454d29c94bb","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJlMjYwMGFhMDk1MjVlMmUxZTFkOThiMDlkZTYxNDU0ZDI5Yzk0YmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:49Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:49Z"},"message":"AE0A9904-3B05-405A-B8F7-A5DD3B38C92D","tree":{"sha":"f64d89541a0899881da6764081991e70ed43bfea","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f64d89541a0899881da6764081991e70ed43bfea"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2e2600aa09525e2e1e1d98b09de61454d29c94bb","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"c2a05aa15ecad5bec37e29b9fe51ef30120f8642","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c2a05aa15ecad5bec37e29b9fe51ef30120f8642"}]}],"files":[{"sha":"657d4c0ca181f2df83261d3ab1922d6ec47ece1e","filename":"README.md","status":"modified","additions":8,"deletions":0,"changes":8,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/2e2600aa09525e2e1e1d98b09de61454d29c94bb/README.md","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/2e2600aa09525e2e1e1d98b09de61454d29c94bb/README.md","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.md?ref=2e2600aa09525e2e1e1d98b09de61454d29c94bb","patch":"@@ + -1 +1,9 @@\n Now this\n+\n+\n+\n+80EC0808-DD33-42DF-8873-B2A605A82432\n+63FE7DEF-8367-47BD-B864-D86DE9F94358\n+69E76A1D-FD86-4A90-A6CE-6A66BEEE75CA\n+81905186-E567-4E4E-A5CB-3E5BFB820E57\n+AE0A9904-3B05-405A-B8F7-A5DD3B38C92D"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 16 Mar 2022 17:44:08 GMT + ETag: + - W/"08ab477429ee039b54be11e41e8f9cd8f35e3d9d10b48fce439ddc60be77c167" + Last-Modified: + - Tue, 31 Dec 2019 03:23:55 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C646:267B:2392731:243022B:623221E7 + X-OAuth-Scopes: + - public_repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1647456248' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-03-17 03:00:00 UTC + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"body": "## [Codecov](https://app.codecov.io/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper) + Report\nPatch coverage has no change and project coverage change: **`+10.00%`** + :tada:\n> Comparison is base [(`4535be1`)](https://app.codecov.io/gh/ThiagoCodecov/example-python/commit/4535be18e90467d6d9a99c0ce651becec7f7eba6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper) + 50.00% compared to head [(`2e2600a`)](https://app.codecov.io/gh/ThiagoCodecov/example-python/commit/2e2600aa09525e2e1e1d98b09de61454d29c94bb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper) + 60.00%.\n\n:exclamation: Your organization is not using the GitHub App Integration. + As a result you may experience degraded service beginning May 15th. Please [install + the GitHub App Integration](https://github.com/apps/codecov) for your organization. + [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper).\n\n
Additional + details and impacted files\n\n\n[![Impacted file tree graph](https://app.codecov.io/gh/ThiagoCodecov/example-python/pull/15/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper)](https://app.codecov.io/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper)\n\n```diff\n@@ Coverage + Diff @@\n## master #15 +/- ##\n=============================================\n+ + Coverage 50.00% 60.00% +10.00% \n+ Complexity 11 10 -1 \n=============================================\n Files 2 2 \n Lines 6 10 +4 \n Branches 0 1 +1 \n=============================================\n+ + Hits 3 6 +3 \n Misses 3 3 \n- + Partials 0 1 +1 \n```\n\n| Flag | Coverage \u0394 + | Complexity \u0394 | |\n|---|---|---|---|\n| integration | `?` | `?` | |\n| + unit | `100.00% <\u00f8> (?)` | `0.00 <\u00f8> (?)` | |\n\nFlags with carried + forward coverage won''t be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper#carryforward-flags-in-the-pull-request-comment) + to find out more.\n\n[see 2 files with indirect coverage changes](https://app.codecov.io/gh/ThiagoCodecov/example-python/pull/15/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper)\n\n
\n\n[:umbrella: + View full report in Codecov by Sentry](https://app.codecov.io/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper). \n:loudspeaker: + Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper).\n"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '3553' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/issues/15/comments + response: + content: "{\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments/1620423805\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/pull/15#issuecomment-1620423805\"\ + ,\"issue_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/issues/15\"\ + ,\"id\":1620423805,\"node_id\":\"IC_kwDOCVXMMc5glbR9\",\"user\":{\"login\":\"\ + joseph-sentry\",\"id\":136376984,\"node_id\":\"U_kgDOCCDymA\",\"avatar_url\"\ + :\"https://avatars.githubusercontent.com/u/136376984?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/joseph-sentry\",\"html_url\":\"https://github.com/joseph-sentry\"\ + ,\"followers_url\":\"https://api.github.com/users/joseph-sentry/followers\"\ + ,\"following_url\":\"https://api.github.com/users/joseph-sentry/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/joseph-sentry/gists{/gist_id}\"\ + ,\"starred_url\":\"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/joseph-sentry/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/joseph-sentry/orgs\",\"\ + repos_url\":\"https://api.github.com/users/joseph-sentry/repos\",\"events_url\"\ + :\"https://api.github.com/users/joseph-sentry/events{/privacy}\",\"received_events_url\"\ + :\"https://api.github.com/users/joseph-sentry/received_events\",\"type\":\"\ + User\",\"site_admin\":false},\"created_at\":\"2023-07-04T15:10:22Z\",\"updated_at\"\ + :\"2023-07-04T15:10:22Z\",\"author_association\":\"NONE\",\"body\":\"## [Codecov](https://app.codecov.io/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper)\ + \ Report\\nPatch coverage has no change and project coverage change: **`+10.00%`**\ + \ :tada:\\n> Comparison is base [(`4535be1`)](https://app.codecov.io/gh/ThiagoCodecov/example-python/commit/4535be18e90467d6d9a99c0ce651becec7f7eba6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper)\ + \ 50.00% compared to head [(`2e2600a`)](https://app.codecov.io/gh/ThiagoCodecov/example-python/commit/2e2600aa09525e2e1e1d98b09de61454d29c94bb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper)\ + \ 60.00%.\\n\\n:exclamation: Your organization is not using the GitHub App Integration.\ + \ As a result you may experience degraded service beginning May 15th. Please\ + \ [install the GitHub App Integration](https://github.com/apps/codecov) for\ + \ your organization. [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper).\\\ + n\\n
Additional details and impacted files\\n\\n\\\ + n[![Impacted file tree graph](https://app.codecov.io/gh/ThiagoCodecov/example-python/pull/15/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper)](https://app.codecov.io/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper)\\\ + n\\n```diff\\n@@ Coverage Diff @@\\n## \ + \ master #15 +/- ##\\n=============================================\\\ + n+ Coverage 50.00% 60.00% +10.00% \\n+ Complexity 11 \ + \ 10 -1 \\n=============================================\\n Files\ + \ 2 2 \\n Lines 6 10 \ + \ +4 \\n Branches 0 1 +1 \\n=============================================\\\ + n+ Hits 3 6 +3 \\n Misses 3 \ + \ 3 \\n- Partials 0 1 +1 \\n```\\\ + n\\n| Flag | Coverage \u0394 | Complexity \u0394 | |\\n|---|---|---|---|\\n|\ + \ integration | `?` | `?` | |\\n| unit | `100.00% <\xF8> (?)` | `0.00 <\xF8\ + > (?)` | |\\n\\nFlags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper#carryforward-flags-in-the-pull-request-comment)\ + \ to find out more.\\n\\n[see 2 files with indirect coverage changes](https://app.codecov.io/gh/ThiagoCodecov/example-python/pull/15/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper)\\\ + n\\n
\\n\\n[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper).\ + \ \\n:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vernon+Cooper).\\\ + n\",\"reactions\":{\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments/1620423805/reactions\"\ + ,\"total_count\":0,\"+1\":0,\"-1\":0,\"laugh\":0,\"hooray\":0,\"confused\":0,\"\ + heart\":0,\"rocket\":0,\"eyes\":0},\"performed_via_github_app\":null}" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '5164' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 04 Jul 2023 15:10:23 GMT + ETag: + - '"d914e81787c93a02fb56a26985bd567b55e4e586cac7b137e25c96d35143464c"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments/1620423805 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D49F:7F4F:123714A:24DC770:64A4365E + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4992' + X-RateLimit-Reset: + - '1688485927' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '8' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-07-11 14:51:11 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 201 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/branches?per_page=100&page=1 + response: + content: '[{"name":"master","commit":{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654"},"protected":false},{"name":"random-branch","commit":{"sha":"b12b8ba6046c1738c5c89325b8a63a6f51bc4ff6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b12b8ba6046c1738c5c89325b8a63a6f51bc4ff6"},"protected":false},{"name":"thiago/base-no-base","commit":{"sha":"620b8042c2f846fcc6dda1c732dedd15cdbe76db","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/620b8042c2f846fcc6dda1c732dedd15cdbe76db"},"protected":false},{"name":"thiago/f/big-pt","commit":{"sha":"d55dc4ef748fd11537e50c9abed4ab1864fa1d94","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d55dc4ef748fd11537e50c9abed4ab1864fa1d94"},"protected":false},{"name":"thiago/f/cool-branch","commit":{"sha":"89f3d20f2be3fb2d098e544814f8ce3636dc78c4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/89f3d20f2be3fb2d098e544814f8ce3636dc78c4"},"protected":false},{"name":"thiago/f/something","commit":{"sha":"c9fb9262268f11b53c6b0682d4f9acac89bdeee5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c9fb9262268f11b53c6b0682d4f9acac89bdeee5"},"protected":false},{"name":"thiago/test-1","commit":{"sha":"2e2600aa09525e2e1e1d98b09de61454d29c94bb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb"},"protected":false}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 03 Jul 2023 04:34:08 GMT + ETag: + - W/"dab6303e47e5d12f9bb596bcb1e3fd8a7e482a11e1e543c0b1e05e279ea55fe3" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C4D7:5028:3DC84CA:7E41E48:64A24FC0 + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4995' + X-RateLimit-Reset: + - '1688361672' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '5' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-08-02 04:17:36 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...4535be18e90467d6d9a99c0ce651becec7f7eba6 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...4535be18e90467d6d9a99c0ce651becec7f7eba6","html_url":"https://github.com/ThiagoCodecov/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...4535be18e90467d6d9a99c0ce651becec7f7eba6","permalink_url":"https://github.com/ThiagoCodecov/example-python/compare/ThiagoCodecov:93189ce...ThiagoCodecov:4535be1","diff_url":"https://github.com/ThiagoCodecov/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...4535be18e90467d6d9a99c0ce651becec7f7eba6.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...4535be18e90467d6d9a99c0ce651becec7f7eba6.patch","base_commit":{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjkzMTg5Y2U1MGYyMjQyOTZkNjQxMmUyODg0YjkzZGNjM2M3Yzg2NTQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"message":"Adding + some encoding","tree":{"sha":"966e4dc888b0828c5dea75f21e82eb2daa9a4485","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/966e4dc888b0828c5dea75f21e82eb2daa9a4485"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","html_url":"https://github.com/ThiagoCodecov/example-python/commit/93189ce50f224296d6412e2884b93dcc3c7c8654","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67"}]},"merge_base_commit":{"sha":"4535be18e90467d6d9a99c0ce651becec7f7eba6","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjQ1MzViZTE4ZTkwNDY3ZDZkOWE5OWMwY2U2NTFiZWNlYzdmN2ViYTY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-31T03:23:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-31T03:23:55Z"},"message":"20EE4D09-F49E-474B-B7AF-4E4916EF82FA","tree":{"sha":"7da71bf52d31d24403d4208835b5d1333ba54695","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7da71bf52d31d24403d4208835b5d1333ba54695"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/4535be18e90467d6d9a99c0ce651becec7f7eba6","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"66567ec66e07440e3170910df61cf458812323fc","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/66567ec66e07440e3170910df61cf458812323fc","html_url":"https://github.com/ThiagoCodecov/example-python/commit/66567ec66e07440e3170910df61cf458812323fc"}]},"status":"behind","ahead_by":0,"behind_by":52,"total_commits":0,"commits":[],"files":[]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 03 Jul 2023 04:34:09 GMT + ETag: + - W/"c625aec9cfb15b0fc585ff2dbcdaeef7a4e39d1881112104a691b9fb0f748c49" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C4D8:2BE9:3486A51:6BBC360:64A24FC0 + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4994' + X-RateLimit-Reset: + - '1688361672' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '6' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-08-02 04:17:36 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d + response: + content: '{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d","html_url":"https://github.com/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d","permalink_url":"https://github.com/joseph-sentry/codecov-demo/compare/joseph-sentry:5b174c2...joseph-sentry:5601846","diff_url":"https://github.com/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d.diff","patch_url":"https://github.com/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d.patch","base_commit":{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","node_id":"C_kwDOJu7MkNoAKDViMTc0YzJiNDBkNTAxYTcwYzQ3OWU5MTAyNWQ1MTA5YjFhZDVjMWI","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"message":"make + change on main","tree":{"sha":"a9d6c66e50012a6d42f3f4c4e6d6de845230e73b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/a9d6c66e50012a6d42f3f4c4e6d6de845230e73b"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQCSXJE75ylD7PwNrXf6ITnsvh9gIP5b/JASjGlKqB7f37XPt9wLvT2YaV3HcSyTRyt\nCY9zBOZT+tELiyZcpqKwY=\n-----END + SSH SIGNATURE-----","payload":"tree a9d6c66e50012a6d42f3f4c4e6d6de845230e73b\nparent + 3fe10c352faae434e0cca7c1c5984c1c98000288\nauthor joseph-sentry + 1692026348 -0400\ncommitter joseph-sentry 1692026348 + -0400\n\nmake change on main\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"3fe10c352faae434e0cca7c1c5984c1c98000288","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/3fe10c352faae434e0cca7c1c5984c1c98000288","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/3fe10c352faae434e0cca7c1c5984c1c98000288"}]},"merge_base_commit":{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","node_id":"C_kwDOJu7MkNoAKDViMTc0YzJiNDBkNTAxYTcwYzQ3OWU5MTAyNWQ1MTA5YjFhZDVjMWI","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"message":"make + change on main","tree":{"sha":"a9d6c66e50012a6d42f3f4c4e6d6de845230e73b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/a9d6c66e50012a6d42f3f4c4e6d6de845230e73b"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQCSXJE75ylD7PwNrXf6ITnsvh9gIP5b/JASjGlKqB7f37XPt9wLvT2YaV3HcSyTRyt\nCY9zBOZT+tELiyZcpqKwY=\n-----END + SSH SIGNATURE-----","payload":"tree a9d6c66e50012a6d42f3f4c4e6d6de845230e73b\nparent + 3fe10c352faae434e0cca7c1c5984c1c98000288\nauthor joseph-sentry + 1692026348 -0400\ncommitter joseph-sentry 1692026348 + -0400\n\nmake change on main\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"3fe10c352faae434e0cca7c1c5984c1c98000288","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/3fe10c352faae434e0cca7c1c5984c1c98000288","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/3fe10c352faae434e0cca7c1c5984c1c98000288"}]},"status":"ahead","ahead_by":1,"behind_by":0,"total_commits":1,"commits":[{"sha":"5601846871b8142ab0df1e0b8774756c658bcc7d","node_id":"C_kwDOJu7MkNoAKDU2MDE4NDY4NzFiODE0MmFiMGRmMWUwYjg3NzQ3NTZjNjU4YmNjN2Q","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:16:17Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:23:50Z"},"message":"make + change","tree":{"sha":"3ca6632aeb641e579bea4391294d9ebe55c062c7","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/3ca6632aeb641e579bea4391294d9ebe55c062c7"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5601846871b8142ab0df1e0b8774756c658bcc7d","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQAmeUxSTgmd3FlcaYtRmVpuCh0n6L0/2Oy7wnsoGF6zjLoBO+aDz22vOG+bywNp4bc\nlmOl+Tu5Jk9woOhH2Olg8=\n-----END + SSH SIGNATURE-----","payload":"tree 3ca6632aeb641e579bea4391294d9ebe55c062c7\nparent + 5b174c2b40d501a70c479e91025d5109b1ad5c1b\nauthor joseph-sentry + 1692026177 -0400\ncommitter joseph-sentry 1692026630 + -0400\n\nmake change\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5601846871b8142ab0df1e0b8774756c658bcc7d","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b"}]}],"files":[{"sha":"8664ca881cd0536ab32eac8dfee923a2b304167d","filename":"api/calculator/calculator.py","status":"modified","additions":1,"deletions":3,"changes":4,"blob_url":"https://github.com/joseph-sentry/codecov-demo/blob/5601846871b8142ab0df1e0b8774756c658bcc7d/api%2Fcalculator%2Fcalculator.py","raw_url":"https://github.com/joseph-sentry/codecov-demo/raw/5601846871b8142ab0df1e0b8774756c658bcc7d/api%2Fcalculator%2Fcalculator.py","contents_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/contents/api%2Fcalculator%2Fcalculator.py?ref=5601846871b8142ab0df1e0b8774756c658bcc7d","patch":"@@ + -9,6 +9,4 @@ def multiply(x, y):\n return x * y\n \n def divide(x, + y):\n- if y == 0:\n- return ''Cannot divide by 0''\n- return + x * 1.0 / y\n\\ No newline at end of file\n+ return x * 1.0 / y"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:44 GMT + ETag: + - W/"3bcbc91ba2aac4e70b8b74075bc06a0b7436e42a61c15cd11f8c28af48d74080" + Last-Modified: + - Mon, 14 Aug 2023 15:23:50 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E72C:7693:10E3C0:22520F:64EF8E1C + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4983' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '17' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:36:05 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/branches/main + response: + content: '{"name":"main","commit":{"sha":"38c2d0214f2a48c9212a140f5311977059a15b35","node_id":"C_kwDOJu7MkNoAKDM4YzJkMDIxNGYyYTQ4YzkyMTJhMTQwZjUzMTE5NzcwNTlhMTViMzU","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"message":"make + change to app\n\nSigned-off-by: joseph-sentry ","tree":{"sha":"9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/38c2d0214f2a48c9212a140f5311977059a15b35","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQJe3OYXkY7fA1+RZ130Zrab06lH36l2XC9HMqjHDAW8iCW5VUbvNhOxNpKSokDItdd\nroqDRnazyFj3+oWVCvxgM=\n-----END + SSH SIGNATURE-----","payload":"tree 9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043\nparent + 1713006782821e36912c23baa98901c7361ee83c\nauthor joseph-sentry + 1692377290 -0400\ncommitter joseph-sentry 1692377290 + -0400\n\nmake change to app\n\nSigned-off-by: joseph-sentry \n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/38c2d0214f2a48c9212a140f5311977059a15b35","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"1713006782821e36912c23baa98901c7361ee83c","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/1713006782821e36912c23baa98901c7361ee83c","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/1713006782821e36912c23baa98901c7361ee83c"}]},"_links":{"self":"https://api.github.com/repos/joseph-sentry/codecov-demo/branches/main","html":"https://github.com/joseph-sentry/codecov-demo/tree/main"},"protected":false,"protection":{"enabled":false,"required_status_checks":{"enforcement_level":"off","contexts":[],"checks":[]}},"protection_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/branches/main/protection"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:44 GMT + ETag: + - W/"28690f5aa55bcb187fc062a1ab6ad5901a1ccbe0225d6f96dacf712da8e22306" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E72D:052A:11B090:23E126:64EF8E1C + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4982' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '18' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:36:05 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b + response: + content: '{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b","permalink_url":"https://github.com/joseph-sentry/codecov-demo/compare/joseph-sentry:38c2d02...joseph-sentry:5b174c2","diff_url":"https://github.com/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b.diff","patch_url":"https://github.com/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b.patch","base_commit":{"sha":"38c2d0214f2a48c9212a140f5311977059a15b35","node_id":"C_kwDOJu7MkNoAKDM4YzJkMDIxNGYyYTQ4YzkyMTJhMTQwZjUzMTE5NzcwNTlhMTViMzU","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"message":"make + change to app\n\nSigned-off-by: joseph-sentry ","tree":{"sha":"9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/38c2d0214f2a48c9212a140f5311977059a15b35","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQJe3OYXkY7fA1+RZ130Zrab06lH36l2XC9HMqjHDAW8iCW5VUbvNhOxNpKSokDItdd\nroqDRnazyFj3+oWVCvxgM=\n-----END + SSH SIGNATURE-----","payload":"tree 9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043\nparent + 1713006782821e36912c23baa98901c7361ee83c\nauthor joseph-sentry + 1692377290 -0400\ncommitter joseph-sentry 1692377290 + -0400\n\nmake change to app\n\nSigned-off-by: joseph-sentry \n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/38c2d0214f2a48c9212a140f5311977059a15b35","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"1713006782821e36912c23baa98901c7361ee83c","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/1713006782821e36912c23baa98901c7361ee83c","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/1713006782821e36912c23baa98901c7361ee83c"}]},"merge_base_commit":{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","node_id":"C_kwDOJu7MkNoAKDViMTc0YzJiNDBkNTAxYTcwYzQ3OWU5MTAyNWQ1MTA5YjFhZDVjMWI","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"message":"make + change on main","tree":{"sha":"a9d6c66e50012a6d42f3f4c4e6d6de845230e73b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/a9d6c66e50012a6d42f3f4c4e6d6de845230e73b"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQCSXJE75ylD7PwNrXf6ITnsvh9gIP5b/JASjGlKqB7f37XPt9wLvT2YaV3HcSyTRyt\nCY9zBOZT+tELiyZcpqKwY=\n-----END + SSH SIGNATURE-----","payload":"tree a9d6c66e50012a6d42f3f4c4e6d6de845230e73b\nparent + 3fe10c352faae434e0cca7c1c5984c1c98000288\nauthor joseph-sentry + 1692026348 -0400\ncommitter joseph-sentry 1692026348 + -0400\n\nmake change on main\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"3fe10c352faae434e0cca7c1c5984c1c98000288","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/3fe10c352faae434e0cca7c1c5984c1c98000288","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/3fe10c352faae434e0cca7c1c5984c1c98000288"}]},"status":"behind","ahead_by":0,"behind_by":2,"total_commits":0,"commits":[],"files":[]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:45 GMT + ETag: + - W/"fb71dcba3dddf292e08598b556f7bcc763019abc6e1678ed3355d3d8ec57454f" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E72E:16C4:FF10B:20679C:64EF8E1C + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4981' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '19' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:36:05 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"body": "## [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD) + Report\nPatch coverage has no change and project coverage change: **`+10.00%`** + :tada:\n> Comparison is base [(`5b174c2`)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD) + 50.00% compared to head [(`5601846`)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5601846871b8142ab0df1e0b8774756c658bcc7d?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD) + 60.00%.\n> Report is 2 commits behind head on main.\n\n:exclamation: Your organization + is not using the GitHub App Integration. As a result you may experience degraded + service beginning May 15th. Please [install the GitHub App Integration](https://github.com/apps/codecov) + for your organization. [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD).\n\n
Additional + details and impacted files\n\n\n[![Impacted file tree graph](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD)\n\n```diff\n@@ Coverage + Diff @@\n## main #9 +/- ##\n=============================================\n+ + Coverage 50.00% 60.00% +10.00% \n+ Complexity 11 10 -1 \n=============================================\n Files 2 2 \n Lines 6 10 +4 \n Branches 0 1 +1 \n=============================================\n+ + Hits 3 6 +3 \n Misses 3 3 \n- + Partials 0 1 +1 \n```\n\n| [Flag](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD) + | Coverage \u0394 | Complexity \u0394 | |\n|---|---|---|---|\n| [integration](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD) + | `?` | `?` | |\n| [unit](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD) + | `100.00% <\u00f8> (?)` | `0.00 <\u00f8> (?)` | |\n\nFlags with carried forward + coverage won''t be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD#carryforward-flags-in-the-pull-request-comment) + to find out more.\n\n[see 2 files with indirect coverage changes](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD)\n\n
\n\n[:umbrella: + View full report in Codecov by Sentry](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD). \n:loudspeaker: + Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD).\n"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '4148' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/issues/9/comments + response: + content: "{\"url\":\"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments/1699669290\"\ + ,\"html_url\":\"https://github.com/joseph-sentry/codecov-demo/pull/9#issuecomment-1699669290\"\ + ,\"issue_url\":\"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/9\"\ + ,\"id\":1699669290,\"node_id\":\"IC_kwDOJu7MkM5lTuUq\",\"user\":{\"login\":\"\ + joseph-sentry\",\"id\":136376984,\"node_id\":\"U_kgDOCCDymA\",\"avatar_url\"\ + :\"https://avatars.githubusercontent.com/u/136376984?u=8154f2067e6b4966acba9c27358d6e49cfbbf45d&v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/joseph-sentry\"\ + ,\"html_url\":\"https://github.com/joseph-sentry\",\"followers_url\":\"https://api.github.com/users/joseph-sentry/followers\"\ + ,\"following_url\":\"https://api.github.com/users/joseph-sentry/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/joseph-sentry/gists{/gist_id}\"\ + ,\"starred_url\":\"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/joseph-sentry/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/joseph-sentry/orgs\",\"\ + repos_url\":\"https://api.github.com/users/joseph-sentry/repos\",\"events_url\"\ + :\"https://api.github.com/users/joseph-sentry/events{/privacy}\",\"received_events_url\"\ + :\"https://api.github.com/users/joseph-sentry/received_events\",\"type\":\"\ + User\",\"site_admin\":false},\"created_at\":\"2023-08-30T18:44:45Z\",\"updated_at\"\ + :\"2023-08-30T18:44:45Z\",\"author_association\":\"OWNER\",\"body\":\"## [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD)\ + \ Report\\nPatch coverage has no change and project coverage change: **`+10.00%`**\ + \ :tada:\\n> Comparison is base [(`5b174c2`)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD)\ + \ 50.00% compared to head [(`5601846`)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5601846871b8142ab0df1e0b8774756c658bcc7d?&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD)\ + \ 60.00%.\\n> Report is 2 commits behind head on main.\\n\\n:exclamation: Your\ + \ organization is not using the GitHub App Integration. As a result you may\ + \ experience degraded service beginning May 15th. Please [install the GitHub\ + \ App Integration](https://github.com/apps/codecov) for your organization. [Read\ + \ more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD).\\\ + n\\n
Additional details and impacted files\\n\\n\\\ + n[![Impacted file tree graph](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD)\\\ + n\\n```diff\\n@@ Coverage Diff @@\\n## \ + \ main #9 +/- ##\\n=============================================\\\ + n+ Coverage 50.00% 60.00% +10.00% \\n+ Complexity 11 \ + \ 10 -1 \\n=============================================\\n Files\ + \ 2 2 \\n Lines 6 10 \ + \ +4 \\n Branches 0 1 +1 \\n=============================================\\\ + n+ Hits 3 6 +3 \\n Misses 3 \ + \ 3 \\n- Partials 0 1 +1 \\n```\\\ + n\\n| [Flag](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD)\ + \ | Coverage \u0394 | Complexity \u0394 | |\\n|---|---|---|---|\\n| [integration](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD)\ + \ | `?` | `?` | |\\n| [unit](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD)\ + \ | `100.00% <\xF8> (?)` | `0.00 <\xF8> (?)` | |\\n\\nFlags with carried forward\ + \ coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD#carryforward-flags-in-the-pull-request-comment)\ + \ to find out more.\\n\\n[see 2 files with indirect coverage changes](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD)\\\ + n\\n
\\n\\n[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD).\ + \ \\n:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tara+Grant+MD).\\\ + n\",\"reactions\":{\"url\":\"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments/1699669290/reactions\"\ + ,\"total_count\":0,\"+1\":0,\"-1\":0,\"laugh\":0,\"hooray\":0,\"confused\":0,\"\ + heart\":0,\"rocket\":0,\"eyes\":0},\"performed_via_github_app\":null}" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '5793' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:45 GMT + ETag: + - '"69ff846c9b55de3f6af22231c0b2754e020dda53e7d719ba917e41faf23d5f4d"' + Location: + - https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments/1699669290 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E730:2F64:165FAC:2D5DB0:64EF8E1D + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4980' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '20' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:36:05 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 201 +version: 1 diff --git a/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_test_results_error.yaml b/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_test_results_error.yaml new file mode 100644 index 0000000000..8a4421905f --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_test_results_error.yaml @@ -0,0 +1,960 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/compare/4535be18e90467d6d9a99c0ce651becec7f7eba6...2e2600aa09525e2e1e1d98b09de61454d29c94bb + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/4535be18e90467d6d9a99c0ce651becec7f7eba6...2e2600aa09525e2e1e1d98b09de61454d29c94bb","html_url":"https://github.com/ThiagoCodecov/example-python/compare/4535be18e90467d6d9a99c0ce651becec7f7eba6...2e2600aa09525e2e1e1d98b09de61454d29c94bb","permalink_url":"https://github.com/ThiagoCodecov/example-python/compare/ThiagoCodecov:4535be1...ThiagoCodecov:2e2600a","diff_url":"https://github.com/ThiagoCodecov/example-python/compare/4535be18e90467d6d9a99c0ce651becec7f7eba6...2e2600aa09525e2e1e1d98b09de61454d29c94bb.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/compare/4535be18e90467d6d9a99c0ce651becec7f7eba6...2e2600aa09525e2e1e1d98b09de61454d29c94bb.patch","base_commit":{"sha":"4535be18e90467d6d9a99c0ce651becec7f7eba6","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjQ1MzViZTE4ZTkwNDY3ZDZkOWE5OWMwY2U2NTFiZWNlYzdmN2ViYTY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-31T03:23:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-31T03:23:55Z"},"message":"20EE4D09-F49E-474B-B7AF-4E4916EF82FA","tree":{"sha":"7da71bf52d31d24403d4208835b5d1333ba54695","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7da71bf52d31d24403d4208835b5d1333ba54695"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/4535be18e90467d6d9a99c0ce651becec7f7eba6","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"66567ec66e07440e3170910df61cf458812323fc","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/66567ec66e07440e3170910df61cf458812323fc","html_url":"https://github.com/ThiagoCodecov/example-python/commit/66567ec66e07440e3170910df61cf458812323fc"}]},"merge_base_commit":{"sha":"30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjMwY2MxZWQ3NTFhNTlmYTllN2FkOGU3OWZmZjQxYTZmZTExZWY1ZGQ=","commit":{"author":{"name":"Thiago","email":"44376991+ThiagoCodecov@users.noreply.github.com","date":"2019-12-09T12:02:11Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2019-12-09T12:02:11Z"},"message":"Update + README.md","tree":{"sha":"c901b3cb75fd85aebf03f254494d5ad06b86777e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c901b3cb75fd85aebf03f254494d5ad06b86777e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJd7jfDCRBK7hj4Ov3rIwAAdHIIAF8TrpEbraVNLuNObBiQefch\nVHGpc6MNxOnnRgGPZZl/egrosAxCJ2og+ArrjQG+D/7eYUhWeb73YPR8lWvVf6YN\nIkNSwv1o/2bmH3pma9DR4wDXnKeO/w/UPhNZaGC1w3dcvsHSJReSRkl/ipLxtPep\n3XpmC8RdIwFNU1hG8Dd5iKqJz3d3iA3mdkb3vrvYmKq2imQigSbXpOvL1o/Du9DN\nxE29+ZeGqTZf0Z5QHgJT7THw8P1bf/UJUado7nn6cIon/slDbP/ZirN3rgEJWXU6\neH9/bdS+HMcCwK5TZwJBXgRkbtzRIgc7mj5cTWOSX8XFuUnOS/QCyfkAIvajseU=\n=3PXD\n-----END + PGP SIGNATURE-----\n","payload":"tree c901b3cb75fd85aebf03f254494d5ad06b86777e\nparent + 2dfb36626ca60a8aa6d2bc624ad0f54b473bb436\nauthor Thiago <44376991+ThiagoCodecov@users.noreply.github.com> + 1575892931 -0300\ncommitter GitHub 1575892931 -0300\n\nUpdate + README.md"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars3.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"2dfb36626ca60a8aa6d2bc624ad0f54b473bb436","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2dfb36626ca60a8aa6d2bc624ad0f54b473bb436","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2dfb36626ca60a8aa6d2bc624ad0f54b473bb436"}]},"status":"diverged","ahead_by":3,"behind_by":11,"total_commits":3,"commits":[{"sha":"e022f588e40269f8ab1c568080dc4a7662ff2335","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmUwMjJmNTg4ZTQwMjY5ZjhhYjFjNTY4MDgwZGM0YTc2NjJmZjIzMzU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:44Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:44Z"},"message":"69E76A1D-FD86-4A90-A6CE-6A66BEEE75CA","tree":{"sha":"6188b6c086ebf6c63626abca649d2596b145c84c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6188b6c086ebf6c63626abca649d2596b145c84c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e022f588e40269f8ab1c568080dc4a7662ff2335","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd"}]},{"sha":"c2a05aa15ecad5bec37e29b9fe51ef30120f8642","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmMyYTA1YWExNWVjYWQ1YmVjMzdlMjliOWZlNTFlZjMwMTIwZjg2NDI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:47Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:47Z"},"message":"81905186-E567-4E4E-A5CB-3E5BFB820E57","tree":{"sha":"dd944d7739d5d429f264709345fe481c1e602be8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/dd944d7739d5d429f264709345fe481c1e602be8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e022f588e40269f8ab1c568080dc4a7662ff2335","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e022f588e40269f8ab1c568080dc4a7662ff2335"}]},{"sha":"2e2600aa09525e2e1e1d98b09de61454d29c94bb","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJlMjYwMGFhMDk1MjVlMmUxZTFkOThiMDlkZTYxNDU0ZDI5Yzk0YmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:49Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:49Z"},"message":"AE0A9904-3B05-405A-B8F7-A5DD3B38C92D","tree":{"sha":"f64d89541a0899881da6764081991e70ed43bfea","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f64d89541a0899881da6764081991e70ed43bfea"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2e2600aa09525e2e1e1d98b09de61454d29c94bb","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"c2a05aa15ecad5bec37e29b9fe51ef30120f8642","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c2a05aa15ecad5bec37e29b9fe51ef30120f8642"}]}],"files":[{"sha":"657d4c0ca181f2df83261d3ab1922d6ec47ece1e","filename":"README.md","status":"modified","additions":8,"deletions":0,"changes":8,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/2e2600aa09525e2e1e1d98b09de61454d29c94bb/README.md","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/2e2600aa09525e2e1e1d98b09de61454d29c94bb/README.md","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.md?ref=2e2600aa09525e2e1e1d98b09de61454d29c94bb","patch":"@@ + -1 +1,9 @@\n Now this\n+\n+\n+\n+80EC0808-DD33-42DF-8873-B2A605A82432\n+63FE7DEF-8367-47BD-B864-D86DE9F94358\n+69E76A1D-FD86-4A90-A6CE-6A66BEEE75CA\n+81905186-E567-4E4E-A5CB-3E5BFB820E57\n+AE0A9904-3B05-405A-B8F7-A5DD3B38C92D"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 03 Jan 2020 20:01:18 GMT + Etag: + - W/"44cd18200d235728506114fd6b930cca" + Last-Modified: + - Tue, 31 Dec 2019 03:23:55 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - D081:309E:10C87AE:17A2BD6:5E0F9D8E + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4995' + X-Ratelimit-Reset: + - '1578085278' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/compare/4535be18e90467d6d9a99c0ce651becec7f7eba6...2e2600aa09525e2e1e1d98b09de61454d29c94bb +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15","id":350683791,"node_id":"MDExOlB1bGxSZXF1ZXN0MzUwNjgzNzkx","html_url":"https://github.com/ThiagoCodecov/example-python/pull/15","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/15.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/15.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/15","number":15,"state":"open","locked":false,"title":"Thiago/test + 1","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"","created_at":"2019-12-09T12:19:01Z","updated_at":"2019-12-09T12:19:01Z","closed_at":null,"merged_at":null,"merge_commit_sha":null,"assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/15/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/2e2600aa09525e2e1e1d98b09de61454d29c94bb","head":{"label":"ThiagoCodecov:thiago/test-1","ref":"thiago/test-1","sha":"2e2600aa09525e2e1e1d98b09de61454d29c94bb","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2019-12-31T03:24:04Z","pushed_at":"2019-12-31T03:24:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":161,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":4,"license":null,"forks":0,"open_issues":4,"watchers":0,"default_branch":"master"}},"base":{"label":"ThiagoCodecov:master","ref":"master","sha":"d723f5cb5c9c9f48c47f2df97c47de20457d3fdc","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2019-12-31T03:24:04Z","pushed_at":"2019-12-31T03:24:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":161,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":4,"license":null,"forks":0,"open_issues":4,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/15"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/15"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/15/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/2e2600aa09525e2e1e1d98b09de61454d29c94bb"}},"author_association":"OWNER","merged":false,"mergeable":false,"rebaseable":false,"mergeable_state":"dirty","merged_by":null,"comments":0,"review_comments":0,"maintainer_can_modify":false,"commits":3,"additions":8,"deletions":0,"changed_files":1}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 03 Jan 2020 20:01:19 GMT + Etag: + - W/"80de2d6223eb160b0cd153534733b75d" + Last-Modified: + - Fri, 03 Jan 2020 19:56:45 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - D082:50AC:E05476:13C1D91:5E0F9D8E + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4994' + X-Ratelimit-Reset: + - '1578085278' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15 +- request: + body: '{"body": "## [Codecov](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=h1) + Report\n> Merging [#15](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=desc) + into [master](None/gh/ThiagoCodecov/example-python/commit/4535be18e90467d6d9a99c0ce651becec7f7eba6&el=desc) + will **increase** coverage by `10.00%`.\n> The diff coverage is `n/a`.\n\n[![Impacted + file tree graph](None/gh/ThiagoCodecov/example-python/pull/15/graphs/tree.svg?width=650&height=150&src=pr&token=CJ00P0THAO)](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree)\n\n```diff\n@@ Coverage + Diff @@\n## master #15 +/- ##\n===========================================\n+ + Coverage 50.00% 60.00% +10.00% \n===========================================\n Files 2 2 \n Lines 6 10 +4 \n Branches 0 1 +1 \n===========================================\n+ + Hits 3 6 +3 \n Misses 3 3 \n- + Partials 0 1 +1 \n```\n\n| Flag | Coverage \u0394 | + |\n|---|---|---|\n| #unit | `100.00% <\u00f8> (?)` | |\n\n| [Impacted Files](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree) + | Coverage \u0394 | |\n|---|---|---|\n| [file\\_2.py](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree#diff-ZmlsZV8yLnB5) + | `50.00% <0.00%> (\u00f8)` | :arrow_up: |\n| [file\\_1.go](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree#diff-ZmlsZV8xLmdv) + | `62.50% <0.00%> (+12.50%)` | :arrow_up: |\n\n------\n\n[Continue to review + full report at Codecov](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=continue).\n> + **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)\n> + `\u0394 = absolute (impact)`, `\u00f8 = not affected`, `? = missing + data`\n> Powered by [Codecov](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=footer). + Last update [d723f5c...2e2600a](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=lastupdated). + Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).\n"}' + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/issues/15/comments + response: + content: "{\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments/570682170\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/pull/15#issuecomment-570682170\"\ + ,\"issue_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/issues/15\"\ + ,\"id\":570682170,\"node_id\":\"MDEyOklzc3VlQ29tbWVudDU3MDY4MjE3MA==\",\"user\"\ + :{\"login\":\"ThiagoCodecov\",\"id\":44376991,\"node_id\":\"MDQ6VXNlcjQ0Mzc2OTkx\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/44376991?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/ThiagoCodecov\",\"\ + html_url\":\"https://github.com/ThiagoCodecov\",\"followers_url\":\"https://api.github.com/users/ThiagoCodecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/ThiagoCodecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}\"\ + ,\"starred_url\":\"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/ThiagoCodecov/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/ThiagoCodecov/orgs\",\"\ + repos_url\":\"https://api.github.com/users/ThiagoCodecov/repos\",\"events_url\"\ + :\"https://api.github.com/users/ThiagoCodecov/events{/privacy}\",\"received_events_url\"\ + :\"https://api.github.com/users/ThiagoCodecov/received_events\",\"type\":\"\ + User\",\"site_admin\":false},\"created_at\":\"2020-01-03T20:01:19Z\",\"updated_at\"\ + :\"2020-01-03T20:01:19Z\",\"author_association\":\"OWNER\",\"body\":\"# [Codecov](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=h1)\ + \ Report\\n> Merging [#15](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=desc)\ + \ into [master](None/gh/ThiagoCodecov/example-python/commit/4535be18e90467d6d9a99c0ce651becec7f7eba6&el=desc)\ + \ will **increase** coverage by `10.00%`.\\n> The diff coverage is `n/a`.\\\ + n\\n[![Impacted file tree graph](None/gh/ThiagoCodecov/example-python/pull/15/graphs/tree.svg?width=650&height=150&src=pr&token=CJ00P0THAO)](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree)\\\ + n\\n```diff\\n@@ Coverage Diff @@\\n## master\ + \ #15 +/- ##\\n===========================================\\n+\ + \ Coverage 50.00% 60.00% +10.00% \\n===========================================\\\ + n Files 2 2 \\n Lines 6 10\ + \ +4 \\n Branches 0 1 +1 \\n===========================================\\\ + n+ Hits 3 6 +3 \\n Misses 3 3\ + \ \\n- Partials 0 1 +1 \\n```\\n\\n|\ + \ Flag | Coverage \u0394 | |\\n|---|---|---|\\n| #unit | `100.00% <\xF8> (?)`\ + \ | |\\n\\n| [Impacted Files](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree)\ + \ | Coverage \u0394 | |\\n|---|---|---|\\n| [file\\\\_2.py](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree#diff-ZmlsZV8yLnB5)\ + \ | `50.00% <0.00%> (\xF8)` | :arrow_up: |\\n| [file\\\\_1.go](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=tree#diff-ZmlsZV8xLmdv)\ + \ | `62.50% <0.00%> (+12.50%)` | :arrow_up: |\\n\\n------\\n\\n[Continue to\ + \ review full report at Codecov](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=continue).\\\ + n> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)\\\ + n> `\u0394 = absolute (impact)`, `\xF8 = not affected`, `? = missing\ + \ data`\\n> Powered by [Codecov](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=footer).\ + \ Last update [d723f5c...2e2600a](None/gh/ThiagoCodecov/example-python/pull/15?src=pr&el=lastupdated).\ + \ Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).\\\ + n\"}" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Length: + - '3545' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 03 Jan 2020 20:01:20 GMT + Etag: + - '"b9894b79f2e1d3509dad975b48cb566b"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments/570682170 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - D083:50AA:26C744:38893C:5E0F9D8F + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4993' + X-Ratelimit-Reset: + - '1578085278' + X-Xss-Protection: + - 1; mode=block + status: + code: 201 + message: Created + status_code: 201 + url: https://api.github.com/repos/ThiagoCodecov/example-python/issues/15/comments +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/commits + response: + content: '[{"sha":"e022f588e40269f8ab1c568080dc4a7662ff2335","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmUwMjJmNTg4ZTQwMjY5ZjhhYjFjNTY4MDgwZGM0YTc2NjJmZjIzMzU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:44Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:44Z"},"message":"69E76A1D-FD86-4A90-A6CE-6A66BEEE75CA","tree":{"sha":"6188b6c086ebf6c63626abca649d2596b145c84c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6188b6c086ebf6c63626abca649d2596b145c84c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e022f588e40269f8ab1c568080dc4a7662ff2335","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd"}]},{"sha":"c2a05aa15ecad5bec37e29b9fe51ef30120f8642","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmMyYTA1YWExNWVjYWQ1YmVjMzdlMjliOWZlNTFlZjMwMTIwZjg2NDI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:47Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:47Z"},"message":"81905186-E567-4E4E-A5CB-3E5BFB820E57","tree":{"sha":"dd944d7739d5d429f264709345fe481c1e602be8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/dd944d7739d5d429f264709345fe481c1e602be8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e022f588e40269f8ab1c568080dc4a7662ff2335","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e022f588e40269f8ab1c568080dc4a7662ff2335"}]},{"sha":"2e2600aa09525e2e1e1d98b09de61454d29c94bb","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJlMjYwMGFhMDk1MjVlMmUxZTFkOThiMDlkZTYxNDU0ZDI5Yzk0YmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:49Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:49Z"},"message":"AE0A9904-3B05-405A-B8F7-A5DD3B38C92D","tree":{"sha":"f64d89541a0899881da6764081991e70ed43bfea","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f64d89541a0899881da6764081991e70ed43bfea"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2e2600aa09525e2e1e1d98b09de61454d29c94bb","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"c2a05aa15ecad5bec37e29b9fe51ef30120f8642","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c2a05aa15ecad5bec37e29b9fe51ef30120f8642"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 17 Jan 2020 00:10:36 GMT + Etag: + - W/"ccf882511531ad012f6664fd8f73c441" + Last-Modified: + - Fri, 17 Jan 2020 00:10:01 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 132D:1D4D:20EF65:2DD4BC:5E20FB7C + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4994' + X-Ratelimit-Reset: + - '1579222789' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/commits +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/branches?per_page=100&page=1 + response: + content: '[{"name":"master","commit":{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654"},"protected":false},{"name":"random-branch","commit":{"sha":"b12b8ba6046c1738c5c89325b8a63a6f51bc4ff6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b12b8ba6046c1738c5c89325b8a63a6f51bc4ff6"},"protected":false},{"name":"thiago/base-no-base","commit":{"sha":"620b8042c2f846fcc6dda1c732dedd15cdbe76db","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/620b8042c2f846fcc6dda1c732dedd15cdbe76db"},"protected":false},{"name":"thiago/f/big-pt","commit":{"sha":"d55dc4ef748fd11537e50c9abed4ab1864fa1d94","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d55dc4ef748fd11537e50c9abed4ab1864fa1d94"},"protected":false},{"name":"thiago/f/cool-branch","commit":{"sha":"89f3d20f2be3fb2d098e544814f8ce3636dc78c4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/89f3d20f2be3fb2d098e544814f8ce3636dc78c4"},"protected":false},{"name":"thiago/f/something","commit":{"sha":"c9fb9262268f11b53c6b0682d4f9acac89bdeee5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c9fb9262268f11b53c6b0682d4f9acac89bdeee5"},"protected":false},{"name":"thiago/test-1","commit":{"sha":"2e2600aa09525e2e1e1d98b09de61454d29c94bb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb"},"protected":false}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 03 Jul 2023 04:24:01 GMT + ETag: + - W/"dab6303e47e5d12f9bb596bcb1e3fd8a7e482a11e1e543c0b1e05e279ea55fe3" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C46C:0919:18CCE0:32A952:64A24D61 + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4997' + X-RateLimit-Reset: + - '1688361672' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '3' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-08-02 04:17:36 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...4535be18e90467d6d9a99c0ce651becec7f7eba6 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...4535be18e90467d6d9a99c0ce651becec7f7eba6","html_url":"https://github.com/ThiagoCodecov/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...4535be18e90467d6d9a99c0ce651becec7f7eba6","permalink_url":"https://github.com/ThiagoCodecov/example-python/compare/ThiagoCodecov:93189ce...ThiagoCodecov:4535be1","diff_url":"https://github.com/ThiagoCodecov/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...4535be18e90467d6d9a99c0ce651becec7f7eba6.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...4535be18e90467d6d9a99c0ce651becec7f7eba6.patch","base_commit":{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjkzMTg5Y2U1MGYyMjQyOTZkNjQxMmUyODg0YjkzZGNjM2M3Yzg2NTQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"message":"Adding + some encoding","tree":{"sha":"966e4dc888b0828c5dea75f21e82eb2daa9a4485","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/966e4dc888b0828c5dea75f21e82eb2daa9a4485"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","html_url":"https://github.com/ThiagoCodecov/example-python/commit/93189ce50f224296d6412e2884b93dcc3c7c8654","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67"}]},"merge_base_commit":{"sha":"4535be18e90467d6d9a99c0ce651becec7f7eba6","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjQ1MzViZTE4ZTkwNDY3ZDZkOWE5OWMwY2U2NTFiZWNlYzdmN2ViYTY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-31T03:23:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-31T03:23:55Z"},"message":"20EE4D09-F49E-474B-B7AF-4E4916EF82FA","tree":{"sha":"7da71bf52d31d24403d4208835b5d1333ba54695","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7da71bf52d31d24403d4208835b5d1333ba54695"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/4535be18e90467d6d9a99c0ce651becec7f7eba6","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4535be18e90467d6d9a99c0ce651becec7f7eba6/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"66567ec66e07440e3170910df61cf458812323fc","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/66567ec66e07440e3170910df61cf458812323fc","html_url":"https://github.com/ThiagoCodecov/example-python/commit/66567ec66e07440e3170910df61cf458812323fc"}]},"status":"behind","ahead_by":0,"behind_by":52,"total_commits":0,"commits":[],"files":[]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 03 Jul 2023 04:24:01 GMT + ETag: + - W/"c625aec9cfb15b0fc585ff2dbcdaeef7a4e39d1881112104a691b9fb0f748c49" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C46D:8600:1BB361:387DFB:64A24D61 + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4996' + X-RateLimit-Reset: + - '1688361672' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '4' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-08-02 04:17:36 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d + response: + content: '{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d","html_url":"https://github.com/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d","permalink_url":"https://github.com/joseph-sentry/codecov-demo/compare/joseph-sentry:5b174c2...joseph-sentry:5601846","diff_url":"https://github.com/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d.diff","patch_url":"https://github.com/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d.patch","base_commit":{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","node_id":"C_kwDOJu7MkNoAKDViMTc0YzJiNDBkNTAxYTcwYzQ3OWU5MTAyNWQ1MTA5YjFhZDVjMWI","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"message":"make + change on main","tree":{"sha":"a9d6c66e50012a6d42f3f4c4e6d6de845230e73b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/a9d6c66e50012a6d42f3f4c4e6d6de845230e73b"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQCSXJE75ylD7PwNrXf6ITnsvh9gIP5b/JASjGlKqB7f37XPt9wLvT2YaV3HcSyTRyt\nCY9zBOZT+tELiyZcpqKwY=\n-----END + SSH SIGNATURE-----","payload":"tree a9d6c66e50012a6d42f3f4c4e6d6de845230e73b\nparent + 3fe10c352faae434e0cca7c1c5984c1c98000288\nauthor joseph-sentry + 1692026348 -0400\ncommitter joseph-sentry 1692026348 + -0400\n\nmake change on main\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"3fe10c352faae434e0cca7c1c5984c1c98000288","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/3fe10c352faae434e0cca7c1c5984c1c98000288","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/3fe10c352faae434e0cca7c1c5984c1c98000288"}]},"merge_base_commit":{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","node_id":"C_kwDOJu7MkNoAKDViMTc0YzJiNDBkNTAxYTcwYzQ3OWU5MTAyNWQ1MTA5YjFhZDVjMWI","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"message":"make + change on main","tree":{"sha":"a9d6c66e50012a6d42f3f4c4e6d6de845230e73b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/a9d6c66e50012a6d42f3f4c4e6d6de845230e73b"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQCSXJE75ylD7PwNrXf6ITnsvh9gIP5b/JASjGlKqB7f37XPt9wLvT2YaV3HcSyTRyt\nCY9zBOZT+tELiyZcpqKwY=\n-----END + SSH SIGNATURE-----","payload":"tree a9d6c66e50012a6d42f3f4c4e6d6de845230e73b\nparent + 3fe10c352faae434e0cca7c1c5984c1c98000288\nauthor joseph-sentry + 1692026348 -0400\ncommitter joseph-sentry 1692026348 + -0400\n\nmake change on main\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"3fe10c352faae434e0cca7c1c5984c1c98000288","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/3fe10c352faae434e0cca7c1c5984c1c98000288","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/3fe10c352faae434e0cca7c1c5984c1c98000288"}]},"status":"ahead","ahead_by":1,"behind_by":0,"total_commits":1,"commits":[{"sha":"5601846871b8142ab0df1e0b8774756c658bcc7d","node_id":"C_kwDOJu7MkNoAKDU2MDE4NDY4NzFiODE0MmFiMGRmMWUwYjg3NzQ3NTZjNjU4YmNjN2Q","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:16:17Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:23:50Z"},"message":"make + change","tree":{"sha":"3ca6632aeb641e579bea4391294d9ebe55c062c7","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/3ca6632aeb641e579bea4391294d9ebe55c062c7"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5601846871b8142ab0df1e0b8774756c658bcc7d","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQAmeUxSTgmd3FlcaYtRmVpuCh0n6L0/2Oy7wnsoGF6zjLoBO+aDz22vOG+bywNp4bc\nlmOl+Tu5Jk9woOhH2Olg8=\n-----END + SSH SIGNATURE-----","payload":"tree 3ca6632aeb641e579bea4391294d9ebe55c062c7\nparent + 5b174c2b40d501a70c479e91025d5109b1ad5c1b\nauthor joseph-sentry + 1692026177 -0400\ncommitter joseph-sentry 1692026630 + -0400\n\nmake change\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5601846871b8142ab0df1e0b8774756c658bcc7d","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b"}]}],"files":[{"sha":"8664ca881cd0536ab32eac8dfee923a2b304167d","filename":"api/calculator/calculator.py","status":"modified","additions":1,"deletions":3,"changes":4,"blob_url":"https://github.com/joseph-sentry/codecov-demo/blob/5601846871b8142ab0df1e0b8774756c658bcc7d/api%2Fcalculator%2Fcalculator.py","raw_url":"https://github.com/joseph-sentry/codecov-demo/raw/5601846871b8142ab0df1e0b8774756c658bcc7d/api%2Fcalculator%2Fcalculator.py","contents_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/contents/api%2Fcalculator%2Fcalculator.py?ref=5601846871b8142ab0df1e0b8774756c658bcc7d","patch":"@@ + -9,6 +9,4 @@ def multiply(x, y):\n return x * y\n \n def divide(x, + y):\n- if y == 0:\n- return ''Cannot divide by 0''\n- return + x * 1.0 / y\n\\ No newline at end of file\n+ return x * 1.0 / y"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:42 GMT + ETag: + - W/"3bcbc91ba2aac4e70b8b74075bc06a0b7436e42a61c15cd11f8c28af48d74080" + Last-Modified: + - Mon, 14 Aug 2023 15:23:50 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E726:5597:D48B2:1B1274:64EF8E1A + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4987' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '13' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:36:05 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/branches/main + response: + content: '{"name":"main","commit":{"sha":"38c2d0214f2a48c9212a140f5311977059a15b35","node_id":"C_kwDOJu7MkNoAKDM4YzJkMDIxNGYyYTQ4YzkyMTJhMTQwZjUzMTE5NzcwNTlhMTViMzU","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"message":"make + change to app\n\nSigned-off-by: joseph-sentry ","tree":{"sha":"9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/38c2d0214f2a48c9212a140f5311977059a15b35","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQJe3OYXkY7fA1+RZ130Zrab06lH36l2XC9HMqjHDAW8iCW5VUbvNhOxNpKSokDItdd\nroqDRnazyFj3+oWVCvxgM=\n-----END + SSH SIGNATURE-----","payload":"tree 9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043\nparent + 1713006782821e36912c23baa98901c7361ee83c\nauthor joseph-sentry + 1692377290 -0400\ncommitter joseph-sentry 1692377290 + -0400\n\nmake change to app\n\nSigned-off-by: joseph-sentry \n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/38c2d0214f2a48c9212a140f5311977059a15b35","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"1713006782821e36912c23baa98901c7361ee83c","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/1713006782821e36912c23baa98901c7361ee83c","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/1713006782821e36912c23baa98901c7361ee83c"}]},"_links":{"self":"https://api.github.com/repos/joseph-sentry/codecov-demo/branches/main","html":"https://github.com/joseph-sentry/codecov-demo/tree/main"},"protected":false,"protection":{"enabled":false,"required_status_checks":{"enforcement_level":"off","contexts":[],"checks":[]}},"protection_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/branches/main/protection"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:42 GMT + ETag: + - W/"28690f5aa55bcb187fc062a1ab6ad5901a1ccbe0225d6f96dacf712da8e22306" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E727:0526:134901:270CDD:64EF8E1A + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4986' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '14' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:36:05 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b + response: + content: '{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b","permalink_url":"https://github.com/joseph-sentry/codecov-demo/compare/joseph-sentry:38c2d02...joseph-sentry:5b174c2","diff_url":"https://github.com/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b.diff","patch_url":"https://github.com/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b.patch","base_commit":{"sha":"38c2d0214f2a48c9212a140f5311977059a15b35","node_id":"C_kwDOJu7MkNoAKDM4YzJkMDIxNGYyYTQ4YzkyMTJhMTQwZjUzMTE5NzcwNTlhMTViMzU","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"message":"make + change to app\n\nSigned-off-by: joseph-sentry ","tree":{"sha":"9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/38c2d0214f2a48c9212a140f5311977059a15b35","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQJe3OYXkY7fA1+RZ130Zrab06lH36l2XC9HMqjHDAW8iCW5VUbvNhOxNpKSokDItdd\nroqDRnazyFj3+oWVCvxgM=\n-----END + SSH SIGNATURE-----","payload":"tree 9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043\nparent + 1713006782821e36912c23baa98901c7361ee83c\nauthor joseph-sentry + 1692377290 -0400\ncommitter joseph-sentry 1692377290 + -0400\n\nmake change to app\n\nSigned-off-by: joseph-sentry \n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/38c2d0214f2a48c9212a140f5311977059a15b35","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"1713006782821e36912c23baa98901c7361ee83c","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/1713006782821e36912c23baa98901c7361ee83c","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/1713006782821e36912c23baa98901c7361ee83c"}]},"merge_base_commit":{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","node_id":"C_kwDOJu7MkNoAKDViMTc0YzJiNDBkNTAxYTcwYzQ3OWU5MTAyNWQ1MTA5YjFhZDVjMWI","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"message":"make + change on main","tree":{"sha":"a9d6c66e50012a6d42f3f4c4e6d6de845230e73b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/a9d6c66e50012a6d42f3f4c4e6d6de845230e73b"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQCSXJE75ylD7PwNrXf6ITnsvh9gIP5b/JASjGlKqB7f37XPt9wLvT2YaV3HcSyTRyt\nCY9zBOZT+tELiyZcpqKwY=\n-----END + SSH SIGNATURE-----","payload":"tree a9d6c66e50012a6d42f3f4c4e6d6de845230e73b\nparent + 3fe10c352faae434e0cca7c1c5984c1c98000288\nauthor joseph-sentry + 1692026348 -0400\ncommitter joseph-sentry 1692026348 + -0400\n\nmake change on main\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"3fe10c352faae434e0cca7c1c5984c1c98000288","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/3fe10c352faae434e0cca7c1c5984c1c98000288","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/3fe10c352faae434e0cca7c1c5984c1c98000288"}]},"status":"behind","ahead_by":0,"behind_by":2,"total_commits":0,"commits":[],"files":[]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:42 GMT + ETag: + - W/"fb71dcba3dddf292e08598b556f7bcc763019abc6e1678ed3355d3d8ec57454f" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E728:44A9:1226D9:24CF0C:64EF8E1A + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4985' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '15' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:36:05 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"body": "## [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell) + Report\n> Merging [#9](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell) + (5601846) into [main](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell) + (5b174c2) will **increase** coverage by `10.00%`.\n> Report is 2 commits behind + head on main.\n> The diff coverage is `n/a`.\n\n:exclamation: Your organization + is not using the GitHub App Integration. As a result you may experience degraded + service beginning May 15th. Please [install the GitHub App Integration](https://github.com/apps/codecov) + for your organization. [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell).\n\n[![Impacted + file tree graph](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\n\n```diff\n@@ Coverage + Diff @@\n## main #9 +/- ##\n=============================================\n+ + Coverage 50.00% 60.00% +10.00% \n+ Complexity 11 10 -1 \n=============================================\n Files 2 2 \n Lines 6 10 +4 \n Branches 0 1 +1 \n=============================================\n+ + Hits 3 6 +3 \n Misses 3 3 \n- + Partials 0 1 +1 \n```\n\n| [Flag](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell) + | Coverage \u0394 | Complexity \u0394 | |\n|---|---|---|---|\n| [integration](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell) + | `?` | `?` | |\n| [unit](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell) + | `100.00% <\u00f8> (?)` | `0.00 <\u00f8> (?)` | |\n\nFlags with carried forward + coverage won''t be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell#carryforward-flags-in-the-pull-request-comment) + to find out more.\n\n[see 2 files with indirect coverage changes](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\n\n------\n\n[Continue + to review full report in Codecov by Sentry](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell).\n> + **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\n> + `\u0394 = absolute (impact)`, `\u00f8 = not affected`, `? = missing + data`\n> Powered by [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell). + Last update [5b174c2...5601846](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell). + Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell).\n"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '4719' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/issues/9/comments + response: + content: "{\"url\":\"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments/1699669247\"\ + ,\"html_url\":\"https://github.com/joseph-sentry/codecov-demo/pull/9#issuecomment-1699669247\"\ + ,\"issue_url\":\"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/9\"\ + ,\"id\":1699669247,\"node_id\":\"IC_kwDOJu7MkM5lTuT_\",\"user\":{\"login\":\"\ + joseph-sentry\",\"id\":136376984,\"node_id\":\"U_kgDOCCDymA\",\"avatar_url\"\ + :\"https://avatars.githubusercontent.com/u/136376984?u=8154f2067e6b4966acba9c27358d6e49cfbbf45d&v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/joseph-sentry\"\ + ,\"html_url\":\"https://github.com/joseph-sentry\",\"followers_url\":\"https://api.github.com/users/joseph-sentry/followers\"\ + ,\"following_url\":\"https://api.github.com/users/joseph-sentry/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/joseph-sentry/gists{/gist_id}\"\ + ,\"starred_url\":\"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/joseph-sentry/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/joseph-sentry/orgs\",\"\ + repos_url\":\"https://api.github.com/users/joseph-sentry/repos\",\"events_url\"\ + :\"https://api.github.com/users/joseph-sentry/events{/privacy}\",\"received_events_url\"\ + :\"https://api.github.com/users/joseph-sentry/received_events\",\"type\":\"\ + User\",\"site_admin\":false},\"created_at\":\"2023-08-30T18:44:43Z\",\"updated_at\"\ + :\"2023-08-30T18:44:43Z\",\"author_association\":\"OWNER\",\"body\":\"## [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\ + \ Report\\n> Merging [#9](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\ + \ (5601846) into [main](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\ + \ (5b174c2) will **increase** coverage by `10.00%`.\\n> Report is 2 commits\ + \ behind head on main.\\n> The diff coverage is `n/a`.\\n\\n:exclamation: Your\ + \ organization is not using the GitHub App Integration. As a result you may\ + \ experience degraded service beginning May 15th. Please [install the GitHub\ + \ App Integration](https://github.com/apps/codecov) for your organization. [Read\ + \ more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell).\\\ + n\\n[![Impacted file tree graph](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\\\ + n\\n```diff\\n@@ Coverage Diff @@\\n## \ + \ main #9 +/- ##\\n=============================================\\\ + n+ Coverage 50.00% 60.00% +10.00% \\n+ Complexity 11 \ + \ 10 -1 \\n=============================================\\n Files\ + \ 2 2 \\n Lines 6 10 \ + \ +4 \\n Branches 0 1 +1 \\n=============================================\\\ + n+ Hits 3 6 +3 \\n Misses 3 \ + \ 3 \\n- Partials 0 1 +1 \\n```\\\ + n\\n| [Flag](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\ + \ | Coverage \u0394 | Complexity \u0394 | |\\n|---|---|---|---|\\n| [integration](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\ + \ | `?` | `?` | |\\n| [unit](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\ + \ | `100.00% <\xF8> (?)` | `0.00 <\xF8> (?)` | |\\n\\nFlags with carried forward\ + \ coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell#carryforward-flags-in-the-pull-request-comment)\ + \ to find out more.\\n\\n[see 2 files with indirect coverage changes](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\\\ + n\\n------\\n\\n[Continue to review full report in Codecov by Sentry](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell).\\\ + n> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell)\\\ + n> `\u0394 = absolute (impact)`, `\xF8 = not affected`, `? = missing\ + \ data`\\n> Powered by [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell).\ + \ Last update [5b174c2...5601846](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell).\ + \ Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=James+Mitchell).\\\ + n\",\"reactions\":{\"url\":\"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments/1699669247/reactions\"\ + ,\"total_count\":0,\"+1\":0,\"-1\":0,\"laugh\":0,\"hooray\":0,\"confused\":0,\"\ + heart\":0,\"rocket\":0,\"eyes\":0},\"performed_via_github_app\":null}" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '6356' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:43 GMT + ETag: + - '"a3267b6fdb96428f156fb276116e0b4c918da9a371be840fc5573f132fbe1dbe"' + Location: + - https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments/1699669247 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E72A:35CC:109BE2:21B617:64EF8E1B + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4984' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '16' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:36:05 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 201 +version: 1 diff --git a/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_upgrade.yaml b/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_upgrade.yaml new file mode 100644 index 0000000000..52d838ec51 --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_upgrade.yaml @@ -0,0 +1,93 @@ +interactions: +- request: + body: '{"body": "The author of this PR, codecove2e, is not an activated member + of this organization on Codecov.\nPlease [activate this user on Codecov](https://app.codecov.io/members/gh/codecove2e?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bryan+Brewer) + to display this PR comment.\nCoverage data is still being uploaded to Codecov.io + for purposes of overall coverage calculations.\nPlease don''t hesitate to email + us at support@codecov.io with any questions."}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '502' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/codecove2e/example-python/issues/2/comments + response: + content: '{"url":"https://api.github.com/repos/codecove2e/example-python/issues/comments/1361234119","html_url":"https://github.com/codecove2e/example-python/pull/4#issuecomment-1361234119","issue_url":"https://api.github.com/repos/codecove2e/example-python/issues/4","id":1361234119,"node_id":"IC_kwDOHrbKcs5RIsjH","user":{"login":"codecove2e","id":93560619,"node_id":"U_kgDOBZOfKw","avatar_url":"https://avatars.githubusercontent.com/u/93560619?v=4","gravatar_id":"","url":"https://api.github.com/users/codecove2e","html_url":"https://github.com/codecove2e","followers_url":"https://api.github.com/users/codecove2e/followers","following_url":"https://api.github.com/users/codecove2e/following{/other_user}","gists_url":"https://api.github.com/users/codecove2e/gists{/gist_id}","starred_url":"https://api.github.com/users/codecove2e/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecove2e/subscriptions","organizations_url":"https://api.github.com/users/codecove2e/orgs","repos_url":"https://api.github.com/users/codecove2e/repos","events_url":"https://api.github.com/users/codecove2e/events{/privacy}","received_events_url":"https://api.github.com/users/codecove2e/received_events","type":"User","site_admin":false},"created_at":"2022-12-21T12:07:36Z","updated_at":"2022-12-21T12:07:36Z","author_association":"OWNER","body":"The + author of this PR, codecove2e, is not an activated member of this organization + on Codecov.\nPlease [activate this user on Codecov](https://app.codecov.io/members/gh/codecove2e?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bryan+Brewer) + to display this PR comment.\nCoverage data is still being uploaded to Codecov.io + for purposes of overall coverage calculations.\nPlease don''t hesitate to email + us at support@codecov.io with any questions.","reactions":{"url":"https://api.github.com/repos/codecove2e/example-python/issues/comments/1361234119/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"performed_via_github_app":null}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '2078' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 21 Dec 2022 12:07:37 GMT + ETag: + - '"bd60879875bf26300655202f2977cfa5b7c96c86445e0f87e8ddf3fa06a94292"' + Location: + - https://api.github.com/repos/codecove2e/example-python/issues/comments/1361234119 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - DC1C:BDA8:357399A:3629679:63A2F708 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4996' + X-RateLimit-Reset: + - '1671627083' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '4' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-02-18 19:09:58 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 201 +version: 1 diff --git a/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_upload_limited.yaml b/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_upload_limited.yaml new file mode 100644 index 0000000000..0ca7f8eda1 --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_upload_limited.yaml @@ -0,0 +1,93 @@ +interactions: +- request: + body: '{"body": "## [Codecov](None/account/gh/test-acc9/billing) upload limit reached + :warning:\nThis org is currently on the free Basic Plan; which includes 250 + free private repo uploads each month. This month''s limit has + been reached and additional reports cannot be generated. For unlimited uploads, upgrade + to our [pro plan](None/account/gh/test-acc9/billing).\n\n**Do you have questions + or need help?** Connect with our sales team today at ` sales@codecov.io `"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '495' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/test-acc9/priv_example/issues/3/comments + response: + content: '{"url":"https://api.github.com/repos/test-acc9/priv_example/issues/comments/1111984446","html_url":"https://github.com/test-acc9/priv_example/pull/1#issuecomment-1111984446","issue_url":"https://api.github.com/repos/test-acc9/priv_example/issues/1","id":1111984446,"node_id":"IC_kwDOHP_eEc5CR4k-","user":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"created_at":"2022-04-28T09:42:44Z","updated_at":"2022-04-28T09:42:44Z","author_association":"OWNER","body":"# + [Codecov](None/account/gh/test-acc9/billing) upload limit reached :warning:\nThis + org is currently on the free Basic Plan; which includes 250 free private repo + uploads each rolling month. This limit has been reached and + additional reports cannot be generated. For unlimited uploads, upgrade + to our [pro plan](None/account/gh/test-acc9/billing).\n\n**Do you have questions + or need help?** Connect with our sales team today at ` sales@codecov.io `","reactions":{"url":"https://api.github.com/repos/test-acc9/priv_example/issues/comments/1111984446/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"performed_via_github_app":null}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '2049' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 28 Apr 2022 09:42:45 GMT + ETag: + - '"58258dde9e030e0de7c45373103c8d3fa593b19403d53a0ad35289df6dc5a682"' + Location: + - https://api.github.com/repos/test-acc9/priv_example/issues/comments/1111984446 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - EA34:E167:23A263:244D3D:626A6194 + X-OAuth-Scopes: + - notifications, repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1651142564' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-04-28 21:00:00 UTC + http_version: HTTP/1.1 + status_code: 201 +version: 1 diff --git a/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_with_components.yaml b/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_with_components.yaml new file mode 100644 index 0000000000..338cc3e1f1 --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/integration/cassetes/test_comment/TestCommentNotifierIntegration/test_notify_with_components.yaml @@ -0,0 +1,826 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecove2e/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...8589c19ce95a2b13cf7b3272cbf275ca9651ae9c + response: + content: '{"url":"https://api.github.com/repos/codecove2e/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","html_url":"https://github.com/codecove2e/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","permalink_url":"https://github.com/codecove2e/example-python/compare/codecove2e:93189ce...codecove2e:8589c19","diff_url":"https://github.com/codecove2e/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...8589c19ce95a2b13cf7b3272cbf275ca9651ae9c.diff","patch_url":"https://github.com/codecove2e/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...8589c19ce95a2b13cf7b3272cbf275ca9651ae9c.patch","base_commit":{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","node_id":"MDY6Q29tbWl0NTE1Mjk1ODU4OjkzMTg5Y2U1MGYyMjQyOTZkNjQxMmUyODg0YjkzZGNjM2M3Yzg2NTQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"message":"Adding + some encoding","tree":{"sha":"966e4dc888b0828c5dea75f21e82eb2daa9a4485","url":"https://api.github.com/repos/codecove2e/example-python/git/trees/966e4dc888b0828c5dea75f21e82eb2daa9a4485"},"url":"https://api.github.com/repos/codecove2e/example-python/git/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","html_url":"https://github.com/codecove2e/example-python/commit/93189ce50f224296d6412e2884b93dcc3c7c8654","comments_url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","url":"https://api.github.com/repos/codecove2e/example-python/commits/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","html_url":"https://github.com/codecove2e/example-python/commit/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67"}]},"merge_base_commit":{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","node_id":"MDY6Q29tbWl0NTE1Mjk1ODU4OjkzMTg5Y2U1MGYyMjQyOTZkNjQxMmUyODg0YjkzZGNjM2M3Yzg2NTQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"message":"Adding + some encoding","tree":{"sha":"966e4dc888b0828c5dea75f21e82eb2daa9a4485","url":"https://api.github.com/repos/codecove2e/example-python/git/trees/966e4dc888b0828c5dea75f21e82eb2daa9a4485"},"url":"https://api.github.com/repos/codecove2e/example-python/git/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","html_url":"https://github.com/codecove2e/example-python/commit/93189ce50f224296d6412e2884b93dcc3c7c8654","comments_url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","url":"https://api.github.com/repos/codecove2e/example-python/commits/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","html_url":"https://github.com/codecove2e/example-python/commit/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67"}]},"status":"ahead","ahead_by":1,"behind_by":0,"total_commits":1,"commits":[{"sha":"8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","node_id":"C_kwDOHrbKctoAKDg1ODljMTljZTk1YTJiMTNjZjdiMzI3MmNiZjI3NWNhOTY1MWFlOWM","commit":{"author":{"name":"codecove2e","email":"93560619+codecove2e@users.noreply.github.com","date":"2022-08-16T19:38:32Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-08-16T19:38:32Z"},"message":"Update + __init__.py","tree":{"sha":"7c5d795e95bf1dcf12ebe45e3f2abf071e8bcaf3","url":"https://api.github.com/repos/codecove2e/example-python/git/trees/7c5d795e95bf1dcf12ebe45e3f2abf071e8bcaf3"},"url":"https://api.github.com/repos/codecove2e/example-python/git/commits/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJi+/I4CRBK7hj4Ov3rIwAApVwIACMRcC8711GZMH7nXKZH1tFK\ny7tOt6/WfCZsqEMtH5aQ5Px45MKT03wp+uPNBDnTJcdPLwZLfIdf7u9OpB9F4aVP\nsoLxW7IkUo9hT+E5i4YdldQOlmBF6pRwXA1AYYUAy1EgMchjf7kRkVCi3owJRXOV\nTP7cnM7ocP/RV46TzOgkhIh4wa0xbR8+LpFU7/+pRTbK+0u++UOB02Saq1RLvGyb\n8g8bHAtSNtTIRPfqI0EzE3piYjp6EFtoVmjDoTWn3z+fNw/6jB9SO9J9lyWFr62r\nbAoWgl8TuKuGogunswjsoWzs+13oFikh+4x2+V/ze0nk983D1ykXnMHQO2fIZV4=\n=i4b6\n-----END + PGP SIGNATURE-----\n","payload":"tree 7c5d795e95bf1dcf12ebe45e3f2abf071e8bcaf3\nparent + 93189ce50f224296d6412e2884b93dcc3c7c8654\nauthor codecove2e <93560619+codecove2e@users.noreply.github.com> + 1660678712 -0300\ncommitter GitHub 1660678712 -0300\n\nUpdate + __init__.py"}},"url":"https://api.github.com/repos/codecove2e/example-python/commits/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","html_url":"https://github.com/codecove2e/example-python/commit/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","comments_url":"https://api.github.com/repos/codecove2e/example-python/commits/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c/comments","author":{"login":"codecove2e","id":93560619,"node_id":"U_kgDOBZOfKw","avatar_url":"https://avatars.githubusercontent.com/u/93560619?v=4","gravatar_id":"","url":"https://api.github.com/users/codecove2e","html_url":"https://github.com/codecove2e","followers_url":"https://api.github.com/users/codecove2e/followers","following_url":"https://api.github.com/users/codecove2e/following{/other_user}","gists_url":"https://api.github.com/users/codecove2e/gists{/gist_id}","starred_url":"https://api.github.com/users/codecove2e/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecove2e/subscriptions","organizations_url":"https://api.github.com/users/codecove2e/orgs","repos_url":"https://api.github.com/users/codecove2e/repos","events_url":"https://api.github.com/users/codecove2e/events{/privacy}","received_events_url":"https://api.github.com/users/codecove2e/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","html_url":"https://github.com/codecove2e/example-python/commit/93189ce50f224296d6412e2884b93dcc3c7c8654"}]}],"files":[{"sha":"5fc28eefb09af57ad8253d7d2141d0f77ba22ee7","filename":"awesome/__init__.py","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/codecove2e/example-python/blob/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c/awesome%2F__init__.py","raw_url":"https://github.com/codecove2e/example-python/raw/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c/awesome%2F__init__.py","contents_url":"https://api.github.com/repos/codecove2e/example-python/contents/awesome%2F__init__.py?ref=8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","patch":"@@ + -3,7 +3,7 @@ def smile():\n \n \n def frown():\n- return \":(\"\n+ return + \"=(\"\n \n \n def shieeee(g):"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 21 Sep 2022 15:11:06 GMT + ETag: + - W/"cd843e8086c843e2f0f12e6978fa4b298126c8171d778dcb8397b878ee926eae" + Last-Modified: + - Tue, 16 Aug 2022 19:38:32 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - F195:046E:6EEB6D:76ACFC:632B2989 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1663776666' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-10-21 13:48:23 UTC + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"body": "## [Codecov](None/gh/codecove2e/example-python/pull/4?src=pr&el=h1) + Report\nBase: **50.00**% // Head: **60.00**% // Increases project coverage by + **`+10.00%`** :tada:\n> Coverage data is based on head [(`8589c19`)](None/gh/codecove2e/example-python/pull/4?src=pr&el=desc) + compared to base [(`93189ce`)](None/gh/codecove2e/example-python/commit/93189ce50f224296d6412e2884b93dcc3c7c8654?el=desc).\n> + Patch has no changes to coverable lines.\n\n
Additional details + and impacted files\n\n\n[![Impacted file tree graph](None/gh/codecove2e/example-python/pull/4/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij)](None/gh/codecove2e/example-python/pull/4?src=pr&el=tree)\n\n```diff\n@@ Coverage + Diff @@\n## master #4 +/- ##\n=============================================\n+ + Coverage 50.00% 60.00% +10.00% \n+ Complexity 11 10 -1 \n=============================================\n Files 2 2 \n Lines 6 10 +4 \n Branches 0 1 +1 \n=============================================\n+ + Hits 3 6 +3 \n Misses 3 3 \n- + Partials 0 1 +1 \n```\n\n| Flag | Coverage \u0394 + | Complexity \u0394 | |\n|---|---|---|---|\n| integration | `?` | `?` | |\n| + unit | `100.00% <\u00f8> (?)` | `0.00 <\u00f8> (?)` | |\n\nFlags with carried + forward coverage won''t be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Molly+Taylor#carryforward-flags-in-the-pull-request-comment) + to find out more.\n\n| [Impacted Files](None/gh/codecove2e/example-python/pull/4?src=pr&el=tree) + | Coverage \u0394 | Complexity \u0394 | |\n|---|---|---|---|\n| [file\\_2.py](None/gh/codecove2e/example-python/pull/4?src=pr&el=tree#diff-ZmlsZV8yLnB5) + | `50.00% <0.00%> (\u00f8)` | `0.00% <0.00%> (\u00f8%)` | |\n| [file\\_1.go](None/gh/codecove2e/example-python/pull/4?src=pr&el=tree#diff-ZmlsZV8xLmdv) + | `62.50% <0.00%> (+12.50%)` | `10.00% <0.00%> (-1.00%)` | :arrow_up: |\n\n
\n\n[:umbrella: + View full report in Codecov by Sentry](None/gh/codecove2e/example-python/pull/4?src=pr&el=continue). \n:loudspeaker: + Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Molly+Taylor).\n"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '2630' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/codecove2e/example-python/issues/4/comments + response: + content: "{\"url\":\"https://api.github.com/repos/codecove2e/example-python/issues/comments/1253851153\"\ + ,\"html_url\":\"https://github.com/codecove2e/example-python/pull/4#issuecomment-1253851153\"\ + ,\"issue_url\":\"https://api.github.com/repos/codecove2e/example-python/issues/4\"\ + ,\"id\":1253851153,\"node_id\":\"IC_kwDOHrbKcs5KvEAR\",\"user\":{\"login\":\"\ + codecove2e\",\"id\":93560619,\"node_id\":\"U_kgDOBZOfKw\",\"avatar_url\":\"\ + https://avatars.githubusercontent.com/u/93560619?v=4\",\"gravatar_id\":\"\"\ + ,\"url\":\"https://api.github.com/users/codecove2e\",\"html_url\":\"https://github.com/codecove2e\"\ + ,\"followers_url\":\"https://api.github.com/users/codecove2e/followers\",\"\ + following_url\":\"https://api.github.com/users/codecove2e/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecove2e/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/codecove2e/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/codecove2e/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/codecove2e/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/codecove2e/repos\",\"events_url\":\"https://api.github.com/users/codecove2e/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/codecove2e/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"created_at\":\"2022-09-21T15:11:06Z\"\ + ,\"updated_at\":\"2022-09-21T15:11:06Z\",\"author_association\":\"OWNER\",\"\ + body\":\"## [Codecov](None/gh/codecove2e/example-python/pull/4?src=pr&el=h1)\ + \ Report\\nBase: **50.00**% // Head: **60.00**% // Increases project coverage\ + \ by **`+10.00%`** :tada:\\n> Coverage data is based on head [(`8589c19`)](None/gh/codecove2e/example-python/pull/4?src=pr&el=desc)\ + \ compared to base [(`93189ce`)](None/gh/codecove2e/example-python/commit/93189ce50f224296d6412e2884b93dcc3c7c8654?el=desc).\\\ + n> Patch has no changes to coverable lines.\\n\\n
Additional\ + \ details and impacted files\\n\\n\\n[![Impacted file tree graph](None/gh/codecove2e/example-python/pull/4/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij)](None/gh/codecove2e/example-python/pull/4?src=pr&el=tree)\\\ + n\\n```diff\\n@@ Coverage Diff @@\\n## \ + \ master #4 +/- ##\\n=============================================\\\ + n+ Coverage 50.00% 60.00% +10.00% \\n+ Complexity 11 \ + \ 10 -1 \\n=============================================\\n Files\ + \ 2 2 \\n Lines 6 10 \ + \ +4 \\n Branches 0 1 +1 \\n=============================================\\\ + n+ Hits 3 6 +3 \\n Misses 3 \ + \ 3 \\n- Partials 0 1 +1 \\n```\\\ + n\\n| Flag | Coverage \u0394 | Complexity \u0394 | |\\n|---|---|---|---|\\n|\ + \ integration | `?` | `?` | |\\n| unit | `100.00% <\xF8> (?)` | `0.00 <\xF8\ + > (?)` | |\\n\\nFlags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Molly+Taylor#carryforward-flags-in-the-pull-request-comment)\ + \ to find out more.\\n\\n| [Impacted Files](None/gh/codecove2e/example-python/pull/4?src=pr&el=tree)\ + \ | Coverage \u0394 | Complexity \u0394 | |\\n|---|---|---|---|\\n| [file\\\\\ + _2.py](None/gh/codecove2e/example-python/pull/4?src=pr&el=tree#diff-ZmlsZV8yLnB5)\ + \ | `50.00% <0.00%> (\xF8)` | `0.00% <0.00%> (\xF8%)` | |\\n| [file\\\\_1.go](None/gh/codecove2e/example-python/pull/4?src=pr&el=tree#diff-ZmlsZV8xLmdv)\ + \ | `62.50% <0.00%> (+12.50%)` | `10.00% <0.00%> (-1.00%)` | :arrow_up: |\\\ + n\\n
\\n\\n[:umbrella: View full report in Codecov by Sentry](None/gh/codecove2e/example-python/pull/4?src=pr&el=continue).\ + \ \\n:loudspeaker: Do you have feedback about the report comment? [Let us\ + \ know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Molly+Taylor).\\\ + n\",\"reactions\":{\"url\":\"https://api.github.com/repos/codecove2e/example-python/issues/comments/1253851153/reactions\"\ + ,\"total_count\":0,\"+1\":0,\"-1\":0,\"laugh\":0,\"hooray\":0,\"confused\":0,\"\ + heart\":0,\"rocket\":0,\"eyes\":0},\"performed_via_github_app\":null}" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '4174' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 21 Sep 2022 15:11:07 GMT + ETag: + - '"70a2f797a0545b4426c2591967246285228ad4f9021cd8669a1e9903253cdcec"' + Location: + - https://api.github.com/repos/codecove2e/example-python/issues/comments/1253851153 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - F196:7870:7198A4:795698:632B298A + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4998' + X-RateLimit-Reset: + - '1663776666' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '2' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-10-21 13:48:23 UTC + http_version: HTTP/1.1 + status_code: 201 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecove2e/example-python/branches?per_page=100&page=1 + response: + content: '[{"name":"codecove2e-patch-1","commit":{"sha":"dd2085468f3d09d3e277c04bec61aeb84a782dee","url":"https://api.github.com/repos/codecove2e/example-python/commits/dd2085468f3d09d3e277c04bec61aeb84a782dee"},"protected":false},{"name":"codecove2e-patch-2","commit":{"sha":"12f3e9a05310ed583cd1b0ad683d373f88a84053","url":"https://api.github.com/repos/codecove2e/example-python/commits/12f3e9a05310ed583cd1b0ad683d373f88a84053"},"protected":false},{"name":"codecove2e-patch-3","commit":{"sha":"0206296b1424912cc05069a9bf4025cbb95f5ecc","url":"https://api.github.com/repos/codecove2e/example-python/commits/0206296b1424912cc05069a9bf4025cbb95f5ecc"},"protected":false},{"name":"master","commit":{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654"},"protected":false},{"name":"random-branch","commit":{"sha":"b12b8ba6046c1738c5c89325b8a63a6f51bc4ff6","url":"https://api.github.com/repos/codecove2e/example-python/commits/b12b8ba6046c1738c5c89325b8a63a6f51bc4ff6"},"protected":false},{"name":"test-branch","commit":{"sha":"bec77909e0a9b04603d2cdddcba62f99592d6579","url":"https://api.github.com/repos/codecove2e/example-python/commits/bec77909e0a9b04603d2cdddcba62f99592d6579"},"protected":false},{"name":"thiago/base-no-base","commit":{"sha":"620b8042c2f846fcc6dda1c732dedd15cdbe76db","url":"https://api.github.com/repos/codecove2e/example-python/commits/620b8042c2f846fcc6dda1c732dedd15cdbe76db"},"protected":false},{"name":"thiago/f/big-pt","commit":{"sha":"d55dc4ef748fd11537e50c9abed4ab1864fa1d94","url":"https://api.github.com/repos/codecove2e/example-python/commits/d55dc4ef748fd11537e50c9abed4ab1864fa1d94"},"protected":false},{"name":"thiago/f/something","commit":{"sha":"c9fb9262268f11b53c6b0682d4f9acac89bdeee5","url":"https://api.github.com/repos/codecove2e/example-python/commits/c9fb9262268f11b53c6b0682d4f9acac89bdeee5"},"protected":false},{"name":"thiago/test-1","commit":{"sha":"2e2600aa09525e2e1e1d98b09de61454d29c94bb","url":"https://api.github.com/repos/codecove2e/example-python/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb"},"protected":false}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 03 Jul 2023 04:38:35 GMT + ETag: + - W/"a73b85e14f3662ed4e2f7350a4352806cf22f390684fc1f3fa4182ebfa79e8d4" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C50B:64A0:3572E4C:6D8129C:64A250CB + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4993' + X-RateLimit-Reset: + - '1688361672' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '7' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-08-02 04:17:36 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecove2e/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...93189ce50f224296d6412e2884b93dcc3c7c8654 + response: + content: '{"url":"https://api.github.com/repos/codecove2e/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...93189ce50f224296d6412e2884b93dcc3c7c8654","html_url":"https://github.com/codecove2e/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...93189ce50f224296d6412e2884b93dcc3c7c8654","permalink_url":"https://github.com/codecove2e/example-python/compare/codecove2e:93189ce...codecove2e:93189ce","diff_url":"https://github.com/codecove2e/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...93189ce50f224296d6412e2884b93dcc3c7c8654.diff","patch_url":"https://github.com/codecove2e/example-python/compare/93189ce50f224296d6412e2884b93dcc3c7c8654...93189ce50f224296d6412e2884b93dcc3c7c8654.patch","base_commit":{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","node_id":"MDY6Q29tbWl0NTE1Mjk1ODU4OjkzMTg5Y2U1MGYyMjQyOTZkNjQxMmUyODg0YjkzZGNjM2M3Yzg2NTQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"message":"Adding + some encoding","tree":{"sha":"966e4dc888b0828c5dea75f21e82eb2daa9a4485","url":"https://api.github.com/repos/codecove2e/example-python/git/trees/966e4dc888b0828c5dea75f21e82eb2daa9a4485"},"url":"https://api.github.com/repos/codecove2e/example-python/git/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","html_url":"https://github.com/codecove2e/example-python/commit/93189ce50f224296d6412e2884b93dcc3c7c8654","comments_url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","url":"https://api.github.com/repos/codecove2e/example-python/commits/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","html_url":"https://github.com/codecove2e/example-python/commit/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67"}]},"merge_base_commit":{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","node_id":"MDY6Q29tbWl0NTE1Mjk1ODU4OjkzMTg5Y2U1MGYyMjQyOTZkNjQxMmUyODg0YjkzZGNjM2M3Yzg2NTQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"message":"Adding + some encoding","tree":{"sha":"966e4dc888b0828c5dea75f21e82eb2daa9a4485","url":"https://api.github.com/repos/codecove2e/example-python/git/trees/966e4dc888b0828c5dea75f21e82eb2daa9a4485"},"url":"https://api.github.com/repos/codecove2e/example-python/git/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","html_url":"https://github.com/codecove2e/example-python/commit/93189ce50f224296d6412e2884b93dcc3c7c8654","comments_url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","url":"https://api.github.com/repos/codecove2e/example-python/commits/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","html_url":"https://github.com/codecove2e/example-python/commit/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67"}]},"status":"identical","ahead_by":0,"behind_by":0,"total_commits":0,"commits":[],"files":[]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 03 Jul 2023 04:38:36 GMT + ETag: + - W/"1ee7ec16be9404907626e70c31409eff643bd43d57aba766d1b696b5a81ef722" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C50C:4061:348EFE3:6BE942E:64A250CB + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4992' + X-RateLimit-Reset: + - '1688361672' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '8' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-08-02 04:17:36 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d + response: + content: '{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d","html_url":"https://github.com/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d","permalink_url":"https://github.com/joseph-sentry/codecov-demo/compare/joseph-sentry:5b174c2...joseph-sentry:5601846","diff_url":"https://github.com/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d.diff","patch_url":"https://github.com/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d.patch","base_commit":{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","node_id":"C_kwDOJu7MkNoAKDViMTc0YzJiNDBkNTAxYTcwYzQ3OWU5MTAyNWQ1MTA5YjFhZDVjMWI","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"message":"make + change on main","tree":{"sha":"a9d6c66e50012a6d42f3f4c4e6d6de845230e73b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/a9d6c66e50012a6d42f3f4c4e6d6de845230e73b"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQCSXJE75ylD7PwNrXf6ITnsvh9gIP5b/JASjGlKqB7f37XPt9wLvT2YaV3HcSyTRyt\nCY9zBOZT+tELiyZcpqKwY=\n-----END + SSH SIGNATURE-----","payload":"tree a9d6c66e50012a6d42f3f4c4e6d6de845230e73b\nparent + 3fe10c352faae434e0cca7c1c5984c1c98000288\nauthor joseph-sentry + 1692026348 -0400\ncommitter joseph-sentry 1692026348 + -0400\n\nmake change on main\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"3fe10c352faae434e0cca7c1c5984c1c98000288","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/3fe10c352faae434e0cca7c1c5984c1c98000288","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/3fe10c352faae434e0cca7c1c5984c1c98000288"}]},"merge_base_commit":{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","node_id":"C_kwDOJu7MkNoAKDViMTc0YzJiNDBkNTAxYTcwYzQ3OWU5MTAyNWQ1MTA5YjFhZDVjMWI","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"message":"make + change on main","tree":{"sha":"a9d6c66e50012a6d42f3f4c4e6d6de845230e73b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/a9d6c66e50012a6d42f3f4c4e6d6de845230e73b"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQCSXJE75ylD7PwNrXf6ITnsvh9gIP5b/JASjGlKqB7f37XPt9wLvT2YaV3HcSyTRyt\nCY9zBOZT+tELiyZcpqKwY=\n-----END + SSH SIGNATURE-----","payload":"tree a9d6c66e50012a6d42f3f4c4e6d6de845230e73b\nparent + 3fe10c352faae434e0cca7c1c5984c1c98000288\nauthor joseph-sentry + 1692026348 -0400\ncommitter joseph-sentry 1692026348 + -0400\n\nmake change on main\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"3fe10c352faae434e0cca7c1c5984c1c98000288","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/3fe10c352faae434e0cca7c1c5984c1c98000288","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/3fe10c352faae434e0cca7c1c5984c1c98000288"}]},"status":"ahead","ahead_by":1,"behind_by":0,"total_commits":1,"commits":[{"sha":"5601846871b8142ab0df1e0b8774756c658bcc7d","node_id":"C_kwDOJu7MkNoAKDU2MDE4NDY4NzFiODE0MmFiMGRmMWUwYjg3NzQ3NTZjNjU4YmNjN2Q","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:16:17Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:23:50Z"},"message":"make + change","tree":{"sha":"3ca6632aeb641e579bea4391294d9ebe55c062c7","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/3ca6632aeb641e579bea4391294d9ebe55c062c7"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5601846871b8142ab0df1e0b8774756c658bcc7d","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQAmeUxSTgmd3FlcaYtRmVpuCh0n6L0/2Oy7wnsoGF6zjLoBO+aDz22vOG+bywNp4bc\nlmOl+Tu5Jk9woOhH2Olg8=\n-----END + SSH SIGNATURE-----","payload":"tree 3ca6632aeb641e579bea4391294d9ebe55c062c7\nparent + 5b174c2b40d501a70c479e91025d5109b1ad5c1b\nauthor joseph-sentry + 1692026177 -0400\ncommitter joseph-sentry 1692026630 + -0400\n\nmake change\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5601846871b8142ab0df1e0b8774756c658bcc7d","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b"}]}],"files":[{"sha":"8664ca881cd0536ab32eac8dfee923a2b304167d","filename":"api/calculator/calculator.py","status":"modified","additions":1,"deletions":3,"changes":4,"blob_url":"https://github.com/joseph-sentry/codecov-demo/blob/5601846871b8142ab0df1e0b8774756c658bcc7d/api%2Fcalculator%2Fcalculator.py","raw_url":"https://github.com/joseph-sentry/codecov-demo/raw/5601846871b8142ab0df1e0b8774756c658bcc7d/api%2Fcalculator%2Fcalculator.py","contents_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/contents/api%2Fcalculator%2Fcalculator.py?ref=5601846871b8142ab0df1e0b8774756c658bcc7d","patch":"@@ + -9,6 +9,4 @@ def multiply(x, y):\n return x * y\n \n def divide(x, + y):\n- if y == 0:\n- return ''Cannot divide by 0''\n- return + x * 1.0 / y\n\\ No newline at end of file\n+ return x * 1.0 / y"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:46 GMT + ETag: + - W/"3bcbc91ba2aac4e70b8b74075bc06a0b7436e42a61c15cd11f8c28af48d74080" + Last-Modified: + - Mon, 14 Aug 2023 15:23:50 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E731:28BF:EE6EF:1E528B:64EF8E1E + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4979' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '21' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:36:05 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/branches/main + response: + content: '{"name":"main","commit":{"sha":"38c2d0214f2a48c9212a140f5311977059a15b35","node_id":"C_kwDOJu7MkNoAKDM4YzJkMDIxNGYyYTQ4YzkyMTJhMTQwZjUzMTE5NzcwNTlhMTViMzU","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"message":"make + change to app\n\nSigned-off-by: joseph-sentry ","tree":{"sha":"9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/38c2d0214f2a48c9212a140f5311977059a15b35","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQJe3OYXkY7fA1+RZ130Zrab06lH36l2XC9HMqjHDAW8iCW5VUbvNhOxNpKSokDItdd\nroqDRnazyFj3+oWVCvxgM=\n-----END + SSH SIGNATURE-----","payload":"tree 9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043\nparent + 1713006782821e36912c23baa98901c7361ee83c\nauthor joseph-sentry + 1692377290 -0400\ncommitter joseph-sentry 1692377290 + -0400\n\nmake change to app\n\nSigned-off-by: joseph-sentry \n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/38c2d0214f2a48c9212a140f5311977059a15b35","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"1713006782821e36912c23baa98901c7361ee83c","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/1713006782821e36912c23baa98901c7361ee83c","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/1713006782821e36912c23baa98901c7361ee83c"}]},"_links":{"self":"https://api.github.com/repos/joseph-sentry/codecov-demo/branches/main","html":"https://github.com/joseph-sentry/codecov-demo/tree/main"},"protected":false,"protection":{"enabled":false,"required_status_checks":{"enforcement_level":"off","contexts":[],"checks":[]}},"protection_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/branches/main/protection"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:46 GMT + ETag: + - W/"28690f5aa55bcb187fc062a1ab6ad5901a1ccbe0225d6f96dacf712da8e22306" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E732:13D8:116896:235695:64EF8E1E + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4978' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '22' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:36:05 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b + response: + content: '{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b","permalink_url":"https://github.com/joseph-sentry/codecov-demo/compare/joseph-sentry:38c2d02...joseph-sentry:5b174c2","diff_url":"https://github.com/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b.diff","patch_url":"https://github.com/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b.patch","base_commit":{"sha":"38c2d0214f2a48c9212a140f5311977059a15b35","node_id":"C_kwDOJu7MkNoAKDM4YzJkMDIxNGYyYTQ4YzkyMTJhMTQwZjUzMTE5NzcwNTlhMTViMzU","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"message":"make + change to app\n\nSigned-off-by: joseph-sentry ","tree":{"sha":"9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/38c2d0214f2a48c9212a140f5311977059a15b35","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQJe3OYXkY7fA1+RZ130Zrab06lH36l2XC9HMqjHDAW8iCW5VUbvNhOxNpKSokDItdd\nroqDRnazyFj3+oWVCvxgM=\n-----END + SSH SIGNATURE-----","payload":"tree 9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043\nparent + 1713006782821e36912c23baa98901c7361ee83c\nauthor joseph-sentry + 1692377290 -0400\ncommitter joseph-sentry 1692377290 + -0400\n\nmake change to app\n\nSigned-off-by: joseph-sentry \n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/38c2d0214f2a48c9212a140f5311977059a15b35","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"1713006782821e36912c23baa98901c7361ee83c","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/1713006782821e36912c23baa98901c7361ee83c","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/1713006782821e36912c23baa98901c7361ee83c"}]},"merge_base_commit":{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","node_id":"C_kwDOJu7MkNoAKDViMTc0YzJiNDBkNTAxYTcwYzQ3OWU5MTAyNWQ1MTA5YjFhZDVjMWI","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"message":"make + change on main","tree":{"sha":"a9d6c66e50012a6d42f3f4c4e6d6de845230e73b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/a9d6c66e50012a6d42f3f4c4e6d6de845230e73b"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQCSXJE75ylD7PwNrXf6ITnsvh9gIP5b/JASjGlKqB7f37XPt9wLvT2YaV3HcSyTRyt\nCY9zBOZT+tELiyZcpqKwY=\n-----END + SSH SIGNATURE-----","payload":"tree a9d6c66e50012a6d42f3f4c4e6d6de845230e73b\nparent + 3fe10c352faae434e0cca7c1c5984c1c98000288\nauthor joseph-sentry + 1692026348 -0400\ncommitter joseph-sentry 1692026348 + -0400\n\nmake change on main\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"3fe10c352faae434e0cca7c1c5984c1c98000288","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/3fe10c352faae434e0cca7c1c5984c1c98000288","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/3fe10c352faae434e0cca7c1c5984c1c98000288"}]},"status":"behind","ahead_by":0,"behind_by":2,"total_commits":0,"commits":[],"files":[]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:46 GMT + ETag: + - W/"fb71dcba3dddf292e08598b556f7bcc763019abc6e1678ed3355d3d8ec57454f" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E733:16C4:FF333:206BFB:64EF8E1E + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4977' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '23' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:36:05 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"body": "## [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal) + Report\nPatch coverage has no change and project coverage change: **`+10.00%`** + :tada:\n> Comparison is base [(`5b174c2`)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal) + 50.00% compared to head [(`5601846`)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5601846871b8142ab0df1e0b8774756c658bcc7d?&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal) + 60.00%.\n> Report is 2 commits behind head on main.\n\n:exclamation: Your organization + is not using the GitHub App Integration. As a result you may experience degraded + service beginning May 15th. Please [install the GitHub App Integration](https://github.com/apps/codecov) + for your organization. [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal).\n\n
Additional + details and impacted files\n\n\n[![Impacted file tree graph](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal)\n\n```diff\n@@ Coverage + Diff @@\n## main #9 +/- ##\n=============================================\n+ + Coverage 50.00% 60.00% +10.00% \n+ Complexity 11 10 -1 \n=============================================\n Files 2 2 \n Lines 6 10 +4 \n Branches 0 1 +1 \n=============================================\n+ + Hits 3 6 +3 \n Misses 3 3 \n- + Partials 0 1 +1 \n```\n\n| [Flag](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal) + | Coverage \u0394 | Complexity \u0394 | |\n|---|---|---|---|\n| [integration](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal) + | `?` | `?` | |\n| [unit](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal) + | `100.00% <\u00f8> (?)` | `0.00 <\u00f8> (?)` | |\n\nFlags with carried forward + coverage won''t be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal#carryforward-flags-in-the-pull-request-comment) + to find out more.\n\n[see 2 files with indirect coverage changes](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal)\n\n| + [Components](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/components?src=pr&el=components&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal) + | Coverage \u0394 | |\n|---|---|---|\n| [go_files](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal) + | `62.50% <\u00f8> (+12.50%)` | :arrow_up: |\n\n
\n\n[:umbrella: View + full report in Codecov by Sentry](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal). \n:loudspeaker: + Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal).\n"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '4689' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/issues/9/comments + response: + content: "{\"url\":\"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments/1699669323\"\ + ,\"html_url\":\"https://github.com/joseph-sentry/codecov-demo/pull/9#issuecomment-1699669323\"\ + ,\"issue_url\":\"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/9\"\ + ,\"id\":1699669323,\"node_id\":\"IC_kwDOJu7MkM5lTuVL\",\"user\":{\"login\":\"\ + joseph-sentry\",\"id\":136376984,\"node_id\":\"U_kgDOCCDymA\",\"avatar_url\"\ + :\"https://avatars.githubusercontent.com/u/136376984?u=8154f2067e6b4966acba9c27358d6e49cfbbf45d&v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/joseph-sentry\"\ + ,\"html_url\":\"https://github.com/joseph-sentry\",\"followers_url\":\"https://api.github.com/users/joseph-sentry/followers\"\ + ,\"following_url\":\"https://api.github.com/users/joseph-sentry/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/joseph-sentry/gists{/gist_id}\"\ + ,\"starred_url\":\"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/joseph-sentry/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/joseph-sentry/orgs\",\"\ + repos_url\":\"https://api.github.com/users/joseph-sentry/repos\",\"events_url\"\ + :\"https://api.github.com/users/joseph-sentry/events{/privacy}\",\"received_events_url\"\ + :\"https://api.github.com/users/joseph-sentry/received_events\",\"type\":\"\ + User\",\"site_admin\":false},\"created_at\":\"2023-08-30T18:44:47Z\",\"updated_at\"\ + :\"2023-08-30T18:44:47Z\",\"author_association\":\"OWNER\",\"body\":\"## [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal)\ + \ Report\\nPatch coverage has no change and project coverage change: **`+10.00%`**\ + \ :tada:\\n> Comparison is base [(`5b174c2`)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal)\ + \ 50.00% compared to head [(`5601846`)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5601846871b8142ab0df1e0b8774756c658bcc7d?&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal)\ + \ 60.00%.\\n> Report is 2 commits behind head on main.\\n\\n:exclamation: Your\ + \ organization is not using the GitHub App Integration. As a result you may\ + \ experience degraded service beginning May 15th. Please [install the GitHub\ + \ App Integration](https://github.com/apps/codecov) for your organization. [Read\ + \ more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal).\\\ + n\\n
Additional details and impacted files\\n\\n\\\ + n[![Impacted file tree graph](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal)\\\ + n\\n```diff\\n@@ Coverage Diff @@\\n## \ + \ main #9 +/- ##\\n=============================================\\\ + n+ Coverage 50.00% 60.00% +10.00% \\n+ Complexity 11 \ + \ 10 -1 \\n=============================================\\n Files\ + \ 2 2 \\n Lines 6 10 \ + \ +4 \\n Branches 0 1 +1 \\n=============================================\\\ + n+ Hits 3 6 +3 \\n Misses 3 \ + \ 3 \\n- Partials 0 1 +1 \\n```\\\ + n\\n| [Flag](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal)\ + \ | Coverage \u0394 | Complexity \u0394 | |\\n|---|---|---|---|\\n| [integration](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal)\ + \ | `?` | `?` | |\\n| [unit](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal)\ + \ | `100.00% <\xF8> (?)` | `0.00 <\xF8> (?)` | |\\n\\nFlags with carried forward\ + \ coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal#carryforward-flags-in-the-pull-request-comment)\ + \ to find out more.\\n\\n[see 2 files with indirect coverage changes](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal)\\\ + n\\n| [Components](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/components?src=pr&el=components&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal)\ + \ | Coverage \u0394 | |\\n|---|---|---|\\n| [go_files](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal)\ + \ | `62.50% <\xF8> (+12.50%)` | :arrow_up: |\\n\\n
\\n\\n[:umbrella:\ + \ View full report in Codecov by Sentry](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal).\ + \ \\n:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Juan+Villarreal).\\\ + n\",\"reactions\":{\"url\":\"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments/1699669323/reactions\"\ + ,\"total_count\":0,\"+1\":0,\"-1\":0,\"laugh\":0,\"hooray\":0,\"confused\":0,\"\ + heart\":0,\"rocket\":0,\"eyes\":0},\"performed_via_github_app\":null}" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '6326' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:47 GMT + ETag: + - '"dbd1e35f816e49a046e87cc8afcaff84664ea90b7cdc670b21e81765f512f702"' + Location: + - https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments/1699669323 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E734:35CC:10A06A:21BF7A:64EF8E1E + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4976' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '24' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:36:05 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 201 +version: 1 diff --git a/apps/worker/services/notification/notifiers/tests/integration/test_comment.py b/apps/worker/services/notification/notifiers/tests/integration/test_comment.py new file mode 100644 index 0000000000..f6c7b90fe6 --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/integration/test_comment.py @@ -0,0 +1,788 @@ +from unittest.mock import PropertyMock + +import pytest +from shared.reports.readonly import ReadOnlyReport + +from database.tests.factories import CommitFactory, PullFactory, RepositoryFactory +from services.comparison import ComparisonContext, ComparisonProxy +from services.comparison.types import Comparison, EnrichedPull, FullCommit +from services.decoration import Decoration +from services.notification.notifiers.comment import CommentNotifier +from tests.helpers import mock_all_plans_and_tiers + + +@pytest.fixture +def is_not_first_pull(mocker): + mocker.patch( + "database.models.core.Pull.is_first_coverage_pull", + return_value=False, + new_callable=PropertyMock, + ) + + +@pytest.fixture +def codecove2e_comparison(dbsession, request, sample_report, small_report): + repository = RepositoryFactory.create( + owner__service="github", + owner__username="codecove2e", + name="example-python", + owner__unencrypted_oauth_token="ghp_testxh25kbya8pcenroaxwqsiq23ff9xzr0u", + image_token="abcdefghij", + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create( + repository=repository, commitid="93189ce50f224296d6412e2884b93dcc3c7c8654" + ) + head_commit = CommitFactory.create( + repository=repository, + branch="new_branch", + commitid="8589c19ce95a2b13cf7b3272cbf275ca9651ae9c", + ) + pull = PullFactory.create( + repository=repository, + base=base_commit.commitid, + head=head_commit.commitid, + pullid=4, + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + repository = base_commit.repository + base_full_commit = FullCommit( + commit=base_commit, report=ReadOnlyReport.create_from_report(small_report) + ) + head_full_commit = FullCommit( + commit=head_commit, report=ReadOnlyReport.create_from_report(sample_report) + ) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={ + "author": {"id": "12345", "username": "codecove2e"}, + "base": { + "branch": "main", + "commitid": "93189ce50f224296d6412e2884b93dcc3c7c8654", + }, + "head": { + "branch": "codecove2e-patch-3", + "commitid": "8589c19ce95a2b13cf7b3272cbf275ca9651ae9c", + }, + "state": "open", + "title": "Update __init__.py", + "id": "4", + "number": "4", + }, + ), + ) + ) + + +@pytest.fixture +def sample_comparison(dbsession, request, sample_report, small_report): + repository = RepositoryFactory.create( + owner__service="github", + owner__username="joseph-sentry", + name="codecov-demo", + owner__unencrypted_oauth_token="ghp_testmgzs9qm7r27wp376fzv10aobbpva7hd3", + image_token="abcdefghij", + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create( + repository=repository, commitid="5b174c2b40d501a70c479e91025d5109b1ad5c1b" + ) + head_commit = CommitFactory.create( + repository=repository, + branch="test", + commitid="5601846871b8142ab0df1e0b8774756c658bcc7d", + ) + pull = PullFactory.create( + repository=repository, + base=base_commit.commitid, + head=head_commit.commitid, + pullid=9, + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + repository = base_commit.repository + base_full_commit = FullCommit( + commit=base_commit, report=ReadOnlyReport.create_from_report(small_report) + ) + head_full_commit = FullCommit( + commit=head_commit, report=ReadOnlyReport.create_from_report(sample_report) + ) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={ + "author": {"id": "12345", "username": "joseph-sentry"}, + "base": { + "branch": "main", + "commitid": "5b174c2b40d501a70c479e91025d5109b1ad5c1b", + }, + "head": { + "branch": "test", + "commitid": "5601846871b8142ab0df1e0b8774756c658bcc7d", + }, + "state": "open", + "title": "make change", + "id": "9", + "number": "9", + }, + ), + ) + ) + + +@pytest.fixture +def sample_comparison_gitlab(dbsession, request, sample_report, small_report): + repository = RepositoryFactory.create( + owner__username="joseph-sentry", + owner__service="gitlab", + owner__unencrypted_oauth_token="test1nioqi3p3681oa43", + service_id="47404140", + name="example-python", + image_token="abcdefghij", + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create( + repository=repository, commitid="0fc784af11c401449e56b24a174bae7b9af86c98" + ) + head_commit = CommitFactory.create( + repository=repository, + branch="behind", + commitid="0b6a213fc300cd328c0625f38f30432ee6e066e5", + ) + pull = PullFactory.create( + repository=repository, + base=base_commit.commitid, + head=head_commit.commitid, + pullid=5, + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + repository = base_commit.repository + base_full_commit = FullCommit( + commit=base_commit, report=ReadOnlyReport.create_from_report(small_report) + ) + head_full_commit = FullCommit( + commit=head_commit, report=ReadOnlyReport.create_from_report(sample_report) + ) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={ + "author": {"id": "15014576", "username": "joseph-sentry"}, + "base": { + "branch": "main", + "commitid": "0fc784af11c401449e56b24a174bae7b9af86c98", + }, + "head": { + "branch": "behind", + "commitid": "0b6a213fc300cd328c0625f38f30432ee6e066e5", + }, + "state": "open", + "title": "Behind", + "id": "1", + "number": "1", + }, + ), + ) + ) + + +@pytest.fixture +def sample_comparison_for_upgrade(dbsession, request, sample_report, small_report): + repository = RepositoryFactory.create( + owner__service="github", + owner__username="codecove2e", + name="example-python", + owner__unencrypted_oauth_token="ghp_testgkdo1u8jqexy9wabk1n0puoetf9ziam5", + image_token="abcdefghij", + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create( + repository=repository, commitid="93189ce50f224296d6412e2884b93dcc3c7c8654" + ) + head_commit = CommitFactory.create( + repository=repository, + branch="new_branch", + commitid="8589c19ce95a2b13cf7b3272cbf275ca9651ae9c", + ) + pull = PullFactory.create( + repository=repository, + base=base_commit.commitid, + head=head_commit.commitid, + pullid=2, + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + repository = base_commit.repository + base_full_commit = FullCommit( + commit=base_commit, report=ReadOnlyReport.create_from_report(small_report) + ) + head_full_commit = FullCommit( + commit=head_commit, report=ReadOnlyReport.create_from_report(sample_report) + ) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={ + "author": {"id": "12345", "username": "codecove2e"}, + "base": { + "branch": "master", + "commitid": "93189ce50f224296d6412e2884b93dcc3c7c8654", + }, + "head": { + "branch": "codecove2e-patch-3", + "commitid": "8589c19ce95a2b13cf7b3272cbf275ca9651ae9c", + }, + "state": "open", + "title": "Update __init__.py", + "id": "4", + "number": "4", + }, + ), + ) + ) + + +@pytest.fixture +def sample_comparison_for_limited_upload( + dbsession, request, sample_report, small_report +): + repository = RepositoryFactory.create( + owner__username="test-acc9", + owner__service="github", + name="priv_example", + owner__unencrypted_oauth_token="ghp_test1xwr5rxl12dbm97a7r4anr6h67uw0thf", + image_token="abcdefghij", + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create( + repository=repository, commitid="ef6edf5ae6643d53a7971fb8823d3f7b2ac65619" + ) + head_commit = CommitFactory.create( + repository=repository, + branch="featureA", + commitid="610ada9fa2bbc49f1a08917da3f73bef2d03709c", + ) + pull = PullFactory.create( + repository=repository, + base=base_commit.commitid, + head=head_commit.commitid, + pullid=3, + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + repository = base_commit.repository + base_full_commit = FullCommit(commit=base_commit, report=small_report) + head_full_commit = FullCommit(commit=head_commit, report=sample_report) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={ + "author": {"id": "12345", "username": "dana-yaish"}, + "base": { + "branch": "main", + "commitid": "ef6edf5ae6643d53a7971fb8823d3f7b2ac65619", + }, + "head": { + "branch": "featureA", + "commitid": "610ada9fa2bbc49f1a08917da3f73bef2d03709c", + }, + "state": "open", + "title": "Create randomcommit.me", + "id": "1", + "number": "1", + }, + ), + ) + ) + + +@pytest.mark.usefixtures("is_not_first_pull") +class TestCommentNotifierIntegration(object): + @pytest.fixture(autouse=True) + def setup(self): + mock_all_plans_and_tiers() + + @pytest.mark.django_db + def test_notify(self, sample_comparison, codecov_vcr, mock_configuration): + sample_comparison.context = ComparisonContext( + all_tests_passed=True, test_results_error=None + ) + mock_configuration._params["setup"] = { + "codecov_url": None, + "codecov_dashboard_url": None, + } + comparison = sample_comparison + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=comparison.repository_service, + ) + result = notifier.notify(comparison) + assert result.notification_attempted + assert result.notification_successful + assert result.explanation is None + message = [ + "## [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + "> Project coverage is 60.00%. Comparing base [(`5b174c2`)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b?dropdown=coverage&el=desc) to head [(`5601846`)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5601846871b8142ab0df1e0b8774756c658bcc7d?dropdown=coverage&el=desc).", + "> Report is 2 commits behind head on main.", + "", + ":white_check_mark: All tests successful. No failed tests found.", + "", + ":exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.", + "", + "[![Impacted file tree graph](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + "## main #9 +/- ##", + "=============================================", + "+ Coverage 50.00% 60.00% +10.00% ", + "+ Complexity 11 10 -1 ", + "=============================================", + " Files 2 2 ", + " Lines 6 10 +4 ", + " Branches 0 1 +1 ", + "=============================================", + "+ Hits 3 6 +3 ", + " Misses 3 3 ", + "- Partials 0 1 +1 ", + "```", + "", + "| [Flag](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flags) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + "| [integration](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag) | `?` | `?` | |", + "| [unit](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag) | `100.00% <ø> (?)` | `0.00 <ø> (?)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + "[see 2 files with indirect coverage changes](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/indirect-changes?src=pr&el=tree-more)", + "", + "------", + "", + "[Continue to review full report in Codecov by Sentry](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + "> Powered by [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?dropdown=coverage&src=pr&el=footer). Last update [5b174c2...5601846](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?dropdown=coverage&src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ] + for exp, res in zip(result.data_sent["message"], message): + assert exp == res + assert result.data_sent["message"] == message + assert result.data_sent == {"commentid": None, "message": message, "pullid": 9} + assert result.data_received == {"id": 1699669247} + + @pytest.mark.django_db + def test_notify_test_results_error( + self, sample_comparison, codecov_vcr, mock_configuration + ): + sample_comparison.context = ComparisonContext( + all_tests_passed=False, + test_results_error=":x: We are unable to process any of the uploaded JUnit XML files. Please ensure your files are in the right format.", + ) + mock_configuration._params["setup"] = { + "codecov_url": None, + "codecov_dashboard_url": None, + } + comparison = sample_comparison + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=comparison.repository_service, + ) + result = notifier.notify(comparison) + assert result.notification_attempted + assert result.notification_successful + assert result.explanation is None + message = [ + "## [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + "> Project coverage is 60.00%. Comparing base [(`5b174c2`)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b?dropdown=coverage&el=desc) to head [(`5601846`)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5601846871b8142ab0df1e0b8774756c658bcc7d?dropdown=coverage&el=desc).", + "> Report is 2 commits behind head on main.", + "", + ":x: We are unable to process any of the uploaded JUnit XML files. Please ensure your files are in the right format.", + "", + ":exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.", + "", + "[![Impacted file tree graph](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + "## main #9 +/- ##", + "=============================================", + "+ Coverage 50.00% 60.00% +10.00% ", + "+ Complexity 11 10 -1 ", + "=============================================", + " Files 2 2 ", + " Lines 6 10 +4 ", + " Branches 0 1 +1 ", + "=============================================", + "+ Hits 3 6 +3 ", + " Misses 3 3 ", + "- Partials 0 1 +1 ", + "```", + "", + "| [Flag](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flags) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + "| [integration](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag) | `?` | `?` | |", + "| [unit](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag) | `100.00% <ø> (?)` | `0.00 <ø> (?)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + "[see 2 files with indirect coverage changes](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/indirect-changes?src=pr&el=tree-more)", + "", + "------", + "", + "[Continue to review full report in Codecov by Sentry](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + "> Powered by [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?dropdown=coverage&src=pr&el=footer). Last update [5b174c2...5601846](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?dropdown=coverage&src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ] + for exp, res in zip(result.data_sent["message"], message): + assert exp == res + assert result.data_sent["message"] == message + assert result.data_sent == {"commentid": None, "message": message, "pullid": 9} + assert result.data_received == {"id": 1699669247} + + @pytest.mark.django_db + def test_notify_upgrade( + self, dbsession, sample_comparison_for_upgrade, codecov_vcr, mock_configuration + ): + mock_configuration._params["setup"] = {"codecov_dashboard_url": None} + comparison = sample_comparison_for_upgrade + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=comparison.repository_service, + decoration_type=Decoration.upgrade, + ) + result = notifier.notify(comparison) + assert result.notification_attempted + assert result.notification_successful + assert result.explanation is None + expected_message = [ + "The author of this PR, codecove2e, is not an activated member of this organization on Codecov.", + "Please [activate this user on Codecov](https://app.codecov.io/members/gh/codecove2e) to display this PR comment.", + "Coverage data is still being uploaded to Codecov.io for purposes of overall coverage calculations.", + "Please don't hesitate to email us at support@codecov.io with any questions.", + ] + for exp, res in zip(result.data_sent["message"], expected_message): + assert exp == res + assert result.data_sent["message"] == expected_message + assert result.data_sent == { + "commentid": None, + "message": expected_message, + "pullid": 2, + } + assert result.data_received == {"id": 1361234119} + + @pytest.mark.django_db + def test_notify_upload_limited( + self, + dbsession, + sample_comparison_for_limited_upload, + codecov_vcr, + mock_configuration, + ): + mock_configuration._params["setup"] = { + "codecov_url": None, + "codecov_dashboard_url": "https://app.codecov.io", + } + comparison = sample_comparison_for_limited_upload + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=comparison.repository_service, + decoration_type=Decoration.upload_limit, + ) + result = notifier.notify(comparison) + assert result.notification_attempted + assert result.notification_successful + assert result.explanation is None + expected_message = [ + "## [Codecov](https://app.codecov.io/plan/gh/test-acc9) upload limit reached :warning:", + "This org is currently on the free Basic Plan; which includes 250 free private repo uploads each rolling month.\ + This limit has been reached and additional reports cannot be generated. For unlimited uploads,\ + upgrade to our [pro plan](https://app.codecov.io/plan/gh/test-acc9).", + "", + "**Do you have questions or need help?** Connect with our sales team today at ` sales@codecov.io `", + ] + for exp, res in zip(result.data_sent["message"], expected_message): + assert exp == res + assert result.data_sent["message"] == expected_message + assert result.data_sent == { + "commentid": None, + "message": expected_message, + "pullid": 3, + } + assert result.data_received == {"id": 1111984446} + + @pytest.mark.django_db + def test_notify_gitlab( + self, sample_comparison_gitlab, codecov_vcr, mock_configuration + ): + mock_configuration._params["setup"] = { + "codecov_url": None, + "codecov_dashboard_url": None, + } + comparison = sample_comparison_gitlab + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=comparison.repository_service, + ) + result = notifier.notify(comparison) + assert result.notification_attempted + assert result.notification_successful + assert result.explanation is None + message = [ + "## [Codecov](https://app.codecov.io/gl/joseph-sentry/example-python/pull/5?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + "> Project coverage is 60.00%. Comparing base [(`0fc784a`)](https://app.codecov.io/gl/joseph-sentry/example-python/commit/0fc784af11c401449e56b24a174bae7b9af86c98?dropdown=coverage&el=desc) to head [(`0b6a213`)](https://app.codecov.io/gl/joseph-sentry/example-python/commit/0b6a213fc300cd328c0625f38f30432ee6e066e5?dropdown=coverage&el=desc).", + "", + "[![Impacted file tree graph](https://app.codecov.io/gl/joseph-sentry/example-python/pull/5/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij)](https://app.codecov.io/gl/joseph-sentry/example-python/pull/5?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + "## main #5 +/- ##", + "=============================================", + "+ Coverage 50.00% 60.00% +10.00% ", + "+ Complexity 11 10 -1 ", + "=============================================", + " Files 2 2 ", + " Lines 6 10 +4 ", + " Branches 0 1 +1 ", + "=============================================", + "+ Hits 3 6 +3 ", + " Misses 3 3 ", + "- Partials 0 1 +1 ", + "```", + "", + "| [Flag](https://app.codecov.io/gl/joseph-sentry/example-python/pull/5/flags?src=pr&el=flags) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + "| [integration](https://app.codecov.io/gl/joseph-sentry/example-python/pull/5/flags?src=pr&el=flag) | `?` | `?` | |", + "| [unit](https://app.codecov.io/gl/joseph-sentry/example-python/pull/5/flags?src=pr&el=flag) | `100.00% <ø> (?)` | `0.00 <ø> (?)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + "[see 2 files with indirect coverage changes](https://app.codecov.io/gl/joseph-sentry/example-python/pull/5/indirect-changes?src=pr&el=tree-more)", + "", + "------", + "", + "[Continue to review full report in Codecov by Sentry](https://app.codecov.io/gl/joseph-sentry/example-python/pull/5?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + "> Powered by [Codecov](https://app.codecov.io/gl/joseph-sentry/example-python/pull/5?dropdown=coverage&src=pr&el=footer). Last update [0fc784a...0b6a213](https://app.codecov.io/gl/joseph-sentry/example-python/pull/5?dropdown=coverage&src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ] + for exp, res in zip(result.data_sent["message"], message): + assert exp == res + assert result.data_sent["message"] == message + assert result.data_sent == {"commentid": None, "message": message, "pullid": 5} + assert result.data_received == {"id": 1457135397} + + @pytest.mark.django_db + def test_notify_new_layout( + self, sample_comparison, codecov_vcr, mock_configuration + ): + mock_configuration._params["setup"] = {"codecov_dashboard_url": None} + comparison = sample_comparison + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "newheader, reach, diff, flags, files, newfooter", + "hide_comment_details": True, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=comparison.repository_service, + ) + result = notifier.notify(comparison) + assert result.notification_attempted + assert result.notification_successful + assert result.explanation is None + message = [ + "## [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + "> Project coverage is 60.00%. Comparing base [(`5b174c2`)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b?dropdown=coverage&el=desc) to head [(`5601846`)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5601846871b8142ab0df1e0b8774756c658bcc7d?dropdown=coverage&el=desc).", + "> Report is 2 commits behind head on main.", + "", + ":exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.", + "", + "
Additional details and impacted files\n", + "", + "[![Impacted file tree graph](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + "## main #9 +/- ##", + "=============================================", + "+ Coverage 50.00% 60.00% +10.00% ", + "+ Complexity 11 10 -1 ", + "=============================================", + " Files 2 2 ", + " Lines 6 10 +4 ", + " Branches 0 1 +1 ", + "=============================================", + "+ Hits 3 6 +3 ", + " Misses 3 3 ", + "- Partials 0 1 +1 ", + "```", + "", + "| [Flag](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flags) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + "| [integration](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag) | `?` | `?` | |", + "| [unit](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag) | `100.00% <ø> (?)` | `0.00 <ø> (?)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + "[see 2 files with indirect coverage changes](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/indirect-changes?src=pr&el=tree-more)", + "", + "
", + "", + "[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?dropdown=coverage&src=pr&el=continue). ", + ":loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/).", + "", + ] + for exp, res in zip(result.data_sent["message"], message): + assert exp == res + + assert result.data_sent["message"] == message + assert result.data_sent == {"commentid": None, "message": message, "pullid": 9} + assert result.data_received == {"id": 1699669290} + + @pytest.mark.django_db + def test_notify_with_components( + self, sample_comparison, codecov_vcr, mock_configuration + ): + mock_configuration._params["setup"] = {"codecov_dashboard_url": None} + comparison = sample_comparison + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "newheader, reach, diff, flags, files, components, newfooter", + "hide_comment_details": True, + }, + notifier_site_settings=True, + current_yaml={ + "component_management": { + "individual_components": [ + {"component_id": "go_files", "paths": [r".*\.go"]} + ] + } + }, + repository_service=comparison.repository_service, + ) + result = notifier.notify(comparison) + assert result.notification_attempted + assert result.notification_successful + assert result.explanation is None + message = [ + "## [Codecov](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + "> Project coverage is 60.00%. Comparing base [(`5b174c2`)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b?dropdown=coverage&el=desc) to head [(`5601846`)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/commit/5601846871b8142ab0df1e0b8774756c658bcc7d?dropdown=coverage&el=desc).", + "> Report is 2 commits behind head on main.", + "", + ":exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.", + "", + "
Additional details and impacted files\n", + "", + "[![Impacted file tree graph](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij)](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + "## main #9 +/- ##", + "=============================================", + "+ Coverage 50.00% 60.00% +10.00% ", + "+ Complexity 11 10 -1 ", + "=============================================", + " Files 2 2 ", + " Lines 6 10 +4 ", + " Branches 0 1 +1 ", + "=============================================", + "+ Hits 3 6 +3 ", + " Misses 3 3 ", + "- Partials 0 1 +1 ", + "```", + "", + "| [Flag](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flags) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + "| [integration](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag) | `?` | `?` | |", + "| [unit](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag) | `100.00% <ø> (?)` | `0.00 <ø> (?)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + "[see 2 files with indirect coverage changes](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/indirect-changes?src=pr&el=tree-more)", + "", + "| [Components](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/components?src=pr&el=components) | Coverage Δ | |", + "|---|---|---|", + "| [go_files](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9/components?src=pr&el=component) | `62.50% <ø> (+12.50%)` | :arrow_up: |", + "", + "
", + "", + "[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/joseph-sentry/codecov-demo/pull/9?dropdown=coverage&src=pr&el=continue). ", + ":loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/).", + "", + ] + for exp, res in zip(result.data_sent["message"], message): + assert exp == res + + assert result.data_sent["message"] == message + assert result.data_sent == {"commentid": None, "message": message, "pullid": 9} + assert result.data_received == {"id": 1699669323} diff --git a/apps/worker/services/notification/notifiers/tests/unit/test_checks.py b/apps/worker/services/notification/notifiers/tests/unit/test_checks.py new file mode 100644 index 0000000000..564b0e8966 --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/unit/test_checks.py @@ -0,0 +1,2381 @@ +from copy import deepcopy +from unittest.mock import Mock +from urllib.parse import quote_plus + +import pytest +from shared.reports.readonly import ReadOnlyReport +from shared.reports.reportfile import ReportFile +from shared.reports.resources import Report +from shared.reports.types import ReportLine +from shared.torngit.exceptions import TorngitClientGeneralError, TorngitError +from shared.torngit.status import Status +from shared.yaml.user_yaml import UserYaml + +from services.decoration import Decoration +from services.notification.notifiers.base import NotificationResult +from services.notification.notifiers.checks import ( + ChangesChecksNotifier, + PatchChecksNotifier, + ProjectChecksNotifier, +) +from services.notification.notifiers.checks.base import ChecksNotifier +from services.notification.notifiers.checks.checks_with_fallback import ( + ChecksWithFallback, +) +from services.notification.notifiers.mixins.status import ( + HelperTextKey, + HelperTextTemplate, +) +from services.notification.notifiers.status import PatchStatusNotifier +from tests.helpers import mock_all_plans_and_tiers + + +@pytest.fixture +def mock_repo_provider(mock_repo_provider): + compare_result = { + "diff": { + "files": { + "file_1.go": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["5", "8", "5", "9"], + "lines": [ + " Overview", + " --------", + " ", + "-Main website: `Codecov `_.", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + "+website: `Codecov `_.", + " ", + " .. code-block:: shell-session", + " ", + ], + }, + { + "header": ["46", "12", "47", "19"], + "lines": [ + " ", + " You may need to configure a ``.coveragerc`` file. Learn more `here `_. Start with this `generic .coveragerc `_ for example.", + " ", + "-We highly suggest adding `source` to your ``.coveragerc`` which solves a number of issues collecting coverage.", + "+We highly suggest adding ``source`` to your ``.coveragerc``, which solves a number of issues collecting coverage.", + " ", + " .. code-block:: ini", + " ", + " [run]", + " source=your_package_name", + "+ ", + "+If there are multiple sources, you instead should add ``include`` to your ``.coveragerc``", + "+", + "+.. code-block:: ini", + "+", + "+ [run]", + "+ include=your_package_name/*", + " ", + " unittests", + " ---------", + ], + }, + { + "header": ["150", "5", "158", "4"], + "lines": [ + " * Twitter: `@codecov `_.", + " * Email: `hello@codecov.io `_.", + " ", + "-We are happy to help if you have any questions. Please contact email our Support at [support@codecov.io](mailto:support@codecov.io)", + "-", + "+We are happy to help if you have any questions. Please contact email our Support at `support@codecov.io `_.", + ], + }, + ], + "stats": {"added": 11, "removed": 4}, + } + } + }, + "commits": [ + { + "commitid": "b92edba44fdd29fcc506317cc3ddeae1a723dd08", + "message": "Update README.rst", + "timestamp": "2018-07-09T23:51:16Z", + "author": { + "id": 8398772, + "username": "jerrode", + "name": "Jerrod", + "email": "jerrod@fundersclub.com", + }, + }, + { + "commitid": "6ae5f1795a441884ed2847bb31154814ac01ef38", + "message": "Update README.rst", + "timestamp": "2018-04-26T08:35:58Z", + "author": { + "id": 11602092, + "username": "TomPed", + "name": "Thomas Pedbereznak", + "email": "tom@tomped.com", + }, + }, + ], + } + mock_repo_provider.get_compare.return_value = compare_result + return mock_repo_provider + + +@pytest.fixture +def comparison_with_multiple_changes(sample_comparison): + first_report = Report() + second_report = Report() + # DELETED FILE + first_deleted_file = ReportFile("deleted.py") + first_deleted_file.append(10, ReportLine.create(coverage=1)) + first_deleted_file.append(12, ReportLine.create(coverage=0)) + first_report.append(first_deleted_file) + # ADDED FILE + second_added_file = ReportFile("added.py") + second_added_file.append(99, ReportLine.create(coverage=1)) + second_added_file.append(101, ReportLine.create(coverage=0)) + second_report.append(second_added_file) + # MODIFIED FILE + first_modified_file = ReportFile("modified.py") + first_modified_file.append(17, ReportLine.create(coverage=1)) + first_modified_file.append(18, ReportLine.create(coverage=1)) + first_modified_file.append(19, ReportLine.create(coverage=1)) + first_modified_file.append(20, ReportLine.create(coverage=0)) + first_modified_file.append(21, ReportLine.create(coverage=1)) + first_modified_file.append(22, ReportLine.create(coverage=1)) + first_modified_file.append(23, ReportLine.create(coverage=1)) + first_modified_file.append(24, ReportLine.create(coverage=1)) + first_report.append(first_modified_file) + second_modified_file = ReportFile("modified.py") + second_modified_file.append(18, ReportLine.create(coverage=1)) + second_modified_file.append(19, ReportLine.create(coverage=0)) + second_modified_file.append(20, ReportLine.create(coverage=0)) + second_modified_file.append(21, ReportLine.create(coverage=1)) + second_modified_file.append(22, ReportLine.create(coverage=0)) + second_modified_file.append(23, ReportLine.create(coverage=0)) + second_modified_file.append(24, ReportLine.create(coverage=1)) + second_report.append(second_modified_file) + # RENAMED WITHOUT CHANGES + first_renamed_without_changes_file = ReportFile("old_renamed.py") + first_renamed_without_changes_file.append(1, ReportLine.create(coverage=1)) + first_renamed_without_changes_file.append(2, ReportLine.create(coverage=1)) + first_renamed_without_changes_file.append(3, ReportLine.create(coverage=0)) + first_renamed_without_changes_file.append(4, ReportLine.create(coverage=1)) + first_renamed_without_changes_file.append(5, ReportLine.create(coverage=0)) + first_report.append(first_renamed_without_changes_file) + second_renamed_without_changes_file = ReportFile("renamed.py") + second_renamed_without_changes_file.append(1, ReportLine.create(coverage=1)) + second_renamed_without_changes_file.append(2, ReportLine.create(coverage=1)) + second_renamed_without_changes_file.append(3, ReportLine.create(coverage=0)) + second_renamed_without_changes_file.append(4, ReportLine.create(coverage=1)) + second_renamed_without_changes_file.append(5, ReportLine.create(coverage=0)) + second_report.append(second_renamed_without_changes_file) + # RENAMED WITH COVERAGE CHANGES FILE + first_renamed_file = ReportFile("old_renamed_with_changes.py") + first_renamed_file.append(2, ReportLine.create(coverage=1)) + first_renamed_file.append(3, ReportLine.create(coverage=1)) + first_renamed_file.append(5, ReportLine.create(coverage=0)) + first_renamed_file.append(8, ReportLine.create(coverage=1)) + first_renamed_file.append(13, ReportLine.create(coverage=1)) + first_report.append(first_renamed_file) + second_renamed_file = ReportFile("renamed_with_changes.py") + second_renamed_file.append(5, ReportLine.create(coverage=1)) + second_renamed_file.append(8, ReportLine.create(coverage=0)) + second_renamed_file.append(13, ReportLine.create(coverage=1)) + second_renamed_file.append(21, ReportLine.create(coverage=1)) + second_renamed_file.append(34, ReportLine.create(coverage=0)) + second_report.append(second_renamed_file) + # UNRELATED FILE + first_unrelated_file = ReportFile("unrelated.py") + first_unrelated_file.append(1, ReportLine.create(coverage=1)) + first_unrelated_file.append(2, ReportLine.create(coverage=1)) + first_unrelated_file.append(4, ReportLine.create(coverage=1)) + first_unrelated_file.append(16, ReportLine.create(coverage=0)) + first_unrelated_file.append(256, ReportLine.create(coverage=1)) + first_unrelated_file.append(65556, ReportLine.create(coverage=1)) + first_report.append(first_unrelated_file) + second_unrelated_file = ReportFile("unrelated.py") + second_unrelated_file.append(2, ReportLine.create(coverage=1)) + second_unrelated_file.append(4, ReportLine.create(coverage=0)) + second_unrelated_file.append(8, ReportLine.create(coverage=0)) + second_unrelated_file.append(16, ReportLine.create(coverage=1)) + second_unrelated_file.append(32, ReportLine.create(coverage=0)) + second_report.append(second_unrelated_file) + sample_comparison.project_coverage_base.report = ReadOnlyReport.create_from_report( + first_report + ) + sample_comparison.head.report = ReadOnlyReport.create_from_report(second_report) + return sample_comparison + + +@pytest.fixture +def multiple_diff_changes(): + return { + "files": { + "modified.py": { + "before": None, + "segments": [ + { + "header": ["20", "8", "20", "8"], + "lines": [ + " return k * k", + " ", + " ", + "-def k(l):", + "- return 2 * l", + "+def k(var):", + "+ return 2 * var", + " ", + " ", + " def sample_function():", + ], + } + ], + "stats": {"added": 2, "removed": 2}, + "type": "modified", + }, + "renamed.py": { + "before": "old_renamed.py", + "segments": [], + "stats": {"added": 0, "removed": 0}, + "type": "modified", + }, + "renamed_with_changes.py": { + "before": "old_renamed_with_changes.py", + "segments": [], + "stats": {"added": 0, "removed": 0}, + "type": "modified", + }, + "added.py": { + "before": None, + "segments": [ + { + "header": ["0", "0", "1", ""], + "lines": ["+This is an explanation"], + } + ], + "stats": {"added": 1, "removed": 0}, + "type": "new", + }, + "deleted.py": { + "before": "tests/test_sample.py", + "stats": {"added": 0, "removed": 0}, + "type": "deleted", + }, + } + } + + +class TestChecksWithFallback(object): + def test_checks_403_failure(self, sample_comparison, mocker, mock_repo_provider): + mock_repo_provider.create_check_run = Mock( + side_effect=TorngitClientGeneralError( + 403, response_data="No Access", message="No Access" + ) + ) + + checks_notifier = PatchChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["flagone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + status_notifier = mocker.MagicMock( + PatchStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["flagone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + ) + status_notifier.notify.return_value = "success" + fallback_notifier = ChecksWithFallback( + checks_notifier=checks_notifier, status_notifier=status_notifier + ) + assert fallback_notifier.name == "checks-patch-with-fallback" + assert fallback_notifier.title == "title" + assert fallback_notifier.is_enabled() == True + assert fallback_notifier.notification_type.value == "checks_patch" + assert fallback_notifier.decoration_type is None + + res = fallback_notifier.notify(sample_comparison) + fallback_notifier.store_results(sample_comparison, res) + assert status_notifier.notify.call_count == 1 + assert fallback_notifier.name == "checks-patch-with-fallback" + assert fallback_notifier.title == "title" + assert fallback_notifier.is_enabled() == True + assert fallback_notifier.notification_type.value == "checks_patch" + assert fallback_notifier.decoration_type is None + assert res == "success" + + def test_checks_failure(self, sample_comparison, mocker, mock_repo_provider): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_repo_provider.create_check_run = Mock( + side_effect=TorngitClientGeneralError( + 409, response_data="No Access", message="No Access" + ) + ) + + checks_notifier = PatchChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["flagone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + status_notifier = mocker.MagicMock( + PatchStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["flagone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + ) + status_notifier.notify.return_value = "success" + fallback_notifier = ChecksWithFallback( + checks_notifier=checks_notifier, status_notifier=status_notifier + ) + assert fallback_notifier.name == "checks-patch-with-fallback" + assert fallback_notifier.title == "title" + assert fallback_notifier.is_enabled() == True + assert fallback_notifier.notification_type.value == "checks_patch" + assert fallback_notifier.decoration_type is None + + res = fallback_notifier.notify(sample_comparison) + assert res.notification_successful == False + assert res.explanation == "client_side_error_provider" + + mock_repo_provider.create_check_run = Mock(side_effect=TorngitError()) + + res = fallback_notifier.notify(sample_comparison) + assert res.notification_successful == False + assert res.explanation == "server_side_error_provider" + + mock_repo_provider.create_check_run.return_value = 1234 + mock_repo_provider.update_check_run = Mock(side_effect=TorngitError()) + res = fallback_notifier.notify(sample_comparison) + assert res.notification_successful == False + assert res.explanation == "server_side_error_provider" + + def test_checks_no_pull(self, sample_comparison_without_pull, mocker): + comparison = sample_comparison_without_pull + checks_notifier = PatchChecksNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["flagone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=None, + ) + status_notifier = mocker.MagicMock( + PatchStatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["flagone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=None, + ) + ) + status_notifier.notify.return_value = "success" + fallback_notifier = ChecksWithFallback( + checks_notifier=checks_notifier, status_notifier=status_notifier + ) + result = fallback_notifier.notify(sample_comparison_without_pull) + assert result == "success" + assert status_notifier.notify.call_count == 1 + + def test_notify_pull_request_not_in_provider( + self, dbsession, sample_comparison_database_pull_without_provider, mocker + ): + comparison = sample_comparison_database_pull_without_provider + checks_notifier = PatchChecksNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["flagone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=None, + ) + status_notifier = mocker.MagicMock( + PatchStatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["flagone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=None, + ) + ) + status_notifier.notify.return_value = "success" + fallback_notifier = ChecksWithFallback( + checks_notifier=checks_notifier, status_notifier=status_notifier + ) + result = fallback_notifier.notify(comparison) + assert result == "success" + assert status_notifier.notify.call_count == 1 + + def test_notify_closed_pull_request(self, dbsession, sample_comparison, mocker): + sample_comparison.pull.state = "closed" + + checks_notifier = PatchChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["flagone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=None, + ) + status_notifier = mocker.MagicMock( + PatchStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["flagone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=None, + ) + ) + status_notifier.notify.return_value = "success" + fallback_notifier = ChecksWithFallback( + checks_notifier=checks_notifier, status_notifier=status_notifier + ) + result = fallback_notifier.notify(sample_comparison) + assert result == "success" + assert status_notifier.notify.call_count == 1 + + +class TestBaseChecksNotifier(object): + def test_create_annotations_single_segment(self, sample_comparison): + notifier = ChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=None, + ) + diff = { + "files": { + "file_1.go": { + "type": "modified", + "before": "None", + "segments": [ + { + "header": ["5", "8", "5", "9"], + "lines": [ + " Overview", + " --------", + " ", + "-Main website: `Codecov `_.", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + "+website: `Codecov `_.", + " ", + " .. code-block:: shell-session", + " ", + ], + } + ], + "totals": True, + } + } + } + expected_annotations = [ + { + "path": "file_1.go", + "start_line": 10, + "end_line": 10, + "annotation_level": "warning", + "message": "Added line #L10 was not covered by tests", + } + ] + annotations = notifier.create_annotations(sample_comparison, diff) + assert expected_annotations == annotations + + def test_create_annotations_multiple_segments(self, sample_comparison): + notifier = ChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=None, + ) + diff = { + "files": { + "file_1.go": { + "type": "modified", + "before": "None", + "segments": [ + { + "header": ["1", "1", "1", "1"], + "lines": [ + " ", + "+ You may need to configure a ``.coveragerc`` file. Learn more `here `_. Start with this `generic .coveragerc `_ for example.", + " ", + "-We highly suggest adding `source` to your ``.coveragerc`` which solves a number of issues collecting coverage.", + "+We highly suggest adding ``source`` to your ``.coveragerc``, which solves a number of issues collecting coverage.", + " ---------", + ], + }, + { + "header": ["5", "8", "5", "9"], + "lines": [ + " Overview", + " --------", + " ", + "-Main website: `Codecov `_.", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + "+website: `Codecov `_.", + " ", + " .. code-block:: shell-session", + " ", + ], + }, + { + "header": ["150", "5", "158", "4"], + "lines": [ + " * Twitter: `@codecov `_.", + " * Email: `hello@codecov.io `_.", + " ", + "-We are happy to help if you have any questions. Please contact email our Support at [support@codecov.io](mailto:support@codecov.io)", + "-", + "+We are happy to help if you have any questions. Please contact email our Support at `support@codecov.io `_.", + ], + }, + ], + "totals": True, + } + } + } + expected_annotations = [ + { + "path": "file_1.go", + "start_line": 2, + "end_line": 2, + "annotation_level": "warning", + "message": "Added line #L2 was not covered by tests", + }, + { + "path": "file_1.go", + "start_line": 10, + "end_line": 10, + "annotation_level": "warning", + "message": "Added line #L10 was not covered by tests", + }, + ] + annotations = notifier.create_annotations(sample_comparison, diff) + assert expected_annotations == annotations + + def test_get_lines_to_annotate_no_consecutive_lines(self, sample_comparison): + notifier = ChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=None, + ) + files_with_change = [ + { + "type": "modified", + "path": "file_1.go", + "additions": [ + {"head_line": 1}, + {"head_line": 2}, + {"head_line": 3}, + {"head_line": 5}, + {"head_line": 6}, + {"head_line": 8}, + ], + } + ] + expected_result = [ + { + "type": "new_line", + "line": 2, + "coverage": 0, + "path": "file_1.go", + "end_line": 2, + }, + { + "type": "new_line", + "line": 6, + "coverage": 0, + "path": "file_1.go", + "end_line": 6, + }, + ] + result = notifier.get_lines_to_annotate(sample_comparison, files_with_change) + assert expected_result == result + + def test_get_lines_to_annotate_consecutive_lines(self, sample_comparison): + notifier = ChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=None, + ) + report = Report() + first_deleted_file = ReportFile("file_1.go") + first_deleted_file.append(1, ReportLine.create(coverage=0)) + first_deleted_file.append(2, ReportLine.create(coverage=0)) + first_deleted_file.append(3, ReportLine.create(coverage=0)) + first_deleted_file.append(5, ReportLine.create(coverage=0)) + report.append(first_deleted_file) + sample_comparison.head.report = report + files_with_change = [ + { + "type": "modified", + "path": "file_1.go", + "additions": [ + {"head_line": 1}, + {"head_line": 2}, + {"head_line": 3}, + {"head_line": 5}, + {"head_line": 6}, + {"head_line": 8}, + ], + } + ] + expected_result = [ + { + "type": "new_line", + "line": 1, + "coverage": 0, + "path": "file_1.go", + "end_line": 3, + }, + { + "type": "new_line", + "line": 5, + "coverage": 0, + "path": "file_1.go", + "end_line": 5, + }, + ] + result = notifier.get_lines_to_annotate(sample_comparison, files_with_change) + assert expected_result == result + + +class TestPatchChecksNotifier(object): + def test_paginate_annotations( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = PatchChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["flagone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + sample_array = list(range(1, 61, 1)) + expected_result = [list(range(1, 51, 1)), list(range(51, 61, 1))] + result = list(notifier.paginate_annotations(sample_array)) + assert expected_result == result + + def test_build_flag_payload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = PatchChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["flagone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "state": "success", + "output": { + "title": "66.67% of diff hit (target 50.00%)", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_flag_payload/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n66.67% of diff hit (target 50.00%)", + "annotations": [], + }, + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_upgrade_payload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"] = { + "codecov_url": "test.example.br", + "codecov_dashboard_url": "test.example.br", + } + notifier = PatchChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + decoration_type=Decoration.upgrade, + ) + expected_result = { + "state": "success", + "output": { + "title": "Codecov Report", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_upgrade_payload/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\nThe author of this PR, codecov-test-user, is not an activated member of this organization on Codecov.\nPlease [activate this user on Codecov](test.example.br/members/gh/test_build_upgrade_payload) to display a detailed status check.\nCoverage data is still being uploaded to Codecov.io for purposes of overall coverage calculations.\nPlease don't hesitate to email us at support@codecov.io with any questions.", + "annotations": [], + }, + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + @pytest.mark.django_db + def test_build_default_payload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_all_plans_and_tiers() + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = PatchChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="default", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "state": "success", + "output": { + "title": "66.67% of diff hit (target 50.00%)", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_default_payload/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n66.67% of diff hit (target 50.00%)", + "annotations": [], + }, + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result["output"]["summary"] == result["output"]["summary"] + assert expected_result == result + + def test_build_payload_target_coverage_failure( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = PatchChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"target": "70%", "paths": ["pathone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "state": "failure", + "output": { + "title": "66.67% of diff hit (target 70.00%)", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_payload_target_coverage_failure/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n66.67% of diff hit (target 70.00%)", + "annotations": [], + }, + "included_helper_text": { + HelperTextKey.CUSTOM_TARGET_PATCH: HelperTextTemplate.CUSTOM_TARGET.format( + context="patch", + notification_type="check", + point_of_comparison="patch", + coverage=66.67, + target="70.00", + ) + }, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_payload_without_base_report( + self, + sample_comparison_without_base_report, + mock_repo_provider, + mock_configuration, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_without_base_report + notifier = PatchChecksNotifier( + repository=comparison.head.commit.repository, + title="default", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({"github_checks": {"annotations": True}}), + repository_service=mock_repo_provider, + ) + expected_result = { + "state": "success", + "output": { + "title": "No report found to compare against", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_payload_without_base_report/{sample_comparison_without_base_report.head.commit.repository.name}/pull/{sample_comparison_without_base_report.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\nNo report found to compare against", + "annotations": [ + { + "path": "file_1.go", + "start_line": 10, + "end_line": 10, + "annotation_level": "warning", + "message": "Added line #L10 was not covered by tests", + } + ], + }, + "included_helper_text": {}, + } + result = notifier.build_payload(comparison) + assert expected_result == result + + def test_build_payload_target_coverage_failure_within_threshold( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + third_file = ReportFile("file_3.c") + third_file.append(100, ReportLine.create(coverage=1, sessions=[[0, 1]])) + third_file.append(101, ReportLine.create(coverage=1, sessions=[[0, 1]])) + third_file.append(102, ReportLine.create(coverage=1, sessions=[[0, 1]])) + third_file.append(103, ReportLine.create(coverage=1, sessions=[[0, 1]])) + report = sample_comparison.project_coverage_base.report.inner_report + report.append(third_file) + sample_comparison.project_coverage_base.report = ( + ReadOnlyReport.create_from_report(report) + ) + notifier = PatchChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="default", + notifier_yaml_settings={"threshold": "5"}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "state": "success", + "output": { + "title": "66.67% of diff hit (within 5.00% threshold of 70.00%)", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_payload_target_coverage_failure_within_threshold/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n66.67% of diff hit (within 5.00% threshold of 70.00%)", + "annotations": [], + }, + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result["state"] == result["state"] + assert expected_result["output"]["summary"] == result["output"]["summary"] + assert expected_result["output"] == result["output"] + assert expected_result == result + + def test_build_payload_with_multiple_changes( + self, + comparison_with_multiple_changes, + mock_repo_provider, + mock_configuration, + multiple_diff_changes, + ): + json_diff = multiple_diff_changes + original_value = deepcopy(multiple_diff_changes) + mock_repo_provider.get_compare.return_value = {"diff": json_diff} + + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = PatchChecksNotifier( + repository=comparison_with_multiple_changes.head.commit.repository, + title="default", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "state": "failure", + "output": { + "title": "50.00% of diff hit (target 76.92%)", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_payload_with_multiple_changes/{comparison_with_multiple_changes.head.commit.repository.name}/pull/{comparison_with_multiple_changes.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n50.00% of diff hit (target 76.92%)", + "annotations": [], + }, + "included_helper_text": {}, # not a custom target, no helper text + } + result = notifier.build_payload(comparison_with_multiple_changes) + assert expected_result["state"] == result["state"] + assert expected_result["output"] == result["output"] + assert expected_result == result + # assert that the value of diff was not changed + for filename in original_value["files"]: + assert original_value["files"][filename].get( + "segments" + ) == multiple_diff_changes["files"][filename].get("segments") + + def test_github_checks_annotations_yaml( + self, + comparison_with_multiple_changes, + mock_repo_provider, + mock_configuration, + multiple_diff_changes, + ): + mock_repo_provider.get_compare.return_value = {"diff": multiple_diff_changes} + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + + empty_annotations = [] + + # checks_yaml_field can be None + notifier = PatchChecksNotifier( + repository=comparison_with_multiple_changes.head.commit.repository, + title="default", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + result = notifier.build_payload(comparison_with_multiple_changes) + assert empty_annotations == result["output"]["annotations"] + + # checks_yaml_field can be dict + notifier = PatchChecksNotifier( + repository=comparison_with_multiple_changes.head.commit.repository, + title="default", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({"github_checks": {"one": "two"}}), + repository_service=mock_repo_provider, + ) + result = notifier.build_payload(comparison_with_multiple_changes) + assert empty_annotations == result["output"]["annotations"] + + # checks_yaml_field can be bool + notifier = PatchChecksNotifier( + repository=comparison_with_multiple_changes.head.commit.repository, + title="default", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({"github_checks": False}), + repository_service=mock_repo_provider, + ) + result = notifier.build_payload(comparison_with_multiple_changes) + assert empty_annotations == result["output"]["annotations"] + + # checks_yaml_field with annotations + notifier = PatchChecksNotifier( + repository=comparison_with_multiple_changes.head.commit.repository, + title="default", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({"github_checks": {"annotations": True}}), + repository_service=mock_repo_provider, + ) + expected_annotations = [ + { + "path": "modified.py", + "start_line": 23, + "end_line": 23, + "annotation_level": "warning", + "message": "Added line #L23 was not covered by tests", + } + ] + result = notifier.build_payload(comparison_with_multiple_changes) + assert expected_annotations == result["output"]["annotations"] + + def test_build_payload_no_diff( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_repo_provider.get_compare.return_value = { + "diff": { + "files": { + "file_1.go": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["15", "8", "15", "9"], + "lines": [ + " Overview", + " --------", + " ", + "-Main website: `Codecov `_.", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + "+website: `Codecov `_.", + " ", + " .. code-block:: shell-session", + " ", + ], + } + ], + "stats": {"added": 11, "removed": 4}, + } + } + } + } + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = PatchChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["flagone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + assert notifier.is_enabled() + notifier.name + base_commit = sample_comparison.project_coverage_base.commit + head_commit = sample_comparison.head.commit + expected_result = { + "state": "success", + "output": { + "title": f"Coverage not affected when comparing {base_commit.commitid[:7]}...{head_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_payload_no_diff/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\nCoverage not affected when comparing {base_commit.commitid[:7]}...{head_commit.commitid[:7]}", + "annotations": [], + }, + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert notifier.notification_type.value == "checks_patch" + assert expected_result == result + + def test_send_notification(self, sample_comparison, mocker, mock_repo_provider): + comparison = sample_comparison + payload = { + "state": "success", + "output": {"title": "Codecov Report", "summary": "Summary"}, + "url": "https://app.codecov.io/gh/codecov/worker/compare/100?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=checks&utm_campaign=pr+comments&utm_term=codecov", + } + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + notifier = PatchChecksNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + result = notifier.send_notification(sample_comparison, payload) + assert result.notification_successful == True + assert result.explanation is None + assert result.data_sent == { + "state": "success", + "output": {"title": "Codecov Report", "summary": "Summary"}, + "url": "https://app.codecov.io/gh/codecov/worker/compare/100?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=checks&utm_campaign=pr+comments&utm_term=codecov", + } + + def test_send_notification_annotations_paginations( + self, sample_comparison, mocker, mock_repo_provider + ): + comparison = sample_comparison + payload = { + "state": "success", + "output": { + "title": "Codecov Report", + "summary": "Summary", + "annotations": list(range(1, 61, 1)), + }, + } + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + notifier = PatchChecksNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_calls = [ + { + "output": { + "title": "Codecov Report", + "summary": "Summary", + "annotations": list(range(1, 51, 1)), + }, + "url": None, + }, + { + "output": { + "title": "Codecov Report", + "summary": "Summary", + "annotations": list(range(51, 61, 1)), + }, + "url": None, + }, + ] + result = notifier.send_notification(sample_comparison, payload) + assert result.notification_successful == True + assert result.explanation is None + calls = [call[1] for call in mock_repo_provider.update_check_run.call_args_list] + assert expected_calls == calls + assert mock_repo_provider.update_check_run.call_count == 2 + assert result.data_sent == { + "state": "success", + "output": { + "title": "Codecov Report", + "summary": "Summary", + "annotations": list(range(1, 61, 1)), + }, + } + + def test_notify( + self, sample_comparison, mocker, mock_repo_provider, mock_configuration + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + notifier = PatchChecksNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"paths": ["pathone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = sample_comparison.project_coverage_base.commit + head_commit = sample_comparison.head.commit + result = notifier.notify(sample_comparison) + assert result.notification_successful == True + assert result.explanation is None + assert result.data_sent == { + "state": "success", + "output": { + "title": f"Coverage not affected when comparing {base_commit.commitid[:7]}...{head_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_notify/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\nCoverage not affected when comparing {base_commit.commitid[:7]}...{head_commit.commitid[:7]}", + "annotations": [], + }, + "included_helper_text": {}, + "url": f"test.example.br/gh/test_notify/{sample_comparison.head.commit.repository.name}/pull/{comparison.pull.pullid}", + } + + def test_notify_passing_empty_upload( + self, sample_comparison, mocker, mock_repo_provider, mock_configuration + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + notifier = PatchChecksNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"paths": ["pathone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + decoration_type=Decoration.passing_empty_upload, + ) + result = notifier.notify(sample_comparison) + assert result.notification_successful == True + assert result.explanation is None + assert result.data_sent == { + "state": "success", + "output": { + "title": "Empty Upload", + "summary": "Non-testable files changed.", + "annotations": [], + }, + "included_helper_text": {}, + "url": f"test.example.br/gh/test_notify_passing_empty_upload/{sample_comparison.head.commit.repository.name}/pull/{comparison.pull.pullid}", + } + + def test_notify_failing_empty_upload( + self, sample_comparison, mocker, mock_repo_provider, mock_configuration + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + notifier = PatchChecksNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"paths": ["pathone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + decoration_type=Decoration.failing_empty_upload, + ) + result = notifier.notify(sample_comparison) + assert result.notification_successful == True + assert result.explanation is None + assert result.data_sent == { + "state": "failure", + "output": { + "title": "Empty Upload", + "summary": "Testable files changed", + "annotations": [], + }, + "included_helper_text": {}, + "url": f"test.example.br/gh/test_notify_failing_empty_upload/{sample_comparison.head.commit.repository.name}/pull/{comparison.pull.pullid}", + } + + def test_notification_exception( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = PatchChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + + # Test exception handling when there's a TorngitClientError + mock_repo_provider.get_compare = Mock( + side_effect=TorngitClientGeneralError( + 400, response_data="Error", message="Error" + ) + ) + result = notifier.notify(sample_comparison) + assert result.notification_successful == False + assert result.explanation == "client_side_error_provider" + assert result.data_sent is None + + # Test exception handling when there's a TorngitError + mock_repo_provider.get_compare = Mock(side_effect=TorngitError()) + result = notifier.notify(sample_comparison) + assert result.notification_successful == False + assert result.explanation == "server_side_error_provider" + assert result.data_sent is None + + def test_notification_exception_not_fit(self, sample_comparison, mocker): + notifier = ChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=None, + ) + mocker.patch.object( + ChecksNotifier, "can_we_set_this_status", return_value=False + ) + result = notifier.notify(sample_comparison) + assert not result.notification_attempted + assert result.notification_successful is None + assert result.explanation == "not_fit_criteria" + assert result.data_sent is None + assert result.data_received is None + + def test_notification_exception_preexisting_commit_status( + self, sample_comparison, mocker, mock_repo_provider + ): + comparison = sample_comparison + notifier = ProjectChecksNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"paths": ["file_1.go"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + mock_repo_provider.get_commit_statuses.return_value = Status( + [ + { + "time": "2024-10-01T22:34:52Z", + "state": "success", + "description": "42.85% (+0.00%) compared to 36be7f3", + "context": "codecov/project/title", + } + ] + ) + result = notifier.notify(sample_comparison) + assert not result.notification_attempted + assert result.notification_successful is None + assert result.explanation == "preexisting_commit_status" + assert result.data_sent is None + assert result.data_received is None + + def test_checks_with_after_n_builds(self, sample_comparison, mocker): + notifier = ChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["unit"]}, + notifier_site_settings=True, + current_yaml=UserYaml( + { + "coverage": { + "status": {"project": True, "patch": True, "changes": True} + }, + "flag_management": { + "default_rules": {"carryforward": False}, + "individual_flags": [ + { + "name": "unit", + "statuses": [{"type": "patch"}], + "after_n_builds": 3, + }, + ], + }, + } + ), + repository_service=None, + ) + + mocker.patch.object(ChecksNotifier, "can_we_set_this_status", return_value=True) + result = notifier.notify(sample_comparison) + assert not result.notification_attempted + assert result.notification_successful is None + assert result.explanation == "need_more_builds" + assert result.data_sent is None + assert result.data_received is None + + +class TestChangesChecksNotifier(object): + def test_build_payload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ChangesChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "state": "success", + "output": { + "title": "No indirect coverage changes found", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_payload/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\nNo indirect coverage changes found", + "annotations": [], + }, + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + assert notifier.notification_type.value == "checks_changes" + + def test_build_upgrade_payload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"] = { + "codecov_url": "test.example.br", + "codecov_dashboard_url": "test.example.br", + } + notifier = ChangesChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + decoration_type=Decoration.upgrade, + ) + expected_result = { + "state": "success", + "output": { + "title": "Codecov Report", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_upgrade_payload/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\nThe author of this PR, codecov-test-user, is not an activated member of this organization on Codecov.\nPlease [activate this user on Codecov](test.example.br/members/gh/test_build_upgrade_payload) to display a detailed status check.\nCoverage data is still being uploaded to Codecov.io for purposes of overall coverage calculations.\nPlease don't hesitate to email us at support@codecov.io with any questions.", + "annotations": [], + }, + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_payload_with_multiple_changes( + self, + comparison_with_multiple_changes, + mock_repo_provider, + mock_configuration, + multiple_diff_changes, + ): + json_diff = multiple_diff_changes + mock_repo_provider.get_compare.return_value = {"diff": json_diff} + + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ChangesChecksNotifier( + repository=comparison_with_multiple_changes.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "state": "failure", + "output": { + "title": "3 files have indirect coverage changes not visible in diff", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_payload_with_multiple_changes/{comparison_with_multiple_changes.head.commit.repository.name}/pull/{comparison_with_multiple_changes.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n3 files have indirect coverage changes not visible in diff", + "annotations": [], + }, + "included_helper_text": {}, + } + result = notifier.build_payload(comparison_with_multiple_changes) + assert expected_result == result + + def test_build_payload_without_base_report( + self, + sample_comparison_without_base_report, + mock_repo_provider, + mock_configuration, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_without_base_report + notifier = ChangesChecksNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "state": "success", + "output": { + "title": "Unable to determine changes, no report found at pull request base", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_payload_without_base_report/{sample_comparison_without_base_report.head.commit.repository.name}/pull/{sample_comparison_without_base_report.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\nUnable to determine changes, no report found at pull request base", + "annotations": [], + }, + "included_helper_text": {}, + } + result = notifier.build_payload(comparison) + assert expected_result == result + + def test_build_failing_empty_upload_payload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"] = { + "codecov_url": "test.example.br", + "codecov_dashboard_url": "test.example.br", + } + notifier = ChangesChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + decoration_type=Decoration.failing_empty_upload, + ) + expected_result = { + "state": "failure", + "output": { + "title": "Empty Upload", + "summary": "Testable files changed", + "annotations": [], + }, + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + +class TestProjectChecksNotifier(object): + def test_analytics_url( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "codecov.io" + repo = sample_comparison.head.commit.repository + base_commit = sample_comparison.project_coverage_base.commit + head_commit = sample_comparison.head.commit + payload = { + "state": "success", + "output": { + "title": f"60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](codecov.io/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "text": "\n".join( + [ + f"## [Codecov](codecov.io/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + f"> Merging [#{sample_comparison.pull.pullid}](codecov.io/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?src=pr&el=desc) ({head_commit.commitid[:7]}) into [master](codecov.io/gh/test_build_default_payload/{repo.name}/commit/{sample_comparison.project_coverage_base.commit.commitid}?el=desc) ({base_commit.commitid[:7]}) will **increase** coverage by `10.00%`.", + "> The diff coverage is `66.67%`.", + "", + f"| [Files with missing lines](codecov.io/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?src=pr&el=tree) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [file\\_1.go](codecov.io/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `62.50% <66.67%> (+12.50%)` | `10.00 <0.00> (-1.00)` | :arrow_up: |", + f"| [file\\_2.py](codecov.io/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?src=pr&el=tree&filepath=file_2.py#diff-ZmlsZV8yLnB5) | `50.00% <0.00%> (ø)` | `0.00% <0.00%> (ø%)` | |", + "", + ] + ), + }, + } + notifier = ProjectChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="default", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={"comment": {"layout": "files"}}, + repository_service=mock_repo_provider, + ) + result = notifier.send_notification(sample_comparison, payload) + expected_result = { + "state": "success", + "output": { + "title": f"60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](codecov.io/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=checks&utm_campaign=pr+comments&utm_term={quote_plus(repo.owner.name)})\n\n60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "text": "\n".join( + [ + f"## [Codecov](codecov.io/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=checks&utm_campaign=pr+comments&utm_term={quote_plus(repo.owner.name)}) Report", + f"> Merging [#{sample_comparison.pull.pullid}](codecov.io/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=checks&utm_campaign=pr+comments&utm_term={quote_plus(repo.owner.name)}) ({head_commit.commitid[:7]}) into [master](codecov.io/gh/test_build_default_payload/{repo.name}/commit/{sample_comparison.project_coverage_base.commit.commitid}?el=desc&utm_medium=referral&utm_source=github&utm_content=checks&utm_campaign=pr+comments&utm_term={quote_plus(repo.owner.name)}) ({base_commit.commitid[:7]}) will **increase** coverage by `10.00%`.", + "> The diff coverage is `66.67%`.", + "", + f"| [Files with missing lines](codecov.io/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=checks&utm_campaign=pr+comments&utm_term={quote_plus(repo.owner.name)}) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [file\\_1.go](codecov.io/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?src=pr&el=tree&filepath=file_1.go&utm_medium=referral&utm_source=github&utm_content=checks&utm_campaign=pr+comments&utm_term={quote_plus(repo.owner.name)}#diff-ZmlsZV8xLmdv) | `62.50% <66.67%> (+12.50%)` | `10.00 <0.00> (-1.00)` | :arrow_up: |", + f"| [file\\_2.py](codecov.io/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?src=pr&el=tree&filepath=file_2.py&utm_medium=referral&utm_source=github&utm_content=checks&utm_campaign=pr+comments&utm_term={quote_plus(repo.owner.name)}#diff-ZmlsZV8yLnB5) | `50.00% <0.00%> (ø)` | `0.00% <0.00%> (ø%)` | |", + "", + ] + ), + }, + } + assert expected_result["output"]["text"].split("\n") == result.data_sent[ + "output" + ]["text"].split("\n") + assert expected_result == result.data_sent + + def test_build_flag_payload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["flagone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + result = notifier.build_payload(sample_comparison) + base_commit = sample_comparison.project_coverage_base.commit + expected_result = { + "state": "success", + "output": { + "title": f"60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_flag_payload/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "annotations": [], + }, + "included_helper_text": {}, + } + assert result == expected_result + assert notifier.notification_type.value == "checks_project" + + def test_build_upgrade_payload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"] = { + "codecov_url": "test.example.br", + "codecov_dashboard_url": "test.example.br", + } + notifier = ProjectChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + decoration_type=Decoration.upgrade, + ) + expected_result = { + "state": "success", + "output": { + "title": "Codecov Report", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_upgrade_payload/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\nThe author of this PR, codecov-test-user, is not an activated member of this organization on Codecov.\nPlease [activate this user on Codecov](test.example.br/members/gh/test_build_upgrade_payload) to display a detailed status check.\nCoverage data is still being uploaded to Codecov.io for purposes of overall coverage calculations.\nPlease don't hesitate to email us at support@codecov.io with any questions.", + "annotations": [], + }, + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_passing_empty_upload_payload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"] = { + "codecov_url": "test.example.br", + "codecov_dashboard_url": "test.example.br", + } + notifier = ProjectChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + decoration_type=Decoration.passing_empty_upload, + ) + expected_result = { + "state": "success", + "output": { + "title": "Empty Upload", + "summary": "Non-testable files changed.", + "annotations": [], + }, + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + @pytest.mark.django_db + def test_build_default_payload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_all_plans_and_tiers() + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="default", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={"comment": {"layout": "files"}}, + repository_service=mock_repo_provider, + ) + result = notifier.build_payload(sample_comparison) + repo = sample_comparison.head.commit.repository + base_commit = sample_comparison.project_coverage_base.commit + head_commit = sample_comparison.head.commit + expected_result = { + "state": "success", + "output": { + "title": f"60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "text": "\n".join( + [ + f"## [Codecov](test.example.br/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"> Project coverage is 60.00%. Comparing base [(`{base_commit.commitid[:7]}`)](test.example.br/gh/test_build_default_payload/{repo.name}/commit/{base_commit.commitid}?dropdown=coverage&el=desc) to head [(`{head_commit.commitid[:7]}`)](test.example.br/gh/test_build_default_payload/{repo.name}/commit/{head_commit.commitid}?dropdown=coverage&el=desc)." + f"", + "", + f"| [Files with missing lines](test.example.br/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | 66.67% | [1 Missing :warning: ](test.example.br/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?src=pr&el=tree) |", + "", + f"| [Files with missing lines](test.example.br/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=tree) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `62.50% <66.67%> (+12.50%)` | `10.00 <0.00> (-1.00)` | :arrow_up: |", + "", + f"... and [1 file with indirect coverage changes](test.example.br/gh/test_build_default_payload/{repo.name}/pull/{sample_comparison.pull.pullid}/indirect-changes?src=pr&el=tree-more)", + "", + ] + ), + "annotations": [], + }, + "included_helper_text": {}, + } + assert expected_result["output"]["text"].split("\n") == result["output"][ + "text" + ].split("\n") + assert expected_result == result + + @pytest.mark.django_db + def test_build_default_payload_with_flags( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_all_plans_and_tiers() + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="default", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={"comment": {"layout": "files, flags"}}, + repository_service=mock_repo_provider, + ) + result = notifier.build_payload(sample_comparison) + repo = sample_comparison.head.commit.repository + base_commit = sample_comparison.project_coverage_base.commit + head_commit = sample_comparison.head.commit + expected_result = { + "state": "success", + "output": { + "title": f"60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_default_payload_with_flags/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "text": "\n".join( + [ + f"## [Codecov](test.example.br/gh/test_build_default_payload_with_flags/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"> Project coverage is 60.00%. Comparing base [(`{base_commit.commitid[:7]}`)](test.example.br/gh/test_build_default_payload_with_flags/{repo.name}/commit/{base_commit.commitid}?dropdown=coverage&el=desc) to head [(`{head_commit.commitid[:7]}`)](test.example.br/gh/test_build_default_payload_with_flags/{repo.name}/commit/{head_commit.commitid}?dropdown=coverage&el=desc)." + f"", + "", + f"| [Files with missing lines](test.example.br/gh/test_build_default_payload_with_flags/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/test_build_default_payload_with_flags/{repo.name}/pull/{sample_comparison.pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | 66.67% | [1 Missing :warning: ](test.example.br/gh/test_build_default_payload_with_flags/{repo.name}/pull/{sample_comparison.pull.pullid}?src=pr&el=tree) |", + "", + f"| [Files with missing lines](test.example.br/gh/test_build_default_payload_with_flags/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=tree) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/test_build_default_payload_with_flags/{repo.name}/pull/{sample_comparison.pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `62.50% <66.67%> (+12.50%)` | `10.00 <0.00> (-1.00)` | :arrow_up: |", + "", + f"... and [1 file with indirect coverage changes](test.example.br/gh/test_build_default_payload_with_flags/{repo.name}/pull/{sample_comparison.pull.pullid}/indirect-changes?src=pr&el=tree-more)", + "", + ] + ), + "annotations": [], + }, + "included_helper_text": {}, + } + assert expected_result["output"]["text"].split("\n") == result["output"][ + "text" + ].split("\n") + assert expected_result == result + + @pytest.mark.django_db + def test_build_default_payload_with_flags_and_footer( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_all_plans_and_tiers() + test_name = "test_build_default_payload_with_flags_and_footer" + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="default", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={"comment": {"layout": "files, flags, footer"}}, + repository_service=mock_repo_provider, + ) + result = notifier.build_payload(sample_comparison) + repo = sample_comparison.head.commit.repository + base_commit = sample_comparison.project_coverage_base.commit + head_commit = sample_comparison.head.commit + expected_result = { + "state": "success", + "output": { + "title": f"60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/{test_name}/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "text": "\n".join( + [ + f"## [Codecov](test.example.br/gh/{test_name}/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"> Project coverage is 60.00%. Comparing base [(`{base_commit.commitid[:7]}`)](test.example.br/gh/{test_name}/{repo.name}/commit/{base_commit.commitid}?dropdown=coverage&el=desc) to head [(`{head_commit.commitid[:7]}`)](test.example.br/gh/{test_name}/{repo.name}/commit/{head_commit.commitid}?dropdown=coverage&el=desc).", + "", + f"| [Files with missing lines](test.example.br/gh/{test_name}/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{test_name}/{repo.name}/pull/{sample_comparison.pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | 66.67% | [1 Missing :warning: ](test.example.br/gh/{test_name}/{repo.name}/pull/{sample_comparison.pull.pullid}?src=pr&el=tree) |", + "", + f"| [Files with missing lines](test.example.br/gh/{test_name}/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=tree) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{test_name}/{repo.name}/pull/{sample_comparison.pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `62.50% <66.67%> (+12.50%)` | `10.00 <0.00> (-1.00)` | :arrow_up: |", + "", + f"... and [1 file with indirect coverage changes](test.example.br/gh/{test_name}/{repo.name}/pull/{sample_comparison.pull.pullid}/indirect-changes?src=pr&el=tree-more)", + "", + "------", + "", + f"[Continue to review full report in Codecov by Sentry](test.example.br/gh/{test_name}/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + f"> Powered by [Codecov](test.example.br/gh/{test_name}/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=footer). Last update [{base_commit.commitid[:7]}...{head_commit.commitid[:7]}](test.example.br/gh/{test_name}/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ], + ), + "annotations": [], + }, + "included_helper_text": {}, + } + assert expected_result["output"]["text"].split("\n") == result["output"][ + "text" + ].split("\n") + assert expected_result == result + + def test_build_default_payload_comment_off( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="default", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={"comment": False}, + repository_service=mock_repo_provider, + ) + result = notifier.build_payload(sample_comparison) + repo = sample_comparison.head.commit.repository + base_commit = sample_comparison.project_coverage_base.commit + expected_result = { + "state": "success", + "output": { + "title": f"60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_default_payload_comment_off/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "annotations": [], + }, + "included_helper_text": {}, + } + assert expected_result == result + + def test_build_default_payload_negative_change_comment_off( + self, sample_comparison_negative_change, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectChecksNotifier( + repository=sample_comparison_negative_change.head.commit.repository, + title="default", + notifier_yaml_settings={"removed_code_behavior": "removals_only"}, + notifier_site_settings=True, + current_yaml={"comment": False}, + repository_service=mock_repo_provider, + ) + result = notifier.build_payload(sample_comparison_negative_change) + repo = sample_comparison_negative_change.head.commit.repository + base_commit = sample_comparison_negative_change.project_coverage_base.commit + expected_result = { + "state": "failure", + "output": { + "title": f"50.00% (-10.00%) compared to {base_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_default_payload_negative_change_comment_off/{repo.name}/pull/{sample_comparison_negative_change.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n50.00% (-10.00%) compared to {base_commit.commitid[:7]}", + "annotations": [], + }, + "included_helper_text": {}, + } + assert expected_result == result + + def test_build_payload_not_auto( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"target": "57%", "flags": ["flagone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + repo = sample_comparison.head.commit.repository + expected_result = { + "state": "success", + "output": { + "title": "60.00% (target 57.00%)", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_payload_not_auto/{repo.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n60.00% (target 57.00%)", + "annotations": [], + }, + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_payload_no_base_report( + self, + sample_comparison_without_base_report, + mock_repo_provider, + mock_configuration, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_without_base_report + notifier = ProjectChecksNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["flagone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + repo = sample_comparison_without_base_report.head.commit.repository + expected_result = { + "state": "success", + "output": { + "title": "No report found to compare against", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_build_payload_no_base_report/{repo.name}/pull/{sample_comparison_without_base_report.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\nNo report found to compare against", + "annotations": [], + }, + "included_helper_text": {}, + } + result = notifier.build_payload(comparison) + assert expected_result == result + + def test_check_notify_no_path_match( + self, sample_comparison, mocker, mock_repo_provider, mock_configuration + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + + notifier = ProjectChecksNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"paths": ["pathone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + result = notifier.notify(sample_comparison) + assert result.notification_successful == True + assert result.explanation is None + assert result.data_sent == { + "state": "success", + "output": { + "title": "No coverage information found on head", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_check_notify_no_path_match/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\nNo coverage information found on head", + "annotations": [], + }, + "included_helper_text": {}, + "url": f"test.example.br/gh/test_check_notify_no_path_match/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}", + } + + def test_check_notify_single_path_match( + self, sample_comparison, mocker, mock_repo_provider, mock_configuration + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + + notifier = ProjectChecksNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"paths": ["file_1.go"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + + base_commit = sample_comparison.project_coverage_base.commit + result = notifier.notify(sample_comparison) + assert result.notification_successful is True + assert result.explanation is None + expected_result = { + "state": "success", + "output": { + "title": f"62.50% (+12.50%) compared to {base_commit.commitid[0:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_check_notify_single_path_match/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n62.50% (+12.50%) compared to {base_commit.commitid[0:7]}", + "annotations": [], + }, + "included_helper_text": {}, + "url": f"test.example.br/gh/test_check_notify_single_path_match/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}", + } + assert result.data_sent["state"] == expected_result["state"] + assert ( + result.data_sent["output"]["summary"] + == expected_result["output"]["summary"] + ) + assert result.data_sent["output"] == expected_result["output"] + + def test_check_notify_multiple_path_match( + self, sample_comparison, mocker, mock_repo_provider, mock_configuration + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + + notifier = ProjectChecksNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"paths": ["file_2.py", "file_1.go"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + + base_commit = sample_comparison.project_coverage_base.commit + result = notifier.notify(sample_comparison) + assert result.notification_successful == True + assert result.explanation is None + assert result.data_sent == { + "state": "success", + "output": { + "title": f"60.00% (+10.00%) compared to {base_commit.commitid[0:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_check_notify_multiple_path_match/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n60.00% (+10.00%) compared to {base_commit.commitid[0:7]}", + "annotations": [], + }, + "included_helper_text": {}, + "url": f"test.example.br/gh/test_check_notify_multiple_path_match/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}", + } + + def test_check_notify_with_paths( + self, sample_comparison, mocker, mock_repo_provider, mock_configuration + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + payload = { + "state": "success", + "output": {"title": "Codecov Report", "summary": "Summary"}, + } + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + + notifier = ProjectChecksNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = sample_comparison.project_coverage_base.commit + result = notifier.notify(sample_comparison) + assert result.notification_successful == True + assert result.explanation is None + assert result.data_sent == { + "state": "success", + "output": { + "title": f"60.00% (+10.00%) compared to {base_commit.commitid[0:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_check_notify_with_paths/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n60.00% (+10.00%) compared to {base_commit.commitid[0:7]}", + "annotations": [], + }, + "included_helper_text": {}, + "url": f"test.example.br/gh/test_check_notify_with_paths/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}", + } + + def test_notify_pass_behavior_when_coverage_not_uploaded( + self, + sample_comparison_coverage_carriedforward, + mock_repo_provider, + mock_configuration, + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + notifier = ProjectChecksNotifier( + repository=sample_comparison_coverage_carriedforward.head.commit.repository, + title="title", + notifier_yaml_settings={ + "flag_coverage_not_uploaded_behavior": "pass", + "flags": ["integration", "missing"], + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = ( + sample_comparison_coverage_carriedforward.project_coverage_base.commit + ) + head_commit = sample_comparison_coverage_carriedforward.head.commit + + expected_result = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "state": "success", + "output": { + "title": f"25.00% (+0.00%) compared to {base_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/{head_commit.repository.owner.username}/{head_commit.repository.name}/pull/{sample_comparison_coverage_carriedforward.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n25.00% (+0.00%) compared to {base_commit.commitid[:7]} [Auto passed due to carriedforward or missing coverage]", + "annotations": [], + }, + "included_helper_text": {}, + "url": f"test.example.br/gh/{head_commit.repository.owner.username}/{head_commit.repository.name}/pull/{sample_comparison_coverage_carriedforward.pull.pullid}", + }, + ) + result = notifier.notify(sample_comparison_coverage_carriedforward) + assert ( + expected_result.data_sent["output"]["summary"] + == result.data_sent["output"]["summary"] + ) + assert expected_result.data_sent["output"] == result.data_sent["output"] + assert expected_result == result + + def test_notify_pass_behavior_when_coverage_uploaded( + self, + sample_comparison_coverage_carriedforward, + mock_repo_provider, + mock_configuration, + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + notifier = ProjectChecksNotifier( + repository=sample_comparison_coverage_carriedforward.head.commit.repository, + title="title", + notifier_yaml_settings={ + "flag_coverage_not_uploaded_behavior": "pass", + "flags": ["unit"], + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = ( + sample_comparison_coverage_carriedforward.project_coverage_base.commit + ) + head_commit = sample_comparison_coverage_carriedforward.head.commit + expected_result = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "state": "success", + "output": { + "title": f"25.00% (+0.00%) compared to {base_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/{head_commit.repository.owner.username}/{head_commit.repository.name}/pull/{sample_comparison_coverage_carriedforward.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n25.00% (+0.00%) compared to {base_commit.commitid[:7]}", + "annotations": [], + }, + "included_helper_text": {}, + "url": f"test.example.br/gh/{head_commit.repository.owner.username}/{head_commit.repository.name}/pull/{sample_comparison_coverage_carriedforward.pull.pullid}", + }, + ) + result = notifier.notify(sample_comparison_coverage_carriedforward) + assert expected_result == result + + def test_notify_include_behavior_when_coverage_not_uploaded( + self, + sample_comparison_coverage_carriedforward, + mock_repo_provider, + mock_configuration, + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + notifier = ProjectChecksNotifier( + repository=sample_comparison_coverage_carriedforward.head.commit.repository, + title="title", + notifier_yaml_settings={ + "flag_coverage_not_uploaded_behavior": "include", + "flags": ["integration", "enterprise"], + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = ( + sample_comparison_coverage_carriedforward.project_coverage_base.commit + ) + head_commit = sample_comparison_coverage_carriedforward.head.commit + + expected_result = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "state": "success", + "output": { + "title": f"36.17% (+0.00%) compared to {base_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/{head_commit.repository.owner.username}/{head_commit.repository.name}/pull/{sample_comparison_coverage_carriedforward.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n36.17% (+0.00%) compared to {base_commit.commitid[:7]}", + "annotations": [], + }, + "included_helper_text": {}, + "url": f"test.example.br/gh/{head_commit.repository.owner.username}/{head_commit.repository.name}/pull/{sample_comparison_coverage_carriedforward.pull.pullid}", + }, + ) + result = notifier.notify(sample_comparison_coverage_carriedforward) + assert expected_result == result + + def test_notify_exclude_behavior_when_coverage_not_uploaded( + self, + sample_comparison_coverage_carriedforward, + mock_repo_provider, + mock_configuration, + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + notifier = ProjectChecksNotifier( + repository=sample_comparison_coverage_carriedforward.head.commit.repository, + title="title", + notifier_yaml_settings={ + "flag_coverage_not_uploaded_behavior": "exclude", + "flags": ["missing"], + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="exclude_flag_coverage_not_uploaded_checks", + data_sent=None, + data_received=None, + ) + result = notifier.notify(sample_comparison_coverage_carriedforward) + assert expected_result == result + + def test_notify_exclude_behavior_when_coverage_uploaded( + self, + sample_comparison_coverage_carriedforward, + mock_repo_provider, + mock_configuration, + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + notifier = ProjectChecksNotifier( + repository=sample_comparison_coverage_carriedforward.head.commit.repository, + title="title", + notifier_yaml_settings={ + "flag_coverage_not_uploaded_behavior": "exclude", + "flags": ["unit"], + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = ( + sample_comparison_coverage_carriedforward.project_coverage_base.commit + ) + head_commit = sample_comparison_coverage_carriedforward.head.commit + + expected_result = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "state": "success", + "output": { + "title": f"25.00% (+0.00%) compared to {base_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/{head_commit.repository.owner.username}/{head_commit.repository.name}/pull/{sample_comparison_coverage_carriedforward.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n25.00% (+0.00%) compared to {base_commit.commitid[:7]}", + "annotations": [], + }, + "included_helper_text": {}, + "url": f"test.example.br/gh/{head_commit.repository.owner.username}/{head_commit.repository.name}/pull/{sample_comparison_coverage_carriedforward.pull.pullid}", + }, + ) + result = notifier.notify(sample_comparison_coverage_carriedforward) + assert expected_result == result + + def test_notify_exclude_behavior_when_some_coverage_uploaded( + self, + sample_comparison_coverage_carriedforward, + mock_repo_provider, + mock_configuration, + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + notifier = ProjectChecksNotifier( + repository=sample_comparison_coverage_carriedforward.head.commit.repository, + title="title", + notifier_yaml_settings={ + "flag_coverage_not_uploaded_behavior": "exclude", + "flags": [ + "unit", + "missing", + "integration", + ], # only "unit" was uploaded, but this should still notify + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = ( + sample_comparison_coverage_carriedforward.project_coverage_base.commit + ) + head_commit = sample_comparison_coverage_carriedforward.head.commit + + expected_result = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "state": "success", + "output": { + "title": f"36.17% (+0.00%) compared to {base_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/{head_commit.repository.owner.username}/{head_commit.repository.name}/pull/{sample_comparison_coverage_carriedforward.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n36.17% (+0.00%) compared to {base_commit.commitid[:7]}", + "annotations": [], + }, + "included_helper_text": {}, + "url": f"test.example.br/gh/{head_commit.repository.owner.username}/{head_commit.repository.name}/pull/{sample_comparison_coverage_carriedforward.pull.pullid}", + }, + ) + result = notifier.notify(sample_comparison_coverage_carriedforward) + assert expected_result == result + + def test_notify_exclude_behavior_no_flags( + self, + sample_comparison_coverage_carriedforward, + mock_repo_provider, + mock_configuration, + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + notifier = ProjectChecksNotifier( + repository=sample_comparison_coverage_carriedforward.head.commit.repository, + title="title", + notifier_yaml_settings={ + "flag_coverage_not_uploaded_behavior": "exclude", + "flags": None, + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = ( + sample_comparison_coverage_carriedforward.project_coverage_base.commit + ) + head_commit = sample_comparison_coverage_carriedforward.head.commit + + # should send the check as normal if there are no flags + expected_result = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "state": "success", + "output": { + "title": f"65.38% (+0.00%) compared to {base_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/{head_commit.repository.owner.username}/{head_commit.repository.name}/pull/{sample_comparison_coverage_carriedforward.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n65.38% (+0.00%) compared to {base_commit.commitid[:7]}", + "annotations": [], + }, + "included_helper_text": {}, + "url": f"test.example.br/gh/{head_commit.repository.owner.username}/{head_commit.repository.name}/pull/{sample_comparison_coverage_carriedforward.pull.pullid}", + }, + ) + result = notifier.notify(sample_comparison_coverage_carriedforward) + assert ( + expected_result.data_sent["output"]["summary"] + == result.data_sent["output"]["summary"] + ) + assert expected_result.data_sent["output"] == result.data_sent["output"] + assert expected_result.data_sent == result.data_sent + assert expected_result == result + + def test_build_payload_comments_true(self, sample_comparison, mock_configuration): + base_commit = sample_comparison.project_coverage_base.commit + head_commit = sample_comparison.head.commit + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings={}, + current_yaml={"comment": True}, + repository_service=None, + ) + res = notifier.build_payload(sample_comparison) + assert res == { + "state": "success", + "output": { + "title": f"60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/{head_commit.repository.owner.username}/{head_commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "annotations": [], + }, + "included_helper_text": {}, + } + + def test_build_payload_comments_false(self, sample_comparison, mock_configuration): + base_commit = sample_comparison.project_coverage_base.commit + head_commit = sample_comparison.head.commit + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings={}, + current_yaml={"comment": False}, + repository_service=None, + ) + res = notifier.build_payload(sample_comparison) + assert res == { + "state": "success", + "output": { + "title": f"60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/{head_commit.repository.owner.username}/{head_commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "annotations": [], + }, + "included_helper_text": {}, + } diff --git a/apps/worker/services/notification/notifiers/tests/unit/test_codecov_slack_app.py b/apps/worker/services/notification/notifiers/tests/unit/test_codecov_slack_app.py new file mode 100644 index 0000000000..cb6e6919ee --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/unit/test_codecov_slack_app.py @@ -0,0 +1,156 @@ +from unittest.mock import patch + +from database.enums import Notification +from services.notification.notifiers.codecov_slack_app import CodecovSlackAppNotifier + + +class TestCodecovSlackAppNotifier(object): + def test_is_enabled(self, dbsession, mock_configuration, sample_comparison): + notifier = CodecovSlackAppNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"enabled": True}, + notifier_site_settings=True, + current_yaml={"slack_app": {"enabled": True}}, + repository_service=None, + ) + assert notifier.is_enabled() == True + + def test_is_enable_false(self, dbsession, mock_configuration, sample_comparison): + notifier = CodecovSlackAppNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"enabled": False}, + notifier_site_settings=True, + current_yaml={"slack_app": {"enabled": False}}, + repository_service=None, + ) + + assert notifier.is_enabled() is False + + def test_notification_type(self, dbsession, mock_configuration, sample_comparison): + notifier = CodecovSlackAppNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"enabled": True}, + notifier_site_settings=True, + current_yaml={"slack_app": {"enabled": True}}, + repository_service=None, + ) + assert notifier.notification_type == Notification.codecov_slack_app + + @patch("requests.post") + def test_notify( + self, mock_requests_post, dbsession, mock_configuration, sample_comparison + ): + mock_requests_post.return_value.status_code = 200 + notifier = CodecovSlackAppNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"enabled": True}, + notifier_site_settings=True, + current_yaml={"slack_app": {"enabled": True}}, + repository_service=None, + ) + result = notifier.notify(sample_comparison) + assert result.notification_successful == True + assert result.explanation == "Successfully notified slack app" + + @patch("requests.post") + def test_notify_failure( + self, mock_requests_post, dbsession, mock_configuration, sample_comparison + ): + mock_requests_post.return_value.status_code = 500 + mock_requests_post.return_value.reason = "Internal Server Error" + notifier = CodecovSlackAppNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"enabled": True}, + notifier_site_settings=True, + current_yaml={"slack_app": {"enabled": True}}, + repository_service=None, + ) + result = notifier.notify(sample_comparison) + assert result.notification_successful == False + assert ( + result.explanation + == "Failed to notify slack app\nError 500: Internal Server Error." + ) + + @patch("requests.post") + def test_notify_request_being_called( + self, mock_requests_post, dbsession, mock_configuration, sample_comparison + ): + mock_requests_post.return_value.status_code = 200 + notifier = CodecovSlackAppNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"enabled": True}, + notifier_site_settings=True, + current_yaml={"slack_app": {"enabled": True}}, + repository_service=None, + ) + result = notifier.notify(sample_comparison) + assert result.notification_successful == True + assert mock_requests_post.call_count == 1 + assert mock_requests_post.is_called_with( + { + "repository": "sing-outside-letter", + "owner": "test_notify", + "comparison": { + "url": "https://app.codecov.io/gh/test_notify/sing-outside-letter/pull/24", + "message": "increased", + "coverage": "10.00", + "notation": "+", + "head_commit": { + "commitid": "936b768a1057bbe8371083ab4ec96a196ec730b6", + "branch": "new_branch", + "message": "Company here customer page by player threat.", + "author": "benjaminford", + "timestamp": "2019-02-01T17:59:47", + "ci_passed": True, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "85.00000", + "d": 0, + "diff": [1, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + "f": 3, + "h": 17, + "m": 3, + "n": 20, + "p": 0, + "s": 1, + }, + "pull": 1, + }, + "base_commit": { + "commitid": "db89f0ead214b647fe7eef9e1c42b78279e234bb", + "branch": None, + "message": "Morning loss contain impact old.", + "author": "stewartbrendan", + "timestamp": "2019-02-01T17:59:47", + "ci_passed": True, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "85.00000", + "d": 0, + "diff": [1, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + "f": 3, + "h": 17, + "m": 3, + "n": 20, + "p": 0, + "s": 1, + }, + "pull": 1, + }, + "head_totals_c": "60.00000", + }, + } + ) diff --git a/apps/worker/services/notification/notifiers/tests/unit/test_comment.py b/apps/worker/services/notification/notifiers/tests/unit/test_comment.py new file mode 100644 index 0000000000..0e09252092 --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/unit/test_comment.py @@ -0,0 +1,5088 @@ +from datetime import datetime, timezone +from decimal import Decimal +from typing import List +from unittest.mock import PropertyMock + +import pytest +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName +from shared.reports.readonly import ReadOnlyReport +from shared.reports.resources import Report, ReportFile +from shared.reports.types import Change, LineSession, ReportLine, ReportTotals +from shared.torngit.exceptions import ( + TorngitClientError, + TorngitClientGeneralError, + TorngitObjectNotFoundError, + TorngitServerUnreachableError, +) +from shared.utils.sessions import Session +from shared.validation.types import CoverageCommentRequiredChanges + +from database.models.core import Commit, GithubAppInstallation, Pull, Repository +from database.tests.factories import RepositoryFactory +from database.tests.factories.core import CommitFactory, OwnerFactory, PullFactory +from services.comparison import ComparisonContext, ComparisonProxy +from services.comparison.types import Comparison, FullCommit, ReportUploadedCount +from services.decoration import Decoration +from services.notification.notifiers.base import NotificationResult +from services.notification.notifiers.comment import CommentNotifier +from services.notification.notifiers.mixins.message.helpers import ( + diff_to_string, + format_number_to_str, + sort_by_importance, +) +from services.notification.notifiers.mixins.message.sections import ( + AnnouncementSectionWriter, + ComponentsSectionWriter, + FileSectionWriter, + HeaderSectionWriter, + MessagesToUserSectionWriter, + NewFilesSectionWriter, + NewFooterSectionWriter, + _get_tree_cell, +) +from services.notification.notifiers.tests.conftest import generate_sample_comparison +from services.repository import EnrichedPull +from services.yaml.reader import get_components_from_yaml +from tests.helpers import mock_all_plans_and_tiers + + +@pytest.fixture +def is_not_first_pull(mocker): + mocker.patch( + "database.models.core.Pull.is_first_coverage_pull", + return_value=False, + new_callable=PropertyMock, + ) + + +@pytest.fixture +def sample_comparison_bunch_empty_flags(request, dbsession, mocker): + """ + This is what this fixture has regarding to flags + - first is on both reports, both with 100% coverage (the common already existing result) + - second is declared on both reports, but is only on the head report, with 50% coverage + - third is declared on both reports, but only has coverage information on the base report, + with 50% coverage + - fourth is declared on both reports, and has no coverage information anywhere + - fifthis only declared at the head report, but has no coverage information there + - sixth is only declared at the head report, and has information: 100% coverage there + - seventh is only declared at the base report, but has no coverage information there + - eighth is only declared at the base report, and has 100% coverage there + """ + head_report = Report() + head_report.add_session(Session(flags=["first"])) + head_report.add_session(Session(flags=["second"])) + head_report.add_session(Session(flags=["third"])) + head_report.add_session(Session(flags=["fourth"])) + head_report.add_session(Session(flags=["fifth"])) + head_report.add_session(Session(flags=["sixth"])) + base_report = Report() + base_report.add_session(Session(flags=["first"])) + base_report.add_session(Session(flags=["second"])) + base_report.add_session(Session(flags=["third"])) + base_report.add_session(Session(flags=["fourth"])) + base_report.add_session(Session(flags=["seventh"])) + base_report.add_session(Session(flags=["eighth"])) + # assert False + file_1 = ReportFile("space.py") + file_1.append( + 1, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 1), LineSession(1, "1/2")] + ), + ) + file_1.append( + 40, + ReportLine.create(coverage=1, sessions=[LineSession(5, 1), LineSession(1, 1)]), + ) + head_report.append(file_1) + file_2 = ReportFile("jupiter.py") + file_2.append( + 1, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 1), LineSession(2, "1/2")] + ), + ) + file_2.append( + 40, + ReportLine.create(coverage=1, sessions=[LineSession(5, 1), LineSession(2, 1)]), + ) + base_report.append(file_2) + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + return_value="github-integration-token", + ) + return generate_sample_comparison( + request.node.name, + dbsession, + ReadOnlyReport.create_from_report(base_report), + ReadOnlyReport.create_from_report(head_report), + ) + + +@pytest.fixture +def mock_repo_provider(mock_repo_provider): + compare_result = { + "diff": { + "files": { + "file_1.go": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["5", "8", "5", "9"], + "lines": [ + " Overview", + " --------", + " ", + "-Main website: `Codecov `_.", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + "+website: `Codecov `_.", + " ", + " .. code-block:: shell-session", + " ", + ], + }, + { + "header": ["46", "12", "47", "19"], + "lines": [ + " ", + " You may need to configure a ``.coveragerc`` file. Learn more `here `_. Start with this `generic .coveragerc `_ for example.", + " ", + "-We highly suggest adding `source` to your ``.coveragerc`` which solves a number of issues collecting coverage.", + "+We highly suggest adding ``source`` to your ``.coveragerc``, which solves a number of issues collecting coverage.", + " ", + " .. code-block:: ini", + " ", + " [run]", + " source=your_package_name", + "+ ", + "+If there are multiple sources, you instead should add ``include`` to your ``.coveragerc``", + "+", + "+.. code-block:: ini", + "+", + "+ [run]", + "+ include=your_package_name/*", + " ", + " unittests", + " ---------", + ], + }, + { + "header": ["150", "5", "158", "4"], + "lines": [ + " * Twitter: `@codecov `_.", + " * Email: `hello@codecov.io `_.", + " ", + "-We are happy to help if you have any questions. Please contact email our Support at [support@codecov.io](mailto:support@codecov.io)", + "-", + "+We are happy to help if you have any questions. Please contact email our Support at `support@codecov.io `_.", + ], + }, + ], + "stats": {"added": 11, "removed": 4}, + } + } + }, + "commits": [ + { + "commitid": "{comparison.project_coverage_base.commit[:7]}44fdd29fcc506317cc3ddeae1a723dd08", + "message": "Update README.rst", + "timestamp": "2018-07-09T23:51:16Z", + "author": { + "id": 8398772, + "username": "jerrode", + "name": "Jerrod", + "email": "jerrod@fundersclub.com", + }, + }, + { + "commitid": "6ae5f1795a441884ed2847bb31154814ac01ef38", + "message": "Update README.rst", + "timestamp": "2018-04-26T08:35:58Z", + "author": { + "id": 11602092, + "username": "TomPed", + "name": "Thomas Pedbereznak", + "email": "tom@tomped.com", + }, + }, + ], + } + + branch_result = {"name": "test", "sha": "aaaaaaa"} + + mock_repo_provider.get_compare.return_value = compare_result + mock_repo_provider.post_comment.return_value = {} + mock_repo_provider.edit_comment.return_value = {} + mock_repo_provider.delete_comment.return_value = {} + mock_repo_provider.get_branch.side_effect = TorngitClientGeneralError( + 404, None, "Branch not found" + ) + return mock_repo_provider + + +class TestCommentNotifierHelpers(object): + @pytest.mark.django_db + def test_sort_by_importance(self): + modified_change = Change( + path="modified.py", + in_diff=True, + totals=ReportTotals( + files=0, + lines=0, + hits=-2, + misses=1, + partials=0, + coverage=-23.333330000000004, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ) + renamed_with_changes_change = Change( + path="renamed_with_changes.py", + in_diff=True, + old_path="old_renamed_with_changes.py", + totals=ReportTotals( + files=0, + lines=0, + hits=-1, + misses=1, + partials=0, + coverage=-20.0, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ) + unrelated_change = Change( + path="unrelated.py", + in_diff=False, + totals=ReportTotals( + files=0, + lines=0, + hits=-3, + misses=2, + partials=0, + coverage=-43.333330000000004, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ) + added_change = Change( + path="added.py", new=True, in_diff=None, old_path=None, totals=None + ) + deleted_change = Change(path="deleted.py", deleted=True) + changes = [ + modified_change, + renamed_with_changes_change, + unrelated_change, + added_change, + deleted_change, + ] + res = sort_by_importance(changes) + expected_result = [ + unrelated_change, + modified_change, + renamed_with_changes_change, + deleted_change, + added_change, + ] + assert expected_result == res + + @pytest.mark.django_db + def test_format_number_to_str(self): + assert "<0.1" == format_number_to_str( + {"coverage": {"precision": 1}}, Decimal("0.001") + ) + assert "10.0" == format_number_to_str( + {"coverage": {"precision": 1}}, Decimal("10.001") + ) + assert "10.1" == format_number_to_str( + {"coverage": {"precision": 1, "round": "up"}}, Decimal("10.001") + ) + + @pytest.mark.django_db + def test_diff_to_string_case_1(self): + case_1 = ( + "master", + ReportTotals(10), + "stable", + ReportTotals(11), + [ + "@@ Coverage Diff @@", + "## master stable +/- ##", + "====================================", + "====================================", + " Files 10 11 +1 ", + "", + ], + ) + case = case_1 + base_title, base_totals, head_title, head_totals, expected_result = case + diff = diff_to_string({}, base_title, base_totals, head_title, head_totals) + assert diff == expected_result + + @pytest.mark.django_db + def test_diff_to_string_case_2(self): + case_2 = ( + "master", + ReportTotals(files=10, coverage="12.0", complexity="10.0"), + "stable", + ReportTotals(files=10, coverage="15.0", complexity="9.0"), + [ + "@@ Coverage Diff @@", + "## master stable +/- ##", + "============================================", + "+ Coverage 12.00% 15.00% +3.00% ", + "- Complexity 10.00% 9.00% -1.00% ", + "============================================", + " Files 10 10 ", + "", + ], + ) + case = case_2 + base_title, base_totals, head_title, head_totals, expected_result = case + diff = diff_to_string({}, base_title, base_totals, head_title, head_totals) + assert diff == expected_result + + @pytest.mark.django_db + def test_diff_to_string_case_3(self): + case_3 = ( + "master", + ReportTotals(files=100), + "#1", + ReportTotals(files=200, lines=2, hits=6, misses=7, partials=8, branches=3), + [ + "@@ Coverage Diff @@", + "## master #1 +/- ##", + "=====================================", + "=====================================", + " Files 100 200 +100 ", + " Lines 0 2 +2 ", + " Branches 0 3 +3 ", + "=====================================", + "+ Hits 0 6 +6 ", + "- Misses 0 7 +7 ", + "- Partials 0 8 +8 ", + ], + ) + case = case_3 + base_title, base_totals, head_title, head_totals, expected_result = case + diff = diff_to_string({}, base_title, base_totals, head_title, head_totals) + assert diff == expected_result + + @pytest.mark.django_db + def test_diff_to_string_case_4(self): + case_4 = ( + "master", + ReportTotals(files=10, coverage="12.0", complexity=10), + "stable", + ReportTotals(files=10, coverage="15.0", complexity=9), + [ + "@@ Coverage Diff @@", + "## master stable +/- ##", + "============================================", + "+ Coverage 12.00% 15.00% +3.00% ", + "+ Complexity 10 9 -1 ", + "============================================", + " Files 10 10 ", + "", + ], + ) + case = case_4 + base_title, base_totals, head_title, head_totals, expected_result = case + diff = diff_to_string({}, base_title, base_totals, head_title, head_totals) + assert diff == expected_result + + @pytest.mark.django_db + def test_diff_to_string_case_different_types(self): + case_1 = ( + "master", + ReportTotals(10, coverage="54.43"), + "stable", + ReportTotals(11, coverage=0), + [ + "@@ Coverage Diff @@", + "## master stable +/- ##", + "===========================================", + "- Coverage 54.43% 0 -54.43% ", + "===========================================", + " Files 10 11 +1 ", + "", + ], + ) + case = case_1 + base_title, base_totals, head_title, head_totals, expected_result = case + diff = diff_to_string({}, base_title, base_totals, head_title, head_totals) + assert diff == expected_result + + +@pytest.mark.usefixtures("is_not_first_pull") +class TestCommentNotifier(object): + @pytest.fixture(autouse=True) + def setup(self): + mock_all_plans_and_tiers() + + @pytest.mark.django_db + def test_is_enabled_settings_individual_settings_false(self, dbsession): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + notifier = CommentNotifier( + repository=repository, + title="some_title", + notifier_yaml_settings=False, + notifier_site_settings=None, + current_yaml={}, + repository_service=None, + ) + assert not notifier.is_enabled() + + @pytest.mark.django_db + def test_is_enabled_settings_individual_settings_none(self, dbsession): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + notifier = CommentNotifier( + repository=repository, + title="some_title", + notifier_yaml_settings=None, + notifier_site_settings=None, + current_yaml={}, + repository_service=None, + ) + assert not notifier.is_enabled() + + @pytest.mark.django_db + def test_is_enabled_settings_individual_settings_true(self, dbsession): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + notifier = CommentNotifier( + repository=repository, + title="some_title", + notifier_yaml_settings=True, + notifier_site_settings=None, + current_yaml={}, + repository_service=None, + ) + assert not notifier.is_enabled() + + @pytest.mark.django_db + def test_is_enabled_settings_individual_settings_dict(self, dbsession): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + notifier = CommentNotifier( + repository=repository, + title="some_title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=None, + current_yaml={}, + repository_service=None, + ) + assert notifier.is_enabled() + + @pytest.mark.django_db + def test_create_message_files_section( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison, + mocker, + ): + comparison = sample_comparison + mocker.patch.object(comparison, "get_behind_by", return_value=0) + mocker.patch.object( + comparison, + "get_diff", + return_value={ + "files": { + "file_1.go": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["105", "8", "105", "9"], + "lines": [ + " Overview", + " --------", + " ", + "-Main website: `Codecov `_.", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + "+website: `Codecov `_.", + " ", + " .. code-block:: shell-session", + " ", + ], + }, + { + "header": ["1046", "12", "1047", "19"], + "lines": [ + " ", + " You may need to configure a ``.coveragerc`` file. Learn more", + " ", + "-We highly suggest adding `source` to your ``.coveragerc``", + "+We highly suggest adding ``source`` to your ``.coveragerc`", + " ", + " .. code-block:: ini", + " ", + " [run]", + " source=your_package_name", + "+ ", + "+If there are multiple sources, you instead should add ``include", + "+", + "+.. code-block:: ini", + "+", + "+ [run]", + "+ include=your_package_name/*", + " ", + " unittests", + " ---------", + ], + }, + { + "header": ["10150", "5", "10158", "4"], + "lines": [ + " * Twitter: `@codecov `_.", + " * Email: `hello@codecov.io `_.", + " ", + "-We are happy to help if you have any questions. ", + "-", + "+We are happy to help if you have any questions. .", + ], + }, + ], + "stats": {"added": 11, "removed": 4}, + }, + "file_2.py": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["10", "8", "10", "9"], + "lines": [ + " Overview", + " --------", + " ", + "-Main website: `Codecov `_.", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + "+website: `Codecov `_.", + " ", + " .. code-block:: shell-session", + " ", + ], + }, + { + "header": ["50", "12", "51", "19"], + "lines": [ + " ", + " You may need to configure a ``.coveragerc`` file. Learn more `here `_. Start with this `generic .coveragerc `_ for example.", + " ", + "-We highly suggest adding `source` to your ``.coveragerc`` which solves a number of issues collecting coverage.", + "+We highly suggest adding ``source`` to your ``.coveragerc``, which solves a number of issues collecting coverage.", + " ", + " .. code-block:: ini", + " ", + " [run]", + " source=your_package_name", + "+ ", + "+If there are multiple sources, you instead should add ``include`` to your ``.coveragerc``", + "+", + "+.. code-block:: ini", + "+", + "+ [run]", + "+ include=your_package_name/*", + " ", + " unittests", + " ---------", + ], + }, + ], + "stats": {"added": 11, "removed": 4}, + }, + } + }, + ) + pull_dict = {"base": {"branch": "master"}} + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "files"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + pull = comparison.pull + repository = sample_comparison.head.commit.repository + expected_result = [ + f"## [Codecov](https://app.codecov.io/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + f"> Project coverage is 60.00%. Comparing base [(`{sample_comparison.project_coverage_base.commit.commitid[:7]}`)](https://app.codecov.io/gh/{repository.slug}/commit/{sample_comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{sample_comparison.head.commit.commitid[:7]}`)](https://app.codecov.io/gh/{repository.slug}/commit/{sample_comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + f"| [Files with missing lines](https://app.codecov.io/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [file\\_1.go](https://app.codecov.io/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `62.50% <ø> (+12.50%)` | `10.00 <0.00> (-1.00)` | :arrow_up: |", + f"| [file\\_2.py](https://app.codecov.io/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_2.py#diff-ZmlsZV8yLnB5) | `50.00% <ø> (ø)` | `0.00 <0.00> (ø)` | |", + "", + ] + res = notifier.create_message(comparison, pull_dict, {"layout": "files"}) + for expected, res in zip(expected_result, res): + assert expected == res + + @pytest.mark.django_db + def test_create_message_with_github_app_comment( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison, + mocker, + ): + comparison = sample_comparison + comparison.context = ComparisonContext(gh_is_using_codecov_commenter=True) + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "files", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + res = notifier.build_message(comparison) + assert ( + res[0] + == ":warning: Please install the !['codecov app svg image'](https://github.com/codecov/engineering-team/assets/152432831/e90313f4-9d3a-4b63-8b54-cfe14e7ec20d) to ensure uploads and comments are reliably processed by Codecov." + ) + + @pytest.mark.django_db + def test_build_message( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + pull = comparison.pull + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = sample_comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"> Project coverage is 60.00%. Comparing base [(`{sample_comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{sample_comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{sample_comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{sample_comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | 66.67% | [1 Missing :warning: ](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree) |", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=============================================", + "+ Coverage 50.00% 60.00% +10.00% ", + "+ Complexity 11 10 -1 ", + "=============================================", + " Files 2 2 ", + " Lines 6 10 +4 ", + " Branches 0 1 +1 ", + "=============================================", + "+ Hits 3 6 +3 ", + " Misses 3 3 ", + "- Partials 0 1 +1 ", + "```", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [integration](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `?` | `?` | |", + f"| [unit](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `100.00% <100.00%> (?)` | `0.00 <0.00> (?)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `62.50% <66.67%> (+12.50%)` | `10.00 <0.00> (-1.00)` | :arrow_up: |", + "", + f"... and [1 file with indirect coverage changes](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/indirect-changes?src=pr&el=tree-more)", + "", + "------", + "", + f"[Continue to review full report in " + f"Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn " + "more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + f"> Powered by " + f"[Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=footer). " + f"Last update " + f"[{sample_comparison.project_coverage_base.commit.commitid[:7]}...{sample_comparison.head.commit.commitid[:7]}](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=lastupdated). " + f"Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_flags_empty_coverage( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_bunch_empty_flags, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_bunch_empty_flags + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "flags"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + f"> Project coverage is 100.00%. Comparing base [(`{comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | |", + "|---|---|---|", + f"| [eighth](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `?` | |", + f"| [fifth](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `∅ <ø> (?)` | |", + f"| [first](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `100.00% <ø> (ø)` | |", + f"| [fourth](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `∅ <ø> (∅)` | |", + f"| [second](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `50.00% <ø> (∅)` | |", + f"| [seventh](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `?` | |", + f"| [sixth](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `100.00% <ø> (?)` | |", + f"| [third](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `∅ <ø> (∅)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + ] + li = 0 + for exp, res in zip(expected_result, result): + li += 1 + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_more_sections( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + pull = comparison.pull + all_sections = [ + "reach, diff, flags, files, footer", + "changes", + "file", + "header", + "suggestions", + "sunburst", + "uncovered", + "random_section", + ] + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": ",".join(all_sections)}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = sample_comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"> Project coverage is 60.00%. Comparing base [(`{comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{sample_comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | 66.67% | [1 Missing :warning: ](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree) |", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=============================================", + "+ Coverage 50.00% 60.00% +10.00% ", + "+ Complexity 11 10 -1 ", + "=============================================", + " Files 2 2 ", + " Lines 6 10 +4 ", + " Branches 0 1 +1 ", + "=============================================", + "+ Hits 3 6 +3 ", + " Misses 3 3 ", + "- Partials 0 1 +1 ", + "```", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [integration](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `?` | `?` | |", + f"| [unit](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `100.00% <100.00%> (?)` | `0.00 <0.00> (?)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `62.50% <66.67%> (+12.50%)` | `10.00 <0.00> (-1.00)` | :arrow_up: |", + "", + f"... and [1 file with indirect coverage changes](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/indirect-changes?src=pr&el=tree-more)", + "", + "------", + "", + f"[Continue to review full report in " + f"Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn " + "more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + f"> Powered by " + f"[Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=footer). " + f"Last update " + f"[{sample_comparison.project_coverage_base.commit.commitid[:7]}...{sample_comparison.head.commit.commitid[:7]}](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=lastupdated). " + f"Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_upgrade_message( + self, + request, + dbsession, + mocker, + mock_configuration, + with_sql_functions, + sample_comparison, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + mocker.patch("services.license.is_enterprise", return_value=False) + comparison = sample_comparison + pull = comparison.enriched_pull.database_pull + repository = sample_comparison.head.commit.repository + notifier = CommentNotifier( + repository=repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + decoration_type=Decoration.upgrade, + ) + result = notifier.build_message(comparison) + provider_pull = comparison.enriched_pull.provider_pull + expected_result = [ + f"The author of this PR, {provider_pull['author']['username']}, is not an activated member of this organization on Codecov.", + f"Please [activate this user on Codecov](test.example.br/members/gh/{pull.repository.owner.username}) to display this PR comment.", + "Coverage data is still being uploaded to Codecov.io for purposes of overall coverage calculations.", + "Please don't hesitate to email us at support@codecov.io with any questions.", + ] + li = 0 + for exp, res in zip(expected_result, result): + li += 1 + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_limited_upload_message( + self, + request, + dbsession, + mocker, + mock_configuration, + with_sql_functions, + sample_comparison, + ): + mock_configuration.params["setup"] = { + "codecov_url": "test.example.br", + "codecov_dashboard_url": "test.example.br", + } + comparison = sample_comparison + pull = comparison.enriched_pull.database_pull + repository = sample_comparison.head.commit.repository + notifier = CommentNotifier( + repository=repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + decoration_type=Decoration.upload_limit, + ) + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/plan/gh/{pull.repository.owner.username}) upload limit reached :warning:", + f"This org is currently on the free Basic Plan; which includes 250 free private repo uploads each rolling month.\ + This limit has been reached and additional reports cannot be generated. For unlimited uploads,\ + upgrade to our [pro plan](test.example.br/plan/gh/{pull.repository.owner.username}).", + "", + "**Do you have questions or need help?** Connect with our sales team today at ` sales@codecov.io `", + ] + li = 0 + for exp, res in zip(expected_result, result): + li += 1 + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_passing_empty_upload( + self, + request, + dbsession, + mocker, + mock_configuration, + with_sql_functions, + sample_comparison, + ): + mock_configuration.params["setup"] = { + "codecov_url": "test.example.br", + "codecov_dashboard_url": "test.example.br", + } + comparison = sample_comparison + repository = sample_comparison.head.commit.repository + notifier = CommentNotifier( + repository=repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + decoration_type=Decoration.passing_empty_upload, + ) + result = notifier.build_message(comparison) + expected_result = [ + "## Codecov Report", + ":heavy_check_mark: **No coverage data to report**, because files changed do not require tests or are set to [ignore](https://docs.codecov.com/docs/ignoring-paths#:~:text=You%20can%20use%20the%20top,will%20be%20skipped%20during%20processing.) ", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_failing_empty_upload( + self, + request, + dbsession, + mocker, + mock_configuration, + with_sql_functions, + sample_comparison, + ): + mock_configuration.params["setup"] = { + "codecov_url": "test.example.br", + "codecov_dashboard_url": "test.example.br", + } + comparison = sample_comparison + repository = sample_comparison.head.commit.repository + notifier = CommentNotifier( + repository=repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + decoration_type=Decoration.failing_empty_upload, + ) + result = notifier.build_message(comparison) + expected_result = [ + "## Codecov Report", + "This is an empty upload", + "Files changed in this PR are testable or aren't ignored by Codecov, please run your tests and upload coverage. If you wish to ignore these files, please visit our [ignoring paths docs](https://docs.codecov.com/docs/ignoring-paths).", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_processing_upload( + self, + request, + dbsession, + mocker, + mock_configuration, + with_sql_functions, + sample_comparison, + ): + mock_configuration.params["setup"] = { + "codecov_url": "test.example.br", + "codecov_dashboard_url": "test.example.br", + } + comparison = sample_comparison + repository = sample_comparison.head.commit.repository + notifier = CommentNotifier( + repository=repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + decoration_type=Decoration.processing_upload, + ) + result = notifier.build_message(comparison) + expected_result = [ + "We're currently processing your upload. This comment will be updated when the results are available.", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_upgrade_message_enterprise( + self, + request, + dbsession, + mocker, + mock_configuration, + with_sql_functions, + sample_comparison, + ): + mocker.patch("services.license.is_enterprise", return_value=True) + + encrypted_license = "wxWEJyYgIcFpi6nBSyKQZQeaQ9Eqpo3SXyUomAqQOzOFjdYB3A8fFM1rm+kOt2ehy9w95AzrQqrqfxi9HJIb2zLOMOB9tSy52OykVCzFtKPBNsXU/y5pQKOfV7iI3w9CHFh3tDwSwgjg8UsMXwQPOhrpvl2GdHpwEhFdaM2O3vY7iElFgZfk5D9E7qEnp+WysQwHKxDeKLI7jWCnBCBJLDjBJRSz0H7AfU55RQDqtTrnR+rsLDHOzJ80/VxwVYhb" + mock_configuration.params["setup"]["enterprise_license"] = encrypted_license + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codecov.mysite.com" + ) + + comparison = sample_comparison + pull = comparison.enriched_pull.database_pull + repository = sample_comparison.head.commit.repository + notifier = CommentNotifier( + repository=repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + decoration_type=Decoration.upgrade, + ) + result = notifier.build_message(comparison) + provider_pull = comparison.enriched_pull.provider_pull + expected_result = [ + f"The author of this PR, {provider_pull['author']['username']}, is not activated in your Codecov Self-Hosted installation.", + f"Please [activate this user](https://codecov.mysite.com/account/gh/{pull.author.username}) to display this PR comment.", + "Coverage data is still being uploaded to Codecov Self-Hosted for the purposes of overall coverage calculations.", + "Please contact your Codecov On-Premises installation administrator with any questions.", + ] + li = 0 + for exp, res in zip(expected_result, result): + li += 1 + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_hide_complexity( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + pull = comparison.pull + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={"codecov": {"ui": {"hide_complexity": True}}}, + repository_service=mock_repo_provider, + ) + repository = sample_comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"> Project coverage is 60.00%. Comparing base [(`{sample_comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{sample_comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{sample_comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{sample_comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | 66.67% | [1 Missing :warning: ](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree) |", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=============================================", + "+ Coverage 50.00% 60.00% +10.00% ", + "+ Complexity 11 10 -1 ", + "=============================================", + " Files 2 2 ", + " Lines 6 10 +4 ", + " Branches 0 1 +1 ", + "=============================================", + "+ Hits 3 6 +3 ", + " Misses 3 3 ", + "- Partials 0 1 +1 ", + "```", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | |", + "|---|---|---|", + f"| [integration](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `?` | |", + f"| [unit](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `100.00% <100.00%> (?)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Coverage Δ | |", + "|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `62.50% <66.67%> (+12.50%)` | :arrow_up: |", + "", + f"... and [1 file with indirect coverage changes](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/indirect-changes?src=pr&el=tree-more)", + "", + "------", + "", + f"[Continue to review full report in " + f"Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn " + "more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + f"> Powered by " + f"[Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=footer). " + f"Last update " + f"[{sample_comparison.project_coverage_base.commit.commitid[:7]}...{sample_comparison.head.commit.commitid[:7]}](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=lastupdated). " + f"Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_no_base_report( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_without_base_report, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_without_base_report + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"> Please [upload](https://docs.codecov.com/docs/codecov-uploader) report for BASE (`master@{comparison.project_coverage_base.commit.commitid[:7]}`). [Learn more](https://docs.codecov.io/docs/error-reference#section-missing-base-commit) about missing BASE report.", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | 66.67% | [1 Missing :warning: ](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree) |", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=========================================", + " Coverage ? 60.00% ", + " Complexity ? 10 ", + "=========================================", + " Files ? 2 ", + " Lines ? 10 ", + " Branches ? 1 ", + "=========================================", + " Hits ? 6 ", + " Misses ? 3 ", + " Partials ? 1 ", + "```", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [unit](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `100.00% <100.00%> (?)` | `0.00 <0.00> (?)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `62.50% <66.67%> (ø)` | `10.00 <0.00> (?)` | |", + "", + "------", + "", + f"[Continue to review full report in " + f"Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn " + "more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + f"> Powered by " + f"[Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=footer). " + f"Last update " + f"[{comparison.project_coverage_base.commit.commitid[:7]}...{comparison.head.commit.commitid[:7]}](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=lastupdated). " + f"Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_no_base_commit( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_without_base_with_pull, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_without_base_with_pull + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + "> Please [upload](https://docs.codecov.com/docs/codecov-uploader) report for BASE (`master@cdf9aa4`). [Learn more](https://docs.codecov.io/docs/error-reference#section-missing-base-commit) about missing BASE report.", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | 66.67% | [1 Missing :warning: ](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree) |", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=========================================", + " Coverage ? 60.00% ", + " Complexity ? 10 ", + "=========================================", + " Files ? 2 ", + " Lines ? 10 ", + " Branches ? 1 ", + "=========================================", + " Hits ? 6 ", + " Misses ? 3 ", + " Partials ? 1 ", + "```", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [unit](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `100.00% <100.00%> (?)` | `0.00 <0.00> (?)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `62.50% <66.67%> (ø)` | `10.00 <0.00> (?)` | |", + "", + "------", + "", + f"[Continue to review full report in " + f"Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn " + "more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + f"> Powered by " + f"[Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=footer). " + f"Last update " + f"[cdf9aa4...{comparison.head.commit.commitid[:7]}](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=lastupdated). " + f"Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_no_change( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_no_change, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_no_change + pull = comparison.pull + + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"> Project coverage is 60.00%. Comparing base [(`{comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | 66.67% | [1 Missing :warning: ](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree) |", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=========================================", + " Coverage 60.00% 60.00% ", + " Complexity 10 10 ", + "=========================================", + " Files 2 2 ", + " Lines 10 10 ", + " Branches 1 1 ", + "=========================================", + " Hits 6 6 ", + " Misses 3 3 ", + " Partials 1 1 ", + "```", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [unit](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `100.00% <100.00%> (ø)` | `0.00 <0.00> (ø)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `62.50% <66.67%> (ø)` | `10.00 <0.00> (ø)` | |", + "", + "------", + "", + f"[Continue to review full report in " + f"Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn " + "more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + f"> Powered by " + f"[Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=footer). " + f"Last update " + f"[{sample_comparison_no_change.project_coverage_base.commit.commitid[:7]}...{sample_comparison_no_change.head.commit.commitid[:7]}](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=lastupdated). " + f"Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ] + li = 0 + for exp, res in zip(expected_result, result): + li += 1 + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_negative_change( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_negative_change, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_negative_change + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + f"> Project coverage is 50.00%. Comparing base [(`{comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=============================================", + "- Coverage 60.00% 50.00% -10.00% ", + "- Complexity 10 11 +1 ", + "=============================================", + " Files 2 2 ", + " Lines 10 6 -4 ", + " Branches 1 0 -1 ", + "=============================================", + "- Hits 6 3 -3 ", + " Misses 3 3 ", + "+ Partials 1 0 -1 ", + "```", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [integration](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `100.00% <ø> (?)` | `0.00 <ø> (?)` | |", + f"| [unit](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `?` | `?` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `50.00% <ø> (-12.50%)` | `11.00 <0.00> (+1.00)` | :arrow_down: |", + "", + f"... and [1 file with indirect coverage changes](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/indirect-changes?src=pr&el=tree-more)", + "", + "------", + "", + f"[Continue to review full report in " + f"Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn " + "more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + f"> Powered by " + f"[Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=footer). " + f"Last update " + f"[{comparison.project_coverage_base.commit.commitid[:7]}...{comparison.head.commit.commitid[:7]}](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=lastupdated). " + f"Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_negative_change_tricky_rounding( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_negative_change, + ): + # This example was taken from a real PR in which we had issues with rounding + # That's why the numbers will be.... dramatic + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_negative_change + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "diff"}, + notifier_site_settings=True, + current_yaml={"coverage": {"precision": 2, "round": "down"}}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + # Change the reports + new_base_report = Report() + new_base_file = ReportFile("file.py") + # Produce numbers that we know can be tricky for rounding + for i in range(1, 6760): + new_base_file.append(i, ReportLine.create(coverage=1)) + for i in range(6760, 7631): + new_base_file.append(i, ReportLine.create(coverage=0)) + new_base_report.append(new_base_file) + comparison.project_coverage_base.report = ReadOnlyReport.create_from_report( + new_base_report + ) + new_head_report = Report() + new_head_file = ReportFile("file.py") + for i in range(1, 6758): + new_head_file.append(i, ReportLine.create(coverage=1)) + for i in range(6758, 7632): + new_head_file.append(i, ReportLine.create(coverage=0)) + new_head_report.append(new_head_file) + comparison.head.report = ReadOnlyReport.create_from_report(new_head_report) + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + f"> Project coverage is 88.54%. Comparing base [(`{comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "==========================================", + "- Coverage 88.58% 88.54% -0.04% ", + "==========================================", + " Files 1 1 ", + " Lines 7630 7631 +1 ", + "==========================================", + "- Hits 6759 6757 -2 ", + "- Misses 871 874 +3 ", + "```", + "", + ] + li = 0 + for exp, res in zip(expected_result, result): + li += 1 + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_negative_change_tricky_rounding_newheader( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_negative_change, + ): + # This example was taken from a real PR in which we had issues with rounding + # That's why the numbers will be.... dramatic + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_negative_change + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "newheader"}, + notifier_site_settings=True, + current_yaml={"coverage": {"precision": 2, "round": "down"}}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + # Change the reports + new_base_report = Report() + new_base_file = ReportFile("file.py") + # Produce numbers that we know can be tricky for rounding + for i in range(1, 6760): + new_base_file.append(i, ReportLine.create(coverage=1)) + for i in range(6760, 7631): + new_base_file.append(i, ReportLine.create(coverage=0)) + new_base_report.append(new_base_file) + comparison.project_coverage_base.report = ReadOnlyReport.create_from_report( + new_base_report + ) + new_head_report = Report() + new_head_file = ReportFile("file.py") + for i in range(1, 6758): + new_head_file.append(i, ReportLine.create(coverage=1)) + for i in range(6758, 7632): + new_head_file.append(i, ReportLine.create(coverage=0)) + new_head_report.append(new_head_file) + comparison.head.report = ReadOnlyReport.create_from_report(new_head_report) + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + f"> Project coverage is 88.54%. Comparing base [(`{comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + ] + li = 0 + for exp, res in zip(expected_result, result): + li += 1 + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_show_carriedforward_flags_no_cf_coverage( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "show_carryforward_flags": True, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"> Project coverage is 60.00%. Comparing base [(`{sample_comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{sample_comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{sample_comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{sample_comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | 66.67% | [1 Missing :warning: ](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree) |", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=============================================", + "+ Coverage 50.00% 60.00% +10.00% ", + "+ Complexity 11 10 -1 ", + "=============================================", + " Files 2 2 ", + " Lines 6 10 +4 ", + " Branches 0 1 +1 ", + "=============================================", + "+ Hits 3 6 +3 ", + " Misses 3 3 ", + "- Partials 0 1 +1 ", + "```", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [integration](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `?` | `?` | |", + f"| [unit](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `100.00% <100.00%> (?)` | `0.00 <0.00> (?)` | |", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `62.50% <66.67%> (+12.50%)` | `10.00 <0.00> (-1.00)` | :arrow_up: |", + "", + f"... and [1 file with indirect coverage changes](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/indirect-changes?src=pr&el=tree-more)", + "", + "------", + "", + f"[Continue to review full report in " + f"Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn " + "more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + f"> Powered by " + f"[Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=footer). " + f"Last update " + f"[{sample_comparison.project_coverage_base.commit.commitid[:7]}...{sample_comparison.head.commit.commitid[:7]}](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=lastupdated). " + f"Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_with_without_flags( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_coverage_carriedforward, + ): + # Without flags table + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_coverage_carriedforward + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, files, footer", + "show_carryforward_flags": True, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + f"> Project coverage is 65.38%. Comparing base [(`{comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=======================================", + " Coverage 65.38% 65.38% ", + "=======================================", + " Files 15 15 ", + " Lines 208 208 ", + "=======================================", + " Hits 136 136 ", + " Misses 4 4 ", + " Partials 68 68 ", + "```", + "", + "------", + "", + f"[Continue to review full report in " + f"Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn " + "more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + f"> Powered by " + f"[Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=footer). " + f"Last update " + f"[{comparison.project_coverage_base.commit.commitid[:7]}...{comparison.head.commit.commitid[:7]}](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=lastupdated). " + f"Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + # With flags table + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_coverage_carriedforward + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "show_carryforward_flags": True, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + f"> Project coverage is 65.38%. Comparing base [(`{comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=======================================", + " Coverage 65.38% 65.38% ", + "=======================================", + " Files 15 15 ", + " Lines 208 208 ", + "=======================================", + " Hits 136 136 ", + " Misses 4 4 ", + " Partials 68 68 ", + "```", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | | *Carryforward flag |", + "|---|---|---|---|", + f"| [enterprise](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `25.00% <ø> (ø)` | | Carriedforward from [1234567](test.example.br/gh/{repository.slug}/commit/123456789sha) |", + f"| [integration](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `25.00% <ø> (ø)` | | Carriedforward |", + f"| [unit](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `25.00% <ø> (ø)` | | |", + "", + "*This pull request uses carry forward flags. [Click here](https://docs.codecov.io/docs/carryforward-flags) to find out more." + "", + "", + "------", + "", + f"[Continue to review full report in " + f"Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn " + "more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + f"> Powered by " + f"[Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=footer). " + f"Last update " + f"[{comparison.project_coverage_base.commit.commitid[:7]}...{comparison.head.commit.commitid[:7]}](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=lastupdated). " + f"Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ] + li = 0 + for exp, res in zip(expected_result, result): + li += 1 + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_show_carriedforward_flags_has_cf_coverage( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_coverage_carriedforward, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_coverage_carriedforward + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "show_carryforward_flags": True, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + f"> Project coverage is 65.38%. Comparing base [(`{comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=======================================", + " Coverage 65.38% 65.38% ", + "=======================================", + " Files 15 15 ", + " Lines 208 208 ", + "=======================================", + " Hits 136 136 ", + " Misses 4 4 ", + " Partials 68 68 ", + "```", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | | *Carryforward flag |", + "|---|---|---|---|", + f"| [enterprise](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `25.00% <ø> (ø)` | | Carriedforward from [1234567](test.example.br/gh/{repository.slug}/commit/123456789sha) |", + f"| [integration](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `25.00% <ø> (ø)` | | Carriedforward |", + f"| [unit](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `25.00% <ø> (ø)` | | |", + "", + "*This pull request uses carry forward flags. [Click here](https://docs.codecov.io/docs/carryforward-flags) to find out more." + "", + "", + "------", + "", + f"[Continue to review full report in " + f"Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn " + "more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + f"> Powered by " + f"[Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=footer). " + f"Last update " + f"[{comparison.project_coverage_base.commit.commitid[:7]}...{comparison.head.commit.commitid[:7]}](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=lastupdated). " + f"Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ] + li = 0 + for exp, res in zip(expected_result, result): + li += 1 + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_hide_carriedforward_flags_has_cf_coverage( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_coverage_carriedforward, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_coverage_carriedforward + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "show_carryforward_flags": False, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + f"> Project coverage is 65.38%. Comparing base [(`{comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=======================================", + " Coverage 65.38% 65.38% ", + "=======================================", + " Files 15 15 ", + " Lines 208 208 ", + "=======================================", + " Hits 136 136 ", + " Misses 4 4 ", + " Partials 68 68 ", + "```", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | |", + "|---|---|---|", + f"| [unit](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `25.00% <ø> (ø)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more." + "", + "", + "------", + "", + f"[Continue to review full report in " + f"Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn " + "more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + f"> Powered by " + f"[Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=footer). " + f"Last update " + f"[{comparison.project_coverage_base.commit.commitid[:7]}...{comparison.head.commit.commitid[:7]}](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=lastupdated). " + f"Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ] + li = 0 + for exp, res in zip(expected_result, result): + li += 1 + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_default_layout( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_report_without_flags, + sample_comparison, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + pull = comparison.pull + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={"codecov": {"ui": {"hide_complexity": True}}}, + repository_service=mock_repo_provider, + ) + repository = sample_comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"> Project coverage is 60.00%. Comparing base [(`{sample_comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{sample_comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{sample_comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{sample_comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_send_actual_notification_spammy( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "spammy", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + data = {"message": ["message"], "commentid": "12345", "pullid": 98} + mock_repo_provider.post_comment.return_value = {"id": 9865} + result = notifier.send_actual_notification(data) + assert result.notification_attempted + assert result.notification_successful + assert result.explanation is None + assert result.data_sent == data + assert result.data_received == {"id": 9865} + mock_repo_provider.post_comment.assert_called_with(98, "message") + assert not mock_repo_provider.edit_comment.called + assert not mock_repo_provider.delete_comment.called + + @pytest.mark.django_db + def test_build_message_no_flags( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_report_without_flags, + sample_comparison, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + pull = sample_comparison.pull + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + sample_comparison.head.report = ReadOnlyReport.create_from_report( + sample_report_without_flags + ) + comparison = sample_comparison + repository = sample_comparison.head.commit.repository + result = notifier.build_message(sample_comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"> Project coverage is 60.00%. Comparing base [(`{sample_comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{sample_comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{sample_comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{sample_comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | 66.67% | [1 Missing :warning: ](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree) |", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=============================================", + "+ Coverage 50.00% 60.00% +10.00% ", + "+ Complexity 11 10 -1 ", + "=============================================", + " Files 2 2 ", + " Lines 6 10 +4 ", + " Branches 0 1 +1 ", + "=============================================", + "+ Hits 3 6 +3 ", + " Misses 3 3 ", + "- Partials 0 1 +1 ", + "```", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [integration](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `?` | `?` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `62.50% <66.67%> (+12.50%)` | `10.00 <0.00> (-1.00)` | :arrow_up: |", + "", + f"... and [1 file with indirect coverage changes](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/indirect-changes?src=pr&el=tree-more)", + "", + "------", + "", + f"[Continue to review full report in " + f"Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn " + "more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + f"> Powered by " + f"[Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=footer). " + f"Last update " + f"[{comparison.project_coverage_base.commit.commitid[:7]}...{comparison.head.commit.commitid[:7]}](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=lastupdated). " + f"Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_send_actual_notification_new_no_permissions( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "new", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + data = {"message": ["message"], "commentid": "12345", "pullid": 98} + mock_repo_provider.post_comment.return_value = {"id": 9865} + mock_repo_provider.delete_comment.side_effect = TorngitClientError( + "code", "response", "message" + ) + result = notifier.send_actual_notification(data) + assert result.notification_attempted + assert not result.notification_successful + assert result.explanation == "no_permissions" + assert result.data_sent == data + assert result.data_received is None + mock_repo_provider.delete_comment.assert_called_with(98, "12345") + assert not mock_repo_provider.post_comment.called + assert not mock_repo_provider.edit_comment.called + + @pytest.mark.django_db + def test_send_actual_notification_new( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "new", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + data = {"message": ["message"], "commentid": "12345", "pullid": 98} + mock_repo_provider.post_comment.return_value = {"id": 9865} + mock_repo_provider.delete_comment.return_value = True + result = notifier.send_actual_notification(data) + assert result.notification_attempted + assert result.notification_successful + assert result.explanation is None + assert result.data_sent == data + assert result.data_received == {"id": 9865} + mock_repo_provider.post_comment.assert_called_with(98, "message") + assert not mock_repo_provider.edit_comment.called + mock_repo_provider.delete_comment.assert_called_with(98, "12345") + + @pytest.mark.django_db + def test_send_actual_notification_new_no_permissions_post( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "new", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + data = {"message": ["message"], "commentid": None, "pullid": 98} + mock_repo_provider.post_comment.side_effect = TorngitClientError( + "code", "response", "message" + ) + mock_repo_provider.edit_comment.side_effect = TorngitClientError( + "code", "response", "message" + ) + result = notifier.send_actual_notification(data) + assert result.notification_attempted + assert not result.notification_successful + assert result.explanation == "comment_posting_permissions" + assert result.data_sent == data + assert result.data_received is None + mock_repo_provider.post_comment.assert_called_with(98, "message") + assert not mock_repo_provider.delete_comment.called + assert not mock_repo_provider.edit_comment.called + + @pytest.mark.django_db + def test_send_actual_notification_new_deleted_comment( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "new", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + data = {"message": ["message"], "commentid": "12345", "pullid": 98} + mock_repo_provider.post_comment.return_value = {"id": 9865} + mock_repo_provider.delete_comment.side_effect = TorngitObjectNotFoundError( + "response", "message" + ) + result = notifier.send_actual_notification(data) + assert result.notification_attempted + assert result.notification_successful + assert result.explanation is None + assert result.data_sent == data + assert result.data_received == {"id": 9865} + mock_repo_provider.post_comment.assert_called_with(98, "message") + assert not mock_repo_provider.edit_comment.called + mock_repo_provider.delete_comment.assert_called_with(98, "12345") + + @pytest.mark.django_db + def test_send_actual_notification_once_deleted_comment( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "once", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + data = {"message": ["message"], "commentid": "12345", "pullid": 98} + mock_repo_provider.post_comment.return_value = {"id": 9865} + mock_repo_provider.edit_comment.side_effect = TorngitObjectNotFoundError( + "response", "message" + ) + result = notifier.send_actual_notification(data) + assert result.notification_attempted is False + assert result.notification_successful is None + assert result.explanation == "comment_deleted" + assert result.data_sent == data + assert result.data_received is None + assert not mock_repo_provider.post_comment.called + mock_repo_provider.edit_comment.assert_called_with(98, "12345", "message") + assert not mock_repo_provider.delete_comment.called + + @pytest.mark.django_db + def test_send_actual_notification_once_non_existing_comment( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "once", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + data = {"message": ["message"], "commentid": None, "pullid": 98} + mock_repo_provider.post_comment.return_value = {"id": 9865} + mock_repo_provider.edit_comment.side_effect = TorngitObjectNotFoundError( + "response", "message" + ) + result = notifier.send_actual_notification(data) + assert result.notification_attempted + assert result.notification_successful + assert result.explanation is None + assert result.data_sent == data + assert result.data_received == {"id": 9865} + mock_repo_provider.post_comment.assert_called_with(98, "message") + assert not mock_repo_provider.delete_comment.called + assert not mock_repo_provider.edit_comment.called + + @pytest.mark.django_db + def test_send_actual_notification_once( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "once", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + data = {"message": ["message"], "commentid": "12345", "pullid": 98} + mock_repo_provider.post_comment.return_value = {"id": 9865} + mock_repo_provider.edit_comment.return_value = {"id": "49"} + result = notifier.send_actual_notification(data) + assert result.notification_attempted + assert result.notification_successful + assert result.explanation is None + assert result.data_sent == data + assert result.data_received == {"id": "49"} + assert not mock_repo_provider.post_comment.called + mock_repo_provider.edit_comment.assert_called_with(98, "12345", "message") + assert not mock_repo_provider.delete_comment.called + + @pytest.mark.django_db + def test_send_actual_notification_once_no_permissions( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "once", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + data = {"message": ["message"], "commentid": "12345", "pullid": 98} + mock_repo_provider.post_comment.return_value = {"id": 9865} + mock_repo_provider.edit_comment.side_effect = TorngitClientError( + "code", "response", "message" + ) + result = notifier.send_actual_notification(data) + assert result.notification_attempted + assert not result.notification_successful + assert result.explanation == "no_permissions" + assert result.data_sent == data + assert result.data_received is None + assert not mock_repo_provider.post_comment.called + mock_repo_provider.edit_comment.assert_called_with(98, "12345", "message") + assert not mock_repo_provider.delete_comment.called + + @pytest.mark.django_db + def test_send_actual_notification_default( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + data = {"message": ["message"], "commentid": "12345", "pullid": 98} + mock_repo_provider.post_comment.return_value = {"id": 9865} + mock_repo_provider.edit_comment.return_value = {"id": "49"} + result = notifier.send_actual_notification(data) + assert result.notification_attempted + assert result.notification_successful + assert result.explanation is None + assert result.data_sent == data + assert result.data_received == {"id": "49"} + assert not mock_repo_provider.post_comment.called + mock_repo_provider.edit_comment.assert_called_with(98, "12345", "message") + assert not mock_repo_provider.delete_comment.called + + @pytest.mark.django_db + def test_send_actual_notification_default_no_permissions_edit( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + data = {"message": ["message"], "commentid": "12345", "pullid": 98} + mock_repo_provider.post_comment.return_value = {"id": 9865} + mock_repo_provider.edit_comment.side_effect = TorngitClientError( + "code", "response", "message" + ) + result = notifier.send_actual_notification(data) + assert result.notification_attempted + assert result.notification_successful + assert result.explanation is None + assert result.data_sent == data + assert result.data_received == {"id": 9865} + mock_repo_provider.post_comment.assert_called_with(98, "message") + mock_repo_provider.edit_comment.assert_called_with(98, "12345", "message") + assert not mock_repo_provider.delete_comment.called + + @pytest.mark.django_db + def test_send_actual_notification_default_no_permissions_twice( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + data = {"message": ["message"], "commentid": "12345", "pullid": 98} + mock_repo_provider.post_comment.side_effect = TorngitClientError( + "code", "response", "message" + ) + mock_repo_provider.edit_comment.side_effect = TorngitClientError( + "code", "response", "message" + ) + result = notifier.send_actual_notification(data) + assert result.notification_attempted + assert not result.notification_successful + assert result.explanation == "comment_posting_permissions" + assert result.data_sent == data + assert result.data_received is None + mock_repo_provider.post_comment.assert_called_with(98, "message") + mock_repo_provider.edit_comment.assert_called_with(98, "12345", "message") + assert not mock_repo_provider.delete_comment.called + + @pytest.mark.django_db + def test_send_actual_notification_default_comment_not_found( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + data = {"message": ["message"], "commentid": "12345", "pullid": 98} + mock_repo_provider.post_comment.return_value = {"id": 9865} + mock_repo_provider.edit_comment.side_effect = TorngitObjectNotFoundError( + "response", "message" + ) + result = notifier.send_actual_notification(data) + assert result.notification_attempted + assert result.notification_successful + assert result.explanation is None + assert result.data_sent == data + assert result.data_received == {"id": 9865} + mock_repo_provider.edit_comment.assert_called_with(98, "12345", "message") + mock_repo_provider.post_comment.assert_called_with(98, "message") + assert not mock_repo_provider.delete_comment.called + + @pytest.mark.django_db + def test_notify_no_pull_request(self, dbsession, sample_comparison_without_pull): + notifier = CommentNotifier( + repository=sample_comparison_without_pull.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = notifier.notify(sample_comparison_without_pull) + assert not result.notification_attempted + assert result.notification_successful == False + assert result.explanation == "no_pull_request" + assert result.data_sent is None + assert result.data_received is None + + @pytest.mark.django_db + def test_notify_pull_head_doesnt_match(self, dbsession, sample_comparison): + sample_comparison.pull.head = "aaaaaaaaaa" + dbsession.flush() + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = notifier.notify(sample_comparison) + assert not result.notification_attempted + assert result.notification_successful is False + assert result.explanation == "pull_head_does_not_match" + assert result.data_sent is None + assert result.data_received is None + + @pytest.mark.django_db + def test_notify_pull_request_not_in_provider( + self, dbsession, sample_comparison_database_pull_without_provider + ): + notifier = CommentNotifier( + repository=sample_comparison_database_pull_without_provider.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = notifier.notify(sample_comparison_database_pull_without_provider) + assert not result.notification_attempted + assert result.notification_successful is False + assert result.explanation == "pull_request_not_in_provider" + assert result.data_sent is None + assert result.data_received is None + + @pytest.mark.django_db + def test_notify_server_unreachable(self, mocker, dbsession, sample_comparison): + mocked_send_actual_notification = mocker.patch.object( + CommentNotifier, + "send_actual_notification", + side_effect=TorngitServerUnreachableError(), + ) + mocked_build_message = mocker.patch.object( + CommentNotifier, "build_message", return_value=["title", "content"] + ) + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = notifier.notify(sample_comparison) + assert result.notification_attempted + assert not result.notification_successful + assert result.explanation == "provider_issue" + assert result.data_sent == { + "commentid": None, + "message": ["title", "content"], + "pullid": sample_comparison.pull.pullid, + } + assert result.data_received is None + + @pytest.mark.django_db + def test_store_results(self, dbsession, sample_comparison): + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent=None, + data_received={"id": 578263422}, + ) + notifier.store_results(sample_comparison, result) + assert sample_comparison.pull.commentid == 578263422 + dbsession.flush() + assert sample_comparison.pull.commentid == 578263422 + dbsession.refresh(sample_comparison.pull) + assert sample_comparison.pull.commentid == "578263422" + + @pytest.mark.django_db + def test_store_results_deleted_comment(self, dbsession, sample_comparison): + sample_comparison.pull.commentid = 12 + dbsession.flush() + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent=None, + data_received={"deleted_comment": True}, + ) + notifier.store_results(sample_comparison, result) + assert sample_comparison.pull.commentid is None + dbsession.flush() + assert sample_comparison.pull.commentid is None + dbsession.refresh(sample_comparison.pull) + assert sample_comparison.pull.commentid is None + + @pytest.mark.django_db + def test_store_results_no_succesfull_result(self, dbsession, sample_comparison): + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = NotificationResult( + notification_attempted=True, + notification_successful=False, + explanation=None, + data_sent=None, + data_received={"id": "yadayada"}, + ) + notifier.store_results(sample_comparison, result) + assert sample_comparison.pull.commentid is None + dbsession.flush() + assert sample_comparison.pull.commentid is None + dbsession.refresh(sample_comparison.pull) + assert sample_comparison.pull.commentid is None + + @pytest.mark.django_db + def test_notify_unable_to_fetch_info(self, dbsession, mocker, sample_comparison): + mocked_build_message = mocker.patch.object( + CommentNotifier, + "build_message", + side_effect=TorngitClientError("code", "response", "message"), + ) + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = notifier.notify(sample_comparison) + assert not result.notification_attempted + assert result.notification_successful is False + assert result.explanation == "unable_build_message" + assert result.data_sent is None + assert result.data_received is None + + @pytest.mark.django_db + def test_notify_not_enough_builds(self, dbsession, sample_comparison): + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + "after_n_builds": 5, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = notifier.notify(sample_comparison) + assert not result.notification_attempted + assert result.notification_successful is False + assert result.explanation == "not_enough_builds" + assert result.data_sent is None + assert result.data_received is None + + @pytest.mark.django_db + @pytest.mark.asyncio + @pytest.mark.parametrize("pull_state", ["open", "closed"]) + async def test_notify_with_enough_builds( + self, dbsession, sample_comparison, mocker, pull_state + ): + build_message_mocker = mocker.patch.object( + CommentNotifier, + "build_message", + return_value="message_test_notify_with_enough_builds", + ) + send_comment_default_behavior_mocker = mocker.patch.object( + CommentNotifier, + "send_comment_default_behavior", + return_value=dict( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_received=None, + ), + ) + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + "after_n_builds": 1, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mocker.MagicMock(), + ) + sample_comparison.pull.state = pull_state + dbsession.flush() + dbsession.refresh(sample_comparison.pull) + result = notifier.notify(sample_comparison) + assert result.notification_attempted + assert result.notification_successful + assert result.explanation is None + assert result.data_sent == { + "commentid": None, + "message": "message_test_notify_with_enough_builds", + "pullid": sample_comparison.pull.pullid, + } + assert result.data_received is None + + @pytest.mark.django_db + def test_notify_exact_same_report_diff_unrelated_report( + self, sample_comparison_no_change, mock_repo_provider + ): + compare_result = { + "diff": { + "files": { + "README.md": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["5", "8", "5", "9"], + "lines": [ + " Overview", + " --------", + " ", + "-Main website: `Codecov `_.", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + "+website: `Codecov `_.", + " ", + " .. code-block:: shell-session", + " ", + ], + }, + { + "header": ["46", "12", "47", "19"], + "lines": [ + " ", + " You may need to configure a ``.coveragerc`` file. Learn more `here `_. Start with this `generic .coveragerc `_ for example.", + " -", + ], + }, + ], + "stats": {"added": 11, "removed": 4}, + } + } + } + } + mock_repo_provider.get_compare.return_value = compare_result + notifier = CommentNotifier( + repository=sample_comparison_no_change.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + "after_n_builds": 1, + "require_changes": [CoverageCommentRequiredChanges.any_change.value], + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + res = notifier.notify(sample_comparison_no_change) + assert res.notification_attempted is False + assert res.notification_successful is False + assert res.explanation == "changes_required" + assert res.data_sent is None + assert res.data_received is None + + @pytest.mark.django_db + def test_notify_exact_same_report_diff_unrelated_report_update_comment( + self, sample_comparison_no_change, mock_repo_provider + ): + compare_result = { + "diff": { + "files": { + "README.md": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["5", "8", "5", "9"], + "lines": [ + " Overview", + " --------", + " ", + "-Main website: `Codecov `_.", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + "+website: `Codecov `_.", + " ", + " .. code-block:: shell-session", + " ", + ], + }, + { + "header": ["46", "12", "47", "19"], + "lines": [ + " ", + " You may need to configure a ``.coveragerc`` file. Learn more `here `_. Start with this `generic .coveragerc `_ for example.", + " -", + ], + }, + ], + "stats": {"added": 11, "removed": 4}, + } + } + } + } + mock_repo_provider.get_compare.return_value = compare_result + sample_comparison_no_change.pull.commentid = 12 + mock_repo_provider.edit_comment.return_value = {"id": 12} + notifier = CommentNotifier( + repository=sample_comparison_no_change.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + "after_n_builds": 1, + "require_changes": [CoverageCommentRequiredChanges.any_change.value], + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + res = notifier.notify(sample_comparison_no_change) + assert res.notification_attempted is True + assert res.notification_successful is True + mock_repo_provider.edit_comment.assert_called() + + @pytest.mark.django_db + def test_message_hide_details_github( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + comparison.repository_service.service = "github" + pull = comparison.pull + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach", "hide_comment_details": True}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = sample_comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"> Project coverage is 60.00%. Comparing base [(`{sample_comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{sample_comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{sample_comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{sample_comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + "
Additional details and impacted files\n", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "
", + ] + li = 0 + for exp, res in zip(expected_result, result): + li += 1 + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_message_announcements_only( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + comparison.repository_service.service = "github" + pull = comparison.pull + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "announcements"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = sample_comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"> Project coverage is 60.00%. Comparing base [(`{sample_comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{sample_comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{sample_comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{sample_comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + ":mega: message", + "", + ] + for exp, res in zip(expected_result, result): + if exp == ":mega: message": + # We might not know the selected message if there are multiple active ones + assert res.startswith(":mega: ") + message = res[7:] + assert message in AnnouncementSectionWriter.current_active_messages + # We have to change the expected line to pass the global check + expected_result[4] = f":mega: {message}" + else: + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_message_hide_details_bitbucket( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + comparison.repository_service.service = "bitbucket" + pull = comparison.pull + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach", "hide_comment_details": True}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = sample_comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"> Project coverage is 60.00%. Comparing base [(`{sample_comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{sample_comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{sample_comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{sample_comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + ] + li = 0 + for exp, res in zip(expected_result, result): + li += 1 + assert exp == res + assert result == expected_result + + +class TestFileSectionWriter(object): + def test_filesection_no_extra_settings(self, sample_comparison, mocker): + section_writer = FileSectionWriter( + sample_comparison.head.commit.repository, + "layout", + show_complexity=False, + settings={}, + current_yaml={}, + ) + changes = [ + Change( + path="file_2.py", + in_diff=True, + totals=ReportTotals( + files=0, + lines=0, + hits=-2, + misses=1, + partials=0, + coverage=-23.333330000000004, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ), + Change( + path="unrelated.py", + in_diff=False, + totals=ReportTotals( + files=0, + lines=0, + hits=-3, + misses=2, + partials=0, + coverage=-43.333330000000004, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ), + Change(path="added.py", new=True, in_diff=None, old_path=None, totals=None), + ] + lines = list( + section_writer.write_section( + sample_comparison, + { + "files": { + "file_1.go": { + "type": "added", + "totals": ReportTotals( + lines=3, + hits=2, + misses=1, + coverage=66.66, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + } + } + }, + changes, + links={"pull": "pull.link"}, + ) + ) + assert lines == [ + "| [Files with missing lines](pull.link?dropdown=coverage&src=pr&el=tree) | Coverage Δ | |", + "|---|---|---|", + "| [file\\_1.go](pull.link?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `62.50% <66.66%> (+12.50%)` | :arrow_up: |", + "", + "... and [3 files with indirect coverage changes](pull.link/indirect-changes?src=pr&el=tree-more)", + ] + + @pytest.mark.parametrize( + "test_analytics_enabled,bundle_analysis_enabled", + [(False, False), (False, True), (True, False), (True, True)], + ) + @pytest.mark.django_db + def test_build_cross_pollination_message( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison, + test_analytics_enabled, + bundle_analysis_enabled, + ): + mock_all_plans_and_tiers() + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + comparison.pull.is_first_coverage_pull = False + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = sample_comparison.head.commit.repository + if bundle_analysis_enabled: + repository.languages = ["javascript"] + if test_analytics_enabled: + repository.test_analytics_enabled = False + dbsession.flush() + result = notifier.build_message(comparison) + + header = "
:rocket: New features to boost your workflow: " + ta_message = "- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems." + ba_message = "- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges." + + end_of_message = [] + + if test_analytics_enabled or bundle_analysis_enabled: + end_of_message += [header, ""] + assert header in result + + if test_analytics_enabled: + end_of_message.append(ta_message) + assert ta_message in result + + if bundle_analysis_enabled: + end_of_message.append(ba_message) + assert ba_message in result + + if len(end_of_message): + assert result[-1] == "
" + else: + assert result[-1] == "" + + def test_get_tree_cell(self): + typ = "added" + path = "path/to/test_file.go" + metrics = "| this is where the metrics go |" + line = _get_tree_cell( + typ=typ, path=path, metrics=metrics, compare="pull.link", is_critical=False + ) + assert ( + line + == f"| [path/to/test\\_file.go](pull.link?src=pr&el=tree&filepath=path%2Fto%2Ftest_file.go#diff-cGF0aC90by90ZXN0X2ZpbGUuZ28=) {metrics}" + ) + + def test_get_tree_cell_with_critical(self): + typ = "added" + path = "path/to/test_file.go" + metrics = "| this is where the metrics go |" + line = _get_tree_cell( + typ=typ, path=path, metrics=metrics, compare="pull.link", is_critical=True + ) + assert ( + line + == f"| [path/to/test\\_file.go](pull.link?src=pr&el=tree&filepath=path%2Fto%2Ftest_file.go#diff-cGF0aC90by90ZXN0X2ZpbGUuZ28=) **Critical** {metrics}" + ) + + def test_filesection_hide_project_cov(self, sample_comparison, mocker): + section_writer = NewFilesSectionWriter( + sample_comparison.head.commit.repository, + "layout", + show_complexity=False, + settings={"hide_project_coverage": True}, + current_yaml={}, + ) + changes = [ + Change( + path="unrelated.py", + in_diff=False, + totals=ReportTotals( + files=0, + lines=0, + hits=-3, + misses=2, + partials=0, + coverage=-43.333330000000004, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ), + Change(path="added.py", new=True, in_diff=None, old_path=None, totals=None), + ] + lines = list( + section_writer.write_section( + sample_comparison, + { + "files": { + "file_1.go": { + "type": "added", + "totals": ReportTotals( + lines=3, + hits=2, + misses=1, + coverage=66.66, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + }, + "file_2.py": { + "type": "added", + "totals": ReportTotals( + lines=3, + hits=-2, + misses=2, + partials=3, + coverage=-23.333330000000004, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + }, + } + }, + changes, + links={"pull": "pull.link"}, + ) + ) + assert lines == [ + "| [Files with missing lines](pull.link?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + "| [file\\_2.py](pull.link?src=pr&el=tree&filepath=file_2.py#diff-ZmlsZV8yLnB5) | -23.33% | [2 Missing and 3 partials :warning: ](pull.link?src=pr&el=tree) |", + "| [file\\_1.go](pull.link?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | 66.66% | [1 Missing :warning: ](pull.link?src=pr&el=tree) |", + ] + + def test_filesection_hide_project_cov_with_changed_files_but_no_missing_lines( + self, sample_comparison, mocker + ): + section_writer = NewFilesSectionWriter( + sample_comparison.head.commit.repository, + "layout", + show_complexity=False, + settings={"hide_project_coverage": True}, + current_yaml={}, + ) + changes = [ + Change( + path="unrelated.py", + in_diff=False, + totals=ReportTotals( + files=0, + lines=0, + hits=-3, + misses=2, + partials=0, + coverage=-43.333330000000004, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ), + Change(path="added.py", new=True, in_diff=None, old_path=None, totals=None), + ] + lines = list( + section_writer.write_section( + sample_comparison, + { + "files": { + "file_1.go": { + "type": "added", + "totals": ReportTotals( + lines=3, + hits=3, + misses=0, + coverage=100.00, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + }, + "file_2.py": { + "type": "added", + "totals": ReportTotals( + lines=3, + hits=3, + misses=0, + partials=0, + coverage=-100.00, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + }, + } + }, + changes, + links={"pull": "pull.link"}, + ) + ) + assert lines == [] + + def test_filesection_hide_project_cov_no_files_changed( + self, sample_comparison, mocker + ): + section_writer = NewFilesSectionWriter( + sample_comparison.head.commit.repository, + "layout", + show_complexity=False, + settings={"hide_project_coverage": True}, + current_yaml={}, + ) + changes = [ + Change( + path="unrelated.py", + in_diff=False, + totals=ReportTotals( + files=0, + lines=0, + hits=-3, + misses=2, + partials=0, + coverage=-43.333330000000004, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ), + Change(path="added.py", new=True, in_diff=None, old_path=None, totals=None), + ] + lines = list( + section_writer.write_section( + sample_comparison, + {"files": {}}, + changes, + links={"pull": "pull.link"}, + ) + ) + assert lines == [] + + +class TestNewHeaderSectionWriter(object): + def test_new_header_section_writer(self, mocker, sample_comparison): + writer = HeaderSectionWriter( + mocker.MagicMock(), + mocker.MagicMock(), + show_complexity=mocker.MagicMock(), + settings={}, + current_yaml=mocker.MagicMock(), + ) + mocker.patch( + "services.notification.notifiers.mixins.message.sections.round_number", + return_value=Decimal(0), + ) + res = list( + writer.write_section( + sample_comparison, + None, + None, + links={"pull": "urlurl", "base": "urlurl", "head": "headurl"}, + ) + ) + assert res == [ + "All modified and coverable lines are covered by tests :white_check_mark:", + f"> Project coverage is 0%. Comparing base [(`{sample_comparison.project_coverage_base.commit.commitid[:7]}`)](urlurl?dropdown=coverage&el=desc) to head [(`{sample_comparison.head.commit.commitid[:7]}`)](headurl?dropdown=coverage&el=desc).", + ] + + def test_new_header_section_writer_with_behind_by(self, mocker, sample_comparison): + writer = HeaderSectionWriter( + mocker.MagicMock(), + mocker.MagicMock(), + show_complexity=mocker.MagicMock(), + settings={}, + current_yaml=mocker.MagicMock(), + ) + mocker.patch( + "services.notification.notifiers.mixins.message.sections.round_number", + return_value=Decimal(0), + ) + res = list( + writer.write_section( + sample_comparison, + None, + None, + links={"pull": "urlurl", "base": "urlurl", "head": "headurl"}, + behind_by=3, + ) + ) + assert res == [ + "All modified and coverable lines are covered by tests :white_check_mark:", + f"> Project coverage is 0%. Comparing base [(`{sample_comparison.project_coverage_base.commit.commitid[:7]}`)](urlurl?dropdown=coverage&el=desc) to head [(`{sample_comparison.head.commit.commitid[:7]}`)](headurl?dropdown=coverage&el=desc).", + "> Report is 3 commits behind head on master.", + ] + + def test_new_header_section_writer_test_results_setup( + self, mocker, sample_comparison + ): + sample_comparison.context = ComparisonContext(all_tests_passed=True) + writer = HeaderSectionWriter( + mocker.MagicMock(), + mocker.MagicMock(), + show_complexity=mocker.MagicMock(), + settings={}, + current_yaml=mocker.MagicMock(), + ) + mocker.patch( + "services.notification.notifiers.mixins.message.sections.round_number", + return_value=Decimal(0), + ) + res = list( + writer.write_section( + sample_comparison, + None, + None, + links={"pull": "urlurl", "base": "urlurl", "head": "headurl"}, + ) + ) + assert res == [ + "All modified and coverable lines are covered by tests :white_check_mark:", + f"> Project coverage is 0%. Comparing base [(`{sample_comparison.project_coverage_base.commit.commitid[:7]}`)](urlurl?dropdown=coverage&el=desc) to head [(`{sample_comparison.head.commit.commitid[:7]}`)](headurl?dropdown=coverage&el=desc).", + "", + ":white_check_mark: All tests successful. No failed tests found.", + ] + + def test_new_header_section_writer_test_results_error( + self, mocker, sample_comparison + ): + sample_comparison.context = ComparisonContext( + all_tests_passed=False, + test_results_error=":x: We are unable to process any of the uploaded JUnit XML files. Please ensure your files are in the right format.", + ) + writer = HeaderSectionWriter( + mocker.MagicMock(), + mocker.MagicMock(), + show_complexity=mocker.MagicMock(), + settings={}, + current_yaml=mocker.MagicMock(), + ) + mocker.patch( + "services.notification.notifiers.mixins.message.sections.round_number", + return_value=Decimal(0), + ) + res = list( + writer.write_section( + sample_comparison, + None, + None, + links={"pull": "urlurl", "base": "urlurl", "head": "headurl"}, + ) + ) + assert res == [ + "All modified and coverable lines are covered by tests :white_check_mark:", + f"> Project coverage is 0%. Comparing base [(`{sample_comparison.project_coverage_base.commit.commitid[:7]}`)](urlurl?dropdown=coverage&el=desc) to head [(`{sample_comparison.head.commit.commitid[:7]}`)](headurl?dropdown=coverage&el=desc).", + "", + ":x: We are unable to process any of the uploaded JUnit XML files. Please ensure your files are in the right format.", + ] + + def test_new_header_section_writer_no_project_coverage( + self, mocker, sample_comparison + ): + writer = HeaderSectionWriter( + mocker.MagicMock(), + mocker.MagicMock(), + show_complexity=mocker.MagicMock(), + settings={"hide_project_coverage": True}, + current_yaml=mocker.MagicMock(), + ) + mocker.patch( + "services.notification.notifiers.mixins.message.sections.round_number", + return_value=Decimal(0), + ) + res = list( + writer.write_section( + sample_comparison, + None, + None, + links={"pull": "urlurl", "base": "urlurl", "head": "headurl"}, + ) + ) + assert res == [ + "All modified and coverable lines are covered by tests :white_check_mark:", + ] + + def test_new_header_section_writer_no_project_coverage_test_results_setup( + self, mocker, sample_comparison + ): + sample_comparison.context = ComparisonContext(all_tests_passed=True) + writer = HeaderSectionWriter( + mocker.MagicMock(), + mocker.MagicMock(), + show_complexity=mocker.MagicMock(), + settings={"hide_project_coverage": True}, + current_yaml=mocker.MagicMock(), + ) + mocker.patch( + "services.notification.notifiers.mixins.message.sections.round_number", + return_value=Decimal(0), + ) + res = list( + writer.write_section( + sample_comparison, + None, + None, + links={"pull": "urlurl", "base": "urlurl", "head": "headurl"}, + ) + ) + assert res == [ + "All modified and coverable lines are covered by tests :white_check_mark:", + "", + ":white_check_mark: All tests successful. No failed tests found.", + ] + + def test_new_header_section_writer_no_project_coverage_test_results_error( + self, mocker, sample_comparison + ): + sample_comparison.context = ComparisonContext( + all_tests_passed=False, + test_results_error=":x: We are unable to process any of the uploaded JUnit XML files. Please ensure your files are in the right format.", + ) + writer = HeaderSectionWriter( + mocker.MagicMock(), + mocker.MagicMock(), + show_complexity=mocker.MagicMock(), + settings={"hide_project_coverage": True}, + current_yaml=mocker.MagicMock(), + ) + mocker.patch( + "services.notification.notifiers.mixins.message.sections.round_number", + return_value=Decimal(0), + ) + res = list( + writer.write_section( + sample_comparison, + None, + None, + links={"pull": "urlurl", "base": "urlurl", "head": "headurl"}, + ) + ) + assert res == [ + "All modified and coverable lines are covered by tests :white_check_mark:", + "", + ":x: We are unable to process any of the uploaded JUnit XML files. Please ensure your files are in the right format.", + ] + + +class TestAnnouncementsSectionWriter(object): + def test_announcement_section_writer(self, mocker): + writer = AnnouncementSectionWriter( + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + ) + res = list(writer.write_section(mocker.MagicMock())) + assert len(res) == 1 + line = res[0] + assert line.startswith(":mega: ") + message = line[7:] + assert message in AnnouncementSectionWriter.current_active_messages + + +class TestNewFooterSectionWriter(object): + def test_footer_section_writer_in_github(self, mocker): + writer = NewFooterSectionWriter( + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + settings={}, + current_yaml=mocker.MagicMock(), + ) + mock_comparison = mocker.MagicMock() + mock_comparison.repository_service.service = "github" + res = list( + writer.write_section(mock_comparison, {}, [], links={"pull": "pull.link"}) + ) + assert res == [ + "", + "[:umbrella: View full report in Codecov by Sentry](pull.link?dropdown=coverage&src=pr&el=continue). ", + ":loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/).", + ] + + def test_footer_section_writer_in_gitlab(self, mocker): + writer = NewFooterSectionWriter( + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + settings={}, + current_yaml=mocker.MagicMock(), + ) + mock_comparison = mocker.MagicMock() + mock_comparison.repository_service.service = "gitlab" + res = list( + writer.write_section(mock_comparison, {}, [], links={"pull": "pull.link"}) + ) + assert res == [ + "", + "[:umbrella: View full report in Codecov by Sentry](pull.link?dropdown=coverage&src=pr&el=continue). ", + ":loudspeaker: Have feedback on the report? [Share it here](https://gitlab.com/codecov-open-source/codecov-user-feedback/-/issues/4).", + ] + + def test_footer_section_writer_in_bitbucket(self, mocker): + writer = NewFooterSectionWriter( + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + settings={}, + current_yaml=mocker.MagicMock(), + ) + mock_comparison = mocker.MagicMock() + mock_comparison.repository_service.service = "bitbucket" + res = list( + writer.write_section(mock_comparison, {}, [], links={"pull": "pull.link"}) + ) + assert res == [ + "", + "[:umbrella: View full report in Codecov by Sentry](pull.link?dropdown=coverage&src=pr&el=continue). ", + ":loudspeaker: Have feedback on the report? [Share it here](https://gitlab.com/codecov-open-source/codecov-user-feedback/-/issues/4).", + ] + + def test_footer_section_writer_with_project_cov_hidden(self, mocker): + writer = NewFooterSectionWriter( + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + settings={ + "layout": "newheader, files, newfooter", + "hide_project_coverage": True, + }, + current_yaml={}, + ) + mock_comparison = mocker.MagicMock() + mock_comparison.repository_service.service = "bitbucket" + res = list( + writer.write_section(mock_comparison, {}, [], links={"pull": "pull.link"}) + ) + assert res == [ + "", + ":loudspeaker: Thoughts on this report? [Let us know!](https://about.codecov.io/pull-request-comment-report/)", + ] + + +@pytest.mark.usefixtures("is_not_first_pull") +class TestCommentNotifierInNewLayout(object): + @pytest.fixture(autouse=True) + def mock_all_plans_and_tiers(self): + mock_all_plans_and_tiers() + + @pytest.mark.django_db + def test_build_message_no_base_commit_new_layout( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_without_base_with_pull, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_without_base_with_pull + comparison.repository_service.service = "github" + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "newheader, reach, diff, flags, files, newfooter" + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + "> Please [upload](https://docs.codecov.com/docs/codecov-uploader) report for BASE (`master@cdf9aa4`). [Learn more](https://docs.codecov.io/docs/error-reference#section-missing-base-commit) about missing BASE report.", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | 66.67% | [1 Missing :warning: ](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree) |", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=========================================", + " Coverage ? 60.00% ", + " Complexity ? 10 ", + "=========================================", + " Files ? 2 ", + " Lines ? 10 ", + " Branches ? 1 ", + "=========================================", + " Hits ? 6 ", + " Misses ? 3 ", + " Partials ? 1 ", + "```", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [unit](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `100.00% <100.00%> (?)` | `0.00 <0.00> (?)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `62.50% <66.67%> (ø)` | `10.00 <0.00> (?)` | |", + "", + "", + f"[:umbrella: View full report in Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue). ", + ":loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/).", + "", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_no_base_report_new_layout( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_without_base_report, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_without_base_report + comparison.repository_service.service = "github" + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "newheader, reach, diff, flags, files, newfooter", + "hide_comment_details": True, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"> Please [upload](https://docs.codecov.com/docs/codecov-uploader) report for BASE (`master@{comparison.project_coverage_base.commit.commitid[:7]}`). [Learn more](https://docs.codecov.io/docs/error-reference#section-missing-base-commit) about missing BASE report.", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | 66.67% | [1 Missing :warning: ](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree) |", + "", + "
Additional details and impacted files\n", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=========================================", + " Coverage ? 60.00% ", + " Complexity ? 10 ", + "=========================================", + " Files ? 2 ", + " Lines ? 10 ", + " Branches ? 1 ", + "=========================================", + " Hits ? 6 ", + " Misses ? 3 ", + " Partials ? 1 ", + "```", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [unit](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `100.00% <100.00%> (?)` | `0.00 <0.00> (?)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `62.50% <66.67%> (ø)` | `10.00 <0.00> (?)` | |", + "", + "
", + "", + f"[:umbrella: View full report in Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue). ", + ":loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/).", + "", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_no_project_coverage( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + comparison.repository_service.service = "github" + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "newheader, newfiles, newfooter", + "hide_project_coverage": True, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + pull_url = f"test.example.br/gh/{repository.slug}/pull/{pull.pullid}" + expected_result = [ + f"## [Codecov]({pull_url}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + "", + f"| [Files with missing lines]({pull_url}?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + f"| [file\\_1.go]({pull_url}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | 66.67% | [1 Missing :warning: ]({pull_url}?src=pr&el=tree) |", + "", + "", + ":loudspeaker: Thoughts on this report? [Let us know!](https://about.codecov.io/pull-request-comment-report/)", + "", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_no_project_coverage_files( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + comparison.repository_service.service = "github" + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "newheader, files, newfooter", + "hide_project_coverage": True, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + pull_url = f"test.example.br/gh/{repository.slug}/pull/{pull.pullid}" + expected_result = [ + f"## [Codecov]({pull_url}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + "", + f"| [Files with missing lines]({pull_url}?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + f"| [file\\_1.go]({pull_url}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | 66.67% | [1 Missing :warning: ]({pull_url}?src=pr&el=tree) |", + "", + f"| [Files with missing lines]({pull_url}?dropdown=coverage&src=pr&el=tree) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [file\\_1.go]({pull_url}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | `62.50% <66.67%> (+12.50%)` | `10.00 <0.00> (-1.00)` | :arrow_up: |", + "", + f"... and [1 file with indirect coverage changes]({pull_url}/indirect-changes?src=pr&el=tree-more)", + "", + "", + ":loudspeaker: Thoughts on this report? [Let us know!](https://about.codecov.io/pull-request-comment-report/)", + "", + ] + assert result == expected_result + for exp, res in zip(expected_result, result): + assert exp == res + + @pytest.mark.django_db + def test_build_message_no_project_coverage_condensed_yaml_configs( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + comparison.repository_service.service = "github" + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "condensed_header, condensed_files, condensed_footer", + "hide_project_coverage": True, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + pull_url = f"test.example.br/gh/{repository.slug}/pull/{pull.pullid}" + expected_result = [ + f"## [Codecov]({pull_url}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + "", + f"| [Files with missing lines]({pull_url}?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + f"| [file\\_1.go]({pull_url}?src=pr&el=tree&filepath=file_1.go#diff-ZmlsZV8xLmdv) | 66.67% | [1 Missing :warning: ]({pull_url}?src=pr&el=tree) |", + "", + "", + ":loudspeaker: Thoughts on this report? [Let us know!](https://about.codecov.io/pull-request-comment-report/)", + "", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_head_and_pull_head_differ_new_layout( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_head_and_pull_head_differ, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_head_and_pull_head_differ + comparison.repository_service.service = "github" + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "newheader, reach, diff, flags, newfooter", + "hide_comment_details": True, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"> Project coverage is 60.00%. Comparing base [(`{comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + f"> :exclamation: **Current head {comparison.head.commit.commitid[:7]} differs from pull request most recent head {comparison.enriched_pull.provider_pull['head']['commitid'][:7]}**", + "> ", + f"> Please [upload](https://docs.codecov.com/docs/codecov-uploader) reports for the commit {comparison.enriched_pull.provider_pull['head']['commitid'][:7]} to get more accurate results.", + "", + "
Additional details and impacted files\n", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=============================================", + "+ Coverage 50.00% 60.00% +10.00% ", + "+ Complexity 11 10 -1 ", + "=============================================", + " Files 2 2 ", + " Lines 6 10 +4 ", + " Branches 0 1 +1 ", + "=============================================", + "+ Hits 3 6 +3 ", + " Misses 3 3 ", + "- Partials 0 1 +1 ", + "```", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [integration](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `?` | `?` | |", + f"| [unit](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `100.00% <100.00%> (?)` | `0.00 <0.00> (?)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + "
", + "", + f"[:umbrella: View full report in Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue). ", + ":loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/).", + "", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_head_and_pull_head_differ_with_components( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_head_and_pull_head_differ, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_head_and_pull_head_differ + comparison.repository_service.service = "github" + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "newheader, reach, diff, flags, components, newfooter", + "hide_comment_details": True, + }, + notifier_site_settings=True, + current_yaml={ + "component_management": { + "individual_components": [ + {"component_id": "go_files", "paths": [r".*\.go"]}, + {"component_id": "unit_flags", "flag_regexes": [r"unit.*"]}, + ] + } + }, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"> Project coverage is 60.00%. Comparing base [(`{comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + f"> :exclamation: **Current head {comparison.head.commit.commitid[:7]} differs from pull request most recent head {comparison.enriched_pull.provider_pull['head']['commitid'][:7]}**", + "> ", + f"> Please [upload](https://docs.codecov.com/docs/codecov-uploader) reports for the commit {comparison.enriched_pull.provider_pull['head']['commitid'][:7]} to get more accurate results.", + "", + "
Additional details and impacted files\n", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=============================================", + "+ Coverage 50.00% 60.00% +10.00% ", + "+ Complexity 11 10 -1 ", + "=============================================", + " Files 2 2 ", + " Lines 6 10 +4 ", + " Branches 0 1 +1 ", + "=============================================", + "+ Hits 3 6 +3 ", + " Misses 3 3 ", + "- Partials 0 1 +1 ", + "```", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | Complexity Δ | |", + "|---|---|---|---|", + f"| [integration](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `?` | `?` | |", + f"| [unit](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `100.00% <100.00%> (?)` | `0.00 <0.00> (?)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.", + "", + f"| [Components](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/components?src=pr&el=components) | Coverage Δ | |", + "|---|---|---|", + f"| [go_files](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/components?src=pr&el=component) | `62.50% <66.67%> (+12.50%)` | :arrow_up: |", + f"| [unit_flags](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/components?src=pr&el=component) | `100.00% <100.00%> (∅)` | |", + "", + "
", + "", + f"[:umbrella: View full report in Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue). ", + ":loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/).", + "", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_team_plan_customer_missing_lines( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_head_and_pull_head_differ, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_head_and_pull_head_differ + comparison.repository_service.service = "github" + # relevant part of this test + comparison.head.commit.repository.owner.plan = "users-teamm" + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + # Irrelevant but they don't overwrite Owner's plan + "layout": "newheader, reach, diff, flags, components, newfooter", + "hide_project_coverage": True, + }, + notifier_site_settings=True, + current_yaml={ + # Irrelevant but here to ensure they don't overwrite Owner's plan + "component_management": { + "individual_components": [ + {"component_id": "go_files", "paths": [r".*\.go"]}, + {"component_id": "unit_flags", "flag_regexes": [r"unit.*"]}, + ] + } + }, + repository_service=mock_repo_provider, + ) + + pull = comparison.pull + repository = sample_comparison_head_and_pull_head_differ.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.", + f"| [Files with missing lines](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=tree) | Patch % | Lines |", + "|---|---|---|", + f"| [file\\_1.go](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree#diff-ZmlsZV8xLmdv) | 66.67% | [1 Missing :warning: ](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree) |", + "", + ":loudspeaker: Thoughts on this report? [Let us know!](https://github.com/codecov/feedback/issues/255)", + ] + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_team_plan_customer_all_lines_covered( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_coverage_carriedforward, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + sample_comparison_coverage_carriedforward.context = ComparisonContext( + all_tests_passed=True + ) + comparison = sample_comparison_coverage_carriedforward + comparison.repository_service.service = "github" + # relevant part of this test + comparison.head.commit.repository.owner.plan = "users-teamm" + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + # Irrelevant but they don't overwrite Owner's plan + "layout": "newheader, reach, diff, flags, components, newfooter", + "hide_project_coverage": True, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + pull = comparison.pull + repository = sample_comparison_coverage_carriedforward.head.commit.repository + result = notifier.build_message(comparison) + + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + "", + ":white_check_mark: All tests successful. No failed tests found.", + "", + ":loudspeaker: Thoughts on this report? [Let us know!](https://github.com/codecov/feedback/issues/255)", + ] + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_team_plan_customer_all_lines_covered_test_results_error( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_coverage_carriedforward, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + sample_comparison_coverage_carriedforward.context = ComparisonContext( + all_tests_passed=False, + test_results_error=":x: We are unable to process any of the uploaded JUnit XML files. Please ensure your files are in the right format.", + ) + comparison = sample_comparison_coverage_carriedforward + comparison.repository_service.service = "github" + # relevant part of this test + comparison.head.commit.repository.owner.plan = "users-teamm" + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + # Irrelevant but they don't overwrite Owner's plan + "layout": "newheader, reach, diff, flags, components, newfooter", + "hide_project_coverage": True, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + pull = comparison.pull + repository = sample_comparison_coverage_carriedforward.head.commit.repository + result = notifier.build_message(comparison) + + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + "", + ":x: We are unable to process any of the uploaded JUnit XML files. Please ensure your files are in the right format.", + "", + ":loudspeaker: Thoughts on this report? [Let us know!](https://github.com/codecov/feedback/issues/255)", + ] + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_team_plan_customer_all_lines_covered_no_third_line( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_coverage_carriedforward, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + sample_comparison_coverage_carriedforward.context = ComparisonContext( + all_tests_passed=False, + test_results_error=None, + ) + comparison = sample_comparison_coverage_carriedforward + comparison.repository_service.service = "github" + # relevant part of this test + comparison.head.commit.repository.owner.plan = "users-teamm" + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + # Irrelevant but they don't overwrite Owner's plan + "layout": "newheader, reach, diff, flags, components, newfooter", + "hide_project_coverage": True, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + pull = comparison.pull + repository = sample_comparison_coverage_carriedforward.head.commit.repository + result = notifier.build_message(comparison) + + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + "", + ":loudspeaker: Thoughts on this report? [Let us know!](https://github.com/codecov/feedback/issues/255)", + ] + assert result == expected_result + + @pytest.mark.django_db + def test_build_message_no_patch_or_proj_change( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison_coverage_carriedforward, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_coverage_carriedforward + pull = comparison.pull + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "layout": "newheader, reach, diff, flags, files, footer", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + repository = comparison.head.commit.repository + result = notifier.build_message(comparison) + expected_result = [ + f"## [Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + f"> Project coverage is 65.38%. Comparing base [(`{comparison.project_coverage_base.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.project_coverage_base.commit.commitid}?dropdown=coverage&el=desc) to head [(`{comparison.head.commit.commitid[:7]}`)](test.example.br/gh/{repository.slug}/commit/{comparison.head.commit.commitid}?dropdown=coverage&el=desc).", + "", + f"[![Impacted file tree graph](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/graphs/tree.svg?width=650&height=150&src=pr&token={repository.image_token})](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + f"## master #{pull.pullid} +/- ##", + "=======================================", + " Coverage 65.38% 65.38% ", + "=======================================", + " Files 15 15 ", + " Lines 208 208 ", + "=======================================", + " Hits 136 136 ", + " Misses 4 4 ", + " Partials 68 68 ", + "```", + "", + f"| [Flag](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flags) | Coverage Δ | |", + "|---|---|---|", + f"| [unit](test.example.br/gh/{repository.slug}/pull/{pull.pullid}/flags?src=pr&el=flag) | `25.00% <ø> (ø)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more." + "", + "", + "------", + "", + f"[Continue to review full report in " + f"Codecov by Sentry](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn " + "more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + f"> Powered by " + f"[Codecov](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=footer). " + f"Last update " + f"[{comparison.project_coverage_base.commit.commitid[:7]}...{comparison.head.commit.commitid[:7]}](test.example.br/gh/{repository.slug}/pull/{pull.pullid}?dropdown=coverage&src=pr&el=lastupdated). " + f"Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ] + + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + +class TestComponentWriterSection(object): + def test_write_message_component_section_empty( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison, + ): + comparison = sample_comparison + section_writer = ComponentsSectionWriter( + repository=comparison.head.commit.repository, + layout="layout", + show_complexity=False, + settings={}, + current_yaml={}, + ) + message = section_writer.write_section( + comparison=comparison, diff=None, changes=None, links={"pull": "urlurl"} + ) + expected = [] + assert message == expected + + def test_get_component_data_for_table( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison, + ): + comparison = sample_comparison + section_writer = ComponentsSectionWriter( + repository=comparison.head.commit.repository, + layout="layout", + show_complexity=False, + settings={}, + current_yaml={ + "component_management": { + "individual_components": [ + {"component_id": "go_files", "paths": [r".*\.go"]}, + {"component_id": "py_files", "paths": [r".*\.py"]}, + ] + } + }, + ) + all_components = get_components_from_yaml(section_writer.current_yaml) + comparison = sample_comparison + component_data = section_writer._get_table_data_for_components( + all_components, comparison + ) + expected_result = [ + { + "name": "go_files", + "before": ReportTotals( + files=1, + lines=4, + hits=2, + misses=2, + partials=0, + coverage="50.00000", + branches=0, + methods=0, + messages=0, + sessions=1, + complexity=11, + complexity_total=20, + diff=0, + ), + "after": ReportTotals( + files=1, + lines=8, + hits=5, + misses=3, + partials=0, + coverage="62.50000", + branches=0, + methods=0, + messages=0, + sessions=1, + complexity=10, + complexity_total=2, + diff=0, + ), + "diff": ReportTotals( + files=1, + lines=3, + hits=2, + misses=1, + partials=0, + coverage="66.66667", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + }, + { + "name": "py_files", + "before": ReportTotals( + files=1, + lines=2, + hits=1, + misses=1, + partials=0, + coverage="50.00000", + branches=0, + methods=0, + messages=0, + sessions=1, + complexity=0, + complexity_total=0, + diff=0, + ), + "after": ReportTotals( + files=1, + lines=2, + hits=1, + misses=0, + partials=1, + coverage="50.00000", + branches=1, + methods=0, + messages=0, + sessions=1, + complexity=0, + complexity_total=0, + diff=0, + ), + "diff": ReportTotals( + files=0, + lines=0, + hits=0, + misses=0, + partials=0, + coverage=None, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=None, + complexity_total=None, + diff=0, + ), + }, + ] + assert component_data == expected_result + + def test_get_component_data_for_table_no_base( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison, + ): + comparison = sample_comparison + comparison.project_coverage_base.report = None + comparison.project_coverage_base.commit = None + section_writer = ComponentsSectionWriter( + repository=comparison.head.commit.repository, + layout="layout", + show_complexity=False, + settings={}, + current_yaml={ + "component_management": { + "individual_components": [ + {"component_id": "go_files", "paths": [r".*\.go"]}, + {"component_id": "py_files", "paths": [r".*\.py"]}, + ] + } + }, + ) + all_components = get_components_from_yaml(section_writer.current_yaml) + comparison = sample_comparison + component_data = section_writer._get_table_data_for_components( + all_components, comparison + ) + expected_result = [ + { + "name": "go_files", + "before": None, + "after": ReportTotals( + files=1, + lines=8, + hits=5, + misses=3, + partials=0, + coverage="62.50000", + branches=0, + methods=0, + messages=0, + sessions=1, + complexity=10, + complexity_total=2, + diff=0, + ), + "diff": None, + }, + { + "name": "py_files", + "before": None, + "after": ReportTotals( + files=1, + lines=2, + hits=1, + misses=0, + partials=1, + coverage="50.00000", + branches=1, + methods=0, + messages=0, + sessions=1, + complexity=0, + complexity_total=0, + diff=0, + ), + "diff": None, + }, + ] + assert component_data == expected_result + + def test_write_message_component_section( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison, + ): + comparison = sample_comparison + section_writer = ComponentsSectionWriter( + repository=comparison.head.commit.repository, + layout="layout", + show_complexity=False, + settings={}, + current_yaml={ + "component_management": { + "individual_components": [ + {"component_id": "go_files", "paths": [r".*\.go"]}, + {"component_id": "py_files", "paths": [r".*\.py"]}, + ] + } + }, + ) + message = section_writer.write_section( + comparison=comparison, diff=None, changes=None, links={"pull": "urlurl"} + ) + expected = [ + "| [Components](urlurl/components?src=pr&el=components) | Coverage Δ | |", + "|---|---|---|", + "| [go_files](urlurl/components?src=pr&el=component) | `62.50% <66.67%> (+12.50%)` | :arrow_up: |", + "| [py_files](urlurl/components?src=pr&el=component) | `50.00% <ø> (ø)` | |", + ] + assert message == expected + + def test_write_message_component_section_no_base( + self, + dbsession, + mock_configuration, + mock_repo_provider, + sample_comparison, + ): + comparison = sample_comparison + comparison.project_coverage_base.report = None + comparison.project_coverage_base.commit = None + section_writer = ComponentsSectionWriter( + repository=comparison.head.commit.repository, + layout="layout", + show_complexity=False, + settings={}, + current_yaml={ + "component_management": { + "individual_components": [ + {"component_id": "go_files", "paths": [r".*\.go"]}, + {"component_id": "py_files", "paths": [r".*\.py"]}, + ] + } + }, + ) + message = section_writer.write_section( + comparison=comparison, diff=None, changes=None, links={"pull": "urlurl"} + ) + expected = [ + "| [Components](urlurl/components?src=pr&el=components) | Coverage Δ | |", + "|---|---|---|", + "| [go_files](urlurl/components?src=pr&el=component) | `62.50% <0.00%> (?)` | |", + "| [py_files](urlurl/components?src=pr&el=component) | `50.00% <0.00%> (?)` | |", + ] + assert message == expected + + +PROJECT_COVERAGE_CTA = ":information_source: You can also turn on [project coverage checks](https://docs.codecov.com/docs/common-recipe-list#set-project-coverage-checks-on-a-pull-request) and [project coverage reporting on Pull Request comment](https://docs.codecov.com/docs/common-recipe-list#show-project-coverage-changes-on-the-pull-request-comment)" + + +class TestCommentNotifierWelcome: + def test_build_message( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + result = notifier.build_message(sample_comparison) + expected_result = [ + "## Welcome to [Codecov](https://codecov.io) :tada:", + "", + "Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.", + "", + "Thanks for integrating Codecov - We've got you covered :open_umbrella:", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + def test_build_message_with_preexisting_bundle_pulls( + self, dbsession, mock_configuration, mock_repo_provider + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + + owner = OwnerFactory.create( + service="github", + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + ) + repository = RepositoryFactory.create(owner=owner) + branch = "new_branch" + # artificially create multiple pull entries with BA comments only + ba_pull_one = PullFactory.create( + repository=repository, + base=CommitFactory.create(repository=repository).commitid, + head=CommitFactory.create(repository=repository, branch=branch).commitid, + commentid=None, + bundle_analysis_commentid="98123978", + ) + ba_pull_two = PullFactory.create( + repository=repository, + base=CommitFactory.create(repository=repository).commitid, + head=CommitFactory.create(repository=repository, branch=branch).commitid, + commentid=None, + bundle_analysis_commentid="23982347", + ) + # Add these entries first so they are created before the pull with commentid only + dbsession.add_all([ba_pull_one, ba_pull_two]) + dbsession.flush() + + # Create new coverage pull + base_commit = CommitFactory.create(repository=repository) + head_commit = CommitFactory.create(repository=repository, branch=branch) + pull = PullFactory.create( + repository=repository, + base=base_commit.commitid, + head=head_commit.commitid, + ) + + head_report = Report() + head_file = ReportFile("file_1.go") + head_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + head_report.append(head_file) + + base_report = Report() + base_file = ReportFile("file_1.go") + base_file.append( + 1, ReportLine.create(coverage=0, sessions=[[0, 1]], complexity=(10, 2)) + ) + base_report.append(base_file) + + head_full_commit = FullCommit( + commit=head_commit, report=ReadOnlyReport.create_from_report(head_report) + ) + base_full_commit = FullCommit( + commit=base_commit, report=ReadOnlyReport.create_from_report(base_report) + ) + comparison = ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={ + "author": {"id": "12345", "username": "codecov-test-user"}, + "base": {"branch": "master", "commitid": base_commit.commitid}, + "head": { + "branch": "reason/some-testing", + "commitid": head_commit.commitid, + }, + "number": str(pull.pullid), + "id": str(pull.pullid), + "state": "open", + "title": "Creating new code for reasons no one knows", + }, + ), + ) + ) + dbsession.add_all([repository, base_commit, head_commit, pull]) + dbsession.flush() + + notifier = CommentNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + result = notifier.build_message(comparison) + + expected_result = [ + "## Welcome to [Codecov](https://codecov.io) :tada:", + "", + "Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.", + "", + "Thanks for integrating Codecov - We've got you covered :open_umbrella:", + ] + for exp, res in zip(expected_result, result): + assert exp == res + assert result == expected_result + + pulls_in_db = dbsession.query(Pull).all() + assert len(pulls_in_db) == 3 + + def test_should_see_project_coverage_cta_public_repo( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + + sample_comparison.head.commit.repository.private = False + + before_introduction_date = datetime(2024, 4, 1, 0, 0, 0).replace( + tzinfo=timezone.utc + ) + sample_comparison.head.commit.repository.owner.createstamp = ( + before_introduction_date + ) + + dbsession.add_all( + [ + sample_comparison.head.commit.repository, + sample_comparison.head.commit.repository.owner, + ] + ) + dbsession.flush() + + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + result = notifier.build_message(sample_comparison) + assert PROJECT_COVERAGE_CTA not in result + + after_introduction_date = datetime(2024, 6, 1, 0, 0, 0).replace( + tzinfo=timezone.utc + ) + sample_comparison.head.commit.repository.owner.createstamp = ( + after_introduction_date + ) + + dbsession.add(sample_comparison.head.commit.repository.owner) + dbsession.flush() + + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + result = notifier.build_message(sample_comparison) + assert PROJECT_COVERAGE_CTA in result + + def test_should_see_project_coverage_cta_introduction_date( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + + sample_comparison.head.commit.repository.private = True + + before_introduction_date = datetime(2024, 4, 1, 0, 0, 0).replace( + tzinfo=timezone.utc + ) + sample_comparison.head.commit.repository.owner.createstamp = ( + before_introduction_date + ) + + dbsession.add_all( + [ + sample_comparison.head.commit.repository, + sample_comparison.head.commit.repository.owner, + ] + ) + dbsession.flush() + + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + result = notifier.build_message(sample_comparison) + assert PROJECT_COVERAGE_CTA not in result + + after_introduction_date = datetime(2024, 6, 1, 0, 0, 0).replace( + tzinfo=timezone.utc + ) + sample_comparison.head.commit.repository.owner.createstamp = ( + after_introduction_date + ) + + dbsession.add(sample_comparison.head.commit.repository.owner) + dbsession.flush() + + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + result = notifier.build_message(sample_comparison) + assert PROJECT_COVERAGE_CTA in result + + def test_should_see_project_coverage_cta_team_plan( + self, dbsession, mock_configuration, mock_repo_provider, sample_comparison + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + + sample_comparison.head.commit.repository.private = True + + after_introduction_date = datetime(2024, 6, 1, 0, 0, 0).replace( + tzinfo=timezone.utc + ) + sample_comparison.head.commit.repository.owner.createstamp = ( + after_introduction_date + ) + + sample_comparison.head.commit.repository.owner.plan = PlanName.TEAM_YEARLY.value + + dbsession.add_all( + [ + sample_comparison.head.commit.repository, + sample_comparison.head.commit.repository.owner, + ] + ) + dbsession.flush() + + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + result = notifier.build_message(sample_comparison) + assert PROJECT_COVERAGE_CTA not in result + + sample_comparison.head.commit.repository.owner.plan = DEFAULT_FREE_PLAN + + dbsession.add(sample_comparison.head.commit.repository.owner) + dbsession.flush() + + notifier = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=mock_repo_provider, + ) + result = notifier.build_message(sample_comparison) + assert PROJECT_COVERAGE_CTA in result + + +class TestMessagesToUserSection(object): + @pytest.mark.parametrize( + "repo, is_enterprise, owner_has_apps, expected", + [ + pytest.param( + RepositoryFactory(owner__service="github", owner__integration_id=None), + False, + False, + ":exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.", + id="owner_not_using_app_should_emit_warning", + ), + pytest.param( + RepositoryFactory(owner__service="github", owner__integration_id=None), + True, + False, + "", + id="is_enterprise_should_not_emit_warning", + ), + pytest.param( + RepositoryFactory( + owner__service="github", owner__integration_id="integration_id" + ), + False, + False, + "", + id="owner_using_app_legacy_should_not_emit_warning", + ), + pytest.param( + RepositoryFactory(owner__service="github", owner__integration_id=None), + False, + True, + "", + id="owner_using_app_should_not_emit_warning", + ), + ], + ) + def test_install_github_app_warning( + self, + mocker, + repo: Repository, + is_enterprise: bool, + owner_has_apps: bool, + expected: str, + ): + if owner_has_apps: + repo.owner.github_app_installations = [ + GithubAppInstallation(owner=repo.owner, app_id=10, installation_id=1000) + ] + commit = CommitFactory(repository=repo) + mock_head = mocker.MagicMock(commit=commit) + mocker.patch( + "services.notification.notifiers.mixins.message.sections.is_enterprise", + return_value=is_enterprise, + ) + fake_comparison = mocker.MagicMock(head=mock_head) + assert ( + MessagesToUserSectionWriter( + repo, mocker.MagicMock(), False, mocker.MagicMock(), {} + )._write_install_github_app_warning(fake_comparison) + == expected + ) + + @pytest.mark.parametrize( + "commit, upload_diff, has_project_status, is_coverage_drop_significant, expected", + [ + pytest.param( + CommitFactory(state="pending"), + [ReportUploadedCount(flag="unit", base_count=4, head_count=3)], + True, + True, + "", + id="commit_not_ready_should_not_send_warning", + ), + pytest.param( + CommitFactory(state="complete"), + [ReportUploadedCount(flag="unit", base_count=4, head_count=3)], + False, + True, + "", + id="no_project_status_should_not_send_warning", + ), + pytest.param( + CommitFactory(state="complete"), + [ReportUploadedCount(flag="unit", base_count=4, head_count=3)], + True, + False, + "", + id="no_significant_drop_in_coverage_should_not_send_warning", + ), + pytest.param( + CommitFactory(state="complete"), + [], + True, + True, + "", + id="no_upload_diff_should_not_send_warning", + ), + pytest.param( + CommitFactory( + state="complete", + commitid="cf59ea49c149c8ef5d7303834362a4d27bbd4a28", + ), + [ + ReportUploadedCount(flag="unit", base_count=4, head_count=3), + ReportUploadedCount(flag="local", base_count=2, head_count=1), + ], + True, + True, + ( + "> :exclamation: There is a different number of reports uploaded between BASE (bbd4a28) and HEAD (cf59ea4). Click for more details." + + "\n> " + + "\n>
HEAD has 2 uploads less than BASE" + + "\n>" + + "\n>| Flag | BASE (bbd4a28) | HEAD (cf59ea4) |" + + "\n>|------|------|------|" + + "\n>|unit|4|3|" + + "\n>|local|2|1|" + + "\n>
" + ), + id="should_send_warning_2_uploads_less", + ), + pytest.param( + CommitFactory( + state="complete", + commitid="cf59ea49c149c8ef5d7303834362a4d27bbd4a28", + ), + [ + ReportUploadedCount(flag="unit", base_count=4, head_count=3), + ], + True, + True, + ( + "> :exclamation: There is a different number of reports uploaded between BASE (bbd4a28) and HEAD (cf59ea4). Click for more details." + + "\n> " + + "\n>
HEAD has 1 upload less than BASE" + + "\n>" + + "\n>| Flag | BASE (bbd4a28) | HEAD (cf59ea4) |" + + "\n>|------|------|------|" + + "\n>|unit|4|3|" + + "\n>
" + ), + id="should_send_warning_1_upload_less", + ), + ], + ) + def test_different_upload_count_warning( + self, + mocker, + commit: Commit, + upload_diff: List[ReportUploadedCount], + has_project_status: bool, + is_coverage_drop_significant: bool, + expected: str, + ): + yaml = {"coverage": {"status": {"project": has_project_status}}} + mocker.patch( + "services.notification.notifiers.mixins.message.sections.is_coverage_drop_significant", + return_value=is_coverage_drop_significant, + ) + mock_head = mocker.MagicMock(commit=commit) + mock_project_coverage_base = mocker.MagicMock( + commit=CommitFactory( + repository=commit.repository, + commitid="bbd4a28cf59ea49c149c8ef5d7303834362a4d27", + ) + ) + fake_comparison = mocker.MagicMock( + head=mock_head, project_coverage_base=mock_project_coverage_base + ) + fake_comparison.get_reports_uploaded_count_per_flag_diff.return_value = ( + upload_diff + ) + assert ( + MessagesToUserSectionWriter( + commit.repository, mocker.MagicMock(), False, mocker.MagicMock(), yaml + )._write_different_upload_count_warning(fake_comparison) + == expected + ) diff --git a/apps/worker/services/notification/notifiers/tests/unit/test_comment_conditions.py b/apps/worker/services/notification/notifiers/tests/unit/test_comment_conditions.py new file mode 100644 index 0000000000..3f805746c2 --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/unit/test_comment_conditions.py @@ -0,0 +1,302 @@ +from typing import Dict, List + +import pytest +from shared.validation.types import ( + CoverageCommentRequiredChanges, + CoverageCommentRequiredChangesANDGroup, +) +from shared.yaml import UserYaml + +from database.enums import Decoration +from database.models.core import Repository +from services.comparison import ComparisonProxy +from services.notification.notifiers.comment import CommentNotifier +from services.notification.notifiers.comment.conditions import ( + HasEnoughRequiredChanges, + NoAutoActivateMessageIfAutoActivateIsOff, +) + + +def _get_notifier( + repository: Repository, + required_changes: CoverageCommentRequiredChangesANDGroup, + repo_provider, +): + return CommentNotifier( + repository=repository, + title="title", + notifier_yaml_settings={"require_changes": required_changes}, + notifier_site_settings=True, + current_yaml={}, + repository_service=repo_provider, + ) + + +def _get_mock_compare_result(file_affected: str, lines_affected: List[str]) -> Dict: + return { + "diff": { + "files": { + file_affected: { + "type": "modified", + "before": None, + "segments": [ + { + "header": lines_affected, + "lines": [ + " Overview", + " --------", + " ", + "-Main website: `Codecov `_.", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + "+website: `Codecov `_.", + " ", + " .. code-block:: shell-session", + " ", + ], + }, + ], + "stats": {"added": 3, "removed": 3}, + } + } + } + } + + +@pytest.mark.parametrize( + "comparison_name, condition, expected", + [ + pytest.param( + "sample_comparison", + [CoverageCommentRequiredChanges.any_change.value], + True, + id="any_change_comparison_with_changes", + ), + pytest.param( + "sample_comparison_without_base_report", + [CoverageCommentRequiredChanges.any_change.value], + False, + id="any_change_comparison_without_base", + ), + pytest.param( + "sample_comparison_no_change", + [CoverageCommentRequiredChanges.any_change.value], + False, + id="any_change_sample_comparison_no_change", + ), + pytest.param( + "sample_comparison", + [CoverageCommentRequiredChanges.coverage_drop.value], + False, + id="coverage_drop_comparison_with_positive_changes", + ), + pytest.param( + "sample_comparison_no_change", + [CoverageCommentRequiredChanges.coverage_drop.value], + False, + id="coverage_drop_sample_comparison_no_change", + ), + pytest.param( + "sample_comparison_without_base_report", + [CoverageCommentRequiredChanges.coverage_drop.value], + True, + id="coverage_drop_comparison_without_base", + ), + ], +) +def test_condition_different_comparisons_no_diff( + comparison_name, condition, expected, mock_repo_provider, request +): + comparison = request.getfixturevalue(comparison_name) + # There's no diff between HEAD and BASE so we can't calculate unexpected coverage. + # Any change then needs to be a coverage change + mock_repo_provider.get_compare.return_value = {"diff": {"files": {}, "commits": []}} + notifier = _get_notifier( + comparison.head.commit.repository, condition, mock_repo_provider + ) + assert HasEnoughRequiredChanges.check_condition(notifier, comparison) == expected + + +@pytest.mark.parametrize( + "condition, expected", + [ + pytest.param( + [CoverageCommentRequiredChanges.any_change.value], False, id="any_change" + ), + pytest.param( + [CoverageCommentRequiredChanges.coverage_drop.value], + False, + id="coverage_drop", + ), + pytest.param( + [CoverageCommentRequiredChanges.uncovered_patch.value], + False, + id="uncovered_patch", + ), + pytest.param( + [CoverageCommentRequiredChanges.no_requirements.value], + True, + id="no_requirements", + ), + ], +) +def test_condition_exact_same_report_coverage_not_affected_by_diff( + sample_comparison_no_change, mock_repo_provider, condition, expected +): + mock_repo_provider.get_compare.return_value = _get_mock_compare_result( + "README.md", ["5", "8", "5", "9"] + ) + notifier = _get_notifier( + sample_comparison_no_change.head.commit.repository, + condition, + mock_repo_provider, + ) + assert ( + HasEnoughRequiredChanges.check_condition(notifier, sample_comparison_no_change) + == expected + ) + + +@pytest.mark.parametrize( + "condition, expected", + [ + pytest.param( + [CoverageCommentRequiredChanges.any_change.value], True, id="any_change" + ), + pytest.param( + [CoverageCommentRequiredChanges.coverage_drop.value], + False, + id="coverage_drop", + ), + pytest.param( + [CoverageCommentRequiredChanges.uncovered_patch.value], + False, + id="uncovered_patch", + ), + pytest.param( + [CoverageCommentRequiredChanges.no_requirements.value], + True, + id="no_requirements", + ), + ], +) +def test_condition_exact_same_report_coverage_affected_by_diff( + sample_comparison_no_change, mock_repo_provider, condition, expected +): + mock_repo_provider.get_compare.return_value = _get_mock_compare_result( + "file_1.go", ["4", "8", "4", "8"] + ) + notifier = _get_notifier( + sample_comparison_no_change.head.commit.repository, + condition, + mock_repo_provider, + ) + assert ( + HasEnoughRequiredChanges.check_condition(notifier, sample_comparison_no_change) + == expected + ) + + +@pytest.mark.parametrize( + "affected_lines, expected", + [ + pytest.param(["4", "8", "4", "8"], False, id="patch_100%_covered"), + pytest.param(["1", "8", "1", "8"], True, id="patch_NOT_100%_covered"), + ], +) +def test_uncovered_patch( + sample_comparison_no_change, mock_repo_provider, affected_lines, expected +): + mock_repo_provider.get_compare.return_value = _get_mock_compare_result( + "file_1.go", affected_lines + ) + notifier = _get_notifier( + sample_comparison_no_change.head.commit.repository, + [CoverageCommentRequiredChanges.uncovered_patch.value], + mock_repo_provider, + ) + assert ( + HasEnoughRequiredChanges.check_condition(notifier, sample_comparison_no_change) + == expected + ) + + +@pytest.mark.parametrize( + "comparison_name, yaml, expected", + [ + pytest.param("sample_comparison", {}, False, id="positive_change"), + pytest.param( + "sample_comparison_negative_change", + {}, + True, + id="negative_change_no_extra_config", + ), + pytest.param( + "sample_comparison_negative_change", + {"coverage": {"status": {"project": True}}}, + True, + id="negative_change_bool_config", + ), + pytest.param( + "sample_comparison_negative_change", + {"coverage": {"status": {"project": {"threshold": 10}}}}, + False, + id="negative_change_high_threshold", + ), + ], +) +def test_coverage_drop_with_different_project_configs( + comparison_name, yaml, expected, request +): + comparison: ComparisonProxy = request.getfixturevalue(comparison_name) + comparison.comparison.current_yaml = UserYaml(yaml) + notifier = _get_notifier( + comparison.head.commit.repository, + [CoverageCommentRequiredChanges.coverage_drop.value], + None, + ) + assert HasEnoughRequiredChanges.check_condition(notifier, comparison) == expected + + +@pytest.mark.parametrize( + "decoration_type, plan_auto_activate, expected", + [ + pytest.param( + Decoration.upgrade, False, False, id="upgrade_no_auto_activate__dont_send" + ), + pytest.param(Decoration.upgrade, True, True, id="upgrade_auto_activate__send"), + pytest.param( + Decoration.upload_limit, + False, + True, + id="other_decoration_no_auto_activate__send", + ), + pytest.param( + Decoration.upload_limit, + True, + True, + id="other_decoration_auto_activate__send", + ), + ], +) +def test_no_auto_activate_message_if_auto_activate_is_off( + sample_comparison_no_change, + mock_repo_provider, + decoration_type, + plan_auto_activate, + expected, +): + notifier = _get_notifier( + sample_comparison_no_change.head.commit.repository, + [CoverageCommentRequiredChanges.any_change.value], + mock_repo_provider, + ) + notifier.decoration_type = decoration_type + notifier.repository.owner.plan_auto_activate = plan_auto_activate + assert ( + NoAutoActivateMessageIfAutoActivateIsOff.check_condition( + notifier, sample_comparison_no_change + ) + == expected + ) diff --git a/apps/worker/services/notification/notifiers/tests/unit/test_generics.py b/apps/worker/services/notification/notifiers/tests/unit/test_generics.py new file mode 100644 index 0000000000..556a77cefc --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/unit/test_generics.py @@ -0,0 +1,270 @@ +import httpx + +from database.tests.factories import RepositoryFactory +from services.notification.notifiers.generics import ( + RequestsYamlBasedNotifier, + StandardNotifier, +) + + +class SampleNotifierForTest(StandardNotifier): + def build_payload(self, comparison): + return {"commitid": comparison.head.commit.commitid} + + def send_actual_notification(self, data): + return { + "notification_attempted": True, + "notification_successful": True, + "explanation": None, + } + + +class TestStandardkNotifier(object): + def test_is_enabled_without_site_settings(self, dbsession): + repository = RepositoryFactory.create( + owner__username="test_is_enabled_without_site_settings" + ) + dbsession.add(repository) + dbsession.flush() + notifier = StandardNotifier( + repository=repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=False, + current_yaml={}, + repository_service=None, + ) + assert not notifier.is_enabled() + + def test_is_enabled_with_site_settings_no_special_config(self, dbsession): + repository = RepositoryFactory.create( + owner__username="test_is_enabled_without_site_settings" + ) + dbsession.add(repository) + dbsession.flush() + notifier = StandardNotifier( + repository=repository, + title="title", + notifier_yaml_settings={"url": "https://example.com/myexample"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + assert notifier.is_enabled() + + def test_is_enabled_with_site_settings_no_url(self, dbsession): + repository = RepositoryFactory.create( + owner__username="test_is_enabled_without_site_settings" + ) + dbsession.add(repository) + dbsession.flush() + notifier = StandardNotifier( + repository=repository, + title="title", + notifier_yaml_settings={"field_1": "something"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + assert not notifier.is_enabled() + + def test_is_enabled_with_site_settings_whitelisted_url(self, dbsession): + repository = RepositoryFactory.create( + owner__username="test_is_enabled_without_site_settings" + ) + dbsession.add(repository) + dbsession.flush() + notifier = StandardNotifier( + repository=repository, + title="title", + notifier_yaml_settings={"url": "https://example.com/myexample"}, + notifier_site_settings=["example.com"], + current_yaml={}, + repository_service=None, + ) + assert notifier.is_enabled() + + def test_is_enabled_with_site_settings_not_whitelisted_url(self, dbsession): + repository = RepositoryFactory.create( + owner__username="test_is_enabled_without_site_settings" + ) + dbsession.add(repository) + dbsession.flush() + notifier = StandardNotifier( + repository=repository, + title="title", + notifier_yaml_settings={"url": "https://example.com/myexample"}, + notifier_site_settings=["badexample.com"], + current_yaml={}, + repository_service=None, + ) + assert not notifier.is_enabled() + + def test_should_notify_comparison(self, sample_comparison): + notifier = StandardNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"url": "https://example.com/myexample"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + assert notifier.should_notify_comparison(sample_comparison) + + def test_should_notify_comparison_bad_branch(self, sample_comparison): + notifier = StandardNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "url": "https://example.com/myexample", + "branches": ["test-.*"], + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + assert not notifier.should_notify_comparison(sample_comparison) + + def test_should_notify_comparison_good_branch(self, sample_comparison): + notifier = StandardNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "url": "https://example.com/myexample", + "branches": ["new_.*"], + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + assert notifier.should_notify_comparison(sample_comparison) + + def test_should_notify_comparison_not_above_threshold(self, sample_comparison): + notifier = StandardNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "url": "https://example.com/myexample", + "threshold": 80.0, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + assert not notifier.should_notify_comparison(sample_comparison) + + def test_should_notify_comparison_no_base( + self, sample_comparison_without_base_report + ): + notifier = StandardNotifier( + repository=sample_comparison_without_base_report.head.commit.repository, + title="title", + notifier_yaml_settings={ + "url": "https://example.com/myexample", + "threshold": 80.0, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + assert not notifier.should_notify_comparison( + sample_comparison_without_base_report + ) + + def test_should_notify_comparison_is_above_threshold(self, sample_comparison): + notifier = StandardNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "url": "https://example.com/myexample", + "threshold": 8.0, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + assert notifier.should_notify_comparison(sample_comparison) + + def test_should_notify_comparison_is_above_threshold_no_coverage( + self, sample_comparison + ): + actual_comparison = sample_comparison.get_filtered_comparison( + path_patterns=[".*txt"], flags=None + ) + notifier = StandardNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "url": "https://example.com/myexample", + "threshold": 8.0, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + assert not notifier.should_notify_comparison(actual_comparison) + + def test_notify(self, sample_comparison): + notifier = SampleNotifierForTest( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "url": "https://example.com/myexample", + "threshold": 8.0, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + res = notifier.notify(sample_comparison) + assert res.notification_attempted + assert res.notification_successful + assert res.explanation is None + assert res.data_sent == {"commitid": sample_comparison.head.commit.commitid} + assert res.data_received is None + + def test_notify_should_not_notify(self, sample_comparison, mocker): + notifier = SampleNotifierForTest( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "url": "https://example.com/myexample", + "threshold": 8.0, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + mocker.patch.object( + SampleNotifierForTest, "should_notify_comparison", return_value=False + ) + res = notifier.notify(sample_comparison) + assert not res.notification_attempted + assert res.notification_successful is None + assert res.explanation == "Did not fit criteria" + assert res.data_sent is None + assert res.data_received is None + + +class TestRequestsYamlBasedNotifier(object): + def test_send_notification_exception(self, mocker, sample_comparison): + mocked_post = mocker.patch.object(httpx.Client, "post") + mocked_post.side_effect = httpx.HTTPError("message") + notifier = RequestsYamlBasedNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "url": "https://example.com/myexample", + "threshold": 8.0, + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + data = {} + res = notifier.send_actual_notification(data) + assert res == { + "notification_attempted": True, + "notification_successful": False, + "explanation": "connection_issue", + } diff --git a/apps/worker/services/notification/notifiers/tests/unit/test_gitter.py b/apps/worker/services/notification/notifiers/tests/unit/test_gitter.py new file mode 100644 index 0000000000..e61446b2a7 --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/unit/test_gitter.py @@ -0,0 +1,39 @@ +from decimal import Decimal + +from services.notification.notifiers.gitter import GitterNotifier +from services.repository import get_repo_provider_service + + +def test_build_payload_without_special_config( + dbsession, mock_configuration, sample_comparison +): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + notifier = GitterNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={}, + repository_service=get_repo_provider_service( + sample_comparison.head.commit.repository + ), + ) + result = notifier.build_payload(comparison) + commit = comparison.head.commit + repository = commit.repository + text = f"Coverage *increased* +10.00% on `new_branch` is `60.00000%` via test.example.br/gh/{repository.slug}/commit/{commit.commitid}" + expected_result = { + "message": text, + "branch": "new_branch", + "pr": comparison.pull.pullid, + "commit": commit.commitid, + "commit_short": commit.commitid[:7], + "text": "increased", + "commit_url": f"https://github.com/{repository.slug}/commit/{commit.commitid}", + "codecov_url": f"test.example.br/gh/{repository.slug}/commit/{commit.commitid}", + "coverage": "60.00000", + "coverage_change": Decimal("10.00"), + } + assert result["message"] == expected_result["message"] + assert result == expected_result diff --git a/apps/worker/services/notification/notifiers/tests/unit/test_hipchat.py b/apps/worker/services/notification/notifiers/tests/unit/test_hipchat.py new file mode 100644 index 0000000000..169bf101f3 --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/unit/test_hipchat.py @@ -0,0 +1,122 @@ +from services.notification.notifiers.hipchat import HipchatNotifier + + +class TestHipchatkNotifier(object): + def test_build_payload_without_special_config( + self, dbsession, mock_configuration, sample_comparison + ): + url = "test.example.br" + mock_configuration.params["setup"]["codecov_dashboard_url"] = url + comparison = sample_comparison + notifier = HipchatNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = notifier.build_payload(comparison) + commit = comparison.head.commit + repository = commit.repository + text = f'Coverage for {repository.slug} increased +10.00% on new_branch is 60.00000% # via {commit.commitid[:7]}' + expected_result = { + "card": None, + "color": "green", + "from": "Codecov", + "message": text, + "message_format": "html", + "notify": False, + } + assert result["message"] == expected_result["message"] + assert result == expected_result + + def test_build_payload_without_base_report( + self, sample_comparison, mock_configuration + ): + sample_comparison.project_coverage_base.report = None + url = "test.example.br" + mock_configuration.params["setup"]["codecov_dashboard_url"] = url + comparison = sample_comparison + notifier = HipchatNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = notifier.build_payload(comparison) + commit = comparison.head.commit + repository = commit.repository + text = f'Coverage for {repository.slug} on new_branch is 60.00000% # via {commit.commitid[:7]}' + expected_result = { + "card": None, + "color": "gray", + "from": "Codecov", + "message": text, + "message_format": "html", + "notify": False, + } + assert result["message"] == expected_result["message"] + assert result == expected_result + + def test_build_payload_with_card(self, sample_comparison, mock_configuration): + url = "test.example.br" + mock_configuration.params["setup"]["codecov_dashboard_url"] = url + notifier = HipchatNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"card": True}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = notifier.build_payload(sample_comparison) + commit = sample_comparison.head.commit + repository = commit.repository + text = f'Coverage for {repository.slug} increased +10.00% on new_branch is 60.00000% # via {commit.commitid[:7]}' + expected_result = { + "card": { + "attributes": [ + { + "label": "Author", + "value": { + "label": commit.author.username, + "url": f"test.example.br/gh/{repository.slug}/commit/{commit.commitid}", + }, + }, + { + "label": "Commit", + "value": { + "label": commit.commitid[:7], + "url": f"test.example.br/gh/{repository.slug}/commit/{commit.commitid}", + }, + }, + { + "label": "Compare", + "value": {"label": "+10.00%", "style": "lozenge-success"}, + }, + ], + "description": { + "format": "html", + "value": f"Coverage for {repository.slug} on new_branch is now 60.00%", + }, + "format": "compact", + "icon": { + "url": f"test.example.br/gh/{repository.slug}/commit/{commit.commitid}/graphs/sunburst.svg?size=100" + }, + "id": commit.commitid, + "style": "application", + "title": f"Codecov ⋅ {repository.slug} on new_branch", + "url": f"test.example.br/gh/{repository.slug}/commit/{commit.commitid}", + }, + "color": "green", + "from": "Codecov", + "message": text, + "message_format": "html", + "notify": False, + } + assert result["message"] == expected_result["message"] + assert result["card"] == expected_result["card"] + assert result == expected_result diff --git a/apps/worker/services/notification/notifiers/tests/unit/test_irc.py b/apps/worker/services/notification/notifiers/tests/unit/test_irc.py new file mode 100644 index 0000000000..9b11ef8b61 --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/unit/test_irc.py @@ -0,0 +1,72 @@ +import socket + +from services.notification.notifiers.irc import IRCNotifier + + +class TestIRCNotifier(object): + def test_build_payload(self, dbsession, mock_configuration, sample_comparison): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + notifier = IRCNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"server": ""}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = notifier.build_payload(comparison) + commit = comparison.head.commit + repository = commit.repository + text = f"Coverage for {repository.slug} *increased* `+10.00%` on `new_branch` is `60.00000%` via `{commit.commitid[:7]}`" + expected_result = {"message": text} + assert result == expected_result + + def test_send_actual_notification( + self, dbsession, mock_configuration, sample_comparison, mocker + ): + mocked_connection = mocker.patch( + "services.notification.notifiers.irc.socket.socket" + ).return_value + mocked_connection.recv.side_effect = [ + b":956adb1eca91.example.com NOTICE * :*** Looking up your hostname...\r\n", + b"PING :fhkFTdCsVx\r\n:956adb1eca91.example.com NOTICE codecov :*** I...\r\n", + b":956adb1eca91.example.com NOTICE codecov :*** Could not resolve you...\r\n", + socket.timeout, + b":956adb1eca91.example.com 451 codecov PRIVMSG :You have not registe...\r\n", + b":956adb1eca91.example.com 001 codecov :Welcome to the Omega IRC Net... USE", + b"RLEN=11 WHOX :are supported by this server\r\n:956adb1eca91.example...\\ |", + b" |_) | _| |_ | | \\ \\ | |____ | (_| |\r\n:956adb1eca91.example.co...6adb", + b"1eca91.example.com 372 codecov :- \\ \\ `. `-._ ... ", + b" \\\r\n:956adb1eca91.example.com 372 codecov :- | * IRC:...----", + b"-----------------------\r\n:956adb1eca91.example.com 372 codecov :-... war", + b"m welcome from the InspIRCd-Docker Team, too!\r\n:956adb1eca91.exam...\r\n", + b":NickServ!services@services.localhost.net NOTICE codecov :This nick...\r\n", + b"", + socket.timeout, + b":codecov!codecov@172.22.0.1 JOIN :#samplechannel\r\n:956adb1eca91.e...\r\n", + socket.timeout, + socket.timeout, + ] + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + notifier = IRCNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "server": "ircserver", + "channel": "#samplechannel", + "password": "s3cret", + "nickserv_password": "password", + }, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + commit = comparison.head.commit + repository = commit.repository + text = f"Coverage for {repository.slug} *increased* `+10.00%` on `new_branch` is `60.00000%` via `{commit.commitid[:7]}`" + data = {"message": text} + result = notifier.send_actual_notification(data) + expected_result = {"successful": True, "reason": None} + assert result == expected_result diff --git a/apps/worker/services/notification/notifiers/tests/unit/test_slack.py b/apps/worker/services/notification/notifiers/tests/unit/test_slack.py new file mode 100644 index 0000000000..68e1af1674 --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/unit/test_slack.py @@ -0,0 +1,137 @@ +from services.notification.notifiers.slack import SlackNotifier + + +class TestSlackNotifier(object): + def test_build_payload_without_attachments( + self, dbsession, mock_configuration, sample_comparison + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + notifier = SlackNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = notifier.build_payload(comparison) + commit = comparison.head.commit + repository = commit.repository + text = f"Coverage for *increased* `` on `new_branch` is `60.00000%` via ``" + expected_result = { + "attachments": [], + "author_link": f"test.example.br/gh/{commit.repository.slug}/commit/{commit.commitid}", + "author_name": "Codecov", + "text": text, + } + assert result == expected_result + + def test_build_payload_with_attachments( + self, dbsession, mock_configuration, sample_comparison + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + notifier = SlackNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"attachments": ["sunburst"]}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = notifier.build_payload(comparison) + commit = comparison.head.commit + repository = commit.repository + text = f"Coverage for *increased* `` on `new_branch` is `60.00000%` via ``" + expected_result = { + "attachments": [ + { + "color": "good", + "fallback": "Commit sunburst attachment", + "image_url": f"test.example.br/gh/{commit.repository.slug}/commit/{commit.commitid}/graphs/sunburst.svg?size=100", + "title": "Commit Sunburst", + "title_link": f"test.example.br/gh/{commit.repository.slug}/commit/{commit.commitid}", + } + ], + "author_link": f"test.example.br/gh/{commit.repository.slug}/commit/{commit.commitid}", + "author_name": "Codecov", + "text": text, + } + assert result["attachments"][0] == expected_result["attachments"][0] + assert result == expected_result + + def test_build_payload_with_message( + self, dbsession, mock_configuration, sample_comparison + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison + notifier = SlackNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"message": "This is a sample"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = notifier.build_payload(comparison) + commit = comparison.head.commit + expected_result = { + "attachments": [], + "author_link": f"test.example.br/gh/{commit.repository.slug}/commit/{commit.commitid}", + "author_name": "Codecov", + "text": "This is a sample", + } + assert result == expected_result + + def test_build_payload_without_pull( + self, sample_comparison_without_pull, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_without_pull + commit = sample_comparison_without_pull.head.commit + repository = commit.repository + notifier = SlackNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = notifier.build_payload(comparison) + text = f"Coverage for *increased* `` on `new_branch` is `60.00000%` via ``" + expected_result = { + "attachments": [], + "author_link": f"test.example.br/gh/{commit.repository.slug}/commit/{commit.commitid}", + "author_name": "Codecov", + "text": text, + } + assert result["text"] == expected_result["text"] + assert result == expected_result + + def test_build_payload_without_base( + self, sample_comparison_without_base_report, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_without_base_report + commit = comparison.head.commit + repository = commit.repository + notifier = SlackNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + ) + result = notifier.build_payload(comparison) + text = f"Coverage for on `new_branch` is `60.00000%` via ``" + expected_result = { + "attachments": [], + "author_link": f"test.example.br/gh/{commit.repository.slug}/commit/{commit.commitid}", + "author_name": "Codecov", + "text": text, + } + assert result["text"] == expected_result["text"] + assert result == expected_result diff --git a/apps/worker/services/notification/notifiers/tests/unit/test_status.py b/apps/worker/services/notification/notifiers/tests/unit/test_status.py new file mode 100644 index 0000000000..ab6b26e6f2 --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/unit/test_status.py @@ -0,0 +1,2801 @@ +from unittest.mock import MagicMock + +import pytest +from mock import AsyncMock +from shared.reports.readonly import ReadOnlyReport +from shared.reports.reportfile import ReportFile +from shared.reports.resources import Report +from shared.reports.types import ReportLine, ReportTotals +from shared.torngit.exceptions import ( + TorngitClientError, + TorngitRepoNotFoundError, + TorngitServerUnreachableError, +) +from shared.torngit.status import Status +from shared.typings.torngit import GithubInstallationInfo, TorngitInstanceData +from shared.yaml.user_yaml import UserYaml + +from database.enums import Notification +from database.tests.factories.core import CommitFactory +from services.comparison import ComparisonProxy +from services.comparison.types import FullCommit +from services.decoration import Decoration +from services.notification.notifiers.base import NotificationResult +from services.notification.notifiers.mixins.status import ( + HELPER_TEXT_MAP, + HelperTextKey, + HelperTextTemplate, +) +from services.notification.notifiers.status import ( + ChangesStatusNotifier, + PatchStatusNotifier, + ProjectStatusNotifier, +) +from services.notification.notifiers.status.base import StatusNotifier +from services.urls import get_pull_url + + +def test_notification_type(mocker): + assert ( + ProjectStatusNotifier( + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + ).notification_type + == Notification.status_project + ) + assert ( + ChangesStatusNotifier( + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + ).notification_type + == Notification.status_changes + ) + + +@pytest.fixture +def multiple_diff_changes(): + return { + "files": { + "modified.py": { + "before": None, + "segments": [ + { + "header": ["20", "8", "20", "8"], + "lines": [ + " return k * k", + " ", + " ", + "-def k(l):", + "- return 2 * l", + "+def k(var):", + "+ return 2 * var", + " ", + " ", + " def sample_function():", + ], + } + ], + "stats": {"added": 2, "removed": 2}, + "type": "modified", + }, + "renamed.py": { + "before": "old_renamed.py", + "segments": [], + "stats": {"added": 0, "removed": 0}, + "type": "modified", + }, + "renamed_with_changes.py": { + "before": "old_renamed_with_changes.py", + "segments": [], + "stats": {"added": 0, "removed": 0}, + "type": "modified", + }, + "added.py": { + "before": None, + "segments": [ + { + "header": ["0", "0", "1", ""], + "lines": ["+This is an explanation"], + } + ], + "stats": {"added": 1, "removed": 0}, + "type": "new", + }, + "deleted.py": { + "before": "tests/test_sample.py", + "stats": {"added": 0, "removed": 0}, + "type": "deleted", + }, + } + } + + +@pytest.fixture +def comparison_100_percent_patch(sample_comparison): + first_report = Report() + second_report = Report() + # MODIFIED FILE + first_modified_file = ReportFile("modified.py") + first_modified_file.append(17, ReportLine.create(coverage=0)) + first_modified_file.append(18, ReportLine.create(coverage=0)) + first_modified_file.append(19, ReportLine.create(coverage=1)) + first_modified_file.append(20, ReportLine.create(coverage=0)) + first_modified_file.append(21, ReportLine.create(coverage=1)) + first_modified_file.append(22, ReportLine.create(coverage=1)) + first_modified_file.append(23, ReportLine.create(coverage=0)) + first_modified_file.append(24, ReportLine.create(coverage=0)) + first_report.append(first_modified_file) + second_modified_file = ReportFile("modified.py") + second_modified_file.append(18, ReportLine.create(coverage=0)) + second_modified_file.append(19, ReportLine.create(coverage=0)) + second_modified_file.append(20, ReportLine.create(coverage=0)) + second_modified_file.append(21, ReportLine.create(coverage=0)) + second_modified_file.append(22, ReportLine.create(coverage=0)) + second_modified_file.append(23, ReportLine.create(coverage=1)) + second_modified_file.append(24, ReportLine.create(coverage=1)) + second_report.append(second_modified_file) + sample_comparison.project_coverage_base.report = ReadOnlyReport.create_from_report( + first_report + ) + sample_comparison.head.report = ReadOnlyReport.create_from_report(second_report) + return sample_comparison + + +@pytest.fixture +def comparison_with_multiple_changes(sample_comparison): + first_report = Report() + second_report = Report() + # DELETED FILE + first_deleted_file = ReportFile("deleted.py") + first_deleted_file.append(10, ReportLine.create(coverage=1)) + first_deleted_file.append(12, ReportLine.create(coverage=0)) + first_report.append(first_deleted_file) + # ADDED FILE + second_added_file = ReportFile("added.py") + second_added_file.append(99, ReportLine.create(coverage=1)) + second_added_file.append(101, ReportLine.create(coverage=0)) + second_report.append(second_added_file) + # MODIFIED FILE + first_modified_file = ReportFile("modified.py") + first_modified_file.append(17, ReportLine.create(coverage=1)) + first_modified_file.append(18, ReportLine.create(coverage=1)) + first_modified_file.append(19, ReportLine.create(coverage=1)) + first_modified_file.append(20, ReportLine.create(coverage=0)) + first_modified_file.append(21, ReportLine.create(coverage=1)) + first_modified_file.append(22, ReportLine.create(coverage=1)) + first_modified_file.append(23, ReportLine.create(coverage=1)) + first_modified_file.append(24, ReportLine.create(coverage=1)) + first_report.append(first_modified_file) + second_modified_file = ReportFile("modified.py") + second_modified_file.append(18, ReportLine.create(coverage=1)) + second_modified_file.append(19, ReportLine.create(coverage=0)) + second_modified_file.append(20, ReportLine.create(coverage=0)) + second_modified_file.append(21, ReportLine.create(coverage=1)) + second_modified_file.append(22, ReportLine.create(coverage=0)) + second_modified_file.append(23, ReportLine.create(coverage=0)) + second_modified_file.append(24, ReportLine.create(coverage=1)) + second_report.append(second_modified_file) + # RENAMED WITHOUT CHANGES + first_renamed_without_changes_file = ReportFile("old_renamed.py") + first_renamed_without_changes_file.append(1, ReportLine.create(coverage=1)) + first_renamed_without_changes_file.append(2, ReportLine.create(coverage=1)) + first_renamed_without_changes_file.append(3, ReportLine.create(coverage=0)) + first_renamed_without_changes_file.append(4, ReportLine.create(coverage=1)) + first_renamed_without_changes_file.append(5, ReportLine.create(coverage=0)) + first_report.append(first_renamed_without_changes_file) + second_renamed_without_changes_file = ReportFile("renamed.py") + second_renamed_without_changes_file.append(1, ReportLine.create(coverage=1)) + second_renamed_without_changes_file.append(2, ReportLine.create(coverage=1)) + second_renamed_without_changes_file.append(3, ReportLine.create(coverage=0)) + second_renamed_without_changes_file.append(4, ReportLine.create(coverage=1)) + second_renamed_without_changes_file.append(5, ReportLine.create(coverage=0)) + second_report.append(second_renamed_without_changes_file) + # RENAMED WITH COVERAGE CHANGES FILE + first_renamed_file = ReportFile("old_renamed_with_changes.py") + first_renamed_file.append(2, ReportLine.create(coverage=1)) + first_renamed_file.append(3, ReportLine.create(coverage=1)) + first_renamed_file.append(5, ReportLine.create(coverage=0)) + first_renamed_file.append(8, ReportLine.create(coverage=1)) + first_renamed_file.append(13, ReportLine.create(coverage=1)) + first_report.append(first_renamed_file) + second_renamed_file = ReportFile("renamed_with_changes.py") + second_renamed_file.append(5, ReportLine.create(coverage=1)) + second_renamed_file.append(8, ReportLine.create(coverage=0)) + second_renamed_file.append(13, ReportLine.create(coverage=1)) + second_renamed_file.append(21, ReportLine.create(coverage=1)) + second_renamed_file.append(34, ReportLine.create(coverage=0)) + second_report.append(second_renamed_file) + # UNRELATED FILE + first_unrelated_file = ReportFile("unrelated.py") + first_unrelated_file.append(1, ReportLine.create(coverage=1)) + first_unrelated_file.append(2, ReportLine.create(coverage=1)) + first_unrelated_file.append(4, ReportLine.create(coverage=1)) + first_unrelated_file.append(16, ReportLine.create(coverage=0)) + first_unrelated_file.append(256, ReportLine.create(coverage=1)) + first_unrelated_file.append(65556, ReportLine.create(coverage=1)) + first_report.append(first_unrelated_file) + second_unrelated_file = ReportFile("unrelated.py") + second_unrelated_file.append(2, ReportLine.create(coverage=1)) + second_unrelated_file.append(4, ReportLine.create(coverage=0)) + second_unrelated_file.append(8, ReportLine.create(coverage=0)) + second_unrelated_file.append(16, ReportLine.create(coverage=1)) + second_unrelated_file.append(32, ReportLine.create(coverage=0)) + second_report.append(second_unrelated_file) + sample_comparison.project_coverage_base.report = ReadOnlyReport.create_from_report( + first_report + ) + sample_comparison.head.report = ReadOnlyReport.create_from_report(second_report) + return sample_comparison + + +@pytest.fixture +def mock_repo_provider(mock_repo_provider): + compare_result = { + "diff": { + "files": { + "file_1.go": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["5", "8", "5", "9"], + "lines": [ + " Overview", + " --------", + " ", + "-Main website: `Codecov `_.", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + "+website: `Codecov `_.", + " ", + " .. code-block:: shell-session", + " ", + ], + }, + { + "header": ["46", "12", "47", "19"], + "lines": [ + " ", + " You may need to configure a ``.coveragerc`` file. Learn more `here `_. Start with this `generic .coveragerc `_ for example.", + " ", + "-We highly suggest adding `source` to your ``.coveragerc`` which solves a number of issues collecting coverage.", + "+We highly suggest adding ``source`` to your ``.coveragerc``, which solves a number of issues collecting coverage.", + " ", + " .. code-block:: ini", + " ", + " [run]", + " source=your_package_name", + "+ ", + "+If there are multiple sources, you instead should add ``include`` to your ``.coveragerc``", + "+", + "+.. code-block:: ini", + "+", + "+ [run]", + "+ include=your_package_name/*", + " ", + " unittests", + " ---------", + ], + }, + { + "header": ["150", "5", "158", "4"], + "lines": [ + " * Twitter: `@codecov `_.", + " * Email: `hello@codecov.io `_.", + " ", + "-We are happy to help if you have any questions. Please contact email our Support at [support@codecov.io](mailto:support@codecov.io)", + "-", + "+We are happy to help if you have any questions. Please contact email our Support at `support@codecov.io `_.", + ], + }, + ], + "stats": {"added": 11, "removed": 4}, + } + } + }, + "commits": [ + { + "commitid": "b92edba44fdd29fcc506317cc3ddeae1a723dd08", + "message": "Update README.rst", + "timestamp": "2018-07-09T23:51:16Z", + "author": { + "id": 8398772, + "username": "jerrode", + "name": "Jerrod", + "email": "jerrod@fundersclub.com", + }, + }, + { + "commitid": "6ae5f1795a441884ed2847bb31154814ac01ef38", + "message": "Update README.rst", + "timestamp": "2018-04-26T08:35:58Z", + "author": { + "id": 11602092, + "username": "TomPed", + "name": "Thomas Pedbereznak", + "email": "tom@tomped.com", + }, + }, + ], + } + mock_repo_provider.get_compare.return_value = compare_result + return mock_repo_provider + + +class TestBaseStatusNotifier(object): + def test_can_we_set_this_status_no_pull(self, sample_comparison_without_pull): + comparison = sample_comparison_without_pull + only_pulls_notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"only_pulls": True}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + assert not only_pulls_notifier.can_we_set_this_status(comparison) + wrong_branch_notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"only_pulls": False, "branches": ["old.*"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + assert not wrong_branch_notifier.can_we_set_this_status(comparison) + right_branch_notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"only_pulls": False, "branches": ["new.*"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + assert right_branch_notifier.can_we_set_this_status(comparison) + no_settings_notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + assert no_settings_notifier.can_we_set_this_status(comparison) + exclude_branch_notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"only_pulls": False, "branches": ["!new_branch"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + assert not exclude_branch_notifier.can_we_set_this_status(comparison) + + def test_notify_after_n_builds_flags(self, sample_comparison, mocker): + comparison = sample_comparison + no_settings_notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["unit"]}, + notifier_site_settings=True, + current_yaml=UserYaml( + { + "coverage": { + "status": {"project": True, "patch": True, "changes": True} + }, + "flag_management": { + "default_rules": {"carryforward": False}, + "individual_flags": [ + { + "name": "unit", + "statuses": [{"type": "patch"}], + "after_n_builds": 3, + }, + ], + }, + } + ), + repository_service={}, + ) + mocker.patch.object(StatusNotifier, "can_we_set_this_status", return_value=True) + result = no_settings_notifier.notify(comparison) + assert not result.notification_attempted + assert result.notification_successful is None + assert result.explanation == "need_more_builds" + assert result.data_sent is None + assert result.data_received is None + + def test_notify_after_n_builds_flags2(self, sample_comparison, mocker): + comparison = sample_comparison + no_settings_notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["unit"]}, + notifier_site_settings=True, + current_yaml=UserYaml( + { + "coverage": { + "status": { + "project": True, + "patch": {"default": False, "unit": {"flags": ["unit"]}}, + "changes": True, + } + }, + "flags": { + "unit": { + "after_n_builds": 3, + } + }, + } + ), + repository_service={}, + ) + mocker.patch.object(StatusNotifier, "can_we_set_this_status", return_value=True) + result = no_settings_notifier.notify(comparison) + assert not result.notification_attempted + assert result.notification_successful is None + assert result.explanation == "need_more_builds" + assert result.data_sent is None + assert result.data_received is None + + def test_notify_cannot_set_status(self, sample_comparison, mocker): + comparison = sample_comparison + no_settings_notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + mocker.patch.object( + StatusNotifier, "can_we_set_this_status", return_value=False + ) + result = no_settings_notifier.notify(comparison) + assert not result.notification_attempted + assert result.notification_successful is None + assert result.explanation == "not_fit_criteria" + assert result.data_sent is None + assert result.data_received is None + + def test_notify_no_base( + self, sample_comparison_without_base_with_pull, mocker, mock_repo_provider + ): + comparison = sample_comparison_without_base_with_pull + no_settings_notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + no_settings_notifier.context = "fake" + mocker.patch.object(StatusNotifier, "can_we_set_this_status", return_value=True) + mocked_build_payload = mocker.patch.object( + StatusNotifier, + "build_payload", + return_value={"state": "success", "message": "somemessage"}, + ) + mocked_send_notification = mocker.patch.object( + StatusNotifier, "send_notification" + ) + mocked_send_notification.return_value = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "message": "somemessage", + "state": "success", + "title": "codecov/project/title", + }, + data_received={"id": "some_id"}, + ) + result = no_settings_notifier.notify(comparison) + assert result.notification_attempted + assert result.notification_successful + assert result.explanation is None + assert result.data_sent == { + "message": "somemessage", + "state": "success", + "title": "codecov/project/title", + } + assert result.data_received == {"id": "some_id"} + + def test_notify_uncached( + self, + sample_comparison, + mocker, + ): + comparison = sample_comparison + payload = { + "message": "something to say", + "state": "success", + "url": get_pull_url(comparison.pull), + } + + class TestNotifier(StatusNotifier): + def build_payload(self, comparison): + return payload + + notifier = TestNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + notifier.context = "fake" + + send_notification = mocker.patch.object(TestNotifier, "send_notification") + notifier.notify(comparison) + send_notification.assert_called_once + + def test_notify_multiple_shas( + self, + sample_comparison, + mocker, + ): + comparison = sample_comparison + comparison.context.gitlab_extra_shas = set(["extra_sha"]) + payload = { + "message": "something to say", + "state": "success", + "url": get_pull_url(comparison.pull), + } + + def set_status_side_effect(commit, *args, **kwargs): + return {"id": f"{commit}-status-set"} + + class TestNotifier(StatusNotifier): + def build_payload(self, comparison): + return payload + + def get_github_app_used(self) -> None: + return None + + def status_already_exists( + self, comparison: ComparisonProxy, title, state, description + ) -> bool: + return False + + fake_repo_service = MagicMock( + name="fake_repo_provider", + set_commit_status=AsyncMock(side_effect=set_status_side_effect), + ) + notifier = TestNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=fake_repo_service, + ) + notifier.context = "fake" + + result = notifier.notify(comparison) + assert result == NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "message": payload["message"], + "state": payload["state"], + "title": "codecov/fake/title", + }, + data_received={"id": f"{comparison.head.commit.commitid}-status-set"}, + github_app_used=None, + ) + assert fake_repo_service.set_commit_status.call_count == 2 + + def test_notify_cached( + self, + sample_comparison, + mocker, + ): + comparison = sample_comparison + + payload = { + "message": "something to say", + "state": "success", + "url": get_pull_url(comparison.pull), + } + + class TestNotifier(StatusNotifier): + def build_payload(self, comparison): + return payload + + notifier = TestNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + notifier.context = "fake" + + mocker.patch( + "shared.helpers.cache.NullBackend.get", + return_value=payload, + ) + + send_notification = mocker.patch.object(TestNotifier, "send_notification") + result = notifier.notify(comparison) + assert result == NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="payload_unchanged", + data_sent=None, + ) + + # payload was cached - we do not send the notification + assert not send_notification.called + + def test_send_notification(self, sample_comparison, mocker, mock_repo_provider): + comparison = sample_comparison + no_settings_notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + no_settings_notifier.context = "fake" + mocked_status_already_exists = mocker.patch.object( + StatusNotifier, "status_already_exists" + ) + mocked_status_already_exists.return_value = False + mock_repo_provider.set_commit_status.side_effect = TorngitClientError( + 403, "response", "message" + ) + payload = { + "message": "something to say", + "state": "success", + "url": "url", + "included_helper_text": "yayaya", + } + result = no_settings_notifier.send_notification(comparison, payload) + assert result.notification_attempted + assert not result.notification_successful + assert result.explanation == "no_write_permission" + expected_data_sent = { + "message": "something to say", + "state": "success", + "title": "codecov/fake/title", + "included_helper_text": "yayaya", + } + assert result.data_sent == expected_data_sent + assert result.data_received is None + + def test_notify_analytics(self, sample_comparison, mocker, mock_repo_provider): + mocker.patch("helpers.environment.is_enterprise", return_value=False) + comparison = sample_comparison + no_settings_notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + no_settings_notifier.context = "fake" + mocked_status_already_exists = mocker.patch.object( + StatusNotifier, "status_already_exists" + ) + mocked_status_already_exists.return_value = False + mock_repo_provider.set_commit_status.side_effect = TorngitClientError( + 403, "response", "message" + ) + payload = {"message": "something to say", "state": "success", "url": "url"} + no_settings_notifier.send_notification(comparison, payload) + + def test_notify_analytics_enterprise( + self, sample_comparison, mocker, mock_repo_provider + ): + mocker.patch("helpers.environment.is_enterprise", return_value=True) + comparison = sample_comparison + no_settings_notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + no_settings_notifier.context = "fake" + mocked_status_already_exists = mocker.patch.object( + StatusNotifier, "status_already_exists" + ) + mocked_status_already_exists.return_value = False + mock_repo_provider.set_commit_status.side_effect = TorngitClientError( + 403, "response", "message" + ) + payload = {"message": "something to say", "state": "success", "url": "url"} + no_settings_notifier.send_notification(comparison, payload) + + def test_determine_status_check_behavior_to_apply(self, sample_comparison): + # uses component level setting if provided + comparison = sample_comparison + notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="component_check", + notifier_yaml_settings={"flag_coverage_not_uploaded_behavior": "exclude"}, + notifier_site_settings=True, + current_yaml={ + "coverage": { + "status": { + "default_rules": { + "flag_coverage_not_uploaded_behavior": "pass" + }, + "project": { + "component_check": { + "flag_coverage_not_uploaded_behavior": "exclude" + } + }, + } + } + }, + repository_service={}, + ) + notifier.context = "fake" + assert ( + notifier.determine_status_check_behavior_to_apply( + comparison, "flag_coverage_not_uploaded_behavior" + ) + == "exclude" + ) + + # uses global setting if no component setting provided + notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="component_check", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={ + "coverage": { + "status": { + "default_rules": { + "flag_coverage_not_uploaded_behavior": "pass" + }, + "project": {"component_check": {}}, + } + } + }, + repository_service={}, + ) + notifier.context = "fake" + assert ( + notifier.determine_status_check_behavior_to_apply( + comparison, "flag_coverage_not_uploaded_behavior" + ) + == "pass" + ) + + # returns None if nothing set for flag_coverage_not_uploaded_behavior behavior field + notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="component_check", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={"coverage": {"status": {"default_rules": {}, "project": {}}}}, + repository_service={}, + ) + notifier.context = "fake" + assert ( + notifier.determine_status_check_behavior_to_apply( + comparison, "flag_coverage_not_uploaded_behavior" + ) + is None + ) + + def test_flag_coverage_was_uploaded_when_none_uploaded( + self, sample_comparison_coverage_carriedforward + ): + comparison = sample_comparison_coverage_carriedforward + notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="component_check", + notifier_yaml_settings={"flags": ["missing"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + notifier.context = "fake" + assert notifier.flag_coverage_was_uploaded(comparison) is False + + def test_flag_coverage_was_uploaded_when_all_uploaded( + self, sample_comparison_coverage_carriedforward + ): + comparison = sample_comparison_coverage_carriedforward + notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="component_check", + notifier_yaml_settings={"flags": ["unit"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + notifier.context = "fake" + assert notifier.flag_coverage_was_uploaded(comparison) is True + + def test_flag_coverage_was_uploaded_when_some_uploaded( + self, sample_comparison_coverage_carriedforward + ): + comparison = sample_comparison_coverage_carriedforward + notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="component_check", + notifier_yaml_settings={"flags": ["unit", "enterprise", "missing"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + notifier.context = "fake" + assert notifier.flag_coverage_was_uploaded(comparison) is True + + def test_flag_coverage_was_uploaded_when_no_status_flags( + self, sample_comparison_coverage_carriedforward + ): + comparison = sample_comparison_coverage_carriedforward + notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="component_check", + notifier_yaml_settings={"flags": None}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + notifier.context = "fake" + assert notifier.flag_coverage_was_uploaded(comparison) is True + + @pytest.mark.parametrize( + "fake_torngit_data, expected", + [ + (TorngitInstanceData(), None), + (TorngitInstanceData(installation=None), None), + ( + TorngitInstanceData( + installation=GithubInstallationInfo( + installation_id="owner.integration_id" + ) + ), + None, + ), + (TorngitInstanceData(installation=GithubInstallationInfo(id=12)), 12), + ], + ) + def test_get_github_app_used( + self, fake_torngit_data, expected, sample_comparison_coverage_carriedforward + ): + fake_torngit = MagicMock(data=fake_torngit_data, name="fake_torngit") + comparison = sample_comparison_coverage_carriedforward + notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="component_check", + notifier_yaml_settings={"flags": None}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=fake_torngit, + ) + notifier.context = "fake" + assert notifier.get_github_app_used() == expected + + def test_get_github_app_used_no_repository_service( + self, sample_comparison_coverage_carriedforward + ): + comparison = sample_comparison_coverage_carriedforward + notifier = StatusNotifier( + repository=comparison.head.commit.repository, + title="component_check", + notifier_yaml_settings={"flags": None}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=None, + ) + notifier.context = "fake" + assert notifier.get_github_app_used() is None + + +class TestProjectStatusNotifier(object): + def test_build_payload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = sample_comparison.project_coverage_base.commit + expected_result = { + "message": f"60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_payload_passing_empty_upload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + decoration_type=Decoration.passing_empty_upload, + ) + expected_result = { + "state": "success", + "message": "Non-testable files changed.", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_payload_failing_empty_upload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + decoration_type=Decoration.failing_empty_upload, + ) + expected_result = { + "state": "failure", + "message": "Testable files changed", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_upgrade_payload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + decoration_type=Decoration.upgrade, + ) + base_commit = sample_comparison.project_coverage_base.commit + expected_result = { + "message": "Please activate this user to display a detailed status check", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_payload_not_auto( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "target": "57%", + "removed_code_behavior": "removals_only", + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "60.00% (target 57.00%)", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_payload_not_auto_not_string( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"target": 57.0}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "60.00% (target 57.00%)", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_payload_no_base_report( + self, + sample_comparison_without_base_report, + mock_repo_provider, + mock_configuration, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_without_base_report + notifier = ProjectStatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "No report found to compare against", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(comparison) + assert expected_result == result + + def test_notify_status_doesnt_exist( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_repo_provider.set_commit_status.return_value = {"id": "some_id"} + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = sample_comparison.project_coverage_base.commit + expected_result = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "message": f"60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "state": "success", + "title": "codecov/project/title", + }, + data_received={"id": "some_id"}, + ) + result = notifier.notify(sample_comparison) + assert expected_result == result + + def test_notify_client_side_exception( + self, sample_comparison, mocker, mock_configuration + ): + mocker.patch.object( + ProjectStatusNotifier, + "send_notification", + side_effect=TorngitRepoNotFoundError("response", "message"), + ) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + base_commit = sample_comparison.project_coverage_base.commit + repo = sample_comparison.head.commit.repository + expected_result = NotificationResult( + notification_attempted=True, + notification_successful=False, + explanation="client_side_error_provider", + data_sent={ + "message": f"60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "state": "success", + "url": f"test.example.br/gh/{repo.slug}/pull/{sample_comparison.pull.pullid}", + "included_helper_text": {}, + }, + data_received=None, + ) + result = notifier.notify(sample_comparison) + assert expected_result == result + + def test_notify_server_side_exception( + self, sample_comparison, mocker, mock_configuration + ): + mocker.patch.object( + ProjectStatusNotifier, + "send_notification", + side_effect=TorngitServerUnreachableError(), + ) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + base_commit = sample_comparison.project_coverage_base.commit + repo = sample_comparison.head.commit.repository + expected_result = NotificationResult( + notification_attempted=True, + notification_successful=False, + explanation="server_side_error_provider", + data_sent={ + "message": f"60.00% (+10.00%) compared to {base_commit.commitid[:7]}", + "state": "success", + "url": f"test.example.br/gh/{repo.slug}/pull/{sample_comparison.pull.pullid}", + "included_helper_text": {}, + }, + data_received=None, + ) + result = notifier.notify(sample_comparison) + assert expected_result.data_sent == result.data_sent + assert expected_result == result + + def test_notify_pass_behavior_when_coverage_not_uploaded( + self, sample_comparison_coverage_carriedforward, mock_repo_provider + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_repo_provider.set_commit_status.return_value = {"id": "some_id"} + notifier = ProjectStatusNotifier( + repository=sample_comparison_coverage_carriedforward.head.commit.repository, + title="title", + notifier_yaml_settings={ + "flag_coverage_not_uploaded_behavior": "pass", + "flags": ["integration", "missing"], + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = ( + sample_comparison_coverage_carriedforward.project_coverage_base.commit + ) + expected_result = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "message": f"25.00% (+0.00%) compared to {base_commit.commitid[:7]} [Auto passed due to carriedforward or missing coverage]", + "state": "success", + "title": "codecov/project/title", + }, + data_received={"id": "some_id"}, + ) + result = notifier.notify(sample_comparison_coverage_carriedforward) + assert expected_result == result + + def test_notify_pass_behavior_when_coverage_uploaded( + self, sample_comparison_coverage_carriedforward, mock_repo_provider + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_repo_provider.set_commit_status.return_value = {"id": "some_id"} + notifier = ProjectStatusNotifier( + repository=sample_comparison_coverage_carriedforward.head.commit.repository, + title="title", + notifier_yaml_settings={ + "flag_coverage_not_uploaded_behavior": "pass", + "flags": ["unit"], + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = ( + sample_comparison_coverage_carriedforward.project_coverage_base.commit + ) + expected_result = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "message": f"25.00% (+0.00%) compared to {base_commit.commitid[:7]}", # no message indicating auto-pass + "state": "success", + "title": "codecov/project/title", + }, + data_received={"id": "some_id"}, + ) + result = notifier.notify(sample_comparison_coverage_carriedforward) + assert expected_result == result + + def test_notify_include_behavior_when_coverage_not_uploaded( + self, sample_comparison_coverage_carriedforward, mock_repo_provider + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_repo_provider.set_commit_status.return_value = {"id": "some_id"} + notifier = ProjectStatusNotifier( + repository=sample_comparison_coverage_carriedforward.head.commit.repository, + title="title", + notifier_yaml_settings={ + "flag_coverage_not_uploaded_behavior": "include", + "flags": ["integration", "enterprise"], + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = ( + sample_comparison_coverage_carriedforward.project_coverage_base.commit + ) + expected_result = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "message": f"36.17% (+0.00%) compared to {base_commit.commitid[:7]}", + "state": "success", + "title": "codecov/project/title", + }, + data_received={"id": "some_id"}, + ) + result = notifier.notify(sample_comparison_coverage_carriedforward) + assert expected_result == result + + def test_notify_exclude_behavior_when_coverage_not_uploaded( + self, sample_comparison_coverage_carriedforward, mock_repo_provider + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + notifier = ProjectStatusNotifier( + repository=sample_comparison_coverage_carriedforward.head.commit.repository, + title="title", + notifier_yaml_settings={ + "flag_coverage_not_uploaded_behavior": "exclude", + "flags": ["missing"], + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="exclude_flag_coverage_not_uploaded_checks", + data_sent=None, + data_received=None, + ) + result = notifier.notify(sample_comparison_coverage_carriedforward) + assert expected_result == result + + def test_notify_exclude_behavior_when_coverage_uploaded( + self, sample_comparison_coverage_carriedforward, mock_repo_provider + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_repo_provider.set_commit_status.return_value = {"id": "some_id"} + notifier = ProjectStatusNotifier( + repository=sample_comparison_coverage_carriedforward.head.commit.repository, + title="title", + notifier_yaml_settings={ + "flag_coverage_not_uploaded_behavior": "exclude", + "flags": ["unit"], + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = ( + sample_comparison_coverage_carriedforward.project_coverage_base.commit + ) + expected_result = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "message": f"25.00% (+0.00%) compared to {base_commit.commitid[:7]}", + "state": "success", + "title": "codecov/project/title", + }, + data_received={"id": "some_id"}, + ) + result = notifier.notify(sample_comparison_coverage_carriedforward) + assert expected_result == result + + def test_notify_exclude_behavior_when_some_coverage_uploaded( + self, sample_comparison_coverage_carriedforward, mock_repo_provider + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_repo_provider.set_commit_status.return_value = {"id": "some_id"} + notifier = ProjectStatusNotifier( + repository=sample_comparison_coverage_carriedforward.head.commit.repository, + title="title", + notifier_yaml_settings={ + "flag_coverage_not_uploaded_behavior": "exclude", + "flags": [ + "unit", + "missing", + "integration", + ], # only "unit" was uploaded, but this should still notify + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = ( + sample_comparison_coverage_carriedforward.project_coverage_base.commit + ) + expected_result = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "message": f"36.17% (+0.00%) compared to {base_commit.commitid[:7]}", + "state": "success", + "title": "codecov/project/title", + }, + data_received={"id": "some_id"}, + ) + result = notifier.notify(sample_comparison_coverage_carriedforward) + assert expected_result == result + + def test_notify_exclude_behavior_no_flags( + self, sample_comparison_coverage_carriedforward, mock_repo_provider + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_repo_provider.set_commit_status.return_value = {"id": "some_id"} + notifier = ProjectStatusNotifier( + repository=sample_comparison_coverage_carriedforward.head.commit.repository, + title="title", + notifier_yaml_settings={ + "flag_coverage_not_uploaded_behavior": "exclude", + "flags": None, + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = ( + sample_comparison_coverage_carriedforward.project_coverage_base.commit + ) + # should send the check as normal if there are no flags + expected_result = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "message": f"65.38% (+0.00%) compared to {base_commit.commitid[:7]}", + "state": "success", + "title": "codecov/project/title", + }, + data_received={"id": "some_id"}, + ) + result = notifier.notify(sample_comparison_coverage_carriedforward) + assert expected_result == result + + def test_notify_path_filter( + self, sample_comparison, mock_repo_provider, mock_configuration, mocker + ): + mocked_send_notification = mocker.patch.object( + ProjectStatusNotifier, "send_notification" + ) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"paths": ["file_1.go"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = sample_comparison.project_coverage_base.commit + expected_result = { + "message": f"62.50% (+12.50%) compared to {base_commit.commitid[:7]}", + "state": "success", + "url": f"test.example.br/gh/{sample_comparison.head.commit.repository.slug}/pull/{sample_comparison.pull.pullid}", + "included_helper_text": {}, + } + result = notifier.notify(sample_comparison) + assert result == mocked_send_notification.return_value + mocked_send_notification.assert_called_with(sample_comparison, expected_result) + + def test_notify_path_and_flags_filter_nothing_on_base( + self, sample_comparison, mock_repo_provider, mock_configuration, mocker + ): + mocked_send_notification = mocker.patch.object( + ProjectStatusNotifier, "send_notification" + ) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"paths": ["file_1.go"], "flags": ["unit"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + # base report does not have unit flag, so there is no coverage there + "message": "No coverage information found on base report", + "state": "success", + "url": f"test.example.br/gh/{sample_comparison.head.commit.repository.slug}/pull/{sample_comparison.pull.pullid}", + "included_helper_text": {}, + } + result = notifier.notify(sample_comparison) + assert result == mocked_send_notification.return_value + mocked_send_notification.assert_called_with(sample_comparison, expected_result) + + def test_notify_path_and_flags_filter_something_on_base( + self, + sample_comparison_matching_flags, + mock_repo_provider, + mock_configuration, + mocker, + ): + mocked_send_notification = mocker.patch.object( + ProjectStatusNotifier, "send_notification" + ) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison_matching_flags.head.commit.repository, + title="title", + notifier_yaml_settings={"paths": ["file_1.go"], "flags": ["unit"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = sample_comparison_matching_flags.project_coverage_base.commit + expected_result = { + # base report does not have unit flag, so there is no coverage there + "message": f"100.00% (+0.00%) compared to {base_commit.commitid[:7]}", + "state": "success", + "url": f"test.example.br/gh/{sample_comparison_matching_flags.head.commit.repository.slug}/pull/{sample_comparison_matching_flags.pull.pullid}", + "included_helper_text": {}, + } + result = notifier.notify(sample_comparison_matching_flags) + assert result == mocked_send_notification.return_value + mocked_send_notification.assert_called_with( + sample_comparison_matching_flags, expected_result + ) + + def test_notify_pass_via_removals_only_behavior( + self, mock_configuration, sample_comparison, mocker + ): + mock_get_impacted_files = mocker.patch.object( + ComparisonProxy, + "get_impacted_files", + return_value={ + "files": [ + { + "base_name": "tests/file1.py", + "head_name": "tests/file1.py", + # Not complete, but we only care about these fields + "removed_diff_coverage": [], + "added_diff_coverage": [], + "unexpected_line_changes": [], + }, + { + "base_name": "tests/file2.go", + "head_name": "tests/file2.go", + "removed_diff_coverage": [[1, "h"], [3, None]], + "added_diff_coverage": [], + "unexpected_line_changes": [], + }, + ], + }, + ) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "target": "80%", + "removed_code_behavior": "removals_only", + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + expected_result = { + "message": "60.00% (target 80.00%), passed because this change only removed code", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert result == expected_result + mock_get_impacted_files.assert_called() + + @pytest.mark.parametrize( + "base_totals, head_totals, impacted_files, expected", + [ + pytest.param( + ReportTotals(hits=1980, misses=120, partials=0), + ReportTotals( + hits=1974, + misses=120, + partials=0, + coverage=round((1974 / (1974 + 120)) * 100, 5), + ), + [ + { + "removed_diff_coverage": [ + (1, "h"), + (2, "h"), + (3, "h"), + (4, "h"), + (5, "h"), + (6, "h"), + (7, "h"), + (8, "h"), + (9, "h"), + (10, "h"), + (11, "h"), + (12, "h"), + (13, "h"), + (14, "h"), + (15, "h"), + ] + } + ], + ( + ( + "success", + ", passed because coverage increased by 0.02% when compared to adjusted base (94.24%)", + ), + {}, + ), + id="many_removed_hits_makes_head_more_covered_than_base", + ), + pytest.param( + ReportTotals(hits=1980, misses=120, partials=0), + ReportTotals( + hits=1974, + misses=120, + partials=0, + coverage=round((1974 / (1974 + 120)) * 100, 5), + ), + [ + { + "removed_diff_coverage": [ + (1, "h"), + (2, "h"), + (3, "h"), + (4, "h"), + (5, "h"), + (6, "h"), + ] + } + ], + ( + ( + "success", + ", passed because coverage increased by 0% when compared to adjusted base (94.27%)", + ), + {}, + ), + id="many_removed_hits_makes_head_same_as_base", + ), + pytest.param( + ReportTotals(hits=1980, misses=120, partials=0), + ReportTotals( + hits=1974, + misses=120, + partials=0, + coverage=round((1974 / (1974 + 120)) * 100, 5), + ), + [ + { + "removed_diff_coverage": [ + (1, "h"), + (2, "h"), + (3, "h"), + ] + } + ], + ( + None, + { + HelperTextKey.RCB_ADJUST_BASE.value: HELPER_TEXT_MAP[ + HelperTextKey.RCB_ADJUST_BASE + ].value.format( + notification_type="status", + coverage=94.27, + adjusted_base_cov=94.28, + ), + }, + ), + id="not_enough_hits_removed_for_status_to_pass", + ), + pytest.param( + ReportTotals(hits=0, misses=0, partials=0), + ReportTotals(hits=0, misses=0, partials=0, coverage="100"), + [], + (None, {}), + id="zero_coverage", + ), + ], + ) + def test_adjust_base_behavior( + self, mocker, base_totals, head_totals, impacted_files, expected + ): + comparison = mocker.MagicMock( + name="fake-comparison", + get_impacted_files=MagicMock(return_value={"files": impacted_files}), + project_coverage_base=FullCommit( + commit=None, report=Report(totals=base_totals) + ), + head=FullCommit(commit=CommitFactory(), report=Report(totals=head_totals)), + ) + settings = {"target": "auto", "threshold": "0"} + status_mixin = ProjectStatusNotifier( + repository="repo", + title="fake-notifier", + notifier_yaml_settings=settings, + notifier_site_settings={}, + current_yaml=settings, + repository_service={}, + ) + result = status_mixin._apply_adjust_base_behavior( + comparison, notification_type="status" + ) + assert result == expected + + def test_notify_pass_adjust_base_behavior( + self, mock_configuration, sample_comparison_negative_change, mocker + ): + sample_comparison = sample_comparison_negative_change + mock_get_impacted_files = mocker.patch.object( + ComparisonProxy, + "get_impacted_files", + return_value={ + "files": [ + { + "base_name": "tests/file1.py", + "head_name": "tests/file1.py", + # Not complete, but we only care about these fields + "removed_diff_coverage": [[1, "h"], [3, "h"], [4, "m"]], + "added_diff_coverage": [], + "unexpected_line_changes": [], + }, + { + "base_name": "tests/file2.go", + "head_name": "tests/file2.go", + "removed_diff_coverage": [[1, "h"]], + "added_diff_coverage": [], + "unexpected_line_changes": [], + }, + ], + }, + ) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"removed_code_behavior": "adjust_base"}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + expected_result = { + "message": f"50.00% (-10.00%) compared to {sample_comparison.project_coverage_base.commit.commitid[:7]}, passed because coverage increased by 0% when compared to adjusted base (50.00%)", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert result == expected_result + mock_get_impacted_files.assert_called() + + def test_notify_pass_adjust_base_behavior_with_threshold( + self, mock_configuration, sample_comparison_negative_change, mocker + ): + sample_comparison = sample_comparison_negative_change + mock_get_impacted_files = mocker.patch.object( + ComparisonProxy, + "get_impacted_files", + return_value={ + "files": [ + { + "base_name": "tests/file1.py", + "head_name": "tests/file1.py", + # Not complete, but we only care about these fields + "removed_diff_coverage": [[1, "h"], [3, "h"], [4, "m"]], + "added_diff_coverage": [], + "unexpected_line_changes": [], + }, + { + "base_name": "tests/file2.go", + "head_name": "tests/file2.go", + "removed_diff_coverage": [[1, "h"]], + "added_diff_coverage": [], + "unexpected_line_changes": [], + }, + ], + }, + ) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "removed_code_behavior": "adjust_base", + "threshold": "5", + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + expected_result = { + "message": f"50.00% (-10.00%) compared to {sample_comparison.project_coverage_base.commit.commitid[:7]}, passed because coverage increased by 5.00% when compared to adjusted base (45.00%)", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert result == expected_result + mock_get_impacted_files.assert_called() + + def test_notify_removed_code_behavior_fail( + self, mock_configuration, sample_comparison, mocker + ): + mock_get_impacted_files = mocker.patch.object( + ComparisonProxy, + "get_impacted_files", + return_value={ + "files": [ + { + "base_name": "tests/file1.py", + "head_name": "tests/file1.py", + # Not complete, but we only care about these fields + "removed_diff_coverage": [[1, "h"]], + "added_diff_coverage": [[2, "h"], [3, "h"]], + "unexpected_line_changes": [], + }, + { + "base_name": "tests/file2.go", + "head_name": "tests/file2.go", + "removed_diff_coverage": [[1, "h"], [3, None]], + "added_diff_coverage": [], + "unexpected_line_changes": [], + }, + ], + }, + repository_service=None, + ) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "target": "80%", + "removed_code_behavior": "removals_only", + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=None, + ) + expected_result = { + "message": "60.00% (target 80.00%)", + "state": "failure", + "included_helper_text": { + HelperTextKey.CUSTOM_TARGET_PROJECT: HelperTextTemplate.CUSTOM_TARGET.format( + context="project", + notification_type="status", + point_of_comparison="head", + coverage="60.00", + target="80.00", + ) + }, + } + result = notifier.build_payload(sample_comparison) + assert result == expected_result + mock_get_impacted_files.assert_called() + + def test_notify_adjust_base_behavior_fail( + self, mock_configuration, sample_comparison_negative_change, mocker + ): + sample_comparison = sample_comparison_negative_change + mock_get_impacted_files = mocker.patch.object( + ComparisonProxy, + "get_impacted_files", + return_value={ + "files": [ + { + "base_name": "tests/file1.py", + "head_name": "tests/file1.py", + # Not complete, but we only care about these fields + "removed_diff_coverage": [[1, "h"], [3, "m"], [4, "m"]], + "added_diff_coverage": [], + "unexpected_line_changes": [], + }, + { + "base_name": "tests/file2.go", + "head_name": "tests/file2.go", + "removed_diff_coverage": [], + "added_diff_coverage": [], + "unexpected_line_changes": [], + }, + ], + }, + ) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"removed_code_behavior": "adjust_base"}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + # included helper text for this user because they have adjust_base in their yaml + expected_result = { + "message": f"50.00% (-10.00%) compared to {sample_comparison.project_coverage_base.commit.commitid[:7]}", + "state": "failure", + "included_helper_text": { + HelperTextKey.RCB_ADJUST_BASE.value: HELPER_TEXT_MAP[ + HelperTextKey.RCB_ADJUST_BASE + ].value.format( + notification_type="status", + coverage="50.00", + adjusted_base_cov=71.43, + ) + }, + } + result = notifier.build_payload(sample_comparison) + assert result == expected_result + mock_get_impacted_files.assert_called() + + def test_notify_rcb_default( + self, mock_configuration, sample_comparison_negative_change, mocker + ): + sample_comparison = sample_comparison_negative_change + mock_get_impacted_files = mocker.patch.object( + ComparisonProxy, + "get_impacted_files", + return_value={ + "files": [ + { + "base_name": "tests/file1.py", + "head_name": "tests/file1.py", + # Not complete, but we only care about these fields + "removed_diff_coverage": [[1, "h"], [3, "m"], [4, "m"]], + "added_diff_coverage": [], + "unexpected_line_changes": [], + }, + { + "base_name": "tests/file2.go", + "head_name": "tests/file2.go", + "removed_diff_coverage": [], + "added_diff_coverage": [], + "unexpected_line_changes": [], + }, + ], + }, + ) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + # NO helper text for this user because they have NOT specified adjust_base in their yaml + expected_result = { + "message": f"50.00% (-10.00%) compared to {sample_comparison.project_coverage_base.commit.commitid[:7]}", + "state": "failure", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert result == expected_result + mock_get_impacted_files.assert_called() + + def test_notify_adjust_base_behavior_skips_if_target_coverage_defined( + self, mock_configuration, sample_comparison_negative_change, mocker + ): + sample_comparison = sample_comparison_negative_change + mock_get_impacted_files = mocker.patch.object( + ComparisonProxy, "get_impacted_files" + ) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "removed_code_behavior": "adjust_base", + "target": "80%", + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + expected_result = { + "message": "50.00% (target 80.00%)", + "state": "failure", + "included_helper_text": { + HelperTextKey.CUSTOM_TARGET_PROJECT: HelperTextTemplate.CUSTOM_TARGET.format( + context="project", + notification_type="status", + point_of_comparison="head", + coverage="50.00", + target="80.00", + ) + }, + } + result = notifier.build_payload(sample_comparison) + assert result == expected_result + mock_get_impacted_files.assert_not_called() + + def test_notify_removed_code_behavior_unknown( + self, mock_configuration, sample_comparison + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "target": "80%", + "removed_code_behavior": "not_valid", + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + expected_result = { + "message": "60.00% (target 80.00%)", + "state": "failure", + "included_helper_text": { + HelperTextKey.CUSTOM_TARGET_PROJECT: HelperTextTemplate.CUSTOM_TARGET.format( + context="project", + notification_type="status", + point_of_comparison="head", + coverage="60.00", + target="80.00", + ) + }, + } + result = notifier.build_payload(sample_comparison) + assert result == expected_result + + def test_notify_fully_covered_patch_behavior_fail( + self, + comparison_with_multiple_changes, + mock_repo_provider, + mock_configuration, + multiple_diff_changes, + mocker, + ): + json_diff = multiple_diff_changes + mock_repo_provider.get_compare.return_value = {"diff": json_diff} + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + mock_get_impacted_files = mocker.patch.object( + ComparisonProxy, + "get_impacted_files", + return_value={ + "files": [ + { + "base_name": "tests/file1.py", + "head_name": "tests/file1.py", + # Not complete, but we only care about these fields + "removed_diff_coverage": [[1, "h"]], + "added_diff_coverage": [[2, "h"], [3, "h"]], + "unexpected_line_changes": [], + }, + { + "base_name": "tests/file2.go", + "head_name": "tests/file2.go", + "removed_diff_coverage": [[1, "h"], [3, None]], + "added_diff_coverage": [], + "unexpected_line_changes": [], + }, + ], + }, + ) + notifier = ProjectStatusNotifier( + repository=comparison_with_multiple_changes.head.commit.repository, + title="title", + notifier_yaml_settings={ + "target": "70%", + "removed_code_behavior": "fully_covered_patch", + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "50.00% (target 70.00%)", + "state": "failure", + "included_helper_text": { + HelperTextKey.CUSTOM_TARGET_PROJECT: HelperTextTemplate.CUSTOM_TARGET.format( + context="project", + notification_type="status", + point_of_comparison="head", + coverage="50.00", + target="70.00", + ) + }, + } + result = notifier.build_payload(comparison_with_multiple_changes) + assert result == expected_result + mock_get_impacted_files.assert_called() + + def test_notify_fully_covered_patch_behavior_fail_indirect_changes( + self, + comparison_with_multiple_changes, + mock_repo_provider, + mock_configuration, + multiple_diff_changes, + mocker, + ): + json_diff = multiple_diff_changes + mock_repo_provider.get_compare.return_value = {"diff": json_diff} + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + mock_get_impacted_files = mocker.patch.object( + ComparisonProxy, + "get_impacted_files", + return_value={ + "files": [ + { + "base_name": "tests/file1.py", + "head_name": "tests/file1.py", + # Not complete, but we only care about these fields + "removed_diff_coverage": [[1, "h"]], + "added_diff_coverage": [[2, "h"], [3, "h"]], + "unexpected_line_changes": "any value in this field", + }, + { + "base_name": "tests/file2.go", + "head_name": "tests/file2.go", + "removed_diff_coverage": [[1, "h"], [3, None]], + "added_diff_coverage": [], + "unexpected_line_changes": [], + }, + ], + }, + ) + notifier = ProjectStatusNotifier( + repository=comparison_with_multiple_changes.head.commit.repository, + title="title", + notifier_yaml_settings={ + "target": "70%", + "removed_code_behavior": "fully_covered_patch", + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "50.00% (target 70.00%)", + "state": "failure", + "included_helper_text": { + HelperTextKey.CUSTOM_TARGET_PROJECT.value: HelperTextTemplate.CUSTOM_TARGET.value.format( + context="project", + notification_type="status", + point_of_comparison="head", + coverage="50.00", + target="70.00", + ), + HelperTextKey.RCB_INDIRECT_CHANGES.value: HELPER_TEXT_MAP[ + HelperTextKey.RCB_INDIRECT_CHANGES + ].value.format( + context="project", + notification_type="status", + ), + }, + } + result = notifier.build_payload(comparison_with_multiple_changes) + assert result == expected_result + mock_get_impacted_files.assert_called() + + def test_notify_fully_covered_patch_behavior_success( + self, + comparison_100_percent_patch, + mock_repo_provider, + mock_configuration, + multiple_diff_changes, + mocker, + ): + json_diff = multiple_diff_changes + mock_repo_provider.get_compare.return_value = {"diff": json_diff} + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + mock_get_impacted_files = mocker.patch.object( + ComparisonProxy, + "get_impacted_files", + return_value={ + "files": [ + { + "base_name": "tests/file1.py", + "head_name": "tests/file1.py", + # Not complete, but we only care about these fields + "removed_diff_coverage": [[1, "h"]], + "added_diff_coverage": [[2, "h"], [3, "h"]], + "unexpected_line_changes": [], + }, + { + "base_name": "tests/file2.go", + "head_name": "tests/file2.go", + "removed_diff_coverage": [[1, "h"], [3, None]], + "added_diff_coverage": [], + "unexpected_line_changes": [], + }, + ], + }, + ) + notifier = ProjectStatusNotifier( + repository=comparison_100_percent_patch.head.commit.repository, + title="title", + notifier_yaml_settings={ + "target": "70%", + "removed_code_behavior": "fully_covered_patch", + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "28.57% (target 70.00%), passed because patch was fully covered by tests, and no indirect coverage changes", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(comparison_100_percent_patch) + assert result == expected_result + mock_get_impacted_files.assert_called() + + def test_notify_fully_covered_patch_behavior_no_coverage_change( + self, mock_configuration, sample_comparison, mocker + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + mock_get_impacted_files = mocker.patch.object( + ComparisonProxy, + "get_impacted_files", + return_value={ + "files": [ + { + "base_name": "tests/file1.py", + "head_name": "tests/file1.py", + # Not complete, but we only care about these fields + "removed_diff_coverage": [[1, "h"]], + "added_diff_coverage": [[2, "h"], [3, "h"]], + "unexpected_line_changes": [], + }, + { + "base_name": "tests/file2.go", + "head_name": "tests/file2.go", + "removed_diff_coverage": [[1, "h"], [3, None]], + "added_diff_coverage": [], + "unexpected_line_changes": [], + }, + ], + }, + ) + mocker.patch.object( + sample_comparison, + "get_diff", + return_value={ + "files": { + "file_1.go": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["105", "8", "105", "9"], + "lines": [ + " Overview", + " --------", + " ", + "-Main website: `Codecov `_.", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + "+website: `Codecov `_.", + " ", + " .. code-block:: shell-session", + " ", + ], + }, + { + "header": ["1046", "12", "1047", "19"], + "lines": [ + " ", + " You may need to configure a ``.coveragerc`` file. Learn more", + " ", + "-We highly suggest adding `source` to your ``.coveragerc``", + "+We highly suggest adding ``source`` to your ``.coveragerc`", + " ", + " .. code-block:: ini", + " ", + " [run]", + " source=your_package_name", + "+ ", + "+If there are multiple sources, you instead should add ``include", + "+", + "+.. code-block:: ini", + "+", + "+ [run]", + "+ include=your_package_name/*", + " ", + " unittests", + " ---------", + ], + }, + { + "header": ["10150", "5", "10158", "4"], + "lines": [ + " * Twitter: `@codecov `_.", + " * Email: `hello@codecov.io `_.", + " ", + "-We are happy to help if you have any questions. ", + "-", + "+We are happy to help if you have any questions. .", + ], + }, + ], + "stats": {"added": 11, "removed": 4}, + }, + "file_2.py": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["10", "8", "10", "9"], + "lines": [ + " Overview", + " --------", + " ", + "-Main website: `Codecov `_.", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + "+website: `Codecov `_.", + " ", + " .. code-block:: shell-session", + " ", + ], + }, + { + "header": ["50", "12", "51", "19"], + "lines": [ + " ", + " You may need to configure a ``.coveragerc`` file. Learn more `here `_. Start with this `generic .coveragerc `_ for example.", + " ", + "-We highly suggest adding `source` to your ``.coveragerc`` which solves a number of issues collecting coverage.", + "+We highly suggest adding ``source`` to your ``.coveragerc``, which solves a number of issues collecting coverage.", + " ", + " .. code-block:: ini", + " ", + " [run]", + " source=your_package_name", + "+ ", + "+If there are multiple sources, you instead should add ``include`` to your ``.coveragerc``", + "+", + "+.. code-block:: ini", + "+", + "+ [run]", + "+ include=your_package_name/*", + " ", + " unittests", + " ---------", + ], + }, + ], + "stats": {"added": 11, "removed": 4}, + }, + } + }, + ) + notifier = ProjectStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "target": "70%", + "removed_code_behavior": "fully_covered_patch", + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service={}, + ) + expected_result = { + "message": "60.00% (target 70.00%), passed because coverage was not affected by patch", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert result == expected_result + mock_get_impacted_files.assert_called() + + +class TestPatchStatusNotifier(object): + def test_build_payload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = PatchStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "66.67% of diff hit (target 50.00%)", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_upgrade_payload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = PatchStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + decoration_type=Decoration.upgrade, + ) + expected_result = { + "message": "Please activate this user to display a detailed status check", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_payload_target_coverage_failure( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = PatchStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"target": "70%"}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "66.67% of diff hit (target 70.00%)", + "state": "failure", + "included_helper_text": { + HelperTextKey.CUSTOM_TARGET_PATCH: HelperTextTemplate.CUSTOM_TARGET.format( + context="patch", + notification_type="status", + point_of_comparison="patch", + coverage=66.67, + target="70.00", + ) + }, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_payload_not_auto_not_string( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = PatchStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"target": 57.0}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "66.67% of diff hit (target 57.00%)", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_payload_target_coverage_failure_within_threshold( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + third_file = ReportFile("file_3.c") + third_file.append(100, ReportLine.create(coverage=1, sessions=[[0, 1]])) + third_file.append(101, ReportLine.create(coverage=1, sessions=[[0, 1]])) + third_file.append(102, ReportLine.create(coverage=1, sessions=[[0, 1]])) + third_file.append(103, ReportLine.create(coverage=1, sessions=[[0, 1]])) + report = sample_comparison.project_coverage_base.report.inner_report + report.append(third_file) + sample_comparison.project_coverage_base.report = ( + ReadOnlyReport.create_from_report(report) + ) + notifier = PatchStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"threshold": "5"}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "66.67% of diff hit (within 5.00% threshold of 70.00%)", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_get_patch_status_bad_threshold( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + third_file = ReportFile("file_3.c") + third_file.append(100, ReportLine.create(coverage=1, sessions=[[0, 1]])) + third_file.append(101, ReportLine.create(coverage=1, sessions=[[0, 1]])) + third_file.append(102, ReportLine.create(coverage=1, sessions=[[0, 1]])) + third_file.append(103, ReportLine.create(coverage=1, sessions=[[0, 1]])) + report = sample_comparison.project_coverage_base.report.inner_report + report.append(third_file) + sample_comparison.project_coverage_base.report = ( + ReadOnlyReport.create_from_report(report) + ) + notifier = PatchStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"threshold": None}, # invalid value for threshold + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "66.67% of diff hit (target 70.00%)", + "state": "failure", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_get_patch_status_bad_threshold_fixed( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + third_file = ReportFile("file_3.c") + third_file.append(100, ReportLine.create(coverage=1, sessions=[[0, 1]])) + third_file.append(101, ReportLine.create(coverage=1, sessions=[[0, 1]])) + third_file.append(102, ReportLine.create(coverage=1, sessions=[[0, 1]])) + third_file.append(103, ReportLine.create(coverage=1, sessions=[[0, 1]])) + report = sample_comparison.project_coverage_base.report.inner_report + report.append(third_file) + sample_comparison.project_coverage_base.report = ( + ReadOnlyReport.create_from_report(report) + ) + notifier = PatchStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "threshold": "5%" + }, # invalid value for threshold, caught and fixed + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "66.67% of diff hit (within 5.00% threshold of 70.00%)", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_payload_no_diff( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_repo_provider.get_compare.return_value = { + "diff": { + "files": { + "file_1.go": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["15", "8", "15", "9"], + "lines": [ + " Overview", + " --------", + " ", + "-Main website: `Codecov `_.", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + "+website: `Codecov `_.", + " ", + " .. code-block:: shell-session", + " ", + ], + } + ], + "stats": {"added": 11, "removed": 4}, + } + } + } + } + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = PatchStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + base_commit = sample_comparison.project_coverage_base.commit + head_commit = sample_comparison.head.commit + expected_result = { + "message": f"Coverage not affected when comparing {base_commit.commitid[:7]}...{head_commit.commitid[:7]}", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_payload_no_diff_no_base_report( + self, + sample_comparison_without_base_with_pull, + mock_repo_provider, + mock_configuration, + ): + mock_repo_provider.get_compare.return_value = { + "diff": { + "files": { + "file_1.go": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["15", "8", "15", "9"], + "lines": [ + " Overview", + " --------", + " ", + "-Main website: `Codecov `_.", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + "+website: `Codecov `_.", + " ", + " .. code-block:: shell-session", + " ", + ], + } + ], + "stats": {"added": 11, "removed": 4}, + } + } + } + } + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_without_base_with_pull + notifier = PatchStatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "Coverage not affected", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(comparison) + assert expected_result == result + + def test_build_payload_without_base_report( + self, + sample_comparison_without_base_report, + mock_repo_provider, + mock_configuration, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_without_base_report + notifier = PatchStatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "No report found to compare against", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(comparison) + assert expected_result == result + + def test_build_payload_with_multiple_changes( + self, + comparison_with_multiple_changes, + mock_repo_provider, + mock_configuration, + multiple_diff_changes, + ): + json_diff = multiple_diff_changes + mock_repo_provider.get_compare.return_value = {"diff": json_diff} + + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = PatchStatusNotifier( + repository=comparison_with_multiple_changes.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "50.00% of diff hit (target 76.92%)", + "state": "failure", + "included_helper_text": {}, # not a custom target, no helper text + } + result = notifier.build_payload(comparison_with_multiple_changes) + assert expected_result == result + + +class TestChangesStatusNotifier(object): + def test_build_payload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ChangesStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "No indirect coverage changes found", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_upgrade_payload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ChangesStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + decoration_type=Decoration.upgrade, + ) + expected_result = { + "message": "Please activate this user to display a detailed status check", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result + + def test_build_payload_with_multiple_changes( + self, + comparison_with_multiple_changes, + mock_repo_provider, + mock_configuration, + multiple_diff_changes, + ): + json_diff = multiple_diff_changes + mock_repo_provider.get_compare.return_value = {"diff": json_diff} + + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ChangesStatusNotifier( + repository=comparison_with_multiple_changes.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "3 files have indirect coverage changes not visible in diff", + "state": "failure", + "included_helper_text": { + "indirect_changes_helper_text": ( + "Your changes status has failed because you have indirect coverage changes. " + "Learn more about [Unexpected Coverage Changes](https://docs.codecov.com/docs/unexpected-coverage-changes) " + "and [reasons for indirect coverage changes](https://docs.codecov.com/docs/unexpected-coverage-changes#reasons-for-indirect-changes)." + ) + }, + } + result = notifier.build_payload(comparison_with_multiple_changes) + assert expected_result == result + + def test_build_payload_without_base_report( + self, + sample_comparison_without_base_report, + mock_repo_provider, + mock_configuration, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_without_base_report + notifier = ChangesStatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "Unable to determine changes, no report found at pull request base", + "state": "success", + "included_helper_text": {}, + } + result = notifier.build_payload(comparison) + assert expected_result == result + + def test_notify_path_filter( + self, sample_comparison, mock_repo_provider, mock_configuration, mocker + ): + mocked_send_notification = mocker.patch.object( + ChangesStatusNotifier, "send_notification" + ) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + notifier = ChangesStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"paths": ["file_1.go"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + expected_result = { + "message": "No indirect coverage changes found", + "state": "success", + "url": f"test.example.br/gh/{sample_comparison.head.commit.repository.slug}/pull/{sample_comparison.pull.pullid}", + "included_helper_text": {}, + } + result = notifier.notify(sample_comparison) + assert result == mocked_send_notification.return_value + mocked_send_notification.assert_called_with(sample_comparison, expected_result) + + def test_build_passing_empty_upload_payload( + self, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_configuration.params["setup"]["codecov_url"] = "test.example.br" + notifier = ChangesStatusNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + decoration_type=Decoration.passing_empty_upload, + ) + expected_result = { + "state": "success", + "message": "Non-testable files changed.", + "included_helper_text": {}, + } + result = notifier.build_payload(sample_comparison) + assert expected_result == result diff --git a/apps/worker/services/notification/notifiers/tests/unit/test_webhook.py b/apps/worker/services/notification/notifiers/tests/unit/test_webhook.py new file mode 100644 index 0000000000..2dca5d7f9d --- /dev/null +++ b/apps/worker/services/notification/notifiers/tests/unit/test_webhook.py @@ -0,0 +1,738 @@ +from decimal import Decimal + +from database.tests.factories import CommitFactory, RepositoryFactory +from services.comparison.types import FullCommit +from services.notification.notifiers.webhook import WebhookNotifier +from services.repository import get_repo_provider_service + + +class TestWebhookNotifier(object): + def test_build_commit_payload( + self, dbsession, mock_configuration, sample_comparison + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + repository = RepositoryFactory.create( + owner__username="TestWebhookNotifier", name="test_build_payload" + ) + dbsession.add(repository) + dbsession.flush() + base_commit = sample_comparison.project_coverage_base.commit + head_commit = sample_comparison.head.commit + pull = sample_comparison.pull + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + notifier = WebhookNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={}, + repository_service=get_repo_provider_service( + sample_comparison.head.commit.repository + ), + ) + repository = base_commit.repository + comparison = sample_comparison + result = notifier.build_commit_payload(comparison.head) + expected_result = { + "author": { + "username": head_commit.author.username, + "service_id": head_commit.author.service_id, + "email": head_commit.author.email, + "service": head_commit.author.service, + "name": head_commit.author.name, + }, + "url": f"test.example.br/gh/{repository.slug}/commit/{head_commit.commitid}", + "timestamp": "2019-02-01T17:59:47+00:00", + "totals": { + "files": 2, + "lines": 10, + "hits": 6, + "misses": 3, + "partials": 1, + "coverage": "60.00000", + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10, + "complexity_total": 2, + "diff": None, + }, + "commitid": head_commit.commitid, + "service_url": f"https://github.com/{repository.slug}/commit/{head_commit.commitid}", + "branch": "new_branch", + "message": head_commit.message, + } + assert result["totals"] == expected_result["totals"] + assert result == expected_result + + def test_build_commit_payload_gitlab( + self, dbsession, mock_configuration, create_sample_comparison + ): + subgroup_namespace_path = "group/subgroup1/subsubgroup" + username_in_db = subgroup_namespace_path.replace("/", ":") + sample_comparison = create_sample_comparison( + username=username_in_db, service="gitlab" + ) + + mock_configuration.params["setup"]["codecov_dashboard_url"] = "codecov.io" + base_commit = sample_comparison.project_coverage_base.commit + head_commit = sample_comparison.head.commit + pull = sample_comparison.pull + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + notifier = WebhookNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={}, + repository_service=get_repo_provider_service( + sample_comparison.head.commit.repository + ), + ) + repository = base_commit.repository + comparison = sample_comparison + result = notifier.build_commit_payload(comparison.head) + expected_result = { + "author": { + "username": head_commit.author.username, + "service_id": head_commit.author.service_id, + "email": head_commit.author.email, + "service": head_commit.author.service, + "name": head_commit.author.name, + }, + "url": f"codecov.io/gl/{username_in_db}/{repository.name}/commit/{head_commit.commitid}", + "timestamp": "2019-02-01T17:59:47+00:00", + "totals": { + "files": 2, + "lines": 10, + "hits": 6, + "misses": 3, + "partials": 1, + "coverage": "60.00000", + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10, + "complexity_total": 2, + "diff": 0, + }, + "commitid": head_commit.commitid, + "service_url": f"https://gitlab.com/{subgroup_namespace_path}/{repository.name}/commit/{head_commit.commitid}", + "branch": "new_branch", + "message": head_commit.message, + } + assert result == expected_result + + def test_build_commit_payload_no_author( + self, dbsession, mock_configuration, sample_report + ): + repository = RepositoryFactory.create( + owner__username="test_build_commit_payload_no_author", + owner__service="github", + ) + dbsession.add(repository) + dbsession.flush() + head_commit = CommitFactory.create( + repository=repository, branch="new_branch", author=None + ) + head_full_commit = FullCommit(commit=head_commit, report=sample_report) + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + dbsession.add(head_commit) + dbsession.flush() + notifier = WebhookNotifier( + repository=repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={}, + repository_service=get_repo_provider_service(repository), + ) + result = notifier.build_commit_payload(head_full_commit) + expected_result = { + "author": None, + "url": f"test.example.br/gh/{repository.slug}/commit/{head_commit.commitid}", + "timestamp": "2019-02-01T17:59:47+00:00", + "totals": { + "files": 2, + "lines": 10, + "hits": 6, + "misses": 3, + "partials": 1, + "coverage": "60.00000", + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10, + "complexity_total": 2, + "diff": 0, + }, + "commitid": head_commit.commitid, + "service_url": f"https://github.com/{repository.slug}/commit/{head_commit.commitid}", + "branch": "new_branch", + "message": head_commit.message, + } + assert result == expected_result + + def test_build_payload(self, dbsession, mock_configuration, sample_comparison): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + repository = RepositoryFactory.create( + owner__username="TestWebhookNotifier", name="test_build_payload" + ) + dbsession.add(repository) + dbsession.flush() + base_commit = sample_comparison.project_coverage_base.commit + head_commit = sample_comparison.head.commit + pull = sample_comparison.pull + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + notifier = WebhookNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={}, + repository_service=get_repo_provider_service( + sample_comparison.head.commit.repository + ), + ) + repository = base_commit.repository + comparison = sample_comparison + result = notifier.build_payload(comparison) + expected_result = { + "repo": { + "url": f"test.example.br/gh/{repository.slug}", + "service_id": repository.service_id, + "name": repository.name, + "private": True, + }, + "head": { + "author": { + "username": head_commit.author.username, + "service_id": head_commit.author.service_id, + "email": head_commit.author.email, + "service": head_commit.author.service, + "name": head_commit.author.name, + }, + "url": f"test.example.br/gh/{repository.slug}/commit/{head_commit.commitid}", + "timestamp": "2019-02-01T17:59:47+00:00", + "totals": { + "files": 2, + "lines": 10, + "hits": 6, + "misses": 3, + "partials": 1, + "coverage": "60.00000", + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10, + "complexity_total": 2, + "diff": None, + }, + "commitid": head_commit.commitid, + "service_url": f"https://github.com/{repository.slug}/commit/{head_commit.commitid}", + "branch": "new_branch", + "message": head_commit.message, + }, + "base": { + "author": { + "username": base_commit.author.username, + "service_id": base_commit.author.service_id, + "email": base_commit.author.email, + "service": base_commit.author.service, + "name": base_commit.author.name, + }, + "url": f"test.example.br/gh/{repository.slug}/commit/{base_commit.commitid}", + "timestamp": "2019-02-01T17:59:47+00:00", + "totals": { + "files": 2, + "lines": 6, + "hits": 3, + "misses": 3, + "partials": 0, + "coverage": "50.00000", + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 11, + "complexity_total": 20, + "diff": None, + }, + "commitid": base_commit.commitid, + "service_url": f"https://github.com/{repository.slug}/commit/{base_commit.commitid}", + "branch": None, + "message": base_commit.message, + }, + "compare": { + "url": f"test.example.br/gh/{repository.slug}/pull/{pull.pullid}", + "message": "increased", + "coverage": Decimal("10.00"), + "notation": "+", + }, + "owner": { + "username": repository.owner.username, + "service_id": repository.owner.service_id, + "service": "github", + }, + "pull": { + "head": {"commit": head_commit.commitid, "branch": "master"}, + "number": str(pull.pullid), + "base": {"commit": base_commit.commitid, "branch": "master"}, + "open": True, + "id": pull.pullid, + "merged": False, + }, + } + assert result["repo"] == expected_result["repo"] + assert result["head"]["totals"] == expected_result["head"]["totals"] + assert result["head"] == expected_result["head"] + assert result["base"]["totals"] == expected_result["base"]["totals"] + assert result["base"] == expected_result["base"] + assert result["compare"] == expected_result["compare"] + assert result["owner"] == expected_result["owner"] + assert result["pull"] == expected_result["pull"] + assert result == expected_result + + def test_build_payload_higher_precision( + self, dbsession, mock_configuration, sample_comparison + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + repository = RepositoryFactory.create( + owner__username="TestWebhookNotifier", name="test_build_payload" + ) + dbsession.add(repository) + dbsession.flush() + base_commit = sample_comparison.project_coverage_base.commit + head_commit = sample_comparison.head.commit + pull = sample_comparison.pull + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + notifier = WebhookNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={"coverage": {"precision": 5, "round": "up"}}, + repository_service=get_repo_provider_service( + sample_comparison.head.commit.repository + ), + ) + repository = base_commit.repository + comparison = sample_comparison + result = notifier.build_payload(comparison) + expected_result = { + "repo": { + "url": f"test.example.br/gh/{repository.slug}", + "service_id": repository.service_id, + "name": repository.name, + "private": True, + }, + "head": { + "author": { + "username": head_commit.author.username, + "service_id": head_commit.author.service_id, + "email": head_commit.author.email, + "service": head_commit.author.service, + "name": head_commit.author.name, + }, + "url": f"test.example.br/gh/{repository.slug}/commit/{head_commit.commitid}", + "timestamp": "2019-02-01T17:59:47+00:00", + "totals": { + "files": 2, + "lines": 10, + "hits": 6, + "misses": 3, + "partials": 1, + "coverage": "60.00000", + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10, + "complexity_total": 2, + "diff": None, + }, + "commitid": head_commit.commitid, + "service_url": f"https://github.com/{repository.slug}/commit/{head_commit.commitid}", + "branch": "new_branch", + "message": head_commit.message, + }, + "base": { + "author": { + "username": base_commit.author.username, + "service_id": base_commit.author.service_id, + "email": base_commit.author.email, + "service": base_commit.author.service, + "name": base_commit.author.name, + }, + "url": f"test.example.br/gh/{repository.slug}/commit/{base_commit.commitid}", + "timestamp": "2019-02-01T17:59:47+00:00", + "totals": { + "files": 2, + "lines": 6, + "hits": 3, + "misses": 3, + "partials": 0, + "coverage": "50.00000", + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 11, + "complexity_total": 20, + "diff": None, + }, + "commitid": base_commit.commitid, + "service_url": f"https://github.com/{repository.slug}/commit/{base_commit.commitid}", + "branch": None, + "message": base_commit.message, + }, + "compare": { + "url": f"test.example.br/gh/{repository.slug}/pull/{pull.pullid}", + "message": "increased", + "coverage": Decimal("10.00"), + "notation": "+", + }, + "owner": { + "username": repository.owner.username, + "service_id": repository.owner.service_id, + "service": "github", + }, + "pull": { + "head": {"commit": head_commit.commitid, "branch": "master"}, + "number": str(pull.pullid), + "base": {"commit": base_commit.commitid, "branch": "master"}, + "open": True, + "id": pull.pullid, + "merged": False, + }, + } + + assert result["repo"] == expected_result["repo"] + assert result["head"] == expected_result["head"] + assert result["base"]["totals"] == expected_result["base"]["totals"] + assert result["base"] == expected_result["base"] + assert result["compare"] == expected_result["compare"] + assert result["owner"] == expected_result["owner"] + assert result["pull"] == expected_result["pull"] + assert result == expected_result + + def test_build_payload_without_pull( + self, sample_comparison_without_pull, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_without_pull + commit = sample_comparison_without_pull.head.commit + base_commit = comparison.project_coverage_base.commit + head_commit = comparison.head.commit + repository = commit.repository + notifier = WebhookNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={}, + repository_service=get_repo_provider_service( + comparison.head.commit.repository + ), + ) + result = notifier.build_payload(comparison) + expected_result = { + "repo": { + "url": f"test.example.br/gh/{repository.slug}", + "service_id": repository.service_id, + "name": repository.name, + "private": True, + }, + "head": { + "author": { + "username": head_commit.author.username, + "service_id": head_commit.author.service_id, + "email": head_commit.author.email, + "service": head_commit.author.service, + "name": head_commit.author.name, + }, + "url": f"test.example.br/gh/{repository.slug}/commit/{head_commit.commitid}", + "timestamp": "2019-02-01T17:59:47+00:00", + "totals": { + "files": 2, + "lines": 10, + "hits": 6, + "misses": 3, + "partials": 1, + "coverage": "60.00000", + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10, + "complexity_total": 2, + "diff": None, + }, + "commitid": head_commit.commitid, + "service_url": f"https://github.com/{repository.slug}/commit/{head_commit.commitid}", + "branch": "new_branch", + "message": head_commit.message, + }, + "base": { + "author": { + "username": base_commit.author.username, + "service_id": base_commit.author.service_id, + "email": base_commit.author.email, + "service": base_commit.author.service, + "name": base_commit.author.name, + }, + "url": f"test.example.br/gh/{repository.slug}/commit/{base_commit.commitid}", + "timestamp": "2019-02-01T17:59:47+00:00", + "totals": { + "files": 2, + "lines": 6, + "hits": 3, + "misses": 3, + "partials": 0, + "coverage": "50.00000", + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 11, + "complexity_total": 20, + "diff": None, + }, + "commitid": base_commit.commitid, + "service_url": f"https://github.com/{repository.slug}/commit/{base_commit.commitid}", + "branch": None, + "message": base_commit.message, + }, + "compare": { + "url": f"test.example.br/gh/{repository.slug}/commit/{head_commit.commitid}", + "message": "increased", + "coverage": Decimal("10.00"), + "notation": "+", + }, + "owner": { + "username": repository.owner.username, + "service_id": repository.owner.service_id, + "service": "github", + }, + "pull": None, + } + + assert result["repo"] == expected_result["repo"] + assert result["head"] == expected_result["head"] + assert result["base"]["totals"] == expected_result["base"]["totals"] + assert result["base"] == expected_result["base"] + assert result["compare"] == expected_result["compare"] + assert result["owner"] == expected_result["owner"] + assert result["pull"] == expected_result["pull"] + assert result == expected_result + + def test_build_payload_without_base_report( + self, + sample_comparison_without_base_report, + mock_configuration, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_without_base_report + commit = comparison.head.commit + repository = commit.repository + notifier = WebhookNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={}, + repository_service=get_repo_provider_service( + comparison.head.commit.repository + ), + ) + result = notifier.build_payload(comparison) + head_commit = comparison.head.commit + base_commit = comparison.project_coverage_base.commit + pull = comparison.pull + expected_result = { + "repo": { + "url": f"test.example.br/gh/{repository.slug}", + "service_id": repository.service_id, + "name": repository.name, + "private": True, + }, + "head": { + "author": { + "username": head_commit.author.username, + "service_id": head_commit.author.service_id, + "email": head_commit.author.email, + "service": head_commit.author.service, + "name": head_commit.author.name, + }, + "url": f"test.example.br/gh/{repository.slug}/commit/{head_commit.commitid}", + "timestamp": "2019-02-01T17:59:47+00:00", + "totals": { + "files": 2, + "lines": 10, + "hits": 6, + "misses": 3, + "partials": 1, + "coverage": "60.00000", + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10, + "complexity_total": 2, + "diff": None, + }, + "commitid": head_commit.commitid, + "service_url": f"https://github.com/{repository.slug}/commit/{head_commit.commitid}", + "branch": "new_branch", + "message": head_commit.message, + }, + "base": { + "author": { + "username": base_commit.author.username, + "service_id": base_commit.author.service_id, + "email": base_commit.author.email, + "service": base_commit.author.service, + "name": base_commit.author.name, + }, + "url": f"test.example.br/gh/{repository.slug}/commit/{base_commit.commitid}", + "timestamp": "2019-02-01T17:59:47+00:00", + "totals": None, + "commitid": base_commit.commitid, + "service_url": f"https://github.com/{repository.slug}/commit/{base_commit.commitid}", + "branch": None, + "message": base_commit.message, + }, + "compare": { + "url": None, + "message": "unknown", + "coverage": None, + "notation": "", + }, + "owner": { + "username": repository.owner.username, + "service_id": repository.owner.service_id, + "service": "github", + }, + "pull": { + "head": {"commit": head_commit.commitid, "branch": "master"}, + "number": str(pull.pullid), + "base": {"commit": base_commit.commitid, "branch": "master"}, + "open": True, + "id": pull.pullid, + "merged": False, + }, + } + + assert result["repo"] == expected_result["repo"] + assert result["head"] == expected_result["head"] + assert result["base"]["totals"] == expected_result["base"]["totals"] + assert result["base"] == expected_result["base"] + assert result["compare"] == expected_result["compare"] + assert result["owner"] == expected_result["owner"] + assert result["pull"] == expected_result["pull"] + assert result == expected_result + + def test_build_payload_without_base( + self, + sample_comparison_without_base_with_pull, + mock_configuration, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = "test.example.br" + comparison = sample_comparison_without_base_with_pull + commit = comparison.head.commit + repository = commit.repository + notifier = WebhookNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml={}, + repository_service=get_repo_provider_service( + comparison.head.commit.repository + ), + ) + result = notifier.build_payload(comparison) + head_commit = comparison.head.commit + pull = comparison.pull + expected_result = { + "repo": { + "url": f"test.example.br/gh/{repository.slug}", + "service_id": repository.service_id, + "name": repository.name, + "private": True, + }, + "head": { + "author": { + "username": head_commit.author.username, + "service_id": head_commit.author.service_id, + "email": head_commit.author.email, + "service": head_commit.author.service, + "name": head_commit.author.name, + }, + "url": f"test.example.br/gh/{repository.slug}/commit/{head_commit.commitid}", + "timestamp": "2019-02-01T17:59:47+00:00", + "totals": { + "files": 2, + "lines": 10, + "hits": 6, + "misses": 3, + "partials": 1, + "coverage": "60.00000", + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10, + "complexity_total": 2, + "diff": None, + }, + "commitid": head_commit.commitid, + "service_url": f"https://github.com/{repository.slug}/commit/{head_commit.commitid}", + "branch": "new_branch", + "message": head_commit.message, + }, + "base": None, + "compare": { + "url": None, + "message": "unknown", + "coverage": None, + "notation": "", + }, + "owner": { + "username": repository.owner.username, + "service_id": repository.owner.service_id, + "service": "github", + }, + "pull": { + "head": {"commit": head_commit.commitid, "branch": "master"}, + "number": str(pull.pullid), + "base": {"commit": "base_commitid", "branch": "master"}, + "open": True, + "id": pull.pullid, + "merged": False, + }, + } + + assert result["repo"] == expected_result["repo"] + assert result["head"] == expected_result["head"] + assert result["base"] == expected_result["base"] + assert result["compare"] == expected_result["compare"] + assert result["owner"] == expected_result["owner"] + assert result["pull"] == expected_result["pull"] + assert result == expected_result diff --git a/apps/worker/services/notification/notifiers/webhook.py b/apps/worker/services/notification/notifiers/webhook.py new file mode 100644 index 0000000000..ecf5645b61 --- /dev/null +++ b/apps/worker/services/notification/notifiers/webhook.py @@ -0,0 +1,78 @@ +import logging + +from shared.torngit.enums import Endpoints + +from database.enums import Notification +from services.comparison.types import Comparison, FullCommit +from services.notification.notifiers.generics import RequestsYamlBasedNotifier +from services.urls import get_commit_url, get_repository_url + +log = logging.getLogger(__name__) + + +class WebhookNotifier(RequestsYamlBasedNotifier): + @property + def notification_type(self) -> Notification: + return Notification.webhook + + def build_commit_payload(self, full_commit: FullCommit): + if full_commit.commit is None: + return None + commit = full_commit.commit + author_dict = None + if commit.author is not None: + author_dict = { + "username": commit.author.username, + "service_id": commit.author.service_id, + "email": commit.author.email, + "service": commit.author.service, + "name": commit.author.name, + } + return { + "author": author_dict, + "url": get_commit_url(commit), + "timestamp": commit.timestamp.isoformat(), + "totals": full_commit.report.totals.asdict() + if full_commit.report is not None + else None, + "commitid": commit.commitid, + "service_url": self.repository_service.get_href( + Endpoints.commit_detail, commitid=commit.commitid + ), + "branch": commit.branch, + "message": commit.message, + } + + def build_payload(self, comparison: Comparison) -> dict: + head_full_commit = comparison.head + base_full_commit = comparison.project_coverage_base + pull = comparison.pull + head_commit = head_full_commit.commit + repository = head_commit.repository + pull_dict = None + if pull: + pull_dict = { + "head": {"commit": pull.head, "branch": "master"}, + "number": str(pull.pullid), + "base": {"commit": pull.base, "branch": "master"}, + "open": pull.state == "open", + "id": pull.pullid, + "merged": pull.state == "merged", + } + return { + "repo": { + "url": get_repository_url(head_commit.repository), + "service_id": repository.service_id, + "name": repository.name, + "private": repository.private, + }, + "head": self.build_commit_payload(head_full_commit), + "base": self.build_commit_payload(base_full_commit), + "compare": self.generate_compare_dict(comparison), + "owner": { + "username": repository.owner.username, + "service_id": repository.owner.service_id, + "service": repository.owner.service, + }, + "pull": pull_dict, + } diff --git a/apps/worker/services/notification/tests/unit/test_commit_notifications.py b/apps/worker/services/notification/tests/unit/test_commit_notifications.py new file mode 100644 index 0000000000..9f728e4d7e --- /dev/null +++ b/apps/worker/services/notification/tests/unit/test_commit_notifications.py @@ -0,0 +1,219 @@ +import pytest + +from database.enums import Decoration, Notification, NotificationState +from database.models.core import GithubAppInstallation +from database.tests.factories import ( + CommitFactory, + CommitNotificationFactory, + PullFactory, + RepositoryFactory, +) +from services.comparison.types import Comparison, FullCommit +from services.notification.commit_notifications import ( + create_or_update_commit_notification_from_notification_result, +) +from services.notification.notifiers.base import NotificationResult +from services.notification.notifiers.comment import CommentNotifier +from services.repository import EnrichedPull + + +@pytest.fixture +def comparison(dbsession): + repository = RepositoryFactory.create( + owner__username="codecov", + owner__unencrypted_oauth_token="testtlxuu2kfef3km1fbecdlmnb2nvpikvmoadi3", + owner__plan="users-pr-inappm", + name="example-python", + image_token="abcdefghij", + private=True, + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository) + head_commit = CommitFactory.create(repository=repository) + pull = PullFactory.create( + repository=repository, + base=base_commit.commitid, + head=head_commit.commitid, + state="merged", + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + return Comparison( + head=FullCommit(commit=head_commit, report=None), + project_coverage_base=FullCommit(commit=base_commit, report=None), + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull(database_pull=pull, provider_pull=None), + ) + + +class TestCommitNotificationsServiceTestCase(object): + def test_create_or_update_commit_notification_not_yet_exists( + self, dbsession, comparison + ): + pull = comparison.pull + commit = pull.get_head_commit() + notifier = CommentNotifier( + repository=pull.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + decoration_type=Decoration.standard, + ) + notify_res = NotificationResult( + notification_attempted=True, + notification_successful=False, + explanation=None, + data_received=dict(id=123), + data_sent=dict(a=1, b=2), + ) + + res = create_or_update_commit_notification_from_notification_result( + comparison, notifier, notify_res + ) + dbsession.flush() + assert res.commit_id == commit.id_ + assert res.decoration_type == notifier.decoration_type + assert res.notification_type == notifier.notification_type + assert res.state == NotificationState.error + + def test_create_or_update_commit_notification_not_yet_exists_no_pull_but_ghapp_info( + self, dbsession, comparison + ): + comparison.enriched_pull = None + commit = comparison.head.commit + app = GithubAppInstallation(owner=commit.repository.owner, installation_id=1234) + dbsession.add(app) + dbsession.flush() + notifier = CommentNotifier( + repository=commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + decoration_type=Decoration.standard, + ) + notify_res = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_received=dict(id=123), + data_sent=dict(a=1, b=2), + github_app_used=app.id, + ) + + res = create_or_update_commit_notification_from_notification_result( + comparison, notifier, notify_res + ) + dbsession.flush() + assert res.commit_id == commit.id_ + assert res.decoration_type == notifier.decoration_type + assert res.notification_type == notifier.notification_type + assert res.state == NotificationState.success + assert res.gh_app_id == app.id + + def test_create_or_update_commit_notification_no_result( + self, dbsession, comparison + ): + commit = comparison.head.commit + notifier = CommentNotifier( + repository=commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + decoration_type=Decoration.standard, + ) + result_dict = None + res = create_or_update_commit_notification_from_notification_result( + comparison, notifier, result_dict + ) + dbsession.flush() + assert res.commit_id == commit.id_ + assert res.decoration_type == notifier.decoration_type + assert res.notification_type == notifier.notification_type + assert res.state == NotificationState.error + + def test_create_or_update_commit_notification_decoration_change( + self, dbsession, comparison + ): + head_commit = comparison.head.commit + + cn = CommitNotificationFactory( + commit=head_commit, + notification_type=Notification.comment, + decoration_type=Decoration.upgrade, + state=NotificationState.success, + ) + dbsession.add(cn) + dbsession.flush() + + notifier = CommentNotifier( + repository=head_commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + decoration_type=Decoration.standard, + ) + notify_res = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_received=dict(id=123), + data_sent=dict(a=1, b=2), + ) + res = create_or_update_commit_notification_from_notification_result( + comparison, notifier, notify_res + ) + dbsession.flush() + assert cn.commit_id == head_commit.id_ + assert cn.decoration_type == notifier.decoration_type + assert cn.notification_type == notifier.notification_type + assert cn.state == NotificationState.success + + def test_create_or_update_commit_notification_now_successful( + self, dbsession, comparison + ): + head_commit = comparison.head.commit + + cn = CommitNotificationFactory( + commit=head_commit, + notification_type=Notification.comment, + decoration_type=Decoration.upgrade, + state=NotificationState.error, + ) + dbsession.add(cn) + dbsession.flush() + + notifier = CommentNotifier( + repository=head_commit.repository, + title="title", + notifier_yaml_settings={"layout": "reach, diff, flags, files, footer"}, + notifier_site_settings=True, + current_yaml={}, + repository_service=None, + decoration_type=Decoration.standard, + ) + notify_res = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_received=dict(id=123), + data_sent=dict(a=1, b=2), + ) + res = create_or_update_commit_notification_from_notification_result( + comparison, notifier, notify_res + ) + dbsession.flush() + assert cn.commit_id == head_commit.id_ + assert cn.decoration_type == notifier.decoration_type + assert cn.notification_type == notifier.notification_type + assert cn.state == NotificationState.success diff --git a/apps/worker/services/notification/tests/unit/test_comparison.py b/apps/worker/services/notification/tests/unit/test_comparison.py new file mode 100644 index 0000000000..d61733c32c --- /dev/null +++ b/apps/worker/services/notification/tests/unit/test_comparison.py @@ -0,0 +1,14 @@ +from services.comparison import ComparisonProxy, FilteredComparison + + +class TestFilteredComparison(object): + def test_get_existing_statuses(self, mocker): + mocked_get_existing_statuses = mocker.patch.object( + ComparisonProxy, "get_existing_statuses" + ) + flags, path_patterns = ["flag"], None + comparison = ComparisonProxy(mocker.MagicMock()) + filtered_comparison = comparison.get_filtered_comparison(flags, path_patterns) + assert isinstance(filtered_comparison, FilteredComparison) + res = filtered_comparison.get_existing_statuses() + assert res == mocked_get_existing_statuses.return_value diff --git a/apps/worker/services/notification/tests/unit/test_notification_result.py b/apps/worker/services/notification/tests/unit/test_notification_result.py new file mode 100644 index 0000000000..52b2ff60c1 --- /dev/null +++ b/apps/worker/services/notification/tests/unit/test_notification_result.py @@ -0,0 +1,35 @@ +from services.notification.notifiers.base import NotificationResult + + +def test_notification_result_or_operation(): + result_default = NotificationResult() + result_explanation = NotificationResult(explanation="some_explanation") + result_not_attempted = NotificationResult( + notification_attempted=False, + notification_successful=False, + explanation="dont_want", + ) + result_attempted = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + ) + result_some_data = NotificationResult( + data_sent={"comment": "hi"}, data_received={"response": "hi"} + ) + assert result_default.merge(result_explanation) == result_explanation + assert result_default.merge(result_not_attempted) == result_not_attempted + assert result_attempted.merge(result_some_data) == NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={"comment": "hi"}, + data_received={"response": "hi"}, + ) + assert result_not_attempted.merge(result_some_data) == NotificationResult( + notification_attempted=False, + notification_successful=False, + explanation="dont_want", + data_sent={"comment": "hi"}, + data_received={"response": "hi"}, + ) diff --git a/apps/worker/services/notification/tests/unit/test_notification_service.py b/apps/worker/services/notification/tests/unit/test_notification_service.py new file mode 100644 index 0000000000..ceaa1c9e5b --- /dev/null +++ b/apps/worker/services/notification/tests/unit/test_notification_service.py @@ -0,0 +1,1181 @@ +import os +from asyncio import CancelledError +from asyncio import TimeoutError as AsyncioTimeoutError + +import mock +import pytest +from celery.exceptions import SoftTimeLimitExceeded +from shared.plan.constants import PlanName +from shared.reports.reportfile import ReportFile +from shared.reports.resources import Report +from shared.reports.types import Change, ReportLine, ReportTotals +from shared.torngit.status import Status +from shared.yaml import UserYaml + +from database.enums import Decoration, Notification, NotificationState +from database.models.core import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, +) +from database.tests.factories import CommitFactory, PullFactory, RepositoryFactory +from services.comparison import ComparisonProxy +from services.comparison.types import Comparison, EnrichedPull, FullCommit +from services.notification import NotificationService +from services.notification.notifiers import ( + CommentNotifier, + PatchChecksNotifier, + StatusType, +) +from services.notification.notifiers.base import NotificationResult +from services.notification.notifiers.checks import ProjectChecksNotifier +from services.notification.notifiers.checks.checks_with_fallback import ( + ChecksWithFallback, +) +from services.notification.notifiers.mixins.status import ( + HelperTextKey, + HelperTextTemplate, +) +from tests.helpers import mock_all_plans_and_tiers + + +@pytest.fixture +def sample_comparison(dbsession, request): + repository = RepositoryFactory.create( + owner__username=request.node.name, + owner__service="github", + using_integration=True, + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository) + head_commit = CommitFactory.create(repository=repository, branch="new_branch") + pull = PullFactory.create( + repository=repository, base=base_commit.commitid, head=head_commit.commitid + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + repository = base_commit.repository + base_full_commit = FullCommit(commit=base_commit, report=None) + head_full_commit = FullCommit(commit=head_commit, report=None) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={ + "head": {"commitid": head_full_commit.commit.commitid}, + "base": { + "commitid": base_full_commit.commit.commitid, + "branch": {}, + }, + }, + ), + ) + ) + + +class TestNotificationService(object): + @pytest.fixture(autouse=True) + def mock_all_plans_and_tiers_fixture(self): + mock_all_plans_and_tiers() + + @pytest.mark.django_db + def test_should_use_checks_notifier_yaml_field_false(self, dbsession): + repository = RepositoryFactory.create() + current_yaml = {"github_checks": False} + service = NotificationService(repository, current_yaml, None) + assert ( + service._should_use_checks_notifier(status_type=StatusType.PROJECT.value) + == False + ) + + @pytest.mark.parametrize( + "repo_data,outcome", + [ + ( + dict( + using_integration=True, + owner__integration_id=12341234, + owner__service="github", + ), + True, + ), + ( + dict( + using_integration=True, + owner__integration_id=12341234, + owner__service="gitlab", + ), + False, + ), + ( + dict( + using_integration=True, + owner__integration_id=12341234, + owner__service="github_enterprise", + ), + True, + ), + ( + dict( + using_integration=False, + owner__integration_id=None, + owner__service="github", + ), + False, + ), + ], + ) + @pytest.mark.django_db + def test_should_use_checks_notifier_deprecated_flow( + self, repo_data, outcome, dbsession + ): + repository = RepositoryFactory.create(**repo_data) + current_yaml = {"github_checks": True} + assert repository.owner.github_app_installations == [] + service = NotificationService(repository, current_yaml, None) + assert ( + service._should_use_checks_notifier(status_type=StatusType.PROJECT.value) + == outcome + ) + + @pytest.mark.django_db + def test_should_use_checks_notifier_ghapp_all_repos_covered(self, dbsession): + repository = RepositoryFactory.create(owner__service="github") + ghapp_installation = GithubAppInstallation( + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + installation_id=456789, + owner=repository.owner, + repository_service_ids=None, + ) + dbsession.add(ghapp_installation) + dbsession.flush() + current_yaml = {"github_checks": True} + assert repository.owner.github_app_installations == [ghapp_installation] + service = NotificationService(repository, current_yaml, None) + assert ( + service._should_use_checks_notifier(status_type=StatusType.PROJECT.value) + == True + ) + + @pytest.mark.django_db + def test_use_checks_notifier_for_team_plan( + self, + dbsession, + ): + repository = RepositoryFactory.create( + owner__service="github", owner__plan=PlanName.TEAM_MONTHLY.value + ) + ghapp_installation = GithubAppInstallation( + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + installation_id=456789, + owner=repository.owner, + repository_service_ids=None, + ) + dbsession.add(ghapp_installation) + dbsession.flush() + current_yaml = {"github_checks": True} + assert repository.owner.github_app_installations == [ghapp_installation] + service = NotificationService(repository, current_yaml, None) + assert ( + service._should_use_checks_notifier(status_type=StatusType.PROJECT.value) + == False + ) + assert ( + service._should_use_checks_notifier(status_type=StatusType.CHANGES.value) + == False + ) + assert ( + service._should_use_checks_notifier(status_type=StatusType.PATCH.value) + == True + ) + + @pytest.mark.django_db + def test_use_status_notifier_for_team_plan(self, dbsession): + repository = RepositoryFactory.create( + owner__service="github", owner__plan=PlanName.TEAM_MONTHLY.value + ) + ghapp_installation = GithubAppInstallation( + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + installation_id=456789, + owner=repository.owner, + repository_service_ids=None, + ) + dbsession.add(ghapp_installation) + dbsession.flush() + current_yaml = {"github_checks": True} + assert repository.owner.github_app_installations == [ghapp_installation] + service = NotificationService(repository, current_yaml, None) + assert ( + service._should_use_status_notifier(status_type=StatusType.PROJECT.value) + == False + ) + assert ( + service._should_use_checks_notifier(status_type=StatusType.CHANGES.value) + == False + ) + assert ( + service._should_use_checks_notifier(status_type=StatusType.PATCH.value) + == True + ) + + @pytest.mark.django_db + def test_use_status_notifier_for_non_team_plan(self, dbsession): + repository = RepositoryFactory.create( + owner__service="github", owner__plan=PlanName.CODECOV_PRO_MONTHLY.value + ) + ghapp_installation = GithubAppInstallation( + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + installation_id=456789, + owner=repository.owner, + repository_service_ids=None, + ) + dbsession.add(ghapp_installation) + dbsession.flush() + current_yaml = {"github_checks": True} + assert repository.owner.github_app_installations == [ghapp_installation] + service = NotificationService(repository, current_yaml, None) + assert ( + service._should_use_status_notifier(status_type=StatusType.PROJECT.value) + == True + ) + assert ( + service._should_use_checks_notifier(status_type=StatusType.CHANGES.value) + == True + ) + assert ( + service._should_use_checks_notifier(status_type=StatusType.PATCH.value) + == True + ) + + @pytest.mark.parametrize( + "gh_installation_name", + [GITHUB_APP_INSTALLATION_DEFAULT_NAME, "notifications-app"], + ) + @pytest.mark.django_db + def test_should_use_checks_notifier_ghapp_some_repos_covered( + self, dbsession, gh_installation_name + ): + repository = RepositoryFactory.create(owner__service="github") + other_repo_same_owner = RepositoryFactory.create(owner=repository.owner) + ghapp_installation = GithubAppInstallation( + name=gh_installation_name, + installation_id=456789, + owner=repository.owner, + repository_service_ids=[repository.service_id], + app_id=123123, + pem_path="path_to_pem_file", + ) + dbsession.add(ghapp_installation) + dbsession.flush() + current_yaml = {"github_checks": True} + assert repository.owner.github_app_installations == [ghapp_installation] + service = NotificationService( + repository, + current_yaml, + None, + gh_installation_name_to_use=gh_installation_name, + ) + assert ( + service._should_use_checks_notifier(status_type=StatusType.PROJECT.value) + == True + ) + service = NotificationService(other_repo_same_owner, current_yaml, None) + assert ( + service._should_use_checks_notifier(status_type=StatusType.PROJECT.value) + == False + ) + + @pytest.mark.django_db + def test_get_notifiers_instances_only_third_party( + self, dbsession, mock_configuration + ): + mock_configuration.params["services"] = { + "notifications": {"slack": ["slack.com"]} + } + repository = RepositoryFactory.create( + owner__unencrypted_oauth_token="testlln8sdeec57lz83oe3l8y9qq4lhqat2f1kzm", + owner__username="ThiagoCodecov", + yaml={"codecov": {"max_report_age": "1y ago"}}, + name="example-python", + ) + dbsession.add(repository) + dbsession.flush() + current_yaml = { + "coverage": {"notify": {"slack": {"default": {"field": "1y ago"}}}} + } + service = NotificationService(repository, current_yaml, None) + instances = list(service.get_notifiers_instances()) + assert len(instances) == 2 + instance = instances[0] + assert instance.repository == repository + assert instance.title == "default" + assert instance.notifier_yaml_settings == {"field": "1y ago"} + assert instance.site_settings == ["slack.com"] + assert instance.current_yaml == current_yaml + + @pytest.mark.django_db + def test_get_notifiers_instances_checks( + self, dbsession, mock_configuration, mocker + ): + repository = RepositoryFactory.create( + owner__integration_id=123, + owner__service="github", + yaml={"codecov": {"max_report_age": "1y ago"}}, + name="example-python", + using_integration=True, + ) + + dbsession.add(repository) + dbsession.flush() + current_yaml = { + "coverage": {"status": {"project": True, "patch": True, "changes": True}} + } + mocker.patch.dict( + os.environ, {"CHECKS_WHITELISTED_OWNERS": f"0,{repository.owner.ownerid}"} + ) + service = NotificationService(repository, current_yaml, None) + instances = list(service.get_notifiers_instances()) + names = sorted([instance.name for instance in instances]) + types = sorted(instance.notification_type.value for instance in instances) + assert names == [ + "checks-changes-with-fallback", + "checks-patch-with-fallback", + "checks-project-with-fallback", + "codecov-slack-app", + ] + assert types == [ + "checks_changes", + "checks_patch", + "checks_project", + "codecov_slack_app", + ] + + @pytest.mark.django_db + def test_get_notifiers_instances_slack_app_false( + self, dbsession, mock_configuration, mocker + ): + mocker.patch("services.notification.get_config", return_value=False) + repository = RepositoryFactory.create( + owner__integration_id=123, + owner__service="github", + yaml={"codecov": {"max_report_age": "1y ago"}}, + name="example-python", + using_integration=True, + ) + + dbsession.add(repository) + dbsession.flush() + current_yaml = { + "coverage": {"status": {"project": True, "patch": True, "changes": True}} + } + mocker.patch.dict( + os.environ, {"CHECKS_WHITELISTED_OWNERS": f"0,{repository.owner.ownerid}"} + ) + service = NotificationService(repository, current_yaml, None) + instances = list(service.get_notifiers_instances()) + names = sorted([instance.name for instance in instances]) + assert names == [ + "checks-changes-with-fallback", + "checks-patch-with-fallback", + "checks-project-with-fallback", + ] + + @pytest.mark.parametrize( + "gh_installation_name", + [GITHUB_APP_INSTALLATION_DEFAULT_NAME, "notifications-app"], + ) + @pytest.mark.django_db + def test_get_notifiers_instances_checks_percentage_whitelist( + self, + dbsession, + mock_configuration, + mocker, + gh_installation_name, + ): + repository = RepositoryFactory.create( + owner__integration_id=123, + owner__service="github", + owner__ownerid=1234, + yaml={"codecov": {"max_report_age": "1y ago"}}, + name="example-python", + using_integration=True, + ) + dbsession.add(repository) + dbsession.flush() + current_yaml = { + "coverage": {"status": {"project": True, "patch": True, "changes": True}} + } + mocker.patch.dict( + os.environ, + { + "CHECKS_WHITELISTED_OWNERS": "0,1", + "CHECKS_WHITELISTED_PERCENTAGE": "35", + }, + ) + service = NotificationService( + repository, + current_yaml, + gh_installation_name, + ) + instances = list(service.get_notifiers_instances()) + # we don't need that for slack-app notifier + names = [ + instance._checks_notifier.name + for instance in instances + if instance.name != "codecov-slack-app" + ] + assert names == ["checks-project", "checks-patch", "checks-changes"] + for instance in instances: + if isinstance(instance, ChecksWithFallback): + assert ( + instance._checks_notifier.repository_service == gh_installation_name + ) + assert ( + instance._status_notifier.repository_service == gh_installation_name + ) + else: + assert instance.repository_service == gh_installation_name + + @pytest.mark.parametrize( + "gh_installation_name", + [GITHUB_APP_INSTALLATION_DEFAULT_NAME, "notifications-app"], + ) + @pytest.mark.django_db + def test_get_notifiers_instances_comment( + self, dbsession, mock_configuration, mocker, gh_installation_name + ): + repository = RepositoryFactory.create( + owner__integration_id=123, + owner__service="github", + owner__ownerid=1234, + yaml={"codecov": {"max_report_age": "1y ago"}}, + name="example-python", + using_integration=True, + ) + dbsession.add(repository) + dbsession.flush() + current_yaml = {"comment": {"layout": "condensed_header"}, "slack_app": False} + service = NotificationService( + repository, + current_yaml, + gh_installation_name, + ) + instances = list(service.get_notifiers_instances()) + assert len(instances) == 1 + assert instances[0].repository_service == gh_installation_name + + @pytest.mark.django_db + def test_notify_general_exception(self, mocker, dbsession, sample_comparison): + current_yaml = {} + commit = sample_comparison.head.commit + good_notifier = mocker.MagicMock( + is_enabled=mocker.MagicMock(return_value=True), + title="good_notifier", + notification_type=Notification.comment, + decoration_type=Decoration.standard, + notify=mock.Mock(), + ) + bad_notifier = mocker.MagicMock( + is_enabled=mocker.MagicMock(return_value=True), + title="bad_notifier", + notification_type=Notification.status_project, + decoration_type=Decoration.standard, + ) + disabled_notifier = mocker.MagicMock( + is_enabled=mocker.MagicMock(return_value=False), + title="disabled_notifier", + notification_type=Notification.status_patch, + decoration_type=Decoration.standard, + notify=mock.Mock(), + ) + good_notifier.notify.return_value = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation="", + data_sent={"some": "data"}, + ) + good_notifier.name = "good_name" + bad_notifier.name = "bad_name" + disabled_notifier.name = "disabled_notifier_name" + bad_notifier.notify.side_effect = Exception("This is bad") + mocker.patch.object( + NotificationService, + "get_notifiers_instances", + return_value=[bad_notifier, good_notifier, disabled_notifier], + ) + notifications_service = NotificationService( + commit.repository, current_yaml, None + ) + expected_result = [ + {"notifier": "bad_name", "title": "bad_notifier", "result": None}, + { + "notifier": "good_name", + "title": "good_notifier", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation="", + data_sent={"some": "data"}, + data_received=None, + ), + }, + ] + res = notifications_service.notify(sample_comparison) + assert expected_result == res + + @pytest.mark.django_db + def test_notify_data_sent_None(self, mocker, dbsession, sample_comparison): + current_yaml = {} + commit = sample_comparison.head.commit + good_notifier = mocker.MagicMock( + is_enabled=mocker.MagicMock(return_value=True), + title="good_notifier", + notification_type=Notification.comment, + decoration_type=Decoration.standard, + notify=mock.Mock(), + ) + skipped_notifier = mocker.MagicMock( + is_enabled=mocker.MagicMock(return_value=True), + title="skippy_notifier", + notification_type=Notification.status_project, + decoration_type=Decoration.standard, + ) + good_notifier.notify.return_value = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation="", + data_sent={"some": "data"}, + ) + skipped_expected_return = NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="exclude_flag_coverage_not_uploaded_checks", + data_sent=None, + data_received=None, + github_app_used=None, + ) + skipped_notifier.notify.return_value = skipped_expected_return + good_notifier.name = "good_name" + skipped_notifier.name = "skippy" + + mocker.patch.object( + NotificationService, + "get_notifiers_instances", + return_value=[skipped_notifier, good_notifier], + ) + + notifications_service = NotificationService( + commit.repository, current_yaml, None + ) + expected_result = [ + { + "notifier": "skippy", + "title": "skippy_notifier", + "result": skipped_expected_return, + }, + { + "notifier": "good_name", + "title": "good_notifier", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation="", + data_sent={"some": "data"}, + data_received=None, + ), + }, + ] + res = notifications_service.notify(sample_comparison) + assert expected_result == res + + @pytest.mark.django_db + def test_notify_individual_notifier_timeout(self, mocker, sample_comparison): + current_yaml = {} + commit = sample_comparison.head.commit + notifier = mocker.MagicMock( + title="fake_notifier", + notify=mock.Mock(), + notification_type=Notification.comment, + decoration_type=Decoration.standard, + ) + notifier.notify.side_effect = AsyncioTimeoutError + notifications_service = NotificationService( + commit.repository, current_yaml, None + ) + res = notifications_service.notify_individual_notifier( + notifier, sample_comparison + ) + assert res == (notifier, None) + + @pytest.mark.django_db + def test_notify_individual_checks_project_notifier( + self, mocker, sample_comparison, mock_repo_provider, mock_configuration + ): + mock_repo_provider.get_commit_statuses.return_value = Status([]) + mock_configuration._params["setup"] = {"codecov_dashboard_url": "test"} + current_yaml = {} + commit = sample_comparison.head.commit + report = Report() + first_deleted_file = ReportFile("file_1.go") + first_deleted_file.append(1, ReportLine.create(coverage=0, sessions=[])) + first_deleted_file.append(2, ReportLine.create(coverage=0, sessions=[])) + first_deleted_file.append(3, ReportLine.create(coverage=0, sessions=[])) + first_deleted_file.append(5, ReportLine.create(coverage=0, sessions=[])) + report.append(first_deleted_file) + sample_comparison.head.report = report + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + notifier = ProjectChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={"flags": ["flagone"]}, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + notifications_service = NotificationService( + commit.repository, current_yaml, mock_repo_provider + ) + res = notifications_service.notify_individual_notifier( + notifier, sample_comparison + ) + + assert res == ( + notifier, + NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "state": "success", + "output": { + "title": "No coverage information found on head", + "summary": f"[View this Pull Request on Codecov](test/gh/test_notify_individual_checks_project_notifier/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\nNo coverage information found on head", + "annotations": [], + }, + "included_helper_text": {}, + "url": f"test/gh/test_notify_individual_checks_project_notifier/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}", + }, + data_received=None, + ), + ) + + @pytest.mark.django_db + def test_notify_individual_checks_patch_and_project_notifier_included_helper_text( + self, + mocker, + sample_comparison, + mock_repo_provider, + mock_configuration, + dbsession, + ): + """ + A failed check/status notification with included_helper_text must pass the + included_helper_text along to the comment notifier. + """ + pull_with_coverage = PullFactory( + repository=sample_comparison.enriched_pull.database_pull.repository, + commentid="1234", + ) # add this so we don't get is_first_coverage_pull + dbsession.add(pull_with_coverage) + dbsession.flush() + mock_repo_provider.get_commit_statuses.return_value = Status([]) + # add this to satisfy create_or_update_commit_notification_from_notification_result + mock_repo_provider.post_comment.return_value = {"id": 9865} + mock_configuration._params["setup"] = {"codecov_dashboard_url": "test"} + current_yaml = { + "coverage": {"status": {"patch": True, "project": True}}, + "comment": {"layout": "condensed_header"}, + "slack_app": False, + "github_checks": {"annotations": False}, + } + commit = sample_comparison.head.commit + report = Report() + first_deleted_file = ReportFile("file_1.go") + first_deleted_file.append(1, ReportLine.create(coverage=0, sessions=[])) + first_deleted_file.append(2, ReportLine.create(coverage=0, sessions=[])) + first_deleted_file.append(3, ReportLine.create(coverage=0, sessions=[])) + first_deleted_file.append(5, ReportLine.create(coverage=0, sessions=[])) + report.append(first_deleted_file) + sample_comparison.head.report = report + sample_comparison.project_coverage_base.report = report + mock_repo_provider.create_check_run.return_value = 2234563 + mock_repo_provider.update_check_run.return_value = "success" + patch_check = PatchChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "target": "70%", + "paths": ["pathone"], + "layout": "reach, diff, flags, files, footer", + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + proj_check = ProjectChecksNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "target": "70%", + "paths": ["pathone"], + "layout": "reach, diff, flags, files, footer", + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + comment = CommentNotifier( + repository=sample_comparison.head.commit.repository, + title="title", + notifier_yaml_settings={ + "target": "70%", + "paths": ["pathone"], + "layout": "reach, diff, flags, files, footer", + }, + notifier_site_settings=True, + current_yaml=UserYaml({}), + repository_service=mock_repo_provider, + ) + mocker.patch.object( + NotificationService, + "get_notifiers_instances", + return_value=[ + patch_check, + proj_check, + comment, + ], + ) + + service = NotificationService( + commit.repository, current_yaml, mock_repo_provider + ) + instances = list(service.get_notifiers_instances()) + names = sorted([instance.name for instance in instances]) + types = sorted(instance.notification_type.value for instance in instances) + assert names == ["checks-patch", "checks-project", "comment"] + assert types == ["checks_patch", "checks_project", "comment"] + + checks_patch_result = { + "state": "failure", + "output": { + "title": "66.67% of diff hit (target 70.00%)", + "summary": f"[View this Pull Request on Codecov](test.example.br/gh/test_notify_individual_checks_patch_and_project_notifier_included_helper_text/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\n66.67% of diff hit (target 70.00%)", + "annotations": [], + }, + "included_helper_text": { + HelperTextKey.CUSTOM_TARGET_PATCH: HelperTextTemplate.CUSTOM_TARGET.format( + context="patch", + notification_type="check", + point_of_comparison="patch", + coverage=66.67, + target="70.00", + ) + }, + } + # forcing this outcome from patch check notifier to test how that affects comment notifier + mocker.patch( + "services.notification.notifiers.checks.patch.PatchChecksNotifier.build_payload", + return_value=checks_patch_result, + ) + + checks_proj_result = { + "state": "failure", + "output": { + "title": "50.00% (target 70.00%)", + "summary": f"[View this Pull Request on Codecov](test/gh/test_notify_individual_checks_patch_and_project_notifier_included_helper_text/{sample_comparison.head.commit.repository.name}/pull/{sample_comparison.pull.pullid}?dropdown=coverage&src=pr&el=h1)\n\nNo coverage information found on head", + "annotations": [], + }, + "included_helper_text": { + HelperTextKey.CUSTOM_TARGET_PROJECT: HelperTextTemplate.CUSTOM_TARGET.format( + context="project", + notification_type="check", + point_of_comparison="head", + coverage="50.00", + target="70.00", + ) + }, + } + # forcing this outcome from project check notifier to test how that affects comment notifier + mocker.patch( + "services.notification.notifiers.checks.project.ProjectChecksNotifier.build_payload", + return_value=checks_proj_result, + ) + + mocker.patch( + "services.comparison.ComparisonProxy.get_behind_by", + return_value=None, + ) + mock_changes = [ + Change( + path="modified.py", + new=False, + deleted=False, + in_diff=True, + old_path=None, + totals=ReportTotals( + files=0, + lines=0, + hits=-3, + misses=2, + partials=0, + coverage=-35.714290000000005, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ), + ] + mocker.patch( + "services.comparison.ComparisonProxy.get_changes", return_value=mock_changes + ) + + # this gets the patched results from PatchChecksNotifier and ProjectChecksNotifier, with included_helper_text + # CommentNotifier is called next, and should have the included_helper_text in the payload + res = service.notify(sample_comparison) + + assert len(res) == 3 + for r in res: + if r["notifier"] == "checks-patch": + assert ( + r["result"].data_sent["included_helper_text"] + == checks_patch_result["included_helper_text"] + ) + + if r["notifier"] == "checks-project": + assert ( + r["result"].data_sent["included_helper_text"] + == checks_proj_result["included_helper_text"] + ) + + if r["notifier"] == "comment": + assert ( + ":x: " + + checks_patch_result["included_helper_text"][ + HelperTextKey.CUSTOM_TARGET_PATCH + ] + and ":x: " + + checks_proj_result["included_helper_text"][ + HelperTextKey.CUSTOM_TARGET_PROJECT + ] + in r["result"].data_sent["message"] + ) + + @pytest.mark.django_db + def test_notify_individual_notifier_timeout_notification_created( + self, mocker, dbsession, sample_comparison + ): + current_yaml = {} + commit = sample_comparison.head.commit + notifier = mocker.MagicMock( + title="fake_notifier", + notify=mock.Mock(), + notification_type=Notification.comment, + decoration_type=Decoration.standard, + ) + notifier.notify.side_effect = AsyncioTimeoutError + notifications_service = NotificationService( + commit.repository, current_yaml, None + ) + res = notifications_service.notify_individual_notifier( + notifier, sample_comparison + ) + assert res == (notifier, None) + + dbsession.flush() + pull = sample_comparison.enriched_pull.database_pull + pull_commit_notifications = pull.get_head_commit_notifications() + assert len(pull_commit_notifications) == 1 + + pull_commit_notification = pull_commit_notifications[0] + assert pull_commit_notification is not None + assert pull_commit_notification.notification_type == notifier.notification_type + assert pull_commit_notification.decoration_type == notifier.decoration_type + assert pull_commit_notification.state == NotificationState.error + + @pytest.mark.django_db + def test_notify_individual_notifier_notification_created_then_updated( + self, mocker, dbsession, sample_comparison + ): + current_yaml = {} + commit = sample_comparison.head.commit + notifier = mocker.MagicMock( + title="fake_notifier", + notify=mock.Mock(), + notification_type=Notification.comment, + decoration_type=Decoration.standard, + ) + # first attempt not successful + notifier.notify.side_effect = AsyncioTimeoutError + notifications_service = NotificationService( + commit.repository, current_yaml, None + ) + res = notifications_service.notify_individual_notifier( + notifier, sample_comparison + ) + assert res == (notifier, None) + + dbsession.flush() + pull = sample_comparison.enriched_pull.database_pull + pull_commit_notifications = pull.get_head_commit_notifications() + assert len(pull_commit_notifications) == 1 + + pull_commit_notification = pull_commit_notifications[0] + assert pull_commit_notification is not None + assert pull_commit_notification.decoration_type == notifier.decoration_type + assert pull_commit_notification.state == NotificationState.error + + # second attempt successful + notifier.notify.side_effect = [ + NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation="", + data_sent={"some": "data"}, + ) + ] + res = notifications_service.notify_individual_notifier( + notifier, sample_comparison + ) + dbsession.commit() + assert pull_commit_notification.state == NotificationState.success + + @pytest.mark.django_db + def test_notify_individual_notifier_cancellation(self, mocker, sample_comparison): + current_yaml = {} + commit = sample_comparison.head.commit + notifier = mocker.MagicMock( + title="fake_notifier", + notify=mock.Mock(), + notification_type=Notification.comment, + decoration_type=Decoration.standard, + ) + notifier.notify.side_effect = CancelledError() + notifications_service = NotificationService( + commit.repository, current_yaml, None + ) + with pytest.raises(CancelledError): + notifications_service.notify_individual_notifier( + notifier, sample_comparison + ) + + @pytest.mark.django_db + def test_notify_timeout_exception(self, mocker, dbsession, sample_comparison): + current_yaml = {} + commit = sample_comparison.head.commit + good_notifier = mocker.MagicMock( + is_enabled=mocker.MagicMock(return_value=True), + notify=mock.Mock(), + title="good_notifier", + notification_type=Notification.comment, + decoration_type=Decoration.standard, + ) + no_attempt_notifier = mocker.MagicMock( + is_enabled=mocker.MagicMock(return_value=True), + notify=mock.Mock(), + title="no_attempt_notifier", + notification_type=Notification.status_project, + decoration_type=Decoration.standard, + ) + bad_notifier = mocker.MagicMock( + is_enabled=mocker.MagicMock(return_value=True), + notify=mock.Mock(), + title="bad_notifier", + notification_type=Notification.status_patch, + decoration_type=Decoration.standard, + ) + disabled_notifier = mocker.MagicMock( + is_enabled=mocker.MagicMock(return_value=False), + title="disabled_notifier", + notification_type=Notification.status_changes, + decoration_type=Decoration.standard, + ) + good_notifier.notify.return_value = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation="", + data_sent={"some": "data"}, + ) + no_attempt_notifier.notify.return_value = NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="no_need_to_send", + data_sent=None, + ) + good_notifier.name = "good_name" + bad_notifier.name = "bad_name" + disabled_notifier.name = "disabled_notifier_name" + bad_notifier.notify.side_effect = SoftTimeLimitExceeded() + mocker.patch.object( + NotificationService, + "get_notifiers_instances", + return_value=[ + bad_notifier, + good_notifier, + disabled_notifier, + no_attempt_notifier, + ], + ) + notifications_service = NotificationService( + commit.repository, current_yaml, None + ) + with pytest.raises(SoftTimeLimitExceeded): + notifications_service.notify(sample_comparison) + + dbsession.flush() + pull_commit_notifications = sample_comparison.enriched_pull.database_pull.get_head_commit_notifications() + assert len(pull_commit_notifications) == 1 + for commit_notification in pull_commit_notifications: + assert commit_notification.state in ( + NotificationState.success, + NotificationState.error, + ) + assert commit_notification.decoration_type == Decoration.standard + assert commit_notification.notification_type in ( + Notification.comment, + Notification.status_patch, + ) + + @pytest.mark.django_db + def test_not_licensed_enterprise(self, mocker, dbsession, sample_comparison): + mocker.patch("services.notification.is_properly_licensed", return_value=False) + mock_notify_individual_notifier = mocker.patch.object( + NotificationService, "notify_individual_notifier" + ) + current_yaml = {} + commit = sample_comparison.head.commit + notifications_service = NotificationService( + commit.repository, current_yaml, None + ) + expected_result = [] + res = notifications_service.notify(sample_comparison) + assert expected_result == res + assert not mock_notify_individual_notifier.called + + @pytest.mark.django_db + def test_get_statuses(self, mocker, dbsession, sample_comparison): + current_yaml = { + "coverage": {"status": {"project": True, "patch": True, "changes": True}}, + "flags": {"banana": {"carryforward": False}}, + "flag_management": { + "default_rules": {"carryforward": False}, + "individual_flags": [ + { + "name": "strawberry", + "carryforward": True, + "statuses": [{"name_prefix": "haha", "type": "patch"}], + } + ], + }, + } + commit = sample_comparison.head.commit + notifications_service = NotificationService( + commit.repository, UserYaml(current_yaml), None + ) + expected_result = [ + ("project", "default", {}), + ("patch", "default", {}), + ("changes", "default", {}), + ( + "patch", + "hahastrawberry", + {"flags": ["strawberry"], "name_prefix": "haha", "type": "patch"}, + ), + ] + res = list(notifications_service.get_statuses(["unit", "banana", "strawberry"])) + assert expected_result == res + + @pytest.mark.django_db + def test_get_component_statuses(self, mocker, dbsession, sample_comparison): + current_yaml = { + "component_management": { + "default_rules": { + "paths": [r"src/important/.*\.cpp"], + "flag_regexes": [r"critical.*"], + "statuses": [ + {"name_prefix": "important/", "type": "project"}, + { + "name_prefix": "legacy/", + "type": "project", + "enabled": False, + }, # this won't be in the results because it's not enabled + ], + }, + "individual_components": [ + { + "component_id": "my-special-component", + "flag_regexes": [r"special.*"], + "statuses": [ + {"name_prefix": "special/", "type": "patch"}, + {"name_prefix": "legacy/", "type": "project"}, + ], + }, + { + "component_id": "inner-app", + "paths": [r"src/inner_app/.*"], + "statuses": [{"type": "patch"}], + }, + {"component_id": "from_default"}, + ], + } + } + commit = sample_comparison.head.commit + notifications_service = NotificationService( + commit.repository, UserYaml(current_yaml), None + ) + expected_result = [ + ( + "patch", + "special/my-special-component", + { + "flags": ["special_flag"], + "paths": [r"src/important/.*\.cpp"], + "type": "patch", + "name_prefix": "special/", + }, + ), + ( + "project", + "legacy/my-special-component", + { + "flags": ["special_flag"], + "paths": [r"src/important/.*\.cpp"], + "type": "project", + "name_prefix": "legacy/", + }, + ), + ( + "patch", + "inner-app", + { + "flags": ["critical_files"], + "paths": [r"src/inner_app/.*"], + "type": "patch", + }, + ), + ( + "project", + "important/from_default", + { + "flags": ["critical_files"], + "name_prefix": "important/", + "type": "project", + "paths": [r"src/important/.*\.cpp"], + }, + ), + ] + res = list( + notifications_service.get_statuses( + ["special_flag", "critical_files", "banana"] + ) + ) + assert expected_result == res diff --git a/apps/worker/services/owner.py b/apps/worker/services/owner.py new file mode 100644 index 0000000000..ac7e0771cc --- /dev/null +++ b/apps/worker/services/owner.py @@ -0,0 +1,56 @@ +import logging + +import shared.torngit as torngit +from shared.bots import get_adapter_auth_information +from shared.config import get_config, get_verify_ssl +from shared.django_apps.codecov_auth.models import Service +from shared.typings.torngit import ( + OwnerInfo, + TorngitInstanceData, +) + +from helpers.token_refresh import get_token_refresh_callback + +log = logging.getLogger(__name__) + + +def get_owner_provider_service( + owner, *, ignore_installation=False, additional_data=None +): + _timeouts = [ + get_config("setup", "http", "timeouts", "connect", default=15), + get_config("setup", "http", "timeouts", "receive", default=30), + ] + service = Service(owner.service) + adapter_auth_info = get_adapter_auth_information( + owner, ignore_installations=ignore_installation + ) + if additional_data is None: + additional_data = {} + data = TorngitInstanceData( + owner=OwnerInfo( + service_id=owner.service_id, ownerid=owner.ownerid, username=owner.username + ), + installation=adapter_auth_info["selected_installation_info"], + fallback_installations=adapter_auth_info["fallback_installations"], + additional_data=additional_data, + ) + + adapter_params = dict( + token=adapter_auth_info["token"], + verify_ssl=get_verify_ssl(service.value), + timeouts=_timeouts, + oauth_consumer_token=dict( + key=get_config(service, "client_id"), + secret=get_config(service, "client_secret"), + ), + # if using integration we will use the integration token + # not the owner's token + on_token_refresh=get_token_refresh_callback(adapter_auth_info["token_owner"]), + **data, + ) + return _get_owner_provider_service_instance(service, **adapter_params) + + +def _get_owner_provider_service_instance(service_name, **adapter_params): + return torngit.get(service_name, **adapter_params) diff --git a/apps/worker/services/path_fixer/__init__.py b/apps/worker/services/path_fixer/__init__.py new file mode 100644 index 0000000000..baf95712b3 --- /dev/null +++ b/apps/worker/services/path_fixer/__init__.py @@ -0,0 +1,176 @@ +import logging +import os.path +from pathlib import PurePosixPath, PureWindowsPath +from typing import Sequence + +import sentry_sdk +from shared.yaml import UserYaml + +from helpers.pathmap import Tree +from services.path_fixer.fixpaths import remove_known_bad_paths +from services.path_fixer.user_path_fixes import UserPathFixes +from services.path_fixer.user_path_includes import UserPathIncludes +from services.yaml import read_yaml_field + +log = logging.getLogger(__name__) + + +def invert_pattern(string: str) -> str: + if string.startswith("!"): + return string[1:] + else: + return "!%s" % string + + +class PathFixer(object): + """ + Applies default path fixes and any fixes specified in the codecov yaml file to resolve file paths in coverage reports. + Also applies any "ignore" and "paths" yaml fields to determine which files to include in the report. + """ + + tree: Tree | None + + @classmethod + @sentry_sdk.trace + def init_from_user_yaml( + cls, + commit_yaml: UserYaml, + toc: list[str], + flags: list[str] | None = None, + extra_fixes: list[str] | None = None, + ): + """ + :param commit_yaml: Codecov yaml file in effect for this commit. + :param toc: List of files prepended to the uploaded report. Not all report formats provide this. + :param flags: Coverage flags specified by the user, if any. + """ + ignore = read_yaml_field(commit_yaml, ("ignore",)) or [] + path_patterns = [invert_pattern(p) for p in ignore] + + for flag in flags or []: + flag_configuration = commit_yaml.get_flag_configuration(flag) or {} + path_patterns.extend( + invert_pattern(p) for p in flag_configuration.get("ignore") or [] + ) + path_patterns.extend(flag_configuration.get("paths") or []) + + disable_default_path_fixes = read_yaml_field( + commit_yaml, ("codecov", "disable_default_path_fixes") + ) + yaml_fixes = read_yaml_field(commit_yaml, ("fixes",)) or [] + + if extra_fixes: + yaml_fixes.extend(extra_fixes) + + return cls( + yaml_fixes=yaml_fixes, + path_patterns=path_patterns, + toc=toc, + should_disable_default_pathfixes=disable_default_path_fixes, + ) + + def __init__( + self, + yaml_fixes: list[str], + path_patterns: list[str], + toc: list[str], + should_disable_default_pathfixes=False, + ) -> None: + self.toc = toc or [] + + self.yaml_fixes = yaml_fixes or [] + self.path_patterns = set(path_patterns) or set([]) + self.should_disable_default_pathfixes = should_disable_default_pathfixes + + self.custom_fixes = UserPathFixes(self.yaml_fixes) + self.path_matcher = UserPathIncludes(self.path_patterns) + + if self.toc and not should_disable_default_pathfixes: + self.tree = Tree(self.toc) + else: + self.tree = None + + def clean_path(self, path: str | None) -> str | None: + if not path: + return None + path = os.path.relpath(path.replace("\\", "/").lstrip("./").lstrip("../")) + if self.yaml_fixes: + # applies pre + path = self.custom_fixes(path, False) + if self.tree: + path = self.tree.resolve_path(path, ancestors=1) + if not path: + return None + elif not self.toc: + path = remove_known_bad_paths("", path) + if self.yaml_fixes: + # applied pre and post + path = self.custom_fixes(path, True) + if not self.path_matcher(path): + # don't include the file if yaml specified paths to include/ignore and it's not in the list to include + return None + return path + + def __call__(self, path: str, bases_to_try=None) -> str | None: + return self.clean_path(path) + + def get_relative_path_aware_pathfixer(self, base_path) -> "BasePathAwarePathFixer": + return BasePathAwarePathFixer(original_path_fixer=self, base_path=base_path) + + +class BasePathAwarePathFixer(PathFixer): + def __init__(self, original_path_fixer, base_path) -> None: + self._resolved_paths: dict[tuple[str, Sequence[str]], str | None] = {} + self.original_path_fixer = original_path_fixer + + # base_path argument is the file path after the "# path=" in the report containing report location, if provided. + # to get the base path we use, strip the coverage report from the path to get the base path + # e.g.: "path/to/coverage.xml" --> "path/to/" + + self.base_path = [] + + if base_path: + # We want to use a `PurePath`, but we have to handle both Windows + # and POSIX paths. The cleanest way to do that is: + # - start by assuming it's a Windows path + # - if it doesn't have a drive letter like C:\, convert to POSIX + pure_path = PureWindowsPath(base_path) + if not pure_path.drive: + pure_path = PurePosixPath(pure_path.as_posix()) + + self.base_path = [pure_path.parent] + + def _try_fix_path(self, path: str, bases_to_try: Sequence[str]) -> str | None: + original_path_fixer_result = self.original_path_fixer(path) + if ( + original_path_fixer_result is not None + or (not self.base_path and not bases_to_try) + or not self.original_path_fixer.toc + ): + return original_path_fixer_result + + if not os.path.isabs(path): + all_base_paths_to_try = ( + self.base_path + list(bases_to_try) if bases_to_try else self.base_path + ) + + for base_path in all_base_paths_to_try: + adjusted_path = os.path.join(base_path, path) + base_path_aware_result = self.original_path_fixer(adjusted_path) + if base_path_aware_result is not None: + return base_path_aware_result + + return original_path_fixer_result + + def __call__( + self, path: str, bases_to_try: Sequence[str] | None = None + ) -> str | None: + if not path: + return None + bases_to_try = bases_to_try or tuple() + key = (path, bases_to_try) + + if key not in self._resolved_paths: + self._resolved_paths[key] = self._try_fix_path(path, bases_to_try) + + return self._resolved_paths[key] diff --git a/apps/worker/services/path_fixer/fixpaths.py b/apps/worker/services/path_fixer/fixpaths.py new file mode 100644 index 0000000000..b30f4ac8ee --- /dev/null +++ b/apps/worker/services/path_fixer/fixpaths.py @@ -0,0 +1,120 @@ +import logging +import re +import string + +log = logging.getLogger(__name__) + +remove_known_bad_paths = re.compile( + r"^(\.*\/)*(%s)?" + % "|".join( + ( + r"((home|Users)/travis/build/[^\/\n]+/[^\/\n]+/)", + r"((home|Users)/jenkins/jobs/[^\/\n]+/workspace/)", + r"(Users/distiller/[^\/\n]+/)", + r"(home/[^\/\n]+/src/([^\/\n]+/){3})", # home/rof/src/github.com|bitbucket.org/owner/repo/ + r"((home|Users)/[^\/\n]+/workspace/[^\/\n]+/[^\/\n]+/)", # /Users/user/workspace/owner/repo + r"(.*/jenkins/workspace/[^\/\n]+/)", + r"((.+/src/)?github\.com/[^\/\n]+/[^\/\n]+/)", + r"(\w:/Repos/[^\/\n]+/[^\/\n]+/)", + r"([\w:/]+projects/[^\/\n]+/)", + r"(\w:/_build/GitHub/[^\/\n]+/)", + r"(build/lib\.[^\/\n]+/)", + r"(home/circleci/code/)", + r"(home/circleci/repo/)", + r"(vendor/src/.*)", + r"(pipeline/source/)", + r"(var/snap-ci/repo/)", + r"(home/ubuntu/[^\/\n]+/)", + r"(.*/site-packages/[^\/\n]+\.egg/)", # python3+ + r"(.*/site-packages/)", + r"(usr/local/lib/[^\/\n]+/dist-packages/)", + r"(.*/slather/spec/fixtures/[^\n]*)", + r"(.*/target/generated-sources/[^\n]*)", + r"(.*/\.phpenv/.*)", + r"(.*/Debug-iphonesimulator/ReactiveCocoa\.build/DerivedSources/RA.*)", + r"(usr/include/.*)", + r"(.*/handlebars\.js/dist/.*)", + r"(node_modules/.*)", + r"(bower_components/.*)", + r"(.*/lib/clang/.*)", + r"(.*[\<\>].*)", + r"(\w\:\/)", # E:/ C:/ + r"(.*/mac-coverage/build/src/.*)", + r"(opt/.*/dist-packages/.*)", # opt/ros/indigo/lib/python2.7/dist-packages/... + r"(.*/iPhoneSimulator.platform/Developer/SDKs/.*)", + r"(Applications/Xcode\.app/Contents/Developer/Toolchains/.*)", + r"((.*/)?\.?v?(irtual)?\.?envs?(-[^\/\n]+)?/.*/[^\/\n]+\.py$)", + r"(Users/[^\/\n]+/Projects/.*/Pods/.*)", + r"(Users/[^\/\n]+/Projects/[^\/\n]+/)", + r"(home/[^\/\n]+/[^\/\n]+/[^\/\n]+/)", # /home/:user/:owner/:repo/ + ) + ), + re.I | re.M, +).sub + + +def unquote_git_path(path: str) -> str: + """ + Undo git-style Unicode armor for paths. + """ + + # This armoring is documented under git-config, `core.quotePath`, e.g. at + # https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath + # There is no builtin codec for this, so we'll do it ourselves. + + # We'll use a string-builder technique. We'll put each byte into a list and + # then turn the list into a string. + rv = [] + i = 0 + while i < len(path): + if path[i] == "\\": + if path[i + 1] == "r": + # The examples all match "users/.../Icon\r" + # https://apple.stackexchange.com/questions/31867/what-is-icon-r-file-and-how-do-i-delete-them + i += 2 + elif path[i + 1] in string.octdigits: + # Decode an escaped byte; the next three characters are octets. + rv.append(int(path[i + 1 : i + 4], 8)) + i += 4 + else: + rv.append(ord(path[i + 1])) + i += 2 + else: + # Just copy the codepoint. + rv.append(ord(path[i])) + i += 1 + # Finally, decode with UTF-8. + return bytes(rv).decode("utf-8") + + +def clean_toc(toc: str) -> list[str]: + """ + Split a newline-delimited table of contents into a list of paths. + + Each path will be cleaned up slightly. + """ + + rv = [] + for path in toc.strip().split("\n"): + # Detect and undo git's Unicode armoring. + if path.startswith('"') and path.endswith('"'): + path = unquote_git_path(path[1:-1]) + + # Unescape escaped spaces. + path = path.replace("\\ ", " ") + # Windows: Fix backslashes. + path = path.replace("\\", "/") + # Fix relative paths which start in the current directory. + if path.startswith("./"): + path = path[2:] + + # Unconditionally remove delombok'd Java source code. + # This can happen when folks upload code which uses the Lombok Java library. + # This code would confuse coverage, duplicating real code, so we discard it. ~ C. + if "/target/delombok/" in path: + continue + + # This path is good; save it. + rv.append(path) + + return rv diff --git a/apps/worker/services/path_fixer/match.py b/apps/worker/services/path_fixer/match.py new file mode 100644 index 0000000000..3518185100 --- /dev/null +++ b/apps/worker/services/path_fixer/match.py @@ -0,0 +1,8 @@ +import re + + +def regexp_match_one(regexp_patterns: list[re.Pattern], path: str) -> bool: + for pattern in regexp_patterns: + if pattern.match(path): + return True + return False diff --git a/apps/worker/services/path_fixer/tests/unit/test_fixpaths.py b/apps/worker/services/path_fixer/tests/unit/test_fixpaths.py new file mode 100644 index 0000000000..8b129c6dde --- /dev/null +++ b/apps/worker/services/path_fixer/tests/unit/test_fixpaths.py @@ -0,0 +1,56 @@ +import os + +import pytest + +from services.path_fixer import fixpaths +from test_utils.base import BaseTestCase + +# Hand-written TOCs. +paths = [ + ("./a\\b", ["a/b"]), + ("./a\n./b", ["a", "b"]), + ("path/target/delombok/a\n./b", ["b"]), + ("comma,txt\nb", ["comma,txt", "b"]), + ('a\n"\\360\\237\\215\\255.txt"\nb', ["a", "🍭.txt", "b"]), +] + +# Hand-written filenames. +unquoted_files = { + "boring.txt": "boring.txt", + "back\\\\slash.txt": "back\\slash.txt", + "\\360\\237\\215\\255.txt": "🍭.txt", + "users/crovercraft/bootstrap/Icon\\r": "users/crovercraft/bootstrap/Icon", + 'test/fixture/vcr_cassettes/clickhouse/get_breakdown_values_escaped_\\".json': 'test/fixture/vcr_cassettes/clickhouse/get_breakdown_values_escaped_".json', +} + + +class TestFixpaths(BaseTestCase): + @pytest.mark.parametrize("toc, result", paths) + def test_clean_toc(self, toc, result): + assert fixpaths.clean_toc(toc) == result + + def test_clean_toc_with_space(self): + assert fixpaths.clean_toc("a\\ b") == ["a b"] + + @pytest.mark.parametrize("path, result", list(unquoted_files.items())) + def test_unquote_git_path(self, path, result): + assert fixpaths.unquote_git_path(path) == result + + def test_some_real_git_paths(self): + prefix = "services/path_fixer/tests/testdir" + filenames = [ + "café.txt", + "comma,txt", + "🍭.txt", + 'fixture/get_breakdown_values_escaped_".json', + ] + joined = [os.path.join(prefix, filename) for filename in filenames] + toc = """"services/path_fixer/tests/testdir/caf\\303\\251.txt" +services/path_fixer/tests/testdir/comma,txt +"services/path_fixer/tests/testdir/\\360\\237\\215\\255.txt" +"services/path_fixer/tests/testdir/fixture/get_breakdown_values_escaped_\\".json" +""" + cleaned = fixpaths.clean_toc(toc) + joined.sort() + cleaned.sort() + assert joined == cleaned diff --git a/apps/worker/services/path_fixer/tests/unit/test_path_fixer.py b/apps/worker/services/path_fixer/tests/unit/test_path_fixer.py new file mode 100644 index 0000000000..085a2cfb48 --- /dev/null +++ b/apps/worker/services/path_fixer/tests/unit/test_path_fixer.py @@ -0,0 +1,179 @@ +from pathlib import PurePosixPath, PureWindowsPath + +from shared.yaml import UserYaml + +from services.path_fixer import PathFixer, invert_pattern +from test_utils.base import BaseTestCase + + +class TestPathFixerHelpers(BaseTestCase): + def test_invert_pattern(self): + assert invert_pattern("aaaa") == "!aaaa" + assert invert_pattern("!aaaa") == "aaaa" + + +class TestPathFixer(BaseTestCase): + def test_path_fixer_empty(self): + pf = PathFixer([], [], []) + assert pf("simple/path/to/something.py") == "simple/path/to/something.py" + assert pf("") is None + assert pf("bower_components/sample.js") == "" + + def test_path_fixer_with_toc(self): + pf = PathFixer([], [], ["file_1.py", "folder/file_2.py"]) + assert pf("fafafa/file_2.py") is None + assert pf("folder/file_2.py") == "folder/file_2.py" + assert pf("file_1.py") == "file_1.py" + assert pf("bad_path.py") is None + assert pf("") is None + + def test_path_fixer_one_exclude_path_pattern(self): + pf = PathFixer([], ["!simple/path"], []) + assert pf("notsimple/path/to/something.py") == "notsimple/path/to/something.py" + assert ( + pf("simple/notapath/to/something.py") == "simple/notapath/to/something.py" + ) + assert pf("simple/path/to/something.py") is None + + def test_path_fixer_one_custom_pathfix(self): + pf = PathFixer(["before/::after/"], [], []) + assert pf("before/path/to/something.py") == "after/path/to/something.py" + assert pf("after/path/to/something.py") == "after/path/to/something.py" + assert ( + pf("after/before/path/to/something.py") + == "after/before/path/to/something.py" + ) + assert ( + pf("simple/notapath/to/something.py") == "simple/notapath/to/something.py" + ) + + def test_init_from_user_yaml(self): + commit_yaml = { + "fixes": [r"(?s:before/tests\-[^\/]+)::after/"], + "ignore": ["complex/path"], + "flags": { + "flagone": {"paths": ["!simple/notapath.*"]}, + "flagtwo": {"paths": ["af"]}, + }, + } + toc = [] + flags = ["flagone"] + pf = PathFixer.init_from_user_yaml(UserYaml(commit_yaml), toc, flags) + assert pf("notsimple/path/to/something.py") == "notsimple/path/to/something.py" + assert pf("complex/path/to/something.py") is None + assert pf("before/tests-apples/test.js") == "after/test.js" + assert pf("after/path/to/something.py") == "after/path/to/something.py" + assert ( + pf("after/before/path/to/something.py") + == "after/before/path/to/something.py" + ) + assert pf("simple/notapath/to/something.py") is None + + def test_init_from_user_yaml_extra_fixes(self): + commit_yaml = { + "fixes": [r"(?s:before/tests\-[^\/]+)::after/"], + "ignore": ["complex/path"], + "flags": { + "flagone": {"paths": ["!simple/notapath.*"]}, + "flagtwo": {"paths": ["af"]}, + }, + } + toc = [] + flags = ["flagone"] + pf = PathFixer.init_from_user_yaml( + UserYaml(commit_yaml), toc, flags, extra_fixes=[r"notsimple::goal"] + ) + # This next path is expected to change with the extra fixes + assert pf("notsimple/path/to/something.py") == "goal/path/to/something.py" + assert pf("complex/path/to/something.py") is None + assert pf("before/tests-apples/test.js") == "after/test.js" + assert pf("after/path/to/something.py") == "after/path/to/something.py" + assert ( + pf("after/before/path/to/something.py") + == "after/before/path/to/something.py" + ) + assert pf("simple/notapath/to/something.py") is None + + +class TestBasePathAwarePathFixer(object): + def test_basepath_uses_main_result_if_not_none_when_disagreement(self): + commit_yaml = { + "fixes": [r"(?s:home/thiago)::root/"], + "ignore": ["complex/path"], + } + toc = ["path.c", "another/path.py", "root/another/path.py"] + flags = [] + pf = PathFixer.init_from_user_yaml(commit_yaml, toc, flags) + base_path = "/home/thiago/testing" + base_aware_pf = pf.get_relative_path_aware_pathfixer(base_path) + assert base_aware_pf("sample/path.c") == "path.c" + assert base_aware_pf("another/path.py") == "another/path.py" + assert base_aware_pf("/another/path.py") == "another/path.py" + + def test_basepath_uses_own_result_if_main_is_none(self): + toc = ["project/__init__.py", "tests/__init__.py", "tests/test_project.py"] + pf = PathFixer.init_from_user_yaml({}, toc, []) + base_path = "/home/travis/build/project/coverage.xml" + base_aware_pf = pf.get_relative_path_aware_pathfixer(base_path) + assert pf("__init__.py") is None + assert base_aware_pf("__init__.py") == "project/__init__.py" + + def test_basepath_uses_own_result_if_main_is_none_multuple_base_paths(self): + toc = ["project/__init__.py", "tests/__init__.py", "tests/test_project.py"] + pf = PathFixer.init_from_user_yaml({}, toc, []) + base_path = "/home/something/coverage.xml" + base_aware_pf = pf.get_relative_path_aware_pathfixer(base_path) + assert pf("__init__.py") is None + assert base_aware_pf("__init__.py") is None + assert ( + base_aware_pf("__init__.py", bases_to_try=("/home/travis/build/project",)) + == "project/__init__.py" + ) + + def test_basepath_does_not_resolve_empty_paths(self): + toc = ["project/__init__.py", "tests/__init__.py", "tests/test_project.py"] + pf = PathFixer.init_from_user_yaml({}, toc, []) + coverage_file = "/some/coverage.xml" + base_aware_pf = pf.get_relative_path_aware_pathfixer(coverage_file) + + assert base_aware_pf("") is None + + def test_basepath_with_win_and_posix_paths(self): + toc = ["project/__init__.py", "tests/__init__.py", "tests/test_project.py"] + pf = PathFixer.init_from_user_yaml({}, toc, []) + + posix_coverage_file_path = "/posix_base_path/coverage.xml" + posix_base_aware_pf = pf.get_relative_path_aware_pathfixer( + posix_coverage_file_path + ) + assert posix_base_aware_pf.base_path == [PurePosixPath("/posix_base_path")] + + windows_coverage_file_path = "C:\\windows_base_path\\coverage.xml" + windows_base_aware_pf = pf.get_relative_path_aware_pathfixer( + windows_coverage_file_path + ) + assert windows_base_aware_pf.base_path == [ + PureWindowsPath("C:\\windows_base_path") + ] + + +def test_ambiguous_paths(): + toc = [ + "foobar/bar/baz.py", + "barfoo/bar/baz.py", + ] + base_path = "/home/runner/work/owner/repo/foobar/build/coverage/coverage.xml" + # ~~~~~~ + bases_to_try = ("/app",) + # The problem here is that the given `file_name` is ambiguous, and neither the + # `base_path` nor the `bases_to_try` is helping us narrow this down. + # The `base_path` does include one of the relevant parent directories, + # but the paths within the coverage file are not relative to *that* file, + # and the `bases_to_try` seem to be completely unrelated. + file_name = "bar/baz.py" + + pf = PathFixer.init_from_user_yaml({}, toc, []) + base_aware_pf = pf.get_relative_path_aware_pathfixer(base_path) + + assert pf(file_name) is None + assert base_aware_pf(file_name, bases_to_try=bases_to_try) is None diff --git a/apps/worker/services/path_fixer/tests/unit/test_user_path_fixes.py b/apps/worker/services/path_fixer/tests/unit/test_user_path_fixes.py new file mode 100644 index 0000000000..bba6fb8671 --- /dev/null +++ b/apps/worker/services/path_fixer/tests/unit/test_user_path_fixes.py @@ -0,0 +1,53 @@ +from services.path_fixer.user_path_fixes import UserPathFixes +from test_utils.base import BaseTestCase + + +class TestUserPathFixes(BaseTestCase): + def test_user_path_fixes_empty(self): + yaml_fixes = [] + upf = UserPathFixes(yaml_fixes) + assert upf("simple/path.c") == "simple/path.c" + + def test_user_path_fixes_add_prefix_only(self): + yaml_fixes = ["::added_prefix"] + upf = UserPathFixes(yaml_fixes) + assert upf("simple/path.c") == "added_prefix/simple/path.c" + assert ( + upf("added_prefix/second_path.java") + == "added_prefix/added_prefix/second_path.java" + ) + + def test_user_path_fixes_remove_prefix_only(self): + yaml_fixes = ["prefix_to_remove::"] + upf = UserPathFixes(yaml_fixes) + assert upf("simple/path.c") == "simple/path.c" + assert upf("added_prefix/second_path.java") == "added_prefix/second_path.java" + assert upf("prefix_to_remove/third_path.py") == "third_path.py" + assert ( + upf("thisisnot/prefix_to_remove/third_path.py") + == "thisisnot/prefix_to_remove/third_path.py" + ) + + def test_user_path_fixes_remove_add(self): + yaml_fixes = ["prefix_to_remove::add"] + upf = UserPathFixes(yaml_fixes) + assert upf("simple/path.c") == "simple/path.c" + assert upf("added_prefix/second_path.java") == "added_prefix/second_path.java" + assert upf("prefix_to_remove/third_path.py") == "add/third_path.py" + assert ( + upf("thisisnot/prefix_to_remove/third_path.py") + == "thisisnot/prefix_to_remove/third_path.py" + ) + + def test_user_path_fixes_remove_add_with_regex(self): + yaml_fixes = [r"(?s:prefix_to_remove/test\-[^\/]+)::add"] + upf = UserPathFixes(yaml_fixes) + assert upf("simple/path.c") == "simple/path.c" + assert upf("added_prefix/second_path.java") == "added_prefix/second_path.java" + assert upf("prefix_to_remove/test-third_folder/path.py") == "add/path.py" + assert upf("prefix_to_remove/test-fourth_folder/path.py") == "add/path.py" + assert upf("prefix_to_remove/test-fourth_oops.py") == "add" + assert ( + upf("thisisnot/prefix_to_remove/test-third_path.py") + == "thisisnot/prefix_to_remove/test-third_path.py" + ) diff --git a/apps/worker/services/path_fixer/tests/unit/test_user_path_includes.py b/apps/worker/services/path_fixer/tests/unit/test_user_path_includes.py new file mode 100644 index 0000000000..23bb013b8e --- /dev/null +++ b/apps/worker/services/path_fixer/tests/unit/test_user_path_includes.py @@ -0,0 +1,30 @@ +from services.path_fixer.user_path_includes import UserPathIncludes +from test_utils.base import BaseTestCase + + +class TestUserPathIncludes(BaseTestCase): + def test_user_path_fixes_empty(self): + path_patterns = [] + upi = UserPathIncludes(path_patterns) + assert upi("sample/path/to/file.go") + assert upi("any/to/file.cpp") + + def test_user_path_fixes_star(self): + path_patterns = [".*", "whatever"] + upi = UserPathIncludes(path_patterns) + assert upi("sample/path/to/file.go") + assert upi("any/to/file.cpp") + + def test_user_path_regex(self): + path_patterns = ["sample/[^/]+/to/.*"] + upi = UserPathIncludes(path_patterns) + assert upi("sample/path/to/file.go") + assert upi("sample/something/to/haha.cpp") + assert not upi("any/to/file.cpp") + + def test_user_path_no_regex_elements(self): + path_patterns = ["normal/sample/path/file.py"] + upi = UserPathIncludes(path_patterns) + assert upi("normal/sample/path/file.py") + assert upi("normal/sample/path/file.pyc") + assert not upi("any/to/file.cpp") diff --git a/apps/worker/services/path_fixer/user_path_fixes.py b/apps/worker/services/path_fixer/user_path_fixes.py new file mode 100644 index 0000000000..8048282258 --- /dev/null +++ b/apps/worker/services/path_fixer/user_path_fixes.py @@ -0,0 +1,71 @@ +import re + +_star_to_glob = re.compile(r"(? str: + # [DEPRECATED] because handled by validators, but some data is cached in db + # a/**/b => a/.*/b + fix = fix.replace("**", r".*") + # a/*/b => a/[^\/\n]+/b + fix = _star_to_glob(r"[^\/\n]+", fix) + return fix.lstrip("/") + + +class UserPathFixes: + """ + This class contains the logic for apply path-fixes to the user, as described in + https://docs.codecov.io/docs/fixing-paths + + There is an initializer and one function: __call__. + The usage of it is: + + yaml_fixes = ['prefix_to_remove::', '::added_prefix', 'prefix_to_remove::add'] + upf = UserPathFixes(yaml_fixes) + fixed_path = upf('simple/path.c') + """ + + yaml_fixes: list[str] + + prefix: str + sub_regex: re.Pattern | None + sub_replacements: list[str] + + def __init__(self, yaml_fixes: list[str] | None): + yaml_fixes = yaml_fixes or [] + self.yaml_fixes = yaml_fixes + self.prefix = "" + self.sub_regex = None + self.sub_replacements = [] + + prefixes = set(f for f in yaml_fixes if f.startswith("::")) + custom_fixes = list(set(yaml_fixes) - prefixes) + + if prefixes: + self.prefix = "/".join(p[2:].rstrip("/") for p in prefixes) + + if custom_fixes: + self.sub_regex = re.compile( + r"^(%s)" + % ")|(".join(_fixpaths_regs(fix.split("::")[0]) for fix in custom_fixes) + ) + self.sub_replacements = [fix.split("::")[1] for fix in custom_fixes] + + def _replacement_fn(self, group: re.Match) -> str: + for group, replacement in zip(group.groups(), self.sub_replacements): + if group: + return replacement + assert False, "unreachable" # this is only ever called with one truthy group + + def __call__(self, path: str, should_add_prefixes=True) -> str: + if should_add_prefixes and self.prefix: + path = "%s/%s" % (self.prefix, path) + + if self.sub_regex: + path = self.sub_regex.sub( + self._replacement_fn, + path, + count=1, + ) + + return path.replace("//", "/").lstrip("/") diff --git a/apps/worker/services/path_fixer/user_path_includes.py b/apps/worker/services/path_fixer/user_path_includes.py new file mode 100644 index 0000000000..a7503e81a3 --- /dev/null +++ b/apps/worker/services/path_fixer/user_path_includes.py @@ -0,0 +1,74 @@ +import re + +from services.path_fixer.match import regexp_match_one + + +class UserPathIncludes: + """ + This class has one purpose: To determine whether a specific path should + be included in the report or not/ + + Its usage is: + + path_patterns = ['.*', 'whatever'] + upi = UserPathIncludes(path_patterns) + should_be_included = upi('sample/path/to/file.go') + """ + + path_patterns: set[str] + + includes: list[re.Pattern] + include_all: bool + + excludes: list[re.Pattern] + exclude_all: bool + + def __init__(self, path_patterns: set[str], assume=True): + self.path_patterns = path_patterns + self.includes = [] + self.include_all = False + self.excludes = [] + self.exclude_all = False + + if not self.path_patterns: + return + + includes = set(p for p in path_patterns if not p.startswith("!")) + excludes = set(path_patterns) - includes + + # create lists of pass/fails + if ".*" in path_patterns: + # match everything, just make sure it is not negative + self.include_all = True + self.includes = [] + elif assume and len(includes) == 0: + self.include_all = True + else: + self.include_all = False + self.includes = [re.compile(i) for i in includes] + + if "!.*" in self.path_patterns: + self.exclude_all = False + else: + self.excludes = [re.compile(e[1:]) for e in excludes] + + def __call__(self, value: str) -> bool: + if not self.path_patterns: + return True + if value: + if self.include_all: + # everything is included + if self.excludes: + # make sure it is not excluded + return not regexp_match_one(self.excludes, value) + else: + return True + # we have to match once + if regexp_match_one(self.includes, value) is True: + # make sure it's not excluded + if self.excludes and regexp_match_one(self.excludes, value): + return False + else: + return True + return False + return False diff --git a/apps/worker/services/processing/__init__.py b/apps/worker/services/processing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/processing/flake_processing.py b/apps/worker/services/processing/flake_processing.py new file mode 100644 index 0000000000..f6b6c3ee5a --- /dev/null +++ b/apps/worker/services/processing/flake_processing.py @@ -0,0 +1,196 @@ +import logging + +from django.db import transaction as django_transaction +from django.db.models import Q +from shared.django_apps.reports.models import ( + CommitReport, + DailyTestRollup, + Flake, + ReportSession, + TestInstance, +) + +from services.test_analytics.ta_metrics import process_flakes_summary + +log = logging.getLogger(__name__) + + +FLAKE_EXPIRY_COUNT = 30 + + +@process_flakes_summary.labels("old").time() +def process_flake_for_repo_commit( + repo_id: int, + commit_id: str, +): + with django_transaction.atomic(): + process_flake_in_transaction(repo_id, commit_id) + + log.info( + "Successfully processed flakes", + extra=dict(repoid=repo_id, commit=commit_id), + ) + + return {"successful": True} + + +def process_flake_in_transaction( + repo_id: int, + commit_id: str, +): + uploads = ReportSession.objects.filter( + report__report_type=CommitReport.ReportType.TEST_RESULTS.value, + report__commit__repository__repoid=repo_id, + report__commit__commitid=commit_id, + state__in=["processed"], + ) + + curr_flakes = fetch_curr_flakes(repo_id) + new_flakes: dict[str, Flake] = dict() + + rollups_to_update: list[DailyTestRollup] = [] + + flaky_tests = list(curr_flakes.keys()) + + for upload in uploads: + test_instances = get_test_instances(upload, flaky_tests) + for test_instance in test_instances: + if test_instance.outcome == TestInstance.Outcome.PASS.value: + flake = new_flakes.get(test_instance.test_id) or curr_flakes.get( + test_instance.test_id + ) + if flake is not None: + update_flake(flake, test_instance) + elif test_instance.outcome in ( + TestInstance.Outcome.FAILURE.value, + TestInstance.Outcome.ERROR.value, + ): + flake = new_flakes.get(test_instance.test_id) or curr_flakes.get( + test_instance.test_id + ) + if flake: + update_flake(flake, test_instance) + else: + flake, rollup = create_flake(test_instance, repo_id) + + new_flakes[test_instance.test_id] = flake + + if rollup: + rollups_to_update.append(rollup) + + if rollups_to_update: + DailyTestRollup.objects.bulk_update( + rollups_to_update, + ["flaky_fail_count"], + ) + + merge_flake_dict = {} + + if new_flakes: + flakes_to_merge = Flake.objects.bulk_create(new_flakes.values()) + merge_flake_dict: dict[str, Flake] = { + flake.test_id: flake for flake in flakes_to_merge + } + + Flake.objects.bulk_update( + curr_flakes.values(), + [ + "count", + "fail_count", + "recent_passes_count", + "end_date", + ], + ) + + curr_flakes = {**merge_flake_dict, **curr_flakes} + + new_flakes.clear() + + upload.state = "flake_processed" + upload.save() + + +def get_test_instances( + upload: ReportSession, + flaky_tests: list[str], +) -> list[TestInstance]: + # get test instances on this upload that either: + # - failed + # - passed but belong to an already flaky test + + upload_filter = Q(upload_id=upload.id) + test_failed_filter = Q(outcome=TestInstance.Outcome.ERROR.value) | Q( + outcome=TestInstance.Outcome.FAILURE.value + ) + test_passed_but_flaky_filter = Q(outcome=TestInstance.Outcome.PASS.value) & Q( + test_id__in=flaky_tests + ) + test_instances = list( + TestInstance.objects.filter( + upload_filter & (test_failed_filter | test_passed_but_flaky_filter) + ) + .select_related("test") + .all() + ) + return test_instances + + +def fetch_curr_flakes(repo_id: int) -> dict[str, Flake]: + flakes = Flake.objects.filter(repository_id=repo_id, end_date__isnull=True) + return {flake.test_id: flake for flake in flakes} + + +def create_flake( + test_instance: TestInstance, + repo_id: int, +) -> tuple[Flake, DailyTestRollup | None]: + # retroactively mark newly caught flake as flaky failure in its rollup + rollup = DailyTestRollup.objects.filter( + repoid=repo_id, + date=test_instance.created_at.date(), + branch=test_instance.branch, + test_id=test_instance.test_id, + ).first() + + if rollup: + rollup.flaky_fail_count += 1 + else: + log.warning( + "Could not find rollup when trying to update its flaky fail count", + extra=dict( + repoid=repo_id, + testid=test_instance.test_id, + branch=test_instance.branch, + date=test_instance.created_at.date(), + ), + ) + + f = Flake( + repository_id=repo_id, + test=test_instance.test, + reduced_error=None, + count=1, + fail_count=1, + start_date=test_instance.created_at, + recent_passes_count=0, + ) + + return f, rollup + + +def update_flake( + flake: Flake, + test_instance: TestInstance, +) -> None: + flake.count += 1 + + match test_instance.outcome: + case TestInstance.Outcome.PASS.value: + flake.recent_passes_count += 1 + if flake.recent_passes_count == FLAKE_EXPIRY_COUNT: + flake.end_date = test_instance.created_at + case TestInstance.Outcome.FAILURE.value | TestInstance.Outcome.ERROR.value: + flake.fail_count += 1 + flake.recent_passes_count = 0 + case _: + pass diff --git a/apps/worker/services/processing/intermediate.py b/apps/worker/services/processing/intermediate.py new file mode 100644 index 0000000000..cbcb404c01 --- /dev/null +++ b/apps/worker/services/processing/intermediate.py @@ -0,0 +1,92 @@ +import orjson +import sentry_sdk +import zstandard +from shared.helpers.redis import get_redis_connection +from shared.reports.resources import Report + +from .metrics import INTERMEDIATE_REPORT_SIZE +from .types import IntermediateReport + +REPORT_TTL = 24 * 60 * 60 + + +@sentry_sdk.trace +def load_intermediate_reports(upload_ids: list[int]) -> list[IntermediateReport]: + redis = get_redis_connection() + dctx = zstandard.ZstdDecompressor() + intermediate_reports: list[IntermediateReport] = [] + + for upload_id in upload_ids: + key = intermediate_report_key(upload_id) + report_dict: dict = redis.hgetall(key) + if not report_dict: + intermediate_reports.append(IntermediateReport(upload_id, Report())) + continue + + # NOTE: our redis client is configured to return `bytes` everywhere, + # so the dict keys are `bytes` as well. + chunks = dctx.decompress(report_dict[b"chunks"]).decode(errors="replace") + report_json = orjson.loads(dctx.decompress(report_dict[b"report_json"])) + + report = Report.from_chunks( + chunks=chunks, + files=report_json["files"], + sessions=report_json["sessions"], + totals=report_json.get("totals"), + ) + intermediate_reports.append(IntermediateReport(upload_id, report)) + + return intermediate_reports + + +@sentry_sdk.trace +def save_intermediate_report(upload_id: int, report: Report): + report_json, chunks, _totals = report.serialize(with_totals=False) + zstd_report_json, zstd_chunks = emit_size_metrics(report_json, chunks) + + report_key = intermediate_report_key(upload_id) + redis = get_redis_connection() + mapping = { + "report_json": zstd_report_json, + "chunks": zstd_chunks, + } + with redis.pipeline() as pipeline: + pipeline.hmset(report_key, mapping) + pipeline.expire(report_key, REPORT_TTL) + pipeline.execute() + return + + +@sentry_sdk.trace +def cleanup_intermediate_reports( + upload_ids: list[int], +): + keys = [intermediate_report_key(upload_id) for upload_id in upload_ids] + redis = get_redis_connection() + redis.delete(*keys) + return + + +def intermediate_report_key(upload_id: int): + return f"intermediate-report/{upload_id}" + + +def emit_size_metrics(report_json: bytes, chunks: bytes) -> tuple[bytes, bytes]: + INTERMEDIATE_REPORT_SIZE.labels(type="report_json", compression="none").observe( + len(report_json) + ) + INTERMEDIATE_REPORT_SIZE.labels(type="chunks", compression="none").observe( + len(chunks) + ) + + zstd_report_json = zstandard.compress(report_json) + zstd_chunks = zstandard.compress(chunks) + + INTERMEDIATE_REPORT_SIZE.labels(type="report_json", compression="zstd").observe( + len(zstd_report_json) + ) + INTERMEDIATE_REPORT_SIZE.labels(type="chunks", compression="zstd").observe( + len(zstd_chunks) + ) + + return zstd_report_json, zstd_chunks diff --git a/apps/worker/services/processing/merging.py b/apps/worker/services/processing/merging.py new file mode 100644 index 0000000000..0ade0521a1 --- /dev/null +++ b/apps/worker/services/processing/merging.py @@ -0,0 +1,181 @@ +import functools +import logging +from decimal import Decimal + +import sentry_sdk +from shared.reports.enums import UploadState +from shared.reports.resources import Report, ReportTotals +from shared.yaml import UserYaml +from sqlalchemy.dialects.postgresql import insert +from sqlalchemy.orm import Session as DbSession + +from database.models.reports import Upload, UploadError, UploadLevelTotals +from helpers.number import precise_round +from services.report import delete_uploads_by_sessionid +from services.report.raw_upload_processor import clear_carryforward_sessions +from services.yaml.reader import read_yaml_field + +from .types import IntermediateReport, MergeResult, ProcessingResult + +log = logging.getLogger(__name__) + + +@sentry_sdk.trace +def merge_reports( + commit_yaml: UserYaml, + master_report: Report, + intermediate_reports: list[IntermediateReport], +) -> tuple[Report, MergeResult]: + session_mapping: dict[int, int] = dict() + deleted_sessions: set[int] = set() + + for intermediate_report in intermediate_reports: + report = intermediate_report.report + if report.is_empty(): + continue + + old_sessionid = next(iter(report.sessions)) + new_sessionid = master_report.next_session_number() + session_mapping[intermediate_report.upload_id] = new_sessionid + + if master_report.is_empty() and old_sessionid == new_sessionid: + # if the master report is empty, we can avoid a costly merge operation + master_report = report + continue + + report.change_sessionid(old_sessionid, new_sessionid) + session = report.sessions[new_sessionid] + + _session_id, session = master_report.add_session( + session, use_id_from_session=True + ) + + joined = True + if flags := session.flags: + session_adjustment = clear_carryforward_sessions( + master_report, report, flags, commit_yaml + ) + deleted_sessions.update(session_adjustment.fully_deleted_sessions) + joined = get_joined_flag(commit_yaml, flags) + + master_report.merge(report, joined) + + return master_report, MergeResult(session_mapping, deleted_sessions) + + +@sentry_sdk.trace +def update_uploads( + db_session: DbSession, + commit_yaml: UserYaml, + processing_results: list[ProcessingResult], + intermediate_reports: list[IntermediateReport], + merge_result: MergeResult, +): + """ + Updates all the `Upload` records with the `MergeResult`. + In particular, this updates the `order_number` to match the new `session_id`, + and it deletes all the `Upload` records matching removed carry-forwarded `Session`s. + """ + + # first, delete removed sessions, as report merging can reuse deleted `session_id`s. + if merge_result.deleted_sessions: + any_upload_id = next(iter(merge_result.session_mapping.keys())) + report_id = ( + db_session.query(Upload.report_id) + .filter(Upload.id_ == any_upload_id) + .first()[0] + ) + + delete_uploads_by_sessionid( + db_session, report_id, merge_result.deleted_sessions + ) + + precision: int = read_yaml_field(commit_yaml, ("coverage", "precision"), 2) + rounding: str = read_yaml_field(commit_yaml, ("coverage", "round"), "nearest") + make_totals = functools.partial(make_upload_totals, precision, rounding) + + reports = {ir.upload_id: ir.report for ir in intermediate_reports} + + # then, update all the `Upload`s with their state, and the final `order_number`, + # as well as add a `UploadLevelTotals` or `UploadError`s where appropriate. + all_errors: list[UploadError] = [] + all_totals: list[dict] = [] + all_upload_updates: list[dict] = [] + for result in processing_results: + upload_id = result["upload_id"] + + if result["successful"]: + update = { + "state_id": UploadState.PROCESSED.db_id, + "state": "processed", + } + report = reports.get(upload_id) + if report is not None: + all_totals.append(make_totals(upload_id, report.totals)) + elif result["error"]: + update = { + "state_id": UploadState.ERROR.db_id, + "state": "error", + } + error = UploadError( + upload_id=upload_id, + error_code=result["error"]["code"], + error_params=result["error"]["params"], + ) + all_errors.append(error) + + update["id_"] = upload_id + order_number = merge_result.session_mapping.get(upload_id) + update["order_number"] = order_number + all_upload_updates.append(update) + + db_session.bulk_update_mappings(Upload, all_upload_updates) + db_session.bulk_save_objects(all_errors) + + if all_totals: + # the `UploadLevelTotals` have a unique constraint for the `upload`, + # so we have to use a manual `insert` statement: + stmt = ( + insert(UploadLevelTotals.__table__) + .values(all_totals) + .on_conflict_do_nothing() + ) + db_session.execute(stmt) + + db_session.flush() + + +# TODO(swatinem): we should eventually remove `UploadLevelTotals` completely +def make_upload_totals( + precision: int, rounding: str, upload_id: int, totals: ReportTotals +) -> dict: + if totals.coverage is not None: + coverage = precise_round(Decimal(totals.coverage), precision, rounding) + else: + coverage = Decimal(0) + + return dict( + upload_id=upload_id, + branches=totals.branches, + coverage=coverage, + hits=totals.hits, + lines=totals.lines, + methods=totals.methods, + misses=totals.misses, + partials=totals.partials, + files=totals.files, + ) + + +def get_joined_flag(commit_yaml: UserYaml, flags: list[str]) -> bool: + for flag in flags: + if read_yaml_field(commit_yaml, ("flags", flag, "joined")) is False: + log.info( + "Customer is using joined=False feature", extra={"flag_used": flag} + ) + sentry_sdk.capture_message( + "Customer is using joined=False feature", tags={"flag_used": flag} + ) + return False + + return True diff --git a/apps/worker/services/processing/metrics.py b/apps/worker/services/processing/metrics.py new file mode 100644 index 0000000000..1160b73321 --- /dev/null +++ b/apps/worker/services/processing/metrics.py @@ -0,0 +1,30 @@ +from shared.metrics import Counter, Histogram + +from helpers.metrics import BYTE_SIZE_BUCKETS + +LABELS_USAGE = Counter( + "worker_labels_usage", + "Number of various real-world `carryforward_mode=labels` usages", + ["codepath"], +) + +# The final serialized `Report` sizes, split into `report_json` and `chunks`. +# As the report is often incrementally updated multiple times, this value can +# be biased towards smaller sizes. +PYREPORT_REPORT_JSON_SIZE = Histogram( + "worker_tasks_upload_finisher_report_json_size", + "Size (in bytes) of a report's `report_json`.", + buckets=BYTE_SIZE_BUCKETS, +) +PYREPORT_CHUNKS_FILE_SIZE = Histogram( + "worker_tasks_upload_finisher_chunks_file_size", + "Size (in bytes) of a report's `chunks` file.", + buckets=BYTE_SIZE_BUCKETS, +) + +INTERMEDIATE_REPORT_SIZE = Histogram( + "worker_intermediate_report_size", + "Size (in bytes) of a serialized intermediate report. The `type` can be `report_json` or `chunks`.", + ["type", "compression"], + buckets=BYTE_SIZE_BUCKETS, +) diff --git a/apps/worker/services/processing/processing.py b/apps/worker/services/processing/processing.py new file mode 100644 index 0000000000..07d2e1a357 --- /dev/null +++ b/apps/worker/services/processing/processing.py @@ -0,0 +1,105 @@ +import logging +from collections.abc import Callable + +import sentry_sdk +from celery.exceptions import CeleryError +from shared.yaml import UserYaml +from sqlalchemy.orm import Session as DbSession + +from database.models.core import Commit +from database.models.reports import Upload +from helpers.reports import delete_archive_setting +from services.archive import ArchiveService +from services.report import ProcessingError, RawReportInfo, ReportService +from services.report.parser.types import VersionOneParsedRawReport + +from .intermediate import save_intermediate_report +from .state import ProcessingState +from .types import ProcessingResult, UploadArguments + +log = logging.getLogger(__name__) + + +@sentry_sdk.trace +def process_upload( + on_processing_error: Callable[[ProcessingError], None], + db_session: DbSession, + repo_id: int, + commit_sha: str, + commit_yaml: UserYaml, + arguments: UploadArguments, +) -> ProcessingResult: + upload_id = arguments["upload_id"] + + commit = ( + db_session.query(Commit) + .filter(Commit.repoid == repo_id, Commit.commitid == commit_sha) + .first() + ) + assert commit + + upload = db_session.query(Upload).filter_by(id_=upload_id).first() + assert upload + + state = ProcessingState(repo_id, commit_sha) + # this in a noop in normal cases, but relevant for task retries: + state.mark_uploads_as_processing([upload_id]) + + report_service = ReportService(commit_yaml) + archive_service = report_service.get_archive_service(commit.repository) + + result = ProcessingResult( + upload_id=upload_id, arguments=arguments, successful=False + ) + db_session.commit() + try: + report_info = RawReportInfo() + processing_result = report_service.build_report_from_raw_content( + report_info, upload + ) + + if error := processing_result.error: + on_processing_error(error) # NOTE: this might throw a `Retry` + result["error"] = error.as_dict() + else: + result["successful"] = True + log.info("Finished processing upload", extra={"result": result}) + + if processing_result.report: + save_intermediate_report(upload_id, processing_result.report) + state.mark_upload_as_processed(upload_id) + + rewrite_or_delete_upload(archive_service, commit_yaml, report_info) + + except CeleryError: + raise + except Exception: + commit.state = "error" + db_session.commit() + log.exception("Could not properly process commit") + raise + + finally: + # this is a noop in the success case, but makes sure unrecoverable errors + # or retries are not blocking later merge/notify stages + state.clear_in_progress_uploads([upload_id]) + + return result + + +def rewrite_or_delete_upload( + archive_service: ArchiveService, commit_yaml: UserYaml, report_info: RawReportInfo +): + should_delete_archive_setting = delete_archive_setting(commit_yaml) + archive_url = report_info.archive_url + + if should_delete_archive_setting and not report_info.error: + if not archive_url.startswith("http"): + archive_service.delete_file(archive_url) + + elif isinstance(report_info.raw_report, VersionOneParsedRawReport): + # only a version 1 report needs to be "rewritten readable" + + archive_service.write_file( + archive_url, report_info.raw_report.content().getvalue() + ) diff --git a/apps/worker/services/processing/state.py b/apps/worker/services/processing/state.py new file mode 100644 index 0000000000..3966b1c0fb --- /dev/null +++ b/apps/worker/services/processing/state.py @@ -0,0 +1,118 @@ +""" +This abstracts the "processing state" for a commit. + +It takes care that each upload for a specific commit is going through the following +states: + +- "processing": when an upload was received and is being parsed/processed. +- "processed": the upload has been processed and an "intermediate report" has been stored, + the upload is now waiting to be merged into the "master report". +- "merged": the upload was fully merged into the "master report". + +The logic in this file also makes sure that processing and merging happens in an "optimal" way +meaning that: + +- "postprocessing", which means triggering notifications and other followup work + only happens once for a commit. +- merging should happen in batches, as that involves loading a bunch of "intermediate report"s + into memory, which should be bounded. +- (ideally in the future) an upload that has been processed into an "intermediate report" + should be merged directly into the "master report" without doing a storage roundtrip for that + "intermediate report". +""" + +from dataclasses import dataclass + +from shared.helpers.redis import get_redis_connection +from shared.metrics import Counter + +MERGE_BATCH_SIZE = 10 + +CLEARED_UPLOADS = Counter( + "worker_processing_cleared_uploads", + "Number of uploads cleared from queue because of errors", +) + + +@dataclass +class UploadNumbers: + processing: int + """ + The number of uploads currently being processed. + """ + + processed: int + """ + The number of uploads that have been processed, + and are waiting on being merged into the "master report". + """ + + +def should_perform_merge(uploads: UploadNumbers) -> bool: + """ + Determines whether a merge should be performed. + + This is the case when no more uploads are expected, + or we reached the desired batch size for merging. + """ + return uploads.processing == 0 or uploads.processed >= MERGE_BATCH_SIZE + + +def should_trigger_postprocessing(uploads: UploadNumbers) -> bool: + """ + Determines whether post-processing steps, such as notifications, etc, + should be performed. + + This is the case when no more uploads are expected, + and all the processed uploads have been merged into the "master report". + """ + return uploads.processing == 0 and uploads.processed == 0 + + +class ProcessingState: + def __init__(self, repoid: int, commitsha: str) -> None: + self._redis = get_redis_connection() + self.repoid = repoid + self.commitsha = commitsha + + def get_upload_numbers(self): + processing = self._redis.scard(self._redis_key("processing")) + processed = self._redis.scard(self._redis_key("processed")) + return UploadNumbers(processing, processed) + + def mark_uploads_as_processing(self, upload_ids: list[int]): + self._redis.sadd(self._redis_key("processing"), *upload_ids) + + def clear_in_progress_uploads(self, upload_ids: list[int]): + removed_uploads = self._redis.srem(self._redis_key("processing"), *upload_ids) + if removed_uploads > 0: + # the normal flow would move the uploads from the "processing" set + # to the "processed" set via `mark_upload_as_processed`. + # this function here is only called in the error case and we don't expect + # this to be triggered often, if at all. + CLEARED_UPLOADS.inc(removed_uploads) + + def mark_upload_as_processed(self, upload_id: int): + res = self._redis.smove( + self._redis_key("processing"), self._redis_key("processed"), upload_id + ) + if not res: + # this can happen when `upload_id` was never in the source set, + # which probably is the case during initial deployment as + # the code adding this to the initial set was not deployed yet + # TODO: make sure to remove this code after a grace period + self._redis.sadd(self._redis_key("processed"), upload_id) + + def mark_uploads_as_merged(self, upload_ids: list[int]): + self._redis.srem(self._redis_key("processed"), *upload_ids) + + def get_uploads_for_merging(self) -> set[int]: + return set( + int(id) + for id in self._redis.srandmember( + self._redis_key("processed"), MERGE_BATCH_SIZE + ) + ) + + def _redis_key(self, state: str) -> str: + return f"upload-processing-state/{self.repoid}/{self.commitsha}/{state}" diff --git a/apps/worker/services/processing/types.py b/apps/worker/services/processing/types.py new file mode 100644 index 0000000000..7e054b574b --- /dev/null +++ b/apps/worker/services/processing/types.py @@ -0,0 +1,66 @@ +from dataclasses import dataclass +from typing import Any, NotRequired, TypedDict + +from shared.reports.resources import Report +from shared.upload.constants import UploadErrorCode + + +class UploadArguments(TypedDict, total=False): + upload_id: int + + # TODO(swatinem): migrate this over to `upload_id` + upload_pk: int + + flags: list[str] + url: str + + name: NotRequired[str] + reportid: NotRequired[str] + build: NotRequired[str] + build_url: NotRequired[str] + job: NotRequired[str] + service: NotRequired[str] + + # TODO(swatinem): remove these fields completely being passed from API: + # `redis_key` being removed in https://github.com/codecov/codecov-api/pull/960 + redis_key: NotRequired[str] + token: NotRequired[str] + + +class ProcessingErrorDict(TypedDict): + code: UploadErrorCode + params: dict[str, Any] + + +class ProcessingResult(TypedDict): + upload_id: int + arguments: UploadArguments + successful: bool + error: NotRequired[ProcessingErrorDict] + + +@dataclass +class IntermediateReport: + upload_id: int + """ + The `Upload` id for which this report was loaded. + """ + + report: Report + """ + The loaded Report. + """ + + +@dataclass +class MergeResult: + session_mapping: dict[int, int] + """ + This is a mapping from the input `upload_id` to the output `session_id` + as it exists in the merged "master Report". + """ + + deleted_sessions: set[int] + """ + The Set of carryforwarded `session_id`s that have been removed from the "master Report". + """ diff --git a/apps/worker/services/report/README.md b/apps/worker/services/report/README.md new file mode 100644 index 0000000000..9277520b1e --- /dev/null +++ b/apps/worker/services/report/README.md @@ -0,0 +1,23 @@ +This is the part of the system responsible for reading the uploaded-reports. + +The classes follow the `adapter` design pattern. Each implementation class is responsible for reading one type of file (like one implentation for gcov, one implementation for cobertura etc) + +- It is an `adapter` in that it tries to read the existing format provider by a third-party into a standard format we have (it's not a textbook adapter design pattern, but it matches the concept of an adapter) + +# Flow + +The flow is as follows: + +- The `services/report/report_processor.py` function `process_report` is called. It first tries to do a simple parsing of the user-uploaded report, determining if it is a json/xml/plist/txt file (or other high-level format). + +- After that is determined, it fetches all the `BaseLanguageProcessor` implementations that can parse that type of file (ie, all implementations that deal with json files). + +- With that list, it tries to see which of the implementations can deal with that specific file. For such, it calls `matches_content` on every implementation until the first one that returns True + +- On that implementation, it calls the `process`, which takes the uploaded report (a string), a few more parameters, and returns a `Report` with the actual coverage information that such report gives. + +# Parsing a new type of file + +To implement a new type of file, one must create a new implementation of `BaseLanguageProcessor` present in `services/report/languages/base.py`. + +Then one needs to add this to the relevant high-level format in `get_possible_processors_list`. Where it is inside that function determines what exact type object is passed to the `matches_content` and `process` methods. For example, if added to the `xml` section, the implementation can expect a python `etree.ElementTree` object passed to it. \ No newline at end of file diff --git a/apps/worker/services/report/__init__.py b/apps/worker/services/report/__init__.py new file mode 100644 index 0000000000..3957fbec70 --- /dev/null +++ b/apps/worker/services/report/__init__.py @@ -0,0 +1,816 @@ +import copy +import itertools +import logging +import uuid +from dataclasses import dataclass +from time import time +from typing import Any + +import orjson +import sentry_sdk +from asgiref.sync import async_to_sync +from celery.exceptions import SoftTimeLimitExceeded +from shared.django_apps.reports.models import ReportType +from shared.reports.carryforward import generate_carryforward_report +from shared.reports.enums import UploadState, UploadType +from shared.reports.resources import Report +from shared.reports.types import TOTALS_MAP +from shared.storage.exceptions import FileNotInStorageError +from shared.torngit.exceptions import TorngitError +from shared.upload.constants import UploadErrorCode +from shared.utils.sessions import Session, SessionType +from shared.yaml import UserYaml +from sqlalchemy.orm import Session as DbSession + +from database.models import Commit, Repository, Upload, UploadError +from database.models.reports import ( + CommitReport, + ReportLevelTotals, + RepositoryFlag, + UploadLevelTotals, + uploadflagmembership, +) +from helpers.exceptions import ( + OwnerWithoutValidBotError, + ReportEmptyError, + ReportExpiredException, + RepositoryWithoutValidBotError, +) +from rollouts import CARRYFORWARD_BASE_SEARCH_RANGE_BY_OWNER +from services.archive import ArchiveService +from services.processing.metrics import ( + PYREPORT_CHUNKS_FILE_SIZE, + PYREPORT_REPORT_JSON_SIZE, +) +from services.processing.types import ProcessingErrorDict, UploadArguments +from services.report.parser import get_proper_parser +from services.report.parser.types import ParsedRawReport +from services.report.parser.version_one import VersionOneReportParser +from services.report.prometheus_metrics import ( + RAW_UPLOAD_RAW_REPORT_COUNT, + RAW_UPLOAD_SIZE, +) +from services.report.raw_upload_processor import process_raw_upload +from services.repository import get_repo_provider_service +from services.yaml.reader import get_paths_from_flags, read_yaml_field + + +@dataclass +class ProcessingError: + code: UploadErrorCode + params: dict[str, Any] + is_retryable: bool = False + + def as_dict(self) -> ProcessingErrorDict: + return {"code": self.code, "params": self.params} + + +@dataclass +class ProcessingResult: + session: Session + report: Report | None = None + error: ProcessingError | None = None + + +@dataclass +class RawReportInfo: + raw_report: ParsedRawReport | None = None + archive_url: str = "" + upload: str = "" + error: ProcessingError | None = None + + +log = logging.getLogger(__name__) + + +class NotReadyToBuildReportYetError(Exception): + pass + + +class BaseReportService: + """ + This is the class that will handle anything report-handling related + + Attributes: + current_yaml (Mapping[str, Any]): The configuration we need to follow. + It's always the user yaml, but might have different uses on different places + """ + + def __init__(self, current_yaml: UserYaml | dict): + if isinstance(current_yaml, dict): + current_yaml = UserYaml(current_yaml) + self.current_yaml = current_yaml + + def initialize_and_save_report( + self, commit: Commit, report_code: str | None = None + ) -> CommitReport: + raise NotImplementedError() + + def create_report_upload( + self, arguments: UploadArguments, commit_report: CommitReport + ) -> Upload: + """ + Creates an `Upload` from the user-given arguments to a job + + The end goal here is that the `Upload` should have all the information needed to + hypothetically redo the job later. + """ + db_session = commit_report.get_db_session() + name = arguments.get("name") + upload = Upload( + report_id=commit_report.id_, + external_id=arguments.get("reportid"), + build_code=arguments.get("build"), + build_url=arguments.get("build_url"), + env=None, + job_code=arguments.get("job"), + name=(name[:100] if name else None), + provider=arguments.get("service"), + state="started", + storage_path=arguments.get("url"), + order_number=None, + upload_extras={}, + upload_type=SessionType.uploaded.value, + state_id=UploadState.UPLOADED.db_id, + upload_type_id=UploadType.UPLOADED.db_id, + ) + db_session.add(upload) + db_session.flush() + return upload + + +class ReportService(BaseReportService): + def __init__( + self, current_yaml: UserYaml | dict, gh_app_installation_name: str | None = None + ): + super().__init__(current_yaml) + self.flag_dict: dict[str, RepositoryFlag] | None = None + self.gh_app_installation_name = gh_app_installation_name + + def has_initialized_report(self, commit: Commit) -> bool: + """ + Says whether a commit has already initialized its report or not + """ + return ( + commit._report_json is not None + or commit._report_json_storage_path is not None + ) + + @sentry_sdk.trace + def initialize_and_save_report( + self, commit: Commit, report_code: str | None = None + ) -> CommitReport: + """ + Initializes the commit report + + + This is one of the main entrypoint of this class. It takes care of: + - Creating the `CommitReport`, if needed + - If that commit is old-style (was created before the report models were installed), + it takes care of backfilling all the information from the report into the new + report models + - If that commit needs something to be carryforwarded, it does that logic and + already saves the report into the database and storage + + Args: + commit (Commit): The commit we want to initialize + + Returns: + CommitReport: The CommitReport for that commit + """ + db_session = commit.get_db_session() + current_report_row = ( + db_session.query(CommitReport) + .filter_by(commit_id=commit.id_, code=report_code) + .filter( + (CommitReport.report_type == None) # noqa: E711 + | (CommitReport.report_type == ReportType.COVERAGE.value) + ) + .first() + ) + if not current_report_row: + # This happens if the commit report is being created for the first time + # or backfilled + current_report_row = CommitReport( + commit_id=commit.id_, + code=report_code, + report_type=ReportType.COVERAGE.value, + ) + db_session.add(current_report_row) + db_session.flush() + + actual_report = self.get_existing_report_for_commit( + commit, report_code=report_code + ) + if actual_report is not None: + log.info( + "Backfilling reports tables from commits.report", + extra=dict(commitid=commit.commitid), + ) + # This case means the report exists in our system, it was just not saved + # yet into the new models therefore it needs backfilling + self.save_full_report(commit, actual_report) + + if not self.has_initialized_report(commit): + report = self.create_new_report_for_commit(commit) + if not report.is_empty(): + # This means there is a report to carryforward + self.save_full_report(commit, report, report_code) + + return current_report_row + + def _attach_flags_to_upload(self, upload: Upload, flag_names: list[str]): + """ + Internal function that manages creating the proper `RepositoryFlag`s, + and attach them to the `Upload` + """ + + all_flags = [] + db_session = upload.get_db_session() + repoid = upload.report.commit.repoid + flag_dict = self.fetch_repo_flags(db_session, repoid) + + for individual_flag in flag_names: + flag_obj = flag_dict.get(individual_flag, None) + if flag_obj is None: + flag_obj = RepositoryFlag( + repository_id=repoid, flag_name=individual_flag + ) + db_session.add(flag_obj) + db_session.flush() + flag_dict[individual_flag] = flag_obj + all_flags.append(flag_obj) + + upload.flags = all_flags + db_session.flush() + return all_flags + + def fetch_repo_flags(self, db_session, repoid: int) -> dict[str, RepositoryFlag]: + if self.flag_dict is None: + existing_flags_on_repo = ( + db_session.query(RepositoryFlag).filter_by(repository_id=repoid).all() + ) + self.flag_dict = {flag.flag_name: flag for flag in existing_flags_on_repo} + return self.flag_dict + + def get_archive_service(self, repository: Repository) -> ArchiveService: + return ArchiveService(repository) + + @sentry_sdk.trace + def get_existing_report_for_commit( + self, commit: Commit, report_class=None, report_code=None + ) -> Report | None: + commitid = commit.commitid + if not self.has_initialized_report(commit): + return None + + try: + archive_service = self.get_archive_service(commit.repository) + chunks = archive_service.read_chunks(commitid, report_code) + except FileNotInStorageError: + log.warning( + "File for chunks not found in storage", + extra=dict( + commit=commitid, repo=commit.repoid, report_code=report_code + ), + ) + return None + + if chunks is None: + return None + + files = commit.report_json["files"] + sessions = commit.report_json["sessions"] + totals = commit.totals + + if report_class is None: + report_class = Report + + # TODO(swatinem): move this logic into the `Report` constructor + for session_id, session in sessions.items(): + if not isinstance(session, Session): + session["id"] = int(session_id) + + return report_class.from_chunks( + chunks=chunks, files=files, sessions=sessions, totals=totals + ) + + def get_appropriate_commit_to_carryforward_from( + self, commit: Commit, max_parenthood_deepness: int = 10 + ) -> Commit | None: + parent_commit = commit.get_parent_commit() + parent_commit_tracking = [] + count = 1 # `parent_commit` is already the first parent + while ( + parent_commit is not None + and parent_commit.state not in ("complete", "skipped") + and count < max_parenthood_deepness + ): + parent_commit_tracking.append(parent_commit.commitid) + if ( + parent_commit.state == "pending" + and parent_commit.parent_commit_id is None + ): + log.warning( + "One of the ancestors commit doesn't seem to have determined its parent yet", + extra=dict( + commit=commit.commitid, + repoid=commit.repoid, + current_parent_commit=parent_commit.commitid, + ), + ) + raise NotReadyToBuildReportYetError() + log.info( + "Going from parent to their parent since they dont match the requisites for CFF", + extra=dict( + commit=commit.commitid, + repoid=commit.repoid, + current_parent_commit=parent_commit.commitid, + parent_tracking=parent_commit_tracking, + current_state=parent_commit.state, + new_parent_commit=parent_commit.parent_commit_id, + ), + ) + parent_commit = parent_commit.get_parent_commit() + count += 1 + if parent_commit is None: + log.warning( + "No parent commit was found to be carriedforward from", + extra=dict( + commit=commit.commitid, + repoid=commit.repoid, + parent_tracing=parent_commit_tracking, + ), + ) + return None + if parent_commit.state not in ("complete", "skipped"): + log.warning( + "None of the parent commits were in a complete state to be used as CFing base", + extra=dict( + commit=commit.commitid, + repoid=commit.repoid, + parent_tracking=parent_commit_tracking, + would_be_state=parent_commit.state, + would_be_parent=parent_commit.commitid, + ), + ) + return None + return parent_commit + + def _possibly_shift_carryforward_report( + self, carryforward_report: Report, base_commit: Commit, head_commit: Commit + ) -> Report: + try: + provider_service = get_repo_provider_service( + repository=head_commit.repository, + installation_name_to_use=self.gh_app_installation_name, + ) + diff = async_to_sync(provider_service.get_compare)( + base=base_commit.commitid, head=head_commit.commitid + ) + # Volatile function, alters carryforward_report + carryforward_report.shift_lines_by_diff(diff["diff"]) + except (RepositoryWithoutValidBotError, OwnerWithoutValidBotError) as exp: + log.error( + "Failed to shift carryforward report lines", + extra=dict( + reason="Can't get provider_service", + commit=head_commit.commitid, + error=str(exp), + ), + ) + except TorngitError as exp: + log.error( + "Failed to shift carryforward report lines.", + extra=dict( + reason="Can't get diff", + commit=head_commit.commitid, + error=str(exp), + error_type=type(exp), + ), + ) + except SoftTimeLimitExceeded: + raise + except Exception: + log.exception( + "Failed to shift carryforward report lines.", + extra=dict( + reason="Unknown", + commit=head_commit.commitid, + ), + ) + return carryforward_report + + def create_new_report_for_commit(self, commit: Commit) -> Report: + log.info( + "Creating new report for commit", + extra=dict(commit=commit.commitid, repoid=commit.repoid), + ) + if not self.current_yaml or not self.current_yaml.has_any_carryforward(): + return Report() + + repo = commit.repository + # This experiment is inactive because the data went back and forth + # on whether it was impactful or not. The `Feature` is left here as + # a knob to turn for support requests about carryforward flags, and + # maybe we'll revisit a general rollout at a later time. + max_parenthood_deepness = CARRYFORWARD_BASE_SEARCH_RANGE_BY_OWNER.check_value( + identifier=repo.ownerid, default=10 + ) + + parent_commit = self.get_appropriate_commit_to_carryforward_from( + commit, max_parenthood_deepness + ) + if parent_commit is None: + log.warning( + "Could not find parent for possible carryforward", + extra=dict(commit=commit.commitid, repoid=commit.repoid), + ) + return Report() + + parent_report = self.get_existing_report_for_commit(parent_commit) + if parent_report is None: + log.warning( + "Could not carryforward report from another commit because parent has no report", + extra=dict( + commit=commit.commitid, + repoid=commit.repoid, + parent_commit=parent_commit.commitid, + ), + ) + return Report() + + flags_to_carryforward = [ + flag_name + for flag_name in parent_report.get_flag_names() + if self.current_yaml.flag_has_carryfoward(flag_name) + ] + if not flags_to_carryforward: + return Report() + + paths_to_carryforward = get_paths_from_flags( + self.current_yaml, flags_to_carryforward + ) + log.info( + "Generating carriedforward report", + extra=dict( + commit=commit.commitid, + repoid=commit.repoid, + parent_commit=parent_commit.commitid, + flags_to_carryforward=flags_to_carryforward, + paths_to_carryforward=paths_to_carryforward, + parent_sessions=parent_report.sessions, + ), + ) + carryforward_report = generate_carryforward_report( + parent_report, + flags_to_carryforward, + paths_to_carryforward, + session_extras=dict(carriedforward_from=parent_commit.commitid), + ) + # If the parent report has labels we also need to carryforward the label index + # Considerations: + # 1. It's necessary for labels flags to be carryforward, so it's ok to carryforward the entire index + # 2. As tests are renamed the index might start to be filled with stale labels. This is not good. + # but I'm unsure if we should try to clean it up at this point. Cleaning it up requires going through + # all lines of the report. It will be handled by a dedicated task that is encoded by the UploadFinisher + # 3. We deepcopy the header so we can change them independently + # 4. The parent_commit always uses the default report to carryforward (i.e. report_code for parent_commit is None) + # parent_commit and commit should belong to the same repository + carryforward_report.header = copy.deepcopy(parent_report.header) + + self._possibly_shift_carryforward_report( + carryforward_report, parent_commit, commit + ) + return carryforward_report + + @sentry_sdk.trace + def parse_raw_report_from_storage( + self, repo: Repository, upload: Upload + ) -> ParsedRawReport: + """Pulls the raw uploaded report from storage and parses it so it's + easier to access different parts of the raw upload. + + Raises: + shared.storage.exceptions.FileNotInStorageError + """ + archive_service = self.get_archive_service(repo) + archive_url = upload.storage_path + + log.info( + "Parsing the raw report from storage", + extra=dict( + commit=upload.report.commit_id, + repoid=repo.repoid, + archive_url=archive_url, + ), + ) + + archive_file = archive_service.read_file(archive_url) + + parser = get_proper_parser(upload, archive_file) + upload_version = ( + "v1" if isinstance(parser, VersionOneReportParser) else "legacy" + ) + RAW_UPLOAD_SIZE.labels(version=upload_version).observe(len(archive_file)) + + raw_uploaded_report = parser.parse_raw_report_from_bytes(archive_file) + + raw_report_count = len(raw_uploaded_report.get_uploaded_files()) + if raw_report_count < 1: + log.warning( + "Raw upload contains no uploaded files", + extra=dict( + commit=upload.report.commit_id, + repoid=repo.repoid, + raw_report_count=raw_report_count, + upload_version=upload_version, + archive_url=archive_url, + ), + ) + RAW_UPLOAD_RAW_REPORT_COUNT.labels(version=upload_version).observe( + raw_report_count + ) + + return raw_uploaded_report + + @sentry_sdk.trace + def build_report_from_raw_content( + self, + raw_report_info: RawReportInfo, + upload: Upload, + ) -> ProcessingResult: + """ + Processes an upload on top of an existing report `master` and returns + a result, which could be successful or not + + Note that this function does not modify the `upload` object, as this should + be done by a separate function + """ + commit = upload.report.commit + flags = upload.flag_names + archive_url = upload.storage_path + reportid = upload.external_id + + session = Session( + provider=upload.provider, + build=upload.build_code, + job=upload.job_code, + name=upload.name, + time=int(time()), + flags=flags, + archive=archive_url, + url=upload.build_url, + ) + result = ProcessingResult(session=session) + + raw_report_info.archive_url = archive_url + raw_report_info.upload = upload.external_id + + try: + raw_report = self.parse_raw_report_from_storage(commit.repository, upload) + raw_report_info.raw_report = raw_report + except FileNotInStorageError as e: + sentry_sdk.capture_exception(e) + log.info( + "Raw report file was not found", + extra=dict( + reportid=reportid, + commit_yaml=self.current_yaml.to_dict(), + archive_url=archive_url, + ), + ) + result.error = ProcessingError( + code=UploadErrorCode.FILE_NOT_IN_STORAGE, + params={"location": archive_url}, + is_retryable=True, + ) + raw_report_info.error = result.error + return result + except Exception as e: + sentry_sdk.capture_exception(e) + log.exception( + "Unknown error when fetching raw report from storage", + extra=dict(archive_path=archive_url), + ) + result.error = ProcessingError( + code=UploadErrorCode.UNKNOWN_STORAGE, + params={"location": archive_url}, + is_retryable=False, + ) + raw_report_info.error = result.error + return result + + log.debug("Retrieved report for processing from url %s", archive_url) + try: + result.report = process_raw_upload(self.current_yaml, raw_report, session) + + log.info( + "Successfully processed report", + extra=dict( + session=session.id, + ci=f"{session.provider}:{session.build}:{session.job}", + reportid=reportid, + commit_yaml=self.current_yaml.to_dict(), + content_len=raw_report.size, + ), + ) + return result + except ReportExpiredException as r: + sentry_sdk.capture_exception(r) + log.info( + "Report is expired", + extra=dict( + reportid=reportid, archive_path=archive_url, file_name=r.filename + ), + ) + result.error = ProcessingError( + code=UploadErrorCode.REPORT_EXPIRED, params={} + ) + raw_report_info.error = result.error + return result + except ReportEmptyError as e: + sentry_sdk.capture_exception(e) + log.warning("Report is empty", extra=dict(reportid=reportid)) + result.error = ProcessingError(code=UploadErrorCode.REPORT_EMPTY, params={}) + raw_report_info.error = result.error + return result + except SoftTimeLimitExceeded as e: + sentry_sdk.capture_exception(e) + log.warning( + "Timed out while processing report", extra=dict(reportid=reportid) + ) + result.error = ProcessingError( + code=UploadErrorCode.PROCESSING_TIMEOUT, params={} + ) + raw_report_info.error = result.error + # Return and attempt to save the error result rather than re-raise + return result + except Exception as e: + sentry_sdk.capture_exception(e) + log.exception( + "Unknown error when processing raw upload", + extra=dict(archive_path=archive_url), + ) + result.error = ProcessingError( + code=UploadErrorCode.UNKNOWN_PROCESSING, + params={"location": archive_url}, + is_retryable=False, + ) + raw_report_info.error = result.error + return result + + @sentry_sdk.trace + def save_report(self, commit: Commit, report: Report, report_code=None): + archive_service = self.get_archive_service(commit.repository) + + report_json, chunks, _totals = report.serialize() + + PYREPORT_REPORT_JSON_SIZE.observe(len(report_json)) + PYREPORT_CHUNKS_FILE_SIZE.observe(len(chunks)) + + chunks_url = archive_service.write_chunks(commit.commitid, chunks, report_code) + + commit.state = "complete" if report else "error" + commit.totals = legacy_totals(report) + if ( + commit.totals is not None + and "c" in commit.totals + and commit.totals["c"] is None + ): + # temporary measure until we ensure the API and frontend don't expect not-null coverages + commit.totals["c"] = 0 + + log.info( + "Calling update to Commit.Report", + extra=dict( + size=len(report_json), + ownerid=commit.repository.ownerid, + repoid=commit.repoid, + commitid=commit.commitid, + ), + ) + # `report_json` is an `ArchiveField`, so this will trigger an upload + # FIXME: we do an unnecessary `loads` roundtrip because of this abstraction, + # and we should just save the `report_json` to archive storage directly instead. + commit.report_json = orjson.loads(report_json) + + # `report` is an accessor which implicitly queries `CommitReport` + if commit_report := commit.report: + db_session = commit.get_db_session() + + report_totals = commit_report.totals + if report_totals is None: + report_totals = ReportLevelTotals(report_id=commit_report.id) + db_session.add(report_totals) + + rounding: str = read_yaml_field( + self.current_yaml, ("coverage", "round"), "nearest" + ) + precision: int = read_yaml_field( + self.current_yaml, ("coverage", "precision"), 2 + ) + report_totals.update_from_totals( + report.totals, precision=precision, rounding=rounding + ) + db_session.flush() + log.info( + "Archived report", + extra=dict( + repoid=commit.repoid, + commit=commit.commitid, + url=chunks_url, + number_sessions=len(report.sessions), + new_report_sessions=dict(itertools.islice(report.sessions.items(), 20)), + ), + ) + return {"url": chunks_url} + + @sentry_sdk.trace + def save_full_report( + self, commit: Commit, report: Report, report_code=None + ) -> dict: + """ + Saves the report (into database and storage) AND takes care of backfilling its sessions + like they were never in the database (useful for backfilling and carryforward cases) + """ + rounding: str = read_yaml_field( + self.current_yaml, ("coverage", "round"), "nearest" + ) + precision: int = read_yaml_field( + self.current_yaml, ("coverage", "precision"), 2 + ) + res = self.save_report(commit, report, report_code) + db_session = commit.get_db_session() + for sess_id, session in report.sessions.items(): + upload = Upload( + build_code=session.build, + build_url=session.url, + env=session.env, + external_id=uuid.uuid4(), + job_code=session.job, + name=session.name[:100] if session.name is not None else None, + order_number=sess_id, + provider=session.provider, + report_id=commit.report.id_, + state="complete", + storage_path=session.archive if session.archive is not None else "", + upload_extras=session.session_extras or {}, + upload_type=( + session.session_type.value + if session.session_type is not None + else "unknown" + ), + ) + db_session.add(upload) + db_session.flush() + self._attach_flags_to_upload(upload, session.flags if session.flags else []) + if session.totals is not None: + upload_totals = UploadLevelTotals(upload_id=upload.id_) + db_session.add(upload_totals) + upload_totals.update_from_totals( + session.totals, precision=precision, rounding=rounding + ) + db_session.flush() + + return res + + +@sentry_sdk.trace +def delete_uploads_by_sessionid( + db_session: DbSession, report_id: int, session_ids: set[int] +): + """ + This deletes all the `Upload` records belonging to the `CommitReport` with `report_id`, + and having an `order_number` corresponding to the given `session_ids`. + """ + uploads = ( + db_session.query(Upload.id_) + .filter( + Upload.report_id == report_id, + Upload.upload_type == SessionType.carriedforward.value, + Upload.order_number.in_(session_ids), + ) + .all() + ) + upload_ids = [upload[0] for upload in uploads] + + db_session.query(UploadError).filter(UploadError.upload_id.in_(upload_ids)).delete( + synchronize_session=False + ) + db_session.query(UploadLevelTotals).filter( + UploadLevelTotals.upload_id.in_(upload_ids) + ).delete(synchronize_session=False) + db_session.query(uploadflagmembership).filter( + uploadflagmembership.c.upload_id.in_(upload_ids) + ).delete(synchronize_session=False) + db_session.query(Upload).filter(Upload.id_.in_(upload_ids)).delete( + synchronize_session=False + ) + db_session.flush() + + +def legacy_totals(report: Report) -> dict: + totals = dict(zip(TOTALS_MAP, report.totals)) + totals["diff"] = report.diff_totals + return totals diff --git a/apps/worker/services/report/fixes.py b/apps/worker/services/report/fixes.py new file mode 100644 index 0000000000..0c137d5e55 --- /dev/null +++ b/apps/worker/services/report/fixes.py @@ -0,0 +1,68 @@ +from services.path_fixer import PathFixer + + +def get_fixes_from_raw(content: str, fix: PathFixer) -> dict[str, dict]: + files: dict[str, dict] = {} + files_long_comments: dict[str, tuple[list[int], list[int]]] = {} + _cur_file = None + + for line in content.splitlines(): + if line: + try: + if line[:5] == "EOF: ": + _, line, filename = line.split(" ", 2) + files.setdefault(fix(filename), {})["eof"] = int(line) + + else: + # filename:5 + # filename:5,10,20 + filename, line = line.split(":", 1) + + if _cur_file != filename: + _cur_file = filename + _fixed = fix(filename) + if not _fixed: + continue + lines: set[int] = files.setdefault(_fixed, {"lines": set()})[ + "lines" + ] + + if ":" not in line: + # multi line + for sp in line.split(","): + lines.add(int(sp)) + + else: + ln, source = line.split(":", 1) + ln = int(ln) + source = source.strip() + + if source[:2] == "/*" or "LCOV_EXCL_START" in source: + files_long_comments.setdefault(_fixed, ([], []))[0].append( + ln + ) + + elif ( + source[-2:] == "*/" + or "LCOV_EXCL_STOP" in source + or "LCOV_EXCL_END" in source + ): + files_long_comments.setdefault(_fixed, ([], []))[1].append( + ln + ) + + lines.add(ln) + + except Exception: + pass + + for filename, (starts, stops) in files_long_comments.items(): + if filename and starts and stops: + starts = sorted(starts) + stops = sorted(stops) + lines = files[filename]["lines"] + for x in range(len(starts)): + if len(stops) > x: + lines.update(range(int(starts[x]) + 1, int(stops[x]))) + + return files diff --git a/apps/worker/services/report/languages/__init__.py b/apps/worker/services/report/languages/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/report/languages/base.py b/apps/worker/services/report/languages/base.py new file mode 100644 index 0000000000..dd6de9d95f --- /dev/null +++ b/apps/worker/services/report/languages/base.py @@ -0,0 +1,41 @@ +from typing import Any + +from services.report.report_builder import ReportBuilderSession + + +class BaseLanguageProcessor(object): + def __init__(self, *args, **kwargs) -> None: + pass + + def matches_content(self, content: Any, first_line: str, name: str) -> bool: + """ + Determines whether this processor is capable of processing this file. + + This is meant to be a high-level verification, and should not go through the whole file + to extensively check if everything is correct. + + One example here is to check something on the first line, or check if a + certain key is present at the top-level json and has the right type of value under + it. Or maybe if a certain set ot XML tags that are unique to this format are here. + + As long as this file can make sure to not accidentally try to parse formats that + belong with other processors, it is not a big deal (for now) + + Args: + content (Any): The actual report content + first_line (str): The first line of the report, as a string + name (str): The filename of the report (as provided by the upload) + Returns: + bool: True if we can read this file, False otherwise + """ + return False + + def process(self, content: Any, report_builder_session: ReportBuilderSession): + """ + Processes a report uploaded by the user, appending coverage information + to the provided `ReportBuilderSession`. + + Raises: + ReportExpiredException: If the report is considered expired + """ + pass diff --git a/apps/worker/services/report/languages/bullseye.py b/apps/worker/services/report/languages/bullseye.py new file mode 100644 index 0000000000..0b3dd6cb80 --- /dev/null +++ b/apps/worker/services/report/languages/bullseye.py @@ -0,0 +1,70 @@ +import sentry_sdk +from lxml.etree import Element +from timestring import Date + +from helpers.exceptions import ReportExpiredException +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import CoverageType, ReportBuilderSession + + +class BullseyeProcessor(BaseLanguageProcessor): + def matches_content(self, content: Element, first_line: str, name: str) -> bool: + return "BullseyeCoverage" in content.tag + + @sentry_sdk.trace + def process( + self, content: Element, report_builder_session: ReportBuilderSession + ) -> None: + return from_xml(content, report_builder_session) + + +def from_xml(xml: Element, report_builder_session: ReportBuilderSession) -> None: + if max_age := report_builder_session.yaml_field( + ("codecov", "max_report_age"), "12h ago" + ): + build_id = xml.attrib.get("buildId") + # build_id format has timestamp at the end "4362c668_2020-10-28_17:55:47" + timestamp = " ".join(build_id.split("_")[1:]) + if timestamp and Date(timestamp) < max_age: + raise ReportExpiredException("Bullseye report expired %s" % timestamp) + + for folder in xml.iter("{https://www.bullseye.com/covxml}folder"): + for file in folder.iter("{https://www.bullseye.com/covxml}src"): + # Get filepath from parent folder(s) + filepath = "" + element = file + while element.getparent().tag == "{https://www.bullseye.com/covxml}folder": + element = element.getparent() + filepath = f"{element.attrib.get('name')}/{filepath}" + filepath += file.attrib.get("name") + + _file = report_builder_session.create_coverage_file(filepath) + if _file is None: + continue + + for function in file.iter("{https://www.bullseye.com/covxml}fn"): + for probe in function.iter("{https://www.bullseye.com/covxml}probe"): + attribs = probe.attrib + ln = int(attribs["line"]) + if attribs["kind"] in ("condition", "decision", "switch-label"): + _type = CoverageType.branch + + elif attribs["kind"] == "function": + _type = CoverageType.method + else: + _type = CoverageType.line + + if attribs["event"] == "full": + coverage = 1 + elif attribs["event"] == "none": + coverage = 0 + else: + coverage = "1/2" + _file.append( + ln, + report_builder_session.create_coverage_line( + coverage, + _type, + ), + ) + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/clover.py b/apps/worker/services/report/languages/clover.py new file mode 100644 index 0000000000..3010e3a0da --- /dev/null +++ b/apps/worker/services/report/languages/clover.py @@ -0,0 +1,109 @@ +import sentry_sdk +from lxml.etree import Element +from timestring import Date + +from helpers.exceptions import ReportExpiredException +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import CoverageType, ReportBuilderSession + + +class CloverProcessor(BaseLanguageProcessor): + def matches_content(self, content: Element, first_line: str, name: str) -> bool: + return content.tag == "coverage" and bool(content.attrib.get("generated")) + + @sentry_sdk.trace + def process( + self, content: Element, report_builder_session: ReportBuilderSession + ) -> None: + return from_xml(content, report_builder_session) + + +def get_end_of_file(filename, xmlfile): + """ + php reports have shown to include + exrta coverage data that extend + past the source code line count + """ + if filename.endswith(".php"): + for metrics in xmlfile.iter("metrics"): + try: + return int(metrics.attrib["loc"]) + except Exception: + pass + + +def from_xml(xml: Element, report_builder_session: ReportBuilderSession) -> None: + if max_age := report_builder_session.yaml_field( + ("codecov", "max_report_age"), "12h ago" + ): + try: + timestamp = next(xml.iter("coverage")).get("generated") + if "-" in timestamp: + t = timestamp.split("-") + timestamp = t[1] + "-" + t[0] + "-" + t[2] + if timestamp and Date(timestamp) < max_age: + # report expired over 12 hours ago + raise ReportExpiredException("Clover report expired %s" % timestamp) + except StopIteration: + pass + + for file in xml.iter("file"): + filename = file.attrib.get("path") or file.attrib["name"] + + # skip empty file documents + if ( + "{" in filename + or ("/vendor/" in ("/" + filename) and filename.endswith(".php")) + or file.find("line") is None + ): + continue + + _file = report_builder_session.create_coverage_file(filename) + if _file is None: + continue + + # fix extra lines + eof = get_end_of_file(filename, file) + + # process coverage + for line in file.iter("line"): + attribs = line.attrib + ln = int(attribs["num"]) + complexity = None + + # skip line + if ln < 1 or (eof and ln > eof): + continue + + # [typescript] https://github.com/gotwarlost/istanbul/blob/89e338fcb1c8a7dea3b9e8f851aa55de2bc3abee/lib/report/clover.js#L108-L110 + if attribs["type"] == "cond": + _type = CoverageType.branch + t, f = int(attribs["truecount"]), int(attribs["falsecount"]) + if t == f == 0: + coverage = "0/2" + elif t == 0 or f == 0: + coverage = "1/2" + else: + coverage = "2/2" + + elif attribs["type"] == "method": + coverage = int(attribs.get("count") or 0) + _type = CoverageType.method + complexity = int(attribs.get("complexity") or 0) + # + + else: + coverage = int(attribs.get("count") or 0) + _type = CoverageType.line + + # add line to report + _file.append( + ln, + report_builder_session.create_coverage_line( + coverage, + _type, + complexity=complexity, + ), + ) + + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/cobertura.py b/apps/worker/services/report/languages/cobertura.py new file mode 100644 index 0000000000..a6e9d2e4ec --- /dev/null +++ b/apps/worker/services/report/languages/cobertura.py @@ -0,0 +1,212 @@ +import logging +import re +from typing import Sequence + +import sentry_sdk +from lxml.etree import Element +from timestring import Date, TimestringInvalid + +from helpers.exceptions import ReportExpiredException +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import CoverageType, ReportBuilderSession + +log = logging.getLogger(__name__) + + +class CoberturaProcessor(BaseLanguageProcessor): + def matches_content(self, content: Element, first_line: str, name: str) -> bool: + return content.tag in ("coverage", "scoverage") + + @sentry_sdk.trace + def process( + self, content: Element, report_builder_session: ReportBuilderSession + ) -> None: + return from_xml(content, report_builder_session) + + +def Int(value): + try: + return int(value) + except ValueError: + return int(float(value)) + + +def get_sources_to_attempt(xml) -> Sequence[str]: + sources = (source.text for source in xml.iter("source")) + return tuple(s for s in sources if isinstance(s, str) and s.startswith("/")) + + +def from_xml(xml: Element, report_builder_session: ReportBuilderSession) -> None: + # # process timestamp + if max_age := report_builder_session.yaml_field( + ("codecov", "max_report_age"), "12h ago" + ): + try: + timestamp = xml.get("timestamp") + parsed_datetime = Date(timestamp) + is_valid_timestamp = True + except TimestringInvalid: + parsed_datetime = None + is_valid_timestamp = False + + if timestamp and is_valid_timestamp and parsed_datetime < max_age: + # report expired over 12 hours ago + raise ReportExpiredException("Cobertura report expired " + timestamp) + + handle_missing_conditions = report_builder_session.yaml_field( + ("parsers", "cobertura", "handle_missing_conditions"), + False, + ) + partials_as_hits = report_builder_session.yaml_field( + ("parsers", "cobertura", "partials_as_hits"), + False, + ) + + for _class in xml.iter("class"): + filename = _class.attrib["filename"] + if not filename: + continue + _file = report_builder_session.create_coverage_file(filename, do_fix_path=False) + assert _file is not None, ( + "`create_coverage_file` with pre-fixed path is infallible" + ) + + for line in _class.iter("line"): + _line = line.attrib + ln: str | int = _line["number"] + if ln == "undefined": + continue + ln = int(ln) + if ln > 0: + coverage: str | int + _type = CoverageType.line + missing_branches = None + + # coverage + branch = _line.get("branch", "") + condition_coverage = _line.get("condition-coverage", "") + if ( + branch.lower() == "true" + and re.search(r"\(\d+\/\d+\)", condition_coverage) is not None + ): + coverage = condition_coverage.split(" ", 1)[1][1:-1] # 1/2 + _type = CoverageType.branch + else: + coverage = Int(_line.get("hits")) + + # [python] [scoverage] [groovy] Conditions + conditions_text = _line.get("missing-branches", None) + if conditions_text: + conditions = conditions_text.split(",") + if len(conditions) > 1 and set(conditions) == set(("exit",)): + # python: "return [...] missed" + conditions = ["loop", "exit"] + missing_branches = conditions + + else: + # [groovy] embedded conditions + conditions = [ + "%(number)s:%(type)s" % _.attrib + for _ in line.iter("condition") + if _.attrib.get("coverage") != "100%" + ] + if handle_missing_conditions: + if isinstance(coverage, str): + covered_conditions, total_conditions = coverage.split("/") + if len(conditions) < int(total_conditions): + # + # + # + # + # + + # + + coverage_difference = int(total_conditions) - int( + covered_conditions + ) + missing_condition_elements = range( + len(conditions), coverage_difference + ) + conditions.extend( + [ + str(condition) + for condition in missing_condition_elements + ] + ) + else: # previous behaviour + if ( + isinstance(coverage, str) + and coverage[0] == "0" + and len(conditions) < int(coverage.split("/")[1]) + ): + # + # + # + # + # + conditions.extend( + map( + str, + range(len(conditions), int(coverage.split("/")[1])), + ) + ) + if conditions: + missing_branches = conditions + if ( + isinstance(coverage, str) + and not coverage[0] == "0" + and partials_as_hits + ): # if coverage[0] is 0 this is a miss + missing_branches = None + coverage = 1 + _type = CoverageType.line + + _file.append( + ln, + report_builder_session.create_coverage_line( + coverage, + _type, + missing_branches=missing_branches, + ), + ) + + # [scala] [scoverage] + for stmt in _class.iter("statement"): + # scoverage will have repeated data + attr = stmt.attrib + if attr.get("ignored") == "true": + continue + coverage = Int(attr["invocation-count"]) + line_no = int(attr["line"]) + coverage_type = CoverageType.line + if attr["branch"] == "true": + coverage_type = CoverageType.branch + elif attr["method"]: + coverage_type = CoverageType.method + + _file.append( + line_no, + report_builder_session.create_coverage_line( + coverage, + coverage_type, + ), + ) + report_builder_session.append(_file) + + # path rename + path_fixer = report_builder_session.path_fixer + source_path_list = get_sources_to_attempt(xml) + path_name_fixing = [] + + for _class in xml.iter("class"): + filename = _class.attrib["filename"] + fixed_name = path_fixer(filename, bases_to_try=source_path_list) + path_name_fixing.append((filename, fixed_name)) + + # paths with `X-packages` should be sorted to the end + path_name_fixing.sort( + key=lambda a: "/dist-packages/" in a[0] or "/site-packages/" in a[0] + ) + + report_builder_session.resolve_paths(path_name_fixing) diff --git a/apps/worker/services/report/languages/coveralls.py b/apps/worker/services/report/languages/coveralls.py new file mode 100644 index 0000000000..804518d295 --- /dev/null +++ b/apps/worker/services/report/languages/coveralls.py @@ -0,0 +1,35 @@ +import orjson +import sentry_sdk + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import ReportBuilderSession + + +class CoverallsProcessor(BaseLanguageProcessor): + def matches_content(self, content: dict, first_line: str, name: str) -> bool: + return "source_files" in content + + @sentry_sdk.trace + def process( + self, content: dict, report_builder_session: ReportBuilderSession + ) -> None: + return from_json(content, report_builder_session) + + +def from_json(report: dict, report_builder_session: ReportBuilderSession) -> None: + for file in report["source_files"]: + _file = report_builder_session.create_coverage_file(file["name"]) + if _file is None: + continue + + # for some reason, the `coverage` field is either a list directly, + # or a string with a json-encoded list. + coverage: str | list = file["coverage"] + if isinstance(coverage, str): + coverage = orjson.loads(coverage) + + for ln, cov in enumerate(coverage, start=1): + if cov is not None: + _line = report_builder_session.create_coverage_line(cov) + _file.append(ln, _line) + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/csharp.py b/apps/worker/services/report/languages/csharp.py new file mode 100644 index 0000000000..0133e68085 --- /dev/null +++ b/apps/worker/services/report/languages/csharp.py @@ -0,0 +1,117 @@ +from collections import defaultdict +from itertools import repeat + +import sentry_sdk +from lxml.etree import Element +from shared.reports.resources import ReportFile + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import CoverageType, ReportBuilderSession + + +class CSharpProcessor(BaseLanguageProcessor): + def matches_content(self, content: Element, first_line: str, name: str) -> bool: + return content.tag == "CoverageSession" + + @sentry_sdk.trace + def process( + self, content: Element, report_builder_session: ReportBuilderSession + ) -> None: + return from_xml(content, report_builder_session) + + +def _build_branches(branch_gen): + branches = defaultdict(list) + for branch in branch_gen: + if branch.attrib["vc"] == "0": + attribs = dict(branch.attrib) + if "sl" in attribs: + if attribs.get("offsetend") is not None: + branches[int(attribs["sl"])].append( + ("%(offset)s:%(offsetend)s" % attribs) + ) + else: + branches[int(attribs["sl"])].append(("%(offset)s" % attribs)) + return branches + + +def from_xml(xml: Element, report_builder_session: ReportBuilderSession) -> None: + """ + https://github.com/OpenCover/opencover/issues/293#issuecomment-94598145 + @sl - start line + @sc - start column + @el - end line + @ec - end column + @bec - branch count + @bev - branches executed + @vc - statement executed + + """ + + file_by_id: dict[str, ReportFile] = {} + for f in xml.iter("File"): + filename = f.attrib["fullPath"].replace("\\", "/") + _file = report_builder_session.create_coverage_file(filename) + if _file is not None: + file_by_id[f.attrib["uid"]] = _file + + for method in xml.iter("Method"): + fileref = method.find("FileRef") + if fileref is None: + continue + _file = file_by_id.get(fileref.attrib["uid"]) + if _file is None: + continue + + branches = _build_branches(method.iter("BranchPoint")) + + for _type, node in zip(repeat(None), method.iter("SequencePoint")): + attrib = node.attrib.get + sl, el = attrib("sl"), attrib("el") + if sl and el: + complexity = ( + int(attrib("cyclomaticComplexity", 0)) + if _type == CoverageType.method + else None + ) + sl, el = int(sl), int(el) + vc, bec = int(attrib("vc")), attrib("bec") + if bec is not None: + bev = attrib("bev") + if bec != "0": + coverage = "%s/%s" % (bev, bec) + _type = _type or CoverageType.branch + elif vc > 0: + coverage = vc + else: + coverage = 0 + else: + coverage = vc + + coverage_type = _type or CoverageType.line + # spans > 1 line + if el > sl: + for ln in range(sl, el + 1): + _file.append( + ln, + report_builder_session.create_coverage_line( + coverage, + coverage_type, + missing_branches=branches.get(ln), + complexity=complexity, + ), + ) + # spans = 1 line + else: + _file.append( + sl, + report_builder_session.create_coverage_line( + coverage, + coverage_type, + missing_branches=branches.get(sl), + complexity=complexity, + ), + ) + + for _file in file_by_id.values(): + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/dlst.py b/apps/worker/services/report/languages/dlst.py new file mode 100644 index 0000000000..79ed822629 --- /dev/null +++ b/apps/worker/services/report/languages/dlst.py @@ -0,0 +1,51 @@ +from io import BytesIO + +import sentry_sdk + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import ReportBuilderSession + + +class DLSTProcessor(BaseLanguageProcessor): + def matches_content(self, content: bytes, first_line: str, name: str) -> bool: + return content[-7:] == b"covered" + + @sentry_sdk.trace + def process( + self, content: bytes, report_builder_session: ReportBuilderSession + ) -> None: + return from_string(content, report_builder_session) + + +def from_string(string: bytes, report_builder_session: ReportBuilderSession) -> None: + filename = report_builder_session.filepath + if filename: + # src/file.lst => src/file.d + filename = report_builder_session.path_fixer("%sd" % filename[:-3]) + + if not filename: + # file.d => src/file.d + last_line = string[string.rfind(b"\n") :].decode(errors="replace").strip() + filename = last_line.split(" is ", 1)[0] + if filename.startswith("source "): + filename = filename[7:] + + _file = report_builder_session.create_coverage_file(filename) + if _file is None: + return None + + for ln, encoded_line in enumerate(BytesIO(string), start=1): + line = encoded_line.decode(errors="replace").rstrip("\n") + try: + coverage = int(line.split("|", 1)[0].strip()) + _file.append( + ln, + report_builder_session.create_coverage_line( + coverage, + ), + ) + except Exception: + # not a vaild line + pass + + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/elm.py b/apps/worker/services/report/languages/elm.py new file mode 100644 index 0000000000..c7a59b325a --- /dev/null +++ b/apps/worker/services/report/languages/elm.py @@ -0,0 +1,55 @@ +import sentry_sdk + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import ReportBuilderSession + + +class ElmProcessor(BaseLanguageProcessor): + def matches_content(self, content: dict, first_line: str, name: str) -> bool: + return isinstance(content, dict) and bool(content.get("coverageData")) + + @sentry_sdk.trace + def process( + self, content: dict, report_builder_session: ReportBuilderSession + ) -> None: + return from_json(content, report_builder_session) + + +def from_json(json: dict, report_builder_session: ReportBuilderSession) -> None: + for name, data in json["coverageData"].items(): + _file = report_builder_session.create_coverage_file(json["moduleMap"][name]) + if _file is None: + continue + + for sec in data: + cov = sec.get("count", 0) + complexity = sec.get("complexity") + sl, sc = sec["from"]["line"], sec["from"]["column"] + el, ec = sec["to"]["line"], sec["to"]["column"] + _file.append( + sl, + report_builder_session.create_coverage_line( + cov, + complexity=complexity, + partials=[[sc, ec if el == sl else None, cov]], + ), + ) + if el > sl: + for ln in range(sl, el): + _file.append( + ln, + report_builder_session.create_coverage_line( + cov, + complexity=complexity, + ), + ) + _file.append( + sl, + report_builder_session.create_coverage_line( + cov, + complexity=complexity, + partials=[[None, ec, cov]], + ), + ) + + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/flowcover.py b/apps/worker/services/report/languages/flowcover.py new file mode 100644 index 0000000000..ad9de2cd8b --- /dev/null +++ b/apps/worker/services/report/languages/flowcover.py @@ -0,0 +1,54 @@ +import sentry_sdk + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import ReportBuilderSession + + +class FlowcoverProcessor(BaseLanguageProcessor): + def matches_content(self, content: dict, first_line: str, name: str) -> bool: + return isinstance(content, dict) and bool(content.get("flowStatus")) + + @sentry_sdk.trace + def process( + self, content: dict, report_builder_session: ReportBuilderSession + ) -> None: + return from_json(content, report_builder_session) + + +def from_json(json: dict, report_builder_session: ReportBuilderSession) -> None: + for fn, data in json["files"].items(): + _file = report_builder_session.create_coverage_file(fn) + if _file is None: + continue + + for loc in data["expressions"].get("covered_locs", []): + start, end = loc["start"], loc["end"] + partials = ( + [[start["column"], end["column"], 1]] + if start["line"] == end["line"] + else None + ) + _file.append( + start["line"], + report_builder_session.create_coverage_line( + 1, + partials=partials, + ), + ) + + for loc in data["expressions"].get("uncovered_locs", []): + start, end = loc["start"], loc["end"] + partials = ( + [[start["column"], end["column"], 0]] + if start["line"] == end["line"] + else None + ) + _file.append( + start["line"], + report_builder_session.create_coverage_line( + 0, + partials=partials, + ), + ) + + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/gap.py b/apps/worker/services/report/languages/gap.py new file mode 100644 index 0000000000..4844bfb0fc --- /dev/null +++ b/apps/worker/services/report/languages/gap.py @@ -0,0 +1,57 @@ +import typing +from io import BytesIO + +import orjson +import sentry_sdk + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import ReportBuilderSession + + +class GapProcessor(BaseLanguageProcessor): + def matches_content(self, content: typing.Any, first_line: str, name: str) -> bool: + try: + val = content if isinstance(content, dict) else orjson.loads(first_line) + return "Type" in val and "File" in val + except (TypeError, ValueError): + return False + + @sentry_sdk.trace + def process( + self, + content: typing.Any, + report_builder_session: ReportBuilderSession, + ) -> None: + if isinstance(content, dict): + content = orjson.dumps(content) + if isinstance(content, str): + content = content.encode() + return from_string(content, report_builder_session) + + +def from_string(string: bytes, report_builder_session: ReportBuilderSession) -> None: + _file = None + for encoded_line in BytesIO(string): + line_str = encoded_line.decode(errors="replace").rstrip("\n") + if not line_str: + continue + + line = orjson.loads(line_str) + if line["Type"] == "S": + if _file is not None: + report_builder_session.append(_file) + + _file = report_builder_session.create_coverage_file(line["File"]) + + elif _file is not None: + coverage = 0 if line["Type"] == "R" else 1 + _file.append( + line["Line"], + report_builder_session.create_coverage_line( + coverage, + ), + ) + + # append last file + if _file is not None: + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/gcov.py b/apps/worker/services/report/languages/gcov.py new file mode 100644 index 0000000000..074e1dc8da --- /dev/null +++ b/apps/worker/services/report/languages/gcov.py @@ -0,0 +1,210 @@ +import re +from collections import defaultdict +from io import BytesIO + +import sentry_sdk + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import CoverageType, ReportBuilderSession +from services.yaml import read_yaml_field + + +class GcovProcessor(BaseLanguageProcessor): + def matches_content(self, content: bytes, first_line: str, name: str) -> bool: + return b"0:Source:" in content.split(b"\n", 1)[0] + + @sentry_sdk.trace + def process( + self, + content: bytes, + report_builder_session: ReportBuilderSession, + ) -> None: + return from_txt(content, report_builder_session) + + +ignored_lines = re.compile(r"(\{|\})(\s*\/\/.*)?").match +detect_loop = re.compile(r"^\s+(for|while)\s?\(").match +detect_conditional = re.compile(r"^\s+((if\s?\()|(\} else if\s?\())").match + + +def from_txt(string: bytes, report_builder_session: ReportBuilderSession) -> None: + filepath = report_builder_session.filepath + path_fixer = report_builder_session.path_fixer + + line_iterator = iter(BytesIO(string)) + # clean and strip lines + filename = next(line_iterator).decode(errors="replace").rstrip("\n") + filename = filename.split(":")[3].lstrip("./") + if filepath and filepath.endswith(filename + ".gcov"): + filename = path_fixer(filepath[:-5]) or path_fixer(filename) + else: + filename = path_fixer(filename) + if not filename: + return None + + settings = report_builder_session.yaml_field(("parsers", "gcov")) + detect_branches_in_methods = read_yaml_field( + settings, ("branch_detection", "method"), False + ) + detect_branches_in_loops = read_yaml_field( + settings, ("branch_detection", "loop"), False + ) + detect_branches_in_conditions = read_yaml_field( + settings, ("branch_detection", "conditional"), False + ) + detect_branches_in_macros = read_yaml_field( + settings, ("branch_detection", "macro"), False + ) + + ignore = False + ln = None + next_is_func = False + data = None + + _cur_branch_detected = None + _cur_line_branch = None + line_branches = {} + lines = defaultdict(list) + line_types = {} + + for encoded_line in line_iterator: + line = encoded_line.decode(errors="replace").rstrip("\n") + if "LCOV_EXCL_START" in line: + ignore = True + + elif "LCOV_EXCL_END" in line or "LCOV_EXCL_STOP" in line: + ignore = False + + elif ignore: + pass + + elif "LCOV_EXCL_LINE" in line: + pass + + elif line[:4] == "func": + # for next line + next_is_func = True + + elif line[:4] == "bran" and ln in lines: + if _cur_branch_detected is False: + # skip read_yaml_fielding/regexp checks because of repeated branchs + continue + + elif _cur_branch_detected is None: + _cur_branch_detected = False # first set to false, prove me true + + # class + if ( + line_types[ln] == CoverageType.method + and not detect_branches_in_methods + ): + continue + # loop + elif detect_loop(data): + line_types[ln] = CoverageType.branch + if not detect_branches_in_loops: + continue + # conditional + elif detect_conditional(data): + line_types[ln] = CoverageType.branch + if not detect_branches_in_conditions: + continue + # else macro + elif not detect_branches_in_macros: + continue + + _cur_branch_detected = True # proven true + _cur_line_branch = line_branches.setdefault(ln, [0, 0]) + + # add a hit + if "taken 0" not in line and "never executed" not in line: + _cur_line_branch[0] += 1 + + # add to total + _cur_line_branch[1] += 1 + + elif line[:4] == "call": + continue + + else: + _cur_branch_detected = None + _cur_line_branch = None + + line = line.split(":", 2) + if len(line) != 3: + ln = None + continue + + elif line[2].strip() == "}": + # skip ending bracket lines + continue + + elif line[2].startswith("@implementation"): + # skip @implementation string; + continue + + if filename.endswith(".swift"): + # swift if reversed + ln, hit, data = tuple(line) + else: + hit, ln, data = tuple(line) + + if ignored_lines(data): + # skip bracket lines + ln = None + continue + + elif "-" in hit: + ln = None + continue + + hit = hit.strip() + try: + ln = int(ln.strip()) + except Exception: + continue + + if hit == "#####": + if data.strip().startswith(("inline", "static")): + ln = None + continue + + coverage = 0 + + elif hit == "=====": + coverage = 0 + + else: + try: + coverage = int(hit) + except Exception: + # https://app.getsentry.com/codecov/v4/issues/125373723/ + ln = None + continue + + if next_is_func: + line_types[ln] = CoverageType.method + else: + line_types[ln] = CoverageType.line + lines[ln].append(coverage) + + next_is_func = False + + _file = report_builder_session.create_coverage_file(filename, do_fix_path=False) + for ln, coverages in lines.items(): + _type = line_types[ln] + branches = line_branches.get(ln) + if branches: + coverage = "%s/%s" % tuple(branches) + _file.append( + ln, + report_builder_session.create_coverage_line(coverage, _type), + ) + else: + for coverage in coverages: + _file.append( + ln, + report_builder_session.create_coverage_line(coverage, _type), + ) + + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/go.py b/apps/worker/services/report/languages/go.py new file mode 100644 index 0000000000..1ccbfdbf14 --- /dev/null +++ b/apps/worker/services/report/languages/go.py @@ -0,0 +1,205 @@ +from collections import defaultdict +from io import BytesIO +from itertools import groupby + +import sentry_sdk +from shared.utils import merge +from shared.utils.merge import LineType, line_type, partials_to_line + +from helpers.exceptions import CorruptRawReportError +from services.report.languages.base import BaseLanguageProcessor +from services.report.languages.helpers import Region, SourceLocation +from services.report.report_builder import ReportBuilderSession + + +class GoProcessor(BaseLanguageProcessor): + def matches_content(self, content: bytes, first_line: str, name: str) -> bool: + return content[:6] == b"mode: " or ".go:" in first_line + + @sentry_sdk.trace + def process( + self, content: bytes, report_builder_session: ReportBuilderSession + ) -> None: + return from_txt(content, report_builder_session) + + +def from_txt(string: bytes, report_builder_session: ReportBuilderSession) -> None: + partials_as_hits = report_builder_session.yaml_field( + ("parsers", "go", "partials_as_hits"), + False, + ) + + # Process the bytes from uploaded report to intermediary representation + files = process_bytes_into_files(string) + + for filename, lines in files.items(): + _file = report_builder_session.create_coverage_file(filename) + if _file is None: + continue + + for ln, partials in lines.items(): + best_in_partials = max(map(lambda p: p[2], partials)) + partials = combine_partials(partials) + if partials: + cov = partials_to_line(partials) + cov_to_use = cov + else: + cov_to_use = best_in_partials + if partials_as_hits and line_type(cov_to_use) == LineType.partial: + cov_to_use = 1 + + _line = report_builder_session.create_coverage_line(cov_to_use) + _file.append(ln, _line) + + report_builder_session.append(_file) + + +def process_bytes_into_files(string: bytes) -> dict[str, dict[int, set]]: + """ + mode: count + github.com/codecov/sample_go/sample_go.go:7.14,9.2 1 1 + github.com/codecov/sample_go/sample_go.go:11.26,13.2 1 1 + github.com/codecov/sample_go/sample_go.go:15.19,17.2 1 0 + + Ending bracket is here v + github.com/codecov/sample_go/sample_go.go:15.19,17.2 1 0 + + All other continuation > .2 should continue + github.com/codecov/sample_go/sample_go.go:15.19,17.9 1 0 + + Need to be cautious of customers who have reports merged in the following way: + FILE:1.0,2.0 1 0 + ... + FILE:1.0,2.0 1 1 + ... + FILE:1.0,2.0 1 0 + Need to respect the coverage + + Line format explanation: + - https://github.com/golang/go/blob/0104a31b8fbcbe52728a08867b26415d282c35d2/src/cmd/cover/profile.go#L56 + - `name.go:line.column,line.column numberOfStatements count` + """ + + files: dict[str, dict[int, set]] = {} + + for encoded_line in BytesIO(string): + line = encoded_line.decode(errors="replace").rstrip("\n") + if not line or line.startswith("mode: "): + continue + + split = line.split(":", 1) + # File outline e.g., "github.com/nfisher/rsqf/rsqf.go:19: calcP 100.0%" + if len(split) < 2 or not split[1] or split[1].endswith("%"): + continue + + filename = split[0] + try: + region = parse_coverage(split[1]) + except ValueError: + # FIXME: do we actually want to raise an error here? + # Why not just skip over invalid lines, as the coverage file likely + # contains other valid lines we can use. + raise CorruptRawReportError( + "name.go:line.column,line.column numberOfStatements hits", + "Go coverage line does not match expected format", + ) + + lines = files.setdefault(filename, defaultdict(set)) + + # add start of line + if region.start.line == region.end.line: + lines[region.start.line].add( + (region.start.column, region.end.column, region.hits) + ) + else: + lines[region.start.line].add((region.start.column, None, region.hits)) + # add middles + for ln in range(region.start.line + 1, region.end.line): + lines[ln].add((0, None, region.hits)) + if region.end.column > 2: + # add end of line + lines[region.end.line].add((None, region.end.column, region.hits)) + + return files + + +def parse_coverage(line: str) -> Region: + region_str, _num_statements, hits = line.split(" ", 2) + start, end = region_str.split(",", 1) + start_line, start_column = start.split(".", 1) + end_line, end_column = end.split(".", 1) + return Region( + start=SourceLocation(line=int(start_line), column=int(start_column)), + end=SourceLocation(line=int(end_line), column=int(end_column)), + hits=int(hits), + ) + + +def combine_partials(partials): + """ + [(INCLUSIVE, EXCLUSIVE, HITS), ...] + | . . . . . | + in: 0+ (2, None, 0) + in: 1 1 (1, 3, 1) + out: 1 1 1 0 0 + out: 1 1 0+ (1, 3, 1), (4, None, 0) + """ + # only 1 partial: return same + if len(partials) == 1: + return list(partials) + + columns = defaultdict(list) + # fill in the partials WITH end values: (_, X, _) + for sc, ec, cov in partials: + if ec is not None: + for c in range(sc or 0, ec): + columns[c].append(cov) + + # get the last column number (+1 for exclusiveness) + lc = ( + max(columns.keys()) if columns else max([sc or 0 for (sc, ec, cov) in partials]) + ) + 1 + # hits for (lc, None, eol) + eol = [] + + # fill in the partials WITHOUT end values: (_, None, _) + for sc, ec, cov in partials: + if ec is None: + for c in range(sc or 0, lc): + columns[c].append(cov) + eol.append(cov) + + columns = [(c, merge.merge_all(cov)) for c, cov in columns.items()] + + # sum all the line hits && sort and group lines based on hits + columns = groupby(sorted(columns), lambda c: c[1]) + + results = [] + for cov, cols in columns: + # unpack iter + cols = list(cols) + # sc from first column + # ec from last (or +1 if singular) + results.append([cols[0][0], (cols[-1] if cols else cols[0])[0] + 1, cov]) + + # remove duds + if results: + fp = results[0] + if fp[0] == 0 and fp[1] == 1: + results.pop(0) + if not results: + return [[0, None, fp[2]]] + + # if there is eol data + if eol: + eol = merge.merge_all(eol) + # if the last partial ec == lc && same hits + lr = results[-1] + if lr[1] == lc and lr[2] == eol: + # then replace the last partial with no end + results[-1] = [lr[0], None, eol] + else: + # else append a new eol partial + results.append([lc, None, eol]) + + return results or None diff --git a/apps/worker/services/report/languages/helpers.py b/apps/worker/services/report/languages/helpers.py new file mode 100644 index 0000000000..9b66514553 --- /dev/null +++ b/apps/worker/services/report/languages/helpers.py @@ -0,0 +1,34 @@ +from dataclasses import dataclass + +from lxml.etree import Element + + +def remove_non_ascii(string: str) -> str: + # ASCII control characters <=31, 127 + # Extended ASCII characters: >=128 + return "".join(c if 31 < ord(c) < 127 else "" for c in string) + + +def child_text(parent: Element, element: str) -> str: + """ + Returns the text content of the first element of type `element` of `parent`. + + This defaults to the empty string if no child is found, or the child does not have any text. + """ + child = parent.find(element) + if child is None: + return "" + return child.text or "" + + +@dataclass +class SourceLocation: + line: int + column: int + + +@dataclass +class Region: + start: SourceLocation + end: SourceLocation + hits: int diff --git a/apps/worker/services/report/languages/jacoco.py b/apps/worker/services/report/languages/jacoco.py new file mode 100644 index 0000000000..4bacd31beb --- /dev/null +++ b/apps/worker/services/report/languages/jacoco.py @@ -0,0 +1,150 @@ +import logging +from collections import defaultdict + +import sentry_sdk +from lxml.etree import Element +from shared.utils.merge import LineType, branch_type +from timestring import Date + +from helpers.exceptions import ReportExpiredException +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import CoverageType, ReportBuilderSession + +log = logging.getLogger(__name__) + + +class JacocoProcessor(BaseLanguageProcessor): + def matches_content(self, content: Element, first_line: str, name: str) -> bool: + return content.tag == "report" + + @sentry_sdk.trace + def process( + self, content: Element, report_builder_session: ReportBuilderSession + ) -> None: + return from_xml(content, report_builder_session) + + +def from_xml(xml: Element, report_builder_session: ReportBuilderSession) -> None: + """ + nr = line number + mi = missed instructions + ci = covered instructions + mb = missed branches + cb = covered branches + """ + path_fixer = report_builder_session.path_fixer + if max_age := report_builder_session.yaml_field( + ("codecov", "max_report_age"), "12h ago" + ): + try: + timestamp = next(xml.iter("sessioninfo")).get("start") + if timestamp and Date(timestamp) < max_age: + # report expired over 12 hours ago + raise ReportExpiredException("Jacoco report expired %s" % timestamp) + + except StopIteration: + pass + + project = xml.attrib.get("name", "") + project = "" if " " in project else project.strip("/") + + partials_as_hits = report_builder_session.yaml_field( + ("parsers", "jacoco", "partials_as_hits"), False + ) + + def try_to_fix_path(path: str) -> str | None: + if project: + # project/package/path + filename = path_fixer("%s/%s" % (project, path)) + if filename: + return filename + + # project/src/main/java/package/path + filename = path_fixer("%s/src/main/java/%s" % (project, path)) + if filename: + return filename + + # package/path + return path_fixer(path) + + for package in xml.iter("package"): + base_name = package.attrib["name"] + + file_method_complixity: dict[str, dict[int, tuple[int, int]]] = defaultdict( + dict + ) + # Classes complexity + for _class in package.iter("class"): + class_name = _class.attrib["name"] + if "$" not in class_name: + method_complixity = file_method_complixity[class_name] + # Method Complexity + for method in _class.iter("method"): + ln = int(method.attrib.get("line", 0)) + if ln > 0: + for counter in method.iter("counter"): + if counter.attrib["type"] == "COMPLEXITY": + m = int(counter.attrib["missed"]) + c = int(counter.attrib["covered"]) + method_complixity[ln] = (c, m + c) + break + + # Statements + for source in package.iter("sourcefile"): + source_name = "%s/%s" % (base_name, source.attrib["name"]) + filename = try_to_fix_path(source_name) + if filename is None: + continue + + method_complixity = file_method_complixity[source_name.split(".")[0]] + + _file = report_builder_session.create_coverage_file( + filename, do_fix_path=False + ) + assert _file is not None, ( + "`create_coverage_file` with pre-fixed path is infallible" + ) + + for line in source.iter("line"): + attr = line.attrib + cov: int | str + if attr["mb"] != "0": + cov = "%s/%s" % (attr["cb"], int(attr["mb"]) + int(attr["cb"])) + coverage_type = CoverageType.branch + + elif attr["cb"] != "0": + cov = "%s/%s" % (attr["cb"], attr["cb"]) + coverage_type = CoverageType.branch + + else: + cov = int(attr["ci"]) + coverage_type = CoverageType.line + + if ( + coverage_type == CoverageType.branch + and branch_type(cov) == LineType.partial + and partials_as_hits + ): + cov = 1 + + ln = int(attr["nr"]) + if ln > 0: + complexity = method_complixity.get(ln) + if complexity: + coverage_type = CoverageType.method + # add line to file + _file.append( + ln, + report_builder_session.create_coverage_line( + cov, + coverage_type, + complexity=complexity, + ), + ) + else: + log.warning( + f"Jacoco report has an invalid coverage line: nr={ln}. Skipping processing line." + ) + + # append file to report + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/jetbrainsxml.py b/apps/worker/services/report/languages/jetbrainsxml.py new file mode 100644 index 0000000000..1bf6bbf5ad --- /dev/null +++ b/apps/worker/services/report/languages/jetbrainsxml.py @@ -0,0 +1,55 @@ +import sentry_sdk +from lxml.etree import Element +from shared.reports.resources import ReportFile + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import ReportBuilderSession + + +class JetBrainsXMLProcessor(BaseLanguageProcessor): + def matches_content(self, content: Element, first_line: str, name: str) -> bool: + return content.tag == "Root" + + @sentry_sdk.trace + def process( + self, content: Element, report_builder_session: ReportBuilderSession + ) -> None: + return from_xml(content, report_builder_session) + + +def from_xml(xml: Element, report_builder_session: ReportBuilderSession) -> None: + file_by_id: dict[str, ReportFile] = {} + for f in xml.iter("File"): + filename = f.attrib["Name"].replace("\\", "/") + _file = report_builder_session.create_coverage_file(filename) + if _file is not None: + file_by_id[str(f.attrib["Index"])] = _file + + for statement in xml.iter("Statement"): + _file = file_by_id.get(str(statement.attrib["FileIndex"])) + if _file is None: + continue + + sl = int(statement.attrib["Line"]) + el = int(statement.attrib["EndLine"]) + sc = int(statement.attrib["Column"]) + ec = int(statement.attrib["EndColumn"]) + cov = 1 if statement.attrib["Covered"] == "True" else 0 + if sl == el: + _file.append( + sl, + report_builder_session.create_coverage_line( + cov, + partials=[[sc, ec, cov]], + ), + ) + else: + _file.append( + sl, + report_builder_session.create_coverage_line( + cov, + ), + ) + + for content in file_by_id.values(): + report_builder_session.append(content) diff --git a/apps/worker/services/report/languages/lcov.py b/apps/worker/services/report/languages/lcov.py new file mode 100644 index 0000000000..319e7e7cbb --- /dev/null +++ b/apps/worker/services/report/languages/lcov.py @@ -0,0 +1,195 @@ +import logging +from collections import defaultdict +from decimal import Decimal, InvalidOperation +from io import BytesIO + +import sentry_sdk +from shared.reports.resources import ReportFile + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import CoverageType, ReportBuilderSession + +log = logging.getLogger(__name__) + + +class LcovProcessor(BaseLanguageProcessor): + def matches_content(self, content: bytes, first_line: str, name: str) -> bool: + return b"\nend_of_record" in content + + @sentry_sdk.trace + def process( + self, content: bytes, report_builder_session: ReportBuilderSession + ) -> None: + return from_txt(content, report_builder_session) + + +def from_txt(reports: bytes, report_builder_session: ReportBuilderSession) -> None: + # http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php + # merge same files + for string in reports.split(b"\nend_of_record"): + if (_file := _process_file(string, report_builder_session)) is not None: + report_builder_session.append(_file) + + +def _process_file( + doc: bytes, report_builder_session: ReportBuilderSession +) -> ReportFile | None: + branches: dict[str, dict[str, int]] = defaultdict(dict) + fn_lines: set[str] = set() # lines of function definitions + + JS = False + CPP = False + skip_lines: list[str] = [] + _file: ReportFile | None = None + + for encoded_line in BytesIO(doc): + line = encoded_line.decode(errors="replace").rstrip("\n") + if line == "" or ":" not in line: + continue + + method, content = line.split(":", 1) + content = content.strip() + if method in ("TN", "LF", "LH", "FNF", "FNH", "BRF", "BRH", "FNDA"): + # TN: test title + # LF: lines found + # LH: lines hit + # FNF: functions found + # FNH: functions hit + # BRF: branches found + # BRH: branches hit + # FNDA: function data + continue + + if method == "SF": + """ + For each source file referenced in the .da file, there is a section + containing filename and coverage data: + + SF: + """ + # file name + _file = report_builder_session.create_coverage_file(content) + JS = content[-3:] == ".js" + CPP = content[-4:] == ".cpp" + continue + + if _file is None: + return None + + if method == "DA": + """ + Then there is a list of execution counts for each instrumented line + (i.e. a line which resulted in executable code): + + DA:,[,] + """ + # DA:,[,] + if line.startswith("undefined,"): + continue + + split = content.split(",", 2) + if len(split) < 2: + continue + line_str = split[0] + hit = split[1] + + if line_str in ("", "undefined") or hit in ("", "undefined"): + continue + if line_str[0] in ("0", "n") or hit[0] in ("=", "s"): + continue + + try: + ln = int(line_str) + cov = parse_int(hit) + except (ValueError, InvalidOperation): + continue + + cov = max(cov, 0) # clamp to 0 + + _line = report_builder_session.create_coverage_line(cov) + _file.append(ln, _line) + + elif method == "FN" and not JS: + """ + Following is a list of line numbers for each function name found in the + source file: + + FN:, + """ + + split = content.split(",", 1) + if len(split) < 2: + continue + line_str, name = split + + if CPP and name[:2] in ("_Z", "_G"): + skip_lines.append(line_str) + continue + + fn_lines.add(line_str) + + elif method == "BRDA" and not JS: + """ + Branch coverage information is stored with one line per branch: + + BRDA:,,, + + Block number and branch number are gcc internal IDs for the branch. + Taken is either "-" if the basic block containing the branch was never + executed or a number indicating how often that branch was taken. + """ + # BRDA:,,, + split = content.split(",", 3) + if len(split) < 4: + continue + line_str, block, branch, taken = split + + if line_str == "1" and _file.name.endswith(".ts"): + continue + + elif line_str not in ("0", ""): + branches[line_str]["%s:%s" % (block, branch)] = ( + 0 if taken in ("-", "0") else 1 + ) + + if _file is None: + return None + + # remove skipped branches + for sl in skip_lines: + branches.pop(sl, None) + + # work branches + for line_str, br in branches.items(): + try: + ln = int(line_str) + except ValueError: + continue + + branch_num = len(br.values()) + branch_sum = sum(br.values()) + missing_branches = [bid for bid, cov in br.items() if cov == 0] + + coverage = f"{branch_sum}/{branch_num}" + coverage_type = ( + CoverageType.method if line_str in fn_lines else CoverageType.branch + ) + + _line = report_builder_session.create_coverage_line( + coverage, + coverage_type, + missing_branches=(missing_branches if missing_branches != [] else None), + ) + # instead of using `.append`/merge, this rather overwrites the line: + _file[ln] = _line + + return _file + + +def parse_int(n: str) -> int: + if n.isnumeric(): + return int(n) + + # Huge ints may be expressed in scientific notation. + # int(float(hit)) may lose precision, but Decimal shouldn't. + return int(Decimal(n)) diff --git a/apps/worker/services/report/languages/lua.py b/apps/worker/services/report/languages/lua.py new file mode 100644 index 0000000000..6a835780e7 --- /dev/null +++ b/apps/worker/services/report/languages/lua.py @@ -0,0 +1,48 @@ +import re + +import sentry_sdk + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import ReportBuilderSession + + +class LuaProcessor(BaseLanguageProcessor): + def matches_content(self, content: bytes, first_line: str, name: str) -> bool: + return content[:7] == b"=======" + + @sentry_sdk.trace + def process( + self, content: bytes, report_builder_session: ReportBuilderSession + ) -> None: + return from_txt(content, report_builder_session) + + +docs = re.compile(r"^=+\n", re.M).split + + +def from_txt(input: bytes, report_builder_session: ReportBuilderSession) -> None: + _file = None + for line in docs(input.decode(errors="replace").replace("\t", " ")): + line = line.rstrip() + if line == "Summary": + _file = None + + elif line.endswith((".lua", ".lisp")): + _file = report_builder_session.create_coverage_file(line) + + elif _file is not None: + for ln, source in enumerate(line.splitlines(), start=1): + try: + cov = source.strip().split(" ")[0] + cov = 0 if cov[-2:] in ("*0", "0") else int(cov) + _file.append( + ln, + report_builder_session.create_coverage_line( + cov, + ), + ) + + except Exception: + pass + + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/mono.py b/apps/worker/services/report/languages/mono.py new file mode 100644 index 0000000000..cfdd25eb27 --- /dev/null +++ b/apps/worker/services/report/languages/mono.py @@ -0,0 +1,48 @@ +import sentry_sdk +from lxml.etree import Element +from shared.reports.resources import ReportFile + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import ReportBuilderSession + + +class MonoProcessor(BaseLanguageProcessor): + def matches_content(self, content: Element, first_line: str, name: str) -> bool: + return content.tag == "coverage" and content.find("assembly") is not None + + @sentry_sdk.trace + def process( + self, content: Element, report_builder_session: ReportBuilderSession + ) -> None: + return from_xml(content, report_builder_session) + + +def from_xml(xml: Element, report_builder_session: ReportBuilderSession) -> None: + files: dict[str, ReportFile | None] = {} + + # loop through methods + for method in xml.iter("method"): + filename = method.attrib["filename"] + if filename not in files: + _file = report_builder_session.create_coverage_file(filename) + files[filename] = _file + + _file = files[filename] + if _file is None: + continue + + # loop through statements + for line in method.iter("statement"): + attr = line.attrib + coverage = int(attr["counter"]) + + _file.append( + int(attr["line"]), + report_builder_session.create_coverage_line( + coverage, + ), + ) + + for _file in files.values(): + if _file is not None: + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/node.py b/apps/worker/services/report/languages/node.py new file mode 100644 index 0000000000..323377a97b --- /dev/null +++ b/apps/worker/services/report/languages/node.py @@ -0,0 +1,404 @@ +from collections import defaultdict +from fractions import Fraction + +import sentry_sdk +from shared.reports.resources import ReportFile +from shared.utils.merge import partials_to_line + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import CoverageType, ReportBuilderSession + + +class NodeProcessor(BaseLanguageProcessor): + def matches_content(self, content: dict, first_line: str, name: str) -> bool: + return isinstance(content, dict) and all( + isinstance(data, dict) for data in content.values() + ) + + @sentry_sdk.trace + def process( + self, content: dict, report_builder_session: ReportBuilderSession + ) -> None: + return from_json(content, report_builder_session) + + +def get_line_coverage(location, cov, line_type): + if location.get("skip"): + return None, None, None + + sl, sc, el, ec = get_location(location) + + if not sl or (sc + 1 == ec and sl == el): + return None, None, None + + if line_type != "m" and sl == el and sc != 0: + partial = [sc, ec, cov] + else: + partial = None + + return sl, cov, partial + + +def get_location(node): + try: + if "loc" in node: + return ( + node["loc"]["start"]["line"], + node["loc"]["start"]["column"], + node["loc"]["end"]["line"], + node["loc"]["end"]["column"], + ) + elif "start" in node: + return ( + node["start"]["line"], + node["start"]["column"], + node["end"]["line"], + node["end"]["column"], + ) + else: + return ( + node["locations"][0]["start"]["line"], + node["locations"][0]["start"]["column"], + node["locations"][-1]["end"]["line"], + node["locations"][-1]["end"]["column"], + ) + except Exception: + return (None, None, None, None) + + +def must_be_dict(value): + if not isinstance(value, dict): + return {} + else: + return value + + +def next_from_json( + report_dict: dict, report_builder_session: ReportBuilderSession +) -> None: + path_fixer = report_builder_session.path_fixer + + for filename, data in report_dict.items(): + filename = path_fixer(filename) or path_fixer( + filename.replace("lib/", "src/", 1) + ) + if filename is None: + continue + _file = report_builder_session.create_coverage_file(filename, do_fix_path=False) + + if "lineData" in data: + jscoverage(_file, data, report_builder_session) + report_builder_session.append(_file) + continue + + if data.get("data"): + # why. idk. node is like that. + data = data["data"] + + ifs = {} + _ifends = {} + for bid, branch in must_be_dict(data.get("branchMap")).items(): + if branch.get("skip") is True: + continue + if branch.get("type") == "if": + # first skip ifs + sl, sc, el, ec = get_location(branch) + ifs[sl] = (sc, el, ec) + + else: + line_parts = defaultdict(list) + line_cov = defaultdict(list) + for lid, location in enumerate(branch["locations"]): + ln, cov, partials = get_line_coverage( + location, data["b"][bid][lid], "b" + ) + if ln: + line_parts[ln].append(partials) + line_cov[ln].append(cov) + if ln == location["end"]["line"]: + _ifends[location["end"]["line"]] = location["end"]["column"] + + for ln, partials in line_parts.items(): + partials = list(filter(None, partials)) + if len(partials) > 1: + branches = [ + str(i) + for i, partial in enumerate(partials) + if partial and partial[2] == 0 + ] + cov = "%d/%d" % ( + len(partials) - len(branches), + len(partials), + ) + partials = sorted(partials, key=lambda p: p[0]) + else: + branches = None + cov = line_cov[ln][0] + partials = None + + _file.append( + ln, + report_builder_session.create_coverage_line( + cov, + CoverageType.branch, + partials=partials, + missing_branches=branches, + ), + ) + + # statements + inlines = {} + line_parts = defaultdict(list) + line_cov = defaultdict(list) + for sid, statement in must_be_dict(data.get("statementMap")).items(): + if statement.get("skip") is True: + continue + + ln, cov, partials = get_line_coverage(statement, data["s"][sid], None) + if ln: + sl, sc, el, ec = get_location(statement) + if ifs.get(ln) == (sc, el, ec): + # we will chop it of later + if partials: + inlines[ln] = partials + + else: + line_parts[ln].append(partials) + line_cov[ln].append(cov) + + for ln, partials in line_parts.items(): + partials = sorted(filter(None, partials), key=lambda p: p[0]) + cov = line_cov[ln][0] + line = _file.get(ln) + if line and line.sessions[0].partials is not None: + continue + else: + _file.append( + ln, + report_builder_session.create_coverage_line( + cov, + partials=partials, + ), + ) + + # NOTE: All the modifications done here rely on *overwriting* the given line record using index notation. + # Using `.append` in this block of code will lead to wrong results as it would *merge* the records. + for bid, branch in must_be_dict(data.get("branchMap")).items(): + # single stmt ifs only + if branch.get("skip") is True or branch.get("type") != "if": + continue + + sl, sc, el, ec = get_location(branch) + if sl: + branches = data["b"][bid] + tb = len(branches) + cov = "%s/%s" % (tb - branches.count(0), tb) + mb = [str(i) for i, b in enumerate(branches) if b == 0] + + line = _file.get(sl) + if line: + inline_part = inlines.pop(sl, None) + if inline_part: + cur_partials = line.sessions[-1].partials + if not cur_partials: + _, cov, partials = get_line_coverage(branch, cov, "b") + _file[sl] = report_builder_session.create_coverage_line( + cov, + CoverageType.branch, + missing_branches=mb, + partials=partials, + ) + continue + + sc, ec = sc + 4, cur_partials[0][0] - 2 + _isc, iec, icov = inline_part + if sc > ec: + cur_partials.append([cur_partials[-1][1] + 2, iec, icov]) + _file[sl] = report_builder_session.create_coverage_line( + cov, + CoverageType.branch, + missing_branches=mb, + partials=cur_partials, + ) + else: + partials = [[sc, ec, cov]] + for p in cur_partials: + if (p[0], p[1]) != (ec + 2, iec): + # add these partials + partials.append(p) + elif p[2] == 0 or isinstance(p[2], str): + # dont add trimmed, this part was missed + partials.append(p) + else: + partials.append([ec + 2, iec, icov]) + _file[sl] = report_builder_session.create_coverage_line( + cov, + CoverageType.branch, + missing_branches=mb, + partials=sorted(partials, key=lambda p: p[0]), + ) + + else: + # if ( exp && expr ) + # change to branch + _file[sl] = report_builder_session.create_coverage_line( + cov, + CoverageType.branch, + missing_branches=mb, + partials=_file[sl].sessions[-1].partials, + ) + + else: + _file.append( + sl, + report_builder_session.create_coverage_line( + cov, + CoverageType.branch, + missing_branches=mb, + ), + ) + + for fid, func in must_be_dict(data["fnMap"]).items(): + if func.get("skip") is not True: + ln, cov, partials = get_line_coverage(func, data["f"][fid], "m") + if ln: + _file.append( + ln, + report_builder_session.create_coverage_line( + cov, + CoverageType.method, + ), + ) + + report_builder_session.append(_file) + + +def _location_to_int(location: dict) -> int | None: + if "loc" in location: + location = location["loc"] + + if location.get("skip"): + return None + + elif location["start"].get("line", 0) == 0: + return None + + return int(location["start"]["line"]) + + +def _jscoverage_eval_partial(partial): + return [ + partial["position"], + partial["position"] + partial["nodeLength"], + Fraction( + "{0}/2".format( + (1 if partial["evalTrue"] else 0) + (1 if partial["evalFalse"] else 0) + ) + ), + # It seems like the above line on Python2 would make something in `partials_to_line` always return True + ] + + +def jscoverage( + _file: ReportFile, data: dict, report_builder_session: ReportBuilderSession +): + branches = { + ln: map(_jscoverage_eval_partial, branchData[1:]) + for ln, branchData in must_be_dict(data["branchData"]).items() + } + + for ln, coverage in enumerate(data["lineData"]): + if coverage is not None: + partials = branches.get(str(ln)) + if partials: + partials = list(partials) + coverage = partials_to_line(partials) + _file.append( + ln, + report_builder_session.create_coverage_line( + coverage, + CoverageType.branch if partials else CoverageType.line, + partials=partials, + ), + ) + + +def from_json(report_dict: dict, report_builder_session: ReportBuilderSession) -> None: + enable_partials = report_builder_session.yaml_field( + ("parsers", "javascript", "enable_partials"), + False, + ) + path_fixer = report_builder_session.path_fixer + + if enable_partials: + if next(iter(report_dict.items()))[0].endswith(".js"): + # only javascript is supported ATM + return next_from_json(report_dict, report_builder_session) + + for filename, data in report_dict.items(): + filename = path_fixer(filename) or path_fixer( + filename.replace("lib/", "src/", 1) + ) + if filename is None: + continue + _file = report_builder_session.create_coverage_file(filename, do_fix_path=False) + + if data.get("data"): + # why. idk. node is like that. + data = data["data"] + + if "lineData" in data: + jscoverage(_file, data, report_builder_session) + report_builder_session.append(_file) + continue + + if "linesCovered" in data: + for ln, coverage in data["linesCovered"].items(): + _file.append( + int(ln), + report_builder_session.create_coverage_line( + coverage, + ), + ) + report_builder_session.append(_file) + continue + + # statements + for sid, statement in must_be_dict(data.get("statementMap")).items(): + if statement.get("skip") is not True: + location_int = _location_to_int(statement) + if location_int: + _file.append( + location_int, + report_builder_session.create_coverage_line( + data["s"][sid], + ), + ) + + for bid, branch in must_be_dict(data.get("branchMap")).items(): + if branch.get("skip") is not True: + # [FUTURE] we can record branch positions in the session + for lid, location in enumerate(branch["locations"]): + location_int = _location_to_int(location) + if location_int: + _file.append( + location_int, + report_builder_session.create_coverage_line( + data["b"][bid][lid], + CoverageType.branch, + ), + ) + + for fid, func in must_be_dict(data.get("fnMap")).items(): + if func.get("skip") is not True: + location_int = _location_to_int(func["loc"]) + if location_int: + _file.append( + location_int, + report_builder_session.create_coverage_line( + data["f"][fid], + CoverageType.method, + ), + ) + + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/pycoverage.py b/apps/worker/services/report/languages/pycoverage.py new file mode 100644 index 0000000000..2bab823ce5 --- /dev/null +++ b/apps/worker/services/report/languages/pycoverage.py @@ -0,0 +1,53 @@ +import sentry_sdk + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import ReportBuilderSession, SpecialLabelsEnum + +COVERAGE_HIT = 1 +COVERAGE_MISS = 0 + + +class PyCoverageProcessor(BaseLanguageProcessor): + def matches_content(self, content: dict, first_line: str, name: str) -> bool: + meta = "meta" in content and content["meta"] + return "files" in content and isinstance(meta, dict) and "show_contexts" in meta + + @sentry_sdk.trace + def process( + self, content: dict, report_builder_session: ReportBuilderSession + ) -> None: + labels_table = content.get("labels_table", {}) + + for filename, file_coverage in content["files"].items(): + _file = report_builder_session.create_coverage_file(filename) + if _file is None: + continue + + lines_and_coverage = [ + (COVERAGE_HIT, ln) for ln in file_coverage["executed_lines"] + ] + [(COVERAGE_MISS, ln) for ln in file_coverage["missing_lines"]] + for cov, ln in lines_and_coverage: + if ln > 0: + label_list_of_lists = [ + [_normalize_label(labels_table, testname)] + for testname in file_coverage.get("contexts", {}).get( + str(ln), [] + ) + ] + _line = report_builder_session.create_coverage_line( + cov, + labels_list_of_lists=label_list_of_lists, + ) + _file.append(ln, _line) + report_builder_session.append(_file) + + +def _normalize_label(labels_table: dict[str, str], testname: int | float | str) -> str: + if isinstance(testname, int) or isinstance(testname, float): + # This is from a compressed report. + # Pull label from the labels_table + # But the labels_table keys are strings, because of JSON format + testname = labels_table[str(testname)] + if testname == "": + return SpecialLabelsEnum.CODECOV_ALL_LABELS_PLACEHOLDER.corresponding_label + return testname.split("|", 1)[0] diff --git a/apps/worker/services/report/languages/rlang.py b/apps/worker/services/report/languages/rlang.py new file mode 100644 index 0000000000..dd95f2ee63 --- /dev/null +++ b/apps/worker/services/report/languages/rlang.py @@ -0,0 +1,42 @@ +import sentry_sdk + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import ReportBuilderSession + + +class RlangProcessor(BaseLanguageProcessor): + def matches_content(self, content: dict, first_line: str, name: str) -> bool: + return isinstance(content, dict) and content.get("uploader") == "R" + + @sentry_sdk.trace + def process( + self, content: dict, report_builder_session: ReportBuilderSession + ) -> None: + return from_json(content, report_builder_session) + + +def from_json(data_dict: dict, report_builder_session: ReportBuilderSession) -> None: + """ + Report example + + uploader: R + files: [] + name: + coverage: [null] + """ + + for data in data_dict["files"]: + _file = report_builder_session.create_coverage_file(data["name"]) + if _file is None: + continue + + for ln, cov in enumerate(data["coverage"]): + if cov is not None: + _file.append( + ln, + report_builder_session.create_coverage_line( + int(cov), + ), + ) + + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/salesforce.py b/apps/worker/services/report/languages/salesforce.py new file mode 100644 index 0000000000..37db59bec9 --- /dev/null +++ b/apps/worker/services/report/languages/salesforce.py @@ -0,0 +1,32 @@ +import sentry_sdk + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import ReportBuilderSession + + +class SalesforceProcessor(BaseLanguageProcessor): + def matches_content(self, content: list, first_line: str, name: str) -> bool: + return bool(content) and isinstance(content, list) and "name" in content[0] + + @sentry_sdk.trace + def process( + self, content: list, report_builder_session: ReportBuilderSession + ) -> None: + return from_json(content, report_builder_session) + + +def from_json(json: list, report_builder_session: ReportBuilderSession) -> None: + for obj in json: + if obj and obj.get("name") and obj.get("lines"): + filename = obj["name"] + (".cls" if "." not in obj["name"] else "") + _file = report_builder_session.create_coverage_file(filename) + if _file is None: + continue + + for ln, cov in obj["lines"].items(): + _file.append( + int(ln), + report_builder_session.create_coverage_line(cov), + ) + + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/scala.py b/apps/worker/services/report/languages/scala.py new file mode 100644 index 0000000000..30c0d9756e --- /dev/null +++ b/apps/worker/services/report/languages/scala.py @@ -0,0 +1,30 @@ +import sentry_sdk + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import ReportBuilderSession + + +class ScalaProcessor(BaseLanguageProcessor): + def matches_content(self, content: dict, first_line: str, name: str) -> bool: + return "fileReports" in content + + @sentry_sdk.trace + def process( + self, content: dict, report_builder_session: ReportBuilderSession + ) -> None: + return from_json(content, report_builder_session) + + +def from_json(data_dict: dict, report_builder_session: ReportBuilderSession) -> None: + for f in data_dict["fileReports"]: + _file = report_builder_session.create_coverage_file(f["filename"]) + if _file is None: + continue + + for ln, cov in f["coverage"].items(): + _file.append( + int(ln), + report_builder_session.create_coverage_line(cov), + ) + + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/scoverage.py b/apps/worker/services/report/languages/scoverage.py new file mode 100644 index 0000000000..a200abfac1 --- /dev/null +++ b/apps/worker/services/report/languages/scoverage.py @@ -0,0 +1,61 @@ +import sentry_sdk +from lxml.etree import Element +from shared.helpers.numeric import maxint +from shared.reports.resources import ReportFile + +from services.report.report_builder import CoverageType, ReportBuilderSession + +from .base import BaseLanguageProcessor +from .helpers import child_text + + +class SCoverageProcessor(BaseLanguageProcessor): + def matches_content(self, content: Element, first_line: str, name: str) -> bool: + return content.tag == "statements" + + @sentry_sdk.trace + def process( + self, content: Element, report_builder_session: ReportBuilderSession + ) -> None: + return from_xml(content, report_builder_session) + + +def from_xml(xml: Element, report_builder_session: ReportBuilderSession) -> None: + files: dict[str, ReportFile | None] = {} + for statement in xml.iter("statement"): + filename = child_text(statement, "source") + if filename not in files: + files[filename] = report_builder_session.create_coverage_file(filename) + + _file = files.get(filename) + if _file is None: + continue + + # Add the line + ln = int(child_text(statement, "line")) + hits = child_text(statement, "count") + + if child_text(statement, "ignored") == "true": + continue + + if child_text(statement, "branch") == "true": + cov = "%s/2" % hits + _file.append( + ln, + report_builder_session.create_coverage_line( + cov, + CoverageType.branch, + ), + ) + else: + cov = maxint(hits) + _file.append( + ln, + report_builder_session.create_coverage_line( + cov, + ), + ) + + for _file in files.values(): + if _file is not None: + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/simplecov.py b/apps/worker/services/report/languages/simplecov.py new file mode 100644 index 0000000000..fa19723f2f --- /dev/null +++ b/apps/worker/services/report/languages/simplecov.py @@ -0,0 +1,53 @@ +import sentry_sdk + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import ReportBuilderSession + + +class SimplecovProcessor(BaseLanguageProcessor): + """ + Handles processing of coverage reports generated by Simplecov (https://github.com/simplecov-ruby/simplecov) + The JSON formatter this processor expects is simplecov-json (https://github.com/vicentllongo/simplecov-json) + + """ + + def matches_content(self, content: dict, first_line: str, name: str) -> bool: + return isinstance(content, dict) and content.get("command_name") == "RSpec" + + @sentry_sdk.trace + def process( + self, content: dict, report_builder_session: ReportBuilderSession + ) -> None: + return from_json(content, report_builder_session) + + +def from_json(json: dict, report_builder_session: ReportBuilderSession) -> None: + for data in json["files"]: + _file = report_builder_session.create_coverage_file(data["filename"]) + if _file is None: + continue + + # Structure depends on which Simplecov version was used so we need to handle either structure + coverage = data["coverage"] + coverage_to_check = ( + coverage["lines"] + if isinstance(coverage, dict) + and coverage.get("lines") # Simplecov version >= 0.18 + else coverage # Simplecov version < 0.18 + ) + + for ln, cov in enumerate(coverage_to_check, start=1): + if cov == "ignored": + # Lines that simplecov skipped are recorded as "ignored" by + # https://github.com/codeclimate-community/simplecov_json_formatter + # and we in turn record that as -1 which indicates a skipped line + # in our report + cov = -1 + _file.append( + ln, + report_builder_session.create_coverage_line( + cov, + ), + ) + + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/tests/__init__.py b/apps/worker/services/report/languages/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/report/languages/tests/unit/__init__.py b/apps/worker/services/report/languages/tests/unit/__init__.py new file mode 100644 index 0000000000..d3b12bf7d7 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/__init__.py @@ -0,0 +1,19 @@ +from services.path_fixer import PathFixer +from services.report.report_builder import ReportBuilder, ReportBuilderSession + + +def create_report_builder_session( + path_fixer: PathFixer | None = None, + filename: str = "filename", + current_yaml: dict | None = None, +) -> ReportBuilderSession: + def fixes(filename, bases_to_try=None): + return filename + + report_builder = ReportBuilder( + path_fixer=path_fixer or fixes, + ignored_lines={}, + sessionid=0, + current_yaml=current_yaml, + ) + return report_builder.create_report_builder_session(filename) diff --git a/apps/worker/services/report/languages/tests/unit/node/ifbinary.json b/apps/worker/services/report/languages/tests/unit/node/ifbinary.json new file mode 100644 index 0000000000..77184ceb58 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/node/ifbinary.json @@ -0,0 +1,120 @@ +{ + "report": { + "file.js": { + "s": { + "433": 8 + }, + "statementMap": { + "433": { + "start": { + "line": 731, + "column": 6 + }, + "end": { + "line": 737, + "column": 7 + } + } + }, + "b": { + "123": [ + 3, + 5 + ], + "124": [ + 8, + 7, + 6 + ] + }, + "branchMap": { + "123": { + "loc": { + "start": { + "line": 731, + "column": 6 + }, + "end": { + "line": 737, + "column": 7 + } + }, + "type": "if", + "locations": [ + { + "start": { + "line": 731, + "column": 6 + }, + "end": { + "line": 737, + "column": 7 + } + }, + { + "start": { + "line": 731, + "column": 6 + }, + "end": { + "line": 737, + "column": 7 + } + } + ] + }, + "124": { + "loc": { + "start": { + "line": 731, + "column": 10 + }, + "end": { + "line": 731, + "column": 80 + } + }, + "type": "binary-expr", + "locations": [ + { + "start": { + "line": 731, + "column": 10 + }, + "end": { + "line": 731, + "column": 30 + } + }, + { + "start": { + "line": 731, + "column": 34 + }, + "end": { + "line": 731, + "column": 55 + } + }, + { + "start": { + "line": 731, + "column": 59 + }, + "end": { + "line": 731, + "column": 80 + } + } + ] + } + }, + "fnMap": {} + } + }, + "result": { + "file.js": { + "731": ["2/2", "b", [[0, "2/2", [], [[10, 30, 8], [34, 55, 7], [59, 80, 6]], null]], null, null, null] + } + } +} diff --git a/apps/worker/services/report/languages/tests/unit/node/ifbinarymb.json b/apps/worker/services/report/languages/tests/unit/node/ifbinarymb.json new file mode 100644 index 0000000000..f2e70acf2d --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/node/ifbinarymb.json @@ -0,0 +1,125 @@ +{ + "report": { + "file.js": { + "fnMap": {}, + "s": { + "211": 7, + "212": 0 + }, + "statementMap": { + "211": { + "start": { + "line": 383, + "column": 6 + }, + "end": { + "line": 385, + "column": 7 + } + }, + "212": { + "start": { + "line": 384, + "column": 8 + }, + "end": { + "line": 384, + "column": 71 + } + } + }, + "b": { + "72": [ 0, 7], + "73": [ 7, 3, 0] + }, + "branchMap": { + "72": { + "loc": { + "start": { + "line": 383, + "column": 6 + }, + "end": { + "line": 385, + "column": 7 + } + }, + "type": "if", + "locations": [ + { + "start": { + "line": 383, + "column": 6 + }, + "end": { + "line": 385, + "column": 7 + } + }, + { + "start": { + "line": 383, + "column": 6 + }, + "end": { + "line": 385, + "column": 7 + } + } + ] + }, + "73": { + "loc": { + "start": { + "line": 383, + "column": 10 + }, + "end": { + "line": 383, + "column": 113 + } + }, + "type": "binary-expr", + "locations": [ + { + "start": { + "line": 383, + "column": 10 + }, + "end": { + "line": 383, + "column": 31 + } + }, + { + "start": { + "line": 383, + "column": 35 + }, + "end": { + "line": 383, + "column": 72 + } + }, + { + "start": { + "line": 383, + "column": 76 + }, + "end": { + "line": 383, + "column": 113 + } + } + ] + } + } + } + }, + "result": { + "file.js": { + "383": ["1/2", "b", [[0, "1/2", ["0"], [[10, 31, 7], [35, 72, 3], [76, 113, 0]], null]], null, null, null], + "384": [0, null, [[0, 0, null, [[8, 71, 0]], null]], null, null, null] + } + } +} diff --git a/apps/worker/services/report/languages/tests/unit/node/inline.json b/apps/worker/services/report/languages/tests/unit/node/inline.json new file mode 100644 index 0000000000..6f9067ebd5 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/node/inline.json @@ -0,0 +1,78 @@ +{ + "report": { + "file.js": { + "b": { + "122": [0, 8] + }, + "branchMap": { + "122": { + "loc": { + "start": { + "line": 728, + "column": 6 + }, + "end": { + "line": 728, + "column": 41 + } + }, + "type": "if", + "locations": [ + { + "start": { + "line": 728, + "column": 6 + }, + "end": { + "line": 728, + "column": 41 + } + }, + { + "start": { + "line": 728, + "column": 6 + }, + "end": { + "line": 728, + "column": 41 + } + } + ] + } + }, + "s": { + "430": 8, + "431": 0 + }, + "statementMap": { + "430": { + "start": { + "line": 728, + "column": 6 + }, + "end": { + "line": 728, + "column": 41 + } + }, + "431": { + "start": { + "line": 728, + "column": 23 + }, + "end": { + "line": 728, + "column": 41 + } + } + }, + "fnMap": {} + } + }, + "result": { + "file.js": { + "728": ["1/2", "b", [[0, "1/2", ["0"], [[10, 21, "1/2"], [23, 41, 0]], null]], null, null, null] + } + } +} diff --git a/apps/worker/services/report/languages/tests/unit/node/inlinehit.json b/apps/worker/services/report/languages/tests/unit/node/inlinehit.json new file mode 100644 index 0000000000..b880092354 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/node/inlinehit.json @@ -0,0 +1,78 @@ +{ + "report": { + "file.js": { + "b": { + "122": [8, 8] + }, + "branchMap": { + "122": { + "loc": { + "start": { + "line": 728, + "column": 6 + }, + "end": { + "line": 728, + "column": 41 + } + }, + "type": "if", + "locations": [ + { + "start": { + "line": 728, + "column": 6 + }, + "end": { + "line": 728, + "column": 41 + } + }, + { + "start": { + "line": 728, + "column": 6 + }, + "end": { + "line": 728, + "column": 41 + } + } + ] + } + }, + "s": { + "430": 1, + "431": 1 + }, + "statementMap": { + "430": { + "start": { + "line": 728, + "column": 6 + }, + "end": { + "line": 728, + "column": 41 + } + }, + "431": { + "start": { + "line": 728, + "column": 23 + }, + "end": { + "line": 728, + "column": 41 + } + } + }, + "fnMap": {} + } + }, + "result": { + "file.js": { + "728": ["2/2", "b", [[0, "2/2", ["0"], [[10, 21, "2/2"], [23, 41, 8]]]], null] + } + } +} diff --git a/apps/worker/services/report/languages/tests/unit/node/node1-result.json b/apps/worker/services/report/languages/tests/unit/node/node1-result.json new file mode 100644 index 0000000000..98d71b7ed2 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/node/node1-result.json @@ -0,0 +1,157 @@ +{ + "archive": [ + "{}\n<<<<< end_of_header >>>>>\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n\n[2123,\"m\",[[0,2123]]]\n[2123,null,[[0,2123,null,[[2,44,2123]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[2123,\"m\",[[0,2123]]]\n[2123,null,[[0,2123,null,[[16,18,2123]],null]]]\n[2123,null,[[0,2123,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[19,23,14861],[27,38,14861],[41,50,631],[53,72,14230]],null]]]\n\n[2123,null,[[0,2123,null,[[2,17,2123]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[32050,\"m\",[[0,32050]]]\n[32050,null,[[0,32050,null,[[2,33,32050]],null]]]\n\n\n[1,null,[[0,1,null,[[11,27,1]],null]]]\n\n[87,\"m\",[[0,87]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,19,\"2/2\"],[21,58,87]],null]]]\n[87,null,[[0,87,null,[[2,44,87]],null]]]\n[87,null,[[0,87,null,[[2,43,87]],null]]]\n\n\n[15001,\"m\",[[0,15001]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,29,15001],[33,53,1404],[55,62,15001]],null]]]\n\n[13627,null,[[0,13627,null,[[14,37,13627]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[32,null,[[0,32,null,[[6,53,32]],null]]]\n[32,null,[[0,32,null,[[6,39,32]],null]]]\n\n\n\n\n\n\n\n[34,null,[[0,34,null,[[6,45,34]],null]]]\n\n\n[13561,null,[[0,13561,null,[[22,33,13561]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,24,13561],[28,56,11677],[60,109,47]],null]]]\n[13,null,[[0,13,null,[[6,54,13]],null]]]\n[13,null,[[0,13,null,[[6,42,13]],null]]]\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,25,24709],[29,60,18276]],null]]]\n[11082,null,[[0,11082,null,[[4,28,11082]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,28,79],[32,81,79]],null]]]\n[53,null,[[0,53,null,[[8,57,53]],null]]]\n[53,null,[[0,53,null,[[8,41,53]],null]]]\n\n\n\n\n[26,null,[[0,26,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[12,78,2]],null]]]\n[2,null,[[0,2,null,[[12,18,2]],null]]]\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[29,null,[[0,29,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[12,52,2]],null]]]\n[2,null,[[0,2,null,[[12,16,2]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[47,null,[[0,47,null,[[8,58,47]],null]]]\n[47,null,[[0,47,null,[[8,40,47]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[33,null,[[0,33,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[32,null,[[0,32,null,[[10,16,32]],null]]]\n\n\n\n\n\n\n\n[33,null,[[0,33,null,[[6,68,33]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[30,null,[[0,30,null,[[8,36,30]],null]]]\n\n\n\n\n[33,null,[[0,33,null,[[6,61,33]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[8,32,1]],null]]]\n\n\n\n\n[13627,null,[[0,13627,null,[[2,40,13627]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,31,52],[35,74,52],[78,116,52]],null]]]\n[5,null,[[0,5,null,[[6,44,5]],null]]]\n\n[47,null,[[0,47,null,[[6,47,47]],null]]]\n\n\n\n[13627,null,[[0,13627,null,[[2,19,13627]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1,null,[[11,27,1]],null]]]\n\n\n\n\n\n\n[263,\"m\",[[0,263]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,19,\"2/2\"],[21,28,263]],null]]]\n\n[248,null,[[0,248,null,[[12,20,248]],null]]]\n\n[248,null,[[0,248,null,[],null]]]\n[235,\"b\",[[0,235]]]\n[235,null,[[0,235,null,[[6,22,235]],null]]]\n[235,null,[[0,235,null,[[6,12,235]],null]]]\n\n[9,\"b\",[[0,9]]]\n[13,\"b\",[[0,13]]]\n[13,null,[[0,13,null,[[6,31,13]],null]]]\n[13,null,[[0,13,null,[[6,12,13]],null]]]\n\n[0,\"b\",[[0,0]]]\n[0,null,[[0,0,null,[[6,13,0]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[6,26,248],[30,50,23]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[[8,22,\"0/2\"],[24,84,0]],null]]]\n[0,null,[[0,0,null,[[4,26,0]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1793,\"m\",[[0,1793]]]\n[1793,null,[[0,1793,null,[[17,33,1793],[46,65,1793]],null]]]\n[1793,null,[[0,1793,null,[[13,64,1793]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[8,null,[[0,8,null,[[15,51,8]],null]]]\n[8,null,[[0,8,null,[[4,30,8]],null]]]\n[8,null,[[0,8,null,[],null]]]\n[10,null,[[0,10,null,[[6,81,10]],null]]]\n\n[7,null,[[0,7,null,[[4,44,7]],null]]]\n[7,null,[[0,7,null,[[4,55,7]],null]]]\n\n[1479,null,[[0,1479,null,[[2,14,1479]],null]]]\n\n\n\n\n\n[3228,\"m\",[[0,3228]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,27,3228],[31,53,50]],null]]]\n[34,null,[[0,34,null,[[4,29,34]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[634,null,[[0,634,null,[[4,34,634]],null]]]\n\n[2560,null,[[0,2560,null,[[4,42,2560]],null]]]\n[2560,null,[[0,2560,null,[[4,33,2560]],null]]]\n\n\n[3194,null,[[0,3194,null,[[17,33,3194]],null]]]\n[3194,null,[[0,3194,null,[[17,36,3194]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,27,3194],[31,50,2820]],null]]]\n[1593,null,[[0,1593,null,[[4,51,1593]],null]]]\n\n\n[3194,null,[[0,3194,null,[[13,87,3194]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,20,\"2/2\"],[22,81,2780]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[209,null,[[0,209,null,[[15,51,209]],null]]]\n[209,null,[[0,209,null,[[4,37,209]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[36,59,185],[62,66,24]],null]]]\n[190,null,[[0,190,null,[[4,37,190]],null]]]\n\n[190,null,[[0,190,null,[[4,25,190]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,18,178],[22,46,2]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[8,49,1]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[1,null,[[0,1,null,[[8,49,1]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[2,null,[[0,2,null,[[8,116,2]],null]]]\n\n\n\n[176,null,[[0,176,null,[[4,16,176]],null]]]\n[176,null,[[0,176,null,[[4,45,176]],null]]]\n[169,null,[[0,169,null,[[4,57,169]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[13,34,2571],[38,66,2070]],null]]]\n[3,null,[[0,3,null,[[4,50,3]],null]]]\n\n\n[2568,null,[[0,2568,null,[[2,14,2568]],null]]]\n\n\n\n\n[3194,\"m\",[[0,3194]]]\n[3194,null,[[0,3194,null,[[17,33,3194],[46,65,3194]],null]]]\n[3194,null,[[0,3194,null,[[13,60,3194]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,28,2787],[32,60,2787],[62,74,2787]],null]]]\n\n[2734,null,[[0,2734,null,[[2,81,2734]],null]]]\n\n\n[2734,\"m\",[[0,2734]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[21,null,[[0,21,null,[[15,51,21]],null]]]\n[21,null,[[0,21,null,[[4,21,21]],null]]]\n[21,null,[[0,21,null,[[4,46,21]],null]]]\n[17,null,[[0,17,null,[[4,26,17]],null]]]\n[17,null,[[0,17,null,[[4,49,17]],null]]]\n[9,null,[[0,9,null,[[4,58,9]],null]]]\n\n[2713,null,[[0,2713,null,[[2,14,2713]],null]]]\n\n\n\n\n[3194,\"m\",[[0,3194]]]\n[3194,null,[[0,3194,null,[[17,33,3194],[46,65,3194]],null]]]\n[3194,null,[[0,3194,null,[[13,57,3194]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,28,2800],[32,60,2800]],null]]]\n[53,null,[[0,53,null,[[4,16,53]],null]]]\n\n[2747,null,[[0,2747,null,[[4,64,2747]],null]]]\n\n\n\n\n\n\n\n\n\n[3205,\"m\",[[0,3205]]]\n[3205,null,[[0,3205,null,[[13,34,3205]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,18,3205],[23,28,293],[32,51,18]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[242,null,[[0,242,null,[[17,61,242]],null]]]\n[242,null,[[0,242,null,[[6,23,242]],null]]]\n[242,null,[[0,242,null,[[6,39,242]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[242,\"b\",[[0,242]]]\n[13,\"b\",[[0,13]]]\n[4,\"b\",[[0,4]]]\n[4,\"b\",[[0,4]]]\n[4,\"b\",[[0,4]]]\n\n[3,null,[[0,3,null,[[8,124,3]],null]]]\n\n\n[239,null,[[0,239,null,[[15,30,239]],null]]]\n[239,null,[[0,239,null,[[6,18,239]],null]]]\n\n[238,null,[[0,238,null,[[21,37,238]],null]]]\n[238,null,[[0,238,null,[[21,40,238]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[102,110,10],[113,117,219]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[29,48,229],[52,72,218],[76,95,22],[98,116,207]],null]]]\n[229,null,[[0,229,null,[[6,79,229]],null]]]\n\n\n[2963,null,[[0,2963,null,[[2,14,2963]],null]]]\n\n\n\n\n[3503,\"m\",[[0,3503]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[61,null,[[0,61,null,[[15,31,61]],null]]]\n[61,null,[[0,61,null,[[17,38,61]],null]]]\n[61,null,[[0,61,null,[[4,37,61]],null]]]\n[61,null,[[0,61,null,[[4,23,61]],null]]]\n[61,null,[[0,61,null,[[4,16,61]],null]]]\n\n[61,null,[[0,61,null,[[18,33,61]],null]]]\n[61,null,[[0,61,null,[[4,43,61]],null]]]\n\n[\"3/3\",\"b\",[[0,\"3/3\",[],[[49,70,58],[75,95,2],[99,133,2]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[8,30,58],[34,62,56]],null]]]\n[0,null,[[0,0,null,[[6,52,0]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[28,null,[[0,28,null,[[6,36,28]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[15,32,30],[36,62,3],[66,101,2]],null]]]\n[2,null,[[0,2,null,[[6,71,2]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[42,60,16],[63,80,28]],null]]]\n\n\n[3442,null,[[0,3442,null,[[17,33,3442],[46,65,3442]],null]]]\n[3442,null,[[0,3442,null,[[13,61,3442]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,28,3065],[32,60,2770],[62,74,3065]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,32,3039],[36,62,43]],null]]]\n[39,null,[[0,39,null,[[15,51,39]],null]]]\n[39,null,[[0,39,null,[[4,37,39]],null]]]\n[39,null,[[0,39,null,[[4,24,39]],null]]]\n[39,null,[[0,39,null,[[4,25,39]],null]]]\n[39,null,[[0,39,null,[[4,25,39]],null]]]\n[27,null,[[0,27,null,[[4,16,27]],null]]]\n[27,null,[[0,27,null,[[4,53,27]],null]]]\n\n[3000,null,[[0,3000,null,[[2,14,3000]],null]]]\n\n\n\n\n[3466,\"m\",[[0,3466]]]\n[3466,null,[[0,3466,null,[[17,33,3466],[46,65,3466]],null]]]\n[3466,null,[[0,3466,null,[[25,52,3466]],null]]]\n[3466,null,[[0,3466,null,[[13,55,3466]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,45,3100],[49,80,113]],null]]]\n[101,null,[[0,101,null,[[4,16,101]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,28,2999],[32,60,2680]],null]]]\n[53,null,[[0,53,null,[[4,16,53]],null]]]\n\n\n[2946,null,[[0,2946,null,[[2,56,2946]],null]]]\n\n\n[2981,\"m\",[[0,2981]]]\n[2981,null,[[0,2981,null,[],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[8,16,3246],[20,44,3204]],null]]]\n[0,null,[[0,0,null,[[17,53,0]],null]]]\n[0,null,[[0,0,null,[[6,25,0]],null]]]\n[0,null,[[0,0,null,[[6,43,0]],null]]]\n[0,null,[[0,0,null,[[6,104,0]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[74,null,[[0,74,null,[[17,53,74]],null]]]\n[74,null,[[0,74,null,[[6,25,74]],null]]]\n[74,null,[[0,74,null,[[6,49,74]],null]]]\n[72,null,[[0,72,null,[[6,28,72]],null]]]\n[72,null,[[0,72,null,[[6,55,72]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[18,null,[[0,18,null,[[17,53,18]],null]]]\n[18,null,[[0,18,null,[[6,25,18]],null]]]\n[18,null,[[0,18,null,[[6,45,18]],null]]]\n[18,null,[[0,18,null,[[6,27,18]],null]]]\n[18,null,[[0,18,null,[[6,31,18]],null]]]\n[18,null,[[0,18,null,[[6,55,18]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[15,23,3154],[27,48,3117]],null]]]\n[\"4/4\",\"b\",[[0,\"4/4\",[],[[26,68,186],[72,98,167],[102,123,153],[127,153,15]],null]]]\n[186,null,[[0,186,null,[[6,18,186]],null]]]\n\n[186,null,[[0,186,null,[[17,53,186]],null]]]\n[186,null,[[0,186,null,[[6,25,186]],null]]]\n[186,null,[[0,186,null,[[6,83,186]],null]]]\n[177,null,[[0,177,null,[[6,53,177]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,23,177],[27,55,13]],null]]]\n[11,null,[[0,11,null,[[8,98,11]],null]]]\n\n[166,null,[[0,166,null,[[8,46,166]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[9,null,[[0,9,null,[[17,53,9]],null]]]\n[9,null,[[0,9,null,[[6,22,9]],null]]]\n[9,null,[[0,9,null,[[6,40,9]],null]]]\n[9,null,[[0,9,null,[[6,63,9]],null]]]\n\n[2959,null,[[0,2959,null,[[6,18,2959]],null]]]\n\n\n\n\n[186,\"m\",[[0,186]]]\n\n\n[186,null,[[0,186,null,[[13,15,186],[25,29,186]],null]]]\n[186,null,[[0,186,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[98,null,[[0,98,null,[[6,20,98]],null]]]\n\n[26,null,[[0,26,null,[[6,28,26]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,25,\"2/2\"],[27,33,26]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,29,123],[33,49,6]],null]]]\n[6,null,[[0,6,null,[[6,41,6]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[69,81,13],[84,93,110]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,24,178],[28,43,14],[47,75,1]],null]]]\n[1,null,[[0,1,null,[[4,22,1]],null]]]\n\n\n[177,null,[[0,177,null,[[2,14,177]],null]]]\n\n\n[9,\"m\",[[0,9]]]\n[9,null,[[0,9,null,[[2,30,9]],null]]]\n\n\n[11,\"m\",[[0,11]]]\n[11,null,[[0,11,null,[[2,24,11]],null]]]\n[11,null,[[0,11,null,[[2,63,11]],null]]]\n\n\n\n\n[35,\"m\",[[0,35]]]\n[35,null,[[0,35,null,[[17,33,35],[46,65,35]],null]]]\n[35,null,[[0,35,null,[[2,78,35]],null]]]\n\n\n\n\n\n\n\n[3533,\"m\",[[0,3533]]]\n[3533,null,[[0,3533,null,[[25,73,3533]],null]]]\n[3533,null,[[0,3533,null,[],null]]]\n[9,\"b\",[[0,9]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,30,9],[34,71,2]],null]]]\n[2,null,[[0,2,null,[[8,77,2]],null]]]\n\n\n[7,null,[[0,7,null,[[6,30,7]],null]]]\n[7,null,[[0,7,null,[[6,18,7]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[10,32,7],[36,60,4],[64,83,3]],null]]]\n[0,null,[[0,0,null,[[8,26,0]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[10,31,7],[35,72,3],[76,113,0]],null]]]\n[0,null,[[0,0,null,[[8,71,0]],null]]]\n\n[7,null,[[0,7,null,[[6,44,7]],null]]]\n\n[8,\"b\",[[0,8]]]\n[8,null,[[0,8,null,[[6,30,8]],null]]]\n[8,null,[[0,8,null,[[6,18,8]],null]]]\n[8,null,[[0,8,null,[[6,53,8]],null]]]\n\n[18,\"b\",[[0,18]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[10,32,\"1/2\"],[34,52,0]],null]]]\n\n[1501,\"b\",[[0,1501]]]\n[1501,null,[[0,1501,null,[[6,30,1501]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[23,51,1501],[55,73,14]],null]]]\n[1501,null,[[0,1501,null,[[23,56,1501]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[36,46,1501],[50,60,1490]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,30,14],[34,47,3]],null]]]\n[11,null,[[0,11,null,[[10,39,11]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[17,36,1484],[40,64,20],[68,94,3]],null]]]\n[2,null,[[0,2,null,[[8,20,2]],null]]]\n[2,null,[[0,2,null,[[8,60,2]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[17,27,1482],[31,50,1201],[54,73,18]],null]]]\n[1,null,[[0,1,null,[[21,45,1]],null]]]\n[1,null,[[0,1,null,[[8,30,1]],null]]]\n\n[1,null,[[0,1,null,[[8,61,1]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,20,1484],[24,50,1203],[54,72,1101]],null]]]\n[24,null,[[0,24,null,[[8,53,24]],null]]]\n\n\n[1460,null,[[0,1460,null,[[6,16,1460]],null]]]\n\n[0,\"b\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],null,null]]]\n[0,null,[[0,0,null,[[19,35,0]],null]]]\n[0,null,[[0,0,null,[[8,20,0]],null]]]\n[0,null,[[0,0,null,[[28,49,0]],null]]]\n[0,null,[[0,0,null,[[24,41,0]],null]]]\n[0,null,[[0,0,null,[[8,31,0]],null]]]\n[0,null,[[0,0,null,[[8,38,0]],null]]]\n[0,null,[[0,0,null,[[8,49,0]],null]]]\n[0,null,[[0,0,null,[[8,46,0]],null]]]\n[0,null,[[0,0,null,[[8,38,0]],null]]]\n[0,null,[[0,0,null,[[8,53,0]],null]]]\n\n\n[26,\"b\",[[0,26]]]\n[26,null,[[0,26,null,[[18,34,26]],null]]]\n[26,null,[[0,26,null,[[6,61,26]],null]]]\n[26,null,[[0,26,null,[[6,35,26]],null]]]\n[26,null,[[0,26,null,[[6,31,26]],null]]]\n[26,null,[[0,26,null,[[6,18,26]],null]]]\n\n[550,\"b\",[[0,550]]]\n[550,null,[[0,550,null,[[6,67,550]],null]]]\n\n[346,\"b\",[[0,346]]]\n[346,null,[[0,346,null,[[6,66,346]],null]]]\n\n[11,\"b\",[[0,11]]]\n[11,null,[[0,11,null,[[6,30,11]],null]]]\n[11,null,[[0,11,null,[[6,18,11]],null]]]\n[11,null,[[0,11,null,[[6,50,11]],null]]]\n\n[62,\"b\",[[0,62]]]\n[76,null,[[0,76,null,[[6,30,76]],null]]]\n[76,null,[[0,76,null,[[6,40,76]],null]]]\n[76,null,[[0,76,null,[[6,18,76]],null]]]\n[76,null,[[0,76,null,[[6,53,76]],null]]]\n\n[383,\"b\",[[0,383]]]\n[383,null,[[0,383,null,[[6,77,383]],null]]]\n\n[114,\"b\",[[0,114]]]\n[114,null,[[0,114,null,[[6,30,114]],null]]]\n[114,null,[[0,114,null,[[6,18,114]],null]]]\n[114,null,[[0,114,null,[[6,84,114]],null]]]\n[109,null,[[0,109,null,[[6,43,109]],null]]]\n[109,null,[[0,109,null,[[6,54,109]],null]]]\n\n[223,\"b\",[[0,223]]]\n[223,null,[[0,223,null,[[6,58,223]],null]]]\n\n[122,\"b\",[[0,122]]]\n[122,null,[[0,122,null,[[6,44,122]],null]]]\n\n[2,\"b\",[[0,2]]]\n[2,null,[[0,2,null,[[6,29,2]],null]]]\n\n[19,\"b\",[[0,19]]]\n[19,null,[[0,19,null,[[6,30,19]],null]]]\n[19,null,[[0,19,null,[[6,32,19]],null]]]\n[19,null,[[0,19,null,[[6,42,19]],null]]]\n\n[45,\"b\",[[0,45]]]\n[45,null,[[0,45,null,[[6,29,45]],null]]]\n\n[24,\"b\",[[0,24]]]\n[24,null,[[0,24,null,[[6,34,24]],null]]]\n\n[0,\"b\",[[0,0]]]\n[0,null,[[0,0,null,[[6,30,0]],null]]]\n[0,null,[[0,0,null,[[6,18,0]],null]]]\n[0,null,[[0,0,null,[[6,25,0]],null]]]\n[0,null,[[0,0,null,[[19,55,0]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],null,null]]]\n[0,null,[[0,0,null,[[8,55,0]],null]]]\n\n[0,null,[[0,0,null,[[8,84,0]],null]]]\n\n\n[76,\"b\",[[0,76]]]\n[76,null,[[0,76,null,[[6,24,76]],null]]]\n\n\n\n[122,\"m\",[[0,122]]]\n[122,null,[[0,122,null,[[13,29,122]],null]]]\n[122,null,[[0,122,null,[[13,39,122]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,28,122],[32,48,3],[52,82,1]],null]]]\n[1,null,[[0,1,null,[[4,54,1]],null]]]\n\n[121,null,[[0,121,null,[[4,43,121]],null]]]\n\n\n\n[11,\"m\",[[0,11]]]\n[11,null,[[0,11,null,[[2,19,11]],null]]]\n[11,null,[[0,11,null,[[2,45,11]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[4,108,2]],null]]]\n\n\n[8,null,[[0,8,null,[[2,47,8]],null]]]\n\n\n[944,\"m\",[[0,944]]]\n[944,null,[[0,944,null,[[13,29,944]],null]]]\n[944,null,[[0,944,null,[[2,41,944]],null]]]\n[944,null,[[0,944,null,[[2,81,944]],null]]]\n[944,null,[[0,944,null,[[2,21,944]],null]]]\n[944,null,[[0,944,null,[[2,14,944]],null]]]\n[944,null,[[0,944,null,[[2,37,944]],null]]]\n\n\n[129,\"m\",[[0,129]]]\n[129,null,[[0,129,null,[[2,25,129]],null]]]\n[125,null,[[0,125,null,[[12,34,125]],null]]]\n[124,null,[[0,124,null,[[2,25,124]],null]]]\n[124,null,[[0,124,null,[[2,13,124]],null]]]\n\n\n[383,\"m\",[[0,383]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[13,21,383],[25,41,383]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[13,21,383],[25,44,383]],null]]]\n\n\n[383,null,[[0,383,null,[[2,25,383]],null]]]\n\n[383,null,[[0,383,null,[[22,38,383],[56,75,383]],null]]]\n[383,null,[[0,383,null,[[17,19,383],[29,33,383]],null]]]\n[383,null,[[0,383,null,[[31,43,383]],null]]]\n[383,null,[[0,383,null,[[25,37,383]],null]]]\n[383,null,[[0,383,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[366,null,[[0,366,null,[[6,20,366]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[28,50,55],[54,58,53]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[3,null,[[0,3,null,[[8,46,3]],null]]]\n[3,null,[[0,3,null,[[8,14,3]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[22,null,[[0,22,null,[[31,47,22],[70,89,22]],null]]]\n[22,null,[[0,22,null,[[6,37,22]],null]]]\n[22,null,[[0,22,null,[[6,99,22]],null]]]\n[18,null,[[0,18,null,[[6,12,18]],null]]]\n\n[394,null,[[0,394,null,[[6,113,394]],null]]]\n\n\n\n[286,null,[[0,286,null,[[20,36,286]],null]]]\n[286,null,[[0,286,null,[[20,39,286]],null]]]\n[286,null,[[0,286,null,[[2,25,286]],null]]]\n\n[284,null,[[0,284,null,[[18,54,284]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,16,284],[20,46,277],[51,89,178]],null]]]\n[111,null,[[0,111,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,21,140],[25,50,13],[52,92,140]],null]]]\n\n\n[104,null,[[0,104,null,[[4,67,104]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],null,null]]]\n[0,null,[[0,0,null,[[6,13,0]],null]]]\n\n[7,null,[[0,7,null,[[6,47,7]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,24,\"2/2\"],[26,62,164]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,17,\"2/2\"],[19,48,163]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[6,34,\"1/2\"],[36,82,0]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,28,\"2/2\"],[30,70,160]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[4,null,[[0,4,null,[[4,57,4]],null]]]\n[4,null,[[0,4,null,[[4,31,4]],null]]]\n[4,null,[[0,4,null,[[4,43,4]],null]]]\n[4,null,[[0,4,null,[[4,75,4]],null]]]\n\n[155,null,[[0,155,null,[[4,22,155]],null]]]\n\n\n\n[159,null,[[0,159,null,[[2,44,159]],null]]]\n[159,null,[[0,159,null,[[2,45,159]],null]]]\n\n[159,null,[[0,159,null,[[2,13,159]],null]]]\n\n\n[178,\"m\",[[0,178]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[111,null,[[0,111,null,[[4,16,111]],null]]]\n\n\n\n[538,\"m\",[[0,538]]]\n[538,null,[[0,538,null,[[2,14,538]],null]]]\n\n\n\n\n\n\n[45,\"m\",[[0,45]]]\n[45,null,[[0,45,null,[[13,29,45]],null]]]\n[45,null,[[0,45,null,[[13,39,45]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[10,null,[[0,10,null,[[4,56,10]],null]]]\n\n\n[35,null,[[0,35,null,[[2,39,35]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[20,null,[[0,20,null,[[4,51,20]],null]]]\n[17,null,[[0,17,null,[[4,42,17]],null]]]\n\n[15,null,[[0,15,null,[[4,24,15]],null]]]\n\n\n[32,null,[[0,32,null,[[2,48,32]],null]]]\n\n\n\n\n[41,\"m\",[[0,41]]]\n[41,null,[[0,41,null,[[13,29,41]],null]]]\n[41,null,[[0,41,null,[],null]]]\n\n\n\n[41,null,[[0,41,null,[[2,14,41]],null]]]\n[41,null,[[0,41,null,[[2,39,41]],null]]]\n[41,null,[[0,41,null,[[2,50,41]],null]]]\n\n\n[33,\"m\",[[0,33]]]\n[33,null,[[0,33,null,[[13,29,33]],null]]]\n[33,null,[[0,33,null,[[2,14,33]],null]]]\n[29,null,[[0,29,null,[[2,24,29]],null]]]\n[29,null,[[0,29,null,[[15,42,29]],null]]]\n[29,null,[[0,29,null,[[2,25,29]],null]]]\n[29,null,[[0,29,null,[],null]]]\n[17,null,[[0,17,null,[[4,33,17]],null]]]\n[17,null,[[0,17,null,[[4,50,17]],null]]]\n[17,null,[[0,17,null,[[4,27,17]],null]]]\n[12,null,[[0,12,null,[[4,59,12]],null]]]\n\n[24,null,[[0,24,null,[[2,14,24]],null]]]\n[24,null,[[0,24,null,[[2,50,24]],null]]]\n\n\n\n\n[284,\"m\",[[0,284]]]\n[284,null,[[0,284,null,[[19,21,284]],null]]]\n[284,null,[[0,284,null,[[17,36,284]],null]]]\n[284,null,[[0,284,null,[[14,18,284]],null]]]\n[284,null,[[0,284,null,[[13,29,284]],null]]]\n\n[284,null,[[0,284,null,[[2,23,284]],null]]]\n[284,null,[[0,284,null,[[2,14,284]],null]]]\n\n[280,null,[[0,280,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[254,null,[[0,254,null,[[6,20,254]],null]]]\n\n[71,null,[[0,71,null,[[6,28,71]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,29,\"2/2\"],[31,37,61]],null]]]\n\n\n[312,null,[[0,312,null,[],null]]]\n[2,null,[[0,2,null,[[6,45,2]],null]]]\n\n\n[312,null,[[0,312,null,[[15,31,312],[47,52,312],[64,69,312]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[6,35,2]],null]]]\n[2,null,[[0,2,null,[[6,22,2]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,42,312],[46,69,15]],null]]]\n[8,null,[[0,8,null,[[6,32,8]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[30,44,3],[47,63,5]],null]]]\n[8,null,[[0,8,null,[[6,33,8]],null]]]\n[8,null,[[0,8,null,[[6,15,8]],null]]]\n\n\n[304,null,[[0,304,null,[[4,24,304]],null]]]\n[304,null,[[0,304,null,[[4,27,304]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,17,304],[21,43,233]],null]]]\n[295,null,[[0,295,null,[[6,34,295]],null]]]\n[295,null,[[0,295,null,[[6,37,295]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[233,null,[[0,233,null,[[6,38,233]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,18,304],[22,48,233]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[10,21,\"1/2\"],[23,41,0]],null]]]\n\n[8,null,[[0,8,null,[[20,42,8]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,30,8],[34,55,7],[59,80,6]],null]]]\n[3,null,[[0,3,null,[[8,27,3]],null]]]\n\n[5,null,[[0,5,null,[[8,23,5]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,45,\"2/2\"],[47,79,5]],null]]]\n[5,null,[[0,5,null,[[8,37,5]],null]]]\n\n\n[296,null,[[0,296,null,[[6,35,296]],null]]]\n\n\n[295,null,[[0,295,null,[[4,110,295]],null]]]\n[263,null,[[0,263,null,[[4,40,263]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[81,null,[[0,81,null,[[6,45,81]],null]]]\n\n\n[263,null,[[0,263,null,[[4,31,263]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],null,null]]]\n[0,null,[[0,0,null,[[4,82,0]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[43,58,59],[61,79,170]],null]]]\n\n\n[295,\"m\",[[0,295]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,13,295],[17,28,290],[32,53,279]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[8,17,\"1/2\"],[19,37,0]],null]]]\n[46,null,[[0,46,null,[[4,25,46]],null]]]\n[46,null,[[0,46,null,[[4,23,46]],null]]]\n[46,null,[[0,46,null,[[4,49,46]],null]]]\n[37,null,[[0,37,null,[[4,49,37]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[29,90,27],[93,145,93]],null]]]\n[113,null,[[0,113,null,[[4,51,113]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,20,129],[24,54,128],[59,82,124],[86,109,101],[115,136,44],[140,162,44]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[8,19,43],[23,30,43],[34,43,43],[45,63,43]],null]]]\n[43,null,[[0,43,null,[[4,30,43]],null]]]\n[43,null,[[0,43,null,[[4,33,43]],null]]]\n[40,null,[[0,40,null,[[4,34,40]],null]]]\n[32,null,[[0,32,null,[[21,48,32]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],null,null]]]\n[0,null,[[0,0,null,[[18,28,0]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],null,null]]]\n[0,null,[[0,0,null,[[8,58,0]],null]]]\n\n[0,null,[[0,0,null,[[8,66,0]],null]]]\n\n\n[32,null,[[0,32,null,[[4,49,32]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,20,86],[24,54,85]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[44,null,[[0,44,null,[[27,56,44]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,25,44],[29,46,44]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[25,64,8],[68,103,8]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],null,null]]]\n[0,null,[[0,0,null,[[8,63,0]],null]]]\n\n[44,null,[[0,44,null,[[6,82,44]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[15,32,37],[36,58,11]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[11,null,[[0,11,null,[[8,56,11]],null]]]\n\n[11,null,[[0,11,null,[[6,82,11]],null]]]\n\n[26,null,[[0,26,null,[[6,38,26]],null]]]\n\n[81,null,[[0,81,null,[[4,26,81]],null]]]\n[81,null,[[0,81,null,[[4,51,81]],null]]]\n\n\n[5,null,[[0,5,null,[[2,20,5]],null]]]\n\n\n[544,\"m\",[[0,544]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[31,null,[[0,31,null,[[4,25,31]],null]]]\n[31,null,[[0,31,null,[[4,39,31]],null]]]\n[30,null,[[0,30,null,[[4,29,30]],null]]]\n[26,null,[[0,26,null,[[4,20,26]],null]]]\n\n[513,null,[[0,513,null,[[4,26,513]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[23,41,513],[45,66,507],[70,90,23],[93,119,490]],null]]]\n\n\n\n\n\n[762,\"m\",[[0,762]]]\n[762,null,[[0,762,null,[[2,17,762]],null]]]\n[762,null,[[0,762,null,[[2,25,762]],null]]]\n[762,null,[[0,762,null,[[2,26,762]],null]]]\n[762,null,[[0,762,null,[[2,25,762]],null]]]\n\n\n\n\n[190,\"m\",[[0,190]]]\n[190,null,[[0,190,null,[[20,39,190]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],[[24,33,190],[37,41,0]],null]]]\n[190,null,[[0,190,null,[[2,35,190]],null]]]\n[190,null,[[0,190,null,[[2,25,190]],null]]]\n[182,null,[[0,182,null,[[2,49,182]],null]]]\n[178,null,[[0,178,null,[[2,31,178]],null]]]\n[178,null,[[0,178,null,[[2,31,178]],null]]]\n[168,null,[[0,168,null,[[2,36,168]],null]]]\n[168,null,[[0,168,null,[[2,14,168]],null]]]\n\n\n\n\n[140,\"m\",[[0,140]]]\n[140,null,[[0,140,null,[[2,35,140]],null]]]\n[140,null,[[0,140,null,[[2,52,140]],null]]]\n[131,null,[[0,131,null,[[2,37,131]],null]]]\n[112,null,[[0,112,null,[[2,58,112]],null]]]\n\n\n\n\n[696,\"m\",[[0,696]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[21,36,696],[40,62,131]],null]]]\n\n[696,null,[[0,696,null,[[19,37,696]],null]]]\n[696,null,[[0,696,null,[[2,34,696]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[89,null,[[0,89,null,[[4,40,89]],null]]]\n[89,null,[[0,89,null,[[4,27,89]],null]]]\n\n\n\n[607,null,[[0,607,null,[[20,41,607],[54,76,607],[90,107,607]],null]]]\n[607,null,[[0,607,null,[[4,33,607],[34,74,607],[75,98,607]],null]]]\n[607,null,[[0,607,null,[[4,38,607]],null]]]\n[474,null,[[0,474,null,[[4,28,474]],null]]]\n[474,null,[[0,474,null,[[4,38,474],[39,73,474],[74,104,474]],null]]]\n\n[563,null,[[0,563,null,[[2,34,563]],null]]]\n\n\n\n\n[563,null,[[0,563,null,[[18,35,563]],null]]]\n[563,null,[[0,563,null,[[24,29,563]],null]]]\n[563,null,[[0,563,null,[[17,22,563]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,21,\"2/2\"],[23,40,563]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,19,563],[23,50,474]],null]]]\n[52,null,[[0,52,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[50,null,[[0,50,null,[[8,24,50]],null]]]\n[50,null,[[0,50,null,[[8,25,50]],null]]]\n[50,null,[[0,50,null,[[8,31,50]],null]]]\n[50,null,[[0,50,null,[[8,14,50]],null]]]\n\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,14,563],[18,25,50],[29,58,44],[62,86,44]],null]]]\n[2,null,[[0,2,null,[[4,62,2]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[361,null,[[0,361,null,[[19,38,361]],null]]]\n[361,null,[[0,361,null,[[20,37,361]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,23,\"2/2\"],[25,50,361]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[105,null,[[0,105,null,[[6,36,105]],null]]]\n\n[337,null,[[0,337,null,[],null]]]\n[261,null,[[0,261,null,[[6,44,261]],null]]]\n\n[276,null,[[0,276,null,[[4,34,276]],null]]]\n\n\n\n\n\n\n\n\n\n[134,\"m\",[[0,134]]]\n[134,null,[[0,134,null,[[13,15,134],[25,29,134]],null]]]\n[134,null,[[0,134,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[106,null,[[0,106,null,[[6,20,106]],null]]]\n\n[52,null,[[0,52,null,[[6,28,52]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,25,\"2/2\"],[27,33,52]],null]]]\n\n\n[150,null,[[0,150,null,[[4,74,150]],null]]]\n\n[126,null,[[0,126,null,[[2,14,126]],null]]]\n\n\n[273,\"m\",[[0,273]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,16,273],[20,40,137]],null]]]\n[12,null,[[0,12,null,[[4,15,12]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[35,null,[[0,35,null,[[4,51,35]],null]]]\n\n[226,null,[[0,226,null,[[4,84,226]],null]]]\n\n[257,null,[[0,257,null,[[2,13,257]],null]]]\n\n\n\n\n\n\n[4037,\"m\",[[0,4037]]]\n[4037,null,[[0,4037,null,[[13,29,4037]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,16,3760],[20,37,2567],[41,79,905]],null]]]\n[3,null,[[0,3,null,[[6,89,3]],null]]]\n\n\n[3757,null,[[0,3757,null,[[4,33,3757]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[13,20,277],[24,47,257]],null]]]\n[243,null,[[0,243,null,[[4,40,243]],null]]]\n\n[34,null,[[0,34,null,[[4,22,34]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[6,14,4000],[18,39,2564],[43,61,3]],null]]]\n[0,null,[[0,0,null,[[4,79,0]],null]]]\n\n\n[4000,null,[[0,4000,null,[[2,38,4000]],null]]]\n\n[4000,null,[[0,4000,null,[[2,14,4000]],null]]]\n[3998,null,[[0,3998,null,[[2,45,3998]],null]]]\n\n\n\n\n[11,\"m\",[[0,11]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],null,null]]]\n[0,null,[[0,0,null,[[4,22,0]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[4,116,1]],null]]]\n\n[10,null,[[0,10,null,[[2,41,10]],null]]]\n[10,null,[[0,10,null,[[2,50,10]],null]]]\n\n\n\n\n[34,\"m\",[[0,34]]]\n[34,null,[[0,34,null,[[13,29,34]],null]]]\n[34,null,[[0,34,null,[[2,14,34]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,25,34],[29,54,33],[59,79,29],[83,110,19]],null]]]\n[10,null,[[0,10,null,[[4,26,10]],null]]]\n[10,null,[[0,10,null,[[4,25,10]],null]]]\n\n[24,null,[[0,24,null,[[4,38,24]],null]]]\n[24,null,[[0,24,null,[[4,44,24]],null]]]\n\n[33,null,[[0,33,null,[[2,50,33]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n[1,null,[[0,1,null,[[23,25,1]],null]]]\n\n\n[2123,\"m\",[[0,2123]]]\n[2123,null,[[0,2123,null,[[4,34,2123]],null]]]\n[2123,null,[[0,2123,null,[[4,26,2123]],null]]]\n\n[2123,null,[[0,2123,null,[[4,27,2123]],null]]]\n[2123,null,[[0,2123,null,[[4,57,2123]],null]]]\n[2123,null,[[0,2123,null,[[4,43,2123]],null]]]\n[2123,null,[[0,2123,null,[[4,23,2123]],null]]]\n[2123,null,[[0,2123,null,[[4,58,2123]],null]]]\n[2123,null,[[0,2123,null,[[4,43,2123]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,28,2123],[32,53,2123],[57,78,2]],null]]]\n[2,null,[[0,2,null,[[6,30,2]],null]]]\n\n\n\n[1088,\"m\",[[0,1088]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[14,31,1088],[35,53,1088]],null]]]\n\n\n[8596,\"m\",[[0,8596]]]\n[8596,null,[[0,8596,null,[[4,31,8596]],null]]]\n\n\n[2123,\"m\",[[0,2123]]]\n[2123,null,[[0,2123,null,[[20,22,2123]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n\n[506,\"m\",[[0,506,null,[[6,62,253],[43,60,506]],null]]]\n[253,null,[[0,253,null,[[6,27,253]],null]]]\n\n\n[2123,null,[[0,2123,null,[],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[557,null,[[0,557,null,[[8,31,557]],null]]]\n\n[557,null,[[0,557,null,[[21,42,557]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,18,\"2/2\"],[20,33,557]],null]]]\n\n\n\n[2123,null,[[0,2123,null,[[4,21,2123]],null]]]\n\n\n\n\n\n\n\n\n[2123,\"m\",[[0,2123]]]\n[2123,null,[[0,2123,null,[[15,31,2123]],null]]]\n[2123,null,[[0,2123,null,[[18,34,2123]],null]]]\n[2123,null,[[0,2123,null,[[4,21,2123]],null]]]\n[2026,null,[[0,2026,null,[[4,45,2026]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n[1,null,[[0,1,null,[[11,27,1]],null]]]\n\n\n\n\n\n\n\n[738,\"m\",[[0,738]]]\n[738,null,[[0,738,null,[[12,40,738]],null]]]\n[738,null,[[0,738,null,[[2,44,738]],null]]]\n[738,null,[[0,738,null,[[12,36,738]],null]]]\n[738,null,[[0,738,null,[[2,16,738]],null]]]\n[738,null,[[0,738,null,[[2,16,738]],null]]]\n[738,null,[[0,738,null,[[2,12,738]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n[1,null,[[0,1,null,[[11,27,1]],null]]]\n\n\n\n\n[495,\"m\",[[0,495]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[495,null,[[0,495,null,[],null]]]\n[309,\"b\",[[0,309]]]\n[309,\"b\",[[0,309]]]\n[309,\"b\",[[0,309]]]\n[317,\"b\",[[0,317]]]\n[317,null,[[0,317,null,[[8,14,317]],null]]]\n\n[31,\"b\",[[0,31]]]\n[31,null,[[0,31,null,[[8,36,31]],null]]]\n[31,null,[[0,31,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],[[16,35,3],[39,58,0]],null]]]\n[3,null,[[0,3,null,[[14,90,3]],null]]]\n\n[0,null,[[0,0,null,[[14,81,0]],null]]]\n\n\n[36,null,[[0,36,null,[[12,47,36]],null]]]\n\n\n[25,null,[[0,25,null,[[8,14,25]],null]]]\n\n[35,\"b\",[[0,35]]]\n[35,null,[[0,35,null,[[8,49,35]],null]]]\n[32,null,[[0,32,null,[[8,14,32]],null]]]\n\n[1,\"b\",[[0,1]]]\n[1,null,[[0,1,null,[[8,35,1]],null]]]\n[1,null,[[0,1,null,[[8,14,1]],null]]]\n\n[55,\"b\",[[0,55]]]\n[55,null,[[0,55,null,[[8,35,55]],null]]]\n[55,null,[[0,55,null,[[8,56,55]],null]]]\n[50,null,[[0,50,null,[[8,14,50]],null]]]\n\n[17,\"b\",[[0,17]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[17,null,[[0,17,null,[[10,42,17]],null]]]\n[17,null,[[0,17,null,[[10,31,17]],null]]]\n\n[0,null,[[0,0,null,[[10,99,0]],null]]]\n\n[17,null,[[0,17,null,[[8,14,17]],null]]]\n\n[9,\"b\",[[0,9]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,22,\"2/2\"],[24,30,9]],null]]]\n\n[31,\"b\",[[0,31]]]\n[31,null,[[0,31,null,[[8,54,31]],null]]]\n\n\n[450,null,[[0,450,null,[[2,14,450]],null]]]\n\n\n\n\n[195,\"m\",[[0,195]]]\n[195,null,[[0,195,null,[[12,27,195]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[180,null,[[0,180,null,[[15,32,180]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,12,180],[16,43,177]],null]]]\n[12,null,[[0,12,null,[[6,12,12]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[15,19,168],[23,52,165]],null]]]\n[13,null,[[0,13,null,[[6,32,13]],null]]]\n[13,null,[[0,13,null,[[16,29,13]],null]]]\n[13,null,[[0,13,null,[[6,40,13]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,35,13],[39,70,4],[74,101,2]],null]]]\n[1,null,[[0,1,null,[[8,35,1]],null]]]\n\n[12,null,[[0,12,null,[[6,12,12]],null]]]\n\n\n[194,null,[[0,194,null,[],null]]]\n[216,null,[[0,216,null,[[14,25,216]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,11,\"2/2\"],[13,47,216]],null]]]\n\n[181,null,[[0,181,null,[[2,18,181]],null]]]\n\n\n\n\n[289,\"m\",[[0,289]]]\n[289,null,[[0,289,null,[[2,18,289]],null]]]\n\n\n\n\n[43,\"m\",[[0,43]]]\n[43,null,[[0,43,null,[[13,29,43]],null]]]\n[43,null,[[0,43,null,[[2,14,43]],null]]]\n[43,null,[[0,43,null,[[2,64,43]],null]]]\n[39,null,[[0,39,null,[[2,48,39]],null]]]\n\n\n[55,\"m\",[[0,55]]]\n[55,null,[[0,55,null,[[13,29,55]],null]]]\n[55,null,[[0,55,null,[[2,14,55]],null]]]\n[55,null,[[0,55,null,[[2,48,55]],null]]]\n[41,null,[[0,41,null,[[2,46,41]],null]]]\n\n\n[1885,\"m\",[[0,1885]]]\n[\"3/3\",\"b\",[[0,\"3/3\",[],[[9,30,1885],[34,52,28],[56,79,25]],null]]]\n\n\n[384,\"m\",[[0,384]]]\n[384,null,[[0,384,null,[[2,65,384]],null]]]\n\n\n\n\n[726,\"m\",[[0,726]]]\n[726,null,[[0,726,null,[],null]]]\n[16,\"b\",[[0,16]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,27,16],[31,53,9],[55,73,16]],null]]]\n\n[583,\"b\",[[0,583]]]\n[583,null,[[0,583,null,[[6,40,583]],null]]]\n\n[46,\"b\",[[0,46]]]\n[46,null,[[0,46,null,[[17,33,46]],null]]]\n[46,null,[[0,46,null,[[6,18,46]],null]]]\n[46,null,[[0,46,null,[[6,63,46]],null]]]\n[46,null,[[0,46,null,[[6,51,46]],null]]]\n\n[61,\"b\",[[0,61]]]\n[61,null,[[0,61,null,[[6,33,61]],null]]]\n\n[26,\"b\",[[0,26]]]\n[26,null,[[0,26,null,[[6,24,26]],null]]]\n\n\n\n[639,\"m\",[[0,639]]]\n[639,null,[[0,639,null,[[13,15,639]],null]]]\n[639,null,[[0,639,null,[[14,18,639]],null]]]\n[639,null,[[0,639,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[241,null,[[0,241,null,[[6,20,241]],null]]]\n\n[79,null,[[0,79,null,[[6,28,79]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,18,320],[22,42,59]],null]]]\n[2,null,[[0,2,null,[[6,22,2]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[5,null,[[0,5,null,[[6,12,5]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[33,null,[[0,33,null,[[6,69,33]],null]]]\n[23,null,[[0,23,null,[[6,25,23]],null]]]\n[18,null,[[0,18,null,[[6,12,18]],null]]]\n\n[280,null,[[0,280,null,[[23,25,280]],null]]]\n[280,null,[[0,280,null,[],null]]]\n[15,null,[[0,15,null,[[8,47,15]],null]]]\n\n[280,null,[[0,280,null,[[17,41,280]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[10,null,[[0,10,null,[[8,37,10]],null]]]\n\n[267,null,[[0,267,null,[[6,46,267]],null]]]\n[267,null,[[0,267,null,[[6,74,267]],null]]]\n\n\n[611,null,[[0,611,null,[[2,14,611]],null]]]\n\n\n[254,\"m\",[[0,254]]]\n[254,null,[[0,254,null,[[2,15,254]],null]]]\n\n\n\n\n[629,\"m\",[[0,629]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[13,21,629],[25,44,280]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[13,21,629],[25,41,280]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,13,629],[17,40,307]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,22,\"2/2\"],[24,36,615]],null]]]\n\n[39,null,[[0,39,null,[[13,49,39]],null]]]\n[39,null,[[0,39,null,[[2,19,39]],null]]]\n[39,null,[[0,39,null,[[2,39,39]],null]]]\n[39,null,[[0,39,null,[[2,52,39]],null]]]\n\n\n\n\n\n[1338,\"m\",[[0,1338]]]\n[1338,null,[[0,1338,null,[],null]]]\n[1094,\"b\",[[0,1094]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,27,1094],[32,67,491],[71,102,402]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[44,54,86],[57,72,27]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n\n\n\n\n\n\n\n\n\n\n\n[254,null,[[0,254,null,[[18,33,254]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[22,null,[[0,22,null,[[10,71,22]],null]]]\n\n[232,null,[[0,232,null,[[10,35,232]],null]]]\n\n\n[959,null,[[0,959,null,[[6,12,959]],null]]]\n\n[9,\"b\",[[0,9]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[10,55,\"1/2\"],[57,66,0],[69,83,0]],null]]]\n[9,null,[[0,9,null,[[6,12,9]],null]]]\n\n[66,\"b\",[[0,66]]]\n[66,null,[[0,66,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,42,\"2/2\"],[44,62,79]],null]]]\n[79,null,[[0,79,null,[[8,54,79]],null]]]\n\n[60,null,[[0,60,null,[[6,12,60]],null]]]\n\n[88,\"b\",[[0,88]]]\n[88,null,[[0,88,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,16,\"2/2\"],[18,64,117]],null]]]\n\n[80,null,[[0,80,null,[[6,12,80]],null]]]\n\n[36,\"b\",[[0,36]]]\n[36,null,[[0,36,null,[[6,57,36]],null]]]\n[35,null,[[0,35,null,[[6,12,35]],null]]]\n\n[3,\"b\",[[0,3]]]\n[36,\"b\",[[0,36]]]\n[36,null,[[0,36,null,[[6,61,36]],null]]]\n[31,null,[[0,31,null,[[6,12,31]],null]]]\n\n[9,\"b\",[[0,9]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[42,51,0],[54,68,9]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n[1,null,[[0,1,null,[[11,27,1]],null]]]\n[1,null,[[0,1,null,[[20,76,1]],null]]]\n\n\n[21359,\"m\",[[0,21359]]]\n[21359,null,[[0,21359,null,[[4,19,21359]],null]]]\n[21359,null,[[0,21359,null,[[4,21,21359]],null]]]\n[21359,null,[[0,21359,null,[[4,17,21359]],null]]]\n[21359,null,[[0,21359,null,[[4,39,21359]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,16,\"2/2\"],[18,47,21359]],null]]]\n\n\n\n\n\n\n\n[126,\"m\",[[0,126]]]\n[126,null,[[0,126,null,[[18,26,126]],null]]]\n[126,null,[[0,126,null,[],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[630,null,[[0,630,null,[[8,31,630]],null]]]\n\n\n\n[126,null,[[0,126,null,[[4,17,126]],null]]]\n\n\n\n[19360,\"m\",[[0,19360]]]\n[19360,null,[[0,19360,null,[[2,72,19360]],null]]]\n\n\n[1873,\"m\",[[0,1873]]]\n[1873,null,[[0,1873,null,[[2,43,1873]],null]]]\n\n\n[15001,\"m\",[[0,15001]]]\n[15001,null,[[0,15001,null,[[2,19,15001]],null]]]\n[15001,null,[[0,15001,null,[[2,17,15001]],null]]]\n[15001,null,[[0,15001,null,[[2,21,15001]],null]]]\n[15001,null,[[0,15001,null,[[2,28,15001]],null]]]\n[15001,null,[[0,15001,null,[[2,14,15001]],null]]]\n\n\n\n\n[14554,\"m\",[[0,14554]]]\n[14554,null,[[0,14554,null,[[2,94,14554]],null]]]\n\n\n\n\n[447,\"m\",[[0,447]]]\n[447,null,[[0,447,null,[[2,55,447]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n\n[1,null,[[0,1,null,[[11,27,1]],null]]]\n\n\n\n\n\n\n\n\n[2026,\"m\",[[0,2026]]]\n[2026,null,[[0,2026,null,[[2,47,2026]],null]]]\n\n[2026,null,[[0,2026,null,[[2,51,2026]],null]]]\n\n[1404,null,[[0,1404,null,[[2,54,1404]],null]]]\n[1404,null,[[0,1404,null,[[2,38,1404]],null]]]\n[1404,null,[[0,1404,null,[[2,36,1404]],null]]]\n\n[1404,null,[[0,1404,null,[[2,39,1404]],null]]]\n\n\n[1,null,[[0,1,null,[[18,32,1],[48,64,1]],null]]]\n\n\n\n[212,\"m\",[[0,212]]]\n[212,null,[[0,212,null,[[13,28,212]],null]]]\n\n[212,null,[[0,212,null,[[25,69,212]],null]]]\n[212,null,[[0,212,null,[[25,69,212]],null]]]\n\n[212,null,[[0,212,null,[[12,50,212]],null]]]\n[212,null,[[0,212,null,[[12,53,212]],null]]]\n\n[212,null,[[0,212,null,[[2,46,212]],null]]]\n[212,null,[[0,212,null,[[2,51,212]],null]]]\n\n[212,null,[[0,212,null,[[2,100,212]],null]]]\n\n[212,null,[[0,212,null,[[2,75,212]],null]]]\n\n\n\n\n\n\n\n\n\n[2906,\"m\",[[0,2906]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[5,null,[[0,5,null,[[4,31,5]],null]]]\n\n\n[2905,null,[[0,2905,null,[[18,33,2905],[42,58,2905]],null]]]\n\n\n\n\n\n[2905,null,[[0,2905,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[4,19,41],[20,104,72]],null]]]\n[3,\"b\",[[0,3,null,[[23,64,3]],null]]]\n[15,\"b\",[[0,15,null,[[17,52,15]],null]]]\n[80,\"b\",[[0,80,null,[[18,54,80]],null]]]\n[293,\"b\",[[0,293]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,22,\"2/2\"],[24,42,293]],null]]]\n[292,null,[[0,292,null,[[6,47,292]],null]]]\n\n[134,\"b\",[[0,134]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,22,\"2/2\"],[24,42,134]],null]]]\n[133,null,[[0,133,null,[[6,32,133]],null]]]\n[133,null,[[0,133,null,[[6,41,133]],null]]]\n\n[27,\"b\",[[0,27,null,[[17,52,27]],null]]]\n[32,\"b\",[[0,32,null,[[21,60,32]],null]]]\n[24,\"b\",[[0,24,null,[[21,60,24]],null]]]\n[19,\"b\",[[0,19,null,[[20,58,19]],null]]]\n[34,\"b\",[[0,34,null,[[18,54,34]],null]]]\n\n[49,\"b\",[[0,49]]]\n[78,\"b\",[[0,78]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,22,\"2/2\"],[24,42,78]],null]]]\n\n[315,\"b\",[[0,315]]]\n[315,null,[[0,315,null,[[6,53,315]],null]]]\n\n[55,\"b\",[[0,55,null,[[20,58,55]],null]]]\n[14,\"b\",[[0,14,null,[[19,56,14]],null]]]\n[109,\"b\",[[0,109,null,[[20,45,109]],null]]]\n[46,\"b\",[[0,46,null,[[18,56,46]],null]]]\n[78,\"b\",[[0,78]]]\n[127,\"b\",[[0,127]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[10,97,1]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[3,null,[[0,3,null,[[10,106,3]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[40,62,48],[65,87,75]],null]]]\n\n[590,\"b\",[[0,590]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n\n[13,null,[[0,13,null,[[20,38,13]],null]]]\n[13,null,[[0,13,null,[[8,20,13]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,36,13],[40,66,10]],null]]]\n[9,null,[[0,9,null,[[10,36,9]],null]]]\n[9,null,[[0,9,null,[[10,61,9]],null]]]\n\n[4,null,[[0,4,null,[[10,29,4]],null]]]\n\n\n\n\n\n\n\n\n\n[1493,null,[[0,1493,null,[[18,34,1493]],null]]]\n[1493,null,[[0,1493,null,[[13,35,1493]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,27,1194],[31,57,543],[61,79,145]],null]]]\n[39,null,[[0,39,null,[[4,61,39]],null]]]\n\n[1155,null,[[0,1155,null,[[4,53,1155]],null]]]\n\n\n\n[152,\"m\",[[0,152]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[6,null,[[0,6,null,[[4,44,6]],null]]]\n[6,null,[[0,6,null,[[4,31,6]],null]]]\n\n\n\n[7,\"m\",[[0,7]]]\n[7,null,[[0,7,null,[],null]]]\n[8,null,[[0,8,null,[[4,54,8]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[6,17,7],[21,43,5]],null]]]\n[0,null,[[0,0,null,[[4,11,0]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[4,95,1]],null]]]\n\n\n\n[33,\"m\",[[0,33]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],null,null]]]\n[0,null,[[0,0,null,[[4,22,0]],null]]]\n\n[33,null,[[0,33,null,[[13,29,33]],null]]]\n[33,null,[[0,33,null,[[2,14,33]],null]]]\n[33,null,[[0,33,null,[[2,44,33]],null]]]\n[33,null,[[0,33,null,[[2,44,33]],null]]]\n\n\n[72,\"m\",[[0,72]]]\n[72,null,[[0,72,null,[[16,35,72]],null]]]\n[72,null,[[0,72,null,[[2,14,72]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[43,null,[[0,43,null,[[4,22,43]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[4,null,[[0,4,null,[[4,22,4]],null]]]\n\n[25,null,[[0,25,null,[[4,40,25]],null]]]\n[25,null,[[0,25,null,[[4,21,25]],null]]]\n\n\n\n\n\n[68,null,[[0,68,null,[],null]]]\n[52,null,[[0,52,null,[[14,34,52]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,26,52],[30,58,21]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,26,48],[31,38,47],[42,61,19],[63,70,48]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,20,3],[24,31,1],[33,39,3]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,36,\"2/2\"],[38,87,68]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[41,57,29],[60,79,17]],null]]]\n\n\n[3,\"m\",[[0,3]]]\n[3,null,[[0,3,null,[[2,14,3]],null]]]\n[3,null,[[0,3,null,[[2,19,3]],null]]]\n[3,null,[[0,3,null,[[2,52,3]],null]]]\n\n\n[15,\"m\",[[0,15]]]\n[15,null,[[0,15,null,[[2,14,15]],null]]]\n[15,null,[[0,15,null,[[2,36,15]],null]]]\n[15,null,[[0,15,null,[[2,41,15]],null]]]\n[13,null,[[0,13,null,[[2,26,13]],null]]]\n[13,null,[[0,13,null,[[2,25,13]],null]]]\n[11,null,[[0,11,null,[[2,42,11]],null]]]\n[11,null,[[0,11,null,[[2,20,11]],null]]]\n[11,null,[[0,11,null,[[2,51,11]],null]]]\n\n\n\n\n\n\n\n\n\n\n[80,\"m\",[[0,80]]]\n[80,null,[[0,80,null,[[2,14,80]],null]]]\n[80,null,[[0,80,null,[[2,36,80]],null]]]\n\n[80,null,[[0,80,null,[[17,22,80]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,39,80],[43,61,8],[65,91,7]],null]]]\n[5,null,[[0,5,null,[[6,22,5]],null]]]\n[5,null,[[0,5,null,[[6,18,5]],null]]]\n\n[80,null,[[0,80,null,[[2,25,80]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[6,24,1]],null]]]\n\n[9,null,[[0,9,null,[[4,37,9]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,25,69],[29,48,56],[52,73,39]],null]]]\n[40,null,[[0,40,null,[[15,31,40],[43,58,40]],null]]]\n[40,null,[[0,40,null,[[4,16,40]],null]]]\n[40,null,[[0,40,null,[[4,39,40]],null]]]\n[39,null,[[0,39,null,[[4,49,39]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,26,39],[30,53,26]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,40,30],[44,70,28]],null]]]\n[20,null,[[0,20,null,[[8,53,20]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[6,24,1]],null]]]\n\n[18,null,[[0,18,null,[[4,37,18]],null]]]\n\n\n[29,null,[[0,29,null,[[31,41,29]],null]]]\n[29,null,[[0,29,null,[[13,63,29]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,24,29],[28,51,17]],null]]]\n[17,null,[[0,17,null,[[4,28,17]],null]]]\n[10,null,[[0,10,null,[[4,25,10]],null]]]\n[10,null,[[0,10,null,[[4,49,10]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],null,null]]]\n[0,null,[[0,0,null,[[4,50,0]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[4,22,1]],null]]]\n\n[11,null,[[0,11,null,[[2,35,11]],null]]]\n\n\n[292,\"m\",[[0,292]]]\n[292,null,[[0,292,null,[[2,14,292]],null]]]\n[292,null,[[0,292,null,[[2,40,292]],null]]]\n\n\n[27,\"m\",[[0,27]]]\n[27,null,[[0,27,null,[[2,14,27]],null]]]\n[27,null,[[0,27,null,[[2,42,27]],null]]]\n[24,null,[[0,24,null,[[2,47,24]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[40,66,7],[69,73,11]],null]]]\n[15,null,[[0,15,null,[[2,46,15]],null]]]\n\n\n[32,\"m\",[[0,32]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,28,32],[32,72,4]],null]]]\n[2,null,[[0,2,null,[[4,65,2]],null]]]\n\n\n[30,null,[[0,30,null,[[2,14,30]],null]]]\n\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[12,null,[[0,12,null,[[4,25,12]],null]]]\n\n[18,null,[[0,18,null,[[4,43,18]],null]]]\n[17,null,[[0,17,null,[[4,21,17]],null]]]\n\n\n[28,null,[[0,28,null,[[2,50,28]],null]]]\n\n\n[24,\"m\",[[0,24]]]\n[24,null,[[0,24,null,[[2,14,24]],null]]]\n[24,null,[[0,24,null,[[2,50,24]],null]]]\n[22,null,[[0,22,null,[[2,18,22]],null]]]\n[22,null,[[0,22,null,[[2,25,22]],null]]]\n[22,null,[[0,22,null,[[2,38,22]],null]]]\n\n\n\n\n\n\n[22,null,[[0,22,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,28,53],[32,55,35]],null]]]\n[26,null,[[0,26,null,[[19,39,26]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,13,\"2/2\"],[15,50,26]],null]]]\n[26,null,[[0,26,null,[[6,46,26]],null]]]\n[26,null,[[0,26,null,[[6,26,26]],null]]]\n[26,null,[[0,26,null,[[6,18,26]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[18,null,[[0,18,null,[[8,42,18]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,22,\"2/2\"],[24,88,8]],null]]]\n[6,null,[[0,6,null,[[8,26,6]],null]]]\n[6,null,[[0,6,null,[[8,24,6]],null]]]\n\n[24,null,[[0,24,null,[[6,28,24]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[27,null,[[0,27,null,[[8,55,27]],null]]]\n\n[0,null,[[0,0,null,[[8,26,0]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,9,\"2/2\"],[11,46,17]],null]]]\n[17,null,[[0,17,null,[[2,14,17]],null]]]\n[17,null,[[0,17,null,[[2,26,17]],null]]]\n[17,null,[[0,17,null,[[2,50,17]],null]]]\n\n\n[19,\"m\",[[0,19]]]\n[19,null,[[0,19,null,[[2,14,19]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[4,69,1]],null]]]\n[18,null,[[0,18,null,[[2,41,18]],null]]]\n[13,null,[[0,13,null,[[2,19,13]],null]]]\n[13,null,[[0,13,null,[[2,49,13]],null]]]\n\n\n\n\n[1,null,[[0,1,null,[[12,14,1]],null]]]\n\n[34,\"m\",[[0,34]]]\n[34,null,[[0,34,null,[[2,14,34]],null]]]\n\n[34,null,[[0,34,null,[[2,33,34]],null]]]\n[34,null,[[0,34,null,[[2,22,34]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[30,null,[[0,30,null,[[17,33,30]],null]]]\n[30,null,[[0,30,null,[[4,16,30]],null]]]\n\n[30,null,[[0,30,null,[[4,27,30]],null]]]\n[30,null,[[0,30,null,[[4,43,30]],null]]]\n[25,null,[[0,25,null,[[4,60,25]],null]]]\n[20,null,[[0,20,null,[[4,27,20]],null]]]\n\n[18,null,[[0,18,null,[[4,36,18]],null]]]\n[18,null,[[0,18,null,[[4,58,18]],null]]]\n\n\n[22,null,[[0,22,null,[[2,31,22]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[43,60,4],[63,67,18]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,19,22],[23,38,4]],null]]]\n[2,null,[[0,2,null,[[4,62,2]],null]]]\n\n\n[20,null,[[0,20,null,[[2,47,20]],null]]]\n\n\n[315,\"m\",[[0,315]]]\n[315,null,[[0,315,null,[[2,14,315]],null]]]\n[312,null,[[0,312,null,[[2,35,312]],null]]]\n[248,null,[[0,248,null,[[2,19,248]],null]]]\n[247,null,[[0,247,null,[[2,54,247]],null]]]\n\n\n[55,\"m\",[[0,55]]]\n[55,null,[[0,55,null,[[2,14,55]],null]]]\n[55,null,[[0,55,null,[[2,42,55]],null]]]\n[55,null,[[0,55,null,[[2,36,55]],null]]]\n[55,null,[[0,55,null,[[2,41,55]],null]]]\n[38,null,[[0,38,null,[[2,26,38]],null]]]\n[38,null,[[0,38,null,[[2,49,38]],null]]]\n\n\n[14,\"m\",[[0,14]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,23,\"2/2\"],[25,79,14]],null]]]\n[12,null,[[0,12,null,[[2,14,12]],null]]]\n[12,null,[[0,12,null,[[2,44,12]],null]]]\n[12,null,[[0,12,null,[[2,41,12]],null]]]\n[10,null,[[0,10,null,[[2,48,10]],null]]]\n\n\n[46,\"m\",[[0,46]]]\n[46,null,[[0,46,null,[[2,14,46]],null]]]\n[46,null,[[0,46,null,[[2,49,46]],null]]]\n\n\n[39,\"m\",[[0,39]]]\n[39,null,[[0,39,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[3,null,[[0,3,null,[[6,73,3]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[38,44,26],[47,87,10],[72,80,0],[83,87,10]],null]]]\n[36,null,[[0,36,null,[],null]]]\n[4,null,[[0,4,null,[[16,36,4]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[4,null,[[0,4,null,[[6,46,4]],null]]]\n[4,null,[[0,4,null,[[6,24,4]],null]]]\n\n[0,null,[[0,0,null,[[6,12,0]],null]]]\n\n\n\n[36,null,[[0,36,null,[[2,90,36]],null]]]\n[36,null,[[0,36,null,[[2,40,36]],null]]]\n[25,null,[[0,25,null,[[2,26,25]],null]]]\n[25,null,[[0,25,null,[[2,20,25]],null]]]\n[25,null,[[0,25,null,[[2,51,25]],null]]]\n\n\n[1109,\"m\",[[0,1109]]]\n[1109,null,[[0,1109,null,[[2,25,1109]],null]]]\n[1109,null,[[0,1109,null,[[2,19,1109]],null]]]\n[1077,null,[[0,1077,null,[[2,54,1077]],null]]]\n\n\n\n\n\n\n[772,\"m\",[[0,772]]]\n[772,null,[[0,772,null,[[13,29,772]],null]]]\n[772,null,[[0,772,null,[[2,25,772]],null]]]\n[766,null,[[0,766,null,[[2,63,766]],null]]]\n[615,null,[[0,615,null,[[2,49,615]],null]]]\n\n\n\n\n[2792,\"m\",[[0,2792]]]\n[2792,null,[[0,2792,null,[[2,17,2792]],null]]]\n[2792,null,[[0,2792,null,[[2,23,2792]],null]]]\n\n[2792,null,[[0,2792,null,[[27,32,2792]],null]]]\n\n\n\n[2792,null,[[0,2792,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,27,2670],[31,55,2559],[59,73,12]],null]]]\n[11,null,[[0,11,null,[[6,47,11]],null]]]\n\n\n[2670,null,[[0,2670,null,[[15,50,2670]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,23,1904],[27,46,1788]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,43,1716],[47,87,919]],null]]]\n[213,\"b\",[[0,213]]]\n[212,null,[[0,212,null,[[22,48,212]],null]]]\n[212,null,[[0,212,null,[[6,38,212]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,33,212],[37,75,210]],null]]]\n[182,null,[[0,182,null,[[8,38,182]],null]]]\n[182,null,[[0,182,null,[[8,29,182]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[3,null,[[0,3,null,[[10,68,3]],null]]]\n\n\n\n[205,null,[[0,205,null,[[6,15,205]],null]]]\n\n\n[1692,null,[[0,1692,null,[[4,30,1692]],null]]]\n[1692,null,[[0,1692,null,[[4,25,1692]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[53,null,[[0,53,null,[[4,26,53]],null]]]\n\n\n\n\n\n\n\n[38,\"m\",[[0,38]]]\n[38,null,[[0,38,null,[[2,19,38]],null]]]\n[38,null,[[0,38,null,[[2,23,38]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[36,40,20],[43,65,6]],null]]]\n[26,null,[[0,26,null,[[2,23,26]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[40,44,22],[47,69,4]],null]]]\n[26,null,[[0,26,null,[[2,25,26]],null]]]\n[26,null,[[0,26,null,[[2,41,26]],null]]]\n[23,null,[[0,23,null,[[2,26,23]],null]]]\n[23,null,[[0,23,null,[[2,47,23]],null]]]\n\n\n\n\n\n[30,\"m\",[[0,30]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[4,29,2]],null]]]\n[2,null,[[0,2,null,[[4,31,2]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[32,48,13],[51,67,15]],null]]]\n[28,null,[[0,28,null,[[4,16,28]],null]]]\n\n[30,null,[[0,30,null,[[2,19,30]],null]]]\n[30,null,[[0,30,null,[[2,38,30]],null]]]\n[29,null,[[0,29,null,[[2,25,29]],null]]]\n[29,null,[[0,29,null,[[2,41,29]],null]]]\n[29,null,[[0,29,null,[[2,26,29]],null]]]\n[29,null,[[0,29,null,[[2,37,29]],null]]]\n\n\n\n\n[352,\"m\",[[0,352]]]\n[352,null,[[0,352,null,[[2,25,352]],null]]]\n[352,null,[[0,352,null,[[2,27,352]],null]]]\n[352,null,[[0,352,null,[],null]]]\n[389,null,[[0,389,null,[[15,31,389]],null]]]\n[389,null,[[0,389,null,[[4,28,389]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[208,null,[[0,208,null,[[6,47,208]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[15,33,136],[37,85,11],[39,57,11],[61,84,10]],null]]]\n[5,null,[[0,5,null,[[6,24,5]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[15,44,131],[48,107,8],[50,55,8],[60,78,4],[82,105,2]],null]]]\n[4,null,[[0,4,null,[[6,100,4]],null]]]\n\n[127,null,[[0,127,null,[[6,23,127]],null]]]\n\n[324,null,[[0,324,null,[[4,72,324]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,27,\"2/2\"],[29,35,324]],null]]]\n\n[287,null,[[0,287,null,[[2,14,287]],null]]]\n\n\n[389,\"m\",[[0,389]]]\n[389,null,[[0,389,null,[[2,36,389]],null]]]\n[369,null,[[0,369,null,[[2,32,369]],null]]]\n\n\n\n\n\n[432,\"m\",[[0,432]]]\n[432,null,[[0,432,null,[[20,39,432]],null]]]\n[432,null,[[0,432,null,[[2,30,432]],null]]]\n\n[432,null,[[0,432,null,[[2,35,432]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[8,18,50],[22,56,0]],null]]]\n[0,null,[[0,0,null,[[6,24,0]],null]]]\n\n[50,null,[[0,50,null,[[6,28,50]],null]]]\n[50,null,[[0,50,null,[[6,18,50]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,17,432],[21,32,309],[36,56,301],[60,82,20]],null]]]\n[15,null,[[0,15,null,[[4,22,15]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,25,417],[29,50,96]],null]]]\n[329,null,[[0,329,null,[[4,44,329]],null]]]\n\n\n[412,null,[[0,412,null,[[2,33,412]],null]]]\n[387,null,[[0,387,null,[[2,52,387]],null]]]\n\n[196,null,[[0,196,null,[[2,36,196]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[45,66,125],[69,89,71]],null]]]\n\n\n[412,\"m\",[[0,412]]]\n[412,null,[[0,412,null,[[2,25,412]],null]]]\n[411,null,[[0,411,null,[[2,49,411]],null]]]\n\n\n\n\n\n[156,\"m\",[[0,156]]]\n[156,null,[[0,156,null,[[2,14,156]],null]]]\n[156,null,[[0,156,null,[[2,51,156]],null]]]\n[152,null,[[0,152,null,[[2,29,152]],null]]]\n[152,null,[[0,152,null,[[2,28,152]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[45,63,108],[66,83,19]],null]]]\n\n\n[102,\"m\",[[0,102]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,26,102],[30,53,96]],null]]]\n\n\n[91,\"m\",[[0,91]]]\n[91,null,[[0,91,null,[[2,15,91]],null]]]\n\n\n[152,\"m\",[[0,152]]]\n\n[152,null,[[0,152,null,[[18,35,152]],null]]]\n[152,null,[[0,152,null,[[2,27,152]],null]]]\n\n[152,null,[[0,152,null,[[27,32,152]],null]]]\n[152,null,[[0,152,null,[[23,28,152]],null]]]\n[152,null,[[0,152,null,[[19,21,152]],null]]]\n[152,null,[[0,152,null,[[18,34,152]],null]]]\n\n[152,null,[[0,152,null,[[2,22,152]],null]]]\n\n[152,null,[[0,152,null,[[2,25,152]],null]]]\n\n[148,null,[[0,148,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[19,null,[[0,19,null,[[6,15,19]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[8,null,[[0,8,null,[[6,45,8]],null]]]\n[8,null,[[0,8,null,[[6,15,8]],null]]]\n\n\n[131,null,[[0,131,null,[[17,33,131]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[6,null,[[0,6,null,[[6,37,6]],null]]]\n[6,null,[[0,6,null,[[6,22,6]],null]]]\n\n\n[131,null,[[0,131,null,[[28,33,131]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[24,43,131],[47,76,116]],null]]]\n[131,null,[[0,131,null,[[22,39,131]],null]]]\n[131,null,[[0,131,null,[[19,24,131]],null]]]\n[131,null,[[0,131,null,[[18,23,131]],null]]]\n\n[131,null,[[0,131,null,[[4,35,131]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[20,33,128],[37,59,37]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[10,21,\"1/2\"],[23,41,0]],null]]]\n[35,null,[[0,35,null,[[6,38,35]],null]]]\n[35,null,[[0,35,null,[[6,37,35]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,20,127],[24,56,120],[60,76,112]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[15,null,[[0,15,null,[[8,61,15]],null]]]\n[13,null,[[0,13,null,[[8,17,13]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,48,91],[52,78,2],[82,101,2],[105,139,2]],null]]]\n[1,null,[[0,1,null,[[8,33,1]],null]]]\n[1,null,[[0,1,null,[[8,39,1]],null]]]\n\n\n\n[\"4/4\",\"b\",[[0,\"4/4\",[],[[24,46,112],[50,66,42],[70,102,42],[106,133,41]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,43,2],[47,64,1],[66,85,2]],null]]]\n[2,null,[[0,2,null,[[6,21,2]],null]]]\n[2,null,[[0,2,null,[[6,37,2]],null]]]\n\n\n[112,null,[[0,112,null,[[4,27,112]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[103,null,[[0,103,null,[[20,26,103]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,18,103],[22,34,101],[38,67,95],[71,96,91],[100,122,86],[127,145,35],[149,167,17]],null]]]\n[31,null,[[0,31,null,[[8,24,31]],null]]]\n[31,null,[[0,31,null,[[8,31,31]],null]]]\n[31,null,[[0,31,null,[[8,45,31]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[26,44,103],[48,62,102]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,34,78],[38,64,73]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,37,67],[41,68,5]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,26,\"2/2\"],[28,93,15]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,20,\"2/2\"],[22,87,13]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,23,\"2/2\"],[25,83,10]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[12,19,\"1/2\"],[21,85,0]],null]]]\n[9,null,[[0,9,null,[[8,36,9]],null]]]\n[9,null,[[0,9,null,[[8,30,9]],null]]]\n\n\n\n[97,\"b\",[[0,97]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,34,24],[38,62,23]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,37,23],[41,66,1]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[8,86,2]],null]]]\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[10,28,\"1/2\"],[30,103,0]],null]]]\n[1,null,[[0,1,null,[[6,38,1]],null]]]\n[1,null,[[0,1,null,[[6,32,1]],null]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[9,38,104],[42,75,95],[80,97,10]],null]]]\n[0,null,[[0,0,null,[[6,85,0]],null]]]\n\n\n[104,null,[[0,104,null,[[4,67,104]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[28,null,[[0,28,null,[[23,52,28]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[20,32,1]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],null,null]]]\n[0,null,[[0,0,null,[[10,60,0]],null]]]\n\n[1,null,[[0,1,null,[[10,68,1]],null]]]\n\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[4,80,1]],null]]]\n\n\n[127,null,[[0,127,null,[[2,54,127]],null]]]\n\n[127,null,[[0,127,null,[[2,32,127]],null]]]\n\n\n[15,\"m\",[[0,15]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[8,42,\"1/2\"],[44,62,0]],null]]]\n[6,null,[[0,6,null,[[4,16,6]],null]]]\n[6,null,[[0,6,null,[[4,41,6]],null]]]\n\n[9,null,[[0,9,null,[[4,22,9]],null]]]\n\n[15,null,[[0,15,null,[[2,19,15]],null]]]\n[13,null,[[0,13,null,[[2,48,13]],null]]]\n\n\n[93,\"m\",[[0,93]]]\n[93,null,[[0,93,null,[[2,49,93]],null]]]\n[88,null,[[0,88,null,[[2,62,88]],null]]]\n\n\n[156,\"m\",[[0,156]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[145,null,[[0,145,null,[[4,37,145]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,18,11],[22,34,8]],null]]]\n[7,null,[[0,7,null,[[6,21,7]],null]]]\n\n[4,null,[[0,4,null,[[6,24,4]],null]]]\n\n\n\n\n[152,\"m\",[[0,152]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[44,70,24],[73,77,128]],null]]]\n\n\n\n\n[75,\"m\",[[0,75]]]\n[75,null,[[0,75,null,[[2,14,75]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[7,null,[[0,7,null,[[20,36,7]],null]]]\n[7,null,[[0,7,null,[[4,16,7]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,42,7],[46,70,2]],null]]]\n[2,null,[[0,2,null,[[6,50,2]],null]]]\n[2,null,[[0,2,null,[[6,81,2]],null]]]\n[2,null,[[0,2,null,[[6,44,2]],null]]]\n[2,null,[[0,2,null,[[6,39,2]],null]]]\n\n[5,null,[[0,5,null,[[6,39,5]],null]]]\n[2,null,[[0,2,null,[[6,59,2]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[13,47,68],[51,82,3]],null]]]\n[3,null,[[0,3,null,[[20,36,3]],null]]]\n[3,null,[[0,3,null,[[4,52,3]],null]]]\n[3,null,[[0,3,null,[[4,77,3]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[8,28,3],[32,65,1]],null]]]\n[0,null,[[0,0,null,[[6,28,0]],null]]]\n[0,null,[[0,0,null,[[22,38,0]],null]]]\n[0,null,[[0,0,null,[[6,27,0]],null]]]\n[0,null,[[0,0,null,[[6,34,0]],null]]]\n[0,null,[[0,0,null,[[6,50,0]],null]]]\n[0,null,[[0,0,null,[[6,83,0]],null]]]\n\n[3,null,[[0,3,null,[[6,44,3]],null]]]\n\n[3,null,[[0,3,null,[[4,37,3]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[23,null,[[0,23,null,[[15,31,23]],null]]]\n[23,null,[[0,23,null,[[20,25,23]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[8,null,[[0,8,null,[[6,64,8]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[4,null,[[0,4,null,[[6,47,4]],null]]]\n\n[11,null,[[0,11,null,[[6,23,11]],null]]]\n[11,null,[[0,11,null,[[6,37,11]],null]]]\n\n[22,null,[[0,22,null,[[4,28,22]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,17,\"2/2\"],[19,36,22]],null]]]\n[21,null,[[0,21,null,[[4,27,21]],null]]]\n[21,null,[[0,21,null,[[4,61,21]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[13,36,42],[40,75,28]],null]]]\n[21,null,[[0,21,null,[[4,25,21]],null]]]\n[21,null,[[0,21,null,[[4,23,21]],null]]]\n[21,null,[[0,21,null,[[4,57,21]],null]]]\n\n[21,null,[[0,21,null,[[4,28,21]],null]]]\n[21,null,[[0,21,null,[[4,51,21]],null]]]\n[19,null,[[0,19,null,[[4,31,19]],null]]]\n\n[44,null,[[0,44,null,[[2,25,44]],null]]]\n[44,null,[[0,44,null,[[2,57,44]],null]]]\n\n\n[15,\"m\",[[0,15]]]\n[15,null,[[0,15,null,[[2,35,15]],null]]]\n\n\n[3,\"m\",[[0,3]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,\"b\",[[0,2]]]\n[2,\"b\",[[0,2]]]\n[2,\"b\",[[0,2]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],null,null]]]\n[0,null,[[0,0,null,[[4,17,0]],null]]]\n\n\n[1,null,[[0,1,null,[[18,34,1]],null]]]\n[\"3/3\",\"b\",[[0,\"3/3\",[],[[9,36,1],[41,67,1],[71,97,1]],null]]]\n\n\n[5,\"m\",[[0,5]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[4,75,2]],null]]]\n\n\n\n[31,\"m\",[[0,31]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[42,62,15],[65,82,1]],null]]]\n[15,null,[[0,15,null,[[4,27,15]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[3,null,[[0,3,null,[[6,24,3]],null]]]\n\n[12,null,[[0,12,null,[[6,25,12]],null]]]\n\n\n\n[27,null,[[0,27,null,[[2,19,27]],null]]]\n\n\n[22,\"m\",[[0,22]]]\n[22,null,[[0,22,null,[[2,36,22]],null]]]\n\n\n[80,\"m\",[[0,80]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],null,null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",[\"0\",\"1\",\"2\"],[[18,34,0],[39,83,0],[87,130,0]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[[8,25,0],[29,37,0]],null]]]\n[0,null,[[0,0,null,[[6,96,0]],null]]]\n\n[0,null,[[0,0,null,[[4,42,0]],null]]]\n\n\n\n\n\n[25,\"m\",[[0,25]]]\n[25,null,[[0,25,null,[[14,16,25]],null]]]\n[25,null,[[0,25,null,[[14,18,25]],null]]]\n\n\n\n[25,null,[[0,25,null,[[2,25,25]],null]]]\n\n[25,null,[[0,25,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[24,null,[[0,24,null,[[6,20,24]],null]]]\n\n[8,null,[[0,8,null,[[6,28,8]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,29,\"2/2\"],[31,37,8]],null]]]\n\n\n[31,null,[[0,31,null,[[20,43,31]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,17,31],[21,31,4],[33,50,31]],null]]]\n\n[31,null,[[0,31,null,[[15,31,31]],null]]]\n[31,null,[[0,31,null,[[4,49,31]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[47,73,8],[76,96,23]],null]]]\n[31,null,[[0,31,null,[[4,57,31]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,15,25],[19,45,4]],null]]]\n[2,null,[[0,2,null,[[4,22,2]],null]]]\n\n\n[23,null,[[0,23,null,[[2,15,23]],null]]]\n\n\n\n\n[48,\"m\",[[0,48]]]\n[48,null,[[0,48,null,[[2,14,48]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[4,25,2]],null]]]\n[2,null,[[0,2,null,[[4,39,2]],null]]]\n\n[46,null,[[0,46,null,[[4,25,46]],null]]]\n[46,null,[[0,46,null,[[4,37,46]],null]]]\n[42,null,[[0,42,null,[[4,34,42]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[42,62,29],[65,82,2]],null]]]\n\n[31,null,[[0,31,null,[[2,19,31]],null]]]\n[31,null,[[0,31,null,[[2,52,31]],null]]]\n\n\n\n\n[46,\"m\",[[0,46]]]\n[46,null,[[0,46,null,[[14,18,46]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n\n[18,null,[[0,18,null,[[19,35,18],[48,67,18]],null]]]\n[18,null,[[0,18,null,[[4,103,18]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,27,\"2/2\"],[29,36,18]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[8,null,[[0,8,null,[[20,36,8]],null]]]\n[8,null,[[0,8,null,[[4,16,8]],null]]]\n[8,null,[[0,8,null,[[4,32,8]],null]]]\n[7,null,[[0,7,null,[[4,45,7]],null]]]\n[7,null,[[0,7,null,[[4,42,7]],null]]]\n[7,null,[[0,7,null,[[4,81,7]],null]]]\n[7,null,[[0,7,null,[[4,11,7]],null]]]\n\n\n[27,null,[[0,27,null,[[2,25,27]],null]]]\n[24,null,[[0,24,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[23,null,[[0,23,null,[[6,20,23]],null]]]\n\n[9,null,[[0,9,null,[[6,28,9]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,29,\"2/2\"],[31,37,9]],null]]]\n\n\n[31,null,[[0,31,null,[[20,36,31]],null]]]\n[31,null,[[0,31,null,[[4,52,31]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[49,71,9],[74,102,22]],null]]]\n[31,null,[[0,31,null,[[4,42,31]],null]]]\n[31,null,[[0,31,null,[[4,72,31]],null]]]\n\n\n\n[18,\"m\",[[0,18]]]\n[18,null,[[0,18,null,[[13,49,18]],null]]]\n[18,null,[[0,18,null,[[2,18,18]],null]]]\n[18,null,[[0,18,null,[[2,35,18]],null]]]\n[18,null,[[0,18,null,[[2,57,18]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n[1,null,[[0,1,null,[[11,27,1]],null]]]\n\n\n\n\n\n[2801,\"m\",[[0,2801]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[6,11,\"1/2\"],[13,20,0]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[27,37,2801],[41,43,1460]],null]]]\n[2801,null,[[0,2801,null,[[2,19,2801]],null]]]\n\n\n\n\n[1089,\"m\",[[0,1089]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,34,1089],[38,61,494]],null]]]\n\n\n\n\n[126,\"m\",[[0,126]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[126,null,[[0,126,null,[[4,16,126]],null]]]\n\n[0,null,[[0,0,null,[[4,22,0]],null]]]\n\n\n\n\n\n[432,\"m\",[[0,432]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,28,432],[32,57,288]],null]]]\n\n\n\n\n[157,\"m\",[[0,157]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,34,157],[38,55,82]],null]]]\n\n\n\n\n[60,\"m\",[[0,60]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,31,\"2/2\"],[33,64,60]],null]]]\n\n\n\n\n[2706,\"m\",[[0,2706]]]\n[2706,\"b\",[[0,2706]]]\n[1781,\"b\",[[0,1781]]]\n[1627,\"b\",[[0,1627]]]\n\n\n\n\n[1721,\"m\",[[0,1721]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,26,1721],[30,55,1122]],null]]]\n\n\n\n\n\n[1523,\"m\",[[0,1523]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,30,\"2/2\"],[32,50,1523]],null]]]\n\n\n\n\n\n[3953,\"m\",[[0,3953]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,23,3953],[27,47,72]],null]]]\n\n\n\n\n[349,\"b\",[[0,349]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[27,30,24],[33,49,325]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n\n[1,null,[[0,1,null,[[9,25,1]],null]]]\n\n[287,\"m\",[[0,287]]]\n[287,null,[[0,287,null,[[18,35,287]],null]]]\n[287,null,[[0,287,null,[[2,27,287]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[14,17,287],[21,29,270]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,33,17],[37,61,16]],null]]]\n[3,null,[[0,3,null,[[6,18,3]],null]]]\n\n\n[287,null,[[0,287,null,[[13,33,287]],null]]]\n[284,null,[[0,284,null,[[2,32,284]],null]]]\n[284,null,[[0,284,null,[[2,14,284]],null]]]\n\n\n[13,\"m\",[[0,13]]]\n[13,null,[[0,13,null,[[2,14,13]],null]]]\n[13,null,[[0,13,null,[[2,41,13]],null]]]\n[13,null,[[0,13,null,[[2,47,13]],null]]]\n\n\n[5,\"m\",[[0,5]]]\n[5,null,[[0,5,null,[[2,14,5]],null]]]\n\n[5,null,[[0,5,null,[[11,43,5]],null]]]\n\n[5,null,[[0,5,null,[[17,33,5]],null]]]\n[5,null,[[0,5,null,[[22,38,5]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[4,71,1]],null]]]\n\n[4,null,[[0,4,null,[[4,35,4]],null]]]\n\n\n[5,null,[[0,5,null,[[2,25,5]],null]]]\n[5,null,[[0,5,null,[[12,46,5]],null]]]\n[5,null,[[0,5,null,[[2,31,5]],null]]]\n[5,null,[[0,5,null,[[2,27,5]],null]]]\n[5,null,[[0,5,null,[[2,25,5]],null]]]\n[5,null,[[0,5,null,[[2,56,5]],null]]]\n\n[5,null,[[0,5,null,[[2,85,5]],null]]]\n[5,null,[[0,5,null,[[2,71,5]],null]]]\n\n[5,null,[[0,5,null,[[2,31,5]],null]]]\n\n[5,null,[[0,5,null,[[2,19,5]],null]]]\n\n[5,null,[[0,5,null,[[2,50,5]],null]]]\n\n\n[36,\"m\",[[0,36]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[13,null,[[0,13,null,[[4,44,13]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[5,null,[[0,5,null,[[4,47,5]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[3,null,[[0,3,null,[[4,47,3]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[6,54,2]],null]]]\n\n[8,null,[[0,8,null,[[6,47,8]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[4,48,2]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[4,48,2]],null]]]\n\n[1,null,[[0,1,null,[[4,22,1]],null]]]\n\n\n\n[3,\"m\",[[0,3]]]\n[3,null,[[0,3,null,[[2,14,3]],null]]]\n[3,null,[[0,3,null,[[2,54,3]],null]]]\n[3,null,[[0,3,null,[[2,19,3]],null]]]\n[3,null,[[0,3,null,[[2,50,3]],null]]]\n\n\n[8,\"m\",[[0,8]]]\n[8,null,[[0,8,null,[[2,14,8]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[4,35,1]],null]]]\n\n[7,null,[[0,7,null,[[4,37,7]],null]]]\n\n\n[8,null,[[0,8,null,[[17,45,8]],null]]]\n[8,null,[[0,8,null,[[13,31,8]],null]]]\n[8,null,[[0,8,null,[[2,25,8]],null]]]\n[8,null,[[0,8,null,[],null]]]\n[6,null,[[0,6,null,[[16,32,6]],null]]]\n\n[6,null,[[0,6,null,[[4,106,6]],null]]]\n\n[5,null,[[0,5,null,[[4,44,5]],null]]]\n\n[6,null,[[0,6,null,[[2,25,6]],null]]]\n\n[6,null,[[0,6,null,[[2,46,6]],null]]]\n[6,null,[[0,6,null,[[2,48,6]],null]]]\n\n\n[2,\"m\",[[0,2]]]\n[2,null,[[0,2,null,[[2,34,2]],null]]]\n[2,null,[[0,2,null,[[2,22,2]],null]]]\n[2,null,[[0,2,null,[[2,35,2]],null]]]\n[1,null,[[0,1,null,[[2,55,1]],null]]]\n[1,null,[[0,1,null,[[2,55,1]],null]]]\n\n\n[2,\"m\",[[0,2]]]\n[2,null,[[0,2,null,[[2,14,2]],null]]]\n[2,null,[[0,2,null,[[2,32,2]],null]]]\n[2,null,[[0,2,null,[[2,51,2]],null]]]\n\n\n[2,\"m\",[[0,2]]]\n[2,null,[[0,2,null,[[2,14,2]],null]]]\n[2,null,[[0,2,null,[[2,35,2]],null]]]\n[2,null,[[0,2,null,[[2,51,2]],null]]]\n\n\n\n\n[27,\"m\",[[0,27]]]\n[27,null,[[0,27,null,[[2,35,27]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[12,null,[[0,12,null,[[4,67,12]],null]]]\n\n[15,null,[[0,15,null,[[4,31,15]],null]]]\n\n\n[27,null,[[0,27,null,[[2,20,27]],null]]]\n[27,null,[[0,27,null,[[2,19,27]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[4,null,[[0,4,null,[],null]]]\n[5,null,[[0,5,null,[[6,58,5]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[4,16,1]],null]]]\n[1,null,[[0,1,null,[],null]]]\n[2,null,[[0,2,null,[[6,57,2]],null]]]\n\n\n\n[27,null,[[0,27,null,[[2,52,27]],null]]]\n\n\n[7,\"m\",[[0,7]]]\n[7,null,[[0,7,null,[[13,29,7]],null]]]\n\n[7,null,[[0,7,null,[[2,52,7]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[4,null,[[0,4,null,[[4,69,4]],null]]]\n\n[3,null,[[0,3,null,[[4,31,3]],null]]]\n\n\n[7,null,[[0,7,null,[[2,51,7]],null]]]\n\n\n[12,\"m\",[[0,12]]]\n[12,null,[[0,12,null,[[2,42,12]],null]]]\n[12,null,[[0,12,null,[[2,55,12]],null]]]\n\n\n\n\n[19,\"m\",[[0,19]]]\n[19,null,[[0,19,null,[[2,35,19]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[12,null,[[0,12,null,[[4,67,12]],null]]]\n\n[7,null,[[0,7,null,[[4,31,7]],null]]]\n\n\n[17,null,[[0,17,null,[],null]]]\n\n\n\n[17,null,[[0,17,null,[[2,19,17]],null]]]\n\n[17,null,[[0,17,null,[[2,44,17]],null]]]\n\n\n\n\n[91,\"m\",[[0,91]]]\n[91,null,[[0,91,null,[[13,29,91]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[3,null,[[0,3,null,[[6,24,3]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[3,null,[[0,3,null,[[6,25,3]],null]]]\n\n[6,null,[[0,6,null,[[4,25,6]],null]]]\n\n\n[91,null,[[0,91,null,[[14,67,91]],null]]]\n[89,null,[[0,89,null,[[2,25,89]],null]]]\n[89,null,[[0,89,null,[[2,27,89]],null]]]\n[89,null,[[0,89,null,[[2,36,89]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[27,null,[[0,27,null,[[4,20,27]],null]]]\n[27,null,[[0,27,null,[[4,41,27]],null]]]\n\n\n[89,null,[[0,89,null,[[2,48,89]],null]]]\n\n\n[76,\"m\",[[0,76]]]\n[76,null,[[0,76,null,[[20,37,76]],null]]]\n[76,null,[[0,76,null,[[13,29,76]],null]]]\n[76,null,[[0,76,null,[[2,19,76]],null]]]\n\n[76,null,[[0,76,null,[[2,27,76]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],[[6,28,76],[32,58,11]],null]]]\n[76,null,[[0,76,null,[[4,16,76]],null]]]\n\n[0,null,[[0,0,null,[[4,22,0]],null]]]\n\n\n[76,null,[[0,76,null,[],null]]]\n[91,null,[[0,91,null,[[4,52,91]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[19,null,[[0,19,null,[[6,28,19]],null]]]\n\n\n[70,null,[[0,70,null,[[2,29,70]],null]]]\n\n[70,null,[[0,70,null,[[2,32,70]],null]]]\n\n[70,null,[[0,70,null,[[2,59,70]],null]]]\n\n\n[28,\"m\",[[0,28]]]\n[28,null,[[0,28,null,[[13,29,28],[43,60,28]],null]]]\n[28,null,[[0,28,null,[[2,19,28]],null]]]\n\n[28,null,[[0,28,null,[[2,27,28]],null]]]\n\n[28,null,[[0,28,null,[[2,29,28]],null]]]\n[28,null,[[0,28,null,[],null]]]\n[31,null,[[0,31,null,[[4,43,31]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[3,null,[[0,3,null,[[6,28,3]],null]]]\n\n\n[28,null,[[0,28,null,[[2,29,28]],null]]]\n\n[28,null,[[0,28,null,[[2,32,28]],null]]]\n\n[28,null,[[0,28,null,[[2,61,28]],null]]]\n\n\n[45,\"m\",[[0,45]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,28,45],[32,53,45],[57,77,0],[80,106,45]],null]]]\n\n\n[6,\"m\",[[0,6]]]\n[6,null,[[0,6,null,[[2,25,6]],null]]]\n\n[6,null,[[0,6,null,[[2,27,6]],null]]]\n[6,null,[[0,6,null,[[2,46,6]],null]]]\n[6,null,[[0,6,null,[[2,45,6]],null]]]\n[6,null,[[0,6,null,[[2,27,6]],null]]]\n[6,null,[[0,6,null,[[2,47,6]],null]]]\n\n[6,null,[[0,6,null,[[2,33,6]],null]]]\n[6,null,[[0,6,null,[[2,52,6]],null]]]\n\n\n[13,\"m\",[[0,13]]]\n[13,null,[[0,13,null,[[2,19,13]],null]]]\n[13,null,[[0,13,null,[[2,19,13]],null]]]\n[13,null,[[0,13,null,[[2,29,13]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[4,67,2]],null]]]\n\n\n[13,null,[[0,13,null,[[2,25,13]],null]]]\n[13,null,[[0,13,null,[],null]]]\n[6,null,[[0,6,null,[[4,56,6]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[6,28,2]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[4,50,1]],null]]]\n\n[13,null,[[0,13,null,[[2,25,13]],null]]]\n[13,null,[[0,13,null,[[2,52,13]],null]]]\n\n[13,null,[[0,13,null,[[2,57,13]],null]]]\n\n\n[6,\"m\",[[0,6]]]\n[6,null,[[0,6,null,[[13,49,6]],null]]]\n[6,null,[[0,6,null,[[2,87,6]],null]]]\n[6,null,[[0,6,null,[[2,25,6]],null]]]\n[6,null,[[0,6,null,[[2,17,6]],null]]]\n[6,null,[[0,6,null,[[2,24,6]],null]]]\n[6,null,[[0,6,null,[[2,33,6]],null]]]\n[6,null,[[0,6,null,[[2,53,6]],null]]]\n\n\n[7,\"m\",[[0,7]]]\n[7,null,[[0,7,null,[[18,34,7]],null]]]\n[7,null,[[0,7,null,[[2,25,7]],null]]]\n[7,null,[[0,7,null,[[2,60,7]],null]]]\n[7,null,[[0,7,null,[[2,33,7]],null]]]\n[7,null,[[0,7,null,[[2,57,7]],null]]]\n\n\n[56,\"m\",[[0,56]]]\n[56,null,[[0,56,null,[[18,34,56]],null]]]\n\n\n\n\n[56,null,[[0,56,null,[[2,32,56]],null]]]\n[56,null,[[0,56,null,[[2,28,56]],null]]]\n[56,null,[[0,56,null,[[2,26,56]],null]]]\n\n[56,null,[[0,56,null,[[2,25,56]],null]]]\n\n[56,null,[[0,56,null,[],null]]]\n[52,null,[[0,52,null,[[19,24,52]],null]]]\n[52,null,[[0,52,null,[[19,35,52],[48,67,52]],null]]]\n[52,null,[[0,52,null,[[4,28,52]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,19,52],[23,50,7]],null]]]\n[4,null,[[0,4,null,[[6,18,4]],null]]]\n[4,null,[[0,4,null,[[6,22,4]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[6,null,[[0,6,null,[[6,79,6]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[15,36,46],[40,62,40]],null]]]\n[7,null,[[0,7,null,[[6,93,7]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[10,18,39],[22,42,2]],null]]]\n[0,null,[[0,0,null,[[8,45,0]],null]]]\n\n[39,null,[[0,39,null,[[8,56,39]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,32,39],[36,57,38]],null]]]\n\n[6,null,[[0,6,null,[[8,109,6]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[10,26,2]],null]]]\n\n[33,null,[[0,33,null,[[8,31,33]],null]]]\n[33,null,[[0,33,null,[[8,53,33]],null]]]\n[33,null,[[0,33,null,[[8,33,33]],null]]]\n[33,null,[[0,33,null,[[8,31,33]],null]]]\n[33,null,[[0,33,null,[[8,39,33]],null]]]\n[33,null,[[0,33,null,[[8,79,33]],null]]]\n\n\n\n\n[56,null,[[0,56,null,[[2,25,56]],null]]]\n\n[56,null,[[0,56,null,[[2,60,56]],null]]]\n\n\n[52,\"m\",[[0,52]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[6,24,52],[28,47,26],[51,73,26]],null]]]\n[0,null,[[0,0,null,[[4,22,0]],null]]]\n\n\n\n[84,\"m\",[[0,84]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[13,21,84],[25,41,7]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[13,21,84],[25,44,7]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[13,15,84],[19,41,7]],null]]]\n\n[84,null,[[0,84,null,[],null]]]\n[7,null,[[0,7,null,[[16,52,7]],null]]]\n[7,null,[[0,7,null,[[4,31,7]],null]]]\n[7,null,[[0,7,null,[[4,38,7]],null]]]\n[7,null,[[0,7,null,[[4,61,7]],null]]]\n\n\n[84,null,[[0,84,null,[[2,14,84]],null]]]\n\n\n[77,\"m\",[[0,77]]]\n[77,null,[[0,77,null,[[13,49,77]],null]]]\n\n[77,null,[[0,77,null,[[2,29,77]],null]]]\n[77,null,[[0,77,null,[[2,74,77]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[21,null,[[0,21,null,[[4,69,21]],null]]]\n\n\n[77,null,[[0,77,null,[[2,56,77]],null]]]\n\n\n[5,\"m\",[[0,5]]]\n[5,null,[[0,5,null,[[13,29,5]],null]]]\n[5,null,[[0,5,null,[[2,26,5]],null]]]\n[5,null,[[0,5,null,[[2,46,5]],null]]]\n[5,null,[[0,5,null,[[2,55,5]],null]]]\n\n\n[4,\"m\",[[0,4]]]\n[4,null,[[0,4,null,[[13,29,4]],null]]]\n[4,null,[[0,4,null,[[2,18,4]],null]]]\n[4,null,[[0,4,null,[[2,27,4]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,43,6],[47,71,6]],null]]]\n[4,null,[[0,4,null,[[4,42,4]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,31,\"2/2\"],[33,39,4]],null]]]\n[2,null,[[0,2,null,[[4,26,2]],null]]]\n\n[4,null,[[0,4,null,[[2,27,4]],null]]]\n[4,null,[[0,4,null,[[2,54,4]],null]]]\n\n\n[23,\"m\",[[0,23]]]\n[23,null,[[0,23,null,[[17,22,23]],null]]]\n[23,null,[[0,23,null,[[13,29,23]],null]]]\n[23,null,[[0,23,null,[[2,37,23]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[4,20,1]],null]]]\n\n[23,null,[[0,23,null,[[2,27,23]],null]]]\n[23,null,[[0,23,null,[[2,56,23]],null]]]\n[23,null,[[0,23,null,[[2,52,23]],null]]]\n\n\n[26,\"m\",[[0,26]]]\n[26,null,[[0,26,null,[[12,38,26]],null]]]\n[26,null,[[0,26,null,[],null]]]\n[13,null,[[0,13,null,[[4,55,13]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[4,null,[[0,4,null,[[6,28,4]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[3,null,[[0,3,null,[[4,49,3]],null]]]\n\n[26,null,[[0,26,null,[[2,13,26]],null]]]\n\n\n[299,\"m\",[[0,299]]]\n[299,null,[[0,299,null,[],null]]]\n[2,\"b\",[[0,2]]]\n[2,null,[[0,2,null,[[6,56,2]],null]]]\n\n[11,\"b\",[[0,11]]]\n[11,null,[[0,11,null,[[6,57,11]],null]]]\n\n[7,\"b\",[[0,7]]]\n[7,\"b\",[[0,7]]]\n[7,null,[[0,7,null,[[6,60,7]],null]]]\n\n[1,\"b\",[[0,1]]]\n[1,null,[[0,1,null,[[6,58,1]],null]]]\n\n[113,\"b\",[[0,113]]]\n[113,null,[[0,113,null,[[6,59,113]],null]]]\n\n[88,\"b\",[[0,88]]]\n[88,null,[[0,88,null,[[6,59,88]],null]]]\n\n[77,\"b\",[[0,77]]]\n[77,null,[[0,77,null,[[6,63,77]],null]]]\n\n\n\n\n\n\n[396,\"m\",[[0,396]]]\n[396,null,[[0,396,null,[[17,33,396],[46,65,396]],null]]]\n[396,null,[[0,396,null,[[13,29,396]],null]]]\n\n\n[396,null,[[0,396,null,[[22,27,396]],null]]]\n\n[396,null,[[0,396,null,[],null]]]\n[299,\"b\",[[0,299]]]\n[299,null,[[0,299,null,[[6,94,299]],null]]]\n\n[29,\"b\",[[0,29]]]\n[29,null,[[0,29,null,[[6,40,29]],null]]]\n\n[4,\"b\",[[0,4]]]\n[4,null,[[0,4,null,[[6,39,4]],null]]]\n\n[4,\"b\",[[0,4]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[3,null,[[0,3,null,[[8,71,3]],null]]]\n[2,null,[[0,2,null,[[8,31,2]],null]]]\n[2,null,[[0,2,null,[[8,49,2]],null]]]\n[2,null,[[0,2,null,[[8,33,2]],null]]]\n[2,null,[[0,2,null,[[8,29,2]],null]]]\n[2,null,[[0,2,null,[[8,31,2]],null]]]\n\n[2,null,[[0,2,null,[[8,30,2]],null]]]\n\n[2,null,[[0,2,null,[[8,47,2]],null]]]\n\n[2,null,[[0,2,null,[[8,63,2]],null]]]\n\n[1,null,[[0,1,null,[[6,12,1]],null]]]\n\n[28,\"b\",[[0,28]]]\n[28,null,[[0,28,null,[[6,18,28]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,32,28],[36,60,18]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[10,null,[[0,10,null,[[22,43,10]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[26,47,10],[51,69,9]],null]]]\n\n[7,null,[[0,7,null,[[10,31,7]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[9,null,[[0,9,null,[[8,36,9]],null]]]\n[9,null,[[0,9,null,[[8,31,9]],null]]]\n[9,null,[[0,9,null,[[8,20,9]],null]]]\n\n\n[19,null,[[0,19,null,[[6,47,19]],null]]]\n[19,null,[[0,19,null,[[6,31,19]],null]]]\n[19,null,[[0,19,null,[[6,27,19]],null]]]\n\n[19,null,[[0,19,null,[[6,29,19]],null]]]\n\n[19,null,[[0,19,null,[[6,28,19]],null]]]\n\n[19,null,[[0,19,null,[[6,45,19]],null]]]\n[19,null,[[0,19,null,[[6,33,19]],null]]]\n\n[19,null,[[0,19,null,[[6,61,19]],null]]]\n\n[4,\"b\",[[0,4]]]\n[4,null,[[0,4,null,[[6,36,4]],null]]]\n[4,null,[[0,4,null,[[6,50,4]],null]]]\n[4,null,[[0,4,null,[[6,85,4]],null]]]\n[4,null,[[0,4,null,[[6,18,4]],null]]]\n[4,null,[[0,4,null,[[6,66,4]],null]]]\n\n[1,\"b\",[[0,1]]]\n[2,null,[[0,2,null,[[6,40,2]],null]]]\n[2,null,[[0,2,null,[[6,18,2]],null]]]\n[2,null,[[0,2,null,[[6,67,2]],null]]]\n\n[4,\"b\",[[0,4]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[4,null,[[0,4,null,[[8,20,4]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[12,31,\"1/2\"],[33,51,0]],null]]]\n\n[4,null,[[0,4,null,[[8,39,4]],null]]]\n[4,null,[[0,4,null,[[8,52,4]],null]]]\n[4,null,[[0,4,null,[[8,87,4]],null]]]\n[4,null,[[0,4,null,[[8,20,4]],null]]]\n[4,null,[[0,4,null,[[8,69,4]],null]]]\n\n\n[8,\"b\",[[0,8]]]\n[8,null,[[0,8,null,[[6,36,8]],null]]]\n[8,null,[[0,8,null,[[6,50,8]],null]]]\n[8,null,[[0,8,null,[[6,85,8]],null]]]\n[8,null,[[0,8,null,[[6,18,8]],null]]]\n[8,null,[[0,8,null,[[6,67,8]],null]]]\n\n[2,\"b\",[[0,2]]]\n[2,null,[[0,2,null,[[6,40,2]],null]]]\n[2,null,[[0,2,null,[[6,18,2]],null]]]\n[2,null,[[0,2,null,[[6,64,2]],null]]]\n\n[1,\"b\",[[0,1]]]\n[1,null,[[0,1,null,[[6,40,1]],null]]]\n[1,null,[[0,1,null,[[6,18,1]],null]]]\n[1,null,[[0,1,null,[[6,57,1]],null]]]\n\n[5,\"b\",[[0,5]]]\n[5,null,[[0,5,null,[[6,18,5]],null]]]\n[5,null,[[0,5,null,[[6,59,5]],null]]]\n\n[6,\"b\",[[0,6]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[5,null,[[0,5,null,[[8,42,5]],null]]]\n\n\n\n[2,null,[[0,2,null,[[2,20,2]],null]]]\n\n\n[391,\"m\",[[0,391]]]\n[391,null,[[0,391,null,[[13,29,391]],null]]]\n[391,null,[[0,391,null,[[13,59,391]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[10,null,[[0,10,null,[[4,29,10]],null]]]\n[10,null,[[0,10,null,[[4,29,10]],null]]]\n[10,null,[[0,10,null,[[4,56,10]],null]]]\n\n[378,null,[[0,378,null,[[4,16,378]],null]]]\n\n\n\n[414,\"m\",[[0,414]]]\n[414,null,[[0,414,null,[[13,29,414]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[23,null,[[0,23,null,[[4,53,23]],null]]]\n[23,null,[[0,23,null,[[4,59,23]],null]]]\n\n[391,null,[[0,391,null,[[4,39,391]],null]]]\n\n\n\n[389,\"m\",[[0,389]]]\n[389,null,[[0,389,null,[[13,29,389]],null]]]\n[389,null,[[0,389,null,[[13,39,389]],null]]]\n[386,null,[[0,386,null,[[2,22,386]],null]]]\n[386,null,[[0,386,null,[],null]]]\n[2,null,[[0,2,null,[[4,48,2]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[35,39,384],[42,93,2]],null]]]\n\n\n[379,\"m\",[[0,379]]]\n[379,null,[[0,379,null,[[13,29,379]],null]]]\n[379,null,[[0,379,null,[[13,45,379]],null]]]\n[376,null,[[0,376,null,[[2,22,376]],null]]]\n[376,null,[[0,376,null,[],null]]]\n[10,null,[[0,10,null,[[4,54,10]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[35,39,367],[42,86,9]],null]]]\n\n\n[379,\"m\",[[0,379]]]\n[379,null,[[0,379,null,[[18,35,379]],null]]]\n[379,null,[[0,379,null,[[2,27,379]],null]]]\n[379,null,[[0,379,null,[[13,38,379]],null]]]\n[376,null,[[0,376,null,[[2,32,376]],null]]]\n[376,null,[[0,376,null,[[2,14,376]],null]]]\n\n\n[184,\"m\",[[0,184]]]\n[184,null,[[0,184,null,[[13,29,184]],null]]]\n[184,null,[[0,184,null,[[2,56,184]],null]]]\n[181,null,[[0,181,null,[[2,49,181]],null]]]\n\n\n[94,\"m\",[[0,94]]]\n\n[94,null,[[0,94,null,[[14,36,94]],null]]]\n[92,null,[[0,92,null,[[24,29,92]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[6,24,92],[28,49,0]],null]]]\n[0,null,[[0,0,null,[[4,29,0]],null]]]\n[0,null,[[0,0,null,[[4,27,0]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,27,92],[31,51,92]],null]]]\n[18,null,[[0,18,null,[[4,58,18]],null]]]\n[18,null,[[0,18,null,[[4,39,18]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],null,null]]]\n[0,null,[[0,0,null,[[4,26,0]],null]]]\n[0,null,[[0,0,null,[[4,39,0]],null]]]\n\n\n[92,null,[[0,92,null,[[2,15,92]],null]]]\n\n\n[16,\"m\",[[0,16]]]\n[16,null,[[0,16,null,[[2,55,16]],null]]]\n\n[16,null,[[0,16,null,[],null]]]\n\n\n\n\n\n\n\n[253,\"m\",[[0,253]]]\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,30,84],[34,50,17]],null]]]\n\n\n[17,null,[[0,17,null,[[8,57,17]],null]]]\n\n\n[84,null,[[0,84,null,[[6,53,84]],null]]]\n\n\n\n\n[253,\"m\",[[0,253]]]\n[300,\"m\",[[0,300]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,27,300],[31,50,256],[54,86,70]],null]]]\n[10,null,[[0,10,null,[[19,35,10]],null]]]\n[10,null,[[0,10,null,[[8,20,10]],null]]]\n[10,null,[[0,10,null,[[8,45,10]],null]]]\n\n[290,null,[[0,290,null,[[8,55,290]],null]]]\n\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],[[14,35,31],[39,58,19],[62,86,6],[90,109,2]],null]]]\n[31,null,[[0,31,null,[[12,47,31]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],null,null]]]\n[0,null,[[0,0,null,[[12,49,0]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[15,null,[[0,15,null,[[12,49,15]],null]]]\n\n\n\n\n[69,null,[[0,69,null,[[6,42,69]],null]]]\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[6,\"b\",[[0,6]]]\n[2,\"b\",[[0,2]]]\n[0,\"b\",[[0,0]]]\n\n\n\n[253,\"m\",[[0,253]]]\n[316,\"m\",[[0,316]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,26,316],[30,53,49]],null]]]\n[5,null,[[0,5,null,[[22,40,5]],null]]]\n[5,null,[[0,5,null,[],null]]]\n[5,null,[[0,5,null,[[10,66,5]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[5,null,[[0,5,null,[[12,31,5]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],[[37,44,5],[48,64,0]],null]]]\n[5,null,[[0,5,null,[[12,24,5]],null]]]\n\n[0,null,[[0,0,null,[[12,22,0]],null]]]\n\n\n\n\n[311,null,[[0,311,null,[[6,62,311]],null]]]\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[74,null,[[0,74,null,[[6,56,74]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[6,null,[[0,6,null,[[8,29,6]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[23,null,[[0,23,null,[[27,63,23]],null]]]\n[23,null,[[0,23,null,[[8,39,23]],null]]]\n[23,null,[[0,23,null,[[8,69,23]],null]]]\n\n[23,null,[[0,23,null,[[8,67,23]],null]]]\n\n\n[51,null,[[0,51,null,[[6,18,51]],null]]]\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[7,null,[[0,7,null,[[6,36,7]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],[[26,41,6],[45,52,0]],null]]]\n\n[7,null,[[0,7,null,[[6,18,7]],null]]]\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[4,null,[[0,4,null,[[8,33,4]],null]]]\n\n[4,null,[[0,4,null,[[30,46,4]],null]]]\n[4,null,[[0,4,null,[[8,20,4]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n\n[2,null,[[0,2,null,[[10,57,2]],null]]]\n[2,null,[[0,2,null,[[10,37,2]],null]]]\n[2,null,[[0,2,null,[[10,22,2]],null]]]\n\n\n[2,null,[[0,2,null,[[10,58,2]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[2,null,[[0,2,null,[[8,33,2]],null]]]\n[2,null,[[0,2,null,[[30,46,2]],null]]]\n[2,null,[[0,2,null,[[8,20,2]],null]]]\n[2,null,[[0,2,null,[[8,56,2]],null]]]\n\n[0,null,[[0,0,null,[[8,38,0]],null]]]\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[33,null,[[0,33,null,[[6,35,33]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[16,null,[[0,16,null,[[8,71,16]],null]]]\n\n\n\n\n\n\n[253,\"m\",[[0,253]]]\n[1260,\"m\",[[0,1260]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,27,1260],[31,46,469]],null]]]\n[11,null,[[0,11,null,[[8,21,11]],null]]]\n\n[1249,null,[[0,1249,null,[[8,38,1249]],null]]]\n\n\n\n\n\n[253,\"m\",[[0,253]]]\n[3518,\"m\",[[0,3518]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,27,3518],[32,43,1245],[47,58,1145]],null]]]\n[127,null,[[0,127,null,[[8,47,127]],null]]]\n\n[3391,null,[[0,3391,null,[[8,38,3391]],null]]]\n\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[[10,28,\"0/2\"],[30,54,0]],null]]]\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[4,null,[[0,4,null,[[8,75,4]],null]]]\n\n[39,null,[[0,39,null,[[8,49,39]],null]]]\n\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[34,null,[[0,34,null,[],null]]]\n[32,null,[[0,32,null,[[19,30,32]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,16,32],[20,54,32]],null]]]\n[12,null,[[0,12,null,[[10,55,12]],null]]]\n\n\n[34,null,[[0,34,null,[[6,51,34]],null]]]\n\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[14,null,[[0,14,null,[],null]]]\n[16,null,[[0,16,null,[[19,30,16]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[12,16,16],[20,38,16],[42,76,0]],null]]]\n[0,null,[[0,0,null,[[10,57,0]],null]]]\n\n\n\n[14,null,[[0,14,null,[[6,22,14]],null]]]\n\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[20,null,[[0,20,null,[[22,38,20]],null]]]\n[20,null,[[0,20,null,[[17,69,20]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],null,null]]]\n[0,null,[[0,0,null,[[8,39,0]],null]]]\n[0,null,[[0,0,null,[[8,36,0]],null]]]\n[0,null,[[0,0,null,[[8,66,0]],null]]]\n[0,null,[[0,0,null,[[8,64,0]],null]]]\n\n[20,null,[[0,20,null,[[8,20,20]],null]]]\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[223,null,[[0,223,null,[[8,44,223]],null]]]\n\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[4,null,[[0,4,null,[[8,61,4]],null]]]\n\n[4,null,[[0,4,null,[[6,36,4]],null]]]\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[13,33,14],[37,53,10]],null]]]\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[4,null,[[0,4,null,[[8,73,4]],null]]]\n\n[11,null,[[0,11,null,[[6,53,11]],null]]]\n[11,null,[[0,11,null,[[6,66,11]],null]]]\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[33,null,[[0,33,null,[[6,42,33]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,25,33],[29,51,6]],null]]]\n[2,null,[[0,2,null,[[8,78,2]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[5,null,[[0,5,null,[[8,20,5]],null]]]\n[5,null,[[0,5,null,[[26,46,5]],null]]]\n[5,null,[[0,5,null,[],null]]]\n[6,null,[[0,6,null,[[21,37,6]],null]]]\n[6,null,[[0,6,null,[[10,43,6]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[14,79,1]],null]]]\n\n[5,null,[[0,5,null,[[14,41,5]],null]]]\n\n[6,null,[[0,6,null,[[10,69,6]],null]]]\n\n\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[4,null,[[0,4,null,[[8,66,4]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[12,34,\"1/2\"],[36,54,0]],null]]]\n\n\n[17,null,[[0,17,null,[[6,35,17]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,19,4],[23,27,4]],null]]]\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[8,30,1]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[30,null,[[0,30,null,[[8,62,30]],null]]]\n\n[36,null,[[0,36,null,[[6,41,36]],null]]]\n[36,null,[[0,36,null,[[6,19,36]],null]]]\n\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[8,null,[[0,8,null,[[6,32,8]],null]]]\n\n[8,null,[[0,8,null,[[17,21,8]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[3,null,[[0,3,null,[[8,24,3]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[5,null,[[0,5,null,[[8,22,5]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[8,null,[[0,8,null,[[17,33,8]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[13,32,8],[36,55,3],[60,81,6],[85,104,4]],null]]]\n[6,null,[[0,6,null,[[10,22,6]],null]]]\n[6,null,[[0,6,null,[[10,33,6]],null]]]\n\n\n\n[8,null,[[0,8,null,[[6,29,8]],null]]]\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[6,null,[[0,6,null,[[8,71,6]],null]]]\n\n[32,null,[[0,32,null,[[6,29,32]],null]]]\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[91,null,[[0,91,null,[[6,29,91]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[68,null,[[0,68,null,[[8,64,68]],null]]]\n[67,null,[[0,67,null,[[8,47,67]],null]]]\n\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[5,null,[[0,5,null,[[8,57,5]],null]]]\n\n\n[7,null,[[0,7,null,[[6,42,7]],null]]]\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[13,33,7],[37,53,2]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[253,\"m\",[[0,253]]]\n[330,\"m\",[[0,330]]]\n[330,null,[[0,330,null,[[21,25,330]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,24,330],[28,54,330]],null]]]\n[50,null,[[0,50,null,[[22,40,50]],null]]]\n[50,null,[[0,50,null,[],null]]]\n[50,null,[[0,50,null,[[10,41,50]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[11,null,[[0,11,null,[[12,31,11]],null]]]\n[11,null,[[0,11,null,[[12,27,11]],null]]]\n\n[0,null,[[0,0,null,[[12,22,0]],null]]]\n\n\n\n\n\n\n[291,null,[[0,291,null,[[6,50,291]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,26,291],[30,52,280]],null]]]\n\n\n[16,null,[[0,16,null,[],null]]]\n[16,null,[[0,16,null,[[10,68,16]],null]]]\n\n[13,null,[[0,13,null,[[10,52,13]],null]]]\n[11,null,[[0,11,null,[[10,58,11]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],[[16,24,5],[28,31,0]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[8,null,[[0,8,null,[[10,33,8]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[10,25,2]],null]]]\n\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n[275,null,[[0,275,null,[[6,31,275]],null]]]\n\n[275,null,[[0,275,null,[[6,37,275]],null]]]\n\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[18,null,[[0,18,null,[[20,38,18]],null]]]\n[18,null,[[0,18,null,[],null]]]\n[18,null,[[0,18,null,[[27,57,18]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[14,35,\"2/2\"],[37,55,16]],null]]]\n\n[13,null,[[0,13,null,[[10,39,13]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[5,null,[[0,5,null,[[12,31,5]],null]]]\n\n[0,null,[[0,0,null,[[12,22,0]],null]]]\n\n\n\n\n[44,null,[[0,44,null,[[6,36,44]],null]]]\n\n\n\n[253,\"m\",[[0,253]]]\n[253,\"m\",[[0,253]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[4,null,[[0,4,null,[[8,20,4]],null]]]\n\n[7,null,[[0,7,null,[[8,32,7]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1,null,[[19,34,1]],null]]]\n[1,null,[[0,1,null,[[23,30,1]],null]]]\n\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n\n[91,\"m\",[[0,91]]]\n[91,null,[[0,91,null,[[2,37,91]],null]]]\n[91,null,[[0,91,null,[[2,37,91]],null]]]\n[91,null,[[0,91,null,[[2,33,91]],null]]]\n\n\n[87,\"m\",[[0,87]]]\n[87,null,[[0,87,null,[[12,36,87]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,23,87],[27,48,60],[52,69,66]],null]]]\n[48,null,[[0,48,null,[[4,29,48]],null]]]\n[48,null,[[0,48,null,[[4,61,48]],null]]]\n\n[39,null,[[0,39,null,[[4,34,39]],null]]]\n\n\n\n[1,null,[[0,1,null,[[9,25,1]],null]]]\n\n\n\n[81,\"m\",[[0,81]]]\n[81,null,[[0,81,null,[[12,14,81]],null]]]\n[81,null,[[0,81,null,[[19,33,81]],null]]]\n[81,null,[[0,81,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[6,null,[[0,6,null,[[6,64,6]],null]]]\n\n\n[352,null,[[0,352,null,[[13,50,352]],null]]]\n\n[352,null,[[0,352,null,[],null]]]\n[54,\"b\",[[0,54]]]\n[75,\"b\",[[0,75]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[14,23,53],[27,49,38]],null]]]\n[38,null,[[0,38,null,[[12,29,38]],null]]]\n[38,null,[[0,38,null,[[12,52,38]],null]]]\n\n[15,null,[[0,15,null,[[10,43,15]],null]]]\n\n[22,null,[[0,22,null,[[8,60,22]],null]]]\n[22,null,[[0,22,null,[[8,49,22]],null]]]\n\n[0,\"b\",[[0,0]]]\n[0,null,[[0,0,null,[[8,60,0]],null]]]\n[0,null,[[0,0,null,[[8,36,0]],null]]]\n[0,null,[[0,0,null,[[8,36,0]],null]]]\n[0,null,[[0,0,null,[[8,14,0]],null]]]\n\n[277,\"b\",[[0,277]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[9,null,[[0,9,null,[[10,62,9]],null]]]\n[9,null,[[0,9,null,[[10,43,9]],null]]]\n[9,null,[[0,9,null,[[10,38,9]],null]]]\n\n[268,null,[[0,268,null,[[10,27,268]],null]]]\n\n\n\n\n\n[11,\"m\",[[0,11]]]\n[11,null,[[0,11,null,[[11,48,11]],null]]]\n\n[11,null,[[0,11,null,[[2,19,11]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[6,15,11],[19,63,0]],null]]]\n[0,null,[[0,0,null,[[4,21,0]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[[26,30,0],[33,39,0]],null]]]\n\n[11,null,[[0,11,null,[[4,34,11]],null]]]\n\n[11,null,[[0,11,null,[[2,23,11]],null]]]\n[11,null,[[0,11,null,[[2,40,11]],null]]]\n\n[11,null,[[0,11,null,[[2,13,11]],null]]]\n\n\n[13,\"m\",[[0,13]]]\n[13,null,[[0,13,null,[[12,14,13]],null]]]\n[13,null,[[0,13,null,[[19,35,13]],null]]]\n[13,null,[[0,13,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[6,67,1]],null]]]\n\n\n[139,null,[[0,139,null,[[13,50,139]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,20,\"2/2\"],[22,28,139]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[4,null,[[0,4,null,[[6,58,4]],null]]]\n[4,null,[[0,4,null,[[6,34,4]],null]]]\n[4,null,[[0,4,null,[[6,34,4]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[6,58,2]],null]]]\n[2,null,[[0,2,null,[[6,40,2]],null]]]\n[2,null,[[0,2,null,[[6,34,2]],null]]]\n\n[121,null,[[0,121,null,[[6,23,121]],null]]]\n\n\n[12,null,[[0,12,null,[[2,56,12]],null]]]\n[12,null,[[0,12,null,[[2,42,12]],null]]]\n\n\n[4,\"m\",[[0,4]]]\n[4,null,[[0,4,null,[[12,14,4]],null]]]\n\n\n[4,null,[[0,4,null,[[11,37,4]],null]]]\n\n[4,null,[[0,4,null,[[17,33,4]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,43,20],[47,59,20]],null]]]\n[20,null,[[0,20,null,[[4,38,20]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[10,30,1]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[1,null,[[0,1,null,[[12,60,1]],null]]]\n\n[1,null,[[0,1,null,[[10,30,1]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[1,null,[[0,1,null,[[12,60,1]],null]]]\n\n\n[2,null,[[0,2,null,[[8,36,2]],null]]]\n\n[4,null,[[0,4,null,[[6,12,4]],null]]]\n\n[16,null,[[0,16,null,[[4,14,16]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[4,30,1]],null]]]\n[1,null,[[0,1,null,[[4,15,1]],null]]]\n\n[3,null,[[0,3,null,[[2,16,3]],null]]]\n\n\n\n\n\n\n\n\n\n\n[125,\"m\",[[0,125]]]\n\n[125,null,[[0,125,null,[[14,28,125]],null]]]\n[125,null,[[0,125,null,[],null]]]\n[282,null,[[0,282,null,[[4,49,282]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[11,31,282],[35,44,127]],null]]]\n[125,null,[[0,125,null,[[2,79,125]],null]]]\n\n\n\n\n[67,\"m\",[[0,67]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[59,null,[[0,59,null,[[4,23,59]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[4,58,2]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[6,null,[[0,6,null,[[4,91,6]],null]]]\n\n\n\n\n\n[124,\"m\",[[0,124]]]\n[124,null,[[0,124,null,[[13,29,124]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[124,null,[[0,124,null,[[4,33,124]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],null,null]]]\n[0,null,[[0,0,null,[[4,40,0]],null]]]\n\n[0,null,[[0,0,null,[[4,22,0]],null]]]\n\n[124,null,[[0,124,null,[[2,14,124]],null]]]\n[124,null,[[0,124,null,[[2,48,124]],null]]]\n\n\n\n\n[112,\"m\",[[0,112]]]\n[112,null,[[0,112,null,[[17,33,112],[46,65,112]],null]]]\n[112,null,[[0,112,null,[[13,38,112]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,25,\"2/2\"],[27,39,112]],null]]]\n\n[6,null,[[0,6,null,[[13,49,6]],null]]]\n[6,null,[[0,6,null,[[2,24,6]],null]]]\n[6,null,[[0,6,null,[[2,40,6]],null]]]\n[6,null,[[0,6,null,[[2,52,6]],null]]]\n\n\n\n\n\n[90,\"m\",[[0,90]]]\n[90,null,[[0,90,null,[[17,33,90],[46,65,90]],null]]]\n[90,null,[[0,90,null,[[13,42,90]],null]]]\n[90,null,[[0,90,null,[],null]]]\n[6,null,[[0,6,null,[[18,54,6]],null]]]\n[6,null,[[0,6,null,[[4,26,6]],null]]]\n[6,null,[[0,6,null,[[4,49,6]],null]]]\n[6,null,[[0,6,null,[[4,59,6]],null]]]\n\n[90,null,[[0,90,null,[[2,14,90]],null]]]\n\n\n\n\n[20,\"m\",[[0,20]]]\n\n[20,null,[[0,20,null,[],null]]]\n[5,\"b\",[[0,5]]]\n[5,null,[[0,5,null,[[6,48,5]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[[8,94,1]],null]]]\n\n[4,null,[[0,4,null,[[8,20,4]],null]]]\n\n\n[2,\"b\",[[0,2]]]\n[14,\"b\",[[0,14]]]\n[14,null,[[0,14,null,[[6,34,14]],null]]]\n[14,null,[[0,14,null,[[6,24,14]],null]]]\n[14,null,[[0,14,null,[[6,18,14]],null]]]\n\n[1,\"b\",[[0,1]]]\n[1,null,[[0,1,null,[[6,100,1]],null]]]\n\n\n\n\n\n\n\n[3,\"m\",[[0,3]]]\n[3,null,[[0,3,null,[[13,66,3]],null]]]\n[3,null,[[0,3,null,[[2,82,3]],null]]]\n\n\n\n\n[2,\"m\",[[0,2]]]\n[2,null,[[0,2,null,[[13,29,2]],null]]]\n[2,null,[[0,2,null,[[2,25,2]],null]]]\n[2,null,[[0,2,null,[[2,27,2]],null]]]\n[2,null,[[0,2,null,[[2,43,2]],null]]]\n[2,null,[[0,2,null,[[2,25,2]],null]]]\n\n[2,null,[[0,2,null,[[2,49,2]],null]]]\n\n\n\n\n\n[18,\"m\",[[0,18]]]\n[18,null,[[0,18,null,[[13,29,18]],null]]]\n[18,null,[[0,18,null,[[2,14,18]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[3,null,[[0,3,null,[[4,53,3]],null]]]\n\n[15,null,[[0,15,null,[[4,45,15]],null]]]\n\n[18,null,[[0,18,null,[[2,25,18]],null]]]\n[14,null,[[0,14,null,[[2,57,14]],null]]]\n\n\n\n\n[28,\"m\",[[0,28]]]\n[28,null,[[0,28,null,[[13,29,28]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[6,null,[[0,6,null,[[4,29,6]],null]]]\n[6,null,[[0,6,null,[[4,44,6]],null]]]\n[6,null,[[0,6,null,[[4,27,6]],null]]]\n[6,null,[[0,6,null,[[4,55,6]],null]]]\n\n[22,null,[[0,22,null,[[2,44,22]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[33,62,20],[65,69,1]],null]]]\n[19,null,[[0,19,null,[[2,47,19]],null]]]\n\n\n\n\n[63,\"m\",[[0,63]]]\n[63,null,[[0,63,null,[[13,49,63]],null]]]\n[63,null,[[0,63,null,[[2,23,63]],null]]]\n[63,null,[[0,63,null,[[2,41,63]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,30,88],[34,59,67]],null]]]\n[28,null,[[0,28,null,[[4,51,28]],null]]]\n\n[60,null,[[0,60,null,[[2,40,60]],null]]]\n[60,null,[[0,60,null,[[2,28,60]],null]]]\n[58,null,[[0,58,null,[[2,52,58]],null]]]\n\n\n\n\n[27,\"m\",[[0,27]]]\n[27,null,[[0,27,null,[[13,49,27]],null]]]\n[27,null,[[0,27,null,[[2,41,27]],null]]]\n[27,null,[[0,27,null,[[2,28,27]],null]]]\n[27,null,[[0,27,null,[[2,52,27]],null]]]\n\n\n\n\n\n[63,\"m\",[[0,63]]]\n[63,null,[[0,63,null,[[13,49,63]],null]]]\n[63,null,[[0,63,null,[[17,19,63]],null]]]\n[63,null,[[0,63,null,[[23,72,63]],null]]]\n[58,null,[[0,58,null,[[23,27,58]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[37,null,[[0,37,null,[],null]]]\n[75,null,[[0,75,null,[],null]]]\n[38,\"b\",[[0,38]]]\n[38,null,[[0,38,null,[[10,38,38],[39,70,38]],null]]]\n[38,null,[[0,38,null,[[10,22,38]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[27,null,[[0,27,null,[[12,79,27]],null]]]\n[27,null,[[0,27,null,[[12,27,27]],null]]]\n\n[11,null,[[0,11,null,[[10,68,11]],null]]]\n[5,null,[[0,5,null,[[10,16,5]],null]]]\n\n[22,\"b\",[[0,22]]]\n[22,null,[[0,22,null,[[10,46,22]],null]]]\n[22,null,[[0,22,null,[[10,16,22]],null]]]\n\n[15,\"b\",[[0,15]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[12,54,2]],null]]]\n\n[13,null,[[0,13,null,[[12,62,13]],null]]]\n\n\n[11,null,[[0,11,null,[[10,16,11]],null]]]\n\n[0,\"b\",[[0,0]]]\n[0,null,[[0,0,null,[[10,28,0]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n\n[47,null,[[0,47,null,[[2,39,47]],null]]]\n[47,null,[[0,47,null,[[2,39,47]],null]]]\n[47,null,[[0,47,null,[[2,27,47]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,31,47],[35,59,1]],null]]]\n[1,null,[[0,1,null,[[4,94,1]],null]]]\n\n[46,null,[[0,46,null,[[2,45,46]],null]]]\n\n\n\n\n[52,\"m\",[[0,52]]]\n[52,null,[[0,52,null,[[17,33,52],[46,65,52]],null]]]\n[52,null,[[0,52,null,[[2,14,52]],null]]]\n[52,null,[[0,52,null,[[2,52,52]],null]]]\n\n\n[251,\"m\",[[0,251]]]\n[251,\"m\",[[0,251]]]\n[385,\"m\",[[0,385]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[22,null,[[0,22,null,[[19,65,22]],null]]]\n\n[22,null,[[0,22,null,[[8,26,22]],null]]]\n[22,null,[[0,22,null,[[8,20,22]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[52,null,[[0,52,null,[[8,38,52]],null]]]\n\n[311,null,[[0,311,null,[[8,56,311]],null]]]\n\n\n\n\n[251,\"m\",[[0,251]]]\n[3351,\"m\",[[0,3351]]]\n[3351,null,[[0,3351,null,[[20,37,3351]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[81,null,[[0,81,null,[[8,35,81]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,31,3270],[35,56,3012]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[125,null,[[0,125,null,[[10,36,125]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[87,null,[[0,87,null,[[10,27,87]],null]]]\n[87,null,[[0,87,null,[[10,48,87]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[13,24,108],[28,39,95],[44,65,13]],null]]]\n[13,null,[[0,13,null,[[10,42,13]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,21,3045],[25,47,119]],null]]]\n[53,null,[[0,53,null,[[8,25,53]],null]]]\n[53,null,[[0,53,null,[[8,48,53]],null]]]\n\n\n[2992,null,[[0,2992,null,[[6,36,2992]],null]]]\n\n\n\n[251,\"m\",[[0,251]]]\n[3939,\"m\",[[0,3939]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[212,null,[[0,212,null,[[25,42,212]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[11,null,[[0,11,null,[[10,54,11]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[15,null,[[0,15,null,[[10,52,15]],null]]]\n\n[186,null,[[0,186,null,[[10,37,186]],null]]]\n\n[212,null,[[0,212,null,[[8,38,212]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[17,37,3727],[41,68,50]],null]]]\n[27,null,[[0,27,null,[[8,39,27]],null]]]\n[27,null,[[0,27,null,[[8,43,27]],null]]]\n[27,null,[[0,27,null,[[8,39,27]],null]]]\n\n[3700,null,[[0,3700,null,[[8,42,3700]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n\n\n\n\n\n\n\n[10,\"m\",[[0,10]]]\n[10,null,[[0,10,null,[[4,23,10]],null]]]\n[10,null,[[0,10,null,[[4,27,10]],null]]]\n[10,null,[[0,10,null,[[4,41,10]],null]]]\n[10,null,[[0,10,null,[[4,29,10]],null]]]\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n[87,\"m\",[[0,87,null,[[51,68,87]],null]]]\n\n\n\n\n\n[2509,\"m\",[[0,2509]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[4,null,[[0,4,null,[[4,34,4]],null]]]\n[4,null,[[0,4,null,[[4,11,4]],null]]]\n\n\n[2505,null,[[0,2505,null,[[12,36,2505]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,34,2505],[38,84,831]],null]]]\n[83,null,[[0,83,null,[[4,29,83]],null]]]\n[83,null,[[0,83,null,[[4,35,83]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[30,null,[[0,30,null,[[4,34,30]],null]]]\n\n[2392,null,[[0,2392,null,[[4,41,2392]],null]]]\n\n\n\n[3962,\"m\",[[0,3962]]]\n[3962,null,[[0,3962,null,[[2,33,3962]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,26,3962],[30,52,3925],[56,76,3895]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[20,null,[[0,20,null,[[6,36,20]],null]]]\n\n\n\n\n[1353,\"m\",[[0,1353]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[56,76,996],[79,100,357]],null]]]\n[1353,null,[[0,1353,null,[[2,32,1353]],null]]]\n\n\n[17,\"m\",[[0,17]]]\n[17,null,[[0,17,null,[[2,47,17]],null]]]\n[17,null,[[0,17,null,[[2,32,17]],null]]]\n\n\n[1488,\"m\",[[0,1488]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[24,43,1488],[47,67,1461]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[24,45,1387],[49,71,1375]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[44,64,179],[67,88,1309]],null]]]\n[1488,null,[[0,1488,null,[[2,32,1488]],null]]]\n\n\n[67,\"m\",[[0,67]]]\n\n\n\n[441,\"m\",[[0,441]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[98,null,[[0,98,null,[[4,54,98]],null]]]\n\n\n[441,null,[[0,441,null,[[2,33,441]],null]]]\n\n\n[59,\"m\",[[0,59]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[24,null,[[0,24,null,[[4,29,24]],null]]]\n\n[35,null,[[0,35,null,[[4,44,35]],null]]]\n\n[59,null,[[0,59,null,[[2,33,59]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[17531,\"m\",[[0,17531]]]\n[17531,null,[[0,17531,null,[[4,27,17531]],null]]]\n[17531,null,[[0,17531,null,[[4,29,17531]],null]]]\n[17531,null,[[0,17531,null,[[4,29,17531]],null]]]\n[17531,null,[[0,17531,null,[[4,25,17531]],null]]]\n[17531,null,[[0,17531,null,[[4,64,17531]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n[31,\"m\",[[0,31]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[21,null,[[0,21,null,[[4,37,21]],null]]]\n\n[10,null,[[0,10,null,[[4,102,10]],null]]]\n\n\n\n\n[2123,\"m\",[[0,2123]]]\n[2123,null,[[0,2123,null,[[4,27,2123]],null]]]\n[2123,null,[[0,2123,null,[[4,36,2123]],null]]]\n\n\n\n\n[17576,\"m\",[[0,17576]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[17531,null,[[0,17531,null,[[6,52,17531]],null]]]\n\n\n[17576,null,[[0,17576,null,[[4,43,17576]],null]]]\n[17576,null,[[0,17576,null,[[4,47,17576]],null]]]\n[17576,null,[[0,17576,null,[[4,49,17576]],null]]]\n[17576,null,[[0,17576,null,[[4,53,17576]],null]]]\n[17576,null,[[0,17576,null,[[4,21,17576]],null]]]\n\n\n\n\n[32202,\"m\",[[0,32202]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[8994,null,[[0,8994,null,[[6,18,8994]],null]]]\n[8976,null,[[0,8976,null,[[6,18,8976]],null]]]\n\n[23208,null,[[0,23208,null,[[6,19,23208]],null]]]\n\n\n\n\n\n[83412,\"m\",[[0,83412]]]\n[83412,null,[[0,83412,null,[[4,36,83412]],null]]]\n\n\n\n\n[5955,\"m\",[[0,5955]]]\n[5955,null,[[0,5955,null,[[4,27,5955]],null]]]\n\n\n\n\n[45,\"m\",[[0,45]]]\n[45,null,[[0,45,null,[[14,24,45]],null]]]\n[45,null,[[0,45,null,[[4,33,45]],null]]]\n\n[45,null,[[0,45,null,[[4,28,45]],null]]]\n[45,null,[[0,45,null,[[4,16,45]],null]]]\n[45,null,[[0,45,null,[[4,29,45]],null]]]\n\n[45,null,[[0,45,null,[[15,37,45]],null]]]\n[45,null,[[0,45,null,[[4,21,45]],null]]]\n[45,null,[[0,45,null,[[4,16,45]],null]]]\n\n\n\n\n\n[235,\"m\",[[0,235]]]\n[235,null,[[0,235,null,[[4,31,235]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,27,235],[31,53,227],[55,62,235]],null]]]\n[12,null,[[0,12,null,[[4,38,12]],null]]]\n[12,null,[[0,12,null,[],null]]]\n[0,null,[[0,0,null,[[6,88,0]],null]]]\n[0,null,[[0,0,null,[[6,27,0]],null]]]\n\n[12,null,[[0,12,null,[[4,21,12]],null]]]\n\n\n[24694,\"m\",[[0,24694]]]\n[24694,null,[[0,24694,null,[[4,61,24694]],null]]]\n\n\n\n\n\n[19714,\"m\",[[0,19714]]]\n[19714,null,[[0,19714,null,[[21,38,19714]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,19,19714],[23,48,19714],[50,67,19714]],null]]]\n\n[19702,null,[[0,19702,null,[[4,37,19702]],null]]]\n[19702,null,[[0,19702,null,[[4,36,19702]],null]]]\n[19702,null,[[0,19702,null,[[4,38,19702]],null]]]\n[19702,null,[[0,19702,null,[[4,51,19702]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,43,\"2/2\"],[45,77,19702]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[87,null,[[0,87,null,[[6,39,87]],null]]]\n\n[16681,null,[[0,16681,null,[[6,54,16681]],null]]]\n\n\n\n[16195,\"m\",[[0,16195]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,31,16195],[35,46,10264]],null]]]\n[5944,null,[[0,5944,null,[[6,29,5944]],null]]]\n\n[10251,null,[[0,10251,null,[[6,41,10251]],null]]]\n\n\n\n[45377,\"m\",[[0,45377]]]\n[45377,null,[[0,45377,null,[[15,52,45377]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,22,45377],[26,40,212],[42,54,45377]],null]]]\n\n[212,null,[[0,212,null,[[15,56,212]],null]]]\n[212,null,[[0,212,null,[[4,43,212]],null]]]\n\n\n[88,\"m\",[[0,88]]]\n[88,null,[[0,88,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[20,34,47],[37,50,41]],null]]]\n\n\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[87,null,[[0,87,null,[[6,38,87]],null]]]\n[87,null,[[0,87,null,[[6,40,87]],null]]]\n[87,null,[[0,87,null,[[6,31,87]],null]]]\n\n\n\n[59,\"m\",[[0,59]]]\n[59,null,[[0,59,null,[[19,43,59]],null]]]\n[59,null,[[0,59,null,[[16,30,59],[38,83,59]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,18,\"2/2\"],[20,75,59]],null]]]\n\n[47,null,[[0,47,null,[[4,29,47]],null]]]\n[47,null,[[0,47,null,[[4,33,47]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,47,81],[52,80,48]],null]]]\n[34,null,[[0,34,null,[[6,27,34]],null]]]\n[34,null,[[0,34,null,[[6,59,34]],null]]]\n\n\n[47,null,[[0,47,null,[[4,120,47]],null]]]\n\n\n[41,\"m\",[[0,41]]]\n[41,null,[[0,41,null,[[16,30,41]],null]]]\n[41,null,[[0,41,null,[[19,43,41]],null]]]\n[41,null,[[0,41,null,[[13,63,41]],null]]]\n[\"5/5\",\"b\",[[0,\"5/5\",[],[[11,45,509],[49,58,503],[62,71,470],[75,86,468],[90,101,468]],null]]]\n[468,null,[[0,468,null,[[6,23,468]],null]]]\n[468,null,[[0,468,null,[[6,49,468]],null]]]\n\n\n[41,null,[[0,41,null,[[4,140,41]],null]]]\n\n\n\n\n\n[19549,\"m\",[[0,19549]]]\n[19549,null,[[0,19549,null,[],null]]]\n[24559,null,[[0,24559,null,[[15,52,24559]],null]]]\n[24559,null,[[0,24559,null,[],null]]]\n[7518,\"b\",[[0,7518]]]\n[7518,null,[[0,7518,null,[[10,27,7518]],null]]]\n[7518,null,[[0,7518,null,[[10,16,7518]],null]]]\n\n[6,\"b\",[[0,6]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],null,null]]]\n[0,null,[[0,0,null,[[12,29,0]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,16,341],[17,27,342]],null]]]\n[342,null,[[0,342,null,[[10,27,342]],null]]]\n[342,null,[[0,342,null,[[10,31,342]],null]]]\n[342,null,[[0,342,null,[[10,48,342]],null]]]\n[342,null,[[0,342,null,[[10,16,342]],null]]]\n\n[200,\"b\",[[0,200]]]\n[200,null,[[0,200,null,[],null]]]\n[59,\"b\",[[0,59]]]\n[59,null,[[0,59,null,[[14,38,59]],null]]]\n[47,null,[[0,47,null,[[14,20,47]],null]]]\n\n[36,\"b\",[[0,36]]]\n[36,null,[[0,36,null,[[14,38,36]],null]]]\n[36,null,[[0,36,null,[[14,20,36]],null]]]\n\n[105,\"b\",[[0,105]]]\n[105,null,[[0,105,null,[[14,25,105]],null]]]\n\n[83,null,[[0,83,null,[[10,16,83]],null]]]\n\n[16499,\"b\",[[0,16499]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[14,20,16499],[24,31,16499],[35,45,16499],[49,97,21]],null]]]\n[2,null,[[0,2,null,[[12,29,2]],null]]]\n\n[16497,null,[[0,16497,null,[[12,23,16497]],null]]]\n\n\n\n\n\n\n\n\n\n\n[19578,\"m\",[[0,19578]]]\n[19578,null,[[0,19578,null,[[4,36,19578]],null]]]\n[19578,null,[[0,19578,null,[[4,49,19578]],null]]]\n[19578,null,[[0,19578,null,[[19,34,19578]],null]]]\n[19578,null,[[0,19578,null,[[4,27,19578]],null]]]\n[19578,null,[[0,19578,null,[[4,27,19578]],null]]]\n\n[19578,null,[[0,19578,null,[[4,33,19578]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n[232,\"m\",[[0,232]]]\n[232,null,[[0,232,null,[[15,56,232]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,18,232],[22,32,109]],null]]]\n[3,null,[[0,3,null,[[6,35,3]],null]]]\n\n\n[229,null,[[0,229,null,[[16,57,229]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,19,229],[23,35,119]],null]]]\n[116,null,[[0,116,null,[[6,26,116]],null]]]\n[116,null,[[0,116,null,[[6,43,116]],null]]]\n\n[113,null,[[0,113,null,[[6,23,113]],null]]]\n[113,null,[[0,113,null,[[6,38,113]],null]]]\n\n\n\n[105,\"m\",[[0,105]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[37,null,[[0,37,null,[[6,23,37]],null]]]\n[37,null,[[0,37,null,[[6,31,37]],null]]]\n\n\n[68,null,[[0,68,null,[[15,56,68]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[6,41,2]],null]]]\n\n[66,null,[[0,66,null,[[6,40,66]],null]]]\n\n\n\n[166,\"m\",[[0,166]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[29,36,156],[39,48,10]],null]]]\n\n[166,null,[[0,166,null,[[15,56,166]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[15,null,[[0,15,null,[[6,14,15]],null]]]\n[15,null,[[0,15,null,[[6,55,15]],null]]]\n[15,null,[[0,15,null,[[6,25,15]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[5,null,[[0,5,null,[[6,14,5]],null]]]\n[5,null,[[0,5,null,[[6,23,5]],null]]]\n\n\n[166,null,[[0,166,null,[[4,38,166]],null]]]\n\n\n[68,\"m\",[[0,68]]]\n[68,null,[[0,68,null,[[15,56,68]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,57,\"2/2\"],[59,71,11],[74,87,11]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,19,\"2/2\"],[21,56,46]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[40,52,28],[55,68,14]],null]]]\n\n\n[13,\"m\",[[0,13]]]\n[13,null,[[0,13,null,[[15,56,13]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[6,41,2]],null]]]\n\n[11,null,[[0,11,null,[[6,45,11]],null]]]\n\n\n\n[170,\"m\",[[0,170]]]\n[170,null,[[0,170,null,[[15,56,170]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,21,68],[25,73,32],[77,148,2]],null]]]\n\n[1,null,[[0,1,null,[[8,32,1]],null]]]\n[1,null,[[0,1,null,[[8,25,1]],null]]]\n[1,null,[[0,1,null,[[8,32,1]],null]]]\n\n[67,null,[[0,67,null,[[6,41,67]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[5,null,[[0,5,null,[[6,41,5]],null]]]\n\n[97,null,[[0,97,null,[[6,42,97]],null]]]\n\n\n\n[118,\"m\",[[0,118]]]\n[118,null,[[0,118,null,[[15,56,118]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[13,24,19],[28,76,9]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,61,\"2/2\"],[63,105,19]],null]]]\n[13,null,[[0,13,null,[[6,46,13]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,19,99],[23,34,2],[38,86,2],[90,138,2]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[10,23,\"1/2\"],[25,43,0]],null]]]\n\n[2,null,[[0,2,null,[[6,30,2]],null]]]\n[2,null,[[0,2,null,[[6,23,2]],null]]]\n[2,null,[[0,2,null,[[6,30,2]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n\n[7,null,[[0,7,null,[[6,15,7]],null]]]\n\n\n[97,null,[[0,97,null,[[4,46,97]],null]]]\n\n\n[707,\"m\",[[0,707]]]\n[707,null,[[0,707,null,[[15,56,707]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,19,\"2/2\"],[21,113,707]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,19,698],[23,34,695]],null]]]\n[175,null,[[0,175,null,[[6,26,175]],null]]]\n[175,null,[[0,175,null,[[6,40,175]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[39,44,520],[47,56,3]],null]]]\n\n\n[10266,\"m\",[[0,10266]]]\n[10266,null,[[0,10266,null,[],null]]]\n\n\n[232,\"b\",[[0,232]]]\n[232,null,[[0,232,null,[[8,36,232]],null]]]\n\n\n[1488,\"b\",[[0,1488,null,[[15,32,1488],[33,68,1488]],null]]]\n[1321,\"b\",[[0,1321,null,[[15,32,1321],[33,68,1321]],null]]]\n[788,\"b\",[[0,788,null,[[15,32,788],[33,66,788]],null]]]\n[392,\"b\",[[0,392,null,[[15,32,392],[33,67,392]],null]]]\n[239,\"b\",[[0,239,null,[[15,32,239],[33,70,239]],null]]]\n[239,\"b\",[[0,239,null,[[15,32,239],[33,70,239]],null]]]\n[1379,\"b\",[[0,1379,null,[[16,33,1379],[34,69,1379]],null]]]\n[1188,\"b\",[[0,1188,null,[[16,33,1188],[34,69,1188]],null]]]\n\n[483,\"b\",[[0,483]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[12,42,483],[46,94,0]],null]]]\n[0,null,[[0,0,null,[[10,50,0]],null]]]\n\n[483,null,[[0,483,null,[[10,27,483]],null]]]\n[483,null,[[0,483,null,[[10,44,483]],null]]]\n\n\n[51,\"b\",[[0,51,null,[[15,32,51],[33,70,51]],null]]]\n[33,\"b\",[[0,33,null,[[15,32,33],[33,64,33]],null]]]\n\n[35,\"b\",[[0,35]]]\n[35,null,[[0,35,null,[[8,25,35]],null]]]\n[35,null,[[0,35,null,[[8,46,35]],null]]]\n\n[189,\"b\",[[0,189]]]\n[189,null,[[0,189,null,[[19,60,189]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,24,189],[28,39,178],[41,73,189]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,24,176],[28,39,156],[41,72,176]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,23,140],[27,38,122],[40,71,140]],null]]]\n\n\n[\"8/8\",\"b\",[[0,\"8/8\",[],[[6,14,301],[15,23,359],[24,32,407],[33,41,564],[42,50,574],[51,59,578],[60,68,580],[69,77,588]],null]]]\n[588,null,[[0,588,null,[[8,38,588]],null]]]\n\n\n[250,\"b\",[[0,250]]]\n[371,null,[[0,371,null,[[8,37,371]],null]]]\n\n\n\n\n\n\n[105,\"b\",[[0,105]]]\n[105,null,[[0,105,null,[[8,38,105]],null]]]\n\n[10,\"b\",[[0,10]]]\n[166,null,[[0,166,null,[[8,48,166]],null]]]\n\n[41,\"b\",[[0,41]]]\n[68,null,[[0,68,null,[[8,45,68]],null]]]\n\n[13,\"b\",[[0,13]]]\n[13,null,[[0,13,null,[[8,38,13]],null]]]\n\n[105,\"b\",[[0,105]]]\n[170,null,[[0,170,null,[[8,45,170]],null]]]\n\n[104,\"b\",[[0,104]]]\n[118,null,[[0,118,null,[[8,42,118]],null]]]\n\n[700,\"b\",[[0,700]]]\n[707,null,[[0,707,null,[[8,44,707]],null]]]\n\n[2,\"b\",[[0,2]]]\n[2,null,[[0,2,null,[[8,43,2]],null]]]\n\n\n[7,null,[[0,7,null,[[4,84,7]],null]]]\n\n\n[1261,\"m\",[[0,1261]]]\n[1261,null,[[0,1261,null,[[14,69,1261]],null]]]\n[1261,null,[[0,1261,null,[[4,27,1261]],null]]]\n[1261,null,[[0,1261,null,[[4,39,1261]],null]]]\n\n\n[37,\"m\",[[0,37]]]\n[37,null,[[0,37,null,[[34,48,37]],null]]]\n[37,null,[[0,37,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,45,\"2/2\"],[47,100,206]],null]]]\n[202,null,[[0,202,null,[[15,48,202]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[5,null,[[0,5,null,[[8,61,5]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[7,null,[[0,7,null,[[8,24,7]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[10,null,[[0,10,null,[[10,25,10]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[19,29,180],[33,40,10]],null]]]\n[10,null,[[0,10,null,[[10,26,10]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[19,29,170],[33,41,28]],null]]]\n[28,null,[[0,28,null,[[10,16,28]],null]]]\n\n[162,null,[[0,162,null,[[8,30,162]],null]]]\n\n[169,null,[[0,169,null,[[6,23,169]],null]]]\n\n[28,null,[[0,28,null,[[18,57,28]],null]]]\n[28,null,[[0,28,null,[[4,21,28]],null]]]\n\n\n[28,null,[[0,28,null,[[15,31,28]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[10,null,[[0,10,null,[[23,36,10]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,32,\"2/2\"],[34,87,10]],null]]]\n\n[26,null,[[0,26,null,[],null]]]\n\n\n\n\n\n\n\n\n\n[740,\"m\",[[0,740]]]\n[740,null,[[0,740,null,[[16,30,740]],null]]]\n[740,null,[[0,740,null,[],null]]]\n[1906,null,[[0,1906,null,[[17,54,1906]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[67,null,[[0,67,null,[[8,29,67]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[47,null,[[0,47,null,[[8,29,47]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[17,27,1792],[31,41,1313]],null]]]\n[1187,null,[[0,1187,null,[[8,24,1187]],null]]]\n\n[605,null,[[0,605,null,[[8,23,605]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,22,\"2/2\"],[24,30,1906]],null]]]\n[1203,null,[[0,1203,null,[[6,23,1203]],null]]]\n[1203,null,[[0,1203,null,[[6,34,1203]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,32,740],[36,47,704],[51,81,40],[83,95,740]],null]]]\n\n[699,null,[[0,699,null,[[4,17,699]],null]]]\n\n\n[83,\"m\",[[0,83]]]\n[83,null,[[0,83,null,[[4,24,83]],null]]]\n[83,null,[[0,83,null,[[14,33,83]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,19,\"2/2\"],[21,91,83]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,51,\"2/2\"],[53,116,65]],null]]]\n[55,null,[[0,55,null,[[4,41,55]],null]]]\n\n\n\n\n[591,\"m\",[[0,591]]]\n[591,null,[[0,591,null,[[16,30,591],[42,47,591],[57,101,591]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[8,22,591],[26,51,588],[53,89,591]],null]]]\n[591,null,[[0,591,null,[[15,52,591]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[10,null,[[0,10,null,[[6,23,10]],null]]]\n[10,null,[[0,10,null,[[6,23,10]],null]]]\n[10,null,[[0,10,null,[[6,21,10]],null]]]\n[10,null,[[0,10,null,[[6,51,10]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,19,591],[23,35,591]],null]]]\n[11,null,[[0,11,null,[[6,53,11]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,21,11],[25,36,7],[38,55,11]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,35,\"2/2\"],[37,73,11]],null]]]\n[3,null,[[0,3,null,[[6,21,3]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,51,\"2/2\"],[53,116,583]],null]]]\n\n[573,null,[[0,573,null,[[14,53,573]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[11,null,[[0,11,null,[[6,28,11]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[15,21,562],[25,41,103]],null]]]\n[539,null,[[0,539,null,[[6,30,539]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[15,31,23],[35,52,21]],null]]]\n[9,null,[[0,9,null,[[6,42,9]],null]]]\n\n[14,null,[[0,14,null,[[6,29,14]],null]]]\n\n[564,null,[[0,564,null,[[4,41,564]],null]]]\n\n\n\n\n[40,\"m\",[[0,40]]]\n[40,null,[[0,40,null,[[13,50,40]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[21,null,[[0,21,null,[[20,36,21]],null]]]\n[21,null,[[0,21,null,[[6,88,21]],null]]]\n[18,null,[[0,18,null,[[6,23,18]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,25,\"2/2\"],[27,75,18]],null]]]\n\n[19,null,[[0,19,null,[[6,33,19]],null]]]\n\n[29,null,[[0,29,null,[[4,16,29]],null]]]\n\n\n[371,\"m\",[[0,371]]]\n[371,null,[[0,371,null,[[14,16,371],[31,47,371]],null]]]\n[371,null,[[0,371,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,45,\"2/2\"],[47,108,3274]],null]]]\n[3270,null,[[0,3270,null,[[15,52,3270]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,22,\"2/2\"],[24,30,3270]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[92,null,[[0,92,null,[[8,60,92]],null]]]\n[92,null,[[0,92,null,[[8,43,92]],null]]]\n[73,null,[[0,73,null,[[8,36,73]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,25,\"2/2\"],[27,88,2832]],null]]]\n[2830,null,[[0,2830,null,[[8,25,2830]],null]]]\n\n\n[346,null,[[0,346,null,[[4,58,346]],null]]]\n[346,null,[[0,346,null,[[4,44,346]],null]]]\n\n\n\n\n[87,\"m\",[[0,87]]]\n[87,null,[[0,87,null,[[14,16,87],[31,45,87]],null]]]\n[87,null,[[0,87,null,[],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,45,\"2/2\"],[47,101,227]],null]]]\n[225,null,[[0,225,null,[[15,52,225]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,19,225],[23,32,177],[36,85,36]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,47,82],[51,74,49]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[17,null,[[0,17,null,[[12,32,17]],null]]]\n[17,null,[[0,17,null,[[12,53,17]],null]]]\n\n[24,null,[[0,24,null,[[12,29,24]],null]]]\n[24,null,[[0,24,null,[[12,50,24]],null]]]\n\n\n[41,null,[[0,41,null,[[8,60,41]],null]]]\n[41,null,[[0,41,null,[[8,50,41]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[15,null,[[0,15,null,[[8,60,15]],null]]]\n[15,null,[[0,15,null,[[8,42,15]],null]]]\n[12,null,[[0,12,null,[[8,36,12]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[3,null,[[0,3,null,[[8,60,3]],null]]]\n[3,null,[[0,3,null,[[8,25,3]],null]]]\n[3,null,[[0,3,null,[],null]]]\n[1,\"b\",[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[16,60,\"1/2\"],[62,79,0]],null]]]\n[3,\"b\",[[0,3]]]\n[3,null,[[0,3,null,[[12,24,3]],null]]]\n[3,null,[[0,3,null,[[12,18,3]],null]]]\n[0,\"b\",[[0,0]]]\n[0,null,[[0,0,null,[[12,43,0]],null]]]\n[0,null,[[0,0,null,[[12,18,0]],null]]]\n\n[3,null,[[0,3,null,[[8,29,3]],null]]]\n[3,null,[[0,3,null,[[8,46,3]],null]]]\n[3,null,[[0,3,null,[[8,36,3]],null]]]\n\n[125,null,[[0,125,null,[[8,25,125]],null]]]\n\n\n\n\n\n\n[107,\"m\",[[0,107]]]\n[107,null,[[0,107,null,[[13,52,107]],null]]]\n[107,null,[[0,107,null,[[4,21,107]],null]]]\n[107,null,[[0,107,null,[],null]]]\n[8,\"b\",[[0,8,null,[[16,28,8]],null]]]\n[4,\"b\",[[0,4,null,[[16,28,4]],null]]]\n[8,\"b\",[[0,8,null,[[16,64,8]],null]]]\n[19,\"b\",[[0,19,null,[[16,63,19]],null]]]\n[2,\"b\",[[0,2,null,[[16,28,2]],null]]]\n[2,\"b\",[[0,2,null,[[15,27,2]],null]]]\n[2,\"b\",[[0,2,null,[[16,32,2]],null]]]\n[2,\"b\",[[0,2,null,[[16,28,2]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[[19,63,\"0/2\"],[65,82,0]],null]]]\n[4,\"b\",[[0,4]]]\n[4,null,[[0,4,null,[[8,46,4]],null]]]\n[4,null,[[0,4,null,[[8,29,4]],null]]]\n[4,null,[[0,4,null,[[8,18,4]],null]]]\n[56,\"b\",[[0,56]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,20,56],[24,32,52]],null]]]\n[25,null,[[0,25,null,[[25,85,25]],null]]]\n[25,null,[[0,25,null,[[22,43,25]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[12,45,2]],null]]]\n[2,null,[[0,2,null,[[12,42,2]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],null,null]]]\n[23,null,[[0,23,null,[[14,46,23]],null]]]\n[23,null,[[0,23,null,[[14,60,23]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[16,33,23],[37,47,16]],null]]]\n[9,null,[[0,9,null,[[14,77,9]],null]]]\n\n\n[16,null,[[0,16,null,[[10,48,16]],null]]]\n[16,null,[[0,16,null,[[10,44,16]],null]]]\n\n[31,null,[[0,31,null,[[8,39,31]],null]]]\n\n\n\n\n\n[48,\"m\",[[0,48]]]\n[48,null,[[0,48,null,[[18,32,48]],null]]]\n[48,null,[[0,48,null,[[12,33,48]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,18,\"2/2\"],[20,73,48]],null]]]\n[35,null,[[0,35,null,[[4,13,35]],null]]]\n\n\n\n\n\n\n\n\n[5972,\"m\",[[0,5972]]]\n[5972,null,[[0,5972,null,[[4,35,5972]],null]]]\n[5972,null,[[0,5972,null,[[15,17,5972],[27,31,5972],[46,60,5972]],null]]]\n[5972,null,[[0,5972,null,[],null]]]\n[28048,null,[[0,28048,null,[[15,39,28048]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[22348,null,[[0,22348,null,[[8,47,22348]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[27,null,[[0,27,null,[[8,38,27]],null]]]\n\n[27,null,[[0,27,null,[[8,61,27]],null]]]\n[27,null,[[0,27,null,[[23,37,27]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[6,null,[[0,6,null,[[10,82,6]],null]]]\n\n\n[21,null,[[0,21,null,[[8,25,21]],null]]]\n[21,null,[[0,21,null,[[18,38,21]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[22,39,10],[42,58,10]],null]]]\n[5,null,[[0,5,null,[[10,57,5]],null]]]\n\n\n[15,null,[[0,15,null,[[8,39,15]],null]]]\n[15,null,[[0,15,null,[[8,36,15]],null]]]\n\n[5673,null,[[0,5673,null,[[8,14,5673]],null]]]\n\n[22363,null,[[0,22363,null,[[6,20,22363]],null]]]\n\n[5960,null,[[0,5960,null,[[4,63,5960]],null]]]\n\n\n\n\n\n[5944,\"m\",[[0,5944]]]\n[5944,null,[[0,5944,null,[[15,31,5944]],null]]]\n[5933,null,[[0,5933,null,[[15,22,5933]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,31,5933],[35,55,5922]],null]]]\n[1971,null,[[0,1971,null,[[6,32,1971]],null]]]\n\n[5933,null,[[0,5933,null,[[4,40,5933]],null]]]\n\n\n[1353,\"m\",[[0,1353]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[34,null,[[0,34,null,[[19,36,34]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[10,38,34],[42,71,11]],null]]]\n[32,null,[[0,32,null,[[8,30,32]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[2,null,[[0,2,null,[[6,87,2]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,29,1319],[33,53,1319],[57,76,1319],[80,102,1280]],null]]]\n[689,null,[[0,689,null,[[6,18,689]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[4,null,[[0,4,null,[[6,53,4]],null]]]\n\n\n[626,null,[[0,626,null,[[4,35,626]],null]]]\n\n\n[19525,\"m\",[[0,19525]]]\n[19525,null,[[0,19525,null,[[23,38,19525]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,20,19525],[24,43,1971]],null]]]\n[14,null,[[0,14,null,[[6,37,14]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],null,null]]]\n[10074,null,[[0,10074,null,[[6,34,10074]],null]]]\n\n[9437,null,[[0,9437,null,[[6,47,9437]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n\n[2123,\"m\",[[0,2123]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[[49,54,0],[57,88,2123]],null]]]\n\n[2123,null,[[0,2123,null,[[4,23,2123]],null]]]\n\n[2123,null,[[0,2123,null,[[4,31,2123]],null]]]\n\n[2123,null,[[0,2123,null,[[4,78,2123]],null]]]\n\n[2123,null,[[0,2123,null,[[4,21,2123]],null]]]\n\n[2123,null,[[0,2123,null,[[4,25,2123]],null]]]\n\n[2123,null,[[0,2123,null,[[4,21,2123]],null]]]\n\n[2123,null,[[0,2123,null,[[4,23,2123]],null]]]\n\n[2123,null,[[0,2123,null,[[4,31,2123]],null]]]\n[2123,null,[[0,2123,null,[[4,31,2123]],null]]]\n[2123,null,[[0,2123,null,[[4,31,2123]],null]]]\n\n[2123,null,[[0,2123,null,[[4,34,2123]],null]]]\n[2123,null,[[0,2123,null,[[4,21,2123]],null]]]\n\n[2123,null,[[0,2123,null,[[4,23,2123]],null]]]\n[2123,null,[[0,2123,null,[[4,22,2123]],null]]]\n[2123,null,[[0,2123,null,[[4,37,2123]],null]]]\n[2123,null,[[0,2123,null,[[4,53,2123]],null]]]\n\n[2123,null,[[0,2123,null,[[4,53,2123]],null]]]\n[2123,null,[[0,2123,null,[[4,51,2123]],null]]]\n\n[2123,null,[[0,2123,null,[[4,39,2123]],null]]]\n[2123,null,[[0,2123,null,[[4,28,2123]],null]]]\n\n[2123,null,[[0,2123,null,[[4,50,2123]],null]]]\n[2123,null,[[0,2123,null,[[4,30,2123]],null]]]\n\n[2123,null,[[0,2123,null,[[4,16,2123]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[41591,\"m\",[[0,41591]]]\n[41591,null,[[0,41591,null,[[4,65,41591]],null]]]\n\n\n[176,\"m\",[[0,176]]]\n[176,null,[[0,176,null,[[16,25,176]],null]]]\n[176,null,[[0,176,null,[],null]]]\n[5777,null,[[0,5777,null,[[16,25,5777]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[11,22,5777],[26,43,2978],[48,66,2889]],null]]]\n[778,null,[[0,778,null,[[8,26,778]],null]]]\n\n\n[5777,null,[[0,5777,null,[[6,23,5777]],null]]]\n\n[176,null,[[0,176,null,[[4,17,176]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[81,\"b\",[[0,81]]]\n[81,null,[[0,81,null,[[4,23,81]],null]]]\n[81,null,[[0,81,null,[[4,32,81]],null]]]\n[81,null,[[0,81,null,[[4,40,81]],null]]]\n[81,null,[[0,81,null,[[4,40,81]],null]]]\n[81,null,[[0,81,null,[[4,52,81]],null]]]\n[81,null,[[0,81,null,[[4,32,81]],null]]]\n[81,null,[[0,81,null,[[4,36,81]],null]]]\n[81,null,[[0,81,null,[[4,32,81]],null]]]\n[81,null,[[0,81,null,[[4,34,81]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[17,27,81],[31,35,66]],null]]]\n[81,null,[[0,81,null,[[4,30,81]],null]]]\n\n\n\n[11,\"m\",[[0,11]]]\n[11,null,[[0,11,null,[[2,62,11]],null]]]\n\n[1,null,[[0,1,null,[[19,37,1],[52,70,1]],null]]]\n\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1,null,[[24,26,1]],null]]]\n\n\n[37,\"b\",[[0,37]]]\n[37,null,[[0,37,null,[[2,25,37]],null]]]\n[37,null,[[0,37,null,[[2,68,37]],null]]]\n\n\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n\n\n\n\n\n[4,\"m\",[[0,4]]]\n[4,null,[[0,4,null,[[2,27,4]],null]]]\n[7769,\"m\",[[0,7769]]]\n[7769,null,[[0,7769,null,[[4,35,7769]],null]]]\n\n\n\n\n\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n\n\n[1,null,[[0,1,null,[[25,263,1]],null]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1,null,[[35,4312,1]],null]]]\n[1,null,[[0,1,null,[[30,2605,1]],null]]]\n\n[1,null,[[0,1,null,[[32,84,1]],null]]]\n[1,null,[[0,1,null,[[27,105,1]],null]]]\n\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n\n[1,null,[[0,1,null,[[35,1052,1]],null]]]\n[1,null,[[0,1,null,[[30,485,1]],null]]]\n\n\n\n\n[221,\"m\",[[0,221]]]\n[221,null,[[0,221,null,[[12,19,221]],null]]]\n[221,null,[[0,221,null,[],null]]]\n[40931,null,[[0,40931,null,[[4,18,40931]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,18,\"2/2\"],[20,33,40931]],null]]]\n\n[40924,null,[[0,40924,null,[[4,22,40924]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,19,\"2/2\"],[21,33,40924]],null]]]\n\n\n\n\n\n[17173,\"m\",[[0,17173]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,15,\"2/2\"],[17,36,17173]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,15,\"2/2\"],[17,29,9474]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,15,\"2/2\"],[17,36,9031]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,16,\"2/2\"],[18,30,8430]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,27,\"2/2\"],[29,41,2642],[45,100,14]],null]]]\n[208,null,[[0,208,null,[[2,57,208]],null]]]\n\n\n\n\n[28340,\"m\",[[0,28340]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,15,\"2/2\"],[17,36,28340]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,15,\"2/2\"],[17,29,23622]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,15,\"2/2\"],[17,30,23593]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,15,\"2/2\"],[17,29,22750]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,15,\"2/2\"],[17,36,22199]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,16,\"2/2\"],[18,30,21907]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[6,27,\"2/2\"],[29,41,153],[45,95,33]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[9,56,10],[60,102,3]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n[42329,\"m\",[[0,42329]]]\n[42329,null,[[0,42329,null,[[4,21,42329]],null]]]\n[42329,null,[[0,42329,null,[[4,22,42329]],null]]]\n\n\n\n\n[38978,\"m\",[[0,38978]]]\n[38978,null,[[0,38978,null,[[4,23,38978]],null]]]\n[38978,null,[[0,38978,null,[[4,19,38978]],null]]]\n\n\n\n\n\n\n\n\n\n[738,\"m\",[[0,738]]]\n[738,null,[[0,738,null,[],null]]]\n[816,null,[[0,816,null,[[4,31,816]],null]]]\n[816,null,[[0,816,null,[[16,38,816]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[8,13,816],[17,37,109]],null]]]\n[78,null,[[0,78,null,[[6,13,78]],null]]]\n[78,null,[[0,78,null,[[6,42,78]],null]]]\n\n[738,null,[[0,738,null,[[6,46,738]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n[1,null,[[0,1,null,[[25,49,1]],null]]]\n[1,null,[[0,1,null,[[26,59,1]],null]]]\n\n[3360,\"m\",[[0,3360]]]\n[\"4/4\",\"b\",[[0,\"4/4\",[],[[9,20,3360],[24,35,3345],[39,54,3344],[58,73,3344]],null]]]\n\n\n[1,null,[[0,1,null,[[34,87,1]],null]]]" + ], + "totals": { + "f": 19, + "n": 3127, + "h": 2945, + "m": 101, + "p": 81, + "c": "94.17972", + "b": 797, + "d": 320, + "M": 0, + "s": 0, + "C": 0, + "N": 0, + "diff": null + }, + "report": { + "files": { + "/home/travis/build/babel/babylon/src/index.js": [ + 0, + [0, 4, 4, 0, 0, "100", 0, 1, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/options.js": [ + 1, + [0, 6, 6, 0, 0, "100", 1, 1, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/parser/comments.js": [ + 2, + [0, 55, 55, 0, 0, "100", 20, 3, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/parser/expression.js": [ + 3, + [0, 596, 536, 40, 20, "89.93289", 159, 37, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/parser/index.js": [ + 4, + [0, 32, 31, 0, 1, "96.87500", 5, 6, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/parser/location.js": [ + 5, + [0, 8, 8, 0, 0, "100", 0, 1, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/parser/lval.js": [ + 6, + [0, 154, 147, 2, 5, "95.45455", 53, 12, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/parser/node.js": [ + 7, + [0, 28, 28, 0, 0, "100", 2, 7, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/parser/statement.js": [ + 8, + [0, 598, 563, 19, 16, "94.14716", 166, 49, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/parser/util.js": [ + 9, + [0, 29, 26, 1, 2, "89.65517", 15, 10, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/plugins/flow.js": [ + 10, + [0, 686, 642, 19, 25, "93.58601", 140, 102, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/plugins/jsx/index.js": [ + 11, + [0, 274, 257, 12, 5, "93.79562", 59, 27, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/tokenizer/context.js": [ + 12, + [0, 44, 44, 0, 0, "100", 11, 10, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/tokenizer/index.js": [ + 13, + [0, 466, 452, 8, 6, "96.99571", 144, 41, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/tokenizer/state.js": [ + 14, + [0, 35, 34, 0, 1, "97.14286", 2, 3, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/tokenizer/types.js": [ + 15, + [0, 57, 57, 0, 0, "100", 3, 1, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/util/identifier.js": [ + 16, + [0, 36, 36, 0, 0, "100", 15, 5, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/util/location.js": [ + 17, + [0, 14, 14, 0, 0, "100", 1, 3, 0, 0, 0, 0, 0], + null, + null + ], + "/home/travis/build/babel/babylon/src/util/whitespace.js": [ + 18, + [0, 5, 5, 0, 0, "100", 1, 1, 0, 0, 0, 0, 0], + null, + null + ] + }, + "sessions": {} + } +} diff --git a/apps/worker/services/report/languages/tests/unit/node/node1.json b/apps/worker/services/report/languages/tests/unit/node/node1.json new file mode 100644 index 0000000000..0d0e858655 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/node/node1.json @@ -0,0 +1,21 @@ +{"/home/travis/build/babel/babylon/src/index.js": {"path":"/home/travis/build/babel/babylon/src/index.js","statementMap":{"1":{"start":{"line":16,"column":0},"end":{"line":16,"column":26}},"2":{"start":{"line":17,"column":0},"end":{"line":17,"column":24}},"3":{"start":{"line":20,"column":2},"end":{"line":20,"column":44}}},"fnMap":{"1":{"name":"parse","decl":{"start":{"line":19,"column":16},"end":{"line":19,"column":21}},"loc":{"start":{"line":19,"column":38},"end":{"line":21,"column":1}}}},"branchMap":{},"s":{"1":1,"2":1,"3":2123},"f":{"1":2123},"b":{},"hash":"5e4c020863adf1e27d36d1981c8a0819a8671d5a"} +,"/home/travis/build/babel/babylon/src/options.js": {"path":"/home/travis/build/babel/babylon/src/options.js","statementMap":{"1":{"start":{"line":12,"column":4},"end":{"line":29,"column":1}},"2":{"start":{"line":34,"column":16},"end":{"line":34,"column":18}},"3":{"start":{"line":35,"column":2},"end":{"line":37,"column":3}},"4":{"start":{"line":36,"column":4},"end":{"line":36,"column":73}},"5":{"start":{"line":38,"column":2},"end":{"line":38,"column":17}}},"fnMap":{"1":{"name":"getOptions","decl":{"start":{"line":33,"column":16},"end":{"line":33,"column":26}},"loc":{"start":{"line":33,"column":50},"end":{"line":39,"column":1}}}},"branchMap":{"1":{"loc":{"start":{"line":36,"column":19},"end":{"line":36,"column":72}},"type":"cond-expr","locations":[{"start":{"line":36,"column":41},"end":{"line":36,"column":50}},{"start":{"line":36,"column":53},"end":{"line":36,"column":72}}]},"2":{"loc":{"start":{"line":36,"column":19},"end":{"line":36,"column":38}},"type":"binary-expr","locations":[{"start":{"line":36,"column":19},"end":{"line":36,"column":23}},{"start":{"line":36,"column":27},"end":{"line":36,"column":38}}]}},"s":{"1":1,"2":2123,"3":2123,"4":14861,"5":2123},"f":{"1":2123},"b":{"1":[631,14230],"2":[14861,14861]},"hash":"f2aa6292315d39d5f086c3e6689ad577b261d6b8"} +,"/home/travis/build/babel/babylon/src/parser/comments.js": {"path":"/home/travis/build/babel/babylon/src/parser/comments.js","statementMap":{"1":{"start":{"line":30,"column":2},"end":{"line":30,"column":33}},"2":{"start":{"line":33,"column":11},"end":{"line":33,"column":27}},"3":{"start":{"line":35,"column":0},"end":{"line":39,"column":2}},"4":{"start":{"line":36,"column":2},"end":{"line":36,"column":58}},"5":{"start":{"line":36,"column":21},"end":{"line":36,"column":58}},"6":{"start":{"line":37,"column":2},"end":{"line":37,"column":44}},"7":{"start":{"line":38,"column":2},"end":{"line":38,"column":43}},"8":{"start":{"line":41,"column":0},"end":{"line":156,"column":2}},"9":{"start":{"line":42,"column":2},"end":{"line":42,"column":62}},"10":{"start":{"line":42,"column":55},"end":{"line":42,"column":62}},"11":{"start":{"line":44,"column":14},"end":{"line":44,"column":37}},"12":{"start":{"line":48,"column":2},"end":{"line":71,"column":3}},"13":{"start":{"line":53,"column":4},"end":{"line":64,"column":5}},"14":{"start":{"line":54,"column":6},"end":{"line":54,"column":53}},"15":{"start":{"line":55,"column":6},"end":{"line":55,"column":39}},"16":{"start":{"line":63,"column":6},"end":{"line":63,"column":45}},"17":{"start":{"line":66,"column":22},"end":{"line":66,"column":33}},"18":{"start":{"line":67,"column":4},"end":{"line":70,"column":5}},"19":{"start":{"line":68,"column":6},"end":{"line":68,"column":54}},"20":{"start":{"line":69,"column":6},"end":{"line":69,"column":42}},"21":{"start":{"line":74,"column":2},"end":{"line":76,"column":3}},"22":{"start":{"line":75,"column":4},"end":{"line":75,"column":28}},"23":{"start":{"line":78,"column":2},"end":{"line":143,"column":3}},"24":{"start":{"line":79,"column":4},"end":{"line":94,"column":5}},"25":{"start":{"line":80,"column":6},"end":{"line":93,"column":7}},"26":{"start":{"line":81,"column":8},"end":{"line":81,"column":57}},"27":{"start":{"line":82,"column":8},"end":{"line":82,"column":41}},"28":{"start":{"line":87,"column":8},"end":{"line":92,"column":9}},"29":{"start":{"line":88,"column":10},"end":{"line":91,"column":11}},"30":{"start":{"line":89,"column":12},"end":{"line":89,"column":78}},"31":{"start":{"line":90,"column":12},"end":{"line":90,"column":18}},"32":{"start":{"line":95,"column":9},"end":{"line":143,"column":3}},"33":{"start":{"line":96,"column":4},"end":{"line":142,"column":5}},"34":{"start":{"line":97,"column":6},"end":{"line":104,"column":7}},"35":{"start":{"line":98,"column":8},"end":{"line":103,"column":9}},"36":{"start":{"line":99,"column":10},"end":{"line":102,"column":11}},"37":{"start":{"line":100,"column":12},"end":{"line":100,"column":52}},"38":{"start":{"line":101,"column":12},"end":{"line":101,"column":16}},"39":{"start":{"line":105,"column":6},"end":{"line":108,"column":7}},"40":{"start":{"line":106,"column":8},"end":{"line":106,"column":58}},"41":{"start":{"line":107,"column":8},"end":{"line":107,"column":40}},"42":{"start":{"line":121,"column":6},"end":{"line":125,"column":7}},"43":{"start":{"line":122,"column":8},"end":{"line":124,"column":9}},"44":{"start":{"line":123,"column":10},"end":{"line":123,"column":16}},"45":{"start":{"line":131,"column":6},"end":{"line":131,"column":68}},"46":{"start":{"line":132,"column":6},"end":{"line":134,"column":7}},"47":{"start":{"line":133,"column":8},"end":{"line":133,"column":36}},"48":{"start":{"line":138,"column":6},"end":{"line":138,"column":61}},"49":{"start":{"line":139,"column":6},"end":{"line":141,"column":7}},"50":{"start":{"line":140,"column":8},"end":{"line":140,"column":32}},"51":{"start":{"line":145,"column":2},"end":{"line":145,"column":40}},"52":{"start":{"line":147,"column":2},"end":{"line":153,"column":3}},"53":{"start":{"line":148,"column":4},"end":{"line":152,"column":5}},"54":{"start":{"line":149,"column":6},"end":{"line":149,"column":44}},"55":{"start":{"line":151,"column":6},"end":{"line":151,"column":47}},"56":{"start":{"line":155,"column":2},"end":{"line":155,"column":19}}},"fnMap":{"1":{"name":"last","decl":{"start":{"line":29,"column":9},"end":{"line":29,"column":13}},"loc":{"start":{"line":29,"column":21},"end":{"line":31,"column":1}}},"2":{"name":"(anonymous_2)","decl":{"start":{"line":35,"column":16},"end":{"line":35,"column":17}},"loc":{"start":{"line":35,"column":35},"end":{"line":39,"column":1}}},"3":{"name":"(anonymous_3)","decl":{"start":{"line":41,"column":20},"end":{"line":41,"column":21}},"loc":{"start":{"line":41,"column":36},"end":{"line":156,"column":1}}}},"branchMap":{"1":{"loc":{"start":{"line":36,"column":2},"end":{"line":36,"column":58}},"type":"if","locations":[{"start":{"line":36,"column":2},"end":{"line":36,"column":58}},{"start":{"line":36,"column":2},"end":{"line":36,"column":58}}]},"2":{"loc":{"start":{"line":42,"column":2},"end":{"line":42,"column":62}},"type":"if","locations":[{"start":{"line":42,"column":2},"end":{"line":42,"column":62}},{"start":{"line":42,"column":2},"end":{"line":42,"column":62}}]},"3":{"loc":{"start":{"line":42,"column":6},"end":{"line":42,"column":53}},"type":"binary-expr","locations":[{"start":{"line":42,"column":6},"end":{"line":42,"column":29}},{"start":{"line":42,"column":33},"end":{"line":42,"column":53}}]},"4":{"loc":{"start":{"line":48,"column":2},"end":{"line":71,"column":3}},"type":"if","locations":[{"start":{"line":48,"column":2},"end":{"line":71,"column":3}},{"start":{"line":48,"column":2},"end":{"line":71,"column":3}}]},"5":{"loc":{"start":{"line":53,"column":4},"end":{"line":64,"column":5}},"type":"if","locations":[{"start":{"line":53,"column":4},"end":{"line":64,"column":5}},{"start":{"line":53,"column":4},"end":{"line":64,"column":5}}]},"6":{"loc":{"start":{"line":67,"column":4},"end":{"line":70,"column":5}},"type":"if","locations":[{"start":{"line":67,"column":4},"end":{"line":70,"column":5}},{"start":{"line":67,"column":4},"end":{"line":70,"column":5}}]},"7":{"loc":{"start":{"line":67,"column":8},"end":{"line":67,"column":109}},"type":"binary-expr","locations":[{"start":{"line":67,"column":8},"end":{"line":67,"column":24}},{"start":{"line":67,"column":28},"end":{"line":67,"column":56}},{"start":{"line":67,"column":60},"end":{"line":67,"column":109}}]},"8":{"loc":{"start":{"line":74,"column":9},"end":{"line":74,"column":60}},"type":"binary-expr","locations":[{"start":{"line":74,"column":9},"end":{"line":74,"column":25}},{"start":{"line":74,"column":29},"end":{"line":74,"column":60}}]},"9":{"loc":{"start":{"line":78,"column":2},"end":{"line":143,"column":3}},"type":"if","locations":[{"start":{"line":78,"column":2},"end":{"line":143,"column":3}},{"start":{"line":78,"column":2},"end":{"line":143,"column":3}}]},"10":{"loc":{"start":{"line":79,"column":4},"end":{"line":94,"column":5}},"type":"if","locations":[{"start":{"line":79,"column":4},"end":{"line":94,"column":5}},{"start":{"line":79,"column":4},"end":{"line":94,"column":5}}]},"11":{"loc":{"start":{"line":80,"column":6},"end":{"line":93,"column":7}},"type":"if","locations":[{"start":{"line":80,"column":6},"end":{"line":93,"column":7}},{"start":{"line":80,"column":6},"end":{"line":93,"column":7}}]},"12":{"loc":{"start":{"line":80,"column":10},"end":{"line":80,"column":81}},"type":"binary-expr","locations":[{"start":{"line":80,"column":10},"end":{"line":80,"column":28}},{"start":{"line":80,"column":32},"end":{"line":80,"column":81}}]},"13":{"loc":{"start":{"line":88,"column":10},"end":{"line":91,"column":11}},"type":"if","locations":[{"start":{"line":88,"column":10},"end":{"line":91,"column":11}},{"start":{"line":88,"column":10},"end":{"line":91,"column":11}}]},"14":{"loc":{"start":{"line":95,"column":9},"end":{"line":143,"column":3}},"type":"if","locations":[{"start":{"line":95,"column":9},"end":{"line":143,"column":3}},{"start":{"line":95,"column":9},"end":{"line":143,"column":3}}]},"15":{"loc":{"start":{"line":96,"column":4},"end":{"line":142,"column":5}},"type":"if","locations":[{"start":{"line":96,"column":4},"end":{"line":142,"column":5}},{"start":{"line":96,"column":4},"end":{"line":142,"column":5}}]},"16":{"loc":{"start":{"line":97,"column":6},"end":{"line":104,"column":7}},"type":"if","locations":[{"start":{"line":97,"column":6},"end":{"line":104,"column":7}},{"start":{"line":97,"column":6},"end":{"line":104,"column":7}}]},"17":{"loc":{"start":{"line":99,"column":10},"end":{"line":102,"column":11}},"type":"if","locations":[{"start":{"line":99,"column":10},"end":{"line":102,"column":11}},{"start":{"line":99,"column":10},"end":{"line":102,"column":11}}]},"18":{"loc":{"start":{"line":105,"column":6},"end":{"line":108,"column":7}},"type":"if","locations":[{"start":{"line":105,"column":6},"end":{"line":108,"column":7}},{"start":{"line":105,"column":6},"end":{"line":108,"column":7}}]},"19":{"loc":{"start":{"line":122,"column":8},"end":{"line":124,"column":9}},"type":"if","locations":[{"start":{"line":122,"column":8},"end":{"line":124,"column":9}},{"start":{"line":122,"column":8},"end":{"line":124,"column":9}}]},"20":{"loc":{"start":{"line":132,"column":6},"end":{"line":134,"column":7}},"type":"if","locations":[{"start":{"line":132,"column":6},"end":{"line":134,"column":7}},{"start":{"line":132,"column":6},"end":{"line":134,"column":7}}]},"21":{"loc":{"start":{"line":139,"column":6},"end":{"line":141,"column":7}},"type":"if","locations":[{"start":{"line":139,"column":6},"end":{"line":141,"column":7}},{"start":{"line":139,"column":6},"end":{"line":141,"column":7}}]},"22":{"loc":{"start":{"line":147,"column":2},"end":{"line":153,"column":3}},"type":"if","locations":[{"start":{"line":147,"column":2},"end":{"line":153,"column":3}},{"start":{"line":147,"column":2},"end":{"line":153,"column":3}}]},"23":{"loc":{"start":{"line":148,"column":4},"end":{"line":152,"column":5}},"type":"if","locations":[{"start":{"line":148,"column":4},"end":{"line":152,"column":5}},{"start":{"line":148,"column":4},"end":{"line":152,"column":5}}]},"24":{"loc":{"start":{"line":148,"column":8},"end":{"line":148,"column":116}},"type":"binary-expr","locations":[{"start":{"line":148,"column":8},"end":{"line":148,"column":31}},{"start":{"line":148,"column":35},"end":{"line":148,"column":74}},{"start":{"line":148,"column":78},"end":{"line":148,"column":116}}]}},"s":{"1":32050,"2":1,"3":1,"4":87,"5":1,"6":87,"7":87,"8":1,"9":15001,"10":1374,"11":13627,"12":13627,"13":66,"14":32,"15":32,"16":34,"17":13561,"18":13561,"19":13,"20":13,"21":13627,"22":11082,"23":13627,"24":7786,"25":79,"26":53,"27":53,"28":26,"29":3,"30":2,"31":2,"32":5841,"33":82,"34":49,"35":29,"36":29,"37":2,"38":2,"39":49,"40":47,"41":47,"42":33,"43":35,"44":32,"45":33,"46":33,"47":30,"48":33,"49":33,"50":1,"51":13627,"52":13627,"53":52,"54":5,"55":47,"56":13627},"f":{"1":32050,"2":87,"3":15001},"b":{"1":[1,86],"2":[1374,13627],"3":[15001,1404],"4":[66,13561],"5":[32,34],"6":[13,13548],"7":[13561,11677,47],"8":[24709,18276],"9":[7786,5841],"10":[79,7707],"11":[53,26],"12":[79,79],"13":[2,1],"14":[82,5759],"15":[49,33],"16":[29,20],"17":[2,27],"18":[47,2],"19":[32,3],"20":[30,3],"21":[1,32],"22":[52,13575],"23":[5,47],"24":[52,52,52]},"hash":"763093c58e4c87d7d223c77c1209d2438b7e8e96"} +,"/home/travis/build/babel/babylon/src/parser/expression.js": {"path":"/home/travis/build/babel/babylon/src/parser/expression.js","statementMap":{"1":{"start":{"line":26,"column":11},"end":{"line":26,"column":27}},"2":{"start":{"line":33,"column":0},"end":{"line":56,"column":2}},"3":{"start":{"line":34,"column":2},"end":{"line":34,"column":28}},"4":{"start":{"line":34,"column":21},"end":{"line":34,"column":28}},"5":{"start":{"line":36,"column":12},"end":{"line":36,"column":20}},"6":{"start":{"line":38,"column":2},"end":{"line":50,"column":3}},"7":{"start":{"line":40,"column":6},"end":{"line":40,"column":22}},"8":{"start":{"line":41,"column":6},"end":{"line":41,"column":12}},"9":{"start":{"line":45,"column":6},"end":{"line":45,"column":31}},"10":{"start":{"line":46,"column":6},"end":{"line":46,"column":12}},"11":{"start":{"line":49,"column":6},"end":{"line":49,"column":13}},"12":{"start":{"line":52,"column":2},"end":{"line":55,"column":3}},"13":{"start":{"line":53,"column":4},"end":{"line":53,"column":84}},"14":{"start":{"line":53,"column":24},"end":{"line":53,"column":84}},"15":{"start":{"line":54,"column":4},"end":{"line":54,"column":26}},"16":{"start":{"line":73,"column":0},"end":{"line":86,"column":2}},"17":{"start":{"line":74,"column":17},"end":{"line":74,"column":33}},"18":{"start":{"line":74,"column":46},"end":{"line":74,"column":65}},"19":{"start":{"line":75,"column":13},"end":{"line":75,"column":64}},"20":{"start":{"line":76,"column":2},"end":{"line":84,"column":3}},"21":{"start":{"line":77,"column":15},"end":{"line":77,"column":51}},"22":{"start":{"line":78,"column":4},"end":{"line":78,"column":30}},"23":{"start":{"line":79,"column":4},"end":{"line":81,"column":5}},"24":{"start":{"line":80,"column":6},"end":{"line":80,"column":81}},"25":{"start":{"line":82,"column":4},"end":{"line":82,"column":44}},"26":{"start":{"line":83,"column":4},"end":{"line":83,"column":55}},"27":{"start":{"line":85,"column":2},"end":{"line":85,"column":14}},"28":{"start":{"line":91,"column":0},"end":{"line":141,"column":2}},"29":{"start":{"line":92,"column":2},"end":{"line":94,"column":3}},"30":{"start":{"line":93,"column":4},"end":{"line":93,"column":29}},"31":{"start":{"line":97,"column":2},"end":{"line":102,"column":3}},"32":{"start":{"line":98,"column":4},"end":{"line":98,"column":34}},"33":{"start":{"line":100,"column":4},"end":{"line":100,"column":42}},"34":{"start":{"line":101,"column":4},"end":{"line":101,"column":33}},"35":{"start":{"line":104,"column":17},"end":{"line":104,"column":33}},"36":{"start":{"line":105,"column":17},"end":{"line":105,"column":36}},"37":{"start":{"line":107,"column":2},"end":{"line":109,"column":3}},"38":{"start":{"line":108,"column":4},"end":{"line":108,"column":51}},"39":{"start":{"line":111,"column":13},"end":{"line":111,"column":87}},"40":{"start":{"line":112,"column":2},"end":{"line":112,"column":81}},"41":{"start":{"line":112,"column":22},"end":{"line":112,"column":81}},"42":{"start":{"line":113,"column":2},"end":{"line":138,"column":3}},"43":{"start":{"line":114,"column":15},"end":{"line":114,"column":51}},"44":{"start":{"line":115,"column":4},"end":{"line":115,"column":37}},"45":{"start":{"line":116,"column":4},"end":{"line":116,"column":67}},"46":{"start":{"line":117,"column":4},"end":{"line":117,"column":37}},"47":{"start":{"line":119,"column":4},"end":{"line":119,"column":25}},"48":{"start":{"line":121,"column":4},"end":{"line":131,"column":5}},"49":{"start":{"line":123,"column":6},"end":{"line":127,"column":7}},"50":{"start":{"line":124,"column":8},"end":{"line":124,"column":49}},"51":{"start":{"line":125,"column":13},"end":{"line":127,"column":7}},"52":{"start":{"line":126,"column":8},"end":{"line":126,"column":49}},"53":{"start":{"line":128,"column":6},"end":{"line":130,"column":7}},"54":{"start":{"line":129,"column":8},"end":{"line":129,"column":116}},"55":{"start":{"line":133,"column":4},"end":{"line":133,"column":16}},"56":{"start":{"line":134,"column":4},"end":{"line":134,"column":45}},"57":{"start":{"line":135,"column":4},"end":{"line":135,"column":57}},"58":{"start":{"line":136,"column":9},"end":{"line":138,"column":3}},"59":{"start":{"line":137,"column":4},"end":{"line":137,"column":50}},"60":{"start":{"line":140,"column":2},"end":{"line":140,"column":14}},"61":{"start":{"line":145,"column":0},"end":{"line":151,"column":2}},"62":{"start":{"line":146,"column":17},"end":{"line":146,"column":33}},"63":{"start":{"line":146,"column":46},"end":{"line":146,"column":65}},"64":{"start":{"line":147,"column":13},"end":{"line":147,"column":60}},"65":{"start":{"line":148,"column":2},"end":{"line":148,"column":74}},"66":{"start":{"line":148,"column":62},"end":{"line":148,"column":74}},"67":{"start":{"line":150,"column":2},"end":{"line":150,"column":81}},"68":{"start":{"line":153,"column":0},"end":{"line":163,"column":2}},"69":{"start":{"line":154,"column":2},"end":{"line":161,"column":3}},"70":{"start":{"line":155,"column":15},"end":{"line":155,"column":51}},"71":{"start":{"line":156,"column":4},"end":{"line":156,"column":21}},"72":{"start":{"line":157,"column":4},"end":{"line":157,"column":46}},"73":{"start":{"line":158,"column":4},"end":{"line":158,"column":26}},"74":{"start":{"line":159,"column":4},"end":{"line":159,"column":49}},"75":{"start":{"line":160,"column":4},"end":{"line":160,"column":58}},"76":{"start":{"line":162,"column":2},"end":{"line":162,"column":14}},"77":{"start":{"line":167,"column":0},"end":{"line":175,"column":2}},"78":{"start":{"line":168,"column":17},"end":{"line":168,"column":33}},"79":{"start":{"line":168,"column":46},"end":{"line":168,"column":65}},"80":{"start":{"line":169,"column":13},"end":{"line":169,"column":57}},"81":{"start":{"line":170,"column":2},"end":{"line":174,"column":3}},"82":{"start":{"line":171,"column":4},"end":{"line":171,"column":16}},"83":{"start":{"line":173,"column":4},"end":{"line":173,"column":64}},"84":{"start":{"line":183,"column":0},"end":{"line":213,"column":2}},"85":{"start":{"line":184,"column":13},"end":{"line":184,"column":34}},"86":{"start":{"line":185,"column":2},"end":{"line":211,"column":3}},"87":{"start":{"line":186,"column":4},"end":{"line":210,"column":5}},"88":{"start":{"line":187,"column":17},"end":{"line":187,"column":61}},"89":{"start":{"line":188,"column":6},"end":{"line":188,"column":23}},"90":{"start":{"line":189,"column":6},"end":{"line":189,"column":39}},"91":{"start":{"line":191,"column":6},"end":{"line":199,"column":7}},"92":{"start":{"line":198,"column":8},"end":{"line":198,"column":124}},"93":{"start":{"line":201,"column":15},"end":{"line":201,"column":30}},"94":{"start":{"line":202,"column":6},"end":{"line":202,"column":18}},"95":{"start":{"line":204,"column":21},"end":{"line":204,"column":37}},"96":{"start":{"line":205,"column":21},"end":{"line":205,"column":40}},"97":{"start":{"line":206,"column":6},"end":{"line":206,"column":125}},"98":{"start":{"line":208,"column":6},"end":{"line":208,"column":118}},"99":{"start":{"line":209,"column":6},"end":{"line":209,"column":79}},"100":{"start":{"line":212,"column":2},"end":{"line":212,"column":14}},"101":{"start":{"line":217,"column":0},"end":{"line":256,"column":2}},"102":{"start":{"line":218,"column":2},"end":{"line":241,"column":3}},"103":{"start":{"line":219,"column":15},"end":{"line":219,"column":31}},"104":{"start":{"line":220,"column":17},"end":{"line":220,"column":38}},"105":{"start":{"line":221,"column":4},"end":{"line":221,"column":37}},"106":{"start":{"line":222,"column":4},"end":{"line":222,"column":23}},"107":{"start":{"line":223,"column":4},"end":{"line":223,"column":16}},"108":{"start":{"line":225,"column":18},"end":{"line":225,"column":33}},"109":{"start":{"line":226,"column":4},"end":{"line":226,"column":43}},"110":{"start":{"line":228,"column":4},"end":{"line":228,"column":136}},"111":{"start":{"line":230,"column":4},"end":{"line":232,"column":5}},"112":{"start":{"line":231,"column":6},"end":{"line":231,"column":52}},"113":{"start":{"line":234,"column":4},"end":{"line":238,"column":5}},"114":{"start":{"line":235,"column":6},"end":{"line":235,"column":36}},"115":{"start":{"line":236,"column":11},"end":{"line":238,"column":5}},"116":{"start":{"line":237,"column":6},"end":{"line":237,"column":71}},"117":{"start":{"line":240,"column":4},"end":{"line":240,"column":82}},"118":{"start":{"line":243,"column":17},"end":{"line":243,"column":33}},"119":{"start":{"line":243,"column":46},"end":{"line":243,"column":65}},"120":{"start":{"line":244,"column":13},"end":{"line":244,"column":61}},"121":{"start":{"line":245,"column":2},"end":{"line":245,"column":74}},"122":{"start":{"line":245,"column":62},"end":{"line":245,"column":74}},"123":{"start":{"line":246,"column":2},"end":{"line":254,"column":3}},"124":{"start":{"line":247,"column":15},"end":{"line":247,"column":51}},"125":{"start":{"line":248,"column":4},"end":{"line":248,"column":37}},"126":{"start":{"line":249,"column":4},"end":{"line":249,"column":24}},"127":{"start":{"line":250,"column":4},"end":{"line":250,"column":25}},"128":{"start":{"line":251,"column":4},"end":{"line":251,"column":25}},"129":{"start":{"line":252,"column":4},"end":{"line":252,"column":16}},"130":{"start":{"line":253,"column":4},"end":{"line":253,"column":53}},"131":{"start":{"line":255,"column":2},"end":{"line":255,"column":14}},"132":{"start":{"line":260,"column":0},"end":{"line":274,"column":2}},"133":{"start":{"line":261,"column":17},"end":{"line":261,"column":33}},"134":{"start":{"line":261,"column":46},"end":{"line":261,"column":65}},"135":{"start":{"line":262,"column":25},"end":{"line":262,"column":52}},"136":{"start":{"line":263,"column":13},"end":{"line":263,"column":55}},"137":{"start":{"line":265,"column":2},"end":{"line":267,"column":3}},"138":{"start":{"line":266,"column":4},"end":{"line":266,"column":16}},"139":{"start":{"line":269,"column":2},"end":{"line":271,"column":3}},"140":{"start":{"line":270,"column":4},"end":{"line":270,"column":16}},"141":{"start":{"line":273,"column":2},"end":{"line":273,"column":56}},"142":{"start":{"line":276,"column":0},"end":{"line":319,"column":2}},"143":{"start":{"line":277,"column":2},"end":{"line":318,"column":3}},"144":{"start":{"line":278,"column":4},"end":{"line":317,"column":5}},"145":{"start":{"line":279,"column":17},"end":{"line":279,"column":53}},"146":{"start":{"line":280,"column":6},"end":{"line":280,"column":25}},"147":{"start":{"line":281,"column":6},"end":{"line":281,"column":43}},"148":{"start":{"line":282,"column":6},"end":{"line":282,"column":104}},"149":{"start":{"line":283,"column":11},"end":{"line":317,"column":5}},"150":{"start":{"line":284,"column":17},"end":{"line":284,"column":53}},"151":{"start":{"line":285,"column":6},"end":{"line":285,"column":25}},"152":{"start":{"line":286,"column":6},"end":{"line":286,"column":49}},"153":{"start":{"line":287,"column":6},"end":{"line":287,"column":28}},"154":{"start":{"line":288,"column":6},"end":{"line":288,"column":55}},"155":{"start":{"line":289,"column":11},"end":{"line":317,"column":5}},"156":{"start":{"line":290,"column":17},"end":{"line":290,"column":53}},"157":{"start":{"line":291,"column":6},"end":{"line":291,"column":25}},"158":{"start":{"line":292,"column":6},"end":{"line":292,"column":45}},"159":{"start":{"line":293,"column":6},"end":{"line":293,"column":27}},"160":{"start":{"line":294,"column":6},"end":{"line":294,"column":31}},"161":{"start":{"line":295,"column":6},"end":{"line":295,"column":55}},"162":{"start":{"line":296,"column":11},"end":{"line":317,"column":5}},"163":{"start":{"line":297,"column":26},"end":{"line":297,"column":153}},"164":{"start":{"line":298,"column":6},"end":{"line":298,"column":18}},"165":{"start":{"line":300,"column":17},"end":{"line":300,"column":53}},"166":{"start":{"line":301,"column":6},"end":{"line":301,"column":25}},"167":{"start":{"line":302,"column":6},"end":{"line":302,"column":83}},"168":{"start":{"line":303,"column":6},"end":{"line":303,"column":53}},"169":{"start":{"line":305,"column":6},"end":{"line":309,"column":7}},"170":{"start":{"line":306,"column":8},"end":{"line":306,"column":98}},"171":{"start":{"line":308,"column":8},"end":{"line":308,"column":46}},"172":{"start":{"line":310,"column":11},"end":{"line":317,"column":5}},"173":{"start":{"line":311,"column":17},"end":{"line":311,"column":53}},"174":{"start":{"line":312,"column":6},"end":{"line":312,"column":22}},"175":{"start":{"line":313,"column":6},"end":{"line":313,"column":40}},"176":{"start":{"line":314,"column":6},"end":{"line":314,"column":63}},"177":{"start":{"line":316,"column":6},"end":{"line":316,"column":18}},"178":{"start":{"line":321,"column":0},"end":{"line":347,"column":2}},"179":{"start":{"line":324,"column":13},"end":{"line":324,"column":15}},"180":{"start":{"line":324,"column":25},"end":{"line":324,"column":29}},"181":{"start":{"line":325,"column":2},"end":{"line":339,"column":3}},"182":{"start":{"line":326,"column":4},"end":{"line":331,"column":5}},"183":{"start":{"line":327,"column":6},"end":{"line":327,"column":20}},"184":{"start":{"line":329,"column":6},"end":{"line":329,"column":28}},"185":{"start":{"line":330,"column":6},"end":{"line":330,"column":33}},"186":{"start":{"line":330,"column":27},"end":{"line":330,"column":33}},"187":{"start":{"line":334,"column":4},"end":{"line":336,"column":5}},"188":{"start":{"line":335,"column":6},"end":{"line":335,"column":41}},"189":{"start":{"line":338,"column":4},"end":{"line":338,"column":96}},"190":{"start":{"line":342,"column":2},"end":{"line":344,"column":3}},"191":{"start":{"line":343,"column":4},"end":{"line":343,"column":22}},"192":{"start":{"line":346,"column":2},"end":{"line":346,"column":14}},"193":{"start":{"line":349,"column":0},"end":{"line":351,"column":2}},"194":{"start":{"line":350,"column":2},"end":{"line":350,"column":30}},"195":{"start":{"line":353,"column":0},"end":{"line":356,"column":2}},"196":{"start":{"line":354,"column":2},"end":{"line":354,"column":24}},"197":{"start":{"line":355,"column":2},"end":{"line":355,"column":63}},"198":{"start":{"line":360,"column":0},"end":{"line":363,"column":2}},"199":{"start":{"line":361,"column":17},"end":{"line":361,"column":33}},"200":{"start":{"line":361,"column":46},"end":{"line":361,"column":65}},"201":{"start":{"line":362,"column":2},"end":{"line":362,"column":78}},"202":{"start":{"line":370,"column":0},"end":{"line":504,"column":2}},"203":{"start":{"line":371,"column":25},"end":{"line":371,"column":73}},"204":{"start":{"line":372,"column":2},"end":{"line":503,"column":3}},"205":{"start":{"line":374,"column":6},"end":{"line":376,"column":7}},"206":{"start":{"line":375,"column":8},"end":{"line":375,"column":77}},"207":{"start":{"line":378,"column":6},"end":{"line":378,"column":30}},"208":{"start":{"line":379,"column":6},"end":{"line":379,"column":18}},"209":{"start":{"line":380,"column":6},"end":{"line":382,"column":7}},"210":{"start":{"line":381,"column":8},"end":{"line":381,"column":26}},"211":{"start":{"line":383,"column":6},"end":{"line":385,"column":7}},"212":{"start":{"line":384,"column":8},"end":{"line":384,"column":71}},"213":{"start":{"line":386,"column":6},"end":{"line":386,"column":44}},"214":{"start":{"line":389,"column":6},"end":{"line":389,"column":30}},"215":{"start":{"line":390,"column":6},"end":{"line":390,"column":18}},"216":{"start":{"line":391,"column":6},"end":{"line":391,"column":53}},"217":{"start":{"line":394,"column":6},"end":{"line":394,"column":52}},"218":{"start":{"line":394,"column":34},"end":{"line":394,"column":52}},"219":{"start":{"line":397,"column":6},"end":{"line":397,"column":30}},"220":{"start":{"line":398,"column":23},"end":{"line":398,"column":73}},"221":{"start":{"line":399,"column":23},"end":{"line":399,"column":56}},"222":{"start":{"line":400,"column":15},"end":{"line":400,"column":61}},"223":{"start":{"line":402,"column":6},"end":{"line":414,"column":7}},"224":{"start":{"line":403,"column":8},"end":{"line":405,"column":9}},"225":{"start":{"line":404,"column":10},"end":{"line":404,"column":39}},"226":{"start":{"line":406,"column":13},"end":{"line":414,"column":7}},"227":{"start":{"line":407,"column":8},"end":{"line":407,"column":20}},"228":{"start":{"line":408,"column":8},"end":{"line":408,"column":60}},"229":{"start":{"line":409,"column":13},"end":{"line":414,"column":7}},"230":{"start":{"line":410,"column":21},"end":{"line":410,"column":45}},"231":{"start":{"line":411,"column":8},"end":{"line":411,"column":30}},"232":{"start":{"line":413,"column":8},"end":{"line":413,"column":61}},"233":{"start":{"line":416,"column":6},"end":{"line":418,"column":7}},"234":{"start":{"line":417,"column":8},"end":{"line":417,"column":53}},"235":{"start":{"line":420,"column":6},"end":{"line":420,"column":16}},"236":{"start":{"line":423,"column":6},"end":{"line":434,"column":7}},"237":{"start":{"line":424,"column":19},"end":{"line":424,"column":35}},"238":{"start":{"line":425,"column":8},"end":{"line":425,"column":20}},"239":{"start":{"line":426,"column":28},"end":{"line":426,"column":49}},"240":{"start":{"line":427,"column":24},"end":{"line":427,"column":41}},"241":{"start":{"line":428,"column":8},"end":{"line":428,"column":31}},"242":{"start":{"line":429,"column":8},"end":{"line":429,"column":38}},"243":{"start":{"line":430,"column":8},"end":{"line":430,"column":49}},"244":{"start":{"line":431,"column":8},"end":{"line":431,"column":46}},"245":{"start":{"line":432,"column":8},"end":{"line":432,"column":38}},"246":{"start":{"line":433,"column":8},"end":{"line":433,"column":53}},"247":{"start":{"line":437,"column":18},"end":{"line":437,"column":34}},"248":{"start":{"line":438,"column":6},"end":{"line":438,"column":61}},"249":{"start":{"line":439,"column":6},"end":{"line":439,"column":35}},"250":{"start":{"line":440,"column":6},"end":{"line":440,"column":31}},"251":{"start":{"line":441,"column":6},"end":{"line":441,"column":18}},"252":{"start":{"line":444,"column":6},"end":{"line":444,"column":67}},"253":{"start":{"line":447,"column":6},"end":{"line":447,"column":66}},"254":{"start":{"line":450,"column":6},"end":{"line":450,"column":30}},"255":{"start":{"line":451,"column":6},"end":{"line":451,"column":18}},"256":{"start":{"line":452,"column":6},"end":{"line":452,"column":50}},"257":{"start":{"line":455,"column":6},"end":{"line":455,"column":30}},"258":{"start":{"line":456,"column":6},"end":{"line":456,"column":40}},"259":{"start":{"line":457,"column":6},"end":{"line":457,"column":18}},"260":{"start":{"line":458,"column":6},"end":{"line":458,"column":53}},"261":{"start":{"line":461,"column":6},"end":{"line":461,"column":77}},"262":{"start":{"line":464,"column":6},"end":{"line":464,"column":30}},"263":{"start":{"line":465,"column":6},"end":{"line":465,"column":18}},"264":{"start":{"line":466,"column":6},"end":{"line":466,"column":84}},"265":{"start":{"line":467,"column":6},"end":{"line":467,"column":43}},"266":{"start":{"line":468,"column":6},"end":{"line":468,"column":54}},"267":{"start":{"line":471,"column":6},"end":{"line":471,"column":58}},"268":{"start":{"line":474,"column":6},"end":{"line":474,"column":44}},"269":{"start":{"line":477,"column":6},"end":{"line":477,"column":29}},"270":{"start":{"line":480,"column":6},"end":{"line":480,"column":30}},"271":{"start":{"line":481,"column":6},"end":{"line":481,"column":32}},"272":{"start":{"line":482,"column":6},"end":{"line":482,"column":42}},"273":{"start":{"line":485,"column":6},"end":{"line":485,"column":29}},"274":{"start":{"line":488,"column":6},"end":{"line":488,"column":34}},"275":{"start":{"line":491,"column":6},"end":{"line":491,"column":30}},"276":{"start":{"line":492,"column":6},"end":{"line":492,"column":18}},"277":{"start":{"line":493,"column":6},"end":{"line":493,"column":25}},"278":{"start":{"line":494,"column":19},"end":{"line":494,"column":55}},"279":{"start":{"line":495,"column":6},"end":{"line":499,"column":7}},"280":{"start":{"line":496,"column":8},"end":{"line":496,"column":55}},"281":{"start":{"line":498,"column":8},"end":{"line":498,"column":84}},"282":{"start":{"line":502,"column":6},"end":{"line":502,"column":24}},"283":{"start":{"line":506,"column":0},"end":{"line":514,"column":2}},"284":{"start":{"line":507,"column":13},"end":{"line":507,"column":29}},"285":{"start":{"line":508,"column":13},"end":{"line":508,"column":39}},"286":{"start":{"line":509,"column":2},"end":{"line":513,"column":3}},"287":{"start":{"line":510,"column":4},"end":{"line":510,"column":54}},"288":{"start":{"line":512,"column":4},"end":{"line":512,"column":43}},"289":{"start":{"line":516,"column":0},"end":{"line":525,"column":2}},"290":{"start":{"line":517,"column":2},"end":{"line":517,"column":19}},"291":{"start":{"line":518,"column":2},"end":{"line":518,"column":45}},"292":{"start":{"line":520,"column":2},"end":{"line":522,"column":3}},"293":{"start":{"line":521,"column":4},"end":{"line":521,"column":108}},"294":{"start":{"line":524,"column":2},"end":{"line":524,"column":47}},"295":{"start":{"line":527,"column":0},"end":{"line":534,"column":2}},"296":{"start":{"line":528,"column":13},"end":{"line":528,"column":29}},"297":{"start":{"line":529,"column":2},"end":{"line":529,"column":41}},"298":{"start":{"line":530,"column":2},"end":{"line":530,"column":81}},"299":{"start":{"line":531,"column":2},"end":{"line":531,"column":21}},"300":{"start":{"line":532,"column":2},"end":{"line":532,"column":14}},"301":{"start":{"line":533,"column":2},"end":{"line":533,"column":37}},"302":{"start":{"line":536,"column":0},"end":{"line":541,"column":2}},"303":{"start":{"line":537,"column":2},"end":{"line":537,"column":25}},"304":{"start":{"line":538,"column":12},"end":{"line":538,"column":34}},"305":{"start":{"line":539,"column":2},"end":{"line":539,"column":25}},"306":{"start":{"line":540,"column":2},"end":{"line":540,"column":13}},"307":{"start":{"line":543,"column":0},"end":{"line":614,"column":2}},"308":{"start":{"line":544,"column":2},"end":{"line":544,"column":42}},"309":{"start":{"line":545,"column":2},"end":{"line":545,"column":45}},"310":{"start":{"line":548,"column":2},"end":{"line":548,"column":25}},"311":{"start":{"line":550,"column":22},"end":{"line":550,"column":38}},"312":{"start":{"line":550,"column":56},"end":{"line":550,"column":75}},"313":{"start":{"line":551,"column":17},"end":{"line":551,"column":19}},"314":{"start":{"line":551,"column":29},"end":{"line":551,"column":33}},"315":{"start":{"line":552,"column":31},"end":{"line":552,"column":43}},"316":{"start":{"line":553,"column":25},"end":{"line":553,"column":37}},"317":{"start":{"line":554,"column":2},"end":{"line":573,"column":3}},"318":{"start":{"line":555,"column":4},"end":{"line":563,"column":5}},"319":{"start":{"line":556,"column":6},"end":{"line":556,"column":20}},"320":{"start":{"line":558,"column":6},"end":{"line":558,"column":60}},"321":{"start":{"line":559,"column":6},"end":{"line":562,"column":7}},"322":{"start":{"line":560,"column":8},"end":{"line":560,"column":46}},"323":{"start":{"line":561,"column":8},"end":{"line":561,"column":14}},"324":{"start":{"line":565,"column":4},"end":{"line":572,"column":5}},"325":{"start":{"line":566,"column":31},"end":{"line":566,"column":47}},"326":{"start":{"line":566,"column":70},"end":{"line":566,"column":89}},"327":{"start":{"line":567,"column":6},"end":{"line":567,"column":37}},"328":{"start":{"line":568,"column":6},"end":{"line":568,"column":99}},"329":{"start":{"line":569,"column":6},"end":{"line":569,"column":12}},"330":{"start":{"line":571,"column":6},"end":{"line":571,"column":113}},"331":{"start":{"line":575,"column":20},"end":{"line":575,"column":36}},"332":{"start":{"line":576,"column":20},"end":{"line":576,"column":39}},"333":{"start":{"line":577,"column":2},"end":{"line":577,"column":25}},"334":{"start":{"line":579,"column":18},"end":{"line":579,"column":54}},"335":{"start":{"line":580,"column":2},"end":{"line":586,"column":3}},"336":{"start":{"line":581,"column":4},"end":{"line":583,"column":5}},"337":{"start":{"line":582,"column":6},"end":{"line":582,"column":92}},"338":{"start":{"line":582,"column":52},"end":{"line":582,"column":92}},"339":{"start":{"line":585,"column":4},"end":{"line":585,"column":67}},"340":{"start":{"line":588,"column":2},"end":{"line":594,"column":3}},"341":{"start":{"line":589,"column":4},"end":{"line":593,"column":5}},"342":{"start":{"line":590,"column":6},"end":{"line":590,"column":13}},"343":{"start":{"line":592,"column":6},"end":{"line":592,"column":47}},"344":{"start":{"line":595,"column":2},"end":{"line":595,"column":62}},"345":{"start":{"line":595,"column":26},"end":{"line":595,"column":62}},"346":{"start":{"line":596,"column":2},"end":{"line":596,"column":48}},"347":{"start":{"line":596,"column":19},"end":{"line":596,"column":48}},"348":{"start":{"line":597,"column":2},"end":{"line":597,"column":82}},"349":{"start":{"line":597,"column":36},"end":{"line":597,"column":82}},"350":{"start":{"line":598,"column":2},"end":{"line":598,"column":70}},"351":{"start":{"line":598,"column":30},"end":{"line":598,"column":70}},"352":{"start":{"line":600,"column":2},"end":{"line":607,"column":3}},"353":{"start":{"line":601,"column":4},"end":{"line":601,"column":57}},"354":{"start":{"line":602,"column":4},"end":{"line":602,"column":31}},"355":{"start":{"line":603,"column":4},"end":{"line":603,"column":43}},"356":{"start":{"line":604,"column":4},"end":{"line":604,"column":75}},"357":{"start":{"line":606,"column":4},"end":{"line":606,"column":22}},"358":{"start":{"line":610,"column":2},"end":{"line":610,"column":44}},"359":{"start":{"line":611,"column":2},"end":{"line":611,"column":45}},"360":{"start":{"line":613,"column":2},"end":{"line":613,"column":13}},"361":{"start":{"line":616,"column":0},"end":{"line":620,"column":2}},"362":{"start":{"line":617,"column":2},"end":{"line":619,"column":3}},"363":{"start":{"line":618,"column":4},"end":{"line":618,"column":16}},"364":{"start":{"line":622,"column":0},"end":{"line":624,"column":2}},"365":{"start":{"line":623,"column":2},"end":{"line":623,"column":14}},"366":{"start":{"line":630,"column":0},"end":{"line":648,"column":2}},"367":{"start":{"line":631,"column":13},"end":{"line":631,"column":29}},"368":{"start":{"line":632,"column":13},"end":{"line":632,"column":39}},"369":{"start":{"line":634,"column":2},"end":{"line":636,"column":3}},"370":{"start":{"line":635,"column":4},"end":{"line":635,"column":56}},"371":{"start":{"line":638,"column":2},"end":{"line":638,"column":39}},"372":{"start":{"line":640,"column":2},"end":{"line":645,"column":3}},"373":{"start":{"line":641,"column":4},"end":{"line":641,"column":51}},"374":{"start":{"line":642,"column":4},"end":{"line":642,"column":42}},"375":{"start":{"line":644,"column":4},"end":{"line":644,"column":24}},"376":{"start":{"line":647,"column":2},"end":{"line":647,"column":48}},"377":{"start":{"line":652,"column":0},"end":{"line":661,"column":2}},"378":{"start":{"line":653,"column":13},"end":{"line":653,"column":29}},"379":{"start":{"line":654,"column":2},"end":{"line":657,"column":4}},"380":{"start":{"line":658,"column":2},"end":{"line":658,"column":14}},"381":{"start":{"line":659,"column":2},"end":{"line":659,"column":39}},"382":{"start":{"line":660,"column":2},"end":{"line":660,"column":50}},"383":{"start":{"line":663,"column":0},"end":{"line":677,"column":2}},"384":{"start":{"line":664,"column":13},"end":{"line":664,"column":29}},"385":{"start":{"line":665,"column":2},"end":{"line":665,"column":14}},"386":{"start":{"line":666,"column":2},"end":{"line":666,"column":24}},"387":{"start":{"line":667,"column":15},"end":{"line":667,"column":42}},"388":{"start":{"line":668,"column":2},"end":{"line":668,"column":25}},"389":{"start":{"line":669,"column":2},"end":{"line":674,"column":3}},"390":{"start":{"line":670,"column":4},"end":{"line":670,"column":33}},"391":{"start":{"line":671,"column":4},"end":{"line":671,"column":50}},"392":{"start":{"line":672,"column":4},"end":{"line":672,"column":27}},"393":{"start":{"line":673,"column":4},"end":{"line":673,"column":59}},"394":{"start":{"line":675,"column":2},"end":{"line":675,"column":14}},"395":{"start":{"line":676,"column":2},"end":{"line":676,"column":50}},"396":{"start":{"line":681,"column":0},"end":{"line":757,"column":2}},"397":{"start":{"line":682,"column":19},"end":{"line":682,"column":21}},"398":{"start":{"line":683,"column":17},"end":{"line":683,"column":36}},"399":{"start":{"line":684,"column":14},"end":{"line":684,"column":18}},"400":{"start":{"line":685,"column":13},"end":{"line":685,"column":29}},"401":{"start":{"line":687,"column":2},"end":{"line":687,"column":23}},"402":{"start":{"line":688,"column":2},"end":{"line":688,"column":14}},"403":{"start":{"line":690,"column":2},"end":{"line":750,"column":3}},"404":{"start":{"line":691,"column":4},"end":{"line":696,"column":5}},"405":{"start":{"line":692,"column":6},"end":{"line":692,"column":20}},"406":{"start":{"line":694,"column":6},"end":{"line":694,"column":28}},"407":{"start":{"line":695,"column":6},"end":{"line":695,"column":37}},"408":{"start":{"line":695,"column":31},"end":{"line":695,"column":37}},"409":{"start":{"line":698,"column":4},"end":{"line":700,"column":5}},"410":{"start":{"line":699,"column":6},"end":{"line":699,"column":45}},"411":{"start":{"line":702,"column":15},"end":{"line":702,"column":31}},"412":{"start":{"line":702,"column":47},"end":{"line":702,"column":52}},"413":{"start":{"line":702,"column":64},"end":{"line":702,"column":69}},"414":{"start":{"line":703,"column":4},"end":{"line":706,"column":5}},"415":{"start":{"line":704,"column":6},"end":{"line":704,"column":35}},"416":{"start":{"line":705,"column":6},"end":{"line":705,"column":22}},"417":{"start":{"line":708,"column":4},"end":{"line":713,"column":5}},"418":{"start":{"line":709,"column":6},"end":{"line":709,"column":32}},"419":{"start":{"line":710,"column":6},"end":{"line":710,"column":64}},"420":{"start":{"line":711,"column":6},"end":{"line":711,"column":33}},"421":{"start":{"line":712,"column":6},"end":{"line":712,"column":15}},"422":{"start":{"line":715,"column":4},"end":{"line":715,"column":24}},"423":{"start":{"line":716,"column":4},"end":{"line":716,"column":27}},"424":{"start":{"line":718,"column":4},"end":{"line":721,"column":5}},"425":{"start":{"line":719,"column":6},"end":{"line":719,"column":34}},"426":{"start":{"line":720,"column":6},"end":{"line":720,"column":37}},"427":{"start":{"line":723,"column":4},"end":{"line":725,"column":5}},"428":{"start":{"line":724,"column":6},"end":{"line":724,"column":38}},"429":{"start":{"line":727,"column":4},"end":{"line":740,"column":5}},"430":{"start":{"line":728,"column":6},"end":{"line":728,"column":41}},"431":{"start":{"line":728,"column":23},"end":{"line":728,"column":41}},"432":{"start":{"line":730,"column":20},"end":{"line":730,"column":42}},"433":{"start":{"line":731,"column":6},"end":{"line":737,"column":7}},"434":{"start":{"line":732,"column":8},"end":{"line":732,"column":27}},"435":{"start":{"line":734,"column":8},"end":{"line":734,"column":23}},"436":{"start":{"line":735,"column":8},"end":{"line":735,"column":79}},"437":{"start":{"line":735,"column":47},"end":{"line":735,"column":79}},"438":{"start":{"line":736,"column":8},"end":{"line":736,"column":37}},"439":{"start":{"line":739,"column":6},"end":{"line":739,"column":35}},"440":{"start":{"line":742,"column":4},"end":{"line":742,"column":110}},"441":{"start":{"line":743,"column":4},"end":{"line":743,"column":40}},"442":{"start":{"line":745,"column":4},"end":{"line":747,"column":5}},"443":{"start":{"line":746,"column":6},"end":{"line":746,"column":45}},"444":{"start":{"line":749,"column":4},"end":{"line":749,"column":31}},"445":{"start":{"line":752,"column":2},"end":{"line":754,"column":3}},"446":{"start":{"line":753,"column":4},"end":{"line":753,"column":82}},"447":{"start":{"line":756,"column":2},"end":{"line":756,"column":81}},"448":{"start":{"line":759,"column":0},"end":{"line":813,"column":2}},"449":{"start":{"line":760,"column":2},"end":{"line":766,"column":3}},"450":{"start":{"line":761,"column":4},"end":{"line":761,"column":37}},"451":{"start":{"line":761,"column":19},"end":{"line":761,"column":37}},"452":{"start":{"line":762,"column":4},"end":{"line":762,"column":25}},"453":{"start":{"line":763,"column":4},"end":{"line":763,"column":23}},"454":{"start":{"line":764,"column":4},"end":{"line":764,"column":49}},"455":{"start":{"line":765,"column":4},"end":{"line":765,"column":49}},"456":{"start":{"line":768,"column":2},"end":{"line":771,"column":3}},"457":{"start":{"line":769,"column":4},"end":{"line":769,"column":146}},"458":{"start":{"line":770,"column":4},"end":{"line":770,"column":51}},"459":{"start":{"line":773,"column":2},"end":{"line":788,"column":3}},"460":{"start":{"line":774,"column":4},"end":{"line":774,"column":63}},"461":{"start":{"line":774,"column":45},"end":{"line":774,"column":63}},"462":{"start":{"line":775,"column":4},"end":{"line":775,"column":30}},"463":{"start":{"line":776,"column":4},"end":{"line":776,"column":33}},"464":{"start":{"line":777,"column":4},"end":{"line":777,"column":34}},"465":{"start":{"line":778,"column":21},"end":{"line":778,"column":48}},"466":{"start":{"line":779,"column":4},"end":{"line":786,"column":5}},"467":{"start":{"line":780,"column":18},"end":{"line":780,"column":28}},"468":{"start":{"line":781,"column":6},"end":{"line":785,"column":7}},"469":{"start":{"line":782,"column":8},"end":{"line":782,"column":58}},"470":{"start":{"line":784,"column":8},"end":{"line":784,"column":66}},"471":{"start":{"line":787,"column":4},"end":{"line":787,"column":49}},"472":{"start":{"line":790,"column":2},"end":{"line":810,"column":3}},"473":{"start":{"line":791,"column":4},"end":{"line":807,"column":5}},"474":{"start":{"line":792,"column":27},"end":{"line":792,"column":56}},"475":{"start":{"line":793,"column":6},"end":{"line":795,"column":7}},"476":{"start":{"line":794,"column":8},"end":{"line":794,"column":104}},"477":{"start":{"line":796,"column":6},"end":{"line":798,"column":7}},"478":{"start":{"line":797,"column":8},"end":{"line":797,"column":63}},"479":{"start":{"line":799,"column":6},"end":{"line":799,"column":82}},"480":{"start":{"line":800,"column":11},"end":{"line":807,"column":5}},"481":{"start":{"line":801,"column":6},"end":{"line":803,"column":7}},"482":{"start":{"line":802,"column":8},"end":{"line":802,"column":56}},"483":{"start":{"line":804,"column":6},"end":{"line":804,"column":82}},"484":{"start":{"line":806,"column":6},"end":{"line":806,"column":38}},"485":{"start":{"line":808,"column":4},"end":{"line":808,"column":26}},"486":{"start":{"line":809,"column":4},"end":{"line":809,"column":51}},"487":{"start":{"line":812,"column":2},"end":{"line":812,"column":20}},"488":{"start":{"line":815,"column":0},"end":{"line":825,"column":2}},"489":{"start":{"line":816,"column":2},"end":{"line":824,"column":3}},"490":{"start":{"line":817,"column":4},"end":{"line":817,"column":25}},"491":{"start":{"line":818,"column":4},"end":{"line":818,"column":39}},"492":{"start":{"line":819,"column":4},"end":{"line":819,"column":29}},"493":{"start":{"line":820,"column":4},"end":{"line":820,"column":20}},"494":{"start":{"line":822,"column":4},"end":{"line":822,"column":26}},"495":{"start":{"line":823,"column":4},"end":{"line":823,"column":120}},"496":{"start":{"line":829,"column":0},"end":{"line":834,"column":2}},"497":{"start":{"line":830,"column":2},"end":{"line":830,"column":17}},"498":{"start":{"line":831,"column":2},"end":{"line":831,"column":25}},"499":{"start":{"line":832,"column":2},"end":{"line":832,"column":26}},"500":{"start":{"line":833,"column":2},"end":{"line":833,"column":25}},"501":{"start":{"line":838,"column":0},"end":{"line":848,"column":2}},"502":{"start":{"line":839,"column":20},"end":{"line":839,"column":39}},"503":{"start":{"line":840,"column":2},"end":{"line":840,"column":42}},"504":{"start":{"line":841,"column":2},"end":{"line":841,"column":35}},"505":{"start":{"line":842,"column":2},"end":{"line":842,"column":25}},"506":{"start":{"line":843,"column":2},"end":{"line":843,"column":49}},"507":{"start":{"line":844,"column":2},"end":{"line":844,"column":31}},"508":{"start":{"line":845,"column":2},"end":{"line":845,"column":31}},"509":{"start":{"line":846,"column":2},"end":{"line":846,"column":36}},"510":{"start":{"line":847,"column":2},"end":{"line":847,"column":14}},"511":{"start":{"line":852,"column":0},"end":{"line":857,"column":2}},"512":{"start":{"line":853,"column":2},"end":{"line":853,"column":35}},"513":{"start":{"line":854,"column":2},"end":{"line":854,"column":52}},"514":{"start":{"line":855,"column":2},"end":{"line":855,"column":37}},"515":{"start":{"line":856,"column":2},"end":{"line":856,"column":58}},"516":{"start":{"line":861,"column":0},"end":{"line":919,"column":2}},"517":{"start":{"line":862,"column":21},"end":{"line":862,"column":62}},"518":{"start":{"line":864,"column":19},"end":{"line":864,"column":37}},"519":{"start":{"line":865,"column":2},"end":{"line":865,"column":34}},"520":{"start":{"line":866,"column":2},"end":{"line":877,"column":3}},"521":{"start":{"line":867,"column":4},"end":{"line":867,"column":40}},"522":{"start":{"line":868,"column":4},"end":{"line":868,"column":27}},"523":{"start":{"line":872,"column":20},"end":{"line":872,"column":41}},"524":{"start":{"line":872,"column":54},"end":{"line":872,"column":76}},"525":{"start":{"line":872,"column":90},"end":{"line":872,"column":107}},"526":{"start":{"line":873,"column":4},"end":{"line":873,"column":33}},"527":{"start":{"line":873,"column":34},"end":{"line":873,"column":74}},"528":{"start":{"line":873,"column":75},"end":{"line":873,"column":98}},"529":{"start":{"line":874,"column":4},"end":{"line":874,"column":38}},"530":{"start":{"line":875,"column":4},"end":{"line":875,"column":28}},"531":{"start":{"line":876,"column":4},"end":{"line":876,"column":38}},"532":{"start":{"line":876,"column":39},"end":{"line":876,"column":73}},"533":{"start":{"line":876,"column":74},"end":{"line":876,"column":104}},"534":{"start":{"line":878,"column":2},"end":{"line":878,"column":34}},"535":{"start":{"line":883,"column":18},"end":{"line":883,"column":35}},"536":{"start":{"line":884,"column":24},"end":{"line":884,"column":29}},"537":{"start":{"line":885,"column":17},"end":{"line":885,"column":22}},"538":{"start":{"line":888,"column":2},"end":{"line":888,"column":40}},"539":{"start":{"line":888,"column":23},"end":{"line":888,"column":40}},"540":{"start":{"line":891,"column":2},"end":{"line":900,"column":3}},"541":{"start":{"line":892,"column":4},"end":{"line":899,"column":5}},"542":{"start":{"line":893,"column":6},"end":{"line":898,"column":7}},"543":{"start":{"line":894,"column":8},"end":{"line":894,"column":24}},"544":{"start":{"line":895,"column":8},"end":{"line":895,"column":25}},"545":{"start":{"line":896,"column":8},"end":{"line":896,"column":31}},"546":{"start":{"line":897,"column":8},"end":{"line":897,"column":14}},"547":{"start":{"line":903,"column":2},"end":{"line":905,"column":3}},"548":{"start":{"line":904,"column":4},"end":{"line":904,"column":62}},"549":{"start":{"line":907,"column":2},"end":{"line":918,"column":3}},"550":{"start":{"line":908,"column":19},"end":{"line":908,"column":38}},"551":{"start":{"line":909,"column":20},"end":{"line":909,"column":37}},"552":{"start":{"line":910,"column":4},"end":{"line":910,"column":50}},"553":{"start":{"line":910,"column":25},"end":{"line":910,"column":50}},"554":{"start":{"line":911,"column":4},"end":{"line":913,"column":5}},"555":{"start":{"line":912,"column":6},"end":{"line":912,"column":36}},"556":{"start":{"line":914,"column":4},"end":{"line":916,"column":5}},"557":{"start":{"line":915,"column":6},"end":{"line":915,"column":44}},"558":{"start":{"line":917,"column":4},"end":{"line":917,"column":34}},"559":{"start":{"line":927,"column":0},"end":{"line":940,"column":2}},"560":{"start":{"line":928,"column":13},"end":{"line":928,"column":15}},"561":{"start":{"line":928,"column":25},"end":{"line":928,"column":29}},"562":{"start":{"line":929,"column":2},"end":{"line":938,"column":3}},"563":{"start":{"line":930,"column":4},"end":{"line":935,"column":5}},"564":{"start":{"line":931,"column":6},"end":{"line":931,"column":20}},"565":{"start":{"line":933,"column":6},"end":{"line":933,"column":28}},"566":{"start":{"line":934,"column":6},"end":{"line":934,"column":33}},"567":{"start":{"line":934,"column":27},"end":{"line":934,"column":33}},"568":{"start":{"line":937,"column":4},"end":{"line":937,"column":74}},"569":{"start":{"line":939,"column":2},"end":{"line":939,"column":14}},"570":{"start":{"line":942,"column":0},"end":{"line":952,"column":2}},"571":{"start":{"line":944,"column":2},"end":{"line":950,"column":3}},"572":{"start":{"line":945,"column":4},"end":{"line":945,"column":15}},"573":{"start":{"line":946,"column":9},"end":{"line":950,"column":3}},"574":{"start":{"line":947,"column":4},"end":{"line":947,"column":51}},"575":{"start":{"line":949,"column":4},"end":{"line":949,"column":84}},"576":{"start":{"line":951,"column":2},"end":{"line":951,"column":13}},"577":{"start":{"line":958,"column":0},"end":{"line":981,"column":2}},"578":{"start":{"line":959,"column":13},"end":{"line":959,"column":29}},"579":{"start":{"line":961,"column":2},"end":{"line":971,"column":3}},"580":{"start":{"line":962,"column":4},"end":{"line":964,"column":5}},"581":{"start":{"line":963,"column":6},"end":{"line":963,"column":89}},"582":{"start":{"line":966,"column":4},"end":{"line":966,"column":33}},"583":{"start":{"line":967,"column":9},"end":{"line":971,"column":3}},"584":{"start":{"line":968,"column":4},"end":{"line":968,"column":40}},"585":{"start":{"line":970,"column":4},"end":{"line":970,"column":22}},"586":{"start":{"line":973,"column":2},"end":{"line":975,"column":3}},"587":{"start":{"line":974,"column":4},"end":{"line":974,"column":79}},"588":{"start":{"line":977,"column":2},"end":{"line":977,"column":38}},"589":{"start":{"line":979,"column":2},"end":{"line":979,"column":14}},"590":{"start":{"line":980,"column":2},"end":{"line":980,"column":45}},"591":{"start":{"line":985,"column":0},"end":{"line":994,"column":2}},"592":{"start":{"line":986,"column":2},"end":{"line":988,"column":3}},"593":{"start":{"line":987,"column":4},"end":{"line":987,"column":22}},"594":{"start":{"line":989,"column":2},"end":{"line":991,"column":3}},"595":{"start":{"line":990,"column":4},"end":{"line":990,"column":116}},"596":{"start":{"line":992,"column":2},"end":{"line":992,"column":41}},"597":{"start":{"line":993,"column":2},"end":{"line":993,"column":50}},"598":{"start":{"line":998,"column":0},"end":{"line":1009,"column":2}},"599":{"start":{"line":999,"column":13},"end":{"line":999,"column":29}},"600":{"start":{"line":1000,"column":2},"end":{"line":1000,"column":14}},"601":{"start":{"line":1001,"column":2},"end":{"line":1007,"column":3}},"602":{"start":{"line":1002,"column":4},"end":{"line":1002,"column":26}},"603":{"start":{"line":1003,"column":4},"end":{"line":1003,"column":25}},"604":{"start":{"line":1005,"column":4},"end":{"line":1005,"column":38}},"605":{"start":{"line":1006,"column":4},"end":{"line":1006,"column":44}},"606":{"start":{"line":1008,"column":2},"end":{"line":1008,"column":50}}},"fnMap":{"1":{"name":"(anonymous_1)","decl":{"start":{"line":33,"column":20},"end":{"line":33,"column":21}},"loc":{"start":{"line":33,"column":46},"end":{"line":56,"column":1}}},"2":{"name":"(anonymous_2)","decl":{"start":{"line":73,"column":21},"end":{"line":73,"column":22}},"loc":{"start":{"line":73,"column":61},"end":{"line":86,"column":1}}},"3":{"name":"(anonymous_3)","decl":{"start":{"line":91,"column":22},"end":{"line":91,"column":23}},"loc":{"start":{"line":91,"column":96},"end":{"line":141,"column":1}}},"4":{"name":"(anonymous_4)","decl":{"start":{"line":145,"column":27},"end":{"line":145,"column":28}},"loc":{"start":{"line":145,"column":85},"end":{"line":151,"column":1}}},"5":{"name":"(anonymous_5)","decl":{"start":{"line":153,"column":22},"end":{"line":153,"column":23}},"loc":{"start":{"line":153,"column":64},"end":{"line":163,"column":1}}},"6":{"name":"(anonymous_6)","decl":{"start":{"line":167,"column":18},"end":{"line":167,"column":19}},"loc":{"start":{"line":167,"column":58},"end":{"line":175,"column":1}}},"7":{"name":"(anonymous_7)","decl":{"start":{"line":183,"column":17},"end":{"line":183,"column":18}},"loc":{"start":{"line":183,"column":75},"end":{"line":213,"column":1}}},"8":{"name":"(anonymous_8)","decl":{"start":{"line":217,"column":21},"end":{"line":217,"column":22}},"loc":{"start":{"line":217,"column":55},"end":{"line":256,"column":1}}},"9":{"name":"(anonymous_9)","decl":{"start":{"line":260,"column":25},"end":{"line":260,"column":26}},"loc":{"start":{"line":260,"column":59},"end":{"line":274,"column":1}}},"10":{"name":"(anonymous_10)","decl":{"start":{"line":276,"column":21},"end":{"line":276,"column":22}},"loc":{"start":{"line":276,"column":66},"end":{"line":319,"column":1}}},"11":{"name":"(anonymous_11)","decl":{"start":{"line":321,"column":34},"end":{"line":321,"column":35}},"loc":{"start":{"line":321,"column":71},"end":{"line":347,"column":1}}},"12":{"name":"(anonymous_12)","decl":{"start":{"line":349,"column":27},"end":{"line":349,"column":28}},"loc":{"start":{"line":349,"column":39},"end":{"line":351,"column":1}}},"13":{"name":"(anonymous_13)","decl":{"start":{"line":353,"column":39},"end":{"line":353,"column":40}},"loc":{"start":{"line":353,"column":61},"end":{"line":356,"column":1}}},"14":{"name":"(anonymous_14)","decl":{"start":{"line":360,"column":21},"end":{"line":360,"column":22}},"loc":{"start":{"line":360,"column":33},"end":{"line":363,"column":1}}},"15":{"name":"(anonymous_15)","decl":{"start":{"line":370,"column":19},"end":{"line":370,"column":20}},"loc":{"start":{"line":370,"column":53},"end":{"line":504,"column":1}}},"16":{"name":"(anonymous_16)","decl":{"start":{"line":506,"column":29},"end":{"line":506,"column":30}},"loc":{"start":{"line":506,"column":41},"end":{"line":514,"column":1}}},"17":{"name":"(anonymous_17)","decl":{"start":{"line":516,"column":23},"end":{"line":516,"column":24}},"loc":{"start":{"line":516,"column":59},"end":{"line":525,"column":1}}},"18":{"name":"(anonymous_18)","decl":{"start":{"line":527,"column":18},"end":{"line":527,"column":19}},"loc":{"start":{"line":527,"column":41},"end":{"line":534,"column":1}}},"19":{"name":"(anonymous_19)","decl":{"start":{"line":536,"column":26},"end":{"line":536,"column":27}},"loc":{"start":{"line":536,"column":38},"end":{"line":541,"column":1}}},"20":{"name":"(anonymous_20)","decl":{"start":{"line":543,"column":40},"end":{"line":543,"column":41}},"loc":{"start":{"line":543,"column":91},"end":{"line":614,"column":1}}},"21":{"name":"(anonymous_21)","decl":{"start":{"line":616,"column":16},"end":{"line":616,"column":17}},"loc":{"start":{"line":616,"column":32},"end":{"line":620,"column":1}}},"22":{"name":"(anonymous_22)","decl":{"start":{"line":622,"column":20},"end":{"line":622,"column":21}},"loc":{"start":{"line":622,"column":36},"end":{"line":624,"column":1}}},"23":{"name":"(anonymous_23)","decl":{"start":{"line":630,"column":14},"end":{"line":630,"column":15}},"loc":{"start":{"line":630,"column":26},"end":{"line":648,"column":1}}},"24":{"name":"(anonymous_24)","decl":{"start":{"line":652,"column":26},"end":{"line":652,"column":27}},"loc":{"start":{"line":652,"column":38},"end":{"line":661,"column":1}}},"25":{"name":"(anonymous_25)","decl":{"start":{"line":663,"column":19},"end":{"line":663,"column":20}},"loc":{"start":{"line":663,"column":31},"end":{"line":677,"column":1}}},"26":{"name":"(anonymous_26)","decl":{"start":{"line":681,"column":14},"end":{"line":681,"column":15}},"loc":{"start":{"line":681,"column":59},"end":{"line":757,"column":1}}},"27":{"name":"(anonymous_27)","decl":{"start":{"line":759,"column":23},"end":{"line":759,"column":24}},"loc":{"start":{"line":759,"column":116},"end":{"line":813,"column":1}}},"28":{"name":"(anonymous_28)","decl":{"start":{"line":815,"column":23},"end":{"line":815,"column":24}},"loc":{"start":{"line":815,"column":39},"end":{"line":825,"column":1}}},"29":{"name":"(anonymous_29)","decl":{"start":{"line":829,"column":18},"end":{"line":829,"column":19}},"loc":{"start":{"line":829,"column":43},"end":{"line":834,"column":1}}},"30":{"name":"(anonymous_30)","decl":{"start":{"line":838,"column":17},"end":{"line":838,"column":18}},"loc":{"start":{"line":838,"column":55},"end":{"line":848,"column":1}}},"31":{"name":"(anonymous_31)","decl":{"start":{"line":852,"column":26},"end":{"line":852,"column":27}},"loc":{"start":{"line":852,"column":59},"end":{"line":857,"column":1}}},"32":{"name":"(anonymous_32)","decl":{"start":{"line":861,"column":23},"end":{"line":861,"column":24}},"loc":{"start":{"line":861,"column":56},"end":{"line":919,"column":1}}},"33":{"name":"(anonymous_33)","decl":{"start":{"line":927,"column":19},"end":{"line":927,"column":20}},"loc":{"start":{"line":927,"column":72},"end":{"line":940,"column":1}}},"34":{"name":"(anonymous_34)","decl":{"start":{"line":942,"column":23},"end":{"line":942,"column":24}},"loc":{"start":{"line":942,"column":69},"end":{"line":952,"column":1}}},"35":{"name":"(anonymous_35)","decl":{"start":{"line":958,"column":21},"end":{"line":958,"column":22}},"loc":{"start":{"line":958,"column":40},"end":{"line":981,"column":1}}},"36":{"name":"(anonymous_36)","decl":{"start":{"line":985,"column":16},"end":{"line":985,"column":17}},"loc":{"start":{"line":985,"column":32},"end":{"line":994,"column":1}}},"37":{"name":"(anonymous_37)","decl":{"start":{"line":998,"column":16},"end":{"line":998,"column":17}},"loc":{"start":{"line":998,"column":28},"end":{"line":1009,"column":1}}}},"branchMap":{"1":{"loc":{"start":{"line":34,"column":2},"end":{"line":34,"column":28}},"type":"if","locations":[{"start":{"line":34,"column":2},"end":{"line":34,"column":28}},{"start":{"line":34,"column":2},"end":{"line":34,"column":28}}]},"2":{"loc":{"start":{"line":38,"column":2},"end":{"line":50,"column":3}},"type":"switch","locations":[{"start":{"line":39,"column":4},"end":{"line":41,"column":12}},{"start":{"line":43,"column":4},"end":{"line":43,"column":25}},{"start":{"line":44,"column":4},"end":{"line":46,"column":12}},{"start":{"line":48,"column":4},"end":{"line":49,"column":13}}]},"3":{"loc":{"start":{"line":52,"column":2},"end":{"line":55,"column":3}},"type":"if","locations":[{"start":{"line":52,"column":2},"end":{"line":55,"column":3}},{"start":{"line":52,"column":2},"end":{"line":55,"column":3}}]},"4":{"loc":{"start":{"line":52,"column":6},"end":{"line":52,"column":50}},"type":"binary-expr","locations":[{"start":{"line":52,"column":6},"end":{"line":52,"column":26}},{"start":{"line":52,"column":30},"end":{"line":52,"column":50}}]},"5":{"loc":{"start":{"line":53,"column":4},"end":{"line":53,"column":84}},"type":"if","locations":[{"start":{"line":53,"column":4},"end":{"line":53,"column":84}},{"start":{"line":53,"column":4},"end":{"line":53,"column":84}}]},"6":{"loc":{"start":{"line":76,"column":2},"end":{"line":84,"column":3}},"type":"if","locations":[{"start":{"line":76,"column":2},"end":{"line":84,"column":3}},{"start":{"line":76,"column":2},"end":{"line":84,"column":3}}]},"7":{"loc":{"start":{"line":92,"column":2},"end":{"line":94,"column":3}},"type":"if","locations":[{"start":{"line":92,"column":2},"end":{"line":94,"column":3}},{"start":{"line":92,"column":2},"end":{"line":94,"column":3}}]},"8":{"loc":{"start":{"line":92,"column":6},"end":{"line":92,"column":53}},"type":"binary-expr","locations":[{"start":{"line":92,"column":6},"end":{"line":92,"column":27}},{"start":{"line":92,"column":31},"end":{"line":92,"column":53}}]},"9":{"loc":{"start":{"line":97,"column":2},"end":{"line":102,"column":3}},"type":"if","locations":[{"start":{"line":97,"column":2},"end":{"line":102,"column":3}},{"start":{"line":97,"column":2},"end":{"line":102,"column":3}}]},"10":{"loc":{"start":{"line":107,"column":2},"end":{"line":109,"column":3}},"type":"if","locations":[{"start":{"line":107,"column":2},"end":{"line":109,"column":3}},{"start":{"line":107,"column":2},"end":{"line":109,"column":3}}]},"11":{"loc":{"start":{"line":107,"column":6},"end":{"line":107,"column":50}},"type":"binary-expr","locations":[{"start":{"line":107,"column":6},"end":{"line":107,"column":27}},{"start":{"line":107,"column":31},"end":{"line":107,"column":50}}]},"12":{"loc":{"start":{"line":112,"column":2},"end":{"line":112,"column":81}},"type":"if","locations":[{"start":{"line":112,"column":2},"end":{"line":112,"column":81}},{"start":{"line":112,"column":2},"end":{"line":112,"column":81}}]},"13":{"loc":{"start":{"line":113,"column":2},"end":{"line":138,"column":3}},"type":"if","locations":[{"start":{"line":113,"column":2},"end":{"line":138,"column":3}},{"start":{"line":113,"column":2},"end":{"line":138,"column":3}}]},"14":{"loc":{"start":{"line":116,"column":16},"end":{"line":116,"column":66}},"type":"cond-expr","locations":[{"start":{"line":116,"column":36},"end":{"line":116,"column":59}},{"start":{"line":116,"column":62},"end":{"line":116,"column":66}}]},"15":{"loc":{"start":{"line":121,"column":4},"end":{"line":131,"column":5}},"type":"if","locations":[{"start":{"line":121,"column":4},"end":{"line":131,"column":5}},{"start":{"line":121,"column":4},"end":{"line":131,"column":5}}]},"16":{"loc":{"start":{"line":121,"column":8},"end":{"line":121,"column":46}},"type":"binary-expr","locations":[{"start":{"line":121,"column":8},"end":{"line":121,"column":18}},{"start":{"line":121,"column":22},"end":{"line":121,"column":46}}]},"17":{"loc":{"start":{"line":123,"column":6},"end":{"line":127,"column":7}},"type":"if","locations":[{"start":{"line":123,"column":6},"end":{"line":127,"column":7}},{"start":{"line":123,"column":6},"end":{"line":127,"column":7}}]},"18":{"loc":{"start":{"line":125,"column":13},"end":{"line":127,"column":7}},"type":"if","locations":[{"start":{"line":125,"column":13},"end":{"line":127,"column":7}},{"start":{"line":125,"column":13},"end":{"line":127,"column":7}}]},"19":{"loc":{"start":{"line":128,"column":6},"end":{"line":130,"column":7}},"type":"if","locations":[{"start":{"line":128,"column":6},"end":{"line":130,"column":7}},{"start":{"line":128,"column":6},"end":{"line":130,"column":7}}]},"20":{"loc":{"start":{"line":136,"column":9},"end":{"line":138,"column":3}},"type":"if","locations":[{"start":{"line":136,"column":9},"end":{"line":138,"column":3}},{"start":{"line":136,"column":9},"end":{"line":138,"column":3}}]},"21":{"loc":{"start":{"line":136,"column":13},"end":{"line":136,"column":66}},"type":"binary-expr","locations":[{"start":{"line":136,"column":13},"end":{"line":136,"column":34}},{"start":{"line":136,"column":38},"end":{"line":136,"column":66}}]},"22":{"loc":{"start":{"line":148,"column":2},"end":{"line":148,"column":74}},"type":"if","locations":[{"start":{"line":148,"column":2},"end":{"line":148,"column":74}},{"start":{"line":148,"column":2},"end":{"line":148,"column":74}}]},"23":{"loc":{"start":{"line":148,"column":6},"end":{"line":148,"column":60}},"type":"binary-expr","locations":[{"start":{"line":148,"column":6},"end":{"line":148,"column":28}},{"start":{"line":148,"column":32},"end":{"line":148,"column":60}}]},"24":{"loc":{"start":{"line":154,"column":2},"end":{"line":161,"column":3}},"type":"if","locations":[{"start":{"line":154,"column":2},"end":{"line":161,"column":3}},{"start":{"line":154,"column":2},"end":{"line":161,"column":3}}]},"25":{"loc":{"start":{"line":170,"column":2},"end":{"line":174,"column":3}},"type":"if","locations":[{"start":{"line":170,"column":2},"end":{"line":174,"column":3}},{"start":{"line":170,"column":2},"end":{"line":174,"column":3}}]},"26":{"loc":{"start":{"line":170,"column":6},"end":{"line":170,"column":60}},"type":"binary-expr","locations":[{"start":{"line":170,"column":6},"end":{"line":170,"column":28}},{"start":{"line":170,"column":32},"end":{"line":170,"column":60}}]},"27":{"loc":{"start":{"line":185,"column":2},"end":{"line":211,"column":3}},"type":"if","locations":[{"start":{"line":185,"column":2},"end":{"line":211,"column":3}},{"start":{"line":185,"column":2},"end":{"line":211,"column":3}}]},"28":{"loc":{"start":{"line":185,"column":6},"end":{"line":185,"column":52}},"type":"binary-expr","locations":[{"start":{"line":185,"column":6},"end":{"line":185,"column":18}},{"start":{"line":185,"column":23},"end":{"line":185,"column":28}},{"start":{"line":185,"column":32},"end":{"line":185,"column":51}}]},"29":{"loc":{"start":{"line":186,"column":4},"end":{"line":210,"column":5}},"type":"if","locations":[{"start":{"line":186,"column":4},"end":{"line":210,"column":5}},{"start":{"line":186,"column":4},"end":{"line":210,"column":5}}]},"30":{"loc":{"start":{"line":191,"column":6},"end":{"line":199,"column":7}},"type":"if","locations":[{"start":{"line":191,"column":6},"end":{"line":199,"column":7}},{"start":{"line":191,"column":6},"end":{"line":199,"column":7}}]},"31":{"loc":{"start":{"line":192,"column":8},"end":{"line":196,"column":33}},"type":"binary-expr","locations":[{"start":{"line":192,"column":8},"end":{"line":192,"column":30}},{"start":{"line":193,"column":8},"end":{"line":193,"column":39}},{"start":{"line":194,"column":8},"end":{"line":194,"column":18}},{"start":{"line":195,"column":8},"end":{"line":195,"column":41}},{"start":{"line":196,"column":8},"end":{"line":196,"column":33}}]},"32":{"loc":{"start":{"line":206,"column":80},"end":{"line":206,"column":117}},"type":"cond-expr","locations":[{"start":{"line":206,"column":102},"end":{"line":206,"column":110}},{"start":{"line":206,"column":113},"end":{"line":206,"column":117}}]},"33":{"loc":{"start":{"line":208,"column":28},"end":{"line":208,"column":116}},"type":"cond-expr","locations":[{"start":{"line":208,"column":76},"end":{"line":208,"column":95}},{"start":{"line":208,"column":98},"end":{"line":208,"column":116}}]},"34":{"loc":{"start":{"line":208,"column":29},"end":{"line":208,"column":72}},"type":"binary-expr","locations":[{"start":{"line":208,"column":29},"end":{"line":208,"column":48}},{"start":{"line":208,"column":52},"end":{"line":208,"column":72}}]},"35":{"loc":{"start":{"line":218,"column":2},"end":{"line":241,"column":3}},"type":"if","locations":[{"start":{"line":218,"column":2},"end":{"line":241,"column":3}},{"start":{"line":218,"column":2},"end":{"line":241,"column":3}}]},"36":{"loc":{"start":{"line":228,"column":49},"end":{"line":228,"column":134}},"type":"binary-expr","locations":[{"start":{"line":228,"column":49},"end":{"line":228,"column":70}},{"start":{"line":228,"column":75},"end":{"line":228,"column":95}},{"start":{"line":228,"column":99},"end":{"line":228,"column":133}}]},"37":{"loc":{"start":{"line":230,"column":4},"end":{"line":232,"column":5}},"type":"if","locations":[{"start":{"line":230,"column":4},"end":{"line":232,"column":5}},{"start":{"line":230,"column":4},"end":{"line":232,"column":5}}]},"38":{"loc":{"start":{"line":230,"column":8},"end":{"line":230,"column":62}},"type":"binary-expr","locations":[{"start":{"line":230,"column":8},"end":{"line":230,"column":30}},{"start":{"line":230,"column":34},"end":{"line":230,"column":62}}]},"39":{"loc":{"start":{"line":234,"column":4},"end":{"line":238,"column":5}},"type":"if","locations":[{"start":{"line":234,"column":4},"end":{"line":238,"column":5}},{"start":{"line":234,"column":4},"end":{"line":238,"column":5}}]},"40":{"loc":{"start":{"line":236,"column":11},"end":{"line":238,"column":5}},"type":"if","locations":[{"start":{"line":236,"column":11},"end":{"line":238,"column":5}},{"start":{"line":236,"column":11},"end":{"line":238,"column":5}}]},"41":{"loc":{"start":{"line":236,"column":15},"end":{"line":236,"column":101}},"type":"binary-expr","locations":[{"start":{"line":236,"column":15},"end":{"line":236,"column":32}},{"start":{"line":236,"column":36},"end":{"line":236,"column":62}},{"start":{"line":236,"column":66},"end":{"line":236,"column":101}}]},"42":{"loc":{"start":{"line":240,"column":33},"end":{"line":240,"column":80}},"type":"cond-expr","locations":[{"start":{"line":240,"column":42},"end":{"line":240,"column":60}},{"start":{"line":240,"column":63},"end":{"line":240,"column":80}}]},"43":{"loc":{"start":{"line":245,"column":2},"end":{"line":245,"column":74}},"type":"if","locations":[{"start":{"line":245,"column":2},"end":{"line":245,"column":74}},{"start":{"line":245,"column":2},"end":{"line":245,"column":74}}]},"44":{"loc":{"start":{"line":245,"column":6},"end":{"line":245,"column":60}},"type":"binary-expr","locations":[{"start":{"line":245,"column":6},"end":{"line":245,"column":28}},{"start":{"line":245,"column":32},"end":{"line":245,"column":60}}]},"45":{"loc":{"start":{"line":246,"column":9},"end":{"line":246,"column":62}},"type":"binary-expr","locations":[{"start":{"line":246,"column":9},"end":{"line":246,"column":32}},{"start":{"line":246,"column":36},"end":{"line":246,"column":62}}]},"46":{"loc":{"start":{"line":265,"column":2},"end":{"line":267,"column":3}},"type":"if","locations":[{"start":{"line":265,"column":2},"end":{"line":267,"column":3}},{"start":{"line":265,"column":2},"end":{"line":267,"column":3}}]},"47":{"loc":{"start":{"line":265,"column":6},"end":{"line":265,"column":80}},"type":"binary-expr","locations":[{"start":{"line":265,"column":6},"end":{"line":265,"column":45}},{"start":{"line":265,"column":49},"end":{"line":265,"column":80}}]},"48":{"loc":{"start":{"line":269,"column":2},"end":{"line":271,"column":3}},"type":"if","locations":[{"start":{"line":269,"column":2},"end":{"line":271,"column":3}},{"start":{"line":269,"column":2},"end":{"line":271,"column":3}}]},"49":{"loc":{"start":{"line":269,"column":6},"end":{"line":269,"column":60}},"type":"binary-expr","locations":[{"start":{"line":269,"column":6},"end":{"line":269,"column":28}},{"start":{"line":269,"column":32},"end":{"line":269,"column":60}}]},"50":{"loc":{"start":{"line":278,"column":4},"end":{"line":317,"column":5}},"type":"if","locations":[{"start":{"line":278,"column":4},"end":{"line":317,"column":5}},{"start":{"line":278,"column":4},"end":{"line":317,"column":5}}]},"51":{"loc":{"start":{"line":278,"column":8},"end":{"line":278,"column":44}},"type":"binary-expr","locations":[{"start":{"line":278,"column":8},"end":{"line":278,"column":16}},{"start":{"line":278,"column":20},"end":{"line":278,"column":44}}]},"52":{"loc":{"start":{"line":283,"column":11},"end":{"line":317,"column":5}},"type":"if","locations":[{"start":{"line":283,"column":11},"end":{"line":317,"column":5}},{"start":{"line":283,"column":11},"end":{"line":317,"column":5}}]},"53":{"loc":{"start":{"line":289,"column":11},"end":{"line":317,"column":5}},"type":"if","locations":[{"start":{"line":289,"column":11},"end":{"line":317,"column":5}},{"start":{"line":289,"column":11},"end":{"line":317,"column":5}}]},"54":{"loc":{"start":{"line":296,"column":11},"end":{"line":317,"column":5}},"type":"if","locations":[{"start":{"line":296,"column":11},"end":{"line":317,"column":5}},{"start":{"line":296,"column":11},"end":{"line":317,"column":5}}]},"55":{"loc":{"start":{"line":296,"column":15},"end":{"line":296,"column":48}},"type":"binary-expr","locations":[{"start":{"line":296,"column":15},"end":{"line":296,"column":23}},{"start":{"line":296,"column":27},"end":{"line":296,"column":48}}]},"56":{"loc":{"start":{"line":297,"column":26},"end":{"line":297,"column":153}},"type":"binary-expr","locations":[{"start":{"line":297,"column":26},"end":{"line":297,"column":68}},{"start":{"line":297,"column":72},"end":{"line":297,"column":98}},{"start":{"line":297,"column":102},"end":{"line":297,"column":123}},{"start":{"line":297,"column":127},"end":{"line":297,"column":153}}]},"57":{"loc":{"start":{"line":305,"column":6},"end":{"line":309,"column":7}},"type":"if","locations":[{"start":{"line":305,"column":6},"end":{"line":309,"column":7}},{"start":{"line":305,"column":6},"end":{"line":309,"column":7}}]},"58":{"loc":{"start":{"line":305,"column":10},"end":{"line":305,"column":55}},"type":"binary-expr","locations":[{"start":{"line":305,"column":10},"end":{"line":305,"column":23}},{"start":{"line":305,"column":27},"end":{"line":305,"column":55}}]},"59":{"loc":{"start":{"line":310,"column":11},"end":{"line":317,"column":5}},"type":"if","locations":[{"start":{"line":310,"column":11},"end":{"line":317,"column":5}},{"start":{"line":310,"column":11},"end":{"line":317,"column":5}}]},"60":{"loc":{"start":{"line":326,"column":4},"end":{"line":331,"column":5}},"type":"if","locations":[{"start":{"line":326,"column":4},"end":{"line":331,"column":5}},{"start":{"line":326,"column":4},"end":{"line":331,"column":5}}]},"61":{"loc":{"start":{"line":330,"column":6},"end":{"line":330,"column":33}},"type":"if","locations":[{"start":{"line":330,"column":6},"end":{"line":330,"column":33}},{"start":{"line":330,"column":6},"end":{"line":330,"column":33}}]},"62":{"loc":{"start":{"line":334,"column":4},"end":{"line":336,"column":5}},"type":"if","locations":[{"start":{"line":334,"column":4},"end":{"line":336,"column":5}},{"start":{"line":334,"column":4},"end":{"line":336,"column":5}}]},"63":{"loc":{"start":{"line":334,"column":8},"end":{"line":334,"column":49}},"type":"binary-expr","locations":[{"start":{"line":334,"column":8},"end":{"line":334,"column":29}},{"start":{"line":334,"column":33},"end":{"line":334,"column":49}}]},"64":{"loc":{"start":{"line":338,"column":48},"end":{"line":338,"column":93}},"type":"cond-expr","locations":[{"start":{"line":338,"column":69},"end":{"line":338,"column":81}},{"start":{"line":338,"column":84},"end":{"line":338,"column":93}}]},"65":{"loc":{"start":{"line":342,"column":2},"end":{"line":344,"column":3}},"type":"if","locations":[{"start":{"line":342,"column":2},"end":{"line":344,"column":3}},{"start":{"line":342,"column":2},"end":{"line":344,"column":3}}]},"66":{"loc":{"start":{"line":342,"column":6},"end":{"line":342,"column":75}},"type":"binary-expr","locations":[{"start":{"line":342,"column":6},"end":{"line":342,"column":24}},{"start":{"line":342,"column":28},"end":{"line":342,"column":43}},{"start":{"line":342,"column":47},"end":{"line":342,"column":75}}]},"67":{"loc":{"start":{"line":372,"column":2},"end":{"line":503,"column":3}},"type":"switch","locations":[{"start":{"line":373,"column":4},"end":{"line":386,"column":44}},{"start":{"line":388,"column":4},"end":{"line":391,"column":53}},{"start":{"line":393,"column":4},"end":{"line":394,"column":52}},{"start":{"line":396,"column":4},"end":{"line":420,"column":16}},{"start":{"line":422,"column":4},"end":{"line":434,"column":7}},{"start":{"line":436,"column":4},"end":{"line":441,"column":18}},{"start":{"line":443,"column":4},"end":{"line":444,"column":67}},{"start":{"line":446,"column":4},"end":{"line":447,"column":66}},{"start":{"line":449,"column":4},"end":{"line":452,"column":50}},{"start":{"line":454,"column":4},"end":{"line":454,"column":18}},{"start":{"line":454,"column":19},"end":{"line":458,"column":53}},{"start":{"line":460,"column":4},"end":{"line":461,"column":77}},{"start":{"line":463,"column":4},"end":{"line":468,"column":54}},{"start":{"line":470,"column":4},"end":{"line":471,"column":58}},{"start":{"line":473,"column":4},"end":{"line":474,"column":44}},{"start":{"line":476,"column":4},"end":{"line":477,"column":29}},{"start":{"line":479,"column":4},"end":{"line":482,"column":42}},{"start":{"line":484,"column":4},"end":{"line":485,"column":29}},{"start":{"line":487,"column":4},"end":{"line":488,"column":34}},{"start":{"line":490,"column":4},"end":{"line":499,"column":7}},{"start":{"line":501,"column":4},"end":{"line":502,"column":24}}]},"68":{"loc":{"start":{"line":374,"column":6},"end":{"line":376,"column":7}},"type":"if","locations":[{"start":{"line":374,"column":6},"end":{"line":376,"column":7}},{"start":{"line":374,"column":6},"end":{"line":376,"column":7}}]},"69":{"loc":{"start":{"line":374,"column":10},"end":{"line":374,"column":71}},"type":"binary-expr","locations":[{"start":{"line":374,"column":10},"end":{"line":374,"column":30}},{"start":{"line":374,"column":34},"end":{"line":374,"column":71}}]},"70":{"loc":{"start":{"line":380,"column":6},"end":{"line":382,"column":7}},"type":"if","locations":[{"start":{"line":380,"column":6},"end":{"line":382,"column":7}},{"start":{"line":380,"column":6},"end":{"line":382,"column":7}}]},"71":{"loc":{"start":{"line":380,"column":10},"end":{"line":380,"column":83}},"type":"binary-expr","locations":[{"start":{"line":380,"column":10},"end":{"line":380,"column":32}},{"start":{"line":380,"column":36},"end":{"line":380,"column":60}},{"start":{"line":380,"column":64},"end":{"line":380,"column":83}}]},"72":{"loc":{"start":{"line":383,"column":6},"end":{"line":385,"column":7}},"type":"if","locations":[{"start":{"line":383,"column":6},"end":{"line":385,"column":7}},{"start":{"line":383,"column":6},"end":{"line":385,"column":7}}]},"73":{"loc":{"start":{"line":383,"column":10},"end":{"line":383,"column":113}},"type":"binary-expr","locations":[{"start":{"line":383,"column":10},"end":{"line":383,"column":31}},{"start":{"line":383,"column":35},"end":{"line":383,"column":72}},{"start":{"line":383,"column":76},"end":{"line":383,"column":113}}]},"74":{"loc":{"start":{"line":394,"column":6},"end":{"line":394,"column":52}},"type":"if","locations":[{"start":{"line":394,"column":6},"end":{"line":394,"column":52}},{"start":{"line":394,"column":6},"end":{"line":394,"column":52}}]},"75":{"loc":{"start":{"line":398,"column":23},"end":{"line":398,"column":73}},"type":"binary-expr","locations":[{"start":{"line":398,"column":23},"end":{"line":398,"column":51}},{"start":{"line":398,"column":55},"end":{"line":398,"column":73}}]},"76":{"loc":{"start":{"line":400,"column":36},"end":{"line":400,"column":60}},"type":"binary-expr","locations":[{"start":{"line":400,"column":36},"end":{"line":400,"column":46}},{"start":{"line":400,"column":50},"end":{"line":400,"column":60}}]},"77":{"loc":{"start":{"line":402,"column":6},"end":{"line":414,"column":7}},"type":"if","locations":[{"start":{"line":402,"column":6},"end":{"line":414,"column":7}},{"start":{"line":402,"column":6},"end":{"line":414,"column":7}}]},"78":{"loc":{"start":{"line":403,"column":8},"end":{"line":405,"column":9}},"type":"if","locations":[{"start":{"line":403,"column":8},"end":{"line":405,"column":9}},{"start":{"line":403,"column":8},"end":{"line":405,"column":9}}]},"79":{"loc":{"start":{"line":403,"column":12},"end":{"line":403,"column":47}},"type":"binary-expr","locations":[{"start":{"line":403,"column":12},"end":{"line":403,"column":30}},{"start":{"line":403,"column":34},"end":{"line":403,"column":47}}]},"80":{"loc":{"start":{"line":406,"column":13},"end":{"line":414,"column":7}},"type":"if","locations":[{"start":{"line":406,"column":13},"end":{"line":414,"column":7}},{"start":{"line":406,"column":13},"end":{"line":414,"column":7}}]},"81":{"loc":{"start":{"line":406,"column":17},"end":{"line":406,"column":94}},"type":"binary-expr","locations":[{"start":{"line":406,"column":17},"end":{"line":406,"column":36}},{"start":{"line":406,"column":40},"end":{"line":406,"column":64}},{"start":{"line":406,"column":68},"end":{"line":406,"column":94}}]},"82":{"loc":{"start":{"line":409,"column":13},"end":{"line":414,"column":7}},"type":"if","locations":[{"start":{"line":409,"column":13},"end":{"line":414,"column":7}},{"start":{"line":409,"column":13},"end":{"line":414,"column":7}}]},"83":{"loc":{"start":{"line":409,"column":17},"end":{"line":409,"column":73}},"type":"binary-expr","locations":[{"start":{"line":409,"column":17},"end":{"line":409,"column":27}},{"start":{"line":409,"column":31},"end":{"line":409,"column":50}},{"start":{"line":409,"column":54},"end":{"line":409,"column":73}}]},"84":{"loc":{"start":{"line":416,"column":6},"end":{"line":418,"column":7}},"type":"if","locations":[{"start":{"line":416,"column":6},"end":{"line":418,"column":7}},{"start":{"line":416,"column":6},"end":{"line":418,"column":7}}]},"85":{"loc":{"start":{"line":416,"column":10},"end":{"line":416,"column":72}},"type":"binary-expr","locations":[{"start":{"line":416,"column":10},"end":{"line":416,"column":20}},{"start":{"line":416,"column":24},"end":{"line":416,"column":50}},{"start":{"line":416,"column":54},"end":{"line":416,"column":72}}]},"86":{"loc":{"start":{"line":423,"column":6},"end":{"line":434,"column":7}},"type":"if","locations":[{"start":{"line":423,"column":6},"end":{"line":434,"column":7}},{"start":{"line":423,"column":6},"end":{"line":434,"column":7}}]},"87":{"loc":{"start":{"line":495,"column":6},"end":{"line":499,"column":7}},"type":"if","locations":[{"start":{"line":495,"column":6},"end":{"line":499,"column":7}},{"start":{"line":495,"column":6},"end":{"line":499,"column":7}}]},"88":{"loc":{"start":{"line":509,"column":2},"end":{"line":513,"column":3}},"type":"if","locations":[{"start":{"line":509,"column":2},"end":{"line":513,"column":3}},{"start":{"line":509,"column":2},"end":{"line":513,"column":3}}]},"89":{"loc":{"start":{"line":509,"column":6},"end":{"line":509,"column":82}},"type":"binary-expr","locations":[{"start":{"line":509,"column":6},"end":{"line":509,"column":28}},{"start":{"line":509,"column":32},"end":{"line":509,"column":48}},{"start":{"line":509,"column":52},"end":{"line":509,"column":82}}]},"90":{"loc":{"start":{"line":520,"column":2},"end":{"line":522,"column":3}},"type":"if","locations":[{"start":{"line":520,"column":2},"end":{"line":522,"column":3}},{"start":{"line":520,"column":2},"end":{"line":522,"column":3}}]},"91":{"loc":{"start":{"line":544,"column":13},"end":{"line":544,"column":41}},"type":"binary-expr","locations":[{"start":{"line":544,"column":13},"end":{"line":544,"column":21}},{"start":{"line":544,"column":25},"end":{"line":544,"column":41}}]},"92":{"loc":{"start":{"line":545,"column":13},"end":{"line":545,"column":44}},"type":"binary-expr","locations":[{"start":{"line":545,"column":13},"end":{"line":545,"column":21}},{"start":{"line":545,"column":25},"end":{"line":545,"column":44}}]},"93":{"loc":{"start":{"line":555,"column":4},"end":{"line":563,"column":5}},"type":"if","locations":[{"start":{"line":555,"column":4},"end":{"line":563,"column":5}},{"start":{"line":555,"column":4},"end":{"line":563,"column":5}}]},"94":{"loc":{"start":{"line":558,"column":28},"end":{"line":558,"column":58}},"type":"binary-expr","locations":[{"start":{"line":558,"column":28},"end":{"line":558,"column":50}},{"start":{"line":558,"column":54},"end":{"line":558,"column":58}}]},"95":{"loc":{"start":{"line":559,"column":6},"end":{"line":562,"column":7}},"type":"if","locations":[{"start":{"line":559,"column":6},"end":{"line":562,"column":7}},{"start":{"line":559,"column":6},"end":{"line":562,"column":7}}]},"96":{"loc":{"start":{"line":565,"column":4},"end":{"line":572,"column":5}},"type":"if","locations":[{"start":{"line":565,"column":4},"end":{"line":572,"column":5}},{"start":{"line":565,"column":4},"end":{"line":572,"column":5}}]},"97":{"loc":{"start":{"line":580,"column":2},"end":{"line":586,"column":3}},"type":"if","locations":[{"start":{"line":580,"column":2},"end":{"line":586,"column":3}},{"start":{"line":580,"column":2},"end":{"line":586,"column":3}}]},"98":{"loc":{"start":{"line":580,"column":6},"end":{"line":580,"column":90}},"type":"binary-expr","locations":[{"start":{"line":580,"column":6},"end":{"line":580,"column":16}},{"start":{"line":580,"column":20},"end":{"line":580,"column":46}},{"start":{"line":580,"column":51},"end":{"line":580,"column":89}}]},"99":{"loc":{"start":{"line":582,"column":6},"end":{"line":582,"column":92}},"type":"if","locations":[{"start":{"line":582,"column":6},"end":{"line":582,"column":92}},{"start":{"line":582,"column":6},"end":{"line":582,"column":92}}]},"100":{"loc":{"start":{"line":582,"column":10},"end":{"line":582,"column":50}},"type":"binary-expr","locations":[{"start":{"line":582,"column":10},"end":{"line":582,"column":21}},{"start":{"line":582,"column":25},"end":{"line":582,"column":50}}]},"101":{"loc":{"start":{"line":588,"column":2},"end":{"line":594,"column":3}},"type":"if","locations":[{"start":{"line":588,"column":2},"end":{"line":594,"column":3}},{"start":{"line":588,"column":2},"end":{"line":594,"column":3}}]},"102":{"loc":{"start":{"line":589,"column":4},"end":{"line":593,"column":5}},"type":"if","locations":[{"start":{"line":589,"column":4},"end":{"line":593,"column":5}},{"start":{"line":589,"column":4},"end":{"line":593,"column":5}}]},"103":{"loc":{"start":{"line":595,"column":2},"end":{"line":595,"column":62}},"type":"if","locations":[{"start":{"line":595,"column":2},"end":{"line":595,"column":62}},{"start":{"line":595,"column":2},"end":{"line":595,"column":62}}]},"104":{"loc":{"start":{"line":596,"column":2},"end":{"line":596,"column":48}},"type":"if","locations":[{"start":{"line":596,"column":2},"end":{"line":596,"column":48}},{"start":{"line":596,"column":2},"end":{"line":596,"column":48}}]},"105":{"loc":{"start":{"line":597,"column":2},"end":{"line":597,"column":82}},"type":"if","locations":[{"start":{"line":597,"column":2},"end":{"line":597,"column":82}},{"start":{"line":597,"column":2},"end":{"line":597,"column":82}}]},"106":{"loc":{"start":{"line":598,"column":2},"end":{"line":598,"column":70}},"type":"if","locations":[{"start":{"line":598,"column":2},"end":{"line":598,"column":70}},{"start":{"line":598,"column":2},"end":{"line":598,"column":70}}]},"107":{"loc":{"start":{"line":600,"column":2},"end":{"line":607,"column":3}},"type":"if","locations":[{"start":{"line":600,"column":2},"end":{"line":607,"column":3}},{"start":{"line":600,"column":2},"end":{"line":607,"column":3}}]},"108":{"loc":{"start":{"line":617,"column":2},"end":{"line":619,"column":3}},"type":"if","locations":[{"start":{"line":617,"column":2},"end":{"line":619,"column":3}},{"start":{"line":617,"column":2},"end":{"line":619,"column":3}}]},"109":{"loc":{"start":{"line":634,"column":2},"end":{"line":636,"column":3}},"type":"if","locations":[{"start":{"line":634,"column":2},"end":{"line":636,"column":3}},{"start":{"line":634,"column":2},"end":{"line":636,"column":3}}]},"110":{"loc":{"start":{"line":640,"column":2},"end":{"line":645,"column":3}},"type":"if","locations":[{"start":{"line":640,"column":2},"end":{"line":645,"column":3}},{"start":{"line":640,"column":2},"end":{"line":645,"column":3}}]},"111":{"loc":{"start":{"line":691,"column":4},"end":{"line":696,"column":5}},"type":"if","locations":[{"start":{"line":691,"column":4},"end":{"line":696,"column":5}},{"start":{"line":691,"column":4},"end":{"line":696,"column":5}}]},"112":{"loc":{"start":{"line":695,"column":6},"end":{"line":695,"column":37}},"type":"if","locations":[{"start":{"line":695,"column":6},"end":{"line":695,"column":37}},{"start":{"line":695,"column":6},"end":{"line":695,"column":37}}]},"113":{"loc":{"start":{"line":703,"column":4},"end":{"line":706,"column":5}},"type":"if","locations":[{"start":{"line":703,"column":4},"end":{"line":706,"column":5}},{"start":{"line":703,"column":4},"end":{"line":706,"column":5}}]},"114":{"loc":{"start":{"line":708,"column":4},"end":{"line":713,"column":5}},"type":"if","locations":[{"start":{"line":708,"column":4},"end":{"line":713,"column":5}},{"start":{"line":708,"column":4},"end":{"line":713,"column":5}}]},"115":{"loc":{"start":{"line":708,"column":8},"end":{"line":708,"column":69}},"type":"binary-expr","locations":[{"start":{"line":708,"column":8},"end":{"line":708,"column":42}},{"start":{"line":708,"column":46},"end":{"line":708,"column":69}}]},"116":{"loc":{"start":{"line":710,"column":18},"end":{"line":710,"column":63}},"type":"cond-expr","locations":[{"start":{"line":710,"column":30},"end":{"line":710,"column":44}},{"start":{"line":710,"column":47},"end":{"line":710,"column":63}}]},"117":{"loc":{"start":{"line":718,"column":4},"end":{"line":721,"column":5}},"type":"if","locations":[{"start":{"line":718,"column":4},"end":{"line":721,"column":5}},{"start":{"line":718,"column":4},"end":{"line":721,"column":5}}]},"118":{"loc":{"start":{"line":718,"column":8},"end":{"line":718,"column":43}},"type":"binary-expr","locations":[{"start":{"line":718,"column":8},"end":{"line":718,"column":17}},{"start":{"line":718,"column":21},"end":{"line":718,"column":43}}]},"119":{"loc":{"start":{"line":723,"column":4},"end":{"line":725,"column":5}},"type":"if","locations":[{"start":{"line":723,"column":4},"end":{"line":725,"column":5}},{"start":{"line":723,"column":4},"end":{"line":725,"column":5}}]},"120":{"loc":{"start":{"line":727,"column":4},"end":{"line":740,"column":5}},"type":"if","locations":[{"start":{"line":727,"column":4},"end":{"line":740,"column":5}},{"start":{"line":727,"column":4},"end":{"line":740,"column":5}}]},"121":{"loc":{"start":{"line":727,"column":8},"end":{"line":727,"column":48}},"type":"binary-expr","locations":[{"start":{"line":727,"column":8},"end":{"line":727,"column":18}},{"start":{"line":727,"column":22},"end":{"line":727,"column":48}}]},"122":{"loc":{"start":{"line":728,"column":6},"end":{"line":728,"column":41}},"type":"if","locations":[{"start":{"line":728,"column":6},"end":{"line":728,"column":41}},{"start":{"line":728,"column":6},"end":{"line":728,"column":41}}]},"123":{"loc":{"start":{"line":731,"column":6},"end":{"line":737,"column":7}},"type":"if","locations":[{"start":{"line":731,"column":6},"end":{"line":737,"column":7}},{"start":{"line":731,"column":6},"end":{"line":737,"column":7}}]},"124":{"loc":{"start":{"line":731,"column":10},"end":{"line":731,"column":80}},"type":"binary-expr","locations":[{"start":{"line":731,"column":10},"end":{"line":731,"column":30}},{"start":{"line":731,"column":34},"end":{"line":731,"column":55}},{"start":{"line":731,"column":59},"end":{"line":731,"column":80}}]},"125":{"loc":{"start":{"line":735,"column":8},"end":{"line":735,"column":79}},"type":"if","locations":[{"start":{"line":735,"column":8},"end":{"line":735,"column":79}},{"start":{"line":735,"column":8},"end":{"line":735,"column":79}}]},"126":{"loc":{"start":{"line":745,"column":4},"end":{"line":747,"column":5}},"type":"if","locations":[{"start":{"line":745,"column":4},"end":{"line":747,"column":5}},{"start":{"line":745,"column":4},"end":{"line":747,"column":5}}]},"127":{"loc":{"start":{"line":752,"column":2},"end":{"line":754,"column":3}},"type":"if","locations":[{"start":{"line":752,"column":2},"end":{"line":754,"column":3}},{"start":{"line":752,"column":2},"end":{"line":754,"column":3}}]},"128":{"loc":{"start":{"line":756,"column":31},"end":{"line":756,"column":79}},"type":"cond-expr","locations":[{"start":{"line":756,"column":43},"end":{"line":756,"column":58}},{"start":{"line":756,"column":61},"end":{"line":756,"column":79}}]},"129":{"loc":{"start":{"line":760,"column":2},"end":{"line":766,"column":3}},"type":"if","locations":[{"start":{"line":760,"column":2},"end":{"line":766,"column":3}},{"start":{"line":760,"column":2},"end":{"line":766,"column":3}}]},"130":{"loc":{"start":{"line":760,"column":6},"end":{"line":760,"column":53}},"type":"binary-expr","locations":[{"start":{"line":760,"column":6},"end":{"line":760,"column":13}},{"start":{"line":760,"column":17},"end":{"line":760,"column":28}},{"start":{"line":760,"column":32},"end":{"line":760,"column":53}}]},"131":{"loc":{"start":{"line":761,"column":4},"end":{"line":761,"column":37}},"type":"if","locations":[{"start":{"line":761,"column":4},"end":{"line":761,"column":37}},{"start":{"line":761,"column":4},"end":{"line":761,"column":37}}]},"132":{"loc":{"start":{"line":768,"column":2},"end":{"line":771,"column":3}},"type":"if","locations":[{"start":{"line":768,"column":2},"end":{"line":771,"column":3}},{"start":{"line":768,"column":2},"end":{"line":771,"column":3}}]},"133":{"loc":{"start":{"line":769,"column":17},"end":{"line":769,"column":145}},"type":"cond-expr","locations":[{"start":{"line":769,"column":29},"end":{"line":769,"column":90}},{"start":{"line":769,"column":93},"end":{"line":769,"column":145}}]},"134":{"loc":{"start":{"line":773,"column":2},"end":{"line":788,"column":3}},"type":"if","locations":[{"start":{"line":773,"column":2},"end":{"line":788,"column":3}},{"start":{"line":773,"column":2},"end":{"line":788,"column":3}}]},"135":{"loc":{"start":{"line":773,"column":6},"end":{"line":773,"column":163}},"type":"binary-expr","locations":[{"start":{"line":773,"column":6},"end":{"line":773,"column":20}},{"start":{"line":773,"column":24},"end":{"line":773,"column":54}},{"start":{"line":773,"column":59},"end":{"line":773,"column":82}},{"start":{"line":773,"column":86},"end":{"line":773,"column":109}},{"start":{"line":773,"column":115},"end":{"line":773,"column":136}},{"start":{"line":773,"column":140},"end":{"line":773,"column":162}}]},"136":{"loc":{"start":{"line":774,"column":4},"end":{"line":774,"column":63}},"type":"if","locations":[{"start":{"line":774,"column":4},"end":{"line":774,"column":63}},{"start":{"line":774,"column":4},"end":{"line":774,"column":63}}]},"137":{"loc":{"start":{"line":774,"column":8},"end":{"line":774,"column":43}},"type":"binary-expr","locations":[{"start":{"line":774,"column":8},"end":{"line":774,"column":19}},{"start":{"line":774,"column":23},"end":{"line":774,"column":30}},{"start":{"line":774,"column":34},"end":{"line":774,"column":43}}]},"138":{"loc":{"start":{"line":778,"column":21},"end":{"line":778,"column":48}},"type":"cond-expr","locations":[{"start":{"line":778,"column":43},"end":{"line":778,"column":44}},{"start":{"line":778,"column":47},"end":{"line":778,"column":48}}]},"139":{"loc":{"start":{"line":779,"column":4},"end":{"line":786,"column":5}},"type":"if","locations":[{"start":{"line":779,"column":4},"end":{"line":786,"column":5}},{"start":{"line":779,"column":4},"end":{"line":786,"column":5}}]},"140":{"loc":{"start":{"line":781,"column":6},"end":{"line":785,"column":7}},"type":"if","locations":[{"start":{"line":781,"column":6},"end":{"line":785,"column":7}},{"start":{"line":781,"column":6},"end":{"line":785,"column":7}}]},"141":{"loc":{"start":{"line":790,"column":2},"end":{"line":810,"column":3}},"type":"if","locations":[{"start":{"line":790,"column":2},"end":{"line":810,"column":3}},{"start":{"line":790,"column":2},"end":{"line":810,"column":3}}]},"142":{"loc":{"start":{"line":790,"column":6},"end":{"line":790,"column":54}},"type":"binary-expr","locations":[{"start":{"line":790,"column":6},"end":{"line":790,"column":20}},{"start":{"line":790,"column":24},"end":{"line":790,"column":54}}]},"143":{"loc":{"start":{"line":791,"column":4},"end":{"line":807,"column":5}},"type":"if","locations":[{"start":{"line":791,"column":4},"end":{"line":807,"column":5}},{"start":{"line":791,"column":4},"end":{"line":807,"column":5}}]},"144":{"loc":{"start":{"line":793,"column":6},"end":{"line":795,"column":7}},"type":"if","locations":[{"start":{"line":793,"column":6},"end":{"line":795,"column":7}},{"start":{"line":793,"column":6},"end":{"line":795,"column":7}}]},"145":{"loc":{"start":{"line":793,"column":10},"end":{"line":793,"column":46}},"type":"binary-expr","locations":[{"start":{"line":793,"column":10},"end":{"line":793,"column":25}},{"start":{"line":793,"column":29},"end":{"line":793,"column":46}}]},"146":{"loc":{"start":{"line":794,"column":25},"end":{"line":794,"column":103}},"type":"binary-expr","locations":[{"start":{"line":794,"column":25},"end":{"line":794,"column":64}},{"start":{"line":794,"column":68},"end":{"line":794,"column":103}}]},"147":{"loc":{"start":{"line":796,"column":6},"end":{"line":798,"column":7}},"type":"if","locations":[{"start":{"line":796,"column":6},"end":{"line":798,"column":7}},{"start":{"line":796,"column":6},"end":{"line":798,"column":7}}]},"148":{"loc":{"start":{"line":800,"column":11},"end":{"line":807,"column":5}},"type":"if","locations":[{"start":{"line":800,"column":11},"end":{"line":807,"column":5}},{"start":{"line":800,"column":11},"end":{"line":807,"column":5}}]},"149":{"loc":{"start":{"line":800,"column":15},"end":{"line":800,"column":58}},"type":"binary-expr","locations":[{"start":{"line":800,"column":15},"end":{"line":800,"column":32}},{"start":{"line":800,"column":36},"end":{"line":800,"column":58}}]},"150":{"loc":{"start":{"line":801,"column":6},"end":{"line":803,"column":7}},"type":"if","locations":[{"start":{"line":801,"column":6},"end":{"line":803,"column":7}},{"start":{"line":801,"column":6},"end":{"line":803,"column":7}}]},"151":{"loc":{"start":{"line":816,"column":2},"end":{"line":824,"column":3}},"type":"if","locations":[{"start":{"line":816,"column":2},"end":{"line":824,"column":3}},{"start":{"line":816,"column":2},"end":{"line":824,"column":3}}]},"152":{"loc":{"start":{"line":823,"column":22},"end":{"line":823,"column":119}},"type":"cond-expr","locations":[{"start":{"line":823,"column":70},"end":{"line":823,"column":90}},{"start":{"line":823,"column":93},"end":{"line":823,"column":119}}]},"153":{"loc":{"start":{"line":823,"column":23},"end":{"line":823,"column":66}},"type":"binary-expr","locations":[{"start":{"line":823,"column":23},"end":{"line":823,"column":41}},{"start":{"line":823,"column":45},"end":{"line":823,"column":66}}]},"154":{"loc":{"start":{"line":840,"column":24},"end":{"line":840,"column":41}},"type":"binary-expr","locations":[{"start":{"line":840,"column":24},"end":{"line":840,"column":33}},{"start":{"line":840,"column":37},"end":{"line":840,"column":41}}]},"155":{"loc":{"start":{"line":862,"column":21},"end":{"line":862,"column":62}},"type":"binary-expr","locations":[{"start":{"line":862,"column":21},"end":{"line":862,"column":36}},{"start":{"line":862,"column":40},"end":{"line":862,"column":62}}]},"156":{"loc":{"start":{"line":866,"column":2},"end":{"line":877,"column":3}},"type":"if","locations":[{"start":{"line":866,"column":2},"end":{"line":877,"column":3}},{"start":{"line":866,"column":2},"end":{"line":877,"column":3}}]},"157":{"loc":{"start":{"line":888,"column":2},"end":{"line":888,"column":40}},"type":"if","locations":[{"start":{"line":888,"column":2},"end":{"line":888,"column":40}},{"start":{"line":888,"column":2},"end":{"line":888,"column":40}}]},"158":{"loc":{"start":{"line":891,"column":2},"end":{"line":900,"column":3}},"type":"if","locations":[{"start":{"line":891,"column":2},"end":{"line":900,"column":3}},{"start":{"line":891,"column":2},"end":{"line":900,"column":3}}]},"159":{"loc":{"start":{"line":891,"column":6},"end":{"line":891,"column":50}},"type":"binary-expr","locations":[{"start":{"line":891,"column":6},"end":{"line":891,"column":19}},{"start":{"line":891,"column":23},"end":{"line":891,"column":50}}]},"160":{"loc":{"start":{"line":893,"column":6},"end":{"line":898,"column":7}},"type":"if","locations":[{"start":{"line":893,"column":6},"end":{"line":898,"column":7}},{"start":{"line":893,"column":6},"end":{"line":898,"column":7}}]},"161":{"loc":{"start":{"line":903,"column":2},"end":{"line":905,"column":3}},"type":"if","locations":[{"start":{"line":903,"column":2},"end":{"line":905,"column":3}},{"start":{"line":903,"column":2},"end":{"line":905,"column":3}}]},"162":{"loc":{"start":{"line":903,"column":6},"end":{"line":903,"column":86}},"type":"binary-expr","locations":[{"start":{"line":903,"column":6},"end":{"line":903,"column":14}},{"start":{"line":903,"column":18},"end":{"line":903,"column":25}},{"start":{"line":903,"column":29},"end":{"line":903,"column":58}},{"start":{"line":903,"column":62},"end":{"line":903,"column":86}}]},"163":{"loc":{"start":{"line":907,"column":2},"end":{"line":918,"column":3}},"type":"if","locations":[{"start":{"line":907,"column":2},"end":{"line":918,"column":3}},{"start":{"line":907,"column":2},"end":{"line":918,"column":3}}]},"164":{"loc":{"start":{"line":910,"column":4},"end":{"line":910,"column":50}},"type":"if","locations":[{"start":{"line":910,"column":4},"end":{"line":910,"column":50}},{"start":{"line":910,"column":4},"end":{"line":910,"column":50}}]},"165":{"loc":{"start":{"line":911,"column":4},"end":{"line":913,"column":5}},"type":"if","locations":[{"start":{"line":911,"column":4},"end":{"line":913,"column":5}},{"start":{"line":911,"column":4},"end":{"line":913,"column":5}}]},"166":{"loc":{"start":{"line":930,"column":4},"end":{"line":935,"column":5}},"type":"if","locations":[{"start":{"line":930,"column":4},"end":{"line":935,"column":5}},{"start":{"line":930,"column":4},"end":{"line":935,"column":5}}]},"167":{"loc":{"start":{"line":934,"column":6},"end":{"line":934,"column":33}},"type":"if","locations":[{"start":{"line":934,"column":6},"end":{"line":934,"column":33}},{"start":{"line":934,"column":6},"end":{"line":934,"column":33}}]},"168":{"loc":{"start":{"line":944,"column":2},"end":{"line":950,"column":3}},"type":"if","locations":[{"start":{"line":944,"column":2},"end":{"line":950,"column":3}},{"start":{"line":944,"column":2},"end":{"line":950,"column":3}}]},"169":{"loc":{"start":{"line":944,"column":6},"end":{"line":944,"column":40}},"type":"binary-expr","locations":[{"start":{"line":944,"column":6},"end":{"line":944,"column":16}},{"start":{"line":944,"column":20},"end":{"line":944,"column":40}}]},"170":{"loc":{"start":{"line":946,"column":9},"end":{"line":950,"column":3}},"type":"if","locations":[{"start":{"line":946,"column":9},"end":{"line":950,"column":3}},{"start":{"line":946,"column":9},"end":{"line":950,"column":3}}]},"171":{"loc":{"start":{"line":961,"column":2},"end":{"line":971,"column":3}},"type":"if","locations":[{"start":{"line":961,"column":2},"end":{"line":971,"column":3}},{"start":{"line":961,"column":2},"end":{"line":971,"column":3}}]},"172":{"loc":{"start":{"line":962,"column":4},"end":{"line":964,"column":5}},"type":"if","locations":[{"start":{"line":962,"column":4},"end":{"line":964,"column":5}},{"start":{"line":962,"column":4},"end":{"line":964,"column":5}}]},"173":{"loc":{"start":{"line":962,"column":8},"end":{"line":962,"column":79}},"type":"binary-expr","locations":[{"start":{"line":962,"column":8},"end":{"line":962,"column":16}},{"start":{"line":962,"column":20},"end":{"line":962,"column":37}},{"start":{"line":962,"column":41},"end":{"line":962,"column":79}}]},"174":{"loc":{"start":{"line":967,"column":9},"end":{"line":971,"column":3}},"type":"if","locations":[{"start":{"line":967,"column":9},"end":{"line":971,"column":3}},{"start":{"line":967,"column":9},"end":{"line":971,"column":3}}]},"175":{"loc":{"start":{"line":967,"column":13},"end":{"line":967,"column":47}},"type":"binary-expr","locations":[{"start":{"line":967,"column":13},"end":{"line":967,"column":20}},{"start":{"line":967,"column":24},"end":{"line":967,"column":47}}]},"176":{"loc":{"start":{"line":973,"column":2},"end":{"line":975,"column":3}},"type":"if","locations":[{"start":{"line":973,"column":2},"end":{"line":975,"column":3}},{"start":{"line":973,"column":2},"end":{"line":975,"column":3}}]},"177":{"loc":{"start":{"line":973,"column":6},"end":{"line":973,"column":61}},"type":"binary-expr","locations":[{"start":{"line":973,"column":6},"end":{"line":973,"column":14}},{"start":{"line":973,"column":18},"end":{"line":973,"column":39}},{"start":{"line":973,"column":43},"end":{"line":973,"column":61}}]},"178":{"loc":{"start":{"line":986,"column":2},"end":{"line":988,"column":3}},"type":"if","locations":[{"start":{"line":986,"column":2},"end":{"line":988,"column":3}},{"start":{"line":986,"column":2},"end":{"line":988,"column":3}}]},"179":{"loc":{"start":{"line":989,"column":2},"end":{"line":991,"column":3}},"type":"if","locations":[{"start":{"line":989,"column":2},"end":{"line":991,"column":3}},{"start":{"line":989,"column":2},"end":{"line":991,"column":3}}]},"180":{"loc":{"start":{"line":1001,"column":2},"end":{"line":1007,"column":3}},"type":"if","locations":[{"start":{"line":1001,"column":2},"end":{"line":1007,"column":3}},{"start":{"line":1001,"column":2},"end":{"line":1007,"column":3}}]},"181":{"loc":{"start":{"line":1001,"column":6},"end":{"line":1001,"column":111}},"type":"binary-expr","locations":[{"start":{"line":1001,"column":6},"end":{"line":1001,"column":25}},{"start":{"line":1001,"column":29},"end":{"line":1001,"column":54}},{"start":{"line":1001,"column":59},"end":{"line":1001,"column":79}},{"start":{"line":1001,"column":83},"end":{"line":1001,"column":110}}]}},"s":{"1":1,"2":1,"3":263,"4":15,"5":248,"6":248,"7":235,"8":235,"9":13,"10":13,"11":0,"12":248,"13":0,"14":0,"15":0,"16":1,"17":1793,"18":1793,"19":1793,"20":1487,"21":8,"22":8,"23":8,"24":10,"25":7,"26":7,"27":1479,"28":1,"29":3228,"30":34,"31":3194,"32":634,"33":2560,"34":2560,"35":3194,"36":3194,"37":3194,"38":1593,"39":3194,"40":2780,"41":520,"42":2780,"43":209,"44":209,"45":209,"46":190,"47":190,"48":178,"49":2,"50":1,"51":1,"52":1,"53":2,"54":2,"55":176,"56":176,"57":169,"58":2571,"59":3,"60":2568,"61":1,"62":3194,"63":3194,"64":3194,"65":2787,"66":53,"67":2734,"68":1,"69":2734,"70":21,"71":21,"72":21,"73":17,"74":17,"75":9,"76":2713,"77":1,"78":3194,"79":3194,"80":3194,"81":2800,"82":53,"83":2747,"84":1,"85":3205,"86":3205,"87":277,"88":242,"89":242,"90":242,"91":242,"92":3,"93":239,"94":239,"95":238,"96":238,"97":238,"98":229,"99":229,"100":2963,"101":1,"102":3503,"103":61,"104":61,"105":61,"106":61,"107":61,"108":61,"109":61,"110":58,"111":58,"112":0,"113":58,"114":28,"115":30,"116":2,"117":44,"118":3442,"119":3442,"120":3442,"121":3065,"122":53,"123":3012,"124":39,"125":39,"126":39,"127":39,"128":39,"129":27,"130":27,"131":3000,"132":1,"133":3466,"134":3466,"135":3466,"136":3466,"137":3100,"138":101,"139":2999,"140":53,"141":2946,"142":1,"143":2981,"144":3246,"145":0,"146":0,"147":0,"148":0,"149":3246,"150":74,"151":74,"152":74,"153":72,"154":72,"155":3172,"156":18,"157":18,"158":18,"159":18,"160":18,"161":18,"162":3154,"163":186,"164":186,"165":186,"166":186,"167":186,"168":177,"169":177,"170":11,"171":166,"172":2968,"173":9,"174":9,"175":9,"176":9,"177":2959,"178":1,"179":186,"180":186,"181":186,"182":124,"183":98,"184":26,"185":26,"186":1,"187":123,"188":6,"189":123,"190":178,"191":1,"192":177,"193":1,"194":9,"195":1,"196":11,"197":11,"198":1,"199":35,"200":35,"201":35,"202":1,"203":3533,"204":3533,"205":9,"206":2,"207":7,"208":7,"209":7,"210":0,"211":7,"212":0,"213":7,"214":8,"215":8,"216":8,"217":18,"218":0,"219":1501,"220":1501,"221":1501,"222":1501,"223":1498,"224":14,"225":11,"226":1484,"227":2,"228":2,"229":1482,"230":1,"231":1,"232":1,"233":1484,"234":24,"235":1460,"236":0,"237":0,"238":0,"239":0,"240":0,"241":0,"242":0,"243":0,"244":0,"245":0,"246":0,"247":26,"248":26,"249":26,"250":26,"251":26,"252":550,"253":346,"254":11,"255":11,"256":11,"257":76,"258":76,"259":76,"260":76,"261":383,"262":114,"263":114,"264":114,"265":109,"266":109,"267":223,"268":122,"269":2,"270":19,"271":19,"272":19,"273":45,"274":24,"275":0,"276":0,"277":0,"278":0,"279":0,"280":0,"281":0,"282":76,"283":1,"284":122,"285":122,"286":122,"287":1,"288":121,"289":1,"290":11,"291":11,"292":10,"293":2,"294":8,"295":1,"296":944,"297":944,"298":944,"299":944,"300":944,"301":944,"302":1,"303":129,"304":125,"305":124,"306":124,"307":1,"308":383,"309":383,"310":383,"311":383,"312":383,"313":383,"314":383,"315":383,"316":383,"317":383,"318":421,"319":366,"320":55,"321":53,"322":3,"323":3,"324":416,"325":22,"326":22,"327":22,"328":22,"329":18,"330":394,"331":286,"332":286,"333":286,"334":284,"335":284,"336":111,"337":140,"338":7,"339":104,"340":171,"341":7,"342":0,"343":7,"344":164,"345":1,"346":163,"347":3,"348":160,"349":0,"350":160,"351":1,"352":159,"353":4,"354":4,"355":4,"356":4,"357":155,"358":159,"359":159,"360":159,"361":1,"362":178,"363":111,"364":1,"365":538,"366":1,"367":45,"368":45,"369":45,"370":10,"371":35,"372":35,"373":20,"374":17,"375":15,"376":32,"377":1,"378":41,"379":41,"380":41,"381":41,"382":41,"383":1,"384":33,"385":33,"386":29,"387":29,"388":29,"389":29,"390":17,"391":17,"392":17,"393":12,"394":24,"395":24,"396":1,"397":284,"398":284,"399":284,"400":284,"401":284,"402":284,"403":280,"404":325,"405":254,"406":71,"407":61,"408":3,"409":312,"410":2,"411":312,"412":312,"413":312,"414":312,"415":2,"416":2,"417":312,"418":8,"419":8,"420":8,"421":8,"422":304,"423":304,"424":304,"425":295,"426":295,"427":304,"428":233,"429":304,"430":8,"431":0,"432":8,"433":8,"434":3,"435":5,"436":5,"437":1,"438":5,"439":296,"440":295,"441":263,"442":263,"443":81,"444":263,"445":229,"446":0,"447":229,"448":1,"449":295,"450":46,"451":0,"452":46,"453":46,"454":46,"455":37,"456":249,"457":120,"458":113,"459":129,"460":43,"461":0,"462":43,"463":43,"464":40,"465":32,"466":32,"467":0,"468":0,"469":0,"470":0,"471":32,"472":86,"473":81,"474":44,"475":44,"476":8,"477":44,"478":0,"479":44,"480":37,"481":11,"482":11,"483":11,"484":26,"485":81,"486":81,"487":5,"488":1,"489":544,"490":31,"491":31,"492":30,"493":26,"494":513,"495":513,"496":1,"497":762,"498":762,"499":762,"500":762,"501":1,"502":190,"503":190,"504":190,"505":190,"506":182,"507":178,"508":178,"509":168,"510":168,"511":1,"512":140,"513":140,"514":131,"515":112,"516":1,"517":696,"518":696,"519":696,"520":696,"521":89,"522":89,"523":607,"524":607,"525":607,"526":607,"527":607,"528":607,"529":607,"530":474,"531":474,"532":474,"533":474,"534":563,"535":563,"536":563,"537":563,"538":563,"539":131,"540":563,"541":52,"542":52,"543":50,"544":50,"545":50,"546":50,"547":563,"548":2,"549":561,"550":361,"551":361,"552":361,"553":48,"554":361,"555":105,"556":337,"557":261,"558":276,"559":1,"560":134,"561":134,"562":134,"563":158,"564":106,"565":52,"566":52,"567":8,"568":150,"569":126,"570":1,"571":273,"572":12,"573":261,"574":35,"575":226,"576":257,"577":1,"578":4037,"579":4037,"580":3760,"581":3,"582":3757,"583":277,"584":243,"585":34,"586":4000,"587":0,"588":4000,"589":4000,"590":3998,"591":1,"592":11,"593":0,"594":11,"595":1,"596":10,"597":10,"598":1,"599":34,"600":34,"601":34,"602":10,"603":10,"604":24,"605":24,"606":33},"f":{"1":263,"2":1793,"3":3228,"4":3194,"5":2734,"6":3194,"7":3205,"8":3503,"9":3466,"10":2981,"11":186,"12":9,"13":11,"14":35,"15":3533,"16":122,"17":11,"18":944,"19":129,"20":383,"21":178,"22":538,"23":45,"24":41,"25":33,"26":284,"27":295,"28":544,"29":762,"30":190,"31":140,"32":696,"33":134,"34":273,"35":4037,"36":11,"37":34},"b":{"1":[15,248],"2":[235,9,13,0],"3":[0,248],"4":[248,23],"5":[0,0],"6":[8,1479],"7":[34,3194],"8":[3228,50],"9":[634,2560],"10":[1593,1601],"11":[3194,2820],"12":[520,2260],"13":[209,2571],"14":[185,24],"15":[2,176],"16":[178,2],"17":[1,1],"18":[1,0],"19":[2,0],"20":[3,2568],"21":[2571,2070],"22":[53,2734],"23":[2787,2787],"24":[21,2713],"25":[53,2747],"26":[2800,2800],"27":[277,2928],"28":[3205,293,18],"29":[242,35],"30":[3,239],"31":[242,13,4,4,4],"32":[10,219],"33":[22,207],"34":[229,218],"35":[61,3442],"36":[58,2,2],"37":[0,58],"38":[58,56],"39":[28,30],"40":[2,28],"41":[30,3,2],"42":[16,28],"43":[53,3012],"44":[3065,2770],"45":[3039,43],"46":[101,2999],"47":[3100,113],"48":[53,2946],"49":[2999,2680],"50":[0,3246],"51":[3246,3204],"52":[74,3172],"53":[18,3154],"54":[186,2968],"55":[3154,3117],"56":[186,167,153,15],"57":[11,166],"58":[177,13],"59":[9,2959],"60":[98,26],"61":[1,25],"62":[6,117],"63":[123,6],"64":[13,110],"65":[1,177],"66":[178,14,1],"67":[9,8,18,1501,0,26,550,346,11,62,76,383,114,223,122,2,19,45,24,0,76],"68":[2,7],"69":[9,2],"70":[0,7],"71":[7,4,3],"72":[0,7],"73":[7,3,0],"74":[0,18],"75":[1501,14],"76":[1501,1490],"77":[14,1484],"78":[11,3],"79":[14,3],"80":[2,1482],"81":[1484,20,3],"82":[1,1481],"83":[1482,1201,18],"84":[24,1460],"85":[1484,1203,1101],"86":[0,0],"87":[0,0],"88":[1,121],"89":[122,3,1],"90":[2,8],"91":[383,383],"92":[383,383],"93":[366,55],"94":[55,53],"95":[3,50],"96":[22,394],"97":[111,171],"98":[284,277,178],"99":[7,133],"100":[140,13],"101":[7,164],"102":[0,7],"103":[1,163],"104":[3,160],"105":[0,160],"106":[1,159],"107":[4,155],"108":[111,65],"109":[10,35],"110":[20,15],"111":[254,71],"112":[3,58],"113":[2,310],"114":[8,304],"115":[312,15],"116":[3,5],"117":[295,9],"118":[304,233],"119":[233,71],"120":[8,296],"121":[304,233],"122":[0,8],"123":[3,5],"124":[8,7,6],"125":[1,4],"126":[81,182],"127":[0,229],"128":[59,170],"129":[46,249],"130":[295,290,279],"131":[0,46],"132":[120,129],"133":[27,93],"134":[43,86],"135":[129,128,124,101,44,44],"136":[0,43],"137":[43,43,43],"138":[18,14],"139":[0,32],"140":[0,0],"141":[81,5],"142":[86,85],"143":[44,37],"144":[8,36],"145":[44,44],"146":[8,8],"147":[0,44],"148":[11,26],"149":[37,11],"150":[11,0],"151":[31,513],"152":[23,490],"153":[513,507],"154":[190,0],"155":[696,131],"156":[89,607],"157":[131,432],"158":[52,511],"159":[563,474],"160":[50,2],"161":[2,561],"162":[563,50,44,44],"163":[361,200],"164":[48,313],"165":[105,256],"166":[106,52],"167":[8,44],"168":[12,261],"169":[273,137],"170":[35,226],"171":[3760,277],"172":[3,3757],"173":[3760,2567,905],"174":[243,34],"175":[277,257],"176":[0,4000],"177":[4000,2564,3],"178":[0,11],"179":[1,10],"180":[10,24],"181":[34,33,29,19]},"hash":"9c7923c433094c1b8d0d0030ec04dc5512d950e0"} +,"/home/travis/build/babel/babylon/src/parser/index.js": {"path":"/home/travis/build/babel/babylon/src/parser/index.js","statementMap":{"1":{"start":{"line":5,"column":23},"end":{"line":5,"column":25}},"2":{"start":{"line":9,"column":4},"end":{"line":9,"column":34}},"3":{"start":{"line":10,"column":4},"end":{"line":10,"column":26}},"4":{"start":{"line":12,"column":4},"end":{"line":12,"column":27}},"5":{"start":{"line":13,"column":4},"end":{"line":13,"column":57}},"6":{"start":{"line":14,"column":4},"end":{"line":14,"column":43}},"7":{"start":{"line":15,"column":4},"end":{"line":15,"column":23}},"8":{"start":{"line":16,"column":4},"end":{"line":16,"column":58}},"9":{"start":{"line":17,"column":4},"end":{"line":17,"column":43}},"10":{"start":{"line":20,"column":4},"end":{"line":22,"column":5}},"11":{"start":{"line":21,"column":6},"end":{"line":21,"column":30}},"12":{"start":{"line":26,"column":4},"end":{"line":26,"column":55}},"13":{"start":{"line":30,"column":4},"end":{"line":30,"column":31}},"14":{"start":{"line":34,"column":20},"end":{"line":34,"column":22}},"15":{"start":{"line":36,"column":4},"end":{"line":40,"column":5}},"16":{"start":{"line":38,"column":6},"end":{"line":38,"column":62}},"17":{"start":{"line":38,"column":43},"end":{"line":38,"column":60}},"18":{"start":{"line":39,"column":6},"end":{"line":39,"column":27}},"19":{"start":{"line":42,"column":4},"end":{"line":49,"column":5}},"20":{"start":{"line":43,"column":6},"end":{"line":48,"column":7}},"21":{"start":{"line":44,"column":8},"end":{"line":44,"column":31}},"22":{"start":{"line":46,"column":21},"end":{"line":46,"column":42}},"23":{"start":{"line":47,"column":8},"end":{"line":47,"column":33}},"24":{"start":{"line":47,"column":20},"end":{"line":47,"column":33}},"25":{"start":{"line":51,"column":4},"end":{"line":51,"column":21}},"26":{"start":{"line":61,"column":15},"end":{"line":61,"column":31}},"27":{"start":{"line":62,"column":18},"end":{"line":62,"column":34}},"28":{"start":{"line":63,"column":4},"end":{"line":63,"column":21}},"29":{"start":{"line":64,"column":4},"end":{"line":64,"column":45}}},"fnMap":{"1":{"name":"(anonymous_1)","decl":{"start":{"line":8,"column":2},"end":{"line":8,"column":3}},"loc":{"start":{"line":8,"column":46},"end":{"line":23,"column":3}}},"2":{"name":"(anonymous_2)","decl":{"start":{"line":25,"column":2},"end":{"line":25,"column":3}},"loc":{"start":{"line":25,"column":35},"end":{"line":27,"column":3}}},"3":{"name":"(anonymous_3)","decl":{"start":{"line":29,"column":2},"end":{"line":29,"column":3}},"loc":{"start":{"line":29,"column":36},"end":{"line":31,"column":3}}},"4":{"name":"(anonymous_4)","decl":{"start":{"line":33,"column":2},"end":{"line":33,"column":3}},"loc":{"start":{"line":33,"column":46},"end":{"line":52,"column":3}}},"5":{"name":"(anonymous_5)","decl":{"start":{"line":38,"column":31},"end":{"line":38,"column":32}},"loc":{"start":{"line":38,"column":43},"end":{"line":38,"column":60}}},"6":{"name":"(anonymous_6)","decl":{"start":{"line":54,"column":2},"end":{"line":54,"column":3}},"loc":{"start":{"line":60,"column":4},"end":{"line":65,"column":3}}}},"branchMap":{"1":{"loc":{"start":{"line":20,"column":4},"end":{"line":22,"column":5}},"type":"if","locations":[{"start":{"line":20,"column":4},"end":{"line":22,"column":5}},{"start":{"line":20,"column":4},"end":{"line":22,"column":5}}]},"2":{"loc":{"start":{"line":20,"column":8},"end":{"line":20,"column":78}},"type":"binary-expr","locations":[{"start":{"line":20,"column":8},"end":{"line":20,"column":28}},{"start":{"line":20,"column":32},"end":{"line":20,"column":53}},{"start":{"line":20,"column":57},"end":{"line":20,"column":78}}]},"3":{"loc":{"start":{"line":26,"column":14},"end":{"line":26,"column":53}},"type":"binary-expr","locations":[{"start":{"line":26,"column":14},"end":{"line":26,"column":31}},{"start":{"line":26,"column":35},"end":{"line":26,"column":53}}]},"4":{"loc":{"start":{"line":36,"column":4},"end":{"line":40,"column":5}},"type":"if","locations":[{"start":{"line":36,"column":4},"end":{"line":40,"column":5}},{"start":{"line":36,"column":4},"end":{"line":40,"column":5}}]},"5":{"loc":{"start":{"line":43,"column":6},"end":{"line":48,"column":7}},"type":"if","locations":[{"start":{"line":43,"column":6},"end":{"line":48,"column":7}},{"start":{"line":43,"column":6},"end":{"line":48,"column":7}}]},"6":{"loc":{"start":{"line":47,"column":8},"end":{"line":47,"column":33}},"type":"if","locations":[{"start":{"line":47,"column":8},"end":{"line":47,"column":33}},{"start":{"line":47,"column":8},"end":{"line":47,"column":33}}]}},"s":{"1":1,"2":2123,"3":2123,"4":2123,"5":2123,"6":2123,"7":2123,"8":2123,"9":2123,"10":2123,"11":2,"12":1088,"13":8596,"14":2123,"15":2123,"16":253,"17":506,"18":253,"19":2123,"20":557,"21":557,"22":557,"23":557,"24":504,"25":2123,"26":2123,"27":2123,"28":2123,"29":2026},"f":{"1":2123,"2":1088,"3":8596,"4":2123,"5":506,"6":2123},"b":{"1":[2,2121],"2":[2123,2123,2],"3":[1088,1088],"4":[253,1870],"5":[557,0],"6":[504,53]},"hash":"f7e9006d113cd234a5c7df0c1859b1ef3cdf039a"} +,"/home/travis/build/babel/babylon/src/parser/location.js": {"path":"/home/travis/build/babel/babylon/src/parser/location.js","statementMap":{"1":{"start":{"line":4,"column":11},"end":{"line":4,"column":27}},"2":{"start":{"line":12,"column":0},"end":{"line":19,"column":2}},"3":{"start":{"line":13,"column":12},"end":{"line":13,"column":40}},"4":{"start":{"line":14,"column":2},"end":{"line":14,"column":44}},"5":{"start":{"line":15,"column":12},"end":{"line":15,"column":36}},"6":{"start":{"line":16,"column":2},"end":{"line":16,"column":16}},"7":{"start":{"line":17,"column":2},"end":{"line":17,"column":16}},"8":{"start":{"line":18,"column":2},"end":{"line":18,"column":12}}},"fnMap":{"1":{"name":"(anonymous_1)","decl":{"start":{"line":12,"column":11},"end":{"line":12,"column":12}},"loc":{"start":{"line":12,"column":35},"end":{"line":19,"column":1}}}},"branchMap":{},"s":{"1":1,"2":1,"3":738,"4":738,"5":738,"6":738,"7":738,"8":738},"f":{"1":738},"b":{},"hash":"513a036132dc8836c5d9933be753a4fe6a778c29"} +,"/home/travis/build/babel/babylon/src/parser/lval.js": {"path":"/home/travis/build/babel/babylon/src/parser/lval.js","statementMap":{"1":{"start":{"line":7,"column":11},"end":{"line":7,"column":27}},"2":{"start":{"line":12,"column":0},"end":{"line":66,"column":2}},"3":{"start":{"line":13,"column":2},"end":{"line":64,"column":3}},"4":{"start":{"line":14,"column":4},"end":{"line":63,"column":5}},"5":{"start":{"line":19,"column":8},"end":{"line":19,"column":14}},"6":{"start":{"line":22,"column":8},"end":{"line":22,"column":36}},"7":{"start":{"line":23,"column":8},"end":{"line":33,"column":9}},"8":{"start":{"line":24,"column":10},"end":{"line":32,"column":11}},"9":{"start":{"line":25,"column":12},"end":{"line":29,"column":13}},"10":{"start":{"line":26,"column":14},"end":{"line":26,"column":90}},"11":{"start":{"line":28,"column":14},"end":{"line":28,"column":81}},"12":{"start":{"line":31,"column":12},"end":{"line":31,"column":47}},"13":{"start":{"line":34,"column":8},"end":{"line":34,"column":14}},"14":{"start":{"line":37,"column":8},"end":{"line":37,"column":49}},"15":{"start":{"line":38,"column":8},"end":{"line":38,"column":14}},"16":{"start":{"line":41,"column":8},"end":{"line":41,"column":35}},"17":{"start":{"line":42,"column":8},"end":{"line":42,"column":14}},"18":{"start":{"line":45,"column":8},"end":{"line":45,"column":35}},"19":{"start":{"line":46,"column":8},"end":{"line":46,"column":56}},"20":{"start":{"line":47,"column":8},"end":{"line":47,"column":14}},"21":{"start":{"line":50,"column":8},"end":{"line":55,"column":9}},"22":{"start":{"line":51,"column":10},"end":{"line":51,"column":42}},"23":{"start":{"line":52,"column":10},"end":{"line":52,"column":31}},"24":{"start":{"line":54,"column":10},"end":{"line":54,"column":99}},"25":{"start":{"line":56,"column":8},"end":{"line":56,"column":14}},"26":{"start":{"line":59,"column":8},"end":{"line":59,"column":30}},"27":{"start":{"line":59,"column":24},"end":{"line":59,"column":30}},"28":{"start":{"line":62,"column":8},"end":{"line":62,"column":54}},"29":{"start":{"line":65,"column":2},"end":{"line":65,"column":14}},"30":{"start":{"line":70,"column":0},"end":{"line":91,"column":2}},"31":{"start":{"line":71,"column":12},"end":{"line":71,"column":27}},"32":{"start":{"line":72,"column":2},"end":{"line":85,"column":3}},"33":{"start":{"line":73,"column":15},"end":{"line":73,"column":32}},"34":{"start":{"line":74,"column":4},"end":{"line":84,"column":5}},"35":{"start":{"line":75,"column":6},"end":{"line":75,"column":12}},"36":{"start":{"line":76,"column":11},"end":{"line":84,"column":5}},"37":{"start":{"line":77,"column":6},"end":{"line":77,"column":32}},"38":{"start":{"line":78,"column":16},"end":{"line":78,"column":29}},"39":{"start":{"line":79,"column":6},"end":{"line":79,"column":40}},"40":{"start":{"line":80,"column":6},"end":{"line":82,"column":7}},"41":{"start":{"line":81,"column":8},"end":{"line":81,"column":35}},"42":{"start":{"line":83,"column":6},"end":{"line":83,"column":12}},"43":{"start":{"line":86,"column":2},"end":{"line":89,"column":3}},"44":{"start":{"line":87,"column":14},"end":{"line":87,"column":25}},"45":{"start":{"line":88,"column":4},"end":{"line":88,"column":47}},"46":{"start":{"line":88,"column":13},"end":{"line":88,"column":47}},"47":{"start":{"line":90,"column":2},"end":{"line":90,"column":18}},"48":{"start":{"line":95,"column":0},"end":{"line":97,"column":2}},"49":{"start":{"line":96,"column":2},"end":{"line":96,"column":18}},"50":{"start":{"line":101,"column":0},"end":{"line":106,"column":2}},"51":{"start":{"line":102,"column":13},"end":{"line":102,"column":29}},"52":{"start":{"line":103,"column":2},"end":{"line":103,"column":14}},"53":{"start":{"line":104,"column":2},"end":{"line":104,"column":64}},"54":{"start":{"line":105,"column":2},"end":{"line":105,"column":48}},"55":{"start":{"line":108,"column":0},"end":{"line":113,"column":2}},"56":{"start":{"line":109,"column":13},"end":{"line":109,"column":29}},"57":{"start":{"line":110,"column":2},"end":{"line":110,"column":14}},"58":{"start":{"line":111,"column":2},"end":{"line":111,"column":48}},"59":{"start":{"line":112,"column":2},"end":{"line":112,"column":46}},"60":{"start":{"line":115,"column":0},"end":{"line":117,"column":2}},"61":{"start":{"line":116,"column":2},"end":{"line":116,"column":80}},"62":{"start":{"line":119,"column":0},"end":{"line":121,"column":2}},"63":{"start":{"line":120,"column":2},"end":{"line":120,"column":65}},"64":{"start":{"line":125,"column":0},"end":{"line":145,"column":2}},"65":{"start":{"line":126,"column":2},"end":{"line":144,"column":3}},"66":{"start":{"line":128,"column":6},"end":{"line":128,"column":73}},"67":{"start":{"line":128,"column":55},"end":{"line":128,"column":73}},"68":{"start":{"line":131,"column":6},"end":{"line":131,"column":40}},"69":{"start":{"line":134,"column":17},"end":{"line":134,"column":33}},"70":{"start":{"line":135,"column":6},"end":{"line":135,"column":18}},"71":{"start":{"line":136,"column":6},"end":{"line":136,"column":63}},"72":{"start":{"line":137,"column":6},"end":{"line":137,"column":51}},"73":{"start":{"line":140,"column":6},"end":{"line":140,"column":33}},"74":{"start":{"line":143,"column":6},"end":{"line":143,"column":24}},"75":{"start":{"line":147,"column":0},"end":{"line":178,"column":2}},"76":{"start":{"line":148,"column":13},"end":{"line":148,"column":15}},"77":{"start":{"line":149,"column":14},"end":{"line":149,"column":18}},"78":{"start":{"line":150,"column":2},"end":{"line":176,"column":3}},"79":{"start":{"line":151,"column":4},"end":{"line":155,"column":5}},"80":{"start":{"line":152,"column":6},"end":{"line":152,"column":20}},"81":{"start":{"line":154,"column":6},"end":{"line":154,"column":28}},"82":{"start":{"line":156,"column":4},"end":{"line":175,"column":5}},"83":{"start":{"line":157,"column":6},"end":{"line":157,"column":22}},"84":{"start":{"line":158,"column":11},"end":{"line":175,"column":5}},"85":{"start":{"line":159,"column":6},"end":{"line":159,"column":12}},"86":{"start":{"line":160,"column":11},"end":{"line":175,"column":5}},"87":{"start":{"line":161,"column":6},"end":{"line":161,"column":69}},"88":{"start":{"line":162,"column":6},"end":{"line":162,"column":25}},"89":{"start":{"line":163,"column":6},"end":{"line":163,"column":12}},"90":{"start":{"line":165,"column":23},"end":{"line":165,"column":25}},"91":{"start":{"line":166,"column":6},"end":{"line":168,"column":7}},"92":{"start":{"line":167,"column":8},"end":{"line":167,"column":47}},"93":{"start":{"line":169,"column":17},"end":{"line":169,"column":41}},"94":{"start":{"line":170,"column":6},"end":{"line":172,"column":7}},"95":{"start":{"line":171,"column":8},"end":{"line":171,"column":37}},"96":{"start":{"line":173,"column":6},"end":{"line":173,"column":46}},"97":{"start":{"line":174,"column":6},"end":{"line":174,"column":74}},"98":{"start":{"line":177,"column":2},"end":{"line":177,"column":14}},"99":{"start":{"line":180,"column":0},"end":{"line":182,"column":2}},"100":{"start":{"line":181,"column":2},"end":{"line":181,"column":15}},"101":{"start":{"line":186,"column":0},"end":{"line":196,"column":2}},"102":{"start":{"line":187,"column":2},"end":{"line":187,"column":45}},"103":{"start":{"line":188,"column":2},"end":{"line":188,"column":42}},"104":{"start":{"line":189,"column":2},"end":{"line":189,"column":41}},"105":{"start":{"line":190,"column":2},"end":{"line":190,"column":36}},"106":{"start":{"line":190,"column":24},"end":{"line":190,"column":36}},"107":{"start":{"line":192,"column":13},"end":{"line":192,"column":49}},"108":{"start":{"line":193,"column":2},"end":{"line":193,"column":19}},"109":{"start":{"line":194,"column":2},"end":{"line":194,"column":39}},"110":{"start":{"line":195,"column":2},"end":{"line":195,"column":52}},"111":{"start":{"line":201,"column":0},"end":{"line":259,"column":2}},"112":{"start":{"line":202,"column":2},"end":{"line":258,"column":3}},"113":{"start":{"line":204,"column":6},"end":{"line":206,"column":7}},"114":{"start":{"line":205,"column":8},"end":{"line":205,"column":107}},"115":{"start":{"line":208,"column":6},"end":{"line":227,"column":7}},"116":{"start":{"line":220,"column":18},"end":{"line":220,"column":33}},"117":{"start":{"line":222,"column":8},"end":{"line":226,"column":9}},"118":{"start":{"line":223,"column":10},"end":{"line":223,"column":71}},"119":{"start":{"line":225,"column":10},"end":{"line":225,"column":35}},"120":{"start":{"line":228,"column":6},"end":{"line":228,"column":12}},"121":{"start":{"line":231,"column":6},"end":{"line":231,"column":109}},"122":{"start":{"line":231,"column":21},"end":{"line":231,"column":109}},"123":{"start":{"line":232,"column":6},"end":{"line":232,"column":12}},"124":{"start":{"line":235,"column":6},"end":{"line":238,"column":7}},"125":{"start":{"line":236,"column":8},"end":{"line":236,"column":62}},"126":{"start":{"line":236,"column":44},"end":{"line":236,"column":62}},"127":{"start":{"line":237,"column":8},"end":{"line":237,"column":54}},"128":{"start":{"line":239,"column":6},"end":{"line":239,"column":12}},"129":{"start":{"line":242,"column":6},"end":{"line":244,"column":7}},"130":{"start":{"line":243,"column":8},"end":{"line":243,"column":64}},"131":{"start":{"line":243,"column":18},"end":{"line":243,"column":64}},"132":{"start":{"line":245,"column":6},"end":{"line":245,"column":12}},"133":{"start":{"line":248,"column":6},"end":{"line":248,"column":57}},"134":{"start":{"line":249,"column":6},"end":{"line":249,"column":12}},"135":{"start":{"line":253,"column":6},"end":{"line":253,"column":61}},"136":{"start":{"line":254,"column":6},"end":{"line":254,"column":12}},"137":{"start":{"line":257,"column":6},"end":{"line":257,"column":83}}},"fnMap":{"1":{"name":"(anonymous_1)","decl":{"start":{"line":12,"column":18},"end":{"line":12,"column":19}},"loc":{"start":{"line":12,"column":45},"end":{"line":66,"column":1}}},"2":{"name":"(anonymous_2)","decl":{"start":{"line":70,"column":22},"end":{"line":70,"column":23}},"loc":{"start":{"line":70,"column":53},"end":{"line":91,"column":1}}},"3":{"name":"(anonymous_3)","decl":{"start":{"line":95,"column":22},"end":{"line":95,"column":23}},"loc":{"start":{"line":95,"column":42},"end":{"line":97,"column":1}}},"4":{"name":"(anonymous_4)","decl":{"start":{"line":101,"column":17},"end":{"line":101,"column":18}},"loc":{"start":{"line":101,"column":51},"end":{"line":106,"column":1}}},"5":{"name":"(anonymous_5)","decl":{"start":{"line":108,"column":15},"end":{"line":108,"column":16}},"loc":{"start":{"line":108,"column":27},"end":{"line":113,"column":1}}},"6":{"name":"(anonymous_6)","decl":{"start":{"line":115,"column":32},"end":{"line":115,"column":33}},"loc":{"start":{"line":115,"column":44},"end":{"line":117,"column":1}}},"7":{"name":"(anonymous_7)","decl":{"start":{"line":119,"column":28},"end":{"line":119,"column":29}},"loc":{"start":{"line":119,"column":40},"end":{"line":121,"column":1}}},"8":{"name":"(anonymous_8)","decl":{"start":{"line":125,"column":22},"end":{"line":125,"column":23}},"loc":{"start":{"line":125,"column":34},"end":{"line":145,"column":1}}},"9":{"name":"(anonymous_9)","decl":{"start":{"line":147,"column":22},"end":{"line":147,"column":23}},"loc":{"start":{"line":147,"column":51},"end":{"line":178,"column":1}}},"10":{"name":"(anonymous_10)","decl":{"start":{"line":180,"column":34},"end":{"line":180,"column":35}},"loc":{"start":{"line":180,"column":51},"end":{"line":182,"column":1}}},"11":{"name":"(anonymous_11)","decl":{"start":{"line":186,"column":23},"end":{"line":186,"column":24}},"loc":{"start":{"line":186,"column":59},"end":{"line":196,"column":1}}},"12":{"name":"(anonymous_12)","decl":{"start":{"line":201,"column":15},"end":{"line":201,"column":16}},"loc":{"start":{"line":201,"column":56},"end":{"line":259,"column":1}}}},"branchMap":{"1":{"loc":{"start":{"line":13,"column":2},"end":{"line":64,"column":3}},"type":"if","locations":[{"start":{"line":13,"column":2},"end":{"line":64,"column":3}},{"start":{"line":13,"column":2},"end":{"line":64,"column":3}}]},"2":{"loc":{"start":{"line":14,"column":4},"end":{"line":63,"column":5}},"type":"switch","locations":[{"start":{"line":15,"column":6},"end":{"line":15,"column":24}},{"start":{"line":16,"column":6},"end":{"line":16,"column":27}},{"start":{"line":17,"column":6},"end":{"line":17,"column":26}},{"start":{"line":18,"column":6},"end":{"line":19,"column":14}},{"start":{"line":21,"column":6},"end":{"line":34,"column":14}},{"start":{"line":36,"column":6},"end":{"line":38,"column":14}},{"start":{"line":40,"column":6},"end":{"line":42,"column":14}},{"start":{"line":44,"column":6},"end":{"line":47,"column":14}},{"start":{"line":49,"column":6},"end":{"line":56,"column":14}},{"start":{"line":58,"column":6},"end":{"line":59,"column":30}},{"start":{"line":61,"column":6},"end":{"line":62,"column":54}}]},"3":{"loc":{"start":{"line":24,"column":10},"end":{"line":32,"column":11}},"type":"if","locations":[{"start":{"line":24,"column":10},"end":{"line":32,"column":11}},{"start":{"line":24,"column":10},"end":{"line":32,"column":11}}]},"4":{"loc":{"start":{"line":25,"column":12},"end":{"line":29,"column":13}},"type":"if","locations":[{"start":{"line":25,"column":12},"end":{"line":29,"column":13}},{"start":{"line":25,"column":12},"end":{"line":29,"column":13}}]},"5":{"loc":{"start":{"line":25,"column":16},"end":{"line":25,"column":58}},"type":"binary-expr","locations":[{"start":{"line":25,"column":16},"end":{"line":25,"column":35}},{"start":{"line":25,"column":39},"end":{"line":25,"column":58}}]},"6":{"loc":{"start":{"line":50,"column":8},"end":{"line":55,"column":9}},"type":"if","locations":[{"start":{"line":50,"column":8},"end":{"line":55,"column":9}},{"start":{"line":50,"column":8},"end":{"line":55,"column":9}}]},"7":{"loc":{"start":{"line":59,"column":8},"end":{"line":59,"column":30}},"type":"if","locations":[{"start":{"line":59,"column":8},"end":{"line":59,"column":30}},{"start":{"line":59,"column":8},"end":{"line":59,"column":30}}]},"8":{"loc":{"start":{"line":72,"column":2},"end":{"line":85,"column":3}},"type":"if","locations":[{"start":{"line":72,"column":2},"end":{"line":85,"column":3}},{"start":{"line":72,"column":2},"end":{"line":85,"column":3}}]},"9":{"loc":{"start":{"line":74,"column":4},"end":{"line":84,"column":5}},"type":"if","locations":[{"start":{"line":74,"column":4},"end":{"line":84,"column":5}},{"start":{"line":74,"column":4},"end":{"line":84,"column":5}}]},"10":{"loc":{"start":{"line":74,"column":8},"end":{"line":74,"column":43}},"type":"binary-expr","locations":[{"start":{"line":74,"column":8},"end":{"line":74,"column":12}},{"start":{"line":74,"column":16},"end":{"line":74,"column":43}}]},"11":{"loc":{"start":{"line":76,"column":11},"end":{"line":84,"column":5}},"type":"if","locations":[{"start":{"line":76,"column":11},"end":{"line":84,"column":5}},{"start":{"line":76,"column":11},"end":{"line":84,"column":5}}]},"12":{"loc":{"start":{"line":76,"column":15},"end":{"line":76,"column":52}},"type":"binary-expr","locations":[{"start":{"line":76,"column":15},"end":{"line":76,"column":19}},{"start":{"line":76,"column":23},"end":{"line":76,"column":52}}]},"13":{"loc":{"start":{"line":80,"column":6},"end":{"line":82,"column":7}},"type":"if","locations":[{"start":{"line":80,"column":6},"end":{"line":82,"column":7}},{"start":{"line":80,"column":6},"end":{"line":82,"column":7}}]},"14":{"loc":{"start":{"line":80,"column":10},"end":{"line":80,"column":101}},"type":"binary-expr","locations":[{"start":{"line":80,"column":10},"end":{"line":80,"column":35}},{"start":{"line":80,"column":39},"end":{"line":80,"column":70}},{"start":{"line":80,"column":74},"end":{"line":80,"column":101}}]},"15":{"loc":{"start":{"line":88,"column":4},"end":{"line":88,"column":47}},"type":"if","locations":[{"start":{"line":88,"column":4},"end":{"line":88,"column":47}},{"start":{"line":88,"column":4},"end":{"line":88,"column":47}}]},"16":{"loc":{"start":{"line":116,"column":9},"end":{"line":116,"column":79}},"type":"binary-expr","locations":[{"start":{"line":116,"column":9},"end":{"line":116,"column":30}},{"start":{"line":116,"column":34},"end":{"line":116,"column":52}},{"start":{"line":116,"column":56},"end":{"line":116,"column":79}}]},"17":{"loc":{"start":{"line":126,"column":2},"end":{"line":144,"column":3}},"type":"switch","locations":[{"start":{"line":127,"column":4},"end":{"line":128,"column":73}},{"start":{"line":130,"column":4},"end":{"line":131,"column":40}},{"start":{"line":133,"column":4},"end":{"line":137,"column":51}},{"start":{"line":139,"column":4},"end":{"line":140,"column":33}},{"start":{"line":142,"column":4},"end":{"line":143,"column":24}}]},"18":{"loc":{"start":{"line":128,"column":6},"end":{"line":128,"column":73}},"type":"if","locations":[{"start":{"line":128,"column":6},"end":{"line":128,"column":73}},{"start":{"line":128,"column":6},"end":{"line":128,"column":73}}]},"19":{"loc":{"start":{"line":128,"column":10},"end":{"line":128,"column":53}},"type":"binary-expr","locations":[{"start":{"line":128,"column":10},"end":{"line":128,"column":27}},{"start":{"line":128,"column":31},"end":{"line":128,"column":53}}]},"20":{"loc":{"start":{"line":151,"column":4},"end":{"line":155,"column":5}},"type":"if","locations":[{"start":{"line":151,"column":4},"end":{"line":155,"column":5}},{"start":{"line":151,"column":4},"end":{"line":155,"column":5}}]},"21":{"loc":{"start":{"line":156,"column":4},"end":{"line":175,"column":5}},"type":"if","locations":[{"start":{"line":156,"column":4},"end":{"line":175,"column":5}},{"start":{"line":156,"column":4},"end":{"line":175,"column":5}}]},"22":{"loc":{"start":{"line":156,"column":8},"end":{"line":156,"column":42}},"type":"binary-expr","locations":[{"start":{"line":156,"column":8},"end":{"line":156,"column":18}},{"start":{"line":156,"column":22},"end":{"line":156,"column":42}}]},"23":{"loc":{"start":{"line":158,"column":11},"end":{"line":175,"column":5}},"type":"if","locations":[{"start":{"line":158,"column":11},"end":{"line":175,"column":5}},{"start":{"line":158,"column":11},"end":{"line":175,"column":5}}]},"24":{"loc":{"start":{"line":160,"column":11},"end":{"line":175,"column":5}},"type":"if","locations":[{"start":{"line":160,"column":11},"end":{"line":175,"column":5}},{"start":{"line":160,"column":11},"end":{"line":175,"column":5}}]},"25":{"loc":{"start":{"line":170,"column":6},"end":{"line":172,"column":7}},"type":"if","locations":[{"start":{"line":170,"column":6},"end":{"line":172,"column":7}},{"start":{"line":170,"column":6},"end":{"line":172,"column":7}}]},"26":{"loc":{"start":{"line":187,"column":13},"end":{"line":187,"column":44}},"type":"binary-expr","locations":[{"start":{"line":187,"column":13},"end":{"line":187,"column":21}},{"start":{"line":187,"column":25},"end":{"line":187,"column":44}}]},"27":{"loc":{"start":{"line":188,"column":13},"end":{"line":188,"column":41}},"type":"binary-expr","locations":[{"start":{"line":188,"column":13},"end":{"line":188,"column":21}},{"start":{"line":188,"column":25},"end":{"line":188,"column":41}}]},"28":{"loc":{"start":{"line":189,"column":9},"end":{"line":189,"column":40}},"type":"binary-expr","locations":[{"start":{"line":189,"column":9},"end":{"line":189,"column":13}},{"start":{"line":189,"column":17},"end":{"line":189,"column":40}}]},"29":{"loc":{"start":{"line":190,"column":2},"end":{"line":190,"column":36}},"type":"if","locations":[{"start":{"line":190,"column":2},"end":{"line":190,"column":36}},{"start":{"line":190,"column":2},"end":{"line":190,"column":36}}]},"30":{"loc":{"start":{"line":202,"column":2},"end":{"line":258,"column":3}},"type":"switch","locations":[{"start":{"line":203,"column":4},"end":{"line":228,"column":12}},{"start":{"line":230,"column":4},"end":{"line":232,"column":12}},{"start":{"line":234,"column":4},"end":{"line":239,"column":12}},{"start":{"line":241,"column":4},"end":{"line":245,"column":12}},{"start":{"line":247,"column":4},"end":{"line":249,"column":12}},{"start":{"line":251,"column":4},"end":{"line":251,"column":24}},{"start":{"line":252,"column":4},"end":{"line":254,"column":12}},{"start":{"line":256,"column":4},"end":{"line":257,"column":83}}]},"31":{"loc":{"start":{"line":204,"column":6},"end":{"line":206,"column":7}},"type":"if","locations":[{"start":{"line":204,"column":6},"end":{"line":206,"column":7}},{"start":{"line":204,"column":6},"end":{"line":206,"column":7}}]},"32":{"loc":{"start":{"line":204,"column":10},"end":{"line":204,"column":103}},"type":"binary-expr","locations":[{"start":{"line":204,"column":10},"end":{"line":204,"column":27}},{"start":{"line":204,"column":32},"end":{"line":204,"column":67}},{"start":{"line":204,"column":71},"end":{"line":204,"column":102}}]},"33":{"loc":{"start":{"line":205,"column":32},"end":{"line":205,"column":72}},"type":"cond-expr","locations":[{"start":{"line":205,"column":44},"end":{"line":205,"column":54}},{"start":{"line":205,"column":57},"end":{"line":205,"column":72}}]},"34":{"loc":{"start":{"line":208,"column":6},"end":{"line":227,"column":7}},"type":"if","locations":[{"start":{"line":208,"column":6},"end":{"line":227,"column":7}},{"start":{"line":208,"column":6},"end":{"line":227,"column":7}}]},"35":{"loc":{"start":{"line":222,"column":8},"end":{"line":226,"column":9}},"type":"if","locations":[{"start":{"line":222,"column":8},"end":{"line":226,"column":9}},{"start":{"line":222,"column":8},"end":{"line":226,"column":9}}]},"36":{"loc":{"start":{"line":231,"column":6},"end":{"line":231,"column":109}},"type":"if","locations":[{"start":{"line":231,"column":6},"end":{"line":231,"column":109}},{"start":{"line":231,"column":6},"end":{"line":231,"column":109}}]},"37":{"loc":{"start":{"line":231,"column":45},"end":{"line":231,"column":83}},"type":"cond-expr","locations":[{"start":{"line":231,"column":57},"end":{"line":231,"column":66}},{"start":{"line":231,"column":69},"end":{"line":231,"column":83}}]},"38":{"loc":{"start":{"line":236,"column":8},"end":{"line":236,"column":62}},"type":"if","locations":[{"start":{"line":236,"column":8},"end":{"line":236,"column":62}},{"start":{"line":236,"column":8},"end":{"line":236,"column":62}}]},"39":{"loc":{"start":{"line":243,"column":8},"end":{"line":243,"column":64}},"type":"if","locations":[{"start":{"line":243,"column":8},"end":{"line":243,"column":64}},{"start":{"line":243,"column":8},"end":{"line":243,"column":64}}]},"40":{"loc":{"start":{"line":257,"column":30},"end":{"line":257,"column":68}},"type":"cond-expr","locations":[{"start":{"line":257,"column":42},"end":{"line":257,"column":51}},{"start":{"line":257,"column":54},"end":{"line":257,"column":68}}]}},"s":{"1":1,"2":1,"3":495,"4":495,"5":317,"6":31,"7":31,"8":39,"9":3,"10":3,"11":0,"12":36,"13":25,"14":35,"15":32,"16":1,"17":1,"18":55,"19":55,"20":50,"21":17,"22":17,"23":17,"24":0,"25":17,"26":9,"27":8,"28":31,"29":450,"30":1,"31":195,"32":195,"33":180,"34":180,"35":12,"36":168,"37":13,"38":13,"39":13,"40":13,"41":1,"42":12,"43":194,"44":216,"45":216,"46":209,"47":181,"48":1,"49":289,"50":1,"51":43,"52":43,"53":43,"54":39,"55":1,"56":55,"57":55,"58":55,"59":41,"60":1,"61":1885,"62":1,"63":384,"64":1,"65":726,"66":16,"67":10,"68":583,"69":46,"70":46,"71":46,"72":46,"73":61,"74":26,"75":1,"76":639,"77":639,"78":639,"79":320,"80":241,"81":79,"82":320,"83":2,"84":318,"85":5,"86":313,"87":33,"88":23,"89":18,"90":280,"91":280,"92":15,"93":280,"94":267,"95":10,"96":267,"97":267,"98":611,"99":1,"100":254,"101":1,"102":629,"103":629,"104":629,"105":615,"106":576,"107":39,"108":39,"109":39,"110":39,"111":1,"112":1338,"113":1094,"114":113,"115":981,"116":254,"117":254,"118":22,"119":232,"120":959,"121":9,"122":0,"123":9,"124":66,"125":79,"126":76,"127":79,"128":60,"129":88,"130":117,"131":108,"132":80,"133":36,"134":35,"135":36,"136":31,"137":9},"f":{"1":495,"2":195,"3":289,"4":43,"5":55,"6":1885,"7":384,"8":726,"9":639,"10":254,"11":629,"12":1338},"b":{"1":[495,0],"2":[309,309,309,317,31,35,1,55,17,9,31],"3":[3,36],"4":[3,0],"5":[3,0],"6":[17,0],"7":[8,1],"8":[180,15],"9":[12,168],"10":[180,177],"11":[13,155],"12":[168,165],"13":[1,12],"14":[13,4,2],"15":[209,7],"16":[1885,28,25],"17":[16,583,46,61,26],"18":[10,6],"19":[16,9],"20":[241,79],"21":[2,318],"22":[320,59],"23":[5,313],"24":[33,280],"25":[10,257],"26":[629,280],"27":[629,280],"28":[629,307],"29":[576,39],"30":[1094,9,66,88,36,3,36,9],"31":[113,981],"32":[1094,491,402],"33":[86,27],"34":[254,727],"35":[22,232],"36":[0,9],"37":[0,0],"38":[76,3],"39":[108,9],"40":[0,9]},"hash":"e5cf49673e0d2c7539d9376c61cba4715defc0b8"} +,"/home/travis/build/babel/babylon/src/parser/node.js": {"path":"/home/travis/build/babel/babylon/src/parser/node.js","statementMap":{"1":{"start":{"line":6,"column":11},"end":{"line":6,"column":27}},"2":{"start":{"line":7,"column":20},"end":{"line":7,"column":76}},"3":{"start":{"line":11,"column":4},"end":{"line":11,"column":19}},"4":{"start":{"line":12,"column":4},"end":{"line":12,"column":21}},"5":{"start":{"line":13,"column":4},"end":{"line":13,"column":17}},"6":{"start":{"line":14,"column":4},"end":{"line":14,"column":39}},"7":{"start":{"line":15,"column":4},"end":{"line":15,"column":47}},"8":{"start":{"line":15,"column":18},"end":{"line":15,"column":47}},"9":{"start":{"line":24,"column":18},"end":{"line":24,"column":26}},"10":{"start":{"line":25,"column":4},"end":{"line":30,"column":5}},"11":{"start":{"line":27,"column":6},"end":{"line":29,"column":7}},"12":{"start":{"line":28,"column":8},"end":{"line":28,"column":31}},"13":{"start":{"line":32,"column":4},"end":{"line":32,"column":17}},"14":{"start":{"line":36,"column":0},"end":{"line":38,"column":2}},"15":{"start":{"line":37,"column":2},"end":{"line":37,"column":72}},"16":{"start":{"line":40,"column":0},"end":{"line":42,"column":2}},"17":{"start":{"line":41,"column":2},"end":{"line":41,"column":43}},"18":{"start":{"line":45,"column":2},"end":{"line":45,"column":19}},"19":{"start":{"line":46,"column":2},"end":{"line":46,"column":17}},"20":{"start":{"line":47,"column":2},"end":{"line":47,"column":21}},"21":{"start":{"line":48,"column":2},"end":{"line":48,"column":28}},"22":{"start":{"line":49,"column":2},"end":{"line":49,"column":14}},"23":{"start":{"line":54,"column":0},"end":{"line":56,"column":2}},"24":{"start":{"line":55,"column":2},"end":{"line":55,"column":94}},"25":{"start":{"line":60,"column":0},"end":{"line":62,"column":2}},"26":{"start":{"line":61,"column":2},"end":{"line":61,"column":55}}},"fnMap":{"1":{"name":"(anonymous_1)","decl":{"start":{"line":10,"column":2},"end":{"line":10,"column":3}},"loc":{"start":{"line":10,"column":69},"end":{"line":16,"column":3}}},"2":{"name":"(anonymous_2)","decl":{"start":{"line":23,"column":2},"end":{"line":23,"column":3}},"loc":{"start":{"line":23,"column":18},"end":{"line":33,"column":3}}},"3":{"name":"(anonymous_3)","decl":{"start":{"line":36,"column":15},"end":{"line":36,"column":16}},"loc":{"start":{"line":36,"column":27},"end":{"line":38,"column":1}}},"4":{"name":"(anonymous_4)","decl":{"start":{"line":40,"column":17},"end":{"line":40,"column":18}},"loc":{"start":{"line":40,"column":37},"end":{"line":42,"column":1}}},"5":{"name":"finishNodeAt","decl":{"start":{"line":44,"column":9},"end":{"line":44,"column":21}},"loc":{"start":{"line":44,"column":44},"end":{"line":50,"column":1}}},"6":{"name":"(anonymous_6)","decl":{"start":{"line":54,"column":16},"end":{"line":54,"column":17}},"loc":{"start":{"line":54,"column":38},"end":{"line":56,"column":1}}},"7":{"name":"(anonymous_7)","decl":{"start":{"line":60,"column":18},"end":{"line":60,"column":19}},"loc":{"start":{"line":60,"column":50},"end":{"line":62,"column":1}}}},"branchMap":{"1":{"loc":{"start":{"line":15,"column":4},"end":{"line":15,"column":47}},"type":"if","locations":[{"start":{"line":15,"column":4},"end":{"line":15,"column":47}},{"start":{"line":15,"column":4},"end":{"line":15,"column":47}}]},"2":{"loc":{"start":{"line":27,"column":6},"end":{"line":29,"column":7}},"type":"if","locations":[{"start":{"line":27,"column":6},"end":{"line":29,"column":7}},{"start":{"line":27,"column":6},"end":{"line":29,"column":7}}]}},"s":{"1":1,"2":1,"3":21359,"4":21359,"5":21359,"6":21359,"7":21359,"8":6,"9":126,"10":126,"11":632,"12":630,"13":126,"14":1,"15":19360,"16":1,"17":1873,"18":15001,"19":15001,"20":15001,"21":15001,"22":15001,"23":1,"24":14554,"25":1,"26":447},"f":{"1":21359,"2":126,"3":19360,"4":1873,"5":15001,"6":14554,"7":447},"b":{"1":[6,21353],"2":[630,2]},"hash":"ffcbe25b257f58ca63f96ef73c3940f99e1cd030"} +,"/home/travis/build/babel/babylon/src/parser/statement.js": {"path":"/home/travis/build/babel/babylon/src/parser/statement.js","statementMap":{"1":{"start":{"line":8,"column":11},"end":{"line":8,"column":27}},"2":{"start":{"line":17,"column":0},"end":{"line":27,"column":2}},"3":{"start":{"line":18,"column":2},"end":{"line":18,"column":47}},"4":{"start":{"line":20,"column":2},"end":{"line":20,"column":51}},"5":{"start":{"line":22,"column":2},"end":{"line":22,"column":54}},"6":{"start":{"line":23,"column":2},"end":{"line":23,"column":38}},"7":{"start":{"line":24,"column":2},"end":{"line":24,"column":36}},"8":{"start":{"line":26,"column":2},"end":{"line":26,"column":39}},"9":{"start":{"line":29,"column":18},"end":{"line":29,"column":32}},"10":{"start":{"line":29,"column":48},"end":{"line":29,"column":64}},"11":{"start":{"line":33,"column":0},"end":{"line":48,"column":2}},"12":{"start":{"line":34,"column":13},"end":{"line":34,"column":28}},"13":{"start":{"line":36,"column":25},"end":{"line":36,"column":69}},"14":{"start":{"line":37,"column":25},"end":{"line":37,"column":69}},"15":{"start":{"line":39,"column":12},"end":{"line":39,"column":50}},"16":{"start":{"line":40,"column":12},"end":{"line":40,"column":53}},"17":{"start":{"line":42,"column":2},"end":{"line":42,"column":46}},"18":{"start":{"line":43,"column":2},"end":{"line":43,"column":51}},"19":{"start":{"line":45,"column":2},"end":{"line":45,"column":100}},"20":{"start":{"line":47,"column":2},"end":{"line":47,"column":75}},"21":{"start":{"line":57,"column":0},"end":{"line":139,"column":2}},"22":{"start":{"line":58,"column":2},"end":{"line":60,"column":3}},"23":{"start":{"line":59,"column":4},"end":{"line":59,"column":31}},"24":{"start":{"line":62,"column":18},"end":{"line":62,"column":33}},"25":{"start":{"line":62,"column":42},"end":{"line":62,"column":58}},"26":{"start":{"line":68,"column":2},"end":{"line":124,"column":3}},"27":{"start":{"line":69,"column":39},"end":{"line":69,"column":104}},"28":{"start":{"line":70,"column":23},"end":{"line":70,"column":64}},"29":{"start":{"line":71,"column":17},"end":{"line":71,"column":52}},"30":{"start":{"line":72,"column":18},"end":{"line":72,"column":54}},"31":{"start":{"line":74,"column":6},"end":{"line":74,"column":42}},"32":{"start":{"line":74,"column":24},"end":{"line":74,"column":42}},"33":{"start":{"line":75,"column":6},"end":{"line":75,"column":47}},"34":{"start":{"line":78,"column":6},"end":{"line":78,"column":42}},"35":{"start":{"line":78,"column":24},"end":{"line":78,"column":42}},"36":{"start":{"line":79,"column":6},"end":{"line":79,"column":32}},"37":{"start":{"line":80,"column":6},"end":{"line":80,"column":41}},"38":{"start":{"line":82,"column":17},"end":{"line":82,"column":52}},"39":{"start":{"line":83,"column":21},"end":{"line":83,"column":60}},"40":{"start":{"line":84,"column":21},"end":{"line":84,"column":60}},"41":{"start":{"line":85,"column":20},"end":{"line":85,"column":58}},"42":{"start":{"line":86,"column":18},"end":{"line":86,"column":54}},"43":{"start":{"line":90,"column":6},"end":{"line":90,"column":42}},"44":{"start":{"line":90,"column":24},"end":{"line":90,"column":42}},"45":{"start":{"line":93,"column":6},"end":{"line":93,"column":53}},"46":{"start":{"line":95,"column":20},"end":{"line":95,"column":58}},"47":{"start":{"line":96,"column":19},"end":{"line":96,"column":56}},"48":{"start":{"line":97,"column":20},"end":{"line":97,"column":45}},"49":{"start":{"line":98,"column":18},"end":{"line":98,"column":56}},"50":{"start":{"line":101,"column":6},"end":{"line":109,"column":7}},"51":{"start":{"line":102,"column":8},"end":{"line":104,"column":9}},"52":{"start":{"line":103,"column":10},"end":{"line":103,"column":97}},"53":{"start":{"line":106,"column":8},"end":{"line":108,"column":9}},"54":{"start":{"line":107,"column":10},"end":{"line":107,"column":106}},"55":{"start":{"line":110,"column":6},"end":{"line":110,"column":88}},"56":{"start":{"line":113,"column":6},"end":{"line":123,"column":7}},"57":{"start":{"line":115,"column":20},"end":{"line":115,"column":38}},"58":{"start":{"line":116,"column":8},"end":{"line":116,"column":20}},"59":{"start":{"line":117,"column":8},"end":{"line":122,"column":9}},"60":{"start":{"line":118,"column":10},"end":{"line":118,"column":36}},"61":{"start":{"line":119,"column":10},"end":{"line":119,"column":61}},"62":{"start":{"line":121,"column":10},"end":{"line":121,"column":29}},"63":{"start":{"line":131,"column":18},"end":{"line":131,"column":34}},"64":{"start":{"line":132,"column":13},"end":{"line":132,"column":35}},"65":{"start":{"line":134,"column":2},"end":{"line":138,"column":3}},"66":{"start":{"line":135,"column":4},"end":{"line":135,"column":61}},"67":{"start":{"line":137,"column":4},"end":{"line":137,"column":53}},"68":{"start":{"line":141,"column":0},"end":{"line":146,"column":2}},"69":{"start":{"line":142,"column":2},"end":{"line":145,"column":3}},"70":{"start":{"line":143,"column":4},"end":{"line":143,"column":44}},"71":{"start":{"line":144,"column":4},"end":{"line":144,"column":31}},"72":{"start":{"line":148,"column":0},"end":{"line":160,"column":2}},"73":{"start":{"line":149,"column":2},"end":{"line":151,"column":3}},"74":{"start":{"line":150,"column":4},"end":{"line":150,"column":54}},"75":{"start":{"line":153,"column":2},"end":{"line":155,"column":3}},"76":{"start":{"line":154,"column":4},"end":{"line":154,"column":11}},"77":{"start":{"line":157,"column":2},"end":{"line":159,"column":3}},"78":{"start":{"line":158,"column":4},"end":{"line":158,"column":95}},"79":{"start":{"line":162,"column":0},"end":{"line":170,"column":2}},"80":{"start":{"line":163,"column":2},"end":{"line":165,"column":3}},"81":{"start":{"line":164,"column":4},"end":{"line":164,"column":22}},"82":{"start":{"line":166,"column":13},"end":{"line":166,"column":29}},"83":{"start":{"line":167,"column":2},"end":{"line":167,"column":14}},"84":{"start":{"line":168,"column":2},"end":{"line":168,"column":44}},"85":{"start":{"line":169,"column":2},"end":{"line":169,"column":44}},"86":{"start":{"line":172,"column":0},"end":{"line":197,"column":2}},"87":{"start":{"line":173,"column":16},"end":{"line":173,"column":35}},"88":{"start":{"line":174,"column":2},"end":{"line":174,"column":14}},"89":{"start":{"line":176,"column":2},"end":{"line":183,"column":3}},"90":{"start":{"line":177,"column":4},"end":{"line":177,"column":22}},"91":{"start":{"line":178,"column":9},"end":{"line":183,"column":3}},"92":{"start":{"line":179,"column":4},"end":{"line":179,"column":22}},"93":{"start":{"line":181,"column":4},"end":{"line":181,"column":40}},"94":{"start":{"line":182,"column":4},"end":{"line":182,"column":21}},"95":{"start":{"line":188,"column":2},"end":{"line":194,"column":3}},"96":{"start":{"line":189,"column":14},"end":{"line":189,"column":34}},"97":{"start":{"line":190,"column":4},"end":{"line":193,"column":5}},"98":{"start":{"line":191,"column":6},"end":{"line":191,"column":70}},"99":{"start":{"line":191,"column":64},"end":{"line":191,"column":70}},"100":{"start":{"line":192,"column":6},"end":{"line":192,"column":39}},"101":{"start":{"line":192,"column":33},"end":{"line":192,"column":39}},"102":{"start":{"line":195,"column":2},"end":{"line":195,"column":87}},"103":{"start":{"line":195,"column":38},"end":{"line":195,"column":87}},"104":{"start":{"line":196,"column":2},"end":{"line":196,"column":81}},"105":{"start":{"line":199,"column":0},"end":{"line":203,"column":2}},"106":{"start":{"line":200,"column":2},"end":{"line":200,"column":14}},"107":{"start":{"line":201,"column":2},"end":{"line":201,"column":19}},"108":{"start":{"line":202,"column":2},"end":{"line":202,"column":52}},"109":{"start":{"line":205,"column":0},"end":{"line":214,"column":2}},"110":{"start":{"line":206,"column":2},"end":{"line":206,"column":14}},"111":{"start":{"line":207,"column":2},"end":{"line":207,"column":36}},"112":{"start":{"line":208,"column":2},"end":{"line":208,"column":41}},"113":{"start":{"line":209,"column":2},"end":{"line":209,"column":26}},"114":{"start":{"line":210,"column":2},"end":{"line":210,"column":25}},"115":{"start":{"line":211,"column":2},"end":{"line":211,"column":42}},"116":{"start":{"line":212,"column":2},"end":{"line":212,"column":20}},"117":{"start":{"line":213,"column":2},"end":{"line":213,"column":51}},"118":{"start":{"line":224,"column":0},"end":{"line":272,"column":2}},"119":{"start":{"line":225,"column":2},"end":{"line":225,"column":14}},"120":{"start":{"line":226,"column":2},"end":{"line":226,"column":36}},"121":{"start":{"line":228,"column":17},"end":{"line":228,"column":22}},"122":{"start":{"line":229,"column":2},"end":{"line":232,"column":3}},"123":{"start":{"line":230,"column":6},"end":{"line":230,"column":22}},"124":{"start":{"line":231,"column":6},"end":{"line":231,"column":18}},"125":{"start":{"line":233,"column":2},"end":{"line":233,"column":25}},"126":{"start":{"line":235,"column":2},"end":{"line":240,"column":3}},"127":{"start":{"line":236,"column":4},"end":{"line":238,"column":5}},"128":{"start":{"line":237,"column":6},"end":{"line":237,"column":24}},"129":{"start":{"line":239,"column":4},"end":{"line":239,"column":37}},"130":{"start":{"line":242,"column":2},"end":{"line":257,"column":3}},"131":{"start":{"line":243,"column":15},"end":{"line":243,"column":31}},"132":{"start":{"line":243,"column":43},"end":{"line":243,"column":58}},"133":{"start":{"line":244,"column":4},"end":{"line":244,"column":16}},"134":{"start":{"line":245,"column":4},"end":{"line":245,"column":39}},"135":{"start":{"line":246,"column":4},"end":{"line":246,"column":49}},"136":{"start":{"line":248,"column":4},"end":{"line":252,"column":5}},"137":{"start":{"line":249,"column":6},"end":{"line":251,"column":7}},"138":{"start":{"line":250,"column":8},"end":{"line":250,"column":53}},"139":{"start":{"line":253,"column":4},"end":{"line":255,"column":5}},"140":{"start":{"line":254,"column":6},"end":{"line":254,"column":24}},"141":{"start":{"line":256,"column":4},"end":{"line":256,"column":37}},"142":{"start":{"line":259,"column":31},"end":{"line":259,"column":41}},"143":{"start":{"line":260,"column":13},"end":{"line":260,"column":63}},"144":{"start":{"line":261,"column":2},"end":{"line":267,"column":3}},"145":{"start":{"line":262,"column":4},"end":{"line":262,"column":28}},"146":{"start":{"line":263,"column":4},"end":{"line":263,"column":25}},"147":{"start":{"line":264,"column":4},"end":{"line":264,"column":49}},"148":{"start":{"line":265,"column":9},"end":{"line":267,"column":3}},"149":{"start":{"line":266,"column":4},"end":{"line":266,"column":50}},"150":{"start":{"line":268,"column":2},"end":{"line":270,"column":3}},"151":{"start":{"line":269,"column":4},"end":{"line":269,"column":22}},"152":{"start":{"line":271,"column":2},"end":{"line":271,"column":35}},"153":{"start":{"line":274,"column":0},"end":{"line":277,"column":2}},"154":{"start":{"line":275,"column":2},"end":{"line":275,"column":14}},"155":{"start":{"line":276,"column":2},"end":{"line":276,"column":40}},"156":{"start":{"line":279,"column":0},"end":{"line":285,"column":2}},"157":{"start":{"line":280,"column":2},"end":{"line":280,"column":14}},"158":{"start":{"line":281,"column":2},"end":{"line":281,"column":42}},"159":{"start":{"line":282,"column":2},"end":{"line":282,"column":47}},"160":{"start":{"line":283,"column":2},"end":{"line":283,"column":74}},"161":{"start":{"line":284,"column":2},"end":{"line":284,"column":46}},"162":{"start":{"line":287,"column":0},"end":{"line":306,"column":2}},"163":{"start":{"line":288,"column":2},"end":{"line":290,"column":3}},"164":{"start":{"line":289,"column":4},"end":{"line":289,"column":65}},"165":{"start":{"line":292,"column":2},"end":{"line":292,"column":14}},"166":{"start":{"line":298,"column":2},"end":{"line":303,"column":3}},"167":{"start":{"line":299,"column":4},"end":{"line":299,"column":25}},"168":{"start":{"line":301,"column":4},"end":{"line":301,"column":43}},"169":{"start":{"line":302,"column":4},"end":{"line":302,"column":21}},"170":{"start":{"line":305,"column":2},"end":{"line":305,"column":50}},"171":{"start":{"line":308,"column":0},"end":{"line":347,"column":2}},"172":{"start":{"line":309,"column":2},"end":{"line":309,"column":14}},"173":{"start":{"line":310,"column":2},"end":{"line":310,"column":50}},"174":{"start":{"line":311,"column":2},"end":{"line":311,"column":18}},"175":{"start":{"line":312,"column":2},"end":{"line":312,"column":25}},"176":{"start":{"line":313,"column":2},"end":{"line":313,"column":38}},"177":{"start":{"line":320,"column":2},"end":{"line":342,"column":3}},"178":{"start":{"line":321,"column":4},"end":{"line":341,"column":5}},"179":{"start":{"line":322,"column":19},"end":{"line":322,"column":39}},"180":{"start":{"line":323,"column":6},"end":{"line":323,"column":50}},"181":{"start":{"line":323,"column":15},"end":{"line":323,"column":50}},"182":{"start":{"line":324,"column":6},"end":{"line":324,"column":46}},"183":{"start":{"line":325,"column":6},"end":{"line":325,"column":26}},"184":{"start":{"line":326,"column":6},"end":{"line":326,"column":18}},"185":{"start":{"line":327,"column":6},"end":{"line":333,"column":7}},"186":{"start":{"line":328,"column":8},"end":{"line":328,"column":42}},"187":{"start":{"line":330,"column":8},"end":{"line":330,"column":88}},"188":{"start":{"line":330,"column":24},"end":{"line":330,"column":88}},"189":{"start":{"line":331,"column":8},"end":{"line":331,"column":26}},"190":{"start":{"line":332,"column":8},"end":{"line":332,"column":24}},"191":{"start":{"line":334,"column":6},"end":{"line":334,"column":28}},"192":{"start":{"line":336,"column":6},"end":{"line":340,"column":7}},"193":{"start":{"line":337,"column":8},"end":{"line":337,"column":55}},"194":{"start":{"line":339,"column":8},"end":{"line":339,"column":26}},"195":{"start":{"line":343,"column":2},"end":{"line":343,"column":46}},"196":{"start":{"line":343,"column":11},"end":{"line":343,"column":46}},"197":{"start":{"line":344,"column":2},"end":{"line":344,"column":14}},"198":{"start":{"line":345,"column":2},"end":{"line":345,"column":26}},"199":{"start":{"line":346,"column":2},"end":{"line":346,"column":50}},"200":{"start":{"line":349,"column":0},"end":{"line":356,"column":2}},"201":{"start":{"line":350,"column":2},"end":{"line":350,"column":14}},"202":{"start":{"line":351,"column":2},"end":{"line":352,"column":69}},"203":{"start":{"line":352,"column":4},"end":{"line":352,"column":69}},"204":{"start":{"line":353,"column":2},"end":{"line":353,"column":41}},"205":{"start":{"line":354,"column":2},"end":{"line":354,"column":19}},"206":{"start":{"line":355,"column":2},"end":{"line":355,"column":49}},"207":{"start":{"line":360,"column":12},"end":{"line":360,"column":14}},"208":{"start":{"line":362,"column":0},"end":{"line":389,"column":2}},"209":{"start":{"line":363,"column":2},"end":{"line":363,"column":14}},"210":{"start":{"line":365,"column":2},"end":{"line":365,"column":33}},"211":{"start":{"line":366,"column":2},"end":{"line":366,"column":22}},"212":{"start":{"line":368,"column":2},"end":{"line":379,"column":3}},"213":{"start":{"line":369,"column":17},"end":{"line":369,"column":33}},"214":{"start":{"line":370,"column":4},"end":{"line":370,"column":16}},"215":{"start":{"line":372,"column":4},"end":{"line":372,"column":27}},"216":{"start":{"line":373,"column":4},"end":{"line":373,"column":43}},"217":{"start":{"line":374,"column":4},"end":{"line":374,"column":60}},"218":{"start":{"line":375,"column":4},"end":{"line":375,"column":27}},"219":{"start":{"line":377,"column":4},"end":{"line":377,"column":36}},"220":{"start":{"line":378,"column":4},"end":{"line":378,"column":58}},"221":{"start":{"line":381,"column":2},"end":{"line":381,"column":31}},"222":{"start":{"line":382,"column":2},"end":{"line":382,"column":68}},"223":{"start":{"line":384,"column":2},"end":{"line":386,"column":3}},"224":{"start":{"line":385,"column":4},"end":{"line":385,"column":62}},"225":{"start":{"line":388,"column":2},"end":{"line":388,"column":47}},"226":{"start":{"line":391,"column":0},"end":{"line":396,"column":2}},"227":{"start":{"line":392,"column":2},"end":{"line":392,"column":14}},"228":{"start":{"line":393,"column":2},"end":{"line":393,"column":35}},"229":{"start":{"line":394,"column":2},"end":{"line":394,"column":19}},"230":{"start":{"line":395,"column":2},"end":{"line":395,"column":54}},"231":{"start":{"line":398,"column":0},"end":{"line":405,"column":2}},"232":{"start":{"line":399,"column":2},"end":{"line":399,"column":14}},"233":{"start":{"line":400,"column":2},"end":{"line":400,"column":42}},"234":{"start":{"line":401,"column":2},"end":{"line":401,"column":36}},"235":{"start":{"line":402,"column":2},"end":{"line":402,"column":41}},"236":{"start":{"line":403,"column":2},"end":{"line":403,"column":26}},"237":{"start":{"line":404,"column":2},"end":{"line":404,"column":49}},"238":{"start":{"line":407,"column":0},"end":{"line":413,"column":2}},"239":{"start":{"line":408,"column":2},"end":{"line":408,"column":79}},"240":{"start":{"line":408,"column":25},"end":{"line":408,"column":79}},"241":{"start":{"line":409,"column":2},"end":{"line":409,"column":14}},"242":{"start":{"line":410,"column":2},"end":{"line":410,"column":44}},"243":{"start":{"line":411,"column":2},"end":{"line":411,"column":41}},"244":{"start":{"line":412,"column":2},"end":{"line":412,"column":48}},"245":{"start":{"line":415,"column":0},"end":{"line":418,"column":2}},"246":{"start":{"line":416,"column":2},"end":{"line":416,"column":14}},"247":{"start":{"line":417,"column":2},"end":{"line":417,"column":49}},"248":{"start":{"line":420,"column":0},"end":{"line":443,"column":2}},"249":{"start":{"line":421,"column":2},"end":{"line":425,"column":3}},"250":{"start":{"line":422,"column":4},"end":{"line":424,"column":5}},"251":{"start":{"line":423,"column":6},"end":{"line":423,"column":73}},"252":{"start":{"line":427,"column":13},"end":{"line":427,"column":87}},"253":{"start":{"line":428,"column":2},"end":{"line":436,"column":3}},"254":{"start":{"line":429,"column":16},"end":{"line":429,"column":36}},"255":{"start":{"line":430,"column":4},"end":{"line":435,"column":5}},"256":{"start":{"line":431,"column":6},"end":{"line":431,"column":46}},"257":{"start":{"line":432,"column":6},"end":{"line":432,"column":24}},"258":{"start":{"line":434,"column":6},"end":{"line":434,"column":12}},"259":{"start":{"line":438,"column":2},"end":{"line":438,"column":90}},"260":{"start":{"line":439,"column":2},"end":{"line":439,"column":40}},"261":{"start":{"line":440,"column":2},"end":{"line":440,"column":26}},"262":{"start":{"line":441,"column":2},"end":{"line":441,"column":20}},"263":{"start":{"line":442,"column":2},"end":{"line":442,"column":51}},"264":{"start":{"line":445,"column":0},"end":{"line":449,"column":2}},"265":{"start":{"line":446,"column":2},"end":{"line":446,"column":25}},"266":{"start":{"line":447,"column":2},"end":{"line":447,"column":19}},"267":{"start":{"line":448,"column":2},"end":{"line":448,"column":54}},"268":{"start":{"line":455,"column":0},"end":{"line":460,"column":2}},"269":{"start":{"line":456,"column":13},"end":{"line":456,"column":29}},"270":{"start":{"line":457,"column":2},"end":{"line":457,"column":25}},"271":{"start":{"line":458,"column":2},"end":{"line":458,"column":63}},"272":{"start":{"line":459,"column":2},"end":{"line":459,"column":49}},"273":{"start":{"line":464,"column":0},"end":{"line":504,"column":2}},"274":{"start":{"line":465,"column":2},"end":{"line":465,"column":17}},"275":{"start":{"line":466,"column":2},"end":{"line":466,"column":23}},"276":{"start":{"line":468,"column":27},"end":{"line":468,"column":32}},"277":{"start":{"line":472,"column":2},"end":{"line":499,"column":3}},"278":{"start":{"line":473,"column":4},"end":{"line":475,"column":5}},"279":{"start":{"line":474,"column":6},"end":{"line":474,"column":47}},"280":{"start":{"line":477,"column":15},"end":{"line":477,"column":50}},"281":{"start":{"line":479,"column":4},"end":{"line":495,"column":5}},"282":{"start":{"line":482,"column":22},"end":{"line":482,"column":48}},"283":{"start":{"line":483,"column":6},"end":{"line":483,"column":38}},"284":{"start":{"line":485,"column":6},"end":{"line":492,"column":7}},"285":{"start":{"line":486,"column":8},"end":{"line":486,"column":38}},"286":{"start":{"line":487,"column":8},"end":{"line":487,"column":29}},"287":{"start":{"line":489,"column":8},"end":{"line":491,"column":9}},"288":{"start":{"line":490,"column":10},"end":{"line":490,"column":68}},"289":{"start":{"line":494,"column":6},"end":{"line":494,"column":15}},"290":{"start":{"line":497,"column":4},"end":{"line":497,"column":30}},"291":{"start":{"line":498,"column":4},"end":{"line":498,"column":25}},"292":{"start":{"line":501,"column":2},"end":{"line":503,"column":3}},"293":{"start":{"line":502,"column":4},"end":{"line":502,"column":26}},"294":{"start":{"line":510,"column":0},"end":{"line":520,"column":2}},"295":{"start":{"line":511,"column":2},"end":{"line":511,"column":19}},"296":{"start":{"line":512,"column":2},"end":{"line":512,"column":23}},"297":{"start":{"line":513,"column":2},"end":{"line":513,"column":66}},"298":{"start":{"line":514,"column":2},"end":{"line":514,"column":23}},"299":{"start":{"line":515,"column":2},"end":{"line":515,"column":70}},"300":{"start":{"line":516,"column":2},"end":{"line":516,"column":25}},"301":{"start":{"line":517,"column":2},"end":{"line":517,"column":41}},"302":{"start":{"line":518,"column":2},"end":{"line":518,"column":26}},"303":{"start":{"line":519,"column":2},"end":{"line":519,"column":47}},"304":{"start":{"line":525,"column":0},"end":{"line":540,"column":2}},"305":{"start":{"line":527,"column":2},"end":{"line":533,"column":3}},"306":{"start":{"line":528,"column":4},"end":{"line":528,"column":29}},"307":{"start":{"line":529,"column":4},"end":{"line":529,"column":31}},"308":{"start":{"line":531,"column":4},"end":{"line":531,"column":68}},"309":{"start":{"line":532,"column":4},"end":{"line":532,"column":16}},"310":{"start":{"line":534,"column":2},"end":{"line":534,"column":19}},"311":{"start":{"line":535,"column":2},"end":{"line":535,"column":38}},"312":{"start":{"line":536,"column":2},"end":{"line":536,"column":25}},"313":{"start":{"line":537,"column":2},"end":{"line":537,"column":41}},"314":{"start":{"line":538,"column":2},"end":{"line":538,"column":26}},"315":{"start":{"line":539,"column":2},"end":{"line":539,"column":37}},"316":{"start":{"line":544,"column":0},"end":{"line":563,"column":2}},"317":{"start":{"line":545,"column":2},"end":{"line":545,"column":25}},"318":{"start":{"line":546,"column":2},"end":{"line":546,"column":27}},"319":{"start":{"line":547,"column":2},"end":{"line":561,"column":3}},"320":{"start":{"line":548,"column":15},"end":{"line":548,"column":31}},"321":{"start":{"line":549,"column":4},"end":{"line":549,"column":28}},"322":{"start":{"line":550,"column":4},"end":{"line":558,"column":5}},"323":{"start":{"line":551,"column":6},"end":{"line":551,"column":47}},"324":{"start":{"line":552,"column":11},"end":{"line":558,"column":5}},"325":{"start":{"line":553,"column":6},"end":{"line":553,"column":24}},"326":{"start":{"line":554,"column":11},"end":{"line":558,"column":5}},"327":{"start":{"line":555,"column":6},"end":{"line":555,"column":100}},"328":{"start":{"line":557,"column":6},"end":{"line":557,"column":23}},"329":{"start":{"line":559,"column":4},"end":{"line":559,"column":72}},"330":{"start":{"line":560,"column":4},"end":{"line":560,"column":35}},"331":{"start":{"line":560,"column":29},"end":{"line":560,"column":35}},"332":{"start":{"line":562,"column":2},"end":{"line":562,"column":14}},"333":{"start":{"line":565,"column":0},"end":{"line":568,"column":2}},"334":{"start":{"line":566,"column":2},"end":{"line":566,"column":36}},"335":{"start":{"line":567,"column":2},"end":{"line":567,"column":32}},"336":{"start":{"line":573,"column":0},"end":{"line":602,"column":2}},"337":{"start":{"line":574,"column":20},"end":{"line":574,"column":39}},"338":{"start":{"line":575,"column":2},"end":{"line":575,"column":30}},"339":{"start":{"line":577,"column":2},"end":{"line":577,"column":35}},"340":{"start":{"line":579,"column":2},"end":{"line":586,"column":3}},"341":{"start":{"line":580,"column":4},"end":{"line":585,"column":5}},"342":{"start":{"line":581,"column":6},"end":{"line":581,"column":24}},"343":{"start":{"line":583,"column":6},"end":{"line":583,"column":28}},"344":{"start":{"line":584,"column":6},"end":{"line":584,"column":18}},"345":{"start":{"line":588,"column":2},"end":{"line":590,"column":3}},"346":{"start":{"line":589,"column":4},"end":{"line":589,"column":22}},"347":{"start":{"line":592,"column":2},"end":{"line":594,"column":3}},"348":{"start":{"line":593,"column":4},"end":{"line":593,"column":44}},"349":{"start":{"line":596,"column":2},"end":{"line":596,"column":33}},"350":{"start":{"line":597,"column":2},"end":{"line":597,"column":52}},"351":{"start":{"line":599,"column":2},"end":{"line":599,"column":36}},"352":{"start":{"line":601,"column":2},"end":{"line":601,"column":91}},"353":{"start":{"line":604,"column":0},"end":{"line":607,"column":2}},"354":{"start":{"line":605,"column":2},"end":{"line":605,"column":25}},"355":{"start":{"line":606,"column":2},"end":{"line":606,"column":49}},"356":{"start":{"line":612,"column":0},"end":{"line":618,"column":2}},"357":{"start":{"line":613,"column":2},"end":{"line":613,"column":14}},"358":{"start":{"line":614,"column":2},"end":{"line":614,"column":51}},"359":{"start":{"line":615,"column":2},"end":{"line":615,"column":29}},"360":{"start":{"line":616,"column":2},"end":{"line":616,"column":28}},"361":{"start":{"line":617,"column":2},"end":{"line":617,"column":85}},"362":{"start":{"line":620,"column":0},"end":{"line":622,"column":2}},"363":{"start":{"line":621,"column":2},"end":{"line":621,"column":54}},"364":{"start":{"line":624,"column":0},"end":{"line":626,"column":2}},"365":{"start":{"line":625,"column":2},"end":{"line":625,"column":15}},"366":{"start":{"line":628,"column":0},"end":{"line":767,"column":2}},"367":{"start":{"line":630,"column":18},"end":{"line":630,"column":35}},"368":{"start":{"line":631,"column":2},"end":{"line":631,"column":27}},"369":{"start":{"line":633,"column":27},"end":{"line":633,"column":32}},"370":{"start":{"line":634,"column":23},"end":{"line":634,"column":28}},"371":{"start":{"line":635,"column":19},"end":{"line":635,"column":21}},"372":{"start":{"line":636,"column":18},"end":{"line":636,"column":34}},"373":{"start":{"line":638,"column":2},"end":{"line":638,"column":22}},"374":{"start":{"line":640,"column":2},"end":{"line":640,"column":25}},"375":{"start":{"line":642,"column":2},"end":{"line":758,"column":3}},"376":{"start":{"line":643,"column":4},"end":{"line":645,"column":5}},"377":{"start":{"line":644,"column":6},"end":{"line":644,"column":15}},"378":{"start":{"line":647,"column":4},"end":{"line":650,"column":5}},"379":{"start":{"line":648,"column":6},"end":{"line":648,"column":45}},"380":{"start":{"line":649,"column":6},"end":{"line":649,"column":15}},"381":{"start":{"line":652,"column":17},"end":{"line":652,"column":33}},"382":{"start":{"line":655,"column":4},"end":{"line":658,"column":5}},"383":{"start":{"line":656,"column":6},"end":{"line":656,"column":37}},"384":{"start":{"line":657,"column":6},"end":{"line":657,"column":22}},"385":{"start":{"line":660,"column":28},"end":{"line":660,"column":33}},"386":{"start":{"line":661,"column":24},"end":{"line":661,"column":76}},"387":{"start":{"line":662,"column":22},"end":{"line":662,"column":39}},"388":{"start":{"line":663,"column":19},"end":{"line":663,"column":24}},"389":{"start":{"line":664,"column":18},"end":{"line":664,"column":23}},"390":{"start":{"line":666,"column":4},"end":{"line":666,"column":35}},"391":{"start":{"line":668,"column":4},"end":{"line":668,"column":60}},"392":{"start":{"line":669,"column":4},"end":{"line":673,"column":5}},"393":{"start":{"line":670,"column":6},"end":{"line":670,"column":41}},"394":{"start":{"line":670,"column":23},"end":{"line":670,"column":41}},"395":{"start":{"line":671,"column":6},"end":{"line":671,"column":38}},"396":{"start":{"line":672,"column":6},"end":{"line":672,"column":37}},"397":{"start":{"line":675,"column":4},"end":{"line":685,"column":5}},"398":{"start":{"line":676,"column":6},"end":{"line":679,"column":7}},"399":{"start":{"line":677,"column":8},"end":{"line":677,"column":61}},"400":{"start":{"line":678,"column":8},"end":{"line":678,"column":17}},"401":{"start":{"line":681,"column":6},"end":{"line":684,"column":7}},"402":{"start":{"line":682,"column":8},"end":{"line":682,"column":33}},"403":{"start":{"line":683,"column":8},"end":{"line":683,"column":39}},"404":{"start":{"line":687,"column":24},"end":{"line":687,"column":133}},"405":{"start":{"line":688,"column":4},"end":{"line":692,"column":5}},"406":{"start":{"line":689,"column":6},"end":{"line":689,"column":85}},"407":{"start":{"line":689,"column":66},"end":{"line":689,"column":85}},"408":{"start":{"line":690,"column":6},"end":{"line":690,"column":21}},"409":{"start":{"line":691,"column":6},"end":{"line":691,"column":37}},"410":{"start":{"line":694,"column":4},"end":{"line":694,"column":27}},"411":{"start":{"line":696,"column":4},"end":{"line":729,"column":5}},"412":{"start":{"line":697,"column":20},"end":{"line":697,"column":26}},"413":{"start":{"line":701,"column":6},"end":{"line":705,"column":7}},"414":{"start":{"line":702,"column":8},"end":{"line":702,"column":24}},"415":{"start":{"line":703,"column":8},"end":{"line":703,"column":31}},"416":{"start":{"line":704,"column":8},"end":{"line":704,"column":45}},"417":{"start":{"line":708,"column":26},"end":{"line":711,"column":7}},"418":{"start":{"line":712,"column":6},"end":{"line":719,"column":7}},"419":{"start":{"line":713,"column":8},"end":{"line":713,"column":93}},"420":{"start":{"line":713,"column":28},"end":{"line":713,"column":93}},"421":{"start":{"line":714,"column":8},"end":{"line":714,"column":87}},"422":{"start":{"line":714,"column":22},"end":{"line":714,"column":87}},"423":{"start":{"line":715,"column":8},"end":{"line":715,"column":83}},"424":{"start":{"line":715,"column":25},"end":{"line":715,"column":83}},"425":{"start":{"line":716,"column":8},"end":{"line":716,"column":85}},"426":{"start":{"line":716,"column":21},"end":{"line":716,"column":85}},"427":{"start":{"line":717,"column":8},"end":{"line":717,"column":36}},"428":{"start":{"line":718,"column":8},"end":{"line":718,"column":30}},"429":{"start":{"line":722,"column":30},"end":{"line":725,"column":7}},"430":{"start":{"line":726,"column":6},"end":{"line":728,"column":7}},"431":{"start":{"line":727,"column":8},"end":{"line":727,"column":86}},"432":{"start":{"line":732,"column":4},"end":{"line":736,"column":5}},"433":{"start":{"line":733,"column":6},"end":{"line":733,"column":103}},"434":{"start":{"line":733,"column":30},"end":{"line":733,"column":103}},"435":{"start":{"line":734,"column":6},"end":{"line":734,"column":38}},"436":{"start":{"line":735,"column":6},"end":{"line":735,"column":32}},"437":{"start":{"line":739,"column":4},"end":{"line":741,"column":5}},"438":{"start":{"line":740,"column":6},"end":{"line":740,"column":85}},"439":{"start":{"line":743,"column":4},"end":{"line":743,"column":67}},"440":{"start":{"line":747,"column":4},"end":{"line":757,"column":5}},"441":{"start":{"line":748,"column":23},"end":{"line":748,"column":52}},"442":{"start":{"line":749,"column":6},"end":{"line":756,"column":7}},"443":{"start":{"line":750,"column":20},"end":{"line":750,"column":32}},"444":{"start":{"line":751,"column":8},"end":{"line":755,"column":9}},"445":{"start":{"line":752,"column":10},"end":{"line":752,"column":60}},"446":{"start":{"line":754,"column":10},"end":{"line":754,"column":68}},"447":{"start":{"line":760,"column":2},"end":{"line":762,"column":3}},"448":{"start":{"line":761,"column":4},"end":{"line":761,"column":80}},"449":{"start":{"line":764,"column":2},"end":{"line":764,"column":54}},"450":{"start":{"line":766,"column":2},"end":{"line":766,"column":32}},"451":{"start":{"line":769,"column":0},"end":{"line":779,"column":2}},"452":{"start":{"line":770,"column":2},"end":{"line":776,"column":3}},"453":{"start":{"line":771,"column":4},"end":{"line":771,"column":62}},"454":{"start":{"line":771,"column":44},"end":{"line":771,"column":62}},"455":{"start":{"line":772,"column":4},"end":{"line":772,"column":16}},"456":{"start":{"line":773,"column":4},"end":{"line":773,"column":41}},"457":{"start":{"line":775,"column":4},"end":{"line":775,"column":22}},"458":{"start":{"line":777,"column":2},"end":{"line":777,"column":19}},"459":{"start":{"line":778,"column":2},"end":{"line":778,"column":48}},"460":{"start":{"line":781,"column":0},"end":{"line":784,"column":2}},"461":{"start":{"line":782,"column":2},"end":{"line":782,"column":49}},"462":{"start":{"line":783,"column":2},"end":{"line":783,"column":62}},"463":{"start":{"line":786,"column":0},"end":{"line":796,"column":2}},"464":{"start":{"line":787,"column":2},"end":{"line":795,"column":3}},"465":{"start":{"line":788,"column":4},"end":{"line":788,"column":37}},"466":{"start":{"line":790,"column":4},"end":{"line":794,"column":5}},"467":{"start":{"line":791,"column":6},"end":{"line":791,"column":21}},"468":{"start":{"line":793,"column":6},"end":{"line":793,"column":24}},"469":{"start":{"line":798,"column":0},"end":{"line":800,"column":2}},"470":{"start":{"line":799,"column":2},"end":{"line":799,"column":78}},"471":{"start":{"line":804,"column":0},"end":{"line":860,"column":2}},"472":{"start":{"line":805,"column":2},"end":{"line":805,"column":14}},"473":{"start":{"line":807,"column":2},"end":{"line":857,"column":3}},"474":{"start":{"line":808,"column":20},"end":{"line":808,"column":36}},"475":{"start":{"line":809,"column":4},"end":{"line":809,"column":16}},"476":{"start":{"line":810,"column":4},"end":{"line":818,"column":5}},"477":{"start":{"line":811,"column":6},"end":{"line":811,"column":50}},"478":{"start":{"line":812,"column":6},"end":{"line":812,"column":81}},"479":{"start":{"line":813,"column":6},"end":{"line":813,"column":44}},"480":{"start":{"line":814,"column":6},"end":{"line":814,"column":39}},"481":{"start":{"line":816,"column":6},"end":{"line":816,"column":39}},"482":{"start":{"line":817,"column":6},"end":{"line":817,"column":59}},"483":{"start":{"line":819,"column":9},"end":{"line":857,"column":3}},"484":{"start":{"line":820,"column":20},"end":{"line":820,"column":36}},"485":{"start":{"line":821,"column":4},"end":{"line":821,"column":52}},"486":{"start":{"line":822,"column":4},"end":{"line":822,"column":77}},"487":{"start":{"line":823,"column":4},"end":{"line":832,"column":5}},"488":{"start":{"line":824,"column":6},"end":{"line":824,"column":28}},"489":{"start":{"line":825,"column":22},"end":{"line":825,"column":38}},"490":{"start":{"line":826,"column":6},"end":{"line":826,"column":27}},"491":{"start":{"line":827,"column":6},"end":{"line":827,"column":34}},"492":{"start":{"line":828,"column":6},"end":{"line":828,"column":50}},"493":{"start":{"line":829,"column":6},"end":{"line":829,"column":83}},"494":{"start":{"line":831,"column":6},"end":{"line":831,"column":44}},"495":{"start":{"line":833,"column":4},"end":{"line":833,"column":37}},"496":{"start":{"line":834,"column":9},"end":{"line":857,"column":3}},"497":{"start":{"line":835,"column":15},"end":{"line":835,"column":31}},"498":{"start":{"line":836,"column":20},"end":{"line":836,"column":25}},"499":{"start":{"line":837,"column":4},"end":{"line":844,"column":5}},"500":{"start":{"line":838,"column":6},"end":{"line":838,"column":64}},"501":{"start":{"line":839,"column":11},"end":{"line":844,"column":5}},"502":{"start":{"line":840,"column":6},"end":{"line":840,"column":47}},"503":{"start":{"line":842,"column":6},"end":{"line":842,"column":23}},"504":{"start":{"line":843,"column":6},"end":{"line":843,"column":37}},"505":{"start":{"line":845,"column":4},"end":{"line":845,"column":28}},"506":{"start":{"line":846,"column":4},"end":{"line":846,"column":36}},"507":{"start":{"line":846,"column":19},"end":{"line":846,"column":36}},"508":{"start":{"line":847,"column":4},"end":{"line":847,"column":27}},"509":{"start":{"line":848,"column":4},"end":{"line":848,"column":61}},"510":{"start":{"line":849,"column":9},"end":{"line":857,"column":3}},"511":{"start":{"line":850,"column":4},"end":{"line":850,"column":25}},"512":{"start":{"line":851,"column":4},"end":{"line":851,"column":23}},"513":{"start":{"line":852,"column":4},"end":{"line":852,"column":57}},"514":{"start":{"line":854,"column":4},"end":{"line":854,"column":28}},"515":{"start":{"line":855,"column":4},"end":{"line":855,"column":51}},"516":{"start":{"line":856,"column":4},"end":{"line":856,"column":31}},"517":{"start":{"line":858,"column":2},"end":{"line":858,"column":25}},"518":{"start":{"line":859,"column":2},"end":{"line":859,"column":57}},"519":{"start":{"line":862,"column":0},"end":{"line":864,"column":2}},"520":{"start":{"line":863,"column":2},"end":{"line":863,"column":35}},"521":{"start":{"line":866,"column":0},"end":{"line":879,"column":2}},"522":{"start":{"line":867,"column":2},"end":{"line":871,"column":3}},"523":{"start":{"line":868,"column":4},"end":{"line":870,"column":44}},"524":{"start":{"line":873,"column":2},"end":{"line":875,"column":3}},"525":{"start":{"line":874,"column":4},"end":{"line":874,"column":17}},"526":{"start":{"line":877,"column":18},"end":{"line":877,"column":34}},"527":{"start":{"line":878,"column":2},"end":{"line":878,"column":99}},"528":{"start":{"line":881,"column":0},"end":{"line":885,"column":2}},"529":{"start":{"line":882,"column":2},"end":{"line":884,"column":3}},"530":{"start":{"line":883,"column":4},"end":{"line":883,"column":75}},"531":{"start":{"line":887,"column":0},"end":{"line":900,"column":2}},"532":{"start":{"line":888,"column":2},"end":{"line":897,"column":3}},"533":{"start":{"line":889,"column":4},"end":{"line":889,"column":83}},"534":{"start":{"line":890,"column":4},"end":{"line":890,"column":27}},"535":{"start":{"line":892,"column":4},"end":{"line":896,"column":5}},"536":{"start":{"line":893,"column":6},"end":{"line":893,"column":24}},"537":{"start":{"line":895,"column":6},"end":{"line":895,"column":25}},"538":{"start":{"line":899,"column":2},"end":{"line":899,"column":19}},"539":{"start":{"line":902,"column":0},"end":{"line":904,"column":2}},"540":{"start":{"line":903,"column":2},"end":{"line":903,"column":36}},"541":{"start":{"line":906,"column":0},"end":{"line":914,"column":2}},"542":{"start":{"line":907,"column":2},"end":{"line":913,"column":3}},"543":{"start":{"line":908,"column":18},"end":{"line":908,"column":131}},"544":{"start":{"line":909,"column":4},"end":{"line":911,"column":5}},"545":{"start":{"line":910,"column":6},"end":{"line":910,"column":96}},"546":{"start":{"line":912,"column":4},"end":{"line":912,"column":42}},"547":{"start":{"line":918,"column":0},"end":{"line":949,"column":2}},"548":{"start":{"line":919,"column":14},"end":{"line":919,"column":16}},"549":{"start":{"line":920,"column":14},"end":{"line":920,"column":18}},"550":{"start":{"line":924,"column":2},"end":{"line":924,"column":25}},"551":{"start":{"line":926,"column":2},"end":{"line":941,"column":3}},"552":{"start":{"line":927,"column":4},"end":{"line":932,"column":5}},"553":{"start":{"line":928,"column":6},"end":{"line":928,"column":20}},"554":{"start":{"line":930,"column":6},"end":{"line":930,"column":28}},"555":{"start":{"line":931,"column":6},"end":{"line":931,"column":37}},"556":{"start":{"line":931,"column":31},"end":{"line":931,"column":37}},"557":{"start":{"line":934,"column":20},"end":{"line":934,"column":43}},"558":{"start":{"line":935,"column":4},"end":{"line":935,"column":50}},"559":{"start":{"line":935,"column":33},"end":{"line":935,"column":50}},"560":{"start":{"line":937,"column":15},"end":{"line":937,"column":31}},"561":{"start":{"line":938,"column":4},"end":{"line":938,"column":49}},"562":{"start":{"line":939,"column":4},"end":{"line":939,"column":97}},"563":{"start":{"line":940,"column":4},"end":{"line":940,"column":57}},"564":{"start":{"line":944,"column":2},"end":{"line":946,"column":3}},"565":{"start":{"line":945,"column":4},"end":{"line":945,"column":22}},"566":{"start":{"line":948,"column":2},"end":{"line":948,"column":15}},"567":{"start":{"line":953,"column":0},"end":{"line":968,"column":2}},"568":{"start":{"line":954,"column":2},"end":{"line":954,"column":14}},"569":{"start":{"line":957,"column":2},"end":{"line":965,"column":3}},"570":{"start":{"line":958,"column":4},"end":{"line":958,"column":25}},"571":{"start":{"line":959,"column":4},"end":{"line":959,"column":39}},"572":{"start":{"line":961,"column":4},"end":{"line":961,"column":25}},"573":{"start":{"line":962,"column":4},"end":{"line":962,"column":37}},"574":{"start":{"line":963,"column":4},"end":{"line":963,"column":34}},"575":{"start":{"line":964,"column":4},"end":{"line":964,"column":83}},"576":{"start":{"line":966,"column":2},"end":{"line":966,"column":19}},"577":{"start":{"line":967,"column":2},"end":{"line":967,"column":52}},"578":{"start":{"line":972,"column":0},"end":{"line":1006,"column":2}},"579":{"start":{"line":973,"column":14},"end":{"line":973,"column":18}},"580":{"start":{"line":974,"column":2},"end":{"line":979,"column":3}},"581":{"start":{"line":976,"column":19},"end":{"line":976,"column":35}},"582":{"start":{"line":976,"column":48},"end":{"line":976,"column":67}},"583":{"start":{"line":977,"column":4},"end":{"line":977,"column":103}},"584":{"start":{"line":978,"column":4},"end":{"line":978,"column":36}},"585":{"start":{"line":978,"column":29},"end":{"line":978,"column":36}},"586":{"start":{"line":981,"column":2},"end":{"line":989,"column":3}},"587":{"start":{"line":982,"column":20},"end":{"line":982,"column":36}},"588":{"start":{"line":983,"column":4},"end":{"line":983,"column":16}},"589":{"start":{"line":984,"column":4},"end":{"line":984,"column":32}},"590":{"start":{"line":985,"column":4},"end":{"line":985,"column":45}},"591":{"start":{"line":986,"column":4},"end":{"line":986,"column":42}},"592":{"start":{"line":987,"column":4},"end":{"line":987,"column":81}},"593":{"start":{"line":988,"column":4},"end":{"line":988,"column":11}},"594":{"start":{"line":991,"column":2},"end":{"line":991,"column":25}},"595":{"start":{"line":992,"column":2},"end":{"line":1005,"column":3}},"596":{"start":{"line":993,"column":4},"end":{"line":998,"column":5}},"597":{"start":{"line":994,"column":6},"end":{"line":994,"column":20}},"598":{"start":{"line":996,"column":6},"end":{"line":996,"column":28}},"599":{"start":{"line":997,"column":6},"end":{"line":997,"column":37}},"600":{"start":{"line":997,"column":31},"end":{"line":997,"column":37}},"601":{"start":{"line":1000,"column":20},"end":{"line":1000,"column":36}},"602":{"start":{"line":1001,"column":4},"end":{"line":1001,"column":52}},"603":{"start":{"line":1002,"column":4},"end":{"line":1002,"column":103}},"604":{"start":{"line":1003,"column":4},"end":{"line":1003,"column":42}},"605":{"start":{"line":1004,"column":4},"end":{"line":1004,"column":72}},"606":{"start":{"line":1008,"column":0},"end":{"line":1013,"column":2}},"607":{"start":{"line":1009,"column":13},"end":{"line":1009,"column":49}},"608":{"start":{"line":1010,"column":2},"end":{"line":1010,"column":18}},"609":{"start":{"line":1011,"column":2},"end":{"line":1011,"column":35}},"610":{"start":{"line":1012,"column":2},"end":{"line":1012,"column":57}}},"fnMap":{"1":{"name":"(anonymous_1)","decl":{"start":{"line":17,"column":19},"end":{"line":17,"column":20}},"loc":{"start":{"line":17,"column":44},"end":{"line":27,"column":1}}},"2":{"name":"(anonymous_2)","decl":{"start":{"line":33,"column":21},"end":{"line":33,"column":22}},"loc":{"start":{"line":33,"column":37},"end":{"line":48,"column":1}}},"3":{"name":"(anonymous_3)","decl":{"start":{"line":57,"column":20},"end":{"line":57,"column":21}},"loc":{"start":{"line":57,"column":53},"end":{"line":139,"column":1}}},"4":{"name":"(anonymous_4)","decl":{"start":{"line":141,"column":20},"end":{"line":141,"column":21}},"loc":{"start":{"line":141,"column":36},"end":{"line":146,"column":1}}},"5":{"name":"(anonymous_5)","decl":{"start":{"line":148,"column":21},"end":{"line":148,"column":22}},"loc":{"start":{"line":148,"column":44},"end":{"line":160,"column":1}}},"6":{"name":"(anonymous_6)","decl":{"start":{"line":162,"column":20},"end":{"line":162,"column":21}},"loc":{"start":{"line":162,"column":32},"end":{"line":170,"column":1}}},"7":{"name":"(anonymous_7)","decl":{"start":{"line":172,"column":33},"end":{"line":172,"column":34}},"loc":{"start":{"line":172,"column":58},"end":{"line":197,"column":1}}},"8":{"name":"(anonymous_8)","decl":{"start":{"line":199,"column":28},"end":{"line":199,"column":29}},"loc":{"start":{"line":199,"column":44},"end":{"line":203,"column":1}}},"9":{"name":"(anonymous_9)","decl":{"start":{"line":205,"column":22},"end":{"line":205,"column":23}},"loc":{"start":{"line":205,"column":38},"end":{"line":214,"column":1}}},"10":{"name":"(anonymous_10)","decl":{"start":{"line":224,"column":23},"end":{"line":224,"column":24}},"loc":{"start":{"line":224,"column":39},"end":{"line":272,"column":1}}},"11":{"name":"(anonymous_11)","decl":{"start":{"line":274,"column":28},"end":{"line":274,"column":29}},"loc":{"start":{"line":274,"column":44},"end":{"line":277,"column":1}}},"12":{"name":"(anonymous_12)","decl":{"start":{"line":279,"column":22},"end":{"line":279,"column":23}},"loc":{"start":{"line":279,"column":38},"end":{"line":285,"column":1}}},"13":{"name":"(anonymous_13)","decl":{"start":{"line":287,"column":26},"end":{"line":287,"column":27}},"loc":{"start":{"line":287,"column":42},"end":{"line":306,"column":1}}},"14":{"name":"(anonymous_14)","decl":{"start":{"line":308,"column":26},"end":{"line":308,"column":27}},"loc":{"start":{"line":308,"column":42},"end":{"line":347,"column":1}}},"15":{"name":"(anonymous_15)","decl":{"start":{"line":349,"column":25},"end":{"line":349,"column":26}},"loc":{"start":{"line":349,"column":41},"end":{"line":356,"column":1}}},"16":{"name":"(anonymous_16)","decl":{"start":{"line":362,"column":23},"end":{"line":362,"column":24}},"loc":{"start":{"line":362,"column":39},"end":{"line":389,"column":1}}},"17":{"name":"(anonymous_17)","decl":{"start":{"line":391,"column":23},"end":{"line":391,"column":24}},"loc":{"start":{"line":391,"column":45},"end":{"line":396,"column":1}}},"18":{"name":"(anonymous_18)","decl":{"start":{"line":398,"column":25},"end":{"line":398,"column":26}},"loc":{"start":{"line":398,"column":41},"end":{"line":405,"column":1}}},"19":{"name":"(anonymous_19)","decl":{"start":{"line":407,"column":24},"end":{"line":407,"column":25}},"loc":{"start":{"line":407,"column":40},"end":{"line":413,"column":1}}},"20":{"name":"(anonymous_20)","decl":{"start":{"line":415,"column":25},"end":{"line":415,"column":26}},"loc":{"start":{"line":415,"column":41},"end":{"line":418,"column":1}}},"21":{"name":"(anonymous_21)","decl":{"start":{"line":420,"column":27},"end":{"line":420,"column":28}},"loc":{"start":{"line":420,"column":60},"end":{"line":443,"column":1}}},"22":{"name":"(anonymous_22)","decl":{"start":{"line":445,"column":30},"end":{"line":445,"column":31}},"loc":{"start":{"line":445,"column":52},"end":{"line":449,"column":1}}},"23":{"name":"(anonymous_23)","decl":{"start":{"line":455,"column":16},"end":{"line":455,"column":17}},"loc":{"start":{"line":455,"column":44},"end":{"line":460,"column":1}}},"24":{"name":"(anonymous_24)","decl":{"start":{"line":464,"column":20},"end":{"line":464,"column":21}},"loc":{"start":{"line":464,"column":68},"end":{"line":504,"column":1}}},"25":{"name":"(anonymous_25)","decl":{"start":{"line":510,"column":14},"end":{"line":510,"column":15}},"loc":{"start":{"line":510,"column":36},"end":{"line":520,"column":1}}},"26":{"name":"(anonymous_26)","decl":{"start":{"line":525,"column":16},"end":{"line":525,"column":17}},"loc":{"start":{"line":525,"column":48},"end":{"line":540,"column":1}}},"27":{"name":"(anonymous_27)","decl":{"start":{"line":544,"column":14},"end":{"line":544,"column":15}},"loc":{"start":{"line":544,"column":43},"end":{"line":563,"column":1}}},"28":{"name":"(anonymous_28)","decl":{"start":{"line":565,"column":18},"end":{"line":565,"column":19}},"loc":{"start":{"line":565,"column":34},"end":{"line":568,"column":1}}},"29":{"name":"(anonymous_29)","decl":{"start":{"line":573,"column":19},"end":{"line":573,"column":20}},"loc":{"start":{"line":573,"column":90},"end":{"line":602,"column":1}}},"30":{"name":"(anonymous_30)","decl":{"start":{"line":604,"column":25},"end":{"line":604,"column":26}},"loc":{"start":{"line":604,"column":41},"end":{"line":607,"column":1}}},"31":{"name":"(anonymous_31)","decl":{"start":{"line":612,"column":16},"end":{"line":612,"column":17}},"loc":{"start":{"line":612,"column":57},"end":{"line":618,"column":1}}},"32":{"name":"(anonymous_32)","decl":{"start":{"line":620,"column":21},"end":{"line":620,"column":22}},"loc":{"start":{"line":620,"column":33},"end":{"line":622,"column":1}}},"33":{"name":"(anonymous_33)","decl":{"start":{"line":624,"column":27},"end":{"line":624,"column":28}},"loc":{"start":{"line":624,"column":39},"end":{"line":626,"column":1}}},"34":{"name":"(anonymous_34)","decl":{"start":{"line":628,"column":20},"end":{"line":628,"column":21}},"loc":{"start":{"line":628,"column":36},"end":{"line":767,"column":1}}},"35":{"name":"(anonymous_35)","decl":{"start":{"line":769,"column":24},"end":{"line":769,"column":25}},"loc":{"start":{"line":769,"column":40},"end":{"line":779,"column":1}}},"36":{"name":"(anonymous_36)","decl":{"start":{"line":781,"column":22},"end":{"line":781,"column":23}},"loc":{"start":{"line":781,"column":73},"end":{"line":784,"column":1}}},"37":{"name":"(anonymous_37)","decl":{"start":{"line":786,"column":18},"end":{"line":786,"column":19}},"loc":{"start":{"line":786,"column":59},"end":{"line":796,"column":1}}},"38":{"name":"(anonymous_38)","decl":{"start":{"line":798,"column":21},"end":{"line":798,"column":22}},"loc":{"start":{"line":798,"column":37},"end":{"line":800,"column":1}}},"39":{"name":"(anonymous_39)","decl":{"start":{"line":804,"column":17},"end":{"line":804,"column":18}},"loc":{"start":{"line":804,"column":33},"end":{"line":860,"column":1}}},"40":{"name":"(anonymous_40)","decl":{"start":{"line":862,"column":28},"end":{"line":862,"column":29}},"loc":{"start":{"line":862,"column":40},"end":{"line":864,"column":1}}},"41":{"name":"(anonymous_41)","decl":{"start":{"line":866,"column":30},"end":{"line":866,"column":31}},"loc":{"start":{"line":866,"column":42},"end":{"line":879,"column":1}}},"42":{"name":"(anonymous_42)","decl":{"start":{"line":881,"column":32},"end":{"line":881,"column":33}},"loc":{"start":{"line":881,"column":48},"end":{"line":885,"column":1}}},"43":{"name":"(anonymous_43)","decl":{"start":{"line":887,"column":21},"end":{"line":887,"column":22}},"loc":{"start":{"line":887,"column":46},"end":{"line":900,"column":1}}},"44":{"name":"(anonymous_44)","decl":{"start":{"line":902,"column":34},"end":{"line":902,"column":35}},"loc":{"start":{"line":902,"column":46},"end":{"line":904,"column":1}}},"45":{"name":"(anonymous_45)","decl":{"start":{"line":906,"column":17},"end":{"line":906,"column":18}},"loc":{"start":{"line":906,"column":33},"end":{"line":914,"column":1}}},"46":{"name":"(anonymous_46)","decl":{"start":{"line":918,"column":27},"end":{"line":918,"column":28}},"loc":{"start":{"line":918,"column":39},"end":{"line":949,"column":1}}},"47":{"name":"(anonymous_47)","decl":{"start":{"line":953,"column":17},"end":{"line":953,"column":18}},"loc":{"start":{"line":953,"column":33},"end":{"line":968,"column":1}}},"48":{"name":"(anonymous_48)","decl":{"start":{"line":972,"column":27},"end":{"line":972,"column":28}},"loc":{"start":{"line":972,"column":43},"end":{"line":1006,"column":1}}},"49":{"name":"(anonymous_49)","decl":{"start":{"line":1008,"column":33},"end":{"line":1008,"column":34}},"loc":{"start":{"line":1008,"column":67},"end":{"line":1013,"column":1}}}},"branchMap":{"1":{"loc":{"start":{"line":58,"column":2},"end":{"line":60,"column":3}},"type":"if","locations":[{"start":{"line":58,"column":2},"end":{"line":60,"column":3}},{"start":{"line":58,"column":2},"end":{"line":60,"column":3}}]},"2":{"loc":{"start":{"line":68,"column":2},"end":{"line":124,"column":3}},"type":"switch","locations":[{"start":{"line":69,"column":4},"end":{"line":69,"column":19}},{"start":{"line":69,"column":20},"end":{"line":69,"column":104}},{"start":{"line":70,"column":4},"end":{"line":70,"column":64}},{"start":{"line":71,"column":4},"end":{"line":71,"column":52}},{"start":{"line":72,"column":4},"end":{"line":72,"column":54}},{"start":{"line":73,"column":4},"end":{"line":75,"column":47}},{"start":{"line":77,"column":4},"end":{"line":80,"column":41}},{"start":{"line":82,"column":4},"end":{"line":82,"column":52}},{"start":{"line":83,"column":4},"end":{"line":83,"column":60}},{"start":{"line":84,"column":4},"end":{"line":84,"column":60}},{"start":{"line":85,"column":4},"end":{"line":85,"column":58}},{"start":{"line":86,"column":4},"end":{"line":86,"column":54}},{"start":{"line":88,"column":4},"end":{"line":88,"column":17}},{"start":{"line":89,"column":4},"end":{"line":90,"column":42}},{"start":{"line":92,"column":4},"end":{"line":93,"column":53}},{"start":{"line":95,"column":4},"end":{"line":95,"column":58}},{"start":{"line":96,"column":4},"end":{"line":96,"column":56}},{"start":{"line":97,"column":4},"end":{"line":97,"column":45}},{"start":{"line":98,"column":4},"end":{"line":98,"column":56}},{"start":{"line":99,"column":4},"end":{"line":99,"column":20}},{"start":{"line":100,"column":4},"end":{"line":110,"column":88}},{"start":{"line":112,"column":4},"end":{"line":123,"column":7}}]},"3":{"loc":{"start":{"line":74,"column":6},"end":{"line":74,"column":42}},"type":"if","locations":[{"start":{"line":74,"column":6},"end":{"line":74,"column":42}},{"start":{"line":74,"column":6},"end":{"line":74,"column":42}}]},"4":{"loc":{"start":{"line":78,"column":6},"end":{"line":78,"column":42}},"type":"if","locations":[{"start":{"line":78,"column":6},"end":{"line":78,"column":42}},{"start":{"line":78,"column":6},"end":{"line":78,"column":42}}]},"5":{"loc":{"start":{"line":90,"column":6},"end":{"line":90,"column":42}},"type":"if","locations":[{"start":{"line":90,"column":6},"end":{"line":90,"column":42}},{"start":{"line":90,"column":6},"end":{"line":90,"column":42}}]},"6":{"loc":{"start":{"line":101,"column":6},"end":{"line":109,"column":7}},"type":"if","locations":[{"start":{"line":101,"column":6},"end":{"line":109,"column":7}},{"start":{"line":101,"column":6},"end":{"line":109,"column":7}}]},"7":{"loc":{"start":{"line":102,"column":8},"end":{"line":104,"column":9}},"type":"if","locations":[{"start":{"line":102,"column":8},"end":{"line":104,"column":9}},{"start":{"line":102,"column":8},"end":{"line":104,"column":9}}]},"8":{"loc":{"start":{"line":106,"column":8},"end":{"line":108,"column":9}},"type":"if","locations":[{"start":{"line":106,"column":8},"end":{"line":108,"column":9}},{"start":{"line":106,"column":8},"end":{"line":108,"column":9}}]},"9":{"loc":{"start":{"line":110,"column":13},"end":{"line":110,"column":87}},"type":"cond-expr","locations":[{"start":{"line":110,"column":40},"end":{"line":110,"column":62}},{"start":{"line":110,"column":65},"end":{"line":110,"column":87}}]},"10":{"loc":{"start":{"line":113,"column":6},"end":{"line":123,"column":7}},"type":"if","locations":[{"start":{"line":113,"column":6},"end":{"line":123,"column":7}},{"start":{"line":113,"column":6},"end":{"line":123,"column":7}}]},"11":{"loc":{"start":{"line":117,"column":8},"end":{"line":122,"column":9}},"type":"if","locations":[{"start":{"line":117,"column":8},"end":{"line":122,"column":9}},{"start":{"line":117,"column":8},"end":{"line":122,"column":9}}]},"12":{"loc":{"start":{"line":117,"column":12},"end":{"line":117,"column":66}},"type":"binary-expr","locations":[{"start":{"line":117,"column":12},"end":{"line":117,"column":36}},{"start":{"line":117,"column":40},"end":{"line":117,"column":66}}]},"13":{"loc":{"start":{"line":134,"column":2},"end":{"line":138,"column":3}},"type":"if","locations":[{"start":{"line":134,"column":2},"end":{"line":138,"column":3}},{"start":{"line":134,"column":2},"end":{"line":138,"column":3}}]},"14":{"loc":{"start":{"line":134,"column":6},"end":{"line":134,"column":79}},"type":"binary-expr","locations":[{"start":{"line":134,"column":6},"end":{"line":134,"column":27}},{"start":{"line":134,"column":31},"end":{"line":134,"column":57}},{"start":{"line":134,"column":61},"end":{"line":134,"column":79}}]},"15":{"loc":{"start":{"line":142,"column":2},"end":{"line":145,"column":3}},"type":"if","locations":[{"start":{"line":142,"column":2},"end":{"line":145,"column":3}},{"start":{"line":142,"column":2},"end":{"line":145,"column":3}}]},"16":{"loc":{"start":{"line":153,"column":2},"end":{"line":155,"column":3}},"type":"if","locations":[{"start":{"line":153,"column":2},"end":{"line":155,"column":3}},{"start":{"line":153,"column":2},"end":{"line":155,"column":3}}]},"17":{"loc":{"start":{"line":153,"column":6},"end":{"line":153,"column":43}},"type":"binary-expr","locations":[{"start":{"line":153,"column":6},"end":{"line":153,"column":17}},{"start":{"line":153,"column":21},"end":{"line":153,"column":43}}]},"18":{"loc":{"start":{"line":157,"column":2},"end":{"line":159,"column":3}},"type":"if","locations":[{"start":{"line":157,"column":2},"end":{"line":159,"column":3}},{"start":{"line":157,"column":2},"end":{"line":159,"column":3}}]},"19":{"loc":{"start":{"line":163,"column":2},"end":{"line":165,"column":3}},"type":"if","locations":[{"start":{"line":163,"column":2},"end":{"line":165,"column":3}},{"start":{"line":163,"column":2},"end":{"line":165,"column":3}}]},"20":{"loc":{"start":{"line":176,"column":2},"end":{"line":183,"column":3}},"type":"if","locations":[{"start":{"line":176,"column":2},"end":{"line":183,"column":3}},{"start":{"line":176,"column":2},"end":{"line":183,"column":3}}]},"21":{"loc":{"start":{"line":178,"column":9},"end":{"line":183,"column":3}},"type":"if","locations":[{"start":{"line":178,"column":9},"end":{"line":183,"column":3}},{"start":{"line":178,"column":9},"end":{"line":183,"column":3}}]},"22":{"loc":{"start":{"line":190,"column":4},"end":{"line":193,"column":5}},"type":"if","locations":[{"start":{"line":190,"column":4},"end":{"line":193,"column":5}},{"start":{"line":190,"column":4},"end":{"line":193,"column":5}}]},"23":{"loc":{"start":{"line":190,"column":8},"end":{"line":190,"column":58}},"type":"binary-expr","locations":[{"start":{"line":190,"column":8},"end":{"line":190,"column":26}},{"start":{"line":190,"column":30},"end":{"line":190,"column":58}}]},"24":{"loc":{"start":{"line":191,"column":6},"end":{"line":191,"column":70}},"type":"if","locations":[{"start":{"line":191,"column":6},"end":{"line":191,"column":70}},{"start":{"line":191,"column":6},"end":{"line":191,"column":70}}]},"25":{"loc":{"start":{"line":191,"column":10},"end":{"line":191,"column":62}},"type":"binary-expr","locations":[{"start":{"line":191,"column":10},"end":{"line":191,"column":26}},{"start":{"line":191,"column":31},"end":{"line":191,"column":38}},{"start":{"line":191,"column":42},"end":{"line":191,"column":61}}]},"26":{"loc":{"start":{"line":192,"column":6},"end":{"line":192,"column":39}},"type":"if","locations":[{"start":{"line":192,"column":6},"end":{"line":192,"column":39}},{"start":{"line":192,"column":6},"end":{"line":192,"column":39}}]},"27":{"loc":{"start":{"line":192,"column":10},"end":{"line":192,"column":31}},"type":"binary-expr","locations":[{"start":{"line":192,"column":10},"end":{"line":192,"column":20}},{"start":{"line":192,"column":24},"end":{"line":192,"column":31}}]},"28":{"loc":{"start":{"line":195,"column":2},"end":{"line":195,"column":87}},"type":"if","locations":[{"start":{"line":195,"column":2},"end":{"line":195,"column":87}},{"start":{"line":195,"column":2},"end":{"line":195,"column":87}}]},"29":{"loc":{"start":{"line":196,"column":31},"end":{"line":196,"column":79}},"type":"cond-expr","locations":[{"start":{"line":196,"column":41},"end":{"line":196,"column":57}},{"start":{"line":196,"column":60},"end":{"line":196,"column":79}}]},"30":{"loc":{"start":{"line":229,"column":2},"end":{"line":232,"column":3}},"type":"if","locations":[{"start":{"line":229,"column":2},"end":{"line":232,"column":3}},{"start":{"line":229,"column":2},"end":{"line":232,"column":3}}]},"31":{"loc":{"start":{"line":229,"column":6},"end":{"line":229,"column":91}},"type":"binary-expr","locations":[{"start":{"line":229,"column":6},"end":{"line":229,"column":39}},{"start":{"line":229,"column":43},"end":{"line":229,"column":61}},{"start":{"line":229,"column":65},"end":{"line":229,"column":91}}]},"32":{"loc":{"start":{"line":235,"column":2},"end":{"line":240,"column":3}},"type":"if","locations":[{"start":{"line":235,"column":2},"end":{"line":240,"column":3}},{"start":{"line":235,"column":2},"end":{"line":240,"column":3}}]},"33":{"loc":{"start":{"line":236,"column":4},"end":{"line":238,"column":5}},"type":"if","locations":[{"start":{"line":236,"column":4},"end":{"line":238,"column":5}},{"start":{"line":236,"column":4},"end":{"line":238,"column":5}}]},"34":{"loc":{"start":{"line":242,"column":2},"end":{"line":257,"column":3}},"type":"if","locations":[{"start":{"line":242,"column":2},"end":{"line":257,"column":3}},{"start":{"line":242,"column":2},"end":{"line":257,"column":3}}]},"35":{"loc":{"start":{"line":242,"column":6},"end":{"line":242,"column":73}},"type":"binary-expr","locations":[{"start":{"line":242,"column":6},"end":{"line":242,"column":25}},{"start":{"line":242,"column":29},"end":{"line":242,"column":48}},{"start":{"line":242,"column":52},"end":{"line":242,"column":73}}]},"36":{"loc":{"start":{"line":248,"column":4},"end":{"line":252,"column":5}},"type":"if","locations":[{"start":{"line":248,"column":4},"end":{"line":252,"column":5}},{"start":{"line":248,"column":4},"end":{"line":252,"column":5}}]},"37":{"loc":{"start":{"line":248,"column":8},"end":{"line":248,"column":53}},"type":"binary-expr","locations":[{"start":{"line":248,"column":8},"end":{"line":248,"column":26}},{"start":{"line":248,"column":30},"end":{"line":248,"column":53}}]},"38":{"loc":{"start":{"line":249,"column":6},"end":{"line":251,"column":7}},"type":"if","locations":[{"start":{"line":249,"column":6},"end":{"line":251,"column":7}},{"start":{"line":249,"column":6},"end":{"line":251,"column":7}}]},"39":{"loc":{"start":{"line":249,"column":10},"end":{"line":249,"column":70}},"type":"binary-expr","locations":[{"start":{"line":249,"column":10},"end":{"line":249,"column":40}},{"start":{"line":249,"column":44},"end":{"line":249,"column":70}}]},"40":{"loc":{"start":{"line":253,"column":4},"end":{"line":255,"column":5}},"type":"if","locations":[{"start":{"line":253,"column":4},"end":{"line":255,"column":5}},{"start":{"line":253,"column":4},"end":{"line":255,"column":5}}]},"41":{"loc":{"start":{"line":261,"column":2},"end":{"line":267,"column":3}},"type":"if","locations":[{"start":{"line":261,"column":2},"end":{"line":267,"column":3}},{"start":{"line":261,"column":2},"end":{"line":267,"column":3}}]},"42":{"loc":{"start":{"line":261,"column":6},"end":{"line":261,"column":51}},"type":"binary-expr","locations":[{"start":{"line":261,"column":6},"end":{"line":261,"column":24}},{"start":{"line":261,"column":28},"end":{"line":261,"column":51}}]},"43":{"loc":{"start":{"line":265,"column":9},"end":{"line":267,"column":3}},"type":"if","locations":[{"start":{"line":265,"column":9},"end":{"line":267,"column":3}},{"start":{"line":265,"column":9},"end":{"line":267,"column":3}}]},"44":{"loc":{"start":{"line":268,"column":2},"end":{"line":270,"column":3}},"type":"if","locations":[{"start":{"line":268,"column":2},"end":{"line":270,"column":3}},{"start":{"line":268,"column":2},"end":{"line":270,"column":3}}]},"45":{"loc":{"start":{"line":283,"column":19},"end":{"line":283,"column":73}},"type":"cond-expr","locations":[{"start":{"line":283,"column":40},"end":{"line":283,"column":66}},{"start":{"line":283,"column":69},"end":{"line":283,"column":73}}]},"46":{"loc":{"start":{"line":288,"column":2},"end":{"line":290,"column":3}},"type":"if","locations":[{"start":{"line":288,"column":2},"end":{"line":290,"column":3}},{"start":{"line":288,"column":2},"end":{"line":290,"column":3}}]},"47":{"loc":{"start":{"line":288,"column":6},"end":{"line":288,"column":72}},"type":"binary-expr","locations":[{"start":{"line":288,"column":6},"end":{"line":288,"column":28}},{"start":{"line":288,"column":32},"end":{"line":288,"column":72}}]},"48":{"loc":{"start":{"line":298,"column":2},"end":{"line":303,"column":3}},"type":"if","locations":[{"start":{"line":298,"column":2},"end":{"line":303,"column":3}},{"start":{"line":298,"column":2},"end":{"line":303,"column":3}}]},"49":{"loc":{"start":{"line":321,"column":4},"end":{"line":341,"column":5}},"type":"if","locations":[{"start":{"line":321,"column":4},"end":{"line":341,"column":5}},{"start":{"line":321,"column":4},"end":{"line":341,"column":5}}]},"50":{"loc":{"start":{"line":321,"column":8},"end":{"line":321,"column":55}},"type":"binary-expr","locations":[{"start":{"line":321,"column":8},"end":{"line":321,"column":28}},{"start":{"line":321,"column":32},"end":{"line":321,"column":55}}]},"51":{"loc":{"start":{"line":323,"column":6},"end":{"line":323,"column":50}},"type":"if","locations":[{"start":{"line":323,"column":6},"end":{"line":323,"column":50}},{"start":{"line":323,"column":6},"end":{"line":323,"column":50}}]},"52":{"loc":{"start":{"line":327,"column":6},"end":{"line":333,"column":7}},"type":"if","locations":[{"start":{"line":327,"column":6},"end":{"line":333,"column":7}},{"start":{"line":327,"column":6},"end":{"line":333,"column":7}}]},"53":{"loc":{"start":{"line":330,"column":8},"end":{"line":330,"column":88}},"type":"if","locations":[{"start":{"line":330,"column":8},"end":{"line":330,"column":88}},{"start":{"line":330,"column":8},"end":{"line":330,"column":88}}]},"54":{"loc":{"start":{"line":336,"column":6},"end":{"line":340,"column":7}},"type":"if","locations":[{"start":{"line":336,"column":6},"end":{"line":340,"column":7}},{"start":{"line":336,"column":6},"end":{"line":340,"column":7}}]},"55":{"loc":{"start":{"line":343,"column":2},"end":{"line":343,"column":46}},"type":"if","locations":[{"start":{"line":343,"column":2},"end":{"line":343,"column":46}},{"start":{"line":343,"column":2},"end":{"line":343,"column":46}}]},"56":{"loc":{"start":{"line":351,"column":2},"end":{"line":352,"column":69}},"type":"if","locations":[{"start":{"line":351,"column":2},"end":{"line":352,"column":69}},{"start":{"line":351,"column":2},"end":{"line":352,"column":69}}]},"57":{"loc":{"start":{"line":368,"column":2},"end":{"line":379,"column":3}},"type":"if","locations":[{"start":{"line":368,"column":2},"end":{"line":379,"column":3}},{"start":{"line":368,"column":2},"end":{"line":379,"column":3}}]},"58":{"loc":{"start":{"line":382,"column":19},"end":{"line":382,"column":67}},"type":"cond-expr","locations":[{"start":{"line":382,"column":43},"end":{"line":382,"column":60}},{"start":{"line":382,"column":63},"end":{"line":382,"column":67}}]},"59":{"loc":{"start":{"line":384,"column":2},"end":{"line":386,"column":3}},"type":"if","locations":[{"start":{"line":384,"column":2},"end":{"line":386,"column":3}},{"start":{"line":384,"column":2},"end":{"line":386,"column":3}}]},"60":{"loc":{"start":{"line":384,"column":6},"end":{"line":384,"column":38}},"type":"binary-expr","locations":[{"start":{"line":384,"column":6},"end":{"line":384,"column":19}},{"start":{"line":384,"column":23},"end":{"line":384,"column":38}}]},"61":{"loc":{"start":{"line":408,"column":2},"end":{"line":408,"column":79}},"type":"if","locations":[{"start":{"line":408,"column":2},"end":{"line":408,"column":79}},{"start":{"line":408,"column":2},"end":{"line":408,"column":79}}]},"62":{"loc":{"start":{"line":422,"column":4},"end":{"line":424,"column":5}},"type":"if","locations":[{"start":{"line":422,"column":4},"end":{"line":424,"column":5}},{"start":{"line":422,"column":4},"end":{"line":424,"column":5}}]},"63":{"loc":{"start":{"line":427,"column":13},"end":{"line":427,"column":87}},"type":"cond-expr","locations":[{"start":{"line":427,"column":38},"end":{"line":427,"column":44}},{"start":{"line":427,"column":47},"end":{"line":427,"column":87}}]},"64":{"loc":{"start":{"line":427,"column":47},"end":{"line":427,"column":87}},"type":"cond-expr","locations":[{"start":{"line":427,"column":72},"end":{"line":427,"column":80}},{"start":{"line":427,"column":83},"end":{"line":427,"column":87}}]},"65":{"loc":{"start":{"line":430,"column":4},"end":{"line":435,"column":5}},"type":"if","locations":[{"start":{"line":430,"column":4},"end":{"line":435,"column":5}},{"start":{"line":430,"column":4},"end":{"line":435,"column":5}}]},"66":{"loc":{"start":{"line":473,"column":4},"end":{"line":475,"column":5}},"type":"if","locations":[{"start":{"line":473,"column":4},"end":{"line":475,"column":5}},{"start":{"line":473,"column":4},"end":{"line":475,"column":5}}]},"67":{"loc":{"start":{"line":473,"column":8},"end":{"line":473,"column":73}},"type":"binary-expr","locations":[{"start":{"line":473,"column":8},"end":{"line":473,"column":27}},{"start":{"line":473,"column":31},"end":{"line":473,"column":55}},{"start":{"line":473,"column":59},"end":{"line":473,"column":73}}]},"68":{"loc":{"start":{"line":479,"column":4},"end":{"line":495,"column":5}},"type":"if","locations":[{"start":{"line":479,"column":4},"end":{"line":495,"column":5}},{"start":{"line":479,"column":4},"end":{"line":495,"column":5}}]},"69":{"loc":{"start":{"line":479,"column":8},"end":{"line":481,"column":44}},"type":"binary-expr","locations":[{"start":{"line":479,"column":8},"end":{"line":479,"column":23}},{"start":{"line":479,"column":27},"end":{"line":479,"column":46}},{"start":{"line":480,"column":8},"end":{"line":480,"column":43}},{"start":{"line":480,"column":47},"end":{"line":480,"column":87}},{"start":{"line":481,"column":8},"end":{"line":481,"column":44}}]},"70":{"loc":{"start":{"line":485,"column":6},"end":{"line":492,"column":7}},"type":"if","locations":[{"start":{"line":485,"column":6},"end":{"line":492,"column":7}},{"start":{"line":485,"column":6},"end":{"line":492,"column":7}}]},"71":{"loc":{"start":{"line":485,"column":10},"end":{"line":485,"column":75}},"type":"binary-expr","locations":[{"start":{"line":485,"column":10},"end":{"line":485,"column":33}},{"start":{"line":485,"column":37},"end":{"line":485,"column":75}}]},"72":{"loc":{"start":{"line":489,"column":8},"end":{"line":491,"column":9}},"type":"if","locations":[{"start":{"line":489,"column":8},"end":{"line":491,"column":9}},{"start":{"line":489,"column":8},"end":{"line":491,"column":9}}]},"73":{"loc":{"start":{"line":501,"column":2},"end":{"line":503,"column":3}},"type":"if","locations":[{"start":{"line":501,"column":2},"end":{"line":503,"column":3}},{"start":{"line":501,"column":2},"end":{"line":503,"column":3}}]},"74":{"loc":{"start":{"line":513,"column":14},"end":{"line":513,"column":65}},"type":"cond-expr","locations":[{"start":{"line":513,"column":36},"end":{"line":513,"column":40}},{"start":{"line":513,"column":43},"end":{"line":513,"column":65}}]},"75":{"loc":{"start":{"line":515,"column":16},"end":{"line":515,"column":69}},"type":"cond-expr","locations":[{"start":{"line":515,"column":40},"end":{"line":515,"column":44}},{"start":{"line":515,"column":47},"end":{"line":515,"column":69}}]},"76":{"loc":{"start":{"line":527,"column":2},"end":{"line":533,"column":3}},"type":"if","locations":[{"start":{"line":527,"column":2},"end":{"line":533,"column":3}},{"start":{"line":527,"column":2},"end":{"line":533,"column":3}}]},"77":{"loc":{"start":{"line":531,"column":11},"end":{"line":531,"column":67}},"type":"cond-expr","locations":[{"start":{"line":531,"column":32},"end":{"line":531,"column":48}},{"start":{"line":531,"column":51},"end":{"line":531,"column":67}}]},"78":{"loc":{"start":{"line":550,"column":4},"end":{"line":558,"column":5}},"type":"if","locations":[{"start":{"line":550,"column":4},"end":{"line":558,"column":5}},{"start":{"line":550,"column":4},"end":{"line":558,"column":5}}]},"79":{"loc":{"start":{"line":552,"column":11},"end":{"line":558,"column":5}},"type":"if","locations":[{"start":{"line":552,"column":11},"end":{"line":558,"column":5}},{"start":{"line":552,"column":11},"end":{"line":558,"column":5}}]},"80":{"loc":{"start":{"line":552,"column":15},"end":{"line":552,"column":85}},"type":"binary-expr","locations":[{"start":{"line":552,"column":15},"end":{"line":552,"column":33}},{"start":{"line":552,"column":37},"end":{"line":552,"column":85}}]},"81":{"loc":{"start":{"line":552,"column":39},"end":{"line":552,"column":84}},"type":"binary-expr","locations":[{"start":{"line":552,"column":39},"end":{"line":552,"column":57}},{"start":{"line":552,"column":61},"end":{"line":552,"column":84}}]},"82":{"loc":{"start":{"line":554,"column":11},"end":{"line":558,"column":5}},"type":"if","locations":[{"start":{"line":554,"column":11},"end":{"line":558,"column":5}},{"start":{"line":554,"column":11},"end":{"line":558,"column":5}}]},"83":{"loc":{"start":{"line":554,"column":15},"end":{"line":554,"column":107}},"type":"binary-expr","locations":[{"start":{"line":554,"column":15},"end":{"line":554,"column":44}},{"start":{"line":554,"column":48},"end":{"line":554,"column":107}}]},"84":{"loc":{"start":{"line":554,"column":50},"end":{"line":554,"column":106}},"type":"binary-expr","locations":[{"start":{"line":554,"column":50},"end":{"line":554,"column":55}},{"start":{"line":554,"column":60},"end":{"line":554,"column":78}},{"start":{"line":554,"column":82},"end":{"line":554,"column":105}}]},"85":{"loc":{"start":{"line":560,"column":4},"end":{"line":560,"column":35}},"type":"if","locations":[{"start":{"line":560,"column":4},"end":{"line":560,"column":35}},{"start":{"line":560,"column":4},"end":{"line":560,"column":35}}]},"86":{"loc":{"start":{"line":579,"column":2},"end":{"line":586,"column":3}},"type":"if","locations":[{"start":{"line":579,"column":2},"end":{"line":586,"column":3}},{"start":{"line":579,"column":2},"end":{"line":586,"column":3}}]},"87":{"loc":{"start":{"line":580,"column":4},"end":{"line":585,"column":5}},"type":"if","locations":[{"start":{"line":580,"column":4},"end":{"line":585,"column":5}},{"start":{"line":580,"column":4},"end":{"line":585,"column":5}}]},"88":{"loc":{"start":{"line":580,"column":8},"end":{"line":580,"column":56}},"type":"binary-expr","locations":[{"start":{"line":580,"column":8},"end":{"line":580,"column":18}},{"start":{"line":580,"column":22},"end":{"line":580,"column":56}}]},"89":{"loc":{"start":{"line":588,"column":2},"end":{"line":590,"column":3}},"type":"if","locations":[{"start":{"line":588,"column":2},"end":{"line":590,"column":3}},{"start":{"line":588,"column":2},"end":{"line":590,"column":3}}]},"90":{"loc":{"start":{"line":588,"column":6},"end":{"line":588,"column":82}},"type":"binary-expr","locations":[{"start":{"line":588,"column":6},"end":{"line":588,"column":17}},{"start":{"line":588,"column":21},"end":{"line":588,"column":32}},{"start":{"line":588,"column":36},"end":{"line":588,"column":56}},{"start":{"line":588,"column":60},"end":{"line":588,"column":82}}]},"91":{"loc":{"start":{"line":592,"column":2},"end":{"line":594,"column":3}},"type":"if","locations":[{"start":{"line":592,"column":2},"end":{"line":594,"column":3}},{"start":{"line":592,"column":2},"end":{"line":594,"column":3}}]},"92":{"loc":{"start":{"line":592,"column":6},"end":{"line":592,"column":50}},"type":"binary-expr","locations":[{"start":{"line":592,"column":6},"end":{"line":592,"column":25}},{"start":{"line":592,"column":29},"end":{"line":592,"column":50}}]},"93":{"loc":{"start":{"line":601,"column":31},"end":{"line":601,"column":89}},"type":"cond-expr","locations":[{"start":{"line":601,"column":45},"end":{"line":601,"column":66}},{"start":{"line":601,"column":69},"end":{"line":601,"column":89}}]},"94":{"loc":{"start":{"line":617,"column":31},"end":{"line":617,"column":83}},"type":"cond-expr","locations":[{"start":{"line":617,"column":45},"end":{"line":617,"column":63}},{"start":{"line":617,"column":66},"end":{"line":617,"column":83}}]},"95":{"loc":{"start":{"line":621,"column":9},"end":{"line":621,"column":53}},"type":"binary-expr","locations":[{"start":{"line":621,"column":9},"end":{"line":621,"column":26}},{"start":{"line":621,"column":30},"end":{"line":621,"column":53}}]},"96":{"loc":{"start":{"line":643,"column":4},"end":{"line":645,"column":5}},"type":"if","locations":[{"start":{"line":643,"column":4},"end":{"line":645,"column":5}},{"start":{"line":643,"column":4},"end":{"line":645,"column":5}}]},"97":{"loc":{"start":{"line":647,"column":4},"end":{"line":650,"column":5}},"type":"if","locations":[{"start":{"line":647,"column":4},"end":{"line":650,"column":5}},{"start":{"line":647,"column":4},"end":{"line":650,"column":5}}]},"98":{"loc":{"start":{"line":655,"column":4},"end":{"line":658,"column":5}},"type":"if","locations":[{"start":{"line":655,"column":4},"end":{"line":658,"column":5}},{"start":{"line":655,"column":4},"end":{"line":658,"column":5}}]},"99":{"loc":{"start":{"line":661,"column":24},"end":{"line":661,"column":76}},"type":"binary-expr","locations":[{"start":{"line":661,"column":24},"end":{"line":661,"column":43}},{"start":{"line":661,"column":47},"end":{"line":661,"column":76}}]},"100":{"loc":{"start":{"line":668,"column":20},"end":{"line":668,"column":59}},"type":"binary-expr","locations":[{"start":{"line":668,"column":20},"end":{"line":668,"column":33}},{"start":{"line":668,"column":37},"end":{"line":668,"column":59}}]},"101":{"loc":{"start":{"line":669,"column":4},"end":{"line":673,"column":5}},"type":"if","locations":[{"start":{"line":669,"column":4},"end":{"line":673,"column":5}},{"start":{"line":669,"column":4},"end":{"line":673,"column":5}}]},"102":{"loc":{"start":{"line":670,"column":6},"end":{"line":670,"column":41}},"type":"if","locations":[{"start":{"line":670,"column":6},"end":{"line":670,"column":41}},{"start":{"line":670,"column":6},"end":{"line":670,"column":41}}]},"103":{"loc":{"start":{"line":675,"column":4},"end":{"line":685,"column":5}},"type":"if","locations":[{"start":{"line":675,"column":4},"end":{"line":685,"column":5}},{"start":{"line":675,"column":4},"end":{"line":685,"column":5}}]},"104":{"loc":{"start":{"line":675,"column":8},"end":{"line":675,"column":76}},"type":"binary-expr","locations":[{"start":{"line":675,"column":8},"end":{"line":675,"column":20}},{"start":{"line":675,"column":24},"end":{"line":675,"column":56}},{"start":{"line":675,"column":60},"end":{"line":675,"column":76}}]},"105":{"loc":{"start":{"line":676,"column":6},"end":{"line":679,"column":7}},"type":"if","locations":[{"start":{"line":676,"column":6},"end":{"line":679,"column":7}},{"start":{"line":676,"column":6},"end":{"line":679,"column":7}}]},"106":{"loc":{"start":{"line":681,"column":6},"end":{"line":684,"column":7}},"type":"if","locations":[{"start":{"line":681,"column":6},"end":{"line":684,"column":7}},{"start":{"line":681,"column":6},"end":{"line":684,"column":7}}]},"107":{"loc":{"start":{"line":681,"column":10},"end":{"line":681,"column":139}},"type":"binary-expr","locations":[{"start":{"line":681,"column":10},"end":{"line":681,"column":48}},{"start":{"line":681,"column":52},"end":{"line":681,"column":78}},{"start":{"line":681,"column":82},"end":{"line":681,"column":101}},{"start":{"line":681,"column":105},"end":{"line":681,"column":139}}]},"108":{"loc":{"start":{"line":687,"column":24},"end":{"line":687,"column":133}},"type":"binary-expr","locations":[{"start":{"line":687,"column":24},"end":{"line":687,"column":46}},{"start":{"line":687,"column":50},"end":{"line":687,"column":66}},{"start":{"line":687,"column":70},"end":{"line":687,"column":102}},{"start":{"line":687,"column":106},"end":{"line":687,"column":133}}]},"109":{"loc":{"start":{"line":688,"column":4},"end":{"line":692,"column":5}},"type":"if","locations":[{"start":{"line":688,"column":4},"end":{"line":692,"column":5}},{"start":{"line":688,"column":4},"end":{"line":692,"column":5}}]},"110":{"loc":{"start":{"line":689,"column":6},"end":{"line":689,"column":85}},"type":"if","locations":[{"start":{"line":689,"column":6},"end":{"line":689,"column":85}},{"start":{"line":689,"column":6},"end":{"line":689,"column":85}}]},"111":{"loc":{"start":{"line":689,"column":10},"end":{"line":689,"column":64}},"type":"binary-expr","locations":[{"start":{"line":689,"column":10},"end":{"line":689,"column":43}},{"start":{"line":689,"column":47},"end":{"line":689,"column":64}}]},"112":{"loc":{"start":{"line":696,"column":4},"end":{"line":729,"column":5}},"type":"if","locations":[{"start":{"line":696,"column":4},"end":{"line":729,"column":5}},{"start":{"line":696,"column":4},"end":{"line":729,"column":5}}]},"113":{"loc":{"start":{"line":701,"column":6},"end":{"line":705,"column":7}},"type":"if","locations":[{"start":{"line":701,"column":6},"end":{"line":705,"column":7}},{"start":{"line":701,"column":6},"end":{"line":705,"column":7}}]},"114":{"loc":{"start":{"line":701,"column":10},"end":{"line":701,"column":168}},"type":"binary-expr","locations":[{"start":{"line":701,"column":10},"end":{"line":701,"column":18}},{"start":{"line":701,"column":22},"end":{"line":701,"column":34}},{"start":{"line":701,"column":38},"end":{"line":701,"column":67}},{"start":{"line":701,"column":71},"end":{"line":701,"column":96}},{"start":{"line":701,"column":100},"end":{"line":701,"column":122}},{"start":{"line":701,"column":127},"end":{"line":701,"column":145}},{"start":{"line":701,"column":149},"end":{"line":701,"column":167}}]},"115":{"loc":{"start":{"line":708,"column":26},"end":{"line":711,"column":7}},"type":"binary-expr","locations":[{"start":{"line":708,"column":26},"end":{"line":708,"column":44}},{"start":{"line":708,"column":48},"end":{"line":708,"column":62}},{"start":{"line":709,"column":9},"end":{"line":709,"column":34}},{"start":{"line":709,"column":38},"end":{"line":709,"column":64}},{"start":{"line":710,"column":9},"end":{"line":710,"column":37}},{"start":{"line":710,"column":41},"end":{"line":710,"column":68}}]},"116":{"loc":{"start":{"line":712,"column":6},"end":{"line":719,"column":7}},"type":"if","locations":[{"start":{"line":712,"column":6},"end":{"line":719,"column":7}},{"start":{"line":712,"column":6},"end":{"line":719,"column":7}}]},"117":{"loc":{"start":{"line":713,"column":8},"end":{"line":713,"column":93}},"type":"if","locations":[{"start":{"line":713,"column":8},"end":{"line":713,"column":93}},{"start":{"line":713,"column":8},"end":{"line":713,"column":93}}]},"118":{"loc":{"start":{"line":714,"column":8},"end":{"line":714,"column":87}},"type":"if","locations":[{"start":{"line":714,"column":8},"end":{"line":714,"column":87}},{"start":{"line":714,"column":8},"end":{"line":714,"column":87}}]},"119":{"loc":{"start":{"line":715,"column":8},"end":{"line":715,"column":83}},"type":"if","locations":[{"start":{"line":715,"column":8},"end":{"line":715,"column":83}},{"start":{"line":715,"column":8},"end":{"line":715,"column":83}}]},"120":{"loc":{"start":{"line":716,"column":8},"end":{"line":716,"column":85}},"type":"if","locations":[{"start":{"line":716,"column":8},"end":{"line":716,"column":85}},{"start":{"line":716,"column":8},"end":{"line":716,"column":85}}]},"121":{"loc":{"start":{"line":722,"column":30},"end":{"line":725,"column":7}},"type":"binary-expr","locations":[{"start":{"line":722,"column":30},"end":{"line":722,"column":43}},{"start":{"line":723,"column":9},"end":{"line":723,"column":34}},{"start":{"line":723,"column":38},"end":{"line":723,"column":62}},{"start":{"line":724,"column":9},"end":{"line":724,"column":37}},{"start":{"line":724,"column":41},"end":{"line":724,"column":66}}]},"122":{"loc":{"start":{"line":726,"column":6},"end":{"line":728,"column":7}},"type":"if","locations":[{"start":{"line":726,"column":6},"end":{"line":728,"column":7}},{"start":{"line":726,"column":6},"end":{"line":728,"column":7}}]},"123":{"loc":{"start":{"line":732,"column":4},"end":{"line":736,"column":5}},"type":"if","locations":[{"start":{"line":732,"column":4},"end":{"line":736,"column":5}},{"start":{"line":732,"column":4},"end":{"line":736,"column":5}}]},"124":{"loc":{"start":{"line":733,"column":6},"end":{"line":733,"column":103}},"type":"if","locations":[{"start":{"line":733,"column":6},"end":{"line":733,"column":103}},{"start":{"line":733,"column":6},"end":{"line":733,"column":103}}]},"125":{"loc":{"start":{"line":739,"column":4},"end":{"line":741,"column":5}},"type":"if","locations":[{"start":{"line":739,"column":4},"end":{"line":741,"column":5}},{"start":{"line":739,"column":4},"end":{"line":741,"column":5}}]},"126":{"loc":{"start":{"line":739,"column":8},"end":{"line":739,"column":97}},"type":"binary-expr","locations":[{"start":{"line":739,"column":9},"end":{"line":739,"column":38}},{"start":{"line":739,"column":42},"end":{"line":739,"column":75}},{"start":{"line":739,"column":80},"end":{"line":739,"column":97}}]},"127":{"loc":{"start":{"line":747,"column":4},"end":{"line":757,"column":5}},"type":"if","locations":[{"start":{"line":747,"column":4},"end":{"line":757,"column":5}},{"start":{"line":747,"column":4},"end":{"line":757,"column":5}}]},"128":{"loc":{"start":{"line":748,"column":23},"end":{"line":748,"column":52}},"type":"cond-expr","locations":[{"start":{"line":748,"column":47},"end":{"line":748,"column":48}},{"start":{"line":748,"column":51},"end":{"line":748,"column":52}}]},"129":{"loc":{"start":{"line":749,"column":6},"end":{"line":756,"column":7}},"type":"if","locations":[{"start":{"line":749,"column":6},"end":{"line":756,"column":7}},{"start":{"line":749,"column":6},"end":{"line":756,"column":7}}]},"130":{"loc":{"start":{"line":751,"column":8},"end":{"line":755,"column":9}},"type":"if","locations":[{"start":{"line":751,"column":8},"end":{"line":755,"column":9}},{"start":{"line":751,"column":8},"end":{"line":755,"column":9}}]},"131":{"loc":{"start":{"line":760,"column":2},"end":{"line":762,"column":3}},"type":"if","locations":[{"start":{"line":760,"column":2},"end":{"line":762,"column":3}},{"start":{"line":760,"column":2},"end":{"line":762,"column":3}}]},"132":{"loc":{"start":{"line":770,"column":2},"end":{"line":776,"column":3}},"type":"if","locations":[{"start":{"line":770,"column":2},"end":{"line":776,"column":3}},{"start":{"line":770,"column":2},"end":{"line":776,"column":3}}]},"133":{"loc":{"start":{"line":771,"column":4},"end":{"line":771,"column":62}},"type":"if","locations":[{"start":{"line":771,"column":4},"end":{"line":771,"column":62}},{"start":{"line":771,"column":4},"end":{"line":771,"column":62}}]},"134":{"loc":{"start":{"line":787,"column":2},"end":{"line":795,"column":3}},"type":"if","locations":[{"start":{"line":787,"column":2},"end":{"line":795,"column":3}},{"start":{"line":787,"column":2},"end":{"line":795,"column":3}}]},"135":{"loc":{"start":{"line":790,"column":4},"end":{"line":794,"column":5}},"type":"if","locations":[{"start":{"line":790,"column":4},"end":{"line":794,"column":5}},{"start":{"line":790,"column":4},"end":{"line":794,"column":5}}]},"136":{"loc":{"start":{"line":790,"column":8},"end":{"line":790,"column":34}},"type":"binary-expr","locations":[{"start":{"line":790,"column":8},"end":{"line":790,"column":18}},{"start":{"line":790,"column":22},"end":{"line":790,"column":34}}]},"137":{"loc":{"start":{"line":799,"column":20},"end":{"line":799,"column":77}},"type":"cond-expr","locations":[{"start":{"line":799,"column":44},"end":{"line":799,"column":70}},{"start":{"line":799,"column":73},"end":{"line":799,"column":77}}]},"138":{"loc":{"start":{"line":807,"column":2},"end":{"line":857,"column":3}},"type":"if","locations":[{"start":{"line":807,"column":2},"end":{"line":857,"column":3}},{"start":{"line":807,"column":2},"end":{"line":857,"column":3}}]},"139":{"loc":{"start":{"line":810,"column":4},"end":{"line":818,"column":5}},"type":"if","locations":[{"start":{"line":810,"column":4},"end":{"line":818,"column":5}},{"start":{"line":810,"column":4},"end":{"line":818,"column":5}}]},"140":{"loc":{"start":{"line":810,"column":8},"end":{"line":810,"column":70}},"type":"binary-expr","locations":[{"start":{"line":810,"column":8},"end":{"line":810,"column":42}},{"start":{"line":810,"column":46},"end":{"line":810,"column":70}}]},"141":{"loc":{"start":{"line":819,"column":9},"end":{"line":857,"column":3}},"type":"if","locations":[{"start":{"line":819,"column":9},"end":{"line":857,"column":3}},{"start":{"line":819,"column":9},"end":{"line":857,"column":3}}]},"142":{"loc":{"start":{"line":819,"column":13},"end":{"line":819,"column":82}},"type":"binary-expr","locations":[{"start":{"line":819,"column":13},"end":{"line":819,"column":47}},{"start":{"line":819,"column":51},"end":{"line":819,"column":82}}]},"143":{"loc":{"start":{"line":823,"column":4},"end":{"line":832,"column":5}},"type":"if","locations":[{"start":{"line":823,"column":4},"end":{"line":832,"column":5}},{"start":{"line":823,"column":4},"end":{"line":832,"column":5}}]},"144":{"loc":{"start":{"line":823,"column":8},"end":{"line":823,"column":65}},"type":"binary-expr","locations":[{"start":{"line":823,"column":8},"end":{"line":823,"column":28}},{"start":{"line":823,"column":32},"end":{"line":823,"column":65}}]},"145":{"loc":{"start":{"line":834,"column":9},"end":{"line":857,"column":3}},"type":"if","locations":[{"start":{"line":834,"column":9},"end":{"line":857,"column":3}},{"start":{"line":834,"column":9},"end":{"line":857,"column":3}}]},"146":{"loc":{"start":{"line":837,"column":4},"end":{"line":844,"column":5}},"type":"if","locations":[{"start":{"line":837,"column":4},"end":{"line":844,"column":5}},{"start":{"line":837,"column":4},"end":{"line":844,"column":5}}]},"147":{"loc":{"start":{"line":839,"column":11},"end":{"line":844,"column":5}},"type":"if","locations":[{"start":{"line":839,"column":11},"end":{"line":844,"column":5}},{"start":{"line":839,"column":11},"end":{"line":844,"column":5}}]},"148":{"loc":{"start":{"line":846,"column":4},"end":{"line":846,"column":36}},"type":"if","locations":[{"start":{"line":846,"column":4},"end":{"line":846,"column":36}},{"start":{"line":846,"column":4},"end":{"line":846,"column":36}}]},"149":{"loc":{"start":{"line":849,"column":9},"end":{"line":857,"column":3}},"type":"if","locations":[{"start":{"line":849,"column":9},"end":{"line":857,"column":3}},{"start":{"line":849,"column":9},"end":{"line":857,"column":3}}]},"150":{"loc":{"start":{"line":849,"column":13},"end":{"line":849,"column":75}},"type":"binary-expr","locations":[{"start":{"line":849,"column":13},"end":{"line":849,"column":36}},{"start":{"line":849,"column":40},"end":{"line":849,"column":75}}]},"151":{"loc":{"start":{"line":867,"column":2},"end":{"line":871,"column":3}},"type":"if","locations":[{"start":{"line":867,"column":2},"end":{"line":871,"column":3}},{"start":{"line":867,"column":2},"end":{"line":871,"column":3}}]},"152":{"loc":{"start":{"line":868,"column":11},"end":{"line":870,"column":43}},"type":"binary-expr","locations":[{"start":{"line":868,"column":11},"end":{"line":868,"column":38}},{"start":{"line":869,"column":11},"end":{"line":869,"column":39}},{"start":{"line":870,"column":11},"end":{"line":870,"column":43}}]},"153":{"loc":{"start":{"line":873,"column":2},"end":{"line":875,"column":3}},"type":"if","locations":[{"start":{"line":873,"column":2},"end":{"line":875,"column":3}},{"start":{"line":873,"column":2},"end":{"line":875,"column":3}}]},"154":{"loc":{"start":{"line":878,"column":9},"end":{"line":878,"column":98}},"type":"binary-expr","locations":[{"start":{"line":878,"column":9},"end":{"line":878,"column":36}},{"start":{"line":878,"column":41},"end":{"line":878,"column":67}},{"start":{"line":878,"column":71},"end":{"line":878,"column":97}}]},"155":{"loc":{"start":{"line":882,"column":2},"end":{"line":884,"column":3}},"type":"if","locations":[{"start":{"line":882,"column":2},"end":{"line":884,"column":3}},{"start":{"line":882,"column":2},"end":{"line":884,"column":3}}]},"156":{"loc":{"start":{"line":888,"column":2},"end":{"line":897,"column":3}},"type":"if","locations":[{"start":{"line":888,"column":2},"end":{"line":897,"column":3}},{"start":{"line":888,"column":2},"end":{"line":897,"column":3}}]},"157":{"loc":{"start":{"line":889,"column":18},"end":{"line":889,"column":82}},"type":"cond-expr","locations":[{"start":{"line":889,"column":42},"end":{"line":889,"column":62}},{"start":{"line":889,"column":65},"end":{"line":889,"column":82}}]},"158":{"loc":{"start":{"line":892,"column":4},"end":{"line":896,"column":5}},"type":"if","locations":[{"start":{"line":892,"column":4},"end":{"line":896,"column":5}},{"start":{"line":892,"column":4},"end":{"line":896,"column":5}}]},"159":{"loc":{"start":{"line":907,"column":2},"end":{"line":913,"column":3}},"type":"if","locations":[{"start":{"line":907,"column":2},"end":{"line":913,"column":3}},{"start":{"line":907,"column":2},"end":{"line":913,"column":3}}]},"160":{"loc":{"start":{"line":908,"column":18},"end":{"line":908,"column":131}},"type":"binary-expr","locations":[{"start":{"line":908,"column":18},"end":{"line":908,"column":34}},{"start":{"line":908,"column":39},"end":{"line":908,"column":83}},{"start":{"line":908,"column":87},"end":{"line":908,"column":130}}]},"161":{"loc":{"start":{"line":909,"column":4},"end":{"line":911,"column":5}},"type":"if","locations":[{"start":{"line":909,"column":4},"end":{"line":911,"column":5}},{"start":{"line":909,"column":4},"end":{"line":911,"column":5}}]},"162":{"loc":{"start":{"line":909,"column":8},"end":{"line":909,"column":37}},"type":"binary-expr","locations":[{"start":{"line":909,"column":8},"end":{"line":909,"column":25}},{"start":{"line":909,"column":29},"end":{"line":909,"column":37}}]},"163":{"loc":{"start":{"line":927,"column":4},"end":{"line":932,"column":5}},"type":"if","locations":[{"start":{"line":927,"column":4},"end":{"line":932,"column":5}},{"start":{"line":927,"column":4},"end":{"line":932,"column":5}}]},"164":{"loc":{"start":{"line":931,"column":6},"end":{"line":931,"column":37}},"type":"if","locations":[{"start":{"line":931,"column":6},"end":{"line":931,"column":37}},{"start":{"line":931,"column":6},"end":{"line":931,"column":37}}]},"165":{"loc":{"start":{"line":935,"column":4},"end":{"line":935,"column":50}},"type":"if","locations":[{"start":{"line":935,"column":4},"end":{"line":935,"column":50}},{"start":{"line":935,"column":4},"end":{"line":935,"column":50}}]},"166":{"loc":{"start":{"line":935,"column":8},"end":{"line":935,"column":31}},"type":"binary-expr","locations":[{"start":{"line":935,"column":8},"end":{"line":935,"column":17}},{"start":{"line":935,"column":21},"end":{"line":935,"column":31}}]},"167":{"loc":{"start":{"line":939,"column":20},"end":{"line":939,"column":96}},"type":"cond-expr","locations":[{"start":{"line":939,"column":47},"end":{"line":939,"column":73}},{"start":{"line":939,"column":76},"end":{"line":939,"column":96}}]},"168":{"loc":{"start":{"line":944,"column":2},"end":{"line":946,"column":3}},"type":"if","locations":[{"start":{"line":944,"column":2},"end":{"line":946,"column":3}},{"start":{"line":944,"column":2},"end":{"line":946,"column":3}}]},"169":{"loc":{"start":{"line":944,"column":6},"end":{"line":944,"column":45}},"type":"binary-expr","locations":[{"start":{"line":944,"column":6},"end":{"line":944,"column":15}},{"start":{"line":944,"column":19},"end":{"line":944,"column":45}}]},"170":{"loc":{"start":{"line":957,"column":2},"end":{"line":965,"column":3}},"type":"if","locations":[{"start":{"line":957,"column":2},"end":{"line":965,"column":3}},{"start":{"line":957,"column":2},"end":{"line":965,"column":3}}]},"171":{"loc":{"start":{"line":964,"column":18},"end":{"line":964,"column":82}},"type":"cond-expr","locations":[{"start":{"line":964,"column":42},"end":{"line":964,"column":62}},{"start":{"line":964,"column":65},"end":{"line":964,"column":82}}]},"172":{"loc":{"start":{"line":974,"column":2},"end":{"line":979,"column":3}},"type":"if","locations":[{"start":{"line":974,"column":2},"end":{"line":979,"column":3}},{"start":{"line":974,"column":2},"end":{"line":979,"column":3}}]},"173":{"loc":{"start":{"line":978,"column":4},"end":{"line":978,"column":36}},"type":"if","locations":[{"start":{"line":978,"column":4},"end":{"line":978,"column":36}},{"start":{"line":978,"column":4},"end":{"line":978,"column":36}}]},"174":{"loc":{"start":{"line":981,"column":2},"end":{"line":989,"column":3}},"type":"if","locations":[{"start":{"line":981,"column":2},"end":{"line":989,"column":3}},{"start":{"line":981,"column":2},"end":{"line":989,"column":3}}]},"175":{"loc":{"start":{"line":993,"column":4},"end":{"line":998,"column":5}},"type":"if","locations":[{"start":{"line":993,"column":4},"end":{"line":998,"column":5}},{"start":{"line":993,"column":4},"end":{"line":998,"column":5}}]},"176":{"loc":{"start":{"line":997,"column":6},"end":{"line":997,"column":37}},"type":"if","locations":[{"start":{"line":997,"column":6},"end":{"line":997,"column":37}},{"start":{"line":997,"column":6},"end":{"line":997,"column":37}}]},"177":{"loc":{"start":{"line":1002,"column":22},"end":{"line":1002,"column":102}},"type":"cond-expr","locations":[{"start":{"line":1002,"column":49},"end":{"line":1002,"column":71}},{"start":{"line":1002,"column":74},"end":{"line":1002,"column":102}}]}},"s":{"1":1,"2":1,"3":2026,"4":2026,"5":1404,"6":1404,"7":1404,"8":1404,"9":1,"10":1,"11":1,"12":212,"13":212,"14":212,"15":212,"16":212,"17":212,"18":212,"19":212,"20":212,"21":1,"22":2906,"23":5,"24":2905,"25":2905,"26":2905,"27":72,"28":3,"29":15,"30":80,"31":293,"32":1,"33":292,"34":134,"35":1,"36":133,"37":133,"38":27,"39":32,"40":24,"41":19,"42":34,"43":78,"44":4,"45":315,"46":55,"47":14,"48":109,"49":46,"50":127,"51":127,"52":1,"53":126,"54":3,"55":123,"56":590,"57":13,"58":13,"59":13,"60":9,"61":9,"62":4,"63":1493,"64":1493,"65":1194,"66":39,"67":1155,"68":1,"69":152,"70":6,"71":6,"72":1,"73":7,"74":8,"75":7,"76":0,"77":7,"78":1,"79":1,"80":33,"81":0,"82":33,"83":33,"84":33,"85":33,"86":1,"87":72,"88":72,"89":72,"90":43,"91":29,"92":4,"93":25,"94":25,"95":68,"96":52,"97":52,"98":48,"99":45,"100":3,"101":1,"102":68,"103":22,"104":46,"105":1,"106":3,"107":3,"108":3,"109":1,"110":15,"111":15,"112":15,"113":13,"114":13,"115":11,"116":11,"117":11,"118":1,"119":80,"120":80,"121":80,"122":80,"123":5,"124":5,"125":80,"126":79,"127":10,"128":1,"129":9,"130":69,"131":40,"132":40,"133":40,"134":40,"135":39,"136":39,"137":30,"138":20,"139":19,"140":1,"141":18,"142":29,"143":29,"144":29,"145":17,"146":10,"147":10,"148":12,"149":0,"150":12,"151":1,"152":11,"153":1,"154":292,"155":292,"156":1,"157":27,"158":27,"159":24,"160":18,"161":15,"162":1,"163":32,"164":2,"165":30,"166":30,"167":12,"168":18,"169":17,"170":28,"171":1,"172":24,"173":24,"174":22,"175":22,"176":22,"177":22,"178":53,"179":26,"180":26,"181":7,"182":26,"183":26,"184":26,"185":26,"186":18,"187":8,"188":2,"189":6,"190":6,"191":24,"192":27,"193":27,"194":0,"195":17,"196":14,"197":17,"198":17,"199":17,"200":1,"201":19,"202":19,"203":1,"204":18,"205":13,"206":13,"207":1,"208":1,"209":34,"210":34,"211":34,"212":34,"213":30,"214":30,"215":30,"216":30,"217":25,"218":20,"219":18,"220":18,"221":22,"222":22,"223":22,"224":2,"225":20,"226":1,"227":315,"228":312,"229":248,"230":247,"231":1,"232":55,"233":55,"234":55,"235":55,"236":38,"237":38,"238":1,"239":14,"240":2,"241":12,"242":12,"243":12,"244":10,"245":1,"246":46,"247":46,"248":1,"249":39,"250":7,"251":3,"252":36,"253":36,"254":4,"255":4,"256":4,"257":4,"258":0,"259":36,"260":36,"261":25,"262":25,"263":25,"264":1,"265":1109,"266":1109,"267":1077,"268":1,"269":772,"270":772,"271":766,"272":615,"273":1,"274":2792,"275":2792,"276":2792,"277":2792,"278":2670,"279":11,"280":2670,"281":1904,"282":212,"283":212,"284":212,"285":182,"286":182,"287":178,"288":3,"289":205,"290":1692,"291":1692,"292":2019,"293":53,"294":1,"295":38,"296":38,"297":26,"298":26,"299":26,"300":26,"301":26,"302":23,"303":23,"304":1,"305":30,"306":2,"307":2,"308":28,"309":28,"310":30,"311":30,"312":29,"313":29,"314":29,"315":29,"316":1,"317":352,"318":352,"319":352,"320":389,"321":389,"322":350,"323":208,"324":136,"325":5,"326":131,"327":4,"328":127,"329":324,"330":324,"331":287,"332":287,"333":1,"334":389,"335":369,"336":1,"337":432,"338":432,"339":432,"340":432,"341":50,"342":0,"343":50,"344":50,"345":432,"346":15,"347":417,"348":329,"349":412,"350":387,"351":196,"352":196,"353":1,"354":412,"355":411,"356":1,"357":156,"358":156,"359":152,"360":152,"361":127,"362":1,"363":102,"364":1,"365":91,"366":1,"367":152,"368":152,"369":152,"370":152,"371":152,"372":152,"373":152,"374":152,"375":148,"376":158,"377":19,"378":139,"379":8,"380":8,"381":131,"382":131,"383":6,"384":6,"385":131,"386":131,"387":131,"388":131,"389":131,"390":131,"391":128,"392":128,"393":35,"394":0,"395":35,"396":35,"397":127,"398":106,"399":15,"400":13,"401":91,"402":1,"403":1,"404":112,"405":112,"406":2,"407":1,"408":2,"409":2,"410":112,"411":112,"412":103,"413":103,"414":31,"415":31,"416":31,"417":103,"418":103,"419":15,"420":2,"421":13,"422":3,"423":10,"424":1,"425":9,"426":0,"427":9,"428":9,"429":97,"430":97,"431":2,"432":104,"433":1,"434":0,"435":1,"436":1,"437":104,"438":0,"439":104,"440":99,"441":28,"442":28,"443":1,"444":1,"445":0,"446":1,"447":128,"448":1,"449":127,"450":127,"451":1,"452":15,"453":6,"454":0,"455":6,"456":6,"457":9,"458":15,"459":13,"460":1,"461":93,"462":88,"463":1,"464":156,"465":145,"466":11,"467":7,"468":4,"469":1,"470":152,"471":1,"472":75,"473":75,"474":7,"475":7,"476":7,"477":2,"478":2,"479":2,"480":2,"481":5,"482":2,"483":68,"484":3,"485":3,"486":3,"487":3,"488":0,"489":0,"490":0,"491":0,"492":0,"493":0,"494":3,"495":3,"496":65,"497":23,"498":23,"499":23,"500":8,"501":15,"502":4,"503":11,"504":11,"505":22,"506":22,"507":10,"508":21,"509":21,"510":42,"511":21,"512":21,"513":21,"514":21,"515":21,"516":19,"517":44,"518":44,"519":1,"520":15,"521":1,"522":3,"523":2,"524":1,"525":0,"526":1,"527":1,"528":1,"529":5,"530":2,"531":1,"532":31,"533":16,"534":15,"535":15,"536":3,"537":12,"538":27,"539":1,"540":22,"541":1,"542":80,"543":0,"544":0,"545":0,"546":0,"547":1,"548":25,"549":25,"550":25,"551":25,"552":32,"553":24,"554":8,"555":8,"556":1,"557":31,"558":31,"559":4,"560":31,"561":31,"562":31,"563":31,"564":25,"565":2,"566":23,"567":1,"568":48,"569":48,"570":2,"571":2,"572":46,"573":46,"574":42,"575":31,"576":31,"577":31,"578":1,"579":46,"580":46,"581":18,"582":18,"583":18,"584":18,"585":11,"586":35,"587":8,"588":8,"589":8,"590":7,"591":7,"592":7,"593":7,"594":27,"595":24,"596":32,"597":23,"598":9,"599":9,"600":1,"601":31,"602":31,"603":31,"604":31,"605":31,"606":1,"607":18,"608":18,"609":18,"610":18},"f":{"1":2026,"2":212,"3":2906,"4":152,"5":7,"6":33,"7":72,"8":3,"9":15,"10":80,"11":292,"12":27,"13":32,"14":24,"15":19,"16":34,"17":315,"18":55,"19":14,"20":46,"21":39,"22":1109,"23":772,"24":2792,"25":38,"26":30,"27":352,"28":389,"29":432,"30":412,"31":156,"32":102,"33":91,"34":152,"35":15,"36":93,"37":156,"38":152,"39":75,"40":15,"41":3,"42":5,"43":31,"44":22,"45":80,"46":25,"47":48,"48":46,"49":18},"b":{"1":[5,2901],"2":[41,72,3,15,80,293,134,27,32,24,19,34,49,78,315,55,14,109,46,78,127,590],"3":[1,292],"4":[1,133],"5":[4,74],"6":[127,0],"7":[1,126],"8":[3,123],"9":[48,75],"10":[13,577],"11":[9,4],"12":[13,10],"13":[39,1155],"14":[1194,543,145],"15":[6,146],"16":[0,7],"17":[7,5],"18":[1,6],"19":[0,33],"20":[43,29],"21":[4,25],"22":[48,4],"23":[52,21],"24":[45,3],"25":[48,47,19],"26":[1,2],"27":[3,1],"28":[22,46],"29":[29,17],"30":[5,75],"31":[80,8,7],"32":[10,69],"33":[1,9],"34":[40,29],"35":[69,56,39],"36":[30,9],"37":[39,26],"38":[20,10],"39":[30,28],"40":[1,18],"41":[17,12],"42":[29,17],"43":[0,12],"44":[1,11],"45":[7,11],"46":[2,30],"47":[32,4],"48":[12,18],"49":[26,27],"50":[53,35],"51":[7,19],"52":[18,8],"53":[2,6],"54":[27,0],"55":[14,3],"56":[1,18],"57":[30,4],"58":[4,18],"59":[2,20],"60":[22,4],"61":[2,12],"62":[3,4],"63":[26,10],"64":[0,10],"65":[4,0],"66":[11,2659],"67":[2670,2559,12],"68":[212,1692],"69":[1904,1788,1716,919,213],"70":[182,30],"71":[212,210],"72":[3,175],"73":[53,1966],"74":[20,6],"75":[22,4],"76":[2,28],"77":[13,15],"78":[208,136],"79":[5,131],"80":[136,11],"81":[11,10],"82":[4,127],"83":[131,8],"84":[8,4,2],"85":[287,37],"86":[50,382],"87":[0,50],"88":[50,0],"89":[15,417],"90":[432,309,301,20],"91":[329,88],"92":[417,96],"93":[125,71],"94":[108,19],"95":[102,96],"96":[19,139],"97":[8,131],"98":[6,125],"99":[131,116],"100":[128,37],"101":[35,93],"102":[0,35],"103":[106,21],"104":[127,120,112],"105":[15,91],"106":[1,90],"107":[91,2,2,2],"108":[112,42,42,41],"109":[2,110],"110":[1,1],"111":[2,1],"112":[103,9],"113":[31,72],"114":[103,101,95,91,86,35,17],"115":[103,102,78,73,67,5],"116":[15,88],"117":[2,13],"118":[3,10],"119":[1,9],"120":[0,9],"121":[97,24,23,23,1],"122":[2,95],"123":[1,103],"124":[0,1],"125":[0,104],"126":[104,95,10],"127":[28,71],"128":[16,12],"129":[1,27],"130":[0,1],"131":[1,127],"132":[6,9],"133":[0,6],"134":[145,11],"135":[7,4],"136":[11,8],"137":[24,128],"138":[7,68],"139":[2,5],"140":[7,2],"141":[3,65],"142":[68,3],"143":[0,3],"144":[3,1],"145":[23,42],"146":[8,15],"147":[4,11],"148":[10,12],"149":[21,21],"150":[42,28],"151":[2,1],"152":[2,2,2],"153":[0,1],"154":[1,1,1],"155":[2,3],"156":[16,15],"157":[15,1],"158":[3,12],"159":[0,80],"160":[0,0,0],"161":[0,0],"162":[0,0],"163":[24,8],"164":[1,7],"165":[4,27],"166":[31,4],"167":[8,23],"168":[2,23],"169":[25,4],"170":[2,46],"171":[29,2],"172":[18,28],"173":[11,7],"174":[8,27],"175":[23,9],"176":[1,8],"177":[9,22]},"hash":"5b810cc6fbdb56d9f2a5aa99bb233c83f7ed8b6b"} +,"/home/travis/build/babel/babylon/src/parser/util.js": {"path":"/home/travis/build/babel/babylon/src/parser/util.js","statementMap":{"1":{"start":{"line":5,"column":11},"end":{"line":5,"column":27}},"2":{"start":{"line":11,"column":0},"end":{"line":16,"column":2}},"3":{"start":{"line":12,"column":2},"end":{"line":12,"column":20}},"4":{"start":{"line":12,"column":13},"end":{"line":12,"column":20}},"5":{"start":{"line":14,"column":14},"end":{"line":14,"column":43}},"6":{"start":{"line":15,"column":2},"end":{"line":15,"column":19}},"7":{"start":{"line":20,"column":0},"end":{"line":22,"column":2}},"8":{"start":{"line":21,"column":2},"end":{"line":21,"column":62}},"9":{"start":{"line":26,"column":0},"end":{"line":32,"column":2}},"10":{"start":{"line":27,"column":2},"end":{"line":31,"column":3}},"11":{"start":{"line":28,"column":4},"end":{"line":28,"column":16}},"12":{"start":{"line":30,"column":4},"end":{"line":30,"column":22}},"13":{"start":{"line":36,"column":0},"end":{"line":38,"column":2}},"14":{"start":{"line":37,"column":2},"end":{"line":37,"column":58}},"15":{"start":{"line":42,"column":0},"end":{"line":44,"column":2}},"16":{"start":{"line":43,"column":2},"end":{"line":43,"column":56}},"17":{"start":{"line":48,"column":0},"end":{"line":50,"column":2}},"18":{"start":{"line":49,"column":2},"end":{"line":49,"column":64}},"19":{"start":{"line":49,"column":33},"end":{"line":49,"column":64}},"20":{"start":{"line":54,"column":0},"end":{"line":58,"column":2}},"21":{"start":{"line":55,"column":2},"end":{"line":57,"column":78}},"22":{"start":{"line":62,"column":0},"end":{"line":64,"column":2}},"23":{"start":{"line":63,"column":2},"end":{"line":63,"column":56}},"24":{"start":{"line":69,"column":0},"end":{"line":71,"column":2}},"25":{"start":{"line":70,"column":2},"end":{"line":70,"column":50}},"26":{"start":{"line":70,"column":32},"end":{"line":70,"column":50}},"27":{"start":{"line":76,"column":0},"end":{"line":78,"column":2}},"28":{"start":{"line":77,"column":2},"end":{"line":77,"column":48}},"29":{"start":{"line":82,"column":0},"end":{"line":84,"column":2}},"30":{"start":{"line":83,"column":2},"end":{"line":83,"column":60}}},"fnMap":{"1":{"name":"(anonymous_1)","decl":{"start":{"line":11,"column":14},"end":{"line":11,"column":15}},"loc":{"start":{"line":11,"column":40},"end":{"line":16,"column":1}}},"2":{"name":"(anonymous_2)","decl":{"start":{"line":20,"column":18},"end":{"line":20,"column":19}},"loc":{"start":{"line":20,"column":32},"end":{"line":22,"column":1}}},"3":{"name":"(anonymous_3)","decl":{"start":{"line":26,"column":22},"end":{"line":26,"column":23}},"loc":{"start":{"line":26,"column":36},"end":{"line":32,"column":1}}},"4":{"name":"(anonymous_4)","decl":{"start":{"line":36,"column":18},"end":{"line":36,"column":19}},"loc":{"start":{"line":36,"column":34},"end":{"line":38,"column":1}}},"5":{"name":"(anonymous_5)","decl":{"start":{"line":42,"column":19},"end":{"line":42,"column":20}},"loc":{"start":{"line":42,"column":35},"end":{"line":44,"column":1}}},"6":{"name":"(anonymous_6)","decl":{"start":{"line":48,"column":22},"end":{"line":48,"column":23}},"loc":{"start":{"line":48,"column":47},"end":{"line":50,"column":1}}},"7":{"name":"(anonymous_7)","decl":{"start":{"line":54,"column":24},"end":{"line":54,"column":25}},"loc":{"start":{"line":54,"column":36},"end":{"line":58,"column":1}}},"8":{"name":"(anonymous_8)","decl":{"start":{"line":62,"column":22},"end":{"line":62,"column":23}},"loc":{"start":{"line":62,"column":34},"end":{"line":64,"column":1}}},"9":{"name":"(anonymous_9)","decl":{"start":{"line":69,"column":15},"end":{"line":69,"column":16}},"loc":{"start":{"line":69,"column":27},"end":{"line":71,"column":1}}},"10":{"name":"(anonymous_10)","decl":{"start":{"line":76,"column":12},"end":{"line":76,"column":13}},"loc":{"start":{"line":76,"column":33},"end":{"line":78,"column":1}}},"11":{"name":"(anonymous_11)","decl":{"start":{"line":82,"column":16},"end":{"line":82,"column":17}},"loc":{"start":{"line":82,"column":61},"end":{"line":84,"column":1}}}},"branchMap":{"1":{"loc":{"start":{"line":12,"column":2},"end":{"line":12,"column":20}},"type":"if","locations":[{"start":{"line":12,"column":2},"end":{"line":12,"column":20}},{"start":{"line":12,"column":2},"end":{"line":12,"column":20}}]},"2":{"loc":{"start":{"line":14,"column":27},"end":{"line":14,"column":43}},"type":"binary-expr","locations":[{"start":{"line":14,"column":27},"end":{"line":14,"column":37}},{"start":{"line":14,"column":41},"end":{"line":14,"column":43}}]},"3":{"loc":{"start":{"line":21,"column":9},"end":{"line":21,"column":61}},"type":"binary-expr","locations":[{"start":{"line":21,"column":9},"end":{"line":21,"column":34}},{"start":{"line":21,"column":38},"end":{"line":21,"column":61}}]},"4":{"loc":{"start":{"line":27,"column":2},"end":{"line":31,"column":3}},"type":"if","locations":[{"start":{"line":27,"column":2},"end":{"line":31,"column":3}},{"start":{"line":27,"column":2},"end":{"line":31,"column":3}}]},"5":{"loc":{"start":{"line":37,"column":9},"end":{"line":37,"column":57}},"type":"binary-expr","locations":[{"start":{"line":37,"column":9},"end":{"line":37,"column":28}},{"start":{"line":37,"column":32},"end":{"line":37,"column":57}}]},"6":{"loc":{"start":{"line":43,"column":9},"end":{"line":43,"column":55}},"type":"binary-expr","locations":[{"start":{"line":43,"column":9},"end":{"line":43,"column":34}},{"start":{"line":43,"column":38},"end":{"line":43,"column":55}}]},"7":{"loc":{"start":{"line":49,"column":2},"end":{"line":49,"column":64}},"type":"if","locations":[{"start":{"line":49,"column":2},"end":{"line":49,"column":64}},{"start":{"line":49,"column":2},"end":{"line":49,"column":64}}]},"8":{"loc":{"start":{"line":55,"column":9},"end":{"line":57,"column":77}},"type":"binary-expr","locations":[{"start":{"line":55,"column":9},"end":{"line":55,"column":27}},{"start":{"line":56,"column":4},"end":{"line":56,"column":25}},{"start":{"line":57,"column":4},"end":{"line":57,"column":77}}]},"9":{"loc":{"start":{"line":63,"column":9},"end":{"line":63,"column":55}},"type":"binary-expr","locations":[{"start":{"line":63,"column":9},"end":{"line":63,"column":26}},{"start":{"line":63,"column":30},"end":{"line":63,"column":55}}]},"10":{"loc":{"start":{"line":70,"column":2},"end":{"line":70,"column":50}},"type":"if","locations":[{"start":{"line":70,"column":2},"end":{"line":70,"column":50}},{"start":{"line":70,"column":2},"end":{"line":70,"column":50}}]},"11":{"loc":{"start":{"line":77,"column":9},"end":{"line":77,"column":47}},"type":"binary-expr","locations":[{"start":{"line":77,"column":9},"end":{"line":77,"column":23}},{"start":{"line":77,"column":27},"end":{"line":77,"column":47}}]},"12":{"loc":{"start":{"line":82,"column":31},"end":{"line":82,"column":59}},"type":"default-arg","locations":[{"start":{"line":82,"column":41},"end":{"line":82,"column":59}}]},"13":{"loc":{"start":{"line":83,"column":13},"end":{"line":83,"column":49}},"type":"cond-expr","locations":[{"start":{"line":83,"column":27},"end":{"line":83,"column":30}},{"start":{"line":83,"column":33},"end":{"line":83,"column":49}}]}},"s":{"1":1,"2":1,"3":2801,"4":0,"5":2801,"6":2801,"7":1,"8":1089,"9":1,"10":126,"11":126,"12":0,"13":1,"14":432,"15":1,"16":157,"17":1,"18":60,"19":14,"20":1,"21":2706,"22":1,"23":1721,"24":1,"25":1523,"26":37,"27":1,"28":3953,"29":1,"30":349},"f":{"1":2801,"2":1089,"3":126,"4":432,"5":157,"6":60,"7":2706,"8":1721,"9":1523,"10":3953,"11":349},"b":{"1":[0,2801],"2":[2801,1460],"3":[1089,494],"4":[126,0],"5":[432,288],"6":[157,82],"7":[14,46],"8":[2706,1781,1627],"9":[1721,1122],"10":[37,1486],"11":[3953,72],"12":[348],"13":[24,325]},"hash":"b42cf3707be28a330ce42b4890cc4441eef02c27"} +,"/home/travis/build/babel/babylon/src/plugins/flow.js": {"path":"/home/travis/build/babel/babylon/src/plugins/flow.js","statementMap":{"1":{"start":{"line":8,"column":9},"end":{"line":8,"column":25}},"2":{"start":{"line":10,"column":0},"end":{"line":22,"column":2}},"3":{"start":{"line":11,"column":18},"end":{"line":11,"column":35}},"4":{"start":{"line":12,"column":2},"end":{"line":12,"column":27}},"5":{"start":{"line":13,"column":2},"end":{"line":13,"column":31}},"6":{"start":{"line":14,"column":2},"end":{"line":18,"column":3}},"7":{"start":{"line":15,"column":4},"end":{"line":17,"column":5}},"8":{"start":{"line":16,"column":6},"end":{"line":16,"column":18}},"9":{"start":{"line":19,"column":13},"end":{"line":19,"column":33}},"10":{"start":{"line":20,"column":2},"end":{"line":20,"column":32}},"11":{"start":{"line":21,"column":2},"end":{"line":21,"column":14}},"12":{"start":{"line":24,"column":0},"end":{"line":28,"column":2}},"13":{"start":{"line":25,"column":2},"end":{"line":25,"column":14}},"14":{"start":{"line":26,"column":2},"end":{"line":26,"column":41}},"15":{"start":{"line":27,"column":2},"end":{"line":27,"column":47}},"16":{"start":{"line":30,"column":0},"end":{"line":59,"column":2}},"17":{"start":{"line":31,"column":2},"end":{"line":31,"column":14}},"18":{"start":{"line":33,"column":11},"end":{"line":33,"column":43}},"19":{"start":{"line":35,"column":17},"end":{"line":35,"column":33}},"20":{"start":{"line":36,"column":22},"end":{"line":36,"column":38}},"21":{"start":{"line":38,"column":2},"end":{"line":42,"column":3}},"22":{"start":{"line":39,"column":4},"end":{"line":39,"column":71}},"23":{"start":{"line":41,"column":4},"end":{"line":41,"column":35}},"24":{"start":{"line":44,"column":2},"end":{"line":44,"column":25}},"25":{"start":{"line":45,"column":12},"end":{"line":45,"column":46}},"26":{"start":{"line":46,"column":2},"end":{"line":46,"column":31}},"27":{"start":{"line":47,"column":2},"end":{"line":47,"column":27}},"28":{"start":{"line":48,"column":2},"end":{"line":48,"column":25}},"29":{"start":{"line":49,"column":2},"end":{"line":49,"column":56}},"30":{"start":{"line":51,"column":2},"end":{"line":51,"column":85}},"31":{"start":{"line":52,"column":2},"end":{"line":52,"column":71}},"32":{"start":{"line":54,"column":2},"end":{"line":54,"column":31}},"33":{"start":{"line":56,"column":2},"end":{"line":56,"column":19}},"34":{"start":{"line":58,"column":2},"end":{"line":58,"column":50}},"35":{"start":{"line":61,"column":0},"end":{"line":81,"column":2}},"36":{"start":{"line":62,"column":2},"end":{"line":80,"column":3}},"37":{"start":{"line":63,"column":4},"end":{"line":63,"column":44}},"38":{"start":{"line":64,"column":9},"end":{"line":80,"column":3}},"39":{"start":{"line":65,"column":4},"end":{"line":65,"column":47}},"40":{"start":{"line":66,"column":9},"end":{"line":80,"column":3}},"41":{"start":{"line":67,"column":4},"end":{"line":67,"column":47}},"42":{"start":{"line":68,"column":9},"end":{"line":80,"column":3}},"43":{"start":{"line":69,"column":4},"end":{"line":73,"column":5}},"44":{"start":{"line":70,"column":6},"end":{"line":70,"column":54}},"45":{"start":{"line":72,"column":6},"end":{"line":72,"column":47}},"46":{"start":{"line":74,"column":9},"end":{"line":80,"column":3}},"47":{"start":{"line":75,"column":4},"end":{"line":75,"column":48}},"48":{"start":{"line":76,"column":9},"end":{"line":80,"column":3}},"49":{"start":{"line":77,"column":4},"end":{"line":77,"column":48}},"50":{"start":{"line":79,"column":4},"end":{"line":79,"column":22}},"51":{"start":{"line":83,"column":0},"end":{"line":88,"column":2}},"52":{"start":{"line":84,"column":2},"end":{"line":84,"column":14}},"53":{"start":{"line":85,"column":2},"end":{"line":85,"column":54}},"54":{"start":{"line":86,"column":2},"end":{"line":86,"column":19}},"55":{"start":{"line":87,"column":2},"end":{"line":87,"column":50}},"56":{"start":{"line":90,"column":0},"end":{"line":113,"column":2}},"57":{"start":{"line":91,"column":2},"end":{"line":91,"column":14}},"58":{"start":{"line":93,"column":2},"end":{"line":97,"column":3}},"59":{"start":{"line":94,"column":4},"end":{"line":94,"column":35}},"60":{"start":{"line":96,"column":4},"end":{"line":96,"column":37}},"61":{"start":{"line":99,"column":17},"end":{"line":99,"column":45}},"62":{"start":{"line":100,"column":13},"end":{"line":100,"column":31}},"63":{"start":{"line":101,"column":2},"end":{"line":101,"column":25}},"64":{"start":{"line":102,"column":2},"end":{"line":108,"column":3}},"65":{"start":{"line":103,"column":16},"end":{"line":103,"column":32}},"66":{"start":{"line":105,"column":4},"end":{"line":105,"column":106}},"67":{"start":{"line":107,"column":4},"end":{"line":107,"column":44}},"68":{"start":{"line":109,"column":2},"end":{"line":109,"column":25}},"69":{"start":{"line":111,"column":2},"end":{"line":111,"column":46}},"70":{"start":{"line":112,"column":2},"end":{"line":112,"column":48}},"71":{"start":{"line":115,"column":0},"end":{"line":121,"column":2}},"72":{"start":{"line":116,"column":2},"end":{"line":116,"column":34}},"73":{"start":{"line":117,"column":2},"end":{"line":117,"column":22}},"74":{"start":{"line":118,"column":2},"end":{"line":118,"column":35}},"75":{"start":{"line":119,"column":2},"end":{"line":119,"column":55}},"76":{"start":{"line":120,"column":2},"end":{"line":120,"column":55}},"77":{"start":{"line":123,"column":0},"end":{"line":127,"column":2}},"78":{"start":{"line":124,"column":2},"end":{"line":124,"column":14}},"79":{"start":{"line":125,"column":2},"end":{"line":125,"column":32}},"80":{"start":{"line":126,"column":2},"end":{"line":126,"column":51}},"81":{"start":{"line":129,"column":0},"end":{"line":133,"column":2}},"82":{"start":{"line":130,"column":2},"end":{"line":130,"column":14}},"83":{"start":{"line":131,"column":2},"end":{"line":131,"column":35}},"84":{"start":{"line":132,"column":2},"end":{"line":132,"column":51}},"85":{"start":{"line":137,"column":0},"end":{"line":163,"column":2}},"86":{"start":{"line":138,"column":2},"end":{"line":138,"column":35}},"87":{"start":{"line":140,"column":2},"end":{"line":144,"column":3}},"88":{"start":{"line":141,"column":4},"end":{"line":141,"column":67}},"89":{"start":{"line":143,"column":4},"end":{"line":143,"column":31}},"90":{"start":{"line":146,"column":2},"end":{"line":146,"column":20}},"91":{"start":{"line":147,"column":2},"end":{"line":147,"column":19}},"92":{"start":{"line":149,"column":2},"end":{"line":153,"column":3}},"93":{"start":{"line":150,"column":4},"end":{"line":152,"column":33}},"94":{"start":{"line":151,"column":6},"end":{"line":151,"column":58}},"95":{"start":{"line":155,"column":2},"end":{"line":160,"column":3}},"96":{"start":{"line":156,"column":4},"end":{"line":156,"column":16}},"97":{"start":{"line":157,"column":4},"end":{"line":159,"column":33}},"98":{"start":{"line":158,"column":6},"end":{"line":158,"column":57}},"99":{"start":{"line":162,"column":2},"end":{"line":162,"column":52}},"100":{"start":{"line":165,"column":0},"end":{"line":176,"column":2}},"101":{"start":{"line":166,"column":13},"end":{"line":166,"column":29}},"102":{"start":{"line":168,"column":2},"end":{"line":168,"column":52}},"103":{"start":{"line":169,"column":2},"end":{"line":173,"column":3}},"104":{"start":{"line":170,"column":4},"end":{"line":170,"column":69}},"105":{"start":{"line":172,"column":4},"end":{"line":172,"column":31}},"106":{"start":{"line":175,"column":2},"end":{"line":175,"column":51}},"107":{"start":{"line":178,"column":0},"end":{"line":181,"column":2}},"108":{"start":{"line":179,"column":2},"end":{"line":179,"column":42}},"109":{"start":{"line":180,"column":2},"end":{"line":180,"column":55}},"110":{"start":{"line":185,"column":0},"end":{"line":201,"column":2}},"111":{"start":{"line":186,"column":2},"end":{"line":186,"column":35}},"112":{"start":{"line":188,"column":2},"end":{"line":192,"column":3}},"113":{"start":{"line":189,"column":4},"end":{"line":189,"column":67}},"114":{"start":{"line":191,"column":4},"end":{"line":191,"column":31}},"115":{"start":{"line":194,"column":2},"end":{"line":197,"column":4}},"116":{"start":{"line":198,"column":2},"end":{"line":198,"column":19}},"117":{"start":{"line":200,"column":2},"end":{"line":200,"column":44}},"118":{"start":{"line":205,"column":0},"end":{"line":229,"column":2}},"119":{"start":{"line":206,"column":13},"end":{"line":206,"column":29}},"120":{"start":{"line":209,"column":2},"end":{"line":216,"column":3}},"121":{"start":{"line":210,"column":4},"end":{"line":214,"column":5}},"122":{"start":{"line":211,"column":6},"end":{"line":211,"column":24}},"123":{"start":{"line":212,"column":11},"end":{"line":214,"column":5}},"124":{"start":{"line":213,"column":6},"end":{"line":213,"column":25}},"125":{"start":{"line":215,"column":4},"end":{"line":215,"column":25}},"126":{"start":{"line":218,"column":14},"end":{"line":218,"column":67}},"127":{"start":{"line":219,"column":2},"end":{"line":219,"column":25}},"128":{"start":{"line":220,"column":2},"end":{"line":220,"column":27}},"129":{"start":{"line":221,"column":2},"end":{"line":221,"column":36}},"130":{"start":{"line":223,"column":2},"end":{"line":226,"column":3}},"131":{"start":{"line":224,"column":4},"end":{"line":224,"column":20}},"132":{"start":{"line":225,"column":4},"end":{"line":225,"column":41}},"133":{"start":{"line":228,"column":2},"end":{"line":228,"column":48}},"134":{"start":{"line":231,"column":0},"end":{"line":255,"column":2}},"135":{"start":{"line":232,"column":20},"end":{"line":232,"column":37}},"136":{"start":{"line":233,"column":13},"end":{"line":233,"column":29}},"137":{"start":{"line":234,"column":2},"end":{"line":234,"column":19}},"138":{"start":{"line":236,"column":2},"end":{"line":236,"column":27}},"139":{"start":{"line":238,"column":2},"end":{"line":242,"column":3}},"140":{"start":{"line":239,"column":4},"end":{"line":239,"column":16}},"141":{"start":{"line":241,"column":4},"end":{"line":241,"column":22}},"142":{"start":{"line":244,"column":2},"end":{"line":249,"column":36}},"143":{"start":{"line":245,"column":4},"end":{"line":245,"column":52}},"144":{"start":{"line":246,"column":4},"end":{"line":248,"column":5}},"145":{"start":{"line":247,"column":6},"end":{"line":247,"column":28}},"146":{"start":{"line":250,"column":2},"end":{"line":250,"column":29}},"147":{"start":{"line":252,"column":2},"end":{"line":252,"column":32}},"148":{"start":{"line":254,"column":2},"end":{"line":254,"column":59}},"149":{"start":{"line":257,"column":0},"end":{"line":275,"column":2}},"150":{"start":{"line":258,"column":13},"end":{"line":258,"column":29}},"151":{"start":{"line":258,"column":43},"end":{"line":258,"column":60}},"152":{"start":{"line":259,"column":2},"end":{"line":259,"column":19}},"153":{"start":{"line":261,"column":2},"end":{"line":261,"column":27}},"154":{"start":{"line":263,"column":2},"end":{"line":263,"column":29}},"155":{"start":{"line":264,"column":2},"end":{"line":269,"column":3}},"156":{"start":{"line":265,"column":4},"end":{"line":265,"column":43}},"157":{"start":{"line":266,"column":4},"end":{"line":268,"column":5}},"158":{"start":{"line":267,"column":6},"end":{"line":267,"column":28}},"159":{"start":{"line":270,"column":2},"end":{"line":270,"column":29}},"160":{"start":{"line":272,"column":2},"end":{"line":272,"column":32}},"161":{"start":{"line":274,"column":2},"end":{"line":274,"column":61}},"162":{"start":{"line":277,"column":0},"end":{"line":279,"column":2}},"163":{"start":{"line":278,"column":2},"end":{"line":278,"column":107}},"164":{"start":{"line":281,"column":0},"end":{"line":292,"column":2}},"165":{"start":{"line":282,"column":2},"end":{"line":282,"column":25}},"166":{"start":{"line":284,"column":2},"end":{"line":284,"column":27}},"167":{"start":{"line":285,"column":2},"end":{"line":285,"column":46}},"168":{"start":{"line":286,"column":2},"end":{"line":286,"column":45}},"169":{"start":{"line":287,"column":2},"end":{"line":287,"column":27}},"170":{"start":{"line":288,"column":2},"end":{"line":288,"column":47}},"171":{"start":{"line":290,"column":2},"end":{"line":290,"column":33}},"172":{"start":{"line":291,"column":2},"end":{"line":291,"column":52}},"173":{"start":{"line":294,"column":0},"end":{"line":318,"column":2}},"174":{"start":{"line":295,"column":2},"end":{"line":295,"column":19}},"175":{"start":{"line":296,"column":2},"end":{"line":296,"column":19}},"176":{"start":{"line":297,"column":2},"end":{"line":297,"column":29}},"177":{"start":{"line":299,"column":2},"end":{"line":301,"column":3}},"178":{"start":{"line":300,"column":4},"end":{"line":300,"column":67}},"179":{"start":{"line":303,"column":2},"end":{"line":303,"column":25}},"180":{"start":{"line":304,"column":2},"end":{"line":309,"column":3}},"181":{"start":{"line":305,"column":4},"end":{"line":305,"column":56}},"182":{"start":{"line":306,"column":4},"end":{"line":308,"column":5}},"183":{"start":{"line":307,"column":6},"end":{"line":307,"column":28}},"184":{"start":{"line":311,"column":2},"end":{"line":313,"column":3}},"185":{"start":{"line":312,"column":4},"end":{"line":312,"column":50}},"186":{"start":{"line":314,"column":2},"end":{"line":314,"column":25}},"187":{"start":{"line":315,"column":2},"end":{"line":315,"column":52}},"188":{"start":{"line":317,"column":2},"end":{"line":317,"column":57}},"189":{"start":{"line":320,"column":0},"end":{"line":328,"column":2}},"190":{"start":{"line":321,"column":13},"end":{"line":321,"column":49}},"191":{"start":{"line":322,"column":2},"end":{"line":322,"column":87}},"192":{"start":{"line":323,"column":2},"end":{"line":323,"column":25}},"193":{"start":{"line":324,"column":2},"end":{"line":324,"column":17}},"194":{"start":{"line":325,"column":2},"end":{"line":325,"column":24}},"195":{"start":{"line":326,"column":2},"end":{"line":326,"column":33}},"196":{"start":{"line":327,"column":2},"end":{"line":327,"column":53}},"197":{"start":{"line":330,"column":0},"end":{"line":336,"column":2}},"198":{"start":{"line":331,"column":18},"end":{"line":331,"column":34}},"199":{"start":{"line":332,"column":2},"end":{"line":332,"column":25}},"200":{"start":{"line":333,"column":2},"end":{"line":333,"column":60}},"201":{"start":{"line":334,"column":2},"end":{"line":334,"column":33}},"202":{"start":{"line":335,"column":2},"end":{"line":335,"column":57}},"203":{"start":{"line":338,"column":0},"end":{"line":389,"column":2}},"204":{"start":{"line":339,"column":18},"end":{"line":339,"column":34}},"205":{"start":{"line":344,"column":2},"end":{"line":344,"column":32}},"206":{"start":{"line":345,"column":2},"end":{"line":345,"column":28}},"207":{"start":{"line":346,"column":2},"end":{"line":346,"column":26}},"208":{"start":{"line":348,"column":2},"end":{"line":348,"column":25}},"209":{"start":{"line":350,"column":2},"end":{"line":384,"column":3}},"210":{"start":{"line":351,"column":19},"end":{"line":351,"column":24}},"211":{"start":{"line":352,"column":19},"end":{"line":352,"column":35}},"212":{"start":{"line":352,"column":48},"end":{"line":352,"column":67}},"213":{"start":{"line":353,"column":4},"end":{"line":353,"column":28}},"214":{"start":{"line":354,"column":4},"end":{"line":357,"column":5}},"215":{"start":{"line":355,"column":6},"end":{"line":355,"column":18}},"216":{"start":{"line":356,"column":6},"end":{"line":356,"column":22}},"217":{"start":{"line":359,"column":4},"end":{"line":383,"column":5}},"218":{"start":{"line":360,"column":6},"end":{"line":360,"column":79}},"219":{"start":{"line":361,"column":11},"end":{"line":383,"column":5}},"220":{"start":{"line":362,"column":6},"end":{"line":362,"column":93}},"221":{"start":{"line":364,"column":6},"end":{"line":368,"column":7}},"222":{"start":{"line":365,"column":8},"end":{"line":365,"column":45}},"223":{"start":{"line":367,"column":8},"end":{"line":367,"column":56}},"224":{"start":{"line":369,"column":6},"end":{"line":382,"column":7}},"225":{"start":{"line":371,"column":8},"end":{"line":371,"column":109}},"226":{"start":{"line":373,"column":8},"end":{"line":375,"column":9}},"227":{"start":{"line":374,"column":10},"end":{"line":374,"column":26}},"228":{"start":{"line":376,"column":8},"end":{"line":376,"column":31}},"229":{"start":{"line":377,"column":8},"end":{"line":377,"column":53}},"230":{"start":{"line":378,"column":8},"end":{"line":378,"column":33}},"231":{"start":{"line":379,"column":8},"end":{"line":379,"column":31}},"232":{"start":{"line":380,"column":8},"end":{"line":380,"column":39}},"233":{"start":{"line":381,"column":8},"end":{"line":381,"column":79}},"234":{"start":{"line":386,"column":2},"end":{"line":386,"column":25}},"235":{"start":{"line":388,"column":2},"end":{"line":388,"column":60}},"236":{"start":{"line":391,"column":0},"end":{"line":395,"column":2}},"237":{"start":{"line":392,"column":2},"end":{"line":394,"column":3}},"238":{"start":{"line":393,"column":4},"end":{"line":393,"column":22}},"239":{"start":{"line":397,"column":0},"end":{"line":410,"column":2}},"240":{"start":{"line":398,"column":2},"end":{"line":398,"column":42}},"241":{"start":{"line":399,"column":2},"end":{"line":399,"column":45}},"242":{"start":{"line":400,"column":13},"end":{"line":400,"column":41}},"243":{"start":{"line":402,"column":2},"end":{"line":407,"column":3}},"244":{"start":{"line":403,"column":16},"end":{"line":403,"column":52}},"245":{"start":{"line":404,"column":4},"end":{"line":404,"column":31}},"246":{"start":{"line":405,"column":4},"end":{"line":405,"column":38}},"247":{"start":{"line":406,"column":4},"end":{"line":406,"column":61}},"248":{"start":{"line":409,"column":2},"end":{"line":409,"column":14}},"249":{"start":{"line":412,"column":0},"end":{"line":423,"column":2}},"250":{"start":{"line":413,"column":13},"end":{"line":413,"column":49}},"251":{"start":{"line":415,"column":2},"end":{"line":415,"column":29}},"252":{"start":{"line":416,"column":2},"end":{"line":416,"column":74}},"253":{"start":{"line":418,"column":2},"end":{"line":420,"column":3}},"254":{"start":{"line":419,"column":4},"end":{"line":419,"column":69}},"255":{"start":{"line":422,"column":2},"end":{"line":422,"column":56}},"256":{"start":{"line":425,"column":0},"end":{"line":430,"column":2}},"257":{"start":{"line":426,"column":13},"end":{"line":426,"column":29}},"258":{"start":{"line":427,"column":2},"end":{"line":427,"column":26}},"259":{"start":{"line":428,"column":2},"end":{"line":428,"column":46}},"260":{"start":{"line":429,"column":2},"end":{"line":429,"column":55}},"261":{"start":{"line":432,"column":0},"end":{"line":444,"column":2}},"262":{"start":{"line":433,"column":13},"end":{"line":433,"column":29}},"263":{"start":{"line":434,"column":2},"end":{"line":434,"column":18}},"264":{"start":{"line":435,"column":2},"end":{"line":435,"column":27}},"265":{"start":{"line":437,"column":2},"end":{"line":441,"column":3}},"266":{"start":{"line":438,"column":4},"end":{"line":438,"column":42}},"267":{"start":{"line":439,"column":4},"end":{"line":439,"column":39}},"268":{"start":{"line":439,"column":33},"end":{"line":439,"column":39}},"269":{"start":{"line":440,"column":4},"end":{"line":440,"column":26}},"270":{"start":{"line":442,"column":2},"end":{"line":442,"column":27}},"271":{"start":{"line":443,"column":2},"end":{"line":443,"column":54}},"272":{"start":{"line":446,"column":0},"end":{"line":456,"column":2}},"273":{"start":{"line":447,"column":17},"end":{"line":447,"column":22}},"274":{"start":{"line":448,"column":13},"end":{"line":448,"column":29}},"275":{"start":{"line":449,"column":2},"end":{"line":449,"column":37}},"276":{"start":{"line":450,"column":2},"end":{"line":452,"column":3}},"277":{"start":{"line":451,"column":4},"end":{"line":451,"column":20}},"278":{"start":{"line":453,"column":2},"end":{"line":453,"column":27}},"279":{"start":{"line":454,"column":2},"end":{"line":454,"column":56}},"280":{"start":{"line":455,"column":2},"end":{"line":455,"column":52}},"281":{"start":{"line":458,"column":0},"end":{"line":470,"column":2}},"282":{"start":{"line":459,"column":12},"end":{"line":459,"column":38}},"283":{"start":{"line":460,"column":2},"end":{"line":465,"column":3}},"284":{"start":{"line":461,"column":4},"end":{"line":461,"column":55}},"285":{"start":{"line":462,"column":4},"end":{"line":464,"column":5}},"286":{"start":{"line":463,"column":6},"end":{"line":463,"column":28}},"287":{"start":{"line":466,"column":2},"end":{"line":468,"column":3}},"288":{"start":{"line":467,"column":4},"end":{"line":467,"column":49}},"289":{"start":{"line":469,"column":2},"end":{"line":469,"column":13}},"290":{"start":{"line":472,"column":0},"end":{"line":496,"column":2}},"291":{"start":{"line":473,"column":2},"end":{"line":495,"column":3}},"292":{"start":{"line":475,"column":6},"end":{"line":475,"column":56}},"293":{"start":{"line":478,"column":6},"end":{"line":478,"column":57}},"294":{"start":{"line":482,"column":6},"end":{"line":482,"column":60}},"295":{"start":{"line":485,"column":6},"end":{"line":485,"column":58}},"296":{"start":{"line":488,"column":6},"end":{"line":488,"column":59}},"297":{"start":{"line":491,"column":6},"end":{"line":491,"column":59}},"298":{"start":{"line":494,"column":6},"end":{"line":494,"column":63}},"299":{"start":{"line":501,"column":0},"end":{"line":619,"column":2}},"300":{"start":{"line":502,"column":17},"end":{"line":502,"column":33}},"301":{"start":{"line":502,"column":46},"end":{"line":502,"column":65}},"302":{"start":{"line":503,"column":13},"end":{"line":503,"column":29}},"303":{"start":{"line":506,"column":22},"end":{"line":506,"column":27}},"304":{"start":{"line":508,"column":2},"end":{"line":616,"column":3}},"305":{"start":{"line":510,"column":6},"end":{"line":510,"column":94}},"306":{"start":{"line":513,"column":6},"end":{"line":513,"column":40}},"307":{"start":{"line":516,"column":6},"end":{"line":516,"column":39}},"308":{"start":{"line":519,"column":6},"end":{"line":532,"column":7}},"309":{"start":{"line":520,"column":8},"end":{"line":520,"column":71}},"310":{"start":{"line":521,"column":8},"end":{"line":521,"column":31}},"311":{"start":{"line":522,"column":8},"end":{"line":522,"column":49}},"312":{"start":{"line":523,"column":8},"end":{"line":523,"column":33}},"313":{"start":{"line":524,"column":8},"end":{"line":524,"column":29}},"314":{"start":{"line":525,"column":8},"end":{"line":525,"column":31}},"315":{"start":{"line":527,"column":8},"end":{"line":527,"column":30}},"316":{"start":{"line":529,"column":8},"end":{"line":529,"column":47}},"317":{"start":{"line":531,"column":8},"end":{"line":531,"column":63}},"318":{"start":{"line":533,"column":6},"end":{"line":533,"column":12}},"319":{"start":{"line":536,"column":6},"end":{"line":536,"column":18}},"320":{"start":{"line":539,"column":6},"end":{"line":546,"column":7}},"321":{"start":{"line":540,"column":8},"end":{"line":545,"column":9}},"322":{"start":{"line":541,"column":22},"end":{"line":541,"column":43}},"323":{"start":{"line":542,"column":10},"end":{"line":542,"column":70}},"324":{"start":{"line":544,"column":10},"end":{"line":544,"column":31}},"325":{"start":{"line":548,"column":6},"end":{"line":552,"column":7}},"326":{"start":{"line":549,"column":8},"end":{"line":549,"column":36}},"327":{"start":{"line":550,"column":8},"end":{"line":550,"column":31}},"328":{"start":{"line":551,"column":8},"end":{"line":551,"column":20}},"329":{"start":{"line":554,"column":6},"end":{"line":554,"column":47}},"330":{"start":{"line":555,"column":6},"end":{"line":555,"column":31}},"331":{"start":{"line":556,"column":6},"end":{"line":556,"column":27}},"332":{"start":{"line":558,"column":6},"end":{"line":558,"column":29}},"333":{"start":{"line":560,"column":6},"end":{"line":560,"column":28}},"334":{"start":{"line":562,"column":6},"end":{"line":562,"column":45}},"335":{"start":{"line":563,"column":6},"end":{"line":563,"column":33}},"336":{"start":{"line":565,"column":6},"end":{"line":565,"column":61}},"337":{"start":{"line":568,"column":6},"end":{"line":568,"column":36}},"338":{"start":{"line":569,"column":6},"end":{"line":569,"column":50}},"339":{"start":{"line":570,"column":6},"end":{"line":570,"column":85}},"340":{"start":{"line":571,"column":6},"end":{"line":571,"column":18}},"341":{"start":{"line":572,"column":6},"end":{"line":572,"column":66}},"342":{"start":{"line":575,"column":6},"end":{"line":575,"column":40}},"343":{"start":{"line":576,"column":6},"end":{"line":576,"column":18}},"344":{"start":{"line":577,"column":6},"end":{"line":577,"column":67}},"345":{"start":{"line":580,"column":6},"end":{"line":589,"column":7}},"346":{"start":{"line":581,"column":8},"end":{"line":581,"column":20}},"347":{"start":{"line":582,"column":8},"end":{"line":582,"column":51}},"348":{"start":{"line":582,"column":33},"end":{"line":582,"column":51}},"349":{"start":{"line":584,"column":8},"end":{"line":584,"column":39}},"350":{"start":{"line":585,"column":8},"end":{"line":585,"column":52}},"351":{"start":{"line":586,"column":8},"end":{"line":586,"column":87}},"352":{"start":{"line":587,"column":8},"end":{"line":587,"column":20}},"353":{"start":{"line":588,"column":8},"end":{"line":588,"column":69}},"354":{"start":{"line":592,"column":6},"end":{"line":592,"column":36}},"355":{"start":{"line":593,"column":6},"end":{"line":593,"column":50}},"356":{"start":{"line":594,"column":6},"end":{"line":594,"column":85}},"357":{"start":{"line":595,"column":6},"end":{"line":595,"column":18}},"358":{"start":{"line":596,"column":6},"end":{"line":596,"column":67}},"359":{"start":{"line":599,"column":6},"end":{"line":599,"column":40}},"360":{"start":{"line":600,"column":6},"end":{"line":600,"column":18}},"361":{"start":{"line":601,"column":6},"end":{"line":601,"column":64}},"362":{"start":{"line":604,"column":6},"end":{"line":604,"column":40}},"363":{"start":{"line":605,"column":6},"end":{"line":605,"column":18}},"364":{"start":{"line":606,"column":6},"end":{"line":606,"column":57}},"365":{"start":{"line":609,"column":6},"end":{"line":609,"column":18}},"366":{"start":{"line":610,"column":6},"end":{"line":610,"column":59}},"367":{"start":{"line":613,"column":6},"end":{"line":615,"column":7}},"368":{"start":{"line":614,"column":8},"end":{"line":614,"column":42}},"369":{"start":{"line":618,"column":2},"end":{"line":618,"column":20}},"370":{"start":{"line":621,"column":0},"end":{"line":631,"column":2}},"371":{"start":{"line":622,"column":13},"end":{"line":622,"column":29}},"372":{"start":{"line":623,"column":13},"end":{"line":623,"column":59}},"373":{"start":{"line":624,"column":2},"end":{"line":630,"column":3}},"374":{"start":{"line":625,"column":4},"end":{"line":625,"column":29}},"375":{"start":{"line":626,"column":4},"end":{"line":626,"column":29}},"376":{"start":{"line":627,"column":4},"end":{"line":627,"column":56}},"377":{"start":{"line":629,"column":4},"end":{"line":629,"column":16}},"378":{"start":{"line":633,"column":0},"end":{"line":641,"column":2}},"379":{"start":{"line":634,"column":13},"end":{"line":634,"column":29}},"380":{"start":{"line":635,"column":2},"end":{"line":640,"column":3}},"381":{"start":{"line":636,"column":4},"end":{"line":636,"column":53}},"382":{"start":{"line":637,"column":4},"end":{"line":637,"column":59}},"383":{"start":{"line":639,"column":4},"end":{"line":639,"column":39}},"384":{"start":{"line":643,"column":0},"end":{"line":651,"column":2}},"385":{"start":{"line":644,"column":13},"end":{"line":644,"column":29}},"386":{"start":{"line":645,"column":13},"end":{"line":645,"column":39}},"387":{"start":{"line":646,"column":2},"end":{"line":646,"column":22}},"388":{"start":{"line":647,"column":2},"end":{"line":649,"column":3}},"389":{"start":{"line":648,"column":4},"end":{"line":648,"column":48}},"390":{"start":{"line":650,"column":2},"end":{"line":650,"column":94}},"391":{"start":{"line":653,"column":0},"end":{"line":661,"column":2}},"392":{"start":{"line":654,"column":13},"end":{"line":654,"column":29}},"393":{"start":{"line":655,"column":13},"end":{"line":655,"column":45}},"394":{"start":{"line":656,"column":2},"end":{"line":656,"column":22}},"395":{"start":{"line":657,"column":2},"end":{"line":659,"column":3}},"396":{"start":{"line":658,"column":4},"end":{"line":658,"column":54}},"397":{"start":{"line":660,"column":2},"end":{"line":660,"column":87}},"398":{"start":{"line":663,"column":0},"end":{"line":669,"column":2}},"399":{"start":{"line":664,"column":18},"end":{"line":664,"column":35}},"400":{"start":{"line":665,"column":2},"end":{"line":665,"column":27}},"401":{"start":{"line":666,"column":13},"end":{"line":666,"column":38}},"402":{"start":{"line":667,"column":2},"end":{"line":667,"column":32}},"403":{"start":{"line":668,"column":2},"end":{"line":668,"column":14}},"404":{"start":{"line":671,"column":0},"end":{"line":675,"column":2}},"405":{"start":{"line":672,"column":13},"end":{"line":672,"column":29}},"406":{"start":{"line":673,"column":2},"end":{"line":673,"column":56}},"407":{"start":{"line":674,"column":2},"end":{"line":674,"column":49}},"408":{"start":{"line":677,"column":0},"end":{"line":698,"column":2}},"409":{"start":{"line":679,"column":14},"end":{"line":679,"column":36}},"410":{"start":{"line":680,"column":24},"end":{"line":680,"column":29}},"411":{"start":{"line":682,"column":2},"end":{"line":685,"column":3}},"412":{"start":{"line":683,"column":4},"end":{"line":683,"column":29}},"413":{"start":{"line":684,"column":4},"end":{"line":684,"column":27}},"414":{"start":{"line":687,"column":2},"end":{"line":690,"column":3}},"415":{"start":{"line":688,"column":4},"end":{"line":688,"column":58}},"416":{"start":{"line":689,"column":4},"end":{"line":689,"column":39}},"417":{"start":{"line":692,"column":2},"end":{"line":695,"column":3}},"418":{"start":{"line":693,"column":4},"end":{"line":693,"column":26}},"419":{"start":{"line":694,"column":4},"end":{"line":694,"column":39}},"420":{"start":{"line":697,"column":2},"end":{"line":697,"column":15}},"421":{"start":{"line":700,"column":0},"end":{"line":709,"column":2}},"422":{"start":{"line":701,"column":2},"end":{"line":701,"column":55}},"423":{"start":{"line":703,"column":2},"end":{"line":708,"column":4}},"424":{"start":{"line":713,"column":2},"end":{"line":723,"column":5}},"425":{"start":{"line":714,"column":4},"end":{"line":722,"column":6}},"426":{"start":{"line":715,"column":6},"end":{"line":719,"column":7}},"427":{"start":{"line":718,"column":8},"end":{"line":718,"column":57}},"428":{"start":{"line":721,"column":6},"end":{"line":721,"column":53}},"429":{"start":{"line":726,"column":2},"end":{"line":737,"column":5}},"430":{"start":{"line":727,"column":4},"end":{"line":736,"column":6}},"431":{"start":{"line":729,"column":6},"end":{"line":735,"column":7}},"432":{"start":{"line":730,"column":19},"end":{"line":730,"column":35}},"433":{"start":{"line":731,"column":8},"end":{"line":731,"column":20}},"434":{"start":{"line":732,"column":8},"end":{"line":732,"column":45}},"435":{"start":{"line":734,"column":8},"end":{"line":734,"column":55}},"436":{"start":{"line":740,"column":2},"end":{"line":758,"column":5}},"437":{"start":{"line":741,"column":4},"end":{"line":757,"column":6}},"438":{"start":{"line":742,"column":6},"end":{"line":754,"column":7}},"439":{"start":{"line":743,"column":8},"end":{"line":753,"column":9}},"440":{"start":{"line":744,"column":10},"end":{"line":746,"column":11}},"441":{"start":{"line":745,"column":12},"end":{"line":745,"column":47}},"442":{"start":{"line":747,"column":15},"end":{"line":753,"column":9}},"443":{"start":{"line":748,"column":10},"end":{"line":752,"column":11}},"444":{"start":{"line":749,"column":12},"end":{"line":749,"column":49}},"445":{"start":{"line":750,"column":17},"end":{"line":752,"column":11}},"446":{"start":{"line":751,"column":12},"end":{"line":751,"column":49}},"447":{"start":{"line":756,"column":6},"end":{"line":756,"column":42}},"448":{"start":{"line":761,"column":2},"end":{"line":767,"column":5}},"449":{"start":{"line":762,"column":4},"end":{"line":766,"column":6}},"450":{"start":{"line":763,"column":6},"end":{"line":765,"column":30}},"451":{"start":{"line":769,"column":2},"end":{"line":790,"column":5}},"452":{"start":{"line":770,"column":4},"end":{"line":789,"column":6}},"453":{"start":{"line":773,"column":6},"end":{"line":786,"column":7}},"454":{"start":{"line":774,"column":22},"end":{"line":774,"column":40}},"455":{"start":{"line":775,"column":8},"end":{"line":785,"column":9}},"456":{"start":{"line":776,"column":10},"end":{"line":776,"column":66}},"457":{"start":{"line":778,"column":10},"end":{"line":784,"column":11}},"458":{"start":{"line":779,"column":12},"end":{"line":779,"column":31}},"459":{"start":{"line":780,"column":12},"end":{"line":780,"column":65}},"460":{"start":{"line":781,"column":12},"end":{"line":781,"column":24}},"461":{"start":{"line":783,"column":12},"end":{"line":783,"column":22}},"462":{"start":{"line":788,"column":6},"end":{"line":788,"column":62}},"463":{"start":{"line":792,"column":2},"end":{"line":809,"column":5}},"464":{"start":{"line":793,"column":4},"end":{"line":808,"column":6}},"465":{"start":{"line":794,"column":6},"end":{"line":794,"column":56}},"466":{"start":{"line":795,"column":6},"end":{"line":797,"column":7}},"467":{"start":{"line":796,"column":8},"end":{"line":796,"column":29}},"468":{"start":{"line":799,"column":6},"end":{"line":805,"column":7}},"469":{"start":{"line":800,"column":27},"end":{"line":800,"column":63}},"470":{"start":{"line":801,"column":8},"end":{"line":801,"column":39}},"471":{"start":{"line":802,"column":8},"end":{"line":802,"column":69}},"472":{"start":{"line":804,"column":8},"end":{"line":804,"column":67}},"473":{"start":{"line":807,"column":6},"end":{"line":807,"column":18}},"474":{"start":{"line":811,"column":2},"end":{"line":819,"column":5}},"475":{"start":{"line":812,"column":4},"end":{"line":818,"column":6}},"476":{"start":{"line":813,"column":6},"end":{"line":813,"column":36}},"477":{"start":{"line":814,"column":6},"end":{"line":816,"column":7}},"478":{"start":{"line":815,"column":8},"end":{"line":815,"column":53}},"479":{"start":{"line":817,"column":6},"end":{"line":817,"column":18}},"480":{"start":{"line":821,"column":2},"end":{"line":847,"column":5}},"481":{"start":{"line":822,"column":4},"end":{"line":846,"column":6}},"482":{"start":{"line":823,"column":6},"end":{"line":845,"column":7}},"483":{"start":{"line":824,"column":8},"end":{"line":824,"column":33}},"484":{"start":{"line":826,"column":30},"end":{"line":826,"column":46}},"485":{"start":{"line":827,"column":8},"end":{"line":827,"column":20}},"486":{"start":{"line":829,"column":8},"end":{"line":837,"column":9}},"487":{"start":{"line":831,"column":10},"end":{"line":831,"column":57}},"488":{"start":{"line":832,"column":10},"end":{"line":832,"column":37}},"489":{"start":{"line":833,"column":10},"end":{"line":833,"column":22}},"490":{"start":{"line":836,"column":10},"end":{"line":836,"column":58}},"491":{"start":{"line":838,"column":13},"end":{"line":845,"column":7}},"492":{"start":{"line":839,"column":8},"end":{"line":839,"column":33}},"493":{"start":{"line":840,"column":30},"end":{"line":840,"column":46}},"494":{"start":{"line":841,"column":8},"end":{"line":841,"column":20}},"495":{"start":{"line":842,"column":8},"end":{"line":842,"column":56}},"496":{"start":{"line":844,"column":8},"end":{"line":844,"column":38}},"497":{"start":{"line":849,"column":2},"end":{"line":856,"column":5}},"498":{"start":{"line":850,"column":4},"end":{"line":855,"column":6}},"499":{"start":{"line":851,"column":6},"end":{"line":851,"column":35}},"500":{"start":{"line":852,"column":6},"end":{"line":854,"column":7}},"501":{"start":{"line":853,"column":8},"end":{"line":853,"column":71}},"502":{"start":{"line":860,"column":2},"end":{"line":868,"column":5}},"503":{"start":{"line":861,"column":4},"end":{"line":867,"column":6}},"504":{"start":{"line":862,"column":6},"end":{"line":866,"column":7}},"505":{"start":{"line":863,"column":8},"end":{"line":863,"column":21}},"506":{"start":{"line":865,"column":8},"end":{"line":865,"column":38}},"507":{"start":{"line":871,"column":2},"end":{"line":879,"column":5}},"508":{"start":{"line":872,"column":4},"end":{"line":878,"column":6}},"509":{"start":{"line":873,"column":6},"end":{"line":877,"column":7}},"510":{"start":{"line":874,"column":8},"end":{"line":874,"column":47}},"511":{"start":{"line":876,"column":8},"end":{"line":876,"column":38}},"512":{"start":{"line":882,"column":2},"end":{"line":886,"column":5}},"513":{"start":{"line":883,"column":4},"end":{"line":885,"column":6}},"514":{"start":{"line":884,"column":6},"end":{"line":884,"column":54}},"515":{"start":{"line":884,"column":30},"end":{"line":884,"column":54}},"516":{"start":{"line":888,"column":2},"end":{"line":896,"column":5}},"517":{"start":{"line":889,"column":4},"end":{"line":895,"column":6}},"518":{"start":{"line":890,"column":6},"end":{"line":894,"column":7}},"519":{"start":{"line":891,"column":8},"end":{"line":891,"column":75}},"520":{"start":{"line":893,"column":8},"end":{"line":893,"column":49}},"521":{"start":{"line":899,"column":2},"end":{"line":909,"column":5}},"522":{"start":{"line":900,"column":4},"end":{"line":908,"column":6}},"523":{"start":{"line":901,"column":6},"end":{"line":906,"column":7}},"524":{"start":{"line":902,"column":19},"end":{"line":902,"column":30}},"525":{"start":{"line":903,"column":8},"end":{"line":905,"column":9}},"526":{"start":{"line":904,"column":10},"end":{"line":904,"column":55}},"527":{"start":{"line":907,"column":6},"end":{"line":907,"column":51}},"528":{"start":{"line":913,"column":2},"end":{"line":924,"column":5}},"529":{"start":{"line":914,"column":4},"end":{"line":923,"column":6}},"530":{"start":{"line":915,"column":6},"end":{"line":920,"column":7}},"531":{"start":{"line":916,"column":19},"end":{"line":916,"column":30}},"532":{"start":{"line":917,"column":8},"end":{"line":919,"column":9}},"533":{"start":{"line":918,"column":10},"end":{"line":918,"column":57}},"534":{"start":{"line":922,"column":6},"end":{"line":922,"column":22}},"535":{"start":{"line":928,"column":2},"end":{"line":941,"column":5}},"536":{"start":{"line":929,"column":4},"end":{"line":940,"column":6}},"537":{"start":{"line":930,"column":22},"end":{"line":930,"column":38}},"538":{"start":{"line":931,"column":17},"end":{"line":931,"column":69}},"539":{"start":{"line":932,"column":6},"end":{"line":939,"column":7}},"540":{"start":{"line":933,"column":8},"end":{"line":933,"column":39}},"541":{"start":{"line":934,"column":8},"end":{"line":934,"column":36}},"542":{"start":{"line":935,"column":8},"end":{"line":935,"column":66}},"543":{"start":{"line":936,"column":8},"end":{"line":936,"column":64}},"544":{"start":{"line":938,"column":8},"end":{"line":938,"column":20}},"545":{"start":{"line":943,"column":2},"end":{"line":949,"column":5}},"546":{"start":{"line":944,"column":4},"end":{"line":948,"column":6}},"547":{"start":{"line":945,"column":6},"end":{"line":947,"column":7}},"548":{"start":{"line":946,"column":8},"end":{"line":946,"column":44}},"549":{"start":{"line":952,"column":2},"end":{"line":959,"column":5}},"550":{"start":{"line":953,"column":4},"end":{"line":958,"column":6}},"551":{"start":{"line":954,"column":6},"end":{"line":956,"column":7}},"552":{"start":{"line":955,"column":8},"end":{"line":955,"column":61}},"553":{"start":{"line":957,"column":6},"end":{"line":957,"column":36}},"554":{"start":{"line":962,"column":2},"end":{"line":966,"column":5}},"555":{"start":{"line":963,"column":4},"end":{"line":965,"column":6}},"556":{"start":{"line":964,"column":6},"end":{"line":964,"column":54}},"557":{"start":{"line":969,"column":2},"end":{"line":977,"column":5}},"558":{"start":{"line":970,"column":4},"end":{"line":976,"column":6}},"559":{"start":{"line":971,"column":6},"end":{"line":973,"column":7}},"560":{"start":{"line":972,"column":8},"end":{"line":972,"column":73}},"561":{"start":{"line":974,"column":6},"end":{"line":974,"column":53}},"562":{"start":{"line":975,"column":6},"end":{"line":975,"column":66}},"563":{"start":{"line":980,"column":2},"end":{"line":1001,"column":5}},"564":{"start":{"line":981,"column":4},"end":{"line":1000,"column":6}},"565":{"start":{"line":982,"column":6},"end":{"line":982,"column":42}},"566":{"start":{"line":983,"column":6},"end":{"line":985,"column":7}},"567":{"start":{"line":984,"column":8},"end":{"line":984,"column":78}},"568":{"start":{"line":986,"column":6},"end":{"line":999,"column":7}},"569":{"start":{"line":987,"column":8},"end":{"line":987,"column":20}},"570":{"start":{"line":988,"column":26},"end":{"line":988,"column":46}},"571":{"start":{"line":989,"column":8},"end":{"line":998,"column":37}},"572":{"start":{"line":990,"column":21},"end":{"line":990,"column":37}},"573":{"start":{"line":991,"column":10},"end":{"line":991,"column":43}},"574":{"start":{"line":992,"column":10},"end":{"line":996,"column":11}},"575":{"start":{"line":993,"column":14},"end":{"line":993,"column":79}},"576":{"start":{"line":995,"column":14},"end":{"line":995,"column":41}},"577":{"start":{"line":997,"column":10},"end":{"line":997,"column":69}},"578":{"start":{"line":1004,"column":2},"end":{"line":1021,"column":5}},"579":{"start":{"line":1005,"column":4},"end":{"line":1020,"column":6}},"580":{"start":{"line":1009,"column":6},"end":{"line":1012,"column":7}},"581":{"start":{"line":1010,"column":8},"end":{"line":1010,"column":66}},"582":{"start":{"line":1011,"column":8},"end":{"line":1011,"column":54}},"583":{"start":{"line":1011,"column":36},"end":{"line":1011,"column":54}},"584":{"start":{"line":1014,"column":6},"end":{"line":1014,"column":35}},"585":{"start":{"line":1017,"column":6},"end":{"line":1019,"column":7}},"586":{"start":{"line":1018,"column":8},"end":{"line":1018,"column":61}},"587":{"start":{"line":1023,"column":2},"end":{"line":1034,"column":5}},"588":{"start":{"line":1024,"column":4},"end":{"line":1033,"column":6}},"589":{"start":{"line":1025,"column":6},"end":{"line":1027,"column":7}},"590":{"start":{"line":1026,"column":8},"end":{"line":1026,"column":30}},"591":{"start":{"line":1028,"column":6},"end":{"line":1030,"column":7}},"592":{"start":{"line":1029,"column":8},"end":{"line":1029,"column":62}},"593":{"start":{"line":1031,"column":6},"end":{"line":1031,"column":41}},"594":{"start":{"line":1032,"column":6},"end":{"line":1032,"column":19}},"595":{"start":{"line":1038,"column":2},"end":{"line":1058,"column":5}},"596":{"start":{"line":1039,"column":4},"end":{"line":1057,"column":6}},"597":{"start":{"line":1040,"column":6},"end":{"line":1040,"column":32}},"598":{"start":{"line":1042,"column":17},"end":{"line":1042,"column":21}},"599":{"start":{"line":1043,"column":6},"end":{"line":1047,"column":7}},"600":{"start":{"line":1044,"column":8},"end":{"line":1044,"column":24}},"601":{"start":{"line":1045,"column":13},"end":{"line":1047,"column":7}},"602":{"start":{"line":1046,"column":8},"end":{"line":1046,"column":22}},"603":{"start":{"line":1048,"column":6},"end":{"line":1054,"column":7}},"604":{"start":{"line":1049,"column":17},"end":{"line":1049,"column":33}},"605":{"start":{"line":1050,"column":8},"end":{"line":1053,"column":9}},"606":{"start":{"line":1051,"column":10},"end":{"line":1051,"column":22}},"607":{"start":{"line":1052,"column":10},"end":{"line":1052,"column":33}},"608":{"start":{"line":1056,"column":6},"end":{"line":1056,"column":29}},"609":{"start":{"line":1061,"column":2},"end":{"line":1068,"column":5}},"610":{"start":{"line":1062,"column":4},"end":{"line":1067,"column":6}},"611":{"start":{"line":1063,"column":6},"end":{"line":1065,"column":7}},"612":{"start":{"line":1064,"column":8},"end":{"line":1064,"column":71}},"613":{"start":{"line":1066,"column":6},"end":{"line":1066,"column":29}},"614":{"start":{"line":1071,"column":2},"end":{"line":1079,"column":5}},"615":{"start":{"line":1072,"column":4},"end":{"line":1078,"column":6}},"616":{"start":{"line":1073,"column":6},"end":{"line":1073,"column":29}},"617":{"start":{"line":1074,"column":6},"end":{"line":1077,"column":7}},"618":{"start":{"line":1075,"column":8},"end":{"line":1075,"column":64}},"619":{"start":{"line":1076,"column":8},"end":{"line":1076,"column":47}},"620":{"start":{"line":1082,"column":2},"end":{"line":1090,"column":5}},"621":{"start":{"line":1083,"column":4},"end":{"line":1089,"column":6}},"622":{"start":{"line":1084,"column":6},"end":{"line":1086,"column":7}},"623":{"start":{"line":1085,"column":8},"end":{"line":1085,"column":57}},"624":{"start":{"line":1088,"column":6},"end":{"line":1088,"column":42}},"625":{"start":{"line":1093,"column":2},"end":{"line":1097,"column":5}},"626":{"start":{"line":1094,"column":4},"end":{"line":1096,"column":6}},"627":{"start":{"line":1095,"column":6},"end":{"line":1095,"column":54}},"628":{"start":{"line":1109,"column":2},"end":{"line":1156,"column":5}},"629":{"start":{"line":1110,"column":4},"end":{"line":1155,"column":6}},"630":{"start":{"line":1111,"column":21},"end":{"line":1111,"column":25}},"631":{"start":{"line":1112,"column":6},"end":{"line":1124,"column":7}},"632":{"start":{"line":1113,"column":22},"end":{"line":1113,"column":40}},"633":{"start":{"line":1114,"column":8},"end":{"line":1123,"column":9}},"634":{"start":{"line":1115,"column":10},"end":{"line":1115,"column":41}},"635":{"start":{"line":1117,"column":10},"end":{"line":1122,"column":11}},"636":{"start":{"line":1118,"column":12},"end":{"line":1118,"column":31}},"637":{"start":{"line":1119,"column":12},"end":{"line":1119,"column":27}},"638":{"start":{"line":1121,"column":12},"end":{"line":1121,"column":22}},"639":{"start":{"line":1128,"column":6},"end":{"line":1128,"column":50}},"640":{"start":{"line":1129,"column":6},"end":{"line":1151,"column":7}},"641":{"start":{"line":1132,"column":8},"end":{"line":1139,"column":9}},"642":{"start":{"line":1133,"column":10},"end":{"line":1133,"column":68}},"643":{"start":{"line":1135,"column":10},"end":{"line":1135,"column":52}},"644":{"start":{"line":1136,"column":10},"end":{"line":1136,"column":58}},"645":{"start":{"line":1138,"column":10},"end":{"line":1138,"column":32}},"646":{"start":{"line":1141,"column":8},"end":{"line":1150,"column":9}},"647":{"start":{"line":1142,"column":10},"end":{"line":1142,"column":33}},"648":{"start":{"line":1143,"column":15},"end":{"line":1150,"column":9}},"649":{"start":{"line":1144,"column":10},"end":{"line":1144,"column":25}},"650":{"start":{"line":1146,"column":10},"end":{"line":1149,"column":12}},"651":{"start":{"line":1152,"column":6},"end":{"line":1152,"column":31}},"652":{"start":{"line":1154,"column":6},"end":{"line":1154,"column":37}},"653":{"start":{"line":1159,"column":2},"end":{"line":1179,"column":5}},"654":{"start":{"line":1160,"column":4},"end":{"line":1178,"column":6}},"655":{"start":{"line":1161,"column":6},"end":{"line":1175,"column":7}},"656":{"start":{"line":1162,"column":20},"end":{"line":1162,"column":38}},"657":{"start":{"line":1163,"column":8},"end":{"line":1174,"column":9}},"658":{"start":{"line":1164,"column":27},"end":{"line":1164,"column":57}},"659":{"start":{"line":1165,"column":10},"end":{"line":1165,"column":55}},"660":{"start":{"line":1165,"column":37},"end":{"line":1165,"column":55}},"661":{"start":{"line":1167,"column":10},"end":{"line":1167,"column":39}},"662":{"start":{"line":1169,"column":10},"end":{"line":1173,"column":11}},"663":{"start":{"line":1170,"column":12},"end":{"line":1170,"column":31}},"664":{"start":{"line":1172,"column":12},"end":{"line":1172,"column":22}},"665":{"start":{"line":1177,"column":6},"end":{"line":1177,"column":36}},"666":{"start":{"line":1181,"column":2},"end":{"line":1189,"column":5}},"667":{"start":{"line":1182,"column":4},"end":{"line":1188,"column":6}},"668":{"start":{"line":1183,"column":6},"end":{"line":1187,"column":7}},"669":{"start":{"line":1184,"column":8},"end":{"line":1184,"column":20}},"670":{"start":{"line":1186,"column":8},"end":{"line":1186,"column":32}}},"fnMap":{"1":{"name":"(anonymous_1)","decl":{"start":{"line":10,"column":30},"end":{"line":10,"column":31}},"loc":{"start":{"line":10,"column":68},"end":{"line":22,"column":1}}},"2":{"name":"(anonymous_2)","decl":{"start":{"line":24,"column":27},"end":{"line":24,"column":28}},"loc":{"start":{"line":24,"column":43},"end":{"line":28,"column":1}}},"3":{"name":"(anonymous_3)","decl":{"start":{"line":30,"column":30},"end":{"line":30,"column":31}},"loc":{"start":{"line":30,"column":46},"end":{"line":59,"column":1}}},"4":{"name":"(anonymous_4)","decl":{"start":{"line":61,"column":22},"end":{"line":61,"column":23}},"loc":{"start":{"line":61,"column":38},"end":{"line":81,"column":1}}},"5":{"name":"(anonymous_5)","decl":{"start":{"line":83,"column":30},"end":{"line":83,"column":31}},"loc":{"start":{"line":83,"column":46},"end":{"line":88,"column":1}}},"6":{"name":"(anonymous_6)","decl":{"start":{"line":90,"column":28},"end":{"line":90,"column":29}},"loc":{"start":{"line":90,"column":44},"end":{"line":113,"column":1}}},"7":{"name":"(anonymous_7)","decl":{"start":{"line":115,"column":35},"end":{"line":115,"column":36}},"loc":{"start":{"line":115,"column":51},"end":{"line":121,"column":1}}},"8":{"name":"(anonymous_8)","decl":{"start":{"line":123,"column":31},"end":{"line":123,"column":32}},"loc":{"start":{"line":123,"column":47},"end":{"line":127,"column":1}}},"9":{"name":"(anonymous_9)","decl":{"start":{"line":129,"column":31},"end":{"line":129,"column":32}},"loc":{"start":{"line":129,"column":47},"end":{"line":133,"column":1}}},"10":{"name":"(anonymous_10)","decl":{"start":{"line":137,"column":27},"end":{"line":137,"column":28}},"loc":{"start":{"line":137,"column":56},"end":{"line":163,"column":1}}},"11":{"name":"(anonymous_11)","decl":{"start":{"line":165,"column":31},"end":{"line":165,"column":32}},"loc":{"start":{"line":165,"column":43},"end":{"line":176,"column":1}}},"12":{"name":"(anonymous_12)","decl":{"start":{"line":178,"column":24},"end":{"line":178,"column":25}},"loc":{"start":{"line":178,"column":40},"end":{"line":181,"column":1}}},"13":{"name":"(anonymous_13)","decl":{"start":{"line":185,"column":24},"end":{"line":185,"column":25}},"loc":{"start":{"line":185,"column":40},"end":{"line":201,"column":1}}},"14":{"name":"(anonymous_14)","decl":{"start":{"line":205,"column":28},"end":{"line":205,"column":29}},"loc":{"start":{"line":205,"column":40},"end":{"line":229,"column":1}}},"15":{"name":"(anonymous_15)","decl":{"start":{"line":231,"column":39},"end":{"line":231,"column":40}},"loc":{"start":{"line":231,"column":51},"end":{"line":255,"column":1}}},"16":{"name":"(anonymous_16)","decl":{"start":{"line":257,"column":41},"end":{"line":257,"column":42}},"loc":{"start":{"line":257,"column":53},"end":{"line":275,"column":1}}},"17":{"name":"(anonymous_17)","decl":{"start":{"line":277,"column":32},"end":{"line":277,"column":33}},"loc":{"start":{"line":277,"column":44},"end":{"line":279,"column":1}}},"18":{"name":"(anonymous_18)","decl":{"start":{"line":281,"column":32},"end":{"line":281,"column":33}},"loc":{"start":{"line":281,"column":58},"end":{"line":292,"column":1}}},"19":{"name":"(anonymous_19)","decl":{"start":{"line":294,"column":34},"end":{"line":294,"column":35}},"loc":{"start":{"line":294,"column":50},"end":{"line":318,"column":1}}},"20":{"name":"(anonymous_20)","decl":{"start":{"line":320,"column":31},"end":{"line":320,"column":32}},"loc":{"start":{"line":320,"column":76},"end":{"line":328,"column":1}}},"21":{"name":"(anonymous_21)","decl":{"start":{"line":330,"column":37},"end":{"line":330,"column":38}},"loc":{"start":{"line":330,"column":63},"end":{"line":336,"column":1}}},"22":{"name":"(anonymous_22)","decl":{"start":{"line":338,"column":25},"end":{"line":338,"column":26}},"loc":{"start":{"line":338,"column":48},"end":{"line":389,"column":1}}},"23":{"name":"(anonymous_23)","decl":{"start":{"line":391,"column":29},"end":{"line":391,"column":30}},"loc":{"start":{"line":391,"column":41},"end":{"line":395,"column":1}}},"24":{"name":"(anonymous_24)","decl":{"start":{"line":397,"column":38},"end":{"line":397,"column":39}},"loc":{"start":{"line":397,"column":72},"end":{"line":410,"column":1}}},"25":{"name":"(anonymous_25)","decl":{"start":{"line":412,"column":26},"end":{"line":412,"column":27}},"loc":{"start":{"line":412,"column":60},"end":{"line":423,"column":1}}},"26":{"name":"(anonymous_26)","decl":{"start":{"line":425,"column":25},"end":{"line":425,"column":26}},"loc":{"start":{"line":425,"column":37},"end":{"line":430,"column":1}}},"27":{"name":"(anonymous_27)","decl":{"start":{"line":432,"column":24},"end":{"line":432,"column":25}},"loc":{"start":{"line":432,"column":36},"end":{"line":444,"column":1}}},"28":{"name":"(anonymous_28)","decl":{"start":{"line":446,"column":32},"end":{"line":446,"column":33}},"loc":{"start":{"line":446,"column":44},"end":{"line":456,"column":1}}},"29":{"name":"(anonymous_29)","decl":{"start":{"line":458,"column":33},"end":{"line":458,"column":34}},"loc":{"start":{"line":458,"column":45},"end":{"line":470,"column":1}}},"30":{"name":"(anonymous_30)","decl":{"start":{"line":472,"column":31},"end":{"line":472,"column":32}},"loc":{"start":{"line":472,"column":71},"end":{"line":496,"column":1}}},"31":{"name":"(anonymous_31)","decl":{"start":{"line":501,"column":26},"end":{"line":501,"column":27}},"loc":{"start":{"line":501,"column":38},"end":{"line":619,"column":1}}},"32":{"name":"(anonymous_32)","decl":{"start":{"line":621,"column":26},"end":{"line":621,"column":27}},"loc":{"start":{"line":621,"column":38},"end":{"line":631,"column":1}}},"33":{"name":"(anonymous_33)","decl":{"start":{"line":633,"column":25},"end":{"line":633,"column":26}},"loc":{"start":{"line":633,"column":37},"end":{"line":641,"column":1}}},"34":{"name":"(anonymous_34)","decl":{"start":{"line":643,"column":31},"end":{"line":643,"column":32}},"loc":{"start":{"line":643,"column":43},"end":{"line":651,"column":1}}},"35":{"name":"(anonymous_35)","decl":{"start":{"line":653,"column":24},"end":{"line":653,"column":25}},"loc":{"start":{"line":653,"column":36},"end":{"line":661,"column":1}}},"36":{"name":"(anonymous_36)","decl":{"start":{"line":663,"column":19},"end":{"line":663,"column":20}},"loc":{"start":{"line":663,"column":31},"end":{"line":669,"column":1}}},"37":{"name":"(anonymous_37)","decl":{"start":{"line":671,"column":29},"end":{"line":671,"column":30}},"loc":{"start":{"line":671,"column":41},"end":{"line":675,"column":1}}},"38":{"name":"(anonymous_38)","decl":{"start":{"line":677,"column":40},"end":{"line":677,"column":41}},"loc":{"start":{"line":677,"column":93},"end":{"line":698,"column":1}}},"39":{"name":"(anonymous_39)","decl":{"start":{"line":700,"column":25},"end":{"line":700,"column":26}},"loc":{"start":{"line":700,"column":41},"end":{"line":709,"column":1}}},"40":{"name":"(anonymous_40)","decl":{"start":{"line":711,"column":15},"end":{"line":711,"column":16}},"loc":{"start":{"line":711,"column":35},"end":{"line":1190,"column":1}}},"41":{"name":"(anonymous_41)","decl":{"start":{"line":713,"column":39},"end":{"line":713,"column":40}},"loc":{"start":{"line":713,"column":56},"end":{"line":723,"column":3}}},"42":{"name":"(anonymous_42)","decl":{"start":{"line":714,"column":11},"end":{"line":714,"column":12}},"loc":{"start":{"line":714,"column":44},"end":{"line":722,"column":5}}},"43":{"name":"(anonymous_43)","decl":{"start":{"line":726,"column":36},"end":{"line":726,"column":37}},"loc":{"start":{"line":726,"column":53},"end":{"line":737,"column":3}}},"44":{"name":"(anonymous_44)","decl":{"start":{"line":727,"column":11},"end":{"line":727,"column":12}},"loc":{"start":{"line":727,"column":44},"end":{"line":736,"column":5}}},"45":{"name":"(anonymous_45)","decl":{"start":{"line":740,"column":46},"end":{"line":740,"column":47}},"loc":{"start":{"line":740,"column":63},"end":{"line":758,"column":3}}},"46":{"name":"(anonymous_46)","decl":{"start":{"line":741,"column":11},"end":{"line":741,"column":12}},"loc":{"start":{"line":741,"column":33},"end":{"line":757,"column":5}}},"47":{"name":"(anonymous_47)","decl":{"start":{"line":761,"column":50},"end":{"line":761,"column":51}},"loc":{"start":{"line":761,"column":67},"end":{"line":767,"column":3}}},"48":{"name":"(anonymous_48)","decl":{"start":{"line":762,"column":11},"end":{"line":762,"column":12}},"loc":{"start":{"line":762,"column":23},"end":{"line":766,"column":5}}},"49":{"name":"(anonymous_49)","decl":{"start":{"line":769,"column":38},"end":{"line":769,"column":39}},"loc":{"start":{"line":769,"column":55},"end":{"line":790,"column":3}}},"50":{"name":"(anonymous_50)","decl":{"start":{"line":770,"column":11},"end":{"line":770,"column":12}},"loc":{"start":{"line":770,"column":71},"end":{"line":789,"column":5}}},"51":{"name":"(anonymous_51)","decl":{"start":{"line":792,"column":36},"end":{"line":792,"column":37}},"loc":{"start":{"line":792,"column":53},"end":{"line":809,"column":3}}},"52":{"name":"(anonymous_52)","decl":{"start":{"line":793,"column":11},"end":{"line":793,"column":12}},"loc":{"start":{"line":793,"column":47},"end":{"line":808,"column":5}}},"53":{"name":"(anonymous_53)","decl":{"start":{"line":811,"column":33},"end":{"line":811,"column":34}},"loc":{"start":{"line":811,"column":50},"end":{"line":819,"column":3}}},"54":{"name":"(anonymous_54)","decl":{"start":{"line":812,"column":11},"end":{"line":812,"column":12}},"loc":{"start":{"line":812,"column":27},"end":{"line":818,"column":5}}},"55":{"name":"(anonymous_55)","decl":{"start":{"line":821,"column":44},"end":{"line":821,"column":45}},"loc":{"start":{"line":821,"column":61},"end":{"line":847,"column":3}}},"56":{"name":"(anonymous_56)","decl":{"start":{"line":822,"column":11},"end":{"line":822,"column":12}},"loc":{"start":{"line":822,"column":27},"end":{"line":846,"column":5}}},"57":{"name":"(anonymous_57)","decl":{"start":{"line":849,"column":34},"end":{"line":849,"column":35}},"loc":{"start":{"line":849,"column":51},"end":{"line":856,"column":3}}},"58":{"name":"(anonymous_58)","decl":{"start":{"line":850,"column":11},"end":{"line":850,"column":12}},"loc":{"start":{"line":850,"column":27},"end":{"line":855,"column":5}}},"59":{"name":"(anonymous_59)","decl":{"start":{"line":860,"column":31},"end":{"line":860,"column":32}},"loc":{"start":{"line":860,"column":48},"end":{"line":868,"column":3}}},"60":{"name":"(anonymous_60)","decl":{"start":{"line":861,"column":11},"end":{"line":861,"column":12}},"loc":{"start":{"line":861,"column":27},"end":{"line":867,"column":5}}},"61":{"name":"(anonymous_61)","decl":{"start":{"line":871,"column":31},"end":{"line":871,"column":32}},"loc":{"start":{"line":871,"column":48},"end":{"line":879,"column":3}}},"62":{"name":"(anonymous_62)","decl":{"start":{"line":872,"column":11},"end":{"line":872,"column":12}},"loc":{"start":{"line":872,"column":27},"end":{"line":878,"column":5}}},"63":{"name":"(anonymous_63)","decl":{"start":{"line":882,"column":35},"end":{"line":882,"column":36}},"loc":{"start":{"line":882,"column":52},"end":{"line":886,"column":3}}},"64":{"name":"(anonymous_64)","decl":{"start":{"line":883,"column":11},"end":{"line":883,"column":12}},"loc":{"start":{"line":883,"column":23},"end":{"line":885,"column":5}}},"65":{"name":"(anonymous_65)","decl":{"start":{"line":888,"column":34},"end":{"line":888,"column":35}},"loc":{"start":{"line":888,"column":51},"end":{"line":896,"column":3}}},"66":{"name":"(anonymous_66)","decl":{"start":{"line":889,"column":11},"end":{"line":889,"column":12}},"loc":{"start":{"line":889,"column":38},"end":{"line":895,"column":5}}},"67":{"name":"(anonymous_67)","decl":{"start":{"line":899,"column":38},"end":{"line":899,"column":39}},"loc":{"start":{"line":899,"column":55},"end":{"line":909,"column":3}}},"68":{"name":"(anonymous_68)","decl":{"start":{"line":900,"column":11},"end":{"line":900,"column":12}},"loc":{"start":{"line":900,"column":42},"end":{"line":908,"column":5}}},"69":{"name":"(anonymous_69)","decl":{"start":{"line":913,"column":38},"end":{"line":913,"column":39}},"loc":{"start":{"line":913,"column":50},"end":{"line":924,"column":3}}},"70":{"name":"(anonymous_70)","decl":{"start":{"line":914,"column":11},"end":{"line":914,"column":12}},"loc":{"start":{"line":914,"column":31},"end":{"line":923,"column":5}}},"71":{"name":"(anonymous_71)","decl":{"start":{"line":928,"column":39},"end":{"line":928,"column":40}},"loc":{"start":{"line":928,"column":56},"end":{"line":941,"column":3}}},"72":{"name":"(anonymous_72)","decl":{"start":{"line":929,"column":11},"end":{"line":929,"column":12}},"loc":{"start":{"line":929,"column":57},"end":{"line":940,"column":5}}},"73":{"name":"(anonymous_73)","decl":{"start":{"line":943,"column":31},"end":{"line":943,"column":32}},"loc":{"start":{"line":943,"column":48},"end":{"line":949,"column":3}}},"74":{"name":"(anonymous_74)","decl":{"start":{"line":944,"column":11},"end":{"line":944,"column":12}},"loc":{"start":{"line":944,"column":27},"end":{"line":948,"column":5}}},"75":{"name":"(anonymous_75)","decl":{"start":{"line":952,"column":40},"end":{"line":952,"column":41}},"loc":{"start":{"line":952,"column":57},"end":{"line":959,"column":3}}},"76":{"name":"(anonymous_76)","decl":{"start":{"line":953,"column":11},"end":{"line":953,"column":12}},"loc":{"start":{"line":953,"column":27},"end":{"line":958,"column":5}}},"77":{"name":"(anonymous_77)","decl":{"start":{"line":962,"column":37},"end":{"line":962,"column":38}},"loc":{"start":{"line":962,"column":54},"end":{"line":966,"column":3}}},"78":{"name":"(anonymous_78)","decl":{"start":{"line":963,"column":11},"end":{"line":963,"column":12}},"loc":{"start":{"line":963,"column":23},"end":{"line":965,"column":5}}},"79":{"name":"(anonymous_79)","decl":{"start":{"line":969,"column":38},"end":{"line":969,"column":39}},"loc":{"start":{"line":969,"column":50},"end":{"line":977,"column":3}}},"80":{"name":"(anonymous_80)","decl":{"start":{"line":970,"column":11},"end":{"line":970,"column":12}},"loc":{"start":{"line":970,"column":62},"end":{"line":976,"column":5}}},"81":{"name":"(anonymous_81)","decl":{"start":{"line":980,"column":37},"end":{"line":980,"column":38}},"loc":{"start":{"line":980,"column":54},"end":{"line":1001,"column":3}}},"82":{"name":"(anonymous_82)","decl":{"start":{"line":981,"column":11},"end":{"line":981,"column":12}},"loc":{"start":{"line":981,"column":40},"end":{"line":1000,"column":5}}},"83":{"name":"(anonymous_83)","decl":{"start":{"line":1004,"column":39},"end":{"line":1004,"column":40}},"loc":{"start":{"line":1004,"column":56},"end":{"line":1021,"column":3}}},"84":{"name":"(anonymous_84)","decl":{"start":{"line":1005,"column":11},"end":{"line":1005,"column":12}},"loc":{"start":{"line":1005,"column":27},"end":{"line":1020,"column":5}}},"85":{"name":"(anonymous_85)","decl":{"start":{"line":1023,"column":50},"end":{"line":1023,"column":51}},"loc":{"start":{"line":1023,"column":62},"end":{"line":1034,"column":3}}},"86":{"name":"(anonymous_86)","decl":{"start":{"line":1024,"column":11},"end":{"line":1024,"column":12}},"loc":{"start":{"line":1024,"column":28},"end":{"line":1033,"column":5}}},"87":{"name":"(anonymous_87)","decl":{"start":{"line":1038,"column":43},"end":{"line":1038,"column":44}},"loc":{"start":{"line":1038,"column":60},"end":{"line":1058,"column":3}}},"88":{"name":"(anonymous_88)","decl":{"start":{"line":1039,"column":11},"end":{"line":1039,"column":12}},"loc":{"start":{"line":1039,"column":27},"end":{"line":1057,"column":5}}},"89":{"name":"(anonymous_89)","decl":{"start":{"line":1061,"column":41},"end":{"line":1061,"column":42}},"loc":{"start":{"line":1061,"column":58},"end":{"line":1068,"column":3}}},"90":{"name":"(anonymous_90)","decl":{"start":{"line":1062,"column":11},"end":{"line":1062,"column":12}},"loc":{"start":{"line":1062,"column":27},"end":{"line":1067,"column":5}}},"91":{"name":"(anonymous_91)","decl":{"start":{"line":1071,"column":34},"end":{"line":1071,"column":35}},"loc":{"start":{"line":1071,"column":51},"end":{"line":1079,"column":3}}},"92":{"name":"(anonymous_92)","decl":{"start":{"line":1072,"column":11},"end":{"line":1072,"column":12}},"loc":{"start":{"line":1072,"column":27},"end":{"line":1078,"column":5}}},"93":{"name":"(anonymous_93)","decl":{"start":{"line":1082,"column":55},"end":{"line":1082,"column":56}},"loc":{"start":{"line":1082,"column":72},"end":{"line":1090,"column":3}}},"94":{"name":"(anonymous_94)","decl":{"start":{"line":1083,"column":11},"end":{"line":1083,"column":12}},"loc":{"start":{"line":1083,"column":33},"end":{"line":1089,"column":5}}},"95":{"name":"(anonymous_95)","decl":{"start":{"line":1093,"column":43},"end":{"line":1093,"column":44}},"loc":{"start":{"line":1093,"column":60},"end":{"line":1097,"column":3}}},"96":{"name":"(anonymous_96)","decl":{"start":{"line":1094,"column":11},"end":{"line":1094,"column":12}},"loc":{"start":{"line":1094,"column":23},"end":{"line":1096,"column":5}}},"97":{"name":"(anonymous_97)","decl":{"start":{"line":1109,"column":38},"end":{"line":1109,"column":39}},"loc":{"start":{"line":1109,"column":55},"end":{"line":1156,"column":3}}},"98":{"name":"(anonymous_98)","decl":{"start":{"line":1110,"column":11},"end":{"line":1110,"column":12}},"loc":{"start":{"line":1110,"column":30},"end":{"line":1155,"column":5}}},"99":{"name":"(anonymous_99)","decl":{"start":{"line":1159,"column":32},"end":{"line":1159,"column":33}},"loc":{"start":{"line":1159,"column":49},"end":{"line":1179,"column":3}}},"100":{"name":"(anonymous_100)","decl":{"start":{"line":1160,"column":11},"end":{"line":1160,"column":12}},"loc":{"start":{"line":1160,"column":27},"end":{"line":1178,"column":5}}},"101":{"name":"(anonymous_101)","decl":{"start":{"line":1181,"column":43},"end":{"line":1181,"column":44}},"loc":{"start":{"line":1181,"column":60},"end":{"line":1189,"column":3}}},"102":{"name":"(anonymous_102)","decl":{"start":{"line":1182,"column":11},"end":{"line":1182,"column":12}},"loc":{"start":{"line":1182,"column":23},"end":{"line":1188,"column":5}}}},"branchMap":{"1":{"loc":{"start":{"line":13,"column":14},"end":{"line":13,"column":29}},"type":"binary-expr","locations":[{"start":{"line":13,"column":14},"end":{"line":13,"column":17}},{"start":{"line":13,"column":21},"end":{"line":13,"column":29}}]},"2":{"loc":{"start":{"line":14,"column":2},"end":{"line":18,"column":3}},"type":"if","locations":[{"start":{"line":14,"column":2},"end":{"line":18,"column":3}},{"start":{"line":14,"column":2},"end":{"line":18,"column":3}}]},"3":{"loc":{"start":{"line":15,"column":4},"end":{"line":17,"column":5}},"type":"if","locations":[{"start":{"line":15,"column":4},"end":{"line":17,"column":5}},{"start":{"line":15,"column":4},"end":{"line":17,"column":5}}]},"4":{"loc":{"start":{"line":15,"column":8},"end":{"line":15,"column":61}},"type":"binary-expr","locations":[{"start":{"line":15,"column":8},"end":{"line":15,"column":33}},{"start":{"line":15,"column":37},"end":{"line":15,"column":61}}]},"5":{"loc":{"start":{"line":38,"column":2},"end":{"line":42,"column":3}},"type":"if","locations":[{"start":{"line":38,"column":2},"end":{"line":42,"column":3}},{"start":{"line":38,"column":2},"end":{"line":42,"column":3}}]},"6":{"loc":{"start":{"line":62,"column":2},"end":{"line":80,"column":3}},"type":"if","locations":[{"start":{"line":62,"column":2},"end":{"line":80,"column":3}},{"start":{"line":62,"column":2},"end":{"line":80,"column":3}}]},"7":{"loc":{"start":{"line":64,"column":9},"end":{"line":80,"column":3}},"type":"if","locations":[{"start":{"line":64,"column":9},"end":{"line":80,"column":3}},{"start":{"line":64,"column":9},"end":{"line":80,"column":3}}]},"8":{"loc":{"start":{"line":66,"column":9},"end":{"line":80,"column":3}},"type":"if","locations":[{"start":{"line":66,"column":9},"end":{"line":80,"column":3}},{"start":{"line":66,"column":9},"end":{"line":80,"column":3}}]},"9":{"loc":{"start":{"line":68,"column":9},"end":{"line":80,"column":3}},"type":"if","locations":[{"start":{"line":68,"column":9},"end":{"line":80,"column":3}},{"start":{"line":68,"column":9},"end":{"line":80,"column":3}}]},"10":{"loc":{"start":{"line":69,"column":4},"end":{"line":73,"column":5}},"type":"if","locations":[{"start":{"line":69,"column":4},"end":{"line":73,"column":5}},{"start":{"line":69,"column":4},"end":{"line":73,"column":5}}]},"11":{"loc":{"start":{"line":74,"column":9},"end":{"line":80,"column":3}},"type":"if","locations":[{"start":{"line":74,"column":9},"end":{"line":80,"column":3}},{"start":{"line":74,"column":9},"end":{"line":80,"column":3}}]},"12":{"loc":{"start":{"line":76,"column":9},"end":{"line":80,"column":3}},"type":"if","locations":[{"start":{"line":76,"column":9},"end":{"line":80,"column":3}},{"start":{"line":76,"column":9},"end":{"line":80,"column":3}}]},"13":{"loc":{"start":{"line":93,"column":2},"end":{"line":97,"column":3}},"type":"if","locations":[{"start":{"line":93,"column":2},"end":{"line":97,"column":3}},{"start":{"line":93,"column":2},"end":{"line":97,"column":3}}]},"14":{"loc":{"start":{"line":140,"column":2},"end":{"line":144,"column":3}},"type":"if","locations":[{"start":{"line":140,"column":2},"end":{"line":144,"column":3}},{"start":{"line":140,"column":2},"end":{"line":144,"column":3}}]},"15":{"loc":{"start":{"line":149,"column":2},"end":{"line":153,"column":3}},"type":"if","locations":[{"start":{"line":149,"column":2},"end":{"line":153,"column":3}},{"start":{"line":149,"column":2},"end":{"line":153,"column":3}}]},"16":{"loc":{"start":{"line":155,"column":2},"end":{"line":160,"column":3}},"type":"if","locations":[{"start":{"line":155,"column":2},"end":{"line":160,"column":3}},{"start":{"line":155,"column":2},"end":{"line":160,"column":3}}]},"17":{"loc":{"start":{"line":169,"column":2},"end":{"line":173,"column":3}},"type":"if","locations":[{"start":{"line":169,"column":2},"end":{"line":173,"column":3}},{"start":{"line":169,"column":2},"end":{"line":173,"column":3}}]},"18":{"loc":{"start":{"line":188,"column":2},"end":{"line":192,"column":3}},"type":"if","locations":[{"start":{"line":188,"column":2},"end":{"line":192,"column":3}},{"start":{"line":188,"column":2},"end":{"line":192,"column":3}}]},"19":{"loc":{"start":{"line":209,"column":2},"end":{"line":216,"column":3}},"type":"if","locations":[{"start":{"line":209,"column":2},"end":{"line":216,"column":3}},{"start":{"line":209,"column":2},"end":{"line":216,"column":3}}]},"20":{"loc":{"start":{"line":210,"column":4},"end":{"line":214,"column":5}},"type":"if","locations":[{"start":{"line":210,"column":4},"end":{"line":214,"column":5}},{"start":{"line":210,"column":4},"end":{"line":214,"column":5}}]},"21":{"loc":{"start":{"line":212,"column":11},"end":{"line":214,"column":5}},"type":"if","locations":[{"start":{"line":212,"column":11},"end":{"line":214,"column":5}},{"start":{"line":212,"column":11},"end":{"line":214,"column":5}}]},"22":{"loc":{"start":{"line":223,"column":2},"end":{"line":226,"column":3}},"type":"if","locations":[{"start":{"line":223,"column":2},"end":{"line":226,"column":3}},{"start":{"line":223,"column":2},"end":{"line":226,"column":3}}]},"23":{"loc":{"start":{"line":238,"column":2},"end":{"line":242,"column":3}},"type":"if","locations":[{"start":{"line":238,"column":2},"end":{"line":242,"column":3}},{"start":{"line":238,"column":2},"end":{"line":242,"column":3}}]},"24":{"loc":{"start":{"line":238,"column":6},"end":{"line":238,"column":58}},"type":"binary-expr","locations":[{"start":{"line":238,"column":6},"end":{"line":238,"column":28}},{"start":{"line":238,"column":32},"end":{"line":238,"column":58}}]},"25":{"loc":{"start":{"line":246,"column":4},"end":{"line":248,"column":5}},"type":"if","locations":[{"start":{"line":246,"column":4},"end":{"line":248,"column":5}},{"start":{"line":246,"column":4},"end":{"line":248,"column":5}}]},"26":{"loc":{"start":{"line":266,"column":4},"end":{"line":268,"column":5}},"type":"if","locations":[{"start":{"line":266,"column":4},"end":{"line":268,"column":5}},{"start":{"line":266,"column":4},"end":{"line":268,"column":5}}]},"27":{"loc":{"start":{"line":278,"column":9},"end":{"line":278,"column":106}},"type":"cond-expr","locations":[{"start":{"line":278,"column":57},"end":{"line":278,"column":77}},{"start":{"line":278,"column":80},"end":{"line":278,"column":106}}]},"28":{"loc":{"start":{"line":278,"column":10},"end":{"line":278,"column":53}},"type":"binary-expr","locations":[{"start":{"line":278,"column":10},"end":{"line":278,"column":28}},{"start":{"line":278,"column":32},"end":{"line":278,"column":53}}]},"29":{"loc":{"start":{"line":299,"column":2},"end":{"line":301,"column":3}},"type":"if","locations":[{"start":{"line":299,"column":2},"end":{"line":301,"column":3}},{"start":{"line":299,"column":2},"end":{"line":301,"column":3}}]},"30":{"loc":{"start":{"line":306,"column":4},"end":{"line":308,"column":5}},"type":"if","locations":[{"start":{"line":306,"column":4},"end":{"line":308,"column":5}},{"start":{"line":306,"column":4},"end":{"line":308,"column":5}}]},"31":{"loc":{"start":{"line":311,"column":2},"end":{"line":313,"column":3}},"type":"if","locations":[{"start":{"line":311,"column":2},"end":{"line":313,"column":3}},{"start":{"line":311,"column":2},"end":{"line":313,"column":3}}]},"32":{"loc":{"start":{"line":354,"column":4},"end":{"line":357,"column":5}},"type":"if","locations":[{"start":{"line":354,"column":4},"end":{"line":357,"column":5}},{"start":{"line":354,"column":4},"end":{"line":357,"column":5}}]},"33":{"loc":{"start":{"line":354,"column":8},"end":{"line":354,"column":50}},"type":"binary-expr","locations":[{"start":{"line":354,"column":8},"end":{"line":354,"column":19}},{"start":{"line":354,"column":23},"end":{"line":354,"column":50}}]},"34":{"loc":{"start":{"line":359,"column":4},"end":{"line":383,"column":5}},"type":"if","locations":[{"start":{"line":359,"column":4},"end":{"line":383,"column":5}},{"start":{"line":359,"column":4},"end":{"line":383,"column":5}}]},"35":{"loc":{"start":{"line":361,"column":11},"end":{"line":383,"column":5}},"type":"if","locations":[{"start":{"line":361,"column":11},"end":{"line":383,"column":5}},{"start":{"line":361,"column":11},"end":{"line":383,"column":5}}]},"36":{"loc":{"start":{"line":361,"column":15},"end":{"line":361,"column":62}},"type":"binary-expr","locations":[{"start":{"line":361,"column":15},"end":{"line":361,"column":36}},{"start":{"line":361,"column":40},"end":{"line":361,"column":62}}]},"37":{"loc":{"start":{"line":364,"column":6},"end":{"line":368,"column":7}},"type":"if","locations":[{"start":{"line":364,"column":6},"end":{"line":368,"column":7}},{"start":{"line":364,"column":6},"end":{"line":368,"column":7}}]},"38":{"loc":{"start":{"line":364,"column":10},"end":{"line":364,"column":42}},"type":"binary-expr","locations":[{"start":{"line":364,"column":10},"end":{"line":364,"column":18}},{"start":{"line":364,"column":22},"end":{"line":364,"column":42}}]},"39":{"loc":{"start":{"line":369,"column":6},"end":{"line":382,"column":7}},"type":"if","locations":[{"start":{"line":369,"column":6},"end":{"line":382,"column":7}},{"start":{"line":369,"column":6},"end":{"line":382,"column":7}}]},"40":{"loc":{"start":{"line":369,"column":10},"end":{"line":369,"column":57}},"type":"binary-expr","locations":[{"start":{"line":369,"column":10},"end":{"line":369,"column":32}},{"start":{"line":369,"column":36},"end":{"line":369,"column":57}}]},"41":{"loc":{"start":{"line":373,"column":8},"end":{"line":375,"column":9}},"type":"if","locations":[{"start":{"line":373,"column":8},"end":{"line":375,"column":9}},{"start":{"line":373,"column":8},"end":{"line":375,"column":9}}]},"42":{"loc":{"start":{"line":392,"column":2},"end":{"line":394,"column":3}},"type":"if","locations":[{"start":{"line":392,"column":2},"end":{"line":394,"column":3}},{"start":{"line":392,"column":2},"end":{"line":394,"column":3}}]},"43":{"loc":{"start":{"line":392,"column":6},"end":{"line":392,"column":73}},"type":"binary-expr","locations":[{"start":{"line":392,"column":6},"end":{"line":392,"column":24}},{"start":{"line":392,"column":28},"end":{"line":392,"column":47}},{"start":{"line":392,"column":51},"end":{"line":392,"column":73}}]},"44":{"loc":{"start":{"line":398,"column":13},"end":{"line":398,"column":41}},"type":"binary-expr","locations":[{"start":{"line":398,"column":13},"end":{"line":398,"column":21}},{"start":{"line":398,"column":25},"end":{"line":398,"column":41}}]},"45":{"loc":{"start":{"line":399,"column":13},"end":{"line":399,"column":44}},"type":"binary-expr","locations":[{"start":{"line":399,"column":13},"end":{"line":399,"column":21}},{"start":{"line":399,"column":25},"end":{"line":399,"column":44}}]},"46":{"loc":{"start":{"line":400,"column":13},"end":{"line":400,"column":41}},"type":"binary-expr","locations":[{"start":{"line":400,"column":13},"end":{"line":400,"column":15}},{"start":{"line":400,"column":19},"end":{"line":400,"column":41}}]},"47":{"loc":{"start":{"line":418,"column":2},"end":{"line":420,"column":3}},"type":"if","locations":[{"start":{"line":418,"column":2},"end":{"line":420,"column":3}},{"start":{"line":418,"column":2},"end":{"line":420,"column":3}}]},"48":{"loc":{"start":{"line":437,"column":9},"end":{"line":437,"column":71}},"type":"binary-expr","locations":[{"start":{"line":437,"column":9},"end":{"line":437,"column":43}},{"start":{"line":437,"column":47},"end":{"line":437,"column":71}}]},"49":{"loc":{"start":{"line":439,"column":4},"end":{"line":439,"column":39}},"type":"if","locations":[{"start":{"line":439,"column":4},"end":{"line":439,"column":39}},{"start":{"line":439,"column":4},"end":{"line":439,"column":39}}]},"50":{"loc":{"start":{"line":450,"column":2},"end":{"line":452,"column":3}},"type":"if","locations":[{"start":{"line":450,"column":2},"end":{"line":452,"column":3}},{"start":{"line":450,"column":2},"end":{"line":452,"column":3}}]},"51":{"loc":{"start":{"line":462,"column":4},"end":{"line":464,"column":5}},"type":"if","locations":[{"start":{"line":462,"column":4},"end":{"line":464,"column":5}},{"start":{"line":462,"column":4},"end":{"line":464,"column":5}}]},"52":{"loc":{"start":{"line":466,"column":2},"end":{"line":468,"column":3}},"type":"if","locations":[{"start":{"line":466,"column":2},"end":{"line":468,"column":3}},{"start":{"line":466,"column":2},"end":{"line":468,"column":3}}]},"53":{"loc":{"start":{"line":473,"column":2},"end":{"line":495,"column":3}},"type":"switch","locations":[{"start":{"line":474,"column":4},"end":{"line":475,"column":56}},{"start":{"line":477,"column":4},"end":{"line":478,"column":57}},{"start":{"line":480,"column":4},"end":{"line":480,"column":16}},{"start":{"line":481,"column":4},"end":{"line":482,"column":60}},{"start":{"line":484,"column":4},"end":{"line":485,"column":58}},{"start":{"line":487,"column":4},"end":{"line":488,"column":59}},{"start":{"line":490,"column":4},"end":{"line":491,"column":59}},{"start":{"line":493,"column":4},"end":{"line":494,"column":63}}]},"54":{"loc":{"start":{"line":508,"column":2},"end":{"line":616,"column":3}},"type":"switch","locations":[{"start":{"line":509,"column":4},"end":{"line":510,"column":94}},{"start":{"line":512,"column":4},"end":{"line":513,"column":40}},{"start":{"line":515,"column":4},"end":{"line":516,"column":39}},{"start":{"line":518,"column":4},"end":{"line":533,"column":12}},{"start":{"line":535,"column":4},"end":{"line":565,"column":61}},{"start":{"line":567,"column":4},"end":{"line":572,"column":66}},{"start":{"line":574,"column":4},"end":{"line":574,"column":18}},{"start":{"line":574,"column":19},"end":{"line":577,"column":67}},{"start":{"line":579,"column":4},"end":{"line":589,"column":7}},{"start":{"line":591,"column":4},"end":{"line":596,"column":67}},{"start":{"line":598,"column":4},"end":{"line":601,"column":64}},{"start":{"line":603,"column":4},"end":{"line":606,"column":57}},{"start":{"line":608,"column":4},"end":{"line":610,"column":59}},{"start":{"line":612,"column":4},"end":{"line":615,"column":7}}]},"55":{"loc":{"start":{"line":519,"column":6},"end":{"line":532,"column":7}},"type":"if","locations":[{"start":{"line":519,"column":6},"end":{"line":532,"column":7}},{"start":{"line":519,"column":6},"end":{"line":532,"column":7}}]},"56":{"loc":{"start":{"line":539,"column":6},"end":{"line":546,"column":7}},"type":"if","locations":[{"start":{"line":539,"column":6},"end":{"line":546,"column":7}},{"start":{"line":539,"column":6},"end":{"line":546,"column":7}}]},"57":{"loc":{"start":{"line":539,"column":10},"end":{"line":539,"column":60}},"type":"binary-expr","locations":[{"start":{"line":539,"column":10},"end":{"line":539,"column":32}},{"start":{"line":539,"column":36},"end":{"line":539,"column":60}}]},"58":{"loc":{"start":{"line":540,"column":8},"end":{"line":545,"column":9}},"type":"if","locations":[{"start":{"line":540,"column":8},"end":{"line":545,"column":9}},{"start":{"line":540,"column":8},"end":{"line":545,"column":9}}]},"59":{"loc":{"start":{"line":542,"column":26},"end":{"line":542,"column":69}},"type":"binary-expr","locations":[{"start":{"line":542,"column":26},"end":{"line":542,"column":47}},{"start":{"line":542,"column":51},"end":{"line":542,"column":69}}]},"60":{"loc":{"start":{"line":548,"column":6},"end":{"line":552,"column":7}},"type":"if","locations":[{"start":{"line":548,"column":6},"end":{"line":552,"column":7}},{"start":{"line":548,"column":6},"end":{"line":552,"column":7}}]},"61":{"loc":{"start":{"line":580,"column":6},"end":{"line":589,"column":7}},"type":"if","locations":[{"start":{"line":580,"column":6},"end":{"line":589,"column":7}},{"start":{"line":580,"column":6},"end":{"line":589,"column":7}}]},"62":{"loc":{"start":{"line":582,"column":8},"end":{"line":582,"column":51}},"type":"if","locations":[{"start":{"line":582,"column":8},"end":{"line":582,"column":51}},{"start":{"line":582,"column":8},"end":{"line":582,"column":51}}]},"63":{"loc":{"start":{"line":613,"column":6},"end":{"line":615,"column":7}},"type":"if","locations":[{"start":{"line":613,"column":6},"end":{"line":615,"column":7}},{"start":{"line":613,"column":6},"end":{"line":615,"column":7}}]},"64":{"loc":{"start":{"line":624,"column":2},"end":{"line":630,"column":3}},"type":"if","locations":[{"start":{"line":624,"column":2},"end":{"line":630,"column":3}},{"start":{"line":624,"column":2},"end":{"line":630,"column":3}}]},"65":{"loc":{"start":{"line":635,"column":2},"end":{"line":640,"column":3}},"type":"if","locations":[{"start":{"line":635,"column":2},"end":{"line":640,"column":3}},{"start":{"line":635,"column":2},"end":{"line":640,"column":3}}]},"66":{"loc":{"start":{"line":650,"column":9},"end":{"line":650,"column":93}},"type":"cond-expr","locations":[{"start":{"line":650,"column":35},"end":{"line":650,"column":39}},{"start":{"line":650,"column":42},"end":{"line":650,"column":93}}]},"67":{"loc":{"start":{"line":660,"column":9},"end":{"line":660,"column":86}},"type":"cond-expr","locations":[{"start":{"line":660,"column":35},"end":{"line":660,"column":39}},{"start":{"line":660,"column":42},"end":{"line":660,"column":86}}]},"68":{"loc":{"start":{"line":682,"column":2},"end":{"line":685,"column":3}},"type":"if","locations":[{"start":{"line":682,"column":2},"end":{"line":685,"column":3}},{"start":{"line":682,"column":2},"end":{"line":685,"column":3}}]},"69":{"loc":{"start":{"line":682,"column":6},"end":{"line":682,"column":49}},"type":"binary-expr","locations":[{"start":{"line":682,"column":6},"end":{"line":682,"column":24}},{"start":{"line":682,"column":28},"end":{"line":682,"column":49}}]},"70":{"loc":{"start":{"line":687,"column":2},"end":{"line":690,"column":3}},"type":"if","locations":[{"start":{"line":687,"column":2},"end":{"line":690,"column":3}},{"start":{"line":687,"column":2},"end":{"line":690,"column":3}}]},"71":{"loc":{"start":{"line":687,"column":6},"end":{"line":687,"column":51}},"type":"binary-expr","locations":[{"start":{"line":687,"column":6},"end":{"line":687,"column":27}},{"start":{"line":687,"column":31},"end":{"line":687,"column":51}}]},"72":{"loc":{"start":{"line":692,"column":2},"end":{"line":695,"column":3}},"type":"if","locations":[{"start":{"line":692,"column":2},"end":{"line":695,"column":3}},{"start":{"line":692,"column":2},"end":{"line":695,"column":3}}]},"73":{"loc":{"start":{"line":715,"column":6},"end":{"line":719,"column":7}},"type":"if","locations":[{"start":{"line":715,"column":6},"end":{"line":719,"column":7}},{"start":{"line":715,"column":6},"end":{"line":719,"column":7}}]},"74":{"loc":{"start":{"line":715,"column":10},"end":{"line":715,"column":50}},"type":"binary-expr","locations":[{"start":{"line":715,"column":10},"end":{"line":715,"column":30}},{"start":{"line":715,"column":34},"end":{"line":715,"column":50}}]},"75":{"loc":{"start":{"line":729,"column":6},"end":{"line":735,"column":7}},"type":"if","locations":[{"start":{"line":729,"column":6},"end":{"line":735,"column":7}},{"start":{"line":729,"column":6},"end":{"line":735,"column":7}}]},"76":{"loc":{"start":{"line":729,"column":10},"end":{"line":729,"column":86}},"type":"binary-expr","locations":[{"start":{"line":729,"column":10},"end":{"line":729,"column":27}},{"start":{"line":729,"column":31},"end":{"line":729,"column":50}},{"start":{"line":729,"column":54},"end":{"line":729,"column":86}}]},"77":{"loc":{"start":{"line":742,"column":6},"end":{"line":754,"column":7}},"type":"if","locations":[{"start":{"line":742,"column":6},"end":{"line":754,"column":7}},{"start":{"line":742,"column":6},"end":{"line":754,"column":7}}]},"78":{"loc":{"start":{"line":743,"column":8},"end":{"line":753,"column":9}},"type":"if","locations":[{"start":{"line":743,"column":8},"end":{"line":753,"column":9}},{"start":{"line":743,"column":8},"end":{"line":753,"column":9}}]},"79":{"loc":{"start":{"line":744,"column":10},"end":{"line":746,"column":11}},"type":"if","locations":[{"start":{"line":744,"column":10},"end":{"line":746,"column":11}},{"start":{"line":744,"column":10},"end":{"line":746,"column":11}}]},"80":{"loc":{"start":{"line":744,"column":14},"end":{"line":744,"column":109}},"type":"binary-expr","locations":[{"start":{"line":744,"column":14},"end":{"line":744,"column":35}},{"start":{"line":744,"column":39},"end":{"line":744,"column":58}},{"start":{"line":744,"column":62},"end":{"line":744,"column":86}},{"start":{"line":744,"column":90},"end":{"line":744,"column":109}}]},"81":{"loc":{"start":{"line":747,"column":15},"end":{"line":753,"column":9}},"type":"if","locations":[{"start":{"line":747,"column":15},"end":{"line":753,"column":9}},{"start":{"line":747,"column":15},"end":{"line":753,"column":9}}]},"82":{"loc":{"start":{"line":748,"column":10},"end":{"line":752,"column":11}},"type":"if","locations":[{"start":{"line":748,"column":10},"end":{"line":752,"column":11}},{"start":{"line":748,"column":10},"end":{"line":752,"column":11}}]},"83":{"loc":{"start":{"line":750,"column":17},"end":{"line":752,"column":11}},"type":"if","locations":[{"start":{"line":750,"column":17},"end":{"line":752,"column":11}},{"start":{"line":750,"column":17},"end":{"line":752,"column":11}}]},"84":{"loc":{"start":{"line":763,"column":13},"end":{"line":765,"column":29}},"type":"binary-expr","locations":[{"start":{"line":763,"column":13},"end":{"line":763,"column":38}},{"start":{"line":764,"column":13},"end":{"line":764,"column":43}},{"start":{"line":765,"column":13},"end":{"line":765,"column":29}}]},"85":{"loc":{"start":{"line":773,"column":6},"end":{"line":786,"column":7}},"type":"if","locations":[{"start":{"line":773,"column":6},"end":{"line":786,"column":7}},{"start":{"line":773,"column":6},"end":{"line":786,"column":7}}]},"86":{"loc":{"start":{"line":773,"column":10},"end":{"line":773,"column":53}},"type":"binary-expr","locations":[{"start":{"line":773,"column":10},"end":{"line":773,"column":26}},{"start":{"line":773,"column":30},"end":{"line":773,"column":53}}]},"87":{"loc":{"start":{"line":778,"column":10},"end":{"line":784,"column":11}},"type":"if","locations":[{"start":{"line":778,"column":10},"end":{"line":784,"column":11}},{"start":{"line":778,"column":10},"end":{"line":784,"column":11}}]},"88":{"loc":{"start":{"line":780,"column":37},"end":{"line":780,"column":64}},"type":"binary-expr","locations":[{"start":{"line":780,"column":37},"end":{"line":780,"column":44}},{"start":{"line":780,"column":48},"end":{"line":780,"column":64}}]},"89":{"loc":{"start":{"line":795,"column":6},"end":{"line":797,"column":7}},"type":"if","locations":[{"start":{"line":795,"column":6},"end":{"line":797,"column":7}},{"start":{"line":795,"column":6},"end":{"line":797,"column":7}}]},"90":{"loc":{"start":{"line":799,"column":6},"end":{"line":805,"column":7}},"type":"if","locations":[{"start":{"line":799,"column":6},"end":{"line":805,"column":7}},{"start":{"line":799,"column":6},"end":{"line":805,"column":7}}]},"91":{"loc":{"start":{"line":814,"column":6},"end":{"line":816,"column":7}},"type":"if","locations":[{"start":{"line":814,"column":6},"end":{"line":816,"column":7}},{"start":{"line":814,"column":6},"end":{"line":816,"column":7}}]},"92":{"loc":{"start":{"line":815,"column":26},"end":{"line":815,"column":52}},"type":"binary-expr","locations":[{"start":{"line":815,"column":26},"end":{"line":815,"column":41}},{"start":{"line":815,"column":45},"end":{"line":815,"column":52}}]},"93":{"loc":{"start":{"line":823,"column":6},"end":{"line":845,"column":7}},"type":"if","locations":[{"start":{"line":823,"column":6},"end":{"line":845,"column":7}},{"start":{"line":823,"column":6},"end":{"line":845,"column":7}}]},"94":{"loc":{"start":{"line":829,"column":8},"end":{"line":837,"column":9}},"type":"if","locations":[{"start":{"line":829,"column":8},"end":{"line":837,"column":9}},{"start":{"line":829,"column":8},"end":{"line":837,"column":9}}]},"95":{"loc":{"start":{"line":838,"column":13},"end":{"line":845,"column":7}},"type":"if","locations":[{"start":{"line":838,"column":13},"end":{"line":845,"column":7}},{"start":{"line":838,"column":13},"end":{"line":845,"column":7}}]},"96":{"loc":{"start":{"line":852,"column":6},"end":{"line":854,"column":7}},"type":"if","locations":[{"start":{"line":852,"column":6},"end":{"line":854,"column":7}},{"start":{"line":852,"column":6},"end":{"line":854,"column":7}}]},"97":{"loc":{"start":{"line":862,"column":6},"end":{"line":866,"column":7}},"type":"if","locations":[{"start":{"line":862,"column":6},"end":{"line":866,"column":7}},{"start":{"line":862,"column":6},"end":{"line":866,"column":7}}]},"98":{"loc":{"start":{"line":862,"column":10},"end":{"line":862,"column":46}},"type":"binary-expr","locations":[{"start":{"line":862,"column":10},"end":{"line":862,"column":27}},{"start":{"line":862,"column":31},"end":{"line":862,"column":46}}]},"99":{"loc":{"start":{"line":873,"column":6},"end":{"line":877,"column":7}},"type":"if","locations":[{"start":{"line":873,"column":6},"end":{"line":877,"column":7}},{"start":{"line":873,"column":6},"end":{"line":877,"column":7}}]},"100":{"loc":{"start":{"line":873,"column":10},"end":{"line":873,"column":59}},"type":"binary-expr","locations":[{"start":{"line":873,"column":10},"end":{"line":873,"column":27}},{"start":{"line":873,"column":32},"end":{"line":873,"column":43}},{"start":{"line":873,"column":47},"end":{"line":873,"column":58}}]},"101":{"loc":{"start":{"line":884,"column":6},"end":{"line":884,"column":54}},"type":"if","locations":[{"start":{"line":884,"column":6},"end":{"line":884,"column":54}},{"start":{"line":884,"column":6},"end":{"line":884,"column":54}}]},"102":{"loc":{"start":{"line":890,"column":6},"end":{"line":894,"column":7}},"type":"if","locations":[{"start":{"line":890,"column":6},"end":{"line":894,"column":7}},{"start":{"line":890,"column":6},"end":{"line":894,"column":7}}]},"103":{"loc":{"start":{"line":903,"column":8},"end":{"line":905,"column":9}},"type":"if","locations":[{"start":{"line":903,"column":8},"end":{"line":905,"column":9}},{"start":{"line":903,"column":8},"end":{"line":905,"column":9}}]},"104":{"loc":{"start":{"line":903,"column":12},"end":{"line":903,"column":54}},"type":"binary-expr","locations":[{"start":{"line":903,"column":12},"end":{"line":903,"column":16}},{"start":{"line":903,"column":20},"end":{"line":903,"column":54}}]},"105":{"loc":{"start":{"line":917,"column":8},"end":{"line":919,"column":9}},"type":"if","locations":[{"start":{"line":917,"column":8},"end":{"line":919,"column":9}},{"start":{"line":917,"column":8},"end":{"line":919,"column":9}}]},"106":{"loc":{"start":{"line":917,"column":12},"end":{"line":917,"column":76}},"type":"binary-expr","locations":[{"start":{"line":917,"column":12},"end":{"line":917,"column":16}},{"start":{"line":917,"column":20},"end":{"line":917,"column":38}},{"start":{"line":917,"column":42},"end":{"line":917,"column":76}}]},"107":{"loc":{"start":{"line":932,"column":6},"end":{"line":939,"column":7}},"type":"if","locations":[{"start":{"line":932,"column":6},"end":{"line":939,"column":7}},{"start":{"line":932,"column":6},"end":{"line":939,"column":7}}]},"108":{"loc":{"start":{"line":945,"column":6},"end":{"line":947,"column":7}},"type":"if","locations":[{"start":{"line":945,"column":6},"end":{"line":947,"column":7}},{"start":{"line":945,"column":6},"end":{"line":947,"column":7}}]},"109":{"loc":{"start":{"line":954,"column":6},"end":{"line":956,"column":7}},"type":"if","locations":[{"start":{"line":954,"column":6},"end":{"line":956,"column":7}},{"start":{"line":954,"column":6},"end":{"line":956,"column":7}}]},"110":{"loc":{"start":{"line":964,"column":13},"end":{"line":964,"column":53}},"type":"binary-expr","locations":[{"start":{"line":964,"column":13},"end":{"line":964,"column":33}},{"start":{"line":964,"column":37},"end":{"line":964,"column":53}}]},"111":{"loc":{"start":{"line":971,"column":6},"end":{"line":973,"column":7}},"type":"if","locations":[{"start":{"line":971,"column":6},"end":{"line":973,"column":7}},{"start":{"line":971,"column":6},"end":{"line":973,"column":7}}]},"112":{"loc":{"start":{"line":983,"column":6},"end":{"line":985,"column":7}},"type":"if","locations":[{"start":{"line":983,"column":6},"end":{"line":985,"column":7}},{"start":{"line":983,"column":6},"end":{"line":985,"column":7}}]},"113":{"loc":{"start":{"line":983,"column":10},"end":{"line":983,"column":51}},"type":"binary-expr","locations":[{"start":{"line":983,"column":10},"end":{"line":983,"column":25}},{"start":{"line":983,"column":29},"end":{"line":983,"column":51}}]},"114":{"loc":{"start":{"line":986,"column":6},"end":{"line":999,"column":7}},"type":"if","locations":[{"start":{"line":986,"column":6},"end":{"line":999,"column":7}},{"start":{"line":986,"column":6},"end":{"line":999,"column":7}}]},"115":{"loc":{"start":{"line":992,"column":10},"end":{"line":996,"column":11}},"type":"if","locations":[{"start":{"line":992,"column":10},"end":{"line":996,"column":11}},{"start":{"line":992,"column":10},"end":{"line":996,"column":11}}]},"116":{"loc":{"start":{"line":1009,"column":6},"end":{"line":1012,"column":7}},"type":"if","locations":[{"start":{"line":1009,"column":6},"end":{"line":1012,"column":7}},{"start":{"line":1009,"column":6},"end":{"line":1012,"column":7}}]},"117":{"loc":{"start":{"line":1011,"column":8},"end":{"line":1011,"column":54}},"type":"if","locations":[{"start":{"line":1011,"column":8},"end":{"line":1011,"column":54}},{"start":{"line":1011,"column":8},"end":{"line":1011,"column":54}}]},"118":{"loc":{"start":{"line":1017,"column":6},"end":{"line":1019,"column":7}},"type":"if","locations":[{"start":{"line":1017,"column":6},"end":{"line":1019,"column":7}},{"start":{"line":1017,"column":6},"end":{"line":1019,"column":7}}]},"119":{"loc":{"start":{"line":1018,"column":9},"end":{"line":1018,"column":27}},"type":"binary-expr","locations":[{"start":{"line":1018,"column":9},"end":{"line":1018,"column":19}},{"start":{"line":1018,"column":23},"end":{"line":1018,"column":27}}]},"120":{"loc":{"start":{"line":1025,"column":6},"end":{"line":1027,"column":7}},"type":"if","locations":[{"start":{"line":1025,"column":6},"end":{"line":1027,"column":7}},{"start":{"line":1025,"column":6},"end":{"line":1027,"column":7}}]},"121":{"loc":{"start":{"line":1028,"column":6},"end":{"line":1030,"column":7}},"type":"if","locations":[{"start":{"line":1028,"column":6},"end":{"line":1030,"column":7}},{"start":{"line":1028,"column":6},"end":{"line":1030,"column":7}}]},"122":{"loc":{"start":{"line":1043,"column":6},"end":{"line":1047,"column":7}},"type":"if","locations":[{"start":{"line":1043,"column":6},"end":{"line":1047,"column":7}},{"start":{"line":1043,"column":6},"end":{"line":1047,"column":7}}]},"123":{"loc":{"start":{"line":1045,"column":13},"end":{"line":1047,"column":7}},"type":"if","locations":[{"start":{"line":1045,"column":13},"end":{"line":1047,"column":7}},{"start":{"line":1045,"column":13},"end":{"line":1047,"column":7}}]},"124":{"loc":{"start":{"line":1048,"column":6},"end":{"line":1054,"column":7}},"type":"if","locations":[{"start":{"line":1048,"column":6},"end":{"line":1054,"column":7}},{"start":{"line":1048,"column":6},"end":{"line":1054,"column":7}}]},"125":{"loc":{"start":{"line":1050,"column":8},"end":{"line":1053,"column":9}},"type":"if","locations":[{"start":{"line":1050,"column":8},"end":{"line":1053,"column":9}},{"start":{"line":1050,"column":8},"end":{"line":1053,"column":9}}]},"126":{"loc":{"start":{"line":1050,"column":12},"end":{"line":1050,"column":104}},"type":"binary-expr","locations":[{"start":{"line":1050,"column":13},"end":{"line":1050,"column":32}},{"start":{"line":1050,"column":36},"end":{"line":1050,"column":55}},{"start":{"line":1050,"column":60},"end":{"line":1050,"column":81}},{"start":{"line":1050,"column":85},"end":{"line":1050,"column":104}}]},"127":{"loc":{"start":{"line":1063,"column":6},"end":{"line":1065,"column":7}},"type":"if","locations":[{"start":{"line":1063,"column":6},"end":{"line":1065,"column":7}},{"start":{"line":1063,"column":6},"end":{"line":1065,"column":7}}]},"128":{"loc":{"start":{"line":1074,"column":6},"end":{"line":1077,"column":7}},"type":"if","locations":[{"start":{"line":1074,"column":6},"end":{"line":1077,"column":7}},{"start":{"line":1074,"column":6},"end":{"line":1077,"column":7}}]},"129":{"loc":{"start":{"line":1084,"column":6},"end":{"line":1086,"column":7}},"type":"if","locations":[{"start":{"line":1084,"column":6},"end":{"line":1086,"column":7}},{"start":{"line":1084,"column":6},"end":{"line":1086,"column":7}}]},"130":{"loc":{"start":{"line":1095,"column":13},"end":{"line":1095,"column":53}},"type":"binary-expr","locations":[{"start":{"line":1095,"column":13},"end":{"line":1095,"column":33}},{"start":{"line":1095,"column":37},"end":{"line":1095,"column":53}}]},"131":{"loc":{"start":{"line":1112,"column":6},"end":{"line":1124,"column":7}},"type":"if","locations":[{"start":{"line":1112,"column":6},"end":{"line":1124,"column":7}},{"start":{"line":1112,"column":6},"end":{"line":1124,"column":7}}]},"132":{"loc":{"start":{"line":1112,"column":10},"end":{"line":1112,"column":54}},"type":"binary-expr","locations":[{"start":{"line":1112,"column":10},"end":{"line":1112,"column":24}},{"start":{"line":1112,"column":28},"end":{"line":1112,"column":54}}]},"133":{"loc":{"start":{"line":1117,"column":10},"end":{"line":1122,"column":11}},"type":"if","locations":[{"start":{"line":1117,"column":10},"end":{"line":1122,"column":11}},{"start":{"line":1117,"column":10},"end":{"line":1122,"column":11}}]},"134":{"loc":{"start":{"line":1129,"column":6},"end":{"line":1151,"column":7}},"type":"if","locations":[{"start":{"line":1129,"column":6},"end":{"line":1151,"column":7}},{"start":{"line":1129,"column":6},"end":{"line":1151,"column":7}}]},"135":{"loc":{"start":{"line":1129,"column":10},"end":{"line":1129,"column":52}},"type":"binary-expr","locations":[{"start":{"line":1129,"column":10},"end":{"line":1129,"column":26}},{"start":{"line":1129,"column":30},"end":{"line":1129,"column":52}}]},"136":{"loc":{"start":{"line":1138,"column":16},"end":{"line":1138,"column":31}},"type":"binary-expr","locations":[{"start":{"line":1138,"column":16},"end":{"line":1138,"column":24}},{"start":{"line":1138,"column":28},"end":{"line":1138,"column":31}}]},"137":{"loc":{"start":{"line":1141,"column":8},"end":{"line":1150,"column":9}},"type":"if","locations":[{"start":{"line":1141,"column":8},"end":{"line":1150,"column":9}},{"start":{"line":1141,"column":8},"end":{"line":1150,"column":9}}]},"138":{"loc":{"start":{"line":1143,"column":15},"end":{"line":1150,"column":9}},"type":"if","locations":[{"start":{"line":1143,"column":15},"end":{"line":1150,"column":9}},{"start":{"line":1143,"column":15},"end":{"line":1150,"column":9}}]},"139":{"loc":{"start":{"line":1161,"column":6},"end":{"line":1175,"column":7}},"type":"if","locations":[{"start":{"line":1161,"column":6},"end":{"line":1175,"column":7}},{"start":{"line":1161,"column":6},"end":{"line":1175,"column":7}}]},"140":{"loc":{"start":{"line":1165,"column":10},"end":{"line":1165,"column":55}},"type":"if","locations":[{"start":{"line":1165,"column":10},"end":{"line":1165,"column":55}},{"start":{"line":1165,"column":10},"end":{"line":1165,"column":55}}]},"141":{"loc":{"start":{"line":1169,"column":10},"end":{"line":1173,"column":11}},"type":"if","locations":[{"start":{"line":1169,"column":10},"end":{"line":1173,"column":11}},{"start":{"line":1169,"column":10},"end":{"line":1173,"column":11}}]},"142":{"loc":{"start":{"line":1183,"column":6},"end":{"line":1187,"column":7}},"type":"if","locations":[{"start":{"line":1183,"column":6},"end":{"line":1187,"column":7}},{"start":{"line":1183,"column":6},"end":{"line":1187,"column":7}}]}},"s":{"1":1,"2":1,"3":287,"4":287,"5":287,"6":287,"7":17,"8":3,"9":287,"10":284,"11":284,"12":1,"13":13,"14":13,"15":13,"16":1,"17":5,"18":5,"19":5,"20":5,"21":5,"22":1,"23":4,"24":5,"25":5,"26":5,"27":5,"28":5,"29":5,"30":5,"31":5,"32":5,"33":5,"34":5,"35":1,"36":36,"37":13,"38":23,"39":5,"40":18,"41":3,"42":15,"43":10,"44":2,"45":8,"46":5,"47":2,"48":3,"49":2,"50":1,"51":1,"52":3,"53":3,"54":3,"55":3,"56":1,"57":8,"58":8,"59":1,"60":7,"61":8,"62":8,"63":8,"64":8,"65":6,"66":6,"67":5,"68":6,"69":6,"70":6,"71":1,"72":2,"73":2,"74":2,"75":1,"76":1,"77":1,"78":2,"79":2,"80":2,"81":1,"82":2,"83":2,"84":2,"85":1,"86":27,"87":27,"88":12,"89":15,"90":27,"91":27,"92":27,"93":4,"94":5,"95":27,"96":1,"97":1,"98":2,"99":27,"100":1,"101":7,"102":7,"103":7,"104":4,"105":3,"106":7,"107":1,"108":12,"109":12,"110":1,"111":19,"112":19,"113":12,"114":7,"115":17,"116":17,"117":17,"118":1,"119":91,"120":91,"121":6,"122":3,"123":3,"124":3,"125":6,"126":91,"127":89,"128":89,"129":89,"130":89,"131":27,"132":27,"133":89,"134":1,"135":76,"136":76,"137":76,"138":76,"139":76,"140":76,"141":0,"142":76,"143":91,"144":89,"145":19,"146":70,"147":70,"148":70,"149":1,"150":28,"151":28,"152":28,"153":28,"154":28,"155":28,"156":31,"157":31,"158":3,"159":28,"160":28,"161":28,"162":1,"163":45,"164":1,"165":6,"166":6,"167":6,"168":6,"169":6,"170":6,"171":6,"172":6,"173":1,"174":13,"175":13,"176":13,"177":13,"178":2,"179":13,"180":13,"181":6,"182":6,"183":2,"184":13,"185":1,"186":13,"187":13,"188":13,"189":1,"190":6,"191":6,"192":6,"193":6,"194":6,"195":6,"196":6,"197":1,"198":7,"199":7,"200":7,"201":7,"202":7,"203":1,"204":56,"205":56,"206":56,"207":56,"208":56,"209":56,"210":52,"211":52,"212":52,"213":52,"214":52,"215":4,"216":4,"217":52,"218":6,"219":46,"220":7,"221":39,"222":0,"223":39,"224":39,"225":6,"226":33,"227":2,"228":33,"229":33,"230":33,"231":33,"232":33,"233":33,"234":56,"235":56,"236":1,"237":52,"238":0,"239":1,"240":84,"241":84,"242":84,"243":84,"244":7,"245":7,"246":7,"247":7,"248":84,"249":1,"250":77,"251":77,"252":77,"253":77,"254":21,"255":77,"256":1,"257":5,"258":5,"259":5,"260":5,"261":1,"262":4,"263":4,"264":4,"265":4,"266":4,"267":4,"268":2,"269":2,"270":4,"271":4,"272":1,"273":23,"274":23,"275":23,"276":23,"277":1,"278":23,"279":23,"280":23,"281":1,"282":26,"283":26,"284":13,"285":13,"286":4,"287":26,"288":3,"289":26,"290":1,"291":299,"292":2,"293":11,"294":7,"295":1,"296":113,"297":88,"298":77,"299":1,"300":396,"301":396,"302":396,"303":396,"304":396,"305":299,"306":29,"307":4,"308":4,"309":3,"310":2,"311":2,"312":2,"313":2,"314":2,"315":2,"316":2,"317":2,"318":1,"319":28,"320":28,"321":17,"322":10,"323":10,"324":7,"325":28,"326":9,"327":9,"328":9,"329":19,"330":19,"331":19,"332":19,"333":19,"334":19,"335":19,"336":19,"337":4,"338":4,"339":4,"340":4,"341":4,"342":2,"343":2,"344":2,"345":4,"346":4,"347":4,"348":0,"349":4,"350":4,"351":4,"352":4,"353":4,"354":8,"355":8,"356":8,"357":8,"358":8,"359":2,"360":2,"361":2,"362":1,"363":1,"364":1,"365":5,"366":5,"367":6,"368":5,"369":2,"370":1,"371":391,"372":391,"373":388,"374":10,"375":10,"376":10,"377":378,"378":1,"379":414,"380":414,"381":23,"382":23,"383":391,"384":1,"385":389,"386":389,"387":386,"388":386,"389":2,"390":386,"391":1,"392":379,"393":379,"394":376,"395":376,"396":10,"397":376,"398":1,"399":379,"400":379,"401":379,"402":376,"403":376,"404":1,"405":184,"406":184,"407":181,"408":1,"409":94,"410":92,"411":92,"412":0,"413":0,"414":92,"415":18,"416":18,"417":92,"418":0,"419":0,"420":92,"421":1,"422":16,"423":16,"424":253,"425":253,"426":84,"427":17,"428":84,"429":253,"430":253,"431":300,"432":10,"433":10,"434":10,"435":290,"436":253,"437":253,"438":115,"439":46,"440":31,"441":31,"442":15,"443":15,"444":0,"445":15,"446":15,"447":69,"448":253,"449":253,"450":6,"451":253,"452":253,"453":316,"454":5,"455":5,"456":5,"457":5,"458":5,"459":5,"460":5,"461":0,"462":311,"463":253,"464":253,"465":74,"466":74,"467":6,"468":74,"469":23,"470":23,"471":23,"472":23,"473":51,"474":253,"475":253,"476":7,"477":7,"478":6,"479":7,"480":253,"481":253,"482":6,"483":4,"484":4,"485":4,"486":4,"487":2,"488":2,"489":2,"490":2,"491":2,"492":2,"493":2,"494":2,"495":2,"496":0,"497":253,"498":253,"499":33,"500":33,"501":16,"502":253,"503":253,"504":1260,"505":11,"506":1249,"507":253,"508":253,"509":3518,"510":127,"511":3391,"512":253,"513":253,"514":0,"515":0,"516":253,"517":253,"518":43,"519":4,"520":39,"521":253,"522":253,"523":34,"524":32,"525":32,"526":12,"527":34,"528":253,"529":253,"530":14,"531":16,"532":16,"533":0,"534":14,"535":253,"536":253,"537":20,"538":20,"539":20,"540":0,"541":0,"542":0,"543":0,"544":20,"545":253,"546":253,"547":227,"548":223,"549":253,"550":253,"551":4,"552":4,"553":4,"554":253,"555":253,"556":14,"557":253,"558":253,"559":11,"560":4,"561":11,"562":11,"563":253,"564":253,"565":33,"566":33,"567":2,"568":33,"569":5,"570":5,"571":5,"572":6,"573":6,"574":6,"575":1,"576":5,"577":6,"578":253,"579":253,"580":17,"581":4,"582":4,"583":0,"584":17,"585":17,"586":4,"587":253,"588":253,"589":36,"590":1,"591":36,"592":30,"593":36,"594":36,"595":253,"596":253,"597":8,"598":8,"599":8,"600":3,"601":5,"602":5,"603":8,"604":8,"605":8,"606":6,"607":6,"608":8,"609":253,"610":253,"611":32,"612":6,"613":32,"614":253,"615":253,"616":91,"617":91,"618":68,"619":67,"620":253,"621":253,"622":7,"623":5,"624":7,"625":253,"626":253,"627":7,"628":253,"629":253,"630":330,"631":330,"632":50,"633":50,"634":50,"635":11,"636":11,"637":11,"638":0,"639":291,"640":291,"641":16,"642":16,"643":13,"644":11,"645":5,"646":11,"647":8,"648":3,"649":2,"650":1,"651":275,"652":275,"653":253,"654":253,"655":44,"656":18,"657":18,"658":18,"659":16,"660":3,"661":13,"662":5,"663":5,"664":0,"665":44,"666":253,"667":253,"668":11,"669":4,"670":7},"f":{"1":287,"2":13,"3":5,"4":36,"5":3,"6":8,"7":2,"8":2,"9":2,"10":27,"11":7,"12":12,"13":19,"14":91,"15":76,"16":28,"17":45,"18":6,"19":13,"20":6,"21":7,"22":56,"23":52,"24":84,"25":77,"26":5,"27":4,"28":23,"29":26,"30":299,"31":396,"32":391,"33":414,"34":389,"35":379,"36":379,"37":184,"38":94,"39":16,"40":253,"41":253,"42":84,"43":253,"44":300,"45":253,"46":115,"47":253,"48":6,"49":253,"50":316,"51":253,"52":74,"53":253,"54":7,"55":253,"56":6,"57":253,"58":33,"59":253,"60":1260,"61":253,"62":3518,"63":253,"64":0,"65":253,"66":43,"67":253,"68":34,"69":253,"70":14,"71":253,"72":20,"73":253,"74":227,"75":253,"76":4,"77":253,"78":14,"79":253,"80":11,"81":253,"82":33,"83":253,"84":17,"85":253,"86":36,"87":253,"88":8,"89":253,"90":32,"91":253,"92":91,"93":253,"94":7,"95":253,"96":7,"97":253,"98":330,"99":253,"100":44,"101":253,"102":11},"b":{"1":[287,270],"2":[17,270],"3":[3,14],"4":[17,16],"5":[1,4],"6":[13,23],"7":[5,18],"8":[3,15],"9":[10,5],"10":[2,8],"11":[2,3],"12":[2,1],"13":[1,7],"14":[12,15],"15":[4,23],"16":[1,26],"17":[4,3],"18":[12,7],"19":[6,85],"20":[3,3],"21":[3,0],"22":[27,62],"23":[76,0],"24":[76,11],"25":[19,70],"26":[3,28],"27":[0,45],"28":[45,45],"29":[2,11],"30":[2,4],"31":[1,12],"32":[4,48],"33":[52,7],"34":[6,46],"35":[7,39],"36":[46,40],"37":[0,39],"38":[39,2],"39":[6,33],"40":[39,38],"41":[2,31],"42":[0,52],"43":[52,26,26],"44":[84,7],"45":[84,7],"46":[84,7],"47":[21,56],"48":[6,6],"49":[2,2],"50":[1,22],"51":[4,9],"52":[3,23],"53":[2,11,7,7,1,113,88,77],"54":[299,29,4,4,28,4,1,2,4,8,2,1,5,6],"55":[3,1],"56":[17,11],"57":[28,18],"58":[10,7],"59":[10,9],"60":[9,19],"61":[4,0],"62":[0,4],"63":[5,1],"64":[10,378],"65":[23,391],"66":[384,2],"67":[367,9],"68":[0,92],"69":[92,0],"70":[18,74],"71":[92,92],"72":[0,92],"73":[17,67],"74":[84,17],"75":[10,290],"76":[300,256,70],"77":[46,69],"78":[31,15],"79":[31,0],"80":[31,19,6,2],"81":[15,0],"82":[0,15],"83":[15,0],"84":[6,2,0],"85":[5,311],"86":[316,49],"87":[5,0],"88":[5,0],"89":[6,68],"90":[23,51],"91":[6,1],"92":[6,0],"93":[4,2],"94":[2,2],"95":[2,0],"96":[16,17],"97":[11,1249],"98":[1260,469],"99":[127,3391],"100":[3518,1245,1145],"101":[0,0],"102":[4,39],"103":[12,20],"104":[32,32],"105":[0,16],"106":[16,16,0],"107":[0,20],"108":[223,4],"109":[4,0],"110":[14,10],"111":[4,7],"112":[2,31],"113":[33,6],"114":[5,28],"115":[1,5],"116":[4,13],"117":[0,4],"118":[4,13],"119":[4,4],"120":[1,35],"121":[30,6],"122":[3,5],"123":[5,0],"124":[8,0],"125":[6,2],"126":[8,3,6,4],"127":[6,26],"128":[68,23],"129":[5,2],"130":[7,2],"131":[50,280],"132":[330,330],"133":[11,0],"134":[16,275],"135":[291,280],"136":[5,0],"137":[8,3],"138":[2,1],"139":[18,26],"140":[3,13],"141":[5,0],"142":[4,7]},"hash":"d9670651c5cd02b04f94e68d2e9e80384286a740"} +,"/home/travis/build/babel/babylon/src/plugins/jsx/index.js": {"path":"/home/travis/build/babel/babylon/src/plugins/jsx/index.js","statementMap":{"1":{"start":{"line":10,"column":19},"end":{"line":10,"column":34}},"2":{"start":{"line":11,"column":23},"end":{"line":11,"column":30}},"3":{"start":{"line":13,"column":0},"end":{"line":13,"column":42}},"4":{"start":{"line":14,"column":0},"end":{"line":14,"column":43}},"5":{"start":{"line":15,"column":0},"end":{"line":15,"column":57}},"6":{"start":{"line":17,"column":0},"end":{"line":17,"column":38}},"7":{"start":{"line":18,"column":0},"end":{"line":18,"column":60}},"8":{"start":{"line":19,"column":0},"end":{"line":19,"column":68}},"9":{"start":{"line":20,"column":0},"end":{"line":20,"column":42}},"10":{"start":{"line":22,"column":0},"end":{"line":26,"column":2}},"11":{"start":{"line":23,"column":2},"end":{"line":23,"column":37}},"12":{"start":{"line":24,"column":2},"end":{"line":24,"column":37}},"13":{"start":{"line":25,"column":2},"end":{"line":25,"column":33}},"14":{"start":{"line":28,"column":0},"end":{"line":36,"column":2}},"15":{"start":{"line":29,"column":12},"end":{"line":29,"column":36}},"16":{"start":{"line":30,"column":2},"end":{"line":35,"column":3}},"17":{"start":{"line":31,"column":4},"end":{"line":31,"column":29}},"18":{"start":{"line":32,"column":4},"end":{"line":32,"column":61}},"19":{"start":{"line":34,"column":4},"end":{"line":34,"column":34}},"20":{"start":{"line":38,"column":9},"end":{"line":38,"column":25}},"21":{"start":{"line":42,"column":0},"end":{"line":81,"column":2}},"22":{"start":{"line":43,"column":12},"end":{"line":43,"column":14}},"23":{"start":{"line":44,"column":19},"end":{"line":44,"column":33}},"24":{"start":{"line":45,"column":2},"end":{"line":80,"column":3}},"25":{"start":{"line":46,"column":4},"end":{"line":48,"column":5}},"26":{"start":{"line":47,"column":6},"end":{"line":47,"column":64}},"27":{"start":{"line":50,"column":13},"end":{"line":50,"column":50}},"28":{"start":{"line":52,"column":4},"end":{"line":79,"column":5}},"29":{"start":{"line":55,"column":8},"end":{"line":61,"column":9}},"30":{"start":{"line":56,"column":10},"end":{"line":59,"column":11}},"31":{"start":{"line":57,"column":12},"end":{"line":57,"column":29}},"32":{"start":{"line":58,"column":12},"end":{"line":58,"column":52}},"33":{"start":{"line":60,"column":10},"end":{"line":60,"column":43}},"34":{"start":{"line":62,"column":8},"end":{"line":62,"column":60}},"35":{"start":{"line":63,"column":8},"end":{"line":63,"column":49}},"36":{"start":{"line":66,"column":8},"end":{"line":66,"column":60}},"37":{"start":{"line":67,"column":8},"end":{"line":67,"column":36}},"38":{"start":{"line":68,"column":8},"end":{"line":68,"column":36}},"39":{"start":{"line":69,"column":8},"end":{"line":69,"column":14}},"40":{"start":{"line":72,"column":8},"end":{"line":78,"column":9}},"41":{"start":{"line":73,"column":10},"end":{"line":73,"column":62}},"42":{"start":{"line":74,"column":10},"end":{"line":74,"column":43}},"43":{"start":{"line":75,"column":10},"end":{"line":75,"column":38}},"44":{"start":{"line":77,"column":10},"end":{"line":77,"column":27}},"45":{"start":{"line":83,"column":0},"end":{"line":97,"column":2}},"46":{"start":{"line":84,"column":11},"end":{"line":84,"column":48}},"47":{"start":{"line":86,"column":2},"end":{"line":86,"column":19}},"48":{"start":{"line":87,"column":2},"end":{"line":92,"column":3}},"49":{"start":{"line":88,"column":4},"end":{"line":88,"column":21}},"50":{"start":{"line":89,"column":4},"end":{"line":89,"column":40}},"51":{"start":{"line":91,"column":4},"end":{"line":91,"column":34}},"52":{"start":{"line":93,"column":2},"end":{"line":93,"column":23}},"53":{"start":{"line":94,"column":2},"end":{"line":94,"column":40}},"54":{"start":{"line":96,"column":2},"end":{"line":96,"column":13}},"55":{"start":{"line":99,"column":0},"end":{"line":123,"column":2}},"56":{"start":{"line":100,"column":12},"end":{"line":100,"column":14}},"57":{"start":{"line":101,"column":19},"end":{"line":101,"column":35}},"58":{"start":{"line":102,"column":2},"end":{"line":120,"column":3}},"59":{"start":{"line":103,"column":4},"end":{"line":105,"column":5}},"60":{"start":{"line":104,"column":6},"end":{"line":104,"column":67}},"61":{"start":{"line":107,"column":13},"end":{"line":107,"column":50}},"62":{"start":{"line":108,"column":4},"end":{"line":108,"column":28}},"63":{"start":{"line":108,"column":22},"end":{"line":108,"column":28}},"64":{"start":{"line":109,"column":4},"end":{"line":119,"column":5}},"65":{"start":{"line":110,"column":6},"end":{"line":110,"column":58}},"66":{"start":{"line":111,"column":6},"end":{"line":111,"column":34}},"67":{"start":{"line":112,"column":6},"end":{"line":112,"column":34}},"68":{"start":{"line":113,"column":11},"end":{"line":119,"column":5}},"69":{"start":{"line":114,"column":6},"end":{"line":114,"column":58}},"70":{"start":{"line":115,"column":6},"end":{"line":115,"column":40}},"71":{"start":{"line":116,"column":6},"end":{"line":116,"column":34}},"72":{"start":{"line":118,"column":6},"end":{"line":118,"column":23}},"73":{"start":{"line":121,"column":2},"end":{"line":121,"column":56}},"74":{"start":{"line":122,"column":2},"end":{"line":122,"column":42}},"75":{"start":{"line":125,"column":0},"end":{"line":157,"column":2}},"76":{"start":{"line":126,"column":12},"end":{"line":126,"column":14}},"77":{"start":{"line":127,"column":14},"end":{"line":127,"column":15}},"78":{"start":{"line":129,"column":11},"end":{"line":129,"column":37}},"79":{"start":{"line":131,"column":17},"end":{"line":131,"column":33}},"80":{"start":{"line":132,"column":2},"end":{"line":151,"column":3}},"81":{"start":{"line":133,"column":4},"end":{"line":133,"column":38}},"82":{"start":{"line":134,"column":4},"end":{"line":149,"column":5}},"83":{"start":{"line":135,"column":6},"end":{"line":147,"column":7}},"84":{"start":{"line":136,"column":8},"end":{"line":144,"column":9}},"85":{"start":{"line":137,"column":10},"end":{"line":137,"column":30}},"86":{"start":{"line":138,"column":10},"end":{"line":139,"column":60}},"87":{"start":{"line":139,"column":12},"end":{"line":139,"column":60}},"88":{"start":{"line":141,"column":10},"end":{"line":141,"column":30}},"89":{"start":{"line":142,"column":10},"end":{"line":143,"column":60}},"90":{"start":{"line":143,"column":12},"end":{"line":143,"column":60}},"91":{"start":{"line":146,"column":8},"end":{"line":146,"column":36}},"92":{"start":{"line":148,"column":6},"end":{"line":148,"column":12}},"93":{"start":{"line":150,"column":4},"end":{"line":150,"column":14}},"94":{"start":{"line":152,"column":2},"end":{"line":155,"column":3}},"95":{"start":{"line":153,"column":4},"end":{"line":153,"column":30}},"96":{"start":{"line":154,"column":4},"end":{"line":154,"column":15}},"97":{"start":{"line":156,"column":2},"end":{"line":156,"column":16}},"98":{"start":{"line":167,"column":0},"end":{"line":174,"column":2}},"99":{"start":{"line":169,"column":14},"end":{"line":169,"column":28}},"100":{"start":{"line":170,"column":2},"end":{"line":172,"column":46}},"101":{"start":{"line":171,"column":4},"end":{"line":171,"column":49}},"102":{"start":{"line":173,"column":2},"end":{"line":173,"column":79}},"103":{"start":{"line":179,"column":2},"end":{"line":181,"column":3}},"104":{"start":{"line":180,"column":4},"end":{"line":180,"column":23}},"105":{"start":{"line":183,"column":2},"end":{"line":185,"column":3}},"106":{"start":{"line":184,"column":4},"end":{"line":184,"column":58}},"107":{"start":{"line":187,"column":2},"end":{"line":189,"column":3}},"108":{"start":{"line":188,"column":4},"end":{"line":188,"column":91}},"109":{"start":{"line":194,"column":0},"end":{"line":205,"column":2}},"110":{"start":{"line":195,"column":13},"end":{"line":195,"column":29}},"111":{"start":{"line":196,"column":2},"end":{"line":202,"column":3}},"112":{"start":{"line":197,"column":4},"end":{"line":197,"column":33}},"113":{"start":{"line":198,"column":9},"end":{"line":202,"column":3}},"114":{"start":{"line":199,"column":4},"end":{"line":199,"column":40}},"115":{"start":{"line":201,"column":4},"end":{"line":201,"column":22}},"116":{"start":{"line":203,"column":2},"end":{"line":203,"column":14}},"117":{"start":{"line":204,"column":2},"end":{"line":204,"column":48}},"118":{"start":{"line":209,"column":0},"end":{"line":218,"column":2}},"119":{"start":{"line":210,"column":17},"end":{"line":210,"column":33}},"120":{"start":{"line":210,"column":46},"end":{"line":210,"column":65}},"121":{"start":{"line":211,"column":13},"end":{"line":211,"column":38}},"122":{"start":{"line":212,"column":2},"end":{"line":212,"column":39}},"123":{"start":{"line":212,"column":27},"end":{"line":212,"column":39}},"124":{"start":{"line":214,"column":13},"end":{"line":214,"column":49}},"125":{"start":{"line":215,"column":2},"end":{"line":215,"column":24}},"126":{"start":{"line":216,"column":2},"end":{"line":216,"column":40}},"127":{"start":{"line":217,"column":2},"end":{"line":217,"column":52}},"128":{"start":{"line":223,"column":0},"end":{"line":233,"column":2}},"129":{"start":{"line":224,"column":17},"end":{"line":224,"column":33}},"130":{"start":{"line":224,"column":46},"end":{"line":224,"column":65}},"131":{"start":{"line":225,"column":13},"end":{"line":225,"column":42}},"132":{"start":{"line":226,"column":2},"end":{"line":231,"column":3}},"133":{"start":{"line":227,"column":18},"end":{"line":227,"column":54}},"134":{"start":{"line":228,"column":4},"end":{"line":228,"column":26}},"135":{"start":{"line":229,"column":4},"end":{"line":229,"column":49}},"136":{"start":{"line":230,"column":4},"end":{"line":230,"column":59}},"137":{"start":{"line":232,"column":2},"end":{"line":232,"column":14}},"138":{"start":{"line":237,"column":0},"end":{"line":257,"column":2}},"139":{"start":{"line":239,"column":2},"end":{"line":256,"column":3}},"140":{"start":{"line":241,"column":6},"end":{"line":241,"column":48}},"141":{"start":{"line":242,"column":6},"end":{"line":246,"column":7}},"142":{"start":{"line":243,"column":8},"end":{"line":243,"column":94}},"143":{"start":{"line":245,"column":8},"end":{"line":245,"column":20}},"144":{"start":{"line":250,"column":6},"end":{"line":250,"column":34}},"145":{"start":{"line":251,"column":6},"end":{"line":251,"column":24}},"146":{"start":{"line":252,"column":6},"end":{"line":252,"column":18}},"147":{"start":{"line":255,"column":6},"end":{"line":255,"column":100}},"148":{"start":{"line":263,"column":0},"end":{"line":266,"column":2}},"149":{"start":{"line":264,"column":13},"end":{"line":264,"column":66}},"150":{"start":{"line":265,"column":2},"end":{"line":265,"column":82}},"151":{"start":{"line":270,"column":0},"end":{"line":278,"column":2}},"152":{"start":{"line":271,"column":13},"end":{"line":271,"column":29}},"153":{"start":{"line":272,"column":2},"end":{"line":272,"column":25}},"154":{"start":{"line":273,"column":2},"end":{"line":273,"column":27}},"155":{"start":{"line":274,"column":2},"end":{"line":274,"column":43}},"156":{"start":{"line":275,"column":2},"end":{"line":275,"column":25}},"157":{"start":{"line":277,"column":2},"end":{"line":277,"column":49}},"158":{"start":{"line":283,"column":0},"end":{"line":293,"column":2}},"159":{"start":{"line":284,"column":13},"end":{"line":284,"column":29}},"160":{"start":{"line":285,"column":2},"end":{"line":285,"column":14}},"161":{"start":{"line":286,"column":2},"end":{"line":290,"column":3}},"162":{"start":{"line":287,"column":4},"end":{"line":287,"column":53}},"163":{"start":{"line":289,"column":4},"end":{"line":289,"column":45}},"164":{"start":{"line":291,"column":2},"end":{"line":291,"column":25}},"165":{"start":{"line":292,"column":2},"end":{"line":292,"column":57}},"166":{"start":{"line":297,"column":0},"end":{"line":308,"column":2}},"167":{"start":{"line":298,"column":13},"end":{"line":298,"column":29}},"168":{"start":{"line":299,"column":2},"end":{"line":304,"column":3}},"169":{"start":{"line":300,"column":4},"end":{"line":300,"column":29}},"170":{"start":{"line":301,"column":4},"end":{"line":301,"column":44}},"171":{"start":{"line":302,"column":4},"end":{"line":302,"column":27}},"172":{"start":{"line":303,"column":4},"end":{"line":303,"column":55}},"173":{"start":{"line":305,"column":2},"end":{"line":305,"column":44}},"174":{"start":{"line":306,"column":2},"end":{"line":306,"column":70}},"175":{"start":{"line":307,"column":2},"end":{"line":307,"column":47}},"176":{"start":{"line":312,"column":0},"end":{"line":322,"column":2}},"177":{"start":{"line":313,"column":13},"end":{"line":313,"column":49}},"178":{"start":{"line":314,"column":2},"end":{"line":314,"column":23}},"179":{"start":{"line":315,"column":2},"end":{"line":315,"column":41}},"180":{"start":{"line":316,"column":2},"end":{"line":318,"column":3}},"181":{"start":{"line":317,"column":4},"end":{"line":317,"column":51}},"182":{"start":{"line":319,"column":2},"end":{"line":319,"column":40}},"183":{"start":{"line":320,"column":2},"end":{"line":320,"column":28}},"184":{"start":{"line":321,"column":2},"end":{"line":321,"column":52}},"185":{"start":{"line":326,"column":0},"end":{"line":331,"column":2}},"186":{"start":{"line":327,"column":13},"end":{"line":327,"column":49}},"187":{"start":{"line":328,"column":2},"end":{"line":328,"column":41}},"188":{"start":{"line":329,"column":2},"end":{"line":329,"column":28}},"189":{"start":{"line":330,"column":2},"end":{"line":330,"column":52}},"190":{"start":{"line":336,"column":0},"end":{"line":388,"column":2}},"191":{"start":{"line":337,"column":13},"end":{"line":337,"column":49}},"192":{"start":{"line":338,"column":17},"end":{"line":338,"column":19}},"193":{"start":{"line":339,"column":23},"end":{"line":339,"column":72}},"194":{"start":{"line":340,"column":23},"end":{"line":340,"column":27}},"195":{"start":{"line":342,"column":2},"end":{"line":379,"column":3}},"196":{"start":{"line":343,"column":4},"end":{"line":371,"column":5}},"197":{"start":{"line":343,"column":14},"end":{"line":371,"column":5}},"198":{"start":{"line":344,"column":6},"end":{"line":370,"column":7}},"199":{"start":{"line":346,"column":10},"end":{"line":346,"column":38}},"200":{"start":{"line":346,"column":39},"end":{"line":346,"column":70}},"201":{"start":{"line":347,"column":10},"end":{"line":347,"column":22}},"202":{"start":{"line":348,"column":10},"end":{"line":351,"column":11}},"203":{"start":{"line":349,"column":12},"end":{"line":349,"column":79}},"204":{"start":{"line":350,"column":12},"end":{"line":350,"column":27}},"205":{"start":{"line":352,"column":10},"end":{"line":352,"column":68}},"206":{"start":{"line":353,"column":10},"end":{"line":353,"column":16}},"207":{"start":{"line":356,"column":10},"end":{"line":356,"column":46}},"208":{"start":{"line":357,"column":10},"end":{"line":357,"column":16}},"209":{"start":{"line":360,"column":10},"end":{"line":364,"column":11}},"210":{"start":{"line":361,"column":12},"end":{"line":361,"column":54}},"211":{"start":{"line":363,"column":12},"end":{"line":363,"column":62}},"212":{"start":{"line":366,"column":10},"end":{"line":366,"column":16}},"213":{"start":{"line":369,"column":10},"end":{"line":369,"column":28}},"214":{"start":{"line":373,"column":4},"end":{"line":378,"column":5}},"215":{"start":{"line":374,"column":6},"end":{"line":377,"column":8}},"216":{"start":{"line":381,"column":2},"end":{"line":381,"column":39}},"217":{"start":{"line":382,"column":2},"end":{"line":382,"column":39}},"218":{"start":{"line":383,"column":2},"end":{"line":383,"column":27}},"219":{"start":{"line":384,"column":2},"end":{"line":386,"column":3}},"220":{"start":{"line":385,"column":4},"end":{"line":385,"column":94}},"221":{"start":{"line":387,"column":2},"end":{"line":387,"column":45}},"222":{"start":{"line":392,"column":0},"end":{"line":396,"column":2}},"223":{"start":{"line":393,"column":17},"end":{"line":393,"column":33}},"224":{"start":{"line":393,"column":46},"end":{"line":393,"column":65}},"225":{"start":{"line":394,"column":2},"end":{"line":394,"column":14}},"226":{"start":{"line":395,"column":2},"end":{"line":395,"column":52}},"227":{"start":{"line":399,"column":2},"end":{"line":412,"column":5}},"228":{"start":{"line":400,"column":4},"end":{"line":411,"column":6}},"229":{"start":{"line":401,"column":6},"end":{"line":410,"column":7}},"230":{"start":{"line":402,"column":19},"end":{"line":402,"column":65}},"231":{"start":{"line":404,"column":8},"end":{"line":404,"column":26}},"232":{"start":{"line":405,"column":8},"end":{"line":405,"column":20}},"233":{"start":{"line":406,"column":13},"end":{"line":410,"column":7}},"234":{"start":{"line":407,"column":8},"end":{"line":407,"column":38}},"235":{"start":{"line":409,"column":8},"end":{"line":409,"column":56}},"236":{"start":{"line":414,"column":2},"end":{"line":444,"column":5}},"237":{"start":{"line":415,"column":4},"end":{"line":443,"column":6}},"238":{"start":{"line":416,"column":20},"end":{"line":416,"column":37}},"239":{"start":{"line":418,"column":6},"end":{"line":420,"column":7}},"240":{"start":{"line":419,"column":8},"end":{"line":419,"column":35}},"241":{"start":{"line":422,"column":6},"end":{"line":435,"column":7}},"242":{"start":{"line":423,"column":8},"end":{"line":425,"column":9}},"243":{"start":{"line":424,"column":10},"end":{"line":424,"column":36}},"244":{"start":{"line":427,"column":8},"end":{"line":430,"column":9}},"245":{"start":{"line":428,"column":10},"end":{"line":428,"column":27}},"246":{"start":{"line":429,"column":10},"end":{"line":429,"column":48}},"247":{"start":{"line":432,"column":8},"end":{"line":434,"column":9}},"248":{"start":{"line":433,"column":10},"end":{"line":433,"column":42}},"249":{"start":{"line":437,"column":6},"end":{"line":440,"column":7}},"250":{"start":{"line":438,"column":8},"end":{"line":438,"column":25}},"251":{"start":{"line":439,"column":8},"end":{"line":439,"column":48}},"252":{"start":{"line":442,"column":6},"end":{"line":442,"column":36}},"253":{"start":{"line":446,"column":2},"end":{"line":466,"column":5}},"254":{"start":{"line":447,"column":4},"end":{"line":465,"column":6}},"255":{"start":{"line":448,"column":6},"end":{"line":464,"column":7}},"256":{"start":{"line":449,"column":25},"end":{"line":449,"column":42}},"257":{"start":{"line":450,"column":8},"end":{"line":456,"column":9}},"258":{"start":{"line":451,"column":10},"end":{"line":451,"column":54}},"259":{"start":{"line":452,"column":15},"end":{"line":456,"column":9}},"260":{"start":{"line":453,"column":10},"end":{"line":453,"column":52}},"261":{"start":{"line":455,"column":10},"end":{"line":455,"column":37}},"262":{"start":{"line":457,"column":8},"end":{"line":457,"column":38}},"263":{"start":{"line":458,"column":13},"end":{"line":464,"column":7}},"264":{"start":{"line":459,"column":8},"end":{"line":459,"column":39}},"265":{"start":{"line":460,"column":8},"end":{"line":460,"column":43}},"266":{"start":{"line":461,"column":8},"end":{"line":461,"column":39}},"267":{"start":{"line":463,"column":8},"end":{"line":463,"column":42}}},"fnMap":{"1":{"name":"(anonymous_1)","decl":{"start":{"line":22,"column":31},"end":{"line":22,"column":32}},"loc":{"start":{"line":22,"column":42},"end":{"line":26,"column":1}}},"2":{"name":"(anonymous_2)","decl":{"start":{"line":28,"column":29},"end":{"line":28,"column":30}},"loc":{"start":{"line":28,"column":48},"end":{"line":36,"column":1}}},"3":{"name":"(anonymous_3)","decl":{"start":{"line":42,"column":18},"end":{"line":42,"column":19}},"loc":{"start":{"line":42,"column":29},"end":{"line":81,"column":1}}},"4":{"name":"(anonymous_4)","decl":{"start":{"line":83,"column":20},"end":{"line":83,"column":21}},"loc":{"start":{"line":83,"column":44},"end":{"line":97,"column":1}}},"5":{"name":"(anonymous_5)","decl":{"start":{"line":99,"column":19},"end":{"line":99,"column":20}},"loc":{"start":{"line":99,"column":35},"end":{"line":123,"column":1}}},"6":{"name":"(anonymous_6)","decl":{"start":{"line":125,"column":19},"end":{"line":125,"column":20}},"loc":{"start":{"line":125,"column":30},"end":{"line":157,"column":1}}},"7":{"name":"(anonymous_7)","decl":{"start":{"line":167,"column":17},"end":{"line":167,"column":18}},"loc":{"start":{"line":167,"column":28},"end":{"line":174,"column":1}}},"8":{"name":"getQualifiedJSXName","decl":{"start":{"line":178,"column":9},"end":{"line":178,"column":28}},"loc":{"start":{"line":178,"column":37},"end":{"line":190,"column":1}}},"9":{"name":"(anonymous_9)","decl":{"start":{"line":194,"column":24},"end":{"line":194,"column":25}},"loc":{"start":{"line":194,"column":35},"end":{"line":205,"column":1}}},"10":{"name":"(anonymous_10)","decl":{"start":{"line":209,"column":28},"end":{"line":209,"column":29}},"loc":{"start":{"line":209,"column":39},"end":{"line":218,"column":1}}},"11":{"name":"(anonymous_11)","decl":{"start":{"line":223,"column":25},"end":{"line":223,"column":26}},"loc":{"start":{"line":223,"column":36},"end":{"line":233,"column":1}}},"12":{"name":"(anonymous_12)","decl":{"start":{"line":237,"column":28},"end":{"line":237,"column":29}},"loc":{"start":{"line":237,"column":39},"end":{"line":257,"column":1}}},"13":{"name":"(anonymous_13)","decl":{"start":{"line":263,"column":29},"end":{"line":263,"column":30}},"loc":{"start":{"line":263,"column":40},"end":{"line":266,"column":1}}},"14":{"name":"(anonymous_14)","decl":{"start":{"line":270,"column":25},"end":{"line":270,"column":26}},"loc":{"start":{"line":270,"column":36},"end":{"line":278,"column":1}}},"15":{"name":"(anonymous_15)","decl":{"start":{"line":283,"column":33},"end":{"line":283,"column":34}},"loc":{"start":{"line":283,"column":44},"end":{"line":293,"column":1}}},"16":{"name":"(anonymous_16)","decl":{"start":{"line":297,"column":23},"end":{"line":297,"column":24}},"loc":{"start":{"line":297,"column":34},"end":{"line":308,"column":1}}},"17":{"name":"(anonymous_17)","decl":{"start":{"line":312,"column":30},"end":{"line":312,"column":31}},"loc":{"start":{"line":312,"column":59},"end":{"line":322,"column":1}}},"18":{"name":"(anonymous_18)","decl":{"start":{"line":326,"column":30},"end":{"line":326,"column":31}},"loc":{"start":{"line":326,"column":59},"end":{"line":331,"column":1}}},"19":{"name":"(anonymous_19)","decl":{"start":{"line":336,"column":23},"end":{"line":336,"column":24}},"loc":{"start":{"line":336,"column":52},"end":{"line":388,"column":1}}},"20":{"name":"(anonymous_20)","decl":{"start":{"line":392,"column":21},"end":{"line":392,"column":22}},"loc":{"start":{"line":392,"column":32},"end":{"line":396,"column":1}}},"21":{"name":"(anonymous_21)","decl":{"start":{"line":398,"column":15},"end":{"line":398,"column":16}},"loc":{"start":{"line":398,"column":34},"end":{"line":467,"column":1}}},"22":{"name":"(anonymous_22)","decl":{"start":{"line":399,"column":35},"end":{"line":399,"column":36}},"loc":{"start":{"line":399,"column":51},"end":{"line":412,"column":3}}},"23":{"name":"(anonymous_23)","decl":{"start":{"line":400,"column":11},"end":{"line":400,"column":12}},"loc":{"start":{"line":400,"column":44},"end":{"line":411,"column":5}}},"24":{"name":"(anonymous_24)","decl":{"start":{"line":414,"column":31},"end":{"line":414,"column":32}},"loc":{"start":{"line":414,"column":47},"end":{"line":444,"column":3}}},"25":{"name":"(anonymous_25)","decl":{"start":{"line":415,"column":11},"end":{"line":415,"column":12}},"loc":{"start":{"line":415,"column":26},"end":{"line":443,"column":5}}},"26":{"name":"(anonymous_26)","decl":{"start":{"line":446,"column":35},"end":{"line":446,"column":36}},"loc":{"start":{"line":446,"column":51},"end":{"line":466,"column":3}}},"27":{"name":"(anonymous_27)","decl":{"start":{"line":447,"column":11},"end":{"line":447,"column":12}},"loc":{"start":{"line":447,"column":30},"end":{"line":465,"column":5}}}},"branchMap":{"1":{"loc":{"start":{"line":30,"column":2},"end":{"line":35,"column":3}},"type":"if","locations":[{"start":{"line":30,"column":2},"end":{"line":35,"column":3}},{"start":{"line":30,"column":2},"end":{"line":35,"column":3}}]},"2":{"loc":{"start":{"line":30,"column":6},"end":{"line":30,"column":69}},"type":"binary-expr","locations":[{"start":{"line":30,"column":6},"end":{"line":30,"column":23}},{"start":{"line":30,"column":27},"end":{"line":30,"column":48}},{"start":{"line":30,"column":52},"end":{"line":30,"column":69}}]},"3":{"loc":{"start":{"line":46,"column":4},"end":{"line":48,"column":5}},"type":"if","locations":[{"start":{"line":46,"column":4},"end":{"line":48,"column":5}},{"start":{"line":46,"column":4},"end":{"line":48,"column":5}}]},"4":{"loc":{"start":{"line":52,"column":4},"end":{"line":79,"column":5}},"type":"switch","locations":[{"start":{"line":53,"column":6},"end":{"line":53,"column":14}},{"start":{"line":54,"column":6},"end":{"line":63,"column":49}},{"start":{"line":65,"column":6},"end":{"line":69,"column":14}},{"start":{"line":71,"column":6},"end":{"line":78,"column":9}}]},"5":{"loc":{"start":{"line":55,"column":8},"end":{"line":61,"column":9}},"type":"if","locations":[{"start":{"line":55,"column":8},"end":{"line":61,"column":9}},{"start":{"line":55,"column":8},"end":{"line":61,"column":9}}]},"6":{"loc":{"start":{"line":56,"column":10},"end":{"line":59,"column":11}},"type":"if","locations":[{"start":{"line":56,"column":10},"end":{"line":59,"column":11}},{"start":{"line":56,"column":10},"end":{"line":59,"column":11}}]},"7":{"loc":{"start":{"line":56,"column":14},"end":{"line":56,"column":49}},"type":"binary-expr","locations":[{"start":{"line":56,"column":14},"end":{"line":56,"column":23}},{"start":{"line":56,"column":27},"end":{"line":56,"column":49}}]},"8":{"loc":{"start":{"line":72,"column":8},"end":{"line":78,"column":9}},"type":"if","locations":[{"start":{"line":72,"column":8},"end":{"line":78,"column":9}},{"start":{"line":72,"column":8},"end":{"line":78,"column":9}}]},"9":{"loc":{"start":{"line":87,"column":2},"end":{"line":92,"column":3}},"type":"if","locations":[{"start":{"line":87,"column":2},"end":{"line":92,"column":3}},{"start":{"line":87,"column":2},"end":{"line":92,"column":3}}]},"10":{"loc":{"start":{"line":87,"column":6},"end":{"line":87,"column":63}},"type":"binary-expr","locations":[{"start":{"line":87,"column":6},"end":{"line":87,"column":15}},{"start":{"line":87,"column":19},"end":{"line":87,"column":63}}]},"11":{"loc":{"start":{"line":89,"column":10},"end":{"line":89,"column":39}},"type":"cond-expr","locations":[{"start":{"line":89,"column":26},"end":{"line":89,"column":30}},{"start":{"line":89,"column":33},"end":{"line":89,"column":39}}]},"12":{"loc":{"start":{"line":103,"column":4},"end":{"line":105,"column":5}},"type":"if","locations":[{"start":{"line":103,"column":4},"end":{"line":105,"column":5}},{"start":{"line":103,"column":4},"end":{"line":105,"column":5}}]},"13":{"loc":{"start":{"line":108,"column":4},"end":{"line":108,"column":28}},"type":"if","locations":[{"start":{"line":108,"column":4},"end":{"line":108,"column":28}},{"start":{"line":108,"column":4},"end":{"line":108,"column":28}}]},"14":{"loc":{"start":{"line":109,"column":4},"end":{"line":119,"column":5}},"type":"if","locations":[{"start":{"line":109,"column":4},"end":{"line":119,"column":5}},{"start":{"line":109,"column":4},"end":{"line":119,"column":5}}]},"15":{"loc":{"start":{"line":113,"column":11},"end":{"line":119,"column":5}},"type":"if","locations":[{"start":{"line":113,"column":11},"end":{"line":119,"column":5}},{"start":{"line":113,"column":11},"end":{"line":119,"column":5}}]},"16":{"loc":{"start":{"line":132,"column":9},"end":{"line":132,"column":59}},"type":"binary-expr","locations":[{"start":{"line":132,"column":9},"end":{"line":132,"column":43}},{"start":{"line":132,"column":47},"end":{"line":132,"column":59}}]},"17":{"loc":{"start":{"line":134,"column":4},"end":{"line":149,"column":5}},"type":"if","locations":[{"start":{"line":134,"column":4},"end":{"line":149,"column":5}},{"start":{"line":134,"column":4},"end":{"line":149,"column":5}}]},"18":{"loc":{"start":{"line":135,"column":6},"end":{"line":147,"column":7}},"type":"if","locations":[{"start":{"line":135,"column":6},"end":{"line":147,"column":7}},{"start":{"line":135,"column":6},"end":{"line":147,"column":7}}]},"19":{"loc":{"start":{"line":136,"column":8},"end":{"line":144,"column":9}},"type":"if","locations":[{"start":{"line":136,"column":8},"end":{"line":144,"column":9}},{"start":{"line":136,"column":8},"end":{"line":144,"column":9}}]},"20":{"loc":{"start":{"line":138,"column":10},"end":{"line":139,"column":60}},"type":"if","locations":[{"start":{"line":138,"column":10},"end":{"line":139,"column":60}},{"start":{"line":138,"column":10},"end":{"line":139,"column":60}}]},"21":{"loc":{"start":{"line":142,"column":10},"end":{"line":143,"column":60}},"type":"if","locations":[{"start":{"line":142,"column":10},"end":{"line":143,"column":60}},{"start":{"line":142,"column":10},"end":{"line":143,"column":60}}]},"22":{"loc":{"start":{"line":152,"column":2},"end":{"line":155,"column":3}},"type":"if","locations":[{"start":{"line":152,"column":2},"end":{"line":155,"column":3}},{"start":{"line":152,"column":2},"end":{"line":155,"column":3}}]},"23":{"loc":{"start":{"line":172,"column":11},"end":{"line":172,"column":44}},"type":"binary-expr","locations":[{"start":{"line":172,"column":11},"end":{"line":172,"column":31}},{"start":{"line":172,"column":35},"end":{"line":172,"column":44}}]},"24":{"loc":{"start":{"line":179,"column":2},"end":{"line":181,"column":3}},"type":"if","locations":[{"start":{"line":179,"column":2},"end":{"line":181,"column":3}},{"start":{"line":179,"column":2},"end":{"line":181,"column":3}}]},"25":{"loc":{"start":{"line":183,"column":2},"end":{"line":185,"column":3}},"type":"if","locations":[{"start":{"line":183,"column":2},"end":{"line":185,"column":3}},{"start":{"line":183,"column":2},"end":{"line":185,"column":3}}]},"26":{"loc":{"start":{"line":187,"column":2},"end":{"line":189,"column":3}},"type":"if","locations":[{"start":{"line":187,"column":2},"end":{"line":189,"column":3}},{"start":{"line":187,"column":2},"end":{"line":189,"column":3}}]},"27":{"loc":{"start":{"line":196,"column":2},"end":{"line":202,"column":3}},"type":"if","locations":[{"start":{"line":196,"column":2},"end":{"line":202,"column":3}},{"start":{"line":196,"column":2},"end":{"line":202,"column":3}}]},"28":{"loc":{"start":{"line":198,"column":9},"end":{"line":202,"column":3}},"type":"if","locations":[{"start":{"line":198,"column":9},"end":{"line":202,"column":3}},{"start":{"line":198,"column":9},"end":{"line":202,"column":3}}]},"29":{"loc":{"start":{"line":212,"column":2},"end":{"line":212,"column":39}},"type":"if","locations":[{"start":{"line":212,"column":2},"end":{"line":212,"column":39}},{"start":{"line":212,"column":2},"end":{"line":212,"column":39}}]},"30":{"loc":{"start":{"line":239,"column":2},"end":{"line":256,"column":3}},"type":"switch","locations":[{"start":{"line":240,"column":4},"end":{"line":246,"column":7}},{"start":{"line":248,"column":4},"end":{"line":248,"column":24}},{"start":{"line":249,"column":4},"end":{"line":252,"column":18}},{"start":{"line":254,"column":4},"end":{"line":255,"column":100}}]},"31":{"loc":{"start":{"line":242,"column":6},"end":{"line":246,"column":7}},"type":"if","locations":[{"start":{"line":242,"column":6},"end":{"line":246,"column":7}},{"start":{"line":242,"column":6},"end":{"line":246,"column":7}}]},"32":{"loc":{"start":{"line":286,"column":2},"end":{"line":290,"column":3}},"type":"if","locations":[{"start":{"line":286,"column":2},"end":{"line":290,"column":3}},{"start":{"line":286,"column":2},"end":{"line":290,"column":3}}]},"33":{"loc":{"start":{"line":299,"column":2},"end":{"line":304,"column":3}},"type":"if","locations":[{"start":{"line":299,"column":2},"end":{"line":304,"column":3}},{"start":{"line":299,"column":2},"end":{"line":304,"column":3}}]},"34":{"loc":{"start":{"line":306,"column":15},"end":{"line":306,"column":69}},"type":"cond-expr","locations":[{"start":{"line":306,"column":33},"end":{"line":306,"column":62}},{"start":{"line":306,"column":65},"end":{"line":306,"column":69}}]},"35":{"loc":{"start":{"line":316,"column":9},"end":{"line":316,"column":59}},"type":"binary-expr","locations":[{"start":{"line":316,"column":9},"end":{"line":316,"column":30}},{"start":{"line":316,"column":34},"end":{"line":316,"column":59}}]},"36":{"loc":{"start":{"line":342,"column":2},"end":{"line":379,"column":3}},"type":"if","locations":[{"start":{"line":342,"column":2},"end":{"line":379,"column":3}},{"start":{"line":342,"column":2},"end":{"line":379,"column":3}}]},"37":{"loc":{"start":{"line":344,"column":6},"end":{"line":370,"column":7}},"type":"switch","locations":[{"start":{"line":345,"column":8},"end":{"line":353,"column":16}},{"start":{"line":355,"column":8},"end":{"line":357,"column":16}},{"start":{"line":359,"column":8},"end":{"line":366,"column":16}},{"start":{"line":368,"column":8},"end":{"line":369,"column":28}}]},"38":{"loc":{"start":{"line":348,"column":10},"end":{"line":351,"column":11}},"type":"if","locations":[{"start":{"line":348,"column":10},"end":{"line":351,"column":11}},{"start":{"line":348,"column":10},"end":{"line":351,"column":11}}]},"39":{"loc":{"start":{"line":360,"column":10},"end":{"line":364,"column":11}},"type":"if","locations":[{"start":{"line":360,"column":10},"end":{"line":364,"column":11}},{"start":{"line":360,"column":10},"end":{"line":364,"column":11}}]},"40":{"loc":{"start":{"line":373,"column":4},"end":{"line":378,"column":5}},"type":"if","locations":[{"start":{"line":373,"column":4},"end":{"line":378,"column":5}},{"start":{"line":373,"column":4},"end":{"line":378,"column":5}}]},"41":{"loc":{"start":{"line":384,"column":2},"end":{"line":386,"column":3}},"type":"if","locations":[{"start":{"line":384,"column":2},"end":{"line":386,"column":3}},{"start":{"line":384,"column":2},"end":{"line":386,"column":3}}]},"42":{"loc":{"start":{"line":384,"column":6},"end":{"line":384,"column":59}},"type":"binary-expr","locations":[{"start":{"line":384,"column":6},"end":{"line":384,"column":31}},{"start":{"line":384,"column":35},"end":{"line":384,"column":59}}]},"43":{"loc":{"start":{"line":401,"column":6},"end":{"line":410,"column":7}},"type":"if","locations":[{"start":{"line":401,"column":6},"end":{"line":410,"column":7}},{"start":{"line":401,"column":6},"end":{"line":410,"column":7}}]},"44":{"loc":{"start":{"line":406,"column":13},"end":{"line":410,"column":7}},"type":"if","locations":[{"start":{"line":406,"column":13},"end":{"line":410,"column":7}},{"start":{"line":406,"column":13},"end":{"line":410,"column":7}}]},"45":{"loc":{"start":{"line":418,"column":6},"end":{"line":420,"column":7}},"type":"if","locations":[{"start":{"line":418,"column":6},"end":{"line":420,"column":7}},{"start":{"line":418,"column":6},"end":{"line":420,"column":7}}]},"46":{"loc":{"start":{"line":422,"column":6},"end":{"line":435,"column":7}},"type":"if","locations":[{"start":{"line":422,"column":6},"end":{"line":435,"column":7}},{"start":{"line":422,"column":6},"end":{"line":435,"column":7}}]},"47":{"loc":{"start":{"line":422,"column":10},"end":{"line":422,"column":56}},"type":"binary-expr","locations":[{"start":{"line":422,"column":10},"end":{"line":422,"column":31}},{"start":{"line":422,"column":35},"end":{"line":422,"column":56}}]},"48":{"loc":{"start":{"line":423,"column":8},"end":{"line":425,"column":9}},"type":"if","locations":[{"start":{"line":423,"column":8},"end":{"line":425,"column":9}},{"start":{"line":423,"column":8},"end":{"line":425,"column":9}}]},"49":{"loc":{"start":{"line":427,"column":8},"end":{"line":430,"column":9}},"type":"if","locations":[{"start":{"line":427,"column":8},"end":{"line":430,"column":9}},{"start":{"line":427,"column":8},"end":{"line":430,"column":9}}]},"50":{"loc":{"start":{"line":432,"column":8},"end":{"line":434,"column":9}},"type":"if","locations":[{"start":{"line":432,"column":8},"end":{"line":434,"column":9}},{"start":{"line":432,"column":8},"end":{"line":434,"column":9}}]},"51":{"loc":{"start":{"line":432,"column":12},"end":{"line":432,"column":65}},"type":"binary-expr","locations":[{"start":{"line":432,"column":13},"end":{"line":432,"column":24}},{"start":{"line":432,"column":28},"end":{"line":432,"column":39}},{"start":{"line":432,"column":44},"end":{"line":432,"column":65}}]},"52":{"loc":{"start":{"line":437,"column":6},"end":{"line":440,"column":7}},"type":"if","locations":[{"start":{"line":437,"column":6},"end":{"line":440,"column":7}},{"start":{"line":437,"column":6},"end":{"line":440,"column":7}}]},"53":{"loc":{"start":{"line":437,"column":10},"end":{"line":437,"column":47}},"type":"binary-expr","locations":[{"start":{"line":437,"column":10},"end":{"line":437,"column":21}},{"start":{"line":437,"column":25},"end":{"line":437,"column":47}}]},"54":{"loc":{"start":{"line":448,"column":6},"end":{"line":464,"column":7}},"type":"if","locations":[{"start":{"line":448,"column":6},"end":{"line":464,"column":7}},{"start":{"line":448,"column":6},"end":{"line":464,"column":7}}]},"55":{"loc":{"start":{"line":450,"column":8},"end":{"line":456,"column":9}},"type":"if","locations":[{"start":{"line":450,"column":8},"end":{"line":456,"column":9}},{"start":{"line":450,"column":8},"end":{"line":456,"column":9}}]},"56":{"loc":{"start":{"line":452,"column":15},"end":{"line":456,"column":9}},"type":"if","locations":[{"start":{"line":452,"column":15},"end":{"line":456,"column":9}},{"start":{"line":452,"column":15},"end":{"line":456,"column":9}}]},"57":{"loc":{"start":{"line":458,"column":13},"end":{"line":464,"column":7}},"type":"if","locations":[{"start":{"line":458,"column":13},"end":{"line":464,"column":7}},{"start":{"line":458,"column":13},"end":{"line":464,"column":7}}]},"58":{"loc":{"start":{"line":458,"column":17},"end":{"line":458,"column":68}},"type":"binary-expr","locations":[{"start":{"line":458,"column":17},"end":{"line":458,"column":37}},{"start":{"line":458,"column":41},"end":{"line":458,"column":68}}]}},"s":{"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":91,"12":91,"13":91,"14":1,"15":87,"16":87,"17":48,"18":48,"19":39,"20":1,"21":1,"22":81,"23":81,"24":81,"25":358,"26":6,"27":352,"28":352,"29":75,"30":53,"31":38,"32":38,"33":15,"34":22,"35":22,"36":0,"37":0,"38":0,"39":0,"40":277,"41":9,"42":9,"43":9,"44":268,"45":1,"46":11,"47":11,"48":11,"49":0,"50":0,"51":11,"52":11,"53":11,"54":11,"55":1,"56":13,"57":13,"58":13,"59":140,"60":1,"61":139,"62":139,"63":12,"64":127,"65":4,"66":4,"67":4,"68":123,"69":2,"70":2,"71":2,"72":121,"73":12,"74":12,"75":1,"76":4,"77":4,"78":4,"79":4,"80":4,"81":20,"82":20,"83":4,"84":2,"85":1,"86":1,"87":1,"88":1,"89":1,"90":1,"91":2,"92":4,"93":16,"94":4,"95":1,"96":1,"97":3,"98":1,"99":125,"100":125,"101":282,"102":125,"103":67,"104":59,"105":8,"106":2,"107":6,"108":6,"109":1,"110":124,"111":124,"112":124,"113":0,"114":0,"115":0,"116":124,"117":124,"118":1,"119":112,"120":112,"121":112,"122":112,"123":106,"124":6,"125":6,"126":6,"127":6,"128":1,"129":90,"130":90,"131":90,"132":90,"133":6,"134":6,"135":6,"136":6,"137":90,"138":1,"139":20,"140":5,"141":5,"142":1,"143":4,"144":14,"145":14,"146":14,"147":1,"148":1,"149":3,"150":3,"151":1,"152":2,"153":2,"154":2,"155":2,"156":2,"157":2,"158":1,"159":18,"160":18,"161":18,"162":3,"163":15,"164":18,"165":14,"166":1,"167":28,"168":28,"169":6,"170":6,"171":6,"172":6,"173":22,"174":22,"175":19,"176":1,"177":63,"178":63,"179":63,"180":63,"181":28,"182":60,"183":60,"184":58,"185":1,"186":27,"187":27,"188":27,"189":27,"190":1,"191":63,"192":63,"193":63,"194":58,"195":58,"196":37,"197":37,"198":75,"199":38,"200":38,"201":38,"202":38,"203":27,"204":27,"205":11,"206":5,"207":22,"208":22,"209":15,"210":2,"211":13,"212":11,"213":0,"214":27,"215":1,"216":47,"217":47,"218":47,"219":47,"220":1,"221":46,"222":1,"223":52,"224":52,"225":52,"226":52,"227":251,"228":251,"229":385,"230":22,"231":22,"232":22,"233":363,"234":52,"235":311,"236":251,"237":251,"238":3351,"239":3351,"240":81,"241":3270,"242":320,"243":125,"244":195,"245":87,"246":87,"247":108,"248":13,"249":3045,"250":53,"251":53,"252":2992,"253":251,"254":251,"255":3939,"256":212,"257":212,"258":11,"259":201,"260":15,"261":186,"262":212,"263":3727,"264":27,"265":27,"266":27,"267":3700},"f":{"1":91,"2":87,"3":81,"4":11,"5":13,"6":4,"7":125,"8":67,"9":124,"10":112,"11":90,"12":20,"13":3,"14":2,"15":18,"16":28,"17":63,"18":27,"19":63,"20":52,"21":251,"22":251,"23":385,"24":251,"25":3351,"26":251,"27":3939},"b":{"1":[48,39],"2":[87,60,66],"3":[6,352],"4":[54,75,0,277],"5":[53,22],"6":[38,15],"7":[53,38],"8":[9,268],"9":[0,11],"10":[11,0],"11":[0,0],"12":[1,139],"13":[12,127],"14":[4,123],"15":[2,121],"16":[20,20],"17":[4,16],"18":[2,2],"19":[1,1],"20":[1,0],"21":[1,0],"22":[1,3],"23":[282,127],"24":[59,8],"25":[2,6],"26":[6,0],"27":[124,0],"28":[0,0],"29":[106,6],"30":[5,2,14,1],"31":[1,4],"32":[3,15],"33":[6,22],"34":[20,1],"35":[88,67],"36":[37,21],"37":[38,22,15,0],"38":[27,11],"39":[2,13],"40":[1,26],"41":[1,46],"42":[47,1],"43":[22,363],"44":[52,311],"45":[81,3270],"46":[320,2950],"47":[3270,3012],"48":[125,195],"49":[87,108],"50":[13,95],"51":[108,95,13],"52":[53,2992],"53":[3045,119],"54":[212,3727],"55":[11,201],"56":[15,186],"57":[27,3700],"58":[3727,50]},"hash":"fd1e066a07dafa095075a6bcf6df9afd51080411"} +,"/home/travis/build/babel/babylon/src/plugins/jsx/xhtml.js": {"path":"/home/travis/build/babel/babylon/src/plugins/jsx/xhtml.js","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"hash":"1a4112b01cbc70550503fc26b73988e476d3cc4b"} +,"/home/travis/build/babel/babylon/src/tokenizer/context.js": {"path":"/home/travis/build/babel/babylon/src/tokenizer/context.js","statementMap":{"1":{"start":{"line":15,"column":4},"end":{"line":15,"column":23}},"2":{"start":{"line":16,"column":4},"end":{"line":16,"column":27}},"3":{"start":{"line":17,"column":4},"end":{"line":17,"column":41}},"4":{"start":{"line":18,"column":4},"end":{"line":18,"column":29}},"5":{"start":{"line":29,"column":4},"end":{"line":37,"column":1}},"6":{"start":{"line":35,"column":51},"end":{"line":35,"column":68}},"7":{"start":{"line":41,"column":0},"end":{"line":56,"column":2}},"8":{"start":{"line":42,"column":2},"end":{"line":45,"column":3}},"9":{"start":{"line":43,"column":4},"end":{"line":43,"column":34}},"10":{"start":{"line":44,"column":4},"end":{"line":44,"column":11}},"11":{"start":{"line":47,"column":12},"end":{"line":47,"column":36}},"12":{"start":{"line":48,"column":2},"end":{"line":55,"column":3}},"13":{"start":{"line":49,"column":4},"end":{"line":49,"column":29}},"14":{"start":{"line":50,"column":4},"end":{"line":50,"column":35}},"15":{"start":{"line":51,"column":9},"end":{"line":55,"column":3}},"16":{"start":{"line":52,"column":4},"end":{"line":52,"column":34}},"17":{"start":{"line":54,"column":4},"end":{"line":54,"column":41}},"18":{"start":{"line":58,"column":0},"end":{"line":66,"column":2}},"19":{"start":{"line":59,"column":2},"end":{"line":59,"column":33}},"20":{"start":{"line":61,"column":2},"end":{"line":65,"column":3}},"21":{"start":{"line":62,"column":4},"end":{"line":64,"column":5}},"22":{"start":{"line":63,"column":6},"end":{"line":63,"column":36}},"23":{"start":{"line":68,"column":0},"end":{"line":71,"column":2}},"24":{"start":{"line":69,"column":2},"end":{"line":69,"column":102}},"25":{"start":{"line":70,"column":2},"end":{"line":70,"column":32}},"26":{"start":{"line":73,"column":0},"end":{"line":76,"column":2}},"27":{"start":{"line":74,"column":2},"end":{"line":74,"column":47}},"28":{"start":{"line":75,"column":2},"end":{"line":75,"column":32}},"29":{"start":{"line":78,"column":0},"end":{"line":83,"column":2}},"30":{"start":{"line":79,"column":24},"end":{"line":80,"column":71}},"31":{"start":{"line":81,"column":2},"end":{"line":81,"column":90}},"32":{"start":{"line":82,"column":2},"end":{"line":82,"column":32}},"33":{"start":{"line":85,"column":0},"end":{"line":87,"column":2}},"34":{"start":{"line":89,"column":0},"end":{"line":95,"column":2}},"35":{"start":{"line":90,"column":2},"end":{"line":92,"column":3}},"36":{"start":{"line":91,"column":4},"end":{"line":91,"column":54}},"37":{"start":{"line":94,"column":2},"end":{"line":94,"column":33}},"38":{"start":{"line":97,"column":0},"end":{"line":104,"column":2}},"39":{"start":{"line":98,"column":2},"end":{"line":102,"column":3}},"40":{"start":{"line":99,"column":4},"end":{"line":99,"column":29}},"41":{"start":{"line":101,"column":4},"end":{"line":101,"column":44}},"42":{"start":{"line":103,"column":2},"end":{"line":103,"column":33}}},"fnMap":{"1":{"name":"(anonymous_1)","decl":{"start":{"line":9,"column":2},"end":{"line":9,"column":3}},"loc":{"start":{"line":14,"column":4},"end":{"line":19,"column":3}}},"2":{"name":"(anonymous_2)","decl":{"start":{"line":35,"column":44},"end":{"line":35,"column":45}},"loc":{"start":{"line":35,"column":51},"end":{"line":35,"column":68}}},"3":{"name":"(anonymous_3)","decl":{"start":{"line":41,"column":52},"end":{"line":41,"column":53}},"loc":{"start":{"line":41,"column":64},"end":{"line":56,"column":1}}},"4":{"name":"(anonymous_4)","decl":{"start":{"line":58,"column":24},"end":{"line":58,"column":25}},"loc":{"start":{"line":58,"column":44},"end":{"line":66,"column":1}}},"5":{"name":"(anonymous_5)","decl":{"start":{"line":68,"column":26},"end":{"line":68,"column":27}},"loc":{"start":{"line":68,"column":46},"end":{"line":71,"column":1}}},"6":{"name":"(anonymous_6)","decl":{"start":{"line":73,"column":32},"end":{"line":73,"column":33}},"loc":{"start":{"line":73,"column":44},"end":{"line":76,"column":1}}},"7":{"name":"(anonymous_7)","decl":{"start":{"line":78,"column":26},"end":{"line":78,"column":27}},"loc":{"start":{"line":78,"column":46},"end":{"line":83,"column":1}}},"8":{"name":"(anonymous_8)","decl":{"start":{"line":85,"column":26},"end":{"line":85,"column":27}},"loc":{"start":{"line":85,"column":38},"end":{"line":87,"column":1}}},"9":{"name":"(anonymous_9)","decl":{"start":{"line":89,"column":29},"end":{"line":89,"column":30}},"loc":{"start":{"line":89,"column":41},"end":{"line":95,"column":1}}},"10":{"name":"(anonymous_10)","decl":{"start":{"line":97,"column":29},"end":{"line":97,"column":30}},"loc":{"start":{"line":97,"column":41},"end":{"line":104,"column":1}}}},"branchMap":{"1":{"loc":{"start":{"line":42,"column":2},"end":{"line":45,"column":3}},"type":"if","locations":[{"start":{"line":42,"column":2},"end":{"line":45,"column":3}},{"start":{"line":42,"column":2},"end":{"line":45,"column":3}}]},"2":{"loc":{"start":{"line":48,"column":2},"end":{"line":55,"column":3}},"type":"if","locations":[{"start":{"line":48,"column":2},"end":{"line":55,"column":3}},{"start":{"line":48,"column":2},"end":{"line":55,"column":3}}]},"3":{"loc":{"start":{"line":48,"column":6},"end":{"line":48,"column":84}},"type":"binary-expr","locations":[{"start":{"line":48,"column":6},"end":{"line":48,"column":34}},{"start":{"line":48,"column":38},"end":{"line":48,"column":84}}]},"4":{"loc":{"start":{"line":51,"column":9},"end":{"line":55,"column":3}},"type":"if","locations":[{"start":{"line":51,"column":9},"end":{"line":55,"column":3}},{"start":{"line":51,"column":9},"end":{"line":55,"column":3}}]},"5":{"loc":{"start":{"line":61,"column":2},"end":{"line":65,"column":3}},"type":"if","locations":[{"start":{"line":61,"column":2},"end":{"line":65,"column":3}},{"start":{"line":61,"column":2},"end":{"line":65,"column":3}}]},"6":{"loc":{"start":{"line":61,"column":6},"end":{"line":61,"column":76}},"type":"binary-expr","locations":[{"start":{"line":61,"column":6},"end":{"line":61,"column":26}},{"start":{"line":61,"column":30},"end":{"line":61,"column":52}},{"start":{"line":61,"column":56},"end":{"line":61,"column":76}}]},"7":{"loc":{"start":{"line":62,"column":4},"end":{"line":64,"column":5}},"type":"if","locations":[{"start":{"line":62,"column":4},"end":{"line":64,"column":5}},{"start":{"line":62,"column":4},"end":{"line":64,"column":5}}]},"8":{"loc":{"start":{"line":69,"column":26},"end":{"line":69,"column":100}},"type":"cond-expr","locations":[{"start":{"line":69,"column":56},"end":{"line":69,"column":76}},{"start":{"line":69,"column":79},"end":{"line":69,"column":100}}]},"9":{"loc":{"start":{"line":79,"column":24},"end":{"line":80,"column":71}},"type":"binary-expr","locations":[{"start":{"line":79,"column":24},"end":{"line":79,"column":43}},{"start":{"line":79,"column":47},"end":{"line":79,"column":67}},{"start":{"line":80,"column":24},"end":{"line":80,"column":45}},{"start":{"line":80,"column":49},"end":{"line":80,"column":71}}]},"10":{"loc":{"start":{"line":81,"column":26},"end":{"line":81,"column":88}},"type":"cond-expr","locations":[{"start":{"line":81,"column":44},"end":{"line":81,"column":64}},{"start":{"line":81,"column":67},"end":{"line":81,"column":88}}]},"11":{"loc":{"start":{"line":90,"column":2},"end":{"line":92,"column":3}},"type":"if","locations":[{"start":{"line":90,"column":2},"end":{"line":92,"column":3}},{"start":{"line":90,"column":2},"end":{"line":92,"column":3}}]},"12":{"loc":{"start":{"line":98,"column":2},"end":{"line":102,"column":3}},"type":"if","locations":[{"start":{"line":98,"column":2},"end":{"line":102,"column":3}},{"start":{"line":98,"column":2},"end":{"line":102,"column":3}}]}},"s":{"1":10,"2":10,"3":10,"4":10,"5":1,"6":87,"7":1,"8":2509,"9":4,"10":4,"11":2505,"12":2505,"13":83,"14":83,"15":2422,"16":30,"17":2392,"18":1,"19":3962,"20":3962,"21":284,"22":20,"23":1,"24":1353,"25":1353,"26":1,"27":17,"28":17,"29":1,"30":1488,"31":1488,"32":1488,"33":1,"34":1,"35":441,"36":98,"37":441,"38":1,"39":59,"40":24,"41":35,"42":59},"f":{"1":10,"2":87,"3":2509,"4":3962,"5":1353,"6":17,"7":1488,"8":67,"9":441,"10":59},"b":{"1":[4,2505],"2":[83,2422],"3":[2505,831],"4":[30,2392],"5":[284,3678],"6":[3962,3925,3895],"7":[20,264],"8":[996,357],"9":[1488,1461,1387,1375],"10":[179,1309],"11":[98,343],"12":[24,35]},"hash":"3082b707a208ee9f1e9021f7847025ae0fd9e6e5"} +,"/home/travis/build/babel/babylon/src/tokenizer/index.js": {"path":"/home/travis/build/babel/babylon/src/tokenizer/index.js","statementMap":{"1":{"start":{"line":18,"column":4},"end":{"line":18,"column":27}},"2":{"start":{"line":19,"column":4},"end":{"line":19,"column":29}},"3":{"start":{"line":20,"column":4},"end":{"line":20,"column":29}},"4":{"start":{"line":21,"column":4},"end":{"line":21,"column":25}},"5":{"start":{"line":22,"column":4},"end":{"line":22,"column":64}},"6":{"start":{"line":36,"column":2},"end":{"line":40,"column":3}},"7":{"start":{"line":37,"column":4},"end":{"line":37,"column":37}},"8":{"start":{"line":39,"column":4},"end":{"line":39,"column":102}},"9":{"start":{"line":45,"column":4},"end":{"line":45,"column":27}},"10":{"start":{"line":46,"column":4},"end":{"line":46,"column":36}},"11":{"start":{"line":52,"column":4},"end":{"line":54,"column":5}},"12":{"start":{"line":53,"column":6},"end":{"line":53,"column":52}},"13":{"start":{"line":56,"column":4},"end":{"line":56,"column":43}},"14":{"start":{"line":57,"column":4},"end":{"line":57,"column":47}},"15":{"start":{"line":58,"column":4},"end":{"line":58,"column":49}},"16":{"start":{"line":59,"column":4},"end":{"line":59,"column":53}},"17":{"start":{"line":60,"column":4},"end":{"line":60,"column":21}},"18":{"start":{"line":66,"column":4},"end":{"line":71,"column":5}},"19":{"start":{"line":67,"column":6},"end":{"line":67,"column":18}},"20":{"start":{"line":68,"column":6},"end":{"line":68,"column":18}},"21":{"start":{"line":70,"column":6},"end":{"line":70,"column":19}},"22":{"start":{"line":77,"column":4},"end":{"line":77,"column":36}},"23":{"start":{"line":83,"column":4},"end":{"line":83,"column":27}},"24":{"start":{"line":89,"column":14},"end":{"line":89,"column":24}},"25":{"start":{"line":90,"column":4},"end":{"line":90,"column":33}},"26":{"start":{"line":92,"column":4},"end":{"line":92,"column":28}},"27":{"start":{"line":93,"column":4},"end":{"line":93,"column":16}},"28":{"start":{"line":94,"column":4},"end":{"line":94,"column":29}},"29":{"start":{"line":96,"column":15},"end":{"line":96,"column":37}},"30":{"start":{"line":97,"column":4},"end":{"line":97,"column":21}},"31":{"start":{"line":98,"column":4},"end":{"line":98,"column":16}},"32":{"start":{"line":105,"column":4},"end":{"line":105,"column":31}},"33":{"start":{"line":106,"column":4},"end":{"line":106,"column":62}},"34":{"start":{"line":106,"column":55},"end":{"line":106,"column":62}},"35":{"start":{"line":107,"column":4},"end":{"line":107,"column":38}},"36":{"start":{"line":108,"column":4},"end":{"line":111,"column":5}},"37":{"start":{"line":109,"column":6},"end":{"line":109,"column":88}},"38":{"start":{"line":110,"column":6},"end":{"line":110,"column":27}},"39":{"start":{"line":112,"column":4},"end":{"line":112,"column":21}},"40":{"start":{"line":116,"column":4},"end":{"line":116,"column":61}},"41":{"start":{"line":123,"column":21},"end":{"line":123,"column":38}},"42":{"start":{"line":124,"column":4},"end":{"line":124,"column":67}},"43":{"start":{"line":124,"column":50},"end":{"line":124,"column":67}},"44":{"start":{"line":126,"column":4},"end":{"line":126,"column":37}},"45":{"start":{"line":127,"column":4},"end":{"line":127,"column":36}},"46":{"start":{"line":128,"column":4},"end":{"line":128,"column":38}},"47":{"start":{"line":129,"column":4},"end":{"line":129,"column":51}},"48":{"start":{"line":130,"column":4},"end":{"line":130,"column":77}},"49":{"start":{"line":130,"column":45},"end":{"line":130,"column":77}},"50":{"start":{"line":132,"column":4},"end":{"line":136,"column":5}},"51":{"start":{"line":133,"column":6},"end":{"line":133,"column":39}},"52":{"start":{"line":135,"column":6},"end":{"line":135,"column":54}},"53":{"start":{"line":142,"column":4},"end":{"line":146,"column":5}},"54":{"start":{"line":143,"column":6},"end":{"line":143,"column":29}},"55":{"start":{"line":145,"column":6},"end":{"line":145,"column":41}},"56":{"start":{"line":150,"column":15},"end":{"line":150,"column":52}},"57":{"start":{"line":151,"column":4},"end":{"line":151,"column":54}},"58":{"start":{"line":151,"column":42},"end":{"line":151,"column":54}},"59":{"start":{"line":153,"column":15},"end":{"line":153,"column":56}},"60":{"start":{"line":154,"column":4},"end":{"line":154,"column":43}},"61":{"start":{"line":158,"column":18},"end":{"line":164,"column":5}},"62":{"start":{"line":166,"column":4},"end":{"line":170,"column":5}},"63":{"start":{"line":167,"column":6},"end":{"line":167,"column":38}},"64":{"start":{"line":168,"column":6},"end":{"line":168,"column":40}},"65":{"start":{"line":169,"column":6},"end":{"line":169,"column":31}},"66":{"start":{"line":174,"column":19},"end":{"line":174,"column":43}},"67":{"start":{"line":175,"column":16},"end":{"line":175,"column":30}},"68":{"start":{"line":175,"column":38},"end":{"line":175,"column":83}},"69":{"start":{"line":176,"column":4},"end":{"line":176,"column":75}},"70":{"start":{"line":176,"column":20},"end":{"line":176,"column":75}},"71":{"start":{"line":178,"column":4},"end":{"line":178,"column":29}},"72":{"start":{"line":179,"column":4},"end":{"line":179,"column":33}},"73":{"start":{"line":181,"column":4},"end":{"line":184,"column":5}},"74":{"start":{"line":182,"column":6},"end":{"line":182,"column":27}},"75":{"start":{"line":183,"column":6},"end":{"line":183,"column":59}},"76":{"start":{"line":186,"column":4},"end":{"line":186,"column":120}},"77":{"start":{"line":190,"column":16},"end":{"line":190,"column":30}},"78":{"start":{"line":191,"column":19},"end":{"line":191,"column":43}},"79":{"start":{"line":192,"column":13},"end":{"line":192,"column":63}},"80":{"start":{"line":193,"column":4},"end":{"line":196,"column":5}},"81":{"start":{"line":194,"column":6},"end":{"line":194,"column":23}},"82":{"start":{"line":195,"column":6},"end":{"line":195,"column":49}},"83":{"start":{"line":198,"column":4},"end":{"line":198,"column":140}},"84":{"start":{"line":205,"column":4},"end":{"line":245,"column":5}},"85":{"start":{"line":205,"column":10},"end":{"line":245,"column":5}},"86":{"start":{"line":206,"column":15},"end":{"line":206,"column":52}},"87":{"start":{"line":207,"column":6},"end":{"line":244,"column":7}},"88":{"start":{"line":209,"column":10},"end":{"line":209,"column":27}},"89":{"start":{"line":210,"column":10},"end":{"line":210,"column":16}},"90":{"start":{"line":213,"column":10},"end":{"line":215,"column":11}},"91":{"start":{"line":214,"column":12},"end":{"line":214,"column":29}},"92":{"start":{"line":218,"column":10},"end":{"line":218,"column":27}},"93":{"start":{"line":219,"column":10},"end":{"line":219,"column":31}},"94":{"start":{"line":220,"column":10},"end":{"line":220,"column":48}},"95":{"start":{"line":221,"column":10},"end":{"line":221,"column":16}},"96":{"start":{"line":224,"column":10},"end":{"line":235,"column":11}},"97":{"start":{"line":226,"column":14},"end":{"line":226,"column":38}},"98":{"start":{"line":227,"column":14},"end":{"line":227,"column":20}},"99":{"start":{"line":230,"column":14},"end":{"line":230,"column":38}},"100":{"start":{"line":231,"column":14},"end":{"line":231,"column":20}},"101":{"start":{"line":234,"column":14},"end":{"line":234,"column":25}},"102":{"start":{"line":236,"column":10},"end":{"line":236,"column":16}},"103":{"start":{"line":239,"column":10},"end":{"line":243,"column":11}},"104":{"start":{"line":240,"column":12},"end":{"line":240,"column":29}},"105":{"start":{"line":242,"column":12},"end":{"line":242,"column":23}},"106":{"start":{"line":254,"column":4},"end":{"line":254,"column":36}},"107":{"start":{"line":255,"column":4},"end":{"line":255,"column":49}},"108":{"start":{"line":256,"column":19},"end":{"line":256,"column":34}},"109":{"start":{"line":257,"column":4},"end":{"line":257,"column":27}},"110":{"start":{"line":258,"column":4},"end":{"line":258,"column":27}},"111":{"start":{"line":260,"column":4},"end":{"line":260,"column":33}},"112":{"start":{"line":273,"column":15},"end":{"line":273,"column":56}},"113":{"start":{"line":274,"column":4},"end":{"line":276,"column":5}},"114":{"start":{"line":275,"column":6},"end":{"line":275,"column":35}},"115":{"start":{"line":278,"column":16},"end":{"line":278,"column":57}},"116":{"start":{"line":279,"column":4},"end":{"line":285,"column":5}},"117":{"start":{"line":280,"column":6},"end":{"line":280,"column":26}},"118":{"start":{"line":281,"column":6},"end":{"line":281,"column":43}},"119":{"start":{"line":283,"column":6},"end":{"line":283,"column":23}},"120":{"start":{"line":284,"column":6},"end":{"line":284,"column":38}},"121":{"start":{"line":289,"column":4},"end":{"line":292,"column":5}},"122":{"start":{"line":290,"column":6},"end":{"line":290,"column":23}},"123":{"start":{"line":291,"column":6},"end":{"line":291,"column":31}},"124":{"start":{"line":294,"column":15},"end":{"line":294,"column":56}},"125":{"start":{"line":295,"column":4},"end":{"line":299,"column":5}},"126":{"start":{"line":296,"column":6},"end":{"line":296,"column":41}},"127":{"start":{"line":298,"column":6},"end":{"line":298,"column":40}},"128":{"start":{"line":303,"column":15},"end":{"line":303,"column":48}},"129":{"start":{"line":304,"column":16},"end":{"line":304,"column":17}},"130":{"start":{"line":305,"column":15},"end":{"line":305,"column":56}},"131":{"start":{"line":307,"column":4},"end":{"line":311,"column":5}},"132":{"start":{"line":308,"column":6},"end":{"line":308,"column":14}},"133":{"start":{"line":309,"column":6},"end":{"line":309,"column":55}},"134":{"start":{"line":310,"column":6},"end":{"line":310,"column":25}},"135":{"start":{"line":313,"column":4},"end":{"line":316,"column":5}},"136":{"start":{"line":314,"column":6},"end":{"line":314,"column":14}},"137":{"start":{"line":315,"column":6},"end":{"line":315,"column":23}},"138":{"start":{"line":318,"column":4},"end":{"line":318,"column":38}},"139":{"start":{"line":322,"column":15},"end":{"line":322,"column":56}},"140":{"start":{"line":323,"column":4},"end":{"line":323,"column":92}},"141":{"start":{"line":323,"column":23},"end":{"line":323,"column":92}},"142":{"start":{"line":324,"column":4},"end":{"line":324,"column":56}},"143":{"start":{"line":324,"column":21},"end":{"line":324,"column":56}},"144":{"start":{"line":325,"column":4},"end":{"line":325,"column":73}},"145":{"start":{"line":329,"column":15},"end":{"line":329,"column":56}},"146":{"start":{"line":330,"column":4},"end":{"line":334,"column":5}},"147":{"start":{"line":331,"column":6},"end":{"line":331,"column":41}},"148":{"start":{"line":333,"column":6},"end":{"line":333,"column":45}},"149":{"start":{"line":338,"column":15},"end":{"line":338,"column":56}},"150":{"start":{"line":340,"column":4},"end":{"line":348,"column":5}},"151":{"start":{"line":341,"column":6},"end":{"line":346,"column":7}},"152":{"start":{"line":343,"column":8},"end":{"line":343,"column":32}},"153":{"start":{"line":344,"column":8},"end":{"line":344,"column":25}},"154":{"start":{"line":345,"column":8},"end":{"line":345,"column":32}},"155":{"start":{"line":347,"column":6},"end":{"line":347,"column":41}},"156":{"start":{"line":350,"column":4},"end":{"line":354,"column":5}},"157":{"start":{"line":351,"column":6},"end":{"line":351,"column":41}},"158":{"start":{"line":353,"column":6},"end":{"line":353,"column":42}},"159":{"start":{"line":358,"column":15},"end":{"line":358,"column":56}},"160":{"start":{"line":359,"column":15},"end":{"line":359,"column":16}},"161":{"start":{"line":361,"column":4},"end":{"line":365,"column":5}},"162":{"start":{"line":362,"column":6},"end":{"line":362,"column":85}},"163":{"start":{"line":363,"column":6},"end":{"line":363,"column":105}},"164":{"start":{"line":363,"column":63},"end":{"line":363,"column":105}},"165":{"start":{"line":364,"column":6},"end":{"line":364,"column":46}},"166":{"start":{"line":367,"column":4},"end":{"line":373,"column":5}},"167":{"start":{"line":368,"column":6},"end":{"line":368,"column":43}},"168":{"start":{"line":368,"column":25},"end":{"line":368,"column":43}},"169":{"start":{"line":370,"column":6},"end":{"line":370,"column":30}},"170":{"start":{"line":371,"column":6},"end":{"line":371,"column":23}},"171":{"start":{"line":372,"column":6},"end":{"line":372,"column":30}},"172":{"start":{"line":375,"column":4},"end":{"line":378,"column":5}},"173":{"start":{"line":377,"column":6},"end":{"line":377,"column":15}},"174":{"start":{"line":380,"column":4},"end":{"line":380,"column":46}},"175":{"start":{"line":384,"column":15},"end":{"line":384,"column":56}},"176":{"start":{"line":385,"column":4},"end":{"line":385,"column":113}},"177":{"start":{"line":385,"column":21},"end":{"line":385,"column":113}},"178":{"start":{"line":386,"column":4},"end":{"line":389,"column":5}},"179":{"start":{"line":387,"column":6},"end":{"line":387,"column":26}},"180":{"start":{"line":388,"column":6},"end":{"line":388,"column":40}},"181":{"start":{"line":390,"column":4},"end":{"line":390,"column":61}},"182":{"start":{"line":394,"column":4},"end":{"line":467,"column":5}},"183":{"start":{"line":398,"column":8},"end":{"line":398,"column":36}},"184":{"start":{"line":401,"column":15},"end":{"line":401,"column":32}},"185":{"start":{"line":401,"column":33},"end":{"line":401,"column":68}},"186":{"start":{"line":402,"column":15},"end":{"line":402,"column":32}},"187":{"start":{"line":402,"column":33},"end":{"line":402,"column":68}},"188":{"start":{"line":403,"column":15},"end":{"line":403,"column":32}},"189":{"start":{"line":403,"column":33},"end":{"line":403,"column":66}},"190":{"start":{"line":404,"column":15},"end":{"line":404,"column":32}},"191":{"start":{"line":404,"column":33},"end":{"line":404,"column":67}},"192":{"start":{"line":405,"column":15},"end":{"line":405,"column":32}},"193":{"start":{"line":405,"column":33},"end":{"line":405,"column":70}},"194":{"start":{"line":406,"column":15},"end":{"line":406,"column":32}},"195":{"start":{"line":406,"column":33},"end":{"line":406,"column":70}},"196":{"start":{"line":407,"column":16},"end":{"line":407,"column":33}},"197":{"start":{"line":407,"column":34},"end":{"line":407,"column":69}},"198":{"start":{"line":408,"column":16},"end":{"line":408,"column":33}},"199":{"start":{"line":408,"column":34},"end":{"line":408,"column":69}},"200":{"start":{"line":411,"column":8},"end":{"line":416,"column":9}},"201":{"start":{"line":412,"column":10},"end":{"line":412,"column":50}},"202":{"start":{"line":414,"column":10},"end":{"line":414,"column":27}},"203":{"start":{"line":415,"column":10},"end":{"line":415,"column":44}},"204":{"start":{"line":418,"column":15},"end":{"line":418,"column":32}},"205":{"start":{"line":418,"column":33},"end":{"line":418,"column":70}},"206":{"start":{"line":419,"column":15},"end":{"line":419,"column":32}},"207":{"start":{"line":419,"column":33},"end":{"line":419,"column":64}},"208":{"start":{"line":422,"column":8},"end":{"line":422,"column":25}},"209":{"start":{"line":423,"column":8},"end":{"line":423,"column":46}},"210":{"start":{"line":426,"column":19},"end":{"line":426,"column":60}},"211":{"start":{"line":427,"column":8},"end":{"line":427,"column":73}},"212":{"start":{"line":427,"column":41},"end":{"line":427,"column":73}},"213":{"start":{"line":428,"column":8},"end":{"line":428,"column":72}},"214":{"start":{"line":428,"column":41},"end":{"line":428,"column":72}},"215":{"start":{"line":429,"column":8},"end":{"line":429,"column":71}},"216":{"start":{"line":429,"column":40},"end":{"line":429,"column":71}},"217":{"start":{"line":433,"column":8},"end":{"line":433,"column":38}},"218":{"start":{"line":437,"column":8},"end":{"line":437,"column":37}},"219":{"start":{"line":445,"column":8},"end":{"line":445,"column":38}},"220":{"start":{"line":448,"column":8},"end":{"line":448,"column":48}},"221":{"start":{"line":451,"column":8},"end":{"line":451,"column":45}},"222":{"start":{"line":454,"column":8},"end":{"line":454,"column":38}},"223":{"start":{"line":457,"column":8},"end":{"line":457,"column":45}},"224":{"start":{"line":460,"column":8},"end":{"line":460,"column":42}},"225":{"start":{"line":463,"column":8},"end":{"line":463,"column":44}},"226":{"start":{"line":466,"column":8},"end":{"line":466,"column":43}},"227":{"start":{"line":469,"column":4},"end":{"line":469,"column":84}},"228":{"start":{"line":473,"column":14},"end":{"line":473,"column":69}},"229":{"start":{"line":474,"column":4},"end":{"line":474,"column":27}},"230":{"start":{"line":475,"column":4},"end":{"line":475,"column":39}},"231":{"start":{"line":479,"column":34},"end":{"line":479,"column":48}},"232":{"start":{"line":480,"column":4},"end":{"line":499,"column":5}},"233":{"start":{"line":481,"column":6},"end":{"line":481,"column":100}},"234":{"start":{"line":481,"column":47},"end":{"line":481,"column":100}},"235":{"start":{"line":482,"column":15},"end":{"line":482,"column":48}},"236":{"start":{"line":483,"column":6},"end":{"line":485,"column":7}},"237":{"start":{"line":484,"column":8},"end":{"line":484,"column":61}},"238":{"start":{"line":486,"column":6},"end":{"line":497,"column":7}},"239":{"start":{"line":487,"column":8},"end":{"line":487,"column":24}},"240":{"start":{"line":489,"column":8},"end":{"line":495,"column":9}},"241":{"start":{"line":490,"column":10},"end":{"line":490,"column":25}},"242":{"start":{"line":491,"column":15},"end":{"line":495,"column":9}},"243":{"start":{"line":492,"column":10},"end":{"line":492,"column":26}},"244":{"start":{"line":493,"column":15},"end":{"line":495,"column":9}},"245":{"start":{"line":494,"column":10},"end":{"line":494,"column":16}},"246":{"start":{"line":496,"column":8},"end":{"line":496,"column":30}},"247":{"start":{"line":498,"column":6},"end":{"line":498,"column":23}},"248":{"start":{"line":500,"column":18},"end":{"line":500,"column":57}},"249":{"start":{"line":501,"column":4},"end":{"line":501,"column":21}},"250":{"start":{"line":504,"column":15},"end":{"line":504,"column":31}},"251":{"start":{"line":505,"column":4},"end":{"line":508,"column":5}},"252":{"start":{"line":506,"column":23},"end":{"line":506,"column":36}},"253":{"start":{"line":507,"column":6},"end":{"line":507,"column":87}},"254":{"start":{"line":507,"column":34},"end":{"line":507,"column":87}},"255":{"start":{"line":509,"column":4},"end":{"line":512,"column":7}},"256":{"start":{"line":520,"column":16},"end":{"line":520,"column":30}},"257":{"start":{"line":520,"column":40},"end":{"line":520,"column":41}},"258":{"start":{"line":521,"column":4},"end":{"line":535,"column":5}},"259":{"start":{"line":522,"column":17},"end":{"line":522,"column":54}},"260":{"start":{"line":523,"column":6},"end":{"line":531,"column":7}},"261":{"start":{"line":524,"column":8},"end":{"line":524,"column":29}},"262":{"start":{"line":525,"column":13},"end":{"line":531,"column":7}},"263":{"start":{"line":526,"column":8},"end":{"line":526,"column":29}},"264":{"start":{"line":527,"column":13},"end":{"line":531,"column":7}},"265":{"start":{"line":528,"column":8},"end":{"line":528,"column":24}},"266":{"start":{"line":530,"column":8},"end":{"line":530,"column":23}},"267":{"start":{"line":532,"column":6},"end":{"line":532,"column":30}},"268":{"start":{"line":532,"column":24},"end":{"line":532,"column":30}},"269":{"start":{"line":533,"column":6},"end":{"line":533,"column":23}},"270":{"start":{"line":534,"column":6},"end":{"line":534,"column":34}},"271":{"start":{"line":536,"column":4},"end":{"line":536,"column":95}},"272":{"start":{"line":536,"column":83},"end":{"line":536,"column":95}},"273":{"start":{"line":538,"column":4},"end":{"line":538,"column":17}},"274":{"start":{"line":542,"column":4},"end":{"line":542,"column":24}},"275":{"start":{"line":543,"column":14},"end":{"line":543,"column":33}},"276":{"start":{"line":544,"column":4},"end":{"line":544,"column":91}},"277":{"start":{"line":544,"column":21},"end":{"line":544,"column":91}},"278":{"start":{"line":545,"column":4},"end":{"line":545,"column":116}},"279":{"start":{"line":545,"column":53},"end":{"line":545,"column":116}},"280":{"start":{"line":546,"column":4},"end":{"line":546,"column":41}},"281":{"start":{"line":552,"column":16},"end":{"line":552,"column":30}},"282":{"start":{"line":552,"column":42},"end":{"line":552,"column":47}},"283":{"start":{"line":552,"column":57},"end":{"line":552,"column":101}},"284":{"start":{"line":553,"column":4},"end":{"line":553,"column":89}},"285":{"start":{"line":553,"column":53},"end":{"line":553,"column":89}},"286":{"start":{"line":554,"column":15},"end":{"line":554,"column":52}},"287":{"start":{"line":555,"column":4},"end":{"line":560,"column":5}},"288":{"start":{"line":556,"column":6},"end":{"line":556,"column":23}},"289":{"start":{"line":557,"column":6},"end":{"line":557,"column":23}},"290":{"start":{"line":558,"column":6},"end":{"line":558,"column":21}},"291":{"start":{"line":559,"column":6},"end":{"line":559,"column":51}},"292":{"start":{"line":561,"column":4},"end":{"line":566,"column":5}},"293":{"start":{"line":562,"column":6},"end":{"line":562,"column":53}},"294":{"start":{"line":563,"column":6},"end":{"line":563,"column":55}},"295":{"start":{"line":563,"column":38},"end":{"line":563,"column":55}},"296":{"start":{"line":564,"column":6},"end":{"line":564,"column":73}},"297":{"start":{"line":564,"column":37},"end":{"line":564,"column":73}},"298":{"start":{"line":565,"column":6},"end":{"line":565,"column":21}},"299":{"start":{"line":567,"column":4},"end":{"line":567,"column":116}},"300":{"start":{"line":567,"column":53},"end":{"line":567,"column":116}},"301":{"start":{"line":569,"column":14},"end":{"line":569,"column":53}},"302":{"start":{"line":570,"column":4},"end":{"line":578,"column":5}},"303":{"start":{"line":571,"column":6},"end":{"line":571,"column":28}},"304":{"start":{"line":572,"column":11},"end":{"line":578,"column":5}},"305":{"start":{"line":573,"column":6},"end":{"line":573,"column":30}},"306":{"start":{"line":574,"column":11},"end":{"line":578,"column":5}},"307":{"start":{"line":575,"column":6},"end":{"line":575,"column":42}},"308":{"start":{"line":577,"column":6},"end":{"line":577,"column":29}},"309":{"start":{"line":579,"column":4},"end":{"line":579,"column":41}},"310":{"start":{"line":585,"column":13},"end":{"line":585,"column":50}},"311":{"start":{"line":587,"column":4},"end":{"line":594,"column":5}},"312":{"start":{"line":588,"column":20},"end":{"line":588,"column":36}},"313":{"start":{"line":589,"column":6},"end":{"line":589,"column":88}},"314":{"start":{"line":590,"column":6},"end":{"line":590,"column":23}},"315":{"start":{"line":591,"column":6},"end":{"line":591,"column":75}},"316":{"start":{"line":591,"column":27},"end":{"line":591,"column":75}},"317":{"start":{"line":593,"column":6},"end":{"line":593,"column":33}},"318":{"start":{"line":595,"column":4},"end":{"line":595,"column":16}},"319":{"start":{"line":599,"column":14},"end":{"line":599,"column":16}},"320":{"start":{"line":599,"column":31},"end":{"line":599,"column":47}},"321":{"start":{"line":600,"column":4},"end":{"line":612,"column":5}},"322":{"start":{"line":601,"column":6},"end":{"line":601,"column":108}},"323":{"start":{"line":601,"column":47},"end":{"line":601,"column":108}},"324":{"start":{"line":602,"column":15},"end":{"line":602,"column":52}},"325":{"start":{"line":603,"column":6},"end":{"line":603,"column":30}},"326":{"start":{"line":603,"column":24},"end":{"line":603,"column":30}},"327":{"start":{"line":604,"column":6},"end":{"line":611,"column":7}},"328":{"start":{"line":605,"column":8},"end":{"line":605,"column":60}},"329":{"start":{"line":606,"column":8},"end":{"line":606,"column":43}},"330":{"start":{"line":607,"column":8},"end":{"line":607,"column":36}},"331":{"start":{"line":609,"column":8},"end":{"line":609,"column":88}},"332":{"start":{"line":609,"column":27},"end":{"line":609,"column":88}},"333":{"start":{"line":610,"column":8},"end":{"line":610,"column":25}},"334":{"start":{"line":613,"column":4},"end":{"line":613,"column":58}},"335":{"start":{"line":614,"column":4},"end":{"line":614,"column":44}},"336":{"start":{"line":620,"column":14},"end":{"line":620,"column":16}},"337":{"start":{"line":620,"column":31},"end":{"line":620,"column":45}},"338":{"start":{"line":621,"column":4},"end":{"line":660,"column":5}},"339":{"start":{"line":622,"column":6},"end":{"line":622,"column":101}},"340":{"start":{"line":622,"column":47},"end":{"line":622,"column":101}},"341":{"start":{"line":623,"column":15},"end":{"line":623,"column":52}},"342":{"start":{"line":624,"column":6},"end":{"line":636,"column":7}},"343":{"start":{"line":625,"column":8},"end":{"line":633,"column":9}},"344":{"start":{"line":626,"column":10},"end":{"line":632,"column":11}},"345":{"start":{"line":627,"column":12},"end":{"line":627,"column":32}},"346":{"start":{"line":628,"column":12},"end":{"line":628,"column":53}},"347":{"start":{"line":630,"column":12},"end":{"line":630,"column":29}},"348":{"start":{"line":631,"column":12},"end":{"line":631,"column":50}},"349":{"start":{"line":634,"column":8},"end":{"line":634,"column":60}},"350":{"start":{"line":635,"column":8},"end":{"line":635,"column":50}},"351":{"start":{"line":637,"column":6},"end":{"line":659,"column":7}},"352":{"start":{"line":638,"column":8},"end":{"line":638,"column":60}},"353":{"start":{"line":639,"column":8},"end":{"line":639,"column":42}},"354":{"start":{"line":640,"column":8},"end":{"line":640,"column":36}},"355":{"start":{"line":641,"column":13},"end":{"line":659,"column":7}},"356":{"start":{"line":642,"column":8},"end":{"line":642,"column":60}},"357":{"start":{"line":643,"column":8},"end":{"line":643,"column":25}},"358":{"start":{"line":644,"column":8},"end":{"line":653,"column":9}},"359":{"start":{"line":646,"column":12},"end":{"line":646,"column":79}},"360":{"start":{"line":646,"column":62},"end":{"line":646,"column":79}},"361":{"start":{"line":648,"column":12},"end":{"line":648,"column":24}},"362":{"start":{"line":649,"column":12},"end":{"line":649,"column":18}},"363":{"start":{"line":651,"column":12},"end":{"line":651,"column":43}},"364":{"start":{"line":652,"column":12},"end":{"line":652,"column":18}},"365":{"start":{"line":654,"column":8},"end":{"line":654,"column":29}},"366":{"start":{"line":655,"column":8},"end":{"line":655,"column":46}},"367":{"start":{"line":656,"column":8},"end":{"line":656,"column":36}},"368":{"start":{"line":658,"column":8},"end":{"line":658,"column":25}},"369":{"start":{"line":666,"column":13},"end":{"line":666,"column":52}},"370":{"start":{"line":667,"column":4},"end":{"line":667,"column":21}},"371":{"start":{"line":668,"column":4},"end":{"line":703,"column":5}},"372":{"start":{"line":669,"column":16},"end":{"line":669,"column":28}},"373":{"start":{"line":670,"column":16},"end":{"line":670,"column":28}},"374":{"start":{"line":671,"column":16},"end":{"line":671,"column":64}},"375":{"start":{"line":672,"column":16},"end":{"line":672,"column":63}},"376":{"start":{"line":673,"column":16},"end":{"line":673,"column":28}},"377":{"start":{"line":674,"column":15},"end":{"line":674,"column":27}},"378":{"start":{"line":675,"column":16},"end":{"line":675,"column":32}},"379":{"start":{"line":676,"column":16},"end":{"line":676,"column":28}},"380":{"start":{"line":677,"column":15},"end":{"line":677,"column":82}},"381":{"start":{"line":677,"column":65},"end":{"line":677,"column":82}},"382":{"start":{"line":679,"column":8},"end":{"line":679,"column":46}},"383":{"start":{"line":680,"column":8},"end":{"line":680,"column":29}},"384":{"start":{"line":681,"column":8},"end":{"line":681,"column":18}},"385":{"start":{"line":683,"column":8},"end":{"line":701,"column":9}},"386":{"start":{"line":684,"column":25},"end":{"line":684,"column":85}},"387":{"start":{"line":685,"column":22},"end":{"line":685,"column":43}},"388":{"start":{"line":686,"column":10},"end":{"line":689,"column":11}},"389":{"start":{"line":687,"column":12},"end":{"line":687,"column":45}},"390":{"start":{"line":688,"column":12},"end":{"line":688,"column":42}},"391":{"start":{"line":690,"column":10},"end":{"line":698,"column":11}},"392":{"start":{"line":691,"column":12},"end":{"line":694,"column":13}},"393":{"start":{"line":692,"column":14},"end":{"line":692,"column":46}},"394":{"start":{"line":693,"column":14},"end":{"line":693,"column":60}},"395":{"start":{"line":695,"column":12},"end":{"line":697,"column":13}},"396":{"start":{"line":696,"column":14},"end":{"line":696,"column":77}},"397":{"start":{"line":699,"column":10},"end":{"line":699,"column":48}},"398":{"start":{"line":700,"column":10},"end":{"line":700,"column":44}},"399":{"start":{"line":702,"column":8},"end":{"line":702,"column":39}},"400":{"start":{"line":709,"column":18},"end":{"line":709,"column":32}},"401":{"start":{"line":710,"column":12},"end":{"line":710,"column":33}},"402":{"start":{"line":711,"column":4},"end":{"line":711,"column":73}},"403":{"start":{"line":711,"column":20},"end":{"line":711,"column":73}},"404":{"start":{"line":712,"column":4},"end":{"line":712,"column":13}},"405":{"start":{"line":722,"column":4},"end":{"line":722,"column":35}},"406":{"start":{"line":723,"column":15},"end":{"line":723,"column":17}},"407":{"start":{"line":723,"column":27},"end":{"line":723,"column":31}},"408":{"start":{"line":723,"column":46},"end":{"line":723,"column":60}},"409":{"start":{"line":724,"column":4},"end":{"line":750,"column":5}},"410":{"start":{"line":725,"column":15},"end":{"line":725,"column":39}},"411":{"start":{"line":726,"column":6},"end":{"line":748,"column":7}},"412":{"start":{"line":727,"column":8},"end":{"line":727,"column":47}},"413":{"start":{"line":728,"column":13},"end":{"line":748,"column":7}},"414":{"start":{"line":729,"column":8},"end":{"line":729,"column":38}},"415":{"start":{"line":731,"column":8},"end":{"line":731,"column":61}},"416":{"start":{"line":732,"column":23},"end":{"line":732,"column":37}},"417":{"start":{"line":734,"column":8},"end":{"line":736,"column":9}},"418":{"start":{"line":735,"column":10},"end":{"line":735,"column":82}},"419":{"start":{"line":738,"column":8},"end":{"line":738,"column":25}},"420":{"start":{"line":739,"column":18},"end":{"line":739,"column":38}},"421":{"start":{"line":740,"column":8},"end":{"line":742,"column":9}},"422":{"start":{"line":741,"column":10},"end":{"line":741,"column":57}},"423":{"start":{"line":744,"column":8},"end":{"line":744,"column":39}},"424":{"start":{"line":745,"column":8},"end":{"line":745,"column":36}},"425":{"start":{"line":747,"column":8},"end":{"line":747,"column":14}},"426":{"start":{"line":749,"column":6},"end":{"line":749,"column":20}},"427":{"start":{"line":751,"column":4},"end":{"line":751,"column":63}},"428":{"start":{"line":758,"column":15},"end":{"line":758,"column":31}},"429":{"start":{"line":759,"column":15},"end":{"line":759,"column":22}},"430":{"start":{"line":760,"column":4},"end":{"line":762,"column":5}},"431":{"start":{"line":761,"column":6},"end":{"line":761,"column":32}},"432":{"start":{"line":763,"column":4},"end":{"line":763,"column":40}},"433":{"start":{"line":767,"column":4},"end":{"line":772,"column":5}},"434":{"start":{"line":768,"column":19},"end":{"line":768,"column":36}},"435":{"start":{"line":769,"column":6},"end":{"line":771,"column":7}},"436":{"start":{"line":770,"column":8},"end":{"line":770,"column":30}},"437":{"start":{"line":774,"column":4},"end":{"line":776,"column":5}},"438":{"start":{"line":775,"column":6},"end":{"line":775,"column":87}},"439":{"start":{"line":778,"column":4},"end":{"line":780,"column":5}},"440":{"start":{"line":779,"column":6},"end":{"line":779,"column":18}},"441":{"start":{"line":782,"column":4},"end":{"line":784,"column":5}},"442":{"start":{"line":783,"column":6},"end":{"line":783,"column":53}},"443":{"start":{"line":786,"column":4},"end":{"line":786,"column":35}},"444":{"start":{"line":790,"column":23},"end":{"line":790,"column":38}},"445":{"start":{"line":791,"column":4},"end":{"line":797,"column":5}},"446":{"start":{"line":792,"column":6},"end":{"line":792,"column":37}},"447":{"start":{"line":793,"column":11},"end":{"line":797,"column":5}},"448":{"start":{"line":794,"column":6},"end":{"line":794,"column":34}},"449":{"start":{"line":796,"column":6},"end":{"line":796,"column":47}}},"fnMap":{"1":{"name":"(anonymous_1)","decl":{"start":{"line":17,"column":2},"end":{"line":17,"column":3}},"loc":{"start":{"line":17,"column":21},"end":{"line":23,"column":3}}},"2":{"name":"codePointToString","decl":{"start":{"line":34,"column":9},"end":{"line":34,"column":26}},"loc":{"start":{"line":34,"column":33},"end":{"line":41,"column":1}}},"3":{"name":"(anonymous_3)","decl":{"start":{"line":44,"column":2},"end":{"line":44,"column":3}},"loc":{"start":{"line":44,"column":30},"end":{"line":47,"column":3}}},"4":{"name":"(anonymous_4)","decl":{"start":{"line":51,"column":2},"end":{"line":51,"column":3}},"loc":{"start":{"line":51,"column":9},"end":{"line":61,"column":3}}},"5":{"name":"(anonymous_5)","decl":{"start":{"line":65,"column":2},"end":{"line":65,"column":3}},"loc":{"start":{"line":65,"column":12},"end":{"line":72,"column":3}}},"6":{"name":"(anonymous_6)","decl":{"start":{"line":76,"column":2},"end":{"line":76,"column":3}},"loc":{"start":{"line":76,"column":14},"end":{"line":78,"column":3}}},"7":{"name":"(anonymous_7)","decl":{"start":{"line":82,"column":2},"end":{"line":82,"column":3}},"loc":{"start":{"line":82,"column":18},"end":{"line":84,"column":3}}},"8":{"name":"(anonymous_8)","decl":{"start":{"line":88,"column":2},"end":{"line":88,"column":3}},"loc":{"start":{"line":88,"column":14},"end":{"line":99,"column":3}}},"9":{"name":"(anonymous_9)","decl":{"start":{"line":104,"column":2},"end":{"line":104,"column":3}},"loc":{"start":{"line":104,"column":20},"end":{"line":113,"column":3}}},"10":{"name":"(anonymous_10)","decl":{"start":{"line":115,"column":2},"end":{"line":115,"column":3}},"loc":{"start":{"line":115,"column":15},"end":{"line":117,"column":3}}},"11":{"name":"(anonymous_11)","decl":{"start":{"line":122,"column":2},"end":{"line":122,"column":3}},"loc":{"start":{"line":122,"column":14},"end":{"line":137,"column":3}}},"12":{"name":"(anonymous_12)","decl":{"start":{"line":139,"column":2},"end":{"line":139,"column":3}},"loc":{"start":{"line":139,"column":18},"end":{"line":147,"column":3}}},"13":{"name":"(anonymous_13)","decl":{"start":{"line":149,"column":2},"end":{"line":149,"column":3}},"loc":{"start":{"line":149,"column":22},"end":{"line":155,"column":3}}},"14":{"name":"(anonymous_14)","decl":{"start":{"line":157,"column":2},"end":{"line":157,"column":3}},"loc":{"start":{"line":157,"column":57},"end":{"line":171,"column":3}}},"15":{"name":"(anonymous_15)","decl":{"start":{"line":173,"column":2},"end":{"line":173,"column":3}},"loc":{"start":{"line":173,"column":21},"end":{"line":187,"column":3}}},"16":{"name":"(anonymous_16)","decl":{"start":{"line":189,"column":2},"end":{"line":189,"column":3}},"loc":{"start":{"line":189,"column":29},"end":{"line":199,"column":3}}},"17":{"name":"(anonymous_17)","decl":{"start":{"line":204,"column":2},"end":{"line":204,"column":3}},"loc":{"start":{"line":204,"column":14},"end":{"line":246,"column":3}}},"18":{"name":"(anonymous_18)","decl":{"start":{"line":253,"column":2},"end":{"line":253,"column":3}},"loc":{"start":{"line":253,"column":25},"end":{"line":261,"column":3}}},"19":{"name":"(anonymous_19)","decl":{"start":{"line":272,"column":2},"end":{"line":272,"column":3}},"loc":{"start":{"line":272,"column":18},"end":{"line":286,"column":3}}},"20":{"name":"(anonymous_20)","decl":{"start":{"line":288,"column":2},"end":{"line":288,"column":3}},"loc":{"start":{"line":288,"column":20},"end":{"line":300,"column":3}}},"21":{"name":"(anonymous_21)","decl":{"start":{"line":302,"column":2},"end":{"line":302,"column":3}},"loc":{"start":{"line":302,"column":30},"end":{"line":319,"column":3}}},"22":{"name":"(anonymous_22)","decl":{"start":{"line":321,"column":2},"end":{"line":321,"column":3}},"loc":{"start":{"line":321,"column":27},"end":{"line":326,"column":3}}},"23":{"name":"(anonymous_23)","decl":{"start":{"line":328,"column":2},"end":{"line":328,"column":3}},"loc":{"start":{"line":328,"column":20},"end":{"line":335,"column":3}}},"24":{"name":"(anonymous_24)","decl":{"start":{"line":337,"column":2},"end":{"line":337,"column":3}},"loc":{"start":{"line":337,"column":27},"end":{"line":355,"column":3}}},"25":{"name":"(anonymous_25)","decl":{"start":{"line":357,"column":2},"end":{"line":357,"column":3}},"loc":{"start":{"line":357,"column":24},"end":{"line":381,"column":3}}},"26":{"name":"(anonymous_26)","decl":{"start":{"line":383,"column":2},"end":{"line":383,"column":3}},"loc":{"start":{"line":383,"column":26},"end":{"line":391,"column":3}}},"27":{"name":"(anonymous_27)","decl":{"start":{"line":393,"column":2},"end":{"line":393,"column":3}},"loc":{"start":{"line":393,"column":25},"end":{"line":470,"column":3}}},"28":{"name":"(anonymous_28)","decl":{"start":{"line":472,"column":2},"end":{"line":472,"column":3}},"loc":{"start":{"line":472,"column":23},"end":{"line":476,"column":3}}},"29":{"name":"(anonymous_29)","decl":{"start":{"line":478,"column":2},"end":{"line":478,"column":3}},"loc":{"start":{"line":478,"column":15},"end":{"line":513,"column":3}}},"30":{"name":"(anonymous_30)","decl":{"start":{"line":519,"column":2},"end":{"line":519,"column":3}},"loc":{"start":{"line":519,"column":22},"end":{"line":539,"column":3}}},"31":{"name":"(anonymous_31)","decl":{"start":{"line":541,"column":2},"end":{"line":541,"column":3}},"loc":{"start":{"line":541,"column":25},"end":{"line":547,"column":3}}},"32":{"name":"(anonymous_32)","decl":{"start":{"line":551,"column":2},"end":{"line":551,"column":3}},"loc":{"start":{"line":551,"column":28},"end":{"line":580,"column":3}}},"33":{"name":"(anonymous_33)","decl":{"start":{"line":584,"column":2},"end":{"line":584,"column":3}},"loc":{"start":{"line":584,"column":18},"end":{"line":596,"column":3}}},"34":{"name":"(anonymous_34)","decl":{"start":{"line":598,"column":2},"end":{"line":598,"column":3}},"loc":{"start":{"line":598,"column":20},"end":{"line":615,"column":3}}},"35":{"name":"(anonymous_35)","decl":{"start":{"line":619,"column":2},"end":{"line":619,"column":3}},"loc":{"start":{"line":619,"column":18},"end":{"line":661,"column":3}}},"36":{"name":"(anonymous_36)","decl":{"start":{"line":665,"column":2},"end":{"line":665,"column":3}},"loc":{"start":{"line":665,"column":30},"end":{"line":704,"column":3}}},"37":{"name":"(anonymous_37)","decl":{"start":{"line":708,"column":2},"end":{"line":708,"column":3}},"loc":{"start":{"line":708,"column":19},"end":{"line":713,"column":3}}},"38":{"name":"(anonymous_38)","decl":{"start":{"line":721,"column":2},"end":{"line":721,"column":3}},"loc":{"start":{"line":721,"column":14},"end":{"line":752,"column":3}}},"39":{"name":"(anonymous_39)","decl":{"start":{"line":757,"column":2},"end":{"line":757,"column":3}},"loc":{"start":{"line":757,"column":13},"end":{"line":764,"column":3}}},"40":{"name":"(anonymous_40)","decl":{"start":{"line":766,"column":2},"end":{"line":766,"column":3}},"loc":{"start":{"line":766,"column":25},"end":{"line":787,"column":3}}},"41":{"name":"(anonymous_41)","decl":{"start":{"line":789,"column":2},"end":{"line":789,"column":3}},"loc":{"start":{"line":789,"column":26},"end":{"line":798,"column":3}}}},"branchMap":{"1":{"loc":{"start":{"line":36,"column":2},"end":{"line":40,"column":3}},"type":"if","locations":[{"start":{"line":36,"column":2},"end":{"line":40,"column":3}},{"start":{"line":36,"column":2},"end":{"line":40,"column":3}}]},"2":{"loc":{"start":{"line":52,"column":4},"end":{"line":54,"column":5}},"type":"if","locations":[{"start":{"line":52,"column":4},"end":{"line":54,"column":5}},{"start":{"line":52,"column":4},"end":{"line":54,"column":5}}]},"3":{"loc":{"start":{"line":66,"column":4},"end":{"line":71,"column":5}},"type":"if","locations":[{"start":{"line":66,"column":4},"end":{"line":71,"column":5}},{"start":{"line":66,"column":4},"end":{"line":71,"column":5}}]},"4":{"loc":{"start":{"line":106,"column":4},"end":{"line":106,"column":62}},"type":"if","locations":[{"start":{"line":106,"column":4},"end":{"line":106,"column":62}},{"start":{"line":106,"column":4},"end":{"line":106,"column":62}}]},"5":{"loc":{"start":{"line":106,"column":8},"end":{"line":106,"column":53}},"type":"binary-expr","locations":[{"start":{"line":106,"column":8},"end":{"line":106,"column":27}},{"start":{"line":106,"column":31},"end":{"line":106,"column":53}}]},"6":{"loc":{"start":{"line":124,"column":4},"end":{"line":124,"column":67}},"type":"if","locations":[{"start":{"line":124,"column":4},"end":{"line":124,"column":67}},{"start":{"line":124,"column":4},"end":{"line":124,"column":67}}]},"7":{"loc":{"start":{"line":124,"column":8},"end":{"line":124,"column":48}},"type":"binary-expr","locations":[{"start":{"line":124,"column":8},"end":{"line":124,"column":19}},{"start":{"line":124,"column":23},"end":{"line":124,"column":48}}]},"8":{"loc":{"start":{"line":130,"column":4},"end":{"line":130,"column":77}},"type":"if","locations":[{"start":{"line":130,"column":4},"end":{"line":130,"column":77}},{"start":{"line":130,"column":4},"end":{"line":130,"column":77}}]},"9":{"loc":{"start":{"line":132,"column":4},"end":{"line":136,"column":5}},"type":"if","locations":[{"start":{"line":132,"column":4},"end":{"line":136,"column":5}},{"start":{"line":132,"column":4},"end":{"line":136,"column":5}}]},"10":{"loc":{"start":{"line":142,"column":4},"end":{"line":146,"column":5}},"type":"if","locations":[{"start":{"line":142,"column":4},"end":{"line":146,"column":5}},{"start":{"line":142,"column":4},"end":{"line":146,"column":5}}]},"11":{"loc":{"start":{"line":142,"column":8},"end":{"line":142,"column":46}},"type":"binary-expr","locations":[{"start":{"line":142,"column":8},"end":{"line":142,"column":31}},{"start":{"line":142,"column":35},"end":{"line":142,"column":46}}]},"12":{"loc":{"start":{"line":151,"column":4},"end":{"line":151,"column":54}},"type":"if","locations":[{"start":{"line":151,"column":4},"end":{"line":151,"column":54}},{"start":{"line":151,"column":4},"end":{"line":151,"column":54}}]},"13":{"loc":{"start":{"line":151,"column":8},"end":{"line":151,"column":40}},"type":"binary-expr","locations":[{"start":{"line":151,"column":8},"end":{"line":151,"column":22}},{"start":{"line":151,"column":26},"end":{"line":151,"column":40}}]},"14":{"loc":{"start":{"line":159,"column":12},"end":{"line":159,"column":50}},"type":"cond-expr","locations":[{"start":{"line":159,"column":20},"end":{"line":159,"column":34}},{"start":{"line":159,"column":37},"end":{"line":159,"column":50}}]},"15":{"loc":{"start":{"line":166,"column":4},"end":{"line":170,"column":5}},"type":"if","locations":[{"start":{"line":166,"column":4},"end":{"line":170,"column":5}},{"start":{"line":166,"column":4},"end":{"line":170,"column":5}}]},"16":{"loc":{"start":{"line":176,"column":4},"end":{"line":176,"column":75}},"type":"if","locations":[{"start":{"line":176,"column":4},"end":{"line":176,"column":75}},{"start":{"line":176,"column":4},"end":{"line":176,"column":75}}]},"17":{"loc":{"start":{"line":181,"column":11},"end":{"line":181,"column":80}},"type":"binary-expr","locations":[{"start":{"line":181,"column":12},"end":{"line":181,"column":47}},{"start":{"line":181,"column":52},"end":{"line":181,"column":80}}]},"18":{"loc":{"start":{"line":193,"column":11},"end":{"line":193,"column":101}},"type":"binary-expr","locations":[{"start":{"line":193,"column":11},"end":{"line":193,"column":45}},{"start":{"line":193,"column":49},"end":{"line":193,"column":58}},{"start":{"line":193,"column":62},"end":{"line":193,"column":71}},{"start":{"line":193,"column":75},"end":{"line":193,"column":86}},{"start":{"line":193,"column":90},"end":{"line":193,"column":101}}]},"19":{"loc":{"start":{"line":207,"column":6},"end":{"line":244,"column":7}},"type":"switch","locations":[{"start":{"line":208,"column":8},"end":{"line":208,"column":16}},{"start":{"line":208,"column":17},"end":{"line":210,"column":16}},{"start":{"line":212,"column":8},"end":{"line":215,"column":11}},{"start":{"line":217,"column":8},"end":{"line":217,"column":16}},{"start":{"line":217,"column":17},"end":{"line":217,"column":27}},{"start":{"line":217,"column":28},"end":{"line":221,"column":16}},{"start":{"line":223,"column":8},"end":{"line":236,"column":16}},{"start":{"line":238,"column":8},"end":{"line":243,"column":11}}]},"20":{"loc":{"start":{"line":213,"column":10},"end":{"line":215,"column":11}},"type":"if","locations":[{"start":{"line":213,"column":10},"end":{"line":215,"column":11}},{"start":{"line":213,"column":10},"end":{"line":215,"column":11}}]},"21":{"loc":{"start":{"line":224,"column":10},"end":{"line":235,"column":11}},"type":"switch","locations":[{"start":{"line":225,"column":12},"end":{"line":227,"column":20}},{"start":{"line":229,"column":12},"end":{"line":231,"column":20}},{"start":{"line":233,"column":12},"end":{"line":234,"column":25}}]},"22":{"loc":{"start":{"line":239,"column":10},"end":{"line":243,"column":11}},"type":"if","locations":[{"start":{"line":239,"column":10},"end":{"line":243,"column":11}},{"start":{"line":239,"column":10},"end":{"line":243,"column":11}}]},"23":{"loc":{"start":{"line":239,"column":14},"end":{"line":239,"column":97}},"type":"binary-expr","locations":[{"start":{"line":239,"column":14},"end":{"line":239,"column":20}},{"start":{"line":239,"column":24},"end":{"line":239,"column":31}},{"start":{"line":239,"column":35},"end":{"line":239,"column":45}},{"start":{"line":239,"column":49},"end":{"line":239,"column":97}}]},"24":{"loc":{"start":{"line":274,"column":4},"end":{"line":276,"column":5}},"type":"if","locations":[{"start":{"line":274,"column":4},"end":{"line":276,"column":5}},{"start":{"line":274,"column":4},"end":{"line":276,"column":5}}]},"25":{"loc":{"start":{"line":274,"column":8},"end":{"line":274,"column":32}},"type":"binary-expr","locations":[{"start":{"line":274,"column":8},"end":{"line":274,"column":18}},{"start":{"line":274,"column":22},"end":{"line":274,"column":32}}]},"26":{"loc":{"start":{"line":279,"column":4},"end":{"line":285,"column":5}},"type":"if","locations":[{"start":{"line":279,"column":4},"end":{"line":285,"column":5}},{"start":{"line":279,"column":4},"end":{"line":285,"column":5}}]},"27":{"loc":{"start":{"line":279,"column":8},"end":{"line":279,"column":35}},"type":"binary-expr","locations":[{"start":{"line":279,"column":8},"end":{"line":279,"column":19}},{"start":{"line":279,"column":23},"end":{"line":279,"column":35}}]},"28":{"loc":{"start":{"line":289,"column":4},"end":{"line":292,"column":5}},"type":"if","locations":[{"start":{"line":289,"column":4},"end":{"line":292,"column":5}},{"start":{"line":289,"column":4},"end":{"line":292,"column":5}}]},"29":{"loc":{"start":{"line":295,"column":4},"end":{"line":299,"column":5}},"type":"if","locations":[{"start":{"line":295,"column":4},"end":{"line":299,"column":5}},{"start":{"line":295,"column":4},"end":{"line":299,"column":5}}]},"30":{"loc":{"start":{"line":303,"column":15},"end":{"line":303,"column":48}},"type":"cond-expr","locations":[{"start":{"line":303,"column":29},"end":{"line":303,"column":36}},{"start":{"line":303,"column":39},"end":{"line":303,"column":48}}]},"31":{"loc":{"start":{"line":307,"column":4},"end":{"line":311,"column":5}},"type":"if","locations":[{"start":{"line":307,"column":4},"end":{"line":311,"column":5}},{"start":{"line":307,"column":4},"end":{"line":311,"column":5}}]},"32":{"loc":{"start":{"line":313,"column":4},"end":{"line":316,"column":5}},"type":"if","locations":[{"start":{"line":313,"column":4},"end":{"line":316,"column":5}},{"start":{"line":313,"column":4},"end":{"line":316,"column":5}}]},"33":{"loc":{"start":{"line":323,"column":4},"end":{"line":323,"column":92}},"type":"if","locations":[{"start":{"line":323,"column":4},"end":{"line":323,"column":92}},{"start":{"line":323,"column":4},"end":{"line":323,"column":92}}]},"34":{"loc":{"start":{"line":323,"column":44},"end":{"line":323,"column":87}},"type":"cond-expr","locations":[{"start":{"line":323,"column":59},"end":{"line":323,"column":71}},{"start":{"line":323,"column":74},"end":{"line":323,"column":87}}]},"35":{"loc":{"start":{"line":324,"column":4},"end":{"line":324,"column":56}},"type":"if","locations":[{"start":{"line":324,"column":4},"end":{"line":324,"column":56}},{"start":{"line":324,"column":4},"end":{"line":324,"column":56}}]},"36":{"loc":{"start":{"line":325,"column":25},"end":{"line":325,"column":68}},"type":"cond-expr","locations":[{"start":{"line":325,"column":40},"end":{"line":325,"column":52}},{"start":{"line":325,"column":55},"end":{"line":325,"column":68}}]},"37":{"loc":{"start":{"line":330,"column":4},"end":{"line":334,"column":5}},"type":"if","locations":[{"start":{"line":330,"column":4},"end":{"line":334,"column":5}},{"start":{"line":330,"column":4},"end":{"line":334,"column":5}}]},"38":{"loc":{"start":{"line":340,"column":4},"end":{"line":348,"column":5}},"type":"if","locations":[{"start":{"line":340,"column":4},"end":{"line":348,"column":5}},{"start":{"line":340,"column":4},"end":{"line":348,"column":5}}]},"39":{"loc":{"start":{"line":341,"column":6},"end":{"line":346,"column":7}},"type":"if","locations":[{"start":{"line":341,"column":6},"end":{"line":346,"column":7}},{"start":{"line":341,"column":6},"end":{"line":346,"column":7}}]},"40":{"loc":{"start":{"line":341,"column":10},"end":{"line":341,"column":148}},"type":"binary-expr","locations":[{"start":{"line":341,"column":10},"end":{"line":341,"column":21}},{"start":{"line":341,"column":25},"end":{"line":341,"column":73}},{"start":{"line":341,"column":77},"end":{"line":341,"column":148}}]},"41":{"loc":{"start":{"line":350,"column":4},"end":{"line":354,"column":5}},"type":"if","locations":[{"start":{"line":350,"column":4},"end":{"line":354,"column":5}},{"start":{"line":350,"column":4},"end":{"line":354,"column":5}}]},"42":{"loc":{"start":{"line":361,"column":4},"end":{"line":365,"column":5}},"type":"if","locations":[{"start":{"line":361,"column":4},"end":{"line":365,"column":5}},{"start":{"line":361,"column":4},"end":{"line":365,"column":5}}]},"43":{"loc":{"start":{"line":362,"column":13},"end":{"line":362,"column":84}},"type":"cond-expr","locations":[{"start":{"line":362,"column":79},"end":{"line":362,"column":80}},{"start":{"line":362,"column":83},"end":{"line":362,"column":84}}]},"44":{"loc":{"start":{"line":362,"column":13},"end":{"line":362,"column":76}},"type":"binary-expr","locations":[{"start":{"line":362,"column":13},"end":{"line":362,"column":24}},{"start":{"line":362,"column":28},"end":{"line":362,"column":76}}]},"45":{"loc":{"start":{"line":363,"column":6},"end":{"line":363,"column":105}},"type":"if","locations":[{"start":{"line":363,"column":6},"end":{"line":363,"column":105}},{"start":{"line":363,"column":6},"end":{"line":363,"column":105}}]},"46":{"loc":{"start":{"line":367,"column":4},"end":{"line":373,"column":5}},"type":"if","locations":[{"start":{"line":367,"column":4},"end":{"line":373,"column":5}},{"start":{"line":367,"column":4},"end":{"line":373,"column":5}}]},"47":{"loc":{"start":{"line":367,"column":8},"end":{"line":367,"column":138}},"type":"binary-expr","locations":[{"start":{"line":367,"column":8},"end":{"line":367,"column":19}},{"start":{"line":367,"column":23},"end":{"line":367,"column":34}},{"start":{"line":367,"column":38},"end":{"line":367,"column":86}},{"start":{"line":367,"column":90},"end":{"line":367,"column":138}}]},"48":{"loc":{"start":{"line":368,"column":6},"end":{"line":368,"column":43}},"type":"if","locations":[{"start":{"line":368,"column":6},"end":{"line":368,"column":43}},{"start":{"line":368,"column":6},"end":{"line":368,"column":43}}]},"49":{"loc":{"start":{"line":375,"column":4},"end":{"line":378,"column":5}},"type":"if","locations":[{"start":{"line":375,"column":4},"end":{"line":378,"column":5}},{"start":{"line":375,"column":4},"end":{"line":378,"column":5}}]},"50":{"loc":{"start":{"line":385,"column":4},"end":{"line":385,"column":113}},"type":"if","locations":[{"start":{"line":385,"column":4},"end":{"line":385,"column":113}},{"start":{"line":385,"column":4},"end":{"line":385,"column":113}}]},"51":{"loc":{"start":{"line":385,"column":55},"end":{"line":385,"column":111}},"type":"cond-expr","locations":[{"start":{"line":385,"column":106},"end":{"line":385,"column":107}},{"start":{"line":385,"column":110},"end":{"line":385,"column":111}}]},"52":{"loc":{"start":{"line":386,"column":4},"end":{"line":389,"column":5}},"type":"if","locations":[{"start":{"line":386,"column":4},"end":{"line":389,"column":5}},{"start":{"line":386,"column":4},"end":{"line":389,"column":5}}]},"53":{"loc":{"start":{"line":386,"column":8},"end":{"line":386,"column":34}},"type":"binary-expr","locations":[{"start":{"line":386,"column":8},"end":{"line":386,"column":19}},{"start":{"line":386,"column":23},"end":{"line":386,"column":34}}]},"54":{"loc":{"start":{"line":390,"column":25},"end":{"line":390,"column":56}},"type":"cond-expr","locations":[{"start":{"line":390,"column":39},"end":{"line":390,"column":44}},{"start":{"line":390,"column":47},"end":{"line":390,"column":56}}]},"55":{"loc":{"start":{"line":394,"column":4},"end":{"line":467,"column":5}},"type":"switch","locations":[{"start":{"line":397,"column":6},"end":{"line":398,"column":36}},{"start":{"line":401,"column":6},"end":{"line":401,"column":68}},{"start":{"line":402,"column":6},"end":{"line":402,"column":68}},{"start":{"line":403,"column":6},"end":{"line":403,"column":66}},{"start":{"line":404,"column":6},"end":{"line":404,"column":67}},{"start":{"line":405,"column":6},"end":{"line":405,"column":70}},{"start":{"line":406,"column":6},"end":{"line":406,"column":70}},{"start":{"line":407,"column":6},"end":{"line":407,"column":69}},{"start":{"line":408,"column":6},"end":{"line":408,"column":69}},{"start":{"line":410,"column":6},"end":{"line":416,"column":9}},{"start":{"line":418,"column":6},"end":{"line":418,"column":70}},{"start":{"line":419,"column":6},"end":{"line":419,"column":64}},{"start":{"line":421,"column":6},"end":{"line":423,"column":46}},{"start":{"line":425,"column":6},"end":{"line":429,"column":71}},{"start":{"line":432,"column":6},"end":{"line":432,"column":14}},{"start":{"line":432,"column":15},"end":{"line":432,"column":23}},{"start":{"line":432,"column":24},"end":{"line":432,"column":32}},{"start":{"line":432,"column":33},"end":{"line":432,"column":41}},{"start":{"line":432,"column":42},"end":{"line":432,"column":50}},{"start":{"line":432,"column":51},"end":{"line":432,"column":59}},{"start":{"line":432,"column":60},"end":{"line":432,"column":68}},{"start":{"line":432,"column":69},"end":{"line":432,"column":77}},{"start":{"line":432,"column":78},"end":{"line":433,"column":38}},{"start":{"line":436,"column":6},"end":{"line":436,"column":14}},{"start":{"line":436,"column":15},"end":{"line":437,"column":37}},{"start":{"line":444,"column":6},"end":{"line":445,"column":38}},{"start":{"line":447,"column":6},"end":{"line":447,"column":14}},{"start":{"line":447,"column":15},"end":{"line":448,"column":48}},{"start":{"line":450,"column":6},"end":{"line":450,"column":15}},{"start":{"line":450,"column":16},"end":{"line":451,"column":45}},{"start":{"line":453,"column":6},"end":{"line":454,"column":38}},{"start":{"line":456,"column":6},"end":{"line":456,"column":14}},{"start":{"line":456,"column":15},"end":{"line":457,"column":45}},{"start":{"line":459,"column":6},"end":{"line":459,"column":14}},{"start":{"line":459,"column":15},"end":{"line":460,"column":42}},{"start":{"line":462,"column":6},"end":{"line":462,"column":14}},{"start":{"line":462,"column":15},"end":{"line":463,"column":44}},{"start":{"line":465,"column":6},"end":{"line":466,"column":43}}]},"56":{"loc":{"start":{"line":411,"column":8},"end":{"line":416,"column":9}},"type":"if","locations":[{"start":{"line":411,"column":8},"end":{"line":416,"column":9}},{"start":{"line":411,"column":8},"end":{"line":416,"column":9}}]},"57":{"loc":{"start":{"line":411,"column":12},"end":{"line":411,"column":94}},"type":"binary-expr","locations":[{"start":{"line":411,"column":12},"end":{"line":411,"column":42}},{"start":{"line":411,"column":46},"end":{"line":411,"column":94}}]},"58":{"loc":{"start":{"line":427,"column":8},"end":{"line":427,"column":73}},"type":"if","locations":[{"start":{"line":427,"column":8},"end":{"line":427,"column":73}},{"start":{"line":427,"column":8},"end":{"line":427,"column":73}}]},"59":{"loc":{"start":{"line":427,"column":12},"end":{"line":427,"column":39}},"type":"binary-expr","locations":[{"start":{"line":427,"column":12},"end":{"line":427,"column":24}},{"start":{"line":427,"column":28},"end":{"line":427,"column":39}}]},"60":{"loc":{"start":{"line":428,"column":8},"end":{"line":428,"column":72}},"type":"if","locations":[{"start":{"line":428,"column":8},"end":{"line":428,"column":72}},{"start":{"line":428,"column":8},"end":{"line":428,"column":72}}]},"61":{"loc":{"start":{"line":428,"column":12},"end":{"line":428,"column":39}},"type":"binary-expr","locations":[{"start":{"line":428,"column":12},"end":{"line":428,"column":24}},{"start":{"line":428,"column":28},"end":{"line":428,"column":39}}]},"62":{"loc":{"start":{"line":429,"column":8},"end":{"line":429,"column":71}},"type":"if","locations":[{"start":{"line":429,"column":8},"end":{"line":429,"column":71}},{"start":{"line":429,"column":8},"end":{"line":429,"column":71}}]},"63":{"loc":{"start":{"line":429,"column":12},"end":{"line":429,"column":38}},"type":"binary-expr","locations":[{"start":{"line":429,"column":12},"end":{"line":429,"column":23}},{"start":{"line":429,"column":27},"end":{"line":429,"column":38}}]},"64":{"loc":{"start":{"line":481,"column":6},"end":{"line":481,"column":100}},"type":"if","locations":[{"start":{"line":481,"column":6},"end":{"line":481,"column":100}},{"start":{"line":481,"column":6},"end":{"line":481,"column":100}}]},"65":{"loc":{"start":{"line":483,"column":6},"end":{"line":485,"column":7}},"type":"if","locations":[{"start":{"line":483,"column":6},"end":{"line":485,"column":7}},{"start":{"line":483,"column":6},"end":{"line":485,"column":7}}]},"66":{"loc":{"start":{"line":486,"column":6},"end":{"line":497,"column":7}},"type":"if","locations":[{"start":{"line":486,"column":6},"end":{"line":497,"column":7}},{"start":{"line":486,"column":6},"end":{"line":497,"column":7}}]},"67":{"loc":{"start":{"line":489,"column":8},"end":{"line":495,"column":9}},"type":"if","locations":[{"start":{"line":489,"column":8},"end":{"line":495,"column":9}},{"start":{"line":489,"column":8},"end":{"line":495,"column":9}}]},"68":{"loc":{"start":{"line":491,"column":15},"end":{"line":495,"column":9}},"type":"if","locations":[{"start":{"line":491,"column":15},"end":{"line":495,"column":9}},{"start":{"line":491,"column":15},"end":{"line":495,"column":9}}]},"69":{"loc":{"start":{"line":491,"column":19},"end":{"line":491,"column":40}},"type":"binary-expr","locations":[{"start":{"line":491,"column":19},"end":{"line":491,"column":29}},{"start":{"line":491,"column":33},"end":{"line":491,"column":40}}]},"70":{"loc":{"start":{"line":493,"column":15},"end":{"line":495,"column":9}},"type":"if","locations":[{"start":{"line":493,"column":15},"end":{"line":495,"column":9}},{"start":{"line":493,"column":15},"end":{"line":495,"column":9}}]},"71":{"loc":{"start":{"line":493,"column":19},"end":{"line":493,"column":41}},"type":"binary-expr","locations":[{"start":{"line":493,"column":19},"end":{"line":493,"column":29}},{"start":{"line":493,"column":33},"end":{"line":493,"column":41}}]},"72":{"loc":{"start":{"line":505,"column":4},"end":{"line":508,"column":5}},"type":"if","locations":[{"start":{"line":505,"column":4},"end":{"line":508,"column":5}},{"start":{"line":505,"column":4},"end":{"line":508,"column":5}}]},"73":{"loc":{"start":{"line":507,"column":6},"end":{"line":507,"column":87}},"type":"if","locations":[{"start":{"line":507,"column":6},"end":{"line":507,"column":87}},{"start":{"line":507,"column":6},"end":{"line":507,"column":87}}]},"74":{"loc":{"start":{"line":523,"column":6},"end":{"line":531,"column":7}},"type":"if","locations":[{"start":{"line":523,"column":6},"end":{"line":531,"column":7}},{"start":{"line":523,"column":6},"end":{"line":531,"column":7}}]},"75":{"loc":{"start":{"line":525,"column":13},"end":{"line":531,"column":7}},"type":"if","locations":[{"start":{"line":525,"column":13},"end":{"line":531,"column":7}},{"start":{"line":525,"column":13},"end":{"line":531,"column":7}}]},"76":{"loc":{"start":{"line":527,"column":13},"end":{"line":531,"column":7}},"type":"if","locations":[{"start":{"line":527,"column":13},"end":{"line":531,"column":7}},{"start":{"line":527,"column":13},"end":{"line":531,"column":7}}]},"77":{"loc":{"start":{"line":527,"column":17},"end":{"line":527,"column":41}},"type":"binary-expr","locations":[{"start":{"line":527,"column":17},"end":{"line":527,"column":27}},{"start":{"line":527,"column":31},"end":{"line":527,"column":41}}]},"78":{"loc":{"start":{"line":532,"column":6},"end":{"line":532,"column":30}},"type":"if","locations":[{"start":{"line":532,"column":6},"end":{"line":532,"column":30}},{"start":{"line":532,"column":6},"end":{"line":532,"column":30}}]},"79":{"loc":{"start":{"line":536,"column":4},"end":{"line":536,"column":95}},"type":"if","locations":[{"start":{"line":536,"column":4},"end":{"line":536,"column":95}},{"start":{"line":536,"column":4},"end":{"line":536,"column":95}}]},"80":{"loc":{"start":{"line":536,"column":8},"end":{"line":536,"column":81}},"type":"binary-expr","locations":[{"start":{"line":536,"column":8},"end":{"line":536,"column":32}},{"start":{"line":536,"column":36},"end":{"line":536,"column":47}},{"start":{"line":536,"column":51},"end":{"line":536,"column":81}}]},"81":{"loc":{"start":{"line":544,"column":4},"end":{"line":544,"column":91}},"type":"if","locations":[{"start":{"line":544,"column":4},"end":{"line":544,"column":91}},{"start":{"line":544,"column":4},"end":{"line":544,"column":91}}]},"82":{"loc":{"start":{"line":545,"column":4},"end":{"line":545,"column":116}},"type":"if","locations":[{"start":{"line":545,"column":4},"end":{"line":545,"column":116}},{"start":{"line":545,"column":4},"end":{"line":545,"column":116}}]},"83":{"loc":{"start":{"line":553,"column":4},"end":{"line":553,"column":89}},"type":"if","locations":[{"start":{"line":553,"column":4},"end":{"line":553,"column":89}},{"start":{"line":553,"column":4},"end":{"line":553,"column":89}}]},"84":{"loc":{"start":{"line":553,"column":8},"end":{"line":553,"column":51}},"type":"binary-expr","locations":[{"start":{"line":553,"column":8},"end":{"line":553,"column":22}},{"start":{"line":553,"column":26},"end":{"line":553,"column":51}}]},"85":{"loc":{"start":{"line":555,"column":4},"end":{"line":560,"column":5}},"type":"if","locations":[{"start":{"line":555,"column":4},"end":{"line":560,"column":5}},{"start":{"line":555,"column":4},"end":{"line":560,"column":5}}]},"86":{"loc":{"start":{"line":561,"column":4},"end":{"line":566,"column":5}},"type":"if","locations":[{"start":{"line":561,"column":4},"end":{"line":566,"column":5}},{"start":{"line":561,"column":4},"end":{"line":566,"column":5}}]},"87":{"loc":{"start":{"line":561,"column":8},"end":{"line":561,"column":35}},"type":"binary-expr","locations":[{"start":{"line":561,"column":8},"end":{"line":561,"column":19}},{"start":{"line":561,"column":23},"end":{"line":561,"column":35}}]},"88":{"loc":{"start":{"line":563,"column":6},"end":{"line":563,"column":55}},"type":"if","locations":[{"start":{"line":563,"column":6},"end":{"line":563,"column":55}},{"start":{"line":563,"column":6},"end":{"line":563,"column":55}}]},"89":{"loc":{"start":{"line":563,"column":10},"end":{"line":563,"column":36}},"type":"binary-expr","locations":[{"start":{"line":563,"column":10},"end":{"line":563,"column":21}},{"start":{"line":563,"column":25},"end":{"line":563,"column":36}}]},"90":{"loc":{"start":{"line":564,"column":6},"end":{"line":564,"column":73}},"type":"if","locations":[{"start":{"line":564,"column":6},"end":{"line":564,"column":73}},{"start":{"line":564,"column":6},"end":{"line":564,"column":73}}]},"91":{"loc":{"start":{"line":567,"column":4},"end":{"line":567,"column":116}},"type":"if","locations":[{"start":{"line":567,"column":4},"end":{"line":567,"column":116}},{"start":{"line":567,"column":4},"end":{"line":567,"column":116}}]},"92":{"loc":{"start":{"line":570,"column":4},"end":{"line":578,"column":5}},"type":"if","locations":[{"start":{"line":570,"column":4},"end":{"line":578,"column":5}},{"start":{"line":570,"column":4},"end":{"line":578,"column":5}}]},"93":{"loc":{"start":{"line":572,"column":11},"end":{"line":578,"column":5}},"type":"if","locations":[{"start":{"line":572,"column":11},"end":{"line":578,"column":5}},{"start":{"line":572,"column":11},"end":{"line":578,"column":5}}]},"94":{"loc":{"start":{"line":572,"column":15},"end":{"line":572,"column":41}},"type":"binary-expr","locations":[{"start":{"line":572,"column":15},"end":{"line":572,"column":21}},{"start":{"line":572,"column":25},"end":{"line":572,"column":41}}]},"95":{"loc":{"start":{"line":574,"column":11},"end":{"line":578,"column":5}},"type":"if","locations":[{"start":{"line":574,"column":11},"end":{"line":578,"column":5}},{"start":{"line":574,"column":11},"end":{"line":578,"column":5}}]},"96":{"loc":{"start":{"line":574,"column":15},"end":{"line":574,"column":52}},"type":"binary-expr","locations":[{"start":{"line":574,"column":15},"end":{"line":574,"column":31}},{"start":{"line":574,"column":35},"end":{"line":574,"column":52}}]},"97":{"loc":{"start":{"line":587,"column":4},"end":{"line":594,"column":5}},"type":"if","locations":[{"start":{"line":587,"column":4},"end":{"line":594,"column":5}},{"start":{"line":587,"column":4},"end":{"line":594,"column":5}}]},"98":{"loc":{"start":{"line":591,"column":6},"end":{"line":591,"column":75}},"type":"if","locations":[{"start":{"line":591,"column":6},"end":{"line":591,"column":75}},{"start":{"line":591,"column":6},"end":{"line":591,"column":75}}]},"99":{"loc":{"start":{"line":601,"column":6},"end":{"line":601,"column":108}},"type":"if","locations":[{"start":{"line":601,"column":6},"end":{"line":601,"column":108}},{"start":{"line":601,"column":6},"end":{"line":601,"column":108}}]},"100":{"loc":{"start":{"line":603,"column":6},"end":{"line":603,"column":30}},"type":"if","locations":[{"start":{"line":603,"column":6},"end":{"line":603,"column":30}},{"start":{"line":603,"column":6},"end":{"line":603,"column":30}}]},"101":{"loc":{"start":{"line":604,"column":6},"end":{"line":611,"column":7}},"type":"if","locations":[{"start":{"line":604,"column":6},"end":{"line":611,"column":7}},{"start":{"line":604,"column":6},"end":{"line":611,"column":7}}]},"102":{"loc":{"start":{"line":609,"column":8},"end":{"line":609,"column":88}},"type":"if","locations":[{"start":{"line":609,"column":8},"end":{"line":609,"column":88}},{"start":{"line":609,"column":8},"end":{"line":609,"column":88}}]},"103":{"loc":{"start":{"line":622,"column":6},"end":{"line":622,"column":101}},"type":"if","locations":[{"start":{"line":622,"column":6},"end":{"line":622,"column":101}},{"start":{"line":622,"column":6},"end":{"line":622,"column":101}}]},"104":{"loc":{"start":{"line":624,"column":6},"end":{"line":636,"column":7}},"type":"if","locations":[{"start":{"line":624,"column":6},"end":{"line":636,"column":7}},{"start":{"line":624,"column":6},"end":{"line":636,"column":7}}]},"105":{"loc":{"start":{"line":624,"column":10},"end":{"line":624,"column":85}},"type":"binary-expr","locations":[{"start":{"line":624,"column":10},"end":{"line":624,"column":19}},{"start":{"line":624,"column":23},"end":{"line":624,"column":32}},{"start":{"line":624,"column":36},"end":{"line":624,"column":85}}]},"106":{"loc":{"start":{"line":625,"column":8},"end":{"line":633,"column":9}},"type":"if","locations":[{"start":{"line":625,"column":8},"end":{"line":633,"column":9}},{"start":{"line":625,"column":8},"end":{"line":633,"column":9}}]},"107":{"loc":{"start":{"line":625,"column":12},"end":{"line":625,"column":74}},"type":"binary-expr","locations":[{"start":{"line":625,"column":12},"end":{"line":625,"column":47}},{"start":{"line":625,"column":51},"end":{"line":625,"column":74}}]},"108":{"loc":{"start":{"line":626,"column":10},"end":{"line":632,"column":11}},"type":"if","locations":[{"start":{"line":626,"column":10},"end":{"line":632,"column":11}},{"start":{"line":626,"column":10},"end":{"line":632,"column":11}}]},"109":{"loc":{"start":{"line":637,"column":6},"end":{"line":659,"column":7}},"type":"if","locations":[{"start":{"line":637,"column":6},"end":{"line":659,"column":7}},{"start":{"line":637,"column":6},"end":{"line":659,"column":7}}]},"110":{"loc":{"start":{"line":641,"column":13},"end":{"line":659,"column":7}},"type":"if","locations":[{"start":{"line":641,"column":13},"end":{"line":659,"column":7}},{"start":{"line":641,"column":13},"end":{"line":659,"column":7}}]},"111":{"loc":{"start":{"line":644,"column":8},"end":{"line":653,"column":9}},"type":"switch","locations":[{"start":{"line":645,"column":10},"end":{"line":646,"column":79}},{"start":{"line":647,"column":10},"end":{"line":649,"column":18}},{"start":{"line":650,"column":10},"end":{"line":652,"column":18}}]},"112":{"loc":{"start":{"line":646,"column":12},"end":{"line":646,"column":79}},"type":"if","locations":[{"start":{"line":646,"column":12},"end":{"line":646,"column":79}},{"start":{"line":646,"column":12},"end":{"line":646,"column":79}}]},"113":{"loc":{"start":{"line":668,"column":4},"end":{"line":703,"column":5}},"type":"switch","locations":[{"start":{"line":669,"column":6},"end":{"line":669,"column":28}},{"start":{"line":670,"column":6},"end":{"line":670,"column":28}},{"start":{"line":671,"column":6},"end":{"line":671,"column":64}},{"start":{"line":672,"column":6},"end":{"line":672,"column":63}},{"start":{"line":673,"column":6},"end":{"line":673,"column":28}},{"start":{"line":674,"column":6},"end":{"line":674,"column":27}},{"start":{"line":675,"column":6},"end":{"line":675,"column":32}},{"start":{"line":676,"column":6},"end":{"line":676,"column":28}},{"start":{"line":677,"column":6},"end":{"line":677,"column":82}},{"start":{"line":678,"column":6},"end":{"line":681,"column":18}},{"start":{"line":682,"column":6},"end":{"line":702,"column":39}}]},"114":{"loc":{"start":{"line":677,"column":15},"end":{"line":677,"column":82}},"type":"if","locations":[{"start":{"line":677,"column":15},"end":{"line":677,"column":82}},{"start":{"line":677,"column":15},"end":{"line":677,"column":82}}]},"115":{"loc":{"start":{"line":683,"column":8},"end":{"line":701,"column":9}},"type":"if","locations":[{"start":{"line":683,"column":8},"end":{"line":701,"column":9}},{"start":{"line":683,"column":8},"end":{"line":701,"column":9}}]},"116":{"loc":{"start":{"line":683,"column":12},"end":{"line":683,"column":32}},"type":"binary-expr","locations":[{"start":{"line":683,"column":12},"end":{"line":683,"column":20}},{"start":{"line":683,"column":24},"end":{"line":683,"column":32}}]},"117":{"loc":{"start":{"line":686,"column":10},"end":{"line":689,"column":11}},"type":"if","locations":[{"start":{"line":686,"column":10},"end":{"line":689,"column":11}},{"start":{"line":686,"column":10},"end":{"line":689,"column":11}}]},"118":{"loc":{"start":{"line":690,"column":10},"end":{"line":698,"column":11}},"type":"if","locations":[{"start":{"line":690,"column":10},"end":{"line":698,"column":11}},{"start":{"line":690,"column":10},"end":{"line":698,"column":11}}]},"119":{"loc":{"start":{"line":691,"column":12},"end":{"line":694,"column":13}},"type":"if","locations":[{"start":{"line":691,"column":12},"end":{"line":694,"column":13}},{"start":{"line":691,"column":12},"end":{"line":694,"column":13}}]},"120":{"loc":{"start":{"line":695,"column":12},"end":{"line":697,"column":13}},"type":"if","locations":[{"start":{"line":695,"column":12},"end":{"line":697,"column":13}},{"start":{"line":695,"column":12},"end":{"line":697,"column":13}}]},"121":{"loc":{"start":{"line":695,"column":16},"end":{"line":695,"column":47}},"type":"binary-expr","locations":[{"start":{"line":695,"column":16},"end":{"line":695,"column":33}},{"start":{"line":695,"column":37},"end":{"line":695,"column":47}}]},"122":{"loc":{"start":{"line":711,"column":4},"end":{"line":711,"column":73}},"type":"if","locations":[{"start":{"line":711,"column":4},"end":{"line":711,"column":73}},{"start":{"line":711,"column":4},"end":{"line":711,"column":73}}]},"123":{"loc":{"start":{"line":726,"column":6},"end":{"line":748,"column":7}},"type":"if","locations":[{"start":{"line":726,"column":6},"end":{"line":748,"column":7}},{"start":{"line":726,"column":6},"end":{"line":748,"column":7}}]},"124":{"loc":{"start":{"line":727,"column":26},"end":{"line":727,"column":46}},"type":"cond-expr","locations":[{"start":{"line":727,"column":41},"end":{"line":727,"column":42}},{"start":{"line":727,"column":45},"end":{"line":727,"column":46}}]},"125":{"loc":{"start":{"line":728,"column":13},"end":{"line":748,"column":7}},"type":"if","locations":[{"start":{"line":728,"column":13},"end":{"line":748,"column":7}},{"start":{"line":728,"column":13},"end":{"line":748,"column":7}}]},"126":{"loc":{"start":{"line":734,"column":8},"end":{"line":736,"column":9}},"type":"if","locations":[{"start":{"line":734,"column":8},"end":{"line":736,"column":9}},{"start":{"line":734,"column":8},"end":{"line":736,"column":9}}]},"127":{"loc":{"start":{"line":740,"column":8},"end":{"line":742,"column":9}},"type":"if","locations":[{"start":{"line":740,"column":8},"end":{"line":742,"column":9}},{"start":{"line":740,"column":8},"end":{"line":742,"column":9}}]},"128":{"loc":{"start":{"line":740,"column":14},"end":{"line":740,"column":58}},"type":"cond-expr","locations":[{"start":{"line":740,"column":22},"end":{"line":740,"column":39}},{"start":{"line":740,"column":42},"end":{"line":740,"column":58}}]},"129":{"loc":{"start":{"line":760,"column":4},"end":{"line":762,"column":5}},"type":"if","locations":[{"start":{"line":760,"column":4},"end":{"line":762,"column":5}},{"start":{"line":760,"column":4},"end":{"line":762,"column":5}}]},"130":{"loc":{"start":{"line":760,"column":8},"end":{"line":760,"column":55}},"type":"binary-expr","locations":[{"start":{"line":760,"column":8},"end":{"line":760,"column":31}},{"start":{"line":760,"column":35},"end":{"line":760,"column":55}}]},"131":{"loc":{"start":{"line":767,"column":4},"end":{"line":772,"column":5}},"type":"if","locations":[{"start":{"line":767,"column":4},"end":{"line":772,"column":5}},{"start":{"line":767,"column":4},"end":{"line":772,"column":5}}]},"132":{"loc":{"start":{"line":769,"column":6},"end":{"line":771,"column":7}},"type":"if","locations":[{"start":{"line":769,"column":6},"end":{"line":771,"column":7}},{"start":{"line":769,"column":6},"end":{"line":771,"column":7}}]},"133":{"loc":{"start":{"line":769,"column":10},"end":{"line":769,"column":71}},"type":"binary-expr","locations":[{"start":{"line":769,"column":10},"end":{"line":769,"column":38}},{"start":{"line":769,"column":42},"end":{"line":769,"column":71}}]},"134":{"loc":{"start":{"line":774,"column":4},"end":{"line":776,"column":5}},"type":"if","locations":[{"start":{"line":774,"column":4},"end":{"line":776,"column":5}},{"start":{"line":774,"column":4},"end":{"line":776,"column":5}}]},"135":{"loc":{"start":{"line":778,"column":4},"end":{"line":780,"column":5}},"type":"if","locations":[{"start":{"line":778,"column":4},"end":{"line":780,"column":5}},{"start":{"line":778,"column":4},"end":{"line":780,"column":5}}]},"136":{"loc":{"start":{"line":778,"column":8},"end":{"line":778,"column":102}},"type":"binary-expr","locations":[{"start":{"line":778,"column":8},"end":{"line":778,"column":29}},{"start":{"line":778,"column":33},"end":{"line":778,"column":53}},{"start":{"line":778,"column":57},"end":{"line":778,"column":76}},{"start":{"line":778,"column":80},"end":{"line":778,"column":102}}]},"137":{"loc":{"start":{"line":782,"column":4},"end":{"line":784,"column":5}},"type":"if","locations":[{"start":{"line":782,"column":4},"end":{"line":784,"column":5}},{"start":{"line":782,"column":4},"end":{"line":784,"column":5}}]},"138":{"loc":{"start":{"line":791,"column":4},"end":{"line":797,"column":5}},"type":"if","locations":[{"start":{"line":791,"column":4},"end":{"line":797,"column":5}},{"start":{"line":791,"column":4},"end":{"line":797,"column":5}}]},"139":{"loc":{"start":{"line":791,"column":8},"end":{"line":791,"column":43}},"type":"binary-expr","locations":[{"start":{"line":791,"column":8},"end":{"line":791,"column":20}},{"start":{"line":791,"column":24},"end":{"line":791,"column":43}}]},"140":{"loc":{"start":{"line":793,"column":11},"end":{"line":797,"column":5}},"type":"if","locations":[{"start":{"line":793,"column":11},"end":{"line":797,"column":5}},{"start":{"line":793,"column":11},"end":{"line":797,"column":5}}]}},"s":{"1":17531,"2":17531,"3":17531,"4":17531,"5":17531,"6":31,"7":21,"8":10,"9":2123,"10":2123,"11":17576,"12":17531,"13":17576,"14":17576,"15":17576,"16":17576,"17":17576,"18":32202,"19":8994,"20":8976,"21":23208,"22":83412,"23":5955,"24":45,"25":45,"26":45,"27":45,"28":45,"29":45,"30":45,"31":45,"32":235,"33":235,"34":223,"35":12,"36":12,"37":0,"38":0,"39":12,"40":24694,"41":19714,"42":19714,"43":19546,"44":19702,"45":19702,"46":19702,"47":19702,"48":19702,"49":2934,"50":16768,"51":87,"52":16681,"53":16195,"54":5944,"55":10251,"56":45377,"57":45377,"58":45165,"59":212,"60":212,"61":88,"62":88,"63":87,"64":87,"65":87,"66":59,"67":59,"68":59,"69":59,"70":12,"71":47,"72":47,"73":47,"74":34,"75":34,"76":47,"77":41,"78":41,"79":41,"80":41,"81":468,"82":468,"83":41,"84":19549,"85":19549,"86":24559,"87":24559,"88":7518,"89":7518,"90":6,"91":0,"92":342,"93":342,"94":342,"95":342,"96":200,"97":59,"98":47,"99":36,"100":36,"101":105,"102":83,"103":16499,"104":2,"105":16497,"106":19578,"107":19578,"108":19578,"109":19578,"110":19578,"111":19578,"112":232,"113":232,"114":3,"115":229,"116":229,"117":116,"118":116,"119":113,"120":113,"121":105,"122":37,"123":37,"124":68,"125":68,"126":2,"127":66,"128":166,"129":166,"130":166,"131":166,"132":15,"133":15,"134":15,"135":166,"136":5,"137":5,"138":166,"139":68,"140":68,"141":22,"142":46,"143":4,"144":42,"145":13,"146":13,"147":2,"148":11,"149":170,"150":170,"151":68,"152":1,"153":1,"154":1,"155":67,"156":102,"157":5,"158":97,"159":118,"160":118,"161":118,"162":19,"163":19,"164":6,"165":13,"166":99,"167":2,"168":0,"169":2,"170":2,"171":2,"172":97,"173":7,"174":97,"175":707,"176":707,"177":9,"178":698,"179":175,"180":175,"181":523,"182":10266,"183":232,"184":1488,"185":1488,"186":1321,"187":1321,"188":788,"189":788,"190":392,"191":392,"192":239,"193":239,"194":239,"195":239,"196":1379,"197":1379,"198":1188,"199":1188,"200":483,"201":0,"202":483,"203":483,"204":51,"205":51,"206":33,"207":33,"208":35,"209":35,"210":189,"211":189,"212":13,"213":176,"214":36,"215":140,"216":34,"217":588,"218":371,"219":105,"220":166,"221":68,"222":13,"223":170,"224":118,"225":707,"226":2,"227":7,"228":1261,"229":1261,"230":1261,"231":37,"232":37,"233":206,"234":4,"235":202,"236":202,"237":5,"238":197,"239":7,"240":190,"241":10,"242":180,"243":10,"244":170,"245":28,"246":162,"247":169,"248":28,"249":28,"250":28,"251":27,"252":10,"253":10,"254":1,"255":26,"256":740,"257":740,"258":740,"259":1906,"260":1906,"261":67,"262":1839,"263":47,"264":1792,"265":1187,"266":605,"267":1906,"268":703,"269":1203,"270":1203,"271":740,"272":41,"273":699,"274":83,"275":83,"276":83,"277":18,"278":65,"279":10,"280":55,"281":591,"282":591,"283":591,"284":591,"285":0,"286":591,"287":591,"288":10,"289":10,"290":10,"291":10,"292":591,"293":11,"294":11,"295":7,"296":11,"297":8,"298":3,"299":583,"300":10,"301":573,"302":573,"303":11,"304":562,"305":539,"306":23,"307":9,"308":14,"309":564,"310":40,"311":40,"312":21,"313":21,"314":18,"315":18,"316":1,"317":19,"318":29,"319":371,"320":371,"321":371,"322":3274,"323":4,"324":3270,"325":3270,"326":346,"327":2924,"328":92,"329":92,"330":73,"331":2832,"332":2,"333":2830,"334":346,"335":346,"336":87,"337":87,"338":87,"339":227,"340":2,"341":225,"342":225,"343":82,"344":41,"345":17,"346":17,"347":24,"348":24,"349":41,"350":41,"351":143,"352":15,"353":15,"354":12,"355":128,"356":3,"357":3,"358":3,"359":1,"360":0,"361":3,"362":3,"363":0,"364":0,"365":3,"366":3,"367":3,"368":125,"369":107,"370":107,"371":107,"372":8,"373":4,"374":8,"375":19,"376":2,"377":2,"378":2,"379":2,"380":0,"381":0,"382":4,"383":4,"384":4,"385":56,"386":25,"387":25,"388":25,"389":2,"390":2,"391":25,"392":23,"393":23,"394":23,"395":23,"396":9,"397":16,"398":16,"399":31,"400":48,"401":48,"402":48,"403":13,"404":35,"405":5972,"406":5972,"407":5972,"408":5972,"409":5972,"410":28048,"411":28048,"412":22348,"413":5700,"414":27,"415":27,"416":27,"417":27,"418":6,"419":21,"420":21,"421":20,"422":5,"423":15,"424":15,"425":5673,"426":22363,"427":5960,"428":5944,"429":5933,"430":5933,"431":1971,"432":5933,"433":1353,"434":34,"435":34,"436":32,"437":1321,"438":2,"439":1319,"440":689,"441":630,"442":4,"443":626,"444":19525,"445":19525,"446":14,"447":19511,"448":10074,"449":9437},"f":{"1":17531,"2":31,"3":2123,"4":17576,"5":32202,"6":83412,"7":5955,"8":45,"9":235,"10":24694,"11":19714,"12":16195,"13":45377,"14":88,"15":59,"16":41,"17":19549,"18":19578,"19":232,"20":105,"21":166,"22":68,"23":13,"24":170,"25":118,"26":707,"27":10266,"28":1261,"29":37,"30":740,"31":83,"32":591,"33":40,"34":371,"35":87,"36":107,"37":48,"38":5972,"39":5944,"40":1353,"41":19525},"b":{"1":[21,10],"2":[17531,45],"3":[8994,23208],"4":[223,12],"5":[235,227],"6":[19546,168],"7":[19714,19714],"8":[2934,16768],"9":[87,16681],"10":[5944,10251],"11":[16195,10264],"12":[45165,212],"13":[45377,212],"14":[47,41],"15":[87,1],"16":[12,47],"17":[81,48],"18":[509,503,470,468,468],"19":[7518,7518,6,341,342,342,200,16499],"20":[0,6],"21":[59,36,105],"22":[2,16497],"23":[16499,16499,16499,21],"24":[3,229],"25":[232,109],"26":[116,113],"27":[229,119],"28":[37,68],"29":[2,66],"30":[156,10],"31":[15,151],"32":[5,161],"33":[22,46],"34":[11,11],"35":[4,42],"36":[28,14],"37":[2,11],"38":[68,102],"39":[1,67],"40":[68,32,2],"41":[5,97],"42":[19,99],"43":[5,14],"44":[19,9],"45":[6,13],"46":[2,97],"47":[99,2,2,2],"48":[0,2],"49":[7,90],"50":[9,698],"51":[4,5],"52":[175,523],"53":[698,695],"54":[520,3],"55":[232,1488,1321,788,392,239,239,1379,1188,483,51,33,35,189,301,359,407,564,574,578,580,588,588,250,371,105,10,166,41,68,13,105,170,104,118,700,707,2],"56":[0,483],"57":[483,0],"58":[13,176],"59":[189,178],"60":[36,140],"61":[176,156],"62":[34,106],"63":[140,122],"64":[4,202],"65":[5,197],"66":[7,190],"67":[10,180],"68":[10,170],"69":[180,10],"70":[28,142],"71":[170,28],"72":[10,17],"73":[1,9],"74":[67,1839],"75":[47,1792],"76":[1187,605],"77":[1792,1313],"78":[703,1203],"79":[41,699],"80":[740,704,40],"81":[18,65],"82":[10,55],"83":[0,591],"84":[591,588],"85":[10,581],"86":[11,580],"87":[591,591],"88":[7,4],"89":[11,7],"90":[8,3],"91":[10,573],"92":[11,562],"93":[539,23],"94":[562,103],"95":[9,14],"96":[23,21],"97":[21,19],"98":[1,17],"99":[4,3270],"100":[346,2924],"101":[92,2832],"102":[2,2830],"103":[2,225],"104":[82,143],"105":[225,177,36],"106":[41,41],"107":[82,49],"108":[17,24],"109":[15,128],"110":[3,125],"111":[1,3,0],"112":[0,1],"113":[8,4,8,19,2,2,2,2,0,4,56],"114":[0,0],"115":[25,31],"116":[56,52],"117":[2,23],"118":[23,2],"119":[23,0],"120":[9,14],"121":[23,16],"122":[13,35],"123":[22348,5700],"124":[22342,6],"125":[27,5673],"126":[6,21],"127":[5,15],"128":[10,10],"129":[1971,3962],"130":[5933,5922],"131":[34,1319],"132":[32,2],"133":[34,11],"134":[2,1319],"135":[689,630],"136":[1319,1319,1319,1280],"137":[4,626],"138":[14,19511],"139":[19525,1971],"140":[10074,9437]},"hash":"0b96307dcefc47464133f55be59f5ce3b08338ca"} +,"/home/travis/build/babel/babylon/src/tokenizer/state.js": {"path":"/home/travis/build/babel/babylon/src/tokenizer/state.js","statementMap":{"1":{"start":{"line":9,"column":4},"end":{"line":9,"column":89}},"2":{"start":{"line":11,"column":4},"end":{"line":11,"column":23}},"3":{"start":{"line":13,"column":4},"end":{"line":13,"column":31}},"4":{"start":{"line":15,"column":4},"end":{"line":15,"column":78}},"5":{"start":{"line":17,"column":4},"end":{"line":17,"column":21}},"6":{"start":{"line":19,"column":4},"end":{"line":19,"column":25}},"7":{"start":{"line":21,"column":4},"end":{"line":21,"column":21}},"8":{"start":{"line":23,"column":4},"end":{"line":23,"column":23}},"9":{"start":{"line":25,"column":4},"end":{"line":25,"column":31}},"10":{"start":{"line":26,"column":4},"end":{"line":26,"column":31}},"11":{"start":{"line":27,"column":4},"end":{"line":27,"column":31}},"12":{"start":{"line":29,"column":4},"end":{"line":29,"column":34}},"13":{"start":{"line":30,"column":4},"end":{"line":30,"column":21}},"14":{"start":{"line":32,"column":4},"end":{"line":32,"column":23}},"15":{"start":{"line":33,"column":4},"end":{"line":33,"column":22}},"16":{"start":{"line":34,"column":4},"end":{"line":34,"column":37}},"17":{"start":{"line":35,"column":4},"end":{"line":35,"column":53}},"18":{"start":{"line":37,"column":4},"end":{"line":37,"column":53}},"19":{"start":{"line":38,"column":4},"end":{"line":38,"column":51}},"20":{"start":{"line":40,"column":4},"end":{"line":40,"column":39}},"21":{"start":{"line":41,"column":4},"end":{"line":41,"column":28}},"22":{"start":{"line":43,"column":4},"end":{"line":43,"column":50}},"23":{"start":{"line":44,"column":4},"end":{"line":44,"column":30}},"24":{"start":{"line":46,"column":4},"end":{"line":46,"column":16}},"25":{"start":{"line":123,"column":4},"end":{"line":123,"column":65}},"26":{"start":{"line":127,"column":16},"end":{"line":127,"column":25}},"27":{"start":{"line":128,"column":4},"end":{"line":136,"column":5}},"28":{"start":{"line":129,"column":16},"end":{"line":129,"column":25}},"29":{"start":{"line":131,"column":6},"end":{"line":133,"column":7}},"30":{"start":{"line":132,"column":8},"end":{"line":132,"column":26}},"31":{"start":{"line":135,"column":6},"end":{"line":135,"column":23}},"32":{"start":{"line":137,"column":4},"end":{"line":137,"column":17}}},"fnMap":{"1":{"name":"(anonymous_1)","decl":{"start":{"line":8,"column":2},"end":{"line":8,"column":3}},"loc":{"start":{"line":8,"column":39},"end":{"line":47,"column":3}}},"2":{"name":"(anonymous_2)","decl":{"start":{"line":122,"column":2},"end":{"line":122,"column":3}},"loc":{"start":{"line":122,"column":16},"end":{"line":124,"column":3}}},"3":{"name":"(anonymous_3)","decl":{"start":{"line":126,"column":2},"end":{"line":126,"column":3}},"loc":{"start":{"line":126,"column":21},"end":{"line":138,"column":3}}}},"branchMap":{"1":{"loc":{"start":{"line":9,"column":18},"end":{"line":9,"column":88}},"type":"cond-expr","locations":[{"start":{"line":9,"column":49},"end":{"line":9,"column":54}},{"start":{"line":9,"column":57},"end":{"line":9,"column":88}}]},"2":{"loc":{"start":{"line":131,"column":6},"end":{"line":133,"column":7}},"type":"if","locations":[{"start":{"line":131,"column":6},"end":{"line":133,"column":7}},{"start":{"line":131,"column":6},"end":{"line":133,"column":7}}]},"3":{"loc":{"start":{"line":131,"column":10},"end":{"line":131,"column":66}},"type":"binary-expr","locations":[{"start":{"line":131,"column":11},"end":{"line":131,"column":22}},{"start":{"line":131,"column":26},"end":{"line":131,"column":43}},{"start":{"line":131,"column":48},"end":{"line":131,"column":66}}]}},"s":{"1":2123,"2":2123,"3":2123,"4":2123,"5":2123,"6":2123,"7":2123,"8":2123,"9":2123,"10":2123,"11":2123,"12":2123,"13":2123,"14":2123,"15":2123,"16":2123,"17":2123,"18":2123,"19":2123,"20":2123,"21":2123,"22":2123,"23":2123,"24":2123,"25":41591,"26":176,"27":176,"28":5777,"29":5777,"30":778,"31":5777,"32":176},"f":{"1":2123,"2":41591,"3":176},"b":{"1":[0,2123],"2":[778,4999],"3":[5777,2978,2889]},"hash":"40e48b5bafa0a3ee9e647b1359b7d05ab7ff395f"} +,"/home/travis/build/babel/babylon/src/tokenizer/types.js": {"path":"/home/travis/build/babel/babylon/src/tokenizer/types.js","statementMap":{"1":{"start":{"line":21,"column":4},"end":{"line":21,"column":23}},"2":{"start":{"line":22,"column":4},"end":{"line":22,"column":32}},"3":{"start":{"line":23,"column":4},"end":{"line":23,"column":40}},"4":{"start":{"line":24,"column":4},"end":{"line":24,"column":40}},"5":{"start":{"line":25,"column":4},"end":{"line":25,"column":52}},"6":{"start":{"line":26,"column":4},"end":{"line":26,"column":32}},"7":{"start":{"line":27,"column":4},"end":{"line":27,"column":36}},"8":{"start":{"line":28,"column":4},"end":{"line":28,"column":32}},"9":{"start":{"line":29,"column":4},"end":{"line":29,"column":34}},"10":{"start":{"line":30,"column":4},"end":{"line":30,"column":36}},"11":{"start":{"line":31,"column":4},"end":{"line":31,"column":30}},"12":{"start":{"line":36,"column":2},"end":{"line":36,"column":62}},"13":{"start":{"line":38,"column":19},"end":{"line":38,"column":37}},"14":{"start":{"line":38,"column":52},"end":{"line":38,"column":70}},"15":{"start":{"line":40,"column":21},"end":{"line":98,"column":1}},"16":{"start":{"line":102,"column":24},"end":{"line":102,"column":26}},"17":{"start":{"line":106,"column":2},"end":{"line":106,"column":25}},"18":{"start":{"line":107,"column":2},"end":{"line":107,"column":68}},"19":{"start":{"line":110,"column":0},"end":{"line":110,"column":12}},"20":{"start":{"line":111,"column":0},"end":{"line":111,"column":23}},"21":{"start":{"line":112,"column":0},"end":{"line":112,"column":12}},"22":{"start":{"line":113,"column":0},"end":{"line":113,"column":15}},"23":{"start":{"line":114,"column":0},"end":{"line":114,"column":15}},"24":{"start":{"line":115,"column":0},"end":{"line":115,"column":26}},"25":{"start":{"line":116,"column":0},"end":{"line":116,"column":43}},"26":{"start":{"line":117,"column":0},"end":{"line":117,"column":23}},"27":{"start":{"line":118,"column":0},"end":{"line":118,"column":14}},"28":{"start":{"line":119,"column":0},"end":{"line":119,"column":26}},"29":{"start":{"line":120,"column":0},"end":{"line":120,"column":27}},"30":{"start":{"line":121,"column":0},"end":{"line":121,"column":9}},"31":{"start":{"line":122,"column":0},"end":{"line":122,"column":25}},"32":{"start":{"line":123,"column":0},"end":{"line":123,"column":13}},"33":{"start":{"line":124,"column":0},"end":{"line":124,"column":24}},"34":{"start":{"line":125,"column":0},"end":{"line":125,"column":10}},"35":{"start":{"line":126,"column":0},"end":{"line":126,"column":10}},"36":{"start":{"line":127,"column":0},"end":{"line":127,"column":10}},"37":{"start":{"line":128,"column":0},"end":{"line":128,"column":12}},"38":{"start":{"line":129,"column":0},"end":{"line":129,"column":28}},"39":{"start":{"line":130,"column":0},"end":{"line":130,"column":11}},"40":{"start":{"line":131,"column":0},"end":{"line":131,"column":48}},"41":{"start":{"line":132,"column":0},"end":{"line":132,"column":23}},"42":{"start":{"line":133,"column":0},"end":{"line":133,"column":24}},"43":{"start":{"line":134,"column":0},"end":{"line":134,"column":12}},"44":{"start":{"line":135,"column":0},"end":{"line":135,"column":26}},"45":{"start":{"line":136,"column":0},"end":{"line":136,"column":13}},"46":{"start":{"line":137,"column":0},"end":{"line":137,"column":13}},"47":{"start":{"line":138,"column":0},"end":{"line":138,"column":50}},"48":{"start":{"line":139,"column":0},"end":{"line":139,"column":23}},"49":{"start":{"line":140,"column":0},"end":{"line":140,"column":23}},"50":{"start":{"line":141,"column":0},"end":{"line":141,"column":24}},"51":{"start":{"line":142,"column":0},"end":{"line":142,"column":39}},"52":{"start":{"line":143,"column":0},"end":{"line":143,"column":47}},"53":{"start":{"line":144,"column":0},"end":{"line":144,"column":65}},"54":{"start":{"line":145,"column":0},"end":{"line":145,"column":63}},"55":{"start":{"line":146,"column":0},"end":{"line":146,"column":65}}},"fnMap":{"1":{"name":"(anonymous_1)","decl":{"start":{"line":20,"column":2},"end":{"line":20,"column":3}},"loc":{"start":{"line":20,"column":32},"end":{"line":32,"column":3}}},"2":{"name":"binop","decl":{"start":{"line":35,"column":9},"end":{"line":35,"column":14}},"loc":{"start":{"line":35,"column":27},"end":{"line":37,"column":1}}},"3":{"name":"kw","decl":{"start":{"line":105,"column":9},"end":{"line":105,"column":11}},"loc":{"start":{"line":105,"column":32},"end":{"line":108,"column":1}}}},"branchMap":{"1":{"loc":{"start":{"line":20,"column":21},"end":{"line":20,"column":30}},"type":"default-arg","locations":[{"start":{"line":20,"column":28},"end":{"line":20,"column":30}}]},"2":{"loc":{"start":{"line":30,"column":17},"end":{"line":30,"column":35}},"type":"binary-expr","locations":[{"start":{"line":30,"column":17},"end":{"line":30,"column":27}},{"start":{"line":30,"column":31},"end":{"line":30,"column":35}}]},"3":{"loc":{"start":{"line":105,"column":18},"end":{"line":105,"column":30}},"type":"default-arg","locations":[{"start":{"line":105,"column":28},"end":{"line":105,"column":30}}]}},"s":{"1":81,"2":81,"3":81,"4":81,"5":81,"6":81,"7":81,"8":81,"9":81,"10":81,"11":81,"12":11,"13":1,"14":1,"15":1,"16":1,"17":37,"18":37,"19":1,"20":1,"21":1,"22":1,"23":1,"24":1,"25":1,"26":1,"27":1,"28":1,"29":1,"30":1,"31":1,"32":1,"33":1,"34":1,"35":1,"36":1,"37":1,"38":1,"39":1,"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1,"50":1,"51":1,"52":1,"53":1,"54":1,"55":1},"f":{"1":81,"2":11,"3":37},"b":{"1":[9],"2":[81,66],"3":[15]},"hash":"ff2900f09f77cfa5c056744ad19c3191983284da"} +,"/home/travis/build/babel/babylon/src/util/identifier.js": {"path":"/home/travis/build/babel/babylon/src/util/identifier.js","statementMap":{"1":{"start":{"line":13,"column":2},"end":{"line":13,"column":27}},"2":{"start":{"line":14,"column":2},"end":{"line":16,"column":4}},"3":{"start":{"line":15,"column":4},"end":{"line":15,"column":35}},"4":{"start":{"line":21,"column":29},"end":{"line":25,"column":1}},"5":{"start":{"line":29,"column":25},"end":{"line":29,"column":263}},"6":{"start":{"line":39,"column":35},"end":{"line":39,"column":4312}},"7":{"start":{"line":40,"column":30},"end":{"line":40,"column":2605}},"8":{"start":{"line":42,"column":32},"end":{"line":42,"column":84}},"9":{"start":{"line":43,"column":27},"end":{"line":43,"column":105}},"10":{"start":{"line":45,"column":0},"end":{"line":45,"column":62}},"11":{"start":{"line":52,"column":35},"end":{"line":52,"column":1052}},"12":{"start":{"line":53,"column":30},"end":{"line":53,"column":485}},"13":{"start":{"line":59,"column":12},"end":{"line":59,"column":19}},"14":{"start":{"line":60,"column":2},"end":{"line":66,"column":3}},"15":{"start":{"line":61,"column":4},"end":{"line":61,"column":18}},"16":{"start":{"line":62,"column":4},"end":{"line":62,"column":33}},"17":{"start":{"line":62,"column":20},"end":{"line":62,"column":33}},"18":{"start":{"line":64,"column":4},"end":{"line":64,"column":22}},"19":{"start":{"line":65,"column":4},"end":{"line":65,"column":33}},"20":{"start":{"line":65,"column":21},"end":{"line":65,"column":33}},"21":{"start":{"line":72,"column":2},"end":{"line":72,"column":36}},"22":{"start":{"line":72,"column":17},"end":{"line":72,"column":36}},"23":{"start":{"line":73,"column":2},"end":{"line":73,"column":29}},"24":{"start":{"line":73,"column":17},"end":{"line":73,"column":29}},"25":{"start":{"line":74,"column":2},"end":{"line":74,"column":36}},"26":{"start":{"line":74,"column":17},"end":{"line":74,"column":36}},"27":{"start":{"line":75,"column":2},"end":{"line":75,"column":30}},"28":{"start":{"line":75,"column":18},"end":{"line":75,"column":30}},"29":{"start":{"line":76,"column":2},"end":{"line":76,"column":101}},"30":{"start":{"line":76,"column":22},"end":{"line":76,"column":101}},"31":{"start":{"line":77,"column":2},"end":{"line":77,"column":57}},"32":{"start":{"line":83,"column":2},"end":{"line":83,"column":36}},"33":{"start":{"line":83,"column":17},"end":{"line":83,"column":36}},"34":{"start":{"line":84,"column":2},"end":{"line":84,"column":29}},"35":{"start":{"line":84,"column":17},"end":{"line":84,"column":29}},"36":{"start":{"line":85,"column":2},"end":{"line":85,"column":30}},"37":{"start":{"line":85,"column":17},"end":{"line":85,"column":30}},"38":{"start":{"line":86,"column":2},"end":{"line":86,"column":29}},"39":{"start":{"line":86,"column":17},"end":{"line":86,"column":29}},"40":{"start":{"line":87,"column":2},"end":{"line":87,"column":36}},"41":{"start":{"line":87,"column":17},"end":{"line":87,"column":36}},"42":{"start":{"line":88,"column":2},"end":{"line":88,"column":30}},"43":{"start":{"line":88,"column":18},"end":{"line":88,"column":30}},"44":{"start":{"line":89,"column":2},"end":{"line":89,"column":96}},"45":{"start":{"line":89,"column":22},"end":{"line":89,"column":96}},"46":{"start":{"line":90,"column":2},"end":{"line":90,"column":103}}},"fnMap":{"1":{"name":"makePredicate","decl":{"start":{"line":12,"column":9},"end":{"line":12,"column":22}},"loc":{"start":{"line":12,"column":30},"end":{"line":17,"column":1}}},"2":{"name":"(anonymous_2)","decl":{"start":{"line":14,"column":9},"end":{"line":14,"column":10}},"loc":{"start":{"line":14,"column":24},"end":{"line":16,"column":3}}},"3":{"name":"isInAstralSet","decl":{"start":{"line":58,"column":9},"end":{"line":58,"column":22}},"loc":{"start":{"line":58,"column":34},"end":{"line":67,"column":1}}},"4":{"name":"isIdentifierStart","decl":{"start":{"line":71,"column":16},"end":{"line":71,"column":33}},"loc":{"start":{"line":71,"column":40},"end":{"line":78,"column":1}}},"5":{"name":"isIdentifierChar","decl":{"start":{"line":82,"column":16},"end":{"line":82,"column":32}},"loc":{"start":{"line":82,"column":39},"end":{"line":91,"column":1}}}},"branchMap":{"1":{"loc":{"start":{"line":62,"column":4},"end":{"line":62,"column":33}},"type":"if","locations":[{"start":{"line":62,"column":4},"end":{"line":62,"column":33}},{"start":{"line":62,"column":4},"end":{"line":62,"column":33}}]},"2":{"loc":{"start":{"line":65,"column":4},"end":{"line":65,"column":33}},"type":"if","locations":[{"start":{"line":65,"column":4},"end":{"line":65,"column":33}},{"start":{"line":65,"column":4},"end":{"line":65,"column":33}}]},"3":{"loc":{"start":{"line":72,"column":2},"end":{"line":72,"column":36}},"type":"if","locations":[{"start":{"line":72,"column":2},"end":{"line":72,"column":36}},{"start":{"line":72,"column":2},"end":{"line":72,"column":36}}]},"4":{"loc":{"start":{"line":73,"column":2},"end":{"line":73,"column":29}},"type":"if","locations":[{"start":{"line":73,"column":2},"end":{"line":73,"column":29}},{"start":{"line":73,"column":2},"end":{"line":73,"column":29}}]},"5":{"loc":{"start":{"line":74,"column":2},"end":{"line":74,"column":36}},"type":"if","locations":[{"start":{"line":74,"column":2},"end":{"line":74,"column":36}},{"start":{"line":74,"column":2},"end":{"line":74,"column":36}}]},"6":{"loc":{"start":{"line":75,"column":2},"end":{"line":75,"column":30}},"type":"if","locations":[{"start":{"line":75,"column":2},"end":{"line":75,"column":30}},{"start":{"line":75,"column":2},"end":{"line":75,"column":30}}]},"7":{"loc":{"start":{"line":76,"column":2},"end":{"line":76,"column":101}},"type":"if","locations":[{"start":{"line":76,"column":2},"end":{"line":76,"column":101}},{"start":{"line":76,"column":2},"end":{"line":76,"column":101}}]},"8":{"loc":{"start":{"line":76,"column":29},"end":{"line":76,"column":100}},"type":"binary-expr","locations":[{"start":{"line":76,"column":29},"end":{"line":76,"column":41}},{"start":{"line":76,"column":45},"end":{"line":76,"column":100}}]},"9":{"loc":{"start":{"line":83,"column":2},"end":{"line":83,"column":36}},"type":"if","locations":[{"start":{"line":83,"column":2},"end":{"line":83,"column":36}},{"start":{"line":83,"column":2},"end":{"line":83,"column":36}}]},"10":{"loc":{"start":{"line":84,"column":2},"end":{"line":84,"column":29}},"type":"if","locations":[{"start":{"line":84,"column":2},"end":{"line":84,"column":29}},{"start":{"line":84,"column":2},"end":{"line":84,"column":29}}]},"11":{"loc":{"start":{"line":85,"column":2},"end":{"line":85,"column":30}},"type":"if","locations":[{"start":{"line":85,"column":2},"end":{"line":85,"column":30}},{"start":{"line":85,"column":2},"end":{"line":85,"column":30}}]},"12":{"loc":{"start":{"line":86,"column":2},"end":{"line":86,"column":29}},"type":"if","locations":[{"start":{"line":86,"column":2},"end":{"line":86,"column":29}},{"start":{"line":86,"column":2},"end":{"line":86,"column":29}}]},"13":{"loc":{"start":{"line":87,"column":2},"end":{"line":87,"column":36}},"type":"if","locations":[{"start":{"line":87,"column":2},"end":{"line":87,"column":36}},{"start":{"line":87,"column":2},"end":{"line":87,"column":36}}]},"14":{"loc":{"start":{"line":88,"column":2},"end":{"line":88,"column":30}},"type":"if","locations":[{"start":{"line":88,"column":2},"end":{"line":88,"column":30}},{"start":{"line":88,"column":2},"end":{"line":88,"column":30}}]},"15":{"loc":{"start":{"line":89,"column":2},"end":{"line":89,"column":96}},"type":"if","locations":[{"start":{"line":89,"column":2},"end":{"line":89,"column":96}},{"start":{"line":89,"column":2},"end":{"line":89,"column":96}}]},"16":{"loc":{"start":{"line":89,"column":29},"end":{"line":89,"column":95}},"type":"binary-expr","locations":[{"start":{"line":89,"column":29},"end":{"line":89,"column":41}},{"start":{"line":89,"column":45},"end":{"line":89,"column":95}}]},"17":{"loc":{"start":{"line":90,"column":9},"end":{"line":90,"column":102}},"type":"binary-expr","locations":[{"start":{"line":90,"column":9},"end":{"line":90,"column":56}},{"start":{"line":90,"column":60},"end":{"line":90,"column":102}}]}},"s":{"1":4,"2":4,"3":7769,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1,"12":1,"13":221,"14":221,"15":40931,"16":40931,"17":7,"18":40924,"19":40924,"20":15,"21":17173,"22":7699,"23":9474,"24":443,"25":9031,"26":601,"27":8430,"28":5580,"29":2850,"30":2642,"31":208,"32":28340,"33":4718,"34":23622,"35":29,"36":23593,"37":843,"38":22750,"39":551,"40":22199,"41":292,"42":21907,"43":21744,"44":163,"45":153,"46":10},"f":{"1":4,"2":7769,"3":221,"4":17173,"5":28340},"b":{"1":[7,40924],"2":[15,40909],"3":[7699,9474],"4":[443,9031],"5":[601,8430],"6":[5580,2850],"7":[2642,208],"8":[2642,14],"9":[4718,23622],"10":[29,23593],"11":[843,22750],"12":[551,22199],"13":[292,21907],"14":[21744,163],"15":[153,10],"16":[153,33],"17":[10,3]},"hash":"4e54a8370bca83a76606b3f06730fa99460e0c59"} +,"/home/travis/build/babel/babylon/src/util/location.js": {"path":"/home/travis/build/babel/babylon/src/util/location.js","statementMap":{"1":{"start":{"line":8,"column":4},"end":{"line":8,"column":21}},"2":{"start":{"line":9,"column":4},"end":{"line":9,"column":22}},"3":{"start":{"line":15,"column":4},"end":{"line":15,"column":23}},"4":{"start":{"line":16,"column":4},"end":{"line":16,"column":19}},"5":{"start":{"line":27,"column":2},"end":{"line":36,"column":3}},"6":{"start":{"line":28,"column":4},"end":{"line":28,"column":31}},"7":{"start":{"line":29,"column":16},"end":{"line":29,"column":38}},"8":{"start":{"line":30,"column":4},"end":{"line":35,"column":5}},"9":{"start":{"line":31,"column":6},"end":{"line":31,"column":13}},"10":{"start":{"line":32,"column":6},"end":{"line":32,"column":42}},"11":{"start":{"line":34,"column":6},"end":{"line":34,"column":46}}},"fnMap":{"1":{"name":"(anonymous_1)","decl":{"start":{"line":7,"column":2},"end":{"line":7,"column":3}},"loc":{"start":{"line":7,"column":25},"end":{"line":10,"column":3}}},"2":{"name":"(anonymous_2)","decl":{"start":{"line":14,"column":2},"end":{"line":14,"column":3}},"loc":{"start":{"line":14,"column":26},"end":{"line":17,"column":3}}},"3":{"name":"getLineInfo","decl":{"start":{"line":26,"column":16},"end":{"line":26,"column":27}},"loc":{"start":{"line":26,"column":43},"end":{"line":37,"column":1}}}},"branchMap":{"1":{"loc":{"start":{"line":30,"column":4},"end":{"line":35,"column":5}},"type":"if","locations":[{"start":{"line":30,"column":4},"end":{"line":35,"column":5}},{"start":{"line":30,"column":4},"end":{"line":35,"column":5}}]},"2":{"loc":{"start":{"line":30,"column":8},"end":{"line":30,"column":37}},"type":"binary-expr","locations":[{"start":{"line":30,"column":8},"end":{"line":30,"column":13}},{"start":{"line":30,"column":17},"end":{"line":30,"column":37}}]}},"s":{"1":42329,"2":42329,"3":38978,"4":38978,"5":738,"6":816,"7":816,"8":816,"9":78,"10":78,"11":738},"f":{"1":42329,"2":38978,"3":738},"b":{"1":[78,738],"2":[816,109]},"hash":"e8e2e15730fb780657a053d74f59ca2ab68a2e93"} +,"/home/travis/build/babel/babylon/src/util/whitespace.js": {"path":"/home/travis/build/babel/babylon/src/util/whitespace.js","statementMap":{"1":{"start":{"line":4,"column":25},"end":{"line":4,"column":49}},"2":{"start":{"line":5,"column":26},"end":{"line":5,"column":59}},"3":{"start":{"line":8,"column":2},"end":{"line":8,"column":74}},"4":{"start":{"line":11,"column":34},"end":{"line":11,"column":87}}},"fnMap":{"1":{"name":"isNewLine","decl":{"start":{"line":7,"column":16},"end":{"line":7,"column":25}},"loc":{"start":{"line":7,"column":49},"end":{"line":9,"column":1}}}},"branchMap":{"1":{"loc":{"start":{"line":8,"column":9},"end":{"line":8,"column":73}},"type":"binary-expr","locations":[{"start":{"line":8,"column":9},"end":{"line":8,"column":20}},{"start":{"line":8,"column":24},"end":{"line":8,"column":35}},{"start":{"line":8,"column":39},"end":{"line":8,"column":54}},{"start":{"line":8,"column":58},"end":{"line":8,"column":73}}]}},"s":{"1":1,"2":1,"3":3360,"4":1},"f":{"1":3360},"b":{"1":[3360,3345,3344,3344]},"hash":"02e24302f8f6bd7929dab961722ce11e0e53b15c"} +} diff --git a/apps/worker/services/report/languages/tests/unit/node/node2-result.json b/apps/worker/services/report/languages/tests/unit/node/node2-result.json new file mode 100644 index 0000000000..8b780b1115 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/node/node2-result.json @@ -0,0 +1,136 @@ +{ + "archive": [ + "{}\n<<<<< end_of_header >>>>>\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n[13,\"m\",[[0,13]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[12,null,[[0,12,null,[[4,28,12]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[12,null,[[0,12,null,[[4,34,12]],null]]]\n\n\n[13,null,[[0,13,null,[[2,32,13]],null]]]\n[13,null,[[0,13,null,[[2,23,13]],null]]]\n[13,null,[[0,13,null,[],null]]]\n\n\n\n\n\n\n\n[13,\"m\",[[0,13]]]\n[6,null,[[0,6,null,[],null]]]\n\n\n\n\n[6,\"m\",[[0,6]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[8,25,1]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[3,null,[[0,3,null,[[8,28,3]],null]]]\n\n[3,null,[[0,3,null,[[6,13,3]],null]]]\n\n\n[6,\"m\",[[0,6]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[8,25,1]],null]]]\n\n\n[6,\"m\",[[0,6]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[10,42,1]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[10,43,1]],null]]]\n\n\n[4,null,[[0,4,null,[[8,30,4]],null]]]\n\n[4,null,[[0,4,null,[],null]]]\n[8,null,[[0,8,null,[[10,42,8]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[14,41,8],[45,71,6]],null]]]\n[4,null,[[0,4,null,[[12,48,4]],null]]]\n\n\n\n[4,\"m\",[[0,4]]]\n[4,null,[[0,4,null,[[10,17,4]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[12,29,1]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[3,null,[[0,3,null,[[12,45,3]],null]]]\n\n\n\n[4,null,[[0,4,null,[[8,39,4]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[10,43,1]],null]]]\n\n\n[3,null,[[0,3,null,[],null]]]\n[4,null,[[0,4,null,[[10,76,4]],null]]]\n\n\n\n\n\n\n\n\n[13,\"m\",[[0,13]]]\n[6,null,[[0,6,null,[],null]]]\n\n\n\n\n[6,\"m\",[[0,6]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[8,25,1]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[3,null,[[0,3,null,[[8,28,3]],null]]]\n\n[3,null,[[0,3,null,[[6,13,3]],null]]]\n\n\n\n[6,\"m\",[[0,6]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[8,40,1]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[8,41,1]],null]]]\n\n\n[4,null,[[0,4,null,[[6,34,4]],null]]]\n\n\n[4,null,[[0,4,null,[],null]]]\n[8,null,[[0,8,null,[[8,40,8]],null]]]\n[8,null,[[0,8,null,[[8,40,8]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[12,39,8],[43,69,7],[73,91,5]],null]]]\n[4,null,[[0,4,null,[[10,49,4]],null]]]\n\n\n\n[4,null,[[0,4,null,[[6,43,4]],null]]]\n\n[4,\"m\",[[0,4]]]\n[4,null,[[0,4,null,[[8,15,4]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[2,null,[[0,2,null,[[10,27,2]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[3,null,[[0,3,null,[[10,43,3]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[8,41,1]],null]]]\n\n\n[4,\"m\",[[0,4]]]\n[4,null,[[0,4,null,[],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[4,\"m\",[[0,4]]]\n[4,null,[[0,4,null,[],null]]]\n\n\n\n\n[3,null,[[0,3,null,[[6,65,3]],null]]]\n[3,null,[[0,3,null,[[6,74,3]],null]]]\n\n[3,null,[[0,3,null,[],null]]]\n[4,\"m\",[[0,4]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[12,37,1]],null]]]\n\n\n[3,\"m\",[[0,3]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[14,39,1]],null]]]\n\n[2,null,[[0,2,null,[[12,31,2]],null]]]\n\n\n\n\n\n\n[13,null,[[0,13,null,[[2,14,13]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1,null,[],null]]]\n\n\n[1,null,[[0,1,null,[],null]]]\n\n[1,null,[[0,1,null,[],null]]]\n\n[2,\"m\",[[0,2]]]\n[2,\"m\",[[0,2]]]\n[0,null,[[0,0,null,[],null]]]\n\n\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[8,18,0]],null]]]\n[0,null,[[0,0,null,[[8,15,0]],null]]]\n\n\n[0,null,[[0,0,null,[[6,23,0]],null]]]\n\n\n\n[2,\"m\",[[0,2]]]\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[8,18,0]],null]]]\n[0,null,[[0,0,null,[[8,15,0]],null]]]\n\n\n[0,null,[[0,0,null,[[6,32,0]],null]]]\n[0,null,[[0,0,null,[],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[],null]]]\n\n\n\n\n[0,null,[[0,0,null,[[6,25,0]],null]]]\n\n\n\n[2,\"m\",[[0,2]]]\n[0,null,[[0,0,null,[],null]]]\n\n\n\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[8,18,0]],null]]]\n[0,null,[[0,0,null,[[8,15,0]],null]]]\n\n\n[0,null,[[0,0,null,[[6,28,0]],null]]]\n[0,null,[[0,0,null,[],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[[11,27,0],[31,79,0]],null]]]\n[0,null,[[0,0,null,[[10,19,0]],null]]]\n\n\n[0,null,[[0,0,null,[],null]]]\n\n\n\n\n[0,null,[[0,0,null,[[6,23,0]],null]]]\n\n\n\n[2,\"m\",[[0,2]]]\n[0,null,[[0,0,null,[],null]]]\n\n\n\n\n\n\n\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[8,25,0]],null]]]\n\n[0,null,[[0,0,null,[[6,13,0]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1,null,[],null]]]\n\n[1,null,[[0,1,null,[],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n[0,\"m\",[[0,0]]]\n[0,null,[[0,0,null,[[6,54,0]],null]]]\n\n[0,\"m\",[[0,0]]]\n[0,null,[[0,0,null,[[6,60,0]],null]]]\n\n\n\n[1,null,[[0,1,null,[],null]]]\n\n\n[2,\"m\",[[0,2]]]\n[2,null,[[0,2,null,[[2,21,2]],null]]]\n\n[2,null,[[0,2,null,[[2,18,2]],null]]]\n\n[2,\"m\",[[0,2]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[6,53,0]],null]]]\n\n\n[0,null,[[0,0,null,[[4,23,0]],null]]]\n\n\n[2,\"m\",[[0,2]]]\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[8,18,0]],null]]]\n[0,null,[[0,0,null,[[8,15,0]],null]]]\n\n\n[0,null,[[0,0,null,[[6,32,0]],null]]]\n[0,null,[[0,0,null,[],null]]]\n\n\n\n\n\n\n\n\n[0,null,[[0,0,null,[],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[10,41,0]],null]]]\n[0,null,[[0,0,null,[[10,41,0]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[12,41,0]],null]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[12,42,0]],null]]]\n\n\n\n[0,null,[[0,0,null,[[6,28,0]],null]]]\n\n\n\n[2,\"m\",[[0,2]]]\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[8,18,0]],null]]]\n[0,null,[[0,0,null,[[8,15,0]],null]]]\n\n\n[0,null,[[0,0,null,[],null]]]\n\n[0,\"m\",[[0,0]]]\n[0,null,[[0,0,null,[[8,18,0]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[10,35,0]],null]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[10,36,0]],null]]]\n\n\n\n[0,null,[[0,0,null,[],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[10,72,0]],null]]]\n\n\n\n\n\n[2,\"m\",[[0,2]]]\n[0,null,[[0,0,null,[[4,28,0]],null]]]\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[8,18,0]],null]]]\n[0,null,[[0,0,null,[[8,15,0]],null]]]\n\n\n[0,null,[[0,0,null,[[6,76,0]],null]]]\n[0,null,[[0,0,null,[],null]]]\n\n\n\n\n\n[0,null,[[0,0,null,[],null]]]\n\n\n[0,\"m\",[[0,0]]]\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[14,24,0]],null]]]\n[0,null,[[0,0,null,[[14,21,0]],null]]]\n\n\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[16,26,0]],null]]]\n[0,null,[[0,0,null,[[16,23,0]],null]]]\n\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[18,28,0]],null]]]\n[0,null,[[0,0,null,[[18,25,0]],null]]]\n\n[0,null,[[0,0,null,[[16,40,0]],null]]]\n\n\n\n\n\n\n\n[2,\"m\",[[0,2]]]\n[0,null,[[0,0,null,[[4,63,0]],null]]]\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[8,18,0]],null]]]\n[0,null,[[0,0,null,[[8,15,0]],null]]]\n\n[0,null,[[0,0,null,[[6,29,0]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1,null,[],null]]]\n\n\n\n[4,\"m\",[[0,4]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[3,null,[[0,3,null,[[4,34,3]],null]]]\n\n\n[4,null,[[0,4,null,[[2,32,4]],null]]]\n[4,null,[[0,4,null,[[2,23,4]],null]]]\n[4,null,[[0,4,null,[],null]]]\n\n\n\n[4,\"m\",[[0,4]]]\n[3,null,[[0,3,null,[],null]]]\n\n\n\n[3,\"m\",[[0,3]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[8,25,1]],null]]]\n\n[2,\"m\",[[0,2]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[10,27,1]],null]]]\n\n[1,null,[[0,1,null,[[8,15,1]],null]]]\n\n\n\n\n[4,null,[[0,4,null,[[2,14,4]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n[1,null,[[0,1,null,[],null]]]\n\n[5,\"m\",[[0,5]]]\n[5,null,[[0,5,null,[[2,35,5]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[4,26,1]],null]]]\n\n\n[5,null,[[0,5,null,[[2,43,5]],null]]]\n[5,\"m\",[[0,5]]]\n[2,null,[[0,2,null,[],null]]]\n[2,\"b\",[[0,2]]]\n[2,null,[[0,2,null,[[8,43,2]],null]]]\n\n[0,\"b\",[[0,0]]]\n[0,null,[[0,0,null,[[8,53,0]],null]]]\n\n\n\n\n[5,null,[[0,5,null,[],null]]]\n\n\n\n\n\n\n[5,\"m\",[[0,5]]]\n[2,null,[[0,2,null,[[4,89,2]],null]]]\n[2,null,[[0,2,null,[],null]]]\n[2,\"m\",[[0,2]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[],null]]]\n[0,null,[[0,0,null,[[10,27,0]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[10,58,1]],null]]]\n\n\n[1,null,[[0,1,null,[[8,15,1]],null]]]\n\n\n\n\n[5,\"m\",[[0,5]]]\n[2,null,[[0,2,null,[[4,89,2]],null]]]\n[2,null,[[0,2,null,[],null]]]\n[2,\"m\",[[0,2]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[],null]]]\n[0,null,[[0,0,null,[[10,27,0]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[10,58,1]],null]]]\n\n\n[1,null,[[0,1,null,[[8,25,1]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n[8,\"m\",[[0,8]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[7,null,[[0,7,null,[[4,20,7]],null]]]\n\n\n[8,null,[[0,8,null,[[2,32,8]],null]]]\n[8,null,[[0,8,null,[[2,21,8]],null]]]\n[8,null,[[0,8,null,[],null]]]\n\n\n\n[8,\"m\",[[0,8]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[6,41,1]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[6,50,1]],null]]]\n\n\n[5,null,[[0,5,null,[[4,40,5]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[7,24,5],[28,45,5]],null]]]\n[1,null,[[0,1,null,[[6,50,1]],null]]]\n\n\n[4,null,[[0,4,null,[[4,44,4]],null]]]\n[4,\"m\",[[0,4]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[8,52,1]],null]]]\n\n\n[3,\"m\",[[0,3]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[10,101,1]],null]]]\n\n\n[2,\"m\",[[0,2]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[12,65,1]],null]]]\n\n[1,null,[[0,1,null,[[10,17,1]],null]]]\n\n\n\n\n\n[8,null,[[0,8,null,[[2,14,8]],null]]]\n\n\n[5,\"m\",[[0,5]]]\n[5,null,[[0,5,null,[[2,8,5]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[4,17,1]],null]]]\n\n\n[4,null,[[0,4,null,[[2,24,4]],null]]]\n[4,null,[[0,4,null,[[2,23,4]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1,null,[],null]]]\n\n\n\n[1,\"m\",[[0,1]]]\n[1,\"m\",[[0,1]]]\n[0,null,[[0,0,null,[],null]]]\n[0,\"m\",[[0,0]]]\n[0,null,[[0,0,null,[[8,30,0]],null]]]\n\n[0,\"m\",[[0,0]]]\n[0,null,[[0,0,null,[[8,18,0]],null]]]\n\n\n\n[1,\"m\",[[0,1]]]\n[0,\"m\",[[0,0]]]\n[0,null,[[0,0,null,[],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[10,40,0]],null]]]\n[0,null,[[0,0,null,[[10,17,0]],null]]]\n\n\n[0,null,[[0,0,null,[[6,35,0]],null]]]\n\n\n\n[1,\"m\",[[0,1]]]\n[0,null,[[0,0,null,[],null]]]\n[0,\"m\",[[0,0]]]\n[0,null,[[0,0,null,[[8,38,0]],null]]]\n\n[0,\"m\",[[0,0]]]\n[0,null,[[0,0,null,[[8,18,0]],null]]]\n\n\n\n[1,\"m\",[[0,1]]]\n[0,null,[[0,0,null,[],null]]]\n[0,\"m\",[[0,0]]]\n[0,null,[[0,0,null,[[8,19,0]],null]]]\n\n[0,\"m\",[[0,0]]]\n[0,null,[[0,0,null,[[8,18,0]],null]]]\n", + "\n{\"present_sessions\":[0]}\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n\n[1,null,[[0,1,null,[],null]]]\n\n\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n\n\n\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n\n[1,null,[[0,1,null,[],null]]]\n\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n[1,\"m\",[[0,1]]]\n[0,null,[[0,0,null,[[2,35,0]],null]]]\n[0,null,[[0,0,null,[[2,19,0]],null]]]\n[0,null,[[0,0,null,[[2,12,0]],null]]]\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",[\"1\"],[],null]]]\n[1,\"m\",[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[[15,25,1],[29,32,1]],null]]]\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n\n\n\n[1,\"m\",[[0,1]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[[13,23,0],[27,30,0]],null]]]\n[0,null,[[0,0,null,[],null]]]\n\n\n\n\n\n\n[1,null,[[0,1,null,[],null]]]\n", + "\n{\"present_sessions\":[0]}\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n\n\n[1,\"m\",[[0,1]]]\n[1,null,[[0,1,null,[[2,26,1]],null]]]\n\n\n[1,null,[[0,1,null,[],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,\"m\",[[0,1]]]\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[6,16,0]],null]]]\n[0,null,[[0,0,null,[[6,13,0]],null]]]\n\n\n[0,null,[[0,0,null,[[4,25,0]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,\"m\",[[0,1]]]\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[6,16,0]],null]]]\n[0,null,[[0,0,null,[[6,13,0]],null]]]\n\n\n[0,null,[[0,0,null,[[4,19,0]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,\"m\",[[0,1]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[[6,25,0],[29,54,0]],null]]]\n[0,null,[[0,0,null,[[4,63,0]],null]]]\n[0,null,[[0,0,null,[[4,30,0]],null]]]\n[0,null,[[0,0,null,[[4,11,0]],null]]]\n\n\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[6,32,0]],null]]]\n[0,null,[[0,0,null,[[6,13,0]],null]]]\n\n\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[8,18,0]],null]]]\n[0,null,[[0,0,null,[[8,15,0]],null]]]\n\n\n[0,null,[[0,0,null,[[6,21,0]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n[1,\"m\",[[0,1]]]\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[6,16,0]],null]]]\n[0,null,[[0,0,null,[[6,13,0]],null]]]\n\n\n[0,null,[[0,0,null,[[4,27,0]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,\"m\",[[0,1]]]\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[6,16,0]],null]]]\n[0,null,[[0,0,null,[[6,13,0]],null]]]\n\n[0,null,[[0,0,null,[],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,\"m\",[[0,0]]]\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[10,36,0]],null]]]\n[0,null,[[0,0,null,[[10,17,0]],null]]]\n\n\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[12,22,0]],null]]]\n[0,null,[[0,0,null,[[12,19,0]],null]]]\n\n\n[0,null,[[0,0,null,[[10,25,0]],null]]]\n\n\n\n\n\n\n[1,null,[[0,1,null,[],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,\"m\",[[0,1]]]\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[6,16,0]],null]]]\n[0,null,[[0,0,null,[[6,13,0]],null]]]\n\n\n[0,null,[[0,0,null,[[4,21,0]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,\"m\",[[0,1]]]\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[6,16,0]],null]]]\n[0,null,[[0,0,null,[[6,13,0]],null]]]\n\n\n[0,null,[[0,0,null,[[4,20,0]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,\"m\",[[0,1]]]\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[6,16,0]],null]]]\n[0,null,[[0,0,null,[[6,13,0]],null]]]\n\n\n[0,null,[[0,0,null,[[4,19,0]],null]]]\n\n\n\n[1,null,[[0,1,null,[],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n\n\n[2,\"m\",[[0,2]]]\n[2,\"m\",[[0,2]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n[1,null,[[0,1,null,[[6,13,1]],null]]]\n\n\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n\n\n\n\n\n\n[1,\"m\",[[0,1]]]\n[1,\"m\",[[0,1]]]\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n\n\n\n\n\n[4,\"m\",[[0,4]]]\n[4,\"m\",[[0,4]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[[6,16,1]],null]]]\n[1,null,[[0,1,null,[[6,13,1]],null]]]\n\n\n[3,null,[[0,3,null,[[4,27,3]],null]]]\n\n\n\n\n\n\n\n\n[1,\"m\",[[0,1]]]\n[1,\"m\",[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",[\"0\"],[],null]]]\n[0,null,[[0,0,null,[[6,16,0]],null]]]\n[0,null,[[0,0,null,[[6,13,0]],null]]]\n\n\n[1,null,[[0,1,null,[[4,27,1]],null]]]\n\n\n\n\n\n[1,null,[[0,1,null,[],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n[1,null,[[0,1,null,[],null]]]\n\n\n\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n\n[1,\"m\",[[0,1]]]\n[1,null,[[0,1,null,[[2,24,1]],null]]]\n\n[1,null,[[0,1,null,[],null]]]\n[4,null,[[0,4,null,[],null]]]\n\n\n\n\n\n[1,null,[[0,1,null,[[2,27,1]],null]]]\n\n\n[2,\"m\",[[0,2]]]\n[2,null,[[0,2,null,[[2,24,2]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[],null]]]\n\n\n\n[1,null,[[0,1,null,[[4,11,1]],null]]]\n\n\n[1,null,[[0,1,null,[[2,47,1]],null]]]\n[1,null,[[0,1,null,[[2,9,1]],null]]]\n\n\n[4,\"m\",[[0,4]]]\n[\"2/2\",\"b\",[[0,\"2/2\",[],[],null]]]\n[1,null,[[0,1,null,[],null]]]\n\n\n\n[1,null,[[0,1,null,[[4,11,1]],null]]]\n\n\n[3,\"m\",[[0,3]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[6,23,0]],null]]]\n\n\n[3,null,[[0,3,null,[[2,13,3]],null]]]\n\n\n[1,\"m\",[[0,1]]]\n[1,null,[[0,1,null,[],null]]]\n[4,\"m\",[[0,4]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[8,25,0]],null]]]\n\n\n\n[1,null,[[0,1,null,[[2,13,1]],null]]]\n\n\n[1,null,[[0,1,null,[],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1,null,[],null]]]\n\n[1,\"m\",[[0,1]]]\n[1,null,[[0,1,null,[[2,32,1]],null]]]\n[1,null,[[0,1,null,[[2,23,1]],null]]]\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n\n\n[2,\"m\",[[0,2,null,[[2,45,1]],null]]]\n\n[1,\"m\",[[0,1,null,[[2,46,1]],null]]]\n\n[1,\"m\",[[0,1,null,[[2,42,1]],null]]]\n\n[1,\"m\",[[0,1,null,[[2,45,1]],null]]]\n\n[1,\"m\",[[0,1,null,[[2,45,1]],null]]]\n\n[1,null,[[0,1,null,[[2,14,1]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n[1,null,[[0,1,null,[],null]]]\n\n\n\n[1,null,[[0,1,null,[],null]]]\n[1,null,[[0,1,null,[],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,\"m\",[[0,1]]]\n[0,null,[[0,0,null,[[4,26,0]],null]]]\n[0,null,[[0,0,null,[[4,87,0]],null]]]\n\n[0,\"m\",[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[[12,22,0]],null]]]\n[0,null,[[0,0,null,[[12,19,0]],null]]]\n\n[0,null,[[0,0,null,[[8,32,0]],null]]]\n[0,null,[[0,0,null,[],null]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",[\"0\",\"1\"],[],null]]]\n[0,null,[[0,0,null,[],null]]]\n\n\n\n\n\n\n[0,null,[[0,0,null,[[8,23,0]],null]]]\n\n\n\n[1,null,[[0,1,null,[],null]]]" + ], + "totals": { + "f": 16, + "n": 503, + "h": 299, + "m": 200, + "p": 4, + "c": "59.44334", + "b": 84, + "d": 114, + "M": 0, + "s": 0, + "C": 0, + "N": 0, + "diff": null + }, + "report": { + "files": { + "/src/hooks/docker.js": [ + 0, + [0, 84, 84, 0, 0, "100", 21, 14, 0, 0, 0, 0, 0], + null, + null + ], + "/src/models/aws.js": [ + 1, + [0, 39, 8, 31, 0, "20.51282", 6, 9, 0, 0, 0, 0, 0], + null, + null + ], + "/src/config/default.js": [ + 2, + [0, 2, 2, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/src/models/docker.js": [ + 3, + [0, 74, 10, 64, 0, "13.51351", 14, 17, 0, 0, 0, 0, 0], + null, + null + ], + "/src/hooks/github.js": [ + 4, + [0, 17, 17, 0, 0, "100", 3, 4, 0, 0, 0, 0, 0], + null, + null + ], + "/src/models/github.js": [ + 5, + [0, 31, 25, 4, 2, "80.64516", 7, 6, 0, 0, 0, 0, 0], + null, + null + ], + "/src/hooks/jira.js": [ + 6, + [0, 33, 33, 0, 0, "100", 8, 6, 0, 0, 0, 0, 0], + null, + null + ], + "/src/models/jira.js": [ + 7, + [0, 27, 6, 21, 0, "22.22222", 1, 12, 0, 0, 0, 0, 0], + null, + null + ], + "/src/app.js": [ + 8, + [0, 35, 29, 5, 1, "82.85714", 3, 3, 0, 0, 0, 0, 0], + null, + null + ], + "/src/routes/index.js": [ + 9, + [0, 5, 5, 0, 0, "100", 0, 1, 0, 0, 0, 0, 0], + null, + null + ], + "/src/routes/containers.js": [ + 10, + [0, 50, 7, 43, 0, "14.00000", 9, 14, 0, 0, 0, 0, 0], + null, + null + ], + "/src/routes/images.js": [ + 11, + [0, 20, 5, 15, 0, "25.00000", 3, 6, 0, 0, 0, 0, 0], + null, + null + ], + "/src/routes/hooks.js": [ + 12, + [0, 25, 22, 2, 1, "88.00000", 3, 8, 0, 0, 0, 0, 0], + null, + null + ], + "/src/hooks/hook.js": [ + 13, + [0, 34, 30, 4, 0, "88.23529", 4, 6, 0, 0, 0, 0, 0], + null, + null + ], + "/src/hooks/sample.js": [ + 14, + [0, 11, 11, 0, 0, "100", 0, 6, 0, 0, 0, 0, 0], + null, + null + ], + "/src/routes/S3.js": [ + 15, + [0, 16, 5, 11, 0, "31.25000", 2, 2, 0, 0, 0, 0, 0], + null, + null + ] + }, + "sessions": {} + } +} diff --git a/apps/worker/services/report/languages/tests/unit/node/node2.json b/apps/worker/services/report/languages/tests/unit/node/node2.json new file mode 100644 index 0000000000..8000d00f60 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/node/node2.json @@ -0,0 +1 @@ +{"/src/hooks/docker.js":{"path":"/src/hooks/docker.js","s":{"1":1,"2":1,"3":13,"4":12,"5":13,"6":12,"7":13,"8":13,"9":13,"10":13,"11":6,"12":6,"13":6,"14":1,"15":6,"16":3,"17":3,"18":6,"19":6,"20":1,"21":6,"22":6,"23":1,"24":5,"25":1,"26":4,"27":4,"28":8,"29":8,"30":4,"31":4,"32":4,"33":4,"34":1,"35":4,"36":3,"37":4,"38":4,"39":1,"40":3,"41":4,"42":13,"43":6,"44":6,"45":6,"46":1,"47":6,"48":3,"49":3,"50":6,"51":6,"52":1,"53":5,"54":1,"55":4,"56":4,"57":8,"58":8,"59":8,"60":4,"61":4,"62":4,"63":4,"64":4,"65":2,"66":4,"67":3,"68":4,"69":1,"70":3,"71":4,"72":3,"73":4,"74":3,"75":3,"76":3,"77":4,"78":4,"79":1,"80":3,"81":3,"82":1,"83":2,"84":13},"b":{"1":[12,1],"2":[12,1],"3":[1,5],"4":[3,3],"5":[1,5],"6":[1,5],"7":[1,4],"8":[4,4],"9":[8,6],"10":[1,3],"11":[3,1],"12":[1,3],"13":[1,5],"14":[3,3],"15":[1,5],"16":[1,4],"17":[4,4],"18":[8,7,5],"19":[2,2],"20":[3,1],"21":[1,3],"22":[1,3],"23":[1,2]},"f":{"1":13,"2":6,"3":6,"4":6,"5":6,"6":4,"7":6,"8":6,"9":6,"10":4,"11":4,"12":4,"13":4,"14":3},"fnMap":{"1":{"name":"dockerHook","line":8,"loc":{"start":{"line":8,"column":17},"end":{"line":8,"column":68}}},"2":{"name":"(anonymous_2)","line":27,"loc":{"start":{"line":27,"column":26},"end":{"line":27,"column":48}}},"3":{"name":"(anonymous_3)","line":33,"loc":{"start":{"line":33,"column":24},"end":{"line":33,"column":45}}},"4":{"name":"(anonymous_4)","line":44,"loc":{"start":{"line":44,"column":40},"end":{"line":44,"column":55}}},"5":{"name":"(anonymous_5)","line":49,"loc":{"start":{"line":49,"column":32},"end":{"line":49,"column":59}}},"6":{"name":"(anonymous_6)","line":67,"loc":{"start":{"line":67,"column":27},"end":{"line":67,"column":42}}},"7":{"name":"(anonymous_7)","line":92,"loc":{"start":{"line":92,"column":26},"end":{"line":92,"column":48}}},"8":{"name":"(anonymous_8)","line":98,"loc":{"start":{"line":98,"column":24},"end":{"line":98,"column":45}}},"9":{"name":"(anonymous_9)","line":110,"loc":{"start":{"line":110,"column":30},"end":{"line":110,"column":57}}},"10":{"name":"(anonymous_10)","line":132,"loc":{"start":{"line":132,"column":25},"end":{"line":132,"column":40}}},"11":{"name":"(anonymous_11)","line":146,"loc":{"start":{"line":146,"column":27},"end":{"line":146,"column":48}}},"12":{"name":"(anonymous_12)","line":166,"loc":{"start":{"line":166,"column":31},"end":{"line":166,"column":52}}},"13":{"name":"(anonymous_13)","line":176,"loc":{"start":{"line":176,"column":69},"end":{"line":176,"column":89}}},"14":{"name":"(anonymous_14)","line":181,"loc":{"start":{"line":181,"column":56},"end":{"line":181,"column":75}}}},"statementMap":{"1":{"start":{"line":3,"column":0},"end":{"line":5,"column":50}},"2":{"start":{"line":8,"column":0},"end":{"line":193,"column":2}},"3":{"start":{"line":9,"column":2},"end":{"line":11,"column":3}},"4":{"start":{"line":10,"column":4},"end":{"line":10,"column":28}},"5":{"start":{"line":13,"column":2},"end":{"line":15,"column":3}},"6":{"start":{"line":14,"column":4},"end":{"line":14,"column":34}},"7":{"start":{"line":17,"column":2},"end":{"line":17,"column":32}},"8":{"start":{"line":18,"column":2},"end":{"line":18,"column":23}},"9":{"start":{"line":19,"column":2},"end":{"line":22,"column":4}},"10":{"start":{"line":27,"column":2},"end":{"line":87,"column":5}},"11":{"start":{"line":28,"column":4},"end":{"line":31,"column":20}},"12":{"start":{"line":33,"column":4},"end":{"line":42,"column":6}},"13":{"start":{"line":34,"column":6},"end":{"line":36,"column":7}},"14":{"start":{"line":35,"column":8},"end":{"line":35,"column":25}},"15":{"start":{"line":38,"column":6},"end":{"line":40,"column":7}},"16":{"start":{"line":39,"column":8},"end":{"line":39,"column":28}},"17":{"start":{"line":41,"column":6},"end":{"line":41,"column":13}},"18":{"start":{"line":44,"column":4},"end":{"line":86,"column":7}},"19":{"start":{"line":45,"column":6},"end":{"line":47,"column":7}},"20":{"start":{"line":46,"column":8},"end":{"line":46,"column":25}},"21":{"start":{"line":49,"column":6},"end":{"line":85,"column":9}},"22":{"start":{"line":50,"column":8},"end":{"line":52,"column":9}},"23":{"start":{"line":51,"column":10},"end":{"line":51,"column":42}},"24":{"start":{"line":54,"column":8},"end":{"line":56,"column":9}},"25":{"start":{"line":55,"column":10},"end":{"line":55,"column":43}},"26":{"start":{"line":58,"column":8},"end":{"line":58,"column":30}},"27":{"start":{"line":60,"column":8},"end":{"line":65,"column":9}},"28":{"start":{"line":61,"column":10},"end":{"line":61,"column":42}},"29":{"start":{"line":62,"column":10},"end":{"line":64,"column":11}},"30":{"start":{"line":63,"column":12},"end":{"line":63,"column":48}},"31":{"start":{"line":67,"column":8},"end":{"line":75,"column":10}},"32":{"start":{"line":68,"column":10},"end":{"line":68,"column":17}},"33":{"start":{"line":69,"column":10},"end":{"line":71,"column":11}},"34":{"start":{"line":70,"column":12},"end":{"line":70,"column":29}},"35":{"start":{"line":72,"column":10},"end":{"line":74,"column":11}},"36":{"start":{"line":73,"column":12},"end":{"line":73,"column":45}},"37":{"start":{"line":77,"column":8},"end":{"line":77,"column":39}},"38":{"start":{"line":78,"column":8},"end":{"line":80,"column":9}},"39":{"start":{"line":79,"column":10},"end":{"line":79,"column":43}},"40":{"start":{"line":82,"column":8},"end":{"line":84,"column":9}},"41":{"start":{"line":83,"column":10},"end":{"line":83,"column":76}},"42":{"start":{"line":92,"column":2},"end":{"line":190,"column":5}},"43":{"start":{"line":93,"column":4},"end":{"line":96,"column":20}},"44":{"start":{"line":98,"column":4},"end":{"line":107,"column":6}},"45":{"start":{"line":99,"column":6},"end":{"line":101,"column":7}},"46":{"start":{"line":100,"column":8},"end":{"line":100,"column":25}},"47":{"start":{"line":103,"column":6},"end":{"line":105,"column":7}},"48":{"start":{"line":104,"column":8},"end":{"line":104,"column":28}},"49":{"start":{"line":106,"column":6},"end":{"line":106,"column":13}},"50":{"start":{"line":110,"column":4},"end":{"line":189,"column":7}},"51":{"start":{"line":111,"column":6},"end":{"line":113,"column":7}},"52":{"start":{"line":112,"column":8},"end":{"line":112,"column":40}},"53":{"start":{"line":115,"column":6},"end":{"line":117,"column":7}},"54":{"start":{"line":116,"column":8},"end":{"line":116,"column":41}},"55":{"start":{"line":119,"column":6},"end":{"line":119,"column":34}},"56":{"start":{"line":122,"column":6},"end":{"line":128,"column":7}},"57":{"start":{"line":123,"column":8},"end":{"line":123,"column":40}},"58":{"start":{"line":124,"column":8},"end":{"line":124,"column":40}},"59":{"start":{"line":125,"column":8},"end":{"line":127,"column":9}},"60":{"start":{"line":126,"column":10},"end":{"line":126,"column":49}},"61":{"start":{"line":130,"column":6},"end":{"line":130,"column":43}},"62":{"start":{"line":132,"column":6},"end":{"line":140,"column":8}},"63":{"start":{"line":133,"column":8},"end":{"line":133,"column":15}},"64":{"start":{"line":134,"column":8},"end":{"line":136,"column":9}},"65":{"start":{"line":135,"column":10},"end":{"line":135,"column":27}},"66":{"start":{"line":137,"column":8},"end":{"line":139,"column":9}},"67":{"start":{"line":138,"column":10},"end":{"line":138,"column":43}},"68":{"start":{"line":142,"column":6},"end":{"line":144,"column":7}},"69":{"start":{"line":143,"column":8},"end":{"line":143,"column":41}},"70":{"start":{"line":146,"column":6},"end":{"line":164,"column":8}},"71":{"start":{"line":147,"column":8},"end":{"line":163,"column":10}},"72":{"start":{"line":166,"column":6},"end":{"line":170,"column":8}},"73":{"start":{"line":167,"column":8},"end":{"line":169,"column":10}},"74":{"start":{"line":172,"column":6},"end":{"line":172,"column":65}},"75":{"start":{"line":173,"column":6},"end":{"line":173,"column":74}},"76":{"start":{"line":175,"column":6},"end":{"line":188,"column":7}},"77":{"start":{"line":176,"column":8},"end":{"line":187,"column":11}},"78":{"start":{"line":177,"column":10},"end":{"line":179,"column":11}},"79":{"start":{"line":178,"column":12},"end":{"line":178,"column":37}},"80":{"start":{"line":181,"column":10},"end":{"line":186,"column":13}},"81":{"start":{"line":182,"column":12},"end":{"line":184,"column":13}},"82":{"start":{"line":183,"column":14},"end":{"line":183,"column":39}},"83":{"start":{"line":185,"column":12},"end":{"line":185,"column":31}},"84":{"start":{"line":192,"column":2},"end":{"line":192,"column":14}}},"branchMap":{"1":{"line":9,"type":"if","locations":[{"start":{"line":9,"column":2},"end":{"line":9,"column":2}},{"start":{"line":9,"column":2},"end":{"line":9,"column":2}}]},"2":{"line":13,"type":"if","locations":[{"start":{"line":13,"column":2},"end":{"line":13,"column":2}},{"start":{"line":13,"column":2},"end":{"line":13,"column":2}}]},"3":{"line":34,"type":"if","locations":[{"start":{"line":34,"column":6},"end":{"line":34,"column":6}},{"start":{"line":34,"column":6},"end":{"line":34,"column":6}}]},"4":{"line":38,"type":"if","locations":[{"start":{"line":38,"column":6},"end":{"line":38,"column":6}},{"start":{"line":38,"column":6},"end":{"line":38,"column":6}}]},"5":{"line":45,"type":"if","locations":[{"start":{"line":45,"column":6},"end":{"line":45,"column":6}},{"start":{"line":45,"column":6},"end":{"line":45,"column":6}}]},"6":{"line":50,"type":"if","locations":[{"start":{"line":50,"column":8},"end":{"line":50,"column":8}},{"start":{"line":50,"column":8},"end":{"line":50,"column":8}}]},"7":{"line":54,"type":"if","locations":[{"start":{"line":54,"column":8},"end":{"line":54,"column":8}},{"start":{"line":54,"column":8},"end":{"line":54,"column":8}}]},"8":{"line":62,"type":"if","locations":[{"start":{"line":62,"column":10},"end":{"line":62,"column":10}},{"start":{"line":62,"column":10},"end":{"line":62,"column":10}}]},"9":{"line":62,"type":"binary-expr","locations":[{"start":{"line":62,"column":14},"end":{"line":62,"column":41}},{"start":{"line":62,"column":45},"end":{"line":62,"column":71}}]},"10":{"line":69,"type":"if","locations":[{"start":{"line":69,"column":10},"end":{"line":69,"column":10}},{"start":{"line":69,"column":10},"end":{"line":69,"column":10}}]},"11":{"line":72,"type":"if","locations":[{"start":{"line":72,"column":10},"end":{"line":72,"column":10}},{"start":{"line":72,"column":10},"end":{"line":72,"column":10}}]},"12":{"line":78,"type":"if","locations":[{"start":{"line":78,"column":8},"end":{"line":78,"column":8}},{"start":{"line":78,"column":8},"end":{"line":78,"column":8}}]},"13":{"line":99,"type":"if","locations":[{"start":{"line":99,"column":6},"end":{"line":99,"column":6}},{"start":{"line":99,"column":6},"end":{"line":99,"column":6}}]},"14":{"line":103,"type":"if","locations":[{"start":{"line":103,"column":6},"end":{"line":103,"column":6}},{"start":{"line":103,"column":6},"end":{"line":103,"column":6}}]},"15":{"line":111,"type":"if","locations":[{"start":{"line":111,"column":6},"end":{"line":111,"column":6}},{"start":{"line":111,"column":6},"end":{"line":111,"column":6}}]},"16":{"line":115,"type":"if","locations":[{"start":{"line":115,"column":6},"end":{"line":115,"column":6}},{"start":{"line":115,"column":6},"end":{"line":115,"column":6}}]},"17":{"line":125,"type":"if","locations":[{"start":{"line":125,"column":8},"end":{"line":125,"column":8}},{"start":{"line":125,"column":8},"end":{"line":125,"column":8}}]},"18":{"line":125,"type":"binary-expr","locations":[{"start":{"line":125,"column":12},"end":{"line":125,"column":39}},{"start":{"line":125,"column":43},"end":{"line":125,"column":69}},{"start":{"line":125,"column":73},"end":{"line":125,"column":91}}]},"19":{"line":134,"type":"if","locations":[{"start":{"line":134,"column":8},"end":{"line":134,"column":8}},{"start":{"line":134,"column":8},"end":{"line":134,"column":8}}]},"20":{"line":137,"type":"if","locations":[{"start":{"line":137,"column":8},"end":{"line":137,"column":8}},{"start":{"line":137,"column":8},"end":{"line":137,"column":8}}]},"21":{"line":142,"type":"if","locations":[{"start":{"line":142,"column":6},"end":{"line":142,"column":6}},{"start":{"line":142,"column":6},"end":{"line":142,"column":6}}]},"22":{"line":177,"type":"if","locations":[{"start":{"line":177,"column":10},"end":{"line":177,"column":10}},{"start":{"line":177,"column":10},"end":{"line":177,"column":10}}]},"23":{"line":182,"type":"if","locations":[{"start":{"line":182,"column":12},"end":{"line":182,"column":12}},{"start":{"line":182,"column":12},"end":{"line":182,"column":12}}]}}},"/src/models/aws.js":{"path":"/src/models/aws.js","s":{"1":1,"2":1,"3":1,"4":1,"5":2,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":2,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":2,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,"30":0,"31":0,"32":0,"33":0,"34":2,"35":0,"36":0,"37":0,"38":0,"39":0},"b":{"1":[0,0],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0],"6":[0,0],"7":[0,0]},"f":{"1":2,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0},"fnMap":{"1":{"name":"AWSModel","line":10,"loc":{"start":{"line":10,"column":17},"end":{"line":10,"column":37}}},"2":{"name":"(anonymous_2)","line":11,"loc":{"start":{"line":11,"column":21},"end":{"line":11,"column":43}}},"3":{"name":"(anonymous_3)","line":15,"loc":{"start":{"line":15,"column":33},"end":{"line":15,"column":53}}},"4":{"name":"(anonymous_4)","line":25,"loc":{"start":{"line":25,"column":20},"end":{"line":25,"column":35}}},"5":{"name":"(anonymous_5)","line":26,"loc":{"start":{"line":26,"column":29},"end":{"line":26,"column":49}}},"6":{"name":"(anonymous_6)","line":44,"loc":{"start":{"line":44,"column":18},"end":{"line":44,"column":58}}},"7":{"name":"(anonymous_7)","line":49,"loc":{"start":{"line":49,"column":27},"end":{"line":49,"column":46}}},"8":{"name":"(anonymous_8)","line":70,"loc":{"start":{"line":70,"column":19},"end":{"line":70,"column":46}}},"9":{"name":"(anonymous_9)","line":79,"loc":{"start":{"line":79,"column":33},"end":{"line":79,"column":53}}}},"statementMap":{"1":{"start":{"line":3,"column":0},"end":{"line":4,"column":27}},"2":{"start":{"line":6,"column":0},"end":{"line":6,"column":49}},"3":{"start":{"line":8,"column":0},"end":{"line":8,"column":24}},"4":{"start":{"line":10,"column":0},"end":{"line":86,"column":2}},"5":{"start":{"line":11,"column":2},"end":{"line":23,"column":4}},"6":{"start":{"line":12,"column":4},"end":{"line":14,"column":6}},"7":{"start":{"line":15,"column":4},"end":{"line":22,"column":7}},"8":{"start":{"line":16,"column":6},"end":{"line":19,"column":7}},"9":{"start":{"line":17,"column":8},"end":{"line":17,"column":18}},"10":{"start":{"line":18,"column":8},"end":{"line":18,"column":15}},"11":{"start":{"line":21,"column":6},"end":{"line":21,"column":23}},"12":{"start":{"line":25,"column":2},"end":{"line":42,"column":4}},"13":{"start":{"line":26,"column":4},"end":{"line":41,"column":7}},"14":{"start":{"line":27,"column":6},"end":{"line":30,"column":7}},"15":{"start":{"line":28,"column":8},"end":{"line":28,"column":18}},"16":{"start":{"line":29,"column":8},"end":{"line":29,"column":15}},"17":{"start":{"line":32,"column":6},"end":{"line":32,"column":32}},"18":{"start":{"line":33,"column":6},"end":{"line":39,"column":7}},"19":{"start":{"line":34,"column":8},"end":{"line":38,"column":9}},"20":{"start":{"line":35,"column":10},"end":{"line":37,"column":13}},"21":{"start":{"line":40,"column":6},"end":{"line":40,"column":25}},"22":{"start":{"line":44,"column":2},"end":{"line":68,"column":4}},"23":{"start":{"line":45,"column":4},"end":{"line":47,"column":6}},"24":{"start":{"line":49,"column":4},"end":{"line":67,"column":7}},"25":{"start":{"line":50,"column":6},"end":{"line":53,"column":7}},"26":{"start":{"line":51,"column":8},"end":{"line":51,"column":18}},"27":{"start":{"line":52,"column":8},"end":{"line":52,"column":15}},"28":{"start":{"line":55,"column":6},"end":{"line":55,"column":28}},"29":{"start":{"line":56,"column":6},"end":{"line":65,"column":7}},"30":{"start":{"line":57,"column":8},"end":{"line":59,"column":9}},"31":{"start":{"line":58,"column":10},"end":{"line":58,"column":19}},"32":{"start":{"line":61,"column":8},"end":{"line":64,"column":11}},"33":{"start":{"line":66,"column":6},"end":{"line":66,"column":23}},"34":{"start":{"line":70,"column":2},"end":{"line":85,"column":4}},"35":{"start":{"line":71,"column":4},"end":{"line":78,"column":6}},"36":{"start":{"line":79,"column":4},"end":{"line":84,"column":7}},"37":{"start":{"line":80,"column":6},"end":{"line":82,"column":7}},"38":{"start":{"line":81,"column":8},"end":{"line":81,"column":25}},"39":{"start":{"line":83,"column":6},"end":{"line":83,"column":13}}},"branchMap":{"1":{"line":16,"type":"if","locations":[{"start":{"line":16,"column":6},"end":{"line":16,"column":6}},{"start":{"line":16,"column":6},"end":{"line":16,"column":6}}]},"2":{"line":27,"type":"if","locations":[{"start":{"line":27,"column":6},"end":{"line":27,"column":6}},{"start":{"line":27,"column":6},"end":{"line":27,"column":6}}]},"3":{"line":34,"type":"if","locations":[{"start":{"line":34,"column":8},"end":{"line":34,"column":8}},{"start":{"line":34,"column":8},"end":{"line":34,"column":8}}]},"4":{"line":50,"type":"if","locations":[{"start":{"line":50,"column":6},"end":{"line":50,"column":6}},{"start":{"line":50,"column":6},"end":{"line":50,"column":6}}]},"5":{"line":57,"type":"if","locations":[{"start":{"line":57,"column":8},"end":{"line":57,"column":8}},{"start":{"line":57,"column":8},"end":{"line":57,"column":8}}]},"6":{"line":57,"type":"binary-expr","locations":[{"start":{"line":57,"column":11},"end":{"line":57,"column":27}},{"start":{"line":57,"column":31},"end":{"line":57,"column":79}}]},"7":{"line":80,"type":"if","locations":[{"start":{"line":80,"column":6},"end":{"line":80,"column":6}},{"start":{"line":80,"column":6},"end":{"line":80,"column":6}}]}}},"/src/config/default.js":{"path":"/src/config/default.js","s":{"1":1,"2":1},"b":{},"f":{},"fnMap":{},"statementMap":{"1":{"start":{"line":3,"column":0},"end":{"line":3,"column":23}},"2":{"start":{"line":5,"column":0},"end":{"line":36,"column":2}}},"branchMap":{}},"/src/models/docker.js":{"path":"/src/models/docker.js","s":{"1":1,"2":0,"3":0,"4":1,"5":1,"6":2,"7":2,"8":2,"9":0,"10":0,"11":0,"12":2,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":2,"29":0,"30":0,"31":0,"32":0,"33":0,"34":0,"35":0,"36":0,"37":0,"38":0,"39":0,"40":0,"41":0,"42":2,"43":0,"44":0,"45":0,"46":0,"47":0,"48":0,"49":0,"50":0,"51":0,"52":0,"53":0,"54":0,"55":0,"56":0,"57":0,"58":0,"59":0,"60":0,"61":0,"62":0,"63":0,"64":2,"65":0,"66":0,"67":0,"68":0,"69":0,"70":0},"b":{"1":[0,0],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0],"6":[0,0],"7":[0,0],"8":[0,0],"9":[0,0],"10":[0,0],"11":[0,0],"12":[0,0],"13":[0,0],"14":[0,0]},"f":{"1":0,"2":0,"3":2,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0},"fnMap":{"1":{"name":"(anonymous_1)","line":9,"loc":{"start":{"line":9,"column":12},"end":{"line":9,"column":33}}},"2":{"name":"(anonymous_2)","line":12,"loc":{"start":{"line":12,"column":12},"end":{"line":12,"column":31}}},"3":{"name":"DockerModel","line":20,"loc":{"start":{"line":20,"column":17},"end":{"line":20,"column":40}}},"4":{"name":"(anonymous_4)","line":25,"loc":{"start":{"line":25,"column":19},"end":{"line":25,"column":30}}},"5":{"name":"(anonymous_5)","line":33,"loc":{"start":{"line":33,"column":26},"end":{"line":33,"column":46}}},"6":{"name":"(anonymous_6)","line":34,"loc":{"start":{"line":34,"column":46},"end":{"line":34,"column":66}}},"7":{"name":"(anonymous_7)","line":67,"loc":{"start":{"line":67,"column":23},"end":{"line":67,"column":38}}},"8":{"name":"(anonymous_8)","line":68,"loc":{"start":{"line":68,"column":42},"end":{"line":68,"column":69}}},"9":{"name":"(anonymous_9)","line":76,"loc":{"start":{"line":76,"column":31},"end":{"line":76,"column":51}}},"10":{"name":"(anonymous_10)","line":95,"loc":{"start":{"line":95,"column":25},"end":{"line":95,"column":55}}},"11":{"name":"(anonymous_11)","line":97,"loc":{"start":{"line":97,"column":34},"end":{"line":97,"column":54}}},"12":{"name":"(anonymous_12)","line":113,"loc":{"start":{"line":113,"column":8},"end":{"line":113,"column":30}}},"13":{"name":"(anonymous_13)","line":114,"loc":{"start":{"line":114,"column":56},"end":{"line":114,"column":70}}},"14":{"name":"(anonymous_14)","line":120,"loc":{"start":{"line":120,"column":60},"end":{"line":120,"column":85}}},"15":{"name":"(anonymous_15)","line":125,"loc":{"start":{"line":125,"column":30},"end":{"line":125,"column":44}}},"16":{"name":"(anonymous_16)","line":138,"loc":{"start":{"line":138,"column":32},"end":{"line":138,"column":60}}},"17":{"name":"(anonymous_17)","line":140,"loc":{"start":{"line":140,"column":23},"end":{"line":140,"column":37}}}},"statementMap":{"1":{"start":{"line":3,"column":0},"end":{"line":15,"column":4}},"2":{"start":{"line":10,"column":6},"end":{"line":10,"column":54}},"3":{"start":{"line":13,"column":6},"end":{"line":13,"column":60}},"4":{"start":{"line":17,"column":0},"end":{"line":17,"column":49}},"5":{"start":{"line":20,"column":0},"end":{"line":149,"column":2}},"6":{"start":{"line":21,"column":2},"end":{"line":21,"column":21}},"7":{"start":{"line":23,"column":2},"end":{"line":23,"column":18}},"8":{"start":{"line":25,"column":2},"end":{"line":31,"column":4}},"9":{"start":{"line":26,"column":4},"end":{"line":28,"column":5}},"10":{"start":{"line":27,"column":6},"end":{"line":27,"column":53}},"11":{"start":{"line":30,"column":4},"end":{"line":30,"column":23}},"12":{"start":{"line":33,"column":2},"end":{"line":65,"column":4}},"13":{"start":{"line":34,"column":4},"end":{"line":64,"column":7}},"14":{"start":{"line":35,"column":6},"end":{"line":38,"column":7}},"15":{"start":{"line":36,"column":8},"end":{"line":36,"column":18}},"16":{"start":{"line":37,"column":8},"end":{"line":37,"column":15}},"17":{"start":{"line":40,"column":6},"end":{"line":40,"column":32}},"18":{"start":{"line":41,"column":6},"end":{"line":48,"column":8}},"19":{"start":{"line":50,"column":6},"end":{"line":62,"column":7}},"20":{"start":{"line":51,"column":8},"end":{"line":61,"column":9}},"21":{"start":{"line":52,"column":10},"end":{"line":52,"column":41}},"22":{"start":{"line":53,"column":10},"end":{"line":53,"column":41}},"23":{"start":{"line":54,"column":10},"end":{"line":56,"column":11}},"24":{"start":{"line":55,"column":12},"end":{"line":55,"column":41}},"25":{"start":{"line":58,"column":10},"end":{"line":60,"column":11}},"26":{"start":{"line":59,"column":12},"end":{"line":59,"column":42}},"27":{"start":{"line":63,"column":6},"end":{"line":63,"column":28}},"28":{"start":{"line":67,"column":2},"end":{"line":93,"column":4}},"29":{"start":{"line":68,"column":4},"end":{"line":92,"column":7}},"30":{"start":{"line":69,"column":6},"end":{"line":72,"column":7}},"31":{"start":{"line":70,"column":8},"end":{"line":70,"column":18}},"32":{"start":{"line":71,"column":8},"end":{"line":71,"column":15}},"33":{"start":{"line":74,"column":6},"end":{"line":85,"column":8}},"34":{"start":{"line":77,"column":8},"end":{"line":77,"column":18}},"35":{"start":{"line":78,"column":8},"end":{"line":80,"column":9}},"36":{"start":{"line":79,"column":10},"end":{"line":79,"column":35}},"37":{"start":{"line":82,"column":8},"end":{"line":84,"column":9}},"38":{"start":{"line":83,"column":10},"end":{"line":83,"column":36}},"39":{"start":{"line":87,"column":6},"end":{"line":91,"column":7}},"40":{"start":{"line":88,"column":8},"end":{"line":90,"column":9}},"41":{"start":{"line":89,"column":10},"end":{"line":89,"column":72}},"42":{"start":{"line":95,"column":2},"end":{"line":136,"column":4}},"43":{"start":{"line":96,"column":4},"end":{"line":96,"column":28}},"44":{"start":{"line":97,"column":4},"end":{"line":135,"column":7}},"45":{"start":{"line":98,"column":6},"end":{"line":101,"column":7}},"46":{"start":{"line":99,"column":8},"end":{"line":99,"column":18}},"47":{"start":{"line":100,"column":8},"end":{"line":100,"column":15}},"48":{"start":{"line":103,"column":6},"end":{"line":103,"column":76}},"49":{"start":{"line":104,"column":6},"end":{"line":108,"column":8}},"50":{"start":{"line":110,"column":6},"end":{"line":134,"column":11}},"51":{"start":{"line":114,"column":10},"end":{"line":133,"column":13}},"52":{"start":{"line":115,"column":12},"end":{"line":118,"column":13}},"53":{"start":{"line":116,"column":14},"end":{"line":116,"column":24}},"54":{"start":{"line":117,"column":14},"end":{"line":117,"column":21}},"55":{"start":{"line":120,"column":12},"end":{"line":132,"column":15}},"56":{"start":{"line":121,"column":14},"end":{"line":124,"column":15}},"57":{"start":{"line":122,"column":16},"end":{"line":122,"column":26}},"58":{"start":{"line":123,"column":16},"end":{"line":123,"column":23}},"59":{"start":{"line":125,"column":14},"end":{"line":131,"column":17}},"60":{"start":{"line":126,"column":16},"end":{"line":129,"column":17}},"61":{"start":{"line":127,"column":18},"end":{"line":127,"column":28}},"62":{"start":{"line":128,"column":18},"end":{"line":128,"column":25}},"63":{"start":{"line":130,"column":16},"end":{"line":130,"column":40}},"64":{"start":{"line":138,"column":2},"end":{"line":148,"column":4}},"65":{"start":{"line":139,"column":4},"end":{"line":139,"column":63}},"66":{"start":{"line":140,"column":4},"end":{"line":146,"column":7}},"67":{"start":{"line":141,"column":6},"end":{"line":144,"column":7}},"68":{"start":{"line":142,"column":8},"end":{"line":142,"column":18}},"69":{"start":{"line":143,"column":8},"end":{"line":143,"column":15}},"70":{"start":{"line":145,"column":6},"end":{"line":145,"column":29}}},"branchMap":{"1":{"line":26,"type":"if","locations":[{"start":{"line":26,"column":4},"end":{"line":26,"column":4}},{"start":{"line":26,"column":4},"end":{"line":26,"column":4}}]},"2":{"line":35,"type":"if","locations":[{"start":{"line":35,"column":6},"end":{"line":35,"column":6}},{"start":{"line":35,"column":6},"end":{"line":35,"column":6}}]},"3":{"line":51,"type":"if","locations":[{"start":{"line":51,"column":8},"end":{"line":51,"column":8}},{"start":{"line":51,"column":8},"end":{"line":51,"column":8}}]},"4":{"line":54,"type":"if","locations":[{"start":{"line":54,"column":10},"end":{"line":54,"column":10}},{"start":{"line":54,"column":10},"end":{"line":54,"column":10}}]},"5":{"line":58,"type":"if","locations":[{"start":{"line":58,"column":10},"end":{"line":58,"column":10}},{"start":{"line":58,"column":10},"end":{"line":58,"column":10}}]},"6":{"line":69,"type":"if","locations":[{"start":{"line":69,"column":6},"end":{"line":69,"column":6}},{"start":{"line":69,"column":6},"end":{"line":69,"column":6}}]},"7":{"line":78,"type":"if","locations":[{"start":{"line":78,"column":8},"end":{"line":78,"column":8}},{"start":{"line":78,"column":8},"end":{"line":78,"column":8}}]},"8":{"line":82,"type":"if","locations":[{"start":{"line":82,"column":8},"end":{"line":82,"column":8}},{"start":{"line":82,"column":8},"end":{"line":82,"column":8}}]},"9":{"line":88,"type":"if","locations":[{"start":{"line":88,"column":8},"end":{"line":88,"column":8}},{"start":{"line":88,"column":8},"end":{"line":88,"column":8}}]},"10":{"line":98,"type":"if","locations":[{"start":{"line":98,"column":6},"end":{"line":98,"column":6}},{"start":{"line":98,"column":6},"end":{"line":98,"column":6}}]},"11":{"line":115,"type":"if","locations":[{"start":{"line":115,"column":12},"end":{"line":115,"column":12}},{"start":{"line":115,"column":12},"end":{"line":115,"column":12}}]},"12":{"line":121,"type":"if","locations":[{"start":{"line":121,"column":14},"end":{"line":121,"column":14}},{"start":{"line":121,"column":14},"end":{"line":121,"column":14}}]},"13":{"line":126,"type":"if","locations":[{"start":{"line":126,"column":16},"end":{"line":126,"column":16}},{"start":{"line":126,"column":16},"end":{"line":126,"column":16}}]},"14":{"line":141,"type":"if","locations":[{"start":{"line":141,"column":6},"end":{"line":141,"column":6}},{"start":{"line":141,"column":6},"end":{"line":141,"column":6}}]}}},"/src/hooks/github.js":{"path":"/src/hooks/github.js","s":{"1":1,"2":1,"3":4,"4":3,"5":4,"6":4,"7":4,"8":4,"9":3,"10":3,"11":3,"12":1,"13":2,"14":2,"15":1,"16":1,"17":4},"b":{"1":[3,1],"2":[1,2],"3":[1,1]},"f":{"1":4,"2":3,"3":3,"4":2},"fnMap":{"1":{"name":"(anonymous_1)","line":7,"loc":{"start":{"line":7,"column":17},"end":{"line":7,"column":43}}},"2":{"name":"(anonymous_2)","line":18,"loc":{"start":{"line":18,"column":26},"end":{"line":18,"column":47}}},"3":{"name":"(anonymous_3)","line":23,"loc":{"start":{"line":23,"column":50},"end":{"line":23,"column":64}}},"4":{"name":"(anonymous_4)","line":27,"loc":{"start":{"line":27,"column":55},"end":{"line":27,"column":69}}}},"statementMap":{"1":{"start":{"line":3,"column":0},"end":{"line":5,"column":52}},"2":{"start":{"line":7,"column":0},"end":{"line":37,"column":2}},"3":{"start":{"line":8,"column":2},"end":{"line":10,"column":3}},"4":{"start":{"line":9,"column":4},"end":{"line":9,"column":34}},"5":{"start":{"line":12,"column":2},"end":{"line":12,"column":32}},"6":{"start":{"line":13,"column":2},"end":{"line":13,"column":23}},"7":{"start":{"line":14,"column":2},"end":{"line":16,"column":4}},"8":{"start":{"line":18,"column":2},"end":{"line":34,"column":5}},"9":{"start":{"line":19,"column":4},"end":{"line":21,"column":27}},"10":{"start":{"line":23,"column":4},"end":{"line":33,"column":7}},"11":{"start":{"line":24,"column":6},"end":{"line":26,"column":7}},"12":{"start":{"line":25,"column":8},"end":{"line":25,"column":25}},"13":{"start":{"line":27,"column":6},"end":{"line":32,"column":9}},"14":{"start":{"line":28,"column":8},"end":{"line":30,"column":9}},"15":{"start":{"line":29,"column":10},"end":{"line":29,"column":27}},"16":{"start":{"line":31,"column":8},"end":{"line":31,"column":15}},"17":{"start":{"line":36,"column":2},"end":{"line":36,"column":14}}},"branchMap":{"1":{"line":8,"type":"if","locations":[{"start":{"line":8,"column":2},"end":{"line":8,"column":2}},{"start":{"line":8,"column":2},"end":{"line":8,"column":2}}]},"2":{"line":24,"type":"if","locations":[{"start":{"line":24,"column":6},"end":{"line":24,"column":6}},{"start":{"line":24,"column":6},"end":{"line":24,"column":6}}]},"3":{"line":28,"type":"if","locations":[{"start":{"line":28,"column":8},"end":{"line":28,"column":8}},{"start":{"line":28,"column":8},"end":{"line":28,"column":8}}]}}},"/src/models/github.js":{"path":"/src/models/github.js","s":{"1":1,"2":1,"3":5,"4":5,"5":1,"6":5,"7":5,"8":2,"9":2,"10":0,"11":5,"12":5,"13":2,"14":2,"15":2,"16":0,"17":2,"18":1,"19":1,"20":5,"21":2,"22":2,"23":2,"24":0,"25":2,"26":1,"27":1},"b":{"1":[1,4],"2":[2,0],"3":[0,2],"4":[1,1],"5":[0,2],"6":[1,1]},"f":{"1":5,"2":2,"3":2,"4":2,"5":2,"6":2},"fnMap":{"1":{"name":"GithubModel","line":4,"loc":{"start":{"line":4,"column":17},"end":{"line":4,"column":51}}},"2":{"name":"(anonymous_2)","line":11,"loc":{"start":{"line":11,"column":28},"end":{"line":11,"column":49}}},"3":{"name":"(anonymous_3)","line":29,"loc":{"start":{"line":29,"column":22},"end":{"line":29,"column":58}}},"4":{"name":"(anonymous_4)","line":32,"loc":{"start":{"line":32,"column":23},"end":{"line":32,"column":43}}},"5":{"name":"(anonymous_5)","line":46,"loc":{"start":{"line":46,"column":19},"end":{"line":46,"column":55}}},"6":{"name":"(anonymous_6)","line":49,"loc":{"start":{"line":49,"column":20},"end":{"line":49,"column":46}}}},"statementMap":{"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":33}},"2":{"start":{"line":4,"column":0},"end":{"line":62,"column":2}},"3":{"start":{"line":5,"column":2},"end":{"line":5,"column":35}},"4":{"start":{"line":6,"column":2},"end":{"line":8,"column":3}},"5":{"start":{"line":7,"column":4},"end":{"line":7,"column":26}},"6":{"start":{"line":10,"column":2},"end":{"line":10,"column":43}},"7":{"start":{"line":11,"column":2},"end":{"line":20,"column":4}},"8":{"start":{"line":12,"column":4},"end":{"line":19,"column":5}},"9":{"start":{"line":14,"column":8},"end":{"line":14,"column":43}},"10":{"start":{"line":17,"column":8},"end":{"line":17,"column":53}},"11":{"start":{"line":22,"column":2},"end":{"line":27,"column":4}},"12":{"start":{"line":29,"column":2},"end":{"line":44,"column":4}},"13":{"start":{"line":30,"column":4},"end":{"line":30,"column":89}},"14":{"start":{"line":31,"column":4},"end":{"line":43,"column":77}},"15":{"start":{"line":33,"column":8},"end":{"line":35,"column":9}},"16":{"start":{"line":34,"column":10},"end":{"line":34,"column":27}},"17":{"start":{"line":37,"column":8},"end":{"line":39,"column":9}},"18":{"start":{"line":38,"column":10},"end":{"line":38,"column":58}},"19":{"start":{"line":41,"column":8},"end":{"line":41,"column":15}},"20":{"start":{"line":46,"column":2},"end":{"line":61,"column":4}},"21":{"start":{"line":47,"column":4},"end":{"line":47,"column":89}},"22":{"start":{"line":48,"column":4},"end":{"line":60,"column":77}},"23":{"start":{"line":50,"column":8},"end":{"line":52,"column":9}},"24":{"start":{"line":51,"column":10},"end":{"line":51,"column":27}},"25":{"start":{"line":54,"column":8},"end":{"line":56,"column":9}},"26":{"start":{"line":55,"column":10},"end":{"line":55,"column":58}},"27":{"start":{"line":58,"column":8},"end":{"line":58,"column":25}}},"branchMap":{"1":{"line":6,"type":"if","locations":[{"start":{"line":6,"column":2},"end":{"line":6,"column":2}},{"start":{"line":6,"column":2},"end":{"line":6,"column":2}}]},"2":{"line":12,"type":"switch","locations":[{"start":{"line":13,"column":6},"end":{"line":15,"column":7}},{"start":{"line":16,"column":6},"end":{"line":18,"column":7}}]},"3":{"line":33,"type":"if","locations":[{"start":{"line":33,"column":8},"end":{"line":33,"column":8}},{"start":{"line":33,"column":8},"end":{"line":33,"column":8}}]},"4":{"line":37,"type":"if","locations":[{"start":{"line":37,"column":8},"end":{"line":37,"column":8}},{"start":{"line":37,"column":8},"end":{"line":37,"column":8}}]},"5":{"line":50,"type":"if","locations":[{"start":{"line":50,"column":8},"end":{"line":50,"column":8}},{"start":{"line":50,"column":8},"end":{"line":50,"column":8}}]},"6":{"line":54,"type":"if","locations":[{"start":{"line":54,"column":8},"end":{"line":54,"column":8}},{"start":{"line":54,"column":8},"end":{"line":54,"column":8}}]}}},"/src/hooks/jira.js":{"path":"/src/hooks/jira.js","s":{"1":1,"2":1,"3":8,"4":7,"5":8,"6":8,"7":8,"8":8,"9":7,"10":1,"11":6,"12":1,"13":5,"14":5,"15":1,"16":4,"17":4,"18":4,"19":1,"20":3,"21":3,"22":1,"23":2,"24":2,"25":1,"26":1,"27":8,"28":1,"29":5,"30":5,"31":1,"32":4,"33":4},"b":{"1":[7,1],"2":[1,6],"3":[1,5],"4":[1,4],"5":[5,5],"6":[1,3],"7":[1,2],"8":[1,1],"9":[1,4]},"f":{"1":8,"2":7,"3":4,"4":3,"5":2,"6":5},"fnMap":{"1":{"name":"jiraHook","line":8,"loc":{"start":{"line":8,"column":17},"end":{"line":8,"column":45}}},"2":{"name":"(anonymous_2)","line":19,"loc":{"start":{"line":19,"column":26},"end":{"line":19,"column":47}}},"3":{"name":"(anonymous_3)","line":34,"loc":{"start":{"line":34,"column":26},"end":{"line":34,"column":51}}},"4":{"name":"(anonymous_4)","line":39,"loc":{"start":{"line":39,"column":63},"end":{"line":39,"column":91}}},"5":{"name":"(anonymous_5)","line":44,"loc":{"start":{"line":44,"column":50},"end":{"line":44,"column":64}}},"6":{"name":"isInt","line":57,"loc":{"start":{"line":57,"column":0},"end":{"line":57,"column":22}}}},"statementMap":{"1":{"start":{"line":3,"column":0},"end":{"line":5,"column":43}},"2":{"start":{"line":8,"column":0},"end":{"line":55,"column":2}},"3":{"start":{"line":9,"column":2},"end":{"line":11,"column":3}},"4":{"start":{"line":10,"column":4},"end":{"line":10,"column":20}},"5":{"start":{"line":13,"column":2},"end":{"line":13,"column":32}},"6":{"start":{"line":14,"column":2},"end":{"line":14,"column":21}},"7":{"start":{"line":15,"column":2},"end":{"line":17,"column":4}},"8":{"start":{"line":19,"column":2},"end":{"line":52,"column":5}},"9":{"start":{"line":20,"column":4},"end":{"line":22,"column":5}},"10":{"start":{"line":21,"column":6},"end":{"line":21,"column":41}},"11":{"start":{"line":24,"column":4},"end":{"line":26,"column":5}},"12":{"start":{"line":25,"column":6},"end":{"line":25,"column":50}},"13":{"start":{"line":28,"column":4},"end":{"line":28,"column":40}},"14":{"start":{"line":29,"column":4},"end":{"line":31,"column":5}},"15":{"start":{"line":30,"column":6},"end":{"line":30,"column":50}},"16":{"start":{"line":33,"column":4},"end":{"line":33,"column":44}},"17":{"start":{"line":34,"column":4},"end":{"line":51,"column":7}},"18":{"start":{"line":35,"column":6},"end":{"line":37,"column":7}},"19":{"start":{"line":36,"column":8},"end":{"line":36,"column":52}},"20":{"start":{"line":39,"column":6},"end":{"line":50,"column":9}},"21":{"start":{"line":40,"column":8},"end":{"line":42,"column":9}},"22":{"start":{"line":41,"column":10},"end":{"line":41,"column":101}},"23":{"start":{"line":44,"column":8},"end":{"line":49,"column":11}},"24":{"start":{"line":45,"column":10},"end":{"line":47,"column":11}},"25":{"start":{"line":46,"column":12},"end":{"line":46,"column":65}},"26":{"start":{"line":48,"column":10},"end":{"line":48,"column":17}},"27":{"start":{"line":54,"column":2},"end":{"line":54,"column":14}},"28":{"start":{"line":57,"column":0},"end":{"line":65,"column":1}},"29":{"start":{"line":58,"column":2},"end":{"line":58,"column":8}},"30":{"start":{"line":59,"column":2},"end":{"line":61,"column":3}},"31":{"start":{"line":60,"column":4},"end":{"line":60,"column":17}},"32":{"start":{"line":63,"column":2},"end":{"line":63,"column":24}},"33":{"start":{"line":64,"column":2},"end":{"line":64,"column":23}}},"branchMap":{"1":{"line":9,"type":"if","locations":[{"start":{"line":9,"column":2},"end":{"line":9,"column":2}},{"start":{"line":9,"column":2},"end":{"line":9,"column":2}}]},"2":{"line":20,"type":"if","locations":[{"start":{"line":20,"column":4},"end":{"line":20,"column":4}},{"start":{"line":20,"column":4},"end":{"line":20,"column":4}}]},"3":{"line":24,"type":"if","locations":[{"start":{"line":24,"column":4},"end":{"line":24,"column":4}},{"start":{"line":24,"column":4},"end":{"line":24,"column":4}}]},"4":{"line":29,"type":"if","locations":[{"start":{"line":29,"column":4},"end":{"line":29,"column":4}},{"start":{"line":29,"column":4},"end":{"line":29,"column":4}}]},"5":{"line":29,"type":"binary-expr","locations":[{"start":{"line":29,"column":7},"end":{"line":29,"column":24}},{"start":{"line":29,"column":28},"end":{"line":29,"column":45}}]},"6":{"line":35,"type":"if","locations":[{"start":{"line":35,"column":6},"end":{"line":35,"column":6}},{"start":{"line":35,"column":6},"end":{"line":35,"column":6}}]},"7":{"line":40,"type":"if","locations":[{"start":{"line":40,"column":8},"end":{"line":40,"column":8}},{"start":{"line":40,"column":8},"end":{"line":40,"column":8}}]},"8":{"line":45,"type":"if","locations":[{"start":{"line":45,"column":10},"end":{"line":45,"column":10}},{"start":{"line":45,"column":10},"end":{"line":45,"column":10}}]},"9":{"line":59,"type":"if","locations":[{"start":{"line":59,"column":2},"end":{"line":59,"column":2}},{"start":{"line":59,"column":2},"end":{"line":59,"column":2}}]}}},"/src/models/jira.js":{"path":"/src/models/jira.js","s":{"1":1,"2":1,"3":1,"4":0,"5":0,"6":0,"7":1,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":1,"15":0,"16":0,"17":0,"18":1,"19":0,"20":0,"21":0},"b":{"1":[0,0]},"f":{"1":1,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0},"fnMap":{"1":{"name":"JiraModel","line":7,"loc":{"start":{"line":7,"column":17},"end":{"line":7,"column":38}}},"2":{"name":"(anonymous_2)","line":8,"loc":{"start":{"line":8,"column":19},"end":{"line":8,"column":41}}},"3":{"name":"(anonymous_3)","line":10,"loc":{"start":{"line":10,"column":12},"end":{"line":10,"column":32}}},"4":{"name":"(anonymous_4)","line":13,"loc":{"start":{"line":13,"column":13},"end":{"line":13,"column":27}}},"5":{"name":"(anonymous_5)","line":18,"loc":{"start":{"line":18,"column":23},"end":{"line":18,"column":60}}},"6":{"name":"(anonymous_6)","line":19,"loc":{"start":{"line":19,"column":32},"end":{"line":19,"column":54}}},"7":{"name":"(anonymous_7)","line":30,"loc":{"start":{"line":30,"column":25},"end":{"line":30,"column":47}}},"8":{"name":"(anonymous_8)","line":32,"loc":{"start":{"line":32,"column":12},"end":{"line":32,"column":34}}},"9":{"name":"(anonymous_9)","line":35,"loc":{"start":{"line":35,"column":13},"end":{"line":35,"column":27}}},"10":{"name":"(anonymous_10)","line":40,"loc":{"start":{"line":40,"column":25},"end":{"line":40,"column":61}}},"11":{"name":"(anonymous_11)","line":42,"loc":{"start":{"line":42,"column":12},"end":{"line":42,"column":29}}},"12":{"name":"(anonymous_12)","line":45,"loc":{"start":{"line":45,"column":13},"end":{"line":45,"column":27}}}},"statementMap":{"1":{"start":{"line":3,"column":0},"end":{"line":5,"column":41}},"2":{"start":{"line":7,"column":0},"end":{"line":49,"column":2}},"3":{"start":{"line":8,"column":2},"end":{"line":16,"column":4}},"4":{"start":{"line":9,"column":4},"end":{"line":15,"column":9}},"5":{"start":{"line":11,"column":8},"end":{"line":11,"column":30}},"6":{"start":{"line":14,"column":8},"end":{"line":14,"column":18}},"7":{"start":{"line":18,"column":2},"end":{"line":28,"column":4}},"8":{"start":{"line":19,"column":4},"end":{"line":27,"column":7}},"9":{"start":{"line":20,"column":6},"end":{"line":25,"column":7}},"10":{"start":{"line":21,"column":8},"end":{"line":24,"column":9}},"11":{"start":{"line":22,"column":10},"end":{"line":22,"column":40}},"12":{"start":{"line":23,"column":10},"end":{"line":23,"column":17}},"13":{"start":{"line":26,"column":6},"end":{"line":26,"column":35}},"14":{"start":{"line":30,"column":2},"end":{"line":38,"column":4}},"15":{"start":{"line":31,"column":4},"end":{"line":37,"column":9}},"16":{"start":{"line":33,"column":8},"end":{"line":33,"column":38}},"17":{"start":{"line":36,"column":8},"end":{"line":36,"column":18}},"18":{"start":{"line":40,"column":2},"end":{"line":48,"column":4}},"19":{"start":{"line":41,"column":4},"end":{"line":47,"column":9}},"20":{"start":{"line":43,"column":8},"end":{"line":43,"column":19}},"21":{"start":{"line":46,"column":8},"end":{"line":46,"column":18}}},"branchMap":{"1":{"line":21,"type":"if","locations":[{"start":{"line":21,"column":8},"end":{"line":21,"column":8}},{"start":{"line":21,"column":8},"end":{"line":21,"column":8}}]}}},"/src/app.js":{"path":"/src/app.js","s":{"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1,"12":1,"13":1,"14":1,"15":1,"16":1,"17":1,"18":1,"19":1,"20":1,"21":1,"22":1,"23":1,"24":1,"25":0,"26":0,"27":0,"28":1,"29":1,"30":1,"31":1,"32":1,"33":0,"34":0,"35":1},"b":{"1":[1,0],"2":[1,1],"3":[0,0]},"f":{"1":0,"2":1,"3":0},"fnMap":{"1":{"name":"(anonymous_1)","line":42,"loc":{"start":{"line":42,"column":8},"end":{"line":42,"column":33}}},"2":{"name":"(anonymous_2)","line":53,"loc":{"start":{"line":53,"column":10},"end":{"line":53,"column":40}}},"3":{"name":"(anonymous_3)","line":64,"loc":{"start":{"line":64,"column":8},"end":{"line":64,"column":38}}}},"statementMap":{"1":{"start":{"line":1,"column":0},"end":{"line":1,"column":33}},"2":{"start":{"line":2,"column":0},"end":{"line":2,"column":27}},"3":{"start":{"line":3,"column":0},"end":{"line":3,"column":39}},"4":{"start":{"line":4,"column":0},"end":{"line":4,"column":31}},"5":{"start":{"line":5,"column":0},"end":{"line":5,"column":44}},"6":{"start":{"line":6,"column":0},"end":{"line":6,"column":40}},"7":{"start":{"line":8,"column":0},"end":{"line":8,"column":39}},"8":{"start":{"line":9,"column":0},"end":{"line":9,"column":54}},"9":{"start":{"line":10,"column":0},"end":{"line":10,"column":46}},"10":{"start":{"line":11,"column":0},"end":{"line":11,"column":38}},"11":{"start":{"line":12,"column":0},"end":{"line":12,"column":33}},"12":{"start":{"line":14,"column":0},"end":{"line":14,"column":20}},"13":{"start":{"line":17,"column":0},"end":{"line":17,"column":48}},"14":{"start":{"line":18,"column":0},"end":{"line":18,"column":30}},"15":{"start":{"line":22,"column":0},"end":{"line":22,"column":23}},"16":{"start":{"line":23,"column":0},"end":{"line":23,"column":27}},"17":{"start":{"line":24,"column":0},"end":{"line":24,"column":24}},"18":{"start":{"line":31,"column":0},"end":{"line":31,"column":56}},"19":{"start":{"line":33,"column":0},"end":{"line":33,"column":21}},"20":{"start":{"line":34,"column":0},"end":{"line":34,"column":40}},"21":{"start":{"line":35,"column":0},"end":{"line":35,"column":48}},"22":{"start":{"line":36,"column":0},"end":{"line":36,"column":25}},"23":{"start":{"line":37,"column":0},"end":{"line":37,"column":21}},"24":{"start":{"line":42,"column":0},"end":{"line":46,"column":3}},"25":{"start":{"line":43,"column":2},"end":{"line":43,"column":35}},"26":{"start":{"line":44,"column":2},"end":{"line":44,"column":19}},"27":{"start":{"line":45,"column":2},"end":{"line":45,"column":12}},"28":{"start":{"line":52,"column":0},"end":{"line":60,"column":1}},"29":{"start":{"line":53,"column":2},"end":{"line":59,"column":5}},"30":{"start":{"line":54,"column":4},"end":{"line":54,"column":34}},"31":{"start":{"line":55,"column":4},"end":{"line":58,"column":7}},"32":{"start":{"line":64,"column":0},"end":{"line":70,"column":3}},"33":{"start":{"line":65,"column":2},"end":{"line":65,"column":32}},"34":{"start":{"line":66,"column":2},"end":{"line":69,"column":5}},"35":{"start":{"line":73,"column":0},"end":{"line":73,"column":21}}},"branchMap":{"1":{"line":52,"type":"if","locations":[{"start":{"line":52,"column":0},"end":{"line":52,"column":0}},{"start":{"line":52,"column":0},"end":{"line":52,"column":0}}]},"2":{"line":54,"type":"binary-expr","locations":[{"start":{"line":54,"column":15},"end":{"line":54,"column":25}},{"start":{"line":54,"column":29},"end":{"line":54,"column":32}}]},"3":{"line":65,"type":"binary-expr","locations":[{"start":{"line":65,"column":13},"end":{"line":65,"column":23}},{"start":{"line":65,"column":27},"end":{"line":65,"column":30}}]}}},"/src/routes/index.js":{"path":"/src/routes/index.js","s":{"1":1,"2":1,"3":1,"4":1,"5":1},"b":{},"f":{"1":1},"fnMap":{"1":{"name":"(anonymous_1)","line":5,"loc":{"start":{"line":5,"column":16},"end":{"line":5,"column":41}}}},"statementMap":{"1":{"start":{"line":1,"column":0},"end":{"line":1,"column":33}},"2":{"start":{"line":2,"column":0},"end":{"line":2,"column":30}},"3":{"start":{"line":5,"column":0},"end":{"line":7,"column":3}},"4":{"start":{"line":6,"column":2},"end":{"line":6,"column":26}},"5":{"start":{"line":9,"column":0},"end":{"line":9,"column":24}}},"branchMap":{}},"/src/routes/containers.js":{"path":"/src/routes/containers.js","s":{"1":1,"2":1,"3":0,"4":0,"5":0,"6":0,"7":0,"8":1,"9":0,"10":0,"11":0,"12":0,"13":0,"14":1,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":1,"29":0,"30":0,"31":0,"32":0,"33":0,"34":1,"35":0,"36":0,"37":0,"38":0,"39":0,"40":0,"41":0,"42":0,"43":0,"44":0,"45":0,"46":0,"47":0,"48":0,"49":0,"50":1},"b":{"1":[0,0],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0],"6":[0,0],"7":[0,0],"8":[0,0],"9":[0,0],"10":[0,0]},"f":{"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0},"fnMap":{"1":{"name":"(anonymous_1)","line":38,"loc":{"start":{"line":38,"column":16},"end":{"line":38,"column":41}}},"2":{"name":"(anonymous_2)","line":39,"loc":{"start":{"line":39,"column":23},"end":{"line":39,"column":49}}},"3":{"name":"(anonymous_3)","line":68,"loc":{"start":{"line":68,"column":26},"end":{"line":68,"column":51}}},"4":{"name":"(anonymous_4)","line":69,"loc":{"start":{"line":69,"column":48},"end":{"line":69,"column":68}}},"5":{"name":"(anonymous_5)","line":118,"loc":{"start":{"line":118,"column":17},"end":{"line":118,"column":42}}},"6":{"name":"(anonymous_6)","line":125,"loc":{"start":{"line":125,"column":45},"end":{"line":125,"column":72}}},"7":{"name":"(anonymous_7)","line":131,"loc":{"start":{"line":131,"column":41},"end":{"line":131,"column":61}}},"8":{"name":"(anonymous_8)","line":149,"loc":{"start":{"line":149,"column":29},"end":{"line":149,"column":54}}},"9":{"name":"(anonymous_9)","line":150,"loc":{"start":{"line":150,"column":54},"end":{"line":150,"column":74}}},"10":{"name":"(anonymous_10)","line":180,"loc":{"start":{"line":180,"column":35},"end":{"line":180,"column":60}}},"11":{"name":"(anonymous_11)","line":181,"loc":{"start":{"line":181,"column":48},"end":{"line":181,"column":68}}},"12":{"name":"(anonymous_12)","line":204,"loc":{"start":{"line":204,"column":56},"end":{"line":204,"column":76}}},"13":{"name":"(anonymous_13)","line":205,"loc":{"start":{"line":205,"column":40},"end":{"line":205,"column":67}}},"14":{"name":"(anonymous_14)","line":211,"loc":{"start":{"line":211,"column":45},"end":{"line":211,"column":65}}}},"statementMap":{"1":{"start":{"line":3,"column":0},"end":{"line":6,"column":28}},"2":{"start":{"line":38,"column":0},"end":{"line":47,"column":3}},"3":{"start":{"line":39,"column":2},"end":{"line":46,"column":5}},"4":{"start":{"line":40,"column":4},"end":{"line":43,"column":5}},"5":{"start":{"line":41,"column":6},"end":{"line":41,"column":16}},"6":{"start":{"line":42,"column":6},"end":{"line":42,"column":13}},"7":{"start":{"line":45,"column":4},"end":{"line":45,"column":25}},"8":{"start":{"line":68,"column":0},"end":{"line":77,"column":3}},"9":{"start":{"line":69,"column":2},"end":{"line":76,"column":5}},"10":{"start":{"line":70,"column":4},"end":{"line":73,"column":5}},"11":{"start":{"line":71,"column":6},"end":{"line":71,"column":16}},"12":{"start":{"line":72,"column":6},"end":{"line":72,"column":13}},"13":{"start":{"line":75,"column":4},"end":{"line":75,"column":19}},"14":{"start":{"line":118,"column":0},"end":{"line":140,"column":3}},"15":{"start":{"line":119,"column":2},"end":{"line":123,"column":3}},"16":{"start":{"line":120,"column":4},"end":{"line":120,"column":63}},"17":{"start":{"line":121,"column":4},"end":{"line":121,"column":30}},"18":{"start":{"line":122,"column":4},"end":{"line":122,"column":11}},"19":{"start":{"line":125,"column":2},"end":{"line":139,"column":5}},"20":{"start":{"line":126,"column":4},"end":{"line":129,"column":5}},"21":{"start":{"line":127,"column":6},"end":{"line":127,"column":32}},"22":{"start":{"line":128,"column":6},"end":{"line":128,"column":13}},"23":{"start":{"line":131,"column":4},"end":{"line":138,"column":7}},"24":{"start":{"line":132,"column":6},"end":{"line":135,"column":7}},"25":{"start":{"line":133,"column":8},"end":{"line":133,"column":18}},"26":{"start":{"line":134,"column":8},"end":{"line":134,"column":15}},"27":{"start":{"line":137,"column":6},"end":{"line":137,"column":21}},"28":{"start":{"line":149,"column":0},"end":{"line":158,"column":3}},"29":{"start":{"line":150,"column":2},"end":{"line":157,"column":5}},"30":{"start":{"line":151,"column":4},"end":{"line":154,"column":5}},"31":{"start":{"line":152,"column":6},"end":{"line":152,"column":16}},"32":{"start":{"line":153,"column":6},"end":{"line":153,"column":13}},"33":{"start":{"line":156,"column":4},"end":{"line":156,"column":27}},"34":{"start":{"line":180,"column":0},"end":{"line":222,"column":3}},"35":{"start":{"line":181,"column":2},"end":{"line":221,"column":5}},"36":{"start":{"line":182,"column":4},"end":{"line":185,"column":5}},"37":{"start":{"line":183,"column":6},"end":{"line":183,"column":16}},"38":{"start":{"line":184,"column":6},"end":{"line":184,"column":13}},"39":{"start":{"line":186,"column":4},"end":{"line":202,"column":6}},"40":{"start":{"line":204,"column":4},"end":{"line":220,"column":7}},"41":{"start":{"line":205,"column":6},"end":{"line":219,"column":9}},"42":{"start":{"line":206,"column":8},"end":{"line":209,"column":9}},"43":{"start":{"line":207,"column":10},"end":{"line":207,"column":36}},"44":{"start":{"line":208,"column":10},"end":{"line":208,"column":17}},"45":{"start":{"line":211,"column":8},"end":{"line":218,"column":11}},"46":{"start":{"line":212,"column":10},"end":{"line":215,"column":11}},"47":{"start":{"line":213,"column":12},"end":{"line":213,"column":22}},"48":{"start":{"line":214,"column":12},"end":{"line":214,"column":19}},"49":{"start":{"line":217,"column":10},"end":{"line":217,"column":25}},"50":{"start":{"line":224,"column":0},"end":{"line":224,"column":24}}},"branchMap":{"1":{"line":40,"type":"if","locations":[{"start":{"line":40,"column":4},"end":{"line":40,"column":4}},{"start":{"line":40,"column":4},"end":{"line":40,"column":4}}]},"2":{"line":70,"type":"if","locations":[{"start":{"line":70,"column":4},"end":{"line":70,"column":4}},{"start":{"line":70,"column":4},"end":{"line":70,"column":4}}]},"3":{"line":119,"type":"if","locations":[{"start":{"line":119,"column":2},"end":{"line":119,"column":2}},{"start":{"line":119,"column":2},"end":{"line":119,"column":2}}]},"4":{"line":119,"type":"binary-expr","locations":[{"start":{"line":119,"column":6},"end":{"line":119,"column":25}},{"start":{"line":119,"column":29},"end":{"line":119,"column":54}}]},"5":{"line":126,"type":"if","locations":[{"start":{"line":126,"column":4},"end":{"line":126,"column":4}},{"start":{"line":126,"column":4},"end":{"line":126,"column":4}}]},"6":{"line":132,"type":"if","locations":[{"start":{"line":132,"column":6},"end":{"line":132,"column":6}},{"start":{"line":132,"column":6},"end":{"line":132,"column":6}}]},"7":{"line":151,"type":"if","locations":[{"start":{"line":151,"column":4},"end":{"line":151,"column":4}},{"start":{"line":151,"column":4},"end":{"line":151,"column":4}}]},"8":{"line":182,"type":"if","locations":[{"start":{"line":182,"column":4},"end":{"line":182,"column":4}},{"start":{"line":182,"column":4},"end":{"line":182,"column":4}}]},"9":{"line":206,"type":"if","locations":[{"start":{"line":206,"column":8},"end":{"line":206,"column":8}},{"start":{"line":206,"column":8},"end":{"line":206,"column":8}}]},"10":{"line":212,"type":"if","locations":[{"start":{"line":212,"column":10},"end":{"line":212,"column":10}},{"start":{"line":212,"column":10},"end":{"line":212,"column":10}}]}}},"/src/routes/images.js":{"path":"/src/routes/images.js","s":{"1":1,"2":1,"3":0,"4":0,"5":0,"6":0,"7":0,"8":1,"9":0,"10":0,"11":0,"12":0,"13":0,"14":1,"15":0,"16":0,"17":0,"18":0,"19":0,"20":1},"b":{"1":[0,0],"2":[0,0],"3":[0,0]},"f":{"1":0,"2":0,"3":0,"4":0,"5":0,"6":0},"fnMap":{"1":{"name":"(anonymous_1)","line":25,"loc":{"start":{"line":25,"column":16},"end":{"line":25,"column":41}}},"2":{"name":"(anonymous_2)","line":26,"loc":{"start":{"line":26,"column":22},"end":{"line":26,"column":44}}},"3":{"name":"(anonymous_3)","line":47,"loc":{"start":{"line":47,"column":17},"end":{"line":47,"column":42}}},"4":{"name":"(anonymous_4)","line":48,"loc":{"start":{"line":48,"column":39},"end":{"line":48,"column":64}}},"5":{"name":"(anonymous_5)","line":77,"loc":{"start":{"line":77,"column":27},"end":{"line":77,"column":52}}},"6":{"name":"(anonymous_6)","line":78,"loc":{"start":{"line":78,"column":44},"end":{"line":78,"column":64}}}},"statementMap":{"1":{"start":{"line":2,"column":0},"end":{"line":5,"column":28}},"2":{"start":{"line":25,"column":0},"end":{"line":34,"column":3}},"3":{"start":{"line":26,"column":2},"end":{"line":33,"column":5}},"4":{"start":{"line":27,"column":4},"end":{"line":30,"column":5}},"5":{"start":{"line":28,"column":6},"end":{"line":28,"column":16}},"6":{"start":{"line":29,"column":6},"end":{"line":29,"column":13}},"7":{"start":{"line":32,"column":4},"end":{"line":32,"column":21}},"8":{"start":{"line":47,"column":0},"end":{"line":56,"column":3}},"9":{"start":{"line":48,"column":2},"end":{"line":55,"column":5}},"10":{"start":{"line":49,"column":4},"end":{"line":52,"column":5}},"11":{"start":{"line":50,"column":6},"end":{"line":50,"column":16}},"12":{"start":{"line":51,"column":6},"end":{"line":51,"column":13}},"13":{"start":{"line":54,"column":4},"end":{"line":54,"column":20}},"14":{"start":{"line":77,"column":0},"end":{"line":86,"column":3}},"15":{"start":{"line":78,"column":2},"end":{"line":85,"column":5}},"16":{"start":{"line":79,"column":4},"end":{"line":82,"column":5}},"17":{"start":{"line":80,"column":6},"end":{"line":80,"column":16}},"18":{"start":{"line":81,"column":6},"end":{"line":81,"column":13}},"19":{"start":{"line":84,"column":4},"end":{"line":84,"column":19}},"20":{"start":{"line":88,"column":0},"end":{"line":88,"column":24}}},"branchMap":{"1":{"line":27,"type":"if","locations":[{"start":{"line":27,"column":4},"end":{"line":27,"column":4}},{"start":{"line":27,"column":4},"end":{"line":27,"column":4}}]},"2":{"line":49,"type":"if","locations":[{"start":{"line":49,"column":4},"end":{"line":49,"column":4}},{"start":{"line":49,"column":4},"end":{"line":49,"column":4}}]},"3":{"line":79,"type":"if","locations":[{"start":{"line":79,"column":4},"end":{"line":79,"column":4}},{"start":{"line":79,"column":4},"end":{"line":79,"column":4}}]}}},"/src/routes/hooks.js":{"path":"/src/routes/hooks.js","s":{"1":1,"2":1,"3":1,"4":1,"5":2,"6":2,"7":1,"8":1,"9":1,"10":1,"11":1,"12":1,"13":1,"14":4,"15":4,"16":1,"17":1,"18":3,"19":1,"20":1,"21":1,"22":0,"23":0,"24":1,"25":1},"b":{"1":[1,1],"2":[1,3],"3":[0,1]},"f":{"1":2,"2":2,"3":1,"4":1,"5":4,"6":4,"7":1,"8":1},"fnMap":{"1":{"name":"(anonymous_1)","line":12,"loc":{"start":{"line":12,"column":24},"end":{"line":12,"column":49}}},"2":{"name":"(anonymous_2)","line":13,"loc":{"start":{"line":13,"column":54},"end":{"line":13,"column":74}}},"3":{"name":"(anonymous_3)","line":36,"loc":{"start":{"line":36,"column":16},"end":{"line":36,"column":41}}},"4":{"name":"(anonymous_4)","line":37,"loc":{"start":{"line":37,"column":24},"end":{"line":37,"column":44}}},"5":{"name":"(anonymous_5)","line":49,"loc":{"start":{"line":49,"column":33},"end":{"line":49,"column":58}}},"6":{"name":"(anonymous_6)","line":50,"loc":{"start":{"line":50,"column":71},"end":{"line":50,"column":86}}},"7":{"name":"(anonymous_7)","line":65,"loc":{"start":{"line":65,"column":24},"end":{"line":65,"column":49}}},"8":{"name":"(anonymous_8)","line":66,"loc":{"start":{"line":66,"column":52},"end":{"line":66,"column":67}}}},"statementMap":{"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":33}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":30}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":37}},"4":{"start":{"line":12,"column":0},"end":{"line":28,"column":3}},"5":{"start":{"line":13,"column":2},"end":{"line":27,"column":5}},"6":{"start":{"line":14,"column":4},"end":{"line":22,"column":5}},"7":{"start":{"line":15,"column":6},"end":{"line":20,"column":9}},"8":{"start":{"line":21,"column":6},"end":{"line":21,"column":13}},"9":{"start":{"line":24,"column":4},"end":{"line":26,"column":7}},"10":{"start":{"line":36,"column":0},"end":{"line":42,"column":3}},"11":{"start":{"line":37,"column":2},"end":{"line":41,"column":5}},"12":{"start":{"line":38,"column":4},"end":{"line":40,"column":7}},"13":{"start":{"line":49,"column":0},"end":{"line":58,"column":3}},"14":{"start":{"line":50,"column":2},"end":{"line":57,"column":5}},"15":{"start":{"line":51,"column":4},"end":{"line":54,"column":5}},"16":{"start":{"line":52,"column":6},"end":{"line":52,"column":16}},"17":{"start":{"line":53,"column":6},"end":{"line":53,"column":13}},"18":{"start":{"line":56,"column":4},"end":{"line":56,"column":27}},"19":{"start":{"line":65,"column":0},"end":{"line":74,"column":3}},"20":{"start":{"line":66,"column":2},"end":{"line":73,"column":5}},"21":{"start":{"line":67,"column":4},"end":{"line":70,"column":5}},"22":{"start":{"line":68,"column":6},"end":{"line":68,"column":16}},"23":{"start":{"line":69,"column":6},"end":{"line":69,"column":13}},"24":{"start":{"line":72,"column":4},"end":{"line":72,"column":27}},"25":{"start":{"line":78,"column":0},"end":{"line":78,"column":24}}},"branchMap":{"1":{"line":14,"type":"if","locations":[{"start":{"line":14,"column":4},"end":{"line":14,"column":4}},{"start":{"line":14,"column":4},"end":{"line":14,"column":4}}]},"2":{"line":51,"type":"if","locations":[{"start":{"line":51,"column":4},"end":{"line":51,"column":4}},{"start":{"line":51,"column":4},"end":{"line":51,"column":4}}]},"3":{"line":67,"type":"if","locations":[{"start":{"line":67,"column":4},"end":{"line":67,"column":4}},{"start":{"line":67,"column":4},"end":{"line":67,"column":4}}]}}},"/src/hooks/hook.js":{"path":"/src/hooks/hook.js","s":{"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":4,"12":1,"13":1,"14":2,"15":2,"16":1,"17":1,"18":1,"19":1,"20":1,"21":4,"22":1,"23":1,"24":3,"25":0,"26":0,"27":3,"28":1,"29":1,"30":4,"31":0,"32":0,"33":1,"34":1},"b":{"1":[1,1],"2":[1,3],"3":[0,0],"4":[0,0]},"f":{"1":1,"2":2,"3":4,"4":0,"5":1,"6":0},"fnMap":{"1":{"name":"(anonymous_1)","line":18,"loc":{"start":{"line":18,"column":23},"end":{"line":18,"column":38}}},"2":{"name":"(anonymous_2)","line":31,"loc":{"start":{"line":31,"column":33},"end":{"line":31,"column":57}}},"3":{"name":"(anonymous_3)","line":45,"loc":{"start":{"line":45,"column":21},"end":{"line":45,"column":59}}},"4":{"name":"(anonymous_4)","line":54,"loc":{"start":{"line":54,"column":41},"end":{"line":54,"column":55}}},"5":{"name":"(anonymous_5)","line":62,"loc":{"start":{"line":62,"column":22},"end":{"line":62,"column":51}}},"6":{"name":"(anonymous_6)","line":64,"loc":{"start":{"line":64,"column":37},"end":{"line":64,"column":51}}}},"statementMap":{"1":{"start":{"line":3,"column":0},"end":{"line":6,"column":39}},"2":{"start":{"line":8,"column":0},"end":{"line":10,"column":2}},"3":{"start":{"line":12,"column":0},"end":{"line":12,"column":16}},"4":{"start":{"line":13,"column":0},"end":{"line":13,"column":33}},"5":{"start":{"line":14,"column":0},"end":{"line":14,"column":29}},"6":{"start":{"line":15,"column":0},"end":{"line":15,"column":33}},"7":{"start":{"line":16,"column":0},"end":{"line":16,"column":33}},"8":{"start":{"line":18,"column":0},"end":{"line":29,"column":2}},"9":{"start":{"line":19,"column":2},"end":{"line":19,"column":24}},"10":{"start":{"line":21,"column":2},"end":{"line":26,"column":3}},"11":{"start":{"line":22,"column":4},"end":{"line":25,"column":7}},"12":{"start":{"line":28,"column":2},"end":{"line":28,"column":27}},"13":{"start":{"line":31,"column":0},"end":{"line":43,"column":2}},"14":{"start":{"line":32,"column":2},"end":{"line":32,"column":24}},"15":{"start":{"line":33,"column":2},"end":{"line":39,"column":3}},"16":{"start":{"line":34,"column":4},"end":{"line":37,"column":7}},"17":{"start":{"line":38,"column":4},"end":{"line":38,"column":11}},"18":{"start":{"line":41,"column":2},"end":{"line":41,"column":47}},"19":{"start":{"line":42,"column":2},"end":{"line":42,"column":9}},"20":{"start":{"line":45,"column":0},"end":{"line":60,"column":2}},"21":{"start":{"line":46,"column":2},"end":{"line":52,"column":3}},"22":{"start":{"line":47,"column":4},"end":{"line":50,"column":7}},"23":{"start":{"line":51,"column":4},"end":{"line":51,"column":11}},"24":{"start":{"line":54,"column":2},"end":{"line":58,"column":5}},"25":{"start":{"line":55,"column":4},"end":{"line":57,"column":5}},"26":{"start":{"line":56,"column":6},"end":{"line":56,"column":23}},"27":{"start":{"line":59,"column":2},"end":{"line":59,"column":13}},"28":{"start":{"line":62,"column":0},"end":{"line":71,"column":2}},"29":{"start":{"line":63,"column":2},"end":{"line":69,"column":3}},"30":{"start":{"line":64,"column":4},"end":{"line":68,"column":7}},"31":{"start":{"line":65,"column":6},"end":{"line":67,"column":7}},"32":{"start":{"line":66,"column":8},"end":{"line":66,"column":25}},"33":{"start":{"line":70,"column":2},"end":{"line":70,"column":13}},"34":{"start":{"line":73,"column":0},"end":{"line":73,"column":22}}},"branchMap":{"1":{"line":33,"type":"if","locations":[{"start":{"line":33,"column":2},"end":{"line":33,"column":2}},{"start":{"line":33,"column":2},"end":{"line":33,"column":2}}]},"2":{"line":46,"type":"if","locations":[{"start":{"line":46,"column":2},"end":{"line":46,"column":2}},{"start":{"line":46,"column":2},"end":{"line":46,"column":2}}]},"3":{"line":55,"type":"if","locations":[{"start":{"line":55,"column":4},"end":{"line":55,"column":4}},{"start":{"line":55,"column":4},"end":{"line":55,"column":4}}]},"4":{"line":65,"type":"if","locations":[{"start":{"line":65,"column":6},"end":{"line":65,"column":6}},{"start":{"line":65,"column":6},"end":{"line":65,"column":6}}]}}},"/src/hooks/sample.js":{"path":"/src/hooks/sample.js","s":{"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1},"b":{},"f":{"1":1,"2":2,"3":0,"4":0,"5":1,"6":0},"fnMap":{"1":{"name":"(anonymous_1)","line":5,"loc":{"start":{"line":5,"column":17},"end":{"line":5,"column":28}}},"2":{"name":"(anonymous_2)","line":16,"loc":{"start":{"line":16,"column":26},"end":{"line":16,"column":41}}},"3":{"name":"(anonymous_3)","line":18,"loc":{"start":{"line":18,"column":27},"end":{"line":18,"column":42}}},"4":{"name":"(anonymous_4)","line":20,"loc":{"start":{"line":20,"column":23},"end":{"line":20,"column":38}}},"5":{"name":"(anonymous_5)","line":22,"loc":{"start":{"line":22,"column":26},"end":{"line":22,"column":41}}},"6":{"name":"(anonymous_6)","line":24,"loc":{"start":{"line":24,"column":26},"end":{"line":24,"column":41}}}},"statementMap":{"1":{"start":{"line":3,"column":0},"end":{"line":3,"column":50}},"2":{"start":{"line":5,"column":0},"end":{"line":27,"column":2}},"3":{"start":{"line":6,"column":2},"end":{"line":6,"column":32}},"4":{"start":{"line":7,"column":2},"end":{"line":7,"column":23}},"5":{"start":{"line":8,"column":2},"end":{"line":14,"column":6}},"6":{"start":{"line":16,"column":2},"end":{"line":16,"column":45}},"7":{"start":{"line":18,"column":2},"end":{"line":18,"column":46}},"8":{"start":{"line":20,"column":2},"end":{"line":20,"column":42}},"9":{"start":{"line":22,"column":2},"end":{"line":22,"column":45}},"10":{"start":{"line":24,"column":2},"end":{"line":24,"column":45}},"11":{"start":{"line":26,"column":2},"end":{"line":26,"column":14}}},"branchMap":{}},"/src/routes/S3.js":{"path":"/src/routes/S3.js","s":{"1":1,"2":1,"3":1,"4":1,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":1},"b":{"1":[0,0],"2":[0,0]},"f":{"1":0,"2":0},"fnMap":{"1":{"name":"(anonymous_1)","line":36,"loc":{"start":{"line":36,"column":16},"end":{"line":36,"column":42}}},"2":{"name":"(anonymous_2)","line":40,"loc":{"start":{"line":40,"column":29},"end":{"line":40,"column":50}}}},"statementMap":{"1":{"start":{"line":2,"column":0},"end":{"line":4,"column":28}},"2":{"start":{"line":6,"column":0},"end":{"line":6,"column":29}},"3":{"start":{"line":7,"column":0},"end":{"line":7,"column":49}},"4":{"start":{"line":36,"column":0},"end":{"line":58,"column":3}},"5":{"start":{"line":37,"column":4},"end":{"line":37,"column":26}},"6":{"start":{"line":38,"column":4},"end":{"line":38,"column":87}},"7":{"start":{"line":40,"column":4},"end":{"line":57,"column":7}},"8":{"start":{"line":41,"column":8},"end":{"line":44,"column":9}},"9":{"start":{"line":42,"column":12},"end":{"line":42,"column":22}},"10":{"start":{"line":43,"column":12},"end":{"line":43,"column":19}},"11":{"start":{"line":45,"column":8},"end":{"line":45,"column":32}},"12":{"start":{"line":46,"column":8},"end":{"line":55,"column":9}},"13":{"start":{"line":48,"column":10},"end":{"line":54,"column":11}},"14":{"start":{"line":49,"column":12},"end":{"line":53,"column":15}},"15":{"start":{"line":56,"column":8},"end":{"line":56,"column":23}},"16":{"start":{"line":60,"column":0},"end":{"line":60,"column":24}}},"branchMap":{"1":{"line":41,"type":"if","locations":[{"start":{"line":41,"column":8},"end":{"line":41,"column":8}},{"start":{"line":41,"column":8},"end":{"line":41,"column":8}}]},"2":{"line":48,"type":"if","locations":[{"start":{"line":48,"column":10},"end":{"line":48,"column":10}},{"start":{"line":48,"column":10},"end":{"line":48,"column":10}}]}}}} diff --git a/apps/worker/services/report/languages/tests/unit/node/node3-result.json b/apps/worker/services/report/languages/tests/unit/node/node3-result.json new file mode 100644 index 0000000000..12884b254a --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/node/node3-result.json @@ -0,0 +1,150 @@ +{ + "archive": [ + "{}\n<<<<< end_of_header >>>>>\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[\"2/3\",\"b\",[[0,\"2/3\",null,[[24,88,\"1/2\"],[24,50,\"1/2\"],[54,88,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[431,446,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[10,21,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,24,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[346,null,[[0,346]]]\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[55,66,\"1/2\"]],null]]]\n[12,null,[[0,12]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[161,168,\"0\"]],null]]]\n\n\n\n\n\n\n\n[152,null,[[0,152]]]\n\n\n[152,null,[[0,152]]]\n\n\n[152,null,[[0,152]]]\n\n\n\n\n[174,null,[[0,174]]]\n\n\n\n[12,null,[[0,12]]]\n[12,null,[[0,12]]]\n\n\n\n\n[8,null,[[0,8]]]\n\n\n\n[12,null,[[0,12]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[22,null,[[0,22]]]\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[84,101,\"1/2\"],[84,90,\"1/2\"],[94,101,\"1/2\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[10,47,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[272,null,[[0,272]]]\n\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[179,206,\"1\"]],null]]]\n[17,null,[[0,17]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[67,87,\"1/2\"]],null]]]\n[17,null,[[0,17]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[393,451,\"1/2\"],[393,419,\"1\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[537,549,\"1\"]],null]]]\n[34,null,[[0,34]]]\n[34,null,[[0,34]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[591,601,\"1\"]],null]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[57,91,\"1\"]],null]]]\n\n\n[166,null,[[0,166]]]\n[1569,null,[[0,1569]]]\n[1569,null,[[0,1569]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[98,113,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[205,302,\"1\"],[213,302,\"1\"],[223,300,\"1\"]],null]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[12,23,\"1\"]],null]]]\n[10,null,[[0,10]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[42,70,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[15,49,\"1/2\"]],null]]]\n\n\n\n[13,null,[[0,13]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[663,681,\"1\"]],null]]]\n[1548,null,[[0,1548]]]\n\n\n\n\n\n\n[272,null,[[0,272]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[10,43,\"1\"]],null]]]\n\n\n\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[10,43,\"1\"],[10,21,\"1/2\"],[25,43,\"1\"]],null]]]\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[195,462,\"0\"],[195,233,\"0\"],[195,212,\"0\"],[216,233,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n[1627,null,[[0,1627]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[121,171,\"1\"],[129,171,\"1\"]],null]]]\n[1468,null,[[0,1468]]]\n\n\n[159,null,[[0,159]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[306,312,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[439,495,\"1/2\"]],null]]]\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[506,584,\"1/2\"],[506,532,\"1/2\"],[536,584,\"1/2\"]],null]]]\n\n\n\n\n\n\n[138,null,[[0,138]]]\n\n[138,null,[[0,138]]]\n[86,null,[[0,86]]]\n\n[52,null,[[0,52]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[8,19,\"1\"]],null]]]\n[45,null,[[0,45]]]\n\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[112,164,\"1\"],[112,135,\"1\"],[139,164,\"1\"]],null]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[292,null,[[0,292]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[10,77,\"1/2\"],[27,77,\"1/2\"]],null]]]\n\n\n\n[316,null,[[0,316]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[30,48,\"1\"]],null]]]\n[272,null,[[0,272]]]\n[\"1\",\"b\",[[0,\"1\",null,[[36,46,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,58,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[44,null,[[0,44]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,58,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[316,null,[[0,316]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[10,22,\"0\"]],null]]]\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[13,26,\"1/2\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[36,47,\"1/2\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[9,37,\"1\"]],null]]]\n[19,null,[[0,19]]]\n\n\n\n\n[18,null,[[0,18]]]\n\n\n\n[37,null,[[0,37]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,21,\"1/2\"]],null]]]\n\n\n\n\n\n[230,null,[[0,230]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[71,78,\"1\"]],null]]]\n[249,null,[[0,249]]]\n\n\n[230,null,[[0,230]]]\n\n[230,null,[[0,230]]]\n\n\n\n[20,null,[[0,20]]]\n\n\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[204,214,\"1\"]],null]]]\n[63,null,[[0,63]]]\n[\"1\",\"b\",[[0,\"1\",null,[[58,92,\"1\"]],null]]]\n[61,null,[[0,61]]]\n\n\n\n[20,null,[[0,20]]]\n\n\n\n\n[57,null,[[0,57]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[130,150,\"1/2\"]],null]]]\n[57,null,[[0,57]]]\n[\"1\",\"b\",[[0,\"1\",null,[[38,48,\"1\"]],null]]]\n[46,null,[[0,46]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[55,68,\"1/2\"]],null]]]\n[46,null,[[0,46]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[55,68,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[57,null,[[0,57]]]\n\n\n\n\n\n\n\n\n[37,null,[[0,37]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[33,60,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[261,285,\"1\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n[36,null,[[0,36]]]\n[36,null,[[0,36]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[21,36,\"1/2\"]],null]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[596,620,\"1/2\"]],null]]]\n\n[36,null,[[0,36]]]\n\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[11002,11030,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n[10,null,[[0,10]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[216,254,\"1\"],[225,254,\"1\"]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[292,337,\"1\"],[292,311,\"1/2\"]],null]]]\n[18,null,[[0,18]]]\n\n\n[\"4/4\",\"b\",[[0,\"4/4\",null,[[370,473,\"1\"],[370,386,\"1\"],[390,473,\"1\"],[390,402,\"1\"]],null]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[8,15,\"1\"]],null]]]\n[4,null,[[0,4]]]\n\n[64,null,[[0,64]]]\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[46,53,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,25,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[161,195,\"0\"],[161,174,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[8,19,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[65,76,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[11,72,\"0\"],[11,33,\"0\"],[38,71,\"0\"]],null]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[18,28,\"1/2\"]],null]]]\n\n\n\n[90,null,[[0,90]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[84,107,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[111,null,[[0,111]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[222,235,\"1/2\"]],null]]]\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[305,403,\"1/2\"],[305,333,\"1/2\"],[337,403,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[515,520,\"1\"]],null]]]\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[11,83,\"1/2\"],[11,18,\"1/2\"],[21,53,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[131,150,\"1/2\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[160,174,\"1/2\"]],null]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[163,219,\"1\"],[163,178,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[31,44,\"1/2\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[37,51,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,48,\"0\"]],null]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[151,164,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[147,260,\"0\"],[162,260,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[843,851,\"1\"]],null]]]\n[7,null,[[0,7]]]\n[7,null,[[0,7]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[988,1074,\"1/2\"],[1005,1074,\"1/2\"]],null]]]\n\n\n[28,null,[[0,28]]]\n[28,null,[[0,28]]]\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[1448,1550,\"1\"]],null]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[11,25,\"1\"]],null]]]\n[8,null,[[0,8]]]\n[8,null,[[0,8]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[287,330,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[75,110,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[48,null,[[0,48]]]\n\n\n\n[48,null,[[0,48]]]\n[48,null,[[0,48]]]\n[\"1\",\"b\",[[0,\"1\",null,[[356,359,\"1\"]],null]]]\n[120,null,[[0,120]]]\n\n[48,null,[[0,48]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[535,614,\"1/2\"],[535,597,\"1/2\"]],null]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[965,976,\"1/2\"]],null]]]\n[56,null,[[0,56]]]\n[56,null,[[0,56]]]\n\n\n[55,null,[[0,55]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[12,27,\"1\"]],null]]]\n[48,null,[[0,48]]]\n\n\n\n\n\n\n\n\n[21,null,[[0,21]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[3,null,[[0,3]]]\n\n[3,null,[[0,3]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[98,139,\"1/2\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[20,null,[[0,20]]]\n\n[3,null,[[0,3]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[12,null,[[0,12]]]\n[12,null,[[0,12]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[10,null,[[0,10]]]\n\n[10,null,[[0,10]]]\n[10,null,[[0,10]]]\n\n[2,null,[[0,2]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[47,60,\"1\"]],null]]]\n[3,null,[[0,3]]]\n\n\n[10,null,[[0,10]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[58,61,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,18,\"0\"]],null]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[167,171,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[226,229,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,34,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,18,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[330,331,\"0\"]],null]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[5,null,[[0,5]]]\n[0,null,[[0,0]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[52,90,\"0\"],[52,68,\"0\"],[72,90,\"0\"]],null]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[2,null,[[0,2]]]\n[0,null,[[0,0]]]\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[53,113,\"0\"],[53,90,\"0\"],[53,69,\"0\"],[73,90,\"0\"],[95,113,\"0\"]],null]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[2,null,[[0,2]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[221,235,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[476,518,\"0\"],[495,518,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[72,87,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,37,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[14,51,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[14,40,\"0\"]],null]]]\n\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[376,567,\"0\"],[376,404,\"0\"]],null]]]\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1107,1133,\"0\"]],null]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1628,1643,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,37,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[7,null,[[0,7]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[156,159,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,39,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[9,82,\"0\"],[20,82,\"0\"],[20,71,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[137,189,\"1/2\"],[146,172,\"1/2\"]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[199,214,\"1/2\"],[217,252,\"1/2\"]],null]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[\"4/4\",\"b\",[[0,\"4/4\",null,[[149,211,\"1/2\"],[149,165,\"1/2\"],[169,211,\"1/2\"],[169,187,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[487,583,\"1/2\"],[487,512,\"1/2\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[35,61,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[176,197,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[52,128,\"1/2\"]],null]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2000,2015,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,46,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[9,72,\"0\"],[9,54,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[57,61,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[16,96,\"0\"],[16,60,\"0\"]],null]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[109,138,\"0\"],[117,138,\"0\"]],null]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[9,72,\"0\"],[9,54,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[77,81,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[85,110,\"0\"],[93,110,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[257,275,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[54,79,\"0\"],[62,79,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[3445,3473,\"1/2\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,60,\"1/2\"]],null]]]\n[5,null,[[0,5]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[176,187,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[218,229,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[14,34,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,30,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[4201,4412,\"1/2\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[8,79,\"0\"],[8,61,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[4973,5029,\"1/2\"]],null]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[683,733,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[896,937,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1109,1164,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1404,1443,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1642,1694,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[454,492,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[717,761,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[978,1023,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[8114,8312,\"1/2\"],[8165,8308,\"1/2\"]],null]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[8685,8738,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[8757,8818,\"1/2\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9092,9138,\"1/2\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[16,32,\"1/2\"]],null]]]\n\n[\"5/5\",\"b\",[[0,\"5/5\",null,[[97,272,\"1\"],[97,106,\"1/2\"],[114,271,\"1\"],[121,271,\"1\"],[121,139,\"1\"]],null]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[9,10,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[14,31,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,18,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9721,9731,\"1/2\"]],null]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[41,48,\"1\"]],null]]]\n[19,null,[[0,19]]]\n[19,null,[[0,19]]]\n\n\n\n[57,null,[[0,57]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[251,258,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[368,419,\"1/2\"],[368,388,\"1/2\"],[397,417,\"1/2\"]],null]]]\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[543,630,\"1/2\"]],null]]]\n\n\n\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[83,162,\"0\"],[83,97,\"0\"],[101,162,\"0\"],[101,133,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[195,274,\"0\"],[195,209,\"0\"],[213,274,\"0\"],[213,245,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[339,348,\"0\"]],null]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1066,1077,\"1/2\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[51,58,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[269,281,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,25,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[527,538,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[669,690,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[743,764,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[856,871,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[898,899,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[16,null,[[0,16]]]\n\n\n[1,null,[[0,1]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[41,82,\"1/2\"],[41,67,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[19,null,[[0,19]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[223,404,\"1/2\"],[250,404,\"1/2\"]],null]]]\n\n\n\n\n[19,null,[[0,19]]]\n[19,null,[[0,19]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[116,293,\"1/2\"],[123,293,\"1/2\"]],null]]]\n\n\n\n[19,null,[[0,19]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[765,816,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[41,88,\"1/2\"],[41,73,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[68,null,[[0,68]]]\n\n\n[1,null,[[0,1]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[41,82,\"1/2\"],[41,67,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[81,null,[[0,81]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[361,378,\"1/2\"]],null]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[20,null,[[0,20]]]\n\n\n\n\n\n[20,null,[[0,20]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[175,216,\"1/2\"]],null]]]\n[20,null,[[0,20]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[253,265,\"1\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[12,32,\"1\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[9,30,\"1\"]],null]]]\n[10,null,[[0,10]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[120,123,\"1\"]],null]]]\n[10,null,[[0,10]]]\n\n\n\n\n\n[20,null,[[0,20]]]\n\n[20,null,[[0,20]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[68,77,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[65,82,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[249,300,\"0\"],[249,263,\"0\"],[267,300,\"0\"],[267,281,\"0\"],[285,300,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[116,152,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[62,66,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[646,678,\"0\"],[646,660,\"0\"],[664,678,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[3,null,[[0,3]]]\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[140,178,\"1\"],[152,178,\"1\"],[164,178,\"1\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[223,240,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[3,null,[[0,3]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[343,375,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[41,50,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[224,232,\"0\"],[247,260,\"0\"],[270,311,\"0\"],[270,289,\"0\"],[293,311,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[337,380,\"0\"],[362,380,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[817,825,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[2,null,[[0,2]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[63,98,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[170,178,\"1/2\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[16,42,\"0\"],[28,42,\"0\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[297,557,\"1/2\"],[309,557,\"0\"]],null]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[2,null,[[0,2]]]\n\n\n\n\n\n\n[2,null,[[0,2]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[93,117,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[13,70,\"0\"],[30,70,\"0\"]],null]]]\n\n\n\n\n[2,null,[[0,2]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[60,382,\"1/2\"]],null]]]\n\n\n[\"6/6\",\"b\",[[0,\"6/6\",null,[[27,159,\"1\"],[21,73,\"1\"],[22,56,\"1/2\"],[55,131,\"1/2\"],[78,148,\"1/2\"],[79,119,\"1/2\"]],null]]]\n\n\n\n\n[2,null,[[0,2]]]\n[63,null,[[0,63]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[55,69,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[13,30,\"1/2\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[120,129,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[189,205,\"0\"],[208,224,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[17,45,\"0\"]],null]]]\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[134,159,\"0\"],[134,145,\"0\"],[149,159,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[283,289,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[57,63,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[16,19,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[39,58,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[15,102,\"0\"],[15,21,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[306,348,\"0\"],[306,321,\"0\"],[325,348,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[494,501,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[619,638,\"0\"]],null]]]\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[134,175,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[291,364,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[382,407,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[428,464,\"0\"],[428,450,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[480,503,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[519,562,\"0\"]],null]]]\n\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[581,726,\"0\"],[588,725,\"0\"],[588,622,\"0\"],[603,622,\"0\"]],null]]]\n\n\n\n\n\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[74,120,\"0\"],[74,93,\"0\"],[97,120,\"0\"],[107,120,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[72,80,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[80,121,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[240,314,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[333,358,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[380,416,\"0\"],[380,402,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[643,657,\"0\"]],null]]]\n\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[86,178,\"0\"],[93,177,\"0\"],[93,127,\"0\"],[108,127,\"0\"]],null]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[18,127,\"0\"],[18,24,\"0\"]],null]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[74,82,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,66,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[194,271,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[440,453,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[3205,3266,\"0\"],[3205,3219,\"0\"],[3225,3264,\"0\"],[3225,3243,\"0\"],[3247,3264,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n[2,null,[[0,2]]]\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[536,549,\"1/2\"]],null]]]\n[2,null,[[0,2]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[641,654,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[57,111,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[96,99,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[2,null,[[0,2]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[222,240,\"1/2\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[149,152,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,33,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[13,null,[[0,13]]]\n[13,null,[[0,13]]]\n\n[13,null,[[0,13]]]\n[13,null,[[0,13]]]\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,47,\"0\"]],null]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[14,90,\"0\"],[14,67,\"0\"],[34,67,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[53,82,\"0\"],[71,81,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[12,123,\"0\"],[23,37,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[56,113,\"0\"],[56,73,\"0\"],[77,113,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[15,54,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[66,101,\"0\"],[74,101,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[11,27,\"0\"]],null]]]\n\n\n\n[\"0/6\",\"b\",[[0,\"0/6\",null,[[11,137,\"0\"],[11,42,\"0\"],[47,137,\"0\"],[47,88,\"0\"],[96,136,\"0\"],[109,136,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[201,286,\"0\"],[201,239,\"0\"],[201,221,\"0\"],[245,285,\"0\"],[245,266,\"0\"]],null]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[103,118,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[174,196,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[323,327,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,27,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[54,117,\"0\"],[54,96,\"0\"],[54,70,\"0\"],[74,96,\"0\"],[100,117,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[24,284,\"0\"],[24,63,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[13,25,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[26,36,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[26,36,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[12,24,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[68,76,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[12,24,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[68,80,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[5,null,[[0,5]]]\n\n[1,null,[[0,1]]]\n[2,null,[[0,2]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[60,null,[[0,60]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[113,119,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,19,\"1/2\"]],null]]]\n\n\n[10,null,[[0,10]]]\n[10,null,[[0,10]]]\n[10,null,[[0,10]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[245,250,\"1\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[34,76,\"1/2\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[9,14,\"1\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[59,98,\"1/2\"]],null]]]\n\n[13,null,[[0,13]]]\n\n\n[13,null,[[0,13]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[286,321,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[13,null,[[0,13]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[10,120,\"1\"],[55,119,\"1/2\"]],null]]]\n\n[14,null,[[0,14]]]\n[14,null,[[0,14]]]\n\n\n\n\n[14,null,[[0,14]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[872,880,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1282,1291,\"1/2\"]],null]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[120,null,[[0,120]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[62,69,\"1\"]],null]]]\n[143,null,[[0,143]]]\n\n[120,null,[[0,120]]]\n\n\n[1,null,[[0,1]]]\n[17,null,[[0,17]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[156,172,\"1/2\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[13,32,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[10,49,\"0\"],[10,29,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[63,null,[[0,63]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[184,187,\"1/2\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[14,33,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[11,50,\"0\"],[11,30,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,41,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[14,33,\"1/2\"]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[11,50,\"1/2\"],[11,30,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[20,61,\"1/2\"]],null]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[174,239,\"1/2\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[253,297,\"1/2\"],[261,297,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[15,34,\"0\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[357,453,\"1\"]],null]]]\n\n\n\n[58,null,[[0,58]]]\n\n\n[26,null,[[0,26]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[199,245,\"1\"]],null]]]\n[5,null,[[0,5]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,28,\"1/2\"]],null]]]\n\n[121,null,[[0,121]]]\n[\"1\",\"b\",[[0,\"1\",null,[[40,43,\"1\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[10,44,\"1\"]],null]]]\n[13,null,[[0,13]]]\n\n\n[108,null,[[0,108]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[47,54,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[102,109,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,29,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,48,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[41,47,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[7,43,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[98,134,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[807,814,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[904,914,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[182,185,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,26,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1228,1232,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,32,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,20,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[148,151,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,34,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[517,520,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,103,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[145,155,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[8,null,[[0,8]]]\n\n\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[11,32,\"1\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[11,45,\"0\"]],null]]]\n\n\n[\"4/4\",\"b\",[[0,\"4/4\",null,[[16,209,\"1/2\"],[16,75,\"1\"],[38,73,\"1\"],[45,73,\"1\"]],null]]]\n\n\n\n\n[108,null,[[0,108]]]\n[108,null,[[0,108]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[898,905,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,51,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[8,null,[[0,8]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[141,159,\"1/2\"]],null]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[94,101,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,42,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n[8,null,[[0,8]]]\n\n\n\n[8,null,[[0,8]]]\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[14,36,\"1/2\"]],null]]]\n\n\n[34,null,[[0,34]]]\n\n\n\n\n\n\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[463,472,\"1\"]],null]]]\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[24,68,\"1/2\"],[24,44,\"1/2\"],[48,68,\"1/2\"]],null]]]\n\n\n\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[746,784,\"1\"],[746,755,\"1\"],[760,784,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,27,\"1/2\"]],null]]]\n[121,null,[[0,121]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[23,66,\"1/2\"],[35,66,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[148,179,\"1\"]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[12,52,\"1\"],[27,46,\"1/2\"]],null]]]\n[108,null,[[0,108]]]\n[108,null,[[0,108]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[308,317,\"1\"]],null]]]\n[108,null,[[0,108]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[455,460,\"1/2\"]],null]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[70,94,\"0\"],[77,93,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[200,204,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[34,null,[[0,34]]]\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2136,2163,\"1/2\"],[2145,2163,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,52,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[126,130,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[81,97,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[15,18,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[13,45,\"0\"],[15,44,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[659,752,\"0\"],[672,752,\"0\"],[681,752,\"0\"],[681,702,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[3038,3047,\"1\"]],null]]]\n[21,null,[[0,21]]]\n[21,null,[[0,21]]]\n\n\n[34,null,[[0,34]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[3309,3314,\"1/2\"]],null]]]\n\n\n\n\n[1,null,[[0,1]]]\n[23,null,[[0,23]]]\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[104,111,\"1\"]],null]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[95,101,\"1\"]],null]]]\n[7,null,[[0,7]]]\n\n[8,null,[[0,8]]]\n[\"1\",\"b\",[[0,\"1\",null,[[173,176,\"1\"]],null]]]\n[8,null,[[0,8]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[52,69,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[8,null,[[0,8]]]\n\n\n\n\n[8,null,[[0,8]]]\n\n\n[8,null,[[0,8]]]\n\n[23,null,[[0,23]]]\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[21,null,[[0,21]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[180,193,\"1/2\"]],null]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[335,353,\"1\"]],null]]]\n\n\n[5,null,[[0,5]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[114,251,\"1/2\"],[114,131,\"1/2\"],[136,251,\"0\"],[136,168,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[17,97,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[112,120,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[236,244,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[664,706,\"1/2\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[738,741,\"1\"]],null]]]\n[5,null,[[0,5]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[67,103,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[133,158,\"1/2\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[76,235,\"0\"]],null]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[112,147,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[159,168,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1921,1959,\"1/2\"]],null]]]\n\n\n\n\n\n\n[21,null,[[0,21]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[62832,62888,\"1/2\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[63535,63651,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[45,87,\"1/2\"]],null]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[8,14,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[36,65,\"0\"]],null]]]\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[63908,64084,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[81,125,\"1/2\"]],null]]]\n\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[8,57,\"0\"],[18,57,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[64331,64403,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,44,\"1/2\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[19,25,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,32,\"0\"]],null]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[66,109,\"0\"],[90,109,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[8,27,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,47,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[18,null,[[0,18]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[30,31,\"1\"]],null]]]\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[8,38,\"1/2\"],[8,24,\"1/2\"],[28,38,\"1/2\"]],null]]]\n[74,null,[[0,74]]]\n\n\n\n[18,null,[[0,18]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[\"1\",\"b\",[[0,\"1\",null,[[7,37,\"1\"]],null]]]\n[1,null,[[0,1]]]\n[\"1\",\"b\",[[0,\"1\",null,[[11,52,\"1\"]],null]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[185,203,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[13,41,\"0\"],[13,31,\"0\"]],null]]]\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[371,400,\"1\"]],null]]]\n[3,null,[[0,3]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[13,59,\"1/2\"],[13,49,\"1/2\"]],null]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[606,633,\"1/2\"]],null]]]\n[5,null,[[0,5]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[12,81,\"0\"],[12,58,\"0\"],[12,48,\"0\"],[62,81,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n[16,null,[[0,16]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[32,35,\"1\"]],null]]]\n[3,null,[[0,3]]]\n\n\n[\"2/3\",\"b\",[[0,\"2/3\",null,[[81,122,\"1/2\"],[81,99,\"1/2\"],[103,122,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,51,\"0\"]],null]]]\n\n\n[16,null,[[0,16]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,29,\"1/2\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n[65,null,[[0,65]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[61,89,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[18,25,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,45,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[65,null,[[0,65]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[336,343,\"1\"]],null]]]\n[95,null,[[0,95]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[411,418,\"1\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[40,54,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[40,54,\"1/2\"]],null]]]\n\n\n[2,null,[[0,2]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n[346,null,[[0,346]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[80,89,\"1\"]],null]]]\n[153,null,[[0,153]]]\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[224,242,\"1\"]],null]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[277,305,\"1\"]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[9,105,\"1\"],[9,30,\"1\"]],null]]]\n\n\n\n\n[8,null,[[0,8]]]\n\n\n[49,null,[[0,49]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[367,402,\"1\"],[378,400,\"1/2\"]],null]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[46,56,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[16,41,\"1/2\"]],null]]]\n\n\n\n[8,null,[[0,8]]]\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[402,466,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[80,114,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[8,null,[[0,8]]]\n\n\n\n[17,null,[[0,17]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[63,67,\"1\"]],null]]]\n\n\n[7,null,[[0,7]]]\n[7,null,[[0,7]]]\n\n[17,null,[[0,17]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1575,1601,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[14,29,\"1/2\"]],null]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[2170,2187,\"1\"]],null]]]\n[105,null,[[0,105]]]\n[105,null,[[0,105]]]\n[105,null,[[0,105]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[2327,2356,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[11,35,\"1/2\"]],null]]]\n\n\n\n\n\n\n[30,null,[[0,30]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[26,31,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,47,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[210,242,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[12,17,\"1/2\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[28,50,\"0\"],[35,50,\"0\"]],null]]]\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[51,244,\"0\"],[51,68,\"0\"],[74,81,\"0\"]],null]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[686,704,\"1/2\"]],null]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[50,55,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[13,48,\"0\"],[13,46,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[181,205,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[5,null,[[0,5]]]\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[20,36,\"0\"]],null]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[12,52,\"0\"],[34,52,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[16,null,[[0,16]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[42,74,\"1/2\"],[52,74,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[22,43,\"0\"]],null]]]\n\n\n[18,null,[[0,18]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[10,69,\"0\"]],null]]]\n\n\n[12,null,[[0,12]]]\n[25,null,[[0,25]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[56,84,\"1/2\"]],null]]]\n[25,null,[[0,25]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[122,162,\"1\"],[134,162,\"1/2\"]],null]]]\n[11,null,[[0,11]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[228,243,\"1\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[34,59,\"1/2\"]],null]]]\n[3,null,[[0,3]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[166,191,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[25,null,[[0,25]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[30,null,[[0,30]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[33,69,\"1/2\"]],null]]]\n[50,null,[[0,50]]]\n\n[30,null,[[0,30]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[115,142,\"1/2\"]],null]]]\n\n\n\n[30,null,[[0,30]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[10,null,[[0,10]]]\n\n\n\n[10,null,[[0,10]]]\n[\"1\",\"b\",[[0,\"1\",null,[[205,217,\"1\"]],null]]]\n[10,null,[[0,10]]]\n[\"1\",\"b\",[[0,\"1\",null,[[41,68,\"1\"]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[65,157,\"1/2\"],[65,128,\"1/2\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[665,680,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[10,null,[[0,10]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[784,790,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[74,80,\"1/2\"]],null]]]\n[10,null,[[0,10]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,14,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[83,100,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[37,null,[[0,37]]]\n[37,null,[[0,37]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[13,37,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[14,49,\"1/2\"]],null]]]\n[57,null,[[0,57]]]\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[152,204,\"0\"],[159,204,\"0\"],[173,204,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[567,584,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[37,null,[[0,37]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[32,81,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[75,95,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[12,14,\"0\"]],null]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[10,14,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[5,null,[[0,5]]]\n[5,null,[[0,5]]]\n[5,null,[[0,5]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[5,null,[[0,5]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[35,53,\"1/2\"]],null]]]\n[5,null,[[0,5]]]\n\n[5,null,[[0,5]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,17,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[13,23,\"1/2\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[48,58,\"1\"]],null]]]\n[10,null,[[0,10]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[119,126,\"1/2\"]],null]]]\n[10,null,[[0,10]]]\n\n\n[10,null,[[0,10]]]\n\n\n\n\n[5,null,[[0,5]]]\n[5,null,[[0,5]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[30,null,[[0,30]]]\n\n\n\n[1,null,[[0,1]]]\n[2,null,[[0,2]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[79,135,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[233,286,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[5,null,[[0,5]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[2,null,[[0,2]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[98,157,\"0\"]],null]]]\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[24,57,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[72,121,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n[12,null,[[0,12]]]\n[4,null,[[0,4]]]\n\n\n[4,null,[[0,4]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[190,206,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[4,null,[[0,4]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[400,431,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[714,1000,\"1/2\"]],null]]]\n\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1056,1081,\"1/2\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[83,90,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[124,144,\"1\"]],null]]]\n[2,null,[[0,2]]]\n[2,null,[[0,2]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[309,340,\"1/2\"]],null]]]\n\n\n\n\n\n\n\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[18,47,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[316,337,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[130,149,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[3402,3407,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[153,181,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[4,null,[[0,4]]]\n\n\n\n\n[4,null,[[0,4]]]\n\n\n[4,null,[[0,4]]]\n\n\n\n\n\n\n\n\n\n\n\n[4,null,[[0,4]]]\n\n\n\n\n\n\n\n\n\n\n[4,null,[[0,4]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[13,24,\"1\"]],null]]]\n\n\n\n\n\n[5,null,[[0,5]]]\n[15,null,[[0,15]]]\n\n\n\n\n\n[15,null,[[0,15]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[217,228,\"1\"]],null]]]\n[10,null,[[0,10]]]\n\n\n\n\n[5,null,[[0,5]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[15,null,[[0,15]]]\n\n\n\n\n[15,null,[[0,15]]]\n[\"0\",\"b\",[[0,\"0\",null,[[38,55,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[15,null,[[0,15]]]\n\n\n\n[5,null,[[0,5]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[8147,8151,\"1\"]],null]]]\n[4,null,[[0,4]]]\n\n\n\n[5,null,[[0,5]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[61,81,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[127,143,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[708,722,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[157,259,\"0\"],[157,185,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1108,1111,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[108,188,\"0\"],[126,188,\"0\"],[149,188,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[2,null,[[0,2]]]\n[0,null,[[0,0]]]\n\n\n[2,null,[[0,2]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[8,12,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[70,121,\"1/2\"],[70,83,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[282,321,\"1/2\"],[282,295,\"1/2\"],[299,321,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[105604,105718,\"1/2\"],[105604,105638,\"1/2\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[218,null,[[0,218]]]\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[85,116,\"1\"]],null]]]\n[35,null,[[0,35]]]\n[35,null,[[0,35]]]\n[55,null,[[0,55]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[258,277,\"1\"]],null]]]\n[127,null,[[0,127]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[29,56,\"1/2\"]],null]]]\n[127,null,[[0,127]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[88,92,\"1\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[59,62,\"1/2\"]],null]]]\n[14,null,[[0,14]]]\n[14,null,[[0,14]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[394,396,\"1\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[12,19,\"1\"]],null]]]\n[187,null,[[0,187]]]\n\n\n\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[840,849,\"1\"]],null]]]\n[162,null,[[0,162]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[889,893,\"1\"]],null]]]\n[11,null,[[0,11]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[937,940,\"1\"]],null]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[\"4/4\",\"b\",[[0,\"4/4\",null,[[122,190,\"1/2\"],[122,142,\"1\"],[146,190,\"1/2\"],[146,166,\"1\"]],null]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[2,null,[[0,2]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n[188,null,[[0,188]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[122,128,\"1\"]],null]]]\n[53,null,[[0,53]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[165,184,\"1/2\"]],null]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[105,119,\"1\"]],null]]]\n[52,null,[[0,52]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[188,null,[[0,188]]]\n\n\n[23,null,[[0,23]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[133,157,\"1/2\"]],null]]]\n[23,null,[[0,23]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[23,null,[[0,23]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[10,27,\"1\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[332,418,\"1\"],[332,349,\"1/2\"]],null]]]\n\n\n[6,null,[[0,6]]]\n\n\n\n\n\n\n\n\n[6,null,[[0,6]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[825,844,\"1/2\"]],null]]]\n\n\n[26,null,[[0,26]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[52,71,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[99,116,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[64,85,\"1/2\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n[26,null,[[0,26]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[156,168,\"1/2\"]],null]]]\n\n\n\n\n[26,null,[[0,26]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[497,500,\"1\"]],null]]]\n[52,null,[[0,52]]]\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[720,770,\"1/2\"],[720,737,\"1/2\"]],null]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[242,256,\"1/2\"]],null]]]\n[26,null,[[0,26]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[70,null,[[0,70]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[47,100,\"1\"],[47,66,\"1\"]],null]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[7,22,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[52,68,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[99,114,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[205,224,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[255,274,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[2,null,[[0,2]]]\n\n\n[1,null,[[0,1]]]\n[4,null,[[0,4]]]\n\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[114,155,\"1/2\"],[114,132,\"1/2\"],[136,155,\"1/2\"]],null]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[112,136,\"1\"]],null]]]\n[2,null,[[0,2]]]\n[2,null,[[0,2]]]\n\n\n\n[2,null,[[0,2]]]\n\n[2,null,[[0,2]]]\n\n\n[4,null,[[0,4]]]\n\n\n[1,null,[[0,1]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[10,62,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n[25,null,[[0,25]]]\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[108,125,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,20,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[44,104,\"1/2\"],[44,63,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n[\"1\",\"b\",[[0,\"1\",null,[[37,40,\"1\"]],null]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[91,101,\"1/2\"]],null]]]\n[4,null,[[0,4]]]\n[\"1\",\"b\",[[0,\"1\",null,[[44,73,\"1\"]],null]]]\n[2,null,[[0,2]]]\n[2,null,[[0,2]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[725,748,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[24,null,[[0,24]]]\n[24,null,[[0,24]]]\n\n\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[352,379,\"1\"],[360,379,\"1\"]],null]]]\n\n\n\n[10,null,[[0,10]]]\n[\"1\",\"b\",[[0,\"1\",null,[[140,158,\"1\"]],null]]]\n[8,null,[[0,8]]]\n\n\n\n\n[2,null,[[0,2]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[305,323,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[2,null,[[0,2]]]\n\n\n\n[14,null,[[0,14]]]\n\n\n[14,null,[[0,14]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[22,26,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,25,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[158,162,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,42,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[342,353,\"0\"]],null]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[10,22,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[307,326,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[384,386,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[104,117,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[664,685,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[43,230,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[27,51,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[113,138,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[200,218,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[137,181,\"0\"],[137,150,\"0\"],[154,181,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[22,34,\"0\"]],null]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[10,22,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[218,242,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[296,308,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[321,324,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[70,86,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[131,141,\"0\"]],null]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[182,527,\"0\"],[182,211,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[4,null,[[0,4]]]\n\n\n\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[219,229,\"1/2\"]],null]]]\n\n\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n\n\n[4,null,[[0,4]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[504,548,\"0\"],[521,548,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[51,77,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[150,166,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[246,259,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[170,183,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1378,1388,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[19,50,\"0\"],[37,50,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[114,129,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[223,228,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[118,125,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[303,321,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[8,null,[[0,8]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[152,166,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[36,47,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[109,113,\"0\"]],null]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[225,243,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[23,62,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[73,89,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[390,445,\"0\"],[390,415,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[9,27,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1067,1081,\"1/2\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[8,31,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[8,null,[[0,8]]]\n\n\n[1,null,[[0,1]]]\n\n[4,null,[[0,4]]]\n\n\n[4,null,[[0,4]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[8,34,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,16,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,35,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n[100,null,[[0,100]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[130,181,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[39,49,\"1/2\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[253,300,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[35,45,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[385,444,\"1\"],[385,402,\"1\"],[406,444,\"1/2\"]],null]]]\n[12,null,[[0,12]]]\n\n\n[88,null,[[0,88]]]\n\n\n\n\n[1,null,[[0,1]]]\n[33,null,[[0,33]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[43,48,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[27,null,[[0,27]]]\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[141,146,\"1\"]],null]]]\n[27,null,[[0,27]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[30,48,\"1/2\"],[38,48,\"0\"]],null]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[35,67,\"1\"]],null]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[137,150,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[300,319,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[11,72,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[132,167,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[201,235,\"1/2\"]],null]]]\n[12,null,[[0,12]]]\n\n\n[12,null,[[0,12]]]\n[\"1\",\"b\",[[0,\"1\",null,[[396,399,\"1\"]],null]]]\n[6,null,[[0,6]]]\n\n\n\n\n[12,null,[[0,12]]]\n\n\n[12,null,[[0,12]]]\n\n\n[12,null,[[0,12]]]\n\n\n\n\n\n[27,null,[[0,27]]]\n\n[27,null,[[0,27]]]\n[\"1\",\"b\",[[0,\"1\",null,[[1463,1484,\"1\"]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[74,125,\"1/2\"],[87,125,\"1/2\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,16,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[45,null,[[0,45]]]\n\n\n[45,null,[[0,45]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[388,396,\"1\"]],null]]]\n[16,null,[[0,16]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[463,470,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[24,43,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[10,45,\"0\"],[28,43,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[27,null,[[0,27]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[53,null,[[0,53]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[77,null,[[0,77]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[68,93,\"1/2\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[47,75,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[41,57,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[376,402,\"1\"],[376,388,\"1\"],[392,402,\"1/2\"]],null]]]\n\n\n[37,null,[[0,37]]]\n[37,null,[[0,37]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[489,499,\"1/2\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[8,36,\"1\"]],null]]]\n\n\n[19,null,[[0,19]]]\n[19,null,[[0,19]]]\n\n\n\n[21,null,[[0,21]]]\n[21,null,[[0,21]]]\n[21,null,[[0,21]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[719,731,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[770,773,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[802,811,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[242,288,\"0\"]],null]]]\n\n[77,null,[[0,77]]]\n[108,null,[[0,108]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n[108,null,[[0,108]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[241,250,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[350,365,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[635,643,\"1\"]],null]]]\n[19,null,[[0,19]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[799,812,\"1\"]],null]]]\n[22,null,[[0,22]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[941,970,\"1\"]],null]]]\n[47,null,[[0,47]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[1020,1054,\"1\"]],null]]]\n[47,null,[[0,47]]]\n\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[134,200,\"1/2\"],[134,163,\"1/2\"],[167,200,\"1/2\"]],null]]]\n\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[1449,1495,\"1/2\"],[1449,1460,\"1/2\"]],null]]]\n[108,null,[[0,108]]]\n[\"1\",\"b\",[[0,\"1\",null,[[1527,1530,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,49,\"1/2\"]],null]]]\n[125,null,[[0,125]]]\n[\"1\",\"b\",[[0,\"1\",null,[[100,114,\"1\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[214,219,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[342,376,\"1/2\"]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[473,533,\"1/2\"],[473,481,\"1\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[596,630,\"1/2\"]],null]]]\n\n\n[125,null,[[0,125]]]\n\n\n\n\n\n\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[1040,1070,\"1\"]],null]]]\n[99,null,[[0,99]]]\n[99,null,[[0,99]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[156,246,\"1\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[12,33,\"1/2\"]],null]]]\n[93,null,[[0,93]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1439,1450,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[52,75,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[1658,1666,\"1\"]],null]]]\n[24,null,[[0,24]]]\n\n[101,null,[[0,101]]]\n\n\n\n[125,null,[[0,125]]]\n\n\n\n\n\n\n\n[58,null,[[0,58]]]\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[174,216,\"1\"]],null]]]\n[2,null,[[0,2]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[313,359,\"1/2\"],[313,324,\"1/2\"]],null]]]\n[56,null,[[0,56]]]\n[\"1\",\"b\",[[0,\"1\",null,[[391,394,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,49,\"1/2\"]],null]]]\n[68,null,[[0,68]]]\n[\"1\",\"b\",[[0,\"1\",null,[[100,114,\"1\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[220,225,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[376,410,\"1/2\"]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[424,484,\"1/2\"],[424,432,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[500,520,\"1/2\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[531,619,\"1\"]],null]]]\n\n\n\n[68,null,[[0,68]]]\n[\"1\",\"b\",[[0,\"1\",null,[[698,701,\"1\"]],null]]]\n[69,null,[[0,69]]]\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[44,306,\"1\"],[44,90,\"1/2\"],[59,90,\"1/2\"]],null]]]\n\n\n\n\n[60,null,[[0,60]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[42,60,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[114,128,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[1395,1424,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,104,\"1/2\"]],null]]]\n\n\n[55,null,[[0,55]]]\n\n\n[55,null,[[0,55]]]\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[2099,2129,\"1\"]],null]]]\n[26,null,[[0,26]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[448,468,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[632,704,\"1/2\"],[655,704,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[\"1\",\"b\",[[0,\"1\",null,[[912,976,\"1\"]],null]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[\"1\",\"b\",[[0,\"1\",null,[[64,147,\"1\"]],null]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[175,240,\"1/2\"]],null]]]\n\n[3,null,[[0,3]]]\n[3,null,[[0,3]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[-1,83,\"1/2\"],[-1,47,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[213,230,\"1/2\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[14,44,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1811,1831,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[185,649,\"1/2\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[13,25,\"0\"],[33,55,\"0\"]],null]]]\n\n\n\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[132,206,\"0\"],[132,150,\"0\"],[157,204,\"0\"],[157,179,\"0\"],[183,204,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[74,91,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[151,188,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[34,56,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[374,397,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[569,591,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1680,1711,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n[30,null,[[0,30]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[11,29,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[11,29,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,41,\"1/2\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[10,52,\"0\"],[10,38,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[10,51,\"0\"],[10,38,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[10,84,\"0\"],[10,34,\"0\"],[38,84,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[105,154,\"0\"],[105,131,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[50,74,\"1/2\"]],null]]]\n[55,null,[[0,55]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[58,91,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[164,179,\"1\"]],null]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[-1,130,\"1/2\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[521,562,\"1/2\"],[521,560,\"1/2\"],[535,560,\"1/2\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[7,null,[[0,7]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[991,996,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[1113,1149,\"1/2\"],[1113,1133,\"1\"]],null]]]\n\n\n[8,null,[[0,8]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[80,102,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[82,104,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[91,113,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[68,119,\"0\"],[68,87,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,33,\"0\"]],null]]]\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[267,337,\"0\"],[283,337,\"0\"],[283,303,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,19,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[52,62,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[95,105,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n[4,null,[[0,4]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[259,332,\"0\"],[273,330,\"0\"],[273,291,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[77,null,[[0,77]]]\n\n\n[0,null,[[0,0]]]\n\n\n[42,null,[[0,42]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[31,79,\"1/2\"],[40,79,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[381,406,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[554,606,\"1\"],[554,572,\"1/2\"],[576,606,\"1\"]],null]]]\n\n\n[32,null,[[0,32]]]\n[32,null,[[0,32]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[689,701,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[42,null,[[0,42]]]\n[58,null,[[0,58]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[7,124,\"1/2\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[11,60,\"0\"]],null]]]\n\n\n[14,null,[[0,14]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[16,52,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[58,63,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[6,null,[[0,6]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[73,92,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[166,189,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[119,125,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[45,50,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[568,591,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[130,185,\"0\"],[130,150,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[326,373,\"0\"],[326,346,\"0\"],[350,373,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[20,null,[[0,20]]]\n\n[20,null,[[0,20]]]\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[311,419,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[47,57,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[655,656,\"1/2\"]],null]]]\n[20,null,[[0,20]]]\n[20,null,[[0,20]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[135,167,\"1\"]],null]]]\n[14,null,[[0,14]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[295,311,\"1/2\"]],null]]]\n[20,null,[[0,20]]]\n[20,null,[[0,20]]]\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[279,284,\"1\"]],null]]]\n[20,null,[[0,20]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[32,46,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[122,132,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[20,null,[[0,20]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[727,737,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[204,218,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[37,158,\"0\"],[55,70,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[13,21,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[87,102,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[20,null,[[0,20]]]\n\n\n[1,null,[[0,1]]]\n[10,null,[[0,10]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[94,121,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[8,40,\"1/2\"],[21,40,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[96,111,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,64,\"1/2\"]],null]]]\n[17,null,[[0,17]]]\n\n[17,null,[[0,17]]]\n\n\n\n[10,null,[[0,10]]]\n\n\n[1,null,[[0,1]]]\n\n[12,null,[[0,12]]]\n\n\n\n[6,null,[[0,6]]]\n\n\n\n\n[\"1/5\",\"b\",[[0,\"1/5\",null,[[167,273,\"1/2\"],[196,273,\"0\"],[196,239,\"0\"],[196,215,\"0\"],[219,239,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[202,207,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[616,629,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,26,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[19,48,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[69,100,\"1/2\"]],null]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[144,149,\"1\"]],null]]]\n[6,null,[[0,6]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[6,null,[[0,6]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1036,1059,\"1/2\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[33,68,\"0\"]],null]]]\n\n\n\n[6,null,[[0,6]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[83,116,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,27,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,45,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,22,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,28,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[512,536,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[9,null,[[0,9]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,30,\"0\"]],null]]]\n\n\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[11,77,\"0\"],[11,30,\"0\"],[34,77,\"0\"],[34,54,\"0\"],[58,77,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[14,null,[[0,14]]]\n[\"2/5\",\"b\",[[0,\"2/5\",null,[[9,75,\"1/2\"],[9,28,\"1/2\"],[32,75,\"0\"],[32,52,\"0\"],[56,75,\"0\"]],null]]]\n[14,null,[[0,14]]]\n[14,null,[[0,14]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[9,75,\"0\"],[9,28,\"0\"],[32,75,\"0\"],[32,52,\"0\"],[56,75,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[6,null,[[0,6]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,24,\"1/2\"]],null]]]\n[6,null,[[0,6]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,24,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[36,62,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,28,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[19,40,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[88,113,\"1/2\"]],null]]]\n\n[6,null,[[0,6]]]\n[6,null,[[0,6]]]\n\n\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[15,30,\"1/2\"]],null]]]\n\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[73,115,\"1/2\"],[73,92,\"1/2\"],[96,115,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[220,355,\"0\"],[220,245,\"0\"],[249,355,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[14,19,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[14,29,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[98,117,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[783,787,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[43,78,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[50,56,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n[5,null,[[0,5]]]\n[7,null,[[0,7]]]\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[109,118,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[12,22,\"1/2\"]],null]]]\n[7,null,[[0,7]]]\n\n\n\n[7,null,[[0,7]]]\n\n\n[7,null,[[0,7]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n[72,null,[[0,72]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[240,261,\"1/2\"]],null]]]\n[72,null,[[0,72]]]\n\n\n[72,null,[[0,72]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[67,71,\"1\"]],null]]]\n[148,null,[[0,148]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[387,408,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[492,521,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[548,572,\"1/2\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[747,777,\"1/2\"]],null]]]\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1393,1403,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1638,1680,\"1/2\"]],null]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[148,null,[[0,148]]]\n[148,null,[[0,148]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[148,null,[[0,148]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[73,102,\"1/2\"]],null]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[200,208,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,62,\"1/2\"]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[72,130,\"1/2\"],[72,82,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[434,510,\"1/2\"],[465,510,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1118,1135,\"1/2\"]],null]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[3,null,[[0,3]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,22,\"1/2\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[60,78,\"1/2\"]],null]]]\n[18,null,[[0,18]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[232,235,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[45,63,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[25,null,[[0,25]]]\n[\"1\",\"b\",[[0,\"1\",null,[[125,132,\"1\"]],null]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[44,null,[[0,44]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[92,140,\"1\"],[104,115,\"1\"]],null]]]\n[20,null,[[0,20]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[7,23,\"1\"]],null]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[273,278,\"1\"]],null]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[70,88,\"1\"]],null]]]\n[16,null,[[0,16]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[173,184,\"1/2\"]],null]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[77,96,\"1\"]],null]]]\n[32,null,[[0,32]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[257,275,\"1\"]],null]]]\n[32,null,[[0,32]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[208,227,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[44,null,[[0,44]]]\n\n\n[1,null,[[0,1]]]\n\n\n[24,null,[[0,24]]]\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[343,371,\"1/2\"]],null]]]\n[24,null,[[0,24]]]\n\n\n\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[648,671,\"1/2\"],[648,656,\"1/2\"],[660,671,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[107,129,\"0\"],[107,114,\"0\"],[118,129,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[231,252,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[442,521,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[579,601,\"0\"]],null]]]\n\n\n\n[24,null,[[0,24]]]\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[10,18,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[112,122,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[\"5/5\",\"b\",[[0,\"5/5\",null,[[57,123,\"1/2\"],[66,123,\"1/2\"],[66,85,\"1/2\"],[89,123,\"1/2\"],[89,108,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[35,null,[[0,35]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[295,401,\"1/2\"]],null]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[479,533,\"1\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[579,598,\"1/2\"]],null]]]\n[35,null,[[0,35]]]\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[89,153,\"1/2\"],[89,106,\"1\"],[112,153,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[319,351,\"1/2\"],[319,332,\"1/2\"],[336,351,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[464,481,\"1\"]],null]]]\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[14,77,\"1\"],[14,29,\"1/2\"],[35,63,\"1\"]],null]]]\n\n\n\n[\"1/4\",\"b\",[[0,\"1/4\",null,[[634,712,\"1/2\"],[662,712,\"0\"],[662,674,\"0\"],[678,712,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[847,941,\"1/2\"],[857,941,\"1/2\"]],null]]]\n\n\n[35,null,[[0,35]]]\n\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[77,163,\"0\"],[86,163,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[172,null,[[0,172]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[128,234,\"1/2\"]],null]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[302,356,\"1\"]],null]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[428,451,\"1\"]],null]]]\n[28,null,[[0,28]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[577,594,\"1\"]],null]]]\n[144,null,[[0,144]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[690,736,\"1/2\"],[690,706,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[868,889,\"1\"],[868,880,\"1/2\"]],null]]]\n[104,null,[[0,104]]]\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[39,72,\"1/2\"],[39,53,\"1\"],[75,83,\"1\"]],null]]]\n\n[68,null,[[0,68]]]\n\n\n\n[1,null,[[0,1]]]\n[2,null,[[0,2]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,17,\"1/2\"]],null]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[-1,410,\"1/2\"]],null]]]\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[25,null,[[0,25]]]\n\n\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[290,378,\"1/2\"],[304,378,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[25,null,[[0,25]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[8,16,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[13,204,\"1/2\"]],null]]]\n\n\n[4,null,[[0,4]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n[3,null,[[0,3]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[166,171,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[392,415,\"1\"]],null]]]\n[2,null,[[0,2]]]\n\n\n\n[1,null,[[0,1]]]\n\n[11,null,[[0,11]]]\n[15,null,[[0,15]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[55,77,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[69,76,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[276,295,\"1/2\"]],null]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[57,89,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[187,235,\"0\"],[197,221,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[55,73,\"0\"]],null]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[63,84,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[336,353,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[426,444,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[163,270,\"0\"],[163,188,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[719,747,\"0\"],[730,747,\"0\"]],null]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[151,179,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[243,375,\"0\"],[243,268,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[8,52,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[7,14,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[3,null,[[0,3]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[194,206,\"1/2\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[225,230,\"1\"]],null]]]\n[6,null,[[0,6]]]\n[6,null,[[0,6]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[359,371,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[3,null,[[0,3]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[154,168,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,70,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[336,347,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[52,74,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,25,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[32,66,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[33,55,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[38,66,\"0\"],[48,66,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[76,114,\"0\"],[88,94,\"0\"]],null]]]\n\n\n\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[115,177,\"0\"],[115,131,\"0\"],[135,177,\"0\"],[147,177,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[423,481,\"0\"],[423,451,\"0\"]],null]]]\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[1554,1596,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[1688,1716,\"0\"],[1697,1716,\"0\"]],null]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[346,374,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[383,405,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[517,535,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,23,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[118,154,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[897,973,\"0\"],[897,917,\"0\"],[921,973,\"0\"],[921,947,\"0\"],[951,973,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,47,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[93,103,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[88,110,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[55,73,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[3127,3140,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[65,75,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,17,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,30,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[269,275,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[367,373,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[123,130,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[910,916,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[962,983,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[48,54,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[104,127,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[208,222,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[326,352,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,29,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[9,16,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[65,87,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[422,436,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[578,599,\"0\"],[578,589,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[185,192,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[253,267,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[402,409,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[2279,2293,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[97,103,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,41,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[2655,2696,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[8,34,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[200,214,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[59,91,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[8,15,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[12,46,\"0\"],[21,46,\"0\"]],null]]]\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[323,355,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[8,40,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,41,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[665,704,\"0\"],[665,682,\"0\"],[686,704,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[8,36,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[78,87,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[210,249,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[464,495,\"0\"],[473,495,\"0\"]],null]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[120,144,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[228,256,\"0\"],[242,256,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[16,28,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[143,148,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,45,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[11,68,\"0\"],[28,68,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[438,445,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[10,98,\"0\"],[10,39,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[852,871,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[8,22,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,23,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[333,352,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[484,491,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[10,73,\"0\"],[10,39,\"0\"],[43,73,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[741,755,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,49,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[3,null,[[0,3]]]\n[3,null,[[0,3]]]\n[\"2/3\",\"b\",[[0,\"2/3\",null,[[10,53,\"1/2\"],[10,23,\"1/2\"],[27,53,\"0\"]],null]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[6,null,[[0,6]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[82,99,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[83,116,\"0\"],[95,116,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[271,285,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[37,44,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[7,15,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,41,\"0\"]],null]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[7,34,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[9,18,\"0\"],[21,53,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[70,82,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[289,307,\"1/2\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[622,641,\"1/2\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[50,70,\"1\"]],null]]]\n\n\n\n[8,null,[[0,8]]]\n[16,null,[[0,16]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[170,null,[[0,170]]]\n\n\n\n[\"5/5\",\"b\",[[0,\"5/5\",null,[[120,161,\"1/2\"],[120,131,\"1/2\"],[135,161,\"1/2\"],[135,146,\"1/2\"],[150,161,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[245,285,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[450,489,\"1/2\"],[450,461,\"1/2\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[12,120,\"1\"]],null]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[627,646,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,23,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[91,176,\"1/2\"],[100,176,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n[152,null,[[0,152]]]\n[152,null,[[0,152]]]\n\n\n[\"2/3\",\"b\",[[0,\"2/3\",null,[[924,993,\"1/2\"],[933,993,\"1/2\"],[953,993,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[18,null,[[0,18]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[1136,1147,\"1\"]],null]]]\n\n\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[10,91,\"0\"],[33,91,\"0\"],[33,50,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[79,82,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[16,null,[[0,16]]]\n\n\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[221,253,\"1/2\"],[234,253,\"1/2\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[14,39,\"1\"]],null]]]\n[16,null,[[0,16]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[8,23,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[15,53,\"1/2\"]],null]]]\n\n[16,null,[[0,16]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[67,73,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[173,208,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[50,70,\"1/2\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,47,\"0\"]],null]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n[\"5/5\",\"b\",[[0,\"5/5\",null,[[120,161,\"1/2\"],[120,131,\"1/2\"],[135,161,\"1/2\"],[135,146,\"1/2\"],[150,161,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[189,228,\"1/2\"],[189,200,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[44,74,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[357,376,\"1/2\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[9,94,\"0\"],[18,94,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"1/3\",\"b\",[[0,\"1/3\",null,[[549,618,\"1/2\"],[558,618,\"0\"],[578,618,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[403,411,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[201023,201043,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[89,116,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[89,95,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[37,54,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n[10,null,[[0,10]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[16,50,\"1/2\"]],null]]]\n[179,null,[[0,179]]]\n\n\n\n[1,null,[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[9,64,\"1\"],[9,58,\"1\"]],null]]]\n\n\n[1,null,[[0,1]]]\n\n[41,null,[[0,41]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[77,103,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[237,271,\"1/2\"],[237,262,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[14,48,\"1/2\"]],null]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[64,84,\"1\"]],null]]]\n[46,null,[[0,46]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[44,111,\"1/2\"],[44,63,\"1/2\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[123,126,\"1/2\"]],null]]]\n[46,null,[[0,46]]]\n[\"1\",\"b\",[[0,\"1\",null,[[28,52,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[12,48,\"1/2\"]],null]]]\n[46,null,[[0,46]]]\n\n\n\n\n[46,null,[[0,46]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[267,290,\"1/2\"]],null]]]\n[46,null,[[0,46]]]\n\n\n\n\n\n[41,null,[[0,41]]]\n\n\n\n[30,null,[[0,30]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[77,103,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[240,257,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[310,344,\"1/2\"],[310,335,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[14,48,\"1/2\"]],null]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[64,84,\"1\"]],null]]]\n[42,null,[[0,42]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[118,185,\"1/2\"],[118,137,\"1/2\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[197,200,\"1/2\"]],null]]]\n[42,null,[[0,42]]]\n[\"1\",\"b\",[[0,\"1\",null,[[28,52,\"1\"]],null]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[48,85,\"1\"]],null]]]\n[34,null,[[0,34]]]\n\n\n\n\n[42,null,[[0,42]]]\n[\"1\",\"b\",[[0,\"1\",null,[[329,352,\"1\"]],null]]]\n[23,null,[[0,23]]]\n\n\n\n\n\n[30,null,[[0,30]]]\n\n\n\n[0,null,[[0,0]]]\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[36,86,\"0\"],[36,65,\"0\"],[69,86,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,19,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[176,202,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[49,66,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[94,128,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[145,176,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[69,95,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[495,536,\"0\"],[495,514,\"0\"],[518,536,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[44,53,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[443,460,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n[3,null,[[0,3]]]\n\n\n[3,null,[[0,3]]]\n[\"1\",\"b\",[[0,\"1\",null,[[83,103,\"1\"]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[9,114,\"1/2\"],[9,28,\"1/2\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n[3,null,[[0,3]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[5,null,[[0,5]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[60,77,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,13,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[13,96,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[108,195,\"1/2\"]],null]]]\n\n\n\n[5,null,[[0,5]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[299,322,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[441,452,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[22,41,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[72,82,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[245,256,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[292,315,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[352,373,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,26,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[494,572,\"0\"]],null]]]\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[643,721,\"0\"],[653,721,\"0\"],[676,721,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[62,73,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[214,223,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[10,13,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[346,353,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[134,372,\"0\"],[134,164,\"0\"],[153,164,\"0\"]],null]]]\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[147,150,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[136,139,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[82,173,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[490,500,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[2,null,[[0,2]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[9,32,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[29,79,\"0\"]],null]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[200,216,\"1/2\"]],null]]]\n[2,null,[[0,2]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[11,48,\"1/2\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n[7,null,[[0,7]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[265,281,\"1/2\"]],null]]]\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[338,380,\"1/2\"],[338,357,\"1/2\"],[361,380,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[486,535,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[563,587,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[769,807,\"1/2\"],[769,792,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[904,927,\"1/2\"]],null]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1103,1115,\"1/2\"]],null]]]\n[7,null,[[0,7]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1190,1205,\"1/2\"]],null]]]\n\n\n\n\n[7,null,[[0,7]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1382,1395,\"1/2\"]],null]]]\n[7,null,[[0,7]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1520,1532,\"1/2\"]],null]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1655,1689,\"1/2\"]],null]]]\n[\"2/3\",\"b\",[[0,\"2/3\",null,[[1698,1779,\"1/2\"],[1715,1779,\"1/2\"],[1734,1779,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[1974,2036,\"1/2\"],[1991,2036,\"1/2\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[18,46,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[56,94,\"1/2\"]],null]]]\n[7,null,[[0,7]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[141,144,\"1\"]],null]]]\n[28,null,[[0,28]]]\n[28,null,[[0,28]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[309,351,\"1/2\"],[319,349,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[21,66,\"1/2\"],[35,61,\"0\"]],null]]]\n\n\n\n\n[7,null,[[0,7]]]\n[\"1\",\"b\",[[0,\"1\",null,[[2533,2590,\"1\"]],null]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[18,23,\"1\"]],null]]]\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[109,197,\"1/2\"],[109,144,\"1/2\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[207,213,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[288,311,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[321,364,\"1/2\"],[331,364,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[56,78,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[7,null,[[0,7]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[3173,3217,\"1/2\"]],null]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[12,122,\"1/2\"],[12,94,\"1/2\"]],null]]]\n\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[174,245,\"1/2\"],[184,245,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[110,113,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[347,350,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[7,null,[[0,7]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[8,null,[[0,8]]]\n[7,null,[[0,7]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[32,36,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n[22,null,[[0,22]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,30,\"1/2\"]],null]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[48,63,\"0\"]],null]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[216002,216018,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n[2,null,[[0,2]]]\n[0,null,[[0,0]]]\n\n\n[2,null,[[0,2]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[15,41,\"1/2\"]],null]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[98,107,\"1\"]],null]]]\n[2,null,[[0,2]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[201,214,\"1\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[15,41,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[102,111,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[17,50,\"0\"],[26,50,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[280,336,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[19,40,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,47,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[474,521,\"0\"],[490,521,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[82,118,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[428,493,\"0\"],[453,491,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[132,140,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[99,276,\"0\"],[112,276,\"0\"]],null]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[45,56,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[92,113,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n[2,null,[[0,2]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[9,47,\"1\"]],null]]]\n[2,null,[[0,2]]]\n[2,null,[[0,2]]]\n\n\n[5,null,[[0,5]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[228,253,\"1/2\"]],null]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[65,94,\"1\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[39,60,\"1/2\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,43,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[76,103,\"0\"]],null]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[32,59,\"1/2\"]],null]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[64,91,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[93,194,\"0\"],[93,132,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[324,340,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[669,741,\"0\"],[706,741,\"0\"]],null]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[2,null,[[0,2]]]\n\n\n[2,null,[[0,2]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[8,32,\"1/2\"]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[6,24,\"1/2\"],[38,59,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[228,232,\"1/2\"]],null]]]\n[2,null,[[0,2]]]\n\n\n[2,null,[[0,2]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[170,192,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[29,45,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,64,\"0\"]],null]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[378,380,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,56,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[600,627,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,71,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[126,140,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[288,318,\"0\"]],null]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1120,1133,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[8,40,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[227,241,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[427,434,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[9,36,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[143,177,\"0\"],[152,177,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[296,303,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[72,87,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[207,239,\"0\"],[207,219,\"0\"],[223,239,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[44,110,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[156,161,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[79,99,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[69,145,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[159,163,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[58,71,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[190,218,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[873,886,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[79,95,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,18,\"1/2\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[62,85,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[180,193,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[27,36,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,28,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[47,95,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[326,339,\"0\"]],null]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[13,22,\"0\"]],null]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[11,28,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[11,28,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[26,29,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,21,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[22,44,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[56,65,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[-1,28,\"0\"],[6,28,\"0\"]],null]]]\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[3528,3580,\"0\"],[3546,3580,\"0\"],[3562,3580,\"0\"]],null]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[3629,3695,\"0\"],[3629,3646,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[3795,3816,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[193,293,\"0\"]],null]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[4561,4614,\"0\"],[4571,4614,\"0\"],[4588,4614,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[4830,4839,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[5021,5045,\"0\"]],null]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[5092,5128,\"0\"],[5107,5128,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[5579,5592,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[150,156,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[19,42,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[377,394,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[71,94,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[6336,6442,\"0\"],[6346,6442,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[6581,6593,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,40,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[142,165,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[6907,6979,\"0\"],[6907,6956,\"0\"],[6917,6956,\"0\"],[6933,6956,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[7517,7609,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[8021,8031,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[59,70,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[206,215,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[266,290,\"0\"],[277,290,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[53,62,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[131,140,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[226,238,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[487,500,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[546,556,\"0\"]],null]]]\n\n\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[612,659,\"0\"],[612,641,\"0\"],[612,625,\"0\"],[629,641,\"0\"],[645,659,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[694,703,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[958,967,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[96,108,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[71,79,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[202,210,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[413,448,\"0\"],[413,427,\"0\"],[431,448,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[525,539,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[101,122,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[38,48,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[2042,2072,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[2110,2119,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[2397,2408,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[33,42,\"0\"]],null]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[2638,2649,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[113,133,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[2,null,[[0,2]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[59,84,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,27,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[6,null,[[0,6]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[21,30,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,34,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[6,null,[[0,6]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[207,227,\"1/2\"]],null]]]\n[6,null,[[0,6]]]\n\n\n\n[6,null,[[0,6]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[35,57,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[6,null,[[0,6]]]\n[6,null,[[0,6]]]\n\n\n[6,null,[[0,6]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[8,33,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[72,87,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[3,null,[[0,3]]]\n\n[3,null,[[0,3]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[28,38,\"1/2\"]],null]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[13,82,\"0\"],[33,82,\"0\"]],null]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[244511,244566,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[104,156,\"0\"],[120,156,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[211,228,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[371,411,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[803,857,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,20,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[128,144,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[190,206,\"0\"]],null]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[159,189,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[2494,2519,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[68,88,\"0\"]],null]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[14,22,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[75,117,\"0\"],[10,44,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[86,94,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[10,18,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[7,20,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[7,28,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[61,74,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[65,78,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[58,61,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[18,38,\"0\"]],null]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[10,18,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[18,78,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[373,413,\"0\"],[385,413,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[110,146,\"0\"]],null]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[243,251,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[344,361,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[15,35,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,27,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[59,84,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[273,290,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[575,628,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[124,152,\"1/2\"]],null]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[7,31,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[58,86,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[8,null,[[0,8]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[174,182,\"1/2\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[122,148,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[8,null,[[0,8]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[746,764,\"1/2\"]],null]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[788,794,\"1\"]],null]]]\n[1,null,[[0,1]]]\n\n\n[7,null,[[0,7]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[916,941,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[7,null,[[0,7]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[82,90,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[208,235,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[373,409,\"0\"],[383,409,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[492,507,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[91,99,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[896,1053,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[27,76,\"0\"]],null]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[6,null,[[0,6]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,26,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[9,32,\"0\"],[42,81,\"0\"],[42,61,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[258,279,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[466,571,\"0\"],[466,513,\"0\"],[466,489,\"0\"],[493,513,\"0\"]],null]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[700,717,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[13,41,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[56,85,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[929,957,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1130,1149,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[1222,1242,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1320,1338,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[43,59,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,32,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[264,269,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[409,438,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[579,604,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[8,18,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[254,296,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[133,178,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[54,121,\"0\"],[70,121,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[188,219,\"0\"]],null]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"1\",\"b\",[[0,\"1\",null,[[12,34,\"1\"]],null]]]\n\n[2,null,[[0,2]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[42,59,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,15,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[125,128,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[2,null,[[0,2]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[9,17,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[106,132,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[2,null,[[0,2]]]\n\n\n\n[6,null,[[0,6]]]\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[20,87,\"1\"],[42,85,\"1\"],[58,85,\"1/2\"]],null]]]\n\n\n[32,null,[[0,32]]]\n[44,null,[[0,44]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[24,47,\"1/2\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[94,127,\"0\"]],null]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[320,339,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[690,709,\"1\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[60,82,\"0\"]],null]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[265349,265391,\"1/2\"],[265349,265377,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[7,26,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[58,90,\"0\"],[66,90,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[265940,265949,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n[\"36/540\",\"b\",[[0,\"36/540\",null,[[1145,1182,\"1/2\"],[1145,1170,\"1/2\"],[1272,1285,\"0\"],[1328,1340,\"0\"],[1367,1392,\"0\"],[1377,1392,\"0\"],[1398,1514,\"0\"],[1419,1514,\"0\"],[1419,1462,\"0\"],[1419,1433,\"0\"],[1435,1462,\"0\"],[1435,1449,\"0\"],[1451,1462,\"0\"],[1497,1513,\"0\"],[1508,1513,\"0\"],[3770,3868,\"0\"],[3770,3819,\"0\"],[3869,3967,\"0\"],[3869,3918,\"0\"],[4003,4391,\"0\"],[4038,4046,\"0\"],[4199,4294,\"0\"],[4199,4248,\"0\"],[4295,4390,\"0\"],[4295,4344,\"0\"],[4437,4460,\"1/2\"],[4437,4447,\"1/2\"],[4567,4614,\"0\"],[4567,4589,\"0\"],[4621,4629,\"1/2\"],[4745,4759,\"0\"],[4790,4832,\"0\"],[4793,4832,\"0\"],[4960,4979,\"1/2\"],[4980,5033,\"1\"],[5101,5109,\"1/2\"],[5151,5169,\"1/2\"],[5171,5212,\"1/2\"],[5389,5404,\"1\"],[5766,5767,\"1\"],[5768,5790,\"1/2\"],[5855,5856,\"1\"],[6129,6132,\"1\"],[6161,6295,\"1\"],[6185,6295,\"1\"],[6185,6195,\"1/2\"],[6203,6221,\"1\"],[6222,6243,\"1\"],[6342,6371,\"1/2\"],[6398,6416,\"0\"],[6453,6454,\"0\"],[6455,6482,\"0\"],[6468,6482,\"0\"],[6533,6547,\"0\"],[6557,6558,\"0\"],[6559,6596,\"0\"],[6579,6596,\"0\"],[6616,6633,\"0\"],[6616,6621,\"0\"],[6623,6633,\"0\"],[6637,6648,\"0\"],[6857,6912,\"0\"],[6955,6956,\"0\"],[6967,6972,\"0\"],[6974,6992,\"0\"],[7244,7272,\"0\"],[5,30,\"0\"],[7444,7697,\"0\"],[7444,7452,\"0\"],[7535,7563,\"0\"],[7535,7547,\"0\"],[7583,7590,\"0\"],[7607,7620,\"0\"],[7636,7695,\"0\"],[7786,7855,\"0\"],[8405,8425,\"0\"],[8469,8487,\"0\"],[8491,8531,\"0\"],[8584,8596,\"0\"],[8609,8620,\"0\"],[8634,8664,\"0\"],[8644,8664,\"0\"],[8672,8685,\"0\"],[8711,8731,\"0\"],[8739,8763,\"0\"],[8934,8974,\"0\"],[8934,8947,\"0\"],[8993,9035,\"0\"],[8993,9007,\"0\"],[9124,9269,\"0\"],[9124,9154,\"0\"],[9156,9269,\"0\"],[9159,9269,\"0\"],[9376,9492,\"0\"],[9664,9674,\"0\"],[9681,9714,\"0\"],[9717,9722,\"0\"],[9838,9881,\"0\"],[9841,9881,\"0\"],[9946,9970,\"0\"],[10029,10067,\"0\"],[10039,10063,\"0\"],[10068,10105,\"0\"],[10079,10104,\"0\"],[10209,10289,\"0\"],[10209,10235,\"0\"],[10453,10472,\"0\"],[10483,10511,\"0\"],[10483,10501,\"0\"],[10503,10511,\"0\"],[10521,10522,\"0\"],[10532,10533,\"0\"],[10546,10547,\"0\"],[10658,10693,\"0\"],[10658,10677,\"0\"],[10694,10695,\"0\"],[10822,10888,\"0\"],[10825,10888,\"0\"],[10825,10848,\"0\"],[10890,10908,\"0\"],[10943,11003,\"0\"],[10943,10961,\"0\"],[10978,11002,\"0\"],[10986,11002,\"0\"],[11071,11072,\"0\"],[11120,11125,\"0\"],[11382,11400,\"0\"],[11459,11463,\"0\"],[11995,12135,\"0\"],[11997,12002,\"0\"],[12024,12050,\"0\"],[12147,12166,\"0\"],[12202,12286,\"0\"],[12202,12262,\"0\"],[12219,12262,\"0\"],[12384,12418,\"0\"],[12384,12402,\"0\"],[12427,12428,\"0\"],[12429,12455,\"0\"],[12429,12435,\"0\"],[12437,12455,\"0\"],[12458,12469,\"0\"],[12474,12479,\"0\"],[12480,12516,\"0\"],[12480,12498,\"0\"],[12552,12577,\"0\"],[12578,12611,\"0\"],[12581,12611,\"0\"],[12620,12631,\"0\"],[12620,12625,\"0\"],[12693,12708,\"0\"],[12781,12793,\"0\"],[12821,12833,\"0\"],[12867,12893,\"0\"],[12926,12940,\"0\"],[12999,13012,\"0\"],[13096,13112,\"0\"],[13401,13411,\"0\"],[13676,13703,\"0\"],[13676,13681,\"0\"],[13754,13778,\"0\"],[13812,13836,\"0\"],[13870,13928,\"0\"],[13870,13882,\"0\"],[13884,13928,\"0\"],[13884,13894,\"0\"],[13896,13928,\"0\"],[13931,13991,\"0\"],[13931,13943,\"0\"],[13945,13991,\"0\"],[13945,13955,\"0\"],[13957,13991,\"0\"],[14005,14006,\"0\"],[14044,14045,\"0\"],[14112,14121,\"0\"],[14144,14169,\"0\"],[14152,14169,\"0\"],[14172,14178,\"0\"],[14227,14228,\"0\"],[14384,14393,\"0\"],[14538,14557,\"0\"],[14589,14627,\"0\"],[14718,14729,\"0\"],[14742,14847,\"0\"],[14742,14754,\"0\"],[14759,14771,\"0\"],[14793,14805,\"0\"],[14853,14865,\"0\"],[14885,14897,\"0\"],[14951,14952,\"0\"],[14960,14961,\"0\"],[15014,15039,\"0\"],[15014,15026,\"0\"],[15040,15057,\"0\"],[15068,15101,\"0\"],[15068,15086,\"0\"],[15102,15120,\"0\"],[15130,15162,\"0\"],[15130,15148,\"0\"],[15453,15470,\"0\"],[15481,15514,\"0\"],[15481,15499,\"0\"],[15515,15533,\"0\"],[15543,15575,\"0\"],[15543,15561,\"0\"],[15666,15884,\"0\"],[15887,16328,\"0\"],[16092,16095,\"0\"],[16103,16106,\"0\"],[16133,16136,\"0\"],[16143,16146,\"0\"],[16166,16204,\"0\"],[16166,16169,\"0\"],[16171,16204,\"0\"],[16171,16179,\"0\"],[16205,16241,\"0\"],[16205,16208,\"0\"],[16210,16241,\"0\"],[16210,16218,\"0\"],[16254,16279,\"0\"],[16422,16432,\"0\"],[16541,16559,\"0\"],[16560,16569,\"0\"],[16560,16563,\"0\"],[16565,16569,\"0\"],[16623,16632,\"0\"],[16623,16626,\"0\"],[16628,16632,\"0\"],[16635,16638,\"0\"],[16662,16665,\"0\"],[16676,16679,\"0\"],[16754,16764,\"0\"],[16878,16897,\"0\"],[16898,16907,\"0\"],[16898,16901,\"0\"],[16903,16907,\"0\"],[16959,16968,\"0\"],[16959,16962,\"0\"],[16964,16968,\"0\"],[16971,16974,\"0\"],[16999,17002,\"0\"],[17012,17015,\"0\"],[17135,17145,\"0\"],[17246,17262,\"0\"],[17276,17293,\"0\"],[17310,17326,\"0\"],[17341,17358,\"0\"],[17393,17396,\"0\"],[17435,17464,\"0\"],[17435,17446,\"0\"],[17435,17438,\"0\"],[17440,17446,\"0\"],[17466,17551,\"0\"],[17466,17469,\"0\"],[17521,17550,\"0\"],[17521,17532,\"0\"],[17521,17524,\"0\"],[17526,17532,\"0\"],[17630,17640,\"0\"],[17738,17753,\"0\"],[17756,17757,\"0\"],[17772,17790,\"0\"],[17808,17823,\"0\"],[17839,17857,\"0\"],[17893,17896,\"0\"],[17935,17963,\"0\"],[17935,17946,\"0\"],[17935,17938,\"0\"],[17940,17946,\"0\"],[17965,18047,\"0\"],[17965,17968,\"0\"],[18018,18046,\"0\"],[18018,18029,\"0\"],[18018,18021,\"0\"],[18023,18029,\"0\"],[18318,18337,\"1/2\"],[18501,18548,\"1/2\"],[18869,18882,\"0\"],[18890,18897,\"0\"],[18898,18907,\"0\"],[18922,18929,\"0\"],[18948,18956,\"0\"],[18963,18968,\"0\"],[18985,18988,\"0\"],[18991,18998,\"0\"],[19111,19124,\"0\"],[19127,19142,\"0\"],[19150,19151,\"0\"],[19217,19225,\"0\"],[19227,19274,\"0\"],[19227,19247,\"0\"],[19318,19323,\"0\"],[19336,19341,\"0\"],[19344,19349,\"0\"],[20764,20806,\"1/2\"],[20938,20943,\"0\"],[20990,21032,\"0\"],[20990,21010,\"0\"],[21079,21109,\"0\"],[21079,21084,\"0\"],[21110,21122,\"0\"],[21134,21150,\"0\"],[21152,21163,\"0\"],[21225,21237,\"0\"],[21239,21253,\"0\"],[21272,21315,\"0\"],[21377,21388,\"0\"],[21393,21416,\"0\"],[21393,21404,\"0\"],[21406,21416,\"0\"],[21470,21553,\"0\"],[21476,21553,\"0\"],[21476,21509,\"0\"],[21522,21552,\"0\"],[21654,21768,\"0\"],[21660,21695,\"0\"],[21672,21695,\"0\"],[21672,21691,\"0\"],[21727,21741,\"0\"],[21744,21763,\"0\"],[21835,21856,\"0\"],[21926,21942,\"0\"],[21967,21992,\"0\"],[22077,22090,\"0\"],[22091,22194,\"0\"],[22091,22099,\"0\"],[22102,22110,\"0\"],[22119,22172,\"0\"],[22127,22138,\"0\"],[22148,22171,\"0\"],[22148,22159,\"0\"],[22230,22247,\"0\"],[22442,22449,\"0\"],[22450,22453,\"0\"],[22469,22497,\"0\"],[22469,22477,\"0\"],[22593,22613,\"0\"],[22593,22600,\"0\"],[22605,22608,\"0\"],[22614,22647,\"0\"],[22617,22647,\"0\"],[22617,22620,\"0\"],[22659,22687,\"0\"],[22659,22667,\"0\"],[22770,22790,\"0\"],[22825,22829,\"0\"],[22844,22856,\"0\"],[22904,22921,\"0\"],[23010,23044,\"0\"],[23010,23020,\"0\"],[23022,23044,\"0\"],[23022,23032,\"0\"],[23034,23044,\"0\"],[23185,23190,\"0\"],[23193,23198,\"0\"],[23214,23219,\"0\"],[23252,23257,\"0\"],[23260,23265,\"0\"],[23301,23308,\"0\"],[23342,23376,\"0\"],[23342,23352,\"0\"],[23354,23376,\"0\"],[23354,23364,\"0\"],[23366,23376,\"0\"],[23443,23448,\"0\"],[23648,23690,\"0\"],[23648,23684,\"0\"],[23651,23684,\"0\"],[23685,23690,\"0\"],[23734,23759,\"0\"],[23734,23745,\"0\"],[23747,23759,\"0\"],[23823,23835,\"0\"],[23845,23866,\"0\"],[23845,23852,\"0\"],[23885,23886,\"0\"],[23932,24236,\"1/2\"],[23982,23993,\"0\"],[23994,24004,\"0\"],[24050,24065,\"0\"],[24069,24115,\"0\"],[24069,24083,\"0\"],[24116,24132,\"0\"],[24116,24123,\"0\"],[24139,24210,\"0\"],[24139,24151,\"0\"],[24166,24209,\"0\"],[24192,24202,\"0\"],[24346,24397,\"0\"],[24346,24363,\"0\"],[24366,24396,\"0\"],[24366,24386,\"0\"],[24402,24435,\"0\"],[24406,24410,\"0\"],[24412,24435,\"0\"],[24421,24435,\"0\"],[24443,24464,\"0\"],[24481,24519,\"0\"],[24481,24506,\"0\"],[24481,24487,\"0\"],[24489,24506,\"0\"],[24509,24519,\"0\"],[24589,24609,\"0\"],[24592,24609,\"0\"],[24698,24762,\"0\"],[25333,25360,\"0\"],[25437,25463,\"0\"],[25440,25463,\"0\"],[25450,25463,\"0\"],[25479,25482,\"0\"],[25491,25538,\"0\"],[25491,25512,\"0\"],[25555,25589,\"0\"],[25555,25576,\"0\"],[25646,25711,\"0\"],[25646,25654,\"0\"],[25657,25710,\"0\"],[25664,25710,\"0\"],[25664,25699,\"0\"],[25990,26080,\"0\"],[25990,26039,\"0\"],[25990,26016,\"0\"],[25990,26004,\"0\"],[26018,26039,\"0\"],[26018,26027,\"0\"],[26084,26192,\"1/2\"],[26140,26147,\"0\"],[26302,26321,\"0\"],[26324,26334,\"0\"],[26457,26481,\"0\"],[26977,26978,\"1/2\"],[27124,27142,\"1/2\"],[27295,27326,\"0\"],[27295,27314,\"0\"],[27316,27326,\"0\"],[27327,27328,\"0\"],[27362,27363,\"0\"],[37,73,\"0\"],[87,102,\"0\"],[87,94,\"0\"],[103,137,\"0\"],[139,190,\"0\"],[139,173,\"0\"],[139,157,\"0\"],[191,220,\"0\"],[221,237,\"0\"],[240,253,\"0\"],[265,273,\"0\"],[276,294,\"0\"],[297,313,\"0\"],[361,374,\"0\"],[397,435,\"1/2\"],[401,435,\"0\"],[401,419,\"0\"],[439,478,\"0\"],[439,457,\"0\"],[479,494,\"0\"],[498,526,\"0\"],[498,516,\"0\"],[731,754,\"0\"],[766,785,\"0\"],[792,805,\"0\"],[832,845,\"0\"],[870,889,\"0\"],[891,1050,\"1/2\"],[899,1050,\"1/2\"],[915,1050,\"1/2\"],[1002,1022,\"0\"],[1051,2390,\"1/2\"],[1051,1070,\"1/2\"],[1134,1137,\"0\"],[1142,1186,\"0\"],[1142,1153,\"0\"],[1235,1238,\"0\"],[1243,1288,\"0\"],[1243,1254,\"0\"],[1318,1365,\"0\"],[1318,1330,\"0\"],[1335,1350,\"0\"],[1398,1434,\"0\"],[1778,1830,\"0\"],[1778,1806,\"0\"],[1778,1786,\"0\"],[1844,1872,\"0\"],[2066,2105,\"0\"],[2266,2385,\"0\"],[2332,2384,\"0\"],[2332,2360,\"0\"],[2332,2340,\"0\"],[2458,2477,\"1/2\"],[2566,2571,\"0\"],[2630,2646,\"0\"],[2648,2654,\"0\"],[2664,2678,\"0\"],[2680,2686,\"0\"],[2965,3015,\"0\"],[2965,2968,\"0\"],[3125,3138,\"0\"],[3203,3236,\"0\"],[3203,3215,\"0\"],[3220,3221,\"0\"],[3238,3274,\"0\"],[3238,3239,\"0\"],[3240,3250,\"0\"],[3251,3261,\"0\"],[3807,4259,\"0\"],[3906,3945,\"0\"],[4369,4400,\"0\"],[4521,4526,\"0\"],[4569,4595,\"0\"],[4569,4575,\"0\"],[4698,4723,\"0\"],[4698,4713,\"0\"],[4741,4770,\"0\"],[4771,4791,\"0\"],[4823,4845,\"0\"],[4823,4842,\"0\"],[4846,4861,\"0\"],[4893,4931,\"0\"],[4893,4908,\"0\"],[4909,4919,\"0\"],[4920,4930,\"0\"],[5042,5049,\"0\"],[5107,5129,\"0\"],[5153,5197,\"0\"],[5157,5197,\"0\"],[5157,5186,\"0\"],[5157,5167,\"0\"],[5169,5186,\"0\"],[5169,5174,\"0\"],[5176,5186,\"0\"],[5198,5235,\"0\"],[5198,5211,\"0\"],[5201,5211,\"0\"],[5236,5256,\"0\"],[5265,5277,\"0\"],[5278,5279,\"0\"],[5323,5338,\"0\"],[5341,5347,\"0\"],[33,37,\"1/2\"]],null]]]\n[\"5/406\",\"b\",[[0,\"5/406\",null,[[985,989,\"1/2\"],[1151,1176,\"0\"],[1157,1176,\"0\"],[1393,1430,\"0\"],[1393,1407,\"0\"],[1463,1464,\"0\"],[1619,1646,\"0\"],[1661,1662,\"0\"],[1681,1682,\"0\"],[1970,1971,\"0\"],[2045,2065,\"0\"],[5822,5925,\"0\"],[5863,5910,\"0\"],[5863,5885,\"0\"],[16085,16097,\"0\"],[16085,16090,\"0\"],[16092,16097,\"0\"],[16230,16256,\"0\"],[16458,16462,\"0\"],[34297,34314,\"0\"],[34418,34496,\"0\"],[34418,34433,\"0\"],[34454,34486,\"0\"],[34497,34553,\"0\"],[34698,34708,\"0\"],[34711,34721,\"0\"],[34724,34741,\"0\"],[34757,34767,\"0\"],[34775,34779,\"0\"],[34813,34833,\"0\"],[34813,34821,\"0\"],[34823,34833,\"0\"],[34849,34869,\"0\"],[34849,34857,\"0\"],[34859,34869,\"0\"],[34939,34989,\"0\"],[34947,34956,\"0\"],[34990,35067,\"0\"],[35040,35041,\"0\"],[35068,35091,\"0\"],[35104,35107,\"0\"],[35123,35124,\"0\"],[35172,35173,\"0\"],[35182,35238,\"0\"],[35206,35207,\"0\"],[35347,35370,\"0\"],[35373,35383,\"0\"],[35386,35405,\"0\"],[35389,35405,\"0\"],[35408,35425,\"0\"],[35411,35425,\"0\"],[35452,35453,\"0\"],[35485,35486,\"0\"],[35521,35522,\"0\"],[35556,35557,\"0\"],[35616,35661,\"0\"],[35616,35631,\"0\"],[35807,35817,\"0\"],[35820,35839,\"0\"],[35842,35862,\"0\"],[35842,35850,\"0\"],[35852,35862,\"0\"],[35878,35898,\"0\"],[35878,35886,\"0\"],[35888,35898,\"0\"],[35911,35919,\"0\"],[35977,36034,\"0\"],[35991,36000,\"0\"],[36044,36078,\"0\"],[36226,36245,\"0\"],[36226,36240,\"0\"],[36325,36333,\"0\"],[36393,36403,\"0\"],[36525,36528,\"0\"],[36565,36568,\"0\"],[36820,36821,\"0\"],[36836,36837,\"0\"],[36853,36854,\"0\"],[36878,36879,\"0\"],[36894,36895,\"0\"],[36911,36912,\"0\"],[36918,36933,\"0\"],[37001,37016,\"0\"],[37039,37040,\"0\"],[37063,37064,\"0\"],[37201,37211,\"0\"],[37214,37224,\"0\"],[37227,37237,\"0\"],[37279,37280,\"0\"],[37468,37505,\"0\"],[37498,37499,\"0\"],[37551,37615,\"0\"],[37573,37605,\"0\"],[37636,37726,\"0\"],[37922,37952,\"0\"],[37922,37937,\"0\"],[38022,38040,\"0\"],[38338,38350,\"0\"],[38353,38368,\"0\"],[38371,38400,\"0\"],[38470,38479,\"0\"],[38482,38519,\"0\"],[38551,38576,\"0\"],[38551,38561,\"0\"],[38670,38901,\"0\"],[38670,38691,\"0\"],[38670,38679,\"0\"],[38681,38691,\"0\"],[38695,38797,\"0\"],[38695,38712,\"0\"],[38798,38900,\"0\"],[38798,38815,\"0\"],[38903,39033,\"0\"],[38903,38928,\"0\"],[38903,38916,\"0\"],[38918,38928,\"0\"],[38931,39033,\"0\"],[38931,38948,\"0\"],[39034,39251,\"0\"],[39262,40003,\"0\"],[39262,39287,\"0\"],[39262,39275,\"0\"],[39277,39287,\"0\"],[39686,39788,\"0\"],[39686,39703,\"0\"],[39789,39891,\"0\"],[39789,39806,\"0\"],[39892,39917,\"0\"],[39970,39998,\"0\"],[40099,40140,\"0\"],[40099,40112,\"0\"],[40141,40222,\"0\"],[40162,40174,\"0\"],[40296,40369,\"0\"],[40321,40347,\"0\"],[40350,40362,\"0\"],[40454,40473,\"0\"],[40482,40511,\"0\"],[40516,40557,\"0\"],[40682,40709,\"0\"],[40825,40835,\"0\"],[40838,40848,\"0\"],[40851,40855,\"0\"],[40861,40871,\"0\"],[40874,40875,\"0\"],[40928,40982,\"0\"],[40928,40948,\"0\"],[40798,40799,\"0\"],[40928,40982,\"0\"],[130,150,\"0\"],[41164,41183,\"0\"],[41186,41200,\"0\"],[41203,41213,\"0\"],[41251,41271,\"0\"],[41251,41259,\"0\"],[41261,41271,\"0\"],[41287,41307,\"0\"],[41287,41295,\"0\"],[41297,41307,\"0\"],[41383,41384,\"0\"],[41404,41405,\"0\"],[41427,41428,\"0\"],[41468,41471,\"0\"],[41774,41793,\"0\"],[41796,41816,\"0\"],[41796,41804,\"0\"],[41806,41816,\"0\"],[41832,41852,\"0\"],[41832,41840,\"0\"],[41842,41852,\"0\"],[41855,41910,\"0\"],[41869,41878,\"0\"],[41985,41986,\"0\"],[42048,42109,\"0\"],[42048,42058,\"0\"],[42189,42279,\"1/2\"],[42189,42208,\"1/2\"],[42356,42366,\"0\"],[42392,42435,\"0\"],[42400,42435,\"0\"],[42403,42435,\"0\"],[42465,42493,\"0\"],[42465,42475,\"0\"],[42500,42549,\"0\"],[42565,42618,\"0\"],[42599,42617,\"0\"],[42622,42629,\"0\"],[42630,42639,\"0\"],[42642,42673,\"0\"],[42645,42673,\"0\"],[42744,42770,\"0\"],[42817,42846,\"0\"],[43087,43133,\"0\"],[43141,43186,\"0\"],[43187,43255,\"0\"],[43353,43369,\"0\"],[43449,43457,\"0\"],[43580,44395,\"1/2\"],[43580,43614,\"1/2\"],[43707,43745,\"0\"],[43746,43799,\"0\"],[43753,43798,\"0\"],[43800,43845,\"0\"],[43807,43844,\"0\"],[43856,43867,\"0\"],[44071,44081,\"0\"],[44196,44214,\"0\"],[44343,44350,\"0\"],[44728,44765,\"0\"],[44846,45001,\"0\"],[44888,44896,\"0\"],[45081,45095,\"0\"],[45098,45099,\"0\"],[45195,45226,\"0\"],[45198,45226,\"0\"],[45310,45331,\"0\"],[45310,45321,\"0\"],[45336,45367,\"0\"],[45442,45449,\"0\"],[45457,45487,\"0\"],[45457,45465,\"0\"],[45461,45465,\"0\"],[45578,45610,\"0\"],[45671,45724,\"0\"],[46531,46589,\"0\"],[46546,46589,\"0\"],[46546,46575,\"0\"],[46546,46559,\"0\"],[46561,46575,\"0\"],[46612,46655,\"0\"],[46612,46622,\"0\"],[46736,46754,\"0\"],[46833,47102,\"0\"],[47526,47582,\"0\"],[47526,47562,\"0\"],[47615,47627,\"0\"],[47656,47754,\"0\"],[47656,47667,\"0\"],[47670,47732,\"0\"],[47772,47837,\"0\"],[47772,47789,\"0\"],[47791,47837,\"0\"],[47794,47837,\"0\"],[47794,47818,\"0\"],[47838,47896,\"0\"],[47838,47849,\"0\"],[47873,47895,\"0\"],[48148,48169,\"0\"],[48496,48597,\"0\"],[48626,48708,\"0\"],[48626,48653,\"0\"],[48655,48708,\"0\"],[48770,48825,\"0\"],[48770,48803,\"0\"],[48770,48783,\"0\"],[48785,48803,\"0\"],[48856,48869,\"0\"],[48888,48951,\"0\"],[48952,49020,\"0\"],[49551,49616,\"0\"],[50315,50333,\"0\"],[50533,50543,\"0\"],[50642,50693,\"0\"],[50642,50656,\"0\"],[50658,50693,\"0\"],[50658,50669,\"0\"],[50885,51063,\"0\"],[50885,50895,\"0\"],[50967,50984,\"0\"],[51033,51050,\"0\"],[51116,51226,\"0\"],[51116,51134,\"0\"],[51139,51156,\"0\"],[51258,51276,\"0\"],[51353,51412,\"0\"],[51690,51701,\"0\"],[51704,51720,\"0\"],[51723,51724,\"0\"],[51785,51786,\"0\"],[51824,52465,\"0\"],[51824,51841,\"0\"],[51843,52465,\"0\"],[51843,51883,\"0\"],[51895,51896,\"0\"],[51934,51935,\"0\"],[52026,52158,\"0\"],[52159,52464,\"0\"],[52276,52408,\"0\"],[52506,52526,\"0\"],[52629,52649,\"0\"],[52811,52829,\"0\"],[52883,52994,\"0\"],[52931,52972,\"0\"],[53168,53210,\"0\"],[53179,53209,\"0\"],[53190,53209,\"0\"],[53213,53237,\"0\"],[53240,53252,\"0\"],[53240,53249,\"0\"],[53294,53319,\"0\"],[53294,53312,\"0\"],[53320,53345,\"0\"],[53320,53338,\"0\"],[53348,53369,\"0\"],[53351,53369,\"0\"],[53372,53397,\"0\"],[53375,53397,\"0\"],[53398,53406,\"0\"],[53407,53415,\"0\"],[53633,53650,\"0\"],[53651,53680,\"0\"],[53651,53668,\"0\"],[53681,53759,\"0\"],[53681,53714,\"0\"],[54038,54097,\"0\"],[54222,54235,\"0\"],[54236,54258,\"0\"],[54842,55180,\"0\"],[54862,55180,\"0\"],[54914,54962,\"0\"],[54963,54987,\"0\"],[55003,55179,\"0\"],[55031,55179,\"0\"],[55097,55178,\"0\"],[55110,55178,\"0\"],[55110,55152,\"0\"],[55224,55244,\"0\"],[55310,55424,\"0\"],[55310,55321,\"0\"],[55514,55570,\"0\"],[55571,55589,\"0\"],[55707,55729,\"0\"],[55811,55861,\"0\"],[56305,56348,\"0\"],[56783,56849,\"0\"],[56796,56849,\"0\"],[56991,57014,\"0\"],[57022,57051,\"0\"],[57022,57035,\"0\"],[57037,57051,\"0\"],[57132,57137,\"0\"],[57178,57213,\"0\"],[57181,57213,\"0\"],[57253,57324,\"0\"],[57325,57333,\"0\"],[57474,57495,\"0\"],[57519,57656,\"0\"],[57532,57656,\"0\"],[57572,57625,\"0\"],[58343,58412,\"0\"],[58671,58740,\"0\"],[58684,58740,\"0\"],[58852,58863,\"0\"],[59182,59201,\"0\"],[15,31,\"0\"],[59338,59412,\"0\"],[59536,59557,\"0\"],[59539,59557,\"0\"],[59658,59715,\"0\"],[59668,59715,\"0\"],[59671,59715,\"0\"],[59822,60160,\"0\"],[59844,59901,\"0\"],[59904,59957,\"0\"],[60083,60086,\"0\"],[60118,60159,\"0\"],[60118,60123,\"0\"],[60181,60208,\"0\"],[60209,60371,\"0\"],[60424,60532,\"0\"],[60424,60454,\"0\"],[60891,60892,\"0\"],[60908,60919,\"0\"],[60960,60986,\"0\"],[61135,61187,\"0\"],[61141,61152,\"0\"],[61448,61519,\"0\"],[61520,61564,\"0\"],[61523,61564,\"0\"],[61591,61670,\"0\"],[61671,61749,\"0\"],[61674,61749,\"0\"],[61878,61935,\"0\"],[61966,62023,\"0\"],[62053,62206,\"0\"],[62069,62092,\"0\"],[62069,62080,\"0\"],[62082,62092,\"0\"],[62105,62116,\"0\"],[62207,62282,\"0\"],[62207,62231,\"0\"],[62210,62231,\"0\"],[62338,62349,\"0\"],[62351,62636,\"0\"],[62371,62388,\"0\"],[62511,62531,\"0\"],[62605,62616,\"0\"],[62710,62721,\"0\"],[62723,62987,\"0\"],[62744,62761,\"0\"],[62884,62904,\"0\"],[63048,63108,\"0\"],[63141,63190,\"0\"],[63216,63274,\"0\"],[63953,63967,\"0\"],[63970,63981,\"0\"]],null]]]\n[\"3/447\",\"b\",[[0,\"3/447\",null,[[100,144,\"0\"],[103,144,\"0\"],[175,179,\"0\"],[338,367,\"0\"],[668,731,\"0\"],[749,786,\"0\"],[807,916,\"0\"],[842,882,\"0\"],[985,986,\"0\"],[1000,1072,\"0\"],[1000,1051,\"0\"],[1083,1085,\"0\"],[1332,1333,\"0\"],[1493,1508,\"0\"],[1917,2006,\"0\"],[1917,1975,\"0\"],[2049,2137,\"0\"],[2084,2136,\"0\"],[2283,2395,\"0\"],[2283,2321,\"0\"],[2323,2395,\"0\"],[2340,2395,\"0\"],[2398,2432,\"0\"],[2433,2534,\"0\"],[2436,2534,\"0\"],[2627,2795,\"0\"],[2627,2685,\"0\"],[2796,2857,\"0\"],[2796,2835,\"0\"],[3685,3717,\"0\"],[3685,3697,\"0\"],[3718,3778,\"0\"],[3718,3732,\"0\"],[3779,3824,\"0\"],[3779,3793,\"0\"],[3795,3824,\"0\"],[3798,3824,\"0\"],[3895,3959,\"0\"],[3895,3921,\"0\"],[3923,3959,\"0\"],[3923,3935,\"0\"],[3994,4038,\"0\"],[4096,4156,\"0\"],[4102,4122,\"0\"],[4157,4211,\"0\"],[4157,4164,\"0\"],[4212,4247,\"0\"],[4289,4319,\"0\"],[4409,4445,\"0\"],[4495,4515,\"0\"],[4746,4771,\"0\"],[4809,4851,\"0\"],[4819,4851,\"0\"],[4830,4851,\"0\"],[4853,4913,\"0\"],[4853,4866,\"0\"],[4857,4866,\"0\"],[4860,4866,\"0\"],[4966,4973,\"0\"],[5014,5045,\"0\"],[5060,5090,\"0\"],[5327,5368,\"0\"],[5327,5348,\"0\"],[5384,5442,\"0\"],[5474,5499,\"0\"],[5543,5598,\"0\"],[5567,5598,\"0\"],[5570,5598,\"0\"],[5766,5886,\"0\"],[5908,5989,\"0\"],[5908,5937,\"0\"],[6021,6053,\"0\"],[6031,6053,\"0\"],[6082,6100,\"0\"],[6140,6156,\"0\"],[6163,6179,\"0\"],[6388,6428,\"0\"],[6907,6939,\"0\"],[6940,7025,\"0\"],[6940,6984,\"0\"],[6986,7025,\"0\"],[7027,7067,\"0\"],[7290,7380,\"0\"],[7290,7341,\"0\"],[7416,7425,\"0\"],[7473,7486,\"0\"],[7523,7533,\"0\"],[29171,29190,\"0\"],[23,33,\"0\"],[29323,29326,\"0\"],[29470,29678,\"0\"],[29493,29678,\"0\"],[29515,29526,\"0\"],[72535,72688,\"0\"],[72788,72789,\"0\"],[72790,72813,\"0\"],[72867,72997,\"0\"],[73096,73493,\"0\"],[73108,73126,\"0\"],[73263,73308,\"0\"],[73263,73275,\"0\"],[73310,73338,\"0\"],[73339,73390,\"0\"],[73455,73456,\"0\"],[73712,73727,\"0\"],[73715,73727,\"0\"],[73994,74029,\"0\"],[74096,74097,\"0\"],[74131,74132,\"0\"],[74533,74568,\"0\"],[74582,74583,\"0\"],[74725,74726,\"0\"],[74783,74784,\"0\"],[74834,74835,\"0\"],[74898,74899,\"0\"],[75101,75125,\"0\"],[75226,75303,\"0\"],[75226,75241,\"0\"],[75321,75335,\"0\"],[75359,75360,\"0\"],[75520,75600,\"0\"],[75520,75557,\"0\"],[75641,75691,\"0\"],[75692,76024,\"0\"],[75786,75818,\"0\"],[75856,75868,\"0\"],[76275,76292,\"0\"],[76369,76438,\"0\"],[76537,76554,\"0\"],[76587,76635,\"0\"],[76663,76686,\"0\"],[76663,76670,\"0\"],[76768,76845,\"0\"],[76768,76795,\"0\"],[77035,77113,\"0\"],[77035,77054,\"0\"],[77114,77173,\"0\"],[77134,77173,\"0\"],[77194,77253,\"0\"],[77270,77405,\"0\"],[77346,77404,\"0\"],[77708,77846,\"0\"],[77708,77753,\"0\"],[77708,77719,\"0\"],[78055,78056,\"0\"],[78060,78076,\"0\"],[78142,78167,\"0\"],[78331,78490,\"0\"],[78350,78490,\"0\"],[78350,78372,\"0\"],[78491,78663,\"0\"],[78491,78510,\"0\"],[78590,78661,\"0\"],[78716,78771,\"0\"],[78804,78818,\"0\"],[78804,78815,\"0\"],[78837,78851,\"0\"],[79038,79055,\"0\"],[79057,79187,\"0\"],[79188,79210,\"0\"],[79215,79216,\"0\"],[79299,79300,\"0\"],[79406,79475,\"0\"],[79531,79612,\"0\"],[79531,79549,\"0\"],[79687,79721,\"0\"],[79722,79766,\"0\"],[79990,80036,\"0\"],[79990,80015,\"0\"],[80037,80094,\"0\"],[80037,80062,\"0\"],[80341,80358,\"0\"],[80431,80454,\"0\"],[80431,80438,\"0\"],[80474,80486,\"0\"],[80526,80574,\"0\"],[80597,80667,\"0\"],[80668,80749,\"0\"],[80668,80695,\"0\"],[80720,80748,\"0\"],[80793,80943,\"0\"],[80813,80943,\"0\"],[80813,80852,\"0\"],[80855,80867,\"0\"],[81050,81231,\"0\"],[81100,81228,\"0\"],[81100,81130,\"0\"],[81152,81173,\"0\"],[81253,81314,\"0\"],[81315,81400,\"0\"],[81466,81547,\"0\"],[81490,81547,\"0\"],[81581,81599,\"0\"],[81602,81603,\"0\"],[81634,81657,\"0\"],[81634,81643,\"0\"],[81645,81657,\"0\"],[81658,81667,\"0\"],[81668,81722,\"0\"],[81799,81860,\"0\"],[81862,81897,\"0\"],[81918,81919,\"0\"],[81975,82014,\"0\"],[82017,82123,\"0\"],[82259,82288,\"0\"],[82289,82328,\"0\"],[82329,82376,\"0\"],[82417,82442,\"0\"],[82515,82540,\"0\"],[82603,82623,\"0\"],[82661,82676,\"0\"],[82702,82724,\"0\"],[82705,82724,\"0\"],[82766,82875,\"0\"],[82766,82776,\"0\"],[82779,82780,\"0\"],[82803,82874,\"0\"],[82834,82873,\"0\"],[82876,82917,\"0\"],[82876,82894,\"0\"],[82918,83007,\"0\"],[82918,82933,\"0\"],[83008,83184,\"0\"],[83008,83019,\"0\"],[83022,83034,\"0\"],[83077,83182,\"0\"],[83202,83315,\"0\"],[83202,83216,\"0\"],[83292,83314,\"0\"],[83342,83374,\"0\"],[83444,83501,\"0\"],[83444,83469,\"0\"],[83527,85392,\"1/2\"],[83527,83546,\"1/2\"],[83656,83742,\"0\"],[83680,83742,\"0\"],[83743,83829,\"0\"],[83768,83829,\"0\"],[83830,83907,\"0\"],[83849,83907,\"0\"],[83908,83973,\"0\"],[83974,84000,\"0\"],[84173,84183,\"0\"],[84221,84259,\"0\"],[84221,84236,\"0\"],[84260,84302,\"0\"],[84260,84270,\"0\"],[84303,84480,\"0\"],[84303,84314,\"0\"],[84317,84326,\"0\"],[84399,84479,\"0\"],[84568,84721,\"0\"],[84582,84721,\"0\"],[84582,84620,\"0\"],[84595,84620,\"0\"],[84622,84721,\"0\"],[84635,84721,\"0\"],[84635,84660,\"0\"],[84662,84721,\"0\"],[84662,84692,\"0\"],[84694,84721,\"0\"],[84747,84806,\"0\"],[84807,84827,\"0\"],[84948,85005,\"0\"],[85006,85067,\"0\"],[85006,85029,\"0\"],[85031,85067,\"0\"],[85031,85053,\"0\"],[85141,85188,\"0\"],[85141,85164,\"0\"],[85166,85188,\"0\"],[85250,85349,\"0\"],[85250,85279,\"0\"],[85281,85349,\"0\"],[85618,85623,\"1/2\"],[85702,85723,\"0\"],[85702,85711,\"0\"],[85713,85723,\"0\"],[85724,85764,\"0\"],[85812,85817,\"0\"],[85819,85830,\"0\"],[85860,85890,\"0\"],[86071,86072,\"0\"],[86286,86556,\"0\"],[86508,86555,\"0\"],[86646,86673,\"0\"],[86674,86773,\"0\"],[86743,86744,\"0\"],[86810,86839,\"0\"],[86865,86925,\"0\"],[86865,86888,\"0\"],[86865,86876,\"0\"],[86878,86888,\"0\"],[86927,87580,\"0\"],[86927,86951,\"0\"],[86927,86939,\"0\"],[86941,86951,\"0\"],[87024,87054,\"0\"],[87195,87196,\"0\"],[87243,87244,\"0\"],[87313,87376,\"0\"],[87346,87376,\"0\"],[87408,87471,\"0\"],[87441,87471,\"0\"],[87607,87641,\"0\"],[87706,87969,\"0\"],[87754,87764,\"0\"],[87769,87803,\"0\"],[87769,87782,\"0\"],[87839,87852,\"0\"],[87911,87924,\"0\"],[88065,88348,\"0\"],[88269,88316,\"0\"],[88428,88767,\"0\"],[88781,88786,\"0\"],[88790,88813,\"0\"],[88793,88813,\"0\"],[88873,88874,\"0\"],[88875,88883,\"0\"],[88909,89158,\"0\"],[89002,89063,\"0\"],[89066,89125,\"0\"],[89361,89393,\"0\"],[89513,89901,\"0\"],[89604,89615,\"0\"],[89817,89884,\"0\"],[89817,89838,\"0\"],[89817,89826,\"0\"],[89828,89838,\"0\"],[89885,89900,\"0\"],[89885,89890,\"0\"],[89973,90451,\"0\"],[90035,90046,\"0\"],[90177,90368,\"0\"],[90177,90198,\"0\"],[90177,90186,\"0\"],[90188,90198,\"0\"],[90436,90441,\"0\"],[90524,91058,\"0\"],[90586,90597,\"0\"],[90735,90923,\"0\"],[90735,90756,\"0\"],[90735,90744,\"0\"],[90746,90756,\"0\"],[90991,90996,\"0\"],[91097,91099,\"0\"],[91121,91150,\"0\"],[91158,91185,\"0\"],[91392,91432,\"0\"],[91392,91412,\"0\"],[91414,91432,\"0\"],[91433,91447,\"0\"],[91484,91485,\"0\"],[91486,91495,\"0\"],[91543,91548,\"0\"],[91549,91582,\"0\"],[91549,91567,\"0\"],[91583,92127,\"0\"],[91587,91628,\"0\"],[91587,91604,\"0\"],[91739,91834,\"0\"],[91739,91747,\"0\"],[91749,91834,\"0\"],[91749,91770,\"0\"],[91772,91834,\"0\"],[91772,91790,\"0\"],[91835,91930,\"0\"],[91835,91843,\"0\"],[91845,91930,\"0\"],[91845,91866,\"0\"],[91868,91930,\"0\"],[91868,91886,\"0\"],[91931,92012,\"0\"],[91948,91958,\"0\"],[92256,92284,\"0\"],[92342,92416,\"0\"],[92481,92522,\"0\"],[92484,92522,\"0\"],[92523,92524,\"0\"],[92658,92704,\"0\"],[92873,92948,\"0\"],[92983,92984,\"0\"],[93024,93031,\"0\"],[93177,93186,\"0\"],[93313,93322,\"0\"],[93416,93472,\"0\"],[93416,93436,\"0\"],[93475,93495,\"0\"],[93511,93567,\"0\"],[93511,93531,\"0\"],[93570,93590,\"0\"],[93606,93674,\"0\"],[93606,93626,\"0\"],[93663,93664,\"0\"],[93677,93697,\"0\"],[93698,93843,\"0\"],[93756,93765,\"0\"],[93859,93923,\"0\"],[93859,93879,\"0\"],[93926,93946,\"0\"],[93962,94030,\"0\"],[93962,93982,\"0\"],[94019,94020,\"0\"],[94033,94053,\"0\"],[94054,94199,\"0\"],[94112,94121,\"0\"],[94215,94278,\"0\"],[94215,94235,\"0\"],[94281,94301,\"0\"],[94326,94351,\"0\"],[94326,94340,\"0\"],[94392,94435,\"0\"],[94510,94547,\"0\"],[94636,94652,\"0\"],[94675,94723,\"0\"],[94686,94723,\"0\"],[94697,94723,\"0\"],[94697,94702,\"0\"],[94704,94723,\"0\"],[94708,94723,\"0\"],[94797,94822,\"0\"],[94886,94893,\"0\"],[94947,95052,\"0\"],[95104,95252,\"0\"],[95106,95117,\"0\"],[95118,95184,\"0\"],[95118,95152,\"0\"],[95185,95252,\"0\"],[95225,95252,\"0\"],[95299,95487,\"0\"],[95322,95487,\"0\"],[95322,95347,\"0\"],[95390,95486,\"0\"],[95393,95486,\"0\"],[95526,95527,\"0\"],[95548,96579,\"0\"],[95548,95554,\"0\"],[95648,95684,\"0\"],[95685,95785,\"0\"],[95832,95865,\"0\"],[96121,96154,\"0\"],[96164,96165,\"0\"],[96234,96578,\"0\"],[96381,96411,\"0\"],[96473,96482,\"0\"],[96484,96485,\"0\"],[96494,96553,\"0\"]],null]]]\n[\"1/518\",\"b\",[[0,\"1/518\",null,[[11313,11342,\"0\"],[11313,11323,\"0\"],[11442,11511,\"0\"],[11442,11445,\"0\"],[11521,11539,\"0\"],[11521,11529,\"0\"],[11531,11539,\"0\"],[11597,11617,\"0\"],[11664,11783,\"0\"],[11664,11689,\"0\"],[11691,11783,\"0\"],[11724,11783,\"0\"],[11784,11960,\"0\"],[11834,11936,\"0\"],[11834,11849,\"0\"],[11851,11936,\"0\"],[11999,12079,\"0\"],[12008,12079,\"0\"],[12032,12079,\"0\"],[12163,12170,\"0\"],[12196,12203,\"0\"],[12268,12269,\"0\"],[12339,12340,\"0\"],[12383,12403,\"0\"],[12418,12451,\"0\"],[12421,12451,\"0\"],[12486,12519,\"0\"],[12489,12519,\"0\"],[12570,12585,\"0\"],[12570,12580,\"0\"],[12582,12585,\"0\"],[12632,12646,\"0\"],[12632,12641,\"0\"],[12643,12646,\"0\"],[12740,12804,\"0\"],[12744,12803,\"0\"],[12744,12761,\"0\"],[12763,12803,\"0\"],[12763,12777,\"0\"],[12810,12811,\"0\"],[12940,13578,\"0\"],[12944,13578,\"0\"],[12944,12973,\"0\"],[12947,12973,\"0\"],[12975,13578,\"0\"],[13095,13141,\"0\"],[13107,13140,\"0\"],[13205,13220,\"0\"],[13231,13243,\"0\"],[13262,13263,\"0\"],[13274,13280,\"0\"],[13333,13397,\"0\"],[13344,13351,\"0\"],[13369,13376,\"0\"],[13419,13559,\"0\"],[13503,13558,\"0\"],[13710,13731,\"0\"],[13782,14134,\"0\"],[13782,14101,\"0\"],[13782,14032,\"0\"],[13782,13815,\"0\"],[13817,14032,\"0\"],[13817,13866,\"0\"],[13868,14032,\"0\"],[13911,14032,\"0\"],[13962,14032,\"0\"],[13996,14031,\"0\"],[14034,14101,\"0\"],[14076,14101,\"0\"],[14198,14331,\"0\"],[14259,14266,\"0\"],[14391,14431,\"0\"],[14789,14796,\"0\"],[14824,14831,\"0\"],[14980,15266,\"0\"],[15020,15266,\"0\"],[15389,15396,\"0\"],[15419,15442,\"0\"],[15494,15495,\"0\"],[15504,15511,\"0\"],[15535,15569,\"0\"],[15570,15578,\"0\"],[15656,15709,\"0\"],[15656,15683,\"0\"],[15798,15935,\"0\"],[15804,15855,\"0\"],[15984,15992,\"0\"],[7,10,\"0\"],[4,7,\"0\"],[16084,16097,\"0\"],[16211,16227,\"0\"],[16211,16218,\"0\"],[16220,16227,\"0\"],[16256,16293,\"0\"],[16258,16276,\"0\"],[16287,16293,\"0\"],[16325,16382,\"0\"],[16325,16326,\"0\"],[16385,16403,\"0\"],[16453,16506,\"0\"],[16453,16454,\"0\"],[16510,16553,\"0\"],[16510,16511,\"0\"],[16557,16614,\"0\"],[16557,16558,\"0\"],[16618,16665,\"0\"],[16618,16619,\"0\"],[16711,16742,\"0\"],[16711,16723,\"0\"],[16725,16742,\"0\"],[16750,16756,\"0\"],[16787,16794,\"0\"],[16798,16805,\"0\"],[16809,16819,\"0\"],[16809,16816,\"0\"],[16822,16829,\"0\"],[16836,16843,\"0\"],[16907,16909,\"0\"],[17023,17027,\"0\"],[17119,17252,\"0\"],[17160,17212,\"0\"],[17246,17252,\"0\"],[17316,17341,\"0\"],[17396,17406,\"0\"],[17414,17415,\"0\"],[17416,17441,\"0\"],[17416,17433,\"0\"],[17816,17822,\"0\"],[17853,17896,\"0\"],[17853,17863,\"0\"],[17951,18069,\"0\"],[17951,17957,\"0\"],[17985,18064,\"0\"],[17985,17990,\"0\"],[18051,18055,\"0\"],[18065,18069,\"0\"],[18088,18122,\"0\"],[18118,18122,\"0\"],[18141,18245,\"0\"],[18189,18245,\"0\"],[18189,18208,\"0\"],[18210,18245,\"0\"],[18210,18228,\"0\"],[18230,18245,\"0\"],[18606,18608,\"0\"],[18627,18680,\"0\"],[18627,18628,\"0\"],[18684,18727,\"0\"],[18684,18685,\"0\"],[18731,18788,\"0\"],[18731,18732,\"0\"],[18792,18839,\"0\"],[18792,18793,\"0\"],[18860,18891,\"0\"],[18860,18872,\"0\"],[18874,18891,\"0\"],[18899,18905,\"0\"],[18941,18945,\"0\"],[18951,18961,\"0\"],[19008,19012,\"0\"],[19037,19038,\"0\"],[19047,19057,\"0\"],[19065,19066,\"0\"],[19067,19092,\"0\"],[19067,19084,\"0\"],[19455,19461,\"0\"],[19479,19501,\"0\"],[19624,19630,\"0\"],[19745,19776,\"0\"],[19745,19757,\"0\"],[19759,19776,\"0\"],[19784,19790,\"0\"],[19802,19812,\"0\"],[19820,19821,\"0\"],[19822,19847,\"0\"],[19822,19839,\"0\"],[19983,19989,\"0\"],[20067,20089,\"0\"],[20158,20183,\"0\"],[20229,20236,\"0\"],[20319,20343,\"0\"],[20355,20356,\"0\"],[20484,20485,\"0\"],[20515,20516,\"0\"],[20546,20547,\"0\"],[20949,21017,\"0\"],[20949,20976,\"0\"],[21118,21119,\"0\"],[21129,21138,\"0\"],[21442,21457,\"0\"],[21442,21449,\"0\"],[21451,21457,\"0\"],[21460,21478,\"0\"],[21484,21502,\"0\"],[21503,21511,\"0\"],[21550,21573,\"0\"],[21553,21573,\"0\"],[21578,21649,\"0\"],[21722,21723,\"0\"],[21736,21751,\"0\"],[22059,22124,\"0\"],[22059,22098,\"0\"],[22059,22078,\"0\"],[22080,22098,\"0\"],[22100,22124,\"0\"],[22149,22195,\"0\"],[22170,22171,\"0\"],[22224,22267,\"0\"],[22240,22267,\"0\"],[22249,22267,\"0\"],[23494,23512,\"0\"],[23494,23502,\"0\"],[23504,23512,\"0\"],[23542,23554,\"0\"],[23722,23741,\"0\"],[23722,23739,\"0\"],[23722,23725,\"0\"],[23841,23847,\"0\"],[23844,23847,\"0\"],[23852,23898,\"0\"],[23904,23920,\"0\"],[23904,23907,\"0\"],[23981,23982,\"0\"],[24078,24109,\"0\"],[24256,24257,\"0\"],[24288,24289,\"0\"],[24418,24419,\"0\"],[24478,24479,\"0\"],[24575,24605,\"0\"],[24752,24753,\"0\"],[24784,24785,\"0\"],[24914,24915,\"0\"],[24977,25017,\"0\"],[25024,25025,\"0\"],[25076,25084,\"0\"],[25270,25271,\"0\"],[25332,25333,\"0\"],[25341,25361,\"0\"],[25529,25530,\"0\"],[25590,25598,\"0\"],[25879,25885,\"0\"],[25919,25925,\"0\"],[25934,26018,\"0\"],[26023,26066,\"0\"],[26060,26066,\"0\"],[26133,26134,\"0\"],[26217,26218,\"0\"],[26381,26406,\"0\"],[26401,26406,\"0\"],[26407,26408,\"0\"],[26418,26444,\"0\"],[26439,26444,\"0\"],[26445,26446,\"0\"],[26496,26504,\"0\"],[194,197,\"0\"],[4,7,\"0\"],[26573,26574,\"0\"],[26654,26657,\"0\"],[26695,26706,\"0\"],[26856,26939,\"0\"],[26856,26875,\"0\"],[26877,26939,\"0\"],[26877,26896,\"0\"],[27004,27005,\"0\"],[27006,27020,\"0\"],[27106,27109,\"0\"],[27131,27132,\"0\"],[27218,27221,\"0\"],[27228,27229,\"0\"],[27238,27245,\"0\"],[27277,27293,\"0\"],[27296,27324,\"0\"],[27296,27301,\"0\"],[27303,27324,\"0\"],[27310,27324,\"0\"],[27310,27316,\"0\"],[27313,27316,\"0\"],[27318,27324,\"0\"],[27321,27324,\"0\"],[27344,27355,\"0\"],[27387,27388,\"0\"],[27423,27536,\"0\"],[27423,27482,\"0\"],[27423,27448,\"0\"],[27450,27482,\"0\"],[27450,27469,\"0\"],[27484,27536,\"0\"],[27484,27509,\"0\"],[27511,27536,\"0\"],[27565,27566,\"0\"],[27621,27626,\"0\"],[27640,27665,\"0\"],[27694,27719,\"0\"],[27753,27765,\"0\"],[27753,27758,\"0\"],[27813,27814,\"0\"],[27933,27938,\"0\"],[27948,27949,\"0\"],[28036,28061,\"0\"],[28089,28114,\"0\"],[28139,28140,\"0\"],[28286,28302,\"0\"],[28286,28290,\"0\"],[28326,28327,\"0\"],[28338,28356,\"0\"],[28338,28344,\"0\"],[28346,28356,\"0\"],[28661,28666,\"0\"],[28733,28755,\"0\"],[28736,28755,\"0\"],[28758,28780,\"0\"],[28761,28780,\"0\"],[28874,28878,\"0\"],[28884,29012,\"0\"],[28884,28903,\"0\"],[28888,28903,\"0\"],[28907,29012,\"0\"],[28907,28926,\"0\"],[28911,28926,\"0\"],[28958,28963,\"0\"],[29031,29076,\"0\"],[29031,29063,\"0\"],[29041,29049,\"0\"],[29045,29049,\"0\"],[29080,29100,\"0\"],[29095,29100,\"0\"],[29246,29265,\"0\"],[29296,29314,\"0\"],[29354,29362,\"0\"],[29393,29401,\"0\"],[29406,29407,\"0\"],[29440,29441,\"0\"],[29569,29573,\"0\"],[29616,29621,\"0\"],[29760,29792,\"0\"],[29768,29776,\"0\"],[29772,29776,\"0\"],[29860,29867,\"0\"],[29892,29899,\"0\"],[29958,29965,\"0\"],[30151,30191,\"0\"],[30151,30167,\"0\"],[30151,30158,\"0\"],[30160,30167,\"0\"],[30291,30297,\"0\"],[30294,30297,\"0\"],[30309,30315,\"0\"],[30312,30315,\"0\"],[30386,30458,\"0\"],[30397,30404,\"0\"],[30534,30541,\"0\"],[30548,30566,\"0\"],[30936,30939,\"0\"],[30964,31030,\"0\"],[30964,30967,\"0\"],[31196,31342,\"0\"],[31282,31311,\"0\"],[31312,31341,\"0\"],[31344,31457,\"0\"],[31344,31372,\"0\"],[31348,31372,\"0\"],[31376,31457,\"0\"],[31376,31404,\"0\"],[31380,31404,\"0\"],[31408,31457,\"0\"],[31408,31430,\"0\"],[31412,31430,\"0\"],[31434,31456,\"0\"],[31438,31456,\"0\"],[31534,31552,\"0\"],[31810,31901,\"0\"],[31908,31909,\"0\"],[31910,31928,\"0\"],[32187,32199,\"0\"],[32212,32328,\"0\"],[32329,32408,\"0\"],[32329,32370,\"0\"],[32461,32526,\"0\"],[32461,32479,\"0\"],[32481,32526,\"0\"],[32481,32497,\"0\"],[32499,32526,\"0\"],[32499,32512,\"0\"],[32514,32526,\"0\"],[32527,32592,\"0\"],[32527,32539,\"0\"],[32541,32592,\"0\"],[32541,32561,\"0\"],[32563,32592,\"0\"],[32687,32705,\"0\"],[118872,118927,\"0\"],[119108,119258,\"0\"],[119286,119288,\"0\"],[119310,119346,\"0\"],[119383,119394,\"0\"],[119397,119452,\"0\"],[119397,119433,\"0\"],[119511,119539,\"0\"],[119514,119539,\"0\"],[119580,119685,\"0\"],[119686,119802,\"0\"],[119713,119802,\"0\"],[119757,119781,\"0\"],[119828,119947,\"0\"],[119828,119886,\"0\"],[120248,120264,\"0\"],[120269,120338,\"0\"],[120279,120338,\"0\"],[120279,120326,\"0\"],[120303,120326,\"0\"],[120366,120374,\"0\"],[120378,120476,\"0\"],[120402,120476,\"0\"],[120427,120476,\"0\"],[120512,120536,\"0\"],[120568,120609,\"0\"],[120568,120585,\"0\"],[120610,120628,\"0\"],[120670,120842,\"0\"],[120697,120842,\"0\"],[120741,120786,\"0\"],[120787,120805,\"0\"],[121012,121171,\"0\"],[121055,121151,\"0\"],[121055,121093,\"0\"],[121172,121261,\"0\"],[121351,121473,\"0\"],[121721,121737,\"1/2\"],[121814,121884,\"0\"],[121818,121883,\"0\"],[121821,121883,\"0\"],[121846,121883,\"0\"],[121893,121903,\"0\"],[121908,121954,\"0\"],[121983,122041,\"0\"],[121986,122041,\"0\"],[121986,122019,\"0\"],[122514,122575,\"0\"],[122514,122546,\"0\"],[122576,122631,\"0\"],[122720,122792,\"0\"],[122720,122732,\"0\"],[122821,122876,\"0\"],[122821,122846,\"0\"],[123022,123099,\"0\"],[123035,123099,\"0\"],[123047,123099,\"0\"],[123135,123146,\"0\"],[123193,123209,\"0\"],[123508,123580,\"0\"],[123674,123711,\"0\"],[123880,123925,\"0\"],[124106,124196,\"0\"],[124152,124185,\"0\"],[124379,124431,\"0\"],[124455,124484,\"0\"],[124536,124607,\"0\"],[124552,124607,\"0\"],[124630,124678,\"0\"],[124996,125162,\"0\"],[124996,125063,\"0\"],[125188,125218,\"0\"],[125399,125442,\"0\"],[125493,125569,\"0\"],[125509,125569,\"0\"],[125570,125616,\"0\"],[125617,125789,\"0\"],[125617,125652,\"0\"],[125617,125648,\"0\"],[125654,125789,\"0\"],[125654,125686,\"0\"],[125654,125683,\"0\"],[125688,125789,\"0\"],[125688,125712,\"0\"],[125714,125789,\"0\"],[125887,125924,\"0\"],[125887,125912,\"0\"],[125927,125970,\"0\"],[125927,125955,\"0\"],[126025,126072,\"0\"],[126073,126135,\"0\"],[126205,126245,\"0\"],[126359,126378,\"0\"],[126502,126521,\"0\"],[126799,126800,\"0\"],[126840,126858,\"0\"],[126917,127014,\"0\"],[126954,126975,\"0\"],[127015,127069,\"0\"],[127018,127069,\"0\"],[127018,127040,\"0\"],[127070,127166,\"0\"],[127070,127092,\"0\"],[127094,127166,\"0\"],[127202,127294,\"0\"],[127332,127368,\"0\"],[127332,127350,\"0\"],[127369,127412,\"0\"],[127402,127410,\"0\"],[127413,127474,\"0\"],[127475,127567,\"0\"],[127568,127625,\"0\"],[127626,127719,\"0\"],[127751,127803,\"0\"],[127783,127803,\"0\"],[127891,128080,\"0\"],[127891,127920,\"0\"],[127922,128080,\"0\"],[127922,127946,\"0\"],[127948,128080,\"0\"],[128081,128139,\"0\"],[128152,128207,\"0\"],[128222,128278,\"0\"],[128315,128344,\"0\"],[128455,128493,\"0\"],[128496,128497,\"0\"],[128544,128583,\"0\"],[128586,128587,\"0\"]],null]]]\n[\"0/428\",\"b\",[[0,\"0/428\",null,[[6590,6636,\"0\"],[6641,6686,\"0\"],[6693,6740,\"0\"],[6748,6796,\"0\"],[7030,7043,\"0\"],[7044,7068,\"0\"],[7342,7392,\"0\"],[7450,7476,\"0\"],[7561,7606,\"0\"],[7664,7697,\"0\"],[7739,7806,\"0\"],[7739,7763,\"0\"],[7833,8503,\"0\"],[7898,7938,\"0\"],[7941,7977,\"0\"],[7980,8019,\"0\"],[8022,8057,\"0\"],[8060,8061,\"0\"],[8116,8157,\"0\"],[8160,8197,\"0\"],[8266,8267,\"0\"],[8325,8367,\"0\"],[8370,8408,\"0\"],[8577,8597,\"0\"],[8604,8618,\"0\"],[8735,8761,\"0\"],[8786,8787,\"0\"],[8883,8909,\"0\"],[8935,8936,\"0\"],[9095,9210,\"0\"],[9095,9116,\"0\"],[9211,10236,\"0\"],[9215,9694,\"0\"],[9234,9256,\"0\"],[9422,9490,\"0\"],[9422,9457,\"0\"],[9491,9557,\"0\"],[9491,9525,\"0\"],[9558,9626,\"0\"],[9558,9593,\"0\"],[9627,9693,\"0\"],[9627,9661,\"0\"],[9695,10161,\"0\"],[9706,9715,\"0\"],[9811,9812,\"0\"],[9813,9872,\"0\"],[9813,9842,\"0\"],[9844,9872,\"0\"],[9875,9904,\"0\"],[9933,9942,\"0\"],[10038,10039,\"0\"],[10040,10101,\"0\"],[10040,10070,\"0\"],[10072,10101,\"0\"],[10104,10134,\"0\"],[10162,10198,\"0\"],[10162,10174,\"0\"],[10199,10235,\"0\"],[10199,10211,\"0\"],[10315,10341,\"0\"],[10366,10367,\"0\"],[10475,10501,\"0\"],[10527,10528,\"0\"],[10631,10711,\"0\"],[10631,10663,\"0\"],[10665,10711,\"0\"],[10757,10792,\"0\"],[10828,10845,\"0\"],[10885,10995,\"0\"],[14920,15013,\"0\"],[14923,15013,\"0\"],[15142,15150,\"0\"],[15603,15896,\"0\"],[15790,15891,\"0\"],[15790,15798,\"0\"],[15800,15891,\"0\"],[15843,15891,\"0\"],[15897,15898,\"0\"],[15900,16521,\"0\"],[16522,16576,\"0\"],[16578,17025,\"0\"],[16802,16839,\"0\"],[17119,17163,\"0\"],[17227,17271,\"0\"],[17366,17413,\"0\"],[17479,17528,\"0\"],[17592,17668,\"0\"],[17669,17809,\"0\"],[17669,17711,\"0\"],[17713,17809,\"0\"],[17713,17756,\"0\"],[17898,17923,\"0\"],[17898,17903,\"0\"],[17905,17923,\"0\"],[17925,18144,\"0\"],[17925,17945,\"0\"],[17933,17945,\"0\"],[17948,18011,\"0\"],[18052,18143,\"0\"],[18052,18100,\"0\"],[18145,18369,\"0\"],[18145,18165,\"0\"],[18153,18165,\"0\"],[18168,18231,\"0\"],[18274,18368,\"0\"],[18274,18323,\"0\"],[18372,18607,\"0\"],[18372,18392,\"0\"],[18380,18392,\"0\"],[18395,18439,\"0\"],[18489,18606,\"0\"],[18489,18554,\"0\"],[18608,18848,\"0\"],[18608,18628,\"0\"],[18616,18628,\"0\"],[18631,18676,\"0\"],[18728,18847,\"0\"],[18728,18793,\"0\"],[18850,18926,\"0\"],[18850,18856,\"0\"],[18858,18926,\"0\"],[18874,18926,\"0\"],[19024,19051,\"0\"],[19052,19087,\"0\"],[19139,19258,\"0\"],[19139,19158,\"0\"],[19459,19463,\"0\"],[19603,19703,\"0\"],[19603,19608,\"0\"],[19610,19703,\"0\"],[19610,19615,\"0\"],[19617,19703,\"0\"],[19617,19622,\"0\"],[19624,19703,\"0\"],[19624,19629,\"0\"],[19705,19853,\"0\"],[19733,19853,\"0\"],[19886,20330,\"0\"],[19886,19906,\"0\"],[19911,19927,\"0\"],[19930,19946,\"0\"],[19949,19965,\"0\"],[19968,19984,\"0\"],[19985,20083,\"0\"],[20084,20155,\"0\"],[20156,20255,\"0\"],[20256,20329,\"0\"],[20333,20343,\"0\"],[20336,20343,\"0\"],[20339,20343,\"0\"],[20344,20788,\"0\"],[20344,20364,\"0\"],[20369,20385,\"0\"],[20388,20404,\"0\"],[20407,20423,\"0\"],[20426,20442,\"0\"],[20443,20514,\"0\"],[20515,20613,\"0\"],[20614,20687,\"0\"],[20688,20787,\"0\"],[20789,20949,\"0\"],[20819,20949,\"0\"],[20819,20832,\"0\"],[20822,20832,\"0\"],[20825,20832,\"0\"],[20828,20832,\"0\"],[20835,20949,\"0\"],[20977,20990,\"0\"],[20980,20990,\"0\"],[20983,20990,\"0\"],[20986,20990,\"0\"],[21124,21158,\"0\"],[21161,21195,\"0\"],[21199,21330,\"0\"],[21212,21249,\"0\"],[21424,21468,\"0\"],[21532,21578,\"0\"],[22032,22048,\"0\"],[22129,22160,\"0\"],[22176,22189,\"0\"],[22179,22189,\"0\"],[22227,22233,\"0\"],[22247,22253,\"0\"],[22483,22511,\"0\"],[22493,22511,\"0\"],[22512,22543,\"0\"],[22551,23532,\"0\"],[23554,23792,\"0\"],[23609,23680,\"0\"],[23710,23790,\"0\"],[23722,23790,\"0\"],[23993,24209,\"0\"],[24490,24792,\"0\"],[24503,24740,\"0\"],[24515,24560,\"0\"],[24759,24792,\"0\"],[24797,24855,\"0\"],[24797,24817,\"0\"],[24902,24912,\"0\"],[25134,25149,\"0\"],[25172,25208,\"0\"],[25280,25418,\"0\"],[25280,25328,\"0\"],[25419,25756,\"0\"],[25442,25756,\"0\"],[25556,25581,\"0\"],[25626,25643,\"0\"],[25650,25667,\"0\"],[25677,25690,\"0\"],[25986,26104,\"0\"],[25999,26078,\"0\"],[26086,26093,\"0\"],[26107,26181,\"0\"],[26319,26364,\"0\"],[26319,26355,\"0\"],[26319,26331,\"0\"],[26371,26396,\"0\"],[26579,26666,\"0\"],[26598,26630,\"0\"],[26634,26665,\"0\"],[26739,26751,\"0\"],[26862,26874,\"0\"],[27146,27176,\"0\"],[27191,27242,\"0\"],[27312,27322,\"0\"],[27491,27508,\"0\"],[27511,27527,\"0\"],[27582,27583,\"0\"],[27652,27710,\"0\"],[27652,27681,\"0\"],[27808,27894,\"0\"],[27823,27894,\"0\"],[27895,28002,\"0\"],[28098,28663,\"0\"],[28152,28193,\"0\"],[28196,28227,\"0\"],[28250,28251,\"0\"],[28329,28410,\"0\"],[28413,28491,\"0\"],[28492,28547,\"0\"],[28608,28662,\"0\"],[28622,28662,\"0\"],[28764,28798,\"0\"],[29006,29079,\"0\"],[29006,29047,\"0\"],[29080,29157,\"0\"],[29080,29123,\"0\"],[29158,29227,\"0\"],[29158,29195,\"0\"],[29228,29301,\"0\"],[29228,29267,\"0\"],[29402,29428,\"0\"],[29451,29477,\"0\"],[29503,29530,\"0\"],[29555,29582,\"0\"],[29601,29867,\"0\"],[29601,29621,\"0\"],[29747,29775,\"0\"],[29747,29759,\"0\"],[29776,29806,\"0\"],[29776,29789,\"0\"],[29807,29835,\"0\"],[29807,29819,\"0\"],[29836,29866,\"0\"],[29836,29849,\"0\"],[29947,29998,\"0\"],[29999,30047,\"0\"],[30048,30101,\"0\"],[30102,30152,\"0\"],[30230,30254,\"0\"],[30289,30349,\"0\"],[30350,30404,\"0\"],[30350,30358,\"0\"],[30405,30480,\"0\"],[30405,30413,\"0\"],[30547,30602,\"0\"],[30572,30602,\"0\"],[30584,30602,\"0\"],[30605,30664,\"0\"],[30631,30664,\"0\"],[30644,30664,\"0\"],[30667,30722,\"0\"],[30692,30722,\"0\"],[30704,30722,\"0\"],[30725,30784,\"0\"],[30751,30784,\"0\"],[30764,30784,\"0\"],[30938,30961,\"0\"],[30962,30987,\"0\"],[30988,31011,\"0\"],[31012,31037,\"0\"],[31038,31065,\"0\"],[31041,31065,\"0\"],[31066,31093,\"0\"],[31069,31093,\"0\"],[31094,31121,\"0\"],[31097,31121,\"0\"],[31122,31149,\"0\"],[31125,31149,\"0\"],[31150,31183,\"0\"],[31159,31183,\"0\"],[31169,31183,\"0\"],[31184,31232,\"0\"],[31193,31232,\"0\"],[31203,31232,\"0\"],[31210,31232,\"0\"],[31504,31507,\"0\"],[31517,31536,\"0\"],[31543,31562,\"0\"],[31641,31682,\"0\"],[31699,31724,\"0\"],[31725,31768,\"0\"],[31813,31897,\"0\"],[31912,31953,\"0\"],[31960,31999,\"0\"],[32091,32103,\"0\"],[32117,32171,\"0\"],[33313,33355,\"0\"],[33313,33325,\"0\"],[33718,33759,\"0\"],[33762,33790,\"0\"],[33813,33814,\"0\"],[33884,33965,\"0\"],[33968,34046,\"0\"],[34076,34080,\"0\"],[34335,34392,\"0\"],[34338,34392,\"0\"],[34594,34608,\"0\"],[34618,34634,\"0\"],[34655,35372,\"0\"],[34683,34715,\"0\"],[34703,34715,\"0\"],[34865,34924,\"0\"],[35233,35255,\"0\"],[35274,35289,\"0\"],[35484,35510,\"0\"],[35554,35610,\"0\"],[35554,35569,\"0\"],[35571,35610,\"0\"],[35611,35810,\"0\"],[35611,35638,\"0\"],[35619,35628,\"0\"],[35668,35677,\"0\"],[35725,35775,\"0\"],[35792,35800,\"0\"],[35811,35999,\"0\"],[35811,35836,\"0\"],[35818,35827,\"0\"],[35868,35877,\"0\"],[35915,35965,\"0\"],[35981,35990,\"0\"],[36002,36055,\"0\"],[36119,36123,\"0\"],[36324,36333,\"0\"],[36408,36417,\"0\"],[36458,36580,\"0\"],[36458,36492,\"0\"],[36529,36579,\"0\"],[36581,36707,\"0\"],[36581,36617,\"0\"],[36656,36706,\"0\"],[36708,36844,\"0\"],[37070,37186,\"0\"],[37081,37186,\"0\"],[37093,37186,\"0\"],[37187,37301,\"0\"],[37198,37301,\"0\"],[37210,37301,\"0\"],[37732,37757,\"0\"],[37764,37787,\"0\"],[37792,37815,\"0\"],[37821,37846,\"0\"],[37943,37981,\"0\"],[38063,38070,\"0\"],[38073,38080,\"0\"],[38082,38105,\"0\"],[38085,38105,\"0\"],[38085,38089,\"0\"],[38097,38104,\"0\"],[38475,38566,\"0\"],[38475,38494,\"0\"],[38496,38566,\"0\"],[38496,38528,\"0\"],[38649,38732,\"0\"],[38786,38848,\"0\"],[38795,38848,\"0\"],[39014,39037,\"0\"],[39063,39070,\"0\"],[39073,39080,\"0\"],[39182,39206,\"0\"],[39194,39206,\"0\"],[39209,39235,\"0\"],[39222,39235,\"0\"],[39238,39262,\"0\"],[39250,39262,\"0\"],[39265,39291,\"0\"],[39278,39291,\"0\"],[39301,39310,\"0\"],[39311,39320,\"0\"],[39321,39330,\"0\"],[39331,39340,\"0\"],[39341,39361,\"0\"],[39395,39411,\"0\"],[39468,39484,\"0\"],[39545,39605,\"0\"],[39545,39559,\"0\"],[39545,39551,\"0\"],[39553,39559,\"0\"],[39606,39611,\"0\"],[39716,39721,\"0\"],[40256,40287,\"0\"],[40256,40259,\"0\"],[41033,41124,\"0\"],[41033,41057,\"0\"],[41059,41124,\"0\"],[41059,41083,\"0\"],[41125,41174,\"0\"],[41365,41426,\"0\"],[41389,41426,\"0\"],[41427,41488,\"0\"],[41451,41488,\"0\"],[41542,41576,\"0\"],[41634,41659,\"0\"],[41638,41658,\"0\"],[41684,41693,\"0\"],[41883,41948,\"0\"],[41983,42015,\"0\"],[41993,42015,\"0\"]],null]]]\n[\"4/366\",\"b\",[[0,\"4/366\",null,[[2302,2635,\"0\"],[2316,2635,\"0\"],[2316,2351,\"0\"],[2443,2556,\"0\"],[2881,2954,\"0\"],[2881,2913,\"0\"],[2955,2986,\"0\"],[2958,2986,\"0\"],[3024,3036,\"0\"],[3038,3078,\"0\"],[3237,3309,\"0\"],[3513,3552,\"0\"],[3553,3597,\"0\"],[3598,3653,\"0\"],[3654,3714,\"0\"],[3715,3742,\"0\"],[3854,3906,\"0\"],[3854,3874,\"0\"],[3907,3931,\"0\"],[4227,4322,\"0\"],[4255,4322,\"0\"],[4280,4322,\"0\"],[4291,4322,\"0\"],[4373,4426,\"0\"],[4373,4401,\"0\"],[4507,4563,\"0\"],[4507,4551,\"0\"],[4507,4522,\"0\"],[4524,4551,\"0\"],[4564,4687,\"0\"],[4564,4608,\"0\"],[4564,4579,\"0\"],[4581,4608,\"0\"],[4610,4687,\"0\"],[4777,4818,\"0\"],[4822,4945,\"0\"],[5164,5244,\"0\"],[5834,5852,\"0\"],[6329,6372,\"0\"],[6349,6372,\"0\"],[6469,6484,\"0\"],[6721,6760,\"0\"],[6721,6745,\"0\"],[7522,7526,\"0\"],[7548,7552,\"0\"],[7892,7910,\"0\"],[8500,8504,\"0\"],[8526,8530,\"0\"],[8973,8994,\"0\"],[8973,8979,\"0\"],[9081,9134,\"0\"],[9185,9202,\"0\"],[9303,9326,\"0\"],[9373,9396,\"0\"],[9484,9517,\"0\"],[9518,9558,\"0\"],[9561,9595,\"0\"],[9596,9672,\"0\"],[9723,10342,\"0\"],[9723,9737,\"0\"],[9757,9813,\"0\"],[9757,9771,\"0\"],[9814,9850,\"0\"],[9814,9827,\"0\"],[9851,9958,\"0\"],[9851,9866,\"0\"],[9959,10066,\"0\"],[9959,9974,\"0\"],[10007,10036,\"0\"],[10010,10036,\"0\"],[10037,10065,\"0\"],[10041,10065,\"0\"],[10067,10099,\"0\"],[10067,10081,\"0\"],[10100,10268,\"0\"],[10100,10115,\"0\"],[10148,10177,\"0\"],[10151,10177,\"0\"],[10178,10234,\"0\"],[10181,10234,\"0\"],[10181,10199,\"0\"],[10235,10267,\"0\"],[10238,10267,\"0\"],[10238,10244,\"0\"],[10269,10341,\"0\"],[10269,10280,\"0\"],[10464,10504,\"0\"],[10464,10482,\"0\"],[10600,10628,\"0\"],[10662,10679,\"0\"],[10782,10886,\"0\"],[11149,11221,\"0\"],[11260,11300,\"0\"],[11381,11399,\"0\"],[11440,11625,\"0\"],[11526,11622,\"0\"],[11529,11622,\"0\"],[11829,11872,\"0\"],[11909,11941,\"0\"],[11992,11993,\"0\"],[52032,52340,\"1/2\"],[52033,52052,\"1/2\"],[194,276,\"0\"],[194,211,\"0\"],[52654,52669,\"0\"],[52735,52751,\"0\"],[52776,52777,\"0\"],[52883,52927,\"0\"],[52984,53016,\"0\"],[53090,53100,\"0\"],[53105,53131,\"0\"],[53105,53116,\"0\"],[53250,53262,\"0\"],[53275,53290,\"0\"],[53329,53340,\"0\"],[53524,53565,\"0\"],[53646,53689,\"0\"],[53738,53908,\"0\"],[53742,53908,\"0\"],[53742,53788,\"0\"],[53742,53766,\"0\"],[53790,53908,\"0\"],[53823,53847,\"0\"],[53956,54128,\"0\"],[53960,54128,\"0\"],[53960,54006,\"0\"],[53960,53984,\"0\"],[54008,54128,\"0\"],[54041,54065,\"0\"],[54156,54181,\"0\"],[54194,54244,\"0\"],[54198,54244,\"0\"],[54198,54222,\"0\"],[54376,54570,\"0\"],[54394,54570,\"0\"],[54415,54570,\"0\"],[54415,54448,\"0\"],[54450,54570,\"0\"],[54477,54501,\"0\"],[54590,54591,\"0\"],[54595,54653,\"0\"],[54628,54652,\"0\"],[54795,54819,\"0\"],[173763,173774,\"0\"],[173763,173767,\"0\"],[173769,173774,\"0\"],[173803,173812,\"0\"],[173829,173863,\"0\"],[173888,173922,\"0\"],[174114,174136,\"0\"],[174114,174118,\"0\"],[174120,174136,\"0\"],[174120,174124,\"0\"],[174126,174136,\"0\"],[174126,174130,\"0\"],[174132,174136,\"0\"],[174160,174292,\"0\"],[174160,174191,\"0\"],[174193,174292,\"0\"],[174193,174224,\"0\"],[174226,174292,\"0\"],[174226,174258,\"0\"],[174260,174292,\"0\"],[174314,174385,\"0\"],[174405,174474,\"0\"],[174405,174437,\"0\"],[174405,174415,\"0\"],[174405,174409,\"0\"],[174411,174415,\"0\"],[174417,174437,\"0\"],[174417,174427,\"0\"],[174417,174421,\"0\"],[174423,174427,\"0\"],[174429,174437,\"0\"],[174429,174432,\"0\"],[174434,174437,\"0\"],[174441,174473,\"0\"],[174441,174451,\"0\"],[174441,174445,\"0\"],[174447,174451,\"0\"],[174453,174473,\"0\"],[174453,174463,\"0\"],[174453,174457,\"0\"],[174459,174463,\"0\"],[174465,174473,\"0\"],[174465,174468,\"0\"],[174470,174473,\"0\"],[174591,174637,\"0\"],[174640,174641,\"0\"],[174657,174681,\"0\"],[174731,174741,\"0\"],[174749,174837,\"0\"],[174751,174836,\"0\"],[174774,174836,\"0\"],[174811,174835,\"0\"],[174847,174857,\"0\"],[174865,174887,\"0\"],[174941,174977,\"0\"],[174978,175162,\"0\"],[174993,175037,\"0\"],[174993,175008,\"0\"],[175208,175254,\"0\"],[175275,175554,\"0\"],[175290,175394,\"0\"],[175314,175394,\"0\"],[175328,175394,\"0\"],[175367,175393,\"0\"],[175395,175553,\"0\"],[175419,175553,\"0\"],[175433,175553,\"0\"],[175466,175490,\"0\"],[175648,175710,\"0\"],[175733,175795,\"0\"],[175803,175849,\"0\"],[175864,175919,\"0\"],[175888,175919,\"0\"],[175970,175985,\"0\"],[175994,176009,\"0\"],[176024,176478,\"0\"],[176028,176263,\"0\"],[176142,176189,\"0\"],[176192,176262,\"0\"],[176249,176261,\"0\"],[176264,176322,\"0\"],[176267,176322,\"0\"],[176267,176279,\"0\"],[176339,176350,\"0\"],[176377,176389,\"0\"],[176419,176477,\"0\"],[176422,176477,\"0\"],[176422,176433,\"0\"],[176561,176623,\"0\"],[176626,177197,\"1/2\"],[176626,176645,\"1/2\"],[176767,176840,\"0\"],[176886,176962,\"0\"],[177004,177075,\"0\"],[177120,177194,\"0\"],[177932,177942,\"0\"],[178075,178109,\"0\"],[178075,178085,\"0\"],[178129,178135,\"0\"],[178136,178161,\"0\"],[178136,178154,\"0\"],[178162,178180,\"0\"],[178393,178428,\"0\"],[178393,178402,\"0\"],[178609,178627,\"0\"],[178787,178817,\"0\"],[41,51,\"0\"],[178906,178926,\"0\"],[178997,179015,\"0\"],[179058,179184,\"0\"],[179259,179323,\"0\"],[179325,179385,\"0\"],[179325,179342,\"0\"],[179386,179433,\"0\"],[179386,179406,\"0\"],[180581,181533,\"0\"],[180768,180797,\"0\"],[180908,181096,\"0\"],[180919,181096,\"0\"],[181193,181194,\"0\"],[181198,181257,\"0\"],[181198,181220,\"0\"],[181284,181285,\"0\"],[181339,181340,\"0\"],[181418,181419,\"0\"],[181561,181599,\"0\"],[181686,181704,\"0\"],[181686,181689,\"0\"],[181705,181723,\"0\"],[181705,181708,\"0\"],[181855,182993,\"0\"],[181858,182993,\"0\"],[181858,181882,\"0\"],[182015,182036,\"0\"],[182041,182081,\"0\"],[182041,182049,\"0\"],[182051,182081,\"0\"],[182051,182060,\"0\"],[182062,182081,\"0\"],[182062,182069,\"0\"],[182071,182081,\"0\"],[182083,182148,\"0\"],[182083,182102,\"0\"],[182107,182147,\"0\"],[182107,182115,\"0\"],[182117,182147,\"0\"],[182117,182126,\"0\"],[182128,182147,\"0\"],[182128,182135,\"0\"],[182137,182147,\"0\"],[182149,182150,\"0\"],[182152,182220,\"0\"],[182221,182298,\"0\"],[182299,182415,\"0\"],[182418,182771,\"0\"],[182433,182471,\"0\"],[182433,182453,\"0\"],[182641,182717,\"0\"],[182772,182991,\"0\"],[182785,182990,\"0\"],[182796,182990,\"0\"],[182807,182990,\"0\"],[184522,184543,\"0\"],[185022,185067,\"0\"],[185022,185045,\"0\"],[185146,185175,\"0\"],[185705,185806,\"0\"],[185705,185723,\"0\"],[185725,185806,\"0\"],[185725,185747,\"0\"],[185781,185805,\"0\"],[186187,186241,\"0\"],[186244,186291,\"0\"],[186244,186269,\"0\"],[186575,186746,\"0\"],[186765,187120,\"0\"],[186789,186803,\"0\"],[186957,187119,\"0\"],[187235,187349,\"0\"],[187622,187826,\"0\"],[187622,187636,\"0\"],[187727,187772,\"0\"],[188036,188090,\"0\"],[188164,188165,\"0\"],[188240,188251,\"0\"],[188400,188423,\"0\"],[188400,188411,\"0\"],[188413,188423,\"0\"],[188426,188437,\"0\"],[188489,188527,\"0\"],[188652,188663,\"0\"],[188715,188857,\"0\"],[188728,188747,\"0\"],[188897,189016,\"0\"],[188911,189015,\"0\"],[189061,189080,\"0\"],[189106,189148,\"0\"],[189394,189433,\"0\"],[189461,189469,\"0\"],[189536,189544,\"0\"],[189612,189623,\"0\"],[189962,189983,\"0\"],[190071,190148,\"0\"],[190369,190416,\"0\"],[190369,190380,\"0\"],[190634,190645,\"0\"],[190782,190838,\"0\"],[190782,190796,\"0\"],[190839,190872,\"0\"],[190839,190850,\"0\"],[191085,191086,\"0\"],[191222,191282,\"0\"],[191228,191248,\"0\"],[191283,191337,\"0\"],[191283,191290,\"0\"],[191338,191373,\"0\"],[191501,191512,\"0\"],[191578,191589,\"0\"],[191775,191781,\"0\"],[191820,191886,\"0\"],[191820,191828,\"0\"],[192365,192384,\"0\"],[192394,192432,\"0\"]],null]]]\n[\"0/516\",\"b\",[[0,\"0/516\",null,[[861,889,\"0\"],[861,886,\"0\"],[890,938,\"0\"],[890,900,\"0\"],[950,953,\"0\"],[1259,1266,\"0\"],[1268,1459,\"0\"],[1268,1280,\"0\"],[1283,1291,\"0\"],[1292,1328,\"0\"],[1309,1328,\"0\"],[1364,1413,\"0\"],[1460,1489,\"0\"],[1693,1781,\"0\"],[1693,1725,\"0\"],[1693,1708,\"0\"],[1710,1725,\"0\"],[1784,1815,\"0\"],[2027,2058,\"0\"],[2156,2166,\"0\"],[2450,2530,\"0\"],[2450,2508,\"0\"],[2450,2453,\"0\"],[2455,2508,\"0\"],[2455,2460,\"0\"],[2463,2507,\"0\"],[2463,2486,\"0\"],[2488,2507,\"0\"],[2552,2558,\"0\"],[2755,2756,\"0\"],[2837,2876,\"0\"],[2879,2921,\"0\"],[2924,2958,\"0\"],[2961,3020,\"0\"],[3497,3534,\"0\"],[3612,3643,\"0\"],[3701,3718,\"0\"],[3803,3820,\"0\"],[3853,3863,\"0\"],[3853,3856,\"0\"],[3864,3874,\"0\"],[3864,3867,\"0\"],[3875,3913,\"0\"],[3875,3904,\"0\"],[4072,4082,\"0\"],[4106,4196,\"0\"],[4142,4152,\"0\"],[4179,4195,\"0\"],[4237,4284,\"0\"],[4418,4586,\"0\"],[4460,4461,\"0\"],[4484,4578,\"0\"],[4484,4514,\"0\"],[4516,4578,\"0\"],[4516,4539,\"0\"],[4544,4549,\"0\"],[4587,4709,\"0\"],[4587,4592,\"0\"],[4642,4708,\"0\"],[4642,4648,\"0\"],[4651,4676,\"0\"],[4794,4900,\"0\"],[4812,4900,\"0\"],[4927,4943,\"0\"],[5087,5105,\"0\"],[5212,5229,\"0\"],[5255,5279,\"0\"],[5287,5312,\"0\"],[5387,5397,\"0\"],[5520,5731,\"0\"],[5520,5531,\"0\"],[5533,5731,\"0\"],[5533,5556,\"0\"],[5559,5568,\"0\"],[5631,5730,\"0\"],[5631,5640,\"0\"],[5732,5794,\"0\"],[5982,6023,\"0\"],[6041,6057,\"0\"],[6248,6252,\"0\"],[6701,6717,\"0\"],[6779,6804,\"0\"],[6844,6854,\"0\"],[6942,6961,\"0\"],[6989,7008,\"0\"],[7039,7058,\"0\"],[7117,7148,\"0\"],[7117,7133,\"0\"],[7139,7142,\"0\"],[7295,7321,\"0\"],[7295,7313,\"0\"],[7445,7521,\"0\"],[7445,7468,\"0\"],[7584,7590,\"0\"],[7717,7767,\"0\"],[7717,7731,\"0\"],[7768,7823,\"0\"],[7768,7784,\"0\"],[7909,7925,\"0\"],[7944,7969,\"0\"],[8068,8096,\"0\"],[8138,8139,\"0\"],[8170,8527,\"0\"],[8170,8190,\"0\"],[8193,8221,\"0\"],[8223,8290,\"0\"],[8223,8228,\"0\"],[8248,8249,\"0\"],[8291,8371,\"0\"],[8291,8296,\"0\"],[8306,8307,\"0\"],[8374,8443,\"0\"],[8374,8379,\"0\"],[8399,8400,\"0\"],[8444,8525,\"0\"],[8444,8449,\"0\"],[8459,8460,\"0\"],[8590,8595,\"0\"],[8618,8649,\"0\"],[8695,8696,\"0\"],[8727,8835,\"0\"],[8727,8736,\"0\"],[8738,8835,\"0\"],[8738,8769,\"0\"],[8792,8793,\"0\"],[8836,8948,\"0\"],[8836,8845,\"0\"],[8847,8948,\"0\"],[8847,8878,\"0\"],[8901,8902,\"0\"],[8949,9056,\"0\"],[8949,8958,\"0\"],[8960,9056,\"0\"],[8960,8989,\"0\"],[9012,9013,\"0\"],[9057,9168,\"0\"],[9057,9066,\"0\"],[9068,9168,\"0\"],[9068,9097,\"0\"],[9120,9121,\"0\"],[9474,9610,\"0\"],[9493,9610,\"0\"],[9603,9609,\"0\"],[9649,9674,\"0\"],[10075,10095,\"0\"],[10186,10206,\"0\"],[10329,10456,\"0\"],[29216,29227,\"0\"],[29216,29220,\"0\"],[29222,29227,\"0\"],[29259,29336,\"0\"],[29561,29601,\"0\"],[29561,29573,\"0\"],[29767,29795,\"0\"],[29947,29951,\"0\"],[30082,30096,\"0\"],[30100,30151,\"0\"],[30123,30151,\"0\"],[30223,30260,\"0\"],[30285,30343,\"0\"],[30285,30326,\"0\"],[30344,30345,\"0\"],[30346,30461,\"0\"],[30368,30461,\"0\"],[30433,30456,\"0\"],[30433,30448,\"0\"],[30580,32193,\"0\"],[31233,31285,\"0\"],[31373,31434,\"0\"],[31373,31409,\"0\"],[31461,31498,\"0\"],[31499,31709,\"0\"],[31509,31709,\"0\"],[31509,31526,\"0\"],[31710,31836,\"0\"],[31722,31798,\"0\"],[31837,31957,\"0\"],[31848,31921,\"0\"],[31958,32086,\"0\"],[31958,31997,\"0\"],[31999,32086,\"0\"],[31999,32036,\"0\"],[32127,32190,\"0\"],[32225,32229,\"0\"],[32302,32347,\"0\"],[32348,32419,\"0\"],[32364,32419,\"0\"],[32661,32722,\"0\"],[32723,34048,\"0\"],[32745,32823,\"0\"],[32745,32784,\"0\"],[32786,32823,\"0\"],[32825,32910,\"0\"],[32989,33121,\"0\"],[32989,33040,\"0\"],[33122,33207,\"0\"],[33288,33423,\"0\"],[33288,33340,\"0\"],[33426,33479,\"0\"],[33547,33693,\"0\"],[33547,33623,\"0\"],[33694,33748,\"0\"],[33818,33966,\"0\"],[33818,33894,\"0\"],[33968,34047,\"0\"],[33968,33974,\"0\"],[33976,34047,\"0\"],[33992,34047,\"0\"],[34102,34197,\"0\"],[34102,34144,\"0\"],[34121,34144,\"0\"],[34198,34291,\"0\"],[34198,34240,\"0\"],[34217,34240,\"0\"],[34314,34318,\"0\"],[34326,34605,\"0\"],[34387,34605,\"0\"],[34390,34605,\"0\"],[34390,34424,\"0\"],[34426,34605,\"0\"],[34426,34449,\"0\"],[34451,34605,\"0\"],[34451,34497,\"0\"],[34468,34473,\"0\"],[34499,34605,\"0\"],[34536,34570,\"0\"],[34610,34708,\"0\"],[34625,34630,\"0\"],[34643,34708,\"0\"],[34643,34677,\"0\"],[34817,34860,\"0\"],[34970,34971,\"0\"],[34976,35068,\"0\"],[34976,35048,\"0\"],[34992,35048,\"0\"],[35134,35284,\"0\"],[35134,35144,\"0\"],[35137,35144,\"0\"],[35204,35248,\"0\"],[35285,35430,\"0\"],[35285,35295,\"0\"],[35288,35295,\"0\"],[35351,35395,\"0\"],[35474,35511,\"0\"],[35591,35604,\"0\"],[35658,35690,\"0\"],[35847,35851,\"0\"],[35922,36063,\"0\"],[36071,36477,\"0\"],[36090,36185,\"0\"],[36186,36280,\"0\"],[36186,36218,\"0\"],[36220,36280,\"0\"],[36233,36280,\"0\"],[36353,36374,\"0\"],[36535,36549,\"0\"],[36565,36570,\"0\"],[36599,36642,\"0\"],[36601,36613,\"0\"],[36620,36637,\"0\"],[36650,36680,\"0\"],[36682,36747,\"0\"],[36693,36709,\"0\"],[36716,36735,\"0\"],[36750,36785,\"0\"],[36761,36785,\"0\"],[36848,36862,\"0\"],[36878,36883,\"0\"],[36909,36952,\"0\"],[9,21,\"0\"],[28,45,\"0\"],[37198,37235,\"0\"],[37198,37221,\"0\"],[37223,37235,\"0\"],[37223,37228,\"0\"],[37230,37235,\"0\"],[37238,37275,\"0\"],[37238,37261,\"0\"],[37263,37275,\"0\"],[37263,37268,\"0\"],[37270,37275,\"0\"],[37278,37282,\"0\"],[37289,37489,\"0\"],[37289,37323,\"0\"],[37325,37489,\"0\"],[37365,37489,\"0\"],[37365,37399,\"0\"],[37401,37489,\"0\"],[37424,37437,\"0\"],[37458,37471,\"0\"],[37492,37636,\"0\"],[37492,37526,\"0\"],[37528,37636,\"0\"],[37528,37562,\"0\"],[37564,37636,\"0\"],[37564,37599,\"0\"],[37601,37636,\"0\"],[37683,37783,\"0\"],[37683,37706,\"0\"],[37786,37888,\"0\"],[37786,37809,\"0\"],[37891,37895,\"0\"],[37903,37904,\"0\"],[37978,37991,\"0\"],[37992,38015,\"0\"],[37992,38003,\"0\"],[38005,38015,\"0\"],[38020,38039,\"0\"],[38024,38034,\"0\"],[38340,38356,\"0\"],[38357,38387,\"0\"],[38357,38371,\"0\"],[38357,38368,\"0\"],[38373,38387,\"0\"],[38373,38383,\"0\"],[38388,38420,\"0\"],[38392,38419,\"0\"],[38392,38405,\"0\"],[38392,38402,\"0\"],[38407,38419,\"0\"],[38407,38415,\"0\"],[38518,38542,\"0\"],[38518,38523,\"0\"],[38526,38529,\"0\"],[38644,38671,\"0\"],[38644,38649,\"0\"],[38652,38655,\"0\"],[38829,38863,\"0\"],[38998,39002,\"0\"],[39020,39024,\"0\"],[39073,39077,\"0\"],[39117,39312,\"0\"],[39120,39312,\"0\"],[39120,39128,\"0\"],[39130,39312,\"0\"],[39159,39188,\"0\"],[39325,39357,\"0\"],[39560,39564,\"0\"],[39750,39760,\"0\"],[39768,39784,\"0\"],[39906,39938,\"0\"],[40073,40086,\"0\"],[40104,40108,\"0\"],[40157,40161,\"0\"],[40201,40392,\"0\"],[40204,40392,\"0\"],[40204,40212,\"0\"],[40214,40392,\"0\"],[40244,40273,\"0\"],[40410,40414,\"0\"],[40458,40461,\"0\"],[40612,40629,\"0\"],[40630,40691,\"0\"],[40630,40653,\"0\"],[40695,40771,\"0\"],[40714,40771,\"0\"],[40810,40814,\"0\"],[40835,41099,\"0\"],[40835,40925,\"0\"],[40835,40869,\"0\"],[40871,40925,\"0\"],[40894,40925,\"0\"],[40930,40959,\"0\"],[41007,41059,\"0\"],[41103,41161,\"0\"],[41247,41251,\"0\"],[41592,41599,\"0\"],[41618,42498,\"0\"],[41618,41667,\"0\"],[41633,41667,\"0\"],[41842,41871,\"0\"],[41910,41921,\"0\"],[42013,42021,\"0\"],[42062,42112,\"0\"],[42062,42071,\"0\"],[42113,42144,\"0\"],[42170,42495,\"0\"],[42170,42196,\"0\"],[42200,42348,\"0\"],[42258,42292,\"0\"],[42306,42343,\"0\"],[42349,42494,\"0\"],[42404,42439,\"0\"],[42453,42489,\"0\"],[42767,42793,\"0\"],[42910,42914,\"0\"],[42922,42984,\"0\"],[42988,43043,\"0\"],[43048,43105,\"0\"],[43145,43283,\"0\"],[43287,43288,\"0\"],[43292,43318,\"0\"],[43319,43458,\"0\"],[43483,43529,\"0\"],[43532,43533,\"0\"],[43549,43550,\"0\"],[43570,43571,\"0\"],[43610,43614,\"0\"],[43619,43888,\"0\"],[43684,43888,\"0\"],[43684,43727,\"0\"],[43768,43801,\"0\"],[43768,43793,\"0\"],[43802,43887,\"0\"],[43802,43820,\"0\"],[43873,43874,\"0\"],[43892,43921,\"0\"],[43932,43974,\"0\"],[43982,44123,\"0\"],[44131,44132,\"0\"],[44576,44598,\"0\"],[44655,44673,\"0\"],[44723,44836,\"0\"],[44751,44772,\"0\"],[44837,45070,\"0\"],[44837,44863,\"0\"],[45072,45144,\"0\"],[45072,45108,\"0\"],[45146,45221,\"0\"],[45146,45183,\"0\"],[45261,45297,\"0\"],[45261,45279,\"0\"],[45298,45341,\"0\"],[45331,45339,\"0\"],[45342,45403,\"0\"],[45404,45496,\"0\"],[45497,45554,\"0\"],[45555,45648,\"0\"],[45762,45966,\"0\"],[45762,45791,\"0\"],[45793,45966,\"0\"],[45793,45832,\"0\"],[45834,45966,\"0\"],[45968,46125,\"0\"],[45968,46104,\"0\"],[45968,46012,\"0\"],[46014,46104,\"0\"],[46044,46104,\"0\"],[46044,46095,\"0\"],[46138,46193,\"0\"],[46208,46264,\"0\"],[46301,46330,\"0\"],[46384,46422,\"0\"],[46467,46506,\"0\"],[46606,46656,\"0\"],[46661,46710,\"0\"],[46890,46957,\"0\"],[46890,46914,\"0\"],[46959,47458,\"0\"],[46959,47011,\"0\"],[46959,46985,\"0\"],[46987,47011,\"0\"],[47135,47161,\"0\"],[47252,47278,\"0\"],[47279,47340,\"0\"],[47341,47408,\"0\"],[47459,48235,\"0\"],[47562,47593,\"0\"],[47620,47663,\"0\"],[47666,47705,\"0\"],[47732,47774,\"0\"],[47777,47815,\"0\"],[47842,47843,\"0\"],[47898,47941,\"0\"],[47944,47984,\"0\"],[48040,48041,\"0\"],[48099,48141,\"0\"],[48144,48185,\"0\"],[48270,48290,\"0\"],[48297,48311,\"0\"],[48319,48444,\"0\"],[48319,48348,\"0\"],[48350,48444,\"0\"],[48350,48389,\"0\"],[48587,48613,\"0\"],[48645,48646,\"0\"],[48733,48759,\"0\"],[48792,48793,\"0\"],[48891,49016,\"0\"],[48891,48920,\"0\"],[48922,49016,\"0\"],[48922,48961,\"0\"],[49095,49260,\"0\"],[49095,49124,\"0\"],[49126,49260,\"0\"],[49126,49210,\"0\"],[49126,49165,\"0\"],[49167,49210,\"0\"],[49261,50228,\"0\"],[49285,49696,\"0\"],[49304,49402,\"0\"],[49304,49354,\"0\"],[49403,49499,\"0\"],[49403,49452,\"0\"],[49500,49598,\"0\"],[49500,49550,\"0\"],[49599,49695,\"0\"],[49599,49648,\"0\"],[49697,50227,\"0\"],[49784,49800,\"0\"],[49801,49891,\"0\"],[49801,49845,\"0\"],[49847,49891,\"0\"],[49894,49938,\"0\"],[50043,50059,\"0\"],[50060,50152,\"0\"],[50060,50105,\"0\"],[50107,50152,\"0\"],[50155,50200,\"0\"],[50307,50333,\"0\"],[50365,50366,\"0\"],[50465,50491,\"0\"],[50524,50525,\"0\"],[50575,50576,\"0\"],[50669,50692,\"0\"],[50739,50751,\"0\"]],null]]]\n[\"6/337\",\"b\",[[0,\"6/337\",null,[[237,280,\"0\"],[237,253,\"0\"],[22323,22478,\"0\"],[22323,22418,\"0\"],[22343,22418,\"0\"],[22442,22478,\"0\"],[22506,22590,\"0\"],[22506,22564,\"0\"],[22506,22533,\"0\"],[22535,22564,\"0\"],[22719,22819,\"0\"],[22737,22819,\"0\"],[22820,23038,\"0\"],[22820,22972,\"0\"],[22839,22972,\"0\"],[22839,22916,\"0\"],[22918,22972,\"0\"],[22974,23038,\"0\"],[23039,23370,\"0\"],[23039,23067,\"0\"],[23070,23369,\"0\"],[23398,23402,\"0\"],[23407,23457,\"0\"],[23458,23581,\"0\"],[23585,24095,\"0\"],[23585,23695,\"0\"],[23696,23763,\"0\"],[23764,23859,\"0\"],[23809,23836,\"0\"],[23877,23924,\"0\"],[23989,24092,\"0\"],[24016,24074,\"0\"],[24016,24052,\"0\"],[24105,24115,\"0\"],[24251,24320,\"0\"],[24251,24305,\"0\"],[24348,24355,\"0\"],[24391,24411,\"0\"],[24515,24516,\"0\"],[53720,53773,\"0\"],[53720,53737,\"0\"],[54045,54072,\"0\"],[54045,54052,\"0\"],[54054,54072,\"0\"],[54107,54159,\"0\"],[54123,54159,\"0\"],[54250,54265,\"0\"],[54328,54389,\"0\"],[54328,54362,\"0\"],[54427,54428,\"0\"],[54433,54464,\"0\"],[54486,54489,\"0\"],[54596,54624,\"0\"],[54714,54772,\"0\"],[54773,54864,\"0\"],[54873,54931,\"0\"],[55077,55168,\"0\"],[55077,55096,\"0\"],[55116,55160,\"0\"],[55258,55304,\"0\"],[55305,55324,\"0\"],[55347,55391,\"0\"],[56217,56351,\"0\"],[56217,56276,\"0\"],[56278,56351,\"0\"],[56278,56303,\"0\"],[56660,56704,\"0\"],[56675,56704,\"0\"],[56706,56736,\"0\"],[56788,56794,\"0\"],[56940,56955,\"0\"],[56956,56986,\"0\"],[57042,57128,\"0\"],[57042,57095,\"0\"],[57057,57095,\"0\"],[57191,57192,\"0\"],[57193,57208,\"0\"],[57330,57406,\"0\"],[57330,57353,\"0\"],[57469,57475,\"0\"],[57549,57561,\"0\"],[57655,57676,\"0\"],[57655,57667,\"0\"],[57669,57676,\"0\"],[57683,57704,\"0\"],[57683,57695,\"0\"],[57697,57704,\"0\"],[57732,57864,\"0\"],[57907,57940,\"0\"],[57907,57920,\"0\"],[57922,57940,\"0\"],[58038,58111,\"0\"],[58038,58070,\"0\"],[58038,58047,\"0\"],[58049,58070,\"0\"],[58049,58058,\"0\"],[58060,58070,\"0\"],[58073,58111,\"0\"],[58073,58091,\"0\"],[58112,58361,\"0\"],[58112,58123,\"0\"],[58551,58552,\"0\"],[58642,58768,\"0\"],[58642,58660,\"0\"],[58662,58768,\"0\"],[58662,58668,\"0\"],[58673,58716,\"0\"],[58769,58785,\"0\"],[58769,58775,\"0\"],[58820,58826,\"0\"],[58830,58873,\"0\"],[59145,59153,\"0\"],[59157,59181,\"0\"],[59210,59294,\"0\"],[59210,59216,\"0\"],[59236,59293,\"0\"],[59236,59244,\"0\"],[59247,59274,\"0\"],[59560,59626,\"0\"],[59588,59592,\"0\"],[59693,59760,\"0\"],[59721,59725,\"0\"],[59797,59801,\"0\"],[59863,59867,\"0\"],[59915,59931,\"0\"],[60045,60332,\"1/2\"],[60046,60065,\"1/2\"],[60873,60895,\"0\"],[60873,60888,\"0\"],[60890,60895,\"0\"],[61128,61281,\"0\"],[61294,61339,\"0\"],[61294,61318,\"0\"],[61387,61426,\"0\"],[61543,61755,\"0\"],[61543,61551,\"0\"],[61554,61641,\"0\"],[61593,61623,\"0\"],[61642,61708,\"0\"],[61642,61650,\"0\"],[61710,61754,\"0\"],[61710,61726,\"0\"],[61710,61718,\"0\"],[61720,61726,\"0\"],[61732,61748,\"0\"],[61756,61819,\"0\"],[61756,61762,\"0\"],[61800,61818,\"0\"],[61800,61806,\"0\"],[61811,61812,\"0\"],[61820,61858,\"0\"],[61824,61858,\"0\"],[61824,61830,\"0\"],[61832,61858,\"0\"],[61922,61940,\"0\"],[62098,62121,\"0\"],[62542,62565,\"0\"],[62669,62845,\"0\"],[62680,62845,\"0\"],[62874,62987,\"0\"],[62898,62987,\"0\"],[62909,62987,\"0\"],[62909,62936,\"0\"],[63023,63065,\"0\"],[63033,63065,\"0\"],[63132,63176,\"0\"],[63142,63176,\"0\"],[63298,63308,\"0\"],[63298,63301,\"0\"],[63309,63319,\"0\"],[63309,63312,\"0\"],[63351,63392,\"0\"],[63396,63397,\"0\"],[63543,63555,\"0\"],[63601,63716,\"0\"],[63601,63618,\"0\"],[63669,63715,\"0\"],[63672,63715,\"0\"],[63672,63696,\"0\"],[63717,63750,\"0\"],[63717,63728,\"0\"],[63751,63795,\"0\"],[63751,63768,\"0\"],[63842,63843,\"0\"],[64083,64117,\"0\"],[64083,64096,\"0\"],[64118,64181,\"0\"],[64182,64218,\"0\"],[64731,64749,\"0\"],[65291,65343,\"0\"],[65406,65465,\"0\"],[65889,65902,\"0\"],[65975,66022,\"0\"],[66051,66122,\"0\"],[66096,66120,\"0\"],[66153,66189,\"0\"],[66190,66226,\"0\"],[66405,66498,\"0\"],[66527,66574,\"0\"],[66705,66773,\"0\"],[66720,66728,\"0\"],[66729,66767,\"0\"],[66729,66759,\"0\"],[66778,66792,\"0\"],[66804,66831,\"0\"],[66804,66810,\"0\"],[66812,66831,\"0\"],[67079,67085,\"0\"],[67122,67181,\"0\"],[67536,67546,\"0\"],[67697,67748,\"0\"],[67697,67711,\"0\"],[67713,67748,\"0\"],[67713,67724,\"0\"],[67960,68059,\"0\"],[67960,67970,\"0\"],[68161,68172,\"0\"],[68175,68191,\"0\"],[68194,68195,\"0\"],[68226,68234,\"0\"],[68293,68294,\"0\"],[68332,68692,\"0\"],[68365,68692,\"0\"],[68396,68692,\"0\"],[68410,68692,\"0\"],[68410,68427,\"0\"],[68429,68692,\"0\"],[68429,68469,\"0\"],[68481,68482,\"0\"],[68517,68518,\"0\"],[68525,68551,\"0\"],[68552,68631,\"0\"],[68562,68631,\"0\"],[68632,68673,\"0\"],[68853,68877,\"0\"],[68974,69001,\"0\"],[69310,69328,\"0\"],[69358,69453,\"0\"],[69403,69431,\"0\"],[69609,69759,\"0\"],[69609,69630,\"0\"],[69633,69658,\"0\"],[69792,69798,\"0\"],[69848,69951,\"0\"],[69848,69866,\"0\"],[69975,70001,\"0\"],[70160,70190,\"0\"],[70396,70397,\"0\"],[70511,70577,\"0\"],[70511,70547,\"0\"],[70626,70809,\"0\"],[70626,70632,\"0\"],[70635,70645,\"0\"],[70674,70686,\"0\"],[70714,70719,\"0\"],[70766,70771,\"0\"],[70862,70868,\"0\"],[70873,70883,\"0\"],[70897,70936,\"0\"],[70917,70936,\"0\"],[70946,70958,\"0\"],[71170,71203,\"0\"],[71170,71181,\"0\"],[71266,71289,\"0\"],[71266,71275,\"0\"],[71291,71617,\"0\"],[71356,71616,\"0\"],[71366,71616,\"0\"],[71366,71398,\"0\"],[71924,72064,\"1/2\"],[71925,71944,\"1/2\"],[72205,72230,\"0\"],[72449,72479,\"0\"],[72640,72670,\"0\"],[72702,72723,\"0\"],[72702,72708,\"0\"],[72776,72777,\"0\"],[73214,73293,\"0\"],[73214,73227,\"0\"],[73331,73332,\"0\"],[73631,73646,\"0\"],[73790,73860,\"0\"],[73922,73923,\"0\"],[73975,74428,\"0\"],[73985,74428,\"0\"],[74012,74071,\"0\"],[74101,74369,\"0\"],[74104,74369,\"0\"],[74104,74124,\"0\"],[74170,74260,\"0\"],[74261,74367,\"0\"],[74497,74498,\"0\"],[74517,74557,\"0\"],[74517,74535,\"0\"],[74537,74557,\"0\"],[74623,74683,\"0\"],[74651,74664,\"0\"],[74688,74708,\"0\"],[74761,74791,\"0\"],[74844,74845,\"0\"],[74850,74865,\"0\"],[74926,75009,\"0\"],[74944,74967,\"0\"],[74947,74967,\"0\"],[75338,75382,\"0\"],[75358,75382,\"0\"],[75520,75688,\"0\"],[75540,75688,\"0\"],[75559,75688,\"0\"],[75627,75671,\"0\"],[75792,75823,\"0\"],[75883,75972,\"0\"],[75883,75905,\"0\"],[75973,76020,\"0\"],[75973,75996,\"0\"],[75976,75996,\"0\"],[76021,76064,\"0\"],[76021,76042,\"0\"],[76024,76042,\"0\"],[76118,76119,\"0\"],[76173,76174,\"0\"],[76188,76789,\"0\"],[76232,76320,\"0\"],[76260,76320,\"0\"],[76514,76559,\"0\"],[76514,76536,\"0\"],[76597,76713,\"0\"],[76600,76713,\"0\"],[76600,76621,\"0\"],[76764,76788,\"0\"],[77184,77185,\"0\"],[77349,77384,\"0\"],[77550,77672,\"0\"],[77579,77638,\"0\"],[77703,77926,\"1/2\"],[77704,77723,\"1/2\"],[145,217,\"0\"]],null]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[138,148,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[133,144,\"0\"]],null]]]\n[0,null,[[0,0]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[7,49,\"1/2\"],[7,35,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[7,49,\"1/2\"],[7,35,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[7,49,\"1/2\"],[7,35,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[7,49,\"1/2\"],[7,35,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[7,49,\"1/2\"],[7,35,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n[1,null,[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[7,49,\"1/2\"],[7,35,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[7,49,\"1/2\"],[7,35,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[7,49,\"1/2\"],[7,35,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[7,49,\"1/2\"],[7,35,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[7,49,\"1/2\"],[7,35,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[129,158,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"7/11\",\"b\",[[0,\"7/11\",null,[[77,188,\"1/2\"],[77,109,\"1/2\"],[77,91,\"1/2\"],[95,109,\"0\"],[115,188,\"1/2\"],[115,167,\"1/2\"],[115,130,\"1/2\"],[134,167,\"0\"],[134,149,\"0\"],[153,167,\"0\"],[173,187,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[11,39,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[146,153,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[53,74,\"1/2\"]],null]]]\n\n[1,null,[[0,1]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[13,33,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[82,91,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[54,104,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[243,259,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[286,287,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[317,332,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[443,465,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[666,714,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[79,84,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[145,170,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[71,86,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[160,182,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[16,35,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[84,106,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[207,221,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[100,114,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[58,88,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,35,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[257,290,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[14,74,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[509,516,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[93,128,\"0\"],[93,118,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[140,145,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[218,236,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[268,274,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[90,154,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[197,220,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[277,370,\"0\"]],null]]]\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[376,597,\"0\"],[376,405,\"0\"],[409,597,\"0\"]],null]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,49,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[5,31,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[37,82,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[88,219,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[80,100,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[69,194,\"0\"],[69,109,\"0\"],[69,88,\"0\"],[92,109,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[203,233,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[265,284,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[128,169,\"0\"],[128,158,\"0\"],[162,169,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[187,199,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[305,323,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[376,393,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[5,30,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[40,105,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,21,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,21,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[76,123,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[178,192,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[246,270,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[504,535,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[574,599,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[609,632,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[152,203,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[973,1028,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1722,1747,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[141,176,\"0\"],[56,81,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[198,223,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[260,265,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[341,366,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[400,406,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[439,455,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[60,154,\"0\"],[42,106,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[181,210,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[337,347,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[418,428,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[317,336,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[472,491,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[63,71,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,59,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[107,183,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[193,218,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[59,99,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[431,462,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[480,505,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[42,90,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1125,1146,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,60,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[165,196,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[750,771,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,38,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[31,137,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[141,176,\"0\"],[56,81,\"0\"]],null]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[189,240,\"0\"],[198,240,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[275,280,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[356,381,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[36,62,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[194,198,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[55,64,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[54,134,\"0\"],[66,134,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[244,267,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[280,305,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[9,27,\"0\"],[14,27,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[139,164,\"0\"]],null]]]\n\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[184,286,\"0\"],[189,286,\"0\"],[189,206,\"0\"],[210,286,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[377,399,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[34,66,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[183,192,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,95,\"0\"]],null]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[463,485,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,83,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[174,206,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[305,360,\"0\"],[305,331,\"0\"],[318,331,\"0\"],[335,360,\"0\"],[347,360,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,24,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[580,594,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[652,678,\"0\"],[652,665,\"0\"],[669,678,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[726,768,\"0\"],[726,739,\"0\"],[743,768,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[802,809,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[82,87,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[154,179,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[384,403,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,24,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[135,173,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,42,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[24,78,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[90,120,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[306,316,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[527,537,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,10,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[104,143,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[433,487,\"0\"]],null]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[13,130,\"0\"],[13,34,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,46,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[9,37,\"0\"],[9,22,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[193,206,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,21,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[5,46,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[39,69,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[93,130,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[23,54,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,37,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[123,151,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[-1,32,\"0\"]],null]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[551,560,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[656,665,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[681,690,\"0\"]],null]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[999,1030,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[39,61,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[157,211,\"0\"]],null]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1432,1440,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[30,99,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[53,69,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[327,370,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[29,65,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[99,137,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[146,168,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[135,170,\"0\"],[53,78,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[183,188,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[258,283,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[330,342,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[84,157,\"0\"],[42,84,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[194,218,\"0\"],[251,274,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[317,330,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,41,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,55,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[152,325,\"1/2\"],[179,214,\"1/2\"],[268,323,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[401,475,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[700,703,\"1\"]],null]]]\n[2,null,[[0,2]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[44,62,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[182,201,\"1/2\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[24,42,\"1\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[93,111,\"1\"]],null]]]\n\n[2,null,[[0,2]]]\n[2,null,[[0,2]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1276,1297,\"1/2\"]],null]]]\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[88,137,\"1/2\"],[105,137,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[66,190,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,33,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[16,47,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[116,121,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[277,299,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[20,41,\"0\"]],null]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[387,439,\"0\"],[416,439,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[560,607,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,34,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,28,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[16,47,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[116,121,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[277,299,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[20,42,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[389,409,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[488,535,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,35,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[54,87,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[138,170,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[389,411,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[458,501,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[702,711,\"0\"],[725,766,\"0\"]],null]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[924,946,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[1224,1233,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[118,189,\"0\"],[118,139,\"0\"],[143,189,\"0\"]],null]]]\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[145,168,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[2265,2315,\"0\"]],null]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[349,365,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[391,408,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[1006,1050,\"0\"],[1006,1024,\"0\"],[1028,1050,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[1216,1226,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[1377,1387,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[1497,1507,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[93,110,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,34,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[60,73,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[278,300,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[468,490,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[532,577,\"0\"]],null]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[36,105,\"1/2\"],[56,105,\"0\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[38,60,\"1/2\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[18,43,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[91,111,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[168,188,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[448,500,\"0\"]],null]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[663,669,\"0\"],[695,700,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[759,765,\"0\"],[768,829,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[875,881,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,33,\"0\"]],null]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[45,60,\"0\"]],null]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[102,161,\"0\"],[102,156,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[233,261,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[192,230,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[327,396,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[135,175,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[276,318,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[81,184,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,19,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[54,75,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[27,75,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[29,30,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[67,72,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[269,270,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[58,78,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[7,32,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[82,91,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[94,129,\"1/2\"],[94,119,\"1/2\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[141,177,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[196,201,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[275,300,\"1/2\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[193,206,\"1/2\"]],null]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[133,150,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[585,620,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,48,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[66,191,\"0\"]],null]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[27,67,\"0\"]],null]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[94,129,\"0\"],[94,119,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[141,177,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[196,201,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[275,300,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[70,98,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[219,244,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,126,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[200,235,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[49,87,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[108,135,\"0\"]],null]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[153,289,\"0\"],[153,280,\"0\"]],null]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[345,378,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[418,440,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[14,83,\"0\"],[14,63,\"0\"]],null]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[542,580,\"0\"],[558,580,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[677,680,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[7,177,\"0\"],[7,33,\"0\"]],null]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[262,300,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[96,131,\"0\"],[96,121,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[143,148,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[226,251,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[141,150,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[54,104,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[290,327,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[624,688,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[71,201,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[345,355,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[516,555,\"0\"]],null]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[753,775,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1011,1039,\"0\"]],null]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[77,82,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[139,164,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[149,191,\"0\"],[149,166,\"0\"],[170,191,\"0\"],[200,221,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[247,271,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[11,28,\"0\"],[38,77,\"0\"],[38,76,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[111,167,\"0\"],[111,166,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[493,513,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[539,551,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[604,616,\"0\"]],null]]]\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[650,693,\"0\"],[650,667,\"0\"],[671,693,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[716,801,\"0\"],[716,736,\"0\"],[741,800,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,26,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,38,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[298,323,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[375,405,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[459,492,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[625,646,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,29,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[94,99,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[218,240,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[296,313,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1164,1181,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[92,127,\"0\"],[92,117,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[139,144,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[214,239,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[76,93,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[105,130,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[181,206,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n", + "\n{\"present_sessions\":[0]}\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[19,38,\"1/2\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[171,212,\"0\"],[171,193,\"0\"],[197,212,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[278,294,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[485,500,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[629,644,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[101,118,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[2,null,[[0,2]]]\n[2,null,[[0,2]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[44,84,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[5,null,[[0,5]]]\n[5,null,[[0,5]]]\n[5,null,[[0,5]]]\n[15,null,[[0,15]]]\n[\"1\",\"b\",[[0,\"1\",null,[[63,94,\"1\"]],null]]]\n[3,null,[[0,3]]]\n\n\n[5,null,[[0,5]]]\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[46,78,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[272,285,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,73,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,84,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[43,56,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[239,312,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[400,467,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[31,52,\"1/2\"]],null]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[256,281,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[301,327,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[352,383,\"1/2\"]],null]]]\n\n[\"2/3\",\"b\",[[0,\"2/3\",null,[[402,469,\"1/2\"],[402,430,\"1/2\"],[434,469,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0/6\",\"b\",[[0,\"0/6\",null,[[12,105,\"0\"],[19,105,\"0\"],[19,47,\"0\"],[51,105,\"0\"],[51,77,\"0\"],[81,105,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,43,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1\",\"b\",[[0,\"1\",null,[[23,39,\"1\"]],null]]]\n[15,null,[[0,15]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[68,98,\"1/2\"]],null]]]\n[15,null,[[0,15]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[152,173,\"1/2\"]],null]]]\n[15,null,[[0,15]]]\n\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[11,21,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[86,97,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,27,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[49,63,\"1/2\"]],null]]]\n\n[4,null,[[0,4]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1896,1957,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[8,null,[[0,8]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[64,81,\"1/2\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,29,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,28,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[8,null,[[0,8]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[8,21,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[105,115,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,26,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,30,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[39,58,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[22,null,[[0,22]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,29,\"1/2\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[10,31,\"1\"]],null]]]\n[4,null,[[0,4]]]\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[74,120,\"1/2\"],[76,119,\"1/2\"],[89,119,\"1/2\"]],null]]]\n[28,null,[[0,28]]]\n\n\n\n\n[22,null,[[0,22]]]\n\n\n[1,null,[[0,1]]]\n[\"1\",\"b\",[[0,\"1\",null,[[8,34,\"1\"]],null]]]\n[25,null,[[0,25]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[114,126,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[52,null,[[0,52]]]\n\n\n[52,null,[[0,52]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[23,33,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[20,null,[[0,20]]]\n[20,null,[[0,20]]]\n[20,null,[[0,20]]]\n[20,null,[[0,20]]]\n[20,null,[[0,20]]]\n[52,null,[[0,52]]]\n\n[52,null,[[0,52]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[573,642,\"0\"],[573,599,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,35,\"0\"]],null]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[193,265,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[140,205,\"0\"],[140,164,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[246,311,\"0\"],[246,270,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[355,362,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[139,170,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[322,359,\"0\"],[332,359,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[397,434,\"0\"],[407,434,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0/7\",\"b\",[[0,\"0/7\",null,[[581,677,\"0\"],[581,627,\"0\"],[581,596,\"0\"],[600,627,\"0\"],[631,677,\"0\"],[631,646,\"0\"],[650,677,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,29,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[13,123,\"0\"],[13,34,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[61,144,\"0\"],[61,78,\"0\"],[82,144,\"0\"],[82,111,\"0\"],[115,144,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[80,96,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[269,276,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[39,57,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[20,39,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[60,80,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[93,115,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[13,52,\"0\"],[13,26,\"0\"],[30,52,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[387,419,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[54,71,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,34,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[150,158,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[54,71,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[12,50,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[172,180,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[153,188,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[39,57,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[20,39,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[60,80,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[93,115,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[13,52,\"0\"],[13,26,\"0\"],[30,52,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[379,411,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1185,1241,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[42,89,\"0\"],[42,64,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[65,85,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[164,328,\"0\"],[164,190,\"0\"],[164,171,\"0\"]],null]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[11675,11720,\"1/2\"],[11685,11720,\"1/2\"]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[11794,11841,\"1/2\"],[11804,11841,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[101,109,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[165,169,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[62,105,\"0\"],[79,105,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[172,175,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[14,25,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[129,143,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[230,257,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[314,343,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[387,412,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[538,561,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[26,38,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[80,106,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[153,170,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[214,229,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[271,290,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[336,425,\"0\"],[336,365,\"0\"],[369,425,\"0\"],[369,395,\"0\"],[399,425,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[469,487,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[533,545,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[587,599,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[642,689,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[14,40,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[10,24,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[14,23,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[14,23,\"0\"]],null]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[10,25,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[94,103,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[213,223,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[20,26,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[20,26,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[149,153,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[126,169,\"0\"],[143,169,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[53,89,\"0\"],[67,89,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[641,656,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[252,257,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[40,47,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[222,246,\"0\"],[229,246,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[751,795,\"0\"],[751,771,\"0\"],[775,795,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[53,55,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[126,129,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[7,null,[[0,7]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,45,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[8,null,[[0,8]]]\n\n\n[7,null,[[0,7]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[528,533,\"1\"]],null]]]\n[7,null,[[0,7]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[91,140,\"1/2\"]],null]]]\n[7,null,[[0,7]]]\n\n\n[7,null,[[0,7]]]\n\n\n\n[1,null,[[0,1]]]\n[21,null,[[0,21]]]\n\n[21,null,[[0,21]]]\n[\"1\",\"b\",[[0,\"1\",null,[[101,106,\"1\"]],null]]]\n[25,null,[[0,25]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[20686,20715,\"1/2\"]],null]]]\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[13,33,\"1/2\"]],null]]]\n\n[5,null,[[0,5]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[83,95,\"1/2\"]],null]]]\n[5,null,[[0,5]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[42,67,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[105,113,\"1\"]],null]]]\n[4,null,[[0,4]]]\n[\"1\",\"b\",[[0,\"1\",null,[[42,58,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[11,44,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[8,null,[[0,8]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[182,196,\"1/2\"]],null]]]\n[4,null,[[0,4]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[5,null,[[0,5]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[173,185,\"1/2\"]],null]]]\n[5,null,[[0,5]]]\n[5,null,[[0,5]]]\n\n[5,null,[[0,5]]]\n\n\n\n[5,null,[[0,5]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[309,321,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,52,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[581,594,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[4,null,[[0,4]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[4,null,[[0,4]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[62,93,\"1\"],[72,93,\"1\"]],null]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[4,null,[[0,4]]]\n\n\n[1,null,[[0,1]]]\n\n[4,null,[[0,4]]]\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[165,171,\"1\"]],null]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[4,null,[[0,4]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[437,459,\"1/2\"]],null]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[914,931,\"1\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[90,107,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[100,114,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[272,287,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n\n\n\n\n[4,null,[[0,4]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,31,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,35,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[153,228,\"0\"],[153,173,\"0\"],[177,228,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[291,308,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[75,97,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[16,null,[[0,16]]]\n\n[16,null,[[0,16]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,29,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[96,180,\"1/2\"],[122,180,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[395,405,\"1/2\"]],null]]]\n[16,null,[[0,16]]]\n\n[16,null,[[0,16]]]\n\n\n\n[4,null,[[0,4]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[36,83,\"1/2\"],[61,83,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"2/3\",\"b\",[[0,\"2/3\",null,[[220,286,\"1/2\"],[220,242,\"1/2\"],[246,286,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[431,480,\"1/2\"],[431,453,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[4,null,[[0,4]]]\n\n\n\n\n\n\n\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[974,1000,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,36,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[4,null,[[0,4]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1198,1205,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,13,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[4,null,[[0,4]]]\n\n\n\n[4,null,[[0,4]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[1795,1836,\"1\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n[4,null,[[0,4]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,33,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[4,null,[[0,4]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[76,89,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[4,null,[[0,4]]]\n\n\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[563,660,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[743,778,\"1/2\"]],null]]]\n\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n\n\n\n\n\n[9,null,[[0,9]]]\n\n\n\n\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[434,452,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[49,55,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[9,null,[[0,9]]]\n\n[9,null,[[0,9]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[9,32,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[382,388,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[4,null,[[0,4]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[77,92,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[36,67,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[41,47,\"0\"],[125,156,\"0\"],[125,147,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[4,null,[[0,4]]]\n\n\n\n\n[4,null,[[0,4]]]\n[\"0\",\"b\",[[0,\"0\",null,[[27,42,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[92,117,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[157,180,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[4,null,[[0,4]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[13,137,\"0\"],[13,63,\"0\"],[67,137,\"0\"]],null]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[807,847,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[884,965,\"1/2\"],[903,965,\"1/2\"],[903,929,\"1/2\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n[\"3/4\",\"b\",[[0,\"3/4\",null,[[1004,1088,\"1/2\"],[1021,1088,\"1/2\"],[1021,1045,\"1/2\"],[1049,1088,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1127,1171,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1209,1216,\"1/2\"]],null]]]\n[4,null,[[0,4]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1256,1267,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[95,112,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[94,118,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[128,136,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[242,273,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[8,23,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[6,null,[[0,6]]]\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[121,128,\"1\"]],null]]]\n[272,null,[[0,272]]]\n[272,null,[[0,272]]]\n\n\n\n\n[6,null,[[0,6]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[447,461,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[6,null,[[0,6]]]\n\n\n[1,null,[[0,1]]]\n[44,null,[[0,44]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[48,80,\"1\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[12,27,\"1\"]],null]]]\n[36,null,[[0,36]]]\n\n[20,null,[[0,20]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[181,189,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[263,267,\"1/2\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,21,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[24,null,[[0,24]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[612,653,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[125,163,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[140,150,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[5,null,[[0,5]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[35,51,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,36,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[82,112,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[4,null,[[0,4]]]\n\n\n\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[145,166,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[4,null,[[0,4]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[308,333,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[8,15,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n\n\n\n\n[4,null,[[0,4]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[8,15,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[25,32,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[134,174,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,17,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[56,77,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[259,277,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[367,385,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[616,634,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,23,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,28,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[63,77,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[4,null,[[0,4]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[172,186,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[4,null,[[0,4]]]\n\n\n[1,null,[[0,1]]]\n[4,null,[[0,4]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[33,61,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[10,32,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[100,138,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,17,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[185,204,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[189,211,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n[9,null,[[0,9]]]\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[557,569,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"2/3\",\"b\",[[0,\"2/3\",null,[[678,739,\"1/2\"],[678,708,\"1/2\"],[712,739,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[934,961,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[9,null,[[0,9]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[10,18,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,23,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[10,18,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,24,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[60,78,\"1/2\"]],null]]]\n\n[9,null,[[0,9]]]\n\n\n\n\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[60,78,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[202,235,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,26,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[727,733,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[107,116,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[216,241,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[380,437,\"0\"],[380,407,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[520,545,\"0\"]],null]]]\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[17,127,\"0\"],[59,127,\"0\"],[59,88,\"0\"],[92,127,\"0\"]],null]]]\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[780,845,\"0\"],[780,807,\"0\"],[811,845,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[54,101,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[16,26,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[80,93,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[136,151,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[206,210,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[248,255,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,25,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[116,152,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n[\"2/3\",\"b\",[[0,\"2/3\",null,[[304,361,\"1/2\"],[304,333,\"1/2\"],[337,361,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[572,613,\"1/2\"],[572,602,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[657,699,\"1/2\"],[657,685,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[8,25,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[30,43,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[218,230,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,24,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[11,44,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[50229,50285,\"1/2\"],[50249,50285,\"1/2\"]],null]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n[2,null,[[0,2]]]\n[2,null,[[0,2]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[83,105,\"1/2\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,52,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[2,null,[[0,2]]]\n\n[2,null,[[0,2]]]\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[484,521,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[172,208,\"0\"]],null]]]\n\n\n[2,null,[[0,2]]]\n\n[2,null,[[0,2]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[25,50,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[115,136,\"1/2\"]],null]]]\n[2,null,[[0,2]]]\n\n\n\n\n\n\n\n\n[2,null,[[0,2]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[401,413,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[2,null,[[0,2]]]\n[2,null,[[0,2]]]\n\n[2,null,[[0,2]]]\n[2,null,[[0,2]]]\n\n\n[2,null,[[0,2]]]\n[2,null,[[0,2]]]\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[64,79,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[42,52,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[149,186,\"0\"],[177,186,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[289,305,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[435,474,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[582,600,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[230,241,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[41,57,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[15,26,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[104,122,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[103,118,\"1/2\"]],null]]]\n\n\n[1,null,[[0,1]]]\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[140,209,\"1/2\"],[140,169,\"1/2\"],[173,209,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[371,376,\"1\"]],null]]]\n[2,null,[[0,2]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[87,104,\"1/2\"]],null]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[122,161,\"1\"]],null]]]\n[\"4/4\",\"b\",[[0,\"4/4\",null,[[9,91,\"1/2\"],[32,91,\"1/2\"],[32,54,\"1/2\"],[58,91,\"1/2\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[10,24,\"1\"]],null]]]\n\n\n[16,null,[[0,16]]]\n\n[24,null,[[0,24]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"4/4\",\"b\",[[0,\"4/4\",null,[[519,589,\"1/2\"],[527,589,\"1/2\"],[547,589,\"1/2\"],[571,589,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[407,440,\"1/2\"],[418,440,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[34,40,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[37,69,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[8,44,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[8,null,[[0,8]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[9,73,\"1/2\"],[9,38,\"1/2\"],[42,73,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[55,85,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[177,184,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9,31,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[4,null,[[0,4]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[123,130,\"1/2\"]],null]]]\n[4,null,[[0,4]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[70,118,\"1/2\"],[70,99,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[127,136,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[313,337,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[375,397,\"1/2\"]],null]]]\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[672,695,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[849,863,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[89,109,\"1\"]],null]]]\n\n\n[3,null,[[0,3]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[131,157,\"1/2\"]],null]]]\n[3,null,[[0,3]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[204,238,\"1/2\"]],null]]]\n[3,null,[[0,3]]]\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[176,186,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,19,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[164,228,\"0\"],[164,182,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[240,257,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[8,10,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[38,null,[[0,38]]]\n\n\n[38,null,[[0,38]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[72,121,\"1/2\"],[72,101,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[22,null,[[0,22]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[2,null,[[0,2]]]\n[\"1\",\"b\",[[0,\"1\",null,[[38,41,\"1\"]],null]]]\n[3,null,[[0,3]]]\n\n\n\n[1,null,[[0,1]]]\n[\"1\",\"b\",[[0,\"1\",null,[[13,71,\"1\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[\"1\",\"b\",[[0,\"1\",null,[[9,30,\"1\"]],null]]]\n[\"1\",\"b\",[[0,\"1\",null,[[25,39,\"1\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[9,71,\"1\"],[18,71,\"1/2\"],[18,46,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n[2,null,[[0,2]]]\n\n\n\n[1,null,[[0,1]]]\n[2,null,[[0,2]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[99,133,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[239,269,\"1/2\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[\"1\",\"b\",[[0,\"1\",null,[[12,74,\"1\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[63,74,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[32,55,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[187,207,\"1\"]],null]]]\n\n\n[3,null,[[0,3]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[122,145,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[3,null,[[0,3]]]\n[3,null,[[0,3]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[306,349,\"1/2\"],[320,349,\"0\"]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[188,197,\"1/2\"],[250,264,\"1/2\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[258,286,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[20,40,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[107,138,\"0\"],[142,185,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,38,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[70,90,\"0\"],[94,137,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[883,911,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[157,181,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[14,67,\"0\"]],null]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[14,27,\"0\"],[30,56,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[479,542,\"0\"],[479,506,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[69,83,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[126,131,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[38,52,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[5,null,[[0,5]]]\n\n\n\n\n\n[5,null,[[0,5]]]\n\n[5,null,[[0,5]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[58,111,\"1/2\"],[86,111,\"1/2\"]],null]]]\n\n\n[4,null,[[0,4]]]\n[\"1\",\"b\",[[0,\"1\",null,[[139,158,\"1\"]],null]]]\n[4,null,[[0,4]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[59,79,\"1/2\"]],null]]]\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n\n\n\n[5,null,[[0,5]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[200,222,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,32,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[329,348,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[65,90,\"1\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[10,39,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[17,73,\"1/2\"]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[122,129,\"1/2\"],[229,236,\"1/2\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[995,1017,\"1/2\"],[1069,1091,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,43,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[69,101,\"0\"],[69,85,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[12,43,\"0\"],[12,28,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[146,166,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[38,108,\"0\"],[53,108,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[277,321,\"1/2\"],[297,321,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[438,460,\"1\"]],null]]]\n[2,null,[[0,2]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[40,44,\"1/2\"]],null]]]\n[2,null,[[0,2]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[128,172,\"0\"],[128,152,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[274,319,\"1/2\"],[283,319,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[331,343,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[937,942,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[990,1052,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1127,1189,\"1/2\"]],null]]]\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[60,67,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[47,53,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[47,53,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[104,110,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[173,178,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[618,626,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[47,76,\"1/2\"],[58,76,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[53,62,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[39,44,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[75,80,\"1\"]],null]]]\n[2,null,[[0,2]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[76,81,\"1\"]],null]]]\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n\n\n\n\n[1,null,[[0,1]]]\n[4,null,[[0,4]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[121,127,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[\"1\",\"b\",[[0,\"1\",null,[[90,116,\"1\"]],null]]]\n[2,null,[[0,2]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[47,61,\"1/2\"]],null]]]\n[2,null,[[0,2]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[455,481,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[495,530,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[96,121,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[43,96,\"0\"],[43,64,\"0\"],[68,96,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[1056,1114,\"1/2\"],[1067,1113,\"1/2\"],[1083,1113,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[25,39,\"1/2\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1203,1214,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1287,1292,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1365,1403,\"1/2\"]],null]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[162,176,\"1/2\"]],null]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1726,1761,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[8,null,[[0,8]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[33,39,\"1/2\"]],null]]]\n[8,null,[[0,8]]]\n\n\n[8,null,[[0,8]]]\n\n[8,null,[[0,8]]]\n\n\n[1,null,[[0,1]]]\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[102,111,\"1/2\"]],null]]]\n[4,null,[[0,4]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[4,null,[[0,4]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[329,336,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[12,59,\"1/2\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[88,91,\"1/2\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[9,null,[[0,9]]]\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[201,210,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[248,315,\"1/2\"],[280,294,\"0\"]],null]]]\n[9,null,[[0,9]]]\n[9,null,[[0,9]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[693,744,\"1/2\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,26,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[350,369,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[186,252,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[63,143,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[489,497,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[994,1081,\"0\"],[994,1035,\"0\"],[1039,1081,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1649,1663,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2669,2702,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[9,null,[[0,9]]]\n\n[9,null,[[0,9]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2988,3002,\"1/2\"]],null]]]\n[9,null,[[0,9]]]\n[9,null,[[0,9]]]\n\n\n[1,null,[[0,1]]]\n[4,null,[[0,4]]]\n\n\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[200,206,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[409,418,\"1/2\"]],null]]]\n\n\n[4,null,[[0,4]]]\n[\"0\",\"b\",[[0,\"0\",null,[[520,535,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,23,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[4,null,[[0,4]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[853,856,\"1/2\"]],null]]]\n\n[4,null,[[0,4]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1056,1071,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[4,null,[[0,4]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[135,138,\"1/2\"]],null]]]\n\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1710,1724,\"1/2\"]],null]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n[4,null,[[0,4]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[177,186,\"1/2\"]],null]]]\n[4,null,[[0,4]]]\n\n[4,null,[[0,4]]]\n[0,null,[[0,0]]]\n\n[4,null,[[0,4]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"1/3\",\"b\",[[0,\"1/3\",null,[[14,51,\"1/2\"],[19,51,\"0\"],[32,51,\"0\"]],null]]]\n\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[25725,25776,\"1/2\"],[25739,25776,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[216,248,\"0\"],[216,230,\"0\"],[234,248,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[352,367,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,15,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[463,499,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1250,1262,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[1328,1340,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[785,807,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[35,67,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[11,63,\"0\"],[11,39,\"0\"],[44,62,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[12,45,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n[\"0/8\",\"b\",[[0,\"0/8\",null,[[429,557,\"0\"],[446,556,\"0\"],[446,484,\"0\"],[456,484,\"0\"],[466,484,\"0\"],[488,556,\"0\"],[488,511,\"0\"],[515,556,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[337,353,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[84,104,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[2661,2668,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[73,89,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[110,165,\"0\"],[120,165,\"0\"],[130,165,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[421,444,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,97,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[108,193,\"0\"],[118,193,\"0\"],[128,193,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[447,470,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,153,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[112,118,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[212,218,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[322,349,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[383,410,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[473,481,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[267,294,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[870,892,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[1174,1176,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[1640,1694,\"0\"],[1654,1694,\"0\"],[1654,1672,\"0\"],[1676,1694,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[108,135,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[169,196,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[222,283,\"0\"],[222,241,\"0\"],[245,283,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[309,351,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[197,244,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[985,1025,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[2527,2539,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[2577,2581,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[2616,2620,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[55,84,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[2819,2846,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[759,781,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[95,131,\"0\"],[95,111,\"0\"],[115,131,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[307,313,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[566,577,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[840,855,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[1032,1040,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[52,83,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[175,190,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[42,102,\"0\"],[42,51,\"0\"],[55,102,\"0\"],[55,63,\"0\"],[67,102,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[226,303,\"0\"],[226,242,\"0\"],[246,303,\"0\"],[246,262,\"0\"],[266,303,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[376,392,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[476,492,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[579,584,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[52,110,\"0\"],[52,65,\"0\"],[69,110,\"0\"],[69,87,\"0\"],[91,110,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[109,117,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1226,1241,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[42,102,\"0\"],[42,51,\"0\"],[55,102,\"0\"],[55,63,\"0\"],[67,102,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[226,339,\"0\"],[226,242,\"0\"],[246,339,\"0\"],[246,262,\"0\"],[266,339,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[412,428,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[511,527,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[611,617,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[52,110,\"0\"],[52,65,\"0\"],[69,110,\"0\"],[69,87,\"0\"],[91,110,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[164,172,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[771,793,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[10,42,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[46,79,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[323,479,\"0\"],[339,479,\"0\"],[339,407,\"0\"],[411,479,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[424,444,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[2354,2361,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[2755,2777,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[10,84,\"0\"],[10,47,\"0\"],[51,84,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[224,256,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[12,91,\"0\"],[12,49,\"0\"],[53,91,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[12,91,\"0\"],[12,49,\"0\"],[53,91,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[240,278,\"0\"],[240,257,\"0\"],[261,278,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[347,372,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[449,474,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[630,645,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[897,901,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[100,112,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[173,255,\"0\"],[173,184,\"0\"],[188,255,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[456,481,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[58,72,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[213,299,\"0\"],[213,236,\"0\"],[271,299,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[44,60,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[60,76,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[294,316,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[95,124,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[13,51,\"0\"],[13,30,\"0\"],[34,51,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[130,148,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[15,122,\"0\"],[15,54,\"0\"],[58,122,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[776,794,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1221,1238,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[1362,1379,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[1707,1718,\"0\"],[1731,1742,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[1903,1956,\"0\"],[1903,1916,\"0\"],[1920,1956,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[2872,2905,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[3291,3317,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[10,86,\"0\"],[10,46,\"0\"],[50,86,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[200,234,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[588,668,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[4480,4487,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[23,65,\"1/2\"],[23,51,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[168,195,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1\",\"b\",[[0,\"1\",null,[[23,38,\"1\"]],null]]]\n[1,null,[[0,1]]]\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[109,155,\"1\"],[109,135,\"1\"],[139,155,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[225,249,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[\"4/4\",\"b\",[[0,\"4/4\",null,[[-1,212,\"1/2\"],[-1,82,\"1/2\"],[-1,27,\"1/2\"],[31,82,\"1/2\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[45,109,\"1/2\"],[45,76,\"1/2\"]],null]]]\n\n[1,null,[[0,1]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[226,268,\"0\"],[226,251,\"0\"],[255,268,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,45,\"0\"]],null]]]\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[76,187,\"0\"],[76,102,\"0\"],[106,187,\"0\"],[106,139,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[239,273,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[13,53,\"0\"],[24,53,\"0\"],[24,45,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,52,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[16,100,\"0\"],[16,50,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[16,54,\"0\"],[16,27,\"0\"],[31,54,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[13,58,\"0\"],[13,26,\"0\"],[31,57,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[20,69,\"0\"],[20,30,\"0\"],[34,69,\"0\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[18,315,\"0\"],[18,43,\"0\"],[47,315,\"0\"],[47,61,\"0\"]],null]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[45,63,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,39,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[138,317,\"0\"],[138,151,\"0\"]],null]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[469,503,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[604,622,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[21,78,\"0\"],[33,78,\"0\"],[45,78,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[790,838,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,56,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[1103,1141,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,48,\"0\"]],null]]]\n\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[21,114,\"0\"],[21,36,\"0\"],[40,114,\"0\"],[40,57,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1689,1694,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[53,86,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[572,581,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[642,651,\"0\"]],null]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[-1,12,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[-1,12,\"0\"]],null]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1313,1342,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[1483,1509,\"0\"],[1483,1496,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[1637,1663,\"0\"],[1637,1650,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1866,1888,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[2129,2175,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,32,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[113,118,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,43,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[48,53,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,56,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[13,58,\"0\"],[24,58,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[126,155,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[20,44,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[227,254,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[72,110,\"0\"],[72,98,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[131,210,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[492,519,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[20,38,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[585,613,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[20,38,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[679,710,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[20,50,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[788,804,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[20,35,\"0\"]],null]]]\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[867,928,\"0\"],[867,895,\"0\"],[899,928,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[1006,1044,\"0\"],[1017,1044,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,35,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[63,160,\"0\"],[63,91,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[257,319,\"0\"],[257,279,\"0\"],[283,319,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,48,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[462,523,\"0\"],[462,491,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n[\"4/4\",\"b\",[[0,\"4/4\",null,[[-1,248,\"1/2\"],[-1,100,\"1/2\"],[-1,27,\"1/2\"],[31,100,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[229,258,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[351,382,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[515,544,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,18,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[47,63,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[106,115,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[224,252,\"0\"],[224,245,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[33,61,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[87,162,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[224,229,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[17,72,\"0\"],[17,48,\"0\"],[52,72,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[127,169,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,39,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[117,122,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,38,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,39,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[62,91,\"0\"],[62,85,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[112,130,\"0\"]],null]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[227,286,\"0\"],[227,255,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[360,375,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[390,419,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[459,518,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[604,611,\"0\"],[630,653,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[686,710,\"0\"]],null]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[759,861,\"0\"],[759,796,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[979,984,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,52,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,36,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[125,153,\"0\"],[125,152,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[254,259,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[349,365,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,36,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[87,98,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[240,268,\"0\"],[240,267,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[379,384,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[84,110,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[268,283,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[863,867,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[947,952,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[986,1007,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[1102,1122,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[172,177,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[158,209,\"0\"],[158,188,\"0\"],[192,209,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,26,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[538,554,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[622,641,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[2,null,[[0,2]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n[\"9/19\",\"b\",[[0,\"9/19\",null,[[20049,20101,\"1/2\"],[20049,20073,\"1/2\"],[20075,20101,\"0\"],[20129,20166,\"1/2\"],[20129,20154,\"1/2\"],[20191,20217,\"0\"],[20227,20253,\"0\"],[20263,20297,\"0\"],[20263,20287,\"0\"],[36,41,\"1\"],[46,51,\"1/2\"],[59,94,\"0\"],[59,85,\"0\"],[98,103,\"0\"],[122,123,\"0\"],[304,305,\"0\"],[359,394,\"1/2\"],[359,385,\"1/2\"],[407,417,\"1\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[498,522,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[932,965,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,17,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[172,213,\"0\"],[172,177,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[317,320,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[65,77,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,19,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[47,82,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[124,149,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[16,67,\"0\"],[25,67,\"0\"],[25,45,\"0\"],[49,67,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,44,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[221,231,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[451,466,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,37,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[137,150,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[305,346,\"0\"],[321,337,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[403,424,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[54,73,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[122,141,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[258,285,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[334,361,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[488,513,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[562,587,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[696,707,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[756,767,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,42,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[81,144,\"0\"],[95,143,\"0\"],[105,143,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[37,71,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,49,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[466,489,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,26,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[612,621,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,28,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[84,121,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,21,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[216,229,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[403,430,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[491,527,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[133,159,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[98,103,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[57,86,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[172,189,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,55,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[9971,10191,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,40,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[18,40,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[330,357,\"0\"],[339,357,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,43,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[211,231,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[42,61,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[1272,1286,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,52,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[44,70,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[176,183,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[380,392,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[407,433,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[536,541,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[97,177,\"1/2\"],[97,126,\"1/2\"],[130,177,\"0\"],[148,177,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[195,271,\"1/2\"],[195,223,\"1/2\"],[227,271,\"0\"],[227,257,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1018,1023,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1077,1083,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[20,84,\"1/2\"],[20,51,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[57,79,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[16,66,\"0\"],[23,66,\"0\"],[30,66,\"0\"],[30,48,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[\"2/5\",\"b\",[[0,\"2/5\",null,[[16,96,\"1/2\"],[16,41,\"1/2\"],[48,95,\"0\"],[55,95,\"0\"],[74,95,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[16,53,\"0\"],[16,39,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[2,null,[[0,2]]]\n[\"1\",\"b\",[[0,\"1\",null,[[17,43,\"1\"]],null]]]\n[3,null,[[0,3]]]\n\n\n\n\n[1,null,[[0,1]]]\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[16,99,\"0\"],[16,41,\"0\"],[45,99,\"0\"],[45,78,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[17,24,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"2/3\",\"b\",[[0,\"2/3\",null,[[130,188,\"1/2\"],[130,158,\"1/2\"],[162,188,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[49,75,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[280,332,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[470,496,\"1/2\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[32,53,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[722,727,\"1/2\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,66,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1829,1850,\"1/2\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1935,1948,\"1/2\"]],null]]]\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[35,64,\"1/2\"],[35,63,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[201,219,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[424,484,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[631,636,\"1/2\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,74,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"1\",\"b\",[[0,\"1\",null,[[969,985,\"1\"]],null]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[\"1\",\"b\",[[0,\"1\",null,[[1226,1242,\"1\"]],null]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[1566,1625,\"1/2\"],[1566,1594,\"1/2\"],[1598,1625,\"1/2\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[225,231,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[615,628,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[879,906,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[5300,5314,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[19,null,[[0,19]]]\n[19,null,[[0,19]]]\n[19,null,[[0,19]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,56,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[125,171,\"0\"],[125,146,\"0\"],[150,171,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[24,67,\"0\"],[24,56,\"0\"],[60,67,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[274,302,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[24,31,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[369,376,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0/7\",\"b\",[[0,\"0/7\",null,[[441,496,\"0\"],[441,465,\"0\"],[441,451,\"0\"],[455,465,\"0\"],[471,495,\"0\"],[471,481,\"0\"],[485,495,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[561,603,\"0\"]],null]]]\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[25,162,\"0\"],[25,46,\"0\"],[52,162,\"0\"],[52,73,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[862,907,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[972,999,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[24,51,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[1167,1220,\"0\"],[1167,1195,\"0\"],[1199,1220,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,41,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[60,72,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[161,189,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,41,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1788,1807,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[24,53,\"1/2\"]],null]]]\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[359,364,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[100,121,\"1/2\"]],null]]]\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,47,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[122,125,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[29,53,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[482,514,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[17,36,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[21,42,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[81,154,\"1\"]],null]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[51,63,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,50,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[36,63,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[20,169,\"0\"],[20,41,\"0\"],[20,31,\"0\"]],null]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[47,52,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,77,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[156,183,\"0\"],[156,177,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[211,238,\"0\"],[211,232,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[264,273,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,50,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[17,62,\"0\"],[17,32,\"0\"],[36,62,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,51,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[275,295,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[13204,13284,\"1/2\"],[13204,13233,\"1/2\"],[13237,13284,\"0\"],[13255,13284,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[13302,13378,\"1/2\"],[13302,13330,\"1/2\"],[13334,13378,\"0\"],[13334,13364,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[13472,13477,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[13546,13552,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[13646,13657,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n[11,null,[[0,11]]]\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[21,45,\"1/2\"]],null]]]\n[10,null,[[0,10]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1188,1220,\"1/2\"]],null]]]\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[10,null,[[0,10]]]\n[10,null,[[0,10]]]\n\n[\"1\",\"b\",[[0,\"1\",null,[[127,145,\"1\"]],null]]]\n[12,null,[[0,12]]]\n\n[12,null,[[0,12]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[25,52,\"1/2\"]],null]]]\n[155,null,[[0,155]]]\n\n\n\n\n\n[\"1/3\",\"b\",[[0,\"1/3\",null,[[457,547,\"1/2\"],[475,547,\"0\"],[512,547,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[10,null,[[0,10]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[2884,2964,\"1/2\"],[2884,2913,\"1/2\"],[2917,2964,\"0\"],[2935,2964,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[2982,3058,\"1/2\"],[2982,3010,\"1/2\"],[3014,3058,\"0\"],[3014,3044,\"0\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[3069,3074,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[3143,3149,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[3243,3254,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[36,46,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[637,717,\"1/2\"],[637,666,\"1/2\"],[670,717,\"0\"],[688,717,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[735,811,\"1/2\"],[735,763,\"1/2\"],[767,811,\"0\"],[767,797,\"0\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[822,827,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[896,902,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[996,1007,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[17,31,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[90,109,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[20,null,[[0,20]]]\n[20,null,[[0,20]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[610,690,\"1/2\"],[610,639,\"1/2\"],[643,690,\"0\"],[661,690,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[708,784,\"1/2\"],[708,736,\"1/2\"],[740,784,\"0\"],[740,770,\"0\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[795,800,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[869,875,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[969,980,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[8,null,[[0,8]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[63,78,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[17,56,\"0\"],[17,32,\"0\"],[36,56,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,52,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[116,130,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[29,43,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[280,310,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[29,51,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[468,494,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[12,null,[[0,12]]]\n[12,null,[[0,12]]]\n[12,null,[[0,12]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[34,72,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[32,65,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,56,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[78,99,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,51,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[142,176,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,51,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[12,null,[[0,12]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[74,81,\"1/2\"]],null]]]\n[12,null,[[0,12]]]\n\n[0,null,[[0,0]]]\n\n[12,null,[[0,12]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2279,2289,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[12,null,[[0,12]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,34,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[119,133,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[24,58,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,34,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[119,133,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[24,58,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[20,59,\"0\"],[20,35,\"0\"],[39,59,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[24,46,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n[8,null,[[0,8]]]\n[8,null,[[0,8]]]\n[\"0\",\"b\",[[0,\"0\",null,[[24,53,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[2,null,[[0,2]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[96,118,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[211,224,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[25,127,\"0\"],[25,45,\"0\"],[49,127,\"0\"],[49,64,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[218,274,\"0\"]],null]]]\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,43,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[123,153,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[7694,7774,\"1/2\"],[7694,7723,\"1/2\"],[7727,7774,\"0\"],[7745,7774,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[7792,7868,\"1/2\"],[7792,7820,\"1/2\"],[7824,7868,\"0\"],[7824,7854,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[8058,8063,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[8132,8138,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[8232,8243,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[50,115,\"0\"],[50,71,\"0\"],[75,115,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[140,166,\"0\"]],null]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[455,459,\"0\"]],null]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[990,1070,\"1/2\"],[990,1019,\"1/2\"],[1023,1070,\"0\"],[1041,1070,\"0\"]],null]]]\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1108,1114,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1278,1286,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[1358,1364,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[1751,1831,\"1/2\"],[1751,1780,\"1/2\"],[1784,1831,\"0\"],[1802,1831,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[1849,1925,\"1/2\"],[1849,1877,\"1/2\"],[1881,1925,\"0\"],[1881,1911,\"0\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1936,1941,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2010,2016,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2110,2121,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[73,84,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,68,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[172,200,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[63,83,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[171,176,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,69,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[63,83,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[171,176,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[124,178,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[24,123,\"0\"],[24,61,\"0\"]],null]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[21,68,\"0\"],[21,49,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[159,216,\"0\"],[159,183,\"0\"],[187,216,\"0\"]],null]]]\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[24,84,\"1/2\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[24,50,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[24,50,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[88,93,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,54,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[88,93,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[25,71,\"0\"],[36,71,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[112,157,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[31,41,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[130,140,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[250,255,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[422,461,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[576,590,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[85,107,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[835,845,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[17,39,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[6,null,[[0,6]]]\n[6,null,[[0,6]]]\n[6,null,[[0,6]]]\n[6,null,[[0,6]]]\n[6,null,[[0,6]]]\n[6,null,[[0,6]]]\n[6,null,[[0,6]]]\n[6,null,[[0,6]]]\n\n[6,null,[[0,6]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[6780,6860,\"1/2\"],[6780,6809,\"1/2\"],[6813,6860,\"0\"],[6831,6860,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[6878,6954,\"1/2\"],[6878,6906,\"1/2\"],[6910,6954,\"0\"],[6910,6940,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[7172,7177,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[7246,7252,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[7346,7357,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[17,58,\"0\"],[30,58,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[136,156,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[243,248,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[113,129,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[17,23,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[122,127,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,51,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[132,152,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[185,205,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[239,259,\"1/2\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[2,null,[[0,2]]]\n[2,null,[[0,2]]]\n[2,null,[[0,2]]]\n[2,null,[[0,2]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[79,90,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[21,34,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[960,970,\"1/2\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[37,58,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[48,74,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[239,249,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[616,629,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[68,76,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[40,57,\"1/2\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[385,444,\"1/2\"],[413,444,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1452,1475,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"3/3\",\"b\",[[0,\"3/3\",null,[[21,49,\"1\"],[21,26,\"1/2\"],[30,49,\"1\"]],null]]]\n[4,null,[[0,4]]]\n\n\n[6,null,[[0,6]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[85,103,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,33,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[110,123,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[202,259,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,50,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[130,199,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[72,82,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[96,101,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[718,738,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,51,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[73,175,\"0\"],[73,100,\"0\"]],null]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[28,67,\"0\"],[39,67,\"0\"]],null]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[150,162,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[88,119,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[227,258,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[22,null,[[0,22]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,33,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,34,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[333,338,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[77,136,\"0\"],[89,105,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[68,76,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[699,725,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[93,98,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[111,134,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[429,445,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[95,100,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[87,92,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[14788,14868,\"1/2\"],[14788,14817,\"1/2\"],[14821,14868,\"0\"],[14839,14868,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[14886,14962,\"1/2\"],[14886,14914,\"1/2\"],[14918,14962,\"0\"],[14918,14948,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[15243,15248,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[15317,15323,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[15417,15428,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[\"2/3\",\"b\",[[0,\"2/3\",null,[[13,82,\"1/2\"],[13,40,\"1/2\"],[44,82,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[146,180,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,38,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[40,53,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[158,164,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[58,72,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[162,195,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[255,289,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[457,462,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[17,70,\"0\"],[33,70,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[145,245,\"0\"],[160,245,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,39,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[47,67,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[486,501,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,55,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[78,104,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[228,250,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[25,259,\"0\"],[25,59,\"0\"]],null]]]\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[64,78,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[152,188,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[271,286,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,44,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,44,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[159,186,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,44,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,44,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[159,186,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,48,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,48,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,44,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[9,null,[[0,9]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[\"2/2\",\"b\",[[0,\"2/2\",null,[[110,202,\"1\"],[142,202,\"1\"]],null]]]\n[9,null,[[0,9]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[10836,10916,\"1/2\"],[10836,10865,\"1/2\"],[10869,10916,\"0\"],[10887,10916,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[10934,11010,\"1/2\"],[10934,10962,\"1/2\"],[10966,11010,\"0\"],[10966,10996,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[11200,11205,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[11274,11280,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[11374,11385,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[47,95,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,29,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[70,135,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[987,992,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[2279,2359,\"1/2\"],[2279,2308,\"1/2\"],[2312,2359,\"0\"],[2330,2359,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[2377,2453,\"1/2\"],[2377,2405,\"1/2\"],[2409,2453,\"0\"],[2409,2439,\"0\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2464,2469,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2538,2544,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2638,2649,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"3/4\",\"b\",[[0,\"3/4\",null,[[17,81,\"1/2\"],[27,81,\"1/2\"],[27,53,\"1/2\"],[57,81,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[240,244,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[21,47,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[31,60,\"1/2\"]],null]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[61,90,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[104,120,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[57,125,\"0\"],[57,83,\"0\"],[87,125,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1095,1137,\"1/2\"],[1106,1137,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"2/3\",\"b\",[[0,\"2/3\",null,[[1216,1277,\"1/2\"],[1216,1247,\"1/2\"],[1251,1277,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[28,40,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,83,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[20,163,\"0\"]],null]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[99,131,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[252,262,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[33,54,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,43,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[24,null,[[0,24]]]\n[\"0\",\"b\",[[0,\"0\",null,[[40,91,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"1\",\"b\",[[0,\"1\",null,[[17,222,\"1\"]],null]]]\n\n\n\n\n[24,null,[[0,24]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[5454,5534,\"1/2\"],[5454,5483,\"1/2\"],[5487,5534,\"0\"],[5505,5534,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[5552,5628,\"1/2\"],[5552,5580,\"1/2\"],[5584,5628,\"0\"],[5584,5614,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[5873,5878,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[5947,5953,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[6047,6058,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[178,185,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,28,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[122,127,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[17,63,\"0\"],[35,62,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[157,162,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,55,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,28,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,28,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[131,149,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[275,301,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,68,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[36,59,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[29,47,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[600,619,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[728,742,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[36,82,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[206,216,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[281,304,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[40,79,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[105,181,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[645,684,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,150,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[1076,1115,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,84,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[37,81,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[1426,1454,\"0\"],[1439,1454,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1566,1589,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,32,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[106,156,\"0\"],[106,129,\"0\"],[133,156,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[60,71,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[388,411,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[31,71,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[92,153,\"0\"],[108,153,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,57,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[125,171,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[29,70,\"0\"]],null]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[92,138,\"0\"],[92,130,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,44,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[159,174,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,44,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[159,174,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,44,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[24,62,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,43,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[227,284,\"0\"],[253,284,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[487,517,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[588,593,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[794,837,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1072,1166,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1449,1454,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[26,76,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[324,376,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,57,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[134,191,\"0\"],[160,191,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[268,298,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[377,387,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[410,453,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[530,624,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[748,753,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,75,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[164,216,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[29,57,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[89,115,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[515,525,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,32,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[14567,14647,\"1/2\"],[14567,14596,\"1/2\"],[14600,14647,\"0\"],[14618,14647,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[14665,14741,\"1/2\"],[14665,14693,\"1/2\"],[14697,14741,\"0\"],[14697,14727,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[15106,15111,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[15180,15186,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[15280,15291,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,34,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[90,95,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,55,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[81,97,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[227,236,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,29,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[79,109,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[29,67,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[667,718,\"0\"],[680,718,\"0\"],[692,718,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[29,67,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[3638,3718,\"1/2\"],[3638,3667,\"1/2\"],[3671,3718,\"0\"],[3689,3718,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[3736,3812,\"1/2\"],[3736,3764,\"1/2\"],[3768,3812,\"0\"],[3768,3798,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[4053,4058,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[4127,4133,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[4227,4238,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[45,73,\"1/2\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[104,134,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[274,282,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[1095,1175,\"1/2\"],[1095,1124,\"1/2\"],[1128,1175,\"0\"],[1146,1175,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[1193,1269,\"1/2\"],[1193,1221,\"1/2\"],[1225,1269,\"0\"],[1225,1255,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1451,1456,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1510,1516,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[135915,135943,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[2,null,[[0,2]]]\n\n\n[1,null,[[0,1]]]\n\n[2,null,[[0,2]]]\n[2,null,[[0,2]]]\n[2,null,[[0,2]]]\n[2,null,[[0,2]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[83,126,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[186,228,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[83,108,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[39,64,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[111,143,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[33,88,\"0\"],[33,82,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[145,150,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,50,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[72,126,\"0\"],[72,120,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[161,181,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,59,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[2854,2934,\"1/2\"],[2854,2883,\"1/2\"],[2887,2934,\"0\"],[2905,2934,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[2952,3028,\"1/2\"],[2952,2980,\"1/2\"],[2984,3028,\"0\"],[2984,3014,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[3150,3155,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[3209,3215,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[270,279,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[352,383,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[1575,1655,\"1/2\"],[1575,1604,\"1/2\"],[1608,1655,\"0\"],[1626,1655,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[1673,1749,\"1/2\"],[1673,1701,\"1/2\"],[1705,1749,\"0\"],[1705,1735,\"0\"]],null]]]\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1760,1765,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1834,1840,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1934,1945,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[17,46,\"0\"]],null]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[141992,142020,\"1/2\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[9,36,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[139,186,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[209,224,\"1/2\"]],null]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[212,261,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,59,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[148,158,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,59,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[146,186,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,41,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[56,69,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[53,67,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/4\",\"b\",[[0,\"0/4\",null,[[152,210,\"0\"],[171,209,\"0\"],[171,188,\"0\"],[192,209,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[997,1006,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,62,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[57,91,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[406,439,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[140,206,\"0\"],[140,189,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[121,141,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[259,285,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[339,349,\"0\"]],null]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,36,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[207,228,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[423,438,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[6628,6708,\"1/2\"],[6628,6657,\"1/2\"],[6661,6708,\"0\"],[6679,6708,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[6726,6802,\"1/2\"],[6726,6754,\"1/2\"],[6758,6802,\"0\"],[6758,6788,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[7064,7069,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[7123,7129,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[27,76,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[90,101,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[193,241,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[255,270,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[512,548,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[580,614,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[643,674,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[706,740,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1104,1155,\"1/2\"]],null]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1184,1236,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[1315,1397,\"1/2\"]],null]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,57,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[106,111,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,42,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,62,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[100,132,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[292,313,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[76,118,\"0\"],[88,118,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[832,838,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[911,960,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,57,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[134,146,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,43,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,24,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[106,111,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[29,54,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,53,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[25,57,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[66,119,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[206,250,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[341,383,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[461,487,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1512,1532,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,65,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,57,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,55,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[13,80,\"0\"],[26,80,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[13,37,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[119,134,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[64,79,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[16,90,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[13,59,\"0\"],[13,32,\"0\"],[36,59,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[108,161,\"0\"],[131,161,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[232,255,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[457,496,\"0\"],[473,496,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[51,62,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[719,746,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,46,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[13,67,\"0\"],[13,36,\"0\"],[40,67,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[62,73,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,53,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[100,126,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[366,404,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[240,246,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[202,247,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[526,569,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[239,256,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[33,45,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[462,478,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[57,111,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[242,269,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,55,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[325,335,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[57,91,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[111,143,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[348,395,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[877,910,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[220,280,\"0\"],[220,263,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,74,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[150,180,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[326,362,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,74,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,127,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[245,301,\"0\"],[245,269,\"0\"],[273,301,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[360,370,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[37,57,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[105,119,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[973,987,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1085,1117,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[1203,1260,\"0\"],[1203,1227,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[31,57,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[193,206,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[246,256,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,37,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,37,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,40,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,41,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[159,183,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[351,372,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[555,575,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[670,694,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[25,89,\"0\"],[25,54,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[24353,24433,\"1/2\"],[24353,24382,\"1/2\"],[24386,24433,\"0\"],[24404,24433,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[24451,24527,\"1/2\"],[24451,24479,\"1/2\"],[24483,24527,\"0\"],[24483,24513,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[24789,24794,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[24863,24869,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[24963,24974,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[46,106,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[176,207,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[479,508,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[64,121,\"0\"],[74,121,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/5\",\"b\",[[0,\"0/5\",null,[[178,256,\"0\"],[186,256,\"0\"],[186,200,\"0\"],[205,255,\"0\"],[205,235,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[274,297,\"0\"]],null]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[52,113,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[199,258,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,56,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[84,119,\"0\"],[90,119,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[112,135,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[288,300,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,92,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[74,99,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[177,217,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,73,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[36,61,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[118,125,\"0\"]],null]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,30,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,36,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[21,75,\"0\"],[21,43,\"0\"],[47,75,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[205,220,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[304,326,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[445,467,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,41,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[153,169,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[266,285,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[25,40,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[134,164,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[191,205,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[72,78,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[33,77,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[588,612,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[24,87,\"0\"]],null]]]\n\n\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[7015,7095,\"1/2\"],[7015,7044,\"1/2\"],[7048,7095,\"0\"],[7066,7095,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[7113,7189,\"1/2\"],[7113,7141,\"1/2\"],[7145,7189,\"0\"],[7145,7175,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[7477,7482,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[7536,7542,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,26,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,57,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[276,296,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[67,93,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[67,93,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[95,105,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[33,57,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[122,137,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,27,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[2092,2172,\"1/2\"],[2092,2121,\"1/2\"],[2125,2172,\"0\"],[2143,2172,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[2190,2266,\"1/2\"],[2190,2218,\"1/2\"],[2222,2266,\"0\"],[2222,2252,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2456,2461,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2515,2521,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,23,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[82,130,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[73,93,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,61,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[320,340,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,61,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[29,69,\"0\"]],null]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[92,115,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[79,89,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[176,187,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,37,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,38,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[67,72,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,28,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[202,220,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[445,462,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[67,72,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[86,132,\"0\"],[103,132,\"0\"],[103,121,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[4014,4094,\"1/2\"],[4014,4043,\"1/2\"],[4047,4094,\"0\"],[4065,4094,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[4112,4188,\"1/2\"],[4112,4140,\"1/2\"],[4144,4188,\"0\"],[4144,4174,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[4500,4505,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[4574,4580,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[4674,4685,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[58,77,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[97,147,\"0\"],[97,126,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[289,325,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[387,416,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[29,32,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[971,1003,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[1111,1140,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[1505,1520,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[2210,2290,\"1/2\"],[2210,2239,\"1/2\"],[2243,2290,\"0\"],[2261,2290,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[2308,2384,\"1/2\"],[2308,2336,\"1/2\"],[2340,2384,\"0\"],[2340,2370,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2573,2578,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2632,2638,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2717,2728,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[17,22,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[287,295,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[379,388,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[17,52,\"0\"],[27,52,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[183,199,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[21,91,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[74,130,\"0\"],[74,104,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[73,90,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[1877,1957,\"1/2\"],[1877,1906,\"1/2\"],[1910,1957,\"0\"],[1928,1957,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[1975,2051,\"1/2\"],[1975,2003,\"1/2\"],[2007,2051,\"0\"],[2007,2037,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2237,2242,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2311,2317,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[2411,2422,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[76,81,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[61,68,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[165,206,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[25,53,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[172,208,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[44,70,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[22,38,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[69,95,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[1,null,[[0,1]]]\n[\"1\",\"b\",[[0,\"1\",null,[[17,39,\"1\"]],null]]]\n[18,null,[[0,18]]]\n[18,null,[[0,18]]]\n\n\n[19,null,[[0,19]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[201,229,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[30,64,\"0\"]],null]]]\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[480,486,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[42,74,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[\"0\",\"b\",[[0,\"0\",null,[[20,49,\"0\"]],null]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[78,120,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[143,174,\"0\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[203,204,\"0\"]],null]]]\n[\"0\",\"b\",[[0,\"0\",null,[[33,51,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[0,null,[[0,0]]]\n\n[\"0\",\"b\",[[0,\"0\",null,[[60,86,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[21,28,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n[\"0\",\"b\",[[0,\"0\",null,[[147,160,\"0\"]],null]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[191,246,\"0\"],[191,234,\"0\"],[191,222,\"0\"]],null]]]\n[\"0/2\",\"b\",[[0,\"0/2\",null,[[282,337,\"0\"],[282,318,\"0\"]],null]]]\n\n[0,null,[[0,0]]]\n[\"0/3\",\"b\",[[0,\"0/3\",null,[[25,88,\"0\"],[25,44,\"0\"],[49,87,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n\n\n\n[0,null,[[0,0]]]\n[\"0\",\"b\",[[0,\"0\",null,[[77,97,\"0\"]],null]]]\n[0,null,[[0,0]]]\n\n[0,null,[[0,0]]]\n\n\n\n\n\n[0,null,[[0,0]]]\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[8079,8159,\"1/2\"],[8079,8108,\"1/2\"],[8112,8159,\"0\"],[8130,8159,\"0\"]],null]]]\n[\"2/4\",\"b\",[[0,\"2/4\",null,[[8177,8253,\"1/2\"],[8177,8205,\"1/2\"],[8209,8253,\"0\"],[8209,8239,\"0\"]],null]]]\n\n[1,null,[[0,1]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[8471,8476,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[8545,8551,\"1/2\"]],null]]]\n[0,null,[[0,0]]]\n[0,null,[[0,0]]]\n\n\n[\"1/2\",\"b\",[[0,\"1/2\",null,[[8645,8656,\"1/2\"]],null]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n[1,null,[[0,1]]]\n", + "\n{\"present_sessions\":[0]}\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n\n\n\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]\n[1,null,[[0,1]]]" + ], + "totals": { + "f": 18, + "n": 9701, + "h": 2848, + "m": 6171, + "p": 682, + "c": "29.35780", + "b": 2944, + "d": 0, + "M": 0, + "s": 0, + "C": 0, + "N": 0, + "diff": null + }, + "report": { + "files": { + "/theme/Responsive/js/external/jquery.js": [ + 0, + [0, 3413, 1203, 1886, 324, "35.24758", 1262, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/js/external/jqueryui.1.12.1/js/jquery-ui.1.12.1.min.js": [ + 1, + [0, 8, 0, 2, 6, "0", 8, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/js/external/jqueryui.1.12.1/eventbooking-theme/jquery-ui.js": [ + 2, + [0, 15, 5, 10, 0, "33.33333", 2, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-da.js": [ + 3, + [0, 7, 6, 1, 0, "85.71429", 1, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-de.js": [ + 4, + [0, 7, 6, 1, 0, "85.71429", 1, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-et.js": [ + 5, + [0, 7, 6, 1, 0, "85.71429", 1, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-en-GB.js": [ + 6, + [0, 7, 6, 1, 0, "85.71429", 1, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-fi.js": [ + 7, + [0, 7, 6, 1, 0, "85.71429", 1, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-fr.js": [ + 8, + [0, 7, 6, 1, 0, "85.71429", 1, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-gl.js": [ + 9, + [0, 7, 6, 1, 0, "85.71429", 1, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-nl.js": [ + 10, + [0, 7, 6, 1, 0, "85.71429", 1, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-nb.js": [ + 11, + [0, 7, 6, 1, 0, "85.71429", 1, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-sv.js": [ + 12, + [0, 7, 6, 1, 0, "85.71429", 1, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/vendor/js/bootstrap.js": [ + 13, + [0, 1095, 259, 821, 15, "23.65297", 301, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/js/onlinebooking.js": [ + 14, + [0, 127, 24, 102, 1, "18.89764", 15, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/vendor/js/qunit/qunit.js": [ + 15, + [0, 1893, 596, 1158, 139, "31.48442", 499, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/vendor/js/sinon/sinon.js": [ + 16, + [0, 3032, 653, 2182, 197, "21.53694", 847, 0, 0, 0, 0, 0, 0], + null, + null + ], + "/tests/onlinebooking.js": [ + 17, + [0, 48, 48, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + null, + null + ] + }, + "sessions": {} + } +} diff --git a/apps/worker/services/report/languages/tests/unit/node/node3.json b/apps/worker/services/report/languages/tests/unit/node/node3.json new file mode 100644 index 0000000000..11581e26e9 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/node/node3.json @@ -0,0 +1,93664 @@ +{ + "/theme/Responsive/js/external/jquery.js": { + "lineData": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + 1, + null, + 1, + null, + 1, + null, + 1, + null, + 1, + null, + 1, + null, + 1, + null, + 1, + null, + 1, + null, + 1, + null, + 1, + null, + 1, + null, + null, + null, + 1, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 346, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 12, + 12, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 152, + null, + null, + 152, + null, + null, + 152, + null, + null, + null, + null, + 174, + null, + null, + null, + 12, + 12, + null, + null, + null, + null, + 8, + null, + null, + null, + 12, + null, + null, + null, + 0, + null, + null, + null, + 22, + null, + 22, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 272, + null, + null, + null, + null, + null, + null, + 272, + 17, + null, + null, + 17, + 17, + null, + null, + null, + 272, + 0, + null, + null, + null, + 272, + 34, + 34, + null, + null, + 272, + null, + null, + 310, + null, + null, + 166, + 1569, + 1569, + null, + null, + 1569, + 0, + null, + null, + null, + 1569, + null, + null, + 13, + 10, + 10, + null, + null, + 3, + null, + null, + null, + 13, + null, + null, + 1556, + 1548, + null, + null, + null, + null, + null, + null, + 272, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 963, + null, + null, + null, + null, + null, + 461, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 1627, + null, + null, + null, + 1627, + 1468, + null, + null, + 159, + null, + null, + 159, + 0, + null, + null, + null, + 159, + 159, + null, + null, + null, + null, + null, + null, + 138, + null, + 138, + 86, + null, + 52, + null, + null, + null, + 1620, + 45, + null, + null, + null, + 1575, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 292, + null, + null, + null, + 102, + null, + null, + null, + 316, + null, + 316, + 272, + 272, + 473, + 0, + null, + null, + null, + 44, + 610, + 0, + null, + null, + null, + null, + 316, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 37, + null, + 37, + 37, + 19, + null, + null, + null, + null, + 18, + null, + null, + null, + 37, + null, + null, + null, + 38, + null, + null, + null, + null, + null, + 230, + null, + null, + null, + 230, + 249, + null, + null, + 230, + null, + 230, + null, + null, + null, + 20, + null, + null, + null, + null, + null, + null, + null, + 20, + 63, + 63, + 61, + null, + null, + null, + 20, + null, + null, + null, + null, + 57, + null, + null, + null, + null, + 57, + 57, + 57, + 46, + null, + 46, + 46, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + 57, + null, + null, + null, + null, + null, + null, + null, + null, + 37, + null, + 37, + 0, + 0, + 0, + null, + null, + null, + null, + 37, + 1, + null, + null, + null, + 36, + 36, + 9, + null, + null, + null, + 36, + null, + 36, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + 1, + null, + 10, + null, + null, + 1, + null, + null, + null, + null, + null, + 410, + null, + null, + 410, + 18, + null, + null, + 392, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 64, + 4, + null, + 64, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + 0, + null, + 1, + null, + null, + null, + 90, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + 1, + 111, + null, + null, + null, + null, + null, + 111, + null, + null, + 111, + null, + null, + 0, + null, + null, + null, + 111, + null, + 95, + 0, + null, + 95, + null, + 95, + null, + null, + null, + 95, + null, + null, + 35, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 35, + 7, + 7, + null, + null, + 28, + null, + null, + 28, + 28, + null, + null, + null, + null, + 60, + null, + null, + null, + 56, + 8, + 8, + null, + null, + null, + null, + null, + 48, + null, + null, + 48, + 0, + null, + 48, + null, + null, + null, + 48, + 48, + 48, + 120, + null, + 48, + null, + null, + 48, + null, + null, + null, + 56, + 56, + 56, + null, + null, + 55, + null, + null, + 56, + 48, + null, + null, + null, + null, + null, + null, + null, + null, + 21, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 3, + null, + 3, + null, + 20, + null, + 0, + null, + 20, + null, + 3, + null, + null, + null, + null, + null, + null, + 1, + 12, + 12, + null, + null, + null, + null, + null, + null, + 1, + 10, + null, + 10, + 10, + null, + 2, + null, + null, + 10, + 3, + null, + null, + 10, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + 5, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + 1, + 2, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 2, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 1, + 7, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 172, + 172, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + 1, + 0, + null, + null, + null, + 1, + 1, + 1, + null, + null, + null, + 1, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + null, + null, + 1, + 1, + 0, + 0, + 0, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 1, + null, + 5, + 5, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + null, + 1, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + 1, + 1, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + 1, + 0, + null, + null, + null, + 1, + 0, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + 1, + null, + null, + 1, + null, + null, + null, + 1, + 0, + null, + null, + null, + 1, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + 1, + null, + 68, + null, + 68, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + 76, + 19, + 19, + null, + null, + null, + 57, + 57, + 0, + null, + null, + null, + 57, + null, + null, + null, + null, + null, + null, + 57, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + 57, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 1, + 16, + null, + null, + 1, + null, + 19, + 0, + null, + null, + null, + 19, + null, + 19, + null, + null, + null, + null, + 19, + 19, + null, + null, + 19, + null, + null, + null, + 19, + null, + null, + null, + null, + 0, + null, + null, + 1, + null, + 68, + 0, + null, + 68, + null, + null, + 1, + null, + 81, + 0, + null, + null, + 81, + null, + null, + null, + null, + null, + 81, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + 1, + 20, + null, + null, + null, + null, + null, + 20, + 20, + 20, + null, + 20, + 3, + 13, + 10, + null, + null, + 3, + 10, + null, + null, + null, + null, + null, + 20, + null, + 20, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 3, + null, + null, + 3, + null, + 3, + 0, + null, + null, + 3, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 2, + null, + null, + 2, + 0, + null, + null, + null, + 2, + 0, + null, + null, + 2, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 2, + null, + null, + null, + null, + null, + null, + 2, + 2, + 0, + null, + 0, + null, + null, + null, + null, + 2, + null, + 2, + null, + null, + 45, + null, + null, + null, + null, + 2, + 63, + null, + 63, + 63, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 2, + null, + null, + null, + null, + null, + null, + 2, + 2, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 2, + null, + null, + null, + 2, + null, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 13, + 13, + null, + 13, + 13, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 1, + null, + null, + 1, + 5, + null, + 1, + 2, + null, + null, + null, + 1, + 1, + 1, + null, + 1, + 60, + null, + null, + null, + 60, + 50, + null, + null, + 10, + 10, + 10, + null, + 10, + null, + null, + 13, + 13, + null, + 3, + null, + 13, + null, + null, + 13, + null, + null, + 13, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + 13, + 78, + null, + 14, + 14, + null, + null, + null, + null, + 14, + null, + null, + null, + 13, + 0, + null, + null, + null, + null, + null, + null, + 10, + null, + null, + null, + null, + null, + null, + null, + 1, + 120, + null, + null, + 120, + 143, + null, + 120, + null, + null, + 1, + 17, + null, + null, + null, + null, + null, + 17, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 63, + null, + null, + null, + 63, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 63, + 84, + 84, + null, + null, + null, + 84, + null, + 84, + 0, + 84, + null, + null, + null, + 58, + null, + null, + 26, + null, + null, + 26, + 5, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + 1, + 8, + null, + 121, + 121, + 229, + 13, + null, + null, + 108, + null, + null, + null, + null, + 1, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 1, + 8, + null, + null, + null, + null, + null, + null, + null, + 26, + null, + null, + 0, + null, + null, + 108, + null, + null, + null, + null, + 108, + 108, + null, + null, + 8, + 8, + 0, + null, + 8, + null, + null, + 8, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 8, + null, + null, + null, + 8, + null, + null, + 1, + 8, + null, + null, + 34, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 34, + 21, + null, + null, + null, + null, + null, + 34, + 121, + 121, + 121, + 0, + 0, + null, + 121, + 121, + 108, + 108, + null, + null, + 121, + 108, + null, + null, + null, + null, + 121, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 34, + null, + null, + null, + null, + null, + null, + null, + null, + 34, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + 34, + 21, + 21, + null, + null, + 34, + null, + null, + 8, + null, + null, + null, + null, + 1, + 23, + null, + null, + null, + null, + 23, + null, + 8, + 7, + null, + 8, + 8, + 8, + 8, + 0, + null, + 8, + null, + null, + null, + null, + 8, + null, + null, + 8, + null, + 23, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 21, + null, + null, + null, + 21, + null, + null, + null, + 21, + null, + null, + 5, + 5, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 5, + 5, + 5, + null, + null, + 5, + 0, + null, + 5, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 21, + null, + null, + null, + null, + null, + null, + 21, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + 1, + null, + null, + null, + 1, + null, + 1, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + 1, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + 1, + 1, + 1, + 1, + null, + null, + null, + null, + 1, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + 1, + 18, + null, + 18, + 74, + 74, + null, + null, + null, + 18, + null, + null, + null, + 1, + null, + 1, + null, + null, + null, + 1, + null, + null, + 1, + 9, + 1, + 5, + null, + null, + null, + null, + 8, + 0, + 0, + null, + null, + null, + null, + 8, + 3, + 13, + null, + null, + null, + null, + 5, + 5, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 1, + 16, + null, + 16, + 3, + null, + null, + 16, + 0, + null, + null, + 16, + 45, + null, + null, + null, + 1, + null, + 65, + null, + null, + null, + 65, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 65, + null, + 65, + 95, + null, + null, + 65, + null, + null, + 1, + null, + null, + 6, + null, + null, + 2, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + 346, + null, + null, + 346, + 153, + null, + null, + null, + null, + 193, + null, + null, + 193, + 57, + null, + null, + null, + null, + 8, + null, + null, + 49, + null, + null, + null, + 57, + null, + null, + 25, + 8, + null, + null, + null, + 8, + null, + null, + null, + null, + null, + null, + 8, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + 8, + null, + null, + null, + 17, + null, + 17, + null, + null, + 7, + 7, + null, + 17, + null, + null, + null, + 32, + 32, + null, + null, + null, + null, + 0, + null, + null, + null, + 136, + 105, + 105, + 105, + null, + null, + null, + 31, + 1, + null, + null, + null, + null, + null, + null, + 30, + null, + null, + null, + 1, + null, + null, + 1, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + 1, + 1, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 5, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + null, + 16, + 16, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 18, + null, + null, + 0, + null, + null, + 12, + 25, + null, + 25, + 25, + null, + null, + 25, + 11, + null, + null, + 25, + null, + null, + 3, + 3, + null, + null, + null, + 3, + 0, + null, + null, + null, + 25, + null, + null, + 1, + null, + null, + null, + null, + 1, + 30, + 30, + 50, + null, + 30, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + 30, + null, + null, + null, + 30, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 10, + null, + null, + null, + 10, + 10, + 10, + 10, + null, + null, + 24, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 10, + 0, + null, + null, + 10, + null, + null, + 10, + null, + null, + 10, + 10, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 37, + null, + null, + 37, + 0, + 0, + null, + null, + 37, + 37, + 57, + 57, + 57, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + 37, + 0, + null, + null, + 37, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 5, + 5, + 5, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 5, + 5, + 5, + null, + 5, + null, + null, + 0, + null, + null, + null, + null, + 10, + 10, + 10, + 10, + 10, + 10, + null, + null, + 10, + null, + null, + null, + null, + 5, + 5, + null, + null, + null, + null, + 0, + null, + null, + null, + 30, + null, + null, + null, + 1, + 2, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + 1, + null, + null, + 5, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 2, + null, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + 4, + 4, + 12, + 4, + null, + null, + 4, + null, + null, + null, + null, + 4, + 0, + null, + null, + 4, + null, + null, + null, + 4, + 0, + null, + null, + null, + null, + null, + null, + 4, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 4, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 4, + 2, + 2, + null, + null, + null, + null, + 4, + null, + null, + null, + null, + null, + null, + null, + 4, + 4, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 4, + 0, + null, + null, + null, + null, + 4, + 0, + null, + 4, + null, + null, + null, + null, + 4, + null, + null, + 4, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 4, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 4, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 13, + null, + null, + null, + null, + null, + 5, + 15, + null, + null, + null, + null, + null, + 15, + null, + null, + 15, + 10, + null, + null, + null, + null, + 5, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 15, + null, + null, + null, + null, + 15, + 0, + 0, + null, + null, + null, + null, + null, + 15, + null, + null, + null, + 5, + null, + null, + 5, + 4, + null, + null, + null, + 5, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 2, + 0, + null, + null, + 2, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + 1, + null, + null, + 1, + 0, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + 1, + 1, + 1, + 1, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + 0, + null, + null, + null, + null, + 1, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 218, + null, + null, + null, + null, + 218, + 35, + 35, + 55, + null, + null, + null, + 183, + 127, + null, + 127, + 127, + null, + null, + 127, + null, + null, + 14, + 14, + 14, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 127, + 113, + 187, + null, + null, + null, + null, + null, + null, + null, + null, + 218, + 162, + null, + null, + null, + 56, + 11, + null, + null, + 45, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 60, + null, + null, + null, + null, + null, + 1, + 2, + null, + null, + 1, + null, + 1, + null, + null, + null, + null, + 188, + null, + null, + 188, + 53, + null, + null, + null, + null, + 53, + null, + null, + null, + 53, + 52, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 188, + null, + null, + 23, + null, + null, + null, + null, + 23, + 23, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 23, + null, + null, + 225, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 12, + null, + null, + 6, + null, + null, + null, + null, + null, + null, + null, + null, + 6, + null, + null, + null, + 6, + null, + null, + 26, + null, + null, + 26, + 0, + null, + null, + 26, + null, + null, + 26, + null, + null, + null, + 0, + null, + 26, + null, + null, + null, + 26, + null, + null, + null, + null, + 26, + null, + 26, + 52, + null, + null, + null, + null, + 26, + null, + null, + null, + null, + null, + 26, + 26, + null, + 0, + null, + null, + null, + null, + 70, + 70, + null, + null, + 1, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 1, + 2, + 0, + null, + null, + 2, + 0, + null, + null, + 2, + 0, + null, + null, + null, + 2, + 0, + null, + null, + 2, + 0, + null, + null, + 2, + null, + null, + 1, + 4, + null, + null, + null, + 4, + 4, + 4, + null, + 4, + 2, + 2, + null, + null, + null, + 2, + null, + 2, + null, + null, + 4, + null, + null, + 1, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 1, + null, + 25, + null, + null, + null, + null, + 25, + 1, + 1, + null, + 1, + 1, + 1, + null, + null, + null, + 4, + 4, + 4, + 2, + 2, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + 24, + 0, + 0, + null, + null, + null, + 24, + 24, + null, + null, + null, + null, + null, + null, + 24, + null, + null, + null, + 10, + 10, + 8, + null, + null, + null, + null, + 2, + 2, + 0, + null, + null, + null, + 2, + null, + null, + null, + 14, + null, + null, + 14, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 1, + null, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 1, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 1, + null, + 1, + null, + null, + 1, + null, + 1, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 4, + null, + null, + null, + 4, + 4, + 4, + null, + null, + 4, + null, + null, + 4, + 4, + null, + null, + 4, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 1, + null, + 1, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + 1, + 8, + null, + null, + null, + null, + null, + 8, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 8, + 0, + 0, + null, + null, + null, + 8, + null, + null, + 1, + null, + 4, + null, + null, + 4, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 1, + null, + 1, + null, + 1, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + null, + null, + 1, + null, + null, + null, + 100, + null, + 100, + 94, + null, + 6, + 6, + null, + null, + 0, + null, + null, + 100, + 12, + null, + null, + 88, + null, + null, + null, + null, + 1, + 33, + null, + null, + 33, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 27, + null, + null, + null, + null, + null, + 27, + 27, + null, + 27, + null, + null, + 27, + null, + null, + null, + 15, + null, + null, + 12, + 0, + null, + null, + null, + 12, + null, + null, + 12, + 12, + 12, + null, + null, + 12, + 12, + 6, + null, + null, + null, + null, + 12, + null, + null, + 12, + null, + null, + 12, + null, + null, + null, + null, + null, + 27, + null, + 27, + 27, + null, + null, + 45, + 0, + 0, + null, + 0, + null, + null, + 45, + null, + null, + 45, + null, + null, + 45, + 16, + null, + null, + null, + 45, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 27, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + 1, + 1, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 53, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + 1, + 77, + null, + null, + 77, + null, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + 77, + null, + null, + 37, + 37, + 40, + 40, + null, + null, + 19, + 19, + null, + null, + null, + 21, + 21, + 21, + null, + null, + 77, + 0, + 77, + 0, + null, + null, + 77, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + 77, + 108, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + 108, + null, + null, + null, + null, + null, + 108, + 0, + null, + null, + null, + 108, + 0, + 0, + 0, + null, + null, + null, + null, + 108, + 19, + null, + null, + null, + 108, + 22, + null, + null, + null, + 108, + 47, + null, + 108, + 47, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + 108, + 108, + 108, + 125, + 125, + 125, + null, + null, + 125, + 0, + null, + null, + null, + 125, + null, + null, + 125, + null, + null, + 125, + null, + null, + 125, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 125, + 99, + 99, + null, + null, + 99, + null, + null, + 93, + 93, + null, + null, + null, + null, + 125, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 125, + 24, + null, + 101, + null, + null, + null, + 125, + null, + null, + null, + null, + null, + null, + null, + 58, + null, + null, + null, + null, + 58, + 2, + null, + null, + null, + 56, + 56, + 56, + 68, + 68, + 68, + null, + null, + 68, + 0, + 0, + null, + 0, + null, + null, + 68, + 68, + 68, + 68, + null, + null, + null, + 68, + 68, + 69, + null, + 69, + null, + null, + null, + null, + 60, + null, + 60, + 0, + null, + 60, + 0, + null, + null, + null, + null, + null, + null, + 68, + 55, + null, + null, + 55, + null, + null, + 55, + null, + null, + null, + null, + 56, + 26, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + null, + null, + null, + null, + 1, + null, + 1, + 0, + null, + null, + 1, + null, + null, + 1, + 0, + null, + null, + null, + 1, + null, + null, + 1, + 1, + 1, + null, + 1, + 1, + null, + null, + null, + null, + 3, + null, + 3, + 3, + null, + 3, + null, + null, + 3, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + null, + null, + null, + 30, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 55, + 55, + null, + null, + null, + 1, + null, + null, + 8, + 0, + null, + null, + null, + 8, + 1, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + 1, + 1, + null, + null, + null, + 7, + null, + null, + null, + 8, + 0, + null, + null, + null, + 8, + null, + null, + 8, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + 4, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 1, + null, + null, + 77, + null, + null, + 0, + null, + null, + 42, + 42, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, + 42, + null, + null, + 0, + 0, + null, + 0, + null, + 42, + null, + null, + 32, + 32, + null, + 42, + 0, + null, + 42, + 58, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 14, + null, + null, + 0, + null, + null, + 14, + null, + null, + null, + 1, + 0, + 0, + null, + 1, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + 1, + 6, + null, + 6, + 0, + null, + null, + null, + 6, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + 6, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 1, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 1, + null, + null, + 20, + null, + 20, + null, + null, + null, + null, + null, + null, + null, + 20, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 20, + 20, + 20, + null, + 20, + 14, + null, + null, + null, + 20, + 20, + 20, + null, + null, + null, + null, + 20, + 20, + null, + 20, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 20, + null, + null, + 20, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 20, + null, + null, + 1, + 10, + null, + null, + null, + 10, + 17, + 0, + null, + null, + 17, + 17, + 17, + null, + 17, + null, + null, + null, + 10, + null, + null, + 1, + null, + 12, + null, + null, + null, + 6, + null, + null, + null, + null, + 6, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 6, + 6, + 6, + 6, + null, + 6, + 6, + null, + null, + 0, + null, + null, + null, + null, + 6, + 6, + 0, + null, + null, + null, + 6, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + null, + 9, + null, + null, + null, + 1, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 14, + 14, + 14, + 14, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 6, + 6, + 6, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 6, + 6, + null, + 6, + 6, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + 1, + 1, + null, + null, + null, + 0, + null, + null, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + 5, + 7, + null, + null, + null, + null, + null, + 7, + 7, + 7, + null, + null, + null, + 7, + null, + null, + 7, + null, + null, + 1, + null, + 1, + null, + 1, + null, + null, + null, + null, + 72, + null, + 72, + 72, + null, + null, + 72, + null, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + 149, + 148, + null, + null, + 1, + null, + null, + null, + null, + 1, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + null, + 1, + 1, + null, + 1, + null, + null, + null, + 1, + null, + null, + 1, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + 1, + 1, + 1, + null, + 1, + null, + 1, + null, + 1, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 148, + 148, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + 148, + null, + null, + 148, + null, + null, + null, + 148, + 148, + null, + 148, + 0, + null, + null, + null, + null, + null, + null, + null, + 148, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 148, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 3, + null, + 1, + null, + null, + null, + 0, + 0, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 18, + 18, + null, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 1, + null, + null, + null, + 25, + 25, + null, + null, + null, + null, + null, + null, + 1, + 44, + null, + null, + null, + 44, + 20, + null, + null, + null, + 24, + null, + null, + 44, + null, + null, + 48, + 16, + null, + null, + 48, + null, + null, + 48, + 32, + null, + null, + null, + 48, + 32, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + 44, + null, + null, + 1, + null, + null, + 24, + null, + null, + null, + null, + null, + null, + null, + 24, + 24, + null, + null, + null, + null, + null, + 24, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 24, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 35, + 0, + null, + null, + null, + 35, + null, + null, + null, + 35, + null, + null, + null, + 35, + null, + null, + 35, + 35, + null, + null, + 35, + 0, + null, + null, + 0, + null, + null, + null, + 35, + 0, + null, + null, + null, + 35, + 26, + null, + null, + null, + 35, + 0, + null, + null, + null, + 35, + null, + null, + 35, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + 172, + null, + null, + null, + 172, + null, + null, + null, + 172, + null, + null, + 172, + 28, + null, + null, + null, + 172, + 144, + null, + null, + null, + 172, + 0, + null, + null, + null, + 172, + 104, + 104, + null, + 68, + null, + null, + null, + 1, + 2, + null, + 24, + null, + null, + null, + 24, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 25, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 25, + null, + null, + 0, + 0, + null, + null, + 25, + null, + null, + null, + null, + 1, + null, + 4, + 4, + null, + null, + 4, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + 3, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 3, + 2, + null, + null, + null, + 1, + null, + 11, + 15, + null, + null, + null, + 15, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 15, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + 1, + null, + 1, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + null, + 1, + null, + 1, + null, + null, + 0, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 0, + 0, + null, + null, + null, + null, + 1, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + 1, + null, + null, + 1, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + null, + null, + 1, + 0, + 0, + null, + 0, + null, + null, + null, + 1, + 3, + null, + null, + null, + null, + null, + 3, + 3, + 6, + 6, + null, + null, + 3, + 0, + null, + null, + 3, + null, + null, + 1, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 1, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + 1, + 3, + 3, + 8, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 6, + 0, + null, + null, + null, + 1, + 1, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + 1, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 1, + 1, + 0, + 0, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + 1, + 1, + 1, + 1, + null, + null, + null, + 1, + null, + null, + 1, + null, + 94, + null, + null, + null, + 8, + 16, + null, + null, + null, + null, + 1, + null, + 170, + null, + null, + null, + 170, + 0, + null, + null, + null, + 170, + 0, + null, + null, + null, + null, + 170, + 170, + null, + null, + null, + 170, + 152, + 0, + 0, + null, + null, + 152, + null, + 0, + null, + null, + 152, + 152, + null, + null, + 18, + 0, + null, + null, + 18, + null, + null, + 18, + null, + null, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 16, + null, + null, + null, + null, + null, + null, + 16, + 16, + 16, + null, + null, + null, + null, + null, + null, + 1, + null, + 0, + null, + null, + 0, + null, + 0, + null, + 0, + null, + null, + null, + 1, + 16, + null, + 16, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 1, + null, + 1, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 1, + null, + 1, + null, + null, + null, + 1, + 0, + null, + null, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 0, + null, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 10, + null, + null, + null, + null, + null, + null, + null, + 1, + 179, + 179, + null, + null, + null, + 1, + 91, + null, + null, + 1, + null, + 41, + null, + null, + 41, + 0, + 0, + null, + null, + null, + 41, + 41, + null, + 41, + 46, + 46, + null, + 46, + 46, + 46, + 46, + 46, + null, + null, + null, + null, + 46, + 46, + 46, + null, + null, + null, + null, + null, + 41, + null, + null, + null, + 30, + null, + null, + 30, + 0, + 0, + null, + null, + null, + 30, + 0, + null, + null, + 30, + 30, + null, + 30, + 42, + null, + null, + 42, + null, + 42, + 42, + 42, + null, + null, + 121, + 34, + null, + null, + null, + null, + 42, + 42, + 23, + null, + null, + null, + null, + null, + 30, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 3, + null, + null, + 3, + 3, + 3, + null, + 0, + null, + null, + null, + 3, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + 5, + null, + null, + 5, + 5, + 5, + null, + null, + 5, + null, + null, + null, + 5, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + 2, + null, + 0, + 0, + null, + null, + null, + 2, + 2, + 5, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + null, + null, + 7, + null, + null, + null, + null, + 7, + null, + null, + 7, + 0, + null, + null, + null, + 7, + 0, + null, + null, + 7, + null, + null, + 0, + 0, + 0, + null, + 7, + null, + null, + 7, + null, + null, + null, + null, + 7, + 7, + 7, + null, + null, + null, + null, + 7, + 7, + 7, + null, + null, + null, + 7, + null, + null, + null, + null, + 7, + 7, + 0, + null, + null, + null, + null, + 7, + null, + 7, + 7, + 7, + null, + 7, + 28, + 28, + null, + null, + null, + 7, + 7, + null, + null, + null, + null, + 7, + 7, + null, + 42, + null, + null, + null, + null, + 42, + null, + 42, + 0, + null, + null, + null, + 42, + 42, + 0, + 0, + 0, + null, + null, + null, + 7, + null, + null, + 7, + null, + 7, + null, + null, + null, + null, + null, + 7, + null, + null, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + 7, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + 1, + null, + null, + 8, + 7, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + 22, + 1, + null, + null, + null, + null, + null, + 1, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + 2, + 0, + null, + null, + 2, + null, + 6, + null, + null, + 6, + 2, + null, + 6, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + 1, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + 1, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + 1, + null, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 1, + null, + null, + 2, + null, + 5, + 2, + 2, + null, + null, + 5, + null, + null, + null, + 5, + null, + null, + 5, + null, + null, + 6, + 0, + 0, + null, + null, + null, + 6, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 1, + 2, + null, + null, + 2, + 5, + 5, + null, + null, + 2, + 2, + null, + null, + 2, + null, + null, + null, + null, + null, + null, + 1, + null, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 2, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 1, + 2, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 6, + null, + 6, + 6, + 0, + null, + null, + null, + 6, + null, + 6, + 6, + null, + null, + null, + 6, + null, + 6, + 0, + null, + null, + 6, + 6, + null, + null, + 6, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 3, + null, + 3, + 3, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 1, + 0, + null, + 1, + 0, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + 1, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 1, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 1, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 8, + 0, + null, + 8, + 0, + 0, + null, + null, + 8, + null, + 8, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 8, + 8, + null, + null, + 8, + 1, + null, + null, + 7, + null, + 7, + 0, + null, + null, + 7, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 6, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 1, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 1, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 1, + 2, + null, + 2, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 2, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 2, + null, + null, + null, + 6, + 32, + null, + null, + 32, + 44, + null, + 44, + null, + null, + 0, + null, + null, + null, + null, + null, + 44, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 44, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + 1 + ], + "functionData": [ + 1, + 0, + 1, + 0, + 346, + 0, + 0, + 12, + 152, + 174, + 12, + 12, + 8, + 12, + 0, + 22, + 0, + 272, + 0, + 0, + 963, + 461, + 0, + 1627, + 138, + 1620, + 0, + 292, + 102, + 316, + 0, + 37, + 38, + 230, + 20, + 57, + 37, + 9, + 10, + 410, + 1, + 64, + 0, + 0, + 0, + 0, + 0, + 90, + 0, + 111, + 3, + 20, + 12, + 10, + 0, + 0, + 5, + 0, + 2, + 0, + 2, + 0, + 7, + 0, + 0, + 0, + 172, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 5, + 0, + 0, + 1, + 1, + 1, + 68, + 0, + 76, + 0, + 16, + 19, + 68, + 81, + 0, + 0, + 20, + 0, + 3, + 0, + 2, + 2, + 0, + 0, + 2, + 45, + 2, + 63, + 0, + 0, + 0, + 2, + 0, + 0, + 2, + 0, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 60, + 120, + 17, + 0, + 63, + 8, + 121, + 0, + 0, + 0, + 0, + 8, + 26, + 0, + 108, + 8, + 34, + 23, + 21, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 18, + 9, + 5, + 0, + 13, + 0, + 16, + 45, + 65, + 0, + 1, + 6, + 2, + 346, + 0, + 0, + 1, + 0, + 5, + 0, + 0, + 16, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 18, + 0, + 12, + 25, + 30, + 50, + 30, + 10, + 37, + 37, + 57, + 0, + 0, + 0, + 0, + 5, + 0, + 5, + 0, + 10, + 5, + 0, + 2, + 0, + 0, + 5, + 0, + 0, + 2, + 0, + 0, + 0, + 0, + 4, + 12, + 4, + 4, + 4, + 4, + 13, + 15, + 5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 0, + 1, + 1, + 218, + 0, + 60, + 2, + 188, + 23, + 225, + 12, + 26, + 70, + 2, + 4, + 0, + 0, + 0, + 0, + 0, + 25, + 0, + 24, + 14, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 8, + 4, + 4, + 0, + 0, + 100, + 33, + 27, + 1, + 0, + 53, + 0, + 77, + 0, + 108, + 108, + 1, + 58, + 1, + 1, + 30, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 55, + 8, + 0, + 0, + 0, + 0, + 4, + 0, + 77, + 0, + 42, + 58, + 14, + 0, + 0, + 6, + 0, + 20, + 0, + 10, + 12, + 6, + 0, + 9, + 1, + 0, + 0, + 0, + 14, + 14, + 0, + 0, + 6, + 6, + 0, + 0, + 0, + 6, + 6, + 1, + 1, + 0, + 0, + 5, + 7, + 72, + 1, + 149, + 0, + 0, + 148, + 1, + 148, + 3, + 1, + 18, + 25, + 44, + 24, + 0, + 35, + 172, + 2, + 24, + 0, + 25, + 4, + 4, + 3, + 0, + 11, + 15, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 8, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 94, + 8, + 16, + 170, + 0, + 16, + 0, + 16, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 10, + 179, + 91, + 41, + 0, + 30, + 0, + 0, + 0, + 0, + 3, + 5, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 5, + 7, + 0, + 8, + 7, + 0, + 22, + 1, + 0, + 2, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 5, + 0, + 0, + 0, + 2, + 0, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 0, + 6, + 6, + 0, + 0, + 0, + 3, + 3, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 8, + 0, + 0, + 0, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 0, + 2, + 0, + 2, + 6, + 32, + 44, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "branchData": { + "18": [ + null, + { + "position": 24, + "nodeLength": 64, + "src": "typeof module === \"object\" && typeof module.exports === \"object\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 24, + "nodeLength": 26, + "src": "typeof module === \"object\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 54, + "nodeLength": 34, + "src": "typeof module.exports === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "27": [ + null, + { + "position": 431, + "nodeLength": 15, + "src": "global.document", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "30": [ + null, + { + "position": 10, + "nodeLength": 11, + "src": "!w.document", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "40": [ + null, + { + "position": 829, + "nodeLength": 29, + "src": "typeof window !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "77": [ + null, + { + "position": 9, + "nodeLength": 15, + "src": "doc || document", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "133": [ + null, + { + "position": 55, + "nodeLength": 11, + "src": "num == null", + "evalFalse": 0, + "evalTrue": 12 + } + ], + "138": [ + null, + { + "position": 161, + "nodeLength": 7, + "src": "num < 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "180": [ + null, + { + "position": 36, + "nodeLength": 5, + "src": "i < 0", + "evalFalse": 22, + "evalTrue": 0 + } + ], + "181": [ + null, + { + "position": 84, + "nodeLength": 17, + "src": "j >= 0 && j < len", + "evalFalse": 0, + "evalTrue": 22 + }, + { + "position": 84, + "nodeLength": 6, + "src": "j >= 0", + "evalFalse": 0, + "evalTrue": 22 + }, + { + "position": 94, + "nodeLength": 7, + "src": "j < len", + "evalFalse": 0, + "evalTrue": 22 + } + ], + "185": [ + null, + { + "position": 10, + "nodeLength": 37, + "src": "this.prevObject || this.constructor()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "197": [ + null, + { + "position": 60, + "nodeLength": 20, + "src": "arguments[0] || {}", + "evalFalse": 0, + "evalTrue": 272 + } + ], + "203": [ + null, + { + "position": 179, + "nodeLength": 27, + "src": "typeof target === \"boolean\"", + "evalFalse": 255, + "evalTrue": 17 + } + ], + "207": [ + null, + { + "position": 67, + "nodeLength": 20, + "src": "arguments[i] || {}", + "evalFalse": 0, + "evalTrue": 17 + } + ], + "212": [ + null, + { + "position": 393, + "nodeLength": 58, + "src": "typeof target !== \"object\" && !jQuery.isFunction(target)", + "evalFalse": 272, + "evalTrue": 0 + }, + { + "position": 393, + "nodeLength": 26, + "src": "typeof target !== \"object\"", + "evalFalse": 243, + "evalTrue": 29 + } + ], + "217": [ + null, + { + "position": 537, + "nodeLength": 12, + "src": "i === length", + "evalFalse": 238, + "evalTrue": 34 + } + ], + "222": [ + null, + { + "position": 591, + "nodeLength": 10, + "src": "i < length", + "evalFalse": 272, + "evalTrue": 310 + } + ], + "225": [ + null, + { + "position": 57, + "nodeLength": 34, + "src": "(options = arguments[i]) != null", + "evalFalse": 144, + "evalTrue": 166 + } + ], + "233": [ + null, + { + "position": 98, + "nodeLength": 15, + "src": "target === copy", + "evalFalse": 1569, + "evalTrue": 0 + } + ], + "238": [ + null, + { + "position": 205, + "nodeLength": 97, + "src": "deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))", + "evalFalse": 1556, + "evalTrue": 13 + }, + { + "position": 213, + "nodeLength": 89, + "src": "copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))", + "evalFalse": 115, + "evalTrue": 13 + }, + { + "position": 223, + "nodeLength": 77, + "src": "jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy))", + "evalFalse": 107, + "evalTrue": 13 + } + ], + "241": [ + null, + { + "position": 12, + "nodeLength": 11, + "src": "copyIsArray", + "evalFalse": 3, + "evalTrue": 10 + } + ], + "243": [ + null, + { + "position": 42, + "nodeLength": 28, + "src": "src && jQuery.isArray(src)", + "evalFalse": 10, + "evalTrue": 0 + } + ], + "246": [ + null, + { + "position": 15, + "nodeLength": 34, + "src": "src && jQuery.isPlainObject(src)", + "evalFalse": 0, + "evalTrue": 3 + } + ], + "253": [ + null, + { + "position": 663, + "nodeLength": 18, + "src": "copy !== undefined", + "evalFalse": 8, + "evalTrue": 1548 + } + ], + "279": [ + null, + { + "position": 10, + "nodeLength": 33, + "src": "jQuery.type(obj) === \"function\"", + "evalFalse": 388, + "evalTrue": 575 + } + ], + "285": [ + null, + { + "position": 10, + "nodeLength": 33, + "src": "obj != null && obj === obj.window", + "evalFalse": 443, + "evalTrue": 18 + }, + { + "position": 10, + "nodeLength": 11, + "src": "obj != null", + "evalFalse": 0, + "evalTrue": 461 + }, + { + "position": 25, + "nodeLength": 18, + "src": "obj === obj.window", + "evalFalse": 443, + "evalTrue": 18 + } + ], + "294": [ + null, + { + "position": 195, + "nodeLength": 267, + "src": "(type === \"number\" || type === \"string\") && !isNaN(obj - parseFloat(obj))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 195, + "nodeLength": 38, + "src": "type === \"number\" || type === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 195, + "nodeLength": 17, + "src": "type === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 216, + "nodeLength": 17, + "src": "type === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "307": [ + null, + { + "position": 121, + "nodeLength": 50, + "src": "!obj || toString.call(obj) !== \"[object Object]\"", + "evalFalse": 159, + "evalTrue": 1468 + }, + { + "position": 129, + "nodeLength": 42, + "src": "toString.call(obj) !== \"[object Object]\"", + "evalFalse": 159, + "evalTrue": 1019 + } + ], + "314": [ + null, + { + "position": 306, + "nodeLength": 6, + "src": "!proto", + "evalFalse": 159, + "evalTrue": 0 + } + ], + "319": [ + null, + { + "position": 439, + "nodeLength": 56, + "src": "hasOwn.call(proto, \"constructor\") && proto.constructor", + "evalFalse": 0, + "evalTrue": 159 + } + ], + "320": [ + null, + { + "position": 506, + "nodeLength": 78, + "src": "typeof Ctor === \"function\" && fnToString.call(Ctor) === ObjectFunctionString", + "evalFalse": 0, + "evalTrue": 159 + }, + { + "position": 506, + "nodeLength": 26, + "src": "typeof Ctor === \"function\"", + "evalFalse": 0, + "evalTrue": 159 + }, + { + "position": 536, + "nodeLength": 48, + "src": "fnToString.call(Ctor) === ObjectFunctionString", + "evalFalse": 0, + "evalTrue": 159 + } + ], + "336": [ + null, + { + "position": 8, + "nodeLength": 11, + "src": "obj == null", + "evalFalse": 1575, + "evalTrue": 45 + } + ], + "341": [ + null, + { + "position": 112, + "nodeLength": 52, + "src": "typeof obj === \"object\" || typeof obj === \"function\"", + "evalFalse": 453, + "evalTrue": 1122 + }, + { + "position": 112, + "nodeLength": 23, + "src": "typeof obj === \"object\"", + "evalFalse": 1048, + "evalTrue": 527 + }, + { + "position": 139, + "nodeLength": 25, + "src": "typeof obj === \"function\"", + "evalFalse": 453, + "evalTrue": 595 + } + ], + "342": [ + null, + { + "position": 57, + "nodeLength": 46, + "src": "class2type[toString.call(obj)] || \"object\"", + "evalFalse": 0, + "evalTrue": 1122 + } + ], + "359": [ + null, + { + "position": 10, + "nodeLength": 67, + "src": "elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase()", + "evalFalse": 102, + "evalTrue": 0 + }, + { + "position": 27, + "nodeLength": 50, + "src": "elem.nodeName.toLowerCase() === name.toLowerCase()", + "evalFalse": 102, + "evalTrue": 0 + } + ], + "365": [ + null, + { + "position": 30, + "nodeLength": 18, + "src": "isArrayLike(obj)", + "evalFalse": 44, + "evalTrue": 272 + } + ], + "367": [ + null, + { + "position": 36, + "nodeLength": 10, + "src": "i < length", + "evalFalse": 272, + "evalTrue": 473 + } + ], + "368": [ + null, + { + "position": 10, + "nodeLength": 48, + "src": "callback.call(obj[i], i, obj[i]) === false", + "evalFalse": 473, + "evalTrue": 0 + } + ], + "374": [ + null, + { + "position": 10, + "nodeLength": 48, + "src": "callback.call(obj[i], i, obj[i]) === false", + "evalFalse": 610, + "evalTrue": 0 + } + ], + "385": [ + null, + { + "position": 10, + "nodeLength": 12, + "src": "text == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "392": [ + null, + { + "position": 13, + "nodeLength": 13, + "src": "results || []", + "evalFalse": 0, + "evalTrue": 37 + } + ], + "394": [ + null, + { + "position": 36, + "nodeLength": 11, + "src": "arr != null", + "evalFalse": 0, + "evalTrue": 37 + } + ], + "395": [ + null, + { + "position": 9, + "nodeLength": 28, + "src": "isArrayLike(Object(arr))", + "evalFalse": 18, + "evalTrue": 19 + } + ], + "397": [ + null, + { + "position": 23, + "nodeLength": 23, + "src": "typeof arr === \"string\"", + "evalFalse": 19, + "evalTrue": 0 + } + ], + "409": [ + null, + { + "position": 10, + "nodeLength": 11, + "src": "arr == null", + "evalFalse": 38, + "evalTrue": 0 + } + ], + "419": [ + null, + { + "position": 71, + "nodeLength": 7, + "src": "j < len", + "evalFalse": 230, + "evalTrue": 249 + } + ], + "437": [ + null, + { + "position": 204, + "nodeLength": 10, + "src": "i < length", + "evalFalse": 20, + "evalTrue": 63 + } + ], + "439": [ + null, + { + "position": 58, + "nodeLength": 34, + "src": "callbackInverse !== callbackExpect", + "evalFalse": 2, + "evalTrue": 61 + } + ], + "454": [ + null, + { + "position": 130, + "nodeLength": 20, + "src": "isArrayLike(elems)", + "evalFalse": 0, + "evalTrue": 57 + } + ], + "456": [ + null, + { + "position": 38, + "nodeLength": 10, + "src": "i < length", + "evalFalse": 57, + "evalTrue": 46 + } + ], + "459": [ + null, + { + "position": 55, + "nodeLength": 13, + "src": "value != null", + "evalFalse": 0, + "evalTrue": 46 + } + ], + "469": [ + null, + { + "position": 55, + "nodeLength": 13, + "src": "value != null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "487": [ + null, + { + "position": 33, + "nodeLength": 27, + "src": "typeof context === \"string\"", + "evalFalse": 37, + "evalTrue": 0 + } + ], + "495": [ + null, + { + "position": 261, + "nodeLength": 24, + "src": "!jQuery.isFunction(fn)", + "evalFalse": 36, + "evalTrue": 1 + } + ], + "502": [ + null, + { + "position": 21, + "nodeLength": 15, + "src": "context || this", + "evalFalse": 0, + "evalTrue": 9 + } + ], + "506": [ + null, + { + "position": 596, + "nodeLength": 24, + "src": "fn.guid || jQuery.guid++", + "evalFalse": 0, + "evalTrue": 36 + } + ], + "518": [ + null, + { + "position": 11002, + "nodeLength": 28, + "src": "typeof Symbol === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "534": [ + null, + { + "position": 216, + "nodeLength": 38, + "src": "!!obj && \"length\" in obj && obj.length", + "evalFalse": 124, + "evalTrue": 286 + }, + { + "position": 225, + "nodeLength": 29, + "src": "\"length\" in obj && obj.length", + "evalFalse": 124, + "evalTrue": 286 + } + ], + "537": [ + null, + { + "position": 292, + "nodeLength": 45, + "src": "type === \"function\" || jQuery.isWindow(obj)", + "evalFalse": 392, + "evalTrue": 18 + }, + { + "position": 292, + "nodeLength": 19, + "src": "type === \"function\"", + "evalFalse": 410, + "evalTrue": 0 + } + ], + "541": [ + null, + { + "position": 370, + "nodeLength": 103, + "src": "type === \"array\" || length === 0 || typeof length === \"number\" && length > 0 && (length - 1) in obj", + "evalFalse": 44, + "evalTrue": 348 + }, + { + "position": 370, + "nodeLength": 16, + "src": "type === \"array\"", + "evalFalse": 325, + "evalTrue": 67 + }, + { + "position": 390, + "nodeLength": 83, + "src": "length === 0 || typeof length === \"number\" && length > 0 && (length - 1) in obj", + "evalFalse": 44, + "evalTrue": 281 + }, + { + "position": 390, + "nodeLength": 12, + "src": "length === 0", + "evalFalse": 271, + "evalTrue": 54 + } + ], + "542": [ + null, + { + "position": 17, + "nodeLength": 65, + "src": "typeof length === \"number\" && length > 0 && (length - 1) in obj", + "evalFalse": 44, + "evalTrue": 227 + }, + { + "position": 411, + "nodeLength": 26, + "src": "typeof length === \"number\"", + "evalFalse": 44, + "evalTrue": 227 + }, + { + "position": 29, + "nodeLength": 35, + "src": "length > 0 && (length - 1) in obj", + "evalFalse": 0, + "evalTrue": 227 + }, + { + "position": 442, + "nodeLength": 10, + "src": "length > 0", + "evalFalse": 0, + "evalTrue": 227 + } + ], + "588": [ + null, + { + "position": 8, + "nodeLength": 7, + "src": "a === b", + "evalFalse": 60, + "evalTrue": 4 + } + ], + "606": [ + null, + { + "position": 46, + "nodeLength": 7, + "src": "i < len", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "607": [ + null, + { + "position": 9, + "nodeLength": 16, + "src": "list[i] === elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "688": [ + null, + { + "position": 161, + "nodeLength": 34, + "src": "high !== high || escapedWhitespace", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 161, + "nodeLength": 13, + "src": "high !== high", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "690": [ + null, + { + "position": 52, + "nodeLength": 8, + "src": "high < 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "701": [ + null, + { + "position": 8, + "nodeLength": 11, + "src": "asCodePoint", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "704": [ + null, + { + "position": 65, + "nodeLength": 11, + "src": "ch === \"\\x00\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "726": [ + null, + { + "position": 11, + "nodeLength": 61, + "src": "elem.disabled === true && (\"form\" in elem || \"label\" in elem)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11, + "nodeLength": 22, + "src": "elem.disabled === true", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38, + "nodeLength": 33, + "src": "\"form\" in elem || \"label\" in elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "741": [ + null, + { + "position": 18, + "nodeLength": 10, + "src": "arr.length", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "754": [ + null, + { + "position": 84, + "nodeLength": 23, + "src": "(target[j++] = els[i++])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "762": [ + null, + { + "position": 63, + "nodeLength": 32, + "src": "context && context.ownerDocument", + "evalFalse": 29, + "evalTrue": 82 + } + ], + "765": [ + null, + { + "position": 175, + "nodeLength": 7, + "src": "context", + "evalFalse": 16, + "evalTrue": 95 + } + ], + "767": [ + null, + { + "position": 222, + "nodeLength": 13, + "src": "results || []", + "evalFalse": 0, + "evalTrue": 111 + } + ], + "770": [ + null, + { + "position": 305, + "nodeLength": 98, + "src": "typeof selector !== \"string\" || !selector || nodeType !== 1 && nodeType !== 9 && nodeType !== 11", + "evalFalse": 111, + "evalTrue": 0 + }, + { + "position": 305, + "nodeLength": 28, + "src": "typeof selector !== \"string\"", + "evalFalse": 111, + "evalTrue": 0 + }, + { + "position": 337, + "nodeLength": 66, + "src": "!selector || nodeType !== 1 && nodeType !== 9 && nodeType !== 11", + "evalFalse": 111, + "evalTrue": 0 + } + ], + "771": [ + null, + { + "position": 14, + "nodeLength": 51, + "src": "nodeType !== 1 && nodeType !== 9 && nodeType !== 11", + "evalFalse": 111, + "evalTrue": 0 + }, + { + "position": 355, + "nodeLength": 14, + "src": "nodeType !== 1", + "evalFalse": 82, + "evalTrue": 29 + }, + { + "position": 17, + "nodeLength": 33, + "src": "nodeType !== 9 && nodeType !== 11", + "evalFalse": 29, + "evalTrue": 0 + }, + { + "position": 374, + "nodeLength": 14, + "src": "nodeType !== 9", + "evalFalse": 29, + "evalTrue": 0 + }, + { + "position": 17, + "nodeLength": 15, + "src": "nodeType !== 11", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "777": [ + null, + { + "position": 515, + "nodeLength": 5, + "src": "!seed", + "evalFalse": 16, + "evalTrue": 95 + } + ], + "779": [ + null, + { + "position": 11, + "nodeLength": 72, + "src": "(context ? context.ownerDocument || context : preferredDoc) !== document", + "evalFalse": 95, + "evalTrue": 0 + }, + { + "position": 11, + "nodeLength": 7, + "src": "context", + "evalFalse": 0, + "evalTrue": 95 + }, + { + "position": 21, + "nodeLength": 32, + "src": "context.ownerDocument || context", + "evalFalse": 0, + "evalTrue": 95 + } + ], + "782": [ + null, + { + "position": 131, + "nodeLength": 19, + "src": "context || document", + "evalFalse": 0, + "evalTrue": 95 + } + ], + "784": [ + null, + { + "position": 160, + "nodeLength": 14, + "src": "documentIsHTML", + "evalFalse": 0, + "evalTrue": 95 + } + ], + "788": [ + null, + { + "position": 163, + "nodeLength": 56, + "src": "nodeType !== 11 && (match = rquickExpr.exec(selector))", + "evalFalse": 60, + "evalTrue": 35 + }, + { + "position": 163, + "nodeLength": 15, + "src": "nodeType !== 11", + "evalFalse": 0, + "evalTrue": 95 + } + ], + "791": [ + null, + { + "position": 31, + "nodeLength": 13, + "src": "(m = match[1])", + "evalFalse": 35, + "evalTrue": 0 + } + ], + "794": [ + null, + { + "position": 37, + "nodeLength": 14, + "src": "nodeType === 9", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "795": [ + null, + { + "position": 13, + "nodeLength": 35, + "src": "(elem = context.getElementById(m))", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "800": [ + null, + { + "position": 151, + "nodeLength": 13, + "src": "elem.id === m", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "814": [ + null, + { + "position": 147, + "nodeLength": 113, + "src": "newContext && (elem = newContext.getElementById(m)) && contains(context, elem) && elem.id === m", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 162, + "nodeLength": 98, + "src": "(elem = newContext.getElementById(m)) && contains(context, elem) && elem.id === m", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "815": [ + null, + { + "position": 48, + "nodeLength": 49, + "src": "contains(context, elem) && elem.id === m", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "816": [ + null, + { + "position": 35, + "nodeLength": 13, + "src": "elem.id === m", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "824": [ + null, + { + "position": 843, + "nodeLength": 8, + "src": "match[2]", + "evalFalse": 28, + "evalTrue": 7 + } + ], + "829": [ + null, + { + "position": 988, + "nodeLength": 86, + "src": "(m = match[3]) && support.getElementsByClassName && context.getElementsByClassName", + "evalFalse": 0, + "evalTrue": 28 + }, + { + "position": 1005, + "nodeLength": 69, + "src": "support.getElementsByClassName && context.getElementsByClassName", + "evalFalse": 0, + "evalTrue": 28 + } + ], + "838": [ + null, + { + "position": 1448, + "nodeLength": 102, + "src": "support.qsa && !compilerCache[selector + \" \"] && (!rbuggyQSA || !rbuggyQSA.test(selector))", + "evalFalse": 4, + "evalTrue": 56 + } + ], + "839": [ + null, + { + "position": 18, + "nodeLength": 83, + "src": "!compilerCache[selector + \" \"] && (!rbuggyQSA || !rbuggyQSA.test(selector))", + "evalFalse": 4, + "evalTrue": 56 + } + ], + "840": [ + null, + { + "position": 40, + "nodeLength": 41, + "src": "!rbuggyQSA || !rbuggyQSA.test(selector)", + "evalFalse": 0, + "evalTrue": 56 + } + ], + "842": [ + null, + { + "position": 11, + "nodeLength": 14, + "src": "nodeType !== 1", + "evalFalse": 48, + "evalTrue": 8 + } + ], + "850": [ + null, + { + "position": 287, + "nodeLength": 43, + "src": "context.nodeName.toLowerCase() !== \"object\"", + "evalFalse": 0, + "evalTrue": 48 + } + ], + "853": [ + null, + { + "position": 75, + "nodeLength": 35, + "src": "(nid = context.getAttribute(\"id\"))", + "evalFalse": 48, + "evalTrue": 0 + } + ], + "862": [ + null, + { + "position": 356, + "nodeLength": 3, + "src": "i--", + "evalFalse": 48, + "evalTrue": 120 + } + ], + "868": [ + null, + { + "position": 535, + "nodeLength": 79, + "src": "rsibling.test(selector) && testContext(context.parentNode) || context", + "evalFalse": 0, + "evalTrue": 48 + }, + { + "position": 535, + "nodeLength": 62, + "src": "rsibling.test(selector) && testContext(context.parentNode)", + "evalFalse": 48, + "evalTrue": 0 + } + ], + "872": [ + null, + { + "position": 965, + "nodeLength": 11, + "src": "newSelector", + "evalFalse": 0, + "evalTrue": 56 + } + ], + "880": [ + null, + { + "position": 12, + "nodeLength": 15, + "src": "nid === expando", + "evalFalse": 8, + "evalTrue": 48 + } + ], + "904": [ + null, + { + "position": 98, + "nodeLength": 41, + "src": "keys.push(key + \" \") > Expr.cacheLength", + "evalFalse": 20, + "evalTrue": 0 + } + ], + "935": [ + null, + { + "position": 47, + "nodeLength": 13, + "src": "el.parentNode", + "evalFalse": 7, + "evalTrue": 3 + } + ], + "952": [ + null, + { + "position": 58, + "nodeLength": 3, + "src": "i--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "964": [ + null, + { + "position": 12, + "nodeLength": 6, + "src": "b && a", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "965": [ + null, + { + "position": 26, + "nodeLength": 79, + "src": "cur && a.nodeType === 1 && b.nodeType === 1 && a.sourceIndex - b.sourceIndex", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33, + "nodeLength": 72, + "src": "a.nodeType === 1 && b.nodeType === 1 && a.sourceIndex - b.sourceIndex", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33, + "nodeLength": 16, + "src": "a.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53, + "nodeLength": 52, + "src": "b.nodeType === 1 && a.sourceIndex - b.sourceIndex", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53, + "nodeLength": 16, + "src": "b.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "969": [ + null, + { + "position": 167, + "nodeLength": 4, + "src": "diff", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "974": [ + null, + { + "position": 226, + "nodeLength": 3, + "src": "cur", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "975": [ + null, + { + "position": 12, + "nodeLength": 22, + "src": "(cur = cur.nextSibling)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "976": [ + null, + { + "position": 9, + "nodeLength": 9, + "src": "cur === b", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "982": [ + null, + { + "position": 330, + "nodeLength": 1, + "src": "a", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "992": [ + null, + { + "position": 52, + "nodeLength": 38, + "src": "name === \"input\" && elem.type === type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52, + "nodeLength": 16, + "src": "name === \"input\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 72, + "nodeLength": 18, + "src": "elem.type === type", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1003": [ + null, + { + "position": 53, + "nodeLength": 60, + "src": "(name === \"input\" || name === \"button\") && elem.type === type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53, + "nodeLength": 37, + "src": "name === \"input\" || name === \"button\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53, + "nodeLength": 16, + "src": "name === \"input\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73, + "nodeLength": 17, + "src": "name === \"button\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95, + "nodeLength": 18, + "src": "elem.type === type", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1019": [ + null, + { + "position": 221, + "nodeLength": 14, + "src": "\"form\" in elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1028": [ + null, + { + "position": 476, + "nodeLength": 42, + "src": "elem.parentNode && elem.disabled === false", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 495, + "nodeLength": 23, + "src": "elem.disabled === false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1031": [ + null, + { + "position": 72, + "nodeLength": 15, + "src": "\"label\" in elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1032": [ + null, + { + "position": 11, + "nodeLength": 26, + "src": "\"label\" in elem.parentNode", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1033": [ + null, + { + "position": 14, + "nodeLength": 37, + "src": "elem.parentNode.disabled === disabled", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1035": [ + null, + { + "position": 14, + "nodeLength": 26, + "src": "elem.disabled === disabled", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1041": [ + null, + { + "position": 376, + "nodeLength": 191, + "src": "elem.isDisabled === disabled || elem.isDisabled !== !disabled && disabledAncestor(elem) === disabled", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 376, + "nodeLength": 28, + "src": "elem.isDisabled === disabled", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1045": [ + null, + { + "position": 114, + "nodeLength": 76, + "src": "elem.isDisabled !== !disabled && disabledAncestor(elem) === disabled", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 493, + "nodeLength": 29, + "src": "elem.isDisabled !== !disabled", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1046": [ + null, + { + "position": 38, + "nodeLength": 37, + "src": "disabledAncestor(elem) === disabled", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1049": [ + null, + { + "position": 1107, + "nodeLength": 26, + "src": "elem.disabled === disabled", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1054": [ + null, + { + "position": 1628, + "nodeLength": 15, + "src": "\"label\" in elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1055": [ + null, + { + "position": 11, + "nodeLength": 26, + "src": "elem.disabled === disabled", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1076": [ + null, + { + "position": 156, + "nodeLength": 3, + "src": "i--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1077": [ + null, + { + "position": 10, + "nodeLength": 29, + "src": "seed[(j = matchIndexes[i])]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1091": [ + null, + { + "position": 9, + "nodeLength": 73, + "src": "context && typeof context.getElementsByTagName !== \"undefined\" && context", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20, + "nodeLength": 62, + "src": "typeof context.getElementsByTagName !== \"undefined\" && context", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20, + "nodeLength": 51, + "src": "typeof context.getElementsByTagName !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1105": [ + null, + { + "position": 137, + "nodeLength": 52, + "src": "elem && (elem.ownerDocument || elem).documentElement", + "evalFalse": 0, + "evalTrue": 172 + }, + { + "position": 146, + "nodeLength": 26, + "src": "elem.ownerDocument || elem", + "evalFalse": 0, + "evalTrue": 172 + } + ], + "1106": [ + null, + { + "position": 199, + "nodeLength": 15, + "src": "documentElement", + "evalFalse": 0, + "evalTrue": 172 + }, + { + "position": 217, + "nodeLength": 35, + "src": "documentElement.nodeName !== \"HTML\"", + "evalFalse": 172, + "evalTrue": 0 + } + ], + "1116": [ + null, + { + "position": 34, + "nodeLength": 4, + "src": "node", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 41, + "nodeLength": 26, + "src": "node.ownerDocument || node", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1119": [ + null, + { + "position": 149, + "nodeLength": 62, + "src": "doc === document || doc.nodeType !== 9 || !doc.documentElement", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 149, + "nodeLength": 16, + "src": "doc === document", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 169, + "nodeLength": 42, + "src": "doc.nodeType !== 9 || !doc.documentElement", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 169, + "nodeLength": 18, + "src": "doc.nodeType !== 9", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1130": [ + null, + { + "position": 487, + "nodeLength": 96, + "src": "preferredDoc !== document && (subWindow = document.defaultView) && subWindow.top !== subWindow", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 487, + "nodeLength": 25, + "src": "preferredDoc !== document", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1131": [ + null, + { + "position": 31, + "nodeLength": 64, + "src": "(subWindow = document.defaultView) && subWindow.top !== subWindow", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 558, + "nodeLength": 27, + "src": "subWindow.top !== subWindow", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1134": [ + null, + { + "position": 35, + "nodeLength": 26, + "src": "subWindow.addEventListener", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1138": [ + null, + { + "position": 176, + "nodeLength": 21, + "src": "subWindow.attachEvent", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1172": [ + null, + { + "position": 52, + "nodeLength": 76, + "src": "!document.getElementsByName || !document.getElementsByName(expando).length", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1176": [ + null, + { + "position": 2000, + "nodeLength": 15, + "src": "support.getById", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1180": [ + null, + { + "position": 12, + "nodeLength": 34, + "src": "elem.getAttribute(\"id\") === attrId", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1184": [ + null, + { + "position": 9, + "nodeLength": 63, + "src": "typeof context.getElementById !== \"undefined\" && documentIsHTML", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9, + "nodeLength": 45, + "src": "typeof context.getElementById !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1186": [ + null, + { + "position": 57, + "nodeLength": 4, + "src": "elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1193": [ + null, + { + "position": 16, + "nodeLength": 80, + "src": "typeof elem.getAttributeNode !== \"undefined\" && elem.getAttributeNode(\"id\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16, + "nodeLength": 44, + "src": "typeof elem.getAttributeNode !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1195": [ + null, + { + "position": 109, + "nodeLength": 29, + "src": "node && node.value === attrId", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 117, + "nodeLength": 21, + "src": "node.value === attrId", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1202": [ + null, + { + "position": 9, + "nodeLength": 63, + "src": "typeof context.getElementById !== \"undefined\" && documentIsHTML", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9, + "nodeLength": 45, + "src": "typeof context.getElementById !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1206": [ + null, + { + "position": 77, + "nodeLength": 4, + "src": "elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1210": [ + null, + { + "position": 85, + "nodeLength": 25, + "src": "node && node.value === id", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93, + "nodeLength": 17, + "src": "node.value === id", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1217": [ + null, + { + "position": 257, + "nodeLength": 18, + "src": "(elem = elems[i++])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1219": [ + null, + { + "position": 54, + "nodeLength": 25, + "src": "node && node.value === id", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62, + "nodeLength": 17, + "src": "node.value === id", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1231": [ + null, + { + "position": 3445, + "nodeLength": 28, + "src": "support.getElementsByTagName", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1233": [ + null, + { + "position": 9, + "nodeLength": 51, + "src": "typeof context.getElementsByTagName !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 5 + } + ], + "1237": [ + null, + { + "position": 176, + "nodeLength": 11, + "src": "support.qsa", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1250": [ + null, + { + "position": 218, + "nodeLength": 11, + "src": "tag === \"*\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1251": [ + null, + { + "position": 14, + "nodeLength": 20, + "src": "(elem = results[i++])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1252": [ + null, + { + "position": 11, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1263": [ + null, + { + "position": 4201, + "nodeLength": 211, + "src": "support.getElementsByClassName && function(className, context) {\n if (typeof context.getElementsByClassName !== \"undefined\" && documentIsHTML) {\n return context.getElementsByClassName(className);\n }\n}", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1264": [ + null, + { + "position": 8, + "nodeLength": 71, + "src": "typeof context.getElementsByClassName !== \"undefined\" && documentIsHTML", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8, + "nodeLength": 53, + "src": "typeof context.getElementsByClassName !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1284": [ + null, + { + "position": 4973, + "nodeLength": 56, + "src": "(support.qsa = rnative.test(document.querySelectorAll))", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1301": [ + null, + { + "position": 683, + "nodeLength": 50, + "src": "el.querySelectorAll(\"[msallowcapture^='']\").length", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1307": [ + null, + { + "position": 896, + "nodeLength": 41, + "src": "!el.querySelectorAll(\"[selected]\").length", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1312": [ + null, + { + "position": 1109, + "nodeLength": 55, + "src": "!el.querySelectorAll(\"[id~=\" + expando + \"-]\").length", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1319": [ + null, + { + "position": 1404, + "nodeLength": 39, + "src": "!el.querySelectorAll(\":checked\").length", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1326": [ + null, + { + "position": 1642, + "nodeLength": 52, + "src": "!el.querySelectorAll(\"a#\" + expando + \"+*\").length", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1343": [ + null, + { + "position": 454, + "nodeLength": 38, + "src": "el.querySelectorAll(\"[name=d]\").length", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1349": [ + null, + { + "position": 717, + "nodeLength": 44, + "src": "el.querySelectorAll(\":enabled\").length !== 2", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1356": [ + null, + { + "position": 978, + "nodeLength": 45, + "src": "el.querySelectorAll(\":disabled\").length !== 2", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1366": [ + null, + { + "position": 8114, + "nodeLength": 198, + "src": "(support.matchesSelector = rnative.test((matches = docElem.matches || docElem.webkitMatchesSelector || docElem.mozMatchesSelector || docElem.oMatchesSelector || docElem.msMatchesSelector)))", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 8165, + "nodeLength": 143, + "src": "docElem.matches || docElem.webkitMatchesSelector || docElem.mozMatchesSelector || docElem.oMatchesSelector || docElem.msMatchesSelector", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1367": [ + null, + { + "position": 20, + "nodeLength": 122, + "src": "docElem.webkitMatchesSelector || docElem.mozMatchesSelector || docElem.oMatchesSelector || docElem.msMatchesSelector", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1368": [ + null, + { + "position": 34, + "nodeLength": 87, + "src": "docElem.mozMatchesSelector || docElem.oMatchesSelector || docElem.msMatchesSelector", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1369": [ + null, + { + "position": 31, + "nodeLength": 55, + "src": "docElem.oMatchesSelector || docElem.msMatchesSelector", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1384": [ + null, + { + "position": 8685, + "nodeLength": 53, + "src": "rbuggyQSA.length && new RegExp(rbuggyQSA.join(\"|\"))", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1385": [ + null, + { + "position": 8757, + "nodeLength": 61, + "src": "rbuggyMatches.length && new RegExp(rbuggyMatches.join(\"|\"))", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1394": [ + null, + { + "position": 9092, + "nodeLength": 46, + "src": "hasCompare || rnative.test(docElem.contains)", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1396": [ + null, + { + "position": 16, + "nodeLength": 16, + "src": "a.nodeType === 9", + "evalFalse": 0, + "evalTrue": 68 + } + ], + "1397": [ + null, + { + "position": 63, + "nodeLength": 17, + "src": "b && b.parentNode", + "evalFalse": 27, + "evalTrue": 41 + } + ], + "1398": [ + null, + { + "position": 97, + "nodeLength": 175, + "src": "a === bup || !!(bup && bup.nodeType === 1 && (adown.contains ? adown.contains(bup) : a.compareDocumentPosition && a.compareDocumentPosition(bup) & 16))", + "evalFalse": 35, + "evalTrue": 33 + }, + { + "position": 97, + "nodeLength": 9, + "src": "a === bup", + "evalFalse": 68, + "evalTrue": 0 + }, + { + "position": 114, + "nodeLength": 157, + "src": "bup && bup.nodeType === 1 && (adown.contains ? adown.contains(bup) : a.compareDocumentPosition && a.compareDocumentPosition(bup) & 16)", + "evalFalse": 35, + "evalTrue": 33 + }, + { + "position": 121, + "nodeLength": 150, + "src": "bup.nodeType === 1 && (adown.contains ? adown.contains(bup) : a.compareDocumentPosition && a.compareDocumentPosition(bup) & 16)", + "evalFalse": 8, + "evalTrue": 33 + }, + { + "position": 121, + "nodeLength": 18, + "src": "bup.nodeType === 1", + "evalFalse": 6, + "evalTrue": 35 + } + ], + "1399": [ + null, + { + "position": -1, + "nodeLength": 14, + "src": "adown.contains", + "evalFalse": 0, + "evalTrue": 35 + } + ], + "1401": [ + null, + { + "position": 50, + "nodeLength": 66, + "src": "a.compareDocumentPosition && a.compareDocumentPosition(bup) & 16", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1405": [ + null, + { + "position": 9, + "nodeLength": 1, + "src": "b", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1406": [ + null, + { + "position": 14, + "nodeLength": 17, + "src": "(b = b.parentNode)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1407": [ + null, + { + "position": 11, + "nodeLength": 7, + "src": "b === a", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1419": [ + null, + { + "position": 9721, + "nodeLength": 10, + "src": "hasCompare", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1423": [ + null, + { + "position": 41, + "nodeLength": 7, + "src": "a === b", + "evalFalse": 57, + "evalTrue": 19 + } + ], + "1430": [ + null, + { + "position": 251, + "nodeLength": 7, + "src": "compare", + "evalFalse": 57, + "evalTrue": 0 + } + ], + "1435": [ + null, + { + "position": 368, + "nodeLength": 51, + "src": "(a.ownerDocument || a) === (b.ownerDocument || b)", + "evalFalse": 0, + "evalTrue": 57 + }, + { + "position": 368, + "nodeLength": 20, + "src": "a.ownerDocument || a", + "evalFalse": 0, + "evalTrue": 57 + }, + { + "position": 397, + "nodeLength": 20, + "src": "b.ownerDocument || b", + "evalFalse": 0, + "evalTrue": 57 + } + ], + "1442": [ + null, + { + "position": 543, + "nodeLength": 87, + "src": "compare & 1 || (!support.sortDetached && b.compareDocumentPosition(a) === compare)", + "evalFalse": 57, + "evalTrue": 0 + } + ], + "1443": [ + null, + { + "position": 18, + "nodeLength": 67, + "src": "!support.sortDetached && b.compareDocumentPosition(a) === compare", + "evalFalse": 57, + "evalTrue": 0 + }, + { + "position": 43, + "nodeLength": 42, + "src": "b.compareDocumentPosition(a) === compare", + "evalFalse": 57, + "evalTrue": 0 + } + ], + "1446": [ + null, + { + "position": 83, + "nodeLength": 79, + "src": "a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83, + "nodeLength": 14, + "src": "a === document", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 101, + "nodeLength": 61, + "src": "a.ownerDocument === preferredDoc && contains(preferredDoc, a)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 101, + "nodeLength": 32, + "src": "a.ownerDocument === preferredDoc", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1449": [ + null, + { + "position": 195, + "nodeLength": 79, + "src": "b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 195, + "nodeLength": 14, + "src": "b === document", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 213, + "nodeLength": 61, + "src": "b.ownerDocument === preferredDoc && contains(preferredDoc, b)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 213, + "nodeLength": 32, + "src": "b.ownerDocument === preferredDoc", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1454": [ + null, + { + "position": 339, + "nodeLength": 9, + "src": "sortInput", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1459": [ + null, + { + "position": 1066, + "nodeLength": 11, + "src": "compare & 4", + "evalFalse": 57, + "evalTrue": 0 + } + ], + "1463": [ + null, + { + "position": 51, + "nodeLength": 7, + "src": "a === b", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1476": [ + null, + { + "position": 269, + "nodeLength": 12, + "src": "!aup || !bup", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1477": [ + null, + { + "position": 11, + "nodeLength": 14, + "src": "a === document", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1478": [ + null, + { + "position": 25, + "nodeLength": 14, + "src": "b === document", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1479": [ + null, + { + "position": 24, + "nodeLength": 3, + "src": "aup", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1480": [ + null, + { + "position": 14, + "nodeLength": 3, + "src": "bup", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1481": [ + null, + { + "position": 13, + "nodeLength": 9, + "src": "sortInput", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1486": [ + null, + { + "position": 527, + "nodeLength": 11, + "src": "aup === bup", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1492": [ + null, + { + "position": 669, + "nodeLength": 21, + "src": "(cur = cur.parentNode)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1496": [ + null, + { + "position": 743, + "nodeLength": 21, + "src": "(cur = cur.parentNode)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1501": [ + null, + { + "position": 856, + "nodeLength": 15, + "src": "ap[i] === bp[i]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1505": [ + null, + { + "position": 898, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1510": [ + null, + { + "position": 151, + "nodeLength": 22, + "src": "ap[i] === preferredDoc", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1511": [ + null, + { + "position": 32, + "nodeLength": 22, + "src": "bp[i] === preferredDoc", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1524": [ + null, + { + "position": 41, + "nodeLength": 41, + "src": "(elem.ownerDocument || elem) !== document", + "evalFalse": 19, + "evalTrue": 0 + }, + { + "position": 41, + "nodeLength": 26, + "src": "elem.ownerDocument || elem", + "evalFalse": 0, + "evalTrue": 19 + } + ], + "1531": [ + null, + { + "position": 223, + "nodeLength": 181, + "src": "support.matchesSelector && documentIsHTML && !compilerCache[expr + \" \"] && (!rbuggyMatches || !rbuggyMatches.test(expr)) && (!rbuggyQSA || !rbuggyQSA.test(expr))", + "evalFalse": 0, + "evalTrue": 19 + }, + { + "position": 250, + "nodeLength": 154, + "src": "documentIsHTML && !compilerCache[expr + \" \"] && (!rbuggyMatches || !rbuggyMatches.test(expr)) && (!rbuggyQSA || !rbuggyQSA.test(expr))", + "evalFalse": 0, + "evalTrue": 19 + } + ], + "1532": [ + null, + { + "position": 19, + "nodeLength": 134, + "src": "!compilerCache[expr + \" \"] && (!rbuggyMatches || !rbuggyMatches.test(expr)) && (!rbuggyQSA || !rbuggyQSA.test(expr))", + "evalFalse": 0, + "evalTrue": 19 + } + ], + "1533": [ + null, + { + "position": 35, + "nodeLength": 98, + "src": "(!rbuggyMatches || !rbuggyMatches.test(expr)) && (!rbuggyQSA || !rbuggyQSA.test(expr))", + "evalFalse": 0, + "evalTrue": 19 + }, + { + "position": 310, + "nodeLength": 45, + "src": "!rbuggyMatches || !rbuggyMatches.test(expr)", + "evalFalse": 0, + "evalTrue": 19 + } + ], + "1534": [ + null, + { + "position": 54, + "nodeLength": 41, + "src": "!rbuggyQSA || !rbuggyQSA.test(expr)", + "evalFalse": 0, + "evalTrue": 19 + } + ], + "1540": [ + null, + { + "position": 116, + "nodeLength": 177, + "src": "ret || support.disconnectedMatch || elem.document && elem.document.nodeType !== 11", + "evalFalse": 0, + "evalTrue": 19 + }, + { + "position": 123, + "nodeLength": 170, + "src": "support.disconnectedMatch || elem.document && elem.document.nodeType !== 11", + "evalFalse": 0, + "evalTrue": 15 + } + ], + "1543": [ + null, + { + "position": 123, + "nodeLength": 46, + "src": "elem.document && elem.document.nodeType !== 11", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 267, + "nodeLength": 29, + "src": "elem.document.nodeType !== 11", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1549": [ + null, + { + "position": 765, + "nodeLength": 51, + "src": "Sizzle(expr, document, null, [elem]).length > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1554": [ + null, + { + "position": 41, + "nodeLength": 47, + "src": "(context.ownerDocument || context) !== document", + "evalFalse": 68, + "evalTrue": 0 + }, + { + "position": 41, + "nodeLength": 32, + "src": "context.ownerDocument || context", + "evalFalse": 0, + "evalTrue": 68 + } + ], + "1562": [ + null, + { + "position": 41, + "nodeLength": 41, + "src": "(elem.ownerDocument || elem) !== document", + "evalFalse": 81, + "evalTrue": 0 + }, + { + "position": 41, + "nodeLength": 26, + "src": "elem.ownerDocument || elem", + "evalFalse": 0, + "evalTrue": 81 + } + ], + "1568": [ + null, + { + "position": 124, + "nodeLength": 56, + "src": "fn && hasOwn.call(Expr.attrHandle, name.toLowerCase())", + "evalFalse": 81, + "evalTrue": 0 + } + ], + "1572": [ + null, + { + "position": 361, + "nodeLength": 17, + "src": "val !== undefined", + "evalFalse": 81, + "evalTrue": 0 + } + ], + "1574": [ + null, + { + "position": 29, + "nodeLength": 37, + "src": "support.attributes || !documentIsHTML", + "evalFalse": 0, + "evalTrue": 81 + } + ], + "1576": [ + null, + { + "position": 74, + "nodeLength": 51, + "src": "(val = elem.getAttributeNode(name)) && val.specified", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1601": [ + null, + { + "position": 175, + "nodeLength": 41, + "src": "!support.sortStable && results.slice(0)", + "evalFalse": 20, + "evalTrue": 0 + } + ], + "1604": [ + null, + { + "position": 253, + "nodeLength": 12, + "src": "hasDuplicate", + "evalFalse": 17, + "evalTrue": 3 + } + ], + "1605": [ + null, + { + "position": 12, + "nodeLength": 20, + "src": "(elem = results[i++])", + "evalFalse": 3, + "evalTrue": 13 + } + ], + "1606": [ + null, + { + "position": 9, + "nodeLength": 21, + "src": "elem === results[i]", + "evalFalse": 3, + "evalTrue": 10 + } + ], + "1610": [ + null, + { + "position": 120, + "nodeLength": 3, + "src": "j--", + "evalFalse": 3, + "evalTrue": 10 + } + ], + "1632": [ + null, + { + "position": 68, + "nodeLength": 9, + "src": "!nodeType", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1634": [ + null, + { + "position": 65, + "nodeLength": 17, + "src": "(node = elem[i++])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1638": [ + null, + { + "position": 249, + "nodeLength": 51, + "src": "nodeType === 1 || nodeType === 9 || nodeType === 11", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 249, + "nodeLength": 14, + "src": "nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 267, + "nodeLength": 33, + "src": "nodeType === 9 || nodeType === 11", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 267, + "nodeLength": 14, + "src": "nodeType === 9", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 285, + "nodeLength": 15, + "src": "nodeType === 11", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1641": [ + null, + { + "position": 116, + "nodeLength": 36, + "src": "typeof elem.textContent === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1645": [ + null, + { + "position": 62, + "nodeLength": 4, + "src": "elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1649": [ + null, + { + "position": 646, + "nodeLength": 32, + "src": "nodeType === 3 || nodeType === 4", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 646, + "nodeLength": 14, + "src": "nodeType === 3", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 664, + "nodeLength": 14, + "src": "nodeType === 4", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1682": [ + null, + { + "position": 140, + "nodeLength": 38, + "src": "match[3] || match[4] || match[5] || \"\"", + "evalFalse": 2, + "evalTrue": 1 + }, + { + "position": 152, + "nodeLength": 26, + "src": "match[4] || match[5] || \"\"", + "evalFalse": 2, + "evalTrue": 1 + }, + { + "position": 164, + "nodeLength": 14, + "src": "match[5] || \"\"", + "evalFalse": 2, + "evalTrue": 1 + } + ], + "1684": [ + null, + { + "position": 223, + "nodeLength": 17, + "src": "match[2] === \"~=\"", + "evalFalse": 3, + "evalTrue": 0 + } + ], + "1704": [ + null, + { + "position": 343, + "nodeLength": 32, + "src": "match[1].slice(0, 3) === \"nth\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1706": [ + null, + { + "position": 41, + "nodeLength": 9, + "src": "!match[3]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1712": [ + null, + { + "position": 224, + "nodeLength": 8, + "src": "match[4]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 247, + "nodeLength": 13, + "src": "match[6] || 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 270, + "nodeLength": 41, + "src": "match[3] === \"even\" || match[3] === \"odd\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 270, + "nodeLength": 19, + "src": "match[3] === \"even\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 293, + "nodeLength": 18, + "src": "match[3] === \"odd\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1713": [ + null, + { + "position": 337, + "nodeLength": 43, + "src": "(match[7] + match[8]) || match[3] === \"odd\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 362, + "nodeLength": 18, + "src": "match[3] === \"odd\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1716": [ + null, + { + "position": 817, + "nodeLength": 8, + "src": "match[3]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1725": [ + null, + { + "position": 26, + "nodeLength": 21, + "src": "!match[6] && match[2]", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "1727": [ + null, + { + "position": 63, + "nodeLength": 35, + "src": "matchExpr[\"CHILD\"].test(match[0])", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "1732": [ + null, + { + "position": 170, + "nodeLength": 8, + "src": "match[3]", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "1733": [ + null, + { + "position": 16, + "nodeLength": 26, + "src": "match[4] || match[5] || \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28, + "nodeLength": 14, + "src": "match[5] || \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1736": [ + null, + { + "position": 297, + "nodeLength": 260, + "src": "unquoted && rpseudo.test(unquoted) && (excess = tokenize(unquoted, true)) && (excess = unquoted.indexOf(\")\", unquoted.length - excess) - unquoted.length)", + "evalFalse": 2, + "evalTrue": 0 + }, + { + "position": 309, + "nodeLength": 248, + "src": "rpseudo.test(unquoted) && (excess = tokenize(unquoted, true)) && (excess = unquoted.indexOf(\")\", unquoted.length - excess) - unquoted.length)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1738": [ + null, + { + "position": 78, + "nodeLength": 169, + "src": "(excess = tokenize(unquoted, true)) && (excess = unquoted.indexOf(\")\", unquoted.length - excess) - unquoted.length)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1756": [ + null, + { + "position": 93, + "nodeLength": 24, + "src": "nodeNameSelector === \"*\"", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "1759": [ + null, + { + "position": 13, + "nodeLength": 57, + "src": "elem.nodeName && elem.nodeName.toLowerCase() === nodeName", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30, + "nodeLength": 40, + "src": "elem.nodeName.toLowerCase() === nodeName", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1766": [ + null, + { + "position": 60, + "nodeLength": 322, + "src": "pattern || (pattern = new RegExp(\"(^|\" + whitespace + \")\" + className + \"(\" + whitespace + \"|$)\")) && classCache(className, function(elem) {\n return pattern.test(typeof elem.className === \"string\" && elem.className || typeof elem.getAttribute !== \"undefined\" && elem.getAttribute(\"class\") || \"\");\n})", + "evalFalse": 0, + "evalTrue": 2 + } + ], + "1767": [ + null, + { + "position": 15, + "nodeLength": 306, + "src": "(pattern = new RegExp(\"(^|\" + whitespace + \")\" + className + \"(\" + whitespace + \"|$)\")) && classCache(className, function(elem) {\n return pattern.test(typeof elem.className === \"string\" && elem.className || typeof elem.getAttribute !== \"undefined\" && elem.getAttribute(\"class\") || \"\");\n})", + "evalFalse": 0, + "evalTrue": 2 + } + ], + "1769": [ + null, + { + "position": 27, + "nodeLength": 132, + "src": "typeof elem.className === \"string\" && elem.className || typeof elem.getAttribute !== \"undefined\" && elem.getAttribute(\"class\") || \"\"", + "evalFalse": 13, + "evalTrue": 32 + }, + { + "position": 21, + "nodeLength": 52, + "src": "typeof elem.className === \"string\" && elem.className", + "evalFalse": 13, + "evalTrue": 32 + }, + { + "position": 22, + "nodeLength": 34, + "src": "typeof elem.className === \"string\"", + "evalFalse": 0, + "evalTrue": 45 + }, + { + "position": 55, + "nodeLength": 76, + "src": "typeof elem.getAttribute !== \"undefined\" && elem.getAttribute(\"class\") || \"\"", + "evalFalse": 13, + "evalTrue": 0 + }, + { + "position": 78, + "nodeLength": 70, + "src": "typeof elem.getAttribute !== \"undefined\" && elem.getAttribute(\"class\")", + "evalFalse": 13, + "evalTrue": 0 + }, + { + "position": 79, + "nodeLength": 40, + "src": "typeof elem.getAttribute !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 13 + } + ], + "1777": [ + null, + { + "position": 55, + "nodeLength": 14, + "src": "result == null", + "evalFalse": 0, + "evalTrue": 63 + } + ], + "1778": [ + null, + { + "position": 13, + "nodeLength": 17, + "src": "operator === \"!=\"", + "evalFalse": 0, + "evalTrue": 63 + } + ], + "1780": [ + null, + { + "position": 120, + "nodeLength": 9, + "src": "!operator", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1786": [ + null, + { + "position": 189, + "nodeLength": 16, + "src": "operator === \"=\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 208, + "nodeLength": 16, + "src": "result === check", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1787": [ + null, + { + "position": 42, + "nodeLength": 17, + "src": "operator === \"!=\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62, + "nodeLength": 16, + "src": "result !== check", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1788": [ + null, + { + "position": 43, + "nodeLength": 17, + "src": "operator === \"^=\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63, + "nodeLength": 38, + "src": "check && result.indexOf(check) === 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 72, + "nodeLength": 29, + "src": "result.indexOf(check) === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1789": [ + null, + { + "position": 65, + "nodeLength": 17, + "src": "operator === \"*=\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85, + "nodeLength": 37, + "src": "check && result.indexOf(check) > -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94, + "nodeLength": 28, + "src": "result.indexOf(check) > -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1790": [ + null, + { + "position": 64, + "nodeLength": 17, + "src": "operator === \"$=\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84, + "nodeLength": 48, + "src": "check && result.slice(-check.length) === check", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93, + "nodeLength": 39, + "src": "result.slice(-check.length) === check", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1791": [ + null, + { + "position": 75, + "nodeLength": 17, + "src": "operator === \"~=\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 97, + "nodeLength": 70, + "src": "(\" \" + result.replace(rwhitespace, \" \") + \" \").indexOf(check) > -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1792": [ + null, + { + "position": 99, + "nodeLength": 17, + "src": "operator === \"|=\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 119, + "nodeLength": 71, + "src": "result === check || result.slice(0, check.length + 1) === check + \"-\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 119, + "nodeLength": 16, + "src": "result === check", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 139, + "nodeLength": 51, + "src": "result.slice(0, check.length + 1) === check + \"-\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1798": [ + null, + { + "position": 17, + "nodeLength": 28, + "src": "type.slice(0, 3) !== \"nth\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1799": [ + null, + { + "position": 56, + "nodeLength": 27, + "src": "type.slice(-4) !== \"last\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1800": [ + null, + { + "position": 98, + "nodeLength": 18, + "src": "what === \"of-type\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1802": [ + null, + { + "position": 134, + "nodeLength": 25, + "src": "first === 1 && last === 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 134, + "nodeLength": 11, + "src": "first === 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 149, + "nodeLength": 10, + "src": "last === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1811": [ + null, + { + "position": 71, + "nodeLength": 18, + "src": "simple !== forward", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1813": [ + null, + { + "position": 172, + "nodeLength": 37, + "src": "ofType && elem.nodeName.toLowerCase()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1814": [ + null, + { + "position": 228, + "nodeLength": 15, + "src": "!xml && !ofType", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1817": [ + null, + { + "position": 283, + "nodeLength": 6, + "src": "parent", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1820": [ + null, + { + "position": 57, + "nodeLength": 6, + "src": "simple", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1821": [ + null, + { + "position": 16, + "nodeLength": 3, + "src": "dir", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1823": [ + null, + { + "position": 39, + "nodeLength": 19, + "src": "(node = node[dir])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1824": [ + null, + { + "position": 15, + "nodeLength": 87, + "src": "ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15, + "nodeLength": 6, + "src": "ofType", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1825": [ + null, + { + "position": 18, + "nodeLength": 36, + "src": "node.nodeName.toLowerCase() === name", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1826": [ + null, + { + "position": 67, + "nodeLength": 19, + "src": "node.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1832": [ + null, + { + "position": 306, + "nodeLength": 42, + "src": "type === \"only\" && !start && \"nextSibling\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 306, + "nodeLength": 15, + "src": "type === \"only\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 325, + "nodeLength": 23, + "src": "!start && \"nextSibling\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1837": [ + null, + { + "position": 494, + "nodeLength": 7, + "src": "forward", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1840": [ + null, + { + "position": 619, + "nodeLength": 19, + "src": "forward && useCache", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1846": [ + null, + { + "position": 134, + "nodeLength": 41, + "src": "node[expando] || (node[expando] = {})", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1850": [ + null, + { + "position": 291, + "nodeLength": 73, + "src": "outerCache[node.uniqueID] || (outerCache[node.uniqueID] = {})", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1853": [ + null, + { + "position": 382, + "nodeLength": 25, + "src": "uniqueCache[type] || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1854": [ + null, + { + "position": 428, + "nodeLength": 36, + "src": "cache[0] === dirruns && cache[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 428, + "nodeLength": 22, + "src": "cache[0] === dirruns", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1855": [ + null, + { + "position": 480, + "nodeLength": 23, + "src": "nodeIndex && cache[2]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1856": [ + null, + { + "position": 519, + "nodeLength": 43, + "src": "nodeIndex && parent.childNodes[nodeIndex]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1858": [ + null, + { + "position": 581, + "nodeLength": 145, + "src": "(node = ++nodeIndex && node && node[dir] || (diff = nodeIndex = 0) || start.pop())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 588, + "nodeLength": 137, + "src": "++nodeIndex && node && node[dir] || (diff = nodeIndex = 0) || start.pop()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 588, + "nodeLength": 34, + "src": "++nodeIndex && node && node[dir]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 603, + "nodeLength": 19, + "src": "node && node[dir]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1861": [ + null, + { + "position": 100, + "nodeLength": 36, + "src": "(diff = nodeIndex = 0) || start.pop()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1864": [ + null, + { + "position": 74, + "nodeLength": 46, + "src": "node.nodeType === 1 && ++diff && node === elem", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74, + "nodeLength": 19, + "src": "node.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 97, + "nodeLength": 23, + "src": "++diff && node === elem", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 107, + "nodeLength": 13, + "src": "node === elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1872": [ + null, + { + "position": 72, + "nodeLength": 8, + "src": "useCache", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1875": [ + null, + { + "position": 80, + "nodeLength": 41, + "src": "node[expando] || (node[expando] = {})", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1879": [ + null, + { + "position": 240, + "nodeLength": 74, + "src": "outerCache[node.uniqueID] || (outerCache[node.uniqueID] = {})", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1882": [ + null, + { + "position": 333, + "nodeLength": 25, + "src": "uniqueCache[type] || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1883": [ + null, + { + "position": 380, + "nodeLength": 36, + "src": "cache[0] === dirruns && cache[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 380, + "nodeLength": 22, + "src": "cache[0] === dirruns", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1889": [ + null, + { + "position": 643, + "nodeLength": 14, + "src": "diff === false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1891": [ + null, + { + "position": 86, + "nodeLength": 92, + "src": "(node = ++nodeIndex && node && node[dir] || (diff = nodeIndex = 0) || start.pop())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93, + "nodeLength": 84, + "src": "++nodeIndex && node && node[dir] || (diff = nodeIndex = 0) || start.pop()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93, + "nodeLength": 34, + "src": "++nodeIndex && node && node[dir]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 108, + "nodeLength": 19, + "src": "node && node[dir]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1892": [ + null, + { + "position": 47, + "nodeLength": 36, + "src": "(diff = nodeIndex = 0) || start.pop()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1894": [ + null, + { + "position": 18, + "nodeLength": 109, + "src": "(ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1) && ++diff", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18, + "nodeLength": 6, + "src": "ofType", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1895": [ + null, + { + "position": 18, + "nodeLength": 36, + "src": "node.nodeName.toLowerCase() === name", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1896": [ + null, + { + "position": 67, + "nodeLength": 19, + "src": "node.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1900": [ + null, + { + "position": 74, + "nodeLength": 8, + "src": "useCache", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1901": [ + null, + { + "position": 25, + "nodeLength": 41, + "src": "node[expando] || (node[expando] = {})", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1905": [ + null, + { + "position": 194, + "nodeLength": 77, + "src": "outerCache[node.uniqueID] || (outerCache[node.uniqueID] = {})", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1911": [ + null, + { + "position": 440, + "nodeLength": 13, + "src": "node === elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1921": [ + null, + { + "position": 3205, + "nodeLength": 61, + "src": "diff === first || (diff % first === 0 && diff / first >= 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3205, + "nodeLength": 14, + "src": "diff === first", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3225, + "nodeLength": 39, + "src": "diff % first === 0 && diff / first >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3225, + "nodeLength": 18, + "src": "diff % first === 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3247, + "nodeLength": 17, + "src": "diff / first >= 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1932": [ + null, + { + "position": 18, + "nodeLength": 121, + "src": "Expr.pseudos[pseudo] || Expr.setFilters[pseudo.toLowerCase()] || Sizzle.error(\"unsupported pseudo: \" + pseudo)", + "evalFalse": 0, + "evalTrue": 2 + }, + { + "position": 44, + "nodeLength": 95, + "src": "Expr.setFilters[pseudo.toLowerCase()] || Sizzle.error(\"unsupported pseudo: \" + pseudo)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1938": [ + null, + { + "position": 536, + "nodeLength": 13, + "src": "fn[expando]", + "evalFalse": 0, + "evalTrue": 2 + } + ], + "1943": [ + null, + { + "position": 641, + "nodeLength": 13, + "src": "fn.length > 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1945": [ + null, + { + "position": 57, + "nodeLength": 54, + "src": "Expr.setFilters.hasOwnProperty(pseudo.toLowerCase())", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1950": [ + null, + { + "position": 96, + "nodeLength": 3, + "src": "i--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1974": [ + null, + { + "position": 222, + "nodeLength": 18, + "src": "matcher[expando]", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "1981": [ + null, + { + "position": 149, + "nodeLength": 3, + "src": "i--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1982": [ + null, + { + "position": 13, + "nodeLength": 20, + "src": "(elem = unmatched[i])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1998": [ + null, + { + "position": 12, + "nodeLength": 35, + "src": "Sizzle(selector, elem).length > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2005": [ + null, + { + "position": 14, + "nodeLength": 76, + "src": "(elem.textContent || elem.innerText || getText(elem)).indexOf(text) > -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14, + "nodeLength": 53, + "src": "elem.textContent || elem.innerText || getText(elem)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34, + "nodeLength": 33, + "src": "elem.innerText || getText(elem)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2018": [ + null, + { + "position": 53, + "nodeLength": 29, + "src": "!ridentifier.test(lang || \"\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 71, + "nodeLength": 10, + "src": "lang || \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2025": [ + null, + { + "position": 12, + "nodeLength": 111, + "src": "(elemLang = documentIsHTML ? elem.lang : elem.getAttribute(\"xml:lang\") || elem.getAttribute(\"lang\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23, + "nodeLength": 14, + "src": "documentIsHTML", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2027": [ + null, + { + "position": 40, + "nodeLength": 58, + "src": "elem.getAttribute(\"xml:lang\") || elem.getAttribute(\"lang\")", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2030": [ + null, + { + "position": 56, + "nodeLength": 57, + "src": "elemLang === lang || elemLang.indexOf(lang + \"-\") === 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56, + "nodeLength": 17, + "src": "elemLang === lang", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 77, + "nodeLength": 36, + "src": "elemLang.indexOf(lang + \"-\") === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2032": [ + null, + { + "position": 267, + "nodeLength": 46, + "src": "(elem = elem.parentNode) && elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 319, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2039": [ + null, + { + "position": 15, + "nodeLength": 39, + "src": "window.location && window.location.hash", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2040": [ + null, + { + "position": 66, + "nodeLength": 35, + "src": "hash && hash.slice(1) === elem.id", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74, + "nodeLength": 27, + "src": "hash.slice(1) === elem.id", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2044": [ + null, + { + "position": 11, + "nodeLength": 16, + "src": "elem === docElem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2048": [ + null, + { + "position": 11, + "nodeLength": 126, + "src": "elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11, + "nodeLength": 31, + "src": "elem === document.activeElement", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47, + "nodeLength": 90, + "src": "(!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47, + "nodeLength": 41, + "src": "!document.hasFocus || document.hasFocus()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 96, + "nodeLength": 40, + "src": "elem.type || elem.href || ~elem.tabIndex", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 109, + "nodeLength": 27, + "src": "elem.href || ~elem.tabIndex", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2059": [ + null, + { + "position": 201, + "nodeLength": 85, + "src": "(nodeName === \"input\" && !!elem.checked) || (nodeName === \"option\" && !!elem.selected)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 201, + "nodeLength": 38, + "src": "nodeName === \"input\" && !!elem.checked", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 201, + "nodeLength": 20, + "src": "nodeName === \"input\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 245, + "nodeLength": 40, + "src": "nodeName === \"option\" && !!elem.selected", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 245, + "nodeLength": 21, + "src": "nodeName === \"option\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2065": [ + null, + { + "position": 103, + "nodeLength": 15, + "src": "elem.parentNode", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2069": [ + null, + { + "position": 174, + "nodeLength": 22, + "src": "elem.selected === true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2078": [ + null, + { + "position": 323, + "nodeLength": 4, + "src": "elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2079": [ + null, + { + "position": 10, + "nodeLength": 17, + "src": "elem.nodeType < 6", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2101": [ + null, + { + "position": 54, + "nodeLength": 63, + "src": "name === \"input\" && elem.type === \"button\" || name === \"button\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54, + "nodeLength": 42, + "src": "name === \"input\" && elem.type === \"button\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54, + "nodeLength": 16, + "src": "name === \"input\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74, + "nodeLength": 22, + "src": "elem.type === \"button\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 100, + "nodeLength": 17, + "src": "name === \"button\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2106": [ + null, + { + "position": 24, + "nodeLength": 260, + "src": "elem.nodeName.toLowerCase() === \"input\" && elem.type === \"text\" && ((attr = elem.getAttribute(\"type\")) == null || attr.toLowerCase() === \"text\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24, + "nodeLength": 39, + "src": "elem.nodeName.toLowerCase() === \"input\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2107": [ + null, + { + "position": 46, + "nodeLength": 213, + "src": "elem.type === \"text\" && ((attr = elem.getAttribute(\"type\")) == null || attr.toLowerCase() === \"text\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73, + "nodeLength": 20, + "src": "elem.type === \"text\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2111": [ + null, + { + "position": 136, + "nodeLength": 74, + "src": "(attr = elem.getAttribute(\"type\")) == null || attr.toLowerCase() === \"text\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 136, + "nodeLength": 41, + "src": "(attr = elem.getAttribute(\"type\")) == null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 181, + "nodeLength": 29, + "src": "attr.toLowerCase() === \"text\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2124": [ + null, + { + "position": 13, + "nodeLength": 12, + "src": "argument < 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2129": [ + null, + { + "position": 26, + "nodeLength": 10, + "src": "i < length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2137": [ + null, + { + "position": 26, + "nodeLength": 10, + "src": "i < length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2144": [ + null, + { + "position": 12, + "nodeLength": 12, + "src": "argument < 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2145": [ + null, + { + "position": 68, + "nodeLength": 8, + "src": "--i >= 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2152": [ + null, + { + "position": 12, + "nodeLength": 12, + "src": "argument < 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2153": [ + null, + { + "position": 68, + "nodeLength": 12, + "src": "++i < length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2181": [ + null, + { + "position": 113, + "nodeLength": 6, + "src": "cached", + "evalFalse": 10, + "evalTrue": 50 + } + ], + "2182": [ + null, + { + "position": 10, + "nodeLength": 9, + "src": "parseOnly", + "evalFalse": 50, + "evalTrue": 0 + } + ], + "2189": [ + null, + { + "position": 245, + "nodeLength": 5, + "src": "soFar", + "evalFalse": 10, + "evalTrue": 13 + } + ], + "2192": [ + null, + { + "position": 34, + "nodeLength": 42, + "src": "!matched || (match = rcomma.exec(soFar))", + "evalFalse": 0, + "evalTrue": 13 + } + ], + "2193": [ + null, + { + "position": 9, + "nodeLength": 5, + "src": "match", + "evalFalse": 10, + "evalTrue": 3 + } + ], + "2195": [ + null, + { + "position": 59, + "nodeLength": 39, + "src": "soFar.slice(match[0].length) || soFar", + "evalFalse": 0, + "evalTrue": 3 + } + ], + "2203": [ + null, + { + "position": 286, + "nodeLength": 35, + "src": "(match = rcombinators.exec(soFar))", + "evalFalse": 13, + "evalTrue": 0 + } + ], + "2215": [ + null, + { + "position": 10, + "nodeLength": 110, + "src": "(match = matchExpr[type].exec(soFar)) && (!preFilters[type] || (match = preFilters[type](match)))", + "evalFalse": 64, + "evalTrue": 14 + }, + { + "position": 55, + "nodeLength": 64, + "src": "!preFilters[type] || (match = preFilters[type](match))", + "evalFalse": 0, + "evalTrue": 14 + } + ], + "2227": [ + null, + { + "position": 872, + "nodeLength": 8, + "src": "!matched", + "evalFalse": 13, + "evalTrue": 0 + } + ], + "2235": [ + null, + { + "position": 1282, + "nodeLength": 9, + "src": "parseOnly", + "evalFalse": 10, + "evalTrue": 0 + } + ], + "2237": [ + null, + { + "position": 30, + "nodeLength": 5, + "src": "soFar", + "evalFalse": 10, + "evalTrue": 0 + } + ], + "2247": [ + null, + { + "position": 62, + "nodeLength": 7, + "src": "i < len", + "evalFalse": 120, + "evalTrue": 143 + } + ], + "2256": [ + null, + { + "position": 59, + "nodeLength": 11, + "src": "skip || dir", + "evalFalse": 0, + "evalTrue": 17 + } + ], + "2257": [ + null, + { + "position": 93, + "nodeLength": 28, + "src": "base && key === \"parentNode\"", + "evalFalse": 1, + "evalTrue": 16 + }, + { + "position": 101, + "nodeLength": 20, + "src": "key === \"parentNode\"", + "evalFalse": 0, + "evalTrue": 16 + } + ], + "2260": [ + null, + { + "position": 156, + "nodeLength": 16, + "src": "combinator.first", + "evalFalse": 17, + "evalTrue": 0 + } + ], + "2263": [ + null, + { + "position": 13, + "nodeLength": 19, + "src": "(elem = elem[dir])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2264": [ + null, + { + "position": 10, + "nodeLength": 39, + "src": "elem.nodeType === 1 || checkNonElements", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2277": [ + null, + { + "position": 184, + "nodeLength": 3, + "src": "xml", + "evalFalse": 63, + "evalTrue": 0 + } + ], + "2278": [ + null, + { + "position": 14, + "nodeLength": 19, + "src": "(elem = elem[dir])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2279": [ + null, + { + "position": 11, + "nodeLength": 39, + "src": "elem.nodeType === 1 || checkNonElements", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2280": [ + null, + { + "position": 12, + "nodeLength": 29, + "src": "matcher(elem, context, xml)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2286": [ + null, + { + "position": 14, + "nodeLength": 19, + "src": "(elem = elem[dir])", + "evalFalse": 0, + "evalTrue": 84 + } + ], + "2287": [ + null, + { + "position": 11, + "nodeLength": 39, + "src": "elem.nodeType === 1 || checkNonElements", + "evalFalse": 0, + "evalTrue": 84 + }, + { + "position": 11, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 84 + } + ], + "2288": [ + null, + { + "position": 20, + "nodeLength": 41, + "src": "elem[expando] || (elem[expando] = {})", + "evalFalse": 0, + "evalTrue": 84 + } + ], + "2292": [ + null, + { + "position": 174, + "nodeLength": 65, + "src": "outerCache[elem.uniqueID] || (outerCache[elem.uniqueID] = {})", + "evalFalse": 0, + "evalTrue": 84 + } + ], + "2294": [ + null, + { + "position": 253, + "nodeLength": 44, + "src": "skip && skip === elem.nodeName.toLowerCase()", + "evalFalse": 84, + "evalTrue": 0 + }, + { + "position": 261, + "nodeLength": 36, + "src": "skip === elem.nodeName.toLowerCase()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2295": [ + null, + { + "position": 15, + "nodeLength": 19, + "src": "elem[dir] || elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2296": [ + null, + { + "position": 357, + "nodeLength": 96, + "src": "(oldCache = uniqueCache[key]) && oldCache[0] === dirruns && oldCache[1] === doneName", + "evalFalse": 26, + "evalTrue": 58 + } + ], + "2297": [ + null, + { + "position": 40, + "nodeLength": 55, + "src": "oldCache[0] === dirruns && oldCache[1] === doneName", + "evalFalse": 18, + "evalTrue": 58 + }, + { + "position": 400, + "nodeLength": 25, + "src": "oldCache[0] === dirruns", + "evalFalse": 18, + "evalTrue": 58 + }, + { + "position": 28, + "nodeLength": 26, + "src": "oldCache[1] === doneName", + "evalFalse": 0, + "evalTrue": 58 + } + ], + "2306": [ + null, + { + "position": 199, + "nodeLength": 46, + "src": "(newCache[2] = matcher(elem, context, xml))", + "evalFalse": 21, + "evalTrue": 5 + } + ], + "2318": [ + null, + { + "position": 9, + "nodeLength": 19, + "src": "matchers.length > 1", + "evalFalse": 0, + "evalTrue": 8 + } + ], + "2321": [ + null, + { + "position": 40, + "nodeLength": 3, + "src": "i--", + "evalFalse": 108, + "evalTrue": 229 + } + ], + "2322": [ + null, + { + "position": 10, + "nodeLength": 34, + "src": "!matchers[i](elem, context, xml)", + "evalFalse": 216, + "evalTrue": 13 + } + ], + "2334": [ + null, + { + "position": 47, + "nodeLength": 7, + "src": "i < len", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2345": [ + null, + { + "position": 76, + "nodeLength": 11, + "src": "map != null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2347": [ + null, + { + "position": 102, + "nodeLength": 7, + "src": "i < len", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2348": [ + null, + { + "position": 9, + "nodeLength": 20, + "src": "(elem = unmatched[i])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2349": [ + null, + { + "position": 9, + "nodeLength": 39, + "src": "!filter || filter(elem, context, xml)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2351": [ + null, + { + "position": 41, + "nodeLength": 6, + "src": "mapped", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2362": [ + null, + { + "position": 7, + "nodeLength": 36, + "src": "postFilter && !postFilter[expando]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2365": [ + null, + { + "position": 98, + "nodeLength": 36, + "src": "postFinder && !postFinder[expando]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2375": [ + null, + { + "position": 144, + "nodeLength": 89, + "src": "seed || multipleContexts(selector || \"*\", context.nodeType ? [context] : context, [])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 170, + "nodeLength": 15, + "src": "selector || \"*\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 187, + "nodeLength": 16, + "src": "context.nodeType", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2378": [ + null, + { + "position": 339, + "nodeLength": 34, + "src": "preFilter && (seed || !selector)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 354, + "nodeLength": 17, + "src": "seed || !selector", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2382": [ + null, + { + "position": 461, + "nodeLength": 7, + "src": "matcher", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2384": [ + null, + { + "position": 110, + "nodeLength": 62, + "src": "postFinder || (seed ? preFilter : preexisting || postFilter)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 126, + "nodeLength": 4, + "src": "seed", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 145, + "nodeLength": 25, + "src": "preexisting || postFilter", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2394": [ + null, + { + "position": 807, + "nodeLength": 7, + "src": "matcher", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2399": [ + null, + { + "position": 904, + "nodeLength": 10, + "src": "postFilter", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2405": [ + null, + { + "position": 182, + "nodeLength": 3, + "src": "i--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2406": [ + null, + { + "position": 11, + "nodeLength": 15, + "src": "(elem = temp[i])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2412": [ + null, + { + "position": 1228, + "nodeLength": 4, + "src": "seed", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2413": [ + null, + { + "position": 9, + "nodeLength": 23, + "src": "postFinder || preFilter", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2414": [ + null, + { + "position": 10, + "nodeLength": 10, + "src": "postFinder", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2418": [ + null, + { + "position": 148, + "nodeLength": 3, + "src": "i--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2419": [ + null, + { + "position": 13, + "nodeLength": 21, + "src": "(elem = matcherOut[i])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2429": [ + null, + { + "position": 517, + "nodeLength": 3, + "src": "i--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2430": [ + null, + { + "position": 12, + "nodeLength": 91, + "src": "(elem = matcherOut[i]) && (temp = postFinder ? indexOf(seed, elem) : preMap[i]) > -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2431": [ + null, + { + "position": 31, + "nodeLength": 59, + "src": "(temp = postFinder ? indexOf(seed, elem) : preMap[i]) > -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53, + "nodeLength": 10, + "src": "postFinder", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2441": [ + null, + { + "position": 13, + "nodeLength": 22, + "src": "matcherOut === results", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2445": [ + null, + { + "position": 145, + "nodeLength": 10, + "src": "postFinder", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2458": [ + null, + { + "position": 126, + "nodeLength": 37, + "src": "leadingRelative || Expr.relative[\" \"]", + "evalFalse": 0, + "evalTrue": 8 + } + ], + "2459": [ + null, + { + "position": 171, + "nodeLength": 15, + "src": "leadingRelative", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "2463": [ + null, + { + "position": 11, + "nodeLength": 21, + "src": "elem === checkContext", + "evalFalse": 21, + "evalTrue": 5 + } + ], + "2466": [ + null, + { + "position": 11, + "nodeLength": 34, + "src": "indexOf(checkContext, elem) > -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2469": [ + null, + { + "position": 16, + "nodeLength": 193, + "src": "(!leadingRelative && (xml || context !== outermostContext)) || ((checkContext = context).nodeType ? matchContext(elem, context, xml) : matchAnyContext(elem, context, xml))", + "evalFalse": 0, + "evalTrue": 108 + }, + { + "position": 16, + "nodeLength": 59, + "src": "!leadingRelative && (xml || context !== outermostContext)", + "evalFalse": 63, + "evalTrue": 45 + }, + { + "position": 38, + "nodeLength": 35, + "src": "xml || context !== outermostContext", + "evalFalse": 63, + "evalTrue": 45 + }, + { + "position": 45, + "nodeLength": 28, + "src": "context !== outermostContext", + "evalFalse": 63, + "evalTrue": 45 + } + ], + "2470": [ + null, + { + "position": -1, + "nodeLength": 32, + "src": "(checkContext = context).nodeType", + "evalFalse": 0, + "evalTrue": 63 + } + ], + "2478": [ + null, + { + "position": 898, + "nodeLength": 7, + "src": "i < len", + "evalFalse": 8, + "evalTrue": 8 + } + ], + "2479": [ + null, + { + "position": 9, + "nodeLength": 42, + "src": "(matcher = Expr.relative[tokens[i].type])", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "2485": [ + null, + { + "position": 141, + "nodeLength": 18, + "src": "matcher[expando]", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "2488": [ + null, + { + "position": 94, + "nodeLength": 7, + "src": "j < len", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2489": [ + null, + { + "position": 11, + "nodeLength": 31, + "src": "Expr.relative[tokens[j].type]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2494": [ + null, + { + "position": 16, + "nodeLength": 35, + "src": "i > 1 && elementMatcher(matchers)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24, + "nodeLength": 5, + "src": "i > 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2495": [ + null, + { + "position": 58, + "nodeLength": 236, + "src": "i > 1 && toSelector(tokens.slice(0, i - 1).concat({\n value: tokens[i - 2].type === \" \" ? \"*\" : \"\"})).replace(rtrim, \"$1\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66, + "nodeLength": 5, + "src": "i > 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2497": [ + null, + { + "position": 154, + "nodeLength": 28, + "src": "tokens[i - 2].type === \" \"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2500": [ + null, + { + "position": 315, + "nodeLength": 50, + "src": "i < j && matcherFromTokens(tokens.slice(i, j))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 323, + "nodeLength": 5, + "src": "i < j", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2501": [ + null, + { + "position": 372, + "nodeLength": 60, + "src": "j < len && matcherFromTokens((tokens = tokens.slice(j)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 380, + "nodeLength": 7, + "src": "j < len", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2502": [ + null, + { + "position": 439, + "nodeLength": 31, + "src": "j < len && toSelector(tokens)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 447, + "nodeLength": 7, + "src": "j < len", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2513": [ + null, + { + "position": 14, + "nodeLength": 22, + "src": "setMatchers.length > 0", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "2514": [ + null, + { + "position": 49, + "nodeLength": 26, + "src": "elementMatchers.length > 0", + "evalFalse": 0, + "evalTrue": 8 + } + ], + "2519": [ + null, + { + "position": 72, + "nodeLength": 10, + "src": "seed && []", + "evalFalse": 5, + "evalTrue": 29 + } + ], + "2523": [ + null, + { + "position": 224, + "nodeLength": 55, + "src": "seed || byElement && Expr.find[\"TAG\"](\"*\", outermost)", + "evalFalse": 0, + "evalTrue": 34 + }, + { + "position": 232, + "nodeLength": 47, + "src": "byElement && Expr.find[\"TAG\"](\"*\", outermost)", + "evalFalse": 0, + "evalTrue": 5 + } + ], + "2525": [ + null, + { + "position": 374, + "nodeLength": 21, + "src": "contextBackup == null", + "evalFalse": 13, + "evalTrue": 21 + }, + { + "position": 402, + "nodeLength": 20, + "src": "Math.random() || 0.1", + "evalFalse": 0, + "evalTrue": 13 + } + ], + "2528": [ + null, + { + "position": 463, + "nodeLength": 9, + "src": "outermost", + "evalFalse": 13, + "evalTrue": 21 + } + ], + "2529": [ + null, + { + "position": 24, + "nodeLength": 44, + "src": "context === document || context || outermost", + "evalFalse": 0, + "evalTrue": 21 + }, + { + "position": 24, + "nodeLength": 20, + "src": "context === document", + "evalFalse": 21, + "evalTrue": 0 + }, + { + "position": 48, + "nodeLength": 20, + "src": "context || outermost", + "evalFalse": 0, + "evalTrue": 21 + } + ], + "2535": [ + null, + { + "position": 746, + "nodeLength": 38, + "src": "i !== len && (elem = elems[i]) != null", + "evalFalse": 34, + "evalTrue": 121 + }, + { + "position": 746, + "nodeLength": 9, + "src": "i !== len", + "evalFalse": 29, + "evalTrue": 126 + }, + { + "position": 760, + "nodeLength": 24, + "src": "(elem = elems[i]) != null", + "evalFalse": 5, + "evalTrue": 121 + } + ], + "2536": [ + null, + { + "position": 10, + "nodeLength": 17, + "src": "byElement && elem", + "evalFalse": 0, + "evalTrue": 121 + } + ], + "2538": [ + null, + { + "position": 23, + "nodeLength": 43, + "src": "!context && elem.ownerDocument !== document", + "evalFalse": 121, + "evalTrue": 0 + }, + { + "position": 35, + "nodeLength": 31, + "src": "elem.ownerDocument !== document", + "evalFalse": 58, + "evalTrue": 0 + } + ], + "2542": [ + null, + { + "position": 148, + "nodeLength": 31, + "src": "(matcher = elementMatchers[j++])", + "evalFalse": 13, + "evalTrue": 121 + } + ], + "2543": [ + null, + { + "position": 12, + "nodeLength": 40, + "src": "matcher(elem, context || document, xml)", + "evalFalse": 13, + "evalTrue": 108 + }, + { + "position": 27, + "nodeLength": 19, + "src": "context || document", + "evalFalse": 0, + "evalTrue": 121 + } + ], + "2548": [ + null, + { + "position": 308, + "nodeLength": 9, + "src": "outermost", + "evalFalse": 13, + "evalTrue": 108 + } + ], + "2554": [ + null, + { + "position": 455, + "nodeLength": 5, + "src": "bySet", + "evalFalse": 121, + "evalTrue": 0 + } + ], + "2556": [ + null, + { + "position": 70, + "nodeLength": 24, + "src": "(elem = !matcher && elem)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 77, + "nodeLength": 16, + "src": "!matcher && elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2561": [ + null, + { + "position": 200, + "nodeLength": 4, + "src": "seed", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2578": [ + null, + { + "position": 2136, + "nodeLength": 27, + "src": "bySet && i !== matchedCount", + "evalFalse": 34, + "evalTrue": 0 + }, + { + "position": 2145, + "nodeLength": 18, + "src": "i !== matchedCount", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2580": [ + null, + { + "position": 25, + "nodeLength": 27, + "src": "(matcher = setMatchers[j++])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2584": [ + null, + { + "position": 126, + "nodeLength": 4, + "src": "seed", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2586": [ + null, + { + "position": 81, + "nodeLength": 16, + "src": "matchedCount > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2587": [ + null, + { + "position": 15, + "nodeLength": 3, + "src": "i--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2588": [ + null, + { + "position": 13, + "nodeLength": 32, + "src": "!(unmatched[i] || setMatched[i])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15, + "nodeLength": 29, + "src": "unmatched[i] || setMatched[i]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2602": [ + null, + { + "position": 659, + "nodeLength": 93, + "src": "outermost && !seed && setMatched.length > 0 && (matchedCount + setMatchers.length) > 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 672, + "nodeLength": 80, + "src": "!seed && setMatched.length > 0 && (matchedCount + setMatchers.length) > 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 681, + "nodeLength": 71, + "src": "setMatched.length > 0 && (matchedCount + setMatchers.length) > 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 681, + "nodeLength": 21, + "src": "setMatched.length > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2603": [ + null, + { + "position": 31, + "nodeLength": 39, + "src": "(matchedCount + setMatchers.length) > 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2610": [ + null, + { + "position": 3038, + "nodeLength": 9, + "src": "outermost", + "evalFalse": 13, + "evalTrue": 21 + } + ], + "2618": [ + null, + { + "position": 3309, + "nodeLength": 5, + "src": "bySet", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "2629": [ + null, + { + "position": 104, + "nodeLength": 7, + "src": "!cached", + "evalFalse": 15, + "evalTrue": 8 + } + ], + "2631": [ + null, + { + "position": 95, + "nodeLength": 6, + "src": "!match", + "evalFalse": 1, + "evalTrue": 7 + } + ], + "2635": [ + null, + { + "position": 173, + "nodeLength": 3, + "src": "i--", + "evalFalse": 8, + "evalTrue": 8 + } + ], + "2637": [ + null, + { + "position": 52, + "nodeLength": 17, + "src": "cached[expando]", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "2664": [ + null, + { + "position": 46, + "nodeLength": 42, + "src": "typeof selector === \"function\" && selector", + "evalFalse": 21, + "evalTrue": 0 + }, + { + "position": 46, + "nodeLength": 30, + "src": "typeof selector === \"function\"", + "evalFalse": 21, + "evalTrue": 0 + } + ], + "2665": [ + null, + { + "position": 100, + "nodeLength": 63, + "src": "!seed && tokenize((selector = compiled.selector || selector))", + "evalFalse": 16, + "evalTrue": 5 + }, + { + "position": 131, + "nodeLength": 29, + "src": "compiled.selector || selector", + "evalFalse": 0, + "evalTrue": 5 + } + ], + "2667": [ + null, + { + "position": 180, + "nodeLength": 13, + "src": "results || []", + "evalFalse": 0, + "evalTrue": 21 + } + ], + "2671": [ + null, + { + "position": 335, + "nodeLength": 18, + "src": "match.length === 1", + "evalFalse": 16, + "evalTrue": 5 + } + ], + "2675": [ + null, + { + "position": 114, + "nodeLength": 137, + "src": "tokens.length > 2 && (token = tokens[0]).type === \"ID\" && context.nodeType === 9 && documentIsHTML && Expr.relative[tokens[1].type]", + "evalFalse": 5, + "evalTrue": 0 + }, + { + "position": 114, + "nodeLength": 17, + "src": "tokens.length > 2", + "evalFalse": 5, + "evalTrue": 0 + }, + { + "position": 136, + "nodeLength": 115, + "src": "(token = tokens[0]).type === \"ID\" && context.nodeType === 9 && documentIsHTML && Expr.relative[tokens[1].type]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 136, + "nodeLength": 32, + "src": "(token = tokens[0]).type === \"ID\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2676": [ + null, + { + "position": 39, + "nodeLength": 75, + "src": "context.nodeType === 9 && documentIsHTML && Expr.relative[tokens[1].type]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 179, + "nodeLength": 22, + "src": "context.nodeType === 9", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25, + "nodeLength": 49, + "src": "documentIsHTML && Expr.relative[tokens[1].type]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2678": [ + null, + { + "position": 17, + "nodeLength": 80, + "src": "Expr.find[\"ID\"](token.matches[0].replace(runescape, funescape), context) || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2679": [ + null, + { + "position": 112, + "nodeLength": 8, + "src": "!context", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2683": [ + null, + { + "position": 236, + "nodeLength": 8, + "src": "compiled", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2691": [ + null, + { + "position": 664, + "nodeLength": 42, + "src": "matchExpr[\"needsContext\"].test(selector)", + "evalFalse": 5, + "evalTrue": 0 + } + ], + "2692": [ + null, + { + "position": 738, + "nodeLength": 3, + "src": "i--", + "evalFalse": 5, + "evalTrue": 5 + } + ], + "2696": [ + null, + { + "position": 67, + "nodeLength": 36, + "src": "Expr.relative[(type = token.type)]", + "evalFalse": 5, + "evalTrue": 0 + } + ], + "2699": [ + null, + { + "position": 133, + "nodeLength": 25, + "src": "(find = Expr.find[type])", + "evalFalse": 5, + "evalTrue": 0 + } + ], + "2701": [ + null, + { + "position": 76, + "nodeLength": 159, + "src": "(seed = find(token.matches[0].replace(runescape, funescape), rsibling.test(tokens[0].type) && testContext(context.parentNode) || context))", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2703": [ + null, + { + "position": 65, + "nodeLength": 79, + "src": "rsibling.test(tokens[0].type) && testContext(context.parentNode) || context", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73, + "nodeLength": 68, + "src": "rsibling.test(tokens[0].type) && testContext(context.parentNode)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2708": [ + null, + { + "position": 112, + "nodeLength": 35, + "src": "seed.length && toSelector(tokens)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2709": [ + null, + { + "position": 159, + "nodeLength": 9, + "src": "!selector", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2722": [ + null, + { + "position": 1921, + "nodeLength": 38, + "src": "compiled || compile(selector, match)", + "evalFalse": 0, + "evalTrue": 21 + } + ], + "2727": [ + null, + { + "position": 92, + "nodeLength": 85, + "src": "!context || rsibling.test(selector) && testContext(context.parentNode) || context", + "evalFalse": 0, + "evalTrue": 21 + }, + { + "position": 105, + "nodeLength": 73, + "src": "rsibling.test(selector) && testContext(context.parentNode) || context", + "evalFalse": 0, + "evalTrue": 5 + }, + { + "position": 105, + "nodeLength": 62, + "src": "rsibling.test(selector) && testContext(context.parentNode)", + "evalFalse": 5, + "evalTrue": 0 + } + ], + "2735": [ + null, + { + "position": 62832, + "nodeLength": 56, + "src": "expando.split(\"\").sort(sortOrder).join(\"\") === expando", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2754": [ + null, + { + "position": 63535, + "nodeLength": 116, + "src": "!assert(function(el) {\n el.innerHTML = \"\";\n return el.firstChild.getAttribute(\"href\") === \"#\";\n})", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2756": [ + null, + { + "position": 45, + "nodeLength": 42, + "src": "el.firstChild.getAttribute(\"href\") === \"#\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2759": [ + null, + { + "position": 8, + "nodeLength": 6, + "src": "!isXML", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2760": [ + null, + { + "position": 36, + "nodeLength": 29, + "src": "name.toLowerCase() === \"type\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2767": [ + null, + { + "position": 63908, + "nodeLength": 176, + "src": "!support.attributes || !assert(function(el) {\n el.innerHTML = \"\";\n el.firstChild.setAttribute(\"value\", \"\");\n return el.firstChild.getAttribute(\"value\") === \"\";\n})", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2770": [ + null, + { + "position": 81, + "nodeLength": 44, + "src": "el.firstChild.getAttribute(\"value\") === \"\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2773": [ + null, + { + "position": 8, + "nodeLength": 49, + "src": "!isXML && elem.nodeName.toLowerCase() === \"input\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18, + "nodeLength": 39, + "src": "elem.nodeName.toLowerCase() === \"input\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2781": [ + null, + { + "position": 64331, + "nodeLength": 72, + "src": "!assert(function(el) {\n return el.getAttribute(\"disabled\") == null;\n})", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2782": [ + null, + { + "position": 9, + "nodeLength": 35, + "src": "el.getAttribute(\"disabled\") == null", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2786": [ + null, + { + "position": 19, + "nodeLength": 6, + "src": "!isXML", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2787": [ + null, + { + "position": 11, + "nodeLength": 21, + "src": "elem[name] === true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2788": [ + null, + { + "position": 50, + "nodeLength": 53, + "src": "(val = elem.getAttributeNode(name)) && val.specified", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2817": [ + null, + { + "position": 30, + "nodeLength": 19, + "src": "until !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2819": [ + null, + { + "position": 66, + "nodeLength": 43, + "src": "(elem = elem[dir]) && elem.nodeType !== 9", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 90, + "nodeLength": 19, + "src": "elem.nodeType !== 9", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2820": [ + null, + { + "position": 8, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2821": [ + null, + { + "position": 9, + "nodeLength": 38, + "src": "truncate && jQuery(elem).is(until)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2834": [ + null, + { + "position": 30, + "nodeLength": 1, + "src": "n", + "evalFalse": 18, + "evalTrue": 74 + } + ], + "2835": [ + null, + { + "position": 8, + "nodeLength": 30, + "src": "n.nodeType === 1 && n !== elem", + "evalFalse": 0, + "evalTrue": 74 + }, + { + "position": 8, + "nodeLength": 16, + "src": "n.nodeType === 1", + "evalFalse": 0, + "evalTrue": 74 + }, + { + "position": 28, + "nodeLength": 10, + "src": "n !== elem", + "evalFalse": 0, + "evalTrue": 74 + } + ], + "2854": [ + null, + { + "position": 7, + "nodeLength": 30, + "src": "jQuery.isFunction(qualifier)", + "evalFalse": 8, + "evalTrue": 1 + } + ], + "2856": [ + null, + { + "position": 11, + "nodeLength": 41, + "src": "!!qualifier.call(elem, i, elem) !== not", + "evalFalse": 2, + "evalTrue": 3 + } + ], + "2861": [ + null, + { + "position": 185, + "nodeLength": 18, + "src": "qualifier.nodeType", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "2863": [ + null, + { + "position": 13, + "nodeLength": 28, + "src": "(elem === qualifier) !== not", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13, + "nodeLength": 18, + "src": "elem === qualifier", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2868": [ + null, + { + "position": 371, + "nodeLength": 29, + "src": "typeof qualifier !== \"string\"", + "evalFalse": 5, + "evalTrue": 3 + } + ], + "2870": [ + null, + { + "position": 13, + "nodeLength": 46, + "src": "(indexOf.call(qualifier, elem) > -1) !== not", + "evalFalse": 0, + "evalTrue": 13 + }, + { + "position": 13, + "nodeLength": 36, + "src": "indexOf.call(qualifier, elem) > -1", + "evalFalse": 13, + "evalTrue": 0 + } + ], + "2875": [ + null, + { + "position": 606, + "nodeLength": 27, + "src": "risSimple.test(qualifier)", + "evalFalse": 0, + "evalTrue": 5 + } + ], + "2882": [ + null, + { + "position": 12, + "nodeLength": 69, + "src": "(indexOf.call(qualifier, elem) > -1) !== not && elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12, + "nodeLength": 46, + "src": "(indexOf.call(qualifier, elem) > -1) !== not", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12, + "nodeLength": 36, + "src": "indexOf.call(qualifier, elem) > -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2889": [ + null, + { + "position": 32, + "nodeLength": 3, + "src": "not", + "evalFalse": 13, + "evalTrue": 3 + } + ], + "2893": [ + null, + { + "position": 81, + "nodeLength": 41, + "src": "elems.length === 1 && elem.nodeType === 1", + "evalFalse": 16, + "evalTrue": 0 + }, + { + "position": 81, + "nodeLength": 18, + "src": "elems.length === 1", + "evalFalse": 16, + "evalTrue": 0 + }, + { + "position": 103, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2894": [ + null, + { + "position": 10, + "nodeLength": 41, + "src": "jQuery.find.matchesSelector(elem, expr)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2898": [ + null, + { + "position": 10, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 45 + } + ], + "2908": [ + null, + { + "position": 61, + "nodeLength": 28, + "src": "typeof selector !== \"string\"", + "evalFalse": 65, + "evalTrue": 0 + } + ], + "2910": [ + null, + { + "position": 18, + "nodeLength": 7, + "src": "i < len", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2911": [ + null, + { + "position": 11, + "nodeLength": 34, + "src": "jQuery.contains(self[i], this)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2920": [ + null, + { + "position": 336, + "nodeLength": 7, + "src": "i < len", + "evalFalse": 65, + "evalTrue": 95 + } + ], + "2924": [ + null, + { + "position": 411, + "nodeLength": 7, + "src": "len > 1", + "evalFalse": 53, + "evalTrue": 12 + } + ], + "2927": [ + null, + { + "position": 40, + "nodeLength": 14, + "src": "selector || []", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2930": [ + null, + { + "position": 40, + "nodeLength": 14, + "src": "selector || []", + "evalFalse": 0, + "evalTrue": 6 + } + ], + "2938": [ + null, + { + "position": 183, + "nodeLength": 62, + "src": "typeof selector === \"string\" && rneedsContext.test(selector)", + "evalFalse": 2, + "evalTrue": 0 + }, + { + "position": 183, + "nodeLength": 28, + "src": "typeof selector === \"string\"", + "evalFalse": 0, + "evalTrue": 2 + } + ], + "2940": [ + null, + { + "position": 93, + "nodeLength": 14, + "src": "selector || []", + "evalFalse": 0, + "evalTrue": 2 + } + ], + "2963": [ + null, + { + "position": 80, + "nodeLength": 9, + "src": "!selector", + "evalFalse": 193, + "evalTrue": 153 + } + ], + "2969": [ + null, + { + "position": 224, + "nodeLength": 18, + "src": "root || rootjQuery", + "evalFalse": 1, + "evalTrue": 192 + } + ], + "2972": [ + null, + { + "position": 277, + "nodeLength": 28, + "src": "typeof selector === \"string\"", + "evalFalse": 136, + "evalTrue": 57 + } + ], + "2973": [ + null, + { + "position": 9, + "nodeLength": 96, + "src": "selector[0] === \"<\" && selector[selector.length - 1] === \">\" && selector.length >= 3", + "evalFalse": 49, + "evalTrue": 8 + }, + { + "position": 9, + "nodeLength": 21, + "src": "selector[0] === \"<\"", + "evalFalse": 49, + "evalTrue": 8 + } + ], + "2974": [ + null, + { + "position": 28, + "nodeLength": 67, + "src": "selector[selector.length - 1] === \">\" && selector.length >= 3", + "evalFalse": 0, + "evalTrue": 8 + }, + { + "position": 40, + "nodeLength": 39, + "src": "selector[selector.length - 1] === \">\"", + "evalFalse": 0, + "evalTrue": 8 + } + ], + "2975": [ + null, + { + "position": 46, + "nodeLength": 20, + "src": "selector.length >= 3", + "evalFalse": 0, + "evalTrue": 8 + } + ], + "2985": [ + null, + { + "position": 367, + "nodeLength": 35, + "src": "match && (match[1] || !context)", + "evalFalse": 32, + "evalTrue": 25 + }, + { + "position": 378, + "nodeLength": 22, + "src": "match[1] || !context", + "evalFalse": 0, + "evalTrue": 25 + } + ], + "2988": [ + null, + { + "position": 46, + "nodeLength": 10, + "src": "match[1]", + "evalFalse": 17, + "evalTrue": 8 + } + ], + "2989": [ + null, + { + "position": 16, + "nodeLength": 25, + "src": "context instanceof jQuery", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "2995": [ + null, + { + "position": 41, + "nodeLength": 27, + "src": "context && context.nodeType", + "evalFalse": 2, + "evalTrue": 6 + }, + { + "position": 71, + "nodeLength": 32, + "src": "context.ownerDocument || context", + "evalFalse": 0, + "evalTrue": 6 + } + ], + "3000": [ + null, + { + "position": 402, + "nodeLength": 64, + "src": "rsingleTag.test(match[1]) && jQuery.isPlainObject(context)", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "3004": [ + null, + { + "position": 80, + "nodeLength": 34, + "src": "jQuery.isFunction(this[match])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3020": [ + null, + { + "position": 63, + "nodeLength": 4, + "src": "elem", + "evalFalse": 10, + "evalTrue": 7 + } + ], + "3030": [ + null, + { + "position": 1575, + "nodeLength": 26, + "src": "!context || context.jquery", + "evalFalse": 0, + "evalTrue": 32 + } + ], + "3031": [ + null, + { + "position": 14, + "nodeLength": 15, + "src": "context || root", + "evalFalse": 0, + "evalTrue": 32 + } + ], + "3040": [ + null, + { + "position": 2170, + "nodeLength": 17, + "src": "selector.nodeType", + "evalFalse": 31, + "evalTrue": 105 + } + ], + "3047": [ + null, + { + "position": 2327, + "nodeLength": 29, + "src": "jQuery.isFunction(selector)", + "evalFalse": 30, + "evalTrue": 1 + } + ], + "3048": [ + null, + { + "position": 11, + "nodeLength": 24, + "src": "root.ready !== undefined", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "3082": [ + null, + { + "position": 26, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3083": [ + null, + { + "position": 10, + "nodeLength": 37, + "src": "jQuery.contains(this, targets[i])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3095": [ + null, + { + "position": 68, + "nodeLength": 52, + "src": "typeof selectors !== \"string\" && jQuery(selectors)", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 68, + "nodeLength": 29, + "src": "typeof selectors !== \"string\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3098": [ + null, + { + "position": 210, + "nodeLength": 32, + "src": "!rneedsContext.test(selectors)", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "3099": [ + null, + { + "position": 12, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3100": [ + null, + { + "position": 28, + "nodeLength": 22, + "src": "cur && cur !== context", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35, + "nodeLength": 15, + "src": "cur !== context", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3103": [ + null, + { + "position": 51, + "nodeLength": 193, + "src": "cur.nodeType < 11 && (targets ? targets.index(cur) > -1 : cur.nodeType === 1 && jQuery.find.matchesSelector(cur, selectors))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51, + "nodeLength": 17, + "src": "cur.nodeType < 11", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74, + "nodeLength": 7, + "src": "targets", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3104": [ + null, + { + "position": 15, + "nodeLength": 25, + "src": "targets.index(cur) > -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3107": [ + null, + { + "position": 93, + "nodeLength": 74, + "src": "cur.nodeType === 1 && jQuery.find.matchesSelector(cur, selectors)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94, + "nodeLength": 18, + "src": "cur.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3117": [ + null, + { + "position": 686, + "nodeLength": 18, + "src": "matched.length > 1", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3124": [ + null, + { + "position": 50, + "nodeLength": 5, + "src": "!elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3125": [ + null, + { + "position": 13, + "nodeLength": 35, + "src": "(this[0] && this[0].parentNode)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13, + "nodeLength": 33, + "src": "this[0] && this[0].parentNode", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3129": [ + null, + { + "position": 181, + "nodeLength": 24, + "src": "typeof elem === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3137": [ + null, + { + "position": 87, + "nodeLength": 11, + "src": "elem.jquery", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3150": [ + null, + { + "position": 20, + "nodeLength": 16, + "src": "selector == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3157": [ + null, + { + "position": 12, + "nodeLength": 40, + "src": "(cur = cur[dir]) && cur.nodeType !== 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34, + "nodeLength": 18, + "src": "cur.nodeType !== 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3164": [ + null, + { + "position": 42, + "nodeLength": 32, + "src": "parent && parent.nodeType !== 11", + "evalFalse": 0, + "evalTrue": 16 + }, + { + "position": 52, + "nodeLength": 22, + "src": "parent.nodeType !== 11", + "evalFalse": 0, + "evalTrue": 16 + } + ], + "3191": [ + null, + { + "position": 22, + "nodeLength": 21, + "src": "elem.parentNode || {}", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3197": [ + null, + { + "position": 10, + "nodeLength": 59, + "src": "elem.contentDocument || jQuery.merge([], elem.childNodes)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3203": [ + null, + { + "position": 56, + "nodeLength": 28, + "src": "name.slice(-5) !== \"Until\"", + "evalFalse": 0, + "evalTrue": 25 + } + ], + "3207": [ + null, + { + "position": 122, + "nodeLength": 40, + "src": "selector && typeof selector === \"string\"", + "evalFalse": 14, + "evalTrue": 11 + }, + { + "position": 134, + "nodeLength": 28, + "src": "typeof selector === \"string\"", + "evalFalse": 0, + "evalTrue": 11 + } + ], + "3211": [ + null, + { + "position": 228, + "nodeLength": 15, + "src": "this.length > 1", + "evalFalse": 22, + "evalTrue": 3 + } + ], + "3214": [ + null, + { + "position": 34, + "nodeLength": 25, + "src": "!guaranteedUnique[name]", + "evalFalse": 0, + "evalTrue": 3 + } + ], + "3219": [ + null, + { + "position": 166, + "nodeLength": 25, + "src": "rparentsprev.test(name)", + "evalFalse": 3, + "evalTrue": 0 + } + ], + "3234": [ + null, + { + "position": 33, + "nodeLength": 36, + "src": "options.match(rnothtmlwhite) || []", + "evalFalse": 0, + "evalTrue": 30 + } + ], + "3266": [ + null, + { + "position": 115, + "nodeLength": 27, + "src": "typeof options === \"string\"", + "evalFalse": 0, + "evalTrue": 30 + } + ], + "3300": [ + null, + { + "position": 205, + "nodeLength": 12, + "src": "queue.length", + "evalFalse": 10, + "evalTrue": 10 + } + ], + "3302": [ + null, + { + "position": 41, + "nodeLength": 27, + "src": "++firingIndex < list.length", + "evalFalse": 10, + "evalTrue": 24 + } + ], + "3305": [ + null, + { + "position": 65, + "nodeLength": 92, + "src": "list[firingIndex].apply(memory[0], memory[1]) === false && options.stopOnFalse", + "evalFalse": 24, + "evalTrue": 0 + }, + { + "position": 65, + "nodeLength": 63, + "src": "list[firingIndex].apply(memory[0], memory[1]) === false", + "evalFalse": 24, + "evalTrue": 0 + } + ], + "3316": [ + null, + { + "position": 665, + "nodeLength": 15, + "src": "!options.memory", + "evalFalse": 10, + "evalTrue": 0 + } + ], + "3323": [ + null, + { + "position": 784, + "nodeLength": 6, + "src": "locked", + "evalFalse": 0, + "evalTrue": 10 + } + ], + "3326": [ + null, + { + "position": 74, + "nodeLength": 6, + "src": "memory", + "evalFalse": 0, + "evalTrue": 10 + } + ], + "3341": [ + null, + { + "position": 10, + "nodeLength": 4, + "src": "list", + "evalFalse": 0, + "evalTrue": 37 + } + ], + "3344": [ + null, + { + "position": 83, + "nodeLength": 17, + "src": "memory && !firing", + "evalFalse": 37, + "evalTrue": 0 + } + ], + "3351": [ + null, + { + "position": 13, + "nodeLength": 24, + "src": "jQuery.isFunction(arg)", + "evalFalse": 0, + "evalTrue": 57 + } + ], + "3352": [ + null, + { + "position": 14, + "nodeLength": 35, + "src": "!options.unique || !self.has(arg)", + "evalFalse": 0, + "evalTrue": 57 + } + ], + "3355": [ + null, + { + "position": 152, + "nodeLength": 52, + "src": "arg && arg.length && jQuery.type(arg) !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 159, + "nodeLength": 45, + "src": "arg.length && jQuery.type(arg) !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 173, + "nodeLength": 31, + "src": "jQuery.type(arg) !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3363": [ + null, + { + "position": 567, + "nodeLength": 17, + "src": "memory && !firing", + "evalFalse": 37, + "evalTrue": 0 + } + ], + "3374": [ + null, + { + "position": 32, + "nodeLength": 49, + "src": "(index = jQuery.inArray(arg, list, index)) > -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3378": [ + null, + { + "position": 75, + "nodeLength": 20, + "src": "index <= firingIndex", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3389": [ + null, + { + "position": 12, + "nodeLength": 2, + "src": "fn", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3390": [ + null, + { + "position": 9, + "nodeLength": 31, + "src": "jQuery.inArray(fn, list) > -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3391": [ + null, + { + "position": 48, + "nodeLength": 15, + "src": "list.length > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3396": [ + null, + { + "position": 10, + "nodeLength": 4, + "src": "list", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3419": [ + null, + { + "position": 35, + "nodeLength": 18, + "src": "!memory && !firing", + "evalFalse": 0, + "evalTrue": 5 + } + ], + "3430": [ + null, + { + "position": 10, + "nodeLength": 7, + "src": "!locked", + "evalFalse": 0, + "evalTrue": 10 + } + ], + "3431": [ + null, + { + "position": 13, + "nodeLength": 10, + "src": "args || []", + "evalFalse": 0, + "evalTrue": 10 + } + ], + "3432": [ + null, + { + "position": 48, + "nodeLength": 10, + "src": "args.slice", + "evalFalse": 7, + "evalTrue": 3 + } + ], + "3434": [ + null, + { + "position": 119, + "nodeLength": 7, + "src": "!firing", + "evalFalse": 0, + "evalTrue": 10 + } + ], + "3470": [ + null, + { + "position": 79, + "nodeLength": 56, + "src": "value && jQuery.isFunction((method = value.promise))", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3474": [ + null, + { + "position": 233, + "nodeLength": 53, + "src": "value && jQuery.isFunction((method = value.then))", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3531": [ + null, + { + "position": 98, + "nodeLength": 59, + "src": "jQuery.isFunction(fns[tuple[4]]) && fns[tuple[4]]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3537": [ + null, + { + "position": 24, + "nodeLength": 33, + "src": "fn && fn.apply(this, arguments)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3538": [ + null, + { + "position": 72, + "nodeLength": 49, + "src": "returned && jQuery.isFunction(returned.promise)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3546": [ + null, + { + "position": 58, + "nodeLength": 2, + "src": "fn", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3566": [ + null, + { + "position": 190, + "nodeLength": 16, + "src": "depth < maxDepth", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "3574": [ + null, + { + "position": 400, + "nodeLength": 31, + "src": "returned === deferred.promise()", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "3582": [ + null, + { + "position": 714, + "nodeLength": 286, + "src": "returned && (typeof returned === \"object\" || typeof returned === \"function\") && returned.then", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "3587": [ + null, + { + "position": 183, + "nodeLength": 102, + "src": "(typeof returned === \"object\" || typeof returned === \"function\") && returned.then", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 192, + "nodeLength": 73, + "src": "typeof returned === \"object\" || typeof returned === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 192, + "nodeLength": 28, + "src": "typeof returned === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3588": [ + null, + { + "position": 42, + "nodeLength": 30, + "src": "typeof returned === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3592": [ + null, + { + "position": 1056, + "nodeLength": 25, + "src": "jQuery.isFunction(then)", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "3595": [ + null, + { + "position": 83, + "nodeLength": 7, + "src": "special", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3622": [ + null, + { + "position": 124, + "nodeLength": 20, + "src": "handler !== Identity", + "evalFalse": 2, + "evalTrue": 2 + } + ], + "3629": [ + null, + { + "position": 309, + "nodeLength": 31, + "src": "special || deferred.resolveWith", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "3634": [ + null, + { + "position": 2413, + "nodeLength": 7, + "src": "special", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "3641": [ + null, + { + "position": 18, + "nodeLength": 29, + "src": "jQuery.Deferred.exceptionHook", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3649": [ + null, + { + "position": 316, + "nodeLength": 21, + "src": "depth + 1 >= maxDepth", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3653": [ + null, + { + "position": 130, + "nodeLength": 19, + "src": "handler !== Thrower", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3667": [ + null, + { + "position": 3402, + "nodeLength": 5, + "src": "depth", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "3673": [ + null, + { + "position": 153, + "nodeLength": 28, + "src": "jQuery.Deferred.getStackHook", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "3688": [ + null, + { + "position": 45, + "nodeLength": 31, + "src": "jQuery.isFunction(onProgress)", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "3700": [ + null, + { + "position": 45, + "nodeLength": 32, + "src": "jQuery.isFunction(onFulfilled)", + "evalFalse": 2, + "evalTrue": 2 + } + ], + "3711": [ + null, + { + "position": 45, + "nodeLength": 31, + "src": "jQuery.isFunction(onRejected)", + "evalFalse": 2, + "evalTrue": 2 + } + ], + "3722": [ + null, + { + "position": 13, + "nodeLength": 11, + "src": "obj != null", + "evalFalse": 8, + "evalTrue": 5 + } + ], + "3738": [ + null, + { + "position": 217, + "nodeLength": 11, + "src": "stateString", + "evalFalse": 5, + "evalTrue": 10 + } + ], + "3765": [ + null, + { + "position": 38, + "nodeLength": 17, + "src": "this === deferred", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3779": [ + null, + { + "position": 8147, + "nodeLength": 4, + "src": "func", + "evalFalse": 1, + "evalTrue": 4 + } + ], + "3808": [ + null, + { + "position": 61, + "nodeLength": 20, + "src": "arguments.length > 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3809": [ + null, + { + "position": 127, + "nodeLength": 16, + "src": "!(--remaining)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3816": [ + null, + { + "position": 708, + "nodeLength": 14, + "src": "remaining <= 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3820": [ + null, + { + "position": 157, + "nodeLength": 102, + "src": "master.state() === \"pending\" || jQuery.isFunction(resolveValues[i] && resolveValues[i].then)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 157, + "nodeLength": 28, + "src": "master.state() === \"pending\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3821": [ + null, + { + "position": 54, + "nodeLength": 45, + "src": "resolveValues[i] && resolveValues[i].then", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3828": [ + null, + { + "position": 1108, + "nodeLength": 3, + "src": "i--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3845": [ + null, + { + "position": 108, + "nodeLength": 80, + "src": "window.console && window.console.warn && error && rerrorNames.test(error.name)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 126, + "nodeLength": 62, + "src": "window.console.warn && error && rerrorNames.test(error.name)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 149, + "nodeLength": 39, + "src": "error && rerrorNames.test(error.name)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3891": [ + null, + { + "position": 8, + "nodeLength": 4, + "src": "hold", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3902": [ + null, + { + "position": 70, + "nodeLength": 51, + "src": "wait === true ? --jQuery.readyWait : jQuery.isReady", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 70, + "nodeLength": 13, + "src": "wait === true", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3910": [ + null, + { + "position": 282, + "nodeLength": 39, + "src": "wait !== true && --jQuery.readyWait > 0", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 282, + "nodeLength": 13, + "src": "wait !== true", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 299, + "nodeLength": 22, + "src": "--jQuery.readyWait > 0", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3932": [ + null, + { + "position": 105604, + "nodeLength": 114, + "src": "document.readyState === \"complete\" || (document.readyState !== \"loading\" && !document.documentElement.doScroll)", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 105604, + "nodeLength": 34, + "src": "document.readyState === \"complete\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3933": [ + null, + { + "position": 40, + "nodeLength": 71, + "src": "document.readyState !== \"loading\" && !document.documentElement.doScroll", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 40, + "nodeLength": 33, + "src": "document.readyState !== \"loading\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3955": [ + null, + { + "position": 41, + "nodeLength": 11, + "src": "key == null", + "evalFalse": 193, + "evalTrue": 25 + } + ], + "3958": [ + null, + { + "position": 85, + "nodeLength": 31, + "src": "jQuery.type(key) === \"object\"", + "evalFalse": 183, + "evalTrue": 35 + } + ], + "3965": [ + null, + { + "position": 258, + "nodeLength": 19, + "src": "value !== undefined", + "evalFalse": 56, + "evalTrue": 127 + } + ], + "3968": [ + null, + { + "position": 29, + "nodeLength": 27, + "src": "!jQuery.isFunction(value)", + "evalFalse": 0, + "evalTrue": 127 + } + ], + "3972": [ + null, + { + "position": 88, + "nodeLength": 4, + "src": "bulk", + "evalFalse": 113, + "evalTrue": 14 + } + ], + "3975": [ + null, + { + "position": 59, + "nodeLength": 3, + "src": "raw", + "evalFalse": 0, + "evalTrue": 14 + } + ], + "3988": [ + null, + { + "position": 394, + "nodeLength": 2, + "src": "fn", + "evalFalse": 14, + "evalTrue": 113 + } + ], + "3989": [ + null, + { + "position": 12, + "nodeLength": 7, + "src": "i < len", + "evalFalse": 113, + "evalTrue": 187 + } + ], + "3991": [ + null, + { + "position": 25, + "nodeLength": 3, + "src": "raw", + "evalFalse": 0, + "evalTrue": 187 + } + ], + "3999": [ + null, + { + "position": 840, + "nodeLength": 9, + "src": "chainable", + "evalFalse": 56, + "evalTrue": 162 + } + ], + "4004": [ + null, + { + "position": 889, + "nodeLength": 4, + "src": "bulk", + "evalFalse": 45, + "evalTrue": 11 + } + ], + "4008": [ + null, + { + "position": 937, + "nodeLength": 3, + "src": "len", + "evalFalse": 2, + "evalTrue": 43 + } + ], + "4018": [ + null, + { + "position": 122, + "nodeLength": 68, + "src": "owner.nodeType === 1 || owner.nodeType === 9 || !(+owner.nodeType)", + "evalFalse": 0, + "evalTrue": 60 + }, + { + "position": 122, + "nodeLength": 20, + "src": "owner.nodeType === 1", + "evalFalse": 2, + "evalTrue": 58 + }, + { + "position": 146, + "nodeLength": 44, + "src": "owner.nodeType === 9 || !(+owner.nodeType)", + "evalFalse": 0, + "evalTrue": 2 + }, + { + "position": 146, + "nodeLength": 20, + "src": "owner.nodeType === 9", + "evalFalse": 1, + "evalTrue": 1 + } + ], + "4038": [ + null, + { + "position": 122, + "nodeLength": 6, + "src": "!value", + "evalFalse": 135, + "evalTrue": 53 + } + ], + "4044": [ + null, + { + "position": 165, + "nodeLength": 19, + "src": "acceptData(owner)", + "evalFalse": 0, + "evalTrue": 53 + } + ], + "4048": [ + null, + { + "position": 105, + "nodeLength": 14, + "src": "owner.nodeType", + "evalFalse": 1, + "evalTrue": 52 + } + ], + "4071": [ + null, + { + "position": 133, + "nodeLength": 24, + "src": "typeof data === \"string\"", + "evalFalse": 0, + "evalTrue": 23 + } + ], + "4085": [ + null, + { + "position": 10, + "nodeLength": 17, + "src": "key === undefined", + "evalFalse": 60, + "evalTrue": 165 + } + ], + "4089": [ + null, + { + "position": 89, + "nodeLength": 73, + "src": "owner[this.expando] && owner[this.expando][jQuery.camelCase(key)]", + "evalFalse": 36, + "evalTrue": 24 + } + ], + "4104": [ + null, + { + "position": 332, + "nodeLength": 86, + "src": "key === undefined || ((key && typeof key === \"string\") && value === undefined)", + "evalFalse": 6, + "evalTrue": 6 + }, + { + "position": 332, + "nodeLength": 17, + "src": "key === undefined", + "evalFalse": 12, + "evalTrue": 0 + } + ], + "4105": [ + null, + { + "position": 28, + "nodeLength": 55, + "src": "(key && typeof key === \"string\") && value === undefined", + "evalFalse": 6, + "evalTrue": 6 + }, + { + "position": 28, + "nodeLength": 30, + "src": "key && typeof key === \"string\"", + "evalFalse": 0, + "evalTrue": 12 + }, + { + "position": 35, + "nodeLength": 23, + "src": "typeof key === \"string\"", + "evalFalse": 0, + "evalTrue": 12 + }, + { + "position": 64, + "nodeLength": 19, + "src": "value === undefined", + "evalFalse": 6, + "evalTrue": 6 + } + ], + "4120": [ + null, + { + "position": 825, + "nodeLength": 19, + "src": "value !== undefined", + "evalFalse": 0, + "evalTrue": 6 + } + ], + "4126": [ + null, + { + "position": 52, + "nodeLength": 19, + "src": "cache === undefined", + "evalFalse": 26, + "evalTrue": 0 + } + ], + "4130": [ + null, + { + "position": 99, + "nodeLength": 17, + "src": "key !== undefined", + "evalFalse": 0, + "evalTrue": 26 + } + ], + "4133": [ + null, + { + "position": 64, + "nodeLength": 21, + "src": "jQuery.isArray(key)", + "evalFalse": 26, + "evalTrue": 0 + } + ], + "4143": [ + null, + { + "position": 156, + "nodeLength": 12, + "src": "key in cache", + "evalFalse": 26, + "evalTrue": 0 + } + ], + "4145": [ + null, + { + "position": 36, + "nodeLength": 32, + "src": "key.match(rnothtmlwhite) || []", + "evalFalse": 0, + "evalTrue": 26 + } + ], + "4150": [ + null, + { + "position": 497, + "nodeLength": 3, + "src": "i--", + "evalFalse": 26, + "evalTrue": 52 + } + ], + "4156": [ + null, + { + "position": 720, + "nodeLength": 50, + "src": "key === undefined || jQuery.isEmptyObject(cache)", + "evalFalse": 0, + "evalTrue": 26 + }, + { + "position": 720, + "nodeLength": 17, + "src": "key === undefined", + "evalFalse": 26, + "evalTrue": 0 + } + ], + "4162": [ + null, + { + "position": 242, + "nodeLength": 14, + "src": "owner.nodeType", + "evalFalse": 0, + "evalTrue": 26 + } + ], + "4171": [ + null, + { + "position": 47, + "nodeLength": 53, + "src": "cache !== undefined && !jQuery.isEmptyObject(cache)", + "evalFalse": 14, + "evalTrue": 56 + }, + { + "position": 47, + "nodeLength": 19, + "src": "cache !== undefined", + "evalFalse": 14, + "evalTrue": 56 + } + ], + "4194": [ + null, + { + "position": 7, + "nodeLength": 15, + "src": "data === \"true\"", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "4198": [ + null, + { + "position": 52, + "nodeLength": 16, + "src": "data === \"false\"", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "4202": [ + null, + { + "position": 99, + "nodeLength": 15, + "src": "data === \"null\"", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "4207": [ + null, + { + "position": 205, + "nodeLength": 19, + "src": "data === +data + \"\"", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "4211": [ + null, + { + "position": 255, + "nodeLength": 19, + "src": "rbrace.test(data)", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "4223": [ + null, + { + "position": 114, + "nodeLength": 41, + "src": "data === undefined && elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 4 + }, + { + "position": 114, + "nodeLength": 18, + "src": "data === undefined", + "evalFalse": 0, + "evalTrue": 4 + }, + { + "position": 136, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "4227": [ + null, + { + "position": 112, + "nodeLength": 24, + "src": "typeof data === \"string\"", + "evalFalse": 2, + "evalTrue": 2 + } + ], + "4243": [ + null, + { + "position": 10, + "nodeLength": 52, + "src": "dataUser.hasData(elem) || dataPriv.hasData(elem)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4269": [ + null, + { + "position": 50, + "nodeLength": 23, + "src": "elem && elem.attributes", + "evalFalse": 0, + "evalTrue": 25 + } + ], + "4272": [ + null, + { + "position": 108, + "nodeLength": 17, + "src": "key === undefined", + "evalFalse": 24, + "evalTrue": 1 + } + ], + "4273": [ + null, + { + "position": 9, + "nodeLength": 11, + "src": "this.length", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "4276": [ + null, + { + "position": 44, + "nodeLength": 60, + "src": "elem.nodeType === 1 && !dataPriv.get(elem, \"hasDataAttrs\")", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 44, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "4278": [ + null, + { + "position": 37, + "nodeLength": 3, + "src": "i--", + "evalFalse": 1, + "evalTrue": 4 + } + ], + "4282": [ + null, + { + "position": 91, + "nodeLength": 10, + "src": "attrs[i]", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "4284": [ + null, + { + "position": 44, + "nodeLength": 29, + "src": "name.indexOf(\"data-\") === 0", + "evalFalse": 2, + "evalTrue": 2 + } + ], + "4298": [ + null, + { + "position": 725, + "nodeLength": 23, + "src": "typeof key === \"object\"", + "evalFalse": 24, + "evalTrue": 0 + } + ], + "4312": [ + null, + { + "position": 352, + "nodeLength": 27, + "src": "elem && value === undefined", + "evalFalse": 14, + "evalTrue": 10 + }, + { + "position": 360, + "nodeLength": 19, + "src": "value === undefined", + "evalFalse": 14, + "evalTrue": 10 + } + ], + "4317": [ + null, + { + "position": 140, + "nodeLength": 18, + "src": "data !== undefined", + "evalFalse": 2, + "evalTrue": 8 + } + ], + "4324": [ + null, + { + "position": 305, + "nodeLength": 18, + "src": "data !== undefined", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "4338": [ + null, + { + "position": 999, + "nodeLength": 20, + "src": "arguments.length > 1", + "evalFalse": 10, + "evalTrue": 14 + } + ], + "4353": [ + null, + { + "position": 22, + "nodeLength": 4, + "src": "elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4354": [ + null, + { + "position": 13, + "nodeLength": 12, + "src": "type || \"fx\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4358": [ + null, + { + "position": 158, + "nodeLength": 4, + "src": "data", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4359": [ + null, + { + "position": 10, + "nodeLength": 32, + "src": "!queue || jQuery.isArray(data)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4365": [ + null, + { + "position": 342, + "nodeLength": 11, + "src": "queue || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4370": [ + null, + { + "position": 10, + "nodeLength": 12, + "src": "type || \"fx\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4381": [ + null, + { + "position": 307, + "nodeLength": 19, + "src": "fn === \"inprogress\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4386": [ + null, + { + "position": 384, + "nodeLength": 2, + "src": "fn", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4390": [ + null, + { + "position": 104, + "nodeLength": 13, + "src": "type === \"fx\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4399": [ + null, + { + "position": 664, + "nodeLength": 21, + "src": "!startLength && hooks", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4407": [ + null, + { + "position": 43, + "nodeLength": 187, + "src": "dataPriv.get(elem, key) || dataPriv.access(elem, key, {\n empty: jQuery.Callbacks(\"once memory\").add(function() {\n dataPriv.remove(elem, [type + \"queue\", key]);\n})})", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4419": [ + null, + { + "position": 27, + "nodeLength": 24, + "src": "typeof type !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4425": [ + null, + { + "position": 113, + "nodeLength": 25, + "src": "arguments.length < setter", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4429": [ + null, + { + "position": 200, + "nodeLength": 18, + "src": "data === undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4437": [ + null, + { + "position": 137, + "nodeLength": 44, + "src": "type === \"fx\" && queue[0] !== \"inprogress\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 137, + "nodeLength": 13, + "src": "type === \"fx\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 154, + "nodeLength": 27, + "src": "queue[0] !== \"inprogress\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4448": [ + null, + { + "position": 22, + "nodeLength": 12, + "src": "type || \"fx\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4460": [ + null, + { + "position": 10, + "nodeLength": 12, + "src": "!(--count)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4465": [ + null, + { + "position": 218, + "nodeLength": 24, + "src": "typeof type !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4469": [ + null, + { + "position": 296, + "nodeLength": 12, + "src": "type || \"fx\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4471": [ + null, + { + "position": 321, + "nodeLength": 3, + "src": "i--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4473": [ + null, + { + "position": 70, + "nodeLength": 16, + "src": "tmp && tmp.empty", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4493": [ + null, + { + "position": 131, + "nodeLength": 10, + "src": "el || elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4496": [ + null, + { + "position": 182, + "nodeLength": 345, + "src": "elem.style.display === \"none\" || elem.style.display === \"\" && jQuery.contains(elem.ownerDocument, elem) && jQuery.css(elem, \"display\") === \"none\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182, + "nodeLength": 29, + "src": "elem.style.display === \"none\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4497": [ + null, + { + "position": 35, + "nodeLength": 309, + "src": "elem.style.display === \"\" && jQuery.contains(elem.ownerDocument, elem) && jQuery.css(elem, \"display\") === \"none\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 220, + "nodeLength": 25, + "src": "elem.style.display === \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4503": [ + null, + { + "position": 217, + "nodeLength": 91, + "src": "jQuery.contains(elem.ownerDocument, elem) && jQuery.css(elem, \"display\") === \"none\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4505": [ + null, + { + "position": 50, + "nodeLength": 40, + "src": "jQuery.css(elem, \"display\") === \"none\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4518": [ + null, + { + "position": 219, + "nodeLength": 10, + "src": "args || []", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "4535": [ + null, + { + "position": 65, + "nodeLength": 5, + "src": "tween", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4543": [ + null, + { + "position": 220, + "nodeLength": 73, + "src": "valueParts && valueParts[3] || (jQuery.cssNumber[prop] ? \"\" : \"px\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 220, + "nodeLength": 29, + "src": "valueParts && valueParts[3]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 255, + "nodeLength": 24, + "src": "jQuery.cssNumber[prop]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4546": [ + null, + { + "position": 390, + "nodeLength": 102, + "src": "(jQuery.cssNumber[prop] || unit !== \"px\" && +initial) && rcssNum.exec(jQuery.css(elem, prop))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 390, + "nodeLength": 53, + "src": "jQuery.cssNumber[prop] || unit !== \"px\" && +initial", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 418, + "nodeLength": 25, + "src": "unit !== \"px\" && +initial", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 418, + "nodeLength": 13, + "src": "unit !== \"px\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4549": [ + null, + { + "position": 504, + "nodeLength": 44, + "src": "initialInUnit && initialInUnit[3] !== unit", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 521, + "nodeLength": 27, + "src": "initialInUnit[3] !== unit", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4552": [ + null, + { + "position": 51, + "nodeLength": 26, + "src": "unit || initialInUnit[3]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4555": [ + null, + { + "position": 150, + "nodeLength": 16, + "src": "valueParts || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4558": [ + null, + { + "position": 246, + "nodeLength": 13, + "src": "+initial || 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4564": [ + null, + { + "position": 170, + "nodeLength": 13, + "src": "scale || \".5\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4573": [ + null, + { + "position": 465, + "nodeLength": 80, + "src": "scale !== (scale = currentValue() / initial) && scale !== 1 && --maxIterations", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 731, + "nodeLength": 46, + "src": "scale !== (scale = currentValue() / initial)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49, + "nodeLength": 30, + "src": "scale !== 1 && --maxIterations", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 782, + "nodeLength": 11, + "src": "scale !== 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4577": [ + null, + { + "position": 1378, + "nodeLength": 10, + "src": "valueParts", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4578": [ + null, + { + "position": 19, + "nodeLength": 31, + "src": "+initialInUnit || +initial || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37, + "nodeLength": 13, + "src": "+initial || 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4581": [ + null, + { + "position": 114, + "nodeLength": 15, + "src": "valueParts[1]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4584": [ + null, + { + "position": 223, + "nodeLength": 5, + "src": "tween", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4602": [ + null, + { + "position": 118, + "nodeLength": 7, + "src": "display", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4611": [ + null, + { + "position": 303, + "nodeLength": 18, + "src": "display === \"none\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4626": [ + null, + { + "position": 152, + "nodeLength": 14, + "src": "index < length", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "4628": [ + null, + { + "position": 36, + "nodeLength": 11, + "src": "!elem.style", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4633": [ + null, + { + "position": 109, + "nodeLength": 4, + "src": "show", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4638": [ + null, + { + "position": 225, + "nodeLength": 18, + "src": "display === \"none\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4639": [ + null, + { + "position": 23, + "nodeLength": 39, + "src": "dataPriv.get(elem, \"display\") || null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4640": [ + null, + { + "position": 73, + "nodeLength": 16, + "src": "!values[index]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4644": [ + null, + { + "position": 390, + "nodeLength": 55, + "src": "elem.style.display === \"\" && isHiddenWithinTree(elem)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 390, + "nodeLength": 25, + "src": "elem.style.display === \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4648": [ + null, + { + "position": 9, + "nodeLength": 18, + "src": "display !== \"none\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4658": [ + null, + { + "position": 1067, + "nodeLength": 14, + "src": "index < length", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "4659": [ + null, + { + "position": 8, + "nodeLength": 23, + "src": "values[index] != null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4675": [ + null, + { + "position": 8, + "nodeLength": 26, + "src": "typeof state === \"boolean\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4676": [ + null, + { + "position": 11, + "nodeLength": 5, + "src": "state", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4680": [ + null, + { + "position": 9, + "nodeLength": 26, + "src": "isHiddenWithinTree(this)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4726": [ + null, + { + "position": 130, + "nodeLength": 51, + "src": "typeof context.getElementsByTagName !== \"undefined\"", + "evalFalse": 6, + "evalTrue": 94 + } + ], + "4727": [ + null, + { + "position": 39, + "nodeLength": 10, + "src": "tag || \"*\"", + "evalFalse": 0, + "evalTrue": 94 + } + ], + "4729": [ + null, + { + "position": 253, + "nodeLength": 47, + "src": "typeof context.querySelectorAll !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 6 + } + ], + "4730": [ + null, + { + "position": 35, + "nodeLength": 10, + "src": "tag || \"*\"", + "evalFalse": 0, + "evalTrue": 6 + } + ], + "4736": [ + null, + { + "position": 385, + "nodeLength": 59, + "src": "tag === undefined || tag && jQuery.nodeName(context, tag)", + "evalFalse": 88, + "evalTrue": 12 + }, + { + "position": 385, + "nodeLength": 17, + "src": "tag === undefined", + "evalFalse": 88, + "evalTrue": 12 + }, + { + "position": 406, + "nodeLength": 38, + "src": "tag && jQuery.nodeName(context, tag)", + "evalFalse": 88, + "evalTrue": 0 + } + ], + "4749": [ + null, + { + "position": 43, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 33, + "evalTrue": 0 + } + ], + "4753": [ + null, + { + "position": 48, + "nodeLength": 62, + "src": "!refElements || dataPriv.get(refElements[i], \"globalEval\")", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4768": [ + null, + { + "position": 141, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 27, + "evalTrue": 27 + } + ], + "4771": [ + null, + { + "position": 30, + "nodeLength": 18, + "src": "elem || elem === 0", + "evalFalse": 0, + "evalTrue": 27 + }, + { + "position": 38, + "nodeLength": 10, + "src": "elem === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4774": [ + null, + { + "position": 35, + "nodeLength": 32, + "src": "jQuery.type(elem) === \"object\"", + "evalFalse": 12, + "evalTrue": 15 + } + ], + "4778": [ + null, + { + "position": 137, + "nodeLength": 13, + "src": "elem.nodeType", + "evalFalse": 15, + "evalTrue": 0 + } + ], + "4781": [ + null, + { + "position": 300, + "nodeLength": 19, + "src": "!rhtml.test(elem)", + "evalFalse": 12, + "evalTrue": 0 + } + ], + "4786": [ + null, + { + "position": 11, + "nodeLength": 61, + "src": "tmp || fragment.appendChild(context.createElement(\"div\"))", + "evalFalse": 0, + "evalTrue": 12 + } + ], + "4789": [ + null, + { + "position": 132, + "nodeLength": 35, + "src": "rtagName.exec(elem) || [\"\", \"\"]", + "evalFalse": 0, + "evalTrue": 12 + } + ], + "4790": [ + null, + { + "position": 201, + "nodeLength": 34, + "src": "wrapMap[tag] || wrapMap._default", + "evalFalse": 0, + "evalTrue": 12 + } + ], + "4795": [ + null, + { + "position": 396, + "nodeLength": 3, + "src": "j--", + "evalFalse": 12, + "evalTrue": 6 + } + ], + "4816": [ + null, + { + "position": 1463, + "nodeLength": 21, + "src": "(elem = nodes[i++])", + "evalFalse": 27, + "evalTrue": 45 + } + ], + "4819": [ + null, + { + "position": 74, + "nodeLength": 51, + "src": "selection && jQuery.inArray(elem, selection) > -1", + "evalFalse": 45, + "evalTrue": 0 + }, + { + "position": 87, + "nodeLength": 38, + "src": "jQuery.inArray(elem, selection) > -1", + "evalFalse": 38, + "evalTrue": 0 + } + ], + "4820": [ + null, + { + "position": 9, + "nodeLength": 7, + "src": "ignored", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4832": [ + null, + { + "position": 388, + "nodeLength": 8, + "src": "contains", + "evalFalse": 29, + "evalTrue": 16 + } + ], + "4837": [ + null, + { + "position": 463, + "nodeLength": 7, + "src": "scripts", + "evalFalse": 45, + "evalTrue": 0 + } + ], + "4839": [ + null, + { + "position": 24, + "nodeLength": 19, + "src": "(elem = tmp[j++])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4840": [ + null, + { + "position": 10, + "nodeLength": 35, + "src": "rscriptType.test(elem.type || \"\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28, + "nodeLength": 15, + "src": "elem.type || \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4904": [ + null, + { + "position": 68, + "nodeLength": 25, + "src": "typeof types === \"object\"", + "evalFalse": 77, + "evalTrue": 0 + } + ], + "4907": [ + null, + { + "position": 47, + "nodeLength": 28, + "src": "typeof selector !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4910": [ + null, + { + "position": 41, + "nodeLength": 16, + "src": "data || selector", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4919": [ + null, + { + "position": 376, + "nodeLength": 26, + "src": "data == null && fn == null", + "evalFalse": 40, + "evalTrue": 37 + }, + { + "position": 376, + "nodeLength": 12, + "src": "data == null", + "evalFalse": 40, + "evalTrue": 37 + }, + { + "position": 392, + "nodeLength": 10, + "src": "fn == null", + "evalFalse": 0, + "evalTrue": 37 + } + ], + "4924": [ + null, + { + "position": 489, + "nodeLength": 10, + "src": "fn == null", + "evalFalse": 0, + "evalTrue": 40 + } + ], + "4925": [ + null, + { + "position": 8, + "nodeLength": 28, + "src": "typeof selector === \"string\"", + "evalFalse": 21, + "evalTrue": 19 + } + ], + "4938": [ + null, + { + "position": 719, + "nodeLength": 12, + "src": "fn === false", + "evalFalse": 77, + "evalTrue": 0 + } + ], + "4940": [ + null, + { + "position": 770, + "nodeLength": 3, + "src": "!fn", + "evalFalse": 77, + "evalTrue": 0 + } + ], + "4944": [ + null, + { + "position": 802, + "nodeLength": 9, + "src": "one === 1", + "evalFalse": 77, + "evalTrue": 0 + } + ], + "4954": [ + null, + { + "position": 242, + "nodeLength": 46, + "src": "origFn.guid || (origFn.guid = jQuery.guid++)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4977": [ + null, + { + "position": 241, + "nodeLength": 9, + "src": "!elemData", + "evalFalse": 108, + "evalTrue": 0 + } + ], + "4982": [ + null, + { + "position": 350, + "nodeLength": 15, + "src": "handler.handler", + "evalFalse": 108, + "evalTrue": 0 + } + ], + "4990": [ + null, + { + "position": 635, + "nodeLength": 8, + "src": "selector", + "evalFalse": 89, + "evalTrue": 19 + } + ], + "4995": [ + null, + { + "position": 799, + "nodeLength": 13, + "src": "!handler.guid", + "evalFalse": 86, + "evalTrue": 22 + } + ], + "5000": [ + null, + { + "position": 941, + "nodeLength": 29, + "src": "!(events = elemData.events)", + "evalFalse": 61, + "evalTrue": 47 + } + ], + "5003": [ + null, + { + "position": 1020, + "nodeLength": 34, + "src": "!(eventHandle = elemData.handle)", + "evalFalse": 61, + "evalTrue": 47 + } + ], + "5008": [ + null, + { + "position": 134, + "nodeLength": 66, + "src": "typeof jQuery !== \"undefined\" && jQuery.event.triggered !== e.type", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 134, + "nodeLength": 29, + "src": "typeof jQuery !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 167, + "nodeLength": 33, + "src": "jQuery.event.triggered !== e.type", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "5014": [ + null, + { + "position": 1449, + "nodeLength": 46, + "src": "(types || \"\").match(rnothtmlwhite) || [\"\"]", + "evalFalse": 0, + "evalTrue": 108 + }, + { + "position": 1449, + "nodeLength": 11, + "src": "types || \"\"", + "evalFalse": 0, + "evalTrue": 108 + } + ], + "5016": [ + null, + { + "position": 1527, + "nodeLength": 3, + "src": "t--", + "evalFalse": 108, + "evalTrue": 125 + } + ], + "5017": [ + null, + { + "position": 10, + "nodeLength": 39, + "src": "rtypenamespace.exec(types[t]) || []", + "evalFalse": 0, + "evalTrue": 125 + } + ], + "5019": [ + null, + { + "position": 100, + "nodeLength": 14, + "src": "tmp[2] || \"\"", + "evalFalse": 47, + "evalTrue": 78 + } + ], + "5022": [ + null, + { + "position": 214, + "nodeLength": 5, + "src": "!type", + "evalFalse": 125, + "evalTrue": 0 + } + ], + "5027": [ + null, + { + "position": 342, + "nodeLength": 34, + "src": "jQuery.event.special[type] || {}", + "evalFalse": 0, + "evalTrue": 125 + } + ], + "5030": [ + null, + { + "position": 473, + "nodeLength": 60, + "src": "(selector ? special.delegateType : special.bindType) || type", + "evalFalse": 0, + "evalTrue": 125 + }, + { + "position": 473, + "nodeLength": 8, + "src": "selector", + "evalFalse": 101, + "evalTrue": 24 + } + ], + "5033": [ + null, + { + "position": 596, + "nodeLength": 34, + "src": "jQuery.event.special[type] || {}", + "evalFalse": 0, + "evalTrue": 125 + } + ], + "5043": [ + null, + { + "position": 145, + "nodeLength": 59, + "src": "selector && jQuery.expr.match.needsContext.test(selector)", + "evalFalse": 125, + "evalTrue": 0 + } + ], + "5048": [ + null, + { + "position": 1040, + "nodeLength": 30, + "src": "!(handlers = events[type])", + "evalFalse": 26, + "evalTrue": 99 + } + ], + "5053": [ + null, + { + "position": 156, + "nodeLength": 90, + "src": "!special.setup || special.setup.call(elem, data, namespaces, eventHandle) === false", + "evalFalse": 6, + "evalTrue": 93 + } + ], + "5054": [ + null, + { + "position": 22, + "nodeLength": 67, + "src": "special.setup.call(elem, data, namespaces, eventHandle) === false", + "evalFalse": 6, + "evalTrue": 0 + } + ], + "5056": [ + null, + { + "position": 12, + "nodeLength": 21, + "src": "elem.addEventListener", + "evalFalse": 0, + "evalTrue": 93 + } + ], + "5062": [ + null, + { + "position": 1439, + "nodeLength": 11, + "src": "special.add", + "evalFalse": 125, + "evalTrue": 0 + } + ], + "5065": [ + null, + { + "position": 52, + "nodeLength": 23, + "src": "!handleObj.handler.guid", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5071": [ + null, + { + "position": 1658, + "nodeLength": 8, + "src": "selector", + "evalFalse": 101, + "evalTrue": 24 + } + ], + "5089": [ + null, + { + "position": 111, + "nodeLength": 48, + "src": "dataPriv.hasData(elem) && dataPriv.get(elem)", + "evalFalse": 2, + "evalTrue": 56 + } + ], + "5091": [ + null, + { + "position": 174, + "nodeLength": 42, + "src": "!elemData || !(events = elemData.events)", + "evalFalse": 56, + "evalTrue": 2 + } + ], + "5096": [ + null, + { + "position": 313, + "nodeLength": 46, + "src": "(types || \"\").match(rnothtmlwhite) || [\"\"]", + "evalFalse": 0, + "evalTrue": 56 + }, + { + "position": 313, + "nodeLength": 11, + "src": "types || \"\"", + "evalFalse": 0, + "evalTrue": 56 + } + ], + "5098": [ + null, + { + "position": 391, + "nodeLength": 3, + "src": "t--", + "evalFalse": 56, + "evalTrue": 68 + } + ], + "5099": [ + null, + { + "position": 10, + "nodeLength": 39, + "src": "rtypenamespace.exec(types[t]) || []", + "evalFalse": 0, + "evalTrue": 68 + } + ], + "5101": [ + null, + { + "position": 100, + "nodeLength": 14, + "src": "tmp[2] || \"\"", + "evalFalse": 26, + "evalTrue": 42 + } + ], + "5104": [ + null, + { + "position": 220, + "nodeLength": 5, + "src": "!type", + "evalFalse": 68, + "evalTrue": 0 + } + ], + "5111": [ + null, + { + "position": 376, + "nodeLength": 34, + "src": "jQuery.event.special[type] || {}", + "evalFalse": 0, + "evalTrue": 68 + } + ], + "5112": [ + null, + { + "position": 424, + "nodeLength": 60, + "src": "(selector ? special.delegateType : special.bindType) || type", + "evalFalse": 0, + "evalTrue": 68 + }, + { + "position": 424, + "nodeLength": 8, + "src": "selector", + "evalFalse": 68, + "evalTrue": 0 + } + ], + "5113": [ + null, + { + "position": 500, + "nodeLength": 20, + "src": "events[type] || []", + "evalFalse": 0, + "evalTrue": 68 + } + ], + "5114": [ + null, + { + "position": 531, + "nodeLength": 88, + "src": "tmp[2] && new RegExp(\"(^|\\\\.)\" + namespaces.join(\"\\\\.(?:.*\\\\.|)\") + \"(\\\\.|$)\")", + "evalFalse": 26, + "evalTrue": 42 + } + ], + "5119": [ + null, + { + "position": 698, + "nodeLength": 3, + "src": "j--", + "evalFalse": 68, + "evalTrue": 69 + } + ], + "5122": [ + null, + { + "position": 44, + "nodeLength": 262, + "src": "(mappedTypes || origType === handleObj.origType) && (!handler || handler.guid === handleObj.guid) && (!tmp || tmp.test(handleObj.namespace)) && (!selector || selector === handleObj.selector || selector === \"**\" && handleObj.selector)", + "evalFalse": 9, + "evalTrue": 60 + }, + { + "position": 44, + "nodeLength": 46, + "src": "mappedTypes || origType === handleObj.origType", + "evalFalse": 0, + "evalTrue": 69 + }, + { + "position": 59, + "nodeLength": 31, + "src": "origType === handleObj.origType", + "evalFalse": 0, + "evalTrue": 69 + } + ], + "5123": [ + null, + { + "position": 58, + "nodeLength": 203, + "src": "(!handler || handler.guid === handleObj.guid) && (!tmp || tmp.test(handleObj.namespace)) && (!selector || selector === handleObj.selector || selector === \"**\" && handleObj.selector)", + "evalFalse": 9, + "evalTrue": 60 + }, + { + "position": 105, + "nodeLength": 43, + "src": "!handler || handler.guid === handleObj.guid", + "evalFalse": 9, + "evalTrue": 60 + }, + { + "position": 117, + "nodeLength": 31, + "src": "handler.guid === handleObj.guid", + "evalFalse": 9, + "evalTrue": 60 + } + ], + "5124": [ + null, + { + "position": 55, + "nodeLength": 147, + "src": "(!tmp || tmp.test(handleObj.namespace)) && (!selector || selector === handleObj.selector || selector === \"**\" && handleObj.selector)", + "evalFalse": 0, + "evalTrue": 60 + }, + { + "position": 162, + "nodeLength": 39, + "src": "!tmp || tmp.test(handleObj.namespace)", + "evalFalse": 0, + "evalTrue": 60 + } + ], + "5125": [ + null, + { + "position": 51, + "nodeLength": 93, + "src": "!selector || selector === handleObj.selector || selector === \"**\" && handleObj.selector", + "evalFalse": 0, + "evalTrue": 60 + }, + { + "position": 64, + "nodeLength": 80, + "src": "selector === handleObj.selector || selector === \"**\" && handleObj.selector", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 64, + "nodeLength": 31, + "src": "selector === handleObj.selector", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5126": [ + null, + { + "position": 40, + "nodeLength": 39, + "src": "selector === \"**\" && handleObj.selector", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 108, + "nodeLength": 17, + "src": "selector === \"**\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5129": [ + null, + { + "position": 42, + "nodeLength": 18, + "src": "handleObj.selector", + "evalFalse": 60, + "evalTrue": 0 + } + ], + "5132": [ + null, + { + "position": 114, + "nodeLength": 14, + "src": "special.remove", + "evalFalse": 60, + "evalTrue": 0 + } + ], + "5140": [ + null, + { + "position": 1395, + "nodeLength": 29, + "src": "origCount && !handlers.length", + "evalFalse": 13, + "evalTrue": 55 + } + ], + "5141": [ + null, + { + "position": 10, + "nodeLength": 94, + "src": "!special.teardown || special.teardown.call(elem, namespaces, elemData.handle) === false", + "evalFalse": 0, + "evalTrue": 55 + } + ], + "5142": [ + null, + { + "position": 25, + "nodeLength": 68, + "src": "special.teardown.call(elem, namespaces, elemData.handle) === false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5152": [ + null, + { + "position": 2099, + "nodeLength": 30, + "src": "jQuery.isEmptyObject(events)", + "evalFalse": 30, + "evalTrue": 26 + } + ], + "5164": [ + null, + { + "position": 105, + "nodeLength": 58, + "src": "(dataPriv.get(this, \"events\") || {})[event.type] || []", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 105, + "nodeLength": 36, + "src": "dataPriv.get(this, \"events\") || {}", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "5165": [ + null, + { + "position": 178, + "nodeLength": 40, + "src": "jQuery.event.special[event.type] || {}", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "5170": [ + null, + { + "position": 448, + "nodeLength": 20, + "src": "i < arguments.length", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "5177": [ + null, + { + "position": 632, + "nodeLength": 72, + "src": "special.preDispatch && special.preDispatch.call(this, event) === false", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 655, + "nodeLength": 49, + "src": "special.preDispatch.call(this, event) === false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5186": [ + null, + { + "position": 912, + "nodeLength": 64, + "src": "(matched = handlerQueue[i++]) && !event.isPropagationStopped()", + "evalFalse": 1, + "evalTrue": 1 + } + ], + "5190": [ + null, + { + "position": 64, + "nodeLength": 83, + "src": "(handleObj = matched.handlers[j++]) && !event.isImmediatePropagationStopped()", + "evalFalse": 1, + "evalTrue": 3 + } + ], + "5195": [ + null, + { + "position": 175, + "nodeLength": 65, + "src": "!event.rnamespace || event.rnamespace.test(handleObj.namespace)", + "evalFalse": 0, + "evalTrue": 3 + } + ], + "5200": [ + null, + { + "position": -1, + "nodeLength": 84, + "src": "(jQuery.event.special[handleObj.origType] || {}).handle || handleObj.handler", + "evalFalse": 0, + "evalTrue": 3 + }, + { + "position": -1, + "nodeLength": 48, + "src": "jQuery.event.special[handleObj.origType] || {}", + "evalFalse": 0, + "evalTrue": 3 + } + ], + "5203": [ + null, + { + "position": 213, + "nodeLength": 17, + "src": "ret !== undefined", + "evalFalse": 3, + "evalTrue": 0 + } + ], + "5204": [ + null, + { + "position": 14, + "nodeLength": 30, + "src": "(event.result = ret) === false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5214": [ + null, + { + "position": 1811, + "nodeLength": 20, + "src": "special.postDispatch", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "5228": [ + null, + { + "position": 185, + "nodeLength": 464, + "src": "delegateCount && cur.nodeType && !(event.type === \"click\" && event.button >= 1)", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "5232": [ + null, + { + "position": 97, + "nodeLength": 366, + "src": "cur.nodeType && !(event.type === \"click\" && event.button >= 1)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5239": [ + null, + { + "position": 320, + "nodeLength": 43, + "src": "event.type === \"click\" && event.button >= 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 320, + "nodeLength": 22, + "src": "event.type === \"click\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 346, + "nodeLength": 17, + "src": "event.button >= 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5241": [ + null, + { + "position": 13, + "nodeLength": 12, + "src": "cur !== this", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33, + "nodeLength": 22, + "src": "cur.parentNode || this", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5245": [ + null, + { + "position": 132, + "nodeLength": 74, + "src": "cur.nodeType === 1 && !(event.type === \"click\" && cur.disabled === true)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 132, + "nodeLength": 18, + "src": "cur.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 157, + "nodeLength": 47, + "src": "event.type === \"click\" && cur.disabled === true", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 157, + "nodeLength": 22, + "src": "event.type === \"click\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 183, + "nodeLength": 21, + "src": "cur.disabled === true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5248": [ + null, + { + "position": 74, + "nodeLength": 17, + "src": "i < delegateCount", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5254": [ + null, + { + "position": 151, + "nodeLength": 37, + "src": "matchedSelectors[sel] === undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5255": [ + null, + { + "position": 34, + "nodeLength": 22, + "src": "handleObj.needsContext", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5256": [ + null, + { + "position": 32, + "nodeLength": 37, + "src": "jQuery(sel, this).index(cur) > -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5259": [ + null, + { + "position": 374, + "nodeLength": 23, + "src": "matchedSelectors[sel]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5263": [ + null, + { + "position": 569, + "nodeLength": 22, + "src": "matchedHandlers.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5272": [ + null, + { + "position": 1680, + "nodeLength": 31, + "src": "delegateCount < handlers.length", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "5284": [ + null, + { + "position": 54, + "nodeLength": 25, + "src": "jQuery.isFunction(hook)", + "evalFalse": 29, + "evalTrue": 1 + } + ], + "5286": [ + null, + { + "position": 11, + "nodeLength": 18, + "src": "this.originalEvent", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5291": [ + null, + { + "position": 11, + "nodeLength": 18, + "src": "this.originalEvent", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5308": [ + null, + { + "position": 10, + "nodeLength": 31, + "src": "originalEvent[jQuery.expando]", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "5323": [ + null, + { + "position": 10, + "nodeLength": 42, + "src": "this !== safeActiveElement() && this.focus", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10, + "nodeLength": 28, + "src": "this !== safeActiveElement()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5332": [ + null, + { + "position": 10, + "nodeLength": 41, + "src": "this === safeActiveElement() && this.blur", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10, + "nodeLength": 28, + "src": "this === safeActiveElement()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5343": [ + null, + { + "position": 10, + "nodeLength": 74, + "src": "this.type === \"checkbox\" && this.click && jQuery.nodeName(this, \"input\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10, + "nodeLength": 24, + "src": "this.type === \"checkbox\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38, + "nodeLength": 46, + "src": "this.click && jQuery.nodeName(this, \"input\")", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5360": [ + null, + { + "position": 105, + "nodeLength": 49, + "src": "event.result !== undefined && event.originalEvent", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 105, + "nodeLength": 26, + "src": "event.result !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5371": [ + null, + { + "position": 50, + "nodeLength": 24, + "src": "elem.removeEventListener", + "evalFalse": 0, + "evalTrue": 55 + } + ], + "5379": [ + null, + { + "position": 58, + "nodeLength": 33, + "src": "!(this instanceof jQuery.Event)", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "5384": [ + null, + { + "position": 164, + "nodeLength": 15, + "src": "src && src.type", + "evalFalse": 7, + "evalTrue": 1 + } + ], + "5390": [ + null, + { + "position": -1, + "nodeLength": 131, + "src": "src.defaultPrevented || src.defaultPrevented === undefined && src.returnValue === false", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "5391": [ + null, + { + "position": 27, + "nodeLength": 103, + "src": "src.defaultPrevented === undefined && src.returnValue === false", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 55, + "nodeLength": 34, + "src": "src.defaultPrevented === undefined", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "5394": [ + null, + { + "position": 77, + "nodeLength": 25, + "src": "src.returnValue === false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5401": [ + null, + { + "position": 521, + "nodeLength": 41, + "src": "(src.target && src.target.nodeType === 3)", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 521, + "nodeLength": 39, + "src": "src.target && src.target.nodeType === 3", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 535, + "nodeLength": 25, + "src": "src.target.nodeType === 3", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "5414": [ + null, + { + "position": 991, + "nodeLength": 5, + "src": "props", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "5419": [ + null, + { + "position": 1113, + "nodeLength": 36, + "src": "src && src.timeStamp || jQuery.now()", + "evalFalse": 0, + "evalTrue": 8 + }, + { + "position": 1113, + "nodeLength": 20, + "src": "src && src.timeStamp", + "evalFalse": 7, + "evalTrue": 1 + } + ], + "5439": [ + null, + { + "position": 80, + "nodeLength": 22, + "src": "e && !this.isSimulated", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5448": [ + null, + { + "position": 82, + "nodeLength": 22, + "src": "e && !this.isSimulated", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5457": [ + null, + { + "position": 91, + "nodeLength": 22, + "src": "e && !this.isSimulated", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5501": [ + null, + { + "position": 68, + "nodeLength": 51, + "src": "event.which == null && rkeyEvent.test(event.type)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68, + "nodeLength": 19, + "src": "event.which == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5502": [ + null, + { + "position": 11, + "nodeLength": 22, + "src": "event.charCode != null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5506": [ + null, + { + "position": 267, + "nodeLength": 70, + "src": "!event.which && button !== undefined && rmouseEvent.test(event.type)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 283, + "nodeLength": 54, + "src": "button !== undefined && rmouseEvent.test(event.type)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 283, + "nodeLength": 20, + "src": "button !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5507": [ + null, + { + "position": 9, + "nodeLength": 10, + "src": "button & 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5511": [ + null, + { + "position": 52, + "nodeLength": 10, + "src": "button & 2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5515": [ + null, + { + "position": 95, + "nodeLength": 10, + "src": "button & 4", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5552": [ + null, + { + "position": 259, + "nodeLength": 73, + "src": "!related || (related !== target && !jQuery.contains(target, related))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 273, + "nodeLength": 57, + "src": "related !== target && !jQuery.contains(target, related)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 273, + "nodeLength": 18, + "src": "related !== target", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5572": [ + null, + { + "position": 31, + "nodeLength": 48, + "src": "types && types.preventDefault && types.handleObj", + "evalFalse": 42, + "evalTrue": 0 + }, + { + "position": 40, + "nodeLength": 39, + "src": "types.preventDefault && types.handleObj", + "evalFalse": 42, + "evalTrue": 0 + } + ], + "5577": [ + null, + { + "position": 39, + "nodeLength": 19, + "src": "handleObj.namespace", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5585": [ + null, + { + "position": 381, + "nodeLength": 25, + "src": "typeof types === \"object\"", + "evalFalse": 42, + "evalTrue": 0 + } + ], + "5593": [ + null, + { + "position": 554, + "nodeLength": 52, + "src": "selector === false || typeof selector === \"function\"", + "evalFalse": 10, + "evalTrue": 32 + }, + { + "position": 554, + "nodeLength": 18, + "src": "selector === false", + "evalFalse": 42, + "evalTrue": 0 + }, + { + "position": 576, + "nodeLength": 30, + "src": "typeof selector === \"function\"", + "evalFalse": 10, + "evalTrue": 32 + } + ], + "5599": [ + null, + { + "position": 689, + "nodeLength": 12, + "src": "fn === false", + "evalFalse": 42, + "evalTrue": 0 + } + ], + "5629": [ + null, + { + "position": 7, + "nodeLength": 117, + "src": "jQuery.nodeName(elem, \"table\") && jQuery.nodeName(content.nodeType !== 11 ? content : content.firstChild, \"tr\")", + "evalFalse": 14, + "evalTrue": 0 + } + ], + "5630": [ + null, + { + "position": 54, + "nodeLength": 23, + "src": "content.nodeType !== 11", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5632": [ + null, + { + "position": 11, + "nodeLength": 49, + "src": "elem.getElementsByTagName(\"tbody\")[0] || elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5640": [ + null, + { + "position": 16, + "nodeLength": 36, + "src": "elem.getAttribute(\"type\") !== null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5646": [ + null, + { + "position": 58, + "nodeLength": 5, + "src": "match", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5658": [ + null, + { + "position": 73, + "nodeLength": 19, + "src": "dest.nodeType !== 1", + "evalFalse": 6, + "evalTrue": 0 + } + ], + "5663": [ + null, + { + "position": 166, + "nodeLength": 23, + "src": "dataPriv.hasData(src)", + "evalFalse": 6, + "evalTrue": 0 + } + ], + "5668": [ + null, + { + "position": 119, + "nodeLength": 6, + "src": "events", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5673": [ + null, + { + "position": 45, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5681": [ + null, + { + "position": 568, + "nodeLength": 23, + "src": "dataUser.hasData(src)", + "evalFalse": 6, + "evalTrue": 0 + } + ], + "5694": [ + null, + { + "position": 130, + "nodeLength": 55, + "src": "nodeName === \"input\" && rcheckableType.test(src.type)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 130, + "nodeLength": 20, + "src": "nodeName === \"input\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5698": [ + null, + { + "position": 326, + "nodeLength": 47, + "src": "nodeName === \"input\" || nodeName === \"textarea\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 326, + "nodeLength": 20, + "src": "nodeName === \"input\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 350, + "nodeLength": 23, + "src": "nodeName === \"textarea\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5716": [ + null, + { + "position": 311, + "nodeLength": 108, + "src": "isFunction || (l > 1 && typeof value === \"string\" && !support.checkClone && rchecked.test(value))", + "evalFalse": 20, + "evalTrue": 0 + } + ], + "5717": [ + null, + { + "position": 18, + "nodeLength": 87, + "src": "l > 1 && typeof value === \"string\" && !support.checkClone && rchecked.test(value)", + "evalFalse": 20, + "evalTrue": 0 + }, + { + "position": 18, + "nodeLength": 5, + "src": "l > 1", + "evalFalse": 20, + "evalTrue": 0 + }, + { + "position": 27, + "nodeLength": 78, + "src": "typeof value === \"string\" && !support.checkClone && rchecked.test(value)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 27, + "nodeLength": 25, + "src": "typeof value === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5718": [ + null, + { + "position": 32, + "nodeLength": 45, + "src": "!support.checkClone && rchecked.test(value)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5721": [ + null, + { + "position": 47, + "nodeLength": 10, + "src": "isFunction", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5728": [ + null, + { + "position": 655, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 20 + } + ], + "5732": [ + null, + { + "position": 135, + "nodeLength": 32, + "src": "fragment.childNodes.length === 1", + "evalFalse": 6, + "evalTrue": 14 + } + ], + "5737": [ + null, + { + "position": 295, + "nodeLength": 16, + "src": "first || ignored", + "evalFalse": 0, + "evalTrue": 20 + } + ], + "5744": [ + null, + { + "position": 279, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 20, + "evalTrue": 20 + } + ], + "5747": [ + null, + { + "position": 32, + "nodeLength": 14, + "src": "i !== iNoClone", + "evalFalse": 20, + "evalTrue": 0 + } + ], + "5751": [ + null, + { + "position": 122, + "nodeLength": 10, + "src": "hasScripts", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5762": [ + null, + { + "position": 727, + "nodeLength": 10, + "src": "hasScripts", + "evalFalse": 20, + "evalTrue": 0 + } + ], + "5769": [ + null, + { + "position": 204, + "nodeLength": 14, + "src": "i < hasScripts", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5771": [ + null, + { + "position": 37, + "nodeLength": 121, + "src": "rscriptType.test(node.type || \"\") && !dataPriv.access(node, \"globalEval\") && jQuery.contains(doc, node)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55, + "nodeLength": 15, + "src": "node.type || \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5772": [ + null, + { + "position": 44, + "nodeLength": 76, + "src": "!dataPriv.access(node, \"globalEval\") && jQuery.contains(doc, node)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5775": [ + null, + { + "position": 13, + "nodeLength": 8, + "src": "node.src", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5778": [ + null, + { + "position": 87, + "nodeLength": 15, + "src": "jQuery._evalUrl", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5795": [ + null, + { + "position": 19, + "nodeLength": 8, + "src": "selector", + "evalFalse": 10, + "evalTrue": 0 + } + ], + "5798": [ + null, + { + "position": 94, + "nodeLength": 27, + "src": "(node = nodes[i]) != null", + "evalFalse": 10, + "evalTrue": 17 + } + ], + "5799": [ + null, + { + "position": 8, + "nodeLength": 32, + "src": "!keepData && node.nodeType === 1", + "evalFalse": 17, + "evalTrue": 0 + }, + { + "position": 21, + "nodeLength": 19, + "src": "node.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5803": [ + null, + { + "position": 96, + "nodeLength": 15, + "src": "node.parentNode", + "evalFalse": 0, + "evalTrue": 17 + } + ], + "5804": [ + null, + { + "position": 9, + "nodeLength": 55, + "src": "keepData && jQuery.contains(node.ownerDocument, node)", + "evalFalse": 0, + "evalTrue": 17 + } + ], + "5825": [ + null, + { + "position": 167, + "nodeLength": 106, + "src": "!support.noCloneChecked && (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem)", + "evalFalse": 6, + "evalTrue": 0 + }, + { + "position": 196, + "nodeLength": 77, + "src": "(elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 196, + "nodeLength": 43, + "src": "elem.nodeType === 1 || elem.nodeType === 11", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 196, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 219, + "nodeLength": 20, + "src": "elem.nodeType === 11", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5832": [ + null, + { + "position": 202, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5838": [ + null, + { + "position": 616, + "nodeLength": 13, + "src": "dataAndEvents", + "evalFalse": 0, + "evalTrue": 6 + } + ], + "5839": [ + null, + { + "position": 9, + "nodeLength": 17, + "src": "deepDataAndEvents", + "evalFalse": 0, + "evalTrue": 6 + } + ], + "5840": [ + null, + { + "position": 19, + "nodeLength": 29, + "src": "srcElements || getAll(elem)", + "evalFalse": 0, + "evalTrue": 6 + } + ], + "5841": [ + null, + { + "position": 69, + "nodeLength": 31, + "src": "destElements || getAll(clone)", + "evalFalse": 0, + "evalTrue": 6 + } + ], + "5843": [ + null, + { + "position": 144, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 6, + "evalTrue": 6 + } + ], + "5853": [ + null, + { + "position": 1036, + "nodeLength": 23, + "src": "destElements.length > 0", + "evalFalse": 6, + "evalTrue": 0 + } + ], + "5854": [ + null, + { + "position": 33, + "nodeLength": 35, + "src": "!inPage && getAll(elem, \"script\")", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5866": [ + null, + { + "position": 83, + "nodeLength": 33, + "src": "(elem = elems[i]) !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5867": [ + null, + { + "position": 9, + "nodeLength": 18, + "src": "acceptData(elem)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5868": [ + null, + { + "position": 12, + "nodeLength": 33, + "src": "(data = elem[dataPriv.expando])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5869": [ + null, + { + "position": 11, + "nodeLength": 11, + "src": "data.events", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5871": [ + null, + { + "position": 13, + "nodeLength": 15, + "src": "special[type]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5885": [ + null, + { + "position": 512, + "nodeLength": 24, + "src": "elem[dataUser.expando]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5907": [ + null, + { + "position": 11, + "nodeLength": 19, + "src": "value === undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5910": [ + null, + { + "position": 11, + "nodeLength": 66, + "src": "this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11, + "nodeLength": 19, + "src": "this.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34, + "nodeLength": 43, + "src": "this.nodeType === 11 || this.nodeType === 9", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34, + "nodeLength": 20, + "src": "this.nodeType === 11", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58, + "nodeLength": 19, + "src": "this.nodeType === 9", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5919": [ + null, + { + "position": 9, + "nodeLength": 66, + "src": "this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9", + "evalFalse": 0, + "evalTrue": 14 + }, + { + "position": 9, + "nodeLength": 19, + "src": "this.nodeType === 1", + "evalFalse": 0, + "evalTrue": 14 + }, + { + "position": 32, + "nodeLength": 43, + "src": "this.nodeType === 11 || this.nodeType === 9", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32, + "nodeLength": 20, + "src": "this.nodeType === 11", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56, + "nodeLength": 19, + "src": "this.nodeType === 9", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5928": [ + null, + { + "position": 9, + "nodeLength": 66, + "src": "this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9, + "nodeLength": 19, + "src": "this.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32, + "nodeLength": 43, + "src": "this.nodeType === 11 || this.nodeType === 9", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32, + "nodeLength": 20, + "src": "this.nodeType === 11", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56, + "nodeLength": 19, + "src": "this.nodeType === 9", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5937": [ + null, + { + "position": 9, + "nodeLength": 15, + "src": "this.parentNode", + "evalFalse": 0, + "evalTrue": 6 + } + ], + "5945": [ + null, + { + "position": 9, + "nodeLength": 15, + "src": "this.parentNode", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5955": [ + null, + { + "position": 36, + "nodeLength": 26, + "src": "(elem = this[i]) != null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5956": [ + null, + { + "position": 9, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5970": [ + null, + { + "position": 19, + "nodeLength": 21, + "src": "dataAndEvents == null", + "evalFalse": 6, + "evalTrue": 0 + } + ], + "5971": [ + null, + { + "position": 88, + "nodeLength": 25, + "src": "deepDataAndEvents == null", + "evalFalse": 0, + "evalTrue": 6 + } + ], + "5980": [ + null, + { + "position": 15, + "nodeLength": 15, + "src": "this[0] || {}", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "5984": [ + null, + { + "position": 73, + "nodeLength": 42, + "src": "value === undefined && elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 73, + "nodeLength": 19, + "src": "value === undefined", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 96, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "5989": [ + null, + { + "position": 220, + "nodeLength": 135, + "src": "typeof value === \"string\" && !rnoInnerhtml.test(value) && !wrapMap[(rtagName.exec(value) || [\"\", \"\"])[1].toLowerCase()]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 220, + "nodeLength": 25, + "src": "typeof value === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 249, + "nodeLength": 106, + "src": "!rnoInnerhtml.test(value) && !wrapMap[(rtagName.exec(value) || [\"\", \"\"])[1].toLowerCase()]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5990": [ + null, + { + "position": 46, + "nodeLength": 36, + "src": "rtagName.exec(value) || [\"\", \"\"]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5995": [ + null, + { + "position": 14, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5996": [ + null, + { + "position": 14, + "nodeLength": 15, + "src": "this[i] || {}", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5999": [ + null, + { + "position": 98, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6011": [ + null, + { + "position": 783, + "nodeLength": 4, + "src": "elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6024": [ + null, + { + "position": 43, + "nodeLength": 35, + "src": "jQuery.inArray(this, ignored) < 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6026": [ + null, + { + "position": 50, + "nodeLength": 6, + "src": "parent", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6050": [ + null, + { + "position": 109, + "nodeLength": 9, + "src": "i <= last", + "evalFalse": 7, + "evalTrue": 7 + } + ], + "6051": [ + null, + { + "position": 12, + "nodeLength": 10, + "src": "i === last", + "evalFalse": 0, + "evalTrue": 7 + } + ], + "6073": [ + null, + { + "position": 240, + "nodeLength": 21, + "src": "!view || !view.opener", + "evalFalse": 0, + "evalTrue": 72 + } + ], + "6089": [ + null, + { + "position": 67, + "nodeLength": 4, + "src": "!div", + "evalFalse": 1, + "evalTrue": 148 + } + ], + "6102": [ + null, + { + "position": 387, + "nodeLength": 21, + "src": "divStyle.top !== \"1%\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "6105": [ + null, + { + "position": 492, + "nodeLength": 29, + "src": "divStyle.marginLeft === \"2px\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "6106": [ + null, + { + "position": 548, + "nodeLength": 24, + "src": "divStyle.width === \"4px\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "6111": [ + null, + { + "position": 747, + "nodeLength": 30, + "src": "divStyle.marginRight === \"4px\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "6125": [ + null, + { + "position": 1393, + "nodeLength": 10, + "src": "!div.style", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "6133": [ + null, + { + "position": 1638, + "nodeLength": 42, + "src": "div.style.backgroundClip === \"content-box\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "6164": [ + null, + { + "position": 73, + "nodeLength": 29, + "src": "computed || getStyles(elem)", + "evalFalse": 0, + "evalTrue": 148 + } + ], + "6168": [ + null, + { + "position": 200, + "nodeLength": 8, + "src": "computed", + "evalFalse": 0, + "evalTrue": 148 + } + ], + "6169": [ + null, + { + "position": 9, + "nodeLength": 53, + "src": "computed.getPropertyValue(name) || computed[name]", + "evalFalse": 0, + "evalTrue": 148 + } + ], + "6171": [ + null, + { + "position": 72, + "nodeLength": 58, + "src": "ret === \"\" && !jQuery.contains(elem.ownerDocument, elem)", + "evalFalse": 148, + "evalTrue": 0 + }, + { + "position": 72, + "nodeLength": 10, + "src": "ret === \"\"", + "evalFalse": 148, + "evalTrue": 0 + } + ], + "6180": [ + null, + { + "position": 434, + "nodeLength": 76, + "src": "!support.pixelMarginRight() && rnumnonpx.test(ret) && rmargin.test(name)", + "evalFalse": 148, + "evalTrue": 0 + }, + { + "position": 465, + "nodeLength": 45, + "src": "rnumnonpx.test(ret) && rmargin.test(name)", + "evalFalse": 148, + "evalTrue": 0 + } + ], + "6198": [ + null, + { + "position": 1118, + "nodeLength": 17, + "src": "ret !== undefined", + "evalFalse": 0, + "evalTrue": 148 + } + ], + "6212": [ + null, + { + "position": 9, + "nodeLength": 13, + "src": "conditionFn()", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "6246": [ + null, + { + "position": 60, + "nodeLength": 18, + "src": "name in emptyStyle", + "evalFalse": 0, + "evalTrue": 18 + } + ], + "6254": [ + null, + { + "position": 232, + "nodeLength": 3, + "src": "i--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6256": [ + null, + { + "position": 45, + "nodeLength": 18, + "src": "name in emptyStyle", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6267": [ + null, + { + "position": 125, + "nodeLength": 7, + "src": "matches", + "evalFalse": 5, + "evalTrue": 20 + } + ], + "6270": [ + null, + { + "position": 114, + "nodeLength": 13, + "src": "subtract || 0", + "evalFalse": 20, + "evalTrue": 0 + }, + { + "position": 136, + "nodeLength": 20, + "src": "matches[3] || \"px\"", + "evalFalse": 0, + "evalTrue": 20 + } + ], + "6279": [ + null, + { + "position": 92, + "nodeLength": 48, + "src": "extra === (isBorderBox ? \"border\" : \"content\")", + "evalFalse": 24, + "evalTrue": 20 + }, + { + "position": 104, + "nodeLength": 11, + "src": "isBorderBox", + "evalFalse": 20, + "evalTrue": 24 + } + ], + "6284": [ + null, + { + "position": 7, + "nodeLength": 16, + "src": "name === \"width\"", + "evalFalse": 8, + "evalTrue": 16 + } + ], + "6287": [ + null, + { + "position": 273, + "nodeLength": 5, + "src": "i < 4", + "evalFalse": 44, + "evalTrue": 48 + } + ], + "6290": [ + null, + { + "position": 70, + "nodeLength": 18, + "src": "extra === \"margin\"", + "evalFalse": 32, + "evalTrue": 16 + } + ], + "6294": [ + null, + { + "position": 173, + "nodeLength": 11, + "src": "isBorderBox", + "evalFalse": 0, + "evalTrue": 48 + } + ], + "6297": [ + null, + { + "position": 77, + "nodeLength": 19, + "src": "extra === \"content\"", + "evalFalse": 16, + "evalTrue": 32 + } + ], + "6302": [ + null, + { + "position": 257, + "nodeLength": 18, + "src": "extra !== \"margin\"", + "evalFalse": 16, + "evalTrue": 32 + } + ], + "6311": [ + null, + { + "position": 208, + "nodeLength": 19, + "src": "extra !== \"padding\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6326": [ + null, + { + "position": 81, + "nodeLength": 63, + "src": "jQuery.css(elem, \"boxSizing\", false, styles) === \"border-box\"", + "evalFalse": 24, + "evalTrue": 0 + } + ], + "6331": [ + null, + { + "position": 343, + "nodeLength": 28, + "src": "elem.getClientRects().length", + "evalFalse": 0, + "evalTrue": 24 + } + ], + "6338": [ + null, + { + "position": 648, + "nodeLength": 23, + "src": "val <= 0 || val == null", + "evalFalse": 24, + "evalTrue": 0 + }, + { + "position": 648, + "nodeLength": 8, + "src": "val <= 0", + "evalFalse": 24, + "evalTrue": 0 + }, + { + "position": 660, + "nodeLength": 11, + "src": "val == null", + "evalFalse": 24, + "evalTrue": 0 + } + ], + "6342": [ + null, + { + "position": 107, + "nodeLength": 22, + "src": "val < 0 || val == null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 107, + "nodeLength": 7, + "src": "val < 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 118, + "nodeLength": 11, + "src": "val == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6347": [ + null, + { + "position": 231, + "nodeLength": 21, + "src": "rnumnonpx.test(val)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6353": [ + null, + { + "position": 442, + "nodeLength": 79, + "src": "isBorderBox && (support.boxSizingReliable() || val === elem.style[name])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6354": [ + null, + { + "position": 19, + "nodeLength": 57, + "src": "support.boxSizingReliable() || val === elem.style[name]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50, + "nodeLength": 26, + "src": "val === elem.style[name]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6357": [ + null, + { + "position": 579, + "nodeLength": 22, + "src": "parseFloat(val) || 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6365": [ + null, + { + "position": 42, + "nodeLength": 47, + "src": "extra || (isBorderBox ? \"border\" : \"content\")", + "evalFalse": 0, + "evalTrue": 24 + }, + { + "position": 62, + "nodeLength": 11, + "src": "isBorderBox", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6379": [ + null, + { + "position": 10, + "nodeLength": 8, + "src": "computed", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6383": [ + null, + { + "position": 112, + "nodeLength": 10, + "src": "ret === \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6416": [ + null, + { + "position": 57, + "nodeLength": 66, + "src": "!elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style", + "evalFalse": 35, + "evalTrue": 0 + }, + { + "position": 66, + "nodeLength": 57, + "src": "elem.nodeType === 3 || elem.nodeType === 8 || !elem.style", + "evalFalse": 35, + "evalTrue": 0 + }, + { + "position": 66, + "nodeLength": 19, + "src": "elem.nodeType === 3", + "evalFalse": 35, + "evalTrue": 0 + }, + { + "position": 89, + "nodeLength": 34, + "src": "elem.nodeType === 8 || !elem.style", + "evalFalse": 35, + "evalTrue": 0 + }, + { + "position": 89, + "nodeLength": 19, + "src": "elem.nodeType === 8", + "evalFalse": 35, + "evalTrue": 0 + } + ], + "6425": [ + null, + { + "position": 295, + "nodeLength": 106, + "src": "jQuery.cssProps[origName] || (jQuery.cssProps[origName] = vendorPropName(origName) || origName)", + "evalFalse": 0, + "evalTrue": 35 + } + ], + "6426": [ + null, + { + "position": 65, + "nodeLength": 38, + "src": "vendorPropName(origName) || origName", + "evalFalse": 0, + "evalTrue": 2 + } + ], + "6429": [ + null, + { + "position": 479, + "nodeLength": 54, + "src": "jQuery.cssHooks[name] || jQuery.cssHooks[origName]", + "evalFalse": 4, + "evalTrue": 31 + } + ], + "6432": [ + null, + { + "position": 579, + "nodeLength": 19, + "src": "value !== undefined", + "evalFalse": 0, + "evalTrue": 35 + } + ], + "6436": [ + null, + { + "position": 89, + "nodeLength": 64, + "src": "type === \"string\" && (ret = rcssNum.exec(value)) && ret[1]", + "evalFalse": 35, + "evalTrue": 0 + }, + { + "position": 89, + "nodeLength": 17, + "src": "type === \"string\"", + "evalFalse": 26, + "evalTrue": 9 + }, + { + "position": 112, + "nodeLength": 41, + "src": "(ret = rcssNum.exec(value)) && ret[1]", + "evalFalse": 9, + "evalTrue": 0 + } + ], + "6444": [ + null, + { + "position": 319, + "nodeLength": 32, + "src": "value == null || value !== value", + "evalFalse": 35, + "evalTrue": 0 + }, + { + "position": 319, + "nodeLength": 13, + "src": "value == null", + "evalFalse": 35, + "evalTrue": 0 + }, + { + "position": 336, + "nodeLength": 15, + "src": "value !== value", + "evalFalse": 35, + "evalTrue": 0 + } + ], + "6449": [ + null, + { + "position": 464, + "nodeLength": 17, + "src": "type === \"number\"", + "evalFalse": 9, + "evalTrue": 26 + } + ], + "6450": [ + null, + { + "position": 14, + "nodeLength": 63, + "src": "ret && ret[3] || (jQuery.cssNumber[origName] ? \"\" : \"px\")", + "evalFalse": 6, + "evalTrue": 20 + }, + { + "position": 14, + "nodeLength": 15, + "src": "ret && ret[3]", + "evalFalse": 26, + "evalTrue": 0 + }, + { + "position": 35, + "nodeLength": 28, + "src": "jQuery.cssNumber[origName]", + "evalFalse": 20, + "evalTrue": 6 + } + ], + "6454": [ + null, + { + "position": 634, + "nodeLength": 78, + "src": "!support.clearCloneStyle && value === \"\" && name.indexOf(\"background\") === 0", + "evalFalse": 35, + "evalTrue": 0 + }, + { + "position": 662, + "nodeLength": 50, + "src": "value === \"\" && name.indexOf(\"background\") === 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 662, + "nodeLength": 12, + "src": "value === \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 678, + "nodeLength": 34, + "src": "name.indexOf(\"background\") === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6459": [ + null, + { + "position": 847, + "nodeLength": 94, + "src": "!hooks || !(\"set\" in hooks) || (value = hooks.set(elem, value, extra)) !== undefined", + "evalFalse": 0, + "evalTrue": 35 + }, + { + "position": 857, + "nodeLength": 84, + "src": "!(\"set\" in hooks) || (value = hooks.set(elem, value, extra)) !== undefined", + "evalFalse": 0, + "evalTrue": 31 + } + ], + "6460": [ + null, + { + "position": 28, + "nodeLength": 55, + "src": "(value = hooks.set(elem, value, extra)) !== undefined", + "evalFalse": 0, + "evalTrue": 25 + } + ], + "6468": [ + null, + { + "position": 77, + "nodeLength": 86, + "src": "hooks && \"get\" in hooks && (ret = hooks.get(elem, false, extra)) !== undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 86, + "nodeLength": 77, + "src": "\"get\" in hooks && (ret = hooks.get(elem, false, extra)) !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6469": [ + null, + { + "position": 23, + "nodeLength": 53, + "src": "(ret = hooks.get(elem, false, extra)) !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6484": [ + null, + { + "position": 128, + "nodeLength": 106, + "src": "jQuery.cssProps[origName] || (jQuery.cssProps[origName] = vendorPropName(origName) || origName)", + "evalFalse": 0, + "evalTrue": 172 + } + ], + "6485": [ + null, + { + "position": 65, + "nodeLength": 38, + "src": "vendorPropName(origName) || origName", + "evalFalse": 0, + "evalTrue": 16 + } + ], + "6488": [ + null, + { + "position": 302, + "nodeLength": 54, + "src": "jQuery.cssHooks[name] || jQuery.cssHooks[origName]", + "evalFalse": 144, + "evalTrue": 28 + } + ], + "6491": [ + null, + { + "position": 428, + "nodeLength": 23, + "src": "hooks && \"get\" in hooks", + "evalFalse": 144, + "evalTrue": 28 + } + ], + "6496": [ + null, + { + "position": 577, + "nodeLength": 17, + "src": "val === undefined", + "evalFalse": 28, + "evalTrue": 144 + } + ], + "6501": [ + null, + { + "position": 690, + "nodeLength": 46, + "src": "val === \"normal\" && name in cssNormalTransform", + "evalFalse": 172, + "evalTrue": 0 + }, + { + "position": 690, + "nodeLength": 16, + "src": "val === \"normal\"", + "evalFalse": 172, + "evalTrue": 0 + } + ], + "6506": [ + null, + { + "position": 868, + "nodeLength": 21, + "src": "extra === \"\" || extra", + "evalFalse": 68, + "evalTrue": 104 + }, + { + "position": 868, + "nodeLength": 12, + "src": "extra === \"\"", + "evalFalse": 172, + "evalTrue": 0 + } + ], + "6508": [ + null, + { + "position": 39, + "nodeLength": 33, + "src": "extra === true || isFinite(num)", + "evalFalse": 0, + "evalTrue": 104 + }, + { + "position": 39, + "nodeLength": 14, + "src": "extra === true", + "evalFalse": 24, + "evalTrue": 80 + }, + { + "position": 75, + "nodeLength": 8, + "src": "num || 0", + "evalFalse": 80, + "evalTrue": 24 + } + ], + "6517": [ + null, + { + "position": 9, + "nodeLength": 8, + "src": "computed", + "evalFalse": 0, + "evalTrue": 24 + } + ], + "6521": [ + null, + { + "position": -1, + "nodeLength": 411, + "src": "rdisplayswap.test(jQuery.css(elem, \"display\")) && (!elem.getClientRects().length || !elem.getBoundingClientRect().width)", + "evalFalse": 24, + "evalTrue": 0 + } + ], + "6529": [ + null, + { + "position": 340, + "nodeLength": 68, + "src": "!elem.getClientRects().length || !elem.getBoundingClientRect().width", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6539": [ + null, + { + "position": 25, + "nodeLength": 26, + "src": "extra && getStyles(elem)", + "evalFalse": 5, + "evalTrue": 20 + } + ], + "6540": [ + null, + { + "position": 68, + "nodeLength": 152, + "src": "extra && augmentWidthOrHeight(elem, name, extra, jQuery.css(elem, \"boxSizing\", false, styles) === \"border-box\", styles)", + "evalFalse": 25, + "evalTrue": 0 + } + ], + "6544": [ + null, + { + "position": 60, + "nodeLength": 63, + "src": "jQuery.css(elem, \"boxSizing\", false, styles) === \"border-box\"", + "evalFalse": 20, + "evalTrue": 0 + } + ], + "6549": [ + null, + { + "position": 290, + "nodeLength": 88, + "src": "subtract && (matches = rcssNum.exec(value)) && (matches[3] || \"px\") !== \"px\"", + "evalFalse": 25, + "evalTrue": 0 + }, + { + "position": 304, + "nodeLength": 74, + "src": "(matches = rcssNum.exec(value)) && (matches[3] || \"px\") !== \"px\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6550": [ + null, + { + "position": 42, + "nodeLength": 31, + "src": "(matches[3] || \"px\") !== \"px\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 350, + "nodeLength": 20, + "src": "matches[3] || \"px\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6563": [ + null, + { + "position": 8, + "nodeLength": 8, + "src": "computed", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "6564": [ + null, + { + "position": 13, + "nodeLength": 191, + "src": "parseFloat(curCSS(elem, \"marginLeft\")) || elem.getBoundingClientRect().left - swap(elem, {\n marginLeft: 0}, function() {\n return elem.getBoundingClientRect().left;\n})", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "6586": [ + null, + { + "position": 89, + "nodeLength": 25, + "src": "typeof value === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6588": [ + null, + { + "position": 166, + "nodeLength": 5, + "src": "i < 4", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6590": [ + null, + { + "position": 51, + "nodeLength": 42, + "src": "parts[i] || parts[i - 2] || parts[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66, + "nodeLength": 28, + "src": "parts[i - 2] || parts[0]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6597": [ + null, + { + "position": 392, + "nodeLength": 23, + "src": "!rmargin.test(prefix)", + "evalFalse": 1, + "evalTrue": 2 + } + ], + "6609": [ + null, + { + "position": 55, + "nodeLength": 22, + "src": "jQuery.isArray(name)", + "evalFalse": 15, + "evalTrue": 0 + } + ], + "6613": [ + null, + { + "position": 69, + "nodeLength": 7, + "src": "i < len", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6620": [ + null, + { + "position": 276, + "nodeLength": 19, + "src": "value !== undefined", + "evalFalse": 0, + "evalTrue": 15 + } + ], + "6623": [ + null, + { + "position": 430, + "nodeLength": 20, + "src": "arguments.length > 1", + "evalFalse": 7, + "evalTrue": 4 + } + ], + "6638": [ + null, + { + "position": 57, + "nodeLength": 32, + "src": "easing || jQuery.easing._default", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6642": [ + null, + { + "position": 187, + "nodeLength": 48, + "src": "unit || (jQuery.cssNumber[prop] ? \"\" : \"px\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 197, + "nodeLength": 24, + "src": "jQuery.cssNumber[prop]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6647": [ + null, + { + "position": 55, + "nodeLength": 18, + "src": "hooks && hooks.get", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6655": [ + null, + { + "position": 63, + "nodeLength": 21, + "src": "this.options.duration", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6664": [ + null, + { + "position": 336, + "nodeLength": 17, + "src": "this.options.step", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6668": [ + null, + { + "position": 426, + "nodeLength": 18, + "src": "hooks && hooks.set", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6686": [ + null, + { + "position": 163, + "nodeLength": 107, + "src": "tween.elem.nodeType !== 1 || tween.elem[tween.prop] != null && tween.elem.style[tween.prop] == null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 163, + "nodeLength": 25, + "src": "tween.elem.nodeType !== 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6687": [ + null, + { + "position": 32, + "nodeLength": 74, + "src": "tween.elem[tween.prop] != null && tween.elem.style[tween.prop] == null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 198, + "nodeLength": 32, + "src": "tween.elem[tween.prop] != null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35, + "nodeLength": 38, + "src": "tween.elem.style[tween.prop] == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6698": [ + null, + { + "position": 719, + "nodeLength": 28, + "src": "!result || result === \"auto\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 730, + "nodeLength": 17, + "src": "result === \"auto\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6705": [ + null, + { + "position": 151, + "nodeLength": 28, + "src": "jQuery.fx.step[tween.prop]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6707": [ + null, + { + "position": 243, + "nodeLength": 132, + "src": "tween.elem.nodeType === 1 && (tween.elem.style[jQuery.cssProps[tween.prop]] != null || jQuery.cssHooks[tween.prop])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 243, + "nodeLength": 25, + "src": "tween.elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6708": [ + null, + { + "position": 34, + "nodeLength": 95, + "src": "tween.elem.style[jQuery.cssProps[tween.prop]] != null || jQuery.cssHooks[tween.prop]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34, + "nodeLength": 57, + "src": "tween.elem.style[jQuery.cssProps[tween.prop]] != null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6722": [ + null, + { + "position": 8, + "nodeLength": 44, + "src": "tween.elem.nodeType && tween.elem.parentNode", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6752": [ + null, + { + "position": 7, + "nodeLength": 7, + "src": "timerId", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6774": [ + null, + { + "position": 194, + "nodeLength": 12, + "src": "includeWidth", + "evalFalse": 3, + "evalTrue": 0 + } + ], + "6775": [ + null, + { + "position": 225, + "nodeLength": 5, + "src": "i < 4", + "evalFalse": 3, + "evalTrue": 6 + } + ], + "6780": [ + null, + { + "position": 359, + "nodeLength": 12, + "src": "includeWidth", + "evalFalse": 3, + "evalTrue": 0 + } + ], + "6789": [ + null, + { + "position": 27, + "nodeLength": 32, + "src": "Animation.tweeners[prop] || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6792": [ + null, + { + "position": 154, + "nodeLength": 14, + "src": "index < length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6793": [ + null, + { + "position": 10, + "nodeLength": 60, + "src": "(tween = collection[index].call(animation, prop, value))", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6803": [ + null, + { + "position": 86, + "nodeLength": 37, + "src": "\"width\" in props || \"height\" in props", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6807": [ + null, + { + "position": 186, + "nodeLength": 43, + "src": "elem.nodeType && isHiddenWithinTree(elem)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6811": [ + null, + { + "position": 336, + "nodeLength": 11, + "src": "!opts.queue", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6813": [ + null, + { + "position": 52, + "nodeLength": 22, + "src": "hooks.unqueued == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6817": [ + null, + { + "position": 10, + "nodeLength": 15, + "src": "!hooks.unqueued", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6829": [ + null, + { + "position": 32, + "nodeLength": 34, + "src": "!jQuery.queue(elem, \"fx\").length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6839": [ + null, + { + "position": 33, + "nodeLength": 22, + "src": "rfxtypes.test(value)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6841": [ + null, + { + "position": 38, + "nodeLength": 28, + "src": "toggle || value === \"toggle\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48, + "nodeLength": 18, + "src": "value === \"toggle\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6842": [ + null, + { + "position": 76, + "nodeLength": 38, + "src": "value === (hidden ? \"hide\" : \"show\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 88, + "nodeLength": 6, + "src": "hidden", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6846": [ + null, + { + "position": 115, + "nodeLength": 62, + "src": "value === \"show\" && dataShow && dataShow[prop] !== undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 115, + "nodeLength": 16, + "src": "value === \"show\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 135, + "nodeLength": 42, + "src": "dataShow && dataShow[prop] !== undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 147, + "nodeLength": 30, + "src": "dataShow[prop] !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6854": [ + null, + { + "position": 423, + "nodeLength": 58, + "src": "dataShow && dataShow[prop] || jQuery.style(elem, prop)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 423, + "nodeLength": 28, + "src": "dataShow && dataShow[prop]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6860": [ + null, + { + "position": 1554, + "nodeLength": 42, + "src": "!propTween && jQuery.isEmptyObject(orig)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6865": [ + null, + { + "position": 1688, + "nodeLength": 28, + "src": "isBox && elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1697, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6873": [ + null, + { + "position": 346, + "nodeLength": 28, + "src": "dataShow && dataShow.display", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6874": [ + null, + { + "position": 383, + "nodeLength": 22, + "src": "restoreDisplay == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6878": [ + null, + { + "position": 517, + "nodeLength": 18, + "src": "display === \"none\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6879": [ + null, + { + "position": 9, + "nodeLength": 14, + "src": "restoreDisplay", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6885": [ + null, + { + "position": 118, + "nodeLength": 36, + "src": "elem.style.display || restoreDisplay", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6892": [ + null, + { + "position": 897, + "nodeLength": 76, + "src": "display === \"inline\" || display === \"inline-block\" && restoreDisplay != null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 897, + "nodeLength": 20, + "src": "display === \"inline\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 921, + "nodeLength": 52, + "src": "display === \"inline-block\" && restoreDisplay != null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 921, + "nodeLength": 26, + "src": "display === \"inline-block\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 951, + "nodeLength": 22, + "src": "restoreDisplay != null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6893": [ + null, + { + "position": 9, + "nodeLength": 38, + "src": "jQuery.css(elem, \"float\") === \"none\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6896": [ + null, + { + "position": 93, + "nodeLength": 10, + "src": "!propTween", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6900": [ + null, + { + "position": 88, + "nodeLength": 22, + "src": "restoreDisplay == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6902": [ + null, + { + "position": 55, + "nodeLength": 18, + "src": "display === \"none\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6910": [ + null, + { + "position": 3127, + "nodeLength": 13, + "src": "opts.overflow", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6924": [ + null, + { + "position": 65, + "nodeLength": 10, + "src": "!propTween", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6925": [ + null, + { + "position": 9, + "nodeLength": 8, + "src": "dataShow", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6926": [ + null, + { + "position": 10, + "nodeLength": 20, + "src": "\"hidden\" in dataShow", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6934": [ + null, + { + "position": 269, + "nodeLength": 6, + "src": "toggle", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6939": [ + null, + { + "position": 367, + "nodeLength": 6, + "src": "hidden", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6950": [ + null, + { + "position": 123, + "nodeLength": 7, + "src": "!hidden", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6961": [ + null, + { + "position": 910, + "nodeLength": 6, + "src": "hidden", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6962": [ + null, + { + "position": 962, + "nodeLength": 21, + "src": "!(prop in dataShow)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6964": [ + null, + { + "position": 48, + "nodeLength": 6, + "src": "hidden", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6980": [ + null, + { + "position": 104, + "nodeLength": 23, + "src": "jQuery.isArray(value)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6985": [ + null, + { + "position": 208, + "nodeLength": 14, + "src": "index !== name", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6991": [ + null, + { + "position": 326, + "nodeLength": 26, + "src": "hooks && \"expand\" in hooks", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6998": [ + null, + { + "position": 10, + "nodeLength": 19, + "src": "!(index in props)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7020": [ + null, + { + "position": 9, + "nodeLength": 7, + "src": "stopped", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7023": [ + null, + { + "position": 65, + "nodeLength": 22, + "src": "fxNow || createFxNow()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7028": [ + null, + { + "position": 248, + "nodeLength": 35, + "src": "remaining / animation.duration || 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7033": [ + null, + { + "position": 422, + "nodeLength": 14, + "src": "index < length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7039": [ + null, + { + "position": 578, + "nodeLength": 21, + "src": "percent < 1 && length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 578, + "nodeLength": 11, + "src": "percent < 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7055": [ + null, + { + "position": 244, + "nodeLength": 22, + "src": "fxNow || createFxNow()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7060": [ + null, + { + "position": 52, + "nodeLength": 61, + "src": "animation.opts.specialEasing[prop] || animation.opts.easing", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7069": [ + null, + { + "position": 131, + "nodeLength": 7, + "src": "gotoEnd", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7070": [ + null, + { + "position": 185, + "nodeLength": 7, + "src": "stopped", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7074": [ + null, + { + "position": 253, + "nodeLength": 14, + "src": "index < length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7079": [ + null, + { + "position": 402, + "nodeLength": 7, + "src": "gotoEnd", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7092": [ + null, + { + "position": 2279, + "nodeLength": 14, + "src": "index < length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7094": [ + null, + { + "position": 97, + "nodeLength": 6, + "src": "result", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7095": [ + null, + { + "position": 9, + "nodeLength": 32, + "src": "jQuery.isFunction(result.stop)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7105": [ + null, + { + "position": 2655, + "nodeLength": 41, + "src": "jQuery.isFunction(animation.opts.start)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7135": [ + null, + { + "position": 8, + "nodeLength": 26, + "src": "jQuery.isFunction(props)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7146": [ + null, + { + "position": 200, + "nodeLength": 14, + "src": "index < length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7148": [ + null, + { + "position": 59, + "nodeLength": 32, + "src": "Animation.tweeners[prop] || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7156": [ + null, + { + "position": 8, + "nodeLength": 7, + "src": "prepend", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7165": [ + null, + { + "position": 12, + "nodeLength": 34, + "src": "speed && typeof speed === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21, + "nodeLength": 25, + "src": "typeof speed === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7166": [ + null, + { + "position": 13, + "nodeLength": 61, + "src": "fn || !fn && easing || jQuery.isFunction(speed) && speed", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19, + "nodeLength": 55, + "src": "!fn && easing || jQuery.isFunction(speed) && speed", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19, + "nodeLength": 13, + "src": "!fn && easing", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7167": [ + null, + { + "position": 19, + "nodeLength": 35, + "src": "jQuery.isFunction(speed) && speed", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7169": [ + null, + { + "position": 105, + "nodeLength": 64, + "src": "fn && easing || easing && !jQuery.isFunction(easing) && easing", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 105, + "nodeLength": 12, + "src": "fn && easing", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 121, + "nodeLength": 48, + "src": "easing && !jQuery.isFunction(easing) && easing", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 131, + "nodeLength": 38, + "src": "!jQuery.isFunction(easing) && easing", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7173": [ + null, + { + "position": 323, + "nodeLength": 32, + "src": "jQuery.fx.off || document.hidden", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7177": [ + null, + { + "position": 8, + "nodeLength": 32, + "src": "typeof opt.duration !== \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7178": [ + null, + { + "position": 9, + "nodeLength": 32, + "src": "opt.duration in jQuery.fx.speeds", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7188": [ + null, + { + "position": 665, + "nodeLength": 39, + "src": "opt.queue == null || opt.queue === true", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 665, + "nodeLength": 17, + "src": "opt.queue == null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 686, + "nodeLength": 18, + "src": "opt.queue === true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7196": [ + null, + { + "position": 8, + "nodeLength": 28, + "src": "jQuery.isFunction(opt.old)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7200": [ + null, + { + "position": 78, + "nodeLength": 9, + "src": "opt.queue", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7226": [ + null, + { + "position": 210, + "nodeLength": 39, + "src": "empty || dataPriv.get(this, \"finish\")", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7232": [ + null, + { + "position": 464, + "nodeLength": 31, + "src": "empty || optall.queue === false", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 473, + "nodeLength": 22, + "src": "optall.queue === false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7243": [ + null, + { + "position": 120, + "nodeLength": 24, + "src": "typeof type !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7248": [ + null, + { + "position": 228, + "nodeLength": 28, + "src": "clearQueue && type !== false", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 242, + "nodeLength": 14, + "src": "type !== false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7249": [ + null, + { + "position": 16, + "nodeLength": 12, + "src": "type || \"fx\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7254": [ + null, + { + "position": 31, + "nodeLength": 35, + "src": "type != null && type + \"queueHooks\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31, + "nodeLength": 12, + "src": "type != null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7258": [ + null, + { + "position": 143, + "nodeLength": 5, + "src": "index", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7259": [ + null, + { + "position": 10, + "nodeLength": 35, + "src": "data[index] && data[index].stop", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7264": [ + null, + { + "position": 11, + "nodeLength": 57, + "src": "data[index] && data[index].stop && rrun.test(index)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28, + "nodeLength": 40, + "src": "data[index].stop && rrun.test(index)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7270": [ + null, + { + "position": 438, + "nodeLength": 7, + "src": "index--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7271": [ + null, + { + "position": 10, + "nodeLength": 88, + "src": "timers[index].elem === this && (type == null || timers[index].queue === type)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10, + "nodeLength": 29, + "src": "timers[index].elem === this", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7272": [ + null, + { + "position": 39, + "nodeLength": 46, + "src": "type == null || timers[index].queue === type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39, + "nodeLength": 12, + "src": "type == null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55, + "nodeLength": 30, + "src": "timers[index].queue === type", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7283": [ + null, + { + "position": 852, + "nodeLength": 19, + "src": "dequeue || !gotoEnd", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7289": [ + null, + { + "position": 8, + "nodeLength": 14, + "src": "type !== false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7290": [ + null, + { + "position": 11, + "nodeLength": 12, + "src": "type || \"fx\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7298": [ + null, + { + "position": 161, + "nodeLength": 5, + "src": "queue", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7306": [ + null, + { + "position": 333, + "nodeLength": 19, + "src": "hooks && hooks.stop", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7311": [ + null, + { + "position": 484, + "nodeLength": 7, + "src": "index--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7312": [ + null, + { + "position": 10, + "nodeLength": 63, + "src": "timers[index].elem === this && timers[index].queue === type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10, + "nodeLength": 29, + "src": "timers[index].elem === this", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43, + "nodeLength": 30, + "src": "timers[index].queue === type", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7319": [ + null, + { + "position": 741, + "nodeLength": 14, + "src": "index < length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7320": [ + null, + { + "position": 10, + "nodeLength": 39, + "src": "queue[index] && queue[index].finish", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7334": [ + null, + { + "position": 10, + "nodeLength": 43, + "src": "speed == null || typeof speed === \"boolean\"", + "evalFalse": 0, + "evalTrue": 8 + }, + { + "position": 10, + "nodeLength": 13, + "src": "speed == null", + "evalFalse": 0, + "evalTrue": 8 + }, + { + "position": 27, + "nodeLength": 26, + "src": "typeof speed === \"boolean\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7362": [ + null, + { + "position": 82, + "nodeLength": 17, + "src": "i < timers.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7366": [ + null, + { + "position": 83, + "nodeLength": 33, + "src": "!timer() && timers[i] === timer", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95, + "nodeLength": 21, + "src": "timers[i] === timer", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7371": [ + null, + { + "position": 271, + "nodeLength": 14, + "src": "!timers.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7379": [ + null, + { + "position": 37, + "nodeLength": 7, + "src": "timer()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7388": [ + null, + { + "position": 7, + "nodeLength": 8, + "src": "!timerId", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7389": [ + null, + { + "position": 13, + "nodeLength": 28, + "src": "window.requestAnimationFrame", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7396": [ + null, + { + "position": 7, + "nodeLength": 27, + "src": "window.cancelAnimationFrame", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7417": [ + null, + { + "position": 9, + "nodeLength": 9, + "src": "jQuery.fx", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21, + "nodeLength": 32, + "src": "jQuery.fx.speeds[time] || time", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7418": [ + null, + { + "position": 70, + "nodeLength": 12, + "src": "type || \"fx\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7438": [ + null, + { + "position": 289, + "nodeLength": 18, + "src": "input.value !== \"\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "7449": [ + null, + { + "position": 622, + "nodeLength": 19, + "src": "input.value === \"t\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "7458": [ + null, + { + "position": 50, + "nodeLength": 20, + "src": "arguments.length > 1", + "evalFalse": 48, + "evalTrue": 46 + } + ], + "7474": [ + null, + { + "position": 120, + "nodeLength": 41, + "src": "nType === 3 || nType === 8 || nType === 2", + "evalFalse": 170, + "evalTrue": 0 + }, + { + "position": 120, + "nodeLength": 11, + "src": "nType === 3", + "evalFalse": 170, + "evalTrue": 0 + }, + { + "position": 135, + "nodeLength": 26, + "src": "nType === 8 || nType === 2", + "evalFalse": 170, + "evalTrue": 0 + }, + { + "position": 135, + "nodeLength": 11, + "src": "nType === 8", + "evalFalse": 170, + "evalTrue": 0 + }, + { + "position": 150, + "nodeLength": 11, + "src": "nType === 2", + "evalFalse": 170, + "evalTrue": 0 + } + ], + "7479": [ + null, + { + "position": 245, + "nodeLength": 40, + "src": "typeof elem.getAttribute === \"undefined\"", + "evalFalse": 170, + "evalTrue": 0 + } + ], + "7485": [ + null, + { + "position": 450, + "nodeLength": 39, + "src": "nType !== 1 || !jQuery.isXMLDoc(elem)", + "evalFalse": 0, + "evalTrue": 170 + }, + { + "position": 450, + "nodeLength": 11, + "src": "nType !== 1", + "evalFalse": 170, + "evalTrue": 0 + } + ], + "7486": [ + null, + { + "position": 12, + "nodeLength": 108, + "src": "jQuery.attrHooks[name.toLowerCase()] || (jQuery.expr.match.bool.test(name) ? boolHook : undefined)", + "evalFalse": 169, + "evalTrue": 1 + } + ], + "7487": [ + null, + { + "position": 47, + "nodeLength": 35, + "src": "jQuery.expr.match.bool.test(name)", + "evalFalse": 169, + "evalTrue": 0 + } + ], + "7490": [ + null, + { + "position": 627, + "nodeLength": 19, + "src": "value !== undefined", + "evalFalse": 18, + "evalTrue": 152 + } + ], + "7491": [ + null, + { + "position": 9, + "nodeLength": 14, + "src": "value === null", + "evalFalse": 152, + "evalTrue": 0 + } + ], + "7496": [ + null, + { + "position": 91, + "nodeLength": 85, + "src": "hooks && \"set\" in hooks && (ret = hooks.set(elem, value, name)) !== undefined", + "evalFalse": 152, + "evalTrue": 0 + }, + { + "position": 100, + "nodeLength": 76, + "src": "\"set\" in hooks && (ret = hooks.set(elem, value, name)) !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7497": [ + null, + { + "position": 23, + "nodeLength": 52, + "src": "(ret = hooks.set(elem, value, name)) !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7505": [ + null, + { + "position": 924, + "nodeLength": 69, + "src": "hooks && \"get\" in hooks && (ret = hooks.get(elem, name)) !== null", + "evalFalse": 18, + "evalTrue": 0 + }, + { + "position": 933, + "nodeLength": 60, + "src": "\"get\" in hooks && (ret = hooks.get(elem, name)) !== null", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 953, + "nodeLength": 40, + "src": "(ret = hooks.get(elem, name)) !== null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7512": [ + null, + { + "position": 1136, + "nodeLength": 11, + "src": "ret == null", + "evalFalse": 13, + "evalTrue": 5 + } + ], + "7518": [ + null, + { + "position": 10, + "nodeLength": 81, + "src": "!support.radioValue && value === \"radio\" && jQuery.nodeName(elem, \"input\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33, + "nodeLength": 58, + "src": "value === \"radio\" && jQuery.nodeName(elem, \"input\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33, + "nodeLength": 17, + "src": "value === \"radio\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7522": [ + null, + { + "position": 79, + "nodeLength": 3, + "src": "val", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7537": [ + null, + { + "position": 170, + "nodeLength": 37, + "src": "value && value.match(rnothtmlwhite)", + "evalFalse": 0, + "evalTrue": 16 + } + ], + "7539": [ + null, + { + "position": 221, + "nodeLength": 32, + "src": "attrNames && elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 16 + }, + { + "position": 234, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 16 + } + ], + "7540": [ + null, + { + "position": 14, + "nodeLength": 25, + "src": "(name = attrNames[i++])", + "evalFalse": 16, + "evalTrue": 16 + } + ], + "7550": [ + null, + { + "position": 8, + "nodeLength": 15, + "src": "value === false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7562": [ + null, + { + "position": 15, + "nodeLength": 38, + "src": "attrHandle[name] || jQuery.find.attr", + "evalFalse": 0, + "evalTrue": 16 + } + ], + "7568": [ + null, + { + "position": 67, + "nodeLength": 6, + "src": "!isXML", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7573": [ + null, + { + "position": 173, + "nodeLength": 35, + "src": "getter(elem, name, isXML) != null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7590": [ + null, + { + "position": 50, + "nodeLength": 20, + "src": "arguments.length > 1", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "7595": [ + null, + { + "position": 17, + "nodeLength": 30, + "src": "jQuery.propFix[name] || name", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7606": [ + null, + { + "position": 120, + "nodeLength": 41, + "src": "nType === 3 || nType === 8 || nType === 2", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 120, + "nodeLength": 11, + "src": "nType === 3", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 135, + "nodeLength": 26, + "src": "nType === 8 || nType === 2", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 135, + "nodeLength": 11, + "src": "nType === 8", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 150, + "nodeLength": 11, + "src": "nType === 2", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "7610": [ + null, + { + "position": 189, + "nodeLength": 39, + "src": "nType !== 1 || !jQuery.isXMLDoc(elem)", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 189, + "nodeLength": 11, + "src": "nType !== 1", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "7613": [ + null, + { + "position": 44, + "nodeLength": 30, + "src": "jQuery.propFix[name] || name", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "7617": [ + null, + { + "position": 357, + "nodeLength": 19, + "src": "value !== undefined", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "7618": [ + null, + { + "position": 9, + "nodeLength": 85, + "src": "hooks && \"set\" in hooks && (ret = hooks.set(elem, value, name)) !== undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18, + "nodeLength": 76, + "src": "\"set\" in hooks && (ret = hooks.set(elem, value, name)) !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7619": [ + null, + { + "position": 23, + "nodeLength": 52, + "src": "(ret = hooks.set(elem, value, name)) !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7626": [ + null, + { + "position": 549, + "nodeLength": 69, + "src": "hooks && \"get\" in hooks && (ret = hooks.get(elem, name)) !== null", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 558, + "nodeLength": 60, + "src": "\"get\" in hooks && (ret = hooks.get(elem, name)) !== null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 578, + "nodeLength": 40, + "src": "(ret = hooks.get(elem, name)) !== null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7644": [ + null, + { + "position": 403, + "nodeLength": 8, + "src": "tabindex", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7649": [ + null, + { + "position": 9, + "nodeLength": 91, + "src": "rfocusable.test(elem.nodeName) || rclickable.test(elem.nodeName) && elem.href", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7650": [ + null, + { + "position": 40, + "nodeLength": 50, + "src": "rclickable.test(elem.nodeName) && elem.href", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7675": [ + null, + { + "position": 201023, + "nodeLength": 20, + "src": "!support.optSelected", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "7682": [ + null, + { + "position": 89, + "nodeLength": 27, + "src": "parent && parent.parentNode", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7692": [ + null, + { + "position": 89, + "nodeLength": 6, + "src": "parent", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7695": [ + null, + { + "position": 37, + "nodeLength": 17, + "src": "parent.parentNode", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7724": [ + null, + { + "position": 16, + "nodeLength": 34, + "src": "value.match(rnothtmlwhite) || []", + "evalFalse": 0, + "evalTrue": 179 + } + ], + "7730": [ + null, + { + "position": 9, + "nodeLength": 55, + "src": "elem.getAttribute && elem.getAttribute(\"class\") || \"\"", + "evalFalse": 16, + "evalTrue": 75 + }, + { + "position": 9, + "nodeLength": 49, + "src": "elem.getAttribute && elem.getAttribute(\"class\")", + "evalFalse": 16, + "evalTrue": 75 + } + ], + "7738": [ + null, + { + "position": 77, + "nodeLength": 26, + "src": "jQuery.isFunction(value)", + "evalFalse": 41, + "evalTrue": 0 + } + ], + "7744": [ + null, + { + "position": 237, + "nodeLength": 34, + "src": "typeof value === \"string\" && value", + "evalFalse": 0, + "evalTrue": 41 + }, + { + "position": 237, + "nodeLength": 25, + "src": "typeof value === \"string\"", + "evalFalse": 0, + "evalTrue": 41 + } + ], + "7745": [ + null, + { + "position": 14, + "nodeLength": 34, + "src": "value.match(rnothtmlwhite) || []", + "evalFalse": 0, + "evalTrue": 41 + } + ], + "7747": [ + null, + { + "position": 64, + "nodeLength": 20, + "src": "(elem = this[i++])", + "evalFalse": 41, + "evalTrue": 46 + } + ], + "7749": [ + null, + { + "position": 44, + "nodeLength": 67, + "src": "elem.nodeType === 1 && (\" \" + stripAndCollapse(curValue) + \" \")", + "evalFalse": 0, + "evalTrue": 46 + }, + { + "position": 44, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 46 + } + ], + "7751": [ + null, + { + "position": 123, + "nodeLength": 3, + "src": "cur", + "evalFalse": 0, + "evalTrue": 46 + } + ], + "7753": [ + null, + { + "position": 28, + "nodeLength": 24, + "src": "(clazz = classes[j++])", + "evalFalse": 46, + "evalTrue": 46 + } + ], + "7754": [ + null, + { + "position": 12, + "nodeLength": 36, + "src": "cur.indexOf(\" \" + clazz + \" \") < 0", + "evalFalse": 0, + "evalTrue": 46 + } + ], + "7761": [ + null, + { + "position": 267, + "nodeLength": 23, + "src": "curValue !== finalValue", + "evalFalse": 0, + "evalTrue": 46 + } + ], + "7775": [ + null, + { + "position": 77, + "nodeLength": 26, + "src": "jQuery.isFunction(value)", + "evalFalse": 30, + "evalTrue": 0 + } + ], + "7781": [ + null, + { + "position": 240, + "nodeLength": 17, + "src": "!arguments.length", + "evalFalse": 30, + "evalTrue": 0 + } + ], + "7785": [ + null, + { + "position": 310, + "nodeLength": 34, + "src": "typeof value === \"string\" && value", + "evalFalse": 0, + "evalTrue": 30 + }, + { + "position": 310, + "nodeLength": 25, + "src": "typeof value === \"string\"", + "evalFalse": 0, + "evalTrue": 30 + } + ], + "7786": [ + null, + { + "position": 14, + "nodeLength": 34, + "src": "value.match(rnothtmlwhite) || []", + "evalFalse": 0, + "evalTrue": 30 + } + ], + "7788": [ + null, + { + "position": 64, + "nodeLength": 20, + "src": "(elem = this[i++])", + "evalFalse": 30, + "evalTrue": 42 + } + ], + "7792": [ + null, + { + "position": 118, + "nodeLength": 67, + "src": "elem.nodeType === 1 && (\" \" + stripAndCollapse(curValue) + \" \")", + "evalFalse": 0, + "evalTrue": 42 + }, + { + "position": 118, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 42 + } + ], + "7794": [ + null, + { + "position": 197, + "nodeLength": 3, + "src": "cur", + "evalFalse": 0, + "evalTrue": 42 + } + ], + "7796": [ + null, + { + "position": 28, + "nodeLength": 24, + "src": "(clazz = classes[j++])", + "evalFalse": 42, + "evalTrue": 121 + } + ], + "7799": [ + null, + { + "position": 48, + "nodeLength": 37, + "src": "cur.indexOf(\" \" + clazz + \" \") > -1", + "evalFalse": 121, + "evalTrue": 34 + } + ], + "7806": [ + null, + { + "position": 329, + "nodeLength": 23, + "src": "curValue !== finalValue", + "evalFalse": 19, + "evalTrue": 23 + } + ], + "7819": [ + null, + { + "position": 36, + "nodeLength": 50, + "src": "typeof stateVal === \"boolean\" && type === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36, + "nodeLength": 29, + "src": "typeof stateVal === \"boolean\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69, + "nodeLength": 17, + "src": "type === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7820": [ + null, + { + "position": 11, + "nodeLength": 8, + "src": "stateVal", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7823": [ + null, + { + "position": 176, + "nodeLength": 26, + "src": "jQuery.isFunction(value)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7835": [ + null, + { + "position": 49, + "nodeLength": 17, + "src": "type === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7840": [ + null, + { + "position": 94, + "nodeLength": 34, + "src": "value.match(rnothtmlwhite) || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7842": [ + null, + { + "position": 145, + "nodeLength": 31, + "src": "(className = classNames[i++])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7845": [ + null, + { + "position": 69, + "nodeLength": 26, + "src": "self.hasClass(className)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7853": [ + null, + { + "position": 495, + "nodeLength": 41, + "src": "value === undefined || type === \"boolean\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 495, + "nodeLength": 19, + "src": "value === undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 518, + "nodeLength": 18, + "src": "type === \"boolean\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7855": [ + null, + { + "position": 44, + "nodeLength": 9, + "src": "className", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7865": [ + null, + { + "position": 443, + "nodeLength": 17, + "src": "this.setAttribute", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7867": [ + null, + { + "position": 33, + "nodeLength": 28, + "src": "className || value === false", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46, + "nodeLength": 15, + "src": "value === false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7869": [ + null, + { + "position": 47, + "nodeLength": 43, + "src": "dataPriv.get(this, \"__className__\") || \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7881": [ + null, + { + "position": 83, + "nodeLength": 20, + "src": "(elem = this[i++])", + "evalFalse": 3, + "evalTrue": 3 + } + ], + "7882": [ + null, + { + "position": 9, + "nodeLength": 105, + "src": "elem.nodeType === 1 && (\" \" + stripAndCollapse(getClass(elem)) + \" \").indexOf(className) > -1", + "evalFalse": 3, + "evalTrue": 0 + }, + { + "position": 9, + "nodeLength": 19, + "src": "elem.nodeType === 1", + "evalFalse": 0, + "evalTrue": 3 + } + ], + "7883": [ + null, + { + "position": 28, + "nodeLength": 76, + "src": "(\" \" + stripAndCollapse(getClass(elem)) + \" \").indexOf(className) > -1", + "evalFalse": 3, + "evalTrue": 0 + } + ], + "7902": [ + null, + { + "position": 60, + "nodeLength": 17, + "src": "!arguments.length", + "evalFalse": 0, + "evalTrue": 5 + } + ], + "7903": [ + null, + { + "position": 9, + "nodeLength": 4, + "src": "elem", + "evalFalse": 0, + "evalTrue": 5 + } + ], + "7904": [ + null, + { + "position": 13, + "nodeLength": 83, + "src": "jQuery.valHooks[elem.type] || jQuery.valHooks[elem.nodeName.toLowerCase()]", + "evalFalse": 0, + "evalTrue": 5 + } + ], + "7907": [ + null, + { + "position": 108, + "nodeLength": 87, + "src": "hooks && \"get\" in hooks && (ret = hooks.get(elem, \"value\")) !== undefined", + "evalFalse": 0, + "evalTrue": 5 + } + ], + "7908": [ + null, + { + "position": 13, + "nodeLength": 73, + "src": "\"get\" in hooks && (ret = hooks.get(elem, \"value\")) !== undefined", + "evalFalse": 0, + "evalTrue": 5 + } + ], + "7909": [ + null, + { + "position": 24, + "nodeLength": 48, + "src": "(ret = hooks.get(elem, \"value\")) !== undefined", + "evalFalse": 0, + "evalTrue": 5 + } + ], + "7917": [ + null, + { + "position": 299, + "nodeLength": 23, + "src": "typeof ret === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7922": [ + null, + { + "position": 441, + "nodeLength": 11, + "src": "ret == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7933": [ + null, + { + "position": 22, + "nodeLength": 19, + "src": "this.nodeType !== 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7937": [ + null, + { + "position": 72, + "nodeLength": 10, + "src": "isFunction", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7944": [ + null, + { + "position": 245, + "nodeLength": 11, + "src": "val == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7947": [ + null, + { + "position": 292, + "nodeLength": 23, + "src": "typeof val === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7950": [ + null, + { + "position": 352, + "nodeLength": 21, + "src": "jQuery.isArray(val)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7952": [ + null, + { + "position": 13, + "nodeLength": 13, + "src": "value == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7956": [ + null, + { + "position": 494, + "nodeLength": 78, + "src": "jQuery.valHooks[this.type] || jQuery.valHooks[this.nodeName.toLowerCase()]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7959": [ + null, + { + "position": 643, + "nodeLength": 78, + "src": "!hooks || !(\"set\" in hooks) || hooks.set(this, val, \"value\") === undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 653, + "nodeLength": 68, + "src": "!(\"set\" in hooks) || hooks.set(this, val, \"value\") === undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 676, + "nodeLength": 45, + "src": "hooks.set(this, val, \"value\") === undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7972": [ + null, + { + "position": 62, + "nodeLength": 11, + "src": "val != null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7987": [ + null, + { + "position": 94, + "nodeLength": 26, + "src": "elem.type === \"select-one\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7988": [ + null, + { + "position": 136, + "nodeLength": 3, + "src": "one", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7989": [ + null, + { + "position": 164, + "nodeLength": 3, + "src": "one", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7991": [ + null, + { + "position": 214, + "nodeLength": 9, + "src": "index < 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7995": [ + null, + { + "position": 10, + "nodeLength": 3, + "src": "one", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "7999": [ + null, + { + "position": 346, + "nodeLength": 7, + "src": "i < max", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8004": [ + null, + { + "position": 134, + "nodeLength": 238, + "src": "(option.selected || i === index) && !option.disabled && (!option.parentNode.disabled || !jQuery.nodeName(option.parentNode, \"optgroup\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 134, + "nodeLength": 30, + "src": "option.selected || i === index", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 153, + "nodeLength": 11, + "src": "i === index", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8007": [ + null, + { + "position": 118, + "nodeLength": 119, + "src": "!option.disabled && (!option.parentNode.disabled || !jQuery.nodeName(option.parentNode, \"optgroup\"))", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8008": [ + null, + { + "position": 28, + "nodeLength": 88, + "src": "!option.parentNode.disabled || !jQuery.nodeName(option.parentNode, \"optgroup\")", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8015": [ + null, + { + "position": 147, + "nodeLength": 3, + "src": "one", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8033": [ + null, + { + "position": 136, + "nodeLength": 3, + "src": "i--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8038": [ + null, + { + "position": 82, + "nodeLength": 91, + "src": "option.selected = jQuery.inArray(jQuery.valHooks.option.get(option), values) > -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8039": [ + null, + { + "position": 23, + "nodeLength": 67, + "src": "jQuery.inArray(jQuery.valHooks.option.get(option), values) > -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8048": [ + null, + { + "position": 490, + "nodeLength": 10, + "src": "!optionSet", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8061": [ + null, + { + "position": 9, + "nodeLength": 23, + "src": "jQuery.isArray(value)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8062": [ + null, + { + "position": 29, + "nodeLength": 50, + "src": "jQuery.inArray(jQuery(elem).val(), value) > -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8066": [ + null, + { + "position": 200, + "nodeLength": 16, + "src": "!support.checkOn", + "evalFalse": 0, + "evalTrue": 2 + } + ], + "8068": [ + null, + { + "position": 11, + "nodeLength": 37, + "src": "elem.getAttribute(\"value\") === null", + "evalFalse": 5, + "evalTrue": 0 + } + ], + "8086": [ + null, + { + "position": 70, + "nodeLength": 16, + "src": "elem || document", + "evalFalse": 0, + "evalTrue": 7 + } + ], + "8087": [ + null, + { + "position": 100, + "nodeLength": 28, + "src": "hasOwn.call(event, \"type\")", + "evalFalse": 7, + "evalTrue": 0 + } + ], + "8088": [ + null, + { + "position": 167, + "nodeLength": 33, + "src": "hasOwn.call(event, \"namespace\")", + "evalFalse": 7, + "evalTrue": 0 + } + ], + "8090": [ + null, + { + "position": 265, + "nodeLength": 16, + "src": "elem || document", + "evalFalse": 0, + "evalTrue": 7 + } + ], + "8093": [ + null, + { + "position": 338, + "nodeLength": 42, + "src": "elem.nodeType === 3 || elem.nodeType === 8", + "evalFalse": 7, + "evalTrue": 0 + }, + { + "position": 338, + "nodeLength": 19, + "src": "elem.nodeType === 3", + "evalFalse": 7, + "evalTrue": 0 + }, + { + "position": 361, + "nodeLength": 19, + "src": "elem.nodeType === 8", + "evalFalse": 7, + "evalTrue": 0 + } + ], + "8098": [ + null, + { + "position": 486, + "nodeLength": 49, + "src": "rfocusMorph.test(type + jQuery.event.triggered)", + "evalFalse": 7, + "evalTrue": 0 + } + ], + "8102": [ + null, + { + "position": 563, + "nodeLength": 24, + "src": "type.indexOf(\".\") > -1", + "evalFalse": 7, + "evalTrue": 0 + } + ], + "8109": [ + null, + { + "position": 769, + "nodeLength": 38, + "src": "type.indexOf(\":\") < 0 && \"on\" + type", + "evalFalse": 0, + "evalTrue": 7 + }, + { + "position": 769, + "nodeLength": 23, + "src": "type.indexOf(\":\") < 0", + "evalFalse": 0, + "evalTrue": 7 + } + ], + "8112": [ + null, + { + "position": 904, + "nodeLength": 23, + "src": "event[jQuery.expando]", + "evalFalse": 7, + "evalTrue": 0 + } + ], + "8114": [ + null, + { + "position": 63, + "nodeLength": 34, + "src": "typeof event === \"object\" && event", + "evalFalse": 7, + "evalTrue": 0 + }, + { + "position": 64, + "nodeLength": 25, + "src": "typeof event === \"object\"", + "evalFalse": 7, + "evalTrue": 0 + } + ], + "8117": [ + null, + { + "position": 1103, + "nodeLength": 12, + "src": "onlyHandlers", + "evalFalse": 7, + "evalTrue": 0 + } + ], + "8119": [ + null, + { + "position": 1190, + "nodeLength": 15, + "src": "event.namespace", + "evalFalse": 7, + "evalTrue": 0 + } + ], + "8125": [ + null, + { + "position": 1382, + "nodeLength": 13, + "src": "!event.target", + "evalFalse": 0, + "evalTrue": 7 + } + ], + "8130": [ + null, + { + "position": 1520, + "nodeLength": 12, + "src": "data == null", + "evalFalse": 7, + "evalTrue": 0 + } + ], + "8135": [ + null, + { + "position": 1655, + "nodeLength": 34, + "src": "jQuery.event.special[type] || {}", + "evalFalse": 0, + "evalTrue": 7 + } + ], + "8136": [ + null, + { + "position": 1698, + "nodeLength": 81, + "src": "!onlyHandlers && special.trigger && special.trigger.apply(elem, data) === false", + "evalFalse": 7, + "evalTrue": 0 + }, + { + "position": 1715, + "nodeLength": 64, + "src": "special.trigger && special.trigger.apply(elem, data) === false", + "evalFalse": 7, + "evalTrue": 0 + }, + { + "position": 1734, + "nodeLength": 45, + "src": "special.trigger.apply(elem, data) === false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8142": [ + null, + { + "position": 1974, + "nodeLength": 62, + "src": "!onlyHandlers && !special.noBubble && !jQuery.isWindow(elem)", + "evalFalse": 0, + "evalTrue": 7 + }, + { + "position": 1991, + "nodeLength": 45, + "src": "!special.noBubble && !jQuery.isWindow(elem)", + "evalFalse": 0, + "evalTrue": 7 + } + ], + "8144": [ + null, + { + "position": 18, + "nodeLength": 28, + "src": "special.delegateType || type", + "evalFalse": 0, + "evalTrue": 7 + } + ], + "8145": [ + null, + { + "position": 56, + "nodeLength": 38, + "src": "!rfocusMorph.test(bubbleType + type)", + "evalFalse": 0, + "evalTrue": 7 + } + ], + "8148": [ + null, + { + "position": 141, + "nodeLength": 3, + "src": "cur", + "evalFalse": 7, + "evalTrue": 28 + } + ], + "8154": [ + null, + { + "position": 309, + "nodeLength": 42, + "src": "tmp === (elem.ownerDocument || document)", + "evalFalse": 0, + "evalTrue": 7 + }, + { + "position": 319, + "nodeLength": 30, + "src": "elem.ownerDocument || document", + "evalFalse": 0, + "evalTrue": 7 + } + ], + "8155": [ + null, + { + "position": 21, + "nodeLength": 45, + "src": "tmp.defaultView || tmp.parentWindow || window", + "evalFalse": 0, + "evalTrue": 7 + }, + { + "position": 35, + "nodeLength": 26, + "src": "tmp.parentWindow || window", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8161": [ + null, + { + "position": 2533, + "nodeLength": 57, + "src": "(cur = eventPath[i++]) && !event.isPropagationStopped()", + "evalFalse": 7, + "evalTrue": 42 + } + ], + "8163": [ + null, + { + "position": 18, + "nodeLength": 5, + "src": "i > 1", + "evalFalse": 7, + "evalTrue": 35 + } + ], + "8165": [ + null, + { + "position": 28, + "nodeLength": 24, + "src": "special.bindType || type", + "evalFalse": 0, + "evalTrue": 7 + } + ], + "8168": [ + null, + { + "position": 109, + "nodeLength": 88, + "src": "(dataPriv.get(cur, \"events\") || {})[event.type] && dataPriv.get(cur, \"handle\")", + "evalFalse": 42, + "evalTrue": 0 + }, + { + "position": 109, + "nodeLength": 35, + "src": "dataPriv.get(cur, \"events\") || {}", + "evalFalse": 0, + "evalTrue": 42 + } + ], + "8170": [ + null, + { + "position": 207, + "nodeLength": 6, + "src": "handle", + "evalFalse": 42, + "evalTrue": 0 + } + ], + "8175": [ + null, + { + "position": 288, + "nodeLength": 23, + "src": "ontype && cur[ontype]", + "evalFalse": 42, + "evalTrue": 0 + } + ], + "8176": [ + null, + { + "position": 321, + "nodeLength": 43, + "src": "handle && handle.apply && acceptData(cur)", + "evalFalse": 42, + "evalTrue": 0 + }, + { + "position": 331, + "nodeLength": 33, + "src": "handle.apply && acceptData(cur)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8178": [ + null, + { + "position": 56, + "nodeLength": 22, + "src": "event.result === false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8186": [ + null, + { + "position": 3173, + "nodeLength": 44, + "src": "!onlyHandlers && !event.isDefaultPrevented()", + "evalFalse": 0, + "evalTrue": 7 + } + ], + "8188": [ + null, + { + "position": 12, + "nodeLength": 110, + "src": "(!special._default || special._default.apply(eventPath.pop(), data) === false) && acceptData(elem)", + "evalFalse": 0, + "evalTrue": 7 + }, + { + "position": 12, + "nodeLength": 82, + "src": "!special._default || special._default.apply(eventPath.pop(), data) === false", + "evalFalse": 0, + "evalTrue": 7 + } + ], + "8189": [ + null, + { + "position": 24, + "nodeLength": 57, + "src": "special._default.apply(eventPath.pop(), data) === false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8194": [ + null, + { + "position": 174, + "nodeLength": 71, + "src": "ontype && jQuery.isFunction(elem[type]) && !jQuery.isWindow(elem)", + "evalFalse": 7, + "evalTrue": 0 + }, + { + "position": 184, + "nodeLength": 61, + "src": "jQuery.isFunction(elem[type]) && !jQuery.isWindow(elem)", + "evalFalse": 7, + "evalTrue": 0 + } + ], + "8199": [ + null, + { + "position": 110, + "nodeLength": 3, + "src": "tmp", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8208": [ + null, + { + "position": 347, + "nodeLength": 3, + "src": "tmp", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8244": [ + null, + { + "position": 32, + "nodeLength": 4, + "src": "elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8258": [ + null, + { + "position": 10, + "nodeLength": 20, + "src": "arguments.length > 0", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "8266": [ + null, + { + "position": 48, + "nodeLength": 15, + "src": "fnOut || fnOver", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8284": [ + null, + { + "position": 216002, + "nodeLength": 16, + "src": "!support.focusin", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "8294": [ + null, + { + "position": 15, + "nodeLength": 26, + "src": "this.ownerDocument || this", + "evalFalse": 0, + "evalTrue": 6 + } + ], + "8297": [ + null, + { + "position": 98, + "nodeLength": 9, + "src": "!attaches", + "evalFalse": 4, + "evalTrue": 2 + } + ], + "8300": [ + null, + { + "position": 201, + "nodeLength": 13, + "src": "attaches || 0", + "evalFalse": 2, + "evalTrue": 4 + } + ], + "8303": [ + null, + { + "position": 15, + "nodeLength": 26, + "src": "this.ownerDocument || this", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8306": [ + null, + { + "position": 102, + "nodeLength": 9, + "src": "!attaches", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8328": [ + null, + { + "position": 17, + "nodeLength": 33, + "src": "!data || typeof data !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26, + "nodeLength": 24, + "src": "typeof data !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8340": [ + null, + { + "position": 280, + "nodeLength": 56, + "src": "!xml || xml.getElementsByTagName(\"parsererror\").length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8356": [ + null, + { + "position": 19, + "nodeLength": 21, + "src": "jQuery.isArray(obj)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8360": [ + null, + { + "position": 9, + "nodeLength": 38, + "src": "traditional || rbracket.test(prefix)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8369": [ + null, + { + "position": 34, + "nodeLength": 34, + "src": "typeof v === \"object\" && v != null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34, + "nodeLength": 21, + "src": "typeof v === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59, + "nodeLength": 9, + "src": "v != null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8377": [ + null, + { + "position": 474, + "nodeLength": 47, + "src": "!traditional && jQuery.type(obj) === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 490, + "nodeLength": 31, + "src": "jQuery.type(obj) === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8399": [ + null, + { + "position": 82, + "nodeLength": 36, + "src": "jQuery.isFunction(valueOrFunction)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8404": [ + null, + { + "position": 57, + "nodeLength": 13, + "src": "value == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8408": [ + null, + { + "position": 428, + "nodeLength": 65, + "src": "jQuery.isArray(a) || (a.jquery && !jQuery.isPlainObject(a))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 453, + "nodeLength": 38, + "src": "a.jquery && !jQuery.isPlainObject(a)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8437": [ + null, + { + "position": 132, + "nodeLength": 8, + "src": "elements", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8443": [ + null, + { + "position": 99, + "nodeLength": 177, + "src": "this.name && !jQuery(this).is(\":disabled\") && rsubmittable.test(this.nodeName) && !rsubmitterTypes.test(type) && (this.checked || !rcheckableType.test(type))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 112, + "nodeLength": 164, + "src": "!jQuery(this).is(\":disabled\") && rsubmittable.test(this.nodeName) && !rsubmitterTypes.test(type) && (this.checked || !rcheckableType.test(type))", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8444": [ + null, + { + "position": 40, + "nodeLength": 123, + "src": "rsubmittable.test(this.nodeName) && !rsubmitterTypes.test(type) && (this.checked || !rcheckableType.test(type))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 194, + "nodeLength": 85, + "src": "!rsubmitterTypes.test(type) && (this.checked || !rcheckableType.test(type))", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8445": [ + null, + { + "position": 38, + "nodeLength": 44, + "src": "this.checked || !rcheckableType.test(type)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8450": [ + null, + { + "position": 45, + "nodeLength": 11, + "src": "val == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8454": [ + null, + { + "position": 92, + "nodeLength": 21, + "src": "jQuery.isArray(val)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8508": [ + null, + { + "position": 9, + "nodeLength": 38, + "src": "typeof dataTypeExpression !== \"string\"", + "evalFalse": 3, + "evalTrue": 2 + } + ], + "8515": [ + null, + { + "position": 38, + "nodeLength": 61, + "src": "dataTypeExpression.toLowerCase().match(rnothtmlwhite) || []", + "evalFalse": 0, + "evalTrue": 5 + } + ], + "8517": [ + null, + { + "position": 228, + "nodeLength": 25, + "src": "jQuery.isFunction(func)", + "evalFalse": 0, + "evalTrue": 5 + } + ], + "8520": [ + null, + { + "position": 65, + "nodeLength": 29, + "src": "(dataType = dataTypes[i++])", + "evalFalse": 5, + "evalTrue": 6 + } + ], + "8523": [ + null, + { + "position": 39, + "nodeLength": 21, + "src": "dataType[0] === \"+\"", + "evalFalse": 6, + "evalTrue": 0 + } + ], + "8524": [ + null, + { + "position": 17, + "nodeLength": 26, + "src": "dataType.slice(1) || \"*\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8525": [ + null, + { + "position": 76, + "nodeLength": 27, + "src": "structure[dataType] || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8529": [ + null, + { + "position": 32, + "nodeLength": 27, + "src": "structure[dataType] || []", + "evalFalse": 0, + "evalTrue": 6 + } + ], + "8540": [ + null, + { + "position": 42, + "nodeLength": 24, + "src": "structure === transports", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8545": [ + null, + { + "position": 64, + "nodeLength": 27, + "src": "structure[dataType] || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8547": [ + null, + { + "position": 93, + "nodeLength": 101, + "src": "typeof dataTypeOrTransport === \"string\" && !seekingTransport && !inspected[dataTypeOrTransport]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93, + "nodeLength": 39, + "src": "typeof dataTypeOrTransport === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8548": [ + null, + { + "position": 46, + "nodeLength": 54, + "src": "!seekingTransport && !inspected[dataTypeOrTransport]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8553": [ + null, + { + "position": 324, + "nodeLength": 16, + "src": "seekingTransport", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8560": [ + null, + { + "position": 669, + "nodeLength": 72, + "src": "inspect(options.dataTypes[0]) || !inspected[\"*\"] && inspect(\"*\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 706, + "nodeLength": 35, + "src": "!inspected[\"*\"] && inspect(\"*\")", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8568": [ + null, + { + "position": 30, + "nodeLength": 37, + "src": "jQuery.ajaxSettings.flatOptions || {}", + "evalFalse": 0, + "evalTrue": 2 + } + ], + "8571": [ + null, + { + "position": 8, + "nodeLength": 24, + "src": "src[key] !== undefined", + "evalFalse": 0, + "evalTrue": 5 + } + ], + "8572": [ + null, + { + "position": 6, + "nodeLength": 18, + "src": "flatOptions[key]", + "evalFalse": 5, + "evalTrue": 0 + }, + { + "position": 38, + "nodeLength": 21, + "src": "deep || (deep = {})", + "evalFalse": 0, + "evalTrue": 5 + } + ], + "8575": [ + null, + { + "position": 228, + "nodeLength": 4, + "src": "deep", + "evalFalse": 0, + "evalTrue": 2 + } + ], + "8593": [ + null, + { + "position": 170, + "nodeLength": 22, + "src": "dataTypes[0] === \"*\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8595": [ + null, + { + "position": 29, + "nodeLength": 16, + "src": "ct === undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8596": [ + null, + { + "position": 9, + "nodeLength": 55, + "src": "s.mimeType || jqXHR.getResponseHeader(\"Content-Type\")", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8601": [ + null, + { + "position": 378, + "nodeLength": 2, + "src": "ct", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8603": [ + null, + { + "position": 9, + "nodeLength": 47, + "src": "contents[type] && contents[type].test(ct)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8611": [ + null, + { + "position": 600, + "nodeLength": 27, + "src": "dataTypes[0] in responses", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8617": [ + null, + { + "position": 9, + "nodeLength": 62, + "src": "!dataTypes[0] || s.converters[type + \" \" + dataTypes[0]]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8621": [ + null, + { + "position": 126, + "nodeLength": 14, + "src": "!firstDataType", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8627": [ + null, + { + "position": 288, + "nodeLength": 30, + "src": "finalDataType || firstDataType", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8633": [ + null, + { + "position": 1120, + "nodeLength": 13, + "src": "finalDataType", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8634": [ + null, + { + "position": 8, + "nodeLength": 32, + "src": "finalDataType !== dataTypes[0]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8652": [ + null, + { + "position": 227, + "nodeLength": 14, + "src": "dataTypes[1]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8661": [ + null, + { + "position": 427, + "nodeLength": 7, + "src": "current", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8663": [ + null, + { + "position": 9, + "nodeLength": 27, + "src": "s.responseFields[current]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8668": [ + null, + { + "position": 143, + "nodeLength": 34, + "src": "!prev && isSuccess && s.dataFilter", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 152, + "nodeLength": 25, + "src": "isSuccess && s.dataFilter", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8675": [ + null, + { + "position": 296, + "nodeLength": 7, + "src": "current", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8678": [ + null, + { + "position": 72, + "nodeLength": 15, + "src": "current === \"*\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8683": [ + null, + { + "position": 207, + "nodeLength": 32, + "src": "prev !== \"*\" && prev !== current", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 207, + "nodeLength": 12, + "src": "prev !== \"*\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 223, + "nodeLength": 16, + "src": "prev !== current", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8686": [ + null, + { + "position": 44, + "nodeLength": 66, + "src": "converters[prev + \" \" + current] || converters[\"* \" + current]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8689": [ + null, + { + "position": 156, + "nodeLength": 5, + "src": "!conv", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8694": [ + null, + { + "position": 79, + "nodeLength": 20, + "src": "tmp[1] === current", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8697": [ + null, + { + "position": 69, + "nodeLength": 76, + "src": "converters[prev + \" \" + tmp[0]] || converters[\"* \" + tmp[0]]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8699": [ + null, + { + "position": 159, + "nodeLength": 4, + "src": "conv", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8702": [ + null, + { + "position": 58, + "nodeLength": 13, + "src": "conv === true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8706": [ + null, + { + "position": 190, + "nodeLength": 28, + "src": "converters[conv2] !== true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8717": [ + null, + { + "position": 873, + "nodeLength": 13, + "src": "conv !== true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8720": [ + null, + { + "position": 79, + "nodeLength": 16, + "src": "conv && s.throws", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8728": [ + null, + { + "position": 46, + "nodeLength": 4, + "src": "conv", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8821": [ + null, + { + "position": 10, + "nodeLength": 8, + "src": "settings", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "8837": [ + null, + { + "position": 62, + "nodeLength": 23, + "src": "typeof url === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8843": [ + null, + { + "position": 180, + "nodeLength": 13, + "src": "options || {}", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8876": [ + null, + { + "position": 561, + "nodeLength": 14, + "src": "s.context || s", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8879": [ + null, + { + "position": -1, + "nodeLength": 71, + "src": "s.context && (callbackContext.nodeType || callbackContext.jquery)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8880": [ + null, + { + "position": 18, + "nodeLength": 50, + "src": "callbackContext.nodeType || callbackContext.jquery", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8889": [ + null, + { + "position": 977, + "nodeLength": 18, + "src": "s.statusCode || {}", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8905": [ + null, + { + "position": 27, + "nodeLength": 9, + "src": "completed", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8906": [ + null, + { + "position": 12, + "nodeLength": 16, + "src": "!responseHeaders", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8908": [ + null, + { + "position": 47, + "nodeLength": 48, + "src": "(match = rheaders.exec(responseHeadersString))", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8914": [ + null, + { + "position": 326, + "nodeLength": 13, + "src": "match == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8919": [ + null, + { + "position": 13, + "nodeLength": 9, + "src": "completed", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8924": [ + null, + { + "position": 11, + "nodeLength": 17, + "src": "completed == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8926": [ + null, + { + "position": 50, + "nodeLength": 49, + "src": "requestHeadersNames[name.toLowerCase()] || name", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8934": [ + null, + { + "position": 11, + "nodeLength": 17, + "src": "completed == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8943": [ + null, + { + "position": 26, + "nodeLength": 3, + "src": "map", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8944": [ + null, + { + "position": 12, + "nodeLength": 9, + "src": "completed", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8961": [ + null, + { + "position": 22, + "nodeLength": 22, + "src": "statusText || strAbort", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8962": [ + null, + { + "position": 56, + "nodeLength": 9, + "src": "transport", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8976": [ + null, + { + "position": -1, + "nodeLength": 29, + "src": "url || s.url || location.href", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6, + "nodeLength": 22, + "src": "s.url || location.href", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8980": [ + null, + { + "position": 3528, + "nodeLength": 52, + "src": "options.method || options.type || s.method || s.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3546, + "nodeLength": 34, + "src": "options.type || s.method || s.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3562, + "nodeLength": 18, + "src": "s.method || s.type", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8983": [ + null, + { + "position": 3629, + "nodeLength": 66, + "src": "(s.dataType || \"*\").toLowerCase().match(rnothtmlwhite) || [\"\"]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3629, + "nodeLength": 17, + "src": "s.dataType || \"*\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8986": [ + null, + { + "position": 3795, + "nodeLength": 21, + "src": "s.crossDomain == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8998": [ + null, + { + "position": 193, + "nodeLength": 100, + "src": "originAnchor.protocol + \"//\" + originAnchor.host !== urlAnchor.protocol + \"//\" + urlAnchor.host", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9009": [ + null, + { + "position": 4561, + "nodeLength": 53, + "src": "s.data && s.processData && typeof s.data !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4571, + "nodeLength": 43, + "src": "s.processData && typeof s.data !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4588, + "nodeLength": 26, + "src": "typeof s.data !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9017": [ + null, + { + "position": 4830, + "nodeLength": 9, + "src": "completed", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9023": [ + null, + { + "position": 5021, + "nodeLength": 24, + "src": "jQuery.event && s.global", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9026": [ + null, + { + "position": 5092, + "nodeLength": 36, + "src": "fireGlobals && jQuery.active++ === 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5107, + "nodeLength": 21, + "src": "jQuery.active++ === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9042": [ + null, + { + "position": 5579, + "nodeLength": 13, + "src": "!s.hasContent", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9048": [ + null, + { + "position": 150, + "nodeLength": 6, + "src": "s.data", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9049": [ + null, + { + "position": 19, + "nodeLength": 23, + "src": "rquery.test(cacheURL)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9056": [ + null, + { + "position": 377, + "nodeLength": 17, + "src": "s.cache === false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9058": [ + null, + { + "position": 71, + "nodeLength": 23, + "src": "rquery.test(cacheURL)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9065": [ + null, + { + "position": 6336, + "nodeLength": 106, + "src": "s.data && s.processData && (s.contentType || \"\").indexOf(\"application/x-www-form-urlencoded\") === 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6346, + "nodeLength": 96, + "src": "s.processData && (s.contentType || \"\").indexOf(\"application/x-www-form-urlencoded\") === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9066": [ + null, + { + "position": 21, + "nodeLength": 74, + "src": "(s.contentType || \"\").indexOf(\"application/x-www-form-urlencoded\") === 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6371, + "nodeLength": 19, + "src": "s.contentType || \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9071": [ + null, + { + "position": 6581, + "nodeLength": 12, + "src": "s.ifModified", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9072": [ + null, + { + "position": 9, + "nodeLength": 31, + "src": "jQuery.lastModified[cacheURL]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9075": [ + null, + { + "position": 142, + "nodeLength": 23, + "src": "jQuery.etag[cacheURL]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9081": [ + null, + { + "position": 6907, + "nodeLength": 72, + "src": "s.data && s.hasContent && s.contentType !== false || options.contentType", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6907, + "nodeLength": 49, + "src": "s.data && s.hasContent && s.contentType !== false", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6917, + "nodeLength": 39, + "src": "s.hasContent && s.contentType !== false", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6933, + "nodeLength": 23, + "src": "s.contentType !== false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9088": [ + null, + { + "position": 39, + "nodeLength": 49, + "src": "s.dataTypes[0] && s.accepts[s.dataTypes[0]]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9090": [ + null, + { + "position": 38, + "nodeLength": 24, + "src": "s.dataTypes[0] !== \"*\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9100": [ + null, + { + "position": 7517, + "nodeLength": 92, + "src": "s.beforeSend && (s.beforeSend.call(callbackContext, jqXHR, s) === false || completed)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9101": [ + null, + { + "position": 20, + "nodeLength": 69, + "src": "s.beforeSend.call(callbackContext, jqXHR, s) === false || completed", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20, + "nodeLength": 56, + "src": "s.beforeSend.call(callbackContext, jqXHR, s) === false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9119": [ + null, + { + "position": 8021, + "nodeLength": 10, + "src": "!transport", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9125": [ + null, + { + "position": 59, + "nodeLength": 11, + "src": "fireGlobals", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9130": [ + null, + { + "position": 206, + "nodeLength": 9, + "src": "completed", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9135": [ + null, + { + "position": 266, + "nodeLength": 24, + "src": "s.async && s.timeout > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 277, + "nodeLength": 13, + "src": "s.timeout > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9147": [ + null, + { + "position": 53, + "nodeLength": 9, + "src": "completed", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9162": [ + null, + { + "position": 131, + "nodeLength": 9, + "src": "completed", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9169": [ + null, + { + "position": 226, + "nodeLength": 12, + "src": "timeoutTimer", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9178": [ + null, + { + "position": 487, + "nodeLength": 13, + "src": "headers || \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9181": [ + null, + { + "position": 546, + "nodeLength": 10, + "src": "status > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9184": [ + null, + { + "position": 612, + "nodeLength": 47, + "src": "status >= 200 && status < 300 || status === 304", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 612, + "nodeLength": 29, + "src": "status >= 200 && status < 300", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 612, + "nodeLength": 13, + "src": "status >= 200", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 629, + "nodeLength": 12, + "src": "status < 300", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 645, + "nodeLength": 14, + "src": "status === 304", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9187": [ + null, + { + "position": 694, + "nodeLength": 9, + "src": "responses", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9195": [ + null, + { + "position": 958, + "nodeLength": 9, + "src": "isSuccess", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9198": [ + null, + { + "position": 96, + "nodeLength": 12, + "src": "s.ifModified", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9200": [ + null, + { + "position": 71, + "nodeLength": 8, + "src": "modified", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9204": [ + null, + { + "position": 202, + "nodeLength": 8, + "src": "modified", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9210": [ + null, + { + "position": 413, + "nodeLength": 35, + "src": "status === 204 || s.type === \"HEAD\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 413, + "nodeLength": 14, + "src": "status === 204", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 431, + "nodeLength": 17, + "src": "s.type === \"HEAD\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9214": [ + null, + { + "position": 525, + "nodeLength": 14, + "src": "status === 304", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9228": [ + null, + { + "position": 101, + "nodeLength": 21, + "src": "status || !statusText", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9230": [ + null, + { + "position": 38, + "nodeLength": 10, + "src": "status < 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9238": [ + null, + { + "position": 2042, + "nodeLength": 30, + "src": "nativeStatusText || statusText", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9241": [ + null, + { + "position": 2110, + "nodeLength": 9, + "src": "isSuccess", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9251": [ + null, + { + "position": 2397, + "nodeLength": 11, + "src": "fireGlobals", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9252": [ + null, + { + "position": 33, + "nodeLength": 9, + "src": "isSuccess", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9253": [ + null, + { + "position": 85, + "nodeLength": 9, + "src": "isSuccess", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9259": [ + null, + { + "position": 2638, + "nodeLength": 11, + "src": "fireGlobals", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9263": [ + null, + { + "position": 113, + "nodeLength": 20, + "src": "!(--jQuery.active)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9285": [ + null, + { + "position": 59, + "nodeLength": 25, + "src": "jQuery.isFunction(data)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9286": [ + null, + { + "position": 11, + "nodeLength": 16, + "src": "type || callback", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9298": [ + null, + { + "position": 106, + "nodeLength": 34, + "src": "jQuery.isPlainObject(url) && url", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9322": [ + null, + { + "position": 21, + "nodeLength": 9, + "src": "this[0]", + "evalFalse": 0, + "evalTrue": 6 + } + ], + "9323": [ + null, + { + "position": 9, + "nodeLength": 25, + "src": "jQuery.isFunction(html)", + "evalFalse": 6, + "evalTrue": 0 + } + ], + "9330": [ + null, + { + "position": 207, + "nodeLength": 20, + "src": "this[0].parentNode", + "evalFalse": 0, + "evalTrue": 6 + } + ], + "9337": [ + null, + { + "position": 35, + "nodeLength": 22, + "src": "elem.firstElementChild", + "evalFalse": 6, + "evalTrue": 0 + } + ], + "9349": [ + null, + { + "position": 8, + "nodeLength": 25, + "src": "jQuery.isFunction(html)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9359": [ + null, + { + "position": 72, + "nodeLength": 15, + "src": "contents.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9372": [ + null, + { + "position": 28, + "nodeLength": 10, + "src": "isFunction", + "evalFalse": 3, + "evalTrue": 0 + } + ], + "9389": [ + null, + { + "position": 13, + "nodeLength": 69, + "src": "elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33, + "nodeLength": 49, + "src": "elem.offsetHeight || elem.getClientRects().length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9412": [ + null, + { + "position": 244511, + "nodeLength": 55, + "src": "!!xhrSupported && (\"withCredentials\" in xhrSupported)", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "9419": [ + null, + { + "position": 104, + "nodeLength": 52, + "src": "support.cors || xhrSupported && !options.crossDomain", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120, + "nodeLength": 36, + "src": "xhrSupported && !options.crossDomain", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9434": [ + null, + { + "position": 211, + "nodeLength": 17, + "src": "options.xhrFields", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9441": [ + null, + { + "position": 371, + "nodeLength": 40, + "src": "options.mimeType && xhr.overrideMimeType", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9450": [ + null, + { + "position": 803, + "nodeLength": 54, + "src": "!options.crossDomain && !headers[\"X-Requested-With\"]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9462": [ + null, + { + "position": 12, + "nodeLength": 8, + "src": "callback", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9466": [ + null, + { + "position": 128, + "nodeLength": 16, + "src": "type === \"abort\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9468": [ + null, + { + "position": 190, + "nodeLength": 16, + "src": "type === \"error\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9473": [ + null, + { + "position": 159, + "nodeLength": 30, + "src": "typeof xhr.status !== \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9485": [ + null, + { + "position": 18, + "nodeLength": 44, + "src": "xhrSuccessStatus[xhr.status] || xhr.status", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9491": [ + null, + { + "position": -1, + "nodeLength": 89, + "src": "(xhr.responseType || \"text\") !== \"text\" || typeof xhr.responseText !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 263, + "nodeLength": 39, + "src": "(xhr.responseType || \"text\") !== \"text\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 264, + "nodeLength": 26, + "src": "xhr.responseType || \"text\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9492": [ + null, + { + "position": 315, + "nodeLength": 36, + "src": "typeof xhr.responseText !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9509": [ + null, + { + "position": 2494, + "nodeLength": 25, + "src": "xhr.onabort !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9515": [ + null, + { + "position": 68, + "nodeLength": 20, + "src": "xhr.readyState === 4", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9522": [ + null, + { + "position": 14, + "nodeLength": 8, + "src": "callback", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9536": [ + null, + { + "position": 75, + "nodeLength": 42, + "src": "options.hasContent && options.data || null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10, + "nodeLength": 34, + "src": "options.hasContent && options.data", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9540": [ + null, + { + "position": 86, + "nodeLength": 8, + "src": "callback", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9547": [ + null, + { + "position": 10, + "nodeLength": 8, + "src": "callback", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9560": [ + null, + { + "position": 7, + "nodeLength": 13, + "src": "s.crossDomain", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9584": [ + null, + { + "position": 7, + "nodeLength": 21, + "src": "s.cache === undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9587": [ + null, + { + "position": 61, + "nodeLength": 13, + "src": "s.crossDomain", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9596": [ + null, + { + "position": 65, + "nodeLength": 13, + "src": "s.crossDomain", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9608": [ + null, + { + "position": 58, + "nodeLength": 3, + "src": "evt", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9609": [ + null, + { + "position": 18, + "nodeLength": 20, + "src": "evt.type === \"error\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9618": [ + null, + { + "position": 10, + "nodeLength": 8, + "src": "callback", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9636": [ + null, + { + "position": 18, + "nodeLength": 60, + "src": "oldCallbacks.pop() || (jQuery.expando + \"_\" + (nonce++))", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9646": [ + null, + { + "position": 62, + "nodeLength": 219, + "src": "s.jsonp !== false && (rjsonp.test(s.url) ? \"url\" : typeof s.data === \"string\" && (s.contentType || \"\").indexOf(\"application/x-www-form-urlencoded\") === 0 && rjsonp.test(s.data) && \"data\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62, + "nodeLength": 17, + "src": "s.jsonp !== false", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85, + "nodeLength": 20, + "src": "rjsonp.test(s.url)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9648": [ + null, + { + "position": 36, + "nodeLength": 155, + "src": "typeof s.data === \"string\" && (s.contentType || \"\").indexOf(\"application/x-www-form-urlencoded\") === 0 && rjsonp.test(s.data) && \"data\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37, + "nodeLength": 26, + "src": "typeof s.data === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9649": [ + null, + { + "position": -1, + "nodeLength": 19, + "src": "s.contentType || \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9650": [ + null, + { + "position": 35, + "nodeLength": 119, + "src": "(s.contentType || \"\").indexOf(\"application/x-www-form-urlencoded\") === 0 && rjsonp.test(s.data) && \"data\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74, + "nodeLength": 80, + "src": "(s.contentType || \"\").indexOf(\"application/x-www-form-urlencoded\") === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9651": [ + null, + { + "position": 87, + "nodeLength": 31, + "src": "rjsonp.test(s.data) && \"data\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9655": [ + null, + { + "position": 373, + "nodeLength": 40, + "src": "jsonProp || s.dataTypes[0] === \"jsonp\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 385, + "nodeLength": 28, + "src": "s.dataTypes[0] === \"jsonp\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9658": [ + null, + { + "position": 110, + "nodeLength": 36, + "src": "jQuery.isFunction(s.jsonpCallback)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9663": [ + null, + { + "position": 243, + "nodeLength": 8, + "src": "jsonProp", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9665": [ + null, + { + "position": 344, + "nodeLength": 17, + "src": "s.jsonp !== false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9666": [ + null, + { + "position": 15, + "nodeLength": 20, + "src": "rquery.test(s.url)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9671": [ + null, + { + "position": 9, + "nodeLength": 18, + "src": "!responseContainer", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9690": [ + null, + { + "position": 59, + "nodeLength": 25, + "src": "overwritten === undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9699": [ + null, + { + "position": 273, + "nodeLength": 17, + "src": "s[callbackName]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9709": [ + null, + { + "position": 575, + "nodeLength": 53, + "src": "responseContainer && jQuery.isFunction(overwritten)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9732": [ + null, + { + "position": 124, + "nodeLength": 28, + "src": "body.childNodes.length === 2", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "9741": [ + null, + { + "position": 7, + "nodeLength": 24, + "src": "typeof data !== \"string\"", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "9744": [ + null, + { + "position": 58, + "nodeLength": 28, + "src": "typeof context === \"boolean\"", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "9751": [ + null, + { + "position": 174, + "nodeLength": 8, + "src": "!context", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "9755": [ + null, + { + "position": 122, + "nodeLength": 26, + "src": "support.createHTMLDocument", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9770": [ + null, + { + "position": 746, + "nodeLength": 18, + "src": "!keepScripts && []", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "9773": [ + null, + { + "position": 788, + "nodeLength": 6, + "src": "parsed", + "evalFalse": 7, + "evalTrue": 1 + } + ], + "9779": [ + null, + { + "position": 916, + "nodeLength": 25, + "src": "scripts && scripts.length", + "evalFalse": 7, + "evalTrue": 0 + } + ], + "9795": [ + null, + { + "position": 82, + "nodeLength": 8, + "src": "off > -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9801": [ + null, + { + "position": 208, + "nodeLength": 27, + "src": "jQuery.isFunction(params)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9808": [ + null, + { + "position": 373, + "nodeLength": 36, + "src": "params && typeof params === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 383, + "nodeLength": 26, + "src": "typeof params === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9813": [ + null, + { + "position": 492, + "nodeLength": 15, + "src": "self.length > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9820": [ + null, + { + "position": 193, + "nodeLength": 13, + "src": "type || \"GET\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9828": [ + null, + { + "position": 91, + "nodeLength": 8, + "src": "selector", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9840": [ + null, + { + "position": 896, + "nodeLength": 157, + "src": "callback && function(jqXHR, status) {\n self.each(function() {\n callback.apply(this, response || [jqXHR.responseText, status, jqXHR]);\n});\n}", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9842": [ + null, + { + "position": 27, + "nodeLength": 49, + "src": "response || [jqXHR.responseText, status, jqXHR]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9872": [ + null, + { + "position": 10, + "nodeLength": 16, + "src": "elem === fn.elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9883": [ + null, + { + "position": 9, + "nodeLength": 23, + "src": "jQuery.isWindow(elem)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42, + "nodeLength": 39, + "src": "elem.nodeType === 9 && elem.defaultView", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42, + "nodeLength": 19, + "src": "elem.nodeType === 9", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9894": [ + null, + { + "position": 258, + "nodeLength": 21, + "src": "position === \"static\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9901": [ + null, + { + "position": 466, + "nodeLength": 105, + "src": "(position === \"absolute\" || position === \"fixed\") && (curCSSTop + curCSSLeft).indexOf(\"auto\") > -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 466, + "nodeLength": 47, + "src": "position === \"absolute\" || position === \"fixed\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 466, + "nodeLength": 23, + "src": "position === \"absolute\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 493, + "nodeLength": 20, + "src": "position === \"fixed\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9902": [ + null, + { + "position": 57, + "nodeLength": 47, + "src": "(curCSSTop + curCSSLeft).indexOf(\"auto\") > -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9906": [ + null, + { + "position": 700, + "nodeLength": 17, + "src": "calculatePosition", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9912": [ + null, + { + "position": 13, + "nodeLength": 28, + "src": "parseFloat(curCSSTop) || 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9913": [ + null, + { + "position": 56, + "nodeLength": 29, + "src": "parseFloat(curCSSLeft) || 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9916": [ + null, + { + "position": 929, + "nodeLength": 28, + "src": "jQuery.isFunction(options)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9922": [ + null, + { + "position": 1130, + "nodeLength": 19, + "src": "options.top != null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9925": [ + null, + { + "position": 1222, + "nodeLength": 20, + "src": "options.left != null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9929": [ + null, + { + "position": 1320, + "nodeLength": 18, + "src": "\"using\" in options", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9942": [ + null, + { + "position": 43, + "nodeLength": 16, + "src": "arguments.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9943": [ + null, + { + "position": 11, + "nodeLength": 21, + "src": "options === undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9953": [ + null, + { + "position": 264, + "nodeLength": 5, + "src": "!elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9960": [ + null, + { + "position": 409, + "nodeLength": 29, + "src": "!elem.getClientRects().length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9967": [ + null, + { + "position": 579, + "nodeLength": 25, + "src": "rect.width || rect.height", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9983": [ + null, + { + "position": 8, + "nodeLength": 10, + "src": "!this[0]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9993": [ + null, + { + "position": 254, + "nodeLength": 42, + "src": "jQuery.css(elem, \"position\") === \"fixed\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10005": [ + null, + { + "position": 133, + "nodeLength": 45, + "src": "!jQuery.nodeName(offsetParent[0], \"html\")", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10037": [ + null, + { + "position": 54, + "nodeLength": 67, + "src": "offsetParent && jQuery.css(offsetParent, \"position\") === \"static\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 70, + "nodeLength": 51, + "src": "jQuery.css(offsetParent, \"position\") === \"static\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10041": [ + null, + { + "position": 188, + "nodeLength": 31, + "src": "offsetParent || documentElement", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10048": [ + null, + { + "position": 12, + "nodeLength": 22, + "src": "\"pageYOffset\" === prop", + "evalFalse": 1, + "evalTrue": 1 + } + ], + "10054": [ + null, + { + "position": 42, + "nodeLength": 17, + "src": "val === undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10055": [ + null, + { + "position": 12, + "nodeLength": 3, + "src": "win", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10058": [ + null, + { + "position": 125, + "nodeLength": 3, + "src": "win", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10060": [ + null, + { + "position": 18, + "nodeLength": 4, + "src": "!top", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10061": [ + null, + { + "position": 53, + "nodeLength": 3, + "src": "top", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10080": [ + null, + { + "position": 9, + "nodeLength": 8, + "src": "computed", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10084": [ + null, + { + "position": 106, + "nodeLength": 26, + "src": "rnumnonpx.test(computed)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10100": [ + null, + { + "position": 20, + "nodeLength": 67, + "src": "arguments.length && (defaultExtra || typeof margin !== \"boolean\")", + "evalFalse": 24, + "evalTrue": 8 + }, + { + "position": 42, + "nodeLength": 43, + "src": "defaultExtra || typeof margin !== \"boolean\"", + "evalFalse": 8, + "evalTrue": 8 + }, + { + "position": 58, + "nodeLength": 27, + "src": "typeof margin !== \"boolean\"", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "10101": [ + null, + { + "position": 96, + "nodeLength": 75, + "src": "defaultExtra || (margin === true || value === true ? \"margin\" : \"border\")", + "evalFalse": 0, + "evalTrue": 32 + }, + { + "position": 114, + "nodeLength": 33, + "src": "margin === true || value === true", + "evalFalse": 0, + "evalTrue": 8 + }, + { + "position": 114, + "nodeLength": 15, + "src": "margin === true", + "evalFalse": 0, + "evalTrue": 8 + }, + { + "position": 133, + "nodeLength": 14, + "src": "value === true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10106": [ + null, + { + "position": 24, + "nodeLength": 23, + "src": "jQuery.isWindow(elem)", + "evalFalse": 44, + "evalTrue": 0 + } + ], + "10109": [ + null, + { + "position": 94, + "nodeLength": 33, + "src": "funcName.indexOf(\"outer\") === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10115": [ + null, + { + "position": 320, + "nodeLength": 19, + "src": "elem.nodeType === 9", + "evalFalse": 44, + "evalTrue": 0 + } + ], + "10127": [ + null, + { + "position": 690, + "nodeLength": 19, + "src": "value === undefined", + "evalFalse": 20, + "evalTrue": 24 + } + ], + "10134": [ + null, + { + "position": 981, + "nodeLength": 9, + "src": "chainable", + "evalFalse": 24, + "evalTrue": 8 + } + ], + "10155": [ + null, + { + "position": 60, + "nodeLength": 22, + "src": "arguments.length === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10157": [ + null, + { + "position": 76, + "nodeLength": 16, + "src": "selector || \"**\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10179": [ + null, + { + "position": 265349, + "nodeLength": 42, + "src": "typeof define === \"function\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 265349, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "10197": [ + null, + { + "position": 7, + "nodeLength": 19, + "src": "window.$ === jQuery", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10201": [ + null, + { + "position": 58, + "nodeLength": 32, + "src": "deep && window.jQuery === jQuery", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66, + "nodeLength": 24, + "src": "window.jQuery === jQuery", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10211": [ + null, + { + "position": 265940, + "nodeLength": 9, + "src": "!noGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ] + } + }, + "/js/external/jqueryui.1.12.1/js/jquery-ui.1.12.1.min.js": { + "lineData": [ + null, + null, + null, + null, + null, + null, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "functionData": [ + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 10, + 5, + 0, + 1, + 0, + 27, + 0, + 13, + 514, + 441, + 0, + 0, + 0, + 0, + 172, + 27, + 0, + 0, + 0, + 14, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 0, + 8, + 0, + 1, + 10, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 41, + 1, + 30, + 1, + 0, + 0, + 1, + 0, + 8, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 15, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 4, + 1, + 4, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 5, + 0, + 0, + 0, + 0, + 0, + 0, + 10, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 10, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 12, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "branchData": { + "6": [ + null, + { + "position": 1145, + "nodeLength": 37, + "src": "\"function\" == typeof define && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1145, + "nodeLength": 25, + "src": "\"function\" == typeof define", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1272, + "nodeLength": 13, + "src": "\"inherit\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1328, + "nodeLength": 12, + "src": "\"hidden\" !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1367, + "nodeLength": 25, + "src": "t.length && t[0] !== document", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1377, + "nodeLength": 15, + "src": "t[0] !== document", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1398, + "nodeLength": 116, + "src": "e = t.css(\"position\") , (\"absolute\" === e || \"relative\" === e || \"fixed\" === e) && (i = parseInt(t.css(\"zIndex\"), 10) , !isNaN(i) && 0 !== i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1419, + "nodeLength": 95, + "src": "(\"absolute\" === e || \"relative\" === e || \"fixed\" === e) && (i = parseInt(t.css(\"zIndex\"), 10) , !isNaN(i) && 0 !== i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1419, + "nodeLength": 43, + "src": "\"absolute\" === e || \"relative\" === e || \"fixed\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1419, + "nodeLength": 14, + "src": "\"absolute\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1435, + "nodeLength": 27, + "src": "\"relative\" === e || \"fixed\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1435, + "nodeLength": 14, + "src": "\"relative\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1451, + "nodeLength": 11, + "src": "\"fixed\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1497, + "nodeLength": 16, + "src": "!isNaN(i) && 0 !== i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1508, + "nodeLength": 5, + "src": "0 !== i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3770, + "nodeLength": 98, + "src": "-1 !== this.className.indexOf(\"ui-datepicker-prev\") && t(this).removeClass(\"ui-datepicker-prev-hover\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3770, + "nodeLength": 49, + "src": "-1 !== this.className.indexOf(\"ui-datepicker-prev\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3869, + "nodeLength": 98, + "src": "-1 !== this.className.indexOf(\"ui-datepicker-next\") && t(this).removeClass(\"ui-datepicker-next-hover\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3869, + "nodeLength": 49, + "src": "-1 !== this.className.indexOf(\"ui-datepicker-next\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4003, + "nodeLength": 388, + "src": "t.datepicker._isDisabledDatepicker(m.inline ? m.dpDiv.parent()[0] : m.input[0]) || (t(this).parents(\".ui-datepicker-calendar\").find(\"a\").removeClass(\"ui-state-hover\") , t(this).addClass(\"ui-state-hover\") , -1 !== this.className.indexOf(\"ui-datepicker-prev\") && t(this).addClass(\"ui-datepicker-prev-hover\") , -1 !== this.className.indexOf(\"ui-datepicker-next\") && t(this).addClass(\"ui-datepicker-next-hover\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4038, + "nodeLength": 8, + "src": "m.inline", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4199, + "nodeLength": 95, + "src": "-1 !== this.className.indexOf(\"ui-datepicker-prev\") && t(this).addClass(\"ui-datepicker-prev-hover\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4199, + "nodeLength": 49, + "src": "-1 !== this.className.indexOf(\"ui-datepicker-prev\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4295, + "nodeLength": 95, + "src": "-1 !== this.className.indexOf(\"ui-datepicker-next\") && t(this).addClass(\"ui-datepicker-next-hover\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4295, + "nodeLength": 49, + "src": "-1 !== this.className.indexOf(\"ui-datepicker-next\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4437, + "nodeLength": 23, + "src": "null == i[s] && (e[s] = i[s])", + "evalFalse": 150, + "evalTrue": 0 + }, + { + "position": 4437, + "nodeLength": 10, + "src": "null == i[s]", + "evalFalse": 150, + "evalTrue": 0 + }, + { + "position": 4567, + "nodeLength": 47, + "src": "e !== this.element.val() && this._trigger(\"change\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4567, + "nodeLength": 22, + "src": "e !== this.element.val()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4621, + "nodeLength": 8, + "src": "t.ui || {}", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 4745, + "nodeLength": 14, + "src": "null != (n = i[o])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4790, + "nodeLength": 42, + "src": "s && s.remove && t(n).triggerHandler(\"remove\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4793, + "nodeLength": 39, + "src": "s.remove && t(n).triggerHandler(\"remove\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4960, + "nodeLength": 19, + "src": "s || (s = i , i = t.Widget)", + "evalFalse": 0, + "evalTrue": 27 + }, + { + "position": 4980, + "nodeLength": 53, + "src": "t.isArray(s) && (s = t.extend.apply(null, [{}].concat(s)))", + "evalFalse": 25, + "evalTrue": 2 + }, + { + "position": 5101, + "nodeLength": 8, + "src": "t[h] || {}", + "evalFalse": 0, + "evalTrue": 27 + }, + { + "position": 5151, + "nodeLength": 18, + "src": "this._createWidget", + "evalFalse": 0, + "evalTrue": 13 + }, + { + "position": 5171, + "nodeLength": 41, + "src": "arguments.length && this._createWidget(t, e)", + "evalFalse": 13, + "evalTrue": 0 + }, + { + "position": 5389, + "nodeLength": 15, + "src": "t.isFunction(s)", + "evalFalse": 73, + "evalTrue": 441 + }, + { + "position": 5766, + "nodeLength": 1, + "src": "n", + "evalFalse": 19, + "evalTrue": 8 + }, + { + "position": 5768, + "nodeLength": 22, + "src": "a.widgetEventPrefix || e", + "evalFalse": 0, + "evalTrue": 8 + }, + { + "position": 5855, + "nodeLength": 1, + "src": "n", + "evalFalse": 19, + "evalTrue": 8 + }, + { + "position": 6129, + "nodeLength": 3, + "src": "a > o", + "evalFalse": 172, + "evalTrue": 261 + }, + { + "position": 6161, + "nodeLength": 134, + "src": "n[o].hasOwnProperty(i) && void 0 !== s && (e[i] = t.isPlainObject(s) ? t.isPlainObject(e[i]) ? t.widget.extend({}, e[i], s) : t.widget.extend({}, s) : s)", + "evalFalse": 365, + "evalTrue": 1020 + }, + { + "position": 6185, + "nodeLength": 110, + "src": "void 0 !== s && (e[i] = t.isPlainObject(s) ? t.isPlainObject(e[i]) ? t.widget.extend({}, e[i], s) : t.widget.extend({}, s) : s)", + "evalFalse": 365, + "evalTrue": 1020 + }, + { + "position": 6185, + "nodeLength": 10, + "src": "void 0 !== s", + "evalFalse": 0, + "evalTrue": 1385 + }, + { + "position": 6203, + "nodeLength": 18, + "src": "t.isPlainObject(s)", + "evalFalse": 1267, + "evalTrue": 118 + }, + { + "position": 6222, + "nodeLength": 21, + "src": "t.isPlainObject(e[i])", + "evalFalse": 83, + "evalTrue": 35 + }, + { + "position": 6342, + "nodeLength": 29, + "src": "i.prototype.widgetFullName || e", + "evalFalse": 0, + "evalTrue": 27 + }, + { + "position": 6398, + "nodeLength": 18, + "src": "\"string\" == typeof n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6453, + "nodeLength": 1, + "src": "o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6455, + "nodeLength": 27, + "src": "this.length || \"instance\" !== n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6468, + "nodeLength": 14, + "src": "\"instance\" !== n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6533, + "nodeLength": 14, + "src": "\"instance\" === n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6557, + "nodeLength": 1, + "src": "o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6559, + "nodeLength": 37, + "src": "t.isFunction(o[n]) && \"_\" !== n.charAt(0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6579, + "nodeLength": 17, + "src": "\"_\" !== n.charAt(0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6616, + "nodeLength": 17, + "src": "i !== o && void 0 !== i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6616, + "nodeLength": 5, + "src": "i !== o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6623, + "nodeLength": 10, + "src": "void 0 !== i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6637, + "nodeLength": 11, + "src": "i && i.jquery", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6857, + "nodeLength": 55, + "src": "a.length && (n = t.widget.extend.apply(null, [n].concat(a)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6955, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6967, + "nodeLength": 5, + "src": "n || {}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6974, + "nodeLength": 18, + "src": "e._init && e._init()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7244, + "nodeLength": 28, + "src": "i || this.defaultElement || this", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5, + "nodeLength": 25, + "src": "this.defaultElement || this", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7444, + "nodeLength": 253, + "src": "i !== this && (t.data(i, this.widgetFullName, this) , this._on(!0, this.element, {\n remove: function(t) {\n t.target === i && this.destroy();\n}}) , this.document = t(i.style ? i.ownerDocument : i.document || i) , this.window = t(this.document[0].defaultView || this.document[0].parentWindow))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7444, + "nodeLength": 8, + "src": "i !== this", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7535, + "nodeLength": 28, + "src": "t.target === i && this.destroy()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7535, + "nodeLength": 12, + "src": "t.target === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7583, + "nodeLength": 7, + "src": "i.style", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7607, + "nodeLength": 13, + "src": "i.document || i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7636, + "nodeLength": 59, + "src": "this.document[0].defaultView || this.document[0].parentWindow", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7786, + "nodeLength": 69, + "src": "this.options.disabled && this._setOptionDisabled(this.options.disabled)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8405, + "nodeLength": 20, + "src": "0 === arguments.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8469, + "nodeLength": 18, + "src": "\"string\" == typeof e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8491, + "nodeLength": 40, + "src": "a = {} , s = e.split(\".\") , e = s.shift() , s.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8584, + "nodeLength": 12, + "src": "s.length - 1 > o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8609, + "nodeLength": 11, + "src": "n[s[o]] || {}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8634, + "nodeLength": 30, + "src": "e = s.pop() , 1 === arguments.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8644, + "nodeLength": 20, + "src": "1 === arguments.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8672, + "nodeLength": 13, + "src": "void 0 === n[e]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8711, + "nodeLength": 20, + "src": "1 === arguments.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8739, + "nodeLength": 24, + "src": "void 0 === this.options[e]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8934, + "nodeLength": 40, + "src": "\"classes\" === t && this._setOptionClasses(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8934, + "nodeLength": 13, + "src": "\"classes\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8993, + "nodeLength": 42, + "src": "\"disabled\" === t && this._setOptionDisabled(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8993, + "nodeLength": 14, + "src": "\"disabled\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9124, + "nodeLength": 145, + "src": "e[i] !== this.options.classes[i] && n && n.length && (s = t(n.get()) , this._removeClass(n, i) , s.addClass(this._classes({\n element: s, \n keys: i, \n classes: e, \n add: !0})))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9124, + "nodeLength": 30, + "src": "e[i] !== this.options.classes[i]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9156, + "nodeLength": 113, + "src": "n && n.length && (s = t(n.get()) , this._removeClass(n, i) , s.addClass(this._classes({\n element: s, \n keys: i, \n classes: e, \n add: !0})))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9159, + "nodeLength": 110, + "src": "n.length && (s = t(n.get()) , this._removeClass(n, i) , s.addClass(this._classes({\n element: s, \n keys: i, \n classes: e, \n add: !0})))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9376, + "nodeLength": 116, + "src": "t && (this._removeClass(this.hoverable, null, \"ui-state-hover\") , this._removeClass(this.focusable, null, \"ui-state-focus\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9664, + "nodeLength": 10, + "src": "i.length > r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9681, + "nodeLength": 33, + "src": "n.classesElementLookup[i[r]] || t()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9717, + "nodeLength": 5, + "src": "e.add", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9838, + "nodeLength": 43, + "src": "o && e.classes[i[r]] && s.push(e.classes[i[r]])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9841, + "nodeLength": 40, + "src": "e.classes[i[r]] && s.push(e.classes[i[r]])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9946, + "nodeLength": 24, + "src": "this.options.classes || {}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10029, + "nodeLength": 38, + "src": "e.keys && i(e.keys.match(/\\S+/g) || [], !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10039, + "nodeLength": 24, + "src": "e.keys.match(/\\S+/g) || []", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10068, + "nodeLength": 37, + "src": "e.extra && i(e.extra.match(/\\S+/g) || [])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10079, + "nodeLength": 25, + "src": "e.extra.match(/\\S+/g) || []", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10209, + "nodeLength": 80, + "src": "-1 !== t.inArray(e.target, n) && (i.classesElementLookup[s] = t(n.not(e.target).get()))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10209, + "nodeLength": 26, + "src": "-1 !== t.inArray(e.target, n)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10453, + "nodeLength": 19, + "src": "\"boolean\" == typeof s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10483, + "nodeLength": 28, + "src": "\"string\" == typeof t || null === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10483, + "nodeLength": 18, + "src": "\"string\" == typeof t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10503, + "nodeLength": 8, + "src": "null === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10521, + "nodeLength": 1, + "src": "n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10532, + "nodeLength": 1, + "src": "n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10546, + "nodeLength": 1, + "src": "n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10658, + "nodeLength": 35, + "src": "\"boolean\" != typeof e && (s = i , i = e , e = !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10658, + "nodeLength": 19, + "src": "\"boolean\" != typeof e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10694, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10822, + "nodeLength": 66, + "src": "e || o.options.disabled !== !0 && !t(this).hasClass(\"ui-state-disabled\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10825, + "nodeLength": 63, + "src": "o.options.disabled !== !0 && !t(this).hasClass(\"ui-state-disabled\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10825, + "nodeLength": 23, + "src": "o.options.disabled !== !0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10890, + "nodeLength": 18, + "src": "\"string\" == typeof a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10943, + "nodeLength": 60, + "src": "\"string\" != typeof a && (r.guid = a.guid = a.guid || r.guid || t.guid++)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10943, + "nodeLength": 18, + "src": "\"string\" != typeof a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10978, + "nodeLength": 24, + "src": "a.guid || r.guid || t.guid++", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10986, + "nodeLength": 16, + "src": "r.guid || t.guid++", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11071, + "nodeLength": 1, + "src": "c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11120, + "nodeLength": 5, + "src": "i || \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11382, + "nodeLength": 18, + "src": "\"string\" == typeof t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11459, + "nodeLength": 4, + "src": "e || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11995, + "nodeLength": 140, + "src": "s = s || {} , i = t.Event(i) , i.type = (e === this.widgetEventPrefix ? e : this.widgetEventPrefix + e).toLowerCase() , i.target = this.element[0] , o = i.originalEvent", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11997, + "nodeLength": 5, + "src": "s || {}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12024, + "nodeLength": 26, + "src": "e === this.widgetEventPrefix", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12147, + "nodeLength": 19, + "src": "n in i || (i[n] = o[n])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12202, + "nodeLength": 84, + "src": "t.isFunction(a) && a.apply(this.element[0], [i].concat(s)) === !1 || i.isDefaultPrevented()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12202, + "nodeLength": 60, + "src": "t.isFunction(a) && a.apply(this.element[0], [i].concat(s)) === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12219, + "nodeLength": 43, + "src": "a.apply(this.element[0], [i].concat(s)) === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12384, + "nodeLength": 34, + "src": "\"string\" == typeof n && (n = {\n effect: n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12384, + "nodeLength": 18, + "src": "\"string\" == typeof n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12427, + "nodeLength": 1, + "src": "n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12429, + "nodeLength": 26, + "src": "n === !0 || \"number\" == typeof n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12429, + "nodeLength": 6, + "src": "n === !0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12437, + "nodeLength": 18, + "src": "\"number\" == typeof n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12458, + "nodeLength": 11, + "src": "n.effect || i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12474, + "nodeLength": 5, + "src": "n || {}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12480, + "nodeLength": 36, + "src": "\"number\" == typeof n && (n = {\n duration: n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12480, + "nodeLength": 18, + "src": "\"number\" == typeof n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12552, + "nodeLength": 25, + "src": "n.delay && s.delay(n.delay)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12578, + "nodeLength": 33, + "src": "a && t.effects && t.effects.effect[r]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12581, + "nodeLength": 30, + "src": "t.effects && t.effects.effect[r]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12620, + "nodeLength": 11, + "src": "r !== e && s[r]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12620, + "nodeLength": 5, + "src": "r !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12693, + "nodeLength": 15, + "src": "o && o.call(s[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12781, + "nodeLength": 12, + "src": "u.test(t[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12821, + "nodeLength": 12, + "src": "u.test(t[1])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12867, + "nodeLength": 26, + "src": "parseInt(t.css(e, i), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12926, + "nodeLength": 14, + "src": "9 === i.nodeType", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12999, + "nodeLength": 13, + "src": "t.isWindow(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13096, + "nodeLength": 16, + "src": "i.preventDefault", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13401, + "nodeLength": 10, + "src": "void 0 !== n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13676, + "nodeLength": 27, + "src": "e === i && (i = s[0].clientWidth)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13676, + "nodeLength": 5, + "src": "e === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13754, + "nodeLength": 24, + "src": "e.isWindow || e.isDocument", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13812, + "nodeLength": 24, + "src": "e.isWindow || e.isDocument", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13870, + "nodeLength": 58, + "src": "\"scroll\" === i || \"auto\" === i && e.width < e.element[0].scrollWidth", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13870, + "nodeLength": 12, + "src": "\"scroll\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13884, + "nodeLength": 44, + "src": "\"auto\" === i && e.width < e.element[0].scrollWidth", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13884, + "nodeLength": 10, + "src": "\"auto\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13896, + "nodeLength": 32, + "src": "e.width < e.element[0].scrollWidth", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13931, + "nodeLength": 60, + "src": "\"scroll\" === s || \"auto\" === s && e.height < e.element[0].scrollHeight", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13931, + "nodeLength": 12, + "src": "\"scroll\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13945, + "nodeLength": 46, + "src": "\"auto\" === s && e.height < e.element[0].scrollHeight", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13945, + "nodeLength": 10, + "src": "\"auto\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13957, + "nodeLength": 34, + "src": "e.height < e.element[0].scrollHeight", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14005, + "nodeLength": 1, + "src": "o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14044, + "nodeLength": 1, + "src": "n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14112, + "nodeLength": 9, + "src": "e || window", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14144, + "nodeLength": 25, + "src": "!!i[0] && 9 === i[0].nodeType", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14152, + "nodeLength": 17, + "src": "9 === i[0].nodeType", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14172, + "nodeLength": 6, + "src": "!s && !n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14227, + "nodeLength": 1, + "src": "o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14384, + "nodeLength": 9, + "src": "!n || !n.of", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14538, + "nodeLength": 19, + "src": "n.collision || \"flip\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14589, + "nodeLength": 38, + "src": "v[0].preventDefault && (n.at = \"left top\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14718, + "nodeLength": 11, + "src": "n[this] || \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14742, + "nodeLength": 105, + "src": "1 === i.length && (i = r.test(i[0]) ? i.concat([\"center\"]) : h.test(i[0]) ? [\"center\"].concat(i) : [\"center\", \"center\"])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14742, + "nodeLength": 12, + "src": "1 === i.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14759, + "nodeLength": 12, + "src": "r.test(i[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14793, + "nodeLength": 12, + "src": "h.test(i[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14853, + "nodeLength": 12, + "src": "r.test(i[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14885, + "nodeLength": 12, + "src": "h.test(i[1])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14951, + "nodeLength": 1, + "src": "t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14960, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15014, + "nodeLength": 25, + "src": "1 === w.length && (w[1] = w[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15014, + "nodeLength": 12, + "src": "1 === w.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15040, + "nodeLength": 17, + "src": "\"right\" === n.at[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15068, + "nodeLength": 33, + "src": "\"center\" === n.at[0] && (m.left += p / 2)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15068, + "nodeLength": 18, + "src": "\"center\" === n.at[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15102, + "nodeLength": 18, + "src": "\"bottom\" === n.at[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15130, + "nodeLength": 32, + "src": "\"center\" === n.at[1] && (m.top += f / 2)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15130, + "nodeLength": 18, + "src": "\"center\" === n.at[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15453, + "nodeLength": 17, + "src": "\"right\" === n.my[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15481, + "nodeLength": 33, + "src": "\"center\" === n.my[0] && (D.left -= l / 2)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15481, + "nodeLength": 18, + "src": "\"center\" === n.my[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15515, + "nodeLength": 18, + "src": "\"bottom\" === n.my[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15543, + "nodeLength": 32, + "src": "\"center\" === n.my[1] && (D.top -= c / 2)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15543, + "nodeLength": 18, + "src": "\"center\" === n.my[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15666, + "nodeLength": 218, + "src": "t.ui.position[w[e]] && t.ui.position[w[e]][i](D, {\n targetWidth: p, \n targetHeight: f, \n elemWidth: l, \n elemHeight: c, \n collisionPosition: s, \n collisionWidth: x, \n collisionHeight: C, \n offset: [u[0] + I[0], u[1] + I[1]], \n my: n.my, \n at: n.at, \n within: b, \n elem: h})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15887, + "nodeLength": 441, + "src": "n.using && (r = function(t) {\n var e = g.left - D.left, i = e + p - l, s = g.top - D.top, r = s + f - c, u = {\n target: {\n element: v, \n left: g.left, \n top: g.top, \n width: p, \n height: f}, \n element: {\n element: h, \n left: D.left, \n top: D.top, \n width: l, \n height: c}, \n horizontal: 0 > i ? \"left\" : e > 0 ? \"right\" : \"center\", \n vertical: 0 > r ? \"top\" : s > 0 ? \"bottom\" : \"middle\"};\n l > p && p > a(e + i) && (u.horizontal = \"center\") , c > f && f > a(s + r) && (u.vertical = \"middle\") , u.important = o(a(e), a(i)) > o(a(s), a(r)) ? \"horizontal\" : \"vertical\" , n.using.call(this, t, u);\n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16092, + "nodeLength": 3, + "src": "0 > i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16103, + "nodeLength": 3, + "src": "e > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16133, + "nodeLength": 3, + "src": "0 > r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16143, + "nodeLength": 3, + "src": "s > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16166, + "nodeLength": 38, + "src": "l > p && p > a(e + i) && (u.horizontal = \"center\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16166, + "nodeLength": 3, + "src": "l > p", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16171, + "nodeLength": 33, + "src": "p > a(e + i) && (u.horizontal = \"center\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16171, + "nodeLength": 8, + "src": "p > a(e + i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16205, + "nodeLength": 36, + "src": "c > f && f > a(s + r) && (u.vertical = \"middle\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16205, + "nodeLength": 3, + "src": "c > f", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16210, + "nodeLength": 31, + "src": "f > a(s + r) && (u.vertical = \"middle\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16210, + "nodeLength": 8, + "src": "f > a(s + r)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16254, + "nodeLength": 25, + "src": "o(a(e), a(i)) > o(a(s), a(r))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16422, + "nodeLength": 10, + "src": "s.isWindow", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16541, + "nodeLength": 18, + "src": "e.collisionWidth > a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16560, + "nodeLength": 9, + "src": "h > 0 && 0 >= l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16560, + "nodeLength": 3, + "src": "h > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16565, + "nodeLength": 4, + "src": "0 >= l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16623, + "nodeLength": 9, + "src": "l > 0 && 0 >= h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16623, + "nodeLength": 3, + "src": "l > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16628, + "nodeLength": 4, + "src": "0 >= h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16635, + "nodeLength": 3, + "src": "h > l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16662, + "nodeLength": 3, + "src": "h > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16676, + "nodeLength": 3, + "src": "l > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16754, + "nodeLength": 10, + "src": "s.isWindow", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16878, + "nodeLength": 19, + "src": "e.collisionHeight > a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16898, + "nodeLength": 9, + "src": "h > 0 && 0 >= l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16898, + "nodeLength": 3, + "src": "h > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16903, + "nodeLength": 4, + "src": "0 >= l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16959, + "nodeLength": 9, + "src": "l > 0 && 0 >= h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16959, + "nodeLength": 3, + "src": "l > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16964, + "nodeLength": 4, + "src": "0 >= h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16971, + "nodeLength": 3, + "src": "h > l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16999, + "nodeLength": 3, + "src": "h > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17012, + "nodeLength": 3, + "src": "l > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17135, + "nodeLength": 10, + "src": "n.isWindow", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17246, + "nodeLength": 16, + "src": "\"left\" === e.my[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17276, + "nodeLength": 17, + "src": "\"right\" === e.my[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17310, + "nodeLength": 16, + "src": "\"left\" === e.at[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17341, + "nodeLength": 17, + "src": "\"right\" === e.at[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17393, + "nodeLength": 3, + "src": "0 > c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17435, + "nodeLength": 29, + "src": "(0 > i || a(c) > i) && (t.left += d + p + f)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17435, + "nodeLength": 11, + "src": "0 > i || a(c) > i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17435, + "nodeLength": 3, + "src": "0 > i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17440, + "nodeLength": 6, + "src": "a(c) > i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17466, + "nodeLength": 85, + "src": "u > 0 && (s = t.left - e.collisionPosition.marginLeft + d + p + f - h , (s > 0 || u > a(s)) && (t.left += d + p + f))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17466, + "nodeLength": 3, + "src": "u > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17521, + "nodeLength": 29, + "src": "(s > 0 || u > a(s)) && (t.left += d + p + f)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17521, + "nodeLength": 11, + "src": "s > 0 || u > a(s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17521, + "nodeLength": 3, + "src": "s > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17526, + "nodeLength": 6, + "src": "u > a(s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17630, + "nodeLength": 10, + "src": "n.isWindow", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17738, + "nodeLength": 15, + "src": "\"top\" === e.my[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17756, + "nodeLength": 1, + "src": "d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17772, + "nodeLength": 18, + "src": "\"bottom\" === e.my[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17808, + "nodeLength": 15, + "src": "\"top\" === e.at[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17839, + "nodeLength": 18, + "src": "\"bottom\" === e.at[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17893, + "nodeLength": 3, + "src": "0 > c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17935, + "nodeLength": 28, + "src": "(0 > s || a(c) > s) && (t.top += p + f + g)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17935, + "nodeLength": 11, + "src": "0 > s || a(c) > s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17935, + "nodeLength": 3, + "src": "0 > s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17940, + "nodeLength": 6, + "src": "a(c) > s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17965, + "nodeLength": 82, + "src": "u > 0 && (i = t.top - e.collisionPosition.marginTop + p + f + g - h , (i > 0 || u > a(i)) && (t.top += p + f + g))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17965, + "nodeLength": 3, + "src": "u > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18018, + "nodeLength": 28, + "src": "(i > 0 || u > a(i)) && (t.top += p + f + g)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18018, + "nodeLength": 11, + "src": "i > 0 || u > a(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18018, + "nodeLength": 3, + "src": "i > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18023, + "nodeLength": 6, + "src": "u > a(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18318, + "nodeLength": 19, + "src": "t.expr.createPseudo", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 18501, + "nodeLength": 47, + "src": "\"onselectstart\" in document.createElement(\"div\")", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 18869, + "nodeLength": 13, + "src": "u[e.type] || {}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18890, + "nodeLength": 7, + "src": "null == t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18898, + "nodeLength": 9, + "src": "i || !e.def", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18922, + "nodeLength": 7, + "src": "s.floor", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18948, + "nodeLength": 8, + "src": "isNaN(t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18963, + "nodeLength": 5, + "src": "s.mod", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18985, + "nodeLength": 3, + "src": "0 > t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18991, + "nodeLength": 7, + "src": "t > s.max", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19111, + "nodeLength": 13, + "src": "r && o.parse(r)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19127, + "nodeLength": 15, + "src": "o.space || \"rgba\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19150, + "nodeLength": 1, + "src": "h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19217, + "nodeLength": 8, + "src": "n.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19227, + "nodeLength": 47, + "src": "\"0,0,0,0\" === n.join() && t.extend(n, o.transparent)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19227, + "nodeLength": 20, + "src": "\"0,0,0,0\" === n.join()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19318, + "nodeLength": 5, + "src": "1 > 6 * i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19336, + "nodeLength": 5, + "src": "1 > 2 * i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19344, + "nodeLength": 5, + "src": "2 > 3 * i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20764, + "nodeLength": 42, + "src": "p.style.backgroundColor.indexOf(\"rgba\") > -1", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 20938, + "nodeLength": 5, + "src": "n === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20990, + "nodeLength": 42, + "src": "(n.jquery || n.nodeType) && (n = t(n).css(a) , a = e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20990, + "nodeLength": 20, + "src": "n.jquery || n.nodeType", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21079, + "nodeLength": 30, + "src": "a !== e && (n = [n, a, r, h] , d = \"array\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21079, + "nodeLength": 5, + "src": "a !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21110, + "nodeLength": 12, + "src": "\"string\" === d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21134, + "nodeLength": 16, + "src": "s(n) || o._default", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21152, + "nodeLength": 11, + "src": "\"array\" === d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21225, + "nodeLength": 12, + "src": "\"object\" === d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21239, + "nodeLength": 14, + "src": "n instanceof l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21272, + "nodeLength": 43, + "src": "n[e.cache] && (u[e.cache] = n[e.cache].slice())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21377, + "nodeLength": 11, + "src": "!u[o] && s.to", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21393, + "nodeLength": 23, + "src": "\"alpha\" === t || null == n[t]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21393, + "nodeLength": 11, + "src": "\"alpha\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21406, + "nodeLength": 10, + "src": "null == n[t]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21470, + "nodeLength": 83, + "src": "u[o] && 0 > t.inArray(null, u[o].slice(0, 3)) && (u[o][3] = 1 , s.from && (u._rgba = s.from(u[o])))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21476, + "nodeLength": 77, + "src": "0 > t.inArray(null, u[o].slice(0, 3)) && (u[o][3] = 1 , s.from && (u._rgba = s.from(u[o])))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21476, + "nodeLength": 33, + "src": "0 > t.inArray(null, u[o].slice(0, 3))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21522, + "nodeLength": 30, + "src": "s.from && (u._rgba = s.from(u[o]))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21654, + "nodeLength": 114, + "src": "r && (a = n[o.cache] || o.to && o.to(n._rgba) || [] , f(o.props, function(t, i) {\n return null != r[i.idx] ? s = r[i.idx] === a[i.idx] : e;\n}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21660, + "nodeLength": 35, + "src": "n[o.cache] || o.to && o.to(n._rgba) || []", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21672, + "nodeLength": 23, + "src": "o.to && o.to(n._rgba) || []", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21672, + "nodeLength": 19, + "src": "o.to && o.to(n._rgba)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21727, + "nodeLength": 14, + "src": "null != r[i.idx]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21744, + "nodeLength": 19, + "src": "r[i.idx] === a[i.idx]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21835, + "nodeLength": 21, + "src": "e[s.cache] && t.push(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21926, + "nodeLength": 16, + "src": "0 === this.alpha()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21967, + "nodeLength": 25, + "src": "a[o.cache] || o.to(a._rgba)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22077, + "nodeLength": 13, + "src": "u[n.type] || {}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22091, + "nodeLength": 103, + "src": "null !== l && (null === a ? h[o] = l : (c.mod && (l - a > c.mod / 2 ? a += c.mod : a - l > c.mod / 2 && (a -= c.mod)) , h[o] = i((l - a) * e + a, n)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22091, + "nodeLength": 8, + "src": "null !== l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22102, + "nodeLength": 8, + "src": "null === a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22119, + "nodeLength": 53, + "src": "c.mod && (l - a > c.mod / 2 ? a += c.mod : a - l > c.mod / 2 && (a -= c.mod))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22127, + "nodeLength": 11, + "src": "l - a > c.mod / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22148, + "nodeLength": 23, + "src": "a - l > c.mod / 2 && (a -= c.mod)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22148, + "nodeLength": 11, + "src": "a - l > c.mod / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22230, + "nodeLength": 17, + "src": "1 === this._rgba[3]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22442, + "nodeLength": 7, + "src": "null == t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22450, + "nodeLength": 3, + "src": "e > 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22469, + "nodeLength": 28, + "src": "1 === i[3] && (i.pop() , e = \"rgb(\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22469, + "nodeLength": 8, + "src": "1 === i[3]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22593, + "nodeLength": 20, + "src": "null == t && (t = e > 2 ? 1 : 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22593, + "nodeLength": 7, + "src": "null == t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22605, + "nodeLength": 3, + "src": "e > 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22614, + "nodeLength": 33, + "src": "e && 3 > e && (t = Math.round(100 * t) + \"%\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22617, + "nodeLength": 30, + "src": "3 > e && (t = Math.round(100 * t) + \"%\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22617, + "nodeLength": 3, + "src": "3 > e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22659, + "nodeLength": 28, + "src": "1 === i[3] && (i.pop() , e = \"hsl(\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22659, + "nodeLength": 8, + "src": "1 === i[3]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22770, + "nodeLength": 20, + "src": "e && i.push(~~(255 * s))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22825, + "nodeLength": 4, + "src": "t || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22844, + "nodeLength": 12, + "src": "1 === t.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22904, + "nodeLength": 17, + "src": "0 === this._rgba[3]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23010, + "nodeLength": 34, + "src": "null == t[0] || null == t[1] || null == t[2]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23010, + "nodeLength": 10, + "src": "null == t[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23022, + "nodeLength": 22, + "src": "null == t[1] || null == t[2]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23022, + "nodeLength": 10, + "src": "null == t[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23034, + "nodeLength": 10, + "src": "null == t[2]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23185, + "nodeLength": 5, + "src": "h === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23193, + "nodeLength": 5, + "src": "s === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23214, + "nodeLength": 5, + "src": "n === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23252, + "nodeLength": 5, + "src": "0 === l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23260, + "nodeLength": 5, + "src": ".5 >= u", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23301, + "nodeLength": 7, + "src": "null == a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23342, + "nodeLength": 34, + "src": "null == t[0] || null == t[1] || null == t[2]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23342, + "nodeLength": 10, + "src": "null == t[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23354, + "nodeLength": 22, + "src": "null == t[1] || null == t[2]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23354, + "nodeLength": 10, + "src": "null == t[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23366, + "nodeLength": 10, + "src": "null == t[2]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23443, + "nodeLength": 5, + "src": ".5 >= s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23648, + "nodeLength": 42, + "src": "h && !this[a] && (this[a] = h(this._rgba)) , s === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23648, + "nodeLength": 36, + "src": "h && !this[a] && (this[a] = h(this._rgba))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23651, + "nodeLength": 33, + "src": "!this[a] && (this[a] = h(this._rgba))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23685, + "nodeLength": 5, + "src": "s === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23734, + "nodeLength": 25, + "src": "\"array\" === r || \"object\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23734, + "nodeLength": 11, + "src": "\"array\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23747, + "nodeLength": 12, + "src": "\"object\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23823, + "nodeLength": 12, + "src": "\"object\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23845, + "nodeLength": 21, + "src": "null == s && (s = d[e.idx])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23845, + "nodeLength": 7, + "src": "null == s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23885, + "nodeLength": 1, + "src": "c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23932, + "nodeLength": 304, + "src": "l.fn[e] || (l.fn[e] = function(n) {\n var o, a = t.type(n), h = \"alpha\" === e ? this._hsla ? \"hsla\" : \"rgba\" : s, l = this[h](), c = l[i.idx];\n return \"undefined\" === a ? c : (\"function\" === a && (n = n.call(this, c) , a = t.type(n)) , null == n && i.empty ? this : (\"string\" === a && (o = r.exec(n) , o && (n = c + parseFloat(o[2]) * (\"+\" === o[1] ? 1 : -1))) , l[i.idx] = n , this[h](l)));\n})", + "evalFalse": 0, + "evalTrue": 8 + }, + { + "position": 23982, + "nodeLength": 11, + "src": "\"alpha\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23994, + "nodeLength": 10, + "src": "this._hsla", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24050, + "nodeLength": 15, + "src": "\"undefined\" === a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24069, + "nodeLength": 46, + "src": "\"function\" === a && (n = n.call(this, c) , a = t.type(n))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24069, + "nodeLength": 14, + "src": "\"function\" === a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24116, + "nodeLength": 16, + "src": "null == n && i.empty", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24116, + "nodeLength": 7, + "src": "null == n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24139, + "nodeLength": 71, + "src": "\"string\" === a && (o = r.exec(n) , o && (n = c + parseFloat(o[2]) * (\"+\" === o[1] ? 1 : -1)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24139, + "nodeLength": 12, + "src": "\"string\" === a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24166, + "nodeLength": 43, + "src": "o && (n = c + parseFloat(o[2]) * (\"+\" === o[1] ? 1 : -1))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24192, + "nodeLength": 10, + "src": "\"+\" === o[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24346, + "nodeLength": 51, + "src": "\"transparent\" !== n && (\"string\" !== t.type(n) || (o = s(n)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24346, + "nodeLength": 17, + "src": "\"transparent\" !== n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24366, + "nodeLength": 30, + "src": "\"string\" !== t.type(n) || (o = s(n))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24366, + "nodeLength": 20, + "src": "\"string\" !== t.type(n)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24402, + "nodeLength": 33, + "src": "n = l(o || n) , !d.rgba && 1 !== n._rgba[3]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24406, + "nodeLength": 4, + "src": "o || n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24412, + "nodeLength": 23, + "src": "!d.rgba && 1 !== n._rgba[3]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24421, + "nodeLength": 14, + "src": "1 !== n._rgba[3]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24443, + "nodeLength": 21, + "src": "\"backgroundColor\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24481, + "nodeLength": 38, + "src": "(\"\" === r || \"transparent\" === r) && a && a.style", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24481, + "nodeLength": 25, + "src": "\"\" === r || \"transparent\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24481, + "nodeLength": 6, + "src": "\"\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24489, + "nodeLength": 17, + "src": "\"transparent\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24509, + "nodeLength": 10, + "src": "a && a.style", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24589, + "nodeLength": 20, + "src": "r && \"transparent\" !== r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24592, + "nodeLength": 17, + "src": "\"transparent\" !== r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24698, + "nodeLength": 64, + "src": "e.colorInit || (e.start = l(e.elem, i) , e.end = l(e.end) , e.colorInit = !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25333, + "nodeLength": 27, + "src": "e.ownerDocument.defaultView", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25437, + "nodeLength": 26, + "src": "n && n.length && n[0] && n[n[0]]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25440, + "nodeLength": 23, + "src": "n.length && n[0] && n[n[0]]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25450, + "nodeLength": 13, + "src": "n[0] && n[n[0]]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25479, + "nodeLength": 3, + "src": "s--", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25491, + "nodeLength": 47, + "src": "\"string\" == typeof n[i] && (o[t.camelCase(i)] = n[i])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25491, + "nodeLength": 21, + "src": "\"string\" == typeof n[i]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25555, + "nodeLength": 34, + "src": "\"string\" == typeof n[i] && (o[i] = n[i])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25555, + "nodeLength": 21, + "src": "\"string\" == typeof n[i]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25646, + "nodeLength": 65, + "src": "e[s] !== o && (n[s] || (t.fx.step[s] || !isNaN(parseFloat(o))) && (a[s] = o))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25646, + "nodeLength": 8, + "src": "e[s] !== o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25657, + "nodeLength": 53, + "src": "n[s] || (t.fx.step[s] || !isNaN(parseFloat(o))) && (a[s] = o)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25664, + "nodeLength": 46, + "src": "(t.fx.step[s] || !isNaN(parseFloat(o))) && (a[s] = o)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25664, + "nodeLength": 35, + "src": "t.fx.step[s] || !isNaN(parseFloat(o))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25990, + "nodeLength": 90, + "src": "(\"none\" !== t.end && !t.setAttr || 1 === t.pos && !t.setAttr) && (p.style(t.elem, i, t.end) , t.setAttr = !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25990, + "nodeLength": 49, + "src": "\"none\" !== t.end && !t.setAttr || 1 === t.pos && !t.setAttr", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25990, + "nodeLength": 26, + "src": "\"none\" !== t.end && !t.setAttr", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25990, + "nodeLength": 14, + "src": "\"none\" !== t.end", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26018, + "nodeLength": 21, + "src": "1 === t.pos && !t.setAttr", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26018, + "nodeLength": 9, + "src": "1 === t.pos", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26084, + "nodeLength": 108, + "src": "t.fn.addBack || (t.fn.addBack = function(t) {\n return this.add(null == t ? this.prevObject : this.prevObject.filter(t));\n})", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 26140, + "nodeLength": 7, + "src": "null == t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26302, + "nodeLength": 19, + "src": "a.attr(\"class\") || \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26324, + "nodeLength": 10, + "src": "h.children", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26457, + "nodeLength": 24, + "src": "n[e] && a[e + \"Class\"](n[e])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26977, + "nodeLength": 1, + "src": "s", + "evalFalse": 41, + "evalTrue": 0 + }, + { + "position": 27124, + "nodeLength": 18, + "src": "arguments.length > 1", + "evalFalse": 30, + "evalTrue": 0 + }, + { + "position": 27295, + "nodeLength": 31, + "src": "\"boolean\" == typeof s || void 0 === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 27295, + "nodeLength": 19, + "src": "\"boolean\" == typeof s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 27316, + "nodeLength": 10, + "src": "void 0 === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 27327, + "nodeLength": 1, + "src": "n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 27362, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37, + "nodeLength": 36, + "src": "t.isPlainObject(e) && (i = e , e = e.effect)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 87, + "nodeLength": 15, + "src": "null == i && (i = {})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 87, + "nodeLength": 7, + "src": "null == i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 103, + "nodeLength": 34, + "src": "t.isFunction(i) && (n = i , s = null , i = {})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 139, + "nodeLength": 51, + "src": "(\"number\" == typeof i || t.fx.speeds[i]) && (n = s , s = i , i = {})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 139, + "nodeLength": 34, + "src": "\"number\" == typeof i || t.fx.speeds[i]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 139, + "nodeLength": 18, + "src": "\"number\" == typeof i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 191, + "nodeLength": 29, + "src": "t.isFunction(s) && (n = s , s = null)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 221, + "nodeLength": 16, + "src": "i && t.extend(e, i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 240, + "nodeLength": 13, + "src": "s || i.duration", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 265, + "nodeLength": 8, + "src": "t.fx.off", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 276, + "nodeLength": 18, + "src": "\"number\" == typeof s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 297, + "nodeLength": 16, + "src": "s in t.fx.speeds", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 361, + "nodeLength": 13, + "src": "n || i.complete", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 397, + "nodeLength": 38, + "src": "!e || \"number\" == typeof e || t.fx.speeds[e]", + "evalFalse": 0, + "evalTrue": 8 + }, + { + "position": 401, + "nodeLength": 34, + "src": "\"number\" == typeof e || t.fx.speeds[e]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 401, + "nodeLength": 18, + "src": "\"number\" == typeof e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 439, + "nodeLength": 39, + "src": "\"string\" != typeof e || t.effects.effect[e]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 439, + "nodeLength": 18, + "src": "\"string\" != typeof e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 479, + "nodeLength": 15, + "src": "t.isFunction(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 498, + "nodeLength": 28, + "src": "\"object\" != typeof e || e.effect", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 498, + "nodeLength": 18, + "src": "\"object\" != typeof e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 731, + "nodeLength": 23, + "src": "n.exec(t) || [\"\", 0, i, s, 0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 766, + "nodeLength": 19, + "src": "parseFloat(o[1]) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 792, + "nodeLength": 13, + "src": "\"auto\" === o[2]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 832, + "nodeLength": 13, + "src": "\"auto\" === o[3]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 870, + "nodeLength": 19, + "src": "parseFloat(o[4]) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 891, + "nodeLength": 159, + "src": "t.expr && t.expr.filters && t.expr.filters.animated && (t.expr.filters.animated = function(e) {\n return function(i) {\n return !!t(i).data(d) || e(i);\n};\n}(t.expr.filters.animated))", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 899, + "nodeLength": 151, + "src": "t.expr.filters && t.expr.filters.animated && (t.expr.filters.animated = function(e) {\n return function(i) {\n return !!t(i).data(d) || e(i);\n};\n}(t.expr.filters.animated))", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 915, + "nodeLength": 135, + "src": "t.expr.filters.animated && (t.expr.filters.animated = function(e) {\n return function(i) {\n return !!t(i).data(d) || e(i);\n};\n}(t.expr.filters.animated))", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 1002, + "nodeLength": 20, + "src": "!!t(i).data(d) || e(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1051, + "nodeLength": 1339, + "src": "t.uiBackCompat !== !1 && t.extend(t.effects, {\n save: function(t, e) {\n for (var i = 0, s = e.length; s > i; i++) \n null !== e[i] && t.data(c + e[i], t[0].style[e[i]]);\n}, \n restore: function(t, e) {\n for (var i, s = 0, n = e.length; n > s; s++) \n null !== e[s] && (i = t.data(c + e[s]) , t.css(e[s], i));\n}, \n setMode: function(t, e) {\n return \"toggle\" === e && (e = t.is(\":hidden\") ? \"show\" : \"hide\") , e;\n}, \n createWrapper: function(e) {\n if (e.parent().is(\".ui-effects-wrapper\")) \n return e.parent();\n var i = {\n width: e.outerWidth(!0), \n height: e.outerHeight(!0), \n \"float\": e.css(\"float\")}, s = t(\"
\").addClass(\"ui-effects-wrapper\").css({\n fontSize: \"100%\", \n background: \"transparent\", \n border: \"none\", \n margin: 0, \n padding: 0}), n = {\n width: e.width(), \n height: e.height()}, o = document.activeElement;\n try {\n o.id;\n } catch (a) {\n o = document.body;\n}\n return e.wrap(s) , (e[0] === o || t.contains(e[0], o)) && t(o).trigger(\"focus\") , s = e.parent() , \"static\" === e.css(\"position\") ? (s.css({\n position: \"relative\"}) , e.css({\n position: \"relative\"})) : (t.extend(i, {\n position: e.css(\"position\"), \n zIndex: e.css(\"z-index\")}) , t.each([\"top\", \"left\", \"bottom\", \"right\"], function(t, s) {\n i[s] = e.css(s) , isNaN(parseInt(i[s], 10)) && (i[s] = \"auto\");\n}) , e.css({\n position: \"relative\", \n top: 0, \n left: 0, \n right: \"auto\", \n bottom: \"auto\"})) , e.css(n) , s.css(i).show();\n}, \n removeWrapper: function(e) {\n var i = document.activeElement;\n return e.parent().is(\".ui-effects-wrapper\") && (e.parent().replaceWith(e) , (e[0] === i || t.contains(e[0], i)) && t(i).trigger(\"focus\")) , e;\n}})", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 1051, + "nodeLength": 19, + "src": "t.uiBackCompat !== !1", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 1134, + "nodeLength": 3, + "src": "s > i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1142, + "nodeLength": 44, + "src": "null !== e[i] && t.data(c + e[i], t[0].style[e[i]])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1142, + "nodeLength": 11, + "src": "null !== e[i]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1235, + "nodeLength": 3, + "src": "n > s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1243, + "nodeLength": 45, + "src": "null !== e[s] && (i = t.data(c + e[s]) , t.css(e[s], i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1243, + "nodeLength": 11, + "src": "null !== e[s]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1318, + "nodeLength": 47, + "src": "\"toggle\" === e && (e = t.is(\":hidden\") ? \"show\" : \"hide\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1318, + "nodeLength": 12, + "src": "\"toggle\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1335, + "nodeLength": 15, + "src": "t.is(\":hidden\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1398, + "nodeLength": 36, + "src": "e.parent().is(\".ui-effects-wrapper\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1778, + "nodeLength": 52, + "src": "(e[0] === o || t.contains(e[0], o)) && t(o).trigger(\"focus\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1778, + "nodeLength": 28, + "src": "e[0] === o || t.contains(e[0], o)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1778, + "nodeLength": 8, + "src": "e[0] === o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1844, + "nodeLength": 28, + "src": "\"static\" === e.css(\"position\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2066, + "nodeLength": 39, + "src": "isNaN(parseInt(i[s], 10)) && (i[s] = \"auto\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2266, + "nodeLength": 119, + "src": "e.parent().is(\".ui-effects-wrapper\") && (e.parent().replaceWith(e) , (e[0] === i || t.contains(e[0], i)) && t(i).trigger(\"focus\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2332, + "nodeLength": 52, + "src": "(e[0] === i || t.contains(e[0], i)) && t(i).trigger(\"focus\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2332, + "nodeLength": 28, + "src": "e[0] === i || t.contains(e[0], i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2332, + "nodeLength": 8, + "src": "e[0] === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2458, + "nodeLength": 19, + "src": "s || (s = i , i = \"effect\")", + "evalFalse": 0, + "evalTrue": 15 + }, + { + "position": 2566, + "nodeLength": 5, + "src": "0 === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2630, + "nodeLength": 16, + "src": "\"horizontal\" !== i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2648, + "nodeLength": 6, + "src": "e || 100", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2664, + "nodeLength": 14, + "src": "\"vertical\" !== i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2680, + "nodeLength": 6, + "src": "e || 100", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2965, + "nodeLength": 50, + "src": "e > 1 && s.splice.apply(s, [1, 0].concat(s.splice(e, i)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2965, + "nodeLength": 3, + "src": "e > 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3125, + "nodeLength": 13, + "src": "t.data(u) || \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3203, + "nodeLength": 33, + "src": "\"toggle\" === e && (e = i ? \"show\" : \"hide\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3203, + "nodeLength": 12, + "src": "\"toggle\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3220, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3238, + "nodeLength": 36, + "src": "(i ? \"hide\" === e : \"show\" === e) && (e = \"none\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3238, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3240, + "nodeLength": 10, + "src": "\"hide\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3251, + "nodeLength": 10, + "src": "\"show\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3807, + "nodeLength": 452, + "src": "/^(static|relative)/.test(s) && (s = \"absolute\" , i = t(\"<\" + e[0].nodeName + \">\").insertAfter(e).css({\n display: /^(inline|ruby)/.test(e.css(\"display\")) ? \"inline-block\" : \"block\", \n visibility: \"hidden\", \n marginTop: e.css(\"marginTop\"), \n marginBottom: e.css(\"marginBottom\"), \n marginLeft: e.css(\"marginLeft\"), \n marginRight: e.css(\"marginRight\"), \n \"float\": e.css(\"float\")}).outerWidth(e.outerWidth()).outerHeight(e.outerHeight()).addClass(\"ui-effects-placeholder\") , e.data(c + \"placeholder\", i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3906, + "nodeLength": 39, + "src": "/^(inline|ruby)/.test(e.css(\"display\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4369, + "nodeLength": 31, + "src": "i && (i.remove() , t.removeData(e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4521, + "nodeLength": 5, + "src": "n || {}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4569, + "nodeLength": 26, + "src": "o[0] > 0 && (n[i] = o[0] * s + o[1])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4569, + "nodeLength": 6, + "src": "o[0] > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4698, + "nodeLength": 25, + "src": "\"hide\" === s.mode && r.hide()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4698, + "nodeLength": 15, + "src": "\"hide\" === s.mode", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4741, + "nodeLength": 29, + "src": "t.isFunction(h) && h.call(r[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4771, + "nodeLength": 20, + "src": "t.isFunction(e) && e()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4823, + "nodeLength": 22, + "src": "t.uiBackCompat === !1 || o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4823, + "nodeLength": 19, + "src": "t.uiBackCompat === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4846, + "nodeLength": 15, + "src": "\"none\" === s.mode", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4893, + "nodeLength": 38, + "src": "(r.is(\":hidden\") ? \"hide\" === l : \"show\" === l)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4893, + "nodeLength": 15, + "src": "r.is(\":hidden\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4909, + "nodeLength": 10, + "src": "\"hide\" === l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4920, + "nodeLength": 10, + "src": "\"show\" === l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5042, + "nodeLength": 7, + "src": "a || \"fx\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5107, + "nodeLength": 22, + "src": "t.effects.mode(i, l) || o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5153, + "nodeLength": 44, + "src": "o && (\"show\" === s || s === o && \"hide\" === s) && i.show()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5157, + "nodeLength": 40, + "src": "(\"show\" === s || s === o && \"hide\" === s) && i.show()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5157, + "nodeLength": 29, + "src": "\"show\" === s || s === o && \"hide\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5157, + "nodeLength": 10, + "src": "\"show\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5169, + "nodeLength": 17, + "src": "s === o && \"hide\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5169, + "nodeLength": 5, + "src": "s === o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5176, + "nodeLength": 10, + "src": "\"hide\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5198, + "nodeLength": 37, + "src": "o && \"none\" === s || t.effects.saveStyle(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5198, + "nodeLength": 13, + "src": "o && \"none\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5201, + "nodeLength": 10, + "src": "\"none\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5236, + "nodeLength": 20, + "src": "t.isFunction(e) && e()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5265, + "nodeLength": 12, + "src": "t.fx.off || !n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5278, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5323, + "nodeLength": 15, + "src": "h && h.call(this)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5341, + "nodeLength": 6, + "src": "a === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33, + "nodeLength": 4, + "src": "i(s)", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "7": [ + null, + { + "position": 985, + "nodeLength": 4, + "src": "i(s)", + "evalFalse": 0, + "evalTrue": 4 + }, + { + "position": 1151, + "nodeLength": 25, + "src": "i(s) || \"boolean\" == typeof s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1157, + "nodeLength": 19, + "src": "\"boolean\" == typeof s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1393, + "nodeLength": 37, + "src": "i.indexOf(e) > 0 && (s = [parseFloat(i), e])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1393, + "nodeLength": 14, + "src": "i.indexOf(e) > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1463, + "nodeLength": 1, + "src": "t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1619, + "nodeLength": 27, + "src": "\"fixed\" === n.css(\"position\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1661, + "nodeLength": 1, + "src": "o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1681, + "nodeLength": 1, + "src": "o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1970, + "nodeLength": 1, + "src": "o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2045, + "nodeLength": 20, + "src": "t.isFunction(i) && i()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5822, + "nodeLength": 103, + "src": "e.clipInit || (e.start = t(e.elem).cssClip() , \"string\" == typeof e.end && (e.end = s(e.end, e.elem)) , e.clipInit = !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5863, + "nodeLength": 47, + "src": "\"string\" == typeof e.end && (e.end = s(e.end, e.elem))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5863, + "nodeLength": 22, + "src": "\"string\" == typeof e.end", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16085, + "nodeLength": 12, + "src": "0 === t || 1 === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16085, + "nodeLength": 5, + "src": "0 === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16092, + "nodeLength": 5, + "src": "1 === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16230, + "nodeLength": 26, + "src": "((e = Math.pow(2, --i)) - 1) / 11 > t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16458, + "nodeLength": 4, + "src": ".5 > t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34297, + "nodeLength": 17, + "src": "e.direction || \"up\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34418, + "nodeLength": 78, + "src": "\"show\" === e.mode && (n.cssClip(r.clip) , h && h.css(t.effects.clipToBox(r)) , r.clip = a)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34418, + "nodeLength": 15, + "src": "\"show\" === e.mode", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34454, + "nodeLength": 32, + "src": "h && h.css(t.effects.clipToBox(r))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34497, + "nodeLength": 56, + "src": "h && h.animate(t.effects.clipToBox(r), e.duration, e.easing)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34698, + "nodeLength": 10, + "src": "\"hide\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34711, + "nodeLength": 10, + "src": "\"show\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34724, + "nodeLength": 17, + "src": "e.direction || \"up\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34757, + "nodeLength": 10, + "src": "e.times || 5", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34775, + "nodeLength": 4, + "src": "l || h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34813, + "nodeLength": 20, + "src": "\"up\" === c || \"down\" === c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34813, + "nodeLength": 8, + "src": "\"up\" === c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34823, + "nodeLength": 10, + "src": "\"down\" === c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34849, + "nodeLength": 20, + "src": "\"up\" === c || \"left\" === c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34849, + "nodeLength": 8, + "src": "\"up\" === c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34859, + "nodeLength": 10, + "src": "\"left\" === c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34939, + "nodeLength": 50, + "src": "u || (u = a[\"top\" === m ? \"outerHeight\" : \"outerWidth\"]() / 3)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34947, + "nodeLength": 9, + "src": "\"top\" === m", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34990, + "nodeLength": 77, + "src": "l && (n = {\n opacity: 1} , n[m] = o , a.css(\"opacity\", 0).css(m, _ ? 2 * -u : 2 * u).animate(n, f, g))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35040, + "nodeLength": 1, + "src": "_", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35068, + "nodeLength": 23, + "src": "h && (u /= Math.pow(2, d - 1))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35104, + "nodeLength": 3, + "src": "d > v", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35123, + "nodeLength": 1, + "src": "_", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35172, + "nodeLength": 1, + "src": "h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35182, + "nodeLength": 56, + "src": "h && (s = {\n opacity: 0} , s[m] = (_ ? \"-=\" : \"+=\") + u , a.animate(s, f, g))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35206, + "nodeLength": 1, + "src": "_", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35347, + "nodeLength": 23, + "src": "e.direction || \"vertical\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35373, + "nodeLength": 10, + "src": "\"both\" === a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35386, + "nodeLength": 19, + "src": "r || \"horizontal\" === a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35389, + "nodeLength": 16, + "src": "\"horizontal\" === a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35408, + "nodeLength": 17, + "src": "r || \"vertical\" === a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35411, + "nodeLength": 14, + "src": "\"vertical\" === a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35452, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35485, + "nodeLength": 1, + "src": "h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35521, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35556, + "nodeLength": 1, + "src": "h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35616, + "nodeLength": 45, + "src": "\"show\" === e.mode && (o.cssClip(n.clip) , n.clip = s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35616, + "nodeLength": 15, + "src": "\"show\" === e.mode", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35807, + "nodeLength": 10, + "src": "\"show\" === o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35820, + "nodeLength": 19, + "src": "e.direction || \"left\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35842, + "nodeLength": 20, + "src": "\"up\" === r || \"down\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35842, + "nodeLength": 8, + "src": "\"up\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35852, + "nodeLength": 10, + "src": "\"down\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35878, + "nodeLength": 20, + "src": "\"up\" === r || \"left\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35878, + "nodeLength": 8, + "src": "\"up\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35888, + "nodeLength": 10, + "src": "\"left\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35911, + "nodeLength": 8, + "src": "\"+=\" === l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35977, + "nodeLength": 57, + "src": "e.distance || n[\"top\" === h ? \"outerHeight\" : \"outerWidth\"](!0) / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35991, + "nodeLength": 9, + "src": "\"top\" === h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36044, + "nodeLength": 34, + "src": "a && (n.css(u) , u[h] = c + s , u.opacity = 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36226, + "nodeLength": 19, + "src": "b.length === u * d && n()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36226, + "nodeLength": 14, + "src": "b.length === u * d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36325, + "nodeLength": 8, + "src": "e.pieces", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36393, + "nodeLength": 10, + "src": "\"show\" === f", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36525, + "nodeLength": 3, + "src": "u > o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36565, + "nodeLength": 3, + "src": "d > a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36820, + "nodeLength": 1, + "src": "g", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36836, + "nodeLength": 1, + "src": "g", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36853, + "nodeLength": 1, + "src": "g", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36878, + "nodeLength": 1, + "src": "g", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36894, + "nodeLength": 1, + "src": "g", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36911, + "nodeLength": 1, + "src": "g", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36918, + "nodeLength": 15, + "src": "e.duration || 500", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37001, + "nodeLength": 15, + "src": "\"show\" === e.mode", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37039, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37063, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37201, + "nodeLength": 10, + "src": "\"show\" === n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37214, + "nodeLength": 10, + "src": "\"hide\" === n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37227, + "nodeLength": 10, + "src": "e.size || 15", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37279, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37468, + "nodeLength": 37, + "src": "h && (r = parseInt(h[1], 10) / 100 * m[a ? 0 : 1])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37498, + "nodeLength": 1, + "src": "a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37551, + "nodeLength": 64, + "src": "o && (s.cssClip(g.clip) , d && d.css(t.effects.clipToBox(g)) , g.clip = p)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37573, + "nodeLength": 32, + "src": "d && d.css(t.effects.clipToBox(g))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37636, + "nodeLength": 90, + "src": "d && d.animate(t.effects.clipToBox(f), u, e.easing).animate(t.effects.clipToBox(g), u, e.easing)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37922, + "nodeLength": 30, + "src": "\"hide\" === e.mode && (n.opacity = 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37922, + "nodeLength": 15, + "src": "\"hide\" === e.mode", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38022, + "nodeLength": 18, + "src": "e.color || \"#ffff99\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38338, + "nodeLength": 12, + "src": "\"effect\" !== c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38353, + "nodeLength": 15, + "src": "e.scale || \"both\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38371, + "nodeLength": 29, + "src": "e.origin || [\"middle\", \"center\"]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38470, + "nodeLength": 9, + "src": "e.from || m", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38482, + "nodeLength": 37, + "src": "e.to || t.effects.scaledDimensions(a, 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38551, + "nodeLength": 25, + "src": "\"show\" === c && (o = _ , _ = v , v = o)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38551, + "nodeLength": 10, + "src": "\"show\" === c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38670, + "nodeLength": 231, + "src": "(\"box\" === d || \"both\" === d) && (n.from.y !== n.to.y && (_ = t.effects.setTransition(a, h, n.from.y, _) , v = t.effects.setTransition(a, h, n.to.y, v)) , n.from.x !== n.to.x && (_ = t.effects.setTransition(a, l, n.from.x, _) , v = t.effects.setTransition(a, l, n.to.x, v)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38670, + "nodeLength": 21, + "src": "\"box\" === d || \"both\" === d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38670, + "nodeLength": 9, + "src": "\"box\" === d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38681, + "nodeLength": 10, + "src": "\"both\" === d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38695, + "nodeLength": 102, + "src": "n.from.y !== n.to.y && (_ = t.effects.setTransition(a, h, n.from.y, _) , v = t.effects.setTransition(a, h, n.to.y, v))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38695, + "nodeLength": 17, + "src": "n.from.y !== n.to.y", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38798, + "nodeLength": 102, + "src": "n.from.x !== n.to.x && (_ = t.effects.setTransition(a, l, n.from.x, _) , v = t.effects.setTransition(a, l, n.to.x, v))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38798, + "nodeLength": 17, + "src": "n.from.x !== n.to.x", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38903, + "nodeLength": 130, + "src": "(\"content\" === d || \"both\" === d) && n.from.y !== n.to.y && (_ = t.effects.setTransition(a, r, n.from.y, _) , v = t.effects.setTransition(a, r, n.to.y, v))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38903, + "nodeLength": 25, + "src": "\"content\" === d || \"both\" === d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38903, + "nodeLength": 13, + "src": "\"content\" === d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38918, + "nodeLength": 10, + "src": "\"both\" === d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38931, + "nodeLength": 102, + "src": "n.from.y !== n.to.y && (_ = t.effects.setTransition(a, r, n.from.y, _) , v = t.effects.setTransition(a, r, n.to.y, v))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38931, + "nodeLength": 17, + "src": "n.from.y !== n.to.y", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39034, + "nodeLength": 217, + "src": "p && (s = t.effects.getBaseline(p, m) , _.top = (m.outerHeight - _.outerHeight) * s.y + g.top , _.left = (m.outerWidth - _.outerWidth) * s.x + g.left , v.top = (m.outerHeight - v.outerHeight) * s.y + g.top , v.left = (m.outerWidth - v.outerWidth) * s.x + g.left)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39262, + "nodeLength": 741, + "src": "(\"content\" === d || \"both\" === d) && (h = h.concat([\"marginTop\", \"marginBottom\"]).concat(r) , l = l.concat([\"marginLeft\", \"marginRight\"]) , a.find(\"*[width]\").each(function() {\n var i = t(this), s = t.effects.scaledDimensions(i), o = {\n height: s.height * n.from.y, \n width: s.width * n.from.x, \n outerHeight: s.outerHeight * n.from.y, \n outerWidth: s.outerWidth * n.from.x}, a = {\n height: s.height * n.to.y, \n width: s.width * n.to.x, \n outerHeight: s.height * n.to.y, \n outerWidth: s.width * n.to.x};\n n.from.y !== n.to.y && (o = t.effects.setTransition(i, h, n.from.y, o) , a = t.effects.setTransition(i, h, n.to.y, a)) , n.from.x !== n.to.x && (o = t.effects.setTransition(i, l, n.from.x, o) , a = t.effects.setTransition(i, l, n.to.x, a)) , u && t.effects.saveStyle(i) , i.css(o) , i.animate(a, e.duration, e.easing, function() {\n u && t.effects.restoreStyle(i);\n});\n}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39262, + "nodeLength": 25, + "src": "\"content\" === d || \"both\" === d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39262, + "nodeLength": 13, + "src": "\"content\" === d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39277, + "nodeLength": 10, + "src": "\"both\" === d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39686, + "nodeLength": 102, + "src": "n.from.y !== n.to.y && (o = t.effects.setTransition(i, h, n.from.y, o) , a = t.effects.setTransition(i, h, n.to.y, a))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39686, + "nodeLength": 17, + "src": "n.from.y !== n.to.y", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39789, + "nodeLength": 102, + "src": "n.from.x !== n.to.x && (o = t.effects.setTransition(i, l, n.from.x, o) , a = t.effects.setTransition(i, l, n.to.x, a))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39789, + "nodeLength": 17, + "src": "n.from.x !== n.to.x", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39892, + "nodeLength": 25, + "src": "u && t.effects.saveStyle(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39970, + "nodeLength": 28, + "src": "u && t.effects.restoreStyle(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40099, + "nodeLength": 41, + "src": "0 === v.opacity && a.css(\"opacity\", _.opacity)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40099, + "nodeLength": 13, + "src": "0 === v.opacity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40141, + "nodeLength": 81, + "src": "u || (a.css(\"position\", \"static\" === f ? \"relative\" : f).offset(e) , t.effects.saveStyle(a))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40162, + "nodeLength": 12, + "src": "\"static\" === f", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40296, + "nodeLength": 73, + "src": "parseInt(e.percent, 10) || (0 === parseInt(e.percent, 10) ? 0 : \"effect\" !== n ? 0 : 100)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40321, + "nodeLength": 26, + "src": "0 === parseInt(e.percent, 10)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40350, + "nodeLength": 12, + "src": "\"effect\" !== n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40454, + "nodeLength": 19, + "src": "e.direction || \"both\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40482, + "nodeLength": 29, + "src": "e.origin || [\"middle\", \"center\"]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40516, + "nodeLength": 41, + "src": "e.fade && (a.from.opacity = 1 , a.to.opacity = 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40682, + "nodeLength": 27, + "src": "parseInt(e.percent, 10) || 150", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40825, + "nodeLength": 10, + "src": "\"show\" === n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40838, + "nodeLength": 10, + "src": "\"hide\" === n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40851, + "nodeLength": 4, + "src": "o || a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40861, + "nodeLength": 10, + "src": "e.times || 5", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40874, + "nodeLength": 1, + "src": "r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40928, + "nodeLength": 54, + "src": "(o || !s.is(\":visible\")) && (s.css(\"opacity\", 0).show() , c = 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40928, + "nodeLength": 20, + "src": "o || !s.is(\":visible\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40798, + "nodeLength": 1, + "src": "visit2889_7_146((visit2890_7_147(o || !s.is(\":visible\"))) && (s.css(\"opacity\", 0).show() , c = 1))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40928, + "nodeLength": 54, + "src": "(visit2890_7_147(o || !s.is(\":visible\"))) && (s.css(\"opacity\", 0).show() , c = 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 130, + "nodeLength": 20, + "src": "o || !s.is(\":visible\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41164, + "nodeLength": 19, + "src": "e.direction || \"left\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41186, + "nodeLength": 14, + "src": "e.distance || 20", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41203, + "nodeLength": 10, + "src": "e.times || 3", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41251, + "nodeLength": 20, + "src": "\"up\" === o || \"down\" === o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41251, + "nodeLength": 8, + "src": "\"up\" === o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41261, + "nodeLength": 10, + "src": "\"down\" === o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41287, + "nodeLength": 20, + "src": "\"up\" === o || \"left\" === o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41287, + "nodeLength": 8, + "src": "\"up\" === o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41297, + "nodeLength": 10, + "src": "\"left\" === o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41383, + "nodeLength": 1, + "src": "u", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41404, + "nodeLength": 1, + "src": "u", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41427, + "nodeLength": 1, + "src": "u", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41468, + "nodeLength": 3, + "src": "r > s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41774, + "nodeLength": 19, + "src": "e.direction || \"left\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41796, + "nodeLength": 20, + "src": "\"up\" === h || \"down\" === h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41796, + "nodeLength": 8, + "src": "\"up\" === h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41806, + "nodeLength": 10, + "src": "\"down\" === h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41832, + "nodeLength": 20, + "src": "\"up\" === h || \"left\" === h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41832, + "nodeLength": 8, + "src": "\"up\" === h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41842, + "nodeLength": 10, + "src": "\"left\" === h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41855, + "nodeLength": 55, + "src": "e.distance || o[\"top\" === l ? \"outerHeight\" : \"outerWidth\"](!0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41869, + "nodeLength": 9, + "src": "\"top\" === l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41985, + "nodeLength": 1, + "src": "c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42048, + "nodeLength": 61, + "src": "\"show\" === r && (o.cssClip(d.clip) , o.css(l, d[l]) , d.clip = s , d[l] = n)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42048, + "nodeLength": 10, + "src": "\"show\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42189, + "nodeLength": 90, + "src": "t.uiBackCompat !== !1 && (f = t.effects.define(\"transfer\", function(e, i) {\n t(this).transfer(e, i);\n}))", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 42189, + "nodeLength": 19, + "src": "t.uiBackCompat !== !1", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 42356, + "nodeLength": 10, + "src": "\"area\" === l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42392, + "nodeLength": 43, + "src": "i.href && o && \"map\" === n.nodeName.toLowerCase()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42400, + "nodeLength": 35, + "src": "o && \"map\" === n.nodeName.toLowerCase()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42403, + "nodeLength": 32, + "src": "\"map\" === n.nodeName.toLowerCase()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42465, + "nodeLength": 28, + "src": "a.length > 0 && a.is(\":visible\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42465, + "nodeLength": 10, + "src": "a.length > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42500, + "nodeLength": 49, + "src": "/^(input|select|textarea|button|object)$/.test(l)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42565, + "nodeLength": 53, + "src": "r && (h = t(i).closest(\"fieldset\")[0] , h && (r = !h.disabled))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42599, + "nodeLength": 18, + "src": "h && (r = !h.disabled)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42622, + "nodeLength": 7, + "src": "\"a\" === l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42630, + "nodeLength": 9, + "src": "i.href || s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42642, + "nodeLength": 31, + "src": "r && t(i).is(\":visible\") && e(t(i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42645, + "nodeLength": 28, + "src": "t(i).is(\":visible\") && e(t(i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42744, + "nodeLength": 26, + "src": "null != t.attr(e, \"tabindex\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42817, + "nodeLength": 29, + "src": "\"string\" == typeof this[0].form", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43087, + "nodeLength": 46, + "src": "this.form = this.element.form() , this.form.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43141, + "nodeLength": 45, + "src": "this.form.data(\"ui-form-reset-instances\") || []", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43187, + "nodeLength": 68, + "src": "t.length || this.form.on(\"reset.ui-form-reset\", this._formResetHandler)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43353, + "nodeLength": 16, + "src": "this.form.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43449, + "nodeLength": 8, + "src": "e.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43580, + "nodeLength": 815, + "src": "\"1.7\" === t.fn.jquery.substring(0, 3) && (t.each([\"Width\", \"Height\"], function(e, i) {\n function s(e, i, s, o) {\n return t.each(n, function() {\n i -= parseFloat(t.css(e, \"padding\" + this)) || 0 , s && (i -= parseFloat(t.css(e, \"border\" + this + \"Width\")) || 0) , o && (i -= parseFloat(t.css(e, \"margin\" + this)) || 0);\n}) , i;\n }\n var n = \"Width\" === i ? [\"Left\", \"Right\"] : [\"Top\", \"Bottom\"], o = i.toLowerCase(), a = {\n innerWidth: t.fn.innerWidth, \n innerHeight: t.fn.innerHeight, \n outerWidth: t.fn.outerWidth, \n outerHeight: t.fn.outerHeight};\n t.fn[\"inner\" + i] = function(e) {\n return void 0 === e ? a[\"inner\" + i].call(this) : this.each(function() {\n t(this).css(o, s(this, e) + \"px\");\n});\n} , t.fn[\"outer\" + i] = function(e, n) {\n return \"number\" != typeof e ? a[\"outer\" + i].call(this, e) : this.each(function() {\n t(this).css(o, s(this, e, !0, n) + \"px\");\n});\n};\n}) , t.fn.addBack = function(t) {\n return this.add(null == t ? this.prevObject : this.prevObject.filter(t));\n})", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 43580, + "nodeLength": 34, + "src": "\"1.7\" === t.fn.jquery.substring(0, 3)", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 43707, + "nodeLength": 38, + "src": "parseFloat(t.css(e, \"padding\" + this)) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43746, + "nodeLength": 53, + "src": "s && (i -= parseFloat(t.css(e, \"border\" + this + \"Width\")) || 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43753, + "nodeLength": 45, + "src": "parseFloat(t.css(e, \"border\" + this + \"Width\")) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43800, + "nodeLength": 45, + "src": "o && (i -= parseFloat(t.css(e, \"margin\" + this)) || 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43807, + "nodeLength": 37, + "src": "parseFloat(t.css(e, \"margin\" + this)) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43856, + "nodeLength": 11, + "src": "\"Width\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 44071, + "nodeLength": 10, + "src": "void 0 === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 44196, + "nodeLength": 18, + "src": "\"number\" != typeof e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 44343, + "nodeLength": 7, + "src": "null == t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 44728, + "nodeLength": 37, + "src": "this[0].labels && this[0].labels.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 44846, + "nodeLength": 155, + "src": "s && (e = this.eq(0).parents().last() , o = e.add(e.length ? e.siblings() : this.siblings()) , i = \"label[for='\" + t.ui.escapeSelector(s) + \"']\" , n = n.add(o.find(i).addBack(i)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 44888, + "nodeLength": 8, + "src": "e.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45081, + "nodeLength": 14, + "src": "\"absolute\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45098, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45195, + "nodeLength": 31, + "src": "s && \"static\" === e.css(\"position\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45198, + "nodeLength": 28, + "src": "\"static\" === e.css(\"position\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45310, + "nodeLength": 21, + "src": "\"fixed\" !== i && o.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45310, + "nodeLength": 11, + "src": "\"fixed\" !== i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45336, + "nodeLength": 31, + "src": "this[0].ownerDocument || document", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45442, + "nodeLength": 7, + "src": "null != i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45457, + "nodeLength": 30, + "src": "(!s || i >= 0) && t.ui.focusable(e, s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45457, + "nodeLength": 8, + "src": "!s || i >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45461, + "nodeLength": 4, + "src": "i >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45578, + "nodeLength": 32, + "src": "this.id || (this.id = \"ui-id-\" + ++t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45671, + "nodeLength": 53, + "src": "/^ui-id-\\d+$/.test(this.id) && t(this).removeAttr(\"id\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46531, + "nodeLength": 58, + "src": "e.collapsible || e.active !== !1 && null != e.active || (e.active = 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46546, + "nodeLength": 43, + "src": "e.active !== !1 && null != e.active || (e.active = 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46546, + "nodeLength": 29, + "src": "e.active !== !1 && null != e.active", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46546, + "nodeLength": 13, + "src": "e.active !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46561, + "nodeLength": 14, + "src": "null != e.active", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46612, + "nodeLength": 43, + "src": "0 > e.active && (e.active += this.headers.length)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46612, + "nodeLength": 10, + "src": "0 > e.active", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46736, + "nodeLength": 18, + "src": "this.active.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46833, + "nodeLength": 269, + "src": "s && (e = t(\"\") , this._addClass(e, \"ui-accordion-header-icon\", \"ui-icon \" + s.header) , e.prependTo(this.headers) , i = this.active.children(\".ui-accordion-header-icon\") , this._removeClass(i, s.header)._addClass(i, null, s.activeHeader)._addClass(this.headers, \"ui-accordion-icons\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47526, + "nodeLength": 56, + "src": "\"content\" !== this.options.heightStyle && t.css(\"height\", \"\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47526, + "nodeLength": 36, + "src": "\"content\" !== this.options.heightStyle", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47615, + "nodeLength": 12, + "src": "\"active\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47656, + "nodeLength": 98, + "src": "\"event\" === t && (this.options.event && this._off(this.headers, this.options.event) , this._setupEvents(e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47656, + "nodeLength": 11, + "src": "\"event\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47670, + "nodeLength": 62, + "src": "this.options.event && this._off(this.headers, this.options.event)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47772, + "nodeLength": 65, + "src": "\"collapsible\" !== t || e || this.options.active !== !1 || this._activate(0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47772, + "nodeLength": 17, + "src": "\"collapsible\" !== t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47791, + "nodeLength": 46, + "src": "e || this.options.active !== !1 || this._activate(0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47794, + "nodeLength": 43, + "src": "this.options.active !== !1 || this._activate(0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47794, + "nodeLength": 24, + "src": "this.options.active !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47838, + "nodeLength": 58, + "src": "\"icons\" === t && (this._destroyIcons() , e && this._createIcons())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47838, + "nodeLength": 11, + "src": "\"icons\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47873, + "nodeLength": 22, + "src": "e && this._createIcons()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48148, + "nodeLength": 21, + "src": "!e.altKey && !e.ctrlKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48496, + "nodeLength": 101, + "src": "o && (t(e.target).attr(\"tabIndex\", -1) , t(o).attr(\"tabIndex\", 0) , t(o).trigger(\"focus\") , e.preventDefault())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48626, + "nodeLength": 82, + "src": "e.keyCode === t.ui.keyCode.UP && e.ctrlKey && t(e.currentTarget).prev().trigger(\"focus\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48626, + "nodeLength": 27, + "src": "e.keyCode === t.ui.keyCode.UP", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48655, + "nodeLength": 53, + "src": "e.ctrlKey && t(e.currentTarget).prev().trigger(\"focus\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48770, + "nodeLength": 55, + "src": "e.active === !1 && e.collapsible === !0 || !this.headers.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48770, + "nodeLength": 33, + "src": "e.active === !1 && e.collapsible === !0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48770, + "nodeLength": 13, + "src": "e.active === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48785, + "nodeLength": 18, + "src": "e.collapsible === !0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48856, + "nodeLength": 13, + "src": "e.active === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48888, + "nodeLength": 63, + "src": "this.active.length && !t.contains(this.element[0], this.active[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48952, + "nodeLength": 68, + "src": "this.headers.length === this.headers.find(\".ui-state-disabled\").length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49551, + "nodeLength": 65, + "src": "e && (this._off(t.not(this.headers)) , this._off(e.not(this.panels)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50315, + "nodeLength": 18, + "src": "this.active.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50533, + "nodeLength": 10, + "src": "\"fill\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50642, + "nodeLength": 51, + "src": "\"absolute\" !== s && \"fixed\" !== s && (e -= i.outerHeight(!0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50642, + "nodeLength": 14, + "src": "\"absolute\" !== s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50658, + "nodeLength": 35, + "src": "\"fixed\" !== s && (e -= i.outerHeight(!0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50658, + "nodeLength": 11, + "src": "\"fixed\" !== s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50885, + "nodeLength": 178, + "src": "\"auto\" === s && (e = 0 , this.headers.next().each(function() {\n var i = t(this).is(\":visible\");\n i || t(this).show() , e = Math.max(e, t(this).css(\"height\", \"\").height()) , i || t(this).hide();\n}).height(e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50885, + "nodeLength": 10, + "src": "\"auto\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50967, + "nodeLength": 17, + "src": "i || t(this).show()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51033, + "nodeLength": 17, + "src": "i || t(this).hide()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51116, + "nodeLength": 110, + "src": "i !== this.active[0] && (i = i || this.active[0] , this._eventHandler({\n target: i, \n currentTarget: i, \n preventDefault: t.noop}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51116, + "nodeLength": 18, + "src": "i !== this.active[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51139, + "nodeLength": 17, + "src": "i || this.active[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51258, + "nodeLength": 18, + "src": "\"number\" == typeof e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51353, + "nodeLength": 59, + "src": "e && t.each(e.split(\" \"), function(t, e) {\n i[e] = \"_eventHandler\";\n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51690, + "nodeLength": 11, + "src": "a[0] === o[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51704, + "nodeLength": 16, + "src": "r && n.collapsible", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51723, + "nodeLength": 1, + "src": "h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51785, + "nodeLength": 1, + "src": "h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51824, + "nodeLength": 641, + "src": "r && !n.collapsible || this._trigger(\"beforeActivate\", e, u) === !1 || (n.active = h ? !1 : this.headers.index(a) , this.active = r ? t() : a , this._toggle(u) , this._removeClass(o, \"ui-accordion-header-active\", \"ui-state-active\") , n.icons && (i = o.children(\".ui-accordion-header-icon\") , this._removeClass(i, null, n.icons.activeHeader)._addClass(i, null, n.icons.header)) , r || (this._removeClass(a, \"ui-accordion-header-collapsed\")._addClass(a, \"ui-accordion-header-active\", \"ui-state-active\") , n.icons && (s = a.children(\".ui-accordion-header-icon\") , this._removeClass(s, null, n.icons.header)._addClass(s, null, n.icons.activeHeader)) , this._addClass(a.next(), \"ui-accordion-content-active\")))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51824, + "nodeLength": 17, + "src": "r && !n.collapsible", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51843, + "nodeLength": 622, + "src": "this._trigger(\"beforeActivate\", e, u) === !1 || (n.active = h ? !1 : this.headers.index(a) , this.active = r ? t() : a , this._toggle(u) , this._removeClass(o, \"ui-accordion-header-active\", \"ui-state-active\") , n.icons && (i = o.children(\".ui-accordion-header-icon\") , this._removeClass(i, null, n.icons.activeHeader)._addClass(i, null, n.icons.header)) , r || (this._removeClass(a, \"ui-accordion-header-collapsed\")._addClass(a, \"ui-accordion-header-active\", \"ui-state-active\") , n.icons && (s = a.children(\".ui-accordion-header-icon\") , this._removeClass(s, null, n.icons.header)._addClass(s, null, n.icons.activeHeader)) , this._addClass(a.next(), \"ui-accordion-content-active\")))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51843, + "nodeLength": 40, + "src": "this._trigger(\"beforeActivate\", e, u) === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51895, + "nodeLength": 1, + "src": "h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51934, + "nodeLength": 1, + "src": "r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52026, + "nodeLength": 132, + "src": "n.icons && (i = o.children(\".ui-accordion-header-icon\") , this._removeClass(i, null, n.icons.activeHeader)._addClass(i, null, n.icons.header))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52159, + "nodeLength": 305, + "src": "r || (this._removeClass(a, \"ui-accordion-header-collapsed\")._addClass(a, \"ui-accordion-header-active\", \"ui-state-active\") , n.icons && (s = a.children(\".ui-accordion-header-icon\") , this._removeClass(s, null, n.icons.header)._addClass(s, null, n.icons.activeHeader)) , this._addClass(a.next(), \"ui-accordion-content-active\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52276, + "nodeLength": 132, + "src": "n.icons && (s = a.children(\".ui-accordion-header-icon\") , this._removeClass(s, null, n.icons.header)._addClass(s, null, n.icons.activeHeader))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52506, + "nodeLength": 20, + "src": "this.prevShow.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52629, + "nodeLength": 20, + "src": "this.options.animate", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52811, + "nodeLength": 18, + "src": "i.length && s.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52883, + "nodeLength": 111, + "src": "i.length && this.headers.filter(function() {\n return 0 === parseInt(t(this).attr(\"tabIndex\"), 10);\n}).attr(\"tabIndex\", -1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52931, + "nodeLength": 41, + "src": "0 === parseInt(t(this).attr(\"tabIndex\"), 10)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53168, + "nodeLength": 42, + "src": "t.length && (!e.length || t.index() < e.index())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53179, + "nodeLength": 30, + "src": "!e.length || t.index() < e.index()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53190, + "nodeLength": 19, + "src": "t.index() < e.index()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53213, + "nodeLength": 24, + "src": "this.options.animate || {}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53240, + "nodeLength": 12, + "src": "l && c.down || c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53240, + "nodeLength": 9, + "src": "l && c.down", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53294, + "nodeLength": 25, + "src": "\"number\" == typeof u && (o = u)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53294, + "nodeLength": 18, + "src": "\"number\" == typeof u", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53320, + "nodeLength": 25, + "src": "\"string\" == typeof u && (n = u)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53320, + "nodeLength": 18, + "src": "\"string\" == typeof u", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53348, + "nodeLength": 21, + "src": "n || u.easing || c.easing", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53351, + "nodeLength": 18, + "src": "u.easing || c.easing", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53372, + "nodeLength": 25, + "src": "o || u.duration || c.duration", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53375, + "nodeLength": 22, + "src": "u.duration || c.duration", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53398, + "nodeLength": 8, + "src": "e.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53407, + "nodeLength": 8, + "src": "t.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53633, + "nodeLength": 17, + "src": "\"height\" !== i.prop", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53651, + "nodeLength": 29, + "src": "\"content-box\" === h && (r += i.now)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53651, + "nodeLength": 17, + "src": "\"content-box\" === h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53681, + "nodeLength": 78, + "src": "\"content\" !== a.options.heightStyle && (i.now = Math.round(s - e.outerHeight() - r) , r = 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53681, + "nodeLength": 33, + "src": "\"content\" !== a.options.heightStyle", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54038, + "nodeLength": 59, + "src": "e.length && (e.parent()[0].className = e.parent()[0].className)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54222, + "nodeLength": 13, + "src": "e || (e = t.body)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54236, + "nodeLength": 22, + "src": "e.nodeName || (e = t.body)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54842, + "nodeLength": 338, + "src": "!this.mouseHandled && i.not(\".ui-state-disabled\").length && (this.select(e) , e.isPropagationStopped() || (this.mouseHandled = !0) , i.has(\".ui-menu\").length ? this.expand(e) : !this.element.is(\":focus\") && s.closest(\".ui-menu\").length && (this.element.trigger(\"focus\", [!0]) , this.active && 1 === this.active.parents(\".ui-menu\").length && clearTimeout(this.timer)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54862, + "nodeLength": 318, + "src": "i.not(\".ui-state-disabled\").length && (this.select(e) , e.isPropagationStopped() || (this.mouseHandled = !0) , i.has(\".ui-menu\").length ? this.expand(e) : !this.element.is(\":focus\") && s.closest(\".ui-menu\").length && (this.element.trigger(\"focus\", [!0]) , this.active && 1 === this.active.parents(\".ui-menu\").length && clearTimeout(this.timer)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54914, + "nodeLength": 48, + "src": "e.isPropagationStopped() || (this.mouseHandled = !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54963, + "nodeLength": 24, + "src": "i.has(\".ui-menu\").length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55003, + "nodeLength": 176, + "src": "!this.element.is(\":focus\") && s.closest(\".ui-menu\").length && (this.element.trigger(\"focus\", [!0]) , this.active && 1 === this.active.parents(\".ui-menu\").length && clearTimeout(this.timer))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55031, + "nodeLength": 148, + "src": "s.closest(\".ui-menu\").length && (this.element.trigger(\"focus\", [!0]) , this.active && 1 === this.active.parents(\".ui-menu\").length && clearTimeout(this.timer))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55097, + "nodeLength": 81, + "src": "this.active && 1 === this.active.parents(\".ui-menu\").length && clearTimeout(this.timer)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55110, + "nodeLength": 68, + "src": "1 === this.active.parents(\".ui-menu\").length && clearTimeout(this.timer)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55110, + "nodeLength": 42, + "src": "1 === this.active.parents(\".ui-menu\").length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55224, + "nodeLength": 20, + "src": "!this.previousFilter", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55310, + "nodeLength": 114, + "src": "i[0] === s[0] && (this._removeClass(s.siblings().children(\".ui-state-active\"), null, \"ui-state-active\") , this.focus(e, s))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55310, + "nodeLength": 11, + "src": "i[0] === s[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55514, + "nodeLength": 56, + "src": "this.active || this.element.find(this.options.items).eq(0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55571, + "nodeLength": 18, + "src": "e || this.focus(t, i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55707, + "nodeLength": 22, + "src": "i && this.collapseAll(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55811, + "nodeLength": 50, + "src": "this._closeOnDocumentClick(t) && this.collapseAll(t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56305, + "nodeLength": 43, + "src": "e.data(\"ui-menu-submenu-caret\") && e.remove()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56783, + "nodeLength": 66, + "src": "this.active && !this.active.is(\".ui-state-disabled\") && this.expand(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56796, + "nodeLength": 53, + "src": "!this.active.is(\".ui-state-disabled\") && this.expand(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56991, + "nodeLength": 23, + "src": "this.previousFilter || \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57022, + "nodeLength": 29, + "src": "e.keyCode >= 96 && 105 >= e.keyCode", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57022, + "nodeLength": 13, + "src": "e.keyCode >= 96", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57037, + "nodeLength": 14, + "src": "105 >= e.keyCode", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57132, + "nodeLength": 5, + "src": "n === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57178, + "nodeLength": 35, + "src": "o && -1 !== i.index(this.active.next())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57181, + "nodeLength": 32, + "src": "-1 !== i.index(this.active.next())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57253, + "nodeLength": 71, + "src": "i.length || (n = String.fromCharCode(e.keyCode) , i = this._filterMenuItems(n))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57325, + "nodeLength": 8, + "src": "i.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57474, + "nodeLength": 21, + "src": "a && e.preventDefault()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57519, + "nodeLength": 137, + "src": "this.active && !this.active.is(\".ui-state-disabled\") && (this.active.children(\"[aria-haspopup='true']\").length ? this.expand(t) : this.select(t))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57532, + "nodeLength": 124, + "src": "!this.active.is(\".ui-state-disabled\") && (this.active.children(\"[aria-haspopup='true']\").length ? this.expand(t) : this.select(t))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57572, + "nodeLength": 53, + "src": "this.active.children(\"[aria-haspopup='true']\").length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58343, + "nodeLength": 69, + "src": "a._isDivider(e) && a._addClass(e, \"ui-menu-divider\", \"ui-widget-content\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58671, + "nodeLength": 69, + "src": "this.active && !t.contains(this.element[0], this.active[0]) && this.blur()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58684, + "nodeLength": 56, + "src": "!t.contains(this.element[0], this.active[0]) && this.blur()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58852, + "nodeLength": 11, + "src": "\"icons\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59182, + "nodeLength": 19, + "src": "t && \"focus\" === t.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15, + "nodeLength": 16, + "src": "\"focus\" === t.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59338, + "nodeLength": 74, + "src": "this.options.role && this.element.attr(\"aria-activedescendant\", s.attr(\"id\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59536, + "nodeLength": 21, + "src": "t && \"keydown\" === t.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59539, + "nodeLength": 18, + "src": "\"keydown\" === t.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59658, + "nodeLength": 57, + "src": "i.length && t && /^mouse/.test(t.type) && this._startOpening(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59668, + "nodeLength": 47, + "src": "t && /^mouse/.test(t.type) && this._startOpening(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59671, + "nodeLength": 44, + "src": "/^mouse/.test(t.type) && this._startOpening(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59822, + "nodeLength": 338, + "src": "this._hasScroll() && (i = parseFloat(t.css(this.activeMenu[0], \"borderTopWidth\")) || 0 , s = parseFloat(t.css(this.activeMenu[0], \"paddingTop\")) || 0 , n = e.offset().top - this.activeMenu.offset().top - i - s , o = this.activeMenu.scrollTop() , a = this.activeMenu.height() , r = e.outerHeight() , 0 > n ? this.activeMenu.scrollTop(o + n) : n + r > a && this.activeMenu.scrollTop(o + n - a + r))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59844, + "nodeLength": 57, + "src": "parseFloat(t.css(this.activeMenu[0], \"borderTopWidth\")) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59904, + "nodeLength": 53, + "src": "parseFloat(t.css(this.activeMenu[0], \"paddingTop\")) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 60083, + "nodeLength": 3, + "src": "0 > n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 60118, + "nodeLength": 41, + "src": "n + r > a && this.activeMenu.scrollTop(o + n - a + r)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 60118, + "nodeLength": 5, + "src": "n + r > a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 60181, + "nodeLength": 27, + "src": "e || clearTimeout(this.timer)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 60209, + "nodeLength": 162, + "src": "this.active && (this._removeClass(this.active.children(\".ui-menu-item-wrapper\"), null, \"ui-state-active\") , this._trigger(\"blur\", t, {\n item: this.active}) , this.active = null)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 60424, + "nodeLength": 108, + "src": "\"true\" === t.attr(\"aria-hidden\") && (this.timer = this._delay(function() {\n this._close() , this._open(t);\n}, this.delay))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 60424, + "nodeLength": 30, + "src": "\"true\" === t.attr(\"aria-hidden\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 60891, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 60908, + "nodeLength": 11, + "src": "e && e.target", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 60960, + "nodeLength": 26, + "src": "s.length || (s = this.element)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61135, + "nodeLength": 52, + "src": "t || (t = this.active ? this.active.parent() : this.element)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61141, + "nodeLength": 11, + "src": "this.active", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61448, + "nodeLength": 71, + "src": "this.active && this.active.parent().closest(\".ui-menu-item\", this.element)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61520, + "nodeLength": 44, + "src": "e && e.length && (this._close() , this.focus(t, e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61523, + "nodeLength": 41, + "src": "e.length && (this._close() , this.focus(t, e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61591, + "nodeLength": 79, + "src": "this.active && this.active.children(\".ui-menu \").find(this.options.items).first()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61671, + "nodeLength": 78, + "src": "e && e.length && (this._open(e.parent()) , this._delay(function() {\n this.focus(t, e);\n}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61674, + "nodeLength": 75, + "src": "e.length && (this._open(e.parent()) , this._delay(function() {\n this.focus(t, e);\n}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61878, + "nodeLength": 57, + "src": "this.active && !this.active.prevAll(\".ui-menu-item\").length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61966, + "nodeLength": 57, + "src": "this.active && !this.active.nextAll(\".ui-menu-item\").length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62053, + "nodeLength": 153, + "src": "this.active && (s = \"first\" === t || \"last\" === t ? this.active[\"first\" === t ? \"prevAll\" : \"nextAll\"](\".ui-menu-item\").eq(-1) : this.active[t + \"All\"](\".ui-menu-item\").eq(0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62069, + "nodeLength": 23, + "src": "\"first\" === t || \"last\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62069, + "nodeLength": 11, + "src": "\"first\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62082, + "nodeLength": 10, + "src": "\"last\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62105, + "nodeLength": 11, + "src": "\"first\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62207, + "nodeLength": 75, + "src": "s && s.length && this.active || (s = this.activeMenu.find(this.options.items)[e]())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62207, + "nodeLength": 24, + "src": "s && s.length && this.active", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62210, + "nodeLength": 21, + "src": "s.length && this.active", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62338, + "nodeLength": 11, + "src": "this.active", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62351, + "nodeLength": 285, + "src": "this.isLastItem() || (this._hasScroll() ? (s = this.active.offset().top , n = this.element.height() , this.active.nextAll(\".ui-menu-item\").each(function() {\n return i = t(this) , 0 > i.offset().top - s - n;\n}) , this.focus(e, i)) : this.focus(e, this.activeMenu.find(this.options.items)[this.active ? \"last\" : \"first\"]()))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62371, + "nodeLength": 17, + "src": "this._hasScroll()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62511, + "nodeLength": 20, + "src": "0 > i.offset().top - s - n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62605, + "nodeLength": 11, + "src": "this.active", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62710, + "nodeLength": 11, + "src": "this.active", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62723, + "nodeLength": 264, + "src": "this.isFirstItem() || (this._hasScroll() ? (s = this.active.offset().top , n = this.element.height() , this.active.prevAll(\".ui-menu-item\").each(function() {\n return i = t(this) , i.offset().top - s + n > 0;\n}) , this.focus(e, i)) : this.focus(e, this.activeMenu.find(this.options.items).first()))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62744, + "nodeLength": 17, + "src": "this._hasScroll()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62884, + "nodeLength": 20, + "src": "i.offset().top - s + n > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63048, + "nodeLength": 60, + "src": "this.element.outerHeight() < this.element.prop(\"scrollHeight\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63141, + "nodeLength": 49, + "src": "this.active || t(e.target).closest(\".ui-menu-item\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63216, + "nodeLength": 58, + "src": "this.active.has(\".ui-menu\").length || this.collapseAll(e, !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63953, + "nodeLength": 14, + "src": "\"textarea\" === n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63970, + "nodeLength": 11, + "src": "\"input\" === n", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "8": [ + null, + { + "position": 100, + "nodeLength": 44, + "src": "o || !a && this._isContentEditable(this.element)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 103, + "nodeLength": 41, + "src": "!a && this._isContentEditable(this.element)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175, + "nodeLength": 4, + "src": "o || a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 338, + "nodeLength": 29, + "src": "this.element.prop(\"readOnly\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 668, + "nodeLength": 63, + "src": "this.menu.active && (e = !0 , n.preventDefault() , this.menu.select(n))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 749, + "nodeLength": 37, + "src": "this.menu.active && this.menu.select(n)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 807, + "nodeLength": 109, + "src": "this.menu.element.is(\":visible\") && (this.isMultiLine || this._value(this.term) , this.close(n) , n.preventDefault())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 842, + "nodeLength": 40, + "src": "this.isMultiLine || this._value(this.term)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 985, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1000, + "nodeLength": 72, + "src": "(!this.isMultiLine || this.menu.element.is(\":visible\")) && s.preventDefault()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1000, + "nodeLength": 51, + "src": "!this.isMultiLine || this.menu.element.is(\":visible\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1083, + "nodeLength": 2, + "src": "!i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1332, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1493, + "nodeLength": 15, + "src": "this.cancelBlur", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1917, + "nodeLength": 89, + "src": "this.element[0] !== t.ui.safeActiveElement(this.document[0]) && this.element.trigger(\"focus\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1917, + "nodeLength": 58, + "src": "this.element[0] !== t.ui.safeActiveElement(this.document[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2049, + "nodeLength": 88, + "src": "this.isNewMenu && (this.isNewMenu = !1 , e.originalEvent && /^mouse/.test(e.originalEvent.type))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2084, + "nodeLength": 52, + "src": "e.originalEvent && /^mouse/.test(e.originalEvent.type)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2283, + "nodeLength": 112, + "src": "!1 !== this._trigger(\"focus\", e, {\n item: n}) && e.originalEvent && /^key/.test(e.originalEvent.type) && this._value(n.value)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2283, + "nodeLength": 38, + "src": "!1 !== this._trigger(\"focus\", e, {\n item: n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2323, + "nodeLength": 72, + "src": "e.originalEvent && /^key/.test(e.originalEvent.type) && this._value(n.value)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2340, + "nodeLength": 55, + "src": "/^key/.test(e.originalEvent.type) && this._value(n.value)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2398, + "nodeLength": 34, + "src": "i.item.attr(\"aria-label\") || n.value", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2433, + "nodeLength": 101, + "src": "s && t.trim(s).length && (this.liveRegion.children().hide() , t(\"
\").text(s).appendTo(this.liveRegion))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2436, + "nodeLength": 98, + "src": "t.trim(s).length && (this.liveRegion.children().hide() , t(\"
\").text(s).appendTo(this.liveRegion))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2627, + "nodeLength": 168, + "src": "this.element[0] !== t.ui.safeActiveElement(this.document[0]) && (this.element.trigger(\"focus\") , this.previous = n , this._delay(function() {\n this.previous = n , this.selectedItem = s;\n}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2627, + "nodeLength": 58, + "src": "this.element[0] !== t.ui.safeActiveElement(this.document[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2796, + "nodeLength": 61, + "src": "!1 !== this._trigger(\"select\", e, {\n item: s}) && this._value(s.value)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2796, + "nodeLength": 39, + "src": "!1 !== this._trigger(\"select\", e, {\n item: s})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3685, + "nodeLength": 32, + "src": "\"source\" === t && this._initSource()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3685, + "nodeLength": 12, + "src": "\"source\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3718, + "nodeLength": 60, + "src": "\"appendTo\" === t && this.menu.element.appendTo(this._appendTo())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3718, + "nodeLength": 14, + "src": "\"appendTo\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3779, + "nodeLength": 45, + "src": "\"disabled\" === t && e && this.xhr && this.xhr.abort()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3779, + "nodeLength": 14, + "src": "\"disabled\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3795, + "nodeLength": 29, + "src": "e && this.xhr && this.xhr.abort()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3798, + "nodeLength": 26, + "src": "this.xhr && this.xhr.abort()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3895, + "nodeLength": 64, + "src": "e.target === this.element[0] || e.target === i || t.contains(i, e.target)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3895, + "nodeLength": 26, + "src": "e.target === this.element[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3923, + "nodeLength": 36, + "src": "e.target === i || t.contains(i, e.target)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3923, + "nodeLength": 12, + "src": "e.target === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3994, + "nodeLength": 44, + "src": "this._isEventTargetInWidget(t) || this.close()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4096, + "nodeLength": 60, + "src": "e && (e = e.jquery || e.nodeType ? t(e) : this.document.find(e).eq(0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4102, + "nodeLength": 20, + "src": "e.jquery || e.nodeType", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4157, + "nodeLength": 54, + "src": "e && e[0] || (e = this.element.closest(\".ui-front, dialog\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4157, + "nodeLength": 7, + "src": "e && e[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4212, + "nodeLength": 35, + "src": "e.length || (e = this.document[0].body)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4289, + "nodeLength": 30, + "src": "t.isArray(this.options.source)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4409, + "nodeLength": 36, + "src": "\"string\" == typeof this.options.source", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4495, + "nodeLength": 20, + "src": "s.xhr && s.xhr.abort()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4746, + "nodeLength": 25, + "src": "this.term === this._value()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4809, + "nodeLength": 42, + "src": "t.altKey || t.ctrlKey || t.metaKey || t.shiftKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4819, + "nodeLength": 32, + "src": "t.ctrlKey || t.metaKey || t.shiftKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4830, + "nodeLength": 21, + "src": "t.metaKey || t.shiftKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4853, + "nodeLength": 60, + "src": "(!e || e && !i && !s) && (this.selectedItem = null , this.search(null, t))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4853, + "nodeLength": 13, + "src": "!e || e && !i && !s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4857, + "nodeLength": 9, + "src": "e && !i && !s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4860, + "nodeLength": 6, + "src": "!i && !s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4966, + "nodeLength": 7, + "src": "null != t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5014, + "nodeLength": 31, + "src": "t.length < this.options.minLength", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5060, + "nodeLength": 30, + "src": "this._trigger(\"search\", e) !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5327, + "nodeLength": 41, + "src": "e === this.requestIndex && this.__response(t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5327, + "nodeLength": 21, + "src": "e === this.requestIndex", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5384, + "nodeLength": 58, + "src": "this.pending || this._removeClass(\"ui-autocomplete-loading\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5474, + "nodeLength": 25, + "src": "t && (t = this._normalize(t))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5543, + "nodeLength": 55, + "src": "!this.options.disabled && t && t.length && !this.cancelSearch", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5567, + "nodeLength": 31, + "src": "t && t.length && !this.cancelSearch", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5570, + "nodeLength": 28, + "src": "t.length && !this.cancelSearch", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5766, + "nodeLength": 120, + "src": "this.menu.element.is(\":visible\") && (this.menu.element.hide() , this.menu.blur() , this.isNewMenu = !0 , this._trigger(\"close\", t))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5908, + "nodeLength": 81, + "src": "this.previous !== this._value() && this._trigger(\"change\", t, {\n item: this.selectedItem})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5908, + "nodeLength": 29, + "src": "this.previous !== this._value()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6021, + "nodeLength": 32, + "src": "e.length && e[0].label && e[0].value", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6031, + "nodeLength": 22, + "src": "e[0].label && e[0].value", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6082, + "nodeLength": 18, + "src": "\"string\" == typeof e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6140, + "nodeLength": 16, + "src": "e.label || e.value", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6163, + "nodeLength": 16, + "src": "e.value || e.label", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6388, + "nodeLength": 40, + "src": "this.options.autoFocus && this.menu.next()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6907, + "nodeLength": 32, + "src": "this.menu.element.is(\":visible\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6940, + "nodeLength": 85, + "src": "this.menu.isFirstItem() && /^previous/.test(t) || this.menu.isLastItem() && /^next/.test(t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6940, + "nodeLength": 44, + "src": "this.menu.isFirstItem() && /^previous/.test(t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6986, + "nodeLength": 39, + "src": "this.menu.isLastItem() && /^next/.test(t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7027, + "nodeLength": 40, + "src": "this.isMultiLine || this._value(this.term)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7290, + "nodeLength": 90, + "src": "(!this.isMultiLine || this.menu.element.is(\":visible\")) && (this._move(t, e) , e.preventDefault())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7290, + "nodeLength": 51, + "src": "!this.isMultiLine || this.menu.element.is(\":visible\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7416, + "nodeLength": 9, + "src": "!t.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7473, + "nodeLength": 13, + "src": "\"inherit\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7523, + "nodeLength": 10, + "src": "\"true\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29171, + "nodeLength": 19, + "src": "t.label || t.value || t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23, + "nodeLength": 10, + "src": "t.value || t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29323, + "nodeLength": 3, + "src": "t > 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29470, + "nodeLength": 208, + "src": "this.options.disabled || this.cancelSearch || (i = e && e.length ? this.options.messages.results(e.length) : this.options.messages.noResults , this.liveRegion.children().hide() , t(\"
\").text(i).appendTo(this.liveRegion))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29493, + "nodeLength": 185, + "src": "this.cancelSearch || (i = e && e.length ? this.options.messages.results(e.length) : this.options.messages.noResults , this.liveRegion.children().hide() , t(\"
\").text(i).appendTo(this.liveRegion))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29515, + "nodeLength": 11, + "src": "e && e.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 72535, + "nodeLength": 153, + "src": "this.options.items.controlgroupLabel && this.element.find(this.options.items.controlgroupLabel).find(\".ui-controlgroup-label-contents\").contents().unwrap()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 72788, + "nodeLength": 1, + "src": "n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 72790, + "nodeLength": 23, + "src": "\"controlgroupLabel\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 72867, + "nodeLength": 130, + "src": "e.children(\".ui-controlgroup-label-contents\").length || e.contents().wrapAll(\"\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73096, + "nodeLength": 397, + "src": "t.fn[s] && (a = e[\"_\" + s + \"Options\"] ? e[\"_\" + s + \"Options\"](\"middle\") : {\n classes: {}} , e.element.find(n).each(function() {\n var n = t(this), o = n[s](\"instance\"), r = t.widget.extend({}, a);\n if (\"button\" !== s || !n.parent(\".ui-spinner\").length) {\n o || (o = n[s]()[s](\"instance\")) , o && (r.classes = e._resolveClassesValues(r.classes, o)) , n[s](r);\n var h = n[s](\"widget\");\n t.data(h[0], \"ui-controlgroup-data\", o ? o : n[s](\"instance\")) , i.push(h[0]);\n }\n}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73108, + "nodeLength": 18, + "src": "e[\"_\" + s + \"Options\"]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73263, + "nodeLength": 45, + "src": "\"button\" !== s || !n.parent(\".ui-spinner\").length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73263, + "nodeLength": 12, + "src": "\"button\" !== s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73310, + "nodeLength": 28, + "src": "o || (o = n[s]()[s](\"instance\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73339, + "nodeLength": 51, + "src": "o && (r.classes = e._resolveClassesValues(r.classes, o))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73455, + "nodeLength": 1, + "src": "o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73712, + "nodeLength": 15, + "src": "s && s[e] && s[e]()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73715, + "nodeLength": 12, + "src": "s[e] && s[e]()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73994, + "nodeLength": 35, + "src": "\"vertical\" === this.options.direction", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74096, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74131, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74533, + "nodeLength": 35, + "src": "\"vertical\" === this.options.direction", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74582, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74725, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74783, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74834, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74898, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75101, + "nodeLength": 24, + "src": "i.options.classes[n] || \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75226, + "nodeLength": 77, + "src": "\"direction\" === t && this._removeClass(\"ui-controlgroup-\" + this.options.direction)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75226, + "nodeLength": 15, + "src": "\"direction\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75321, + "nodeLength": 14, + "src": "\"disabled\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75359, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75520, + "nodeLength": 80, + "src": "\"horizontal\" === this.options.direction && this._addClass(null, \"ui-helper-clearfix\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75520, + "nodeLength": 37, + "src": "\"horizontal\" === this.options.direction", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75641, + "nodeLength": 50, + "src": "this.options.onlyVisible && (e = e.filter(\":visible\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75692, + "nodeLength": 332, + "src": "e.length && (t.each([\"first\", \"last\"], function(t, s) {\n var n = e[s]().data(\"ui-controlgroup-data\");\n if (n && i[\"_\" + n.widgetName + \"Options\"]) {\n var o = i[\"_\" + n.widgetName + \"Options\"](1 === e.length ? \"only\" : s);\n o.classes = i._resolveClassesValues(o.classes, n) , n.element[n.widgetName](o);\n } else \n i._updateCornerClass(e[s](), s);\n}) , this._callChildMethod(\"refresh\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75786, + "nodeLength": 32, + "src": "n && i[\"_\" + n.widgetName + \"Options\"]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75856, + "nodeLength": 12, + "src": "1 === e.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76275, + "nodeLength": 17, + "src": "this._super() || {}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76369, + "nodeLength": 69, + "src": "this.label.length || t.error(\"No label found for checkboxradio widget\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76537, + "nodeLength": 17, + "src": "3 === this.nodeType", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76587, + "nodeLength": 48, + "src": "this.originalLabel && (n.label = this.originalLabel)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76663, + "nodeLength": 23, + "src": "null != e && (n.disabled = e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76663, + "nodeLength": 7, + "src": "null != e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76768, + "nodeLength": 77, + "src": "null == this.options.disabled && (this.options.disabled = this.element[0].disabled)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76768, + "nodeLength": 27, + "src": "null == this.options.disabled", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 77035, + "nodeLength": 78, + "src": "\"radio\" === this.type && this._addClass(this.label, \"ui-checkboxradio-radio-label\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 77035, + "nodeLength": 19, + "src": "\"radio\" === this.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 77114, + "nodeLength": 59, + "src": "this.options.label && this.options.label !== this.originalLabel", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 77134, + "nodeLength": 39, + "src": "this.options.label !== this.originalLabel", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 77194, + "nodeLength": 59, + "src": "this.originalLabel && (this.options.label = this.originalLabel)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 77270, + "nodeLength": 135, + "src": "t && (this._addClass(this.label, \"ui-checkboxradio-checked\", \"ui-state-active\") , this.icon && this._addClass(this.icon, null, \"ui-state-hover\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 77346, + "nodeLength": 58, + "src": "this.icon && this._addClass(this.icon, null, \"ui-state-hover\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 77708, + "nodeLength": 138, + "src": "\"input\" === e && /radio|checkbox/.test(this.type) || t.error(\"Can't create checkboxradio on element.nodeName=\" + e + \" and element.type=\" + this.type)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 77708, + "nodeLength": 45, + "src": "\"input\" === e && /radio|checkbox/.test(this.type)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 77708, + "nodeLength": 11, + "src": "\"input\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 78055, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 78060, + "nodeLength": 16, + "src": "this.form.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 78142, + "nodeLength": 25, + "src": "0 === t(this).form().length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 78331, + "nodeLength": 159, + "src": "this.options.icon && \"checkbox\" === this.type && this._toggleClass(this.icon, null, \"ui-icon-check ui-state-checked\", e)._toggleClass(this.icon, null, \"ui-icon-blank\", !e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 78350, + "nodeLength": 140, + "src": "\"checkbox\" === this.type && this._toggleClass(this.icon, null, \"ui-icon-check ui-state-checked\", e)._toggleClass(this.icon, null, \"ui-icon-blank\", !e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 78350, + "nodeLength": 22, + "src": "\"checkbox\" === this.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 78491, + "nodeLength": 172, + "src": "\"radio\" === this.type && this._getRadioGroup().each(function() {\n var e = t(this).checkboxradio(\"instance\");\n e && e._removeClass(e.label, \"ui-checkboxradio-checked\", \"ui-state-active\");\n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 78491, + "nodeLength": 19, + "src": "\"radio\" === this.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 78590, + "nodeLength": 71, + "src": "e && e._removeClass(e.label, \"ui-checkboxradio-checked\", \"ui-state-active\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 78716, + "nodeLength": 55, + "src": "this.icon && (this.icon.remove() , this.iconSpace.remove())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 78804, + "nodeLength": 14, + "src": "\"label\" !== t || e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 78804, + "nodeLength": 11, + "src": "\"label\" !== t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 78837, + "nodeLength": 14, + "src": "\"disabled\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 79038, + "nodeLength": 17, + "src": "this.options.icon", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 79057, + "nodeLength": 130, + "src": "this.icon || (this.icon = t(\"\") , this.iconSpace = t(\" \") , this._addClass(this.iconSpace, \"ui-checkboxradio-icon-space\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 79188, + "nodeLength": 22, + "src": "\"checkbox\" === this.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 79215, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 79299, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 79406, + "nodeLength": 69, + "src": "e || this._removeClass(this.icon, null, \"ui-icon-check ui-state-checked\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 79531, + "nodeLength": 81, + "src": "void 0 !== this.icon && (this.icon.remove() , this.iconSpace.remove() , delete this.icon)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 79531, + "nodeLength": 18, + "src": "void 0 !== this.icon", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 79687, + "nodeLength": 34, + "src": "this.icon && (t = t.not(this.icon[0]))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 79722, + "nodeLength": 44, + "src": "this.iconSpace && (t = t.not(this.iconSpace[0]))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 79990, + "nodeLength": 46, + "src": "null !== this.options.label && this._updateLabel()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 79990, + "nodeLength": 25, + "src": "null !== this.options.label", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 80037, + "nodeLength": 57, + "src": "e !== this.options.disabled && this._setOptions({\n disabled: e})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 80037, + "nodeLength": 25, + "src": "e !== this.options.disabled", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 80341, + "nodeLength": 17, + "src": "this._super() || {}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 80431, + "nodeLength": 23, + "src": "null != t && (e.disabled = t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 80431, + "nodeLength": 7, + "src": "null != t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 80474, + "nodeLength": 12, + "src": "this.isInput", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 80526, + "nodeLength": 48, + "src": "this.originalLabel && (e.label = this.originalLabel)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 80597, + "nodeLength": 70, + "src": "!this.option.showLabel & !this.options.icon && (this.options.showLabel = !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 80668, + "nodeLength": 81, + "src": "null == this.options.disabled && (this.options.disabled = this.element[0].disabled || !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 80668, + "nodeLength": 27, + "src": "null == this.options.disabled", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 80720, + "nodeLength": 28, + "src": "this.element[0].disabled || !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 80793, + "nodeLength": 150, + "src": "this.options.label && this.options.label !== this.originalLabel && (this.isInput ? this.element.val(this.options.label) : this.element.html(this.options.label))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 80813, + "nodeLength": 130, + "src": "this.options.label !== this.originalLabel && (this.isInput ? this.element.val(this.options.label) : this.element.html(this.options.label))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 80813, + "nodeLength": 39, + "src": "this.options.label !== this.originalLabel", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 80855, + "nodeLength": 12, + "src": "this.isInput", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81050, + "nodeLength": 181, + "src": "this.element.is(\"a\") && this._on({\n keyup: function(e) {\n e.keyCode === t.ui.keyCode.SPACE && (e.preventDefault() , this.element[0].click ? this.element[0].click() : this.element.trigger(\"click\"));\n}})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81100, + "nodeLength": 128, + "src": "e.keyCode === t.ui.keyCode.SPACE && (e.preventDefault() , this.element[0].click ? this.element[0].click() : this.element.trigger(\"click\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81100, + "nodeLength": 30, + "src": "e.keyCode === t.ui.keyCode.SPACE", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81152, + "nodeLength": 21, + "src": "this.element[0].click", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81253, + "nodeLength": 61, + "src": "this.element.is(\"button\") || this.element.attr(\"role\", \"button\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81315, + "nodeLength": 85, + "src": "this.options.icon && (this._updateIcon(\"icon\", this.options.icon) , this._updateTooltip())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81466, + "nodeLength": 81, + "src": "this.options.showLabel || this.title || this.element.attr(\"title\", this.options.label)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81490, + "nodeLength": 57, + "src": "this.title || this.element.attr(\"title\", this.options.label)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81581, + "nodeLength": 18, + "src": "\"iconPosition\" !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81602, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81634, + "nodeLength": 23, + "src": "\"top\" === n || \"bottom\" === n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81634, + "nodeLength": 9, + "src": "\"top\" === n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81645, + "nodeLength": 12, + "src": "\"bottom\" === n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81658, + "nodeLength": 9, + "src": "this.icon", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81668, + "nodeLength": 54, + "src": "s && this._removeClass(this.icon, null, this.options.icon)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81799, + "nodeLength": 61, + "src": "this.options.showLabel || this._addClass(\"ui-button-icon-only\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81862, + "nodeLength": 35, + "src": "s && this._addClass(this.icon, null, i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81918, + "nodeLength": 1, + "src": "o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81975, + "nodeLength": 39, + "src": "this.iconSpace && this.iconSpace.remove()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82017, + "nodeLength": 106, + "src": "this.iconSpace || (this.iconSpace = t(\" \") , this._addClass(this.iconSpace, \"ui-button-icon-space\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82259, + "nodeLength": 29, + "src": "this.icon && this.icon.remove()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82289, + "nodeLength": 39, + "src": "this.iconSpace && this.iconSpace.remove()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82329, + "nodeLength": 47, + "src": "this.hasTitle || this.element.removeAttr(\"title\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82417, + "nodeLength": 25, + "src": "/^(?:end|bottom)/.test(t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82515, + "nodeLength": 25, + "src": "/^(?:end|bottom)/.test(t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82603, + "nodeLength": 20, + "src": "void 0 === t.showLabel", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82661, + "nodeLength": 15, + "src": "void 0 === t.icon", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82702, + "nodeLength": 22, + "src": "e || i || (t.showLabel = !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82705, + "nodeLength": 19, + "src": "i || (t.showLabel = !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82766, + "nodeLength": 109, + "src": "\"icon\" === t && (e ? this._updateIcon(t, e) : this.icon && (this.icon.remove() , this.iconSpace && this.iconSpace.remove()))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82766, + "nodeLength": 10, + "src": "\"icon\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82779, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82803, + "nodeLength": 71, + "src": "this.icon && (this.icon.remove() , this.iconSpace && this.iconSpace.remove())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82834, + "nodeLength": 39, + "src": "this.iconSpace && this.iconSpace.remove()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82876, + "nodeLength": 41, + "src": "\"iconPosition\" === t && this._updateIcon(t, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82876, + "nodeLength": 18, + "src": "\"iconPosition\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82918, + "nodeLength": 89, + "src": "\"showLabel\" === t && (this._toggleClass(\"ui-button-icon-only\", null, !e) , this._updateTooltip())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82918, + "nodeLength": 15, + "src": "\"showLabel\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83008, + "nodeLength": 176, + "src": "\"label\" === t && (this.isInput ? this.element.val(e) : (this.element.html(e) , this.icon && (this._attachIcon(this.options.iconPosition) , this._attachIconSpace(this.options.iconPosition))))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83008, + "nodeLength": 11, + "src": "\"label\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83022, + "nodeLength": 12, + "src": "this.isInput", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83077, + "nodeLength": 105, + "src": "this.icon && (this._attachIcon(this.options.iconPosition) , this._attachIconSpace(this.options.iconPosition))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83202, + "nodeLength": 113, + "src": "\"disabled\" === t && (this._toggleClass(null, \"ui-state-disabled\", e) , this.element[0].disabled = e , e && this.element.blur())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83202, + "nodeLength": 14, + "src": "\"disabled\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83292, + "nodeLength": 22, + "src": "e && this.element.blur()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83342, + "nodeLength": 32, + "src": "this.element.is(\"input, button\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83444, + "nodeLength": 57, + "src": "t !== this.options.disabled && this._setOptions({\n disabled: t})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83444, + "nodeLength": 25, + "src": "t !== this.options.disabled", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83527, + "nodeLength": 1865, + "src": "t.uiBackCompat !== !1 && (t.widget(\"ui.button\", t.ui.button, {\n options: {\n text: !0, \n icons: {\n primary: null, \n secondary: null}}, \n _create: function() {\n this.options.showLabel && !this.options.text && (this.options.showLabel = this.options.text) , !this.options.showLabel && this.options.text && (this.options.text = this.options.showLabel) , this.options.icon || !this.options.icons.primary && !this.options.icons.secondary ? this.options.icon && (this.options.icons.primary = this.options.icon) : this.options.icons.primary ? this.options.icon = this.options.icons.primary : (this.options.icon = this.options.icons.secondary , this.options.iconPosition = \"end\") , this._super();\n}, \n _setOption: function(t, e) {\n return \"text\" === t ? (this._super(\"showLabel\", e) , void 0) : (\"showLabel\" === t && (this.options.text = e) , \"icon\" === t && (this.options.icons.primary = e) , \"icons\" === t && (e.primary ? (this._super(\"icon\", e.primary) , this._super(\"iconPosition\", \"beginning\")) : e.secondary && (this._super(\"icon\", e.secondary) , this._super(\"iconPosition\", \"end\"))) , this._superApply(arguments) , void 0);\n}}) , t.fn.button = function(e) {\n return function() {\n return !this.length || this.length && \"INPUT\" !== this[0].tagName || this.length && \"INPUT\" === this[0].tagName && \"checkbox\" !== this.attr(\"type\") && \"radio\" !== this.attr(\"type\") ? e.apply(this, arguments) : (t.ui.checkboxradio || t.error(\"Checkboxradio widget missing\") , 0 === arguments.length ? this.checkboxradio({\n icon: !1}) : this.checkboxradio.apply(this, arguments));\n};\n}(t.fn.button) , t.fn.buttonset = function() {\n return t.ui.controlgroup || t.error(\"Controlgroup widget missing\") , \"option\" === arguments[0] && \"items\" === arguments[1] && arguments[2] ? this.controlgroup.apply(this, [arguments[0], \"items.button\", arguments[2]]) : \"option\" === arguments[0] && \"items\" === arguments[1] ? this.controlgroup.apply(this, [arguments[0], \"items.button\"]) : (\"object\" == typeof arguments[0] && arguments[0].items && (arguments[0].items = {\n button: arguments[0].items}) , this.controlgroup.apply(this, arguments));\n})", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 83527, + "nodeLength": 19, + "src": "t.uiBackCompat !== !1", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 83656, + "nodeLength": 86, + "src": "this.options.showLabel && !this.options.text && (this.options.showLabel = this.options.text)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83680, + "nodeLength": 62, + "src": "!this.options.text && (this.options.showLabel = this.options.text)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83743, + "nodeLength": 86, + "src": "!this.options.showLabel && this.options.text && (this.options.text = this.options.showLabel)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83768, + "nodeLength": 61, + "src": "this.options.text && (this.options.text = this.options.showLabel)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83830, + "nodeLength": 77, + "src": "this.options.icon || !this.options.icons.primary && !this.options.icons.secondary", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83849, + "nodeLength": 58, + "src": "!this.options.icons.primary && !this.options.icons.secondary", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83908, + "nodeLength": 65, + "src": "this.options.icon && (this.options.icons.primary = this.options.icon)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 83974, + "nodeLength": 26, + "src": "this.options.icons.primary", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84173, + "nodeLength": 10, + "src": "\"text\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84221, + "nodeLength": 38, + "src": "\"showLabel\" === t && (this.options.text = e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84221, + "nodeLength": 15, + "src": "\"showLabel\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84260, + "nodeLength": 42, + "src": "\"icon\" === t && (this.options.icons.primary = e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84260, + "nodeLength": 10, + "src": "\"icon\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84303, + "nodeLength": 177, + "src": "\"icons\" === t && (e.primary ? (this._super(\"icon\", e.primary) , this._super(\"iconPosition\", \"beginning\")) : e.secondary && (this._super(\"icon\", e.secondary) , this._super(\"iconPosition\", \"end\")))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84303, + "nodeLength": 11, + "src": "\"icons\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84317, + "nodeLength": 9, + "src": "e.primary", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84399, + "nodeLength": 80, + "src": "e.secondary && (this._super(\"icon\", e.secondary) , this._super(\"iconPosition\", \"end\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84568, + "nodeLength": 153, + "src": "!this.length || this.length && \"INPUT\" !== this[0].tagName || this.length && \"INPUT\" === this[0].tagName && \"checkbox\" !== this.attr(\"type\") && \"radio\" !== this.attr(\"type\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84582, + "nodeLength": 139, + "src": "this.length && \"INPUT\" !== this[0].tagName || this.length && \"INPUT\" === this[0].tagName && \"checkbox\" !== this.attr(\"type\") && \"radio\" !== this.attr(\"type\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84582, + "nodeLength": 38, + "src": "this.length && \"INPUT\" !== this[0].tagName", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84595, + "nodeLength": 25, + "src": "\"INPUT\" !== this[0].tagName", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84622, + "nodeLength": 99, + "src": "this.length && \"INPUT\" === this[0].tagName && \"checkbox\" !== this.attr(\"type\") && \"radio\" !== this.attr(\"type\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84635, + "nodeLength": 86, + "src": "\"INPUT\" === this[0].tagName && \"checkbox\" !== this.attr(\"type\") && \"radio\" !== this.attr(\"type\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84635, + "nodeLength": 25, + "src": "\"INPUT\" === this[0].tagName", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84662, + "nodeLength": 59, + "src": "\"checkbox\" !== this.attr(\"type\") && \"radio\" !== this.attr(\"type\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84662, + "nodeLength": 30, + "src": "\"checkbox\" !== this.attr(\"type\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84694, + "nodeLength": 27, + "src": "\"radio\" !== this.attr(\"type\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84747, + "nodeLength": 59, + "src": "t.ui.checkboxradio || t.error(\"Checkboxradio widget missing\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84807, + "nodeLength": 20, + "src": "0 === arguments.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84948, + "nodeLength": 57, + "src": "t.ui.controlgroup || t.error(\"Controlgroup widget missing\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85006, + "nodeLength": 61, + "src": "\"option\" === arguments[0] && \"items\" === arguments[1] && arguments[2]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85006, + "nodeLength": 23, + "src": "\"option\" === arguments[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85031, + "nodeLength": 36, + "src": "\"items\" === arguments[1] && arguments[2]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85031, + "nodeLength": 22, + "src": "\"items\" === arguments[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85141, + "nodeLength": 47, + "src": "\"option\" === arguments[0] && \"items\" === arguments[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85141, + "nodeLength": 23, + "src": "\"option\" === arguments[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85166, + "nodeLength": 22, + "src": "\"items\" === arguments[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85250, + "nodeLength": 99, + "src": "\"object\" == typeof arguments[0] && arguments[0].items && (arguments[0].items = {\n button: arguments[0].items})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85250, + "nodeLength": 29, + "src": "\"object\" == typeof arguments[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85281, + "nodeLength": 68, + "src": "arguments[0].items && (arguments[0].items = {\n button: arguments[0].items})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85618, + "nodeLength": 5, + "src": "t || {}", + "evalFalse": 0, + "evalTrue": 10 + }, + { + "position": 85702, + "nodeLength": 21, + "src": "\"div\" === s || \"span\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85702, + "nodeLength": 9, + "src": "\"div\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85713, + "nodeLength": 10, + "src": "\"span\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85724, + "nodeLength": 40, + "src": "e.id || (this.uuid += 1 , e.id = \"dp\" + this.uuid)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85812, + "nodeLength": 5, + "src": "i || {}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85819, + "nodeLength": 11, + "src": "\"input\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 85860, + "nodeLength": 30, + "src": "n && this._inlineDatepicker(e, o)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 86071, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 86286, + "nodeLength": 270, + "src": "s.hasClass(this.markerClassName) || (this._attachments(s, i) , s.addClass(this.markerClassName).on(\"keydown\", this._doKeyDown).on(\"keypress\", this._doKeyPress).on(\"keyup\", this._doKeyUp) , this._autoSize(i) , t.data(e, \"datepicker\", i) , i.settings.disabled && this._disableDatepicker(e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 86508, + "nodeLength": 47, + "src": "i.settings.disabled && this._disableDatepicker(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 86646, + "nodeLength": 27, + "src": "i.append && i.append.remove()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 86674, + "nodeLength": 99, + "src": "a && (i.append = t(\"\" + a + \"\") , e[r ? \"before\" : \"after\"](i.append))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 86743, + "nodeLength": 1, + "src": "r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 86810, + "nodeLength": 29, + "src": "i.trigger && i.trigger.remove()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 86865, + "nodeLength": 60, + "src": "(\"focus\" === s || \"both\" === s) && e.on(\"focus\", this._showDatepicker)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 86865, + "nodeLength": 23, + "src": "\"focus\" === s || \"both\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 86865, + "nodeLength": 11, + "src": "\"focus\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 86878, + "nodeLength": 10, + "src": "\"both\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 86927, + "nodeLength": 653, + "src": "(\"button\" === s || \"both\" === s) && (n = this._get(i, \"buttonText\") , o = this._get(i, \"buttonImage\") , i.trigger = t(this._get(i, \"buttonImageOnly\") ? t(\"\").addClass(this._triggerClass).attr({\n src: o, \n alt: n, \n title: n}) : t(\"\").addClass(this._triggerClass).html(o ? t(\"\").attr({\n src: o, \n alt: n, \n title: n}) : n)) , e[r ? \"before\" : \"after\"](i.trigger) , i.trigger.on(\"click\", function() {\n return t.datepicker._datepickerShowing && t.datepicker._lastInput === e[0] ? t.datepicker._hideDatepicker() : t.datepicker._datepickerShowing && t.datepicker._lastInput !== e[0] ? (t.datepicker._hideDatepicker() , t.datepicker._showDatepicker(e[0])) : t.datepicker._showDatepicker(e[0]) , !1;\n}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 86927, + "nodeLength": 24, + "src": "\"button\" === s || \"both\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 86927, + "nodeLength": 12, + "src": "\"button\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 86941, + "nodeLength": 10, + "src": "\"both\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 87024, + "nodeLength": 30, + "src": "this._get(i, \"buttonImageOnly\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 87195, + "nodeLength": 1, + "src": "o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 87243, + "nodeLength": 1, + "src": "r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 87313, + "nodeLength": 63, + "src": "t.datepicker._datepickerShowing && t.datepicker._lastInput === e[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 87346, + "nodeLength": 30, + "src": "t.datepicker._lastInput === e[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 87408, + "nodeLength": 63, + "src": "t.datepicker._datepickerShowing && t.datepicker._lastInput !== e[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 87441, + "nodeLength": 30, + "src": "t.datepicker._lastInput !== e[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 87607, + "nodeLength": 34, + "src": "this._get(t, \"autoSize\") && !t.inline", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 87706, + "nodeLength": 263, + "src": "a.match(/[DM]/) && (e = function(t) {\n for (i = 0 , s = 0 , n = 0; t.length > n; n++) \n t[n].length > i && (i = t[n].length , s = n);\n return s;\n} , o.setMonth(e(this._get(t, a.match(/MM/) ? \"monthNames\" : \"monthNamesShort\"))) , o.setDate(e(this._get(t, a.match(/DD/) ? \"dayNames\" : \"dayNamesShort\")) + 20 - o.getDay()))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 87754, + "nodeLength": 10, + "src": "t.length > n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 87769, + "nodeLength": 34, + "src": "t[n].length > i && (i = t[n].length , s = n)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 87769, + "nodeLength": 13, + "src": "t[n].length > i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 87839, + "nodeLength": 13, + "src": "a.match(/MM/)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 87911, + "nodeLength": 13, + "src": "a.match(/DD/)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 88065, + "nodeLength": 283, + "src": "s.hasClass(this.markerClassName) || (s.addClass(this.markerClassName).append(i.dpDiv) , t.data(e, \"datepicker\", i) , this._setDate(i, this._getDefaultDate(i), !0) , this._updateDatepicker(i) , this._updateAlternate(i) , i.settings.disabled && this._disableDatepicker(e) , i.dpDiv.css(\"display\", \"block\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 88269, + "nodeLength": 47, + "src": "i.settings.disabled && this._disableDatepicker(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 88428, + "nodeLength": 339, + "src": "d || (this.uuid += 1 , r = \"dp\" + this.uuid , this._dialogInput = t(\"\") , this._dialogInput.on(\"keydown\", this._doKeyDown) , t(\"body\").append(this._dialogInput) , d = this._dialogInst = this._newInst(this._dialogInput, !1) , d.settings = {} , t.data(this._dialogInput[0], \"datepicker\", d))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 88781, + "nodeLength": 5, + "src": "n || {}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 88790, + "nodeLength": 23, + "src": "i && i.constructor === Date", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 88793, + "nodeLength": 20, + "src": "i.constructor === Date", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 88873, + "nodeLength": 1, + "src": "o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 88875, + "nodeLength": 8, + "src": "o.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 88909, + "nodeLength": 249, + "src": "this._pos || (h = document.documentElement.clientWidth , l = document.documentElement.clientHeight , c = document.documentElement.scrollLeft || document.body.scrollLeft , u = document.documentElement.scrollTop || document.body.scrollTop , this._pos = [h / 2 - 100 + c, l / 2 - 150 + u])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 89002, + "nodeLength": 61, + "src": "document.documentElement.scrollLeft || document.body.scrollLeft", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 89066, + "nodeLength": 59, + "src": "document.documentElement.scrollTop || document.body.scrollTop", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 89361, + "nodeLength": 32, + "src": "t.blockUI && t.blockUI(this.dpDiv)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 89513, + "nodeLength": 388, + "src": "s.hasClass(this.markerClassName) && (i = e.nodeName.toLowerCase() , t.removeData(e, \"datepicker\") , \"input\" === i ? (n.append.remove() , n.trigger.remove() , s.removeClass(this.markerClassName).off(\"focus\", this._showDatepicker).off(\"keydown\", this._doKeyDown).off(\"keypress\", this._doKeyPress).off(\"keyup\", this._doKeyUp)) : (\"div\" === i || \"span\" === i) && s.removeClass(this.markerClassName).empty() , m === n && (m = null))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 89604, + "nodeLength": 11, + "src": "\"input\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 89817, + "nodeLength": 67, + "src": "(\"div\" === i || \"span\" === i) && s.removeClass(this.markerClassName).empty()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 89817, + "nodeLength": 21, + "src": "\"div\" === i || \"span\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 89817, + "nodeLength": 9, + "src": "\"div\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 89828, + "nodeLength": 10, + "src": "\"span\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 89885, + "nodeLength": 15, + "src": "m === n && (m = null)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 89885, + "nodeLength": 5, + "src": "m === n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 89973, + "nodeLength": 478, + "src": "n.hasClass(this.markerClassName) && (i = e.nodeName.toLowerCase() , \"input\" === i ? (e.disabled = !1 , o.trigger.filter(\"button\").each(function() {\n this.disabled = !1;\n}).end().filter(\"img\").css({\n opacity: \"1.0\", \n cursor: \"\"})) : (\"div\" === i || \"span\" === i) && (s = n.children(\".\" + this._inlineClass) , s.children().removeClass(\"ui-state-disabled\") , s.find(\"select.ui-datepicker-month, select.ui-datepicker-year\").prop(\"disabled\", !1)) , this._disabledInputs = t.map(this._disabledInputs, function(t) {\n return t === e ? null : t;\n}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 90035, + "nodeLength": 11, + "src": "\"input\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 90177, + "nodeLength": 191, + "src": "(\"div\" === i || \"span\" === i) && (s = n.children(\".\" + this._inlineClass) , s.children().removeClass(\"ui-state-disabled\") , s.find(\"select.ui-datepicker-month, select.ui-datepicker-year\").prop(\"disabled\", !1))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 90177, + "nodeLength": 21, + "src": "\"div\" === i || \"span\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 90177, + "nodeLength": 9, + "src": "\"div\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 90188, + "nodeLength": 10, + "src": "\"span\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 90436, + "nodeLength": 5, + "src": "t === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 90524, + "nodeLength": 534, + "src": "n.hasClass(this.markerClassName) && (i = e.nodeName.toLowerCase() , \"input\" === i ? (e.disabled = !0 , o.trigger.filter(\"button\").each(function() {\n this.disabled = !0;\n}).end().filter(\"img\").css({\n opacity: \"0.5\", \n cursor: \"default\"})) : (\"div\" === i || \"span\" === i) && (s = n.children(\".\" + this._inlineClass) , s.children().addClass(\"ui-state-disabled\") , s.find(\"select.ui-datepicker-month, select.ui-datepicker-year\").prop(\"disabled\", !0)) , this._disabledInputs = t.map(this._disabledInputs, function(t) {\n return t === e ? null : t;\n}) , this._disabledInputs[this._disabledInputs.length] = e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 90586, + "nodeLength": 11, + "src": "\"input\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 90735, + "nodeLength": 188, + "src": "(\"div\" === i || \"span\" === i) && (s = n.children(\".\" + this._inlineClass) , s.children().addClass(\"ui-state-disabled\") , s.find(\"select.ui-datepicker-month, select.ui-datepicker-year\").prop(\"disabled\", !0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 90735, + "nodeLength": 21, + "src": "\"div\" === i || \"span\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 90735, + "nodeLength": 9, + "src": "\"div\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 90746, + "nodeLength": 10, + "src": "\"span\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 90991, + "nodeLength": 5, + "src": "t === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91097, + "nodeLength": 2, + "src": "!t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91121, + "nodeLength": 29, + "src": "this._disabledInputs.length > e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91158, + "nodeLength": 27, + "src": "this._disabledInputs[e] === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91392, + "nodeLength": 40, + "src": "2 === arguments.length && \"string\" == typeof i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91392, + "nodeLength": 20, + "src": "2 === arguments.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91414, + "nodeLength": 18, + "src": "\"string\" == typeof i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91433, + "nodeLength": 14, + "src": "\"defaults\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91484, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91486, + "nodeLength": 9, + "src": "\"all\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91543, + "nodeLength": 5, + "src": "i || {}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91549, + "nodeLength": 33, + "src": "\"string\" == typeof i && (n = {} , n[i] = s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91549, + "nodeLength": 18, + "src": "\"string\" == typeof i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91583, + "nodeLength": 544, + "src": "l && (this._curInst === l && this._hideDatepicker() , o = this._getDateDatepicker(e, !0) , r = this._getMinMaxDate(l, \"min\") , h = this._getMinMaxDate(l, \"max\") , a(l.settings, n) , null !== r && void 0 !== n.dateFormat && void 0 === n.minDate && (l.settings.minDate = this._formatDate(l, r)) , null !== h && void 0 !== n.dateFormat && void 0 === n.maxDate && (l.settings.maxDate = this._formatDate(l, h)) , \"disabled\" in n && (n.disabled ? this._disableDatepicker(e) : this._enableDatepicker(e)) , this._attachments(t(e), l) , this._autoSize(l) , this._setDate(l, o) , this._updateAlternate(l) , this._updateDatepicker(l))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91587, + "nodeLength": 41, + "src": "this._curInst === l && this._hideDatepicker()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91587, + "nodeLength": 17, + "src": "this._curInst === l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91739, + "nodeLength": 95, + "src": "null !== r && void 0 !== n.dateFormat && void 0 === n.minDate && (l.settings.minDate = this._formatDate(l, r))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91739, + "nodeLength": 8, + "src": "null !== r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91749, + "nodeLength": 85, + "src": "void 0 !== n.dateFormat && void 0 === n.minDate && (l.settings.minDate = this._formatDate(l, r))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91749, + "nodeLength": 21, + "src": "void 0 !== n.dateFormat", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91772, + "nodeLength": 62, + "src": "void 0 === n.minDate && (l.settings.minDate = this._formatDate(l, r))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91772, + "nodeLength": 18, + "src": "void 0 === n.minDate", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91835, + "nodeLength": 95, + "src": "null !== h && void 0 !== n.dateFormat && void 0 === n.maxDate && (l.settings.maxDate = this._formatDate(l, h))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91835, + "nodeLength": 8, + "src": "null !== h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91845, + "nodeLength": 85, + "src": "void 0 !== n.dateFormat && void 0 === n.maxDate && (l.settings.maxDate = this._formatDate(l, h))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91845, + "nodeLength": 21, + "src": "void 0 !== n.dateFormat", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91868, + "nodeLength": 62, + "src": "void 0 === n.maxDate && (l.settings.maxDate = this._formatDate(l, h))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91868, + "nodeLength": 18, + "src": "void 0 === n.maxDate", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91931, + "nodeLength": 81, + "src": "\"disabled\" in n && (n.disabled ? this._disableDatepicker(e) : this._enableDatepicker(e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91948, + "nodeLength": 10, + "src": "n.disabled", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 92256, + "nodeLength": 28, + "src": "e && this._updateDatepicker(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 92342, + "nodeLength": 74, + "src": "i && (this._setDate(i, e) , this._updateDatepicker(i) , this._updateAlternate(i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 92481, + "nodeLength": 41, + "src": "i && !i.inline && this._setDateFromField(i, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 92484, + "nodeLength": 38, + "src": "!i.inline && this._setDateFromField(i, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 92523, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 92658, + "nodeLength": 46, + "src": "o._keyEvent = !0 , t.datepicker._datepickerShowing", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 92873, + "nodeLength": 75, + "src": "n[0] && t.datepicker._selectDay(e.target, o.selectedMonth, o.selectedYear, n[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 92983, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93024, + "nodeLength": 7, + "src": "o.input", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93177, + "nodeLength": 9, + "src": "e.ctrlKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93313, + "nodeLength": 9, + "src": "e.ctrlKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93416, + "nodeLength": 56, + "src": "(e.ctrlKey || e.metaKey) && t.datepicker._clearDate(e.target)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93416, + "nodeLength": 20, + "src": "e.ctrlKey || e.metaKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93475, + "nodeLength": 20, + "src": "e.ctrlKey || e.metaKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93511, + "nodeLength": 56, + "src": "(e.ctrlKey || e.metaKey) && t.datepicker._gotoToday(e.target)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93511, + "nodeLength": 20, + "src": "e.ctrlKey || e.metaKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93570, + "nodeLength": 20, + "src": "e.ctrlKey || e.metaKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93606, + "nodeLength": 68, + "src": "(e.ctrlKey || e.metaKey) && t.datepicker._adjustDate(e.target, r ? 1 : -1, \"D\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93606, + "nodeLength": 20, + "src": "e.ctrlKey || e.metaKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93663, + "nodeLength": 1, + "src": "r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93677, + "nodeLength": 20, + "src": "e.ctrlKey || e.metaKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93698, + "nodeLength": 145, + "src": "e.originalEvent.altKey && t.datepicker._adjustDate(e.target, e.ctrlKey ? -t.datepicker._get(o, \"stepBigMonths\") : -t.datepicker._get(o, \"stepMonths\"), \"M\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93756, + "nodeLength": 9, + "src": "e.ctrlKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93859, + "nodeLength": 64, + "src": "(e.ctrlKey || e.metaKey) && t.datepicker._adjustDate(e.target, -7, \"D\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93859, + "nodeLength": 20, + "src": "e.ctrlKey || e.metaKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93926, + "nodeLength": 20, + "src": "e.ctrlKey || e.metaKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93962, + "nodeLength": 68, + "src": "(e.ctrlKey || e.metaKey) && t.datepicker._adjustDate(e.target, r ? -1 : 1, \"D\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93962, + "nodeLength": 20, + "src": "e.ctrlKey || e.metaKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94019, + "nodeLength": 1, + "src": "r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94033, + "nodeLength": 20, + "src": "e.ctrlKey || e.metaKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94054, + "nodeLength": 145, + "src": "e.originalEvent.altKey && t.datepicker._adjustDate(e.target, e.ctrlKey ? +t.datepicker._get(o, \"stepBigMonths\") : +t.datepicker._get(o, \"stepMonths\"), \"M\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94112, + "nodeLength": 9, + "src": "e.ctrlKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94215, + "nodeLength": 63, + "src": "(e.ctrlKey || e.metaKey) && t.datepicker._adjustDate(e.target, 7, \"D\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94215, + "nodeLength": 20, + "src": "e.ctrlKey || e.metaKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94281, + "nodeLength": 20, + "src": "e.ctrlKey || e.metaKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94326, + "nodeLength": 25, + "src": "36 === e.keyCode && e.ctrlKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94326, + "nodeLength": 14, + "src": "36 === e.keyCode", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94392, + "nodeLength": 43, + "src": "a && (e.preventDefault() , e.stopPropagation())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94510, + "nodeLength": 37, + "src": "t.datepicker._get(n, \"constrainInput\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94636, + "nodeLength": 16, + "src": "null == e.charCode", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94675, + "nodeLength": 48, + "src": "e.ctrlKey || e.metaKey || \" \" > s || !i || i.indexOf(s) > -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94686, + "nodeLength": 37, + "src": "e.metaKey || \" \" > s || !i || i.indexOf(s) > -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94697, + "nodeLength": 26, + "src": "\" \" > s || !i || i.indexOf(s) > -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94697, + "nodeLength": 5, + "src": "\" \" > s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94704, + "nodeLength": 19, + "src": "!i || i.indexOf(s) > -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94708, + "nodeLength": 15, + "src": "i.indexOf(s) > -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94797, + "nodeLength": 25, + "src": "s.input.val() !== s.lastVal", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94886, + "nodeLength": 7, + "src": "s.input", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94947, + "nodeLength": 105, + "src": "i && (t.datepicker._setDateFromField(s) , t.datepicker._updateAlternate(s) , t.datepicker._updateDatepicker(s))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95104, + "nodeLength": 148, + "src": "e = e.target || e , \"input\" !== e.nodeName.toLowerCase() && (e = t(\"input\", e.parentNode)[0]) , !t.datepicker._isDisabledDatepicker(e) && t.datepicker._lastInput !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95106, + "nodeLength": 11, + "src": "e.target || e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95118, + "nodeLength": 66, + "src": "\"input\" !== e.nodeName.toLowerCase() && (e = t(\"input\", e.parentNode)[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95118, + "nodeLength": 34, + "src": "\"input\" !== e.nodeName.toLowerCase()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95185, + "nodeLength": 67, + "src": "!t.datepicker._isDisabledDatepicker(e) && t.datepicker._lastInput !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95225, + "nodeLength": 27, + "src": "t.datepicker._lastInput !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95299, + "nodeLength": 188, + "src": "t.datepicker._curInst && t.datepicker._curInst !== s && (t.datepicker._curInst.dpDiv.stop(!0, !0) , s && t.datepicker._datepickerShowing && t.datepicker._hideDatepicker(t.datepicker._curInst.input[0]))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95322, + "nodeLength": 165, + "src": "t.datepicker._curInst !== s && (t.datepicker._curInst.dpDiv.stop(!0, !0) , s && t.datepicker._datepickerShowing && t.datepicker._hideDatepicker(t.datepicker._curInst.input[0]))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95322, + "nodeLength": 25, + "src": "t.datepicker._curInst !== s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95390, + "nodeLength": 96, + "src": "s && t.datepicker._datepickerShowing && t.datepicker._hideDatepicker(t.datepicker._curInst.input[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95393, + "nodeLength": 93, + "src": "t.datepicker._datepickerShowing && t.datepicker._hideDatepicker(t.datepicker._curInst.input[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95526, + "nodeLength": 1, + "src": "n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95548, + "nodeLength": 1031, + "src": "o !== !1 && (a(s.settings, o) , s.lastVal = null , t.datepicker._lastInput = e , t.datepicker._setDateFromField(s) , t.datepicker._inDialog && (e.value = \"\") , t.datepicker._pos || (t.datepicker._pos = t.datepicker._findPos(e) , t.datepicker._pos[1] += e.offsetHeight) , r = !1 , t(e).parents().each(function() {\n return r |= \"fixed\" === t(this).css(\"position\") , !r;\n}) , h = {\n left: t.datepicker._pos[0], \n top: t.datepicker._pos[1]} , t.datepicker._pos = null , s.dpDiv.empty() , s.dpDiv.css({\n position: \"absolute\", \n display: \"block\", \n top: \"-1000px\"}) , t.datepicker._updateDatepicker(s) , h = t.datepicker._checkOffset(s, h, r) , s.dpDiv.css({\n position: t.datepicker._inDialog && t.blockUI ? \"static\" : r ? \"fixed\" : \"absolute\", \n display: \"none\", \n left: h.left + \"px\", \n top: h.top + \"px\"}) , s.inline || (l = t.datepicker._get(s, \"showAnim\") , c = t.datepicker._get(s, \"duration\") , s.dpDiv.css(\"z-index\", i(t(e)) + 1) , t.datepicker._datepickerShowing = !0 , t.effects && t.effects.effect[l] ? s.dpDiv.show(l, t.datepicker._get(s, \"showOptions\"), c) : s.dpDiv[l || \"show\"](l ? c : null) , t.datepicker._shouldFocusInput(s) && s.input.trigger(\"focus\") , t.datepicker._curInst = s))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95548, + "nodeLength": 6, + "src": "o !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95648, + "nodeLength": 36, + "src": "t.datepicker._inDialog && (e.value = \"\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95685, + "nodeLength": 100, + "src": "t.datepicker._pos || (t.datepicker._pos = t.datepicker._findPos(e) , t.datepicker._pos[1] += e.offsetHeight)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95832, + "nodeLength": 33, + "src": "\"fixed\" === t(this).css(\"position\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 96121, + "nodeLength": 33, + "src": "t.datepicker._inDialog && t.blockUI", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 96164, + "nodeLength": 1, + "src": "r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 96234, + "nodeLength": 344, + "src": "s.inline || (l = t.datepicker._get(s, \"showAnim\") , c = t.datepicker._get(s, \"duration\") , s.dpDiv.css(\"z-index\", i(t(e)) + 1) , t.datepicker._datepickerShowing = !0 , t.effects && t.effects.effect[l] ? s.dpDiv.show(l, t.datepicker._get(s, \"showOptions\"), c) : s.dpDiv[l || \"show\"](l ? c : null) , t.datepicker._shouldFocusInput(s) && s.input.trigger(\"focus\") , t.datepicker._curInst = s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 96381, + "nodeLength": 30, + "src": "t.effects && t.effects.effect[l]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 96473, + "nodeLength": 9, + "src": "l || \"show\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 96484, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 96494, + "nodeLength": 59, + "src": "t.datepicker._shouldFocusInput(s) && s.input.trigger(\"focus\")", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "9": [ + null, + { + "position": 11313, + "nodeLength": 29, + "src": "r.length > 0 && o.apply(r.get(0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11313, + "nodeLength": 10, + "src": "r.length > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11442, + "nodeLength": 69, + "src": "n > 1 && e.dpDiv.addClass(\"ui-datepicker-multi-\" + n).css(\"width\", a * n + \"em\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11442, + "nodeLength": 3, + "src": "n > 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11521, + "nodeLength": 18, + "src": "1 !== s[0] || 1 !== s[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11521, + "nodeLength": 8, + "src": "1 !== s[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11531, + "nodeLength": 8, + "src": "1 !== s[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11597, + "nodeLength": 20, + "src": "this._get(e, \"isRTL\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11664, + "nodeLength": 119, + "src": "e === t.datepicker._curInst && t.datepicker._datepickerShowing && t.datepicker._shouldFocusInput(e) && e.input.trigger(\"focus\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11664, + "nodeLength": 25, + "src": "e === t.datepicker._curInst", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11691, + "nodeLength": 92, + "src": "t.datepicker._datepickerShowing && t.datepicker._shouldFocusInput(e) && e.input.trigger(\"focus\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11724, + "nodeLength": 59, + "src": "t.datepicker._shouldFocusInput(e) && e.input.trigger(\"focus\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11784, + "nodeLength": 176, + "src": "e.yearshtml && (i = e.yearshtml , setTimeout(function() {\n i === e.yearshtml && e.yearshtml && e.dpDiv.find(\"select.ui-datepicker-year:first\").replaceWith(e.yearshtml) , i = e.yearshtml = null;\n}, 0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11834, + "nodeLength": 102, + "src": "i === e.yearshtml && e.yearshtml && e.dpDiv.find(\"select.ui-datepicker-year:first\").replaceWith(e.yearshtml)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11834, + "nodeLength": 15, + "src": "i === e.yearshtml", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11851, + "nodeLength": 85, + "src": "e.yearshtml && e.dpDiv.find(\"select.ui-datepicker-year:first\").replaceWith(e.yearshtml)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11999, + "nodeLength": 80, + "src": "t.input && t.input.is(\":visible\") && !t.input.is(\":disabled\") && !t.input.is(\":focus\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12008, + "nodeLength": 71, + "src": "t.input.is(\":visible\") && !t.input.is(\":disabled\") && !t.input.is(\":focus\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12032, + "nodeLength": 47, + "src": "!t.input.is(\":disabled\") && !t.input.is(\":focus\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12163, + "nodeLength": 7, + "src": "e.input", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12196, + "nodeLength": 7, + "src": "e.input", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12268, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12339, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12383, + "nodeLength": 20, + "src": "this._get(e, \"isRTL\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12418, + "nodeLength": 33, + "src": "s && i.left === e.input.offset().left", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12421, + "nodeLength": 30, + "src": "i.left === e.input.offset().left", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12486, + "nodeLength": 33, + "src": "s && i.top === e.input.offset().top + r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12489, + "nodeLength": 30, + "src": "i.top === e.input.offset().top + r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12570, + "nodeLength": 15, + "src": "i.left + n > h && h > n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12570, + "nodeLength": 10, + "src": "i.left + n > h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12582, + "nodeLength": 3, + "src": "h > n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12632, + "nodeLength": 14, + "src": "i.top + o > l && l > o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12632, + "nodeLength": 9, + "src": "i.top + o > l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12643, + "nodeLength": 3, + "src": "l > o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12740, + "nodeLength": 64, + "src": "e && (\"hidden\" === e.type || 1 !== e.nodeType || t.expr.filters.hidden(e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12744, + "nodeLength": 59, + "src": "\"hidden\" === e.type || 1 !== e.nodeType || t.expr.filters.hidden(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12744, + "nodeLength": 17, + "src": "\"hidden\" === e.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12763, + "nodeLength": 40, + "src": "1 !== e.nodeType || t.expr.filters.hidden(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12763, + "nodeLength": 14, + "src": "1 !== e.nodeType", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12810, + "nodeLength": 1, + "src": "n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12940, + "nodeLength": 638, + "src": "!a || e && a !== t.data(e, \"datepicker\") || this._datepickerShowing && (i = this._get(a, \"showAnim\") , s = this._get(a, \"duration\") , n = function() {\n t.datepicker._tidyDialog(a);\n} , t.effects && (t.effects.effect[i] || t.effects[i]) ? a.dpDiv.hide(i, t.datepicker._get(a, \"showOptions\"), s, n) : a.dpDiv[\"slideDown\" === i ? \"slideUp\" : \"fadeIn\" === i ? \"fadeOut\" : \"hide\"](i ? s : null, n) , i || n() , this._datepickerShowing = !1 , o = this._get(a, \"onClose\") , o && o.apply(a.input ? a.input[0] : null, [a.input ? a.input.val() : \"\", a]) , this._lastInput = null , this._inDialog && (this._dialogInput.css({\n position: \"absolute\", \n left: \"0\", \n top: \"-100px\"}) , t.blockUI && (t.unblockUI() , t(\"body\").append(this.dpDiv))) , this._inDialog = !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12944, + "nodeLength": 634, + "src": "e && a !== t.data(e, \"datepicker\") || this._datepickerShowing && (i = this._get(a, \"showAnim\") , s = this._get(a, \"duration\") , n = function() {\n t.datepicker._tidyDialog(a);\n} , t.effects && (t.effects.effect[i] || t.effects[i]) ? a.dpDiv.hide(i, t.datepicker._get(a, \"showOptions\"), s, n) : a.dpDiv[\"slideDown\" === i ? \"slideUp\" : \"fadeIn\" === i ? \"fadeOut\" : \"hide\"](i ? s : null, n) , i || n() , this._datepickerShowing = !1 , o = this._get(a, \"onClose\") , o && o.apply(a.input ? a.input[0] : null, [a.input ? a.input.val() : \"\", a]) , this._lastInput = null , this._inDialog && (this._dialogInput.css({\n position: \"absolute\", \n left: \"0\", \n top: \"-100px\"}) , t.blockUI && (t.unblockUI() , t(\"body\").append(this.dpDiv))) , this._inDialog = !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12944, + "nodeLength": 29, + "src": "e && a !== t.data(e, \"datepicker\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12947, + "nodeLength": 26, + "src": "a !== t.data(e, \"datepicker\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12975, + "nodeLength": 603, + "src": "this._datepickerShowing && (i = this._get(a, \"showAnim\") , s = this._get(a, \"duration\") , n = function() {\n t.datepicker._tidyDialog(a);\n} , t.effects && (t.effects.effect[i] || t.effects[i]) ? a.dpDiv.hide(i, t.datepicker._get(a, \"showOptions\"), s, n) : a.dpDiv[\"slideDown\" === i ? \"slideUp\" : \"fadeIn\" === i ? \"fadeOut\" : \"hide\"](i ? s : null, n) , i || n() , this._datepickerShowing = !1 , o = this._get(a, \"onClose\") , o && o.apply(a.input ? a.input[0] : null, [a.input ? a.input.val() : \"\", a]) , this._lastInput = null , this._inDialog && (this._dialogInput.css({\n position: \"absolute\", \n left: \"0\", \n top: \"-100px\"}) , t.blockUI && (t.unblockUI() , t(\"body\").append(this.dpDiv))) , this._inDialog = !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13095, + "nodeLength": 46, + "src": "t.effects && (t.effects.effect[i] || t.effects[i])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13107, + "nodeLength": 33, + "src": "t.effects.effect[i] || t.effects[i]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13205, + "nodeLength": 15, + "src": "\"slideDown\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13231, + "nodeLength": 12, + "src": "\"fadeIn\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13262, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13274, + "nodeLength": 6, + "src": "i || n()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13333, + "nodeLength": 64, + "src": "o && o.apply(a.input ? a.input[0] : null, [a.input ? a.input.val() : \"\", a])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13344, + "nodeLength": 7, + "src": "a.input", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13369, + "nodeLength": 7, + "src": "a.input", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13419, + "nodeLength": 140, + "src": "this._inDialog && (this._dialogInput.css({\n position: \"absolute\", \n left: \"0\", \n top: \"-100px\"}) , t.blockUI && (t.unblockUI() , t(\"body\").append(this.dpDiv)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13503, + "nodeLength": 55, + "src": "t.blockUI && (t.unblockUI() , t(\"body\").append(this.dpDiv))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13710, + "nodeLength": 21, + "src": "t.datepicker._curInst", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13782, + "nodeLength": 352, + "src": "(i[0].id !== t.datepicker._mainDivId && 0 === i.parents(\"#\" + t.datepicker._mainDivId).length && !i.hasClass(t.datepicker.markerClassName) && !i.closest(\".\" + t.datepicker._triggerClass).length && t.datepicker._datepickerShowing && (!t.datepicker._inDialog || !t.blockUI) || i.hasClass(t.datepicker.markerClassName) && t.datepicker._curInst !== s) && t.datepicker._hideDatepicker()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13782, + "nodeLength": 319, + "src": "i[0].id !== t.datepicker._mainDivId && 0 === i.parents(\"#\" + t.datepicker._mainDivId).length && !i.hasClass(t.datepicker.markerClassName) && !i.closest(\".\" + t.datepicker._triggerClass).length && t.datepicker._datepickerShowing && (!t.datepicker._inDialog || !t.blockUI) || i.hasClass(t.datepicker.markerClassName) && t.datepicker._curInst !== s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13782, + "nodeLength": 250, + "src": "i[0].id !== t.datepicker._mainDivId && 0 === i.parents(\"#\" + t.datepicker._mainDivId).length && !i.hasClass(t.datepicker.markerClassName) && !i.closest(\".\" + t.datepicker._triggerClass).length && t.datepicker._datepickerShowing && (!t.datepicker._inDialog || !t.blockUI)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13782, + "nodeLength": 33, + "src": "i[0].id !== t.datepicker._mainDivId", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13817, + "nodeLength": 215, + "src": "0 === i.parents(\"#\" + t.datepicker._mainDivId).length && !i.hasClass(t.datepicker.markerClassName) && !i.closest(\".\" + t.datepicker._triggerClass).length && t.datepicker._datepickerShowing && (!t.datepicker._inDialog || !t.blockUI)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13817, + "nodeLength": 49, + "src": "0 === i.parents(\"#\" + t.datepicker._mainDivId).length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13868, + "nodeLength": 164, + "src": "!i.hasClass(t.datepicker.markerClassName) && !i.closest(\".\" + t.datepicker._triggerClass).length && t.datepicker._datepickerShowing && (!t.datepicker._inDialog || !t.blockUI)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13911, + "nodeLength": 121, + "src": "!i.closest(\".\" + t.datepicker._triggerClass).length && t.datepicker._datepickerShowing && (!t.datepicker._inDialog || !t.blockUI)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13962, + "nodeLength": 70, + "src": "t.datepicker._datepickerShowing && (!t.datepicker._inDialog || !t.blockUI)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13996, + "nodeLength": 35, + "src": "!t.datepicker._inDialog || !t.blockUI", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14034, + "nodeLength": 67, + "src": "i.hasClass(t.datepicker.markerClassName) && t.datepicker._curInst !== s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14076, + "nodeLength": 25, + "src": "t.datepicker._curInst !== s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14198, + "nodeLength": 133, + "src": "this._isDisabledDatepicker(n[0]) || (this._adjustInstDate(o, i + (\"M\" === s ? this._get(o, \"showCurrentAtPos\") : 0), s) , this._updateDatepicker(o))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14259, + "nodeLength": 7, + "src": "\"M\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14391, + "nodeLength": 40, + "src": "this._get(n, \"gotoCurrent\") && n.currentDay", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14789, + "nodeLength": 7, + "src": "\"M\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14824, + "nodeLength": 7, + "src": "\"M\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14980, + "nodeLength": 286, + "src": "t(n).hasClass(this._unselectableClass) || this._isDisabledDatepicker(a[0]) || (o = this._getInst(a[0]) , o.selectedDay = o.currentDay = t(\"a\", n).html() , o.selectedMonth = o.currentMonth = i , o.selectedYear = o.currentYear = s , this._selectDate(e, this._formatDate(o, o.currentDay, o.currentMonth, o.currentYear)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15020, + "nodeLength": 246, + "src": "this._isDisabledDatepicker(a[0]) || (o = this._getInst(a[0]) , o.selectedDay = o.currentDay = t(\"a\", n).html() , o.selectedMonth = o.currentMonth = i , o.selectedYear = o.currentYear = s , this._selectDate(e, this._formatDate(o, o.currentDay, o.currentMonth, o.currentYear)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15389, + "nodeLength": 7, + "src": "null != i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15419, + "nodeLength": 23, + "src": "o.input && o.input.val(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15494, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15504, + "nodeLength": 7, + "src": "o.input", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15535, + "nodeLength": 34, + "src": "o.input && o.input.trigger(\"change\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15570, + "nodeLength": 8, + "src": "o.inline", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15656, + "nodeLength": 53, + "src": "\"object\" != typeof o.input[0] && o.input.trigger(\"focus\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15656, + "nodeLength": 27, + "src": "\"object\" != typeof o.input[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15798, + "nodeLength": 137, + "src": "o && (i = this._get(e, \"altFormat\") || this._get(e, \"dateFormat\") , s = this._getDate(e) , n = this.formatDate(i, s, this._getFormatConfig(e)) , t(o).val(n))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15804, + "nodeLength": 51, + "src": "this._get(e, \"altFormat\") || this._get(e, \"dateFormat\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15984, + "nodeLength": 8, + "src": "e > 0 && 6 > e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7, + "nodeLength": 3, + "src": "e > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4, + "nodeLength": 3, + "src": "6 > e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16084, + "nodeLength": 13, + "src": "i.getDay() || 7", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16211, + "nodeLength": 16, + "src": "null == e || null == i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16211, + "nodeLength": 7, + "src": "null == e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16220, + "nodeLength": 7, + "src": "null == i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16256, + "nodeLength": 37, + "src": "i = \"object\" == typeof i ? \"\" + i : i + \"\" , \"\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16258, + "nodeLength": 18, + "src": "\"object\" == typeof i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16287, + "nodeLength": 6, + "src": "\"\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16325, + "nodeLength": 57, + "src": "(s ? s.shortYearCutoff : null) || this._defaults.shortYearCutoff", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16325, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16385, + "nodeLength": 18, + "src": "\"string\" != typeof l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16453, + "nodeLength": 53, + "src": "(s ? s.dayNamesShort : null) || this._defaults.dayNamesShort", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16453, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16510, + "nodeLength": 43, + "src": "(s ? s.dayNames : null) || this._defaults.dayNames", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16510, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16557, + "nodeLength": 57, + "src": "(s ? s.monthNamesShort : null) || this._defaults.monthNamesShort", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16557, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16618, + "nodeLength": 47, + "src": "(s ? s.monthNames : null) || this._defaults.monthNames", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16618, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16711, + "nodeLength": 31, + "src": "e.length > n + 1 && e.charAt(n + 1) === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16711, + "nodeLength": 12, + "src": "e.length > n + 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16725, + "nodeLength": 17, + "src": "e.charAt(n + 1) === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16750, + "nodeLength": 6, + "src": "i && n++", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16787, + "nodeLength": 7, + "src": "\"@\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16798, + "nodeLength": 7, + "src": "\"!\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16809, + "nodeLength": 10, + "src": "\"y\" === t && e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16809, + "nodeLength": 7, + "src": "\"y\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16822, + "nodeLength": 7, + "src": "\"o\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16836, + "nodeLength": 7, + "src": "\"y\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16907, + "nodeLength": 2, + "src": "!a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17023, + "nodeLength": 4, + "src": "y(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17119, + "nodeLength": 133, + "src": "t.each(a, function(t, e) {\n var s = e[1];\n return i.substr(h, s.length).toLowerCase() === s.toLowerCase() ? (o = e[0] , h += s.length , !1) : void 0;\n}) , -1 !== o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17160, + "nodeLength": 52, + "src": "i.substr(h, s.length).toLowerCase() === s.toLowerCase()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17246, + "nodeLength": 6, + "src": "-1 !== o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17316, + "nodeLength": 25, + "src": "i.charAt(h) !== e.charAt(n)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17396, + "nodeLength": 10, + "src": "e.length > n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17414, + "nodeLength": 1, + "src": "b", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17416, + "nodeLength": 25, + "src": "\"'\" !== e.charAt(n) || y(\"'\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17416, + "nodeLength": 17, + "src": "\"'\" !== e.charAt(n)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17816, + "nodeLength": 6, + "src": "y(\"'\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17853, + "nodeLength": 43, + "src": "i.length > h && (a = i.substr(h) , !/^\\s+/.test(a))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17853, + "nodeLength": 10, + "src": "i.length > h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17951, + "nodeLength": 118, + "src": "-1 === g ? g = (new Date()).getFullYear() : 100 > g && (g += (new Date()).getFullYear() - (new Date()).getFullYear() % 100 + (c >= g ? 0 : -100)) , v > -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17951, + "nodeLength": 6, + "src": "-1 === g", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17985, + "nodeLength": 79, + "src": "100 > g && (g += (new Date()).getFullYear() - (new Date()).getFullYear() % 100 + (c >= g ? 0 : -100))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17985, + "nodeLength": 5, + "src": "100 > g", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18051, + "nodeLength": 4, + "src": "c >= g", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18065, + "nodeLength": 4, + "src": "v > -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18088, + "nodeLength": 34, + "src": "o = this._getDaysInMonth(g, m - 1) , o >= _", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18118, + "nodeLength": 4, + "src": "o >= _", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18141, + "nodeLength": 104, + "src": "r = this._daylightSavingAdjust(new Date(g, m - 1, _)) , r.getFullYear() !== g || r.getMonth() + 1 !== m || r.getDate() !== _", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18189, + "nodeLength": 56, + "src": "r.getFullYear() !== g || r.getMonth() + 1 !== m || r.getDate() !== _", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18189, + "nodeLength": 19, + "src": "r.getFullYear() !== g", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18210, + "nodeLength": 35, + "src": "r.getMonth() + 1 !== m || r.getDate() !== _", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18210, + "nodeLength": 18, + "src": "r.getMonth() + 1 !== m", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18230, + "nodeLength": 15, + "src": "r.getDate() !== _", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18606, + "nodeLength": 2, + "src": "!e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18627, + "nodeLength": 53, + "src": "(i ? i.dayNamesShort : null) || this._defaults.dayNamesShort", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18627, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18684, + "nodeLength": 43, + "src": "(i ? i.dayNames : null) || this._defaults.dayNames", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18684, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18731, + "nodeLength": 57, + "src": "(i ? i.monthNamesShort : null) || this._defaults.monthNamesShort", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18731, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18792, + "nodeLength": 47, + "src": "(i ? i.monthNames : null) || this._defaults.monthNames", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18792, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18860, + "nodeLength": 31, + "src": "t.length > s + 1 && t.charAt(s + 1) === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18860, + "nodeLength": 12, + "src": "t.length > s + 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18874, + "nodeLength": 17, + "src": "t.charAt(s + 1) === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18899, + "nodeLength": 6, + "src": "i && s++", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18941, + "nodeLength": 4, + "src": "h(t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18951, + "nodeLength": 10, + "src": "i > s.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19008, + "nodeLength": 4, + "src": "h(t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19037, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19047, + "nodeLength": 10, + "src": "t.length > s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19065, + "nodeLength": 1, + "src": "d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19067, + "nodeLength": 25, + "src": "\"'\" !== t.charAt(s) || h(\"'\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19067, + "nodeLength": 17, + "src": "\"'\" !== t.charAt(s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19455, + "nodeLength": 6, + "src": "h(\"y\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19479, + "nodeLength": 22, + "src": "10 > e.getFullYear() % 100", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19624, + "nodeLength": 6, + "src": "h(\"'\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19745, + "nodeLength": 31, + "src": "t.length > e + 1 && t.charAt(e + 1) === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19745, + "nodeLength": 12, + "src": "t.length > e + 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19759, + "nodeLength": 17, + "src": "t.charAt(e + 1) === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19784, + "nodeLength": 6, + "src": "s && e++", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19802, + "nodeLength": 10, + "src": "t.length > e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19820, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19822, + "nodeLength": 25, + "src": "\"'\" !== t.charAt(e) || n(\"'\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19822, + "nodeLength": 17, + "src": "\"'\" !== t.charAt(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19983, + "nodeLength": 6, + "src": "n(\"'\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20067, + "nodeLength": 22, + "src": "void 0 !== t.settings[e]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20158, + "nodeLength": 25, + "src": "t.input.val() !== t.lastVal", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20229, + "nodeLength": 7, + "src": "t.input", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20319, + "nodeLength": 24, + "src": "this.parseDate(i, s, a) || n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20355, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20484, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20515, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20546, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20949, + "nodeLength": 68, + "src": "(i.toLowerCase().match(/^c/) ? t.datepicker._getDate(e) : null) || new Date()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20949, + "nodeLength": 27, + "src": "i.toLowerCase().match(/^c/)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21118, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21129, + "nodeLength": 9, + "src": "l[2] || \"d\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21442, + "nodeLength": 15, + "src": "null == i || \"\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21442, + "nodeLength": 7, + "src": "null == i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21451, + "nodeLength": 6, + "src": "\"\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21460, + "nodeLength": 18, + "src": "\"string\" == typeof i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21484, + "nodeLength": 18, + "src": "\"number\" == typeof i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21503, + "nodeLength": 8, + "src": "isNaN(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21550, + "nodeLength": 23, + "src": "a && \"Invalid Date\" == \"\" + a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21553, + "nodeLength": 20, + "src": "\"Invalid Date\" == \"\" + a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21578, + "nodeLength": 71, + "src": "a && (a.setHours(0) , a.setMinutes(0) , a.setSeconds(0) , a.setMilliseconds(0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21722, + "nodeLength": 1, + "src": "t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21736, + "nodeLength": 15, + "src": "t.getHours() > 12", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22059, + "nodeLength": 65, + "src": "n === t.selectedMonth && o === t.selectedYear || i || this._notifyChange(t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22059, + "nodeLength": 39, + "src": "n === t.selectedMonth && o === t.selectedYear", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22059, + "nodeLength": 19, + "src": "n === t.selectedMonth", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22080, + "nodeLength": 18, + "src": "o === t.selectedYear", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22100, + "nodeLength": 24, + "src": "i || this._notifyChange(t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22149, + "nodeLength": 46, + "src": "t.input && t.input.val(s ? \"\" : this._formatDate(t))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22170, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22224, + "nodeLength": 43, + "src": "!t.currentYear || t.input && \"\" === t.input.val()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22240, + "nodeLength": 27, + "src": "t.input && \"\" === t.input.val()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22249, + "nodeLength": 18, + "src": "\"\" === t.input.val()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23494, + "nodeLength": 18, + "src": "1 !== U[0] || 1 !== U[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23494, + "nodeLength": 8, + "src": "1 !== U[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23504, + "nodeLength": 8, + "src": "1 !== U[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23542, + "nodeLength": 12, + "src": "t.currentDay", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23722, + "nodeLength": 19, + "src": "0 > Z && (Z += 12 , te--) , J", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23722, + "nodeLength": 17, + "src": "0 > Z && (Z += 12 , te--)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23722, + "nodeLength": 3, + "src": "0 > Z", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23841, + "nodeLength": 6, + "src": "Q && Q > e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23844, + "nodeLength": 3, + "src": "Q > e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23852, + "nodeLength": 46, + "src": "this._daylightSavingAdjust(new Date(te, Z, 1)) > e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23904, + "nodeLength": 16, + "src": "0 > Z && (Z = 11 , te--)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23904, + "nodeLength": 3, + "src": "0 > Z", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23981, + "nodeLength": 1, + "src": "K", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24078, + "nodeLength": 31, + "src": "this._canAdjustMonth(t, -1, te, Z)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24256, + "nodeLength": 1, + "src": "Y", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24288, + "nodeLength": 1, + "src": "q", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24418, + "nodeLength": 1, + "src": "Y", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24478, + "nodeLength": 1, + "src": "K", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24575, + "nodeLength": 30, + "src": "this._canAdjustMonth(t, 1, te, Z)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24752, + "nodeLength": 1, + "src": "Y", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24784, + "nodeLength": 1, + "src": "q", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24914, + "nodeLength": 1, + "src": "Y", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24977, + "nodeLength": 40, + "src": "this._get(t, \"gotoCurrent\") && t.currentDay", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25024, + "nodeLength": 1, + "src": "K", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25076, + "nodeLength": 8, + "src": "t.inline", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25270, + "nodeLength": 1, + "src": "j", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25332, + "nodeLength": 1, + "src": "Y", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25341, + "nodeLength": 20, + "src": "this._isInRange(t, r)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25529, + "nodeLength": 1, + "src": "Y", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25590, + "nodeLength": 8, + "src": "isNaN(c)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25879, + "nodeLength": 6, + "src": "U[0] > k", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25919, + "nodeLength": 6, + "src": "U[1] > C", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25934, + "nodeLength": 84, + "src": "D = this._daylightSavingAdjust(new Date(te, Z, t.selectedDay)) , I = \" ui-corner-all\" , T = \"\" , X", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26023, + "nodeLength": 43, + "src": "T += \"
\" + r[c] + \"\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28884, + "nodeLength": 19, + "src": "!h || c >= s.getMonth()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28888, + "nodeLength": 15, + "src": "c >= s.getMonth()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28907, + "nodeLength": 105, + "src": "(!l || n.getMonth() >= c) && (y += \"\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28907, + "nodeLength": 19, + "src": "!l || n.getMonth() >= c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28911, + "nodeLength": 15, + "src": "n.getMonth() >= c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28958, + "nodeLength": 5, + "src": "c === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29031, + "nodeLength": 45, + "src": "v || (b += y + (!o && m && _ ? \"\" : \" \")) , !t.yearshtml", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29031, + "nodeLength": 32, + "src": "v || (b += y + (!o && m && _ ? \"\" : \" \"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29041, + "nodeLength": 8, + "src": "!o && m && _", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29045, + "nodeLength": 4, + "src": "m && _", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29080, + "nodeLength": 20, + "src": "t.yearshtml = \"\" , o || !_", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29095, + "nodeLength": 5, + "src": "o || !_", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29246, + "nodeLength": 19, + "src": "t.match(/c[+\\-].*/)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29296, + "nodeLength": 18, + "src": "t.match(/[+\\-].*/)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29354, + "nodeLength": 8, + "src": "isNaN(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29393, + "nodeLength": 8, + "src": "u[1] || \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29406, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29440, + "nodeLength": 1, + "src": "n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29569, + "nodeLength": 4, + "src": "g >= f", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29616, + "nodeLength": 5, + "src": "f === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29760, + "nodeLength": 32, + "src": "v && (b += (!o && m && _ ? \"\" : \" \") + y)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29768, + "nodeLength": 8, + "src": "!o && m && _", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29772, + "nodeLength": 4, + "src": "m && _", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29860, + "nodeLength": 7, + "src": "\"Y\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29892, + "nodeLength": 7, + "src": "\"M\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29958, + "nodeLength": 7, + "src": "\"D\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30151, + "nodeLength": 40, + "src": "(\"M\" === i || \"Y\" === i) && this._notifyChange(t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30151, + "nodeLength": 16, + "src": "\"M\" === i || \"Y\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30151, + "nodeLength": 7, + "src": "\"M\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30160, + "nodeLength": 7, + "src": "\"Y\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30291, + "nodeLength": 6, + "src": "i && i > e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30294, + "nodeLength": 3, + "src": "i > e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30309, + "nodeLength": 6, + "src": "s && n > s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30312, + "nodeLength": 3, + "src": "n > s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30386, + "nodeLength": 72, + "src": "e && e.apply(t.input ? t.input[0] : null, [t.selectedYear, t.selectedMonth + 1, t])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30397, + "nodeLength": 7, + "src": "t.input", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30534, + "nodeLength": 7, + "src": "null == e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30548, + "nodeLength": 18, + "src": "\"number\" == typeof e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30936, + "nodeLength": 3, + "src": "0 > e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30964, + "nodeLength": 66, + "src": "0 > e && o.setDate(this._getDaysInMonth(o.getFullYear(), o.getMonth()))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30964, + "nodeLength": 3, + "src": "0 > e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31196, + "nodeLength": 146, + "src": "h && (i = h.split(\":\") , s = (new Date()).getFullYear() , a = parseInt(i[0], 10) , r = parseInt(i[1], 10) , i[0].match(/[+\\-].*/) && (a += s) , i[1].match(/[+\\-].*/) && (r += s))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31282, + "nodeLength": 29, + "src": "i[0].match(/[+\\-].*/) && (a += s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31312, + "nodeLength": 29, + "src": "i[1].match(/[+\\-].*/) && (r += s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31344, + "nodeLength": 113, + "src": "(!n || e.getTime() >= n.getTime()) && (!o || e.getTime() <= o.getTime()) && (!a || e.getFullYear() >= a) && (!r || r >= e.getFullYear())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31344, + "nodeLength": 28, + "src": "!n || e.getTime() >= n.getTime()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31348, + "nodeLength": 24, + "src": "e.getTime() >= n.getTime()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31376, + "nodeLength": 81, + "src": "(!o || e.getTime() <= o.getTime()) && (!a || e.getFullYear() >= a) && (!r || r >= e.getFullYear())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31376, + "nodeLength": 28, + "src": "!o || e.getTime() <= o.getTime()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31380, + "nodeLength": 24, + "src": "e.getTime() <= o.getTime()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31408, + "nodeLength": 49, + "src": "(!a || e.getFullYear() >= a) && (!r || r >= e.getFullYear())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31408, + "nodeLength": 22, + "src": "!a || e.getFullYear() >= a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31412, + "nodeLength": 18, + "src": "e.getFullYear() >= a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31434, + "nodeLength": 22, + "src": "!r || r >= e.getFullYear()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31438, + "nodeLength": 18, + "src": "r >= e.getFullYear()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31534, + "nodeLength": 18, + "src": "\"string\" != typeof e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31810, + "nodeLength": 91, + "src": "e || (t.currentDay = t.selectedDay , t.currentMonth = t.selectedMonth , t.currentYear = t.selectedYear)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31908, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31910, + "nodeLength": 18, + "src": "\"object\" == typeof e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32187, + "nodeLength": 12, + "src": "!this.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32212, + "nodeLength": 116, + "src": "t.datepicker.initialized || (t(document).on(\"mousedown\", t.datepicker._checkExternalClick) , t.datepicker.initialized = !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32329, + "nodeLength": 79, + "src": "0 === t(\"#\" + t.datepicker._mainDivId).length && t(\"body\").append(t.datepicker.dpDiv)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32329, + "nodeLength": 41, + "src": "0 === t(\"#\" + t.datepicker._mainDivId).length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32461, + "nodeLength": 65, + "src": "\"string\" != typeof e || \"isDisabled\" !== e && \"getDate\" !== e && \"widget\" !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32461, + "nodeLength": 18, + "src": "\"string\" != typeof e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32481, + "nodeLength": 45, + "src": "\"isDisabled\" !== e && \"getDate\" !== e && \"widget\" !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32481, + "nodeLength": 16, + "src": "\"isDisabled\" !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32499, + "nodeLength": 27, + "src": "\"getDate\" !== e && \"widget\" !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32499, + "nodeLength": 13, + "src": "\"getDate\" !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32514, + "nodeLength": 12, + "src": "\"widget\" !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32527, + "nodeLength": 65, + "src": "\"option\" === e && 2 === arguments.length && \"string\" == typeof arguments[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32527, + "nodeLength": 12, + "src": "\"option\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32541, + "nodeLength": 51, + "src": "2 === arguments.length && \"string\" == typeof arguments[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32541, + "nodeLength": 20, + "src": "2 === arguments.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32563, + "nodeLength": 29, + "src": "\"string\" == typeof arguments[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32687, + "nodeLength": 18, + "src": "\"string\" == typeof e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 118872, + "nodeLength": 55, + "src": "!0 === t.data(i.target, e.widgetName + \".preventClickEvent\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 119108, + "nodeLength": 150, + "src": "this._mouseMoveDelegate && this.document.off(\"mousemove.\" + this.widgetName, this._mouseMoveDelegate).off(\"mouseup.\" + this.widgetName, this._mouseUpDelegate)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 119286, + "nodeLength": 2, + "src": "!_", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 119310, + "nodeLength": 36, + "src": "this._mouseStarted && this._mouseUp(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 119383, + "nodeLength": 11, + "src": "1 === e.which", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 119397, + "nodeLength": 55, + "src": "\"string\" == typeof this.options.cancel && e.target.nodeName", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 119397, + "nodeLength": 36, + "src": "\"string\" == typeof this.options.cancel", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 119511, + "nodeLength": 28, + "src": "s && !n && this._mouseCapture(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 119514, + "nodeLength": 25, + "src": "!n && this._mouseCapture(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 119580, + "nodeLength": 105, + "src": "this.mouseDelayMet || (this._mouseDelayTimer = setTimeout(function() {\n i.mouseDelayMet = !0;\n}, this.options.delay))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 119686, + "nodeLength": 116, + "src": "this._mouseDistanceMet(e) && this._mouseDelayMet(e) && (this._mouseStarted = this._mouseStart(e) !== !1 , !this._mouseStarted)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 119713, + "nodeLength": 89, + "src": "this._mouseDelayMet(e) && (this._mouseStarted = this._mouseStart(e) !== !1 , !this._mouseStarted)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 119757, + "nodeLength": 24, + "src": "this._mouseStart(e) !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 119828, + "nodeLength": 119, + "src": "!0 === t.data(e.target, this.widgetName + \".preventClickEvent\") && t.removeData(e.target, this.widgetName + \".preventClickEvent\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 119828, + "nodeLength": 58, + "src": "!0 === t.data(e.target, this.widgetName + \".preventClickEvent\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120248, + "nodeLength": 16, + "src": "this._mouseMoved", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120269, + "nodeLength": 69, + "src": "t.ui.ie && (!document.documentMode || 9 > document.documentMode) && !e.button", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120279, + "nodeLength": 59, + "src": "(!document.documentMode || 9 > document.documentMode) && !e.button", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120279, + "nodeLength": 47, + "src": "!document.documentMode || 9 > document.documentMode", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120303, + "nodeLength": 23, + "src": "9 > document.documentMode", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120366, + "nodeLength": 8, + "src": "!e.which", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120378, + "nodeLength": 98, + "src": "e.originalEvent.altKey || e.originalEvent.ctrlKey || e.originalEvent.metaKey || e.originalEvent.shiftKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120402, + "nodeLength": 74, + "src": "e.originalEvent.ctrlKey || e.originalEvent.metaKey || e.originalEvent.shiftKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120427, + "nodeLength": 49, + "src": "e.originalEvent.metaKey || e.originalEvent.shiftKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120512, + "nodeLength": 24, + "src": "!this.ignoreMissingWhich", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120568, + "nodeLength": 41, + "src": "(e.which || e.button) && (this._mouseMoved = !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120568, + "nodeLength": 17, + "src": "e.which || e.button", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120610, + "nodeLength": 18, + "src": "this._mouseStarted", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120670, + "nodeLength": 172, + "src": "this._mouseDistanceMet(e) && this._mouseDelayMet(e) && (this._mouseStarted = this._mouseStart(this._mouseDownEvent, e) !== !1 , this._mouseStarted ? this._mouseDrag(e) : this._mouseUp(e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120697, + "nodeLength": 145, + "src": "this._mouseDelayMet(e) && (this._mouseStarted = this._mouseStart(this._mouseDownEvent, e) !== !1 , this._mouseStarted ? this._mouseDrag(e) : this._mouseUp(e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120741, + "nodeLength": 45, + "src": "this._mouseStart(this._mouseDownEvent, e) !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120787, + "nodeLength": 18, + "src": "this._mouseStarted", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 121012, + "nodeLength": 159, + "src": "this._mouseStarted && (this._mouseStarted = !1 , e.target === this._mouseDownEvent.target && t.data(e.target, this.widgetName + \".preventClickEvent\", !0) , this._mouseStop(e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 121055, + "nodeLength": 96, + "src": "e.target === this._mouseDownEvent.target && t.data(e.target, this.widgetName + \".preventClickEvent\", !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 121055, + "nodeLength": 38, + "src": "e.target === this._mouseDownEvent.target", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 121172, + "nodeLength": 89, + "src": "this._mouseDelayTimer && (clearTimeout(this._mouseDelayTimer) , delete this._mouseDelayTimer)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 121351, + "nodeLength": 122, + "src": "Math.max(Math.abs(this._mouseDownEvent.pageX - t.pageX), Math.abs(this._mouseDownEvent.pageY - t.pageY)) >= this.options.distance", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 121721, + "nodeLength": 16, + "src": "o.plugins[n] || []", + "evalFalse": 0, + "evalTrue": 25 + }, + { + "position": 121814, + "nodeLength": 70, + "src": "o && (s || t.element[0].parentNode && 11 !== t.element[0].parentNode.nodeType)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 121818, + "nodeLength": 65, + "src": "s || t.element[0].parentNode && 11 !== t.element[0].parentNode.nodeType", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 121821, + "nodeLength": 62, + "src": "t.element[0].parentNode && 11 !== t.element[0].parentNode.nodeType", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 121846, + "nodeLength": 37, + "src": "11 !== t.element[0].parentNode.nodeType", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 121893, + "nodeLength": 10, + "src": "o.length > n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 121908, + "nodeLength": 46, + "src": "t.options[o[n][0]] && o[n][1].apply(t.element, i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 121983, + "nodeLength": 58, + "src": "e && \"body\" !== e.nodeName.toLowerCase() && t(e).trigger(\"blur\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 121986, + "nodeLength": 55, + "src": "\"body\" !== e.nodeName.toLowerCase() && t(e).trigger(\"blur\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 121986, + "nodeLength": 33, + "src": "\"body\" !== e.nodeName.toLowerCase()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 122514, + "nodeLength": 61, + "src": "\"original\" === this.options.helper && this._setPositionRelative()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 122514, + "nodeLength": 32, + "src": "\"original\" === this.options.helper", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 122576, + "nodeLength": 55, + "src": "this.options.addClasses && this._addClass(\"ui-draggable\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 122720, + "nodeLength": 72, + "src": "\"handle\" === t && (this._removeHandleClassName() , this._setHandleClassName())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 122720, + "nodeLength": 12, + "src": "\"handle\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 122821, + "nodeLength": 55, + "src": "(this.helper || this.element).is(\".ui-draggable-dragging\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 122821, + "nodeLength": 25, + "src": "this.helper || this.element", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 123022, + "nodeLength": 77, + "src": "this.helper || i.disabled || t(e.target).closest(\".ui-resizable-handle\").length > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 123035, + "nodeLength": 64, + "src": "i.disabled || t(e.target).closest(\".ui-resizable-handle\").length > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 123047, + "nodeLength": 52, + "src": "t(e.target).closest(\".ui-resizable-handle\").length > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 123135, + "nodeLength": 11, + "src": "this.handle", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 123193, + "nodeLength": 16, + "src": "i.iframeFix === !0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 123508, + "nodeLength": 72, + "src": "this.iframeBlocks && (this.iframeBlocks.remove() , delete this.iframeBlocks)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 123674, + "nodeLength": 37, + "src": "s.closest(i).length || t.ui.safeBlur(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 123880, + "nodeLength": 45, + "src": "t.ui.ddmanager && (t.ui.ddmanager.current = this)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 124106, + "nodeLength": 90, + "src": "this.helper.parents().filter(function() {\n return \"fixed\" === t(this).css(\"position\");\n}).length > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 124152, + "nodeLength": 33, + "src": "\"fixed\" === t(this).css(\"position\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 124379, + "nodeLength": 52, + "src": "i.cursorAt && this._adjustOffsetFromHelper(i.cursorAt)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 124455, + "nodeLength": 29, + "src": "this._trigger(\"start\", e) === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 124536, + "nodeLength": 71, + "src": "t.ui.ddmanager && !i.dropBehaviour && t.ui.ddmanager.prepareOffsets(this, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 124552, + "nodeLength": 55, + "src": "!i.dropBehaviour && t.ui.ddmanager.prepareOffsets(this, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 124630, + "nodeLength": 48, + "src": "t.ui.ddmanager && t.ui.ddmanager.dragStart(this, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 124996, + "nodeLength": 166, + "src": "this.hasFixedAncestor && (this.offset.parent = this._getParentOffset()) , this.position = this._generatePosition(e, !0) , this.positionAbs = this._convertPositionTo(\"absolute\") , !i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 124996, + "nodeLength": 67, + "src": "this.hasFixedAncestor && (this.offset.parent = this._getParentOffset())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125188, + "nodeLength": 30, + "src": "this._trigger(\"drag\", e, s) === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125399, + "nodeLength": 43, + "src": "t.ui.ddmanager && t.ui.ddmanager.drag(this, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125493, + "nodeLength": 76, + "src": "t.ui.ddmanager && !this.options.dropBehaviour && (s = t.ui.ddmanager.drop(this, e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125509, + "nodeLength": 60, + "src": "!this.options.dropBehaviour && (s = t.ui.ddmanager.drop(this, e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125570, + "nodeLength": 46, + "src": "this.dropped && (s = this.dropped , this.dropped = !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125617, + "nodeLength": 172, + "src": "\"invalid\" === this.options.revert && !s || \"valid\" === this.options.revert && s || this.options.revert === !0 || t.isFunction(this.options.revert) && this.options.revert.call(this.element, s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125617, + "nodeLength": 35, + "src": "\"invalid\" === this.options.revert && !s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125617, + "nodeLength": 31, + "src": "\"invalid\" === this.options.revert", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125654, + "nodeLength": 135, + "src": "\"valid\" === this.options.revert && s || this.options.revert === !0 || t.isFunction(this.options.revert) && this.options.revert.call(this.element, s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125654, + "nodeLength": 32, + "src": "\"valid\" === this.options.revert && s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125654, + "nodeLength": 29, + "src": "\"valid\" === this.options.revert", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125688, + "nodeLength": 101, + "src": "this.options.revert === !0 || t.isFunction(this.options.revert) && this.options.revert.call(this.element, s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125688, + "nodeLength": 24, + "src": "this.options.revert === !0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125714, + "nodeLength": 75, + "src": "t.isFunction(this.options.revert) && this.options.revert.call(this.element, s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125887, + "nodeLength": 37, + "src": "i._trigger(\"stop\", e) !== !1 && i._clear()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125887, + "nodeLength": 25, + "src": "i._trigger(\"stop\", e) !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125927, + "nodeLength": 43, + "src": "this._trigger(\"stop\", e) !== !1 && this._clear()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125927, + "nodeLength": 28, + "src": "this._trigger(\"stop\", e) !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 126025, + "nodeLength": 47, + "src": "t.ui.ddmanager && t.ui.ddmanager.dragStop(this, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 126073, + "nodeLength": 62, + "src": "this.handleElement.is(e.target) && this.element.trigger(\"focus\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 126205, + "nodeLength": 40, + "src": "this.helper.is(\".ui-draggable-dragging\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 126359, + "nodeLength": 19, + "src": "this.options.handle", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 126502, + "nodeLength": 19, + "src": "this.options.handle", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 126799, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 126840, + "nodeLength": 18, + "src": "\"clone\" === i.helper", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 126917, + "nodeLength": 97, + "src": "n.parents(\"body\").length || n.appendTo(\"parent\" === i.appendTo ? this.element[0].parentNode : i.appendTo)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 126954, + "nodeLength": 21, + "src": "\"parent\" === i.appendTo", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127015, + "nodeLength": 54, + "src": "s && n[0] === this.element[0] && this._setPositionRelative()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127018, + "nodeLength": 51, + "src": "n[0] === this.element[0] && this._setPositionRelative()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127018, + "nodeLength": 22, + "src": "n[0] === this.element[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127070, + "nodeLength": 96, + "src": "n[0] === this.element[0] || /(fixed|absolute)/.test(n.css(\"position\")) || n.css(\"position\", \"absolute\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127070, + "nodeLength": 22, + "src": "n[0] === this.element[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127094, + "nodeLength": 72, + "src": "/(fixed|absolute)/.test(n.css(\"position\")) || n.css(\"position\", \"absolute\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127202, + "nodeLength": 92, + "src": "/^(?:r|a|f)/.test(this.element.css(\"position\")) || (this.element[0].style.position = \"relative\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127332, + "nodeLength": 36, + "src": "\"string\" == typeof e && (e = e.split(\" \"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127332, + "nodeLength": 18, + "src": "\"string\" == typeof e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127369, + "nodeLength": 43, + "src": "t.isArray(e) && (e = {\n left: +e[0], \n top: +e[1] || 0})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127402, + "nodeLength": 8, + "src": "+e[1] || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127413, + "nodeLength": 61, + "src": "\"left\" in e && (this.offset.click.left = e.left + this.margins.left)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127475, + "nodeLength": 92, + "src": "\"right\" in e && (this.offset.click.left = this.helperProportions.width - e.right + this.margins.left)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127568, + "nodeLength": 57, + "src": "\"top\" in e && (this.offset.click.top = e.top + this.margins.top)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127626, + "nodeLength": 93, + "src": "\"bottom\" in e && (this.offset.click.top = this.helperProportions.height - e.bottom + this.margins.top)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127751, + "nodeLength": 52, + "src": "/(html|body)/i.test(t.tagName) || t === this.document[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127783, + "nodeLength": 20, + "src": "t === this.document[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127891, + "nodeLength": 189, + "src": "\"absolute\" === this.cssPosition && this.scrollParent[0] !== i && t.contains(this.scrollParent[0], this.offsetParent[0]) && (e.left += this.scrollParent.scrollLeft() , e.top += this.scrollParent.scrollTop())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127891, + "nodeLength": 29, + "src": "\"absolute\" === this.cssPosition", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127922, + "nodeLength": 158, + "src": "this.scrollParent[0] !== i && t.contains(this.scrollParent[0], this.offsetParent[0]) && (e.left += this.scrollParent.scrollLeft() , e.top += this.scrollParent.scrollTop())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127922, + "nodeLength": 24, + "src": "this.scrollParent[0] !== i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 127948, + "nodeLength": 132, + "src": "t.contains(this.scrollParent[0], this.offsetParent[0]) && (e.left += this.scrollParent.scrollLeft() , e.top += this.scrollParent.scrollTop())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 128081, + "nodeLength": 58, + "src": "this._isRootNode(this.offsetParent[0]) && (e = {\n top: 0, \n left: 0})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 128152, + "nodeLength": 55, + "src": "parseInt(this.offsetParent.css(\"borderTopWidth\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 128222, + "nodeLength": 56, + "src": "parseInt(this.offsetParent.css(\"borderLeftWidth\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 128315, + "nodeLength": 29, + "src": "\"relative\" !== this.cssPosition", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 128455, + "nodeLength": 38, + "src": "parseInt(this.helper.css(\"top\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 128496, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 128544, + "nodeLength": 39, + "src": "parseInt(this.helper.css(\"left\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 128586, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "10": [ + null, + { + "position": 6590, + "nodeLength": 46, + "src": "parseInt(this.element.css(\"marginLeft\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6641, + "nodeLength": 45, + "src": "parseInt(this.element.css(\"marginTop\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6693, + "nodeLength": 47, + "src": "parseInt(this.element.css(\"marginRight\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6748, + "nodeLength": 48, + "src": "parseInt(this.element.css(\"marginBottom\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7030, + "nodeLength": 13, + "src": "n.containment", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7044, + "nodeLength": 24, + "src": "\"window\" === n.containment", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7342, + "nodeLength": 50, + "src": "t(window).height() || o.body.parentNode.scrollHeight", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7450, + "nodeLength": 26, + "src": "\"document\" === n.containment", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7561, + "nodeLength": 45, + "src": "t(o).height() || o.body.parentNode.scrollHeight", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7664, + "nodeLength": 33, + "src": "n.containment.constructor === Array", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7739, + "nodeLength": 67, + "src": "\"parent\" === n.containment && (n.containment = this.helper[0].parentNode)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7739, + "nodeLength": 24, + "src": "\"parent\" === n.containment", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7833, + "nodeLength": 670, + "src": "s && (e = /(scroll|auto)/.test(i.css(\"overflow\")) , this.containment = [(parseInt(i.css(\"borderLeftWidth\"), 10) || 0) + (parseInt(i.css(\"paddingLeft\"), 10) || 0), (parseInt(i.css(\"borderTopWidth\"), 10) || 0) + (parseInt(i.css(\"paddingTop\"), 10) || 0), (e ? Math.max(s.scrollWidth, s.offsetWidth) : s.offsetWidth) - (parseInt(i.css(\"borderRightWidth\"), 10) || 0) - (parseInt(i.css(\"paddingRight\"), 10) || 0) - this.helperProportions.width - this.margins.left - this.margins.right, (e ? Math.max(s.scrollHeight, s.offsetHeight) : s.offsetHeight) - (parseInt(i.css(\"borderBottomWidth\"), 10) || 0) - (parseInt(i.css(\"paddingBottom\"), 10) || 0) - this.helperProportions.height - this.margins.top - this.margins.bottom] , this.relativeContainer = i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7898, + "nodeLength": 40, + "src": "parseInt(i.css(\"borderLeftWidth\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7941, + "nodeLength": 36, + "src": "parseInt(i.css(\"paddingLeft\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7980, + "nodeLength": 39, + "src": "parseInt(i.css(\"borderTopWidth\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8022, + "nodeLength": 35, + "src": "parseInt(i.css(\"paddingTop\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8060, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8116, + "nodeLength": 41, + "src": "parseInt(i.css(\"borderRightWidth\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8160, + "nodeLength": 37, + "src": "parseInt(i.css(\"paddingRight\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8266, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8325, + "nodeLength": 42, + "src": "parseInt(i.css(\"borderBottomWidth\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8370, + "nodeLength": 38, + "src": "parseInt(i.css(\"paddingBottom\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8577, + "nodeLength": 20, + "src": "e || (e = this.position)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8604, + "nodeLength": 14, + "src": "\"absolute\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8735, + "nodeLength": 26, + "src": "\"fixed\" === this.cssPosition", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8786, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8883, + "nodeLength": 26, + "src": "\"fixed\" === this.cssPosition", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8935, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9095, + "nodeLength": 115, + "src": "r && this.offset.scroll || (this.offset.scroll = {\n top: this.scrollParent.scrollTop(), \n left: this.scrollParent.scrollLeft()})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9095, + "nodeLength": 21, + "src": "r && this.offset.scroll", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9211, + "nodeLength": 1025, + "src": "e && (this.containment && (this.relativeContainer ? (s = this.relativeContainer.offset() , i = [this.containment[0] + s.left, this.containment[1] + s.top, this.containment[2] + s.left, this.containment[3] + s.top]) : i = this.containment , t.pageX - this.offset.click.left < i[0] && (h = i[0] + this.offset.click.left) , t.pageY - this.offset.click.top < i[1] && (l = i[1] + this.offset.click.top) , t.pageX - this.offset.click.left > i[2] && (h = i[2] + this.offset.click.left) , t.pageY - this.offset.click.top > i[3] && (l = i[3] + this.offset.click.top)) , a.grid && (n = a.grid[1] ? this.originalPageY + Math.round((l - this.originalPageY) / a.grid[1]) * a.grid[1] : this.originalPageY , l = i ? n - this.offset.click.top >= i[1] || n - this.offset.click.top > i[3] ? n : n - this.offset.click.top >= i[1] ? n - a.grid[1] : n + a.grid[1] : n , o = a.grid[0] ? this.originalPageX + Math.round((h - this.originalPageX) / a.grid[0]) * a.grid[0] : this.originalPageX , h = i ? o - this.offset.click.left >= i[0] || o - this.offset.click.left > i[2] ? o : o - this.offset.click.left >= i[0] ? o - a.grid[0] : o + a.grid[0] : o) , \"y\" === a.axis && (h = this.originalPageX) , \"x\" === a.axis && (l = this.originalPageY))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9215, + "nodeLength": 479, + "src": "this.containment && (this.relativeContainer ? (s = this.relativeContainer.offset() , i = [this.containment[0] + s.left, this.containment[1] + s.top, this.containment[2] + s.left, this.containment[3] + s.top]) : i = this.containment , t.pageX - this.offset.click.left < i[0] && (h = i[0] + this.offset.click.left) , t.pageY - this.offset.click.top < i[1] && (l = i[1] + this.offset.click.top) , t.pageX - this.offset.click.left > i[2] && (h = i[2] + this.offset.click.left) , t.pageY - this.offset.click.top > i[3] && (l = i[3] + this.offset.click.top))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9234, + "nodeLength": 22, + "src": "this.relativeContainer", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9422, + "nodeLength": 68, + "src": "t.pageX - this.offset.click.left < i[0] && (h = i[0] + this.offset.click.left)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9422, + "nodeLength": 35, + "src": "t.pageX - this.offset.click.left < i[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9491, + "nodeLength": 66, + "src": "t.pageY - this.offset.click.top < i[1] && (l = i[1] + this.offset.click.top)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9491, + "nodeLength": 34, + "src": "t.pageY - this.offset.click.top < i[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9558, + "nodeLength": 68, + "src": "t.pageX - this.offset.click.left > i[2] && (h = i[2] + this.offset.click.left)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9558, + "nodeLength": 35, + "src": "t.pageX - this.offset.click.left > i[2]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9627, + "nodeLength": 66, + "src": "t.pageY - this.offset.click.top > i[3] && (l = i[3] + this.offset.click.top)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9627, + "nodeLength": 34, + "src": "t.pageY - this.offset.click.top > i[3]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9695, + "nodeLength": 466, + "src": "a.grid && (n = a.grid[1] ? this.originalPageY + Math.round((l - this.originalPageY) / a.grid[1]) * a.grid[1] : this.originalPageY , l = i ? n - this.offset.click.top >= i[1] || n - this.offset.click.top > i[3] ? n : n - this.offset.click.top >= i[1] ? n - a.grid[1] : n + a.grid[1] : n , o = a.grid[0] ? this.originalPageX + Math.round((h - this.originalPageX) / a.grid[0]) * a.grid[0] : this.originalPageX , h = i ? o - this.offset.click.left >= i[0] || o - this.offset.click.left > i[2] ? o : o - this.offset.click.left >= i[0] ? o - a.grid[0] : o + a.grid[0] : o)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9706, + "nodeLength": 9, + "src": "a.grid[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9811, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9813, + "nodeLength": 59, + "src": "n - this.offset.click.top >= i[1] || n - this.offset.click.top > i[3]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9813, + "nodeLength": 29, + "src": "n - this.offset.click.top >= i[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9844, + "nodeLength": 28, + "src": "n - this.offset.click.top > i[3]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9875, + "nodeLength": 29, + "src": "n - this.offset.click.top >= i[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9933, + "nodeLength": 9, + "src": "a.grid[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10038, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10040, + "nodeLength": 61, + "src": "o - this.offset.click.left >= i[0] || o - this.offset.click.left > i[2]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10040, + "nodeLength": 30, + "src": "o - this.offset.click.left >= i[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10072, + "nodeLength": 29, + "src": "o - this.offset.click.left > i[2]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10104, + "nodeLength": 30, + "src": "o - this.offset.click.left >= i[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10162, + "nodeLength": 36, + "src": "\"y\" === a.axis && (h = this.originalPageX)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10162, + "nodeLength": 12, + "src": "\"y\" === a.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10199, + "nodeLength": 36, + "src": "\"x\" === a.axis && (l = this.originalPageY)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10199, + "nodeLength": 12, + "src": "\"x\" === a.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10315, + "nodeLength": 26, + "src": "\"fixed\" === this.cssPosition", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10366, + "nodeLength": 1, + "src": "r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10475, + "nodeLength": 26, + "src": "\"fixed\" === this.cssPosition", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10527, + "nodeLength": 1, + "src": "r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10631, + "nodeLength": 80, + "src": "this.helper[0] === this.element[0] || this.cancelHelperRemoval || this.helper.remove()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10631, + "nodeLength": 32, + "src": "this.helper[0] === this.element[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10665, + "nodeLength": 46, + "src": "this.cancelHelperRemoval || this.helper.remove()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10757, + "nodeLength": 35, + "src": "this.destroyOnClear && this.destroy()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10828, + "nodeLength": 17, + "src": "s || this._uiHash()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10885, + "nodeLength": 110, + "src": "/^(drag|start|stop)/.test(e) && (this.positionAbs = this._convertPositionTo(\"absolute\") , s.offset = this.positionAbs)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14920, + "nodeLength": 93, + "src": "i && !i.options.disabled && (s.sortables.push(i) , i.refreshPositions() , i._trigger(\"activate\", e, n))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14923, + "nodeLength": 90, + "src": "!i.options.disabled && (s.sortables.push(i) , i.refreshPositions() , i._trigger(\"activate\", e, n))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15142, + "nodeLength": 8, + "src": "t.isOver", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15603, + "nodeLength": 293, + "src": "o._intersectsWith(o.containerCache) && (n = !0 , t.each(s.sortables, function() {\n return this.positionAbs = s.positionAbs , this.helperProportions = s.helperProportions , this.offset.click = s.offset.click , this !== o && this._intersectsWith(this.containerCache) && t.contains(o.element[0], this.element[0]) && (n = !1) , n;\n}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15790, + "nodeLength": 101, + "src": "this !== o && this._intersectsWith(this.containerCache) && t.contains(o.element[0], this.element[0]) && (n = !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15790, + "nodeLength": 8, + "src": "this !== o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15800, + "nodeLength": 91, + "src": "this._intersectsWith(this.containerCache) && t.contains(o.element[0], this.element[0]) && (n = !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15843, + "nodeLength": 48, + "src": "t.contains(o.element[0], this.element[0]) && (n = !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15897, + "nodeLength": 1, + "src": "n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15900, + "nodeLength": 621, + "src": "o.isOver || (o.isOver = 1 , s._parent = i.helper.parent() , o.currentItem = i.helper.appendTo(o.element).data(\"ui-sortable-item\", !0) , o.options._helper = o.options.helper , o.options.helper = function() {\n return i.helper[0];\n} , e.target = o.currentItem[0] , o._mouseCapture(e, !0) , o._mouseStart(e, !0, !0) , o.offset.click.top = s.offset.click.top , o.offset.click.left = s.offset.click.left , o.offset.parent.left -= s.offset.parent.left - o.offset.parent.left , o.offset.parent.top -= s.offset.parent.top - o.offset.parent.top , s._trigger(\"toSortable\", e) , s.dropped = o.element , t.each(s.sortables, function() {\n this.refreshPositions();\n}) , s.currentItem = s.element , o.fromOutside = s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16522, + "nodeLength": 54, + "src": "o.currentItem && (o._mouseDrag(e) , i.position = o.position)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16578, + "nodeLength": 447, + "src": "o.isOver && (o.isOver = 0 , o.cancelHelperRemoval = !0 , o.options._revert = o.options.revert , o.options.revert = !1 , o._trigger(\"out\", e, o._uiHash(o)) , o._mouseStop(e, !0) , o.options.revert = o.options._revert , o.options.helper = o.options._helper , o.placeholder && o.placeholder.remove() , i.helper.appendTo(s._parent) , s._refreshOffsets(e) , i.position = s._generatePosition(e, !0) , s._trigger(\"fromSortable\", e) , s.dropped = !1 , t.each(s.sortables, function() {\n this.refreshPositions();\n}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16802, + "nodeLength": 37, + "src": "o.placeholder && o.placeholder.remove()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17119, + "nodeLength": 44, + "src": "n.css(\"cursor\") && (o._cursor = n.css(\"cursor\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17227, + "nodeLength": 44, + "src": "n._cursor && t(\"body\").css(\"cursor\", n._cursor)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17366, + "nodeLength": 47, + "src": "n.css(\"opacity\") && (o._opacity = n.css(\"opacity\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17479, + "nodeLength": 49, + "src": "n._opacity && t(i.helper).css(\"opacity\", n._opacity)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17592, + "nodeLength": 76, + "src": "i.scrollParentNotHidden || (i.scrollParentNotHidden = i.helper.scrollParent(!1))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17669, + "nodeLength": 140, + "src": "i.scrollParentNotHidden[0] !== i.document[0] && \"HTML\" !== i.scrollParentNotHidden[0].tagName && (i.overflowOffset = i.scrollParentNotHidden.offset())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17669, + "nodeLength": 42, + "src": "i.scrollParentNotHidden[0] !== i.document[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17713, + "nodeLength": 96, + "src": "\"HTML\" !== i.scrollParentNotHidden[0].tagName && (i.overflowOffset = i.scrollParentNotHidden.offset())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17713, + "nodeLength": 43, + "src": "\"HTML\" !== i.scrollParentNotHidden[0].tagName", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17898, + "nodeLength": 25, + "src": "a !== r && \"HTML\" !== a.tagName", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17898, + "nodeLength": 5, + "src": "a !== r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17905, + "nodeLength": 18, + "src": "\"HTML\" !== a.tagName", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17925, + "nodeLength": 219, + "src": "n.axis && \"x\" === n.axis || (s.overflowOffset.top + a.offsetHeight - e.pageY < n.scrollSensitivity ? a.scrollTop = o = a.scrollTop + n.scrollSpeed : e.pageY - s.overflowOffset.top < n.scrollSensitivity && (a.scrollTop = o = a.scrollTop - n.scrollSpeed))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17925, + "nodeLength": 20, + "src": "n.axis && \"x\" === n.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17933, + "nodeLength": 12, + "src": "\"x\" === n.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17948, + "nodeLength": 63, + "src": "s.overflowOffset.top + a.offsetHeight - e.pageY < n.scrollSensitivity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18052, + "nodeLength": 91, + "src": "e.pageY - s.overflowOffset.top < n.scrollSensitivity && (a.scrollTop = o = a.scrollTop - n.scrollSpeed)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18052, + "nodeLength": 48, + "src": "e.pageY - s.overflowOffset.top < n.scrollSensitivity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18145, + "nodeLength": 224, + "src": "n.axis && \"y\" === n.axis || (s.overflowOffset.left + a.offsetWidth - e.pageX < n.scrollSensitivity ? a.scrollLeft = o = a.scrollLeft + n.scrollSpeed : e.pageX - s.overflowOffset.left < n.scrollSensitivity && (a.scrollLeft = o = a.scrollLeft - n.scrollSpeed))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18145, + "nodeLength": 20, + "src": "n.axis && \"y\" === n.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18153, + "nodeLength": 12, + "src": "\"y\" === n.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18168, + "nodeLength": 63, + "src": "s.overflowOffset.left + a.offsetWidth - e.pageX < n.scrollSensitivity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18274, + "nodeLength": 94, + "src": "e.pageX - s.overflowOffset.left < n.scrollSensitivity && (a.scrollLeft = o = a.scrollLeft - n.scrollSpeed)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18274, + "nodeLength": 49, + "src": "e.pageX - s.overflowOffset.left < n.scrollSensitivity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18372, + "nodeLength": 235, + "src": "n.axis && \"x\" === n.axis || (e.pageY - t(r).scrollTop() < n.scrollSensitivity ? o = t(r).scrollTop(t(r).scrollTop() - n.scrollSpeed) : t(window).height() - (e.pageY - t(r).scrollTop()) < n.scrollSensitivity && (o = t(r).scrollTop(t(r).scrollTop() + n.scrollSpeed)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18372, + "nodeLength": 20, + "src": "n.axis && \"x\" === n.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18380, + "nodeLength": 12, + "src": "\"x\" === n.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18395, + "nodeLength": 44, + "src": "e.pageY - t(r).scrollTop() < n.scrollSensitivity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18489, + "nodeLength": 117, + "src": "t(window).height() - (e.pageY - t(r).scrollTop()) < n.scrollSensitivity && (o = t(r).scrollTop(t(r).scrollTop() + n.scrollSpeed))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18489, + "nodeLength": 65, + "src": "t(window).height() - (e.pageY - t(r).scrollTop()) < n.scrollSensitivity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18608, + "nodeLength": 240, + "src": "n.axis && \"y\" === n.axis || (e.pageX - t(r).scrollLeft() < n.scrollSensitivity ? o = t(r).scrollLeft(t(r).scrollLeft() - n.scrollSpeed) : t(window).width() - (e.pageX - t(r).scrollLeft()) < n.scrollSensitivity && (o = t(r).scrollLeft(t(r).scrollLeft() + n.scrollSpeed)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18608, + "nodeLength": 20, + "src": "n.axis && \"y\" === n.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18616, + "nodeLength": 12, + "src": "\"y\" === n.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18631, + "nodeLength": 45, + "src": "e.pageX - t(r).scrollLeft() < n.scrollSensitivity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18728, + "nodeLength": 119, + "src": "t(window).width() - (e.pageX - t(r).scrollLeft()) < n.scrollSensitivity && (o = t(r).scrollLeft(t(r).scrollLeft() + n.scrollSpeed))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18728, + "nodeLength": 65, + "src": "t(window).width() - (e.pageX - t(r).scrollLeft()) < n.scrollSensitivity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18850, + "nodeLength": 76, + "src": "o !== !1 && t.ui.ddmanager && !n.dropBehaviour && t.ui.ddmanager.prepareOffsets(s, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18850, + "nodeLength": 6, + "src": "o !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18858, + "nodeLength": 68, + "src": "t.ui.ddmanager && !n.dropBehaviour && t.ui.ddmanager.prepareOffsets(s, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18874, + "nodeLength": 52, + "src": "!n.dropBehaviour && t.ui.ddmanager.prepareOffsets(s, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19024, + "nodeLength": 27, + "src": "n.snap.constructor !== String", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19052, + "nodeLength": 35, + "src": "n.snap.items || \":data(ui-draggable)\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19139, + "nodeLength": 119, + "src": "this !== s.element[0] && s.snapElements.push({\n item: this, \n width: e.outerWidth(), \n height: e.outerHeight(), \n top: i.top, \n left: i.left})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19139, + "nodeLength": 19, + "src": "this !== s.element[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19459, + "nodeLength": 4, + "src": "d >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19603, + "nodeLength": 100, + "src": "h - g > _ || m > l + g || c - g > b || v > u + g || !t.contains(s.snapElements[d].item.ownerDocument, s.snapElements[d].item)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19603, + "nodeLength": 5, + "src": "h - g > _", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19610, + "nodeLength": 93, + "src": "m > l + g || c - g > b || v > u + g || !t.contains(s.snapElements[d].item.ownerDocument, s.snapElements[d].item)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19610, + "nodeLength": 5, + "src": "m > l + g", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19617, + "nodeLength": 86, + "src": "c - g > b || v > u + g || !t.contains(s.snapElements[d].item.ownerDocument, s.snapElements[d].item)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19617, + "nodeLength": 5, + "src": "c - g > b", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19624, + "nodeLength": 79, + "src": "v > u + g || !t.contains(s.snapElements[d].item.ownerDocument, s.snapElements[d].item)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19624, + "nodeLength": 5, + "src": "v > u + g", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19705, + "nodeLength": 148, + "src": "s.snapElements[d].snapping && s.options.snap.release && s.options.snap.release.call(s.element, e, t.extend(s._uiHash(), {\n snapItem: s.snapElements[d].item}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19733, + "nodeLength": 120, + "src": "s.options.snap.release && s.options.snap.release.call(s.element, e, t.extend(s._uiHash(), {\n snapItem: s.snapElements[d].item}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19886, + "nodeLength": 444, + "src": "\"inner\" !== f.snapMode && (n = g >= Math.abs(c - b) , o = g >= Math.abs(u - v) , a = g >= Math.abs(h - _) , r = g >= Math.abs(l - m) , n && (i.position.top = s._convertPositionTo(\"relative\", {\n top: c - s.helperProportions.height, \n left: 0}).top) , o && (i.position.top = s._convertPositionTo(\"relative\", {\n top: u, \n left: 0}).top) , a && (i.position.left = s._convertPositionTo(\"relative\", {\n top: 0, \n left: h - s.helperProportions.width}).left) , r && (i.position.left = s._convertPositionTo(\"relative\", {\n top: 0, \n left: l}).left))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19886, + "nodeLength": 20, + "src": "\"inner\" !== f.snapMode", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19911, + "nodeLength": 16, + "src": "g >= Math.abs(c - b)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19930, + "nodeLength": 16, + "src": "g >= Math.abs(u - v)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19949, + "nodeLength": 16, + "src": "g >= Math.abs(h - _)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19968, + "nodeLength": 16, + "src": "g >= Math.abs(l - m)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19985, + "nodeLength": 98, + "src": "n && (i.position.top = s._convertPositionTo(\"relative\", {\n top: c - s.helperProportions.height, \n left: 0}).top)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20084, + "nodeLength": 71, + "src": "o && (i.position.top = s._convertPositionTo(\"relative\", {\n top: u, \n left: 0}).top)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20156, + "nodeLength": 99, + "src": "a && (i.position.left = s._convertPositionTo(\"relative\", {\n top: 0, \n left: h - s.helperProportions.width}).left)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20256, + "nodeLength": 73, + "src": "r && (i.position.left = s._convertPositionTo(\"relative\", {\n top: 0, \n left: l}).left)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20333, + "nodeLength": 10, + "src": "n || o || a || r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20336, + "nodeLength": 7, + "src": "o || a || r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20339, + "nodeLength": 4, + "src": "a || r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20344, + "nodeLength": 444, + "src": "\"outer\" !== f.snapMode && (n = g >= Math.abs(c - v) , o = g >= Math.abs(u - b) , a = g >= Math.abs(h - m) , r = g >= Math.abs(l - _) , n && (i.position.top = s._convertPositionTo(\"relative\", {\n top: c, \n left: 0}).top) , o && (i.position.top = s._convertPositionTo(\"relative\", {\n top: u - s.helperProportions.height, \n left: 0}).top) , a && (i.position.left = s._convertPositionTo(\"relative\", {\n top: 0, \n left: h}).left) , r && (i.position.left = s._convertPositionTo(\"relative\", {\n top: 0, \n left: l - s.helperProportions.width}).left))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20344, + "nodeLength": 20, + "src": "\"outer\" !== f.snapMode", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20369, + "nodeLength": 16, + "src": "g >= Math.abs(c - v)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20388, + "nodeLength": 16, + "src": "g >= Math.abs(u - b)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20407, + "nodeLength": 16, + "src": "g >= Math.abs(h - m)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20426, + "nodeLength": 16, + "src": "g >= Math.abs(l - _)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20443, + "nodeLength": 71, + "src": "n && (i.position.top = s._convertPositionTo(\"relative\", {\n top: c, \n left: 0}).top)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20515, + "nodeLength": 98, + "src": "o && (i.position.top = s._convertPositionTo(\"relative\", {\n top: u - s.helperProportions.height, \n left: 0}).top)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20614, + "nodeLength": 73, + "src": "a && (i.position.left = s._convertPositionTo(\"relative\", {\n top: 0, \n left: h}).left)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20688, + "nodeLength": 99, + "src": "r && (i.position.left = s._convertPositionTo(\"relative\", {\n top: 0, \n left: l - s.helperProportions.width}).left)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20789, + "nodeLength": 160, + "src": "!s.snapElements[d].snapping && (n || o || a || r || p) && s.options.snap.snap && s.options.snap.snap.call(s.element, e, t.extend(s._uiHash(), {\n snapItem: s.snapElements[d].item}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20819, + "nodeLength": 130, + "src": "(n || o || a || r || p) && s.options.snap.snap && s.options.snap.snap.call(s.element, e, t.extend(s._uiHash(), {\n snapItem: s.snapElements[d].item}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20819, + "nodeLength": 13, + "src": "n || o || a || r || p", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20822, + "nodeLength": 10, + "src": "o || a || r || p", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20825, + "nodeLength": 7, + "src": "a || r || p", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20828, + "nodeLength": 4, + "src": "r || p", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20835, + "nodeLength": 114, + "src": "s.options.snap.snap && s.options.snap.snap.call(s.element, e, t.extend(s._uiHash(), {\n snapItem: s.snapElements[d].item}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20977, + "nodeLength": 13, + "src": "n || o || a || r || p", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20980, + "nodeLength": 10, + "src": "o || a || r || p", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20983, + "nodeLength": 7, + "src": "a || r || p", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20986, + "nodeLength": 4, + "src": "r || p", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21124, + "nodeLength": 34, + "src": "parseInt(t(e).css(\"zIndex\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21161, + "nodeLength": 34, + "src": "parseInt(t(i).css(\"zIndex\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21199, + "nodeLength": 131, + "src": "a.length && (n = parseInt(t(a[0]).css(\"zIndex\"), 10) || 0 , t(a).each(function(e) {\n t(this).css(\"zIndex\", n + e);\n}) , this.css(\"zIndex\", n + a.length))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21212, + "nodeLength": 37, + "src": "parseInt(t(a[0]).css(\"zIndex\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21424, + "nodeLength": 44, + "src": "n.css(\"zIndex\") && (o._zIndex = n.css(\"zIndex\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21532, + "nodeLength": 46, + "src": "n._zIndex && t(i.helper).css(\"zIndex\", n._zIndex)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22032, + "nodeLength": 16, + "src": "parseFloat(t) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22129, + "nodeLength": 31, + "src": "\"hidden\" === t(e).css(\"overflow\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22176, + "nodeLength": 13, + "src": "i && \"left\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22179, + "nodeLength": 10, + "src": "\"left\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22227, + "nodeLength": 6, + "src": "e[s] > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22247, + "nodeLength": 6, + "src": "e[s] > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22483, + "nodeLength": 28, + "src": "i.helper || i.ghost || i.animate", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22493, + "nodeLength": 18, + "src": "i.ghost || i.animate", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22512, + "nodeLength": 31, + "src": "i.helper || \"ui-resizable-helper\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22551, + "nodeLength": 981, + "src": "this.element[0].nodeName.match(/^(canvas|textarea|input|select|button|img)$/i) && (this.element.wrap(t(\"
\").css({\n position: this.element.css(\"position\"), \n width: this.element.outerWidth(), \n height: this.element.outerHeight(), \n top: this.element.css(\"top\"), \n left: this.element.css(\"left\")})) , this.element = this.element.parent().data(\"ui-resizable\", this.element.resizable(\"instance\")) , this.elementIsWrapper = !0 , e = {\n marginTop: this.originalElement.css(\"marginTop\"), \n marginRight: this.originalElement.css(\"marginRight\"), \n marginBottom: this.originalElement.css(\"marginBottom\"), \n marginLeft: this.originalElement.css(\"marginLeft\")} , this.element.css(e) , this.originalElement.css(\"margin\", 0) , this.originalResizeStyle = this.originalElement.css(\"resize\") , this.originalElement.css(\"resize\", \"none\") , this._proportionallyResizeElements.push(this.originalElement.css({\n position: \"static\", \n zoom: 1, \n display: \"block\"})) , this.originalElement.css(e) , this._proportionallyResize())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23554, + "nodeLength": 238, + "src": "i.autoHide && t(this.element).on(\"mouseenter\", function() {\n i.disabled || (s._removeClass(\"ui-resizable-autohide\") , s._handles.show());\n}).on(\"mouseleave\", function() {\n i.disabled || s.resizing || (s._addClass(\"ui-resizable-autohide\") , s._handles.hide());\n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23609, + "nodeLength": 71, + "src": "i.disabled || (s._removeClass(\"ui-resizable-autohide\") , s._handles.show())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23710, + "nodeLength": 80, + "src": "i.disabled || s.resizing || (s._addClass(\"ui-resizable-autohide\") , s._handles.hide())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23722, + "nodeLength": 68, + "src": "s.resizing || (s._addClass(\"ui-resizable-autohide\") , s._handles.hide())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23993, + "nodeLength": 216, + "src": "this.elementIsWrapper && (i(this.element) , e = this.element , this.originalElement.css({\n position: e.css(\"position\"), \n width: e.outerWidth(), \n height: e.outerHeight(), \n top: e.css(\"top\"), \n left: e.css(\"left\")}).insertAfter(e) , e.remove())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24490, + "nodeLength": 302, + "src": "this.handles = a.handles || (t(\".ui-resizable-handle\", this.element).length ? {\n n: \".ui-resizable-n\", \n e: \".ui-resizable-e\", \n s: \".ui-resizable-s\", \n w: \".ui-resizable-w\", \n se: \".ui-resizable-se\", \n sw: \".ui-resizable-sw\", \n ne: \".ui-resizable-ne\", \n nw: \".ui-resizable-nw\"} : \"e,s,se\") , this._handles = t() , this.handles.constructor === String", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24503, + "nodeLength": 237, + "src": "a.handles || (t(\".ui-resizable-handle\", this.element).length ? {\n n: \".ui-resizable-n\", \n e: \".ui-resizable-e\", \n s: \".ui-resizable-s\", \n w: \".ui-resizable-w\", \n se: \".ui-resizable-se\", \n sw: \".ui-resizable-sw\", \n ne: \".ui-resizable-ne\", \n nw: \".ui-resizable-nw\"} : \"e,s,se\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24515, + "nodeLength": 45, + "src": "t(\".ui-resizable-handle\", this.element).length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24759, + "nodeLength": 33, + "src": "this.handles.constructor === String", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24797, + "nodeLength": 58, + "src": "\"all\" === this.handles && (this.handles = \"n,e,s,w,se,sw,ne,nw\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24797, + "nodeLength": 20, + "src": "\"all\" === this.handles", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24902, + "nodeLength": 10, + "src": "s.length > i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25134, + "nodeLength": 15, + "src": "e || this.element", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25172, + "nodeLength": 36, + "src": "this.handles[i].constructor === String", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25280, + "nodeLength": 138, + "src": "(this.handles[i].jquery || this.handles[i].nodeType) && (this.handles[i] = t(this.handles[i]) , this._on(this.handles[i], {\n mousedown: r._mouseDown}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25280, + "nodeLength": 48, + "src": "this.handles[i].jquery || this.handles[i].nodeType", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25419, + "nodeLength": 337, + "src": "this.elementIsWrapper && this.originalElement[0].nodeName.match(/^(textarea|input|select|button)$/i) && (s = t(this.handles[i], this.element) , o = /sw|ne|nw|se|n|s/.test(i) ? s.outerHeight() : s.outerWidth() , n = [\"padding\", /ne|nw|n/.test(i) ? \"Top\" : /se|sw|s/.test(i) ? \"Bottom\" : /^e$/.test(i) ? \"Right\" : \"Left\"].join(\"\") , e.css(n, o) , this._proportionallyResize())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25442, + "nodeLength": 314, + "src": "this.originalElement[0].nodeName.match(/^(textarea|input|select|button)$/i) && (s = t(this.handles[i], this.element) , o = /sw|ne|nw|se|n|s/.test(i) ? s.outerHeight() : s.outerWidth() , n = [\"padding\", /ne|nw|n/.test(i) ? \"Top\" : /se|sw|s/.test(i) ? \"Bottom\" : /^e$/.test(i) ? \"Right\" : \"Left\"].join(\"\") , e.css(n, o) , this._proportionallyResize())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25556, + "nodeLength": 25, + "src": "/sw|ne|nw|se|n|s/.test(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25626, + "nodeLength": 17, + "src": "/ne|nw|n/.test(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25650, + "nodeLength": 17, + "src": "/se|sw|s/.test(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25677, + "nodeLength": 13, + "src": "/^e$/.test(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25986, + "nodeLength": 118, + "src": "r.resizing || (this.className && (o = this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i)) , r.axis = o && o[1] ? o[1] : \"se\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25999, + "nodeLength": 79, + "src": "this.className && (o = this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26086, + "nodeLength": 7, + "src": "o && o[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26107, + "nodeLength": 74, + "src": "a.autoHide && (this._handles.hide() , this._addClass(\"ui-resizable-autohide\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26319, + "nodeLength": 45, + "src": "(s === e.target || t.contains(s, e.target)) && (n = !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26319, + "nodeLength": 36, + "src": "s === e.target || t.contains(s, e.target)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26319, + "nodeLength": 12, + "src": "s === e.target", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26371, + "nodeLength": 25, + "src": "!this.options.disabled && n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26579, + "nodeLength": 87, + "src": "o.containment && (i += t(o.containment).scrollLeft() || 0 , s += t(o.containment).scrollTop() || 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26598, + "nodeLength": 32, + "src": "t(o.containment).scrollLeft() || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26634, + "nodeLength": 31, + "src": "t(o.containment).scrollTop() || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26739, + "nodeLength": 12, + "src": "this._helper", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26862, + "nodeLength": 12, + "src": "this._helper", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 27146, + "nodeLength": 30, + "src": "\"number\" == typeof o.aspectRatio", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 27191, + "nodeLength": 51, + "src": "this.originalSize.width / this.originalSize.height || 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 27312, + "nodeLength": 10, + "src": "\"auto\" === n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 27491, + "nodeLength": 17, + "src": "e.pageX - n.left || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 27511, + "nodeLength": 16, + "src": "e.pageY - n.top || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 27582, + "nodeLength": 1, + "src": "h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 27652, + "nodeLength": 58, + "src": "(this._aspectRatio || e.shiftKey) && (i = this._updateRatio(i, e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 27652, + "nodeLength": 29, + "src": "this._aspectRatio || e.shiftKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 27808, + "nodeLength": 86, + "src": "!this._helper && this._proportionallyResizeElements.length && this._proportionallyResize()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 27823, + "nodeLength": 71, + "src": "this._proportionallyResizeElements.length && this._proportionallyResize()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 27895, + "nodeLength": 107, + "src": "t.isEmptyObject(s) || (this._updatePrevProperties() , this._trigger(\"resize\", e, this.ui()) , this._applyChanges())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28098, + "nodeLength": 565, + "src": "this._helper && (i = this._proportionallyResizeElements , s = i.length && /textarea/i.test(i[0].nodeName) , n = s && this._hasScroll(i[0], \"left\") ? 0 : c.sizeDiff.height , o = s ? 0 : c.sizeDiff.width , a = {\n width: c.helper.width() - o, \n height: c.helper.height() - n} , r = parseFloat(c.element.css(\"left\")) + (c.position.left - c.originalPosition.left) || null , h = parseFloat(c.element.css(\"top\")) + (c.position.top - c.originalPosition.top) || null , l.animate || this.element.css(t.extend(a, {\n top: h, \n left: r})) , c.helper.height(c.size.height) , c.helper.width(c.size.width) , this._helper && !l.animate && this._proportionallyResize())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28152, + "nodeLength": 41, + "src": "i.length && /textarea/i.test(i[0].nodeName)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28196, + "nodeLength": 31, + "src": "s && this._hasScroll(i[0], \"left\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28250, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28329, + "nodeLength": 81, + "src": "parseFloat(c.element.css(\"left\")) + (c.position.left - c.originalPosition.left) || null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28413, + "nodeLength": 78, + "src": "parseFloat(c.element.css(\"top\")) + (c.position.top - c.originalPosition.top) || null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28492, + "nodeLength": 55, + "src": "l.animate || this.element.css(t.extend(a, {\n top: h, \n left: r}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28608, + "nodeLength": 54, + "src": "this._helper && !l.animate && this._proportionallyResize()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28622, + "nodeLength": 40, + "src": "!l.animate && this._proportionallyResize()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28764, + "nodeLength": 34, + "src": "this._helper && this.helper.remove()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29006, + "nodeLength": 73, + "src": "this.position.top !== this.prevPosition.top && (t.top = this.position.top + \"px\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29006, + "nodeLength": 41, + "src": "this.position.top !== this.prevPosition.top", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29080, + "nodeLength": 77, + "src": "this.position.left !== this.prevPosition.left && (t.left = this.position.left + \"px\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29080, + "nodeLength": 43, + "src": "this.position.left !== this.prevPosition.left", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29158, + "nodeLength": 69, + "src": "this.size.width !== this.prevSize.width && (t.width = this.size.width + \"px\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29158, + "nodeLength": 37, + "src": "this.size.width !== this.prevSize.width", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29228, + "nodeLength": 73, + "src": "this.size.height !== this.prevSize.height && (t.height = this.size.height + \"px\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29228, + "nodeLength": 39, + "src": "this.size.height !== this.prevSize.height", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29402, + "nodeLength": 26, + "src": "this._isNumber(a.minWidth)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29451, + "nodeLength": 26, + "src": "this._isNumber(a.maxWidth)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29503, + "nodeLength": 27, + "src": "this._isNumber(a.minHeight)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29555, + "nodeLength": 27, + "src": "this._isNumber(a.maxHeight)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29601, + "nodeLength": 266, + "src": "(this._aspectRatio || t) && (e = o.minHeight * this.aspectRatio , s = o.minWidth / this.aspectRatio , i = o.maxHeight * this.aspectRatio , n = o.maxWidth / this.aspectRatio , e > o.minWidth && (o.minWidth = e) , s > o.minHeight && (o.minHeight = s) , o.maxWidth > i && (o.maxWidth = i) , o.maxHeight > n && (o.maxHeight = n))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29601, + "nodeLength": 20, + "src": "this._aspectRatio || t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29747, + "nodeLength": 28, + "src": "e > o.minWidth && (o.minWidth = e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29747, + "nodeLength": 12, + "src": "e > o.minWidth", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29776, + "nodeLength": 30, + "src": "s > o.minHeight && (o.minHeight = s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29776, + "nodeLength": 13, + "src": "s > o.minHeight", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29807, + "nodeLength": 28, + "src": "o.maxWidth > i && (o.maxWidth = i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29807, + "nodeLength": 12, + "src": "o.maxWidth > i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29836, + "nodeLength": 30, + "src": "o.maxHeight > n && (o.maxHeight = n)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29836, + "nodeLength": 13, + "src": "o.maxHeight > n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29947, + "nodeLength": 51, + "src": "this._isNumber(t.left) && (this.position.left = t.left)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29999, + "nodeLength": 48, + "src": "this._isNumber(t.top) && (this.position.top = t.top)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30048, + "nodeLength": 53, + "src": "this._isNumber(t.height) && (this.size.height = t.height)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30102, + "nodeLength": 50, + "src": "this._isNumber(t.width) && (this.size.width = t.width)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30230, + "nodeLength": 24, + "src": "this._isNumber(t.height)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30289, + "nodeLength": 60, + "src": "this._isNumber(t.width) && (t.height = t.width / this.aspectRatio)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30350, + "nodeLength": 54, + "src": "\"sw\" === s && (t.left = e.left + (i.width - t.width) , t.top = null)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30350, + "nodeLength": 8, + "src": "\"sw\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30405, + "nodeLength": 75, + "src": "\"nw\" === s && (t.top = e.top + (i.height - t.height) , t.left = e.left + (i.width - t.width))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30405, + "nodeLength": 8, + "src": "\"nw\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30547, + "nodeLength": 55, + "src": "this._isNumber(t.width) && e.maxWidth && e.maxWidth < t.width", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30572, + "nodeLength": 30, + "src": "e.maxWidth && e.maxWidth < t.width", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30584, + "nodeLength": 18, + "src": "e.maxWidth < t.width", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30605, + "nodeLength": 59, + "src": "this._isNumber(t.height) && e.maxHeight && e.maxHeight < t.height", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30631, + "nodeLength": 33, + "src": "e.maxHeight && e.maxHeight < t.height", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30644, + "nodeLength": 20, + "src": "e.maxHeight < t.height", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30667, + "nodeLength": 55, + "src": "this._isNumber(t.width) && e.minWidth && e.minWidth > t.width", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30692, + "nodeLength": 30, + "src": "e.minWidth && e.minWidth > t.width", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30704, + "nodeLength": 18, + "src": "e.minWidth > t.width", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30725, + "nodeLength": 59, + "src": "this._isNumber(t.height) && e.minHeight && e.minHeight > t.height", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30751, + "nodeLength": 33, + "src": "e.minHeight && e.minHeight > t.height", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30764, + "nodeLength": 20, + "src": "e.minHeight > t.height", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30938, + "nodeLength": 23, + "src": "o && (t.width = e.minWidth)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30962, + "nodeLength": 25, + "src": "a && (t.height = e.minHeight)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30988, + "nodeLength": 23, + "src": "s && (t.width = e.maxWidth)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31012, + "nodeLength": 25, + "src": "n && (t.height = e.maxHeight)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31038, + "nodeLength": 27, + "src": "o && l && (t.left = r - e.minWidth)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31041, + "nodeLength": 24, + "src": "l && (t.left = r - e.minWidth)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31066, + "nodeLength": 27, + "src": "s && l && (t.left = r - e.maxWidth)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31069, + "nodeLength": 24, + "src": "l && (t.left = r - e.maxWidth)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31094, + "nodeLength": 27, + "src": "a && c && (t.top = h - e.minHeight)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31097, + "nodeLength": 24, + "src": "c && (t.top = h - e.minHeight)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31122, + "nodeLength": 27, + "src": "n && c && (t.top = h - e.maxHeight)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31125, + "nodeLength": 24, + "src": "c && (t.top = h - e.maxHeight)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31150, + "nodeLength": 33, + "src": "t.width || t.height || t.left || !t.top", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31159, + "nodeLength": 24, + "src": "t.height || t.left || !t.top", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31169, + "nodeLength": 14, + "src": "t.left || !t.top", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31184, + "nodeLength": 48, + "src": "t.width || t.height || t.top || !t.left || (t.left = null)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31193, + "nodeLength": 39, + "src": "t.height || t.top || !t.left || (t.left = null)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31203, + "nodeLength": 29, + "src": "t.top || !t.left || (t.left = null)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31210, + "nodeLength": 22, + "src": "!t.left || (t.left = null)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31504, + "nodeLength": 3, + "src": "4 > e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31517, + "nodeLength": 19, + "src": "parseFloat(s[e]) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31543, + "nodeLength": 19, + "src": "parseFloat(n[e]) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31641, + "nodeLength": 41, + "src": "this._proportionallyResizeElements.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31699, + "nodeLength": 25, + "src": "this.helper || this.element", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31725, + "nodeLength": 43, + "src": "this._proportionallyResizeElements.length > e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31813, + "nodeLength": 84, + "src": "this.outerDimensions || (this.outerDimensions = this._getPaddingPlusBorderDimensions(t))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31912, + "nodeLength": 41, + "src": "i.height() - this.outerDimensions.height || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31960, + "nodeLength": 39, + "src": "i.width() - this.outerDimensions.width || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32091, + "nodeLength": 12, + "src": "this._helper", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32117, + "nodeLength": 54, + "src": "this.helper || t(\"
\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33313, + "nodeLength": 42, + "src": "\"resize\" !== e && this._trigger(e, i, this.ui())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33313, + "nodeLength": 12, + "src": "\"resize\" !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33718, + "nodeLength": 41, + "src": "n.length && /textarea/i.test(n[0].nodeName)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33762, + "nodeLength": 28, + "src": "o && i._hasScroll(n[0], \"left\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33813, + "nodeLength": 1, + "src": "o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33884, + "nodeLength": 81, + "src": "parseFloat(i.element.css(\"left\")) + (i.position.left - i.originalPosition.left) || null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33968, + "nodeLength": 78, + "src": "parseFloat(i.element.css(\"top\")) + (i.position.top - i.originalPosition.top) || null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34076, + "nodeLength": 4, + "src": "c && l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34335, + "nodeLength": 57, + "src": "n && n.length && t(n[0]).css({\n width: s.width, \n height: s.height})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34338, + "nodeLength": 54, + "src": "n.length && t(n[0]).css({\n width: s.width, \n height: s.height})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34594, + "nodeLength": 14, + "src": "u instanceof t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34618, + "nodeLength": 16, + "src": "/parent/.test(u)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34655, + "nodeLength": 717, + "src": "d && (h.containerElement = t(d) , /document/.test(u) || u === document ? (h.containerOffset = {\n left: 0, \n top: 0} , h.containerPosition = {\n left: 0, \n top: 0} , h.parentData = {\n element: t(document), \n left: 0, \n top: 0, \n width: t(document).width(), \n height: t(document).height() || document.body.parentNode.scrollHeight}) : (e = t(d) , i = [] , t([\"Top\", \"Right\", \"Left\", \"Bottom\"]).each(function(t, s) {\n i[t] = h._num(e.css(\"padding\" + s));\n}) , h.containerOffset = e.offset() , h.containerPosition = e.position() , h.containerSize = {\n height: e.innerHeight() - i[3], \n width: e.innerWidth() - i[1]} , s = h.containerOffset , n = h.containerSize.height , o = h.containerSize.width , a = h._hasScroll(d, \"left\") ? d.scrollWidth : o , r = h._hasScroll(d) ? d.scrollHeight : n , h.parentData = {\n element: d, \n left: s.left, \n top: s.top, \n width: a, \n height: r}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34683, + "nodeLength": 32, + "src": "/document/.test(u) || u === document", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34703, + "nodeLength": 12, + "src": "u === document", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34865, + "nodeLength": 59, + "src": "t(document).height() || document.body.parentNode.scrollHeight", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35233, + "nodeLength": 22, + "src": "h._hasScroll(d, \"left\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35274, + "nodeLength": 15, + "src": "h._hasScroll(d)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35484, + "nodeLength": 26, + "src": "a._aspectRatio || e.shiftKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35554, + "nodeLength": 56, + "src": "d[0] !== document && /static/.test(d.css(\"position\")) && (u = h)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35554, + "nodeLength": 15, + "src": "d[0] !== document", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35571, + "nodeLength": 39, + "src": "/static/.test(d.css(\"position\")) && (u = h)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35611, + "nodeLength": 199, + "src": "l.left < (a._helper ? h.left : 0) && (a.size.width = a.size.width + (a._helper ? a.position.left - h.left : a.position.left - u.left) , c && (a.size.height = a.size.width / a.aspectRatio , p = !1) , a.position.left = r.helper ? h.left : 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35611, + "nodeLength": 27, + "src": "l.left < (a._helper ? h.left : 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35619, + "nodeLength": 9, + "src": "a._helper", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35668, + "nodeLength": 9, + "src": "a._helper", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35725, + "nodeLength": 50, + "src": "c && (a.size.height = a.size.width / a.aspectRatio , p = !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35792, + "nodeLength": 8, + "src": "r.helper", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35811, + "nodeLength": 188, + "src": "l.top < (a._helper ? h.top : 0) && (a.size.height = a.size.height + (a._helper ? a.position.top - h.top : a.position.top) , c && (a.size.width = a.size.height * a.aspectRatio , p = !1) , a.position.top = a._helper ? h.top : 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35811, + "nodeLength": 25, + "src": "l.top < (a._helper ? h.top : 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35818, + "nodeLength": 9, + "src": "a._helper", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35868, + "nodeLength": 9, + "src": "a._helper", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35915, + "nodeLength": 50, + "src": "c && (a.size.width = a.size.height * a.aspectRatio , p = !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35981, + "nodeLength": 9, + "src": "a._helper", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36002, + "nodeLength": 53, + "src": "a.containerElement.get(0) === a.element.parent().get(0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36119, + "nodeLength": 4, + "src": "n && o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36324, + "nodeLength": 9, + "src": "a._helper", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36408, + "nodeLength": 9, + "src": "a._helper", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36458, + "nodeLength": 122, + "src": "i + a.size.width >= a.parentData.width && (a.size.width = a.parentData.width - i , c && (a.size.height = a.size.width / a.aspectRatio , p = !1))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36458, + "nodeLength": 34, + "src": "i + a.size.width >= a.parentData.width", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36529, + "nodeLength": 50, + "src": "c && (a.size.height = a.size.width / a.aspectRatio , p = !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36581, + "nodeLength": 126, + "src": "s + a.size.height >= a.parentData.height && (a.size.height = a.parentData.height - s , c && (a.size.width = a.size.height * a.aspectRatio , p = !1))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36581, + "nodeLength": 36, + "src": "s + a.size.height >= a.parentData.height", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36656, + "nodeLength": 50, + "src": "c && (a.size.width = a.size.height * a.aspectRatio , p = !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36708, + "nodeLength": 136, + "src": "p || (a.position.left = a.prevPosition.left , a.position.top = a.prevPosition.top , a.size.width = a.prevSize.width , a.size.height = a.prevSize.height)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37070, + "nodeLength": 116, + "src": "e._helper && !i.animate && /relative/.test(o.css(\"position\")) && t(this).css({\n left: r.left - n.left - s.left, \n width: h, \n height: l})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37081, + "nodeLength": 105, + "src": "!i.animate && /relative/.test(o.css(\"position\")) && t(this).css({\n left: r.left - n.left - s.left, \n width: h, \n height: l})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37093, + "nodeLength": 93, + "src": "/relative/.test(o.css(\"position\")) && t(this).css({\n left: r.left - n.left - s.left, \n width: h, \n height: l})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37187, + "nodeLength": 114, + "src": "e._helper && !i.animate && /static/.test(o.css(\"position\")) && t(this).css({\n left: r.left - n.left - s.left, \n width: h, \n height: l})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37198, + "nodeLength": 103, + "src": "!i.animate && /static/.test(o.css(\"position\")) && t(this).css({\n left: r.left - n.left - s.left, \n width: h, \n height: l})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37210, + "nodeLength": 91, + "src": "/static/.test(o.css(\"position\")) && t(this).css({\n left: r.left - n.left - s.left, \n width: h, \n height: l})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37732, + "nodeLength": 25, + "src": "s.size.height - o.height || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37764, + "nodeLength": 23, + "src": "s.size.width - o.width || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37792, + "nodeLength": 23, + "src": "s.position.top - a.top || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37821, + "nodeLength": 25, + "src": "s.position.left - a.left || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37943, + "nodeLength": 38, + "src": "e.parents(i.originalElement[0]).length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38063, + "nodeLength": 7, + "src": "s[e] || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38073, + "nodeLength": 7, + "src": "r[e] || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38082, + "nodeLength": 23, + "src": "i && i >= 0 && (n[e] = i || null)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38085, + "nodeLength": 20, + "src": "i >= 0 && (n[e] = i || null)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38085, + "nodeLength": 4, + "src": "i >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38097, + "nodeLength": 7, + "src": "i || null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38475, + "nodeLength": 91, + "src": "t.uiBackCompat !== !1 && \"string\" == typeof e.options.ghost && e.ghost.addClass(this.options.ghost)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38475, + "nodeLength": 19, + "src": "t.uiBackCompat !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38496, + "nodeLength": 70, + "src": "\"string\" == typeof e.options.ghost && e.ghost.addClass(this.options.ghost)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38496, + "nodeLength": 32, + "src": "\"string\" == typeof e.options.ghost", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38649, + "nodeLength": 83, + "src": "e.ghost && e.ghost.css({\n position: \"relative\", \n height: e.size.height, \n width: e.size.width})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38786, + "nodeLength": 62, + "src": "e.ghost && e.helper && e.helper.get(0).removeChild(e.ghost.get(0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38795, + "nodeLength": 53, + "src": "e.helper && e.helper.get(0).removeChild(e.ghost.get(0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39014, + "nodeLength": 23, + "src": "\"number\" == typeof s.grid", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39063, + "nodeLength": 7, + "src": "h[0] || 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39073, + "nodeLength": 7, + "src": "h[1] || 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39182, + "nodeLength": 24, + "src": "s.maxWidth && p > s.maxWidth", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39194, + "nodeLength": 12, + "src": "p > s.maxWidth", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39209, + "nodeLength": 26, + "src": "s.maxHeight && f > s.maxHeight", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39222, + "nodeLength": 13, + "src": "f > s.maxHeight", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39238, + "nodeLength": 24, + "src": "s.minWidth && s.minWidth > p", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39250, + "nodeLength": 12, + "src": "s.minWidth > p", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39265, + "nodeLength": 26, + "src": "s.minHeight && s.minHeight > f", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39278, + "nodeLength": 13, + "src": "s.minHeight > f", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39301, + "nodeLength": 9, + "src": "_ && (p += l)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39311, + "nodeLength": 9, + "src": "v && (f += c)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39321, + "nodeLength": 9, + "src": "g && (p -= l)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39331, + "nodeLength": 9, + "src": "m && (f -= c)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39341, + "nodeLength": 20, + "src": "/^(se|s|e)$/.test(r)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39395, + "nodeLength": 16, + "src": "/^(ne)$/.test(r)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39468, + "nodeLength": 16, + "src": "/^(sw)$/.test(r)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39545, + "nodeLength": 60, + "src": "(0 >= f - c || 0 >= p - l) && (e = i._getPaddingPlusBorderDimensions(this))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39545, + "nodeLength": 14, + "src": "0 >= f - c || 0 >= p - l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39545, + "nodeLength": 6, + "src": "0 >= f - c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39553, + "nodeLength": 6, + "src": "0 >= p - l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39606, + "nodeLength": 5, + "src": "f - c > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39716, + "nodeLength": 5, + "src": "p - l > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40256, + "nodeLength": 31, + "src": "0 > i && t(this).css(\"top\", e.top - i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40256, + "nodeLength": 3, + "src": "0 > i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41033, + "nodeLength": 91, + "src": "null == this.options.title && null != this.originalTitle && (this.options.title = this.originalTitle)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41033, + "nodeLength": 24, + "src": "null == this.options.title", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41059, + "nodeLength": 65, + "src": "null != this.originalTitle && (this.options.title = this.originalTitle)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41059, + "nodeLength": 24, + "src": "null != this.originalTitle", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41125, + "nodeLength": 49, + "src": "this.options.disabled && (this.options.disabled = !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41365, + "nodeLength": 61, + "src": "this.options.draggable && t.fn.draggable && this._makeDraggable()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41389, + "nodeLength": 37, + "src": "t.fn.draggable && this._makeDraggable()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41427, + "nodeLength": 61, + "src": "this.options.resizable && t.fn.resizable && this._makeResizable()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41451, + "nodeLength": 37, + "src": "t.fn.resizable && this._makeResizable()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41542, + "nodeLength": 34, + "src": "this.options.autoOpen && this.open()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41634, + "nodeLength": 25, + "src": "e && (e.jquery || e.nodeType)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41638, + "nodeLength": 20, + "src": "e.jquery || e.nodeType", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41684, + "nodeLength": 9, + "src": "e || \"body\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41883, + "nodeLength": 65, + "src": "this.originalTitle && this.element.attr(\"title\", this.originalTitle)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41983, + "nodeLength": 32, + "src": "t.length && t[0] !== this.element[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41993, + "nodeLength": 22, + "src": "t[0] !== this.element[0]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "11": [ + null, + { + "position": 2302, + "nodeLength": 333, + "src": "this._isOpen && this._trigger(\"beforeClose\", e) !== !1 && (this._isOpen = !1 , this._focusedElement = null , this._destroyOverlay() , this._untrackInstance() , this.opener.filter(\":focusable\").trigger(\"focus\").length || t.ui.safeBlur(t.ui.safeActiveElement(this.document[0])) , this._hide(this.uiDialog, this.options.hide, function() {\n i._trigger(\"close\", e);\n}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2316, + "nodeLength": 319, + "src": "this._trigger(\"beforeClose\", e) !== !1 && (this._isOpen = !1 , this._focusedElement = null , this._destroyOverlay() , this._untrackInstance() , this.opener.filter(\":focusable\").trigger(\"focus\").length || t.ui.safeBlur(t.ui.safeActiveElement(this.document[0])) , this._hide(this.uiDialog, this.options.hide, function() {\n i._trigger(\"close\", e);\n}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2316, + "nodeLength": 35, + "src": "this._trigger(\"beforeClose\", e) !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2443, + "nodeLength": 113, + "src": "this.opener.filter(\":focusable\").trigger(\"focus\").length || t.ui.safeBlur(t.ui.safeActiveElement(this.document[0]))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2881, + "nodeLength": 73, + "src": "o >= +this.uiDialog.css(\"z-index\") && (this.uiDialog.css(\"z-index\", o + 1) , s = !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2881, + "nodeLength": 32, + "src": "o >= +this.uiDialog.css(\"z-index\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2955, + "nodeLength": 31, + "src": "s && !i && this._trigger(\"focus\", e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2958, + "nodeLength": 28, + "src": "!i && this._trigger(\"focus\", e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3024, + "nodeLength": 12, + "src": "this._isOpen", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3038, + "nodeLength": 40, + "src": "this._moveToTop() && this._focusTabbable()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3237, + "nodeLength": 72, + "src": "this.overlay && this.overlay.css(\"z-index\", this.uiDialog.css(\"z-index\") - 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3513, + "nodeLength": 39, + "src": "t || (t = this.element.find(\"[autofocus]\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3553, + "nodeLength": 44, + "src": "t.length || (t = this.element.find(\":tabbable\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3598, + "nodeLength": 55, + "src": "t.length || (t = this.uiDialogButtonPane.find(\":tabbable\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3654, + "nodeLength": 60, + "src": "t.length || (t = this.uiDialogTitlebarClose.filter(\":tabbable\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3715, + "nodeLength": 27, + "src": "t.length || (t = this.uiDialog)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3854, + "nodeLength": 52, + "src": "this.uiDialog[0] === e || t.contains(this.uiDialog[0], e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3854, + "nodeLength": 20, + "src": "this.uiDialog[0] === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3907, + "nodeLength": 24, + "src": "i || this._focusTabbable()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4227, + "nodeLength": 95, + "src": "this.options.closeOnEscape && !e.isDefaultPrevented() && e.keyCode && e.keyCode === t.ui.keyCode.ESCAPE", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4255, + "nodeLength": 67, + "src": "!e.isDefaultPrevented() && e.keyCode && e.keyCode === t.ui.keyCode.ESCAPE", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4280, + "nodeLength": 42, + "src": "e.keyCode && e.keyCode === t.ui.keyCode.ESCAPE", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4291, + "nodeLength": 31, + "src": "e.keyCode === t.ui.keyCode.ESCAPE", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4373, + "nodeLength": 53, + "src": "e.keyCode === t.ui.keyCode.TAB && !e.isDefaultPrevented()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4373, + "nodeLength": 28, + "src": "e.keyCode === t.ui.keyCode.TAB", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4507, + "nodeLength": 56, + "src": "e.target !== n[0] && e.target !== this.uiDialog[0] || e.shiftKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4507, + "nodeLength": 44, + "src": "e.target !== n[0] && e.target !== this.uiDialog[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4507, + "nodeLength": 15, + "src": "e.target !== n[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4524, + "nodeLength": 27, + "src": "e.target !== this.uiDialog[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4564, + "nodeLength": 123, + "src": "e.target !== s[0] && e.target !== this.uiDialog[0] || !e.shiftKey || (this._delay(function() {\n n.trigger(\"focus\");\n}) , e.preventDefault())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4564, + "nodeLength": 44, + "src": "e.target !== s[0] && e.target !== this.uiDialog[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4564, + "nodeLength": 15, + "src": "e.target !== s[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4581, + "nodeLength": 27, + "src": "e.target !== this.uiDialog[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4610, + "nodeLength": 77, + "src": "!e.shiftKey || (this._delay(function() {\n n.trigger(\"focus\");\n}) , e.preventDefault())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4777, + "nodeLength": 41, + "src": "this._moveToTop(t) && this._focusTabbable()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4822, + "nodeLength": 123, + "src": "this.element.find(\"[aria-describedby]\").length || this.uiDialog.attr({\n \"aria-describedby\": this.element.uniqueId().attr(\"id\")})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5164, + "nodeLength": 80, + "src": "t(e.target).closest(\".ui-dialog-titlebar-close\") || this.uiDialog.trigger(\"focus\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5834, + "nodeLength": 18, + "src": "this.options.title", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6329, + "nodeLength": 43, + "src": "t.isEmptyObject(i) || t.isArray(i) && !i.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6349, + "nodeLength": 23, + "src": "t.isArray(i) && !i.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6469, + "nodeLength": 15, + "src": "t.isFunction(s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6721, + "nodeLength": 39, + "src": "\"boolean\" == typeof s.text && delete s.text", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6721, + "nodeLength": 24, + "src": "\"boolean\" == typeof s.text", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7522, + "nodeLength": 4, + "src": "a >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7548, + "nodeLength": 4, + "src": "r >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7892, + "nodeLength": 18, + "src": "\"string\" == typeof n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8500, + "nodeLength": 4, + "src": "r >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8526, + "nodeLength": 4, + "src": "h >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8973, + "nodeLength": 21, + "src": "-1 !== i && e.splice(i, 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8973, + "nodeLength": 6, + "src": "-1 !== i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9081, + "nodeLength": 53, + "src": "t || (t = [] , this.document.data(\"ui-dialog-instances\", t))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9185, + "nodeLength": 17, + "src": "\"auto\" === t.height", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9303, + "nodeLength": 23, + "src": "t || this.uiDialog.show()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9373, + "nodeLength": 23, + "src": "t || this.uiDialog.hide()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9484, + "nodeLength": 33, + "src": "t in i.sizeRelatedOptions && (s = !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9518, + "nodeLength": 40, + "src": "t in i.resizableRelatedOptions && (n[t] = e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9561, + "nodeLength": 34, + "src": "s && (this._size() , this._position())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9596, + "nodeLength": 76, + "src": "this.uiDialog.is(\":data(ui-resizable)\") && this.uiDialog.resizable(\"option\", n)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9723, + "nodeLength": 619, + "src": "\"disabled\" !== e && (this._super(e, i) , \"appendTo\" === e && this.uiDialog.appendTo(this._appendTo()) , \"buttons\" === e && this._createButtons() , \"closeText\" === e && this.uiDialogTitlebarClose.button({\n label: t(\"\").text(\"\" + this.options.closeText).html()}) , \"draggable\" === e && (s = o.is(\":data(ui-draggable)\") , s && !i && o.draggable(\"destroy\") , !s && i && this._makeDraggable()) , \"position\" === e && this._position() , \"resizable\" === e && (n = o.is(\":data(ui-resizable)\") , n && !i && o.resizable(\"destroy\") , n && \"string\" == typeof i && o.resizable(\"option\", \"handles\", i) , n || i === !1 || this._makeResizable()) , \"title\" === e && this._title(this.uiDialogTitlebar.find(\".ui-dialog-title\")))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9723, + "nodeLength": 14, + "src": "\"disabled\" !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9757, + "nodeLength": 56, + "src": "\"appendTo\" === e && this.uiDialog.appendTo(this._appendTo())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9757, + "nodeLength": 14, + "src": "\"appendTo\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9814, + "nodeLength": 36, + "src": "\"buttons\" === e && this._createButtons()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9814, + "nodeLength": 13, + "src": "\"buttons\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9851, + "nodeLength": 107, + "src": "\"closeText\" === e && this.uiDialogTitlebarClose.button({\n label: t(\"\").text(\"\" + this.options.closeText).html()})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9851, + "nodeLength": 15, + "src": "\"closeText\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9959, + "nodeLength": 107, + "src": "\"draggable\" === e && (s = o.is(\":data(ui-draggable)\") , s && !i && o.draggable(\"destroy\") , !s && i && this._makeDraggable())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9959, + "nodeLength": 15, + "src": "\"draggable\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10007, + "nodeLength": 29, + "src": "s && !i && o.draggable(\"destroy\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10010, + "nodeLength": 26, + "src": "!i && o.draggable(\"destroy\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10037, + "nodeLength": 28, + "src": "!s && i && this._makeDraggable()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10041, + "nodeLength": 24, + "src": "i && this._makeDraggable()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10067, + "nodeLength": 32, + "src": "\"position\" === e && this._position()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10067, + "nodeLength": 14, + "src": "\"position\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10100, + "nodeLength": 168, + "src": "\"resizable\" === e && (n = o.is(\":data(ui-resizable)\") , n && !i && o.resizable(\"destroy\") , n && \"string\" == typeof i && o.resizable(\"option\", \"handles\", i) , n || i === !1 || this._makeResizable())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10100, + "nodeLength": 15, + "src": "\"resizable\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10148, + "nodeLength": 29, + "src": "n && !i && o.resizable(\"destroy\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10151, + "nodeLength": 26, + "src": "!i && o.resizable(\"destroy\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10178, + "nodeLength": 56, + "src": "n && \"string\" == typeof i && o.resizable(\"option\", \"handles\", i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10181, + "nodeLength": 53, + "src": "\"string\" == typeof i && o.resizable(\"option\", \"handles\", i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10181, + "nodeLength": 18, + "src": "\"string\" == typeof i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10235, + "nodeLength": 32, + "src": "n || i === !1 || this._makeResizable()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10238, + "nodeLength": 29, + "src": "i === !1 || this._makeResizable()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10238, + "nodeLength": 6, + "src": "i === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10269, + "nodeLength": 72, + "src": "\"title\" === e && this._title(this.uiDialogTitlebar.find(\".ui-dialog-title\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10269, + "nodeLength": 11, + "src": "\"title\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10464, + "nodeLength": 40, + "src": "s.minWidth > s.width && (s.width = s.minWidth)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10464, + "nodeLength": 18, + "src": "s.minWidth > s.width", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10600, + "nodeLength": 28, + "src": "\"number\" == typeof s.maxHeight", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10662, + "nodeLength": 17, + "src": "\"auto\" === s.height", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10782, + "nodeLength": 104, + "src": "this.uiDialog.is(\":data(ui-resizable)\") && this.uiDialog.resizable(\"option\", \"minHeight\", this._minHeight())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11149, + "nodeLength": 72, + "src": "this.iframeBlocks && (this.iframeBlocks.remove() , delete this.iframeBlocks)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11260, + "nodeLength": 40, + "src": "t(e.target).closest(\".ui-dialog\").length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11381, + "nodeLength": 18, + "src": "this.options.modal", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11440, + "nodeLength": 185, + "src": "this.document.data(\"ui-dialog-overlays\") || this._on(this.document, {\n focusin: function(t) {\n e || this._allowInteraction(t) || (t.preventDefault() , this._trackingInstances()[0]._focusTabbable());\n}})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11526, + "nodeLength": 96, + "src": "e || this._allowInteraction(t) || (t.preventDefault() , this._trackingInstances()[0]._focusTabbable())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11529, + "nodeLength": 93, + "src": "this._allowInteraction(t) || (t.preventDefault() , this._trackingInstances()[0]._focusTabbable())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11829, + "nodeLength": 43, + "src": "this.document.data(\"ui-dialog-overlays\") || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11909, + "nodeLength": 32, + "src": "this.options.modal && this.overlay", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11992, + "nodeLength": 1, + "src": "t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52032, + "nodeLength": 308, + "src": "t.uiBackCompat !== !1 && t.widget(\"ui.dialog\", t.ui.dialog, {\n options: {\n dialogClass: \"\"}, \n _createWrapper: function() {\n this._super() , this.uiDialog.addClass(this.options.dialogClass);\n}, \n _setOption: function(t, e) {\n \"dialogClass\" === t && this.uiDialog.removeClass(this.options.dialogClass).addClass(e) , this._superApply(arguments);\n}})", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 52033, + "nodeLength": 19, + "src": "t.uiBackCompat !== !1", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 194, + "nodeLength": 82, + "src": "\"dialogClass\" === t && this.uiDialog.removeClass(this.options.dialogClass).addClass(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 194, + "nodeLength": 17, + "src": "\"dialogClass\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52654, + "nodeLength": 15, + "src": "t.isFunction(s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52735, + "nodeLength": 16, + "src": "arguments.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52776, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52883, + "nodeLength": 44, + "src": "i.addClasses && this._addClass(\"ui-droppable\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52984, + "nodeLength": 32, + "src": "t.ui.ddmanager.droppables[e] || []", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53090, + "nodeLength": 10, + "src": "t.length > e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53105, + "nodeLength": 26, + "src": "t[e] === this && t.splice(e, 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53105, + "nodeLength": 11, + "src": "t[e] === this", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53250, + "nodeLength": 12, + "src": "\"accept\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53275, + "nodeLength": 15, + "src": "t.isFunction(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53329, + "nodeLength": 11, + "src": "\"scope\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53524, + "nodeLength": 41, + "src": "i && this._trigger(\"activate\", e, this.ui(i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53646, + "nodeLength": 43, + "src": "i && this._trigger(\"deactivate\", e, this.ui(i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53738, + "nodeLength": 170, + "src": "i && (i.currentItem || i.element)[0] !== this.element[0] && this.accept.call(this.element[0], i.currentItem || i.element) && (this._addHoverClass() , this._trigger(\"over\", e, this.ui(i)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53742, + "nodeLength": 166, + "src": "(i.currentItem || i.element)[0] !== this.element[0] && this.accept.call(this.element[0], i.currentItem || i.element) && (this._addHoverClass() , this._trigger(\"over\", e, this.ui(i)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53742, + "nodeLength": 46, + "src": "(i.currentItem || i.element)[0] !== this.element[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53742, + "nodeLength": 24, + "src": "i.currentItem || i.element", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53790, + "nodeLength": 118, + "src": "this.accept.call(this.element[0], i.currentItem || i.element) && (this._addHoverClass() , this._trigger(\"over\", e, this.ui(i)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53823, + "nodeLength": 24, + "src": "i.currentItem || i.element", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53956, + "nodeLength": 172, + "src": "i && (i.currentItem || i.element)[0] !== this.element[0] && this.accept.call(this.element[0], i.currentItem || i.element) && (this._removeHoverClass() , this._trigger(\"out\", e, this.ui(i)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53960, + "nodeLength": 168, + "src": "(i.currentItem || i.element)[0] !== this.element[0] && this.accept.call(this.element[0], i.currentItem || i.element) && (this._removeHoverClass() , this._trigger(\"out\", e, this.ui(i)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53960, + "nodeLength": 46, + "src": "(i.currentItem || i.element)[0] !== this.element[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53960, + "nodeLength": 24, + "src": "i.currentItem || i.element", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54008, + "nodeLength": 120, + "src": "this.accept.call(this.element[0], i.currentItem || i.element) && (this._removeHoverClass() , this._trigger(\"out\", e, this.ui(i)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54041, + "nodeLength": 24, + "src": "i.currentItem || i.element", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54156, + "nodeLength": 25, + "src": "i || t.ui.ddmanager.current", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54194, + "nodeLength": 50, + "src": "s && (s.currentItem || s.element)[0] !== this.element[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54198, + "nodeLength": 46, + "src": "(s.currentItem || s.element)[0] !== this.element[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54198, + "nodeLength": 24, + "src": "s.currentItem || s.element", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54376, + "nodeLength": 194, + "src": "i.options.greedy && !i.options.disabled && i.options.scope === s.options.scope && i.accept.call(i.element[0], s.currentItem || s.element) && v(s, t.extend(i, {\n offset: i.element.offset()}), i.options.tolerance, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54394, + "nodeLength": 176, + "src": "!i.options.disabled && i.options.scope === s.options.scope && i.accept.call(i.element[0], s.currentItem || s.element) && v(s, t.extend(i, {\n offset: i.element.offset()}), i.options.tolerance, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54415, + "nodeLength": 155, + "src": "i.options.scope === s.options.scope && i.accept.call(i.element[0], s.currentItem || s.element) && v(s, t.extend(i, {\n offset: i.element.offset()}), i.options.tolerance, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54415, + "nodeLength": 33, + "src": "i.options.scope === s.options.scope", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54450, + "nodeLength": 120, + "src": "i.accept.call(i.element[0], s.currentItem || s.element) && v(s, t.extend(i, {\n offset: i.element.offset()}), i.options.tolerance, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54477, + "nodeLength": 24, + "src": "s.currentItem || s.element", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54590, + "nodeLength": 1, + "src": "n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54595, + "nodeLength": 58, + "src": "this.accept.call(this.element[0], s.currentItem || s.element)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54628, + "nodeLength": 24, + "src": "s.currentItem || s.element", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54795, + "nodeLength": 24, + "src": "t.currentItem || t.element", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 173763, + "nodeLength": 11, + "src": "t >= e && e + i > t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 173763, + "nodeLength": 4, + "src": "t >= e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 173769, + "nodeLength": 5, + "src": "e + i > t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 173803, + "nodeLength": 9, + "src": "!i.offset", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 173829, + "nodeLength": 34, + "src": "e.positionAbs || e.position.absolute", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 173888, + "nodeLength": 34, + "src": "e.positionAbs || e.position.absolute", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174114, + "nodeLength": 22, + "src": "o >= l && u >= r && a >= c && d >= h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174114, + "nodeLength": 4, + "src": "o >= l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174120, + "nodeLength": 16, + "src": "u >= r && a >= c && d >= h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174120, + "nodeLength": 4, + "src": "u >= r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174126, + "nodeLength": 10, + "src": "a >= c && d >= h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174126, + "nodeLength": 4, + "src": "a >= c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174132, + "nodeLength": 4, + "src": "d >= h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174160, + "nodeLength": 132, + "src": "o + e.helperProportions.width / 2 > l && u > r - e.helperProportions.width / 2 && a + e.helperProportions.height / 2 > c && d > h - e.helperProportions.height / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174160, + "nodeLength": 31, + "src": "o + e.helperProportions.width / 2 > l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174193, + "nodeLength": 99, + "src": "u > r - e.helperProportions.width / 2 && a + e.helperProportions.height / 2 > c && d > h - e.helperProportions.height / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174193, + "nodeLength": 31, + "src": "u > r - e.helperProportions.width / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174226, + "nodeLength": 66, + "src": "a + e.helperProportions.height / 2 > c && d > h - e.helperProportions.height / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174226, + "nodeLength": 32, + "src": "a + e.helperProportions.height / 2 > c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174260, + "nodeLength": 32, + "src": "d > h - e.helperProportions.height / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174314, + "nodeLength": 71, + "src": "t(n.pageY, c, i.proportions().height) && t(n.pageX, l, i.proportions().width)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174405, + "nodeLength": 69, + "src": "(a >= c && d >= a || h >= c && d >= h || c > a && h > d) && (o >= l && u >= o || r >= l && u >= r || l > o && r > u)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174405, + "nodeLength": 32, + "src": "a >= c && d >= a || h >= c && d >= h || c > a && h > d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174405, + "nodeLength": 10, + "src": "a >= c && d >= a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174405, + "nodeLength": 4, + "src": "a >= c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174411, + "nodeLength": 4, + "src": "d >= a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174417, + "nodeLength": 20, + "src": "h >= c && d >= h || c > a && h > d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174417, + "nodeLength": 10, + "src": "h >= c && d >= h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174417, + "nodeLength": 4, + "src": "h >= c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174423, + "nodeLength": 4, + "src": "d >= h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174429, + "nodeLength": 8, + "src": "c > a && h > d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174429, + "nodeLength": 3, + "src": "c > a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174434, + "nodeLength": 3, + "src": "h > d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174441, + "nodeLength": 32, + "src": "o >= l && u >= o || r >= l && u >= r || l > o && r > u", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174441, + "nodeLength": 10, + "src": "o >= l && u >= o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174441, + "nodeLength": 4, + "src": "o >= l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174447, + "nodeLength": 4, + "src": "u >= o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174453, + "nodeLength": 20, + "src": "r >= l && u >= r || l > o && r > u", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174453, + "nodeLength": 10, + "src": "r >= l && u >= r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174453, + "nodeLength": 4, + "src": "r >= l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174459, + "nodeLength": 4, + "src": "u >= r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174465, + "nodeLength": 8, + "src": "l > o && r > u", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174465, + "nodeLength": 3, + "src": "l > o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174470, + "nodeLength": 3, + "src": "r > u", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174591, + "nodeLength": 46, + "src": "t.ui.ddmanager.droppables[e.options.scope] || []", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174640, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174657, + "nodeLength": 24, + "src": "e.currentItem || e.element", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174731, + "nodeLength": 10, + "src": "o.length > s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174749, + "nodeLength": 88, + "src": "!(o[s].options.disabled || e && !o[s].accept.call(o[s].element[0], e.currentItem || e.element))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174751, + "nodeLength": 85, + "src": "o[s].options.disabled || e && !o[s].accept.call(o[s].element[0], e.currentItem || e.element)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174774, + "nodeLength": 62, + "src": "e && !o[s].accept.call(o[s].element[0], e.currentItem || e.element)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174811, + "nodeLength": 24, + "src": "e.currentItem || e.element", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174847, + "nodeLength": 10, + "src": "r.length > n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174865, + "nodeLength": 22, + "src": "r[n] === o[s].element[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174941, + "nodeLength": 36, + "src": "\"none\" !== o[s].element.css(\"display\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174978, + "nodeLength": 184, + "src": "o[s].visible && (\"mousedown\" === a && o[s]._activate.call(o[s], i) , o[s].offset = o[s].element.offset() , o[s].proportions({\n width: o[s].element[0].offsetWidth, \n height: o[s].element[0].offsetHeight}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174993, + "nodeLength": 44, + "src": "\"mousedown\" === a && o[s]._activate.call(o[s], i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 174993, + "nodeLength": 15, + "src": "\"mousedown\" === a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175208, + "nodeLength": 46, + "src": "t.ui.ddmanager.droppables[e.options.scope] || []", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175275, + "nodeLength": 279, + "src": "this.options && (!this.options.disabled && this.visible && v(e, this, this.options.tolerance, i) && (s = this._drop.call(this, i) || s) , !this.options.disabled && this.visible && this.accept.call(this.element[0], e.currentItem || e.element) && (this.isout = !0 , this.isover = !1 , this._deactivate.call(this, i)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175290, + "nodeLength": 104, + "src": "!this.options.disabled && this.visible && v(e, this, this.options.tolerance, i) && (s = this._drop.call(this, i) || s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175314, + "nodeLength": 80, + "src": "this.visible && v(e, this, this.options.tolerance, i) && (s = this._drop.call(this, i) || s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175328, + "nodeLength": 66, + "src": "v(e, this, this.options.tolerance, i) && (s = this._drop.call(this, i) || s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175367, + "nodeLength": 26, + "src": "this._drop.call(this, i) || s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175395, + "nodeLength": 158, + "src": "!this.options.disabled && this.visible && this.accept.call(this.element[0], e.currentItem || e.element) && (this.isout = !0 , this.isover = !1 , this._deactivate.call(this, i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175419, + "nodeLength": 134, + "src": "this.visible && this.accept.call(this.element[0], e.currentItem || e.element) && (this.isout = !0 , this.isover = !1 , this._deactivate.call(this, i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175433, + "nodeLength": 120, + "src": "this.accept.call(this.element[0], e.currentItem || e.element) && (this.isout = !0 , this.isover = !1 , this._deactivate.call(this, i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175466, + "nodeLength": 24, + "src": "e.currentItem || e.element", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175648, + "nodeLength": 62, + "src": "e.options.refreshPositions || t.ui.ddmanager.prepareOffsets(e, i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175733, + "nodeLength": 62, + "src": "e.options.refreshPositions && t.ui.ddmanager.prepareOffsets(e, i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175803, + "nodeLength": 46, + "src": "t.ui.ddmanager.droppables[e.options.scope] || []", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175864, + "nodeLength": 55, + "src": "!this.options.disabled && !this.greedyChild && this.visible", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175888, + "nodeLength": 31, + "src": "!this.greedyChild && this.visible", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175970, + "nodeLength": 15, + "src": "!a && this.isover", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 175994, + "nodeLength": 15, + "src": "a && !this.isover", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 176024, + "nodeLength": 454, + "src": "r && (this.options.greedy && (n = this.options.scope , o = this.element.parents(\":data(ui-droppable)\").filter(function() {\n return t(this).droppable(\"instance\").options.scope === n;\n}) , o.length && (s = t(o[0]).droppable(\"instance\") , s.greedyChild = \"isover\" === r)) , s && \"isover\" === r && (s.isover = !1 , s.isout = !0 , s._out.call(s, i)) , this[r] = !0 , this[\"isout\" === r ? \"isover\" : \"isout\"] = !1 , this[\"isover\" === r ? \"_over\" : \"_out\"].call(this, i) , s && \"isout\" === r && (s.isout = !1 , s.isover = !0 , s._over.call(s, i)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 176028, + "nodeLength": 235, + "src": "this.options.greedy && (n = this.options.scope , o = this.element.parents(\":data(ui-droppable)\").filter(function() {\n return t(this).droppable(\"instance\").options.scope === n;\n}) , o.length && (s = t(o[0]).droppable(\"instance\") , s.greedyChild = \"isover\" === r))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 176142, + "nodeLength": 47, + "src": "t(this).droppable(\"instance\").options.scope === n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 176192, + "nodeLength": 70, + "src": "o.length && (s = t(o[0]).droppable(\"instance\") , s.greedyChild = \"isover\" === r)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 176249, + "nodeLength": 12, + "src": "\"isover\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 176264, + "nodeLength": 58, + "src": "s && \"isover\" === r && (s.isover = !1 , s.isout = !0 , s._out.call(s, i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 176267, + "nodeLength": 55, + "src": "\"isover\" === r && (s.isover = !1 , s.isout = !0 , s._out.call(s, i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 176267, + "nodeLength": 12, + "src": "\"isover\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 176339, + "nodeLength": 11, + "src": "\"isout\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 176377, + "nodeLength": 12, + "src": "\"isover\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 176419, + "nodeLength": 58, + "src": "s && \"isout\" === r && (s.isout = !1 , s.isover = !0 , s._over.call(s, i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 176422, + "nodeLength": 55, + "src": "\"isout\" === r && (s.isout = !1 , s.isover = !0 , s._over.call(s, i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 176422, + "nodeLength": 11, + "src": "\"isout\" === r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 176561, + "nodeLength": 62, + "src": "e.options.refreshPositions || t.ui.ddmanager.prepareOffsets(e, i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 176626, + "nodeLength": 571, + "src": "t.uiBackCompat !== !1 && t.widget(\"ui.droppable\", t.ui.droppable, {\n options: {\n hoverClass: !1, \n activeClass: !1}, \n _addActiveClass: function() {\n this._super() , this.options.activeClass && this.element.addClass(this.options.activeClass);\n}, \n _removeActiveClass: function() {\n this._super() , this.options.activeClass && this.element.removeClass(this.options.activeClass);\n}, \n _addHoverClass: function() {\n this._super() , this.options.hoverClass && this.element.addClass(this.options.hoverClass);\n}, \n _removeHoverClass: function() {\n this._super() , this.options.hoverClass && this.element.removeClass(this.options.hoverClass);\n}})", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 176626, + "nodeLength": 19, + "src": "t.uiBackCompat !== !1", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 176767, + "nodeLength": 73, + "src": "this.options.activeClass && this.element.addClass(this.options.activeClass)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 176886, + "nodeLength": 76, + "src": "this.options.activeClass && this.element.removeClass(this.options.activeClass)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 177004, + "nodeLength": 71, + "src": "this.options.hoverClass && this.element.addClass(this.options.hoverClass)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 177120, + "nodeLength": 74, + "src": "this.options.hoverClass && this.element.removeClass(this.options.hoverClass)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 177932, + "nodeLength": 10, + "src": "void 0 === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 178075, + "nodeLength": 34, + "src": "void 0 === t && (t = this.options.value)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 178075, + "nodeLength": 10, + "src": "void 0 === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 178129, + "nodeLength": 6, + "src": "t === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 178136, + "nodeLength": 25, + "src": "\"number\" != typeof t && (t = 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 178136, + "nodeLength": 18, + "src": "\"number\" != typeof t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 178162, + "nodeLength": 18, + "src": "this.indeterminate", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 178393, + "nodeLength": 35, + "src": "\"max\" === t && (e = Math.max(this.min, e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 178393, + "nodeLength": 9, + "src": "\"max\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 178609, + "nodeLength": 18, + "src": "this.indeterminate", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 178787, + "nodeLength": 30, + "src": "this.indeterminate || e > this.min", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41, + "nodeLength": 10, + "src": "e > this.min", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 178906, + "nodeLength": 20, + "src": "e === this.options.max", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 178997, + "nodeLength": 18, + "src": "this.indeterminate", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 179058, + "nodeLength": 126, + "src": "this.overlayDiv || (this.overlayDiv = t(\"
\").appendTo(this.valueDiv) , this._addClass(this.overlayDiv, \"ui-progressbar-overlay\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 179259, + "nodeLength": 64, + "src": "this.overlayDiv && (this.overlayDiv.remove() , this.overlayDiv = null)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 179325, + "nodeLength": 60, + "src": "this.oldValue !== e && (this.oldValue = e , this._trigger(\"change\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 179325, + "nodeLength": 17, + "src": "this.oldValue !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 179386, + "nodeLength": 47, + "src": "e === this.options.max && this._trigger(\"complete\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 179386, + "nodeLength": 20, + "src": "e === this.options.max", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 180581, + "nodeLength": 952, + "src": "this.options.disabled || (this.selectees = t(s.filter, this.element[0]) , this._trigger(\"start\", e) , t(s.appendTo).append(this.helper) , this.helper.css({\n left: e.pageX, \n top: e.pageY, \n width: 0, \n height: 0}) , s.autoRefresh && this.refresh() , this.selectees.filter(\".ui-selected\").each(function() {\n var s = t.data(this, \"selectable-item\");\n s.startselected = !0 , e.metaKey || e.ctrlKey || (i._removeClass(s.$element, \"ui-selected\") , s.selected = !1 , i._addClass(s.$element, \"ui-unselecting\") , s.unselecting = !0 , i._trigger(\"unselecting\", e, {\n unselecting: s.element}));\n}) , t(e.target).parents().addBack().each(function() {\n var s, n = t.data(this, \"selectable-item\");\n return n ? (s = !e.metaKey && !e.ctrlKey || !n.$element.hasClass(\"ui-selected\") , i._removeClass(n.$element, s ? \"ui-unselecting\" : \"ui-selected\")._addClass(n.$element, s ? \"ui-selecting\" : \"ui-unselecting\") , n.unselecting = !s , n.selecting = s , n.selected = s , s ? i._trigger(\"selecting\", e, {\n selecting: n.element}) : i._trigger(\"unselecting\", e, {\n unselecting: n.element}) , !1) : void 0;\n}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 180768, + "nodeLength": 29, + "src": "s.autoRefresh && this.refresh()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 180908, + "nodeLength": 188, + "src": "e.metaKey || e.ctrlKey || (i._removeClass(s.$element, \"ui-selected\") , s.selected = !1 , i._addClass(s.$element, \"ui-unselecting\") , s.unselecting = !0 , i._trigger(\"unselecting\", e, {\n unselecting: s.element}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 180919, + "nodeLength": 177, + "src": "e.ctrlKey || (i._removeClass(s.$element, \"ui-selected\") , s.selected = !1 , i._addClass(s.$element, \"ui-unselecting\") , s.unselecting = !0 , i._trigger(\"unselecting\", e, {\n unselecting: s.element}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 181193, + "nodeLength": 1, + "src": "n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 181198, + "nodeLength": 59, + "src": "!e.metaKey && !e.ctrlKey || !n.$element.hasClass(\"ui-selected\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 181198, + "nodeLength": 22, + "src": "!e.metaKey && !e.ctrlKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 181284, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 181339, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 181418, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 181561, + "nodeLength": 38, + "src": "this.dragged = !0 , !this.options.disabled", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 181686, + "nodeLength": 18, + "src": "o > r && (i = r , r = o , o = i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 181686, + "nodeLength": 3, + "src": "o > r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 181705, + "nodeLength": 18, + "src": "a > h && (i = h , h = a , a = i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 181705, + "nodeLength": 3, + "src": "a > h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 181855, + "nodeLength": 1138, + "src": "i && i.element !== s.element[0] && (c.left = i.left + s.elementPos.left , c.right = i.right + s.elementPos.left , c.top = i.top + s.elementPos.top , c.bottom = i.bottom + s.elementPos.top , \"touch\" === n.tolerance ? l = !(c.left > r || o > c.right || c.top > h || a > c.bottom) : \"fit\" === n.tolerance && (l = c.left > o && r > c.right && c.top > a && h > c.bottom) , l ? (i.selected && (s._removeClass(i.$element, \"ui-selected\") , i.selected = !1) , i.unselecting && (s._removeClass(i.$element, \"ui-unselecting\") , i.unselecting = !1) , i.selecting || (s._addClass(i.$element, \"ui-selecting\") , i.selecting = !0 , s._trigger(\"selecting\", e, {\n selecting: i.element}))) : (i.selecting && ((e.metaKey || e.ctrlKey) && i.startselected ? (s._removeClass(i.$element, \"ui-selecting\") , i.selecting = !1 , s._addClass(i.$element, \"ui-selected\") , i.selected = !0) : (s._removeClass(i.$element, \"ui-selecting\") , i.selecting = !1 , i.startselected && (s._addClass(i.$element, \"ui-unselecting\") , i.unselecting = !0) , s._trigger(\"unselecting\", e, {\n unselecting: i.element}))) , i.selected && (e.metaKey || e.ctrlKey || i.startselected || (s._removeClass(i.$element, \"ui-selected\") , i.selected = !1 , s._addClass(i.$element, \"ui-unselecting\") , i.unselecting = !0 , s._trigger(\"unselecting\", e, {\n unselecting: i.element})))))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 181858, + "nodeLength": 1135, + "src": "i.element !== s.element[0] && (c.left = i.left + s.elementPos.left , c.right = i.right + s.elementPos.left , c.top = i.top + s.elementPos.top , c.bottom = i.bottom + s.elementPos.top , \"touch\" === n.tolerance ? l = !(c.left > r || o > c.right || c.top > h || a > c.bottom) : \"fit\" === n.tolerance && (l = c.left > o && r > c.right && c.top > a && h > c.bottom) , l ? (i.selected && (s._removeClass(i.$element, \"ui-selected\") , i.selected = !1) , i.unselecting && (s._removeClass(i.$element, \"ui-unselecting\") , i.unselecting = !1) , i.selecting || (s._addClass(i.$element, \"ui-selecting\") , i.selecting = !0 , s._trigger(\"selecting\", e, {\n selecting: i.element}))) : (i.selecting && ((e.metaKey || e.ctrlKey) && i.startselected ? (s._removeClass(i.$element, \"ui-selecting\") , i.selecting = !1 , s._addClass(i.$element, \"ui-selected\") , i.selected = !0) : (s._removeClass(i.$element, \"ui-selecting\") , i.selecting = !1 , i.startselected && (s._addClass(i.$element, \"ui-unselecting\") , i.unselecting = !0) , s._trigger(\"unselecting\", e, {\n unselecting: i.element}))) , i.selected && (e.metaKey || e.ctrlKey || i.startselected || (s._removeClass(i.$element, \"ui-selected\") , i.selected = !1 , s._addClass(i.$element, \"ui-unselecting\") , i.unselecting = !0 , s._trigger(\"unselecting\", e, {\n unselecting: i.element})))))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 181858, + "nodeLength": 24, + "src": "i.element !== s.element[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182015, + "nodeLength": 21, + "src": "\"touch\" === n.tolerance", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182041, + "nodeLength": 40, + "src": "c.left > r || o > c.right || c.top > h || a > c.bottom", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182041, + "nodeLength": 8, + "src": "c.left > r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182051, + "nodeLength": 30, + "src": "o > c.right || c.top > h || a > c.bottom", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182051, + "nodeLength": 9, + "src": "o > c.right", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182062, + "nodeLength": 19, + "src": "c.top > h || a > c.bottom", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182062, + "nodeLength": 7, + "src": "c.top > h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182071, + "nodeLength": 10, + "src": "a > c.bottom", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182083, + "nodeLength": 65, + "src": "\"fit\" === n.tolerance && (l = c.left > o && r > c.right && c.top > a && h > c.bottom)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182083, + "nodeLength": 19, + "src": "\"fit\" === n.tolerance", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182107, + "nodeLength": 40, + "src": "c.left > o && r > c.right && c.top > a && h > c.bottom", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182107, + "nodeLength": 8, + "src": "c.left > o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182117, + "nodeLength": 30, + "src": "r > c.right && c.top > a && h > c.bottom", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182117, + "nodeLength": 9, + "src": "r > c.right", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182128, + "nodeLength": 19, + "src": "c.top > a && h > c.bottom", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182128, + "nodeLength": 7, + "src": "c.top > a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182137, + "nodeLength": 10, + "src": "h > c.bottom", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182149, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182152, + "nodeLength": 68, + "src": "i.selected && (s._removeClass(i.$element, \"ui-selected\") , i.selected = !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182221, + "nodeLength": 77, + "src": "i.unselecting && (s._removeClass(i.$element, \"ui-unselecting\") , i.unselecting = !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182299, + "nodeLength": 116, + "src": "i.selecting || (s._addClass(i.$element, \"ui-selecting\") , i.selecting = !0 , s._trigger(\"selecting\", e, {\n selecting: i.element}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182418, + "nodeLength": 353, + "src": "i.selecting && ((e.metaKey || e.ctrlKey) && i.startselected ? (s._removeClass(i.$element, \"ui-selecting\") , i.selecting = !1 , s._addClass(i.$element, \"ui-selected\") , i.selected = !0) : (s._removeClass(i.$element, \"ui-selecting\") , i.selecting = !1 , i.startselected && (s._addClass(i.$element, \"ui-unselecting\") , i.unselecting = !0) , s._trigger(\"unselecting\", e, {\n unselecting: i.element})))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182433, + "nodeLength": 38, + "src": "(e.metaKey || e.ctrlKey) && i.startselected", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182433, + "nodeLength": 20, + "src": "e.metaKey || e.ctrlKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182641, + "nodeLength": 76, + "src": "i.startselected && (s._addClass(i.$element, \"ui-unselecting\") , i.unselecting = !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182772, + "nodeLength": 219, + "src": "i.selected && (e.metaKey || e.ctrlKey || i.startselected || (s._removeClass(i.$element, \"ui-selected\") , i.selected = !1 , s._addClass(i.$element, \"ui-unselecting\") , i.unselecting = !0 , s._trigger(\"unselecting\", e, {\n unselecting: i.element})))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182785, + "nodeLength": 205, + "src": "e.metaKey || e.ctrlKey || i.startselected || (s._removeClass(i.$element, \"ui-selected\") , i.selected = !1 , s._addClass(i.$element, \"ui-unselecting\") , i.unselecting = !0 , s._trigger(\"unselecting\", e, {\n unselecting: i.element}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182796, + "nodeLength": 194, + "src": "e.ctrlKey || i.startselected || (s._removeClass(i.$element, \"ui-selected\") , i.selected = !1 , s._addClass(i.$element, \"ui-unselecting\") , i.unselecting = !0 , s._trigger(\"unselecting\", e, {\n unselecting: i.element}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 182807, + "nodeLength": 183, + "src": "i.startselected || (s._removeClass(i.$element, \"ui-selected\") , i.selected = !1 , s._addClass(i.$element, \"ui-unselecting\") , i.unselecting = !0 , s._trigger(\"unselecting\", e, {\n unselecting: i.element}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 184522, + "nodeLength": 21, + "src": "this.options.disabled", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 185022, + "nodeLength": 45, + "src": "this.options.width !== !1 && this._resizeButton()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 185022, + "nodeLength": 23, + "src": "this.options.width !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 185146, + "nodeLength": 29, + "src": "i._rendered || i._refreshMenu()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 185705, + "nodeLength": 101, + "src": "null != e.focusIndex && s.index !== e.focusIndex && (e._trigger(\"focus\", t, {\n item: s}) , e.isOpen || e._select(s, t))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 185705, + "nodeLength": 18, + "src": "null != e.focusIndex", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 185725, + "nodeLength": 81, + "src": "s.index !== e.focusIndex && (e._trigger(\"focus\", t, {\n item: s}) , e.isOpen || e._select(s, t))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 185725, + "nodeLength": 22, + "src": "s.index !== e.focusIndex", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 185781, + "nodeLength": 24, + "src": "e.isOpen || e._select(s, t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 186187, + "nodeLength": 54, + "src": "this._getSelectedItem().data(\"ui-selectmenu-item\") || {}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 186244, + "nodeLength": 47, + "src": "null === this.options.width && this._resizeButton()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 186244, + "nodeLength": 25, + "src": "null === this.options.width", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 186575, + "nodeLength": 171, + "src": "e.length && (t = this._getSelectedItem() , this.menuInstance.focus(null, t) , this._setAria(t.data(\"ui-selectmenu-item\")) , this._setOption(\"disabled\", this.element.prop(\"disabled\")))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 186765, + "nodeLength": 355, + "src": "this.options.disabled || (this._rendered ? (this._removeClass(this.menu.find(\".ui-state-active\"), null, \"ui-state-active\") , this.menuInstance.focus(null, this._getSelectedItem())) : this._refreshMenu() , this.menuItems.length && (this.isOpen = !0 , this._toggleAttr() , this._resizeMenu() , this._position() , this._on(this.document, this._documentClick) , this._trigger(\"open\", t)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 186789, + "nodeLength": 14, + "src": "this._rendered", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 186957, + "nodeLength": 162, + "src": "this.menuItems.length && (this.isOpen = !0 , this._toggleAttr() , this._resizeMenu() , this._position() , this._on(this.document, this._documentClick) , this._trigger(\"open\", t))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 187235, + "nodeLength": 114, + "src": "this.isOpen && (this.isOpen = !1 , this._toggleAttr() , this.range = null , this._off(this.document) , this._trigger(\"close\", t))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 187622, + "nodeLength": 204, + "src": "o.optgroup !== n && (a = t(\"
  • \", {\n text: o.optgroup}) , s._addClass(a, \"ui-selectmenu-optgroup\", \"ui-menu-divider\" + (o.element.parent(\"optgroup\").prop(\"disabled\") ? \" ui-state-disabled\" : \"\")) , a.appendTo(e) , n = o.optgroup)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 187622, + "nodeLength": 14, + "src": "o.optgroup !== n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 187727, + "nodeLength": 45, + "src": "o.element.parent(\"optgroup\").prop(\"disabled\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 188036, + "nodeLength": 54, + "src": "i.disabled && this._addClass(s, null, \"ui-state-disabled\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 188164, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 188240, + "nodeLength": 11, + "src": "this.isOpen", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 188400, + "nodeLength": 23, + "src": "\"first\" === t || \"last\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 188400, + "nodeLength": 11, + "src": "\"first\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 188413, + "nodeLength": 10, + "src": "\"last\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 188426, + "nodeLength": 11, + "src": "\"first\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 188489, + "nodeLength": 38, + "src": "s.length && this.menuInstance.focus(e, s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 188652, + "nodeLength": 11, + "src": "this.isOpen", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 188715, + "nodeLength": 142, + "src": "this.range && (window.getSelection ? (t = window.getSelection() , t.removeAllRanges() , t.addRange(this.range)) : this.range.select() , this.button.focus())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 188728, + "nodeLength": 19, + "src": "window.getSelection", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 188897, + "nodeLength": 119, + "src": "this.isOpen && (t(e.target).closest(\".ui-selectmenu-menu, #\" + t.ui.escapeSelector(this.ids.button)).length || this.close(e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 188911, + "nodeLength": 104, + "src": "t(e.target).closest(\".ui-selectmenu-menu, #\" + t.ui.escapeSelector(this.ids.button)).length || this.close(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 189061, + "nodeLength": 19, + "src": "window.getSelection", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 189106, + "nodeLength": 42, + "src": "t.rangeCount && (this.range = t.getRangeAt(0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 189394, + "nodeLength": 39, + "src": "this.isOpen && this._selectFocusedItem(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 189461, + "nodeLength": 8, + "src": "e.altKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 189536, + "nodeLength": 8, + "src": "e.altKey", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 189612, + "nodeLength": 11, + "src": "this.isOpen", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 189962, + "nodeLength": 21, + "src": "i && e.preventDefault()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 190071, + "nodeLength": 77, + "src": "e.hasClass(\"ui-state-disabled\") || this._select(e.data(\"ui-selectmenu-item\"), t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 190369, + "nodeLength": 47, + "src": "t.index !== i && this._trigger(\"change\", e, {\n item: t})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 190369, + "nodeLength": 11, + "src": "t.index !== i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 190634, + "nodeLength": 11, + "src": "\"icons\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 190782, + "nodeLength": 56, + "src": "\"appendTo\" === t && this.menuWrap.appendTo(this._appendTo())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 190782, + "nodeLength": 14, + "src": "\"appendTo\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 190839, + "nodeLength": 33, + "src": "\"width\" === t && this._resizeButton()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 190839, + "nodeLength": 11, + "src": "\"width\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 191085, + "nodeLength": 1, + "src": "t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 191222, + "nodeLength": 60, + "src": "e && (e = e.jquery || e.nodeType ? t(e) : this.document.find(e).eq(0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 191228, + "nodeLength": 20, + "src": "e.jquery || e.nodeType", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 191283, + "nodeLength": 54, + "src": "e && e[0] || (e = this.element.closest(\".ui-front, dialog\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 191283, + "nodeLength": 7, + "src": "e && e[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 191338, + "nodeLength": 35, + "src": "e.length || (e = this.document[0].body)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 191501, + "nodeLength": 11, + "src": "this.isOpen", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 191578, + "nodeLength": 11, + "src": "this.isOpen", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 191775, + "nodeLength": 6, + "src": "t === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 191820, + "nodeLength": 66, + "src": "null === t && (t = this.element.show().outerWidth() , this.element.hide())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 191820, + "nodeLength": 8, + "src": "null === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 192365, + "nodeLength": 19, + "src": "i.attr(\"label\") || \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 192394, + "nodeLength": 38, + "src": "i.prop(\"disabled\") || t.prop(\"disabled\")", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "12": [ + null, + { + "position": 861, + "nodeLength": 28, + "src": "s.values && s.values.length || 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 861, + "nodeLength": 25, + "src": "s.values && s.values.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 890, + "nodeLength": 48, + "src": "n.length > i && (n.slice(i).remove() , n = n.slice(0, i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 890, + "nodeLength": 10, + "src": "n.length > i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 950, + "nodeLength": 3, + "src": "i > e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1259, + "nodeLength": 7, + "src": "e.range", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1268, + "nodeLength": 191, + "src": "e.range === !0 && (e.values ? e.values.length && 2 !== e.values.length ? e.values = [e.values[0], e.values[0]] : t.isArray(e.values) && (e.values = e.values.slice(0)) : e.values = [this._valueMin(), this._valueMin()])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1268, + "nodeLength": 12, + "src": "e.range === !0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1283, + "nodeLength": 8, + "src": "e.values", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1292, + "nodeLength": 36, + "src": "e.values.length && 2 !== e.values.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1309, + "nodeLength": 19, + "src": "2 !== e.values.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1364, + "nodeLength": 49, + "src": "t.isArray(e.values) && (e.values = e.values.slice(0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1460, + "nodeLength": 29, + "src": "this.range && this.range.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1693, + "nodeLength": 88, + "src": "(\"min\" === e.range || \"max\" === e.range) && this._addClass(this.range, \"ui-slider-range-\" + e.range)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1693, + "nodeLength": 32, + "src": "\"min\" === e.range || \"max\" === e.range", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1693, + "nodeLength": 15, + "src": "\"min\" === e.range", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1710, + "nodeLength": 15, + "src": "\"max\" === e.range", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1784, + "nodeLength": 31, + "src": "this.range && this.range.remove()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2027, + "nodeLength": 31, + "src": "this.range && this.range.remove()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2156, + "nodeLength": 10, + "src": "u.disabled", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2450, + "nodeLength": 80, + "src": "(n > i || n === i && (e === c._lastChangedValue || c.values(e) === u.min)) && (n = i , o = t(this) , a = e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2450, + "nodeLength": 58, + "src": "n > i || n === i && (e === c._lastChangedValue || c.values(e) === u.min)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2450, + "nodeLength": 3, + "src": "n > i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2455, + "nodeLength": 53, + "src": "n === i && (e === c._lastChangedValue || c.values(e) === u.min)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2455, + "nodeLength": 5, + "src": "n === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2463, + "nodeLength": 44, + "src": "e === c._lastChangedValue || c.values(e) === u.min", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2463, + "nodeLength": 23, + "src": "e === c._lastChangedValue", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2488, + "nodeLength": 19, + "src": "c.values(e) === u.min", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2552, + "nodeLength": 6, + "src": "r === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2755, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2837, + "nodeLength": 39, + "src": "parseInt(o.css(\"borderTopWidth\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2879, + "nodeLength": 42, + "src": "parseInt(o.css(\"borderBottomWidth\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2924, + "nodeLength": 34, + "src": "parseInt(o.css(\"marginTop\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2961, + "nodeLength": 59, + "src": "this.handles.hasClass(\"ui-state-hover\") || this._slide(e, a, s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3497, + "nodeLength": 37, + "src": "\"vertical\" === this.options.orientation", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3612, + "nodeLength": 31, + "src": "\"horizontal\" === this.orientation", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3701, + "nodeLength": 17, + "src": "this._clickOffset", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3803, + "nodeLength": 17, + "src": "this._clickOffset", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3853, + "nodeLength": 10, + "src": "s > 1 && (s = 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3853, + "nodeLength": 3, + "src": "s > 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3864, + "nodeLength": 10, + "src": "0 > s && (s = 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3864, + "nodeLength": 3, + "src": "0 > s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3875, + "nodeLength": 38, + "src": "\"vertical\" === this.orientation && (s = 1 - s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3875, + "nodeLength": 29, + "src": "\"vertical\" === this.orientation", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4072, + "nodeLength": 10, + "src": "void 0 !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4106, + "nodeLength": 90, + "src": "this._hasMultipleValues() && (s.value = void 0 !== e ? e : this.values(t) , s.values = i || this.values())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4142, + "nodeLength": 10, + "src": "void 0 !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4179, + "nodeLength": 16, + "src": "i || this.values()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4237, + "nodeLength": 47, + "src": "this.options.values && this.options.values.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4418, + "nodeLength": 168, + "src": "this._hasMultipleValues() && (n = this.values(e ? 0 : 1) , o = this.values(e) , 2 === this.options.values.length && this.options.range === !0 && (i = 0 === e ? Math.min(n, i) : Math.max(n, i)) , a[e] = i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4460, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4484, + "nodeLength": 94, + "src": "2 === this.options.values.length && this.options.range === !0 && (i = 0 === e ? Math.min(n, i) : Math.max(n, i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4484, + "nodeLength": 30, + "src": "2 === this.options.values.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4516, + "nodeLength": 62, + "src": "this.options.range === !0 && (i = 0 === e ? Math.min(n, i) : Math.max(n, i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4516, + "nodeLength": 23, + "src": "this.options.range === !0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4544, + "nodeLength": 5, + "src": "0 === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4587, + "nodeLength": 122, + "src": "i !== o && (s = this._trigger(\"slide\", t, this._uiHash(e, i, a)) , s !== !1 && (this._hasMultipleValues() ? this.values(e, i) : this.value(i)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4587, + "nodeLength": 5, + "src": "i !== o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4642, + "nodeLength": 66, + "src": "s !== !1 && (this._hasMultipleValues() ? this.values(e, i) : this.value(i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4642, + "nodeLength": 6, + "src": "s !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4651, + "nodeLength": 25, + "src": "this._hasMultipleValues()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4794, + "nodeLength": 106, + "src": "this._keySliding || this._mouseSliding || (this._lastChangedValue = e , this._trigger(\"change\", t, this._uiHash(e)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4812, + "nodeLength": 88, + "src": "this._mouseSliding || (this._lastChangedValue = e , this._trigger(\"change\", t, this._uiHash(e)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4927, + "nodeLength": 16, + "src": "arguments.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5087, + "nodeLength": 18, + "src": "arguments.length > 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5212, + "nodeLength": 17, + "src": "!arguments.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5255, + "nodeLength": 24, + "src": "!t.isArray(arguments[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5287, + "nodeLength": 25, + "src": "this._hasMultipleValues()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5387, + "nodeLength": 10, + "src": "s.length > o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5520, + "nodeLength": 211, + "src": "\"range\" === e && this.options.range === !0 && (\"min\" === i ? (this.options.value = this._values(0) , this.options.values = null) : \"max\" === i && (this.options.value = this._values(this.options.values.length - 1) , this.options.values = null))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5520, + "nodeLength": 11, + "src": "\"range\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5533, + "nodeLength": 198, + "src": "this.options.range === !0 && (\"min\" === i ? (this.options.value = this._values(0) , this.options.values = null) : \"max\" === i && (this.options.value = this._values(this.options.values.length - 1) , this.options.values = null))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5533, + "nodeLength": 23, + "src": "this.options.range === !0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5559, + "nodeLength": 9, + "src": "\"min\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5631, + "nodeLength": 99, + "src": "\"max\" === i && (this.options.value = this._values(this.options.values.length - 1) , this.options.values = null)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5631, + "nodeLength": 9, + "src": "\"max\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5732, + "nodeLength": 62, + "src": "t.isArray(this.options.values) && (n = this.options.values.length)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5982, + "nodeLength": 41, + "src": "this.options.range && this._refreshRange(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6041, + "nodeLength": 16, + "src": "\"horizontal\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6248, + "nodeLength": 4, + "src": "s >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6701, + "nodeLength": 16, + "src": "arguments.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6779, + "nodeLength": 25, + "src": "this._hasMultipleValues()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6844, + "nodeLength": 10, + "src": "i.length > s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6942, + "nodeLength": 19, + "src": "this._valueMin() >= t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6989, + "nodeLength": 19, + "src": "t >= this._valueMax()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7039, + "nodeLength": 19, + "src": "this.options.step > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7117, + "nodeLength": 31, + "src": "2 * Math.abs(i) >= e && (s += i > 0 ? e : -e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7117, + "nodeLength": 16, + "src": "2 * Math.abs(i) >= e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7139, + "nodeLength": 3, + "src": "i > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7295, + "nodeLength": 26, + "src": "t > this.options.max && (t -= i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7295, + "nodeLength": 18, + "src": "t > this.options.max", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7445, + "nodeLength": 76, + "src": "null !== this.options.min && (t = Math.max(t, this._precisionOf(this.options.min)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7445, + "nodeLength": 23, + "src": "null !== this.options.min", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7584, + "nodeLength": 6, + "src": "-1 === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7717, + "nodeLength": 50, + "src": "\"vertical\" === t && this.range.css({\n width: \"\", \n left: \"\"})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7717, + "nodeLength": 14, + "src": "\"vertical\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7768, + "nodeLength": 55, + "src": "\"horizontal\" === t && this.range.css({\n height: \"\", \n bottom: \"\"})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7768, + "nodeLength": 16, + "src": "\"horizontal\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7909, + "nodeLength": 16, + "src": "this._animateOff", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7944, + "nodeLength": 25, + "src": "this._hasMultipleValues()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8068, + "nodeLength": 28, + "src": "\"horizontal\" === h.orientation", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8138, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8170, + "nodeLength": 357, + "src": "h.options.range === !0 && (\"horizontal\" === h.orientation ? (0 === s && h.range.stop(1, 1)[l ? \"animate\" : \"css\"]({\n left: i + \"%\"}, r.animate) , 1 === s && h.range[l ? \"animate\" : \"css\"]({\n width: i - e + \"%\"}, {\n queue: !1, \n duration: r.animate})) : (0 === s && h.range.stop(1, 1)[l ? \"animate\" : \"css\"]({\n bottom: i + \"%\"}, r.animate) , 1 === s && h.range[l ? \"animate\" : \"css\"]({\n height: i - e + \"%\"}, {\n queue: !1, \n duration: r.animate})))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8170, + "nodeLength": 20, + "src": "h.options.range === !0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8193, + "nodeLength": 28, + "src": "\"horizontal\" === h.orientation", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8223, + "nodeLength": 67, + "src": "0 === s && h.range.stop(1, 1)[l ? \"animate\" : \"css\"]({\n left: i + \"%\"}, r.animate)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8223, + "nodeLength": 5, + "src": "0 === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8248, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8291, + "nodeLength": 80, + "src": "1 === s && h.range[l ? \"animate\" : \"css\"]({\n width: i - e + \"%\"}, {\n queue: !1, \n duration: r.animate})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8291, + "nodeLength": 5, + "src": "1 === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8306, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8374, + "nodeLength": 69, + "src": "0 === s && h.range.stop(1, 1)[l ? \"animate\" : \"css\"]({\n bottom: i + \"%\"}, r.animate)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8374, + "nodeLength": 5, + "src": "0 === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8399, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8444, + "nodeLength": 81, + "src": "1 === s && h.range[l ? \"animate\" : \"css\"]({\n height: i - e + \"%\"}, {\n queue: !1, \n duration: r.animate})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8444, + "nodeLength": 5, + "src": "1 === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8459, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8590, + "nodeLength": 5, + "src": "o !== n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8618, + "nodeLength": 31, + "src": "\"horizontal\" === this.orientation", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8695, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8727, + "nodeLength": 108, + "src": "\"min\" === a && \"horizontal\" === this.orientation && this.range.stop(1, 1)[l ? \"animate\" : \"css\"]({\n width: i + \"%\"}, r.animate)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8727, + "nodeLength": 9, + "src": "\"min\" === a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8738, + "nodeLength": 97, + "src": "\"horizontal\" === this.orientation && this.range.stop(1, 1)[l ? \"animate\" : \"css\"]({\n width: i + \"%\"}, r.animate)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8738, + "nodeLength": 31, + "src": "\"horizontal\" === this.orientation", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8792, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8836, + "nodeLength": 112, + "src": "\"max\" === a && \"horizontal\" === this.orientation && this.range.stop(1, 1)[l ? \"animate\" : \"css\"]({\n width: 100 - i + \"%\"}, r.animate)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8836, + "nodeLength": 9, + "src": "\"max\" === a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8847, + "nodeLength": 101, + "src": "\"horizontal\" === this.orientation && this.range.stop(1, 1)[l ? \"animate\" : \"css\"]({\n width: 100 - i + \"%\"}, r.animate)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8847, + "nodeLength": 31, + "src": "\"horizontal\" === this.orientation", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8901, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8949, + "nodeLength": 107, + "src": "\"min\" === a && \"vertical\" === this.orientation && this.range.stop(1, 1)[l ? \"animate\" : \"css\"]({\n height: i + \"%\"}, r.animate)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8949, + "nodeLength": 9, + "src": "\"min\" === a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8960, + "nodeLength": 96, + "src": "\"vertical\" === this.orientation && this.range.stop(1, 1)[l ? \"animate\" : \"css\"]({\n height: i + \"%\"}, r.animate)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8960, + "nodeLength": 29, + "src": "\"vertical\" === this.orientation", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9012, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9057, + "nodeLength": 111, + "src": "\"max\" === a && \"vertical\" === this.orientation && this.range.stop(1, 1)[l ? \"animate\" : \"css\"]({\n height: 100 - i + \"%\"}, r.animate)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9057, + "nodeLength": 9, + "src": "\"max\" === a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9068, + "nodeLength": 100, + "src": "\"vertical\" === this.orientation && this.range.stop(1, 1)[l ? \"animate\" : \"css\"]({\n height: 100 - i + \"%\"}, r.animate)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9068, + "nodeLength": 29, + "src": "\"vertical\" === this.orientation", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9120, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9474, + "nodeLength": 136, + "src": "e.preventDefault() , !this._keySliding && (this._keySliding = !0 , this._addClass(t(e.target), null, \"ui-state-active\") , i = this._start(e, a) , i === !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9493, + "nodeLength": 117, + "src": "!this._keySliding && (this._keySliding = !0 , this._addClass(t(e.target), null, \"ui-state-active\") , i = this._start(e, a) , i === !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9603, + "nodeLength": 6, + "src": "i === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9649, + "nodeLength": 25, + "src": "this._hasMultipleValues()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10075, + "nodeLength": 20, + "src": "s === this._valueMax()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10186, + "nodeLength": 20, + "src": "s === this._valueMin()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10329, + "nodeLength": 127, + "src": "this._keySliding && (this._keySliding = !1 , this._stop(e, i) , this._change(e, i) , this._removeClass(t(e.target), null, \"ui-state-active\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29216, + "nodeLength": 11, + "src": "t >= e && e + i > t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29216, + "nodeLength": 4, + "src": "t >= e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29222, + "nodeLength": 5, + "src": "e + i > t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29259, + "nodeLength": 77, + "src": "/left|right/.test(t.css(\"float\")) || /inline|table-cell/.test(t.css(\"display\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29561, + "nodeLength": 40, + "src": "\"handle\" === t && this._setHandleClassName()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29561, + "nodeLength": 12, + "src": "\"handle\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29767, + "nodeLength": 28, + "src": "this.instance.options.handle", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 29947, + "nodeLength": 4, + "src": "t >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30082, + "nodeLength": 14, + "src": "this.reverting", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30100, + "nodeLength": 51, + "src": "this.options.disabled || \"static\" === this.options.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30123, + "nodeLength": 28, + "src": "\"static\" === this.options.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30223, + "nodeLength": 37, + "src": "t.data(this, o.widgetName + \"-item\") === o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30285, + "nodeLength": 58, + "src": "t.data(e.target, o.widgetName + \"-item\") === o && (s = t(e.target))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30285, + "nodeLength": 41, + "src": "t.data(e.target, o.widgetName + \"-item\") === o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30344, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30346, + "nodeLength": 115, + "src": "!this.options.handle || i || (t(this.options.handle, s).find(\"*\").addBack().each(function() {\n this === e.target && (n = !0);\n}) , n)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30368, + "nodeLength": 93, + "src": "i || (t(this.options.handle, s).find(\"*\").addBack().each(function() {\n this === e.target && (n = !0);\n}) , n)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30433, + "nodeLength": 23, + "src": "this === e.target && (n = !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30433, + "nodeLength": 15, + "src": "this === e.target", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30580, + "nodeLength": 1613, + "src": "this.currentContainer = this , this.refreshPositions() , this.helper = this._createHelper(e) , this._cacheHelperProportions() , this._cacheMargins() , this.scrollParent = this.helper.scrollParent() , this.offset = this.currentItem.offset() , this.offset = {\n top: this.offset.top - this.margins.top, \n left: this.offset.left - this.margins.left} , t.extend(this.offset, {\n click: {\n left: e.pageX - this.offset.left, \n top: e.pageY - this.offset.top}, \n parent: this._getParentOffset(), \n relative: this._getRelativeOffset()}) , this.helper.css(\"position\", \"absolute\") , this.cssPosition = this.helper.css(\"position\") , this.originalPosition = this._generatePosition(e) , this.originalPageX = e.pageX , this.originalPageY = e.pageY , a.cursorAt && this._adjustOffsetFromHelper(a.cursorAt) , this.domPosition = {\n prev: this.currentItem.prev()[0], \n parent: this.currentItem.parent()[0]} , this.helper[0] !== this.currentItem[0] && this.currentItem.hide() , this._createPlaceholder() , a.containment && this._setContainment() , a.cursor && \"auto\" !== a.cursor && (o = this.document.find(\"body\") , this.storedCursor = o.css(\"cursor\") , o.css(\"cursor\", a.cursor) , this.storedStylesheet = t(\"\").appendTo(o)) , a.opacity && (this.helper.css(\"opacity\") && (this._storedOpacity = this.helper.css(\"opacity\")) , this.helper.css(\"opacity\", a.opacity)) , a.zIndex && (this.helper.css(\"zIndex\") && (this._storedZIndex = this.helper.css(\"zIndex\")) , this.helper.css(\"zIndex\", a.zIndex)) , this.scrollParent[0] !== this.document[0] && \"HTML\" !== this.scrollParent[0].tagName && (this.overflowOffset = this.scrollParent.offset()) , this._trigger(\"start\", e, this._uiHash()) , this._preserveHelperProportions || this._cacheHelperProportions() , !s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31233, + "nodeLength": 52, + "src": "a.cursorAt && this._adjustOffsetFromHelper(a.cursorAt)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31373, + "nodeLength": 61, + "src": "this.helper[0] !== this.currentItem[0] && this.currentItem.hide()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31373, + "nodeLength": 36, + "src": "this.helper[0] !== this.currentItem[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31461, + "nodeLength": 37, + "src": "a.containment && this._setContainment()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31499, + "nodeLength": 210, + "src": "a.cursor && \"auto\" !== a.cursor && (o = this.document.find(\"body\") , this.storedCursor = o.css(\"cursor\") , o.css(\"cursor\", a.cursor) , this.storedStylesheet = t(\"\").appendTo(o))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31509, + "nodeLength": 200, + "src": "\"auto\" !== a.cursor && (o = this.document.find(\"body\") , this.storedCursor = o.css(\"cursor\") , o.css(\"cursor\", a.cursor) , this.storedStylesheet = t(\"\").appendTo(o))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31509, + "nodeLength": 17, + "src": "\"auto\" !== a.cursor", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31710, + "nodeLength": 126, + "src": "a.opacity && (this.helper.css(\"opacity\") && (this._storedOpacity = this.helper.css(\"opacity\")) , this.helper.css(\"opacity\", a.opacity))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31722, + "nodeLength": 76, + "src": "this.helper.css(\"opacity\") && (this._storedOpacity = this.helper.css(\"opacity\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31837, + "nodeLength": 120, + "src": "a.zIndex && (this.helper.css(\"zIndex\") && (this._storedZIndex = this.helper.css(\"zIndex\")) , this.helper.css(\"zIndex\", a.zIndex))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31848, + "nodeLength": 73, + "src": "this.helper.css(\"zIndex\") && (this._storedZIndex = this.helper.css(\"zIndex\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31958, + "nodeLength": 128, + "src": "this.scrollParent[0] !== this.document[0] && \"HTML\" !== this.scrollParent[0].tagName && (this.overflowOffset = this.scrollParent.offset())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31958, + "nodeLength": 39, + "src": "this.scrollParent[0] !== this.document[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31999, + "nodeLength": 87, + "src": "\"HTML\" !== this.scrollParent[0].tagName && (this.overflowOffset = this.scrollParent.offset())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31999, + "nodeLength": 37, + "src": "\"HTML\" !== this.scrollParent[0].tagName", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32127, + "nodeLength": 63, + "src": "this._preserveHelperProportions || this._cacheHelperProportions()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32225, + "nodeLength": 4, + "src": "n >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32302, + "nodeLength": 45, + "src": "t.ui.ddmanager && (t.ui.ddmanager.current = this)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32348, + "nodeLength": 71, + "src": "t.ui.ddmanager && !a.dropBehaviour && t.ui.ddmanager.prepareOffsets(this, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32364, + "nodeLength": 55, + "src": "!a.dropBehaviour && t.ui.ddmanager.prepareOffsets(this, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32661, + "nodeLength": 61, + "src": "this.lastPositionAbs || (this.lastPositionAbs = this.positionAbs)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32723, + "nodeLength": 1325, + "src": "this.options.scroll && (this.scrollParent[0] !== this.document[0] && \"HTML\" !== this.scrollParent[0].tagName ? (this.overflowOffset.top + this.scrollParent[0].offsetHeight - e.pageY < a.scrollSensitivity ? this.scrollParent[0].scrollTop = r = this.scrollParent[0].scrollTop + a.scrollSpeed : e.pageY - this.overflowOffset.top < a.scrollSensitivity && (this.scrollParent[0].scrollTop = r = this.scrollParent[0].scrollTop - a.scrollSpeed) , this.overflowOffset.left + this.scrollParent[0].offsetWidth - e.pageX < a.scrollSensitivity ? this.scrollParent[0].scrollLeft = r = this.scrollParent[0].scrollLeft + a.scrollSpeed : e.pageX - this.overflowOffset.left < a.scrollSensitivity && (this.scrollParent[0].scrollLeft = r = this.scrollParent[0].scrollLeft - a.scrollSpeed)) : (e.pageY - this.document.scrollTop() < a.scrollSensitivity ? r = this.document.scrollTop(this.document.scrollTop() - a.scrollSpeed) : this.window.height() - (e.pageY - this.document.scrollTop()) < a.scrollSensitivity && (r = this.document.scrollTop(this.document.scrollTop() + a.scrollSpeed)) , e.pageX - this.document.scrollLeft() < a.scrollSensitivity ? r = this.document.scrollLeft(this.document.scrollLeft() - a.scrollSpeed) : this.window.width() - (e.pageX - this.document.scrollLeft()) < a.scrollSensitivity && (r = this.document.scrollLeft(this.document.scrollLeft() + a.scrollSpeed))) , r !== !1 && t.ui.ddmanager && !a.dropBehaviour && t.ui.ddmanager.prepareOffsets(this, e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32745, + "nodeLength": 78, + "src": "this.scrollParent[0] !== this.document[0] && \"HTML\" !== this.scrollParent[0].tagName", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32745, + "nodeLength": 39, + "src": "this.scrollParent[0] !== this.document[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32786, + "nodeLength": 37, + "src": "\"HTML\" !== this.scrollParent[0].tagName", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32825, + "nodeLength": 85, + "src": "this.overflowOffset.top + this.scrollParent[0].offsetHeight - e.pageY < a.scrollSensitivity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32989, + "nodeLength": 132, + "src": "e.pageY - this.overflowOffset.top < a.scrollSensitivity && (this.scrollParent[0].scrollTop = r = this.scrollParent[0].scrollTop - a.scrollSpeed)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32989, + "nodeLength": 51, + "src": "e.pageY - this.overflowOffset.top < a.scrollSensitivity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33122, + "nodeLength": 85, + "src": "this.overflowOffset.left + this.scrollParent[0].offsetWidth - e.pageX < a.scrollSensitivity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33288, + "nodeLength": 135, + "src": "e.pageX - this.overflowOffset.left < a.scrollSensitivity && (this.scrollParent[0].scrollLeft = r = this.scrollParent[0].scrollLeft - a.scrollSpeed)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33288, + "nodeLength": 52, + "src": "e.pageX - this.overflowOffset.left < a.scrollSensitivity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33426, + "nodeLength": 53, + "src": "e.pageY - this.document.scrollTop() < a.scrollSensitivity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33547, + "nodeLength": 146, + "src": "this.window.height() - (e.pageY - this.document.scrollTop()) < a.scrollSensitivity && (r = this.document.scrollTop(this.document.scrollTop() + a.scrollSpeed))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33547, + "nodeLength": 76, + "src": "this.window.height() - (e.pageY - this.document.scrollTop()) < a.scrollSensitivity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33694, + "nodeLength": 54, + "src": "e.pageX - this.document.scrollLeft() < a.scrollSensitivity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33818, + "nodeLength": 148, + "src": "this.window.width() - (e.pageX - this.document.scrollLeft()) < a.scrollSensitivity && (r = this.document.scrollLeft(this.document.scrollLeft() + a.scrollSpeed))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33818, + "nodeLength": 76, + "src": "this.window.width() - (e.pageX - this.document.scrollLeft()) < a.scrollSensitivity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33968, + "nodeLength": 79, + "src": "r !== !1 && t.ui.ddmanager && !a.dropBehaviour && t.ui.ddmanager.prepareOffsets(this, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33968, + "nodeLength": 6, + "src": "r !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33976, + "nodeLength": 71, + "src": "t.ui.ddmanager && !a.dropBehaviour && t.ui.ddmanager.prepareOffsets(this, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33992, + "nodeLength": 55, + "src": "!a.dropBehaviour && t.ui.ddmanager.prepareOffsets(this, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34102, + "nodeLength": 95, + "src": "this.options.axis && \"y\" === this.options.axis || (this.helper[0].style.left = this.position.left + \"px\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34102, + "nodeLength": 42, + "src": "this.options.axis && \"y\" === this.options.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34121, + "nodeLength": 23, + "src": "\"y\" === this.options.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34198, + "nodeLength": 93, + "src": "this.options.axis && \"x\" === this.options.axis || (this.helper[0].style.top = this.position.top + \"px\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34198, + "nodeLength": 42, + "src": "this.options.axis && \"x\" === this.options.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34217, + "nodeLength": 23, + "src": "\"x\" === this.options.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34314, + "nodeLength": 4, + "src": "i >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34326, + "nodeLength": 279, + "src": "s = this.items[i] , n = s.item[0] , o = this._intersectsWithPointer(s) , o && s.instance === this.currentContainer && n !== this.currentItem[0] && this.placeholder[1 === o ? \"next\" : \"prev\"]()[0] !== n && !t.contains(this.placeholder[0], n) && (\"semi-dynamic\" === this.options.type ? !t.contains(this.element[0], n) : !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34387, + "nodeLength": 218, + "src": "o && s.instance === this.currentContainer && n !== this.currentItem[0] && this.placeholder[1 === o ? \"next\" : \"prev\"]()[0] !== n && !t.contains(this.placeholder[0], n) && (\"semi-dynamic\" === this.options.type ? !t.contains(this.element[0], n) : !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34390, + "nodeLength": 215, + "src": "s.instance === this.currentContainer && n !== this.currentItem[0] && this.placeholder[1 === o ? \"next\" : \"prev\"]()[0] !== n && !t.contains(this.placeholder[0], n) && (\"semi-dynamic\" === this.options.type ? !t.contains(this.element[0], n) : !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34390, + "nodeLength": 34, + "src": "s.instance === this.currentContainer", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34426, + "nodeLength": 179, + "src": "n !== this.currentItem[0] && this.placeholder[1 === o ? \"next\" : \"prev\"]()[0] !== n && !t.contains(this.placeholder[0], n) && (\"semi-dynamic\" === this.options.type ? !t.contains(this.element[0], n) : !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34426, + "nodeLength": 23, + "src": "n !== this.currentItem[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34451, + "nodeLength": 154, + "src": "this.placeholder[1 === o ? \"next\" : \"prev\"]()[0] !== n && !t.contains(this.placeholder[0], n) && (\"semi-dynamic\" === this.options.type ? !t.contains(this.element[0], n) : !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34451, + "nodeLength": 46, + "src": "this.placeholder[1 === o ? \"next\" : \"prev\"]()[0] !== n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34468, + "nodeLength": 5, + "src": "1 === o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34499, + "nodeLength": 106, + "src": "!t.contains(this.placeholder[0], n) && (\"semi-dynamic\" === this.options.type ? !t.contains(this.element[0], n) : !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34536, + "nodeLength": 34, + "src": "\"semi-dynamic\" === this.options.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34610, + "nodeLength": 98, + "src": "this.direction = 1 === o ? \"down\" : \"up\" , \"pointer\" !== this.options.tolerance && !this._intersectsWithSides(s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34625, + "nodeLength": 5, + "src": "1 === o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34643, + "nodeLength": 65, + "src": "\"pointer\" !== this.options.tolerance && !this._intersectsWithSides(s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34643, + "nodeLength": 34, + "src": "\"pointer\" !== this.options.tolerance", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34817, + "nodeLength": 43, + "src": "t.ui.ddmanager && t.ui.ddmanager.drag(this, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34970, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34976, + "nodeLength": 92, + "src": "t.ui.ddmanager && !this.options.dropBehaviour && t.ui.ddmanager.drop(this, e) , this.options.revert", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34976, + "nodeLength": 72, + "src": "t.ui.ddmanager && !this.options.dropBehaviour && t.ui.ddmanager.drop(this, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34992, + "nodeLength": 56, + "src": "!this.options.dropBehaviour && t.ui.ddmanager.drop(this, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35134, + "nodeLength": 150, + "src": "o && \"x\" !== o || (a.left = n.left - this.offset.parent.left - this.margins.left + (this.offsetParent[0] === this.document[0].body ? 0 : this.offsetParent[0].scrollLeft))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35134, + "nodeLength": 10, + "src": "o && \"x\" !== o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35137, + "nodeLength": 7, + "src": "\"x\" !== o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35204, + "nodeLength": 44, + "src": "this.offsetParent[0] === this.document[0].body", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35285, + "nodeLength": 145, + "src": "o && \"y\" !== o || (a.top = n.top - this.offset.parent.top - this.margins.top + (this.offsetParent[0] === this.document[0].body ? 0 : this.offsetParent[0].scrollTop))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35285, + "nodeLength": 10, + "src": "o && \"y\" !== o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35288, + "nodeLength": 7, + "src": "\"y\" !== o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35351, + "nodeLength": 44, + "src": "this.offsetParent[0] === this.document[0].body", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35474, + "nodeLength": 37, + "src": "parseInt(this.options.revert, 10) || 500", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35591, + "nodeLength": 13, + "src": "this.dragging", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35658, + "nodeLength": 32, + "src": "\"original\" === this.options.helper", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35847, + "nodeLength": 4, + "src": "e >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35922, + "nodeLength": 141, + "src": "this.containers[e].containerCache.over && (this.containers[e]._trigger(\"out\", null, this._uiHash(this)) , this.containers[e].containerCache.over = 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36071, + "nodeLength": 406, + "src": "this.placeholder && (this.placeholder[0].parentNode && this.placeholder[0].parentNode.removeChild(this.placeholder[0]) , \"original\" !== this.options.helper && this.helper && this.helper[0].parentNode && this.helper.remove() , t.extend(this, {\n helper: null, \n dragging: !1, \n reverting: !1, \n _noFinalSort: null}) , this.domPosition.prev ? t(this.domPosition.prev).after(this.currentItem) : t(this.domPosition.parent).prepend(this.currentItem))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36090, + "nodeLength": 95, + "src": "this.placeholder[0].parentNode && this.placeholder[0].parentNode.removeChild(this.placeholder[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36186, + "nodeLength": 94, + "src": "\"original\" !== this.options.helper && this.helper && this.helper[0].parentNode && this.helper.remove()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36186, + "nodeLength": 32, + "src": "\"original\" !== this.options.helper", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36220, + "nodeLength": 60, + "src": "this.helper && this.helper[0].parentNode && this.helper.remove()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36233, + "nodeLength": 47, + "src": "this.helper[0].parentNode && this.helper.remove()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36353, + "nodeLength": 21, + "src": "this.domPosition.prev", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36535, + "nodeLength": 14, + "src": "e && e.connected", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36565, + "nodeLength": 5, + "src": "e || {}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36599, + "nodeLength": 43, + "src": "t(e.item || this).attr(e.attribute || \"id\") || \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36601, + "nodeLength": 12, + "src": "e.item || this", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36620, + "nodeLength": 17, + "src": "e.attribute || \"id\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36650, + "nodeLength": 30, + "src": "e.expression || /(.+)[\\-=_](.+)/", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36682, + "nodeLength": 65, + "src": "i && s.push((e.key || i[1] + \"[]\") + \"=\" + (e.key && e.expression ? i[1] : i[2]))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36693, + "nodeLength": 16, + "src": "e.key || i[1] + \"[]\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36716, + "nodeLength": 19, + "src": "e.key && e.expression", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36750, + "nodeLength": 35, + "src": "!s.length && e.key && s.push(e.key + \"=\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36761, + "nodeLength": 24, + "src": "e.key && s.push(e.key + \"=\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36848, + "nodeLength": 14, + "src": "e && e.connected", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36878, + "nodeLength": 5, + "src": "e || {}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36909, + "nodeLength": 43, + "src": "t(e.item || this).attr(e.attribute || \"id\") || \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9, + "nodeLength": 12, + "src": "e.item || this", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 28, + "nodeLength": 17, + "src": "e.attribute || \"id\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37198, + "nodeLength": 37, + "src": "\"x\" === this.options.axis || s + l > r && h > s + l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37198, + "nodeLength": 23, + "src": "\"x\" === this.options.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37223, + "nodeLength": 12, + "src": "s + l > r && h > s + l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37223, + "nodeLength": 5, + "src": "s + l > r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37230, + "nodeLength": 5, + "src": "h > s + l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37238, + "nodeLength": 37, + "src": "\"y\" === this.options.axis || e + c > o && a > e + c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37238, + "nodeLength": 23, + "src": "\"y\" === this.options.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37263, + "nodeLength": 12, + "src": "e + c > o && a > e + c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37263, + "nodeLength": 5, + "src": "e + c > o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37270, + "nodeLength": 5, + "src": "a > e + c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37278, + "nodeLength": 4, + "src": "u && d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37289, + "nodeLength": 200, + "src": "\"pointer\" === this.options.tolerance || this.options.forcePointerForContainers || \"pointer\" !== this.options.tolerance && this.helperProportions[this.floating ? \"width\" : \"height\"] > t[this.floating ? \"width\" : \"height\"]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37289, + "nodeLength": 34, + "src": "\"pointer\" === this.options.tolerance", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37325, + "nodeLength": 164, + "src": "this.options.forcePointerForContainers || \"pointer\" !== this.options.tolerance && this.helperProportions[this.floating ? \"width\" : \"height\"] > t[this.floating ? \"width\" : \"height\"]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37365, + "nodeLength": 124, + "src": "\"pointer\" !== this.options.tolerance && this.helperProportions[this.floating ? \"width\" : \"height\"] > t[this.floating ? \"width\" : \"height\"]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37365, + "nodeLength": 34, + "src": "\"pointer\" !== this.options.tolerance", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37401, + "nodeLength": 88, + "src": "this.helperProportions[this.floating ? \"width\" : \"height\"] > t[this.floating ? \"width\" : \"height\"]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37424, + "nodeLength": 13, + "src": "this.floating", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37458, + "nodeLength": 13, + "src": "this.floating", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37492, + "nodeLength": 144, + "src": "e + this.helperProportions.width / 2 > o && a > i - this.helperProportions.width / 2 && s + this.helperProportions.height / 2 > r && h > n - this.helperProportions.height / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37492, + "nodeLength": 34, + "src": "e + this.helperProportions.width / 2 > o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37528, + "nodeLength": 108, + "src": "a > i - this.helperProportions.width / 2 && s + this.helperProportions.height / 2 > r && h > n - this.helperProportions.height / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37528, + "nodeLength": 34, + "src": "a > i - this.helperProportions.width / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37564, + "nodeLength": 72, + "src": "s + this.helperProportions.height / 2 > r && h > n - this.helperProportions.height / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37564, + "nodeLength": 35, + "src": "s + this.helperProportions.height / 2 > r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37601, + "nodeLength": 35, + "src": "h > n - this.helperProportions.height / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37683, + "nodeLength": 100, + "src": "\"x\" === this.options.axis || this._isOverAxis(this.positionAbs.top + this.offset.click.top, t.top, t.height)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37683, + "nodeLength": 23, + "src": "\"x\" === this.options.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37786, + "nodeLength": 102, + "src": "\"y\" === this.options.axis || this._isOverAxis(this.positionAbs.left + this.offset.click.left, t.left, t.width)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37786, + "nodeLength": 23, + "src": "\"y\" === this.options.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37891, + "nodeLength": 4, + "src": "s && n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37903, + "nodeLength": 1, + "src": "o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37978, + "nodeLength": 13, + "src": "this.floating", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37992, + "nodeLength": 23, + "src": "\"right\" === i || \"down\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 37992, + "nodeLength": 11, + "src": "\"right\" === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38005, + "nodeLength": 10, + "src": "\"down\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38020, + "nodeLength": 19, + "src": "e && (\"down\" === e ? 2 : 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38024, + "nodeLength": 10, + "src": "\"down\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38340, + "nodeLength": 16, + "src": "this.floating && n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38357, + "nodeLength": 30, + "src": "\"right\" === n && i || \"left\" === n && !i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38357, + "nodeLength": 14, + "src": "\"right\" === n && i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38357, + "nodeLength": 11, + "src": "\"right\" === n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38373, + "nodeLength": 14, + "src": "\"left\" === n && !i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38373, + "nodeLength": 10, + "src": "\"left\" === n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38388, + "nodeLength": 32, + "src": "s && (\"down\" === s && e || \"up\" === s && !e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38392, + "nodeLength": 27, + "src": "\"down\" === s && e || \"up\" === s && !e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38392, + "nodeLength": 13, + "src": "\"down\" === s && e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38392, + "nodeLength": 10, + "src": "\"down\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38407, + "nodeLength": 12, + "src": "\"up\" === s && !e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38407, + "nodeLength": 8, + "src": "\"up\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38518, + "nodeLength": 24, + "src": "0 !== t && (t > 0 ? \"down\" : \"up\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38518, + "nodeLength": 5, + "src": "0 !== t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38526, + "nodeLength": 3, + "src": "t > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38644, + "nodeLength": 27, + "src": "0 !== t && (t > 0 ? \"right\" : \"left\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38644, + "nodeLength": 5, + "src": "0 !== t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38652, + "nodeLength": 3, + "src": "t > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38829, + "nodeLength": 34, + "src": "t.connectWith.constructor === String", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38998, + "nodeLength": 4, + "src": "l && e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39020, + "nodeLength": 4, + "src": "s >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39073, + "nodeLength": 4, + "src": "n >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39117, + "nodeLength": 195, + "src": "a && a !== this && !a.options.disabled && h.push([t.isFunction(a.options.items) ? a.options.items.call(a.element) : t(a.options.items, a.element).not(\".ui-sortable-helper\").not(\".ui-sortable-placeholder\"), a])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39120, + "nodeLength": 192, + "src": "a !== this && !a.options.disabled && h.push([t.isFunction(a.options.items) ? a.options.items.call(a.element) : t(a.options.items, a.element).not(\".ui-sortable-helper\").not(\".ui-sortable-placeholder\"), a])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39120, + "nodeLength": 8, + "src": "a !== this", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39130, + "nodeLength": 182, + "src": "!a.options.disabled && h.push([t.isFunction(a.options.items) ? a.options.items.call(a.element) : t(a.options.items, a.element).not(\".ui-sortable-helper\").not(\".ui-sortable-placeholder\"), a])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39159, + "nodeLength": 29, + "src": "t.isFunction(a.options.items)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39325, + "nodeLength": 32, + "src": "t.isFunction(this.options.items)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39560, + "nodeLength": 4, + "src": "s >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39750, + "nodeLength": 10, + "src": "e.length > i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39768, + "nodeLength": 16, + "src": "e[i] === t.item[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39906, + "nodeLength": 32, + "src": "t.isFunction(this.options.items)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40073, + "nodeLength": 13, + "src": "d && this.ready", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40104, + "nodeLength": 4, + "src": "i >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40157, + "nodeLength": 4, + "src": "s >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40201, + "nodeLength": 191, + "src": "o && o !== this && !o.options.disabled && (u.push([t.isFunction(o.options.items) ? o.options.items.call(o.element[0], e, {\n item: this.currentItem}) : t(o.options.items, o.element), o]) , this.containers.push(o))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40204, + "nodeLength": 188, + "src": "o !== this && !o.options.disabled && (u.push([t.isFunction(o.options.items) ? o.options.items.call(o.element[0], e, {\n item: this.currentItem}) : t(o.options.items, o.element), o]) , this.containers.push(o))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40204, + "nodeLength": 8, + "src": "o !== this", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40214, + "nodeLength": 178, + "src": "!o.options.disabled && (u.push([t.isFunction(o.options.items) ? o.options.items.call(o.element[0], e, {\n item: this.currentItem}) : t(o.options.items, o.element), o]) , this.containers.push(o))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40244, + "nodeLength": 29, + "src": "t.isFunction(o.options.items)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40410, + "nodeLength": 4, + "src": "i >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40458, + "nodeLength": 3, + "src": "l > s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40612, + "nodeLength": 17, + "src": "this.items.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40630, + "nodeLength": 61, + "src": "\"x\" === this.options.axis || this._isFloating(this.items[0].item)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40630, + "nodeLength": 23, + "src": "\"x\" === this.options.axis", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40695, + "nodeLength": 76, + "src": "this.offsetParent && this.helper && (this.offset.parent = this._getParentOffset())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40714, + "nodeLength": 57, + "src": "this.helper && (this.offset.parent = this._getParentOffset())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40810, + "nodeLength": 4, + "src": "i >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40835, + "nodeLength": 264, + "src": "s.instance !== this.currentContainer && this.currentContainer && s.item[0] !== this.currentItem[0] || (n = this.options.toleranceElement ? t(this.options.toleranceElement, s.item) : s.item , e || (s.width = n.outerWidth() , s.height = n.outerHeight()) , o = n.offset() , s.left = o.left , s.top = o.top)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40835, + "nodeLength": 90, + "src": "s.instance !== this.currentContainer && this.currentContainer && s.item[0] !== this.currentItem[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40835, + "nodeLength": 34, + "src": "s.instance !== this.currentContainer", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40871, + "nodeLength": 54, + "src": "this.currentContainer && s.item[0] !== this.currentItem[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40894, + "nodeLength": 31, + "src": "s.item[0] !== this.currentItem[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40930, + "nodeLength": 29, + "src": "this.options.toleranceElement", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41007, + "nodeLength": 52, + "src": "e || (s.width = n.outerWidth() , s.height = n.outerHeight())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41103, + "nodeLength": 58, + "src": "this.options.custom && this.options.custom.refreshContainers", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41247, + "nodeLength": 4, + "src": "i >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41592, + "nodeLength": 7, + "src": "e || this", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41618, + "nodeLength": 880, + "src": "s.placeholder && s.placeholder.constructor !== String || (i = s.placeholder , s.placeholder = {\n element: function() {\n var s = e.currentItem[0].nodeName.toLowerCase(), n = t(\"<\" + s + \">\", e.document[0]);\n return e._addClass(n, \"ui-sortable-placeholder\", i || e.currentItem[0].className)._removeClass(n, \"ui-sortable-helper\") , \"tbody\" === s ? e._createTrPlaceholder(e.currentItem.find(\"tr\").eq(0), t(\"\", e.document[0]).appendTo(n)) : \"tr\" === s ? e._createTrPlaceholder(e.currentItem, n) : \"img\" === s && n.attr(\"src\", e.currentItem.attr(\"src\")) , i || n.css(\"visibility\", \"hidden\") , n;\n}, \n update: function(t, n) {\n (!i || s.forcePlaceholderSize) && (n.height() || n.height(e.currentItem.innerHeight() - parseInt(e.currentItem.css(\"paddingTop\") || 0, 10) - parseInt(e.currentItem.css(\"paddingBottom\") || 0, 10)) , n.width() || n.width(e.currentItem.innerWidth() - parseInt(e.currentItem.css(\"paddingLeft\") || 0, 10) - parseInt(e.currentItem.css(\"paddingRight\") || 0, 10)));\n}})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41618, + "nodeLength": 49, + "src": "s.placeholder && s.placeholder.constructor !== String", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41633, + "nodeLength": 34, + "src": "s.placeholder.constructor !== String", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41842, + "nodeLength": 29, + "src": "i || e.currentItem[0].className", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 41910, + "nodeLength": 11, + "src": "\"tbody\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42013, + "nodeLength": 8, + "src": "\"tr\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42062, + "nodeLength": 50, + "src": "\"img\" === s && n.attr(\"src\", e.currentItem.attr(\"src\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42062, + "nodeLength": 9, + "src": "\"img\" === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42113, + "nodeLength": 31, + "src": "i || n.css(\"visibility\", \"hidden\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42170, + "nodeLength": 325, + "src": "(!i || s.forcePlaceholderSize) && (n.height() || n.height(e.currentItem.innerHeight() - parseInt(e.currentItem.css(\"paddingTop\") || 0, 10) - parseInt(e.currentItem.css(\"paddingBottom\") || 0, 10)) , n.width() || n.width(e.currentItem.innerWidth() - parseInt(e.currentItem.css(\"paddingLeft\") || 0, 10) - parseInt(e.currentItem.css(\"paddingRight\") || 0, 10)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42170, + "nodeLength": 26, + "src": "!i || s.forcePlaceholderSize", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42200, + "nodeLength": 148, + "src": "n.height() || n.height(e.currentItem.innerHeight() - parseInt(e.currentItem.css(\"paddingTop\") || 0, 10) - parseInt(e.currentItem.css(\"paddingBottom\") || 0, 10))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42258, + "nodeLength": 34, + "src": "e.currentItem.css(\"paddingTop\") || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42306, + "nodeLength": 37, + "src": "e.currentItem.css(\"paddingBottom\") || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42349, + "nodeLength": 145, + "src": "n.width() || n.width(e.currentItem.innerWidth() - parseInt(e.currentItem.css(\"paddingLeft\") || 0, 10) - parseInt(e.currentItem.css(\"paddingRight\") || 0, 10))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42404, + "nodeLength": 35, + "src": "e.currentItem.css(\"paddingLeft\") || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42453, + "nodeLength": 36, + "src": "e.currentItem.css(\"paddingRight\") || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42767, + "nodeLength": 26, + "src": "t(this).attr(\"colspan\") || 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42910, + "nodeLength": 4, + "src": "i >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42922, + "nodeLength": 62, + "src": "!t.contains(this.currentItem[0], this.containers[i].element[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42988, + "nodeLength": 55, + "src": "this._intersectsWith(this.containers[i].containerCache)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43048, + "nodeLength": 57, + "src": "d && t.contains(this.containers[i].element[0], d.element[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43145, + "nodeLength": 138, + "src": "this.containers[i].containerCache.over && (this.containers[i]._trigger(\"out\", e, this._uiHash(this)) , this.containers[i].containerCache.over = 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43287, + "nodeLength": 1, + "src": "d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43292, + "nodeLength": 26, + "src": "1 === this.containers.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43319, + "nodeLength": 139, + "src": "this.containers[p].containerCache.over || (this.containers[p]._trigger(\"over\", e, this._uiHash(this)) , this.containers[p].containerCache.over = 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43483, + "nodeLength": 46, + "src": "d.floating || this._isFloating(this.currentItem)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43532, + "nodeLength": 1, + "src": "c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43549, + "nodeLength": 1, + "src": "c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43570, + "nodeLength": 1, + "src": "c", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43610, + "nodeLength": 4, + "src": "s >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43619, + "nodeLength": 269, + "src": "t.contains(this.containers[p].element[0], this.items[s].item[0]) && this.items[s].item[0] !== this.currentItem[0] && (h = this.items[s].item.offset()[a] , l = !1 , e[u] - h > this.items[s][r] / 2 && (l = !0) , n > Math.abs(e[u] - h) && (n = Math.abs(e[u] - h) , o = this.items[s] , this.direction = l ? \"up\" : \"down\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43684, + "nodeLength": 204, + "src": "this.items[s].item[0] !== this.currentItem[0] && (h = this.items[s].item.offset()[a] , l = !1 , e[u] - h > this.items[s][r] / 2 && (l = !0) , n > Math.abs(e[u] - h) && (n = Math.abs(e[u] - h) , o = this.items[s] , this.direction = l ? \"up\" : \"down\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43684, + "nodeLength": 43, + "src": "this.items[s].item[0] !== this.currentItem[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43768, + "nodeLength": 33, + "src": "e[u] - h > this.items[s][r] / 2 && (l = !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43768, + "nodeLength": 25, + "src": "e[u] - h > this.items[s][r] / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43802, + "nodeLength": 85, + "src": "n > Math.abs(e[u] - h) && (n = Math.abs(e[u] - h) , o = this.items[s] , this.direction = l ? \"up\" : \"down\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43802, + "nodeLength": 18, + "src": "n > Math.abs(e[u] - h)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43873, + "nodeLength": 1, + "src": "l", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43892, + "nodeLength": 29, + "src": "!o && !this.options.dropOnEmpty", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43932, + "nodeLength": 42, + "src": "this.currentContainer === this.containers[p]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43982, + "nodeLength": 141, + "src": "this.currentContainer.containerCache.over || (this.containers[p]._trigger(\"over\", e, this._uiHash()) , this.currentContainer.containerCache.over = 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 44131, + "nodeLength": 1, + "src": "o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 44576, + "nodeLength": 22, + "src": "t.isFunction(i.helper)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 44655, + "nodeLength": 18, + "src": "\"clone\" === i.helper", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 44723, + "nodeLength": 113, + "src": "s.parents(\"body\").length || t(\"parent\" !== i.appendTo ? i.appendTo : this.currentItem[0].parentNode)[0].appendChild(s[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 44751, + "nodeLength": 21, + "src": "\"parent\" !== i.appendTo", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 44837, + "nodeLength": 233, + "src": "s[0] === this.currentItem[0] && (this._storedCSS = {\n width: this.currentItem[0].style.width, \n height: this.currentItem[0].style.height, \n position: this.currentItem.css(\"position\"), \n top: this.currentItem.css(\"top\"), \n left: this.currentItem.css(\"left\")})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 44837, + "nodeLength": 26, + "src": "s[0] === this.currentItem[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45072, + "nodeLength": 72, + "src": "(!s[0].style.width || i.forceHelperSize) && s.width(this.currentItem.width())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45072, + "nodeLength": 36, + "src": "!s[0].style.width || i.forceHelperSize", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45146, + "nodeLength": 75, + "src": "(!s[0].style.height || i.forceHelperSize) && s.height(this.currentItem.height())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45146, + "nodeLength": 37, + "src": "!s[0].style.height || i.forceHelperSize", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45261, + "nodeLength": 36, + "src": "\"string\" == typeof e && (e = e.split(\" \"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45261, + "nodeLength": 18, + "src": "\"string\" == typeof e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45298, + "nodeLength": 43, + "src": "t.isArray(e) && (e = {\n left: +e[0], \n top: +e[1] || 0})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45331, + "nodeLength": 8, + "src": "+e[1] || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45342, + "nodeLength": 61, + "src": "\"left\" in e && (this.offset.click.left = e.left + this.margins.left)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45404, + "nodeLength": 92, + "src": "\"right\" in e && (this.offset.click.left = this.helperProportions.width - e.right + this.margins.left)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45497, + "nodeLength": 57, + "src": "\"top\" in e && (this.offset.click.top = e.top + this.margins.top)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45555, + "nodeLength": 93, + "src": "\"bottom\" in e && (this.offset.click.top = this.helperProportions.height - e.bottom + this.margins.top)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45762, + "nodeLength": 204, + "src": "\"absolute\" === this.cssPosition && this.scrollParent[0] !== this.document[0] && t.contains(this.scrollParent[0], this.offsetParent[0]) && (e.left += this.scrollParent.scrollLeft() , e.top += this.scrollParent.scrollTop())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45762, + "nodeLength": 29, + "src": "\"absolute\" === this.cssPosition", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45793, + "nodeLength": 173, + "src": "this.scrollParent[0] !== this.document[0] && t.contains(this.scrollParent[0], this.offsetParent[0]) && (e.left += this.scrollParent.scrollLeft() , e.top += this.scrollParent.scrollTop())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45793, + "nodeLength": 39, + "src": "this.scrollParent[0] !== this.document[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45834, + "nodeLength": 132, + "src": "t.contains(this.scrollParent[0], this.offsetParent[0]) && (e.left += this.scrollParent.scrollLeft() , e.top += this.scrollParent.scrollTop())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45968, + "nodeLength": 157, + "src": "(this.offsetParent[0] === this.document[0].body || this.offsetParent[0].tagName && \"html\" === this.offsetParent[0].tagName.toLowerCase() && t.ui.ie) && (e = {\n top: 0, \n left: 0})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45968, + "nodeLength": 136, + "src": "this.offsetParent[0] === this.document[0].body || this.offsetParent[0].tagName && \"html\" === this.offsetParent[0].tagName.toLowerCase() && t.ui.ie", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45968, + "nodeLength": 44, + "src": "this.offsetParent[0] === this.document[0].body", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46014, + "nodeLength": 90, + "src": "this.offsetParent[0].tagName && \"html\" === this.offsetParent[0].tagName.toLowerCase() && t.ui.ie", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46044, + "nodeLength": 60, + "src": "\"html\" === this.offsetParent[0].tagName.toLowerCase() && t.ui.ie", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46044, + "nodeLength": 51, + "src": "\"html\" === this.offsetParent[0].tagName.toLowerCase()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46138, + "nodeLength": 55, + "src": "parseInt(this.offsetParent.css(\"borderTopWidth\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46208, + "nodeLength": 56, + "src": "parseInt(this.offsetParent.css(\"borderLeftWidth\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46301, + "nodeLength": 29, + "src": "\"relative\" === this.cssPosition", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46384, + "nodeLength": 38, + "src": "parseInt(this.helper.css(\"top\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46467, + "nodeLength": 39, + "src": "parseInt(this.helper.css(\"left\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46606, + "nodeLength": 50, + "src": "parseInt(this.currentItem.css(\"marginLeft\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46661, + "nodeLength": 49, + "src": "parseInt(this.currentItem.css(\"marginTop\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46890, + "nodeLength": 67, + "src": "\"parent\" === n.containment && (n.containment = this.helper[0].parentNode)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46890, + "nodeLength": 24, + "src": "\"parent\" === n.containment", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46959, + "nodeLength": 499, + "src": "(\"document\" === n.containment || \"window\" === n.containment) && (this.containment = [0 - this.offset.relative.left - this.offset.parent.left, 0 - this.offset.relative.top - this.offset.parent.top, \"document\" === n.containment ? this.document.width() : this.window.width() - this.helperProportions.width - this.margins.left, (\"document\" === n.containment ? this.document.height() || document.body.parentNode.scrollHeight : this.window.height() || this.document[0].body.parentNode.scrollHeight) - this.helperProportions.height - this.margins.top])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46959, + "nodeLength": 52, + "src": "\"document\" === n.containment || \"window\" === n.containment", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46959, + "nodeLength": 26, + "src": "\"document\" === n.containment", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 46987, + "nodeLength": 24, + "src": "\"window\" === n.containment", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47135, + "nodeLength": 26, + "src": "\"document\" === n.containment", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47252, + "nodeLength": 26, + "src": "\"document\" === n.containment", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47279, + "nodeLength": 61, + "src": "this.document.height() || document.body.parentNode.scrollHeight", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47341, + "nodeLength": 67, + "src": "this.window.height() || this.document[0].body.parentNode.scrollHeight", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47459, + "nodeLength": 776, + "src": "/^(document|window|parent)$/.test(n.containment) || (e = t(n.containment)[0] , i = t(n.containment).offset() , s = \"hidden\" !== t(e).css(\"overflow\") , this.containment = [i.left + (parseInt(t(e).css(\"borderLeftWidth\"), 10) || 0) + (parseInt(t(e).css(\"paddingLeft\"), 10) || 0) - this.margins.left, i.top + (parseInt(t(e).css(\"borderTopWidth\"), 10) || 0) + (parseInt(t(e).css(\"paddingTop\"), 10) || 0) - this.margins.top, i.left + (s ? Math.max(e.scrollWidth, e.offsetWidth) : e.offsetWidth) - (parseInt(t(e).css(\"borderLeftWidth\"), 10) || 0) - (parseInt(t(e).css(\"paddingRight\"), 10) || 0) - this.helperProportions.width - this.margins.left, i.top + (s ? Math.max(e.scrollHeight, e.offsetHeight) : e.offsetHeight) - (parseInt(t(e).css(\"borderTopWidth\"), 10) || 0) - (parseInt(t(e).css(\"paddingBottom\"), 10) || 0) - this.helperProportions.height - this.margins.top])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47562, + "nodeLength": 31, + "src": "\"hidden\" !== t(e).css(\"overflow\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47620, + "nodeLength": 43, + "src": "parseInt(t(e).css(\"borderLeftWidth\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47666, + "nodeLength": 39, + "src": "parseInt(t(e).css(\"paddingLeft\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47732, + "nodeLength": 42, + "src": "parseInt(t(e).css(\"borderTopWidth\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47777, + "nodeLength": 38, + "src": "parseInt(t(e).css(\"paddingTop\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47842, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47898, + "nodeLength": 43, + "src": "parseInt(t(e).css(\"borderLeftWidth\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47944, + "nodeLength": 40, + "src": "parseInt(t(e).css(\"paddingRight\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48040, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48099, + "nodeLength": 42, + "src": "parseInt(t(e).css(\"borderTopWidth\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48144, + "nodeLength": 41, + "src": "parseInt(t(e).css(\"paddingBottom\"), 10) || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48270, + "nodeLength": 20, + "src": "i || (i = this.position)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48297, + "nodeLength": 14, + "src": "\"absolute\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48319, + "nodeLength": 125, + "src": "\"absolute\" !== this.cssPosition || this.scrollParent[0] !== this.document[0] && t.contains(this.scrollParent[0], this.offsetParent[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48319, + "nodeLength": 29, + "src": "\"absolute\" !== this.cssPosition", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48350, + "nodeLength": 94, + "src": "this.scrollParent[0] !== this.document[0] && t.contains(this.scrollParent[0], this.offsetParent[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48350, + "nodeLength": 39, + "src": "this.scrollParent[0] !== this.document[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48587, + "nodeLength": 26, + "src": "\"fixed\" === this.cssPosition", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48645, + "nodeLength": 1, + "src": "o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48733, + "nodeLength": 26, + "src": "\"fixed\" === this.cssPosition", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48792, + "nodeLength": 1, + "src": "o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48891, + "nodeLength": 125, + "src": "\"absolute\" !== this.cssPosition || this.scrollParent[0] !== this.document[0] && t.contains(this.scrollParent[0], this.offsetParent[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48891, + "nodeLength": 29, + "src": "\"absolute\" !== this.cssPosition", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48922, + "nodeLength": 94, + "src": "this.scrollParent[0] !== this.document[0] && t.contains(this.scrollParent[0], this.offsetParent[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 48922, + "nodeLength": 39, + "src": "this.scrollParent[0] !== this.document[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49095, + "nodeLength": 165, + "src": "\"relative\" !== this.cssPosition || this.scrollParent[0] !== this.document[0] && this.scrollParent[0] !== this.offsetParent[0] || (this.offset.relative = this._getRelativeOffset())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49095, + "nodeLength": 29, + "src": "\"relative\" !== this.cssPosition", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49126, + "nodeLength": 134, + "src": "this.scrollParent[0] !== this.document[0] && this.scrollParent[0] !== this.offsetParent[0] || (this.offset.relative = this._getRelativeOffset())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49126, + "nodeLength": 84, + "src": "this.scrollParent[0] !== this.document[0] && this.scrollParent[0] !== this.offsetParent[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49126, + "nodeLength": 39, + "src": "this.scrollParent[0] !== this.document[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49167, + "nodeLength": 43, + "src": "this.scrollParent[0] !== this.offsetParent[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49261, + "nodeLength": 967, + "src": "this.originalPosition && (this.containment && (e.pageX - this.offset.click.left < this.containment[0] && (o = this.containment[0] + this.offset.click.left) , e.pageY - this.offset.click.top < this.containment[1] && (a = this.containment[1] + this.offset.click.top) , e.pageX - this.offset.click.left > this.containment[2] && (o = this.containment[2] + this.offset.click.left) , e.pageY - this.offset.click.top > this.containment[3] && (a = this.containment[3] + this.offset.click.top)) , n.grid && (i = this.originalPageY + Math.round((a - this.originalPageY) / n.grid[1]) * n.grid[1] , a = this.containment ? i - this.offset.click.top >= this.containment[1] && i - this.offset.click.top <= this.containment[3] ? i : i - this.offset.click.top >= this.containment[1] ? i - n.grid[1] : i + n.grid[1] : i , s = this.originalPageX + Math.round((o - this.originalPageX) / n.grid[0]) * n.grid[0] , o = this.containment ? s - this.offset.click.left >= this.containment[0] && s - this.offset.click.left <= this.containment[2] ? s : s - this.offset.click.left >= this.containment[0] ? s - n.grid[0] : s + n.grid[0] : s))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49285, + "nodeLength": 411, + "src": "this.containment && (e.pageX - this.offset.click.left < this.containment[0] && (o = this.containment[0] + this.offset.click.left) , e.pageY - this.offset.click.top < this.containment[1] && (a = this.containment[1] + this.offset.click.top) , e.pageX - this.offset.click.left > this.containment[2] && (o = this.containment[2] + this.offset.click.left) , e.pageY - this.offset.click.top > this.containment[3] && (a = this.containment[3] + this.offset.click.top))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49304, + "nodeLength": 98, + "src": "e.pageX - this.offset.click.left < this.containment[0] && (o = this.containment[0] + this.offset.click.left)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49304, + "nodeLength": 50, + "src": "e.pageX - this.offset.click.left < this.containment[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49403, + "nodeLength": 96, + "src": "e.pageY - this.offset.click.top < this.containment[1] && (a = this.containment[1] + this.offset.click.top)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49403, + "nodeLength": 49, + "src": "e.pageY - this.offset.click.top < this.containment[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49500, + "nodeLength": 98, + "src": "e.pageX - this.offset.click.left > this.containment[2] && (o = this.containment[2] + this.offset.click.left)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49500, + "nodeLength": 50, + "src": "e.pageX - this.offset.click.left > this.containment[2]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49599, + "nodeLength": 96, + "src": "e.pageY - this.offset.click.top > this.containment[3] && (a = this.containment[3] + this.offset.click.top)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49599, + "nodeLength": 49, + "src": "e.pageY - this.offset.click.top > this.containment[3]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49697, + "nodeLength": 530, + "src": "n.grid && (i = this.originalPageY + Math.round((a - this.originalPageY) / n.grid[1]) * n.grid[1] , a = this.containment ? i - this.offset.click.top >= this.containment[1] && i - this.offset.click.top <= this.containment[3] ? i : i - this.offset.click.top >= this.containment[1] ? i - n.grid[1] : i + n.grid[1] : i , s = this.originalPageX + Math.round((o - this.originalPageX) / n.grid[0]) * n.grid[0] , o = this.containment ? s - this.offset.click.left >= this.containment[0] && s - this.offset.click.left <= this.containment[2] ? s : s - this.offset.click.left >= this.containment[0] ? s - n.grid[0] : s + n.grid[0] : s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49784, + "nodeLength": 16, + "src": "this.containment", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49801, + "nodeLength": 90, + "src": "i - this.offset.click.top >= this.containment[1] && i - this.offset.click.top <= this.containment[3]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49801, + "nodeLength": 44, + "src": "i - this.offset.click.top >= this.containment[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49847, + "nodeLength": 44, + "src": "i - this.offset.click.top <= this.containment[3]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49894, + "nodeLength": 44, + "src": "i - this.offset.click.top >= this.containment[1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50043, + "nodeLength": 16, + "src": "this.containment", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50060, + "nodeLength": 92, + "src": "s - this.offset.click.left >= this.containment[0] && s - this.offset.click.left <= this.containment[2]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50060, + "nodeLength": 45, + "src": "s - this.offset.click.left >= this.containment[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50107, + "nodeLength": 45, + "src": "s - this.offset.click.left <= this.containment[2]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50155, + "nodeLength": 45, + "src": "s - this.offset.click.left >= this.containment[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50307, + "nodeLength": 26, + "src": "\"fixed\" === this.cssPosition", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50365, + "nodeLength": 1, + "src": "h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50465, + "nodeLength": 26, + "src": "\"fixed\" === this.cssPosition", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50524, + "nodeLength": 1, + "src": "h", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50575, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50669, + "nodeLength": 23, + "src": "\"down\" === this.direction", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50739, + "nodeLength": 12, + "src": "this.counter", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "13": [ + null, + { + "position": 237, + "nodeLength": 43, + "src": "n === this.counter && this.refreshPositions(!s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 237, + "nodeLength": 16, + "src": "n === this.counter", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22323, + "nodeLength": 155, + "src": "!this._noFinalSort && this.currentItem.parent().length && this.placeholder.before(this.currentItem) , this._noFinalSort = null , this.helper[0] === this.currentItem[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22323, + "nodeLength": 95, + "src": "!this._noFinalSort && this.currentItem.parent().length && this.placeholder.before(this.currentItem)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22343, + "nodeLength": 75, + "src": "this.currentItem.parent().length && this.placeholder.before(this.currentItem)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22442, + "nodeLength": 36, + "src": "this.helper[0] === this.currentItem[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22506, + "nodeLength": 84, + "src": "(\"auto\" === this._storedCSS[s] || \"static\" === this._storedCSS[s]) && (this._storedCSS[s] = \"\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22506, + "nodeLength": 58, + "src": "\"auto\" === this._storedCSS[s] || \"static\" === this._storedCSS[s]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22506, + "nodeLength": 27, + "src": "\"auto\" === this._storedCSS[s]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22535, + "nodeLength": 29, + "src": "\"static\" === this._storedCSS[s]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22719, + "nodeLength": 100, + "src": "this.fromOutside && !e && n.push(function(t) {\n this._trigger(\"receive\", t, this._uiHash(this.fromOutside));\n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22737, + "nodeLength": 82, + "src": "!e && n.push(function(t) {\n this._trigger(\"receive\", t, this._uiHash(this.fromOutside));\n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22820, + "nodeLength": 218, + "src": "!this.fromOutside && this.domPosition.prev === this.currentItem.prev().not(\".ui-sortable-helper\")[0] && this.domPosition.parent === this.currentItem.parent()[0] || e || n.push(function(t) {\n this._trigger(\"update\", t, this._uiHash());\n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22820, + "nodeLength": 152, + "src": "!this.fromOutside && this.domPosition.prev === this.currentItem.prev().not(\".ui-sortable-helper\")[0] && this.domPosition.parent === this.currentItem.parent()[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22839, + "nodeLength": 133, + "src": "this.domPosition.prev === this.currentItem.prev().not(\".ui-sortable-helper\")[0] && this.domPosition.parent === this.currentItem.parent()[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22839, + "nodeLength": 77, + "src": "this.domPosition.prev === this.currentItem.prev().not(\".ui-sortable-helper\")[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22918, + "nodeLength": 54, + "src": "this.domPosition.parent === this.currentItem.parent()[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 22974, + "nodeLength": 64, + "src": "e || n.push(function(t) {\n this._trigger(\"update\", t, this._uiHash());\n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23039, + "nodeLength": 331, + "src": "this !== this.currentContainer && (e || (n.push(function(t) {\n this._trigger(\"remove\", t, this._uiHash());\n}) , n.push(function(t) {\n return function(e) {\n t._trigger(\"receive\", e, this._uiHash(this));\n};\n}.call(this, this.currentContainer)) , n.push(function(t) {\n return function(e) {\n t._trigger(\"update\", e, this._uiHash(this));\n};\n}.call(this, this.currentContainer))))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23039, + "nodeLength": 28, + "src": "this !== this.currentContainer", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23070, + "nodeLength": 299, + "src": "e || (n.push(function(t) {\n this._trigger(\"remove\", t, this._uiHash());\n}) , n.push(function(t) {\n return function(e) {\n t._trigger(\"receive\", e, this._uiHash(this));\n};\n}.call(this, this.currentContainer)) , n.push(function(t) {\n return function(e) {\n t._trigger(\"update\", e, this._uiHash(this));\n};\n}.call(this, this.currentContainer)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23398, + "nodeLength": 4, + "src": "s >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23407, + "nodeLength": 50, + "src": "e || n.push(i(\"deactivate\", this, this.containers[s]))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23458, + "nodeLength": 123, + "src": "this.containers[s].containerCache.over && (n.push(i(\"out\", this, this.containers[s])) , this.containers[s].containerCache.over = 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23585, + "nodeLength": 510, + "src": "this.storedCursor && (this.document.find(\"body\").css(\"cursor\", this.storedCursor) , this.storedStylesheet.remove()) , this._storedOpacity && this.helper.css(\"opacity\", this._storedOpacity) , this._storedZIndex && this.helper.css(\"zIndex\", \"auto\" === this._storedZIndex ? \"\" : this._storedZIndex) , this.dragging = !1 , e || this._trigger(\"beforeStop\", t, this._uiHash()) , this.placeholder[0].parentNode.removeChild(this.placeholder[0]) , this.cancelHelperRemoval || (this.helper[0] !== this.currentItem[0] && this.helper.remove() , this.helper = null) , !e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23585, + "nodeLength": 110, + "src": "this.storedCursor && (this.document.find(\"body\").css(\"cursor\", this.storedCursor) , this.storedStylesheet.remove())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23696, + "nodeLength": 67, + "src": "this._storedOpacity && this.helper.css(\"opacity\", this._storedOpacity)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23764, + "nodeLength": 95, + "src": "this._storedZIndex && this.helper.css(\"zIndex\", \"auto\" === this._storedZIndex ? \"\" : this._storedZIndex)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23809, + "nodeLength": 27, + "src": "\"auto\" === this._storedZIndex", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23877, + "nodeLength": 47, + "src": "e || this._trigger(\"beforeStop\", t, this._uiHash())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23989, + "nodeLength": 103, + "src": "this.cancelHelperRemoval || (this.helper[0] !== this.currentItem[0] && this.helper.remove() , this.helper = null)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24016, + "nodeLength": 58, + "src": "this.helper[0] !== this.currentItem[0] && this.helper.remove()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24016, + "nodeLength": 36, + "src": "this.helper[0] !== this.currentItem[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24105, + "nodeLength": 10, + "src": "n.length > s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24251, + "nodeLength": 69, + "src": "t.Widget.prototype._trigger.apply(this, arguments) === !1 && this.cancel()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24251, + "nodeLength": 54, + "src": "t.Widget.prototype._trigger.apply(this, arguments) === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24348, + "nodeLength": 7, + "src": "e || this", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24391, + "nodeLength": 20, + "src": "i.placeholder || t([])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24515, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53720, + "nodeLength": 53, + "src": "\"\" !== this.value() && this._value(this.element.val(), !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53720, + "nodeLength": 17, + "src": "\"\" !== this.value()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54045, + "nodeLength": 27, + "src": "null != n && n.length && (e[s] = n)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54045, + "nodeLength": 7, + "src": "null != n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54054, + "nodeLength": 18, + "src": "n.length && (e[s] = n)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54107, + "nodeLength": 52, + "src": "this._start(t) && this._keydown(t) && t.preventDefault()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54123, + "nodeLength": 36, + "src": "this._keydown(t) && t.preventDefault()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54250, + "nodeLength": 15, + "src": "this.cancelBlur", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54328, + "nodeLength": 61, + "src": "this.previous !== this.element.val() && this._trigger(\"change\", t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54328, + "nodeLength": 34, + "src": "this.previous !== this.element.val()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54427, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54433, + "nodeLength": 31, + "src": "!this.spinning && !this._start(t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54486, + "nodeLength": 3, + "src": "e > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54596, + "nodeLength": 28, + "src": "this.spinning && this._stop(t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54714, + "nodeLength": 58, + "src": "this.element[0] === t.ui.safeActiveElement(this.document[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54773, + "nodeLength": 91, + "src": "e || (this.element.trigger(\"focus\") , this.previous = s , this._delay(function() {\n this.previous = s;\n}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 54873, + "nodeLength": 58, + "src": "this.element[0] === t.ui.safeActiveElement(this.document[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55077, + "nodeLength": 91, + "src": "this._start(e) !== !1 && this._repeat(null, t(e.currentTarget).hasClass(\"ui-spinner-up\") ? 1 : -1, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55077, + "nodeLength": 19, + "src": "this._start(e) !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55116, + "nodeLength": 44, + "src": "t(e.currentTarget).hasClass(\"ui-spinner-up\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55258, + "nodeLength": 46, + "src": "t(e.currentTarget).hasClass(\"ui-state-active\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55305, + "nodeLength": 19, + "src": "this._start(e) === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55347, + "nodeLength": 44, + "src": "t(e.currentTarget).hasClass(\"ui-spinner-up\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56217, + "nodeLength": 134, + "src": "this.buttons.height() > Math.ceil(.5 * this.uiSpinner.height()) && this.uiSpinner.height() > 0 && this.uiSpinner.height(this.uiSpinner.height())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56217, + "nodeLength": 59, + "src": "this.buttons.height() > Math.ceil(.5 * this.uiSpinner.height())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56278, + "nodeLength": 73, + "src": "this.uiSpinner.height() > 0 && this.uiSpinner.height(this.uiSpinner.height())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56278, + "nodeLength": 25, + "src": "this.uiSpinner.height() > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56660, + "nodeLength": 44, + "src": "this.spinning || this._trigger(\"start\", t) !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56675, + "nodeLength": 29, + "src": "this._trigger(\"start\", t) !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56706, + "nodeLength": 30, + "src": "this.counter || (this.counter = 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56788, + "nodeLength": 6, + "src": "t || 500", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56940, + "nodeLength": 15, + "src": "this.value() || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56956, + "nodeLength": 30, + "src": "this.counter || (this.counter = 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57042, + "nodeLength": 86, + "src": "this.spinning && this._trigger(\"spin\", e, {\n value: i}) === !1 || (this._value(i) , this.counter++)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57042, + "nodeLength": 53, + "src": "this.spinning && this._trigger(\"spin\", e, {\n value: i}) === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57057, + "nodeLength": 38, + "src": "this._trigger(\"spin\", e, {\n value: i}) === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57191, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57193, + "nodeLength": 15, + "src": "t.isFunction(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57330, + "nodeLength": 76, + "src": "null !== this.options.min && (t = Math.max(t, this._precisionOf(this.options.min)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57330, + "nodeLength": 23, + "src": "null !== this.options.min", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57469, + "nodeLength": 6, + "src": "-1 === i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57549, + "nodeLength": 12, + "src": "null !== s.min", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57655, + "nodeLength": 21, + "src": "null !== s.max && t > s.max", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57655, + "nodeLength": 12, + "src": "null !== s.max", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57669, + "nodeLength": 7, + "src": "t > s.max", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57683, + "nodeLength": 21, + "src": "null !== s.min && s.min > t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57683, + "nodeLength": 12, + "src": "null !== s.min", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57697, + "nodeLength": 7, + "src": "s.min > t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57732, + "nodeLength": 132, + "src": "this.spinning && (clearTimeout(this.timer) , clearTimeout(this.mousewheelTimer) , this.counter = 0 , this.spinning = !1 , this._trigger(\"stop\", t))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57907, + "nodeLength": 33, + "src": "\"culture\" === t || \"numberFormat\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57907, + "nodeLength": 13, + "src": "\"culture\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57922, + "nodeLength": 18, + "src": "\"numberFormat\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58038, + "nodeLength": 73, + "src": "(\"max\" === t || \"min\" === t || \"step\" === t) && \"string\" == typeof e && (e = this._parse(e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58038, + "nodeLength": 32, + "src": "\"max\" === t || \"min\" === t || \"step\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58038, + "nodeLength": 9, + "src": "\"max\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58049, + "nodeLength": 21, + "src": "\"min\" === t || \"step\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58049, + "nodeLength": 9, + "src": "\"min\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58060, + "nodeLength": 10, + "src": "\"step\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58073, + "nodeLength": 38, + "src": "\"string\" == typeof e && (e = this._parse(e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58073, + "nodeLength": 18, + "src": "\"string\" == typeof e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58112, + "nodeLength": 249, + "src": "\"icons\" === t && (s = this.buttons.first().find(\".ui-icon\") , this._removeClass(s, null, this.options.icons.up) , this._addClass(s, null, e.up) , n = this.buttons.last().find(\".ui-icon\") , this._removeClass(n, null, this.options.icons.down) , this._addClass(n, null, e.down))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58112, + "nodeLength": 11, + "src": "\"icons\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58551, + "nodeLength": 1, + "src": "t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58642, + "nodeLength": 126, + "src": "\"string\" == typeof t && \"\" !== t && (t = window.Globalize && this.options.numberFormat ? Globalize.parseFloat(t, 10, this.options.culture) : +t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58642, + "nodeLength": 18, + "src": "\"string\" == typeof t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58662, + "nodeLength": 106, + "src": "\"\" !== t && (t = window.Globalize && this.options.numberFormat ? Globalize.parseFloat(t, 10, this.options.culture) : +t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58662, + "nodeLength": 6, + "src": "\"\" !== t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58673, + "nodeLength": 43, + "src": "window.Globalize && this.options.numberFormat", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58769, + "nodeLength": 16, + "src": "\"\" === t || isNaN(t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58769, + "nodeLength": 6, + "src": "\"\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58820, + "nodeLength": 6, + "src": "\"\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58830, + "nodeLength": 43, + "src": "window.Globalize && this.options.numberFormat", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59145, + "nodeLength": 8, + "src": "null === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59157, + "nodeLength": 24, + "src": "t === this._adjustValue(t)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59210, + "nodeLength": 84, + "src": "\"\" !== t && (i = this._parse(t) , null !== i && (e || (i = this._adjustValue(i)) , t = this._format(i)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59210, + "nodeLength": 6, + "src": "\"\" !== t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59236, + "nodeLength": 57, + "src": "null !== i && (e || (i = this._adjustValue(i)) , t = this._format(i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59236, + "nodeLength": 8, + "src": "null !== i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59247, + "nodeLength": 27, + "src": "e || (i = this._adjustValue(i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59560, + "nodeLength": 66, + "src": "this._start() && (this._spin((t || 1) * this.options.step) , this._stop())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59588, + "nodeLength": 4, + "src": "t || 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59693, + "nodeLength": 67, + "src": "this._start() && (this._spin((t || 1) * -this.options.step) , this._stop())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59721, + "nodeLength": 4, + "src": "t || 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59797, + "nodeLength": 4, + "src": "t || 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59863, + "nodeLength": 4, + "src": "t || 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59915, + "nodeLength": 16, + "src": "arguments.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 60045, + "nodeLength": 287, + "src": "t.uiBackCompat !== !1 && t.widget(\"ui.spinner\", t.ui.spinner, {\n _enhance: function() {\n this.uiSpinner = this.element.attr(\"autocomplete\", \"off\").wrap(this._uiSpinnerHtml()).parent().append(this._buttonHtml());\n}, \n _uiSpinnerHtml: function() {\n return \"\";\n}, \n _buttonHtml: function() {\n return \"\";\n}})", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 60046, + "nodeLength": 19, + "src": "t.uiBackCompat !== !1", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 60873, + "nodeLength": 22, + "src": "e.hash.length > 1 && i === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 60873, + "nodeLength": 15, + "src": "e.hash.length > 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 60890, + "nodeLength": 5, + "src": "i === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61128, + "nodeLength": 153, + "src": "t.isArray(i.disabled) && (i.disabled = t.unique(i.disabled.concat(t.map(this.tabs.filter(\".ui-state-disabled\"), function(t) {\n return e.tabs.index(t);\n}))).sort())", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61294, + "nodeLength": 45, + "src": "this.options.active !== !1 && this.anchors.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61294, + "nodeLength": 24, + "src": "this.options.active !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61387, + "nodeLength": 39, + "src": "this.active.length && this.load(i.active)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61543, + "nodeLength": 212, + "src": "null === e && (s && this.tabs.each(function(i, n) {\n return t(n).attr(\"aria-controls\") === s ? (e = i , !1) : void 0;\n}) , null === e && (e = this.tabs.index(this.tabs.filter(\".ui-tabs-active\"))) , (null === e || -1 === e) && (e = this.tabs.length ? 0 : !1))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61543, + "nodeLength": 8, + "src": "null === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61554, + "nodeLength": 87, + "src": "s && this.tabs.each(function(i, n) {\n return t(n).attr(\"aria-controls\") === s ? (e = i , !1) : void 0;\n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61593, + "nodeLength": 30, + "src": "t(n).attr(\"aria-controls\") === s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61642, + "nodeLength": 66, + "src": "null === e && (e = this.tabs.index(this.tabs.filter(\".ui-tabs-active\")))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61642, + "nodeLength": 8, + "src": "null === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61710, + "nodeLength": 44, + "src": "(null === e || -1 === e) && (e = this.tabs.length ? 0 : !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61710, + "nodeLength": 16, + "src": "null === e || -1 === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61710, + "nodeLength": 8, + "src": "null === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61720, + "nodeLength": 6, + "src": "-1 === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61732, + "nodeLength": 16, + "src": "this.tabs.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61756, + "nodeLength": 63, + "src": "e !== !1 && (e = this.tabs.index(this.tabs.eq(e)) , -1 === e && (e = i ? !1 : 0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61756, + "nodeLength": 6, + "src": "e !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61800, + "nodeLength": 18, + "src": "-1 === e && (e = i ? !1 : 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61800, + "nodeLength": 6, + "src": "-1 === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61811, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61820, + "nodeLength": 38, + "src": "!i && e === !1 && this.anchors.length && (e = 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61824, + "nodeLength": 34, + "src": "e === !1 && this.anchors.length && (e = 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61824, + "nodeLength": 6, + "src": "e === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61832, + "nodeLength": 26, + "src": "this.anchors.length && (e = 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61922, + "nodeLength": 18, + "src": "this.active.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62098, + "nodeLength": 23, + "src": "!this._handlePageNav(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62542, + "nodeLength": 23, + "src": "s === this.options.active", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62669, + "nodeLength": 176, + "src": "e.ctrlKey || e.metaKey || (i.attr(\"aria-selected\", \"false\") , this.tabs.eq(s).attr(\"aria-selected\", \"true\") , this.activating = this._delay(function() {\n this.option(\"active\", s);\n}, this.delay))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62680, + "nodeLength": 165, + "src": "e.metaKey || (i.attr(\"aria-selected\", \"false\") , this.tabs.eq(s).attr(\"aria-selected\", \"true\") , this.activating = this._delay(function() {\n this.option(\"active\", s);\n}, this.delay))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62874, + "nodeLength": 113, + "src": "this._handlePageNav(e) || e.ctrlKey && e.keyCode === t.ui.keyCode.UP && (e.preventDefault() , this.active.trigger(\"focus\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62898, + "nodeLength": 89, + "src": "e.ctrlKey && e.keyCode === t.ui.keyCode.UP && (e.preventDefault() , this.active.trigger(\"focus\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62909, + "nodeLength": 78, + "src": "e.keyCode === t.ui.keyCode.UP && (e.preventDefault() , this.active.trigger(\"focus\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62909, + "nodeLength": 27, + "src": "e.keyCode === t.ui.keyCode.UP", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63023, + "nodeLength": 42, + "src": "e.altKey && e.keyCode === t.ui.keyCode.PAGE_UP", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63033, + "nodeLength": 32, + "src": "e.keyCode === t.ui.keyCode.PAGE_UP", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63132, + "nodeLength": 44, + "src": "e.altKey && e.keyCode === t.ui.keyCode.PAGE_DOWN", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63142, + "nodeLength": 34, + "src": "e.keyCode === t.ui.keyCode.PAGE_DOWN", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63298, + "nodeLength": 10, + "src": "e > n && (e = 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63298, + "nodeLength": 3, + "src": "e > n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63309, + "nodeLength": 10, + "src": "0 > e && (e = n)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63309, + "nodeLength": 3, + "src": "0 > e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63351, + "nodeLength": 41, + "src": "-1 !== t.inArray(s(), this.options.disabled)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63396, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63543, + "nodeLength": 12, + "src": "\"active\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63601, + "nodeLength": 115, + "src": "\"collapsible\" === t && (this._toggleClass(\"ui-tabs-collapsible\", null, e) , e || this.options.active !== !1 || this._activate(0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63601, + "nodeLength": 17, + "src": "\"collapsible\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63669, + "nodeLength": 46, + "src": "e || this.options.active !== !1 || this._activate(0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63672, + "nodeLength": 43, + "src": "this.options.active !== !1 || this._activate(0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63672, + "nodeLength": 24, + "src": "this.options.active !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63717, + "nodeLength": 33, + "src": "\"event\" === t && this._setupEvents(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63717, + "nodeLength": 11, + "src": "\"event\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63751, + "nodeLength": 44, + "src": "\"heightStyle\" === t && this._setupHeightStyle(e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63751, + "nodeLength": 17, + "src": "\"heightStyle\" === t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63842, + "nodeLength": 1, + "src": "t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 64083, + "nodeLength": 34, + "src": "e.active !== !1 && this.anchors.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 64083, + "nodeLength": 13, + "src": "e.active !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 64118, + "nodeLength": 63, + "src": "this.active.length && !t.contains(this.tablist[0], this.active[0])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 64182, + "nodeLength": 36, + "src": "this.tabs.length === e.disabled.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 64731, + "nodeLength": 18, + "src": "this.active.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 65291, + "nodeLength": 52, + "src": "t(this).is(\".ui-state-disabled\") && e.preventDefault()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 65406, + "nodeLength": 59, + "src": "t(this).closest(\"li\").is(\".ui-state-disabled\") && this.blur()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 65889, + "nodeLength": 13, + "src": "e._isLocal(s)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 65975, + "nodeLength": 47, + "src": "h.attr(\"aria-controls\") || t({}).uniqueId()[0].id", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66051, + "nodeLength": 71, + "src": "o.length || (o = e._createPanel(a) , o.insertAfter(e.panels[i - 1] || e.tablist))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66096, + "nodeLength": 24, + "src": "e.panels[i - 1] || e.tablist", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66153, + "nodeLength": 36, + "src": "o.length && (e.panels = e.panels.add(o))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66190, + "nodeLength": 36, + "src": "l && h.data(\"ui-tabs-aria-controls\", l)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66405, + "nodeLength": 93, + "src": "i && (this._off(i.not(this.tabs)) , this._off(s.not(this.anchors)) , this._off(n.not(this.panels)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66527, + "nodeLength": 47, + "src": "this.tablist || this.element.find(\"ol, ul\").eq(0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66705, + "nodeLength": 68, + "src": "t.isArray(e) && (e.length ? e.length === this.anchors.length && (e = !0) : e = !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66720, + "nodeLength": 8, + "src": "e.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66729, + "nodeLength": 38, + "src": "e.length === this.anchors.length && (e = !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66729, + "nodeLength": 30, + "src": "e.length === this.anchors.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66778, + "nodeLength": 14, + "src": "s = this.tabs[n]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66804, + "nodeLength": 27, + "src": "e === !0 || -1 !== t.inArray(n, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66804, + "nodeLength": 6, + "src": "e === !0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66812, + "nodeLength": 19, + "src": "-1 !== t.inArray(n, e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 67079, + "nodeLength": 6, + "src": "e === !0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 67122, + "nodeLength": 59, + "src": "e && t.each(e.split(\" \"), function(t, e) {\n i[e] = \"_eventHandler\";\n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 67536, + "nodeLength": 10, + "src": "\"fill\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 67697, + "nodeLength": 51, + "src": "\"absolute\" !== s && \"fixed\" !== s && (i -= e.outerHeight(!0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 67697, + "nodeLength": 14, + "src": "\"absolute\" !== s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 67713, + "nodeLength": 35, + "src": "\"fixed\" !== s && (i -= e.outerHeight(!0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 67713, + "nodeLength": 11, + "src": "\"fixed\" !== s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 67960, + "nodeLength": 99, + "src": "\"auto\" === e && (i = 0 , this.panels.each(function() {\n i = Math.max(i, t(this).height(\"\").height());\n}).height(i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 67960, + "nodeLength": 10, + "src": "\"auto\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68161, + "nodeLength": 11, + "src": "o[0] === s[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68175, + "nodeLength": 16, + "src": "a && i.collapsible", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68194, + "nodeLength": 1, + "src": "r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68226, + "nodeLength": 8, + "src": "s.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68293, + "nodeLength": 1, + "src": "r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68332, + "nodeLength": 360, + "src": "o.hasClass(\"ui-state-disabled\") || o.hasClass(\"ui-tabs-loading\") || this.running || a && !i.collapsible || this._trigger(\"beforeActivate\", e, c) === !1 || (i.active = r ? !1 : this.tabs.index(o) , this.active = a ? t() : o , this.xhr && this.xhr.abort() , l.length || h.length || t.error(\"jQuery UI Tabs: Mismatching fragment identifier.\") , h.length && this.load(this.tabs.index(o), e) , this._toggle(e, c))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68365, + "nodeLength": 327, + "src": "o.hasClass(\"ui-tabs-loading\") || this.running || a && !i.collapsible || this._trigger(\"beforeActivate\", e, c) === !1 || (i.active = r ? !1 : this.tabs.index(o) , this.active = a ? t() : o , this.xhr && this.xhr.abort() , l.length || h.length || t.error(\"jQuery UI Tabs: Mismatching fragment identifier.\") , h.length && this.load(this.tabs.index(o), e) , this._toggle(e, c))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68396, + "nodeLength": 296, + "src": "this.running || a && !i.collapsible || this._trigger(\"beforeActivate\", e, c) === !1 || (i.active = r ? !1 : this.tabs.index(o) , this.active = a ? t() : o , this.xhr && this.xhr.abort() , l.length || h.length || t.error(\"jQuery UI Tabs: Mismatching fragment identifier.\") , h.length && this.load(this.tabs.index(o), e) , this._toggle(e, c))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68410, + "nodeLength": 282, + "src": "a && !i.collapsible || this._trigger(\"beforeActivate\", e, c) === !1 || (i.active = r ? !1 : this.tabs.index(o) , this.active = a ? t() : o , this.xhr && this.xhr.abort() , l.length || h.length || t.error(\"jQuery UI Tabs: Mismatching fragment identifier.\") , h.length && this.load(this.tabs.index(o), e) , this._toggle(e, c))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68410, + "nodeLength": 17, + "src": "a && !i.collapsible", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68429, + "nodeLength": 263, + "src": "this._trigger(\"beforeActivate\", e, c) === !1 || (i.active = r ? !1 : this.tabs.index(o) , this.active = a ? t() : o , this.xhr && this.xhr.abort() , l.length || h.length || t.error(\"jQuery UI Tabs: Mismatching fragment identifier.\") , h.length && this.load(this.tabs.index(o), e) , this._toggle(e, c))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68429, + "nodeLength": 40, + "src": "this._trigger(\"beforeActivate\", e, c) === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68481, + "nodeLength": 1, + "src": "r", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68517, + "nodeLength": 1, + "src": "a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68525, + "nodeLength": 26, + "src": "this.xhr && this.xhr.abort()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68552, + "nodeLength": 79, + "src": "l.length || h.length || t.error(\"jQuery UI Tabs: Mismatching fragment identifier.\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68562, + "nodeLength": 69, + "src": "h.length || t.error(\"jQuery UI Tabs: Mismatching fragment identifier.\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68632, + "nodeLength": 41, + "src": "h.length && this.load(this.tabs.index(o), e)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68853, + "nodeLength": 24, + "src": "a.length && o.options.show", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68974, + "nodeLength": 27, + "src": "r.length && this.options.hide", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69310, + "nodeLength": 18, + "src": "a.length && r.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69358, + "nodeLength": 95, + "src": "a.length && this.tabs.filter(function() {\n return 0 === t(this).attr(\"tabIndex\");\n}).attr(\"tabIndex\", -1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69403, + "nodeLength": 28, + "src": "0 === t(this).attr(\"tabIndex\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69609, + "nodeLength": 150, + "src": "s[0] !== this.active[0] && (s.length || (s = this.active) , i = s.find(\".ui-tabs-anchor\")[0] , this._eventHandler({\n target: i, \n currentTarget: i, \n preventDefault: t.noop}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69609, + "nodeLength": 21, + "src": "s[0] !== this.active[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69633, + "nodeLength": 25, + "src": "s.length || (s = this.active)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69792, + "nodeLength": 6, + "src": "e === !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69848, + "nodeLength": 103, + "src": "\"string\" == typeof e && (e = this.anchors.index(this.anchors.filter(\"[href$='\" + t.ui.escapeSelector(e) + \"']\")))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69848, + "nodeLength": 18, + "src": "\"string\" == typeof e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69975, + "nodeLength": 26, + "src": "this.xhr && this.xhr.abort()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 70160, + "nodeLength": 30, + "src": "t.data(this, \"ui-tabs-destroy\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 70396, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 70511, + "nodeLength": 66, + "src": "\"content\" !== this.options.heightStyle && this.panels.css(\"height\", \"\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 70511, + "nodeLength": 36, + "src": "\"content\" !== this.options.heightStyle", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 70626, + "nodeLength": 183, + "src": "i !== !1 && (void 0 === e ? i = !1 : (e = this._getIndex(e) , i = t.isArray(i) ? t.map(i, function(t) {\n return t !== e ? t : null;\n}) : t.map(this.tabs, function(t, i) {\n return i !== e ? i : null;\n})) , this._setOptionDisabled(i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 70626, + "nodeLength": 6, + "src": "i !== !1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 70635, + "nodeLength": 10, + "src": "void 0 === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 70674, + "nodeLength": 12, + "src": "t.isArray(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 70714, + "nodeLength": 5, + "src": "t !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 70766, + "nodeLength": 5, + "src": "i !== e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 70862, + "nodeLength": 6, + "src": "i !== !0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 70873, + "nodeLength": 10, + "src": "void 0 === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 70897, + "nodeLength": 39, + "src": "e = this._getIndex(e) , -1 !== t.inArray(e, i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 70917, + "nodeLength": 19, + "src": "-1 !== t.inArray(e, i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 70946, + "nodeLength": 12, + "src": "t.isArray(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 71170, + "nodeLength": 33, + "src": "\"abort\" === e && s.panels.stop(!1, !0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 71170, + "nodeLength": 11, + "src": "\"abort\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 71266, + "nodeLength": 23, + "src": "t === s.xhr && delete s.xhr", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 71266, + "nodeLength": 9, + "src": "t === s.xhr", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 71291, + "nodeLength": 326, + "src": "this._isLocal(o[0]) || (this.xhr = t.ajax(this._ajaxSettings(o, i, r)) , this.xhr && \"canceled\" !== this.xhr.statusText && (this._addClass(n, \"ui-tabs-loading\") , a.attr(\"aria-busy\", \"true\") , this.xhr.done(function(t, e, n) {\n setTimeout(function() {\n a.html(t) , s._trigger(\"load\", i, r) , h(n, e);\n}, 1);\n}).fail(function(t, e) {\n setTimeout(function() {\n h(t, e);\n}, 1);\n})))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 71356, + "nodeLength": 260, + "src": "this.xhr && \"canceled\" !== this.xhr.statusText && (this._addClass(n, \"ui-tabs-loading\") , a.attr(\"aria-busy\", \"true\") , this.xhr.done(function(t, e, n) {\n setTimeout(function() {\n a.html(t) , s._trigger(\"load\", i, r) , h(n, e);\n}, 1);\n}).fail(function(t, e) {\n setTimeout(function() {\n h(t, e);\n}, 1);\n}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 71366, + "nodeLength": 250, + "src": "\"canceled\" !== this.xhr.statusText && (this._addClass(n, \"ui-tabs-loading\") , a.attr(\"aria-busy\", \"true\") , this.xhr.done(function(t, e, n) {\n setTimeout(function() {\n a.html(t) , s._trigger(\"load\", i, r) , h(n, e);\n}, 1);\n}).fail(function(t, e) {\n setTimeout(function() {\n h(t, e);\n}, 1);\n}))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 71366, + "nodeLength": 32, + "src": "\"canceled\" !== this.xhr.statusText", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 71924, + "nodeLength": 140, + "src": "t.uiBackCompat !== !1 && t.widget(\"ui.tabs\", t.ui.tabs, {\n _processTabs: function() {\n this._superApply(arguments) , this._addClass(this.tabs, \"ui-tab\");\n}})", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 71925, + "nodeLength": 19, + "src": "t.uiBackCompat !== !1", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 72205, + "nodeLength": 25, + "src": "t(this).attr(\"title\") || \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 72449, + "nodeLength": 30, + "src": "e.attr(\"aria-describedby\") || \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 72640, + "nodeLength": 30, + "src": "e.attr(\"aria-describedby\") || \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 72702, + "nodeLength": 21, + "src": "-1 !== n && s.splice(n, 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 72702, + "nodeLength": 6, + "src": "-1 !== n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 72776, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73214, + "nodeLength": 79, + "src": "\"content\" === e && t.each(this.tooltips, function(t, e) {\n s._updateContent(e.element);\n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73214, + "nodeLength": 13, + "src": "\"content\" === e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73331, + "nodeLength": 1, + "src": "t", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73631, + "nodeLength": 15, + "src": "e.is(\"[title]\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73790, + "nodeLength": 70, + "src": "e.data(\"ui-tooltip-title\") && e.attr(\"title\", e.data(\"ui-tooltip-title\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73922, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73975, + "nodeLength": 453, + "src": "s.length && !s.data(\"ui-tooltip-id\") && (s.attr(\"title\") && s.data(\"ui-tooltip-title\", s.attr(\"title\")) , s.data(\"ui-tooltip-open\", !0) , e && \"mouseover\" === e.type && s.parents().each(function() {\n var e, s = t(this);\n s.data(\"ui-tooltip-open\") && (e = t.Event(\"blur\") , e.target = e.currentTarget = this , i.close(e, !0)) , s.attr(\"title\") && (s.uniqueId() , i.parents[this.id] = {\n element: this, \n title: s.attr(\"title\")} , s.attr(\"title\", \"\"));\n}) , this._registerCloseHandlers(e, s) , this._updateContent(s, e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73985, + "nodeLength": 443, + "src": "!s.data(\"ui-tooltip-id\") && (s.attr(\"title\") && s.data(\"ui-tooltip-title\", s.attr(\"title\")) , s.data(\"ui-tooltip-open\", !0) , e && \"mouseover\" === e.type && s.parents().each(function() {\n var e, s = t(this);\n s.data(\"ui-tooltip-open\") && (e = t.Event(\"blur\") , e.target = e.currentTarget = this , i.close(e, !0)) , s.attr(\"title\") && (s.uniqueId() , i.parents[this.id] = {\n element: this, \n title: s.attr(\"title\")} , s.attr(\"title\", \"\"));\n}) , this._registerCloseHandlers(e, s) , this._updateContent(s, e))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74012, + "nodeLength": 59, + "src": "s.attr(\"title\") && s.data(\"ui-tooltip-title\", s.attr(\"title\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74101, + "nodeLength": 268, + "src": "e && \"mouseover\" === e.type && s.parents().each(function() {\n var e, s = t(this);\n s.data(\"ui-tooltip-open\") && (e = t.Event(\"blur\") , e.target = e.currentTarget = this , i.close(e, !0)) , s.attr(\"title\") && (s.uniqueId() , i.parents[this.id] = {\n element: this, \n title: s.attr(\"title\")} , s.attr(\"title\", \"\"));\n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74104, + "nodeLength": 265, + "src": "\"mouseover\" === e.type && s.parents().each(function() {\n var e, s = t(this);\n s.data(\"ui-tooltip-open\") && (e = t.Event(\"blur\") , e.target = e.currentTarget = this , i.close(e, !0)) , s.attr(\"title\") && (s.uniqueId() , i.parents[this.id] = {\n element: this, \n title: s.attr(\"title\")} , s.attr(\"title\", \"\"));\n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74104, + "nodeLength": 20, + "src": "\"mouseover\" === e.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74170, + "nodeLength": 90, + "src": "s.data(\"ui-tooltip-open\") && (e = t.Event(\"blur\") , e.target = e.currentTarget = this , i.close(e, !0))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74261, + "nodeLength": 106, + "src": "s.attr(\"title\") && (s.uniqueId() , i.parents[this.id] = {\n element: this, \n title: s.attr(\"title\")} , s.attr(\"title\", \"\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74497, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74517, + "nodeLength": 40, + "src": "\"string\" == typeof s || s.nodeType || s.jquery", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74517, + "nodeLength": 18, + "src": "\"string\" == typeof s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74537, + "nodeLength": 20, + "src": "s.nodeType || s.jquery", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74623, + "nodeLength": 60, + "src": "t.data(\"ui-tooltip-open\") && (e && (e.type = o) , this._open(e, t, i))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74651, + "nodeLength": 13, + "src": "e && (e.type = o)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74688, + "nodeLength": 20, + "src": "i && this._open(e, t, i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74761, + "nodeLength": 30, + "src": "a.is(\":hidden\") || a.position(l)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74844, + "nodeLength": 1, + "src": "s", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74850, + "nodeLength": 15, + "src": "o = this._find(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74926, + "nodeLength": 83, + "src": "i.is(\"[title]\") && (e && \"mouseover\" === e.type ? i.attr(\"title\", \"\") : i.removeAttr(\"title\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74944, + "nodeLength": 23, + "src": "e && \"mouseover\" === e.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74947, + "nodeLength": 20, + "src": "\"mouseover\" === e.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75338, + "nodeLength": 44, + "src": "this.options.track && e && /^mouse/.test(e.type)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75358, + "nodeLength": 24, + "src": "e && /^mouse/.test(e.type)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75520, + "nodeLength": 168, + "src": "this.options.track && this.options.show && this.options.show.delay && (r = this.delayedShow = setInterval(function() {\n a.is(\":visible\") && (n(l.of) , clearInterval(r));\n}, t.fx.interval))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75540, + "nodeLength": 148, + "src": "this.options.show && this.options.show.delay && (r = this.delayedShow = setInterval(function() {\n a.is(\":visible\") && (n(l.of) , clearInterval(r));\n}, t.fx.interval))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75559, + "nodeLength": 129, + "src": "this.options.show.delay && (r = this.delayedShow = setInterval(function() {\n a.is(\":visible\") && (n(l.of) , clearInterval(r));\n}, t.fx.interval))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75627, + "nodeLength": 44, + "src": "a.is(\":visible\") && (n(l.of) , clearInterval(r))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75792, + "nodeLength": 31, + "src": "e.keyCode === t.ui.keyCode.ESCAPE", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75883, + "nodeLength": 89, + "src": "i[0] !== this.element[0] && (s.remove = function() {\n this._removeTooltip(this._find(i).tooltip);\n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75883, + "nodeLength": 22, + "src": "i[0] !== this.element[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75973, + "nodeLength": 47, + "src": "e && \"mouseover\" !== e.type || (s.mouseleave = \"close\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75973, + "nodeLength": 23, + "src": "e && \"mouseover\" !== e.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75976, + "nodeLength": 20, + "src": "\"mouseover\" !== e.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76021, + "nodeLength": 43, + "src": "e && \"focusin\" !== e.type || (s.focusout = \"close\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76021, + "nodeLength": 21, + "src": "e && \"focusin\" !== e.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76024, + "nodeLength": 18, + "src": "\"focusin\" !== e.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76118, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76173, + "nodeLength": 1, + "src": "o", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76188, + "nodeLength": 601, + "src": "o.closing || (clearInterval(this.delayedShow) , n.data(\"ui-tooltip-title\") && !n.attr(\"title\") && n.attr(\"title\", n.data(\"ui-tooltip-title\")) , this._removeDescribedBy(n) , o.hiding = !0 , i.stop(!0) , this._hide(i, this.options.hide, function() {\n s._removeTooltip(t(this));\n}) , n.removeData(\"ui-tooltip-open\") , this._off(n, \"mouseleave focusout keyup\") , n[0] !== this.element[0] && this._off(n, \"remove\") , this._off(this.document, \"mousemove\") , e && \"mouseleave\" === e.type && t.each(this.parents, function(e, i) {\n t(i.element).attr(\"title\", i.title) , delete s.parents[e];\n}) , o.closing = !0 , this._trigger(\"close\", e, {\n tooltip: i}) , o.hiding || (o.closing = !1))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76232, + "nodeLength": 88, + "src": "n.data(\"ui-tooltip-title\") && !n.attr(\"title\") && n.attr(\"title\", n.data(\"ui-tooltip-title\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76260, + "nodeLength": 60, + "src": "!n.attr(\"title\") && n.attr(\"title\", n.data(\"ui-tooltip-title\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76514, + "nodeLength": 45, + "src": "n[0] !== this.element[0] && this._off(n, \"remove\")", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76514, + "nodeLength": 22, + "src": "n[0] !== this.element[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76597, + "nodeLength": 116, + "src": "e && \"mouseleave\" === e.type && t.each(this.parents, function(e, i) {\n t(i.element).attr(\"title\", i.title) , delete s.parents[e];\n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76600, + "nodeLength": 113, + "src": "\"mouseleave\" === e.type && t.each(this.parents, function(e, i) {\n t(i.element).attr(\"title\", i.title) , delete s.parents[e];\n})", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76600, + "nodeLength": 21, + "src": "\"mouseleave\" === e.type", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76764, + "nodeLength": 24, + "src": "o.hiding || (o.closing = !1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 77184, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 77349, + "nodeLength": 35, + "src": "e.length || (e = this.document[0].body)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 77550, + "nodeLength": 122, + "src": "o.data(\"ui-tooltip-title\") && (o.attr(\"title\") || o.attr(\"title\", o.data(\"ui-tooltip-title\")) , o.removeData(\"ui-tooltip-title\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 77579, + "nodeLength": 59, + "src": "o.attr(\"title\") || o.attr(\"title\", o.data(\"ui-tooltip-title\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 77703, + "nodeLength": 223, + "src": "t.uiBackCompat !== !1 && t.widget(\"ui.tooltip\", t.ui.tooltip, {\n options: {\n tooltipClass: null}, \n _tooltip: function() {\n var t = this._superApply(arguments);\n return this.options.tooltipClass && t.tooltip.addClass(this.options.tooltipClass) , t;\n}})", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 77704, + "nodeLength": 19, + "src": "t.uiBackCompat !== !1", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 145, + "nodeLength": 72, + "src": "this.options.tooltipClass && t.tooltip.addClass(this.options.tooltipClass)", + "evalFalse": 0, + "evalTrue": 0 + } + ] + } + }, + "/js/external/jqueryui.1.12.1/eventbooking-theme/jquery-ui.js": { + "lineData": [ + null, + null, + null, + null, + 1, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0 + ], + "functionData": [ + 0, + 1, + 0, + 1, + 0 + ], + "branchData": { + "30": [ + null, + { + "position": 138, + "nodeLength": 10, + "src": "beforeShow", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "46": [ + null, + { + "position": 133, + "nodeLength": 11, + "src": "afterUpdate", + "evalFalse": 0, + "evalTrue": 0 + } + ] + } + }, + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-da.js": { + "lineData": [ + null, + null, + null, + 1, + 1, + null, + null, + 0, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1 + ], + "functionData": [ + 1, + 1 + ], + "branchData": { + "4": [ + null, + { + "position": 7, + "nodeLength": 42, + "src": "typeof define === \"function\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + } + ] + } + }, + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-de.js": { + "lineData": [ + null, + null, + null, + 1, + 1, + null, + null, + 0, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1 + ], + "functionData": [ + 1, + 1 + ], + "branchData": { + "4": [ + null, + { + "position": 7, + "nodeLength": 42, + "src": "typeof define === \"function\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + } + ] + } + }, + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-et.js": { + "lineData": [ + null, + null, + null, + 1, + 1, + null, + null, + 0, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1 + ], + "functionData": [ + 1, + 1 + ], + "branchData": { + "4": [ + null, + { + "position": 7, + "nodeLength": 42, + "src": "typeof define === \"function\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + } + ] + } + }, + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-en-GB.js": { + "lineData": [ + null, + null, + null, + 1, + 1, + null, + null, + 0, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1 + ], + "functionData": [ + 1, + 1 + ], + "branchData": { + "4": [ + null, + { + "position": 7, + "nodeLength": 42, + "src": "typeof define === \"function\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + } + ] + } + }, + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-fi.js": { + "lineData": [ + null, + null, + null, + 1, + 1, + null, + null, + 0, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1 + ], + "functionData": [ + 1, + 1 + ], + "branchData": { + "4": [ + null, + { + "position": 7, + "nodeLength": 42, + "src": "typeof define === \"function\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + } + ] + } + }, + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-fr.js": { + "lineData": [ + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + 0, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1 + ], + "functionData": [ + 1, + 1 + ], + "branchData": { + "6": [ + null, + { + "position": 7, + "nodeLength": 42, + "src": "typeof define === \"function\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + } + ] + } + }, + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-gl.js": { + "lineData": [ + null, + null, + null, + 1, + 1, + null, + null, + 0, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1 + ], + "functionData": [ + 1, + 1 + ], + "branchData": { + "4": [ + null, + { + "position": 7, + "nodeLength": 42, + "src": "typeof define === \"function\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + } + ] + } + }, + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-nl.js": { + "lineData": [ + null, + null, + null, + 1, + 1, + null, + null, + 0, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1 + ], + "functionData": [ + 1, + 1 + ], + "branchData": { + "4": [ + null, + { + "position": 7, + "nodeLength": 42, + "src": "typeof define === \"function\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + } + ] + } + }, + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-nb.js": { + "lineData": [ + null, + null, + null, + 1, + 1, + null, + null, + 0, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1 + ], + "functionData": [ + 1, + 1 + ], + "branchData": { + "4": [ + null, + { + "position": 7, + "nodeLength": 42, + "src": "typeof define === \"function\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + } + ] + } + }, + "/js/external/jqueryui.1.12.1/js/i18n/datepicker-sv.js": { + "lineData": [ + null, + null, + null, + 1, + 1, + null, + null, + 0, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1 + ], + "functionData": [ + 1, + 1 + ], + "branchData": { + "4": [ + null, + { + "position": 7, + "nodeLength": 42, + "src": "typeof define === \"function\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + } + ] + } + }, + "/vendor/js/bootstrap.js": { + "lineData": [ + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 1, + 1, + 1, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + 1, + null, + 1, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + null, + null, + 0, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 1, + 1, + null, + 1, + null, + 1, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + 1, + 0, + null, + null, + 1, + null, + 1, + null, + 1, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + 1, + null, + 1, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + 1, + null, + 1, + null, + 1, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + null, + 0, + null, + 0, + null, + null, + null, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + 1, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 1, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 1, + null, + 1, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + 1, + null, + 1, + null, + 1, + null, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + 1, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + 1, + 0, + null, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 1, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + 1, + 1, + 0, + null, + null, + 1, + null, + 1, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + 0, + null, + 0, + null, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + null, + 0, + null, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 1, + null, + 1, + 1, + null, + 1, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + 0, + 0, + null, + 0, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + 0, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 1, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + null, + null, + 1, + null, + 1, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 0, + null, + null, + 1, + null, + 1, + 2, + null, + 2, + 0, + 2, + 2, + 2, + null, + 2, + 2, + null, + null, + null, + 1, + null, + null, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + 0, + null, + null, + null, + null, + null, + 1, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 1, + 0, + null, + null, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + null, + null, + 1, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + 0, + null, + 0, + null, + null, + 1, + 1, + 1, + 1, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 1, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + null, + 0, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + null, + null, + null, + 1, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + 0, + null, + null, + 1, + null, + 1, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + 1, + 0, + null, + null, + 1, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 1, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + 1, + null, + 1, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + 0, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + 1, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + null, + 0, + null, + null, + null, + 1, + null, + 1, + null, + 1, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + null, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 1, + null, + 1, + null, + 1, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + 1, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + 0 + ], + "functionData": [ + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "branchData": { + "7": [ + null, + { + "position": 129, + "nodeLength": 29, + "src": "typeof jQuery === 'undefined'", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "14": [ + null, + { + "position": 77, + "nodeLength": 111, + "src": "(version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1) || (version[0] > 3)", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 77, + "nodeLength": 32, + "src": "version[0] < 2 && version[1] < 9", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 77, + "nodeLength": 14, + "src": "version[0] < 2", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 95, + "nodeLength": 14, + "src": "version[1] < 9", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 115, + "nodeLength": 73, + "src": "(version[0] == 1 && version[1] == 9 && version[2] < 1) || (version[0] > 3)", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 115, + "nodeLength": 52, + "src": "version[0] == 1 && version[1] == 9 && version[2] < 1", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 115, + "nodeLength": 15, + "src": "version[0] == 1", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 134, + "nodeLength": 33, + "src": "version[1] == 9 && version[2] < 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 134, + "nodeLength": 15, + "src": "version[1] == 9", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 153, + "nodeLength": 14, + "src": "version[2] < 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 173, + "nodeLength": 14, + "src": "version[0] > 3", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "45": [ + null, + { + "position": 11, + "nodeLength": 28, + "src": "el.style[name] !== undefined", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "58": [ + null, + { + "position": 146, + "nodeLength": 7, + "src": "!called", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "66": [ + null, + { + "position": 53, + "nodeLength": 21, + "src": "!$.support.transition", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "72": [ + null, + { + "position": 13, + "nodeLength": 20, + "src": "$(e.target).is(this)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "107": [ + null, + { + "position": 82, + "nodeLength": 9, + "src": "!selector", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "109": [ + null, + { + "position": 54, + "nodeLength": 50, + "src": "selector && selector.replace(/.*(?=#[^\\s]*$)/, '')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "112": [ + null, + { + "position": 243, + "nodeLength": 16, + "src": "selector === '#'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "114": [ + null, + { + "position": 286, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "116": [ + null, + { + "position": 317, + "nodeLength": 15, + "src": "!$parent.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "122": [ + null, + { + "position": 443, + "nodeLength": 22, + "src": "e.isDefaultPrevented()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "131": [ + null, + { + "position": 666, + "nodeLength": 48, + "src": "$.support.transition && $parent.hasClass('fade')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "147": [ + null, + { + "position": 79, + "nodeLength": 5, + "src": "!data", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "148": [ + null, + { + "position": 145, + "nodeLength": 25, + "src": "typeof option == 'string'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "204": [ + null, + { + "position": 71, + "nodeLength": 15, + "src": "$el.is('input')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "209": [ + null, + { + "position": 160, + "nodeLength": 22, + "src": "data.resetText == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "213": [ + null, + { + "position": 16, + "nodeLength": 19, + "src": "data[state] == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "215": [ + null, + { + "position": 84, + "nodeLength": 22, + "src": "state == 'loadingText'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "218": [ + null, + { + "position": 207, + "nodeLength": 14, + "src": "this.isLoading", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "229": [ + null, + { + "position": 100, + "nodeLength": 14, + "src": "$parent.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "231": [ + null, + { + "position": 58, + "nodeLength": 30, + "src": "$input.prop('type') == 'radio'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "232": [ + null, + { + "position": 13, + "nodeLength": 22, + "src": "$input.prop('checked')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "235": [ + null, + { + "position": 257, + "nodeLength": 33, + "src": "$input.prop('type') == 'checkbox'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "236": [ + null, + { + "position": 14, + "nodeLength": 60, + "src": "($input.prop('checked')) !== this.$element.hasClass('active')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "240": [ + null, + { + "position": 509, + "nodeLength": 7, + "src": "changed", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "255": [ + null, + { + "position": 93, + "nodeLength": 35, + "src": "typeof option == 'object' && option", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 93, + "nodeLength": 25, + "src": "typeof option == 'object'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "257": [ + null, + { + "position": 140, + "nodeLength": 5, + "src": "!data", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "259": [ + null, + { + "position": 218, + "nodeLength": 18, + "src": "option == 'toggle'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "260": [ + null, + { + "position": 268, + "nodeLength": 6, + "src": "option", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "286": [ + null, + { + "position": 90, + "nodeLength": 64, + "src": "!($(e.target).is('input[type=\"radio\"], input[type=\"checkbox\"]'))", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "290": [ + null, + { + "position": 197, + "nodeLength": 23, + "src": "$btn.is('input,button')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "325": [ + null, + { + "position": 277, + "nodeLength": 93, + "src": "this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this))", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "327": [ + null, + { + "position": 376, + "nodeLength": 221, + "src": "this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element.on('mouseenter.bs.carousel', $.proxy(this.pause, this)).on('mouseleave.bs.carousel', $.proxy(this.cycle, this))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 376, + "nodeLength": 29, + "src": "this.options.pause == 'hover'", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 409, + "nodeLength": 188, + "src": "!('ontouchstart' in document.documentElement) && this.$element.on('mouseenter.bs.carousel', $.proxy(this.pause, this)).on('mouseleave.bs.carousel', $.proxy(this.cycle, this))", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "344": [ + null, + { + "position": 9, + "nodeLength": 40, + "src": "/input|textarea/i.test(e.target.tagName)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "355": [ + null, + { + "position": 5, + "nodeLength": 26, + "src": "e || (this.paused = false)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "357": [ + null, + { + "position": 37, + "nodeLength": 45, + "src": "this.interval && clearInterval(this.interval)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "359": [ + null, + { + "position": 88, + "nodeLength": 131, + "src": "this.options.interval && !this.paused && (this.interval = setInterval($.proxy(this.next, this), this.options.interval))", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "360": [ + null, + { + "position": 30, + "nodeLength": 100, + "src": "!this.paused && (this.interval = setInterval($.proxy(this.next, this), this.options.interval))", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "368": [ + null, + { + "position": 80, + "nodeLength": 20, + "src": "item || this.$active", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "373": [ + null, + { + "position": 69, + "nodeLength": 125, + "src": "(direction == 'prev' && activeIndex === 0) || (direction == 'next' && activeIndex == (this.$items.length - 1))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69, + "nodeLength": 40, + "src": "direction == 'prev' && activeIndex === 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69, + "nodeLength": 19, + "src": "direction == 'prev'", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 92, + "nodeLength": 17, + "src": "activeIndex === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "374": [ + null, + { + "position": 61, + "nodeLength": 62, + "src": "direction == 'next' && activeIndex == (this.$items.length - 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61, + "nodeLength": 19, + "src": "direction == 'next'", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84, + "nodeLength": 39, + "src": "activeIndex == (this.$items.length - 1)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "375": [ + null, + { + "position": 203, + "nodeLength": 30, + "src": "willWrap && !this.options.wrap", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "376": [ + null, + { + "position": 265, + "nodeLength": 19, + "src": "direction == 'prev'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "385": [ + null, + { + "position": 128, + "nodeLength": 41, + "src": "pos > (this.$items.length - 1) || pos < 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 128, + "nodeLength": 30, + "src": "pos > (this.$items.length - 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 162, + "nodeLength": 7, + "src": "pos < 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "387": [ + null, + { + "position": 187, + "nodeLength": 12, + "src": "this.sliding", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "388": [ + null, + { + "position": 305, + "nodeLength": 18, + "src": "activeIndex == pos", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "390": [ + null, + { + "position": 376, + "nodeLength": 17, + "src": "pos > activeIndex", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "394": [ + null, + { + "position": 5, + "nodeLength": 25, + "src": "e || (this.paused = true)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "396": [ + null, + { + "position": 40, + "nodeLength": 65, + "src": "this.$element.find('.next, .prev').length && $.support.transition", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "407": [ + null, + { + "position": 9, + "nodeLength": 12, + "src": "this.sliding", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "412": [ + null, + { + "position": 9, + "nodeLength": 12, + "src": "this.sliding", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "418": [ + null, + { + "position": 76, + "nodeLength": 47, + "src": "next || this.getItemForDirection(type, $active)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "420": [ + null, + { + "position": 178, + "nodeLength": 14, + "src": "type == 'next'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "423": [ + null, + { + "position": 246, + "nodeLength": 24, + "src": "$next.hasClass('active')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "431": [ + null, + { + "position": 504, + "nodeLength": 31, + "src": "slideEvent.isDefaultPrevented()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "435": [ + null, + { + "position": 574, + "nodeLength": 25, + "src": "isCycling && this.pause()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "437": [ + null, + { + "position": 609, + "nodeLength": 23, + "src": "this.$indicators.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "440": [ + null, + { + "position": 152, + "nodeLength": 51, + "src": "$nextIndicator && $nextIndicator.addClass('active')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "444": [ + null, + { + "position": 973, + "nodeLength": 55, + "src": "$.support.transition && this.$element.hasClass('slide')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "466": [ + null, + { + "position": 1722, + "nodeLength": 25, + "src": "isCycling && this.cycle()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "479": [ + null, + { + "position": 141, + "nodeLength": 35, + "src": "typeof option == 'object' && option", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56, + "nodeLength": 25, + "src": "typeof option == 'object'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "480": [ + null, + { + "position": 198, + "nodeLength": 25, + "src": "typeof option == 'string'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "482": [ + null, + { + "position": 260, + "nodeLength": 5, + "src": "!data", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "483": [ + null, + { + "position": 341, + "nodeLength": 25, + "src": "typeof option == 'number'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "484": [ + null, + { + "position": 400, + "nodeLength": 6, + "src": "action", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "485": [ + null, + { + "position": 439, + "nodeLength": 16, + "src": "options.interval", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "510": [ + null, + { + "position": 60, + "nodeLength": 94, + "src": "$this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\\s]+$)/, '')", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42, + "nodeLength": 64, + "src": "(href = $this.attr('href')) && href.replace(/.*(?=#[^\\s]+$)/, '')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "511": [ + null, + { + "position": 181, + "nodeLength": 29, + "src": "!$target.hasClass('carousel')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "514": [ + null, + { + "position": 337, + "nodeLength": 10, + "src": "slideIndex", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "518": [ + null, + { + "position": 418, + "nodeLength": 10, + "src": "slideIndex", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "561": [ + null, + { + "position": 317, + "nodeLength": 19, + "src": "this.options.parent", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "567": [ + null, + { + "position": 472, + "nodeLength": 19, + "src": "this.options.toggle", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "580": [ + null, + { + "position": 63, + "nodeLength": 8, + "src": "hasWidth", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "584": [ + null, + { + "position": 9, + "nodeLength": 50, + "src": "this.transitioning || this.$element.hasClass('in')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "587": [ + null, + { + "position": 107, + "nodeLength": 76, + "src": "this.$parent && this.$parent.children('.panel').children('.in, .collapsing')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "589": [ + null, + { + "position": 193, + "nodeLength": 25, + "src": "actives && actives.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "591": [ + null, + { + "position": 59, + "nodeLength": 40, + "src": "activesData && activesData.transitioning", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "596": [ + null, + { + "position": 431, + "nodeLength": 31, + "src": "startEvent.isDefaultPrevented()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "598": [ + null, + { + "position": 480, + "nodeLength": 25, + "src": "actives && actives.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "600": [ + null, + { + "position": 42, + "nodeLength": 48, + "src": "activesData || actives.data('bs.collapse', null)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "625": [ + null, + { + "position": 1125, + "nodeLength": 21, + "src": "!$.support.transition", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "635": [ + null, + { + "position": 9, + "nodeLength": 51, + "src": "this.transitioning || !this.$element.hasClass('in')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "639": [ + null, + { + "position": 165, + "nodeLength": 31, + "src": "startEvent.isDefaultPrevented()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "664": [ + null, + { + "position": 750, + "nodeLength": 21, + "src": "!$.support.transition", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "673": [ + null, + { + "position": 10, + "nodeLength": 28, + "src": "this.$element.hasClass('in')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "697": [ + null, + { + "position": 31, + "nodeLength": 106, + "src": "$trigger.attr('data-target') || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\\s]+$)/, '')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "698": [ + null, + { + "position": 38, + "nodeLength": 67, + "src": "(href = $trigger.attr('href')) && href.replace(/.*(?=#[^\\s]+$)/, '')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "711": [ + null, + { + "position": 141, + "nodeLength": 35, + "src": "typeof option == 'object' && option", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 56, + "nodeLength": 25, + "src": "typeof option == 'object'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "713": [ + null, + { + "position": 189, + "nodeLength": 51, + "src": "!data && options.toggle && /show|hide/.test(option)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 198, + "nodeLength": 42, + "src": "options.toggle && /show|hide/.test(option)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "714": [ + null, + { + "position": 275, + "nodeLength": 5, + "src": "!data", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "715": [ + null, + { + "position": 356, + "nodeLength": 25, + "src": "typeof option == 'string'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "740": [ + null, + { + "position": 36, + "nodeLength": 26, + "src": "!$this.attr('data-target')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "744": [ + null, + { + "position": 194, + "nodeLength": 4, + "src": "data", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "777": [ + null, + { + "position": 55, + "nodeLength": 9, + "src": "!selector", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "779": [ + null, + { + "position": 54, + "nodeLength": 80, + "src": "selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\\s]*$)/, '')", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 66, + "nodeLength": 68, + "src": "/#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\\s]*$)/, '')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "782": [ + null, + { + "position": 244, + "nodeLength": 23, + "src": "selector && $(selector)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "784": [ + null, + { + "position": 280, + "nodeLength": 25, + "src": "$parent && $parent.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "788": [ + null, + { + "position": 9, + "nodeLength": 18, + "src": "e && e.which === 3", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14, + "nodeLength": 13, + "src": "e.which === 3", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "795": [ + null, + { + "position": 139, + "nodeLength": 25, + "src": "!$parent.hasClass('open')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "797": [ + null, + { + "position": 184, + "nodeLength": 102, + "src": "e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 189, + "nodeLength": 97, + "src": "e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 189, + "nodeLength": 17, + "src": "e.type == 'click'", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 210, + "nodeLength": 76, + "src": "/input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "801": [ + null, + { + "position": 377, + "nodeLength": 22, + "src": "e.isDefaultPrevented()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "811": [ + null, + { + "position": 34, + "nodeLength": 32, + "src": "$this.is('.disabled, :disabled')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "818": [ + null, + { + "position": 183, + "nodeLength": 9, + "src": "!isActive", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "819": [ + null, + { + "position": 11, + "nodeLength": 84, + "src": "'ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "830": [ + null, + { + "position": 463, + "nodeLength": 22, + "src": "e.isDefaultPrevented()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "845": [ + null, + { + "position": 9, + "nodeLength": 74, + "src": "!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "852": [ + null, + { + "position": 174, + "nodeLength": 32, + "src": "$this.is('.disabled, :disabled')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "857": [ + null, + { + "position": 305, + "nodeLength": 55, + "src": "!isActive && e.which != 27 || isActive && e.which == 27", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 305, + "nodeLength": 26, + "src": "!isActive && e.which != 27", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 318, + "nodeLength": 13, + "src": "e.which != 27", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 335, + "nodeLength": 25, + "src": "isActive && e.which == 27", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 347, + "nodeLength": 13, + "src": "e.which == 27", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "858": [ + null, + { + "position": 11, + "nodeLength": 13, + "src": "e.which == 27", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "865": [ + null, + { + "position": 580, + "nodeLength": 14, + "src": "!$items.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "869": [ + null, + { + "position": 652, + "nodeLength": 26, + "src": "e.which == 38 && index > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 652, + "nodeLength": 13, + "src": "e.which == 38", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 669, + "nodeLength": 9, + "src": "index > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "870": [ + null, + { + "position": 726, + "nodeLength": 42, + "src": "e.which == 40 && index < $items.length - 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 726, + "nodeLength": 13, + "src": "e.which == 40", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 743, + "nodeLength": 25, + "src": "index < $items.length - 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "871": [ + null, + { + "position": 802, + "nodeLength": 7, + "src": "!~index", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "885": [ + null, + { + "position": 82, + "nodeLength": 5, + "src": "!data", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "886": [ + null, + { + "position": 154, + "nodeLength": 25, + "src": "typeof option == 'string'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "943": [ + null, + { + "position": 384, + "nodeLength": 19, + "src": "this.options.remote", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "964": [ + null, + { + "position": 12, + "nodeLength": 12, + "src": "this.isShown", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "973": [ + null, + { + "position": 135, + "nodeLength": 38, + "src": "this.isShown || e.isDefaultPrevented()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "988": [ + null, + { + "position": 13, + "nodeLength": 29, + "src": "$(e.target).is(that.$element)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "993": [ + null, + { + "position": 24, + "nodeLength": 54, + "src": "$.support.transition && that.$element.hasClass('fade')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "995": [ + null, + { + "position": 90, + "nodeLength": 30, + "src": "!that.$element.parent().length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1005": [ + null, + { + "position": 306, + "nodeLength": 10, + "src": "transition", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1015": [ + null, + { + "position": 527, + "nodeLength": 10, + "src": "transition", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1026": [ + null, + { + "position": 9, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1032": [ + null, + { + "position": 104, + "nodeLength": 39, + "src": "!this.isShown || e.isDefaultPrevented()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1048": [ + null, + { + "position": 433, + "nodeLength": 54, + "src": "$.support.transition && this.$element.hasClass('fade')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1059": [ + null, + { + "position": 13, + "nodeLength": 117, + "src": "document !== e.target && this.$element[0] !== e.target && !this.$element.has(e.target).length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13, + "nodeLength": 21, + "src": "document !== e.target", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1060": [ + null, + { + "position": 36, + "nodeLength": 80, + "src": "this.$element[0] !== e.target && !this.$element.has(e.target).length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52, + "nodeLength": 29, + "src": "this.$element[0] !== e.target", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1068": [ + null, + { + "position": 9, + "nodeLength": 37, + "src": "this.isShown && this.options.keyboard", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1070": [ + null, + { + "position": 9, + "nodeLength": 28, + "src": "e.which == 27 && this.hide()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 9, + "nodeLength": 13, + "src": "e.which == 27", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1072": [ + null, + { + "position": 193, + "nodeLength": 13, + "src": "!this.isShown", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1078": [ + null, + { + "position": 9, + "nodeLength": 12, + "src": "this.isShown", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1097": [ + null, + { + "position": 5, + "nodeLength": 41, + "src": "this.$backdrop && this.$backdrop.remove()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1103": [ + null, + { + "position": 39, + "nodeLength": 30, + "src": "this.$element.hasClass('fade')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1105": [ + null, + { + "position": 93, + "nodeLength": 37, + "src": "this.isShown && this.options.backdrop", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1106": [ + null, + { + "position": 23, + "nodeLength": 31, + "src": "$.support.transition && animate", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1113": [ + null, + { + "position": 13, + "nodeLength": 24, + "src": "this.ignoreBackdropClick", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1117": [ + null, + { + "position": 123, + "nodeLength": 28, + "src": "e.target !== e.currentTarget", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1118": [ + null, + { + "position": -1, + "nodeLength": 33, + "src": "this.options.backdrop == 'static'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1123": [ + null, + { + "position": 551, + "nodeLength": 9, + "src": "doAnimate", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1127": [ + null, + { + "position": 656, + "nodeLength": 9, + "src": "!callback", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1129": [ + null, + { + "position": 681, + "nodeLength": 9, + "src": "doAnimate", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1135": [ + null, + { + "position": 999, + "nodeLength": 31, + "src": "!this.isShown && this.$backdrop", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1140": [ + null, + { + "position": 39, + "nodeLength": 22, + "src": "callback && callback()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1142": [ + null, + { + "position": 157, + "nodeLength": 54, + "src": "$.support.transition && this.$element.hasClass('fade')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1148": [ + null, + { + "position": 1432, + "nodeLength": 8, + "src": "callback", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1160": [ + null, + { + "position": 30, + "nodeLength": 69, + "src": "this.$element[0].scrollHeight > document.documentElement.clientHeight", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1163": [ + null, + { + "position": 21, + "nodeLength": 45, + "src": "!this.bodyIsOverflowing && modalIsOverflowing", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1164": [ + null, + { + "position": 115, + "nodeLength": 45, + "src": "this.bodyIsOverflowing && !modalIsOverflowing", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1177": [ + null, + { + "position": 53, + "nodeLength": 16, + "src": "!fullWindowWidth", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1181": [ + null, + { + "position": 327, + "nodeLength": 43, + "src": "document.body.clientWidth < fullWindowWidth", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1186": [ + null, + { + "position": 29, + "nodeLength": 36, + "src": "this.$body.css('padding-right') || 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1187": [ + null, + { + "position": 99, + "nodeLength": 38, + "src": "document.body.style.paddingRight || ''", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1188": [ + null, + { + "position": 146, + "nodeLength": 22, + "src": "this.bodyIsOverflowing", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1212": [ + null, + { + "position": 135, + "nodeLength": 35, + "src": "typeof option == 'object' && option", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53, + "nodeLength": 25, + "src": "typeof option == 'object'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1214": [ + null, + { + "position": 183, + "nodeLength": 5, + "src": "!data", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1215": [ + null, + { + "position": 258, + "nodeLength": 25, + "src": "typeof option == 'string'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1216": [ + null, + { + "position": 330, + "nodeLength": 12, + "src": "options.show", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1241": [ + null, + { + "position": 84, + "nodeLength": 73, + "src": "$this.attr('data-target') || (href && href.replace(/.*(?=#[^\\s]+$)/, ''))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42, + "nodeLength": 42, + "src": "href && href.replace(/.*(?=#[^\\s]+$)/, '')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1242": [ + null, + { + "position": 194, + "nodeLength": 24, + "src": "$target.data('bs.modal')", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 251, + "nodeLength": 23, + "src": "!/#/.test(href) && href", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1244": [ + null, + { + "position": 317, + "nodeLength": 13, + "src": "$this.is('a')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1247": [ + null, + { + "position": 11, + "nodeLength": 30, + "src": "showEvent.isDefaultPrevented()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1249": [ + null, + { + "position": 9, + "nodeLength": 46, + "src": "$this.is(':visible') && $this.trigger('focus')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1310": [ + null, + { + "position": 152, + "nodeLength": 173, + "src": "this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport))", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 179, + "nodeLength": 35, + "src": "$.isFunction(this.options.viewport)", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 268, + "nodeLength": 55, + "src": "this.options.viewport.selector || this.options.viewport", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1313": [ + null, + { + "position": 401, + "nodeLength": 74, + "src": "this.$element[0] instanceof document.constructor && !this.options.selector", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1319": [ + null, + { + "position": 700, + "nodeLength": 3, + "src": "i--", + "evalFalse": 1, + "evalTrue": 2 + } + ], + "1322": [ + null, + { + "position": 44, + "nodeLength": 18, + "src": "trigger == 'click'", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "1324": [ + null, + { + "position": 182, + "nodeLength": 19, + "src": "trigger != 'manual'", + "evalFalse": 0, + "evalTrue": 2 + } + ], + "1325": [ + null, + { + "position": 24, + "nodeLength": 18, + "src": "trigger == 'hover'", + "evalFalse": 1, + "evalTrue": 1 + } + ], + "1326": [ + null, + { + "position": 93, + "nodeLength": 18, + "src": "trigger == 'hover'", + "evalFalse": 1, + "evalTrue": 1 + } + ], + "1333": [ + null, + { + "position": 1276, + "nodeLength": 21, + "src": "this.options.selector", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1345": [ + null, + { + "position": 88, + "nodeLength": 49, + "src": "options.delay && typeof options.delay == 'number'", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 105, + "nodeLength": 32, + "src": "typeof options.delay == 'number'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1359": [ + null, + { + "position": 66, + "nodeLength": 124, + "src": "this._options && $.each(this._options, function(key, value) {\n if (defaults[key] != value) \n options[key] = value;\n})", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1360": [ + null, + { + "position": 11, + "nodeLength": 22, + "src": "defaults[key] != value", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1367": [ + null, + { + "position": 16, + "nodeLength": 31, + "src": "obj instanceof this.constructor", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1370": [ + null, + { + "position": 116, + "nodeLength": 5, + "src": "!self", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1375": [ + null, + { + "position": 277, + "nodeLength": 22, + "src": "obj instanceof $.Event", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1376": [ + null, + { + "position": 20, + "nodeLength": 21, + "src": "obj.type == 'focusin'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1379": [ + null, + { + "position": 387, + "nodeLength": 52, + "src": "self.tip().hasClass('in') || self.hoverState == 'in'", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 416, + "nodeLength": 23, + "src": "self.hoverState == 'in'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1388": [ + null, + { + "position": 560, + "nodeLength": 47, + "src": "!self.options.delay || !self.options.delay.show", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1391": [ + null, + { + "position": 11, + "nodeLength": 23, + "src": "self.hoverState == 'in'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1397": [ + null, + { + "position": 11, + "nodeLength": 17, + "src": "this.inState[key]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1404": [ + null, + { + "position": 16, + "nodeLength": 31, + "src": "obj instanceof this.constructor", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1407": [ + null, + { + "position": 116, + "nodeLength": 5, + "src": "!self", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1412": [ + null, + { + "position": 277, + "nodeLength": 22, + "src": "obj instanceof $.Event", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1413": [ + null, + { + "position": 20, + "nodeLength": 22, + "src": "obj.type == 'focusout'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1416": [ + null, + { + "position": 389, + "nodeLength": 20, + "src": "self.isInStateTrue()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1422": [ + null, + { + "position": 488, + "nodeLength": 47, + "src": "!self.options.delay || !self.options.delay.hide", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1425": [ + null, + { + "position": 11, + "nodeLength": 24, + "src": "self.hoverState == 'out'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1432": [ + null, + { + "position": 54, + "nodeLength": 33, + "src": "this.hasContent() && this.enabled", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1436": [ + null, + { + "position": 138, + "nodeLength": 32, + "src": "e.isDefaultPrevented() || !inDom", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1447": [ + null, + { + "position": 389, + "nodeLength": 22, + "src": "this.options.animation", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1449": [ + null, + { + "position": 458, + "nodeLength": 43, + "src": "typeof this.options.placement == 'function'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1455": [ + null, + { + "position": 702, + "nodeLength": 9, + "src": "autoPlace", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 725, + "nodeLength": 41, + "src": "placement.replace(autoToken, '') || 'top'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1463": [ + null, + { + "position": 924, + "nodeLength": 22, + "src": "this.options.container", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1470": [ + null, + { + "position": 1224, + "nodeLength": 9, + "src": "autoPlace", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1474": [ + null, + { + "position": 118, + "nodeLength": 71, + "src": "placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 118, + "nodeLength": 21, + "src": "placement == 'bottom'", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 143, + "nodeLength": 46, + "src": "pos.bottom + actualHeight > viewportDim.bottom", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1475": [ + null, + { + "position": 104, + "nodeLength": 68, + "src": "placement == 'top' && pos.top - actualHeight < viewportDim.top", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 104, + "nodeLength": 18, + "src": "placement == 'top'", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 129, + "nodeLength": 43, + "src": "pos.top - actualHeight < viewportDim.top", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1476": [ + null, + { + "position": 104, + "nodeLength": 70, + "src": "placement == 'right' && pos.right + actualWidth > viewportDim.width", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 104, + "nodeLength": 20, + "src": "placement == 'right'", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 129, + "nodeLength": 45, + "src": "pos.right + actualWidth > viewportDim.width", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1477": [ + null, + { + "position": 104, + "nodeLength": 69, + "src": "placement == 'left' && pos.left - actualWidth < viewportDim.left", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 104, + "nodeLength": 19, + "src": "placement == 'left'", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 129, + "nodeLength": 44, + "src": "pos.left - actualWidth < viewportDim.left", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1494": [ + null, + { + "position": 145, + "nodeLength": 23, + "src": "prevHoverState == 'out'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1497": [ + null, + { + "position": 2265, + "nodeLength": 50, + "src": "$.support.transition && this.$tip.hasClass('fade')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1515": [ + null, + { + "position": 349, + "nodeLength": 16, + "src": "isNaN(marginTop)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1516": [ + null, + { + "position": 391, + "nodeLength": 17, + "src": "isNaN(marginLeft)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1538": [ + null, + { + "position": 1006, + "nodeLength": 44, + "src": "placement == 'top' && actualHeight != height", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1006, + "nodeLength": 18, + "src": "placement == 'top'", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1028, + "nodeLength": 22, + "src": "actualHeight != height", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1544": [ + null, + { + "position": 1216, + "nodeLength": 10, + "src": "delta.left", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1548": [ + null, + { + "position": 1377, + "nodeLength": 10, + "src": "isVertical", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1549": [ + null, + { + "position": 1497, + "nodeLength": 10, + "src": "isVertical", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1557": [ + null, + { + "position": 23, + "nodeLength": 10, + "src": "isVertical", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1558": [ + null, + { + "position": 99, + "nodeLength": 10, + "src": "isVertical", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1565": [ + null, + { + "position": 93, + "nodeLength": 17, + "src": "this.options.html", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1575": [ + null, + { + "position": 11, + "nodeLength": 23, + "src": "that.hoverState != 'in'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1576": [ + null, + { + "position": 60, + "nodeLength": 13, + "src": "that.$element", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1581": [ + null, + { + "position": 278, + "nodeLength": 22, + "src": "callback && callback()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1586": [ + null, + { + "position": 468, + "nodeLength": 22, + "src": "e.isDefaultPrevented()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1590": [ + null, + { + "position": 532, + "nodeLength": 45, + "src": "$.support.transition && $tip.hasClass('fade')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1603": [ + null, + { + "position": 36, + "nodeLength": 69, + "src": "$e.attr('title') || typeof $e.attr('data-original-title') != 'string'", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 56, + "nodeLength": 49, + "src": "typeof $e.attr('data-original-title') != 'string'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1604": [ + null, + { + "position": 38, + "nodeLength": 22, + "src": "$e.attr('title') || ''", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1613": [ + null, + { + "position": 18, + "nodeLength": 25, + "src": "$element || this.$element", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1616": [ + null, + { + "position": 91, + "nodeLength": 20, + "src": "el.tagName == 'BODY'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1619": [ + null, + { + "position": 168, + "nodeLength": 20, + "src": "elRect.width == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1623": [ + null, + { + "position": 448, + "nodeLength": 52, + "src": "window.SVGElement && el instanceof window.SVGElement", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1626": [ + null, + { + "position": 663, + "nodeLength": 6, + "src": "isBody", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 695, + "nodeLength": 5, + "src": "isSvg", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1627": [ + null, + { + "position": 759, + "nodeLength": 6, + "src": "isBody", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 768, + "nodeLength": 61, + "src": "document.documentElement.scrollTop || document.body.scrollTop", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1628": [ + null, + { + "position": 875, + "nodeLength": 6, + "src": "isBody", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1634": [ + null, + { + "position": 12, + "nodeLength": 21, + "src": "placement == 'bottom'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1635": [ + null, + { + "position": 118, + "nodeLength": 18, + "src": "placement == 'top'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1636": [ + null, + { + "position": 118, + "nodeLength": 19, + "src": "placement == 'left'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1643": [ + null, + { + "position": 45, + "nodeLength": 15, + "src": "!this.$viewport", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1645": [ + null, + { + "position": 102, + "nodeLength": 59, + "src": "this.options.viewport && this.options.viewport.padding || 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 102, + "nodeLength": 54, + "src": "this.options.viewport && this.options.viewport.padding", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1648": [ + null, + { + "position": 233, + "nodeLength": 28, + "src": "/right|left/.test(placement)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1651": [ + null, + { + "position": 192, + "nodeLength": 38, + "src": "topEdgeOffset < viewportDimensions.top", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1653": [ + null, + { + "position": 327, + "nodeLength": 69, + "src": "bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1659": [ + null, + { + "position": 135, + "nodeLength": 40, + "src": "leftEdgeOffset < viewportDimensions.left", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1661": [ + null, + { + "position": 276, + "nodeLength": 42, + "src": "rightEdgeOffset > viewportDimensions.right", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1674": [ + null, + { + "position": 81, + "nodeLength": 103, + "src": "$e.attr('data-original-title') || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1675": [ + null, + { + "position": 40, + "nodeLength": 28, + "src": "typeof o.title == 'function'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1682": [ + null, + { + "position": 51, + "nodeLength": 31, + "src": "document.getElementById(prefix)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1687": [ + null, + { + "position": 9, + "nodeLength": 10, + "src": "!this.$tip", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1689": [ + null, + { + "position": 54, + "nodeLength": 21, + "src": "this.$tip.length != 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1697": [ + null, + { + "position": 27, + "nodeLength": 48, + "src": "this.$arrow || this.tip().find('.tooltip-arrow')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1714": [ + null, + { + "position": 29, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1716": [ + null, + { + "position": 67, + "nodeLength": 5, + "src": "!self", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1722": [ + null, + { + "position": 269, + "nodeLength": 1, + "src": "e", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1724": [ + null, + { + "position": 58, + "nodeLength": 20, + "src": "self.isInStateTrue()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1727": [ + null, + { + "position": 7, + "nodeLength": 25, + "src": "self.tip().hasClass('in')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1736": [ + null, + { + "position": 82, + "nodeLength": 9, + "src": "that.$tip", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1754": [ + null, + { + "position": 94, + "nodeLength": 35, + "src": "typeof option == 'object' && option", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 94, + "nodeLength": 25, + "src": "typeof option == 'object'", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1756": [ + null, + { + "position": 141, + "nodeLength": 36, + "src": "!data && /destroy|hide/.test(option)", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1757": [ + null, + { + "position": 196, + "nodeLength": 5, + "src": "!data", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1758": [ + null, + { + "position": 275, + "nodeLength": 25, + "src": "typeof option == 'string'", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1797": [ + null, + { + "position": 193, + "nodeLength": 13, + "src": "!$.fn.tooltip", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1825": [ + null, + { + "position": 133, + "nodeLength": 17, + "src": "this.options.html", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1827": [ + null, + { + "position": 118, + "nodeLength": 17, + "src": "this.options.html", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 139, + "nodeLength": 26, + "src": "typeof content == 'string'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1834": [ + null, + { + "position": 585, + "nodeLength": 35, + "src": "!$tip.find('.popover-title').html()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1838": [ + null, + { + "position": 12, + "nodeLength": 36, + "src": "this.getTitle() || this.getContent()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1845": [ + null, + { + "position": 66, + "nodeLength": 125, + "src": "$e.attr('data-content') || (typeof o.content == 'function' ? o.content.call($e[0]) : o.content)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1846": [ + null, + { + "position": 33, + "nodeLength": 30, + "src": "typeof o.content == 'function'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1852": [ + null, + { + "position": 27, + "nodeLength": 40, + "src": "this.$arrow || this.tip().find('.arrow')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1863": [ + null, + { + "position": 94, + "nodeLength": 35, + "src": "typeof option == 'object' && option", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94, + "nodeLength": 25, + "src": "typeof option == 'object'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1865": [ + null, + { + "position": 141, + "nodeLength": 36, + "src": "!data && /destroy|hide/.test(option)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1866": [ + null, + { + "position": 196, + "nodeLength": 5, + "src": "!data", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1867": [ + null, + { + "position": 275, + "nodeLength": 25, + "src": "typeof option == 'string'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1904": [ + null, + { + "position": 70, + "nodeLength": 28, + "src": "$(element).is(document.body)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1906": [ + null, + { + "position": 219, + "nodeLength": 25, + "src": "this.options.target || ''", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1924": [ + null, + { + "position": 12, + "nodeLength": 114, + "src": "this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1936": [ + null, + { + "position": 200, + "nodeLength": 35, + "src": "!$.isWindow(this.$scrollElement[0])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1945": [ + null, + { + "position": 49, + "nodeLength": 38, + "src": "$el.data('target') || $el.attr('href')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1946": [ + null, + { + "position": 108, + "nodeLength": 27, + "src": "/^#./.test(href) && $(href)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1948": [ + null, + { + "position": 153, + "nodeLength": 136, + "src": "($href && $href.length && $href.is(':visible') && [[$href[offsetMethod]().top + offsetBase, href]]) || null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 153, + "nodeLength": 127, + "src": "$href && $href.length && $href.is(':visible') && [[$href[offsetMethod]().top + offsetBase, href]]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1949": [ + null, + { + "position": 18, + "nodeLength": 108, + "src": "$href.length && $href.is(':visible') && [[$href[offsetMethod]().top + offsetBase, href]]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1950": [ + null, + { + "position": 25, + "nodeLength": 82, + "src": "$href.is(':visible') && [[$href[offsetMethod]().top + offsetBase, href]]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1969": [ + null, + { + "position": 345, + "nodeLength": 33, + "src": "this.scrollHeight != scrollHeight", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1973": [ + null, + { + "position": 418, + "nodeLength": 22, + "src": "scrollTop >= maxScroll", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1974": [ + null, + { + "position": 14, + "nodeLength": 69, + "src": "activeTarget != (i = targets[targets.length - 1]) && this.activate(i)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14, + "nodeLength": 49, + "src": "activeTarget != (i = targets[targets.length - 1])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1977": [ + null, + { + "position": 542, + "nodeLength": 38, + "src": "activeTarget && scrollTop < offsets[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 558, + "nodeLength": 22, + "src": "scrollTop < offsets[0]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1982": [ + null, + { + "position": 677, + "nodeLength": 3, + "src": "i--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1983": [ + null, + { + "position": 7, + "nodeLength": 170, + "src": "activeTarget != targets[i] && scrollTop >= offsets[i] && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) && this.activate(targets[i])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7, + "nodeLength": 26, + "src": "activeTarget != targets[i]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1984": [ + null, + { + "position": 37, + "nodeLength": 132, + "src": "scrollTop >= offsets[i] && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) && this.activate(targets[i])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47, + "nodeLength": 23, + "src": "scrollTop >= offsets[i]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1985": [ + null, + { + "position": 35, + "nodeLength": 96, + "src": "(offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) && this.activate(targets[i])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84, + "nodeLength": 58, + "src": "offsets[i + 1] === undefined || scrollTop < offsets[i + 1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 84, + "nodeLength": 28, + "src": "offsets[i + 1] === undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 116, + "nodeLength": 26, + "src": "scrollTop < offsets[i + 1]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2003": [ + null, + { + "position": 262, + "nodeLength": 38, + "src": "active.parent('.dropdown-menu').length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2026": [ + null, + { + "position": 96, + "nodeLength": 35, + "src": "typeof option == 'object' && option", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 96, + "nodeLength": 25, + "src": "typeof option == 'object'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2028": [ + null, + { + "position": 143, + "nodeLength": 5, + "src": "!data", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2029": [ + null, + { + "position": 226, + "nodeLength": 25, + "src": "typeof option == 'string'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2090": [ + null, + { + "position": 141, + "nodeLength": 9, + "src": "!selector", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2092": [ + null, + { + "position": 54, + "nodeLength": 50, + "src": "selector && selector.replace(/.*(?=#[^\\s]*$)/, '')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2095": [ + null, + { + "position": 290, + "nodeLength": 37, + "src": "$this.parent('li').hasClass('active')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2108": [ + null, + { + "position": 624, + "nodeLength": 64, + "src": "showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2127": [ + null, + { + "position": 71, + "nodeLength": 130, + "src": "callback && $.support.transition && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2128": [ + null, + { + "position": 17, + "nodeLength": 112, + "src": "$.support.transition && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2129": [ + null, + { + "position": 30, + "nodeLength": 80, + "src": "$active.length && $active.hasClass('fade') || !!container.find('> .fade').length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30, + "nodeLength": 42, + "src": "$active.length && $active.hasClass('fade')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2145": [ + null, + { + "position": 345, + "nodeLength": 10, + "src": "transition", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2152": [ + null, + { + "position": 516, + "nodeLength": 39, + "src": "element.parent('.dropdown-menu').length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2161": [ + null, + { + "position": 753, + "nodeLength": 22, + "src": "callback && callback()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2164": [ + null, + { + "position": 1011, + "nodeLength": 28, + "src": "$active.length && transition", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2182": [ + null, + { + "position": 77, + "nodeLength": 5, + "src": "!data", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2183": [ + null, + { + "position": 139, + "nodeLength": 25, + "src": "typeof option == 'string'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2260": [ + null, + { + "position": 149, + "nodeLength": 42, + "src": "offsetTop != null && this.affixed == 'top'", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 149, + "nodeLength": 17, + "src": "offsetTop != null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 170, + "nodeLength": 21, + "src": "this.affixed == 'top'", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 200, + "nodeLength": 21, + "src": "scrollTop < offsetTop", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2262": [ + null, + { + "position": 247, + "nodeLength": 24, + "src": "this.affixed == 'bottom'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2263": [ + null, + { + "position": 11, + "nodeLength": 17, + "src": "offsetTop != null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38, + "nodeLength": 39, + "src": "(scrollTop + this.unpin <= position.top)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 38, + "nodeLength": 38, + "src": "scrollTop + this.unpin <= position.top", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2264": [ + null, + { + "position": 111, + "nodeLength": 56, + "src": "(scrollTop + targetHeight <= scrollHeight - offsetBottom)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 111, + "nodeLength": 55, + "src": "scrollTop + targetHeight <= scrollHeight - offsetBottom", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2267": [ + null, + { + "position": 493, + "nodeLength": 20, + "src": "this.affixed == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2268": [ + null, + { + "position": 539, + "nodeLength": 12, + "src": "initializing", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2269": [ + null, + { + "position": 604, + "nodeLength": 12, + "src": "initializing", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2271": [ + null, + { + "position": 650, + "nodeLength": 43, + "src": "offsetTop != null && scrollTop <= offsetTop", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 650, + "nodeLength": 17, + "src": "offsetTop != null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 671, + "nodeLength": 22, + "src": "scrollTop <= offsetTop", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2272": [ + null, + { + "position": 716, + "nodeLength": 85, + "src": "offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 716, + "nodeLength": 20, + "src": "offsetBottom != null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 741, + "nodeLength": 59, + "src": "colliderTop + colliderHeight >= scrollHeight - offsetBottom", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2278": [ + null, + { + "position": 9, + "nodeLength": 17, + "src": "this.pinnedOffset", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2290": [ + null, + { + "position": 9, + "nodeLength": 29, + "src": "!this.$element.is(':visible')", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2298": [ + null, + { + "position": 298, + "nodeLength": 25, + "src": "typeof offset != 'object'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2299": [ + null, + { + "position": 375, + "nodeLength": 30, + "src": "typeof offsetTop == 'function'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2300": [ + null, + { + "position": 459, + "nodeLength": 33, + "src": "typeof offsetBottom == 'function'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2304": [ + null, + { + "position": 625, + "nodeLength": 21, + "src": "this.affixed != affix", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2305": [ + null, + { + "position": 11, + "nodeLength": 18, + "src": "this.unpin != null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2307": [ + null, + { + "position": 94, + "nodeLength": 5, + "src": "affix", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2312": [ + null, + { + "position": 218, + "nodeLength": 22, + "src": "e.isDefaultPrevented()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2315": [ + null, + { + "position": 296, + "nodeLength": 17, + "src": "affix == 'bottom'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2323": [ + null, + { + "position": 1164, + "nodeLength": 17, + "src": "affix == 'bottom'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2338": [ + null, + { + "position": 92, + "nodeLength": 35, + "src": "typeof option == 'object' && option", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 92, + "nodeLength": 25, + "src": "typeof option == 'object'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2340": [ + null, + { + "position": 139, + "nodeLength": 5, + "src": "!data", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2341": [ + null, + { + "position": 214, + "nodeLength": 25, + "src": "typeof option == 'string'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2368": [ + null, + { + "position": 76, + "nodeLength": 17, + "src": "data.offset || {}", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2370": [ + null, + { + "position": 105, + "nodeLength": 25, + "src": "data.offsetBottom != null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2371": [ + null, + { + "position": 181, + "nodeLength": 25, + "src": "data.offsetTop != null", + "evalFalse": 0, + "evalTrue": 0 + } + ] + } + }, + "/js/onlinebooking.js": { + "lineData": [ + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 2, + 2, + 1, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + 5, + 5, + 5, + 15, + 15, + 3, + null, + null, + 5, + null, + 1, + null, + null, + 1, + 1, + null, + null, + null, + null, + 0, + null, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 1, + 1, + null, + null, + 0, + null, + null, + null, + null, + 1, + 1, + 1 + ], + "functionData": [ + 0, + 0, + 0, + 0, + 0, + 2, + 1, + 0, + 0, + 1, + 5, + 15, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1 + ], + "branchData": { + "1": [ + null, + { + "position": 19, + "nodeLength": 19, + "src": "DinnerBooking || {}", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "23": [ + null, + { + "position": 171, + "nodeLength": 41, + "src": "ui.value === undefined || ui.value <= min", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 171, + "nodeLength": 22, + "src": "ui.value === undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 197, + "nodeLength": 15, + "src": "ui.value <= min", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "26": [ + null, + { + "position": 278, + "nodeLength": 16, + "src": "ui.value === max", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "33": [ + null, + { + "position": 485, + "nodeLength": 15, + "src": "ui.value <= min", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "38": [ + null, + { + "position": 629, + "nodeLength": 15, + "src": "ui.value >= min", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "49": [ + null, + { + "position": 101, + "nodeLength": 17, + "src": "now !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "128": [ + null, + { + "position": 44, + "nodeLength": 40, + "src": "jqXHR.responseJSON.message !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "149": [ + null, + { + "position": 63, + "nodeLength": 31, + "src": "availableTimeId === inputTimeId", + "evalFalse": 12, + "evalTrue": 3 + } + ], + "189": [ + null, + { + "position": 46, + "nodeLength": 32, + "src": "date && !isNaN(Date.parse(date))", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "243": [ + null, + { + "position": 272, + "nodeLength": 13, + "src": "i < daysCount", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "244": [ + null, + { + "position": 10, + "nodeLength": 63, + "src": "DinnerBooking.Onlinebooking.Day.days[i].TimeDay.day === sqlDate", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "245": [ + null, + { + "position": 11, + "nodeLength": 73, + "src": "DinnerBooking.Onlinebooking.Day.days[i].Booking.availability_status === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "247": [ + null, + { + "position": 43, + "nodeLength": 13, + "src": "date >= today", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "251": [ + null, + { + "position": 239, + "nodeLength": 73, + "src": "DinnerBooking.Onlinebooking.Day.days[i].Booking.availability_status === 2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "255": [ + null, + { + "position": 400, + "nodeLength": 67, + "src": "DinnerBooking.Onlinebooking.Day.days[i].TimeNote.note !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ] + } + }, + "/vendor/js/qunit/qunit.js": { + "lineData": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + null, + 1, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 4, + 0, + null, + null, + null, + 1, + 1, + 1, + 15, + 15, + 15, + 15, + 15, + null, + null, + null, + 1, + 1, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 4, + 4, + null, + 4, + null, + 0, + null, + null, + null, + 1, + 1, + 1, + 0, + null, + null, + 1, + null, + null, + null, + null, + null, + 1, + 8, + null, + null, + null, + 8, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 8, + null, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + 22, + 32, + 32, + 4, + 28, + 28, + null, + null, + null, + null, + 22, + null, + null, + 1, + 77, + 25, + null, + null, + null, + 52, + 0, + null, + null, + 52, + null, + null, + 52, + null, + 0, + 0, + null, + 0, + 0, + 0, + 20, + 20, + 20, + 20, + 20, + 52, + null, + 52, + null, + null, + 0, + 0, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + 1, + null, + null, + 1, + null, + null, + 1, + 1, + null, + 1, + 0, + null, + null, + 1, + null, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + 1, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 1, + 0, + null, + null, + null, + 1, + null, + null, + null, + 1, + 1, + 0, + null, + 1, + 0, + null, + 1, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + 1, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + 7, + 8, + 0, + null, + null, + 8, + null, + null, + 7, + null, + null, + 1, + 7, + null, + null, + 7, + 7, + null, + null, + 7, + null, + null, + null, + 1, + 21, + null, + 21, + 21, + 25, + null, + null, + null, + null, + null, + 1, + null, + 1, + 5, + null, + 5, + null, + 5, + 5, + 5, + 0, + null, + 5, + 4, + 4, + 8, + 0, + null, + 8, + null, + 4, + 4, + null, + null, + 1, + null, + null, + null, + 1, + 5, + null, + null, + null, + 5, + 5, + 5, + null, + 5, + null, + null, + null, + 5, + null, + null, + 1, + 1, + 1, + null, + 1, + 4, + null, + 4, + null, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + null, + null, + 4, + 6, + 0, + null, + null, + null, + 4, + null, + 4, + null, + null, + null, + null, + 4, + null, + null, + 0, + 0, + 0, + null, + 4, + null, + null, + null, + 1, + null, + 1, + 4, + null, + null, + 4, + 1, + 1, + null, + null, + 4, + null, + null, + 1, + null, + 4, + null, + null, + null, + null, + 4, + 1, + 1, + 1, + null, + null, + null, + null, + null, + 4, + null, + 4, + 4, + 4, + 4, + 4, + null, + 4, + null, + 4, + 4, + null, + null, + null, + null, + null, + null, + 4, + 1, + null, + null, + null, + null, + 4, + null, + 4, + null, + 4, + null, + 4, + 0, + 0, + null, + null, + 4, + 4, + null, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 4, + 4, + 4, + null, + null, + null, + null, + 4, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + 16, + null, + 16, + 16, + 0, + null, + 16, + 0, + null, + null, + null, + null, + 16, + 16, + null, + 16, + null, + null, + null, + 4, + 4, + 0, + 4, + 0, + 4, + 0, + null, + null, + 4, + null, + null, + null, + null, + null, + null, + null, + 4, + null, + 4, + 4, + null, + 4, + 9, + 0, + 0, + 0, + null, + null, + null, + 4, + null, + null, + 4, + 4, + 0, + null, + 4, + null, + null, + null, + 4, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 4, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 4, + null, + null, + null, + 4, + 0, + 0, + null, + null, + null, + null, + 4, + null, + null, + null, + 4, + 0, + null, + null, + 4, + null, + null, + 4, + 4, + null, + 4, + null, + 4, + null, + 4, + null, + 4, + null, + null, + null, + 4, + null, + null, + 4, + null, + 4, + null, + 4, + null, + null, + null, + null, + null, + 9, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 9, + 0, + null, + 0, + 0, + null, + null, + null, + 9, + null, + 9, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 4, + null, + null, + null, + 4, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 4, + null, + null, + null, + null, + 4, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 4, + 0, + null, + null, + null, + 4, + 0, + null, + null, + 4, + null, + 0, + null, + null, + 4, + null, + 0, + null, + null, + 4, + 0, + null, + null, + 4, + 4, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + 1, + 6, + null, + null, + null, + null, + null, + 6, + 272, + 272, + null, + null, + null, + null, + 6, + 6, + 0, + null, + null, + 6, + null, + null, + 1, + 44, + null, + null, + 44, + 20, + 36, + null, + 20, + null, + null, + 24, + 0, + 24, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + 24, + null, + null, + 24, + 0, + null, + null, + null, + 1, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 1, + 5, + null, + 5, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 1, + 4, + null, + null, + null, + 4, + null, + 4, + 4, + 0, + null, + null, + 4, + 4, + 0, + null, + null, + null, + null, + 1, + 4, + 0, + null, + null, + 4, + null, + 4, + null, + null, + null, + null, + 4, + null, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + 1, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + 1, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + 1, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + 1, + 4, + null, + null, + null, + 4, + 0, + 0, + 0, + null, + null, + 4, + null, + null, + 1, + 4, + 4, + 0, + null, + null, + null, + 1, + 1, + 4, + null, + 4, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 9, + null, + null, + null, + null, + null, + null, + null, + 9, + 0, + null, + null, + 9, + 0, + null, + null, + null, + null, + 9, + 0, + null, + null, + 9, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 9, + null, + 9, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + 1, + null, + 1, + null, + null, + 1, + 0, + null, + null, + 1, + null, + null, + null, + 1, + 0, + null, + null, + 0, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + 0, + null, + 0, + null, + null, + null, + 1, + 1, + 0, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + 1, + null, + 1, + 1, + null, + 1, + null, + null, + null, + null, + 1, + null, + null, + 1, + null, + 1, + null, + null, + null, + 2, + 2, + null, + 2, + 0, + 0, + 0, + null, + null, + null, + 2, + null, + 2, + null, + null, + null, + null, + null, + null, + 2, + 0, + 0, + 0, + 0, + 0, + null, + null, + 2, + null, + 2, + 2, + 2, + 2, + null, + null, + null, + null, + null, + null, + null, + null, + 2, + 2, + 0, + 0, + 0, + 0, + null, + 2, + 2, + null, + 2, + 2, + null, + null, + 2, + 2, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + 1, + 1, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 1, + 1, + 1, + 1, + null, + 1, + null, + 1, + null, + 1, + null, + null, + 1, + 1, + 1, + null, + null, + 0, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + null, + null, + 1, + null, + null, + 1, + 1, + null, + null, + null, + 1, + 2, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + 0, + null, + 1, + 1, + null, + 1, + 24, + 24, + null, + null, + 16, + null, + 24, + null, + 0, + 0, + null, + null, + 1, + 1, + 1, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 1, + 8, + 0, + null, + null, + 8, + 0, + null, + null, + null, + 1, + null, + 1, + null, + 1, + 0, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + 1, + 0, + null, + null, + 1, + 1, + 1, + null, + null, + null, + 1, + null, + null, + 1, + 4, + 0, + null, + null, + 4, + 4, + 4, + null, + null, + null, + 1, + null, + null, + 1, + null, + null, + 1, + 1, + 0, + null, + null, + 1, + null, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + null, + null, + 1, + null, + null, + 1, + null, + null, + 0, + 1, + 0, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + 1, + null, + null, + 3, + 3, + 3, + null, + null, + 3, + 3, + null, + null, + null, + null, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 1, + null, + null, + 1, + 0, + null, + null, + null, + null, + 1, + 38, + 0, + null, + 38, + null, + null, + 38, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + 1, + null, + null, + 1, + 0, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 22, + null, + null, + 1, + 0, + null, + null, + 1, + 2, + 2, + 3, + null, + null, + null, + 1, + 15, + null, + null, + 1, + 15, + 14, + null, + null, + null, + 1, + 3, + 1, + null, + 2, + null, + null, + null, + 1, + 2, + null, + null, + 2, + 0, + null, + null, + null, + 2, + null, + null, + 1, + 42, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 1, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 3, + 3, + 0, + null, + null, + null, + null, + null, + 3, + 3, + null, + 3, + 3, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 1, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + null, + 1, + 5, + null, + null, + null, + null, + null, + 5, + null, + 5, + null, + null, + 4, + null, + null, + 4, + 4, + 4, + 4, + 4, + null, + 4, + null, + null, + null, + 5, + null, + null, + 1, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + null, + null, + 1, + 1, + 1, + 1, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + null, + 1, + null, + 1, + 1, + 1, + 1, + null, + 1, + null, + 1, + null, + 1, + 1, + 1, + 1, + null, + 1, + null, + null, + 1, + 1, + null, + null, + null, + 1, + 2, + 2, + 2, + null, + null, + null, + 1, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + null, + null, + 0, + null, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + 0, + 0, + null, + 1, + 2, + 2, + 2, + 0, + 0, + 0, + null, + 2, + 2, + 0, + null, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + null, + null, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + null, + null, + null, + 1, + 1, + null, + 1, + 1, + null, + null, + null, + 1, + 1, + null, + 1, + 1, + null, + null, + null, + 1, + 1, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + null, + 1, + 1, + 1, + 1, + null, + 0, + null, + null, + 1, + 1, + null, + 1, + 1, + 1, + null, + null, + null, + 1, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + 1, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + 2, + null, + 2, + 4, + null, + 4, + null, + null, + null, + null, + 1, + 4, + null, + null, + null, + null, + null, + 4, + 0, + null, + null, + 4, + 4, + null, + 4, + 4, + 4, + null, + 4, + 4, + 4, + 4, + null, + 4, + 4, + null, + 4, + null, + 4, + null, + null, + null, + 1, + 1, + null, + null, + 1, + 2, + 2, + 2, + null, + null, + 1, + 1, + null, + null, + null, + 1, + 1, + 1, + 1, + 0, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + 1, + 1, + null, + null, + null, + 1, + 8, + null, + 8, + 8, + null, + null, + 8, + null, + 8, + null, + null, + 1, + 4, + null, + 4, + 4, + 4, + null, + null, + null, + 0, + null, + null, + 4, + 4, + 4, + null, + 4, + null, + null, + null, + 1, + null, + null, + 0, + null, + null, + 1, + 9, + null, + null, + null, + null, + null, + null, + null, + null, + 9, + 0, + null, + null, + 9, + 9, + 9, + null, + null, + null, + null, + 9, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 9, + 0, + null, + null, + 9, + null, + 9, + 9, + 9, + 9, + null, + null, + 1, + 4, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 4, + 0, + null, + null, + 4, + null, + 4, + null, + 4, + 4, + null, + 4, + null, + null, + 4, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + 4, + null, + 4, + null, + 4, + null, + 4, + 0, + 0, + 0, + 0, + 0, + null, + 4, + 0, + null, + null, + 4, + null, + 4, + 4, + 4, + 4, + null, + null, + null, + 4, + 4, + 4, + 4, + 4, + 4, + null, + 4, + 0, + null, + 4, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + 1, + 0, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 1 + ], + "functionData": [ + 1, + 0, + 0, + 4, + 1, + 1, + 1, + 4, + 0, + 8, + 0, + 0, + 22, + 77, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 7, + 8, + 21, + 5, + 5, + 4, + 0, + 4, + 4, + 4, + 4, + 4, + 0, + 0, + 0, + 16, + 16, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 9, + 0, + 4, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 6, + 44, + 0, + 0, + 5, + 4, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 4, + 1, + 4, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 2, + 2, + 2, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 8, + 0, + 1, + 1, + 4, + 1, + 1, + 1, + 0, + 38, + 0, + 1, + 22, + 0, + 2, + 15, + 15, + 3, + 2, + 42, + 0, + 0, + 1, + 0, + 5, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 4, + 1, + 1, + 1, + 8, + 4, + 0, + 9, + 4, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1 + ], + "branchData": { + "14": [ + null, + { + "position": 31, + "nodeLength": 21, + "src": "'default' in global$1", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "21": [ + null, + { + "position": 256, + "nodeLength": 25, + "src": "window && window.document", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "22": [ + null, + { + "position": 301, + "nodeLength": 26, + "src": "window && window.navigator", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "23": [ + null, + { + "position": 352, + "nodeLength": 31, + "src": "window && window.sessionStorage", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "25": [ + null, + { + "position": 402, + "nodeLength": 67, + "src": "typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 402, + "nodeLength": 28, + "src": "typeof Symbol === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 434, + "nodeLength": 35, + "src": "typeof Symbol.iterator === \"symbol\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "28": [ + null, + { + "position": 12, + "nodeLength": 93, + "src": "obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19, + "nodeLength": 86, + "src": "typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 19, + "nodeLength": 28, + "src": "typeof Symbol === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51, + "nodeLength": 54, + "src": "obj.constructor === Symbol && obj !== Symbol.prototype", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51, + "nodeLength": 26, + "src": "obj.constructor === Symbol", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 81, + "nodeLength": 24, + "src": "obj !== Symbol.prototype", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "42": [ + null, + { + "position": 9, + "nodeLength": 34, + "src": "!(instance instanceof Constructor)", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "49": [ + null, + { + "position": 23, + "nodeLength": 16, + "src": "i < props.length", + "evalFalse": 1, + "evalTrue": 15 + } + ], + "51": [ + null, + { + "position": 68, + "nodeLength": 30, + "src": "descriptor.enumerable || false", + "evalFalse": 15, + "evalTrue": 0 + } + ], + "53": [ + null, + { + "position": 152, + "nodeLength": 21, + "src": "\"value\" in descriptor", + "evalFalse": 0, + "evalTrue": 15 + } + ], + "59": [ + null, + { + "position": 11, + "nodeLength": 10, + "src": "protoProps", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "60": [ + null, + { + "position": 86, + "nodeLength": 11, + "src": "staticProps", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "106": [ + null, + { + "position": 9, + "nodeLength": 18, + "src": "Array.isArray(arr)", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "107": [ + null, + { + "position": 49, + "nodeLength": 14, + "src": "i < arr.length", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "117": [ + null, + { + "position": 1896, + "nodeLength": 61, + "src": "Date.now || function() {\n return new Date().getTime();\n}", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "122": [ + null, + { + "position": 14, + "nodeLength": 39, + "src": "window && window.document !== undefined", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 24, + "nodeLength": 29, + "src": "window.document !== undefined", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "123": [ + null, + { + "position": 70, + "nodeLength": 24, + "src": "setTimeout !== undefined", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "132": [ + null, + { + "position": 64, + "nodeLength": 17, + "src": "i < result.length", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "133": [ + null, + { + "position": 17, + "nodeLength": 12, + "src": "j < b.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "134": [ + null, + { + "position": 10, + "nodeLength": 18, + "src": "result[i] === b[j]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "146": [ + null, + { + "position": 8, + "nodeLength": 13, + "src": "array.indexOf", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "150": [ + null, + { + "position": 105, + "nodeLength": 10, + "src": "i < length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "151": [ + null, + { + "position": 9, + "nodeLength": 17, + "src": "array[i] === elem", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "169": [ + null, + { + "position": 34, + "nodeLength": 16, + "src": "is(\"array\", obj)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "171": [ + null, + { + "position": 9, + "nodeLength": 21, + "src": "hasOwn.call(obj, key)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "173": [ + null, + { + "position": 39, + "nodeLength": 19, + "src": "val === Object(val)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "181": [ + null, + { + "position": 9, + "nodeLength": 20, + "src": "hasOwn.call(b, prop)", + "evalFalse": 0, + "evalTrue": 32 + } + ], + "182": [ + null, + { + "position": 10, + "nodeLength": 21, + "src": "b[prop] === undefined", + "evalFalse": 28, + "evalTrue": 4 + } + ], + "184": [ + null, + { + "position": 74, + "nodeLength": 46, + "src": "!(undefOnly && typeof a[prop] !== \"undefined\")", + "evalFalse": 0, + "evalTrue": 28 + }, + { + "position": 76, + "nodeLength": 43, + "src": "undefOnly && typeof a[prop] !== \"undefined\"", + "evalFalse": 28, + "evalTrue": 0 + }, + { + "position": 89, + "nodeLength": 30, + "src": "typeof a[prop] !== \"undefined\"", + "evalFalse": 5, + "evalTrue": 0 + } + ], + "194": [ + null, + { + "position": 8, + "nodeLength": 26, + "src": "typeof obj === \"undefined\"", + "evalFalse": 52, + "evalTrue": 25 + } + ], + "199": [ + null, + { + "position": 114, + "nodeLength": 12, + "src": "obj === null", + "evalFalse": 52, + "evalTrue": 0 + } + ], + "204": [ + null, + { + "position": 73, + "nodeLength": 17, + "src": "match && match[1]", + "evalFalse": 0, + "evalTrue": 52 + } + ], + "208": [ + null, + { + "position": 23, + "nodeLength": 10, + "src": "isNaN(obj)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "224": [ + null, + { + "position": 573, + "nodeLength": 69, + "src": "(typeof obj === \"undefined\" ? \"undefined\" : _typeof(obj)) === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 573, + "nodeLength": 26, + "src": "typeof obj === \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "231": [ + null, + { + "position": 11, + "nodeLength": 24, + "src": "objectType(obj) === type", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "245": [ + null, + { + "position": 193, + "nodeLength": 72, + "src": "Object.getPrototypeOf || function(obj) {\n return obj.__proto__;\n}", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "254": [ + null, + { + "position": 140, + "nodeLength": 65, + "src": "(typeof a === \"undefined\" ? \"undefined\" : _typeof(a)) === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 140, + "nodeLength": 24, + "src": "typeof a === \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "257": [ + null, + { + "position": 246, + "nodeLength": 65, + "src": "(typeof b === \"undefined\" ? \"undefined\" : _typeof(b)) === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 246, + "nodeLength": 24, + "src": "typeof b === \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "261": [ + null, + { + "position": 355, + "nodeLength": 7, + "src": "a === b", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "269": [ + null, + { + "position": 139, + "nodeLength": 31, + "src": "a.constructor === b.constructor", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "276": [ + null, + { + "position": 322, + "nodeLength": 37, + "src": "protoA && protoA.constructor === null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 332, + "nodeLength": 27, + "src": "protoA.constructor === null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "279": [ + null, + { + "position": 397, + "nodeLength": 37, + "src": "protoB && protoB.constructor === null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 407, + "nodeLength": 27, + "src": "protoB.constructor === null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "285": [ + null, + { + "position": 581, + "nodeLength": 96, + "src": "protoA === null && protoB === Object.prototype || protoB === null && protoA === Object.prototype", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 581, + "nodeLength": 46, + "src": "protoA === null && protoB === Object.prototype", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 581, + "nodeLength": 15, + "src": "protoA === null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 600, + "nodeLength": 27, + "src": "protoB === Object.prototype", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 631, + "nodeLength": 46, + "src": "protoB === null && protoA === Object.prototype", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 631, + "nodeLength": 15, + "src": "protoB === null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 650, + "nodeLength": 27, + "src": "protoA === Object.prototype", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "293": [ + null, + { + "position": 12, + "nodeLength": 17, + "src": "\"flags\" in regexp", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "310": [ + null, + { + "position": 13, + "nodeLength": 110, + "src": "a.source === b.source && getRegExpFlags(a) === getRegExpFlags(b)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13, + "nodeLength": 21, + "src": "a.source === b.source", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "313": [ + null, + { + "position": 70, + "nodeLength": 39, + "src": "getRegExpFlags(a) === getRegExpFlags(b)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "322": [ + null, + { + "position": 61, + "nodeLength": 83, + "src": "caller !== Object && typeof caller !== \"undefined\" && a.toString() === b.toString()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 61, + "nodeLength": 17, + "src": "caller !== Object", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82, + "nodeLength": 62, + "src": "typeof caller !== \"undefined\" && a.toString() === b.toString()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 82, + "nodeLength": 29, + "src": "typeof caller !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 115, + "nodeLength": 29, + "src": "a.toString() === b.toString()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "329": [ + null, + { + "position": 80, + "nodeLength": 16, + "src": "len !== b.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "338": [ + null, + { + "position": 269, + "nodeLength": 7, + "src": "i < len", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "340": [ + null, + { + "position": 39, + "nodeLength": 18, + "src": "j < parents.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "341": [ + null, + { + "position": 20, + "nodeLength": 19, + "src": "parents[j] === a[i]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "342": [ + null, + { + "position": 60, + "nodeLength": 20, + "src": "parentsB[j] === b[i]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "343": [ + null, + { + "position": 93, + "nodeLength": 22, + "src": "aCircular || bCircular", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "344": [ + null, + { + "position": 13, + "nodeLength": 39, + "src": "a[i] === b[i] || aCircular && bCircular", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13, + "nodeLength": 13, + "src": "a[i] === b[i]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30, + "nodeLength": 22, + "src": "aCircular && bCircular", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "353": [ + null, + { + "position": 387, + "nodeLength": 32, + "src": "!loop && !innerEquiv(a[i], b[i])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "368": [ + null, + { + "position": 54, + "nodeLength": 17, + "src": "a.size !== b.size", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "376": [ + null, + { + "position": 12, + "nodeLength": 22, + "src": "innerEquiv(bVal, aVal)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "381": [ + null, + { + "position": 150, + "nodeLength": 8, + "src": "!innerEq", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "393": [ + null, + { + "position": 54, + "nodeLength": 17, + "src": "a.size !== b.size", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "401": [ + null, + { + "position": 12, + "nodeLength": 38, + "src": "innerEquiv([bVal, bKey], [aVal, aKey])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "406": [ + null, + { + "position": 172, + "nodeLength": 8, + "src": "!innerEq", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "422": [ + null, + { + "position": 153, + "nodeLength": 35, + "src": "compareConstructors(a, b) === false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "436": [ + null, + { + "position": 39, + "nodeLength": 18, + "src": "j < parents.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "437": [ + null, + { + "position": 20, + "nodeLength": 19, + "src": "parents[j] === a[i]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "438": [ + null, + { + "position": 60, + "nodeLength": 20, + "src": "parentsB[j] === b[i]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "439": [ + null, + { + "position": 93, + "nodeLength": 22, + "src": "aCircular || bCircular", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "440": [ + null, + { + "position": 13, + "nodeLength": 39, + "src": "a[i] === b[i] || aCircular && bCircular", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13, + "nodeLength": 13, + "src": "a[i] === b[i]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30, + "nodeLength": 22, + "src": "aCircular && bCircular", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "449": [ + null, + { + "position": 379, + "nodeLength": 32, + "src": "!loop && !innerEquiv(a[i], b[i])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "468": [ + null, + { + "position": 1185, + "nodeLength": 56, + "src": "eq && innerEquiv(aProperties.sort(), bProperties.sort())", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "474": [ + null, + { + "position": 42, + "nodeLength": 47, + "src": "objectType(b) === type && callbacks[type](b, a)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42, + "nodeLength": 22, + "src": "objectType(b) === type", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "481": [ + null, + { + "position": 65, + "nodeLength": 20, + "src": "arguments.length < 2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "486": [ + null, + { + "position": 164, + "nodeLength": 164, + "src": "(a === b || typeEquiv(a, b)) && (arguments.length === 2 || innerEquiv.apply(this, [].slice.call(arguments, 1)))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 164, + "nodeLength": 26, + "src": "a === b || typeEquiv(a, b)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 164, + "nodeLength": 7, + "src": "a === b", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "489": [ + null, + { + "position": -1, + "nodeLength": 77, + "src": "arguments.length === 2 || innerEquiv.apply(this, [].slice.call(arguments, 1))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 86, + "nodeLength": 22, + "src": "arguments.length === 2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "552": [ + null, + { + "position": 11675, + "nodeLength": 45, + "src": "window && window.QUnit && window.QUnit.config", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 11685, + "nodeLength": 35, + "src": "window.QUnit && window.QUnit.config", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "555": [ + null, + { + "position": 11794, + "nodeLength": 47, + "src": "window && window.QUnit && !window.QUnit.version", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 11804, + "nodeLength": 37, + "src": "window.QUnit && !window.QUnit.version", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "575": [ + null, + { + "position": 101, + "nodeLength": 8, + "src": "arr.join", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "578": [ + null, + { + "position": 165, + "nodeLength": 4, + "src": "!arr", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "587": [ + null, + { + "position": 62, + "nodeLength": 43, + "src": "dump.maxDepth && dump.depth > dump.maxDepth", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 79, + "nodeLength": 26, + "src": "dump.depth > dump.maxDepth", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "592": [ + null, + { + "position": 172, + "nodeLength": 3, + "src": "i--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "603": [ + null, + { + "position": -1, + "nodeLength": 218, + "src": "toString.call(obj) === \"[object Array]\" || typeof obj.length === \"number\" && obj.item !== undefined && (obj.length ? obj.item(0) === obj[0] : obj.item(0) === null && obj[0] === undefined)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36, + "nodeLength": 39, + "src": "toString.call(obj) === \"[object Array]\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "606": [ + null, + { + "position": 73, + "nodeLength": 144, + "src": "typeof obj.length === \"number\" && obj.item !== undefined && (obj.length ? obj.item(0) === obj[0] : obj.item(0) === null && obj[0] === undefined)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 111, + "nodeLength": 30, + "src": "typeof obj.length === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33, + "nodeLength": 110, + "src": "obj.item !== undefined && (obj.length ? obj.item(0) === obj[0] : obj.item(0) === null && obj[0] === undefined)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 146, + "nodeLength": 22, + "src": "obj.item !== undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26, + "nodeLength": 10, + "src": "obj.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39, + "nodeLength": 22, + "src": "obj.item(0) === obj[0]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 64, + "nodeLength": 44, + "src": "obj.item(0) === null && obj[0] === undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 64, + "nodeLength": 20, + "src": "obj.item(0) === null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 88, + "nodeLength": 20, + "src": "obj[0] === undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "615": [ + null, + { + "position": 14, + "nodeLength": 11, + "src": "stack || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "621": [ + null, + { + "position": 129, + "nodeLength": 14, + "src": "inStack !== -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "625": [ + null, + { + "position": 230, + "nodeLength": 27, + "src": "objType || this.typeOf(obj)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "627": [ + null, + { + "position": 314, + "nodeLength": 29, + "src": "typeof parser === \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "629": [ + null, + { + "position": 387, + "nodeLength": 25, + "src": "parserType === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "635": [ + null, + { + "position": 538, + "nodeLength": 23, + "src": "parserType === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "640": [ + null, + { + "position": 26, + "nodeLength": 12, + "src": "obj === null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "642": [ + null, + { + "position": 80, + "nodeLength": 26, + "src": "typeof obj === \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "644": [ + null, + { + "position": 153, + "nodeLength": 17, + "src": "is(\"regexp\", obj)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "646": [ + null, + { + "position": 214, + "nodeLength": 15, + "src": "is(\"date\", obj)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "648": [ + null, + { + "position": 271, + "nodeLength": 19, + "src": "is(\"function\", obj)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "650": [ + null, + { + "position": 336, + "nodeLength": 89, + "src": "obj.setInterval !== undefined && obj.document !== undefined && obj.nodeType === undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 336, + "nodeLength": 29, + "src": "obj.setInterval !== undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 369, + "nodeLength": 56, + "src": "obj.document !== undefined && obj.nodeType === undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 369, + "nodeLength": 26, + "src": "obj.document !== undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 399, + "nodeLength": 26, + "src": "obj.nodeType === undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "652": [ + null, + { + "position": 469, + "nodeLength": 18, + "src": "obj.nodeType === 9", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "654": [ + null, + { + "position": 533, + "nodeLength": 12, + "src": "obj.nodeType", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "656": [ + null, + { + "position": 587, + "nodeLength": 12, + "src": "isArray(obj)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "658": [ + null, + { + "position": 642, + "nodeLength": 47, + "src": "obj.constructor === Error.prototype.constructor", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "661": [ + null, + { + "position": 14, + "nodeLength": 26, + "src": "typeof obj === \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "667": [ + null, + { + "position": 10, + "nodeLength": 14, + "src": "this.multiline", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "668": [ + null, + { + "position": 14, + "nodeLength": 9, + "src": "this.HTML", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "670": [ + null, + { + "position": 14, + "nodeLength": 9, + "src": "this.HTML", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "676": [ + null, + { + "position": 10, + "nodeLength": 15, + "src": "!this.multiline", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "680": [ + null, + { + "position": 94, + "nodeLength": 9, + "src": "this.HTML", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "683": [ + null, + { + "position": 213, + "nodeLength": 10, + "src": "extra || 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "686": [ + null, + { + "position": 20, + "nodeLength": 6, + "src": "a || 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "689": [ + null, + { + "position": 20, + "nodeLength": 6, + "src": "a || 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "717": [ + null, + { + "position": 77, + "nodeLength": 12, + "src": "\"name\" in fn", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 103, + "nodeLength": 21, + "src": "reName.exec(fn) || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "719": [ + null, + { + "position": 149, + "nodeLength": 4, + "src": "name", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "738": [ + null, + { + "position": 126, + "nodeLength": 43, + "src": "dump.maxDepth && dump.depth > dump.maxDepth", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 143, + "nodeLength": 26, + "src": "dump.depth > dump.maxDepth", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "752": [ + null, + { + "position": 53, + "nodeLength": 36, + "src": "key in map && inArray(key, keys) < 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 67, + "nodeLength": 22, + "src": "inArray(key, keys) < 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "757": [ + null, + { + "position": 641, + "nodeLength": 15, + "src": "i < keys.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "769": [ + null, + { + "position": 53, + "nodeLength": 9, + "src": "dump.HTML", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "770": [ + null, + { + "position": 97, + "nodeLength": 9, + "src": "dump.HTML", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "775": [ + null, + { + "position": 252, + "nodeLength": 5, + "src": "attrs", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "776": [ + null, + { + "position": 40, + "nodeLength": 7, + "src": "i < len", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "782": [ + null, + { + "position": 222, + "nodeLength": 24, + "src": "val && val !== \"inherit\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 229, + "nodeLength": 17, + "src": "val !== \"inherit\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "790": [ + null, + { + "position": 751, + "nodeLength": 44, + "src": "_node.nodeType === 3 || _node.nodeType === 4", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 751, + "nodeLength": 20, + "src": "_node.nodeType === 3", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 775, + "nodeLength": 20, + "src": "_node.nodeType === 4", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "802": [ + null, + { + "position": 53, + "nodeLength": 2, + "src": "!l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "807": [ + null, + { + "position": 126, + "nodeLength": 3, + "src": "l--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "855": [ + null, + { + "position": 10, + "nodeLength": 35, + "src": "objectType(callback) !== \"function\"", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "865": [ + null, + { + "position": 528, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 1, + "evalTrue": 7 + } + ], + "869": [ + null, + { + "position": 91, + "nodeLength": 49, + "src": "objectType(config.callbacks[key]) === \"undefined\"", + "evalFalse": 0, + "evalTrue": 7 + } + ], + "881": [ + null, + { + "position": 101, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 21, + "evalTrue": 25 + } + ], + "888": [ + null, + { + "position": 20686, + "nodeLength": 29, + "src": "sourceFromStacktrace(0) || \"\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "891": [ + null, + { + "position": 13, + "nodeLength": 20, + "src": "offset === undefined", + "evalFalse": 5, + "evalTrue": 0 + } + ], + "895": [ + null, + { + "position": 83, + "nodeLength": 12, + "src": "e && e.stack", + "evalFalse": 0, + "evalTrue": 5 + } + ], + "897": [ + null, + { + "position": 42, + "nodeLength": 25, + "src": "/^error$/i.test(stack[0])", + "evalFalse": 5, + "evalTrue": 0 + } + ], + "900": [ + null, + { + "position": 105, + "nodeLength": 8, + "src": "fileName", + "evalFalse": 1, + "evalTrue": 4 + } + ], + "902": [ + null, + { + "position": 42, + "nodeLength": 16, + "src": "i < stack.length", + "evalFalse": 4, + "evalTrue": 8 + } + ], + "903": [ + null, + { + "position": 11, + "nodeLength": 33, + "src": "stack[i].indexOf(fileName) !== -1", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "908": [ + null, + { + "position": 182, + "nodeLength": 14, + "src": "include.length", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "921": [ + null, + { + "position": 173, + "nodeLength": 12, + "src": "!error.stack", + "evalFalse": 0, + "evalTrue": 5 + } + ], + "950": [ + null, + { + "position": 309, + "nodeLength": 12, + "src": "i < l.length", + "evalFalse": 4, + "evalTrue": 6 + } + ], + "951": [ + null, + { + "position": 9, + "nodeLength": 43, + "src": "this.module.tests[i].name === this.testName", + "evalFalse": 6, + "evalTrue": 0 + } + ], + "963": [ + null, + { + "position": 581, + "nodeLength": 13, + "src": "settings.skip", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "980": [ + null, + { + "position": 62, + "nodeLength": 31, + "src": "module && module.testsRun === 0", + "evalFalse": 4, + "evalTrue": 1 + }, + { + "position": 72, + "nodeLength": 21, + "src": "module.testsRun === 0", + "evalFalse": 3, + "evalTrue": 1 + } + ], + "995": [ + null, + { + "position": 165, + "nodeLength": 6, + "src": "i >= 0", + "evalFalse": 4, + "evalTrue": 1 + } + ], + "1006": [ + null, + { + "position": 437, + "nodeLength": 22, + "src": "module.testEnvironment", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "1022": [ + null, + { + "position": 914, + "nodeLength": 17, + "src": "!config.pollution", + "evalFalse": 3, + "evalTrue": 1 + } + ], + "1034": [ + null, + { + "position": 90, + "nodeLength": 17, + "src": "config.notrycatch", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "1042": [ + null, + { + "position": 100, + "nodeLength": 14, + "src": "e.message || e", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1048": [ + null, + { + "position": 272, + "nodeLength": 15, + "src": "config.blocking", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1067": [ + null, + { + "position": 10, + "nodeLength": 21, + "src": "hookName === \"before\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1068": [ + null, + { + "position": 11, + "nodeLength": 24, + "src": "hookOwner.testsRun !== 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1075": [ + null, + { + "position": 153, + "nodeLength": 75, + "src": "hookName === \"after\" && hookOwner.testsRun !== numberOfTests(hookOwner) - 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 153, + "nodeLength": 20, + "src": "hookName === \"after\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 177, + "nodeLength": 51, + "src": "hookOwner.testsRun !== numberOfTests(hookOwner) - 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1080": [ + null, + { + "position": 291, + "nodeLength": 17, + "src": "config.notrycatch", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1087": [ + null, + { + "position": 75, + "nodeLength": 22, + "src": "error.message || error", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1102": [ + null, + { + "position": 10, + "nodeLength": 19, + "src": "module.parentModule", + "evalFalse": 16, + "evalTrue": 0 + } + ], + "1105": [ + null, + { + "position": 96, + "nodeLength": 84, + "src": "module.testEnvironment && objectType(module.testEnvironment[handler]) === \"function\"", + "evalFalse": 16, + "evalTrue": 0 + }, + { + "position": 122, + "nodeLength": 58, + "src": "objectType(module.testEnvironment[handler]) === \"function\"", + "evalFalse": 16, + "evalTrue": 0 + } + ], + "1111": [ + null, + { + "position": 395, + "nodeLength": 10, + "src": "!this.skip", + "evalFalse": 0, + "evalTrue": 16 + } + ], + "1119": [ + null, + { + "position": 36, + "nodeLength": 47, + "src": "config.requireExpects && this.expected === null", + "evalFalse": 4, + "evalTrue": 0 + }, + { + "position": 61, + "nodeLength": 22, + "src": "this.expected === null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1121": [ + null, + { + "position": 220, + "nodeLength": 66, + "src": "this.expected !== null && this.expected !== this.assertions.length", + "evalFalse": 4, + "evalTrue": 0 + }, + { + "position": 220, + "nodeLength": 22, + "src": "this.expected !== null", + "evalFalse": 4, + "evalTrue": 0 + }, + { + "position": 246, + "nodeLength": 40, + "src": "this.expected !== this.assertions.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1123": [ + null, + { + "position": 431, + "nodeLength": 49, + "src": "this.expected === null && !this.assertions.length", + "evalFalse": 4, + "evalTrue": 0 + }, + { + "position": 431, + "nodeLength": 22, + "src": "this.expected === null", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "1140": [ + null, + { + "position": 974, + "nodeLength": 26, + "src": "i < this.assertions.length", + "evalFalse": 4, + "evalTrue": 9 + } + ], + "1141": [ + null, + { + "position": 10, + "nodeLength": 26, + "src": "!this.assertions[i].result", + "evalFalse": 9, + "evalTrue": 0 + } + ], + "1151": [ + null, + { + "position": 1198, + "nodeLength": 7, + "src": "storage", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "1152": [ + null, + { + "position": 10, + "nodeLength": 3, + "src": "bad", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "1166": [ + null, + { + "position": 181, + "nodeLength": 7, + "src": "skipped", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "1176": [ + null, + { + "position": 1795, + "nodeLength": 41, + "src": "module.testsRun === numberOfTests(module)", + "evalFalse": 3, + "evalTrue": 1 + } + ], + "1191": [ + null, + { + "position": 9, + "nodeLength": 24, + "src": "this.preserveEnvironment", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "1202": [ + null, + { + "position": 76, + "nodeLength": 13, + "src": "!this.valid()", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "1222": [ + null, + { + "position": 563, + "nodeLength": 97, + "src": "config.storage && +config.storage.getItem(\"qunit-test-\" + this.module.name + \"-\" + this.testName)", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "1225": [ + null, + { + "position": 743, + "nodeLength": 35, + "src": "config.reorder && previousFailCount", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "1244": [ + null, + { + "position": 233, + "nodeLength": 28, + "src": "resultInfo.negative || false", + "evalFalse": 9, + "evalTrue": 0 + } + ], + "1248": [ + null, + { + "position": 434, + "nodeLength": 18, + "src": "!resultInfo.result", + "evalFalse": 9, + "evalTrue": 0 + } + ], + "1251": [ + null, + { + "position": 49, + "nodeLength": 6, + "src": "source", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1265": [ + null, + { + "position": 9, + "nodeLength": 23, + "src": "!(this instanceof Test)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1273": [ + null, + { + "position": 92, + "nodeLength": 18, + "src": "message || \"error\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1274": [ + null, + { + "position": 125, + "nodeLength": 14, + "src": "actual || null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1279": [ + null, + { + "position": 382, + "nodeLength": 6, + "src": "source", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1296": [ + null, + { + "position": 77, + "nodeLength": 15, + "src": "promise != null", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "1298": [ + null, + { + "position": 36, + "nodeLength": 31, + "src": "objectType(then) === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1303": [ + null, + { + "position": 41, + "nodeLength": 6, + "src": "!phase", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125, + "nodeLength": 31, + "src": "error && error.message || error", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125, + "nodeLength": 22, + "src": "error && error.message", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1319": [ + null, + { + "position": 106, + "nodeLength": 44, + "src": "config.module && config.module.toLowerCase()", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "1323": [ + null, + { + "position": 27, + "nodeLength": 15, + "src": "testModule.name", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1324": [ + null, + { + "position": 92, + "nodeLength": 25, + "src": "testModuleName === module", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1326": [ + null, + { + "position": 157, + "nodeLength": 23, + "src": "testModule.parentModule", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1334": [ + null, + { + "position": 13, + "nodeLength": 124, + "src": "inArray(testModule.moduleId, config.moduleId) > -1 || testModule.parentModule && moduleChainIdMatch(testModule.parentModule)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13, + "nodeLength": 50, + "src": "inArray(testModule.moduleId, config.moduleId) > -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 67, + "nodeLength": 70, + "src": "testModule.parentModule && moduleChainIdMatch(testModule.parentModule)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1338": [ + null, + { + "position": 807, + "nodeLength": 40, + "src": "this.callback && this.callback.validTest", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "1342": [ + null, + { + "position": 884, + "nodeLength": 81, + "src": "config.moduleId && config.moduleId.length > 0 && !moduleChainIdMatch(this.module)", + "evalFalse": 4, + "evalTrue": 0 + }, + { + "position": 903, + "nodeLength": 62, + "src": "config.moduleId.length > 0 && !moduleChainIdMatch(this.module)", + "evalFalse": 4, + "evalTrue": 0 + }, + { + "position": 903, + "nodeLength": 26, + "src": "config.moduleId.length > 0", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "1347": [ + null, + { + "position": 1004, + "nodeLength": 84, + "src": "config.testId && config.testId.length > 0 && inArray(this.testId, config.testId) < 0", + "evalFalse": 4, + "evalTrue": 0 + }, + { + "position": 1021, + "nodeLength": 67, + "src": "config.testId.length > 0 && inArray(this.testId, config.testId) < 0", + "evalFalse": 4, + "evalTrue": 0 + }, + { + "position": 1021, + "nodeLength": 24, + "src": "config.testId.length > 0", + "evalFalse": 4, + "evalTrue": 0 + }, + { + "position": 1049, + "nodeLength": 39, + "src": "inArray(this.testId, config.testId) < 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1352": [ + null, + { + "position": 1127, + "nodeLength": 44, + "src": "module && !moduleChainNameMatch(this.module)", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "1356": [ + null, + { + "position": 1209, + "nodeLength": 7, + "src": "!filter", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "1360": [ + null, + { + "position": 1256, + "nodeLength": 11, + "src": "regexFilter", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1367": [ + null, + { + "position": 95, + "nodeLength": 17, + "src": "match !== exclude", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1374": [ + null, + { + "position": 94, + "nodeLength": 24, + "src": "filter.charAt(0) !== \"!\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1375": [ + null, + { + "position": 128, + "nodeLength": 8, + "src": "!include", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1380": [ + null, + { + "position": 242, + "nodeLength": 31, + "src": "fullName.indexOf(filter) !== -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1390": [ + null, + { + "position": 8, + "nodeLength": 15, + "src": "!config.current", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1409": [ + null, + { + "position": 121, + "nodeLength": 7, + "src": "i < len", + "evalFalse": 6, + "evalTrue": 272 + } + ], + "1417": [ + null, + { + "position": 447, + "nodeLength": 14, + "src": "hex.length < 8", + "evalFalse": 6, + "evalTrue": 0 + } + ], + "1428": [ + null, + { + "position": 48, + "nodeLength": 32, + "src": "objectType(callback) === \"array\"", + "evalFalse": 24, + "evalTrue": 20 + } + ], + "1429": [ + null, + { + "position": 12, + "nodeLength": 15, + "src": "callback.length", + "evalFalse": 20, + "evalTrue": 36 + } + ], + "1435": [ + null, + { + "position": 181, + "nodeLength": 8, + "src": "priority", + "evalFalse": 24, + "evalTrue": 0 + } + ], + "1437": [ + null, + { + "position": 263, + "nodeLength": 4, + "src": "seed", + "evalFalse": 24, + "evalTrue": 0 + } + ], + "1438": [ + null, + { + "position": 9, + "nodeLength": 12, + "src": "!unitSampler", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1449": [ + null, + { + "position": 612, + "nodeLength": 41, + "src": "internalState.autorun && !config.blocking", + "evalFalse": 24, + "evalTrue": 0 + } + ], + "1458": [ + null, + { + "position": 125, + "nodeLength": 38, + "src": "parseInt(generateHash(seed), 16) || -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1465": [ + null, + { + "position": 140, + "nodeLength": 10, + "src": "sample < 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1476": [ + null, + { + "position": 35, + "nodeLength": 16, + "src": "config.noglobals", + "evalFalse": 5, + "evalTrue": 0 + } + ], + "1478": [ + null, + { + "position": 10, + "nodeLength": 26, + "src": "hasOwn.call(global$1, key)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1481": [ + null, + { + "position": 82, + "nodeLength": 30, + "src": "/^qunit-test-output/.test(key)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1498": [ + null, + { + "position": 145, + "nodeLength": 21, + "src": "newGlobals.length > 0", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "1503": [ + null, + { + "position": 308, + "nodeLength": 25, + "src": "deletedGlobals.length > 0", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "1510": [ + null, + { + "position": 8, + "nodeLength": 7, + "src": "focused", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "1526": [ + null, + { + "position": 8, + "nodeLength": 7, + "src": "focused", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1542": [ + null, + { + "position": 25, + "nodeLength": 7, + "src": "focused", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1565": [ + null, + { + "position": 134, + "nodeLength": 40, + "src": "config.testTimeout && defined.setTimeout", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1574": [ + null, + { + "position": 9, + "nodeLength": 8, + "src": "released", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1594": [ + null, + { + "position": 56, + "nodeLength": 21, + "src": "isNaN(test.semaphore)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1602": [ + null, + { + "position": 259, + "nodeLength": 18, + "src": "test.semaphore > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1607": [ + null, + { + "position": 367, + "nodeLength": 18, + "src": "test.semaphore < 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1615": [ + null, + { + "position": 616, + "nodeLength": 18, + "src": "defined.setTimeout", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1616": [ + null, + { + "position": 9, + "nodeLength": 14, + "src": "config.timeout", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1620": [ + null, + { + "position": 10, + "nodeLength": 18, + "src": "test.semaphore > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1624": [ + null, + { + "position": 63, + "nodeLength": 14, + "src": "config.timeout", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1640": [ + null, + { + "position": 172, + "nodeLength": 14, + "src": "modules.length", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "1651": [ + null, + { + "position": 33, + "nodeLength": 28, + "src": "module = module.parentModule", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "1672": [ + null, + { + "position": 10, + "nodeLength": 22, + "src": "arguments.length === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1688": [ + null, + { + "position": 100, + "nodeLength": 38, + "src": "typeof acceptCallCount === \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1696": [ + null, + { + "position": 11, + "nodeLength": 6, + "src": "popped", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1702": [ + null, + { + "position": 185, + "nodeLength": 19, + "src": "acceptCallCount > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1719": [ + null, + { + "position": 189, + "nodeLength": 22, + "src": "this instanceof Assert", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1734": [ + null, + { + "position": 41, + "nodeLength": 57, + "src": "assert instanceof Assert && assert.test || config.current", + "evalFalse": 0, + "evalTrue": 9 + }, + { + "position": 41, + "nodeLength": 39, + "src": "assert instanceof Assert && assert.test", + "evalFalse": 0, + "evalTrue": 9 + } + ], + "1741": [ + null, + { + "position": 557, + "nodeLength": 12, + "src": "!currentTest", + "evalFalse": 9, + "evalTrue": 0 + } + ], + "1745": [ + null, + { + "position": 678, + "nodeLength": 61, + "src": "currentTest.usedAsync === true && currentTest.semaphore === 0", + "evalFalse": 9, + "evalTrue": 0 + }, + { + "position": 678, + "nodeLength": 30, + "src": "currentTest.usedAsync === true", + "evalFalse": 9, + "evalTrue": 0 + }, + { + "position": 712, + "nodeLength": 27, + "src": "currentTest.semaphore === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1751": [ + null, + { + "position": 934, + "nodeLength": 27, + "src": "!(assert instanceof Assert)", + "evalFalse": 9, + "evalTrue": 0 + } + ], + "1760": [ + null, + { + "position": 10, + "nodeLength": 8, + "src": "!message", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1761": [ + null, + { + "position": 17, + "nodeLength": 6, + "src": "result", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1774": [ + null, + { + "position": 10, + "nodeLength": 8, + "src": "!message", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1775": [ + null, + { + "position": 17, + "nodeLength": 7, + "src": "!result", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1790": [ + null, + { + "position": 60, + "nodeLength": 18, + "src": "expected == actual", + "evalFalse": 0, + "evalTrue": 9 + } + ], + "1804": [ + null, + { + "position": 60, + "nodeLength": 18, + "src": "expected != actual", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1866": [ + null, + { + "position": 15, + "nodeLength": 19, + "src": "expected === actual", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1876": [ + null, + { + "position": 15, + "nodeLength": 19, + "src": "expected !== actual", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1888": [ + null, + { + "position": 68, + "nodeLength": 53, + "src": "this instanceof Assert && this.test || config.current", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68, + "nodeLength": 35, + "src": "this instanceof Assert && this.test", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1891": [ + null, + { + "position": 202, + "nodeLength": 33, + "src": "objectType(expected) === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1892": [ + null, + { + "position": 11, + "nodeLength": 15, + "src": "message == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1908": [ + null, + { + "position": 727, + "nodeLength": 6, + "src": "actual", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1912": [ + null, + { + "position": 107, + "nodeLength": 9, + "src": "!expected", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1917": [ + null, + { + "position": 216, + "nodeLength": 25, + "src": "expectedType === \"regexp\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1921": [ + null, + { + "position": 380, + "nodeLength": 57, + "src": "expectedType === \"function\" && actual instanceof expected", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 380, + "nodeLength": 27, + "src": "expectedType === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1925": [ + null, + { + "position": 520, + "nodeLength": 25, + "src": "expectedType === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1926": [ + null, + { + "position": 17, + "nodeLength": 110, + "src": "actual instanceof expected.constructor && actual.name === expected.name && actual.message === expected.message", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59, + "nodeLength": 68, + "src": "actual.name === expected.name && actual.message === expected.message", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59, + "nodeLength": 29, + "src": "actual.name === expected.name", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 92, + "nodeLength": 35, + "src": "actual.message === expected.message", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1929": [ + null, + { + "position": 780, + "nodeLength": 65, + "src": "expectedType === \"function\" && expected.call({}, actual) === true", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 780, + "nodeLength": 27, + "src": "expectedType === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 811, + "nodeLength": 34, + "src": "expected.call({}, actual) === true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1962": [ + null, + { + "position": 54, + "nodeLength": 47, + "src": "resultErrorString.substring(0, 7) === \"[object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1963": [ + null, + { + "position": 16, + "nodeLength": 10, + "src": "error.name", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1964": [ + null, + { + "position": 80, + "nodeLength": 13, + "src": "error.message", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1966": [ + null, + { + "position": 136, + "nodeLength": 15, + "src": "name && message", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1968": [ + null, + { + "position": 206, + "nodeLength": 4, + "src": "name", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1970": [ + null, + { + "position": 248, + "nodeLength": 7, + "src": "message", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1983": [ + null, + { + "position": 9, + "nodeLength": 16, + "src": "defined.document", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1986": [ + null, + { + "position": 116, + "nodeLength": 36, + "src": "window.QUnit && window.QUnit.version", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1994": [ + null, + { + "position": 304, + "nodeLength": 57, + "src": "typeof module !== \"undefined\" && module && module.exports", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 304, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 337, + "nodeLength": 24, + "src": "module && module.exports", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2002": [ + null, + { + "position": 572, + "nodeLength": 41, + "src": "typeof exports !== \"undefined\" && exports", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 572, + "nodeLength": 30, + "src": "typeof exports !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2006": [ + null, + { + "position": 657, + "nodeLength": 42, + "src": "typeof define === \"function\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 657, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2015": [ + null, + { + "position": 8, + "nodeLength": 17, + "src": "!defined.document", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2028": [ + null, + { + "position": 30, + "nodeLength": 13, + "src": "onErrorFnPrev", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2034": [ + null, + { + "position": 218, + "nodeLength": 12, + "src": "ret !== true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2035": [ + null, + { + "position": 10, + "nodeLength": 14, + "src": "config.current", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2036": [ + null, + { + "position": 11, + "nodeLength": 33, + "src": "config.current.ignoreGlobalErrors", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2062": [ + null, + { + "position": 50229, + "nodeLength": 56, + "src": "defined.document && window.location.protocol !== \"file:\"", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 50249, + "nodeLength": 36, + "src": "window.location.protocol !== \"file:\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2074": [ + null, + { + "position": 83, + "nodeLength": 22, + "src": "arguments.length === 2", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "2075": [ + null, + { + "position": 10, + "nodeLength": 42, + "src": "objectType(testEnvironment) === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2090": [ + null, + { + "position": 484, + "nodeLength": 37, + "src": "objectType(executeNow) === \"function\"", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "2095": [ + null, + { + "position": 172, + "nodeLength": 36, + "src": "module.parentModule || currentModule", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2101": [ + null, + { + "position": 25, + "nodeLength": 25, + "src": "config.moduleStack.length", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "2102": [ + null, + { + "position": 115, + "nodeLength": 21, + "src": "parentModule !== null", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "2113": [ + null, + { + "position": 401, + "nodeLength": 12, + "src": "parentModule", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "2140": [ + null, + { + "position": 64, + "nodeLength": 15, + "src": "!config.current", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2143": [ + null, + { + "position": 42, + "nodeLength": 10, + "src": "runStarted", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2145": [ + null, + { + "position": 149, + "nodeLength": 37, + "src": "globalStartAlreadyCalled || count > 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 177, + "nodeLength": 9, + "src": "count > 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2147": [ + null, + { + "position": 289, + "nodeLength": 16, + "src": "config.autostart", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2149": [ + null, + { + "position": 435, + "nodeLength": 39, + "src": "!defined.document && !config.pageLoaded", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2153": [ + null, + { + "position": 582, + "nodeLength": 18, + "src": "!config.pageLoaded", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2186": [ + null, + { + "position": 230, + "nodeLength": 11, + "src": "!runStarted", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2189": [ + null, + { + "position": 41, + "nodeLength": 16, + "src": "config.autostart", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2196": [ + null, + { + "position": 15, + "nodeLength": 11, + "src": "offset || 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2213": [ + null, + { + "position": 104, + "nodeLength": 18, + "src": "defined.setTimeout", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2228": [ + null, + { + "position": 103, + "nodeLength": 15, + "src": "!config.started", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2234": [ + null, + { + "position": 140, + "nodeLength": 69, + "src": "config.modules[0].name === \"\" && config.modules[0].tests.length === 0", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 140, + "nodeLength": 29, + "src": "config.modules[0].name === \"\"", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 173, + "nodeLength": 36, + "src": "config.modules[0].tests.length === 0", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2239": [ + null, + { + "position": 371, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 1, + "evalTrue": 2 + } + ], + "2262": [ + null, + { + "position": 87, + "nodeLength": 17, + "src": "config.depth || 0", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2264": [ + null, + { + "position": 122, + "nodeLength": 39, + "src": "config.queue.length && !config.blocking", + "evalFalse": 1, + "evalTrue": 24 + } + ], + "2265": [ + null, + { + "position": 9, + "nodeLength": 82, + "src": "!defined.setTimeout || config.updateRate <= 0 || now() - start < config.updateRate", + "evalFalse": 0, + "evalTrue": 24 + }, + { + "position": 32, + "nodeLength": 59, + "src": "config.updateRate <= 0 || now() - start < config.updateRate", + "evalFalse": 0, + "evalTrue": 24 + }, + { + "position": 32, + "nodeLength": 22, + "src": "config.updateRate <= 0", + "evalFalse": 24, + "evalTrue": 0 + }, + { + "position": 58, + "nodeLength": 33, + "src": "now() - start < config.updateRate", + "evalFalse": 0, + "evalTrue": 24 + } + ], + "2266": [ + null, + { + "position": 10, + "nodeLength": 14, + "src": "config.current", + "evalFalse": 8, + "evalTrue": 16 + } + ], + "2278": [ + null, + { + "position": 519, + "nodeLength": 70, + "src": "last && !config.blocking && !config.queue.length && config.depth === 0", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 527, + "nodeLength": 62, + "src": "!config.blocking && !config.queue.length && config.depth === 0", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 547, + "nodeLength": 42, + "src": "!config.queue.length && config.depth === 0", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 571, + "nodeLength": 18, + "src": "config.depth === 0", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2303": [ + null, + { + "position": 407, + "nodeLength": 33, + "src": "storage && config.stats.bad === 0", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 418, + "nodeLength": 22, + "src": "config.stats.bad === 0", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2304": [ + null, + { + "position": 34, + "nodeLength": 6, + "src": "i >= 0", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2306": [ + null, + { + "position": 37, + "nodeLength": 32, + "src": "key.indexOf(\"qunit-test-\") === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2314": [ + null, + { + "position": 8, + "nodeLength": 36, + "src": "module.testEnvironment === undefined", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "2327": [ + null, + { + "position": 9, + "nodeLength": 64, + "src": "typeof window === \"undefined\" || typeof document === \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 9, + "nodeLength": 29, + "src": "typeof window === \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 42, + "nodeLength": 31, + "src": "typeof document === \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2338": [ + null, + { + "position": 55, + "nodeLength": 30, + "src": "hasOwn.call(config, \"fixture\")", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2343": [ + null, + { + "position": 177, + "nodeLength": 7, + "src": "fixture", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2352": [ + null, + { + "position": 9, + "nodeLength": 22, + "src": "config.fixture == null", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "2357": [ + null, + { + "position": 123, + "nodeLength": 7, + "src": "fixture", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "2368": [ + null, + { + "position": 70, + "nodeLength": 48, + "src": "typeof window !== \"undefined\" && window.location", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 70, + "nodeLength": 29, + "src": "typeof window !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2369": [ + null, + { + "position": 127, + "nodeLength": 9, + "src": "!location", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2378": [ + null, + { + "position": 313, + "nodeLength": 24, + "src": "urlParams.moduleId || []", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2379": [ + null, + { + "position": 375, + "nodeLength": 22, + "src": "urlParams.testId || []", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2388": [ + null, + { + "position": 672, + "nodeLength": 23, + "src": "urlParams.seed === true", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2392": [ + null, + { + "position": 849, + "nodeLength": 14, + "src": "urlParams.seed", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2416": [ + null, + { + "position": 89, + "nodeLength": 20, + "src": "i < urlConfig.length", + "evalFalse": 1, + "evalTrue": 3 + } + ], + "2420": [ + null, + { + "position": 131, + "nodeLength": 26, + "src": "typeof option !== \"string\"", + "evalFalse": 0, + "evalTrue": 3 + } + ], + "2424": [ + null, + { + "position": 204, + "nodeLength": 34, + "src": "QUnit.config[option] === undefined", + "evalFalse": 0, + "evalTrue": 3 + } + ], + "2436": [ + null, + { + "position": 176, + "nodeLength": 10, + "src": "i < length", + "evalFalse": 1, + "evalTrue": 1 + } + ], + "2437": [ + null, + { + "position": 10, + "nodeLength": 9, + "src": "params[i]", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2442": [ + null, + { + "position": 164, + "nodeLength": 64, + "src": "param.length === 1 || decodeQueryParam(param.slice(1).join(\"=\"))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 164, + "nodeLength": 18, + "src": "param.length === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2443": [ + null, + { + "position": 240, + "nodeLength": 17, + "src": "name in urlParams", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2461": [ + null, + { + "position": 8, + "nodeLength": 2, + "src": "!s", + "evalFalse": 38, + "evalTrue": 0 + } + ], + "2486": [ + null, + { + "position": 72, + "nodeLength": 49, + "src": "typeof window === \"undefined\" || !window.document", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 72, + "nodeLength": 29, + "src": "typeof window === \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2508": [ + null, + { + "position": 38, + "nodeLength": 3, + "src": "i--", + "evalFalse": 2, + "evalTrue": 3 + } + ], + "2514": [ + null, + { + "position": 13, + "nodeLength": 58, + "src": "(\" \" + elem.className + \" \").indexOf(\" \" + name + \" \") >= 0", + "evalFalse": 14, + "evalTrue": 1 + } + ], + "2518": [ + null, + { + "position": 9, + "nodeLength": 21, + "src": "!hasClass(elem, name)", + "evalFalse": 1, + "evalTrue": 14 + } + ], + "2519": [ + null, + { + "position": 25, + "nodeLength": 14, + "src": "elem.className", + "evalFalse": 6, + "evalTrue": 8 + } + ], + "2524": [ + null, + { + "position": 9, + "nodeLength": 62, + "src": "force || typeof force === \"undefined\" && !hasClass(elem, name)", + "evalFalse": 2, + "evalTrue": 1 + }, + { + "position": 18, + "nodeLength": 53, + "src": "typeof force === \"undefined\" && !hasClass(elem, name)", + "evalFalse": 2, + "evalTrue": 0 + }, + { + "position": 18, + "nodeLength": 28, + "src": "typeof force === \"undefined\"", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "2535": [ + null, + { + "position": 99, + "nodeLength": 34, + "src": "set.indexOf(\" \" + name + \" \") >= 0", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "2540": [ + null, + { + "position": 239, + "nodeLength": 30, + "src": "typeof set.trim === \"function\"", + "evalFalse": 0, + "evalTrue": 2 + } + ], + "2544": [ + null, + { + "position": 12, + "nodeLength": 62, + "src": "document$$1.getElementById && document$$1.getElementById(name)", + "evalFalse": 1, + "evalTrue": 41 + } + ], + "2549": [ + null, + { + "position": 63, + "nodeLength": 11, + "src": "abortButton", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2560": [ + null, + { + "position": 32, + "nodeLength": 23, + "src": "ev && ev.preventDefault", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2577": [ + null, + { + "position": 187, + "nodeLength": 20, + "src": "i < urlConfig.length", + "evalFalse": 1, + "evalTrue": 3 + } + ], + "2581": [ + null, + { + "position": 122, + "nodeLength": 23, + "src": "typeof val === \"string\"", + "evalFalse": 3, + "evalTrue": 0 + } + ], + "2591": [ + null, + { + "position": 306, + "nodeLength": 43, + "src": "!val.value || typeof val.value === \"string\"", + "evalFalse": 0, + "evalTrue": 3 + }, + { + "position": 320, + "nodeLength": 29, + "src": "typeof val.value === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2592": [ + null, + { + "position": 188, + "nodeLength": 9, + "src": "val.value", + "evalFalse": 3, + "evalTrue": 0 + }, + { + "position": 250, + "nodeLength": 14, + "src": "config[val.id]", + "evalFalse": 3, + "evalTrue": 0 + } + ], + "2596": [ + null, + { + "position": 258, + "nodeLength": 28, + "src": "QUnit.is(\"array\", val.value)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2597": [ + null, + { + "position": 20, + "nodeLength": 20, + "src": "j < val.value.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2599": [ + null, + { + "position": 107, + "nodeLength": 31, + "src": "config[val.id] === val.value[j]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 142, + "nodeLength": 43, + "src": "(selection = true) && \" selected='selected'\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2603": [ + null, + { + "position": 13, + "nodeLength": 25, + "src": "hasOwn.call(val.value, j)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2604": [ + null, + { + "position": 70, + "nodeLength": 20, + "src": "config[val.id] === j", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 94, + "nodeLength": 43, + "src": "(selection = true) && \" selected='selected'\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2608": [ + null, + { + "position": 883, + "nodeLength": 28, + "src": "config[val.id] && !selection", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2629": [ + null, + { + "position": 157, + "nodeLength": 24, + "src": "\"selectedIndex\" in field", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2630": [ + null, + { + "position": 14, + "nodeLength": 53, + "src": "field.options[field.selectedIndex].value || undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2632": [ + null, + { + "position": 14, + "nodeLength": 13, + "src": "field.checked", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30, + "nodeLength": 26, + "src": "field.defaultValue || true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2639": [ + null, + { + "position": 479, + "nodeLength": 63, + "src": "\"hidepassed\" === field.name && \"replaceState\" in window.history", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 479, + "nodeLength": 27, + "src": "\"hidepassed\" === field.name", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2641": [ + null, + { + "position": 69, + "nodeLength": 14, + "src": "value || false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2643": [ + null, + { + "position": 126, + "nodeLength": 5, + "src": "tests", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2644": [ + null, + { + "position": 38, + "nodeLength": 14, + "src": "value || false", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2664": [ + null, + { + "position": 58, + "nodeLength": 53, + "src": "hasOwn.call(params, key) && params[key] !== undefined", + "evalFalse": 0, + "evalTrue": 4 + }, + { + "position": 86, + "nodeLength": 25, + "src": "params[key] !== undefined", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "2668": [ + null, + { + "position": 139, + "nodeLength": 19, + "src": "i < arrValue.length", + "evalFalse": 4, + "evalTrue": 4 + } + ], + "2670": [ + null, + { + "position": 59, + "nodeLength": 20, + "src": "arrValue[i] !== true", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "2686": [ + null, + { + "position": 200, + "nodeLength": 22, + "src": "i < modulesList.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2687": [ + null, + { + "position": 10, + "nodeLength": 22, + "src": "modulesList[i].checked", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2693": [ + null, + { + "position": 14, + "nodeLength": 13, + "src": "filter === \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2694": [ + null, + { + "position": 65, + "nodeLength": 28, + "src": "selectedModules.length === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2733": [ + null, + { + "position": 329, + "nodeLength": 19, + "src": "config.filter || \"\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2754": [ + null, + { + "position": 65, + "nodeLength": 25, + "src": "i < config.modules.length", + "evalFalse": 1, + "evalTrue": 2 + } + ], + "2755": [ + null, + { + "position": 10, + "nodeLength": 29, + "src": "config.modules[i].name !== \"\"", + "evalFalse": 0, + "evalTrue": 2 + } + ], + "2756": [ + null, + { + "position": 17, + "nodeLength": 56, + "src": "config.moduleId.indexOf(config.modules[i].moduleId) > -1", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "2757": [ + null, + { + "position": 122, + "nodeLength": 7, + "src": "checked", + "evalFalse": 2, + "evalTrue": 0 + }, + { + "position": 229, + "nodeLength": 7, + "src": "checked", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "2787": [ + null, + { + "position": 995, + "nodeLength": 22, + "src": "config.moduleId.length", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1069, + "nodeLength": 22, + "src": "config.moduleId.length", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2815": [ + null, + { + "position": 10, + "nodeLength": 33, + "src": "dropDown.style.display !== \"none\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2827": [ + null, + { + "position": 69, + "nodeLength": 32, + "src": "e.keyCode === 27 || !inContainer", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69, + "nodeLength": 16, + "src": "e.keyCode === 27", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2828": [ + null, + { + "position": 12, + "nodeLength": 31, + "src": "e.keyCode === 27 && inContainer", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12, + "nodeLength": 16, + "src": "e.keyCode === 27", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2847": [ + null, + { + "position": 146, + "nodeLength": 20, + "src": "i < listItems.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2849": [ + null, + { + "position": 38, + "nodeLength": 70, + "src": "!searchText || item.textContent.toLowerCase().indexOf(searchText) > -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53, + "nodeLength": 55, + "src": "item.textContent.toLowerCase().indexOf(searchText) > -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2861": [ + null, + { + "position": 41, + "nodeLength": 32, + "src": "evt && evt.target || allCheckbox", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 41, + "nodeLength": 17, + "src": "evt && evt.target", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2868": [ + null, + { + "position": 277, + "nodeLength": 44, + "src": "checkbox.checked && checkbox !== allCheckbox", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 297, + "nodeLength": 24, + "src": "checkbox !== allCheckbox", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2872": [ + null, + { + "position": 438, + "nodeLength": 22, + "src": "i < modulesList.length", + "evalFalse": 1, + "evalTrue": 2 + } + ], + "2874": [ + null, + { + "position": 40, + "nodeLength": 4, + "src": "!evt", + "evalFalse": 0, + "evalTrue": 2 + } + ], + "2876": [ + null, + { + "position": 128, + "nodeLength": 44, + "src": "checkbox === allCheckbox && checkbox.checked", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 128, + "nodeLength": 24, + "src": "checkbox === allCheckbox", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2880": [ + null, + { + "position": 274, + "nodeLength": 45, + "src": "dirty || item.checked !== item.defaultChecked", + "evalFalse": 2, + "evalTrue": 0 + }, + { + "position": 283, + "nodeLength": 36, + "src": "item.checked !== item.defaultChecked", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "2881": [ + null, + { + "position": 331, + "nodeLength": 12, + "src": "item.checked", + "evalFalse": 2, + "evalTrue": 0 + } + ], + "2886": [ + null, + { + "position": 937, + "nodeLength": 5, + "src": "dirty", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2887": [ + null, + { + "position": 990, + "nodeLength": 62, + "src": "selectedNames.join(\", \") || allCheckbox.parentNode.textContent", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2888": [ + null, + { + "position": 1127, + "nodeLength": 62, + "src": "selectedNames.join(\"\\n\") || allCheckbox.parentNode.textContent", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2897": [ + null, + { + "position": 60, + "nodeLength": 7, + "src": "toolbar", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2908": [ + null, + { + "position": 47, + "nodeLength": 6, + "src": "header", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2916": [ + null, + { + "position": 47, + "nodeLength": 6, + "src": "banner", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2926": [ + null, + { + "position": 104, + "nodeLength": 6, + "src": "result", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2930": [ + null, + { + "position": 173, + "nodeLength": 5, + "src": "tests", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2940": [ + null, + { + "position": 618, + "nodeLength": 8, + "src": "controls", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2947": [ + null, + { + "position": 47, + "nodeLength": 29, + "src": "!testId || testId.length <= 0", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 58, + "nodeLength": 18, + "src": "testId.length <= 0", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2956": [ + null, + { + "position": 53, + "nodeLength": 9, + "src": "userAgent", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2965": [ + null, + { + "position": 39, + "nodeLength": 5, + "src": "qunit", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2979": [ + null, + { + "position": 75, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 1, + "evalTrue": 2 + } + ], + "2982": [ + null, + { + "position": 76, + "nodeLength": 5, + "src": "x < z", + "evalFalse": 2, + "evalTrue": 4 + } + ], + "2997": [ + null, + { + "position": 121, + "nodeLength": 6, + "src": "!tests", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "3026": [ + null, + { + "position": 90, + "nodeLength": 26, + "src": "i < details.modules.length", + "evalFalse": 1, + "evalTrue": 2 + } + ], + "3028": [ + null, + { + "position": 47, + "nodeLength": 14, + "src": "moduleObj.name", + "evalFalse": 0, + "evalTrue": 2 + } + ], + "3040": [ + null, + { + "position": 455, + "nodeLength": 26, + "src": "tests && config.hidepassed", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3055": [ + null, + { + "position": 495, + "nodeLength": 35, + "src": "abortButton && abortButton.disabled", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3058": [ + null, + { + "position": 96, + "nodeLength": 25, + "src": "i < tests.children.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3060": [ + null, + { + "position": 43, + "nodeLength": 53, + "src": "test.className === \"\" || test.className === \"running\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 43, + "nodeLength": 21, + "src": "test.className === \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 68, + "nodeLength": 28, + "src": "test.className === \"running\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3071": [ + null, + { + "position": 1056, + "nodeLength": 58, + "src": "banner && (!abortButton || abortButton.disabled === false)", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 1067, + "nodeLength": 46, + "src": "!abortButton || abortButton.disabled === false", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 1083, + "nodeLength": 30, + "src": "abortButton.disabled === false", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "3072": [ + null, + { + "position": 25, + "nodeLength": 14, + "src": "details.failed", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3075": [ + null, + { + "position": 1203, + "nodeLength": 11, + "src": "abortButton", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "3079": [ + null, + { + "position": 1287, + "nodeLength": 5, + "src": "tests", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "3083": [ + null, + { + "position": 1365, + "nodeLength": 38, + "src": "config.altertitle && document$$1.title", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "3087": [ + null, + { + "position": 162, + "nodeLength": 14, + "src": "details.failed", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3091": [ + null, + { + "position": 1726, + "nodeLength": 35, + "src": "config.scrolltop && window.scrollTo", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "3099": [ + null, + { + "position": 33, + "nodeLength": 6, + "src": "module", + "evalFalse": 0, + "evalTrue": 8 + } + ], + "3112": [ + null, + { + "position": 102, + "nodeLength": 9, + "src": "testBlock", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "3121": [ + null, + { + "position": 329, + "nodeLength": 7, + "src": "running", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "3122": [ + null, + { + "position": 12, + "nodeLength": 47, + "src": "QUnit.config.reorder && details.previousFailure", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "3124": [ + null, + { + "position": 88, + "nodeLength": 3, + "src": "bad", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "3144": [ + null, + { + "position": 201, + "nodeLength": 9, + "src": "!testItem", + "evalFalse": 9, + "evalTrue": 0 + } + ], + "3148": [ + null, + { + "position": 248, + "nodeLength": 67, + "src": "escapeText(details.message) || (details.result ? \"okay\" : \"failed\")", + "evalFalse": 0, + "evalTrue": 9 + }, + { + "position": 280, + "nodeLength": 14, + "src": "details.result", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3155": [ + null, + { + "position": 693, + "nodeLength": 51, + "src": "!details.result && hasOwn.call(details, \"expected\")", + "evalFalse": 9, + "evalTrue": 0 + } + ], + "3156": [ + null, + { + "position": 10, + "nodeLength": 16, + "src": "details.negative", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3165": [ + null, + { + "position": 350, + "nodeLength": 19, + "src": "actual !== expected", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3170": [ + null, + { + "position": 186, + "nodeLength": 66, + "src": "!/^(true|false)$/.test(actual) && !/^(true|false)$/.test(expected)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3172": [ + null, + { + "position": 63, + "nodeLength": 80, + "src": "stripHtml(diff).length !== stripHtml(expected).length + stripHtml(actual).length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3176": [ + null, + { + "position": 489, + "nodeLength": 8, + "src": "showDiff", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3179": [ + null, + { + "position": 994, + "nodeLength": 87, + "src": "expected.indexOf(\"[object Array]\") !== -1 || expected.indexOf(\"[object Object]\") !== -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 994, + "nodeLength": 41, + "src": "expected.indexOf(\"[object Array]\") !== -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1039, + "nodeLength": 42, + "src": "expected.indexOf(\"[object Object]\") !== -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3185": [ + null, + { + "position": 1649, + "nodeLength": 14, + "src": "details.source", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3192": [ + null, + { + "position": 2669, + "nodeLength": 33, + "src": "!details.result && details.source", + "evalFalse": 9, + "evalTrue": 0 + } + ], + "3199": [ + null, + { + "position": 2988, + "nodeLength": 14, + "src": "details.result", + "evalFalse": 0, + "evalTrue": 9 + } + ], + "3216": [ + null, + { + "position": 200, + "nodeLength": 6, + "src": "!tests", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "3227": [ + null, + { + "position": 409, + "nodeLength": 9, + "src": "bad === 0", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "3231": [ + null, + { + "position": 520, + "nodeLength": 15, + "src": "config.collapse", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3232": [ + null, + { + "position": 10, + "nodeLength": 13, + "src": "!collapseNext", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3246": [ + null, + { + "position": 853, + "nodeLength": 3, + "src": "bad", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "3250": [ + null, + { + "position": 1056, + "nodeLength": 15, + "src": "details.skipped", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "3261": [ + null, + { + "position": 135, + "nodeLength": 3, + "src": "bad", + "evalFalse": 4, + "evalTrue": 0 + } + ], + "3270": [ + null, + { + "position": 1710, + "nodeLength": 14, + "src": "details.source", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "3274": [ + null, + { + "position": 177, + "nodeLength": 9, + "src": "bad === 0", + "evalFalse": 0, + "evalTrue": 4 + } + ], + "3287": [ + null, + { + "position": 14, + "nodeLength": 37, + "src": "p && p.version && p.version.major > 0", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 19, + "nodeLength": 32, + "src": "p.version && p.version.major > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 32, + "nodeLength": 19, + "src": "p.version.major > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3290": [ + null, + { + "position": 25725, + "nodeLength": 51, + "src": "notPhantom && document$$1.readyState === \"complete\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 25739, + "nodeLength": 37, + "src": "document$$1.readyState === \"complete\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3356": [ + null, + { + "position": 216, + "nodeLength": 32, + "src": "text1 === null || text2 === null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 216, + "nodeLength": 14, + "src": "text1 === null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 234, + "nodeLength": 14, + "src": "text2 === null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3361": [ + null, + { + "position": 352, + "nodeLength": 15, + "src": "text1 === text2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3362": [ + null, + { + "position": 10, + "nodeLength": 5, + "src": "text1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3368": [ + null, + { + "position": 463, + "nodeLength": 36, + "src": "typeof optChecklines === \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3390": [ + null, + { + "position": 1250, + "nodeLength": 12, + "src": "commonprefix", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3393": [ + null, + { + "position": 1328, + "nodeLength": 12, + "src": "commonsuffix", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3426": [ + null, + { + "position": 785, + "nodeLength": 22, + "src": "pointer < diffs.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3429": [ + null, + { + "position": 35, + "nodeLength": 32, + "src": "diffs[pointer][0] === DIFF_EQUAL", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3430": [ + null, + { + "position": 11, + "nodeLength": 52, + "src": "diffs[pointer][1].length < 4 && (postIns || postDel)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 11, + "nodeLength": 28, + "src": "diffs[pointer][1].length < 4", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 44, + "nodeLength": 18, + "src": "postIns || postDel", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3448": [ + null, + { + "position": 12, + "nodeLength": 33, + "src": "diffs[pointer][0] === DIFF_DELETE", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3462": [ + null, + { + "position": 429, + "nodeLength": 128, + "src": "lastequality && (preIns && preDel && postIns && postDel || lastequality.length < 2 && preIns + preDel + postIns + postDel === 3)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 446, + "nodeLength": 110, + "src": "preIns && preDel && postIns && postDel || lastequality.length < 2 && preIns + preDel + postIns + postDel === 3", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 446, + "nodeLength": 38, + "src": "preIns && preDel && postIns && postDel", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 456, + "nodeLength": 28, + "src": "preDel && postIns && postDel", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 466, + "nodeLength": 18, + "src": "postIns && postDel", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 488, + "nodeLength": 68, + "src": "lastequality.length < 2 && preIns + preDel + postIns + postDel === 3", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 488, + "nodeLength": 23, + "src": "lastequality.length < 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 515, + "nodeLength": 41, + "src": "preIns + preDel + postIns + postDel === 3", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3471": [ + null, + { + "position": 337, + "nodeLength": 16, + "src": "preIns && preDel", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3478": [ + null, + { + "position": 84, + "nodeLength": 20, + "src": "equalitiesLength > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3487": [ + null, + { + "position": 2661, + "nodeLength": 7, + "src": "changes", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3503": [ + null, + { + "position": 73, + "nodeLength": 16, + "src": "x < diffs.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3532": [ + null, + { + "position": 110, + "nodeLength": 55, + "src": "!text1 || !text2 || text1.charAt(0) !== text2.charAt(0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 120, + "nodeLength": 45, + "src": "!text2 || text1.charAt(0) !== text2.charAt(0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 130, + "nodeLength": 35, + "src": "text1.charAt(0) !== text2.charAt(0)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3542": [ + null, + { + "position": 421, + "nodeLength": 23, + "src": "pointermin < pointermid", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3543": [ + null, + { + "position": 10, + "nodeLength": 87, + "src": "text1.substring(pointerstart, pointermid) === text2.substring(pointerstart, pointermid)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3564": [ + null, + { + "position": 108, + "nodeLength": 85, + "src": "!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 118, + "nodeLength": 75, + "src": "!text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 128, + "nodeLength": 65, + "src": "text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3574": [ + null, + { + "position": 447, + "nodeLength": 23, + "src": "pointermin < pointermid", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3575": [ + null, + { + "position": 10, + "nodeLength": 143, + "src": "text1.substring(text1.length - pointermid, text1.length - pointerend) === text2.substring(text2.length - pointermid, text2.length - pointerend)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3601": [ + null, + { + "position": 112, + "nodeLength": 6, + "src": "!text1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3607": [ + null, + { + "position": 212, + "nodeLength": 6, + "src": "!text2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3613": [ + null, + { + "position": 322, + "nodeLength": 27, + "src": "text1.length > text2.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3614": [ + null, + { + "position": 383, + "nodeLength": 27, + "src": "text1.length > text2.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3616": [ + null, + { + "position": 473, + "nodeLength": 8, + "src": "i !== -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3622": [ + null, + { + "position": 267, + "nodeLength": 27, + "src": "text1.length > text2.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3628": [ + null, + { + "position": 870, + "nodeLength": 22, + "src": "shorttext.length === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3637": [ + null, + { + "position": 1174, + "nodeLength": 2, + "src": "hm", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3654": [ + null, + { + "position": 1640, + "nodeLength": 54, + "src": "checklines && text1.length > 100 && text2.length > 100", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1654, + "nodeLength": 40, + "src": "text1.length > 100 && text2.length > 100", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1654, + "nodeLength": 18, + "src": "text1.length > 100", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1676, + "nodeLength": 18, + "src": "text2.length > 100", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3675": [ + null, + { + "position": 108, + "nodeLength": 27, + "src": "text1.length > text2.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3676": [ + null, + { + "position": 169, + "nodeLength": 27, + "src": "text1.length > text2.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3677": [ + null, + { + "position": 222, + "nodeLength": 61, + "src": "longtext.length < 4 || shorttext.length * 2 < longtext.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 222, + "nodeLength": 19, + "src": "longtext.length < 4", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 245, + "nodeLength": 38, + "src": "shorttext.length * 2 < longtext.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3701": [ + null, + { + "position": 309, + "nodeLength": 42, + "src": "(j = shorttext.indexOf(seed, j + 1)) !== -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3704": [ + null, + { + "position": 197, + "nodeLength": 47, + "src": "bestCommon.length < suffixLength + prefixLength", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3712": [ + null, + { + "position": 985, + "nodeLength": 40, + "src": "bestCommon.length * 2 >= longtext.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3724": [ + null, + { + "position": 2527, + "nodeLength": 12, + "src": "!hm1 && !hm2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3726": [ + null, + { + "position": 2577, + "nodeLength": 4, + "src": "!hm2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3728": [ + null, + { + "position": 2616, + "nodeLength": 4, + "src": "!hm1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3733": [ + null, + { + "position": 55, + "nodeLength": 29, + "src": "hm1[4].length > hm2[4].length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3737": [ + null, + { + "position": 2819, + "nodeLength": 27, + "src": "text1.length > text2.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3787": [ + null, + { + "position": 759, + "nodeLength": 22, + "src": "pointer < diffs.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3800": [ + null, + { + "position": 95, + "nodeLength": 36, + "src": "countDelete >= 1 && countInsert >= 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95, + "nodeLength": 16, + "src": "countDelete >= 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 115, + "nodeLength": 16, + "src": "countInsert >= 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3806": [ + null, + { + "position": 307, + "nodeLength": 6, + "src": "j >= 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3848": [ + null, + { + "position": 566, + "nodeLength": 11, + "src": "x < vLength", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3858": [ + null, + { + "position": 840, + "nodeLength": 15, + "src": "delta % 2 !== 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3866": [ + null, + { + "position": 1032, + "nodeLength": 8, + "src": "d < maxD", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3869": [ + null, + { + "position": 52, + "nodeLength": 31, + "src": "new Date().getTime() > deadline", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3874": [ + null, + { + "position": 175, + "nodeLength": 15, + "src": "k1 <= d - k1end", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3876": [ + null, + { + "position": 42, + "nodeLength": 60, + "src": "k1 === -d || k1 !== d && v1[k1Offset - 1] < v1[k1Offset + 1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42, + "nodeLength": 9, + "src": "k1 === -d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55, + "nodeLength": 47, + "src": "k1 !== d && v1[k1Offset - 1] < v1[k1Offset + 1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55, + "nodeLength": 8, + "src": "k1 !== d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 67, + "nodeLength": 35, + "src": "v1[k1Offset - 1] < v1[k1Offset + 1]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3882": [ + null, + { + "position": 226, + "nodeLength": 77, + "src": "x1 < text1Length && y1 < text2Length && text1.charAt(x1) === text2.charAt(y1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 226, + "nodeLength": 16, + "src": "x1 < text1Length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 246, + "nodeLength": 57, + "src": "y1 < text2Length && text1.charAt(x1) === text2.charAt(y1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 246, + "nodeLength": 16, + "src": "y1 < text2Length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 266, + "nodeLength": 37, + "src": "text1.charAt(x1) === text2.charAt(y1)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3887": [ + null, + { + "position": 376, + "nodeLength": 16, + "src": "x1 > text1Length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3891": [ + null, + { + "position": 476, + "nodeLength": 16, + "src": "y1 > text2Length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3895": [ + null, + { + "position": 579, + "nodeLength": 5, + "src": "front", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3897": [ + null, + { + "position": 52, + "nodeLength": 58, + "src": "k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] !== -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52, + "nodeLength": 13, + "src": "k2Offset >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69, + "nodeLength": 41, + "src": "k2Offset < vLength && v2[k2Offset] !== -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69, + "nodeLength": 18, + "src": "k2Offset < vLength", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91, + "nodeLength": 19, + "src": "v2[k2Offset] !== -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3901": [ + null, + { + "position": 109, + "nodeLength": 8, + "src": "x1 >= x2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3911": [ + null, + { + "position": 1226, + "nodeLength": 15, + "src": "k2 <= d - k2end", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3913": [ + null, + { + "position": 42, + "nodeLength": 60, + "src": "k2 === -d || k2 !== d && v2[k2Offset - 1] < v2[k2Offset + 1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 42, + "nodeLength": 9, + "src": "k2 === -d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55, + "nodeLength": 47, + "src": "k2 !== d && v2[k2Offset - 1] < v2[k2Offset + 1]", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55, + "nodeLength": 8, + "src": "k2 !== d", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 67, + "nodeLength": 35, + "src": "v2[k2Offset - 1] < v2[k2Offset + 1]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3919": [ + null, + { + "position": 226, + "nodeLength": 113, + "src": "x2 < text1Length && y2 < text2Length && text1.charAt(text1Length - x2 - 1) === text2.charAt(text2Length - y2 - 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 226, + "nodeLength": 16, + "src": "x2 < text1Length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 246, + "nodeLength": 93, + "src": "y2 < text2Length && text1.charAt(text1Length - x2 - 1) === text2.charAt(text2Length - y2 - 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 246, + "nodeLength": 16, + "src": "y2 < text2Length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 266, + "nodeLength": 73, + "src": "text1.charAt(text1Length - x2 - 1) === text2.charAt(text2Length - y2 - 1)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3924": [ + null, + { + "position": 412, + "nodeLength": 16, + "src": "x2 > text1Length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3928": [ + null, + { + "position": 511, + "nodeLength": 16, + "src": "y2 > text2Length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3932": [ + null, + { + "position": 611, + "nodeLength": 6, + "src": "!front", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3934": [ + null, + { + "position": 52, + "nodeLength": 58, + "src": "k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] !== -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52, + "nodeLength": 13, + "src": "k1Offset >= 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69, + "nodeLength": 41, + "src": "k1Offset < vLength && v1[k1Offset] !== -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 69, + "nodeLength": 18, + "src": "k1Offset < vLength", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91, + "nodeLength": 19, + "src": "v1[k1Offset] !== -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3940": [ + null, + { + "position": 164, + "nodeLength": 8, + "src": "x1 >= x2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4002": [ + null, + { + "position": 771, + "nodeLength": 22, + "src": "pointer < diffs.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4003": [ + null, + { + "position": 10, + "nodeLength": 32, + "src": "diffs[pointer][0] === DIFF_EQUAL", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4013": [ + null, + { + "position": 46, + "nodeLength": 33, + "src": "diffs[pointer][0] === DIFF_INSERT", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4021": [ + null, + { + "position": 323, + "nodeLength": 156, + "src": "lastequality && lastequality.length <= Math.max(lengthInsertions1, lengthDeletions1) && lastequality.length <= Math.max(lengthInsertions2, lengthDeletions2)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 339, + "nodeLength": 140, + "src": "lastequality.length <= Math.max(lengthInsertions1, lengthDeletions1) && lastequality.length <= Math.max(lengthInsertions2, lengthDeletions2)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 339, + "nodeLength": 68, + "src": "lastequality.length <= Math.max(lengthInsertions1, lengthDeletions1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 411, + "nodeLength": 68, + "src": "lastequality.length <= Math.max(lengthInsertions2, lengthDeletions2)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4034": [ + null, + { + "position": 424, + "nodeLength": 20, + "src": "equalitiesLength > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4049": [ + null, + { + "position": 2354, + "nodeLength": 7, + "src": "changes", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4060": [ + null, + { + "position": 2755, + "nodeLength": 22, + "src": "pointer < diffs.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4061": [ + null, + { + "position": 10, + "nodeLength": 74, + "src": "diffs[pointer - 1][0] === DIFF_DELETE && diffs[pointer][0] === DIFF_INSERT", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10, + "nodeLength": 37, + "src": "diffs[pointer - 1][0] === DIFF_DELETE", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 51, + "nodeLength": 33, + "src": "diffs[pointer][0] === DIFF_INSERT", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4066": [ + null, + { + "position": 224, + "nodeLength": 32, + "src": "overlapLength1 >= overlapLength2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4067": [ + null, + { + "position": 12, + "nodeLength": 79, + "src": "overlapLength1 >= deletion.length / 2 || overlapLength1 >= insertion.length / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12, + "nodeLength": 37, + "src": "overlapLength1 >= deletion.length / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53, + "nodeLength": 38, + "src": "overlapLength1 >= insertion.length / 2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4076": [ + null, + { + "position": 12, + "nodeLength": 79, + "src": "overlapLength2 >= deletion.length / 2 || overlapLength2 >= insertion.length / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 12, + "nodeLength": 37, + "src": "overlapLength2 >= deletion.length / 2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 53, + "nodeLength": 38, + "src": "overlapLength2 >= insertion.length / 2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4111": [ + null, + { + "position": 240, + "nodeLength": 38, + "src": "text1Length === 0 || text2Length === 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 240, + "nodeLength": 17, + "src": "text1Length === 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 261, + "nodeLength": 17, + "src": "text2Length === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4116": [ + null, + { + "position": 347, + "nodeLength": 25, + "src": "text1Length > text2Length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4118": [ + null, + { + "position": 449, + "nodeLength": 25, + "src": "text1Length < text2Length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4124": [ + null, + { + "position": 630, + "nodeLength": 15, + "src": "text1 === text2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4133": [ + null, + { + "position": 897, + "nodeLength": 4, + "src": "true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4136": [ + null, + { + "position": 100, + "nodeLength": 12, + "src": "found === -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4140": [ + null, + { + "position": 173, + "nodeLength": 82, + "src": "found === 0 || text1.substring(textLength - length) === text2.substring(0, length)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 173, + "nodeLength": 11, + "src": "found === 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 188, + "nodeLength": 67, + "src": "text1.substring(textLength - length) === text2.substring(0, length)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4187": [ + null, + { + "position": 456, + "nodeLength": 25, + "src": "lineEnd < text.length - 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4189": [ + null, + { + "position": 58, + "nodeLength": 14, + "src": "lineEnd === -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4195": [ + null, + { + "position": 213, + "nodeLength": 86, + "src": "lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : lineHash[line] !== undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 213, + "nodeLength": 23, + "src": "lineHash.hasOwnProperty", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 271, + "nodeLength": 28, + "src": "lineHash[line] !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4224": [ + null, + { + "position": 44, + "nodeLength": 16, + "src": "x < diffs.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4227": [ + null, + { + "position": 60, + "nodeLength": 16, + "src": "y < chars.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4248": [ + null, + { + "position": 294, + "nodeLength": 22, + "src": "pointer < diffs.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4263": [ + null, + { + "position": 95, + "nodeLength": 29, + "src": "countDelete + countInsert > 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4264": [ + null, + { + "position": 13, + "nodeLength": 38, + "src": "countDelete !== 0 && countInsert !== 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13, + "nodeLength": 17, + "src": "countDelete !== 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34, + "nodeLength": 17, + "src": "countInsert !== 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4268": [ + null, + { + "position": 130, + "nodeLength": 18, + "src": "commonlength !== 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4269": [ + null, + { + "position": 15, + "nodeLength": 107, + "src": "pointer - countDelete - countInsert > 0 && diffs[pointer - countDelete - countInsert - 1][0] === DIFF_EQUAL", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15, + "nodeLength": 39, + "src": "pointer - countDelete - countInsert > 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 58, + "nodeLength": 64, + "src": "diffs[pointer - countDelete - countInsert - 1][0] === DIFF_EQUAL", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4281": [ + null, + { + "position": 776, + "nodeLength": 18, + "src": "commonlength !== 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4289": [ + null, + { + "position": 1221, + "nodeLength": 17, + "src": "countDelete === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4291": [ + null, + { + "position": 1362, + "nodeLength": 17, + "src": "countInsert === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4296": [ + null, + { + "position": 1707, + "nodeLength": 11, + "src": "countDelete", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1731, + "nodeLength": 11, + "src": "countInsert", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4297": [ + null, + { + "position": 1903, + "nodeLength": 53, + "src": "pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1903, + "nodeLength": 13, + "src": "pointer !== 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1920, + "nodeLength": 36, + "src": "diffs[pointer - 1][0] === DIFF_EQUAL", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4312": [ + null, + { + "position": 2872, + "nodeLength": 33, + "src": "diffs[diffs.length - 1][1] === \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4323": [ + null, + { + "position": 3291, + "nodeLength": 26, + "src": "pointer < diffs.length - 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4324": [ + null, + { + "position": 10, + "nodeLength": 76, + "src": "diffs[pointer - 1][0] === DIFF_EQUAL && diffs[pointer + 1][0] === DIFF_EQUAL", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10, + "nodeLength": 36, + "src": "diffs[pointer - 1][0] === DIFF_EQUAL", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50, + "nodeLength": 36, + "src": "diffs[pointer + 1][0] === DIFF_EQUAL", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4330": [ + null, + { + "position": 200, + "nodeLength": 34, + "src": "position === diffs[pointer - 1][1]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4337": [ + null, + { + "position": 588, + "nodeLength": 80, + "src": "diffPointer.substring(0, diffs[pointer + 1][1].length) === diffs[pointer + 1][1]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4350": [ + null, + { + "position": 4480, + "nodeLength": 7, + "src": "changes", + "evalFalse": 0, + "evalTrue": 0 + } + ] + } + }, + "/vendor/js/sinon/sinon.js": { + "lineData": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + 0, + 0, + null, + 1, + 0, + null, + 1, + null, + null, + 1, + 1, + 1, + 1, + 3, + 1, + 2, + 1, + 1, + 1, + null, + null, + 1, + 1, + null, + 0, + 0, + null, + 1, + 1, + null, + 1, + null, + null, + null, + 0, + 0, + null, + null, + 1, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + 1, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + 0, + 0, + null, + null, + 1, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + 0, + null, + 1, + 1, + null, + null, + null, + null, + 1, + 1, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 1, + 1, + 2, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 1, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + 1, + null, + 1, + 1, + 1, + 1, + 1, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + 1, + 1, + 1, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 1, + 0, + null, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + null, + null, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + 1, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + 1, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 1, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + 1, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 1, + null, + 1, + 0, + null, + null, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + null, + 1, + null, + 1, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + 1, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 1, + 0, + 1, + 0, + 0, + null, + 1, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + null, + 1, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 1, + null, + null, + 1, + 0, + null, + null, + 1, + 2, + 4, + 3, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + 1, + null, + 1, + 1, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 1, + null, + 1, + 0, + null, + 1, + 0, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + 0, + null, + null, + null, + 1, + null, + 1, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 1, + null, + 1, + 1, + 1, + null, + 1, + 0, + null, + 1, + 0, + null, + 1, + 0, + 0, + null, + 0, + null, + null, + 1, + 1, + 1, + 1, + null, + null, + 1, + 1, + 1, + null, + 1, + null, + null, + null, + 1, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + null, + null, + null, + 1, + null, + 1, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 1, + null, + 1, + 0, + null, + null, + 1, + null, + null, + 1, + 19, + 19, + 19, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + null, + null, + 1, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + 1, + 1, + 0, + null, + null, + 1, + 1, + 1, + 1, + 1, + null, + null, + null, + 1, + null, + null, + 1, + 1, + 1, + null, + 1, + 1, + null, + 1, + null, + null, + 1, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 1, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 1, + 0, + 0, + null, + 0, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 1, + null, + null, + 1, + 1, + null, + 1, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + null, + 1, + 1, + null, + 1, + null, + null, + 1, + null, + null, + 1, + null, + null, + 1, + null, + null, + 1, + null, + null, + 1, + null, + null, + 1, + null, + null, + 11, + null, + null, + 1, + null, + null, + 1, + null, + null, + null, + 1, + 1, + 10, + 10, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 10, + 10, + null, + 10, + 12, + null, + 12, + 155, + 155, + null, + null, + null, + null, + null, + 12, + 0, + null, + null, + null, + 10, + null, + null, + 1, + 1, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + 1, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + null, + null, + 1, + 1, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + 20, + 0, + 20, + 0, + null, + 20, + 20, + null, + null, + 1, + 1, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + 8, + 8, + 0, + null, + null, + null, + null, + 1, + null, + 0, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 1, + 12, + 12, + 12, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + 12, + 12, + 12, + null, + 0, + null, + 12, + null, + 0, + 0, + null, + null, + 12, + 0, + null, + 12, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 1, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + null, + 1, + 8, + 8, + 0, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + null, + 1, + 2, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + 1, + 0, + 0, + null, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + 1, + 0, + null, + null, + 1, + null, + null, + 1, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + null, + null, + null, + null, + 1, + 1, + 0, + 0, + null, + 0, + null, + null, + 1, + 1, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + 1, + 1, + 0, + 0, + 0, + null, + 0, + null, + null, + 1, + null, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 1, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 1, + null, + 1, + 6, + 0, + null, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + null, + 6, + null, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + 1, + 1, + null, + 1, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + 1, + 1, + 1, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + null, + 1, + 2, + 2, + 2, + 2, + null, + null, + 1, + 1, + null, + 1, + 1, + 1, + null, + null, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + 1, + null, + null, + 1, + null, + 1, + 0, + null, + 0, + 0, + null, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + null, + null, + null, + 1, + null, + null, + null, + 1, + null, + 1, + 0, + null, + 1, + null, + null, + 1, + 1, + null, + null, + 1, + null, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + null, + null, + null, + 1, + 1, + null, + 1, + 1, + 1, + 1, + null, + null, + 1, + null, + 1, + 1, + null, + 1, + 0, + null, + 1, + null, + null, + 1, + 1, + 0, + null, + null, + 0, + null, + 1, + null, + null, + 1, + 1, + 1, + null, + null, + 1, + null, + 1, + 0, + null, + null, + 1, + null, + null, + null, + 0, + 0, + null, + null, + null, + 10, + 4, + null, + null, + 6, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 1, + 22, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + null, + 1, + 0, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + null, + 1, + 1, + 0, + null, + 1, + 1, + 0, + null, + null, + 1, + 1, + 0, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + null, + 1, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 1, + null, + 1, + 1, + null, + 1, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + 0, + null, + null, + 1, + 1, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 1, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + 1, + 9, + 0, + 0, + 0, + null, + null, + null, + null, + 1, + null, + 22, + 9, + null, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 1, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + 1, + 0, + null, + null, + 1, + null, + 1, + 1, + 1, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + 1, + null, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + 1, + null, + 1, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 1, + 24, + 0, + 0, + 0, + null, + null, + null, + 1, + 31, + null, + null, + null, + null, + 24, + null, + null, + null, + 1, + 1, + null, + 1, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + 1, + null, + 1, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + null, + 0, + null, + null, + 1, + null, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + null, + 1, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + null, + null, + 1, + 1, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + 1, + null, + 1, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 1, + null, + 0, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + null, + null, + 1, + 0, + 1, + 0, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 1, + null, + 1, + null, + 1, + 1, + 2, + null, + null, + 1, + null, + 2, + 2, + 2, + 2, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + null, + null, + 1, + null, + 1, + null, + 1, + 0, + 0, + null, + null, + 1, + null, + 1, + null, + 1, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 1, + 0, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + 1, + null, + 1, + null, + 1, + null, + 1, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 1, + null, + null, + 1, + 0, + null, + null, + 1, + 1, + 1, + null, + 1, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 0, + null, + null, + 1, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 1, + null, + 1, + 1, + 1, + 1, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + null, + 0, + 0, + null, + 0, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + 1, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 1, + 0, + 1, + 0, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + 1, + 1, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + null, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + null, + 1, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 1, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 1, + 1, + 0, + null, + 1, + 1, + 0, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + null, + 0, + null, + 0, + null, + 1, + null, + 1, + 0, + 0, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + 1, + 0, + 0, + null, + null, + null, + 1, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + 1, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + 1, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + 1, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + 0, + null, + null, + 1, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + 1, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + 0, + null, + 0, + null, + null, + 1, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + 1, + 1, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + null, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 1, + 0, + 1, + 0, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + 1, + null, + 1, + null, + 1, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + 1, + 0, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + 0, + null, + null, + 1, + 0, + 1, + 0, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + null, + 1, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 1, + 0, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + null, + null, + 1, + null, + 0, + null, + 0, + null, + null, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1, + null, + 1, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + null, + null, + 1, + 0, + 1, + 0, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 0, + 0, + 0, + null, + null, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + 0, + null, + null, + null, + 1, + 1, + 0, + 0, + null, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + 1, + 1, + null, + 1, + 0, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + 0, + 0, + null, + null, + 0, + 0, + null, + null, + null, + null, + null, + null, + 1, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + null, + null, + null, + 0, + null, + 0, + null, + null, + null, + 1, + 0, + 0, + 0, + null, + null, + 1, + 19, + 18, + 18, + null, + null, + 19, + 0, + null, + 0, + 0, + null, + 0, + null, + 0, + 0, + null, + 0, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + 1, + 0, + null, + null, + null, + 1, + null, + null, + null, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + null, + 0, + null, + null, + null, + null, + 0, + null, + 0, + 0, + null, + 0, + null, + 0, + null, + null, + null, + null, + 0, + 0, + null, + null, + 0, + 0, + 0, + null, + 0, + 0, + 0, + null, + null, + null, + 0, + null, + null, + null, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + 0, + null, + null, + null, + null, + 1, + 1, + 0, + null, + 1, + 1, + 1, + 1, + 1, + null, + null, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + 0, + 0, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 0, + 0, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + 1 + ], + "functionData": [ + 1, + 0, + 1, + 1, + 3, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 1, + 2, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 2, + 0, + 1, + 1, + 1, + 0, + 0, + 19, + 19, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 11, + 1, + 1, + 10, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 20, + 0, + 1, + 1, + 8, + 0, + 0, + 0, + 12, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 8, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 2, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 10, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 22, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 24, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 2, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 19, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "branchData": { + "38": [ + null, + { + "position": 23, + "nodeLength": 42, + "src": "typeof define === 'function' && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 23, + "nodeLength": 28, + "src": "typeof define === 'function'", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "42": [ + null, + { + "position": 168, + "nodeLength": 27, + "src": "typeof exports === 'object'", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "52": [ + null, + { + "position": 23, + "nodeLength": 15, + "src": "mod == \"samsam\"", + "evalFalse": 2, + "evalTrue": 1 + } + ], + "54": [ + null, + { + "position": 109, + "nodeLength": 46, + "src": "typeof deps === \"function\" && mod.length === 0", + "evalFalse": 1, + "evalTrue": 1 + }, + { + "position": 109, + "nodeLength": 26, + "src": "typeof deps === \"function\"", + "evalFalse": 1, + "evalTrue": 1 + }, + { + "position": 139, + "nodeLength": 16, + "src": "mod.length === 0", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "56": [ + null, + { + "position": 225, + "nodeLength": 24, + "src": "typeof fn === \"function\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "61": [ + null, + { + "position": -1, + "nodeLength": 213, + "src": "(typeof define === \"function\" && define.amd && function(m) {\n define(\"samsam\", m);\n}) || (typeof module === \"object\" && function(m) {\n module.exports = m();\n}) || function(m) {\n this.samsam = m();\n}", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": -1, + "nodeLength": 83, + "src": "typeof define === \"function\" && define.amd && function(m) {\n define(\"samsam\", m);\n}", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": -1, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 31, + "nodeLength": 51, + "src": "define.amd && function(m) {\n define(\"samsam\", m);\n}", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "62": [ + null, + { + "position": 89, + "nodeLength": 123, + "src": "(typeof module === \"object\" && function(m) {\n module.exports = m();\n}) || function(m) {\n this.samsam = m();\n}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91, + "nodeLength": 74, + "src": "typeof module === \"object\" && function(m) {\n module.exports = m();\n}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 91, + "nodeLength": 26, + "src": "typeof module === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "67": [ + null, + { + "position": 45, + "nodeLength": 64, + "src": "typeof document !== \"undefined\" && document.createElement(\"div\")", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 45, + "nodeLength": 31, + "src": "typeof document !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "74": [ + null, + { + "position": 226, + "nodeLength": 42, + "src": "typeof value === \"number\" && value !== val", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 226, + "nodeLength": 25, + "src": "typeof value === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 255, + "nodeLength": 13, + "src": "value !== val", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "92": [ + null, + { + "position": 13, + "nodeLength": 32, + "src": "getClass(object) === 'Arguments'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "93": [ + null, + { + "position": 76, + "nodeLength": 111, + "src": "typeof object !== \"object\" || typeof object.length !== \"number\" || getClass(object) === \"Array\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 76, + "nodeLength": 26, + "src": "typeof object !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 106, + "nodeLength": 81, + "src": "typeof object.length !== \"number\" || getClass(object) === \"Array\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 106, + "nodeLength": 33, + "src": "typeof object.length !== \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "94": [ + null, + { + "position": 52, + "nodeLength": 28, + "src": "getClass(object) === \"Array\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "97": [ + null, + { + "position": 239, + "nodeLength": 34, + "src": "typeof object.callee == \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "117": [ + null, + { + "position": 13, + "nodeLength": 40, + "src": "!object || object.nodeType !== 1 || !div", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24, + "nodeLength": 29, + "src": "object.nodeType !== 1 || !div", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24, + "nodeLength": 21, + "src": "object.nodeType !== 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "136": [ + null, + { + "position": 17, + "nodeLength": 35, + "src": "o.hasOwnProperty.call(object, prop)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "151": [ + null, + { + "position": 16, + "nodeLength": 84, + "src": "typeof value.getTime == \"function\" && value.getTime() == value.valueOf()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16, + "nodeLength": 34, + "src": "typeof value.getTime == \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "152": [ + null, + { + "position": 49, + "nodeLength": 34, + "src": "value.getTime() == value.valueOf()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "162": [ + null, + { + "position": 16, + "nodeLength": 38, + "src": "value === 0 && 1 / value === -Infinity", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16, + "nodeLength": 11, + "src": "value === 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31, + "nodeLength": 23, + "src": "1 / value === -Infinity", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "177": [ + null, + { + "position": 13, + "nodeLength": 45, + "src": "obj1 === obj2 || (isNaN(obj1) && isNaN(obj2))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13, + "nodeLength": 13, + "src": "obj1 === obj2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 31, + "nodeLength": 26, + "src": "isNaN(obj1) && isNaN(obj2)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "178": [ + null, + { + "position": 20, + "nodeLength": 49, + "src": "obj1 !== 0 || isNegZero(obj1) === isNegZero(obj2)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20, + "nodeLength": 10, + "src": "obj1 !== 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 34, + "nodeLength": 35, + "src": "isNegZero(obj1) === isNegZero(obj2)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "220": [ + null, + { + "position": 18, + "nodeLength": 297, + "src": "typeof value === 'object' && value !== null && !(value instanceof Boolean) && !(value instanceof Date) && !(value instanceof Number) && !(value instanceof RegExp) && !(value instanceof String)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 18, + "nodeLength": 25, + "src": "typeof value === 'object'", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47, + "nodeLength": 268, + "src": "value !== null && !(value instanceof Boolean) && !(value instanceof Date) && !(value instanceof Number) && !(value instanceof RegExp) && !(value instanceof String)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47, + "nodeLength": 14, + "src": "value !== null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "221": [ + null, + { + "position": 37, + "nodeLength": 230, + "src": "!(value instanceof Boolean) && !(value instanceof Date) && !(value instanceof Number) && !(value instanceof RegExp) && !(value instanceof String)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "222": [ + null, + { + "position": 50, + "nodeLength": 179, + "src": "!(value instanceof Date) && !(value instanceof Number) && !(value instanceof RegExp) && !(value instanceof String)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "223": [ + null, + { + "position": 50, + "nodeLength": 128, + "src": "!(value instanceof Number) && !(value instanceof RegExp) && !(value instanceof String)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "224": [ + null, + { + "position": 50, + "nodeLength": 77, + "src": "!(value instanceof RegExp) && !(value instanceof String)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "241": [ + null, + { + "position": 45, + "nodeLength": 18, + "src": "i < objects.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "242": [ + null, + { + "position": 21, + "nodeLength": 18, + "src": "objects[i] === obj", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "256": [ + null, + { + "position": 138, + "nodeLength": 179, + "src": "obj1 === obj2 || isNaN(obj1) || isNaN(obj2) || obj1 == null || obj2 == null || type1 !== \"object\" || type2 !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 138, + "nodeLength": 13, + "src": "obj1 === obj2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "257": [ + null, + { + "position": 36, + "nodeLength": 142, + "src": "isNaN(obj1) || isNaN(obj2) || obj1 == null || obj2 == null || type1 !== \"object\" || type2 !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 192, + "nodeLength": 127, + "src": "isNaN(obj2) || obj1 == null || obj2 == null || type1 !== \"object\" || type2 !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "258": [ + null, + { + "position": 34, + "nodeLength": 92, + "src": "obj1 == null || obj2 == null || type1 !== \"object\" || type2 !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 229, + "nodeLength": 12, + "src": "obj1 == null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 15, + "nodeLength": 76, + "src": "obj2 == null || type1 !== \"object\" || type2 !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 246, + "nodeLength": 12, + "src": "obj2 == null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "259": [ + null, + { + "position": 35, + "nodeLength": 40, + "src": "type1 !== \"object\" || type2 !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 283, + "nodeLength": 18, + "src": "type1 !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21, + "nodeLength": 18, + "src": "type2 !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "265": [ + null, + { + "position": 469, + "nodeLength": 34, + "src": "isElement(obj1) || isElement(obj2)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "268": [ + null, + { + "position": 604, + "nodeLength": 18, + "src": "isDate1 || isDate2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "269": [ + null, + { + "position": 21, + "nodeLength": 57, + "src": "!isDate1 || !isDate2 || obj1.getTime() !== obj2.getTime()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33, + "nodeLength": 45, + "src": "!isDate2 || obj1.getTime() !== obj2.getTime()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45, + "nodeLength": 33, + "src": "obj1.getTime() !== obj2.getTime()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "274": [ + null, + { + "position": 790, + "nodeLength": 48, + "src": "obj1 instanceof RegExp && obj2 instanceof RegExp", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "275": [ + null, + { + "position": 21, + "nodeLength": 35, + "src": "obj1.toString() !== obj2.toString()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "283": [ + null, + { + "position": 1103, + "nodeLength": 38, + "src": "isArguments(obj1) || isArguments(obj2)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "284": [ + null, + { + "position": 21, + "nodeLength": 27, + "src": "obj1.length !== obj2.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "286": [ + null, + { + "position": 21, + "nodeLength": 93, + "src": "type1 !== type2 || class1 !== class2 || keys1.length !== keys2.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21, + "nodeLength": 15, + "src": "type1 !== type2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40, + "nodeLength": 74, + "src": "class1 !== class2 || keys1.length !== keys2.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40, + "nodeLength": 17, + "src": "class1 !== class2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "287": [ + null, + { + "position": 44, + "nodeLength": 29, + "src": "keys1.length !== keys2.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "299": [ + null, + { + "position": 1689, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "301": [ + null, + { + "position": 53, + "nodeLength": 33, + "src": "!o.hasOwnProperty.call(obj2, key)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "316": [ + null, + { + "position": 572, + "nodeLength": 9, + "src": "isObject1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "317": [ + null, + { + "position": 642, + "nodeLength": 9, + "src": "isObject2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "323": [ + null, + { + "position": -1, + "nodeLength": 13, + "src": "index1 !== -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "326": [ + null, + { + "position": -1, + "nodeLength": 13, + "src": "index2 !== -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "331": [ + null, + { + "position": 1313, + "nodeLength": 29, + "src": "compared[newPath1 + newPath2]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "336": [ + null, + { + "position": 1483, + "nodeLength": 26, + "src": "index1 === -1 && isObject1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1483, + "nodeLength": 13, + "src": "index1 === -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "340": [ + null, + { + "position": 1637, + "nodeLength": 26, + "src": "index2 === -1 && isObject2", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1637, + "nodeLength": 13, + "src": "index2 === -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "346": [ + null, + { + "position": 1866, + "nodeLength": 22, + "src": "isObject1 && isObject2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "354": [ + null, + { + "position": 2129, + "nodeLength": 46, + "src": "!deepEqual(value1, value2, newPath1, newPath2)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "367": [ + null, + { + "position": 13, + "nodeLength": 19, + "src": "subset.length === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "369": [ + null, + { + "position": 113, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "370": [ + null, + { + "position": 17, + "nodeLength": 26, + "src": "match(array[i], subset[0])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "371": [ + null, + { + "position": 48, + "nodeLength": 5, + "src": "j < k", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "372": [ + null, + { + "position": 25, + "nodeLength": 31, + "src": "!match(array[i + j], subset[j])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "388": [ + null, + { + "position": 13, + "nodeLength": 45, + "src": "matcher && typeof matcher.test === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24, + "nodeLength": 34, + "src": "typeof matcher.test === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "392": [ + null, + { + "position": 126, + "nodeLength": 29, + "src": "typeof matcher === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "393": [ + null, + { + "position": 20, + "nodeLength": 24, + "src": "matcher(object) === true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "396": [ + null, + { + "position": 227, + "nodeLength": 27, + "src": "typeof matcher === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "398": [ + null, + { + "position": 72, + "nodeLength": 38, + "src": "typeof object === \"string\" || !!object", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 72, + "nodeLength": 26, + "src": "typeof object === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "399": [ + null, + { + "position": 131, + "nodeLength": 79, + "src": "notNull && (String(object)).toLowerCase().indexOf(matcher) >= 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "400": [ + null, + { + "position": 27, + "nodeLength": 51, + "src": "(String(object)).toLowerCase().indexOf(matcher) >= 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "403": [ + null, + { + "position": 492, + "nodeLength": 27, + "src": "typeof matcher === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "404": [ + null, + { + "position": 20, + "nodeLength": 18, + "src": "matcher === object", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "407": [ + null, + { + "position": 585, + "nodeLength": 28, + "src": "typeof matcher === \"boolean\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "408": [ + null, + { + "position": 20, + "nodeLength": 18, + "src": "matcher === object", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "411": [ + null, + { + "position": 679, + "nodeLength": 31, + "src": "typeof (matcher) === \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "412": [ + null, + { + "position": 20, + "nodeLength": 30, + "src": "typeof (object) === \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "415": [ + null, + { + "position": 788, + "nodeLength": 16, + "src": "matcher === null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "416": [ + null, + { + "position": 20, + "nodeLength": 15, + "src": "object === null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "419": [ + null, + { + "position": 867, + "nodeLength": 61, + "src": "getClass(object) === \"Array\" && getClass(matcher) === \"Array\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 867, + "nodeLength": 28, + "src": "getClass(object) === \"Array\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 899, + "nodeLength": 29, + "src": "getClass(matcher) === \"Array\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "423": [ + null, + { + "position": 1006, + "nodeLength": 38, + "src": "matcher && typeof matcher === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1017, + "nodeLength": 27, + "src": "typeof matcher === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "424": [ + null, + { + "position": 17, + "nodeLength": 18, + "src": "matcher === object", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "430": [ + null, + { + "position": 63, + "nodeLength": 97, + "src": "typeof value === \"undefined\" && typeof object.getAttribute === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63, + "nodeLength": 28, + "src": "typeof value === \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "431": [ + null, + { + "position": 55, + "nodeLength": 41, + "src": "typeof object.getAttribute === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "434": [ + null, + { + "position": 257, + "nodeLength": 62, + "src": "matcher[prop] === null || typeof matcher[prop] === 'undefined'", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 257, + "nodeLength": 22, + "src": "matcher[prop] === null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 283, + "nodeLength": 36, + "src": "typeof matcher[prop] === 'undefined'", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "435": [ + null, + { + "position": 25, + "nodeLength": 23, + "src": "value !== matcher[prop]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "438": [ + null, + { + "position": 462, + "nodeLength": 61, + "src": "typeof value === \"undefined\" || !match(value, matcher[prop])", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 462, + "nodeLength": 29, + "src": "typeof value === \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "460": [ + null, + { + "position": -1, + "nodeLength": 249, + "src": "(typeof define === \"function\" && define.amd && function(m) {\n define(\"formatio\", [\"samsam\"], m);\n}) || (typeof module === \"object\" && function(m) {\n module.exports = m(require(\"samsam\"));\n}) || function(m) {\n this.formatio = m(this.samsam);\n}", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": -1, + "nodeLength": 101, + "src": "typeof define === \"function\" && define.amd && function(m) {\n define(\"formatio\", [\"samsam\"], m);\n}", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": -1, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 31, + "nodeLength": 69, + "src": "define.amd && function(m) {\n define(\"formatio\", [\"samsam\"], m);\n}", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "462": [ + null, + { + "position": 106, + "nodeLength": 142, + "src": "(typeof module === \"object\" && function(m) {\n module.exports = m(require(\"samsam\"));\n}) || function(m) {\n this.formatio = m(this.samsam);\n}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 108, + "nodeLength": 89, + "src": "typeof module === \"object\" && function(m) {\n module.exports = m(require(\"samsam\"));\n}", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 108, + "nodeLength": 26, + "src": "typeof module === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "476": [ + null, + { + "position": 229, + "nodeLength": 29, + "src": "typeof global !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "479": [ + null, + { + "position": 351, + "nodeLength": 31, + "src": "typeof document !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "485": [ + null, + { + "position": 515, + "nodeLength": 29, + "src": "typeof window !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "490": [ + null, + { + "position": 13, + "nodeLength": 5, + "src": "!func", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "491": [ + null, + { + "position": 47, + "nodeLength": 16, + "src": "func.displayName", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "492": [ + null, + { + "position": 106, + "nodeLength": 9, + "src": "func.name", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "494": [ + null, + { + "position": 224, + "nodeLength": 28, + "src": "(matches && matches[1]) || \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 224, + "nodeLength": 21, + "src": "matches && matches[1]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "498": [ + null, + { + "position": 33, + "nodeLength": 28, + "src": "object && object.constructor", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "499": [ + null, + { + "position": 87, + "nodeLength": 75, + "src": "f.excludeConstructors || formatio.excludeConstructors || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "500": [ + null, + { + "position": 40, + "nodeLength": 34, + "src": "formatio.excludeConstructors || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "503": [ + null, + { + "position": 224, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "504": [ + null, + { + "position": 17, + "nodeLength": 55, + "src": "typeof excludes[i] === \"string\" && excludes[i] === name", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17, + "nodeLength": 31, + "src": "typeof excludes[i] === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52, + "nodeLength": 20, + "src": "excludes[i] === name", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "506": [ + null, + { + "position": 127, + "nodeLength": 42, + "src": "excludes[i].test && excludes[i].test(name)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "515": [ + null, + { + "position": 13, + "nodeLength": 26, + "src": "typeof object !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "517": [ + null, + { + "position": 117, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "518": [ + null, + { + "position": 17, + "nodeLength": 21, + "src": "objects[i] === object", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "524": [ + null, + { + "position": 13, + "nodeLength": 26, + "src": "typeof object === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "526": [ + null, + { + "position": 62, + "nodeLength": 29, + "src": "typeof qs !== \"boolean\" || qs", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 62, + "nodeLength": 23, + "src": "typeof qs !== \"boolean\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "527": [ + null, + { + "position": 112, + "nodeLength": 18, + "src": "processed || quote", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "530": [ + null, + { + "position": 227, + "nodeLength": 59, + "src": "typeof object === \"function\" && !(object instanceof RegExp)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 227, + "nodeLength": 28, + "src": "typeof object === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "534": [ + null, + { + "position": 360, + "nodeLength": 15, + "src": "processed || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "536": [ + null, + { + "position": 390, + "nodeLength": 29, + "src": "isCircular(object, processed)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "538": [ + null, + { + "position": 459, + "nodeLength": 59, + "src": "Object.prototype.toString.call(object) === \"[object Array]\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "542": [ + null, + { + "position": 604, + "nodeLength": 7, + "src": "!object", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 630, + "nodeLength": 23, + "src": "(1 / object) === -Infinity", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "543": [ + null, + { + "position": 686, + "nodeLength": 24, + "src": "samsam.isElement(object)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "545": [ + null, + { + "position": 759, + "nodeLength": 102, + "src": "typeof object.toString === \"function\" && object.toString !== Object.prototype.toString", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 759, + "nodeLength": 37, + "src": "typeof object.toString === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "546": [ + null, + { + "position": 56, + "nodeLength": 45, + "src": "object.toString !== Object.prototype.toString", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "551": [ + null, + { + "position": 979, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "552": [ + null, + { + "position": 17, + "nodeLength": 35, + "src": "object === specialObjects[i].object", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "565": [ + null, + { + "position": 21, + "nodeLength": 15, + "src": "processed || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "569": [ + null, + { + "position": 125, + "nodeLength": 28, + "src": "(this.limitChildrenCount > 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125, + "nodeLength": 27, + "src": "this.limitChildrenCount > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "572": [ + null, + { + "position": 254, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "576": [ + null, + { + "position": 349, + "nodeLength": 16, + "src": "l < array.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "583": [ + null, + { + "position": 21, + "nodeLength": 15, + "src": "processed || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "585": [ + null, + { + "position": 87, + "nodeLength": 11, + "src": "indent || 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "589": [ + null, + { + "position": 240, + "nodeLength": 28, + "src": "(this.limitChildrenCount > 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 240, + "nodeLength": 27, + "src": "this.limitChildrenCount > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "592": [ + null, + { + "position": 379, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "596": [ + null, + { + "position": 84, + "nodeLength": 26, + "src": "isCircular(obj, processed)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "602": [ + null, + { + "position": 268, + "nodeLength": 15, + "src": "/\\s/.test(prop)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "608": [ + null, + { + "position": 863, + "nodeLength": 4, + "src": "cons", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "610": [ + null, + { + "position": 947, + "nodeLength": 5, + "src": "i < k", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "612": [ + null, + { + "position": 986, + "nodeLength": 21, + "src": "l < properties.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "615": [ + null, + { + "position": 1102, + "nodeLength": 20, + "src": "length + indent > 80", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "626": [ + null, + { + "position": 172, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "630": [ + null, + { + "position": 158, + "nodeLength": 51, + "src": "attrName !== \"contenteditable\" || val !== \"inherit\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 158, + "nodeLength": 30, + "src": "attrName !== \"contenteditable\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 192, + "nodeLength": 17, + "src": "val !== \"inherit\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "631": [ + null, + { + "position": 21, + "nodeLength": 5, + "src": "!!val", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "635": [ + null, + { + "position": 538, + "nodeLength": 16, + "src": "pairs.length > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "638": [ + null, + { + "position": 622, + "nodeLength": 19, + "src": "content.length > 20", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "672": [ + null, + { + "position": 20049, + "nodeLength": 52, + "src": "\"object\" == typeof exports && \"undefined\" != typeof module", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 20049, + "nodeLength": 24, + "src": "\"object\" == typeof exports", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 20075, + "nodeLength": 26, + "src": "\"undefined\" != typeof module", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20129, + "nodeLength": 37, + "src": "\"function\" == typeof define && define.amd", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 20129, + "nodeLength": 25, + "src": "\"function\" == typeof define", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 20191, + "nodeLength": 26, + "src": "\"undefined\" != typeof window", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20227, + "nodeLength": 26, + "src": "\"undefined\" != typeof global", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20263, + "nodeLength": 34, + "src": "\"undefined\" != typeof self && (f = self)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20263, + "nodeLength": 24, + "src": "\"undefined\" != typeof self", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36, + "nodeLength": 5, + "src": "!n[o]", + "evalFalse": 1, + "evalTrue": 1 + }, + { + "position": 46, + "nodeLength": 5, + "src": "!t[o]", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 59, + "nodeLength": 35, + "src": "typeof require == \"function\" && require", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 59, + "nodeLength": 26, + "src": "typeof require == \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 98, + "nodeLength": 5, + "src": "!u && a", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 122, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 304, + "nodeLength": 1, + "src": "n", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 359, + "nodeLength": 35, + "src": "typeof require == \"function\" && require", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 359, + "nodeLength": 26, + "src": "typeof require == \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 407, + "nodeLength": 10, + "src": "o < r.length", + "evalFalse": 1, + "evalTrue": 1 + } + ], + "697": [ + null, + { + "position": 498, + "nodeLength": 24, + "src": "'setImmediate' in global", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "708": [ + null, + { + "position": 932, + "nodeLength": 33, + "src": "typeof timeoutResult === \"object\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "720": [ + null, + { + "position": 13, + "nodeLength": 4, + "src": "!str", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "728": [ + null, + { + "position": 172, + "nodeLength": 41, + "src": "l > 3 || !/^(\\d\\d:){0,2}\\d\\d?$/.test(str)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 172, + "nodeLength": 5, + "src": "l > 3", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "732": [ + null, + { + "position": 317, + "nodeLength": 3, + "src": "i--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "735": [ + null, + { + "position": 65, + "nodeLength": 12, + "src": "parsed >= 60", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "749": [ + null, + { + "position": 13, + "nodeLength": 6, + "src": "!epoch", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "750": [ + null, + { + "position": 47, + "nodeLength": 35, + "src": "typeof epoch.getTime === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "751": [ + null, + { + "position": 124, + "nodeLength": 25, + "src": "typeof epoch === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "756": [ + null, + { + "position": 16, + "nodeLength": 51, + "src": "timer && timer.callAt >= from && timer.callAt <= to", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25, + "nodeLength": 42, + "src": "timer.callAt >= from && timer.callAt <= to", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25, + "nodeLength": 20, + "src": "timer.callAt >= from", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49, + "nodeLength": 18, + "src": "timer.callAt <= to", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "762": [ + null, + { + "position": 17, + "nodeLength": 27, + "src": "source.hasOwnProperty(prop)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "768": [ + null, + { + "position": 221, + "nodeLength": 10, + "src": "source.now", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "777": [ + null, + { + "position": 451, + "nodeLength": 15, + "src": "source.toSource", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "826": [ + null, + { + "position": 13, + "nodeLength": 24, + "src": "timer.func === undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "830": [ + null, + { + "position": 137, + "nodeLength": 13, + "src": "!clock.timers", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "836": [ + null, + { + "position": 305, + "nodeLength": 41, + "src": "timer.delay || (clock.duringTick ? 1 : 0)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 321, + "nodeLength": 16, + "src": "clock.duringTick", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "840": [ + null, + { + "position": 403, + "nodeLength": 21, + "src": "addTimerReturnsObject", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "854": [ + null, + { + "position": 54, + "nodeLength": 19, + "src": "a.callAt < b.callAt", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "857": [ + null, + { + "position": 122, + "nodeLength": 19, + "src": "a.callAt > b.callAt", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "862": [ + null, + { + "position": 258, + "nodeLength": 27, + "src": "a.immediate && !b.immediate", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "865": [ + null, + { + "position": 334, + "nodeLength": 27, + "src": "!a.immediate && b.immediate", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "870": [ + null, + { + "position": 488, + "nodeLength": 25, + "src": "a.createdAt < b.createdAt", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "873": [ + null, + { + "position": 562, + "nodeLength": 25, + "src": "a.createdAt > b.createdAt", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "878": [ + null, + { + "position": 696, + "nodeLength": 11, + "src": "a.id < b.id", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "881": [ + null, + { + "position": 756, + "nodeLength": 11, + "src": "a.id > b.id", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "895": [ + null, + { + "position": 17, + "nodeLength": 25, + "src": "timers.hasOwnProperty(id)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "898": [ + null, + { + "position": 81, + "nodeLength": 63, + "src": "isInRange && (!timer || compareTimers(timer, timers[id]) === 1)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 95, + "nodeLength": 48, + "src": "!timer || compareTimers(timer, timers[id]) === 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 105, + "nodeLength": 38, + "src": "compareTimers(timer, timers[id]) === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "910": [ + null, + { + "position": 37, + "nodeLength": 34, + "src": "typeof timer.interval === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "917": [ + null, + { + "position": 17, + "nodeLength": 32, + "src": "typeof timer.func === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "926": [ + null, + { + "position": 466, + "nodeLength": 23, + "src": "!clock.timers[timer.id]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "927": [ + null, + { + "position": 17, + "nodeLength": 9, + "src": "exception", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "933": [ + null, + { + "position": 612, + "nodeLength": 9, + "src": "exception", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "939": [ + null, + { + "position": 13, + "nodeLength": 15, + "src": "timer.immediate", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "941": [ + null, + { + "position": 84, + "nodeLength": 37, + "src": "typeof timer.interval !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "949": [ + null, + { + "position": 13, + "nodeLength": 8, + "src": "!timerId", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "955": [ + null, + { + "position": 216, + "nodeLength": 13, + "src": "!clock.timers", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "961": [ + null, + { + "position": 403, + "nodeLength": 27, + "src": "typeof timerId === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "965": [ + null, + { + "position": 491, + "nodeLength": 36, + "src": "clock.timers.hasOwnProperty(timerId)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "968": [ + null, + { + "position": 133, + "nodeLength": 26, + "src": "timerType(timer) === ttype", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "981": [ + null, + { + "position": 98, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "984": [ + null, + { + "position": 57, + "nodeLength": 29, + "src": "target[method].hadOwnProperty", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1003": [ + null, + { + "position": 172, + "nodeLength": 17, + "src": "method === \"Date\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1012": [ + null, + { + "position": 21, + "nodeLength": 34, + "src": "clock[method].hasOwnProperty(prop)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1031": [ + null, + { + "position": 9971, + "nodeLength": 220, + "src": "Object.keys || function(obj) {\n var ks = [], key;\n for (key in obj) {\n if (obj.hasOwnProperty(key)) {\n ks.push(key);\n }\n }\n return ks;\n}", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1036": [ + null, + { + "position": 17, + "nodeLength": 23, + "src": "obj.hasOwnProperty(key)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1093": [ + null, + { + "position": 18, + "nodeLength": 22, + "src": "typeof ms === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1101": [ + null, + { + "position": 330, + "nodeLength": 27, + "src": "timer && tickFrom <= tickTo", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 339, + "nodeLength": 18, + "src": "tickFrom <= tickTo", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1102": [ + null, + { + "position": 21, + "nodeLength": 22, + "src": "clock.timers[timer.id]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1108": [ + null, + { + "position": 211, + "nodeLength": 20, + "src": "oldNow !== clock.now", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1114": [ + null, + { + "position": 42, + "nodeLength": 19, + "src": "firstException || e", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1125": [ + null, + { + "position": 1272, + "nodeLength": 14, + "src": "firstException", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1146": [ + null, + { + "position": 21, + "nodeLength": 31, + "src": "clock.timers.hasOwnProperty(id)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1162": [ + null, + { + "position": 44, + "nodeLength": 26, + "src": "typeof target === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1168": [ + null, + { + "position": 176, + "nodeLength": 7, + "src": "!target", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1178": [ + null, + { + "position": 380, + "nodeLength": 12, + "src": "toFake || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1180": [ + null, + { + "position": 407, + "nodeLength": 26, + "src": "clock.methods.length === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1184": [ + null, + { + "position": 536, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1191": [ + null, + { + "position": 14531, + "nodeLength": 14, + "src": "global || this", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1193": [ + null, + { + "position": 14755, + "nodeLength": 29, + "src": "typeof global !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 14796, + "nodeLength": 27, + "src": "typeof self !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 14833, + "nodeLength": 29, + "src": "typeof window !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1211": [ + null, + { + "position": 97, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 97, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 130, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 148, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1212": [ + null, + { + "position": 195, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 195, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 227, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 227, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1235": [ + null, + { + "position": 1018, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1237": [ + null, + { + "position": 1077, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1260": [ + null, + { + "position": 20, + "nodeLength": 64, + "src": "typeof document !== \"undefined\" && document.createElement(\"div\")", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 20, + "nodeLength": 31, + "src": "typeof document !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1268": [ + null, + { + "position": 57, + "nodeLength": 22, + "src": "div.parentNode === obj", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1283": [ + null, + { + "position": 16, + "nodeLength": 50, + "src": "div && obj && obj.nodeType === 1 && isDOMNode(obj)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 23, + "nodeLength": 43, + "src": "obj && obj.nodeType === 1 && isDOMNode(obj)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30, + "nodeLength": 36, + "src": "obj.nodeType === 1 && isDOMNode(obj)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30, + "nodeLength": 18, + "src": "obj.nodeType === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1287": [ + null, + { + "position": 16, + "nodeLength": 80, + "src": "typeof obj === \"function\" || !!(obj && obj.constructor && obj.call && obj.apply)", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 16, + "nodeLength": 25, + "src": "typeof obj === \"function\"", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 48, + "nodeLength": 47, + "src": "obj && obj.constructor && obj.call && obj.apply", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 55, + "nodeLength": 40, + "src": "obj.constructor && obj.call && obj.apply", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74, + "nodeLength": 21, + "src": "obj.call && obj.apply", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1291": [ + null, + { + "position": 16, + "nodeLength": 37, + "src": "typeof val === \"number\" && isNaN(val)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16, + "nodeLength": 23, + "src": "typeof val === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1296": [ + null, + { + "position": 17, + "nodeLength": 26, + "src": "!hasOwn.call(target, prop)", + "evalFalse": 1, + "evalTrue": 3 + } + ], + "1303": [ + null, + { + "position": 16, + "nodeLength": 83, + "src": "typeof obj === \"function\" && typeof obj.restore === \"function\" && obj.restore.sinon", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 16, + "nodeLength": 25, + "src": "typeof obj === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45, + "nodeLength": 54, + "src": "typeof obj.restore === \"function\" && obj.restore.sinon", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 45, + "nodeLength": 33, + "src": "typeof obj.restore === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1311": [ + null, + { + "position": 17, + "nodeLength": 7, + "src": "!object", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1315": [ + null, + { + "position": 130, + "nodeLength": 58, + "src": "typeof method !== \"function\" && typeof method !== \"object\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 130, + "nodeLength": 28, + "src": "typeof method !== \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 162, + "nodeLength": 26, + "src": "typeof method !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1322": [ + null, + { + "position": 49, + "nodeLength": 26, + "src": "!isFunction(wrappedMethod)", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1325": [ + null, + { + "position": 280, + "nodeLength": 52, + "src": "wrappedMethod.restore && wrappedMethod.restore.sinon", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1327": [ + null, + { + "position": 470, + "nodeLength": 26, + "src": "wrappedMethod.calledBefore", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1328": [ + null, + { + "position": 32, + "nodeLength": 21, + "src": "wrappedMethod.returns", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1332": [ + null, + { + "position": 722, + "nodeLength": 5, + "src": "error", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1333": [ + null, + { + "position": 25, + "nodeLength": 41, + "src": "wrappedMethod && wrappedMethod.stackTrace", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1351": [ + null, + { + "position": 1829, + "nodeLength": 21, + "src": "object.hasOwnProperty", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1353": [ + null, + { + "position": 1935, + "nodeLength": 13, + "src": "hasES5Support", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1354": [ + null, + { + "position": 35, + "nodeLength": 29, + "src": "(typeof method === \"function\")", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 35, + "nodeLength": 28, + "src": "typeof method === \"function\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1357": [ + null, + { + "position": 201, + "nodeLength": 18, + "src": "!wrappedMethodDesc", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1360": [ + null, + { + "position": 424, + "nodeLength": 60, + "src": "wrappedMethodDesc.restore && wrappedMethodDesc.restore.sinon", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1363": [ + null, + { + "position": 631, + "nodeLength": 5, + "src": "error", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1364": [ + null, + { + "position": 25, + "nodeLength": 49, + "src": "wrappedMethodDesc && wrappedMethodDesc.stackTrace", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1371": [ + null, + { + "position": 969, + "nodeLength": 16, + "src": "i < types.length", + "evalFalse": 1, + "evalTrue": 1 + } + ], + "1377": [ + null, + { + "position": 1226, + "nodeLength": 16, + "src": "i < types.length", + "evalFalse": 1, + "evalTrue": 1 + } + ], + "1384": [ + null, + { + "position": 1566, + "nodeLength": 59, + "src": "typeof method === \"function\" && object[property] !== method", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1566, + "nodeLength": 28, + "src": "typeof method === \"function\"", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 1598, + "nodeLength": 27, + "src": "object[property] !== method", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1404": [ + null, + { + "position": 225, + "nodeLength": 6, + "src": "!owned", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1411": [ + null, + { + "position": 615, + "nodeLength": 13, + "src": "hasES5Support", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1417": [ + null, + { + "position": 879, + "nodeLength": 27, + "src": "object[property] === method", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1424": [ + null, + { + "position": 5300, + "nodeLength": 14, + "src": "!hasES5Support", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1438": [ + null, + { + "position": 17, + "nodeLength": 39, + "src": "sinon.match && sinon.match.isMatcher(a)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1442": [ + null, + { + "position": 125, + "nodeLength": 46, + "src": "typeof a !== \"object\" || typeof b !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 125, + "nodeLength": 21, + "src": "typeof a !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 150, + "nodeLength": 21, + "src": "typeof b !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1443": [ + null, + { + "position": 24, + "nodeLength": 43, + "src": "isReallyNaN(a) && isReallyNaN(b) || a === b", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24, + "nodeLength": 32, + "src": "isReallyNaN(a) && isReallyNaN(b)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 60, + "nodeLength": 7, + "src": "a === b", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1446": [ + null, + { + "position": 274, + "nodeLength": 28, + "src": "isElement(a) || isElement(b)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1447": [ + null, + { + "position": 24, + "nodeLength": 7, + "src": "a === b", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1450": [ + null, + { + "position": 369, + "nodeLength": 7, + "src": "a === b", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1454": [ + null, + { + "position": 441, + "nodeLength": 55, + "src": "(a === null && b !== null) || (a !== null && b === null)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 441, + "nodeLength": 24, + "src": "a === null && b !== null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 441, + "nodeLength": 10, + "src": "a === null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 455, + "nodeLength": 10, + "src": "b !== null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 471, + "nodeLength": 24, + "src": "a !== null && b === null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 471, + "nodeLength": 10, + "src": "a !== null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 485, + "nodeLength": 10, + "src": "b === null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1458": [ + null, + { + "position": 561, + "nodeLength": 42, + "src": "a instanceof RegExp && b instanceof RegExp", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1459": [ + null, + { + "position": 25, + "nodeLength": 137, + "src": "(a.source === b.source) && (a.global === b.global) && (a.ignoreCase === b.ignoreCase) && (a.multiline === b.multiline)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25, + "nodeLength": 21, + "src": "a.source === b.source", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52, + "nodeLength": 110, + "src": "(a.global === b.global) && (a.ignoreCase === b.ignoreCase) && (a.multiline === b.multiline)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 52, + "nodeLength": 21, + "src": "a.global === b.global", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1460": [ + null, + { + "position": 46, + "nodeLength": 63, + "src": "(a.ignoreCase === b.ignoreCase) && (a.multiline === b.multiline)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 102, + "nodeLength": 29, + "src": "a.ignoreCase === b.ignoreCase", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 137, + "nodeLength": 27, + "src": "a.multiline === b.multiline", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1464": [ + null, + { + "position": 862, + "nodeLength": 45, + "src": "aString !== Object.prototype.toString.call(b)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1468": [ + null, + { + "position": 972, + "nodeLength": 27, + "src": "aString === \"[object Date]\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1469": [ + null, + { + "position": 24, + "nodeLength": 27, + "src": "a.valueOf() === b.valueOf()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1476": [ + null, + { + "position": 1167, + "nodeLength": 53, + "src": "aString === \"[object Array]\" && a.length !== b.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1167, + "nodeLength": 28, + "src": "aString === \"[object Array]\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1199, + "nodeLength": 21, + "src": "a.length !== b.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1481": [ + null, + { + "position": 21, + "nodeLength": 20, + "src": "hasOwn.call(a, prop)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1484": [ + null, + { + "position": 60, + "nodeLength": 12, + "src": "!(prop in b)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1488": [ + null, + { + "position": 161, + "nodeLength": 28, + "src": "!deepEqual(a[prop], b[prop])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1495": [ + null, + { + "position": 21, + "nodeLength": 20, + "src": "hasOwn.call(b, prop)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1500": [ + null, + { + "position": 1788, + "nodeLength": 19, + "src": "aLength === bLength", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1504": [ + null, + { + "position": 24, + "nodeLength": 29, + "src": "func.displayName || func.name", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1510": [ + null, + { + "position": 359, + "nodeLength": 5, + "src": "!name", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1512": [ + null, + { + "position": 100, + "nodeLength": 21, + "src": "matches && matches[1]", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1519": [ + null, + { + "position": 17, + "nodeLength": 30, + "src": "this.getCall && this.callCount", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1524": [ + null, + { + "position": 122, + "nodeLength": 3, + "src": "i--", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1528": [ + null, + { + "position": 29, + "nodeLength": 24, + "src": "thisValue[prop] === this", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1535": [ + null, + { + "position": 482, + "nodeLength": 32, + "src": "this.displayName || \"sinon fake\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1539": [ + null, + { + "position": 17, + "nodeLength": 19, + "src": "obj !== Object(obj)", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1546": [ + null, + { + "position": 21, + "nodeLength": 21, + "src": "hasOwn.call(obj, key)", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1558": [ + null, + { + "position": 81, + "nodeLength": 73, + "src": "proto && !(descriptor = Object.getOwnPropertyDescriptor(proto, property))", + "evalFalse": 1, + "evalTrue": 1 + } + ], + "1566": [ + null, + { + "position": 51, + "nodeLength": 12, + "src": "custom || {}", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1570": [ + null, + { + "position": 21, + "nodeLength": 29, + "src": "defaults.hasOwnProperty(prop)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1571": [ + null, + { + "position": 36, + "nodeLength": 27, + "src": "custom.hasOwnProperty(prop)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1587": [ + null, + { + "position": 20, + "nodeLength": 149, + "src": "count === 1 && \"once\" || count === 2 && \"twice\" || count === 3 && \"thrice\" || (count || 0) + \" times\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20, + "nodeLength": 21, + "src": "count === 1 && \"once\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20, + "nodeLength": 11, + "src": "count === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1588": [ + null, + { + "position": 40, + "nodeLength": 108, + "src": "count === 2 && \"twice\" || count === 3 && \"thrice\" || (count || 0) + \" times\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 63, + "nodeLength": 22, + "src": "count === 2 && \"twice\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 64, + "nodeLength": 11, + "src": "count === 2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1589": [ + null, + { + "position": 41, + "nodeLength": 66, + "src": "count === 3 && \"thrice\" || (count || 0) + \" times\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 106, + "nodeLength": 23, + "src": "count === 3 && \"thrice\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 107, + "nodeLength": 11, + "src": "count === 3", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1590": [ + null, + { + "position": 43, + "nodeLength": 10, + "src": "count || 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1594": [ + null, + { + "position": 47, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1595": [ + null, + { + "position": 21, + "nodeLength": 56, + "src": "!spies[i - 1].calledBefore(spies[i]) || !spies[i].called", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1608": [ + null, + { + "position": 156, + "nodeLength": 27, + "src": "aCall && aCall.callId || -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 156, + "nodeLength": 21, + "src": "aCall && aCall.callId", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1609": [ + null, + { + "position": 211, + "nodeLength": 27, + "src": "bCall && bCall.callId || -1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 211, + "nodeLength": 21, + "src": "bCall && bCall.callId", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1611": [ + null, + { + "position": 264, + "nodeLength": 9, + "src": "aId < bId", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1616": [ + null, + { + "position": 17, + "nodeLength": 33, + "src": "typeof constructor !== \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1623": [ + null, + { + "position": 17, + "nodeLength": 45, + "src": "object !== null && typeof object === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17, + "nodeLength": 15, + "src": "object !== null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36, + "nodeLength": 26, + "src": "typeof object === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1625": [ + null, + { + "position": 25, + "nodeLength": 26, + "src": "isRestorable(object[prop])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1629": [ + null, + { + "position": 275, + "nodeLength": 20, + "src": "isRestorable(object)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1637": [ + null, + { + "position": 13204, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 13204, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 13237, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13255, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1638": [ + null, + { + "position": 13302, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 13302, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 13334, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13334, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1644": [ + null, + { + "position": 13472, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1649": [ + null, + { + "position": 13546, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1654": [ + null, + { + "position": 13646, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1658": [ + null, + { + "position": 13727, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 13728, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1705": [ + null, + { + "position": 21, + "nodeLength": 24, + "src": "obj.hasOwnProperty(prop)", + "evalFalse": 0, + "evalTrue": 10 + } + ], + "1709": [ + null, + { + "position": 1188, + "nodeLength": 32, + "src": "result.join(\"\") !== \"0123456789\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1724": [ + null, + { + "position": 127, + "nodeLength": 18, + "src": "i < sources.length", + "evalFalse": 10, + "evalTrue": 12 + } + ], + "1728": [ + null, + { + "position": 25, + "nodeLength": 27, + "src": "source.hasOwnProperty(prop)", + "evalFalse": 0, + "evalTrue": 155 + } + ], + "1735": [ + null, + { + "position": 457, + "nodeLength": 90, + "src": "hasDontEnumBug && source.hasOwnProperty(\"toString\") && source.toString !== target.toString", + "evalFalse": 12, + "evalTrue": 0 + }, + { + "position": 475, + "nodeLength": 72, + "src": "source.hasOwnProperty(\"toString\") && source.toString !== target.toString", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 512, + "nodeLength": 35, + "src": "source.toString !== target.toString", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1752": [ + null, + { + "position": 2884, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2884, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2917, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2935, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1753": [ + null, + { + "position": 2982, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2982, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 3014, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3014, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1755": [ + null, + { + "position": 3069, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1760": [ + null, + { + "position": 3143, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1765": [ + null, + { + "position": 3243, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1769": [ + null, + { + "position": 3324, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 3325, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1788": [ + null, + { + "position": 36, + "nodeLength": 10, + "src": "count || 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1801": [ + null, + { + "position": 637, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 637, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 670, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 688, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1802": [ + null, + { + "position": 735, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 735, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 767, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 767, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1804": [ + null, + { + "position": 822, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1809": [ + null, + { + "position": 896, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1814": [ + null, + { + "position": 996, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1818": [ + null, + { + "position": 1077, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 1078, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1836": [ + null, + { + "position": 17, + "nodeLength": 14, + "src": "value === null", + "evalFalse": 20, + "evalTrue": 0 + } + ], + "1838": [ + null, + { + "position": 90, + "nodeLength": 19, + "src": "value === undefined", + "evalFalse": 20, + "evalTrue": 0 + } + ], + "1854": [ + null, + { + "position": 610, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 610, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 643, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 661, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1855": [ + null, + { + "position": 708, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 708, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 740, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 740, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1857": [ + null, + { + "position": 795, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1862": [ + null, + { + "position": 869, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "1867": [ + null, + { + "position": 969, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1871": [ + null, + { + "position": 1050, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 1051, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "1893": [ + null, + { + "position": 63, + "nodeLength": 15, + "src": "actual !== type", + "evalFalse": 8, + "evalTrue": 0 + } + ], + "1910": [ + null, + { + "position": 17, + "nodeLength": 39, + "src": "actual === null || actual === undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 17, + "nodeLength": 15, + "src": "actual === null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36, + "nodeLength": 20, + "src": "actual === undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1914": [ + null, + { + "position": 21, + "nodeLength": 31, + "src": "expectation.hasOwnProperty(key)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1917": [ + null, + { + "position": 116, + "nodeLength": 14, + "src": "isMatcher(exp)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1918": [ + null, + { + "position": 29, + "nodeLength": 14, + "src": "!exp.test(act)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1921": [ + null, + { + "position": 280, + "nodeLength": 30, + "src": "sinon.typeOf(exp) === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1922": [ + null, + { + "position": 29, + "nodeLength": 22, + "src": "!matchObject(exp, act)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1925": [ + null, + { + "position": 468, + "nodeLength": 26, + "src": "!sinon.deepEqual(exp, act)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1938": [ + null, + { + "position": 34, + "nodeLength": 38, + "src": "typeof expectation.test === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1940": [ + null, + { + "position": 32, + "nodeLength": 33, + "src": "expectation.test(actual) === true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1947": [ + null, + { + "position": 25, + "nodeLength": 31, + "src": "expectation.hasOwnProperty(key)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1959": [ + null, + { + "position": 78, + "nodeLength": 21, + "src": "expectation == actual", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1964": [ + null, + { + "position": 25, + "nodeLength": 26, + "src": "typeof actual !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1967": [ + null, + { + "position": 142, + "nodeLength": 34, + "src": "actual.indexOf(expectation) !== -1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1973": [ + null, + { + "position": 25, + "nodeLength": 26, + "src": "typeof actual !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "1981": [ + null, + { + "position": 74, + "nodeLength": 7, + "src": "message", + "evalFalse": 0, + "evalTrue": 12 + } + ], + "1992": [ + null, + { + "position": 2279, + "nodeLength": 10, + "src": "!m.message", + "evalFalse": 12, + "evalTrue": 0 + } + ], + "1999": [ + null, + { + "position": 17, + "nodeLength": 17, + "src": "!arguments.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2001": [ + null, + { + "position": 119, + "nodeLength": 14, + "src": "!isMatcher(m2)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2007": [ + null, + { + "position": 24, + "nodeLength": 34, + "src": "m1.test(actual) || m2.test(actual)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2014": [ + null, + { + "position": 17, + "nodeLength": 17, + "src": "!arguments.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2016": [ + null, + { + "position": 119, + "nodeLength": 14, + "src": "!isMatcher(m2)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2022": [ + null, + { + "position": 24, + "nodeLength": 34, + "src": "m1.test(actual) && m2.test(actual)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2035": [ + null, + { + "position": 20, + "nodeLength": 39, + "src": "actual !== null && actual !== undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 20, + "nodeLength": 15, + "src": "actual !== null", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39, + "nodeLength": 20, + "src": "actual !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2048": [ + null, + { + "position": 24, + "nodeLength": 22, + "src": "expectation === actual", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2055": [ + null, + { + "position": 24, + "nodeLength": 29, + "src": "sinon.typeOf(actual) === type", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2069": [ + null, + { + "position": 96, + "nodeLength": 22, + "src": "arguments.length === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2071": [ + null, + { + "position": 211, + "nodeLength": 13, + "src": "!onlyProperty", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2076": [ + null, + { + "position": 25, + "nodeLength": 102, + "src": "actual === undefined || actual === null || !propertyTest(actual, property)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25, + "nodeLength": 20, + "src": "actual === undefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49, + "nodeLength": 78, + "src": "actual === null || !propertyTest(actual, property)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49, + "nodeLength": 15, + "src": "actual === null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2080": [ + null, + { + "position": 218, + "nodeLength": 56, + "src": "onlyProperty || sinon.deepEqual(value, actual[property])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2086": [ + null, + { + "position": 17, + "nodeLength": 26, + "src": "typeof actual === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2089": [ + null, + { + "position": 123, + "nodeLength": 30, + "src": "actual[property] !== undefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2109": [ + null, + { + "position": 7694, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7694, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7727, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7745, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2110": [ + null, + { + "position": 7792, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7792, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7824, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7824, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2118": [ + null, + { + "position": 8058, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2123": [ + null, + { + "position": 8132, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2128": [ + null, + { + "position": 8232, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2132": [ + null, + { + "position": 8313, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 8314, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2174": [ + null, + { + "position": 50, + "nodeLength": 65, + "src": "typeof v === \"object\" && v.toString === Object.prototype.toString", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 50, + "nodeLength": 21, + "src": "typeof v === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 75, + "nodeLength": 40, + "src": "v.toString === Object.prototype.toString", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2175": [ + null, + { + "position": 140, + "nodeLength": 26, + "src": "isObjectWithNativeToString", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2178": [ + null, + { + "position": 455, + "nodeLength": 4, + "src": "util", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2181": [ + null, + { + "position": 990, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 990, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1023, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1041, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2184": [ + null, + { + "position": 1108, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2191": [ + null, + { + "position": 1278, + "nodeLength": 8, + "src": "formatio", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2193": [ + null, + { + "position": 1358, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2208": [ + null, + { + "position": 1751, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1751, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1784, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1802, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2209": [ + null, + { + "position": 1849, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1849, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1881, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1881, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2211": [ + null, + { + "position": 1936, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2216": [ + null, + { + "position": 2010, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2221": [ + null, + { + "position": 2110, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2225": [ + null, + { + "position": 2201, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 2202, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2226": [ + null, + { + "position": 2273, + "nodeLength": 40, + "src": "typeof formatio === \"object\" && formatio", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 2274, + "nodeLength": 28, + "src": "typeof formatio === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2251": [ + null, + { + "position": 73, + "nodeLength": 11, + "src": "args.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2259": [ + null, + { + "position": 21, + "nodeLength": 47, + "src": "sinon.match && sinon.match.isMatcher(thisValue)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2262": [ + null, + { + "position": 172, + "nodeLength": 28, + "src": "this.thisValue === thisValue", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2267": [ + null, + { + "position": 63, + "nodeLength": 20, + "src": "l > this.args.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2270": [ + null, + { + "position": 171, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2271": [ + null, + { + "position": 25, + "nodeLength": 44, + "src": "!sinon.deepEqual(arguments[i], this.args[i])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2281": [ + null, + { + "position": 63, + "nodeLength": 20, + "src": "l > this.args.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2284": [ + null, + { + "position": 171, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2287": [ + null, + { + "position": 124, + "nodeLength": 54, + "src": "!sinon.match || !sinon.match(expectation).test(actual)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2295": [ + null, + { + "position": 24, + "nodeLength": 99, + "src": "arguments.length === this.args.length && this.calledWith.apply(this, arguments)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24, + "nodeLength": 37, + "src": "arguments.length === this.args.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2312": [ + null, + { + "position": 21, + "nodeLength": 47, + "src": "typeof error === \"undefined\" || !this.exception", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21, + "nodeLength": 28, + "src": "typeof error === \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2316": [ + null, + { + "position": 159, + "nodeLength": 57, + "src": "this.exception === error || this.exception.name === error", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 159, + "nodeLength": 24, + "src": "this.exception === error", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 187, + "nodeLength": 29, + "src": "this.exception.name === error", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2320": [ + null, + { + "position": 24, + "nodeLength": 60, + "src": "this.proxy.prototype && this.thisValue instanceof this.proxy", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2324": [ + null, + { + "position": 24, + "nodeLength": 26, + "src": "this.callId < other.callId", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2328": [ + null, + { + "position": 24, + "nodeLength": 26, + "src": "this.callId > other.callId", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2354": [ + null, + { + "position": 88, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2355": [ + null, + { + "position": 25, + "nodeLength": 29, + "src": "typeof args[i] === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2369": [ + null, + { + "position": 88, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2370": [ + null, + { + "position": 25, + "nodeLength": 46, + "src": "args[i] && typeof args[i][prop] === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36, + "nodeLength": 35, + "src": "typeof args[i][prop] === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2381": [ + null, + { + "position": 112, + "nodeLength": 45, + "src": "this.stack && this.stack.split(\"\\n\").slice(3)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2385": [ + null, + { + "position": 31, + "nodeLength": 10, + "src": "this.proxy", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2388": [ + null, + { + "position": 130, + "nodeLength": 10, + "src": "!this.args", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2392": [ + null, + { + "position": 250, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2398": [ + null, + { + "position": 422, + "nodeLength": 39, + "src": "typeof this.returnValue !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2402": [ + null, + { + "position": 576, + "nodeLength": 14, + "src": "this.exception", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2405": [ + null, + { + "position": 85, + "nodeLength": 22, + "src": "this.exception.message", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2409": [ + null, + { + "position": 835, + "nodeLength": 10, + "src": "this.stack", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2421": [ + null, + { + "position": 17, + "nodeLength": 22, + "src": "typeof id !== \"number\"", + "evalFalse": 6, + "evalTrue": 0 + } + ], + "2441": [ + null, + { + "position": 6780, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 6780, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 6813, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6831, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2442": [ + null, + { + "position": 6878, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 6878, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 6910, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6910, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2451": [ + null, + { + "position": 7172, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2456": [ + null, + { + "position": 7246, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2461": [ + null, + { + "position": 7346, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2465": [ + null, + { + "position": 7427, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 7428, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2491": [ + null, + { + "position": 17, + "nodeLength": 41, + "src": "!property && typeof object === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 30, + "nodeLength": 28, + "src": "typeof object === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2495": [ + null, + { + "position": 136, + "nodeLength": 20, + "src": "!object && !property", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2499": [ + null, + { + "position": 243, + "nodeLength": 5, + "src": "types", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2501": [ + null, + { + "position": 113, + "nodeLength": 16, + "src": "i < types.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2511": [ + null, + { + "position": 17, + "nodeLength": 6, + "src": "!fakes", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2515": [ + null, + { + "position": 122, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2516": [ + null, + { + "position": 21, + "nodeLength": 30, + "src": "fakes[i].matches(args, strict)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2526": [ + null, + { + "position": 132, + "nodeLength": 20, + "src": "this.callCount === 1", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2527": [ + null, + { + "position": 185, + "nodeLength": 20, + "src": "this.callCount === 2", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2528": [ + null, + { + "position": 239, + "nodeLength": 20, + "src": "this.callCount === 3", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2542": [ + null, + { + "position": 79, + "nodeLength": 11, + "src": "proxyLength", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2559": [ + null, + { + "position": 21, + "nodeLength": 13, + "src": "this.invoking", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2582": [ + null, + { + "position": 960, + "nodeLength": 10, + "src": "this.fakes", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2583": [ + null, + { + "position": 37, + "nodeLength": 21, + "src": "i < this.fakes.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2594": [ + null, + { + "position": 48, + "nodeLength": 26, + "src": "typeof func !== \"function\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2600": [ + null, + { + "position": 239, + "nodeLength": 10, + "src": "!spyLength", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2612": [ + null, + { + "position": 616, + "nodeLength": 13, + "src": "name || \"spy\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2635": [ + null, + { + "position": 68, + "nodeLength": 8, + "src": "matching", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2638": [ + null, + { + "position": 40, + "nodeLength": 17, + "src": "this.func || func", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2642": [ + null, + { + "position": 385, + "nodeLength": 59, + "src": "thisCall.calledWithNew() && typeof returnValue !== \"object\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 413, + "nodeLength": 31, + "src": "typeof returnValue !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2658": [ + null, + { + "position": 1452, + "nodeLength": 23, + "src": "exception !== undefined", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2671": [ + null, + { + "position": 21, + "nodeLength": 28, + "src": "i < 0 || i >= this.callCount", + "evalFalse": 6, + "evalTrue": 4 + }, + { + "position": 21, + "nodeLength": 5, + "src": "i < 0", + "evalFalse": 10, + "evalTrue": 0 + }, + { + "position": 30, + "nodeLength": 19, + "src": "i >= this.callCount", + "evalFalse": 6, + "evalTrue": 4 + } + ], + "2684": [ + null, + { + "position": 85, + "nodeLength": 18, + "src": "i < this.callCount", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2692": [ + null, + { + "position": 21, + "nodeLength": 12, + "src": "!this.called", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2696": [ + null, + { + "position": 110, + "nodeLength": 13, + "src": "!spyFn.called", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2700": [ + null, + { + "position": 202, + "nodeLength": 57, + "src": "this.callIds[0] < spyFn.callIds[spyFn.callIds.length - 1]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2704": [ + null, + { + "position": 21, + "nodeLength": 29, + "src": "!this.called || !spyFn.called", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2708": [ + null, + { + "position": 130, + "nodeLength": 69, + "src": "this.callIds[this.callCount - 1] > spyFn.callIds[spyFn.callCount - 1]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2714": [ + null, + { + "position": 72, + "nodeLength": 10, + "src": "this.fakes", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2717": [ + null, + { + "position": 96, + "nodeLength": 5, + "src": "match", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2734": [ + null, + { + "position": 718, + "nodeLength": 20, + "src": "i < this.args.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2735": [ + null, + { + "position": 25, + "nodeLength": 26, + "src": "fake.matches(this.args[i])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2752": [ + null, + { + "position": 73, + "nodeLength": 102, + "src": "margs.length <= args.length && sinon.deepEqual(margs, args.slice(0, margs.length))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 73, + "nodeLength": 27, + "src": "margs.length <= args.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2754": [ + null, + { + "position": 28, + "nodeLength": 39, + "src": "!strict || margs.length === args.length", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 39, + "nodeLength": 28, + "src": "margs.length === args.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2763": [ + null, + { + "position": 150, + "nodeLength": 12, + "src": "format || \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2766": [ + null, + { + "position": 88, + "nodeLength": 31, + "src": "typeof formatter === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2768": [ + null, + { + "position": 227, + "nodeLength": 31, + "src": "!isNaN(parseInt(specifyer, 10))", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2779": [ + null, + { + "position": 21, + "nodeLength": 12, + "src": "!this.called", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2780": [ + null, + { + "position": 25, + "nodeLength": 9, + "src": "notCalled", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2789": [ + null, + { + "position": 333, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2792": [ + null, + { + "position": 77, + "nodeLength": 59, + "src": "currentCall[actual || method].apply(currentCall, arguments)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 89, + "nodeLength": 16, + "src": "actual || method", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2795": [ + null, + { + "position": 68, + "nodeLength": 8, + "src": "matchAny", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2801": [ + null, + { + "position": 699, + "nodeLength": 26, + "src": "matches === this.callCount", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2862": [ + null, + { + "position": 93, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2864": [ + null, + { + "position": 111, + "nodeLength": 23, + "src": "/\\n/.test(calls[i - 1])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2870": [ + null, + { + "position": 429, + "nodeLength": 16, + "src": "calls.length > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2876": [ + null, + { + "position": 95, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2886": [ + null, + { + "position": 87, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2902": [ + null, + { + "position": 14788, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 14788, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 14821, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14839, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2903": [ + null, + { + "position": 14886, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 14886, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 14918, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14918, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2914": [ + null, + { + "position": 15243, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2919": [ + null, + { + "position": 15317, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2924": [ + null, + { + "position": 15417, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2928": [ + null, + { + "position": 15498, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 15499, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "2952": [ + null, + { + "position": 13, + "nodeLength": 69, + "src": "typeof process === \"object\" && typeof process.nextTick === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 13, + "nodeLength": 27, + "src": "typeof process === \"object\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 44, + "nodeLength": 38, + "src": "typeof process.nextTick === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2956": [ + null, + { + "position": 146, + "nodeLength": 34, + "src": "typeof setImmediate === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "2966": [ + null, + { + "position": 13, + "nodeLength": 25, + "src": "typeof error === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2967": [ + null, + { + "position": 40, + "nodeLength": 13, + "src": "message || \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2969": [ + null, + { + "position": 158, + "nodeLength": 6, + "src": "!error", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2981": [ + null, + { + "position": 58, + "nodeLength": 14, + "src": "callArgAt >= 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2987": [ + null, + { + "position": 162, + "nodeLength": 33, + "src": "callArgAt === useLeftMostCallback", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2991": [ + null, + { + "position": 255, + "nodeLength": 34, + "src": "callArgAt === useRightMostCallback", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2997": [ + null, + { + "position": 457, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "2998": [ + null, + { + "position": 17, + "nodeLength": 53, + "src": "!callArgProp && typeof argumentList[i] === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33, + "nodeLength": 37, + "src": "typeof argumentList[i] === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3002": [ + null, + { + "position": 145, + "nodeLength": 100, + "src": "callArgProp && argumentList[i] && typeof argumentList[i][callArgProp] === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 160, + "nodeLength": 85, + "src": "argumentList[i] && typeof argumentList[i][callArgProp] === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3003": [ + null, + { + "position": 34, + "nodeLength": 50, + "src": "typeof argumentList[i][callArgProp] === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3013": [ + null, + { + "position": 17, + "nodeLength": 22, + "src": "behavior.callArgAt < 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3016": [ + null, + { + "position": 47, + "nodeLength": 20, + "src": "behavior.callArgProp", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3025": [ + null, + { + "position": 486, + "nodeLength": 15, + "src": "args.length > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3036": [ + null, + { + "position": 17, + "nodeLength": 38, + "src": "typeof behavior.callArgAt === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3039": [ + null, + { + "position": 78, + "nodeLength": 26, + "src": "typeof func !== \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3043": [ + null, + { + "position": 228, + "nodeLength": 22, + "src": "behavior.callbackAsync", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3063": [ + null, + { + "position": 25, + "nodeLength": 234, + "src": "typeof this.callArgAt === \"number\" || this.exception || typeof this.returnArgAt === \"number\" || this.returnThis || this.returnValueDefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25, + "nodeLength": 34, + "src": "typeof this.callArgAt === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3064": [ + null, + { + "position": 61, + "nodeLength": 172, + "src": "this.exception || typeof this.returnArgAt === \"number\" || this.returnThis || this.returnValueDefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3065": [ + null, + { + "position": 41, + "nodeLength": 130, + "src": "typeof this.returnArgAt === \"number\" || this.returnThis || this.returnValueDefined", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 114, + "nodeLength": 36, + "src": "typeof this.returnArgAt === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3066": [ + null, + { + "position": 63, + "nodeLength": 66, + "src": "this.returnThis || this.returnValueDefined", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3073": [ + null, + { + "position": 64, + "nodeLength": 14, + "src": "this.exception", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3075": [ + null, + { + "position": 152, + "nodeLength": 36, + "src": "typeof this.returnArgAt === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3077": [ + null, + { + "position": 271, + "nodeLength": 15, + "src": "this.returnThis", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3109": [ + null, + { + "position": 21, + "nodeLength": 23, + "src": "typeof pos !== \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3123": [ + null, + { + "position": 21, + "nodeLength": 23, + "src": "typeof pos !== \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3126": [ + null, + { + "position": 159, + "nodeLength": 27, + "src": "typeof context !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3140": [ + null, + { + "position": 21, + "nodeLength": 23, + "src": "typeof pos !== \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3154": [ + null, + { + "position": 21, + "nodeLength": 23, + "src": "typeof pos !== \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3157": [ + null, + { + "position": 159, + "nodeLength": 27, + "src": "typeof context !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3191": [ + null, + { + "position": 21, + "nodeLength": 27, + "src": "typeof context !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3215": [ + null, + { + "position": 21, + "nodeLength": 27, + "src": "typeof context !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3240": [ + null, + { + "position": 21, + "nodeLength": 23, + "src": "typeof pos !== \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3267": [ + null, + { + "position": 110, + "nodeLength": 92, + "src": "proto.hasOwnProperty(method) && method.match(/^(callsArg|yields)/) && !method.match(/Async/)", + "evalFalse": 13, + "evalTrue": 9 + }, + { + "position": 142, + "nodeLength": 60, + "src": "method.match(/^(callsArg|yields)/) && !method.match(/Async/)", + "evalFalse": 13, + "evalTrue": 9 + } + ], + "3276": [ + null, + { + "position": 10836, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 10836, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 10869, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10887, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3277": [ + null, + { + "position": 10934, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 10934, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 10966, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 10966, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3285": [ + null, + { + "position": 11200, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3290": [ + null, + { + "position": 11274, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3295": [ + null, + { + "position": 11374, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "3299": [ + null, + { + "position": 11455, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 11456, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "3311": [ + null, + { + "position": 47, + "nodeLength": 48, + "src": "typeof Object.getOwnPropertyNames !== \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3324": [ + null, + { + "position": 21, + "nodeLength": 8, + "src": "!seen[k]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3326": [ + null, + { + "position": 70, + "nodeLength": 65, + "src": "typeof Object.getOwnPropertyDescriptor(obj, k).get === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3333": [ + null, + { + "position": 987, + "nodeLength": 5, + "src": "proto", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3361": [ + null, + { + "position": 2279, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2279, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2312, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2330, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3362": [ + null, + { + "position": 2377, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2377, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2409, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2409, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3364": [ + null, + { + "position": 2464, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3369": [ + null, + { + "position": 2538, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3374": [ + null, + { + "position": 2638, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "3378": [ + null, + { + "position": 2719, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 2720, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "3400": [ + null, + { + "position": 17, + "nodeLength": 64, + "src": "!!func && typeof func !== \"function\" && typeof func !== \"object\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 27, + "nodeLength": 54, + "src": "typeof func !== \"function\" && typeof func !== \"object\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 27, + "nodeLength": 26, + "src": "typeof func !== \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 57, + "nodeLength": 24, + "src": "typeof func !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3406": [ + null, + { + "position": 240, + "nodeLength": 4, + "src": "func", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "3407": [ + null, + { + "position": 21, + "nodeLength": 26, + "src": "typeof func === \"function\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "3408": [ + null, + { + "position": 31, + "nodeLength": 29, + "src": "sinon.spy && sinon.spy.create", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "3411": [ + null, + { + "position": 61, + "nodeLength": 29, + "src": "sinon.spy && sinon.spy.create", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3413": [ + null, + { + "position": 104, + "nodeLength": 16, + "src": "i < types.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3420": [ + null, + { + "position": 57, + "nodeLength": 68, + "src": "typeof object === \"object\" && typeof object[property] === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 57, + "nodeLength": 26, + "src": "typeof object === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 87, + "nodeLength": 38, + "src": "typeof object[property] === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3426": [ + null, + { + "position": 1095, + "nodeLength": 42, + "src": "!object && typeof property === \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1106, + "nodeLength": 31, + "src": "typeof property === \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3430": [ + null, + { + "position": 1216, + "nodeLength": 61, + "src": "typeof property === \"undefined\" && typeof object === \"object\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1216, + "nodeLength": 31, + "src": "typeof property === \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1251, + "nodeLength": 26, + "src": "typeof object === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3431": [ + null, + { + "position": 28, + "nodeLength": 12, + "src": "object || {}", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3435": [ + null, + { + "position": 28, + "nodeLength": 180, + "src": "propOwner !== Object.prototype && prop !== \"constructor\" && typeof sinon.getPropertyDescriptor(propOwner, prop).value === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 213, + "nodeLength": 30, + "src": "propOwner !== Object.prototype", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3436": [ + null, + { + "position": 57, + "nodeLength": 122, + "src": "prop !== \"constructor\" && typeof sinon.getPropertyDescriptor(propOwner, prop).value === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 272, + "nodeLength": 22, + "src": "prop !== \"constructor\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3437": [ + null, + { + "position": 49, + "nodeLength": 72, + "src": "typeof sinon.getPropertyDescriptor(propOwner, prop).value === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3452": [ + null, + { + "position": 21, + "nodeLength": 62, + "src": "stubInstance.parent && getCurrentBehavior(stubInstance.parent)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3456": [ + null, + { + "position": 20, + "nodeLength": 143, + "src": "stubInstance.defaultBehavior || getParentBehaviour(stubInstance) || sinon.behavior.create(stubInstance)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3457": [ + null, + { + "position": 51, + "nodeLength": 91, + "src": "getParentBehaviour(stubInstance) || sinon.behavior.create(stubInstance)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3463": [ + null, + { + "position": 99, + "nodeLength": 32, + "src": "behavior && behavior.isPresent()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3501": [ + null, + { + "position": 252, + "nodeLength": 10, + "src": "this.fakes", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3502": [ + null, + { + "position": 33, + "nodeLength": 21, + "src": "i < this.fakes.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3509": [ + null, + { + "position": 21, + "nodeLength": 22, + "src": "!this.behaviors[index]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3531": [ + null, + { + "position": 40, + "nodeLength": 51, + "src": "this.defaultBehavior || sinon.behavior.create(this)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3538": [ + null, + { + "position": 17, + "nodeLength": 205, + "src": "sinon.behavior.hasOwnProperty(method) && !proto.hasOwnProperty(method) && method !== \"create\" && method !== \"withArgs\" && method !== \"invoke\"", + "evalFalse": 7, + "evalTrue": 24 + } + ], + "3539": [ + null, + { + "position": 56, + "nodeLength": 148, + "src": "!proto.hasOwnProperty(method) && method !== \"create\" && method !== \"withArgs\" && method !== \"invoke\"", + "evalFalse": 7, + "evalTrue": 24 + } + ], + "3540": [ + null, + { + "position": 48, + "nodeLength": 99, + "src": "method !== \"create\" && method !== \"withArgs\" && method !== \"invoke\"", + "evalFalse": 2, + "evalTrue": 24 + }, + { + "position": 126, + "nodeLength": 19, + "src": "method !== \"create\"", + "evalFalse": 0, + "evalTrue": 26 + } + ], + "3541": [ + null, + { + "position": 38, + "nodeLength": 60, + "src": "method !== \"withArgs\" && method !== \"invoke\"", + "evalFalse": 2, + "evalTrue": 24 + }, + { + "position": 166, + "nodeLength": 21, + "src": "method !== \"withArgs\"", + "evalFalse": 1, + "evalTrue": 25 + } + ], + "3542": [ + null, + { + "position": 40, + "nodeLength": 19, + "src": "method !== \"invoke\"", + "evalFalse": 1, + "evalTrue": 24 + } + ], + "3553": [ + null, + { + "position": 5454, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 5454, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 5487, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5505, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3554": [ + null, + { + "position": 5552, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 5552, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 5584, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 5584, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3564": [ + null, + { + "position": 5873, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3569": [ + null, + { + "position": 5947, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "3574": [ + null, + { + "position": 6047, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "3578": [ + null, + { + "position": 6128, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 6129, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "3610": [ + null, + { + "position": 178, + "nodeLength": 7, + "src": "!object", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3618": [ + null, + { + "position": 17, + "nodeLength": 11, + "src": "!collection", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3622": [ + null, + { + "position": 122, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3628": [ + null, + { + "position": 17, + "nodeLength": 46, + "src": "compareLength && (arr1.length !== arr2.length)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 35, + "nodeLength": 27, + "src": "arr1.length !== arr2.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3632": [ + null, + { + "position": 157, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3633": [ + null, + { + "position": 21, + "nodeLength": 34, + "src": "!sinon.deepEqual(arr1[i], arr2[i])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3642": [ + null, + { + "position": 21, + "nodeLength": 7, + "src": "!object", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3654": [ + null, + { + "position": 21, + "nodeLength": 7, + "src": "!method", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3658": [ + null, + { + "position": 131, + "nodeLength": 18, + "src": "!this.expectations", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3663": [ + null, + { + "position": 275, + "nodeLength": 26, + "src": "!this.expectations[method]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3684": [ + null, + { + "position": 25, + "nodeLength": 43, + "src": "typeof object[proxy].restore === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3691": [ + null, + { + "position": 36, + "nodeLength": 23, + "src": "this.expectations || {}", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3697": [ + null, + { + "position": 29, + "nodeLength": 18, + "src": "!expectation.met()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3707": [ + null, + { + "position": 600, + "nodeLength": 19, + "src": "messages.length > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3709": [ + null, + { + "position": 728, + "nodeLength": 14, + "src": "met.length > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3717": [ + null, + { + "position": 36, + "nodeLength": 46, + "src": "this.expectations && this.expectations[method]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3719": [ + null, + { + "position": 206, + "nodeLength": 10, + "src": "args || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3722": [ + null, + { + "position": 281, + "nodeLength": 23, + "src": "i < expectations.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3723": [ + null, + { + "position": 40, + "nodeLength": 39, + "src": "expectations[i].expectedArguments || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3724": [ + null, + { + "position": 105, + "nodeLength": 76, + "src": "arrayEquals(expectedArgs, currentArgs, expectations[i].expectsExactArgCount)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3729": [ + null, + { + "position": 645, + "nodeLength": 39, + "src": "i < expectationsWithMatchingArgs.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3730": [ + null, + { + "position": 25, + "nodeLength": 125, + "src": "!expectationsWithMatchingArgs[i].met() && expectationsWithMatchingArgs[i].allowsCall(thisValue, args)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3739": [ + null, + { + "position": 1076, + "nodeLength": 39, + "src": "i < expectationsWithMatchingArgs.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3740": [ + null, + { + "position": 25, + "nodeLength": 59, + "src": "expectationsWithMatchingArgs[i].allowsCall(thisValue, args)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3741": [ + null, + { + "position": 37, + "nodeLength": 44, + "src": "available || expectationsWithMatchingArgs[i]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3747": [ + null, + { + "position": 1426, + "nodeLength": 28, + "src": "available && exhausted === 0", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1439, + "nodeLength": 15, + "src": "exhausted === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3751": [ + null, + { + "position": 1566, + "nodeLength": 23, + "src": "i < expectations.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3768": [ + null, + { + "position": 17, + "nodeLength": 15, + "src": "callCount === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3779": [ + null, + { + "position": 106, + "nodeLength": 50, + "src": "typeof min === \"number\" && typeof max === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 106, + "nodeLength": 23, + "src": "typeof min === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 133, + "nodeLength": 23, + "src": "typeof max === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3782": [ + null, + { + "position": 60, + "nodeLength": 11, + "src": "min !== max", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3789": [ + null, + { + "position": 388, + "nodeLength": 23, + "src": "typeof min === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3797": [ + null, + { + "position": 31, + "nodeLength": 40, + "src": "typeof expectation.minCalls === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3798": [ + null, + { + "position": 92, + "nodeLength": 61, + "src": "!hasMinLimit || expectation.callCount >= expectation.minCalls", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 108, + "nodeLength": 45, + "src": "expectation.callCount >= expectation.minCalls", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3802": [ + null, + { + "position": 17, + "nodeLength": 40, + "src": "typeof expectation.maxCalls !== \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3806": [ + null, + { + "position": 125, + "nodeLength": 46, + "src": "expectation.callCount === expectation.maxCalls", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3810": [ + null, + { + "position": 29, + "nodeLength": 41, + "src": "match && match.isMatcher(possibleMatcher)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3812": [ + null, + { + "position": 92, + "nodeLength": 46, + "src": "isMatcher && possibleMatcher.test(arg) || true", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 92, + "nodeLength": 38, + "src": "isMatcher && possibleMatcher.test(arg)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3834": [ + null, + { + "position": 21, + "nodeLength": 23, + "src": "typeof num !== \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3838": [ + null, + { + "position": 159, + "nodeLength": 15, + "src": "!this.limitsSet", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3849": [ + null, + { + "position": 21, + "nodeLength": 23, + "src": "typeof num !== \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3853": [ + null, + { + "position": 159, + "nodeLength": 15, + "src": "!this.limitsSet", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3880": [ + null, + { + "position": 21, + "nodeLength": 23, + "src": "typeof num !== \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3889": [ + null, + { + "position": 24, + "nodeLength": 38, + "src": "!this.failed && receivedMinCalls(this)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3893": [ + null, + { + "position": 21, + "nodeLength": 22, + "src": "receivedMaxCalls(this)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3898": [ + null, + { + "position": 227, + "nodeLength": 57, + "src": "\"expectedThis\" in this && this.expectedThis !== thisValue", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 253, + "nodeLength": 31, + "src": "this.expectedThis !== thisValue", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3903": [ + null, + { + "position": 487, + "nodeLength": 30, + "src": "!(\"expectedArguments\" in this)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3907": [ + null, + { + "position": 588, + "nodeLength": 5, + "src": "!args", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3912": [ + null, + { + "position": 794, + "nodeLength": 43, + "src": "args.length < this.expectedArguments.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3917": [ + null, + { + "position": 1072, + "nodeLength": 94, + "src": "this.expectsExactArgCount && args.length !== this.expectedArguments.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3918": [ + null, + { + "position": 48, + "nodeLength": 45, + "src": "args.length !== this.expectedArguments.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3923": [ + null, + { + "position": 1449, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3925": [ + null, + { + "position": 26, + "nodeLength": 50, + "src": "!verifyMatcher(this.expectedArguments[i], args[i])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3930": [ + null, + { + "position": 324, + "nodeLength": 52, + "src": "!sinon.deepEqual(this.expectedArguments[i], args[i])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3938": [ + null, + { + "position": 21, + "nodeLength": 36, + "src": "this.met() && receivedMaxCalls(this)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3942": [ + null, + { + "position": 134, + "nodeLength": 57, + "src": "\"expectedThis\" in this && this.expectedThis !== thisValue", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 160, + "nodeLength": 31, + "src": "this.expectedThis !== thisValue", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3946": [ + null, + { + "position": 268, + "nodeLength": 30, + "src": "!(\"expectedArguments\" in this)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3950": [ + null, + { + "position": 377, + "nodeLength": 10, + "src": "args || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3952": [ + null, + { + "position": 410, + "nodeLength": 43, + "src": "args.length < this.expectedArguments.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3956": [ + null, + { + "position": 530, + "nodeLength": 94, + "src": "this.expectsExactArgCount && args.length !== this.expectedArguments.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3957": [ + null, + { + "position": 48, + "nodeLength": 45, + "src": "args.length !== this.expectedArguments.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3961": [ + null, + { + "position": 748, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3962": [ + null, + { + "position": 25, + "nodeLength": 50, + "src": "!verifyMatcher(this.expectedArguments[i], args[i])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3966": [ + null, + { + "position": 164, + "nodeLength": 52, + "src": "!sinon.deepEqual(this.expectedArguments[i], args[i])", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3991": [ + null, + { + "position": 29, + "nodeLength": 28, + "src": "this.expectedArguments || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3993": [ + null, + { + "position": 89, + "nodeLength": 26, + "src": "!this.expectsExactArgCount", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "3998": [ + null, + { + "position": 28, + "nodeLength": 43, + "src": "this.method || \"anonymous mock expectation\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4005": [ + null, + { + "position": 515, + "nodeLength": 10, + "src": "this.met()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4014": [ + null, + { + "position": 21, + "nodeLength": 11, + "src": "!this.met()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4039": [ + null, + { + "position": 14567, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 14567, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 14600, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14618, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4040": [ + null, + { + "position": 14665, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 14665, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 14697, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 14697, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4055": [ + null, + { + "position": 15106, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4060": [ + null, + { + "position": 15180, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4065": [ + null, + { + "position": 15280, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "4069": [ + null, + { + "position": 15361, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 15362, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "4092": [ + null, + { + "position": 13, + "nodeLength": 21, + "src": "!fakeCollection.fakes", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4102": [ + null, + { + "position": 90, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4103": [ + null, + { + "position": 17, + "nodeLength": 38, + "src": "typeof fakes[i][method] === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4112": [ + null, + { + "position": 81, + "nodeLength": 16, + "src": "i < fakes.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4143": [ + null, + { + "position": 227, + "nodeLength": 9, + "src": "exception", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4158": [ + null, + { + "position": 21, + "nodeLength": 8, + "src": "property", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4161": [ + null, + { + "position": 79, + "nodeLength": 30, + "src": "typeof original !== \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4162": [ + null, + { + "position": 29, + "nodeLength": 38, + "src": "!hasOwnProperty.call(object, property)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4175": [ + null, + { + "position": 667, + "nodeLength": 51, + "src": "!property && !!object && typeof object === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 680, + "nodeLength": 38, + "src": "!!object && typeof object === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 692, + "nodeLength": 26, + "src": "typeof object === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4179": [ + null, + { + "position": 29, + "nodeLength": 38, + "src": "typeof stubbedObj[prop] === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4217": [ + null, + { + "position": 3638, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 3638, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 3671, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3689, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4218": [ + null, + { + "position": 3736, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 3736, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 3768, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 3768, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4228": [ + null, + { + "position": 4053, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4233": [ + null, + { + "position": 4127, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4238": [ + null, + { + "position": 4227, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "4242": [ + null, + { + "position": 4308, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 4309, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "4266": [ + null, + { + "position": 45, + "nodeLength": 28, + "src": "typeof lolex !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "4272": [ + null, + { + "position": 104, + "nodeLength": 30, + "src": "typeof methods[0] === \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4278": [ + null, + { + "position": 274, + "nodeLength": 8, + "src": "now || 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4292": [ + null, + { + "position": 104, + "nodeLength": 35, + "src": "typeof setImmediate !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4293": [ + null, + { + "position": 198, + "nodeLength": 37, + "src": "typeof clearImmediate !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4300": [ + null, + { + "position": 1095, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1095, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1128, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1146, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4301": [ + null, + { + "position": 1193, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1193, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1225, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1225, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4309": [ + null, + { + "position": 1451, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4311": [ + null, + { + "position": 1510, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4330": [ + null, + { + "position": 135915, + "nodeLength": 28, + "src": "typeof sinon === \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4360": [ + null, + { + "position": 83, + "nodeLength": 43, + "src": "typeof progressEventRaw.loaded === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4361": [ + null, + { + "position": 186, + "nodeLength": 42, + "src": "typeof progressEventRaw.total === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4371": [ + null, + { + "position": 83, + "nodeLength": 25, + "src": "customData.detail || null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4380": [ + null, + { + "position": 39, + "nodeLength": 25, + "src": "this.eventListeners || {}", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4381": [ + null, + { + "position": 111, + "nodeLength": 32, + "src": "this.eventListeners[event] || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4386": [ + null, + { + "position": 33, + "nodeLength": 55, + "src": "this.eventListeners && this.eventListeners[event] || []", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 33, + "nodeLength": 49, + "src": "this.eventListeners && this.eventListeners[event]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4388": [ + null, + { + "position": 145, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4389": [ + null, + { + "position": 25, + "nodeLength": 25, + "src": "listeners[i] === listener", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4397": [ + null, + { + "position": 72, + "nodeLength": 54, + "src": "this.eventListeners && this.eventListeners[type] || []", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 72, + "nodeLength": 48, + "src": "this.eventListeners && this.eventListeners[type]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4399": [ + null, + { + "position": 161, + "nodeLength": 20, + "src": "i < listeners.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4400": [ + null, + { + "position": 25, + "nodeLength": 34, + "src": "typeof listeners[i] === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4412": [ + null, + { + "position": 2854, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2854, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2887, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2905, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4413": [ + null, + { + "position": 2952, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2952, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2984, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2984, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4420": [ + null, + { + "position": 3150, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4422": [ + null, + { + "position": 3209, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4461": [ + null, + { + "position": 270, + "nodeLength": 9, + "src": "err.stack", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4465": [ + null, + { + "position": 352, + "nodeLength": 31, + "src": "logError.useImmediateExceptions", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4493": [ + null, + { + "position": 1575, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1575, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1608, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1626, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4494": [ + null, + { + "position": 1673, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1673, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1705, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1705, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4496": [ + null, + { + "position": 1760, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4501": [ + null, + { + "position": 1834, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4506": [ + null, + { + "position": 1934, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "4510": [ + null, + { + "position": 2015, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 2016, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "4530": [ + null, + { + "position": 17, + "nodeLength": 29, + "src": "typeof window !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4533": [ + null, + { + "position": 141992, + "nodeLength": 28, + "src": "typeof sinon === \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4534": [ + null, + { + "position": 9, + "nodeLength": 27, + "src": "typeof this === \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4546": [ + null, + { + "position": 139, + "nodeLength": 47, + "src": "typeof xdr.GlobalXDomainRequest !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4547": [ + null, + { + "position": 209, + "nodeLength": 15, + "src": "xdr.supportsXDR", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4559": [ + null, + { + "position": 212, + "nodeLength": 49, + "src": "typeof FakeXDomainRequest.onCreate === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4565": [ + null, + { + "position": 17, + "nodeLength": 42, + "src": "x.readyState !== FakeXDomainRequest.OPENED", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4569": [ + null, + { + "position": 148, + "nodeLength": 10, + "src": "x.sendFlag", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4575": [ + null, + { + "position": 17, + "nodeLength": 42, + "src": "x.readyState === FakeXDomainRequest.UNSENT", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4578": [ + null, + { + "position": 146, + "nodeLength": 40, + "src": "x.readyState === FakeXDomainRequest.DONE", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4584": [ + null, + { + "position": 17, + "nodeLength": 24, + "src": "typeof body !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4612": [ + null, + { + "position": 56, + "nodeLength": 13, + "src": "this.sendFlag", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4618": [ + null, + { + "position": 53, + "nodeLength": 14, + "src": "this.isTimeout", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4620": [ + null, + { + "position": 152, + "nodeLength": 58, + "src": "this.errorFlag || (this.status < 200 || this.status > 299)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 171, + "nodeLength": 38, + "src": "this.status < 200 || this.status > 299", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 171, + "nodeLength": 17, + "src": "this.status < 200", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 192, + "nodeLength": 17, + "src": "this.status > 299", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4629": [ + null, + { + "position": 997, + "nodeLength": 9, + "src": "eventName", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4630": [ + null, + { + "position": 25, + "nodeLength": 37, + "src": "typeof this[eventName] === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4643": [ + null, + { + "position": 57, + "nodeLength": 34, + "src": "!/^(get|head)$/i.test(this.method)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4652": [ + null, + { + "position": 406, + "nodeLength": 33, + "src": "typeof this.onSend === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4662": [ + null, + { + "position": 140, + "nodeLength": 66, + "src": "this.readyState > sinon.FakeXDomainRequest.UNSENT && this.sendFlag", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 140, + "nodeLength": 49, + "src": "this.readyState > sinon.FakeXDomainRequest.UNSENT", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4672": [ + null, + { + "position": 121, + "nodeLength": 20, + "src": "this.chunkSize || 10", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4680": [ + null, + { + "position": 223, + "nodeLength": 19, + "src": "index < body.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4689": [ + null, + { + "position": 259, + "nodeLength": 26, + "src": "typeof status === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4690": [ + null, + { + "position": 339, + "nodeLength": 10, + "src": "body || \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4711": [ + null, + { + "position": 21, + "nodeLength": 15, + "src": "xdr.supportsXDR", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4717": [ + null, + { + "position": 207, + "nodeLength": 21, + "src": "keepOnCreate !== true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4721": [ + null, + { + "position": 423, + "nodeLength": 15, + "src": "xdr.supportsXDR", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4730": [ + null, + { + "position": 6628, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 6628, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 6661, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6679, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4731": [ + null, + { + "position": 6726, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 6726, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 6758, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 6758, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4742": [ + null, + { + "position": 7064, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4744": [ + null, + { + "position": 7123, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4749": [ + null, + { + "position": 7287, + "nodeLength": 29, + "src": "typeof global !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4768": [ + null, + { + "position": 27, + "nodeLength": 49, + "src": "typeof globalScope.XMLHttpRequest !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "4769": [ + null, + { + "position": 90, + "nodeLength": 11, + "src": "supportsXHR", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "4773": [ + null, + { + "position": 193, + "nodeLength": 48, + "src": "typeof globalScope.ActiveXObject !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4774": [ + null, + { + "position": 255, + "nodeLength": 15, + "src": "supportsActiveX", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4783": [ + null, + { + "position": 512, + "nodeLength": 36, + "src": "typeof ProgressEvent !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "4784": [ + null, + { + "position": 580, + "nodeLength": 34, + "src": "typeof CustomEvent !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4785": [ + null, + { + "position": 643, + "nodeLength": 31, + "src": "typeof FormData !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "4786": [ + null, + { + "position": 706, + "nodeLength": 34, + "src": "typeof ArrayBuffer !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "4797": [ + null, + { + "position": 1104, + "nodeLength": 51, + "src": "typeof sinonXhr.GlobalActiveXObject !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "4798": [ + null, + { + "position": 1184, + "nodeLength": 52, + "src": "typeof sinonXhr.GlobalXMLHttpRequest !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "4800": [ + null, + { + "position": 1315, + "nodeLength": 82, + "src": "sinonXhr.supportsXHR && \"withCredentials\" in (new sinonXhr.GlobalXMLHttpRequest())", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "4842": [ + null, + { + "position": 25, + "nodeLength": 32, + "src": "this.eventListeners[event] || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4844": [ + null, + { + "position": 106, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4845": [ + null, + { + "position": 17, + "nodeLength": 25, + "src": "listeners[i] === listener", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4852": [ + null, + { + "position": 25, + "nodeLength": 37, + "src": "this.eventListeners[event.type] || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4854": [ + null, + { + "position": 100, + "nodeLength": 32, + "src": "(listener = listeners[i]) != null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4872": [ + null, + { + "position": 292, + "nodeLength": 21, + "src": "sinonXhr.supportsCORS", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4883": [ + null, + { + "position": 76, + "nodeLength": 42, + "src": "listener && typeof listener === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 88, + "nodeLength": 30, + "src": "typeof listener === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4889": [ + null, + { + "position": 832, + "nodeLength": 6, + "src": "i >= 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4893": [ + null, + { + "position": 911, + "nodeLength": 49, + "src": "typeof FakeXMLHttpRequest.onCreate === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4899": [ + null, + { + "position": 13, + "nodeLength": 44, + "src": "xhr.readyState !== FakeXMLHttpRequest.OPENED", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4903": [ + null, + { + "position": 134, + "nodeLength": 12, + "src": "xhr.sendFlag", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4912": [ + null, + { + "position": 17, + "nodeLength": 26, + "src": "h.toLowerCase() === header", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4923": [ + null, + { + "position": 13, + "nodeLength": 11, + "src": "!collection", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4927": [ + null, + { + "position": 106, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4932": [ + null, + { + "position": 29, + "nodeLength": 25, + "src": "index < collection.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4933": [ + null, + { + "position": 17, + "nodeLength": 36, + "src": "callback(collection[index]) === true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4980": [ + null, + { + "position": 25, + "nodeLength": 32, + "src": "!IE6Re.test(navigator.userAgent)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4989": [ + null, + { + "position": 66, + "nodeLength": 53, + "src": "xhr.readyState >= FakeXMLHttpRequest.HEADERS_RECEIVED", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4992": [ + null, + { + "position": 206, + "nodeLength": 44, + "src": "xhr.readyState >= FakeXMLHttpRequest.LOADING", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4995": [ + null, + { + "position": 341, + "nodeLength": 42, + "src": "xhr.readyState === FakeXMLHttpRequest.DONE", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "4998": [ + null, + { + "position": 461, + "nodeLength": 26, + "src": "fakeXhr.onreadystatechange", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5003": [ + null, + { + "position": 1512, + "nodeLength": 20, + "src": "xhr.addEventListener", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5005": [ + null, + { + "position": 21, + "nodeLength": 44, + "src": "fakeXhr.eventListeners.hasOwnProperty(event)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5023": [ + null, + { + "position": 13, + "nodeLength": 44, + "src": "xhr.readyState !== FakeXMLHttpRequest.OPENED", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5029": [ + null, + { + "position": 13, + "nodeLength": 42, + "src": "xhr.readyState === FakeXMLHttpRequest.DONE", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5035": [ + null, + { + "position": 13, + "nodeLength": 67, + "src": "xhr.async && xhr.readyState !== FakeXMLHttpRequest.HEADERS_RECEIVED", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 26, + "nodeLength": 54, + "src": "xhr.readyState !== FakeXMLHttpRequest.HEADERS_RECEIVED", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5041": [ + null, + { + "position": 13, + "nodeLength": 24, + "src": "typeof body !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5052": [ + null, + { + "position": 119, + "nodeLength": 15, + "src": "i < body.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5054": [ + null, + { + "position": 64, + "nodeLength": 15, + "src": "charCode >= 256", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5064": [ + null, + { + "position": 16, + "nodeLength": 74, + "src": "!contentType || /(text\\/xml)|(application\\/xml)|(\\+xml)/.test(contentType)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5068": [ + null, + { + "position": 13, + "nodeLength": 46, + "src": "responseType === \"\" || responseType === \"text\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13, + "nodeLength": 19, + "src": "responseType === \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 36, + "nodeLength": 23, + "src": "responseType === \"text\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5070": [ + null, + { + "position": 108, + "nodeLength": 53, + "src": "supportsArrayBuffer && responseType === \"arraybuffer\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 131, + "nodeLength": 30, + "src": "responseType === \"arraybuffer\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5072": [ + null, + { + "position": 232, + "nodeLength": 23, + "src": "responseType === \"json\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5079": [ + null, + { + "position": 457, + "nodeLength": 39, + "src": "supportsBlob && responseType === \"blob\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 473, + "nodeLength": 23, + "src": "responseType === \"blob\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5081": [ + null, + { + "position": 51, + "nodeLength": 11, + "src": "contentType", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5085": [ + null, + { + "position": 719, + "nodeLength": 27, + "src": "responseType === \"document\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5086": [ + null, + { + "position": 17, + "nodeLength": 29, + "src": "isXmlContentType(contentType)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5095": [ + null, + { + "position": 13, + "nodeLength": 54, + "src": "xhr.responseType === \"\" || xhr.responseType === \"text\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 13, + "nodeLength": 23, + "src": "xhr.responseType === \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 40, + "nodeLength": 27, + "src": "xhr.responseType === \"text\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5105": [ + null, + { + "position": 62, + "nodeLength": 11, + "src": "text !== \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5107": [ + null, + { + "position": 21, + "nodeLength": 32, + "src": "typeof DOMParser !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5177": [ + null, + { + "position": 100, + "nodeLength": 26, + "src": "typeof async === \"boolean\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5184": [ + null, + { + "position": 366, + "nodeLength": 38, + "src": "FakeXMLHttpRequest.useFilters === true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5189": [ + null, + { + "position": 240, + "nodeLength": 6, + "src": "defake", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5202": [ + null, + { + "position": 202, + "nodeLength": 45, + "src": "typeof this.onreadystatechange === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5210": [ + null, + { + "position": 526, + "nodeLength": 43, + "src": "this.readyState === FakeXMLHttpRequest.DONE", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5213": [ + null, + { + "position": 31, + "nodeLength": 18, + "src": "this.progress || 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5214": [ + null, + { + "position": 80, + "nodeLength": 18, + "src": "this.progress || 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5217": [ + null, + { + "position": 239, + "nodeLength": 17, + "src": "this.status === 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5218": [ + null, + { + "position": 33, + "nodeLength": 12, + "src": "this.aborted", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5224": [ + null, + { + "position": 462, + "nodeLength": 16, + "src": "supportsProgress", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5241": [ + null, + { + "position": 57, + "nodeLength": 54, + "src": "unsafeHeaders[header] || /^(Sec-|Proxy-)/.test(header)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5245": [ + null, + { + "position": 242, + "nodeLength": 27, + "src": "this.requestHeaders[header]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5258": [ + null, + { + "position": 25, + "nodeLength": 30, + "src": "headers.hasOwnProperty(header)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5263": [ + null, + { + "position": 325, + "nodeLength": 10, + "src": "this.async", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5274": [ + null, + { + "position": 57, + "nodeLength": 34, + "src": "!/^(get|head)$/i.test(this.method)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5276": [ + null, + { + "position": 111, + "nodeLength": 32, + "src": "this.requestHeaders[contentType]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5279": [ + null, + { + "position": 348, + "nodeLength": 47, + "src": "supportsFormData && !(data instanceof FormData)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5291": [ + null, + { + "position": 877, + "nodeLength": 33, + "src": "typeof this.onSend === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5305": [ + null, + { + "position": 220, + "nodeLength": 60, + "src": "this.readyState > FakeXMLHttpRequest.UNSENT && this.sendFlag", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 220, + "nodeLength": 43, + "src": "this.readyState > FakeXMLHttpRequest.UNSENT", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5323": [ + null, + { + "position": 21, + "nodeLength": 53, + "src": "this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5327": [ + null, + { + "position": 150, + "nodeLength": 30, + "src": "/^Set-Cookie2?$/i.test(header)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5333": [ + null, + { + "position": 326, + "nodeLength": 36, + "src": "this.responseHeaders[header] || null", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5337": [ + null, + { + "position": 21, + "nodeLength": 53, + "src": "this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5344": [ + null, + { + "position": 25, + "nodeLength": 102, + "src": "this.responseHeaders.hasOwnProperty(header) && !/^Set-Cookie2?$/i.test(header)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5359": [ + null, + { + "position": 245, + "nodeLength": 56, + "src": "this.responseType === \"\" || this.responseType === \"text\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 245, + "nodeLength": 24, + "src": "this.responseType === \"\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 273, + "nodeLength": 28, + "src": "this.responseType === \"text\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5361": [ + null, + { + "position": 360, + "nodeLength": 10, + "src": "this.async", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5362": [ + null, + { + "position": 37, + "nodeLength": 20, + "src": "this.chunkSize || 10", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5368": [ + null, + { + "position": 105, + "nodeLength": 14, + "src": "isTextResponse", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5372": [ + null, + { + "position": 332, + "nodeLength": 19, + "src": "index < body.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5376": [ + null, + { + "position": 973, + "nodeLength": 14, + "src": "isTextResponse", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5380": [ + null, + { + "position": 1085, + "nodeLength": 32, + "src": "this.responseType === \"document\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5382": [ + null, + { + "position": 1203, + "nodeLength": 57, + "src": "this.responseType === \"\" && isXmlContentType(contentType)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1203, + "nodeLength": 24, + "src": "this.responseType === \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5390": [ + null, + { + "position": 31, + "nodeLength": 26, + "src": "typeof status === \"number\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5392": [ + null, + { + "position": 193, + "nodeLength": 13, + "src": "headers || {}", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5393": [ + null, + { + "position": 246, + "nodeLength": 10, + "src": "body || \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5397": [ + null, + { + "position": 21, + "nodeLength": 16, + "src": "supportsProgress", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5403": [ + null, + { + "position": 21, + "nodeLength": 16, + "src": "supportsProgress", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5409": [ + null, + { + "position": 21, + "nodeLength": 19, + "src": "supportsCustomEvent", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5425": [ + null, + { + "position": 21, + "nodeLength": 20, + "src": "sinonXhr.supportsXHR", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5429": [ + null, + { + "position": 159, + "nodeLength": 24, + "src": "sinonXhr.supportsActiveX", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5435": [ + null, + { + "position": 351, + "nodeLength": 21, + "src": "keepOnCreate !== true", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5439": [ + null, + { + "position": 555, + "nodeLength": 20, + "src": "sinonXhr.supportsXHR", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5443": [ + null, + { + "position": 670, + "nodeLength": 24, + "src": "sinonXhr.supportsActiveX", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5445": [ + null, + { + "position": 25, + "nodeLength": 64, + "src": "objId === \"Microsoft.XMLHTTP\" || /^Msxml2\\.XMLHTTP/i.test(objId)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25, + "nodeLength": 29, + "src": "objId === \"Microsoft.XMLHTTP\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5460": [ + null, + { + "position": 24353, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 24353, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 24386, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24404, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5461": [ + null, + { + "position": 24451, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 24451, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 24483, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 24483, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5472": [ + null, + { + "position": 24789, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "5477": [ + null, + { + "position": 24863, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "5482": [ + null, + { + "position": 24963, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "5486": [ + null, + { + "position": 25052, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 25053, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "5487": [ + null, + { + "position": 25124, + "nodeLength": 29, + "src": "typeof global !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "5514": [ + null, + { + "position": 46, + "nodeLength": 60, + "src": "Object.prototype.toString.call(handler) !== \"[object Array]\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5518": [ + null, + { + "position": 176, + "nodeLength": 31, + "src": "typeof response[2] !== \"string\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5526": [ + null, + { + "position": 479, + "nodeLength": 29, + "src": "typeof window !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "5531": [ + null, + { + "position": 64, + "nodeLength": 57, + "src": "!rmeth || rmeth.toLowerCase() === reqMethod.toLowerCase()", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74, + "nodeLength": 47, + "src": "rmeth.toLowerCase() === reqMethod.toLowerCase()", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5533": [ + null, + { + "position": 178, + "nodeLength": 78, + "src": "!url || url === reqUrl || (typeof url.test === \"function\" && url.test(reqUrl))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 186, + "nodeLength": 70, + "src": "url === reqUrl || (typeof url.test === \"function\" && url.test(reqUrl))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 186, + "nodeLength": 14, + "src": "url === reqUrl", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 205, + "nodeLength": 50, + "src": "typeof url.test === \"function\" && url.test(reqUrl)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 205, + "nodeLength": 30, + "src": "typeof url.test === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5535": [ + null, + { + "position": 274, + "nodeLength": 23, + "src": "matchMethod && matchUrl", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5541": [ + null, + { + "position": 52, + "nodeLength": 61, + "src": "!/^https?:\\/\\//.test(requestUrl) || rCurrLoc.test(requestUrl)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5545": [ + null, + { + "position": 199, + "nodeLength": 59, + "src": "matchOne(response, this.getHTTPMethod(request), requestUrl)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5546": [ + null, + { + "position": 17, + "nodeLength": 39, + "src": "typeof response.response === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5548": [ + null, + { + "position": 84, + "nodeLength": 35, + "src": "ru && typeof ru.exec === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 90, + "nodeLength": 29, + "src": "typeof ru.exec === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5563": [ + null, + { + "position": 112, + "nodeLength": 23, + "src": "!sinon.xhr.supportsCORS", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5585": [ + null, + { + "position": 288, + "nodeLength": 12, + "src": "config || {}", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5587": [ + null, + { + "position": 25, + "nodeLength": 67, + "src": "whitelist.hasOwnProperty(setting) && config.hasOwnProperty(setting)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5599": [ + null, + { + "position": 74, + "nodeLength": 25, + "src": "server.respondImmediately", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5601": [ + null, + { + "position": 177, + "nodeLength": 40, + "src": "server.autoRespond && !server.responding", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5605": [ + null, + { + "position": 152, + "nodeLength": 29, + "src": "server.autoRespondAfter || 10", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5613": [ + null, + { + "position": 21, + "nodeLength": 52, + "src": "this.fakeHTTPMethods && /post/i.test(request.method)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5614": [ + null, + { + "position": 36, + "nodeLength": 25, + "src": "request.requestBody || \"\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5615": [ + null, + { + "position": 118, + "nodeLength": 7, + "src": "matches", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5622": [ + null, + { + "position": 21, + "nodeLength": 9, + "src": "xhr.async", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5623": [ + null, + { + "position": 25, + "nodeLength": 11, + "src": "!this.queue", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5643": [ + null, + { + "position": 21, + "nodeLength": 54, + "src": "arguments.length === 1 && typeof method !== \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 21, + "nodeLength": 22, + "src": "arguments.length === 1", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 47, + "nodeLength": 28, + "src": "typeof method !== \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5648": [ + null, + { + "position": 205, + "nodeLength": 15, + "src": "!this.responses", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5652": [ + null, + { + "position": 304, + "nodeLength": 22, + "src": "arguments.length === 1", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5657": [ + null, + { + "position": 445, + "nodeLength": 22, + "src": "arguments.length === 2", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5666": [ + null, + { + "position": 97, + "nodeLength": 26, + "src": "typeof body === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5671": [ + null, + { + "position": 21, + "nodeLength": 20, + "src": "arguments.length > 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5675": [ + null, + { + "position": 153, + "nodeLength": 16, + "src": "this.queue || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5678": [ + null, + { + "position": 266, + "nodeLength": 19, + "src": "i < requests.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5685": [ + null, + { + "position": 25, + "nodeLength": 15, + "src": "request.aborted", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5689": [ + null, + { + "position": 134, + "nodeLength": 30, + "src": "this.response || [404, {}, \"\"]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5691": [ + null, + { + "position": 191, + "nodeLength": 14, + "src": "this.responses", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5692": [ + null, + { + "position": 72, + "nodeLength": 6, + "src": "i >= 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5693": [ + null, + { + "position": 33, + "nodeLength": 44, + "src": "match.call(this, this.responses[i], request)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5700": [ + null, + { + "position": 588, + "nodeLength": 24, + "src": "request.readyState !== 4", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5711": [ + null, + { + "position": 24, + "nodeLength": 63, + "src": "this.xhr.restore && this.xhr.restore.apply(this.xhr, arguments)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5716": [ + null, + { + "position": 7015, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7015, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7048, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7066, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5717": [ + null, + { + "position": 7113, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7113, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 7145, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 7145, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5728": [ + null, + { + "position": 7477, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "5730": [ + null, + { + "position": 7536, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "5764": [ + null, + { + "position": 17, + "nodeLength": 9, + "src": "xhr.async", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5765": [ + null, + { + "position": 21, + "nodeLength": 36, + "src": "typeof setTimeout.clock === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5772": [ + null, + { + "position": 276, + "nodeLength": 20, + "src": "!this.longestTimeout", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5778": [ + null, + { + "position": 67, + "nodeLength": 26, + "src": "server.longestTimeout || 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5784": [ + null, + { + "position": 67, + "nodeLength": 26, + "src": "server.longestTimeout || 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5797": [ + null, + { + "position": 95, + "nodeLength": 10, + "src": "this.clock", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5798": [ + null, + { + "position": 33, + "nodeLength": 24, + "src": "this.longestTimeout || 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5801": [ + null, + { + "position": 122, + "nodeLength": 15, + "src": "this.resetClock", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5811": [ + null, + { + "position": 17, + "nodeLength": 10, + "src": "this.clock", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5819": [ + null, + { + "position": 2092, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2092, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2125, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2143, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5820": [ + null, + { + "position": 2190, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2190, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2222, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2222, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5829": [ + null, + { + "position": 2456, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "5831": [ + null, + { + "position": 2515, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "5860": [ + null, + { + "position": 17, + "nodeLength": 6, + "src": "!value", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5864": [ + null, + { + "position": 82, + "nodeLength": 48, + "src": "config.injectInto && !(key in config.injectInto)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5875": [ + null, + { + "position": 73, + "nodeLength": 20, + "src": "config.useFakeServer", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5876": [ + null, + { + "position": 21, + "nodeLength": 40, + "src": "typeof config.useFakeServer === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5883": [ + null, + { + "position": 320, + "nodeLength": 20, + "src": "config.useFakeTimers", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5884": [ + null, + { + "position": 21, + "nodeLength": 40, + "src": "typeof config.useFakeTimers === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5904": [ + null, + { + "position": 29, + "nodeLength": 40, + "src": "this.serverPrototype || sinon.fakeServer", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5906": [ + null, + { + "position": 92, + "nodeLength": 23, + "src": "!proto || !proto.create", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5917": [ + null, + { + "position": 79, + "nodeLength": 10, + "src": "this.clock", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5921": [ + null, + { + "position": 176, + "nodeLength": 11, + "src": "this.server", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5932": [ + null, + { + "position": 21, + "nodeLength": 16, + "src": "arguments.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5941": [ + null, + { + "position": 21, + "nodeLength": 17, + "src": "this.injectedKeys", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5942": [ + null, + { + "position": 67, + "nodeLength": 5, + "src": "i < j", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5950": [ + null, + { + "position": 21, + "nodeLength": 7, + "src": "!config", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5955": [ + null, + { + "position": 202, + "nodeLength": 18, + "src": "sandbox.args || []", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5962": [ + null, + { + "position": 445, + "nodeLength": 17, + "src": "config.properties", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5963": [ + null, + { + "position": 67, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5965": [ + null, + { + "position": 86, + "nodeLength": 46, + "src": "exposed[prop] || prop === \"sandbox\" && sandbox", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 103, + "nodeLength": 29, + "src": "prop === \"sandbox\" && sandbox", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 103, + "nodeLength": 18, + "src": "prop === \"sandbox\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5983": [ + null, + { + "position": 4014, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 4014, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 4047, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4065, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5984": [ + null, + { + "position": 4112, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 4112, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 4144, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 4144, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "5995": [ + null, + { + "position": 4500, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "6000": [ + null, + { + "position": 4574, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "6005": [ + null, + { + "position": 4674, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "6009": [ + null, + { + "position": 4755, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 4756, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "6032": [ + null, + { + "position": 58, + "nodeLength": 19, + "src": "type !== \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6038": [ + null, + { + "position": 97, + "nodeLength": 50, + "src": "config.injectIntoThis && this || config.injectInto", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 97, + "nodeLength": 29, + "src": "config.injectIntoThis && this", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6041": [ + null, + { + "position": 289, + "nodeLength": 36, + "src": "args.length && args[args.length - 1]", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6044": [ + null, + { + "position": 387, + "nodeLength": 29, + "src": "typeof oldDone === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6046": [ + null, + { + "position": 29, + "nodeLength": 3, + "src": "res", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6061": [ + null, + { + "position": 971, + "nodeLength": 32, + "src": "typeof exception !== \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6064": [ + null, + { + "position": 1111, + "nodeLength": 29, + "src": "typeof oldDone !== \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6071": [ + null, + { + "position": 1505, + "nodeLength": 15, + "src": "callback.length", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6092": [ + null, + { + "position": 2210, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2210, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2243, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2261, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6093": [ + null, + { + "position": 2308, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2308, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2340, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2340, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6101": [ + null, + { + "position": 2573, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "6103": [ + null, + { + "position": 2632, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "6105": [ + null, + { + "position": 2717, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "6108": [ + null, + { + "position": 2791, + "nodeLength": 42, + "src": "typeof sinon === \"object\" && sinon || null", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 2792, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 2793, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "6126": [ + null, + { + "position": 17, + "nodeLength": 5, + "src": "setUp", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6138": [ + null, + { + "position": 287, + "nodeLength": 8, + "src": "tearDown", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6142": [ + null, + { + "position": 379, + "nodeLength": 9, + "src": "exception", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6152": [ + null, + { + "position": 17, + "nodeLength": 35, + "src": "!tests || typeof tests !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 27, + "nodeLength": 25, + "src": "typeof tests !== \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6156": [ + null, + { + "position": 183, + "nodeLength": 16, + "src": "prefix || \"test\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6166": [ + null, + { + "position": 21, + "nodeLength": 70, + "src": "tests.hasOwnProperty(testName) && !/^(setUp|tearDown)$/.test(testName)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6169": [ + null, + { + "position": 74, + "nodeLength": 56, + "src": "typeof property === \"function\" && rPrefix.test(testName)", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 74, + "nodeLength": 30, + "src": "typeof property === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6172": [ + null, + { + "position": 73, + "nodeLength": 17, + "src": "setUp || tearDown", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6190": [ + null, + { + "position": 1877, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1877, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1910, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 1928, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6191": [ + null, + { + "position": 1975, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 1975, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 2007, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 2007, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6199": [ + null, + { + "position": 2237, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "6204": [ + null, + { + "position": 2311, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "6209": [ + null, + { + "position": 2411, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "6213": [ + null, + { + "position": 2492, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 2493, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "6240": [ + null, + { + "position": 76, + "nodeLength": 5, + "src": "i < l", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6243": [ + null, + { + "position": 61, + "nodeLength": 7, + "src": "!method", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6247": [ + null, + { + "position": 165, + "nodeLength": 41, + "src": "method.proxy && method.proxy.isSinonProxy", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6250": [ + null, + { + "position": 25, + "nodeLength": 28, + "src": "typeof method !== \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6254": [ + null, + { + "position": 172, + "nodeLength": 36, + "src": "typeof method.getCall !== \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6269": [ + null, + { + "position": 44, + "nodeLength": 26, + "src": "assertionArgs.length !== 0", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6281": [ + null, + { + "position": 22, + "nodeLength": 16, + "src": "object || global", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6282": [ + null, + { + "position": 69, + "nodeLength": 26, + "src": "object.fail || assert.fail", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6287": [ + null, + { + "position": 17, + "nodeLength": 22, + "src": "arguments.length === 2", + "evalFalse": 1, + "evalTrue": 18 + } + ], + "6300": [ + null, + { + "position": 201, + "nodeLength": 28, + "src": "typeof method === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6303": [ + null, + { + "position": 30, + "nodeLength": 34, + "src": "typeof fake[method] === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6307": [ + null, + { + "position": 480, + "nodeLength": 6, + "src": "failed", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6308": [ + null, + { + "position": 42, + "nodeLength": 32, + "src": "fake.printf || fake.proxy.printf", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6316": [ + null, + { + "position": 20, + "nodeLength": 29, + "src": "!prefix || /^fail/.test(prop)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6325": [ + null, + { + "position": 78, + "nodeLength": 42, + "src": "this.failException || assert.failException", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6337": [ + null, + { + "position": 143, + "nodeLength": 31, + "src": "!sinon.calledInOrder(arguments)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6342": [ + null, + { + "position": 203, + "nodeLength": 1, + "src": "i", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6343": [ + null, + { + "position": 33, + "nodeLength": 18, + "src": "!calls[--i].called", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6362": [ + null, + { + "position": 60, + "nodeLength": 26, + "src": "method.callCount !== count", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6372": [ + null, + { + "position": 21, + "nodeLength": 7, + "src": "!target", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6376": [ + null, + { + "position": 147, + "nodeLength": 13, + "src": "options || {}", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6377": [ + null, + { + "position": 191, + "nodeLength": 55, + "src": "typeof o.prefix === \"undefined\" && \"assert\" || o.prefix", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 191, + "nodeLength": 43, + "src": "typeof o.prefix === \"undefined\" && \"assert\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 191, + "nodeLength": 31, + "src": "typeof o.prefix === \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6378": [ + null, + { + "position": 282, + "nodeLength": 55, + "src": "typeof o.includeFail === \"undefined\" || !!o.includeFail", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 282, + "nodeLength": 36, + "src": "typeof o.includeFail === \"undefined\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6381": [ + null, + { + "position": 25, + "nodeLength": 63, + "src": "method !== \"expose\" && (includeFail || !/^(fail)/.test(method))", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 25, + "nodeLength": 19, + "src": "method !== \"expose\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 49, + "nodeLength": 38, + "src": "includeFail || !/^(fail)/.test(method)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6391": [ + null, + { + "position": 77, + "nodeLength": 20, + "src": "matcher.test(actual)", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6434": [ + null, + { + "position": 8079, + "nodeLength": 80, + "src": "typeof module !== \"undefined\" && module.exports && typeof require === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 8079, + "nodeLength": 29, + "src": "typeof module !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 8112, + "nodeLength": 47, + "src": "module.exports && typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8130, + "nodeLength": 29, + "src": "typeof require === \"function\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6435": [ + null, + { + "position": 8177, + "nodeLength": 76, + "src": "typeof define === \"function\" && typeof define.amd === \"object\" && define.amd", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 8177, + "nodeLength": 28, + "src": "typeof define === \"function\"", + "evalFalse": 1, + "evalTrue": 0 + }, + { + "position": 8209, + "nodeLength": 44, + "src": "typeof define.amd === \"object\" && define.amd", + "evalFalse": 0, + "evalTrue": 0 + }, + { + "position": 8209, + "nodeLength": 30, + "src": "typeof define.amd === \"object\"", + "evalFalse": 0, + "evalTrue": 0 + } + ], + "6444": [ + null, + { + "position": 8471, + "nodeLength": 5, + "src": "isAMD", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "6449": [ + null, + { + "position": 8545, + "nodeLength": 6, + "src": "isNode", + "evalFalse": 1, + "evalTrue": 0 + } + ], + "6454": [ + null, + { + "position": 8645, + "nodeLength": 11, + "src": "sinonGlobal", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "6458": [ + null, + { + "position": 8734, + "nodeLength": 34, + "src": "typeof sinon === \"object\" && sinon", + "evalFalse": 0, + "evalTrue": 1 + }, + { + "position": 8735, + "nodeLength": 25, + "src": "typeof sinon === \"object\"", + "evalFalse": 0, + "evalTrue": 1 + } + ], + "6459": [ + null, + { + "position": 8806, + "nodeLength": 29, + "src": "typeof global !== \"undefined\"", + "evalFalse": 1, + "evalTrue": 0 + } + ] + } + }, + "/tests/onlinebooking.js": { + "lineData": [ + null, + null, + null, + 1, + null, + null, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + null, + null, + null, + 1, + null, + null, + 1, + 1, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + 1, + 1, + 1, + null, + null, + 1, + 1, + 1, + 1, + null, + null, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + null, + null, + 1, + 1, + 1, + 1, + null, + 1, + null, + 1, + 1, + 1, + null, + null, + null, + 1, + 1, + 1 + ], + "functionData": [ + 1, + 1, + 1, + 1, + 1 + ], + "branchData": {} + } +} \ No newline at end of file diff --git a/apps/worker/services/report/languages/tests/unit/test_bullseye.py b/apps/worker/services/report/languages/tests/unit/test_bullseye.py new file mode 100644 index 0000000000..3edb522737 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_bullseye.py @@ -0,0 +1,172 @@ +import time + +import pytest +from lxml import etree + +from helpers.exceptions import ReportExpiredException +from services.report.languages import bullseye +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +xml = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + +expected_result = { + "archive": { + "calc/CalcCore.cpp": [ + (11, 1, "m", [[0, 1, None, None, None]], None, None), + (40, "1/2", "m", [[0, "1/2", None, None, None]], None, None), + (47, 1, "m", [[0, 1, None, None, None]], None, None), + (49, 1, "b", [[0, 1, None, None, None]], None, None), + (57, 1, "m", [[0, 1, None, None, None]], None, None), + (60, 0, "b", [[0, 0, None, None, None]], None, None), + (63, 0, "b", [[0, 0, None, None, None]], None, None), + (66, 1, "b", [[0, 1, None, None, None]], None, None), + (69, 1, "b", [[0, 1, None, None, None]], None, None), + (70, 1, "b", [[0, 1, None, None, None]], None, None), + ], + "calc/CalcCore.h": [(15, 1, "m", [[0, 1, None, None, None]], None, None)], + "calc/Calculator.cpp": [ + (100, 1, "m", [[0, 1, None, None, None]], None, None), + (122, 1, "b", [[0, 1, None, None, None]], None, None), + (125, 1, None, [[0, 1, None, None, None]], None, None), + (126, 0, None, [[0, 0, None, None, None]], None, None), + ], + }, + "report": { + "files": { + "calc/CalcCore.cpp": [ + 0, + [0, 10, 7, 2, 1, "70.00000", 6, 4, 0, 0, 0, 0, 0], + None, + None, + ], + "calc/CalcCore.h": [ + 1, + [0, 1, 1, 0, 0, "100", 0, 1, 0, 0, 0, 0, 0], + None, + None, + ], + "calc/Calculator.cpp": [ + 2, + [0, 4, 3, 1, 0, "75.00000", 1, 1, 0, 0, 0, 0, 0], + None, + None, + ], + }, + "sessions": {}, + }, + "totals": { + "f": 3, + "n": 15, + "h": 11, + "m": 3, + "p": 1, + "c": "73.33333", + "b": 7, + "d": 6, + "M": 0, + "s": 0, + "C": 0, + "N": 0, + "diff": None, + }, +} + + +class TestBullseye(BaseTestCase): + def test_report(self): + def fixes(path): + if path == "ignore": + return None + assert path in ( + "calc/CalcCore.cpp", + "calc/CalcCore.h", + "calc/Calculator.cpp", + ) + return path + + date = time.strftime("%Y-%m-%d_%H:%M:%S", (time.gmtime(time.time()))) + report_builder_session = create_report_builder_session(path_fixer=fixes) + bullseye.from_xml( + etree.fromstring((xml % date).encode(), None), report_builder_session + ) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + assert processed_report == expected_result + + @pytest.mark.parametrize( + "date", + [ + (time.strftime("%Y-%m-%d_%H:00:00", (time.gmtime(time.time() - 172800)))), + "2020-10-28_17:55:47", + ], + ) + def test_expired(self, date): + report_builder_session = create_report_builder_session() + with pytest.raises(ReportExpiredException, match="Bullseye report expired"): + bullseye.from_xml( + etree.fromstring((xml % date).encode(), None), report_builder_session + ) + + def test_matches_content(self): + processor = bullseye.BullseyeProcessor() + content = etree.fromstring( + (xml % time.strftime("%Y-%m-%d_%H:%M:%S")).encode(), None + ) + first_line = xml.split("\n", 1)[0] + name = "coverage.xml" + assert processor.matches_content(content, first_line, name) diff --git a/apps/worker/services/report/languages/tests/unit/test_clover.py b/apps/worker/services/report/languages/tests/unit/test_clover.py new file mode 100644 index 0000000000..e06d1128eb --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_clover.py @@ -0,0 +1,147 @@ +import datetime +import xml.etree.cElementTree as etree +from time import time + +import pytest + +from helpers.exceptions import ReportExpiredException +from services.report.languages import clover +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +xml = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + + +class TestCloverProcessor(BaseTestCase): + def test_report(self): + def fixes(path): + if path == "ignore": + return None + assert path in ("source.php", "file.php", "nolines") + return path + + report_builder_session = create_report_builder_session(path_fixer=fixes) + clover.from_xml(etree.fromstring(xml % int(time())), report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report == { + "archive": { + "file.php": [(11, 1, None, [[0, 1, None, None, None]], None, None)], + "source.php": [ + (1, "1/2", "b", [[0, "1/2", None, None, None]], None, None), + (2, "1/2", "b", [[0, "1/2", None, None, None]], None, None), + (3, "2/2", "b", [[0, "2/2", None, None, None]], None, None), + (4, "0/2", "b", [[0, "0/2", None, None, None]], None, None), + (5, 1, "m", [[0, 1, None, None, 0]], None, 0), + (6, 2969, "m", [[0, 2969, None, None, 9]], None, 9), + (8, 0, None, [[0, 0, None, None, None]], None, None), + (11, 1, None, [[0, 1, None, None, None]], None, None), + (21, 0, None, [[0, 0, None, None, None]], None, None), + (22, 0, None, [[0, 0, None, None, None]], None, None), + (23, 0, None, [[0, 0, None, None, None]], None, None), + ], + }, + "report": { + "files": { + "file.php": [ + 1, + [0, 1, 1, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "source.php": [ + 0, + [0, 11, 4, 5, 2, "36.36364", 4, 2, 0, 0, 9, 0, 0], + None, + None, + ], + }, + "sessions": {}, + }, + "totals": { + "C": 9, + "M": 0, + "N": 0, + "b": 4, + "c": "41.66667", + "d": 2, + "diff": None, + "f": 2, + "h": 5, + "m": 5, + "n": 12, + "p": 2, + "s": 0, + }, + } + + @pytest.mark.parametrize( + "date", + [ + (datetime.datetime.now() - datetime.timedelta(seconds=172800)) + .replace(minute=0, second=0) + .strftime("%s"), + "01-01-2014", + ], + ) + def test_expired(self, date): + report_builder_session = create_report_builder_session() + with pytest.raises(ReportExpiredException, match="Clover report expired"): + clover.from_xml(etree.fromstring(xml % date), report_builder_session) diff --git a/apps/worker/services/report/languages/tests/unit/test_cobertura.py b/apps/worker/services/report/languages/tests/unit/test_cobertura.py new file mode 100644 index 0000000000..ab2d704068 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_cobertura.py @@ -0,0 +1,554 @@ +import datetime +import os +import xml.etree.cElementTree as etree +from time import time + +import pytest + +from helpers.exceptions import ReportExpiredException +from services.path_fixer import PathFixer +from services.report.languages import cobertura +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +xml = """ + +<%scoverage branch-rate="0.07143" line-rate="0.5506" timestamp="%s" version="3.7.1"> + %s + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + + +class TestCobertura(BaseTestCase): + def test_report(self): + def fixes(path, *, bases_to_try): + if path == "ignore": + return None + assert path in ("source", "empty", "file", "nolines") + return path + + report_builder_session = create_report_builder_session( + path_fixer=fixes, + current_yaml={"codecov": {"max_report_age": None}}, + ) + cobertura.from_xml( + etree.fromstring(xml % ("", int(time()), "", "")), report_builder_session + ) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + expected_result = { + "archive": { + "file": [ + (1, 0, "m", [[0, 0, None, None, None]], None, None), + (2, 1, "b", [[0, 1, None, None, None]], None, None), + (3, 1, None, [[0, 1, None, None, None]], None, None), + ], + "source": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + (2, "0/2", "b", [[0, "0/2", ["exit"], None, None]], None, None), + (3, "1/2", "b", [[0, "1/2", ["30"], None, None]], None, None), + (4, "2/2", "b", [[0, "2/2", None, None, None]], None, None), + ( + 5, + "2/4", + "b", + [[0, "2/4", ["0:jump", "1:jump"], None, None]], + None, + None, + ), + ( + 6, + "2/4", + "b", + [[0, "2/4", ["0:jump", "1:jump"], None, None]], + None, + None, + ), + ( + 7, + "0/2", + "b", + [[0, "0/2", ["loop", "exit"], None, None]], + None, + None, + ), + (8, 1, None, [[0, 1, None, None, None]], None, None), + (9, "1/2", "b", [[0, "1/2", None, None, None]], None, None), + ], + }, + "report": { + "files": { + "file": [ + 1, + [0, 3, 2, 1, 0, "66.66667", 1, 1, 0, 0, 0, 0, 0], + None, + None, + ], + "source": [ + 0, + [0, 9, 3, 2, 4, "33.33333", 7, 0, 0, 0, 0, 0, 0], + None, + None, + ], + }, + "sessions": {}, + }, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 8, + "c": "41.66667", + "d": 1, + "diff": None, + "f": 2, + "h": 5, + "m": 3, + "n": 12, + "p": 4, + "s": 0, + }, + } + assert processed_report["archive"] == expected_result["archive"] + assert processed_report["report"] == expected_result["report"] + assert processed_report["totals"] == expected_result["totals"] + assert processed_report == expected_result + + def test_report_missing_conditions(self): + def fixes(path, *, bases_to_try): + if path == "ignore": + return None + assert path in ("source", "empty", "file", "nolines") + return path + + report_builder_session = create_report_builder_session( + path_fixer=fixes, + current_yaml={ + "codecov": { + "max_report_age": None, + }, + "parsers": {"cobertura": {"handle_missing_conditions": True}}, + }, + ) + cobertura.from_xml( + etree.fromstring(xml % ("", int(time()), "", "")), report_builder_session + ) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + expected_result = { + "archive": { + "file": [ + (1, 0, "m", [[0, 0, None, None, None]], None, None), + (2, 1, "b", [[0, 1, None, None, None]], None, None), + (3, 1, None, [[0, 1, None, None, None]], None, None), + ], + "source": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + (2, "0/2", "b", [[0, "0/2", ["exit"], None, None]], None, None), + (3, "1/2", "b", [[0, "1/2", ["30"], None, None]], None, None), + (4, "2/2", "b", [[0, "2/2", None, None, None]], None, None), + ( + 5, + "2/4", + "b", + [[0, "2/4", ["0:jump", "1:jump"], None, None]], + None, + None, + ), + ( + 6, + "2/4", + "b", + [[0, "2/4", ["0:jump", "1:jump"], None, None]], + None, + None, + ), + ( + 7, + "0/2", + "b", + [[0, "0/2", ["loop", "exit"], None, None]], + None, + None, + ), + (8, 1, None, [[0, 1, None, None, None]], None, None), + (9, "1/2", "b", [[0, "1/2", ["0"], None, None]], None, None), + ], + }, + "report": { + "files": { + "file": [ + 1, + [0, 3, 2, 1, 0, "66.66667", 1, 1, 0, 0, 0, 0, 0], + None, + None, + ], + "source": [ + 0, + [0, 9, 3, 2, 4, "33.33333", 7, 0, 0, 0, 0, 0, 0], + None, + None, + ], + }, + "sessions": {}, + }, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 8, + "c": "41.66667", + "d": 1, + "diff": None, + "f": 2, + "h": 5, + "m": 3, + "n": 12, + "p": 4, + "s": 0, + }, + } + assert processed_report["archive"] == expected_result["archive"] + assert processed_report["report"] == expected_result["report"] + assert processed_report["totals"] == expected_result["totals"] + assert processed_report == expected_result + + def test_report_missing_conditions_and_partials_as_hits(self): + def fixes(path, *, bases_to_try): + if path == "ignore": + return None + assert path in ("source", "empty", "file", "nolines") + return path + + report_builder_session = create_report_builder_session( + path_fixer=fixes, + current_yaml={ + "codecov": { + "max_report_age": None, + }, + "parsers": { + "cobertura": { + "handle_missing_conditions": True, + "partials_as_hits": True, + } + }, + }, + ) + cobertura.from_xml( + etree.fromstring(xml % ("", int(time()), "", "")), report_builder_session + ) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + expected_result = { + "archive": { + "file": [ + (1, 0, "m", [[0, 0, None, None, None]], None, None), + (2, 1, "b", [[0, 1, None, None, None]], None, None), + (3, 1, None, [[0, 1, None, None, None]], None, None), + ], + "source": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + (2, "0/2", "b", [[0, "0/2", ["exit"], None, None]], None, None), + (3, 1, None, [[0, 1, None, None, None]], None, None), + (4, 1, None, [[0, 1, None, None, None]], None, None), + (5, 1, None, [[0, 1, None, None, None]], None, None), + (6, 1, None, [[0, 1, None, None, None]], None, None), + ( + 7, + "0/2", + "b", + [[0, "0/2", ["loop", "exit"], None, None]], + None, + None, + ), + (8, 1, None, [[0, 1, None, None, None]], None, None), + (9, 1, None, [[0, 1, None, None, None]], None, None), + ], + }, + "report": { + "files": { + "file": [ + 1, + [0, 3, 2, 1, 0, "66.66667", 1, 1, 0, 0, 0, 0, 0], + None, + None, + ], + "source": [ + 0, + [0, 9, 7, 2, 0, "77.77778", 2, 0, 0, 0, 0, 0, 0], + None, + None, + ], + }, + "sessions": {}, + }, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 3, + "c": "75.00000", + "d": 1, + "diff": None, + "f": 2, + "h": 9, + "m": 3, + "n": 12, + "p": 0, + "s": 0, + }, + } + assert processed_report["archive"] == expected_result["archive"] + assert processed_report["report"] == expected_result["report"] + assert processed_report["totals"] == expected_result["totals"] + assert processed_report == expected_result + + def test_timestamp_zero_passes(self): + # Some reports have timestamp as a string zero, check we can handle that + timestring = "0" + report_builder_session = create_report_builder_session() + cobertura.from_xml( + etree.fromstring(xml % ("", timestring, "", "")), report_builder_session + ) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + assert len(processed_report["archive"]["file"]) == 3 + assert processed_report["totals"]["c"] == "41.66667" + + @pytest.mark.parametrize( + "date", + [ + (datetime.datetime.now() - datetime.timedelta(seconds=172800)) + .replace(minute=0, second=0) + .strftime("%s"), + "01-01-2014", + ], + ) + def test_expired(self, date): + report_builder_session = create_report_builder_session() + with pytest.raises(ReportExpiredException, match="Cobertura report expired"): + cobertura.from_xml( + etree.fromstring(xml % ("", date, "", "")), report_builder_session + ) + + report_builder_session = create_report_builder_session() + with pytest.raises(ReportExpiredException, match="Cobertura report expired"): + cobertura.from_xml( + etree.fromstring(xml % ("s", date, "", "s")), report_builder_session + ) + + def test_matches_content(self): + processor = cobertura.CoberturaProcessor() + first_line = xml.split("\n", 1)[0] + name = "coverage.xml" + + content = etree.fromstring(xml % ("", int(time()), "", "")) + assert processor.matches_content(content, first_line, name) + + content = etree.fromstring(xml % ("s", int(time()), "", "s")) + assert processor.matches_content(content, first_line, name) + + def test_not_matches_content(self): + processor = cobertura.CoberturaProcessor() + content = etree.fromstring( + """ + + + 258 + 0 + 258 + 0 + 1 + 1 + 0 + + """ + ) + first_line = xml.split("\n", 1)[0] + name = "coverage.xml" + assert not processor.matches_content(content, first_line, name) + + def test_use_source_for_filename_if_one_path_source(self): + sources = """ + + /user/repo + + """ + report_builder_session = create_report_builder_session( + path_fixer=lambda path, bases_to_try: [ + os.path.join(b, path) for b in bases_to_try + ][0], + current_yaml={"codecov": {"max_report_age": None}}, + ) + cobertura.from_xml( + etree.fromstring(xml % ("", int(time()), sources, "")), + report_builder_session, + ) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + # prepend the source + assert "/user/repo/source" in processed_report["report"]["files"] + assert "/user/repo/file" in processed_report["report"]["files"] + + def test_use_source_for_filename_if_one_bad_source(self): + sources = """ + + not a path + + """ + report_builder_session = create_report_builder_session( + current_yaml={"codecov": {"max_report_age": None}}, + ) + cobertura.from_xml( + etree.fromstring(xml % ("", int(time()), sources, "")), + report_builder_session, + ) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + # doesnt use the source + assert "source" in processed_report["report"]["files"] + assert "file" in processed_report["report"]["files"] + + def test_use_source_for_filename_if_multiple_sources_only_second_works(self): + sources = """ + + /here + /there + + """ + path_fixer = PathFixer([], [], ["/there/source", "/there/file"]) + report_builder_session = create_report_builder_session( + path_fixer=path_fixer.get_relative_path_aware_pathfixer("/somewhere"), + current_yaml={"codecov": {"max_report_age": None}}, + ) + cobertura.from_xml( + etree.fromstring(xml % ("", int(time()), sources, "")), + report_builder_session, + ) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + # doesnt use the source as we dont know which one + assert "/there/source" in processed_report["report"]["files"] + assert "/there/file" in processed_report["report"]["files"] + + def test_use_source_for_filename_if_multiple_sources_works_without_base(self): + sources = """ + + /here + /there + + """ + path_fixer = PathFixer([], [], ["source", "file", "/here/source"]) + report_builder_session = create_report_builder_session( + path_fixer=path_fixer.get_relative_path_aware_pathfixer("/somewhere"), + current_yaml={"codecov": {"max_report_age": None}}, + ) + cobertura.from_xml( + etree.fromstring(xml % ("", int(time()), sources, "")), + report_builder_session, + ) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + # doesnt use the source as we dont know which one + assert "source" in processed_report["report"]["files"] + assert "file" in processed_report["report"]["files"] + + def test_use_source_for_filename_if_multiple_sources_first_and_second_works(self): + sources = """ + + /here + /there + + """ + path_fixer = PathFixer( + [], [], ["/here/source", "/there/source", "/here/file", "/there/file"] + ) + report_builder_session = create_report_builder_session( + path_fixer=path_fixer.get_relative_path_aware_pathfixer("/somewhere"), + current_yaml={"codecov": {"max_report_age": None}}, + ) + cobertura.from_xml( + etree.fromstring(xml % ("", int(time()), sources, "")), + report_builder_session, + ) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + # doesnt use the source as we dont know which one + assert "/here/source" in processed_report["report"]["files"] + assert "/here/file" in processed_report["report"]["files"] + + +def test_empty_filename(): + xml = """ + + + + + + + + +""" + report_builder_session = create_report_builder_session() + cobertura.from_xml( + etree.fromstring(xml), + report_builder_session, + ) + report = report_builder_session.output_report() + + assert not report.is_empty() diff --git a/apps/worker/services/report/languages/tests/unit/test_coveralls.py b/apps/worker/services/report/languages/tests/unit/test_coveralls.py new file mode 100644 index 0000000000..2c6f6264dd --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_coveralls.py @@ -0,0 +1,85 @@ +from services.report.languages import coveralls +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +json = { + "source_files": [ + {"name": "file", "coverage": [0, 1, None]}, + {"name": "ignore", "coverage": [None, 1, 0]}, + ] +} + +nested_json = { + "source_files": [ + { + "name": "foobar", + "coverage": "[null,null,1,null,1]", + } + ] +} + + +class TestCoveralls(BaseTestCase): + def test_detect(self): + processor = coveralls.CoverallsProcessor() + assert processor.matches_content({"source_files": ""}, "", "") + assert not processor.matches_content({"coverage": ""}, "", "") + + def test_report(self): + def fixes(path): + assert path in ("file", "ignore") + return path if path == "file" else None + + report_builder_session = create_report_builder_session(path_fixer=fixes) + coveralls.from_json(json, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report == { + "archive": { + "file": [ + (1, 0, None, [[0, 0, None, None, None]], None, None), + (2, 1, None, [[0, 1, None, None, None]], None, None), + ] + }, + "report": { + "files": { + "file": [ + 0, + [0, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ] + }, + "sessions": {}, + }, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "50.00000", + "d": 0, + "diff": None, + "f": 1, + "h": 1, + "m": 1, + "n": 2, + "p": 0, + "s": 0, + }, + } + + def test_nested_json(self): + report_builder_session = create_report_builder_session() + coveralls.from_json(nested_json, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"] == { + "foobar": [ + (3, 1, None, [[0, 1, None, None, None]], None, None), + (5, 1, None, [[0, 1, None, None, None]], None, None), + ] + } diff --git a/apps/worker/services/report/languages/tests/unit/test_csharp.py b/apps/worker/services/report/languages/tests/unit/test_csharp.py new file mode 100644 index 0000000000..e590855a7b --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_csharp.py @@ -0,0 +1,120 @@ +import xml.etree.cElementTree as etree + +from services.report.languages import csharp +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +xml = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + + +class TestCSharp(BaseTestCase): + def test_report(self): + def fixes(path): + if path == "ignore": + return None + assert path in ("source",) + return path + + report_builder_session = create_report_builder_session(path_fixer=fixes) + csharp.from_xml(etree.fromstring(xml), report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report == { + "archive": { + "source": [ + (1, "2/2", "b", [[0, "2/2", None, None, None]], None, None), + (2, 2, None, [[0, 2, None, None, None]], None, None), + (3, 0, None, [[0, 0, None, None, None]], None, None), + (4, 0, None, [[0, 0, None, None, None]], None, None), + (5, 0, None, [[0, 0, None, None, None]], None, None), + (6, 1, None, [[0, 1, None, None, None]], None, None), + (10, "1/2", "b", [[0, "1/2", ["1:2"], None, None]], None, None), + ] + }, + "report": { + "files": { + "source": [ + 0, + [0, 7, 3, 3, 1, "42.85714", 2, 0, 0, 0, 0, 0, 0], + None, + None, + ] + }, + "sessions": {}, + }, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 2, + "c": "42.85714", + "d": 0, + "diff": None, + "f": 1, + "h": 3, + "m": 3, + "n": 7, + "p": 1, + "s": 0, + }, + } diff --git a/apps/worker/services/report/languages/tests/unit/test_csharp2.py b/apps/worker/services/report/languages/tests/unit/test_csharp2.py new file mode 100644 index 0000000000..385c2ca3fc --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_csharp2.py @@ -0,0 +1,90 @@ +import xml.etree.cElementTree as etree + +from services.report.languages import csharp +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +xml = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 100665117 + System.Void Givolio.Tests.BundleConfigUnitTests/<>c::.cctor() + + + + + + + + + + +""" + + +class TestCSharp2(BaseTestCase): + def test_report(self): + def fixes(path): + if path == "ignore": + return None + assert path in ("source",) + return path + + report_builder_session = create_report_builder_session(path_fixer=fixes) + csharp.from_xml(etree.fromstring(xml), report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"] == { + "source": [ + (1, 2, None, [[0, 2, None, None, None]], None, None), + (2, 2, None, [[0, 2, None, None, None]], None, None), + (3, 0, None, [[0, 0, None, None, None]], None, None), + (4, 0, None, [[0, 0, None, None, None]], None, None), + (5, 0, None, [[0, 0, None, None, None]], None, None), + (6, 0, None, [[0, 0, None, None, None]], None, None), + (10, 2, None, [[0, 2, None, None, None]], None, None), + ] + } diff --git a/apps/worker/services/report/languages/tests/unit/test_dlst.py b/apps/worker/services/report/languages/tests/unit/test_dlst.py new file mode 100644 index 0000000000..3824894482 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_dlst.py @@ -0,0 +1,51 @@ +import pytest + +from services.report.languages import dlst +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +RAW = b""" |empty + 1|coverage +0000000|missed +this is not line.... +source file.d is 77% covered""" + + +class TestDLST(BaseTestCase): + @pytest.mark.parametrize("filename", ["src/file.lst", "bad/path.lst", ""]) + def test_report(self, filename): + def fixer(path): + if path in ("file.d", "src/file.d"): + return "src/file.d" + + report_builder_session = create_report_builder_session( + path_fixer=fixer, filename=filename + ) + dlst.from_string(RAW, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + expected_result_archive = { + "src/file.d": [ + (2, 1, None, [[0, 1, None, None, None]], None, None), + (3, 0, None, [[0, 0, None, None, None]], None, None), + ] + } + assert expected_result_archive == processed_report["archive"] + + def test_none(self): + report_builder_session = create_report_builder_session( + path_fixer=lambda _: False, filename=None + ) + dlst.from_string(b" 1|test\nignore is 100% covered", report_builder_session) + report = report_builder_session.output_report() + assert not report + + def test_matches_content(self): + content, first_line, name = ( + b" 1|test\nignore is 100% covered", + " 1|test", + "name", + ) + assert dlst.DLSTProcessor().matches_content(content, first_line, name) diff --git a/apps/worker/services/report/languages/tests/unit/test_flowcover.py b/apps/worker/services/report/languages/tests/unit/test_flowcover.py new file mode 100644 index 0000000000..4cd64a6787 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_flowcover.py @@ -0,0 +1,35 @@ +from services.report.languages import flowcover +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +json = { + "files": { + "file.js": { + "expressions": { + "covered_locs": [ + {"start": {"line": 1, "column": 1}, "end": {"line": 1, "column": 5}} + ], + "uncovered_locs": [ + {"start": {"line": 2, "column": 1}, "end": {"line": 3, "column": 5}} + ], + } + } + } +} + + +class TestFlowCover(BaseTestCase): + def test_report(self): + report_builder_session = create_report_builder_session() + flowcover.from_json(json, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + expected_result_archive = { + "file.js": [ + (1, 1, None, [[0, 1, None, [[1, 5, 1]], None]], None, None), + (2, 0, None, [[0, 0, None, None, None]], None, None), + ] + } + assert expected_result_archive == processed_report["archive"] diff --git a/apps/worker/services/report/languages/tests/unit/test_gap.py b/apps/worker/services/report/languages/tests/unit/test_gap.py new file mode 100644 index 0000000000..96f0f27df2 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_gap.py @@ -0,0 +1,56 @@ +from services.report.languages import gap +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +RAW = b"""{"Type":"S","File":"lib/error.g","FileId":37} +{"Type":"R","Line":1,"FileId":37} +{"Type":"E","Line":2,"FileId":37} +{"Type":"R","Line":3,"FileId":37} + +{"Type":"R","Line":4,"FileId":37} + +{"Type":"S","File":"lib/test.g","FileId":1} +{"Type":"R","Line":1,"FileId":1} +""" + + +class TestGap(BaseTestCase): + def test_report(self): + report_builder_session = create_report_builder_session() + gap.from_string(RAW, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + expected_result_archive = { + "lib/error.g": [ + (1, 0, None, [[0, 0, None, None, None]], None, None), + (2, 1, None, [[0, 1, None, None, None]], None, None), + (3, 0, None, [[0, 0, None, None, None]], None, None), + (4, 0, None, [[0, 0, None, None, None]], None, None), + ], + "lib/test.g": [(1, 0, None, [[0, 0, None, None, None]], None, None)], + } + assert expected_result_archive == processed_report["archive"] + + def test_report_from_dict(self): + data = {"Type": "S", "File": "lib/error.g", "FileId": 37} + report_builder_session = create_report_builder_session(filename="aaa") + gap.GapProcessor().process(data, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + expected_result_archive = {} + assert expected_result_archive == processed_report["archive"] + + def test_detect(self): + processor = gap.GapProcessor() + assert processor.matches_content(b"", "", "") is False + assert ( + processor.matches_content( + b"", '{"Type":"S","File":"lib/error.g","FileId":37}', "" + ) + is True + ) + assert processor.matches_content(b'{"coverage"}', "", "") is False + assert processor.matches_content(b"-1.7", "", "") is False diff --git a/apps/worker/services/report/languages/tests/unit/test_gcov.py b/apps/worker/services/report/languages/tests/unit/test_gcov.py new file mode 100644 index 0000000000..b6ae3ba992 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_gcov.py @@ -0,0 +1,322 @@ +from shared.reports.resources import Report + +from services.report.languages import gcov +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +txt = b""" -: 0:Source:tmp.c + -: 1:not covered source + 1: 2:hit source +#####: 3:missed source +unconditional 0 never executed +: + + 0: 4:ignored /* LCOV_EXCL_START */ + 1: 5:ignored + 0: 6:ignored /* LCOV_EXCL_END */ + 0: 7:ignored LCOV_EXCL_LINE +=====: 8:sytax error +#####: 9:} +branch 0 never executed +branch 1 never executed + 1: 10: if ( ) +branch 0 taken 221 (fallthrough) +branch 1 taken 3 +branch 2 never executed +branch 3 taken 0 +function -[RGPropertyDeclaration .cxx_destruct] called 0 returned 0% blocks executed 0% + 1: 11:method +#####: 10:inline +#####: 11:static +#####: 12:} // hello world + 1: 13: MACRO_METHOD('blah'); +branch 0 never executed +branch 1 never executed + 1: 14: for (x) +branch 0 taken 3 +branch 1 taken 3 + 1: 15: } + 1: 16:@implementation blah; +""" + +txt_duplicate = b""" -: 0:Source:/project/rsl/h264/Mp4NaluParser.h +209*:13: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +#####:13: +call 0 never executed:: +call 1 never executed:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +#####:13: +call 0 never executed:: +call 1 never executed:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +#####:13: +call 0 never executed:: +call 1 never executed:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +4:13: +call 0 returned 100%:: +call 1 returned 100%:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +#####:13: +call 0 never executed:: +call 1 never executed:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +90:13: +call 0 returned 100%:: +call 1 returned 100%:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +90:13: +call 0 returned 100%:: +call 1 returned 100%:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +#####:13: +call 0 never executed:: +call 1 never executed:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +#####:13: +call 0 never executed:: +call 1 never executed:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +#####:13: +call 0 never executed:: +call 1 never executed:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +#####:13: +call 0 never executed:: +call 1 never executed:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +#####:13: +call 0 never executed:: +call 1 never executed:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +7:13: +call 0 returned 100%:: +call 1 returned 100%:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +#####:13: +call 0 never executed:: +call 1 never executed:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +#####:13: +call 0 never executed:: +call 1 never executed:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +18:13: +call 0 returned 100%:: +call 1 returned 100%:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +#####:13: +call 0 never executed:: +call 1 never executed:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +#####:13: +call 0 never executed:: +call 1 never executed:: +_ZN3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEEC2Ev:: +func +#####:13: +call 0 never executed:: +call 1 never executed:: +func +730:15: +call 0 returned 100%:: +call 1 returned 100%:: +730:16: +call 0 returned 100%:: +730:17: +94222*:19: +94222*:20: +_ZNK3rsl4h26413Mp4NaluParserINS_8DataViewIKhEEE7IsEmptyEv:: +""" + + +class TestGcov(BaseTestCase): + def test_report(self): + report_builder_session = create_report_builder_session( + filename="temp.c.gcov", + current_yaml={ + "parsers": { + "gcov": {"branch_detection": {"conditional": True, "loop": True}} + } + }, + ) + gcov.from_txt(txt, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"] == { + "tmp.c": [ + (2, 1, None, [[0, 1, None, None, None]], None, None), + (3, 0, None, [[0, 0, None, None, None]], None, None), + (8, 0, None, [[0, 0, None, None, None]], None, None), + (10, "2/4", "b", [[0, "2/4", None, None, None]], None, None), + (11, 1, "m", [[0, 1, None, None, None]], None, None), + (13, 1, None, [[0, 1, None, None, None]], None, None), + (14, "2/2", "b", [[0, "2/2", None, None, None]], None, None), + ] + } + + def test_report_duplicate_lines(self): + report_builder_session = create_report_builder_session( + filename="#project#rsl#h264#Mp4NaluParser.h.gcov.reduced", + current_yaml={ + "parsers": { + "gcov": {"branch_detection": {"conditional": True, "loop": True}} + } + }, + ) + gcov.from_txt(txt_duplicate, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"] == { + "project/rsl/h264/Mp4NaluParser.h": [ + (13, 90, "m", [[0, 90, None, None, None]], None, None), + (15, 730, "m", [[0, 730, None, None, None]], None, None), + (16, 730, None, [[0, 730, None, None, None]], None, None), + (17, 730, None, [[0, 730, None, None, None]], None, None), + ] + } + + def test_no_cond_branch_report(self): + report_builder_session = create_report_builder_session( + current_yaml={ + "parsers": {"gcov": {"branch_detection": {"conditional": False}}} + }, + ) + gcov.from_txt(txt, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"]["tmp.c"][3] == ( + 10, + 1, + "b", + [[0, 1, None, None, None]], + None, + None, + ) + + def test_single_line_report(self): + report_builder_session = create_report_builder_session( + current_yaml={ + "parsers": {"gcov": {"branch_detection": {"conditional": False}}} + }, + ) + gcov.from_txt(b" -: 0:Source:another_tmp.c", report_builder_session) + report = report_builder_session.output_report() + + assert not report + assert isinstance(report, Report) + + def test_no_cond_loop_report(self): + report_builder_session = create_report_builder_session( + current_yaml={"parsers": {"gcov": {"branch_detection": {"loop": False}}}}, + ) + gcov.from_txt(txt, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"]["tmp.c"][3] == ( + 10, + 1, + "b", + [[0, 1, None, None, None]], + None, + None, + ) + assert processed_report["archive"]["tmp.c"][6] == ( + 14, + 1, + "b", + [[0, 1, None, None, None]], + None, + None, + ) + + def test_track_macro_report(self): + report_builder_session = create_report_builder_session( + current_yaml={"parsers": {"gcov": {"branch_detection": {"macro": True}}}}, + ) + gcov.from_txt(txt, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"]["tmp.c"][3] == ( + 10, + 1, + "b", + [[0, 1, None, None, None]], + None, + None, + ) + assert processed_report["archive"]["tmp.c"][5] == ( + 13, + "0/2", + None, + [[0, "0/2", None, None, None]], + None, + None, + ) + + def test_no_yaml(self): + report_builder_session = create_report_builder_session( + current_yaml={"parsers": {"gcov": {}}}, + ) + gcov.from_txt(txt, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"]["tmp.c"][3] == ( + 10, + 1, + "b", + [[0, 1, None, None, None]], + None, + None, + ) + assert processed_report["archive"]["tmp.c"][5] == ( + 13, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + ) + + def test_detect(self): + processor = gcov.GcovProcessor() + assert processor.matches_content(b" -: 0:Source:black", "", "") is True + assert processor.matches_content(b"..... 0:Source:white", "", "") is True + assert processor.matches_content(b"", "", "") is False + assert processor.matches_content(b"0:Source", "", "") is False + + def test_ignored(self): + report_builder_session = create_report_builder_session( + current_yaml={"parsers": {"gcov": {}}}, + path_fixer=lambda _: None, + ) + gcov.from_txt(b" -: 0:Source:black\n", report_builder_session) + report = report_builder_session.output_report() + + assert not report diff --git a/apps/worker/services/report/languages/tests/unit/test_go.py b/apps/worker/services/report/languages/tests/unit/test_go.py new file mode 100644 index 0000000000..2bdc6c7800 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_go.py @@ -0,0 +1,672 @@ +import pytest +from shared.reports.types import ReportTotals + +from helpers.exceptions import CorruptRawReportError +from services.report.languages import go +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +txt = b"""mode: atomic +source:1.1,1.10 1 1 +source:7.14,9.10 1 1 +source:11.26,13.2 1 1 +ignore:15.19,17.2 1 1 +ignore: +source:15.19,17.2 1 0 + +source:15.19,17.2 1 1 +""" + +huge_txt = b"""mode: count +path/file.go:18.95,22.51 3 0 +path/file.go:28.2,29.61 2 0 +path/file.go:37.2,37.15 1 0 +path/file.go:22.51,24.3 1 3 +path/file.go:24.8,26.3 1 0 +path/file.go:29.61,31.3 1 0 +path/file.go:31.8,32.24 1 0 +path/file.go:32.24,34.4 1 0 +path/file.go:41.75,45.45 3 0 +path/file.go:51.2,52.55 2 0 +path/file.go:60.2,60.15 1 0 +path/file.go:45.45,47.3 1 0 +path/file.go:47.8,49.3 1 0 +path/file.go:52.55,54.3 1 0 +path/file.go:54.8,55.24 1 0 +path/file.go:55.24,57.4 1 0 +path/file.go:64.74,68.45 3 0 +path/file.go:74.2,75.55 2 0 +path/file.go:83.2,83.15 1 0 +path/file.go:68.45,70.3 1 0 +path/file.go:70.8,72.3 1 0 +path/file.go:75.55,77.3 1 0 +path/file.go:77.8,78.24 1 0 +path/file.go:78.24,80.4 1 1 +path/file.go:87.87,91.49 3 1 +path/file.go:97.2,98.59 2 0 +path/file.go:106.2,106.15 1 0 +path/file.go:91.49,93.3 1 0 +path/file.go:93.8,95.3 1 0 +path/file.go:98.59,100.3 1 0 +path/file.go:100.8,101.24 1 0 +path/file.go:101.24,103.4 1 0 +path/file.go:110.70,114.55 3 0 +path/file.go:122.2,122.11 1 0 +path/file.go:114.55,116.3 1 0 +path/file.go:116.8,117.24 1 0 +path/file.go:117.24,119.4 1 0 +path/file.go:126.36,128.2 1 0 +path/file.go:131.68,135.57 3 0 +path/file.go:143.2,144.61 2 0 +path/file.go:152.2,152.15 1 0 +path/file.go:135.57,137.3 1 0 +path/file.go:137.8,138.24 1 0 +path/file.go:138.24,140.4 1 0 +path/file.go:144.61,146.3 1 0 +path/file.go:146.8,147.24 1 0 +path/file.go:147.24,149.4 1 0 +path/file.go:156.126,160.81 3 0 +path/file.go:166.2,167.91 2 0 +path/file.go:175.2,175.15 1 0 +path/file.go:160.81,162.3 1 0 +path/file.go:162.8,164.3 1 0 +path/file.go:167.91,169.3 1 0 +path/file.go:169.8,170.24 1 0 +path/file.go:170.24,172.4 1 0 +path/file.go:179.64,183.53 3 0 +path/file.go:191.2,192.55 2 0 +path/file.go:200.2,200.15 1 0 +path/file.go:183.53,185.3 1 0 +path/file.go:185.8,186.24 1 0 +path/file.go:186.24,188.4 1 0 +path/file.go:192.55,194.3 1 0 +path/file.go:194.8,195.24 1 0 +path/file.go:195.24,197.4 1 0 +path/file.go:204.112,208.73 3 0 +path/file.go:216.2,217.66 2 0 +path/file.go:225.2,225.15 1 0 +path/file.go:208.73,210.3 1 0 +path/file.go:210.8,211.24 1 0 +path/file.go:211.24,213.4 1 0 +path/file.go:217.66,219.3 1 0 +path/file.go:219.8,220.24 1 0 +path/file.go:220.24,222.4 1 0 +path/file.go:229.89,233.61 3 0 +path/file.go:241.2,242.63 2 0 +path/file.go:250.2,250.15 1 0 +path/file.go:233.61,235.3 1 0 +path/file.go:235.8,236.24 1 0 +path/file.go:236.24,238.4 1 0 +path/file.go:242.63,244.3 1 0 +path/file.go:244.8,245.24 1 0 +path/file.go:245.24,247.4 1 0 +path/file.go:254.82,258.53 3 0 +path/file.go:266.2,267.55 2 0 +path/file.go:275.2,275.15 1 0 +path/file.go:258.53,260.3 1 0 +path/file.go:260.8,261.24 1 0 +path/file.go:261.24,263.4 1 0 +path/file.go:267.55,269.3 1 0 +path/file.go:269.8,270.24 1 0 +path/file.go:270.24,272.4 1 0 +path/file.go:279.107,283.61 3 0 +path/file.go:291.2,292.63 2 1 +path/file.go:300.2,300.15 1 0""" + + +class TestGo(BaseTestCase): + def test_report(self): + def fixes(path): + return None if "ignore" in path else path + + report_builder_session = create_report_builder_session(path_fixer=fixes) + go.from_txt(txt, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + expected_result_archive = { + "source": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + (7, 1, None, [[0, 1, None, None, None]], None, None), + (8, 1, None, [[0, 1, None, None, None]], None, None), + (9, 1, None, [[0, 1, None, None, None]], None, None), + (11, 1, None, [[0, 1, None, None, None]], None, None), + (12, 1, None, [[0, 1, None, None, None]], None, None), + (15, 1, None, [[0, 1, None, None, None]], None, None), + (16, 1, None, [[0, 1, None, None, None]], None, None), + ] + } + + assert expected_result_archive == processed_report["archive"] + + def test_huge_report(self): + def fixes(path): + return None if "ignore" in path else path + + report_builder_session = create_report_builder_session(path_fixer=fixes) + go.from_txt(huge_txt, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"] == { + "path/file.go": [ + (18, 0, None, [[0, 0, None, None, None]], None, None), + (19, 0, None, [[0, 0, None, None, None]], None, None), + (20, 0, None, [[0, 0, None, None, None]], None, None), + (21, 0, None, [[0, 0, None, None, None]], None, None), + (22, "1/2", None, [[0, "1/2", None, None, None]], None, None), + (23, 3, None, [[0, 3, None, None, None]], None, None), + (24, "1/2", None, [[0, "1/2", None, None, None]], None, None), + (25, 0, None, [[0, 0, None, None, None]], None, None), + (26, 0, None, [[0, 0, None, None, None]], None, None), + (28, 0, None, [[0, 0, None, None, None]], None, None), + (29, 0, None, [[0, 0, None, None, None]], None, None), + (30, 0, None, [[0, 0, None, None, None]], None, None), + (31, 0, None, [[0, 0, None, None, None]], None, None), + (32, 0, None, [[0, 0, None, None, None]], None, None), + (33, 0, None, [[0, 0, None, None, None]], None, None), + (34, 0, None, [[0, 0, None, None, None]], None, None), + (37, 0, None, [[0, 0, None, None, None]], None, None), + (41, 0, None, [[0, 0, None, None, None]], None, None), + (42, 0, None, [[0, 0, None, None, None]], None, None), + (43, 0, None, [[0, 0, None, None, None]], None, None), + (44, 0, None, [[0, 0, None, None, None]], None, None), + (45, 0, None, [[0, 0, None, None, None]], None, None), + (46, 0, None, [[0, 0, None, None, None]], None, None), + (47, 0, None, [[0, 0, None, None, None]], None, None), + (48, 0, None, [[0, 0, None, None, None]], None, None), + (49, 0, None, [[0, 0, None, None, None]], None, None), + (51, 0, None, [[0, 0, None, None, None]], None, None), + (52, 0, None, [[0, 0, None, None, None]], None, None), + (53, 0, None, [[0, 0, None, None, None]], None, None), + (54, 0, None, [[0, 0, None, None, None]], None, None), + (55, 0, None, [[0, 0, None, None, None]], None, None), + (56, 0, None, [[0, 0, None, None, None]], None, None), + (57, 0, None, [[0, 0, None, None, None]], None, None), + (60, 0, None, [[0, 0, None, None, None]], None, None), + (64, 0, None, [[0, 0, None, None, None]], None, None), + (65, 0, None, [[0, 0, None, None, None]], None, None), + (66, 0, None, [[0, 0, None, None, None]], None, None), + (67, 0, None, [[0, 0, None, None, None]], None, None), + (68, 0, None, [[0, 0, None, None, None]], None, None), + (69, 0, None, [[0, 0, None, None, None]], None, None), + (70, 0, None, [[0, 0, None, None, None]], None, None), + (71, 0, None, [[0, 0, None, None, None]], None, None), + (72, 0, None, [[0, 0, None, None, None]], None, None), + (74, 0, None, [[0, 0, None, None, None]], None, None), + (75, 0, None, [[0, 0, None, None, None]], None, None), + (76, 0, None, [[0, 0, None, None, None]], None, None), + (77, 0, None, [[0, 0, None, None, None]], None, None), + (78, "1/2", None, [[0, "1/2", None, None, None]], None, None), + (79, 1, None, [[0, 1, None, None, None]], None, None), + (80, 1, None, [[0, 1, None, None, None]], None, None), + (83, 0, None, [[0, 0, None, None, None]], None, None), + (87, 1, None, [[0, 1, None, None, None]], None, None), + (88, 1, None, [[0, 1, None, None, None]], None, None), + (89, 1, None, [[0, 1, None, None, None]], None, None), + (90, 1, None, [[0, 1, None, None, None]], None, None), + (91, "1/2", None, [[0, "1/2", None, None, None]], None, None), + (92, 0, None, [[0, 0, None, None, None]], None, None), + (93, 0, None, [[0, 0, None, None, None]], None, None), + (94, 0, None, [[0, 0, None, None, None]], None, None), + (95, 0, None, [[0, 0, None, None, None]], None, None), + (97, 0, None, [[0, 0, None, None, None]], None, None), + (98, 0, None, [[0, 0, None, None, None]], None, None), + (99, 0, None, [[0, 0, None, None, None]], None, None), + (100, 0, None, [[0, 0, None, None, None]], None, None), + (101, 0, None, [[0, 0, None, None, None]], None, None), + (102, 0, None, [[0, 0, None, None, None]], None, None), + (103, 0, None, [[0, 0, None, None, None]], None, None), + (106, 0, None, [[0, 0, None, None, None]], None, None), + (110, 0, None, [[0, 0, None, None, None]], None, None), + (111, 0, None, [[0, 0, None, None, None]], None, None), + (112, 0, None, [[0, 0, None, None, None]], None, None), + (113, 0, None, [[0, 0, None, None, None]], None, None), + (114, 0, None, [[0, 0, None, None, None]], None, None), + (115, 0, None, [[0, 0, None, None, None]], None, None), + (116, 0, None, [[0, 0, None, None, None]], None, None), + (117, 0, None, [[0, 0, None, None, None]], None, None), + (118, 0, None, [[0, 0, None, None, None]], None, None), + (119, 0, None, [[0, 0, None, None, None]], None, None), + (122, 0, None, [[0, 0, None, None, None]], None, None), + (126, 0, None, [[0, 0, None, None, None]], None, None), + (127, 0, None, [[0, 0, None, None, None]], None, None), + (131, 0, None, [[0, 0, None, None, None]], None, None), + (132, 0, None, [[0, 0, None, None, None]], None, None), + (133, 0, None, [[0, 0, None, None, None]], None, None), + (134, 0, None, [[0, 0, None, None, None]], None, None), + (135, 0, None, [[0, 0, None, None, None]], None, None), + (136, 0, None, [[0, 0, None, None, None]], None, None), + (137, 0, None, [[0, 0, None, None, None]], None, None), + (138, 0, None, [[0, 0, None, None, None]], None, None), + (139, 0, None, [[0, 0, None, None, None]], None, None), + (140, 0, None, [[0, 0, None, None, None]], None, None), + (143, 0, None, [[0, 0, None, None, None]], None, None), + (144, 0, None, [[0, 0, None, None, None]], None, None), + (145, 0, None, [[0, 0, None, None, None]], None, None), + (146, 0, None, [[0, 0, None, None, None]], None, None), + (147, 0, None, [[0, 0, None, None, None]], None, None), + (148, 0, None, [[0, 0, None, None, None]], None, None), + (149, 0, None, [[0, 0, None, None, None]], None, None), + (152, 0, None, [[0, 0, None, None, None]], None, None), + (156, 0, None, [[0, 0, None, None, None]], None, None), + (157, 0, None, [[0, 0, None, None, None]], None, None), + (158, 0, None, [[0, 0, None, None, None]], None, None), + (159, 0, None, [[0, 0, None, None, None]], None, None), + (160, 0, None, [[0, 0, None, None, None]], None, None), + (161, 0, None, [[0, 0, None, None, None]], None, None), + (162, 0, None, [[0, 0, None, None, None]], None, None), + (163, 0, None, [[0, 0, None, None, None]], None, None), + (164, 0, None, [[0, 0, None, None, None]], None, None), + (166, 0, None, [[0, 0, None, None, None]], None, None), + (167, 0, None, [[0, 0, None, None, None]], None, None), + (168, 0, None, [[0, 0, None, None, None]], None, None), + (169, 0, None, [[0, 0, None, None, None]], None, None), + (170, 0, None, [[0, 0, None, None, None]], None, None), + (171, 0, None, [[0, 0, None, None, None]], None, None), + (172, 0, None, [[0, 0, None, None, None]], None, None), + (175, 0, None, [[0, 0, None, None, None]], None, None), + (179, 0, None, [[0, 0, None, None, None]], None, None), + (180, 0, None, [[0, 0, None, None, None]], None, None), + (181, 0, None, [[0, 0, None, None, None]], None, None), + (182, 0, None, [[0, 0, None, None, None]], None, None), + (183, 0, None, [[0, 0, None, None, None]], None, None), + (184, 0, None, [[0, 0, None, None, None]], None, None), + (185, 0, None, [[0, 0, None, None, None]], None, None), + (186, 0, None, [[0, 0, None, None, None]], None, None), + (187, 0, None, [[0, 0, None, None, None]], None, None), + (188, 0, None, [[0, 0, None, None, None]], None, None), + (191, 0, None, [[0, 0, None, None, None]], None, None), + (192, 0, None, [[0, 0, None, None, None]], None, None), + (193, 0, None, [[0, 0, None, None, None]], None, None), + (194, 0, None, [[0, 0, None, None, None]], None, None), + (195, 0, None, [[0, 0, None, None, None]], None, None), + (196, 0, None, [[0, 0, None, None, None]], None, None), + (197, 0, None, [[0, 0, None, None, None]], None, None), + (200, 0, None, [[0, 0, None, None, None]], None, None), + (204, 0, None, [[0, 0, None, None, None]], None, None), + (205, 0, None, [[0, 0, None, None, None]], None, None), + (206, 0, None, [[0, 0, None, None, None]], None, None), + (207, 0, None, [[0, 0, None, None, None]], None, None), + (208, 0, None, [[0, 0, None, None, None]], None, None), + (209, 0, None, [[0, 0, None, None, None]], None, None), + (210, 0, None, [[0, 0, None, None, None]], None, None), + (211, 0, None, [[0, 0, None, None, None]], None, None), + (212, 0, None, [[0, 0, None, None, None]], None, None), + (213, 0, None, [[0, 0, None, None, None]], None, None), + (216, 0, None, [[0, 0, None, None, None]], None, None), + (217, 0, None, [[0, 0, None, None, None]], None, None), + (218, 0, None, [[0, 0, None, None, None]], None, None), + (219, 0, None, [[0, 0, None, None, None]], None, None), + (220, 0, None, [[0, 0, None, None, None]], None, None), + (221, 0, None, [[0, 0, None, None, None]], None, None), + (222, 0, None, [[0, 0, None, None, None]], None, None), + (225, 0, None, [[0, 0, None, None, None]], None, None), + (229, 0, None, [[0, 0, None, None, None]], None, None), + (230, 0, None, [[0, 0, None, None, None]], None, None), + (231, 0, None, [[0, 0, None, None, None]], None, None), + (232, 0, None, [[0, 0, None, None, None]], None, None), + (233, 0, None, [[0, 0, None, None, None]], None, None), + (234, 0, None, [[0, 0, None, None, None]], None, None), + (235, 0, None, [[0, 0, None, None, None]], None, None), + (236, 0, None, [[0, 0, None, None, None]], None, None), + (237, 0, None, [[0, 0, None, None, None]], None, None), + (238, 0, None, [[0, 0, None, None, None]], None, None), + (241, 0, None, [[0, 0, None, None, None]], None, None), + (242, 0, None, [[0, 0, None, None, None]], None, None), + (243, 0, None, [[0, 0, None, None, None]], None, None), + (244, 0, None, [[0, 0, None, None, None]], None, None), + (245, 0, None, [[0, 0, None, None, None]], None, None), + (246, 0, None, [[0, 0, None, None, None]], None, None), + (247, 0, None, [[0, 0, None, None, None]], None, None), + (250, 0, None, [[0, 0, None, None, None]], None, None), + (254, 0, None, [[0, 0, None, None, None]], None, None), + (255, 0, None, [[0, 0, None, None, None]], None, None), + (256, 0, None, [[0, 0, None, None, None]], None, None), + (257, 0, None, [[0, 0, None, None, None]], None, None), + (258, 0, None, [[0, 0, None, None, None]], None, None), + (259, 0, None, [[0, 0, None, None, None]], None, None), + (260, 0, None, [[0, 0, None, None, None]], None, None), + (261, 0, None, [[0, 0, None, None, None]], None, None), + (262, 0, None, [[0, 0, None, None, None]], None, None), + (263, 0, None, [[0, 0, None, None, None]], None, None), + (266, 0, None, [[0, 0, None, None, None]], None, None), + (267, 0, None, [[0, 0, None, None, None]], None, None), + (268, 0, None, [[0, 0, None, None, None]], None, None), + (269, 0, None, [[0, 0, None, None, None]], None, None), + (270, 0, None, [[0, 0, None, None, None]], None, None), + (271, 0, None, [[0, 0, None, None, None]], None, None), + (272, 0, None, [[0, 0, None, None, None]], None, None), + (275, 0, None, [[0, 0, None, None, None]], None, None), + (279, 0, None, [[0, 0, None, None, None]], None, None), + (280, 0, None, [[0, 0, None, None, None]], None, None), + (281, 0, None, [[0, 0, None, None, None]], None, None), + (282, 0, None, [[0, 0, None, None, None]], None, None), + (283, 0, None, [[0, 0, None, None, None]], None, None), + (291, 1, None, [[0, 1, None, None, None]], None, None), + (292, 1, None, [[0, 1, None, None, None]], None, None), + (300, 0, None, [[0, 0, None, None, None]], None, None), + ] + } + + assert report.totals == ReportTotals( + files=1, + lines=196, + hits=9, + misses=183, + partials=4, + coverage="4.59184", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=None, + ) + + def test_huge_report_partials_as_hits(self): + def fixes(path): + return None if "ignore" in path else path + + report_builder_session = create_report_builder_session( + path_fixer=fixes, + current_yaml={"parsers": {"go": {"partials_as_hits": True}}}, + ) + go.from_txt(huge_txt, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"] == { + "path/file.go": [ + (18, 0, None, [[0, 0, None, None, None]], None, None), + (19, 0, None, [[0, 0, None, None, None]], None, None), + (20, 0, None, [[0, 0, None, None, None]], None, None), + (21, 0, None, [[0, 0, None, None, None]], None, None), + (22, 1, None, [[0, 1, None, None, None]], None, None), + (23, 3, None, [[0, 3, None, None, None]], None, None), + (24, 1, None, [[0, 1, None, None, None]], None, None), + (25, 0, None, [[0, 0, None, None, None]], None, None), + (26, 0, None, [[0, 0, None, None, None]], None, None), + (28, 0, None, [[0, 0, None, None, None]], None, None), + (29, 0, None, [[0, 0, None, None, None]], None, None), + (30, 0, None, [[0, 0, None, None, None]], None, None), + (31, 0, None, [[0, 0, None, None, None]], None, None), + (32, 0, None, [[0, 0, None, None, None]], None, None), + (33, 0, None, [[0, 0, None, None, None]], None, None), + (34, 0, None, [[0, 0, None, None, None]], None, None), + (37, 0, None, [[0, 0, None, None, None]], None, None), + (41, 0, None, [[0, 0, None, None, None]], None, None), + (42, 0, None, [[0, 0, None, None, None]], None, None), + (43, 0, None, [[0, 0, None, None, None]], None, None), + (44, 0, None, [[0, 0, None, None, None]], None, None), + (45, 0, None, [[0, 0, None, None, None]], None, None), + (46, 0, None, [[0, 0, None, None, None]], None, None), + (47, 0, None, [[0, 0, None, None, None]], None, None), + (48, 0, None, [[0, 0, None, None, None]], None, None), + (49, 0, None, [[0, 0, None, None, None]], None, None), + (51, 0, None, [[0, 0, None, None, None]], None, None), + (52, 0, None, [[0, 0, None, None, None]], None, None), + (53, 0, None, [[0, 0, None, None, None]], None, None), + (54, 0, None, [[0, 0, None, None, None]], None, None), + (55, 0, None, [[0, 0, None, None, None]], None, None), + (56, 0, None, [[0, 0, None, None, None]], None, None), + (57, 0, None, [[0, 0, None, None, None]], None, None), + (60, 0, None, [[0, 0, None, None, None]], None, None), + (64, 0, None, [[0, 0, None, None, None]], None, None), + (65, 0, None, [[0, 0, None, None, None]], None, None), + (66, 0, None, [[0, 0, None, None, None]], None, None), + (67, 0, None, [[0, 0, None, None, None]], None, None), + (68, 0, None, [[0, 0, None, None, None]], None, None), + (69, 0, None, [[0, 0, None, None, None]], None, None), + (70, 0, None, [[0, 0, None, None, None]], None, None), + (71, 0, None, [[0, 0, None, None, None]], None, None), + (72, 0, None, [[0, 0, None, None, None]], None, None), + (74, 0, None, [[0, 0, None, None, None]], None, None), + (75, 0, None, [[0, 0, None, None, None]], None, None), + (76, 0, None, [[0, 0, None, None, None]], None, None), + (77, 0, None, [[0, 0, None, None, None]], None, None), + (78, 1, None, [[0, 1, None, None, None]], None, None), + (79, 1, None, [[0, 1, None, None, None]], None, None), + (80, 1, None, [[0, 1, None, None, None]], None, None), + (83, 0, None, [[0, 0, None, None, None]], None, None), + (87, 1, None, [[0, 1, None, None, None]], None, None), + (88, 1, None, [[0, 1, None, None, None]], None, None), + (89, 1, None, [[0, 1, None, None, None]], None, None), + (90, 1, None, [[0, 1, None, None, None]], None, None), + (91, 1, None, [[0, 1, None, None, None]], None, None), + (92, 0, None, [[0, 0, None, None, None]], None, None), + (93, 0, None, [[0, 0, None, None, None]], None, None), + (94, 0, None, [[0, 0, None, None, None]], None, None), + (95, 0, None, [[0, 0, None, None, None]], None, None), + (97, 0, None, [[0, 0, None, None, None]], None, None), + (98, 0, None, [[0, 0, None, None, None]], None, None), + (99, 0, None, [[0, 0, None, None, None]], None, None), + (100, 0, None, [[0, 0, None, None, None]], None, None), + (101, 0, None, [[0, 0, None, None, None]], None, None), + (102, 0, None, [[0, 0, None, None, None]], None, None), + (103, 0, None, [[0, 0, None, None, None]], None, None), + (106, 0, None, [[0, 0, None, None, None]], None, None), + (110, 0, None, [[0, 0, None, None, None]], None, None), + (111, 0, None, [[0, 0, None, None, None]], None, None), + (112, 0, None, [[0, 0, None, None, None]], None, None), + (113, 0, None, [[0, 0, None, None, None]], None, None), + (114, 0, None, [[0, 0, None, None, None]], None, None), + (115, 0, None, [[0, 0, None, None, None]], None, None), + (116, 0, None, [[0, 0, None, None, None]], None, None), + (117, 0, None, [[0, 0, None, None, None]], None, None), + (118, 0, None, [[0, 0, None, None, None]], None, None), + (119, 0, None, [[0, 0, None, None, None]], None, None), + (122, 0, None, [[0, 0, None, None, None]], None, None), + (126, 0, None, [[0, 0, None, None, None]], None, None), + (127, 0, None, [[0, 0, None, None, None]], None, None), + (131, 0, None, [[0, 0, None, None, None]], None, None), + (132, 0, None, [[0, 0, None, None, None]], None, None), + (133, 0, None, [[0, 0, None, None, None]], None, None), + (134, 0, None, [[0, 0, None, None, None]], None, None), + (135, 0, None, [[0, 0, None, None, None]], None, None), + (136, 0, None, [[0, 0, None, None, None]], None, None), + (137, 0, None, [[0, 0, None, None, None]], None, None), + (138, 0, None, [[0, 0, None, None, None]], None, None), + (139, 0, None, [[0, 0, None, None, None]], None, None), + (140, 0, None, [[0, 0, None, None, None]], None, None), + (143, 0, None, [[0, 0, None, None, None]], None, None), + (144, 0, None, [[0, 0, None, None, None]], None, None), + (145, 0, None, [[0, 0, None, None, None]], None, None), + (146, 0, None, [[0, 0, None, None, None]], None, None), + (147, 0, None, [[0, 0, None, None, None]], None, None), + (148, 0, None, [[0, 0, None, None, None]], None, None), + (149, 0, None, [[0, 0, None, None, None]], None, None), + (152, 0, None, [[0, 0, None, None, None]], None, None), + (156, 0, None, [[0, 0, None, None, None]], None, None), + (157, 0, None, [[0, 0, None, None, None]], None, None), + (158, 0, None, [[0, 0, None, None, None]], None, None), + (159, 0, None, [[0, 0, None, None, None]], None, None), + (160, 0, None, [[0, 0, None, None, None]], None, None), + (161, 0, None, [[0, 0, None, None, None]], None, None), + (162, 0, None, [[0, 0, None, None, None]], None, None), + (163, 0, None, [[0, 0, None, None, None]], None, None), + (164, 0, None, [[0, 0, None, None, None]], None, None), + (166, 0, None, [[0, 0, None, None, None]], None, None), + (167, 0, None, [[0, 0, None, None, None]], None, None), + (168, 0, None, [[0, 0, None, None, None]], None, None), + (169, 0, None, [[0, 0, None, None, None]], None, None), + (170, 0, None, [[0, 0, None, None, None]], None, None), + (171, 0, None, [[0, 0, None, None, None]], None, None), + (172, 0, None, [[0, 0, None, None, None]], None, None), + (175, 0, None, [[0, 0, None, None, None]], None, None), + (179, 0, None, [[0, 0, None, None, None]], None, None), + (180, 0, None, [[0, 0, None, None, None]], None, None), + (181, 0, None, [[0, 0, None, None, None]], None, None), + (182, 0, None, [[0, 0, None, None, None]], None, None), + (183, 0, None, [[0, 0, None, None, None]], None, None), + (184, 0, None, [[0, 0, None, None, None]], None, None), + (185, 0, None, [[0, 0, None, None, None]], None, None), + (186, 0, None, [[0, 0, None, None, None]], None, None), + (187, 0, None, [[0, 0, None, None, None]], None, None), + (188, 0, None, [[0, 0, None, None, None]], None, None), + (191, 0, None, [[0, 0, None, None, None]], None, None), + (192, 0, None, [[0, 0, None, None, None]], None, None), + (193, 0, None, [[0, 0, None, None, None]], None, None), + (194, 0, None, [[0, 0, None, None, None]], None, None), + (195, 0, None, [[0, 0, None, None, None]], None, None), + (196, 0, None, [[0, 0, None, None, None]], None, None), + (197, 0, None, [[0, 0, None, None, None]], None, None), + (200, 0, None, [[0, 0, None, None, None]], None, None), + (204, 0, None, [[0, 0, None, None, None]], None, None), + (205, 0, None, [[0, 0, None, None, None]], None, None), + (206, 0, None, [[0, 0, None, None, None]], None, None), + (207, 0, None, [[0, 0, None, None, None]], None, None), + (208, 0, None, [[0, 0, None, None, None]], None, None), + (209, 0, None, [[0, 0, None, None, None]], None, None), + (210, 0, None, [[0, 0, None, None, None]], None, None), + (211, 0, None, [[0, 0, None, None, None]], None, None), + (212, 0, None, [[0, 0, None, None, None]], None, None), + (213, 0, None, [[0, 0, None, None, None]], None, None), + (216, 0, None, [[0, 0, None, None, None]], None, None), + (217, 0, None, [[0, 0, None, None, None]], None, None), + (218, 0, None, [[0, 0, None, None, None]], None, None), + (219, 0, None, [[0, 0, None, None, None]], None, None), + (220, 0, None, [[0, 0, None, None, None]], None, None), + (221, 0, None, [[0, 0, None, None, None]], None, None), + (222, 0, None, [[0, 0, None, None, None]], None, None), + (225, 0, None, [[0, 0, None, None, None]], None, None), + (229, 0, None, [[0, 0, None, None, None]], None, None), + (230, 0, None, [[0, 0, None, None, None]], None, None), + (231, 0, None, [[0, 0, None, None, None]], None, None), + (232, 0, None, [[0, 0, None, None, None]], None, None), + (233, 0, None, [[0, 0, None, None, None]], None, None), + (234, 0, None, [[0, 0, None, None, None]], None, None), + (235, 0, None, [[0, 0, None, None, None]], None, None), + (236, 0, None, [[0, 0, None, None, None]], None, None), + (237, 0, None, [[0, 0, None, None, None]], None, None), + (238, 0, None, [[0, 0, None, None, None]], None, None), + (241, 0, None, [[0, 0, None, None, None]], None, None), + (242, 0, None, [[0, 0, None, None, None]], None, None), + (243, 0, None, [[0, 0, None, None, None]], None, None), + (244, 0, None, [[0, 0, None, None, None]], None, None), + (245, 0, None, [[0, 0, None, None, None]], None, None), + (246, 0, None, [[0, 0, None, None, None]], None, None), + (247, 0, None, [[0, 0, None, None, None]], None, None), + (250, 0, None, [[0, 0, None, None, None]], None, None), + (254, 0, None, [[0, 0, None, None, None]], None, None), + (255, 0, None, [[0, 0, None, None, None]], None, None), + (256, 0, None, [[0, 0, None, None, None]], None, None), + (257, 0, None, [[0, 0, None, None, None]], None, None), + (258, 0, None, [[0, 0, None, None, None]], None, None), + (259, 0, None, [[0, 0, None, None, None]], None, None), + (260, 0, None, [[0, 0, None, None, None]], None, None), + (261, 0, None, [[0, 0, None, None, None]], None, None), + (262, 0, None, [[0, 0, None, None, None]], None, None), + (263, 0, None, [[0, 0, None, None, None]], None, None), + (266, 0, None, [[0, 0, None, None, None]], None, None), + (267, 0, None, [[0, 0, None, None, None]], None, None), + (268, 0, None, [[0, 0, None, None, None]], None, None), + (269, 0, None, [[0, 0, None, None, None]], None, None), + (270, 0, None, [[0, 0, None, None, None]], None, None), + (271, 0, None, [[0, 0, None, None, None]], None, None), + (272, 0, None, [[0, 0, None, None, None]], None, None), + (275, 0, None, [[0, 0, None, None, None]], None, None), + (279, 0, None, [[0, 0, None, None, None]], None, None), + (280, 0, None, [[0, 0, None, None, None]], None, None), + (281, 0, None, [[0, 0, None, None, None]], None, None), + (282, 0, None, [[0, 0, None, None, None]], None, None), + (283, 0, None, [[0, 0, None, None, None]], None, None), + (291, 1, None, [[0, 1, None, None, None]], None, None), + (292, 1, None, [[0, 1, None, None, None]], None, None), + (300, 0, None, [[0, 0, None, None, None]], None, None), + ] + } + + assert report.totals == ReportTotals( + files=1, + lines=196, + hits=13, + misses=183, + partials=0, + coverage="6.63265", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=None, + ) + + def test_combine_partials(self): + assert go.combine_partials([(1, 5, 1), (9, 12, 0), (5, 7, 1), (8, 9, 0)]) == [ + [1, 7, 1], + [8, 12, 0], + ] # same combine + assert go.combine_partials([(1, 2, 0), (4, 10, 1)]) == [ + [1, 2, 0], + [4, 10, 1], + ] # outer == same + assert go.combine_partials([[1, None, 1]]) == [[1, None, 1]] # single == same + assert go.combine_partials([(2, 24, 1), (24, None, 0)]) == [ + [2, 24, 1], + [24, None, 0], + ] + assert go.combine_partials([(2, 2, 1), (2, 2, 0)]) is None + assert go.combine_partials([(0, None, 28), (0, None, 0)]) == [[0, None, 28]] + assert go.combine_partials([(2, 35, 1), (35, None, 1)]) == [[2, None, 1]] + assert go.combine_partials([(2, 35, "1/2"), (35, None, "1/2")]) == [ + [2, None, "1/2"] + ] + assert go.combine_partials([(2, 35, "1/2"), (35, None, "2/2")]) == [ + [2, 35, "1/2"], + [35, None, "2/2"], + ] + assert go.combine_partials([(None, 2, 1), (1, 5, 1)]) == [[0, 5, 1]] + assert go.combine_partials([(None, 1, 1), (1, 2, 0), (2, 3, 1)]) == [ + [1, 2, 0], + [2, 3, 1], + ] + assert go.combine_partials([(1, None, 1), (2, None, 0)]) == [ + [1, None, 1] + ] # hit&miss overlay == hit + assert go.combine_partials([(1, 5, 0), (4, 10, 1)]) == [ + [1, 4, 0], + [4, 10, 1], + ] # intersect + assert go.combine_partials(10 * [(1, 5, 0)] + 10 * [(4, 10, 1)]) == [ + [1, 4, 0], + [4, 10, 1], + ] # intersect + assert go.combine_partials([(1, 10, 0), (4, 6, 1)]) == [ + [1, 4, 0], + [4, 6, 1], + [6, 10, 0], + ] # inner overlay + + @pytest.mark.parametrize( + "line", + [ + # b"path/file.go20.46 2 0", # this is actually skipped over + b"path/file.go:53", + b"path/file.go:185.129.6 1 0", + b"path/file.go:1917,1915.57 2 0", + b"path/file.go:115corrupt-path/file.go:115.11,116.13 1 3", + b"path/file.go:178.43corrupt-path/file.go:186.2,186.15 1 0", + b"path/file.go:65.17corrupt-path/file.go:648.41,650.34 2 0", + b"path/file.go:185.16,187.3 1corrupt-path/file.go:702.2,702.11 1 0", + b"path/file.go:651.41,653.34 2corrupt-path/file.go:49.121,56.16 2 3", + b"path/file.go:242.63,244.3path/file.go:242.63,244.3 1 0", + ], + ) + def test_corrupt_report_line(self, line: bytes): + report_builder_session = create_report_builder_session() + + with pytest.raises(CorruptRawReportError) as ex: + go.from_txt(line, report_builder_session) + + assert ( + ex.value.corruption_error + == "Go coverage line does not match expected format" + ) + assert ( + ex.value.expected_format + == "name.go:line.column,line.column numberOfStatements hits" + ) diff --git a/apps/worker/services/report/languages/tests/unit/test_jacoco.py b/apps/worker/services/report/languages/tests/unit/test_jacoco.py new file mode 100644 index 0000000000..cbf704bc66 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_jacoco.py @@ -0,0 +1,165 @@ +import datetime +import logging +import xml.etree.cElementTree as etree +from time import time + +import pytest +from pytest import LogCaptureFixture + +from helpers.exceptions import ReportExpiredException +from services.report.languages import jacoco +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +xml = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + + +class TestJacoco(BaseTestCase): + @pytest.fixture(autouse=True) + def inject_fixtures(self, caplog: LogCaptureFixture): + self.caplog = caplog + + def test_report(self): + def fixes(path): + if path == "base/ignore": + return None + assert path in ("base/source.java", "base/file.java", "base/empty") + return path + + report_builder_session = create_report_builder_session(path_fixer=fixes) + + with self.caplog.at_level(logging.WARNING, logger=jacoco.__name__): + jacoco.from_xml(etree.fromstring(xml % int(time())), report_builder_session) + + assert ( + self.caplog.records[-1].message + == "Jacoco report has an invalid coverage line: nr=0. Skipping processing line." + ) + + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + expected_result_archive = { + "base/file.java": [(1, 1, None, [[0, 1, None, None, None]], None, None)], + "base/source.java": [ + (1, "2/2", "m", [[0, "2/2", None, None, (1, 4)]], None, (1, 4)), + (2, "1/2", "m", [[0, "1/2", None, None, (1, 4)]], None, (1, 4)), + (3, 0, None, [[0, 0, None, None, None]], None, None), + (4, 2, None, [[0, 2, None, None, None]], None, None), + ], + } + + assert expected_result_archive == processed_report["archive"] + + def test_report_partials_as_hits(self): + def fixes(path): + if path == "base/ignore": + return None + assert path in ("base/source.java", "base/file.java", "base/empty") + return path + + report_builder_session = create_report_builder_session( + current_yaml={"parsers": {"jacoco": {"partials_as_hits": True}}}, + path_fixer=fixes, + ) + jacoco.from_xml(etree.fromstring(xml % int(time())), report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + expected_result_archive = { + "base/file.java": [(1, 1, None, [[0, 1, None, None, None]], None, None)], + "base/source.java": [ + (1, "2/2", "m", [[0, "2/2", None, None, (1, 4)]], None, (1, 4)), + (2, 1, "m", [[0, 1, None, None, (1, 4)]], None, (1, 4)), + (3, 0, None, [[0, 0, None, None, None]], None, None), + (4, 2, None, [[0, 2, None, None, None]], None, None), + ], + } + + assert expected_result_archive == processed_report["archive"] + + @pytest.mark.parametrize( + "module, path", + [("a", "module_a/package/file"), ("b", "module_b/src/main/java/package/file")], + ) + def test_multi_module(self, module, path): + data = ( + """ + + + + + + + + """ + % module + ) + + def fixes(path): + if module == "a": + return path if "src/main/java" not in path else None + else: + return path if "src/main/java" in path else None + + report_builder_session = create_report_builder_session(path_fixer=fixes) + jacoco.from_xml(etree.fromstring(data), report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert [path] == list(processed_report["archive"].keys()) + + @pytest.mark.parametrize( + "date", + [ + (datetime.datetime.now() - datetime.timedelta(seconds=172800)) + .replace(minute=0, second=0) + .strftime("%s"), + "01-01-2014", + ], + ) + def test_expired(self, date): + report_builder_session = create_report_builder_session() + + with pytest.raises(ReportExpiredException, match="Jacoco report expired"): + jacoco.from_xml(etree.fromstring(xml % date), report_builder_session) diff --git a/apps/worker/services/report/languages/tests/unit/test_jetbrainsxml.py b/apps/worker/services/report/languages/tests/unit/test_jetbrainsxml.py new file mode 100644 index 0000000000..f55d67164c --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_jetbrainsxml.py @@ -0,0 +1,22 @@ +import xml.etree.cElementTree as etree + +from services.report.languages import jetbrainsxml + +from . import create_report_builder_session + + +def test_simple_jetbrainsxml(): + xml = """ + + + + +""" + report_builder_session = create_report_builder_session() + jetbrainsxml.from_xml( + etree.fromstring(xml), + report_builder_session, + ) + report = report_builder_session.output_report() + + assert not report.is_empty() diff --git a/apps/worker/services/report/languages/tests/unit/test_lcov.py b/apps/worker/services/report/languages/tests/unit/test_lcov.py new file mode 100644 index 0000000000..dbd54d74f6 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_lcov.py @@ -0,0 +1,217 @@ +from services.report.languages import lcov +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +txt = b""" +TN: +SF:file.js +FNDA:76,jsx +FN:76,(anonymous_1) +removed +DA:0,skipped +DA:null,skipped +DA:1,1,46ba21aa66ea047aced7130c2760d7d4 +DA:=,= +BRDA:0,1,0,1 +BRDA:0,1,0,1 +BRDA:1,1,0,1 +BRDA:1,1,1,1 +end_of_record + +TN: +SF:empty.js +FNF:0 +FNH:0 +DA:0,1 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record + +TN: +SF:file.ts +FNF:0 +FNH:0 +DA:2,1 +LF:1 +LH:1 +BRF:0 +BRH:0 +BRDA:1,1,0,1 +end_of_record + +TN: +SF:file.js +FNDA:76,jsx +FN:76,(anonymous_1) +removed +DA:0,skipped +DA:null,skipped +DA:1,0 +DA:=,= +DA:2,1e+0 +BRDA:0,0,0,0 +BRDA:1,1,1,1 +end_of_record + +TN: +SF:ignore +end_of_record + +TN: +SF:file.cpp +FN:2,not_hit +FN:3,_Zalkfjeo +FN:4,_Gsabebra +FN:78,_Zalkfjeo2 +FNDA:1,_ln1_is_skipped +FNDA:,not_hit +FNDA:2,ignored +FNDA:3,ignored +FNDA:4,ignored +FNDA:78,1+e0 +DA:1,1 +DA:77,0 +DA:78,1 +BRDA:2,1,0,1 +BRDA:2,1,1,- +BRDA:2,1,3,0 +BRDA:5,1,0,1 +BRDA:5,1,1,1 +BRDA:77,3,0,0 +BRDA:77,3,1,0 +BRDA:77,4,0,0 +BRDA:77,4,1,0 +end_of_record +""" + +negative_count = b""" +TN: +SF:file.js +DA:1,1 +DA:2,2 +DA:3,0 +DA:4,-1 +DA:5,-5 +DA:6,-20 +end_of_record +""" + +corrupt_txt = b""" +TN: +SF:foo.cpp + +DA:1,1 + +DA:DA:130,0 +DA:0, +DA:,0 +DA: +DA:not_int,123 +DA:123,not_decimal + +FN:just_a_name_no_line + +end_of_record +""" + + +class TestLcov(BaseTestCase): + def test_report(self): + def fixes(path): + if path == "ignore": + return None + assert path in ("file.js", "file.ts", "file.cpp", "empty.js") + return path + + report_builder_session = create_report_builder_session(path_fixer=fixes) + lcov.from_txt(txt, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"] == { + "file.cpp": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + (2, "1/3", "m", [[0, "1/3", ["1:1", "1:3"], None, None]], None, None), + (5, "2/2", "b", [[0, "2/2", None, None, None]], None, None), + ( + 77, + "0/4", + "b", + [[0, "0/4", ["3:0", "3:1", "4:0", "4:1"], None, None]], + None, + None, + ), + ( + 78, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + ), + ], + "file.js": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + (2, 1, None, [[0, 1, None, None, None]], None, None), + ], + "file.ts": [(2, 1, None, [[0, 1, None, None, None]], None, None)], + } + + def test_detect(self): + processor = lcov.LcovProcessor() + assert processor.matches_content(b"hello\nend_of_record\n", "", "") is True + assert processor.matches_content(txt, "", "") is True + assert processor.matches_content(b"hello_end_of_record", "", "") is False + assert processor.matches_content(b"", "", "") is False + + def test_negative_execution_count(self): + report_builder_session = create_report_builder_session() + lcov.from_txt(negative_count, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"] == { + "file.js": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + (2, 2, None, [[0, 2, None, None, None]], None, None), + (3, 0, None, [[0, 0, None, None, None]], None, None), + (4, 0, None, [[0, 0, None, None, None]], None, None), + (5, 0, None, [[0, 0, None, None, None]], None, None), + (6, 0, None, [[0, 0, None, None, None]], None, None), + ] + } + + def test_skips_corrupted_lines(self): + report_builder_session = create_report_builder_session() + lcov.from_txt(corrupt_txt, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"] == { + "foo.cpp": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + ] + } + + def test_regression_partial_branch(self): + # See https://github.com/codecov/feedback/issues/513 + text = b""" +SF:foo.c +DA:1047,731835 +BRDA:1047,0,0,0 +BRDA:1047,0,1,1 +end_of_record +""" + report_builder_session = create_report_builder_session() + lcov.from_txt(text, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"] == { + "foo.c": [ + (1047, "1/2", "b", [[0, "1/2", ["0:0"], None, None]], None, None), + ] + } diff --git a/apps/worker/services/report/languages/tests/unit/test_lua.py b/apps/worker/services/report/languages/tests/unit/test_lua.py new file mode 100644 index 0000000000..e1b41dfa46 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_lua.py @@ -0,0 +1,116 @@ +from services.report.languages import lua +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +txt = b""" +============================================================================== +source.lua +============================================================================== + line +**0 line + 1 line + 0 line + 1 line + +============================================================================== +file.lua +============================================================================== + 1 line + line + 1 line + 0 line +**0 line + line + +============================================================================== +ignore.lua +============================================================================== +**0 line + +============================================================================== +empty.lua +============================================================================== + line + +============================================================================== +Summary +============================================================================== + +27 29 48.21% ../src/split.lua +------------------------ +27 29 48.21% + +""" + + +class TestLua(BaseTestCase): + def test_report(self): + def fixes(path): + if path == "ignore.lua": + return None + assert path in ("source.lua", "empty.lua", "file.lua") + return path + + report_builder_session = create_report_builder_session(path_fixer=fixes) + lua.from_txt(txt, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"] == { + "file.lua": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + (3, 1, None, [[0, 1, None, None, None]], None, None), + (4, 0, None, [[0, 0, None, None, None]], None, None), + (5, 0, None, [[0, 0, None, None, None]], None, None), + ], + "source.lua": [ + (2, 0, None, [[0, 0, None, None, None]], None, None), + (3, 1, None, [[0, 1, None, None, None]], None, None), + (4, 0, None, [[0, 0, None, None, None]], None, None), + (5, 1, None, [[0, 1, None, None, None]], None, None), + ], + } + + def test_report_with_line_breaks_in_the_beginning(self): + content = b"\n".join( + [ + b"==============================================================================", + b"socks.lua", + b"==============================================================================", + b"", + b" 914 socks = []", + b"", + b" 914 function fact(n)", + b" 914 if n == 0 then", + b" 914 return 1", + b" 914 else", + b" 914 return 0", + b" 914 end", + b" 914 end", + b"<<<<<< EOF", + ] + ) + report_builder_session = create_report_builder_session() + lua.from_txt(content, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"] == { + "socks.lua": [ + (2, 914, None, [[0, 914, None, None, None]], None, None), + (4, 914, None, [[0, 914, None, None, None]], None, None), + (5, 914, None, [[0, 914, None, None, None]], None, None), + (6, 914, None, [[0, 914, None, None, None]], None, None), + (7, 914, None, [[0, 914, None, None, None]], None, None), + (8, 914, None, [[0, 914, None, None, None]], None, None), + (9, 914, None, [[0, 914, None, None, None]], None, None), + (10, 914, None, [[0, 914, None, None, None]], None, None), + ] + } + + def test_detect(self): + processor = lua.LuaProcessor() + assert processor.matches_content(b"=========", "", "") is True + assert processor.matches_content(b"=== fefef", "", "") is False + assert processor.matches_content(b"", "", "") is False diff --git a/apps/worker/services/report/languages/tests/unit/test_node.py b/apps/worker/services/report/languages/tests/unit/test_node.py new file mode 100644 index 0000000000..38f64c7c42 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_node.py @@ -0,0 +1,272 @@ +import dataclasses +from fractions import Fraction +from json import JSONEncoder, dumps, loads +from pathlib import Path + +import pytest + +from services.report import legacy_totals +from services.report.languages import node +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +here = Path(__file__) +folder = here.parent + +base_report = { + "ignore": {}, + "empty": { + "statementMap": {"1": {"skip": True}}, + "branchMap": {"1": {"skip": True}}, + "fnMap": {"1": {"skip": True}}, + }, +} + + +class OwnEncoder(JSONEncoder): + def default(self, o): + if dataclasses.is_dataclass(o): + return dataclasses.astuple(o) + if isinstance(o, Fraction): + return str(o) + return super().default(o) + + +class TestNodeProcessor(BaseTestCase): + def readjson(self, filename): + with open(folder / filename, "r") as d: + contents = loads(d.read()) + return contents + + def readfile(self, filename, if_empty_write=None): + with open(folder / filename, "r") as r: + contents = r.read() + + # codecov: assert not covered start [FUTURE new concept] + if contents.strip() == "" and if_empty_write: + with open(folder / filename, "w+") as r: + r.write(if_empty_write) + return if_empty_write + return contents + + @pytest.mark.parametrize( + "location, expected_result", + [ + ({"skip": True}, (None, None, None)), + ({"start": {"line": 0}}, (None, None, None)), + ( + {"start": {"line": 1, "column": 1}, "end": {"line": 1, "column": 2}}, + (None, None, None), + ), + ( + {"start": {"line": 1, "column": 1}, "end": {"line": 10, "column": 2}}, + (1, None, None), + ), + ], + ) + def test_get_location(self, location, expected_result): + assert node.get_line_coverage(location, None, None) == expected_result + + @pytest.mark.parametrize( + "location, expected", + [ + ({"skip": True}, None), + ({"start": {"line": 0}}, None), + ({"start": {"line": 1, "column": 1}, "end": {"line": 1, "column": 2}}, 1), + ], + ) + def test_location_to_int(self, location, expected): + assert node._location_to_int(location) == expected + + @pytest.mark.parametrize("i", [1, 2, 3]) + def test_report(self, i): + def fixes(path): + if path == "ignore": + return None + return path + + nodejson = loads(self.readfile("node/node%s.json" % i)) + nodejson.update(base_report) + + report_builder_session = create_report_builder_session( + current_yaml={"parsers": {"javascript": {"enable_partials": True}}}, + path_fixer=fixes, + ) + node.from_json(nodejson, report_builder_session) + report = report_builder_session.output_report() + report_json, chunks, _totals = report.serialize() + loaded_json = loads(report_json) + loaded_json.pop("totals") + + expected_result = loads(self.readfile("node/node%s-result.json" % i)) + assert { + "report": loaded_json, + "archive": chunks.decode().split("<<<<< end_of_chunk >>>>>"), + "totals": legacy_totals(report), + } == expected_result + + @pytest.mark.parametrize("name", ["inline", "ifbinary", "ifbinarymb"]) + def test_singles(self, name): + record = self.readjson("node/%s.json" % name) + report_builder_session = create_report_builder_session( + current_yaml={"parsers": {"javascript": {"enable_partials": True}}}, + ) + node.from_json(record["report"], report_builder_session) + report = report_builder_session.output_report() + + for filename, lines in record["result"].items(): + for ln, result in lines.items(): + assert loads(dumps(report[filename][int(ln)], cls=OwnEncoder)) == result + + @pytest.mark.parametrize( + "name,result", + [ + ( + "inline", + { + "file.js": { + "728": [ + 8, + None, + [[0, 8, None, None, None]], + None, + None, + None, + ] + } + }, + ), + ( + "ifbinary", + { + "file.js": { + "731": [ + 8, + None, + [ + [ + 0, + 8, + None, + None, + None, + ] + ], + None, + None, + None, + ] + } + }, + ), + ], + ) + def test_singles_no_partials_statement_map(self, name, result): + record = self.readjson("node/%s.json" % name) + report_builder_session = create_report_builder_session( + current_yaml={"parsers": {"javascript": {"enable_partials": False}}}, + ) + # In this test in particular we're trying to test the statementMap coverage in `from_json` + # Because the branchMap is analysed afterwards it overwrites the line coverage there. + # So forcing the statementMap only + record["report"]["file.js"].pop("branchMap") + node.from_json(record["report"], report_builder_session) + report = report_builder_session.output_report() + for filename, lines in result.items(): + for ln, result in lines.items(): + assert loads(dumps(report[filename][int(ln)], cls=OwnEncoder)) == result + + @pytest.mark.parametrize( + "name,result", + [ + ( + "inline", + { + "file.js": { + "728": [ + 8, + "b", + [[0, 8, None, None, None]], + None, + None, + None, + ] + } + }, + ), + ( + "ifbinary", + { + "file.js": { + "731": [ + 8, + "b", + [ + [ + 0, + 8, + None, + None, + None, + ] + ], + None, + None, + None, + ] + } + }, + ), + ], + ) + def test_singles_no_partials_branch_map(self, name, result): + record = self.readjson("node/%s.json" % name) + report_builder_session = create_report_builder_session( + current_yaml={"parsers": {"javascript": {"enable_partials": False}}}, + ) + node.from_json(record["report"], report_builder_session) + report = report_builder_session.output_report() + for filename, lines in result.items(): + for ln, result in lines.items(): + assert loads(dumps(report[filename][int(ln)], cls=OwnEncoder)) == result + + def test_matches_content_bad_user_input(self): + processor = node.NodeProcessor() + user_input_1 = {"filename_1": {}, "filename_2": 1} + assert not processor.matches_content( + user_input_1, "first_line", "coverage.json" + ) + user_input_2 = {"filename_1": "adsadasddsa", "filename_2": {}} + assert not processor.matches_content( + user_input_2, "first_line", "coverage.json" + ) + user_input_3 = "filename: 1" + assert not processor.matches_content( + user_input_3, "first_line", "coverage.json" + ) + + def test_matches_content_good_user_input(self): + processor = node.NodeProcessor() + user_input_1 = {"filename_1": {}, "filename_2": {"statementMap": {}}} + assert processor.matches_content(user_input_1, "first_line", "coverage.json") + user_input_2 = {"filename_1": {"statementMap": 1}, "filename_2": {}} + assert processor.matches_content(user_input_2, "first_line", "coverage.json") + + def test_no_statement_map(self): + user_input = { + "filename.py": { + "branches": {"covered": 0, "pct": 100, "skipped": 0, "total": 0}, + "functions": {"covered": 0, "pct": 0, "skipped": 0, "total": 1}, + "lines": {"covered": 2, "pct": 66.67, "skipped": 0, "total": 3}, + "statements": {"covered": 2, "pct": 66.67, "skipped": 0, "total": 3}, + } + } + report_builder_session = create_report_builder_session( + current_yaml={"parsers": {"javascript": {"enable_partials": True}}}, + ) + + node.from_json(user_input, report_builder_session) + report = report_builder_session.output_report() + + assert report.is_empty() diff --git a/apps/worker/services/report/languages/tests/unit/test_pycoverage.py b/apps/worker/services/report/languages/tests/unit/test_pycoverage.py new file mode 100644 index 0000000000..7455c7d250 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_pycoverage.py @@ -0,0 +1,607 @@ +"""Tests for pycoverage language processor that output actual labels. +This is going to be deprecated soon. +For the tests with encoded labels see services/report/languages/tests/unit/test_pycoverage_encoded_labels.py +""" + +from services.report.languages.pycoverage import PyCoverageProcessor +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +SAMPLE = { + "meta": { + "version": "6.4.1", + "timestamp": "2022-06-27T01:44:41.238230", + "branch_coverage": False, + "show_contexts": True, + }, + "files": { + "another.py": { + "executed_lines": [1, 2, 3, 4], + "summary": { + "covered_lines": 4, + "num_statements": 4, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + }, + "missing_lines": [], + "excluded_lines": [], + "contexts": { + "1": [""], + "2": [ + "test_another.py::test_fib_simple_case|run", + "test_another.py::test_fib_bigger_cases|run", + ], + "3": [ + "test_another.py::test_fib_simple_case|run", + "test_another.py::test_fib_bigger_cases|run", + ], + "4": ["test_another.py::test_fib_bigger_cases|run"], + }, + }, + "source.py": { + "executed_lines": [1, 3, 4, 5, 9], + "summary": { + "covered_lines": 5, + "num_statements": 7, + "percent_covered": 71.42857142857143, + "percent_covered_display": "71", + "missing_lines": 2, + "excluded_lines": 0, + }, + "missing_lines": [6, 10], + "excluded_lines": [], + "contexts": { + "1": [""], + "3": [""], + "9": [""], + "4": ["test_source.py::test_some_code|run"], + "5": ["test_source.py::test_some_code|run"], + }, + }, + "test_another.py": { + "executed_lines": [1, 3, 4, 5, 7, 8], + "summary": { + "covered_lines": 6, + "num_statements": 6, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + }, + "missing_lines": [], + "excluded_lines": [], + "contexts": { + "1": [""], + "3": [""], + "7": [""], + "4": ["test_another.py::test_fib_simple_case|run"], + "5": ["test_another.py::test_fib_simple_case|run"], + "8": ["test_another.py::test_fib_bigger_cases|run"], + }, + }, + "test_source.py": { + "executed_lines": [1, 4, 5], + "summary": { + "covered_lines": 3, + "num_statements": 3, + "percent_covered": 100.0, + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + }, + "missing_lines": [], + "excluded_lines": [], + "contexts": { + "1": [""], + "4": [""], + "5": ["test_source.py::test_some_code|run"], + }, + }, + }, + "totals": { + "covered_lines": 18, + "num_statements": 20, + "percent_covered": 90.0, + "percent_covered_display": "90", + "missing_lines": 2, + "excluded_lines": 0, + }, +} + +COMPRESSED_SAMPLE = { + "meta": { + "version": "6.5.0", + "timestamp": "2023-05-15T18:35:30.641570", + "branch_coverage": False, + "show_contexts": True, + }, + "totals": { + "covered_lines": 4, + "num_statements": 9, + "percent_covered": "44.44444", + "percent_covered_display": "44", + "missing_lines": 5, + "excluded_lines": 0, + }, + "files": { + "awesome.py": { + "executed_lines": [1, 2, 3, 5], + "summary": { + "covered_lines": 4, + "num_statements": 5, + "percent_covered": "80.0", + "percent_covered_display": "80", + "missing_lines": 1, + "excluded_lines": 0, + }, + "missing_lines": [4], + "excluded_lines": [], + "contexts": { + "1": [0], + "2": [1, 2], + "3": [2, 3], + "5": [4], + }, + }, + "__init__.py": { + "executed_lines": [], + "summary": { + "covered_lines": 0, + "num_statements": 4, + "percent_covered": "0.0", + "percent_covered_display": "0", + "missing_lines": 4, + "excluded_lines": 0, + }, + "missing_lines": [1, 3, 4, 5], + "excluded_lines": [], + "contexts": {}, + }, + "services/__init__.py": { + "executed_lines": [0], + "summary": { + "covered_lines": 0, + "num_statements": 0, + "percent_covered": "100.0", + "percent_covered_display": "100", + "missing_lines": 0, + "excluded_lines": 0, + }, + "missing_lines": [], + "excluded_lines": [], + "contexts": {"0": [0]}, + }, + }, + "labels_table": { + "0": "", + "1": "label_1", + "2": "label_2", + "3": "label_3", + "4": "label_5", + }, +} + + +class TestPyCoverageProcessor(BaseTestCase): + def test_matches_content_pycoverage(self): + p = PyCoverageProcessor() + assert p.matches_content(SAMPLE, "", "coverage.json") + assert not p.matches_content({"meta": True}, "", "coverage.json") + assert not p.matches_content({"meta": {}}, "", "coverage.json") + + def test_process_pycoverage(self): + content = SAMPLE + p = PyCoverageProcessor() + report_builder_session = create_report_builder_session( + current_yaml={ + "flag_management": { + "default_rules": { + "carryforward": "true", + "carryforward_mode": "labels", + } + } + }, + ) + p.process(content, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"]["source.py"][0] == ( + 1, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["Th2dMtk4M_codecov"])], + ) + assert processed_report == { + "archive": { + "another.py": [ + ( + 1, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["Th2dMtk4M_codecov"])], + ), + ( + 2, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [ + (0, 1, None, ["test_another.py::test_fib_simple_case"]), + (0, 1, None, ["test_another.py::test_fib_bigger_cases"]), + ], + ), + ( + 3, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [ + (0, 1, None, ["test_another.py::test_fib_simple_case"]), + (0, 1, None, ["test_another.py::test_fib_bigger_cases"]), + ], + ), + ( + 4, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["test_another.py::test_fib_bigger_cases"])], + ), + ], + "source.py": [ + ( + 1, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["Th2dMtk4M_codecov"])], + ), + ( + 3, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["Th2dMtk4M_codecov"])], + ), + ( + 4, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["test_source.py::test_some_code"])], + ), + ( + 5, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["test_source.py::test_some_code"])], + ), + ( + 6, + 0, + None, + [[0, 0, None, None, None]], + None, + None, + [], + ), + ( + 9, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["Th2dMtk4M_codecov"])], + ), + ( + 10, + 0, + None, + [[0, 0, None, None, None]], + None, + None, + [], + ), + ], + "test_another.py": [ + ( + 1, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["Th2dMtk4M_codecov"])], + ), + ( + 3, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["Th2dMtk4M_codecov"])], + ), + ( + 4, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["test_another.py::test_fib_simple_case"])], + ), + ( + 5, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["test_another.py::test_fib_simple_case"])], + ), + ( + 7, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["Th2dMtk4M_codecov"])], + ), + ( + 8, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["test_another.py::test_fib_bigger_cases"])], + ), + ], + "test_source.py": [ + ( + 1, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["Th2dMtk4M_codecov"])], + ), + ( + 4, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["Th2dMtk4M_codecov"])], + ), + ( + 5, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["test_source.py::test_some_code"])], + ), + ], + }, + "report": { + "files": { + "another.py": [ + 0, + [0, 4, 4, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "source.py": [ + 1, + [0, 7, 5, 2, 0, "71.42857", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "test_another.py": [ + 2, + [0, 6, 6, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "test_source.py": [ + 3, + [0, 3, 3, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + }, + "sessions": {}, + }, + "totals": { + "f": 4, + "n": 20, + "h": 18, + "m": 2, + "p": 0, + "c": "90.00000", + "b": 0, + "d": 0, + "M": 0, + "s": 0, + "C": 0, + "N": 0, + "diff": None, + }, + } + + def test_process_compressed_report(self): + content = COMPRESSED_SAMPLE + p = PyCoverageProcessor() + report_builder_session = create_report_builder_session( + current_yaml={ + "flag_management": { + "default_rules": { + "carryforward": "true", + "carryforward_mode": "labels", + } + } + }, + ) + p.process(content, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report == { + "archive": { + "awesome.py": [ + ( + 1, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["Th2dMtk4M_codecov"])], + ), + ( + 2, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [ + (0, 1, None, ["label_1"]), + (0, 1, None, ["label_2"]), + ], + ), + ( + 3, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [ + (0, 1, None, ["label_2"]), + (0, 1, None, ["label_3"]), + ], + ), + ( + 4, + 0, + None, + [[0, 0, None, None, None]], + None, + None, + [], + ), + ( + 5, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, ["label_5"])], + ), + ], + "__init__.py": [ + ( + 1, + 0, + None, + [[0, 0, None, None, None]], + None, + None, + [], + ), + ( + 3, + 0, + None, + [[0, 0, None, None, None]], + None, + None, + [], + ), + ( + 4, + 0, + None, + [[0, 0, None, None, None]], + None, + None, + [], + ), + ( + 5, + 0, + None, + [[0, 0, None, None, None]], + None, + None, + [], + ), + ], + }, + "report": { + "files": { + "awesome.py": [ + 0, + [0, 5, 4, 1, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "__init__.py": [ + 1, + [0, 4, 0, 4, 0, "0", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + }, + "sessions": {}, + }, + "totals": { + "f": 2, + "n": 9, + "h": 4, + "m": 5, + "p": 0, + "c": "44.44444", + "b": 0, + "d": 0, + "M": 0, + "s": 0, + "C": 0, + "N": 0, + "diff": None, + }, + } diff --git a/apps/worker/services/report/languages/tests/unit/test_rlang.py b/apps/worker/services/report/languages/tests/unit/test_rlang.py new file mode 100644 index 0000000000..50c4809b80 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_rlang.py @@ -0,0 +1,32 @@ +from services.report.languages import rlang +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +json = { + "uploader": "R", + "files": [ + {"name": "source/cov.r", "coverage": [None, 1, 0]}, + {"name": "source/app.r", "coverage": [None, 1]}, + ], +} + + +class TestRlang(BaseTestCase): + def test_report(self): + def fixes(path): + assert path in ("source/cov.r", "source/app.r") + return path + + report_builder_session = create_report_builder_session(path_fixer=fixes) + rlang.from_json(json, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"] == { + "source/app.r": [(1, 1, None, [[0, 1, None, None, None]], None, None)], + "source/cov.r": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + (2, 0, None, [[0, 0, None, None, None]], None, None), + ], + } diff --git a/apps/worker/services/report/languages/tests/unit/test_salesforce.py b/apps/worker/services/report/languages/tests/unit/test_salesforce.py new file mode 100644 index 0000000000..0a01a34a28 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_salesforce.py @@ -0,0 +1,118 @@ +from services.report.languages.salesforce import SalesforceProcessor +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + + +class TestSalesforce(BaseTestCase): + def test_salesforce_processor_nones(self): + user_input = [ + None, + None, + None, + {"name": "file.py", "lines": {1: 5}}, + None, + None, + None, + None, + None, + None, + ] + processor = SalesforceProcessor() + report_builder_session = create_report_builder_session() + processor.process(user_input, report_builder_session) + report = report_builder_session.output_report() + result = self.convert_report_to_better_readable(report) + + assert result == { + "archive": { + "file.py": [(1, 5, None, [[0, 5, None, None, None]], None, None)] + }, + "report": { + "files": { + "file.py": [ + 0, + [0, 1, 1, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ] + }, + "sessions": {}, + }, + "totals": { + "f": 1, + "n": 1, + "h": 1, + "m": 0, + "p": 0, + "c": "100", + "b": 0, + "d": 0, + "M": 0, + "s": 0, + "C": 0, + "N": 0, + "diff": None, + }, + } + + def test_salesforce_matcher(self): + name = "test-result-codecoverage.json" + + file_contents = [ + { + "id": "01p4T000004n1shQAA", + "name": "LIFXController", + "totalLines": 29, + "lines": {"3": 1, "6": 1, "7": 1, "8": 1, "9": 1, "10": 1}, + "totalCovered": 27, + "coveredPercent": 93, + } + ] + processor = SalesforceProcessor() + + assert processor.matches_content(file_contents, "[", name) is True + + def test_salesforce_matcher_detailed_file(self): + name = "test-result-7075400002Mmqrs-codecoverage.json" + file_contents = [ + [ + { + "apexClassOrTriggerName": "LIFXController", + "apexClassOrTriggerId": "01p4T000004n1shQAA", + "apexTestClassId": "01p4T000004n1siQAA", + "apexTestMethodName": "testGetLights", + "numLinesCovered": 11, + "numLinesUncovered": 18, + "percentage": "38%", + "coverage": { + "coveredLines": [1, 2, 3, 4, 5, 6, 7, 8, 9], + "uncoveredLines": [10, 11, 12, 13], + }, + }, + { + "apexClassOrTriggerName": "LIFXController", + "apexClassOrTriggerId": "01p4T000004n1shQAA", + "apexTestClassId": "01p4T000004n1siQAA", + "apexTestMethodName": "testSetPower", + "numLinesCovered": 17, + "numLinesUncovered": 12, + "percentage": "59%", + "coverage": { + "coveredLines": [1, 2, 3, 4, 5, 6], + "uncoveredLines": [7, 8, 9, 10, 11, 12], + }, + }, + ] + ] + processor = SalesforceProcessor() + + assert processor.matches_content(file_contents, "[", name) is False + + def test_salesforce_matcher_empty_array(self): + name = "test-result-codecoverage.json" + + file_contents = [] + processor = SalesforceProcessor() + + assert processor.matches_content(file_contents, "[", name) is False diff --git a/apps/worker/services/report/languages/tests/unit/test_scala.py b/apps/worker/services/report/languages/tests/unit/test_scala.py new file mode 100644 index 0000000000..c2f6b99746 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_scala.py @@ -0,0 +1,69 @@ +from services.report.languages import scala +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +json = { + "total": 87, + "fileReports": [ + { + "filename": "f1", + "total": 100, + "coverage": { + "34": 1, + "19": 1, + "26": 0, + "16": 0, + "32": 0, + "36": 1, + "25": 1, + "18": 1, + }, + }, + { + "filename": "f2", + "total": 38, + "coverage": {"24": 1, "25": 0, "28": 1, "23": 1}, + }, + { + "filename": "ignore", + "total": 38, + "coverage": {"24": 1, "25": 1, "28": 1, "23": 1}, + }, + ], +} + + +class TestScala(BaseTestCase): + def test_report(self): + def fixes(path): + if path == "ignore": + return None + assert path in ("f1", "f2", "ignore") + return path + + report_builder_session = create_report_builder_session(path_fixer=fixes) + scala.from_json(json, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + expected_result_archive = { + "f1": [ + (16, 0, None, [[0, 0, None, None, None]], None, None), + (18, 1, None, [[0, 1, None, None, None]], None, None), + (19, 1, None, [[0, 1, None, None, None]], None, None), + (25, 1, None, [[0, 1, None, None, None]], None, None), + (26, 0, None, [[0, 0, None, None, None]], None, None), + (32, 0, None, [[0, 0, None, None, None]], None, None), + (34, 1, None, [[0, 1, None, None, None]], None, None), + (36, 1, None, [[0, 1, None, None, None]], None, None), + ], + "f2": [ + (23, 1, None, [[0, 1, None, None, None]], None, None), + (24, 1, None, [[0, 1, None, None, None]], None, None), + (25, 0, None, [[0, 0, None, None, None]], None, None), + (28, 1, None, [[0, 1, None, None, None]], None, None), + ], + } + + assert expected_result_archive == processed_report["archive"] diff --git a/apps/worker/services/report/languages/tests/unit/test_scoverage.py b/apps/worker/services/report/languages/tests/unit/test_scoverage.py new file mode 100644 index 0000000000..44c4decd84 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_scoverage.py @@ -0,0 +1,70 @@ +import xml.etree.cElementTree as etree + +from services.report.languages import scoverage +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +xml = """ + + + source.scala + 1 + false + 1 + + + source.scala + 2 + true + 0 + false + + + source.scala + 10 + true + 0 + true + + + ignore + 1 + false + 0 + + + source.scala + 3 + false + 0 + + + ignore + 1 + false + 0 + + +""" + + +class TestSCoverage(BaseTestCase): + def test_report(self): + def fixes(path): + if path == "ignore": + return None + return path + + report_builder_session = create_report_builder_session(path_fixer=fixes) + scoverage.from_xml(etree.fromstring(xml), report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"] == { + "source.scala": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + (2, "0/2", "b", [[0, "0/2", None, None, None]], None, None), + (3, 0, None, [[0, 0, None, None, None]], None, None), + ] + } diff --git a/apps/worker/services/report/languages/tests/unit/test_simplecov.py b/apps/worker/services/report/languages/tests/unit/test_simplecov.py new file mode 100644 index 0000000000..9315ce941b --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_simplecov.py @@ -0,0 +1,96 @@ +from json import loads + +from services.report.languages import simplecov +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +txt_v17 = """ +{ + "timestamp": 1597939304, + "command_name": "RSpec", + "files": [ + { + "filename": "controllers/tests_controller.rb", + "covered_percent": 27.5, + "coverage": [ + 1, + null, + 0 + ] + } + ] +} +""" + +txt_v18 = """ +{ + "timestamp": 1597939304, + "command_name": "RSpec", + "files": [ + { + "filename": "controllers/tests_controller.rb", + "covered_percent": 27.5, + "coverage": { + "lines": [ + 1, + null, + 0 + ] + }, + "covered_strength": 0.275, + "covered_lines": 11, + "lines_of_code": 40 + } + ] +} +""" + + +class TestSimplecovProcessor(BaseTestCase): + def test_parse_simplecov(self): + def fixes(path): + assert path == "controllers/tests_controller.rb" + return path + + expected_result_archive = { + "controllers/tests_controller.rb": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + (2, None, None, [[0, None, None, None, None]], None, None), + (3, 0, None, [[0, 0, None, None, None]], None, None), + ] + } + + report_builder_session = create_report_builder_session(path_fixer=fixes) + simplecov.from_json(loads(txt_v17), report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert expected_result_archive == processed_report["archive"] + + simplecov.from_json(loads(txt_v18), report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert expected_result_archive == processed_report["archive"] + + def test_process(self): + def fixes(path): + assert path == "controllers/tests_controller.rb" + return path + + expected_result_archive = { + "controllers/tests_controller.rb": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + (2, None, None, [[0, None, None, None, None]], None, None), + (3, 0, None, [[0, 0, None, None, None]], None, None), + ] + } + + report_builder_session = create_report_builder_session(path_fixer=fixes) + processor = simplecov.SimplecovProcessor() + processor.process(loads(txt_v17), report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert expected_result_archive == processed_report["archive"] diff --git a/apps/worker/services/report/languages/tests/unit/test_v1.py b/apps/worker/services/report/languages/tests/unit/test_v1.py new file mode 100644 index 0000000000..ab25018c30 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_v1.py @@ -0,0 +1,95 @@ +import pytest + +from helpers.exceptions import CorruptRawReportError +from services.report.languages import v1 +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +json = { + "coverage": { + "source": [None, 1], + "file": {"1": 1, "2": "1", "3": True, "4": "1/2"}, + "empty": {}, + }, + "messages": {"source": {"1": "Message"}}, +} + +alternative_report_format = { + "coverage": { + "/home/repo/app/scable/channel.rb": {"lines": [1, 1, None, None]}, + "/home/repo/app/scable/something.rb": {}, + "/home/repo/app/scable/something_else.rb": {"lines": []}, + "/home/repo/lib/exceptions.rb": {"lines": [1, 0, 10, None]}, + }, + "timestamp": 1588372645, +} + + +class TestVOne(BaseTestCase): + def test_report(self): + def fixes(path): + assert path in ("source", "file", "empty") + return path + + report_builder_session = create_report_builder_session(path_fixer=fixes) + v1.from_json(json, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + expected_result_archive = { + "file": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + (2, 1, None, [[0, 1, None, None, None]], None, None), + (3, True, "b", [[0, True, None, None, None]], None, None), + (4, "1/2", "b", [[0, "1/2", None, None, None]], None, None), + ], + "source": [(1, 1, None, [[0, 1, None, None, None]], None, None)], + } + + assert expected_result_archive == processed_report["archive"] + + def test_not_list(self): + report_builder_session = create_report_builder_session() + v1.from_json({"coverage": ""}, report_builder_session) + report = report_builder_session.output_report() + + assert not report + + def test_report_with_alternative_format(self): + report_builder_session = create_report_builder_session() + v1.from_json(alternative_report_format, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + expected_result_archive = { + "/home/repo/app/scable/channel.rb": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + (2, 1, None, [[0, 1, None, None, None]], None, None), + ], + "/home/repo/lib/exceptions.rb": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + (2, 0, None, [[0, 0, None, None, None]], None, None), + (3, 10, None, [[0, 10, None, None, None]], None, None), + ], + } + assert expected_result_archive == processed_report["archive"] + + def test_corrupted_report(self): + corrupted_report = { + "coverage": { + "source": [None, 1], + "file": {"file1": 1, "file2": 2}, + } + } + report_builder_session = create_report_builder_session() + + with pytest.raises(CorruptRawReportError) as e: + v1.from_json(corrupted_report, report_builder_session) + + exp = e.value + assert ( + exp.corruption_error + == "file dictionaries expected to have integers, not strings" + ) + assert exp.expected_format == "v1" diff --git a/apps/worker/services/report/languages/tests/unit/test_vb.py b/apps/worker/services/report/languages/tests/unit/test_vb.py new file mode 100644 index 0000000000..fd02c1ce02 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_vb.py @@ -0,0 +1,66 @@ +import xml.etree.cElementTree as etree + +from services.report.languages import vb +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +txt = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + + +class TestVBOne(BaseTestCase): + def test_report(self): + report_builder_session = create_report_builder_session() + vb.from_xml(etree.fromstring(txt), report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + expected_result_archive = { + "Source/Mobius/csharp/Tests.Common/Picklers.cs": [ + (42, 1, None, [[0, 1, None, None, None]], None, None), + (45, 0, None, [[0, 0, None, None, None]], None, None), + (50, 1, None, [[0, 1, None, None, None]], None, None), + (52, True, None, [[0, True, None, None, None]], None, None), + ], + "Source/Mobius/csharp/Tests.Common/RowHelper.cs": [ + (90, 1, None, [[0, 1, None, None, None]], None, None), + (91, 0, None, [[0, 0, None, None, None]], None, None), + (92, 1, None, [[0, 1, None, None, None]], None, None), + ], + } + + assert expected_result_archive == processed_report["archive"] diff --git a/apps/worker/services/report/languages/tests/unit/test_vb2.py b/apps/worker/services/report/languages/tests/unit/test_vb2.py new file mode 100644 index 0000000000..001de5a9fc --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_vb2.py @@ -0,0 +1,65 @@ +import xml.etree.cElementTree as etree + +from services.report.languages import vb2 +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +txt = """ + + + 258 + 0 + 258 + 0 + 1 + 1 + 0 + + + 260 + 0 + 260 + 0 + 0 + 5 + 1 + + + 261 + 0 + 262 + 0 + 2 + 5 + 1 + + + 1 + source\\mobius\\cpp\\riosock\\riosock.cpp + + + 5 + Source\\Mobius\\csharp\\Tests.Common\\RowHelper.cs + + +""" + + +class TestVBTwo(BaseTestCase): + def test_report(self): + report_builder_session = create_report_builder_session() + report = vb2.from_xml(etree.fromstring(txt), report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert processed_report["archive"] == { + "Source/Mobius/csharp/Tests.Common/RowHelper.cs": [ + (260, 1, None, [[0, 1, None, None, None]], None, None), + (261, 0, None, [[0, 0, None, None, None]], None, None), + (262, 0, None, [[0, 0, None, None, None]], None, None), + ], + "source/mobius/cpp/riosock/riosock.cpp": [ + (258, True, None, [[0, True, None, None, None]], None, None) + ], + } diff --git a/apps/worker/services/report/languages/tests/unit/test_xcode.py b/apps/worker/services/report/languages/tests/unit/test_xcode.py new file mode 100644 index 0000000000..dfc8526090 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_xcode.py @@ -0,0 +1,66 @@ +from services.report.languages import xcode +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +txt = b"""/source: + | 1|line + 1| 2|line + ------------------ + | -[UVWelcomeViewController dealloc]: + | 0| 360| noErr OSErr: function performed properly - no error + ------------------ + 0| 3|line + +/totally_empty: +/file: + | 1|line + 1k| 2|line + warning: The file '/Users/Jack/Documents/Coupgon/sdk-ios/Source/CPGCoupgonsViewController.swift' isn't covered. + \033\x1b[0;36m/file:\033[0m + 1m| 3|line + 1| 4| } + +/ignore: + 0| 1|line + +""" + + +class TestXCode(BaseTestCase): + def test_report(self): + def fixes(path): + if path == "ignore": + return None + assert path in ("source", "file", "empty", "totally_empty") + return path + + report_builder_session = create_report_builder_session(path_fixer=fixes) + xcode.from_txt(txt, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + expected_result_archive = { + "file": [ + (2, 1000, None, [[0, 1000, None, None, None]], None, None), + (3, 99999, None, [[0, 99999, None, None, None]], None, None), + ], + "source": [ + (2, 1, None, [[0, 1, None, None, None]], None, None), + (3, 0, None, [[0, 0, None, None, None]], None, None), + ], + } + + assert expected_result_archive == processed_report["archive"] + + def test_removes_last(self): + report_builder_session = create_report_builder_session() + xcode.from_txt( + b"""\nnothing\n/file:\n 1 | 1|line\n/totally_empty:""", + report_builder_session, + ) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert "totally_empty" not in processed_report["archive"] + assert "file" in processed_report["archive"] diff --git a/apps/worker/services/report/languages/tests/unit/test_xcode2.py b/apps/worker/services/report/languages/tests/unit/test_xcode2.py new file mode 100644 index 0000000000..a8d5cd9870 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_xcode2.py @@ -0,0 +1,65 @@ +from services.report.languages import xcode +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +txt = b"""/source: + 1| |line + 2| 1|line + ------------------ + | -[UVWelcomeViewController dealloc]: + | 0| 360| noErr OSErr: function performed properly - no error + ------------------ + 3| 0|line + +/totally_empty: +/file: + 1| |line + 2| 1k|line + warning: The file '/Users/Jack/Documents/Coupgon/sdk-ios/Source/CPGCoupgonsViewController.swift' isn't covered. + \033\x1b[0;36m/file:\033[0m + 3| 1m|line + 4| 1| } + +/ignore: + 1| 0|line +""" + + +class TestXCode2(BaseTestCase): + def test_report(self): + def fixes(path): + if path == "ignore": + return None + assert path in ("source", "file", "empty", "totally_empty") + return path + + report_builder_session = create_report_builder_session(path_fixer=fixes) + xcode.from_txt(txt, report_builder_session) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + expected_result_archive = { + "file": [ + (2, 1000, None, [[0, 1000, None, None, None]], None, None), + (3, 99999, None, [[0, 99999, None, None, None]], None, None), + ], + "source": [ + (2, 1, None, [[0, 1, None, None, None]], None, None), + (3, 0, None, [[0, 0, None, None, None]], None, None), + ], + } + + assert expected_result_archive == processed_report["archive"] + + def test_removes_last(self): + report_builder_session = create_report_builder_session() + xcode.from_txt( + b"""\nnothing\n/file:\n 1 | 1|line\n/totally_empty:""", + report_builder_session, + ) + report = report_builder_session.output_report() + processed_report = self.convert_report_to_better_readable(report) + + assert "totally_empty" not in processed_report["archive"] + assert "file" in processed_report["archive"] diff --git a/apps/worker/services/report/languages/tests/unit/test_xcodeplist.py b/apps/worker/services/report/languages/tests/unit/test_xcodeplist.py new file mode 100644 index 0000000000..8dae1dc1ae --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/test_xcodeplist.py @@ -0,0 +1,64 @@ +from pathlib import Path + +from services.report.languages import xcodeplist +from test_utils.base import BaseTestCase + +from . import create_report_builder_session + +here = Path(__file__) +folder = here.parent + +sample_small_plist = b""" + + + + quiz + + question + + + text + What does 'API' stand for? + answer + API stands for Application Programming Interface. + + + text + What's so good about pragmatic REST? + answer + It's focused on the api consumer, so it makes it easier for developers to contribute to your app library! + + + + +""" + + +class TestXCodePlist(BaseTestCase): + def readfile(self, filename, if_empty_write=None): + with open(folder / filename, "r") as r: + contents = r.read() + + # codecov: assert not covered start [FUTURE new concept] + if contents.strip() == "" and if_empty_write: + with open(folder / filename, "w+") as r: + r.write(if_empty_write) + return if_empty_write + return contents + + def test_report(self): + report_builder_session = create_report_builder_session() + xcodeplist.from_xml( + self.readfile("xccoverage.xml").encode(), report_builder_session + ) + report = report_builder_session.output_report() + _report_json, chunks, _totals = report.serialize() + + assert chunks.decode() == self.readfile("xcodeplist.txt") + + def test_detect(self): + processor = xcodeplist.XCodePlistProcessor() + assert processor.matches_content( + "content", "first_line", "/path/to/xccoverage.plist" + ) + assert processor.matches_content(sample_small_plist, "first_line", None) diff --git a/apps/worker/services/report/languages/tests/unit/xccoverage.xml b/apps/worker/services/report/languages/tests/unit/xccoverage.xml new file mode 100644 index 0000000000..ba65b9b003 --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/xccoverage.xml @@ -0,0 +1,133154 @@ + + + + + $archiver + NSKeyedArchiver + $objects + + $null + + $class + + CF$UID + 7338 + + buildableCoverageObjects + + CF$UID + 2 + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3 + + + + + $class + + CF$UID + 7337 + + buildableIdentifier + + CF$UID + 7 + + lineCoverage + + CF$UID + 6 + + name + + CF$UID + 4 + + productPath + + CF$UID + 8 + + sourceFiles + + CF$UID + 9 + + uniqueIdentifier + + CF$UID + 5 + + + SPTDataLoaderTests.xctest + 352B7887-2970-4203-BE16-7D28E4D6DEBD + 0.98645669291338578 + 050E06941A10C62100A10A0E:primary + /Users/dflems/Code/SPTDataLoader/build/Debug/SPTDataLoaderTests.xctest + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 10 + + + CF$UID + 53 + + + CF$UID + 172 + + + CF$UID + 492 + + + CF$UID + 556 + + + CF$UID + 652 + + + CF$UID + 756 + + + CF$UID + 806 + + + CF$UID + 843 + + + CF$UID + 993 + + + CF$UID + 1038 + + + CF$UID + 1075 + + + CF$UID + 1183 + + + CF$UID + 1260 + + + CF$UID + 1324 + + + CF$UID + 2142 + + + CF$UID + 2389 + + + CF$UID + 2778 + + + CF$UID + 3083 + + + CF$UID + 3149 + + + CF$UID + 3210 + + + CF$UID + 3303 + + + CF$UID + 3346 + + + CF$UID + 3643 + + + CF$UID + 3733 + + + CF$UID + 4001 + + + CF$UID + 4150 + + + CF$UID + 4189 + + + CF$UID + 4276 + + + CF$UID + 4483 + + + CF$UID + 4826 + + + CF$UID + 4886 + + + CF$UID + 5113 + + + CF$UID + 5671 + + + CF$UID + 5976 + + + CF$UID + 6177 + + + CF$UID + 6421 + + + CF$UID + 6721 + + + CF$UID + 7074 + + + CF$UID + 7164 + + + CF$UID + 7292 + + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 14 + + functions + + CF$UID + 15 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 22 + + name + + CF$UID + 11 + + uniqueIdentifier + + CF$UID + 12 + + + NSBundleMock.m + 8D2D266F-3760-4C07-A57E-7FC40ED56F1E + 1 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/NSBundleMock.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 16 + + + + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 26 + name + + CF$UID + 17 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 18 + + + -[NSBundleMock preferredLocalizations] + CB27B30C-7F6B-4EA3-A0DF-CE5A8B0D8CD3 + Xcode.SourceCodeSymbolKind.InstanceMethod + + $classes + + IDESchemeActionCodeCoverageFunction + DVTCoverageDataContainer + NSObject + + $classname + IDESchemeActionCodeCoverageFunction + + + $classes + + NSMutableArray + NSArray + NSObject + + $classname + NSMutableArray + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 23 + + + CF$UID + 25 + + + CF$UID + 26 + + + CF$UID + 27 + + + CF$UID + 28 + + + CF$UID + 29 + + + CF$UID + 30 + + + CF$UID + 31 + + + CF$UID + 32 + + + CF$UID + 33 + + + CF$UID + 34 + + + CF$UID + 35 + + + CF$UID + 36 + + + CF$UID + 37 + + + CF$UID + 38 + + + CF$UID + 39 + + + CF$UID + 40 + + + CF$UID + 41 + + + CF$UID + 42 + + + CF$UID + 43 + + + CF$UID + 44 + + + CF$UID + 45 + + + CF$UID + 46 + + + CF$UID + 47 + + + CF$UID + 48 + + + CF$UID + 49 + + + CF$UID + 50 + + + CF$UID + 51 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $classes + + DVTSourceFileLineCoverageData + NSObject + + $classname + DVTSourceFileLineCoverageData + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $classes + + IDESchemeActionCodeCoverageFile + DVTCoverageDataContainer + NSObject + + $classname + IDESchemeActionCodeCoverageFile + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 57 + + functions + + CF$UID + 58 + + lineCoverage + + CF$UID + 56 + + lines + + CF$UID + 84 + + name + + CF$UID + 54 + + uniqueIdentifier + + CF$UID + 55 + + + SPTDataLoaderRequestResponseHandlerMock.m + 328EF89B-04D5-4DEA-84C5-4F0200286CDF + 0.93333333333333335 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderRequestResponseHandlerMock.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 59 + + + CF$UID + 62 + + + CF$UID + 65 + + + CF$UID + 68 + + + CF$UID + 71 + + + CF$UID + 74 + + + CF$UID + 77 + + + CF$UID + 81 + + + + + $class + + CF$UID + 20 + + executionCount + 8 + lineCoverage + + CF$UID + 13 + + lineNumber + 40 + name + + CF$UID + 60 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 61 + + + -[SPTDataLoaderRequestResponseHandlerMock successfulResponse:] + D6EC48E3-362B-49A3-8C9C-9F5547A7D115 + + $class + + CF$UID + 20 + + executionCount + 5 + lineCoverage + + CF$UID + 13 + + lineNumber + 46 + name + + CF$UID + 63 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 64 + + + -[SPTDataLoaderRequestResponseHandlerMock failedResponse:] + DDD50670-2C3B-4483-B375-1F6BFF378698 + + $class + + CF$UID + 20 + + executionCount + 4 + lineCoverage + + CF$UID + 13 + + lineNumber + 55 + name + + CF$UID + 66 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 67 + + + -[SPTDataLoaderRequestResponseHandlerMock cancelledRequest:] + F912B898-D26E-4015-A642-38E105A0CD6F + + $class + + CF$UID + 20 + + executionCount + 3 + lineCoverage + + CF$UID + 13 + + lineNumber + 60 + name + + CF$UID + 69 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 70 + + + -[SPTDataLoaderRequestResponseHandlerMock receivedDataChunk:forResponse:] + 0D74C2C6-FA59-4B00-BD2E-CC6D49039978 + + $class + + CF$UID + 20 + + executionCount + 11 + lineCoverage + + CF$UID + 13 + + lineNumber + 66 + name + + CF$UID + 72 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 73 + + + -[SPTDataLoaderRequestResponseHandlerMock receivedInitialResponse:] + 28B741AE-3798-4358-A90E-F2D87F34FA39 + + $class + + CF$UID + 20 + + executionCount + 12 + lineCoverage + + CF$UID + 13 + + lineNumber + 72 + name + + CF$UID + 75 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 76 + + + -[SPTDataLoaderRequestResponseHandlerMock shouldAuthoriseRequest:] + 98F5D911-1B0B-463E-8FED-9B802EDF360A + + $class + + CF$UID + 20 + + executionCount + 0 + lineCoverage + + CF$UID + 80 + + lineNumber + 77 + name + + CF$UID + 78 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 79 + + + -[SPTDataLoaderRequestResponseHandlerMock authoriseRequest:] + 48F89882-8A2B-4E7D-8E7E-8DB03ECF0654 + 0.0 + + $class + + CF$UID + 20 + + executionCount + 3 + lineCoverage + + CF$UID + 13 + + lineNumber + 82 + name + + CF$UID + 82 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 83 + + + -[SPTDataLoaderRequestResponseHandlerMock needsNewBodyStream:forRequest:] + 48916F21-0511-4FC3-8B48-AC7D97DE10A7 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 85 + + + CF$UID + 86 + + + CF$UID + 87 + + + CF$UID + 88 + + + CF$UID + 89 + + + CF$UID + 90 + + + CF$UID + 91 + + + CF$UID + 92 + + + CF$UID + 93 + + + CF$UID + 94 + + + CF$UID + 95 + + + CF$UID + 96 + + + CF$UID + 97 + + + CF$UID + 98 + + + CF$UID + 99 + + + CF$UID + 100 + + + CF$UID + 101 + + + CF$UID + 102 + + + CF$UID + 103 + + + CF$UID + 104 + + + CF$UID + 105 + + + CF$UID + 106 + + + CF$UID + 107 + + + CF$UID + 108 + + + CF$UID + 109 + + + CF$UID + 110 + + + CF$UID + 111 + + + CF$UID + 112 + + + CF$UID + 113 + + + CF$UID + 114 + + + CF$UID + 115 + + + CF$UID + 116 + + + CF$UID + 117 + + + CF$UID + 118 + + + CF$UID + 119 + + + CF$UID + 120 + + + CF$UID + 121 + + + CF$UID + 122 + + + CF$UID + 123 + + + CF$UID + 124 + + + CF$UID + 125 + + + CF$UID + 126 + + + CF$UID + 127 + + + CF$UID + 128 + + + CF$UID + 129 + + + CF$UID + 130 + + + CF$UID + 131 + + + CF$UID + 132 + + + CF$UID + 133 + + + CF$UID + 137 + + + CF$UID + 138 + + + CF$UID + 139 + + + CF$UID + 140 + + + CF$UID + 141 + + + CF$UID + 142 + + + CF$UID + 143 + + + CF$UID + 144 + + + CF$UID + 145 + + + CF$UID + 146 + + + CF$UID + 147 + + + CF$UID + 148 + + + CF$UID + 149 + + + CF$UID + 150 + + + CF$UID + 151 + + + CF$UID + 152 + + + CF$UID + 153 + + + CF$UID + 154 + + + CF$UID + 155 + + + CF$UID + 156 + + + CF$UID + 157 + + + CF$UID + 158 + + + CF$UID + 159 + + + CF$UID + 160 + + + CF$UID + 161 + + + CF$UID + 162 + + + CF$UID + 163 + + + CF$UID + 164 + + + CF$UID + 165 + + + CF$UID + 166 + + + CF$UID + 167 + + + CF$UID + 168 + + + CF$UID + 169 + + + CF$UID + 170 + + + CF$UID + 171 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 134 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 135 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 49 + len + 34 + x + 5 + + + $classes + + DVTSourceFileCodeCoverageCoveredRange + DVTSourceFileCodeCoverageRange + NSObject + + $classname + DVTSourceFileCodeCoverageCoveredRange + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 12 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 12 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 12 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 176 + + functions + + CF$UID + 177 + + lineCoverage + + CF$UID + 175 + + lines + + CF$UID + 248 + + name + + CF$UID + 173 + + uniqueIdentifier + + CF$UID + 174 + + + SPTDataLoaderTest.m + 1CC687D1-B926-476D-847C-D2A3A862104E + 0.99367088607594933 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderTest.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 178 + + + CF$UID + 181 + + + CF$UID + 184 + + + CF$UID + 187 + + + CF$UID + 190 + + + CF$UID + 193 + + + CF$UID + 196 + + + CF$UID + 199 + + + CF$UID + 202 + + + CF$UID + 205 + + + CF$UID + 208 + + + CF$UID + 212 + + + CF$UID + 215 + + + CF$UID + 218 + + + CF$UID + 221 + + + CF$UID + 224 + + + CF$UID + 227 + + + CF$UID + 230 + + + CF$UID + 233 + + + CF$UID + 236 + + + CF$UID + 239 + + + CF$UID + 242 + + + CF$UID + 245 + + + + + $class + + CF$UID + 20 + + executionCount + 19 + lineCoverage + + CF$UID + 13 + + lineNumber + 49 + name + + CF$UID + 179 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 180 + + + -[SPTDataLoaderTest setUp] + D44C3FB1-36D6-4785-9292-2EBE9969F7BB + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 62 + name + + CF$UID + 182 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 183 + + + -[SPTDataLoaderTest testNotNil] + 2DE38F95-082B-4CA3-9A10-B677B6F3DB70 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 67 + name + + CF$UID + 185 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 186 + + + -[SPTDataLoaderTest testPerformRequestRelayedToRequestResponseHandlerDelegate] + CE44D289-910B-4B30-B81C-1301101C6F20 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 74 + name + + CF$UID + 188 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 189 + + + -[SPTDataLoaderTest testCancelAllLoads] + 1FCD64D8-F32B-4D8A-B42A-E74538EDAF78 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 89 + name + + CF$UID + 191 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 192 + + + -[SPTDataLoaderTest testRelaySuccessfulResponseToDelegate] + A2C8FD4F-7EF9-46E0-AA68-66107628EA6D + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 98 + name + + CF$UID + 194 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 195 + + + -[SPTDataLoaderTest testRelayFailureResponseToDelegate] + 2945081B-B24E-4456-83AE-59F26CD18B68 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 107 + name + + CF$UID + 197 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 198 + + + -[SPTDataLoaderTest testRelayCancelledRequestToDelegate] + ACB20CC1-C7A2-4BE4-B065-2B9F55DBC51D + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 115 + name + + CF$UID + 200 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 201 + + + -[SPTDataLoaderTest testRelayReceivedDataChunkToDelegate] + F10F2189-F2B8-4068-90F0-93A514502034 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 126 + name + + CF$UID + 203 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 204 + + + -[SPTDataLoaderTest testRelayReceivedInitialResponseToDelegate] + CDEFD3B8-3F2D-4A50-9DA3-18A4788860D6 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 137 + name + + CF$UID + 206 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 207 + + + -[SPTDataLoaderTest testRelayBodyStreamPromptToDelegate] + DFAE606B-9CED-411C-B5D8-DDF2CAC6A789 + + $class + + CF$UID + 20 + + executionCount + 0 + lineCoverage + + CF$UID + 80 + + lineNumber + 141 + name + + CF$UID + 209 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 210 + + + __56-[SPTDataLoaderTest testRelayBodyStreamPromptToDelegate]_block_invoke + 87C8FF73-3509-4468-ABB9-E57DA0505D62 + Xcode.SourceCodeSymbolKind.Function + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 146 + name + + CF$UID + 213 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 214 + + + -[SPTDataLoaderTest testBodyStreamPromptWithoutDelegateSupport] + 8AE01A28-3DF1-4D43-9FE0-D6A9EA331D46 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 152 + name + + CF$UID + 216 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 217 + + + __63-[SPTDataLoaderTest testBodyStreamPromptWithoutDelegateSupport]_block_invoke + F038C317-80F2-418F-ACE0-CAB335BB7E50 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 159 + name + + CF$UID + 219 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 220 + + + -[SPTDataLoaderTest testDelegateCallbackOnSeparateQueue] + 5D2D632B-4BDD-4DF0-AC82-560EB0D5EF1C + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 162 + name + + CF$UID + 222 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 223 + + + __56-[SPTDataLoaderTest testDelegateCallbackOnSeparateQueue]_block_invoke + 03F2B7CF-7A89-4D42-9A44-62834FB2F2AC + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 174 + name + + CF$UID + 225 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 226 + + + -[SPTDataLoaderTest testErrorDelegateCallbackWhenMismatchInChunkSupport] + 61A5C941-5CD4-4089-9918-E1F5084A59E2 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 182 + name + + CF$UID + 228 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 229 + + + -[SPTDataLoaderTest testNoCallsToReceiveInitialResponseIfRequestDoesNotSupportChunks] + 35C6F046-C748-4C56-B0CA-39AA535C9D35 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 192 + name + + CF$UID + 231 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 232 + + + -[SPTDataLoaderTest testSuccessfulResponseDoesNotEchoToDelegateWithUntrackedRequest] + 75F94B8B-9495-4B79-8416-701729ABB15C + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 200 + name + + CF$UID + 234 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 235 + + + -[SPTDataLoaderTest testSuccessfulResponseDoesNotEchoToDelegateWithFailedRequest] + 33330CFA-7148-49A9-97DE-89FBC44DB27A + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 208 + name + + CF$UID + 237 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 238 + + + -[SPTDataLoaderTest testSuccessfulResponseDoesNotEchoToDelegateWithCancelledRequest] + 182DB208-012D-42A3-A763-55209ED9C973 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 216 + name + + CF$UID + 240 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 241 + + + -[SPTDataLoaderTest testSuccessfulResponseDoesNotEchoToDelegateWithReceivedDataChunkRequest] + 6438C8B4-A99E-4BC0-BA04-800D33367142 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 225 + name + + CF$UID + 243 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 244 + + + -[SPTDataLoaderTest testSuccessfulResponseDoesNotEchoToDelegateWithReceivedInitialResponseRequest] + E28893E0-C987-42A8-B703-8F0D2786929D + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 233 + name + + CF$UID + 246 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 247 + + + -[SPTDataLoaderTest testCancellingCancellationTokenFiresDelegateCancelMessage] + 761DA63E-0400-432B-8EB3-1772626A8905 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 249 + + + CF$UID + 250 + + + CF$UID + 251 + + + CF$UID + 252 + + + CF$UID + 253 + + + CF$UID + 254 + + + CF$UID + 255 + + + CF$UID + 256 + + + CF$UID + 257 + + + CF$UID + 258 + + + CF$UID + 259 + + + CF$UID + 260 + + + CF$UID + 261 + + + CF$UID + 262 + + + CF$UID + 263 + + + CF$UID + 264 + + + CF$UID + 265 + + + CF$UID + 266 + + + CF$UID + 267 + + + CF$UID + 268 + + + CF$UID + 269 + + + CF$UID + 270 + + + CF$UID + 271 + + + CF$UID + 272 + + + CF$UID + 273 + + + CF$UID + 274 + + + CF$UID + 275 + + + CF$UID + 276 + + + CF$UID + 277 + + + CF$UID + 278 + + + CF$UID + 279 + + + CF$UID + 280 + + + CF$UID + 281 + + + CF$UID + 282 + + + CF$UID + 283 + + + CF$UID + 284 + + + CF$UID + 285 + + + CF$UID + 286 + + + CF$UID + 287 + + + CF$UID + 288 + + + CF$UID + 289 + + + CF$UID + 290 + + + CF$UID + 291 + + + CF$UID + 292 + + + CF$UID + 293 + + + CF$UID + 294 + + + CF$UID + 295 + + + CF$UID + 296 + + + CF$UID + 297 + + + CF$UID + 298 + + + CF$UID + 299 + + + CF$UID + 300 + + + CF$UID + 301 + + + CF$UID + 302 + + + CF$UID + 303 + + + CF$UID + 304 + + + CF$UID + 305 + + + CF$UID + 306 + + + CF$UID + 307 + + + CF$UID + 308 + + + CF$UID + 309 + + + CF$UID + 310 + + + CF$UID + 311 + + + CF$UID + 312 + + + CF$UID + 313 + + + CF$UID + 314 + + + CF$UID + 315 + + + CF$UID + 316 + + + CF$UID + 317 + + + CF$UID + 318 + + + CF$UID + 319 + + + CF$UID + 320 + + + CF$UID + 321 + + + CF$UID + 322 + + + CF$UID + 323 + + + CF$UID + 324 + + + CF$UID + 325 + + + CF$UID + 326 + + + CF$UID + 332 + + + CF$UID + 333 + + + CF$UID + 334 + + + CF$UID + 335 + + + CF$UID + 336 + + + CF$UID + 337 + + + CF$UID + 338 + + + CF$UID + 339 + + + CF$UID + 340 + + + CF$UID + 341 + + + CF$UID + 342 + + + CF$UID + 343 + + + CF$UID + 344 + + + CF$UID + 345 + + + CF$UID + 346 + + + CF$UID + 347 + + + CF$UID + 348 + + + CF$UID + 349 + + + CF$UID + 350 + + + CF$UID + 351 + + + CF$UID + 352 + + + CF$UID + 353 + + + CF$UID + 354 + + + CF$UID + 355 + + + CF$UID + 356 + + + CF$UID + 357 + + + CF$UID + 358 + + + CF$UID + 359 + + + CF$UID + 360 + + + CF$UID + 361 + + + CF$UID + 362 + + + CF$UID + 363 + + + CF$UID + 364 + + + CF$UID + 365 + + + CF$UID + 366 + + + CF$UID + 367 + + + CF$UID + 368 + + + CF$UID + 369 + + + CF$UID + 370 + + + CF$UID + 371 + + + CF$UID + 372 + + + CF$UID + 373 + + + CF$UID + 374 + + + CF$UID + 375 + + + CF$UID + 376 + + + CF$UID + 377 + + + CF$UID + 378 + + + CF$UID + 379 + + + CF$UID + 380 + + + CF$UID + 381 + + + CF$UID + 382 + + + CF$UID + 383 + + + CF$UID + 384 + + + CF$UID + 385 + + + CF$UID + 386 + + + CF$UID + 387 + + + CF$UID + 388 + + + CF$UID + 389 + + + CF$UID + 390 + + + CF$UID + 391 + + + CF$UID + 392 + + + CF$UID + 393 + + + CF$UID + 394 + + + CF$UID + 395 + + + CF$UID + 396 + + + CF$UID + 397 + + + CF$UID + 398 + + + CF$UID + 399 + + + CF$UID + 400 + + + CF$UID + 401 + + + CF$UID + 402 + + + CF$UID + 403 + + + CF$UID + 404 + + + CF$UID + 405 + + + CF$UID + 406 + + + CF$UID + 407 + + + CF$UID + 408 + + + CF$UID + 409 + + + CF$UID + 410 + + + CF$UID + 411 + + + CF$UID + 412 + + + CF$UID + 413 + + + CF$UID + 414 + + + CF$UID + 415 + + + CF$UID + 416 + + + CF$UID + 417 + + + CF$UID + 418 + + + CF$UID + 419 + + + CF$UID + 420 + + + CF$UID + 421 + + + CF$UID + 422 + + + CF$UID + 423 + + + CF$UID + 424 + + + CF$UID + 425 + + + CF$UID + 426 + + + CF$UID + 427 + + + CF$UID + 428 + + + CF$UID + 429 + + + CF$UID + 430 + + + CF$UID + 431 + + + CF$UID + 432 + + + CF$UID + 433 + + + CF$UID + 434 + + + CF$UID + 435 + + + CF$UID + 436 + + + CF$UID + 437 + + + CF$UID + 438 + + + CF$UID + 439 + + + CF$UID + 440 + + + CF$UID + 441 + + + CF$UID + 442 + + + CF$UID + 443 + + + CF$UID + 444 + + + CF$UID + 445 + + + CF$UID + 446 + + + CF$UID + 447 + + + CF$UID + 448 + + + CF$UID + 449 + + + CF$UID + 450 + + + CF$UID + 451 + + + CF$UID + 452 + + + CF$UID + 453 + + + CF$UID + 454 + + + CF$UID + 455 + + + CF$UID + 456 + + + CF$UID + 457 + + + CF$UID + 458 + + + CF$UID + 459 + + + CF$UID + 460 + + + CF$UID + 461 + + + CF$UID + 462 + + + CF$UID + 463 + + + CF$UID + 464 + + + CF$UID + 465 + + + CF$UID + 466 + + + CF$UID + 467 + + + CF$UID + 468 + + + CF$UID + 469 + + + CF$UID + 470 + + + CF$UID + 471 + + + CF$UID + 472 + + + CF$UID + 473 + + + CF$UID + 474 + + + CF$UID + 475 + + + CF$UID + 476 + + + CF$UID + 477 + + + CF$UID + 478 + + + CF$UID + 479 + + + CF$UID + 480 + + + CF$UID + 481 + + + CF$UID + 482 + + + CF$UID + 483 + + + CF$UID + 484 + + + CF$UID + 485 + + + CF$UID + 486 + + + CF$UID + 487 + + + CF$UID + 488 + + + CF$UID + 489 + + + CF$UID + 490 + + + CF$UID + 491 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 327 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 328 + + + CF$UID + 329 + + + CF$UID + 330 + + + CF$UID + 331 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 78 + len + 46 + x + 6 + + + $class + + CF$UID + 136 + + c + 47 + l + 78 + len + 2 + x + 1 + + + $class + + CF$UID + 136 + + c + 49 + l + 78 + len + 3 + x + 5 + + + $class + + CF$UID + 136 + + c + 52 + l + 78 + len + 2 + x + 1 + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 495 + + functions + + CF$UID + 496 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 506 + + name + + CF$UID + 493 + + uniqueIdentifier + + CF$UID + 494 + + + NSDictionaryHeaderSizeTest.m + 191D8186-8A4E-4D74-BB7D-64CA30B82817 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/NSDictionaryHeaderSizeTest.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 497 + + + CF$UID + 500 + + + CF$UID + 503 + + + + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 34 + name + + CF$UID + 498 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 499 + + + -[NSDictionaryHeaderSizeTest testNoSizeForNonStringKeys] + FD3A58C5-8BB5-480F-8E43-36889B1F1E10 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 40 + name + + CF$UID + 501 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 502 + + + -[NSDictionaryHeaderSizeTest testNoSizeForNonStringObjects] + 10AE5C65-63AF-4D1D-9109-95581F9F5A8A + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 46 + name + + CF$UID + 504 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 505 + + + -[NSDictionaryHeaderSizeTest testSize] + 2019AFFD-F7FC-442E-8E4D-E6A6CF079915 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 507 + + + CF$UID + 508 + + + CF$UID + 509 + + + CF$UID + 510 + + + CF$UID + 511 + + + CF$UID + 512 + + + CF$UID + 513 + + + CF$UID + 514 + + + CF$UID + 515 + + + CF$UID + 516 + + + CF$UID + 517 + + + CF$UID + 518 + + + CF$UID + 519 + + + CF$UID + 520 + + + CF$UID + 521 + + + CF$UID + 522 + + + CF$UID + 523 + + + CF$UID + 524 + + + CF$UID + 525 + + + CF$UID + 526 + + + CF$UID + 527 + + + CF$UID + 528 + + + CF$UID + 529 + + + CF$UID + 530 + + + CF$UID + 531 + + + CF$UID + 532 + + + CF$UID + 533 + + + CF$UID + 534 + + + CF$UID + 535 + + + CF$UID + 536 + + + CF$UID + 537 + + + CF$UID + 538 + + + CF$UID + 539 + + + CF$UID + 540 + + + CF$UID + 541 + + + CF$UID + 542 + + + CF$UID + 543 + + + CF$UID + 544 + + + CF$UID + 545 + + + CF$UID + 546 + + + CF$UID + 547 + + + CF$UID + 548 + + + CF$UID + 549 + + + CF$UID + 550 + + + CF$UID + 551 + + + CF$UID + 552 + + + CF$UID + 553 + + + CF$UID + 554 + + + CF$UID + 555 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 560 + + functions + + CF$UID + 561 + + lineCoverage + + CF$UID + 559 + + lines + + CF$UID + 581 + + name + + CF$UID + 557 + + uniqueIdentifier + + CF$UID + 558 + + + SPTDataLoaderAuthoriserMock.m + 8EEFDB7F-2442-43BA-AD90-C0B595C2399E + 0.66666666666666663 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderAuthoriserMock.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 562 + + + CF$UID + 566 + + + CF$UID + 569 + + + CF$UID + 572 + + + CF$UID + 575 + + + CF$UID + 578 + + + + + $class + + CF$UID + 20 + + executionCount + 23 + lineCoverage + + CF$UID + 565 + + lineNumber + 35 + name + + CF$UID + 563 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 564 + + + -[SPTDataLoaderAuthoriserMock init] + F6EAA509-F51E-4A01-A0B0-6E54DD02245C + 0.77777777777777779 + + $class + + CF$UID + 20 + + executionCount + 8 + lineCoverage + + CF$UID + 13 + + lineNumber + 46 + name + + CF$UID + 567 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 568 + + + -[SPTDataLoaderAuthoriserMock requestRequiresAuthorisation:] + 7DBB613C-D821-4F86-9AEE-2F83109CB0DE + + $class + + CF$UID + 20 + + executionCount + 3 + lineCoverage + + CF$UID + 13 + + lineNumber + 51 + name + + CF$UID + 570 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 571 + + + -[SPTDataLoaderAuthoriserMock authoriseRequest:] + 6FCFD9D5-A99E-41C1-BBFE-7C2C5BE4E680 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 57 + name + + CF$UID + 573 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 574 + + + -[SPTDataLoaderAuthoriserMock requestFailedAuthorisation:] + 7B64FE30-0753-4E08-B89B-038EE7B434E5 + + $class + + CF$UID + 20 + + executionCount + 0 + lineCoverage + + CF$UID + 80 + + lineNumber + 61 + name + + CF$UID + 576 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 577 + + + -[SPTDataLoaderAuthoriserMock copyWithZone:] + 362E5BA1-AEBC-438D-8696-9FB22DFD7EA8 + + $class + + CF$UID + 20 + + executionCount + 0 + lineCoverage + + CF$UID + 80 + + lineNumber + 66 + name + + CF$UID + 579 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 580 + + + -[SPTDataLoaderAuthoriserMock refresh] + B4B865D1-94A8-4F5C-9B4D-5BE326C5B715 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 582 + + + CF$UID + 583 + + + CF$UID + 584 + + + CF$UID + 585 + + + CF$UID + 586 + + + CF$UID + 587 + + + CF$UID + 588 + + + CF$UID + 589 + + + CF$UID + 590 + + + CF$UID + 591 + + + CF$UID + 592 + + + CF$UID + 593 + + + CF$UID + 594 + + + CF$UID + 595 + + + CF$UID + 596 + + + CF$UID + 597 + + + CF$UID + 598 + + + CF$UID + 599 + + + CF$UID + 600 + + + CF$UID + 601 + + + CF$UID + 602 + + + CF$UID + 603 + + + CF$UID + 604 + + + CF$UID + 605 + + + CF$UID + 606 + + + CF$UID + 607 + + + CF$UID + 608 + + + CF$UID + 609 + + + CF$UID + 610 + + + CF$UID + 611 + + + CF$UID + 612 + + + CF$UID + 613 + + + CF$UID + 614 + + + CF$UID + 615 + + + CF$UID + 616 + + + CF$UID + 617 + + + CF$UID + 620 + + + CF$UID + 621 + + + CF$UID + 622 + + + CF$UID + 623 + + + CF$UID + 624 + + + CF$UID + 625 + + + CF$UID + 626 + + + CF$UID + 627 + + + CF$UID + 628 + + + CF$UID + 629 + + + CF$UID + 630 + + + CF$UID + 631 + + + CF$UID + 632 + + + CF$UID + 633 + + + CF$UID + 634 + + + CF$UID + 635 + + + CF$UID + 636 + + + CF$UID + 637 + + + CF$UID + 638 + + + CF$UID + 639 + + + CF$UID + 640 + + + CF$UID + 641 + + + CF$UID + 642 + + + CF$UID + 643 + + + CF$UID + 644 + + + CF$UID + 645 + + + CF$UID + 646 + + + CF$UID + 647 + + + CF$UID + 648 + + + CF$UID + 649 + + + CF$UID + 650 + + + CF$UID + 651 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 23 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 23 + s + + CF$UID + 618 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 619 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 36 + len + 32 + x + 23 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 23 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 23 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 23 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 23 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 23 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 655 + + functions + + CF$UID + 656 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 669 + + name + + CF$UID + 653 + + uniqueIdentifier + + CF$UID + 654 + + + SPTDataLoaderExponentialTimerTest.m + 70D77CF3-003D-4806-B0D6-820CD35750A0 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderExponentialTimerTest.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 657 + + + CF$UID + 660 + + + CF$UID + 663 + + + CF$UID + 666 + + + + + $class + + CF$UID + 20 + + executionCount + 3 + lineCoverage + + CF$UID + 13 + + lineNumber + 36 + name + + CF$UID + 658 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 659 + + + -[SPTDataLoaderExponentialTimerTest setUp] + 36F38DD9-EAF4-45F7-AD40-041A95AA5662 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 44 + name + + CF$UID + 661 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 662 + + + -[SPTDataLoaderExponentialTimerTest testReset] + 64B4887D-A88C-4B16-BBA6-8AB3B2ADEFAD + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 55 + name + + CF$UID + 664 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 665 + + + -[SPTDataLoaderExponentialTimerTest testInitialTimeOfZeroResultsInZeroAlways] + D08FEC8B-548A-4C99-8E54-C9C919DD9AEE + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 65 + name + + CF$UID + 667 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 668 + + + -[SPTDataLoaderExponentialTimerTest testMaxTimeReached] + ADBE0BF1-E597-4B7D-A5F3-3ABFF7F84D85 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 670 + + + CF$UID + 671 + + + CF$UID + 672 + + + CF$UID + 673 + + + CF$UID + 674 + + + CF$UID + 675 + + + CF$UID + 676 + + + CF$UID + 677 + + + CF$UID + 678 + + + CF$UID + 679 + + + CF$UID + 680 + + + CF$UID + 681 + + + CF$UID + 682 + + + CF$UID + 683 + + + CF$UID + 684 + + + CF$UID + 685 + + + CF$UID + 686 + + + CF$UID + 687 + + + CF$UID + 688 + + + CF$UID + 689 + + + CF$UID + 690 + + + CF$UID + 691 + + + CF$UID + 692 + + + CF$UID + 693 + + + CF$UID + 694 + + + CF$UID + 695 + + + CF$UID + 696 + + + CF$UID + 697 + + + CF$UID + 698 + + + CF$UID + 699 + + + CF$UID + 700 + + + CF$UID + 701 + + + CF$UID + 702 + + + CF$UID + 703 + + + CF$UID + 704 + + + CF$UID + 705 + + + CF$UID + 706 + + + CF$UID + 707 + + + CF$UID + 708 + + + CF$UID + 709 + + + CF$UID + 710 + + + CF$UID + 711 + + + CF$UID + 712 + + + CF$UID + 713 + + + CF$UID + 714 + + + CF$UID + 715 + + + CF$UID + 721 + + + CF$UID + 722 + + + CF$UID + 723 + + + CF$UID + 724 + + + CF$UID + 725 + + + CF$UID + 726 + + + CF$UID + 727 + + + CF$UID + 728 + + + CF$UID + 729 + + + CF$UID + 730 + + + CF$UID + 731 + + + CF$UID + 732 + + + CF$UID + 738 + + + CF$UID + 739 + + + CF$UID + 740 + + + CF$UID + 741 + + + CF$UID + 742 + + + CF$UID + 743 + + + CF$UID + 744 + + + CF$UID + 745 + + + CF$UID + 746 + + + CF$UID + 752 + + + CF$UID + 753 + + + CF$UID + 754 + + + CF$UID + 755 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 716 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 717 + + + CF$UID + 718 + + + CF$UID + 719 + + + CF$UID + 720 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 46 + len + 25 + x + 4 + + + $class + + CF$UID + 136 + + c + 26 + l + 46 + len + 2 + x + 1 + + + $class + + CF$UID + 136 + + c + 28 + l + 46 + len + 3 + x + 3 + + + $class + + CF$UID + 136 + + c + 31 + l + 46 + len + 2 + x + 1 + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 733 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 734 + + + CF$UID + 735 + + + CF$UID + 736 + + + CF$UID + 737 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 58 + len + 26 + x + 11 + + + $class + + CF$UID + 136 + + c + 27 + l + 58 + len + 2 + x + 1 + + + $class + + CF$UID + 136 + + c + 29 + l + 58 + len + 3 + x + 10 + + + $class + + CF$UID + 136 + + c + 32 + l + 58 + len + 2 + x + 1 + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 101 + s + + CF$UID + 747 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 748 + + + CF$UID + 749 + + + CF$UID + 750 + + + CF$UID + 751 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 67 + len + 27 + x + 101 + + + $class + + CF$UID + 136 + + c + 28 + l + 67 + len + 2 + x + 1 + + + $class + + CF$UID + 136 + + c + 30 + l + 67 + len + 3 + x + 100 + + + $class + + CF$UID + 136 + + c + 33 + l + 67 + len + 2 + x + 1 + + + $class + + CF$UID + 24 + + c + 100 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 100 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 759 + + functions + + CF$UID + 760 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 767 + + name + + CF$UID + 757 + + uniqueIdentifier + + CF$UID + 758 + + + NSURLSessionDataTaskMock.m + ACC351CE-0897-49D5-8C24-6A89B02602C7 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/NSURLSessionDataTaskMock.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 761 + + + CF$UID + 764 + + + + + $class + + CF$UID + 20 + + executionCount + 17 + lineCoverage + + CF$UID + 13 + + lineNumber + 31 + name + + CF$UID + 762 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 763 + + + -[NSURLSessionDataTaskMock resume] + F892B20A-8E41-4C61-A611-DB9022DD32C6 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 36 + name + + CF$UID + 765 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 766 + + + -[NSURLSessionDataTaskMock cancel] + EE0F746E-37E1-4674-988D-6E680C8051C3 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 768 + + + CF$UID + 769 + + + CF$UID + 770 + + + CF$UID + 771 + + + CF$UID + 772 + + + CF$UID + 773 + + + CF$UID + 774 + + + CF$UID + 775 + + + CF$UID + 776 + + + CF$UID + 777 + + + CF$UID + 778 + + + CF$UID + 779 + + + CF$UID + 780 + + + CF$UID + 781 + + + CF$UID + 782 + + + CF$UID + 783 + + + CF$UID + 784 + + + CF$UID + 785 + + + CF$UID + 786 + + + CF$UID + 787 + + + CF$UID + 788 + + + CF$UID + 789 + + + CF$UID + 790 + + + CF$UID + 791 + + + CF$UID + 792 + + + CF$UID + 793 + + + CF$UID + 794 + + + CF$UID + 795 + + + CF$UID + 796 + + + CF$UID + 797 + + + CF$UID + 798 + + + CF$UID + 799 + + + CF$UID + 800 + + + CF$UID + 801 + + + CF$UID + 802 + + + CF$UID + 803 + + + CF$UID + 804 + + + CF$UID + 805 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 809 + + functions + + CF$UID + 810 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 814 + + name + + CF$UID + 807 + + uniqueIdentifier + + CF$UID + 808 + + + SPTDataLoaderServerTrustPolicyMock.m + E074973F-E8E3-4E12-86B6-EB7BD457ABBB + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderServerTrustPolicyMock.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 811 + + + + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 26 + name + + CF$UID + 812 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 813 + + + -[SPTDataLoaderServerTrustPolicyMock validateChallenge:] + FEEA8DEF-61F9-40D7-91A8-8FC641AA62B1 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 815 + + + CF$UID + 816 + + + CF$UID + 817 + + + CF$UID + 818 + + + CF$UID + 819 + + + CF$UID + 820 + + + CF$UID + 821 + + + CF$UID + 822 + + + CF$UID + 823 + + + CF$UID + 824 + + + CF$UID + 825 + + + CF$UID + 826 + + + CF$UID + 827 + + + CF$UID + 828 + + + CF$UID + 829 + + + CF$UID + 830 + + + CF$UID + 831 + + + CF$UID + 832 + + + CF$UID + 833 + + + CF$UID + 834 + + + CF$UID + 835 + + + CF$UID + 836 + + + CF$UID + 837 + + + CF$UID + 838 + + + CF$UID + 839 + + + CF$UID + 840 + + + CF$UID + 841 + + + CF$UID + 842 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 846 + + functions + + CF$UID + 847 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 875 + + name + + CF$UID + 844 + + uniqueIdentifier + + CF$UID + 845 + + + SPTDataLoaderRateLimiterTest.m + DAD8C1E0-2F22-44DC-855C-9B261EEABF25 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderRateLimiterTest.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 848 + + + CF$UID + 851 + + + CF$UID + 854 + + + CF$UID + 857 + + + CF$UID + 860 + + + CF$UID + 863 + + + CF$UID + 866 + + + CF$UID + 869 + + + CF$UID + 872 + + + + + $class + + CF$UID + 20 + + executionCount + 8 + lineCoverage + + CF$UID + 13 + + lineNumber + 39 + name + + CF$UID + 849 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 850 + + + -[SPTDataLoaderRateLimiterTest setUp] + 4B03DF28-964E-4FA3-B821-37065D4BF42E + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 48 + name + + CF$UID + 852 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 853 + + + -[SPTDataLoaderRateLimiterTest testNotNil] + FC9896F7-7978-4828-AFD4-7F3D63CBF821 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 53 + name + + CF$UID + 855 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 856 + + + -[SPTDataLoaderRateLimiterTest testEarliestTimeUntilRequestCanBeRealisedWithRetryAfter] + A532F22E-1E75-495E-BF05-C3E24C1ACB90 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 64 + name + + CF$UID + 858 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 859 + + + -[SPTDataLoaderRateLimiterTest testEarliestTimeUntilRequestCanBeRealised] + 2AE67178-FCD9-4EC0-8D81-E280E6166DBA + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 74 + name + + CF$UID + 861 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 862 + + + -[SPTDataLoaderRateLimiterTest testExecutedRequestWithNilRequest] + DF7406AC-B229-4FFB-8124-225D919E8FB5 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 83 + name + + CF$UID + 864 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 865 + + + -[SPTDataLoaderRateLimiterTest testRequestsPerSecondDefault] + FE64CCCD-E44D-4816-A1D8-CA930F049134 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 90 + name + + CF$UID + 867 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 868 + + + -[SPTDataLoaderRateLimiterTest testRequestsPerSecondCustom] + 9ED49275-6DBB-429E-A680-23E64C831738 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 99 + name + + CF$UID + 870 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 871 + + + -[SPTDataLoaderRateLimiterTest testSetRetryAfterWithNilURL] + 0DCDB743-2506-438B-AE6D-18FE7E77C128 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 108 + name + + CF$UID + 873 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 874 + + + -[SPTDataLoaderRateLimiterTest testResetRetryAfterAfterSuccessfulExecution] + 47349162-E560-4CD3-9211-B485CCDA809B + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 876 + + + CF$UID + 877 + + + CF$UID + 878 + + + CF$UID + 879 + + + CF$UID + 880 + + + CF$UID + 881 + + + CF$UID + 882 + + + CF$UID + 883 + + + CF$UID + 884 + + + CF$UID + 885 + + + CF$UID + 886 + + + CF$UID + 887 + + + CF$UID + 888 + + + CF$UID + 889 + + + CF$UID + 890 + + + CF$UID + 891 + + + CF$UID + 892 + + + CF$UID + 893 + + + CF$UID + 894 + + + CF$UID + 895 + + + CF$UID + 896 + + + CF$UID + 897 + + + CF$UID + 898 + + + CF$UID + 899 + + + CF$UID + 900 + + + CF$UID + 901 + + + CF$UID + 902 + + + CF$UID + 903 + + + CF$UID + 904 + + + CF$UID + 905 + + + CF$UID + 906 + + + CF$UID + 907 + + + CF$UID + 908 + + + CF$UID + 909 + + + CF$UID + 910 + + + CF$UID + 911 + + + CF$UID + 912 + + + CF$UID + 913 + + + CF$UID + 914 + + + CF$UID + 915 + + + CF$UID + 916 + + + CF$UID + 917 + + + CF$UID + 918 + + + CF$UID + 919 + + + CF$UID + 920 + + + CF$UID + 921 + + + CF$UID + 922 + + + CF$UID + 923 + + + CF$UID + 924 + + + CF$UID + 925 + + + CF$UID + 926 + + + CF$UID + 927 + + + CF$UID + 928 + + + CF$UID + 929 + + + CF$UID + 930 + + + CF$UID + 931 + + + CF$UID + 932 + + + CF$UID + 933 + + + CF$UID + 934 + + + CF$UID + 935 + + + CF$UID + 936 + + + CF$UID + 937 + + + CF$UID + 938 + + + CF$UID + 939 + + + CF$UID + 940 + + + CF$UID + 941 + + + CF$UID + 942 + + + CF$UID + 943 + + + CF$UID + 944 + + + CF$UID + 945 + + + CF$UID + 946 + + + CF$UID + 947 + + + CF$UID + 948 + + + CF$UID + 949 + + + CF$UID + 950 + + + CF$UID + 951 + + + CF$UID + 952 + + + CF$UID + 953 + + + CF$UID + 954 + + + CF$UID + 955 + + + CF$UID + 956 + + + CF$UID + 957 + + + CF$UID + 958 + + + CF$UID + 959 + + + CF$UID + 960 + + + CF$UID + 961 + + + CF$UID + 962 + + + CF$UID + 963 + + + CF$UID + 964 + + + CF$UID + 965 + + + CF$UID + 966 + + + CF$UID + 967 + + + CF$UID + 968 + + + CF$UID + 969 + + + CF$UID + 970 + + + CF$UID + 971 + + + CF$UID + 972 + + + CF$UID + 973 + + + CF$UID + 974 + + + CF$UID + 975 + + + CF$UID + 976 + + + CF$UID + 977 + + + CF$UID + 978 + + + CF$UID + 979 + + + CF$UID + 980 + + + CF$UID + 981 + + + CF$UID + 982 + + + CF$UID + 983 + + + CF$UID + 984 + + + CF$UID + 985 + + + CF$UID + 986 + + + CF$UID + 987 + + + CF$UID + 988 + + + CF$UID + 989 + + + CF$UID + 990 + + + CF$UID + 991 + + + CF$UID + 992 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 996 + + functions + + CF$UID + 997 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 1001 + + name + + CF$UID + 994 + + uniqueIdentifier + + CF$UID + 995 + + + SPTDataLoaderConsumptionObserverMock.m + 35EF0F09-2D68-4EEC-AE2F-255E0C85E8F1 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderConsumptionObserverMock.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 998 + + + + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 28 + name + + CF$UID + 999 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1000 + + + -[SPTDataLoaderConsumptionObserverMock endedRequestWithResponse:bytesDownloaded:bytesUploaded:] + 76FE97EC-CA3A-42A3-84FF-02F998D7CC33 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1002 + + + CF$UID + 1003 + + + CF$UID + 1004 + + + CF$UID + 1005 + + + CF$UID + 1006 + + + CF$UID + 1007 + + + CF$UID + 1008 + + + CF$UID + 1009 + + + CF$UID + 1010 + + + CF$UID + 1011 + + + CF$UID + 1012 + + + CF$UID + 1013 + + + CF$UID + 1014 + + + CF$UID + 1015 + + + CF$UID + 1016 + + + CF$UID + 1017 + + + CF$UID + 1018 + + + CF$UID + 1019 + + + CF$UID + 1020 + + + CF$UID + 1021 + + + CF$UID + 1022 + + + CF$UID + 1023 + + + CF$UID + 1024 + + + CF$UID + 1025 + + + CF$UID + 1026 + + + CF$UID + 1027 + + + CF$UID + 1028 + + + CF$UID + 1029 + + + CF$UID + 1030 + + + CF$UID + 1031 + + + CF$UID + 1032 + + + CF$UID + 1035 + + + CF$UID + 1036 + + + CF$UID + 1037 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 1033 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1034 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 31 + len + 35 + x + 2 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 1041 + + functions + + CF$UID + 1042 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 1046 + + name + + CF$UID + 1039 + + uniqueIdentifier + + CF$UID + 1040 + + + SPTDataLoaderCancellationTokenDelegateMock.m + 4F5F3367-DE50-4398-99FC-00CB67B4C90E + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderCancellationTokenDelegateMock.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1043 + + + + + $class + + CF$UID + 20 + + executionCount + 8 + lineCoverage + + CF$UID + 13 + + lineNumber + 26 + name + + CF$UID + 1044 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1045 + + + -[SPTDataLoaderCancellationTokenDelegateMock cancellationTokenDidCancel:] + EF9B4C0B-707A-4CF7-9780-B3FBAA472E8A + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1047 + + + CF$UID + 1048 + + + CF$UID + 1049 + + + CF$UID + 1050 + + + CF$UID + 1051 + + + CF$UID + 1052 + + + CF$UID + 1053 + + + CF$UID + 1054 + + + CF$UID + 1055 + + + CF$UID + 1056 + + + CF$UID + 1057 + + + CF$UID + 1058 + + + CF$UID + 1059 + + + CF$UID + 1060 + + + CF$UID + 1061 + + + CF$UID + 1062 + + + CF$UID + 1063 + + + CF$UID + 1064 + + + CF$UID + 1065 + + + CF$UID + 1066 + + + CF$UID + 1067 + + + CF$UID + 1068 + + + CF$UID + 1069 + + + CF$UID + 1070 + + + CF$UID + 1071 + + + CF$UID + 1072 + + + CF$UID + 1073 + + + CF$UID + 1074 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 1078 + + functions + + CF$UID + 1079 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 1104 + + name + + CF$UID + 1076 + + uniqueIdentifier + + CF$UID + 1077 + + + SPTDataLoaderDelegateMock.m + DE1AAA92-B5A5-44DF-8836-F3E1A0B267FA + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderDelegateMock.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1080 + + + CF$UID + 1083 + + + CF$UID + 1086 + + + CF$UID + 1089 + + + CF$UID + 1092 + + + CF$UID + 1095 + + + CF$UID + 1098 + + + CF$UID + 1101 + + + + + $class + + CF$UID + 20 + + executionCount + 23 + lineCoverage + + CF$UID + 13 + + lineNumber + 26 + name + + CF$UID + 1081 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1082 + + + -[SPTDataLoaderDelegateMock respondsToSelector:] + 2D5A1C06-E8F5-4AFF-ACF5-D5E4533AF2D8 + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 35 + name + + CF$UID + 1084 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1085 + + + -[SPTDataLoaderDelegateMock dataLoader:didReceiveSuccessfulResponse:] + 930619E8-179C-4FE1-97D5-EF457B7BC63D + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 43 + name + + CF$UID + 1087 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1088 + + + -[SPTDataLoaderDelegateMock dataLoader:didReceiveErrorResponse:] + 1DB74B68-2BA4-48E5-BAF0-B91A7106967A + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 48 + name + + CF$UID + 1090 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1091 + + + -[SPTDataLoaderDelegateMock dataLoader:didCancelRequest:] + CD719812-CAD8-4620-A8CC-BDE46C3FE0AE + + $class + + CF$UID + 20 + + executionCount + 17 + lineCoverage + + CF$UID + 13 + + lineNumber + 53 + name + + CF$UID + 1093 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1094 + + + -[SPTDataLoaderDelegateMock dataLoaderShouldSupportChunks:] + B403796D-4DB0-4480-B3A6-FEC143F0C3A4 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 60 + name + + CF$UID + 1096 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1097 + + + -[SPTDataLoaderDelegateMock dataLoader:didReceiveDataChunk:forResponse:] + E535C138-50C8-4C01-A4D2-34B014289F80 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 65 + name + + CF$UID + 1099 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1100 + + + -[SPTDataLoaderDelegateMock dataLoader:didReceiveInitialResponse:] + 0C124EFE-1D4E-4460-B461-AFE660C6CAA4 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 70 + name + + CF$UID + 1102 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1103 + + + -[SPTDataLoaderDelegateMock dataLoader:needsNewBodyStream:forRequest:] + 60F49A44-986C-4CE4-BCCA-12FDD25D2D70 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1105 + + + CF$UID + 1106 + + + CF$UID + 1107 + + + CF$UID + 1108 + + + CF$UID + 1109 + + + CF$UID + 1110 + + + CF$UID + 1111 + + + CF$UID + 1112 + + + CF$UID + 1113 + + + CF$UID + 1114 + + + CF$UID + 1115 + + + CF$UID + 1116 + + + CF$UID + 1117 + + + CF$UID + 1118 + + + CF$UID + 1119 + + + CF$UID + 1120 + + + CF$UID + 1121 + + + CF$UID + 1122 + + + CF$UID + 1123 + + + CF$UID + 1124 + + + CF$UID + 1125 + + + CF$UID + 1126 + + + CF$UID + 1127 + + + CF$UID + 1128 + + + CF$UID + 1129 + + + CF$UID + 1130 + + + CF$UID + 1131 + + + CF$UID + 1134 + + + CF$UID + 1135 + + + CF$UID + 1138 + + + CF$UID + 1139 + + + CF$UID + 1140 + + + CF$UID + 1141 + + + CF$UID + 1142 + + + CF$UID + 1143 + + + CF$UID + 1144 + + + CF$UID + 1145 + + + CF$UID + 1148 + + + CF$UID + 1149 + + + CF$UID + 1150 + + + CF$UID + 1151 + + + CF$UID + 1152 + + + CF$UID + 1153 + + + CF$UID + 1154 + + + CF$UID + 1155 + + + CF$UID + 1156 + + + CF$UID + 1157 + + + CF$UID + 1158 + + + CF$UID + 1159 + + + CF$UID + 1160 + + + CF$UID + 1161 + + + CF$UID + 1162 + + + CF$UID + 1163 + + + CF$UID + 1164 + + + CF$UID + 1165 + + + CF$UID + 1166 + + + CF$UID + 1167 + + + CF$UID + 1168 + + + CF$UID + 1169 + + + CF$UID + 1170 + + + CF$UID + 1171 + + + CF$UID + 1172 + + + CF$UID + 1173 + + + CF$UID + 1174 + + + CF$UID + 1175 + + + CF$UID + 1176 + + + CF$UID + 1177 + + + CF$UID + 1178 + + + CF$UID + 1179 + + + CF$UID + 1180 + + + CF$UID + 1181 + + + CF$UID + 1182 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 23 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 23 + s + + CF$UID + 1132 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1133 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 27 + len + 75 + x + 23 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 21 + s + + CF$UID + 1136 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1137 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 29 + len + 11 + x + 23 + + + $class + + CF$UID + 24 + + c + 21 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 21 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 23 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 1146 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1147 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 37 + len + 38 + x + 2 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 1186 + + functions + + CF$UID + 1187 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 1197 + + name + + CF$UID + 1184 + + uniqueIdentifier + + CF$UID + 1185 + + + SPTDataLoaderCancellationTokenImplementationTest.m + 5EFB4C1F-DAA6-4236-AA48-08448846976F + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderCancellationTokenImplementationTest.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1188 + + + CF$UID + 1191 + + + CF$UID + 1194 + + + + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 40 + name + + CF$UID + 1189 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1190 + + + -[SPTDataLoaderCancellationTokenImplementationTest setUp] + A230E438-D6FF-43BB-B857-944F0FB41C3D + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 49 + name + + CF$UID + 1192 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1193 + + + -[SPTDataLoaderCancellationTokenImplementationTest testCancel] + 1E2F372D-E631-4523-A03A-A243D5648B4F + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 57 + name + + CF$UID + 1195 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1196 + + + -[SPTDataLoaderCancellationTokenImplementationTest testMultipleCancelsOnlyMakeOneDelegateCall] + BD3E157C-6885-4CFE-B654-7F1319087D21 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1198 + + + CF$UID + 1199 + + + CF$UID + 1200 + + + CF$UID + 1201 + + + CF$UID + 1202 + + + CF$UID + 1203 + + + CF$UID + 1204 + + + CF$UID + 1205 + + + CF$UID + 1206 + + + CF$UID + 1207 + + + CF$UID + 1208 + + + CF$UID + 1209 + + + CF$UID + 1210 + + + CF$UID + 1211 + + + CF$UID + 1212 + + + CF$UID + 1213 + + + CF$UID + 1214 + + + CF$UID + 1215 + + + CF$UID + 1216 + + + CF$UID + 1217 + + + CF$UID + 1218 + + + CF$UID + 1219 + + + CF$UID + 1220 + + + CF$UID + 1221 + + + CF$UID + 1222 + + + CF$UID + 1223 + + + CF$UID + 1224 + + + CF$UID + 1225 + + + CF$UID + 1226 + + + CF$UID + 1227 + + + CF$UID + 1228 + + + CF$UID + 1229 + + + CF$UID + 1230 + + + CF$UID + 1231 + + + CF$UID + 1232 + + + CF$UID + 1233 + + + CF$UID + 1234 + + + CF$UID + 1235 + + + CF$UID + 1236 + + + CF$UID + 1237 + + + CF$UID + 1238 + + + CF$UID + 1239 + + + CF$UID + 1240 + + + CF$UID + 1241 + + + CF$UID + 1242 + + + CF$UID + 1243 + + + CF$UID + 1244 + + + CF$UID + 1245 + + + CF$UID + 1246 + + + CF$UID + 1247 + + + CF$UID + 1248 + + + CF$UID + 1249 + + + CF$UID + 1250 + + + CF$UID + 1251 + + + CF$UID + 1252 + + + CF$UID + 1253 + + + CF$UID + 1254 + + + CF$UID + 1255 + + + CF$UID + 1256 + + + CF$UID + 1257 + + + CF$UID + 1258 + + + CF$UID + 1259 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 1263 + + functions + + CF$UID + 1264 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 1271 + + name + + CF$UID + 1261 + + uniqueIdentifier + + CF$UID + 1262 + + + SPTDataLoaderCancellationTokenFactoryImplementationTest.m + CD50112D-E9A2-4BD6-B050-0F6666320616 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderCancellationTokenFactoryImplementationTest.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1265 + + + CF$UID + 1268 + + + + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 38 + name + + CF$UID + 1266 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1267 + + + -[SPTDataLoaderCancellationTokenFactoryImplementationTest setUp] + A79F8A91-EEC3-4F42-A079-BE2A416443F4 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 46 + name + + CF$UID + 1269 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1270 + + + -[SPTDataLoaderCancellationTokenFactoryImplementationTest testCreateCancellationToken] + 657F2E12-61D1-4726-B95C-DA67B79CCDB2 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1272 + + + CF$UID + 1273 + + + CF$UID + 1274 + + + CF$UID + 1275 + + + CF$UID + 1276 + + + CF$UID + 1277 + + + CF$UID + 1278 + + + CF$UID + 1279 + + + CF$UID + 1280 + + + CF$UID + 1281 + + + CF$UID + 1282 + + + CF$UID + 1283 + + + CF$UID + 1284 + + + CF$UID + 1285 + + + CF$UID + 1286 + + + CF$UID + 1287 + + + CF$UID + 1288 + + + CF$UID + 1289 + + + CF$UID + 1290 + + + CF$UID + 1291 + + + CF$UID + 1292 + + + CF$UID + 1293 + + + CF$UID + 1294 + + + CF$UID + 1295 + + + CF$UID + 1296 + + + CF$UID + 1297 + + + CF$UID + 1298 + + + CF$UID + 1299 + + + CF$UID + 1300 + + + CF$UID + 1301 + + + CF$UID + 1302 + + + CF$UID + 1303 + + + CF$UID + 1304 + + + CF$UID + 1305 + + + CF$UID + 1306 + + + CF$UID + 1307 + + + CF$UID + 1308 + + + CF$UID + 1309 + + + CF$UID + 1310 + + + CF$UID + 1311 + + + CF$UID + 1312 + + + CF$UID + 1313 + + + CF$UID + 1314 + + + CF$UID + 1315 + + + CF$UID + 1316 + + + CF$UID + 1317 + + + CF$UID + 1318 + + + CF$UID + 1319 + + + CF$UID + 1320 + + + CF$UID + 1321 + + + CF$UID + 1322 + + + CF$UID + 1323 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 1328 + + functions + + CF$UID + 1329 + + lineCoverage + + CF$UID + 1327 + + lines + + CF$UID + 1472 + + name + + CF$UID + 1325 + + uniqueIdentifier + + CF$UID + 1326 + + + SPTDataLoaderServiceTest.m + 0CEED6DB-3259-49C6-A10A-968534892C63 + 0.99461400359066432 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderServiceTest.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1330 + + + CF$UID + 1333 + + + CF$UID + 1336 + + + CF$UID + 1339 + + + CF$UID + 1342 + + + CF$UID + 1345 + + + CF$UID + 1348 + + + CF$UID + 1351 + + + CF$UID + 1354 + + + CF$UID + 1357 + + + CF$UID + 1360 + + + CF$UID + 1363 + + + CF$UID + 1366 + + + CF$UID + 1369 + + + CF$UID + 1372 + + + CF$UID + 1375 + + + CF$UID + 1378 + + + CF$UID + 1382 + + + CF$UID + 1385 + + + CF$UID + 1388 + + + CF$UID + 1391 + + + CF$UID + 1394 + + + CF$UID + 1397 + + + CF$UID + 1400 + + + CF$UID + 1403 + + + CF$UID + 1406 + + + CF$UID + 1409 + + + CF$UID + 1412 + + + CF$UID + 1415 + + + CF$UID + 1418 + + + CF$UID + 1421 + + + CF$UID + 1424 + + + CF$UID + 1427 + + + CF$UID + 1430 + + + CF$UID + 1433 + + + CF$UID + 1436 + + + CF$UID + 1439 + + + CF$UID + 1442 + + + CF$UID + 1445 + + + CF$UID + 1448 + + + CF$UID + 1451 + + + CF$UID + 1454 + + + CF$UID + 1457 + + + CF$UID + 1460 + + + CF$UID + 1463 + + + CF$UID + 1466 + + + CF$UID + 1469 + + + + + $class + + CF$UID + 20 + + executionCount + 32 + lineCoverage + + CF$UID + 13 + + lineNumber + 71 + name + + CF$UID + 1331 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1332 + + + -[SPTDataLoaderServiceTest setUp] + 6748F2AF-DA16-40E8-8F08-D91381BFDE49 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 86 + name + + CF$UID + 1334 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1335 + + + -[SPTDataLoaderServiceTest testNotNil] + D0DF613A-0526-4538-A7BF-D9ADCE8540AD + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 91 + name + + CF$UID + 1337 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1338 + + + -[SPTDataLoaderServiceTest testFactoryNotNil] + 5026972E-7AE8-4C7D-AB4B-4AD8723D35D5 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 97 + name + + CF$UID + 1340 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1341 + + + -[SPTDataLoaderServiceTest testNoOperationForTask] + 07A3DEEB-2648-44D6-97AA-BA7CCDAD826B + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 102 + name + + CF$UID + 1343 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 1344 + + + __50-[SPTDataLoaderServiceTest testNoOperationForTask]_block_invoke + 6C2BF8D1-25DB-4841-85C7-04252BA840BE + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 106 + name + + CF$UID + 1346 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1347 + + + -[SPTDataLoaderServiceTest testOperationForTaskWithValidTask] + AAB10C5D-9C1B-435B-8338-C4DEE7012633 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 118 + name + + CF$UID + 1349 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 1350 + + + __61-[SPTDataLoaderServiceTest testOperationForTaskWithValidTask]_block_invoke + 8A57022A-868C-437E-BB65-D1F2DD37FEAB + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 122 + name + + CF$UID + 1352 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1353 + + + -[SPTDataLoaderServiceTest testResolverChangingAddress] + D17C1F8B-50E5-4A98-B23C-6C05E8128B43 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 135 + name + + CF$UID + 1355 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1356 + + + -[SPTDataLoaderServiceTest testAuthenticatingRequest] + A13E4D3A-4213-4EF5-808F-B1BF3D098E30 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 145 + name + + CF$UID + 1358 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1359 + + + -[SPTDataLoaderServiceTest testRequestAuthorised] + 74F7D43A-1416-48D1-8B71-4A299D2B6E81 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 154 + name + + CF$UID + 1361 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1362 + + + -[SPTDataLoaderServiceTest testRequestAuthorisationFailed] + 29342C9B-ACA4-4E34-9F30-11F3FD8E5E8A + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 163 + name + + CF$UID + 1364 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1365 + + + -[SPTDataLoaderServiceTest testSessionDidReceiveResponse] + DC8125B9-B665-44A1-A7F9-D3B172DF14E5 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 172 + name + + CF$UID + 1367 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 1368 + + + __57-[SPTDataLoaderServiceTest testSessionDidReceiveResponse]_block_invoke + D64F16E7-B4C9-4CAB-B799-C0D706410CE2 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 180 + name + + CF$UID + 1370 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1371 + + + -[SPTDataLoaderServiceTest testRedirectionCallbackAbortsTooManyRedirects] + 5D161ED0-C6D9-40C6-BFD2-866E7E317CD5 + + $class + + CF$UID + 20 + + executionCount + 11 + lineCoverage + + CF$UID + 13 + + lineNumber + 196 + name + + CF$UID + 1373 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 1374 + + + __73-[SPTDataLoaderServiceTest testRedirectionCallbackAbortsTooManyRedirects]_block_invoke + 6671BF2E-599F-4CEA-91A9-228F9790BCDC + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 227 + name + + CF$UID + 1376 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1377 + + + -[SPTDataLoaderServiceTest testRedirectionCallbackDoesNotAbortAfterFewRedirects] + 3F640F77-4EA3-46EE-B5F9-591FF481407E + + $class + + CF$UID + 20 + + executionCount + 3 + lineCoverage + + CF$UID + 1381 + + lineNumber + 243 + name + + CF$UID + 1379 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 1380 + + + __80-[SPTDataLoaderServiceTest testRedirectionCallbackDoesNotAbortAfterFewRedirects]_block_invoke + B583C19A-1E8C-4BA8-95FD-FA1FE18B26A0 + 0.88888888888888884 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 270 + name + + CF$UID + 1383 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1384 + + + -[SPTDataLoaderServiceTest testSwitchingToDownloadTask] + 7A1CB57E-4F32-4945-BE8B-7CA35220F52B + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 276 + name + + CF$UID + 1386 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1387 + + + -[SPTDataLoaderServiceTest testSessionDidReceiveData] + EA1F3F51-14BC-4C39-AB53-D3AD9891FF59 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 288 + name + + CF$UID + 1389 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1390 + + + -[SPTDataLoaderServiceTest testSessionDidComplete] + 1662437B-4505-463D-9E23-C67F0FC2357A + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 298 + name + + CF$UID + 1392 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1393 + + + -[SPTDataLoaderServiceTest testSessionWillCacheResponse] + 955FD397-475B-47BE-A4D3-F9C4A0ECAE5B + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 309 + name + + CF$UID + 1395 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 1396 + + + __56-[SPTDataLoaderServiceTest testSessionWillCacheResponse]_block_invoke + 04025A82-03BB-43AE-BC58-58D16EB8DD27 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 317 + name + + CF$UID + 1398 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1399 + + + -[SPTDataLoaderServiceTest testSessionWillNotCacheResponse] + E003106C-A96B-4E74-893C-D0726EA1D44F + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 329 + name + + CF$UID + 1401 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 1402 + + + __59-[SPTDataLoaderServiceTest testSessionWillNotCacheResponse]_block_invoke + 7413AEFD-8F67-42B2-86E3-156184F5BADB + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 337 + name + + CF$UID + 1404 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1405 + + + -[SPTDataLoaderServiceTest testConsumptionObserverCalled] + 6F13350C-F7D5-4D6F-9C85-1CF34FC89C17 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 357 + name + + CF$UID + 1407 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1408 + + + -[SPTDataLoaderServiceTest testAllowingAllCertificates] + F24CE6BF-59C1-49AA-B8E6-7EEED523B00F + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 363 + name + + CF$UID + 1410 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 1411 + + + __55-[SPTDataLoaderServiceTest testAllowingAllCertificates]_block_invoke + 450A4E1A-C8CE-4F4D-9246-394DC208EACF + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 379 + name + + CF$UID + 1413 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1414 + + + -[SPTDataLoaderServiceTest testDidReceiveChallengeWithEmptyCompletionHandlerDoesNotCrash] + 0EAD572B-B0C1-4347-B1E0-7A8106D8657E + + $class + + CF$UID + 20 + + executionCount + 0 + lineCoverage + + CF$UID + 80 + + lineNumber + 383 + name + + CF$UID + 1416 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 1417 + + + __89-[SPTDataLoaderServiceTest testDidReceiveChallengeWithEmptyCompletionHandlerDoesNotCrash]_block_invoke + 73A5C313-87F6-44EE-BB52-672BD34E4285 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 392 + name + + CF$UID + 1419 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1420 + + + -[SPTDataLoaderServiceTest testCancellingLoads] + 911B72E6-B533-4E35-9F05-FB0E7D0B6C61 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 406 + name + + CF$UID + 1422 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1423 + + + -[SPTDataLoaderServiceTest testDidReceiveChallengeWhenNotAllowingAllCertificatesForwardsResponsiblity] + A5F107BA-4EC4-4375-835F-18DCD1FBE3C5 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 411 + name + + CF$UID + 1425 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 1426 + + + __102-[SPTDataLoaderServiceTest testDidReceiveChallengeWhenNotAllowingAllCertificatesForwardsResponsiblity]_block_invoke + CE3736C9-14AB-436B-B4C2-5BF2BF28234E + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 422 + name + + CF$UID + 1428 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1429 + + + -[SPTDataLoaderServiceTest testServerTrustPolicyProvidesProperDispositionAndURLCredentialWhenDidReceiveChallenge] + 320E974D-8205-41DA-8890-58ADD33A5DB1 + + $class + + CF$UID + 20 + + executionCount + 3 + lineCoverage + + CF$UID + 13 + + lineNumber + 434 + name + + CF$UID + 1431 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 1432 + + + __113-[SPTDataLoaderServiceTest testServerTrustPolicyProvidesProperDispositionAndURLCredentialWhenDidReceiveChallenge]_block_invoke + 2552DAD8-69A9-4CF1-B2BE-BC9B1F2CC82E + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 473 + name + + CF$UID + 1434 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1435 + + + -[SPTDataLoaderServiceTest testWillCacheResponseWithNilCompletionHandler] + 1045EA90-5D6B-4872-AB8E-9FDC3CD8F3A4 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 483 + name + + CF$UID + 1437 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1438 + + + -[SPTDataLoaderServiceTest testConsumptionObserverTakesIntoAccountResponseHeaders] + A2E74DE4-7082-4DF6-95DD-43AB23E6E91B + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 486 + name + + CF$UID + 1440 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 1441 + + + __82-[SPTDataLoaderServiceTest testConsumptionObserverTakesIntoAccountResponseHeaders]_block_invoke + BD03DBF9-D810-48AF-A355-D655BEA01BD3 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 512 + name + + CF$UID + 1443 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1444 + + + -[SPTDataLoaderServiceTest testRedirectionToDifferentHostWithHeaders] + 40D64386-86B1-436A-A351-9FB5AE39E403 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 536 + name + + CF$UID + 1446 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 1447 + + + __69-[SPTDataLoaderServiceTest testRedirectionToDifferentHostWithHeaders]_block_invoke + 40A42E07-CFC0-4F3E-8F6E-CB95B08CD59C + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 546 + name + + CF$UID + 1449 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1450 + + + -[SPTDataLoaderServiceTest testDoNotPerformRequestThatIsAlreadyCancelled] + 2FF7574D-5FCF-4FF5-96A8-63B4FDC9D845 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 560 + name + + CF$UID + 1452 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1453 + + + -[SPTDataLoaderServiceTest testDoNotPerformRequestThatHasNoURL] + 4D4ED17B-1623-4017-B25C-A0A23A9C72DF + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 568 + name + + CF$UID + 1455 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1456 + + + -[SPTDataLoaderServiceTest testCancellingRequestFromHandler] + 4AB254E3-5CA7-42E1-B63E-D80B58D86401 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 581 + name + + CF$UID + 1458 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1459 + + + -[SPTDataLoaderServiceTest testNotRemovingHandlerIfRetrying] + 80CB1249-DCB9-4829-9C4F-1C5A83B0A983 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 599 + name + + CF$UID + 1461 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1462 + + + -[SPTDataLoaderServiceTest testRecreateTaskOnDidComplete] + 1BD70FDA-C97F-4D41-A598-AFB0713CA8CE + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 618 + name + + CF$UID + 1464 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1465 + + + -[SPTDataLoaderServiceTest testDoNotRecreateTaskWhenNoHandlerAssociatedWithTask] + 0738FB42-7EB8-48D8-B5EE-DE5B81238302 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 637 + name + + CF$UID + 1467 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 1468 + + + -[SPTDataLoaderServiceTest testProvidingNewBodyStream] + E0F4600C-DA99-4412-9527-03FE076C5D37 + + $class + + CF$UID + 20 + + executionCount + 0 + lineCoverage + + CF$UID + 80 + + lineNumber + 642 + name + + CF$UID + 1470 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 1471 + + + __54-[SPTDataLoaderServiceTest testProvidingNewBodyStream]_block_invoke + 9A74230A-7F6D-4892-AC19-6E925E7845D4 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1473 + + + CF$UID + 1474 + + + CF$UID + 1475 + + + CF$UID + 1476 + + + CF$UID + 1477 + + + CF$UID + 1478 + + + CF$UID + 1479 + + + CF$UID + 1480 + + + CF$UID + 1481 + + + CF$UID + 1482 + + + CF$UID + 1483 + + + CF$UID + 1484 + + + CF$UID + 1485 + + + CF$UID + 1486 + + + CF$UID + 1487 + + + CF$UID + 1488 + + + CF$UID + 1489 + + + CF$UID + 1490 + + + CF$UID + 1491 + + + CF$UID + 1492 + + + CF$UID + 1493 + + + CF$UID + 1494 + + + CF$UID + 1495 + + + CF$UID + 1496 + + + CF$UID + 1497 + + + CF$UID + 1498 + + + CF$UID + 1499 + + + CF$UID + 1500 + + + CF$UID + 1501 + + + CF$UID + 1502 + + + CF$UID + 1503 + + + CF$UID + 1504 + + + CF$UID + 1505 + + + CF$UID + 1506 + + + CF$UID + 1507 + + + CF$UID + 1508 + + + CF$UID + 1509 + + + CF$UID + 1510 + + + CF$UID + 1511 + + + CF$UID + 1512 + + + CF$UID + 1513 + + + CF$UID + 1514 + + + CF$UID + 1515 + + + CF$UID + 1516 + + + CF$UID + 1517 + + + CF$UID + 1518 + + + CF$UID + 1519 + + + CF$UID + 1520 + + + CF$UID + 1521 + + + CF$UID + 1522 + + + CF$UID + 1523 + + + CF$UID + 1524 + + + CF$UID + 1525 + + + CF$UID + 1526 + + + CF$UID + 1527 + + + CF$UID + 1528 + + + CF$UID + 1529 + + + CF$UID + 1530 + + + CF$UID + 1531 + + + CF$UID + 1532 + + + CF$UID + 1533 + + + CF$UID + 1534 + + + CF$UID + 1535 + + + CF$UID + 1536 + + + CF$UID + 1537 + + + CF$UID + 1538 + + + CF$UID + 1539 + + + CF$UID + 1540 + + + CF$UID + 1541 + + + CF$UID + 1542 + + + CF$UID + 1543 + + + CF$UID + 1544 + + + CF$UID + 1545 + + + CF$UID + 1546 + + + CF$UID + 1547 + + + CF$UID + 1548 + + + CF$UID + 1549 + + + CF$UID + 1550 + + + CF$UID + 1551 + + + CF$UID + 1552 + + + CF$UID + 1553 + + + CF$UID + 1554 + + + CF$UID + 1555 + + + CF$UID + 1556 + + + CF$UID + 1557 + + + CF$UID + 1558 + + + CF$UID + 1559 + + + CF$UID + 1560 + + + CF$UID + 1561 + + + CF$UID + 1562 + + + CF$UID + 1563 + + + CF$UID + 1564 + + + CF$UID + 1565 + + + CF$UID + 1566 + + + CF$UID + 1567 + + + CF$UID + 1568 + + + CF$UID + 1569 + + + CF$UID + 1570 + + + CF$UID + 1571 + + + CF$UID + 1572 + + + CF$UID + 1573 + + + CF$UID + 1574 + + + CF$UID + 1577 + + + CF$UID + 1578 + + + CF$UID + 1579 + + + CF$UID + 1580 + + + CF$UID + 1581 + + + CF$UID + 1582 + + + CF$UID + 1583 + + + CF$UID + 1584 + + + CF$UID + 1585 + + + CF$UID + 1586 + + + CF$UID + 1587 + + + CF$UID + 1588 + + + CF$UID + 1589 + + + CF$UID + 1590 + + + CF$UID + 1591 + + + CF$UID + 1592 + + + CF$UID + 1595 + + + CF$UID + 1596 + + + CF$UID + 1597 + + + CF$UID + 1598 + + + CF$UID + 1599 + + + CF$UID + 1600 + + + CF$UID + 1601 + + + CF$UID + 1602 + + + CF$UID + 1603 + + + CF$UID + 1604 + + + CF$UID + 1605 + + + CF$UID + 1606 + + + CF$UID + 1607 + + + CF$UID + 1608 + + + CF$UID + 1609 + + + CF$UID + 1610 + + + CF$UID + 1611 + + + CF$UID + 1612 + + + CF$UID + 1613 + + + CF$UID + 1614 + + + CF$UID + 1615 + + + CF$UID + 1616 + + + CF$UID + 1617 + + + CF$UID + 1618 + + + CF$UID + 1619 + + + CF$UID + 1620 + + + CF$UID + 1621 + + + CF$UID + 1622 + + + CF$UID + 1623 + + + CF$UID + 1624 + + + CF$UID + 1625 + + + CF$UID + 1626 + + + CF$UID + 1627 + + + CF$UID + 1628 + + + CF$UID + 1629 + + + CF$UID + 1630 + + + CF$UID + 1631 + + + CF$UID + 1632 + + + CF$UID + 1633 + + + CF$UID + 1634 + + + CF$UID + 1635 + + + CF$UID + 1636 + + + CF$UID + 1637 + + + CF$UID + 1638 + + + CF$UID + 1639 + + + CF$UID + 1640 + + + CF$UID + 1641 + + + CF$UID + 1642 + + + CF$UID + 1643 + + + CF$UID + 1644 + + + CF$UID + 1645 + + + CF$UID + 1646 + + + CF$UID + 1647 + + + CF$UID + 1648 + + + CF$UID + 1649 + + + CF$UID + 1650 + + + CF$UID + 1651 + + + CF$UID + 1652 + + + CF$UID + 1653 + + + CF$UID + 1654 + + + CF$UID + 1655 + + + CF$UID + 1656 + + + CF$UID + 1657 + + + CF$UID + 1658 + + + CF$UID + 1659 + + + CF$UID + 1660 + + + CF$UID + 1661 + + + CF$UID + 1662 + + + CF$UID + 1663 + + + CF$UID + 1664 + + + CF$UID + 1665 + + + CF$UID + 1666 + + + CF$UID + 1667 + + + CF$UID + 1668 + + + CF$UID + 1669 + + + CF$UID + 1670 + + + CF$UID + 1671 + + + CF$UID + 1672 + + + CF$UID + 1673 + + + CF$UID + 1674 + + + CF$UID + 1675 + + + CF$UID + 1678 + + + CF$UID + 1679 + + + CF$UID + 1682 + + + CF$UID + 1683 + + + CF$UID + 1684 + + + CF$UID + 1685 + + + CF$UID + 1686 + + + CF$UID + 1687 + + + CF$UID + 1688 + + + CF$UID + 1689 + + + CF$UID + 1695 + + + CF$UID + 1696 + + + CF$UID + 1697 + + + CF$UID + 1698 + + + CF$UID + 1699 + + + CF$UID + 1700 + + + CF$UID + 1701 + + + CF$UID + 1702 + + + CF$UID + 1705 + + + CF$UID + 1706 + + + CF$UID + 1707 + + + CF$UID + 1708 + + + CF$UID + 1709 + + + CF$UID + 1710 + + + CF$UID + 1711 + + + CF$UID + 1712 + + + CF$UID + 1713 + + + CF$UID + 1714 + + + CF$UID + 1715 + + + CF$UID + 1716 + + + CF$UID + 1717 + + + CF$UID + 1718 + + + CF$UID + 1719 + + + CF$UID + 1720 + + + CF$UID + 1721 + + + CF$UID + 1722 + + + CF$UID + 1723 + + + CF$UID + 1724 + + + CF$UID + 1725 + + + CF$UID + 1726 + + + CF$UID + 1727 + + + CF$UID + 1728 + + + CF$UID + 1729 + + + CF$UID + 1730 + + + CF$UID + 1731 + + + CF$UID + 1732 + + + CF$UID + 1733 + + + CF$UID + 1736 + + + CF$UID + 1737 + + + CF$UID + 1738 + + + CF$UID + 1739 + + + CF$UID + 1740 + + + CF$UID + 1741 + + + CF$UID + 1742 + + + CF$UID + 1743 + + + CF$UID + 1744 + + + CF$UID + 1745 + + + CF$UID + 1751 + + + CF$UID + 1752 + + + CF$UID + 1753 + + + CF$UID + 1754 + + + CF$UID + 1755 + + + CF$UID + 1756 + + + CF$UID + 1757 + + + CF$UID + 1758 + + + CF$UID + 1759 + + + CF$UID + 1760 + + + CF$UID + 1761 + + + CF$UID + 1762 + + + CF$UID + 1763 + + + CF$UID + 1764 + + + CF$UID + 1765 + + + CF$UID + 1766 + + + CF$UID + 1767 + + + CF$UID + 1768 + + + CF$UID + 1769 + + + CF$UID + 1770 + + + CF$UID + 1771 + + + CF$UID + 1772 + + + CF$UID + 1773 + + + CF$UID + 1774 + + + CF$UID + 1775 + + + CF$UID + 1776 + + + CF$UID + 1777 + + + CF$UID + 1778 + + + CF$UID + 1779 + + + CF$UID + 1780 + + + CF$UID + 1781 + + + CF$UID + 1782 + + + CF$UID + 1783 + + + CF$UID + 1784 + + + CF$UID + 1785 + + + CF$UID + 1786 + + + CF$UID + 1787 + + + CF$UID + 1788 + + + CF$UID + 1789 + + + CF$UID + 1790 + + + CF$UID + 1791 + + + CF$UID + 1792 + + + CF$UID + 1793 + + + CF$UID + 1794 + + + CF$UID + 1795 + + + CF$UID + 1796 + + + CF$UID + 1797 + + + CF$UID + 1798 + + + CF$UID + 1799 + + + CF$UID + 1800 + + + CF$UID + 1801 + + + CF$UID + 1802 + + + CF$UID + 1803 + + + CF$UID + 1804 + + + CF$UID + 1805 + + + CF$UID + 1806 + + + CF$UID + 1807 + + + CF$UID + 1808 + + + CF$UID + 1809 + + + CF$UID + 1810 + + + CF$UID + 1811 + + + CF$UID + 1812 + + + CF$UID + 1813 + + + CF$UID + 1814 + + + CF$UID + 1815 + + + CF$UID + 1816 + + + CF$UID + 1817 + + + CF$UID + 1818 + + + CF$UID + 1819 + + + CF$UID + 1820 + + + CF$UID + 1821 + + + CF$UID + 1822 + + + CF$UID + 1823 + + + CF$UID + 1824 + + + CF$UID + 1825 + + + CF$UID + 1826 + + + CF$UID + 1827 + + + CF$UID + 1828 + + + CF$UID + 1829 + + + CF$UID + 1830 + + + CF$UID + 1831 + + + CF$UID + 1832 + + + CF$UID + 1833 + + + CF$UID + 1834 + + + CF$UID + 1835 + + + CF$UID + 1836 + + + CF$UID + 1837 + + + CF$UID + 1838 + + + CF$UID + 1839 + + + CF$UID + 1840 + + + CF$UID + 1841 + + + CF$UID + 1842 + + + CF$UID + 1843 + + + CF$UID + 1844 + + + CF$UID + 1845 + + + CF$UID + 1846 + + + CF$UID + 1847 + + + CF$UID + 1848 + + + CF$UID + 1849 + + + CF$UID + 1850 + + + CF$UID + 1851 + + + CF$UID + 1852 + + + CF$UID + 1853 + + + CF$UID + 1854 + + + CF$UID + 1855 + + + CF$UID + 1856 + + + CF$UID + 1857 + + + CF$UID + 1858 + + + CF$UID + 1862 + + + CF$UID + 1863 + + + CF$UID + 1864 + + + CF$UID + 1865 + + + CF$UID + 1866 + + + CF$UID + 1867 + + + CF$UID + 1868 + + + CF$UID + 1869 + + + CF$UID + 1870 + + + CF$UID + 1871 + + + CF$UID + 1872 + + + CF$UID + 1873 + + + CF$UID + 1874 + + + CF$UID + 1875 + + + CF$UID + 1876 + + + CF$UID + 1877 + + + CF$UID + 1878 + + + CF$UID + 1879 + + + CF$UID + 1880 + + + CF$UID + 1881 + + + CF$UID + 1882 + + + CF$UID + 1883 + + + CF$UID + 1884 + + + CF$UID + 1885 + + + CF$UID + 1886 + + + CF$UID + 1887 + + + CF$UID + 1888 + + + CF$UID + 1889 + + + CF$UID + 1890 + + + CF$UID + 1891 + + + CF$UID + 1892 + + + CF$UID + 1893 + + + CF$UID + 1894 + + + CF$UID + 1895 + + + CF$UID + 1896 + + + CF$UID + 1897 + + + CF$UID + 1898 + + + CF$UID + 1899 + + + CF$UID + 1900 + + + CF$UID + 1901 + + + CF$UID + 1902 + + + CF$UID + 1903 + + + CF$UID + 1904 + + + CF$UID + 1905 + + + CF$UID + 1906 + + + CF$UID + 1907 + + + CF$UID + 1908 + + + CF$UID + 1909 + + + CF$UID + 1910 + + + CF$UID + 1911 + + + CF$UID + 1912 + + + CF$UID + 1913 + + + CF$UID + 1914 + + + CF$UID + 1915 + + + CF$UID + 1916 + + + CF$UID + 1917 + + + CF$UID + 1918 + + + CF$UID + 1919 + + + CF$UID + 1920 + + + CF$UID + 1921 + + + CF$UID + 1922 + + + CF$UID + 1923 + + + CF$UID + 1924 + + + CF$UID + 1925 + + + CF$UID + 1926 + + + CF$UID + 1927 + + + CF$UID + 1928 + + + CF$UID + 1929 + + + CF$UID + 1930 + + + CF$UID + 1931 + + + CF$UID + 1932 + + + CF$UID + 1933 + + + CF$UID + 1934 + + + CF$UID + 1935 + + + CF$UID + 1936 + + + CF$UID + 1937 + + + CF$UID + 1938 + + + CF$UID + 1939 + + + CF$UID + 1940 + + + CF$UID + 1941 + + + CF$UID + 1942 + + + CF$UID + 1943 + + + CF$UID + 1944 + + + CF$UID + 1945 + + + CF$UID + 1946 + + + CF$UID + 1947 + + + CF$UID + 1948 + + + CF$UID + 1949 + + + CF$UID + 1950 + + + CF$UID + 1951 + + + CF$UID + 1952 + + + CF$UID + 1953 + + + CF$UID + 1954 + + + CF$UID + 1955 + + + CF$UID + 1956 + + + CF$UID + 1957 + + + CF$UID + 1958 + + + CF$UID + 1959 + + + CF$UID + 1960 + + + CF$UID + 1961 + + + CF$UID + 1962 + + + CF$UID + 1963 + + + CF$UID + 1964 + + + CF$UID + 1965 + + + CF$UID + 1966 + + + CF$UID + 1967 + + + CF$UID + 1968 + + + CF$UID + 1969 + + + CF$UID + 1970 + + + CF$UID + 1971 + + + CF$UID + 1972 + + + CF$UID + 1973 + + + CF$UID + 1974 + + + CF$UID + 1975 + + + CF$UID + 1976 + + + CF$UID + 1977 + + + CF$UID + 1978 + + + CF$UID + 1979 + + + CF$UID + 1980 + + + CF$UID + 1981 + + + CF$UID + 1982 + + + CF$UID + 1983 + + + CF$UID + 1984 + + + CF$UID + 1985 + + + CF$UID + 1986 + + + CF$UID + 1987 + + + CF$UID + 1988 + + + CF$UID + 1989 + + + CF$UID + 1990 + + + CF$UID + 1991 + + + CF$UID + 1992 + + + CF$UID + 1993 + + + CF$UID + 1994 + + + CF$UID + 1995 + + + CF$UID + 1996 + + + CF$UID + 1997 + + + CF$UID + 1998 + + + CF$UID + 1999 + + + CF$UID + 2000 + + + CF$UID + 2001 + + + CF$UID + 2002 + + + CF$UID + 2003 + + + CF$UID + 2004 + + + CF$UID + 2005 + + + CF$UID + 2006 + + + CF$UID + 2007 + + + CF$UID + 2008 + + + CF$UID + 2009 + + + CF$UID + 2010 + + + CF$UID + 2011 + + + CF$UID + 2012 + + + CF$UID + 2013 + + + CF$UID + 2014 + + + CF$UID + 2015 + + + CF$UID + 2016 + + + CF$UID + 2017 + + + CF$UID + 2018 + + + CF$UID + 2019 + + + CF$UID + 2020 + + + CF$UID + 2021 + + + CF$UID + 2022 + + + CF$UID + 2023 + + + CF$UID + 2024 + + + CF$UID + 2025 + + + CF$UID + 2026 + + + CF$UID + 2027 + + + CF$UID + 2028 + + + CF$UID + 2029 + + + CF$UID + 2030 + + + CF$UID + 2031 + + + CF$UID + 2032 + + + CF$UID + 2033 + + + CF$UID + 2034 + + + CF$UID + 2035 + + + CF$UID + 2036 + + + CF$UID + 2037 + + + CF$UID + 2038 + + + CF$UID + 2039 + + + CF$UID + 2040 + + + CF$UID + 2041 + + + CF$UID + 2042 + + + CF$UID + 2043 + + + CF$UID + 2044 + + + CF$UID + 2045 + + + CF$UID + 2046 + + + CF$UID + 2047 + + + CF$UID + 2048 + + + CF$UID + 2049 + + + CF$UID + 2050 + + + CF$UID + 2051 + + + CF$UID + 2052 + + + CF$UID + 2053 + + + CF$UID + 2054 + + + CF$UID + 2055 + + + CF$UID + 2056 + + + CF$UID + 2057 + + + CF$UID + 2058 + + + CF$UID + 2059 + + + CF$UID + 2060 + + + CF$UID + 2061 + + + CF$UID + 2062 + + + CF$UID + 2063 + + + CF$UID + 2064 + + + CF$UID + 2065 + + + CF$UID + 2066 + + + CF$UID + 2067 + + + CF$UID + 2068 + + + CF$UID + 2069 + + + CF$UID + 2070 + + + CF$UID + 2071 + + + CF$UID + 2072 + + + CF$UID + 2073 + + + CF$UID + 2074 + + + CF$UID + 2075 + + + CF$UID + 2076 + + + CF$UID + 2077 + + + CF$UID + 2078 + + + CF$UID + 2079 + + + CF$UID + 2080 + + + CF$UID + 2081 + + + CF$UID + 2082 + + + CF$UID + 2083 + + + CF$UID + 2084 + + + CF$UID + 2085 + + + CF$UID + 2086 + + + CF$UID + 2087 + + + CF$UID + 2088 + + + CF$UID + 2089 + + + CF$UID + 2090 + + + CF$UID + 2091 + + + CF$UID + 2092 + + + CF$UID + 2093 + + + CF$UID + 2094 + + + CF$UID + 2095 + + + CF$UID + 2096 + + + CF$UID + 2097 + + + CF$UID + 2098 + + + CF$UID + 2099 + + + CF$UID + 2100 + + + CF$UID + 2101 + + + CF$UID + 2102 + + + CF$UID + 2103 + + + CF$UID + 2104 + + + CF$UID + 2105 + + + CF$UID + 2106 + + + CF$UID + 2107 + + + CF$UID + 2108 + + + CF$UID + 2109 + + + CF$UID + 2110 + + + CF$UID + 2111 + + + CF$UID + 2112 + + + CF$UID + 2113 + + + CF$UID + 2114 + + + CF$UID + 2115 + + + CF$UID + 2116 + + + CF$UID + 2117 + + + CF$UID + 2118 + + + CF$UID + 2119 + + + CF$UID + 2120 + + + CF$UID + 2121 + + + CF$UID + 2122 + + + CF$UID + 2123 + + + CF$UID + 2124 + + + CF$UID + 2125 + + + CF$UID + 2126 + + + CF$UID + 2127 + + + CF$UID + 2128 + + + CF$UID + 2129 + + + CF$UID + 2130 + + + CF$UID + 2131 + + + CF$UID + 2132 + + + CF$UID + 2133 + + + CF$UID + 2134 + + + CF$UID + 2135 + + + CF$UID + 2136 + + + CF$UID + 2137 + + + CF$UID + 2138 + + + CF$UID + 2139 + + + CF$UID + 2140 + + + CF$UID + 2141 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 32 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 32 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 32 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 32 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 32 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 32 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 32 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 32 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 32 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 32 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 32 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 1575 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1576 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 102 + len + 77 + x + 1 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 1593 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1594 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 118 + len + 77 + x + 1 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 12 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 12 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 12 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 12 + s + + CF$UID + 1676 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1677 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 199 + len + 31 + x + 11 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 1680 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1681 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 201 + len + 15 + x + 11 + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 12 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 1690 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1691 + + + CF$UID + 1692 + + + CF$UID + 1693 + + + CF$UID + 1694 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 209 + len + 47 + x + 11 + + + $class + + CF$UID + 136 + + c + 48 + l + 209 + len + 2 + x + 1 + + + $class + + CF$UID + 136 + + c + 50 + l + 209 + len + 3 + x + 10 + + + $class + + CF$UID + 136 + + c + 53 + l + 209 + len + 2 + x + 1 + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 1703 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1704 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 217 + len + 44 + x + 11 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 1734 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1735 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 246 + len + 31 + x + 3 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 1746 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1747 + + + CF$UID + 1748 + + + CF$UID + 1749 + + + CF$UID + 1750 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 256 + len + 43 + x + 4 + + + $class + + CF$UID + 136 + + c + 44 + l + 256 + len + 2 + x + 1 + + + $class + + CF$UID + 136 + + c + 46 + l + 256 + len + 3 + x + 3 + + + $class + + CF$UID + 136 + + c + 49 + l + 256 + len + 2 + x + 1 + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 1859 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 1860 + + + CF$UID + 1861 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 364 + len + 53 + x + 2 + + + $class + + CF$UID + 136 + + c + 54 + l + 364 + len + 1 + x + 0 + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 2145 + + functions + + CF$UID + 2146 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 2192 + + name + + CF$UID + 2143 + + uniqueIdentifier + + CF$UID + 2144 + + + SPTDataLoaderResponseTest.m + D9E54C0F-FE10-4EE0-9BFC-682C44971673 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderResponseTest.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 2147 + + + CF$UID + 2150 + + + CF$UID + 2153 + + + CF$UID + 2156 + + + CF$UID + 2159 + + + CF$UID + 2162 + + + CF$UID + 2165 + + + CF$UID + 2168 + + + CF$UID + 2171 + + + CF$UID + 2174 + + + CF$UID + 2177 + + + CF$UID + 2180 + + + CF$UID + 2183 + + + CF$UID + 2186 + + + CF$UID + 2189 + + + + + $class + + CF$UID + 20 + + executionCount + 14 + lineCoverage + + CF$UID + 13 + + lineNumber + 42 + name + + CF$UID + 2148 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2149 + + + -[SPTDataLoaderResponseTest setUp] + 4A6AF78D-FC31-407D-92A1-AAEBE76C8D0D + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 56 + name + + CF$UID + 2151 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2152 + + + -[SPTDataLoaderResponseTest testNotNil] + D02DBA00-305C-4543-9276-5A11177D8D0D + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 61 + name + + CF$UID + 2154 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2155 + + + -[SPTDataLoaderResponseTest testShouldRetryWithOKHTTPStatusCode] + F5CE7E25-A90F-4295-941A-3EC90F1FC182 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 67 + name + + CF$UID + 2157 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2158 + + + -[SPTDataLoaderResponseTest testShouldRetryWithNotFoundHTTPStatusCode] + 7D3BA417-3216-4F2D-B8C7-768493BC15B2 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 80 + name + + CF$UID + 2160 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2161 + + + -[SPTDataLoaderResponseTest testShouldRetryForCertificateRejection] + BDCB3548-4732-449D-AE47-1496A4FFA1FF + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 90 + name + + CF$UID + 2163 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2164 + + + -[SPTDataLoaderResponseTest testShouldRetryForTimedOut] + E77D359B-EC05-4E0B-A47F-668E2ABAADD5 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 100 + name + + CF$UID + 2166 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2167 + + + -[SPTDataLoaderResponseTest testShouldRetryDefault] + 5AA6B45A-A338-4B3D-A648-E6E2715BA45E + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 109 + name + + CF$UID + 2169 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2170 + + + -[SPTDataLoaderResponseTest testErrorForHTTPStatusCode] + FF8E895F-5ECF-4237-B469-C84AAE2A3131 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 114 + name + + CF$UID + 2172 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2173 + + + -[SPTDataLoaderResponseTest testErrorForHTTPStatusCodeNotFound] + 5573C497-1BFA-45A2-9D5C-D663E40A81DF + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 128 + name + + CF$UID + 2175 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2176 + + + -[SPTDataLoaderResponseTest testHeaders] + D59C0A2E-1C08-4F1B-849C-B69A16FE5C78 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 133 + name + + CF$UID + 2178 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2179 + + + -[SPTDataLoaderResponseTest testRelativeRetryAfter] + E3D0B98E-11C1-4892-8ACA-52D0CB3150A5 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 145 + name + + CF$UID + 2181 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2182 + + + -[SPTDataLoaderResponseTest testAbsoluteRetryAfter] + 86E43C53-46A9-4659-8164-F7F693248835 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 158 + name + + CF$UID + 2184 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2185 + + + -[SPTDataLoaderResponseTest testShouldNotRetryWithInvalidHTTPStatusCode] + 86D76160-6731-42A6-AF04-4F8E4F13C486 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 171 + name + + CF$UID + 2187 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2188 + + + -[SPTDataLoaderResponseTest testShouldNotRetryForCancelled] + 7CCD7C60-5E18-4C45-9612-F0F9CF0F80E1 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 181 + name + + CF$UID + 2190 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2191 + + + -[SPTDataLoaderResponseTest testDescription] + 3B761CBA-81B7-4299-B7B2-138CBD875740 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 2193 + + + CF$UID + 2194 + + + CF$UID + 2195 + + + CF$UID + 2196 + + + CF$UID + 2197 + + + CF$UID + 2198 + + + CF$UID + 2199 + + + CF$UID + 2200 + + + CF$UID + 2201 + + + CF$UID + 2202 + + + CF$UID + 2203 + + + CF$UID + 2204 + + + CF$UID + 2205 + + + CF$UID + 2206 + + + CF$UID + 2207 + + + CF$UID + 2208 + + + CF$UID + 2209 + + + CF$UID + 2210 + + + CF$UID + 2211 + + + CF$UID + 2212 + + + CF$UID + 2213 + + + CF$UID + 2214 + + + CF$UID + 2215 + + + CF$UID + 2216 + + + CF$UID + 2217 + + + CF$UID + 2218 + + + CF$UID + 2219 + + + CF$UID + 2220 + + + CF$UID + 2221 + + + CF$UID + 2222 + + + CF$UID + 2223 + + + CF$UID + 2224 + + + CF$UID + 2225 + + + CF$UID + 2226 + + + CF$UID + 2227 + + + CF$UID + 2228 + + + CF$UID + 2229 + + + CF$UID + 2230 + + + CF$UID + 2231 + + + CF$UID + 2232 + + + CF$UID + 2233 + + + CF$UID + 2234 + + + CF$UID + 2235 + + + CF$UID + 2236 + + + CF$UID + 2237 + + + CF$UID + 2238 + + + CF$UID + 2239 + + + CF$UID + 2240 + + + CF$UID + 2241 + + + CF$UID + 2242 + + + CF$UID + 2243 + + + CF$UID + 2244 + + + CF$UID + 2245 + + + CF$UID + 2246 + + + CF$UID + 2247 + + + CF$UID + 2248 + + + CF$UID + 2249 + + + CF$UID + 2250 + + + CF$UID + 2251 + + + CF$UID + 2252 + + + CF$UID + 2253 + + + CF$UID + 2254 + + + CF$UID + 2255 + + + CF$UID + 2256 + + + CF$UID + 2257 + + + CF$UID + 2258 + + + CF$UID + 2259 + + + CF$UID + 2260 + + + CF$UID + 2261 + + + CF$UID + 2262 + + + CF$UID + 2263 + + + CF$UID + 2264 + + + CF$UID + 2265 + + + CF$UID + 2266 + + + CF$UID + 2267 + + + CF$UID + 2268 + + + CF$UID + 2269 + + + CF$UID + 2270 + + + CF$UID + 2271 + + + CF$UID + 2272 + + + CF$UID + 2273 + + + CF$UID + 2274 + + + CF$UID + 2275 + + + CF$UID + 2276 + + + CF$UID + 2277 + + + CF$UID + 2278 + + + CF$UID + 2279 + + + CF$UID + 2280 + + + CF$UID + 2281 + + + CF$UID + 2282 + + + CF$UID + 2283 + + + CF$UID + 2284 + + + CF$UID + 2285 + + + CF$UID + 2286 + + + CF$UID + 2287 + + + CF$UID + 2288 + + + CF$UID + 2289 + + + CF$UID + 2290 + + + CF$UID + 2291 + + + CF$UID + 2292 + + + CF$UID + 2293 + + + CF$UID + 2294 + + + CF$UID + 2295 + + + CF$UID + 2296 + + + CF$UID + 2297 + + + CF$UID + 2298 + + + CF$UID + 2299 + + + CF$UID + 2300 + + + CF$UID + 2301 + + + CF$UID + 2302 + + + CF$UID + 2303 + + + CF$UID + 2304 + + + CF$UID + 2305 + + + CF$UID + 2306 + + + CF$UID + 2307 + + + CF$UID + 2308 + + + CF$UID + 2309 + + + CF$UID + 2310 + + + CF$UID + 2311 + + + CF$UID + 2312 + + + CF$UID + 2313 + + + CF$UID + 2314 + + + CF$UID + 2315 + + + CF$UID + 2316 + + + CF$UID + 2317 + + + CF$UID + 2318 + + + CF$UID + 2319 + + + CF$UID + 2320 + + + CF$UID + 2321 + + + CF$UID + 2322 + + + CF$UID + 2323 + + + CF$UID + 2324 + + + CF$UID + 2325 + + + CF$UID + 2326 + + + CF$UID + 2327 + + + CF$UID + 2328 + + + CF$UID + 2329 + + + CF$UID + 2330 + + + CF$UID + 2331 + + + CF$UID + 2332 + + + CF$UID + 2333 + + + CF$UID + 2334 + + + CF$UID + 2335 + + + CF$UID + 2336 + + + CF$UID + 2337 + + + CF$UID + 2338 + + + CF$UID + 2339 + + + CF$UID + 2340 + + + CF$UID + 2341 + + + CF$UID + 2342 + + + CF$UID + 2343 + + + CF$UID + 2344 + + + CF$UID + 2345 + + + CF$UID + 2346 + + + CF$UID + 2347 + + + CF$UID + 2348 + + + CF$UID + 2349 + + + CF$UID + 2350 + + + CF$UID + 2351 + + + CF$UID + 2352 + + + CF$UID + 2353 + + + CF$UID + 2354 + + + CF$UID + 2355 + + + CF$UID + 2356 + + + CF$UID + 2357 + + + CF$UID + 2358 + + + CF$UID + 2359 + + + CF$UID + 2360 + + + CF$UID + 2361 + + + CF$UID + 2362 + + + CF$UID + 2363 + + + CF$UID + 2364 + + + CF$UID + 2365 + + + CF$UID + 2366 + + + CF$UID + 2367 + + + CF$UID + 2368 + + + CF$UID + 2369 + + + CF$UID + 2370 + + + CF$UID + 2371 + + + CF$UID + 2372 + + + CF$UID + 2373 + + + CF$UID + 2374 + + + CF$UID + 2375 + + + CF$UID + 2376 + + + CF$UID + 2377 + + + CF$UID + 2378 + + + CF$UID + 2379 + + + CF$UID + 2380 + + + CF$UID + 2381 + + + CF$UID + 2382 + + + CF$UID + 2383 + + + CF$UID + 2384 + + + CF$UID + 2385 + + + CF$UID + 2386 + + + CF$UID + 2387 + + + CF$UID + 2388 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 2393 + + functions + + CF$UID + 2394 + + lineCoverage + + CF$UID + 2392 + + lines + + CF$UID + 2471 + + name + + CF$UID + 2390 + + uniqueIdentifier + + CF$UID + 2391 + + + SPTDataLoaderServerTrustPolicyTest.m + 53DD6908-AA2C-4952-B8E9-0D93701B64D7 + 0.97916666666666663 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderServerTrustPolicyTest.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 2395 + + + CF$UID + 2398 + + + CF$UID + 2401 + + + CF$UID + 2404 + + + CF$UID + 2407 + + + CF$UID + 2410 + + + CF$UID + 2413 + + + CF$UID + 2416 + + + CF$UID + 2419 + + + CF$UID + 2422 + + + CF$UID + 2425 + + + CF$UID + 2428 + + + CF$UID + 2431 + + + CF$UID + 2434 + + + CF$UID + 2437 + + + CF$UID + 2440 + + + CF$UID + 2443 + + + CF$UID + 2446 + + + CF$UID + 2449 + + + CF$UID + 2452 + + + CF$UID + 2455 + + + CF$UID + 2458 + + + CF$UID + 2461 + + + CF$UID + 2464 + + + CF$UID + 2468 + + + + + $class + + CF$UID + 20 + + executionCount + 17 + lineCoverage + + CF$UID + 13 + + lineNumber + 99 + name + + CF$UID + 2396 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2397 + + + -[SPTDataLoaderServerTrustPolicyTest setUp] + 3763C67D-E917-425D-AEAB-9DD94818928B + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 109 + name + + CF$UID + 2399 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2400 + + + -[SPTDataLoaderServerTrustPolicyTest testGoogleServerTrustCertificatePathsNotNil] + B6DD3515-2E45-4BAA-89DA-32DF5008D8F7 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 116 + name + + CF$UID + 2402 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2403 + + + -[SPTDataLoaderServerTrustPolicyTest testSpotifyServerTrustCertificatePathsNotNil] + 71F3F554-9FE0-4400-9EB0-1A7F3238A137 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 123 + name + + CF$UID + 2405 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2406 + + + -[SPTDataLoaderServerTrustPolicyTest testGoogleComServerTrustNotNil] + FDF0BA32-8C71-4144-AB2B-99B19EFE9F82 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 130 + name + + CF$UID + 2408 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2409 + + + -[SPTDataLoaderServerTrustPolicyTest testSpotifyComServerTrustNotNil] + 0474D1CD-8AC6-430C-B2CB-F520CB872253 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 139 + name + + CF$UID + 2411 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2412 + + + -[SPTDataLoaderServerTrustPolicyTest testNotNil] + A24DA0D8-9EF8-4737-A49E-967C17BA3F16 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 144 + name + + CF$UID + 2414 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2415 + + + -[SPTDataLoaderServerTrustPolicyTest testNil] + E17C4111-002A-4CD7-A2E6-48CB43D4A8A6 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 153 + name + + CF$UID + 2417 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2418 + + + -[SPTDataLoaderServerTrustPolicyTest testHostsAndCertificatesNotNil] + EC82DDBD-5693-49F7-84EF-473A7089F29B + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 158 + name + + CF$UID + 2420 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2421 + + + -[SPTDataLoaderServerTrustPolicyTest testHostsAndCertificatesCountShouldBeGreaterThanZero] + 7957FFB6-6737-4DAD-B850-9C1CBFC38C2B + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 164 + name + + CF$UID + 2423 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2424 + + + -[SPTDataLoaderServerTrustPolicyTest testTrustedHostsShouldHaveCertificates] + 5CE8BCAE-8F89-40D0-857B-D3EF850C8F5D + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 177 + name + + CF$UID + 2426 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2427 + + + -[SPTDataLoaderServerTrustPolicyTest testCertificatesForValidHostShouldNotBeNil] + 2E08AF07-6CEB-4DDF-B76B-41CD90DEABD8 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 183 + name + + CF$UID + 2429 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2430 + + + -[SPTDataLoaderServerTrustPolicyTest testCertificatesForInvalidHostShouldBeNil] + DEADCDCA-0B02-4E19-9F4E-B18BE39ACD76 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 189 + name + + CF$UID + 2432 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2433 + + + -[SPTDataLoaderServerTrustPolicyTest testCerificatesForHostVariationMatchesWhenKnownHostContainsWildcardShouldNotBeNil] + 3CEB491C-442E-42D0-B7B5-11DA44E772DF + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 205 + name + + CF$UID + 2435 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2436 + + + -[SPTDataLoaderServerTrustPolicyTest testUnknownCertificateForKnownHostShouldBeInvalid] + AC3EB9C5-2F25-40DC-BEF1-CEE702FE52CE + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 214 + name + + CF$UID + 2438 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2439 + + + -[SPTDataLoaderServerTrustPolicyTest testUnknownHostShouldBeInvalid] + 64BFA81D-B02A-4EE5-A988-6CC2316BE4F4 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 225 + name + + CF$UID + 2441 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2442 + + + -[SPTDataLoaderServerTrustPolicyTest testValidatesSpotifyComServerTrustWithCertificateChainPinned] + 08DEF671-0105-4050-B529-57547891D4D2 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 235 + name + + CF$UID + 2444 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2445 + + + -[SPTDataLoaderServerTrustPolicyTest testMalformedAuthenticationChallengeShouldBypassValidation] + 3F17D526-48C1-4AD1-9DAA-23EC55EFB08F + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 278 + name + + CF$UID + 2447 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2448 + + + -[SPTDataLoaderServerTrustPolicyTest testValidAuthenticationChallengeShouldTriggerValidationAttempt] + 85C3CE80-441D-42B6-A0ED-9546D9A7BD5B + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 298 + name + + CF$UID + 2450 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2451 + + + -[SPTDataLoaderServerTrustPolicyValidationSpy validateWithTrust:host:] + A6F35654-BE8D-4296-B271-C8B4A95755CE + + $class + + CF$UID + 20 + + executionCount + 25 + lineCoverage + + CF$UID + 13 + + lineNumber + 80 + name + + CF$UID + 2453 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 2454 + + + SPTDataLoaderServerTrustUnitSpotifyTestCertificatePaths + DDE2AF8D-7D7E-4A5D-8EFB-CB1F0B28CDC7 + + $class + + CF$UID + 20 + + executionCount + 31 + lineCoverage + + CF$UID + 13 + + lineNumber + 70 + name + + CF$UID + 2456 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 2457 + + + SPTDataLoaderServerTrustUnitTestCertificatePathsInDirectory + D912136C-43AD-4D59-9F56-5A51A4569FCC + + $class + + CF$UID + 20 + + executionCount + 6 + lineCoverage + + CF$UID + 13 + + lineNumber + 76 + name + + CF$UID + 2459 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 2460 + + + SPTDataLoaderServerTrustUnitGoogleTestCertificatePaths + 3A06AEE3-DA83-4EAE-AACE-AF0764DFCC51 + + $class + + CF$UID + 20 + + executionCount + 4 + lineCoverage + + CF$UID + 13 + + lineNumber + 84 + name + + CF$UID + 2462 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 2463 + + + SPTDataLoaderUnitTestCreateGoogleComServerTrust + FE73B006-2E66-4715-A7C4-C265FF0A317E + + $class + + CF$UID + 20 + + executionCount + 7 + lineCoverage + + CF$UID + 2467 + + lineNumber + 48 + name + + CF$UID + 2465 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 2466 + + + SPTDataLoaderUnitTestCreateTrustChainForCertPaths + E35F1F88-3A2E-47D7-A911-7985B8360FBC + 0.80952380952380953 + + $class + + CF$UID + 20 + + executionCount + 3 + lineCoverage + + CF$UID + 13 + + lineNumber + 89 + name + + CF$UID + 2469 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 2470 + + + SPTDataLoaderUnitTestCreateSpotifyComServerTrust + 17692BF5-E576-41FA-A8B4-C75909888347 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 2472 + + + CF$UID + 2473 + + + CF$UID + 2474 + + + CF$UID + 2475 + + + CF$UID + 2476 + + + CF$UID + 2477 + + + CF$UID + 2478 + + + CF$UID + 2479 + + + CF$UID + 2480 + + + CF$UID + 2481 + + + CF$UID + 2482 + + + CF$UID + 2483 + + + CF$UID + 2484 + + + CF$UID + 2485 + + + CF$UID + 2486 + + + CF$UID + 2487 + + + CF$UID + 2488 + + + CF$UID + 2489 + + + CF$UID + 2490 + + + CF$UID + 2491 + + + CF$UID + 2492 + + + CF$UID + 2493 + + + CF$UID + 2494 + + + CF$UID + 2495 + + + CF$UID + 2496 + + + CF$UID + 2497 + + + CF$UID + 2498 + + + CF$UID + 2499 + + + CF$UID + 2500 + + + CF$UID + 2501 + + + CF$UID + 2502 + + + CF$UID + 2503 + + + CF$UID + 2504 + + + CF$UID + 2505 + + + CF$UID + 2506 + + + CF$UID + 2507 + + + CF$UID + 2508 + + + CF$UID + 2509 + + + CF$UID + 2510 + + + CF$UID + 2511 + + + CF$UID + 2512 + + + CF$UID + 2513 + + + CF$UID + 2514 + + + CF$UID + 2515 + + + CF$UID + 2516 + + + CF$UID + 2517 + + + CF$UID + 2518 + + + CF$UID + 2519 + + + CF$UID + 2520 + + + CF$UID + 2521 + + + CF$UID + 2522 + + + CF$UID + 2523 + + + CF$UID + 2526 + + + CF$UID + 2527 + + + CF$UID + 2528 + + + CF$UID + 2529 + + + CF$UID + 2530 + + + CF$UID + 2531 + + + CF$UID + 2532 + + + CF$UID + 2533 + + + CF$UID + 2536 + + + CF$UID + 2537 + + + CF$UID + 2538 + + + CF$UID + 2539 + + + CF$UID + 2540 + + + CF$UID + 2541 + + + CF$UID + 2542 + + + CF$UID + 2543 + + + CF$UID + 2544 + + + CF$UID + 2545 + + + CF$UID + 2546 + + + CF$UID + 2547 + + + CF$UID + 2548 + + + CF$UID + 2549 + + + CF$UID + 2550 + + + CF$UID + 2551 + + + CF$UID + 2552 + + + CF$UID + 2553 + + + CF$UID + 2554 + + + CF$UID + 2555 + + + CF$UID + 2556 + + + CF$UID + 2557 + + + CF$UID + 2558 + + + CF$UID + 2559 + + + CF$UID + 2560 + + + CF$UID + 2561 + + + CF$UID + 2562 + + + CF$UID + 2563 + + + CF$UID + 2564 + + + CF$UID + 2565 + + + CF$UID + 2566 + + + CF$UID + 2567 + + + CF$UID + 2568 + + + CF$UID + 2569 + + + CF$UID + 2570 + + + CF$UID + 2571 + + + CF$UID + 2572 + + + CF$UID + 2573 + + + CF$UID + 2574 + + + CF$UID + 2575 + + + CF$UID + 2576 + + + CF$UID + 2577 + + + CF$UID + 2578 + + + CF$UID + 2579 + + + CF$UID + 2580 + + + CF$UID + 2581 + + + CF$UID + 2582 + + + CF$UID + 2583 + + + CF$UID + 2584 + + + CF$UID + 2585 + + + CF$UID + 2586 + + + CF$UID + 2587 + + + CF$UID + 2588 + + + CF$UID + 2589 + + + CF$UID + 2590 + + + CF$UID + 2591 + + + CF$UID + 2592 + + + CF$UID + 2593 + + + CF$UID + 2594 + + + CF$UID + 2595 + + + CF$UID + 2596 + + + CF$UID + 2597 + + + CF$UID + 2598 + + + CF$UID + 2599 + + + CF$UID + 2600 + + + CF$UID + 2601 + + + CF$UID + 2602 + + + CF$UID + 2603 + + + CF$UID + 2604 + + + CF$UID + 2605 + + + CF$UID + 2606 + + + CF$UID + 2607 + + + CF$UID + 2608 + + + CF$UID + 2609 + + + CF$UID + 2610 + + + CF$UID + 2611 + + + CF$UID + 2612 + + + CF$UID + 2613 + + + CF$UID + 2614 + + + CF$UID + 2615 + + + CF$UID + 2616 + + + CF$UID + 2617 + + + CF$UID + 2618 + + + CF$UID + 2619 + + + CF$UID + 2620 + + + CF$UID + 2621 + + + CF$UID + 2622 + + + CF$UID + 2623 + + + CF$UID + 2624 + + + CF$UID + 2625 + + + CF$UID + 2626 + + + CF$UID + 2627 + + + CF$UID + 2628 + + + CF$UID + 2629 + + + CF$UID + 2630 + + + CF$UID + 2631 + + + CF$UID + 2632 + + + CF$UID + 2633 + + + CF$UID + 2634 + + + CF$UID + 2635 + + + CF$UID + 2636 + + + CF$UID + 2637 + + + CF$UID + 2638 + + + CF$UID + 2639 + + + CF$UID + 2640 + + + CF$UID + 2641 + + + CF$UID + 2642 + + + CF$UID + 2643 + + + CF$UID + 2644 + + + CF$UID + 2645 + + + CF$UID + 2646 + + + CF$UID + 2647 + + + CF$UID + 2648 + + + CF$UID + 2649 + + + CF$UID + 2650 + + + CF$UID + 2651 + + + CF$UID + 2652 + + + CF$UID + 2653 + + + CF$UID + 2654 + + + CF$UID + 2655 + + + CF$UID + 2656 + + + CF$UID + 2657 + + + CF$UID + 2658 + + + CF$UID + 2659 + + + CF$UID + 2660 + + + CF$UID + 2661 + + + CF$UID + 2662 + + + CF$UID + 2663 + + + CF$UID + 2664 + + + CF$UID + 2665 + + + CF$UID + 2666 + + + CF$UID + 2667 + + + CF$UID + 2668 + + + CF$UID + 2669 + + + CF$UID + 2670 + + + CF$UID + 2671 + + + CF$UID + 2672 + + + CF$UID + 2673 + + + CF$UID + 2674 + + + CF$UID + 2675 + + + CF$UID + 2676 + + + CF$UID + 2677 + + + CF$UID + 2678 + + + CF$UID + 2679 + + + CF$UID + 2680 + + + CF$UID + 2681 + + + CF$UID + 2682 + + + CF$UID + 2683 + + + CF$UID + 2684 + + + CF$UID + 2685 + + + CF$UID + 2686 + + + CF$UID + 2687 + + + CF$UID + 2688 + + + CF$UID + 2689 + + + CF$UID + 2690 + + + CF$UID + 2691 + + + CF$UID + 2692 + + + CF$UID + 2693 + + + CF$UID + 2694 + + + CF$UID + 2695 + + + CF$UID + 2696 + + + CF$UID + 2697 + + + CF$UID + 2698 + + + CF$UID + 2699 + + + CF$UID + 2700 + + + CF$UID + 2701 + + + CF$UID + 2702 + + + CF$UID + 2703 + + + CF$UID + 2704 + + + CF$UID + 2705 + + + CF$UID + 2706 + + + CF$UID + 2707 + + + CF$UID + 2708 + + + CF$UID + 2709 + + + CF$UID + 2710 + + + CF$UID + 2711 + + + CF$UID + 2712 + + + CF$UID + 2713 + + + CF$UID + 2714 + + + CF$UID + 2715 + + + CF$UID + 2716 + + + CF$UID + 2717 + + + CF$UID + 2718 + + + CF$UID + 2719 + + + CF$UID + 2720 + + + CF$UID + 2721 + + + CF$UID + 2722 + + + CF$UID + 2723 + + + CF$UID + 2724 + + + CF$UID + 2725 + + + CF$UID + 2726 + + + CF$UID + 2727 + + + CF$UID + 2728 + + + CF$UID + 2729 + + + CF$UID + 2730 + + + CF$UID + 2731 + + + CF$UID + 2732 + + + CF$UID + 2733 + + + CF$UID + 2734 + + + CF$UID + 2735 + + + CF$UID + 2736 + + + CF$UID + 2737 + + + CF$UID + 2738 + + + CF$UID + 2739 + + + CF$UID + 2740 + + + CF$UID + 2741 + + + CF$UID + 2742 + + + CF$UID + 2743 + + + CF$UID + 2744 + + + CF$UID + 2745 + + + CF$UID + 2746 + + + CF$UID + 2747 + + + CF$UID + 2748 + + + CF$UID + 2749 + + + CF$UID + 2750 + + + CF$UID + 2751 + + + CF$UID + 2752 + + + CF$UID + 2753 + + + CF$UID + 2754 + + + CF$UID + 2755 + + + CF$UID + 2756 + + + CF$UID + 2757 + + + CF$UID + 2758 + + + CF$UID + 2759 + + + CF$UID + 2760 + + + CF$UID + 2761 + + + CF$UID + 2762 + + + CF$UID + 2763 + + + CF$UID + 2764 + + + CF$UID + 2765 + + + CF$UID + 2766 + + + CF$UID + 2767 + + + CF$UID + 2768 + + + CF$UID + 2769 + + + CF$UID + 2770 + + + CF$UID + 2771 + + + CF$UID + 2772 + + + CF$UID + 2773 + + + CF$UID + 2774 + + + CF$UID + 2775 + + + CF$UID + 2776 + + + CF$UID + 2777 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 2524 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 2525 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 52 + len + 23 + x + 18 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 2534 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 2535 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 60 + len + 24 + x + 7 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 25 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 25 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 25 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 2781 + + functions + + CF$UID + 2782 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 2849 + + name + + CF$UID + 2779 + + uniqueIdentifier + + CF$UID + 2780 + + + SPTDataLoaderRequestTest.m + 9400845B-CEFB-4E7E-8A6C-3354761AAD6B + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderRequestTest.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 2783 + + + CF$UID + 2786 + + + CF$UID + 2789 + + + CF$UID + 2792 + + + CF$UID + 2795 + + + CF$UID + 2798 + + + CF$UID + 2801 + + + CF$UID + 2804 + + + CF$UID + 2807 + + + CF$UID + 2810 + + + CF$UID + 2813 + + + CF$UID + 2816 + + + CF$UID + 2819 + + + CF$UID + 2822 + + + CF$UID + 2825 + + + CF$UID + 2828 + + + CF$UID + 2831 + + + CF$UID + 2834 + + + CF$UID + 2837 + + + CF$UID + 2840 + + + CF$UID + 2843 + + + CF$UID + 2846 + + + + + $class + + CF$UID + 20 + + executionCount + 19 + lineCoverage + + CF$UID + 13 + + lineNumber + 49 + name + + CF$UID + 2784 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2785 + + + -[SPTDataLoaderRequestTest setUp] + 5023D276-CB1A-4950-A722-4EE06987CD7C + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 58 + name + + CF$UID + 2787 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2788 + + + -[SPTDataLoaderRequestTest testNotNil] + C7D69797-00F6-4A49-9D22-A652DF18651A + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 63 + name + + CF$UID + 2790 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2791 + + + -[SPTDataLoaderRequestTest testHeadersEmptyInitially] + 1F781481-9031-4E38-9B88-6AF56AFE1640 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 68 + name + + CF$UID + 2793 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2794 + + + -[SPTDataLoaderRequestTest testAddValueToNilHeader] + 97A2C08C-D3E0-403E-A2F5-96727C02BC85 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 77 + name + + CF$UID + 2796 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2797 + + + -[SPTDataLoaderRequestTest testAddNilValueToHeader] + 8CDAF504-0584-4CDF-B6BB-EBB9874050D0 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 87 + name + + CF$UID + 2799 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2800 + + + -[SPTDataLoaderRequestTest testAddValueToHeader] + 206F9151-0101-4BD7-919A-9A0F8BA7DB72 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 93 + name + + CF$UID + 2802 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2803 + + + -[SPTDataLoaderRequestTest testRemoveHeader] + B3013D06-9A72-4919-9A04-7177AB5E46F3 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 100 + name + + CF$UID + 2805 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2806 + + + -[SPTDataLoaderRequestTest testURLRequestContentLengthHeader] + 3BFD1753-7EA2-48A1-AAD0-9636FCCD3A7F + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 108 + name + + CF$UID + 2808 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2809 + + + -[SPTDataLoaderRequestTest testURLRequestCopyingHeaders] + 567F5149-B9CC-4E69-B8F2-EA83499D89FA + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 117 + name + + CF$UID + 2811 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2812 + + + -[SPTDataLoaderRequestTest testURLRequestCachePolicy] + A30FA163-C3EE-4DD9-A52A-119C90DA8962 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 124 + name + + CF$UID + 2814 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2815 + + + -[SPTDataLoaderRequestTest testURLRequestMethod] + 57DF6189-931B-431D-A912-18489752894F + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 131 + name + + CF$UID + 2817 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2818 + + + -[SPTDataLoaderRequestTest testCopy] + 31F27DEA-1D81-468B-9A97-7E46FF3D30ED + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 153 + name + + CF$UID + 2820 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2821 + + + -[SPTDataLoaderRequestTest testAcceptLanguageWithNoEnglishLanguages] + D25ABE49-5718-4FCB-9EBC-9C2AEB6F9C86 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 160 + name + + CF$UID + 2823 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 2824 + + + __68-[SPTDataLoaderRequestTest testAcceptLanguageWithNoEnglishLanguages]_block_invoke + 5E0635B5-2400-47BF-B178-F75B8A4A8FE6 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 173 + name + + CF$UID + 2826 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2827 + + + -[SPTDataLoaderRequestTest testDescription] + B3E43E1D-DB45-48F4-A64F-CD61DC0C85BE + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 183 + name + + CF$UID + 2829 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2830 + + + -[SPTDataLoaderRequestTest testAcceptLanguageWithMultipleLanguagesContainingEnglish] + B8F340CA-58FA-419C-9B90-3BF78833C2E5 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 190 + name + + CF$UID + 2832 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 2833 + + + __84-[SPTDataLoaderRequestTest testAcceptLanguageWithMultipleLanguagesContainingEnglish]_block_invoke + CC2A311B-5461-4FA9-B2DA-103037D5A3EB + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 203 + name + + CF$UID + 2835 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2836 + + + -[SPTDataLoaderRequestTest testDeleteMethod] + 8A9F698A-C7D8-4AB2-A4C4-87E55D226F6A + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 209 + name + + CF$UID + 2838 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2839 + + + -[SPTDataLoaderRequestTest testPutMethod] + C891907C-B32F-4EE9-A233-B0DD2CA08628 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 215 + name + + CF$UID + 2841 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2842 + + + -[SPTDataLoaderRequestTest testPostMethod] + 2610EAD8-39E5-4FBD-9838-FECDF3E026E1 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 221 + name + + CF$UID + 2844 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2845 + + + -[SPTDataLoaderRequestTest testCopyDoesntIncrementUniqueIdentifierBarrier] + ADA96E80-9252-4933-BE72-3AC8F9776C51 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 228 + name + + CF$UID + 2847 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 2848 + + + -[SPTDataLoaderRequestTest testStreamInUrlRequest] + CE866226-91BD-48ED-917D-13A8EF3B7F79 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 2850 + + + CF$UID + 2851 + + + CF$UID + 2852 + + + CF$UID + 2853 + + + CF$UID + 2854 + + + CF$UID + 2855 + + + CF$UID + 2856 + + + CF$UID + 2857 + + + CF$UID + 2858 + + + CF$UID + 2859 + + + CF$UID + 2860 + + + CF$UID + 2861 + + + CF$UID + 2862 + + + CF$UID + 2863 + + + CF$UID + 2864 + + + CF$UID + 2865 + + + CF$UID + 2866 + + + CF$UID + 2867 + + + CF$UID + 2868 + + + CF$UID + 2869 + + + CF$UID + 2870 + + + CF$UID + 2871 + + + CF$UID + 2872 + + + CF$UID + 2873 + + + CF$UID + 2874 + + + CF$UID + 2875 + + + CF$UID + 2876 + + + CF$UID + 2877 + + + CF$UID + 2878 + + + CF$UID + 2879 + + + CF$UID + 2880 + + + CF$UID + 2881 + + + CF$UID + 2882 + + + CF$UID + 2883 + + + CF$UID + 2884 + + + CF$UID + 2885 + + + CF$UID + 2886 + + + CF$UID + 2887 + + + CF$UID + 2888 + + + CF$UID + 2889 + + + CF$UID + 2890 + + + CF$UID + 2891 + + + CF$UID + 2892 + + + CF$UID + 2893 + + + CF$UID + 2894 + + + CF$UID + 2895 + + + CF$UID + 2896 + + + CF$UID + 2897 + + + CF$UID + 2898 + + + CF$UID + 2899 + + + CF$UID + 2900 + + + CF$UID + 2901 + + + CF$UID + 2902 + + + CF$UID + 2903 + + + CF$UID + 2904 + + + CF$UID + 2905 + + + CF$UID + 2906 + + + CF$UID + 2907 + + + CF$UID + 2908 + + + CF$UID + 2909 + + + CF$UID + 2910 + + + CF$UID + 2911 + + + CF$UID + 2912 + + + CF$UID + 2913 + + + CF$UID + 2914 + + + CF$UID + 2915 + + + CF$UID + 2916 + + + CF$UID + 2917 + + + CF$UID + 2918 + + + CF$UID + 2919 + + + CF$UID + 2920 + + + CF$UID + 2921 + + + CF$UID + 2922 + + + CF$UID + 2923 + + + CF$UID + 2924 + + + CF$UID + 2925 + + + CF$UID + 2926 + + + CF$UID + 2927 + + + CF$UID + 2928 + + + CF$UID + 2929 + + + CF$UID + 2930 + + + CF$UID + 2931 + + + CF$UID + 2932 + + + CF$UID + 2933 + + + CF$UID + 2934 + + + CF$UID + 2935 + + + CF$UID + 2936 + + + CF$UID + 2937 + + + CF$UID + 2938 + + + CF$UID + 2939 + + + CF$UID + 2940 + + + CF$UID + 2941 + + + CF$UID + 2942 + + + CF$UID + 2943 + + + CF$UID + 2944 + + + CF$UID + 2945 + + + CF$UID + 2946 + + + CF$UID + 2947 + + + CF$UID + 2948 + + + CF$UID + 2949 + + + CF$UID + 2950 + + + CF$UID + 2951 + + + CF$UID + 2952 + + + CF$UID + 2953 + + + CF$UID + 2954 + + + CF$UID + 2955 + + + CF$UID + 2956 + + + CF$UID + 2957 + + + CF$UID + 2958 + + + CF$UID + 2959 + + + CF$UID + 2960 + + + CF$UID + 2961 + + + CF$UID + 2962 + + + CF$UID + 2963 + + + CF$UID + 2964 + + + CF$UID + 2965 + + + CF$UID + 2966 + + + CF$UID + 2967 + + + CF$UID + 2968 + + + CF$UID + 2969 + + + CF$UID + 2970 + + + CF$UID + 2971 + + + CF$UID + 2972 + + + CF$UID + 2973 + + + CF$UID + 2974 + + + CF$UID + 2975 + + + CF$UID + 2976 + + + CF$UID + 2977 + + + CF$UID + 2978 + + + CF$UID + 2979 + + + CF$UID + 2980 + + + CF$UID + 2981 + + + CF$UID + 2982 + + + CF$UID + 2983 + + + CF$UID + 2984 + + + CF$UID + 2985 + + + CF$UID + 2986 + + + CF$UID + 2987 + + + CF$UID + 2988 + + + CF$UID + 2989 + + + CF$UID + 2990 + + + CF$UID + 2991 + + + CF$UID + 2992 + + + CF$UID + 2993 + + + CF$UID + 2994 + + + CF$UID + 2995 + + + CF$UID + 2996 + + + CF$UID + 2997 + + + CF$UID + 2998 + + + CF$UID + 2999 + + + CF$UID + 3000 + + + CF$UID + 3001 + + + CF$UID + 3002 + + + CF$UID + 3003 + + + CF$UID + 3004 + + + CF$UID + 3005 + + + CF$UID + 3006 + + + CF$UID + 3007 + + + CF$UID + 3008 + + + CF$UID + 3009 + + + CF$UID + 3010 + + + CF$UID + 3011 + + + CF$UID + 3012 + + + CF$UID + 3013 + + + CF$UID + 3014 + + + CF$UID + 3015 + + + CF$UID + 3016 + + + CF$UID + 3017 + + + CF$UID + 3018 + + + CF$UID + 3019 + + + CF$UID + 3020 + + + CF$UID + 3021 + + + CF$UID + 3022 + + + CF$UID + 3023 + + + CF$UID + 3024 + + + CF$UID + 3025 + + + CF$UID + 3026 + + + CF$UID + 3027 + + + CF$UID + 3028 + + + CF$UID + 3029 + + + CF$UID + 3030 + + + CF$UID + 3031 + + + CF$UID + 3032 + + + CF$UID + 3033 + + + CF$UID + 3034 + + + CF$UID + 3035 + + + CF$UID + 3036 + + + CF$UID + 3037 + + + CF$UID + 3038 + + + CF$UID + 3039 + + + CF$UID + 3040 + + + CF$UID + 3041 + + + CF$UID + 3042 + + + CF$UID + 3043 + + + CF$UID + 3044 + + + CF$UID + 3045 + + + CF$UID + 3046 + + + CF$UID + 3047 + + + CF$UID + 3048 + + + CF$UID + 3049 + + + CF$UID + 3050 + + + CF$UID + 3051 + + + CF$UID + 3052 + + + CF$UID + 3053 + + + CF$UID + 3054 + + + CF$UID + 3055 + + + CF$UID + 3056 + + + CF$UID + 3057 + + + CF$UID + 3058 + + + CF$UID + 3059 + + + CF$UID + 3060 + + + CF$UID + 3061 + + + CF$UID + 3062 + + + CF$UID + 3063 + + + CF$UID + 3064 + + + CF$UID + 3065 + + + CF$UID + 3066 + + + CF$UID + 3067 + + + CF$UID + 3068 + + + CF$UID + 3069 + + + CF$UID + 3070 + + + CF$UID + 3071 + + + CF$UID + 3072 + + + CF$UID + 3073 + + + CF$UID + 3074 + + + CF$UID + 3075 + + + CF$UID + 3076 + + + CF$UID + 3077 + + + CF$UID + 3078 + + + CF$UID + 3079 + + + CF$UID + 3080 + + + CF$UID + 3081 + + + CF$UID + 3082 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 3086 + + functions + + CF$UID + 3087 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 3100 + + name + + CF$UID + 3084 + + uniqueIdentifier + + CF$UID + 3085 + + + SPTDataLoaderRequestResponseHandlerDelegateMock.m + CF6EDCCB-1710-4BDC-87DF-5B7847DCFB39 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderRequestResponseHandlerDelegateMock.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3088 + + + CF$UID + 3091 + + + CF$UID + 3094 + + + CF$UID + 3097 + + + + + $class + + CF$UID + 20 + + executionCount + 26 + lineCoverage + + CF$UID + 13 + + lineNumber + 27 + name + + CF$UID + 3089 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3090 + + + -[SPTDataLoaderRequestResponseHandlerDelegateMock requestResponseHandler:performRequest:] + 81796C10-47C1-4B1E-B942-0DBB0F95C363 + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 33 + name + + CF$UID + 3092 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3093 + + + -[SPTDataLoaderRequestResponseHandlerDelegateMock requestResponseHandler:authorisedRequest:] + ABF6F59D-589F-47BA-A187-51E207E2FBD1 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 40 + name + + CF$UID + 3095 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3096 + + + -[SPTDataLoaderRequestResponseHandlerDelegateMock requestResponseHandler:failedToAuthoriseRequest:error:] + 5ABE0B55-B0A2-4DD2-B6CD-B394F5E2898D + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 46 + name + + CF$UID + 3098 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3099 + + + -[SPTDataLoaderRequestResponseHandlerDelegateMock requestResponseHandler:cancelRequest:] + A387A294-0164-49B0-B109-892CFDF757D8 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3101 + + + CF$UID + 3102 + + + CF$UID + 3103 + + + CF$UID + 3104 + + + CF$UID + 3105 + + + CF$UID + 3106 + + + CF$UID + 3107 + + + CF$UID + 3108 + + + CF$UID + 3109 + + + CF$UID + 3110 + + + CF$UID + 3111 + + + CF$UID + 3112 + + + CF$UID + 3113 + + + CF$UID + 3114 + + + CF$UID + 3115 + + + CF$UID + 3116 + + + CF$UID + 3117 + + + CF$UID + 3118 + + + CF$UID + 3119 + + + CF$UID + 3120 + + + CF$UID + 3121 + + + CF$UID + 3122 + + + CF$UID + 3123 + + + CF$UID + 3124 + + + CF$UID + 3125 + + + CF$UID + 3126 + + + CF$UID + 3127 + + + CF$UID + 3128 + + + CF$UID + 3129 + + + CF$UID + 3130 + + + CF$UID + 3131 + + + CF$UID + 3132 + + + CF$UID + 3133 + + + CF$UID + 3134 + + + CF$UID + 3135 + + + CF$UID + 3136 + + + CF$UID + 3137 + + + CF$UID + 3138 + + + CF$UID + 3139 + + + CF$UID + 3140 + + + CF$UID + 3141 + + + CF$UID + 3142 + + + CF$UID + 3143 + + + CF$UID + 3144 + + + CF$UID + 3145 + + + CF$UID + 3146 + + + CF$UID + 3147 + + + CF$UID + 3148 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 26 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 26 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 26 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 3153 + + functions + + CF$UID + 3154 + + lineCoverage + + CF$UID + 3152 + + lines + + CF$UID + 3164 + + name + + CF$UID + 3150 + + uniqueIdentifier + + CF$UID + 3151 + + + NSURLSessionTaskMock.m + 5D899B87-2EE4-4AAE-A496-8AB3AA775264 + 0.75 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/NSURLSessionTaskMock.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3155 + + + CF$UID + 3158 + + + CF$UID + 3161 + + + + + $class + + CF$UID + 20 + + executionCount + 5 + lineCoverage + + CF$UID + 13 + + lineNumber + 28 + name + + CF$UID + 3156 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3157 + + + -[NSURLSessionTaskMock resume] + 1E9F7E8E-502F-4D19-A251-1859657CFF53 + + $class + + CF$UID + 20 + + executionCount + 0 + lineCoverage + + CF$UID + 80 + + lineNumber + 36 + name + + CF$UID + 3159 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3160 + + + -[NSURLSessionTaskMock cancel] + 0B3B2E5E-BBD8-4D16-8E38-64027BD105BE + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 41 + name + + CF$UID + 3162 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3163 + + + -[NSURLSessionTaskMock response] + A4C6A3F3-7A0D-4832-B75A-6C9F44A29641 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3165 + + + CF$UID + 3166 + + + CF$UID + 3167 + + + CF$UID + 3168 + + + CF$UID + 3169 + + + CF$UID + 3170 + + + CF$UID + 3171 + + + CF$UID + 3172 + + + CF$UID + 3173 + + + CF$UID + 3174 + + + CF$UID + 3175 + + + CF$UID + 3176 + + + CF$UID + 3177 + + + CF$UID + 3178 + + + CF$UID + 3179 + + + CF$UID + 3180 + + + CF$UID + 3181 + + + CF$UID + 3182 + + + CF$UID + 3183 + + + CF$UID + 3184 + + + CF$UID + 3185 + + + CF$UID + 3186 + + + CF$UID + 3187 + + + CF$UID + 3188 + + + CF$UID + 3189 + + + CF$UID + 3190 + + + CF$UID + 3191 + + + CF$UID + 3192 + + + CF$UID + 3193 + + + CF$UID + 3194 + + + CF$UID + 3197 + + + CF$UID + 3198 + + + CF$UID + 3199 + + + CF$UID + 3200 + + + CF$UID + 3201 + + + CF$UID + 3202 + + + CF$UID + 3203 + + + CF$UID + 3204 + + + CF$UID + 3205 + + + CF$UID + 3206 + + + CF$UID + 3207 + + + CF$UID + 3208 + + + CF$UID + 3209 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 3195 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3196 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 30 + len + 29 + x + 5 + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 3213 + + functions + + CF$UID + 3214 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 3230 + + name + + CF$UID + 3211 + + uniqueIdentifier + + CF$UID + 3212 + + + SPTDataLoaderResolverTest.m + F0991C75-6B95-4AAB-8BB7-D22337768021 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderResolverTest.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3215 + + + CF$UID + 3218 + + + CF$UID + 3221 + + + CF$UID + 3224 + + + CF$UID + 3227 + + + + + $class + + CF$UID + 20 + + executionCount + 4 + lineCoverage + + CF$UID + 13 + + lineNumber + 36 + name + + CF$UID + 3216 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3217 + + + -[SPTDataLoaderResolverTest setUp] + 60F32174-16F0-4FA5-B8E8-E2B8EAB4A36D + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 44 + name + + CF$UID + 3219 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3220 + + + -[SPTDataLoaderResolverTest testNotNil] + 739C5519-23F5-4FAB-A649-470EE8B86FA2 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 49 + name + + CF$UID + 3222 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3223 + + + -[SPTDataLoaderResolverTest testDefaultToHostIfNoOverridingAddress] + 9759B71B-997C-4F7E-B1C3-0476FB1B5EFB + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 56 + name + + CF$UID + 3225 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3226 + + + -[SPTDataLoaderResolverTest testAddressGivenIfReachableForHostOverride] + AD35DC5B-6D79-46F1-BBDA-81BEB2809CE8 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 65 + name + + CF$UID + 3228 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3229 + + + -[SPTDataLoaderResolverTest testAddressNotGivenIfNotReachableForHostOverride] + D66EA918-D030-43B5-B235-3EA6A0B231FE + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3231 + + + CF$UID + 3232 + + + CF$UID + 3233 + + + CF$UID + 3234 + + + CF$UID + 3235 + + + CF$UID + 3236 + + + CF$UID + 3237 + + + CF$UID + 3238 + + + CF$UID + 3239 + + + CF$UID + 3240 + + + CF$UID + 3241 + + + CF$UID + 3242 + + + CF$UID + 3243 + + + CF$UID + 3244 + + + CF$UID + 3245 + + + CF$UID + 3246 + + + CF$UID + 3247 + + + CF$UID + 3248 + + + CF$UID + 3249 + + + CF$UID + 3250 + + + CF$UID + 3251 + + + CF$UID + 3252 + + + CF$UID + 3253 + + + CF$UID + 3254 + + + CF$UID + 3255 + + + CF$UID + 3256 + + + CF$UID + 3257 + + + CF$UID + 3258 + + + CF$UID + 3259 + + + CF$UID + 3260 + + + CF$UID + 3261 + + + CF$UID + 3262 + + + CF$UID + 3263 + + + CF$UID + 3264 + + + CF$UID + 3265 + + + CF$UID + 3266 + + + CF$UID + 3267 + + + CF$UID + 3268 + + + CF$UID + 3269 + + + CF$UID + 3270 + + + CF$UID + 3271 + + + CF$UID + 3272 + + + CF$UID + 3273 + + + CF$UID + 3274 + + + CF$UID + 3275 + + + CF$UID + 3276 + + + CF$UID + 3277 + + + CF$UID + 3278 + + + CF$UID + 3279 + + + CF$UID + 3280 + + + CF$UID + 3281 + + + CF$UID + 3282 + + + CF$UID + 3283 + + + CF$UID + 3284 + + + CF$UID + 3285 + + + CF$UID + 3286 + + + CF$UID + 3287 + + + CF$UID + 3288 + + + CF$UID + 3289 + + + CF$UID + 3290 + + + CF$UID + 3291 + + + CF$UID + 3292 + + + CF$UID + 3293 + + + CF$UID + 3294 + + + CF$UID + 3295 + + + CF$UID + 3296 + + + CF$UID + 3297 + + + CF$UID + 3298 + + + CF$UID + 3299 + + + CF$UID + 3300 + + + CF$UID + 3301 + + + CF$UID + 3302 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 3306 + + functions + + CF$UID + 3307 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 3311 + + name + + CF$UID + 3304 + + uniqueIdentifier + + CF$UID + 3305 + + + SPTDataLoaderCancellationTokenFactoryMock.m + F2F763FA-A4E2-4313-B83C-ECEC13FD0752 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderCancellationTokenFactoryMock.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3308 + + + + + $class + + CF$UID + 20 + + executionCount + 16 + lineCoverage + + CF$UID + 13 + + lineNumber + 29 + name + + CF$UID + 3309 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3310 + + + -[SPTDataLoaderCancellationTokenFactoryMock createCancellationTokenWithDelegate:cancelObject:] + C66B597B-1829-42FE-9BAB-7756154E7404 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3312 + + + CF$UID + 3313 + + + CF$UID + 3314 + + + CF$UID + 3315 + + + CF$UID + 3316 + + + CF$UID + 3317 + + + CF$UID + 3318 + + + CF$UID + 3319 + + + CF$UID + 3320 + + + CF$UID + 3321 + + + CF$UID + 3322 + + + CF$UID + 3323 + + + CF$UID + 3324 + + + CF$UID + 3325 + + + CF$UID + 3326 + + + CF$UID + 3327 + + + CF$UID + 3328 + + + CF$UID + 3329 + + + CF$UID + 3330 + + + CF$UID + 3331 + + + CF$UID + 3332 + + + CF$UID + 3333 + + + CF$UID + 3334 + + + CF$UID + 3335 + + + CF$UID + 3336 + + + CF$UID + 3337 + + + CF$UID + 3338 + + + CF$UID + 3339 + + + CF$UID + 3340 + + + CF$UID + 3341 + + + CF$UID + 3344 + + + CF$UID + 3345 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 3342 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3343 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 30 + len + 136 + x + 11 + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 3350 + + functions + + CF$UID + 3351 + + lineCoverage + + CF$UID + 3349 + + lines + + CF$UID + 3415 + + name + + CF$UID + 3347 + + uniqueIdentifier + + CF$UID + 3348 + + + SPTDataLoaderFactoryTest.m + C16020CE-4A3C-4250-9A3B-6CC78F9E757E + 0.99295774647887325 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderFactoryTest.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3352 + + + CF$UID + 3355 + + + CF$UID + 3358 + + + CF$UID + 3361 + + + CF$UID + 3364 + + + CF$UID + 3367 + + + CF$UID + 3370 + + + CF$UID + 3373 + + + CF$UID + 3376 + + + CF$UID + 3379 + + + CF$UID + 3382 + + + CF$UID + 3385 + + + CF$UID + 3388 + + + CF$UID + 3391 + + + CF$UID + 3394 + + + CF$UID + 3397 + + + CF$UID + 3400 + + + CF$UID + 3403 + + + CF$UID + 3406 + + + CF$UID + 3409 + + + CF$UID + 3412 + + + + + $class + + CF$UID + 20 + + executionCount + 18 + lineCoverage + + CF$UID + 13 + + lineNumber + 52 + name + + CF$UID + 3353 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3354 + + + -[SPTDataLoaderFactoryTest setUp] + B5803D07-8039-456F-BDED-29853EE03569 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 63 + name + + CF$UID + 3356 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3357 + + + -[SPTDataLoaderFactoryTest testNotNil] + E7EF16F6-BF10-4D76-82A2-482C1421479F + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 68 + name + + CF$UID + 3359 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3360 + + + -[SPTDataLoaderFactoryTest testCreateDataLoader] + ACF12E39-4AF1-47D1-8F15-862A8D15803D + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 74 + name + + CF$UID + 3362 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3363 + + + -[SPTDataLoaderFactoryTest testSuccessfulResponse] + 79DCB509-CAB2-4DE8-B79D-26FD0A429038 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 84 + name + + CF$UID + 3365 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3366 + + + -[SPTDataLoaderFactoryTest testFailedResponse] + F1394D7C-7F45-47EF-9D47-8104748DC9F1 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 94 + name + + CF$UID + 3368 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3369 + + + -[SPTDataLoaderFactoryTest testCancelledRequest] + F31A9002-E3C2-4B26-80E0-CBEB13E293F0 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 103 + name + + CF$UID + 3371 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3372 + + + -[SPTDataLoaderFactoryTest testReceivedDataChunk] + B747E172-CF8E-4FA2-8B6A-A8BF4F64F68B + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 113 + name + + CF$UID + 3374 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3375 + + + -[SPTDataLoaderFactoryTest testReceivedInitialResponse] + 2BD5E1EC-1DC9-46B1-8EEB-AD786E136761 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 123 + name + + CF$UID + 3377 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3378 + + + -[SPTDataLoaderFactoryTest testShouldAuthoriseRequest] + E9C94991-4C58-4528-ABE0-94520F813C7E + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 132 + name + + CF$UID + 3380 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3381 + + + -[SPTDataLoaderFactoryTest testShouldNotAuthoriseRequest] + 33EFC879-310E-49EA-B491-FD845B0D69B8 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 140 + name + + CF$UID + 3383 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3384 + + + -[SPTDataLoaderFactoryTest testAuthoriseRequest] + 9451E87B-0E38-4D11-B44A-706D71253C93 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 149 + name + + CF$UID + 3386 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3387 + + + -[SPTDataLoaderFactoryTest testOfflineChangesCachePolicy] + 3EA320CA-E67D-49AB-BE73-C87A4B57B3F5 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 158 + name + + CF$UID + 3389 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3390 + + + -[SPTDataLoaderFactoryTest testRelayToDelegateWhenPerformingRequest] + 23EAE00E-D04B-4BCB-A584-2A706CF26315 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 168 + name + + CF$UID + 3392 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3393 + + + -[SPTDataLoaderFactoryTest testRelayAuthorisingSuccessToDelegate] + ED30F6D9-76A5-409B-961B-958BA8FC3C34 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 176 + name + + CF$UID + 3395 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3396 + + + -[SPTDataLoaderFactoryTest testRelayAuthorisationFailureToDelegate] + 322BE1B4-5D73-4CDF-8E90-31CA709F822D + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 185 + name + + CF$UID + 3398 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3399 + + + -[SPTDataLoaderFactoryTest testRetryAuthorisation] + 37E4088F-1EAB-4FE4-9A3C-61E0A71AB723 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 197 + name + + CF$UID + 3401 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3402 + + + -[SPTDataLoaderFactoryTest testRequestTimeout] + 12EEF3C5-1A8C-402F-B99F-01BD505134E5 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 201 + name + + CF$UID + 3404 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 3405 + + + __46-[SPTDataLoaderFactoryTest testRequestTimeout]_block_invoke + 7A211F53-8125-48E6-A62A-41844305FFBF + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 212 + name + + CF$UID + 3407 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3408 + + + -[SPTDataLoaderFactoryTest testForwardCancelToRequestResponseHandlerDelegate] + DAA6B239-CB2F-40EF-AF36-ACF882A78B05 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 220 + name + + CF$UID + 3410 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3411 + + + -[SPTDataLoaderFactoryTest testRequestingNewBodyStream] + 43EFD18E-D167-41E7-B246-D1DC4C762F72 + + $class + + CF$UID + 20 + + executionCount + 0 + lineCoverage + + CF$UID + 80 + + lineNumber + 224 + name + + CF$UID + 3413 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 3414 + + + __55-[SPTDataLoaderFactoryTest testRequestingNewBodyStream]_block_invoke + 6108FF90-BB1A-43C7-9815-814DE2EC812B + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3416 + + + CF$UID + 3417 + + + CF$UID + 3418 + + + CF$UID + 3419 + + + CF$UID + 3420 + + + CF$UID + 3421 + + + CF$UID + 3422 + + + CF$UID + 3423 + + + CF$UID + 3424 + + + CF$UID + 3425 + + + CF$UID + 3426 + + + CF$UID + 3427 + + + CF$UID + 3428 + + + CF$UID + 3429 + + + CF$UID + 3430 + + + CF$UID + 3431 + + + CF$UID + 3432 + + + CF$UID + 3433 + + + CF$UID + 3434 + + + CF$UID + 3435 + + + CF$UID + 3436 + + + CF$UID + 3437 + + + CF$UID + 3438 + + + CF$UID + 3439 + + + CF$UID + 3440 + + + CF$UID + 3441 + + + CF$UID + 3442 + + + CF$UID + 3443 + + + CF$UID + 3444 + + + CF$UID + 3445 + + + CF$UID + 3446 + + + CF$UID + 3447 + + + CF$UID + 3448 + + + CF$UID + 3449 + + + CF$UID + 3450 + + + CF$UID + 3451 + + + CF$UID + 3452 + + + CF$UID + 3453 + + + CF$UID + 3454 + + + CF$UID + 3455 + + + CF$UID + 3456 + + + CF$UID + 3457 + + + CF$UID + 3458 + + + CF$UID + 3459 + + + CF$UID + 3460 + + + CF$UID + 3461 + + + CF$UID + 3462 + + + CF$UID + 3463 + + + CF$UID + 3464 + + + CF$UID + 3465 + + + CF$UID + 3466 + + + CF$UID + 3467 + + + CF$UID + 3468 + + + CF$UID + 3469 + + + CF$UID + 3470 + + + CF$UID + 3471 + + + CF$UID + 3472 + + + CF$UID + 3473 + + + CF$UID + 3474 + + + CF$UID + 3475 + + + CF$UID + 3476 + + + CF$UID + 3477 + + + CF$UID + 3478 + + + CF$UID + 3479 + + + CF$UID + 3480 + + + CF$UID + 3481 + + + CF$UID + 3482 + + + CF$UID + 3483 + + + CF$UID + 3484 + + + CF$UID + 3485 + + + CF$UID + 3486 + + + CF$UID + 3487 + + + CF$UID + 3488 + + + CF$UID + 3489 + + + CF$UID + 3490 + + + CF$UID + 3491 + + + CF$UID + 3492 + + + CF$UID + 3493 + + + CF$UID + 3494 + + + CF$UID + 3495 + + + CF$UID + 3496 + + + CF$UID + 3497 + + + CF$UID + 3498 + + + CF$UID + 3499 + + + CF$UID + 3500 + + + CF$UID + 3501 + + + CF$UID + 3502 + + + CF$UID + 3503 + + + CF$UID + 3504 + + + CF$UID + 3505 + + + CF$UID + 3506 + + + CF$UID + 3507 + + + CF$UID + 3508 + + + CF$UID + 3509 + + + CF$UID + 3510 + + + CF$UID + 3511 + + + CF$UID + 3512 + + + CF$UID + 3513 + + + CF$UID + 3514 + + + CF$UID + 3515 + + + CF$UID + 3516 + + + CF$UID + 3517 + + + CF$UID + 3518 + + + CF$UID + 3519 + + + CF$UID + 3520 + + + CF$UID + 3521 + + + CF$UID + 3522 + + + CF$UID + 3523 + + + CF$UID + 3524 + + + CF$UID + 3525 + + + CF$UID + 3526 + + + CF$UID + 3527 + + + CF$UID + 3528 + + + CF$UID + 3529 + + + CF$UID + 3530 + + + CF$UID + 3531 + + + CF$UID + 3532 + + + CF$UID + 3533 + + + CF$UID + 3534 + + + CF$UID + 3535 + + + CF$UID + 3536 + + + CF$UID + 3537 + + + CF$UID + 3538 + + + CF$UID + 3539 + + + CF$UID + 3540 + + + CF$UID + 3541 + + + CF$UID + 3542 + + + CF$UID + 3543 + + + CF$UID + 3544 + + + CF$UID + 3545 + + + CF$UID + 3546 + + + CF$UID + 3547 + + + CF$UID + 3548 + + + CF$UID + 3549 + + + CF$UID + 3550 + + + CF$UID + 3551 + + + CF$UID + 3552 + + + CF$UID + 3553 + + + CF$UID + 3554 + + + CF$UID + 3555 + + + CF$UID + 3556 + + + CF$UID + 3557 + + + CF$UID + 3558 + + + CF$UID + 3559 + + + CF$UID + 3560 + + + CF$UID + 3561 + + + CF$UID + 3562 + + + CF$UID + 3563 + + + CF$UID + 3564 + + + CF$UID + 3565 + + + CF$UID + 3566 + + + CF$UID + 3567 + + + CF$UID + 3568 + + + CF$UID + 3569 + + + CF$UID + 3570 + + + CF$UID + 3571 + + + CF$UID + 3572 + + + CF$UID + 3573 + + + CF$UID + 3574 + + + CF$UID + 3575 + + + CF$UID + 3576 + + + CF$UID + 3577 + + + CF$UID + 3578 + + + CF$UID + 3579 + + + CF$UID + 3580 + + + CF$UID + 3581 + + + CF$UID + 3582 + + + CF$UID + 3583 + + + CF$UID + 3584 + + + CF$UID + 3585 + + + CF$UID + 3586 + + + CF$UID + 3587 + + + CF$UID + 3588 + + + CF$UID + 3589 + + + CF$UID + 3590 + + + CF$UID + 3591 + + + CF$UID + 3592 + + + CF$UID + 3593 + + + CF$UID + 3594 + + + CF$UID + 3595 + + + CF$UID + 3596 + + + CF$UID + 3597 + + + CF$UID + 3598 + + + CF$UID + 3599 + + + CF$UID + 3600 + + + CF$UID + 3601 + + + CF$UID + 3602 + + + CF$UID + 3603 + + + CF$UID + 3604 + + + CF$UID + 3605 + + + CF$UID + 3606 + + + CF$UID + 3607 + + + CF$UID + 3608 + + + CF$UID + 3609 + + + CF$UID + 3610 + + + CF$UID + 3611 + + + CF$UID + 3612 + + + CF$UID + 3613 + + + CF$UID + 3614 + + + CF$UID + 3615 + + + CF$UID + 3616 + + + CF$UID + 3617 + + + CF$UID + 3618 + + + CF$UID + 3619 + + + CF$UID + 3620 + + + CF$UID + 3621 + + + CF$UID + 3622 + + + CF$UID + 3623 + + + CF$UID + 3624 + + + CF$UID + 3625 + + + CF$UID + 3626 + + + CF$UID + 3627 + + + CF$UID + 3628 + + + CF$UID + 3629 + + + CF$UID + 3630 + + + CF$UID + 3631 + + + CF$UID + 3632 + + + CF$UID + 3633 + + + CF$UID + 3634 + + + CF$UID + 3635 + + + CF$UID + 3636 + + + CF$UID + 3637 + + + CF$UID + 3638 + + + CF$UID + 3639 + + + CF$UID + 3640 + + + CF$UID + 3641 + + + CF$UID + 3642 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 3646 + + functions + + CF$UID + 3647 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 3663 + + name + + CF$UID + 3644 + + uniqueIdentifier + + CF$UID + 3645 + + + SPTDataLoaderResolverAddressTest.m + B995C6FA-FF3A-42F4-A05D-F037DA0A3B37 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderResolverAddressTest.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3648 + + + CF$UID + 3651 + + + CF$UID + 3654 + + + CF$UID + 3657 + + + CF$UID + 3660 + + + + + $class + + CF$UID + 20 + + executionCount + 4 + lineCoverage + + CF$UID + 13 + + lineNumber + 42 + name + + CF$UID + 3649 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3650 + + + -[SPTDataLoaderResolverAddressTest setUp] + 73DC23AF-7450-48EF-B9B3-ACCC412AAAFC + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 50 + name + + CF$UID + 3652 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3653 + + + -[SPTDataLoaderResolverAddressTest testNotNil] + F8C6E73D-77EF-4759-89F2-69B2545BD016 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 55 + name + + CF$UID + 3655 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3656 + + + -[SPTDataLoaderResolverAddressTest testReachable] + 351C62B4-80CD-4F5B-970C-E7CA06793008 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 60 + name + + CF$UID + 3658 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3659 + + + -[SPTDataLoaderResolverAddressTest testNotReachableIfFailed] + A85D0DAC-2D24-4D49-8746-819735E48ED5 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 66 + name + + CF$UID + 3661 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3662 + + + -[SPTDataLoaderResolverAddressTest testLastFailedTimeNonsensical] + 04C2E396-5A2D-4CCA-AD8F-93F7237F8D5A + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3664 + + + CF$UID + 3665 + + + CF$UID + 3666 + + + CF$UID + 3667 + + + CF$UID + 3668 + + + CF$UID + 3669 + + + CF$UID + 3670 + + + CF$UID + 3671 + + + CF$UID + 3672 + + + CF$UID + 3673 + + + CF$UID + 3674 + + + CF$UID + 3675 + + + CF$UID + 3676 + + + CF$UID + 3677 + + + CF$UID + 3678 + + + CF$UID + 3679 + + + CF$UID + 3680 + + + CF$UID + 3681 + + + CF$UID + 3682 + + + CF$UID + 3683 + + + CF$UID + 3684 + + + CF$UID + 3685 + + + CF$UID + 3686 + + + CF$UID + 3687 + + + CF$UID + 3688 + + + CF$UID + 3689 + + + CF$UID + 3690 + + + CF$UID + 3691 + + + CF$UID + 3692 + + + CF$UID + 3693 + + + CF$UID + 3694 + + + CF$UID + 3695 + + + CF$UID + 3696 + + + CF$UID + 3697 + + + CF$UID + 3698 + + + CF$UID + 3699 + + + CF$UID + 3700 + + + CF$UID + 3701 + + + CF$UID + 3702 + + + CF$UID + 3703 + + + CF$UID + 3704 + + + CF$UID + 3705 + + + CF$UID + 3706 + + + CF$UID + 3707 + + + CF$UID + 3708 + + + CF$UID + 3709 + + + CF$UID + 3710 + + + CF$UID + 3711 + + + CF$UID + 3712 + + + CF$UID + 3713 + + + CF$UID + 3714 + + + CF$UID + 3715 + + + CF$UID + 3716 + + + CF$UID + 3717 + + + CF$UID + 3718 + + + CF$UID + 3719 + + + CF$UID + 3720 + + + CF$UID + 3721 + + + CF$UID + 3722 + + + CF$UID + 3723 + + + CF$UID + 3724 + + + CF$UID + 3725 + + + CF$UID + 3726 + + + CF$UID + 3727 + + + CF$UID + 3728 + + + CF$UID + 3729 + + + CF$UID + 3730 + + + CF$UID + 3731 + + + CF$UID + 3732 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 3737 + + functions + + CF$UID + 3738 + + lineCoverage + + CF$UID + 3736 + + lines + + CF$UID + 3793 + + name + + CF$UID + 3734 + + uniqueIdentifier + + CF$UID + 3735 + + + SPTDataLoaderRequestTaskHandlerTest.m + 407AABD5-DCFF-4F9F-B489-3906F849EDEA + 0.9921875 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/SPTDataLoaderRequestTaskHandlerTest.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3739 + + + CF$UID + 3742 + + + CF$UID + 3745 + + + CF$UID + 3748 + + + CF$UID + 3751 + + + CF$UID + 3754 + + + CF$UID + 3757 + + + CF$UID + 3760 + + + CF$UID + 3763 + + + CF$UID + 3766 + + + CF$UID + 3769 + + + CF$UID + 3772 + + + CF$UID + 3775 + + + CF$UID + 3778 + + + CF$UID + 3781 + + + CF$UID + 3784 + + + CF$UID + 3787 + + + CF$UID + 3790 + + + + + $class + + CF$UID + 20 + + executionCount + 15 + lineCoverage + + CF$UID + 13 + + lineNumber + 56 + name + + CF$UID + 3740 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3741 + + + -[SPTDataLoaderRequestTaskHandlerTest setUp] + 9DD11624-D310-4AB3-83A2-A9C8FD18E41F + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 72 + name + + CF$UID + 3743 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3744 + + + -[SPTDataLoaderRequestTaskHandlerTest testNotNil] + 74AA57E2-F14A-44F2-85F3-01F4688918B7 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 77 + name + + CF$UID + 3746 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3747 + + + -[SPTDataLoaderRequestTaskHandlerTest testReceiveDataRelayedToRequestResponseHandler] + DDA1E7E9-DFCB-448E-9452-3A3877C407B5 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 85 + name + + CF$UID + 3749 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3750 + + + -[SPTDataLoaderRequestTaskHandlerTest testRelaySuccessfulResponse] + 86095B1E-5BCF-480D-909E-1090BA7A6E47 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 91 + name + + CF$UID + 3752 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3753 + + + -[SPTDataLoaderRequestTaskHandlerTest testRelayFailedResponse] + 8BD232E5-4361-4306-B798-254614D484B1 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 99 + name + + CF$UID + 3755 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3756 + + + -[SPTDataLoaderRequestTaskHandlerTest testRelayRetryAfterToRateLimiter] + DEDA73C1-2B12-4D61-8202-32C5C68431CF + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 110 + name + + CF$UID + 3758 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3759 + + + -[SPTDataLoaderRequestTaskHandlerTest testRelayNewBodyStreamPrompt] + 2819BCA5-988F-412E-9109-9F9A867BB926 + + $class + + CF$UID + 20 + + executionCount + 0 + lineCoverage + + CF$UID + 80 + + lineNumber + 111 + name + + CF$UID + 3761 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 3762 + + + __67-[SPTDataLoaderRequestTaskHandlerTest testRelayNewBodyStreamPrompt]_block_invoke + 9B15092D-97AD-4366-B02F-2ACF15C5D027 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 116 + name + + CF$UID + 3764 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3765 + + + -[SPTDataLoaderRequestTaskHandlerTest testRetry] + 25935C4F-C466-4631-BFC8-9A95C746E66A + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 129 + name + + CF$UID + 3767 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3768 + + + -[SPTDataLoaderRequestTaskHandlerTest testDataCreationWithContentLengthFromResponse] + 5CB2B852-C2DD-49D2-AB26-5FA65F51AEBA + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 141 + name + + CF$UID + 3770 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3771 + + + -[SPTDataLoaderRequestTaskHandlerTest testStartCallsResume] + C66D5EB0-3991-4C05-A0D2-77F537AA4B9C + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 147 + name + + CF$UID + 3773 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3774 + + + -[SPTDataLoaderRequestTaskHandlerTest testResponseCreatedIfNoInitialDataReceived] + F935B265-2C00-4F0B-8F5C-32BB1A18B937 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 153 + name + + CF$UID + 3776 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3777 + + + -[SPTDataLoaderRequestTaskHandlerTest testDataAppendedWhenNotStreaming] + 998F86BF-743C-4F27-87C3-F51E838246E4 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 165 + name + + CF$UID + 3779 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3780 + + + -[SPTDataLoaderRequestTaskHandlerTest testCancelledError] + 852757B2-8777-465B-B39D-0F4BDF253512 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 173 + name + + CF$UID + 3782 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3783 + + + -[SPTDataLoaderRequestTaskHandlerTest testRetryWithRateLimiter] + 6D549B34-9308-4A24-8668-F05D53D0512C + + $class + + CF$UID + 20 + + executionCount + 3 + lineCoverage + + CF$UID + 13 + + lineNumber + 177 + name + + CF$UID + 3785 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 3786 + + + __63-[SPTDataLoaderRequestTaskHandlerTest testRetryWithRateLimiter]_block_invoke + 938831E5-5F60-401C-9D11-27827A462693 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 193 + name + + CF$UID + 3788 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3789 + + + -[SPTDataLoaderRequestTaskHandlerTest testCompletingWhenDeallocatingDuringFlight] + 0A7EC8F6-C945-46D8-9C63-087BAFC0AFB7 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 200 + name + + CF$UID + 3791 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 3792 + + + -[SPTDataLoaderRequestTaskHandlerTest testCancelledWhenReceivingCancelError] + 9617BE2F-6528-4D0B-B65C-6E21615BDC1B + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3794 + + + CF$UID + 3795 + + + CF$UID + 3796 + + + CF$UID + 3797 + + + CF$UID + 3798 + + + CF$UID + 3799 + + + CF$UID + 3800 + + + CF$UID + 3801 + + + CF$UID + 3802 + + + CF$UID + 3803 + + + CF$UID + 3804 + + + CF$UID + 3805 + + + CF$UID + 3806 + + + CF$UID + 3807 + + + CF$UID + 3808 + + + CF$UID + 3809 + + + CF$UID + 3810 + + + CF$UID + 3811 + + + CF$UID + 3812 + + + CF$UID + 3813 + + + CF$UID + 3814 + + + CF$UID + 3815 + + + CF$UID + 3816 + + + CF$UID + 3817 + + + CF$UID + 3818 + + + CF$UID + 3819 + + + CF$UID + 3820 + + + CF$UID + 3821 + + + CF$UID + 3822 + + + CF$UID + 3823 + + + CF$UID + 3824 + + + CF$UID + 3825 + + + CF$UID + 3826 + + + CF$UID + 3827 + + + CF$UID + 3828 + + + CF$UID + 3829 + + + CF$UID + 3830 + + + CF$UID + 3831 + + + CF$UID + 3832 + + + CF$UID + 3833 + + + CF$UID + 3834 + + + CF$UID + 3835 + + + CF$UID + 3836 + + + CF$UID + 3837 + + + CF$UID + 3838 + + + CF$UID + 3839 + + + CF$UID + 3840 + + + CF$UID + 3841 + + + CF$UID + 3842 + + + CF$UID + 3843 + + + CF$UID + 3844 + + + CF$UID + 3845 + + + CF$UID + 3846 + + + CF$UID + 3847 + + + CF$UID + 3848 + + + CF$UID + 3849 + + + CF$UID + 3850 + + + CF$UID + 3851 + + + CF$UID + 3852 + + + CF$UID + 3853 + + + CF$UID + 3854 + + + CF$UID + 3855 + + + CF$UID + 3856 + + + CF$UID + 3857 + + + CF$UID + 3858 + + + CF$UID + 3859 + + + CF$UID + 3860 + + + CF$UID + 3861 + + + CF$UID + 3862 + + + CF$UID + 3863 + + + CF$UID + 3864 + + + CF$UID + 3865 + + + CF$UID + 3866 + + + CF$UID + 3867 + + + CF$UID + 3868 + + + CF$UID + 3869 + + + CF$UID + 3870 + + + CF$UID + 3871 + + + CF$UID + 3872 + + + CF$UID + 3873 + + + CF$UID + 3874 + + + CF$UID + 3875 + + + CF$UID + 3876 + + + CF$UID + 3877 + + + CF$UID + 3878 + + + CF$UID + 3879 + + + CF$UID + 3880 + + + CF$UID + 3881 + + + CF$UID + 3882 + + + CF$UID + 3883 + + + CF$UID + 3884 + + + CF$UID + 3885 + + + CF$UID + 3886 + + + CF$UID + 3887 + + + CF$UID + 3888 + + + CF$UID + 3889 + + + CF$UID + 3890 + + + CF$UID + 3891 + + + CF$UID + 3892 + + + CF$UID + 3893 + + + CF$UID + 3894 + + + CF$UID + 3895 + + + CF$UID + 3896 + + + CF$UID + 3897 + + + CF$UID + 3898 + + + CF$UID + 3899 + + + CF$UID + 3900 + + + CF$UID + 3901 + + + CF$UID + 3902 + + + CF$UID + 3903 + + + CF$UID + 3904 + + + CF$UID + 3905 + + + CF$UID + 3906 + + + CF$UID + 3907 + + + CF$UID + 3908 + + + CF$UID + 3909 + + + CF$UID + 3910 + + + CF$UID + 3911 + + + CF$UID + 3912 + + + CF$UID + 3913 + + + CF$UID + 3914 + + + CF$UID + 3915 + + + CF$UID + 3916 + + + CF$UID + 3917 + + + CF$UID + 3918 + + + CF$UID + 3919 + + + CF$UID + 3920 + + + CF$UID + 3921 + + + CF$UID + 3922 + + + CF$UID + 3923 + + + CF$UID + 3924 + + + CF$UID + 3925 + + + CF$UID + 3926 + + + CF$UID + 3927 + + + CF$UID + 3928 + + + CF$UID + 3929 + + + CF$UID + 3930 + + + CF$UID + 3931 + + + CF$UID + 3932 + + + CF$UID + 3933 + + + CF$UID + 3934 + + + CF$UID + 3935 + + + CF$UID + 3936 + + + CF$UID + 3937 + + + CF$UID + 3938 + + + CF$UID + 3939 + + + CF$UID + 3940 + + + CF$UID + 3941 + + + CF$UID + 3942 + + + CF$UID + 3943 + + + CF$UID + 3944 + + + CF$UID + 3945 + + + CF$UID + 3946 + + + CF$UID + 3947 + + + CF$UID + 3948 + + + CF$UID + 3949 + + + CF$UID + 3950 + + + CF$UID + 3951 + + + CF$UID + 3952 + + + CF$UID + 3953 + + + CF$UID + 3954 + + + CF$UID + 3955 + + + CF$UID + 3956 + + + CF$UID + 3957 + + + CF$UID + 3958 + + + CF$UID + 3959 + + + CF$UID + 3960 + + + CF$UID + 3961 + + + CF$UID + 3962 + + + CF$UID + 3963 + + + CF$UID + 3964 + + + CF$UID + 3965 + + + CF$UID + 3966 + + + CF$UID + 3967 + + + CF$UID + 3968 + + + CF$UID + 3969 + + + CF$UID + 3970 + + + CF$UID + 3971 + + + CF$UID + 3972 + + + CF$UID + 3973 + + + CF$UID + 3974 + + + CF$UID + 3975 + + + CF$UID + 3978 + + + CF$UID + 3979 + + + CF$UID + 3980 + + + CF$UID + 3981 + + + CF$UID + 3982 + + + CF$UID + 3983 + + + CF$UID + 3984 + + + CF$UID + 3985 + + + CF$UID + 3986 + + + CF$UID + 3987 + + + CF$UID + 3988 + + + CF$UID + 3989 + + + CF$UID + 3990 + + + CF$UID + 3991 + + + CF$UID + 3992 + + + CF$UID + 3993 + + + CF$UID + 3994 + + + CF$UID + 3995 + + + CF$UID + 3996 + + + CF$UID + 3997 + + + CF$UID + 3998 + + + CF$UID + 3999 + + + CF$UID + 4000 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 3976 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 3977 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 182 + len + 95 + x + 3 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 4005 + + functions + + CF$UID + 4006 + + lineCoverage + + CF$UID + 4004 + + lines + + CF$UID + 4027 + + name + + CF$UID + 4002 + + uniqueIdentifier + + CF$UID + 4003 + + + NSURLAuthenticationChallengeMock.m + B549B84F-7000-4A81-8D2B-59ADB51A63AF + 0.96296296296296291 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/NSURLAuthenticationChallengeMock.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4007 + + + CF$UID + 4011 + + + CF$UID + 4014 + + + CF$UID + 4017 + + + CF$UID + 4020 + + + CF$UID + 4023 + + + + + $class + + CF$UID + 20 + + executionCount + 5 + lineCoverage + + CF$UID + 13 + + lineNumber + 43 + name + + CF$UID + 4008 + + symbolKindIdentifier + + CF$UID + 4010 + + uniqueIdentifier + + CF$UID + 4009 + + + +[NSURLAuthenticationChallengeMock mockAuthenticationChallengeWithHost:authenticationMethod:serverTrust:] + 7C1DDF8A-9E30-45BC-B1AC-240F09AC0F1B + Xcode.SourceCodeSymbolKind.ClassMethod + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 71 + name + + CF$UID + 4012 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4013 + + + -[NSURLProtectionSpaceMock host] + 0EF02B96-105F-480E-B071-97E664420685 + + $class + + CF$UID + 20 + + executionCount + 5 + lineCoverage + + CF$UID + 13 + + lineNumber + 76 + name + + CF$UID + 4015 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4016 + + + -[NSURLProtectionSpaceMock serverTrust] + 99236E6D-F52B-47F4-A27E-7CBBBA88C9A7 + + $class + + CF$UID + 20 + + executionCount + 5 + lineCoverage + + CF$UID + 13 + + lineNumber + 81 + name + + CF$UID + 4018 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4019 + + + -[NSURLProtectionSpaceMock dealloc] + 89CF6532-0352-414E-AFC4-EA6BB7ECAD26 + + $class + + CF$UID + 20 + + executionCount + 5 + lineCoverage + + CF$UID + 13 + + lineNumber + 91 + name + + CF$UID + 4021 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4022 + + + -[NSURLProtectionSpaceMock mockServerTrust] + 3A061C90-5E64-42A9-9412-19A0B05DF055 + + $class + + CF$UID + 20 + + executionCount + 5 + lineCoverage + + CF$UID + 4026 + + lineNumber + 99 + name + + CF$UID + 4024 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4025 + + + -[NSURLProtectionSpaceMock setMockServerTrust:] + ADCAE542-59E4-40D3-BCBE-9A603F588FB1 + 0.875 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4028 + + + CF$UID + 4029 + + + CF$UID + 4030 + + + CF$UID + 4031 + + + CF$UID + 4032 + + + CF$UID + 4033 + + + CF$UID + 4034 + + + CF$UID + 4035 + + + CF$UID + 4036 + + + CF$UID + 4037 + + + CF$UID + 4038 + + + CF$UID + 4039 + + + CF$UID + 4040 + + + CF$UID + 4041 + + + CF$UID + 4042 + + + CF$UID + 4043 + + + CF$UID + 4044 + + + CF$UID + 4045 + + + CF$UID + 4046 + + + CF$UID + 4047 + + + CF$UID + 4048 + + + CF$UID + 4049 + + + CF$UID + 4050 + + + CF$UID + 4051 + + + CF$UID + 4052 + + + CF$UID + 4053 + + + CF$UID + 4054 + + + CF$UID + 4055 + + + CF$UID + 4056 + + + CF$UID + 4057 + + + CF$UID + 4058 + + + CF$UID + 4059 + + + CF$UID + 4060 + + + CF$UID + 4061 + + + CF$UID + 4062 + + + CF$UID + 4063 + + + CF$UID + 4064 + + + CF$UID + 4065 + + + CF$UID + 4066 + + + CF$UID + 4067 + + + CF$UID + 4068 + + + CF$UID + 4069 + + + CF$UID + 4070 + + + CF$UID + 4071 + + + CF$UID + 4074 + + + CF$UID + 4075 + + + CF$UID + 4076 + + + CF$UID + 4077 + + + CF$UID + 4078 + + + CF$UID + 4079 + + + CF$UID + 4080 + + + CF$UID + 4081 + + + CF$UID + 4082 + + + CF$UID + 4083 + + + CF$UID + 4084 + + + CF$UID + 4085 + + + CF$UID + 4086 + + + CF$UID + 4087 + + + CF$UID + 4088 + + + CF$UID + 4089 + + + CF$UID + 4090 + + + CF$UID + 4091 + + + CF$UID + 4092 + + + CF$UID + 4093 + + + CF$UID + 4094 + + + CF$UID + 4095 + + + CF$UID + 4096 + + + CF$UID + 4097 + + + CF$UID + 4098 + + + CF$UID + 4099 + + + CF$UID + 4100 + + + CF$UID + 4101 + + + CF$UID + 4102 + + + CF$UID + 4103 + + + CF$UID + 4104 + + + CF$UID + 4105 + + + CF$UID + 4106 + + + CF$UID + 4107 + + + CF$UID + 4108 + + + CF$UID + 4109 + + + CF$UID + 4110 + + + CF$UID + 4111 + + + CF$UID + 4114 + + + CF$UID + 4115 + + + CF$UID + 4116 + + + CF$UID + 4117 + + + CF$UID + 4118 + + + CF$UID + 4119 + + + CF$UID + 4120 + + + CF$UID + 4121 + + + CF$UID + 4122 + + + CF$UID + 4123 + + + CF$UID + 4124 + + + CF$UID + 4125 + + + CF$UID + 4126 + + + CF$UID + 4127 + + + CF$UID + 4128 + + + CF$UID + 4129 + + + CF$UID + 4130 + + + CF$UID + 4131 + + + CF$UID + 4134 + + + CF$UID + 4135 + + + CF$UID + 4136 + + + CF$UID + 4137 + + + CF$UID + 4138 + + + CF$UID + 4139 + + + CF$UID + 4140 + + + CF$UID + 4143 + + + CF$UID + 4144 + + + CF$UID + 4145 + + + CF$UID + 4146 + + + CF$UID + 4147 + + + CF$UID + 4148 + + + CF$UID + 4149 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 4072 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4073 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 44 + len + 114 + x + 2 + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 4112 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4113 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 82 + len + 26 + x + 5 + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 4132 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4133 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 100 + len + 15 + x + 5 + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 4141 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4142 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 107 + len + 26 + x + 5 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 4153 + + functions + + CF$UID + 4154 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 4158 + + name + + CF$UID + 4151 + + uniqueIdentifier + + CF$UID + 4152 + + + NSURLSessionMock.m + 20741CF1-4103-4D5B-BFE5-659119E836F6 + /Users/dflems/Code/SPTDataLoader/SPTDataLoaderTests/NSURLSessionMock.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4155 + + + + + $class + + CF$UID + 20 + + executionCount + 20 + lineCoverage + + CF$UID + 13 + + lineNumber + 27 + name + + CF$UID + 4156 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4157 + + + -[NSURLSessionMock dataTaskWithRequest:] + D80E8B6D-A96F-4EED-89EB-5FA6E790DE80 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4159 + + + CF$UID + 4160 + + + CF$UID + 4161 + + + CF$UID + 4162 + + + CF$UID + 4163 + + + CF$UID + 4164 + + + CF$UID + 4165 + + + CF$UID + 4166 + + + CF$UID + 4167 + + + CF$UID + 4168 + + + CF$UID + 4169 + + + CF$UID + 4170 + + + CF$UID + 4171 + + + CF$UID + 4172 + + + CF$UID + 4173 + + + CF$UID + 4174 + + + CF$UID + 4175 + + + CF$UID + 4176 + + + CF$UID + 4177 + + + CF$UID + 4178 + + + CF$UID + 4179 + + + CF$UID + 4180 + + + CF$UID + 4181 + + + CF$UID + 4182 + + + CF$UID + 4183 + + + CF$UID + 4184 + + + CF$UID + 4185 + + + CF$UID + 4186 + + + CF$UID + 4187 + + + CF$UID + 4188 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 4192 + + functions + + CF$UID + 4193 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 4203 + + name + + CF$UID + 4190 + + uniqueIdentifier + + CF$UID + 4191 + + + SPTDataLoaderCancellationTokenImplementation.m + AB77BBC0-D97F-4A54-B20E-F635C835FB17 + /Users/dflems/Code/SPTDataLoader/SPTDataLoader/SPTDataLoaderCancellationTokenImplementation.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4194 + + + CF$UID + 4197 + + + CF$UID + 4200 + + + + + $class + + CF$UID + 20 + + executionCount + 20 + lineCoverage + + CF$UID + 13 + + lineNumber + 38 + name + + CF$UID + 4195 + + symbolKindIdentifier + + CF$UID + 4010 + + uniqueIdentifier + + CF$UID + 4196 + + + +[SPTDataLoaderCancellationTokenImplementation cancellationTokenImplementationWithDelegate:cancelObject:] + 3D8F076A-22C4-4923-B336-DD0F2AD710E2 + + $class + + CF$UID + 20 + + executionCount + 20 + lineCoverage + + CF$UID + 13 + + lineNumber + 43 + name + + CF$UID + 4198 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4199 + + + -[SPTDataLoaderCancellationTokenImplementation initWithDelegate:cancelObject:] + B954B58D-D7CD-4FE3-AB78-BF48AB817A19 + + $class + + CF$UID + 20 + + executionCount + 10 + lineCoverage + + CF$UID + 13 + + lineNumber + 60 + name + + CF$UID + 4201 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4202 + + + -[SPTDataLoaderCancellationTokenImplementation cancel] + 68760055-AB36-4420-9CA5-4C5F95CD6ACD + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4204 + + + CF$UID + 4205 + + + CF$UID + 4206 + + + CF$UID + 4207 + + + CF$UID + 4208 + + + CF$UID + 4209 + + + CF$UID + 4210 + + + CF$UID + 4211 + + + CF$UID + 4212 + + + CF$UID + 4213 + + + CF$UID + 4214 + + + CF$UID + 4215 + + + CF$UID + 4216 + + + CF$UID + 4217 + + + CF$UID + 4218 + + + CF$UID + 4219 + + + CF$UID + 4220 + + + CF$UID + 4221 + + + CF$UID + 4222 + + + CF$UID + 4223 + + + CF$UID + 4224 + + + CF$UID + 4225 + + + CF$UID + 4226 + + + CF$UID + 4227 + + + CF$UID + 4228 + + + CF$UID + 4229 + + + CF$UID + 4230 + + + CF$UID + 4231 + + + CF$UID + 4232 + + + CF$UID + 4233 + + + CF$UID + 4234 + + + CF$UID + 4235 + + + CF$UID + 4236 + + + CF$UID + 4237 + + + CF$UID + 4238 + + + CF$UID + 4239 + + + CF$UID + 4240 + + + CF$UID + 4241 + + + CF$UID + 4242 + + + CF$UID + 4243 + + + CF$UID + 4244 + + + CF$UID + 4245 + + + CF$UID + 4246 + + + CF$UID + 4247 + + + CF$UID + 4248 + + + CF$UID + 4249 + + + CF$UID + 4250 + + + CF$UID + 4251 + + + CF$UID + 4252 + + + CF$UID + 4253 + + + CF$UID + 4254 + + + CF$UID + 4255 + + + CF$UID + 4256 + + + CF$UID + 4257 + + + CF$UID + 4258 + + + CF$UID + 4259 + + + CF$UID + 4260 + + + CF$UID + 4261 + + + CF$UID + 4262 + + + CF$UID + 4263 + + + CF$UID + 4264 + + + CF$UID + 4267 + + + CF$UID + 4268 + + + CF$UID + 4269 + + + CF$UID + 4270 + + + CF$UID + 4271 + + + CF$UID + 4272 + + + CF$UID + 4273 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 4265 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4266 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 61 + len + 24 + x + 10 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 9 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 9 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 9 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 9 + s + + CF$UID + 4274 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4275 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 68 + len + 1 + x + 10 + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 4280 + + functions + + CF$UID + 4281 + + lineCoverage + + CF$UID + 4279 + + lines + + CF$UID + 4308 + + name + + CF$UID + 4277 + + uniqueIdentifier + + CF$UID + 4278 + + + SPTDataLoaderExponentialTimer.m + 7A157CA5-50FE-4BB6-8083-656FE24495C3 + 0.96052631578947367 + /Users/dflems/Code/SPTDataLoader/SPTDataLoader/SPTDataLoaderExponentialTimer.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4282 + + + CF$UID + 4285 + + + CF$UID + 4288 + + + CF$UID + 4292 + + + CF$UID + 4295 + + + CF$UID + 4298 + + + CF$UID + 4301 + + + CF$UID + 4305 + + + + + $class + + CF$UID + 20 + + executionCount + 34 + lineCoverage + + CF$UID + 13 + + lineNumber + 55 + name + + CF$UID + 4283 + + symbolKindIdentifier + + CF$UID + 4010 + + uniqueIdentifier + + CF$UID + 4284 + + + +[SPTDataLoaderExponentialTimer exponentialTimerWithInitialTime:maxTime:] + 80AE795A-F598-4433-B1A4-995C398333D2 + + $class + + CF$UID + 20 + + executionCount + 35 + lineCoverage + + CF$UID + 13 + + lineNumber + 62 + name + + CF$UID + 4286 + + symbolKindIdentifier + + CF$UID + 4010 + + uniqueIdentifier + + CF$UID + 4287 + + + +[SPTDataLoaderExponentialTimer exponentialTimerWithInitialTime:maxTime:jitter:] + 7C16F93B-8D2C-4F1D-98D9-8473A1000499 + + $class + + CF$UID + 20 + + executionCount + 35 + lineCoverage + + CF$UID + 4291 + + lineNumber + 73 + name + + CF$UID + 4289 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4290 + + + -[SPTDataLoaderExponentialTimer initWithInitialTime:maxTime:growFactor:jitter:] + 507EDFE4-4E14-4936-B9BC-7F4C6145E5FF + 0.84615384615384615 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 90 + name + + CF$UID + 4293 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4294 + + + -[SPTDataLoaderExponentialTimer reset] + 2B4A4897-A8FA-4D1A-B53E-78D6755F3EA8 + + $class + + CF$UID + 20 + + executionCount + 115 + lineCoverage + + CF$UID + 13 + + lineNumber + 95 + name + + CF$UID + 4296 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4297 + + + -[SPTDataLoaderExponentialTimer calculateNext] + 29AC12C0-9821-4324-A5A1-26ADA9D636AA + + $class + + CF$UID + 20 + + executionCount + 115 + lineCoverage + + CF$UID + 13 + + lineNumber + 117 + name + + CF$UID + 4299 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4300 + + + -[SPTDataLoaderExponentialTimer timeIntervalAndCalculateNext] + F3BEA49E-D1F5-46DA-9A63-AEDF7C181D00 + + $class + + CF$UID + 20 + + executionCount + 105 + lineCoverage + + CF$UID + 4304 + + lineNumber + 134 + name + + CF$UID + 4302 + + symbolKindIdentifier + + CF$UID + 4010 + + uniqueIdentifier + + CF$UID + 4303 + + + +[SPTDataLoaderExponentialTimer normalWithMu:sigma:] + A6FE2476-0D3A-4290-BB14-DAE95BC00089 + 0.95454545454545459 + + $class + + CF$UID + 20 + + executionCount + 288 + lineCoverage + + CF$UID + 13 + + lineNumber + 129 + name + + CF$UID + 4306 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 4307 + + + SPTExptRandom + 700CAA85-2035-44D4-B355-2929E0994A6E + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4309 + + + CF$UID + 4310 + + + CF$UID + 4311 + + + CF$UID + 4312 + + + CF$UID + 4313 + + + CF$UID + 4314 + + + CF$UID + 4315 + + + CF$UID + 4316 + + + CF$UID + 4317 + + + CF$UID + 4318 + + + CF$UID + 4319 + + + CF$UID + 4320 + + + CF$UID + 4321 + + + CF$UID + 4322 + + + CF$UID + 4323 + + + CF$UID + 4324 + + + CF$UID + 4325 + + + CF$UID + 4326 + + + CF$UID + 4327 + + + CF$UID + 4328 + + + CF$UID + 4329 + + + CF$UID + 4330 + + + CF$UID + 4331 + + + CF$UID + 4332 + + + CF$UID + 4333 + + + CF$UID + 4334 + + + CF$UID + 4335 + + + CF$UID + 4336 + + + CF$UID + 4337 + + + CF$UID + 4338 + + + CF$UID + 4339 + + + CF$UID + 4340 + + + CF$UID + 4341 + + + CF$UID + 4342 + + + CF$UID + 4343 + + + CF$UID + 4344 + + + CF$UID + 4345 + + + CF$UID + 4346 + + + CF$UID + 4347 + + + CF$UID + 4348 + + + CF$UID + 4349 + + + CF$UID + 4350 + + + CF$UID + 4351 + + + CF$UID + 4352 + + + CF$UID + 4353 + + + CF$UID + 4354 + + + CF$UID + 4355 + + + CF$UID + 4356 + + + CF$UID + 4357 + + + CF$UID + 4358 + + + CF$UID + 4359 + + + CF$UID + 4360 + + + CF$UID + 4361 + + + CF$UID + 4362 + + + CF$UID + 4363 + + + CF$UID + 4364 + + + CF$UID + 4365 + + + CF$UID + 4366 + + + CF$UID + 4367 + + + CF$UID + 4368 + + + CF$UID + 4369 + + + CF$UID + 4370 + + + CF$UID + 4371 + + + CF$UID + 4372 + + + CF$UID + 4373 + + + CF$UID + 4374 + + + CF$UID + 4375 + + + CF$UID + 4376 + + + CF$UID + 4377 + + + CF$UID + 4378 + + + CF$UID + 4379 + + + CF$UID + 4380 + + + CF$UID + 4381 + + + CF$UID + 4382 + + + CF$UID + 4385 + + + CF$UID + 4386 + + + CF$UID + 4387 + + + CF$UID + 4388 + + + CF$UID + 4389 + + + CF$UID + 4390 + + + CF$UID + 4391 + + + CF$UID + 4392 + + + CF$UID + 4393 + + + CF$UID + 4394 + + + CF$UID + 4395 + + + CF$UID + 4396 + + + CF$UID + 4397 + + + CF$UID + 4398 + + + CF$UID + 4399 + + + CF$UID + 4400 + + + CF$UID + 4401 + + + CF$UID + 4402 + + + CF$UID + 4403 + + + CF$UID + 4404 + + + CF$UID + 4405 + + + CF$UID + 4406 + + + CF$UID + 4407 + + + CF$UID + 4408 + + + CF$UID + 4411 + + + CF$UID + 4412 + + + CF$UID + 4413 + + + CF$UID + 4414 + + + CF$UID + 4417 + + + CF$UID + 4418 + + + CF$UID + 4421 + + + CF$UID + 4422 + + + CF$UID + 4423 + + + CF$UID + 4424 + + + CF$UID + 4425 + + + CF$UID + 4428 + + + CF$UID + 4429 + + + CF$UID + 4430 + + + CF$UID + 4431 + + + CF$UID + 4432 + + + CF$UID + 4433 + + + CF$UID + 4434 + + + CF$UID + 4435 + + + CF$UID + 4436 + + + CF$UID + 4437 + + + CF$UID + 4438 + + + CF$UID + 4439 + + + CF$UID + 4440 + + + CF$UID + 4441 + + + CF$UID + 4442 + + + CF$UID + 4443 + + + CF$UID + 4444 + + + CF$UID + 4445 + + + CF$UID + 4446 + + + CF$UID + 4447 + + + CF$UID + 4448 + + + CF$UID + 4449 + + + CF$UID + 4450 + + + CF$UID + 4451 + + + CF$UID + 4452 + + + CF$UID + 4453 + + + CF$UID + 4454 + + + CF$UID + 4455 + + + CF$UID + 4456 + + + CF$UID + 4457 + + + CF$UID + 4458 + + + CF$UID + 4459 + + + CF$UID + 4460 + + + CF$UID + 4461 + + + CF$UID + 4467 + + + CF$UID + 4468 + + + CF$UID + 4469 + + + CF$UID + 4470 + + + CF$UID + 4471 + + + CF$UID + 4472 + + + CF$UID + 4475 + + + CF$UID + 4476 + + + CF$UID + 4477 + + + CF$UID + 4478 + + + CF$UID + 4479 + + + CF$UID + 4482 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 34 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 34 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 34 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 4383 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4384 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 74 + len + 32 + x + 35 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 115 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 115 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 115 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 115 + s + + CF$UID + 4409 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4410 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 98 + len + 33 + x + 115 + + + $class + + CF$UID + 24 + + c + 99 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 99 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 115 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 115 + s + + CF$UID + 4415 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4416 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 102 + len + 30 + x + 115 + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 105 + s + + CF$UID + 4419 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4420 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 104 + len + 11 + x + 115 + + + $class + + CF$UID + 24 + + c + 105 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 105 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 105 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 115 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 115 + s + + CF$UID + 4426 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4427 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 109 + len + 42 + x + 115 + + + $class + + CF$UID + 24 + + c + 54 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 54 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 115 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 115 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 115 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 115 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 115 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 115 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 115 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 115 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 115 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 288 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 288 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 288 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 105 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 105 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 105 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 105 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 105 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 105 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 105 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 105 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 105 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 144 + s + + CF$UID + 4462 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4463 + + + CF$UID + 4464 + + + CF$UID + 4465 + + + CF$UID + 4466 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 143 + len + 32 + x + 144 + + + $class + + CF$UID + 136 + + c + 33 + l + 143 + len + 2 + x + 105 + + + $class + + CF$UID + 136 + + c + 35 + l + 143 + len + 3 + x + 39 + + + $class + + CF$UID + 136 + + c + 38 + l + 143 + len + 2 + x + 105 + + + $class + + CF$UID + 24 + + c + 144 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 144 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 144 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 144 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 144 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 144 + s + + CF$UID + 4473 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4474 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 149 + len + 32 + x + 144 + + + $class + + CF$UID + 24 + + c + 105 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 105 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 144 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 105 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 4480 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4481 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 154 + len + 65 + x + 0 + + + $class + + CF$UID + 24 + + c + 105 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 4486 + + functions + + CF$UID + 4487 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 4530 + + name + + CF$UID + 4484 + + uniqueIdentifier + + CF$UID + 4485 + + + SPTDataLoaderRequestTaskHandler.m + 453849E1-8A5B-40A6-BCF5-62D6085DAA7E + /Users/dflems/Code/SPTDataLoader/SPTDataLoader/SPTDataLoaderRequestTaskHandler.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4488 + + + CF$UID + 4491 + + + CF$UID + 4494 + + + CF$UID + 4497 + + + CF$UID + 4500 + + + CF$UID + 4503 + + + CF$UID + 4506 + + + CF$UID + 4509 + + + CF$UID + 4512 + + + CF$UID + 4515 + + + CF$UID + 4518 + + + CF$UID + 4521 + + + CF$UID + 4524 + + + CF$UID + 4527 + + + + + $class + + CF$UID + 20 + + executionCount + 31 + lineCoverage + + CF$UID + 13 + + lineNumber + 68 + name + + CF$UID + 4489 + + symbolKindIdentifier + + CF$UID + 4010 + + uniqueIdentifier + + CF$UID + 4490 + + + +[SPTDataLoaderRequestTaskHandler dataLoaderRequestTaskHandlerWithTask:request:requestResponseHandler:rateLimiter:] + FCD3DA2F-5878-4268-9B84-DDE4C3798920 + + $class + + CF$UID + 20 + + executionCount + 31 + lineCoverage + + CF$UID + 13 + + lineNumber + 79 + name + + CF$UID + 4492 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4493 + + + -[SPTDataLoaderRequestTaskHandler initWithTask:request:requestResponseHandler:rateLimiter:] + 5AA99135-FA28-4D26-A548-F19B353744C8 + + $class + + CF$UID + 20 + + executionCount + 34 + lineCoverage + + CF$UID + 13 + + lineNumber + 91 + name + + CF$UID + 4495 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 4496 + + + __91-[SPTDataLoaderRequestTaskHandler initWithTask:request:requestResponseHandler:rateLimiter:]_block_invoke + 2FCFB63E-61CB-4887-A9B7-4912B870EFDB + + $class + + CF$UID + 20 + + executionCount + 4 + lineCoverage + + CF$UID + 13 + + lineNumber + 103 + name + + CF$UID + 4498 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4499 + + + -[SPTDataLoaderRequestTaskHandler receiveData:] + 4E9E0384-7283-4E48-8270-1B99F389C23F + + $class + + CF$UID + 20 + + executionCount + 4 + lineCoverage + + CF$UID + 13 + + lineNumber + 104 + name + + CF$UID + 4501 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 4502 + + + __47-[SPTDataLoaderRequestTaskHandler receiveData:]_block_invoke + 79DE2A1B-5908-4416-87ED-ACDAE642A9B5 + + $class + + CF$UID + 20 + + executionCount + 18 + lineCoverage + + CF$UID + 13 + + lineNumber + 117 + name + + CF$UID + 4504 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4505 + + + -[SPTDataLoaderRequestTaskHandler completeWithError:] + 8E2A6DB3-824C-4A16-8620-970C2CE1B352 + + $class + + CF$UID + 20 + + executionCount + 10 + lineCoverage + + CF$UID + 13 + + lineNumber + 162 + name + + CF$UID + 4507 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4508 + + + -[SPTDataLoaderRequestTaskHandler receiveResponse:] + 451C7943-D65C-4EA2-B8EB-8DDCB3E5EE65 + + $class + + CF$UID + 20 + + executionCount + 15 + lineCoverage + + CF$UID + 13 + + lineNumber + 181 + name + + CF$UID + 4510 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4511 + + + -[SPTDataLoaderRequestTaskHandler mayRedirect] + F26A2CC7-48BC-4055-8B18-0CD3007888B9 + + $class + + CF$UID + 20 + + executionCount + 25 + lineCoverage + + CF$UID + 13 + + lineNumber + 191 + name + + CF$UID + 4513 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4514 + + + -[SPTDataLoaderRequestTaskHandler start] + 78DE3EB4-F9AF-4C63-B7BA-3F3E2AF4BE73 + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 197 + name + + CF$UID + 4516 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4517 + + + -[SPTDataLoaderRequestTaskHandler provideNewBodyStreamWithCompletion:] + 5D74431B-6D02-417E-879A-1900E61650E3 + + $class + + CF$UID + 20 + + executionCount + 34 + lineCoverage + + CF$UID + 13 + + lineNumber + 202 + name + + CF$UID + 4519 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4520 + + + -[SPTDataLoaderRequestTaskHandler checkRateLimiterAndExecute] + 49B16DD8-A824-400D-B2C6-9835492E76D5 + + $class + + CF$UID + 20 + + executionCount + 28 + lineCoverage + + CF$UID + 13 + + lineNumber + 215 + name + + CF$UID + 4522 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4523 + + + -[SPTDataLoaderRequestTaskHandler checkRetryLimiterAndExecute] + 673DFE04-D166-4507-8BAF-FFC2579A1AEF + + $class + + CF$UID + 20 + + executionCount + 5 + lineCoverage + + CF$UID + 13 + + lineNumber + 235 + name + + CF$UID + 4525 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4526 + + + -[SPTDataLoaderRequestTaskHandler completeIfInFlight] + F9A942A2-6BF7-4480-8EBE-807489CF73DD + + $class + + CF$UID + 20 + + executionCount + 4 + lineCoverage + + CF$UID + 13 + + lineNumber + 245 + name + + CF$UID + 4528 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4529 + + + -[SPTDataLoaderRequestTaskHandler dealloc] + 20FB66B2-CBCD-4ED1-92FB-97B4C466841E + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4531 + + + CF$UID + 4532 + + + CF$UID + 4533 + + + CF$UID + 4534 + + + CF$UID + 4535 + + + CF$UID + 4536 + + + CF$UID + 4537 + + + CF$UID + 4538 + + + CF$UID + 4539 + + + CF$UID + 4540 + + + CF$UID + 4541 + + + CF$UID + 4542 + + + CF$UID + 4543 + + + CF$UID + 4544 + + + CF$UID + 4545 + + + CF$UID + 4546 + + + CF$UID + 4547 + + + CF$UID + 4548 + + + CF$UID + 4549 + + + CF$UID + 4550 + + + CF$UID + 4551 + + + CF$UID + 4552 + + + CF$UID + 4553 + + + CF$UID + 4554 + + + CF$UID + 4555 + + + CF$UID + 4556 + + + CF$UID + 4557 + + + CF$UID + 4558 + + + CF$UID + 4559 + + + CF$UID + 4560 + + + CF$UID + 4561 + + + CF$UID + 4562 + + + CF$UID + 4563 + + + CF$UID + 4564 + + + CF$UID + 4565 + + + CF$UID + 4566 + + + CF$UID + 4567 + + + CF$UID + 4568 + + + CF$UID + 4569 + + + CF$UID + 4570 + + + CF$UID + 4571 + + + CF$UID + 4572 + + + CF$UID + 4573 + + + CF$UID + 4574 + + + CF$UID + 4575 + + + CF$UID + 4576 + + + CF$UID + 4577 + + + CF$UID + 4578 + + + CF$UID + 4579 + + + CF$UID + 4580 + + + CF$UID + 4581 + + + CF$UID + 4582 + + + CF$UID + 4583 + + + CF$UID + 4584 + + + CF$UID + 4585 + + + CF$UID + 4586 + + + CF$UID + 4587 + + + CF$UID + 4588 + + + CF$UID + 4589 + + + CF$UID + 4590 + + + CF$UID + 4591 + + + CF$UID + 4592 + + + CF$UID + 4593 + + + CF$UID + 4594 + + + CF$UID + 4595 + + + CF$UID + 4596 + + + CF$UID + 4597 + + + CF$UID + 4598 + + + CF$UID + 4599 + + + CF$UID + 4600 + + + CF$UID + 4601 + + + CF$UID + 4602 + + + CF$UID + 4603 + + + CF$UID + 4604 + + + CF$UID + 4605 + + + CF$UID + 4606 + + + CF$UID + 4607 + + + CF$UID + 4608 + + + CF$UID + 4609 + + + CF$UID + 4610 + + + CF$UID + 4611 + + + CF$UID + 4612 + + + CF$UID + 4613 + + + CF$UID + 4614 + + + CF$UID + 4615 + + + CF$UID + 4616 + + + CF$UID + 4617 + + + CF$UID + 4618 + + + CF$UID + 4619 + + + CF$UID + 4620 + + + CF$UID + 4621 + + + CF$UID + 4622 + + + CF$UID + 4623 + + + CF$UID + 4624 + + + CF$UID + 4625 + + + CF$UID + 4626 + + + CF$UID + 4627 + + + CF$UID + 4628 + + + CF$UID + 4629 + + + CF$UID + 4630 + + + CF$UID + 4631 + + + CF$UID + 4632 + + + CF$UID + 4633 + + + CF$UID + 4634 + + + CF$UID + 4635 + + + CF$UID + 4636 + + + CF$UID + 4637 + + + CF$UID + 4638 + + + CF$UID + 4641 + + + CF$UID + 4642 + + + CF$UID + 4645 + + + CF$UID + 4646 + + + CF$UID + 4647 + + + CF$UID + 4648 + + + CF$UID + 4649 + + + CF$UID + 4650 + + + CF$UID + 4651 + + + CF$UID + 4652 + + + CF$UID + 4653 + + + CF$UID + 4656 + + + CF$UID + 4657 + + + CF$UID + 4658 + + + CF$UID + 4659 + + + CF$UID + 4664 + + + CF$UID + 4665 + + + CF$UID + 4666 + + + CF$UID + 4667 + + + CF$UID + 4668 + + + CF$UID + 4669 + + + CF$UID + 4670 + + + CF$UID + 4671 + + + CF$UID + 4672 + + + CF$UID + 4675 + + + CF$UID + 4676 + + + CF$UID + 4677 + + + CF$UID + 4678 + + + CF$UID + 4679 + + + CF$UID + 4680 + + + CF$UID + 4681 + + + CF$UID + 4684 + + + CF$UID + 4685 + + + CF$UID + 4686 + + + CF$UID + 4687 + + + CF$UID + 4688 + + + CF$UID + 4691 + + + CF$UID + 4692 + + + CF$UID + 4695 + + + CF$UID + 4696 + + + CF$UID + 4697 + + + CF$UID + 4698 + + + CF$UID + 4699 + + + CF$UID + 4700 + + + CF$UID + 4701 + + + CF$UID + 4702 + + + CF$UID + 4703 + + + CF$UID + 4704 + + + CF$UID + 4705 + + + CF$UID + 4706 + + + CF$UID + 4707 + + + CF$UID + 4710 + + + CF$UID + 4711 + + + CF$UID + 4712 + + + CF$UID + 4713 + + + CF$UID + 4714 + + + CF$UID + 4715 + + + CF$UID + 4716 + + + CF$UID + 4719 + + + CF$UID + 4720 + + + CF$UID + 4723 + + + CF$UID + 4724 + + + CF$UID + 4725 + + + CF$UID + 4726 + + + CF$UID + 4727 + + + CF$UID + 4730 + + + CF$UID + 4731 + + + CF$UID + 4732 + + + CF$UID + 4733 + + + CF$UID + 4734 + + + CF$UID + 4735 + + + CF$UID + 4736 + + + CF$UID + 4737 + + + CF$UID + 4738 + + + CF$UID + 4739 + + + CF$UID + 4742 + + + CF$UID + 4743 + + + CF$UID + 4744 + + + CF$UID + 4745 + + + CF$UID + 4748 + + + CF$UID + 4749 + + + CF$UID + 4750 + + + CF$UID + 4751 + + + CF$UID + 4752 + + + CF$UID + 4753 + + + CF$UID + 4754 + + + CF$UID + 4755 + + + CF$UID + 4756 + + + CF$UID + 4757 + + + CF$UID + 4758 + + + CF$UID + 4759 + + + CF$UID + 4760 + + + CF$UID + 4761 + + + CF$UID + 4762 + + + CF$UID + 4763 + + + CF$UID + 4764 + + + CF$UID + 4767 + + + CF$UID + 4768 + + + CF$UID + 4771 + + + CF$UID + 4772 + + + CF$UID + 4773 + + + CF$UID + 4774 + + + CF$UID + 4775 + + + CF$UID + 4776 + + + CF$UID + 4777 + + + CF$UID + 4778 + + + CF$UID + 4779 + + + CF$UID + 4780 + + + CF$UID + 4783 + + + CF$UID + 4784 + + + CF$UID + 4787 + + + CF$UID + 4788 + + + CF$UID + 4791 + + + CF$UID + 4792 + + + CF$UID + 4793 + + + CF$UID + 4794 + + + CF$UID + 4795 + + + CF$UID + 4796 + + + CF$UID + 4797 + + + CF$UID + 4798 + + + CF$UID + 4799 + + + CF$UID + 4800 + + + CF$UID + 4801 + + + CF$UID + 4802 + + + CF$UID + 4805 + + + CF$UID + 4806 + + + CF$UID + 4807 + + + CF$UID + 4808 + + + CF$UID + 4809 + + + CF$UID + 4816 + + + CF$UID + 4817 + + + CF$UID + 4818 + + + CF$UID + 4819 + + + CF$UID + 4820 + + + CF$UID + 4821 + + + CF$UID + 4822 + + + CF$UID + 4823 + + + CF$UID + 4824 + + + CF$UID + 4825 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 65 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 65 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 65 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 4639 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4640 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 108 + len + 33 + x + 4 + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 4643 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4644 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 110 + len + 15 + x + 4 + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 4654 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4655 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 119 + len + 24 + x + 18 + + + $class + + CF$UID + 24 + + c + 9 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 9 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 4660 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4661 + + + CF$UID + 4662 + + + CF$UID + 4663 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 123 + len + 59 + x + 18 + + + $class + + CF$UID + 136 + + c + 60 + l + 123 + len + 33 + x + 10 + + + $class + + CF$UID + 136 + + c + 93 + l + 123 + len + 2 + x + 18 + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 18 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 4673 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4674 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 132 + len + 15 + x + 14 + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 4682 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4683 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 139 + len + 34 + x + 14 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 4689 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4690 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 144 + len + 29 + x + 14 + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 4693 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4694 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 146 + len + 69 + x + 7 + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 4708 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4709 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 159 + len + 1 + x + 18 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 4717 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4718 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 166 + len + 60 + x + 10 + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 4721 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4722 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 168 + len + 52 + x + 3 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 4728 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4729 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 173 + len + 28 + x + 10 + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 4740 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4741 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 183 + len + 76 + x + 15 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 4746 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4747 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 187 + len + 14 + x + 14 + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 25 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 25 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 25 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 25 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 34 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 34 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 34 + s + + CF$UID + 4765 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4766 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 204 + len + 25 + x + 34 + + + $class + + CF$UID + 24 + + c + 28 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 4769 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4770 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 206 + len + 11 + x + 34 + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 34 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 28 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 28 + s + + CF$UID + 4781 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4782 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 216 + len + 42 + x + 28 + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 4785 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4786 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 218 + len + 33 + x + 5 + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 4789 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4790 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 220 + len + 15 + x + 5 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 28 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 23 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 23 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 23 + s + + CF$UID + 4803 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4804 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 232 + len + 1 + x + 28 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 4810 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4811 + + + CF$UID + 4812 + + + CF$UID + 4813 + + + CF$UID + 4814 + + + CF$UID + 4815 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 237 + len + 56 + x + 5 + + + $class + + CF$UID + 136 + + c + 57 + l + 237 + len + 26 + x + 4 + + + $class + + CF$UID + 136 + + c + 83 + l + 237 + len + 4 + x + 5 + + + $class + + CF$UID + 136 + + c + 87 + l + 237 + len + 30 + x + 4 + + + $class + + CF$UID + 136 + + c + 117 + l + 237 + len + 2 + x + 5 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 4829 + + functions + + CF$UID + 4830 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 4834 + + name + + CF$UID + 4827 + + uniqueIdentifier + + CF$UID + 4828 + + + NSDictionary+HeaderSize.m + CA9FA203-10F4-4B01-AE15-30F126A54336 + /Users/dflems/Code/SPTDataLoader/SPTDataLoader/NSDictionary+HeaderSize.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4831 + + + + + $class + + CF$UID + 20 + + executionCount + 4 + lineCoverage + + CF$UID + 13 + + lineNumber + 28 + name + + CF$UID + 4832 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4833 + + + -[NSDictionary(HeaderSize) spt_byteSizeOfHeaders] + D69F6370-95FC-4FC5-BF46-297608DF3BCE + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4835 + + + CF$UID + 4836 + + + CF$UID + 4837 + + + CF$UID + 4838 + + + CF$UID + 4839 + + + CF$UID + 4840 + + + CF$UID + 4841 + + + CF$UID + 4842 + + + CF$UID + 4843 + + + CF$UID + 4844 + + + CF$UID + 4845 + + + CF$UID + 4846 + + + CF$UID + 4847 + + + CF$UID + 4848 + + + CF$UID + 4849 + + + CF$UID + 4850 + + + CF$UID + 4851 + + + CF$UID + 4852 + + + CF$UID + 4853 + + + CF$UID + 4854 + + + CF$UID + 4855 + + + CF$UID + 4856 + + + CF$UID + 4857 + + + CF$UID + 4858 + + + CF$UID + 4859 + + + CF$UID + 4860 + + + CF$UID + 4861 + + + CF$UID + 4862 + + + CF$UID + 4863 + + + CF$UID + 4864 + + + CF$UID + 4865 + + + CF$UID + 4868 + + + CF$UID + 4869 + + + CF$UID + 4870 + + + CF$UID + 4871 + + + CF$UID + 4872 + + + CF$UID + 4873 + + + CF$UID + 4876 + + + CF$UID + 4877 + + + CF$UID + 4878 + + + CF$UID + 4879 + + + CF$UID + 4880 + + + CF$UID + 4881 + + + CF$UID + 4882 + + + CF$UID + 4883 + + + CF$UID + 4884 + + + CF$UID + 4885 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 4866 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4867 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 31 + len + 51 + x + 4 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 4874 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4875 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 37 + len + 54 + x + 3 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 4889 + + functions + + CF$UID + 4890 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 4909 + + name + + CF$UID + 4887 + + uniqueIdentifier + + CF$UID + 4888 + + + SPTDataLoaderResponse.m + D3B83A4E-0C79-449C-9E24-2E3D3544C13B + /Users/dflems/Code/SPTDataLoader/SPTDataLoader/SPTDataLoaderResponse.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4891 + + + CF$UID + 4894 + + + CF$UID + 4897 + + + CF$UID + 4900 + + + CF$UID + 4903 + + + CF$UID + 4906 + + + + + $class + + CF$UID + 20 + + executionCount + 58 + lineCoverage + + CF$UID + 13 + + lineNumber + 46 + name + + CF$UID + 4892 + + symbolKindIdentifier + + CF$UID + 4010 + + uniqueIdentifier + + CF$UID + 4893 + + + +[SPTDataLoaderResponse dataLoaderResponseWithRequest:response:] + CEE1D5C9-7210-47C2-9681-C8B2C126ACE9 + + $class + + CF$UID + 20 + + executionCount + 58 + lineCoverage + + CF$UID + 13 + + lineNumber + 51 + name + + CF$UID + 4895 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4896 + + + -[SPTDataLoaderResponse initWithRequest:response:] + 438A71C5-1841-4679-AF15-BE5EFD9A2FE0 + + $class + + CF$UID + 20 + + executionCount + 14 + lineCoverage + + CF$UID + 13 + + lineNumber + 76 + name + + CF$UID + 4898 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4899 + + + -[SPTDataLoaderResponse shouldRetry] + 5B231AD2-2BC4-4BED-B70B-83CEA3D49688 + + $class + + CF$UID + 20 + + executionCount + 58 + lineCoverage + + CF$UID + 13 + + lineNumber + 168 + name + + CF$UID + 4901 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4902 + + + -[SPTDataLoaderResponse retryAfterForHeaders:] + A224BDD3-064A-43BA-9E0B-B562C049C8B7 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 171 + name + + CF$UID + 4904 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 4905 + + + __46-[SPTDataLoaderResponse retryAfterForHeaders:]_block_invoke + BAF7F2D6-AC00-4771-AD60-3D25EF6BBE73 + + $class + + CF$UID + 20 + + executionCount + 4 + lineCoverage + + CF$UID + 13 + + lineNumber + 186 + name + + CF$UID + 4907 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 4908 + + + -[SPTDataLoaderResponse description] + E7700B2B-0220-40AB-9D12-B922710F9A57 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4910 + + + CF$UID + 4911 + + + CF$UID + 4912 + + + CF$UID + 4913 + + + CF$UID + 4914 + + + CF$UID + 4915 + + + CF$UID + 4916 + + + CF$UID + 4917 + + + CF$UID + 4918 + + + CF$UID + 4919 + + + CF$UID + 4920 + + + CF$UID + 4921 + + + CF$UID + 4922 + + + CF$UID + 4923 + + + CF$UID + 4924 + + + CF$UID + 4925 + + + CF$UID + 4926 + + + CF$UID + 4927 + + + CF$UID + 4928 + + + CF$UID + 4929 + + + CF$UID + 4930 + + + CF$UID + 4931 + + + CF$UID + 4932 + + + CF$UID + 4933 + + + CF$UID + 4934 + + + CF$UID + 4935 + + + CF$UID + 4936 + + + CF$UID + 4937 + + + CF$UID + 4938 + + + CF$UID + 4939 + + + CF$UID + 4940 + + + CF$UID + 4941 + + + CF$UID + 4942 + + + CF$UID + 4943 + + + CF$UID + 4944 + + + CF$UID + 4945 + + + CF$UID + 4946 + + + CF$UID + 4947 + + + CF$UID + 4948 + + + CF$UID + 4949 + + + CF$UID + 4950 + + + CF$UID + 4951 + + + CF$UID + 4952 + + + CF$UID + 4953 + + + CF$UID + 4954 + + + CF$UID + 4955 + + + CF$UID + 4956 + + + CF$UID + 4957 + + + CF$UID + 4958 + + + CF$UID + 4959 + + + CF$UID + 4960 + + + CF$UID + 4961 + + + CF$UID + 4962 + + + CF$UID + 4963 + + + CF$UID + 4964 + + + CF$UID + 4965 + + + CF$UID + 4966 + + + CF$UID + 4969 + + + CF$UID + 4970 + + + CF$UID + 4971 + + + CF$UID + 4975 + + + CF$UID + 4976 + + + CF$UID + 4977 + + + CF$UID + 4978 + + + CF$UID + 4979 + + + CF$UID + 4980 + + + CF$UID + 4981 + + + CF$UID + 4982 + + + CF$UID + 4983 + + + CF$UID + 4984 + + + CF$UID + 4985 + + + CF$UID + 4986 + + + CF$UID + 4987 + + + CF$UID + 4988 + + + CF$UID + 4989 + + + CF$UID + 4990 + + + CF$UID + 4991 + + + CF$UID + 4994 + + + CF$UID + 4995 + + + CF$UID + 4996 + + + CF$UID + 4997 + + + CF$UID + 4998 + + + CF$UID + 4999 + + + CF$UID + 5000 + + + CF$UID + 5001 + + + CF$UID + 5002 + + + CF$UID + 5003 + + + CF$UID + 5004 + + + CF$UID + 5005 + + + CF$UID + 5006 + + + CF$UID + 5007 + + + CF$UID + 5008 + + + CF$UID + 5009 + + + CF$UID + 5010 + + + CF$UID + 5011 + + + CF$UID + 5012 + + + CF$UID + 5013 + + + CF$UID + 5014 + + + CF$UID + 5015 + + + CF$UID + 5016 + + + CF$UID + 5017 + + + CF$UID + 5018 + + + CF$UID + 5019 + + + CF$UID + 5020 + + + CF$UID + 5021 + + + CF$UID + 5022 + + + CF$UID + 5023 + + + CF$UID + 5024 + + + CF$UID + 5025 + + + CF$UID + 5026 + + + CF$UID + 5027 + + + CF$UID + 5028 + + + CF$UID + 5029 + + + CF$UID + 5030 + + + CF$UID + 5031 + + + CF$UID + 5032 + + + CF$UID + 5033 + + + CF$UID + 5034 + + + CF$UID + 5035 + + + CF$UID + 5036 + + + CF$UID + 5037 + + + CF$UID + 5038 + + + CF$UID + 5039 + + + CF$UID + 5040 + + + CF$UID + 5041 + + + CF$UID + 5042 + + + CF$UID + 5045 + + + CF$UID + 5046 + + + CF$UID + 5047 + + + CF$UID + 5048 + + + CF$UID + 5049 + + + CF$UID + 5050 + + + CF$UID + 5051 + + + CF$UID + 5052 + + + CF$UID + 5053 + + + CF$UID + 5054 + + + CF$UID + 5055 + + + CF$UID + 5056 + + + CF$UID + 5057 + + + CF$UID + 5058 + + + CF$UID + 5059 + + + CF$UID + 5060 + + + CF$UID + 5061 + + + CF$UID + 5062 + + + CF$UID + 5063 + + + CF$UID + 5064 + + + CF$UID + 5065 + + + CF$UID + 5066 + + + CF$UID + 5067 + + + CF$UID + 5068 + + + CF$UID + 5069 + + + CF$UID + 5070 + + + CF$UID + 5071 + + + CF$UID + 5072 + + + CF$UID + 5073 + + + CF$UID + 5074 + + + CF$UID + 5075 + + + CF$UID + 5076 + + + CF$UID + 5077 + + + CF$UID + 5078 + + + CF$UID + 5079 + + + CF$UID + 5080 + + + CF$UID + 5081 + + + CF$UID + 5082 + + + CF$UID + 5085 + + + CF$UID + 5088 + + + CF$UID + 5089 + + + CF$UID + 5090 + + + CF$UID + 5091 + + + CF$UID + 5092 + + + CF$UID + 5093 + + + CF$UID + 5094 + + + CF$UID + 5095 + + + CF$UID + 5096 + + + CF$UID + 5097 + + + CF$UID + 5098 + + + CF$UID + 5099 + + + CF$UID + 5102 + + + CF$UID + 5103 + + + CF$UID + 5104 + + + CF$UID + 5105 + + + CF$UID + 5106 + + + CF$UID + 5107 + + + CF$UID + 5108 + + + CF$UID + 5109 + + + CF$UID + 5110 + + + CF$UID + 5111 + + + CF$UID + 5112 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 4967 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4968 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 57 + len + 64 + x + 58 + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 4972 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4973 + + + CF$UID + 4974 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 60 + len + 96 + x + 17 + + + $class + + CF$UID + 136 + + c + 97 + l + 60 + len + 2 + x + 22 + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 4992 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 4993 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 77 + len + 78 + x + 14 + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 5043 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5044 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 126 + len + 62 + x + 11 + + + $class + + CF$UID + 24 + + c + 9 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 9 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 9 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 5083 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5084 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 164 + len + 13 + x + 2 + + + $class + + CF$UID + 24 + + c + 11 + s + + CF$UID + 5086 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5087 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 165 + len + 1 + x + 14 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 59 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 59 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 59 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 59 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 5100 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5101 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 177 + len + 34 + x + 58 + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 55 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 55 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 58 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 5117 + + functions + + CF$UID + 5118 + + lineCoverage + + CF$UID + 5116 + + lines + + CF$UID + 5188 + + name + + CF$UID + 5114 + + uniqueIdentifier + + CF$UID + 5115 + + + SPTDataLoaderService.m + 4A825782-A306-4AF9-9AE4-196C28C3DD63 + 0.96539792387543255 + /Users/dflems/Code/SPTDataLoader/SPTDataLoader/SPTDataLoaderService.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5119 + + + CF$UID + 5122 + + + CF$UID + 5125 + + + CF$UID + 5128 + + + CF$UID + 5131 + + + CF$UID + 5134 + + + CF$UID + 5137 + + + CF$UID + 5141 + + + CF$UID + 5144 + + + CF$UID + 5147 + + + CF$UID + 5150 + + + CF$UID + 5153 + + + CF$UID + 5156 + + + CF$UID + 5159 + + + CF$UID + 5162 + + + CF$UID + 5165 + + + CF$UID + 5168 + + + CF$UID + 5171 + + + CF$UID + 5174 + + + CF$UID + 5178 + + + CF$UID + 5182 + + + CF$UID + 5185 + + + + + $class + + CF$UID + 20 + + executionCount + 33 + lineCoverage + + CF$UID + 13 + + lineNumber + 60 + name + + CF$UID + 5120 + + symbolKindIdentifier + + CF$UID + 4010 + + uniqueIdentifier + + CF$UID + 5121 + + + +[SPTDataLoaderService dataLoaderServiceWithUserAgent:rateLimiter:resolver:customURLProtocolClasses:] + 16DCDD0B-B75B-4174-A8CA-56AB8B7C7FA0 + + $class + + CF$UID + 20 + + executionCount + 33 + lineCoverage + + CF$UID + 13 + + lineNumber + 68 + name + + CF$UID + 5123 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5124 + + + -[SPTDataLoaderService initWithUserAgent:rateLimiter:resolver:customURLProtocolClasses:] + 52BD3A1D-E2FD-4EC3-BBCA-028F454FFA93 + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 100 + name + + CF$UID + 5126 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5127 + + + -[SPTDataLoaderService createDataLoaderFactoryWithAuthorisers:] + 1DB4A19F-5F57-4240-B805-CD3A3E49FC10 + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 105 + name + + CF$UID + 5129 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5130 + + + -[SPTDataLoaderService addConsumptionObserver:on:] + 03BA83BE-1CCB-4AD7-9D35-80D5003E9D5B + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 114 + name + + CF$UID + 5132 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5133 + + + -[SPTDataLoaderService removeConsumptionObserver:] + 472EF077-4FBB-41C3-ADE1-75B78C00A23D + + $class + + CF$UID + 20 + + executionCount + 30 + lineCoverage + + CF$UID + 13 + + lineNumber + 123 + name + + CF$UID + 5135 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5136 + + + -[SPTDataLoaderService handlerForTask:] + D3045342-95AB-41F4-81AB-8843232BB5E5 + + $class + + CF$UID + 20 + + executionCount + 22 + lineCoverage + + CF$UID + 5140 + + lineNumber + 138 + name + + CF$UID + 5138 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5139 + + + -[SPTDataLoaderService performRequest:requestResponseHandler:] + 9FF1EB34-A135-46C8-B4D8-6586FB61747D + 0.94285714285714284 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 175 + name + + CF$UID + 5142 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5143 + + + -[SPTDataLoaderService cancelAllLoads] + 7A7A92C4-E518-4A26-A1C8-290BE6D6CCDE + + $class + + CF$UID + 20 + + executionCount + 21 + lineCoverage + + CF$UID + 13 + + lineNumber + 189 + name + + CF$UID + 5145 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5146 + + + -[SPTDataLoaderService requestResponseHandler:performRequest:] + 07819FDC-9E71-429F-9D57-C0627F6B569A + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 204 + name + + CF$UID + 5148 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5149 + + + -[SPTDataLoaderService requestResponseHandler:cancelRequest:] + 40D80BCE-7A08-41B0-94B8-05896566DD87 + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 219 + name + + CF$UID + 5151 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5152 + + + -[SPTDataLoaderService requestResponseHandler:authorisedRequest:] + C118EC2A-835B-411A-8401-5041DF28B2EF + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 226 + name + + CF$UID + 5154 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5155 + + + -[SPTDataLoaderService requestResponseHandler:failedToAuthoriseRequest:error:] + 634B47EE-D975-43B2-82EC-665BC3D65232 + + $class + + CF$UID + 20 + + executionCount + 3 + lineCoverage + + CF$UID + 13 + + lineNumber + 238 + name + + CF$UID + 5157 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5158 + + + -[SPTDataLoaderService URLSession:dataTask:didReceiveResponse:completionHandler:] + 88F57FC8-DEE5-420B-BAB2-4621A9FC7ED1 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 248 + name + + CF$UID + 5160 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5161 + + + -[SPTDataLoaderService URLSession:dataTask:didBecomeDownloadTask:] + 3805A9D1-1792-4866-B532-B143681EB5DC + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 255 + name + + CF$UID + 5163 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5164 + + + -[SPTDataLoaderService URLSession:dataTask:didReceiveData:] + CDAD373A-758F-4242-9A68-B92B5950BA10 + + $class + + CF$UID + 20 + + executionCount + 3 + lineCoverage + + CF$UID + 13 + + lineNumber + 264 + name + + CF$UID + 5166 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5167 + + + -[SPTDataLoaderService URLSession:dataTask:willCacheResponse:completionHandler:] + DBE5C41C-D1A7-407B-A752-1D2BEE9E3CA1 + + $class + + CF$UID + 20 + + executionCount + 7 + lineCoverage + + CF$UID + 13 + + lineNumber + 276 + name + + CF$UID + 5169 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5170 + + + -[SPTDataLoaderService URLSession:task:didReceiveChallenge:completionHandler:] + 3EC34CAF-FD52-4E2B-8A18-1B6B567E8403 + + $class + + CF$UID + 20 + + executionCount + 8 + lineCoverage + + CF$UID + 13 + + lineNumber + 309 + name + + CF$UID + 5172 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5173 + + + -[SPTDataLoaderService URLSession:task:didCompleteWithError:] + 1CC15143-6B10-4C8B-99A7-04B75CDBD03C + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 5177 + + lineNumber + 326 + name + + CF$UID + 5175 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 5176 + + + __61-[SPTDataLoaderService URLSession:task:didCompleteWithError:]_block_invoke + 5C8EA611-3A6E-4AF7-875E-86E5EB6770F4 + 0.88235294117647056 + + $class + + CF$UID + 20 + + executionCount + 15 + lineCoverage + + CF$UID + 5181 + + lineNumber + 359 + name + + CF$UID + 5179 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5180 + + + -[SPTDataLoaderService URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:] + 785D0C85-400A-4AF6-9AD7-12314DADE387 + 0.91666666666666663 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 399 + name + + CF$UID + 5183 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5184 + + + -[SPTDataLoaderService URLSession:task:needNewBodyStream:] + 3364814F-DDA9-40C9-8C46-E943B2E1454C + + $class + + CF$UID + 20 + + executionCount + 0 + lineCoverage + + CF$UID + 80 + + lineNumber + 407 + name + + CF$UID + 5186 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5187 + + + -[SPTDataLoaderService dealloc] + AD4B2F50-FE59-4B8C-AB3A-F7D17BE3B215 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5189 + + + CF$UID + 5190 + + + CF$UID + 5191 + + + CF$UID + 5192 + + + CF$UID + 5193 + + + CF$UID + 5194 + + + CF$UID + 5195 + + + CF$UID + 5196 + + + CF$UID + 5197 + + + CF$UID + 5198 + + + CF$UID + 5199 + + + CF$UID + 5200 + + + CF$UID + 5201 + + + CF$UID + 5202 + + + CF$UID + 5203 + + + CF$UID + 5204 + + + CF$UID + 5205 + + + CF$UID + 5206 + + + CF$UID + 5207 + + + CF$UID + 5208 + + + CF$UID + 5209 + + + CF$UID + 5210 + + + CF$UID + 5211 + + + CF$UID + 5212 + + + CF$UID + 5213 + + + CF$UID + 5214 + + + CF$UID + 5215 + + + CF$UID + 5216 + + + CF$UID + 5217 + + + CF$UID + 5218 + + + CF$UID + 5219 + + + CF$UID + 5220 + + + CF$UID + 5221 + + + CF$UID + 5222 + + + CF$UID + 5223 + + + CF$UID + 5224 + + + CF$UID + 5225 + + + CF$UID + 5226 + + + CF$UID + 5227 + + + CF$UID + 5228 + + + CF$UID + 5229 + + + CF$UID + 5230 + + + CF$UID + 5231 + + + CF$UID + 5232 + + + CF$UID + 5233 + + + CF$UID + 5234 + + + CF$UID + 5235 + + + CF$UID + 5236 + + + CF$UID + 5237 + + + CF$UID + 5238 + + + CF$UID + 5239 + + + CF$UID + 5240 + + + CF$UID + 5241 + + + CF$UID + 5242 + + + CF$UID + 5243 + + + CF$UID + 5244 + + + CF$UID + 5245 + + + CF$UID + 5246 + + + CF$UID + 5247 + + + CF$UID + 5248 + + + CF$UID + 5249 + + + CF$UID + 5250 + + + CF$UID + 5251 + + + CF$UID + 5252 + + + CF$UID + 5253 + + + CF$UID + 5254 + + + CF$UID + 5255 + + + CF$UID + 5256 + + + CF$UID + 5257 + + + CF$UID + 5258 + + + CF$UID + 5259 + + + CF$UID + 5260 + + + CF$UID + 5261 + + + CF$UID + 5262 + + + CF$UID + 5263 + + + CF$UID + 5264 + + + CF$UID + 5265 + + + CF$UID + 5266 + + + CF$UID + 5267 + + + CF$UID + 5268 + + + CF$UID + 5269 + + + CF$UID + 5270 + + + CF$UID + 5271 + + + CF$UID + 5272 + + + CF$UID + 5273 + + + CF$UID + 5274 + + + CF$UID + 5275 + + + CF$UID + 5276 + + + CF$UID + 5277 + + + CF$UID + 5278 + + + CF$UID + 5279 + + + CF$UID + 5280 + + + CF$UID + 5281 + + + CF$UID + 5282 + + + CF$UID + 5283 + + + CF$UID + 5284 + + + CF$UID + 5285 + + + CF$UID + 5286 + + + CF$UID + 5287 + + + CF$UID + 5288 + + + CF$UID + 5289 + + + CF$UID + 5290 + + + CF$UID + 5291 + + + CF$UID + 5292 + + + CF$UID + 5293 + + + CF$UID + 5294 + + + CF$UID + 5295 + + + CF$UID + 5296 + + + CF$UID + 5297 + + + CF$UID + 5298 + + + CF$UID + 5299 + + + CF$UID + 5300 + + + CF$UID + 5301 + + + CF$UID + 5302 + + + CF$UID + 5303 + + + CF$UID + 5304 + + + CF$UID + 5305 + + + CF$UID + 5306 + + + CF$UID + 5307 + + + CF$UID + 5308 + + + CF$UID + 5309 + + + CF$UID + 5310 + + + CF$UID + 5311 + + + CF$UID + 5312 + + + CF$UID + 5313 + + + CF$UID + 5314 + + + CF$UID + 5315 + + + CF$UID + 5316 + + + CF$UID + 5317 + + + CF$UID + 5320 + + + CF$UID + 5321 + + + CF$UID + 5322 + + + CF$UID + 5323 + + + CF$UID + 5326 + + + CF$UID + 5327 + + + CF$UID + 5328 + + + CF$UID + 5329 + + + CF$UID + 5330 + + + CF$UID + 5331 + + + CF$UID + 5334 + + + CF$UID + 5335 + + + CF$UID + 5336 + + + CF$UID + 5337 + + + CF$UID + 5340 + + + CF$UID + 5341 + + + CF$UID + 5342 + + + CF$UID + 5343 + + + CF$UID + 5344 + + + CF$UID + 5345 + + + CF$UID + 5350 + + + CF$UID + 5351 + + + CF$UID + 5352 + + + CF$UID + 5353 + + + CF$UID + 5354 + + + CF$UID + 5355 + + + CF$UID + 5358 + + + CF$UID + 5359 + + + CF$UID + 5360 + + + CF$UID + 5361 + + + CF$UID + 5362 + + + CF$UID + 5363 + + + CF$UID + 5364 + + + CF$UID + 5365 + + + CF$UID + 5366 + + + CF$UID + 5367 + + + CF$UID + 5368 + + + CF$UID + 5369 + + + CF$UID + 5370 + + + CF$UID + 5371 + + + CF$UID + 5372 + + + CF$UID + 5373 + + + CF$UID + 5374 + + + CF$UID + 5377 + + + CF$UID + 5378 + + + CF$UID + 5379 + + + CF$UID + 5380 + + + CF$UID + 5381 + + + CF$UID + 5382 + + + CF$UID + 5383 + + + CF$UID + 5384 + + + CF$UID + 5385 + + + CF$UID + 5386 + + + CF$UID + 5387 + + + CF$UID + 5388 + + + CF$UID + 5389 + + + CF$UID + 5390 + + + CF$UID + 5391 + + + CF$UID + 5392 + + + CF$UID + 5393 + + + CF$UID + 5394 + + + CF$UID + 5397 + + + CF$UID + 5400 + + + CF$UID + 5401 + + + CF$UID + 5402 + + + CF$UID + 5403 + + + CF$UID + 5404 + + + CF$UID + 5405 + + + CF$UID + 5406 + + + CF$UID + 5407 + + + CF$UID + 5408 + + + CF$UID + 5411 + + + CF$UID + 5412 + + + CF$UID + 5413 + + + CF$UID + 5414 + + + CF$UID + 5415 + + + CF$UID + 5416 + + + CF$UID + 5417 + + + CF$UID + 5418 + + + CF$UID + 5419 + + + CF$UID + 5420 + + + CF$UID + 5421 + + + CF$UID + 5422 + + + CF$UID + 5423 + + + CF$UID + 5424 + + + CF$UID + 5425 + + + CF$UID + 5426 + + + CF$UID + 5427 + + + CF$UID + 5428 + + + CF$UID + 5429 + + + CF$UID + 5430 + + + CF$UID + 5431 + + + CF$UID + 5432 + + + CF$UID + 5433 + + + CF$UID + 5434 + + + CF$UID + 5435 + + + CF$UID + 5436 + + + CF$UID + 5437 + + + CF$UID + 5438 + + + CF$UID + 5439 + + + CF$UID + 5440 + + + CF$UID + 5441 + + + CF$UID + 5442 + + + CF$UID + 5443 + + + CF$UID + 5444 + + + CF$UID + 5445 + + + CF$UID + 5446 + + + CF$UID + 5447 + + + CF$UID + 5448 + + + CF$UID + 5449 + + + CF$UID + 5450 + + + CF$UID + 5451 + + + CF$UID + 5452 + + + CF$UID + 5453 + + + CF$UID + 5454 + + + CF$UID + 5455 + + + CF$UID + 5456 + + + CF$UID + 5457 + + + CF$UID + 5458 + + + CF$UID + 5459 + + + CF$UID + 5460 + + + CF$UID + 5461 + + + CF$UID + 5462 + + + CF$UID + 5463 + + + CF$UID + 5464 + + + CF$UID + 5465 + + + CF$UID + 5466 + + + CF$UID + 5467 + + + CF$UID + 5468 + + + CF$UID + 5469 + + + CF$UID + 5470 + + + CF$UID + 5471 + + + CF$UID + 5472 + + + CF$UID + 5473 + + + CF$UID + 5474 + + + CF$UID + 5475 + + + CF$UID + 5478 + + + CF$UID + 5479 + + + CF$UID + 5480 + + + CF$UID + 5481 + + + CF$UID + 5484 + + + CF$UID + 5487 + + + CF$UID + 5488 + + + CF$UID + 5489 + + + CF$UID + 5490 + + + CF$UID + 5491 + + + CF$UID + 5492 + + + CF$UID + 5493 + + + CF$UID + 5496 + + + CF$UID + 5497 + + + CF$UID + 5498 + + + CF$UID + 5499 + + + CF$UID + 5500 + + + CF$UID + 5501 + + + CF$UID + 5502 + + + CF$UID + 5505 + + + CF$UID + 5506 + + + CF$UID + 5507 + + + CF$UID + 5508 + + + CF$UID + 5514 + + + CF$UID + 5517 + + + CF$UID + 5518 + + + CF$UID + 5519 + + + CF$UID + 5520 + + + CF$UID + 5523 + + + CF$UID + 5524 + + + CF$UID + 5525 + + + CF$UID + 5528 + + + CF$UID + 5529 + + + CF$UID + 5530 + + + CF$UID + 5531 + + + CF$UID + 5532 + + + CF$UID + 5533 + + + CF$UID + 5536 + + + CF$UID + 5537 + + + CF$UID + 5538 + + + CF$UID + 5539 + + + CF$UID + 5540 + + + CF$UID + 5541 + + + CF$UID + 5542 + + + CF$UID + 5543 + + + CF$UID + 5544 + + + CF$UID + 5547 + + + CF$UID + 5548 + + + CF$UID + 5549 + + + CF$UID + 5550 + + + CF$UID + 5551 + + + CF$UID + 5556 + + + CF$UID + 5557 + + + CF$UID + 5558 + + + CF$UID + 5559 + + + CF$UID + 5560 + + + CF$UID + 5561 + + + CF$UID + 5562 + + + CF$UID + 5563 + + + CF$UID + 5564 + + + CF$UID + 5565 + + + CF$UID + 5566 + + + CF$UID + 5569 + + + CF$UID + 5570 + + + CF$UID + 5571 + + + CF$UID + 5572 + + + CF$UID + 5573 + + + CF$UID + 5574 + + + CF$UID + 5575 + + + CF$UID + 5578 + + + CF$UID + 5579 + + + CF$UID + 5580 + + + CF$UID + 5581 + + + CF$UID + 5582 + + + CF$UID + 5583 + + + CF$UID + 5584 + + + CF$UID + 5585 + + + CF$UID + 5588 + + + CF$UID + 5589 + + + CF$UID + 5590 + + + CF$UID + 5593 + + + CF$UID + 5594 + + + CF$UID + 5597 + + + CF$UID + 5598 + + + CF$UID + 5599 + + + CF$UID + 5600 + + + CF$UID + 5601 + + + CF$UID + 5604 + + + CF$UID + 5605 + + + CF$UID + 5606 + + + CF$UID + 5607 + + + CF$UID + 5608 + + + CF$UID + 5609 + + + CF$UID + 5610 + + + CF$UID + 5611 + + + CF$UID + 5612 + + + CF$UID + 5615 + + + CF$UID + 5616 + + + CF$UID + 5617 + + + CF$UID + 5618 + + + CF$UID + 5619 + + + CF$UID + 5620 + + + CF$UID + 5621 + + + CF$UID + 5624 + + + CF$UID + 5625 + + + CF$UID + 5626 + + + CF$UID + 5627 + + + CF$UID + 5628 + + + CF$UID + 5629 + + + CF$UID + 5630 + + + CF$UID + 5631 + + + CF$UID + 5636 + + + CF$UID + 5637 + + + CF$UID + 5638 + + + CF$UID + 5639 + + + CF$UID + 5640 + + + CF$UID + 5641 + + + CF$UID + 5642 + + + CF$UID + 5643 + + + CF$UID + 5644 + + + CF$UID + 5645 + + + CF$UID + 5646 + + + CF$UID + 5647 + + + CF$UID + 5648 + + + CF$UID + 5649 + + + CF$UID + 5650 + + + CF$UID + 5651 + + + CF$UID + 5652 + + + CF$UID + 5653 + + + CF$UID + 5656 + + + CF$UID + 5657 + + + CF$UID + 5658 + + + CF$UID + 5659 + + + CF$UID + 5660 + + + CF$UID + 5661 + + + CF$UID + 5662 + + + CF$UID + 5663 + + + CF$UID + 5664 + + + CF$UID + 5665 + + + CF$UID + 5666 + + + CF$UID + 5667 + + + CF$UID + 5668 + + + CF$UID + 5669 + + + CF$UID + 5670 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 33 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 25 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 25 + s + + CF$UID + 5318 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5319 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 129 + len + 41 + x + 25 + + + $class + + CF$UID + 24 + + c + 24 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 24 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 25 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 5324 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5325 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 133 + len + 14 + x + 6 + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 5332 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5333 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 139 + len + 45 + x + 22 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 21 + s + + CF$UID + 5338 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5339 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 143 + len + 33 + x + 21 + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 21 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 5346 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5347 + + + CF$UID + 5348 + + + CF$UID + 5349 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 149 + len + 47 + x + 16 + + + $class + + CF$UID + 136 + + c + 48 + l + 149 + len + 4 + x + 1 + + + $class + + CF$UID + 136 + + c + 52 + l + 149 + len + 2 + x + 16 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 5356 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5357 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 155 + len + 24 + x + 1 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 5375 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5376 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 172 + len + 1 + x + 22 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 21 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 21 + s + + CF$UID + 5395 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5396 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 190 + len + 88 + x + 21 + + + $class + + CF$UID + 24 + + c + 13 + s + + CF$UID + 5398 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5399 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 191 + len + 69 + x + 13 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 13 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 21 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 5409 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5410 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 200 + len + 1 + x + 21 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 5476 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5477 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 265 + len + 28 + x + 3 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 5482 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5483 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 269 + len + 77 + x + 1 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 5485 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5486 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 270 + len + 1 + x + 3 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 5494 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5495 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 277 + len + 28 + x + 7 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 5503 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5504 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 284 + len + 40 + x + 6 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 5509 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5510 + + + CF$UID + 5511 + + + CF$UID + 5512 + + + CF$UID + 5513 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 288 + len + 11 + x + 6 + + + $class + + CF$UID + 136 + + c + 12 + l + 288 + len + 109 + x + 4 + + + $class + + CF$UID + 136 + + c + 121 + l + 288 + len + 22 + x + 2 + + + $class + + CF$UID + 136 + + c + 143 + l + 288 + len + 2 + x + 4 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 5515 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5516 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 289 + len + 66 + x + 2 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 5521 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5522 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 293 + len + 15 + x + 2 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 5526 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5527 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 296 + len + 11 + x + 4 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 5534 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5535 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 302 + len + 1 + x + 7 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 5545 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5546 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 311 + len + 24 + x + 8 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 5552 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5553 + + + CF$UID + 5554 + + + CF$UID + 5555 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 316 + len + 27 + x + 6 + + + $class + + CF$UID + 136 + + c + 28 + l + 316 + len + 18 + x + 3 + + + $class + + CF$UID + 136 + + c + 46 + l + 316 + len + 2 + x + 6 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 5567 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5568 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 327 + len + 37 + x + 2 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 5576 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5577 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 334 + len + 77 + x + 2 + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 5586 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5587 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 342 + len + 13 + x + 2 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 5591 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5592 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 345 + len + 79 + x + 2 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 5595 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5596 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 347 + len + 19 + x + 2 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 5602 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5603 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 352 + len + 1 + x + 8 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 5613 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5614 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 361 + len + 37 + x + 15 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 15 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 5622 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5623 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 368 + len + 28 + x + 14 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 5632 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5633 + + + CF$UID + 5634 + + + CF$UID + 5635 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 376 + len + 47 + x + 14 + + + $class + + CF$UID + 136 + + c + 48 + l + 376 + len + 4 + x + 1 + + + $class + + CF$UID + 136 + + c + 52 + l + 376 + len + 2 + x + 14 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 14 + s + + CF$UID + 5654 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5655 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 394 + len + 1 + x + 15 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 5674 + + functions + + CF$UID + 5675 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 5724 + + name + + CF$UID + 5672 + + uniqueIdentifier + + CF$UID + 5673 + + + SPTDataLoaderFactory.m + DEF0822E-AC19-44DA-98EC-315C230E050C + /Users/dflems/Code/SPTDataLoader/SPTDataLoader/SPTDataLoaderFactory.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5676 + + + CF$UID + 5679 + + + CF$UID + 5682 + + + CF$UID + 5685 + + + CF$UID + 5688 + + + CF$UID + 5691 + + + CF$UID + 5694 + + + CF$UID + 5697 + + + CF$UID + 5700 + + + CF$UID + 5703 + + + CF$UID + 5706 + + + CF$UID + 5709 + + + CF$UID + 5712 + + + CF$UID + 5715 + + + CF$UID + 5718 + + + CF$UID + 5721 + + + + + $class + + CF$UID + 20 + + executionCount + 22 + lineCoverage + + CF$UID + 13 + + lineNumber + 47 + name + + CF$UID + 5677 + + symbolKindIdentifier + + CF$UID + 4010 + + uniqueIdentifier + + CF$UID + 5678 + + + +[SPTDataLoaderFactory dataLoaderFactoryWithRequestResponseHandlerDelegate:authorisers:] + 322E25AE-CB44-4186-9D55-AE1776EDFAE4 + + $class + + CF$UID + 20 + + executionCount + 22 + lineCoverage + + CF$UID + 13 + + lineNumber + 53 + name + + CF$UID + 5680 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5681 + + + -[SPTDataLoaderFactory initWithRequestResponseHandlerDelegate:authorisers:] + 13A4A79D-36E5-44E5-B3E0-B0F5035954F0 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 73 + name + + CF$UID + 5683 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5684 + + + -[SPTDataLoaderFactory createDataLoader] + BFBD5EC7-A34B-4DA6-A6EE-BF830D29F58B + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 84 + name + + CF$UID + 5686 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5687 + + + -[SPTDataLoaderFactory successfulResponse:] + 83690933-2043-46DD-B4AD-72275A54A1DF + + $class + + CF$UID + 20 + + executionCount + 4 + lineCoverage + + CF$UID + 13 + + lineNumber + 94 + name + + CF$UID + 5689 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5690 + + + -[SPTDataLoaderFactory failedResponse:] + 93291CC4-396F-4630-81B7-7F247C35BC98 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 118 + name + + CF$UID + 5692 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5693 + + + -[SPTDataLoaderFactory cancelledRequest:] + 4D5EB8F1-3E82-432F-99D3-650A0AC51E6C + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 128 + name + + CF$UID + 5695 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5696 + + + -[SPTDataLoaderFactory receivedDataChunk:forResponse:] + 8CB45A94-A9FD-4B7F-95D6-96FDEC8AAD80 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 137 + name + + CF$UID + 5698 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5699 + + + -[SPTDataLoaderFactory receivedInitialResponse:] + F0654FFB-2560-4784-B402-D120A7768F3A + + $class + + CF$UID + 20 + + executionCount + 4 + lineCoverage + + CF$UID + 13 + + lineNumber + 146 + name + + CF$UID + 5701 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5702 + + + -[SPTDataLoaderFactory shouldAuthoriseRequest:] + 1F0FEF02-29A9-4DA5-89FF-A9B21D7A8E01 + + $class + + CF$UID + 20 + + executionCount + 3 + lineCoverage + + CF$UID + 13 + + lineNumber + 157 + name + + CF$UID + 5704 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5705 + + + -[SPTDataLoaderFactory authoriseRequest:] + 2B0D55C8-C79E-472D-989E-21BB896597A3 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 167 + name + + CF$UID + 5707 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5708 + + + -[SPTDataLoaderFactory needsNewBodyStream:forRequest:] + C64A8F99-FA6B-49BD-8A85-4D10661D8034 + + $class + + CF$UID + 20 + + executionCount + 10 + lineCoverage + + CF$UID + 13 + + lineNumber + 179 + name + + CF$UID + 5710 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5711 + + + -[SPTDataLoaderFactory requestResponseHandler:performRequest:] + 1205B4A3-1C4B-4207-8A88-C8F9B9B3B903 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 194 + name + + CF$UID + 5713 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 5714 + + + __62-[SPTDataLoaderFactory requestResponseHandler:performRequest:]_block_invoke + 639C371A-9011-4E4F-ADF7-28115699B60E + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 212 + name + + CF$UID + 5716 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5717 + + + -[SPTDataLoaderFactory requestResponseHandler:cancelRequest:] + 4AF372E2-72AB-4938-8DB7-B8BDF7570CAB + + $class + + CF$UID + 20 + + executionCount + 4 + lineCoverage + + CF$UID + 13 + + lineNumber + 220 + name + + CF$UID + 5719 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5720 + + + -[SPTDataLoaderFactory dataLoaderAuthoriser:authorisedRequest:] + 4019B217-DE20-49F6-A1B1-C43E19C0EC72 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 230 + name + + CF$UID + 5722 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5723 + + + -[SPTDataLoaderFactory dataLoaderAuthoriser:didFailToAuthoriseRequest:withError:] + E0B2EB34-2425-4A7A-9762-9492AD62D1B3 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5725 + + + CF$UID + 5726 + + + CF$UID + 5727 + + + CF$UID + 5728 + + + CF$UID + 5729 + + + CF$UID + 5730 + + + CF$UID + 5731 + + + CF$UID + 5732 + + + CF$UID + 5733 + + + CF$UID + 5734 + + + CF$UID + 5735 + + + CF$UID + 5736 + + + CF$UID + 5737 + + + CF$UID + 5738 + + + CF$UID + 5739 + + + CF$UID + 5740 + + + CF$UID + 5741 + + + CF$UID + 5742 + + + CF$UID + 5743 + + + CF$UID + 5744 + + + CF$UID + 5745 + + + CF$UID + 5746 + + + CF$UID + 5747 + + + CF$UID + 5748 + + + CF$UID + 5749 + + + CF$UID + 5750 + + + CF$UID + 5751 + + + CF$UID + 5752 + + + CF$UID + 5753 + + + CF$UID + 5754 + + + CF$UID + 5755 + + + CF$UID + 5756 + + + CF$UID + 5757 + + + CF$UID + 5758 + + + CF$UID + 5759 + + + CF$UID + 5760 + + + CF$UID + 5761 + + + CF$UID + 5762 + + + CF$UID + 5763 + + + CF$UID + 5764 + + + CF$UID + 5765 + + + CF$UID + 5766 + + + CF$UID + 5767 + + + CF$UID + 5768 + + + CF$UID + 5769 + + + CF$UID + 5770 + + + CF$UID + 5771 + + + CF$UID + 5772 + + + CF$UID + 5773 + + + CF$UID + 5774 + + + CF$UID + 5775 + + + CF$UID + 5776 + + + CF$UID + 5777 + + + CF$UID + 5778 + + + CF$UID + 5779 + + + CF$UID + 5780 + + + CF$UID + 5781 + + + CF$UID + 5782 + + + CF$UID + 5783 + + + CF$UID + 5784 + + + CF$UID + 5785 + + + CF$UID + 5786 + + + CF$UID + 5787 + + + CF$UID + 5788 + + + CF$UID + 5789 + + + CF$UID + 5790 + + + CF$UID + 5791 + + + CF$UID + 5792 + + + CF$UID + 5793 + + + CF$UID + 5794 + + + CF$UID + 5795 + + + CF$UID + 5796 + + + CF$UID + 5797 + + + CF$UID + 5798 + + + CF$UID + 5799 + + + CF$UID + 5800 + + + CF$UID + 5801 + + + CF$UID + 5802 + + + CF$UID + 5803 + + + CF$UID + 5804 + + + CF$UID + 5805 + + + CF$UID + 5806 + + + CF$UID + 5807 + + + CF$UID + 5808 + + + CF$UID + 5809 + + + CF$UID + 5810 + + + CF$UID + 5811 + + + CF$UID + 5812 + + + CF$UID + 5813 + + + CF$UID + 5814 + + + CF$UID + 5815 + + + CF$UID + 5816 + + + CF$UID + 5817 + + + CF$UID + 5818 + + + CF$UID + 5819 + + + CF$UID + 5820 + + + CF$UID + 5825 + + + CF$UID + 5826 + + + CF$UID + 5827 + + + CF$UID + 5828 + + + CF$UID + 5829 + + + CF$UID + 5830 + + + CF$UID + 5831 + + + CF$UID + 5832 + + + CF$UID + 5833 + + + CF$UID + 5834 + + + CF$UID + 5835 + + + CF$UID + 5836 + + + CF$UID + 5837 + + + CF$UID + 5838 + + + CF$UID + 5839 + + + CF$UID + 5840 + + + CF$UID + 5841 + + + CF$UID + 5842 + + + CF$UID + 5843 + + + CF$UID + 5846 + + + CF$UID + 5847 + + + CF$UID + 5848 + + + CF$UID + 5849 + + + CF$UID + 5850 + + + CF$UID + 5851 + + + CF$UID + 5852 + + + CF$UID + 5853 + + + CF$UID + 5854 + + + CF$UID + 5855 + + + CF$UID + 5856 + + + CF$UID + 5857 + + + CF$UID + 5858 + + + CF$UID + 5859 + + + CF$UID + 5860 + + + CF$UID + 5861 + + + CF$UID + 5862 + + + CF$UID + 5863 + + + CF$UID + 5864 + + + CF$UID + 5865 + + + CF$UID + 5866 + + + CF$UID + 5867 + + + CF$UID + 5868 + + + CF$UID + 5869 + + + CF$UID + 5870 + + + CF$UID + 5871 + + + CF$UID + 5872 + + + CF$UID + 5873 + + + CF$UID + 5874 + + + CF$UID + 5875 + + + CF$UID + 5876 + + + CF$UID + 5877 + + + CF$UID + 5878 + + + CF$UID + 5881 + + + CF$UID + 5882 + + + CF$UID + 5883 + + + CF$UID + 5884 + + + CF$UID + 5885 + + + CF$UID + 5888 + + + CF$UID + 5889 + + + CF$UID + 5890 + + + CF$UID + 5891 + + + CF$UID + 5892 + + + CF$UID + 5893 + + + CF$UID + 5894 + + + CF$UID + 5895 + + + CF$UID + 5896 + + + CF$UID + 5897 + + + CF$UID + 5898 + + + CF$UID + 5899 + + + CF$UID + 5900 + + + CF$UID + 5901 + + + CF$UID + 5902 + + + CF$UID + 5903 + + + CF$UID + 5904 + + + CF$UID + 5905 + + + CF$UID + 5906 + + + CF$UID + 5907 + + + CF$UID + 5908 + + + CF$UID + 5909 + + + CF$UID + 5910 + + + CF$UID + 5911 + + + CF$UID + 5912 + + + CF$UID + 5913 + + + CF$UID + 5914 + + + CF$UID + 5917 + + + CF$UID + 5918 + + + CF$UID + 5919 + + + CF$UID + 5920 + + + CF$UID + 5921 + + + CF$UID + 5922 + + + CF$UID + 5923 + + + CF$UID + 5924 + + + CF$UID + 5925 + + + CF$UID + 5928 + + + CF$UID + 5929 + + + CF$UID + 5930 + + + CF$UID + 5931 + + + CF$UID + 5932 + + + CF$UID + 5933 + + + CF$UID + 5934 + + + CF$UID + 5935 + + + CF$UID + 5936 + + + CF$UID + 5937 + + + CF$UID + 5938 + + + CF$UID + 5939 + + + CF$UID + 5940 + + + CF$UID + 5941 + + + CF$UID + 5942 + + + CF$UID + 5943 + + + CF$UID + 5944 + + + CF$UID + 5945 + + + CF$UID + 5946 + + + CF$UID + 5947 + + + CF$UID + 5948 + + + CF$UID + 5949 + + + CF$UID + 5950 + + + CF$UID + 5951 + + + CF$UID + 5952 + + + CF$UID + 5953 + + + CF$UID + 5954 + + + CF$UID + 5955 + + + CF$UID + 5956 + + + CF$UID + 5957 + + + CF$UID + 5958 + + + CF$UID + 5959 + + + CF$UID + 5960 + + + CF$UID + 5963 + + + CF$UID + 5964 + + + CF$UID + 5965 + + + CF$UID + 5966 + + + CF$UID + 5967 + + + CF$UID + 5968 + + + CF$UID + 5969 + + + CF$UID + 5970 + + + CF$UID + 5971 + + + CF$UID + 5972 + + + CF$UID + 5973 + + + CF$UID + 5974 + + + CF$UID + 5975 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 21 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 21 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 21 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 5821 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5822 + + + CF$UID + 5823 + + + CF$UID + 5824 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 96 + len + 82 + x + 4 + + + $class + + CF$UID + 136 + + c + 83 + l + 96 + len + 38 + x + 2 + + + $class + + CF$UID + 136 + + c + 121 + l + 96 + len + 2 + x + 4 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 5844 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5845 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 115 + len + 1 + x + 4 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 5879 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5880 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 148 + len + 63 + x + 4 + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 5886 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5887 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 153 + len + 13 + x + 1 + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 5915 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5916 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 180 + len + 22 + x + 10 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 5926 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5927 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 189 + len + 31 + x + 10 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 10 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 5961 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5962 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 222 + len + 114 + x + 4 + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 5980 + + functions + + CF$UID + 5981 + + lineCoverage + + CF$UID + 5979 + + lines + + CF$UID + 6010 + + name + + CF$UID + 5977 + + uniqueIdentifier + + CF$UID + 5978 + + + SPTDataLoaderRateLimiter.m + 42E79C09-CFF0-48ED-A344-4E2CACE5217B + 0.97802197802197799 + /Users/dflems/Code/SPTDataLoader/SPTDataLoader/SPTDataLoaderRateLimiter.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 5982 + + + CF$UID + 5985 + + + CF$UID + 5988 + + + CF$UID + 5991 + + + CF$UID + 5995 + + + CF$UID + 5998 + + + CF$UID + 6001 + + + CF$UID + 6004 + + + CF$UID + 6007 + + + + + $class + + CF$UID + 20 + + executionCount + 55 + lineCoverage + + CF$UID + 13 + + lineNumber + 42 + name + + CF$UID + 5983 + + symbolKindIdentifier + + CF$UID + 4010 + + uniqueIdentifier + + CF$UID + 5984 + + + +[SPTDataLoaderRateLimiter rateLimiterWithDefaultRequestsPerSecond:] + DF231CB7-23A8-4929-8FAE-ADDE7A0EC6DE + + $class + + CF$UID + 20 + + executionCount + 55 + lineCoverage + + CF$UID + 13 + + lineNumber + 47 + name + + CF$UID + 5986 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5987 + + + -[SPTDataLoaderRateLimiter initWithDefaultRequestsPerSecond:] + FB331E1D-AF1C-4221-B564-7FA25631ACCA + + $class + + CF$UID + 20 + + executionCount + 38 + lineCoverage + + CF$UID + 13 + + lineNumber + 60 + name + + CF$UID + 5989 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5990 + + + -[SPTDataLoaderRateLimiter earliestTimeUntilRequestCanBeExecuted:] + 27A69C13-4EF9-436E-BB28-A6200254B22A + + $class + + CF$UID + 20 + + executionCount + 17 + lineCoverage + + CF$UID + 5994 + + lineNumber + 90 + name + + CF$UID + 5992 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5993 + + + -[SPTDataLoaderRateLimiter executedRequest:] + 8F2AC740-A6F4-49A0-B6BF-0F8E1BBA16F2 + 0.84615384615384615 + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 105 + name + + CF$UID + 5996 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 5997 + + + -[SPTDataLoaderRateLimiter requestsPerSecondForURL:] + 989C3D4C-222F-4364-B8E7-4AEF79DA0055 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 110 + name + + CF$UID + 5999 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6000 + + + -[SPTDataLoaderRateLimiter setRequestsPerSecond:forURL:] + 5FCF8AFA-5719-4374-8303-AEC8E0F6CC46 + + $class + + CF$UID + 20 + + executionCount + 5 + lineCoverage + + CF$UID + 13 + + lineNumber + 117 + name + + CF$UID + 6002 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6003 + + + -[SPTDataLoaderRateLimiter setRetryAfter:forURL:] + 46EBD2B7-582D-4F24-935E-53732F63DE04 + + $class + + CF$UID + 20 + + executionCount + 37 + lineCoverage + + CF$UID + 13 + + lineNumber + 128 + name + + CF$UID + 6005 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6006 + + + -[SPTDataLoaderRateLimiter requestsPerSecondForServiceKey:] + 53820323-C1E7-484F-8547-194E8E67C7CB + + $class + + CF$UID + 20 + + executionCount + 62 + lineCoverage + + CF$UID + 13 + + lineNumber + 136 + name + + CF$UID + 6008 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6009 + + + -[SPTDataLoaderRateLimiter serviceKeyFromURL:] + 395066A2-CF34-42DA-99EB-F80A36E65BFE + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6011 + + + CF$UID + 6012 + + + CF$UID + 6013 + + + CF$UID + 6014 + + + CF$UID + 6015 + + + CF$UID + 6016 + + + CF$UID + 6017 + + + CF$UID + 6018 + + + CF$UID + 6019 + + + CF$UID + 6020 + + + CF$UID + 6021 + + + CF$UID + 6022 + + + CF$UID + 6023 + + + CF$UID + 6024 + + + CF$UID + 6025 + + + CF$UID + 6026 + + + CF$UID + 6027 + + + CF$UID + 6028 + + + CF$UID + 6029 + + + CF$UID + 6030 + + + CF$UID + 6031 + + + CF$UID + 6032 + + + CF$UID + 6033 + + + CF$UID + 6034 + + + CF$UID + 6035 + + + CF$UID + 6036 + + + CF$UID + 6037 + + + CF$UID + 6038 + + + CF$UID + 6039 + + + CF$UID + 6040 + + + CF$UID + 6041 + + + CF$UID + 6042 + + + CF$UID + 6043 + + + CF$UID + 6044 + + + CF$UID + 6045 + + + CF$UID + 6046 + + + CF$UID + 6047 + + + CF$UID + 6048 + + + CF$UID + 6049 + + + CF$UID + 6050 + + + CF$UID + 6051 + + + CF$UID + 6052 + + + CF$UID + 6053 + + + CF$UID + 6054 + + + CF$UID + 6055 + + + CF$UID + 6056 + + + CF$UID + 6057 + + + CF$UID + 6058 + + + CF$UID + 6059 + + + CF$UID + 6060 + + + CF$UID + 6061 + + + CF$UID + 6062 + + + CF$UID + 6063 + + + CF$UID + 6064 + + + CF$UID + 6065 + + + CF$UID + 6066 + + + CF$UID + 6067 + + + CF$UID + 6068 + + + CF$UID + 6069 + + + CF$UID + 6070 + + + CF$UID + 6071 + + + CF$UID + 6072 + + + CF$UID + 6073 + + + CF$UID + 6074 + + + CF$UID + 6075 + + + CF$UID + 6076 + + + CF$UID + 6077 + + + CF$UID + 6078 + + + CF$UID + 6079 + + + CF$UID + 6082 + + + CF$UID + 6083 + + + CF$UID + 6084 + + + CF$UID + 6085 + + + CF$UID + 6086 + + + CF$UID + 6087 + + + CF$UID + 6088 + + + CF$UID + 6089 + + + CF$UID + 6090 + + + CF$UID + 6091 + + + CF$UID + 6092 + + + CF$UID + 6093 + + + CF$UID + 6094 + + + CF$UID + 6097 + + + CF$UID + 6098 + + + CF$UID + 6099 + + + CF$UID + 6100 + + + CF$UID + 6101 + + + CF$UID + 6102 + + + CF$UID + 6103 + + + CF$UID + 6104 + + + CF$UID + 6105 + + + CF$UID + 6106 + + + CF$UID + 6109 + + + CF$UID + 6110 + + + CF$UID + 6111 + + + CF$UID + 6112 + + + CF$UID + 6113 + + + CF$UID + 6114 + + + CF$UID + 6115 + + + CF$UID + 6116 + + + CF$UID + 6117 + + + CF$UID + 6118 + + + CF$UID + 6121 + + + CF$UID + 6122 + + + CF$UID + 6123 + + + CF$UID + 6124 + + + CF$UID + 6125 + + + CF$UID + 6126 + + + CF$UID + 6127 + + + CF$UID + 6128 + + + CF$UID + 6129 + + + CF$UID + 6130 + + + CF$UID + 6131 + + + CF$UID + 6132 + + + CF$UID + 6133 + + + CF$UID + 6134 + + + CF$UID + 6135 + + + CF$UID + 6136 + + + CF$UID + 6139 + + + CF$UID + 6140 + + + CF$UID + 6141 + + + CF$UID + 6142 + + + CF$UID + 6143 + + + CF$UID + 6144 + + + CF$UID + 6145 + + + CF$UID + 6148 + + + CF$UID + 6149 + + + CF$UID + 6150 + + + CF$UID + 6151 + + + CF$UID + 6152 + + + CF$UID + 6153 + + + CF$UID + 6158 + + + CF$UID + 6159 + + + CF$UID + 6160 + + + CF$UID + 6161 + + + CF$UID + 6162 + + + CF$UID + 6163 + + + CF$UID + 6166 + + + CF$UID + 6167 + + + CF$UID + 6168 + + + CF$UID + 6169 + + + CF$UID + 6170 + + + CF$UID + 6171 + + + CF$UID + 6172 + + + CF$UID + 6173 + + + CF$UID + 6174 + + + CF$UID + 6175 + + + CF$UID + 6176 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 55 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 55 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 55 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 55 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 55 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 55 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 55 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 55 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 55 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 55 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 55 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 55 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 55 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 55 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 38 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 38 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 38 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 38 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 38 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 38 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 38 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 38 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 38 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 38 + s + + CF$UID + 6080 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6081 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 69 + len + 35 + x + 38 + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 38 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 38 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 6095 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6096 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 82 + len + 28 + x + 35 + + + $class + + CF$UID + 24 + + c + 28 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 28 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 35 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 38 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 6107 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6108 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 92 + len + 21 + x + 17 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 6119 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6120 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 102 + len + 1 + x + 17 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 6137 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6138 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 118 + len + 14 + x + 5 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 6146 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6147 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 125 + len + 1 + x + 5 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 37 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 37 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 37 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 6154 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6155 + + + CF$UID + 6156 + + + CF$UID + 6157 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 131 + len + 49 + x + 1 + + + $class + + CF$UID + 136 + + c + 50 + l + 131 + len + 3 + x + 37 + + + $class + + CF$UID + 136 + + c + 53 + l + 131 + len + 22 + x + 36 + + + $class + + CF$UID + 24 + + c + 37 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 37 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 62 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 62 + s + + CF$UID + 6164 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6165 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 137 + len + 14 + x + 62 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 62 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 61 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 61 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 61 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 61 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 61 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 61 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 61 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 62 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 6181 + + functions + + CF$UID + 6182 + + lineCoverage + + CF$UID + 6180 + + lines + + CF$UID + 6209 + + name + + CF$UID + 6178 + + uniqueIdentifier + + CF$UID + 6179 + + + SPTDataLoaderServerTrustPolicy.m + 35A0A575-63D6-4E19-B2A4-4D000DB22076 + 0.93382352941176472 + /Users/dflems/Code/SPTDataLoader/SPTDataLoader/SPTDataLoaderServerTrustPolicy.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6183 + + + CF$UID + 6186 + + + CF$UID + 6189 + + + CF$UID + 6192 + + + CF$UID + 6196 + + + CF$UID + 6199 + + + CF$UID + 6203 + + + CF$UID + 6206 + + + + + $class + + CF$UID + 20 + + executionCount + 23 + lineCoverage + + CF$UID + 13 + + lineNumber + 60 + name + + CF$UID + 6184 + + symbolKindIdentifier + + CF$UID + 4010 + + uniqueIdentifier + + CF$UID + 6185 + + + +[SPTDataLoaderServerTrustPolicy policyWithHostsAndCertificatePaths:] + F72025B1-6853-4F33-B8AC-1EA8A3BD74F6 + + $class + + CF$UID + 20 + + executionCount + 4 + lineCoverage + + CF$UID + 13 + + lineNumber + 69 + name + + CF$UID + 6187 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6188 + + + -[SPTDataLoaderServerTrustPolicy validateChallenge:] + 8A620F67-4838-46EE-9638-6C621F677CF4 + + $class + + CF$UID + 20 + + executionCount + 9 + lineCoverage + + CF$UID + 13 + + lineNumber + 91 + name + + CF$UID + 6190 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6191 + + + -[SPTDataLoaderServerTrustPolicy certificatesForHost:] + FEE437EF-080F-4247-8DC2-AE5DA2B8F2F9 + + $class + + CF$UID + 20 + + executionCount + 4 + lineCoverage + + CF$UID + 6195 + + lineNumber + 110 + name + + CF$UID + 6193 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6194 + + + -[SPTDataLoaderServerTrustPolicy validateWithTrust:host:] + DDD45B07-FCA9-454F-B5C3-FFE60674D398 + 0.83333333333333337 + + $class + + CF$UID + 20 + + executionCount + 22 + lineCoverage + + CF$UID + 13 + + lineNumber + 156 + name + + CF$UID + 6197 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6198 + + + -[SPTDataLoaderServerTrustPolicy initWithHostsAndCertificatePaths:] + 11A3B43A-E01A-4CE0-AF3F-470BE4C3F2A4 + + $class + + CF$UID + 20 + + executionCount + 22 + lineCoverage + + CF$UID + 6202 + + lineNumber + 161 + name + + CF$UID + 6200 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 6201 + + + __67-[SPTDataLoaderServerTrustPolicy initWithHostsAndCertificatePaths:]_block_invoke + C0237679-C00A-44D2-BB04-5338A3F3DA60 + 0.81818181818181823 + + $class + + CF$UID + 20 + + executionCount + 5 + lineCoverage + + CF$UID + 13 + + lineNumber + 27 + name + + CF$UID + 6204 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 6205 + + + SPTEvaluteTrust + C72628D9-8DD6-4F50-99CE-9312E6DBA086 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 37 + name + + CF$UID + 6207 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 6208 + + + SPTCertificatesForTrust + 51A1A84F-D315-4296-BD74-D07526AEBD63 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6210 + + + CF$UID + 6211 + + + CF$UID + 6212 + + + CF$UID + 6213 + + + CF$UID + 6214 + + + CF$UID + 6215 + + + CF$UID + 6216 + + + CF$UID + 6217 + + + CF$UID + 6218 + + + CF$UID + 6219 + + + CF$UID + 6220 + + + CF$UID + 6221 + + + CF$UID + 6222 + + + CF$UID + 6223 + + + CF$UID + 6224 + + + CF$UID + 6225 + + + CF$UID + 6226 + + + CF$UID + 6227 + + + CF$UID + 6228 + + + CF$UID + 6229 + + + CF$UID + 6230 + + + CF$UID + 6231 + + + CF$UID + 6232 + + + CF$UID + 6233 + + + CF$UID + 6234 + + + CF$UID + 6235 + + + CF$UID + 6236 + + + CF$UID + 6237 + + + CF$UID + 6238 + + + CF$UID + 6239 + + + CF$UID + 6240 + + + CF$UID + 6241 + + + CF$UID + 6244 + + + CF$UID + 6245 + + + CF$UID + 6246 + + + CF$UID + 6247 + + + CF$UID + 6248 + + + CF$UID + 6249 + + + CF$UID + 6250 + + + CF$UID + 6251 + + + CF$UID + 6252 + + + CF$UID + 6256 + + + CF$UID + 6257 + + + CF$UID + 6258 + + + CF$UID + 6259 + + + CF$UID + 6260 + + + CF$UID + 6261 + + + CF$UID + 6262 + + + CF$UID + 6263 + + + CF$UID + 6264 + + + CF$UID + 6265 + + + CF$UID + 6266 + + + CF$UID + 6267 + + + CF$UID + 6268 + + + CF$UID + 6269 + + + CF$UID + 6270 + + + CF$UID + 6271 + + + CF$UID + 6272 + + + CF$UID + 6273 + + + CF$UID + 6274 + + + CF$UID + 6275 + + + CF$UID + 6278 + + + CF$UID + 6279 + + + CF$UID + 6280 + + + CF$UID + 6281 + + + CF$UID + 6284 + + + CF$UID + 6285 + + + CF$UID + 6286 + + + CF$UID + 6287 + + + CF$UID + 6288 + + + CF$UID + 6289 + + + CF$UID + 6292 + + + CF$UID + 6293 + + + CF$UID + 6294 + + + CF$UID + 6295 + + + CF$UID + 6296 + + + CF$UID + 6299 + + + CF$UID + 6300 + + + CF$UID + 6301 + + + CF$UID + 6302 + + + CF$UID + 6303 + + + CF$UID + 6306 + + + CF$UID + 6307 + + + CF$UID + 6308 + + + CF$UID + 6309 + + + CF$UID + 6312 + + + CF$UID + 6315 + + + CF$UID + 6316 + + + CF$UID + 6317 + + + CF$UID + 6318 + + + CF$UID + 6319 + + + CF$UID + 6320 + + + CF$UID + 6321 + + + CF$UID + 6322 + + + CF$UID + 6323 + + + CF$UID + 6324 + + + CF$UID + 6327 + + + CF$UID + 6328 + + + CF$UID + 6329 + + + CF$UID + 6330 + + + CF$UID + 6331 + + + CF$UID + 6332 + + + CF$UID + 6333 + + + CF$UID + 6334 + + + CF$UID + 6335 + + + CF$UID + 6336 + + + CF$UID + 6337 + + + CF$UID + 6338 + + + CF$UID + 6339 + + + CF$UID + 6340 + + + CF$UID + 6341 + + + CF$UID + 6344 + + + CF$UID + 6345 + + + CF$UID + 6346 + + + CF$UID + 6347 + + + CF$UID + 6348 + + + CF$UID + 6349 + + + CF$UID + 6350 + + + CF$UID + 6351 + + + CF$UID + 6352 + + + CF$UID + 6353 + + + CF$UID + 6356 + + + CF$UID + 6357 + + + CF$UID + 6358 + + + CF$UID + 6359 + + + CF$UID + 6360 + + + CF$UID + 6361 + + + CF$UID + 6364 + + + CF$UID + 6365 + + + CF$UID + 6366 + + + CF$UID + 6367 + + + CF$UID + 6368 + + + CF$UID + 6369 + + + CF$UID + 6370 + + + CF$UID + 6371 + + + CF$UID + 6372 + + + CF$UID + 6373 + + + CF$UID + 6374 + + + CF$UID + 6377 + + + CF$UID + 6378 + + + CF$UID + 6379 + + + CF$UID + 6380 + + + CF$UID + 6381 + + + CF$UID + 6382 + + + CF$UID + 6383 + + + CF$UID + 6384 + + + CF$UID + 6385 + + + CF$UID + 6386 + + + CF$UID + 6387 + + + CF$UID + 6388 + + + CF$UID + 6391 + + + CF$UID + 6394 + + + CF$UID + 6395 + + + CF$UID + 6396 + + + CF$UID + 6397 + + + CF$UID + 6398 + + + CF$UID + 6399 + + + CF$UID + 6400 + + + CF$UID + 6401 + + + CF$UID + 6402 + + + CF$UID + 6403 + + + CF$UID + 6404 + + + CF$UID + 6405 + + + CF$UID + 6406 + + + CF$UID + 6407 + + + CF$UID + 6410 + + + CF$UID + 6411 + + + CF$UID + 6412 + + + CF$UID + 6413 + + + CF$UID + 6414 + + + CF$UID + 6415 + + + CF$UID + 6416 + + + CF$UID + 6417 + + + CF$UID + 6418 + + + CF$UID + 6419 + + + CF$UID + 6420 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 6242 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6243 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 32 + len + 91 + x + 3 + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 6253 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6254 + + + CF$UID + 6255 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 41 + len + 33 + x + 2 + + + $class + + CF$UID + 136 + + c + 34 + l + 41 + len + 7 + x + 1 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 23 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 23 + s + + CF$UID + 6276 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6277 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 61 + len + 35 + x + 23 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 23 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 6282 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6283 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 65 + len + 83 + x + 22 + + + $class + + CF$UID + 24 + + c + 23 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 6290 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6291 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 71 + len + 86 + x + 4 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 6297 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6298 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 76 + len + 16 + x + 3 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 6304 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6305 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 81 + len + 15 + x + 2 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 6310 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6311 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 85 + len + 51 + x + 1 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 6313 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6314 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 86 + len + 1 + x + 4 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 9 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 9 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 9 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 9 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 9 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 9 + s + + CF$UID + 6325 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6326 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 96 + len + 28 + x + 9 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 9 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 9 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 6342 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6343 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 111 + len + 15 + x + 4 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 6354 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6355 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 121 + len + 33 + x + 4 + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 6362 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6363 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 127 + len + 58 + x + 1 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 6375 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6376 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 138 + len + 33 + x + 1 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 6389 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6390 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 150 + len + 13 + x + 0 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 6392 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6393 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 151 + len + 1 + x + 4 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 44 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 44 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 67 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 67 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 67 + s + + CF$UID + 6408 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6409 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 165 + len + 27 + x + 45 + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 67 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 67 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 44 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 44 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 22 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 6424 + + functions + + CF$UID + 6425 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 6465 + + name + + CF$UID + 6422 + + uniqueIdentifier + + CF$UID + 6423 + + + SPTDataLoaderRequest.m + C40CFB4C-7373-4E46-935F-D03584EB2BCF + /Users/dflems/Code/SPTDataLoader/SPTDataLoader/SPTDataLoaderRequest.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6426 + + + CF$UID + 6429 + + + CF$UID + 6432 + + + CF$UID + 6435 + + + CF$UID + 6438 + + + CF$UID + 6441 + + + CF$UID + 6444 + + + CF$UID + 6447 + + + CF$UID + 6450 + + + CF$UID + 6453 + + + CF$UID + 6456 + + + CF$UID + 6459 + + + CF$UID + 6462 + + + + + $class + + CF$UID + 20 + + executionCount + 71 + lineCoverage + + CF$UID + 13 + + lineNumber + 46 + name + + CF$UID + 6427 + + symbolKindIdentifier + + CF$UID + 4010 + + uniqueIdentifier + + CF$UID + 6428 + + + +[SPTDataLoaderRequest requestWithURL:sourceIdentifier:] + B6FA07D2-12BF-44A2-9B78-D2173E1676DA + + $class + + CF$UID + 20 + + executionCount + 90 + lineCoverage + + CF$UID + 13 + + lineNumber + 58 + name + + CF$UID + 6430 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6431 + + + -[SPTDataLoaderRequest initWithURL:sourceIdentifier:uniqueIdentifier:] + 0CD81318-DC0A-46EA-844B-B1B8BB81616C + + $class + + CF$UID + 20 + + executionCount + 67 + lineCoverage + + CF$UID + 13 + + lineNumber + 73 + name + + CF$UID + 6433 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6434 + + + -[SPTDataLoaderRequest headers] + 5F85502A-BFBD-4A95-942E-DC2009C74F9A + + $class + + CF$UID + 20 + + executionCount + 7 + lineCoverage + + CF$UID + 13 + + lineNumber + 80 + name + + CF$UID + 6436 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6437 + + + -[SPTDataLoaderRequest addValue:forHeader:] + E4B135C4-5DF5-4AC0-A18F-3A4C7CB05391 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 96 + name + + CF$UID + 6439 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6440 + + + -[SPTDataLoaderRequest removeHeader:] + E31BCAE8-DDC6-4A8B-A9B7-4173E1E543CB + + $class + + CF$UID + 20 + + executionCount + 30 + lineCoverage + + CF$UID + 13 + + lineNumber + 105 + name + + CF$UID + 6442 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6443 + + + -[SPTDataLoaderRequest urlRequest] + 63369BC6-1902-4DDD-A1F2-3EF2A4A56265 + + $class + + CF$UID + 20 + + executionCount + 31 + lineCoverage + + CF$UID + 13 + + lineNumber + 136 + name + + CF$UID + 6445 + + symbolKindIdentifier + + CF$UID + 4010 + + uniqueIdentifier + + CF$UID + 6446 + + + +[SPTDataLoaderRequest languageHeaderValue] + 23BBAC06-B5A6-4195-AF95-A7FF8DF4DCB1 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 139 + name + + CF$UID + 6448 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 6449 + + + __43+[SPTDataLoaderRequest languageHeaderValue]_block_invoke + D2BA9C52-D40B-4B0F-822D-5C396900814B + + $class + + CF$UID + 20 + + executionCount + 3 + lineCoverage + + CF$UID + 13 + + lineNumber + 146 + name + + CF$UID + 6451 + + symbolKindIdentifier + + CF$UID + 4010 + + uniqueIdentifier + + CF$UID + 6452 + + + +[SPTDataLoaderRequest generateLanguageHeaderValue] + A382142C-2E4B-4BE7-B94C-170EB85EA07B + + $class + + CF$UID + 20 + + executionCount + 3 + lineCoverage + + CF$UID + 13 + + lineNumber + 151 + name + + CF$UID + 6454 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 6455 + + + __51+[SPTDataLoaderRequest generateLanguageHeaderValue]_block_invoke + F6DABC45-C3C6-49B3-A13A-F9500A6465F0 + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 186 + name + + CF$UID + 6457 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6458 + + + -[SPTDataLoaderRequest description] + 599923D3-EF2B-42DE-B441-31B855C77599 + + $class + + CF$UID + 20 + + executionCount + 19 + lineCoverage + + CF$UID + 13 + + lineNumber + 193 + name + + CF$UID + 6460 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6461 + + + -[SPTDataLoaderRequest copyWithZone:] + 47D297E4-39FC-439F-B986-E0F26B07DBCF + + $class + + CF$UID + 20 + + executionCount + 30 + lineCoverage + + CF$UID + 13 + + lineNumber + 221 + name + + CF$UID + 6463 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 6464 + + + NSStringFromSPTDataLoaderRequestMethod + C6127621-0F77-47C9-9FBF-C31D09511FBA + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6466 + + + CF$UID + 6467 + + + CF$UID + 6468 + + + CF$UID + 6469 + + + CF$UID + 6470 + + + CF$UID + 6471 + + + CF$UID + 6472 + + + CF$UID + 6473 + + + CF$UID + 6474 + + + CF$UID + 6475 + + + CF$UID + 6476 + + + CF$UID + 6477 + + + CF$UID + 6478 + + + CF$UID + 6479 + + + CF$UID + 6480 + + + CF$UID + 6481 + + + CF$UID + 6482 + + + CF$UID + 6483 + + + CF$UID + 6484 + + + CF$UID + 6485 + + + CF$UID + 6486 + + + CF$UID + 6487 + + + CF$UID + 6488 + + + CF$UID + 6489 + + + CF$UID + 6490 + + + CF$UID + 6491 + + + CF$UID + 6492 + + + CF$UID + 6493 + + + CF$UID + 6494 + + + CF$UID + 6495 + + + CF$UID + 6496 + + + CF$UID + 6497 + + + CF$UID + 6498 + + + CF$UID + 6499 + + + CF$UID + 6500 + + + CF$UID + 6501 + + + CF$UID + 6502 + + + CF$UID + 6503 + + + CF$UID + 6504 + + + CF$UID + 6505 + + + CF$UID + 6506 + + + CF$UID + 6507 + + + CF$UID + 6508 + + + CF$UID + 6509 + + + CF$UID + 6510 + + + CF$UID + 6511 + + + CF$UID + 6512 + + + CF$UID + 6513 + + + CF$UID + 6514 + + + CF$UID + 6515 + + + CF$UID + 6516 + + + CF$UID + 6517 + + + CF$UID + 6518 + + + CF$UID + 6519 + + + CF$UID + 6520 + + + CF$UID + 6521 + + + CF$UID + 6522 + + + CF$UID + 6523 + + + CF$UID + 6524 + + + CF$UID + 6525 + + + CF$UID + 6526 + + + CF$UID + 6527 + + + CF$UID + 6528 + + + CF$UID + 6529 + + + CF$UID + 6530 + + + CF$UID + 6531 + + + CF$UID + 6532 + + + CF$UID + 6533 + + + CF$UID + 6534 + + + CF$UID + 6535 + + + CF$UID + 6536 + + + CF$UID + 6537 + + + CF$UID + 6538 + + + CF$UID + 6539 + + + CF$UID + 6540 + + + CF$UID + 6541 + + + CF$UID + 6542 + + + CF$UID + 6543 + + + CF$UID + 6544 + + + CF$UID + 6545 + + + CF$UID + 6546 + + + CF$UID + 6549 + + + CF$UID + 6550 + + + CF$UID + 6551 + + + CF$UID + 6552 + + + CF$UID + 6553 + + + CF$UID + 6558 + + + CF$UID + 6559 + + + CF$UID + 6560 + + + CF$UID + 6561 + + + CF$UID + 6562 + + + CF$UID + 6563 + + + CF$UID + 6564 + + + CF$UID + 6567 + + + CF$UID + 6568 + + + CF$UID + 6569 + + + CF$UID + 6570 + + + CF$UID + 6571 + + + CF$UID + 6572 + + + CF$UID + 6573 + + + CF$UID + 6574 + + + CF$UID + 6575 + + + CF$UID + 6576 + + + CF$UID + 6577 + + + CF$UID + 6578 + + + CF$UID + 6579 + + + CF$UID + 6580 + + + CF$UID + 6581 + + + CF$UID + 6582 + + + CF$UID + 6583 + + + CF$UID + 6584 + + + CF$UID + 6585 + + + CF$UID + 6586 + + + CF$UID + 6587 + + + CF$UID + 6588 + + + CF$UID + 6589 + + + CF$UID + 6592 + + + CF$UID + 6593 + + + CF$UID + 6597 + + + CF$UID + 6598 + + + CF$UID + 6599 + + + CF$UID + 6600 + + + CF$UID + 6601 + + + CF$UID + 6602 + + + CF$UID + 6603 + + + CF$UID + 6604 + + + CF$UID + 6605 + + + CF$UID + 6606 + + + CF$UID + 6607 + + + CF$UID + 6608 + + + CF$UID + 6609 + + + CF$UID + 6610 + + + CF$UID + 6611 + + + CF$UID + 6612 + + + CF$UID + 6613 + + + CF$UID + 6614 + + + CF$UID + 6615 + + + CF$UID + 6616 + + + CF$UID + 6617 + + + CF$UID + 6618 + + + CF$UID + 6619 + + + CF$UID + 6620 + + + CF$UID + 6621 + + + CF$UID + 6622 + + + CF$UID + 6623 + + + CF$UID + 6624 + + + CF$UID + 6625 + + + CF$UID + 6626 + + + CF$UID + 6627 + + + CF$UID + 6628 + + + CF$UID + 6629 + + + CF$UID + 6630 + + + CF$UID + 6631 + + + CF$UID + 6632 + + + CF$UID + 6633 + + + CF$UID + 6634 + + + CF$UID + 6635 + + + CF$UID + 6638 + + + CF$UID + 6639 + + + CF$UID + 6640 + + + CF$UID + 6641 + + + CF$UID + 6642 + + + CF$UID + 6643 + + + CF$UID + 6644 + + + CF$UID + 6645 + + + CF$UID + 6646 + + + CF$UID + 6647 + + + CF$UID + 6650 + + + CF$UID + 6651 + + + CF$UID + 6652 + + + CF$UID + 6653 + + + CF$UID + 6654 + + + CF$UID + 6657 + + + CF$UID + 6658 + + + CF$UID + 6661 + + + CF$UID + 6662 + + + CF$UID + 6663 + + + CF$UID + 6664 + + + CF$UID + 6665 + + + CF$UID + 6668 + + + CF$UID + 6669 + + + CF$UID + 6670 + + + CF$UID + 6671 + + + CF$UID + 6672 + + + CF$UID + 6673 + + + CF$UID + 6674 + + + CF$UID + 6675 + + + CF$UID + 6676 + + + CF$UID + 6677 + + + CF$UID + 6678 + + + CF$UID + 6679 + + + CF$UID + 6680 + + + CF$UID + 6681 + + + CF$UID + 6682 + + + CF$UID + 6683 + + + CF$UID + 6684 + + + CF$UID + 6685 + + + CF$UID + 6686 + + + CF$UID + 6687 + + + CF$UID + 6688 + + + CF$UID + 6689 + + + CF$UID + 6690 + + + CF$UID + 6691 + + + CF$UID + 6692 + + + CF$UID + 6693 + + + CF$UID + 6694 + + + CF$UID + 6695 + + + CF$UID + 6696 + + + CF$UID + 6697 + + + CF$UID + 6698 + + + CF$UID + 6699 + + + CF$UID + 6700 + + + CF$UID + 6701 + + + CF$UID + 6702 + + + CF$UID + 6703 + + + CF$UID + 6704 + + + CF$UID + 6705 + + + CF$UID + 6706 + + + CF$UID + 6707 + + + CF$UID + 6708 + + + CF$UID + 6709 + + + CF$UID + 6710 + + + CF$UID + 6711 + + + CF$UID + 6712 + + + CF$UID + 6715 + + + CF$UID + 6716 + + + CF$UID + 6719 + + + CF$UID + 6720 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 71 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 71 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 71 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 71 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 71 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 71 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 71 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 71 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 90 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 90 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 90 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 90 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 90 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 90 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 90 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 90 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 90 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 90 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 90 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 90 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 90 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 67 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 67 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 67 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 67 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 67 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 6547 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6548 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 81 + len + 17 + x + 7 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 6554 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6555 + + + CF$UID + 6556 + + + CF$UID + 6557 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 86 + len + 22 + x + 6 + + + $class + + CF$UID + 136 + + c + 23 + l + 86 + len + 6 + x + 1 + + + $class + + CF$UID + 136 + + c + 29 + l + 86 + len + 2 + x + 6 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 6565 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6566 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 93 + len + 1 + x + 7 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 6590 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6591 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 116 + len + 32 + x + 30 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 29 + s + + CF$UID + 6594 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6595 + + + CF$UID + 6596 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 118 + len + 11 + x + 30 + + + $class + + CF$UID + 136 + + c + 12 + l + 118 + len + 15 + x + 29 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 32 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 32 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 32 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 31 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 6 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 6636 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6637 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 157 + len + 64 + x + 3 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 6648 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6649 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 167 + len + 90 + x + 5 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 6655 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6656 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 172 + len + 46 + x + 5 + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 6659 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6660 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 174 + len + 15 + x + 5 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 6666 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6667 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 179 + len + 26 + x + 3 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 19 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 27 + s + + CF$UID + 6713 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6714 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 224 + len + 86 + x + 27 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 6717 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6718 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 226 + len + 86 + x + 1 + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 6725 + + functions + + CF$UID + 6726 + + lineCoverage + + CF$UID + 6724 + + lines + + CF$UID + 6788 + + name + + CF$UID + 6722 + + uniqueIdentifier + + CF$UID + 6723 + + + SPTDataLoader.m + 4667DDB9-E7E6-43BE-B97C-FDF428DC4CB3 + 0.98907103825136611 + /Users/dflems/Code/SPTDataLoader/SPTDataLoader/SPTDataLoader.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6727 + + + CF$UID + 6730 + + + CF$UID + 6733 + + + CF$UID + 6736 + + + CF$UID + 6739 + + + CF$UID + 6742 + + + CF$UID + 6745 + + + CF$UID + 6748 + + + CF$UID + 6751 + + + CF$UID + 6754 + + + CF$UID + 6757 + + + CF$UID + 6760 + + + CF$UID + 6764 + + + CF$UID + 6767 + + + CF$UID + 6770 + + + CF$UID + 6773 + + + CF$UID + 6776 + + + CF$UID + 6779 + + + CF$UID + 6782 + + + CF$UID + 6785 + + + + + $class + + CF$UID + 20 + + executionCount + 20 + lineCoverage + + CF$UID + 13 + + lineNumber + 47 + name + + CF$UID + 6728 + + symbolKindIdentifier + + CF$UID + 4010 + + uniqueIdentifier + + CF$UID + 6729 + + + +[SPTDataLoader dataLoaderWithRequestResponseHandlerDelegate:cancellationTokenFactory:] + 455901E3-0FE1-49F8-9A8B-F174B0C32BD6 + + $class + + CF$UID + 20 + + executionCount + 20 + lineCoverage + + CF$UID + 13 + + lineNumber + 54 + name + + CF$UID + 6731 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6732 + + + -[SPTDataLoader initWithRequestResponseHandlerDelegate:cancellationTokenFactory:] + C63EB22E-86C5-4F11-8204-17E2E3D6D279 + + $class + + CF$UID + 20 + + executionCount + 8 + lineCoverage + + CF$UID + 13 + + lineNumber + 68 + name + + CF$UID + 6734 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6735 + + + -[SPTDataLoader executeDelegateBlock:] + F69CE39C-C7BE-43A7-ADB1-86DD96089AA9 + + $class + + CF$UID + 20 + + executionCount + 17 + lineCoverage + + CF$UID + 13 + + lineNumber + 79 + name + + CF$UID + 6737 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6738 + + + -[SPTDataLoader performRequest:] + 6CAB5FE4-BDE1-448A-A33B-E607B773FEBC + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 115 + name + + CF$UID + 6740 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6741 + + + -[SPTDataLoader cancelAllLoads] + 712072C9-6D56-4021-A44E-FF44296AC1CF + + $class + + CF$UID + 20 + + executionCount + 13 + lineCoverage + + CF$UID + 13 + + lineNumber + 125 + name + + CF$UID + 6743 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6744 + + + -[SPTDataLoader isRequestExpected:] + 590F0022-5E95-444B-B9B0-F55937B4BC90 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 139 + name + + CF$UID + 6746 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6747 + + + -[SPTDataLoader dealloc] + B3EB781C-F2DB-4FC1-8BA1-C9BDB2EA4752 + + $class + + CF$UID + 20 + + executionCount + 3 + lineCoverage + + CF$UID + 13 + + lineNumber + 148 + name + + CF$UID + 6749 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6750 + + + -[SPTDataLoader successfulResponse:] + A216383C-5DD2-4907-9B5B-1EADC4786946 + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 153 + name + + CF$UID + 6752 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 6753 + + + __36-[SPTDataLoader successfulResponse:]_block_invoke + 5930070C-8025-4421-A4A6-17CBA990C6D0 + + $class + + CF$UID + 20 + + executionCount + 3 + lineCoverage + + CF$UID + 13 + + lineNumber + 162 + name + + CF$UID + 6755 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6756 + + + -[SPTDataLoader failedResponse:] + 7C55CA43-6D5F-4A27-974A-662E4F22A321 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 167 + name + + CF$UID + 6758 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 6759 + + + __32-[SPTDataLoader failedResponse:]_block_invoke + 34CC1547-B5BA-4216-91C4-1F15A530FB87 + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 6763 + + lineNumber + 176 + name + + CF$UID + 6761 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6762 + + + -[SPTDataLoader cancelledRequest:] + 7C242011-66D0-457E-91BD-E2A77E50AC50 + 0.8571428571428571 + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 182 + name + + CF$UID + 6765 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 6766 + + + __34-[SPTDataLoader cancelledRequest:]_block_invoke + 2743FF13-8769-4B60-9574-C9EC1CEFB5F6 + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 192 + name + + CF$UID + 6768 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6769 + + + -[SPTDataLoader receivedDataChunk:forResponse:] + 914DC104-F00B-4AA8-B22B-3E49AF2F82FB + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 202 + name + + CF$UID + 6771 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 6772 + + + __47-[SPTDataLoader receivedDataChunk:forResponse:]_block_invoke + 036AAB2A-2781-4DEF-9F2C-1BB67E5BBCF9 + + $class + + CF$UID + 20 + + executionCount + 3 + lineCoverage + + CF$UID + 13 + + lineNumber + 209 + name + + CF$UID + 6774 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6775 + + + -[SPTDataLoader receivedInitialResponse:] + DED3D2C4-A275-45BD-B408-46CF0DCC9C38 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 220 + name + + CF$UID + 6777 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 6778 + + + __41-[SPTDataLoader receivedInitialResponse:]_block_invoke + 0B667E43-96C5-43CC-B2FD-5613A75926B1 + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 228 + name + + CF$UID + 6780 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6781 + + + -[SPTDataLoader needsNewBodyStream:forRequest:] + 2CB04E53-0923-4D80-BFC3-6B4393FE618C + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 230 + name + + CF$UID + 6783 + + symbolKindIdentifier + + CF$UID + 211 + + uniqueIdentifier + + CF$UID + 6784 + + + __47-[SPTDataLoader needsNewBodyStream:forRequest:]_block_invoke + 5C34AB19-CF4F-4087-8C00-C7E231E420BD + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 243 + name + + CF$UID + 6786 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 6787 + + + -[SPTDataLoader cancellationTokenDidCancel:] + FD75AB32-76C9-4797-A4FE-2E4329C88DE1 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6789 + + + CF$UID + 6790 + + + CF$UID + 6791 + + + CF$UID + 6792 + + + CF$UID + 6793 + + + CF$UID + 6794 + + + CF$UID + 6795 + + + CF$UID + 6796 + + + CF$UID + 6797 + + + CF$UID + 6798 + + + CF$UID + 6799 + + + CF$UID + 6800 + + + CF$UID + 6801 + + + CF$UID + 6802 + + + CF$UID + 6803 + + + CF$UID + 6804 + + + CF$UID + 6805 + + + CF$UID + 6806 + + + CF$UID + 6807 + + + CF$UID + 6808 + + + CF$UID + 6809 + + + CF$UID + 6810 + + + CF$UID + 6811 + + + CF$UID + 6812 + + + CF$UID + 6813 + + + CF$UID + 6814 + + + CF$UID + 6815 + + + CF$UID + 6816 + + + CF$UID + 6817 + + + CF$UID + 6818 + + + CF$UID + 6819 + + + CF$UID + 6820 + + + CF$UID + 6821 + + + CF$UID + 6822 + + + CF$UID + 6823 + + + CF$UID + 6824 + + + CF$UID + 6825 + + + CF$UID + 6826 + + + CF$UID + 6827 + + + CF$UID + 6828 + + + CF$UID + 6829 + + + CF$UID + 6830 + + + CF$UID + 6831 + + + CF$UID + 6832 + + + CF$UID + 6833 + + + CF$UID + 6834 + + + CF$UID + 6835 + + + CF$UID + 6836 + + + CF$UID + 6837 + + + CF$UID + 6838 + + + CF$UID + 6839 + + + CF$UID + 6840 + + + CF$UID + 6841 + + + CF$UID + 6842 + + + CF$UID + 6843 + + + CF$UID + 6844 + + + CF$UID + 6845 + + + CF$UID + 6846 + + + CF$UID + 6847 + + + CF$UID + 6848 + + + CF$UID + 6849 + + + CF$UID + 6850 + + + CF$UID + 6851 + + + CF$UID + 6852 + + + CF$UID + 6853 + + + CF$UID + 6854 + + + CF$UID + 6855 + + + CF$UID + 6856 + + + CF$UID + 6857 + + + CF$UID + 6862 + + + CF$UID + 6863 + + + CF$UID + 6866 + + + CF$UID + 6867 + + + CF$UID + 6868 + + + CF$UID + 6869 + + + CF$UID + 6870 + + + CF$UID + 6871 + + + CF$UID + 6872 + + + CF$UID + 6873 + + + CF$UID + 6874 + + + CF$UID + 6875 + + + CF$UID + 6876 + + + CF$UID + 6877 + + + CF$UID + 6878 + + + CF$UID + 6879 + + + CF$UID + 6880 + + + CF$UID + 6881 + + + CF$UID + 6882 + + + CF$UID + 6887 + + + CF$UID + 6888 + + + CF$UID + 6889 + + + CF$UID + 6890 + + + CF$UID + 6891 + + + CF$UID + 6892 + + + CF$UID + 6893 + + + CF$UID + 6894 + + + CF$UID + 6895 + + + CF$UID + 6896 + + + CF$UID + 6897 + + + CF$UID + 6898 + + + CF$UID + 6899 + + + CF$UID + 6900 + + + CF$UID + 6901 + + + CF$UID + 6902 + + + CF$UID + 6903 + + + CF$UID + 6904 + + + CF$UID + 6905 + + + CF$UID + 6906 + + + CF$UID + 6907 + + + CF$UID + 6908 + + + CF$UID + 6909 + + + CF$UID + 6910 + + + CF$UID + 6911 + + + CF$UID + 6912 + + + CF$UID + 6913 + + + CF$UID + 6914 + + + CF$UID + 6915 + + + CF$UID + 6916 + + + CF$UID + 6917 + + + CF$UID + 6918 + + + CF$UID + 6919 + + + CF$UID + 6920 + + + CF$UID + 6921 + + + CF$UID + 6922 + + + CF$UID + 6923 + + + CF$UID + 6924 + + + CF$UID + 6925 + + + CF$UID + 6926 + + + CF$UID + 6927 + + + CF$UID + 6928 + + + CF$UID + 6929 + + + CF$UID + 6930 + + + CF$UID + 6931 + + + CF$UID + 6934 + + + CF$UID + 6935 + + + CF$UID + 6936 + + + CF$UID + 6937 + + + CF$UID + 6938 + + + CF$UID + 6939 + + + CF$UID + 6940 + + + CF$UID + 6941 + + + CF$UID + 6942 + + + CF$UID + 6943 + + + CF$UID + 6944 + + + CF$UID + 6945 + + + CF$UID + 6946 + + + CF$UID + 6947 + + + CF$UID + 6948 + + + CF$UID + 6949 + + + CF$UID + 6952 + + + CF$UID + 6953 + + + CF$UID + 6954 + + + CF$UID + 6955 + + + CF$UID + 6956 + + + CF$UID + 6957 + + + CF$UID + 6958 + + + CF$UID + 6959 + + + CF$UID + 6960 + + + CF$UID + 6961 + + + CF$UID + 6964 + + + CF$UID + 6965 + + + CF$UID + 6966 + + + CF$UID + 6967 + + + CF$UID + 6970 + + + CF$UID + 6971 + + + CF$UID + 6972 + + + CF$UID + 6973 + + + CF$UID + 6974 + + + CF$UID + 6975 + + + CF$UID + 6976 + + + CF$UID + 6977 + + + CF$UID + 6978 + + + CF$UID + 6979 + + + CF$UID + 6982 + + + CF$UID + 6983 + + + CF$UID + 6984 + + + CF$UID + 6985 + + + CF$UID + 6988 + + + CF$UID + 6989 + + + CF$UID + 6990 + + + CF$UID + 6991 + + + CF$UID + 6992 + + + CF$UID + 6993 + + + CF$UID + 6994 + + + CF$UID + 6995 + + + CF$UID + 6996 + + + CF$UID + 6997 + + + CF$UID + 6998 + + + CF$UID + 6999 + + + CF$UID + 7002 + + + CF$UID + 7003 + + + CF$UID + 7004 + + + CF$UID + 7005 + + + CF$UID + 7008 + + + CF$UID + 7009 + + + CF$UID + 7010 + + + CF$UID + 7011 + + + CF$UID + 7012 + + + CF$UID + 7013 + + + CF$UID + 7014 + + + CF$UID + 7015 + + + CF$UID + 7016 + + + CF$UID + 7017 + + + CF$UID + 7018 + + + CF$UID + 7019 + + + CF$UID + 7020 + + + CF$UID + 7023 + + + CF$UID + 7024 + + + CF$UID + 7025 + + + CF$UID + 7026 + + + CF$UID + 7029 + + + CF$UID + 7030 + + + CF$UID + 7031 + + + CF$UID + 7032 + + + CF$UID + 7033 + + + CF$UID + 7036 + + + CF$UID + 7037 + + + CF$UID + 7038 + + + CF$UID + 7039 + + + CF$UID + 7040 + + + CF$UID + 7041 + + + CF$UID + 7042 + + + CF$UID + 7043 + + + CF$UID + 7044 + + + CF$UID + 7047 + + + CF$UID + 7048 + + + CF$UID + 7049 + + + CF$UID + 7050 + + + CF$UID + 7051 + + + CF$UID + 7054 + + + CF$UID + 7055 + + + CF$UID + 7056 + + + CF$UID + 7057 + + + CF$UID + 7058 + + + CF$UID + 7059 + + + CF$UID + 7062 + + + CF$UID + 7063 + + + CF$UID + 7064 + + + CF$UID + 7065 + + + CF$UID + 7066 + + + CF$UID + 7067 + + + CF$UID + 7068 + + + CF$UID + 7069 + + + CF$UID + 7070 + + + CF$UID + 7071 + + + CF$UID + 7072 + + + CF$UID + 7073 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 20 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 6858 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6859 + + + CF$UID + 6860 + + + CF$UID + 6861 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 69 + len + 59 + x + 8 + + + $class + + CF$UID + 136 + + c + 60 + l + 69 + len + 23 + x + 7 + + + $class + + CF$UID + 136 + + c + 83 + l + 69 + len + 2 + x + 8 + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 6864 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6865 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 71 + len + 11 + x + 8 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 6883 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6884 + + + CF$UID + 6885 + + + CF$UID + 6886 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 88 + len + 28 + x + 17 + + + $class + + CF$UID + 136 + + c + 29 + l + 88 + len + 20 + x + 15 + + + $class + + CF$UID + 136 + + c + 49 + l + 88 + len + 2 + x + 17 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 16 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 17 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 13 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 13 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 13 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 6932 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6933 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 133 + len + 13 + x + 5 + + + $class + + CF$UID + 24 + + c + 13 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 6950 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6951 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 149 + len + 52 + x + 3 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 6962 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6963 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 159 + len + 1 + x + 3 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 6968 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6969 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 163 + len + 52 + x + 3 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 6980 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6981 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 173 + len + 1 + x + 3 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 6986 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 6987 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 177 + len + 43 + x + 2 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 7000 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7001 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 189 + len + 1 + x + 2 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 7006 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7007 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 193 + len + 52 + x + 2 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 7021 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7022 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 206 + len + 1 + x + 2 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 7027 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7028 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 210 + len + 52 + x + 3 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 3 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 7034 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7035 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 215 + len + 34 + x + 2 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 7045 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7046 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 224 + len + 1 + x + 3 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 7052 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7053 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 229 + len + 97 + x + 2 + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 7060 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7061 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 235 + len + 11 + x + 2 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 7077 + + functions + + CF$UID + 7078 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 7091 + + name + + CF$UID + 7075 + + uniqueIdentifier + + CF$UID + 7076 + + + SPTDataLoaderResolverAddress.m + E555A13F-24C6-4606-A086-7AC7A6AA2CF5 + /Users/dflems/Code/SPTDataLoader/SPTDataLoader/SPTDataLoaderResolverAddress.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7079 + + + CF$UID + 7082 + + + CF$UID + 7085 + + + CF$UID + 7088 + + + + + $class + + CF$UID + 20 + + executionCount + 8 + lineCoverage + + CF$UID + 13 + + lineNumber + 37 + name + + CF$UID + 7080 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 7081 + + + -[SPTDataLoaderResolverAddress isReachable] + 90516970-953E-45E4-88DA-F7D72D89C6B5 + + $class + + CF$UID + 20 + + executionCount + 8 + lineCoverage + + CF$UID + 13 + + lineNumber + 48 + name + + CF$UID + 7083 + + symbolKindIdentifier + + CF$UID + 4010 + + uniqueIdentifier + + CF$UID + 7084 + + + +[SPTDataLoaderResolverAddress dataLoaderResolverAddressWithAddress:] + 9546340F-5FDD-4113-8777-C3CD948D5018 + + $class + + CF$UID + 20 + + executionCount + 8 + lineCoverage + + CF$UID + 13 + + lineNumber + 53 + name + + CF$UID + 7086 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 7087 + + + -[SPTDataLoaderResolverAddress initWithAddress:] + 08CD39C4-1782-4E50-9BF4-95EC80933703 + + $class + + CF$UID + 20 + + executionCount + 2 + lineCoverage + + CF$UID + 13 + + lineNumber + 66 + name + + CF$UID + 7089 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 7090 + + + -[SPTDataLoaderResolverAddress failedToReach] + 3CDD8B83-146E-4071-A3BB-5EC43BDE3054 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7092 + + + CF$UID + 7093 + + + CF$UID + 7094 + + + CF$UID + 7095 + + + CF$UID + 7096 + + + CF$UID + 7097 + + + CF$UID + 7098 + + + CF$UID + 7099 + + + CF$UID + 7100 + + + CF$UID + 7101 + + + CF$UID + 7102 + + + CF$UID + 7103 + + + CF$UID + 7104 + + + CF$UID + 7105 + + + CF$UID + 7106 + + + CF$UID + 7107 + + + CF$UID + 7108 + + + CF$UID + 7109 + + + CF$UID + 7110 + + + CF$UID + 7111 + + + CF$UID + 7112 + + + CF$UID + 7113 + + + CF$UID + 7114 + + + CF$UID + 7115 + + + CF$UID + 7116 + + + CF$UID + 7117 + + + CF$UID + 7118 + + + CF$UID + 7119 + + + CF$UID + 7120 + + + CF$UID + 7121 + + + CF$UID + 7122 + + + CF$UID + 7123 + + + CF$UID + 7124 + + + CF$UID + 7125 + + + CF$UID + 7126 + + + CF$UID + 7127 + + + CF$UID + 7128 + + + CF$UID + 7129 + + + CF$UID + 7130 + + + CF$UID + 7131 + + + CF$UID + 7134 + + + CF$UID + 7135 + + + CF$UID + 7136 + + + CF$UID + 7137 + + + CF$UID + 7140 + + + CF$UID + 7141 + + + CF$UID + 7142 + + + CF$UID + 7143 + + + CF$UID + 7144 + + + CF$UID + 7145 + + + CF$UID + 7146 + + + CF$UID + 7147 + + + CF$UID + 7148 + + + CF$UID + 7149 + + + CF$UID + 7150 + + + CF$UID + 7151 + + + CF$UID + 7152 + + + CF$UID + 7153 + + + CF$UID + 7154 + + + CF$UID + 7155 + + + CF$UID + 7156 + + + CF$UID + 7157 + + + CF$UID + 7158 + + + CF$UID + 7159 + + + CF$UID + 7160 + + + CF$UID + 7161 + + + CF$UID + 7162 + + + CF$UID + 7163 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 7132 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7133 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 40 + len + 25 + x + 8 + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 7 + s + + CF$UID + 7138 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7139 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 44 + len + 39 + x + 7 + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 8 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 2 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 7168 + + functions + + CF$UID + 7169 + + lineCoverage + + CF$UID + 7167 + + lines + + CF$UID + 7186 + + name + + CF$UID + 7165 + + uniqueIdentifier + + CF$UID + 7166 + + + SPTDataLoaderResolver.m + 0C0A03C7-52A1-4EF2-9F56-2C1491611D98 + 0.96078431372549022 + /Users/dflems/Code/SPTDataLoader/SPTDataLoader/SPTDataLoaderResolver.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7170 + + + CF$UID + 7173 + + + CF$UID + 7176 + + + CF$UID + 7179 + + + CF$UID + 7182 + + + + + $class + + CF$UID + 20 + + executionCount + 34 + lineCoverage + + CF$UID + 13 + + lineNumber + 37 + name + + CF$UID + 7171 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 7172 + + + -[SPTDataLoaderResolver addressForHost:] + EF2B298B-F477-436F-8734-F40C8AC67F50 + + $class + + CF$UID + 20 + + executionCount + 4 + lineCoverage + + CF$UID + 13 + + lineNumber + 49 + name + + CF$UID + 7174 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 7175 + + + -[SPTDataLoaderResolver setAddresses:forHost:] + E24633C5-E7C8-41B7-B941-BC230418E3B1 + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 66 + name + + CF$UID + 7177 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 7178 + + + -[SPTDataLoaderResolver markAddressAsUnreachable:] + 5B5509C0-119A-48A5-9CB1-D95AF4B7E003 + + $class + + CF$UID + 20 + + executionCount + 5 + lineCoverage + + CF$UID + 13 + + lineNumber + 72 + name + + CF$UID + 7180 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 7181 + + + -[SPTDataLoaderResolver resolverAddressForAddress:] + 4C9ECE11-3E43-4CBC-955D-F4E1AEC77FC9 + + $class + + CF$UID + 20 + + executionCount + 36 + lineCoverage + + CF$UID + 7185 + + lineNumber + 88 + name + + CF$UID + 7183 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 7184 + + + -[SPTDataLoaderResolver init] + F06B2604-B411-42DF-A61A-FA6F3A6A2199 + 0.80000000000000004 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7187 + + + CF$UID + 7188 + + + CF$UID + 7189 + + + CF$UID + 7190 + + + CF$UID + 7191 + + + CF$UID + 7192 + + + CF$UID + 7193 + + + CF$UID + 7194 + + + CF$UID + 7195 + + + CF$UID + 7196 + + + CF$UID + 7197 + + + CF$UID + 7198 + + + CF$UID + 7199 + + + CF$UID + 7200 + + + CF$UID + 7201 + + + CF$UID + 7202 + + + CF$UID + 7203 + + + CF$UID + 7204 + + + CF$UID + 7205 + + + CF$UID + 7206 + + + CF$UID + 7207 + + + CF$UID + 7208 + + + CF$UID + 7209 + + + CF$UID + 7210 + + + CF$UID + 7211 + + + CF$UID + 7212 + + + CF$UID + 7213 + + + CF$UID + 7214 + + + CF$UID + 7215 + + + CF$UID + 7216 + + + CF$UID + 7217 + + + CF$UID + 7218 + + + CF$UID + 7219 + + + CF$UID + 7220 + + + CF$UID + 7221 + + + CF$UID + 7222 + + + CF$UID + 7223 + + + CF$UID + 7224 + + + CF$UID + 7225 + + + CF$UID + 7226 + + + CF$UID + 7229 + + + CF$UID + 7230 + + + CF$UID + 7231 + + + CF$UID + 7232 + + + CF$UID + 7233 + + + CF$UID + 7236 + + + CF$UID + 7237 + + + CF$UID + 7238 + + + CF$UID + 7239 + + + CF$UID + 7240 + + + CF$UID + 7241 + + + CF$UID + 7242 + + + CF$UID + 7243 + + + CF$UID + 7244 + + + CF$UID + 7245 + + + CF$UID + 7246 + + + CF$UID + 7247 + + + CF$UID + 7248 + + + CF$UID + 7249 + + + CF$UID + 7250 + + + CF$UID + 7251 + + + CF$UID + 7252 + + + CF$UID + 7253 + + + CF$UID + 7254 + + + CF$UID + 7255 + + + CF$UID + 7256 + + + CF$UID + 7257 + + + CF$UID + 7258 + + + CF$UID + 7259 + + + CF$UID + 7260 + + + CF$UID + 7261 + + + CF$UID + 7262 + + + CF$UID + 7263 + + + CF$UID + 7264 + + + CF$UID + 7265 + + + CF$UID + 7266 + + + CF$UID + 7267 + + + CF$UID + 7268 + + + CF$UID + 7269 + + + CF$UID + 7270 + + + CF$UID + 7271 + + + CF$UID + 7272 + + + CF$UID + 7275 + + + CF$UID + 7276 + + + CF$UID + 7277 + + + CF$UID + 7278 + + + CF$UID + 7279 + + + CF$UID + 7280 + + + CF$UID + 7281 + + + CF$UID + 7284 + + + CF$UID + 7285 + + + CF$UID + 7286 + + + CF$UID + 7287 + + + CF$UID + 7288 + + + CF$UID + 7289 + + + CF$UID + 7290 + + + CF$UID + 7291 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 34 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 34 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 7227 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7228 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 40 + len + 35 + x + 5 + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 34 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 30 + s + + CF$UID + 7234 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7235 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 45 + len + 15 + x + 30 + + + $class + + CF$UID + 24 + + c + 34 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 4 + s + + CF$UID + 7273 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7274 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 82 + len + 14 + x + 4 + + + $class + + CF$UID + 24 + + c + 5 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 36 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 36 + s + + CF$UID + 7282 + + x + + + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7283 + + + + + $class + + CF$UID + 136 + + c + 1 + l + 89 + len + 32 + x + 36 + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 36 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 36 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 36 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 36 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 36 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 36 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 52 + + documentLocation + + CF$UID + 7295 + + functions + + CF$UID + 7296 + + lineCoverage + + CF$UID + 13 + + lines + + CF$UID + 7300 + + name + + CF$UID + 7293 + + uniqueIdentifier + + CF$UID + 7294 + + + SPTDataLoaderCancellationTokenFactoryImplementation.m + AFE618E3-3E47-4CBE-943E-826A357F815E + /Users/dflems/Code/SPTDataLoader/SPTDataLoader/SPTDataLoaderCancellationTokenFactoryImplementation.m + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7297 + + + + + $class + + CF$UID + 20 + + executionCount + 1 + lineCoverage + + CF$UID + 13 + + lineNumber + 33 + name + + CF$UID + 7298 + + symbolKindIdentifier + + CF$UID + 19 + + uniqueIdentifier + + CF$UID + 7299 + + + -[SPTDataLoaderCancellationTokenFactoryImplementation createCancellationTokenWithDelegate:cancelObject:] + 560B9BF6-3244-4EC2-8339-E5E9F2EF4149 + + $class + + CF$UID + 21 + + NS.objects + + + CF$UID + 7301 + + + CF$UID + 7302 + + + CF$UID + 7303 + + + CF$UID + 7304 + + + CF$UID + 7305 + + + CF$UID + 7306 + + + CF$UID + 7307 + + + CF$UID + 7308 + + + CF$UID + 7309 + + + CF$UID + 7310 + + + CF$UID + 7311 + + + CF$UID + 7312 + + + CF$UID + 7313 + + + CF$UID + 7314 + + + CF$UID + 7315 + + + CF$UID + 7316 + + + CF$UID + 7317 + + + CF$UID + 7318 + + + CF$UID + 7319 + + + CF$UID + 7320 + + + CF$UID + 7321 + + + CF$UID + 7322 + + + CF$UID + 7323 + + + CF$UID + 7324 + + + CF$UID + 7325 + + + CF$UID + 7326 + + + CF$UID + 7327 + + + CF$UID + 7328 + + + CF$UID + 7329 + + + CF$UID + 7330 + + + CF$UID + 7331 + + + CF$UID + 7332 + + + CF$UID + 7333 + + + CF$UID + 7334 + + + CF$UID + 7335 + + + CF$UID + 7336 + + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 0 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $class + + CF$UID + 24 + + c + 1 + s + + CF$UID + 0 + + x + + + + $classes + + IDESchemeActionCodeCoverageTarget + DVTCoverageDataContainer + NSObject + + $classname + IDESchemeActionCodeCoverageTarget + + + $classes + + IDESchemeActionCodeCoverage + NSObject + + $classname + IDESchemeActionCodeCoverage + + + $top + + root + + CF$UID + 1 + + + $version + 100000 + + diff --git a/apps/worker/services/report/languages/tests/unit/xcodeplist.txt b/apps/worker/services/report/languages/tests/unit/xcodeplist.txt new file mode 100644 index 0000000000..adf5e394cf --- /dev/null +++ b/apps/worker/services/report/languages/tests/unit/xcodeplist.txt @@ -0,0 +1,5616 @@ +{} +<<<<< end_of_header >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] + + +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +["1/1","b",[[0,"1/1",null,[[1,35,5]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[5,null,[[0,5]]] + + +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] + + +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] + + +[11,null,[[0,11]]] +[11,null,[[0,11]]] +[11,null,[[0,11]]] +[11,null,[[0,11]]] + + +[12,null,[[0,12]]] +[12,null,[[0,12]]] +[12,null,[[0,12]]] + + +[0,null,[[0,0]]] +[0,null,[[0,0]]] + + + +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +["2/2","b",[[0,"2/2",null,[[1,47,6],[49,52,5]],null]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[23,null,[[0,23]]] +["1/1","b",[[0,"1/1",null,[[1,33,23]],null]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[23,null,[[0,23]]] +[23,null,[[0,23]]] +[23,null,[[0,23]]] +[23,null,[[0,23]]] +[23,null,[[0,23]]] + + +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] + + +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] + + +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +["2/2","b",[[0,"2/2",null,[[1,26,4],[28,31,3]],null]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +["2/2","b",[[0,"2/2",null,[[1,27,11],[29,32,10]],null]]] +[10,null,[[0,10]]] +[10,null,[[0,10]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +["2/2","b",[[0,"2/2",null,[[1,28,101],[30,33,100]],null]]] +[100,null,[[0,100]]] +[100,null,[[0,100]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +["1/1","b",[[0,"1/1",null,[[1,36,2]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + +[23,null,[[0,23]]] +["1/1","b",[[0,"1/1",null,[[1,76,23]],null]]] +[2,null,[[0,2]]] +["1/1","b",[[0,"1/1",null,[[1,12,23]],null]]] +[21,null,[[0,21]]] +[21,null,[[0,21]]] +[23,null,[[0,23]]] + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +["1/1","b",[[0,"1/1",null,[[1,39,2]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] + + +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[32,null,[[0,32]]] +[32,null,[[0,32]]] +[32,null,[[0,32]]] +[32,null,[[0,32]]] +[32,null,[[0,32]]] +[32,null,[[0,32]]] +[32,null,[[0,32]]] +[32,null,[[0,32]]] +[32,null,[[0,32]]] +[32,null,[[0,32]]] +[32,null,[[0,32]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +["1/1","b",[[0,"1/1",null,[[1,78,1]],null]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +["1/1","b",[[0,"1/1",null,[[1,78,1]],null]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[12,null,[[0,12]]] +[12,null,[[0,12]]] +[12,null,[[0,12]]] +["1/1","b",[[0,"1/1",null,[[1,32,11]],null]]] +[2,null,[[0,2]]] +["1/1","b",[[0,"1/1",null,[[1,16,11]],null]]] +[11,null,[[0,11]]] +[11,null,[[0,11]]] +[12,null,[[0,12]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +["2/2","b",[[0,"2/2",null,[[1,48,11],[50,53,10]],null]]] +[11,null,[[0,11]]] +[11,null,[[0,11]]] +[11,null,[[0,11]]] +[11,null,[[0,11]]] +[11,null,[[0,11]]] +[11,null,[[0,11]]] +[11,null,[[0,11]]] +["1/1","b",[[0,"1/1",null,[[1,45,11]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[11,null,[[0,11]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +["1/1","b",[[0,"1/1",null,[[1,32,3]],null]]] +[1,null,[[0,1]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +["2/2","b",[[0,"2/2",null,[[1,44,4],[46,49,3]],null]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[3,null,[[0,3]]] +["1/2","b",[[0,"1/2",null,[[1,54,2],[54,55,0]],null]]] +[3,null,[[0,3]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[18,null,[[0,18]]] +[18,null,[[0,18]]] +["1/1","b",[[0,"1/1",null,[[1,24,18]],null]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[18,null,[[0,18]]] +[18,null,[[0,18]]] +[18,null,[[0,18]]] +[18,null,[[0,18]]] +[18,null,[[0,18]]] +["1/1","b",[[0,"1/1",null,[[1,25,7]],null]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] + +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] + +[6,null,[[0,6]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] + +[25,null,[[0,25]]] +[25,null,[[0,25]]] +[25,null,[[0,25]]] + +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] + +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] + + + + + + +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[1,null,[[0,1]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + +[26,null,[[0,26]]] +[26,null,[[0,26]]] +[26,null,[[0,26]]] + + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + +[5,null,[[0,5]]] +[5,null,[[0,5]]] +["1/1","b",[[0,"1/1",null,[[1,30,5]],null]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[5,null,[[0,5]]] + + +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[16,null,[[0,16]]] +["1/1","b",[[0,"1/1",null,[[1,137,11]],null]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[18,null,[[0,18]]] +[18,null,[[0,18]]] +[18,null,[[0,18]]] +[18,null,[[0,18]]] +[18,null,[[0,18]]] +[18,null,[[0,18]]] +[18,null,[[0,18]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[15,null,[[0,15]]] +[15,null,[[0,15]]] +[15,null,[[0,15]]] +[15,null,[[0,15]]] +[15,null,[[0,15]]] +[15,null,[[0,15]]] +[15,null,[[0,15]]] +[15,null,[[0,15]]] +[15,null,[[0,15]]] +[15,null,[[0,15]]] +[15,null,[[0,15]]] +[15,null,[[0,15]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +["1/1","b",[[0,"1/1",null,[[1,96,3]],null]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[4,null,[[0,4]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[5,null,[[0,5]]] +["1/1","b",[[0,"1/1",null,[[1,115,2]],null]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] + + + + + + + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] + + +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] + + +[5,null,[[0,5]]] +["1/1","b",[[0,"1/1",null,[[1,27,5]],null]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[5,null,[[0,5]]] + + + + +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] + + +[5,null,[[0,5]]] +["1/1","b",[[0,"1/1",null,[[1,16,5]],null]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +["1/1","b",[[0,"1/1",null,[[1,27,5]],null]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] + + +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] + + + + + + + + +[10,null,[[0,10]]] +["1/1","b",[[0,"1/1",null,[[1,25,10]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[10,null,[[0,10]]] +[9,null,[[0,9]]] +[9,null,[[0,9]]] +[9,null,[[0,9]]] +["1/1","b",[[0,"1/1",null,[[1,2,10]],null]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[34,null,[[0,34]]] +[34,null,[[0,34]]] +[34,null,[[0,34]]] + + + + +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] + + + + + +[35,null,[[0,35]]] +["1/1","b",[[0,"1/1",null,[[1,33,35]],null]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[115,null,[[0,115]]] +[115,null,[[0,115]]] +[115,null,[[0,115]]] +["1/1","b",[[0,"1/1",null,[[1,34,115]],null]]] +[99,null,[[0,99]]] +[99,null,[[0,99]]] +[115,null,[[0,115]]] +["1/1","b",[[0,"1/1",null,[[1,31,115]],null]]] +[10,null,[[0,10]]] +["1/1","b",[[0,"1/1",null,[[1,12,115]],null]]] +[105,null,[[0,105]]] +[105,null,[[0,105]]] +[105,null,[[0,105]]] +[115,null,[[0,115]]] +["1/1","b",[[0,"1/1",null,[[1,43,115]],null]]] +[54,null,[[0,54]]] +[54,null,[[0,54]]] +[115,null,[[0,115]]] +[115,null,[[0,115]]] +[115,null,[[0,115]]] + + +[115,null,[[0,115]]] +[115,null,[[0,115]]] +[115,null,[[0,115]]] +[115,null,[[0,115]]] +[115,null,[[0,115]]] +[115,null,[[0,115]]] + + + + + + +[288,null,[[0,288]]] +[288,null,[[0,288]]] +[288,null,[[0,288]]] + + +[105,null,[[0,105]]] +[105,null,[[0,105]]] +[105,null,[[0,105]]] +[105,null,[[0,105]]] +[105,null,[[0,105]]] +[105,null,[[0,105]]] +[105,null,[[0,105]]] +[105,null,[[0,105]]] +[105,null,[[0,105]]] +["2/2","b",[[0,"2/2",null,[[1,33,144],[35,38,39]],null]]] +[144,null,[[0,144]]] +[144,null,[[0,144]]] +[144,null,[[0,144]]] +[144,null,[[0,144]]] +[144,null,[[0,144]]] +["1/1","b",[[0,"1/1",null,[[1,33,144]],null]]] +[105,null,[[0,105]]] +[105,null,[[0,105]]] +[144,null,[[0,144]]] +[105,null,[[0,105]]] +["0/1","b",[[0,"0/1",null,[[1,66,0]],null]]] +[105,null,[[0,105]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] + + + + + +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[65,null,[[0,65]]] +[65,null,[[0,65]]] +[65,null,[[0,65]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] + + +[4,null,[[0,4]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +["1/1","b",[[0,"1/1",null,[[1,34,4]],null]]] +[6,null,[[0,6]]] +["1/1","b",[[0,"1/1",null,[[1,16,4]],null]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +[8,null,[[0,8]]] +[4,null,[[0,4]]] + + +[18,null,[[0,18]]] +[18,null,[[0,18]]] +["1/1","b",[[0,"1/1",null,[[1,25,18]],null]]] +[9,null,[[0,9]]] +[9,null,[[0,9]]] +[18,null,[[0,18]]] +["2/2","b",[[0,"2/2",null,[[1,60,18],[60,93,10]],null]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[18,null,[[0,18]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +["1/1","b",[[0,"1/1",null,[[1,16,14]],null]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +["1/1","b",[[0,"1/1",null,[[1,35,14]],null]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[14,null,[[0,14]]] +["1/1","b",[[0,"1/1",null,[[1,30,14]],null]]] +[7,null,[[0,7]]] +["1/1","b",[[0,"1/1",null,[[1,70,7]],null]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +[7,null,[[0,7]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[7,null,[[0,7]]] +[14,null,[[0,14]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +["1/1","b",[[0,"1/1",null,[[1,2,18]],null]]] + + +[10,null,[[0,10]]] +[10,null,[[0,10]]] +[10,null,[[0,10]]] +[10,null,[[0,10]]] +["1/1","b",[[0,"1/1",null,[[1,61,10]],null]]] +[3,null,[[0,3]]] +["1/1","b",[[0,"1/1",null,[[1,53,3]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[3,null,[[0,3]]] +[10,null,[[0,10]]] +["1/1","b",[[0,"1/1",null,[[1,29,10]],null]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[10,null,[[0,10]]] +[10,null,[[0,10]]] +[10,null,[[0,10]]] + + +[15,null,[[0,15]]] +[15,null,[[0,15]]] +["1/1","b",[[0,"1/1",null,[[1,77,15]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[15,null,[[0,15]]] +["1/1","b",[[0,"1/1",null,[[1,15,14]],null]]] +[15,null,[[0,15]]] + + +[25,null,[[0,25]]] +[25,null,[[0,25]]] +[25,null,[[0,25]]] +[25,null,[[0,25]]] + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] + + +[34,null,[[0,34]]] +[34,null,[[0,34]]] +["1/1","b",[[0,"1/1",null,[[1,26,34]],null]]] +[28,null,[[0,28]]] +["1/1","b",[[0,"1/1",null,[[1,12,34]],null]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +[34,null,[[0,34]]] + + +[28,null,[[0,28]]] +["1/1","b",[[0,"1/1",null,[[1,43,28]],null]]] +[5,null,[[0,5]]] +["1/1","b",[[0,"1/1",null,[[1,34,5]],null]]] +[3,null,[[0,3]]] +["1/1","b",[[0,"1/1",null,[[1,16,5]],null]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[28,null,[[0,28]]] +[23,null,[[0,23]]] +[23,null,[[0,23]]] +["1/1","b",[[0,"1/1",null,[[1,2,28]],null]]] + + +[5,null,[[0,5]]] +[5,null,[[0,5]]] +["4/4","b",[[0,"4/4",null,[[1,57,5],[57,83,4],[83,87,5],[87,117,4]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[5,null,[[0,5]]] + + + + +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +["1/1","b",[[0,"1/1",null,[[1,52,4]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[4,null,[[0,4]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +["1/1","b",[[0,"1/1",null,[[1,55,3]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[3,null,[[0,3]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[58,null,[[0,58]]] +[58,null,[[0,58]]] +[58,null,[[0,58]]] + + +[58,null,[[0,58]]] +[58,null,[[0,58]]] +[58,null,[[0,58]]] +[58,null,[[0,58]]] +[58,null,[[0,58]]] +[58,null,[[0,58]]] +["1/1","b",[[0,"1/1",null,[[1,65,58]],null]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +["1/1","b",[[0,"1/1",null,[[1,97,17]],null]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[58,null,[[0,58]]] +[58,null,[[0,58]]] +[58,null,[[0,58]]] +[58,null,[[0,58]]] +[58,null,[[0,58]]] +[58,null,[[0,58]]] + + +[14,null,[[0,14]]] +["1/1","b",[[0,"1/1",null,[[1,79,14]],null]]] +[3,null,[[0,3]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[14,null,[[0,14]]] +["1/1","b",[[0,"1/1",null,[[1,63,11]],null]]] +[9,null,[[0,9]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[9,null,[[0,9]]] +[9,null,[[0,9]]] +[11,null,[[0,11]]] +["1/1","b",[[0,"1/1",null,[[1,14,2]],null]]] +["1/1","b",[[0,"1/1",null,[[1,2,14]],null]]] + + +[58,null,[[0,58]]] +[58,null,[[0,58]]] +[58,null,[[0,58]]] +[59,null,[[0,59]]] +[59,null,[[0,59]]] +[59,null,[[0,59]]] +[59,null,[[0,59]]] +[58,null,[[0,58]]] +[58,null,[[0,58]]] +["1/1","b",[[0,"1/1",null,[[1,35,58]],null]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[58,null,[[0,58]]] +[55,null,[[0,55]]] +[55,null,[[0,55]]] +[58,null,[[0,58]]] + + +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] + + + + + +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] +[33,null,[[0,33]]] + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[25,null,[[0,25]]] +["1/1","b",[[0,"1/1",null,[[1,42,25]],null]]] +[24,null,[[0,24]]] +[24,null,[[0,24]]] +[25,null,[[0,25]]] +["1/1","b",[[0,"1/1",null,[[1,15,6]],null]]] +[30,null,[[0,30]]] + + + +[22,null,[[0,22]]] +["1/1","b",[[0,"1/1",null,[[1,46,22]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[22,null,[[0,22]]] +["1/1","b",[[0,"1/1",null,[[1,34,21]],null]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[21,null,[[0,21]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +["2/2","b",[[0,"2/2",null,[[1,48,16],[48,52,1]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +["1/1","b",[[0,"1/1",null,[[1,25,1]],null]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +["1/1","b",[[0,"1/1",null,[[1,2,22]],null]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + + + +[21,null,[[0,21]]] +["1/1","b",[[0,"1/1",null,[[1,89,21]],null]]] +["1/1","b",[[0,"1/1",null,[[1,70,13]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[13,null,[[0,13]]] +[21,null,[[0,21]]] +[20,null,[[0,20]]] +["1/1","b",[[0,"1/1",null,[[1,2,21]],null]]] + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + + + + + +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + + + +[3,null,[[0,3]]] +["1/1","b",[[0,"1/1",null,[[1,29,3]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +["1/1","b",[[0,"1/1",null,[[1,78,1]],null]]] +["1/1","b",[[0,"1/1",null,[[1,2,3]],null]]] + + + + + +[7,null,[[0,7]]] +["1/1","b",[[0,"1/1",null,[[1,29,7]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[7,null,[[0,7]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +["1/1","b",[[0,"1/1",null,[[1,41,6]],null]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +["3/3","b",[[0,"3/3",null,[[1,12,6],[12,121,4],[121,143,2]],null]]] +["1/1","b",[[0,"1/1",null,[[1,67,2]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +["1/1","b",[[0,"1/1",null,[[1,16,2]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +["1/1","b",[[0,"1/1",null,[[1,12,4]],null]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +["1/1","b",[[0,"1/1",null,[[1,2,7]],null]]] + + + + + + +[8,null,[[0,8]]] +[8,null,[[0,8]]] +["1/1","b",[[0,"1/1",null,[[1,25,8]],null]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +["2/2","b",[[0,"2/2",null,[[1,28,6],[28,46,3]],null]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[6,null,[[0,6]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[2,null,[[0,2]]] +[4,null,[[0,4]]] +["1/1","b",[[0,"1/1",null,[[1,38,2]],null]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +["1/1","b",[[0,"1/1",null,[[1,78,2]],null]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +["1/1","b",[[0,"1/1",null,[[1,14,2]],null]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +["1/1","b",[[0,"1/1",null,[[1,80,2]],null]]] +[1,null,[[0,1]]] +["1/1","b",[[0,"1/1",null,[[1,20,2]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[4,null,[[0,4]]] +["1/1","b",[[0,"1/1",null,[[1,2,8]],null]]] + + + + + + +[15,null,[[0,15]]] +[15,null,[[0,15]]] +["1/1","b",[[0,"1/1",null,[[1,38,15]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[15,null,[[0,15]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +["1/1","b",[[0,"1/1",null,[[1,29,14]],null]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +["2/2","b",[[0,"2/2",null,[[1,48,14],[48,52,1]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +[14,null,[[0,14]]] +["1/1","b",[[0,"1/1",null,[[1,2,15]],null]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + + +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] + + + +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[21,null,[[0,21]]] +[21,null,[[0,21]]] +[21,null,[[0,21]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[4,null,[[0,4]]] +[4,null,[[0,4]]] +["2/2","b",[[0,"2/2",null,[[1,83,4],[83,121,2]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[4,null,[[0,4]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +["1/1","b",[[0,"1/1",null,[[1,2,4]],null]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[4,null,[[0,4]]] +[4,null,[[0,4]]] +["1/1","b",[[0,"1/1",null,[[1,64,4]],null]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +["1/1","b",[[0,"1/1",null,[[1,14,1]],null]]] +[4,null,[[0,4]]] + + +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + + + +[10,null,[[0,10]]] +["1/1","b",[[0,"1/1",null,[[1,23,10]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[10,null,[[0,10]]] +[10,null,[[0,10]]] +[10,null,[[0,10]]] +[10,null,[[0,10]]] +[10,null,[[0,10]]] +[10,null,[[0,10]]] +["1/1","b",[[0,"1/1",null,[[1,32,10]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] +[10,null,[[0,10]]] +[10,null,[[0,10]]] +[10,null,[[0,10]]] + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + + + +[4,null,[[0,4]]] +[4,null,[[0,4]]] +["1/1","b",[[0,"1/1",null,[[1,115,4]],null]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[4,null,[[0,4]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[55,null,[[0,55]]] +[55,null,[[0,55]]] +[55,null,[[0,55]]] + + +[55,null,[[0,55]]] +[55,null,[[0,55]]] +[55,null,[[0,55]]] +[55,null,[[0,55]]] +[55,null,[[0,55]]] +[55,null,[[0,55]]] +[55,null,[[0,55]]] +[55,null,[[0,55]]] +[55,null,[[0,55]]] +[55,null,[[0,55]]] +[55,null,[[0,55]]] + + +[38,null,[[0,38]]] +[38,null,[[0,38]]] +[38,null,[[0,38]]] +[38,null,[[0,38]]] +[38,null,[[0,38]]] +[38,null,[[0,38]]] +[38,null,[[0,38]]] +[38,null,[[0,38]]] +[38,null,[[0,38]]] +["1/1","b",[[0,"1/1",null,[[1,36,38]],null]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[38,null,[[0,38]]] +[38,null,[[0,38]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +["1/1","b",[[0,"1/1",null,[[1,29,35]],null]]] +[28,null,[[0,28]]] +[28,null,[[0,28]]] +[35,null,[[0,35]]] +[35,null,[[0,35]]] +[38,null,[[0,38]]] + + +[17,null,[[0,17]]] +[17,null,[[0,17]]] +["1/1","b",[[0,"1/1",null,[[1,22,17]],null]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] +["1/1","b",[[0,"1/1",null,[[1,2,17]],null]]] + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[5,null,[[0,5]]] +["1/1","b",[[0,"1/1",null,[[1,15,5]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[5,null,[[0,5]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +["1/1","b",[[0,"1/1",null,[[1,2,5]],null]]] + + +[37,null,[[0,37]]] +[37,null,[[0,37]]] +[37,null,[[0,37]]] +["3/3","b",[[0,"3/3",null,[[1,50,1],[50,53,37],[53,75,36]],null]]] +[37,null,[[0,37]]] +[37,null,[[0,37]]] + + +[62,null,[[0,62]]] +["1/1","b",[[0,"1/1",null,[[1,15,62]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[62,null,[[0,62]]] +[61,null,[[0,61]]] +[61,null,[[0,61]]] +[61,null,[[0,61]]] +[61,null,[[0,61]]] +[61,null,[[0,61]]] +[61,null,[[0,61]]] +[61,null,[[0,61]]] +[62,null,[[0,62]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +["1/1","b",[[0,"1/1",null,[[1,92,3]],null]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +["2/2","b",[[0,"2/2",null,[[1,34,2],[34,41,1]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + + + + + + + + + + +[23,null,[[0,23]]] +["1/1","b",[[0,"1/1",null,[[1,36,23]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[23,null,[[0,23]]] +["1/1","b",[[0,"1/1",null,[[1,84,22]],null]]] +[23,null,[[0,23]]] + + +[4,null,[[0,4]]] +[4,null,[[0,4]]] +["1/1","b",[[0,"1/1",null,[[1,87,4]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[4,null,[[0,4]]] +[3,null,[[0,3]]] +["1/1","b",[[0,"1/1",null,[[1,17,3]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[3,null,[[0,3]]] +[2,null,[[0,2]]] +["1/1","b",[[0,"1/1",null,[[1,16,2]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +["1/1","b",[[0,"1/1",null,[[1,52,1]],null]]] +["1/1","b",[[0,"1/1",null,[[1,2,4]],null]]] + + + + +[9,null,[[0,9]]] +[9,null,[[0,9]]] +[9,null,[[0,9]]] +[9,null,[[0,9]]] +[9,null,[[0,9]]] +["1/1","b",[[0,"1/1",null,[[1,29,9]],null]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[9,null,[[0,9]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[7,null,[[0,7]]] +[9,null,[[0,9]]] + + +[4,null,[[0,4]]] +["1/1","b",[[0,"1/1",null,[[1,16,4]],null]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +["1/1","b",[[0,"1/1",null,[[1,34,4]],null]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[4,null,[[0,4]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +["1/1","b",[[0,"1/1",null,[[1,59,1]],null]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +["1/1","b",[[0,"1/1",null,[[1,34,1]],null]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +["0/1","b",[[0,"0/1",null,[[1,14,0]],null]]] +["1/1","b",[[0,"1/1",null,[[1,2,4]],null]]] + + + + +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[44,null,[[0,44]]] +[44,null,[[0,44]]] +[67,null,[[0,67]]] +[67,null,[[0,67]]] +["1/1","b",[[0,"1/1",null,[[1,28,45]],null]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[67,null,[[0,67]]] +[67,null,[[0,67]]] +[44,null,[[0,44]]] +[44,null,[[0,44]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +[22,null,[[0,22]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[71,null,[[0,71]]] +[71,null,[[0,71]]] +[71,null,[[0,71]]] +[71,null,[[0,71]]] +[71,null,[[0,71]]] +[71,null,[[0,71]]] +[71,null,[[0,71]]] +[71,null,[[0,71]]] + + + + +[90,null,[[0,90]]] +[90,null,[[0,90]]] +[90,null,[[0,90]]] +[90,null,[[0,90]]] +[90,null,[[0,90]]] +[90,null,[[0,90]]] +[90,null,[[0,90]]] +[90,null,[[0,90]]] +[90,null,[[0,90]]] +[90,null,[[0,90]]] +[90,null,[[0,90]]] +[90,null,[[0,90]]] +[90,null,[[0,90]]] + + +[67,null,[[0,67]]] +[67,null,[[0,67]]] +[67,null,[[0,67]]] +[67,null,[[0,67]]] +[67,null,[[0,67]]] + + +[7,null,[[0,7]]] +["1/1","b",[[0,"1/1",null,[[1,18,7]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[7,null,[[0,7]]] +[6,null,[[0,6]]] +["2/2","b",[[0,"2/2",null,[[1,23,6],[23,29,1]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[6,null,[[0,6]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +["1/1","b",[[0,"1/1",null,[[1,2,7]],null]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + + +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +["1/1","b",[[0,"1/1",null,[[1,33,30]],null]]] +[1,null,[[0,1]]] +["2/2","b",[[0,"2/2",null,[[1,12,30],[12,27,29]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] + + +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] +[32,null,[[0,32]]] +[32,null,[[0,32]]] +[32,null,[[0,32]]] +[31,null,[[0,31]]] +[31,null,[[0,31]]] + + +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +[6,null,[[0,6]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +["1/1","b",[[0,"1/1",null,[[1,65,3]],null]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +["1/1","b",[[0,"1/1",null,[[1,91,5]],null]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +["1/1","b",[[0,"1/1",null,[[1,47,5]],null]]] +[3,null,[[0,3]]] +["1/1","b",[[0,"1/1",null,[[1,16,5]],null]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +["1/1","b",[[0,"1/1",null,[[1,27,3]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] + + + + +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] +[19,null,[[0,19]]] + + + + + + + + + +[30,null,[[0,30]]] +[30,null,[[0,30]]] +[1,null,[[0,1]]] +["1/1","b",[[0,"1/1",null,[[1,87,27]],null]]] +[1,null,[[0,1]]] +["1/1","b",[[0,"1/1",null,[[1,87,1]],null]]] +[30,null,[[0,30]]] +[30,null,[[0,30]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] + + + +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] +[20,null,[[0,20]]] + + +[8,null,[[0,8]]] +["2/2","b",[[0,"2/2",null,[[1,60,8],[60,83,7]],null]]] +[7,null,[[0,7]]] +["1/1","b",[[0,"1/1",null,[[1,12,8]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[8,null,[[0,8]]] + + + + +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] +[17,null,[[0,17]]] +["2/2","b",[[0,"2/2",null,[[1,29,17],[29,49,15]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[17,null,[[0,17]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[16,null,[[0,16]]] +[17,null,[[0,17]]] + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] + + +[13,null,[[0,13]]] +[13,null,[[0,13]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[13,null,[[0,13]]] +["1/1","b",[[0,"1/1",null,[[1,14,5]],null]]] +[13,null,[[0,13]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + + + + + +[3,null,[[0,3]]] +["1/1","b",[[0,"1/1",null,[[1,53,3]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[3,null,[[0,3]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +["1/1","b",[[0,"1/1",null,[[1,2,3]],null]]] + + +[3,null,[[0,3]]] +["1/1","b",[[0,"1/1",null,[[1,53,3]],null]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[3,null,[[0,3]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +["1/1","b",[[0,"1/1",null,[[1,2,3]],null]]] + + +[2,null,[[0,2]]] +["1/1","b",[[0,"1/1",null,[[1,44,2]],null]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +["1/1","b",[[0,"1/1",null,[[1,2,2]],null]]] + + +[2,null,[[0,2]]] +["1/1","b",[[0,"1/1",null,[[1,53,2]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] +["1/1","b",[[0,"1/1",null,[[1,2,2]],null]]] + + +[3,null,[[0,3]]] +["1/1","b",[[0,"1/1",null,[[1,53,3]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[3,null,[[0,3]]] +[3,null,[[0,3]]] +["1/1","b",[[0,"1/1",null,[[1,35,2]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[1,null,[[0,1]]] +["1/1","b",[[0,"1/1",null,[[1,2,3]],null]]] + + + +[2,null,[[0,2]]] +["1/1","b",[[0,"1/1",null,[[1,98,2]],null]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +["1/1","b",[[0,"1/1",null,[[1,12,2]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[2,null,[[0,2]]] + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +["1/1","b",[[0,"1/1",null,[[1,26,8]],null]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[8,null,[[0,8]]] +["1/1","b",[[0,"1/1",null,[[1,40,7]],null]]] +[8,null,[[0,8]]] + + +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] + + +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] +[8,null,[[0,8]]] + + +[2,null,[[0,2]]] +[2,null,[[0,2]]] +[2,null,[[0,2]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[34,null,[[0,34]]] +[34,null,[[0,34]]] +[5,null,[[0,5]]] +["1/1","b",[[0,"1/1",null,[[1,36,5]],null]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[5,null,[[0,5]]] +[34,null,[[0,34]]] +["1/1","b",[[0,"1/1",null,[[1,16,30]],null]]] +[34,null,[[0,34]]] + + +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] +[4,null,[[0,4]]] + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] + + +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[5,null,[[0,5]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +["1/1","b",[[0,"1/1",null,[[1,15,4]],null]]] +[5,null,[[0,5]]] + + + + +[36,null,[[0,36]]] +["1/1","b",[[0,"1/1",null,[[1,33,36]],null]]] +[0,null,[[0,0]]] +[0,null,[[0,0]]] +[36,null,[[0,36]]] +[36,null,[[0,36]]] +[36,null,[[0,36]]] +[36,null,[[0,36]]] +[36,null,[[0,36]]] +[36,null,[[0,36]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] +[1,null,[[0,1]]] \ No newline at end of file diff --git a/apps/worker/services/report/languages/v1.py b/apps/worker/services/report/languages/v1.py new file mode 100644 index 0000000000..68d8d3ade0 --- /dev/null +++ b/apps/worker/services/report/languages/v1.py @@ -0,0 +1,86 @@ +import sentry_sdk + +from helpers.exceptions import CorruptRawReportError +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import CoverageType, ReportBuilderSession + + +class VOneProcessor(BaseLanguageProcessor): + def matches_content(self, content: dict, first_line: str, name: str) -> bool: + return "coverage" in content or "RSpec" in content or "MiniTest" in content + + @sentry_sdk.trace + def process( + self, content: dict, report_builder_session: ReportBuilderSession + ) -> None: + if "RSpec" in content: + content = content["RSpec"] + + elif "MiniTest" in content: + content = content["MiniTest"] + + return from_json(content, report_builder_session) + + +def _list_to_dict(lines): + """ + in: [None, 1] || {"1": 1} + out: {"1": 1} + """ + if isinstance(lines, list): + if len(lines) > 1: + return { + ln: cov for ln, cov in enumerate(lines[1:], start=1) if cov is not None + } + else: + return {} + elif "lines" in lines: + # lines format here is + # { "lines": [ None, None, 1, 1,...] }, + # We add a fake first line because this function starts from line at index 1 not 0 + return _list_to_dict([None] + lines["lines"]) + else: + return lines or {} + + +def from_json(json: str, report_builder_session: ReportBuilderSession) -> None: + if not isinstance(json["coverage"], dict): + return + + for fn, lns in json["coverage"].items(): + _file = report_builder_session.create_coverage_file(fn) + if _file is None: + continue + + lns = _list_to_dict(lns) + for ln, cov in lns.items(): + try: + line_number = int(ln) + except ValueError: + raise CorruptRawReportError( + "v1", + "file dictionaries expected to have integers, not strings", + ) + if line_number > 0: + if isinstance(cov, str): + try: + int(cov) + except Exception: + pass + else: + cov = int(cov) + + coverage_type = ( + CoverageType.branch + if type(cov) in (str, bool) + else CoverageType.line + ) + _file.append( + line_number, + report_builder_session.create_coverage_line( + cov, + coverage_type, + ), + ) + + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/vb.py b/apps/worker/services/report/languages/vb.py new file mode 100644 index 0000000000..6c4e15cc35 --- /dev/null +++ b/apps/worker/services/report/languages/vb.py @@ -0,0 +1,50 @@ +import sentry_sdk +from lxml.etree import Element +from shared.reports.resources import ReportFile + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import ReportBuilderSession + + +class VbProcessor(BaseLanguageProcessor): + def matches_content(self, content: Element, first_line: str, name: str) -> bool: + return content.tag == "results" + + @sentry_sdk.trace + def process( + self, content: Element, report_builder_session: ReportBuilderSession + ) -> None: + return from_xml(content, report_builder_session) + + +def from_xml(xml: Element, report_builder_session: ReportBuilderSession) -> None: + files: dict[str, ReportFile] = {} + for module in xml.iter("module"): + # loop through sources + for sf in module.iter("source_file"): + _file = report_builder_session.create_coverage_file( + sf.attrib["path"].replace("\\", "/") + ) + if _file is not None: + files[sf.attrib["id"]] = _file + + # loop through each line + for line in module.iter("range"): + attr = line.attrib + _file = files.get(attr["source_id"]) + if _file is None: + continue + + cov_txt = attr["covered"] + coverage = 1 if cov_txt == "yes" else 0 if cov_txt == "no" else True + for ln in range(int(attr["start_line"]), int(attr["end_line"]) + 1): + _file.append( + ln, + report_builder_session.create_coverage_line( + coverage, + ), + ) + + # add files + for _file in files.values(): + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/vb2.py b/apps/worker/services/report/languages/vb2.py new file mode 100644 index 0000000000..596bd411a0 --- /dev/null +++ b/apps/worker/services/report/languages/vb2.py @@ -0,0 +1,51 @@ +import sentry_sdk +from lxml.etree import Element +from shared.reports.resources import ReportFile + +from services.report.report_builder import ReportBuilderSession + +from .base import BaseLanguageProcessor +from .helpers import child_text + + +class VbTwoProcessor(BaseLanguageProcessor): + def matches_content(self, content: Element, first_line: str, name: str) -> bool: + return content.tag == "CoverageDSPriv" + + @sentry_sdk.trace + def process( + self, content: Element, report_builder_session: ReportBuilderSession + ) -> None: + return from_xml(content, report_builder_session) + + +def from_xml(xml: Element, report_builder_session: ReportBuilderSession) -> None: + files: dict[str, ReportFile] = {} + for source in xml.iterfind("SourceFileNames"): + _file = report_builder_session.create_coverage_file( + child_text(source, "SourceFileName").replace("\\", "/") + ) + if _file is not None: + files[child_text(source, "SourceFileID")] = _file + + for line in xml.iterfind("Lines"): + _file = files.get(child_text(line, "SourceFileID")) + if _file is None: + continue + + # 0 == hit, 1 == partial, 2 == miss + cov_txt = child_text(line, "Coverage") + cov = 1 if cov_txt == "0" else 0 if cov_txt == "2" else True + for ln in range( + int(child_text(line, "LnStart")), + int(child_text(line, "LnEnd")) + 1, + ): + _file.append( + ln, + report_builder_session.create_coverage_line( + cov, + ), + ) + + for _file in files.values(): + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/xcode.py b/apps/worker/services/report/languages/xcode.py new file mode 100644 index 0000000000..23312b8bd7 --- /dev/null +++ b/apps/worker/services/report/languages/xcode.py @@ -0,0 +1,144 @@ +from io import BytesIO + +import sentry_sdk +from shared.helpers.numeric import maxint + +from services.report.languages.base import BaseLanguageProcessor +from services.report.languages.helpers import remove_non_ascii +from services.report.report_builder import ReportBuilderSession + +START_PARTIAL = "\033[0;41m" +END_PARTIAL = "\033[0m" +NAME_COLOR = "\033\x1b[0;36m" + + +class XCodeProcessor(BaseLanguageProcessor): + def matches_content(self, content: bytes, first_line: str, name: str) -> bool: + return name.endswith( + ("app.coverage.txt", "framework.coverage.txt", "xctest.coverage.txt") + ) or first_line.endswith( + ( + ".h:", + ".m:", + ".swift:", + ".hpp:", + ".cpp:", + ".cxx:", + ".c:", + ".C:", + ".cc:", + ".cxx:", + ".c++:", + ) + ) + + @sentry_sdk.trace + def process( + self, content: bytes, report_builder_session: ReportBuilderSession + ) -> None: + return from_txt(content, report_builder_session) + + +def get_partials_in_line(line): + if START_PARTIAL in line: + partials = [] + while START_PARTIAL in line and END_PARTIAL in line: + # get start of column + sc = line.find(START_PARTIAL) + + # remove the START_PARTIAL + line = line.replace(START_PARTIAL, "", 1).lstrip("\x1b") + + # trim empty. e.g., [0;41m print("See you later!") + # ^^^^ + ll = len(line) + line = line.lstrip() + offset = ll - len(line) + + # get end of column` + ec = line.find(END_PARTIAL) + + # remove the END_PARTIAL + line = line.replace(END_PARTIAL, "", 1) + + # add partial + partials.append([sc + offset, ec + offset, 0]) + + return partials + + +def from_txt(content: bytes, report_builder_session: ReportBuilderSession) -> None: + _file = None + ln_i = 1 + cov_i = 0 + for encoded_line in BytesIO(content): + line = encoded_line.decode(errors="replace").rstrip("\n") + line = remove_non_ascii(line).strip(" ") + if not line or line[0] in ("-", "|", "w"): + continue + + line = line.replace(NAME_COLOR, "") + if line.endswith(":") and "|" not in line: + if _file is not None: + report_builder_session.append(_file) + # file names could be "relative/path.abc:" or "/absolute/path.abc:" + # new file + _file = report_builder_session.create_coverage_file( + line.replace(END_PARTIAL, "")[1:-1] + ) + continue + + if _file is None: + continue + + parts = line.split("|") + lnl = len(parts) + if lnl > 1: + if lnl > 2 and parts[2].strip() == "}": + # skip ending bracket lines + continue + + try: + ln = int(parts[ln_i].strip()) + except Exception: + # bad xcode line + if parts[0] == "1": + ln_i, cov_i = 0, 1 + ln = 1 + else: + continue + + if parts[2]: + partials = get_partials_in_line(parts[2]) + if partials: + _file.append( + ln, + report_builder_session.create_coverage_line( + 0, + partials=partials, + ), + ) + continue + + cov_s = parts[cov_i].replace("E", "").strip() + if cov_s != "": + try: + if "k" in cov_s or "K" in cov_s: + cov = maxint(str(int(float(cov_s.replace("k", "")) * 1000.0))) + elif "m" in cov_s or "M" in cov_s: + cov = 99999 + else: + cov = maxint(str(int(float(cov_s)))) + except Exception: + cov = 1 + + try: + _file.append( + ln, + report_builder_session.create_coverage_line(cov), + ) + except Exception: + pass + + if _file is not None: + report_builder_session.append(_file) diff --git a/apps/worker/services/report/languages/xcodeplist.py b/apps/worker/services/report/languages/xcodeplist.py new file mode 100644 index 0000000000..abcf1b9ab4 --- /dev/null +++ b/apps/worker/services/report/languages/xcodeplist.py @@ -0,0 +1,86 @@ +import plistlib + +import sentry_sdk + +from services.report.languages.base import BaseLanguageProcessor +from services.report.report_builder import CoverageType, ReportBuilderSession + + +class XCodePlistProcessor(BaseLanguageProcessor): + def matches_content(self, content: bytes, first_line: str, name: str) -> bool: + if name: + return name.endswith("xccoverage.plist") + if content.find(b'') > -1 and content.startswith(b" None: + return from_xml(content, report_builder_session) + + +def from_xml(xml: bytes, report_builder_session: ReportBuilderSession) -> None: + objects = plistlib.loads(xml)["$objects"] + + for obj in objects[2]["NS.objects"]: + for sourceFile in objects[objects[obj["CF$UID"]]["sourceFiles"]["CF$UID"]][ + "NS.objects" + ]: + # get filename + filename = objects[ + objects[sourceFile["CF$UID"]]["documentLocation"]["CF$UID"] + ] + _file = report_builder_session.create_coverage_file(filename) + if _file is None: + continue + + # loop lines + for ln, line in enumerate( + objects[objects[sourceFile["CF$UID"]]["lines"]["CF$UID"]]["NS.objects"], + start=1, + ): + # get line object + line = objects[line["CF$UID"]] + # is line is tracked in coverage? + if line["x"] is not False: + # does line have partial content? + if line["s"]["CF$UID"] != 0: + partials = [] + hits = 0 + # loop branches + for branch in objects[line["s"]["CF$UID"]]["NS.objects"]: + # get branch object + branch = objects[branch["CF$UID"]] + # skip ending branches + if branch["len"] != 2: # ending method + # append partials + partials.append( + [ + branch["c"], + branch["c"] + branch["len"], + branch["x"], + ] + ) + hits += 1 if branch["x"] > 0 else 0 + # set coverage ratio + coverage = "%s/%s" % (hits, len(partials)) + + else: + # statement line + partials = None + coverage = line["c"] + + # append line to report + _file.append( + ln, + report_builder_session.create_coverage_line( + coverage, + CoverageType.branch if partials else CoverageType.line, + partials=partials, + ), + ) + + # append file to report + report_builder_session.append(_file) diff --git a/apps/worker/services/report/parser/__init__.py b/apps/worker/services/report/parser/__init__.py new file mode 100644 index 0000000000..3e9240a308 --- /dev/null +++ b/apps/worker/services/report/parser/__init__.py @@ -0,0 +1,18 @@ +import sentry_sdk + +from database.models.reports import Upload +from services.report.parser.legacy import LegacyReportParser +from services.report.parser.version_one import VersionOneReportParser + + +def get_proper_parser(upload: Upload, contents: bytes): + if upload.upload_extras and upload.upload_extras.get("format_version") == "v1": + contents = contents.strip() + if contents.startswith(b"{") and contents.endswith(b"}"): + return VersionOneReportParser() + else: + with sentry_sdk.new_scope() as scope: + scope.set_extra("upload_extras", upload.upload_extras) + scope.set_extra("contents", contents[:64]) + sentry_sdk.capture_message("Upload `format_version` lied to us") + return LegacyReportParser() diff --git a/apps/worker/services/report/parser/legacy.py b/apps/worker/services/report/parser/legacy.py new file mode 100644 index 0000000000..1b8e9d59a3 --- /dev/null +++ b/apps/worker/services/report/parser/legacy.py @@ -0,0 +1,141 @@ +import string +from io import BytesIO + +import sentry_sdk + +from services.report.parser.types import LegacyParsedRawReport, ParsedUploadedReportFile + + +class LegacyReportParser(object): + network_separator = b"<<<<<< network" + env_separator = b"<<<<<< ENV" + eof_separator = b"<<<<<< EOF" + ignore_from_now_on_marker = b"==FROMNOWONIGNOREDBYCODECOV==>>>" + + separator_lines = [network_separator, env_separator, eof_separator] + + def _find_place_to_cut(self, raw_report: bytes): + """Finds the locations of all separators in the report, as listed above. + + Args: + raw_report (bytes): the raw_report to parse + + Yields: + tuple: tuple in the format (separator_location, separator) + """ + common_base = b"<<<<<<" + starting_point = 0 + while 0 <= starting_point <= len(raw_report): + next_place = raw_report.find(common_base, starting_point) + if next_place >= 0: + starting_point = next_place + 1 + for separator in self.separator_lines: + w = raw_report.find( + separator, next_place, next_place + len(separator) + ) + if w >= 0: + yield w, separator + starting_point = next_place + len(separator) + else: + return + + def _get_sections_to_cut(self, raw_report: bytes): + """Finds which are the sections to cut when parsing `raw_report`. + It yields, for each section, where it starts, ends and what separator it uses + + Args: + raw_report (bytes): the raw_report to parse + + Yields: + tuple: tuple in the format (start_index, end_index, separator used) + """ + places_to_cut = sorted(self._find_place_to_cut(raw_report)) + if places_to_cut: + yield (0, places_to_cut[0][0], places_to_cut[0][1]) + for prev, nex in zip(places_to_cut, places_to_cut[1:]): + yield (prev[0] + len(prev[1]), nex[0], nex[1]) + yield ( + places_to_cut[-1][0] + len(places_to_cut[-1][1]), + len(raw_report), + None, + ) + else: + yield (0, len(raw_report), None) + + def cut_sections(self, raw_report: bytes): + """Cuts `raw_report` into the sections that we recognize in a report + + This function takes the proper steps to find all the relevant sections of a report: + - toc: the 'network', list of files present on this report + - env: the envvars the user set on the upload + - uploaded_files: the actual report files + - report_fixes: the report fixes some languages need + + and splits them, also taking care of 'strip()' them, removing whitespaces, + as the original logic also does. + + Args: + raw_report (bytes): the raw_report to parse + + Yields: + dict: Dicts with contents, filename and footer of each section + """ + whitespaces = set(string.whitespace.encode()) + sections = self._get_sections_to_cut(raw_report) + for start, end, separator in sections: + i_start, i_end = start, end + while i_start < i_end and raw_report[i_start] in whitespaces: + i_start += 1 + while i_start < i_end and raw_report[i_end - 1] in whitespaces: + i_end -= 1 + if i_start < i_end: + filename = None + if raw_report[i_start : i_start + len(b"# path=")] == b"# path=": + content = BytesIO(raw_report) + content.seek(i_start) + first_line = next(iter(content)) + filename = first_line.split(b"# path=")[1].decode().strip() + i_start = i_start + len(first_line) + while i_start < i_end and raw_report[i_start] in whitespaces: + i_start += 1 + yield { + "contents": raw_report[i_start:i_end], + "filename": filename, + "footer": separator, + } + + @sentry_sdk.trace + def parse_raw_report_from_bytes(self, raw_report: bytes) -> LegacyParsedRawReport: + raw_report, _, _compat_report_str = raw_report.partition( + self.ignore_from_now_on_marker + ) + sections = self.cut_sections(raw_report) + res = self._generate_parsed_report_from_sections(sections) + return res + + def _generate_parsed_report_from_sections(self, sections): + uploaded_files = [] + toc_section = None + env_section = None + report_fixes_section = None + for sect in sections: + if sect["footer"] == self.network_separator: + toc_section = sect["contents"] + elif sect["footer"] == self.env_separator: + env_section = sect["contents"] + else: + if sect["filename"] == "fixes": + report_fixes_section = sect["contents"] + else: + uploaded_files.append( + ParsedUploadedReportFile( + filename=sect.get("filename"), + file_contents=sect["contents"], + ) + ) + return LegacyParsedRawReport( + toc=toc_section, + env=env_section, + uploaded_files=uploaded_files, + report_fixes=report_fixes_section, + ) diff --git a/apps/worker/services/report/parser/tests/unit/test_parsers.py b/apps/worker/services/report/parser/tests/unit/test_parsers.py new file mode 100644 index 0000000000..a0cd11d739 --- /dev/null +++ b/apps/worker/services/report/parser/tests/unit/test_parsers.py @@ -0,0 +1,27 @@ +import pytest + +from database.tests.factories import UploadFactory +from services.report.parser import ( + LegacyReportParser, + VersionOneReportParser, + get_proper_parser, +) + + +@pytest.mark.parametrize( + "upload_extras, contents, expected_type", + [ + (None, b"", LegacyReportParser), + ({}, b"", LegacyReportParser), + ({"something": 1}, b"", LegacyReportParser), + ({"format_version": "v1"}, b"{}", VersionOneReportParser), + ({"format_version": "v1", "something": "else"}, b"{}", VersionOneReportParser), + ({"format_version": None}, b"", LegacyReportParser), + ({"format_version": "v1"}, b"not/a/v1/format.txt", LegacyReportParser), + ], +) +def test_get_proper_parser(dbsession, upload_extras, contents, expected_type): + upload = UploadFactory.create(upload_extras=upload_extras) + dbsession.add(upload) + dbsession.flush() + assert isinstance(get_proper_parser(upload, contents), expected_type) diff --git a/apps/worker/services/report/parser/tests/unit/test_version_one_parser.py b/apps/worker/services/report/parser/tests/unit/test_version_one_parser.py new file mode 100644 index 0000000000..340a168566 --- /dev/null +++ b/apps/worker/services/report/parser/tests/unit/test_version_one_parser.py @@ -0,0 +1,109 @@ +import base64 +import zlib + +from services.report.parser.version_one import ( + ParsedUploadedReportFile, + VersionOneReportParser, +) + +input_data = b"""{ + "report_fixes": { + "format": "legacy", + "value": { + "SwiftExample/AppDelegate.swift": { + "eof": 15, + "lines": [1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 13] + }, + "SwiftExample/Hello.swift": { + "eof": 30, + "lines": [1, 17, 3, 22, 7, 9, 12, 14] + } + } + }, + "network_files": [ + "path/to/file1.c", + "path/from/another.cpp", + "path/from/aaaaaa.cpp" + ], + "coverage_files": [ + { + "filename": "coverage.xml", + "format": "base64+compressed", + "data": "eJwlzMEJAzEMRNH7VjEFhFSSJhRLLAOW7bWk/mPI+fP+Z25zcEU5dPa5EUyIW76uNkdYS8vaEOViNI4b1nlimB4AY4VPRZqvgzkalVojr0p0+Z49LP9rg8s9BNL5lLx/AW8tRQ==", + "labels": null + }, + { + "filename": "another.coverage.json", + "format": "base64+compressed", + "data": "eJztV11P2zAUfedXGEsTIC1xPigtKC0PDE17mDYJNImnyk3c1lvsRLGD6L/nOl9NoWkjlr3tJW1ufM859/ravg5uX0SMnlmmeCKn2LUdjG5nJ8Hplx93j08/71GYwEe6YicIPTw9PN5/R2drrdMbQsJkwTKdZ9RWSZ6FbJlkK2ZLpglAktrPcnw70tEZYNYmtMioDNdWRjWbYsd2xu6lj1HMJWtso5FzhZHmgilNRQrKrkaT8bXnTq7xVq5vj20Xz0AbQsUjOLUs9JVJZnAitNg0+u10c4Mq5ZJFC6rDNYsjltlhIkBtxBrJyLJKyCCl4R8wqPK1beqIAbDSmL1wvQHj3pAkFfCGt4gFahhTpdo8u1/6sS15zEp4JlK9OUBffCcqC/EsIAXDUMxlKRygXiYm6cR44PekBbFgep1EeyS9GVEhnlOZyI1IcjV3LzBCa64V1Av8U3wlKVSoGXTxC6NuxALVaFaz4gfJXEB1T3EuI7YEQ9QGJpC1cnC3RFJq7AiRHIwxOAZeSCzlTHCj1cGkn0+xymsv97hXWQNTrLOcmbmXEdew/qx6wQD1J3TuEA/S3yRJcKW4XFmlMwOjqZctrzcE78gQu8eI/Va4/hC0rmN4vV3emuHyOEM9dFS7g8CewXrk8qJj4VQMjeuBCtod2KogpDcpUP3ORYpRe34PBdWB5g6K5nWhmcn4AJ7/cTzYM3vkuNwj+pbC1f9S2J/E4VI94Db2uXls1/14oA1tz5Yy6YLuPIe6j3XTIT2ENKboXb+zM+5Dx39xrPduexqu4yc+dIGaCSb1sUpuBqKyFak1lRRTvDB9KvsmNcuS9C4RwkyhEVwkvJ6gJY0VOHH5nIS0mqJcahMtnP0Nx19qqYm9t0v+Pa/7L3j9HgH3JW4N6upqupuegZtQaPtgjg/UoWn3ofrJfM4l1/N5RxtcGXf6f1JdAKobAtleEYLmAjE7eQU4GYHA", + "labels": ["simple", "a.py::fileclass::test_simple"] + } + ], + "metadata": { + } +}""" + + +def test_version_one_parser(): + subject = VersionOneReportParser() + res = subject.parse_raw_report_from_bytes(input_data) + assert res.get_env() is None + assert res.get_report_fixes(None) == { + "SwiftExample/AppDelegate.swift": { + "eof": 15, + "lines": [1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 13], + }, + "SwiftExample/Hello.swift": { + "eof": 30, + "lines": [1, 17, 3, 22, 7, 9, 12, 14], + }, + } + assert res.get_toc() == [ + "path/to/file1.c", + "path/from/another.cpp", + "path/from/aaaaaa.cpp", + ] + assert len(res.get_uploaded_files()) == 2 + first_file, second_file = res.get_uploaded_files() + assert isinstance(first_file, ParsedUploadedReportFile) + assert first_file.filename == "coverage.xml" + assert ( + first_file.contents + == b"Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit,\nsed do eiusmod tempor incididunt\nut labore et dolore magna aliqua." + ) + assert first_file.size == 123 + assert first_file.labels is None + assert second_file.filename == "another.coverage.json" + assert ( + second_file.contents + == b'\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n' + ) + assert second_file.size == 3415 + assert second_file.labels == ["simple", "a.py::fileclass::test_simple"] + + assert ( + res.content().getvalue().decode("utf-8") + == f"path/to/file1.c\npath/from/another.cpp\npath/from/aaaaaa.cpp\n<<<<<< network\n\n# path=coverage.xml\n{first_file.contents.decode('utf-8')}\n<<<<<< EOF\n\n# path=another.coverage.json\n{second_file.contents.decode('utf-8')}\n<<<<<< EOF\n\n" + ) + + +def test_version_one_parser_parse_coverage_file_contents_bad_format(): + subject = VersionOneReportParser() + coverage_file = {"format": "unknown", "data": b"simple", "filename": "filename.py"} + assert subject._parse_coverage_file_contents(coverage_file) == b"simple" + + +def test_version_one_parser_parse_coverage_file_contents_base64_zip_format(): + original_input = b"some_cool_string right \n here" + formatted_input = base64.b64encode(zlib.compress(original_input)) + # An assert for the sake of showing the result + assert formatted_input == b"eJwrzs9NjU/Oz8+JLy4pysxLVyjKTM8oUeBSyEgtSgUArOcK4w==" + subject = VersionOneReportParser() + coverage_file = { + "format": "base64+compressed", + "data": formatted_input, + "filename": "filename.py", + } + res = subject._parse_coverage_file_contents(coverage_file) + assert isinstance(res, bytes) + assert res == b"some_cool_string right \n here" diff --git a/apps/worker/services/report/parser/types.py b/apps/worker/services/report/parser/types.py new file mode 100644 index 0000000000..f937597116 --- /dev/null +++ b/apps/worker/services/report/parser/types.py @@ -0,0 +1,123 @@ +from io import BytesIO +from typing import Any + +from services.path_fixer.fixpaths import clean_toc +from services.report.fixes import get_fixes_from_raw + + +class ParsedUploadedReportFile(object): + def __init__( + self, + filename: str | None, + file_contents: bytes, + labels: list[str] | None = None, + ): + self.filename = filename + self.contents = file_contents + self.size = len(self.contents) + self.labels = labels + + def get_first_line(self): + return BytesIO(self.contents).readline() + + +class ParsedRawReport(object): + """ + Parsed raw report parent class + + Attributes + ---------- + toc + table of contents, this lists the files relevant to the report, + i.e. the files contained in the repository + env + list of env vars in environment of uploader (legacy only) + uploaded_files + list of class ParsedUploadedReportFile describing uploaded coverage files + report_fixes + list of objects describing report_fixes for each file, the format differs between + legacy and VersionOne parsed raw report + """ + + def __init__( + self, + toc: Any, + env: Any, + uploaded_files: list[ParsedUploadedReportFile], + report_fixes: Any, + ): + self.toc = toc + self.env = env + self.uploaded_files = uploaded_files + self.report_fixes = report_fixes + + def has_toc(self) -> bool: + return self.toc is not None + + def has_env(self) -> bool: + return self.env is not None + + def has_report_fixes(self) -> bool: + return self.report_fixes is not None + + @property + def size(self): + return sum(f.size for f in self.uploaded_files) + + def content(self) -> BytesIO: + buffer = BytesIO() + if self.has_toc(): + for file in self.get_toc(): + buffer.write(f"{file}\n".encode("utf-8")) + buffer.write(b"<<<<<< network\n\n") + for file in self.uploaded_files: + buffer.write(f"# path={file.filename}\n".encode("utf-8")) + buffer.write(file.contents) + buffer.write(b"\n<<<<<< EOF\n\n") + buffer.seek(0) + return buffer + + +class VersionOneParsedRawReport(ParsedRawReport): + """ + report_fixes : Dict[str, Dict[str, any]] + { + : { + eof: int | None + lines: List[int] + }, + ... + } + """ + + def get_toc(self) -> list[str]: + return self.toc + + def get_env(self): + return self.env + + def get_uploaded_files(self): + return self.uploaded_files + + def get_report_fixes(self, path_fixer) -> dict[str, dict[str, Any]]: + return self.report_fixes + + +class LegacyParsedRawReport(ParsedRawReport): + """ + report_fixes : BinaryIO + :,,... + """ + + def get_toc(self) -> list[str]: + return clean_toc(self.toc.decode(errors="replace").strip()) + + def get_env(self): + return self.env.decode(errors="replace") + + def get_uploaded_files(self): + return self.uploaded_files + + def get_report_fixes(self, path_fixer) -> dict[str, dict[str, Any]]: + report_fixes = self.report_fixes.decode(errors="replace") + return get_fixes_from_raw(report_fixes, path_fixer) diff --git a/apps/worker/services/report/parser/version_one.py b/apps/worker/services/report/parser/version_one.py new file mode 100644 index 0000000000..be4015f946 --- /dev/null +++ b/apps/worker/services/report/parser/version_one.py @@ -0,0 +1,50 @@ +import base64 +import logging +import zlib + +import orjson +import sentry_sdk + +from services.report.parser.types import ( + ParsedUploadedReportFile, + VersionOneParsedRawReport, +) + +log = logging.getLogger(__name__) + + +class VersionOneReportParser(object): + @sentry_sdk.trace + def parse_raw_report_from_bytes(self, raw_report: bytes): + data = orjson.loads(raw_report) + return VersionOneParsedRawReport( + toc=data["network_files"], + env=None, + uploaded_files=[ + self._parse_single_coverage_file(x) for x in data["coverage_files"] + ], + report_fixes=self._parse_report_fixes( + # want backwards compatibility with older versions of the CLI that still name this section path_fixes + data["report_fixes"] if "report_fixes" in data else data["path_fixes"] + ), + ) + + def _parse_report_fixes(self, value): + return value["value"] + + def _parse_single_coverage_file(self, coverage_file): + actual_data = self._parse_coverage_file_contents(coverage_file) + return ParsedUploadedReportFile( + filename=coverage_file["filename"], + file_contents=actual_data, + labels=coverage_file["labels"], + ) + + def _parse_coverage_file_contents(self, coverage_file): + if coverage_file["format"] == "base64+compressed": + return zlib.decompress(base64.b64decode(coverage_file["data"])) + log.warning( + "Unkown format found while parsing upload", + extra=dict(coverage_file_filename=coverage_file["filename"]), + ) + return coverage_file["data"] diff --git a/apps/worker/services/report/prometheus_metrics.py b/apps/worker/services/report/prometheus_metrics.py new file mode 100644 index 0000000000..60ddfcda60 --- /dev/null +++ b/apps/worker/services/report/prometheus_metrics.py @@ -0,0 +1,30 @@ +from shared.metrics import Histogram + +from helpers.metrics import KiB, MiB + +RAW_UPLOAD_SIZE = Histogram( + "worker_services_report_raw_upload_size", + "Size (in bytes) of a raw upload (which may contain several raw reports)", + ["version"], + buckets=[ + 100 * KiB, + 500 * KiB, + 1 * MiB, + 5 * MiB, + 10 * MiB, + 50 * MiB, + 100 * MiB, + 200 * MiB, + 500 * MiB, + 1000 * MiB, + ], +) + +RAW_UPLOAD_RAW_REPORT_COUNT = Histogram( + "worker_services_report_raw_upload_raw_report_count", + "Number of raw coverage files contained in a raw upload", + ["version"], + # The 0.98 bucket is to stop Prometheus from interpolating values much + # lower than 1 in its histogram_quantile function. + buckets=[0.98, 1, 2, 3, 4, 5, 7, 10, 30, 50, 100], +) diff --git a/apps/worker/services/report/raw_upload_processor.py b/apps/worker/services/report/raw_upload_processor.py new file mode 100644 index 0000000000..ed8c787c13 --- /dev/null +++ b/apps/worker/services/report/raw_upload_processor.py @@ -0,0 +1,176 @@ +import logging +from dataclasses import dataclass + +import sentry_sdk +from shared.reports.resources import Report +from shared.utils.sessions import Session, SessionType +from shared.yaml import UserYaml + +from database.models.reports import Upload +from helpers.exceptions import ReportEmptyError, ReportExpiredException +from helpers.labels import get_all_report_labels, get_labels_per_session +from services.path_fixer import PathFixer +from services.processing.metrics import LABELS_USAGE +from services.report.parser.types import ParsedRawReport +from services.report.report_builder import ReportBuilder +from services.report.report_processor import process_report + +log = logging.getLogger(__name__) + + +@dataclass +class SessionAdjustmentResult: + fully_deleted_sessions: list[int] + partially_deleted_sessions: list[int] + + +@sentry_sdk.trace +def process_raw_upload( + commit_yaml, + raw_reports: ParsedRawReport, + session: Session, +) -> Report: + # ---------------------- + # Extract `git ls-files` + # ---------------------- + toc = [] + if raw_reports.has_toc(): + toc = raw_reports.get_toc() + + if raw_reports.has_env(): + env = raw_reports.get_env() + session.env = dict([e.split("=", 1) for e in env.split("\n") if "=" in e]) + + path_fixer = PathFixer.init_from_user_yaml( + commit_yaml=commit_yaml, toc=toc, flags=session.flags + ) + + # ------------------ + # Extract bash fixes + # ------------------ + ignored_lines = {} + if raw_reports.has_report_fixes(): + ignored_lines = raw_reports.get_report_fixes(path_fixer) + + # [javascript] check for both coverage.json and coverage/coverage.lcov + skip_files = set() + for report_file in raw_reports.get_uploaded_files(): + if report_file.filename == "coverage/coverage.json": + skip_files.add("coverage/coverage.lcov") + + report = Report() + sessionid = session.id = report.next_session_number() + + # --------------- + # Process reports + # --------------- + for report_file in raw_reports.get_uploaded_files(): + current_filename = report_file.filename + if current_filename in skip_files or not report_file.contents: + continue + + path_fixer_to_use = path_fixer.get_relative_path_aware_pathfixer( + current_filename + ) + report_builder_to_use = ReportBuilder( + commit_yaml, sessionid, ignored_lines, path_fixer_to_use + ) + if report_builder_to_use.supports_labels(): + # NOTE: this here is very conservative, as it checks for *any* `carryforward_mode=labels`, + # not taking the `flags` into account at all. + LABELS_USAGE.labels(codepath="report_builder").inc() + + try: + report_from_file = process_report( + report=report_file, report_builder=report_builder_to_use + ) + except ReportExpiredException as r: + r.filename = current_filename + raise + + if not report_from_file: + continue + if report.is_empty(): + # if the initial report is empty, we can avoid a costly merge operation + report = report_from_file + else: + # merging the smaller report into the larger one is faster, + # so swap the two reports in that case. + if len(report_from_file._files) > len(report._files): + report_from_file, report = report, report_from_file + + report.merge(report_from_file) + + if not report: + raise ReportEmptyError("No files found in report.") + + _sessionid, session = report.add_session(session, use_id_from_session=True) + session.totals = report.totals + + return report + + +@sentry_sdk.trace +def clear_carryforward_sessions( + original_report: Report, + to_merge_report: Report, + to_merge_flags: list[str], + current_yaml: UserYaml, + upload: Upload | None = None, +): + flags_under_carryforward_rules = { + f for f in to_merge_flags if current_yaml.flag_has_carryfoward(f) + } + to_partially_overwrite_flags = { + f + for f in flags_under_carryforward_rules + if current_yaml.get_flag_configuration(f).get("carryforward_mode") == "labels" + } + if to_partially_overwrite_flags: + # NOTE: this here might be the most accurate counter, as it takes into account the + # actual `to_merge_flags` that were used for this particular upload. + sentry_sdk.capture_message( + "Customer is using `carryforward_mode=labels` feature", + extras={"flags": to_partially_overwrite_flags}, + ) + LABELS_USAGE.labels(codepath="carryforward_cleanup").inc() + + to_fully_overwrite_flags = flags_under_carryforward_rules.difference( + to_partially_overwrite_flags + ) + + if upload is None and to_partially_overwrite_flags: + log.warning("Upload is None, but there are partial_overwrite_flags present") + + session_ids_to_fully_delete = [] + session_ids_to_partially_delete = [] + + if to_fully_overwrite_flags or to_partially_overwrite_flags: + for session_id, session in original_report.sessions.items(): + if session.session_type == SessionType.carriedforward and session.flags: + if any(f in to_fully_overwrite_flags for f in session.flags): + session_ids_to_fully_delete.append(session_id) + if any(f in to_partially_overwrite_flags for f in session.flags): + session_ids_to_partially_delete.append(session_id) + + actually_fully_deleted_sessions = set() + if session_ids_to_fully_delete: + original_report.delete_multiple_sessions(session_ids_to_fully_delete) + actually_fully_deleted_sessions.update(session_ids_to_fully_delete) + + if session_ids_to_partially_delete: + all_labels = get_all_report_labels(to_merge_report) + original_report.delete_labels(session_ids_to_partially_delete, all_labels) + fully_deleted_sessions = [ + s + for s in session_ids_to_partially_delete + if not get_labels_per_session(original_report, s) + ] + if fully_deleted_sessions: + original_report.delete_multiple_sessions(fully_deleted_sessions) + actually_fully_deleted_sessions.update(fully_deleted_sessions) + + return SessionAdjustmentResult( + sorted(actually_fully_deleted_sessions), + sorted(set(session_ids_to_partially_delete) - actually_fully_deleted_sessions), + ) diff --git a/apps/worker/services/report/report_builder.py b/apps/worker/services/report/report_builder.py new file mode 100644 index 0000000000..c12318c15e --- /dev/null +++ b/apps/worker/services/report/report_builder.py @@ -0,0 +1,270 @@ +import dataclasses +import logging +from enum import Enum +from typing import Any, List, Sequence + +from shared.reports.reportfile import ReportFile +from shared.reports.resources import Report +from shared.reports.types import CoverageDatapoint, LineSession, ReportLine +from shared.yaml.user_yaml import UserYaml + +from helpers.labels import SpecialLabelsEnum +from services.path_fixer import PathFixer +from services.yaml.reader import read_yaml_field + +log = logging.getLogger(__name__) + + +class CoverageType(Enum): + line = ("line", None) + branch = ("branch", "b") + method = ("method", "m") + + def __init__(self, code, report_value): + self.code = code + self.report_value = report_value + + def map_to_string(self): + return self.report_value + + +class ReportBuilderSession(object): + def __init__( + self, + report_builder: "ReportBuilder", + report_filepath: str, + ): + self.filepath = report_filepath + self._report_builder = report_builder + self._report = Report() + self._present_labels = set() + + @property + def path_fixer(self): + return self._report_builder.path_fixer + + def resolve_paths(self, paths): + return self._report.resolve_paths(paths) + + def yaml_field(self, keys: Sequence[str], default: Any = None) -> Any: + return read_yaml_field(self._report_builder.current_yaml, keys, default) + + def get_file(self, filename: str) -> ReportFile | None: + return self._report.get(filename) + + def append(self, file: ReportFile): + if file is not None: + for line_number, line in file.lines: + if line.datapoints: + for datapoint in line.datapoints: + if datapoint.label_ids: + for label in datapoint.label_ids: + self._present_labels.add(label) + return self._report.append(file) + + def output_report(self) -> Report: + """ + Outputs a Report. + This function applies all the needed modifications before a report + can be output + + Returns: + Report: The legacy report desired + """ + if self._present_labels: + if self._present_labels == { + SpecialLabelsEnum.CODECOV_ALL_LABELS_PLACEHOLDER + }: + log.warning( + "Report only has SpecialLabels. Might indicate it was not generated with contexts" + ) + for file in self._report: + for line_number, line in file.lines: + self._possibly_modify_line_to_account_for_special_labels( + file, line_number, line + ) + self._report._totals = None + return self._report + + def _possibly_modify_line_to_account_for_special_labels( + self, file: ReportFile, line_number: int, line: ReportLine + ) -> None: + """Possibly modify the report line in the file + to account for any label in the SpecialLabelsEnum + + Args: + file (ReportFile): The file we want to modify + line_number (int): The line number in case we + need to set the new line back into the files + line (ReportLine): The original line + """ + if not line.datapoints: + return + + new_datapoints = [ + item + for datapoint in line.datapoints + for item in self._possibly_convert_datapoints(datapoint) + ] + if new_datapoints and new_datapoints != line.datapoints: + # A check to avoid unnecessary replacement + file[line_number] = dataclasses.replace( + line, + datapoints=sorted( + new_datapoints, + key=lambda x: ( + x.sessionid, + x.coverage, + x.coverage_type, + ), + ), + ) + file._totals = None + + def _possibly_convert_datapoints( + self, datapoint: CoverageDatapoint + ) -> List[CoverageDatapoint]: + """Possibly convert datapoints + The datapoint that might need to be converted + + Args: + datapoint (CoverageDatapoint): The datapoint to convert + """ + if datapoint.label_ids and any( + label == SpecialLabelsEnum.CODECOV_ALL_LABELS_PLACEHOLDER + for label in datapoint.label_ids + ): + new_label = ( + SpecialLabelsEnum.CODECOV_ALL_LABELS_PLACEHOLDER.corresponding_label + ) + return [ + dataclasses.replace( + datapoint, + label_ids=sorted( + set( + [ + label + for label in datapoint.label_ids + if label + != SpecialLabelsEnum.CODECOV_ALL_LABELS_PLACEHOLDER + ] + + [new_label] + ) + ), + ) + ] + return [datapoint] + + def create_coverage_file( + self, path: str, do_fix_path: bool = True + ) -> ReportFile | None: + fixed_path = self._report_builder.path_fixer(path) if do_fix_path else path + if not fixed_path: + return None + + return ReportFile( + fixed_path, ignore=self._report_builder.ignored_lines.get(fixed_path) + ) + + def create_coverage_line( + self, + coverage: int | str, + coverage_type: CoverageType | None = None, + labels_list_of_lists: list[list[str | SpecialLabelsEnum]] + | list[list[int]] + | None = None, + partials=None, + missing_branches=None, + complexity=None, + ) -> ReportLine: + sessionid = self._report_builder.sessionid + coverage_type_str = coverage_type.map_to_string() if coverage_type else None + datapoints = ( + [ + CoverageDatapoint( + sessionid=sessionid, + coverage=coverage, + coverage_type=coverage_type_str, + label_ids=label_ids, + ) + # Avoid creating datapoints that don't contain any labels + for label_ids in (labels_list_of_lists or []) + if label_ids + ] + if self._report_builder._supports_labels + else None + ) + return ReportLine.create( + coverage=coverage, + type=coverage_type_str, + sessions=[ + ( + LineSession( + id=sessionid, + coverage=coverage, + branches=missing_branches, + partials=partials, + complexity=complexity, + ) + ) + ], + datapoints=datapoints, + complexity=complexity, + ) + + +class ReportBuilder(object): + def __init__( + self, + current_yaml: UserYaml, + sessionid: int, + ignored_lines: dict, + path_fixer: PathFixer, + ): + self.current_yaml = current_yaml + self.sessionid = sessionid + self.ignored_lines = ignored_lines + self.path_fixer = path_fixer + self._supports_labels = self.supports_labels() + + def create_report_builder_session(self, filepath) -> ReportBuilderSession: + return ReportBuilderSession(self, filepath) + + def supports_labels(self) -> bool: + """Returns wether a report supports labels. + This is true if the client has configured some flag with carryforward_mode == "labels" + """ + if self.current_yaml is None or self.current_yaml == {}: + return False + old_flag_style = self.current_yaml.get("flags") + flag_management = self.current_yaml.get("flag_management") + # Check if some of the old style flags uses labels + old_flag_with_carryforward_labels = False + if old_flag_style: + old_flag_with_carryforward_labels = any( + map( + lambda flag_definition: flag_definition.get("carryforward_mode") + == "labels", + old_flag_style.values(), + ) + ) + # Check if some of the flags or default rules use labels + flag_management_default_rule_carryforward_labels = False + flag_management_flag_with_carryforward_labels = False + if flag_management: + flag_management_default_rule_carryforward_labels = ( + flag_management.get("default_rules", {}).get("carryforward_mode") + == "labels" + ) + flag_management_flag_with_carryforward_labels = any( + map( + lambda flag_definition: flag_definition.get("carryforward_mode") + == "labels", + flag_management.get("individual_flags", []), + ) + ) + return ( + old_flag_with_carryforward_labels + or flag_management_default_rule_carryforward_labels + or flag_management_flag_with_carryforward_labels + ) diff --git a/apps/worker/services/report/report_processor.py b/apps/worker/services/report/report_processor.py new file mode 100644 index 0000000000..3326168abd --- /dev/null +++ b/apps/worker/services/report/report_processor.py @@ -0,0 +1,243 @@ +import logging +from typing import Literal + +import orjson +import sentry_sdk +from lxml import etree +from shared.metrics import Counter, Histogram +from shared.reports.resources import Report + +from helpers.exceptions import CorruptRawReportError +from helpers.metrics import KiB, MiB +from services.report.languages.base import BaseLanguageProcessor +from services.report.languages.helpers import remove_non_ascii +from services.report.parser.types import ParsedUploadedReportFile +from services.report.report_builder import ReportBuilder + +from .languages.bullseye import BullseyeProcessor +from .languages.clover import CloverProcessor +from .languages.cobertura import CoberturaProcessor +from .languages.coveralls import CoverallsProcessor +from .languages.csharp import CSharpProcessor +from .languages.dlst import DLSTProcessor +from .languages.elm import ElmProcessor +from .languages.flowcover import FlowcoverProcessor +from .languages.gap import GapProcessor +from .languages.gcov import GcovProcessor +from .languages.go import GoProcessor +from .languages.jacoco import JacocoProcessor +from .languages.jetbrainsxml import JetBrainsXMLProcessor +from .languages.lcov import LcovProcessor +from .languages.lua import LuaProcessor +from .languages.mono import MonoProcessor +from .languages.node import NodeProcessor +from .languages.pycoverage import PyCoverageProcessor +from .languages.rlang import RlangProcessor +from .languages.salesforce import SalesforceProcessor +from .languages.scala import ScalaProcessor +from .languages.scoverage import SCoverageProcessor +from .languages.simplecov import SimplecovProcessor +from .languages.v1 import VOneProcessor +from .languages.vb import VbProcessor +from .languages.vb2 import VbTwoProcessor +from .languages.xcode import XCodeProcessor +from .languages.xcodeplist import XCodePlistProcessor + +log = logging.getLogger(__name__) + + +RAW_REPORT_PROCESSOR_RUNTIME_SECONDS = Histogram( + "worker_services_report_raw_processor_duration_seconds", + "Time it takes (in seconds) for a raw report processor to run", + ["processor"], + buckets=[0.05, 0.1, 0.5, 1, 2, 5, 7.5, 10, 15, 20, 30, 60, 120, 180, 300, 600, 900], +) + +RAW_REPORT_SIZE = Histogram( + "worker_services_report_raw_report_size", + "Size (in bytes) of a raw report", + ["processor"], + buckets=[ + 10 * KiB, + 100 * KiB, + 200 * KiB, + 500 * KiB, + 1 * MiB, + 2 * MiB, + 5 * MiB, + 10 * MiB, + 20 * MiB, + 50 * MiB, + 100 * MiB, + 200 * MiB, + ], +) + +RAW_REPORT_PROCESSOR_COUNTER = Counter( + "worker_services_report_raw_processor_runs", + "Number of times a raw report processor was run and with what result", + ["processor", "result"], +) + + +@sentry_sdk.trace +def report_type_matching( + report: ParsedUploadedReportFile, first_line: str +) -> ( + tuple[bytes, Literal["txt"] | Literal["plist"]] + | tuple[dict | list, Literal["json"]] + | tuple[etree.Element, Literal["xml"]] +): + name = report.filename or "" + raw_report = report.contents + xcode_first_line_endings = ( + ".h:", + ".m:", + ".swift:", + ".hpp:", + ".cpp:", + ".cxx:", + ".c:", + ".C:", + ".cc:", + ".cxx:", + ".c++:", + ) + xcode_filename_endings = ( + "app.coverage.txt", + "framework.coverage.txt", + "xctest.coverage.txt", + ) + if first_line.endswith(xcode_first_line_endings) or name.endswith( + xcode_filename_endings + ): + return raw_report, "txt" + if raw_report.find(b'') >= 0 or name.endswith(".plist"): + return raw_report, "plist" + if not raw_report: + return raw_report, "txt" + + try: + processed = orjson.loads(raw_report) + if isinstance(processed, dict) or isinstance(processed, list): + return processed, "json" + except ValueError: + pass + + try: + parser = etree.XMLParser(recover=True, resolve_entities=False) + processed = etree.fromstring(raw_report, parser=parser) + if processed is not None and len(processed) > 0: + return processed, "xml" + except (ValueError, etree.XMLSyntaxError): + pass + + return raw_report, "txt" + + +def process_report( + report: ParsedUploadedReportFile, report_builder: ReportBuilder +) -> Report | None: + report_filename = report.filename or "" + first_line = remove_non_ascii(report.get_first_line().decode(errors="replace")) + raw_report = report.contents + + if b"" in raw_report: + log.warning( + "Ignored report", + extra=dict(report_filename=report_filename, first_line=first_line[:100]), + ) + return None + + parsed_report, report_type = report_type_matching(report, first_line) + + processors: list[BaseLanguageProcessor] = [] + if report_type == "plist": + processors = [XCodePlistProcessor()] + elif report_type == "xml": + processors = [ + BullseyeProcessor(), + SCoverageProcessor(), + JetBrainsXMLProcessor(), + CloverProcessor(), + MonoProcessor(), + CSharpProcessor(), + JacocoProcessor(), + VbProcessor(), + VbTwoProcessor(), + CoberturaProcessor(), + ] + elif report_type == "txt": + if parsed_report[-11:] == b"has no code": + # empty [dlst] + return None + processors = [ + LcovProcessor(), + GcovProcessor(), + LuaProcessor(), + GapProcessor(), + DLSTProcessor(), + GoProcessor(), + XCodeProcessor(), + ] + elif report_type == "json" and parsed_report: + processors = [ + SalesforceProcessor(), + ElmProcessor(), + RlangProcessor(), + FlowcoverProcessor(), + VOneProcessor(), + ScalaProcessor(), + CoverallsProcessor(), + SimplecovProcessor(), + GapProcessor(), + PyCoverageProcessor(), + NodeProcessor(), + ] + + for processor in processors: + if not processor.matches_content(parsed_report, first_line, report_filename): + continue + processor_name = type(processor).__name__ + + RAW_REPORT_SIZE.labels(processor=processor_name).observe(report.size) + with RAW_REPORT_PROCESSOR_RUNTIME_SECONDS.labels( + processor=processor_name + ).time(): + try: + report_builder_session = report_builder.create_report_builder_session( + report_filename + ) + processor.process(parsed_report, report_builder_session) + RAW_REPORT_PROCESSOR_COUNTER.labels( + processor=processor_name, result="success" + ).inc() + return report_builder_session.output_report() + except CorruptRawReportError as e: + log.warning( + "Processor matched file but later a problem with file was discovered", + extra=dict( + processor_name=processor_name, + expected_format=e.expected_format, + corruption_error=e.corruption_error, + ), + exc_info=True, + ) + RAW_REPORT_PROCESSOR_COUNTER.labels( + processor=processor_name, result="corrupt_raw_report" + ).inc() + return None + except Exception: + RAW_REPORT_PROCESSOR_COUNTER.labels( + processor=processor_name, result="failure" + ).inc() + raise + log.warning( + "File format could not be recognized", + extra=dict( + report_filename=report_filename, + first_line=first_line[:100], + report_type=report_type, + ), + ) + return None diff --git a/apps/worker/services/report/tests/unit/report.v3.json b/apps/worker/services/report/tests/unit/report.v3.json new file mode 100644 index 0000000000..83a8e6a135 --- /dev/null +++ b/apps/worker/services/report/tests/unit/report.v3.json @@ -0,0 +1,100 @@ +{ + "files": { + "folder/empty/c.py": [0, [0, 1, 1, 1, 1, "100", 1, 1, 1, 0]], + "folder/empty/e.py": [1, [0, 1, 1, 1, 1, "100", 1, 1, 1, 0]], + "folder/empty/b.py": [2, [0, 1, 1, 1, 1, "100", 1, 1, 1, 0]], + "folder/example.py": [3, [0, 9, 6, 1, 2, "66.66666", 0, 0, 0, 0]], + "folder/empty/a.py": [4, [0, 1, 1, 1, 1, "100", 1, 1, 1, 0]], + "folder/empty/d.py": [5, [0, 1, 1, 1, 1, "100", 1, 1, 1, 0]] + }, + "sessions": { + "0": { + "c": "travis", + "e": { + "PYTHON_VERSION": "2.7" + }, + "d": 1449324775, + "f": [ + "api", + "docker" + ], + "n": "1235", + "p": true, + "u": "http://", + "t": { + "M": 0, + "c": "100", + "b": 0, + "d": 0, + "f": 1, + "h": 6, + "m": 1, + "n": 9, + "p": 2 + } + }, + "1": { + "c": "circleci", + "e": { + "PYTHON_VERSION": "2.7.8" + }, + "d": 1449324775, + "f": [ + "unittests", + "docker" + ], + "n": "116", + "p": false, + "u": "http://", + "t": { + "M": 0, + "c": "100", + "b": 0, + "d": 0, + "f": 1, + "h": 6, + "m": 1, + "n": 9, + "p": 2 + } + }, + "2": { + "c": "circleci", + "d": 1449324775, + "n": "117", + "p": false, + "u": "http://", + "t": { + "M": 0, + "c": "100", + "b": 0, + "d": 0, + "f": 1, + "h": 6, + "m": 1, + "n": 9, + "p": 2 + } + } + }, + "totals": { + "f": 6, + "n": 9, + "p": 2, + "h": 6, + "m": 1, + "s": 3, + "b": 0, + "d": 0, + "M": 0, + "c": "100" + }, + "chunks": [ + "\n[1, null, [[1, 1, null]], null]", + "\n[1, null, [[1, 1, null]], null]", + "\n[1, null, [[1, 1, null]], null]", + "\n[1, null, [[1, 1, null], [2, 0, null]], null]\n[0, null, [[1, 0, null]], null]\n[\"1/2\", \"b\", [[1, \"1/2\", null]], null]\n\n[1, \"b\", [[1, 1, null]], null]\n\n\n\n[\"1/2\", \"b\", [[1, \"1/2\", [\"exit\"]], [3, 0, null], [2, \"1/2\", null, [[0, 5, 0], [5, 10, 1]]]], null]", + "\n[1, null, [[1, 1, null]], null]", + "\n[1, null, [[1, 1, null]], null]" + ] +} diff --git a/apps/worker/services/report/tests/unit/test_fixes.py b/apps/worker/services/report/tests/unit/test_fixes.py new file mode 100644 index 0000000000..6784af44f8 --- /dev/null +++ b/apps/worker/services/report/tests/unit/test_fixes.py @@ -0,0 +1,76 @@ +import unittest + +from services.report import fixes + + +class TestFixes(unittest.TestCase): + def test_fixes(self): + res = fixes.get_fixes_from_raw( + "\n".join( + ( + "./file.kt:2:/*", + "", + "EOF: 188 ./file.kt", + "file.go:20: ", + "file.go:x:", + "file.go", + "lcov:10: // LCOV_EXCL_START", + "lcov:21:// LCOV_EXCL_STOP", + "file.go:21: /* ", + "file.php:23:", + "file.go:23: */ ", + "file.go:50: /* ", + "file.go:52: */ ", + "file.php:17: {", + ) + ), + lambda a: a.replace("./", ""), + ) + assert res == { + "file.kt": {"eof": 188, "lines": set([2])}, + "lcov": {"lines": set([10, 21] + list(range(11, 21)))}, + "file.go": {"lines": set([20, 21, 23, 50, 52, 22, 51])}, + "file.php": {"lines": set([23, 17])}, + } + + def test_fixes_multiple(self): + res = fixes.get_fixes_from_raw("file:1,2,3", str) + assert res == {"file": {"lines": set([1, 2, 3])}} + + def test_fixes_single(self): + res = fixes.get_fixes_from_raw("file:1:a", str) + assert res == {"file": {"lines": set([1])}} + + def test_fixes_lcov(self): + res = fixes.get_fixes_from_raw( + "file:1:LCOV_EXCL_START\nfile:5:LCOV_EXCL_STOP", str + ) + assert res == {"file": {"lines": set([1, 5, 2, 3, 4])}} + + def test_fixes_comment(self): + res = fixes.get_fixes_from_raw("file:1:/*\nfile:5:*/", str) + assert res == {"file": {"lines": set([1, 5, 2, 3, 4])}} + + def test_get_fixes_from_raw_with_both_eof_and_lines(self): + content = [ + "./src/main/kotlin/codecov/index.kt:8,12,16", + "./src/main/kotlin/codecov/Request.kt:33,37,38,40", + "./src/test/kotlin/codecov/test_index.kt:13,16,17", + "EOF: 17 ./src/main/kotlin/codecov/index.kt", + "EOF: 40 ./src/main/kotlin/codecov/Request.kt", + "EOF: 18 ./src/test/kotlin/codecov/test_index.kt", + ] + content = "\n".join(content) + res = fixes.get_fixes_from_raw(content, lambda x: x) + expected_result = { + "./src/main/kotlin/codecov/Request.kt": { + "eof": 40, + "lines": {40, 33, 37, 38}, + }, + "./src/main/kotlin/codecov/index.kt": {"eof": 17, "lines": {8, 16, 12}}, + "./src/test/kotlin/codecov/test_index.kt": { + "eof": 18, + "lines": {16, 17, 13}, + }, + } + assert expected_result == res diff --git a/apps/worker/services/report/tests/unit/test_parser.py b/apps/worker/services/report/tests/unit/test_parser.py new file mode 100644 index 0000000000..01288d2670 --- /dev/null +++ b/apps/worker/services/report/tests/unit/test_parser.py @@ -0,0 +1,551 @@ +from services.report.parser import LegacyReportParser + +simple_content = b"""./codecov.yaml +Makefile +awesome/__init__.py +awesome/code_fib.py +dev.sh +tests/__init__.py +tests/test_number_two.py +tests/test_sample.py +unit.coverage.xml +<<<<<< network +# path=flagtwo.coverage.xml + + + + + + /Users/thiagorramos/Projects/clientenv/example-python + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +<<<<<< EOF""" + + +simple_no_toc = b""" +# path=flagtwo.coverage.xml + + + + + + /Users/thiagorramos/Projects/clientenv/example-python + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +<<<<<< EOF""" + +more_complex = b"""PRODUCTION_TOKEN=aaaaaaaa-e799-40d4-b89d-cea4b18f29a4 +<<<<<< ENV +./codecov.yaml +Makefile +awesome/__init__.py +awesome/code_fib.py +dev.sh +tests/__init__.py +tests/test_number_two.py +tests/test_sample.py +unit.coverage.xml +<<<<<< network +# path=unit.coverage.xml + + + + + + /Users/thiagorramos/Projects/clientenv/example-python + + + + + + + + + + + + + + + + + + + + + + +<<<<<< EOF + + + + + + /Users/thiagorramos/Projects/clientenv/example-python + + + + + + + + + + + + + + + + + + + + + +<<<<<< EOF""" + +more_complex_with_line_end = b"""PRODUCTION_TOKEN=aaaaaaaa-e799-40d4-b89d-cea4b18f29a4 +<<<<<< ENV +./codecov.yaml +Makefile +awesome/__init__.py +awesome/code_fib.py +dev.sh +tests/__init__.py +tests/test_number_two.py +tests/test_sample.py +unit.coverage.xml +<<<<<< network +# path=unit.coverage.xml + + + + + +<<<<<< EOF +# path=profile.cov +mode: count +github.com/path/ckey/key.go:43.38,47.2 1 1 +github.com/path/ckey/key.go:51.82,58.45 2 12 +github.com/path/ckey/key.go:64.2,66.33 3 12 +github.com/path/ckey/key.go:70.2,72.41 2 12 +github.com/path/ckey/key.go:58.45,60.3 1 12 +github.com/path/ckey/key.go:60.8,62.3 1 0 +<<<<<< EOF +""" + +line_end_no_line_break = b"""PRODUCTION_TOKEN=aaaaaaaa-e799-40d4-b89d-cea4b18f29a4 +<<<<<< ENV +./codecov.yaml +Makefile +awesome/__init__.py +awesome/code_fib.py +dev.sh +tests/__init__.py +tests/test_number_two.py +tests/test_sample.py +unit.coverage.xml +<<<<<< network +# path=unit.coverage.xml + + + + +<<<<<< EOF +# path=profile.cov +mode: count +github.com/path/ckey/key.go:43.38,47.2 1 1 +github.com/path/ckey/key.go:51.82,58.45 2 12 +github.com/path/ckey/key.go:64.2,66.33 3 12 +github.com/path/ckey/key.go:70.2,72.41 2 12 +github.com/path/ckey/key.go:58.45,60.3 1 12 +github.com/path/ckey/key.go:60.8,62.3 1 0<<<<<< EOF +""" + +cases_little_mor_ridiculous = b"""PRODUCTION_TOKEN=aaaaaaaa-e799-40d4-b89d-cea4b18f29a4 +<<<<<< ENV +./codecov.yaml +Makefile +awesome/__init__.py +awesome/code_fib.py +dev.sh +tests/__init__.py +tests/test_number_two.py +tests/test_sample.py +unit.coverage.xml +<<<<<< network +# path=unit.coverage.xml + + +<<<<<<< EOF# path=profile.cov +mode: count +github.com/path/ckey/key.go:43.38,47.2 1 1 +github.com/path/ckey/key.go:51.82,58.45 2 12 +github.com/path/ckey/key.go:64.2,66.33 3 12 +github.com/path/ckey/key.go:70.2,72.41 2 12 +github.com/path/ckey/key.go:58.45,60.3 1 12 +github.com/path/ckey/key.go:60.8,62.3 1 0\r + + + + + + + + + + +<<<<<< EOF +""" + +cases_no_eof_end = b"""PRODUCTION_TOKEN=aaaaaaaa-e799-40d4-b89d-cea4b18f29a4 +<<<<<< ENV +./codecov.yaml +Makefile +awesome/__init__.py +awesome/code_fib.py +dev.sh +tests/__init__.py +tests/test_number_two.py +tests/test_sample.py +unit.coverage.xml +<<<<<< network +# path=unit.coverage.xml + + +<<<<<<< EOF# path=profile.cov +mode: count +github.com/path/ckey/key.go:43.38,47.2 1 1""" + +cases_emptylines_betweenpath_and_content = b"""p2/redis/b_test.go +p1/driver.go +p1/driver_test.go +p1/options.go +p2/a.go +p2/a_test.go +<<<<<< network +# path=coverage.txt + +mode: count +mode: count +github.com/mypath/bugsbunny.go:10.33,13.20 1 3 +github.com/mypath/bugsbunny.go:19.2,20.36 2 2 +github.com/mypath/bugsbunny.go:26.2,26.22 1 2 +github.com/mypath/bugsbunny.go:31.2,38.16 3 2 +github.com/mypath/bugsbunny.go:41.2,43.12 2 2 +github.com/mypath/bugsbunny.go:13.20,16.3 2 1 +github.com/mypath/bugsbunny.go:20.36,22.17 2 1""" + + +class TestParser(object): + def test_parser_with_toc(self): + res = LegacyReportParser().parse_raw_report_from_bytes(simple_content) + assert res.has_toc() + assert not res.has_env() + assert not res.has_report_fixes() + assert len(res.uploaded_files) == 1 + + def test_parser_no_toc(self): + res = LegacyReportParser().parse_raw_report_from_bytes(simple_no_toc) + assert not res.has_toc() + assert not res.has_env() + assert not res.has_report_fixes() + assert len(res.uploaded_files) == 1 + + def test_parser_more_complete(self): + res = LegacyReportParser().parse_raw_report_from_bytes(more_complex) + assert res.has_toc() + assert res.has_env() + assert not res.has_report_fixes() + assert len(res.uploaded_files) == 2 + assert res.uploaded_files[0].filename == "unit.coverage.xml" + assert res.uploaded_files[0].contents.startswith(b'') + assert res.uploaded_files[0].contents.endswith(b"") + assert res.uploaded_files[1].filename is None + assert res.uploaded_files[1].contents.startswith(b'') + assert res.uploaded_files[1].contents.endswith(b"") + + def test_parser_more_complete_with_line_end(self): + res = LegacyReportParser().parse_raw_report_from_bytes( + more_complex_with_line_end + ) + assert res.has_toc() + assert res.has_env() + assert not res.has_report_fixes() + assert len(res.uploaded_files) == 2 + assert res.uploaded_files[0].filename == "unit.coverage.xml" + assert ( + res.uploaded_files[0].contents + == b'\n\n \n \n' + ) + assert res.uploaded_files[1].filename == "profile.cov" + expected_second_file = b"\n".join( + [ + b"mode: count", + b"github.com/path/ckey/key.go:43.38,47.2 1 1", + b"github.com/path/ckey/key.go:51.82,58.45 2 12", + b"github.com/path/ckey/key.go:64.2,66.33 3 12", + b"github.com/path/ckey/key.go:70.2,72.41 2 12", + b"github.com/path/ckey/key.go:58.45,60.3 1 12", + b"github.com/path/ckey/key.go:60.8,62.3 1 0", + ] + ) + assert res.uploaded_files[1].contents == expected_second_file + + def test_line_end_no_line_break(self): + res = LegacyReportParser().parse_raw_report_from_bytes(line_end_no_line_break) + assert res.has_toc() + assert res.has_env() + assert not res.has_report_fixes() + assert len(res.uploaded_files) == 2 + assert res.uploaded_files[0].filename == "unit.coverage.xml" + assert ( + res.uploaded_files[0].contents + == b'\n\n \n \n' + ) + assert res.uploaded_files[1].filename == "profile.cov" + expected_second_file = b"\n".join( + [ + b"mode: count", + b"github.com/path/ckey/key.go:43.38,47.2 1 1", + b"github.com/path/ckey/key.go:51.82,58.45 2 12", + b"github.com/path/ckey/key.go:64.2,66.33 3 12", + b"github.com/path/ckey/key.go:70.2,72.41 2 12", + b"github.com/path/ckey/key.go:58.45,60.3 1 12", + b"github.com/path/ckey/key.go:60.8,62.3 1 0", + ] + ) + assert res.uploaded_files[1].contents == expected_second_file + + def test_cases_little_mor_ridiculous(self): + res = LegacyReportParser().parse_raw_report_from_bytes( + cases_little_mor_ridiculous + ) + assert res.has_toc() + assert res.toc == b"\n".join( + [ + b"./codecov.yaml", + b"Makefile", + b"awesome/__init__.py", + b"awesome/code_fib.py", + b"dev.sh", + b"tests/__init__.py", + b"tests/test_number_two.py", + b"tests/test_sample.py", + b"unit.coverage.xml", + ] + ) + assert res.has_env() + assert not res.has_report_fixes() + assert len(res.uploaded_files) == 2 + assert res.uploaded_files[0].filename == "unit.coverage.xml" + assert ( + res.uploaded_files[0].contents + == b'\n\n<' + ) + assert res.uploaded_files[1].filename == "profile.cov" + expected_second_file = b"\n".join( + [ + b"mode: count", + b"github.com/path/ckey/key.go:43.38,47.2 1 1", + b"github.com/path/ckey/key.go:51.82,58.45 2 12", + b"github.com/path/ckey/key.go:64.2,66.33 3 12", + b"github.com/path/ckey/key.go:70.2,72.41 2 12", + b"github.com/path/ckey/key.go:58.45,60.3 1 12", + b"github.com/path/ckey/key.go:60.8,62.3 1 0", + ] + ) + assert res.uploaded_files[1].contents == expected_second_file + + def test_cases_no_eof_end(self): + res = LegacyReportParser().parse_raw_report_from_bytes(cases_no_eof_end) + assert res.has_toc() + assert res.toc == b"\n".join( + [ + b"./codecov.yaml", + b"Makefile", + b"awesome/__init__.py", + b"awesome/code_fib.py", + b"dev.sh", + b"tests/__init__.py", + b"tests/test_number_two.py", + b"tests/test_sample.py", + b"unit.coverage.xml", + ] + ) + assert res.has_env() + assert not res.has_report_fixes() + assert len(res.uploaded_files) == 2 + assert res.uploaded_files[0].filename == "unit.coverage.xml" + assert ( + res.uploaded_files[0].contents + == b'\n\n<' + ) + assert res.uploaded_files[1].filename == "profile.cov" + expected_second_file = b"\n".join( + [b"mode: count", b"github.com/path/ckey/key.go:43.38,47.2 1 1"] + ) + assert res.uploaded_files[1].contents == expected_second_file + + def test_cases_emptylines_betweenpath_and_content(self): + res = LegacyReportParser().parse_raw_report_from_bytes( + cases_emptylines_betweenpath_and_content + ) + assert res.has_toc() + assert res.toc == b"\n".join( + [ + b"p2/redis/b_test.go", + b"p1/driver.go", + b"p1/driver_test.go", + b"p1/options.go", + b"p2/a.go", + b"p2/a_test.go", + ] + ) + assert not res.has_env() + assert not res.has_report_fixes() + assert len(res.uploaded_files) == 1 + assert res.uploaded_files[0].filename == "coverage.txt" + assert ( + res.uploaded_files[0].contents + == b"""mode: count +mode: count +github.com/mypath/bugsbunny.go:10.33,13.20 1 3 +github.com/mypath/bugsbunny.go:19.2,20.36 2 2 +github.com/mypath/bugsbunny.go:26.2,26.22 1 2 +github.com/mypath/bugsbunny.go:31.2,38.16 3 2 +github.com/mypath/bugsbunny.go:41.2,43.12 2 2 +github.com/mypath/bugsbunny.go:13.20,16.3 2 1 +github.com/mypath/bugsbunny.go:20.36,22.17 2 1""" + ) + + def test_cases_compatibility_mode(self): + res = LegacyReportParser().parse_raw_report_from_bytes( + b"==FROMNOWONIGNOREDBYCODECOV==>>>".join([simple_content, more_complex]) + ) + would_be_simple_content_res = LegacyReportParser().parse_raw_report_from_bytes( + simple_content + ) + assert res.has_toc() + assert would_be_simple_content_res.has_toc() + assert res.toc == would_be_simple_content_res.toc + assert not res.has_env() + assert not would_be_simple_content_res.has_env() + assert not res.has_report_fixes() + assert not would_be_simple_content_res.has_report_fixes() + assert len(res.uploaded_files) == len( + would_be_simple_content_res.uploaded_files + ) + assert ( + res.uploaded_files[0].filename + == would_be_simple_content_res.uploaded_files[0].filename + ) + assert ( + res.uploaded_files[0].contents + == would_be_simple_content_res.uploaded_files[0].contents + ) + + def test_cases_compatibility_mode_failed_case(self): + res = LegacyReportParser().parse_raw_report_from_bytes( + b"==FROMNOWONIGNOREDBYCODECOV==>>>".join( + [simple_content, b"somemeaninglessstuff"] + ) + ) + would_be_simple_content_res = LegacyReportParser().parse_raw_report_from_bytes( + simple_content + ) + assert res.has_toc() + assert would_be_simple_content_res.has_toc() + assert res.toc == would_be_simple_content_res.toc + assert not res.has_env() + assert not would_be_simple_content_res.has_env() + assert not res.has_report_fixes() + assert not would_be_simple_content_res.has_report_fixes() + assert len(res.uploaded_files) == len( + would_be_simple_content_res.uploaded_files + ) + assert ( + res.uploaded_files[0].filename + == would_be_simple_content_res.uploaded_files[0].filename + ) + assert ( + res.uploaded_files[0].contents + == would_be_simple_content_res.uploaded_files[0].contents + ) diff --git a/apps/worker/services/report/tests/unit/test_process.py b/apps/worker/services/report/tests/unit/test_process.py new file mode 100644 index 0000000000..ecc5eea723 --- /dev/null +++ b/apps/worker/services/report/tests/unit/test_process.py @@ -0,0 +1,783 @@ +from json import loads +from unittest.mock import patch + +import pytest +from lxml import etree +from shared.reports.reportfile import ReportFile +from shared.reports.resources import Report +from shared.reports.types import LineSession, ReportLine, ReportTotals +from shared.utils.sessions import Session +from shared.yaml import UserYaml + +from helpers.exceptions import ( + CorruptRawReportError, + ReportEmptyError, + ReportExpiredException, +) +from services.report import raw_upload_processor as process +from services.report.parser import LegacyReportParser +from services.report.parser.types import LegacyParsedRawReport, ParsedUploadedReportFile +from services.report.report_builder import ReportBuilder +from test_utils.base import BaseTestCase + + +class TestProcessRawUpload(BaseTestCase): + @pytest.mark.parametrize("keys", ["nm", "n", "m", "nme", "ne", "M"]) + def test_process_raw_upload(self, keys): + report = [] + # add env + if "e" in keys: + report.append("A=b") + report.append("<<<<<< ENV") + + # add network + if "n" in keys: + report.append("path/to/file") + report.append("<<<<<< network") + + # add report + report.append("# path=app.coverage.txt") + report.append("/file:\n 1 | 1|line") + + if "m" in keys: + report.append("<<<<<< EOF") + report.append("# path=app.coverage.txt") + report.append("/file2:\n 1 | 1|line") + + parsed_report = LegacyReportParser().parse_raw_report_from_bytes( + "\n".join(report).encode() + ) + master = process.process_raw_upload(None, parsed_report, Session()) + + if "e" in keys: + assert master.sessions[0].env == {"A": "b"} + + assert master.totals.files == 1 + ( + 1 if ("m" in keys and "n" not in keys) else 0 + ) + assert master.totals.sessions == 1 + + if "n" in keys: + assert master.get("path/to/file") + assert master["path/to/file"][1].coverage == 1 + else: + assert master.get("file") + assert master["file"][1].coverage == 1 + assert ("file2" in master) is ("m" in keys and "n" not in keys) + + def test_process_raw_upload_skipped_files(self): + lcov_section = [ + "TN:", + "SF:file.js", + "FNDA:76,jsx", + "FN:76,(anonymous_1)", + "removed", + "DA:0,skipped", + "DA:null,skipped", + "DA:1,1", + "DA:=,=", + "BRDA:0,1,0,1", + "BRDA:0,1,0,1", + "BRDA:1,1,0,1", + "BRDA:1,1,1,1", + "end_of_record", + ] + json_section = [ + "{", + ' "coverage": {', + ' "source": [null, 1],', + ' "file": {"1": 1, "2": "1", "3": true, "4": "1/2"},', + ' "empty": {}', + " },", + ' "messages": {', + ' "source": {', + ' "1": "Message"', + " }", + " }", + "}", + ] + report = [] + report.append("# path=coverage/coverage.lcov") + report.extend(lcov_section) + report.append("<<<<<< EOF") + report.append("# path=coverage/coverage.json") + report.extend(json_section) + + parsed_report = LegacyReportParser().parse_raw_report_from_bytes( + "\n".join(report).encode() + ) + master = process.process_raw_upload({}, parsed_report, Session()) + assert master.files == ["source", "file"] + + def test_process_raw_upload_empty_report(self): + report_data = [] + report_data.append("# path=coverage/coverage.txt") + report_data.extend(["5"]) + report_data.append("<<<<<< EOF") + report_data.append("# path=coverage/other.txt") + report_data.append("json_section") + + original_report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + original_report.append(first_file) + original_report.append(second_file) + original_report.add_session(Session(flags=["unit"])) + assert len(original_report.sessions) == 1 + + parsed_report = LegacyReportParser().parse_raw_report_from_bytes( + "\n".join(report_data).encode() + ) + with pytest.raises(ReportEmptyError): + process.process_raw_upload( + UserYaml({}), parsed_report, Session(flags=["fruits"]) + ) + assert len(original_report.sessions) == 1 + assert sorted(original_report.flags.keys()) == ["unit"] + assert original_report.files == ["file_1.go", "file_2.py"] + assert original_report.flags["unit"].totals == ReportTotals( + files=2, + lines=10, + hits=10, + misses=0, + partials=0, + coverage="100", + branches=1, + methods=0, + messages=0, + sessions=1, + complexity=0, + complexity_total=0, + diff=0, + ) + assert original_report.flags.get("fruits") is None + + report_json, _chunks, totals = original_report.serialize() + + assert totals == ReportTotals( + 2, 10, 6, 3, 1, "60.00000", 1, 0, 0, 1, 10, 2, None + ) + assert loads(report_json) == { + "files": { + "file_1.go": [ + 0, + [0, 8, 5, 3, 0, "62.50000", 0, 0, 0, 0, 10, 2, 0], + None, + None, + ], + "file_2.py": [ + 1, + [0, 2, 1, 0, 1, "50.00000", 1, 0, 0, 0, 0, 0, 0], + None, + None, + ], + }, + "sessions": { + "0": { + "t": None, + "d": None, + "a": None, + "f": ["unit"], + "c": None, + "n": None, + "N": None, + "j": None, + "u": None, + "p": None, + "e": None, + "st": "uploaded", + "se": {}, + } + }, + "totals": [2, 10, 6, 3, 1, "60.00000", 1, 0, 0, 1, 10, 2, None], + } + + def test_none(self): + with pytest.raises(ReportEmptyError, match="No files found in report."): + parsed_report = LegacyReportParser().parse_raw_report_from_bytes(b"") + process.process_raw_upload(None, parsed_report, Session()) + + +class TestProcessRawUploadFixed(BaseTestCase): + def test_fixes(self): + report_lines = [ + "# path=coverage.info", + "mode: count", + "file.go:7.14,9.2 1 1", + "<<<<<< EOF", + "# path=fixes", + "file.go:8:", + "<<<<<< EOF", + "", + ] + parsed_report = LegacyReportParser().parse_raw_report_from_bytes( + "\n".join(report_lines).encode() + ) + report = process.process_raw_upload({}, parsed_report, Session()) + + assert 2 not in report["file.go"], "2 never existed" + assert report["file.go"][7].coverage == 1 + assert 8 not in report["file.go"], "8 should have been removed" + assert 9 not in report["file.go"], "9 should have been removed" + + +class TestProcessRawUploadFlags(BaseTestCase): + @pytest.mark.parametrize( + "flag", + [{"paths": ["!tests/.*"]}, {"ignore": ["tests/.*"]}, {"paths": ["folder/"]}], + ) + def test_flags(self, flag): + parsed_report = LegacyReportParser().parse_raw_report_from_bytes( + b'{"coverage": {"tests/test.py": [null, 0], "folder/file.py": [null, 1]}}' + ) + master = process.process_raw_upload( + UserYaml({"flags": {"docker": flag}}), + parsed_report, + Session(flags=["docker"]), + ) + + assert master.files == ["folder/file.py"] + assert master.sessions[0].flags == ["docker"] + + +class TestProcessReport(BaseTestCase): + @pytest.mark.parametrize("report", [b"", b"" + ), + ), + ( + "xcode.from_txt", + ParsedUploadedReportFile( + filename="app.coverage.txt", file_contents=b"" + ), + ), + ( + "xcode.from_txt", + ParsedUploadedReportFile( + filename="/Users/path/to/framework.coverage.txt", + file_contents=b"", + ), + ), + ( + "xcode.from_txt", + ParsedUploadedReportFile( + filename="framework.coverage.txt", file_contents=b"" + ), + ), + ( + "xcode.from_txt", + ParsedUploadedReportFile( + filename="/Users/path/to/xctest.coverage.txt", + file_contents=b"", + ), + ), + ( + "xcode.from_txt", + ParsedUploadedReportFile( + filename="xctest.coverage.txt", file_contents=b"" + ), + ), + ( + "xcode.from_txt", + ParsedUploadedReportFile( + filename="coverage.txt", file_contents=b"/blah/app.h:\n" + ), + ), + ( + "dlst.from_string", + ParsedUploadedReportFile(filename=None, file_contents=b"data\ncovered"), + ), + ( + "vb.from_xml", + ParsedUploadedReportFile( + filename=None, + file_contents=b"", + ), + ), + ( + "lcov.from_txt", + ParsedUploadedReportFile( + filename=None, file_contents=b"content\nend_of_record" + ), + ), + ( + "gcov.from_txt", + ParsedUploadedReportFile( + filename=None, file_contents=b"0:Source:\nline2" + ), + ), + ( + "lua.from_txt", + ParsedUploadedReportFile(filename=None, file_contents=b"======="), + ), + ( + "gap.from_string", + ParsedUploadedReportFile( + filename=None, file_contents=b'{"Type": "S", "File": "a"}' + ), + ), + ( + "gap.from_string", + ParsedUploadedReportFile( + filename=None, + file_contents=b'{"Type": "S", "File": "a"}\n{"Type":"R","Line":1,"FileId":37}', + ), + ), + ( + "v1.from_json", + ParsedUploadedReportFile( + filename=None, file_contents=b'{"RSpec": {"coverage": {}}}' + ), + ), + ( + "v1.from_json", + ParsedUploadedReportFile( + filename=None, file_contents=b'{"MiniTest": {"coverage": {}}}' + ), + ), + ( + "v1.from_json", + ParsedUploadedReportFile( + filename=None, file_contents=b'{"coverage": {}}' + ), + ), + ( + "v1.from_json", + ParsedUploadedReportFile( + filename=None, + file_contents=('{"coverage": {"data": "' + "\xf1" + '"}}').encode(), + ), + ), + ( + "rlang.from_json", + ParsedUploadedReportFile( + filename=None, file_contents=b'{"uploader": "R"}' + ), + ), + ( + "scala.from_json", + ParsedUploadedReportFile( + filename=None, file_contents=b'{"fileReports": ""}' + ), + ), + ( + "coveralls.from_json", + ParsedUploadedReportFile( + filename=None, file_contents=b'{"source_files": ""}' + ), + ), + ( + "node.from_json", + ParsedUploadedReportFile( + filename=None, file_contents=b'{"filename": {"branchMap": ""}}' + ), + ), + ( + "scoverage.from_xml", + ParsedUploadedReportFile( + filename=None, + file_contents=( + "" + "\xf1" + "" + ).encode(), + ), + ), + ( + "clover.from_xml", + ParsedUploadedReportFile( + filename=None, + file_contents=b'', + ), + ), + ( + "cobertura.from_xml", + ParsedUploadedReportFile( + filename=None, + file_contents=b"", + ), + ), + ( + "csharp.from_xml", + ParsedUploadedReportFile( + filename=None, + file_contents=b"", + ), + ), + ( + "jacoco.from_xml", + ParsedUploadedReportFile( + filename=None, + file_contents=b"", + ), + ), + ( + "xcodeplist.from_xml", + ParsedUploadedReportFile( + filename=None, + file_contents=b'\n', + ), + ), + ( + "xcodeplist.from_xml", + ParsedUploadedReportFile( + filename="3CB41F9A-1DEA-4DE1-B321-6F462C460DB6.xccoverage.plist", + file_contents=b"__", + ), + ), + ( + "scoverage.from_xml", + ParsedUploadedReportFile( + filename=None, + file_contents=b'\n', + ), + ), + ( + "clover.from_xml", + ParsedUploadedReportFile( + filename=None, + file_contents=b'\n', + ), + ), + ( + "cobertura.from_xml", + ParsedUploadedReportFile( + filename=None, + file_contents=b'\n', + ), + ), + ( + "csharp.from_xml", + ParsedUploadedReportFile( + filename=None, + file_contents=b'\n', + ), + ), + ( + "jacoco.from_xml", + ParsedUploadedReportFile( + filename=None, + file_contents=b'\n', + ), + ), + ( + "salesforce.from_json", + ParsedUploadedReportFile( + filename=None, file_contents=b'[{"name": "banana"}]' + ), + ), + ( + "bullseye.from_xml", + ParsedUploadedReportFile( + filename=None, + file_contents=b'', + ), + ), + ], + ) + def test_detect(self, lang, report): + with patch("services.report.languages.%s" % lang) as func: + res = process.process_report( + report=report, + report_builder=ReportBuilder( + current_yaml=None, sessionid=0, ignored_lines={}, path_fixer=str + ), + ) + + assert res is not None + assert func.called + + @pytest.mark.parametrize( + "report", + [(ParsedUploadedReportFile(filename=None, file_contents=b'[{"a": "banana"}]'))], + ) + def test_detect_nothing_found(self, report): + res = process.process_report( + report=report, + report_builder=ReportBuilder( + current_yaml=None, sessionid=0, ignored_lines={}, path_fixer=str + ), + ) + assert res is None + + def test_xxe_entity_not_called(self, mocker): + report_xxe_xml = b""" + + ]> + &xxe; + """ + func = mocker.patch("services.report.languages.scoverage.from_xml") + process.process_report( + report=ParsedUploadedReportFile( + filename="filename", file_contents=report_xxe_xml + ), + report_builder=ReportBuilder( + current_yaml=None, sessionid=0, ignored_lines={}, path_fixer=str + ), + ) + assert func.called + # should be from_xml(xml, report_builder_session). Don't have direct ref to builder_session, so using Mocker.ANY + func.assert_called_with(mocker.ANY, mocker.ANY) + expected_xml_string = "&xxe;" + output_xml_string = etree.tostring(func.call_args_list[0][0][0]).decode() + assert output_xml_string == expected_xml_string + + def test_format_not_recognized(self, mocker): + mocked = mocker.patch("services.report.report_processor.report_type_matching") + mocked.return_value = "bad_processing", "new_type" + r = ParsedUploadedReportFile( + filename="/Users/path/to/app.coverage.txt", + file_contents=b"", + ) + result = process.process_report( + report=r, + report_builder=ReportBuilder( + current_yaml=None, sessionid=0, ignored_lines={}, path_fixer=str + ), + ) + assert result is None + assert mocked.called + mocked.assert_called_with(r, "") + + def test_process_report_exception_raised(self, mocker): + class SpecialUnexpectedException(Exception): + pass + + mocker.patch( + "services.report.report_processor.report_type_matching", + return_value=(b"", "plist"), + ) + mocker.patch( + "services.report.report_processor.XCodePlistProcessor.matches_content", + return_value=True, + ) + mocker.patch( + "services.report.report_processor.XCodePlistProcessor.process", + side_effect=SpecialUnexpectedException(), + ) + + with pytest.raises(SpecialUnexpectedException): + process.process_report( + report=ParsedUploadedReportFile( + filename="/Users/path/to/app.coverage.txt", + file_contents=b"", + ), + report_builder=ReportBuilder( + current_yaml=None, sessionid=0, ignored_lines={}, path_fixer=str + ), + ) + + def test_process_report_corrupt_format(self, mocker): + mocker.patch( + "services.report.report_processor.report_type_matching", + return_value=(b"", "plist"), + ) + mocker.patch( + "services.report.report_processor.XCodePlistProcessor.matches_content", + return_value=True, + ) + mocker.patch( + "services.report.report_processor.XCodePlistProcessor.process", + side_effect=CorruptRawReportError("expected_format", "error_explanation"), + ) + + res = process.process_report( + report=ParsedUploadedReportFile( + filename="/Users/path/to/app.coverage.txt", + file_contents=b"", + ), + report_builder=ReportBuilder( + current_yaml=None, sessionid=0, ignored_lines={}, path_fixer=str + ), + ) + assert res is None + + def test_process_raw_upload_multiple_raw_reports(self, mocker): + first_raw_report_result = Report() + first_banana = ReportFile("banana.py") + first_banana.append(1, ReportLine.create(1, sessions=[LineSession(0, 1)])) + first_banana.append(2, ReportLine.create(0, sessions=[LineSession(0, 0)])) + first_raw_report_result.append(first_banana) + second_raw_report_result = Report() + second_banana = ReportFile("banana.py") + second_banana.append(2, ReportLine.create(1, sessions=[LineSession(0, 1)])) + second_banana.append(3, ReportLine.create(0, sessions=[LineSession(0, 0)])) + second_raw_report_result.append(second_banana) + second_another_file = ReportFile("another.c") + second_another_file.append( + 2, ReportLine.create(0, sessions=[LineSession(0, 0)]) + ) + second_another_file.append( + 3, ReportLine.create(1, sessions=[LineSession(0, 1)]) + ) + second_raw_report_result.append(second_another_file) + third_raw_report_result = Report() + third_banana = ReportFile("banana.py") + third_banana.append( + 3, ReportLine.create("1/2", sessions=[LineSession(0, "1/2")]) + ) + third_banana.append(5, ReportLine.create(0, sessions=[LineSession(0, 0)])) + third_raw_report_result.append(third_banana) + uploaded_reports = LegacyParsedRawReport( + toc=None, + env=None, + report_fixes=None, + uploaded_files=[ + ParsedUploadedReportFile( + filename="/Users/path/to/app.coverage.txt", + file_contents=b"", + ), + ParsedUploadedReportFile( + filename="/Users/path/to/app.coverage.txt", + file_contents=b"", + ), + ParsedUploadedReportFile( + filename="/Users/path/to/app.coverage.txt", + file_contents=b"", + ), + ], + ) + mocker.patch.object( + process, + "process_report", + side_effect=[ + first_raw_report_result, + second_raw_report_result, + third_raw_report_result, + ], + ) + session = Session(flags=["flag_one", "flag_two"]) + res = process.process_raw_upload(UserYaml({}), uploaded_reports, session) + + assert session.totals == ReportTotals( + files=2, + lines=6, + hits=3, + misses=2, + partials=1, + coverage="50.00000", + branches=0, + methods=0, + messages=0, + sessions=1, + complexity=0, + complexity_total=0, + diff=0, + ) + assert sorted(res.files) == ["another.c", "banana.py"] + assert res.get("banana.py").totals == ReportTotals( + files=0, + lines=4, + hits=2, + misses=1, + partials=1, + coverage="50.00000", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + assert res.get("another.c").totals == ReportTotals( + files=0, + lines=2, + hits=1, + misses=1, + partials=0, + coverage="50.00000", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + assert sorted(res.sessions.keys()) == [0] + assert res.sessions[0] == session + + def test_process_raw_upload_expired_report(self, mocker): + filename = "/Users/path/to/app.coverage.txt" + uploaded_reports = LegacyParsedRawReport( + toc=None, + env=None, + report_fixes=None, + uploaded_files=[ + ParsedUploadedReportFile( + filename="/Users/path/to/app.coverage.txt", + file_contents=b"", + ), + ], + ) + mocker.patch.object( + process, + "process_report", + side_effect=[ + ReportExpiredException(), + ], + ) + session = Session(flags=["flag_one", "flag_two"]) + with pytest.raises(ReportExpiredException) as e: + _ = process.process_raw_upload(UserYaml({}), uploaded_reports, session) + + assert e.value.filename == filename diff --git a/apps/worker/services/report/tests/unit/test_report_builder.py b/apps/worker/services/report/tests/unit/test_report_builder.py new file mode 100644 index 0000000000..ff403821dc --- /dev/null +++ b/apps/worker/services/report/tests/unit/test_report_builder.py @@ -0,0 +1,386 @@ +import pytest +from shared.reports.reportfile import ReportFile +from shared.reports.types import CoverageDatapoint, LineSession, ReportLine + +from services.report.report_builder import ( + CoverageType, + ReportBuilder, + SpecialLabelsEnum, +) + + +def test_report_builder_generate_session(mocker): + current_yaml, sessionid, ignored_lines, path_fixer = ( + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + ) + filepath = "filepath" + builder = ReportBuilder(current_yaml, sessionid, ignored_lines, path_fixer) + builder_session = builder.create_report_builder_session(filepath) + assert builder_session.path_fixer == path_fixer + + +def test_report_builder_session(mocker): + current_yaml, sessionid, ignored_lines, path_fixer = ( + {"beta_groups": ["labels"]}, + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + ) + filepath = "filepath" + builder = ReportBuilder(current_yaml, sessionid, ignored_lines, path_fixer) + builder_session = builder.create_report_builder_session(filepath) + first_file = ReportFile("filename.py") + first_file.append(2, ReportLine.create(coverage=0)) + first_file.append( + 3, + ReportLine.create( + coverage=0, + datapoints=[ + CoverageDatapoint( + sessionid=0, + coverage=1, + coverage_type=None, + label_ids=[SpecialLabelsEnum.CODECOV_ALL_LABELS_PLACEHOLDER], + ) + ], + ), + ) + first_file.append( + 10, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + ( + LineSession( + id=0, + coverage=1, + ) + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=0, + coverage=1, + coverage_type=None, + label_ids=["some_label", "other"], + ), + CoverageDatapoint( + sessionid=0, + coverage=1, + coverage_type=None, + label_ids=None, + ), + ], + complexity=None, + ), + ) + builder_session.append(first_file) + final_report = builder_session.output_report() + assert final_report.files == ["filename.py"] + assert sorted(final_report.get("filename.py").lines) == [ + ( + 2, + ReportLine.create( + coverage=0, type=None, sessions=None, datapoints=None, complexity=None + ), + ), + ( + 3, + ReportLine.create( + coverage=0, + type=None, + sessions=None, + datapoints=[ + CoverageDatapoint( + sessionid=0, + coverage=1, + coverage_type=None, + label_ids=["Th2dMtk4M_codecov"], + ), + ], + complexity=None, + ), + ), + ( + 10, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + LineSession( + id=0, coverage=1, branches=None, partials=None, complexity=None + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=0, + coverage=1, + coverage_type=None, + label_ids=["some_label", "other"], + ), + CoverageDatapoint( + sessionid=0, + coverage=1, + coverage_type=None, + label_ids=None, + ), + ], + complexity=None, + ), + ), + ] + + +def test_report_builder_session_only_all_labels(mocker): + current_yaml, sessionid, ignored_lines, path_fixer = ( + {}, + mocker.MagicMock(), + mocker.MagicMock(), + mocker.MagicMock(), + ) + filepath = "filepath" + builder = ReportBuilder(current_yaml, sessionid, ignored_lines, path_fixer) + builder_session = builder.create_report_builder_session(filepath) + first_file = ReportFile("filename.py") + first_file.append(2, ReportLine.create(coverage=0)) + first_file.append( + 3, + ReportLine.create( + coverage=0, + datapoints=[ + CoverageDatapoint( + sessionid=0, + coverage=1, + coverage_type=None, + label_ids=[SpecialLabelsEnum.CODECOV_ALL_LABELS_PLACEHOLDER], + ) + ], + ), + ) + first_file.append( + 10, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + ( + LineSession( + id=0, + coverage=1, + ) + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=0, + coverage=1, + coverage_type=None, + label_ids=[SpecialLabelsEnum.CODECOV_ALL_LABELS_PLACEHOLDER], + ), + CoverageDatapoint( + sessionid=0, + coverage=1, + coverage_type=None, + label_ids=None, + ), + ], + complexity=None, + ), + ) + builder_session.append(first_file) + final_report = builder_session.output_report() + assert final_report.files == ["filename.py"] + assert sorted(final_report.get("filename.py").lines) == [ + ( + 2, + ReportLine.create( + coverage=0, type=None, sessions=None, datapoints=None, complexity=None + ), + ), + ( + 3, + ReportLine.create( + coverage=0, + type=None, + sessions=None, + datapoints=[ + CoverageDatapoint( + sessionid=0, + coverage=1, + coverage_type=None, + label_ids=["Th2dMtk4M_codecov"], + ), + ], + complexity=None, + ), + ), + ( + 10, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + LineSession( + id=0, coverage=1, branches=None, partials=None, complexity=None + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=0, + coverage=1, + coverage_type=None, + label_ids=["Th2dMtk4M_codecov"], + ), + CoverageDatapoint( + sessionid=0, + coverage=1, + coverage_type=None, + label_ids=None, + ), + ], + complexity=None, + ), + ), + ] + + +def test_report_builder_session_create_line(mocker): + current_yaml, sessionid, ignored_lines, path_fixer = ( + { + "flag_management": { + "default_rules": { + "carryforward": "true", + "carryforward_mode": "labels", + } + } + }, + 45, + mocker.MagicMock(), + mocker.MagicMock(), + ) + filepath = "filepath" + builder = ReportBuilder(current_yaml, sessionid, ignored_lines, path_fixer) + builder_session = builder.create_report_builder_session(filepath) + line = builder_session.create_coverage_line(1, CoverageType.branch) + assert line == ReportLine.create( + coverage=1, + type="b", + sessions=[ + LineSession( + id=45, coverage=1, branches=None, partials=None, complexity=None + ) + ], + datapoints=[], + complexity=None, + ) + + +def test_report_builder_session_create_line_mixed_labels(mocker): + current_yaml, sessionid, ignored_lines, path_fixer = ( + { + "flag_management": { + "default_rules": { + "carryforward": "true", + "carryforward_mode": "labels", + } + } + }, + 45, + mocker.MagicMock(), + mocker.MagicMock(), + ) + filepath = "filepath" + builder = ReportBuilder(current_yaml, sessionid, ignored_lines, path_fixer) + builder_session = builder.create_report_builder_session(filepath) + line = builder_session.create_coverage_line( + 1, + CoverageType.branch, + labels_list_of_lists=[["label1"], [], ["label2"], None], + ) + assert line == ReportLine.create( + coverage=1, + type="b", + sessions=[ + LineSession( + id=45, coverage=1, branches=None, partials=None, complexity=None + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=45, + coverage=1, + coverage_type="b", + label_ids=["label1"], + ), + CoverageDatapoint( + sessionid=45, + coverage=1, + coverage_type="b", + label_ids=["label2"], + ), + ], + complexity=None, + ) + + +@pytest.mark.parametrize( + "current_yaml,expected_result", + [ + ({}, False), + ({"flags": {"oldflag": {"carryforward": "true"}}}, False), + ( + { + "flags": { + "oldflag": {"carryforward": "true", "carryforward_mode": "labels"} + } + }, + True, + ), + ( + { + "flag_management": { + "default_rules": { + "carryforward": "true", + "carryforward_mode": "labels", + } + } + }, + True, + ), + ( + { + "flag_management": { + "default_rules": { + "carryforward": "true", + "carryforward_mode": "all", + } + } + }, + False, + ), + ( + { + "flag_management": { + "default_rules": { + "carryforward": "true", + "carryforward_mode": "all", + }, + "individual_flags": [ + { + "name": "some_flag", + "carryforward_mode": "labels", + } + ], + } + }, + True, + ), + ], +) +def test_report_builder_supports_flags(current_yaml, expected_result): + builder = ReportBuilder(current_yaml, 0, None, None) + assert builder.supports_labels() == expected_result diff --git a/apps/worker/services/report/tests/unit/test_report_processor.py b/apps/worker/services/report/tests/unit/test_report_processor.py new file mode 100644 index 0000000000..9a99c9b0cf --- /dev/null +++ b/apps/worker/services/report/tests/unit/test_report_processor.py @@ -0,0 +1,63 @@ +import pytest + +from services.report.languages.helpers import remove_non_ascii +from services.report.parser.types import ParsedUploadedReportFile +from services.report.report_processor import process_report, report_type_matching + +xcode_report = b"""/Users/distiller/project/Auth0/A0ChallengeGenerator.m: + 28| |@implementation A0SHA256ChallengeGenerator + 29| | + 30| 7|- (instancetype)init { + 31| 7| NSMutableData *data = [NSMutableData dataWithLength:kVerifierSize]; + 32| 7| int result __attribute__((unused)) = SecRandomCopyBytes(kSecRandomDefault, kVerifierSize, data.mutableBytes); +""" + + +@pytest.mark.parametrize( + "input,expected_type,expected_content", + [ + (b"", "txt", b""), + (b"{}", "json", {}), + (xcode_report, "txt", None), + (b'{"value":1}', "json", {"value": 1}), + ( + b'source.scala', + "xml", + None, + ), + ( + b'\n\n\n\n\nsource.scala', + "xml", + None, + ), + ( + # NOTE: The `\ufeff` is a BOM (byte-order-mark) + '\ufeffsource.scala'.encode(), + "xml", + None, + ), + (b"normal file", "txt", b"normal file"), + (b"1", "txt", b"1"), + ], +) +def test_report_type_matching(input: bytes, expected_type: str, expected_content): + report = ParsedUploadedReportFile(filename="name", file_contents=input) + first_line = remove_non_ascii(report.get_first_line().decode(errors="replace")) + + content, detected_type = report_type_matching( + report, + first_line, + ) + assert detected_type == expected_type + if expected_content is not None: + assert content == expected_content + + +def test_empty_json(): + raw_report = ParsedUploadedReportFile(filename="name", file_contents=b"{}") + report = process_report(raw_report, None) + assert report is None + + raw_report = ParsedUploadedReportFile(filename="name", file_contents=b"[]") + report = process_report(raw_report, None) + assert report is None diff --git a/apps/worker/services/report/tests/unit/test_sessions.py b/apps/worker/services/report/tests/unit/test_sessions.py new file mode 100644 index 0000000000..e772fdf704 --- /dev/null +++ b/apps/worker/services/report/tests/unit/test_sessions.py @@ -0,0 +1,996 @@ +import pytest +from mock import MagicMock +from shared.reports.reportfile import ReportFile +from shared.reports.resources import Report +from shared.reports.types import CoverageDatapoint, LineSession, ReportLine +from shared.utils.sessions import Session, SessionType +from shared.yaml import UserYaml + +from helpers.labels import SpecialLabelsEnum +from services.report.raw_upload_processor import ( + SessionAdjustmentResult, + clear_carryforward_sessions, +) +from test_utils.base import BaseTestCase + +# Not calling add_sessions here on purpose, so it doesnt +# interfere with this logic + + +class TestAdjustSession(BaseTestCase): + @pytest.fixture + def sample_first_report(self): + first_report = Report( + sessions={ + 0: Session( + flags=["enterprise"], + id=0, + session_type=SessionType.carriedforward, + ), + 1: Session( + flags=["enterprise"], id=1, session_type=SessionType.uploaded + ), + 2: Session( + flags=["unit"], id=2, session_type=SessionType.carriedforward + ), + 3: Session( + flags=["unrelated"], id=3, session_type=SessionType.uploaded + ), + } + ) + first_file = ReportFile("first_file.py") + c = 0 + for list_of_lists_of_labels in [ + [["one_label"]], + [["another_label"]], + [["another_label"], ["one_label"]], + [["another_label", "one_label"]], + [[SpecialLabelsEnum.CODECOV_ALL_LABELS_PLACEHOLDER.corresponding_label]], + ]: + for sessionid in range(4): + first_file.append( + c % 7 + 1, + self.create_sample_line( + coverage=c, + sessionid=sessionid, + list_of_lists_of_labels=list_of_lists_of_labels, + ), + ) + c += 1 + second_file = ReportFile("second_file.py") + first_report.append(first_file) + first_report.append(second_file) + assert self.convert_report_to_better_readable(first_report)["archive"] == { + "first_file.py": [ + ( + 1, + 14, + None, + [ + [0, 0, None, None, None], + [3, 7, None, None, None], + [2, 14, None, None, None], + ], + None, + None, + [ + (0, 0, None, ["one_label"]), + (2, 14, None, ["another_label", "one_label"]), + (3, 7, None, ["another_label"]), + ], + ), + ( + 2, + 15, + None, + [ + [1, 1, None, None, None], + [0, 8, None, None, None], + [3, 15, None, None, None], + ], + None, + None, + [ + (0, 8, None, ["another_label"]), + (0, 8, None, ["one_label"]), + (1, 1, None, ["one_label"]), + (3, 15, None, ["another_label", "one_label"]), + ], + ), + ( + 3, + 16, + None, + [ + [2, 2, None, None, None], + [1, 9, None, None, None], + [0, 16, None, None, None], + ], + None, + None, + [ + (0, 16, None, ["Th2dMtk4M_codecov"]), + (1, 9, None, ["another_label"]), + (1, 9, None, ["one_label"]), + (2, 2, None, ["one_label"]), + ], + ), + ( + 4, + 17, + None, + [ + [3, 3, None, None, None], + [2, 10, None, None, None], + [1, 17, None, None, None], + ], + None, + None, + [ + (1, 17, None, ["Th2dMtk4M_codecov"]), + (2, 10, None, ["another_label"]), + (2, 10, None, ["one_label"]), + (3, 3, None, ["one_label"]), + ], + ), + ( + 5, + 18, + None, + [ + [0, 4, None, None, None], + [3, 11, None, None, None], + [2, 18, None, None, None], + ], + None, + None, + [ + (0, 4, None, ["another_label"]), + (2, 18, None, ["Th2dMtk4M_codecov"]), + (3, 11, None, ["another_label"]), + (3, 11, None, ["one_label"]), + ], + ), + ( + 6, + 19, + None, + [ + [1, 5, None, None, None], + [0, 12, None, None, None], + [3, 19, None, None, None], + ], + None, + None, + [ + (0, 12, None, ["another_label", "one_label"]), + (1, 5, None, ["another_label"]), + (3, 19, None, ["Th2dMtk4M_codecov"]), + ], + ), + ( + 7, + 13, + None, + [[2, 6, None, None, None], [1, 13, None, None, None]], + None, + None, + [ + (1, 13, None, ["another_label", "one_label"]), + (2, 6, None, ["another_label"]), + ], + ), + ] + } + return first_report + + def create_sample_line( + self, *, coverage, sessionid=None, list_of_lists_of_labels=None + ): + datapoints = [ + CoverageDatapoint( + sessionid=sessionid, + coverage=coverage, + coverage_type=None, + label_ids=labels, + ) + for labels in (list_of_lists_of_labels or [[]]) + ] + return ReportLine.create( + coverage=coverage, + sessions=[ + ( + LineSession( + id=sessionid, + coverage=coverage, + ) + ) + ], + datapoints=datapoints, + ) + + def test_adjust_sessions_no_cf(self, sample_first_report): + first_value = self.convert_report_to_better_readable(sample_first_report) + first_to_merge_session = Session(flags=["enterprise"], id=3) + second_report = Report(sessions={3: first_to_merge_session}) + current_yaml = UserYaml({}) + assert clear_carryforward_sessions( + sample_first_report, second_report, ["enterprise"], current_yaml + ) == SessionAdjustmentResult([], []) + assert first_value == self.convert_report_to_better_readable( + sample_first_report + ) + + def test_adjust_sessions_full_cf_only(self, sample_first_report): + first_to_merge_session = Session(flags=["enterprise"], id=3) + second_report = Report(sessions={3: first_to_merge_session}) + current_yaml = UserYaml( + { + "flag_management": { + "individual_flags": [{"name": "enterprise", "carryforward": True}] + } + } + ) + assert clear_carryforward_sessions( + sample_first_report, second_report, ["enterprise"], current_yaml + ) == SessionAdjustmentResult([0], []) + assert self.convert_report_to_better_readable(sample_first_report) == { + "archive": { + "first_file.py": [ + ( + 1, + 14, + None, + [[3, 7, None, None, None], [2, 14, None, None, None]], + None, + None, + [ + (2, 14, None, ["another_label", "one_label"]), + (3, 7, None, ["another_label"]), + ], + ), + ( + 2, + 15, + None, + [[1, 1, None, None, None], [3, 15, None, None, None]], + None, + None, + [ + (1, 1, None, ["one_label"]), + (3, 15, None, ["another_label", "one_label"]), + ], + ), + ( + 3, + 9, + None, + [[2, 2, None, None, None], [1, 9, None, None, None]], + None, + None, + [ + (1, 9, None, ["another_label"]), + (1, 9, None, ["one_label"]), + (2, 2, None, ["one_label"]), + ], + ), + ( + 4, + 17, + None, + [ + [3, 3, None, None, None], + [2, 10, None, None, None], + [1, 17, None, None, None], + ], + None, + None, + [ + (1, 17, None, ["Th2dMtk4M_codecov"]), + (2, 10, None, ["another_label"]), + (2, 10, None, ["one_label"]), + (3, 3, None, ["one_label"]), + ], + ), + ( + 5, + 18, + None, + [[3, 11, None, None, None], [2, 18, None, None, None]], + None, + None, + [ + (2, 18, None, ["Th2dMtk4M_codecov"]), + (3, 11, None, ["another_label"]), + (3, 11, None, ["one_label"]), + ], + ), + ( + 6, + 19, + None, + [[1, 5, None, None, None], [3, 19, None, None, None]], + None, + None, + [ + (1, 5, None, ["another_label"]), + (3, 19, None, ["Th2dMtk4M_codecov"]), + ], + ), + ( + 7, + 13, + None, + [[2, 6, None, None, None], [1, 13, None, None, None]], + None, + None, + [ + (1, 13, None, ["another_label", "one_label"]), + (2, 6, None, ["another_label"]), + ], + ), + ] + }, + "report": { + "files": { + "first_file.py": [ + 0, + [0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ] + }, + "sessions": { + "1": { + "t": None, + "d": None, + "a": None, + "f": ["enterprise"], + "c": None, + "n": None, + "N": None, + "j": None, + "u": None, + "p": None, + "e": None, + "st": "uploaded", + "se": {}, + }, + "2": { + "t": None, + "d": None, + "a": None, + "f": ["unit"], + "c": None, + "n": None, + "N": None, + "j": None, + "u": None, + "p": None, + "e": None, + "st": "carriedforward", + "se": {}, + }, + "3": { + "t": None, + "d": None, + "a": None, + "f": ["unrelated"], + "c": None, + "n": None, + "N": None, + "j": None, + "u": None, + "p": None, + "e": None, + "st": "uploaded", + "se": {}, + }, + }, + }, + "totals": { + "f": 1, + "n": 7, + "h": 7, + "m": 0, + "p": 0, + "c": "100", + "b": 0, + "d": 0, + "M": 0, + "s": 3, + "C": 0, + "N": 0, + "diff": None, + }, + } + + def test_adjust_sessions_partial_cf_only_no_changes( + self, sample_first_report, mocker + ): + first_to_merge_session = Session(flags=["enterprise"], id=3) + second_report = Report( + sessions={first_to_merge_session.id: first_to_merge_session} + ) + current_yaml = UserYaml( + { + "flag_management": { + "individual_flags": [ + { + "name": "enterprise", + "carryforward_mode": "labels", + "carryforward": True, + } + ] + } + } + ) + first_value = self.convert_report_to_better_readable(sample_first_report) + assert clear_carryforward_sessions( + sample_first_report, second_report, ["enterprise"], current_yaml + ) == SessionAdjustmentResult([], [0]) + after_result = self.convert_report_to_better_readable(sample_first_report) + assert after_result == first_value + + def test_adjust_sessions_partial_cf_only_no_changes_encoding_labels( + self, sample_first_report, mocker + ): + first_to_merge_session = Session(flags=["enterprise"], id=3) + second_report = Report( + sessions={first_to_merge_session.id: first_to_merge_session} + ) + current_yaml = UserYaml( + { + "flag_management": { + "individual_flags": [ + { + "name": "enterprise", + "carryforward_mode": "labels", + "carryforward": True, + } + ] + } + } + ) + first_value = self.convert_report_to_better_readable(sample_first_report) + upload = MagicMock( + name="fake_upload", + **{ + "report": MagicMock( + name="fake_commit_report", + **{ + "code": None, + "commit": MagicMock( + name="fake_commit", + **{"repository": MagicMock(name="fake_repo")}, + ), + }, + ) + }, + ) + assert clear_carryforward_sessions( + sample_first_report, + second_report, + ["enterprise"], + current_yaml, + upload=upload, + ) == SessionAdjustmentResult([], [0]) + after_result = self.convert_report_to_better_readable(sample_first_report) + assert after_result == first_value + + def test_adjust_sessions_partial_cf_only_some_changes(self, sample_first_report): + first_to_merge_session = Session(flags=["enterprise"], id=3) + second_report = Report( + sessions={first_to_merge_session.id: first_to_merge_session} + ) + current_yaml = UserYaml( + { + "flag_management": { + "individual_flags": [ + { + "name": "enterprise", + "carryforward_mode": "labels", + "carryforward": True, + } + ] + } + } + ) + second_report_file = ReportFile("unrelatedfile.py") + second_report_file.append( + 90, + self.create_sample_line( + coverage=90, sessionid=3, list_of_lists_of_labels=[["one_label"]] + ), + ) + second_report.append(second_report_file) + assert clear_carryforward_sessions( + sample_first_report, second_report, ["enterprise"], current_yaml + ) == SessionAdjustmentResult([], [0]) + assert self.convert_report_to_better_readable(sample_first_report) == { + "archive": { + "first_file.py": [ + ( + 1, + 14, + None, + [[3, 7, None, None, None], [2, 14, None, None, None]], + None, + None, + [ + (2, 14, None, ["another_label", "one_label"]), + (3, 7, None, ["another_label"]), + ], + ), + ( + 2, + 15, + None, + [ + [1, 1, None, None, None], + [0, 8, None, None, None], + [3, 15, None, None, None], + ], + None, + None, + [ + (0, 8, None, ["another_label"]), + (1, 1, None, ["one_label"]), + (3, 15, None, ["another_label", "one_label"]), + ], + ), + ( + 3, + 16, + None, + [ + [2, 2, None, None, None], + [1, 9, None, None, None], + [0, 16, None, None, None], + ], + None, + None, + [ + (0, 16, None, ["Th2dMtk4M_codecov"]), + (1, 9, None, ["another_label"]), + (1, 9, None, ["one_label"]), + (2, 2, None, ["one_label"]), + ], + ), + ( + 4, + 17, + None, + [ + [3, 3, None, None, None], + [2, 10, None, None, None], + [1, 17, None, None, None], + ], + None, + None, + [ + (1, 17, None, ["Th2dMtk4M_codecov"]), + (2, 10, None, ["another_label"]), + (2, 10, None, ["one_label"]), + (3, 3, None, ["one_label"]), + ], + ), + ( + 5, + 18, + None, + [ + [0, 4, None, None, None], + [3, 11, None, None, None], + [2, 18, None, None, None], + ], + None, + None, + [ + (0, 4, None, ["another_label"]), + (2, 18, None, ["Th2dMtk4M_codecov"]), + (3, 11, None, ["another_label"]), + (3, 11, None, ["one_label"]), + ], + ), + ( + 6, + 19, + None, + [[1, 5, None, None, None], [3, 19, None, None, None]], + None, + None, + [ + (1, 5, None, ["another_label"]), + (3, 19, None, ["Th2dMtk4M_codecov"]), + ], + ), + ( + 7, + 13, + None, + [[2, 6, None, None, None], [1, 13, None, None, None]], + None, + None, + [ + (1, 13, None, ["another_label", "one_label"]), + (2, 6, None, ["another_label"]), + ], + ), + ] + }, + "report": { + "files": { + "first_file.py": [ + 0, + [0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ] + }, + "sessions": { + "0": { + "t": None, + "d": None, + "a": None, + "f": ["enterprise"], + "c": None, + "n": None, + "N": None, + "j": None, + "u": None, + "p": None, + "e": None, + "st": "carriedforward", + "se": {}, + }, + "1": { + "t": None, + "d": None, + "a": None, + "f": ["enterprise"], + "c": None, + "n": None, + "N": None, + "j": None, + "u": None, + "p": None, + "e": None, + "st": "uploaded", + "se": {}, + }, + "2": { + "t": None, + "d": None, + "a": None, + "f": ["unit"], + "c": None, + "n": None, + "N": None, + "j": None, + "u": None, + "p": None, + "e": None, + "st": "carriedforward", + "se": {}, + }, + "3": { + "t": None, + "d": None, + "a": None, + "f": ["unrelated"], + "c": None, + "n": None, + "N": None, + "j": None, + "u": None, + "p": None, + "e": None, + "st": "uploaded", + "se": {}, + }, + }, + }, + "totals": { + "f": 1, + "n": 7, + "h": 7, + "m": 0, + "p": 0, + "c": "100", + "b": 0, + "d": 0, + "M": 0, + "s": 4, + "C": 0, + "N": 0, + "diff": None, + }, + } + + def test_adjust_sessions_partial_cf_only_full_deletion_due_to_lost_labels( + self, sample_first_report + ): + first_to_merge_session = Session(flags=["enterprise"], id=3) + second_report = Report(sessions={3: first_to_merge_session}) + current_yaml = UserYaml( + { + "flag_management": { + "individual_flags": [ + { + "name": "enterprise", + "carryforward_mode": "labels", + "carryforward": True, + } + ] + } + } + ) + + second_report_file = ReportFile("unrelatedfile.py") + second_report_file.append( + 90, + self.create_sample_line( + coverage=90, sessionid=3, list_of_lists_of_labels=[["one_label"]] + ), + ) + a_report_file = ReportFile("first_file.py") + a_report_file.append( + 90, + self.create_sample_line( + coverage=90, + sessionid=3, + list_of_lists_of_labels=[ + ["another_label"], + [ + SpecialLabelsEnum.CODECOV_ALL_LABELS_PLACEHOLDER.corresponding_label + ], + ], + ), + ) + second_report.append(second_report_file) + second_report.append(a_report_file) + assert clear_carryforward_sessions( + sample_first_report, second_report, ["enterprise"], current_yaml + ) == SessionAdjustmentResult([0], []) + res = self.convert_report_to_better_readable(sample_first_report) + assert res["report"]["sessions"] == { + "1": { + "t": None, + "d": None, + "a": None, + "f": ["enterprise"], + "c": None, + "n": None, + "N": None, + "j": None, + "u": None, + "p": None, + "e": None, + "st": "uploaded", + "se": {}, + }, + "2": { + "t": None, + "d": None, + "a": None, + "f": ["unit"], + "c": None, + "n": None, + "N": None, + "j": None, + "u": None, + "p": None, + "e": None, + "st": "carriedforward", + "se": {}, + }, + "3": { + "t": None, + "d": None, + "a": None, + "f": ["unrelated"], + "c": None, + "n": None, + "N": None, + "j": None, + "u": None, + "p": None, + "e": None, + "st": "uploaded", + "se": {}, + }, + } + assert self.convert_report_to_better_readable(sample_first_report)[ + "archive" + ] == { + "first_file.py": [ + ( + 1, + 14, + None, + [[3, 7, None, None, None], [2, 14, None, None, None]], + None, + None, + [ + (2, 14, None, ["another_label", "one_label"]), + (3, 7, None, ["another_label"]), + ], + ), + ( + 2, + 15, + None, + [[1, 1, None, None, None], [3, 15, None, None, None]], + None, + None, + [ + (1, 1, None, ["one_label"]), + (3, 15, None, ["another_label", "one_label"]), + ], + ), + ( + 3, + 9, + None, + [[2, 2, None, None, None], [1, 9, None, None, None]], + None, + None, + [ + (1, 9, None, ["another_label"]), + (1, 9, None, ["one_label"]), + (2, 2, None, ["one_label"]), + ], + ), + ( + 4, + 17, + None, + [ + [3, 3, None, None, None], + [2, 10, None, None, None], + [1, 17, None, None, None], + ], + None, + None, + [ + (1, 17, None, ["Th2dMtk4M_codecov"]), + (2, 10, None, ["another_label"]), + (2, 10, None, ["one_label"]), + (3, 3, None, ["one_label"]), + ], + ), + ( + 5, + 18, + None, + [[3, 11, None, None, None], [2, 18, None, None, None]], + None, + None, + [ + (2, 18, None, ["Th2dMtk4M_codecov"]), + (3, 11, None, ["another_label"]), + (3, 11, None, ["one_label"]), + ], + ), + ( + 6, + 19, + None, + [[1, 5, None, None, None], [3, 19, None, None, None]], + None, + None, + [ + (1, 5, None, ["another_label"]), + (3, 19, None, ["Th2dMtk4M_codecov"]), + ], + ), + ( + 7, + 13, + None, + [[2, 6, None, None, None], [1, 13, None, None, None]], + None, + None, + [ + (1, 13, None, ["another_label", "one_label"]), + (2, 6, None, ["another_label"]), + ], + ), + ] + } + + +{ + "first_file.py": [ + ( + 1, + 14, + None, + [[3, 7, None, None, None], [2, 14, None, None, None]], + None, + None, + [ + (2, 14, None, ["another_label", "one_label"]), + (3, 7, None, ["another_label"]), + ], + ), + ( + 2, + 15, + None, + [[1, 1, None, None, None], [3, 15, None, None, None]], + None, + None, + [ + (1, 1, None, ["one_label"]), + (3, 15, None, ["another_label", "one_label"]), + ], + ), + ( + 3, + 9, + None, + [[2, 2, None, None, None], [1, 9, None, None, None]], + None, + None, + [ + (1, 9, None, ["another_label"]), + (1, 9, None, ["one_label"]), + (2, 2, None, ["one_label"]), + ], + ), + ( + 4, + 17, + None, + [ + [3, 3, None, None, None], + [2, 10, None, None, None], + [1, 17, None, None, None], + ], + None, + None, + [ + (1, 17, None, ["Th2dMtk4M_codecov"]), + (2, 10, None, ["another_label"]), + (2, 10, None, ["one_label"]), + (3, 3, None, ["one_label"]), + ], + ), + ( + 5, + 18, + None, + [[3, 11, None, None, None], [2, 18, None, None, None]], + None, + None, + [ + (2, 18, None, ["Th2dMtk4M_codecov"]), + (3, 11, None, ["another_label"]), + (3, 11, None, ["one_label"]), + ], + ), + ( + 6, + 19, + None, + [[1, 5, None, None, None], [3, 19, None, None, None]], + None, + None, + [(1, 5, None, ["another_label"]), (3, 19, None, ["Th2dMtk4M_codecov"])], + ), + ( + 7, + 13, + None, + [[2, 6, None, None, None], [1, 13, None, None, None]], + None, + None, + [ + (1, 13, None, ["another_label", "one_label"]), + (2, 6, None, ["another_label"]), + ], + ), + ] +} diff --git a/apps/worker/services/repository.py b/apps/worker/services/repository.py new file mode 100644 index 0000000000..bd1b2460ab --- /dev/null +++ b/apps/worker/services/repository.py @@ -0,0 +1,625 @@ +import logging +import re +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Mapping, Optional, Tuple + +import sentry_sdk +import shared.torngit as torngit +from asgiref.sync import async_to_sync +from shared.bots import get_adapter_auth_information +from shared.config import get_config, get_verify_ssl +from shared.torngit.base import TorngitBaseAdapter +from shared.torngit.exceptions import ( + TorngitClientError, + TorngitError, + TorngitObjectNotFoundError, +) +from shared.typings.torngit import ( + AdditionalData, + OwnerInfo, + RepoInfo, + TorngitInstanceData, +) +from shared.validation.exceptions import InvalidYamlException +from shared.yaml import UserYaml +from shared.yaml.user_yaml import OwnerContext +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Query, Session, lazyload + +from database.enums import CommitErrorTypes +from database.models import Commit, Owner, Pull, Repository +from database.models.core import GITHUB_APP_INSTALLATION_DEFAULT_NAME +from helpers.save_commit_error import save_commit_error +from helpers.token_refresh import get_token_refresh_callback +from services.yaml import read_yaml_field, save_repo_yaml_to_database_if_needed +from services.yaml.fetcher import fetch_commit_yaml_from_provider + +log = logging.getLogger(__name__) + +merged_pull = re.compile(r".*Merged in [^\s]+ \(pull request \#(\d+)\).*").match + + +@sentry_sdk.trace +def get_repo_provider_service( + repository: Repository, + installation_name_to_use: str = GITHUB_APP_INSTALLATION_DEFAULT_NAME, + additional_data: AdditionalData = None, +) -> TorngitBaseAdapter: + adapter_auth_info = get_adapter_auth_information( + repository.owner, + repository=repository, + installation_name_to_use=installation_name_to_use, + ) + if additional_data is None: + additional_data = {} + data = TorngitInstanceData( + repo=RepoInfo( + name=repository.name, + using_integration=( + adapter_auth_info.get("selected_installation_info") is not None + ), + service_id=repository.service_id, + repoid=repository.repoid, + ), + owner=OwnerInfo( + service_id=repository.owner.service_id, + ownerid=repository.ownerid, + username=repository.owner.username, + ), + installation=adapter_auth_info["selected_installation_info"], + fallback_installations=adapter_auth_info["fallback_installations"], + additional_data=additional_data, + ) + + adapter_params = dict( + token=adapter_auth_info["token"], + token_type_mapping=adapter_auth_info["token_type_mapping"], + on_token_refresh=get_token_refresh_callback(adapter_auth_info["token_owner"]), + **data, + ) + return _get_repo_provider_service_instance(repository.service, adapter_params) + + +def _get_repo_provider_service_instance(service: str, adapter_params: dict): + _timeouts = [ + get_config("setup", "http", "timeouts", "connect", default=30), + get_config("setup", "http", "timeouts", "receive", default=60), + ] + return torngit.get( + service, + # Args for the Torngit instance + timeouts=_timeouts, + verify_ssl=get_verify_ssl(service), + oauth_consumer_token=dict( + key=get_config(service, "client_id"), + secret=get_config(service, "client_secret"), + ), + **adapter_params, + ) + + +@sentry_sdk.trace +async def fetch_appropriate_parent_for_commit( + repository_service: TorngitBaseAdapter, commit: Commit, git_commit=None +) -> str | None: + closest_parent_without_message = None + db_session = commit.get_db_session() + commitid = commit.commitid + if git_commit: + parents = git_commit["parents"] + possible_commit_query = db_session.query(Commit.commitid, Commit.branch).filter( + Commit.commitid.in_(parents), + Commit.repoid == commit.repoid, + ~Commit.message.is_(None), + ~Commit.deleted.is_(True), + ) + possible_commit = _possibly_filter_out_branch(commit, possible_commit_query) + if possible_commit: + return possible_commit.commitid + + ancestors_tree = await repository_service.get_ancestors_tree(commitid) + elements = [ancestors_tree] + while elements: + parents = [k for el in elements for k in el["parents"]] + parent_commits = [p["commitid"] for p in parents] + closest_parent_query = db_session.query(Commit.commitid, Commit.branch).filter( + Commit.commitid.in_(parent_commits), + Commit.repoid == commit.repoid, + ~Commit.message.is_(None), + ~Commit.deleted.is_(True), + ) + closest_parent = _possibly_filter_out_branch(commit, closest_parent_query) + if closest_parent: + return closest_parent.commitid + + if closest_parent_without_message is None: + parent_query = db_session.query(Commit.commitid, Commit.branch).filter( + Commit.commitid.in_(parent_commits), + Commit.repoid == commit.repoid, + ~Commit.deleted.is_(True), + ) + parent = _possibly_filter_out_branch(commit, parent_query) + if parent: + closest_parent_without_message = parent.commitid + elements = parents + + log.warning( + "Unable to find a parent commit that was properly found on Github", + extra=dict(commit=commit.commitid, repoid=commit.repoid), + ) + return closest_parent_without_message + + +def _possibly_filter_out_branch(commit: Commit, query: Query) -> Commit | None: + commits = query.all() + if len(commits) == 1: + return commits[0] + + # if we have more than one possible commit, pick the first one with a matching `branch`: + for possible_commit in commits: + if possible_commit.branch == commit.branch: + return possible_commit + + return None + + +def possibly_update_commit_from_provider_info( + commit: Commit, repository_service: TorngitBaseAdapter +) -> bool: + try: + if not commit.message: + log.info( + "Commit does not have all needed info. Reaching provider to fetch info" + ) + async_to_sync(update_commit_from_provider_info)(repository_service, commit) + return True + except TorngitObjectNotFoundError: + log.warning( + "Could not update commit with info because it was not found at the provider" + ) + return False + log.debug("Not updating commit because it already seems to be populated") + return False + + +@sentry_sdk.trace +async def update_commit_from_provider_info( + repository_service: TorngitBaseAdapter, commit: Commit +): + """ + Takes the result from the torngit commit details, and updates the commit + properties with it + """ + db_session = commit.get_db_session() + commitid = commit.commitid + git_commit = await repository_service.get_commit(commitid) + + if git_commit is None: + log.error( + "Could not find commit on git provider", + extra=dict(repoid=commit.repoid, commit=commit.commitid), + ) + return + + log.debug("Found git commit", extra=dict(commit=git_commit)) + + author_info = git_commit["author"] + if not author_info.get("id"): + commit_author = None + log.info( + "Not trying to set an author because it does not have an id", + extra=dict( + author_info=author_info, + git_commit=git_commit, + commit=commit.commitid, + ), + ) + else: + commit_author = upsert_author( + db_session, + commit.repository.service, + author_info["id"], + author_info["username"], + author_info["email"], + author_info["name"], + ) + + commit.parent_commit_id = await fetch_appropriate_parent_for_commit( + repository_service, commit, git_commit + ) + commit.message = git_commit["message"] + commit.author = commit_author + commit.updatestamp = datetime.now() + commit.timestamp = git_commit["timestamp"] + + # attempt to populate commit.pullid from repository_service if we don't have it + if not commit.pullid: + commit.pullid = await repository_service.find_pull_request( + commit=commitid, branch=commit.branch + ) + + # if our records or the call above returned a pullid, fetch it's details + if commit.pullid: + pull_details = await repository_service.get_pull_request(pullid=commit.pullid) + # There's a chance that the commit comes from a fork + # so we append the branch name with the fork slug + branch_name = pull_details["head"]["branch"] + # TODO: 'slug' is in a `.get` because currently only GitHub returns that info + if pull_details["head"].get("slug") != pull_details["base"].get("slug"): + branch_name = pull_details["head"]["slug"] + ":" + branch_name + commit.branch = branch_name + commit.merged = False + else: + possible_branches = await repository_service.get_best_effort_branches( + commit.commitid + ) + if commit.repository.branch in possible_branches: + commit.merged = True + commit.branch = commit.repository.branch + else: + commit.merged = False + + if commit.repository.service == "bitbucket": + res = merged_pull(git_commit["message"]) + if res: + pullid = res.groups()[0] + if pullid != commit.pullid: + pull_details = await repository_service.get_pull_request(pullid) + commit.branch = pull_details["base"]["branch"] + + db_session.flush() + log.info( + "Updated commit with info from git provider", + extra=dict( + repoid=commit.repoid, + commit=commit.commitid, + branch_value=commit.branch, + author_value=commit.author_id, + ), + ) + db_session.commit() + + +def upsert_author( + db_session, service, service_id, username, email=None, name=None +) -> Owner: + query = db_session.query(Owner).filter( + Owner.service == service, Owner.service_id == str(service_id) + ) + author = query.first() + + if author: + needs_update = False + db_session.begin(nested=True) + if author.username != username and username is not None: + author.username = username + needs_update = True + if author.name != name and name is not None: + author.name = name + needs_update = True + if author.email != email and email is not None: + author.email = email + needs_update = True + + if needs_update: + db_session.commit() + else: + db_session.rollback() + else: + db_session.begin(nested=True) + try: + author = Owner( + service=service, + service_id=str(service_id), + username=username, + name=name, + email=email, + createstamp=datetime.now(), + ) + db_session.add(author) + db_session.commit() + except IntegrityError: + log.warning( + "IntegrityError in upsert_author", + extra=dict(service=service, service_id=service_id, username=username), + ) + db_session.rollback() + author = query.one() + + return author + + +WEBHOOK_EVENTS = { + "github": ["pull_request", "delete", "push", "public", "status", "repository"], + "github_enterprise": [ + "pull_request", + "delete", + "push", + "public", + "status", + "repository", + ], + "bitbucket": [ + "repo:push", + "pullrequest:created", + "pullrequest:updated", + "pullrequest:fulfilled", + "repo:commit_status_created", + "repo:commit_status_updated", + ], + # https://confluence.atlassian.com/bitbucketserver/post-service-webhook-for-bitbucket-server-776640367.html + "bitbucket_server": [ + "repo:modified", + "repo:refs_changed", + "pr:opened", + "pr:merged", + "pr:declined", + "pr:deleted", + ], + "gitlab": { + "push_events": True, + "issues_events": False, + "merge_requests_events": True, + "tag_push_events": False, + "note_events": False, + "job_events": False, + "build_events": True, + "pipeline_events": True, + "wiki_events": False, + }, + "gitlab_enterprise": { + "push_events": True, + "issues_events": False, + "merge_requests_events": True, + "tag_push_events": False, + "note_events": False, + "job_events": False, + "build_events": True, + "pipeline_events": True, + "wiki_events": False, + }, +} + + +async def create_webhook_on_provider( + repository_service, token=None, webhook_secret: Optional[str] = None +): + """ + Posts to the provider a webhook so we can receive updates from this + repo + """ + webhook_url = get_config("setup", "webhook_url") or get_config( + "setup", "codecov_url" + ) + + if webhook_secret is None: + webhook_secret = get_config( + repository_service.service, + "webhook_secret", + default="ab164bf3f7d947f2a0681b215404873e", + ) + return await repository_service.post_webhook( + f"Codecov Webhook. {webhook_url}", + f"{webhook_url}/webhooks/{repository_service.service}", + WEBHOOK_EVENTS[repository_service.service], + webhook_secret, + token=token, + ) + + +async def gitlab_webhook_update(repository_service, hookid, secret): + """ + Edits an existing Gitlab webhook - adds a secret. + """ + webhook_url = get_config("setup", "webhook_url") or get_config( + "setup", "codecov_url" + ) + return await repository_service.edit_webhook( + hookid=hookid, + name=f"Codecov Webhook. {webhook_url}", + url=f"{webhook_url}/webhooks/{repository_service.service}", + events=WEBHOOK_EVENTS[repository_service.service], + secret=secret, + ) + + +def get_repo_provider_service_by_id(db_session, repoid, commitid=None): + repo = db_session.query(Repository).filter(Repository.repoid == int(repoid)).first() + + assert repo, "repo-not-found" + + return get_repo_provider_service(repo) + + +@dataclass +class EnrichedPull(object): + database_pull: Pull + provider_pull: Optional[Mapping[str, Any]] + + +@sentry_sdk.trace +async def fetch_and_update_pull_request_information_from_commit( + repository_service: TorngitBaseAdapter, commit, current_yaml +) -> Optional[EnrichedPull]: + db_session = commit.get_db_session() + pullid = commit.pullid + if not commit.pullid: + try: + pullid = await repository_service.find_pull_request( + commit=commit.commitid, branch=commit.branch + ) + except TorngitClientError: + log.warning( + "Unable to fetch what pull request the commit belongs to", + exc_info=True, + extra=dict(repoid=commit.repoid, commit=commit.commitid), + ) + if not pullid: + return None + enriched_pull = await fetch_and_update_pull_request_information( + repository_service, db_session, commit.repoid, pullid, current_yaml + ) + pull = enriched_pull.database_pull + if pull is not None: + head = pull.get_head_commit() + if head is None or head.timestamp <= commit.timestamp: + pull.head = commit.commitid + return enriched_pull + + +async def _pick_best_base_comparedto_pair( + repository_service, pull, current_yaml, pull_information +) -> Tuple[str, Optional[str]]: + db_session = pull.get_db_session() + repoid = pull.repoid + candidates_to_base = ( + [pull.user_provided_base_sha, pull_information["base"]["commitid"]] + if pull is not None and pull.user_provided_base_sha is not None + else [pull_information["base"]["commitid"]] + ) + for pull_base_sha in candidates_to_base: + base_commit = ( + db_session.query(Commit) + .filter_by(commitid=pull_base_sha, repoid=repoid) + .first() + ) + if base_commit: + return (pull_base_sha, pull_base_sha) + try: + commit_dict = await repository_service.get_commit(pull_base_sha) + new_base_query = db_session.query(Commit).filter( + Commit.repoid == repoid, + Commit.branch == pull_information["base"]["branch"], + (Commit.pullid.is_(None) | Commit.merged), + Commit.timestamp < commit_dict["timestamp"], + ) + if read_yaml_field(current_yaml, ("codecov", "require_ci_to_pass"), True): + new_base_query = new_base_query.filter(Commit.ci_passed) + new_base_query = new_base_query.order_by(Commit.timestamp.desc()) + new_base = new_base_query.first() + if new_base: + return (pull_base_sha, new_base.commitid) + except TorngitObjectNotFoundError: + log.warning( + "Cannot find (in the provider) commit that is supposed to be the PR base", + extra=dict(repoid=repoid, supposed_base=pull_base_sha), + ) + return (candidates_to_base[0], None) + + +@sentry_sdk.trace +async def fetch_and_update_pull_request_information( + repository_service, + db_session: Session, + repoid: int | str, + pullid: int | str, + current_yaml, +) -> EnrichedPull: + pull = ( + db_session.query(Pull) + .options(lazyload("repository")) + .filter_by(pullid=pullid, repoid=repoid) + .first() + ) + try: + pull_information = await repository_service.get_pull_request(pullid=pullid) + except TorngitClientError: + log.warning( + "Unable to find pull request information on provider to update it due to client error", + extra=dict(repoid=repoid, pullid=pullid), + ) + return EnrichedPull(database_pull=pull, provider_pull=None) + except TorngitError: + log.warning( + "Unable to find pull request information on provider to update it due to unknown provider error", + extra=dict(repoid=repoid, pullid=pullid), + ) + return EnrichedPull(database_pull=pull, provider_pull=None) + if not pull: + pull = Pull( + repoid=repoid, + pullid=pullid, + state=pull_information["state"], + title=pull_information["title"], + issueid=pull_information["id"], + ) + db_session.add(pull) + db_session.flush() + else: + pull.state = pull_information["state"] + pull.title = pull_information["title"] + pull.issueid = pull_information["id"] + base_commit_sha, compared_to = await _pick_best_base_comparedto_pair( + repository_service, pull, current_yaml, pull_information + ) + pull.base = base_commit_sha + pull.compared_to = compared_to + + if pull is not None and not pull.author: + pr_author = upsert_author( + db_session, + repository_service.service, + pull_information["author"]["id"], + pull_information["author"]["username"], + ) + if pr_author: + pull.author = pr_author + + db_session.commit() + + return EnrichedPull(database_pull=pull, provider_pull=pull_information) + + +@sentry_sdk.trace +def fetch_commit_yaml_and_possibly_store( + commit: Commit, repository_service: TorngitBaseAdapter +) -> UserYaml: + repository = commit.repository + try: + log.info( + "Fetching commit yaml from provider for commit", + extra=dict(repoid=commit.repoid, commit=commit.commitid), + ) + commit_yaml = async_to_sync(fetch_commit_yaml_from_provider)( + commit, repository_service + ) + save_repo_yaml_to_database_if_needed(commit, commit_yaml) + except InvalidYamlException as ex: + save_commit_error( + commit, + error_code=CommitErrorTypes.INVALID_YAML.value, + error_params=dict( + repoid=repository.repoid, + commit=commit.commitid, + error_location=ex.error_location, + ), + ) + log.warning( + "Unable to use yaml from commit because it is invalid", + extra=dict( + repoid=repository.repoid, + commit=commit.commitid, + error_location=ex.error_location, + ), + exc_info=True, + ) + commit_yaml = None + except TorngitClientError: + log.warning( + "Unable to use yaml from commit because it cannot be fetched", + extra=dict(repoid=repository.repoid, commit=commit.commitid), + exc_info=True, + ) + commit_yaml = None + context = OwnerContext( + owner_onboarding_date=repository.owner.createstamp, + owner_plan=repository.owner.plan, + ownerid=repository.ownerid, + ) + return UserYaml.get_final_yaml( + owner_yaml=repository.owner.yaml, + repo_yaml=repository.yaml, + commit_yaml=commit_yaml, + owner_context=context, + ) diff --git a/apps/worker/services/seats.py b/apps/worker/services/seats.py new file mode 100644 index 0000000000..3c2612a6c0 --- /dev/null +++ b/apps/worker/services/seats.py @@ -0,0 +1,111 @@ +import logging +from dataclasses import dataclass +from enum import Enum + +from shared.plan.service import PlanService +from sqlalchemy.orm import Session + +from database.models import Owner +from services.decoration import _is_bot_account +from services.repository import EnrichedPull + +log = logging.getLogger(__name__) + + +class ShouldActivateSeat(Enum): + NO_ACTIVATE = "no_activate" + MANUAL_ACTIVATE = "manual_activate" + AUTO_ACTIVATE = "auto_activate" + + +@dataclass +class SeatActivationInfo: + should_activate_seat: ShouldActivateSeat = ShouldActivateSeat.NO_ACTIVATE + owner_id: int | None = None + author_id: int | None = None + reason: str | None = None + + +def determine_seat_activation(pull: EnrichedPull) -> SeatActivationInfo: + """ + this function will determine if a user needs to be activated based on information about the user, their org, their repo, and the PR + 1. if the repo is public they don't need to be activated + 2. get repostiory owner info + 2.1 (custom gitlab logic) if they are on gitlab we get their root group as the org instead of the repo owner + 3. get pr author info + 4. if org doesn't use seats, or author is included in seats already then no need to be activated + 5. if author is bot user then no need to be activated + 6. user must either be manually activated or auto activated + """ + db_pull = pull.database_pull + provider_pull = pull.provider_pull + if provider_pull is None: + log.warning( + "Provider pull was None when determining whether to activate seat for user", + extra=dict( + pullid=db_pull.pullid, + repoid=db_pull.repoid, + head_commit=db_pull.head, + base_commit=db_pull.base, + ), + ) + return SeatActivationInfo(reason="no_provider_pull") + + if db_pull.repository.private is False: + return SeatActivationInfo(reason="public_repo") + + org = db_pull.repository.owner + + db_session: Session = db_pull.get_db_session() + + # do not access plan directly - only through PlanService + org_plan = PlanService(current_org=org) + # use the org that has the plan - for GL this is the root_org rather than the repository.owner org + org = org_plan.current_org + + if not org_plan.is_pr_billing_plan: + return SeatActivationInfo(reason="no_pr_billing_plan") + + pr_author = ( + db_session.query(Owner) + .filter( + Owner.service == org.service, + Owner.service_id == provider_pull.get("author", {}).get("id"), + ) + .first() + ) + + if not pr_author: + log.info( + "PR author not found in database", + extra=dict( + author_service=org.service, + author_service_id=provider_pull.get("author", {}).get("id"), + author_username=provider_pull.get("author", {}).get("username"), + ), + ) + return SeatActivationInfo(reason="no_pr_author") + + if ( + org.plan_activated_users is not None + and pr_author.ownerid in org.plan_activated_users + ): + return SeatActivationInfo(reason="author_in_plan_activated_users") + + if _is_bot_account(pr_author): + return SeatActivationInfo(reason="is_bot_account") + + if not org.plan_auto_activate: + return SeatActivationInfo( + ShouldActivateSeat.MANUAL_ACTIVATE, + org.ownerid, + pr_author.ownerid, + reason="manual_activate", + ) + else: + return SeatActivationInfo( + ShouldActivateSeat.AUTO_ACTIVATE, + org.ownerid, + pr_author.ownerid, + reason="auto_activate", + ) diff --git a/apps/worker/services/smtp.py b/apps/worker/services/smtp.py new file mode 100644 index 0000000000..1beb3b73dd --- /dev/null +++ b/apps/worker/services/smtp.py @@ -0,0 +1,130 @@ +import logging +import smtplib +import ssl + +from shared.config import get_config + +from helpers.email import Email + +log = logging.getLogger(__name__) + + +class SMTPServiceError(Exception): ... + + +class SMTPService: + connection = None + + @classmethod + def active(cls): + return cls.connection is not None + + @property + def extra_dict(self): + return {"host": self.host, "port": self.port, "username": self.username} + + def _load_config(self): + if get_config("services", "smtp", default={}) == {}: + return False + self.host = get_config("services", "smtp", "host", default="mailhog") + self.port = get_config("services", "smtp", "port", default=1025) + self.username = get_config("services", "smtp", "username", default=None) + self.password = get_config("services", "smtp", "password", default=None) + self.ssl_context = ssl.create_default_context() + return True + + def tls_and_auth(self): + self.try_starttls() + if self.username and self.password: + self.try_login() + + def try_starttls(self): + # only necessary if SMTP server supports TLS and authentication, + # for example mailhog does not need these two steps + try: + SMTPService.connection.starttls(context=self.ssl_context) + except smtplib.SMTPNotSupportedError: + log.warning( + "Server does not support TLS, continuing initialization of SMTP connection", + extra=dict( + host=self.host, + port=self.port, + username=self.username, + password=self.password, + ), + ) + except smtplib.SMTPResponseException as exc: + log.warning("Error doing STARTTLS command on SMTP", extra=self.extra_dict) + raise SMTPServiceError("Error doing STARTTLS command on SMTP") from exc + + def try_login(self): + try: + SMTPService.connection.login(self.username, self.password) + except smtplib.SMTPNotSupportedError: + log.warning( + "Server does not support AUTH, continuing initialization of SMTP connection", + extra=self.extra_dict, + ) + except smtplib.SMTPAuthenticationError as exc: + log.warning( + "SMTP server did not accept username/password combination", + extra=self.extra_dict, + ) + raise SMTPServiceError( + "SMTP server did not accept username/password combination" + ) from exc + + def make_connection(self): + try: + SMTPService.connection.connect(self.host, self.port) + except smtplib.SMTPConnectError as exc: + raise SMTPServiceError("Error starting connection for SMTPService") from exc + self.tls_and_auth() + + def __init__(self): + if not self._load_config(): + log.warning("Unable to load SMTP config") + return + if SMTPService.connection is None: + try: + SMTPService.connection = smtplib.SMTP( + host=self.host, + port=self.port, + ) + + except smtplib.SMTPConnectError as exc: + raise SMTPServiceError( + "Error starting connection for SMTPService" + ) from exc + + self.tls_and_auth() + + def send(self, email: Email): + if not SMTPService.connection: + self.make_connection() + else: + try: + SMTPService.connection.noop() + except smtplib.SMTPServerDisconnected: + self.make_connection() # reconnect if disconnected + try: + errs = SMTPService.connection.send_message( + email.message, + ) + if len(errs) != 0: + err_msg = " ".join( + list(map(lambda err_tuple: f"{err_tuple[0]} {err_tuple[1]}", errs)) + ) + log.warning(f"Error sending email message: {err_msg}") + raise SMTPServiceError(f"Error sending email message: {err_msg}") + except smtplib.SMTPRecipientsRefused as exc: + log.warning("All recipients were refused", extra=self.extra_dict) + raise SMTPServiceError("All recipients were refused") from exc + except smtplib.SMTPSenderRefused as exc: + log.warning("Sender was refused", extra=self.extra_dict) + raise SMTPServiceError("Sender was refused") from exc + except smtplib.SMTPDataError as exc: + log.warning( + "The SMTP server did not accept the data", extra=self.extra_dict + ) + raise SMTPServiceError("The SMTP server did not accept the data") from exc diff --git a/apps/worker/services/static_analysis/__init__.py b/apps/worker/services/static_analysis/__init__.py new file mode 100644 index 0000000000..a9672e0d4d --- /dev/null +++ b/apps/worker/services/static_analysis/__init__.py @@ -0,0 +1,188 @@ +import json +import logging +import typing + +import sentry_sdk +from shared.storage.exceptions import FileNotInStorageError + +from database.models.staticanalysis import ( + StaticAnalysisSingleFileSnapshot, + StaticAnalysisSuite, + StaticAnalysisSuiteFilepath, +) +from services.archive import ArchiveService +from services.static_analysis.git_diff_parser import DiffChange, DiffChangeType +from services.static_analysis.single_file_analyzer import ( + AntecessorFindingResult, + SingleFileSnapshotAnalyzer, +) + +log = logging.getLogger(__name__) + + +def _get_analysis_content_mapping(analysis: StaticAnalysisSuite, filepaths): + db_session = analysis.get_db_session() + return dict( + db_session.query( + StaticAnalysisSuiteFilepath.filepath, + StaticAnalysisSingleFileSnapshot.content_location, + ) + .join( + StaticAnalysisSuiteFilepath, + StaticAnalysisSuiteFilepath.file_snapshot_id + == StaticAnalysisSingleFileSnapshot.id_, + ) + .filter( + StaticAnalysisSuiteFilepath.filepath.in_(filepaths), + StaticAnalysisSuiteFilepath.analysis_suite_id == analysis.id_, + ) + ) + + +class StaticAnalysisComparisonService(object): + def __init__( + self, + base_static_analysis: StaticAnalysisSuite, + head_static_analysis: StaticAnalysisSuite, + git_diff: typing.List[DiffChange], + ): + self._base_static_analysis = base_static_analysis + self._head_static_analysis = head_static_analysis + self._git_diff = git_diff + self._archive_service = None + + @property + def archive_service(self): + if self._archive_service is None: + self._archive_service = ArchiveService( + self._base_static_analysis.commit.repository + ) + return self._archive_service + + @sentry_sdk.trace + def get_base_lines_relevant_to_change(self) -> typing.List[typing.Dict]: + final_result = {"all": False, "files": {}} + db_session = self._base_static_analysis.get_db_session() + head_analysis_content_locations_mapping = _get_analysis_content_mapping( + self._head_static_analysis, + [ + change.after_filepath + for change in self._git_diff + if change.after_filepath + ], + ) + base_analysis_content_locations_mapping = _get_analysis_content_mapping( + self._base_static_analysis, + [ + change.before_filepath + for change in self._git_diff + if change.before_filepath + ], + ) + # @giovanni-guidini 2023-06-14 + # NOTE: Maybe we can paralelize this bit. + # There's some level of IO involved. + for change in self._git_diff: + # This check should happen way earlier + if change.change_type == DiffChangeType.new: + return {"all": True} + final_result["files"][change.before_filepath] = self._analyze_single_change( + db_session, + change, + base_analysis_content_locations_mapping.get(change.before_filepath), + head_analysis_content_locations_mapping.get(change.after_filepath), + ) + return final_result + + def _load_snapshot_data( + self, filepath, content_location + ) -> typing.Optional[SingleFileSnapshotAnalyzer]: + if not content_location: + return None + try: + return SingleFileSnapshotAnalyzer( + filepath, + json.loads(self.archive_service.read_file(content_location)), + ) + except FileNotInStorageError: + log.warning( + "Unable to load file for static analysis comparison", + extra=dict(filepath=filepath, content_location=content_location), + ) + return None + + def _analyze_single_change( + self, + db_session, + change: DiffChange, + base_analysis_file_obj_content_location, + head_analysis_file_obj_content_location, + ): + if change.change_type == DiffChangeType.deleted: + # file simply deleted. + # all lines involved in it needs their tests rechecked + return {"all": True, "lines": None} + if change.change_type == DiffChangeType.modified: + result_so_far = {"all": False, "lines": set()} + head_analysis_file_data = self._load_snapshot_data( + change.after_filepath, head_analysis_file_obj_content_location + ) + base_analysis_file_data = self._load_snapshot_data( + change.before_filepath, base_analysis_file_obj_content_location + ) + if not head_analysis_file_data and not base_analysis_file_data: + return None + if head_analysis_file_data is None or base_analysis_file_data is None: + log.warning( + "Failed to load snapshot for file. Fallback to all lines in the file", + extra=dict( + file_path=change.after_filepath, + is_missing_head=(head_analysis_file_data is None), + is_missing_base=(base_analysis_file_data is None), + ), + ) + return {"all": True, "lines": None} + + for base_line in change.lines_only_on_base: + corresponding_exec_line = ( + base_analysis_file_data.get_corresponding_executable_line(base_line) + ) + if corresponding_exec_line is not None: + result_so_far["lines"].add(corresponding_exec_line) + affected_statement_lines = set( + x + for x in ( + head_analysis_file_data.get_corresponding_executable_line(li) + for li in change.lines_only_on_head + ) + if x is not None + ) + for head_line in affected_statement_lines: + ( + matching_type, + antecessor_head_line, + ) = head_analysis_file_data.get_antecessor_executable_line( + head_line, lines_to_not_consider=affected_statement_lines + ) + if matching_type == AntecessorFindingResult.file: + return {"all": True, "lines": None} + elif matching_type == AntecessorFindingResult.function: + matching_function = ( + base_analysis_file_data.find_function_by_identifier( + antecessor_head_line + ) + ) + if matching_function: + line_entrypoint = matching_function["start_line"] + result_so_far["lines"].add(line_entrypoint) + else: + # No matches, function does not exist on base, go to everything + return {"all": True, "lines": None} + elif matching_type == AntecessorFindingResult.line: + result_so_far["lines"].add(antecessor_head_line) + return result_so_far + log.warning( + "Unknown type of change. Fallback to all lines", + extra=dict(change_type=change.change_type), + ) + return {"all": True, "lines": None} diff --git a/apps/worker/services/static_analysis/git_diff_parser.py b/apps/worker/services/static_analysis/git_diff_parser.py new file mode 100644 index 0000000000..6307f87e10 --- /dev/null +++ b/apps/worker/services/static_analysis/git_diff_parser.py @@ -0,0 +1,85 @@ +import typing +from dataclasses import dataclass +from enum import Enum + +import sentry_sdk + +from services.comparison.changes import get_segment_offsets + + +class DiffChangeType(Enum): + new = "new" + deleted = "deleted" + modified = "modified" + binary = "binary" + + @classmethod + def get_from_string(cls, string_value): + for i in cls: + if i.value == string_value: + return i + + +@dataclass +class DiffChange(object): + __slots__ = ( + "before_filepath", + "after_filepath", + "change_type", + "lines_only_on_base", + "lines_only_on_head", + ) + before_filepath: typing.Optional[str] + after_filepath: typing.Optional[str] + change_type: DiffChangeType + lines_only_on_base: typing.Optional[typing.List[int]] + lines_only_on_head: typing.Optional[typing.List[int]] + + def map_base_line_to_head_line(self, base_line: int): + return self._map_this_to_other( + base_line, self.lines_only_on_base, self.lines_only_on_head + ) + + def map_head_line_to_base_line(self, head_line: int): + return self._map_this_to_other( + head_line, self.lines_only_on_head, self.lines_only_on_base + ) + + def _map_this_to_other(self, line_number, this, other): + if self.change_type in ( + DiffChangeType.binary, + DiffChangeType.deleted, + DiffChangeType.new, + ): + return None + if line_number in this: + return None + smaller_lines = sum(1 for x in this if x < line_number) + current_point = line_number - smaller_lines + for lh in other: + if lh <= current_point: + current_point += 1 + return current_point + + +# NOTE: Computationally intensive. +@sentry_sdk.trace +def parse_git_diff_json(diff_json) -> typing.List[DiffChange]: + for key, value in diff_json["diff"]["files"].items(): + change_type = DiffChangeType.get_from_string(value["type"]) + after = None if change_type == DiffChangeType.deleted else key + before = ( + None if change_type == DiffChangeType.new else (value.get("before") or key) + ) + _, additions, removals = ( + get_segment_offsets(value["segments"]) + if change_type not in (DiffChangeType.binary, DiffChangeType.deleted) + else (None, None, None) + ) + yield DiffChange( + before_filepath=before, + after_filepath=after, + change_type=DiffChangeType.get_from_string(value["type"]), + lines_only_on_base=sorted(removals) if removals is not None else None, + lines_only_on_head=sorted(additions) if additions is not None else None, + ) diff --git a/apps/worker/services/static_analysis/single_file_analyzer.py b/apps/worker/services/static_analysis/single_file_analyzer.py new file mode 100644 index 0000000000..8642707f8f --- /dev/null +++ b/apps/worker/services/static_analysis/single_file_analyzer.py @@ -0,0 +1,129 @@ +import logging +import typing +from enum import Enum, auto + +log = logging.getLogger(__name__) + + +class AntecessorFindingResult(Enum): + line = auto() + function = auto() + file = auto() + + +class SingleFileSnapshotAnalyzer(object): + """ + This is an analyzer for a single snapshot of a file (meaning a version of a file in + a particular moment of time) + + For now, the expected structure of the file snapshot is + (there can be more fields, but those are the ones being used in this context): + + empty_lines: + a list of lines that we know are empty + functions: + a list of functions/methods in this file, and its details. + The structure of a function is (some fields declared here might not be used): + declaration_line: The line where the function is declared + identifier: A unique identifier (in the global context) for the function + Something that can later help us tell that a moved function is + still the same function + start_line: The line where the function code starts + end_line: The line where the function code ends + code_hash: A hash of the function body that helps us tell when it changed + complexity_metrics: Some complexity metrics not used here + hash: The hash code of the file so its easy to tell when it has changed + language: The programming language of the file (not used here) + number_lines: The number of lines this file has + statements: A list of statements in this file. A statement structure is a tuple of two + elements: + - The first element is the line number where that statement is + - The second element is a dict with more information about that line: + - line_surety_ancestorship: It's the number of the line that we know + will be executed before this statement happens. Like + "We are sure this line will be an ancestor to this statement" + This is a way to construct a light version of the flowchart graph + of the file + start_column: The column where this code starts + line_hash: The hash of this line (to later tell line changes vs code change) + len: The number of lines (in addition to this one that this code entails) + extra_connected_lines: Which lines are not contiguous to this, but should + be considered to affect this line. One example is the "else" that indirectly + affects the "if", because it's like part of the if "jumping logic" + definition_lines: The lines where things (like classes, functions, enums) are defined + - Those don't have much use for now + import_lines: The lines where imports are. It's useful for other analysis. + But not this one + + We will eventually having a schema to validate data against this so we can ensure data + is valid when we use it. The schema will be better documentation of the format than this + """ + + def __init__(self, filepath, analysis_file_data): + self._filepath = filepath + self._analysis_file_data = analysis_file_data + self._statement_mapping = dict(analysis_file_data["statements"]) + + def get_corresponding_executable_line(self, line_number: int) -> int: + for that_line, statement_data in self._analysis_file_data["statements"]: + if ( + that_line <= line_number + and that_line + statement_data["len"] >= line_number + ): + return that_line + if line_number in statement_data["extra_connected_lines"]: + return that_line + # This is a logging.warning for now while we implement things + # But there will be a really reasonable case where customers + # change no code. So it won't have a corresponding executable line + log.warning( + "Not able to find corresponding executable line", + extra=dict( + filepath_=self._filepath, + line_number=line_number, + allstuff=self._analysis_file_data["statements"], + ), + ) + return None + + def get_antecessor_executable_line( + self, line_number: int, lines_to_not_consider: typing.List[int] + ) -> int: + current_line = line_number + while ( + current_line in lines_to_not_consider + and self._statement_mapping.get(current_line, {}).get( + "line_surety_ancestorship" + ) + and current_line + != self._statement_mapping.get(current_line, {}).get( + "line_surety_ancestorship" + ) + ): + current_line = self._statement_mapping.get(current_line, {}).get( + "line_surety_ancestorship" + ) + if current_line not in lines_to_not_consider: + return (AntecessorFindingResult.line, current_line) + for f in self._analysis_file_data["functions"]: + if ( + f.get("start_line") <= current_line + and f.get("end_line") >= current_line + ): + return (AntecessorFindingResult.function, f["identifier"]) + log.warning( + "Somehow not able to find antecessor line", + extra=dict( + filepath_=self._filepath, + line_number=line_number, + lines_to_not_consider=lines_to_not_consider, + allstuff=self._analysis_file_data["statements"], + ), + ) + return (AntecessorFindingResult.file, self._filepath) + + def find_function_by_identifier(self, function_identifier): + for func in self._analysis_file_data["functions"]: + if func["identifier"] == function_identifier: + return func + return None diff --git a/apps/worker/services/static_analysis/tests/__init__.py b/apps/worker/services/static_analysis/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/static_analysis/tests/unit/__init__.py b/apps/worker/services/static_analysis/tests/unit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/static_analysis/tests/unit/test_git_diff_parser.py b/apps/worker/services/static_analysis/tests/unit/test_git_diff_parser.py new file mode 100644 index 0000000000..d603586691 --- /dev/null +++ b/apps/worker/services/static_analysis/tests/unit/test_git_diff_parser.py @@ -0,0 +1,217 @@ +from services.static_analysis.git_diff_parser import ( + DiffChange, + DiffChangeType, + parse_git_diff_json, +) + + +class TestDiffChange(object): + def test_line_mapping_modified_file(self): + sample_git_diff_change = DiffChange( + before_filepath="README.rst", + after_filepath="README.rst", + change_type=DiffChangeType.modified, + lines_only_on_base=[12, 49, 153, 154], + lines_only_on_head=[12, 13, 50, 56, 57, 58, 59, 60, 61, 62, 161], + ) + # base to head + assert sample_git_diff_change.map_base_line_to_head_line(1) == 1 + assert sample_git_diff_change.map_base_line_to_head_line(11) == 11 + assert sample_git_diff_change.map_base_line_to_head_line(12) is None + assert sample_git_diff_change.map_base_line_to_head_line(13) == 14 + assert sample_git_diff_change.map_base_line_to_head_line(48) == 49 + assert sample_git_diff_change.map_base_line_to_head_line(49) is None + assert sample_git_diff_change.map_base_line_to_head_line(50) == 51 + # head to base + assert sample_git_diff_change.map_head_line_to_base_line(1) == 1 + assert sample_git_diff_change.map_head_line_to_base_line(11) == 11 + assert sample_git_diff_change.map_head_line_to_base_line(12) is None + assert sample_git_diff_change.map_head_line_to_base_line(13) is None + assert sample_git_diff_change.map_head_line_to_base_line(14) == 13 + assert sample_git_diff_change.map_head_line_to_base_line(49) == 48 + assert sample_git_diff_change.map_head_line_to_base_line(50) is None + assert sample_git_diff_change.map_head_line_to_base_line(51) == 50 + # next one is reasonable because there is 7 more head lines than base lines + assert sample_git_diff_change.map_head_line_to_base_line(1000) == 993 + assert sample_git_diff_change.map_base_line_to_head_line(993) == 1000 + + def test_line_mapping_deleted_file(self): + sample_git_diff_change = DiffChange( + before_filepath="README.rst", + after_filepath="README.rst", + change_type=DiffChangeType.deleted, + lines_only_on_base=None, + lines_only_on_head=None, + ) + assert sample_git_diff_change.map_head_line_to_base_line(1) is None + + def test_line_mapping_binary_file(self): + sample_git_diff_change = DiffChange( + before_filepath="README.rst", + after_filepath="README.rst", + change_type=DiffChangeType.binary, + lines_only_on_base=None, + lines_only_on_head=None, + ) + assert sample_git_diff_change.map_head_line_to_base_line(1) is None + + def test_line_mapping_new_file(self): + sample_git_diff_change = DiffChange( + before_filepath="README.rst", + after_filepath="README.rst", + change_type=DiffChangeType.new, + lines_only_on_base=None, + lines_only_on_head=None, + ) + assert sample_git_diff_change.map_head_line_to_base_line(1) is None + + +class TestParseGitDiffJson(object): + def test_parse_git_diff_json_single_file(self): + input_data = { + "diff": { + "files": { + "README.rst": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["9", "7", "9", "8"], + "lines": [ + " Overview", + " --------", + " ", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + " ", + " .. code-block:: shell-session", + " ", + ], + }, + { + "header": ["46", "12", "47", "19"], + "lines": [ + " ", + " You may need to configure a ``.coveragerc`` file. Learn more `here `_. Start with this `generic .coveragerc `_ for example.", + " ", + "-We highly suggest adding `source` to your ``.coveragerc`` which solves a number of issues collecting coverage.", + "+We highly suggest adding ``source`` to your ``.coveragerc``, which solves a number of issues collecting coverage.", + " ", + " .. code-block:: ini", + " ", + " [run]", + " source=your_package_name", + "+ ", + "+If there are multiple sources, you instead should add ``include`` to your ``.coveragerc``", + "+", + "+.. code-block:: ini", + "+", + "+ [run]", + "+ include=your_package_name/*", + " ", + " unittests", + " ---------", + ], + }, + { + "header": ["150", "5", "158", "4"], + "lines": [ + " * Twitter: `@codecov `_.", + " * Email: `hello@codecov.io `_.", + " ", + "-We are happy to help if you have any questions. Please contact email our Support at [support@codecov.io](mailto:support@codecov.io)", + "-", + "+We are happy to help if you have any questions. Please contact email our Support at `support@codecov.io `_.", + ], + }, + ], + "stats": {"added": 11, "removed": 4}, + } + } + }, + } + res = list(parse_git_diff_json(input_data)) + assert res == [ + DiffChange( + before_filepath="README.rst", + after_filepath="README.rst", + change_type=DiffChangeType.modified, + lines_only_on_base=[12, 49, 153, 154], + lines_only_on_head=[12, 13, 50, 56, 57, 58, 59, 60, 61, 62, 161], + ) + ] + + def test_parse_git_diff_json_multiple_files(self): + input_data = { + "files": { + "banana.py": { + "type": "new", + "before": None, + "segments": [ + { + "header": ["0", "0", "1", "2"], + "lines": ["+suhduad", "+dsandsa"], + } + ], + "stats": {"added": 2, "removed": 0}, + }, + "codecov-alpine": { + "type": "binary", + "stats": {"added": 0, "removed": 0}, + }, + "codecov/settings_dev.py": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["49", "3", "49", "4"], + "lines": [ + ' SESSION_COOKIE_DOMAIN = "localhost"', + " ", + " GRAPHQL_PLAYGROUND = True", + "+IS_DEV = True", + ], + } + ], + "stats": {"added": 1, "removed": 0}, + }, + "production.yml": { + "type": "deleted", + "before": "production.yml", + "stats": {"added": 0, "removed": 0}, + }, + } + } + expected_result = [ + DiffChange( + before_filepath=None, + after_filepath="banana.py", + change_type=DiffChangeType.new, + lines_only_on_base=[], + lines_only_on_head=[1, 2], + ), + DiffChange( + before_filepath="codecov-alpine", + after_filepath="codecov-alpine", + change_type=DiffChangeType.binary, + lines_only_on_base=None, + lines_only_on_head=None, + ), + DiffChange( + before_filepath="codecov/settings_dev.py", + after_filepath="codecov/settings_dev.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[], + lines_only_on_head=[52], + ), + DiffChange( + before_filepath="production.yml", + after_filepath=None, + change_type=DiffChangeType.deleted, + lines_only_on_base=None, + lines_only_on_head=None, + ), + ] + res = list(parse_git_diff_json({"diff": input_data})) + assert res == expected_result diff --git a/apps/worker/services/static_analysis/tests/unit/test_single_file_analyzer.py b/apps/worker/services/static_analysis/tests/unit/test_single_file_analyzer.py new file mode 100644 index 0000000000..f920cb4568 --- /dev/null +++ b/apps/worker/services/static_analysis/tests/unit/test_single_file_analyzer.py @@ -0,0 +1,107 @@ +from services.static_analysis.single_file_analyzer import ( + AntecessorFindingResult, + SingleFileSnapshotAnalyzer, +) + +# While the structure of this is correct, the data itself was manually edited +# to make interesting test cases +sample_input_data = { + "empty_lines": [4, 8, 11], + "warnings": [], + "filename": "source.py", + "functions": [ + { + "identifier": "some_function", + "start_line": 5, + "end_line": 10, + "code_hash": "e4b52b6da12184142fcd7ff2c8412662", + "complexity_metrics": { + "conditions": 1, + "mccabe_cyclomatic_complexity": 2, + "returns": 1, + "max_nested_conditional": 1, + }, + } + ], + "hash": "811d0016249a5b1400a685164e5295de", + "language": "python", + "number_lines": 11, + "statements": [ + ( + 1, + { + "line_surety_ancestorship": None, + "start_column": 0, + "line_hash": "55c30cf01e202728b6952e9cba304798", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 2, + { + "line_surety_ancestorship": 1, + "start_column": 4, + "line_hash": "1d7be9f2145760a59513a4049fcd0d1c", + "len": 1, + "extra_connected_lines": (), + }, + ), + ( + 5, + { + "line_surety_ancestorship": None, + "start_column": 4, + "line_hash": "1d7be9f2145760a59513a4049fcd0d1c", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 6, + { + "line_surety_ancestorship": 5, + "start_column": 4, + "line_hash": "52f98812dca4687f18373b87433df695", + "len": 0, + "extra_connected_lines": (14,), + }, + ), + ( + 7, + { + "line_surety_ancestorship": 6, + "start_column": 4, + "line_hash": "52f98812dca4687f18373b87433df695", + "len": 0, + "extra_connected_lines": (), + }, + ), + ], + "definition_lines": [(4, 6)], + "import_lines": [], +} + + +def test_simple_single_file_snapshot_analyzer_get_corresponding_executable_line(): + sfsa = SingleFileSnapshotAnalyzer("filepath", sample_input_data) + assert sfsa.get_corresponding_executable_line(3) == 2 + assert sfsa.get_corresponding_executable_line(2) == 2 + assert sfsa.get_corresponding_executable_line(4) is None + assert sfsa.get_corresponding_executable_line(14) == 6 + + +def test_get_antecessor_executable_line(): + sfsa = SingleFileSnapshotAnalyzer("filepath", sample_input_data) + assert sfsa.get_antecessor_executable_line(7, lines_to_not_consider=[6, 7]) == ( + AntecessorFindingResult.line, + 5, + ) + assert sfsa.get_antecessor_executable_line(2, lines_to_not_consider=[1, 2]) == ( + AntecessorFindingResult.file, + "filepath", + ) + assert sfsa.get_antecessor_executable_line(5, lines_to_not_consider=[5]) == ( + AntecessorFindingResult.function, + "some_function", + ) diff --git a/apps/worker/services/static_analysis/tests/unit/test_static_analysis_comparison.py b/apps/worker/services/static_analysis/tests/unit/test_static_analysis_comparison.py new file mode 100644 index 0000000000..c14f519e69 --- /dev/null +++ b/apps/worker/services/static_analysis/tests/unit/test_static_analysis_comparison.py @@ -0,0 +1,954 @@ +import json + +import pytest + +from database.tests.factories.core import RepositoryFactory +from database.tests.factories.staticanalysis import ( + StaticAnalysisSingleFileSnapshotFactory, + StaticAnalysisSuiteFactory, + StaticAnalysisSuiteFilepathFactory, +) +from services.static_analysis import ( + SingleFileSnapshotAnalyzer, + StaticAnalysisComparisonService, + _get_analysis_content_mapping, +) +from services.static_analysis.git_diff_parser import DiffChange, DiffChangeType + + +def test_get_analysis_content_mapping(dbsession): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + static_analysis_suite = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + secondary_static_analysis = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + dbsession.add(static_analysis_suite) + dbsession.add(secondary_static_analysis) + dbsession.flush() + snapshot_1 = StaticAnalysisSingleFileSnapshotFactory.create(repository=repository) + snapshot_2 = StaticAnalysisSingleFileSnapshotFactory.create(repository=repository) + snapshot_3 = StaticAnalysisSingleFileSnapshotFactory.create(repository=repository) + snapshot_4 = StaticAnalysisSingleFileSnapshotFactory.create(repository=repository) + snapshot_5 = StaticAnalysisSingleFileSnapshotFactory.create(repository=repository) + dbsession.add_all([snapshot_1, snapshot_2, snapshot_3, snapshot_4, snapshot_5]) + dbsession.flush() + f_1 = StaticAnalysisSuiteFilepathFactory.create( + file_snapshot=snapshot_1, analysis_suite=static_analysis_suite + ) + f_2 = StaticAnalysisSuiteFilepathFactory.create( + file_snapshot=snapshot_2, analysis_suite=static_analysis_suite + ) + f_3 = StaticAnalysisSuiteFilepathFactory.create( + file_snapshot=snapshot_3, analysis_suite=static_analysis_suite + ) + f_4 = StaticAnalysisSuiteFilepathFactory.create( + file_snapshot=snapshot_4, analysis_suite=static_analysis_suite + ) + f_s_2 = StaticAnalysisSuiteFilepathFactory.create( + file_snapshot=snapshot_2, + analysis_suite=secondary_static_analysis, + filepath=f_1.filepath, + ) + f_s_3 = StaticAnalysisSuiteFilepathFactory.create( + file_snapshot=snapshot_3, analysis_suite=secondary_static_analysis + ) + f_s_5 = StaticAnalysisSuiteFilepathFactory.create( + file_snapshot=snapshot_5, analysis_suite=secondary_static_analysis + ) + dbsession.add_all([f_1, f_2, f_3, f_4, f_s_2, f_s_3, f_s_5]) + dbsession.flush() + first_res = _get_analysis_content_mapping( + static_analysis_suite, + [f_1.filepath, f_2.filepath, f_4.filepath, "somenonexistent.gh"], + ) + assert first_res == { + f_1.filepath: snapshot_1.content_location, + f_2.filepath: snapshot_2.content_location, + f_4.filepath: snapshot_4.content_location, + } + secondary_res = _get_analysis_content_mapping( + secondary_static_analysis, + [f_s_2.filepath, f_s_3.filepath], + ) + assert secondary_res == { + f_s_2.filepath: snapshot_2.content_location, + f_s_3.filepath: snapshot_3.content_location, + } + + +@pytest.fixture() +def sample_service(dbsession): + repository = RepositoryFactory.create() + head_static_analysis = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + base_static_analysis = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + dbsession.add(head_static_analysis) + dbsession.add(base_static_analysis) + dbsession.flush() + return StaticAnalysisComparisonService( + base_static_analysis=base_static_analysis, + head_static_analysis=head_static_analysis, + git_diff=[ + DiffChange( + before_filepath="path/changed.py", + after_filepath="path/changed.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[], + lines_only_on_head=[20], + ), + ], + ) + + +class TestStaticAnalysisComparisonService(object): + def test_load_snapshot_data_unhappy_cases(self, sample_service, mock_storage): + assert sample_service._load_snapshot_data("filepath", None) is None + assert sample_service._load_snapshot_data("filepath", "fake_location") is None + + def test_load_snapshot_data_happy_cases(self, sample_service, mock_storage): + mock_storage.write_file( + "archive", + "real_content_location", + json.dumps({"statements": [(1, {"ha": "pokemon"})]}), + ) + res = sample_service._load_snapshot_data("filepath", "real_content_location") + assert isinstance(res, SingleFileSnapshotAnalyzer) + assert res._filepath == "filepath" + assert res._analysis_file_data == {"statements": [[1, {"ha": "pokemon"}]]} + assert res._statement_mapping == {1: {"ha": "pokemon"}} + + def test_get_base_lines_relevant_to_change_deleted_plus_changed_normal( + self, dbsession, mock_storage + ): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + snapshot_deleted = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository + ) + changed_snapshot_base = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository + ) + changed_snapshot_head = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository + ) + dbsession.add_all( + [ + snapshot_deleted, + changed_snapshot_base, + changed_snapshot_head, + ] + ) + dbsession.flush() + mock_storage.write_file( + "archive", snapshot_deleted.content_location, json.dumps({"statements": []}) + ) + mock_storage.write_file( + "archive", + changed_snapshot_base.content_location, + json.dumps( + { + "statements": [ + ( + 30, + { + "len": 1, + "line_surety_ancestorship": 29, + "extra_connected_lines": [35], + }, + ), + ] + } + ), + ) + mock_storage.write_file( + "archive", + changed_snapshot_head.content_location, + json.dumps( + { + "functions": [], + "statements": [ + (1, {"len": 0, "extra_connected_lines": []}), + (2, {"len": 1, "extra_connected_lines": []}), + (8, {"len": 0, "extra_connected_lines": []}), + ( + 10, + { + "len": 1, + "line_surety_ancestorship": 8, + "extra_connected_lines": [20], + }, + ), + ], + } + ), + ) + head_static_analysis = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + base_static_analysis = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + dbsession.add(head_static_analysis) + dbsession.add(base_static_analysis) + dbsession.flush() + deleted_sasff = StaticAnalysisSuiteFilepathFactory.create( + file_snapshot=snapshot_deleted, + analysis_suite=base_static_analysis, + filepath="deleted.py", + ) + old_changed_sasff = StaticAnalysisSuiteFilepathFactory.create( + file_snapshot=changed_snapshot_base, + analysis_suite=base_static_analysis, + filepath="path/changed.py", + ) + new_changed_sasff = StaticAnalysisSuiteFilepathFactory.create( + file_snapshot=changed_snapshot_head, + analysis_suite=head_static_analysis, + filepath="path/changed.py", + ) + dbsession.add_all([deleted_sasff, old_changed_sasff, new_changed_sasff]) + dbsession.flush() + service = StaticAnalysisComparisonService( + base_static_analysis=base_static_analysis, + head_static_analysis=head_static_analysis, + git_diff=[ + DiffChange( + before_filepath="path/changed.py", + after_filepath="path/changed.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[30], + lines_only_on_head=[20], + ), + DiffChange( + before_filepath="deleted.py", + after_filepath=None, + change_type=DiffChangeType.deleted, + lines_only_on_base=None, + lines_only_on_head=None, + ), + ], + ) + assert service.get_base_lines_relevant_to_change() == { + "all": False, + "files": { + "deleted.py": {"all": True, "lines": None}, + "path/changed.py": {"all": False, "lines": {8, 30}}, + }, + } + + def test_get_base_lines_relevant_to_change_one_new_file( + self, dbsession, mock_storage + ): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + snapshot_deleted = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository + ) + changed_snapshot_base = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository + ) + changed_snapshot_head = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository + ) + dbsession.add_all( + [ + snapshot_deleted, + changed_snapshot_base, + changed_snapshot_head, + ] + ) + dbsession.flush() + mock_storage.write_file( + "archive", snapshot_deleted.content_location, json.dumps({"statements": []}) + ) + mock_storage.write_file( + "archive", + changed_snapshot_base.content_location, + json.dumps({"statements": [(1, {})]}), + ) + mock_storage.write_file( + "archive", + changed_snapshot_head.content_location, + json.dumps( + { + "functions": [], + "statements": [ + (1, {"len": 0, "extra_connected_lines": []}), + (2, {"len": 1, "extra_connected_lines": []}), + (8, {"len": 0, "extra_connected_lines": []}), + ( + 10, + { + "len": 1, + "line_surety_ancestorship": 8, + "extra_connected_lines": [20], + }, + ), + ], + } + ), + ) + head_static_analysis = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + base_static_analysis = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + dbsession.add(head_static_analysis) + dbsession.add(base_static_analysis) + dbsession.flush() + deleted_sasff = StaticAnalysisSuiteFilepathFactory.create( + file_snapshot=snapshot_deleted, + analysis_suite=base_static_analysis, + filepath="deleted.py", + ) + old_changed_sasff = StaticAnalysisSuiteFilepathFactory.create( + file_snapshot=changed_snapshot_base, + analysis_suite=base_static_analysis, + filepath="path/changed.py", + ) + new_changed_sasff = StaticAnalysisSuiteFilepathFactory.create( + file_snapshot=changed_snapshot_head, + analysis_suite=head_static_analysis, + filepath="path/changed.py", + ) + dbsession.add_all([deleted_sasff, old_changed_sasff, new_changed_sasff]) + dbsession.flush() + service = StaticAnalysisComparisonService( + base_static_analysis=base_static_analysis, + head_static_analysis=head_static_analysis, + git_diff=[ + DiffChange( + before_filepath="path/changed.py", + after_filepath="path/changed.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[], + lines_only_on_head=[20], + ), + DiffChange( + before_filepath=None, + after_filepath="path/new.py", + change_type=DiffChangeType.new, + lines_only_on_base=[], + lines_only_on_head=[20], + ), + DiffChange( + before_filepath="deleted.py", + after_filepath=None, + change_type=DiffChangeType.deleted, + lines_only_on_base=None, + lines_only_on_head=None, + ), + ], + ) + assert service.get_base_lines_relevant_to_change() == {"all": True} + + def test_analyze_single_change_first_line_file(self, dbsession, mock_storage): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + changed_snapshot_base = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository + ) + changed_snapshot_head = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository + ) + dbsession.add_all( + [ + changed_snapshot_base, + changed_snapshot_head, + ] + ) + dbsession.flush() + mock_storage.write_file( + "archive", + changed_snapshot_base.content_location, + json.dumps( + { + "statements": [ + ( + 6, + { + "len": 1, + "extra_connected_lines": [9], + }, + ), + ] + } + ), + ) + mock_storage.write_file( + "archive", + changed_snapshot_head.content_location, + json.dumps( + { + "functions": [], + "statements": [ + ( + 10, + { + "len": 0, + "extra_connected_lines": [20], + }, + ), + ( + 11, + { + "len": 0, + "line_surety_ancestorship": 10, + "extra_connected_lines": [], + }, + ), + (12, {"len": 1, "extra_connected_lines": []}), + ( + 18, + { + "len": 0, + "line_surety_ancestorship": 12, + "extra_connected_lines": [], + }, + ), + ], + } + ), + ) + head_static_analysis = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + base_static_analysis = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + dbsession.add(head_static_analysis) + dbsession.add(base_static_analysis) + dbsession.flush() + change = DiffChange( + before_filepath="path/changed.py", + after_filepath="path/changed.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[9], + lines_only_on_head=[11], + ) + service = StaticAnalysisComparisonService( + base_static_analysis=base_static_analysis, + head_static_analysis=head_static_analysis, + git_diff=[change], + ) + assert service._analyze_single_change( + dbsession, + change, + changed_snapshot_base.content_location, + changed_snapshot_head.content_location, + ) == {"all": False, "lines": {6, 10}} + + def test_analyze_single_change_base_change(self, dbsession, mock_storage): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + changed_snapshot_base = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository + ) + changed_snapshot_head = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository + ) + dbsession.add_all( + [ + changed_snapshot_base, + changed_snapshot_head, + ] + ) + dbsession.flush() + mock_storage.write_file( + "archive", + changed_snapshot_base.content_location, + json.dumps( + { + "functions": [ + { + "identifier": "banana_function", + "start_line": 3, + "end_line": 8, + } + ], + "statements": [ + ( + 1, + { + "len": 0, + "line_surety_ancestorship": None, + "extra_connected_lines": [], + }, + ), + ( + 2, + { + "len": 0, + "line_surety_ancestorship": 1, + "extra_connected_lines": [], + }, + ), + ], + } + ), + ) + mock_storage.write_file( + "archive", + changed_snapshot_head.content_location, + json.dumps( + { + "functions": [ + { + "identifier": "banana_function", + "start_line": 3, + "end_line": 8, + } + ], + "statements": [ + ( + 10, + { + "len": 0, + "extra_connected_lines": [20], + }, + ), + ( + 11, + { + "len": 0, + "line_surety_ancestorship": 10, + "extra_connected_lines": [], + }, + ), + (12, {"len": 1, "extra_connected_lines": []}), + ( + 18, + { + "len": 0, + "line_surety_ancestorship": 12, + "extra_connected_lines": [], + }, + ), + ], + } + ), + ) + head_static_analysis = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + base_static_analysis = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + dbsession.add(head_static_analysis) + dbsession.add(base_static_analysis) + dbsession.flush() + service = StaticAnalysisComparisonService( + base_static_analysis=base_static_analysis, + head_static_analysis=head_static_analysis, + git_diff=[ + DiffChange( + before_filepath="path/changed.py", + after_filepath="path/changed.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[], + lines_only_on_head=[20], + ), + ], + ) + assert service._analyze_single_change( + dbsession, + DiffChange( + before_filepath="path/changed.py", + after_filepath="path/changed.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[], + lines_only_on_head=[20], + ), + changed_snapshot_base.content_location, + changed_snapshot_head.content_location, + ) == {"all": True, "lines": None} + assert service._analyze_single_change( + dbsession, + DiffChange( + before_filepath="path/changed.py", + after_filepath="path/changed.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[], + lines_only_on_head=[11], + ), + changed_snapshot_base.content_location, + changed_snapshot_head.content_location, + ) == {"all": False, "lines": {10}} + assert service._analyze_single_change( + dbsession, + DiffChange( + before_filepath="path/changed.py", + after_filepath="path/changed.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[], + lines_only_on_head=[99, 100], + ), + changed_snapshot_base.content_location, + changed_snapshot_head.content_location, + ) == {"all": False, "lines": set()} + + def test_analyze_single_change_base_change_missing_head_snapshot( + self, dbsession, mock_storage + ): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + changed_snapshot_base = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository + ) + changed_snapshot_head = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository + ) + dbsession.add_all( + [ + changed_snapshot_base, + changed_snapshot_head, + ] + ) + dbsession.flush() + mock_storage.write_file( + "archive", + changed_snapshot_base.content_location, + json.dumps( + { + "functions": [ + { + "identifier": "banana_function", + "start_line": 3, + "end_line": 8, + } + ], + "statements": [ + ( + 1, + { + "len": 0, + "line_surety_ancestorship": None, + "extra_connected_lines": [], + }, + ), + ( + 2, + { + "len": 0, + "line_surety_ancestorship": 1, + "extra_connected_lines": [], + }, + ), + ], + } + ), + ) + head_static_analysis = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + base_static_analysis = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + dbsession.add(head_static_analysis) + dbsession.add(base_static_analysis) + dbsession.flush() + service = StaticAnalysisComparisonService( + base_static_analysis=base_static_analysis, + head_static_analysis=head_static_analysis, + git_diff=[ + DiffChange( + before_filepath="path/changed.py", + after_filepath="path/changed.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[], + lines_only_on_head=[20], + ), + ], + ) + assert service._analyze_single_change( + dbsession, + DiffChange( + before_filepath="path/changed.py", + after_filepath="path/changed.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[], + lines_only_on_head=[20], + ), + changed_snapshot_base.content_location, + changed_snapshot_head.content_location, + ) == {"all": True, "lines": None} + assert service._analyze_single_change( + dbsession, + DiffChange( + before_filepath="path/changed.py", + after_filepath="path/changed.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[], + lines_only_on_head=[11], + ), + changed_snapshot_base.content_location, + changed_snapshot_head.content_location, + ) == {"all": True, "lines": None} + assert service._analyze_single_change( + dbsession, + DiffChange( + before_filepath="path/changed.py", + after_filepath="path/changed.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[], + lines_only_on_head=[99, 100], + ), + changed_snapshot_base.content_location, + changed_snapshot_head.content_location, + ) == {"all": True, "lines": None} + + def test_analyze_single_change_function_based(self, dbsession, mock_storage): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + changed_snapshot_base = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository + ) + changed_snapshot_head = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository + ) + dbsession.add_all( + [ + changed_snapshot_base, + changed_snapshot_head, + ] + ) + dbsession.flush() + mock_storage.write_file( + "archive", + changed_snapshot_base.content_location, + json.dumps( + { + "functions": [ + { + "identifier": "banana_function", + "start_line": 3, + "end_line": 8, + } + ], + "statements": [(1, {})], + } + ), + ) + mock_storage.write_file( + "archive", + changed_snapshot_head.content_location, + json.dumps( + { + "functions": [ + { + "identifier": "banana_function", + "start_line": 9, + "end_line": 11, + } + ], + "statements": [ + ( + 10, + { + "len": 1, + "extra_connected_lines": [20], + }, + ), + ( + 11, + { + "len": 0, + "line_surety_ancestorship": 10, + "extra_connected_lines": [], + }, + ), + (12, {"len": 1, "extra_connected_lines": []}), + ( + 18, + { + "len": 0, + "line_surety_ancestorship": 12, + "extra_connected_lines": [], + }, + ), + ], + } + ), + ) + head_static_analysis = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + base_static_analysis = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + dbsession.add(head_static_analysis) + dbsession.add(base_static_analysis) + dbsession.flush() + service = StaticAnalysisComparisonService( + base_static_analysis=base_static_analysis, + head_static_analysis=head_static_analysis, + git_diff=[ + DiffChange( + before_filepath="path/changed.py", + after_filepath="path/changed.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[], + lines_only_on_head=[20], + ), + ], + ) + change = DiffChange( + before_filepath="path/changed.py", + after_filepath="path/changed.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[], + lines_only_on_head=[20], + ) + assert service._analyze_single_change( + dbsession, + change, + changed_snapshot_base.content_location, + changed_snapshot_head.content_location, + ) == {"all": False, "lines": {3}} + + def test_analyze_single_change_no_static_analysis_found( + self, dbsession, mock_storage, mocker, sample_service + ): + mocked_load_snapshot = mocker.patch.object( + StaticAnalysisComparisonService, "_load_snapshot_data", return_value=None + ) + change = DiffChange( + before_filepath="path/changed.py", + after_filepath="path/changed.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[], + lines_only_on_head=[20], + ) + first_location, second_location = mocker.MagicMock(), mocker.MagicMock() + assert ( + sample_service._analyze_single_change( + dbsession, + change, + first_location, + second_location, + ) + is None + ) + assert mocked_load_snapshot.call_count == 2 + mocked_load_snapshot.assert_any_call("path/changed.py", second_location) + mocked_load_snapshot.assert_any_call("path/changed.py", first_location) + + def test_analyze_single_change_function_based_no_function_found( + self, dbsession, mock_storage + ): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + changed_snapshot_base = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository + ) + changed_snapshot_head = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository + ) + dbsession.add_all( + [ + changed_snapshot_base, + changed_snapshot_head, + ] + ) + dbsession.flush() + mock_storage.write_file( + "archive", + changed_snapshot_base.content_location, + json.dumps( + { + "functions": [], + "statements": [(1, {})], + } + ), + ) + mock_storage.write_file( + "archive", + changed_snapshot_head.content_location, + json.dumps( + { + "functions": [ + { + "identifier": "banana_function", + "start_line": 9, + "end_line": 11, + } + ], + "statements": [ + ( + 10, + { + "len": 1, + "extra_connected_lines": [20], + }, + ), + ( + 11, + { + "len": 0, + "line_surety_ancestorship": 10, + "extra_connected_lines": [], + }, + ), + (12, {"len": 1, "extra_connected_lines": []}), + ( + 18, + { + "len": 0, + "line_surety_ancestorship": 12, + "extra_connected_lines": [], + }, + ), + ], + } + ), + ) + head_static_analysis = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + base_static_analysis = StaticAnalysisSuiteFactory.create( + commit__repository=repository + ) + dbsession.add(head_static_analysis) + dbsession.add(base_static_analysis) + dbsession.flush() + service = StaticAnalysisComparisonService( + base_static_analysis=base_static_analysis, + head_static_analysis=head_static_analysis, + git_diff=[ + DiffChange( + before_filepath="path/changed.py", + after_filepath="path/changed.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[], + lines_only_on_head=[20], + ), + ], + ) + change = DiffChange( + before_filepath="path/changed.py", + after_filepath="path/changed.py", + change_type=DiffChangeType.modified, + lines_only_on_base=[], + lines_only_on_head=[20], + ) + assert service._analyze_single_change( + dbsession, + change, + changed_snapshot_base.content_location, + changed_snapshot_head.content_location, + ) == {"all": True, "lines": None} diff --git a/apps/worker/services/stripe.py b/apps/worker/services/stripe.py new file mode 100644 index 0000000000..2574b7cf85 --- /dev/null +++ b/apps/worker/services/stripe.py @@ -0,0 +1,8 @@ +import stripe +from shared.config import get_config + +stripe.api_key = get_config("services", "stripe", "api_key") +stripe.api_version = "2024-12-18.acacia" + +client = stripe.http_client.RequestsClient() +stripe.default_http_client = client diff --git a/apps/worker/services/template.py b/apps/worker/services/template.py new file mode 100644 index 0000000000..5c69c74a4d --- /dev/null +++ b/apps/worker/services/template.py @@ -0,0 +1,18 @@ +from jinja2 import Environment, PackageLoader, StrictUndefined, select_autoescape + + +class TemplateService: + env = None + + def __init__(self): + if TemplateService.env is None: + # this loads the templates from the templates directory in this repository since it's looking for a dir named `templates` next to app.py + TemplateService.env = Environment( + loader=PackageLoader("app"), + autoescape=select_autoescape(), + undefined=StrictUndefined, + ) + + def get_template(self, name): + template = TemplateService.env.get_template(f"{name}") + return template diff --git a/apps/worker/services/test_analytics/ta_cache_rollups.py b/apps/worker/services/test_analytics/ta_cache_rollups.py new file mode 100644 index 0000000000..fdca2c7a4b --- /dev/null +++ b/apps/worker/services/test_analytics/ta_cache_rollups.py @@ -0,0 +1,83 @@ +from datetime import UTC +from io import BytesIO + +import polars as pl +import shared.storage + +from django_scaffold import settings +from services.test_analytics.ta_metrics import ( + read_rollups_from_db_summary, + rollup_size_summary, +) +from services.test_analytics.ta_timeseries import ( + get_branch_summary, + get_summary, + get_testrun_branch_summary_via_testrun, +) + + +def rollup_blob_path(repoid: int, branch: str | None = None) -> str: + return ( + f"test_analytics/branch_rollups/{repoid}/{branch}.arrow" + if branch + else f"test_analytics/repo_rollups/{repoid}.arrow" + ) + + +POLARS_SCHEMA = [ + "computed_name", + ("flags", pl.List(pl.String)), + "failing_commits", + "last_duration", + "avg_duration", + "pass_count", + "fail_count", + "flaky_fail_count", + "skip_count", + ("updated_at", pl.Datetime(time_zone=UTC)), + "timestamp_bin", +] + + +def cache_rollups(repoid: int, branch: str | None = None): + storage_service = shared.storage.get_appropriate_storage_service(repoid) + serialized_table: BytesIO + + with read_rollups_from_db_summary.labels("new").time(): + if branch: + if branch in {"main", "master", "develop"}: + summaries = get_branch_summary(repoid, branch) + else: + summaries = get_testrun_branch_summary_via_testrun(repoid, branch) + else: + summaries = get_summary(repoid) + + data = [ + { + "computed_name": summary.computed_name, + "flags": summary.flags, + "failing_commits": summary.failing_commits, + "last_duration": summary.last_duration_seconds, + "avg_duration": summary.avg_duration_seconds, + "pass_count": summary.pass_count, + "fail_count": summary.fail_count, + "flaky_fail_count": summary.flaky_fail_count, + "skip_count": summary.skip_count, + "updated_at": summary.updated_at, + "timestamp_bin": summary.timestamp_bin.date(), + } + for summary in summaries + ] + + serialized_table = pl.DataFrame( + data, + POLARS_SCHEMA, + orient="row", + ).write_ipc(None) + + serialized_table.seek(0) + + storage_service.write_file( + settings.GCS_BUCKET_NAME, rollup_blob_path(repoid, branch), serialized_table + ) + rollup_size_summary.labels("new").observe(serialized_table.tell()) diff --git a/apps/worker/services/test_analytics/ta_metrics.py b/apps/worker/services/test_analytics/ta_metrics.py new file mode 100644 index 0000000000..002d5778de --- /dev/null +++ b/apps/worker/services/test_analytics/ta_metrics.py @@ -0,0 +1,39 @@ +from shared.metrics import Summary + +write_tests_summary = Summary( + "write_tests_summary", + "The time it takes to write tests to the database", + ["impl"], +) + +read_tests_totals_summary = Summary( + "read_tests_totals_summary", + "The time it takes to read tests totals from the database", + ["impl"], +) + +read_failures_summary = Summary( + "read_failures_summary", + "The time it takes to read failures from the database", + ["impl"], +) + + +read_rollups_from_db_summary = Summary( + "read_rollups_from_db_summary", + "The time it takes to read rollups from the database", + ["impl"], +) + +rollup_size_summary = Summary( + "rollup_size_summary", + "The size of the rollup", + ["impl"], +) + + +process_flakes_summary = Summary( + "process_flakes_summary", + "The time it takes to process flakes", + ["impl"], +) diff --git a/apps/worker/services/test_analytics/ta_process_flakes.py b/apps/worker/services/test_analytics/ta_process_flakes.py new file mode 100644 index 0000000000..7ae87564ba --- /dev/null +++ b/apps/worker/services/test_analytics/ta_process_flakes.py @@ -0,0 +1,123 @@ +import logging +from datetime import datetime + +from django.db import transaction +from django.db.models import Q, QuerySet +from redis.exceptions import LockError +from shared.django_apps.reports.models import CommitReport, ReportSession +from shared.django_apps.ta_timeseries.models import Testrun +from shared.django_apps.test_analytics.models import Flake +from shared.helpers.redis import get_redis_connection + +from services.test_analytics.ta_metrics import process_flakes_summary + +log = logging.getLogger(__name__) + +FAIL_FILTER = Q(outcome="failure") | Q(outcome="flaky_failure") | Q(outcome="error") + +LOCK_NAME = "ta_flake_lock:{}" +KEY_NAME = "ta_flake_key:{}" + + +def get_relevant_uploads(repo_id: int, commit_id: str) -> QuerySet[ReportSession]: + return ReportSession.objects.filter( + report__report_type=CommitReport.ReportType.TEST_RESULTS.value, + report__commit__repository__repoid=repo_id, + report__commit__commitid=commit_id, + state__in=["processed"], + ) + + +def fetch_current_flakes(repo_id: int) -> dict[bytes, Flake]: + return { + bytes(flake.test_id): flake for flake in Flake.objects.filter(repoid=repo_id) + } + + +def get_testruns( + upload: ReportSession, curr_flakes: dict[bytes, Flake] +) -> QuerySet[Testrun]: + upload_filter = Q(upload_id=upload.id) + flaky_pass_filter = Q(outcome="pass") & Q(test_id__in=curr_flakes.keys()) + return Testrun.objects.filter(upload_filter & (FAIL_FILTER | flaky_pass_filter)) + + +def handle_pass(curr_flakes: dict[bytes, Flake], test_id: bytes): + # possible that we expire it and stop caring about it + if test_id not in curr_flakes: + return + + curr_flakes[test_id].recent_passes_count += 1 + curr_flakes[test_id].count += 1 + if curr_flakes[test_id].recent_passes_count == 30: + curr_flakes[test_id].end_date = datetime.now() + curr_flakes[test_id].save() + del curr_flakes[test_id] + + +def handle_failure( + curr_flakes: dict[bytes, Flake], test_id: bytes, testrun: Testrun, repo_id: int +): + existing_flake = curr_flakes.get(test_id) + if existing_flake: + existing_flake.fail_count += 1 + existing_flake.count += 1 + existing_flake.recent_passes_count = 0 + else: + if testrun.outcome != "flaky_failure": + testrun.outcome = "flaky_failure" + new_flake = Flake( + repoid=repo_id, + test_id=test_id, + count=1, + fail_count=1, + recent_passes_count=0, + start_date=datetime.now(), + ) + curr_flakes[test_id] = new_flake + + +@process_flakes_summary.labels("new").time() +def process_flakes_for_commit(repo_id: int, commit_id: str): + uploads = get_relevant_uploads(repo_id, commit_id) + + curr_flakes = fetch_current_flakes(repo_id) + + for upload in uploads: + testruns = get_testruns(upload, curr_flakes) + + for testrun in testruns: + test_id = bytes(testrun.test_id) + match testrun.outcome: + case "pass": + handle_pass(curr_flakes, test_id) + case "failure" | "flaky_failure" | "error": + handle_failure(curr_flakes, test_id, testrun, repo_id) + case _: + continue + + Testrun.objects.bulk_update(testruns, ["outcome"]) + + Flake.objects.bulk_create( + curr_flakes.values(), + update_conflicts=True, + unique_fields=["id"], + update_fields=["end_date", "count", "recent_passes_count", "fail_count"], + ) + + transaction.commit() + + +def process_flakes_for_repo(repo_id: int): + redis_client = get_redis_connection() + lock_name = LOCK_NAME.format(repo_id) + key_name = KEY_NAME.format(repo_id) + try: + with redis_client.lock(lock_name, timeout=300, blocking_timeout=3): + while commit_ids := redis_client.lpop(key_name, 10): + for commit_id in commit_ids: + process_flakes_for_commit(repo_id, commit_id.decode()) + return True + except LockError: + log.warning("Failed to acquire lock for repo %s", repo_id) + return False diff --git a/apps/worker/services/test_analytics/ta_processing.py b/apps/worker/services/test_analytics/ta_processing.py new file mode 100644 index 0000000000..fa26ba69dc --- /dev/null +++ b/apps/worker/services/test_analytics/ta_processing.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +import sentry_sdk +import test_results_parser +from shared.config import get_config +from shared.django_apps.core.models import Commit, Repository +from shared.django_apps.reports.models import ReportSession, UploadError + +from services.archive import ArchiveService +from services.test_analytics.ta_timeseries import get_flaky_tests_set, insert_testrun +from services.yaml import UserYaml, read_yaml_field + + +@dataclass +class TAProcInfo: + repository: Repository + branch: str | None + user_yaml: UserYaml + + +def handle_file_not_found(upload: ReportSession): + upload.state = "processed" + upload.save() + UploadError.objects.create( + report_session=upload, + error_code="file_not_in_storage", + error_params={}, + ) + + +def handle_parsing_error(upload: ReportSession, exc: Exception): + sentry_sdk.capture_exception(exc, tags={"upload_state": upload.state}) + upload.state = "processed" + upload.save() + UploadError.objects.create( + report_session=upload, + error_code="unsupported_file_format", + error_params={"error_message": str(exc)}, + ) + + +def get_ta_processing_info( + repoid: int, + commitid: str, + commit_yaml: dict[str, Any], +) -> TAProcInfo: + repository = Repository.objects.get(repoid=repoid) + + commit = Commit.objects.get(repository=repository, commitid=commitid) + branch = commit.branch + if branch is None: + raise ValueError("Branch is None") + + user_yaml: UserYaml = UserYaml(commit_yaml) + return TAProcInfo( + repository, + branch, + user_yaml, + ) + + +def should_delete_archive_settings(user_yaml: UserYaml) -> bool: + if get_config("services", "minio", "expire_raw_after_n_days"): + return True + return not read_yaml_field(user_yaml, ("codecov", "archive", "uploads"), _else=True) + + +def rewrite_or_delete_upload( + archive_service: ArchiveService, + user_yaml: UserYaml, + upload: ReportSession, + readable_file: bytes, +): + if should_delete_archive_settings(user_yaml): + archive_url = upload.storage_path + if archive_url and not archive_url.startswith("http"): + archive_service.delete_file(archive_url) + else: + archive_service.write_file(upload.storage_path, bytes(readable_file)) + + +def insert_testruns_timeseries( + repoid: int, + commitid: str, + branch: str | None, + upload: ReportSession, + parsing_infos: list[test_results_parser.ParsingInfo], +): + flaky_test_set = get_flaky_tests_set(repoid) + + for parsing_info in parsing_infos: + insert_testrun( + timestamp=upload.created_at, + repo_id=repoid, + commit_sha=commitid, + branch=branch, + upload_id=upload.id, + flags=upload.flag_names, + parsing_info=parsing_info, + flaky_test_ids=flaky_test_set, + ) diff --git a/apps/worker/services/test_analytics/ta_processor.py b/apps/worker/services/test_analytics/ta_processor.py new file mode 100644 index 0000000000..0b1dc7cdd6 --- /dev/null +++ b/apps/worker/services/test_analytics/ta_processor.py @@ -0,0 +1,82 @@ +import logging +from typing import Any + +from shared.django_apps.reports.models import ReportSession +from shared.storage.exceptions import FileNotInStorageError +from test_results_parser import parse_raw_upload + +from services.archive import ArchiveService +from services.processing.types import UploadArguments +from services.test_analytics.ta_metrics import write_tests_summary +from services.test_analytics.ta_processing import ( + get_ta_processing_info, + handle_file_not_found, + handle_parsing_error, + insert_testruns_timeseries, + rewrite_or_delete_upload, +) + +log = logging.getLogger(__name__) + + +def ta_processor_impl( + repoid: int, + commitid: str, + commit_yaml: dict[str, Any], + argument: UploadArguments, + update_state: bool = False, +) -> bool: + log.info( + "Processing single TA argument", + extra=dict( + upload_id=argument.get("upload_id"), + repoid=repoid, + commitid=commitid, + ), + ) + + upload_id = argument.get("upload_id") + if upload_id is None: + return False + + upload = ReportSession.objects.get(id=upload_id) + if upload.state == "processed": + # don't need to process again because the intermediate result should already be in redis + return False + + if upload.storage_path is None: + if update_state: + handle_file_not_found(upload) + return False + + ta_proc_info = get_ta_processing_info(repoid, commitid, commit_yaml) + + archive_service = ArchiveService(ta_proc_info.repository) + + try: + payload_bytes = archive_service.read_file(upload.storage_path) + except FileNotInStorageError: + if update_state: + handle_file_not_found(upload) + return False + + try: + parsing_infos, readable_file = parse_raw_upload(payload_bytes) + except RuntimeError as exc: + if update_state: + handle_parsing_error(upload, exc) + return False + + with write_tests_summary.labels("new").time(): + insert_testruns_timeseries( + repoid, commitid, ta_proc_info.branch, upload, parsing_infos + ) + + if update_state: + upload.state = "processed" + upload.save() + + rewrite_or_delete_upload( + archive_service, ta_proc_info.user_yaml, upload, readable_file + ) + return True diff --git a/apps/worker/services/test_analytics/ta_timeseries.py b/apps/worker/services/test_analytics/ta_timeseries.py new file mode 100644 index 0000000000..b0f346caf5 --- /dev/null +++ b/apps/worker/services/test_analytics/ta_timeseries.py @@ -0,0 +1,263 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timedelta +from typing import TypedDict + +import test_results_parser +from django.db import connections +from django.db.models import Q +from shared.django_apps.ta_timeseries.models import ( + Testrun, + TestrunBranchSummary, + TestrunSummary, +) +from shared.django_apps.test_analytics.models import Flake + +from services.test_analytics.utils import calc_test_id +from services.test_results import FlakeInfo + +LOWER_BOUND_NUM_DAYS = 60 + + +def get_flaky_tests_set(repo_id: int) -> set[bytes]: + return { + bytes(test_id) + for test_id in Flake.objects.filter(repoid=repo_id, end_date__isnull=True) + .values_list("test_id", flat=True) + .distinct() + } + + +def get_flaky_tests_dict(repo_id: int) -> dict[bytes, FlakeInfo]: + return { + bytes(flake.test_id): FlakeInfo(flake.fail_count, flake.count) + for flake in Flake.objects.filter(repoid=repo_id, end_date__isnull=True) + } + + +def insert_testrun( + timestamp: datetime, + repo_id: int | None, + commit_sha: str | None, + branch: str | None, + upload_id: int | None, + flags: list[str] | None, + parsing_info: test_results_parser.ParsingInfo, + flaky_test_ids: set[bytes] | None = None, +): + testruns_to_create = [] + for testrun in parsing_info["testruns"]: + test_id = calc_test_id( + testrun["name"], testrun["classname"], testrun["testsuite"] + ) + outcome = testrun["outcome"] + + if outcome == "error": + outcome = "failure" + + if outcome == "failure" and flaky_test_ids and test_id in flaky_test_ids: + outcome = "flaky_failure" + + testruns_to_create.append( + Testrun( + timestamp=timestamp, + test_id=test_id, + name=testrun["name"], + classname=testrun["classname"], + testsuite=testrun["testsuite"], + computed_name=testrun["computed_name"], + outcome=outcome, + duration_seconds=testrun["duration"], + failure_message=testrun["failure_message"], + framework=parsing_info["framework"], + filename=testrun["filename"], + repo_id=repo_id, + commit_sha=commit_sha, + branch=branch, + flags=flags, + upload_id=upload_id, + ) + ) + Testrun.objects.bulk_create(testruns_to_create) + + +class TestInstance(TypedDict): + test_id: bytes + computed_name: str + failure_message: str + upload_id: int + duration_seconds: float | None + + +def get_pr_comment_failures(repo_id: int, commit_sha: str) -> list[TestInstance]: + with connections["ta_timeseries"].cursor() as cursor: + cursor.execute( + """ + SELECT + test_id, + LAST(computed_name, timestamp) as computed_name, + LAST(failure_message, timestamp) as failure_message, + LAST(upload_id, timestamp) as upload_id, + LAST(duration_seconds, timestamp) as duration_seconds + FROM ta_timeseries_testrun + WHERE repo_id = %s AND commit_sha = %s AND outcome IN ('failure', 'flaky_failure') + GROUP BY test_id + """, + [repo_id, commit_sha], + ) + return [ + { + "test_id": bytes(test_id), + "computed_name": computed_name, + "failure_message": failure_message, + "upload_id": upload_id, + "duration_seconds": duration_seconds, + } + for test_id, computed_name, failure_message, upload_id, duration_seconds in cursor.fetchall() + ] + + +class PRCommentAgg(TypedDict): + passed: int + failed: int + skipped: int + + +def get_pr_comment_agg(repo_id: int, commit_sha: str) -> PRCommentAgg: + with connections["ta_timeseries"].cursor() as cursor: + cursor.execute( + """ + SELECT outcome, count(*) FROM ( + SELECT + test_id, + LAST(outcome, timestamp) as outcome + FROM ta_timeseries_testrun + WHERE repo_id = %s AND commit_sha = %s + GROUP BY test_id + ) AS t + GROUP BY outcome + """, + [repo_id, commit_sha], + ) + outcome_dict = {outcome: count for outcome, count in cursor.fetchall()} + + return { + "passed": outcome_dict.get("pass", 0), + "failed": outcome_dict.get("failure", 0) + + outcome_dict.get("flaky_failure", 0), + "skipped": outcome_dict.get("skip", 0), + } + + +def get_testruns_for_flake_detection( + upload_id: int, + flaky_test_ids: set[bytes], +) -> list[Testrun]: + return list( + Testrun.objects.filter( + Q(upload_id=upload_id) + & ( + Q(outcome="failure") + | Q(outcome="flaky_failure") + | (Q(outcome="pass") & Q(test_id__in=flaky_test_ids)) + ) + ) + ) + + +def update_testrun_to_flaky(timestamp: datetime, test_id: bytes): + with connections["ta_timeseries"].cursor() as cursor: + cursor.execute( + "UPDATE ta_timeseries_testrun SET outcome = %s WHERE timestamp = %s AND test_id = %s", + ["flaky_failure", timestamp, test_id], + ) + + +def timestamp_lower_bound(): + return datetime.now() - timedelta(days=LOWER_BOUND_NUM_DAYS) + + +def get_summary(repo_id: int) -> list[TestrunSummary]: + return list( + TestrunSummary.objects.filter( + repo_id=repo_id, timestamp_bin__gte=timestamp_lower_bound() + ) + ) + + +def get_branch_summary(repo_id: int, branch: str) -> list[TestrunBranchSummary]: + return list( + TestrunBranchSummary.objects.filter( + repo_id=repo_id, branch=branch, timestamp_bin__gte=timestamp_lower_bound() + ) + ) + + +@dataclass +class BranchSummary: + testsuite: str + classname: str + name: str + timestamp_bin: datetime + computed_name: str + failing_commits: int + last_duration_seconds: float + avg_duration_seconds: float + pass_count: int + fail_count: int + skip_count: int + flaky_fail_count: int + updated_at: datetime + flags: list[str] + + +def get_testrun_branch_summary_via_testrun( + repo_id: int, branch: str +) -> list[BranchSummary]: + with connections["ta_timeseries"].cursor() as cursor: + cursor.execute( + """ + select + testsuite, + classname, + name, + time_bucket(interval '1 days', timestamp) as timestamp_bin, + + min(computed_name) as computed_name, + COUNT(DISTINCT CASE WHEN outcome = 'failure' OR outcome = 'flaky_failure' THEN commit_sha ELSE NULL END) AS failing_commits, + last(duration_seconds, timestamp) as last_duration_seconds, + avg(duration_seconds) as avg_duration_seconds, + COUNT(*) FILTER (WHERE outcome = 'pass') AS pass_count, + COUNT(*) FILTER (WHERE outcome = 'failure') AS fail_count, + COUNT(*) FILTER (WHERE outcome = 'skip') AS skip_count, + COUNT(*) FILTER (WHERE outcome = 'flaky_failure') AS flaky_fail_count, + MAX(timestamp) AS updated_at, + array_merge_dedup_agg(flags) as flags + from ta_timeseries_testrun + where repo_id = %s and branch = %s and timestamp > %s + group by + testsuite, classname, name, timestamp_bin; + """, + [repo_id, branch, timestamp_lower_bound()], + ) + + return [ + BranchSummary( + testsuite=row[0], + classname=row[1], + name=row[2], + timestamp_bin=row[3], + computed_name=row[4], + failing_commits=row[5], + last_duration_seconds=row[6], + avg_duration_seconds=row[7], + pass_count=row[8], + fail_count=row[9], + skip_count=row[10], + flaky_fail_count=row[11], + updated_at=row[12], + flags=row[13] or [], + ) + for row in cursor.fetchall() + ] diff --git a/apps/worker/services/test_analytics/tests/conftest.py b/apps/worker/services/test_analytics/tests/conftest.py new file mode 100644 index 0000000000..7586780d9c --- /dev/null +++ b/apps/worker/services/test_analytics/tests/conftest.py @@ -0,0 +1,42 @@ +import os +from typing import Any + +import pytest +import yaml +from shared.config import _get_config_instance, get_config +from shared.storage import get_appropriate_storage_service +from shared.storage.exceptions import BucketAlreadyExistsError + + +@pytest.fixture +def storage(mock_configuration): + storage_service = get_appropriate_storage_service() + try: + storage_service.create_root_storage(get_config("services", "minio", "bucket")) + except BucketAlreadyExistsError: + pass + return storage_service + + +@pytest.fixture() +def custom_config(tmp_path): + config_instance = _get_config_instance() + saved_config = config_instance._params + + file_path = tmp_path / "codecov.yml" + os.environ["CODECOV_YML"] = str(file_path) + + _conf = config_instance._params or {} + + def set(custom_config: dict[Any, Any]): + # clear cache + config_instance._params = None + + # for overwrites + _conf.update(custom_config) + file_path.write_text(yaml.dump(_conf)) + + yield set + + os.environ.pop("CODECOV_YML") + config_instance._params = saved_config diff --git a/apps/worker/services/test_analytics/tests/samples/sample_test.json b/apps/worker/services/test_analytics/tests/samples/sample_test.json new file mode 100644 index 0000000000..1b05856e84 --- /dev/null +++ b/apps/worker/services/test_analytics/tests/samples/sample_test.json @@ -0,0 +1,11 @@ +{ + "test_results_files": [ + { + "filename": "codecov-demo/temp.junit.xml", + "format": "base64+compressed", + "data": "eJy1VMluwjAQvfMVI1dCoBbHZiklJEFVS4V66Kkqx8okBqw6i2KHwt/XWSChnCrRXDLjefNm8Uuc2T6UsOOpEnHkIooJAh75cSCijYsyve49oJnnaK60yoR5NWyIWMhdlBzyE5OWpnGqXGQY1kzILOXGoQjUl0gSHhSBItdFQ2OJPJdgMuqXjtIsTFzUJ/1Bj9IeuX+n1KZjmwwxoZSMDWwbK13W/HhZvC1fn5eLCZaxzyQq2/KZ4uBLplQJY4nAmocJNhA/k0zHKc5xn7WPqimKYxYEjc6Iad66DrHKVjplvv4f9jCTWiTy0GQnV2MPxE4E/Lxzz6muGMzFKbbJaZXiqQajIHBdIHjUvqFkCrcA31tugEUA2lJP11nkayM3eKobKIsA00D2lAz9CV9NSHujpx16B/3uievI9mceU/sChryAr6ExZKdrtwpw+VQjXeSVPVVjtubn6HoBp8iVlnDGd9VFtFpGFFYuCqsWgfVLFDg52ANiw2Mxpyk3zz94x6qU4DnWUW2VWfwkmrbyfgBbcXMH", + "labels": "" + } + ], + "metadata": {} +} \ No newline at end of file diff --git a/apps/worker/services/test_analytics/tests/snapshots/ta_cache_rollups__cache_test_rollups__0.json b/apps/worker/services/test_analytics/tests/snapshots/ta_cache_rollups__cache_test_rollups__0.json new file mode 100644 index 0000000000..987b14e08d --- /dev/null +++ b/apps/worker/services/test_analytics/tests/snapshots/ta_cache_rollups__cache_test_rollups__0.json @@ -0,0 +1,42 @@ +{ + "computed_name": [ + "computed_name2", + "computed_name" + ], + "flags": [ + [ + "test-rollups2" + ], + [ + "test-rollups" + ] + ], + "failing_commits": [ + 2, + 1 + ], + "last_duration": [ + 200.0, + 100.0 + ], + "avg_duration": [ + 200.0, + 100.0 + ], + "pass_count": [ + 0, + 0 + ], + "fail_count": [ + 2, + 1 + ], + "flaky_fail_count": [ + 0, + 0 + ], + "skip_count": [ + 0, + 0 + ] +} diff --git a/apps/worker/services/test_analytics/tests/snapshots/ta_cache_rollups__cache_test_rollups_use_timeseries_branch__0.json b/apps/worker/services/test_analytics/tests/snapshots/ta_cache_rollups__cache_test_rollups_use_timeseries_branch__0.json new file mode 100644 index 0000000000..6798c8832c --- /dev/null +++ b/apps/worker/services/test_analytics/tests/snapshots/ta_cache_rollups__cache_test_rollups_use_timeseries_branch__0.json @@ -0,0 +1,43 @@ +{ + "computed_name": [ + "computed_name", + "computed_name2" + ], + "flags": [ + [ + "test-rollups" + ], + [ + "test-rollups", + "test-rollups2" + ] + ], + "failing_commits": [ + 0, + 1 + ], + "last_duration": [ + 100.0, + 1.0 + ], + "avg_duration": [ + 100.0, + 50.5 + ], + "pass_count": [ + 1, + 1 + ], + "fail_count": [ + 0, + 1 + ], + "flaky_fail_count": [ + 0, + 0 + ], + "skip_count": [ + 0, + 0 + ] +} diff --git a/apps/worker/services/test_analytics/tests/snapshots/ta_cache_rollups__cache_test_rollups_use_timeseries_main__0.json b/apps/worker/services/test_analytics/tests/snapshots/ta_cache_rollups__cache_test_rollups_use_timeseries_main__0.json new file mode 100644 index 0000000000..987b14e08d --- /dev/null +++ b/apps/worker/services/test_analytics/tests/snapshots/ta_cache_rollups__cache_test_rollups_use_timeseries_main__0.json @@ -0,0 +1,42 @@ +{ + "computed_name": [ + "computed_name2", + "computed_name" + ], + "flags": [ + [ + "test-rollups2" + ], + [ + "test-rollups" + ] + ], + "failing_commits": [ + 2, + 1 + ], + "last_duration": [ + 200.0, + 100.0 + ], + "avg_duration": [ + 200.0, + 100.0 + ], + "pass_count": [ + 0, + 0 + ], + "fail_count": [ + 2, + 1 + ], + "flaky_fail_count": [ + 0, + 0 + ], + "skip_count": [ + 0, + 0 + ] +} diff --git a/apps/worker/services/test_analytics/tests/snapshots/ta_process_flakes__testrun_filters__0.json b/apps/worker/services/test_analytics/tests/snapshots/ta_process_flakes__testrun_filters__0.json new file mode 100644 index 0000000000..2a136b9c4a --- /dev/null +++ b/apps/worker/services/test_analytics/tests/snapshots/ta_process_flakes__testrun_filters__0.json @@ -0,0 +1,22 @@ +{ + "test1": { + "count": 6, + "fail_count": 2, + "recent_passes_count": 1 + }, + "test3": { + "count": 1, + "fail_count": 1, + "recent_passes_count": 0 + }, + "test4": { + "count": 1, + "fail_count": 1, + "recent_passes_count": 0 + }, + "test5": { + "count": 1, + "fail_count": 1, + "recent_passes_count": 0 + } +} diff --git a/apps/worker/services/test_analytics/tests/snapshots/ta_processing__insert_testruns_timeseries__0.json b/apps/worker/services/test_analytics/tests/snapshots/ta_processing__insert_testruns_timeseries__0.json new file mode 100644 index 0000000000..fcf583086b --- /dev/null +++ b/apps/worker/services/test_analytics/tests/snapshots/ta_processing__insert_testruns_timeseries__0.json @@ -0,0 +1,38 @@ +[ + { + "timestamp": "2025-01-01T00:00:00+00:00", + "test_id": "7a44f8a4b65ee2abd9617fc99a63fc2e", + "name": "test_1_name", + "classname": "test_1_classname", + "testsuite": "test_1_testsuite", + "computed_name": "test_1_computed_name", + "outcome": "pass", + "duration_seconds": 1.0, + "failure_message": null, + "framework": "Pytest", + "filename": "test_1_file", + "repo_id": 1, + "commit_sha": "123", + "branch": "main", + "flags": [], + "upload_id": 1 + }, + { + "timestamp": "2025-01-01T00:00:00+00:00", + "test_id": "25ce6e22db03ef4f4230eb999c776f99", + "name": "test_2_name", + "classname": "test_2_classname", + "testsuite": "test_2_testsuite", + "computed_name": "test_2", + "outcome": "failure", + "duration_seconds": 1.0, + "failure_message": "test_2_failure_message", + "framework": "Pytest", + "filename": "test_2_file", + "repo_id": 1, + "commit_sha": "123", + "branch": "main", + "flags": [], + "upload_id": 1 + } +] diff --git a/apps/worker/services/test_analytics/tests/test_ta_cache_rollups.py b/apps/worker/services/test_analytics/tests/test_ta_cache_rollups.py new file mode 100644 index 0000000000..1b5288ed3e --- /dev/null +++ b/apps/worker/services/test_analytics/tests/test_ta_cache_rollups.py @@ -0,0 +1,274 @@ +import datetime as dt + +import polars as pl +import pytest +from shared.config import get_config +from shared.django_apps.ta_timeseries.models import ( + Testrun, + TestrunBranchSummary, + TestrunSummary, +) + +from services.test_analytics.utils import calc_test_id +from tasks.cache_test_rollups import CacheTestRollupsTask + + +def read_table(storage, storage_path: str): + decompressed_table: bytes = storage.read_file( + get_config("services", "minio", "bucket", default="archive"), storage_path + ) + return pl.read_ipc(decompressed_table) + + +@pytest.mark.django_db(databases=["ta_timeseries"], transaction=True) +def test_cache_test_rollups(storage, snapshot): + TestrunSummary.objects.create( + timestamp_bin=dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=1), + repo_id=1, + name="name", + classname="classname", + testsuite="testsuite", + computed_name="computed_name", + failing_commits=1, + avg_duration_seconds=100, + last_duration_seconds=100, + pass_count=0, + fail_count=1, + skip_count=0, + flaky_fail_count=0, + updated_at=dt.datetime.now(dt.timezone.utc), + flags=["test-rollups"], + ) + + TestrunSummary.objects.create( + timestamp_bin=dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=1), + repo_id=1, + name="name2", + classname="classname2", + testsuite="testsuite2", + computed_name="computed_name2", + failing_commits=2, + avg_duration_seconds=200, + last_duration_seconds=200, + pass_count=0, + fail_count=2, + skip_count=0, + flaky_fail_count=0, + updated_at=dt.datetime.now(dt.timezone.utc), + flags=["test-rollups2"], + ) + + TestrunSummary.objects.create( + timestamp_bin=dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=61), + repo_id=1, + name="name3", + classname="classname3", + testsuite="testsuite3", + computed_name="computed_name3", + failing_commits=2, + avg_duration_seconds=200, + last_duration_seconds=200, + pass_count=0, + fail_count=2, + skip_count=0, + flaky_fail_count=0, + updated_at=dt.datetime.now(dt.timezone.utc), + flags=["test-rollups3"], + ) + + CacheTestRollupsTask().run_impl( + _db_session=None, + repo_id=1, + branch=None, + impl_type="new", + ) + + table = read_table(storage, "test_analytics/repo_rollups/1.arrow") + table_dict = table.to_dict(as_series=False) + del table_dict["timestamp_bin"] + del table_dict["updated_at"] + assert snapshot("json") == table_dict + + +@pytest.mark.django_db(databases=["ta_timeseries"], transaction=True) +def test_cache_test_rollups_use_timeseries_main(storage, snapshot): + TestrunBranchSummary.objects.create( + timestamp_bin=dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=1), + repo_id=1, + branch="main", + name="name", + classname="classname", + testsuite="testsuite", + computed_name="computed_name", + failing_commits=1, + avg_duration_seconds=100, + last_duration_seconds=100, + pass_count=0, + fail_count=1, + skip_count=0, + flaky_fail_count=0, + updated_at=dt.datetime.now(dt.timezone.utc), + flags=["test-rollups"], + ) + + TestrunBranchSummary.objects.create( + timestamp_bin=dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=1), + repo_id=1, + branch="main", + name="name2", + classname="classname2", + testsuite="testsuite2", + computed_name="computed_name2", + failing_commits=2, + avg_duration_seconds=200, + last_duration_seconds=200, + pass_count=0, + fail_count=2, + skip_count=0, + flaky_fail_count=0, + updated_at=dt.datetime.now(dt.timezone.utc), + flags=["test-rollups2"], + ) + + TestrunBranchSummary.objects.create( + timestamp_bin=dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=61), + repo_id=1, + branch="main", + name="name3", + classname="classname3", + testsuite="testsuite3", + computed_name="computed_name3", + failing_commits=2, + avg_duration_seconds=200, + last_duration_seconds=200, + pass_count=0, + fail_count=2, + skip_count=0, + flaky_fail_count=0, + updated_at=dt.datetime.now(dt.timezone.utc), + flags=["test-rollups3"], + ) + + TestrunBranchSummary.objects.create( + timestamp_bin=dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=1), + repo_id=1, + branch="feature", + name="name4", + classname="classname4", + testsuite="testsuite4", + computed_name="computed_name4", + failing_commits=2, + avg_duration_seconds=200, + last_duration_seconds=200, + pass_count=0, + fail_count=2, + skip_count=0, + flaky_fail_count=0, + updated_at=dt.datetime.now(dt.timezone.utc), + flags=["test-rollups3"], + ) + + CacheTestRollupsTask().run_impl( + _db_session=None, + repo_id=1, + branch="main", + impl_type="new", + ) + + table = read_table(storage, "test_analytics/branch_rollups/1/main.arrow") + table_dict = table.to_dict(as_series=False) + del table_dict["timestamp_bin"] + del table_dict["updated_at"] + assert snapshot("json") == table_dict + + +@pytest.mark.django_db(databases=["ta_timeseries"], transaction=True) +def test_cache_test_rollups_use_timeseries_branch(storage, snapshot): + Testrun.objects.create( + timestamp=dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=1), + test_id=calc_test_id("name", "classname", "testsuite"), + name="name", + classname="classname", + testsuite="testsuite", + computed_name="computed_name", + outcome="pass", + duration_seconds=100, + failure_message="failure_message", + framework="framework", + filename="filename", + repo_id=1, + commit_sha="commit_sha", + branch="feature", + flags=["test-rollups"], + upload_id=1, + ) + + Testrun.objects.create( + timestamp=dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=1), + test_id=calc_test_id("name2", "classname2", "testsuite2"), + name="name2", + classname="classname2", + testsuite="testsuite2", + computed_name="computed_name2", + outcome="pass", + duration_seconds=100, + failure_message="failure_message", + framework="framework", + filename="filename", + repo_id=1, + commit_sha="commit_sha", + branch="feature", + flags=["test-rollups"], + upload_id=1, + ) + + Testrun.objects.create( + timestamp=dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=1), + test_id=calc_test_id("name2", "classname2", "testsuite2"), + name="name2", + classname="classname2", + testsuite="testsuite2", + computed_name="computed_name2", + outcome="failure", + duration_seconds=1, + failure_message="failure_message", + framework="framework", + filename="filename", + repo_id=1, + commit_sha="other_commit_sha", + branch="feature", + flags=["test-rollups", "test-rollups2"], + upload_id=1, + ) + + Testrun.objects.create( + timestamp=dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=61), + test_id=calc_test_id("name3", "classname3", "testsuite3"), + name="name3", + classname="classname3", + testsuite="testsuite3", + computed_name="computed_name3", + outcome="pass", + duration_seconds=100, + failure_message="failure_message", + framework="framework", + filename="filename", + repo_id=1, + commit_sha="commit_sha", + branch="main", + flags=["test-rollups"], + upload_id=1, + ) + + CacheTestRollupsTask().run_impl( + _db_session=None, + repo_id=1, + branch="feature", + impl_type="new", + ) + + table = read_table(storage, "test_analytics/branch_rollups/1/feature.arrow") + table_dict = table.to_dict(as_series=False) + del table_dict["timestamp_bin"] + del table_dict["updated_at"] + assert snapshot("json") == table_dict diff --git a/apps/worker/services/test_analytics/tests/test_ta_process_flakes.py b/apps/worker/services/test_analytics/tests/test_ta_process_flakes.py new file mode 100644 index 0000000000..1cc6dd8856 --- /dev/null +++ b/apps/worker/services/test_analytics/tests/test_ta_process_flakes.py @@ -0,0 +1,312 @@ +from typing import TypedDict + +import pytest +from django.utils import timezone +from shared.django_apps.reports.models import CommitReport, ReportSession +from shared.django_apps.reports.tests.factories import CommitReportFactory +from shared.django_apps.ta_timeseries.models import Testrun +from shared.django_apps.test_analytics.models import Flake +from shared.helpers.redis import get_redis_connection + +from services.test_analytics.ta_process_flakes import KEY_NAME, process_flakes_for_repo + + +class TestrunData(TypedDict): + test_id: str + outcome: str + + +class UploadData(TypedDict): + state: str + testruns: list[TestrunData] + + +class FlakeDataRequired(TypedDict): + test_id: str + + +class FlakeDataOptional(FlakeDataRequired, total=False): + count: int + fail_count: int + recent_passes_count: int + start_date: timezone.datetime + end_date: timezone.datetime | None + + +class SetupResult(TypedDict): + repoid: int + commitid: str + + +pytestmark = pytest.mark.django_db( + databases=["default", "ta_timeseries"], transaction=True +) + + +@pytest.fixture +def setup_test_data(db): + def _create_test_data( + uploads: list[UploadData], + existing_flakes: list[FlakeDataOptional], + ) -> SetupResult: + report = CommitReportFactory( + report_type=CommitReport.ReportType.TEST_RESULTS.value, + ) + report.save() + repo_id = report.commit.repository.repoid + commit_id = report.commit.commitid + + redis = get_redis_connection() + redis.lpush(KEY_NAME.format(repo_id), commit_id) + + sessions = [] + testruns = [] + for upload in uploads: + session = ReportSession.objects.create( + report=report, + state=upload["state"], + ) + sessions.append(session) + + for testrun_data in upload.get("testruns", []): + testrun = Testrun.objects.create( + timestamp=timezone.now(), + test_id=testrun_data["test_id"].encode(), + outcome=testrun_data["outcome"], + repo_id=repo_id, + commit_sha=commit_id, + upload_id=session.id, + ) + testruns.append(testrun) + + created_flakes = [] + for flake_data in existing_flakes: + flake = Flake.objects.create( + repoid=repo_id, + test_id=flake_data["test_id"].encode(), + recent_passes_count=flake_data.get("recent_passes_count", 0), + count=flake_data.get("count", 0), + fail_count=flake_data.get("fail_count", 0), + start_date=flake_data.get("start_date", timezone.now()), + end_date=flake_data.get("end_date", None), + ) + created_flakes.append(flake) + + return { + "repoid": repo_id, + "commitid": commit_id, + } + + return _create_test_data + + +def test_process_flakes_valid_states_only(setup_test_data): + result = setup_test_data( + uploads=[ + { + "state": "processed", + "testruns": [{"test_id": "test1", "outcome": "failure"}], + }, + { + "state": "finished", + "testruns": [{"test_id": "test3", "outcome": "failure"}], + }, + { + "state": "started", + "testruns": [{"test_id": "test4", "outcome": "failure"}], + }, + ], + existing_flakes=[], + ) + + process_flakes_for_repo(result["repoid"]) + + assert Flake.objects.count() == 1 + + +def test_testrun_filters(setup_test_data, snapshot): + result = setup_test_data( + uploads=[ + { + "state": "processed", + "testruns": [ + {"test_id": "test1", "outcome": "pass"}, + {"test_id": "test2", "outcome": "pass"}, + {"test_id": "test3", "outcome": "failure"}, + {"test_id": "test4", "outcome": "flaky_failure"}, + {"test_id": "test5", "outcome": "error"}, + {"test_id": "test6", "outcome": "skip"}, + ], + } + ], + existing_flakes=[ + {"test_id": "test1", "count": 5, "fail_count": 2}, + ], + ) + + process_flakes_for_repo(result["repoid"]) + + flakes = { + bytes(flake.test_id).decode(): { + "count": flake.count, + "fail_count": flake.fail_count, + "recent_passes_count": flake.recent_passes_count, + } + for flake in Flake.objects.all() + } + + assert snapshot("json") == flakes + + +def test_update_existing_flakes(setup_test_data): + result = setup_test_data( + uploads=[ + { + "state": "processed", + "testruns": [ + {"test_id": "test1", "outcome": "pass"}, + {"test_id": "test1", "outcome": "failure"}, + ], + } + ], + existing_flakes=[ + { + "test_id": "test1", + "count": 5, + "fail_count": 2, + "recent_passes_count": 0, + }, + ], + ) + + process_flakes_for_repo(result["repoid"]) + + flake = Flake.objects.get(test_id=b"test1") + assert flake.count == 7 + assert flake.fail_count == 3 + assert flake.recent_passes_count == 0 + + +def test_create_new_flakes(setup_test_data): + result = setup_test_data( + uploads=[ + { + "state": "processed", + "testruns": [ + {"test_id": "test1", "outcome": "failure"}, + {"test_id": "test2", "outcome": "flaky_failure"}, + {"test_id": "test3", "outcome": "error"}, + ], + } + ], + existing_flakes=[], + ) + + process_flakes_for_repo(result["repoid"]) + + assert Flake.objects.count() == 3 + for test_id in [b"test1", b"test2", b"test3"]: + flake = Flake.objects.get(test_id=test_id) + assert flake.count == 1 + assert flake.fail_count == 1 + assert flake.recent_passes_count == 0 + assert flake.start_date is not None + assert flake.end_date is None + + +def test_flake_expiry_and_recreation(setup_test_data): + result = setup_test_data( + uploads=[ + { + "state": "processed", + "testruns": [ + {"test_id": "test1", "outcome": "pass"}, + {"test_id": "test1", "outcome": "failure"}, + ], + } + ], + existing_flakes=[ + { + "test_id": "test1", + "count": 29, + "fail_count": 1, + "recent_passes_count": 29, + }, + ], + ) + + process_flakes_for_repo(result["repoid"]) + + flakes = Flake.objects.filter(test_id=b"test1").order_by("start_date") + assert len(flakes) == 2 + + expired_flake = flakes[0] + assert expired_flake.end_date is not None + assert expired_flake.recent_passes_count == 30 + + new_flake = flakes[1] + assert new_flake.end_date is None + assert new_flake.count == 1 + assert new_flake.fail_count == 1 + assert new_flake.recent_passes_count == 0 + + +def test_flake_expiry_and_more_passes(setup_test_data): + result = setup_test_data( + uploads=[ + { + "state": "processed", + "testruns": [ + {"test_id": "test1", "outcome": "pass"}, + {"test_id": "test1", "outcome": "pass"}, + {"test_id": "test1", "outcome": "pass"}, + ], + } + ], + existing_flakes=[ + { + "test_id": "test1", + "count": 29, + "fail_count": 1, + "recent_passes_count": 29, + }, + ], + ) + + process_flakes_for_repo(result["repoid"]) + + flakes = Flake.objects.filter(test_id=b"test1").order_by("start_date") + assert len(flakes) == 1 + + expired_flake = flakes[0] + assert expired_flake.end_date is not None + assert expired_flake.recent_passes_count == 30 + + +def test_testrun_outcome_updates(setup_test_data): + result = setup_test_data( + uploads=[ + { + "state": "processed", + "testruns": [ + {"test_id": "test1", "outcome": "failure"}, + {"test_id": "test2", "outcome": "error"}, + {"test_id": "test3", "outcome": "flaky_failure"}, + ], + } + ], + existing_flakes=[], + ) + + process_flakes_for_repo(result["repoid"]) + + testruns = { + bytes(testrun.test_id).decode(): testrun.outcome + for testrun in Testrun.objects.all() + } + + assert testruns == { + "test1": "flaky_failure", # Updated from failure + "test2": "flaky_failure", # Updated from error + "test3": "flaky_failure", # Already flaky_failure, unchanged + } diff --git a/apps/worker/services/test_analytics/tests/test_ta_processing.py b/apps/worker/services/test_analytics/tests/test_ta_processing.py new file mode 100644 index 0000000000..84aa1e4243 --- /dev/null +++ b/apps/worker/services/test_analytics/tests/test_ta_processing.py @@ -0,0 +1,249 @@ +from __future__ import annotations + +from datetime import datetime + +import pytest +import test_results_parser +from shared.django_apps.reports.models import UploadError +from shared.django_apps.reports.tests.factories import UploadFactory +from shared.django_apps.ta_timeseries.models import Testrun +from shared.storage import get_appropriate_storage_service +from shared.storage.exceptions import BucketAlreadyExistsError, FileNotInStorageError + +from services.archive import ArchiveService +from services.test_analytics.ta_processing import ( + handle_file_not_found, + handle_parsing_error, + insert_testruns_timeseries, + rewrite_or_delete_upload, + should_delete_archive_settings, +) +from services.yaml import UserYaml + + +@pytest.fixture(autouse=True) +def minio_service(custom_config): + conf = { + "services": { + "minio": { + "port": 9000, + }, + } + } + + custom_config(conf) + + storage = get_appropriate_storage_service(1) + try: + storage.create_root_storage() + except BucketAlreadyExistsError: + pass + + +@pytest.mark.django_db +def test_handle_file_not_found(): + upload = UploadFactory() + + handle_file_not_found(upload) + + assert upload.state == "processed" + + error = UploadError.objects.filter(report_session=upload).first() + assert error is not None + assert error.error_code == "file_not_in_storage" + + +@pytest.mark.django_db +def test_parsing_error(): + upload = UploadFactory() + + handle_parsing_error(upload, Exception("test string")) + + assert upload.state == "processed" + + error = UploadError.objects.filter(report_session=upload).first() + assert error is not None + assert error.error_code == "unsupported_file_format" + assert error.error_params["error_message"] == "test string" + + +@pytest.mark.parametrize( + "expire_raw,uploads,result", + [ + (None, None, False), + (7, None, True), + (True, None, True), + (None, False, True), + ], +) +def test_should_delete_archive(expire_raw, uploads, result, custom_config): + custom_config( + { + "services": { + "minio": {"expire_raw_after_n_days": expire_raw}, + } + } + ) + + fake_yaml = UserYaml.from_dict( + {"codecov": {"archive": {"uploads": uploads}}} if uploads is not None else {} + ) + assert should_delete_archive_settings(fake_yaml) == result + + +@pytest.mark.django_db +def test_rewrite_or_delete_upload_deletes(custom_config): + conf = { + "services": { + "minio": { + "port": 9000, + "expire_raw_after_n_days": 1, + }, + } + } + + custom_config(conf) + + upload = UploadFactory(storage_path="url") + archive_service = ArchiveService(upload.report.commit.repository) + + archive_service.write_file(upload.storage_path, b"test") + + rewrite_or_delete_upload( + archive_service, UserYaml.from_dict({}), upload, b"rewritten" + ) + + with pytest.raises(FileNotInStorageError): + archive_service.read_file(upload.storage_path) + + +@pytest.mark.django_db +def test_rewrite_or_delete_upload_does_not_delete(custom_config): + conf = { + "services": { + "minio": { + "port": 9000, + "expire_raw_after_n_days": 1, + }, + } + } + + custom_config(conf) + + upload = UploadFactory(storage_path="http_url") + archive_service = ArchiveService(upload.report.commit.repository) + + archive_service.write_file(upload.storage_path, b"test") + + rewrite_or_delete_upload( + archive_service, UserYaml.from_dict({}), upload, b"rewritten" + ) + + assert archive_service.read_file(upload.storage_path) == b"test" + + +@pytest.mark.django_db +def test_rewrite_or_delete_upload_rewrites(custom_config): + conf = { + "services": { + "minio": { + "port": 9000, + }, + } + } + + custom_config(conf) + + upload = UploadFactory(storage_path="url") + archive_service = ArchiveService(upload.report.commit.repository) + + archive_service.write_file(upload.storage_path, b"test") + + rewrite_or_delete_upload( + archive_service, UserYaml.from_dict({}), upload, b"rewritten" + ) + + assert archive_service.read_file(upload.storage_path) == b"rewritten" + + +@pytest.mark.django_db(databases=["default", "ta_timeseries"]) +def test_insert_testruns_timeseries(snapshot): + parsing_infos: list[test_results_parser.ParsingInfo] = [ + { + "framework": "Pytest", + "testruns": [ + { + "name": "test_1_name", + "classname": "test_1_classname", + "duration": 1, + "outcome": "pass", + "testsuite": "test_1_testsuite", + "failure_message": None, + "filename": "test_1_file", + "build_url": "test_1_build_url", + "computed_name": "test_1_computed_name", + } + ], + }, + { + "framework": "Pytest", + "testruns": [ + { + "name": "test_2_name", + "classname": "test_2_classname", + "duration": 1, + "outcome": "failure", + "testsuite": "test_2_testsuite", + "failure_message": "test_2_failure_message", + "filename": "test_2_file", + "build_url": "test_2_build_url", + "computed_name": "test_2", + } + ], + }, + ] + + upload = UploadFactory() + upload.report.commit.repository.repoid = 1 + upload.report.commit.commitid = "123" + upload.report.commit.branch = "main" + upload.id = 1 + upload.created_at = datetime(2025, 1, 1, 0, 0, 0) + + insert_testruns_timeseries( + repoid=upload.report.commit.repository.repoid, + commitid=upload.report.commit.commitid, + branch=upload.report.commit.branch, + upload=upload, + parsing_infos=parsing_infos, + ) + + testruns = Testrun.objects.filter(upload_id=upload.id) + assert testruns.count() == 2 + + testruns_list = list( + testruns.values( + "timestamp", + "test_id", + "name", + "classname", + "testsuite", + "computed_name", + "outcome", + "duration_seconds", + "failure_message", + "framework", + "filename", + "repo_id", + "commit_sha", + "branch", + "flags", + "upload_id", + ) + ) + + for testrun in testruns_list: + testrun["timestamp"] = testrun["timestamp"].isoformat() + testrun["test_id"] = testrun["test_id"].hex() + + assert snapshot("json") == testruns_list diff --git a/apps/worker/services/test_analytics/tests/test_ta_processor.py b/apps/worker/services/test_analytics/tests/test_ta_processor.py new file mode 100644 index 0000000000..d89143f392 --- /dev/null +++ b/apps/worker/services/test_analytics/tests/test_ta_processor.py @@ -0,0 +1,218 @@ +from pathlib import Path + +import pytest +from shared.django_apps.core.tests.factories import CommitFactory, RepositoryFactory +from shared.django_apps.reports.models import UploadError +from shared.django_apps.reports.tests.factories import ( + RepositoryFlagFactory, + UploadFactory, + UploadFlagMembershipFactory, +) +from shared.django_apps.ta_timeseries.models import Testrun +from shared.storage.exceptions import FileNotInStorageError + +from services.processing.types import UploadArguments +from services.test_analytics.ta_processor import ta_processor_impl + + +@pytest.fixture +def sample_test_json_path(): + return Path(__file__).parent / "samples" / "sample_test.json" + + +@pytest.mark.django_db(databases=["default", "ta_timeseries"]) +@pytest.mark.parametrize("update_state", [True, False]) +def test_ta_processor_impl_no_upload_id(update_state): + repository = RepositoryFactory.create() + commit = CommitFactory.create(repository=repository, branch="main") + commit_yaml = {} + + argument: UploadArguments = {} + + result = ta_processor_impl( + repository.repoid, + commit.commitid, + commit_yaml, + argument, + update_state=update_state, + ) + + assert result is False + + +@pytest.mark.django_db(databases=["default", "ta_timeseries"]) +@pytest.mark.parametrize("update_state", [True, False]) +def test_ta_processor_impl_already_processed(update_state): + repository = RepositoryFactory.create() + commit = CommitFactory.create(repository=repository, branch="main") + upload = UploadFactory.create(report__commit=commit, state="processed") + commit_yaml = {} + + argument: UploadArguments = {"upload_id": upload.id} + + result = ta_processor_impl( + repository.repoid, + commit.commitid, + commit_yaml, + argument, + update_state=update_state, + ) + + assert result is False + + +@pytest.mark.django_db(databases=["default", "timeseries"]) +def test_ta_processor_impl_no_storage_path(storage): + repository = RepositoryFactory.create() + commit = CommitFactory.create(repository=repository, branch="main") + upload = UploadFactory.create( + report__commit=commit, state="processing", storage_path=None + ) + commit_yaml = {} + + argument: UploadArguments = {"upload_id": upload.id} + + result = ta_processor_impl( + repository.repoid, commit.commitid, commit_yaml, argument, update_state=True + ) + + assert result is False + + upload.refresh_from_db() + assert upload.state == "processed" + + error = UploadError.objects.get(report_session=upload) + assert error.error_code == "file_not_in_storage" + assert error.error_params == {} + + +@pytest.mark.parametrize("storage_path", [None, "path/to/nonexistent.xml"]) +@pytest.mark.django_db(databases=["default", "ta_timeseries"]) +def test_ta_processor_impl_file_not_found(storage, storage_path): + repository = RepositoryFactory.create() + commit = CommitFactory.create(repository=repository, branch="main") + upload = UploadFactory.create( + report__commit=commit, + state="processing", + storage_path=None, + ) + commit_yaml = {} + + argument: UploadArguments = {"upload_id": upload.id} + + result = ta_processor_impl( + repository.repoid, commit.commitid, commit_yaml, argument, update_state=True + ) + + assert result is False + + upload.refresh_from_db() + assert upload.state == "processed" + + error = UploadError.objects.get(report_session=upload) + assert error.error_code == "file_not_in_storage" + assert error.error_params == {} + + +@pytest.mark.django_db(databases=["default", "ta_timeseries"]) +def test_ta_processor_impl_parsing_error(storage): + repository = RepositoryFactory.create() + commit = CommitFactory.create(repository=repository, branch="main") + upload = UploadFactory.create( + report__commit=commit, state="processing", storage_path="path/to/invalid.xml" + ) + commit_yaml = {} + + argument: UploadArguments = {"upload_id": upload.id} + + storage.write_file("archive", "path/to/invalid.xml", b"invalid xml content") + + result = ta_processor_impl( + repository.repoid, commit.commitid, commit_yaml, argument, update_state=True + ) + + assert result is False + + upload.refresh_from_db() + assert upload.state == "processed" + + error = UploadError.objects.get(report_session=upload) + assert error.error_code == "unsupported_file_format" + assert error.error_params == { + "error_message": "Error deserializing json\n\nCaused by:\n expected value at line 1 column 1" + } + + +@pytest.mark.django_db(databases=["default", "ta_timeseries"]) +def test_ta_processor_impl_success_delete_archive(storage, sample_test_json_path): + repository = RepositoryFactory.create() + commit = CommitFactory.create(repository=repository, branch="main") + upload = UploadFactory.create( + report__commit=commit, + state="processing", + storage_path="path/to/valid.json", + ) + + flag = RepositoryFlagFactory.create(repository=repository, flag_name="unit") + UploadFlagMembershipFactory.create(report_session=upload, flag=flag) + + commit_yaml = {"codecov": {"archive": {"uploads": False}}} + + argument: UploadArguments = {"upload_id": upload.id} + + with open(sample_test_json_path, "rb") as f: + sample_content = f.read() + + storage.write_file("archive", "path/to/valid.json", sample_content) + + result = ta_processor_impl( + repository.repoid, commit.commitid, commit_yaml, argument, update_state=True + ) + + assert result is True + + testrun_db = Testrun.objects.filter(upload_id=upload.id).first() + assert testrun_db is not None + assert testrun_db.branch == commit.branch + assert testrun_db.upload_id == upload.id + assert testrun_db.flags == [flag.flag_name] + + with pytest.raises(FileNotInStorageError): + storage.read_file("archive", "path/to/valid.json") + + +@pytest.mark.django_db(databases=["default", "ta_timeseries"]) +def test_ta_processor_impl_success_keep_archive(storage, sample_test_json_path): + repository = RepositoryFactory.create() + commit = CommitFactory.create(repository=repository, branch="main") + upload = UploadFactory.create( + report__commit=commit, + state="processing", + storage_path="path/to/valid.json", + ) + + flag = RepositoryFlagFactory.create(repository=repository, flag_name="unit") + UploadFlagMembershipFactory.create(report_session=upload, flag=flag) + + commit_yaml = {"codecov": {"archive": {"uploads": True}}} + + argument: UploadArguments = {"upload_id": upload.id} + + with open(sample_test_json_path, "rb") as f: + sample_content = f.read() + + storage.write_file("archive", "path/to/valid.json", sample_content) + + result = ta_processor_impl( + repository.repoid, commit.commitid, commit_yaml, argument, update_state=True + ) + + assert result is True + + testrun_db = Testrun.objects.filter(upload_id=upload.id).first() + assert testrun_db is not None + assert testrun_db.branch == commit.branch + assert testrun_db.upload_id == upload.id + assert testrun_db.flags == [flag.flag_name] + + assert storage.read_file("archive", "path/to/valid.json") is not None diff --git a/apps/worker/services/test_analytics/tests/test_ta_timeseries.py b/apps/worker/services/test_analytics/tests/test_ta_timeseries.py new file mode 100644 index 0000000000..fe57c695bc --- /dev/null +++ b/apps/worker/services/test_analytics/tests/test_ta_timeseries.py @@ -0,0 +1,529 @@ +import time +from datetime import datetime, timedelta + +import pytest +from django.db import connections +from shared.django_apps.ta_timeseries.models import Testrun +from time_machine import travel + +from services.test_analytics.ta_timeseries import ( + calc_test_id, + get_pr_comment_agg, + get_pr_comment_failures, + get_summary, + get_testrun_branch_summary_via_testrun, + get_testruns_for_flake_detection, + insert_testrun, + update_testrun_to_flaky, +) + + +@pytest.mark.django_db(databases=["ta_timeseries"]) +def test_insert_testrun(): + insert_testrun( + timestamp=datetime.now(), + repo_id=1, + commit_sha="commit_sha", + branch="branch", + upload_id=1, + flags=["flag1", "flag2"], + parsing_info={ + "framework": "Pytest", + "testruns": [ + { + "name": "test_name", + "classname": "test_classname", + "computed_name": "computed_name", + "duration": 1.0, + "outcome": "pass", + "testsuite": "test_suite", + "failure_message": None, + "filename": "test_filename", + "build_url": None, + } + ], + }, + ) + + t = Testrun.objects.get( + name="test_name", + classname="test_classname", + testsuite="test_suite", + failure_message=None, + filename="test_filename", + ) + assert t.outcome == "pass" + + +@pytest.mark.django_db(databases=["ta_timeseries"]) +def test_pr_comment_agg(): + insert_testrun( + timestamp=datetime.now(), + repo_id=1, + commit_sha="commit_sha", + branch="branch", + upload_id=1, + flags=["flag1", "flag2"], + parsing_info={ + "framework": "Pytest", + "testruns": [ + { + "name": "test_name", + "classname": "test_classname", + "computed_name": "computed_name", + "duration": 1.0, + "outcome": "pass", + "testsuite": "test_suite", + "failure_message": None, + "filename": "test_filename", + "build_url": None, + } + ], + }, + ) + + agg = get_pr_comment_agg(1, "commit_sha") + assert agg == { + "passed": 1, + "failed": 0, + "skipped": 0, + } + + +@pytest.mark.django_db(databases=["ta_timeseries"]) +def test_pr_comment_failures(): + insert_testrun( + timestamp=datetime.now(), + repo_id=1, + commit_sha="commit_sha", + branch="branch", + upload_id=1, + flags=["flag1", "flag2"], + parsing_info={ + "framework": "Pytest", + "testruns": [ + { + "name": "test_name", + "classname": "test_classname", + "computed_name": "computed_name", + "duration": 1.0, + "outcome": "failure", + "testsuite": "test_suite", + "failure_message": "failure_message", + "filename": "test_filename", + "build_url": None, + } + ], + }, + ) + + failures = get_pr_comment_failures(1, "commit_sha") + assert len(failures) == 1 + failure = failures[0] + assert failure["test_id"] == calc_test_id( + "test_name", "test_classname", "test_suite" + ) + assert failure["computed_name"] == "computed_name" + assert failure["failure_message"] == "failure_message" + assert failure["duration_seconds"] == 1.0 + assert failure["upload_id"] == 1 + + +@pytest.mark.django_db(databases=["ta_timeseries"]) +def test_get_testruns_for_flake_detection(db): + test_ids = {calc_test_id("flaky_test_name", "test_classname", "test_suite")} + insert_testrun( + timestamp=datetime.now(), + repo_id=1, + commit_sha="commit_sha", + branch="branch", + upload_id=1, + flags=["flag1", "flag2"], + parsing_info={ + "framework": "Pytest", + "testruns": [ + { + "name": "test_name", + "classname": "test_classname", + "computed_name": "computed_name", + "duration": 1.0, + "outcome": "failure", + "testsuite": "test_suite", + "failure_message": "failure_message", + "filename": "test_filename", + "build_url": None, + }, + { + "name": "flaky_test_name", + "classname": "test_classname", + "computed_name": "computed_name", + "duration": 1.0, + "outcome": "failure", + "testsuite": "test_suite", + "failure_message": "failure_message", + "filename": "test_filename", + "build_url": None, + }, + { + "name": "flaky_test_name", + "classname": "test_classname", + "computed_name": "computed_name", + "duration": 1.0, + "outcome": "pass", + "testsuite": "test_suite", + "failure_message": "failure_message", + "filename": "test_filename", + "build_url": None, + }, + { + "name": "test_name", + "classname": "test_classname", + "computed_name": "computed_name", + "duration": 1.0, + "outcome": "pass", + "testsuite": "test_suite", + "failure_message": "failure_message", + "filename": "test_filename", + "build_url": None, + }, + ], + }, + flaky_test_ids=test_ids, + ) + + testruns = get_testruns_for_flake_detection(1, test_ids) + assert len(testruns) == 3 + assert testruns[0].outcome == "failure" + assert testruns[0].failure_message == "failure_message" + assert testruns[0].name == "test_name" + assert testruns[1].outcome == "flaky_failure" + assert testruns[1].failure_message == "failure_message" + assert testruns[1].name == "flaky_test_name" + assert testruns[2].outcome == "pass" + assert testruns[2].failure_message == "failure_message" + assert testruns[2].name == "flaky_test_name" + + +@pytest.mark.django_db(databases=["ta_timeseries"]) +@travel(datetime(2025, 1, 1), tick=False) +def test_update_testrun_to_flaky(): + insert_testrun( + timestamp=datetime.now(), + repo_id=1, + commit_sha="commit_sha", + branch="branch", + upload_id=1, + flags=["flag1", "flag2"], + parsing_info={ + "framework": "Pytest", + "testruns": [ + { + "name": "test_name", + "classname": "test_classname", + "computed_name": "computed_name", + "duration": 1.0, + "outcome": "failure", + "testsuite": "test_suite", + "failure_message": "failure_message", + "filename": "test_filename", + "build_url": None, + }, + ], + }, + ) + update_testrun_to_flaky( + datetime.now(), + calc_test_id("test_name", "test_classname", "test_suite"), + ) + testrun = Testrun.objects.get( + name="test_name", + classname="test_classname", + testsuite="test_suite", + ) + assert testrun.outcome == "flaky_failure" + + +@pytest.fixture +@pytest.mark.django_db(databases=["ta_timeseries"], transaction=True) +def continuous_aggregate_policy(): + connection = connections["ta_timeseries"] + with connection.cursor() as cursor: + cursor.execute( + """ + TRUNCATE TABLE ta_timeseries_testrun; + TRUNCATE TABLE ta_timeseries_testrun_summary_1day; + """ + ) + cursor.execute( + """ + SELECT _timescaledb_internal.start_background_workers(); + SELECT remove_continuous_aggregate_policy('ta_timeseries_testrun_summary_1day'); + SELECT add_continuous_aggregate_policy( + 'ta_timeseries_testrun_summary_1day', + start_offset => '7 days', + end_offset => NULL, + schedule_interval => INTERVAL '10 milliseconds' + ); + """ + ) + + yield + + with connection.cursor() as cursor: + cursor.execute( + """ + SELECT remove_continuous_aggregate_policy('ta_timeseries_testrun_summary_1day'); + SELECT add_continuous_aggregate_policy( + 'ta_timeseries_testrun_summary_1day', + start_offset => '7 days', + end_offset => '1 days', + schedule_interval => INTERVAL '1 days' + ); + """ + ) + + +@pytest.mark.integration +@pytest.mark.django_db(databases=["ta_timeseries"], transaction=True) +def test_get_testrun_summary(continuous_aggregate_policy): + insert_testrun( + timestamp=datetime.now() - timedelta(days=2), + repo_id=1, + commit_sha="commit_sha", + branch="branch", + upload_id=1, + flags=["flag1"], + parsing_info={ + "framework": "Pytest", + "testruns": [ + { + "name": "test_name", + "classname": "test_classname", + "computed_name": "computed_name", + "duration": 1.0, + "outcome": "pass", + "testsuite": "test_suite", + "failure_message": "failure_message", + "filename": "test_filename", + "build_url": None, + }, + ], + }, + ) + insert_testrun( + timestamp=datetime.now() - timedelta(days=2), + repo_id=1, + commit_sha="commit_sha", + branch="branch", + upload_id=1, + flags=["flag1", "flag2"], + parsing_info={ + "framework": "Pytest", + "testruns": [ + { + "name": "test_name2", + "classname": "test_classname2", + "computed_name": "computed_name2", + "duration": 1.0, + "outcome": "pass", + "testsuite": "test_suite2", + "failure_message": "failure_message2", + "filename": "test_filename2", + "build_url": None, + }, + ], + }, + ) + + insert_testrun( + timestamp=datetime.now() - timedelta(days=2), + repo_id=1, + commit_sha="commit_sha", + branch="branch", + upload_id=1, + flags=["flag2"], + parsing_info={ + "framework": "Pytest", + "testruns": [ + { + "name": "test_name", + "classname": "test_classname", + "computed_name": "computed_name", + "duration": 3.0, + "outcome": "failure", + "testsuite": "test_suite", + "failure_message": "failure_message", + "filename": "test_filename", + "build_url": None, + }, + ], + }, + ) + + i = 0 + summaries = get_summary(1) + while len(summaries) < 2: + i += 1 + time.sleep(1) + summaries = get_summary(1) + if i > 10: + raise Exception("summaries not found") + + assert len(summaries) == 2 + assert summaries[0].testsuite == "test_suite" + assert summaries[0].classname == "test_classname" + assert summaries[0].name == "test_name" + assert summaries[0].avg_duration_seconds == 2.0 + assert summaries[0].last_duration_seconds == 3.0 + assert summaries[0].pass_count == 1 + assert summaries[0].fail_count == 1 + assert summaries[0].flaky_fail_count == 0 + assert summaries[0].skip_count == 0 + assert summaries[0].flags == ["flag1", "flag2"] + + assert summaries[1].testsuite == "test_suite2" + assert summaries[1].classname == "test_classname2" + assert summaries[1].name == "test_name2" + assert summaries[1].avg_duration_seconds == 1.0 + assert summaries[1].last_duration_seconds == 1.0 + assert summaries[1].pass_count == 1 + assert summaries[1].fail_count == 0 + assert summaries[1].flaky_fail_count == 0 + assert summaries[1].skip_count == 0 + assert summaries[1].flags == ["flag1", "flag2"] + + +@pytest.mark.integration +@pytest.mark.django_db(databases=["ta_timeseries"], transaction=True) +def test_get_testrun_branch_summary_via_testrun(): + insert_testrun( + timestamp=datetime.now(), + repo_id=1, + commit_sha="commit_sha1", + branch="feature-branch", + upload_id=1, + flags=["flag1"], + parsing_info={ + "framework": "Pytest", + "testruns": [ + { + "name": "test_name", + "classname": "test_classname", + "computed_name": "computed_name", + "duration": 1.0, + "outcome": "pass", + "testsuite": "test_suite", + "failure_message": None, + "filename": "test_filename", + "build_url": None, + }, + ], + }, + ) + + insert_testrun( + timestamp=datetime.now(), + repo_id=1, + commit_sha="commit_sha2", + branch="feature-branch", + upload_id=2, + flags=["flag1", "flag2"], + parsing_info={ + "framework": "Pytest", + "testruns": [ + { + "name": "test_name", + "classname": "test_classname", + "computed_name": "computed_name", + "duration": 3.0, + "outcome": "failure", + "testsuite": "test_suite", + "failure_message": "failure_message", + "filename": "test_filename", + "build_url": None, + }, + ], + }, + ) + + insert_testrun( + timestamp=datetime.now(), + repo_id=1, + commit_sha="commit_sha2", + branch="feature-branch", + upload_id=2, + flags=["flag2"], + parsing_info={ + "framework": "Pytest", + "testruns": [ + { + "name": "test_name2", + "classname": "test_classname2", + "computed_name": "computed_name2", + "duration": 2.0, + "outcome": "pass", + "testsuite": "test_suite2", + "failure_message": None, + "filename": "test_filename2", + "build_url": None, + }, + ], + }, + ) + + # Add a test on a different branch that should not be included + insert_testrun( + timestamp=datetime.now(), + repo_id=1, + commit_sha="commit_sha3", + branch="other-branch", + upload_id=3, + flags=["flag1"], + parsing_info={ + "framework": "Pytest", + "testruns": [ + { + "name": "test_name", + "classname": "test_classname", + "computed_name": "computed_name", + "duration": 5.0, + "outcome": "failure", + "testsuite": "test_suite", + "failure_message": "failure_message", + "filename": "test_filename", + "build_url": None, + }, + ], + }, + ) + + summaries = get_testrun_branch_summary_via_testrun(1, "feature-branch") + assert len(summaries) == 2 + + # Check first test summary + first_test = next(s for s in summaries if s.name == "test_name") + assert first_test.testsuite == "test_suite" + assert first_test.classname == "test_classname" + assert first_test.computed_name == "computed_name" + assert first_test.failing_commits == 1 # One commit had a failure + assert first_test.last_duration_seconds == 3.0 # Last run was 3.0 + assert first_test.avg_duration_seconds == 2.0 # Average of 1.0 and 3.0 + assert first_test.pass_count == 1 + assert first_test.fail_count == 1 + assert first_test.skip_count == 0 + assert first_test.flaky_fail_count == 0 + assert sorted(first_test.flags) == ["flag1", "flag2"] + + # Check second test summary + second_test = next(s for s in summaries if s.name == "test_name2") + assert second_test.testsuite == "test_suite2" + assert second_test.classname == "test_classname2" + assert second_test.computed_name == "computed_name2" + assert second_test.failing_commits == 0 # No failures + assert second_test.last_duration_seconds == 2.0 + assert second_test.avg_duration_seconds == 2.0 + assert second_test.pass_count == 1 + assert second_test.fail_count == 0 + assert second_test.skip_count == 0 + assert second_test.flaky_fail_count == 0 + assert second_test.flags == ["flag2"] diff --git a/apps/worker/services/test_analytics/utils.py b/apps/worker/services/test_analytics/utils.py new file mode 100644 index 0000000000..9366773dde --- /dev/null +++ b/apps/worker/services/test_analytics/utils.py @@ -0,0 +1,11 @@ +import mmh3 + + +def calc_test_id(name: str, classname: str, testsuite: str) -> bytes: + h = mmh3.mmh3_x64_128() # assumes we're running on x64 machines + h.update(testsuite.encode("utf-8")) + h.update(classname.encode("utf-8")) + h.update(name.encode("utf-8")) + test_id_hash = h.digest() + + return test_id_hash diff --git a/apps/worker/services/test_results.py b/apps/worker/services/test_results.py new file mode 100644 index 0000000000..a0be95a7f4 --- /dev/null +++ b/apps/worker/services/test_results.py @@ -0,0 +1,511 @@ +import logging +from dataclasses import dataclass +from hashlib import sha256 +from typing import Generic, Sequence, TypedDict, TypeVar + +from shared.django_apps.codecov_auth.models import Plan +from shared.plan.constants import TierName +from shared.yaml import UserYaml +from sqlalchemy import desc, func +from sqlalchemy.orm import joinedload +from sqlalchemy.orm.session import Session + +from database.enums import ReportType +from database.models import ( + Commit, + CommitReport, + Flake, + Repository, + RepositoryFlag, + TestInstance, + Upload, +) +from helpers.notifier import BaseNotifier, NotifierResult +from services.license import requires_license +from services.processing.types import UploadArguments +from services.report import BaseReportService +from services.urls import get_members_url, get_test_analytics_url +from services.yaml import read_yaml_field + + +class FinisherResult(TypedDict): + notify_attempted: bool + notify_succeeded: bool + queue_notify: bool + + +log = logging.getLogger(__name__) + + +class TestResultsReportService(BaseReportService): + def __init__(self, current_yaml: UserYaml): + super().__init__(current_yaml) + self.flag_dict = None + + def initialize_and_save_report( + self, commit: Commit, report_code: str | None = None + ) -> CommitReport: + db_session = commit.get_db_session() + current_report_row = ( + db_session.query(CommitReport) + .filter_by( + commit_id=commit.id_, + code=report_code, + report_type=ReportType.TEST_RESULTS.value, + ) + .first() + ) + if not current_report_row: + # This happens if the commit report is being created for the first time + # or backfilled + current_report_row = CommitReport( + commit_id=commit.id_, + code=report_code, + report_type=ReportType.TEST_RESULTS.value, + ) + db_session.add(current_report_row) + db_session.flush() + + return current_report_row + + # support flags in test results + def create_report_upload( + self, arguments: UploadArguments, commit_report: CommitReport + ) -> Upload: + upload = super().create_report_upload(arguments, commit_report) + self._attach_flags_to_upload(upload, arguments["flags"]) + return upload + + def _attach_flags_to_upload(self, upload: Upload, flag_names: Sequence[str]): + """Internal function that manages creating the proper `RepositoryFlag`s and attach the sessions to them + + Args: + upload (Upload): Description + flag_names (Sequence[str]): Description + + Returns: + TYPE: Description + """ + all_flags = [] + db_session = upload.get_db_session() + repoid = upload.report.commit.repoid + + if self.flag_dict is None: + self.fetch_repo_flags(db_session, repoid) + + for individual_flag in flag_names: + flag_obj = self.flag_dict.get(individual_flag, None) + if flag_obj is None: + flag_obj = RepositoryFlag( + repository_id=repoid, flag_name=individual_flag + ) + db_session.add(flag_obj) + db_session.flush() + self.flag_dict[individual_flag] = flag_obj + all_flags.append(flag_obj) + upload.flags = all_flags + db_session.flush() + return all_flags + + def fetch_repo_flags(self, db_session, repoid): + existing_flags_on_repo = ( + db_session.query(RepositoryFlag).filter_by(repository_id=repoid).all() + ) + self.flag_dict = {flag.flag_name: flag for flag in existing_flags_on_repo} + + +def generate_flags_hash(flag_names: list[str]) -> str: + return sha256((" ".join(sorted(flag_names))).encode("utf-8")).hexdigest() + + +def generate_test_id(repoid, testsuite, name, flags_hash): + return sha256( + (" ".join([str(x) for x in [repoid, testsuite, name, flags_hash]])).encode( + "utf-8" + ) + ).hexdigest() + + +T = TypeVar("T", str, bytes) + + +@dataclass +class TestResultsNotificationFailure(Generic[T]): + failure_message: str + display_name: str + envs: list[str] + test_id: T + duration_seconds: float + build_url: str | None = None + + +@dataclass +class FlakeInfo: + failed: int + count: int + + +@dataclass +class TACommentInDepthInfo(Generic[T]): + failures: list[TestResultsNotificationFailure[T]] + flaky_tests: dict[T, FlakeInfo] + + +@dataclass +class TestResultsNotificationPayload(Generic[T]): + failed: int + passed: int + skipped: int + info: TACommentInDepthInfo[T] | None = None + + +@dataclass +class ErrorPayload: + error_code: str + error_message: str | None = None + + +def wrap_in_details(summary: str, content: str): + result = f"
    {summary}\n{content}\n
    " + return result + + +def make_quoted(content: str) -> str: + lines = content.splitlines() + result = "\n".join("> " + line for line in lines) + return f"\n{result}\n" + + +def properly_backtick(content: str) -> str: + max_backtick_count = 0 + curr_backtick_count = 0 + prev_char = None + for char in content: + if char == "`": + curr_backtick_count += 1 + else: + curr_backtick_count = 0 + + if curr_backtick_count > max_backtick_count: + max_backtick_count = curr_backtick_count + + backticks = "`" * (max_backtick_count + 1) + return f"{backticks}python\n{content}\n{backticks}" + + +def wrap_in_code(content: str) -> str: + if "```" in content: + return properly_backtick(content) + else: + return f"\n```python\n{content}\n```\n" + + +def display_duration(f: float) -> str: + split_duration = str(f).split(".") + before_dot = split_duration[0] + if len(before_dot) > 3: + return before_dot + else: + return f"{f:.3g}" + + +def generate_failure_info( + fail: TestResultsNotificationFailure[T], +): + if fail.failure_message is not None: + failure_message = fail.failure_message + else: + failure_message = "No failure message available" + + failure_message = wrap_in_code(failure_message) + if fail.build_url: + return f"{failure_message}\n[View]({fail.build_url}) the CI Build" + else: + return failure_message + + +def generate_view_test_analytics_line(commit: Commit) -> str: + repo = commit.repository + test_analytics_url = get_test_analytics_url(repo, commit) + return f"\nTo view more test analytics, go to the [Test Analytics Dashboard]({test_analytics_url})\n📋 Got 3 mins? [Take this short survey](https://forms.gle/BpocVj23nhr2Y45G7) to help us improve Test Analytics." + + +def messagify_failure( + failure: TestResultsNotificationFailure[T], +) -> str: + test_name = wrap_in_code(failure.display_name.replace("\x1f", " ")) + formatted_duration = display_duration(failure.duration_seconds) + stack_trace_summary = f"Stack Traces | {formatted_duration}s run time" + stack_trace = wrap_in_details( + stack_trace_summary, + make_quoted(generate_failure_info(failure)), + ) + return make_quoted(f"{test_name}\n{stack_trace}") + + +def messagify_flake( + flaky_failure: TestResultsNotificationFailure[T], + flake_info: FlakeInfo, +) -> str: + test_name = wrap_in_code(flaky_failure.display_name.replace("\x1f", " ")) + formatted_duration = display_duration(flaky_failure.duration_seconds) + flake_rate = flake_info.failed / flake_info.count * 100 + flake_rate_section = f"**Flake rate in main:** {flake_rate:.2f}% (Passed {flake_info.count - flake_info.failed} times, Failed {flake_info.failed} times)" + stack_trace_summary = f"Stack Traces | {formatted_duration}s run time" + stack_trace = wrap_in_details( + stack_trace_summary, + make_quoted(generate_failure_info(flaky_failure)), + ) + return make_quoted(f"{test_name}\n{flake_rate_section}\n{stack_trace}") + + +def specific_error_message(error: ErrorPayload) -> str: + title = f"### :x: {error.error_code.replace('_', ' ').capitalize()}" + if error.error_code == "unsupported_file_format": + description = "\n".join( + [ + "Upload processing failed due to unsupported file format. Please review the parser error message:", + f"`{error.error_message}`", + "For more help, visit our [troubleshooting guide](https://docs.codecov.com/docs/test-analytics#troubleshooting).", + ] + ) + elif error.error_code == "file_not_in_storage": + description = "\n".join( + [ + "No result to display due to the CLI not being able to find the file.", + "Please ensure the file contains `junit` in the name and automated file search is enabled,", + "or the desired file specified by the `file` and `search_dir` arguments of the CLI.", + ] + ) + else: + raise ValueError("Unrecognized error code") + message = [ + title, + make_quoted(description), + ] + return "\n".join(message) + + +@dataclass +class TestResultsNotifier(BaseNotifier, Generic[T]): + payload: TestResultsNotificationPayload[T] | None = None + error: ErrorPayload | None = None + + def build_message(self) -> str: + if self.payload is None: + raise ValueError("Payload passed to notifier is None, cannot build message") + + message = [] + + if self.error: + message.append(specific_error_message(self.error)) + + if self.error and self.payload.info: + message.append("---") + + if self.payload.info: + message.append(f"### :x: {self.payload.failed} Tests Failed:") + + completed = self.payload.failed + self.payload.passed + + message += [ + "| Tests completed | Failed | Passed | Skipped |", + "|---|---|---|---|", + f"| {completed} | {self.payload.failed} | {self.payload.passed} | {self.payload.skipped} |", + ] + + failures = sorted( + ( + failure + for failure in self.payload.info.failures + if failure.test_id not in self.payload.info.flaky_tests + ), + key=lambda x: (x.duration_seconds, x.display_name), + ) + if failures: + failure_content = [ + f"{messagify_failure(failure)}" for failure in failures + ] + + top_3_failed_section = wrap_in_details( + f"View the top {min(3, len(failures))} failed test(s) by shortest run time", + "\n".join(failure_content), + ) + + message.append(top_3_failed_section) + + flaky_failures = [ + failure + for failure in self.payload.info.failures + if failure.test_id in self.payload.info.flaky_tests + ] + if flaky_failures: + flake_content = [ + f"{messagify_flake(flaky_failure, self.payload.info.flaky_tests[flaky_failure.test_id])}" + for flaky_failure in flaky_failures + ] + + flaky_section = wrap_in_details( + f"View the full list of {len(flaky_failures)} :snowflake: flaky tests", + "\n".join(flake_content), + ) + + message.append(flaky_section) + + message.append(generate_view_test_analytics_line(self.commit)) + return "\n".join(message) + + def error_comment(self): + """ + This is no longer used in the new TA finisher task, but this is what used to display the error comment + """ + message: str + + if self.error is None: + message = ":x: We are unable to process any of the uploaded JUnit XML files. Please ensure your files are in the right format." + else: + message = specific_error_message(self.error) + + pull = self.get_pull() + if pull is None: + return False, "no_pull" + + sent_to_provider = self.send_to_provider(pull, message) + + if sent_to_provider is False: + return (False, "torngit_error") + + return (True, "comment_posted") + + def upgrade_comment(self): + pull = self.get_pull() + if pull is None: + return False, "no_pull" + + db_pull = pull.database_pull + provider_pull = pull.provider_pull + if provider_pull is None: + return False, "missing_provider_pull" + + link = get_members_url(db_pull) + + author_username = provider_pull["author"].get("username") + + if not requires_license(): + message = "\n".join( + [ + f"The author of this PR, {author_username}, is not an activated member of this organization on Codecov.", + f"Please [activate this user on Codecov]({link}) to display this PR comment.", + "Coverage data is still being uploaded to Codecov.io for purposes of overall coverage calculations.", + "Please don't hesitate to email us at support@codecov.io with any questions.", + ] + ) + else: + message = "\n".join( + [ + f"The author of this PR, {author_username}, is not activated in your Codecov Self-Hosted installation.", + f"Please [activate this user]({link}) to display this PR comment.", + "Coverage data is still being uploaded to Codecov Self-Hosted for the purposes of overall coverage calculations.", + "Please contact your Codecov On-Premises installation administrator with any questions.", + ] + ) + + sent_to_provider = self.send_to_provider(pull, message) + if sent_to_provider == False: + return (False, "torngit_error") + + return (True, "comment_posted") + + def notify(self): + assert self._pull + pull = self._pull + + message = self.build_message() + + sent_to_provider = self.send_to_provider(pull, message) + if sent_to_provider == False: + return NotifierResult.TORNGIT_ERROR + + return NotifierResult.COMMENT_POSTED + + +def latest_failures_for_commit( + db_session: Session, repo_id: int, commit_sha: str +) -> list[TestInstance]: + """ + This will result in a SQL query that looks something like this: + + SELECT DISTINCT ON (rti.test_id) rti.id, ... + FROM reports_testinstance rti + JOIN reports_upload ru ON ru.id = rti.upload_id + LEFT OUTER JOIN reports_test rt ON rt.id = rti.test_id + WHERE ... + ORDER BY rti.test_id, ru.created_at DESC + + The goal of this query is to return: + > the latest test instance failure for each unique test based on upload creation time + + The `DISTINCT ON` test_id with the order by test_id, enforces that we are only fetching one test instance for each test. + + The ordering by `upload.create_at DESC` enforces that we get the latest test instance for that unique test. + """ + + return ( + db_session.query(TestInstance) + .join(TestInstance.upload) + .options(joinedload(TestInstance.test)) + .filter(TestInstance.repoid == repo_id, TestInstance.commitid == commit_sha) + .filter(TestInstance.outcome.in_(["failure", "error"])) + .order_by(TestInstance.test_id) + .order_by(desc(Upload.created_at)) + .distinct(TestInstance.test_id) + .all() + ) + + +def get_test_summary_for_commit( + db_session: Session, repo_id: int, commit_sha: str +) -> dict[str, int]: + cte = ( + db_session.query(TestInstance) + .join(TestInstance.upload) + .options(joinedload(TestInstance.test)) + .filter(TestInstance.repoid == repo_id, TestInstance.commitid == commit_sha) + .order_by(TestInstance.test_id) + .order_by(desc(Upload.created_at)) + .distinct(TestInstance.test_id) + .cte(name="latest_test_instances") + ) + return dict( + db_session.query(cte.c.outcome, func.count(cte.c.test_id)) + .group_by(cte.c.outcome) + .all() + ) + + +def not_private_and_free_or_team(repo: Repository): + plan = Plan.objects.select_related("tier").get(name=repo.owner.plan) + + return not ( + repo.private + and plan + and plan.tier.tier_name in {TierName.BASIC.value, TierName.TEAM.value} + ) + + +def should_do_flaky_detection(repo: Repository, commit_yaml: UserYaml) -> bool: + has_flaky_configured = read_yaml_field( + commit_yaml, ("test_analytics", "flake_detection"), True + ) + has_valid_plan_repo_or_owner = not_private_and_free_or_team(repo) + + return has_flaky_configured and has_valid_plan_repo_or_owner + + +def get_flake_set(db_session: Session, repoid: int) -> set[str]: + repo_flakes: list[Flake] = ( + db_session.query(Flake.testid) + .filter(Flake.repoid == repoid, Flake.end_date.is_(None)) + .all() + ) + return {flake.testid for flake in repo_flakes} diff --git a/apps/worker/services/tests/__init__.py b/apps/worker/services/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/services/tests/cassetes/test_ai_pr_review/test_perform_duplicate_review.yaml b/apps/worker/services/tests/cassetes/test_ai_pr_review/test_perform_duplicate_review.yaml new file mode 100644 index 0000000000..1663c434c4 --- /dev/null +++ b/apps/worker/services/tests/cassetes/test_ai_pr_review/test_perform_duplicate_review.yaml @@ -0,0 +1,85 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/scott-codecov/codecov-test/pulls/40 + response: + content: '{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","id":1609199716,"node_id":"PR_kwDOHO5Jtc5f6nBk","html_url":"https://github.com/scott-codecov/codecov-test/pull/40","diff_url":"https://github.com/scott-codecov/codecov-test/pull/40.diff","patch_url":"https://github.com/scott-codecov/codecov-test/pull/40.patch","issue_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/40","number":40,"state":"open","locked":false,"title":"Test + AI PR review","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":null,"created_at":"2023-11-20T14:17:17Z","updated_at":"2023-11-20T14:53:55Z","closed_at":null,"merged_at":null,"merge_commit_sha":"388cc84b5f6a167db13df1f139f4305e32f9f1eb","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40/commits","review_comments_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40/comments","review_comment_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments{/number}","comments_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/40/comments","statuses_url":"https://api.github.com/repos/scott-codecov/codecov-test/statuses/b607bb0e17e1b8d8699272a26e32986a933f9946","head":{"label":"scott-codecov:scott-codecov-patch-3","ref":"scott-codecov-patch-3","sha":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"repo":{"id":485378485,"node_id":"R_kgDOHO5JtQ","name":"codecov-test","full_name":"scott-codecov/codecov-test","private":true,"owner":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/scott-codecov/codecov-test","description":null,"fork":false,"url":"https://api.github.com/repos/scott-codecov/codecov-test","forks_url":"https://api.github.com/repos/scott-codecov/codecov-test/forks","keys_url":"https://api.github.com/repos/scott-codecov/codecov-test/keys{/key_id}","collaborators_url":"https://api.github.com/repos/scott-codecov/codecov-test/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/scott-codecov/codecov-test/teams","hooks_url":"https://api.github.com/repos/scott-codecov/codecov-test/hooks","issue_events_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/events{/number}","events_url":"https://api.github.com/repos/scott-codecov/codecov-test/events","assignees_url":"https://api.github.com/repos/scott-codecov/codecov-test/assignees{/user}","branches_url":"https://api.github.com/repos/scott-codecov/codecov-test/branches{/branch}","tags_url":"https://api.github.com/repos/scott-codecov/codecov-test/tags","blobs_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/refs{/sha}","trees_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/trees{/sha}","statuses_url":"https://api.github.com/repos/scott-codecov/codecov-test/statuses/{sha}","languages_url":"https://api.github.com/repos/scott-codecov/codecov-test/languages","stargazers_url":"https://api.github.com/repos/scott-codecov/codecov-test/stargazers","contributors_url":"https://api.github.com/repos/scott-codecov/codecov-test/contributors","subscribers_url":"https://api.github.com/repos/scott-codecov/codecov-test/subscribers","subscription_url":"https://api.github.com/repos/scott-codecov/codecov-test/subscription","commits_url":"https://api.github.com/repos/scott-codecov/codecov-test/commits{/sha}","git_commits_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/commits{/sha}","comments_url":"https://api.github.com/repos/scott-codecov/codecov-test/comments{/number}","issue_comment_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/comments{/number}","contents_url":"https://api.github.com/repos/scott-codecov/codecov-test/contents/{+path}","compare_url":"https://api.github.com/repos/scott-codecov/codecov-test/compare/{base}...{head}","merges_url":"https://api.github.com/repos/scott-codecov/codecov-test/merges","archive_url":"https://api.github.com/repos/scott-codecov/codecov-test/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/scott-codecov/codecov-test/downloads","issues_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues{/number}","pulls_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls{/number}","milestones_url":"https://api.github.com/repos/scott-codecov/codecov-test/milestones{/number}","notifications_url":"https://api.github.com/repos/scott-codecov/codecov-test/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/scott-codecov/codecov-test/labels{/name}","releases_url":"https://api.github.com/repos/scott-codecov/codecov-test/releases{/id}","deployments_url":"https://api.github.com/repos/scott-codecov/codecov-test/deployments","created_at":"2022-04-25T13:15:44Z","updated_at":"2022-04-29T10:48:46Z","pushed_at":"2023-11-20T14:17:18Z","git_url":"git://github.com/scott-codecov/codecov-test.git","ssh_url":"git@github.com:scott-codecov/codecov-test.git","clone_url":"https://github.com/scott-codecov/codecov-test.git","svn_url":"https://github.com/scott-codecov/codecov-test","homepage":null,"size":239,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":14,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":14,"watchers":0,"default_branch":"master"}},"base":{"label":"scott-codecov:master","ref":"master","sha":"ece177a1e98a568a5428751b21e9c2530ab16927","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"repo":{"id":485378485,"node_id":"R_kgDOHO5JtQ","name":"codecov-test","full_name":"scott-codecov/codecov-test","private":true,"owner":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/scott-codecov/codecov-test","description":null,"fork":false,"url":"https://api.github.com/repos/scott-codecov/codecov-test","forks_url":"https://api.github.com/repos/scott-codecov/codecov-test/forks","keys_url":"https://api.github.com/repos/scott-codecov/codecov-test/keys{/key_id}","collaborators_url":"https://api.github.com/repos/scott-codecov/codecov-test/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/scott-codecov/codecov-test/teams","hooks_url":"https://api.github.com/repos/scott-codecov/codecov-test/hooks","issue_events_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/events{/number}","events_url":"https://api.github.com/repos/scott-codecov/codecov-test/events","assignees_url":"https://api.github.com/repos/scott-codecov/codecov-test/assignees{/user}","branches_url":"https://api.github.com/repos/scott-codecov/codecov-test/branches{/branch}","tags_url":"https://api.github.com/repos/scott-codecov/codecov-test/tags","blobs_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/refs{/sha}","trees_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/trees{/sha}","statuses_url":"https://api.github.com/repos/scott-codecov/codecov-test/statuses/{sha}","languages_url":"https://api.github.com/repos/scott-codecov/codecov-test/languages","stargazers_url":"https://api.github.com/repos/scott-codecov/codecov-test/stargazers","contributors_url":"https://api.github.com/repos/scott-codecov/codecov-test/contributors","subscribers_url":"https://api.github.com/repos/scott-codecov/codecov-test/subscribers","subscription_url":"https://api.github.com/repos/scott-codecov/codecov-test/subscription","commits_url":"https://api.github.com/repos/scott-codecov/codecov-test/commits{/sha}","git_commits_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/commits{/sha}","comments_url":"https://api.github.com/repos/scott-codecov/codecov-test/comments{/number}","issue_comment_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/comments{/number}","contents_url":"https://api.github.com/repos/scott-codecov/codecov-test/contents/{+path}","compare_url":"https://api.github.com/repos/scott-codecov/codecov-test/compare/{base}...{head}","merges_url":"https://api.github.com/repos/scott-codecov/codecov-test/merges","archive_url":"https://api.github.com/repos/scott-codecov/codecov-test/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/scott-codecov/codecov-test/downloads","issues_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues{/number}","pulls_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls{/number}","milestones_url":"https://api.github.com/repos/scott-codecov/codecov-test/milestones{/number}","notifications_url":"https://api.github.com/repos/scott-codecov/codecov-test/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/scott-codecov/codecov-test/labels{/name}","releases_url":"https://api.github.com/repos/scott-codecov/codecov-test/releases{/id}","deployments_url":"https://api.github.com/repos/scott-codecov/codecov-test/deployments","created_at":"2022-04-25T13:15:44Z","updated_at":"2022-04-29T10:48:46Z","pushed_at":"2023-11-20T14:17:18Z","git_url":"git://github.com/scott-codecov/codecov-test.git","ssh_url":"git@github.com:scott-codecov/codecov-test.git","clone_url":"https://github.com/scott-codecov/codecov-test.git","svn_url":"https://github.com/scott-codecov/codecov-test","homepage":null,"size":239,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":14,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":14,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40"},"issue":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/issues/40"},"comments":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/issues/40/comments"},"review_comments":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40/comments"},"review_comment":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40/commits"},"statuses":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/statuses/b607bb0e17e1b8d8699272a26e32986a933f9946"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null,"merged":false,"mergeable":true,"rebaseable":true,"mergeable_state":"unstable","merged_by":null,"comments":0,"review_comments":16,"maintainer_can_modify":false,"commits":1,"additions":2,"deletions":11,"changed_files":1}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 20 Nov 2023 14:53:56 GMT + ETag: + - W/"e23f5a9033ae5e1901f888c916718b425573074a64aebd9178c14b0f2f106cc4" + Last-Modified: + - Mon, 20 Nov 2023 14:53:55 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FA13:7260:467D957:9373AB6:655B7304 + X-OAuth-Scopes: + - '' + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4988' + X-RateLimit-Reset: + - '1700494278' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '12' + X-XSS-Protection: + - '0' + x-accepted-github-permissions: + - pull_requests=read; contents=read + x-github-api-version-selected: + - '2022-11-28' + x-oauth-client-id: + - Iv1.88e0c58abd4e2e45 + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/apps/worker/services/tests/cassetes/test_ai_pr_review/test_perform_initial_review.yaml b/apps/worker/services/tests/cassetes/test_ai_pr_review/test_perform_initial_review.yaml new file mode 100644 index 0000000000..b7b618a191 --- /dev/null +++ b/apps/worker/services/tests/cassetes/test_ai_pr_review/test_perform_initial_review.yaml @@ -0,0 +1,423 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/scott-codecov/codecov-test/pulls/40 + response: + content: '{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","id":1609199716,"node_id":"PR_kwDOHO5Jtc5f6nBk","html_url":"https://github.com/scott-codecov/codecov-test/pull/40","diff_url":"https://github.com/scott-codecov/codecov-test/pull/40.diff","patch_url":"https://github.com/scott-codecov/codecov-test/pull/40.patch","issue_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/40","number":40,"state":"open","locked":false,"title":"Test + AI PR review","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":null,"created_at":"2023-11-20T14:17:17Z","updated_at":"2023-11-20T14:53:19Z","closed_at":null,"merged_at":null,"merge_commit_sha":"388cc84b5f6a167db13df1f139f4305e32f9f1eb","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40/commits","review_comments_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40/comments","review_comment_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments{/number}","comments_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/40/comments","statuses_url":"https://api.github.com/repos/scott-codecov/codecov-test/statuses/b607bb0e17e1b8d8699272a26e32986a933f9946","head":{"label":"scott-codecov:scott-codecov-patch-3","ref":"scott-codecov-patch-3","sha":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"repo":{"id":485378485,"node_id":"R_kgDOHO5JtQ","name":"codecov-test","full_name":"scott-codecov/codecov-test","private":true,"owner":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/scott-codecov/codecov-test","description":null,"fork":false,"url":"https://api.github.com/repos/scott-codecov/codecov-test","forks_url":"https://api.github.com/repos/scott-codecov/codecov-test/forks","keys_url":"https://api.github.com/repos/scott-codecov/codecov-test/keys{/key_id}","collaborators_url":"https://api.github.com/repos/scott-codecov/codecov-test/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/scott-codecov/codecov-test/teams","hooks_url":"https://api.github.com/repos/scott-codecov/codecov-test/hooks","issue_events_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/events{/number}","events_url":"https://api.github.com/repos/scott-codecov/codecov-test/events","assignees_url":"https://api.github.com/repos/scott-codecov/codecov-test/assignees{/user}","branches_url":"https://api.github.com/repos/scott-codecov/codecov-test/branches{/branch}","tags_url":"https://api.github.com/repos/scott-codecov/codecov-test/tags","blobs_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/refs{/sha}","trees_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/trees{/sha}","statuses_url":"https://api.github.com/repos/scott-codecov/codecov-test/statuses/{sha}","languages_url":"https://api.github.com/repos/scott-codecov/codecov-test/languages","stargazers_url":"https://api.github.com/repos/scott-codecov/codecov-test/stargazers","contributors_url":"https://api.github.com/repos/scott-codecov/codecov-test/contributors","subscribers_url":"https://api.github.com/repos/scott-codecov/codecov-test/subscribers","subscription_url":"https://api.github.com/repos/scott-codecov/codecov-test/subscription","commits_url":"https://api.github.com/repos/scott-codecov/codecov-test/commits{/sha}","git_commits_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/commits{/sha}","comments_url":"https://api.github.com/repos/scott-codecov/codecov-test/comments{/number}","issue_comment_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/comments{/number}","contents_url":"https://api.github.com/repos/scott-codecov/codecov-test/contents/{+path}","compare_url":"https://api.github.com/repos/scott-codecov/codecov-test/compare/{base}...{head}","merges_url":"https://api.github.com/repos/scott-codecov/codecov-test/merges","archive_url":"https://api.github.com/repos/scott-codecov/codecov-test/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/scott-codecov/codecov-test/downloads","issues_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues{/number}","pulls_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls{/number}","milestones_url":"https://api.github.com/repos/scott-codecov/codecov-test/milestones{/number}","notifications_url":"https://api.github.com/repos/scott-codecov/codecov-test/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/scott-codecov/codecov-test/labels{/name}","releases_url":"https://api.github.com/repos/scott-codecov/codecov-test/releases{/id}","deployments_url":"https://api.github.com/repos/scott-codecov/codecov-test/deployments","created_at":"2022-04-25T13:15:44Z","updated_at":"2022-04-29T10:48:46Z","pushed_at":"2023-11-20T14:17:18Z","git_url":"git://github.com/scott-codecov/codecov-test.git","ssh_url":"git@github.com:scott-codecov/codecov-test.git","clone_url":"https://github.com/scott-codecov/codecov-test.git","svn_url":"https://github.com/scott-codecov/codecov-test","homepage":null,"size":239,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":14,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":14,"watchers":0,"default_branch":"master"}},"base":{"label":"scott-codecov:master","ref":"master","sha":"ece177a1e98a568a5428751b21e9c2530ab16927","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"repo":{"id":485378485,"node_id":"R_kgDOHO5JtQ","name":"codecov-test","full_name":"scott-codecov/codecov-test","private":true,"owner":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/scott-codecov/codecov-test","description":null,"fork":false,"url":"https://api.github.com/repos/scott-codecov/codecov-test","forks_url":"https://api.github.com/repos/scott-codecov/codecov-test/forks","keys_url":"https://api.github.com/repos/scott-codecov/codecov-test/keys{/key_id}","collaborators_url":"https://api.github.com/repos/scott-codecov/codecov-test/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/scott-codecov/codecov-test/teams","hooks_url":"https://api.github.com/repos/scott-codecov/codecov-test/hooks","issue_events_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/events{/number}","events_url":"https://api.github.com/repos/scott-codecov/codecov-test/events","assignees_url":"https://api.github.com/repos/scott-codecov/codecov-test/assignees{/user}","branches_url":"https://api.github.com/repos/scott-codecov/codecov-test/branches{/branch}","tags_url":"https://api.github.com/repos/scott-codecov/codecov-test/tags","blobs_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/refs{/sha}","trees_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/trees{/sha}","statuses_url":"https://api.github.com/repos/scott-codecov/codecov-test/statuses/{sha}","languages_url":"https://api.github.com/repos/scott-codecov/codecov-test/languages","stargazers_url":"https://api.github.com/repos/scott-codecov/codecov-test/stargazers","contributors_url":"https://api.github.com/repos/scott-codecov/codecov-test/contributors","subscribers_url":"https://api.github.com/repos/scott-codecov/codecov-test/subscribers","subscription_url":"https://api.github.com/repos/scott-codecov/codecov-test/subscription","commits_url":"https://api.github.com/repos/scott-codecov/codecov-test/commits{/sha}","git_commits_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/commits{/sha}","comments_url":"https://api.github.com/repos/scott-codecov/codecov-test/comments{/number}","issue_comment_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/comments{/number}","contents_url":"https://api.github.com/repos/scott-codecov/codecov-test/contents/{+path}","compare_url":"https://api.github.com/repos/scott-codecov/codecov-test/compare/{base}...{head}","merges_url":"https://api.github.com/repos/scott-codecov/codecov-test/merges","archive_url":"https://api.github.com/repos/scott-codecov/codecov-test/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/scott-codecov/codecov-test/downloads","issues_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues{/number}","pulls_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls{/number}","milestones_url":"https://api.github.com/repos/scott-codecov/codecov-test/milestones{/number}","notifications_url":"https://api.github.com/repos/scott-codecov/codecov-test/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/scott-codecov/codecov-test/labels{/name}","releases_url":"https://api.github.com/repos/scott-codecov/codecov-test/releases{/id}","deployments_url":"https://api.github.com/repos/scott-codecov/codecov-test/deployments","created_at":"2022-04-25T13:15:44Z","updated_at":"2022-04-29T10:48:46Z","pushed_at":"2023-11-20T14:17:18Z","git_url":"git://github.com/scott-codecov/codecov-test.git","ssh_url":"git@github.com:scott-codecov/codecov-test.git","clone_url":"https://github.com/scott-codecov/codecov-test.git","svn_url":"https://github.com/scott-codecov/codecov-test","homepage":null,"size":239,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":14,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":14,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40"},"issue":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/issues/40"},"comments":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/issues/40/comments"},"review_comments":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40/comments"},"review_comment":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40/commits"},"statuses":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/statuses/b607bb0e17e1b8d8699272a26e32986a933f9946"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null,"merged":false,"mergeable":true,"rebaseable":true,"mergeable_state":"unstable","merged_by":null,"comments":0,"review_comments":11,"maintainer_can_modify":false,"commits":1,"additions":2,"deletions":11,"changed_files":1}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 20 Nov 2023 14:53:24 GMT + ETag: + - W/"74fefe96f3867526ebe69e6a7c81a7fb7a083dffa1d84305d52b6d847c07f9ae" + Last-Modified: + - Mon, 20 Nov 2023 14:53:19 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FA0B:6556:2F15AFC:62B3653:655B72E4 + X-OAuth-Scopes: + - '' + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4991' + X-RateLimit-Reset: + - '1700494278' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '9' + X-XSS-Protection: + - '0' + x-accepted-github-permissions: + - pull_requests=read; contents=read + x-github-api-version-selected: + - '2022-11-28' + x-oauth-client-id: + - Iv1.88e0c58abd4e2e45 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/vnd.github.v3.diff + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/scott-codecov/codecov-test/pulls/40 + response: + content: "diff --git a/main/foo.py b/main/foo.py\nindex 9d285a4..41d8fd2 100644\n--- + a/main/foo.py\n+++ b/main/foo.py\n@@ -54,14 +54,5 @@ def mul4(x, y):\n def div4(x, + y):\n return x / y\n \n-def add5(x, y):\n- return x + y\n-\n-def sub5(x, + y):\n- return x - y\n-\n-def mul5(x, y):\n- return x * y\n-\n-def div5(x, + y):\n- return x / y\n+def testing(x, y):\n+ return x % y\n" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '361' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/vnd.github.v3.diff; charset=utf-8 + Date: + - Mon, 20 Nov 2023 14:53:24 GMT + ETag: + - '"668db0d87533fcf3c5093eb6ece232b132b2186efa5bb068a5b1616c422cd009"' + Last-Modified: + - Mon, 20 Nov 2023 14:53:19 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=diff + X-GitHub-Request-Id: + - FA0C:1ABD:41426D6:88D917B:655B72E4 + X-OAuth-Scopes: + - '' + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4990' + X-RateLimit-Reset: + - '1700494278' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '10' + X-XSS-Protection: + - '0' + x-accepted-github-permissions: + - pull_requests=read; contents=read + x-github-api-version-selected: + - '2022-11-28' + x-oauth-client-id: + - Iv1.88e0c58abd4e2e45 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"messages": [{"role": "user", "content": "\n Your purpose is to + act as a highly experienced software engineer and provide a thorough\n review + of code changes and suggest improvements. Do not comment on minor style issues,\n missing + comments or documentation. Identify and resolve significant concerns to improve\n overall + code quality.\n\n You will receive a Git diff where each line has been + prefixed with a unique identifer in\n square brackets. When referencing + lines in this diff use that identifier.\n\n Format your output as JSON + such that there is 1 top-level comment that summarizes your review\n and + multiple additional comments addressing specific lines in the code with the + changes you\n deem appropriate.\n\n The output should have this + JSON form:\n\n {\n \"body\": \"This is the summary comment\",\n \"comments\": + [\n {\n \"line_id\": 123,\n \"body\": + \"This is a comment about the code with line ID 123\",\n }\n ]\n }\n\n Limit + the number of comments to 10 at most.\n\n Here is the Git diff on which + you should base your review:\n\n [1] diff --git a/main/foo.py b/main/foo.py\n[2] + index 9d285a4..41d8fd2 100644\n[3] --- a/main/foo.py\n[4] +++ b/main/foo.py\n[5] + @@ -54,14 +54,5 @@ def mul4(x, y):\n[6] def div4(x, y):\n[7] return x + / y\n[8] \n[9] -def add5(x, y):\n[10] - return x + y\n[11] -\n[12] -def + sub5(x, y):\n[13] - return x - y\n[14] -\n[15] -def mul5(x, y):\n[16] - return + x * y\n[17] -\n[18] -def div5(x, y):\n[19] - return x / y\n[20] +def testing(x, + y):\n[21] + return x % y\n[22] \n "}], "model": "gpt-4"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '1768' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - AsyncOpenAI/Python 1.2.4 + x-stainless-arch: + - arm64 + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.2.4 + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.10.8 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + content: "{\n \"id\": \"chatcmpl-8MzwL6ONjJUzs3j8bTnWnwrwSsIze\",\n \"object\": + \"chat.completion\",\n \"created\": 1700492005,\n \"model\": \"gpt-4-0613\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": \"{\\n \\\"body\\\": \\\"The code quality + seems decent overall, but there are some significant concerns. Specifically, + the removal of the add5, sub5, mul5 and div5 functions and replacing them with + a testing function. Unless there is a specific reason for removing these functions + they should be kept as they may be used elsewhere in the codebase which would + lead to a lack of functionality and potential runtime exceptions. Also, the + 'testing' function is not appropriately named for what it does, it seems to + return the modulus of two numbers which isn't related to testing.\\\",\\n \\\"comments\\\": + [\\n {\\n \\\"line_id\\\": 9,\\n \\\"body\\\": + \\\"It looks like you've removed the add5 method. If this is being used elsewhere + in the codebase then its removal could cause issues. Please ensure that this + method isn't being used elsewhere before removal.\\\"\\n },\\n {\\n + \ \\\"line_id\\\": 12,\\n \\\"body\\\": \\\"You've removed + the sub5 method. Like with the add5 method, make sure that it's not being used + in other places in the codebase which could cause runtime issues.\\\"\\n },\\n + \ {\\n \\\"line_id\\\": 15,\\n \\\"body\\\": \\\"The + mul5 method has been removed. If it is used elsewhere, this could cause potential + problems. Validate it before deleting.\\\"\\n },\\n {\\n \\\"line_id\\\": + 18,\\n \\\"body\\\": \\\"You've also removed div5, again ensure it's + not being used anywhere else to prevent bugs and exceptions.\\\"\\n },\\n + \ {\\n \\\"line_id\\\": 20,\\n \\\"body\\\": \\\"This + new 'testing' function's name is not descriptive of its functionality. It looks + like it's performing a modulus operation, not testing. You should name this + function appropriately.\\\"\\n }\\n ]\\n}\"\n },\n \"finish_reason\": + \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 444,\n \"completion_tokens\": + 367,\n \"total_tokens\": 811\n }\n}\n" + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 829185b7fb102a90-ORD + Cache-Control: + - no-cache, must-revalidate + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Mon, 20 Nov 2023 14:53:53 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=p_3yp63VzSuwNk5.CA3kl8plTqNo8Hsb5QI1YsAr49w-1700492033-0-Ad4nyXhgxgh+XWx3krcrQYQDz60XLTGB/pE0rpbMGIfUx6RqaXdois1+sqtQE3hId9RlIm5JbkS6pRJvSBgzNzg=; + path=/; expires=Mon, 20-Nov-23 15:23:53 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=W0ReQkot08PZyftr5njoOXMmrUcFDwE6ERJG0iJwwjk-1700492033848-0-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + access-control-allow-origin: + - '*' + alt-svc: + - h3=":443"; ma=86400 + openai-model: + - gpt-4-0613 + openai-organization: + - functional-software + openai-processing-ms: + - '28368' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=15724800; includeSubDomains + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '300000' + x-ratelimit-limit-tokens_usage_based: + - '300000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '299573' + x-ratelimit-remaining-tokens_usage_based: + - '299573' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 85ms + x-ratelimit-reset-tokens_usage_based: + - 85ms + x-request-id: + - b4e74fa8a6f29812879968b0cc0b693a + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"commit_id": "b607bb0e17e1b8d8699272a26e32986a933f9946", "body": "The + code quality seems decent overall, but there are some significant concerns. + Specifically, the removal of the add5, sub5, mul5 and div5 functions and replacing + them with a testing function. Unless there is a specific reason for removing + these functions they should be kept as they may be used elsewhere in the codebase + which would lead to a lack of functionality and potential runtime exceptions. + Also, the ''testing'' function is not appropriately named for what it does, + it seems to return the modulus of two numbers which isn''t related to testing.", + "event": "COMMENT", "comments": [{"path": "main/foo.py", "position": 4, "body": + "It looks like you''ve removed the add5 method. If this is being used elsewhere + in the codebase then its removal could cause issues. Please ensure that this + method isn''t being used elsewhere before removal."}, {"path": "main/foo.py", + "position": 7, "body": "You''ve removed the sub5 method. Like with the add5 + method, make sure that it''s not being used in other places in the codebase + which could cause runtime issues."}, {"path": "main/foo.py", "position": 10, + "body": "The mul5 method has been removed. If it is used elsewhere, this could + cause potential problems. Validate it before deleting."}, {"path": "main/foo.py", + "position": 13, "body": "You''ve also removed div5, again ensure it''s not being + used anywhere else to prevent bugs and exceptions."}, {"path": "main/foo.py", + "position": 15, "body": "This new ''testing'' function''s name is not descriptive + of its functionality. It looks like it''s performing a modulus operation, not + testing. You should name this function appropriately."}]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '1692' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/scott-codecov/codecov-test/pulls/40/reviews + response: + content: '{"id":1740008775,"node_id":"PRR_kwDOHO5Jtc5ntm1H","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?u=1ea5f79283a26325f56e7cfa9eaca5cff3d538a4&v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"The + code quality seems decent overall, but there are some significant concerns. + Specifically, the removal of the add5, sub5, mul5 and div5 functions and replacing + them with a testing function. Unless there is a specific reason for removing + these functions they should be kept as they may be used elsewhere in the codebase + which would lead to a lack of functionality and potential runtime exceptions. + Also, the ''testing'' function is not appropriately named for what it does, + it seems to return the modulus of two numbers which isn''t related to testing.","state":"COMMENTED","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#pullrequestreview-1740008775","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#pullrequestreview-1740008775"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"submitted_at":"2023-11-20T14:53:55Z","commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 20 Nov 2023 14:53:55 GMT + ETag: + - W/"5397475b11e593c33b946a2bf17635c49034f50df65f994892c841de275b3381" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FA12:0A0F:26C7F07:517E47E:655B7302 + X-OAuth-Scopes: + - '' + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4989' + X-RateLimit-Reset: + - '1700494278' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '11' + X-XSS-Protection: + - '0' + x-accepted-github-permissions: + - pull_requests=write + x-github-api-version-selected: + - '2022-11-28' + x-oauth-client-id: + - Iv1.88e0c58abd4e2e45 + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/apps/worker/services/tests/cassetes/test_ai_pr_review/test_perform_new_commit.yaml b/apps/worker/services/tests/cassetes/test_ai_pr_review/test_perform_new_commit.yaml new file mode 100644 index 0000000000..8c3a7aef0d --- /dev/null +++ b/apps/worker/services/tests/cassetes/test_ai_pr_review/test_perform_new_commit.yaml @@ -0,0 +1,835 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/scott-codecov/codecov-test/pulls/40 + response: + content: '{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","id":1609199716,"node_id":"PR_kwDOHO5Jtc5f6nBk","html_url":"https://github.com/scott-codecov/codecov-test/pull/40","diff_url":"https://github.com/scott-codecov/codecov-test/pull/40.diff","patch_url":"https://github.com/scott-codecov/codecov-test/pull/40.patch","issue_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/40","number":40,"state":"open","locked":false,"title":"Test + AI PR review","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":null,"created_at":"2023-11-20T14:17:17Z","updated_at":"2023-11-20T14:56:16Z","closed_at":null,"merged_at":null,"merge_commit_sha":"30103e093dfe0875dcad2b93eb2f2488ece393ea","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40/commits","review_comments_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40/comments","review_comment_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments{/number}","comments_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/40/comments","statuses_url":"https://api.github.com/repos/scott-codecov/codecov-test/statuses/5c64a5143951193dde7b14c14611eebe1025f862","head":{"label":"scott-codecov:scott-codecov-patch-3","ref":"scott-codecov-patch-3","sha":"5c64a5143951193dde7b14c14611eebe1025f862","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"repo":{"id":485378485,"node_id":"R_kgDOHO5JtQ","name":"codecov-test","full_name":"scott-codecov/codecov-test","private":true,"owner":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/scott-codecov/codecov-test","description":null,"fork":false,"url":"https://api.github.com/repos/scott-codecov/codecov-test","forks_url":"https://api.github.com/repos/scott-codecov/codecov-test/forks","keys_url":"https://api.github.com/repos/scott-codecov/codecov-test/keys{/key_id}","collaborators_url":"https://api.github.com/repos/scott-codecov/codecov-test/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/scott-codecov/codecov-test/teams","hooks_url":"https://api.github.com/repos/scott-codecov/codecov-test/hooks","issue_events_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/events{/number}","events_url":"https://api.github.com/repos/scott-codecov/codecov-test/events","assignees_url":"https://api.github.com/repos/scott-codecov/codecov-test/assignees{/user}","branches_url":"https://api.github.com/repos/scott-codecov/codecov-test/branches{/branch}","tags_url":"https://api.github.com/repos/scott-codecov/codecov-test/tags","blobs_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/refs{/sha}","trees_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/trees{/sha}","statuses_url":"https://api.github.com/repos/scott-codecov/codecov-test/statuses/{sha}","languages_url":"https://api.github.com/repos/scott-codecov/codecov-test/languages","stargazers_url":"https://api.github.com/repos/scott-codecov/codecov-test/stargazers","contributors_url":"https://api.github.com/repos/scott-codecov/codecov-test/contributors","subscribers_url":"https://api.github.com/repos/scott-codecov/codecov-test/subscribers","subscription_url":"https://api.github.com/repos/scott-codecov/codecov-test/subscription","commits_url":"https://api.github.com/repos/scott-codecov/codecov-test/commits{/sha}","git_commits_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/commits{/sha}","comments_url":"https://api.github.com/repos/scott-codecov/codecov-test/comments{/number}","issue_comment_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/comments{/number}","contents_url":"https://api.github.com/repos/scott-codecov/codecov-test/contents/{+path}","compare_url":"https://api.github.com/repos/scott-codecov/codecov-test/compare/{base}...{head}","merges_url":"https://api.github.com/repos/scott-codecov/codecov-test/merges","archive_url":"https://api.github.com/repos/scott-codecov/codecov-test/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/scott-codecov/codecov-test/downloads","issues_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues{/number}","pulls_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls{/number}","milestones_url":"https://api.github.com/repos/scott-codecov/codecov-test/milestones{/number}","notifications_url":"https://api.github.com/repos/scott-codecov/codecov-test/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/scott-codecov/codecov-test/labels{/name}","releases_url":"https://api.github.com/repos/scott-codecov/codecov-test/releases{/id}","deployments_url":"https://api.github.com/repos/scott-codecov/codecov-test/deployments","created_at":"2022-04-25T13:15:44Z","updated_at":"2022-04-29T10:48:46Z","pushed_at":"2023-11-20T14:56:17Z","git_url":"git://github.com/scott-codecov/codecov-test.git","ssh_url":"git@github.com:scott-codecov/codecov-test.git","clone_url":"https://github.com/scott-codecov/codecov-test.git","svn_url":"https://github.com/scott-codecov/codecov-test","homepage":null,"size":239,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":14,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":14,"watchers":0,"default_branch":"master"}},"base":{"label":"scott-codecov:master","ref":"master","sha":"ece177a1e98a568a5428751b21e9c2530ab16927","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"repo":{"id":485378485,"node_id":"R_kgDOHO5JtQ","name":"codecov-test","full_name":"scott-codecov/codecov-test","private":true,"owner":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/scott-codecov/codecov-test","description":null,"fork":false,"url":"https://api.github.com/repos/scott-codecov/codecov-test","forks_url":"https://api.github.com/repos/scott-codecov/codecov-test/forks","keys_url":"https://api.github.com/repos/scott-codecov/codecov-test/keys{/key_id}","collaborators_url":"https://api.github.com/repos/scott-codecov/codecov-test/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/scott-codecov/codecov-test/teams","hooks_url":"https://api.github.com/repos/scott-codecov/codecov-test/hooks","issue_events_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/events{/number}","events_url":"https://api.github.com/repos/scott-codecov/codecov-test/events","assignees_url":"https://api.github.com/repos/scott-codecov/codecov-test/assignees{/user}","branches_url":"https://api.github.com/repos/scott-codecov/codecov-test/branches{/branch}","tags_url":"https://api.github.com/repos/scott-codecov/codecov-test/tags","blobs_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/refs{/sha}","trees_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/trees{/sha}","statuses_url":"https://api.github.com/repos/scott-codecov/codecov-test/statuses/{sha}","languages_url":"https://api.github.com/repos/scott-codecov/codecov-test/languages","stargazers_url":"https://api.github.com/repos/scott-codecov/codecov-test/stargazers","contributors_url":"https://api.github.com/repos/scott-codecov/codecov-test/contributors","subscribers_url":"https://api.github.com/repos/scott-codecov/codecov-test/subscribers","subscription_url":"https://api.github.com/repos/scott-codecov/codecov-test/subscription","commits_url":"https://api.github.com/repos/scott-codecov/codecov-test/commits{/sha}","git_commits_url":"https://api.github.com/repos/scott-codecov/codecov-test/git/commits{/sha}","comments_url":"https://api.github.com/repos/scott-codecov/codecov-test/comments{/number}","issue_comment_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues/comments{/number}","contents_url":"https://api.github.com/repos/scott-codecov/codecov-test/contents/{+path}","compare_url":"https://api.github.com/repos/scott-codecov/codecov-test/compare/{base}...{head}","merges_url":"https://api.github.com/repos/scott-codecov/codecov-test/merges","archive_url":"https://api.github.com/repos/scott-codecov/codecov-test/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/scott-codecov/codecov-test/downloads","issues_url":"https://api.github.com/repos/scott-codecov/codecov-test/issues{/number}","pulls_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls{/number}","milestones_url":"https://api.github.com/repos/scott-codecov/codecov-test/milestones{/number}","notifications_url":"https://api.github.com/repos/scott-codecov/codecov-test/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/scott-codecov/codecov-test/labels{/name}","releases_url":"https://api.github.com/repos/scott-codecov/codecov-test/releases{/id}","deployments_url":"https://api.github.com/repos/scott-codecov/codecov-test/deployments","created_at":"2022-04-25T13:15:44Z","updated_at":"2022-04-29T10:48:46Z","pushed_at":"2023-11-20T14:56:17Z","git_url":"git://github.com/scott-codecov/codecov-test.git","ssh_url":"git@github.com:scott-codecov/codecov-test.git","clone_url":"https://github.com/scott-codecov/codecov-test.git","svn_url":"https://github.com/scott-codecov/codecov-test","homepage":null,"size":239,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":14,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":14,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40"},"issue":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/issues/40"},"comments":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/issues/40/comments"},"review_comments":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40/comments"},"review_comment":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40/commits"},"statuses":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/statuses/5c64a5143951193dde7b14c14611eebe1025f862"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null,"merged":false,"mergeable":true,"rebaseable":true,"mergeable_state":"unstable","merged_by":null,"comments":0,"review_comments":16,"maintainer_can_modify":false,"commits":2,"additions":6,"deletions":12,"changed_files":2}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 20 Nov 2023 14:56:58 GMT + ETag: + - W/"296b07aa4eea57ba4a7acd389a2bfada2e39b4cc4030cbb54c9a64dacf5ad366" + Last-Modified: + - Mon, 20 Nov 2023 14:56:16 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FA69:9369:200C52D:432C6D9:655B73BA + X-OAuth-Scopes: + - '' + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4987' + X-RateLimit-Reset: + - '1700494278' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '13' + X-XSS-Protection: + - '0' + x-accepted-github-permissions: + - pull_requests=read; contents=read + x-github-api-version-selected: + - '2022-11-28' + x-oauth-client-id: + - Iv1.88e0c58abd4e2e45 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/vnd.github.v3.diff + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/scott-codecov/codecov-test/pulls/40 + response: + content: "diff --git a/main/bar.py b/main/bar.py\nindex 066a224..5991ec6 100644\n--- + a/main/bar.py\n+++ b/main/bar.py\n@@ -62,4 +62,7 @@ def add6(x, y):\n return + x + y\n \n def sub6(x, y):\n- return x - y\n\\ No newline at end of file\n+ + \ return x - y\n+\n+def add7(x, y):\n+ return x + y\n\\ No newline at end + of file\ndiff --git a/main/foo.py b/main/foo.py\nindex 9d285a4..41d8fd2 100644\n--- + a/main/foo.py\n+++ b/main/foo.py\n@@ -54,14 +54,5 @@ def mul4(x, y):\n def div4(x, + y):\n return x / y\n \n-def add5(x, y):\n- return x + y\n-\n-def sub5(x, + y):\n- return x - y\n-\n-def mul5(x, y):\n- return x * y\n-\n-def div5(x, + y):\n- return x / y\n+def testing(x, y):\n+ return x % y\n" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '666' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/vnd.github.v3.diff; charset=utf-8 + Date: + - Mon, 20 Nov 2023 14:56:59 GMT + ETag: + - '"cc2f80a6dfa37a49996d0a1ac913a4b5c35981e3cd3f50fcfde4eb5b197a4f55"' + Last-Modified: + - Mon, 20 Nov 2023 14:56:16 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=diff + X-GitHub-Request-Id: + - FA6A:1AA9:8EA6FF:127DB9A:655B73BB + X-OAuth-Scopes: + - '' + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4986' + X-RateLimit-Reset: + - '1700494278' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '14' + X-XSS-Protection: + - '0' + x-accepted-github-permissions: + - pull_requests=read; contents=read + x-github-api-version-selected: + - '2022-11-28' + x-oauth-client-id: + - Iv1.88e0c58abd4e2e45 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"messages": [{"role": "user", "content": "\n Your purpose is to + act as a highly experienced software engineer and provide a thorough\n review + of code changes and suggest improvements. Do not comment on minor style issues,\n missing + comments or documentation. Identify and resolve significant concerns to improve\n overall + code quality.\n\n You will receive a Git diff where each line has been + prefixed with a unique identifer in\n square brackets. When referencing + lines in this diff use that identifier.\n\n Format your output as JSON + such that there is 1 top-level comment that summarizes your review\n and + multiple additional comments addressing specific lines in the code with the + changes you\n deem appropriate.\n\n The output should have this + JSON form:\n\n {\n \"body\": \"This is the summary comment\",\n \"comments\": + [\n {\n \"line_id\": 123,\n \"body\": + \"This is a comment about the code with line ID 123\",\n }\n ]\n }\n\n Limit + the number of comments to 10 at most.\n\n Here is the Git diff on which + you should base your review:\n\n [1] diff --git a/main/bar.py b/main/bar.py\n[2] + index 066a224..5991ec6 100644\n[3] --- a/main/bar.py\n[4] +++ b/main/bar.py\n[5] + @@ -62,4 +62,7 @@ def add6(x, y):\n[6] return x + y\n[7] \n[8] def sub6(x, + y):\n[9] - return x - y\n[10] \\ No newline at end of file\n[11] + return + x - y\n[12] +\n[13] +def add7(x, y):\n[14] + return x + y\n[15] \\ No newline + at end of file\n[16] diff --git a/main/foo.py b/main/foo.py\n[17] index 9d285a4..41d8fd2 + 100644\n[18] --- a/main/foo.py\n[19] +++ b/main/foo.py\n[20] @@ -54,14 +54,5 + @@ def mul4(x, y):\n[21] def div4(x, y):\n[22] return x / y\n[23] \n[24] + -def add5(x, y):\n[25] - return x + y\n[26] -\n[27] -def sub5(x, y):\n[28] + - return x - y\n[29] -\n[30] -def mul5(x, y):\n[31] - return x * y\n[32] + -\n[33] -def div5(x, y):\n[34] - return x / y\n[35] +def testing(x, y):\n[36] + + return x % y\n[37] \n "}], "model": "gpt-4"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '2165' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - AsyncOpenAI/Python 1.2.4 + x-stainless-arch: + - arm64 + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.2.4 + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.10.8 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + content: "{\n \"id\": \"chatcmpl-8MzznvBLFYL4gS9jmfv1PAqsYge8n\",\n \"object\": + \"chat.completion\",\n \"created\": 1700492219,\n \"model\": \"gpt-4-0613\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": \"{\\n \\\"body\\\": \\\"The code changes + reflect transfer of add5, sub5, mul5, div5 functions from main/foo.py to main/bar.py + and they are renamed to add7, sub7, mul7, div7, respectively. There\u2019s + also a new testing function added. While these changes do not result in any + syntax errors, they introduce potential redundancy and confusion in naming of + functions.\\\",\\n \\\"comments\\\": [\\n {\\n \\\"line_id\\\": + 14,\\n \\\"body\\\": \\\"add7 function in bar.py is identically implemented + as add6. Consider removing redundant code.\\\"\\n },\\n {\\n \\\"line_id\\\": + 35,\\n \\\"body\\\": \\\"A new function 'testing' has been added + in foo.py file. Please make sure to add a more descriptive name for the function + that reflects what it does.\\\"\\n },\\n {\\n \\\"line_id\\\": + 35,\\n \\\"body\\\": \\\"The new 'testing' function uses the modulus + operation. The name doesn't reflect this, consider renaming it to 'modulus' + or a name that better reflects its functionality.\\\"\\n },\\n {\\n + \ \\\"line_id\\\": 24,\\n \\\"body\\\": \\\"Upon removal + of add5, sub5, mul5, div5 functions from foo.py, remember to update all call + sites that reference these functions.\\\"\\n },\\n {\\n \\\"line_id\\\": + 13,\\n \\\"body\\\": \\\"The new functions you've added named add7, + sub7, mul7, div7 are identical to some existing functions. It is recommended + to avoid duplicate functions, perhaps by creating a utility function if the + implementation across these functions is expected to remain identical.\\\"\\n + \ }\\n ]\\n}\"\n },\n \"finish_reason\": \"stop\"\n }\n + \ ],\n \"usage\": {\n \"prompt_tokens\": 605,\n \"completion_tokens\": + 343,\n \"total_tokens\": 948\n }\n}\n" + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 82918af47f44e20b-ORD + Cache-Control: + - no-cache, must-revalidate + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Mon, 20 Nov 2023 14:57:33 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=IzLHHOSkb3GsbWdl3ba5gtlo01gyLZUsGH0EywjECiI-1700492253-0-AeVW0HsDY7UprERytxSXL4vBLL4IcY0F2HYgOexbuiTq00GMqueTbmQ67ovaywdnb99xkTKnsOLrK4IHTeZuhd8=; + path=/; expires=Mon, 20-Nov-23 15:27:33 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=WSElFoSEEWNWnQwJ.4vWDd2fmWjMrkwEeP2ktZD2EvM-1700492253622-0-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + access-control-allow-origin: + - '*' + alt-svc: + - h3=":443"; ma=86400 + openai-model: + - gpt-4-0613 + openai-organization: + - functional-software + openai-processing-ms: + - '33926' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=15724800; includeSubDomains + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '300000' + x-ratelimit-limit-tokens_usage_based: + - '300000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '299478' + x-ratelimit-remaining-tokens_usage_based: + - '299478' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 104ms + x-ratelimit-reset-tokens_usage_based: + - 104ms + x-request-id: + - aad5ef2ac250b9a3b7774bd5cc43e1b1 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/scott-codecov/codecov-test/pulls/40/comments?per_page=100&page=1 + response: + content: '[{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299190","pull_request_review_id":1739975935,"id":1399299190,"node_id":"PRRC_kwDOHO5Jtc5TZ5x2","diff_hunk":"@@ + -54,14 +54,5 @@ def mul4(x, y):\n def div4(x, y):\n return x / y\n \n-def + add5(x, y):","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"The + function `add5` has been removed. If there are any dependencies on this function + elsewhere in the codebase, they''ll need to be updated or this can potentially + break your code. Please ensure this function is not needed elsewhere or replaced + by a similar functionality.","created_at":"2023-11-20T14:39:23Z","updated_at":"2023-11-20T14:39:24Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399299190","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299190"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399299190"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299190/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":57,"original_line":57,"side":"LEFT","original_position":4,"position":4,"subject_type":"line"},{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299193","pull_request_review_id":1739975935,"id":1399299193,"node_id":"PRRC_kwDOHO5Jtc5TZ5x5","diff_hunk":"@@ + -54,14 +54,5 @@\n def div4(x, y):\n return x / y\n \n-def add5(x, y):\n- return + x + y\n-\n-def sub5(x, y):","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"The + function `sub5` has been removed. Please ensure this function is not needed + elsewhere or replaced by a similar functionality.","created_at":"2023-11-20T14:39:23Z","updated_at":"2023-11-20T14:39:24Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399299193","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299193"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399299193"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299193/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":60,"original_line":60,"side":"LEFT","original_position":7,"position":7,"subject_type":"line"},{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299195","pull_request_review_id":1739975935,"id":1399299195,"node_id":"PRRC_kwDOHO5Jtc5TZ5x7","diff_hunk":"@@ + -54,14 +54,5 @@\n def div4(x, y):\n return x / y\n \n-def add5(x, y):\n- return + x + y\n-\n-def sub5(x, y):\n- return x - y\n-\n-def mul5(x, y):","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"The + function `mul5` has been removed. Please ensure this function is not needed + elsewhere or replaced by a similar functionality.","created_at":"2023-11-20T14:39:24Z","updated_at":"2023-11-20T14:39:24Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399299195","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299195"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399299195"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299195/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":63,"original_line":63,"side":"LEFT","original_position":10,"position":10,"subject_type":"line"},{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299201","pull_request_review_id":1739975935,"id":1399299201,"node_id":"PRRC_kwDOHO5Jtc5TZ5yB","diff_hunk":"@@ + -54,14 +54,5 @@\n def div4(x, y):\n return x / y\n \n-def add5(x, y):\n- return + x + y\n-\n-def sub5(x, y):\n- return x - y\n-\n-def mul5(x, y):\n- return + x * y\n-\n-def div5(x, y):","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"The + function `div5` has been removed. Please ensure this function is not needed + elsewhere or replaced by a similar functionality.","created_at":"2023-11-20T14:39:24Z","updated_at":"2023-11-20T14:39:24Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399299201","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299201"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399299201"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299201/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":66,"original_line":66,"side":"LEFT","original_position":13,"position":13,"subject_type":"line"},{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299203","pull_request_review_id":1739975935,"id":1399299203,"node_id":"PRRC_kwDOHO5Jtc5TZ5yD","diff_hunk":"@@ + -54,14 +54,5 @@\n def div4(x, y):\n return x / y\n \n-def add5(x, y):\n- return + x + y\n-\n-def sub5(x, y):\n- return x - y\n-\n-def mul5(x, y):\n- return + x * y\n-\n-def div5(x, y):\n- return x / y\n+def testing(x, y):","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"The + new `testing` function introduced gives the modulus of x and y. Make sure to + handle the scenario of a divide by zero error when y equals to zero.","created_at":"2023-11-20T14:39:24Z","updated_at":"2023-11-20T14:39:24Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399299203","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299203"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399299203"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299203/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":57,"original_line":57,"side":"RIGHT","original_position":15,"position":15,"subject_type":"line"},{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299206","pull_request_review_id":1739975935,"id":1399299206,"node_id":"PRRC_kwDOHO5Jtc5TZ5yG","diff_hunk":"@@ + -54,14 +54,5 @@\n def div4(x, y):\n return x / y\n \n-def add5(x, y):\n- return + x + y\n-\n-def sub5(x, y):\n- return x - y\n-\n-def mul5(x, y):\n- return + x * y\n-\n-def div5(x, y):\n- return x / y\n+def testing(x, y):\n+ return + x % y","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"The + new `testing` function does not handle floating point numbers as expected. The + modulus operator returns a floating-point result instead of rounding down to + the nearest whole number. Make sure to handle this as per the requirements.","created_at":"2023-11-20T14:39:24Z","updated_at":"2023-11-20T14:39:24Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399299206","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299206"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399299206"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399299206/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":58,"original_line":58,"side":"RIGHT","original_position":16,"position":16,"subject_type":"line"},{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399316016","pull_request_review_id":1740003613,"id":1399316016,"node_id":"PRRC_kwDOHO5Jtc5TZ94w","diff_hunk":"@@ + -54,14 +54,5 @@ def mul4(x, y):\n def div4(x, y):\n return x / y\n \n-def + add5(x, y):","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"Check + if there is any code that is calling the add5 function before removing it. If + there is, make sure those instances are handled appropriately.","created_at":"2023-11-20T14:51:31Z","updated_at":"2023-11-20T14:51:32Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399316016","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399316016"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399316016"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399316016/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":57,"original_line":57,"side":"LEFT","original_position":4,"position":4,"subject_type":"line"},{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399316021","pull_request_review_id":1740003613,"id":1399316021,"node_id":"PRRC_kwDOHO5Jtc5TZ941","diff_hunk":"@@ + -54,14 +54,5 @@\n def div4(x, y):\n return x / y\n \n-def add5(x, y):\n- return + x + y\n-\n-def sub5(x, y):","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"Ensure + that there are no dependencies on the sub5 function before deleting it. Analyze + the impact of this deletion before proceeding.","created_at":"2023-11-20T14:51:31Z","updated_at":"2023-11-20T14:51:32Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399316021","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399316021"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399316021"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399316021/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":60,"original_line":60,"side":"LEFT","original_position":7,"position":7,"subject_type":"line"},{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399316023","pull_request_review_id":1740003613,"id":1399316023,"node_id":"PRRC_kwDOHO5Jtc5TZ943","diff_hunk":"@@ + -54,14 +54,5 @@\n def div4(x, y):\n return x / y\n \n-def add5(x, y):\n- return + x + y\n-\n-def sub5(x, y):\n- return x - y\n-\n-def mul5(x, y):","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"Removal + of the mul5 function might impact existing functionality. If there are dependencies, + ensure that they are properly handled.","created_at":"2023-11-20T14:51:31Z","updated_at":"2023-11-20T14:51:32Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399316023","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399316023"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399316023"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399316023/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":63,"original_line":63,"side":"LEFT","original_position":10,"position":10,"subject_type":"line"},{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399316026","pull_request_review_id":1740003613,"id":1399316026,"node_id":"PRRC_kwDOHO5Jtc5TZ946","diff_hunk":"@@ + -54,14 +54,5 @@\n def div4(x, y):\n return x / y\n \n-def add5(x, y):\n- return + x + y\n-\n-def sub5(x, y):\n- return x - y\n-\n-def mul5(x, y):\n- return + x * y\n-\n-def div5(x, y):","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"Removal + of the div5 function could impact parts of your program that rely on it. Re-evaluate + to confirm this is the right course.","created_at":"2023-11-20T14:51:31Z","updated_at":"2023-11-20T14:51:32Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399316026","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399316026"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399316026"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399316026/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":66,"original_line":66,"side":"LEFT","original_position":13,"position":13,"subject_type":"line"},{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399316027","pull_request_review_id":1740003613,"id":1399316027,"node_id":"PRRC_kwDOHO5Jtc5TZ947","diff_hunk":"@@ + -54,14 +54,5 @@\n def div4(x, y):\n return x / y\n \n-def add5(x, y):\n- return + x + y\n-\n-def sub5(x, y):\n- return x - y\n-\n-def mul5(x, y):\n- return + x * y\n-\n-def div5(x, y):\n- return x / y\n+def testing(x, y):","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"This + new function called ''testing'' performs a modulo operation. Consider renaming + the function to something more descriptive, e.g., mod or modulo.","created_at":"2023-11-20T14:51:32Z","updated_at":"2023-11-20T14:51:32Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399316027","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399316027"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399316027"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399316027/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":57,"original_line":57,"side":"RIGHT","original_position":15,"position":15,"subject_type":"line"},{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319213","pull_request_review_id":1740008775,"id":1399319213,"node_id":"PRRC_kwDOHO5Jtc5TZ-qt","diff_hunk":"@@ + -54,14 +54,5 @@ def mul4(x, y):\n def div4(x, y):\n return x / y\n \n-def + add5(x, y):","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"It + looks like you''ve removed the add5 method. If this is being used elsewhere + in the codebase then its removal could cause issues. Please ensure that this + method isn''t being used elsewhere before removal.","created_at":"2023-11-20T14:53:54Z","updated_at":"2023-11-20T14:53:55Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399319213","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319213"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399319213"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319213/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":57,"original_line":57,"side":"LEFT","original_position":4,"position":4,"subject_type":"line"},{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319214","pull_request_review_id":1740008775,"id":1399319214,"node_id":"PRRC_kwDOHO5Jtc5TZ-qu","diff_hunk":"@@ + -54,14 +54,5 @@ def mul4(x, y):\n def div4(x, y):\n return x / y\n \n-def + add5(x, y):\n- return x + y\n-\n-def sub5(x, y):","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"You''ve + removed the sub5 method. Like with the add5 method, make sure that it''s not + being used in other places in the codebase which could cause runtime issues.","created_at":"2023-11-20T14:53:54Z","updated_at":"2023-11-20T14:53:55Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399319214","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319214"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399319214"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319214/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":60,"original_line":60,"side":"LEFT","original_position":7,"position":7,"subject_type":"line"},{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319215","pull_request_review_id":1740008775,"id":1399319215,"node_id":"PRRC_kwDOHO5Jtc5TZ-qv","diff_hunk":"@@ + -54,14 +54,5 @@ def mul4(x, y):\n def div4(x, y):\n return x / y\n \n-def + add5(x, y):\n- return x + y\n-\n-def sub5(x, y):\n- return x - y\n-\n-def + mul5(x, y):","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"The + mul5 method has been removed. If it is used elsewhere, this could cause potential + problems. Validate it before deleting.","created_at":"2023-11-20T14:53:54Z","updated_at":"2023-11-20T14:53:55Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399319215","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319215"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399319215"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319215/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":63,"original_line":63,"side":"LEFT","original_position":10,"position":10,"subject_type":"line"},{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319219","pull_request_review_id":1740008775,"id":1399319219,"node_id":"PRRC_kwDOHO5Jtc5TZ-qz","diff_hunk":"@@ + -54,14 +54,5 @@ def mul4(x, y):\n def div4(x, y):\n return x / y\n \n-def + add5(x, y):\n- return x + y\n-\n-def sub5(x, y):\n- return x - y\n-\n-def + mul5(x, y):\n- return x * y\n-\n-def div5(x, y):","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"You''ve + also removed div5, again ensure it''s not being used anywhere else to prevent + bugs and exceptions.","created_at":"2023-11-20T14:53:54Z","updated_at":"2023-11-20T14:53:55Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399319219","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319219"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399319219"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319219/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":66,"original_line":66,"side":"LEFT","original_position":13,"position":13,"subject_type":"line"},{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319220","pull_request_review_id":1740008775,"id":1399319220,"node_id":"PRRC_kwDOHO5Jtc5TZ-q0","diff_hunk":"@@ + -54,14 +54,5 @@ def mul4(x, y):\n def div4(x, y):\n return x / y\n \n-def + add5(x, y):\n- return x + y\n-\n-def sub5(x, y):\n- return x - y\n-\n-def + mul5(x, y):\n- return x * y\n-\n-def div5(x, y):\n- return x / y\n+def + testing(x, y):","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"This + new ''testing'' function''s name is not descriptive of its functionality. It + looks like it''s performing a modulus operation, not testing. You should name + this function appropriately.","created_at":"2023-11-20T14:53:55Z","updated_at":"2023-11-20T14:53:55Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399319220","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319220"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399319220"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319220/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":57,"original_line":57,"side":"RIGHT","original_position":15,"position":15,"subject_type":"line"}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 20 Nov 2023 14:57:34 GMT + ETag: + - W/"34bf2a70870a2944dfb55a2babeb2299d1d4add01673eb7b2200f5887a35b4f8" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FA79:2E86:93B1E2:1320300:655B73DD + X-OAuth-Scopes: + - '' + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4985' + X-RateLimit-Reset: + - '1700494278' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '15' + X-XSS-Protection: + - '0' + x-accepted-github-permissions: + - pull_requests=read + x-github-api-version-selected: + - '2022-11-28' + x-oauth-client-id: + - Iv1.88e0c58abd4e2e45 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/scott-codecov/codecov-test/pulls/40/comments?per_page=100&page=2 + response: + content: '[]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '2' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 20 Nov 2023 14:57:34 GMT + ETag: + - '"75cdf323b1b22c1c1c5eb1332cd50eaeee03e9a548fea0444f4061d44b44dc0d"' + Link: + - ; + rel="prev", ; + rel="last", ; + rel="first" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FA79:2E86:93B279:1320448:655B73DE + X-OAuth-Scopes: + - '' + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4984' + X-RateLimit-Reset: + - '1700494278' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '16' + X-XSS-Protection: + - '0' + x-accepted-github-permissions: + - pull_requests=read + x-github-api-version-selected: + - '2022-11-28' + x-oauth-client-id: + - Iv1.88e0c58abd4e2e45 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"body": "Upon removal of add5, sub5, mul5, div5 functions from foo.py, + remember to update all call sites that reference these functions."}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '139' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: PATCH + uri: https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319213 + response: + content: '{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319213","pull_request_review_id":1740008775,"id":1399319213,"node_id":"PRRC_kwDOHO5Jtc5TZ-qt","diff_hunk":"@@ + -54,14 +54,5 @@ def mul4(x, y):\n def div4(x, y):\n return x / y\n \n-def + add5(x, y):","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"Upon + removal of add5, sub5, mul5, div5 functions from foo.py, remember to update + all call sites that reference these functions.","created_at":"2023-11-20T14:53:54Z","updated_at":"2023-11-20T14:57:34Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399319213","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319213"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399319213"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319213/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":57,"original_line":57,"side":"LEFT","original_position":4,"position":4,"subject_type":"line"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 20 Nov 2023 14:57:35 GMT + ETag: + - W/"ee80dc073626d7b6a7c7469e1fe400e969fafccce776eae5b2531d2245a88be8" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FA7A:4171:9023DB:12ACDC0:655B73DE + X-OAuth-Scopes: + - '' + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4983' + X-RateLimit-Reset: + - '1700494278' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '17' + X-XSS-Protection: + - '0' + x-accepted-github-permissions: + - pull_requests=write + x-github-api-version-selected: + - '2022-11-28' + x-oauth-client-id: + - Iv1.88e0c58abd4e2e45 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"body": "The new ''testing'' function uses the modulus operation. The + name doesn''t reflect this, consider renaming it to ''modulus'' or a name that + better reflects its functionality."}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '181' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: PATCH + uri: https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319220 + response: + content: '{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319220","pull_request_review_id":1740008775,"id":1399319220,"node_id":"PRRC_kwDOHO5Jtc5TZ-q0","diff_hunk":"@@ + -54,14 +54,5 @@ def mul4(x, y):\n def div4(x, y):\n return x / y\n \n-def + add5(x, y):\n- return x + y\n-\n-def sub5(x, y):\n- return x - y\n-\n-def + mul5(x, y):\n- return x * y\n-\n-def div5(x, y):\n- return x / y\n+def + testing(x, y):","path":"main/foo.py","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862","original_commit_id":"b607bb0e17e1b8d8699272a26e32986a933f9946","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"The + new ''testing'' function uses the modulus operation. The name doesn''t reflect + this, consider renaming it to ''modulus'' or a name that better reflects its + functionality.","created_at":"2023-11-20T14:53:55Z","updated_at":"2023-11-20T14:57:35Z","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399319220","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319220"},"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#discussion_r1399319220"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"reactions":{"url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/comments/1399319220/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":57,"original_line":57,"side":"RIGHT","original_position":15,"position":15,"subject_type":"line"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 20 Nov 2023 14:57:35 GMT + ETag: + - W/"8e9b3e8922c3274894baa8442d145f1321a199f55ba5dd972c351a3bfa6ecacc" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FA7B:9369:20108A1:4335182:655B73DF + X-OAuth-Scopes: + - '' + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4982' + X-RateLimit-Reset: + - '1700494278' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '18' + X-XSS-Protection: + - '0' + x-accepted-github-permissions: + - pull_requests=write + x-github-api-version-selected: + - '2022-11-28' + x-oauth-client-id: + - Iv1.88e0c58abd4e2e45 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"commit_id": "5c64a5143951193dde7b14c14611eebe1025f862", "body": "CodecovAI + submitted a new review for 5c64a5143951193dde7b14c14611eebe1025f862", "event": + "COMMENT", "comments": [{"path": "main/bar.py", "position": 9, "body": "add7 + function in bar.py is identically implemented as add6. Consider removing redundant + code."}, {"path": "main/bar.py", "position": 8, "body": "The new functions you''ve + added named add7, sub7, mul7, div7 are identical to some existing functions. + It is recommended to avoid duplicate functions, perhaps by creating a utility + function if the implementation across these functions is expected to remain + identical."}]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '643' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/scott-codecov/codecov-test/pulls/40/reviews + response: + content: '{"id":1740017976,"node_id":"PRR_kwDOHO5Jtc5ntpE4","user":{"login":"scott-codecov","id":103445133,"node_id":"U_kgDOBipyjQ","avatar_url":"https://avatars.githubusercontent.com/u/103445133?u=1ea5f79283a26325f56e7cfa9eaca5cff3d538a4&v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov","html_url":"https://github.com/scott-codecov","followers_url":"https://api.github.com/users/scott-codecov/followers","following_url":"https://api.github.com/users/scott-codecov/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov/orgs","repos_url":"https://api.github.com/users/scott-codecov/repos","events_url":"https://api.github.com/users/scott-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov/received_events","type":"User","site_admin":false},"body":"CodecovAI + submitted a new review for 5c64a5143951193dde7b14c14611eebe1025f862","state":"COMMENTED","html_url":"https://github.com/scott-codecov/codecov-test/pull/40#pullrequestreview-1740017976","pull_request_url":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40","author_association":"OWNER","_links":{"html":{"href":"https://github.com/scott-codecov/codecov-test/pull/40#pullrequestreview-1740017976"},"pull_request":{"href":"https://api.github.com/repos/scott-codecov/codecov-test/pulls/40"}},"submitted_at":"2023-11-20T14:57:36Z","commit_id":"5c64a5143951193dde7b14c14611eebe1025f862"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 20 Nov 2023 14:57:37 GMT + ETag: + - W/"a4514ce707ba155eb3dbfeaa0d9ad4da4ff272e8c63a4b61bad6cffd4c1b9f3a" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FA7C:7CD2:981A2A:13ABB2E:655B73E0 + X-OAuth-Scopes: + - '' + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4981' + X-RateLimit-Reset: + - '1700494278' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '19' + X-XSS-Protection: + - '0' + x-accepted-github-permissions: + - pull_requests=write + x-github-api-version-selected: + - '2022-11-28' + x-oauth-client-id: + - Iv1.88e0c58abd4e2e45 + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/apps/worker/services/tests/cassetes/test_bots/TestBotsService/test_get_owner_appropriate_bot_token_with_user_with_integration_bot_using_it.yaml b/apps/worker/services/tests/cassetes/test_bots/TestBotsService/test_get_owner_appropriate_bot_token_with_user_with_integration_bot_using_it.yaml new file mode 100644 index 0000000000..ad71c4404f --- /dev/null +++ b/apps/worker/services/tests/cassetes/test_bots/TestBotsService/test_get_owner_appropriate_bot_token_with_user_with_integration_bot_using_it.yaml @@ -0,0 +1,62 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/vnd.github.machine-man-preview+json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - Codecov + method: POST + uri: https://api.github.com/app/installations/1654873/access_tokens + response: + body: + string: '{"token":"v1.test50wm4qyel2pbtpbusklcarg7c2etcbunnswp","expires_at":"2019-08-26T01:25:56Z","permissions":{"checks":"write","pull_requests":"write","statuses":"write","administration":"read","contents":"read","issues":"read","metadata":"read"},"repository_selection":"selected"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - public, max-age=60, s-maxage=60 + Content-Length: + - '277' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 26 Aug 2019 00:25:57 GMT + ETag: + - '"d5bbd7f7363c549c2faa22e8f4419077"' + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.machine-man-preview; format=json + X-GitHub-Request-Id: + - 3B5C:44FD:19779AA:3C3CF0D:5D632714 + X-XSS-Protection: + - 1; mode=block + status: + code: 201 + message: Created +version: 1 diff --git a/apps/worker/services/tests/cassetes/test_bots/TestBotsService/test_get_repo_appropriate_bot_token_repo_with_user_with_integration_bot_using_it.yaml b/apps/worker/services/tests/cassetes/test_bots/TestBotsService/test_get_repo_appropriate_bot_token_repo_with_user_with_integration_bot_using_it.yaml new file mode 100644 index 0000000000..ad71c4404f --- /dev/null +++ b/apps/worker/services/tests/cassetes/test_bots/TestBotsService/test_get_repo_appropriate_bot_token_repo_with_user_with_integration_bot_using_it.yaml @@ -0,0 +1,62 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/vnd.github.machine-man-preview+json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - Codecov + method: POST + uri: https://api.github.com/app/installations/1654873/access_tokens + response: + body: + string: '{"token":"v1.test50wm4qyel2pbtpbusklcarg7c2etcbunnswp","expires_at":"2019-08-26T01:25:56Z","permissions":{"checks":"write","pull_requests":"write","statuses":"write","administration":"read","contents":"read","issues":"read","metadata":"read"},"repository_selection":"selected"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - public, max-age=60, s-maxage=60 + Content-Length: + - '277' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 26 Aug 2019 00:25:57 GMT + ETag: + - '"d5bbd7f7363c549c2faa22e8f4419077"' + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.machine-man-preview; format=json + X-GitHub-Request-Id: + - 3B5C:44FD:19779AA:3C3CF0D:5D632714 + X-XSS-Protection: + - 1; mode=block + status: + code: 201 + message: Created +version: 1 diff --git a/apps/worker/services/tests/cassetes/test_bots/TestBotsService/testget_owner_appropriate_bot_token_with_user_with_integration_bot_using_it.yaml b/apps/worker/services/tests/cassetes/test_bots/TestBotsService/testget_owner_appropriate_bot_token_with_user_with_integration_bot_using_it.yaml new file mode 100644 index 0000000000..32968cbd43 --- /dev/null +++ b/apps/worker/services/tests/cassetes/test_bots/TestBotsService/testget_owner_appropriate_bot_token_with_user_with_integration_bot_using_it.yaml @@ -0,0 +1,57 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/vnd.github.machine-man-preview+json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - Codecov + method: POST + uri: https://api.github.com/app/installations/1654873/access_tokens + response: + body: + string: '{"message":"A JSON web token could not be decoded","documentation_url":"https://docs.github.com/rest"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Length: + - '102' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 15 May 2024 08:50:34 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=machine-man-preview; format=json + X-GitHub-Request-Id: + - D126:1EF29A:631E2B1:63AF5F7:6644775A + X-XSS-Protection: + - '0' + status: + code: 401 + message: Unauthorized +version: 1 diff --git a/apps/worker/services/tests/cassetes/test_bots/TestBotsService/testget_repo_appropriate_bot_token_repo_with_user_with_integration_bot_using_it.yaml b/apps/worker/services/tests/cassetes/test_bots/TestBotsService/testget_repo_appropriate_bot_token_repo_with_user_with_integration_bot_using_it.yaml new file mode 100644 index 0000000000..58dc5d396e --- /dev/null +++ b/apps/worker/services/tests/cassetes/test_bots/TestBotsService/testget_repo_appropriate_bot_token_repo_with_user_with_integration_bot_using_it.yaml @@ -0,0 +1,57 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/vnd.github.machine-man-preview+json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - Codecov + method: POST + uri: https://api.github.com/app/installations/1654873/access_tokens + response: + body: + string: '{"message":"A JSON web token could not be decoded","documentation_url":"https://docs.github.com/rest"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Length: + - '102' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 15 May 2024 08:50:34 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=machine-man-preview; format=json + X-GitHub-Request-Id: + - D124:231513:6173EA3:6202FCF:6644775A + X-XSS-Protection: + - '0' + status: + code: 401 + message: Unauthorized +version: 1 diff --git a/apps/worker/services/tests/integration/cassetes/test_bots/TestRepositoryServiceIntegration/test_get_repo_appropriate_bot_token_bad_data.yaml b/apps/worker/services/tests/integration/cassetes/test_bots/TestRepositoryServiceIntegration/test_get_repo_appropriate_bot_token_bad_data.yaml new file mode 100644 index 0000000000..162e8abae6 --- /dev/null +++ b/apps/worker/services/tests/integration/cassetes/test_bots/TestRepositoryServiceIntegration/test_get_repo_appropriate_bot_token_bad_data.yaml @@ -0,0 +1,58 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/vnd.github.machine-man-preview+json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - Codecov + method: POST + uri: https://api.github.com/app/installations/5944641/access_tokens + response: + body: + string: '{"message":"Integration must generate a public key","documentation_url":"https://developer.github.com/v3"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Content-Length: + - '106' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 31 Mar 2020 21:28:13 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 401 Unauthorized + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.machine-man-preview; format=json + X-GitHub-Request-Id: + - E466:7A72:1F788:2900F:5E83B5ED + X-XSS-Protection: + - 1; mode=block + status: + code: 401 + message: Unauthorized +version: 1 diff --git a/apps/worker/services/tests/integration/cassetes/test_bots/TestRepositoryServiceIntegration/test_get_repo_appropriate_bot_token_non_existing_integration.yaml b/apps/worker/services/tests/integration/cassetes/test_bots/TestRepositoryServiceIntegration/test_get_repo_appropriate_bot_token_non_existing_integration.yaml new file mode 100644 index 0000000000..68ea5e9eb8 --- /dev/null +++ b/apps/worker/services/tests/integration/cassetes/test_bots/TestRepositoryServiceIntegration/test_get_repo_appropriate_bot_token_non_existing_integration.yaml @@ -0,0 +1,61 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/vnd.github.machine-man-preview+json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - Codecov + method: POST + uri: https://api.github.com/app/installations/5944641/access_tokens + response: + body: + string: !!binary | + H4sIAAAAAAAAAx3MMQ7CMAwAwK8gszb1wNYHMPIFZBKrjUjsKHbaAfF3UOeT7gOVzWhlWOChfrnr + kAQTJI2jsjh5VnmOXv6+uTdbEBPvXLRxn9fs23jNUSvuN6TWDK+xMzkHCsJHyGJOpZxLcH2zwPcH + 7H7LEnMAAAA= + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 31 Mar 2020 21:26:16 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 404 Not Found + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.machine-man-preview; format=json + X-GitHub-Request-Id: + - E456:7A74:9D6C2:D3440:5E83B578 + X-XSS-Protection: + - 1; mode=block + status: + code: 404 + message: Not Found +version: 1 diff --git a/apps/worker/services/tests/integration/cassetes/test_bots/TestRepositoryServiceIntegration/test_get_token_type_mapping_bad_data.yaml b/apps/worker/services/tests/integration/cassetes/test_bots/TestRepositoryServiceIntegration/test_get_token_type_mapping_bad_data.yaml new file mode 100644 index 0000000000..22c41b0556 --- /dev/null +++ b/apps/worker/services/tests/integration/cassetes/test_bots/TestRepositoryServiceIntegration/test_get_token_type_mapping_bad_data.yaml @@ -0,0 +1,57 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/vnd.github.machine-man-preview+json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - Codecov + method: POST + uri: https://api.github.com/app/installations/5944641/access_tokens + response: + body: + string: '{"message":"Integration must generate a public key","documentation_url":"https://docs.github.com/rest"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Length: + - '103' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 15 May 2024 08:58:07 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=machine-man-preview; format=json + X-GitHub-Request-Id: + - D18C:26966D:614450E:61D4561:6644791F + X-XSS-Protection: + - '0' + status: + code: 401 + message: Unauthorized +version: 1 diff --git a/apps/worker/services/tests/integration/cassetes/test_repository_service/TestRepositoryServiceIntegration/test_get_repo_provider_service_bitbucket.yaml b/apps/worker/services/tests/integration/cassetes/test_repository_service/TestRepositoryServiceIntegration/test_get_repo_provider_service_bitbucket.yaml new file mode 100644 index 0000000000..5f677ed68a --- /dev/null +++ b/apps/worker/services/tests/integration/cassetes/test_repository_service/TestRepositoryServiceIntegration/test_get_repo_provider_service_bitbucket.yaml @@ -0,0 +1,95 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/commit/6895b64?oauth_consumer_key=testzdcviyi3x7f8h0&oauth_token=H6scSkq7rKZDXtDqe4&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1569615857&oauth_nonce=b98bd6399cff4029ae7cadeef4d7ecd2&oauth_version=1.0&oauth_signature=QoXcCFU5p8sc0mHtAypAkqXi1wQ%3D + response: + content: '{"rendered": {"message": {"raw": "Adding ''include'' term if multiple + sources\n\nbased on a support ticket around multiple sources\r\n\r\nhttps://codecov.freshdesk.com/a/tickets/87", + "markup": "markdown", "html": "

    Adding ''include'' term if multiple sources

    \n

    based + on a support ticket around multiple sources

    \n

    https://codecov.freshdesk.com/a/tickets/87

    ", + "type": "rendered"}}, "hash": "6895b6479dbe12b5cb3baa02416c6343ddb888b4", "repository": + {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6895b6479dbe12b5cb3baa02416c6343ddb888b4"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6895b6479dbe12b5cb3baa02416c6343ddb888b4/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/6895b6479dbe12b5cb3baa02416c6343ddb888b4"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/6895b6479dbe12b5cb3baa02416c6343ddb888b4"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6895b6479dbe12b5cb3baa02416c6343ddb888b4/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6895b6479dbe12b5cb3baa02416c6343ddb888b4/statuses"}}, + "author": {"raw": "Jerrod ", "type": "author"}, "summary": + {"raw": "Adding ''include'' term if multiple sources\n\nbased on a support ticket + around multiple sources\r\n\r\nhttps://codecov.freshdesk.com/a/tickets/87", + "markup": "markdown", "html": "

    Adding ''include'' term if multiple sources

    \n

    based + on a support ticket around multiple sources

    \n

    https://codecov.freshdesk.com/a/tickets/87

    ", + "type": "rendered"}, "participants": [], "parents": [{"hash": "adb252173d2107fad86bcdcbc149884c2dd4c609", + "type": "commit", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/adb252173d2107fad86bcdcbc149884c2dd4c609"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609"}}}], + "date": "2018-07-09T23:39:20+00:00", "message": "Adding ''include'' term if + multiple sources\n\nbased on a support ticket around multiple sources\r\n\r\nhttps://codecov.freshdesk.com/a/tickets/87", + "type": "commit"}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 27 Sep 2019 20:24:18 GMT + Etag: + - '"gz[30b44719f3163e6c11b3ad93a6deddd8]"' + Last-Modified: + - Thu, 26 Sep 2019 00:58:27 GMT + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Reads-Before-Write-From: + - default + X-Render-Time: + - '0.431403160095' + X-Request-Count: + - '76' + X-Served-By: + - app-144 + X-Static-Version: + - ca263699922c + X-Version: + - ca263699922c + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/commit/6895b64?oauth_consumer_key=testzdcviyi3x7f8h0&oauth_token=H6scSkq7rKZDXtDqe4&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1569615857&oauth_nonce=b98bd6399cff4029ae7cadeef4d7ecd2&oauth_version=1.0&oauth_signature=QoXcCFU5p8sc0mHtAypAkqXi1wQ%3D +version: 1 diff --git a/apps/worker/services/tests/integration/cassetes/test_repository_service/TestRepositoryServiceIntegration/test_get_repo_provider_service_github.yaml b/apps/worker/services/tests/integration/cassetes/test_repository_service/TestRepositoryServiceIntegration/test_get_repo_provider_service_github.yaml new file mode 100644 index 0000000000..65ff7e07c4 --- /dev/null +++ b/apps/worker/services/tests/integration/cassetes/test_repository_service/TestRepositoryServiceIntegration/test_get_repo_provider_service_github.yaml @@ -0,0 +1,92 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b64 + response: + content: '{"sha":"6895b6479dbe12b5cb3baa02416c6343ddb888b4","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjY4OTViNjQ3OWRiZTEyYjVjYjNiYWEwMjQxNmM2MzQzZGRiODg4YjQ=","commit":{"author":{"name":"Jerrod","email":"jerrod@fundersclub.com","date":"2018-07-09T23:39:20Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2018-07-09T23:39:20Z"},"message":"Adding + ''include'' term if multiple sources\n\nbased on a support ticket around multiple + sources\r\n\r\nhttps://codecov.freshdesk.com/a/tickets/87","tree":{"sha":"3c47e2b9d9791503b56f0e4f78e76b9d061ad529","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3c47e2b9d9791503b56f0e4f78e76b9d061ad529"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJbQ/IoCRBK7hj4Ov3rIwAAdHIIAGm5AdlM8E0E7TyFKWgwPpjO\nsxiQswFXWosTZnJAn2NN/JF5aNqxUFLa9mo7Z+jztQuxrWsAFQsNFHf/t90iZi4w\ne0CkIHJdI8ukcae5/3eP+9h8GyqEq/RcvxYtvW6zYkWAK3Pyqwrs+qwH1MuLsl6E\n02fgD6T99Pq2V+3S1+dfgU6ot4IrMwT7aR+u9fCM8G4tF4y/5znIzuke6amVt52S\nUfjnHOHbDxdD4Mkxn8107zX1XmQ4BEzhh1kjTVd3Mean6ye7xsFxFGYHA5Zd1iyM\nCsmW5waqonRf03m1bQ9pYleufcwpr72iARLiBFhTOcAF6vpdoshO1qmTtsweFno=\n=vKnQ\n-----END + PGP SIGNATURE-----\n","payload":"tree 3c47e2b9d9791503b56f0e4f78e76b9d061ad529\nparent + adb252173d2107fad86bcdcbc149884c2dd4c609\nauthor Jerrod + 1531179560 -0700\ncommitter GitHub 1531179560 -0700\n\nAdding + ''include'' term if multiple sources\n\nbased on a support ticket around multiple + sources\r\n\r\nhttps://codecov.freshdesk.com/a/tickets/87"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6895b6479dbe12b5cb3baa02416c6343ddb888b4","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4/comments","author":null,"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars3.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"adb252173d2107fad86bcdcbc149884c2dd4c609","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609","html_url":"https://github.com/ThiagoCodecov/example-python/commit/adb252173d2107fad86bcdcbc149884c2dd4c609"}],"stats":{"total":9,"additions":8,"deletions":1},"files":[{"sha":"1fbfc366bd98e0c8df4fd297061a420b674857f4","filename":"README.rst","status":"modified","additions":8,"deletions":1,"changes":9,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/6895b6479dbe12b5cb3baa02416c6343ddb888b4/README.rst","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/6895b6479dbe12b5cb3baa02416c6343ddb888b4/README.rst","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.rst?ref=6895b6479dbe12b5cb3baa02416c6343ddb888b4","patch":"@@ + -47,12 +47,19 @@ Below are some examples on how to include coverage tracking + during your tests. C\n \n You may need to configure a ``.coveragerc`` file. + Learn more `here `_. + Start with this `generic .coveragerc `_ + for example.\n \n-We highly suggest adding `source` to your ``.coveragerc`` + which solves a number of issues collecting coverage.\n+We highly suggest adding + `source` to your ``.coveragerc``, which solves a number of issues collecting + coverage.\n \n .. code-block:: ini\n \n [run]\n source=your_package_name\n+ \n+If + there are multiple sources, you instead should add ''include'' to your ``.coveragerc``\n+\n+.. + code-block:: ini\n+\n+ [run]\n+ include=your_package_name/*\n \n unittests\n + ---------"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 27 Sep 2019 20:17:39 GMT + Etag: + - W/"34680c6edd4947802517db4ac91cbc6f" + Last-Modified: + - Mon, 09 Jul 2018 23:39:20 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 464F:069D:1DF8E:28829:5D8E6E63 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4998' + X-Ratelimit-Reset: + - '1569618944' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b64 +version: 1 diff --git a/apps/worker/services/tests/integration/cassetes/test_repository_service/TestRepositoryServiceIntegration/test_get_repo_provider_service_gitlab.yaml b/apps/worker/services/tests/integration/cassetes/test_repository_service/TestRepositoryServiceIntegration/test_get_repo_provider_service_gitlab.yaml new file mode 100644 index 0000000000..0b116520f2 --- /dev/null +++ b/apps/worker/services/tests/integration/cassetes/test_repository_service/TestRepositoryServiceIntegration/test_get_repo_provider_service_gitlab.yaml @@ -0,0 +1,137 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/commits/0028015f7fa260f5fd68f78c0deffc15183d955e + response: + content: '{"id":"0028015f7fa260f5fd68f78c0deffc15183d955e","short_id":"0028015f","created_at":"2014-10-19T14:32:33.000Z","parent_ids":["5716de23b27020419d1a40dd93b469c041a1eeef"],"title":"added + large file","message":"added large file\n","author_name":"stevepeak","author_email":"steve@stevepeak.net","authored_date":"2014-10-19T14:32:33.000Z","committer_name":"stevepeak","committer_email":"steve@stevepeak.net","committed_date":"2014-10-19T14:32:33.000Z","stats":{"additions":816,"deletions":0,"total":816},"status":"success","last_pipeline":{"id":558130,"sha":"0028015f7fa260f5fd68f78c0deffc15183d955e","ref":null,"status":"success","web_url":"https://gitlab.com/codecov/ci-repo/pipelines/558130"},"project_id":187725}' + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Connection: + - close + Content-Length: + - '710' + Content-Type: + - application/json + Date: + - Fri, 27 Sep 2019 20:25:56 GMT + Etag: + - W/"14982171f02402a9f16a64fd67c7a652" + Gitlab-Lb: + - fe-10-lb-gprd + Gitlab-Sv: + - localhost + Ratelimit-Limit: + - '600' + Ratelimit-Observed: + - '2' + Ratelimit-Remaining: + - '598' + Ratelimit-Reset: + - '1569616016' + Ratelimit-Resettime: + - Fri, 27 Sep 2019 20:26:56 GMT + Referrer-Policy: + - strict-origin-when-cross-origin + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Request-Id: + - 8gMdbsH0tv5 + X-Runtime: + - '0.062895' + status: + code: 200 + message: OK + status_code: 200 + url: https://gitlab.com/api/v4/projects/187725/repository/commits/0028015f7fa260f5fd68f78c0deffc15183d955e +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/users?search=steve%40stevepeak.net + response: + content: '[{"id":109479,"name":"Steve Peak","username":"stevepeak","state":"active","avatar_url":"https://secure.gravatar.com/avatar/3712e9b9aee2ce5090aae58c2495cdee?s=80\u0026d=identicon","web_url":"https://gitlab.com/stevepeak"}]' + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Connection: + - close + Content-Length: + - '221' + Content-Type: + - application/json + Date: + - Fri, 27 Sep 2019 20:25:57 GMT + Etag: + - W/"741342810fb814ea719ddb0b3c927b6d" + Gitlab-Lb: + - fe-09-lb-gprd + Gitlab-Sv: + - localhost + Link: + - ; + rel="first", ; + rel="last" + Ratelimit-Limit: + - '600' + Ratelimit-Observed: + - '3' + Ratelimit-Remaining: + - '597' + Ratelimit-Reset: + - '1569616017' + Ratelimit-Resettime: + - Fri, 27 Sep 2019 20:26:57 GMT + Referrer-Policy: + - strict-origin-when-cross-origin + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Next-Page: + - '' + X-Page: + - '1' + X-Per-Page: + - '20' + X-Prev-Page: + - '' + X-Request-Id: + - NPfOv9UCz9 + X-Runtime: + - '0.083548' + X-Total: + - '1' + X-Total-Pages: + - '1' + status: + code: 200 + message: OK + status_code: 200 + url: https://gitlab.com/api/v4/users?search=steve%40stevepeak.net +version: 1 diff --git a/apps/worker/services/tests/integration/test_repository_service.py b/apps/worker/services/tests/integration/test_repository_service.py new file mode 100644 index 0000000000..d6e776b7b7 --- /dev/null +++ b/apps/worker/services/tests/integration/test_repository_service.py @@ -0,0 +1,103 @@ +import pytest + +from database.tests.factories import RepositoryFactory +from services.repository import ( + get_repo_provider_service, +) + + +class TestRepositoryServiceIntegration(object): + @pytest.mark.asyncio + async def test_get_repo_provider_service_github(self, dbsession, codecov_vcr): + repo = RepositoryFactory.create( + owner__unencrypted_oauth_token="testlln8sdeec57lz83oe3l8y9qq4lhqat2f1kzm", + owner__username="ThiagoCodecov", + owner__service="github", + name="example-python", + ) + dbsession.add(repo) + dbsession.flush() + service = get_repo_provider_service(repo) + expected_result = { + "author": { + "id": None, + "username": None, + "email": "jerrod@fundersclub.com", + "name": "Jerrod", + }, + "message": "Adding 'include' term if multiple sources\n\nbased on a support ticket around multiple sources\r\n\r\nhttps://codecov.freshdesk.com/a/tickets/87", + "parents": ["adb252173d2107fad86bcdcbc149884c2dd4c609"], + "commitid": "6895b64", + "timestamp": "2018-07-09T23:39:20Z", + } + + commit = await service.get_commit("6895b64") + assert commit["author"] == expected_result["author"] + assert commit == expected_result + + @pytest.mark.asyncio + async def test_get_repo_provider_service_bitbucket( + self, dbsession, mock_configuration, codecov_vcr + ): + mock_configuration.params["bitbucket"] = { + "client_id": "testzdcviyi3x7f8h0", + "client_secret": "testw35rwjj75gbaervbsmgl13vf39jd", + } + repo = RepositoryFactory.create( + owner__unencrypted_oauth_token="H6scSkq7rKZDXtDqe4:kdTf3NVM9RkUc9rAaDM853j5f32PkBGU", + owner__username="ThiagoCodecov", + owner__service="bitbucket", + name="example-python", + ) + dbsession.add(repo) + dbsession.flush() + service = get_repo_provider_service(repo) + commit = await service.get_commit("6895b64") + expected_result = { + "author": { + "id": None, + "username": None, + "email": "jerrod@fundersclub.com", + "name": "Jerrod", + }, + "message": "Adding 'include' term if multiple sources\n\nbased on a support ticket around multiple sources\r\n\r\nhttps://codecov.freshdesk.com/a/tickets/87", + "parents": ["adb252173d2107fad86bcdcbc149884c2dd4c609"], + "commitid": "6895b64", + "timestamp": "2018-07-09T23:39:20+00:00", + } + assert commit["author"] == expected_result["author"] + assert commit == expected_result + + @pytest.mark.asyncio + async def test_get_repo_provider_service_gitlab( + self, dbsession, mock_configuration, codecov_vcr + ): + mock_configuration.params["bitbucket"] = { + "client_id": "testzdcviyi3x7f8h0", + "client_secret": "testw35rwjj75gbaervbsmgl13vf39jd", + } + repo = RepositoryFactory.create( + owner__unencrypted_oauth_token="test10r65j3084oje16v12yzfuojw4yovzwa18y9txooo716odibjdwk8cn1p42r", + owner__username="stevepeak", + owner__service="gitlab", + name="example-python", + service_id="187725", + ) + dbsession.add(repo) + dbsession.flush() + service = get_repo_provider_service(repo) + commit = await service.get_commit("0028015f7fa260f5fd68f78c0deffc15183d955e") + expected_result = { + "author": { + "id": None, + "username": None, + "email": "steve@stevepeak.net", + "name": "stevepeak", + }, + "message": "added large file\n", + "parents": ["5716de23b27020419d1a40dd93b469c041a1eeef"], + "commitid": "0028015f7fa260f5fd68f78c0deffc15183d955e", + "timestamp": "2014-10-19T14:32:33.000Z", + } + assert commit["author"] == expected_result["author"] + assert commit == expected_result diff --git a/apps/worker/services/tests/test_activation.py b/apps/worker/services/tests/test_activation.py new file mode 100644 index 0000000000..002406a125 --- /dev/null +++ b/apps/worker/services/tests/test_activation.py @@ -0,0 +1,231 @@ +from datetime import datetime + +from database.tests.factories import OwnerFactory +from services.activation import activate_user, get_installation_plan_activated_users + + +class TestActivationServiceTestCase(object): + def test_activate_user_no_seats( + self, request, dbsession, mocker, with_sql_functions + ): + org = OwnerFactory.create( + plan_user_count=0, plan_activated_users=[], plan_auto_activate=True + ) + user = OwnerFactory.create_from_test_request(request) + dbsession.add(org) + dbsession.add(user) + dbsession.flush() + + was_activated = activate_user(dbsession, org.ownerid, user.ownerid) + assert was_activated is False + dbsession.commit() + assert user.ownerid not in org.plan_activated_users + + def test_activate_user_success( + self, request, dbsession, mocker, with_sql_functions + ): + org = OwnerFactory.create( + plan_user_count=1, plan_activated_users=[], plan_auto_activate=True + ) + user = OwnerFactory.create_from_test_request(request) + dbsession.add(org) + dbsession.add(user) + dbsession.flush() + + was_activated = activate_user(dbsession, org.ownerid, user.ownerid) + assert was_activated is True + dbsession.commit() + assert user.ownerid in org.plan_activated_users + + def test_activate_user_success_for_users_free( + self, request, dbsession, mocker, with_sql_functions + ): + org = OwnerFactory.create( + plan="users-free", + plan_user_count=1, + plan_activated_users=None, + plan_auto_activate=True, + ) + user = OwnerFactory.create_from_test_request(request) + dbsession.add(org) + dbsession.add(user) + dbsession.flush() + + was_activated = activate_user(dbsession, org.ownerid, user.ownerid) + assert was_activated is True + dbsession.commit() + assert user.ownerid in org.plan_activated_users + + def test_activate_user_success_for_enterprise_pr_billing( + self, request, dbsession, mocker, mock_configuration, with_sql_functions + ): + mocker.patch("services.license.is_enterprise", return_value=True) + mocker.patch("services.license._get_now", return_value=datetime(2020, 4, 2)) + + org = OwnerFactory.create( + service="github", + oauth_token=None, + plan_activated_users=list(range(15, 20)), + plan_auto_activate=True, + ) + dbsession.add(org) + dbsession.flush() + + encrypted_license = "wxWEJyYgIcFpi6nBSyKQZQeaQ9Eqpo3SXyUomAqQOzOFjdYB3A8fFM1rm+kOt2ehy9w95AzrQqrqfxi9HJIb2zLOMOB9tSy52OykVCzFtKPBNsXU/y5pQKOfV7iI3w9CHFh3tDwSwgjg8UsMXwQPOhrpvl2GdHpwEhFdaM2O3vY7iElFgZfk5D9E7qEnp+WysQwHKxDeKLI7jWCnBCBJLDjBJRSz0H7AfU55RQDqtTrnR+rsLDHOzJ80/VxwVYhb" + mock_configuration.params["setup"]["enterprise_license"] = encrypted_license + mock_configuration.params["setup"]["codecov_url"] = "https://codecov.mysite.com" + + user = OwnerFactory.create_from_test_request(request) + dbsession.add(org) + dbsession.add(user) + dbsession.flush() + + was_activated = activate_user(dbsession, org.ownerid, user.ownerid) + assert was_activated is True + dbsession.commit() + assert user.ownerid in org.plan_activated_users + + def test_activate_user_success_user_org_overlap( + self, request, dbsession, mock_configuration, mocker, with_sql_functions + ): + mocker.patch("services.license.is_enterprise", return_value=True) + mocker.patch("services.license._get_now", return_value=datetime(2020, 4, 2)) + + # Create two orgs to ensure our seat availability checking works across + # multiple organizations. + org = OwnerFactory.create( + service="github", + oauth_token=None, + plan_activated_users=list(range(1, 6)), + plan_auto_activate=True, + ) + dbsession.add(org) + dbsession.flush() + + org_second = OwnerFactory.create( + service="github", + oauth_token=None, + plan_activated_users=list(range(2, 8)), + plan_auto_activate=True, + ) + dbsession.add(org_second) + dbsession.flush() + + assert get_installation_plan_activated_users(dbsession)[0][0] == 7 + + # {'company': 'Test Company', 'expires': '2021-01-01 00:00:00', 'url': 'https://codecov.mysite.com', 'trial': False, 'users': 10, 'repos': None, 'pr_billing': True} + encrypted_license = "wxWEJyYgIcFpi6nBSyKQZQeaQ9Eqpo3SXyUomAqQOzOFjdYB3A8fFM1rm+kOt2ehy9w95AzrQqrqfxi9HJIb2zLOMOB9tSy52OykVCzFtKPBNsXU/y5pQKOfV7iI3w9CHFh3tDwSwgjg8UsMXwQPOhrpvl2GdHpwEhFdaM2O3vY7iElFgZfk5D9E7qEnp+WysQwHKxDeKLI7jWCnBCBJLDjBJRSz0H7AfU55RQDqtTrnR+rsLDHOzJ80/VxwVYhb" + mock_configuration.params["setup"]["enterprise_license"] = encrypted_license + mock_configuration.params["setup"]["codecov_url"] = "https://codecov.mysite.com" + + user = OwnerFactory.create_from_test_request(request) + dbsession.add(org_second) + dbsession.add(user) + dbsession.flush() + + was_activated = activate_user(dbsession, org_second.ownerid, user.ownerid) + assert was_activated is True + dbsession.commit() + + was_activated = activate_user(dbsession, org.ownerid, user.ownerid) + assert was_activated is True + dbsession.commit() + + assert get_installation_plan_activated_users(dbsession)[0][0] == 8 + + def test_activate_user_failure_for_enterprise_pr_billing_no_seats( + self, request, dbsession, mock_configuration, mocker, with_sql_functions + ): + mocker.patch("services.license.is_enterprise", return_value=True) + mocker.patch("services.license._get_now", return_value=datetime(2020, 4, 2)) + + # Create two orgs to ensure our seat availability checking works across + # multiple organizations. + org = OwnerFactory.create( + service="github", + oauth_token=None, + plan_activated_users=list(range(15, 20)), + plan_auto_activate=True, + ) + dbsession.add(org) + dbsession.flush() + + org_second = OwnerFactory.create( + service="github", + oauth_token=None, + plan_activated_users=list(range(21, 35)), + plan_auto_activate=True, + ) + dbsession.add(org_second) + dbsession.flush() + + encrypted_license = "wxWEJyYgIcFpi6nBSyKQZQeaQ9Eqpo3SXyUomAqQOzOFjdYB3A8fFM1rm+kOt2ehy9w95AzrQqrqfxi9HJIb2zLOMOB9tSy52OykVCzFtKPBNsXU/y5pQKOfV7iI3w9CHFh3tDwSwgjg8UsMXwQPOhrpvl2GdHpwEhFdaM2O3vY7iElFgZfk5D9E7qEnp+WysQwHKxDeKLI7jWCnBCBJLDjBJRSz0H7AfU55RQDqtTrnR+rsLDHOzJ80/VxwVYhb" + mock_configuration.params["setup"]["enterprise_license"] = encrypted_license + mock_configuration.params["setup"]["codecov_url"] = "https://codecov.mysite.com" + + user = OwnerFactory.create_from_test_request(request) + dbsession.add(org_second) + dbsession.add(user) + dbsession.flush() + + was_activated = activate_user(dbsession, org_second.ownerid, user.ownerid) + assert was_activated is False + dbsession.commit() + assert user.ownerid not in org.plan_activated_users + + def test_activate_user_enterprise_pr_billing_invalid_license( + self, request, dbsession, mocker, mock_configuration, with_sql_functions + ): + mocker.patch("services.license.is_enterprise", return_value=True) + + org = OwnerFactory.create( + service="github", + oauth_token=None, + plan_activated_users=list(range(15, 20)), + plan_auto_activate=True, + ) + dbsession.add(org) + dbsession.flush() + + encrypted_license = "wxWEJyYgIcFpi6nBSyKQZQeaQ9Eqpo3SXyUomAqQOzOFjdYB3A8fFM1rm+kOt2ehy9w95AzrQqrqfxi9HJIb2zLOMOB9tSy52OykVCzFtKPBNsXU/y5pQKOfV7iI3w9CHFh3tDwSwgjg8UsMXwQPOhrpvl2GdHpwEhFdaM2O3vY7iElFgZfk5D9E7qEnp+WysQwHKxDeKLI7jWCnBCBJLDjBJRSz0H7AfU55RQDqtTrnR+rsLDHOzJ80/VxwVYhb" + mock_configuration.params["setup"]["enterprise_license"] = encrypted_license + mock_configuration.params["setup"]["codecov_url"] = "https://codecov.mysite.com" + + user = OwnerFactory.create_from_test_request(request) + dbsession.add(org) + dbsession.add(user) + dbsession.flush() + + was_activated = activate_user(dbsession, org.ownerid, user.ownerid) + assert was_activated is False + + def test_pr_billing_enterprise_no_seats_for_auto_actiavation( + self, request, dbsession, mocker, mock_configuration, with_sql_functions + ): + mocker.patch("services.license.is_enterprise", return_value=True) + mocker.patch("services.license._get_now", return_value=datetime(2020, 4, 2)) + + user = OwnerFactory.create_from_test_request(request) + dbsession.add(user) + dbsession.flush() + + org = OwnerFactory.create( + service="github", + oauth_token=None, + plan_activated_users=[user.ownerid], + plan_auto_activate=True, + ) + dbsession.add(org) + dbsession.flush() + + encrypted_license = "AtFDCJPhzM0SEF6MdCay6SwaDEZjkIlxH64UAo+Qm2auVe7SsfwxvjgXviKYBK2t+mQSbQQIc9hluF4oI6r+8ZpVCYvOnHv/Qp7Ism747cGKHHGpePm/E3MDaFTGyRdTaGach9K0/3UdoGJh9Gcf1FhEiutHV2qmhWLKQFLdD9QJu31vFGChS63NH864XV3Hp62GEmhuV+/tyVNTVmh7UXShaNVEC8CU+714TUVYO0SWuysPDr6wv6mBskZE5Evb" + mock_configuration.params["setup"]["enterprise_license"] = encrypted_license + mock_configuration.params["setup"]["codecov_url"] = "https://codecov.mysite.com" + + # Make a new user, this would be the 11th activated user + second_user = OwnerFactory.create_from_test_request(request) + dbsession.add(second_user) + dbsession.flush() + + was_activated = activate_user(dbsession, org.ownerid, second_user.ownerid) + assert was_activated is False diff --git a/apps/worker/services/tests/test_ai_pr_review.py b/apps/worker/services/tests/test_ai_pr_review.py new file mode 100644 index 0000000000..2264d68f72 --- /dev/null +++ b/apps/worker/services/tests/test_ai_pr_review.py @@ -0,0 +1,267 @@ +import json + +import pytest + +from database.tests.factories import OwnerFactory, RepositoryFactory +from services.ai_pr_review import Diff, LineInfo, perform_review +from services.archive import ArchiveService + +TEST_DIFF = """diff --git a/codecov_auth/signals.py b/codecov_auth/signals.py +index d728f92f..37f333fb 100644 +--- a/codecov_auth/signals.py ++++ b/codecov_auth/signals.py +@@ -1,10 +1,13 @@ ++import json + import logging + from datetime import datetime + ++from django.conf import settings + from django.db.models.signals import post_save + from django.dispatch import receiver ++from google.cloud import pubsub_v1 + +-from codecov_auth.models import Owner, OwnerProfile ++from codecov_auth.models import OrganizationLevelToken, Owner, OwnerProfile + + + @receiver(post_save, sender=Owner) +@@ -13,3 +16,34 @@ def create_owner_profile_when_owner_is_created( + ): + if created: + return OwnerProfile.objects.create(owner_id=instance.ownerid) ++ ++ ++_pubsub_publisher = None ++ ++ ++def _get_pubsub_publisher(): ++ global _pubsub_publisher ++ if not _pubsub_publisher: ++ _pubsub_publisher = pubsub_v1.PublisherClient() ++ return _pubsub_publisher ++ ++ ++@receiver( ++ post_save, sender=OrganizationLevelToken, dispatch_uid="shelter_sync_org_token" ++) ++def update_repository(sender, instance: OrganizationLevelToken, **kwargs): ++ pubsub_project_id = settings.SHELTER_PUBSUB_PROJECT_ID ++ topic_id = settings.SHELTER_PUBSUB_SYNC_REPO_TOPIC_ID ++ if pubsub_project_id and topic_id: ++ publisher = _get_pubsub_publisher() ++ topic_path = publisher.topic_path(pubsub_project_id, topic_id) ++ publisher.publish( ++ topic_path, ++ json.dumps( ++ { ++ "type": "org_token", ++ "sync": "one", ++ "id": instance.id, ++ } ++ ).encode("utf-8"), ++ ) +diff --git a/codecov_auth/tests/test_signals.py b/codecov_auth/tests/test_signals.py +new file mode 100644 +index 00000000..b2fb0642 +--- /dev/null ++++ b/codecov_auth/tests/test_signals.py +@@ -0,0 +1,26 @@ ++import os ++ ++import pytest ++from django.test import override_settings ++ ++from codecov_auth.tests.factories import OrganizationLevelTokenFactory ++ ++ ++@override_settings( ++ SHELTER_PUBSUB_PROJECT_ID="test-project-id", ++ SHELTER_PUBSUB_SYNC_REPO_TOPIC_ID="test-topic-id", ++) ++@pytest.mark.django_db ++def test_shelter_org_token_sync(mocker): ++ # this prevents the pubsub SDK from trying to load credentials ++ os.environ["PUBSUB_EMULATOR_HOST"] = "localhost" ++ ++ publish = mocker.patch("google.cloud.pubsub_v1.PublisherClient.publish") ++ ++ # this triggers the publish via Django signals ++ OrganizationLevelTokenFactory(id=91728376) ++ ++ publish.assert_called_once_with( ++ "projects/test-project-id/topics/test-topic-id", ++ b'{"type": "org_token", "sync": "one", "id": 91728376}', ++ ) +diff --git a/core/signals.py b/core/signals.py +index 77500d63..adffea32 100644 +--- a/core/signals.py ++++ b/core/signals.py +@@ -18,12 +18,19 @@ def _get_pubsub_publisher(): + + + @receiver(post_save, sender=Repository, dispatch_uid="shelter_sync_repo") +-def update_repository(sender, instance, **kwargs): ++def update_repository(sender, instance: Repository, **kwargs): + pubsub_project_id = settings.SHELTER_PUBSUB_PROJECT_ID + topic_id = settings.SHELTER_PUBSUB_SYNC_REPO_TOPIC_ID + if pubsub_project_id and topic_id: + publisher = _get_pubsub_publisher() + topic_path = publisher.topic_path(pubsub_project_id, topic_id) + publisher.publish( +- topic_path, json.dumps({"sync": instance.repoid}).encode("utf-8") ++ topic_path, ++ json.dumps( ++ { ++ "type": "repo", ++ "sync": "one", ++ "id": instance.repoid, ++ } ++ ).encode("utf-8"), + ) +diff --git a/core/tests/test_signals.py b/core/tests/test_signals.py +index b6eafc65..26a8c8e2 100644 +--- a/core/tests/test_signals.py ++++ b/core/tests/test_signals.py +@@ -21,5 +21,6 @@ def test_shelter_repo_sync(mocker): + RepositoryFactory(repoid=91728376) + + publish.assert_called_once_with( +- "projects/test-project-id/topics/test-topic-id", b'{"sync": 91728376}' ++ "projects/test-project-id/topics/test-topic-id", ++ b'{"type": "repo", "sync": "one", "id": 91728376}', + ) +""" + +config_params = { + "services": { + "openai": { + "api_key": "placeholder", # replace this temporarily if you need to regenerate the VCR cassettes + }, + "minio": { + "hash_key": "test-hash", + }, + }, +} + +torngit_token = { + "key": "placeholder", # replace this temporarily if you need to regenerate the VCR cassettes + "secret": None, + "username": "scott-codecov", +} + + +def test_review_index(): + diff = Diff(TEST_DIFF) + assert diff.line_info(29) == LineInfo( + file_path="codecov_auth/signals.py", position=23 + ) + assert diff.line_info(123) == LineInfo( + file_path="core/tests/test_signals.py", position=6 + ) + + +@pytest.mark.asyncio +async def test_perform_initial_review( + dbsession, codecov_vcr, mocker, mock_configuration, mock_storage +): + mock_configuration.set_params(config_params) + + bot_token = mocker.patch("shared.bots.repo_bots.get_repo_particular_bot_token") + bot_token.return_value = (torngit_token, None) + + owner = OwnerFactory.create(service="github", username="scott-codecov") + repository = RepositoryFactory.create(owner=owner, name="codecov-test") + dbsession.add(owner) + dbsession.add(repository) + dbsession.commit() + + archive = ArchiveService(repository) + + await perform_review(repository, 40) + + assert json.loads( + mock_storage.read_file( + "archive", f"ai_pr_review/{archive.storage_hash}/pull_40.json" + ) + ) == { + "commit_sha": "b607bb0e17e1b8d8699272a26e32986a933f9946", + "review_ids": [1740008775], + } + + +@pytest.mark.asyncio +async def test_perform_duplicate_review( + dbsession, codecov_vcr, mocker, mock_configuration, mock_storage +): + mock_configuration.set_params(config_params) + + bot_token = mocker.patch("shared.bots.repo_bots.get_repo_particular_bot_token") + bot_token.return_value = (torngit_token, None) + + owner = OwnerFactory(service="github", username="scott-codecov") + repository = RepositoryFactory(owner=owner, name="codecov-test") + dbsession.add(owner) + dbsession.add(repository) + dbsession.commit() + + archive = ArchiveService(repository) + + mock_storage.write_file( + "archive", + f"ai_pr_review/{archive.storage_hash}/pull_40.json", + json.dumps( + { + "commit_sha": "b607bb0e17e1b8d8699272a26e32986a933f9946", + "review_ids": [1740008775], + } + ), + ) + + perform = mocker.patch("services.ai_pr_review.Review.perform") + perform.return_value = None + + await perform_review(repository, 40) + + # noop - we already made a review for this sha + assert not perform.called + + +@pytest.mark.asyncio +async def test_perform_new_commit( + dbsession, codecov_vcr, mocker, mock_configuration, mock_storage +): + mock_configuration.set_params(config_params) + + bot_token = mocker.patch("shared.bots.repo_bots.get_repo_particular_bot_token") + bot_token.return_value = (torngit_token, None) + + owner = OwnerFactory(service="github", username="scott-codecov") + repository = RepositoryFactory(owner=owner, name="codecov-test") + dbsession.add(owner) + dbsession.add(repository) + dbsession.commit() + + archive = ArchiveService(repository) + + mock_storage.write_file( + "archive", + f"ai_pr_review/{archive.storage_hash}/pull_40.json", + json.dumps( + { + "commit_sha": "b607bb0e17e1b8d8699272a26e32986a933f9946", + "review_ids": [1740008775], + } + ), + ) + + await perform_review(repository, 40) + + assert json.loads( + mock_storage.read_file( + "archive", + f"ai_pr_review/{archive.storage_hash}/pull_40.json", + ) + ) == { + "commit_sha": "5c64a5143951193dde7b14c14611eebe1025f862", + "review_ids": [1740008775, 1740017976], + } diff --git a/apps/worker/services/tests/test_billing.py b/apps/worker/services/tests/test_billing.py new file mode 100644 index 0000000000..7a20a2b0b8 --- /dev/null +++ b/apps/worker/services/tests/test_billing.py @@ -0,0 +1,74 @@ +import pytest +from django.test import override_settings +from shared.plan.constants import PlanName +from shared.plan.service import PlanService + +from database.tests.factories import OwnerFactory +from tests.helpers import mock_all_plans_and_tiers + + +class TestBillingServiceTestCase(object): + """ + BillingService is deprecated - use PlanService instead. + """ + + @pytest.fixture(autouse=True) + def setup(self): + mock_all_plans_and_tiers() + + @pytest.mark.django_db + def test_pr_author_plan_check(self, request, dbsession, with_sql_functions): + owner = OwnerFactory.create(service="github", plan="users-pr-inappm") + dbsession.add(owner) + dbsession.flush() + plan = PlanService(owner) + assert plan.is_pr_billing_plan + + @pytest.mark.django_db + @override_settings(IS_ENTERPRISE=True) + def test_pr_author_enterprise_plan_check( + self, request, dbsession, mock_configuration, with_sql_functions + ): + owner = OwnerFactory.create(service="github") + dbsession.add(owner) + dbsession.flush() + + encrypted_license = "wxWEJyYgIcFpi6nBSyKQZQeaQ9Eqpo3SXyUomAqQOzOFjdYB3A8fFM1rm+kOt2ehy9w95AzrQqrqfxi9HJIb2zLOMOB9tSy52OykVCzFtKPBNsXU/y5pQKOfV7iI3w9CHFh3tDwSwgjg8UsMXwQPOhrpvl2GdHpwEhFdaM2O3vY7iElFgZfk5D9E7qEnp+WysQwHKxDeKLI7jWCnBCBJLDjBJRSz0H7AfU55RQDqtTrnR+rsLDHOzJ80/VxwVYhb" + mock_configuration.params["setup"]["enterprise_license"] = encrypted_license + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codecov.mysite.com" + ) + + plan = PlanService(owner) + + assert plan.is_pr_billing_plan + + @pytest.mark.django_db + def test_plan_not_pr_author(self, request, dbsession, with_sql_functions): + owner = OwnerFactory.create( + service="github", plan=PlanName.CODECOV_PRO_MONTHLY_LEGACY.value + ) + dbsession.add(owner) + dbsession.flush() + + plan = PlanService(owner) + + assert not plan.is_pr_billing_plan + + @pytest.mark.django_db + @override_settings(IS_ENTERPRISE=True) + def test_pr_author_enterprise_plan_check_non_pr_plan( + self, request, dbsession, mocker, mock_configuration, with_sql_functions + ): + owner = OwnerFactory.create(service="github") + dbsession.add(owner) + dbsession.flush() + + encrypted_license = "0dRbhbzp8TVFQp7P4e2ES9lSfyQlTo8J7LQ" + mock_configuration.params["setup"]["enterprise_license"] = encrypted_license + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codeov.mysite.com" + ) + plan = PlanService(owner) + + assert not plan.is_pr_billing_plan diff --git a/apps/worker/services/tests/test_commit_status.py b/apps/worker/services/tests/test_commit_status.py new file mode 100644 index 0000000000..1f3a2472ab --- /dev/null +++ b/apps/worker/services/tests/test_commit_status.py @@ -0,0 +1,67 @@ +from services.commit_status import RepositoryCIFilter, _ci_providers + + +class TestCommitStatus(object): + def test_ci_providers_no_config(self, mock_configuration): + assert _ci_providers() == [] + + def test_ci_providers_config_list(self, mock_configuration): + mock_configuration.params["services"]["ci_providers"] = [ + "ser_1", + "la_3", + "something_4", + ] + assert _ci_providers() == ["ser_1", "la_3", "something_4"] + + def test_ci_providers_config_string(self, mock_configuration): + mock_configuration.params["services"]["ci_providers"] = ( + "ser_1, la_3, something_4" + ) + assert sorted(_ci_providers()) == sorted(["ser_1", "la_3", "something_4"]) + + +class TestRepositoryCIFilter(object): + def test_filter(self): + service = RepositoryCIFilter( + {"codecov": {"ci": ["simple", "!excluded", "another", "!reject"]}} + ) + assert service._filter({"url": "https://www.example.com", "context": "simple"}) + assert service._filter({"url": "https://www.another.simple", "context": "ok"}) + assert service._filter( + {"url": "http://www.another.simple", "context": "reject"} + ) + assert not service._filter( + {"url": "http://www.excluded.simple", "context": "reject"} + ) + assert not service._filter( + {"url": "http://www.another.reject", "context": "simple"} + ) + assert not service._filter( + {"url": "http://www.example.com", "context": "nothing"} + ) + assert not service._filter( + {"url": "http://www.example.com", "context": "excluded"} + ) + assert not service._filter( + {"url": "http://reject.example.com", "context": "ok"} + ) + assert not service._filter({"url": "http://www.reject.com", "context": "ok"}) + assert not service._filter( + {"url": "http://www.reject.com", "context": "simple"} + ) + assert not service._filter( + {"url": "http://www.ok.com", "context": "simple/reject"} + ) + assert service._filter({"url": "http://www.ok.com", "context": "jenkins build"}) + + def test_filter_jenkins_excluded(self): + service = RepositoryCIFilter( + { + "codecov": { + "ci": ["simple", "!excluded", "!jenkins", "another", "!reject"] + } + } + ) + assert not service._filter( + {"url": "http://www.ok.com", "context": "jenkins build"} + ) diff --git a/apps/worker/services/tests/test_decoration.py b/apps/worker/services/tests/test_decoration.py new file mode 100644 index 0000000000..cfced3a2e0 --- /dev/null +++ b/apps/worker/services/tests/test_decoration.py @@ -0,0 +1,1035 @@ +from datetime import datetime, timedelta + +import pytest +from shared.django_apps.codecov_auth.tests.factories import ( + OwnerFactory as DjangoOwnerFactory, +) +from shared.django_apps.core.tests.factories import CommitFactory as DjangoCommitFactory +from shared.django_apps.core.tests.factories import ( + RepositoryFactory as DjangoRepositoryFactory, +) +from shared.django_apps.reports.models import ReportSession, ReportType +from shared.django_apps.reports.tests.factories import CommitReportFactory +from shared.django_apps.reports.tests.factories import ( + UploadFactory as DjangoUploadFactory, +) +from shared.plan.constants import DEFAULT_FREE_PLAN +from shared.plan.service import PlanService +from shared.upload.utils import UploaderType, insert_coverage_measurement +from shared.utils.test_utils import mock_config_helper + +from database.enums import TrialStatus +from database.tests.factories import ( + CommitFactory, + OwnerFactory, + PullFactory, + ReportFactory, + RepositoryFactory, + UploadFactory, +) +from services.decoration import ( + BOT_USER_EMAILS, + Decoration, + _is_bot_account, + determine_decoration_details, + determine_uploads_used, +) +from services.repository import EnrichedPull +from tests.helpers import mock_all_plans_and_tiers + + +@pytest.fixture +def enriched_pull(dbsession, request): + repository = RepositoryFactory.create( + owner__username="codecov", + owner__service="github", + owner__unencrypted_oauth_token="testtlxuu2kfef3km1fbecdlmnb2nvpikvmoadi3", + owner__plan="users-pr-inappm", + name="example-python", + image_token="abcdefghij", + private=True, + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create( + repository=repository, + author__username=f"base{request.node.name[-20:]}", + author__service="github", + ) + head_commit = CommitFactory.create( + repository=repository, + author__username=f"head{request.node.name[-20:]}", + author__service="github", + ) + pull = PullFactory.create( + author__service="github", + repository=repository, + base=base_commit.commitid, + head=head_commit.commitid, + state="merged", + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + provider_pull = { + "author": {"id": "7123", "username": "tomcat"}, + "base": { + "branch": "master", + "commitid": "b92edba44fdd29fcc506317cc3ddeae1a723dd08", + }, + "head": { + "branch": "reason/some-testing", + "commitid": "a06aef4356ca35b34c5486269585288489e578db", + }, + "number": "1", + "id": "1", + "state": "open", + "title": "Creating new code for reasons no one knows", + } + return EnrichedPull(database_pull=pull, provider_pull=provider_pull) + + +@pytest.fixture +def gitlab_root_group(dbsession): + root_group = OwnerFactory.create( + username="root_group", + service="gitlab", + unencrypted_oauth_token="testtlxuu2kfef3km1fbecdlmnb2nvpikvmoadi3", + plan="users-pr-inappm", + plan_activated_users=[], + plan_auto_activate=False, + plan_user_count=3, + ) + dbsession.add(root_group) + dbsession.flush() + return root_group + + +@pytest.fixture +def gitlab_middle_group(dbsession, gitlab_root_group): + mid_group = OwnerFactory.create( + username="mid_group", + service="gitlab", + unencrypted_oauth_token="testtlxuu2kfef3km1fbecdlmnb2nvpikvmoadi4", + plan="users-pr-inappy", + plan_activated_users=[], + parent_service_id=gitlab_root_group.service_id, + plan_auto_activate=True, + ) + dbsession.add(mid_group) + dbsession.flush() + return mid_group + + +@pytest.fixture +def gitlab_enriched_pull_subgroup(dbsession, gitlab_middle_group): + subgroup = OwnerFactory.create( + username="subgroup", + service="gitlab", + unencrypted_oauth_token="testtlxuu2kfef3km1fbecdlmnb2nvpikvmoadi3", + plan=None, + parent_service_id=gitlab_middle_group.service_id, + plan_activated_users=[], + plan_auto_activate=True, + ) + dbsession.add(subgroup) + dbsession.flush() + + repository = RepositoryFactory.create( + owner=subgroup, name="example-python", image_token="abcdefghij", private=True + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository) + head_commit = CommitFactory.create(repository=repository) + pull = PullFactory.create( + repository=repository, + base=base_commit.commitid, + head=head_commit.commitid, + state="merged", + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + provider_pull = { + "author": {"id": "7123", "username": "tomcat"}, + "base": { + "branch": "master", + "commitid": "b92edba44fdd29fcc506317cc3ddeae1a723dd08", + }, + "head": { + "branch": "reason/some-testing", + "commitid": "a06aef4356ca35b34c5486269585288489e578db", + }, + "number": "1", + "id": "1", + "state": "open", + "title": "Creating new code for reasons no one knows", + } + return EnrichedPull(database_pull=pull, provider_pull=provider_pull) + + +@pytest.fixture +def gitlab_enriched_pull_root(dbsession, gitlab_root_group): + repository = RepositoryFactory.create( + owner=gitlab_root_group, + name="example-python", + image_token="abcdefghij", + private=True, + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository) + head_commit = CommitFactory.create(repository=repository) + pull = PullFactory.create( + repository=repository, + base=base_commit.commitid, + head=head_commit.commitid, + state="merged", + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + provider_pull = { + "author": {"id": "7123", "username": "tomcat"}, + "base": { + "branch": "master", + "commitid": "b92edba44fdd29fcc506317cc3ddeae1a723dd08", + }, + "head": { + "branch": "reason/some-testing", + "commitid": "a06aef4356ca35b34c5486269585288489e578db", + }, + "number": "1", + "id": "1", + "state": "open", + "title": "Creating new code for reasons no one knows", + } + return EnrichedPull(database_pull=pull, provider_pull=provider_pull) + + +class TestDecorationServiceTestCase(object): + @pytest.fixture(autouse=True) + def setup(self): + mock_all_plans_and_tiers() + + @pytest.mark.django_db + def test_decoration_type_basic_plan_upload_limit( + self, enriched_pull, dbsession, mocker + ): + mocker.patch("services.license.is_enterprise", return_value=False) + pr_author = OwnerFactory.create( + service="github", + username=enriched_pull.provider_pull["author"]["username"], + service_id=enriched_pull.provider_pull["author"]["id"], + ) + dbsession.add(pr_author) + dbsession.flush() + + enriched_pull.database_pull.repository.owner.plan = DEFAULT_FREE_PLAN + enriched_pull.database_pull.repository.private = True + + commit = CommitFactory.create( + repository=enriched_pull.database_pull.repository, + author__service="github", + timestamp=datetime.now(), + ) + + report = ReportFactory.create( + commit=commit, report_type=ReportType.COVERAGE.value + ) + for i in range(249): + upload = UploadFactory.create(report=report, storage_path="url") + dbsession.add(upload) + insert_coverage_measurement( + owner_id=enriched_pull.database_pull.repository.owner.ownerid, + repo_id=enriched_pull.database_pull.repository.repoid, + commit_id=commit.id, + upload_id=upload.id, + uploader_used=UploaderType.LEGACY.value, + private_repo=enriched_pull.database_pull.repository.private, + report_type=report.report_type, + ) + dbsession.flush() + + decoration_details = determine_decoration_details(enriched_pull) + assert decoration_details.decoration_type != Decoration.upload_limit + assert decoration_details.reason != "Org has exceeded the upload limit" + + upload = UploadFactory.create(report=report, storage_path="url") + dbsession.add(upload) + dbsession.flush() + + insert_coverage_measurement( + owner_id=enriched_pull.database_pull.repository.owner.ownerid, + repo_id=enriched_pull.database_pull.repository.repoid, + commit_id=commit.id, + upload_id=upload.id, + uploader_used=UploaderType.LEGACY.value, + private_repo=enriched_pull.database_pull.repository.private, + report_type=report.report_type, + ) + + decoration_details = determine_decoration_details(enriched_pull) + assert decoration_details.decoration_type == Decoration.upload_limit + assert decoration_details.reason == "Org has exceeded the upload limit" + + @pytest.mark.django_db + def test_decoration_type_team_plan_upload_limit( + self, enriched_pull, dbsession, mocker + ): + mocker.patch("services.license.is_enterprise", return_value=False) + pr_author = OwnerFactory.create( + service="github", + username=enriched_pull.provider_pull["author"]["username"], + service_id=enriched_pull.provider_pull["author"]["id"], + ) + dbsession.add(pr_author) + dbsession.flush() + + enriched_pull.database_pull.repository.owner.plan = "users-teamm" + enriched_pull.database_pull.repository.private = True + + commit = CommitFactory.create( + repository=enriched_pull.database_pull.repository, + author__service="github", + timestamp=datetime.now(), + ) + + report = ReportFactory.create( + commit=commit, report_type=ReportType.COVERAGE.value + ) + for i in range(2499): + upload = UploadFactory.create(report=report, storage_path="url") + dbsession.add(upload) + insert_coverage_measurement( + owner_id=enriched_pull.database_pull.repository.owner.ownerid, + repo_id=enriched_pull.database_pull.repository.repoid, + commit_id=commit.id, + upload_id=upload.id, + uploader_used=UploaderType.LEGACY.value, + private_repo=enriched_pull.database_pull.repository.private, + report_type=report.report_type, + ) + dbsession.flush() + + decoration_details = determine_decoration_details(enriched_pull) + assert decoration_details.decoration_type != Decoration.upload_limit + assert decoration_details.reason != "Org has exceeded the upload limit" + + upload = UploadFactory.create(report=report, storage_path="url") + dbsession.add(upload) + dbsession.flush() + insert_coverage_measurement( + owner_id=enriched_pull.database_pull.repository.owner.ownerid, + repo_id=enriched_pull.database_pull.repository.repoid, + commit_id=commit.id, + upload_id=upload.id, + uploader_used=UploaderType.LEGACY.value, + private_repo=enriched_pull.database_pull.repository.private, + report_type=report.report_type, + ) + + decoration_details = determine_decoration_details(enriched_pull) + assert decoration_details.decoration_type == Decoration.upload_limit + assert decoration_details.reason == "Org has exceeded the upload limit" + + @pytest.mark.django_db + def test_decoration_type_unlimited_upload_on_enterprise( + self, enriched_pull, dbsession, mocker, mock_configuration + ): + mocker.patch("services.license.is_enterprise", return_value=True) + encrypted_license = "wxWEJyYgIcFpi6nBSyKQZQeaQ9Eqpo3SXyUomAqQOzOFjdYB3A8fFM1rm+kOt2ehy9w95AzrQqrqfxi9HJIb2zLOMOB9tSy52OykVCzFtKPBNsXU/y5pQKOfV7iI3w9CHFh3tDwSwgjg8UsMXwQPOhrpvl2GdHpwEhFdaM2O3vY7iElFgZfk5D9E7qEnp+WysQwHKxDeKLI7jWCnBCBJLDjBJRSz0H7AfU55RQDqtTrnR+rsLDHOzJ80/VxwVYhb" + mock_configuration.params["setup"]["enterprise_license"] = encrypted_license + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codecov.mysite.com" + ) + + pr_author = OwnerFactory.create( + service="github", + username=enriched_pull.provider_pull["author"]["username"], + service_id=enriched_pull.provider_pull["author"]["id"], + ) + dbsession.add(pr_author) + dbsession.flush() + + enriched_pull.database_pull.repository.owner.plan = DEFAULT_FREE_PLAN + enriched_pull.database_pull.repository.private = True + + commit = CommitFactory.create( + repository=enriched_pull.database_pull.repository, + author__service="github", + timestamp=datetime.now(), + ) + + report = ReportFactory.create(commit=commit) + for i in range(250): + upload = UploadFactory.create(report=report, storage_path="url") + dbsession.add(upload) + insert_coverage_measurement( + owner_id=enriched_pull.database_pull.repository.owner.ownerid, + repo_id=enriched_pull.database_pull.repository.repoid, + commit_id=commit.id, + upload_id=upload.id, + uploader_used=UploaderType.LEGACY.value, + private_repo=enriched_pull.database_pull.repository.private, + report_type=report.report_type, + ) + dbsession.flush() + + decoration_details = determine_decoration_details(enriched_pull) + # self-hosted should not be limited with their uploads + assert decoration_details.decoration_type != Decoration.upload_limit + assert decoration_details.reason != "Org has exceeded the upload limit" + + @pytest.mark.django_db + def test_uploads_used_with_expired_trial(self, mocker): + owner = DjangoOwnerFactory( + service="github", + trial_status=TrialStatus.EXPIRED.value, + trial_start_date=datetime.now() + timedelta(days=-10), + trial_end_date=datetime.now() + timedelta(days=-2), + plan=DEFAULT_FREE_PLAN, + ) + repository = DjangoRepositoryFactory( + author=owner, + private=True, + ) + commit = DjangoCommitFactory( + repository=repository, + author__service="github", + timestamp=datetime.now(), + ) + report = CommitReportFactory( + commit=commit, report_type=ReportType.COVERAGE.value + ) + + report_before_trial = DjangoUploadFactory(report=report, storage_path="url") + report_before_trial.created_at += timedelta(days=-12) + report_before_trial.save() + upload_before_trial = insert_coverage_measurement( + owner_id=owner.ownerid, + repo_id=repository.repoid, + commit_id=commit.id, + upload_id=report_before_trial.id, + uploader_used=UploaderType.LEGACY.value, + private_repo=repository.private, + report_type=report.report_type, + ) + upload_before_trial.created_at += timedelta(days=-12) + upload_before_trial.save() + + report_during_trial = DjangoUploadFactory(report=report, storage_path="url") + report_during_trial.created_at += timedelta(days=-5) + report_during_trial.save() + upload_during_trial = insert_coverage_measurement( + owner_id=owner.ownerid, + repo_id=repository.repoid, + commit_id=commit.id, + upload_id=report_during_trial.id, + uploader_used=UploaderType.LEGACY.value, + private_repo=repository.private, + report_type=report.report_type, + ) + upload_during_trial.created_at += timedelta(days=-5) + upload_during_trial.save() + + report_after_trial = DjangoUploadFactory(report=report, storage_path="url") + insert_coverage_measurement( + owner_id=owner.ownerid, + repo_id=repository.repoid, + commit_id=commit.id, + upload_id=report_after_trial.id, + uploader_used=UploaderType.LEGACY.value, + private_repo=repository.private, + report_type=report.report_type, + ) + + uploads_present = ReportSession.objects.all() + assert len(uploads_present) == 3 + + mock_config_helper(mocker, configs={"setup.upload_throttling_enabled": True}) + plan_service = PlanService(current_org=owner) + uploads_used = determine_uploads_used(plan_service=plan_service) + + assert uploads_used == 2 + + @pytest.mark.django_db + def test_get_decoration_type_no_pull(self, mocker): + decoration_details = determine_decoration_details(None) + + assert decoration_details.decoration_type == Decoration.standard + assert decoration_details.reason == "No pull" + assert decoration_details.should_attempt_author_auto_activation is False + + @pytest.mark.django_db + def test_get_decoration_type_no_provider_pull(self, mocker, enriched_pull): + enriched_pull.provider_pull = None + + decoration_details = determine_decoration_details(enriched_pull) + + assert decoration_details.decoration_type == Decoration.standard + assert ( + decoration_details.reason + == "Can't determine PR author - no pull info from provider" + ) + assert decoration_details.should_attempt_author_auto_activation is False + + @pytest.mark.django_db + def test_get_decoration_type_public_repo(self, dbsession, mocker, enriched_pull): + enriched_pull.database_pull.repository.private = False + dbsession.flush() + + decoration_details = determine_decoration_details(enriched_pull) + + assert decoration_details.decoration_type == Decoration.standard + assert decoration_details.reason == "Public repo" + assert decoration_details.should_attempt_author_auto_activation is False + + @pytest.mark.django_db + def test_get_decoration_type_not_pr_plan(self, dbsession, mocker, enriched_pull): + enriched_pull.database_pull.repository.owner.plan = "users-inappm" + dbsession.flush() + + decoration_details = determine_decoration_details(enriched_pull) + + assert decoration_details.decoration_type == Decoration.standard + assert decoration_details.reason == "Org not on PR plan" + assert decoration_details.should_attempt_author_auto_activation is False + + @pytest.mark.django_db + # what is a users plan? + def test_get_decoration_type_for_users_plan(self, dbsession): + repository = RepositoryFactory.create( + owner__username="drazisil-org", + owner__service="github", + owner__unencrypted_oauth_token="testtfasdfasdflxuu2kfer2ef23", + owner__plan=DEFAULT_FREE_PLAN, + private=True, + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create( + repository=repository, + author__service="github", + ) + head_commit = CommitFactory.create( + repository=repository, + author__service="github", + ) + pull = PullFactory.create( + author__service="github", + repository=repository, + base=base_commit.commitid, + head=head_commit.commitid, + state="merged", + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + provider_pull = { + "author": {"id": "7123", "username": "tomcat"}, + "base": { + "branch": "master", + "commitid": "b92edba44fdd29fcc506317cc3ddeae1a723dd08", + }, + "head": { + "branch": "reason/some-testing", + "commitid": "a06aef4356ca35b34c5486269585288489e578db", + }, + "number": "1", + "id": "1", + "state": "open", + "title": "Creating new code for reasons no one knows", + } + enriched_pull_whitelisted = EnrichedPull( + database_pull=pull, provider_pull=provider_pull + ) + + pr_author = OwnerFactory.create( + service="github", + username=enriched_pull_whitelisted.provider_pull["author"]["username"], + service_id=enriched_pull_whitelisted.provider_pull["author"]["id"], + ) + dbsession.add(pr_author) + dbsession.flush() + + decoration_details = determine_decoration_details(enriched_pull_whitelisted) + dbsession.commit() + + assert decoration_details.decoration_type == Decoration.upgrade + assert decoration_details.reason == "User must be manually activated" + assert decoration_details.should_attempt_author_auto_activation is False + assert ( + pr_author.ownerid + not in enriched_pull_whitelisted.database_pull.repository.owner.plan_activated_users + ) + + @pytest.mark.django_db + def test_get_decoration_type_pr_author_not_in_db(self, mocker, enriched_pull): + enriched_pull.provider_pull["author"]["id"] = "190" + + decoration_details = determine_decoration_details(enriched_pull) + + assert decoration_details.decoration_type == Decoration.upgrade + assert decoration_details.reason == "PR author not found in database" + assert decoration_details.should_attempt_author_auto_activation is False + + @pytest.mark.django_db + def test_get_decoration_type_pr_author_manual_activation_required( + self, dbsession, mocker, enriched_pull, with_sql_functions + ): + enriched_pull.database_pull.repository.owner.plan_user_count = 3 + enriched_pull.database_pull.repository.owner.plan_activated_users = [] + enriched_pull.database_pull.repository.owner.plan_auto_activate = False + + pr_author = OwnerFactory.create( + service="github", + username=enriched_pull.provider_pull["author"]["username"], + service_id=enriched_pull.provider_pull["author"]["id"], + ) + dbsession.add(pr_author) + dbsession.flush() + + decoration_details = determine_decoration_details(enriched_pull) + dbsession.commit() + + assert decoration_details.decoration_type == Decoration.upgrade + assert decoration_details.reason == "User must be manually activated" + assert decoration_details.should_attempt_author_auto_activation is False + assert ( + pr_author.ownerid + not in enriched_pull.database_pull.repository.owner.plan_activated_users + ) + + @pytest.mark.django_db + @pytest.mark.parametrize( + "is_bot,param,value", + [ + (True, "email", "dependabot[bot]@users.noreply.github.com"), + (True, "email", "29139614+renovate[bot]@users.noreply.github.com"), + (True, "email", "157164994+sentry-autofix[bot]@users.noreply.github.com"), + (True, "service_id", "29139614"), + (True, "service_id", "157164994"), + (False, None, None), + ], + ) + def test_is_bot_account(self, is_bot, param, value): + pr_author = OwnerFactory.create( + service="github", + ) + if is_bot and param == "email": + pr_author.email = value + elif is_bot and param == "service_id": + pr_author.service_id = value + assert _is_bot_account(pr_author) == is_bot + + @pytest.mark.django_db + def test_get_decoration_type_bot(self, dbsession, mocker, enriched_pull): + pr_author = OwnerFactory.create( + service="github", + username=enriched_pull.provider_pull["author"]["username"], + email=BOT_USER_EMAILS[0], + service_id=enriched_pull.provider_pull["author"]["id"], + ) + dbsession.add(pr_author) + dbsession.flush() + + decoration_details = determine_decoration_details(enriched_pull) + dbsession.commit() + + assert decoration_details.decoration_type == Decoration.standard + assert ( + decoration_details.reason + == "Bot user detected (does not need to be activated)" + ) + assert decoration_details.should_attempt_author_auto_activation is False + + @pytest.mark.django_db + def test_get_decoration_type_pr_author_already_active( + self, dbsession, mocker, enriched_pull + ): + pr_author = OwnerFactory.create( + service="github", + username=enriched_pull.provider_pull["author"]["username"], + service_id=enriched_pull.provider_pull["author"]["id"], + ) + dbsession.add(pr_author) + dbsession.flush() + enriched_pull.database_pull.repository.owner.plan_user_count = 3 + enriched_pull.database_pull.repository.owner.plan_activated_users = [ + pr_author.ownerid + ] + enriched_pull.database_pull.repository.owner.plan_auto_activate = False + dbsession.flush() + + decoration_details = determine_decoration_details(enriched_pull) + dbsession.commit() + + assert decoration_details.decoration_type == Decoration.standard + assert decoration_details.reason == "User is currently activated" + assert decoration_details.should_attempt_author_auto_activation is False + + @pytest.mark.django_db + def test_get_decoration_type_should_attempt_pr_author_auto_activation( + self, dbsession, mocker, enriched_pull + ): + enriched_pull.database_pull.repository.owner.plan_user_count = 3 + enriched_pull.database_pull.repository.owner.plan_activated_users = [] + enriched_pull.database_pull.repository.owner.plan_auto_activate = True + + pr_author = OwnerFactory.create( + service="github", + username=enriched_pull.provider_pull["author"]["username"], + service_id=enriched_pull.provider_pull["author"]["id"], + ) + dbsession.add(pr_author) + dbsession.flush() + + decoration_details = determine_decoration_details(enriched_pull) + dbsession.commit() + + assert decoration_details.decoration_type == Decoration.upgrade + assert decoration_details.reason == "User must be activated" + assert decoration_details.should_attempt_author_auto_activation is True + assert ( + decoration_details.activation_org_ownerid + == enriched_pull.database_pull.repository.owner.ownerid + ) + assert decoration_details.activation_author_ownerid == pr_author.ownerid + # activation hasnt happened yet + assert ( + pr_author.ownerid + not in enriched_pull.database_pull.repository.owner.plan_activated_users + ) + + @pytest.mark.django_db + def test_get_decoration_type_should_attempt_pr_author_auto_activation_users_developer( + self, dbsession, mocker, enriched_pull + ): + enriched_pull.database_pull.repository.owner.plan = DEFAULT_FREE_PLAN + enriched_pull.database_pull.repository.owner.plan_user_count = 1 + enriched_pull.database_pull.repository.owner.plan_activated_users = [] + enriched_pull.database_pull.repository.owner.plan_auto_activate = True + + pr_author = OwnerFactory.create( + service="github", + username=enriched_pull.provider_pull["author"]["username"], + service_id=enriched_pull.provider_pull["author"]["id"], + ) + dbsession.add(pr_author) + dbsession.flush() + + decoration_details = determine_decoration_details(enriched_pull) + dbsession.commit() + + assert decoration_details.decoration_type == Decoration.upgrade + assert decoration_details.reason == "User must be activated" + assert decoration_details.should_attempt_author_auto_activation is True + assert ( + decoration_details.activation_org_ownerid + == enriched_pull.database_pull.repository.owner.ownerid + ) + assert decoration_details.activation_author_ownerid == pr_author.ownerid + # activation hasnt happened yet + assert ( + pr_author.ownerid + not in enriched_pull.database_pull.repository.owner.plan_activated_users + ) + + @pytest.mark.django_db + def test_get_decoration_type_passing_empty_upload( + self, dbsession, mocker, enriched_pull + ): + enriched_pull.database_pull.repository.private = False + dbsession.flush() + + decoration_details = determine_decoration_details(enriched_pull, "pass") + + assert decoration_details.decoration_type == Decoration.passing_empty_upload + assert decoration_details.reason == "Non testable files got changed." + assert decoration_details.should_attempt_author_auto_activation is False + + @pytest.mark.django_db + def test_get_decoration_type_failing_empty_upload( + self, dbsession, mocker, enriched_pull + ): + enriched_pull.database_pull.repository.private = False + dbsession.flush() + + decoration_details = determine_decoration_details(enriched_pull, "fail") + + assert decoration_details.decoration_type == Decoration.failing_empty_upload + assert decoration_details.reason == "Testable files got changed." + assert decoration_details.should_attempt_author_auto_activation is False + + @pytest.mark.django_db + def test_get_decoration_type_processing_upload( + self, dbsession, mocker, enriched_pull + ): + enriched_pull.database_pull.repository.private = False + dbsession.flush() + + decoration_details = determine_decoration_details(enriched_pull, "processing") + + assert decoration_details.decoration_type == Decoration.processing_upload + assert decoration_details.reason == "Upload is still processing." + assert decoration_details.should_attempt_author_auto_activation is False + + +class TestDecorationServiceGitLabTestCase(object): + @pytest.fixture(autouse=True) + def setup(self): + mock_all_plans_and_tiers() + + @pytest.mark.django_db + def test_get_decoration_type_not_pr_plan_gitlab_subgroup( + self, + dbsession, + mocker, + gitlab_root_group, + gitlab_enriched_pull_subgroup, + with_sql_functions, + ): + gitlab_root_group.plan = "users-inappm" + dbsession.flush() + + decoration_details = determine_decoration_details(gitlab_enriched_pull_subgroup) + + assert decoration_details.decoration_type == Decoration.standard + assert decoration_details.reason == "Org not on PR plan" + assert decoration_details.should_attempt_author_auto_activation is False + + @pytest.mark.django_db + def test_get_decoration_type_pr_author_not_in_db_gitlab_subgroup( + self, + mocker, + gitlab_root_group, + gitlab_enriched_pull_subgroup, + with_sql_functions, + ): + gitlab_enriched_pull_subgroup.provider_pull["author"]["id"] = "190" + + decoration_details = determine_decoration_details(gitlab_enriched_pull_subgroup) + + assert decoration_details.decoration_type == Decoration.upgrade + assert decoration_details.reason == "PR author not found in database" + assert decoration_details.should_attempt_author_auto_activation is False + + @pytest.mark.django_db + def test_get_decoration_type_pr_author_manual_activation_required_gitlab_subgroup( + self, + dbsession, + mocker, + gitlab_root_group, + gitlab_enriched_pull_subgroup, + with_sql_functions, + ): + gitlab_root_group.plan_auto_activate = False + # setting on child group should not matter, uses setting from root + child_group = gitlab_enriched_pull_subgroup.database_pull.repository.owner + child_group.plan_auto_activate = True + + pr_author = OwnerFactory.create( + username=gitlab_enriched_pull_subgroup.provider_pull["author"]["username"], + service="gitlab", + service_id=gitlab_enriched_pull_subgroup.provider_pull["author"]["id"], + ) + dbsession.add(pr_author) + dbsession.flush() + + decoration_details = determine_decoration_details(gitlab_enriched_pull_subgroup) + dbsession.commit() + + assert decoration_details.decoration_type == Decoration.upgrade + assert decoration_details.reason == "User must be manually activated" + assert decoration_details.should_attempt_author_auto_activation is False + assert decoration_details.activation_org_ownerid is None + assert decoration_details.activation_author_ownerid is None + + # allow auto-activate on root + gitlab_root_group.plan_auto_activate = True + # setting on child group should not matter, uses setting from root + child_group.plan_auto_activate = False + decoration_details = determine_decoration_details(gitlab_enriched_pull_subgroup) + dbsession.commit() + + assert decoration_details.decoration_type == Decoration.upgrade + assert decoration_details.reason == "User must be activated" + assert decoration_details.should_attempt_author_auto_activation is True + assert decoration_details.activation_org_ownerid == gitlab_root_group.ownerid + assert decoration_details.activation_author_ownerid == pr_author.ownerid + # activation hasn't happened yet + assert pr_author.ownerid not in gitlab_root_group.plan_activated_users + + @pytest.mark.django_db + def test_get_decoration_type_pr_author_already_active_subgroup( + self, + dbsession, + mocker, + gitlab_root_group, + gitlab_enriched_pull_subgroup, + with_sql_functions, + ): + pr_author = OwnerFactory.create( + username=gitlab_enriched_pull_subgroup.provider_pull["author"]["username"], + service="gitlab", + service_id=gitlab_enriched_pull_subgroup.provider_pull["author"]["id"], + ) + dbsession.add(pr_author) + dbsession.flush() + gitlab_root_group.plan_activated_users = [pr_author.ownerid] + gitlab_root_group.plan_auto_activate = False + dbsession.flush() + + decoration_details = determine_decoration_details(gitlab_enriched_pull_subgroup) + dbsession.commit() + + assert decoration_details.decoration_type == Decoration.standard + assert decoration_details.reason == "User is currently activated" + assert decoration_details.should_attempt_author_auto_activation is False + assert decoration_details.activation_org_ownerid is None + assert decoration_details.activation_author_ownerid is None + + @pytest.mark.django_db + def test_get_decoration_type_should_attempt_pr_author_auto_activation( + self, + dbsession, + mocker, + gitlab_root_group, + gitlab_enriched_pull_subgroup, + with_sql_functions, + ): + pr_author = OwnerFactory.create( + username=gitlab_enriched_pull_subgroup.provider_pull["author"]["username"], + service="gitlab", + service_id=gitlab_enriched_pull_subgroup.provider_pull["author"]["id"], + ) + dbsession.add(pr_author) + dbsession.flush() + gitlab_root_group.plan_user_count = 3 + gitlab_root_group.plan_activated_users = [] + gitlab_root_group.plan_auto_activate = True + dbsession.flush() + + decoration_details = determine_decoration_details(gitlab_enriched_pull_subgroup) + dbsession.commit() + + assert decoration_details.decoration_type == Decoration.upgrade + assert decoration_details.reason == "User must be activated" + assert decoration_details.should_attempt_author_auto_activation is True + assert decoration_details.activation_org_ownerid == gitlab_root_group.ownerid + assert decoration_details.activation_author_ownerid == pr_author.ownerid + # activation hasn't happened yet + assert pr_author.ownerid not in gitlab_root_group.plan_activated_users + + @pytest.mark.django_db + def test_get_decoration_type_owner_activated_users_null( + self, dbsession, mocker, enriched_pull + ): + enriched_pull.database_pull.repository.owner.plan_user_count = 3 + enriched_pull.database_pull.repository.owner.plan_activated_users = None + enriched_pull.database_pull.repository.owner.plan_auto_activate = True + + pr_author = OwnerFactory.create( + service="github", + username=enriched_pull.provider_pull["author"]["username"], + service_id=enriched_pull.provider_pull["author"]["id"], + ) + dbsession.add(pr_author) + dbsession.flush() + + decoration_details = determine_decoration_details(enriched_pull) + dbsession.commit() + + assert decoration_details.decoration_type == Decoration.upgrade + assert decoration_details.reason == "User must be activated" + assert decoration_details.should_attempt_author_auto_activation is True + assert ( + decoration_details.activation_org_ownerid + == enriched_pull.database_pull.repository.owner.ownerid + ) + assert decoration_details.activation_author_ownerid == pr_author.ownerid + assert enriched_pull.database_pull.repository.owner.plan_activated_users is None + + @pytest.mark.django_db + def test_uploads_used_with_expired_trial(self, mocker, dbsession): + owner = DjangoOwnerFactory( + service="gitlab", + trial_status=TrialStatus.EXPIRED.value, + trial_start_date=datetime.now() + timedelta(days=-10), + trial_end_date=datetime.now() + timedelta(days=-2), + plan=DEFAULT_FREE_PLAN, + ) + repository = DjangoRepositoryFactory( + author=owner, + private=True, + ) + commit = DjangoCommitFactory( + repository=repository, + author__service="gitlab", + timestamp=datetime.now(), + ) + report = CommitReportFactory( + commit=commit, report_type=ReportType.COVERAGE.value + ) + DjangoUploadFactory(report=report, storage_path="url") + DjangoUploadFactory(report=report, storage_path="url") + + uploads_present = ReportSession.objects.all() + assert len(uploads_present) == 2 + + mock_config_helper(mocker, configs={"setup.upload_throttling_enabled": False}) + plan_service = PlanService(current_org=owner) + uploads_used = determine_uploads_used(plan_service=plan_service) + + assert uploads_used == 0 + + @pytest.mark.django_db + def test_author_is_activated_on_subgroup_not_root( + self, dbsession, gitlab_root_group, gitlab_enriched_pull_subgroup + ): + pr_author = OwnerFactory.create( + username=gitlab_enriched_pull_subgroup.provider_pull["author"]["username"], + service="gitlab", + service_id=gitlab_enriched_pull_subgroup.provider_pull["author"]["id"], + ) + dbsession.add(pr_author) + dbsession.flush() + + # user is activated on subgroup but not root group and root group does not auto activate + gitlab_root_group.plan_auto_activate = False + child_group = gitlab_enriched_pull_subgroup.database_pull.repository.owner + child_group.plan_auto_activate = False + child_group.plan_activated_users = [pr_author.ownerid] + dbsession.flush() + + decoration_details = determine_decoration_details(gitlab_enriched_pull_subgroup) + dbsession.commit() + + assert decoration_details.decoration_type == Decoration.upgrade + assert decoration_details.reason == "User must be manually activated" + assert decoration_details.should_attempt_author_auto_activation is False + assert decoration_details.activation_org_ownerid is None + assert decoration_details.activation_author_ownerid is None + + assert pr_author.ownerid not in gitlab_root_group.plan_activated_users + assert ( + pr_author.ownerid + in gitlab_enriched_pull_subgroup.database_pull.repository.owner.plan_activated_users + ) + + # allow auto-activate on root for user to get non-blocking decoration + gitlab_root_group.plan_auto_activate = True + decoration_details = determine_decoration_details(gitlab_enriched_pull_subgroup) + dbsession.commit() + + assert decoration_details.decoration_type == Decoration.upgrade + assert decoration_details.reason == "User must be activated" + assert decoration_details.should_attempt_author_auto_activation is True + assert decoration_details.activation_org_ownerid == gitlab_root_group.ownerid + assert decoration_details.activation_author_ownerid == pr_author.ownerid diff --git a/apps/worker/services/tests/test_failure_normalizer.py b/apps/worker/services/tests/test_failure_normalizer.py new file mode 100644 index 0000000000..66209ed42b --- /dev/null +++ b/apps/worker/services/tests/test_failure_normalizer.py @@ -0,0 +1,127 @@ +import pytest + +from services.failure_normalizer import FailureNormalizer + +test_string = "abcdefAB-1234-1234-1234-abcdefabcdef test_string 2024-03-10 test 0x44358378 20240312T155215Z 2024-03-12T15:52:15Z 15:52:15Z 2024-03-12T08:52:15-07:00 https://api.codecov.io/commits/list :1:2 :3: :: 0xabcdef1234" + + +def test_failure_normalizer(): + user_dict = {"TEST": [r"test_string"]} + f = FailureNormalizer(user_dict) + s = f.normalize_failure_message(test_string) + + assert ( + s + == "UUID TEST DATE test HEXNUMBER DATETIME DATETIME TIME DATETIME URL LINENO LINENO :: HEXNUMBER" + ) + + +def test_failure_normalizer_ignore_predefined(): + user_dict = {"TEST": [r"test_string"]} + f = FailureNormalizer(user_dict, True) + s = f.normalize_failure_message(test_string) + + assert ( + s + == "abcdefAB-1234-1234-1234-abcdefabcdef TEST 2024-03-10 test 0x44358378 20240312T155215Z 2024-03-12T15:52:15Z 15:52:15Z 2024-03-12T08:52:15-07:00 https://api.codecov.io/commits/list :1:2 :3: :: 0xabcdef1234" + ) + + +def test_failure_normalizer_append_predefined(): + user_dict = {"UUID": ["test"]} + f = FailureNormalizer(user_dict) + s = f.normalize_failure_message(test_string) + + assert ( + s + == "UUID UUID_string DATE UUID HEXNUMBER DATETIME DATETIME TIME DATETIME URL LINENO LINENO :: HEXNUMBER" + ) + + +def test_failure_normalizer_overwrite_predefined(): + user_dict = {"UUID": ["test"]} + f = FailureNormalizer(user_dict, override_predefined=True) + s = f.normalize_failure_message(test_string) + + assert ( + s + == "HASH UUID_string DATE UUID HEXNUMBER DATETIME DATETIME TIME DATETIME URL LINENO LINENO :: HEXNUMBER" + ) + + +def test_failure_normalizer_filepath(): + thing_string = "hello/my/name/is/hello/world.js" + user_dict = {"UUID": ["test"]} + f = FailureNormalizer(user_dict, override_predefined=True) + s = f.normalize_failure_message(thing_string) + + assert s == "FILEPATH/is/hello/world.js" + + +@pytest.mark.parametrize( + "input,expected", + [ + ( + """def test_subtract(): +> assert Calculator.subtract(1, 2) == 1.0 +E assert -1 == 1.0 +E + where -1 = <function Calculator.subtract at 0x7f43b21a3130>(1, 2) +E + where <function Calculator.subtract at 0x7f43b21a3130> = Calculator.subtract + +app/test_calculator.py:12: AssertionError" +""", + """def test_subtract(): +> assert Calculator.subtract(NO, NO) == NO +E assert NO == NO +E + where NO = <function Calculator.subtract at HEXNUMBER>(NO, NO) +E + where <function Calculator.subtract at HEXNUMBER> = Calculator.subtract + +app/test_calculator.pyLINENO AssertionError" +""", + ), + ( + """mocker = <pytest_mock.plugin.MockFixture object at 0x6ddc0ae62550> +mock_configuration = <shared.config.ConfigHelper object at 0x54dc9bb7c210> +chain = mocker.patch("tasks.upload.chain") +storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" +) +message="", +commitid="abf6d4df662c47e32460020ab14abf9303581429", +s = b'\\x592f6b514678496f4333336a54314f71774c744f7934524d4479517778715270446678487459344769777458454a584d632b61633349432f35636c52635659473330782f7a496b7a5053542b426333454d614c5635673d3d' +altchars = None, validate = False +""", + """mocker = <pytest_mock.plugin.MockFixture object at HEXNUMBER> +mock_configuration = <shared.config.ConfigHelper object at HEXNUMBER> +chain = mocker.patch("tasks.upload.chain") +storage_path = ( + "FILEPATH/testing/UUID/bundle_report.sqlite" +) +message="", +commitid="HASH", +s = b'\\xHASH' +altchars = None, validate = False +""", + ), + ], +) +def test_from_random_cases(input, expected): + test_message = input + order_to_process = [ + "UUID", + "DATETIME", + "DATE", + "TIME", + "URL", + "FILEPATH", + "LINENO", + "HASH", + "HEXNUMBER", + "NO", + ] + + normalizer_class = FailureNormalizer( + dict(), override_predefined=True, key_analysis_order=order_to_process + ) + s = normalizer_class.normalize_failure_message(test_message) + assert s == expected diff --git a/apps/worker/services/tests/test_github.py b/apps/worker/services/tests/test_github.py new file mode 100644 index 0000000000..79749d1fd6 --- /dev/null +++ b/apps/worker/services/tests/test_github.py @@ -0,0 +1,99 @@ +from unittest.mock import MagicMock + +import pytest +from redis import RedisError + +from database.models.core import GithubAppInstallation, Owner +from database.tests.factories.core import CommitFactory, RepositoryFactory +from services.github import get_github_app_for_commit, set_github_app_for_commit + + +class TestGetSetGithubAppsToCommits(object): + def _get_commit(self, dbsession): + commit = CommitFactory(repository__owner__service="github") + dbsession.add(commit) + dbsession.flush() + return commit + + def _get_app(self, owner: Owner, dbsession): + app = GithubAppInstallation( + owner=owner, installation_id=1250, app_id=250, pem_path="some_path" + ) + dbsession.add(app) + dbsession.flush() + return app + + @pytest.fixture + def mock_redis(self, mocker): + fake_redis = MagicMock(name="fake_redis") + mock_conn = mocker.patch("services.github.get_redis_connection") + mock_conn.return_value = fake_redis + return fake_redis + + def test_set_app_for_commit_no_app(self, mock_redis, dbsession): + commit = self._get_commit(dbsession) + assert set_github_app_for_commit(None, commit) == False + mock_redis.set.assert_not_called() + + def test_set_app_for_commit_redis_success(self, mock_redis, dbsession): + commit = self._get_commit(dbsession) + app = self._get_app(commit.repository.owner, dbsession) + assert set_github_app_for_commit(app.id, commit) == True + mock_redis.set.assert_called_with( + f"app_to_use_for_commit_{commit.id}", str(app.id), ex=(60 * 60 * 2) + ) + + def test_set_app_for_commit_redis_error(self, mock_redis, dbsession): + commit = self._get_commit(dbsession) + mock_redis.set.side_effect = RedisError + assert set_github_app_for_commit("1000", commit) == False + mock_redis.set.assert_called_with( + f"app_to_use_for_commit_{commit.id}", "1000", ex=(60 * 60 * 2) + ) + + def test_get_app_for_commit(self, mock_redis, dbsession): + repo_github = RepositoryFactory(owner__service="github") + repo_ghe = RepositoryFactory(owner__service="github_enterprise") + repo_gitlab = RepositoryFactory(owner__service="gitlab") + redis_keys = { + "app_to_use_for_commit_12": b"1200", + "app_to_use_for_commit_10": b"1000", + } + fake_commit_12 = MagicMock( + name="fake_commit", **{"id": 12, "repository": repo_github} + ) + fake_commit_10 = MagicMock( + name="fake_commit", + **{"id": 10, "repository": repo_ghe}, + ) + fake_commit_50 = MagicMock( + name="fake_commit", **{"id": 50, "repository": repo_github} + ) + fake_commit_gitlab = MagicMock( + name="fake_commit", **{"id": 12, "repository": repo_gitlab} + ) + mock_redis.get.side_effect = lambda key: redis_keys.get(key) + assert get_github_app_for_commit(fake_commit_12) == "1200" + assert get_github_app_for_commit(fake_commit_10) == "1000" + assert get_github_app_for_commit(fake_commit_50) is None + # This feature is Github-exclusive, so we skip checking for commits that are in repos of other providers + assert get_github_app_for_commit(fake_commit_gitlab) is None + + def test_get_app_for_commit_error(self, mock_redis): + repo_github = RepositoryFactory(owner__service="github") + mock_redis.get.side_effect = RedisError + fake_commit_12 = MagicMock( + name="fake_commit", **{"id": 12, "repository": repo_github} + ) + assert get_github_app_for_commit(fake_commit_12) is None + mock_redis.get.assert_called_with("app_to_use_for_commit_12") + + @pytest.mark.integration + def test_get_and_set_app_for_commit(self, dbsession): + commit = self._get_commit(dbsession) + # String + set_github_app_for_commit("12", commit) + assert get_github_app_for_commit(commit) == "12" + # Int + set_github_app_for_commit(24, commit) + assert get_github_app_for_commit(commit) == "24" diff --git a/apps/worker/services/tests/test_license.py b/apps/worker/services/tests/test_license.py new file mode 100644 index 0000000000..e0d524b7c5 --- /dev/null +++ b/apps/worker/services/tests/test_license.py @@ -0,0 +1,163 @@ +from datetime import datetime + +from database.tests.factories import OwnerFactory, RepositoryFactory +from services.license import ( + InvalidLicenseReason, + calculate_reason_for_not_being_valid, + has_valid_license, + is_properly_licensed, + requires_license, +) + + +class TestLicenseService(object): + def test_is_properly_licensed_doesnt_require_license(self, dbsession, mocker): + mocker.patch("services.license.requires_license", return_value=False) + mocker.patch("services.license.has_valid_license", return_value=False) + assert is_properly_licensed(dbsession) + + def test_is_properly_licensed_requires_license_doesnt_have_it( + self, dbsession, mocker + ): + mocker.patch("services.license.requires_license", return_value=True) + mocker.patch("services.license.has_valid_license", return_value=False) + assert not is_properly_licensed(dbsession) + + def test_is_properly_licensed_requires_license_has_it(self, dbsession, mocker): + mocker.patch("services.license.requires_license", return_value=True) + mocker.patch("services.license.has_valid_license", return_value=True) + assert is_properly_licensed(dbsession) + + def test_requires_license(self, mocker): + mocker.patch("services.license.is_enterprise", return_value=True) + assert requires_license() + mocker.patch("services.license.is_enterprise", return_value=False) + assert not requires_license() + + def test_has_valid_license(self, dbsession, mocker): + mocked_reason = mocker.patch( + "services.license.reason_for_not_being_valid", return_value=None + ) + assert has_valid_license(dbsession) + mocked_reason.assert_called_with(dbsession) + mocker.patch( + "services.license.reason_for_not_being_valid", return_value="something" + ) + assert not has_valid_license(dbsession) + mocked_reason.assert_called_with(dbsession) + + def test_calculate_reason_for_not_being_valid_no_license( + self, dbsession, mock_configuration + ): + assert ( + calculate_reason_for_not_being_valid(dbsession) + == InvalidLicenseReason.invalid + ) + + def test_calculate_reason_for_not_being_valid_bad_url( + self, dbsession, mock_configuration + ): + encrypted_license = "0dRbhbzp8TVFQp7P4e2ES9lSfyQlTo8J7LQ/N51yeAE/KcRBCnU+QsVvVMDuLL4xNGXGGk9p4ZTmIl0II3cMr0tIoPHe9Re2UjommalyFYuP8JjjnNR/Ql2DnjOzEnTzsE2Poq9xlNHcIU4F9gC2WOYPnazR6U+t4CelcvIAbEpbOMOiw34nVyd3OEmWusquMNrwkNkk/lwjwCJmj6bTXQ==" + mock_configuration.params["setup"]["enterprise_license"] = encrypted_license + mock_configuration.params["setup"]["codecov_url"] = "https://bad.site.org" + assert ( + calculate_reason_for_not_being_valid(dbsession) + == InvalidLicenseReason.url_mismatch + ) + + def test_calculate_reason_for_not_being_valid_simple_license( + self, dbsession, mock_configuration, mocker + ): + mocker.patch("services.license._get_now", return_value=datetime(2020, 4, 2)) + encrypted_license = "0dRbhbzp8TVFQp7P4e2ES9lSfyQlTo8J7LQ/N51yeAE/KcRBCnU+QsVvVMDuLL4xNGXGGk9p4ZTmIl0II3cMr0tIoPHe9Re2UjommalyFYuP8JjjnNR/Ql2DnjOzEnTzsE2Poq9xlNHcIU4F9gC2WOYPnazR6U+t4CelcvIAbEpbOMOiw34nVyd3OEmWusquMNrwkNkk/lwjwCJmj6bTXQ==" + mock_configuration.params["setup"]["enterprise_license"] = encrypted_license + mock_configuration.params["setup"]["codecov_url"] = "https://codeov.mysite.com" + assert calculate_reason_for_not_being_valid(dbsession) is None + + def test_calculate_reason_for_not_being_valid_too_many_owners( + self, dbsession, mock_configuration + ): + for i in range(11): + owner = OwnerFactory.create( + service="github", username=f"test_calculate_reason_{i}" + ) + dbsession.add(owner) + dbsession.flush() + encrypted_license = "0dRbhbzp8TVFQp7P4e2ES9lSfyQlTo8J7LQ/N51yeAE/KcRBCnU+QsVvVMDuLL4xNGXGGk9p4ZTmIl0II3cMr0tIoPHe9Re2UjommalyFYuP8JjjnNR/Ql2DnjOzEnTzsE2Poq9xlNHcIU4F9gC2WOYPnazR6U+t4CelcvIAbEpbOMOiw34nVyd3OEmWusquMNrwkNkk/lwjwCJmj6bTXQ==" + mock_configuration.params["setup"]["enterprise_license"] = encrypted_license + mock_configuration.params["setup"]["codecov_url"] = "https://codeov.mysite.com" + assert ( + calculate_reason_for_not_being_valid(dbsession) + == InvalidLicenseReason.users_exceeded + ) + + def test_calculate_reason_for_not_being_valid_too_many_plan_activated_users( + self, dbsession, mock_configuration + ): + org_owner = OwnerFactory.create( + service="github", oauth_token=None, plan_activated_users=list(range(1, 12)) + ) + dbsession.add(org_owner) + dbsession.flush() + encrypted_license = "wxWEJyYgIcFpi6nBSyKQZQeaQ9Eqpo3SXyUomAqQOzOFjdYB3A8fFM1rm+kOt2ehy9w95AzrQqrqfxi9HJIb2zLOMOB9tSy52OykVCzFtKPBNsXU/y5pQKOfV7iI3w9CHFh3tDwSwgjg8UsMXwQPOhrpvl2GdHpwEhFdaM2O3vY7iElFgZfk5D9E7qEnp+WysQwHKxDeKLI7jWCnBCBJLDjBJRSz0H7AfU55RQDqtTrnR+rsLDHOzJ80/VxwVYhb" + mock_configuration.params["setup"]["enterprise_license"] = encrypted_license + mock_configuration.params["setup"]["codecov_url"] = "https://codecov.mysite.com" + assert ( + calculate_reason_for_not_being_valid(dbsession) + == InvalidLicenseReason.users_exceeded + ) + + def test_calculate_reason_for_not_being_valid_repos_exceeded( + self, dbsession, mock_configuration + ): + # number of max repos is 20 + owner = OwnerFactory.create(service="github") + dbsession.add(owner) + dbsession.flush() + for i in range(21): + repo = RepositoryFactory.create(updatestamp=datetime.now(), owner=owner) + dbsession.add(repo) + dbsession.flush() + encrypted_license = "0dRbhbzp8TVFQp7P4e2ES9lSfyQlTo8J7LQ/N51yeAE/KcRBCnU+QsVvVMDuLL4xNGXGGk9p4ZTmIl0II3cMr0tIoPHe9Re2UjommalyFYuP8JjjnNR/Ql2DnjOzEnTzsE2Poq9xlNHcIU4F9gC2WOYPnazR6U+t4CelcvIAbEpbOMOiw34nVyd3OEmWusquMNrwkNkk/lwjwCJmj6bTXQ==" + mock_configuration.params["setup"]["enterprise_license"] = encrypted_license + mock_configuration.params["setup"]["codecov_url"] = "https://codeov.mysite.com" + assert ( + calculate_reason_for_not_being_valid(dbsession) + == InvalidLicenseReason.repos_exceeded + ) + + def test_calculate_reason_for_not_being_valid_repos_warning( + self, dbsession, mock_configuration, mocker + ): + mocker.patch("services.license._get_now", return_value=datetime(2020, 4, 2)) + # number of max repos is 20 + owner = OwnerFactory.create(service="github") + dbsession.add(owner) + dbsession.flush() + for i in range(18): + repo = RepositoryFactory.create(updatestamp=datetime.now(), owner=owner) + dbsession.add(repo) + dbsession.flush() + encrypted_license = "0dRbhbzp8TVFQp7P4e2ES9lSfyQlTo8J7LQ/N51yeAE/KcRBCnU+QsVvVMDuLL4xNGXGGk9p4ZTmIl0II3cMr0tIoPHe9Re2UjommalyFYuP8JjjnNR/Ql2DnjOzEnTzsE2Poq9xlNHcIU4F9gC2WOYPnazR6U+t4CelcvIAbEpbOMOiw34nVyd3OEmWusquMNrwkNkk/lwjwCJmj6bTXQ==" + mock_configuration.params["setup"]["enterprise_license"] = encrypted_license + mock_configuration.params["setup"]["codecov_url"] = "https://codeov.mysite.com" + assert calculate_reason_for_not_being_valid(dbsession) is None + + def test_calculate_reason_for_not_being_valid_expired( + self, dbsession, mock_configuration, mocker + ): + mocker.patch("services.license._get_now", return_value=datetime(2021, 10, 11)) + owner = OwnerFactory.create(service="github") + dbsession.add(owner) + dbsession.flush() + for i in range(18): + repo = RepositoryFactory.create(updatestamp=datetime.now(), owner=owner) + dbsession.add(repo) + dbsession.flush() + encrypted_license = "0dRbhbzp8TVFQp7P4e2ES9lSfyQlTo8J7LQ/N51yeAE/KcRBCnU+QsVvVMDuLL4xNGXGGk9p4ZTmIl0II3cMr0tIoPHe9Re2UjommalyFYuP8JjjnNR/Ql2DnjOzEnTzsE2Poq9xlNHcIU4F9gC2WOYPnazR6U+t4CelcvIAbEpbOMOiw34nVyd3OEmWusquMNrwkNkk/lwjwCJmj6bTXQ==" + mock_configuration.params["setup"]["enterprise_license"] = encrypted_license + mock_configuration.params["setup"]["codecov_url"] = "https://codeov.mysite.com" + assert ( + calculate_reason_for_not_being_valid(dbsession) + == InvalidLicenseReason.expired + ) diff --git a/apps/worker/services/tests/test_owner_service.py b/apps/worker/services/tests/test_owner_service.py new file mode 100644 index 0000000000..8ee095a126 --- /dev/null +++ b/apps/worker/services/tests/test_owner_service.py @@ -0,0 +1,167 @@ +from shared.rate_limits import gh_app_key_name, owner_key_name +from shared.reports.types import UploadType +from shared.typings.torngit import AdditionalData + +from database.models.core import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, +) +from database.tests.factories import OwnerFactory +from services.owner import get_owner_provider_service + + +class TestOwnerServiceTestCase(object): + def test_get_owner_provider_service(self, dbsession): + owner = OwnerFactory.create( + service="github", + unencrypted_oauth_token="bcaa0dc0c66b4a8c8c65ac919a1a91aa", + bot=None, + ) + dbsession.add(owner) + dbsession.flush() + res = get_owner_provider_service(owner) + expected_data = { + "owner": { + "ownerid": owner.ownerid, + "service_id": owner.service_id, + "username": owner.username, + }, + "repo": {}, + "installation": None, + "fallback_installations": None, + "additional_data": {}, + } + assert res.service == "github" + assert res.data == expected_data + assert res.token == { + "key": "bcaa0dc0c66b4a8c8c65ac919a1a91aa", + "secret": None, + "entity_name": owner_key_name(owner.ownerid), + } + + def test_get_owner_provider_service_with_installation(self, dbsession, mocker): + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + return_value="integration_token", + ) + owner = OwnerFactory.create( + service="github", + unencrypted_oauth_token="bcaa0dc0c66b4a8c8c65ac919a1a91aa", + bot=None, + ) + dbsession.add(owner) + installation = GithubAppInstallation( + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + installation_id=1500, + repository_service_ids=None, + owner=owner, + ) + dbsession.add(installation) + dbsession.flush() + res = get_owner_provider_service(owner) + expected_data = { + "owner": { + "ownerid": owner.ownerid, + "service_id": owner.service_id, + "username": owner.username, + }, + "repo": {}, + "installation": { + "id": installation.id, + "installation_id": 1500, + "pem_path": None, + "app_id": None, + }, + "fallback_installations": [], + "additional_data": {}, + } + assert res.service == "github" + assert res.data == expected_data + assert res.token == { + "key": "integration_token", + "username": "installation_1500", + "entity_name": gh_app_key_name( + installation_id=installation.installation_id, app_id=None + ), + } + + def test_get_owner_provider_service_other_service(self, dbsession): + owner = OwnerFactory.create( + service="gitlab", unencrypted_oauth_token="testenll80qbqhofao65", bot=None + ) + dbsession.add(owner) + dbsession.flush() + res = get_owner_provider_service(owner) + expected_data = { + "owner": { + "ownerid": owner.ownerid, + "service_id": owner.service_id, + "username": owner.username, + }, + "repo": {}, + "installation": None, + "fallback_installations": None, + "additional_data": {}, + } + assert res.service == "gitlab" + assert res.data == expected_data + assert res.token == { + "key": "testenll80qbqhofao65", + "secret": None, + "entity_name": owner_key_name(owner.ownerid), + } + + def test_get_owner_provider_service_different_bot(self, dbsession): + bot_token = "bcaa0dc0c66b4a8c8c65ac919a1a91aa" + owner = OwnerFactory.create( + unencrypted_oauth_token="testyftq3ovzkb3zmt823u3t04lkrt9w", + bot=OwnerFactory.create(unencrypted_oauth_token=bot_token), + ) + dbsession.add(owner) + dbsession.flush() + res = get_owner_provider_service(owner, ignore_installation=True) + expected_data = { + "owner": { + "ownerid": owner.ownerid, + "service_id": owner.service_id, + "username": owner.username, + }, + "repo": {}, + "installation": None, + "fallback_installations": None, + "additional_data": {}, + } + assert res.data["repo"] == expected_data["repo"] + assert res.data == expected_data + assert res.token == { + "key": bot_token, + "secret": None, + "entity_name": owner_key_name(owner.bot.ownerid), + } + + def test_get_owner_provider_service_additional_data(self, dbsession): + owner = OwnerFactory.create( + service="gitlab", unencrypted_oauth_token="testenll80qbqhofao65", bot=None + ) + dbsession.add(owner) + dbsession.flush() + additional_data: AdditionalData = {"upload_type": UploadType.BUNDLE_ANALYSIS} + res = get_owner_provider_service(owner, additional_data=additional_data) + expected_data = { + "owner": { + "ownerid": owner.ownerid, + "service_id": owner.service_id, + "username": owner.username, + }, + "repo": {}, + "installation": None, + "fallback_installations": None, + "additional_data": {"upload_type": UploadType.BUNDLE_ANALYSIS}, + } + assert res.service == "gitlab" + assert res.data == expected_data + assert res.token == { + "key": "testenll80qbqhofao65", + "secret": None, + "entity_name": owner_key_name(owner.ownerid), + } diff --git a/apps/worker/services/tests/test_processing_state.py b/apps/worker/services/tests/test_processing_state.py new file mode 100644 index 0000000000..69abe5bb38 --- /dev/null +++ b/apps/worker/services/tests/test_processing_state.py @@ -0,0 +1,71 @@ +from uuid import uuid4 + +from services.processing.state import ( + ProcessingState, + should_perform_merge, + should_trigger_postprocessing, +) + + +def test_single_upload(): + state = ProcessingState(1234, uuid4().hex) + state.mark_uploads_as_processing([1]) + + state.mark_upload_as_processed(1) + + # this is the only in-progress upload, nothing more to expect + assert should_perform_merge(state.get_upload_numbers()) + + assert state.get_uploads_for_merging() == {1} + state.mark_uploads_as_merged([1]) + + assert should_trigger_postprocessing(state.get_upload_numbers()) + + +def test_concurrent_uploads(): + state = ProcessingState(1234, uuid4().hex) + state.mark_uploads_as_processing([1]) + + state.mark_upload_as_processed(1) + # meanwhile, another upload comes in: + state.mark_uploads_as_processing([2]) + + # not merging/postprocessing yet, as that will be debounced with the second upload + assert not should_perform_merge(state.get_upload_numbers()) + + state.mark_upload_as_processed(2) + + assert should_perform_merge(state.get_upload_numbers()) + + assert state.get_uploads_for_merging() == {1, 2} + state.mark_uploads_as_merged([1, 2]) + + assert should_trigger_postprocessing(state.get_upload_numbers()) + + +def test_batch_merging_many_uploads(): + state = ProcessingState(1234, uuid4().hex) + + state.mark_uploads_as_processing([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) + + for id in range(1, 12): + state.mark_upload_as_processed(id) + + # we have only processed 8 out of 9. we want to do a batched merge + assert should_perform_merge(state.get_upload_numbers()) + merging = state.get_uploads_for_merging() + assert len(merging) == 10 # = MERGE_BATCH_SIZE + state.mark_uploads_as_merged(merging) + + # but no notifications yet + assert not should_trigger_postprocessing(state.get_upload_numbers()) + + state.mark_upload_as_processed(12) + + # with the last upload being processed, we do another merge, and then trigger notifications + assert should_perform_merge(state.get_upload_numbers()) + merging = state.get_uploads_for_merging() + assert len(merging) == 2 + state.mark_uploads_as_merged(merging) + + assert should_trigger_postprocessing(state.get_upload_numbers()) diff --git a/apps/worker/services/tests/test_redis.py b/apps/worker/services/tests/test_redis.py new file mode 100644 index 0000000000..94f4206ef5 --- /dev/null +++ b/apps/worker/services/tests/test_redis.py @@ -0,0 +1,8 @@ +from shared.helpers.redis import get_redis_connection + + +def test_get_redis_connection(mocker): + mocked = mocker.patch("shared.helpers.redis.Redis.from_url") + res = get_redis_connection() + assert res is not None + mocked.assert_called_with("redis://redis:6379") diff --git a/apps/worker/services/tests/test_report.py b/apps/worker/services/tests/test_report.py new file mode 100644 index 0000000000..41989e3871 --- /dev/null +++ b/apps/worker/services/tests/test_report.py @@ -0,0 +1,3895 @@ +from decimal import Decimal + +import mock +import pytest +from celery.exceptions import SoftTimeLimitExceeded +from shared.reports.resources import Report, ReportFile, Session, SessionType +from shared.reports.types import ReportLine, ReportTotals +from shared.torngit.exceptions import TorngitRateLimitError +from shared.yaml import UserYaml + +from database.models import CommitReport, RepositoryFlag, Upload +from database.tests.factories import CommitFactory +from helpers.exceptions import RepositoryWithoutValidBotError +from services.archive import ArchiveService +from services.report import NotReadyToBuildReportYetError, ReportService +from services.report import log as report_log +from services.report.raw_upload_processor import ( + SessionAdjustmentResult, + clear_carryforward_sessions, +) +from test_utils.base import BaseTestCase + + +@pytest.fixture +def sample_report(): + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1], [1, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1], [1, 0]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session( + Session( + flags=["unit"], + provider="circleci", + session_type=SessionType.uploaded, + build="aycaramba", + totals=ReportTotals(2, 10), + ) + ) + report.add_session( + Session( + flags=["integration"], + provider="travis", + session_type=SessionType.carriedforward, + build="poli", + ) + ) + return report + + +@pytest.fixture +def sample_commit_with_report_big(dbsession, mock_storage): + sessions_dict = { + "0": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": [], + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "t": None, + "u": None, + }, + "1": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["unit"], + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "t": None, + "u": None, + }, + "2": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["enterprise"], + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "t": None, + "u": None, + }, + "3": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["unit", "enterprise"], + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "t": None, + "u": None, + }, + } + file_headers = { + "file_00.py": [ + 0, + [0, 14, 12, 0, 2, "85.71429", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_01.py": [ + 1, + [0, 11, 8, 0, 3, "72.72727", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_02.py": [ + 2, + [0, 13, 9, 0, 4, "69.23077", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_03.py": [ + 3, + [0, 16, 8, 0, 8, "50.00000", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_04.py": [ + 4, + [0, 10, 6, 0, 4, "60.00000", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_05.py": [ + 5, + [0, 14, 10, 0, 4, "71.42857", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_06.py": [ + 6, + [0, 9, 7, 1, 1, "77.77778", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_07.py": [ + 7, + [0, 11, 9, 0, 2, "81.81818", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_08.py": [ + 8, + [0, 11, 6, 0, 5, "54.54545", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_09.py": [ + 9, + [0, 14, 10, 1, 3, "71.42857", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_10.py": [ + 10, + [0, 10, 6, 1, 3, "60.00000", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_11.py": [ + 11, + [0, 23, 15, 1, 7, "65.21739", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_12.py": [ + 12, + [0, 14, 8, 0, 6, "57.14286", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_13.py": [ + 13, + [0, 15, 9, 0, 6, "60.00000", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_14.py": [ + 14, + [0, 23, 13, 0, 10, "56.52174", 0, 0, 0, 0, 0, 0, 0], + None, + ], + } + commit = CommitFactory.create( + _report_json={"sessions": sessions_dict, "files": file_headers} + ) + dbsession.add(commit) + dbsession.flush() + with open("tasks/tests/samples/sample_chunks_4_sessions.txt") as f: + content = f.read().encode() + archive_hash = ArchiveService.get_archive_hash(commit.repository) + chunks_url = f"v4/repos/{archive_hash}/commits/{commit.commitid}/chunks.txt" + mock_storage.write_file("archive", chunks_url, content) + return commit + + +@pytest.fixture +def sample_commit_with_report_big_with_labels(dbsession, mock_storage): + sessions_dict = { + "0": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["enterprise"], + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "t": None, + "u": None, + }, + } + file_headers = { + "file_00.py": [ + 0, + [0, 4, 0, 4, 0, "0", 0, 0, 0, 0, 0, 0, 0], + [[0, 4, 0, 4, 0, "0", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "file_01.py": [ + 1, + [0, 32, 32, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + [[0, 32, 32, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + } + commit = CommitFactory.create( + _report_json={"sessions": sessions_dict, "files": file_headers} + ) + dbsession.add(commit) + dbsession.flush() + with open("tasks/tests/samples/sample_chunks_with_header.txt") as f: + content = f.read().encode() + archive_hash = ArchiveService.get_archive_hash(commit.repository) + chunks_url = f"v4/repos/{archive_hash}/commits/{commit.commitid}/chunks.txt" + mock_storage.write_file("archive", chunks_url, content) + return commit + + +@pytest.fixture +def sample_commit_with_report_big_already_carriedforward(dbsession, mock_storage): + sessions_dict = { + "0": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": [], + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "t": None, + "u": None, + }, + "1": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["unit"], + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "t": None, + "u": None, + }, + "2": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["enterprise"], + "j": None, + "n": None, + "p": None, + "st": "carriedforward", + "t": None, + "u": None, + }, + "3": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["unit", "enterprise"], + "j": None, + "n": None, + "p": None, + "st": "carriedforward", + "t": None, + "u": None, + }, + } + file_headers = { + "file_00.py": [ + 0, + [0, 14, 12, 0, 2, "85.71429", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_01.py": [ + 1, + [0, 11, 8, 0, 3, "72.72727", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_10.py": [ + 10, + [0, 10, 6, 1, 3, "60.00000", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_11.py": [ + 11, + [0, 23, 15, 1, 7, "65.21739", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_12.py": [ + 12, + [0, 14, 8, 0, 6, "57.14286", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_13.py": [ + 13, + [0, 15, 9, 0, 6, "60.00000", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_14.py": [ + 14, + [0, 23, 13, 0, 10, "56.52174", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_02.py": [ + 2, + [0, 13, 9, 0, 4, "69.23077", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_03.py": [ + 3, + [0, 16, 8, 0, 8, "50.00000", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_04.py": [ + 4, + [0, 10, 6, 0, 4, "60.00000", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_05.py": [ + 5, + [0, 14, 10, 0, 4, "71.42857", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_06.py": [ + 6, + [0, 9, 7, 1, 1, "77.77778", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_07.py": [ + 7, + [0, 11, 9, 0, 2, "81.81818", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_08.py": [ + 8, + [0, 11, 6, 0, 5, "54.54545", 0, 0, 0, 0, 0, 0, 0], + None, + ], + "file_09.py": [ + 9, + [0, 14, 10, 1, 3, "71.42857", 0, 0, 0, 0, 0, 0, 0], + None, + ], + } + commit = CommitFactory.create( + _report_json={"sessions": sessions_dict, "files": file_headers} + ) + dbsession.add(commit) + dbsession.flush() + with open("tasks/tests/samples/sample_chunks_4_sessions.txt") as f: + content = f.read().encode() + archive_hash = ArchiveService.get_archive_hash(commit.repository) + chunks_url = f"v4/repos/{archive_hash}/commits/{commit.commitid}/chunks.txt" + mock_storage.write_file("archive", chunks_url, content) + return commit + + +class TestReportService(BaseTestCase): + @pytest.mark.django_db(databases={"default", "timeseries"}) + def test_create_new_report_for_commit( + self, + dbsession, + sample_commit_with_report_big, + mock_storage, + ): + parent_commit = sample_commit_with_report_big + commit = CommitFactory.create( + repository=parent_commit.repository, + parent_commit_id=parent_commit.commitid, + _report_json=None, + ) + dbsession.add(commit) + dbsession.flush() + dbsession.add(CommitReport(commit_id=commit.id_)) + dbsession.flush() + yaml_dict = {"flags": {"enterprise": {"carryforward": True}}} + report = ReportService(UserYaml(yaml_dict)).create_new_report_for_commit(commit) + assert report is not None + assert sorted(report.files) == sorted( + [ + "file_00.py", + "file_01.py", + "file_02.py", + "file_03.py", + "file_04.py", + "file_05.py", + "file_06.py", + "file_07.py", + "file_08.py", + "file_09.py", + "file_10.py", + "file_11.py", + "file_12.py", + "file_13.py", + "file_14.py", + ] + ) + assert report.totals == ReportTotals( + files=15, + lines=188, + hits=68, + misses=26, + partials=94, + coverage="36.17021", + branches=0, + methods=0, + messages=0, + sessions=2, + complexity=0, + complexity_total=0, + diff=0, + ) + readable_report = self.convert_report_to_better_readable(report) + expected_results = { + "archive": { + "file_00.py": [ + (1, 1, None, [[2, 1, None, None, None]], None, None), + (2, 1, None, [[2, 1, None, None, None]], None, None), + (3, "1/3", None, [[2, "1/3", None, None, None]], None, None), + (4, "1/2", None, [[3, "1/2", None, None, None]], None, None), + (5, 0, None, [[3, 0, None, None, None]], None, None), + (6, 0, None, [[2, 0, None, None, None]], None, None), + (7, 0, None, [[3, 0, None, None, None]], None, None), + (8, 0, None, [[3, 0, None, None, None]], None, None), + ( + 9, + "1/3", + None, + [[3, 0, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + (10, 0, None, [[2, 0, None, None, None]], None, None), + (11, "1/2", None, [[2, "1/2", None, None, None]], None, None), + ( + 12, + "2/2", + None, + [[2, 1, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + ( + 13, + "2/2", + None, + [[3, 1, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + ( + 14, + "1/3", + None, + [[3, 0, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ], + "file_01.py": [ + ( + 2, + "1/3", + None, + [[2, 0, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + (3, "1/2", None, [[3, "1/2", None, None, None]], None, None), + (4, "1/2", None, [[3, "1/2", None, None, None]], None, None), + ( + 5, + "1/3", + None, + [[2, 0, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 6, + "1/3", + None, + [[3, "1/2", None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 7, + "1/3", + None, + [[3, "1/2", None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + (8, 1, None, [[2, 1, None, None, None]], None, None), + (9, 1, None, [[2, 1, None, None, None]], None, None), + ( + 10, + "1/2", + None, + [[3, 0, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + ( + 11, + 1, + None, + [[3, 0, None, None, None], [2, 1, None, None, None]], + None, + None, + ), + ], + "file_02.py": [ + (1, 1, None, [[2, 1, None, None, None]], None, None), + (2, "1/3", None, [[3, "1/3", None, None, None]], None, None), + ( + 4, + "1/2", + None, + [[3, 0, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + (5, 1, None, [[3, 1, None, None, None]], None, None), + (6, "1/3", None, [[2, "1/3", None, None, None]], None, None), + (8, 1, None, [[2, 1, None, None, None]], None, None), + ( + 9, + "3/3", + None, + [[3, 1, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 10, + "1/3", + None, + [[3, 0, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + (11, "1/2", None, [[2, "1/2", None, None, None]], None, None), + ( + 12, + "2/2", + None, + [[2, 1, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + ( + 13, + "1/3", + None, + [[3, 0, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ], + "file_03.py": [ + ( + 2, + 1, + None, + [[3, 0, None, None, None], [2, 1, None, None, None]], + None, + None, + ), + (3, "1/2", None, [[3, "1/2", None, None, None]], None, None), + (4, 0, None, [[3, 0, None, None, None]], None, None), + (5, "1/3", None, [[2, "1/3", None, None, None]], None, None), + (6, "1/3", None, [[3, "1/3", None, None, None]], None, None), + ( + 7, + "2/2", + None, + [[3, 1, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + (8, 0, None, [[3, 0, None, None, None]], None, None), + (9, "1/3", None, [[3, "1/3", None, None, None]], None, None), + (10, "1/3", None, [[2, "1/3", None, None, None]], None, None), + (11, "1/2", None, [[2, "1/2", None, None, None]], None, None), + (12, "1/2", None, [[3, "1/2", None, None, None]], None, None), + ( + 13, + "1/3", + None, + [[2, 0, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + (14, "1/2", None, [[3, "1/2", None, None, None]], None, None), + ( + 15, + "3/3", + None, + [[2, 1, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 16, + "2/2", + None, + [[2, 1, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + ], + "file_04.py": [ + (1, "1/3", None, [[2, "1/3", None, None, None]], None, None), + (2, 0, None, [[3, 0, None, None, None]], None, None), + (3, "1/2", None, [[2, "1/2", None, None, None]], None, None), + (4, "1/2", None, [[2, "1/2", None, None, None]], None, None), + ( + 5, + "2/2", + None, + [[3, 1, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + (6, "1/2", None, [[3, "1/2", None, None, None]], None, None), + ( + 7, + 1, + None, + [[3, 0, None, None, None], [2, 1, None, None, None]], + None, + None, + ), + ( + 8, + "3/3", + None, + [[2, 1, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + (9, "1/3", None, [[2, "1/3", None, None, None]], None, None), + (10, "1/2", None, [[2, "1/2", None, None, None]], None, None), + ], + "file_05.py": [ + (2, 0, None, [[2, 0, None, None, None]], None, None), + (3, "1/2", None, [[2, "1/2", None, None, None]], None, None), + (4, 0, None, [[3, 0, None, None, None]], None, None), + (5, "1/3", None, [[3, "1/3", None, None, None]], None, None), + ( + 6, + "3/3", + None, + [[3, 1, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + (7, "1/3", None, [[3, "1/3", None, None, None]], None, None), + ( + 8, + "2/2", + None, + [[2, 1, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + (9, "1/3", None, [[2, "1/3", None, None, None]], None, None), + ( + 10, + "1/3", + None, + [[2, 0, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 11, + "3/3", + None, + [[2, 1, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 12, + "1/3", + None, + [[2, "1/2", None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 13, + "1/3", + None, + [[3, "1/2", None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 14, + "1/2", + None, + [[2, 0, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + ], + "file_06.py": [ + (3, "1/2", None, [[3, "1/2", None, None, None]], None, None), + (4, 1, None, [[3, 1, None, None, None]], None, None), + (5, 1, None, [[3, 1, None, None, None]], None, None), + (6, 1, None, [[2, 1, None, None, None]], None, None), + (7, 1, None, [[3, 1, None, None, None]], None, None), + ( + 8, + "2/2", + None, + [[2, 1, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + ( + 9, + "1/2", + None, + [[3, 0, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + ], + "file_07.py": [ + (1, 1, None, [[3, 1, None, None, None]], None, None), + ( + 2, + 1, + None, + [[2, 0, None, None, None], [3, 1, None, None, None]], + None, + None, + ), + (3, 1, None, [[2, 1, None, None, None]], None, None), + ( + 4, + "1/2", + None, + [[2, "1/2", None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 5, + "2/2", + None, + [[3, 1, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + (6, 0, None, [[2, 0, None, None, None]], None, None), + (7, "1/3", None, [[3, "1/3", None, None, None]], None, None), + ( + 8, + "1/3", + None, + [[2, "1/2", None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + (9, "1/3", None, [[2, "1/3", None, None, None]], None, None), + ( + 10, + "3/3", + None, + [[2, 1, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 11, + "1/2", + None, + [[2, 0, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + ], + "file_08.py": [ + (1, 0, None, [[3, 0, None, None, None]], None, None), + (2, 0, None, [[2, 0, None, None, None]], None, None), + (3, 0, None, [[2, 0, None, None, None]], None, None), + (4, "1/3", None, [[2, "1/3", None, None, None]], None, None), + (5, "1/2", None, [[3, "1/2", None, None, None]], None, None), + (6, 0, None, [[2, 0, None, None, None]], None, None), + ( + 7, + 1, + None, + [[2, 0, None, None, None], [3, 1, None, None, None]], + None, + None, + ), + ( + 8, + 1, + None, + [[3, 0, None, None, None], [2, 1, None, None, None]], + None, + None, + ), + (9, "1/2", None, [[3, "1/2", None, None, None]], None, None), + ( + 10, + "1/3", + None, + [[3, "1/2", None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 11, + "1/3", + None, + [[2, 0, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ], + "file_09.py": [ + (1, 0, None, [[2, 0, None, None, None]], None, None), + (3, "1/3", None, [[3, "1/3", None, None, None]], None, None), + ( + 6, + "3/3", + None, + [[2, 1, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + (7, "1/2", None, [[2, "1/2", None, None, None]], None, None), + (8, "1/2", None, [[2, "1/2", None, None, None]], None, None), + (9, 1, None, [[2, 1, None, None, None]], None, None), + ( + 10, + 1, + None, + [[2, 0, None, None, None], [3, 1, None, None, None]], + None, + None, + ), + (11, "1/3", None, [[2, "1/3", None, None, None]], None, None), + (12, "1/3", None, [[3, "1/3", None, None, None]], None, None), + ( + 13, + 1, + None, + [[2, 0, None, None, None], [3, 1, None, None, None]], + None, + None, + ), + ( + 14, + 1, + None, + [[3, 0, None, None, None], [2, 1, None, None, None]], + None, + None, + ), + ], + "file_10.py": [ + (2, 1, None, [[3, 1, None, None, None]], None, None), + ( + 3, + "1/2", + None, + [[2, 0, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + (4, "1/2", None, [[2, "1/2", None, None, None]], None, None), + ( + 6, + "1/2", + None, + [[2, 0, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + (7, 1, None, [[3, 1, None, None, None]], None, None), + ( + 8, + "1/2", + None, + [[3, 0, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + ( + 9, + "1/3", + None, + [[2, "1/2", None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 10, + "3/3", + None, + [[3, 1, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ], + "file_11.py": [ + (1, 0, None, [[3, 0, None, None, None]], None, None), + (3, "1/2", None, [[2, "1/2", None, None, None]], None, None), + (4, "1/2", None, [[3, "1/2", None, None, None]], None, None), + (5, 0, None, [[2, 0, None, None, None]], None, None), + (6, 0, None, [[3, 0, None, None, None]], None, None), + (7, "1/3", None, [[2, "1/3", None, None, None]], None, None), + (8, 1, None, [[2, 1, None, None, None]], None, None), + (9, "1/2", None, [[2, "1/2", None, None, None]], None, None), + (10, 1, None, [[3, 1, None, None, None]], None, None), + ( + 11, + "2/2", + None, + [[2, 1, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + (12, 1, None, [[3, 1, None, None, None]], None, None), + ( + 13, + "1/2", + None, + [[3, 0, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + ( + 14, + "1/2", + None, + [[3, 0, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + (15, 0, None, [[2, 0, None, None, None]], None, None), + ( + 16, + 1, + None, + [[2, 0, None, None, None], [3, 1, None, None, None]], + None, + None, + ), + ( + 17, + "1/3", + None, + [[3, "1/2", None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 18, + "1/2", + None, + [[2, 0, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + (19, 0, None, [[3, 0, None, None, None]], None, None), + (20, 1, None, [[3, 1, None, None, None]], None, None), + ( + 21, + "2/2", + None, + [[3, 1, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + ( + 22, + "3/3", + None, + [[3, 1, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 23, + "1/3", + None, + [[2, 0, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ], + "file_12.py": [ + (2, "1/2", None, [[3, "1/2", None, None, None]], None, None), + (3, "1/3", None, [[3, "1/3", None, None, None]], None, None), + (4, 0, None, [[2, 0, None, None, None]], None, None), + (5, 0, None, [[3, 0, None, None, None]], None, None), + (7, 1, None, [[3, 1, None, None, None]], None, None), + ( + 8, + "1/2", + None, + [[3, "1/2", None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 9, + "1/2", + None, + [[2, 0, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + (10, 0, None, [[3, 0, None, None, None]], None, None), + (11, "1/3", None, [[3, "1/3", None, None, None]], None, None), + ( + 12, + "3/3", + None, + [[3, 1, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 13, + "3/3", + None, + [[2, 1, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 14, + "2/2", + None, + [[3, 1, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + ], + "file_13.py": [ + (2, 1, None, [[3, 1, None, None, None]], None, None), + ( + 6, + 1, + None, + [[3, 0, None, None, None], [2, 1, None, None, None]], + None, + None, + ), + (7, "1/3", None, [[2, "1/3", None, None, None]], None, None), + ( + 8, + "3/3", + None, + [[2, 1, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 9, + 1, + None, + [[3, 0, None, None, None], [2, 1, None, None, None]], + None, + None, + ), + ( + 10, + "1/3", + None, + [[2, "1/2", None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 11, + "1/3", + None, + [[2, 0, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 12, + "1/3", + None, + [[2, "1/2", None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + (13, "1/2", None, [[3, "1/2", None, None, None]], None, None), + (14, 1, None, [[3, 1, None, None, None]], None, None), + ( + 15, + "2/2", + None, + [[3, 1, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + ], + "file_14.py": [ + (1, 1, None, [[2, 1, None, None, None]], None, None), + (2, 0, None, [[2, 0, None, None, None]], None, None), + ( + 3, + "1/3", + None, + [[3, 0, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 5, + "2/2", + None, + [[2, 1, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + (6, "1/3", None, [[3, "1/3", None, None, None]], None, None), + (7, 1, None, [[2, 1, None, None, None]], None, None), + (8, "1/3", None, [[2, "1/3", None, None, None]], None, None), + (9, "1/2", None, [[2, "1/2", None, None, None]], None, None), + (10, 1, None, [[2, 1, None, None, None]], None, None), + ( + 11, + "3/3", + None, + [[3, 1, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 12, + 1, + None, + [[2, 0, None, None, None], [3, 1, None, None, None]], + None, + None, + ), + (13, "1/3", None, [[3, "1/3", None, None, None]], None, None), + (14, "1/3", None, [[3, "1/3", None, None, None]], None, None), + (15, 0, None, [[2, 0, None, None, None]], None, None), + ( + 16, + "1/2", + None, + [[2, 0, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + ( + 17, + "1/3", + None, + [[3, 0, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 18, + "1/3", + None, + [[3, 0, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 19, + "1/2", + None, + [[3, 0, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + ( + 20, + "3/3", + None, + [[3, 1, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 21, + "1/3", + None, + [[2, "1/2", None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 22, + "1/3", + None, + [[3, "1/2", None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 23, + 1, + None, + [[2, 0, None, None, None], [3, 1, None, None, None]], + None, + None, + ), + ], + }, + "report": { + "files": { + "file_00.py": [ + 0, + [0, 14, 4, 5, 5, "28.57143", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_01.py": [ + 1, + [0, 10, 3, 0, 7, "30.00000", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_02.py": [ + 2, + [0, 11, 5, 0, 6, "45.45455", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_03.py": [ + 3, + [0, 15, 4, 2, 9, "26.66667", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_04.py": [ + 4, + [0, 10, 3, 1, 6, "30.00000", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_05.py": [ + 5, + [0, 13, 3, 2, 8, "23.07692", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_06.py": [ + 6, + [0, 7, 5, 0, 2, "71.42857", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_07.py": [ + 7, + [0, 11, 5, 1, 5, "45.45455", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_08.py": [ + 8, + [0, 11, 2, 4, 5, "18.18182", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_09.py": [ + 9, + [0, 11, 5, 1, 5, "45.45455", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_10.py": [ + 10, + [0, 8, 3, 0, 5, "37.50000", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_11.py": [ + 11, + [0, 22, 8, 5, 9, "36.36364", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_12.py": [ + 12, + [0, 12, 4, 3, 5, "33.33333", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_13.py": [ + 13, + [0, 11, 6, 0, 5, "54.54545", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_14.py": [ + 14, + [0, 22, 8, 2, 12, "36.36364", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + }, + "sessions": { + "2": { + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["enterprise"], + "j": None, + "N": "Carriedforward", + "n": None, + "p": None, + "se": {"carriedforward_from": parent_commit.commitid}, + "st": "carriedforward", + "t": None, + "u": None, + }, + "3": { + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["unit", "enterprise"], + "j": None, + "N": "Carriedforward", + "n": None, + "p": None, + "se": {"carriedforward_from": parent_commit.commitid}, + "st": "carriedforward", + "t": None, + "u": None, + }, + }, + }, + "totals": { + "b": 0, + "c": "36.17021", + "C": 0, + "d": 0, + "diff": None, + "f": 15, + "h": 68, + "M": 0, + "m": 26, + "N": 0, + "n": 188, + "p": 94, + "s": 2, + }, + } + assert ( + expected_results["report"]["sessions"]["2"] + == readable_report["report"]["sessions"]["2"] + ) + assert ( + expected_results["report"]["sessions"]["3"] + == readable_report["report"]["sessions"]["3"] + ) + assert ( + expected_results["report"]["sessions"] + == readable_report["report"]["sessions"] + ) + assert expected_results["report"]["files"] == readable_report["report"]["files"] + assert expected_results["report"] == readable_report["report"] + assert expected_results == readable_report + + @pytest.mark.django_db(databases={"default", "timeseries"}) + def test_create_new_report_for_commit_with_labels( + self, dbsession, sample_commit_with_report_big_with_labels + ): + parent_commit = sample_commit_with_report_big_with_labels + commit = CommitFactory.create( + repository=parent_commit.repository, + parent_commit_id=parent_commit.commitid, + _report_json=None, + ) + dbsession.add(commit) + dbsession.flush() + dbsession.add(CommitReport(commit_id=commit.id_)) + dbsession.flush() + yaml_dict = {"flags": {"enterprise": {"carryforward": True}}} + report = ReportService(UserYaml(yaml_dict)).create_new_report_for_commit(commit) + assert report is not None + assert report.labels_index == { + 0: "Th2dMtk4M_codecov", + 1: "core/tests/test_menu_interface.py::TestMenuInterface::test_init", + 2: "core/tests/test_main.py::TestMainMenu::test_init_values", + 3: "core/tests/test_main.py::TestMainMenu::test_invalid_menu_choice", + 4: "core/tests/test_menu_interface.py::TestMenuInterface::test_menu_options", + 5: "core/tests/test_menu_interface.py::TestMenuInterface::test_set_loop", + 6: "core/tests/test_main.py::TestMainMenu::test_menu_choice_emotions", + 7: "core/tests/test_menu_interface.py::TestMenuInterface::test_name", + 8: "core/tests/test_menu_interface.py::TestMenuInterface::test_parent", + 9: "core/tests/test_main.py::TestMainMenu::test_menu_choice_fruits", + 10: "core/tests/test_main.py::TestMainMenu::test_menu_options", + } + assert sorted(report.files) == sorted( + [ + "file_00.py", + "file_01.py", + ] + ) + assert report.totals == ReportTotals( + files=2, + lines=36, + hits=32, + misses=4, + partials=0, + coverage="88.88889", + branches=0, + methods=0, + messages=0, + sessions=1, + complexity=0, + complexity_total=0, + diff=0, + ) + readable_report = self.convert_report_to_better_readable(report) + expected_results = { + "archive": { + "file_00.py": [ + ( + 1, + 0, + None, + [[0, 0, None, None, None]], + None, + None, + [(0, 0, None, [])], + ), + ( + 3, + 0, + None, + [[0, 0, None, None, None]], + None, + None, + [(0, 0, None, [])], + ), + ( + 4, + 0, + None, + [[0, 0, None, None, None]], + None, + None, + [(0, 0, None, [])], + ), + ( + 5, + 0, + None, + [[0, 0, None, None, None]], + None, + None, + [(0, 0, None, [])], + ), + ], + "file_01.py": [ + ( + 1, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 2, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 5, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 6, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 7, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 8, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 9, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 12, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 13, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 14, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 16, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 17, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])], + ), + ( + 18, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])], + ), + ( + 19, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])], + ), + ( + 21, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 22, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 23, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [1, 3, 5, 6, 9])], + ), + ( + 25, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 26, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 27, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [1, 2, 8])], + ), + ( + 29, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 30, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 31, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [1, 2, 7])], + ), + ( + 33, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 34, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [3, 5, 6, 9])], + ), + ( + 36, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 37, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 38, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [3, 4, 6, 9, 10])], + ), + ( + 39, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [4])], + ), + ( + 41, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [3, 4, 6, 9, 10])], + ), + ( + 43, + 1, + None, + [[0, 1, None, None, None]], + None, + None, + [(0, 1, None, [0])], + ), + ( + 44, + 0, + None, + [[0, 0, None, None, None]], + None, + None, + [(0, 0, None, [])], + ), + ], + }, + "report": { + "files": { + "file_00.py": [ + 0, + [0, 4, 0, 4, 0, "0", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_01.py": [ + 1, + [0, 32, 32, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + }, + "sessions": { + "0": { + "N": "Carriedforward", + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["enterprise"], + "j": None, + "n": None, + "p": None, + "se": {"carriedforward_from": parent_commit.commitid}, + "st": "carriedforward", + "t": None, + "u": None, + } + }, + }, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "88.88889", + "d": 0, + "diff": None, + "f": 2, + "h": 32, + "m": 4, + "n": 36, + "p": 0, + "s": 1, + }, + } + assert expected_results["report"]["files"] == readable_report["report"]["files"] + assert expected_results["report"] == readable_report["report"] + assert expected_results == readable_report + + @pytest.mark.django_db(databases={"default", "timeseries"}) + def test_build_report_from_commit_carriedforward_add_sessions( + self, dbsession, sample_commit_with_report_big, mocker + ): + parent_commit = sample_commit_with_report_big + commit = CommitFactory.create( + repository=parent_commit.repository, + parent_commit_id=parent_commit.commitid, + _report_json=None, + ) + dbsession.add(commit) + dbsession.flush() + dbsession.add(CommitReport(commit_id=commit.id_)) + dbsession.flush() + yaml_dict = {"flags": {"enterprise": {"carryforward": True}}} + + def fake_possibly_shift(report, base, head): + return report + + mock_possibly_shift = mocker.patch.object( + ReportService, + "_possibly_shift_carryforward_report", + side_effect=fake_possibly_shift, + ) + report = ReportService(UserYaml(yaml_dict)).create_new_report_for_commit(commit) + assert report is not None + assert len(report.files) == 15 + mock_possibly_shift.assert_called() + to_merge_session = Session(flags=["enterprise"]) + report.add_session(to_merge_session) + assert sorted(report.sessions.keys()) == [2, 3, 4] + assert clear_carryforward_sessions( + report, Report(), ["enterprise"], UserYaml(yaml_dict) + ) == SessionAdjustmentResult( + fully_deleted_sessions=[2, 3], partially_deleted_sessions=[] + ) + assert sorted(report.sessions.keys()) == [4] + readable_report = self.convert_report_to_better_readable(report) + expected_results = { + "archive": {}, + "report": { + "files": {}, + "sessions": { + "4": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["enterprise"], + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "se": {}, + "t": None, + "u": None, + } + }, + }, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": None, + "d": 0, + "diff": None, + "f": 0, + "h": 0, + "m": 0, + "n": 0, + "p": 0, + "s": 1, + }, + } + assert ( + readable_report["report"]["sessions"] + == expected_results["report"]["sessions"] + ) + assert ( + readable_report["report"]["sessions"] + == expected_results["report"]["sessions"] + ) + assert readable_report["report"] == expected_results["report"] + assert readable_report == expected_results + + def test_get_existing_report_for_commit_already_carriedforward_add_sessions( + self, dbsession, sample_commit_with_report_big_already_carriedforward + ): + commit = sample_commit_with_report_big_already_carriedforward + dbsession.add(commit) + dbsession.flush() + yaml_dict = {"flags": {"enterprise": {"carryforward": True}}} + report = ReportService(UserYaml(yaml_dict)).get_existing_report_for_commit( + commit + ) + assert report is not None + assert len(report.files) == 15 + assert sorted(report.sessions.keys()) == [0, 1, 2, 3] + first_to_merge_session = Session(flags=["enterprise"]) + report.add_session(first_to_merge_session) + assert sorted(report.sessions.keys()) == [0, 1, 2, 3, 4] + assert clear_carryforward_sessions( + report, Report(), ["enterprise"], UserYaml(yaml_dict) + ) == SessionAdjustmentResult( + fully_deleted_sessions=[2, 3], partially_deleted_sessions=[] + ) + assert sorted(report.sessions.keys()) == [0, 1, 4] + readable_report = self.convert_report_to_better_readable(report) + expected_sessions_dict = { + "0": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": None, + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "se": {}, + "t": None, + "u": None, + }, + "1": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["unit"], + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "se": {}, + "t": None, + "u": None, + }, + "4": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["enterprise"], + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "se": {}, + "t": None, + "u": None, + }, + } + assert readable_report["report"]["sessions"]["0"] == expected_sessions_dict["0"] + assert readable_report["report"]["sessions"]["1"] == expected_sessions_dict["1"] + assert readable_report["report"]["sessions"]["4"] == expected_sessions_dict["4"] + assert readable_report["report"]["sessions"] == expected_sessions_dict + newly_added_session = { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["unit"], + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "se": {}, + "t": None, + "u": None, + } + second_to_merge_session = Session(flags=["unit"]) + report.add_session(second_to_merge_session) + assert sorted(report.sessions.keys()) == [0, 1, 3, 4] + assert clear_carryforward_sessions( + report, Report(), ["unit"], UserYaml(yaml_dict) + ) == SessionAdjustmentResult( + fully_deleted_sessions=[], partially_deleted_sessions=[] + ) + assert sorted(report.sessions.keys()) == [0, 1, 3, 4] + new_readable_report = self.convert_report_to_better_readable(report) + assert len(new_readable_report["report"]["sessions"]) == 4 + assert ( + new_readable_report["report"]["sessions"]["0"] + == expected_sessions_dict["0"] + ) + assert ( + new_readable_report["report"]["sessions"]["1"] + == expected_sessions_dict["1"] + ) + assert ( + new_readable_report["report"]["sessions"]["4"] + == expected_sessions_dict["4"] + ) + assert new_readable_report["report"]["sessions"]["3"] == newly_added_session + + @pytest.mark.django_db(databases={"default", "timeseries"}) + def test_create_new_report_for_commit_with_path_filters( + self, dbsession, sample_commit_with_report_big, mocker + ): + parent_commit = sample_commit_with_report_big + commit = CommitFactory.create( + repository=parent_commit.repository, + parent_commit_id=parent_commit.commitid, + _report_json=None, + ) + dbsession.add(commit) + dbsession.flush() + dbsession.add(CommitReport(commit_id=commit.id_)) + dbsession.flush() + yaml_dict = { + "flags": { + "enterprise": {"carryforward": True, "paths": ["file_1.*"]}, + "special_flag": {"paths": ["file_0.*"]}, + } + } + + def fake_possibly_shift(report, base, head): + return report + + mock_possibly_shift = mocker.patch.object( + ReportService, + "_possibly_shift_carryforward_report", + side_effect=fake_possibly_shift, + ) + report = ReportService(UserYaml(yaml_dict)).create_new_report_for_commit(commit) + assert report is not None + assert sorted(report.files) == sorted( + ["file_10.py", "file_11.py", "file_12.py", "file_13.py", "file_14.py"] + ) + mock_possibly_shift.assert_called() + assert report.totals == ReportTotals( + files=5, + lines=75, + hits=29, + misses=10, + partials=36, + coverage="38.66667", + branches=0, + methods=0, + messages=0, + sessions=2, + complexity=0, + complexity_total=0, + diff=0, + ) + readable_report = self.convert_report_to_better_readable(report) + + assert readable_report == { + "archive": { + "file_10.py": [ + (2, 1, None, [[3, 1, None, None, None]], None, None), + ( + 3, + "1/2", + None, + [[2, 0, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + (4, "1/2", None, [[2, "1/2", None, None, None]], None, None), + ( + 6, + "1/2", + None, + [[2, 0, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + (7, 1, None, [[3, 1, None, None, None]], None, None), + ( + 8, + "1/2", + None, + [[3, 0, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + ( + 9, + "1/3", + None, + [[2, "1/2", None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 10, + "3/3", + None, + [[3, 1, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ], + "file_11.py": [ + (1, 0, None, [[3, 0, None, None, None]], None, None), + (3, "1/2", None, [[2, "1/2", None, None, None]], None, None), + (4, "1/2", None, [[3, "1/2", None, None, None]], None, None), + (5, 0, None, [[2, 0, None, None, None]], None, None), + (6, 0, None, [[3, 0, None, None, None]], None, None), + (7, "1/3", None, [[2, "1/3", None, None, None]], None, None), + (8, 1, None, [[2, 1, None, None, None]], None, None), + (9, "1/2", None, [[2, "1/2", None, None, None]], None, None), + (10, 1, None, [[3, 1, None, None, None]], None, None), + ( + 11, + "2/2", + None, + [[2, 1, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + (12, 1, None, [[3, 1, None, None, None]], None, None), + ( + 13, + "1/2", + None, + [[3, 0, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + ( + 14, + "1/2", + None, + [[3, 0, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + (15, 0, None, [[2, 0, None, None, None]], None, None), + ( + 16, + 1, + None, + [[2, 0, None, None, None], [3, 1, None, None, None]], + None, + None, + ), + ( + 17, + "1/3", + None, + [[3, "1/2", None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 18, + "1/2", + None, + [[2, 0, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + (19, 0, None, [[3, 0, None, None, None]], None, None), + (20, 1, None, [[3, 1, None, None, None]], None, None), + ( + 21, + "2/2", + None, + [[3, 1, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + ( + 22, + "3/3", + None, + [[3, 1, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 23, + "1/3", + None, + [[2, 0, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ], + "file_12.py": [ + (2, "1/2", None, [[3, "1/2", None, None, None]], None, None), + (3, "1/3", None, [[3, "1/3", None, None, None]], None, None), + (4, 0, None, [[2, 0, None, None, None]], None, None), + (5, 0, None, [[3, 0, None, None, None]], None, None), + (7, 1, None, [[3, 1, None, None, None]], None, None), + ( + 8, + "1/2", + None, + [[3, "1/2", None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 9, + "1/2", + None, + [[2, 0, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + (10, 0, None, [[3, 0, None, None, None]], None, None), + (11, "1/3", None, [[3, "1/3", None, None, None]], None, None), + ( + 12, + "3/3", + None, + [[3, 1, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 13, + "3/3", + None, + [[2, 1, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 14, + "2/2", + None, + [[3, 1, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + ], + "file_13.py": [ + (2, 1, None, [[3, 1, None, None, None]], None, None), + ( + 6, + 1, + None, + [[3, 0, None, None, None], [2, 1, None, None, None]], + None, + None, + ), + (7, "1/3", None, [[2, "1/3", None, None, None]], None, None), + ( + 8, + "3/3", + None, + [[2, 1, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 9, + 1, + None, + [[3, 0, None, None, None], [2, 1, None, None, None]], + None, + None, + ), + ( + 10, + "1/3", + None, + [[2, "1/2", None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 11, + "1/3", + None, + [[2, 0, None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 12, + "1/3", + None, + [[2, "1/2", None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + (13, "1/2", None, [[3, "1/2", None, None, None]], None, None), + (14, 1, None, [[3, 1, None, None, None]], None, None), + ( + 15, + "2/2", + None, + [[3, 1, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + ], + "file_14.py": [ + (1, 1, None, [[2, 1, None, None, None]], None, None), + (2, 0, None, [[2, 0, None, None, None]], None, None), + ( + 3, + "1/3", + None, + [[3, 0, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 5, + "2/2", + None, + [[2, 1, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + (6, "1/3", None, [[3, "1/3", None, None, None]], None, None), + (7, 1, None, [[2, 1, None, None, None]], None, None), + (8, "1/3", None, [[2, "1/3", None, None, None]], None, None), + (9, "1/2", None, [[2, "1/2", None, None, None]], None, None), + (10, 1, None, [[2, 1, None, None, None]], None, None), + ( + 11, + "3/3", + None, + [[3, 1, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 12, + 1, + None, + [[2, 0, None, None, None], [3, 1, None, None, None]], + None, + None, + ), + (13, "1/3", None, [[3, "1/3", None, None, None]], None, None), + (14, "1/3", None, [[3, "1/3", None, None, None]], None, None), + (15, 0, None, [[2, 0, None, None, None]], None, None), + ( + 16, + "1/2", + None, + [[2, 0, None, None, None], [3, "1/2", None, None, None]], + None, + None, + ), + ( + 17, + "1/3", + None, + [[3, 0, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 18, + "1/3", + None, + [[3, 0, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 19, + "1/2", + None, + [[3, 0, None, None, None], [2, "1/2", None, None, None]], + None, + None, + ), + ( + 20, + "3/3", + None, + [[3, 1, None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 21, + "1/3", + None, + [[2, "1/2", None, None, None], [3, "1/3", None, None, None]], + None, + None, + ), + ( + 22, + "1/3", + None, + [[3, "1/2", None, None, None], [2, "1/3", None, None, None]], + None, + None, + ), + ( + 23, + 1, + None, + [[2, 0, None, None, None], [3, 1, None, None, None]], + None, + None, + ), + ], + }, + "report": { + "files": { + "file_10.py": [ + 0, + [0, 8, 3, 0, 5, "37.50000", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_11.py": [ + 1, + [0, 22, 8, 5, 9, "36.36364", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_12.py": [ + 2, + [0, 12, 4, 3, 5, "33.33333", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_13.py": [ + 3, + [0, 11, 6, 0, 5, "54.54545", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_14.py": [ + 4, + [0, 22, 8, 2, 12, "36.36364", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + }, + "sessions": { + "2": { + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["enterprise"], + "j": None, + "N": "Carriedforward", + "n": None, + "p": None, + "se": {"carriedforward_from": parent_commit.commitid}, + "st": "carriedforward", + "t": None, + "u": None, + }, + "3": { + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["unit", "enterprise"], + "j": None, + "N": "Carriedforward", + "n": None, + "p": None, + "se": {"carriedforward_from": parent_commit.commitid}, + "st": "carriedforward", + "t": None, + "u": None, + }, + }, + }, + "totals": { + "b": 0, + "c": "38.66667", + "C": 0, + "d": 0, + "diff": None, + "f": 5, + "h": 29, + "M": 0, + "m": 10, + "N": 0, + "n": 75, + "p": 36, + "s": 2, + }, + } + + def test_create_new_report_for_commit_no_flags( + self, dbsession, sample_commit_with_report_big, mocker + ): + parent_commit = sample_commit_with_report_big + commit = CommitFactory.create( + repository=parent_commit.repository, + parent_commit_id=parent_commit.commitid, + _report_json=None, + ) + dbsession.add(commit) + dbsession.flush() + yaml_dict = { + "flags": { + "enterprise": {"paths": ["file_1.*"]}, + "special_flag": {"paths": ["file_0.*"]}, + } + } + mock_possibly_shift = mocker.patch.object( + ReportService, "_possibly_shift_carryforward_report" + ) + report = ReportService(UserYaml(yaml_dict)).create_new_report_for_commit(commit) + assert report is not None + assert sorted(report.files) == [] + mock_possibly_shift.assert_not_called() + assert report.totals == ReportTotals( + files=0, + lines=0, + hits=0, + misses=0, + partials=0, + coverage=None, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + readable_report = self.convert_report_to_better_readable(report) + expected_results = { + "archive": {}, + "report": {"files": {}, "sessions": {}}, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": None, + "d": 0, + "diff": None, + "f": 0, + "h": 0, + "m": 0, + "n": 0, + "p": 0, + "s": 0, + }, + } + assert ( + expected_results["report"]["sessions"] + == readable_report["report"]["sessions"] + ) + assert expected_results["report"]["files"] == readable_report["report"]["files"] + assert expected_results["report"] == readable_report["report"] + assert expected_results["totals"] == readable_report["totals"] + assert expected_results == readable_report + + @pytest.mark.django_db(databases={"default", "timeseries"}) + def test_create_new_report_for_commit_no_parent( + self, dbsession, sample_commit_with_report_big, mocker + ): + parent_commit = sample_commit_with_report_big + commit = CommitFactory.create( + repository=parent_commit.repository, + parent_commit_id=None, + _report_json=None, + ) + dbsession.add(commit) + dbsession.flush() + yaml_dict = {"flags": {"enterprise": {"carryforward": True}}} + mock_possibly_shift = mocker.patch.object( + ReportService, "_possibly_shift_carryforward_report" + ) + report = ReportService(UserYaml(yaml_dict)).create_new_report_for_commit(commit) + assert report is not None + assert sorted(report.files) == [] + mock_possibly_shift.assert_not_called() + assert report.totals == ReportTotals( + files=0, + lines=0, + hits=0, + misses=0, + partials=0, + coverage=None, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + readable_report = self.convert_report_to_better_readable(report) + expected_results = { + "archive": {}, + "report": {"files": {}, "sessions": {}}, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": None, + "d": 0, + "diff": None, + "f": 0, + "h": 0, + "m": 0, + "n": 0, + "p": 0, + "s": 0, + }, + } + assert ( + expected_results["report"]["sessions"] + == readable_report["report"]["sessions"] + ) + assert expected_results["report"]["files"] == readable_report["report"]["files"] + assert expected_results["report"] == readable_report["report"] + assert expected_results["totals"] == readable_report["totals"] + assert expected_results == readable_report + + @pytest.mark.django_db(databases={"default", "timeseries"}) + def test_create_new_report_for_commit_parent_not_ready( + self, dbsession, sample_commit_with_report_big, mocker + ): + grandparent_commit = sample_commit_with_report_big + parent_commit = CommitFactory.create( + repository=grandparent_commit.repository, + parent_commit_id=grandparent_commit.commitid, + _report_json=None, + state="pending", + ) + commit = CommitFactory.create( + repository=grandparent_commit.repository, + parent_commit_id=parent_commit.commitid, + _report_json=None, + ) + dbsession.add(parent_commit) + dbsession.add(commit) + dbsession.flush() + dbsession.add(CommitReport(commit_id=commit.id_)) + dbsession.flush() + yaml_dict = {"flags": {"enterprise": {"carryforward": True}}} + mock_possibly_shift = mocker.patch.object( + ReportService, "_possibly_shift_carryforward_report" + ) + report = ReportService(UserYaml(yaml_dict)).create_new_report_for_commit(commit) + assert report is not None + mock_possibly_shift.assert_called() + assert sorted(report.files) == [ + "file_00.py", + "file_01.py", + "file_02.py", + "file_03.py", + "file_04.py", + "file_05.py", + "file_06.py", + "file_07.py", + "file_08.py", + "file_09.py", + "file_10.py", + "file_11.py", + "file_12.py", + "file_13.py", + "file_14.py", + ] + assert report.totals == ReportTotals( + files=15, + lines=188, + hits=68, + misses=26, + partials=94, + coverage="36.17021", + branches=0, + methods=0, + messages=0, + sessions=2, + complexity=0, + complexity_total=0, + diff=0, + ) + readable_report = self.convert_report_to_better_readable(report) + assert readable_report["report"] == { + "files": { + "file_00.py": [ + 0, + [0, 14, 4, 5, 5, "28.57143", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_01.py": [ + 1, + [0, 10, 3, 0, 7, "30.00000", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_02.py": [ + 2, + [0, 11, 5, 0, 6, "45.45455", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_03.py": [ + 3, + [0, 15, 4, 2, 9, "26.66667", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_04.py": [ + 4, + [0, 10, 3, 1, 6, "30.00000", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_05.py": [ + 5, + [0, 13, 3, 2, 8, "23.07692", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_06.py": [ + 6, + [0, 7, 5, 0, 2, "71.42857", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_07.py": [ + 7, + [0, 11, 5, 1, 5, "45.45455", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_08.py": [ + 8, + [0, 11, 2, 4, 5, "18.18182", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_09.py": [ + 9, + [0, 11, 5, 1, 5, "45.45455", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_10.py": [ + 10, + [0, 8, 3, 0, 5, "37.50000", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_11.py": [ + 11, + [0, 22, 8, 5, 9, "36.36364", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_12.py": [ + 12, + [0, 12, 4, 3, 5, "33.33333", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_13.py": [ + 13, + [0, 11, 6, 0, 5, "54.54545", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_14.py": [ + 14, + [0, 22, 8, 2, 12, "36.36364", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + }, + "sessions": { + "2": { + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["enterprise"], + "j": None, + "N": "Carriedforward", + "n": None, + "p": None, + "se": {"carriedforward_from": grandparent_commit.commitid}, + "st": "carriedforward", + "t": None, + "u": None, + }, + "3": { + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["unit", "enterprise"], + "j": None, + "N": "Carriedforward", + "n": None, + "p": None, + "se": {"carriedforward_from": grandparent_commit.commitid}, + "st": "carriedforward", + "t": None, + "u": None, + }, + }, + } + + @pytest.mark.django_db(databases={"default", "timeseries"}) + def test_create_new_report_for_commit_parent_not_ready_but_skipped( + self, dbsession, sample_commit_with_report_big, mocker + ): + parent_commit = sample_commit_with_report_big + parent_commit.state = "skipped" + dbsession.flush() + commit = CommitFactory.create( + repository=parent_commit.repository, + parent_commit_id=parent_commit.commitid, + _report_json=None, + ) + dbsession.add(parent_commit) + dbsession.add(commit) + dbsession.flush() + dbsession.add(CommitReport(commit_id=commit.id_)) + dbsession.flush() + yaml_dict = {"flags": {"enterprise": {"carryforward": True}}} + mock_possibly_shift = mocker.patch.object( + ReportService, "_possibly_shift_carryforward_report" + ) + report = ReportService(UserYaml(yaml_dict)).create_new_report_for_commit(commit) + assert report is not None + mock_possibly_shift.assert_called() + assert sorted(report.files) == sorted( + [ + "file_00.py", + "file_01.py", + "file_02.py", + "file_03.py", + "file_04.py", + "file_05.py", + "file_06.py", + "file_07.py", + "file_08.py", + "file_09.py", + "file_10.py", + "file_11.py", + "file_12.py", + "file_13.py", + "file_14.py", + ] + ) + assert report.totals == ReportTotals( + files=15, + lines=188, + hits=68, + misses=26, + partials=94, + coverage="36.17021", + branches=0, + methods=0, + messages=0, + sessions=2, + complexity=0, + complexity_total=0, + diff=0, + ) + readable_report = self.convert_report_to_better_readable(report) + expected_results_report = { + "sessions": { + "2": { + "N": "Carriedforward", + "a": None, + "c": None, + "d": readable_report["report"]["sessions"]["2"]["d"], + "e": None, + "f": ["enterprise"], + "j": None, + "n": None, + "p": None, + "st": "carriedforward", + "se": {"carriedforward_from": parent_commit.commitid}, + "t": None, + "u": None, + }, + "3": { + "N": "Carriedforward", + "a": None, + "c": None, + "d": readable_report["report"]["sessions"]["3"]["d"], + "e": None, + "f": ["unit", "enterprise"], + "j": None, + "n": None, + "p": None, + "st": "carriedforward", + "se": {"carriedforward_from": parent_commit.commitid}, + "t": None, + "u": None, + }, + } + } + assert ( + expected_results_report["sessions"]["2"] + == readable_report["report"]["sessions"]["2"] + ) + assert ( + expected_results_report["sessions"]["3"] + == readable_report["report"]["sessions"]["3"] + ) + assert ( + expected_results_report["sessions"] == readable_report["report"]["sessions"] + ) + + @pytest.mark.django_db(databases={"default", "timeseries"}) + def test_create_new_report_for_commit_too_many_ancestors_not_ready( + self, dbsession, sample_commit_with_report_big, mocker + ): + grandparent_commit = sample_commit_with_report_big + current_commit = grandparent_commit + for i in range(10): + current_commit = CommitFactory.create( + repository=grandparent_commit.repository, + parent_commit_id=current_commit.commitid, + _report_json=None, + state="pending", + ) + dbsession.add(current_commit) + commit = CommitFactory.create( + repository=grandparent_commit.repository, + parent_commit_id=current_commit.commitid, + _report_json=None, + ) + dbsession.add(commit) + dbsession.flush() + yaml_dict = {"flags": {"enterprise": {"carryforward": True}}} + mock_possibly_shift = mocker.patch.object( + ReportService, "_possibly_shift_carryforward_report" + ) + report = ReportService(UserYaml(yaml_dict)).create_new_report_for_commit(commit) + assert report is not None + mock_possibly_shift.assert_not_called() + assert sorted(report.files) == [] + readable_report = self.convert_report_to_better_readable(report) + expected_results_report = {"files": {}, "sessions": {}} + assert expected_results_report == readable_report["report"] + + @pytest.mark.django_db(databases={"default", "timeseries"}) + def test_create_new_report_parent_had_no_parent_and_pending(self, dbsession): + current_commit = CommitFactory.create(parent_commit_id=None, state="pending") + dbsession.add(current_commit) + for i in range(5): + current_commit = CommitFactory.create( + repository=current_commit.repository, + parent_commit_id=current_commit.commitid, + _report_json=None, + state="pending", + ) + dbsession.add(current_commit) + commit = CommitFactory.create( + repository=current_commit.repository, + parent_commit_id=current_commit.commitid, + _report_json=None, + ) + dbsession.add(commit) + dbsession.flush() + yaml_dict = {"flags": {"enterprise": {"carryforward": True}}} + with pytest.raises(NotReadyToBuildReportYetError): + ReportService(UserYaml(yaml_dict)).create_new_report_for_commit(commit) + + @pytest.mark.django_db(databases={"default", "timeseries"}) + def test_create_new_report_for_commit_potential_cf_but_not_real_cf( + self, dbsession, sample_commit_with_report_big + ): + parent_commit = sample_commit_with_report_big + dbsession.flush() + commit = CommitFactory.create( + repository=parent_commit.repository, + parent_commit_id=parent_commit.commitid, + _report_json=None, + ) + dbsession.add(parent_commit) + dbsession.add(commit) + dbsession.flush() + dbsession.add(CommitReport(commit_id=commit.id_)) + dbsession.flush() + yaml_dict = { + "flag_management": { + "default_rules": {"carryforward": False}, + "individual_flags": [{"name": "banana", "carryforward": True}], + } + } + report = ReportService(UserYaml(yaml_dict)).create_new_report_for_commit(commit) + assert report.is_empty() + + @pytest.mark.django_db(databases={"default", "timeseries"}) + def test_create_new_report_for_commit_parent_has_no_report( + self, mock_storage, dbsession + ): + parent = CommitFactory.create() + dbsession.add(parent) + dbsession.flush() + commit = CommitFactory.create( + parent_commit_id=parent.commitid, repository=parent.repository + ) + dbsession.add(commit) + dbsession.flush() + report_service = ReportService( + UserYaml({"flags": {"enterprise": {"carryforward": True}}}) + ) + r = report_service.create_new_report_for_commit(commit) + assert r.files == [] + + def test_save_full_report( + self, dbsession, mock_storage, sample_report, mock_configuration + ): + mock_configuration.set_params( + { + "setup": { + "save_report_data_in_storage": { + "only_codecov": False, + }, + } + } + ) + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + sample_report.sessions[0].archive = "path/to/upload/location" + sample_report.sessions[ + 0 + ].name = "this name contains more than 100 chars 1111111111111111111111111111111111111111111111111111111111111this is more than 100" + report_service = ReportService({}) + res = report_service.save_full_report(commit, sample_report) + storage_hash = report_service.get_archive_service( + commit.repository + ).storage_hash + assert res == { + "url": f"v4/repos/{storage_hash}/commits/{commit.commitid}/chunks.txt" + } + assert len(current_report_row.uploads) == 2 + first_upload = dbsession.query(Upload).filter_by( + report_id=current_report_row.id_, provider="circleci" + )[0] + second_upload = dbsession.query(Upload).filter_by( + report_id=current_report_row.id_, provider="travis" + )[0] + dbsession.refresh(second_upload) + dbsession.refresh(first_upload) + assert first_upload.build_code == "aycaramba" + assert first_upload.build_url is None + assert first_upload.env is None + assert first_upload.job_code is None + assert ( + first_upload.name + == "this name contains more than 100 chars 1111111111111111111111111111111111111111111111111111111111111" + ) + assert first_upload.provider == "circleci" + assert first_upload.report_id == current_report_row.id_ + assert first_upload.state == "complete" + assert first_upload.storage_path == "path/to/upload/location" + assert first_upload.order_number == 0 + assert len(first_upload.flags) == 1 + assert first_upload.flags[0].repository == commit.repository + assert first_upload.flags[0].flag_name == "unit" + assert first_upload.totals is not None + assert first_upload.totals.branches == 0 + assert first_upload.totals.coverage == Decimal("0.0") + assert first_upload.totals.hits == 0 + assert first_upload.totals.lines == 10 + assert first_upload.totals.methods == 0 + assert first_upload.totals.misses == 0 + assert first_upload.totals.partials == 0 + assert first_upload.totals.files == 2 + assert first_upload.upload_extras == {} + assert first_upload.upload_type == "uploaded" + assert second_upload.build_code == "poli" + assert second_upload.build_url is None + assert second_upload.env is None + assert second_upload.job_code is None + assert second_upload.name is None + assert second_upload.provider == "travis" + assert second_upload.report_id == current_report_row.id_ + assert second_upload.state == "complete" + assert second_upload.storage_path == "" + assert second_upload.order_number == 1 + assert len(second_upload.flags) == 1 + assert second_upload.flags[0].repository == commit.repository + assert second_upload.flags[0].flag_name == "integration" + assert second_upload.totals is None + assert second_upload.upload_extras == {} + assert second_upload.upload_type == "carriedforward" + + def test_save_report_empty_report(self, dbsession, mock_storage): + report = Report() + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + report_service = ReportService({}) + res = report_service.save_report(commit, report) + storage_hash = report_service.get_archive_service( + commit.repository + ).storage_hash + assert res == { + "url": f"v4/repos/{storage_hash}/commits/{commit.commitid}/chunks.txt" + } + assert commit.totals == { + "f": 0, + "n": 0, + "h": 0, + "m": 0, + "p": 0, + "c": 0, + "b": 0, + "d": 0, + "M": 0, + "s": 0, + "C": 0, + "N": 0, + "diff": None, + } + assert commit.report_json == { + "files": {}, + "sessions": {}, + "totals": [0, 0, 0, 0, 0, None, 0, 0, 0, 0, 0, 0, None], + } + assert res["url"] in mock_storage.storage["archive"] + assert ( + mock_storage.storage["archive"][res["url"]].decode() + == "{}\n<<<<< end_of_header >>>>>\n" + ) + + def test_save_report(self, dbsession, mock_storage, sample_report): + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + report_service = ReportService({}) + res = report_service.save_report(commit, sample_report) + storage_hash = report_service.get_archive_service( + commit.repository + ).storage_hash + + assert res == { + "url": f"v4/repos/{storage_hash}/commits/{commit.commitid}/chunks.txt" + } + assert len(current_report_row.uploads) == 0 + assert commit.report_json == { + "files": { + "file_1.go": [ + 0, + [0, 8, 5, 3, 0, "62.50000", 0, 0, 0, 0, 10, 2, 0], + None, + None, + ], + "file_2.py": [ + 1, + [0, 2, 1, 0, 1, "50.00000", 1, 0, 0, 0, 0, 0, 0], + None, + None, + ], + }, + "sessions": { + "0": { + "t": [2, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "d": None, + "a": None, + "f": ["unit"], + "c": "circleci", + "n": "aycaramba", + "N": None, + "j": None, + "u": None, + "p": None, + "e": None, + "st": "uploaded", + "se": {}, + }, + "1": { + "t": None, + "d": None, + "a": None, + "f": ["integration"], + "c": "travis", + "n": "poli", + "N": None, + "j": None, + "u": None, + "p": None, + "e": None, + "st": "carriedforward", + "se": {}, + }, + }, + "totals": [2, 10, 6, 3, 1, "60.00000", 1, 0, 0, 2, 10, 2, None], + } + assert res["url"] in mock_storage.storage["archive"] + expected_content = "\n".join( + [ + "{}", + "<<<<< end_of_header >>>>>", + '{"present_sessions":[0,1]}', + "[1,null,[[0,1]],null,[10,2]]", + "[0,null,[[0,1]]]", + "[1,null,[[0,1]]]", + "", + "[1,null,[[0,1],[1,1]]]", + "[0,null,[[0,1]]]", + "", + "[1,null,[[0,1],[1,0]]]", + "[1,null,[[0,1]]]", + "[0,null,[[0,1]]]", + "<<<<< end_of_chunk >>>>>", + '{"present_sessions":[0]}', + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "[1,null,[[0,1]]]", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + '["1/2","b",[[0,1]]]', + ] + ) + assert mock_storage.storage["archive"][res["url"]].decode() == expected_content + + def test_initialize_and_save_report_brand_new(self, dbsession, mock_storage): + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + report_service = ReportService({}) + r = report_service.initialize_and_save_report(commit) + assert r is not None + assert len(mock_storage.storage["archive"]) == 0 + + def test_initialize_and_save_report_report_but_no_details( + self, dbsession, mock_storage + ): + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + report_row = CommitReport(commit_id=commit.id_) + dbsession.add(report_row) + dbsession.flush() + report_service = ReportService({}) + r = report_service.initialize_and_save_report(commit) + dbsession.refresh(report_row) + assert r is not None + assert len(mock_storage.storage["archive"]) == 0 + + @pytest.mark.django_db(databases={"default"}) + def test_initialize_and_save_report_carryforward_needed( + self, dbsession, sample_commit_with_report_big, mocker, mock_storage + ): + parent_commit = sample_commit_with_report_big + commit = CommitFactory.create( + _report_json=None, + parent_commit_id=parent_commit.commitid, + repository=parent_commit.repository, + ) + dbsession.add(commit) + dbsession.flush() + yaml_dict = {"flags": {"enterprise": {"carryforward": True}}} + report_service = ReportService(UserYaml(yaml_dict)) + r = report_service.initialize_and_save_report(commit) + assert len(r.uploads) == 2 + first_upload = dbsession.query(Upload).filter_by( + report_id=r.id_, order_number=2 + )[0] + second_upload = dbsession.query(Upload).filter_by( + report_id=r.id_, order_number=3 + )[0] + assert first_upload.build_code is None + assert first_upload.build_url is None + assert first_upload.env is None + assert first_upload.job_code is None + assert first_upload.name == "Carriedforward" + assert first_upload.provider is None + assert first_upload.report_id == r.id_ + assert first_upload.state == "complete" + assert first_upload.storage_path == "" + assert first_upload.order_number == 2 + assert len(first_upload.flags) == 1 + assert first_upload.flags[0].repository == commit.repository + assert first_upload.flags[0].flag_name == "enterprise" + assert first_upload.totals is None + assert first_upload.upload_extras == { + "carriedforward_from": parent_commit.commitid + } + assert first_upload.upload_type == "carriedforward" + assert second_upload.build_code is None + assert second_upload.build_url is None + assert second_upload.env is None + assert second_upload.job_code is None + assert second_upload.name == "Carriedforward" + assert second_upload.provider is None + assert second_upload.report_id == r.id_ + assert second_upload.state == "complete" + assert second_upload.storage_path == "" + assert second_upload.order_number == 3 + assert len(second_upload.flags) == 2 + assert sorted([f.flag_name for f in second_upload.flags]) == [ + "enterprise", + "unit", + ] + assert second_upload.totals is None + assert second_upload.upload_extras == { + "carriedforward_from": parent_commit.commitid + } + assert second_upload.upload_type == "carriedforward" + + @pytest.mark.django_db(databases={"default"}) + def test_initialize_and_save_report_report_but_no_details_carryforward_needed( + self, dbsession, sample_commit_with_report_big, mock_storage + ): + parent_commit = sample_commit_with_report_big + commit = CommitFactory.create( + _report_json=None, + parent_commit_id=parent_commit.commitid, + repository=parent_commit.repository, + ) + dbsession.add(commit) + dbsession.flush() + report_row = CommitReport(commit_id=commit.id_) + dbsession.add(report_row) + dbsession.flush() + yaml_dict = {"flags": {"enterprise": {"carryforward": True}}} + report_service = ReportService(UserYaml(yaml_dict)) + r = report_service.initialize_and_save_report(commit) + assert len(r.uploads) == 2 + first_upload = dbsession.query(Upload).filter_by( + report_id=r.id_, order_number=2 + )[0] + second_upload = dbsession.query(Upload).filter_by( + report_id=r.id_, order_number=3 + )[0] + assert first_upload.build_code is None + assert first_upload.build_url is None + assert first_upload.env is None + assert first_upload.job_code is None + assert first_upload.name == "Carriedforward" + assert first_upload.provider is None + assert first_upload.report_id == r.id_ + assert first_upload.state == "complete" + assert first_upload.storage_path == "" + assert first_upload.order_number == 2 + assert len(first_upload.flags) == 1 + assert first_upload.flags[0].repository == commit.repository + assert first_upload.flags[0].flag_name == "enterprise" + assert first_upload.totals is None + assert first_upload.upload_extras == { + "carriedforward_from": parent_commit.commitid + } + assert first_upload.upload_type == "carriedforward" + assert second_upload.build_code is None + assert second_upload.build_url is None + assert second_upload.env is None + assert second_upload.job_code is None + assert second_upload.name == "Carriedforward" + assert second_upload.provider is None + assert second_upload.report_id == r.id_ + assert second_upload.state == "complete" + assert second_upload.storage_path == "" + assert second_upload.order_number == 3 + assert len(second_upload.flags) == 2 + assert sorted([f.flag_name for f in second_upload.flags]) == [ + "enterprise", + "unit", + ] + assert second_upload.totals is None + assert second_upload.upload_extras == { + "carriedforward_from": parent_commit.commitid + } + assert second_upload.upload_type == "carriedforward" + + def test_initialize_and_save_report_needs_backporting( + self, dbsession, sample_commit_with_report_big, mock_storage, mocker + ): + commit = sample_commit_with_report_big + report_service = ReportService({}) + r = report_service.initialize_and_save_report(commit) + assert r is not None + assert len(r.uploads) == 4 + first_upload = dbsession.query(Upload).filter_by(order_number=0).first() + assert sorted([f.flag_name for f in first_upload.flags]) == [] + second_upload = dbsession.query(Upload).filter_by(order_number=1).first() + assert sorted([f.flag_name for f in second_upload.flags]) == ["unit"] + third_upload = dbsession.query(Upload).filter_by(order_number=2).first() + assert sorted([f.flag_name for f in third_upload.flags]) == ["enterprise"] + fourth_upload = dbsession.query(Upload).filter_by(order_number=3).first() + assert sorted([f.flag_name for f in fourth_upload.flags]) == [ + "enterprise", + "unit", + ] + assert ( + dbsession.query(RepositoryFlag) + .filter_by(repository_id=commit.repoid) + .count() + == 2 + ) + storage_keys = mock_storage.storage["archive"].keys() + assert any(map(lambda key: key.endswith("chunks.txt"), storage_keys)) + + def test_initialize_and_save_report_existing_report( + self, mock_storage, sample_report, dbsession, mocker + ): + mocker_save_full_report = mocker.patch.object(ReportService, "save_full_report") + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + report_service = ReportService({}) + report_service.save_report(commit, sample_report) + res = report_service.initialize_and_save_report(commit) + assert res == current_report_row + assert not mocker_save_full_report.called + + @pytest.mark.django_db + def test_create_report_upload(self, dbsession): + arguments = { + "branch": "master", + "build": "646048900", + "build_url": "http://github.com/greenlantern/reponame/actions/runs/646048900", + "cmd_args": "n,F,Q,C", + "commit": "1280bf4b8d596f41b101ac425758226c021876da", + "job": "thisjob", + "flags": ["unittest"], + "name": "this name contains more than 100 chars 1111111111111111111111111111111111111111111111111111111111111this is more than 100", + "owner": "greenlantern", + "package": "github-action-20210309-2b87ace", + "pr": "33", + "repo": "reponame", + "reportid": "6e2b6449-4e60-43f8-80ae-2c03a5c03d92", + "service": "github-actions", + "slug": "greenlantern/reponame", + "url": "v4/raw/2021-03-12/C00AE6C87E34AF41A6D38D154C609782/1280bf4b8d596f41b101ac425758226c021876da/6e2b6449-4e60-43f8-80ae-2c03a5c03d92.txt", + "using_global_token": "false", + "version": "v4", + } + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + report_service = ReportService({}) + res = report_service.create_report_upload(arguments, current_report_row) + dbsession.flush() + assert res.build_code == "646048900" + assert ( + res.build_url + == "http://github.com/greenlantern/reponame/actions/runs/646048900" + ) + assert res.env is None + assert res.job_code == "thisjob" + assert ( + res.name + == "this name contains more than 100 chars 1111111111111111111111111111111111111111111111111111111111111" + ) + assert res.provider == "github-actions" + assert res.report_id == current_report_row.id_ + assert res.state == "started" + assert ( + res.storage_path + == "v4/raw/2021-03-12/C00AE6C87E34AF41A6D38D154C609782/1280bf4b8d596f41b101ac425758226c021876da/6e2b6449-4e60-43f8-80ae-2c03a5c03d92.txt" + ) + assert res.order_number is None + assert res.totals is None + assert res.upload_extras == {} + assert res.upload_type == "uploaded" + + def test_shift_carryforward_report( + self, dbsession, sample_report, mocker, mock_repo_provider + ): + parent_commit = CommitFactory() + commit = CommitFactory(parent_commit_id=parent_commit.commitid) + dbsession.add(parent_commit) + dbsession.add(commit) + dbsession.flush() + fake_diff = { + "diff": { + "files": { + "file_1.go": { + "type": "modified", + "before": None, + "segments": [ + { + "header": [3, 3, 3, 4], + "lines": [ + " some go code in line 3", + "-this line was removed", + "+this line was added", + "+this line was also added", + " ", + ], + }, + { + "header": [9, 1, 10, 5], + "lines": [ + " some go code in line 9", + "+add", + "+add", + "+add", + "+add", + ], + }, + ], + } + } + } + } + + def fake_get_compare(base, head): + assert base == parent_commit.commitid + assert head == commit.commitid + return fake_diff + + mock_repo_provider.get_compare = mock.AsyncMock(side_effect=fake_get_compare) + result = ReportService({})._possibly_shift_carryforward_report( + sample_report, parent_commit, commit + ) + readable_report = self.convert_report_to_better_readable(result) + assert readable_report["archive"] == { + "file_1.go": [ + (1, 1, None, [[0, 1, None, None, None]], None, (10, 2)), + (2, 0, None, [[0, 1, None, None, None]], None, None), + (3, 1, None, [[0, 1, None, None, None]], None, None), + ( + 6, + 1, + None, + [[0, 1, None, None, None], [1, 1, None, None, None]], + None, + None, + ), + (7, 0, None, [[0, 1, None, None, None]], None, None), + ( + 9, + 1, + None, + [[0, 1, None, None, None], [1, 0, None, None, None]], + None, + None, + ), + (10, 1, None, [[0, 1, None, None, None]], None, None), + (15, 0, None, [[0, 1, None, None, None]], None, None), + ], + "file_2.py": [ + (12, 1, None, [[0, 1, None, None, None]], None, None), + (51, "1/2", "b", [[0, 1, None, None, None]], None, None), + ], + } + + @pytest.mark.django_db(databases={"default", "timeseries"}) + def test_create_new_report_for_commit_and_shift( + self, dbsession, sample_report, mocker, mock_repo_provider, mock_storage + ): + parent_commit = CommitFactory() + parent_commit_report = CommitReport(commit_id=parent_commit.id_) + dbsession.add(parent_commit) + dbsession.add(parent_commit_report) + dbsession.flush() + + commit = CommitFactory.create( + repository=parent_commit.repository, + parent_commit_id=parent_commit.commitid, + _report_json=None, + ) + dbsession.add(commit) + dbsession.flush() + dbsession.add(CommitReport(commit_id=commit.id_)) + dbsession.flush() + yaml_dict = { + "flags": { + "integration": {"carryforward": True}, + "unit": {"carryforward": True}, + } + } + + fake_diff = { + "diff": { + "files": { + "file_1.go": { + "type": "modified", + "before": None, + "segments": [ + { + "header": [3, 3, 3, 4], + "lines": [ + " some go code in line 3", + "-this line was removed", + "+this line was added", + "+this line was also added", + " ", + ], + }, + { + "header": [9, 1, 10, 5], + "lines": [ + " some go code in line 9", + "+add", + "+add", + "+add", + "+add", + ], + }, + ], + } + } + } + } + + def fake_get_compare(base, head): + assert base == parent_commit.commitid + assert head == commit.commitid + return fake_diff + + mock_repo_provider.get_compare = mock.AsyncMock(side_effect=fake_get_compare) + + mock_get_report = mocker.patch.object( + ReportService, "get_existing_report_for_commit", return_value=sample_report + ) + + result = ReportService(UserYaml(yaml_dict)).create_new_report_for_commit(commit) + assert mock_get_report.call_count == 1 + readable_report = self.convert_report_to_better_readable(result) + assert readable_report["archive"] == { + "file_1.go": [ + (1, 1, None, [[0, 1, None, None, None]], None, (10, 2)), + (2, 0, None, [[0, 1, None, None, None]], None, None), + (3, 1, None, [[0, 1, None, None, None]], None, None), + ( + 6, + 1, + None, + [[0, 1, None, None, None], [1, 1, None, None, None]], + None, + None, + ), + (7, 0, None, [[0, 1, None, None, None]], None, None), + ( + 9, + 1, + None, + [[0, 1, None, None, None], [1, 0, None, None, None]], + None, + None, + ), + (10, 1, None, [[0, 1, None, None, None]], None, None), + (15, 0, None, [[0, 1, None, None, None]], None, None), + ], + "file_2.py": [ + (12, 1, None, [[0, 1, None, None, None]], None, None), + (51, "1/2", "b", [[0, 1, None, None, None]], None, None), + ], + } + + def test_possibly_shift_carryforward_report_cant_get_diff( + self, dbsession, sample_report, mocker + ): + parent_commit = CommitFactory() + commit = CommitFactory(parent_commit_id=parent_commit.commitid) + dbsession.add(parent_commit) + dbsession.add(commit) + dbsession.flush() + mock_log_error = mocker.patch.object(report_log, "error") + + def raise_error(*args, **kwargs): + raise TorngitRateLimitError(response_data="", message="error", reset=None) + + fake_provider = mocker.Mock() + fake_provider.get_compare = raise_error + mock_provider_service = mocker.patch( + "services.report.get_repo_provider_service", return_value=fake_provider + ) + result = ReportService({})._possibly_shift_carryforward_report( + sample_report, parent_commit, commit + ) + assert result == sample_report + mock_provider_service.assert_called() + mock_log_error.assert_called_with( + "Failed to shift carryforward report lines.", + extra=dict( + reason="Can't get diff", + commit=commit.commitid, + error=str( + TorngitRateLimitError(response_data="", message="error", reset=None) + ), + error_type=type( + TorngitRateLimitError(response_data="", message="error", reset=None) + ), + ), + ) + + def test_possibly_shift_carryforward_report_bot_error( + self, dbsession, sample_report, mocker + ): + parent_commit = CommitFactory() + commit = CommitFactory(parent_commit_id=parent_commit.commitid) + dbsession.add(parent_commit) + dbsession.add(commit) + dbsession.flush() + mock_log_error = mocker.patch.object(report_log, "error") + + def raise_error(*args, **kwargs): + raise RepositoryWithoutValidBotError() + + mock_provider_service = mocker.patch( + "services.report.get_repo_provider_service", side_effect=raise_error + ) + result = ReportService({})._possibly_shift_carryforward_report( + sample_report, parent_commit, commit + ) + assert result == sample_report + mock_provider_service.assert_called() + mock_log_error.assert_called_with( + "Failed to shift carryforward report lines", + extra=dict( + reason="Can't get provider_service", + commit=commit.commitid, + error=str(RepositoryWithoutValidBotError()), + ), + ) + + def test_possibly_shift_carryforward_report_random_processing_error( + self, dbsession, mocker, mock_repo_provider + ): + parent_commit = CommitFactory() + commit = CommitFactory(parent_commit_id=parent_commit.commitid) + dbsession.add(parent_commit) + dbsession.add(commit) + dbsession.flush() + mock_log_error = mocker.patch.object(report_log, "error") + + def raise_error(*args, **kwargs): + raise Exception("Very random and hard to get exception") + + mock_repo_provider.get_compare = mock.AsyncMock( + side_effect=lambda *args, **kwargs: dict(diff={}) + ) + mock_report = mocker.Mock() + mock_report.shift_lines_by_diff = raise_error + result = ReportService({})._possibly_shift_carryforward_report( + mock_report, parent_commit, commit + ) + assert result == mock_report + mock_log_error.assert_called_with( + "Failed to shift carryforward report lines.", + exc_info=True, + extra=dict( + reason="Unknown", + commit=commit.commitid, + ), + ) + + def test_possibly_shift_carryforward_report_softtimelimit_reraised( + self, dbsession, mocker, mock_repo_provider + ): + parent_commit = CommitFactory() + commit = CommitFactory(parent_commit_id=parent_commit.commitid) + dbsession.add(parent_commit) + dbsession.add(commit) + dbsession.flush() + + def raise_error(*args, **kwargs): + raise SoftTimeLimitExceeded() + + mock_report = mocker.Mock() + mock_report.shift_lines_by_diff = raise_error + with pytest.raises(SoftTimeLimitExceeded): + ReportService({})._possibly_shift_carryforward_report( + mock_report, parent_commit, commit + ) diff --git a/apps/worker/services/tests/test_repository_service.py b/apps/worker/services/tests/test_repository_service.py new file mode 100644 index 0000000000..34e9f0c926 --- /dev/null +++ b/apps/worker/services/tests/test_repository_service.py @@ -0,0 +1,2145 @@ +import inspect +from datetime import datetime +from unittest.mock import MagicMock, patch + +import mock +import pytest +from freezegun import freeze_time +from shared.encryption.oauth import get_encryptor_from_configuration +from shared.rate_limits import gh_app_key_name, owner_key_name +from shared.reports.types import UploadType +from shared.torngit.base import TorngitBaseAdapter +from shared.torngit.exceptions import ( + TorngitClientError, + TorngitObjectNotFoundError, + TorngitServerUnreachableError, +) +from shared.typings.torngit import ( + AdditionalData, + GithubInstallationInfo, + OwnerInfo, + RepoInfo, + TorngitInstanceData, +) + +from database.models import Owner +from database.models.core import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, + Pull, + Repository, +) +from database.tests.factories import ( + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) +from services.repository import ( + _pick_best_base_comparedto_pair, + fetch_and_update_pull_request_information, + fetch_and_update_pull_request_information_from_commit, + fetch_appropriate_parent_for_commit, + fetch_commit_yaml_and_possibly_store, + get_repo_provider_service, + get_repo_provider_service_by_id, + update_commit_from_provider_info, + upsert_author, +) +from tasks.notify import get_repo_provider_service_for_specific_commit + + +@pytest.fixture +def repo(dbsession) -> Repository: + repo = RepositoryFactory.create( + owner__unencrypted_oauth_token="testyftq3ovzkb3zmt823u3t04lkrt9w", + owner__service="github", + name="example-python", + ) + dbsession.add(repo) + dbsession.flush() + return repo + + +@pytest.fixture +def pull(dbsession, repo) -> Pull: + pull = PullFactory.create(repository=repo, author=None) + dbsession.add(pull) + dbsession.flush() + return pull + + +def test_get_repo_provider_service_github(dbsession, repo): + res = get_repo_provider_service(repo) + expected_data = { + "owner": { + "ownerid": repo.owner.ownerid, + "service_id": repo.owner.service_id, + "username": repo.owner.username, + }, + "repo": { + "name": "example-python", + "using_integration": False, + "service_id": repo.service_id, + "repoid": repo.repoid, + }, + "installation": None, + "fallback_installations": None, + "additional_data": {}, + } + assert res.data == expected_data + assert repo.owner.service == "github" + assert res._on_token_refresh is not None + assert inspect.isawaitable(res._on_token_refresh(None)) + assert res.token == { + "username": repo.owner.username, + "key": "testyftq3ovzkb3zmt823u3t04lkrt9w", + "secret": None, + "entity_name": owner_key_name(repo.owner.ownerid), + } + + +def test_get_repo_provider_service_additional_data(dbsession, repo): + additional_data: AdditionalData = {"upload_type": UploadType.TEST_RESULTS} + res = get_repo_provider_service(repo, additional_data=additional_data) + expected_data = { + "owner": { + "ownerid": repo.owner.ownerid, + "service_id": repo.owner.service_id, + "username": repo.owner.username, + }, + "repo": { + "name": "example-python", + "using_integration": False, + "service_id": repo.service_id, + "repoid": repo.repoid, + }, + "installation": None, + "fallback_installations": None, + "additional_data": {"upload_type": UploadType.TEST_RESULTS}, + } + assert res.data == expected_data + assert repo.owner.service == "github" + assert res._on_token_refresh is not None + assert inspect.isawaitable(res._on_token_refresh(None)) + assert res.token == { + "username": repo.owner.username, + "key": "testyftq3ovzkb3zmt823u3t04lkrt9w", + "secret": None, + "entity_name": owner_key_name(repo.owner.ownerid), + } + + +def test_get_repo_provider_service_github_with_installations(dbsession, mocker, repo): + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + return_value="installation_token", + ) + installation_0 = GithubAppInstallation( + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + installation_id=1200, + app_id=200, + repository_service_ids=None, + owner=repo.owner, + ) + installation_1 = GithubAppInstallation( + name="my_app", + installation_id=1300, + app_id=300, + pem_path="path", + repository_service_ids=None, + owner=repo.owner, + ) + repo.owner.github_app_installations = [installation_0, installation_1] + dbsession.add_all([repo, installation_0, installation_1]) + dbsession.flush() + res = get_repo_provider_service(repo, installation_name_to_use="my_app") + expected_data = { + "owner": { + "ownerid": repo.owner.ownerid, + "service_id": repo.owner.service_id, + "username": repo.owner.username, + }, + "repo": { + "name": "example-python", + "using_integration": True, + "service_id": repo.service_id, + "repoid": repo.repoid, + }, + "installation": { + "id": installation_1.id, + "installation_id": 1300, + "app_id": 300, + "pem_path": "path", + }, + "fallback_installations": [ + { + "id": installation_0.id, + "app_id": 200, + "installation_id": 1200, + "pem_path": None, + } + ], + "additional_data": {}, + } + assert res.data == expected_data + assert repo.owner.service == "github" + assert res._on_token_refresh is None + assert res.token == { + "key": "installation_token", + "username": "installation_1300", + "entity_name": gh_app_key_name( + installation_id=installation_1.installation_id, + app_id=installation_1.app_id, + ), + } + + +def test_get_repo_provider_service_bitbucket(dbsession): + repo = RepositoryFactory.create( + owner__unencrypted_oauth_token="testyftq3ovzkb3zmt823u3t04lkrt9w", + owner__service="bitbucket", + name="example-python", + ) + dbsession.add(repo) + dbsession.flush() + res = get_repo_provider_service(repo) + expected_data = { + "owner": { + "ownerid": repo.owner.ownerid, + "service_id": repo.owner.service_id, + "username": repo.owner.username, + }, + "repo": { + "name": "example-python", + "using_integration": False, + "service_id": repo.service_id, + "repoid": repo.repoid, + }, + "installation": None, + "fallback_installations": None, + "additional_data": {}, + } + assert res.data == expected_data + assert repo.owner.service == "bitbucket" + assert res._on_token_refresh is None + assert res.token == { + "username": repo.owner.username, + "key": "testyftq3ovzkb3zmt823u3t04lkrt9w", + "secret": None, + "entity_name": owner_key_name(repo.owner.ownerid), + } + + +def test_get_repo_provider_service_with_token_refresh_callback(dbsession): + repo = RepositoryFactory.create( + owner__unencrypted_oauth_token="testyftq3ovzkb3zmt823u3t04lkrt9w", + owner__service="gitlab", + name="example-python", + ) + dbsession.add(repo) + dbsession.flush() + res = get_repo_provider_service(repo) + expected_data = { + "owner": { + "ownerid": repo.owner.ownerid, + "service_id": repo.owner.service_id, + "username": repo.owner.username, + }, + "repo": { + "name": "example-python", + "using_integration": False, + "service_id": repo.service_id, + "repoid": repo.repoid, + }, + "installation": None, + "fallback_installations": None, + "additional_data": {}, + } + assert res.data == expected_data + assert res._on_token_refresh is not None + assert inspect.isawaitable(res._on_token_refresh(None)) + assert res.token == { + "username": repo.owner.username, + "key": "testyftq3ovzkb3zmt823u3t04lkrt9w", + "secret": None, + "entity_name": owner_key_name(repo.owner.ownerid), + } + + +def test_get_repo_provider_service_repo_bot(dbsession, mock_configuration): + repo = RepositoryFactory.create( + owner__unencrypted_oauth_token="testyftq3ovzkb3zmt823u3t04lkrt9w", + owner__service="gitlab", + name="example-python", + private=False, + ) + dbsession.add(repo) + dbsession.flush() + res = get_repo_provider_service(repo) + expected_data = { + "owner": { + "ownerid": repo.owner.ownerid, + "service_id": repo.owner.service_id, + "username": repo.owner.username, + }, + "repo": { + "name": "example-python", + "using_integration": False, + "service_id": repo.service_id, + "repoid": repo.repoid, + }, + "installation": None, + "fallback_installations": None, + "additional_data": {}, + } + assert res.data == expected_data + assert res.token == { + "username": repo.owner.username, + "key": "testyftq3ovzkb3zmt823u3t04lkrt9w", + "secret": None, + "entity_name": owner_key_name(repo.owner.ownerid), + } + assert res._on_token_refresh is not None + + +@pytest.mark.asyncio +async def test_token_refresh_callback(dbsession): + repo = RepositoryFactory.create( + owner__unencrypted_oauth_token="testyftq3ovzkb3zmt823u3t04lkrt9w", + owner__service="gitlab", + name="example-python", + ) + dbsession.add(repo) + dbsession.flush() + res = get_repo_provider_service(repo) + new_token = dict(key="new_access_token", refresh_token="new_refresh_token") + await res._on_token_refresh(new_token) + owner = dbsession.query(Owner).filter_by(ownerid=repo.owner.ownerid).first() + encryptor = get_encryptor_from_configuration() + saved_token = encryptor.decrypt_token(owner.oauth_token) + assert saved_token["key"] == "new_access_token" + assert saved_token["refresh_token"] == "new_refresh_token" + + +def test_get_repo_provider_service_different_bot(dbsession): + bot_token = "bcaa0dc0c66b4a8c8c65ac919a1a91aa" + bot = OwnerFactory.create(unencrypted_oauth_token=bot_token) + repo = RepositoryFactory.create( + owner__unencrypted_oauth_token="testyftq3ovzkb3zmt823u3t04lkrt9w", + bot=bot, + name="example-python", + ) + dbsession.add(repo) + dbsession.add(bot) + dbsession.flush() + res = get_repo_provider_service(repo) + expected_data = { + "owner": { + "ownerid": repo.owner.ownerid, + "service_id": repo.owner.service_id, + "username": repo.owner.username, + }, + "repo": { + "name": "example-python", + "using_integration": False, + "service_id": repo.service_id, + "repoid": repo.repoid, + }, + "installation": None, + "fallback_installations": None, + "additional_data": {}, + } + assert res.data["repo"] == expected_data["repo"] + assert res.data == expected_data + assert res.token == { + "username": repo.bot.username, + "key": bot_token, + "secret": None, + "entity_name": owner_key_name(repo.bot.ownerid), + } + + +def test_get_repo_provider_service_no_bot(dbsession): + bot_token = "bcaa0dc0c66b4a8c8c65ac919a1a91aa" + owner_bot = OwnerFactory.create(unencrypted_oauth_token=bot_token) + repo = RepositoryFactory.create( + owner__unencrypted_oauth_token="testyftq3ovzkb3zmt823u3t04lkrt9w", + owner__bot=owner_bot, + bot=None, + name="example-python", + ) + dbsession.add(repo) + dbsession.add(owner_bot) + dbsession.flush() + res = get_repo_provider_service(repo) + expected_data = { + "owner": { + "ownerid": repo.owner.ownerid, + "service_id": repo.owner.service_id, + "username": repo.owner.username, + }, + "repo": { + "name": "example-python", + "using_integration": False, + "service_id": repo.service_id, + "repoid": repo.repoid, + }, + "installation": None, + "fallback_installations": None, + "additional_data": {}, + } + assert res.data == expected_data + assert res.token == { + "username": repo.owner.bot.username, + "key": bot_token, + "secret": None, + "entity_name": owner_key_name(repo.owner.bot.ownerid), + } + + +@pytest.mark.asyncio +async def test_fetch_appropriate_parent_for_commit_grandparent( + dbsession, mock_repo_provider +): + grandparent_commit_id = "8aa5aa054aaa21cf5a664acd504a1af6f5caafaa" + parent_commit_id = "a" * 32 + repository = RepositoryFactory.create() + parent_commit = CommitFactory.create( + commitid=grandparent_commit_id, repository=repository + ) + commit = CommitFactory.create(parent_commit_id=None, repository=repository) + f = { + "commitid": commit.commitid, + "parents": [ + { + "commitid": parent_commit_id, + "parents": [{"commitid": grandparent_commit_id, "parents": []}], + } + ], + } + dbsession.add(parent_commit) + dbsession.add(commit) + dbsession.flush() + git_commit = {"parents": [parent_commit_id]} + mock_repo_provider.get_ancestors_tree.return_value = f + result = await fetch_appropriate_parent_for_commit( + mock_repo_provider, commit, git_commit + ) + assert grandparent_commit_id == result + + +@pytest.mark.asyncio +async def test_fetch_appropriate_parent_for_commit_parent_has_no_message( + dbsession, mock_repo_provider +): + grandparent_commit_id = "8aa5aa054aaa21cf5a664acd504a1af6f5caafaa" + parent_commit_id = "a" * 32 + repository = RepositoryFactory.create() + parent_with_no_message = CommitFactory.create( + commitid=parent_commit_id, + repository=repository, + message=None, + parent_commit_id=None, + ) + parent_commit = CommitFactory.create( + commitid=grandparent_commit_id, repository=repository + ) + commit = CommitFactory.create(parent_commit_id=None, repository=repository) + f = { + "commitid": commit.commitid, + "parents": [ + { + "commitid": parent_commit_id, + "parents": [{"commitid": grandparent_commit_id, "parents": []}], + } + ], + } + dbsession.add(parent_commit) + dbsession.add(commit) + dbsession.add(parent_with_no_message) + dbsession.flush() + git_commit = {"parents": [parent_commit_id]} + mock_repo_provider.get_ancestors_tree.return_value = f + result = await fetch_appropriate_parent_for_commit( + mock_repo_provider, commit, git_commit + ) + assert grandparent_commit_id == result + + +@pytest.mark.asyncio +async def test_fetch_appropriate_parent_for_commit_parent_is_deleted( + dbsession, mock_repo_provider +): + grandparent_commit_id = "8aa5aa054aaa21cf5a664acd504a1af6f5caafaa" + parent_commit_id = "a" * 32 + repository = RepositoryFactory.create() + parent_with_no_message = CommitFactory.create( + commitid=parent_commit_id, + repository=repository, + message="message", + parent_commit_id=None, + deleted=True, + ) + parent_commit = CommitFactory.create( + commitid=grandparent_commit_id, repository=repository + ) + commit = CommitFactory.create(parent_commit_id=None, repository=repository) + f = { + "commitid": commit.commitid, + "parents": [ + { + "commitid": parent_commit_id, + "parents": [{"commitid": grandparent_commit_id, "parents": []}], + } + ], + } + dbsession.add(parent_commit) + dbsession.add(commit) + dbsession.add(parent_with_no_message) + dbsession.flush() + git_commit = {"parents": [parent_commit_id]} + mock_repo_provider.get_ancestors_tree.return_value = f + result = await fetch_appropriate_parent_for_commit( + mock_repo_provider, commit, git_commit + ) + assert grandparent_commit_id == result + + +@pytest.mark.asyncio +async def test_fetch_appropriate_parent_for_commit_parent_has_no_message_but_nothing_better( + dbsession, mock_repo_provider +): + grandparent_commit_id = "8aa5aa054aaa21cf5a664acd504a1af6f5caafaa" + parent_commit_id = "a" * 32 + repository = RepositoryFactory.create() + parent_with_no_message = CommitFactory.create( + commitid=parent_commit_id, + repository=repository, + message=None, + parent_commit_id=None, + ) + commit = CommitFactory.create(parent_commit_id=None, repository=repository) + f = { + "commitid": commit.commitid, + "parents": [ + { + "commitid": parent_commit_id, + "parents": [{"commitid": grandparent_commit_id, "parents": []}], + } + ], + } + dbsession.add(commit) + dbsession.add(parent_with_no_message) + dbsession.flush() + git_commit = {"parents": [parent_commit_id]} + mock_repo_provider.get_ancestors_tree.return_value = f + result = await fetch_appropriate_parent_for_commit( + mock_repo_provider, commit, git_commit + ) + assert parent_commit_id == result + + +@pytest.mark.asyncio +async def test_fetch_appropriate_parent_for_multiple_commit_parent_has_no_message_but_nothing_better( + dbsession, mock_repo_provider +): + grandparent_commit_id = "8aa5aa054aaa21cf5a664acd504a1af6f5caafaa" + parent_commit_id = "a" * 32 + sec_parent_commit_id = "b" * 32 + repository = RepositoryFactory.create() + parent_with_no_message = CommitFactory.create( + commitid=parent_commit_id, + repository=repository, + message=None, + parent_commit_id=None, + ) + sec_parent_with_no_message = CommitFactory.create( + commitid=sec_parent_commit_id, + repository=repository, + message=None, + parent_commit_id=None, + branch="bbb", + ) + commit = CommitFactory.create( + parent_commit_id=None, repository=repository, branch="bbb" + ) + f = { + "commitid": commit.commitid, + "parents": [ + { + "commitid": parent_commit_id, + "parents": [{"commitid": grandparent_commit_id, "parents": []}], + }, + { + "commitid": sec_parent_commit_id, + "parents": [{"commitid": grandparent_commit_id, "parents": []}], + }, + ], + } + dbsession.add(commit) + dbsession.add(parent_with_no_message) + dbsession.add(sec_parent_with_no_message) + dbsession.flush() + git_commit = {"parents": [parent_commit_id, sec_parent_commit_id]} + mock_repo_provider.get_ancestors_tree.return_value = f + result = await fetch_appropriate_parent_for_commit( + mock_repo_provider, commit, git_commit + ) + assert sec_parent_commit_id == result + + +@pytest.mark.asyncio +async def test_fetch_appropriate_parent_for_commit_grandparent_wrong_repo_with_same( + dbsession, mock_repo_provider +): + grandparent_commit_id = "8aa5aa054aaa21cf5a664acd504a1af6f5caafaa" + parent_commit_id = "39594a6cd3213e4a606de77486f16bbf22c4f42e" + repository = RepositoryFactory.create() + second_repository = RepositoryFactory.create() + parent_commit = CommitFactory.create( + commitid=grandparent_commit_id, repository=repository + ) + commit = CommitFactory.create(parent_commit_id=None, repository=repository) + deceiving_parent_commit = CommitFactory.create( + commitid=parent_commit_id, repository=second_repository + ) + f = { + "commitid": commit.commitid, + "parents": [ + { + "commitid": parent_commit_id, + "parents": [{"commitid": grandparent_commit_id, "parents": []}], + } + ], + } + dbsession.add(parent_commit) + dbsession.add(commit) + dbsession.add(deceiving_parent_commit) + dbsession.flush() + git_commit = {"parents": [parent_commit_id]} + mock_repo_provider.get_ancestors_tree.return_value = f + result = await fetch_appropriate_parent_for_commit( + mock_repo_provider, commit, git_commit + ) + assert grandparent_commit_id == result + + +@pytest.mark.asyncio +async def test_fetch_appropriate_parent_for_commit_grandparents_wrong_repo( + dbsession, mock_repo_provider +): + grandparent_commit_id = "8aa5aa054aaa21cf5a664acd504a1af6f5caafaa" + parent_commit_id = "39594a6cd3213e4a606de77486f16bbf22c4f42e" + second_parent_commit_id = "aaaaaa6cd3213e4a606de77486f16bbf22c4f422" + repository = RepositoryFactory.create() + second_repository = RepositoryFactory.create() + parent_commit = CommitFactory.create( + commitid=grandparent_commit_id, repository=repository, branch="aaa" + ) + seconed_parent_commit = CommitFactory.create( + commitid=second_parent_commit_id, repository=repository, branch="bbb" + ) + commit = CommitFactory.create( + parent_commit_id=None, repository=repository, branch="bbb" + ) + deceiving_parent_commit = CommitFactory.create( + commitid=parent_commit_id, repository=second_repository + ) + f = { + "commitid": commit.commitid, + "parents": [ + { + "commitid": parent_commit_id, + "parents": [ + {"commitid": grandparent_commit_id, "parents": []}, + {"commitid": second_parent_commit_id, "parents": []}, + ], + }, + ], + } + dbsession.add(seconed_parent_commit) + dbsession.add(parent_commit) + dbsession.add(commit) + dbsession.add(deceiving_parent_commit) + dbsession.flush() + git_commit = {"parents": [parent_commit_id]} + mock_repo_provider.get_ancestors_tree.return_value = f + result = await fetch_appropriate_parent_for_commit( + mock_repo_provider, commit, git_commit + ) + assert second_parent_commit_id == result + + +@pytest.mark.asyncio +async def test_fetch_appropriate_parent_for_commit_direct_parent( + dbsession, mock_repo_provider +): + parent_commit_id = "8aa5be054aeb21cf5a664ecd504a1af6f5ceafba" + repository = RepositoryFactory.create() + parent_commit = CommitFactory.create( + commitid=parent_commit_id, repository=repository + ) + commit = CommitFactory.create(parent_commit_id=None, repository=repository) + dbsession.add(parent_commit) + dbsession.add(commit) + dbsession.flush() + git_commit = {"parents": [parent_commit_id]} + expected_result = parent_commit_id + result = await fetch_appropriate_parent_for_commit( + mock_repo_provider, commit, git_commit + ) + assert expected_result == result + + +@pytest.mark.asyncio +async def test_fetch_appropriate_parent_for_commit_multiple_parents( + dbsession, mock_repo_provider +): + first_parent_commit_id = "8aa5be054aeb21cf5a664ecd504a1af6f5ceafba" + second_parent_commit_id = "a" * 32 + repository = RepositoryFactory.create() + second_parent_commit = CommitFactory.create( + commitid=second_parent_commit_id, repository=repository, branch="2ndBranch" + ) + first_parent_commit = CommitFactory.create( + commitid=first_parent_commit_id, repository=repository, branch="1stBranch" + ) + commit = CommitFactory.create( + parent_commit_id=None, repository=repository, branch="1stBranch" + ) + dbsession.add(second_parent_commit) + dbsession.add(first_parent_commit) + dbsession.add(commit) + dbsession.flush() + git_commit = {"parents": [first_parent_commit_id, second_parent_commit_id]} + expected_result = first_parent_commit_id + result = await fetch_appropriate_parent_for_commit( + mock_repo_provider, commit, git_commit + ) + assert expected_result == result + + +@freeze_time("2024-03-28T00:00:00") +def test_upsert_author_doesnt_exist(dbsession): + service = "github" + author_id = "123" + username = "username" + email = "email" + name = "name" + author = upsert_author(dbsession, service, author_id, username, email, name) + dbsession.flush() + assert author.free == 0 + assert author is not None + assert author.service == "github" + assert author.service_id == "123" + assert author.name == "name" + assert author.email == "email" + assert author.username == "username" + assert author.plan_activated_users is None + assert author.admins is None + assert author.permission is None + assert author.integration_id is None + assert author.yaml is None + assert author.oauth_token is None + assert author.bot_id is None + assert author.createstamp.isoformat() == "2024-03-28T00:00:00" + + +def test_upsert_author_already_exists(dbsession): + username = "username" + email = "email@email.com" + service = "bitbucket" + service_id = "975" + owner = OwnerFactory.create( + service=service, + service_id=service_id, + email=email, + username=username, + yaml=dict(a=["12", "3"]), + ) + dbsession.add(owner) + dbsession.flush() + + author = upsert_author(dbsession, service, service_id, username, None, None) + dbsession.flush() + assert author.ownerid == owner.ownerid + assert author.free == 0 + assert author is not None + assert author.service == service + assert author.service_id == service_id + assert author.name == owner.name + assert author.email == email + assert author.username == username + assert author.plan_activated_users == [] + assert author.admins == [] + assert author.permission == [] + assert author.integration_id is None + assert author.yaml == {"a": ["12", "3"]} + assert author.oauth_token == owner.oauth_token + assert author.bot_id == owner.bot_id + + +def test_upsert_author_needs_update(dbsession): + username = "username" + email = "email@email.com" + service = "bitbucket" + service_id = "975" + owner = OwnerFactory.create( + service=service, + service_id=service_id, + email=email, + username=username, + yaml=dict(a=["12", "3"]), + ) + dbsession.add(owner) + dbsession.flush() + + new_name = "Newt Namenheim" + new_username = "new_username" + new_email = "new_email@email.com" + author = upsert_author( + dbsession, service, service_id, new_username, new_email, new_name + ) + dbsession.flush() + + assert author is not None + assert author.ownerid == owner.ownerid + assert author.free == 0 + assert author.service == service + assert author.service_id == service_id + assert author.name == new_name + assert author.email == new_email + assert author.username == new_username + assert author.plan_activated_users == [] + assert author.admins == [] + assert author.permission == [] + assert author.integration_id is None + assert author.yaml == {"a": ["12", "3"]} + assert author.oauth_token == owner.oauth_token + assert author.bot_id == owner.bot_id + + +@pytest.mark.asyncio +async def test_update_commit_from_provider_info_no_author_id(dbsession, mocker): + possible_parent_commit = CommitFactory.create( + message="possible_parent_commit", pullid=None + ) + commit = CommitFactory.create( + message="", + author=None, + pullid=1, + totals=None, + _report_json=None, + repository=possible_parent_commit.repository, + ) + dbsession.add(possible_parent_commit) + dbsession.add(commit) + dbsession.flush() + dbsession.refresh(commit) + f = { + "author": { + "id": None, + "username": None, + "email": "email@email.com", + "name": "Mario", + }, + "message": "This message is brought to you by", + "parents": [possible_parent_commit.commitid], + "timestamp": "2018-07-09T23:39:20Z", + } + get_pull_request_result = { + "head": {"branch": "newbranchyeah"}, + "base": {"branch": "main"}, + } + repository_service = mocker.MagicMock( + get_commit=mock.AsyncMock(return_value=f), + get_pull_request=mock.AsyncMock(return_value=get_pull_request_result), + ) + await update_commit_from_provider_info(repository_service, commit) + dbsession.flush() + dbsession.refresh(commit) + assert commit.author is None + assert commit.message == "This message is brought to you by" + assert commit.pullid == 1 + assert commit.totals is None + assert commit.report_json == {} + assert commit.branch == "newbranchyeah" + assert commit.merged is False + assert commit.timestamp == datetime(2018, 7, 9, 23, 39, 20) + assert commit.parent_commit_id == possible_parent_commit.commitid + assert commit.state == "complete" + + +@pytest.mark.asyncio +async def test_update_commit_from_provider_info_no_pullid_on_defaultbranch( + dbsession, mocker, mock_repo_provider +): + repository = RepositoryFactory.create(branch="superbranch") + dbsession.add(repository) + dbsession.flush() + possible_parent_commit = CommitFactory.create( + message="possible_parent_commit", pullid=None, repository=repository + ) + commit = CommitFactory.create( + message="", + author=None, + pullid=None, + totals=None, + branch="papapa", + _report_json=None, + repository=repository, + ) + dbsession.add(possible_parent_commit) + dbsession.add(commit) + dbsession.flush() + dbsession.refresh(commit) + mock_repo_provider.find_pull_request.return_value = None + mock_repo_provider.get_best_effort_branches.return_value = [ + "superbranch", + "else", + "pokemon", + ] + mock_repo_provider.get_commit.return_value = { + "author": { + "id": None, + "username": None, + "email": "email@email.com", + "name": "Mario", + }, + "message": "This message is brought to you by", + "parents": [possible_parent_commit.commitid], + "timestamp": "2018-07-09T23:39:20Z", + } + await update_commit_from_provider_info(mock_repo_provider, commit) + dbsession.flush() + dbsession.refresh(commit) + assert commit.author is None + assert commit.message == "This message is brought to you by" + assert commit.pullid is None + assert commit.totals is None + assert commit.report_json == {} + assert commit.branch == "superbranch" + assert commit.merged is True + assert commit.timestamp == datetime(2018, 7, 9, 23, 39, 20) + assert commit.parent_commit_id == possible_parent_commit.commitid + assert commit.state == "complete" + + +@pytest.mark.asyncio +async def test_update_commit_from_provider_info_no_pullid_not_on_defaultbranch( + dbsession, mocker, mock_repo_provider +): + repository = RepositoryFactory.create(branch="superbranch") + dbsession.add(repository) + dbsession.flush() + possible_parent_commit = CommitFactory.create( + message="possible_parent_commit", pullid=None, repository=repository + ) + commit = CommitFactory.create( + message="", + author=None, + pullid=None, + branch="papapa", + totals=None, + _report_json=None, + repository=repository, + ) + dbsession.add(possible_parent_commit) + dbsession.add(commit) + dbsession.flush() + dbsession.refresh(commit) + mock_repo_provider.find_pull_request.return_value = None + mock_repo_provider.get_best_effort_branches.return_value = ["else", "pokemon"] + mock_repo_provider.get_commit.return_value = { + "author": { + "id": None, + "username": None, + "email": "email@email.com", + "name": "Mario", + }, + "message": "This message is brought to you by", + "parents": [possible_parent_commit.commitid], + "timestamp": "2018-07-09T23:39:20Z", + } + await update_commit_from_provider_info(mock_repo_provider, commit) + dbsession.flush() + dbsession.refresh(commit) + assert commit.author is None + assert commit.message == "This message is brought to you by" + assert commit.pullid is None + assert commit.totals is None + assert commit.report_json == {} + assert commit.branch == "papapa" + assert commit.merged is False + assert commit.timestamp == datetime(2018, 7, 9, 23, 39, 20) + assert commit.parent_commit_id == possible_parent_commit.commitid + assert commit.state == "complete" + + +@pytest.mark.asyncio +async def test_update_commit_from_provider_info_with_author_id(dbsession, mocker): + possible_parent_commit = CommitFactory.create( + message="possible_parent_commit", pullid=None + ) + commit = CommitFactory.create( + message="", + author=None, + pullid=1, + totals=None, + _report_json=None, + repository=possible_parent_commit.repository, + ) + dbsession.add(possible_parent_commit) + dbsession.add(commit) + dbsession.flush() + dbsession.refresh(commit) + f = { + "author": { + "id": "author_id", + "username": "author_username", + "email": "email@email.com", + "name": "Mario", + }, + "message": "This message is brought to you by", + "parents": [possible_parent_commit.commitid], + "timestamp": "2018-07-09T23:39:20Z", + } + get_pull_request_result = { + "head": {"branch": "newbranchyeah"}, + "base": {"branch": "main"}, + } + repository_service = mocker.MagicMock( + get_commit=mock.AsyncMock(return_value=f), + get_pull_request=mock.AsyncMock(return_value=get_pull_request_result), + ) + await update_commit_from_provider_info(repository_service, commit) + dbsession.flush() + dbsession.refresh(commit) + assert commit.message == "This message is brought to you by" + assert commit.pullid == 1 + assert commit.totals is None + assert commit.report_json == {} + assert commit.branch == "newbranchyeah" + assert commit.parent_commit_id == possible_parent_commit.commitid + assert commit.state == "complete" + assert commit.author is not None + assert commit.timestamp == datetime(2018, 7, 9, 23, 39, 20) + assert commit.author.username == "author_username" + + +@pytest.mark.asyncio +async def test_update_commit_from_provider_info_pull_from_fork(dbsession, mocker): + possible_parent_commit = CommitFactory.create( + message="possible_parent_commit", pullid=None + ) + commit = CommitFactory.create( + message="", + author=None, + pullid=1, + totals=None, + _report_json=None, + repository=possible_parent_commit.repository, + ) + dbsession.add(possible_parent_commit) + dbsession.add(commit) + dbsession.flush() + dbsession.refresh(commit) + f = { + "author": { + "id": "author_id", + "username": "author_username", + "email": "email@email.com", + "name": "Mario", + }, + "message": "This message is brought to you by", + "parents": [possible_parent_commit.commitid], + "timestamp": "2018-07-09T23:39:20Z", + } + get_pull_request_result = { + "head": {"branch": "main", "slug": f"some-guy/{commit.repository.name}"}, + "base": { + "branch": "main", + "slug": f"{commit.repository.owner.username}/{commit.repository.name}", + }, + } + repository_service = mocker.MagicMock( + get_commit=mock.AsyncMock(return_value=f), + get_pull_request=mock.AsyncMock(return_value=get_pull_request_result), + ) + await update_commit_from_provider_info(repository_service, commit) + dbsession.flush() + dbsession.refresh(commit) + assert commit.message == "This message is brought to you by" + assert commit.pullid == 1 + assert commit.totals is None + assert commit.report_json == {} + assert commit.branch == f"some-guy/{commit.repository.name}:main" + assert commit.parent_commit_id == possible_parent_commit.commitid + assert commit.state == "complete" + assert commit.author is not None + assert commit.timestamp == datetime(2018, 7, 9, 23, 39, 20) + assert commit.author.username == "author_username" + + +@pytest.mark.asyncio +async def test_update_commit_from_provider_info_bitbucket_merge(dbsession, mocker): + possible_parent_commit = CommitFactory.create( + message="possible_parent_commit", + pullid=None, + repository__owner__service="bitbucket", + ) + commit = CommitFactory.create( + message="", + author=None, + pullid=1, + totals=None, + _report_json=None, + repository=possible_parent_commit.repository, + ) + dbsession.add(possible_parent_commit) + dbsession.add(commit) + dbsession.flush() + dbsession.refresh(commit) + f = { + "author": { + "id": "author_id", + "username": "author_username", + "email": "email@email.com", + "name": "Mario", + }, + "message": "Merged in aaaa/coverage.py (pull request #99) Fix #123: crash", + "parents": [possible_parent_commit.commitid], + "timestamp": "2018-07-09T23:39:20Z", + } + get_pull_request_result = { + "head": {"branch": "newbranchyeah"}, + "base": {"branch": "thebasebranch"}, + } + repository_service = mocker.MagicMock( + get_commit=mock.AsyncMock(return_value=f), + get_pull_request=mock.AsyncMock(return_value=get_pull_request_result), + ) + await update_commit_from_provider_info(repository_service, commit) + dbsession.flush() + dbsession.refresh(commit) + assert ( + commit.message + == "Merged in aaaa/coverage.py (pull request #99) Fix #123: crash" + ) + assert commit.pullid == 1 + assert commit.totals is None + assert commit.report_json == {} + assert commit.branch == "thebasebranch" + assert commit.parent_commit_id == possible_parent_commit.commitid + assert commit.state == "complete" + assert commit.author is not None + assert commit.timestamp == datetime(2018, 7, 9, 23, 39, 20) + assert commit.author.username == "author_username" + + +@pytest.mark.asyncio +async def test_get_repo_gh_no_integration(dbsession, mocker): + owner = OwnerFactory.create( + service="github", + username="1nf1n1t3l00p", + service_id="45343385", + unencrypted_oauth_token="bcaa0dc0c66b4a8c8c65ac919a1a91aa", + ) + dbsession.add(owner) + + repo = RepositoryFactory.create( + private=True, + name="pytest", + using_integration=False, + service_id="123456", + owner=owner, + ) + dbsession.add(repo) + dbsession.flush() + + res = get_repo_provider_service_by_id(dbsession, repo.repoid) + + expected_data = { + "owner": { + "ownerid": owner.ownerid, + "service_id": owner.service_id, + "username": owner.username, + }, + "repo": { + "name": "pytest", + "using_integration": False, + "service_id": "123456", + "repoid": repo.repoid, + }, + "installation": None, + "fallback_installations": None, + "additional_data": {}, + } + assert res.data["repo"] == expected_data["repo"] + assert res.data == expected_data + assert res.token == { + "username": "1nf1n1t3l00p", + "key": "bcaa0dc0c66b4a8c8c65ac919a1a91aa", + "secret": None, + "entity_name": owner_key_name(repo.owner.ownerid), + } + + +class TestGetRepoProviderServiceForSpecificCommit(object): + @pytest.fixture + def mock_get_repo_provider_service(self, mocker): + mock_get_repo_provider_service = mocker.patch( + "tasks.notify.get_repo_provider_service" + ) + return mock_get_repo_provider_service + + @pytest.fixture + def mock_redis(self, mocker): + fake_redis = MagicMock(name="fake_redis") + mock_conn = mocker.patch("services.github.get_redis_connection") + mock_conn.return_value = fake_redis + return fake_redis + + def test_get_repo_provider_service_for_specific_commit_not_gh( + self, dbsession, mock_get_repo_provider_service, mock_redis + ): + commit = CommitFactory(repository__owner__service="gitlab") + mock_get_repo_provider_service.return_value = "the TorngitAdapter" + response = get_repo_provider_service_for_specific_commit(commit, "some_name") + assert response == "the TorngitAdapter" + mock_get_repo_provider_service.assert_called_with( + commit.repository, "some_name" + ) + + @patch("tasks.notify._possibly_pin_commit_to_github_app") + def test_get_repo_provider_service_for_specific_commit_no_specific_app_for_commit( + self, mock_pin, dbsession, mock_get_repo_provider_service, mock_redis + ): + commit = CommitFactory(repository__owner__service="github") + assert commit.id not in [10000, 15000] + redis_keys = { + "app_to_use_for_commit_15000": b"1200", + "app_to_use_for_commit_10000": b"1000", + } + mock_redis.get.side_effect = lambda key: redis_keys.get(key) + + mock_get_repo_provider_service.return_value = "the TorngitAdapter" + + response = get_repo_provider_service_for_specific_commit(commit, "some_name") + assert response == "the TorngitAdapter" + mock_get_repo_provider_service.assert_called_with( + commit.repository, "some_name" + ) + + @patch("tasks.notify.get_github_app_token", return_value=("the app token", None)) + @patch( + "tasks.notify._get_repo_provider_service_instance", + return_value="the TorngitAdapter", + ) + def test_get_repo_provider_service_for_specific_commit( + self, + mock_get_instance, + mock_get_app_token, + dbsession, + mock_get_repo_provider_service, + mock_redis, + ): + commit = CommitFactory(repository__owner__service="github") + app = GithubAppInstallation( + owner=commit.repository.owner, app_id=12, installation_id=1200 + ) + dbsession.add_all([commit, app]) + dbsession.flush() + assert commit.repository.owner.github_app_installations == [app] + redis_keys = { + f"app_to_use_for_commit_{commit.id}": str(app.id).encode(), + } + mock_redis.get.side_effect = lambda key: redis_keys.get(key) + response = get_repo_provider_service_for_specific_commit(commit, "some_name") + assert response == "the TorngitAdapter" + mock_get_instance.assert_called_once() + + data = TorngitInstanceData( + repo=RepoInfo( + name=commit.repository.name, + using_integration=True, + service_id=commit.repository.service_id, + repoid=commit.repository.repoid, + ), + owner=OwnerInfo( + service_id=commit.repository.owner.service_id, + ownerid=commit.repository.ownerid, + username=commit.repository.owner.username, + ), + installation=GithubInstallationInfo( + id=app.id, app_id=12, installation_id=1200, pem_path=None + ), + fallback_installations=None, + ) + mock_get_instance.assert_called_with( + "github", + dict( + **data, + token="the app token", + token_type_mapping=None, + on_token_refresh=None, + ), + ) + + @pytest.mark.asyncio + async def test_fetch_and_update_pull_request_information_from_commit_new_pull_commits_in_place( + self, dbsession, mocker + ): + now = datetime.utcnow() + commit = CommitFactory.create(message="", totals=None, _report_json=None) + base_commit = CommitFactory.create(repository=commit.repository) + dbsession.add(commit) + dbsession.add(base_commit) + dbsession.flush() + current_yaml = {} + get_pull_request_result = { + "base": {"branch": "master", "commitid": base_commit.commitid}, + "head": {"branch": "reason/some-testing", "commitid": commit.commitid}, + "number": "1", + "id": "1", + "state": "open", + "title": "Creating new code for reasons no one knows", + "author": {"id": "123", "username": "pr_author_username"}, + } + repository_service = mocker.MagicMock( + service="github", + get_pull_request=mock.AsyncMock(return_value=get_pull_request_result), + ) + + # Setting the pullid for the commit without flushing. This ensures that we don't try to build the pull object, + # so that it can go through the path that creates/updates the pull object from `get_pull_request_result` + commit.pullid = 1 + enriched_pull = await fetch_and_update_pull_request_information_from_commit( + repository_service, commit, current_yaml + ) + res = enriched_pull.database_pull + dbsession.flush() + dbsession.refresh(res) + assert res is not None + assert res.repoid == commit.repoid + assert res.pullid == 1 + assert res.issueid == 1 + assert res.updatestamp > now + assert res.state == "open" + assert res.title == "Creating new code for reasons no one knows" + assert res.base == base_commit.commitid + assert res.compared_to == base_commit.commitid + assert res.head == commit.commitid + assert res.commentid is None + assert res.diff is None + assert res._flare is None + assert res._flare_storage_path is None + assert ( + res.author + == dbsession.query(Owner) + .filter( + Owner.service == "github", + Owner.service_id == get_pull_request_result["author"]["id"], + Owner.username == get_pull_request_result["author"]["username"], + ) + .first() + ) + + @pytest.mark.asyncio + async def test_fetch_and_update_pull_request_information_from_commit_existing_pull_commits_in_place( + self, dbsession, mocker, repo, pull + ): + now = datetime.utcnow() + commit = CommitFactory.create( + message="", + pullid=pull.pullid, + totals=None, + _report_json=None, + repository=repo, + ) + base_commit = CommitFactory.create(repository=repo, branch="master") + dbsession.add(pull) + dbsession.add(commit) + dbsession.add(base_commit) + dbsession.flush() + current_yaml = {} + f = { + "author": { + "id": "author_id", + "username": "author_username", + "email": "email@email.com", + "name": "Mario", + }, + "message": "Merged in aaaa/coverage.py (pull request #99) Fix #123: crash", + "timestamp": datetime(2019, 10, 10), + "parents": [], + } + get_pull_request_result = { + "base": {"branch": "master", "commitid": base_commit.commitid}, + "head": {"branch": "reason/some-testing", "commitid": commit.commitid}, + "number": str(pull.pullid), + "id": str(pull.pullid), + "state": "open", + "title": "Creating new code for reasons no one knows", + "author": {"id": "123", "username": "pr_author_username"}, + } + repository_service = mocker.MagicMock( + service="github", + get_commit=mock.AsyncMock(return_value=f), + get_pull_request=mock.AsyncMock(return_value=get_pull_request_result), + ) + enriched_pull = await fetch_and_update_pull_request_information_from_commit( + repository_service, commit, current_yaml + ) + res = enriched_pull.database_pull + dbsession.flush() + dbsession.refresh(res) + assert res is not None + assert res == pull + assert res.repoid == commit.repoid + assert res.pullid == pull.pullid + assert res.issueid == pull.pullid + assert res.updatestamp > now + assert res.state == "open" + assert res.title == "Creating new code for reasons no one knows" + assert res.base == base_commit.commitid + assert res.compared_to == base_commit.commitid + assert res.head == commit.commitid + assert res.commentid is None + assert res.diff is None + assert res._flare is None + assert res._flare_storage_path is None + assert ( + res.author + == dbsession.query(Owner) + .filter( + Owner.service == "github", + Owner.service_id == get_pull_request_result["author"]["id"], + Owner.username == get_pull_request_result["author"]["username"], + ) + .first() + ) + + @pytest.mark.asyncio + async def test_fetch_and_update_pull_request_multiple_pulls_same_repo( + self, dbsession, mocker, repo, pull + ): + now = datetime.utcnow() + pull.title = "purposelly bad title" + second_pull = PullFactory.create(repository=repo) + commit = CommitFactory.create( + message="", + pullid=pull.pullid, + totals=None, + _report_json=None, + repository=repo, + ) + base_commit = CommitFactory.create(repository=repo, branch="master") + dbsession.add(pull) + dbsession.add(second_pull) + dbsession.add(commit) + dbsession.add(base_commit) + dbsession.flush() + current_yaml = {} + f = { + "author": { + "id": "author_id", + "username": "author_username", + "email": "email@email.com", + "name": "Mario", + }, + "message": "Merged in aaaa/coverage.py (pull request #99) Fix #123: crash", + "timestamp": datetime(2019, 10, 10), + "parents": [], + } + get_pull_request_result = { + "base": {"branch": "master", "commitid": base_commit.commitid}, + "head": {"branch": "reason/some-testing", "commitid": commit.commitid}, + "number": str(pull.pullid), + "id": str(pull.pullid), + "state": "open", + "title": "Creating new code for reasons no one knows", + "author": {"id": "123", "username": "pr_author_username"}, + } + + repository_service = mocker.MagicMock( + service="github", + get_commit=mock.AsyncMock(return_value=f), + get_pull_request=mock.AsyncMock(return_value=get_pull_request_result), + ) + enriched_pull = await fetch_and_update_pull_request_information_from_commit( + repository_service, commit, current_yaml + ) + res = enriched_pull.database_pull + dbsession.flush() + dbsession.refresh(res) + assert res is not None + assert res == pull + assert res != second_pull + assert res.repoid == commit.repoid + assert res.pullid == pull.pullid + assert res.issueid == pull.pullid + assert res.updatestamp > now + assert res.state == "open" + assert res.title == "Creating new code for reasons no one knows" + assert res.base == base_commit.commitid + assert res.compared_to == base_commit.commitid + assert res.head == commit.commitid + assert res.commentid is None + assert res.diff is None + assert res._flare is None + assert res._flare_storage_path is None + assert ( + res.author + == dbsession.query(Owner) + .filter( + Owner.service == "github", + Owner.service_id == get_pull_request_result["author"]["id"], + Owner.username == get_pull_request_result["author"]["username"], + ) + .first() + ) + + @pytest.mark.asyncio + async def test_fetch_and_update_pull_request_information_from_commit_different_compared_to( + self, + dbsession, + mocker, + repo, + pull, + ): + now = datetime.utcnow() + commit = CommitFactory.create( + message="", + pullid=pull.pullid, + totals=None, + _report_json=None, + repository=repo, + ) + second_comparedto_commit = CommitFactory.create( + repository=repo, + branch="master", + merged=True, + timestamp=datetime(2019, 5, 6), + ) + compared_to_commit = CommitFactory.create( + repository=repo, + branch="master", + merged=True, + timestamp=datetime(2019, 7, 15), + ) + dbsession.add(commit) + dbsession.add(second_comparedto_commit) + dbsession.add(compared_to_commit) + dbsession.flush() + current_yaml = {} + f = { + "author": { + "id": "author_id", + "username": "author_username", + "email": "email@email.com", + "name": "Mario", + }, + "message": "Merged in aaaa/coverage.py (pull request #99) Fix #123: crash", + "parents": [], + "timestamp": datetime(2019, 10, 10), + } + get_pull_request_result = { + "base": {"branch": "master", "commitid": "somecommitid"}, + "head": {"branch": "reason/some-testing", "commitid": commit.commitid}, + "number": str(pull.pullid), + "id": str(pull.pullid), + "state": "open", + "title": "Creating new code for reasons no one knows", + "author": {"id": "123", "username": "pr_author_username"}, + } + repository_service = mocker.MagicMock( + service="github", + get_commit=mock.AsyncMock(return_value=f), + get_pull_request=mock.AsyncMock(return_value=get_pull_request_result), + ) + enriched_pull = await fetch_and_update_pull_request_information_from_commit( + repository_service, commit, current_yaml + ) + res = enriched_pull.database_pull + dbsession.flush() + dbsession.refresh(res) + assert res is not None + assert res == pull + assert res.repoid == commit.repoid + assert res.pullid == pull.pullid + assert res.issueid == pull.pullid + assert res.updatestamp > now + assert res.state == "open" + assert res.title == "Creating new code for reasons no one knows" + assert res.base == "somecommitid" + assert res.compared_to == compared_to_commit.commitid + assert res.head == commit.commitid + assert res.commentid is None + assert res.diff is None + assert res._flare is None + assert res._flare_storage_path is None + assert ( + res.author + == dbsession.query(Owner) + .filter( + Owner.service == "github", + Owner.service_id == get_pull_request_result["author"]["id"], + Owner.username == get_pull_request_result["author"]["username"], + ) + .first() + ) + + @pytest.mark.asyncio + async def test_fetch_and_update_pull_request_information_no_compared_to( + self, dbsession, mocker, repo, pull + ): + now = datetime.utcnow() + compared_to_commit = CommitFactory.create( + repository=repo, branch="master", merged=True + ) + commit = CommitFactory.create( + message="", + pullid=pull.pullid, + totals=None, + _report_json=None, + repository=repo, + ) + dbsession.add(pull) + dbsession.add(commit) + dbsession.add(compared_to_commit) + dbsession.flush() + current_yaml = {} + get_pull_request_result = { + "base": {"branch": "master", "commitid": "somecommitid"}, + "head": {"branch": "reason/some-testing", "commitid": commit.commitid}, + "number": str(pull.pullid), + "id": str(pull.pullid), + "state": "open", + "title": "Creating new code for reasons no one knows", + "author": {"id": "123", "username": "pr_author_username"}, + } + repository_service = mocker.MagicMock( + service="github", + get_commit=mock.AsyncMock( + side_effect=TorngitObjectNotFoundError("response", "message") + ), + get_pull_request=mock.AsyncMock(return_value=get_pull_request_result), + ) + enriched_pull = await fetch_and_update_pull_request_information( + repository_service, dbsession, pull.repoid, pull.pullid, current_yaml + ) + res = enriched_pull.database_pull + dbsession.flush() + dbsession.refresh(res) + assert res is not None + assert res == pull + assert res.repoid == commit.repoid + assert res.pullid == pull.pullid + assert res.issueid == pull.pullid + assert res.updatestamp > now + assert res.state == "open" + assert res.title == "Creating new code for reasons no one knows" + assert res.base == "somecommitid" + assert res.compared_to is None + assert res.head is None + assert res.commentid is None + assert res.diff is None + assert res._flare is None + assert res._flare_storage_path is None + assert ( + res.author + == dbsession.query(Owner) + .filter( + Owner.service == "github", + Owner.service_id == get_pull_request_result["author"]["id"], + Owner.username == get_pull_request_result["author"]["username"], + ) + .first() + ) + + @pytest.mark.asyncio + async def test_fetch_and_update_pull_request_information_torngitexception( + self, dbsession, mocker, repo + ): + commit = CommitFactory.create( + message="", + pullid=None, + totals=None, + _report_json=None, + repository=repo, + ) + compared_to_commit = CommitFactory.create( + repository=repo, branch="master", merged=True + ) + dbsession.add(commit) + dbsession.add(compared_to_commit) + dbsession.flush() + current_yaml = {} + repository_service = mocker.MagicMock( + find_pull_request=mock.AsyncMock( + side_effect=TorngitClientError(422, "response", "message") + ) + ) + res = await fetch_and_update_pull_request_information_from_commit( + repository_service, commit, current_yaml + ) + assert res is None + + @pytest.mark.asyncio + async def test_fetch_and_update_pull_request_information_torngitexception_getting_pull( + self, dbsession, mocker, repo + ): + commit = CommitFactory.create( + message="", + totals=None, + _report_json=None, + repository=repo, + ) + compared_to_commit = CommitFactory.create( + repository=repo, branch="master", merged=True + ) + dbsession.add(commit) + dbsession.add(compared_to_commit) + dbsession.flush() + + commit.pullid = "123" + current_yaml = {} + repository_service = mocker.MagicMock( + get_pull_request=mock.AsyncMock( + side_effect=TorngitObjectNotFoundError("response", "message") + ) + ) + res = await fetch_and_update_pull_request_information_from_commit( + repository_service, commit, current_yaml + ) + assert res.database_pull is None + assert res.provider_pull is None + + @pytest.mark.asyncio + async def test_fetch_and_update_pull_request_information_torngitserverexception_getting_pull( + self, dbsession, mocker, repo, pull + ): + current_yaml = {} + repository_service = mocker.MagicMock( + get_pull_request=mock.AsyncMock(side_effect=TorngitServerUnreachableError()) + ) + res = await fetch_and_update_pull_request_information( + repository_service, dbsession, pull.repoid, pull.pullid, current_yaml + ) + assert res.database_pull == pull + assert res.provider_pull is None + + @pytest.mark.asyncio + async def test_fetch_and_update_pull_request_information_notfound_pull_already_exists( + self, dbsession, mocker, repo, pull + ): + commit = CommitFactory.create( + message="", + pullid=pull.pullid, + totals=None, + _report_json=None, + repository=repo, + ) + compared_to_commit = CommitFactory.create( + repository=repo, branch="master", merged=True + ) + dbsession.add(commit) + dbsession.add(compared_to_commit) + dbsession.flush() + current_yaml = {} + repository_service = mocker.MagicMock( + get_pull_request=mock.AsyncMock( + side_effect=TorngitObjectNotFoundError("response", "message") + ) + ) + res = await fetch_and_update_pull_request_information_from_commit( + repository_service, commit, current_yaml + ) + assert res.database_pull == pull + + @pytest.mark.asyncio + async def test_pick_best_base_comparedto_pair_no_user_provided_base_no_candidate( + self, mocker, dbsession, repo, pull + ): + async def get_commit_mocked(commit_sha): + return {"timestamp": datetime(2021, 3, 10).isoformat()} + + dbsession.flush() + repository_service = mocker.Mock( + TorngitBaseAdapter, get_commit=get_commit_mocked + ) + current_yaml = mocker.MagicMock() + pull_information = { + "base": {"commitid": "abcqwert" * 5, "branch": "basebranch"} + } + res = await _pick_best_base_comparedto_pair( + repository_service, pull, current_yaml, pull_information + ) + assert res == ("abcqwertabcqwertabcqwertabcqwertabcqwert", None) + + @pytest.mark.asyncio + async def test_pick_best_base_comparedto_pair_yes_user_provided_base_no_candidate( + self, mocker, dbsession, repo, pull + ): + async def get_commit_mocked(commit_sha): + return {"timestamp": datetime(2021, 3, 10).isoformat()} + + pull.user_provided_base_sha = "lkjhgfdslkjhgfdslkjhgfdslkjhgfdslkjhgfds" + dbsession.add(pull) + dbsession.flush() + repository_service = mocker.Mock( + TorngitBaseAdapter, get_commit=get_commit_mocked + ) + current_yaml = mocker.MagicMock() + pull_information = { + "base": {"commitid": "abcqwert" * 5, "branch": "basebranch"} + } + res = await _pick_best_base_comparedto_pair( + repository_service, pull, current_yaml, pull_information + ) + assert res == ("lkjhgfdslkjhgfdslkjhgfdslkjhgfdslkjhgfds", None) + + @pytest.mark.asyncio + async def test_pick_best_base_comparedto_pair_yes_user_provided_base_exact_match( + self, mocker, dbsession, repo, pull + ): + async def get_commit_mocked(commit_sha): + return {"timestamp": datetime(2021, 3, 10).isoformat()} + + pull.user_provided_base_sha = "1007cbfb857592b9e7cbe3ecb25748870e2c07fc" + dbsession.add(pull) + dbsession.flush() + commit = CommitFactory.create( + repository=repo, commitid="1007cbfb857592b9e7cbe3ecb25748870e2c07fc" + ) + dbsession.add(commit) + dbsession.flush() + repository_service = mocker.Mock( + TorngitBaseAdapter, get_commit=get_commit_mocked + ) + current_yaml = mocker.MagicMock() + pull_information = { + "base": {"commitid": "abcqwert" * 5, "branch": "basebranch"} + } + res = await _pick_best_base_comparedto_pair( + repository_service, pull, current_yaml, pull_information + ) + assert res == ( + "1007cbfb857592b9e7cbe3ecb25748870e2c07fc", + "1007cbfb857592b9e7cbe3ecb25748870e2c07fc", + ) + + @pytest.mark.asyncio + async def test_pick_best_base_comparedto_pair_yes_user_given_no_base_exact_match( + self, mocker, dbsession, repo, pull + ): + async def get_commit_mocked(commit_sha): + return {"timestamp": datetime(2021, 3, 10).isoformat()} + + pull.user_provided_base_sha = "1007cbfb857592b9e7cbe3ecb25748870e2c07fc" + dbsession.add(pull) + dbsession.flush() + commit = CommitFactory.create( + repository=repo, commitid="1007cbfb857592b9e7cbe3ecb25748870e2c07fc" + ) + dbsession.add(commit) + dbsession.flush() + repository_service = mocker.Mock( + TorngitBaseAdapter, get_commit=get_commit_mocked + ) + current_yaml = mocker.MagicMock() + pull_information = { + "base": {"commitid": "abcqwert" * 5, "branch": "basebranch"} + } + res = await _pick_best_base_comparedto_pair( + repository_service, pull, current_yaml, pull_information + ) + assert res == ( + "1007cbfb857592b9e7cbe3ecb25748870e2c07fc", + "1007cbfb857592b9e7cbe3ecb25748870e2c07fc", + ) + + @pytest.mark.asyncio + async def test_pick_best_base_comparedto_pair_yes_user_given_no_base_no_match( + self, mocker, dbsession, repo, pull + ): + async def get_commit_mocked(commit_sha): + return {"timestamp": datetime(2021, 3, 10).isoformat()} + + pull.user_provided_base_sha = "1007cbfb857592b9e7cbe3ecb25748870e2c07fc" + dbsession.add(pull) + dbsession.flush() + commit = CommitFactory.create( + repository=repo, + commitid="e9868516aafd365aeab2957d3745353b532d3a37", + branch="basebranch", + timestamp=datetime(2021, 3, 9), + pullid=None, + ) + other_commit = CommitFactory.create( + repository=repo, + commitid="2c07d7804dd9ff61ca5a1d6ee01de108af8cc7e0", + branch="basebranch", + timestamp=datetime(2021, 3, 11), + pullid=None, + ) + dbsession.add(commit) + dbsession.add(other_commit) + dbsession.flush() + repository_service = mocker.Mock( + TorngitBaseAdapter, get_commit=get_commit_mocked + ) + current_yaml = mocker.MagicMock() + pull_information = { + "base": {"commitid": "abcqwert" * 5, "branch": "basebranch"} + } + res = await _pick_best_base_comparedto_pair( + repository_service, pull, current_yaml, pull_information + ) + assert res == ( + "1007cbfb857592b9e7cbe3ecb25748870e2c07fc", + "e9868516aafd365aeab2957d3745353b532d3a37", + ) + + @pytest.mark.asyncio + async def test_pick_best_base_comparedto_pair_yes_user_given_not_found( + self, + mocker, + dbsession, + repo, + pull, + ): + async def get_commit_mocked(commit_sha): + if commit_sha == "1007cbfb857592b9e7cbe3ecb25748870e2c07fc": + raise TorngitObjectNotFoundError("response", "message") + return {"timestamp": datetime(2021, 3, 10).isoformat()} + + pull.user_provided_base_sha = "1007cbfb857592b9e7cbe3ecb25748870e2c07fc" + dbsession.add(pull) + dbsession.flush() + commit = CommitFactory.create( + repository=repo, + commitid="e9868516aafd365aeab2957d3745353b532d3a37", + branch="basebranch", + timestamp=datetime(2021, 3, 9), + pullid=None, + ) + other_commit = CommitFactory.create( + repository=repo, + commitid="2c07d7804dd9ff61ca5a1d6ee01de108af8cc7e0", + branch="basebranch", + timestamp=datetime(2021, 3, 11), + pullid=None, + ) + dbsession.add(commit) + dbsession.add(other_commit) + dbsession.flush() + repository_service = mocker.Mock( + TorngitBaseAdapter, get_commit=get_commit_mocked + ) + current_yaml = mocker.MagicMock() + pull_information = { + "base": {"commitid": "abcqwert" * 5, "branch": "basebranch"} + } + res = await _pick_best_base_comparedto_pair( + repository_service, pull, current_yaml, pull_information + ) + assert res == ( + "abcqwertabcqwertabcqwertabcqwertabcqwert", + "e9868516aafd365aeab2957d3745353b532d3a37", + ) + + @pytest.mark.asyncio + async def test_pick_best_base_comparedto_pair_no_user_given( + self, mocker, dbsession, repo, pull + ): + async def get_commit_mocked(commit_sha): + return {"timestamp": datetime(2021, 3, 10).isoformat()} + + commit = CommitFactory.create( + repository=repo, + commitid="e9868516aafd365aeab2957d3745353b532d3a37", + branch="basebranch", + timestamp=datetime(2021, 3, 9), + pullid=None, + ) + other_commit = CommitFactory.create( + repository=repo, + commitid="2c07d7804dd9ff61ca5a1d6ee01de108af8cc7e0", + branch="basebranch", + timestamp=datetime(2021, 3, 11), + pullid=None, + ) + dbsession.add(commit) + dbsession.add(other_commit) + dbsession.flush() + repository_service = mocker.Mock( + TorngitBaseAdapter, get_commit=get_commit_mocked + ) + current_yaml = mocker.MagicMock() + pull_information = { + "base": {"commitid": "abcqwert" * 5, "branch": "basebranch"} + } + res = await _pick_best_base_comparedto_pair( + repository_service, pull, current_yaml, pull_information + ) + assert res == ( + "abcqwertabcqwertabcqwertabcqwertabcqwert", + "e9868516aafd365aeab2957d3745353b532d3a37", + ) + + +def test_fetch_commit_yaml_and_possibly_store_only_commit_yaml( + dbsession, mocker, mock_configuration +): + commit = CommitFactory.create() + get_source_result = { + "content": "\n".join(["codecov:", " notify:", " require_ci_to_pass: yes"]) + } + list_top_level_files_result = [ + {"name": ".gitignore", "path": ".gitignore", "type": "file"}, + {"name": ".travis.yml", "path": ".travis.yml", "type": "file"}, + {"name": "README.rst", "path": "README.rst", "type": "file"}, + {"name": "awesome", "path": "awesome", "type": "folder"}, + {"name": "codecov", "path": "codecov", "type": "file"}, + {"name": "codecov.yaml", "path": "codecov.yaml", "type": "file"}, + {"name": "tests", "path": "tests", "type": "folder"}, + ] + repository_service = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=list_top_level_files_result), + get_source=mock.AsyncMock(return_value=get_source_result), + ) + + result = fetch_commit_yaml_and_possibly_store(commit, repository_service) + expected_result = {"codecov": {"notify": {}, "require_ci_to_pass": True}} + assert result.to_dict() == expected_result + repository_service.get_source.assert_called_with("codecov.yaml", commit.commitid) + repository_service.list_top_level_files.assert_called_with(commit.commitid) + + +def test_fetch_commit_yaml_and_possibly_store_commit_yaml_and_base_yaml( + dbsession, mock_configuration, mocker +): + mock_configuration.set_params({"site": {"coverage": {"precision": 14}}}) + commit = CommitFactory.create() + get_source_result = { + "content": "\n".join(["codecov:", " notify:", " require_ci_to_pass: yes"]) + } + list_top_level_files_result = [ + {"name": ".travis.yml", "path": ".travis.yml", "type": "file"}, + {"name": "awesome", "path": "awesome", "type": "folder"}, + {"name": ".codecov.yaml", "path": ".codecov.yaml", "type": "file"}, + ] + repository_service = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=list_top_level_files_result), + get_source=mock.AsyncMock(return_value=get_source_result), + ) + + result = fetch_commit_yaml_and_possibly_store(commit, repository_service) + expected_result = { + "codecov": {"notify": {}, "require_ci_to_pass": True}, + "coverage": {"precision": 14}, + } + assert result.to_dict() == expected_result + repository_service.get_source.assert_called_with(".codecov.yaml", commit.commitid) + repository_service.list_top_level_files.assert_called_with(commit.commitid) + + +def test_fetch_commit_yaml_and_possibly_store_commit_yaml_and_repo_yaml( + dbsession, mock_configuration, mocker +): + mock_configuration.set_params({"site": {"coverage": {"precision": 14}}}) + commit = CommitFactory.create( + repository__yaml={"codecov": {"max_report_age": "1y ago"}}, + repository__branch="supeduperbranch", + branch="supeduperbranch", + ) + get_source_result = { + "content": "\n".join(["codecov:", " notify:", " require_ci_to_pass: yes"]) + } + list_top_level_files_result = [ + {"name": ".gitignore", "path": ".gitignore", "type": "file"}, + {"name": ".codecov.yaml", "path": ".codecov.yaml", "type": "file"}, + {"name": "tests", "path": "tests", "type": "folder"}, + ] + repository_service = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=list_top_level_files_result), + get_source=mock.AsyncMock(return_value=get_source_result), + ) + + result = fetch_commit_yaml_and_possibly_store(commit, repository_service) + expected_result = { + "codecov": {"notify": {}, "require_ci_to_pass": True}, + "coverage": {"precision": 14}, + } + assert result.to_dict() == expected_result + assert commit.repository.yaml == { + "codecov": {"notify": {}, "require_ci_to_pass": True} + } + repository_service.get_source.assert_called_with(".codecov.yaml", commit.commitid) + repository_service.list_top_level_files.assert_called_with(commit.commitid) + + +def test_fetch_commit_yaml_and_possibly_store_commit_yaml_no_commit_yaml( + dbsession, mock_configuration, mocker +): + mock_configuration.set_params({"site": {"coverage": {"round": "up"}}}) + commit = CommitFactory.create( + repository__owner__yaml={"coverage": {"precision": 2}}, + repository__yaml={"codecov": {"max_report_age": "1y ago"}}, + repository__branch="supeduperbranch", + branch="supeduperbranch", + ) + repository_service = mocker.MagicMock( + list_top_level_files=mock.AsyncMock( + side_effect=TorngitClientError(404, "fake_response", "message") + ) + ) + + result = fetch_commit_yaml_and_possibly_store(commit, repository_service) + expected_result = { + "coverage": {"precision": 2, "round": "up"}, + "codecov": {"max_report_age": "1y ago"}, + } + assert result.to_dict() == expected_result + assert commit.repository.yaml == {"codecov": {"max_report_age": "1y ago"}} + + +def test_fetch_commit_yaml_and_possibly_store_commit_yaml_invalid_commit_yaml( + dbsession, mock_configuration, mocker +): + mock_configuration.set_params({"site": {"comment": {"behavior": "new"}}}) + commit = CommitFactory.create( + repository__owner__yaml={"coverage": {"precision": 2}}, + # User needs to be less than PATCH_CENTRIC_DEFAULT_TIME_START + repository__owner__createstamp=datetime.fromisoformat( + "2024-03-30 00:00:00.000+00:00" + ), + repository__yaml={"codecov": {"max_report_age": "1y ago"}}, + repository__branch="supeduperbranch", + branch="supeduperbranch", + ) + dbsession.add(commit) + get_source_result = { + "content": "\n".join(["bad_key:", " notify:", " require_ci_to_pass: yes"]) + } + list_top_level_files_result = [ + {"name": ".gitignore", "path": ".gitignore", "type": "file"}, + {"name": ".codecov.yaml", "path": ".codecov.yaml", "type": "file"}, + {"name": "tests", "path": "tests", "type": "folder"}, + ] + repository_service = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=list_top_level_files_result), + get_source=mock.AsyncMock(return_value=get_source_result), + ) + + result = fetch_commit_yaml_and_possibly_store(commit, repository_service) + expected_result = { + "coverage": {"precision": 2}, + "codecov": {"max_report_age": "1y ago"}, + "comment": {"behavior": "new"}, + } + assert result.to_dict() == expected_result + assert commit.repository.yaml == {"codecov": {"max_report_age": "1y ago"}} diff --git a/apps/worker/services/tests/test_seats.py b/apps/worker/services/tests/test_seats.py new file mode 100644 index 0000000000..95df921e40 --- /dev/null +++ b/apps/worker/services/tests/test_seats.py @@ -0,0 +1,167 @@ +import pytest +from shared.plan.constants import PlanName + +from database.tests.factories import OwnerFactory, PullFactory +from services.repository import EnrichedPull +from services.seats import ShouldActivateSeat, determine_seat_activation +from tests.helpers import mock_all_plans_and_tiers + + +def test_seat_provider_none(dbsession): + pull = PullFactory() + dbsession.add(pull) + dbsession.flush() + + pull.repository.private = True + dbsession.flush() + + enriched_pull = EnrichedPull( + database_pull=pull, + provider_pull=None, + ) + activate_seat_info = determine_seat_activation(enriched_pull) + + assert activate_seat_info.should_activate_seat == ShouldActivateSeat.NO_ACTIVATE + assert activate_seat_info.reason == "no_provider_pull" + + +def test_seat_repo_public(dbsession): + pull = PullFactory() + dbsession.add(pull) + dbsession.flush() + + pull.repository.private = False + dbsession.flush() + + enriched_pull = EnrichedPull( + database_pull=pull, + provider_pull={"author": {"id": "100", "username": "test_username"}}, + ) + activate_seat_info = determine_seat_activation(enriched_pull) + + assert activate_seat_info.should_activate_seat == ShouldActivateSeat.NO_ACTIVATE + assert activate_seat_info.reason == "public_repo" + + +@pytest.mark.django_db +def test_seat_billing_plan(dbsession): + mock_all_plans_and_tiers() + pull = PullFactory() + dbsession.add(pull) + dbsession.flush() + + pull.repository.private = True + pull.repository.owner.plan = PlanName.CODECOV_PRO_MONTHLY_LEGACY.value + dbsession.flush() + + enriched_pull = EnrichedPull( + database_pull=pull, + provider_pull={"author": {"id": "100", "username": "test_username"}}, + ) + activate_seat_info = determine_seat_activation(enriched_pull) + + assert activate_seat_info.should_activate_seat == ShouldActivateSeat.NO_ACTIVATE + assert activate_seat_info.reason == "no_pr_billing_plan" + + +@pytest.mark.django_db +def test_seat_no_author(dbsession): + mock_all_plans_and_tiers() + pull = PullFactory() + dbsession.add(pull) + dbsession.flush() + + pull.repository.private = True + pull.repository.owner.plan = PlanName.CODECOV_PRO_MONTHLY.value + dbsession.flush() + + enriched_pull = EnrichedPull( + database_pull=pull, + provider_pull={"author": {"id": "100", "username": "test_username"}}, + ) + activate_seat_info = determine_seat_activation(enriched_pull) + + assert activate_seat_info.should_activate_seat == ShouldActivateSeat.NO_ACTIVATE + assert activate_seat_info.reason == "no_pr_author" + + +@pytest.mark.django_db +def test_seat_author_in_org(dbsession): + mock_all_plans_and_tiers() + pull = PullFactory() + dbsession.add(pull) + dbsession.flush() + + pull.repository.private = True + pull.repository.owner.plan = PlanName.CODECOV_PRO_MONTHLY.value + pull.repository.owner.service = "github" + dbsession.flush() + + author = OwnerFactory(service="github", service_id=100) + dbsession.add(author) + dbsession.flush() + + pull.repository.owner.plan_activated_users = [author.ownerid] + dbsession.flush() + + enriched_pull = EnrichedPull( + database_pull=pull, + provider_pull={"author": {"id": "100", "username": "test_username"}}, + ) + activate_seat_info = determine_seat_activation(enriched_pull) + + assert activate_seat_info.should_activate_seat == ShouldActivateSeat.NO_ACTIVATE + assert activate_seat_info.reason == "author_in_plan_activated_users" + + +@pytest.mark.django_db +def test_seat_author_not_in_org(dbsession): + mock_all_plans_and_tiers() + pull = PullFactory() + dbsession.add(pull) + dbsession.flush() + + pull.repository.private = True + pull.repository.owner.plan = PlanName.CODECOV_PRO_MONTHLY.value + pull.repository.owner.service = "github" + dbsession.flush() + + author = OwnerFactory(service="github", service_id=100) + dbsession.add(author) + dbsession.flush() + + enriched_pull = EnrichedPull( + database_pull=pull, + provider_pull={"author": {"id": "100", "username": "test_username"}}, + ) + activate_seat_info = determine_seat_activation(enriched_pull) + + assert activate_seat_info.should_activate_seat == ShouldActivateSeat.MANUAL_ACTIVATE + assert activate_seat_info.reason == "manual_activate" + + +@pytest.mark.django_db +def test_seat_author_auto_activate(dbsession): + mock_all_plans_and_tiers() + pull = PullFactory() + dbsession.add(pull) + dbsession.flush() + + pull.repository.private = True + pull.repository.owner.plan = PlanName.CODECOV_PRO_MONTHLY.value + pull.repository.owner.plan_auto_activate = True + pull.repository.owner.service = "github" + dbsession.flush() + + author = OwnerFactory(service="github", service_id=100) + dbsession.add(author) + dbsession.flush() + + enriched_pull = EnrichedPull( + database_pull=pull, + provider_pull={"author": {"id": "100", "username": "test_username"}}, + ) + activate_seat_info = determine_seat_activation(enriched_pull) + + assert activate_seat_info.should_activate_seat == ShouldActivateSeat.AUTO_ACTIVATE + assert activate_seat_info.reason == "auto_activate" diff --git a/apps/worker/services/tests/test_smtp.py b/apps/worker/services/tests/test_smtp.py new file mode 100644 index 0000000000..0b89e83d50 --- /dev/null +++ b/apps/worker/services/tests/test_smtp.py @@ -0,0 +1,314 @@ +import logging +from smtplib import ( + SMTPAuthenticationError, + SMTPConnectError, + SMTPDataError, + SMTPNotSupportedError, + SMTPRecipientsRefused, + SMTPResponseException, + SMTPSenderRefused, + SMTPServerDisconnected, +) +from unittest.mock import MagicMock, call + +import pytest + +import services.smtp +from helpers.email import Email +from services.smtp import SMTPService, SMTPServiceError + +LOGGER = logging.getLogger(__name__) + +to_addr = "test_to@codecov.io" +from_addr = "test_from@codecov.io" +test_email = Email( + from_addr=from_addr, + subject="Test subject", + text="Hello world", + to_addr=to_addr, +) + + +@pytest.fixture +def set_username_and_password(mock_configuration): + mock_configuration._params["services"]["smtp"]["username"] = "test_username" + mock_configuration._params["services"]["smtp"]["password"] = "test_password" + + +@pytest.fixture +def reset_connection_at_start(): + services.smtp.SMTPService.connection = None + + +class TestSMTP(object): + def test_correct_init( + self, + mocker, + mock_configuration, + set_username_and_password, + reset_connection_at_start, + ): + mocker.patch("smtplib.SMTP") + + m = mocker.patch("ssl.create_default_context", return_value=MagicMock()) + service = SMTPService() + service.connection.starttls.assert_called_with(context=m.return_value) + service.connection.login.assert_called_with("test_username", "test_password") + + def test_idempotentconnectionection(self, mocker, mock_configuration): + first = SMTPService() + firstconnection = first.connection + second = SMTPService() + secondconnection = second.connection + assert id(firstconnection) == id(secondconnection) + + def test_empty_config(self, mocker, mock_configuration, reset_connection_at_start): + del mock_configuration._params["services"]["smtp"] + service = SMTPService() + assert service.connection is None + + def test_send(self, mocker, mock_configuration): + mocker.patch("smtplib.SMTP") + email = Email( + to_addr="test_to@codecov.io", + from_addr="test_from@codecov.io", + subject="Test subject", + text="test text", + html="test html", + ) + + smtp = SMTPService() + smtp.send(email=email) + + smtp.connection.send_message.assert_called_with(email.message) + + def test_send_email_recipients_refused( + self, mocker, mock_configuration, dbsession, reset_connection_at_start + ): + m = MagicMock() + m.configure_mock(**{"send_message.side_effect": SMTPRecipientsRefused(to_addr)}) + mocker.patch( + "smtplib.SMTP", + return_value=m, + ) + + smtp = SMTPService() + + with pytest.raises(SMTPServiceError, match="All recipients were refused"): + smtp.send(email=test_email) + + def test_send_email_sender_refused( + self, mocker, mock_configuration, dbsession, reset_connection_at_start + ): + m = MagicMock() + m.configure_mock( + **{"send_message.side_effect": SMTPSenderRefused(123, "", to_addr)} + ) + mocker.patch( + "smtplib.SMTP", + return_value=m, + ) + + smtp = SMTPService() + + with pytest.raises(SMTPServiceError, match="Sender was refused"): + smtp.send(email=test_email) + + def test_send_email_data_error( + self, mocker, mock_configuration, dbsession, reset_connection_at_start + ): + m = MagicMock() + m.configure_mock(**{"send_message.side_effect": SMTPDataError(123, "")}) + mocker.patch( + "smtplib.SMTP", + return_value=m, + ) + + smtp = SMTPService() + + with pytest.raises( + SMTPServiceError, match="The SMTP server did not accept the data" + ): + smtp.send(email=test_email) + + def test_send_email_sends_errs( + self, mocker, mock_configuration, dbsession, reset_connection_at_start + ): + m = MagicMock() + m.configure_mock(**{"send_message.return_value": [(123, "abc"), (456, "def")]}) + mocker.patch( + "smtplib.SMTP", + return_value=m, + ) + + smtp = SMTPService() + + with pytest.raises(SMTPServiceError, match="123 abc 456 def"): + smtp.send(email=test_email) + + def test_smtp_active(self, mocker, mock_configuration, dbsession): + smtp = SMTPService() + assert smtp.active() == True + SMTPService.connection = None + assert smtp.active() == False + + def test_smtp_disconnected( + self, + mocker, + mock_configuration, + dbsession, + set_username_and_password, + reset_connection_at_start, + ): + m = MagicMock() + m.configure_mock(**{"noop.side_effect": SMTPServerDisconnected()}) + mocker.patch( + "smtplib.SMTP", + return_value=m, + ) + email = Email( + to_addr="test_to@codecov.io", + from_addr="test_from@codecov.io", + subject="Test subject", + text="test text", + html="test html", + ) + + smtp = SMTPService() + + smtp.send(email) + + smtp.connection.connect.assert_has_calls([call("mailhog", 1025)]) + smtp.connection.starttls.assert_has_calls( + [call(context=smtp.ssl_context), call(context=smtp.ssl_context)] + ) + smtp.connection.login.assert_has_calls( + [ + call("test_username", "test_password"), + call("test_username", "test_password"), + ] + ) + smtp.connection.noop.assert_has_calls([call()]) + smtp.connection.send_message(call(email.message)) + + def test_smtp_init_connect_fail( + self, mocker, mock_configuration, dbsession, reset_connection_at_start + ): + m = MagicMock() + mocker.patch("smtplib.SMTP", side_effect=SMTPConnectError(123, "abc")) + email = Email( + to_addr="test_to@codecov.io", + from_addr="test_from@codecov.io", + subject="Test subject", + text="test text", + html="test html", + ) + + with pytest.raises( + SMTPServiceError, match="Error starting connection for SMTPService" + ): + smtp = SMTPService() + + def test_smtp_disconnected_fail( + self, mocker, mock_configuration, dbsession, reset_connection_at_start + ): + m = MagicMock() + m.configure_mock( + **{ + "noop.side_effect": SMTPServerDisconnected(), + "connect.side_effect": SMTPConnectError(123, "abc"), + } + ) + mocker.patch( + "smtplib.SMTP", + return_value=m, + ) + email = Email( + to_addr="test_to@codecov.io", + from_addr="test_from@codecov.io", + subject="Test subject", + text="test text", + html="test html", + ) + + with pytest.raises( + SMTPServiceError, match="Error starting connection for SMTPService" + ): + smtp = SMTPService() + smtp.send(email) + + @pytest.mark.parametrize( + "fn, err_msg, side_effect", + [ + ( + "starttls", + "Error doing STARTTLS command on SMTP", + SMTPResponseException(123, "abc"), + ), + ( + "login", + "SMTP server did not accept username/password combination", + SMTPAuthenticationError(123, "abc"), + ), + ], + ) + def test_smtp_tls_not_supported( + self, + caplog, + mocker, + mock_configuration, + dbsession, + reset_connection_at_start, + set_username_and_password, + fn, + err_msg, + side_effect, + ): + m = MagicMock() + m.configure_mock(**{f"{fn}.side_effect": side_effect}) + mocker.patch( + "smtplib.SMTP", + return_value=m, + ) + + with caplog.at_level(logging.WARNING): + with pytest.raises(SMTPServiceError, match=err_msg): + smtp = SMTPService() + + assert err_msg in caplog.text + + @pytest.mark.parametrize( + "fn, err_msg", + [ + ( + "starttls", + "Server does not support TLS, continuing initialization of SMTP connection", + ), + ( + "login", + "Server does not support AUTH, continuing initialization of SMTP connection", + ), + ], + ) + def test_smtp_not_supported( + self, + caplog, + mocker, + mock_configuration, + dbsession, + reset_connection_at_start, + set_username_and_password, + fn, + err_msg, + ): + m = MagicMock() + m.configure_mock(**{f"{fn}.side_effect": SMTPNotSupportedError()}) + mocker.patch( + "smtplib.SMTP", + return_value=m, + ) + + with caplog.at_level(logging.WARNING): + smtp = SMTPService() + + assert err_msg in caplog.text diff --git a/apps/worker/services/tests/test_template.py b/apps/worker/services/tests/test_template.py new file mode 100644 index 0000000000..6c7a6937c5 --- /dev/null +++ b/apps/worker/services/tests/test_template.py @@ -0,0 +1,48 @@ +import pytest +from jinja2.exceptions import TemplateNotFound, UndefinedError + +from services.template import TemplateService + + +class TestTemplate(object): + def test_get_template(self): + ts = TemplateService() + template = ts.get_template("test.txt") + populated_template = template.render(**dict(username="test_username")) + assert populated_template == "Test template test_username" + + def test_get_template_html(self): + ts = TemplateService() + template = ts.get_template("test.html") + populated_template = template.render(**dict(username="test_username")) + expected_result = """ + + + + + + Document + + + +

    + test template test_username +

    + + +""" + for expected_line, actual_line in zip( + expected_result.splitlines(), populated_template.splitlines() + ): + assert expected_line == actual_line + + def test_get_template_no_kwargs(self): + ts = TemplateService() + template = ts.get_template("test.txt") + with pytest.raises(UndefinedError): + template.render(not_username="") + + def test_get_template_non_existing(self): + ts = TemplateService() + with pytest.raises(TemplateNotFound): + ts.get_template("nonexistent") diff --git a/apps/worker/services/tests/test_test_results.py b/apps/worker/services/tests/test_test_results.py new file mode 100644 index 0000000000..fdbb873d1f --- /dev/null +++ b/apps/worker/services/tests/test_test_results.py @@ -0,0 +1,309 @@ +import mock +import pytest +from shared.plan.constants import DEFAULT_FREE_PLAN +from shared.torngit.exceptions import TorngitClientError + +from database.tests.factories import ( + CommitFactory, + OwnerFactory, + RepositoryFactory, +) +from helpers.notifier import NotifierResult +from services.test_results import ( + ErrorPayload, + FlakeInfo, + TACommentInDepthInfo, + TestResultsNotificationFailure, + TestResultsNotificationPayload, + TestResultsNotifier, + generate_failure_info, + generate_flags_hash, + generate_test_id, + should_do_flaky_detection, +) +from services.urls import services_short_dict +from services.yaml import UserYaml +from tests.helpers import mock_all_plans_and_tiers + + +def mock_repo_service(): + repo_service = mock.Mock( + post_comment=mock.AsyncMock(), + edit_comment=mock.AsyncMock(), + ) + return repo_service + + +def test_send_to_provider(): + tn = TestResultsNotifier(CommitFactory(), None) + tn._pull = mock.Mock() + tn._pull.database_pull.commentid = None + tn._repo_service = mock_repo_service() + m = dict(id=1) + tn._repo_service.post_comment.return_value = m + + res = tn.send_to_provider(tn._pull, "hello world") + + assert res == True + + tn._repo_service.post_comment.assert_called_with( + tn._pull.database_pull.pullid, "hello world" + ) + assert tn._pull.database_pull.commentid == 1 + + +def test_send_to_provider_edit(): + tn = TestResultsNotifier(CommitFactory(), None) + tn._pull = mock.Mock() + tn._pull.database_pull.commentid = 1 + tn._repo_service = mock_repo_service() + m = dict(id=1) + tn._repo_service.edit_comment.return_value = m + + res = tn.send_to_provider(tn._pull, "hello world") + + assert res == True + tn._repo_service.edit_comment.assert_called_with( + tn._pull.database_pull.pullid, 1, "hello world" + ) + + +def test_send_to_provider_fail(): + tn = TestResultsNotifier(CommitFactory(), None) + tn._pull = mock.Mock() + tn._pull.database_pull.commentid = 1 + tn._repo_service = mock_repo_service() + tn._repo_service.edit_comment.side_effect = TorngitClientError + + res = tn.send_to_provider(tn._pull, "hello world") + + assert res == False + + +def test_generate_failure_info(): + flags_hash = generate_flags_hash([]) + test_id = generate_test_id(1, "testsuite", "testname", flags_hash) + fail = TestResultsNotificationFailure( + "hello world", + "testname", + [], + test_id, + 1.0, + "https://example.com/build_url", + ) + + res = generate_failure_info(fail) + + assert ( + res + == """ +```python +hello world +``` + +[View](https://example.com/build_url) the CI Build""" + ) + + +def test_build_message(): + flags_hash = generate_flags_hash([]) + test_id = generate_test_id(1, "testsuite", "testname", flags_hash) + fail = TestResultsNotificationFailure( + "hello world", + "testname", + [], + test_id, + 1.0, + "https://example.com/build_url", + ) + info = TACommentInDepthInfo(failures=[fail], flaky_tests={}) + payload = TestResultsNotificationPayload(1, 2, 3, info) + commit = CommitFactory(branch="thing/thing") + tn = TestResultsNotifier(commit, None, None, None, payload) + res = tn.build_message() + + assert ( + res + == f"""### :x: 1 Tests Failed: +| Tests completed | Failed | Passed | Skipped | +|---|---|---|---| +| 3 | 1 | 2 | 3 | +
    View the top 1 failed test(s) by shortest run time + +> +> ```python +> testname +> ``` +> +>
    Stack Traces | 1s run time +> +> > +> > ```python +> > hello world +> > ``` +> > +> > [View](https://example.com/build_url) the CI Build +> +>
    + +
    + +To view more test analytics, go to the [Test Analytics Dashboard](https://app.codecov.io/{services_short_dict.get(commit.repository.service)}/{commit.repository.owner.username}/{commit.repository.name}/tests/thing%2Fthing) +📋 Got 3 mins? [Take this short survey](https://forms.gle/BpocVj23nhr2Y45G7) to help us improve Test Analytics.""" + ) + + +def test_build_message_with_flake(): + flags_hash = generate_flags_hash([]) + test_id = generate_test_id(1, "testsuite", "testname", flags_hash) + fail = TestResultsNotificationFailure( + "hello world", + "testname", + [], + test_id, + 1.0, + "https://example.com/build_url", + ) + flaky_test = FlakeInfo(1, 3) + info = TACommentInDepthInfo(failures=[fail], flaky_tests={test_id: flaky_test}) + payload = TestResultsNotificationPayload(1, 2, 3, info) + commit = CommitFactory(branch="test_branch") + tn = TestResultsNotifier(commit, None, None, None, payload) + res = tn.build_message() + + assert ( + res + == f"""### :x: 1 Tests Failed: +| Tests completed | Failed | Passed | Skipped | +|---|---|---|---| +| 3 | 1 | 2 | 3 | +
    View the full list of 1 :snowflake: flaky tests + +> +> ```python +> testname +> ``` +> +> **Flake rate in main:** 33.33% (Passed 2 times, Failed 1 times) +>
    Stack Traces | 1s run time +> +> > +> > ```python +> > hello world +> > ``` +> > +> > [View](https://example.com/build_url) the CI Build +> +>
    + +
    + +To view more test analytics, go to the [Test Analytics Dashboard](https://app.codecov.io/{services_short_dict.get(commit.repository.service)}/{commit.repository.owner.username}/{commit.repository.name}/tests/{commit.branch}) +📋 Got 3 mins? [Take this short survey](https://forms.gle/BpocVj23nhr2Y45G7) to help us improve Test Analytics.""" + ) + + +def test_notify(mocker): + mocker.patch("helpers.notifier.get_repo_provider_service", return_value=mock.Mock()) + mocker.patch( + "helpers.notifier.fetch_and_update_pull_request_information_from_commit", + return_value=mock.Mock(), + ) + tn = TestResultsNotifier(CommitFactory(), None, _pull=mock.Mock()) + tn.build_message = mock.Mock() + tn.send_to_provider = mock.Mock() + + notification_result = tn.notify() + + assert notification_result == NotifierResult.COMMENT_POSTED + + +def test_notify_fail_torngit_error( + mocker, +): + mocker.patch("helpers.notifier.get_repo_provider_service", return_value=mock.Mock()) + mocker.patch( + "helpers.notifier.fetch_and_update_pull_request_information_from_commit", + return_value=mock.Mock(), + ) + tn = TestResultsNotifier(CommitFactory(), None, _pull=mock.Mock()) + tn.build_message = mock.Mock() + tn.send_to_provider = mock.Mock(return_value=False) + + notification_result = tn.notify() + + assert notification_result == NotifierResult.TORNGIT_ERROR + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "config,private,plan,ex_result", + [ + (False, False, "users-inappm", False), + (True, True, DEFAULT_FREE_PLAN, False), + (True, False, DEFAULT_FREE_PLAN, True), + (True, False, "users-inappm", True), + (True, True, "users-inappm", True), + ], +) +def test_should_do_flake_detection(dbsession, mocker, config, private, plan, ex_result): + mock_all_plans_and_tiers() + owner = OwnerFactory(plan=plan) + repo = RepositoryFactory(private=private, owner=owner) + dbsession.add(repo) + dbsession.flush() + + yaml = {"test_analytics": {"flake_detection": config}} + + result = should_do_flaky_detection(repo, UserYaml.from_dict(yaml)) + + assert result == ex_result + + +def test_specific_error_message(mocker): + mock_repo_service = mock.AsyncMock() + mocker.patch( + "helpers.notifier.get_repo_provider_service", return_value=mock_repo_service + ) + mocker.patch( + "helpers.notifier.fetch_and_update_pull_request_information_from_commit", + return_value=mock.AsyncMock(), + ) + + error = ErrorPayload( + "unsupported_file_format", + "Error parsing JUnit XML in test.xml at 4:32: ParserError: No name found", + ) + tn = TestResultsNotifier(CommitFactory(), None, error=error) + result = tn.error_comment() + expected = """### :x: Unsupported file format + +> Upload processing failed due to unsupported file format. Please review the parser error message: +> `Error parsing JUnit XML in test.xml at 4:32: ParserError: No name found` +> For more help, visit our [troubleshooting guide](https://docs.codecov.com/docs/test-analytics#troubleshooting). +""" + + assert result == (True, "comment_posted") + mock_repo_service.edit_comment.assert_called_with( + tn._pull.database_pull.pullid, tn._pull.database_pull.commentid, expected + ) + + +def test_specific_error_message_no_error(mocker): + mock_repo_service = mock.AsyncMock() + mocker.patch( + "helpers.notifier.get_repo_provider_service", return_value=mock_repo_service + ) + mocker.patch( + "helpers.notifier.fetch_and_update_pull_request_information_from_commit", + return_value=mock.AsyncMock(), + ) + + tn = TestResultsNotifier(CommitFactory(), None) + result = tn.error_comment() + expected = """:x: We are unable to process any of the uploaded JUnit XML files. Please ensure your files are in the right format.""" + + assert result == (True, "comment_posted") + mock_repo_service.edit_comment.assert_called_with( + tn._pull.database_pull.pullid, tn._pull.database_pull.commentid, expected + ) diff --git a/apps/worker/services/tests/test_timeseries.py b/apps/worker/services/tests/test_timeseries.py new file mode 100644 index 0000000000..8f595d815b --- /dev/null +++ b/apps/worker/services/tests/test_timeseries.py @@ -0,0 +1,1159 @@ +from datetime import datetime, timezone + +import pytest +from celery import group +from shared.reports.readonly import ReadOnlyReport +from shared.reports.reportfile import ReportFile +from shared.reports.resources import Report +from shared.reports.types import ReportLine +from shared.utils.sessions import Session +from shared.yaml import UserYaml + +from database.models.timeseries import Dataset, Measurement, MeasurementName +from database.tests.factories import CommitFactory, RepositoryFactory +from database.tests.factories.reports import RepositoryFlagFactory +from database.tests.factories.timeseries import DatasetFactory, MeasurementFactory +from services.timeseries import ( + backfill_batch_size, + delete_repository_data, + delete_repository_measurements, + repository_commits_query, + repository_datasets_query, +) +from tasks.save_commit_measurements import save_commit_measurements + + +@pytest.fixture +def sample_report(): + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=["flag1", "flag2"])) + return report + + +@pytest.fixture +def sample_report_for_components(): + report = Report() + first_file = ReportFile("poker.py") + first_file.append(1, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(2, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file = ReportFile("folder/poker2.py") + second_file.append(3, ReportLine.create(coverage=0, sessions=[[0, 0]])) + second_file.append(4, ReportLine.create(coverage=1, sessions=[[0, 1]])) + third_file = ReportFile("random.go") + third_file.append(5, ReportLine.create(coverage=0, sessions=[[0, 0]])) + third_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 0]])) + third_file.append(8, ReportLine.create(coverage=0, sessions=[[0, 1]])) + third_file.append(7, ReportLine.create(coverage=1, sessions=[[0, 0]])) + report.append(first_file) + report.append(second_file) + report.append(third_file) + report.add_session( + Session(flags=["test-flag-123", "test-flag-456", "random-flago-987"]) + ) + return report + + +def _create_repository(dbsession): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + + coverage_dataset = DatasetFactory.create( + repository_id=repository.repoid, + name=MeasurementName.coverage.value, + backfilled=True, + ) + dbsession.add(coverage_dataset) + flag_coverage_dataset = DatasetFactory.create( + repository_id=repository.repoid, + name=MeasurementName.flag_coverage.value, + backfilled=False, + ) + dbsession.add(flag_coverage_dataset) + component_coverage_dataset = DatasetFactory.create( + repository_id=repository.repoid, + name=MeasurementName.component_coverage.value, + backfilled=False, + ) + dbsession.add(component_coverage_dataset) + dbsession.flush() + + return repository + + +@pytest.fixture +def repository(dbsession): + return _create_repository(dbsession) + + +@pytest.fixture +def dataset_names(): + return [ + MeasurementName.coverage.value, + MeasurementName.flag_coverage.value, + MeasurementName.component_coverage.value, + ] + + +class TestTimeseriesService(object): + def test_insert_commit_measurement( + self, dbsession, sample_report, repository, dataset_names, mocker + ): + mocker.patch( + "services.report.ReportService.get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report(sample_report), + ) + + commit = CommitFactory.create(branch="foo", repository=repository) + dbsession.add(commit) + dbsession.flush() + + save_commit_measurements(commit, dataset_names=dataset_names) + + measurement = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.coverage.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + ) + .one_or_none() + ) + + assert measurement + assert measurement.name == MeasurementName.coverage.value + assert measurement.owner_id == commit.repository.ownerid + assert measurement.repo_id == commit.repoid + assert measurement.measurable_id == f"{commit.repoid}" + assert measurement.commit_sha == commit.commitid + assert measurement.timestamp.replace( + tzinfo=timezone.utc + ) == commit.timestamp.replace(tzinfo=timezone.utc) + assert measurement.branch == "foo" + assert measurement.value == 60.0 + + def test_save_commit_measurements_no_report( + self, dbsession, repository, dataset_names, mocker + ): + mocker.patch( + "services.report.ReportService.get_existing_report_for_commit", + return_value=None, + ) + + commit = CommitFactory.create(branch="foo", repository=repository) + dbsession.add(commit) + dbsession.flush() + + save_commit_measurements(commit, dataset_names=dataset_names) + + measurement = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.coverage.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + ) + .one_or_none() + ) + + assert measurement is None + + def test_update_commit_measurement( + self, dbsession, sample_report, repository, dataset_names, mocker + ): + mocker.patch( + "services.report.ReportService.get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report(sample_report), + ) + + commit = CommitFactory.create(branch="foo", repository=repository) + dbsession.add(commit) + dbsession.flush() + + measurement = MeasurementFactory.create( + name=MeasurementName.coverage.value, + owner_id=commit.repository.ownerid, + repo_id=commit.repoid, + measurable_id=commit.repoid, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + branch="testing", + value=0, + ) + dbsession.add(measurement) + dbsession.flush() + + save_commit_measurements(commit, dataset_names=dataset_names) + + measurements = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.coverage.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + ) + .all() + ) + + assert len(measurements) == 1 + measurement = measurements[0] + assert measurement.name == MeasurementName.coverage.value + assert measurement.owner_id == commit.repository.ownerid + assert measurement.repo_id == commit.repoid + assert measurement.measurable_id == f"{commit.repoid}" + assert measurement.commit_sha == commit.commitid + assert measurement.timestamp.replace( + tzinfo=timezone.utc + ) == commit.timestamp.replace(tzinfo=timezone.utc) + assert measurement.branch == "foo" + assert measurement.value == 60.0 + + def test_commit_measurement_insert_flags( + self, dbsession, sample_report, repository, dataset_names, mocker + ): + mocker.patch( + "services.report.ReportService.get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report(sample_report), + ) + + commit = CommitFactory.create(branch="foo", repository=repository) + dbsession.add(commit) + dbsession.flush() + + repository_flag1 = RepositoryFlagFactory( + repository=commit.repository, flag_name="flag1" + ) + dbsession.add(repository_flag1) + dbsession.flush() + + repository_flag2 = RepositoryFlagFactory( + repository=commit.repository, flag_name="flag2" + ) + dbsession.add(repository_flag2) + dbsession.flush() + + save_commit_measurements(commit, dataset_names=dataset_names) + + measurement = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.flag_coverage.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id=f"{repository_flag1.id}", + ) + .one_or_none() + ) + + assert measurement + assert measurement.name == MeasurementName.flag_coverage.value + assert measurement.owner_id == commit.repository.ownerid + assert measurement.repo_id == commit.repoid + assert measurement.measurable_id == f"{repository_flag1.id}" + assert measurement.commit_sha == commit.commitid + assert measurement.timestamp.replace( + tzinfo=timezone.utc + ) == commit.timestamp.replace(tzinfo=timezone.utc) + assert measurement.branch == "foo" + assert measurement.value == 100.0 + + measurement = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.flag_coverage.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id=f"{repository_flag2.id}", + ) + .one_or_none() + ) + + assert measurement + assert measurement.name == MeasurementName.flag_coverage.value + assert measurement.owner_id == commit.repository.ownerid + assert measurement.repo_id == commit.repoid + assert measurement.measurable_id == f"{repository_flag2.id}" + assert measurement.commit_sha == commit.commitid + assert measurement.timestamp.replace( + tzinfo=timezone.utc + ) == commit.timestamp.replace(tzinfo=timezone.utc) + assert measurement.branch == "foo" + assert measurement.value == 100.0 + + def test_commit_measurement_update_flags( + self, dbsession, sample_report, repository, dataset_names, mocker + ): + mocker.patch( + "services.report.ReportService.get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report(sample_report), + ) + + commit = CommitFactory.create(branch="foo", repository=repository) + dbsession.add(commit) + dbsession.flush() + + repository_flag1 = RepositoryFlagFactory( + repository=commit.repository, flag_name="flag1" + ) + dbsession.add(repository_flag1) + dbsession.flush() + + repository_flag2 = RepositoryFlagFactory( + repository=commit.repository, flag_name="flag2" + ) + dbsession.add(repository_flag2) + dbsession.flush() + + measurement1 = MeasurementFactory.create( + name=MeasurementName.flag_coverage.value, + owner_id=commit.repository.ownerid, + repo_id=commit.repoid, + measurable_id=repository_flag1.id, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + branch="testing", + value=0, + ) + dbsession.add(measurement1) + dbsession.flush() + + measurement2 = MeasurementFactory.create( + name=MeasurementName.flag_coverage.value, + owner_id=commit.repository.ownerid, + repo_id=commit.repoid, + measurable_id=repository_flag2.id, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + branch="testing", + value=0, + ) + dbsession.add(measurement2) + dbsession.flush() + + save_commit_measurements(commit, dataset_names=dataset_names) + + measurement = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.flag_coverage.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id=f"{repository_flag1.id}", + ) + .one_or_none() + ) + + assert measurement + assert measurement.name == MeasurementName.flag_coverage.value + assert measurement.owner_id == commit.repository.ownerid + assert measurement.repo_id == commit.repoid + assert measurement.measurable_id == f"{repository_flag1.id}" + assert measurement.commit_sha == commit.commitid + assert measurement.timestamp.replace( + tzinfo=timezone.utc + ) == commit.timestamp.replace(tzinfo=timezone.utc) + assert measurement.branch == "foo" + assert measurement.value == 100.0 + + measurement = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.flag_coverage.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id=f"{repository_flag2.id}", + ) + .one_or_none() + ) + + assert measurement + assert measurement.name == MeasurementName.flag_coverage.value + assert measurement.owner_id == commit.repository.ownerid + assert measurement.repo_id == commit.repoid + assert measurement.measurable_id == f"{repository_flag2.id}" + assert measurement.commit_sha == commit.commitid + assert measurement.timestamp.replace( + tzinfo=timezone.utc + ) == commit.timestamp.replace(tzinfo=timezone.utc) + assert measurement.branch == "foo" + assert measurement.value == 100.0 + + def test_commit_measurement_insert_components( + self, dbsession, sample_report_for_components, repository, dataset_names, mocker + ): + mocker.patch( + "tasks.save_commit_measurements.PARALLEL_COMPONENT_COMPARISON.check_value", + return_value=False, + ) + mocker.patch( + "services.report.ReportService.get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report( + sample_report_for_components + ), + ) + + commit = CommitFactory.create(branch="foo", repository=repository) + dbsession.add(commit) + dbsession.flush() + + get_repo_yaml = mocker.patch("tasks.save_commit_measurements.get_repo_yaml") + yaml_dict = { + "component_management": { + "default_rules": { + "paths": [r".*\.go"], + "flag_regexes": [r"test-flag-*"], + }, + "individual_components": [ + {"component_id": "python_files", "paths": [r".*\.py"]}, + {"component_id": "rules_from_default"}, + { + "component_id": "i_have_flags", + "flag_regexes": [r"random-.*"], + }, + { + "component_id": "all_settings", + "name": "all settings", + "flag_regexes": [], + "paths": [r"folder/*"], + }, + { # testing duplicate component on purpose this was causing crashes + "component_id": "all_settings", + "name": "all settings", + "flag_regexes": [], + "paths": [r"folder/*"], + }, + { + "component_id": "path_not_found", + "name": "no expected covarage", + "flag_regexes": [], + "paths": ["asdfasdf"], + }, + { + "component_id": "empty_path", + "name": "no expected covarage", + "flag_regexes": [], + "paths": [], + }, + ], + } + } + get_repo_yaml.return_value = UserYaml(yaml_dict) + save_commit_measurements(commit, dataset_names=dataset_names) + + # 1 for coverage, 3 for flags, 4 for valid components + assert len(dbsession.query(Measurement).all()) == 8 + + python_file_measurement = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.component_coverage.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id="python_files", + ) + .one_or_none() + ) + assert python_file_measurement + assert python_file_measurement.name == MeasurementName.component_coverage.value + assert python_file_measurement.owner_id == commit.repository.ownerid + assert python_file_measurement.repo_id == commit.repoid + assert python_file_measurement.measurable_id == "python_files" + assert python_file_measurement.commit_sha == commit.commitid + assert python_file_measurement.timestamp.replace( + tzinfo=timezone.utc + ) == commit.timestamp.replace(tzinfo=timezone.utc) + assert python_file_measurement.branch == "foo" + assert python_file_measurement.value == 75.0 + + default_component_settings_measurement = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.component_coverage.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id="rules_from_default", + ) + .one_or_none() + ) + assert default_component_settings_measurement + assert ( + default_component_settings_measurement.name + == MeasurementName.component_coverage.value + ) + assert ( + default_component_settings_measurement.owner_id == commit.repository.ownerid + ) + assert default_component_settings_measurement.repo_id == commit.repoid + assert ( + default_component_settings_measurement.measurable_id == "rules_from_default" + ) + assert default_component_settings_measurement.commit_sha == commit.commitid + assert default_component_settings_measurement.timestamp.replace( + tzinfo=timezone.utc + ) == commit.timestamp.replace(tzinfo=timezone.utc) + assert default_component_settings_measurement.branch == "foo" + assert default_component_settings_measurement.value == 25.0 + + manual_flags_measurements = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.component_coverage.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id="i_have_flags", + ) + .one_or_none() + ) + assert manual_flags_measurements + assert ( + manual_flags_measurements.name == MeasurementName.component_coverage.value + ) + assert manual_flags_measurements.owner_id == commit.repository.ownerid + assert manual_flags_measurements.repo_id == commit.repoid + assert manual_flags_measurements.measurable_id == "i_have_flags" + assert manual_flags_measurements.commit_sha == commit.commitid + assert manual_flags_measurements.timestamp.replace( + tzinfo=timezone.utc + ) == commit.timestamp.replace(tzinfo=timezone.utc) + assert manual_flags_measurements.branch == "foo" + assert manual_flags_measurements.value == 25.0 + + all_settings_measurements = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.component_coverage.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id="all_settings", + ) + .one_or_none() + ) + assert all_settings_measurements + assert ( + all_settings_measurements.name == MeasurementName.component_coverage.value + ) + assert all_settings_measurements.owner_id == commit.repository.ownerid + assert all_settings_measurements.repo_id == commit.repoid + assert all_settings_measurements.measurable_id == "all_settings" + assert all_settings_measurements.commit_sha == commit.commitid + assert all_settings_measurements.timestamp.replace( + tzinfo=timezone.utc + ) == commit.timestamp.replace(tzinfo=timezone.utc) + assert all_settings_measurements.branch == "foo" + assert all_settings_measurements.value == 50.0 + + path_not_found_measurements = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.component_coverage.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id="path_not_found", + ) + .one_or_none() + ) + assert path_not_found_measurements is None + + empty_path_measurements = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.component_coverage.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id="empty_path", + ) + .one_or_none() + ) + assert empty_path_measurements is None + + def test_commit_measurement_update_component_parallel( + self, + sample_report_for_components, + repository, + dataset_names, + mocker, + mock_repo_provider, + ): + dbsession = repository.get_db_session() + mocker.patch.object(dbsession, "close") + mocker.patch("tasks.base.get_db_session", return_value=dbsession) + mocker.patch.object(group, "apply_async", group.apply) + + mocker.patch( + "tasks.save_commit_measurements.PARALLEL_COMPONENT_COMPARISON.check_value", + return_value=True, + ) + + mocker.patch( + "services.report.ReportService.get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report( + sample_report_for_components + ), + ) + + commit = CommitFactory.create(branch="foo", repository=repository) + dbsession.add(commit) + dbsession.flush() + + get_repo_yaml = mocker.patch("tasks.save_commit_measurements.get_repo_yaml") + get_current_yaml = mocker.patch("tasks.upsert_component.get_repo_yaml") + yaml_dict = { + "component_management": { + "individual_components": [ + { + "component_id": "test-component-123", + "name": "test component", + "flag_regexes": ["random-flago-987"], + "paths": [r"folder/*"], + }, + ], + } + } + get_repo_yaml.return_value = UserYaml(yaml_dict) + get_current_yaml.return_value = UserYaml(yaml_dict) + + save_commit_measurements(commit, dataset_names=dataset_names) + + # Want to commit here to have the results persisted properly. + # Otherwise the results aren't going to be reflected in the select below. + # dbsession.commit() + + measurements = ( + dbsession.query(Measurement) + .filter_by(name=MeasurementName.component_coverage.value) + .all() + ) + + assert len(measurements) == 1 + dbsession.add(commit) + assert measurements[0].name == MeasurementName.component_coverage.value + assert measurements[0].owner_id == commit.repository.ownerid + assert measurements[0].repo_id == commit.repoid + assert measurements[0].measurable_id == "test-component-123" + assert measurements[0].commit_sha == commit.commitid + assert measurements[0].timestamp.replace( + tzinfo=timezone.utc + ) == commit.timestamp.replace(tzinfo=timezone.utc) + + def test_commit_measurement_update_component( + self, dbsession, sample_report_for_components, repository, dataset_names, mocker + ): + mocker.patch( + "tasks.save_commit_measurements.PARALLEL_COMPONENT_COMPARISON.check_value", + return_value=False, + ) + mocker.patch( + "services.report.ReportService.get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report( + sample_report_for_components + ), + ) + + commit = CommitFactory.create(branch="foo", repository=repository) + dbsession.add(commit) + dbsession.flush() + + get_repo_yaml = mocker.patch("tasks.save_commit_measurements.get_repo_yaml") + yaml_dict = { + "component_management": { + "individual_components": [ + { + "component_id": "test-component-123", + "name": "test component", + "flag_regexes": ["random-flago-987"], + "paths": [r"folder/*"], + }, + ], + } + } + get_repo_yaml.return_value = UserYaml(yaml_dict) + + measurement = MeasurementFactory.create( + name=MeasurementName.component_coverage.value, + owner_id=commit.repository.ownerid, + repo_id=commit.repoid, + measurable_id="test-component-123", + commit_sha=commit.commitid, + timestamp=commit.timestamp, + branch="testing", + value=0, + ) + dbsession.add(measurement) + dbsession.flush() + + save_commit_measurements(commit, dataset_names=dataset_names) + + # Want to commit here to have the results persisted properly. + # Otherwise the results aren't going to be reflected in the select below. + dbsession.commit() + + measurement = ( + dbsession.query(Measurement) + .filter_by( + name=MeasurementName.component_coverage.value, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + measurable_id="test-component-123", + ) + .one_or_none() + ) + + assert measurement + assert measurement.name == MeasurementName.component_coverage.value + assert measurement.owner_id == commit.repository.ownerid + assert measurement.repo_id == commit.repoid + assert measurement.measurable_id == "test-component-123" + assert measurement.commit_sha == commit.commitid + assert measurement.timestamp.replace( + tzinfo=timezone.utc + ) == commit.timestamp.replace(tzinfo=timezone.utc) + assert measurement.branch == "foo" + assert measurement.value == 50.0 + + def test_commit_measurement_no_datasets( + self, mock_storage, dbsession, dataset_names, mocker + ): + mocker.patch( + "tasks.save_commit_measurements.PARALLEL_COMPONENT_COMPARISON.check_value", + return_value=False, + ) + + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + + commit = CommitFactory.create(branch="foo", repository=repository) + dbsession.add(commit) + dbsession.flush() + + save_commit_measurements(commit, dataset_names=[]) + + assert dbsession.query(Measurement).count() == 0 + + def test_repository_commits_query(self, dbsession, repository, mocker): + commit1 = CommitFactory.create( + repository=repository, + timestamp=datetime(2022, 6, 1, 0, 0, 0).replace(tzinfo=timezone.utc), + ) + dbsession.add(commit1) + commit2 = CommitFactory.create( + repository=repository, + timestamp=datetime(2022, 6, 10, 0, 0, 0).replace(tzinfo=timezone.utc), + ) + dbsession.add(commit2) + commit3 = CommitFactory.create( + repository=repository, + timestamp=datetime(2022, 6, 17, 0, 0, 0).replace(tzinfo=timezone.utc), + ) + dbsession.add(commit3) + commit4 = CommitFactory.create( + timestamp=datetime(2022, 6, 10, 0, 0, 0).replace(tzinfo=timezone.utc) + ) + dbsession.add(commit4) + dbsession.flush() + + commits = repository_commits_query( + repository, + start_date=datetime(2022, 6, 1, 0, 0, 0).replace(tzinfo=timezone.utc), + end_date=datetime(2022, 6, 15, 0, 0, 0).replace(tzinfo=timezone.utc), + ) + + assert len(list(commits)) == 2 + assert commits[0].id_ == commit2.id_ + assert commits[1].id_ == commit1.id_ + + def test_repository_datasets_query(self, repository): + datasets = repository_datasets_query(repository) + assert [dataset.name for dataset in datasets] == [ + MeasurementName.coverage.value, + MeasurementName.flag_coverage.value, + MeasurementName.component_coverage.value, + ] + + datasets = repository_datasets_query(repository, backfilled=True) + assert [dataset.name for dataset in datasets] == [ + MeasurementName.coverage.value, + ] + + datasets = repository_datasets_query(repository, backfilled=False) + assert [dataset.name for dataset in datasets] == [ + MeasurementName.flag_coverage.value, + MeasurementName.component_coverage.value, + ] + + def test_backfill_batch_size(self, repository, mocker): + mocker.patch( + "tasks.save_commit_measurements.PARALLEL_COMPONENT_COMPARISON.check_value", + return_value=False, + ) + dbsession = repository.get_db_session() + coverage_dataset = ( + dbsession.query(Dataset.name) + .filter_by( + repository_id=repository.repoid, name=MeasurementName.coverage.value + ) + .first() + ) + flag_coverage_dataset = ( + dbsession.query(Dataset.name) + .filter_by( + repository_id=repository.repoid, + name=MeasurementName.flag_coverage.value, + ) + .first() + ) + component_coverage_dataset = ( + dbsession.query(Dataset.name) + .filter_by( + repository_id=repository.repoid, + name=MeasurementName.component_coverage.value, + ) + .first() + ) + + # Initially batch size is 500 for all measurement names + batch_size = backfill_batch_size(repository, coverage_dataset) + assert batch_size == 500 + batch_size = backfill_batch_size(repository, flag_coverage_dataset) + assert batch_size == 500 + batch_size = backfill_batch_size(repository, component_coverage_dataset) + assert batch_size == 500 + + dbsession = repository.get_db_session() + flag1 = RepositoryFlagFactory(repository=repository, flag_name="flag1") + flag2 = RepositoryFlagFactory(repository=repository, flag_name="flag2") + dbsession.add(flag1) + dbsession.add(flag2) + dbsession.flush() + + # Adding flags should only affect flag coverage measurement + batch_size = backfill_batch_size(repository, coverage_dataset) + assert batch_size == 500 + batch_size = backfill_batch_size(repository, flag_coverage_dataset) + assert batch_size == 250 + batch_size = backfill_batch_size(repository, component_coverage_dataset) + assert batch_size == 500 + + get_repo_yaml = mocker.patch("services.timeseries.get_repo_yaml") + yaml_dict = { + "component_management": { + "default_rules": { + "paths": [r".*\.go"], + "flag_regexes": [r"test-flag-*"], + }, + "individual_components": [ + {"component_id": "component_1"}, + {"component_id": "component_2"}, + {"component_id": "component_3"}, + {"component_id": "component_4"}, + {"component_id": "component_5"}, + ], + } + } + get_repo_yaml.return_value = UserYaml(yaml_dict) + + # Adding componets should only affect component coverage measurement + batch_size = backfill_batch_size(repository, coverage_dataset) + assert batch_size == 500 + batch_size = backfill_batch_size(repository, flag_coverage_dataset) + assert batch_size == 250 + batch_size = backfill_batch_size(repository, component_coverage_dataset) + assert batch_size == 100 + + def test_delete_repository_data( + self, dbsession, sample_report, repository, dataset_names, mocker + ): + mocker.patch( + "services.report.ReportService.get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report(sample_report), + ) + + commit = CommitFactory.create(branch="foo", repository=repository) + dbsession.add(commit) + dbsession.flush() + save_commit_measurements(commit, dataset_names=dataset_names) + commit = CommitFactory.create(branch="bar", repository=repository) + dbsession.add(commit) + dbsession.flush() + save_commit_measurements(commit, dataset_names=dataset_names) + + assert ( + dbsession.query(Dataset).filter_by(repository_id=repository.repoid).count() + == 3 + ) + # repo coverage + 2x flag coverage for each commit + assert ( + dbsession.query(Measurement).filter_by(repo_id=repository.repoid).count() + == 6 + ) + + delete_repository_data(repository) + + assert ( + dbsession.query(Dataset).filter_by(repository_id=repository.repoid).count() + == 0 + ) + assert ( + dbsession.query(Measurement).filter_by(repo_id=repository.repoid).count() + == 0 + ) + + def test_delete_repository_data_side_effects( + self, dbsession, sample_report, repository, dataset_names, mocker + ): + mocker.patch( + "tasks.save_commit_measurements.PARALLEL_COMPONENT_COMPARISON.check_value", + return_value=False, + ) + mocker.patch( + "services.report.ReportService.get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report(sample_report), + ) + + commit = CommitFactory.create(branch="foo", repository=repository) + dbsession.add(commit) + dbsession.flush() + save_commit_measurements(commit, dataset_names=dataset_names) + commit = CommitFactory.create(branch="bar", repository=repository) + dbsession.add(commit) + dbsession.flush() + save_commit_measurements(commit, dataset_names=dataset_names) + + # Another unrelated repository, make sure that this one isn't deleted as a side effect + other_repository = _create_repository(dbsession) + other_commit = CommitFactory.create(branch="foo", repository=other_repository) + dbsession.add(other_commit) + dbsession.flush() + save_commit_measurements(other_commit, dataset_names=dataset_names) + other_commit = CommitFactory.create(branch="bar", repository=other_repository) + dbsession.add(other_commit) + dbsession.flush() + save_commit_measurements(other_commit, dataset_names=dataset_names) + + assert ( + dbsession.query(Dataset) + .filter_by(repository_id=other_repository.repoid) + .count() + != 0 + ) + assert ( + dbsession.query(Measurement) + .filter_by(repo_id=other_repository.repoid) + .count() + != 0 + ) + + delete_repository_data(repository) + + # Intended repo data/measurement is deleted + assert ( + dbsession.query(Dataset).filter_by(repository_id=repository.repoid).count() + == 0 + ) + assert ( + dbsession.query(Measurement).filter_by(repo_id=repository.repoid).count() + == 0 + ) + + # Other repo data/measurement is not deleted + assert ( + dbsession.query(Dataset) + .filter_by(repository_id=other_repository.repoid) + .count() + != 0 + ) + assert ( + dbsession.query(Measurement) + .filter_by(repo_id=other_repository.repoid) + .count() + != 0 + ) + + def test_delete_repository_data_measurements_only( + self, + dbsession, + sample_report_for_components, + repository, + dataset_names, + mocker, + mock_repo_provider, + ): + def validate_invariants(repository, other_repository): + assert ( + dbsession.query(Dataset) + .filter_by(repository_id=repository.repoid) + .count() + == 3 + ) + assert ( + dbsession.query(Dataset) + .filter_by(repository_id=other_repository.repoid) + .count() + == 3 + ) + # 2x(1 coverage, 3 flag coverage, 4 component coverage) + assert ( + dbsession.query(Measurement) + .filter_by(repo_id=other_repository.repoid) + .count() + == 16 + ) + + mocker.patch( + "tasks.save_commit_measurements.PARALLEL_COMPONENT_COMPARISON.check_value", + return_value=True, + ) + dbsession = repository.get_db_session() + mocker.patch.object(dbsession, "close") + mocker.patch("tasks.base.get_db_session", return_value=dbsession) + mocker.patch.object(group, "apply_async", group.apply) + + mocker.patch( + "services.report.ReportService.get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report( + sample_report_for_components + ), + ) + + get_repo_yaml = mocker.patch("tasks.save_commit_measurements.get_repo_yaml") + get_current_yaml = mocker.patch("tasks.upsert_component.get_repo_yaml") + yaml_dict = { + "component_management": { + "default_rules": { + "paths": [r".*\.go"], + "flag_regexes": [r"test-flag-*"], + }, + "individual_components": [ + {"component_id": "python_files", "paths": [r".*\.py"]}, + {"component_id": "rules_from_default"}, + { + "component_id": "i_have_flags", + "flag_regexes": [r"random-.*"], + }, + { + "component_id": "all_settings", + "name": "all settings", + "flag_regexes": [], + "paths": [r"folder/*"], + }, + ], + } + } + get_repo_yaml.return_value = UserYaml(yaml_dict) + get_current_yaml.return_value = UserYaml(yaml_dict) + + commit = CommitFactory.create(branch="foo", repository=repository) + dbsession.add(commit) + dbsession.flush() + save_commit_measurements(commit, dataset_names=dataset_names) + commit = CommitFactory.create(branch="bar", repository=repository) + dbsession.add(commit) + dbsession.flush() + save_commit_measurements(commit, dataset_names=dataset_names) + + # Another unrelated repository, make sure that this one isn't deleted as a side effect + other_repository = _create_repository(dbsession) + other_commit = CommitFactory.create(branch="foo", repository=other_repository) + dbsession.add(other_commit) + dbsession.flush() + save_commit_measurements(other_commit, dataset_names=dataset_names) + other_commit = CommitFactory.create(branch="bar", repository=other_repository) + dbsession.add(other_commit) + dbsession.flush() + save_commit_measurements(other_commit, dataset_names=dataset_names) + + flag_ids = set( + [ + flag.measurable_id + for flag in ( + dbsession.query(Measurement).filter_by( + repo_id=repository.repoid, + name=MeasurementName.flag_coverage.value, + ) + ) + ] + ) + + m = dbsession.query(Measurement).filter_by(repo_id=repository.repoid).all() + + # 2x(1 coverage, 3 flag coverage, 4 component coverage) = 16 + assert ( + dbsession.query(Measurement).filter_by(repo_id=repository.repoid).count() + == 16 + ) + validate_invariants(repository, other_repository) + + # Delete the coverage type + delete_repository_measurements( + repository, MeasurementName.coverage.value, f"{repository.repoid}" + ) + + # 2x(0 coverage, 3 flag coverage, 4 component coverage) = 14 + assert ( + dbsession.query(Measurement).filter_by(repo_id=repository.repoid).count() + == 14 + ) + validate_invariants(repository, other_repository) + + # Delete the flag coverages + expected_measurement_count = 14 + for flag_id in flag_ids: + assert ( + dbsession.query(Measurement) + .filter_by(repo_id=repository.repoid) + .count() + == expected_measurement_count + ) + validate_invariants(repository, other_repository) + delete_repository_measurements( + repository, MeasurementName.flag_coverage.value, f"{flag_id}" + ) + # Lose a flag coverage measurement from each commit (ie total should be 2 less) + expected_measurement_count -= 2 + + # 2x(0 coverage, 0 flag coverage, 4 component coverage) = 8 + assert ( + dbsession.query(Measurement).filter_by(repo_id=repository.repoid).count() + == expected_measurement_count + ) + validate_invariants(repository, other_repository) + + for component in yaml_dict["component_management"]["individual_components"]: + assert ( + dbsession.query(Measurement) + .filter_by(repo_id=repository.repoid) + .count() + == expected_measurement_count + ) + validate_invariants(repository, other_repository) + component_id = component["component_id"] + delete_repository_measurements( + repository, MeasurementName.component_coverage.value, component_id + ) + # Lose a component coverage measurement from each commit (ie total should be 2 less) + expected_measurement_count -= 2 + + # 2x(0 coverage, 0 flag coverage, 0 component coverage) = 0 + assert ( + dbsession.query(Measurement).filter_by(repo_id=repository.repoid).count() + == expected_measurement_count + ) + validate_invariants(repository, other_repository) diff --git a/apps/worker/services/tests/test_urls.py b/apps/worker/services/tests/test_urls.py new file mode 100644 index 0000000000..54ab2f1e99 --- /dev/null +++ b/apps/worker/services/tests/test_urls.py @@ -0,0 +1,89 @@ +from database.tests.factories import OwnerFactory, PullFactory, RepositoryFactory +from services.urls import append_tracking_params_to_urls, get_members_url, get_plan_url + + +def test_append_tracking_params_to_urls(): + message = [ + "[This link](https://stage.codecov.io/gh/test_repo/pull/pull123?src=pr&el=h1) should be changed", + "And [this one](https://codecov.io/bb/test_repo/pull) too, plus also [this one](codecov.io)", + "However, [this one](https://www.xkcd.com/) should not be changed since it does not link to Codecov", + "(Also should not replace this parenthetical non-link reference to codecov.io)", + "Also should recognize that these are two separate URLs: [banana](https://codecov.io/pokemon)and[banana](https://codecov.io/pokemon)", + ] + + service = "github" + notification_type = "comment" + org_name = "Acme Corporation" + + expected_result = [ + "[This link](https://stage.codecov.io/gh/test_repo/pull/pull123?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Acme+Corporation) should be changed", + "And [this one](https://codecov.io/bb/test_repo/pull?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Acme+Corporation) too, plus also [this one](codecov.io?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Acme+Corporation)", + "However, [this one](https://www.xkcd.com/) should not be changed since it does not link to Codecov", + "(Also should not replace this parenthetical non-link reference to codecov.io)", + "Also should recognize that these are two separate URLs: [banana](https://codecov.io/pokemon?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Acme+Corporation)and[banana](https://codecov.io/pokemon?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Acme+Corporation)", + ] + result = [ + append_tracking_params_to_urls( + m, service=service, notification_type=notification_type, org_name=org_name + ) + for m in message + ] + + assert result == expected_result + + +class TestURLs(object): + def test_gitlab_url_username_swap(self, dbsession): + base_for_member_url = "https://app.codecov.io/members/" + base_for_plan_url = "https://app.codecov.io/plan/" + + github_org = OwnerFactory.create( + service="github", + username="gh", + ) + dbsession.add(github_org) + r = RepositoryFactory.create(owner=github_org) + dbsession.add(r) + gh_pull = PullFactory.create(repository=r) + dbsession.add(gh_pull) + dbsession.flush() + member_url = get_members_url(gh_pull) + assert member_url == base_for_member_url + "gh/gh" + + gitlab_root_org = OwnerFactory.create(service="gitlab", username="gl_root") + dbsession.add(gitlab_root_org) + r = RepositoryFactory.create(owner=gitlab_root_org) + dbsession.add(r) + gl_root_pull = PullFactory.create(repository=r) + dbsession.add(gl_root_pull) + dbsession.flush() + plan_url = get_plan_url(gl_root_pull) + assert plan_url == base_for_plan_url + "gl/gl_root" + + gitlab_mid_org = OwnerFactory.create( + service="gitlab", + username="gl_mid", + parent_service_id=gitlab_root_org.service_id, + ) + dbsession.add(gitlab_mid_org) + r = RepositoryFactory.create(owner=gitlab_mid_org) + dbsession.add(r) + gl_mid_pull = PullFactory.create(repository=r) + dbsession.add(gl_mid_pull) + dbsession.flush() + member_url = get_members_url(gl_mid_pull) + assert member_url == base_for_member_url + "gl/gl_root" + + gitlab_sub_org = OwnerFactory.create( + service="gitlab", + username="gl_child", + parent_service_id=gitlab_mid_org.service_id, + ) + dbsession.add(gitlab_sub_org) + r = RepositoryFactory.create(owner=gitlab_sub_org) + dbsession.add(r) + gl_child_pull = PullFactory.create(repository=r) + dbsession.add(gl_child_pull) + dbsession.flush() + plan_url = get_plan_url(gl_child_pull) + assert plan_url == base_for_plan_url + "gl/gl_root" diff --git a/apps/worker/services/tests/unit/test_archive_service.py b/apps/worker/services/tests/unit/test_archive_service.py new file mode 100644 index 0000000000..79b46bba9d --- /dev/null +++ b/apps/worker/services/tests/unit/test_archive_service.py @@ -0,0 +1,105 @@ +import json + +from shared.storage import MinioStorageService + +from database.tests.factories import RepositoryFactory +from services.archive import ArchiveService +from test_utils.base import BaseTestCase + + +class TestArchiveService(BaseTestCase): + def test_read_file_hard_to_decode(self, mocker): + mock_read_file = mocker.patch.object(MinioStorageService, "read_file") + mock_read_file.return_value = b"\x80abc" + repo = RepositoryFactory.create() + service = ArchiveService(repo) + expected_result = b"\x80abc" + path = "path/to/file" + result = service.read_file(path) + assert expected_result == result + + +class TestWriteJsonData(BaseTestCase): + def test_write_report_details_to_storage(self, mocker, dbsession): + repo = RepositoryFactory() + dbsession.add(repo) + dbsession.flush() + mock_write_file = mocker.patch.object(MinioStorageService, "write_file") + + data = [ + { + "filename": "file_1.go", + "file_index": 0, + "file_totals": [0, 8, 5, 3, 0, "62.50000", 0, 0, 0, 0, 10, 2, 0], + "diff_totals": None, + }, + { + "filename": "file_2.py", + "file_index": 1, + "file_totals": [0, 2, 1, 0, 1, "50.00000", 1, 0, 0, 0, 0, 0, 0], + "diff_totals": None, + }, + ] + archive_service = ArchiveService(repository=repo) + commitid = "some-commit-sha" + external_id = "some-uuid4-id" + path = archive_service.write_json_data_to_storage( + commit_id=commitid, + table="reports_reportdetails", + field="files_array", + external_id=external_id, + data=data, + ) + assert ( + path + == f"v4/repos/{archive_service.storage_hash}/commits/{commitid}/json_data/reports_reportdetails/files_array/{external_id}.json" + ) + mock_write_file.assert_called_with( + archive_service.root, + path, + json.dumps(data), + is_already_gzipped=False, + reduced_redundancy=False, + ) + + def test_write_report_details_to_storage_no_commitid(self, mocker, dbsession): + repo = RepositoryFactory() + dbsession.add(repo) + dbsession.flush() + mock_write_file = mocker.patch.object(MinioStorageService, "write_file") + + data = [ + { + "filename": "file_1.go", + "file_index": 0, + "file_totals": [0, 8, 5, 3, 0, "62.50000", 0, 0, 0, 0, 10, 2, 0], + "diff_totals": None, + }, + { + "filename": "file_2.py", + "file_index": 1, + "file_totals": [0, 2, 1, 0, 1, "50.00000", 1, 0, 0, 0, 0, 0, 0], + "diff_totals": None, + }, + ] + archive_service = ArchiveService(repository=repo) + commitid = None + external_id = "some-uuid4-id" + path = archive_service.write_json_data_to_storage( + commit_id=commitid, + table="reports_reportdetails", + field="files_array", + external_id=external_id, + data=data, + ) + assert ( + path + == f"v4/repos/{archive_service.storage_hash}/json_data/reports_reportdetails/files_array/{external_id}.json" + ) + mock_write_file.assert_called_with( + archive_service.root, + path, + json.dumps(data), + is_already_gzipped=False, + reduced_redundancy=False, + ) diff --git a/apps/worker/services/tests/unit/test_bots.py b/apps/worker/services/tests/unit/test_bots.py new file mode 100644 index 0000000000..afa5d4a920 --- /dev/null +++ b/apps/worker/services/tests/unit/test_bots.py @@ -0,0 +1,668 @@ +import datetime +from typing import List, Optional +from unittest.mock import patch + +import pytest +from shared.bots import get_adapter_auth_information +from shared.bots.types import AdapterAuthInformation +from shared.rate_limits import gh_app_key_name, owner_key_name +from shared.torngit.base import TokenType +from shared.typings.oauth_token_types import Token +from shared.typings.torngit import GithubInstallationInfo +from shared.utils.test_utils import mock_config_helper + +from database.models.core import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, +) +from database.tests.factories.core import OwnerFactory, RepositoryFactory + + +def get_github_integration_token_side_effect( + service: str, + installation_id: int = None, + app_id: Optional[str] = None, + pem_path: Optional[str] = None, +): + return f"installation_token_{installation_id}_{app_id}" + + +# The tests for this fn also exist on shared. These, however, are testing the sqlalchemy implementation of them +class TestGettingAdapterAuthInformation(object): + class TestGitHubOwnerNoRepoInfo(object): + def _generate_test_owner( + self, + dbsession, + *, + with_bot: bool, + integration_id: int | None = None, + ghapp_installations: List[GithubAppInstallation] = None, + ): + if ghapp_installations is None: + ghapp_installations = [] + owner = OwnerFactory( + service="github", + bot=None, + unencrypted_oauth_token="owner_token: :refresh_token", + integration_id=integration_id, + ) + if with_bot: + owner.bot = OwnerFactory( + service="github", + unencrypted_oauth_token="bot_token: :bot_refresh_token", + ) + dbsession.add(owner) + dbsession.flush() + + if ghapp_installations: + for app in ghapp_installations: + app.owner = owner + dbsession.add(app) + + dbsession.flush() + + assert bool(owner.bot) == with_bot + assert owner.github_app_installations == ghapp_installations + + return owner + + def test_select_owner_info(self, dbsession): + owner = self._generate_test_owner(dbsession, with_bot=False) + expected = AdapterAuthInformation( + token=Token( + key="owner_token", + refresh_token="refresh_token", + secret=None, + entity_name=owner_key_name(owner.ownerid), + ), + token_owner=owner, + selected_installation_info=None, + fallback_installations=None, + token_type_mapping=None, + ) + assert get_adapter_auth_information(owner) == expected + + def test_select_owner_bot_info(self, dbsession): + owner = self._generate_test_owner(dbsession, with_bot=True) + expected = AdapterAuthInformation( + token=Token( + key="bot_token", + refresh_token="bot_refresh_token", + secret=None, + entity_name=owner_key_name(owner.bot.ownerid), + ), + token_owner=owner.bot, + selected_installation_info=None, + fallback_installations=None, + token_type_mapping=None, + ) + assert get_adapter_auth_information(owner) == expected + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + def test_select_owner_single_installation(self, dbsession): + installations = [ + GithubAppInstallation( + repository_service_ids=None, + installation_id=1200, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=200, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ) + ] + owner = self._generate_test_owner( + dbsession, with_bot=False, ghapp_installations=installations + ) + expected = AdapterAuthInformation( + token=Token( + key="installation_token_1200_200", + entity_name="200_1200", + username="installation_1200", + ), + token_owner=None, + selected_installation_info=GithubInstallationInfo( + id=installations[0].id, + installation_id=1200, + app_id=200, + pem_path="pem_path", + ), + fallback_installations=[], + token_type_mapping=None, + ) + assert get_adapter_auth_information(owner) == expected + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + def test_select_owner_single_installation_ignoring_installations( + self, dbsession + ): + installations = [ + GithubAppInstallation( + repository_service_ids=None, + installation_id=1200, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=200, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ) + ] + owner = self._generate_test_owner( + dbsession, with_bot=False, ghapp_installations=installations + ) + expected = AdapterAuthInformation( + token=Token( + key="owner_token", + refresh_token="refresh_token", + secret=None, + entity_name=owner_key_name(owner.ownerid), + ), + token_owner=owner, + selected_installation_info=None, + fallback_installations=None, + token_type_mapping=None, + ) + assert ( + get_adapter_auth_information(owner, ignore_installations=True) + == expected + ) + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + def test_select_owner_deprecated_using_integration(self, dbsession): + owner = self._generate_test_owner( + dbsession, with_bot=False, integration_id=1500 + ) + owner.oauth_token = None + # Owner has no GithubApp, no token, and no bot configured + # The integration_id is selected + expected = AdapterAuthInformation( + token=Token( + key="installation_token_1500_None", + entity_name=gh_app_key_name( + installation_id=owner.integration_id, app_id=None + ), + username="installation_1500", + ), + token_owner=None, + selected_installation_info=GithubInstallationInfo(installation_id=1500), + fallback_installations=[], + token_type_mapping=None, + ) + assert get_adapter_auth_information(owner) == expected + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + def test_select_owner_multiple_installations_default_name(self, dbsession): + installations = [ + GithubAppInstallation( + repository_service_ids=None, + installation_id=1200, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=200, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ), + # This should be ignored in the selection because of the name + GithubAppInstallation( + repository_service_ids=None, + installation_id=1300, + name="my_dedicated_app", + app_id=300, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ), + ] + owner = self._generate_test_owner( + dbsession, with_bot=False, ghapp_installations=installations + ) + expected = AdapterAuthInformation( + token=Token( + key="installation_token_1200_200", + entity_name="200_1200", + username="installation_1200", + ), + token_owner=None, + selected_installation_info=GithubInstallationInfo( + id=installations[0].id, + installation_id=1200, + app_id=200, + pem_path="pem_path", + ), + fallback_installations=[], + token_type_mapping=None, + ) + assert get_adapter_auth_information(owner) == expected + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + def test_select_owner_multiple_installations_custom_name(self, dbsession): + installations = [ + GithubAppInstallation( + repository_service_ids=None, + installation_id=1200, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=200, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ), + # This should be selected first + GithubAppInstallation( + repository_service_ids=None, + installation_id=1300, + name="my_dedicated_app", + app_id=300, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ), + ] + owner = self._generate_test_owner( + dbsession, with_bot=False, ghapp_installations=installations + ) + expected = AdapterAuthInformation( + token=Token( + key="installation_token_1300_300", + entity_name="300_1300", + username="installation_1300", + ), + token_owner=None, + selected_installation_info=GithubInstallationInfo( + id=installations[1].id, + installation_id=1300, + app_id=300, + pem_path="pem_path", + ), + fallback_installations=[ + GithubInstallationInfo( + id=installations[0].id, + installation_id=1200, + app_id=200, + pem_path="pem_path", + ) + ], + token_type_mapping=None, + ) + assert ( + get_adapter_auth_information( + owner, installation_name_to_use="my_dedicated_app" + ) + == expected + ) + + class TestGitHubOwnerWithRepoInfo(object): + def _generate_test_repo( + self, + dbsession, + *, + with_bot: bool, + with_owner_bot: bool, + integration_id: int | None = None, + ghapp_installations: List[GithubAppInstallation] = None, + ): + if ghapp_installations is None: + ghapp_installations = [] + owner = OwnerFactory( + service="github", + bot=None, + unencrypted_oauth_token="owner_token: :refresh_token", + integration_id=integration_id, + ) + if with_owner_bot: + owner.bot = OwnerFactory( + service="github", + unencrypted_oauth_token="bot_token: :bot_refresh_token", + ) + dbsession.add(owner) + dbsession.flush() + + if ghapp_installations: + for app in ghapp_installations: + app.owner = owner + dbsession.add(app) + + dbsession.flush() + + repo = RepositoryFactory( + owner=owner, using_integration=(integration_id is not None) + ) + if with_bot: + repo.bot = OwnerFactory( + service="github", + unencrypted_oauth_token="repo_bot_token: :repo_bot_refresh_token", + ) + + dbsession.add(repo) + dbsession.flush() + + assert bool(owner.bot) == with_owner_bot + assert bool(repo.bot) == with_bot + assert owner.github_app_installations == ghapp_installations + + return repo + + def test_select_repo_info_fallback_to_owner(self, dbsession): + repo = self._generate_test_repo( + dbsession, with_bot=False, with_owner_bot=False + ) + expected = AdapterAuthInformation( + token=Token( + key="owner_token", + refresh_token="refresh_token", + secret=None, + username=repo.owner.username, + entity_name=owner_key_name(repo.owner.ownerid), + ), + token_owner=repo.owner, + selected_installation_info=None, + fallback_installations=None, + token_type_mapping=None, + ) + assert get_adapter_auth_information(repo.owner, repo) == expected + + def test_select_owner_bot_info(self, dbsession): + repo = self._generate_test_repo( + dbsession, with_owner_bot=True, with_bot=False + ) + expected = AdapterAuthInformation( + token=Token( + key="bot_token", + refresh_token="bot_refresh_token", + secret=None, + username=repo.owner.bot.username, + entity_name=owner_key_name(repo.owner.bot.ownerid), + ), + token_owner=repo.owner.bot, + selected_installation_info=None, + fallback_installations=None, + token_type_mapping=None, + ) + assert get_adapter_auth_information(repo.owner, repo) == expected + + def test_select_repo_bot_info(self, dbsession): + repo = self._generate_test_repo( + dbsession, with_owner_bot=True, with_bot=True + ) + expected = AdapterAuthInformation( + token=Token( + key="repo_bot_token", + refresh_token="repo_bot_refresh_token", + secret=None, + username=repo.bot.username, + entity_name=owner_key_name(repo.bot.ownerid), + ), + token_owner=repo.bot, + selected_installation_info=None, + fallback_installations=None, + token_type_mapping=None, + ) + assert get_adapter_auth_information(repo.owner, repo) == expected + + def test_select_repo_bot_info_public_repo(self, dbsession, mock_configuration): + repo = self._generate_test_repo( + dbsession, with_owner_bot=True, with_bot=True + ) + mock_configuration.set_params( + { + "github": { + "bot": {"key": "some_key"}, + "bots": { + "read": {"key": "read_bot_key"}, + "status": {"key": "status_bot_key"}, + "comment": {"key": "commenter_bot_key"}, + }, + } + } + ) + repo.private = False + + repo_bot_token = Token( + key="repo_bot_token", + refresh_token="repo_bot_refresh_token", + secret=None, + username=repo.bot.username, + entity_name=owner_key_name(repo.bot.ownerid), + ) + expected = AdapterAuthInformation( + token=repo_bot_token, + token_owner=repo.bot, + selected_installation_info=None, + fallback_installations=None, + token_type_mapping={ + TokenType.comment: Token(key="commenter_bot_key"), + TokenType.read: repo_bot_token, + TokenType.admin: repo_bot_token, + TokenType.status: repo_bot_token, + TokenType.tokenless: repo_bot_token, + TokenType.pull: repo_bot_token, + TokenType.commit: repo_bot_token, + }, + ) + assert get_adapter_auth_information(repo.owner, repo) == expected + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + def test_select_repo_single_installation(self, dbsession): + installations = [ + GithubAppInstallation( + repository_service_ids=None, + installation_id=1200, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=200, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ) + ] + repo = self._generate_test_repo( + dbsession, + with_bot=False, + with_owner_bot=False, + ghapp_installations=installations, + ) + expected = AdapterAuthInformation( + token=Token( + key="installation_token_1200_200", + entity_name="200_1200", + username="installation_1200", + ), + token_owner=None, + selected_installation_info=GithubInstallationInfo( + id=installations[0].id, + installation_id=1200, + app_id=200, + pem_path="pem_path", + ), + fallback_installations=[], + token_type_mapping=None, + ) + assert get_adapter_auth_information(repo.owner, repo) == expected + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + def test_select_repo_deprecated_using_integration(self, dbsession): + repo = self._generate_test_repo( + dbsession, with_bot=False, integration_id=1500, with_owner_bot=False + ) + repo.owner.oauth_token = None + # Repo's owner has no GithubApp, no token, and no bot configured + # The repo has not a bot configured + # The integration_id is no longer verified + # So we fail with exception + expected = AdapterAuthInformation( + token=Token( + key="installation_token_1500_None", + username="installation_1500", + entity_name=gh_app_key_name( + installation_id=repo.owner.integration_id, app_id=None + ), + ), + token_owner=None, + selected_installation_info=GithubInstallationInfo(installation_id=1500), + fallback_installations=[], + token_type_mapping=None, + ) + assert get_adapter_auth_information(repo.owner, repo) == expected + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + def test_select_repo_multiple_installations_default_name(self, dbsession): + installations = [ + GithubAppInstallation( + repository_service_ids=None, + installation_id=1200, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=200, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ), + # This should be ignored in the selection because of the name + GithubAppInstallation( + repository_service_ids=None, + installation_id=1300, + name="my_dedicated_app", + app_id=300, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ), + ] + repo = self._generate_test_repo( + dbsession, + with_bot=False, + with_owner_bot=False, + ghapp_installations=installations, + ) + expected = AdapterAuthInformation( + token=Token( + key="installation_token_1200_200", + entity_name="200_1200", + username="installation_1200", + ), + token_owner=None, + selected_installation_info=GithubInstallationInfo( + id=installations[0].id, + installation_id=1200, + app_id=200, + pem_path="pem_path", + ), + fallback_installations=[], + token_type_mapping=None, + ) + assert get_adapter_auth_information(repo.owner, repo) == expected + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + def test_select_repo_multiple_installations_custom_name(self, dbsession): + installations = [ + GithubAppInstallation( + repository_service_ids=None, + installation_id=1200, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=200, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ), + # This should be selected first + GithubAppInstallation( + repository_service_ids=None, + installation_id=1300, + name="my_dedicated_app", + app_id=300, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ), + ] + repo = self._generate_test_repo( + dbsession, + with_bot=False, + with_owner_bot=False, + ghapp_installations=installations, + ) + expected = AdapterAuthInformation( + token=Token( + key="installation_token_1300_300", + entity_name="300_1300", + username="installation_1300", + ), + token_owner=None, + selected_installation_info=GithubInstallationInfo( + id=installations[1].id, + installation_id=1300, + app_id=300, + pem_path="pem_path", + ), + fallback_installations=[ + GithubInstallationInfo( + id=installations[0].id, + installation_id=1200, + app_id=200, + pem_path="pem_path", + ) + ], + token_type_mapping=None, + ) + assert ( + get_adapter_auth_information( + repo.owner, repo, installation_name_to_use="my_dedicated_app" + ) + == expected + ) + + @pytest.mark.parametrize("service", ["github", "gitlab"]) + def test_select_repo_public_with_no_token_no_admin_token_configured( + self, service, dbsession, mocker + ): + repo = RepositoryFactory(owner__service=service, private=False) + repo.owner.oauth_token = None + dbsession.add(repo) + dbsession.flush() + mock_config_helper( + mocker, + configs={ + f"{service}.bots.tokenless": {"key": "tokenless_bot_token"}, + f"{service}.bots.comment": {"key": "commenter_bot_token"}, + f"{service}.bots.read": {"key": "reader_bot_token"}, + f"{service}.bots.status": {"key": "status_bot_token"}, + }, + ) + expected = AdapterAuthInformation( + token=Token( + key="tokenless_bot_token", + entity_name="tokenless", + ), + token_owner=None, + selected_installation_info=None, + fallback_installations=None, + token_type_mapping={ + TokenType.comment: Token(key="commenter_bot_token"), + TokenType.read: Token( + key="reader_bot_token", + entity_name="read", + ), + TokenType.admin: None, + TokenType.status: Token(key="status_bot_token"), + TokenType.tokenless: Token( + key="tokenless_bot_token", + entity_name="tokenless", + ), + TokenType.pull: None, + TokenType.commit: None, + }, + ) + assert get_adapter_auth_information(repo.owner, repo) == expected diff --git a/apps/worker/services/timeseries.py b/apps/worker/services/timeseries.py new file mode 100644 index 0000000000..f42f8d2cd0 --- /dev/null +++ b/apps/worker/services/timeseries.py @@ -0,0 +1,243 @@ +import dataclasses +import logging +from datetime import datetime +from typing import Any, Iterable, Mapping, Optional + +from shared.reports.resources import Report +from sqlalchemy.dialects.postgresql import insert +from sqlalchemy.orm import Session + +from database.models import Commit, Dataset, Measurement, MeasurementName +from database.models.core import Repository +from database.models.reports import RepositoryFlag +from helpers.timeseries import backfill_max_batch_size +from services.yaml import UserYaml, get_repo_yaml + +log = logging.getLogger(__name__) + + +def maybe_upsert_coverage_measurement(commit, dataset_names, db_session, report): + if MeasurementName.coverage.value in dataset_names: + if report.totals.coverage is not None: + measurements = [ + create_measurement_dict( + MeasurementName.coverage.value, + commit, + measurable_id=f"{commit.repoid}", + value=float(report.totals.coverage), + ) + ] + upsert_measurements(db_session, measurements) + + +def maybe_upsert_flag_measurements(commit, dataset_names, db_session, report): + if MeasurementName.flag_coverage.value in dataset_names: + flag_ids = repository_flag_ids(commit.repository) + measurements = [] + + for flag_name, flag in report.flags.items(): + if flag.totals.coverage is not None: + flag_id = flag_ids.get(flag_name) + if not flag_id: + log.warning( + "Repository flag not found. Created repository flag.", + extra=dict(repoid=commit.repoid, flag_name=flag_name), + ) + repo_flag = RepositoryFlag( + repository_id=commit.repoid, + flag_name=flag_name, + ) + db_session.add(repo_flag) + db_session.flush() + flag_id = repo_flag.id + + measurements.append( + create_measurement_dict( + MeasurementName.flag_coverage.value, + commit, + measurable_id=f"{flag_id}", + value=float(flag.totals.coverage), + ) + ) + + if len(measurements) > 0: + log.info( + "Upserting flag coverage measurements", + extra=dict( + repoid=commit.repoid, + commit_id=commit.id_, + count=len(measurements), + ), + ) + upsert_measurements(db_session, measurements) + + +@dataclasses.dataclass +class ComponentForMeasurement: + component_id: str + flags: list[str] + paths: list[str] + + +def get_relevant_components( + current_yaml: UserYaml, report_flags: list[str] +) -> list[ComponentForMeasurement]: + components = current_yaml.get_components() + if not components: + return [] + + components_for_measurement = {} + for component in components: + if component.paths or component.flag_regexes: + flags = component.get_matching_flags(report_flags) + components_for_measurement[component.component_id] = ( + ComponentForMeasurement(component.component_id, flags, component.paths) + ) + return list(components_for_measurement.values()) + + +def upsert_components_measurements( + commit: Commit, report: Report, components: list[ComponentForMeasurement] +): + measurements = [] + for component in components: + filtered_report = report.filter(flags=component.flags, paths=component.paths) + if filtered_report.totals.coverage is not None: + measurements.append( + create_measurement_dict( + MeasurementName.component_coverage.value, + commit, + measurable_id=component.component_id, + value=float(filtered_report.totals.coverage), + ) + ) + + if len(measurements) > 0: + db_session = commit.get_db_session() + upsert_measurements(db_session, measurements) + log.info( + "Upserted component coverage measurements", + extra=dict( + repoid=commit.repoid, commit_id=commit.id_, count=len(measurements) + ), + ) + + +def create_measurement_dict( + name: str, commit: Commit, measurable_id: str, value: float +) -> dict[str, Any]: + return dict( + name=name, + owner_id=commit.repository.ownerid, + repo_id=commit.repoid, + measurable_id=measurable_id, + branch=commit.branch, + commit_sha=commit.commitid, + timestamp=commit.timestamp, + value=value, + ) + + +def upsert_measurements( + db_session: Session, measurements: list[dict[str, Any]] +) -> None: + command = insert(Measurement.__table__).values(measurements) + command = command.on_conflict_do_update( + index_elements=[ + Measurement.name, + Measurement.owner_id, + Measurement.repo_id, + Measurement.measurable_id, + Measurement.commit_sha, + Measurement.timestamp, + ], + set_=dict( + branch=command.excluded.branch, + value=command.excluded.value, + ), + ) + db_session.execute(command) + db_session.flush() + + +def repository_commits_query( + repository: Repository, + start_date: datetime, + end_date: datetime, +) -> Iterable[Commit]: + db_session = repository.get_db_session() + + commits = ( + db_session.query(Commit.id_) + .filter( + Commit.repoid == repository.repoid, + Commit.timestamp >= start_date, + Commit.timestamp <= end_date, + ) + .order_by(Commit.timestamp.desc()) + .yield_per(100) + ) + + return commits + + +def repository_datasets_query( + repository: Repository, backfilled: Optional[bool] = None +) -> Iterable[Dataset]: + db_session = repository.get_db_session() + + datasets = db_session.query(Dataset.name).filter_by(repository_id=repository.repoid) + if backfilled is not None: + datasets = datasets.filter_by(backfilled=backfilled) + + return datasets + + +def repository_flag_ids(repository: Repository) -> Mapping[str, int]: + db_session = repository.get_db_session() + repo_flags = ( + db_session.query(RepositoryFlag).filter_by(repository=repository).yield_per(100) + ) + + return {repo_flag.flag_name: repo_flag.id for repo_flag in repo_flags} + + +def backfill_batch_size(repository: Repository, dataset: Dataset) -> int: + db_session = repository.get_db_session() + batch_size = backfill_max_batch_size() + + if dataset.name == MeasurementName.component_coverage.value: + current_yaml = get_repo_yaml(repository) + component_count = max(len(current_yaml.get_components()), 1) + batch_size = int(batch_size / component_count) + elif dataset.name == MeasurementName.flag_coverage.value: + flag_count = ( + db_session.query(RepositoryFlag) + .filter_by(repository_id=repository.repoid) + .count() + ) + flag_count = max(flag_count, 1) + batch_size = int(batch_size / flag_count) + + return max(batch_size, 1) + + +def delete_repository_data(repository: Repository): + db_session = repository.get_db_session() + db_session.query(Dataset).filter_by(repository_id=repository.repoid).delete() + db_session.query(Measurement).filter_by( + owner_id=repository.ownerid, + repo_id=repository.repoid, + ).delete() + + +def delete_repository_measurements( + repository: Repository, measurement_type: str, measurement_id: str +): + db_session = repository.get_db_session() + db_session.query(Measurement).filter_by( + owner_id=repository.ownerid, + repo_id=repository.repoid, + name=measurement_type, + measurable_id=measurement_id, + ).delete() diff --git a/apps/worker/services/urls.py b/apps/worker/services/urls.py new file mode 100644 index 0000000000..d7655e8fed --- /dev/null +++ b/apps/worker/services/urls.py @@ -0,0 +1,271 @@ +import logging +import re +from dataclasses import dataclass +from enum import Enum +from typing import List +from urllib.parse import parse_qs, quote_plus, urlencode, urlparse, urlunparse + +from shared.config import get_config +from shared.django_apps.codecov_auth.models import Service + +from database.models import Commit, Pull, Repository +from services.license import requires_license + +services_short_dict = dict( + github="gh", + github_enterprise="ghe", + bitbucket="bb", + bitbucket_server="bbs", + gitlab="gl", + gitlab_enterprise="gle", +) + +log = logging.getLogger(__name__) + + +class SiteUrls(Enum): + commit_url = ( + "{base_url}/{service_short}/{username}/{project_name}/commit/{commit_sha}" + ) + compare_url = "{base_url}/{service_short}/{username}/{project_name}/compare/{base_sha}...{head_sha}" + repository_url = "{base_url}/{service_short}/{username}/{project_name}" + graph_url = "{base_url}/{service_short}/{username}/{project_name}/commit/{commit_sha}/graphs/{graph_filename}" + pull_url = "{base_url}/{service_short}/{username}/{project_name}/pull/{pull_id}" + new_client_pull_url = "https://app.codecov.io/{service_short}/{username}/{project_name}/compare/{pull_id}" + pull_graph_url = "{base_url}/{service_short}/{username}/{project_name}/pull/{pull_id}/graphs/{graph_filename}" + org_acccount_url = "{dashboard_base_url}/account/{service_short}/{username}" + members_url = "{dashboard_base_url}/members/{service_short}/{username}" + members_url_self_hosted = "{dashboard_base_url}/account/{service_short}/{username}" + plan_url = "{dashboard_base_url}/plan/{service_short}/{username}" + test_analytics_url = "{dashboard_base_url}/{service_short}/{username}/{project_name}/tests/{branch_name}" + + def get_url(self, **kwargs) -> str: + return self.value.format(**kwargs) + + +def get_base_url() -> str: + return get_config("setup", "codecov_url") + + +def get_dashboard_base_url() -> str: + configured_dashboard_url = get_config("setup", "codecov_dashboard_url") + configured_base_url = get_base_url() + # Enterprise users usually configure the base url not the dashboard one, + # app.codecov.io is for cloud users so we want to prioritize the values correctly + if requires_license(): + return configured_dashboard_url or configured_base_url + else: + return configured_dashboard_url or "https://app.codecov.io" + + +def _get_username_for_url(repository: Repository) -> str: + username = repository.owner.username + if repository.owner.service == Service.GITLAB.value: + # if GL, direct url to root org not subgroup + root_org = repository.owner.root_organization + if root_org is not None: + username = root_org.username + return username + + +def get_commit_url(commit: Commit) -> str: + return SiteUrls.commit_url.get_url( + base_url=get_dashboard_base_url(), + service_short=services_short_dict.get(commit.repository.service), + username=commit.repository.owner.username, + project_name=commit.repository.name, + commit_sha=commit.commitid, + ) + + +def get_commit_url_from_commit_sha(repository, commit_sha) -> str: + return SiteUrls.commit_url.get_url( + base_url=get_dashboard_base_url(), + service_short=services_short_dict.get(repository.service), + username=repository.owner.username, + project_name=repository.name, + commit_sha=commit_sha, + ) + + +def get_graph_url(commit: Commit, graph_filename: str, **kwargs) -> str: + url = SiteUrls.graph_url.get_url( + base_url=get_dashboard_base_url(), + service_short=services_short_dict.get(commit.repository.service), + username=commit.repository.owner.username, + project_name=commit.repository.name, + commit_sha=commit.commitid, + graph_filename=graph_filename, + ) + encoded_kwargs = urlencode(kwargs) + return f"{url}?{encoded_kwargs}" + + +def get_compare_url(base_commit: Commit, head_commit: Commit) -> str: + log.warning( + "Compare links are deprecated.", extra=dict(head_commit=head_commit.commitid) + ) + return SiteUrls.compare_url.get_url( + base_url=get_base_url(), + service_short=services_short_dict.get(head_commit.repository.service), + username=head_commit.repository.owner.username, + project_name=head_commit.repository.name, + base_sha=base_commit.commitid, + head_sha=head_commit.commitid, + ) + + +def get_repository_url(repository: Repository) -> str: + return SiteUrls.repository_url.get_url( + base_url=get_dashboard_base_url(), + service_short=services_short_dict.get(repository.service), + username=repository.owner.username, + project_name=repository.name, + ) + + +def get_pull_url(pull: Pull) -> str: + repository = pull.repository + return SiteUrls.pull_url.get_url( + base_url=get_dashboard_base_url(), + service_short=services_short_dict.get(repository.service), + username=repository.owner.username, + project_name=repository.name, + pull_id=pull.pullid, + ) + + +def get_bundle_analysis_pull_url(pull: Pull) -> str: + repository = pull.repository + pull_url = SiteUrls.pull_url.get_url( + base_url=get_dashboard_base_url(), + service_short=services_short_dict.get(repository.service), + username=repository.owner.username, + project_name=repository.name, + pull_id=pull.pullid, + ) + params = [QueryParams(name="dropdown", value="bundle")] + return append_query_params_to_url(url=pull_url, params=params) + + +def get_pull_graph_url(pull: Pull, graph_filename: str, **kwargs) -> str: + repository = pull.repository + url = SiteUrls.pull_graph_url.get_url( + base_url=get_dashboard_base_url(), + service_short=services_short_dict.get(repository.service), + username=repository.owner.username, + project_name=repository.name, + pull_id=pull.pullid, + graph_filename=graph_filename, + ) + encoded_kwargs = urlencode(kwargs) + return f"{url}?{encoded_kwargs}" + + +def get_org_account_url(pull: Pull) -> str: + repository = pull.repository + return SiteUrls.org_acccount_url.get_url( + dashboard_base_url=get_dashboard_base_url(), + service_short=services_short_dict.get(repository.service), + username=repository.owner.username, + ) + + +def get_members_url(pull: Pull) -> str: + repository = pull.repository + username = _get_username_for_url(repository=repository) + if not requires_license(): + return SiteUrls.members_url.get_url( + dashboard_base_url=get_dashboard_base_url(), + service_short=services_short_dict.get(repository.service), + username=username, + ) + else: + return SiteUrls.members_url_self_hosted.get_url( + dashboard_base_url=get_dashboard_base_url(), + service_short=services_short_dict.get(repository.service), + username=pull.author.username, + ) + + +def get_plan_url(pull: Pull) -> str: + repository = pull.repository + username = _get_username_for_url(repository=repository) + return SiteUrls.plan_url.get_url( + dashboard_base_url=get_dashboard_base_url(), + service_short=services_short_dict.get(repository.service), + username=username, + ) + + +def get_test_analytics_url(repo: Repository, commit: Commit) -> str: + if commit.branch is not None: + branch_name = quote_plus(commit.branch) + else: + branch_name = quote_plus(repo.branch) + return SiteUrls.test_analytics_url.get_url( + dashboard_base_url=get_dashboard_base_url(), + service_short=services_short_dict.get(repo.service), + username=repo.owner.username, + project_name=repo.name, + branch_name=branch_name, + ) + + +@dataclass +class QueryParams: + name: str + value: str + + +def append_query_params_to_url(url: str, params: List[QueryParams]) -> str: + parsed_url = urlparse(url) + query_dict = parse_qs(parsed_url.query) + # Add tracking parameters + for param in params: + query_dict[param.name] = param.value + parsed_url = parsed_url._replace(query=urlencode(query_dict, doseq=True)) + return urlunparse(parsed_url) + + +def append_tracking_params_to_urls( + input_string: str, service: str, notification_type: str, org_name: str +) -> str: + """ + Append tracking parameters to markdown links pointing to a codecov urls in a given string, using regex to + detect and modify the urls. + + Args: + input_string (str): a string that may contain a markdown link to a Codecov url, for example: PR comments, Checks annotation. + + Returns: + string: the string with tracking parameters appended to the Codecov url. + + Example: + input: "This string has a [link](codecov.io/pulls) to a codecov url that will be changed (but we won't change this reference to codecov.io) since it's not a Markdown link." + output: "This string has a [link](codecov.io/pulls?) to a codecov url that will be changed (but we won't change this reference to codecov.io) since it's not a Markdown link." + + """ + # regex matches against the pattern: ](codecov.io) + # group 1 is "](", group 2 is the url, group 3 is ")" + cond = re.compile(r"(\]\()(\S*?codecov\.io[\S]*?)(\))") + + # Function used during regex substitution to append params to the url + def add_params(match): + # Extract url from regex and parse the query string + url = match.group(2) + parsed_url = urlparse(url) + query_dict = parse_qs(parsed_url.query) + # Add tracking parameters + query_dict["utm_medium"] = "referral" + query_dict["utm_source"] = service + query_dict["utm_content"] = notification_type + query_dict["utm_campaign"] = "pr comments" + query_dict["utm_term"] = org_name + # Reconstruct the url with the new query string + parsed_url = parsed_url._replace(query=urlencode(query_dict, doseq=True)) + url_with_tracking_params = urlunparse(parsed_url) + + return "](" + url_with_tracking_params + ")" + + return cond.sub(add_params, input_string) diff --git a/apps/worker/services/yaml/__init__.py b/apps/worker/services/yaml/__init__.py new file mode 100644 index 0000000000..1da8fcae98 --- /dev/null +++ b/apps/worker/services/yaml/__init__.py @@ -0,0 +1,152 @@ +import logging +from typing import Any, Mapping + +from shared.django_apps.utils.model_utils import get_ownerid_if_member +from shared.torngit.exceptions import TorngitClientError, TorngitError +from shared.validation.exceptions import InvalidYamlException +from shared.yaml import UserYaml +from shared.yaml.user_yaml import OwnerContext + +from database.enums import CommitErrorTypes +from database.models import Commit +from database.models.core import Repository +from helpers.save_commit_error import save_commit_error +from services.yaml.fetcher import fetch_commit_yaml_from_provider +from services.yaml.reader import read_yaml_field + +log = logging.getLogger(__name__) + + +def get_repo_yaml(repository: Repository): + context = OwnerContext( + owner_onboarding_date=repository.owner.createstamp, + owner_plan=repository.owner.plan, + ownerid=repository.ownerid, + ) + return UserYaml.get_final_yaml( + owner_yaml=repository.owner.yaml, + repo_yaml=repository.yaml, + owner_context=context, + ) + + +async def get_current_yaml(commit: Commit, repository_service) -> UserYaml: + """ + Fetches what the current yaml is supposed to be + + + This function wraps the whole logic of fetching the current yaml for a given commit + - It makes best effort in trying to fetch and parse the data from the repo + - It merges it with the owner YAML and with the default system YAML as needed + - It handles possible exceptions that come from fetching data from the repository + + Args: + commit (Commit): The commit we want to get the provider from + repository_service : The service (as fetched from get_repo_provider_service) that we can use + to fetch the YAML data. If None, we just pretend the YAML data isn't fetchable + + Returns: + dict: The yaml, parsed, processed and ready to use as the final yaml + """ + commit_yaml = None + repository = commit.repository + try: + commit_yaml = await fetch_commit_yaml_from_provider(commit, repository_service) + except InvalidYamlException as ex: + save_commit_error( + commit, + error_code=CommitErrorTypes.INVALID_YAML.value, + error_params=dict( + repoid=repository.repoid, + commit_yaml=commit_yaml, + error_location=ex.error_location, + ), + ) + + log.warning( + "Unable to use yaml from commit because it is invalid", + extra=dict( + repoid=repository.repoid, + commit=commit.commitid, + error_location=ex.error_location, + ), + exc_info=True, + ) + except TorngitClientError: + save_commit_error( + commit, + error_code=CommitErrorTypes.YAML_CLIENT_ERROR.value, + error_params=dict( + repoid=repository.repoid, + commit_yaml=commit_yaml, + ), + ) + + log.warning( + "Unable to use yaml from commit because it cannot be fetched due to client issues", + extra=dict(repoid=repository.repoid, commit=commit.commitid), + exc_info=True, + ) + except TorngitError: + save_commit_error( + commit, + error_code=CommitErrorTypes.YAML_UNKNOWN_ERROR.value, + error_params=dict( + repoid=repository.repoid, + commit_yaml=commit_yaml, + ), + ) + + log.warning( + "Unable to use yaml from commit because it cannot be fetched due to unknown issues", + extra=dict(repoid=repository.repoid, commit=commit.commitid), + exc_info=True, + ) + context = OwnerContext( + owner_onboarding_date=repository.owner.createstamp, + owner_plan=repository.owner.plan, + ownerid=repository.ownerid, + ) + return UserYaml.get_final_yaml( + owner_yaml=repository.owner.yaml, + repo_yaml=repository.yaml, + commit_yaml=commit_yaml, + owner_context=context, + ) + + +def save_repo_yaml_to_database_if_needed( + current_commit: Commit, new_yaml: UserYaml | Mapping[str, Any] +) -> bool: + repository = current_commit.repository + existing_yaml = get_repo_yaml(repository) + syb = read_yaml_field(existing_yaml, ("codecov", "strict_yaml_branch")) + branches_considered_for_yaml = ( + syb, + current_commit.repository.branch, + read_yaml_field(existing_yaml, ("codecov", "branch")), + ) + if current_commit.branch and current_commit.branch in branches_considered_for_yaml: + if not syb or syb == current_commit.branch: + yaml_branch = read_yaml_field(new_yaml, ("codecov", "branch")) + if yaml_branch: + repository.branch = yaml_branch + + maybe_update_repo_bot(new_yaml, repository) + repository.yaml = new_yaml + return True + + return False + + +def maybe_update_repo_bot( + new_yaml: UserYaml | Mapping[str, Any], + repository: Repository, +) -> None: + new_bot_owner_username = read_yaml_field(new_yaml, ("codecov", "bot")) + if new_bot_owner_username: + bot_owner_id = get_ownerid_if_member( + repository.owner.service, new_bot_owner_username, repository.ownerid + ) + if bot_owner_id and bot_owner_id != repository.bot_id: + repository.bot_id = bot_owner_id diff --git a/apps/worker/services/yaml/fetcher.py b/apps/worker/services/yaml/fetcher.py new file mode 100644 index 0000000000..ae43dacb9a --- /dev/null +++ b/apps/worker/services/yaml/fetcher.py @@ -0,0 +1,30 @@ +import logging + +import shared.torngit as torngit +from shared.helpers.cache import cache +from shared.yaml import ( + fetch_current_yaml_from_provider_via_reference as shared_fetch_current_yaml_from_provider_via_reference, +) + +from database.models import Commit +from services.yaml.parser import parse_yaml_file + +log = logging.getLogger(__name__) + + +@cache.cache_function() +async def fetch_commit_yaml_from_provider( + commit: Commit, repository_service: torngit.base.TorngitBaseAdapter +) -> dict: + yaml_content = await shared_fetch_current_yaml_from_provider_via_reference( + commit.commitid, repository_service + ) + if yaml_content: + return parse_yaml_file( + yaml_content, + show_secrets_for=( + commit.repository.service, + commit.repository.owner.service_id, + commit.repository.service_id, + ), + ) diff --git a/apps/worker/services/yaml/parser.py b/apps/worker/services/yaml/parser.py new file mode 100644 index 0000000000..3a9603c3c1 --- /dev/null +++ b/apps/worker/services/yaml/parser.py @@ -0,0 +1,16 @@ +from typing import Dict, Optional + +from shared.validation.exceptions import InvalidYamlException +from shared.yaml.validation import validate_yaml +from yaml import safe_load +from yaml.error import YAMLError + + +def parse_yaml_file(content: str, show_secrets_for) -> Optional[Dict]: + try: + yaml_dict = safe_load(content) + except YAMLError as e: + raise InvalidYamlException("invalid_yaml", e) + if yaml_dict is None: + return None + return validate_yaml(yaml_dict, show_secrets_for=show_secrets_for) diff --git a/apps/worker/services/yaml/reader.py b/apps/worker/services/yaml/reader.py new file mode 100644 index 0000000000..f11ec032df --- /dev/null +++ b/apps/worker/services/yaml/reader.py @@ -0,0 +1,74 @@ +import logging +from decimal import Decimal +from typing import Any, List, Mapping + +from shared.yaml.user_yaml import UserYaml + +from helpers.components import Component +from helpers.number import precise_round + +log = logging.getLogger(__name__) + + +""" + Carries tools to help reading of a already-processed user yaml +""" + + +def read_yaml_field(yaml_dict: UserYaml | Mapping[str, Any], keys, _else=None) -> Any: + log.debug("Field %s requested", keys) + try: + for key in keys: + if hasattr(yaml_dict, "__getitem__"): + yaml_dict = yaml_dict[key] + else: + yaml_dict = getattr(yaml_dict, key) + return yaml_dict + except (TypeError, AttributeError, KeyError): + return _else + + +def get_minimum_precision(yaml_dict: Mapping[str, Any]) -> Decimal: + precision = read_yaml_field(yaml_dict, ("coverage", "precision"), 2) + return Decimal("0.1") ** precision + + +def round_number(yaml_dict: UserYaml, number: Decimal) -> Decimal: + rounding = read_yaml_field(yaml_dict, ("coverage", "round"), "nearest") + precision = read_yaml_field(yaml_dict, ("coverage", "precision"), 2) + return precise_round(number, precision=precision, rounding=rounding) + + +def get_paths_from_flags(yaml_dict: UserYaml, flags): + if flags: + res = [] + for flag in flags: + flag_configuration = yaml_dict.get_flag_configuration(flag) + if flag_configuration is not None: + paths_from_flag = flag_configuration.get("paths") + if paths_from_flag is None: + # flag is implicitly associated with all paths, so no filter here + return [] + res.extend(paths_from_flag) + return list(set(res)) + else: + return [] + + +def get_components_from_yaml(yaml: UserYaml) -> List[Component]: + component_definitions = read_yaml_field(yaml, ("component_management",)) + if not component_definitions: + return [] + # Default set of rules that is overriden by individual components. + # The individual components inherit the values from default_definition if they don't have a particular key defined in the default rules + default_definition = component_definitions.get("default_rules", {}) + + individual_components = list( + map( + lambda component_dict: Component.from_dict( + {**default_definition, **component_dict} + ), + component_definitions.get("individual_components", []), + ) + ) + return individual_components diff --git a/apps/worker/services/yaml/tests/samples/big.yaml b/apps/worker/services/yaml/tests/samples/big.yaml new file mode 100644 index 0000000000..1791764c41 --- /dev/null +++ b/apps/worker/services/yaml/tests/samples/big.yaml @@ -0,0 +1,145 @@ +codecov: + token: uuid # Your private repository token + url: "http://codecov.io" # for Codecov Enterprise customers + slug: "owner/repo" # for Codecov Enterprise customers + branch: master # override the default branch + bot: username # set user whom will be the consumer of oauth requests + ci: # Custom CI domains if Codecov does not identify them automatically + - ci.domain.com + - "!provider" # ignore these providers when checking if CI passed + # ex. You may test on Travis, Circle, and AppVeyor, but only need + # to check if Travis passes. Therefore add: !circle and !appveyor + notify: # ADVANCED USE ONLY + after_n_builds: 2 # number of expected builds to recieve before sending notifications + # after: check ci status unless disabled via require_ci_to_pass + require_ci_to_pass: yes # yes: will delay sending notifications until all ci is finished + # no: will send notifications without checking ci status and wait till "after_n_builds" are uploaded + countdown: 12 # number of seconds to wait before first ci build check + delay: 4 # number of seconds to wait between ci build checks + +coverage: + precision: 2 # 2 = xx.xx%, 0 = xx% + round: down # default down + range: 50...60 # default 70...90. red...green + + notify: + irc: + default: + server: "chat.freenode.net" + branches: null # all branches by default + threshold: 1% + message: "Coverage {{changed}} for {{owner}}/{{repo}}" # customize the message + flags: null + paths: null + + slack: + default: + url: "secret:c/nCgqn5v1HY5VFIs9i4W3UY6eleB2rTBdBKK/ilhPR7Ch4N0FE1aO6SRfAxp3Zlm4tLNusaPY7ettH6dTYj/YhiRohxiNqJMJ4L9YQmESo=" + threshold: 1% + branches: null # all branches by default + message: "Coverage {{changed}} for {{owner}}/{{repo}}" # customize the message + attachments: "sunburst, diff" + only_pulls: false + flags: null + paths: null + + email: + default: + to: + - example@domain.com + - secondexample@seconddomain.com + threshold: 1% + only_pulls: false + layout: reach, diff, flags + flags: null + paths: null + + hipchat: + default: + url: "secret:c/nCgqn5v1HY5VFIs9i4W3UY6eleB2rTBdBKK/ilhPR7Ch4N0FE1aO6SRfAxp3Zlm4tLNusaPY7ettH6dTYj/YhiRohxiNqJMJ4L9YQmESo=" + threshold: 1% + branches: null # all branches by default + notify: false # if the hipchat message is silent or loud (default false) + message: "Coverage {{changed}} for {{owner}}/{{repo}}" # customize the message + flags: null + paths: null + + gitter: + default: + url: "secret:c/nCgqn5v1HY5VFIs9i4W3UY6eleB2rTBdBKK/ilhPR7Ch4N0FE1aO6SRfAxp3Zlm4tLNusaPY7ettH6dTYj/YhiRohxiNqJMJ4L9YQmESo=" + threshold: 1% + branches: null # all branches by default + message: "Coverage {{changed}} for {{owner}}/{{repo}}" # customize the message + + webhook: + _name_: + url: "secret:c/nCgqn5v1HY5VFIs9i4W3UY6eleB2rTBdBKK/ilhPR7Ch4N0FE1aO6SRfAxp3Zlm4tLNusaPY7ettH6dTYj/YhiRohxiNqJMJ4L9YQmESo=" + threshold: 1% + branches: null # all branches by default + + status: + project: + default: + base: parent + target: auto + threshold: 1% + branches: + - master + if_no_uploads: error + if_not_found: success + if_ci_failed: error + only_pulls: false + flags: + - integration + paths: + - folder + + patch: + default: + base: parent + target: 80% + branches: null + if_no_uploads: success + if_not_found: success + if_ci_failed: error + only_pulls: false + flags: + - integration + paths: + - folder + + changes: + default: + base: parent + branches: null + if_no_uploads: error + if_not_found: success + if_ci_failed: error + only_pulls: false + flags: + - integration + paths: + - folder + + flags: + integration: + assume: + branches: + - master + ignore: + - app/ui + + ignore: # files and folders for processing + - tests/* + + fixes: + - "old_path::new_path" + +comment: + layout: diff, flags, reach + branches: + - "*" + behavior: default # defualt = posts once then update, posts new if delete + # once = post once then updates + # new = delete old, post new + # spammy = post new diff --git a/apps/worker/services/yaml/tests/samples/sample_yaml_1.yaml b/apps/worker/services/yaml/tests/samples/sample_yaml_1.yaml new file mode 100644 index 0000000000..e7bb8d2898 --- /dev/null +++ b/apps/worker/services/yaml/tests/samples/sample_yaml_1.yaml @@ -0,0 +1,26 @@ +codecov: + notify: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: yes + patch: yes + changes: no + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "header, diff" + behavior: default + require_changes: no \ No newline at end of file diff --git a/apps/worker/services/yaml/tests/test_yaml.py b/apps/worker/services/yaml/tests/test_yaml.py new file mode 100644 index 0000000000..8e5ad59d78 --- /dev/null +++ b/apps/worker/services/yaml/tests/test_yaml.py @@ -0,0 +1,404 @@ +import os +from datetime import datetime, timezone + +import mock +import pytest +from shared.torngit.exceptions import TorngitClientError, TorngitServerUnreachableError +from shared.validation.types import CoverageCommentRequiredChanges +from shared.yaml import UserYaml + +from database.tests.factories import CommitFactory +from services.yaml import get_current_yaml +from test_utils.base import BaseTestCase + + +class TestYamlService(BaseTestCase): + def test_get_final_yaml_no_yaml_no_config_yaml(self, mock_configuration): + expected_result = {} + result = UserYaml.get_final_yaml( + owner_yaml=None, repo_yaml=None, commit_yaml=None + ) + assert expected_result == result.to_dict() + + def test_get_final_yaml_empty_yaml_no_config_yaml(self, mock_configuration): + expected_result = {} + result = UserYaml.get_final_yaml(owner_yaml={}, repo_yaml={}, commit_yaml={}) + assert expected_result == result.to_dict() + + def test_get_final_yaml_no_yaml(self, mock_configuration): + mock_configuration.set_params( + { + "site": { + "coverage": {"precision": 2}, + "parsers": {"javascript": {"enable_partials": True}}, + } + } + ) + expected_result = { + "coverage": {"precision": 2}, + "parsers": {"javascript": {"enable_partials": True}}, + } + result = UserYaml.get_final_yaml(owner_yaml={}, repo_yaml={}, commit_yaml={}) + assert expected_result == result.to_dict() + + def test_get_final_yaml_no_thing_set_at_all(self, mocker, mock_configuration): + mock_configuration._params = None + mocker.patch.dict(os.environ, {}, clear=True) + mocker.patch.object( + mock_configuration, "load_yaml_file", side_effect=FileNotFoundError() + ) + expected_result = { + "codecov": {"require_ci_to_pass": True, "notify": {"wait_for_ci": True}}, + "coverage": { + "precision": 2, + "round": "down", + "range": [60.0, 80.0], + "status": { + "project": True, + "patch": True, + "changes": False, + "default_rules": {"flag_coverage_not_uploaded_behavior": "include"}, + }, + }, + "comment": { + "layout": "reach,diff,flags,tree,reach", + "behavior": "default", + "show_carryforward_flags": False, + }, + "github_checks": {"annotations": True}, + "slack_app": True, + } + result = UserYaml.get_final_yaml(owner_yaml={}, repo_yaml={}, commit_yaml={}) + assert expected_result == result.to_dict() + + def test_get_final_yaml_owner_yaml(self, mock_configuration): + mock_configuration.set_params( + { + "site": { + "coverage": {"precision": 2}, + "parsers": {"javascript": {"enable_partials": True}}, + } + } + ) + expected_result = { + "coverage": {"precision": 2}, + "parsers": { + "javascript": {"enable_partials": True}, + "new_language": "damn right", + }, + } + result = UserYaml.get_final_yaml( + owner_yaml={"parsers": {"new_language": "damn right"}}, + repo_yaml={}, + commit_yaml={}, + ) + assert expected_result == result.to_dict() + + def test_get_final_yaml_both_repo_and_commit_yaml(self, mock_configuration): + mock_configuration.set_params( + { + "site": { + "coverage": {"precision": 2}, + "parsers": {"javascript": {"enable_partials": True}}, + } + } + ) + expected_result = { + "coverage": {"precision": 2}, + "parsers": { + "javascript": {"enable_partials": True}, + "different_language": "say what", + }, + } + result = UserYaml.get_final_yaml( + owner_yaml=None, + repo_yaml={"parsers": {"new_language": "damn right"}}, + commit_yaml={"parsers": {"different_language": "say what"}}, + ) + assert expected_result == result.to_dict() + + @pytest.mark.asyncio + async def test_get_current_yaml(self, mocker, mock_configuration): + mock_configuration.set_params( + { + "site": { + "comment": { + "behavior": "default", + "layout": "header, diff", + "require_changes": [ + CoverageCommentRequiredChanges.no_requirements.value + ], + } + } + } + ) + mocked_list_files_result = [ + {"name": ".gitignore", "path": ".gitignore", "type": "file"}, + {"name": ".travis.yml", "path": ".travis.yml", "type": "file"}, + {"name": "README.rst", "path": "README.rst", "type": "file"}, + {"name": "awesome", "path": "awesome", "type": "folder"}, + {"name": "codecov", "path": "codecov", "type": "file"}, + {"name": "codecov.yaml", "path": "codecov.yaml", "type": "file"}, + {"name": "tests", "path": "tests", "type": "folder"}, + ] + list_files_future = mocked_list_files_result + sample_yaml = "\n".join( + ["codecov:", " notify:", " require_ci_to_pass: yes"] + ) + contents_result = {"content": sample_yaml} + contents_result_future = contents_result + valid_handler = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=list_files_future), + get_source=mock.AsyncMock(return_value=contents_result_future), + ) + commit = CommitFactory.create( + repository__yaml={ + "coverage": { + "precision": 2, + "round": "down", + "range": [70.0, 100.0], + "status": {"project": True, "patch": True, "changes": False}, + } + } + ) + res = await get_current_yaml(commit, valid_handler) + assert res.to_dict() == { + "codecov": {"notify": {}, "require_ci_to_pass": True}, + "comment": { + "behavior": "default", + "layout": "header, diff", + "require_changes": [ + CoverageCommentRequiredChanges.no_requirements.value + ], + }, + } + + @pytest.mark.asyncio + async def test_get_current_yaml_with_owner_yaml(self, mocker, mock_configuration): + mock_configuration.set_params( + { + "site": { + "comment": { + "behavior": "default", + "layout": "header, diff", + "require_changes": [ + CoverageCommentRequiredChanges.no_requirements.value + ], + } + } + } + ) + mocked_list_files_result = [ + {"name": ".gitignore", "path": ".gitignore", "type": "file"}, + {"name": ".travis.yml", "path": ".travis.yml", "type": "file"}, + {"name": "README.rst", "path": "README.rst", "type": "file"}, + {"name": "awesome", "path": "awesome", "type": "folder"}, + {"name": "codecov", "path": "codecov", "type": "file"}, + {"name": "codecov.yaml", "path": "codecov.yaml", "type": "file"}, + {"name": "tests", "path": "tests", "type": "folder"}, + ] + list_files_future = mocked_list_files_result + sample_yaml = "\n".join( + ["codecov:", " notify:", " require_ci_to_pass: yes"] + ) + contents_result = {"content": sample_yaml} + contents_result_future = contents_result + valid_handler = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=list_files_future), + get_source=mock.AsyncMock(return_value=contents_result_future), + ) + commit = CommitFactory.create( + repository__yaml={ + "coverage": { + "precision": 2, + "round": "down", + "range": [70.0, 100.0], + "status": {"project": True, "patch": True, "changes": False}, + } + }, + repository__owner__yaml={"codecov": {"bot": "ThiagoCodecov"}}, + ) + res = await get_current_yaml(commit, valid_handler) + assert res.to_dict() == { + "codecov": { + "bot": "ThiagoCodecov", + "notify": {}, + "require_ci_to_pass": True, + }, + "comment": { + "behavior": "default", + "layout": "header, diff", + "require_changes": [ + CoverageCommentRequiredChanges.no_requirements.value + ], + }, + } + + @pytest.mark.asyncio + async def test_get_current_yaml_invalid_yaml( + self, mocker, dbsession, mock_configuration + ): + mock_configuration.set_params( + { + "site": { + "comment": { + "behavior": "default", + "layout": "header, diff", + "require_changes": [ + CoverageCommentRequiredChanges.no_requirements.value + ], + } + } + } + ) + mocked_list_files_result = [ + {"name": "codecov.yaml", "path": "codecov.yaml", "type": "file"} + ] + list_files_future = mocked_list_files_result + sample_yaml = "\n".join( + ["@codecov:", " notify:", " require_ci_to_pass: yes"] + ) + contents_result = {"content": sample_yaml} + contents_result_future = contents_result + valid_handler = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=list_files_future), + get_source=mock.AsyncMock(return_value=contents_result_future), + ) + commit = CommitFactory.create( + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + repository__owner__createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + repository__yaml={ + "coverage": { + "precision": 2, + "round": "down", + "range": [70.0, 100.0], + "status": {"project": True, "patch": True, "changes": False}, + } + }, + ) + + dbsession.add(commit) + + res = await get_current_yaml(commit, valid_handler) + assert res.to_dict() == { + "coverage": { + "precision": 2, + "round": "down", + "range": [70.0, 100.0], + "status": {"project": True, "patch": True, "changes": False}, + }, + "comment": { + "behavior": "default", + "layout": "header, diff", + "require_changes": [ + CoverageCommentRequiredChanges.no_requirements.value + ], + }, + } + + assert commit.errors[0].error_code == "invalid_yaml" + assert len(commit.errors) == 1 + + @pytest.mark.asyncio + async def test_get_current_yaml_no_permissions( + self, mocker, mock_configuration, dbsession + ): + mock_configuration.set_params( + { + "site": { + "comment": { + "behavior": "default", + "layout": "header, diff", + "require_changes": [ + CoverageCommentRequiredChanges.no_requirements.value + ], + } + } + } + ) + valid_handler = mocker.MagicMock( + list_top_level_files=mocker.MagicMock( + side_effect=TorngitClientError(404, "response", "message") + ) + ) + commit = CommitFactory.create( + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + repository__owner__createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + repository__yaml={ + "coverage": { + "precision": 2, + "round": "down", + "range": [70.0, 100.0], + "status": {"project": True, "patch": True, "changes": False}, + } + }, + ) + dbsession.add(commit) + res = await get_current_yaml(commit, valid_handler) + assert res.to_dict() == { + "coverage": { + "precision": 2, + "round": "down", + "range": [70.0, 100.0], + "status": {"project": True, "patch": True, "changes": False}, + }, + "comment": { + "behavior": "default", + "layout": "header, diff", + "require_changes": [ + CoverageCommentRequiredChanges.no_requirements.value + ], + }, + } + + @pytest.mark.asyncio + async def test_get_current_yaml_unreachable_provider( + self, mocker, mock_configuration, dbsession + ): + mock_configuration.set_params( + { + "site": { + "comment": { + "behavior": "default", + "layout": "header, diff", + "require_changes": [ + CoverageCommentRequiredChanges.no_requirements.value + ], + } + } + } + ) + valid_handler = mocker.MagicMock( + list_top_level_files=mocker.MagicMock( + side_effect=TorngitServerUnreachableError() + ) + ) + commit = CommitFactory.create( + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + repository__owner__createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + repository__yaml={ + "coverage": { + "precision": 2, + "round": "down", + "range": [70.0, 100.0], + "status": {"project": True, "patch": True, "changes": False}, + } + }, + ) + dbsession.add(commit) + res = await get_current_yaml(commit, valid_handler) + assert res.to_dict() == { + "coverage": { + "precision": 2, + "round": "down", + "range": [70.0, 100.0], + "status": {"project": True, "patch": True, "changes": False}, + }, + "comment": { + "behavior": "default", + "layout": "header, diff", + "require_changes": [ + CoverageCommentRequiredChanges.no_requirements.value + ], + }, + } diff --git a/apps/worker/services/yaml/tests/test_yaml_fetching.py b/apps/worker/services/yaml/tests/test_yaml_fetching.py new file mode 100644 index 0000000000..81a25fb414 --- /dev/null +++ b/apps/worker/services/yaml/tests/test_yaml_fetching.py @@ -0,0 +1,160 @@ +import mock +import pytest + +from database.tests.factories import CommitFactory +from services.yaml.fetcher import fetch_commit_yaml_from_provider +from test_utils.base import BaseTestCase + +sample_yaml = """ +codecov: + notify: + require_ci_to_pass: yes +""" + +sample_yaml_with_secret = """ +coverage: + precision: 2 # 2 = xx.xx%, 0 = xx% + round: down # default down + range: 50...60 # default 70...90. red...green + + notify: + + slack: + default: + url: "secret:c/nCgqn5v1HY5VFIs9i4W3UY6eleB2rTBdBKK/ilhPR7Ch4N0FE1aO6SRfAxp3Zlm4tLNusaPY7ettH6dTYj/YhiRohxiNqJMJ4L9YQmESo=" + threshold: 1% + branches: null # all branches by default + message: "Coverage {{changed}} for {{owner}}/{{repo}}" # customize the message + attachments: "sunburst, diff" + only_pulls: false + flags: null + paths: null +""" + + +class TestYamlSavingService(BaseTestCase): + @pytest.mark.asyncio + async def test_fetch_commit_yaml_from_provider(self, mocker): + mocked_list_files_result = [ + {"name": ".gitignore", "path": ".gitignore", "type": "file"}, + {"name": ".travis.yml", "path": ".travis.yml", "type": "file"}, + {"name": "README.rst", "path": "README.rst", "type": "file"}, + {"name": "awesome", "path": "awesome", "type": "folder"}, + {"name": "codecov", "path": "codecov", "type": "file"}, + {"name": "codecov.yaml", "path": "codecov.yaml", "type": "file"}, + {"name": "tests", "path": "tests", "type": "folder"}, + ] + list_files_future = mocked_list_files_result + contents_result = {"content": sample_yaml} + contents_result_future = contents_result + valid_handler = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=list_files_future), + get_source=mock.AsyncMock(return_value=contents_result_future), + ) + commit = CommitFactory.create() + res = await fetch_commit_yaml_from_provider(commit, valid_handler) + assert res == {"codecov": {"notify": {}, "require_ci_to_pass": True}} + valid_handler.list_top_level_files.assert_called_with(commit.commitid) + valid_handler.get_source.assert_called_with("codecov.yaml", commit.commitid) + + @pytest.mark.asyncio + async def test_fetch_commit_yaml_from_provider_with_secret(self, mocker, dbsession): + mocked_list_files_result = [ + {"name": ".gitignore", "path": ".gitignore", "type": "file"}, + {"name": ".travis.yml", "path": ".travis.yml", "type": "file"}, + {"name": "README.rst", "path": "README.rst", "type": "file"}, + {"name": "awesome", "path": "awesome", "type": "folder"}, + {"name": "codecov", "path": "codecov", "type": "file"}, + {"name": "codecov.yaml", "path": "codecov.yaml", "type": "file"}, + {"name": "tests", "path": "tests", "type": "folder"}, + ] + list_files_future = mocked_list_files_result + contents_result = {"content": sample_yaml_with_secret} + contents_result_future = contents_result + valid_handler = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=list_files_future), + get_source=mock.AsyncMock(return_value=contents_result_future), + ) + good_commit = CommitFactory.create( + repository__owner__service="github", + repository__owner__service_id=44376991, + repository__service_id=156617777, + ) + dbsession.add(good_commit) + res = await fetch_commit_yaml_from_provider(good_commit, valid_handler) + assert res == { + "coverage": { + "precision": 2, + "round": "down", + "range": [50.0, 60.0], + "notify": { + "slack": { + "default": { + "url": "http://test.thiago.website", + "threshold": 1.0, + "branches": None, + "message": "Coverage {{changed}} for {{owner}}/{{repo}}", + "attachments": "sunburst, diff", + "only_pulls": False, + "flags": None, + "paths": None, + } + } + }, + } + } + valid_handler.list_top_level_files.assert_called_with(good_commit.commitid) + valid_handler.get_source.assert_called_with( + "codecov.yaml", good_commit.commitid + ) + + @pytest.mark.asyncio + async def test_fetch_commit_yaml_from_provider_with_secret_bad_commit( + self, mocker, dbsession + ): + mocked_list_files_result = [ + {"name": ".gitignore", "path": ".gitignore", "type": "file"}, + {"name": ".travis.yml", "path": ".travis.yml", "type": "file"}, + {"name": "README.rst", "path": "README.rst", "type": "file"}, + {"name": "awesome", "path": "awesome", "type": "folder"}, + {"name": "codecov", "path": "codecov", "type": "file"}, + {"name": "codecov.yaml", "path": "codecov.yaml", "type": "file"}, + {"name": "tests", "path": "tests", "type": "folder"}, + ] + list_files_future = mocked_list_files_result + contents_result = {"content": sample_yaml_with_secret} + contents_result_future = contents_result + valid_handler = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=list_files_future), + get_source=mock.AsyncMock(return_value=contents_result_future), + ) + bad_commit = CommitFactory.create( + repository__owner__service="github", + repository__owner__service_id=44123999, # correct one is 44376991 + repository__service_id=156617777, + ) + dbsession.add(bad_commit) + res = await fetch_commit_yaml_from_provider(bad_commit, valid_handler) + assert res == { + "coverage": { + "precision": 2, + "round": "down", + "range": [50.0, 60.0], + "notify": { + "slack": { + "default": { + "url": "secret:c/nCgqn5v1HY5VFIs9i4W3UY6eleB2rTBdBKK/ilhPR7Ch4N0FE1aO6SRfAxp3Zlm4tLNusaPY7ettH6dTYj/YhiRohxiNqJMJ4L9YQmESo=", + "threshold": 1.0, + "branches": None, + "message": "Coverage {{changed}} for {{owner}}/{{repo}}", + "attachments": "sunburst, diff", + "only_pulls": False, + "flags": None, + "paths": None, + } + } + }, + } + } + valid_handler.list_top_level_files.assert_called_with(bad_commit.commitid) + valid_handler.get_source.assert_called_with("codecov.yaml", bad_commit.commitid) diff --git a/apps/worker/services/yaml/tests/test_yaml_parsing.py b/apps/worker/services/yaml/tests/test_yaml_parsing.py new file mode 100644 index 0000000000..689a063da9 --- /dev/null +++ b/apps/worker/services/yaml/tests/test_yaml_parsing.py @@ -0,0 +1,211 @@ +from pathlib import Path + +import pytest +from shared.validation.exceptions import InvalidYamlException +from shared.validation.types import CoverageCommentRequiredChanges + +from services.yaml.parser import parse_yaml_file +from test_utils.base import BaseTestCase + +here = Path(__file__) + + +class TestYamlSavingService(BaseTestCase): + def test_parse_empty_yaml(self): + contents = "" + res = parse_yaml_file(contents, show_secrets_for=("github", 123, 456)) + assert res is None + + def test_parse_invalid_yaml(self): + contents = "invalid: aaa : bbb" + with pytest.raises(InvalidYamlException): + parse_yaml_file(contents, show_secrets_for=("github", 123, 456)) + + def test_parse_simple_yaml(self): + with open(here.parent / "samples" / "sample_yaml_1.yaml") as f: + contents = f.read() + res = parse_yaml_file(contents, show_secrets_for=("github", 123, 456)) + expected_result = { + "coverage": { + "precision": 2, + "round": "down", + "range": [70.0, 100.0], + "status": {"project": True, "patch": True, "changes": False}, + }, + "codecov": {"notify": {}, "require_ci_to_pass": True}, + "comment": { + "behavior": "default", + "layout": "header, diff", + "require_changes": [ + CoverageCommentRequiredChanges.no_requirements.value + ], + }, + "parsers": { + "gcov": { + "branch_detection": { + "conditional": True, + "loop": True, + "macro": False, + "method": False, + } + } + }, + } + assert res == expected_result + + def test_parse_big_yaml_file(self): + with open(here.parent / "samples" / "big.yaml") as f: + contents = f.read() + res = parse_yaml_file( + contents, show_secrets_for=("github", 44376991, 156617777) + ) + expected_result = { + "comment": { + "branches": [".*"], + "layout": "diff, flags, reach", + "behavior": "default", + }, + "ignore": [r"(?s:tests/[^\/]*)\Z"], + "flags": { + "integration": { + "ignore": ["^app/ui.*"], + "assume": {"branches": ["^master$"]}, + } + }, + "codecov": { + "ci": ["ci.domain.com", "!provider"], + "url": "http://codecov.io", + "bot": "username", + "token": "uuid", + "notify": {"countdown": 12, "after_n_builds": 2, "delay": 4}, + "branch": "master", + "slug": "owner/repo", + "require_ci_to_pass": True, + }, + "coverage": { + "status": { + "project": { + "default": { + "if_ci_failed": "error", + "only_pulls": False, + "branches": ["^master$"], + "target": "auto", + "paths": ["^folder.*"], + "base": "parent", + "flags": ["integration"], + "if_not_found": "success", + "if_no_uploads": "error", + "threshold": 1.0, + } + }, + "changes": { + "default": { + "if_ci_failed": "error", + "only_pulls": False, + "branches": None, + "paths": ["^folder.*"], + "base": "parent", + "flags": ["integration"], + "if_not_found": "success", + "if_no_uploads": "error", + } + }, + "patch": { + "default": { + "if_ci_failed": "error", + "only_pulls": False, + "branches": None, + "target": 80.0, + "paths": ["^folder.*"], + "base": "parent", + "flags": ["integration"], + "if_not_found": "success", + "if_no_uploads": "success", + } + }, + }, + "range": [50.0, 60.0], + "precision": 2, + "round": "down", + "notify": { + "slack": { + "default": { + "only_pulls": False, + "branches": None, + "attachments": "sunburst, diff", + "paths": None, + "url": "http://test.thiago.website", + "flags": None, + "threshold": 1.0, + "message": "Coverage {{changed}} for {{owner}}/{{repo}}", + } + }, + "hipchat": { + "default": { + "paths": None, + "branches": None, + "url": "http://test.thiago.website", + "flags": None, + "notify": False, + "threshold": 1.0, + "message": "Coverage {{changed}} for {{owner}}/{{repo}}", + } + }, + "irc": { + "default": { + "paths": None, + "branches": None, + "threshold": 1.0, + "flags": None, + "message": "Coverage {{changed}} for {{owner}}/{{repo}}", + "server": "chat.freenode.net", + } + }, + "webhook": { + "_name_": { + "url": "http://test.thiago.website", + "threshold": 1.0, + "branches": None, + } + }, + "email": { + "default": { + "paths": None, + "only_pulls": False, + "layout": "reach, diff, flags", + "to": [ + "example@domain.com", + "secondexample@seconddomain.com", + ], + "threshold": 1.0, + "flags": None, + } + }, + "gitter": { + "default": { + "url": "http://test.thiago.website", + "threshold": 1.0, + "message": "Coverage {{changed}} for {{owner}}/{{repo}}", + "branches": None, + } + }, + }, + }, + "fixes": ["^old_path::new_path"], + } + assert sorted(res.get("comment").items()) == sorted( + expected_result.get("comment").items() + ) + assert sorted(res.get("ignore")) == sorted(expected_result.get("ignore")) + assert sorted(res.get("flags").items()) == sorted( + expected_result.get("flags").items() + ) + assert sorted(res.get("codecov").items()) == sorted( + expected_result.get("codecov").items() + ) + assert sorted(res.get("coverage").items()) == sorted( + expected_result.get("coverage").items() + ) + assert sorted(res.get("fixes")) == sorted(expected_result.get("fixes")) + assert sorted(res.items()) == sorted(expected_result.items()) + assert res == expected_result diff --git a/apps/worker/services/yaml/tests/test_yaml_reader.py b/apps/worker/services/yaml/tests/test_yaml_reader.py new file mode 100644 index 0000000000..151b376059 --- /dev/null +++ b/apps/worker/services/yaml/tests/test_yaml_reader.py @@ -0,0 +1,177 @@ +from decimal import Decimal + +from shared.yaml.user_yaml import UserYaml + +from helpers.components import Component +from services.yaml.reader import ( + get_components_from_yaml, + get_paths_from_flags, + round_number, +) + + +class TestYamlReader(object): + def test_round_number(self): + round_up_yaml_dict = {"coverage": {"precision": 5, "round": "up"}} + assert Decimal("1.23457") == round_number( + round_up_yaml_dict, Decimal("1.23456789") + ) + assert Decimal("1.23457") == round_number( + round_up_yaml_dict, Decimal("1.234565") + ) + assert Decimal("1.23456") == round_number( + round_up_yaml_dict, Decimal("1.234555") + ) + round_down_yaml_dict = {"coverage": {"precision": 5, "round": "down"}} + assert Decimal("1.23456") == round_number( + round_down_yaml_dict, Decimal("1.23456789") + ) + assert Decimal("1.23456") == round_number( + round_down_yaml_dict, Decimal("1.234565") + ) + assert Decimal("1.23455") == round_number( + round_down_yaml_dict, Decimal("1.234555") + ) + yaml_dict = {"coverage": {"precision": 5, "round": "nearest"}} + assert Decimal("1.23457") == round_number(yaml_dict, Decimal("1.23456789")) + assert Decimal("1.23456") == round_number(yaml_dict, Decimal("1.234565")) + assert Decimal("1.23456") == round_number(yaml_dict, Decimal("1.234555")) + + def test_get_paths_from_flags(self): + yaml_dict = UserYaml( + { + "flags": { + "sample_1": {"paths": ["path_1/.*", r"path_2/.*\.py"]}, + "sample_2": {"paths": None}, + "sample_3": {"paths": ["path_1/.*"]}, + "sample_4": {"paths": []}, + "sample_5": { + "paths": [ + "path_5/.*", + r"path_6/.*/[^\/]+", + r"path_8/specific\.py", + ] + }, + } + } + ) + flags_to_use = ["sample_1", "sample_4", "sample_5"] + expected_result = [ + "path_1/.*", + r"path_2/.*\.py", + "path_5/.*", + r"path_6/.*/[^\/]+", + r"path_8/specific\.py", + ] + result = get_paths_from_flags(yaml_dict, flags_to_use) + assert set(expected_result) == set(result) + assert [] == get_paths_from_flags( + yaml_dict, ["sample_1", "sample_2", "sample_4", "sample_5"] + ) + assert [ + "path_1/.*", + r"path_2/.*\.py", + "path_5/.*", + r"path_6/.*/[^\/]+", + r"path_8/specific\.py", + ] == sorted( + get_paths_from_flags( + yaml_dict, ["sample_1", "sample_4", "sample_5", "banana"] + ) + ) + + def test_get_components_no_default(self): + yaml_dict = UserYaml( + { + "component_management": { + "individual_components": [ + {"component_id": "py_files", "paths": [r".*\.py"]} + ] + } + } + ) + components = get_components_from_yaml(yaml_dict) + assert len(components) == 1 + assert components == [ + Component( + component_id="py_files", + paths=[r".*\.py"], + name="", + flag_regexes=[], + statuses=[], + ) + ] + + def test_get_components_default_no_components(self): + yaml_dict = UserYaml({"component_management": {}}) + components = get_components_from_yaml(yaml_dict) + assert len(components) == 0 + + def test_get_components_default_only(self): + yaml_dict = UserYaml( + { + "component_management": { + "default_rules": {"paths": [r".*\.py"], "flag_regexes": [r"flag.*"]} + } + } + ) + components = get_components_from_yaml(yaml_dict) + assert len(components) == 0 + + def test_get_components_all(self): + yaml_dict = UserYaml( + { + "component_management": { + "default_rules": { + "paths": [r".*\.py"], + "flag_regexes": [r"flag.*"], + }, + "individual_components": [ + {"component_id": "go_files", "paths": [r".*\.go"]}, + {"component_id": "rules_from_default"}, + { + "component_id": "I have my flags", + "flag_regexes": [r"python-.*"], + }, + { + "component_id": "required", + "name": "display", + "flag_regexes": [], + "paths": [r"src/.*"], + }, + ], + } + } + ) + components = get_components_from_yaml(yaml_dict) + assert len(components) == 4 + assert components == [ + Component( + component_id="go_files", + paths=[r".*\.go"], + name="", + flag_regexes=[r"flag.*"], + statuses=[], + ), + Component( + component_id="rules_from_default", + paths=[r".*\.py"], + name="", + flag_regexes=[r"flag.*"], + statuses=[], + ), + Component( + component_id="I have my flags", + paths=[r".*\.py"], + name="", + flag_regexes=[r"python-.*"], + statuses=[], + ), + Component( + component_id="required", + name="display", + paths=[r"src/.*"], + flag_regexes=[], + statuses=[], + ), + ] diff --git a/apps/worker/services/yaml/tests/test_yaml_saving.py b/apps/worker/services/yaml/tests/test_yaml_saving.py new file mode 100644 index 0000000000..3bb512a8cd --- /dev/null +++ b/apps/worker/services/yaml/tests/test_yaml_saving.py @@ -0,0 +1,48 @@ +import pytest + +from database.tests.factories import CommitFactory, OwnerFactory +from services.yaml import save_repo_yaml_to_database_if_needed +from test_utils.base import BaseTestCase + + +class TestYamlSavingService(BaseTestCase): + def test_save_repo_yaml_to_database_if_needed(self, mocker): + commit = CommitFactory.create( + branch="master", repository__branch="master", repository__yaml={"old_stuff"} + ) + commit_yaml = {"new_values": "aHAAA"} + res = save_repo_yaml_to_database_if_needed(commit, commit_yaml) + assert res + assert commit.repository.yaml == commit_yaml + assert commit.repository.branch == "master" + assert commit.repository.bot_id is None + + @pytest.mark.django_db + def test_save_repo_yaml_to_database_if_needed_with_new_branch_and_bot(self, mocker): + commit = CommitFactory.create( + branch="master", + repository__branch="master", + repository__service_id="github", + repository__yaml={"old_stuff"}, + ) + bot_owner = OwnerFactory.create(name="robot", service="github") + commit_yaml = { + "new_values": "aHAAA", + "codecov": {"branch": "brand_new_branch", "bot": "robot"}, + } + res = save_repo_yaml_to_database_if_needed(commit, commit_yaml) + assert res + assert commit.repository.yaml == commit_yaml + assert commit.repository.branch == "brand_new_branch" + assert commit.repository.bot_id == bot_owner.ownerid + + def test_save_repo_yaml_to_database_not_needed(self, mocker): + commit = CommitFactory.create( + branch="master", + repository__branch="develop", + repository__yaml={"old_stuff": "old_feelings"}, + ) + commit_yaml = {"new_values": "aHAAA"} + res = save_repo_yaml_to_database_if_needed(commit, commit_yaml) + assert not res + assert commit.repository.yaml == {"old_stuff": "old_feelings"} diff --git a/apps/worker/ta_storage/__init__.py b/apps/worker/ta_storage/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/ta_storage/pg.py b/apps/worker/ta_storage/pg.py new file mode 100644 index 0000000000..04efeec1a0 --- /dev/null +++ b/apps/worker/ta_storage/pg.py @@ -0,0 +1,332 @@ +from __future__ import annotations + +from datetime import date, datetime +from typing import Any, Literal, TypedDict + +import test_results_parser +from sqlalchemy.dialects.postgresql import insert +from sqlalchemy.orm import Session + +from database.models import ( + DailyTestRollup, + RepositoryFlag, + Test, + TestFlagBridge, + TestInstance, + Upload, +) +from services.test_results import generate_flags_hash, generate_test_id + + +class DailyTotals(TypedDict): + test_id: str + repoid: int + pass_count: int + fail_count: int + skip_count: int + flaky_fail_count: int + branch: str + date: date + latest_run: datetime + commits_where_fail: list[str] + last_duration_seconds: float + avg_duration_seconds: float + + +def get_repo_flag_ids(db_session: Session, repoid: int, flags: list[str]) -> set[int]: + if not flags: + return set() + + return set( + db_session.query(RepositoryFlag.id_) + .filter( + RepositoryFlag.repository_id == repoid, + RepositoryFlag.flag_name.in_(flags), + ) + .all() + ) + + +def modify_structures( + tests_to_write: dict[str, dict[str, Any]], + test_instances_to_write: list[dict[str, Any]], + test_flag_bridge_data: list[dict], + daily_totals: dict[str, DailyTotals], + testrun: test_results_parser.Testrun, + upload: Upload, + repoid: int, + branch: str | None, + commit_sha: str, + repo_flag_ids: set[int], + flaky_test_set: set[str], + framework: str | None, +): + flags_hash = generate_flags_hash(upload.flag_names) + + test_name = f"{testrun['classname']}\x1f{testrun['name']}" + test_id = generate_test_id( + repoid, + testrun["testsuite"], + test_name, + flags_hash, + ) + + test = generate_test_dict( + test_id, test_name, repoid, testrun, flags_hash, framework + ) + tests_to_write[test_id] = test + + test_instance = generate_test_instance_dict( + test_id, upload, testrun, commit_sha, branch, repoid + ) + test_instances_to_write.append(test_instance) + + if repo_flag_ids: + test_flag_bridge_data.extend( + {"test_id": test_id, "flag_id": flag_id} for flag_id in repo_flag_ids + ) + + if test_id in daily_totals: + update_daily_totals( + daily_totals, + test_id, + testrun["duration"], + testrun["outcome"], + ) + else: + create_daily_totals( + daily_totals, + test_id, + repoid, + testrun["duration"], + testrun["outcome"], + branch, + commit_sha, + flaky_test_set, + ) + + +def generate_test_dict( + test_id: str, + test_name: str, + repoid: int, + testrun: test_results_parser.Testrun, + flags_hash: str, + framework: str | None, +) -> dict[str, Any]: + return { + "id": test_id, + "repoid": repoid, + "name": test_name, + "testsuite": testrun["testsuite"], + "flags_hash": flags_hash, + "framework": framework, + "filename": testrun["filename"], + "computed_name": testrun["computed_name"], + } + + +def generate_test_instance_dict( + test_id: str, + upload: Upload, + testrun: test_results_parser.Testrun, + commit_sha: str, + branch: str | None, + repoid: int, +) -> dict[str, Any]: + return { + "test_id": test_id, + "upload_id": upload.id, + "duration_seconds": testrun["duration"], + "outcome": testrun["outcome"], + "failure_message": testrun["failure_message"], + "commitid": commit_sha, + "branch": branch, + "reduced_error_id": None, + "repoid": repoid, + } + + +def update_daily_totals( + daily_totals: dict[str, DailyTotals], + test_id: str, + duration_seconds: float | None, + outcome: Literal["pass", "failure", "error", "skip"], +): + # logic below is a little complicated but we're basically doing: + + # (old_avg * num of values used to compute old avg) + new value + # ------------------------------------------------------------- + # num of values used to compute old avg + 1 + if ( + duration_seconds is not None + and daily_totals[test_id]["avg_duration_seconds"] is not None + ): + daily_totals[test_id]["avg_duration_seconds"] = ( + daily_totals[test_id]["avg_duration_seconds"] + * ( + daily_totals[test_id]["pass_count"] + + daily_totals[test_id]["fail_count"] + ) + + duration_seconds + ) / ( + daily_totals[test_id]["pass_count"] + + daily_totals[test_id]["fail_count"] + + 1 + ) + + if outcome == "pass": + daily_totals[test_id]["pass_count"] += 1 + elif outcome == "failure" or outcome == "error": + daily_totals[test_id]["fail_count"] += 1 + elif outcome == "skip": + daily_totals[test_id]["skip_count"] += 1 + + +def create_daily_totals( + daily_totals: dict, + test_id: str, + repoid: int, + duration_seconds: float | None, + outcome: Literal["pass", "failure", "error", "skip"], + branch: str | None, + commit_sha: str, + flaky_test_set: set[str], +): + daily_totals[test_id] = { + "test_id": test_id, + "repoid": repoid, + "last_duration_seconds": duration_seconds or 0.0, + "avg_duration_seconds": duration_seconds or 0.0, + "pass_count": 1 if outcome == "pass" else 0, + "fail_count": 1 if outcome == "failure" or outcome == "error" else 0, + "skip_count": 1 if outcome == "skip" else 0, + "flaky_fail_count": 1 + if test_id in flaky_test_set and (outcome == "failure" or outcome == "error") + else 0, + "branch": branch, + "date": date.today(), + "latest_run": datetime.now(), + "commits_where_fail": [commit_sha] + if (outcome == "failure" or outcome == "error") + else [], + } + + +def save_tests(db_session: Session, tests_to_write: dict[str, dict[str, Any]]): + test_data = sorted( + tests_to_write.values(), + key=lambda x: str(x["id"]), + ) + + test_insert = insert(Test.__table__).values(test_data) + insert_on_conflict_do_update = test_insert.on_conflict_do_update( + index_elements=["id"], + set_={ + "framework": test_insert.excluded.framework, + "computed_name": test_insert.excluded.computed_name, + "filename": test_insert.excluded.filename, + }, + ) + db_session.execute(insert_on_conflict_do_update) + db_session.commit() + + +def save_test_flag_bridges(db_session: Session, test_flag_bridge_data: list[dict]): + insert_on_conflict_do_nothing_flags = ( + insert(TestFlagBridge.__table__) + .values(test_flag_bridge_data) + .on_conflict_do_nothing(index_elements=["test_id", "flag_id"]) + ) + db_session.execute(insert_on_conflict_do_nothing_flags) + db_session.commit() + + +def save_daily_test_rollups(db_session: Session, daily_rollups: dict[str, DailyTotals]): + sorted_rollups = sorted(daily_rollups.values(), key=lambda x: str(x["test_id"])) + rollup_table = DailyTestRollup.__table__ + stmt = insert(rollup_table).values(sorted_rollups) + stmt = stmt.on_conflict_do_update( + index_elements=[ + "repoid", + "branch", + "test_id", + "date", + ], + set_={ + "last_duration_seconds": stmt.excluded.last_duration_seconds, + "avg_duration_seconds": ( + rollup_table.c.avg_duration_seconds + * (rollup_table.c.pass_count + rollup_table.c.fail_count) + + stmt.excluded.avg_duration_seconds + ) + / (rollup_table.c.pass_count + rollup_table.c.fail_count + 1), + "latest_run": stmt.excluded.latest_run, + "pass_count": rollup_table.c.pass_count + stmt.excluded.pass_count, + "skip_count": rollup_table.c.skip_count + stmt.excluded.skip_count, + "fail_count": rollup_table.c.fail_count + stmt.excluded.fail_count, + "flaky_fail_count": rollup_table.c.flaky_fail_count + + stmt.excluded.flaky_fail_count, + "commits_where_fail": rollup_table.c.commits_where_fail + + stmt.excluded.commits_where_fail, + }, + ) + db_session.execute(stmt) + db_session.commit() + + +def save_test_instances(db_session: Session, test_instance_data: list[dict]): + insert_test_instances = insert(TestInstance.__table__).values(test_instance_data) + db_session.execute(insert_test_instances) + db_session.commit() + + +class PGDriver: + def __init__(self, db_session: Session, flaky_test_set: set[str]): + self.db_session = db_session + self.flaky_test_set = flaky_test_set + + def write_testruns( + self, + timestamp: int | None, + repo_id: int, + commit_sha: str, + branch_name: str, + upload: Upload, + framework: str | None, + testruns: list[test_results_parser.Testrun], + ): + tests_to_write: dict[str, dict[str, Any]] = {} + test_instances_to_write: list[dict[str, Any]] = [] + daily_totals: dict[str, DailyTotals] = dict() + test_flag_bridge_data: list[dict] = [] + + repo_flag_ids = get_repo_flag_ids(self.db_session, repo_id, upload.flag_names) + + for testrun in testruns: + modify_structures( + tests_to_write, + test_instances_to_write, + test_flag_bridge_data, + daily_totals, + testrun, + upload, + repo_id, + branch_name, + commit_sha, + repo_flag_ids, + self.flaky_test_set, + framework, + ) + + if len(tests_to_write) > 0: + save_tests(self.db_session, tests_to_write) + + if len(test_flag_bridge_data) > 0: + save_test_flag_bridges(self.db_session, test_flag_bridge_data) + + if len(daily_totals) > 0: + save_daily_test_rollups(self.db_session, daily_totals) + + if len(test_instances_to_write) > 0: + save_test_instances(self.db_session, test_instances_to_write) diff --git a/apps/worker/ta_storage/tests/test_pg.py b/apps/worker/ta_storage/tests/test_pg.py new file mode 100644 index 0000000000..370cd1b42c --- /dev/null +++ b/apps/worker/ta_storage/tests/test_pg.py @@ -0,0 +1,63 @@ +from database.models import DailyTestRollup, Test, TestFlagBridge, TestInstance +from database.tests.factories import RepositoryFlagFactory, UploadFactory +from ta_storage.pg import PGDriver + + +def test_pg_driver(dbsession): + pg = PGDriver(dbsession, set()) + + upload = UploadFactory() + dbsession.add(upload) + dbsession.flush() + + repo_flag_1 = RepositoryFlagFactory( + repository=upload.report.commit.repository, flag_name="flag1" + ) + repo_flag_2 = RepositoryFlagFactory( + repository=upload.report.commit.repository, flag_name="flag2" + ) + dbsession.add(repo_flag_1) + dbsession.add(repo_flag_2) + dbsession.flush() + + upload.flags.append(repo_flag_1) + upload.flags.append(repo_flag_2) + dbsession.flush() + + pg.write_testruns( + None, + upload.report.commit.repoid, + upload.report.commit.id, + upload.report.commit.branch, + upload, + "pytest", + [ + { + "name": "test_name", + "classname": "test_class", + "testsuite": "test_suite", + "duration": 100.0, + "outcome": "pass", + "build_url": "https://example.com/build/123", + "filename": "test_file", + "computed_name": "test_computed_name", + "failure_message": None, + }, + { + "name": "test_name2", + "classname": "test_class2", + "testsuite": "test_suite2", + "duration": 100.0, + "outcome": "failure", + "build_url": "https://example.com/build/123", + "filename": "test_file2", + "computed_name": "test_computed_name2", + "failure_message": "test_failure_message", + }, + ], + ) + + assert dbsession.query(Test).count() == 2 + assert dbsession.query(TestInstance).count() == 2 + assert dbsession.query(TestFlagBridge).count() == 4 + assert dbsession.query(DailyTestRollup).count() == 2 diff --git a/apps/worker/tasks/README.md b/apps/worker/tasks/README.md new file mode 100644 index 0000000000..4689db53bd --- /dev/null +++ b/apps/worker/tasks/README.md @@ -0,0 +1,81 @@ +# Tasks + +The actual tasks that power the worker live on this folder. + +The task system is powered by [celery](https://docs.celeryproject.org/en/latest/index.html). + +If you don't understand tasks well, you can see them as the Django (or your favorite web framework) views from this system. They are the entrypoint to talking to the workers. + +## Existing tasks + +Please take a look at the code in this directory (the `tasks` directory) +to get all the tasks defined. + +## Rules about tasks + +As described in the repo README, there should be a couple of cares when using tasks: + +1. Tasks, just like Django views, are an entrypoint to the external world. A lot of the task is about doing wiring, ie, properly parsing and organizing data structures so the actual logic doesn't have to. + +2. There is only so much logic a task should have. The heavier the logic gets, the more sensible it is to move them to services. + - A good example is how `NotifyTask` and `NotificationService` interact. Even though `NotifyTask` is the only place that uses `NotificationService` nowadays, it was sensible to create `NotificationService` to deal with the actual intricacies of starting the right notifications, collecting results and handling errors. What the task does is collect the proper config yamls, the proper Report objects, pass them over to the service and then collect the results of the notifications, while handling top level things like retries and some higher-level rules. + - There are exceptions to this rule. It is ok to hold heavier logic in a task if you feel the logic is too specific to it and wouldn't be easily used anywhere else without a lot of interfacing compromises. But even in those cases, move the logic away from the main function and put them on a different method + +3. Tasks, just like views, are meant to be on their own. We should not import logic pieces from one task to another (nor to anywhere else). If something is useful to two different tasks, consider putting the shared logic in a service (see point above) and then importing that service on both tasks. Just make sure the service interface (method signatures and return type, for example) are not too specific to the tasks you are calling. + +4. It is ok for tasks to schedule other tasks (which is different than directly calling code from them), but it is not ok for a task to wait for other task to finish. This is the textbook example of causes of a deadlock. If you really need to coordinate different tasks, consider using celery's built-in tools, like chaining/grouping/chording tasks. + +5. Tasks should not receive complex types as parameters at their main function. The only acceptable basic types for a task to receive as parameter are int, float, str. Lists and dicts are acceptable as long as they only contain other acceptable types. Avoid temptation to use Enums, for example, even if they look easy enough to deal with (been there, broke that). After the main function, feel free to pass whatever you want to whoever you want, since you are already inside your own python code anyway. + - The reason for that is about how those tasks are serialized and deserialized when sent to the worker. Task arguments are stored to redis ( or another non-python queue system) when it's scheduled. So its data (read, arguments) has to be serialized to a no-python-specific format in order to be saved there and deserialized when it gets back to python. + - The most two common serializers provided out of the box by celery are `json` (current default) and `pickle` (previous default) + - `pickle` is a system that python users for serializer/deserializing python objects. It's a very sensitive to changs in the the code. Such that in some "simple" changes can unexpectedely make the pickler crash with old versions of a pickled object. Add this to point number 8 below, and there is a risk of the pickler crashing on each deploy. Also, pickle stopped being the default for a reason (security purposes and harder to read when the data on redis) + `json` is only meant for json-serializable objects (which are only the above-allowed objects). Hooking it with your own serializer/deserializer for getting custom objects opens its own can of worms. It's usually cleaner and easier to pass a "dried-up" version of the object (like its id or a dict containing all its relevant info) and "hydrate" the object yourself inside the function. + +6. Task return types usually don't matter much, but be aware that our system is not meant to have the task returned values used anywhere else. A normal practice is to return a small dict with some decent information about the task general result. This eases testing and the results are also logged to some extent on datadog. + - They aren't used anywhere because a) they aren't saved anywhere, and b) some of them they take so long, that there is not a sensible way to make customers wait for them. + - If there is ever an interest on getting actual returned values from tasks, we might need to put them on their own queue with their own workers, so they don't suffer from waiting for other long-running tasks to finish. + +7. Testing tasks should be done at two levels: unit and integration. The reasons are the same reasons one would unit-test and integration-test any system. It just happens that tasks are the best candidate inside the worker to see something from start to finish. + +8. This worker system, by its very own nature, is asynchronous. So you should never be reliant on good timing for deploys and or coordinating changes. Deploys don't happen on all machines at once (not within the worker system, let alone when coordinating different systems deploys). So the system should be prepared during a deploy of "new_version" code, that + - "new_version" workers will send tasks that will be processed by "old_version" workers + - "old_version" workers will send tasks that will be processed by "new_version" workers + - A deploy on web might be happening at the same time, and both new_web and old_web workers will send tasks to both old_version worker and new_version workers. + +9. Tasks are the only things in the worker system that are allowed to do actual database `COMMIT;`s. The base class mixin will already take care of commit the changes for you at the end of the task either way, but feel free to commit earlier whenever it is needed + - This is done so the services can all build on top of each other with the safety that they will not unexpectedly do a db commit before the caller is ready to commit that data. + - This also frees `services` functions to do nested db transactions without any function it calls accidentally commiting the nested transaction + +## Adding new tasks + +When adding a new task, you will already have a ton you need to do. But be sure to: + +1. Import that task on the [init file](./__init__.py) +2. Add a name to it. Or don't. But add whatever its name is to the [celery configuration](../celery_config.py) +3. Don't start scheduling things to it right away (see point above about the system being asychronous) +4. Make sure celery displays it when spinning up the system on your local machine + +You should be able to copy-paste any of the tasks when creating your own, but the important entrypoint of the task is the `run_impl` function. + +It needs to have + +* `self` as the first parameter (normal python class stuff) +* `db_session` as the second one (for getting access to the db, but even if you don't need db, you should have this argument) +* The arguments you want to have this function receive as input. Notice they will always be expected to be called as keyword parameters (ie, passing the name of the argument right at the function call instead of just putting them in the right order), so it is recommended to add `*` before them. +* Type the expected parameters as well. +* `**kwargs` - This is a safety measure to ensure that this function won't go crazy whenever you want to add more parameters to it. +* In the end, your function signature should look a bit like this: + +``` + def run_impl( + self, + db_session: Session, + *, + repoid: int, + commitid: str, + current_yaml=None, + **kwargs, + ): +``` + + diff --git a/apps/worker/tasks/__init__.py b/apps/worker/tasks/__init__.py new file mode 100644 index 0000000000..04c246658d --- /dev/null +++ b/apps/worker/tasks/__init__.py @@ -0,0 +1,65 @@ +# ruff: noqa: F401 +from app import celery_app +from tasks.activate_account_user import activate_account_user_task +from tasks.ai_pr_review import ai_pr_view_task +from tasks.backfill_existing_gh_app_installations import ( + backfill_existing_gh_app_installations_name, + backfill_existing_individual_gh_app_installation_name, +) +from tasks.backfill_owners_without_gh_app_installations import ( + backfill_owners_without_gh_app_installation_individual_name, + backfill_owners_without_gh_app_installations_name, +) +from tasks.brolly_stats_rollup import brolly_stats_rollup_task +from tasks.bundle_analysis_notify import bundle_analysis_notify_task +from tasks.bundle_analysis_processor import bundle_analysis_processor_task +from tasks.bundle_analysis_save_measurements import ( + bundle_analysis_save_measurements_task, +) +from tasks.cache_rollup_cron_task import cache_rollup_task +from tasks.cache_test_rollups import cache_test_rollups_task +from tasks.cache_test_rollups_redis import cache_test_rollups_redis_task +from tasks.commit_update import commit_update_task +from tasks.compute_comparison import compute_comparison_task +from tasks.compute_component_comparison import compute_component_comparison_task +from tasks.delete_owner import delete_owner_task +from tasks.flush_repo import flush_repo +from tasks.github_app_webhooks_check import gh_webhook_check_task +from tasks.github_marketplace import ghm_sync_plans_task +from tasks.health_check import health_check_task +from tasks.hourly_check import hourly_check_task +from tasks.http_request import http_request_task +from tasks.label_analysis import label_analysis_task +from tasks.manual_trigger import manual_trigger_task +from tasks.new_user_activated import new_user_activated_task +from tasks.notify import notify_task +from tasks.notify_error import notify_error_task +from tasks.plan_manager_task import daily_plan_manager_task_name +from tasks.preprocess_upload import preprocess_upload_task +from tasks.process_flakes import process_flakes_task +from tasks.regular_cleanup import regular_cleanup_task +from tasks.save_commit_measurements import save_commit_measurements_task +from tasks.save_report_results import save_report_results_task +from tasks.send_email import send_email +from tasks.static_analysis_suite_check import static_analysis_suite_check_task +from tasks.status_set_error import status_set_error_task +from tasks.status_set_pending import status_set_pending_task +from tasks.sync_pull import pull_sync_task +from tasks.sync_repo_languages import sync_repo_language_task +from tasks.sync_repo_languages_gql import sync_repo_languages_gql_task +from tasks.sync_repos import sync_repos_task +from tasks.sync_teams import sync_teams_task +from tasks.test_results_finisher import test_results_finisher_task +from tasks.test_results_processor import test_results_processor_task +from tasks.timeseries_backfill import ( + timeseries_backfill_commits_task, + timeseries_backfill_dataset_task, +) +from tasks.timeseries_delete import timeseries_delete_task +from tasks.trial_expiration import trial_expiration_task +from tasks.trial_expiration_cron import trial_expiration_cron_task +from tasks.update_branches import update_branches_task_name +from tasks.upload import upload_task +from tasks.upload_finisher import upload_finisher_task +from tasks.upload_processor import upload_processor_task +from tasks.upsert_component import upsert_component_task diff --git a/apps/worker/tasks/activate_account_user.py b/apps/worker/tasks/activate_account_user.py new file mode 100644 index 0000000000..8e606f4da6 --- /dev/null +++ b/apps/worker/tasks/activate_account_user.py @@ -0,0 +1,63 @@ +import logging + +from shared.celery_config import activate_account_user_task_name +from shared.django_apps.codecov_auth.models import Account, Owner +from sqlalchemy.orm.session import Session + +from app import celery_app +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class ActivateAccountUserTask(BaseCodecovTask, name=activate_account_user_task_name): + def run_impl( + self, + _db_session: Session, + *, + user_ownerid: int, + org_ownerid: int, + **kwargs, + ): + """ + Runs the task to activate a user onto an account. + :param user_ownerid: the user's owner id + :param org_ownerid: the organization owner id + """ + log_context = {"user_ownerid": user_ownerid, "org_ownerid": org_ownerid} + log.info( + "Syncing account for user", + extra=log_context, + ) + + owner_user: Owner = Owner.objects.get(pk=user_ownerid) + + # NOTE: We're currently ignoring organizations that don't have an account. + org_owner: Owner = Owner.objects.get(pk=org_ownerid) + account: Account | None = org_owner.account + if not account: + log.info( + "Organization does not have an account. Skipping account user activation." + ) + return {"successful": True} + + if account.can_activate_user(owner_user.user): + account.activate_owner_user_onto_account(owner_user) + account.save() + else: + log.info( + "User was not able to activate on account. It could be that the user is already activated, " + "or the account is in an inconsistent state.", + extra=log_context, + ) + + log.info( + "Successfully synced account for user", + extra=log_context, + ) + + return {"successful": True} + + +RegisteredActivateAccountUserTask = celery_app.register_task(ActivateAccountUserTask()) +activate_account_user_task = celery_app.tasks[ActivateAccountUserTask.name] diff --git a/apps/worker/tasks/ai_pr_review.py b/apps/worker/tasks/ai_pr_review.py new file mode 100644 index 0000000000..dc35475764 --- /dev/null +++ b/apps/worker/tasks/ai_pr_review.py @@ -0,0 +1,48 @@ +import logging + +from asgiref.sync import async_to_sync +from celery.exceptions import SoftTimeLimitExceeded +from sqlalchemy.orm.session import Session + +from app import celery_app +from database.models import Repository +from helpers.exceptions import RepositoryWithoutValidBotError +from services.ai_pr_review import perform_review +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class AiPrReviewTask(BaseCodecovTask, name="app.tasks.ai_pr_review.AiPrReview"): + throws = (SoftTimeLimitExceeded,) + + def run_impl( + self, + db_session: Session, + *, + repoid: int, + pullid: int, + **kwargs, + ): + log.info("Starting AI PR review task", extra=dict(repoid=repoid, pullid=pullid)) + + repository = db_session.query(Repository).filter_by(repoid=repoid).first() + assert repository + if repository.owner.service != "github": + log.warning("AI PR review only supports GitHub currently") + return {"successful": False, "error": "not_github"} + + try: + async_to_sync(perform_review)(repository, pullid) + return {"successful": True} + except RepositoryWithoutValidBotError: + log.warning( + "No valid bot found for repo", + extra=dict(pullid=pullid, repoid=repoid), + exc_info=True, + ) + return {"successful": False, "error": "no_bot"} + + +RegisteredAiPrReviewTask = celery_app.register_task(AiPrReviewTask()) +ai_pr_view_task = celery_app.tasks[RegisteredAiPrReviewTask.name] diff --git a/apps/worker/tasks/backfill_existing_gh_app_installations.py b/apps/worker/tasks/backfill_existing_gh_app_installations.py new file mode 100644 index 0000000000..8c7a31266b --- /dev/null +++ b/apps/worker/tasks/backfill_existing_gh_app_installations.py @@ -0,0 +1,132 @@ +import logging +from typing import List, Optional + +from sqlalchemy.orm.session import Session + +from app import celery_app +from celery_config import ( + backfill_existing_gh_app_installations_name, + backfill_existing_individual_gh_app_installation_name, +) +from database.models.core import GithubAppInstallation, Owner +from helpers.backfills import ( + add_repos_service_ids_from_provider, + maybe_set_installation_to_all_repos, +) +from services.owner import get_owner_provider_service +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class BackfillExistingGHAppInstallationsTask( + BaseCodecovTask, name=backfill_existing_gh_app_installations_name +): + def run_impl( + self, + db_session: Session, + owner_ids: Optional[List[int]] = None, + yield_amount: int = 1000, + *args, + **kwargs, + ): + log.info( + "Starting Existing GH App backfill task", + ) + + # Backfill gh apps we already have + # Get owners that have installations, and installations queries + owners_query = ( + db_session.query(Owner) + .join(GithubAppInstallation, Owner.ownerid == GithubAppInstallation.ownerid) + .filter( + Owner.service == "github", + ) + ) + gh_app_installations_query = db_session.query(GithubAppInstallation) + + # Filter if owner_ids were provided + if owner_ids: + owners_query = owners_query.filter(Owner.ownerid.in_(owner_ids)) + gh_app_installations_query = gh_app_installations_query.filter( + GithubAppInstallation.ownerid.in_(owner_ids) + ) + + gh_app_installations: List[GithubAppInstallation] = ( + gh_app_installations_query.yield_per(yield_amount) + ) + + for gh_app_installation in gh_app_installations: + self.app.tasks[ + backfill_existing_individual_gh_app_installation_name + ].apply_async(kwargs=dict(gh_app_installation_id=gh_app_installation.id)) + + return {"successful": True, "reason": "backfill tasks queued"} + + +RegisteredBackfillExistingGHAppInstallationsTask = celery_app.register_task( + BackfillExistingGHAppInstallationsTask() +) +backfill_existing_gh_app_installations_task = celery_app.tasks[ + RegisteredBackfillExistingGHAppInstallationsTask.name +] + + +class BackfillExistingIndividualGHAppInstallationTask( + BaseCodecovTask, name=backfill_existing_individual_gh_app_installation_name +): + def run_impl( + self, + db_session: Session, + gh_app_installation_id: int, + *args, + **kwargs, + ): + gh_app_installation = db_session.query(GithubAppInstallation).get( + gh_app_installation_id + ) + + # Check if gh app has 'all' repositories selected + owner = gh_app_installation.owner + ownerid = gh_app_installation.owner.ownerid + + log.info( + "Attempt to backfill gh_app_installation", + extra=dict(owner_id=ownerid, parent_id=self.request.parent_id), + ) + + try: + owner_service = get_owner_provider_service(owner=owner) + is_selection_all = maybe_set_installation_to_all_repos( + db_session=db_session, + owner_service=owner_service, + gh_app_installation=gh_app_installation, + ) + + if not is_selection_all: + # Find and add all repos the gh app has access to + add_repos_service_ids_from_provider( + db_session=db_session, + ownerid=ownerid, + owner_service=owner_service, + gh_app_installation=gh_app_installation, + ) + log.info( + "Successful backfill", + extra=dict(ownerid=ownerid, parent_id=self.request.parent_id), + ) + return {"successful": True, "reason": "backfill task finished"} + except Exception: + log.info( + "Backfill unsuccessful for this owner", + extra=dict(ownerid=ownerid, parent_id=self.request.parent_id), + ) + return {"successful": False, "reason": "backfill unsuccessful"} + + +RegisteredBackfillExistingIndividualGHAppInstallationTask = celery_app.register_task( + BackfillExistingIndividualGHAppInstallationTask() +) +backfill_existing_individual_gh_app_installation_task = celery_app.tasks[ + RegisteredBackfillExistingIndividualGHAppInstallationTask.name +] diff --git a/apps/worker/tasks/backfill_owners_without_gh_app_installations.py b/apps/worker/tasks/backfill_owners_without_gh_app_installations.py new file mode 100644 index 0000000000..2af5d02e2b --- /dev/null +++ b/apps/worker/tasks/backfill_owners_without_gh_app_installations.py @@ -0,0 +1,216 @@ +import logging +from typing import List, Optional + +from shared.config import get_config +from sqlalchemy.orm.session import Session + +from app import celery_app +from celery_config import ( + backfill_owners_without_gh_app_installation_individual_name, + backfill_owners_without_gh_app_installations_name, +) +from database.models.core import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, + Owner, +) +from helpers.backfills import ( + add_repos_service_ids_from_provider, + maybe_set_installation_to_all_repos, +) +from services.owner import get_owner_provider_service +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class BackfillOwnersWithoutGHAppInstallations( + BaseCodecovTask, name=backfill_owners_without_gh_app_installations_name +): + def backfill_owners_with_integration_without_gh_app( + self, + db_session: Session, + owner_ids: List[int] = None, + yield_amount: int = 1000, + ): + owners_with_integration_id_without_gh_app_query = ( + db_session.query(Owner) + .outerjoin( + GithubAppInstallation, + Owner.ownerid == GithubAppInstallation.ownerid, + ) + .filter( + GithubAppInstallation.ownerid == None, # noqa: E711 + Owner.integration_id.isnot(None), + Owner.service == "github", + ) + ) + + if owner_ids: + owners_with_integration_id_without_gh_app_query = ( + owners_with_integration_id_without_gh_app_query.filter( + Owner.ownerid.in_(owner_ids) + ) + ) + + owners: List[Owner] = owners_with_integration_id_without_gh_app_query.yield_per( + yield_amount + ) + + for owner in owners: + ownerid = owner.ownerid + try: + owner_service = get_owner_provider_service(owner=owner) + + # Create new GH app installation and add all repos the gh app has access to + log.info( + "This owner has no Github App Installation", + extra=dict(ownerid=ownerid), + ) + gh_app_installation = GithubAppInstallation( + owner=owner, + installation_id=owner.integration_id, + app_id=get_config("github", "integration", "id"), + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + ) + db_session.add(gh_app_installation) + + is_selection_all = maybe_set_installation_to_all_repos( + db_session=db_session, + owner_service=owner_service, + gh_app_installation=gh_app_installation, + ) + + if not is_selection_all: + # Find and add all repos the gh app has access to + add_repos_service_ids_from_provider( + db_session=db_session, + ownerid=ownerid, + owner_service=owner_service, + gh_app_installation=gh_app_installation, + ) + log.info("Successful backfill", extra=dict(ownerid=ownerid)) + except Exception: + log.info( + "Backfill unsuccessful for this owner", extra=dict(ownerid=ownerid) + ) + + def run_impl( + self, + db_session: Session, + owner_ids: Optional[List[int]] = None, + yield_amount: int = 1000, + *args, + **kwargs, + ): + log.info( + "Starting backfill for owners without gh app task", + ) + + # Backfill owners with legacy integration + adding new gh app + owners_with_integration_id_without_gh_app_query = ( + db_session.query(Owner) + .outerjoin( + GithubAppInstallation, + Owner.ownerid == GithubAppInstallation.ownerid, + ) + .filter( + GithubAppInstallation.ownerid is None, + Owner.integration_id.isnot(None), + Owner.service == "github", + ) + ) + + if owner_ids: + owners_with_integration_id_without_gh_app_query = ( + owners_with_integration_id_without_gh_app_query.filter( + Owner.ownerid.in_(owner_ids) + ) + ) + + owners: List[Owner] = owners_with_integration_id_without_gh_app_query.yield_per( + yield_amount + ) + + for owner in owners: + self.app.tasks[ + backfill_owners_without_gh_app_installation_individual_name + ].apply_async(kwargs=dict(ownerid=owner.ownerid)) + + return {"successful": True, "reason": "backfill tasks queued"} + + +RegisterOwnersWithoutGHAppInstallations = celery_app.register_task( + BackfillOwnersWithoutGHAppInstallations() +) +backfill_owners_without_gh_app_installations = celery_app.tasks[ + RegisterOwnersWithoutGHAppInstallations.name +] + + +class BackfillOwnersWithoutGHAppInstallationIndividual( + BaseCodecovTask, name=backfill_owners_without_gh_app_installation_individual_name +): + def run_impl( + self, + db_session: Session, + ownerid: int, + *args, + **kwargs, + ): + owner = db_session.query(Owner).get(ownerid) + + log.info( + "Attempt to create GH App", + extra=dict(owner_id=ownerid, parent_id=self.request.parent_id), + ) + + try: + owner_service = get_owner_provider_service(owner=owner) + + # Create new GH app installation and add all repos the gh app has access to + log.info( + "This owner has no Github App Installation", + extra=dict(ownerid=ownerid, parent_id=self.request.parent_id), + ) + gh_app_installation = GithubAppInstallation( + owner=owner, + installation_id=owner.integration_id, + app_id=get_config("github", "integration", "id"), + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + ) + db_session.add(gh_app_installation) + + is_selection_all = maybe_set_installation_to_all_repos( + db_session=db_session, + owner_service=owner_service, + gh_app_installation=gh_app_installation, + ) + + if not is_selection_all: + # Find and add all repos the gh app has access to + add_repos_service_ids_from_provider( + db_session=db_session, + ownerid=ownerid, + owner_service=owner_service, + gh_app_installation=gh_app_installation, + ) + log.info( + "Successful backfill", + extra=dict(ownerid=ownerid, parent_id=self.request.parent_id), + ) + return {"successful": True, "reason": "backfill task finished"} + except Exception: + log.info( + "Backfill unsuccessful for this owner", + extra=dict(ownerid=ownerid, parent_id=self.request.parent_id), + ) + return {"successful": False, "reason": "backfill unsuccessful"} + + +RegisterOwnersWithoutGHAppInstallationIndividual = celery_app.register_task( + BackfillOwnersWithoutGHAppInstallationIndividual() +) +backfill_owners_without_gh_app_installation_individual = celery_app.tasks[ + RegisterOwnersWithoutGHAppInstallationIndividual.name +] diff --git a/apps/worker/tasks/backfill_test_instances.py b/apps/worker/tasks/backfill_test_instances.py new file mode 100644 index 0000000000..8c572bebe7 --- /dev/null +++ b/apps/worker/tasks/backfill_test_instances.py @@ -0,0 +1,52 @@ +import logging + +from shared.django_apps.reports.models import TestInstance + +from app import celery_app +from celery_config import backfill_test_instances_task_name +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class BackfillTestInstancesTask( + BaseCodecovTask, name=backfill_test_instances_task_name +): + def run_impl(self, *args, dry_run=True, **kwargs): + log.info( + "Updating test instances", + ) + + test_instance_filter = TestInstance.objects.select_related( + "upload__report__commit" + ).filter( + branch=None, + commitid=None, + ) + num_test_instances = test_instance_filter.count() + all_test_instances = test_instance_filter.all() + + chunk_size = 1000 + + chunks = [ + all_test_instances[i : i + chunk_size] + for i in range(0, num_test_instances, chunk_size) + ] + + for chunk in chunks: + for test_instance in chunk: + test_instance.branch = test_instance.upload.report.commit.branch + test_instance.commitid = test_instance.upload.report.commit.commitid + TestInstance.objects.bulk_update(chunk, ["branch", "commit"]) + + log.info( + "Done updating test instances", + ) + + return {"successful": True} + + +RegisteredTrialExpirationCronTask = celery_app.register_task( + BackfillTestInstancesTask() +) +backfill_test_instances_task = celery_app.tasks[BackfillTestInstancesTask.name] diff --git a/apps/worker/tasks/base.py b/apps/worker/tasks/base.py new file mode 100644 index 0000000000..08d64a9375 --- /dev/null +++ b/apps/worker/tasks/base.py @@ -0,0 +1,390 @@ +import logging +from datetime import datetime + +import sentry_sdk +from celery._state import get_current_task +from celery.exceptions import MaxRetriesExceededError, SoftTimeLimitExceeded +from celery.worker.request import Request +from shared.celery_router import route_tasks_based_on_user_plan +from shared.metrics import Counter, Histogram +from shared.torngit.base import TorngitBaseAdapter +from shared.typings.torngit import AdditionalData +from sqlalchemy.exc import ( + DataError, + IntegrityError, + InvalidRequestError, + SQLAlchemyError, +) + +from app import celery_app +from celery_task_router import _get_user_plan_from_task +from database.engine import get_db_session +from database.enums import CommitErrorTypes +from database.models.core import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + Commit, + Repository, +) +from helpers.checkpoint_logger import from_kwargs as load_checkpoints_from_kwargs +from helpers.checkpoint_logger.flows import TestResultsFlow, UploadFlow +from helpers.clock import get_seconds_to_next_hour +from helpers.exceptions import NoConfiguredAppsAvailable, RepositoryWithoutValidBotError +from helpers.log_context import LogContext, set_log_context +from helpers.save_commit_error import save_commit_error +from services.repository import get_repo_provider_service + +log = logging.getLogger("worker") + +REQUEST_TIMEOUT_COUNTER = Counter( + "worker_task_counts_timeouts", + "Number of times a task experienced any kind of timeout", + ["task"], +) +REQUEST_HARD_TIMEOUT_COUNTER = Counter( + "worker_task_counts_hard_timeouts", + "Number of times a task experienced a hard timeout", + ["task"], +) + + +class BaseCodecovRequest(Request): + @property + def metrics_prefix(self): + return f"worker.task.{self.name}" + + def on_timeout(self, soft: bool, timeout: int): + res = super().on_timeout(soft, timeout) + if not soft: + REQUEST_HARD_TIMEOUT_COUNTER.labels(task=self.name).inc() + REQUEST_TIMEOUT_COUNTER.labels(task=self.name).inc() + + if UploadFlow.has_begun(): + UploadFlow.log(UploadFlow.CELERY_TIMEOUT) + if TestResultsFlow.has_begun(): + TestResultsFlow.log(TestResultsFlow.CELERY_TIMEOUT) + + return res + + +# Task reliability metrics +TASK_RUN_COUNTER = Counter( + "worker_task_counts_runs", "Number of times this task was run", ["task"] +) +TASK_RETRY_COUNTER = Counter( + "worker_task_counts_retries", "Number of times this task was retried", ["task"] +) +TASK_SUCCESS_COUNTER = Counter( + "worker_task_counts_successes", + "Number of times this task completed without error", + ["task"], +) +TASK_FAILURE_COUNTER = Counter( + "worker_task_counts_failures", + "Number of times this task failed with an exception", + ["task"], +) + +# Task runtime metrics +TASK_FULL_RUNTIME = Histogram( + "worker_task_timers_full_runtime_seconds", + "Total runtime in seconds of this task including db commits and error handling", + ["task"], + buckets=[0.05, 0.1, 0.5, 1, 2, 5, 10, 30, 60, 120, 180, 300, 600, 900], +) +TASK_CORE_RUNTIME = Histogram( + "worker_task_timers_core_runtime_seconds", + "Runtime in seconds of this task's main logic, not including db commits or error handling", + ["task"], + buckets=[0.05, 0.1, 0.5, 1, 2, 5, 10, 30, 60, 120, 180, 300, 600, 900], +) +TASK_TIME_IN_QUEUE = Histogram( + "worker_tasks_timers_time_in_queue_seconds", + "Time in {TODO} spent waiting in the queue before being run", + ["task", "queue"], + buckets=[ + 0.01, + 0.05, + 0.1, + 0.25, + 0.5, + 0.75, + 1, + 1.5, + 2, + 3, + 5, + 7, + 10, + 15, + 20, + 30, + 45, + 60, + 90, + 120, + 180, + ], +) + + +class BaseCodecovTask(celery_app.Task): + Request = BaseCodecovRequest + + def __init_subclass__(cls, name=None): + cls.name = name + + cls.metrics_prefix = f"worker.task.{name}" + + # Task reliability metrics + cls.task_run_counter = TASK_RUN_COUNTER.labels(task=name) + cls.task_retry_counter = TASK_RETRY_COUNTER.labels(task=name) + cls.task_success_counter = TASK_SUCCESS_COUNTER.labels(task=name) + cls.task_failure_counter = TASK_FAILURE_COUNTER.labels(task=name) + + # Task runtime metrics + cls.task_full_runtime = TASK_FULL_RUNTIME.labels(task=name) + cls.task_core_runtime = TASK_CORE_RUNTIME.labels(task=name) + + @property + def hard_time_limit_task(self): + if self.request.timelimit is not None and self.request.timelimit[0] is not None: + return self.request.timelimit[0] + if self.time_limit is not None: + return self.time_limit + return self.app.conf.task_time_limit or 0 + + @sentry_sdk.trace + def apply_async(self, args=None, kwargs=None, **options): + db_session = get_db_session() + user_plan = _get_user_plan_from_task(db_session, self.name, kwargs) + route_with_extra_config = route_tasks_based_on_user_plan(self.name, user_plan) + extra_config = route_with_extra_config.get("extra_config", {}) + celery_compatible_config = { + "time_limit": extra_config.get("hard_timelimit", None), + "soft_time_limit": extra_config.get("soft_timelimit", None), + "user_plan": user_plan, + } + options = {**options, **celery_compatible_config} + + opt_headers = options.pop("headers", {}) + opt_headers = opt_headers if opt_headers is not None else {} + + # Pass current time in task headers so we can emit a metric of + # how long the task was in the queue for + current_time = datetime.now() + headers = { + **opt_headers, + "created_timestamp": current_time.isoformat(), + } + return super().apply_async(args=args, kwargs=kwargs, headers=headers, **options) + + # Called when attempting to retry the task on db error + def _retry(self, countdown=None): + if not countdown: + countdown = self.default_retry_delay + + try: + self.retry(countdown=countdown) + except MaxRetriesExceededError: + if UploadFlow.has_begun(): + UploadFlow.log(UploadFlow.UNCAUGHT_RETRY_EXCEPTION) + if TestResultsFlow.has_begun(): + TestResultsFlow.log(TestResultsFlow.UNCAUGHT_RETRY_EXCEPTION) + + def _analyse_error(self, exception: SQLAlchemyError, *args, **kwargs): + try: + import psycopg2 + + if hasattr(exception, "orig") and isinstance( + exception.orig, psycopg2.errors.DeadlockDetected + ): + log.exception( + "Deadlock while talking to database", + extra=dict(task_args=args, task_kwargs=kwargs), + exc_info=True, + ) + return + elif hasattr(exception, "orig") and isinstance( + exception.orig, psycopg2.OperationalError + ): + log.warning( + "Database seems to be unavailable", + extra=dict(task_args=args, task_kwargs=kwargs), + exc_info=True, + ) + return + except ImportError: + pass + log.exception( + "An error talking to the database occurred", + extra=dict(task_args=args, task_kwargs=kwargs), + exc_info=True, + ) + + def _emit_queue_metrics(self): + created_timestamp = self.request.get("created_timestamp", None) + if created_timestamp: + enqueued_time = datetime.fromisoformat(created_timestamp) + now = datetime.now() + delta = now - enqueued_time + + queue_name = self.request.get("delivery_info", {}).get("routing_key", None) + time_in_queue_timer = TASK_TIME_IN_QUEUE.labels( + task=self.name, queue=queue_name + ) # TODO is None a valid label value + time_in_queue_timer.observe(delta.total_seconds()) + + def run(self, *args, **kwargs): + with self.task_full_runtime.time(): # Timer isn't tested + db_session = get_db_session() + + log_context = LogContext( + repo_id=kwargs.get("repoid") or kwargs.get("repo_id"), + owner_id=kwargs.get("ownerid"), + commit_sha=kwargs.get("commitid") or kwargs.get("commit_id"), + ) + + task = get_current_task() + if task and task.request: + log_context.task_name = task.name + log_context.task_id = task.request.id + + log_context.populate_from_sqlalchemy(db_session) + set_log_context(log_context) + load_checkpoints_from_kwargs([UploadFlow, TestResultsFlow], kwargs) + + self.task_run_counter.inc() + self._emit_queue_metrics() + + try: + with self.task_core_runtime.time(): # Timer isn't tested + return self.run_impl(db_session, *args, **kwargs) + except (DataError, IntegrityError): + log.exception( + "Errors related to the constraints of database happened", + extra=dict(task_args=args, task_kwargs=kwargs), + ) + db_session.rollback() + self._retry() + except SQLAlchemyError as ex: + self._analyse_error(ex, args, kwargs) + db_session.rollback() + self._retry() + except MaxRetriesExceededError as ex: + if UploadFlow.has_begun(): + UploadFlow.log(UploadFlow.UNCAUGHT_RETRY_EXCEPTION) + if TestResultsFlow.has_begun(): + TestResultsFlow.log(TestResultsFlow.UNCAUGHT_RETRY_EXCEPTION) + finally: + self.wrap_up_dbsession(db_session) + + def wrap_up_dbsession(self, db_session): + """ + Wraps up dbsession, commita what is relevant and closes the session + + This function deals with the very corner case of when a `SoftTimeLimitExceeded` + is raised during the execution of `db_session.commit()`. When it happens, + the dbsession gets into a bad state, which disallows further operations in it. + + And because we reuse dbsessions, this would mean future tasks happening inside the + same process would also lose access to db. + + So we need to do two ugly exception-catching: + 1) For if `SoftTimeLimitExceeded` was raised while commiting + 2) For if the exception left `db_session` in an unusable state + """ + try: + db_session.commit() + db_session.close() + except SoftTimeLimitExceeded: + log.warning( + "We had an issue where a timeout happened directly during the DB commit", + exc_info=True, + ) + try: + db_session.commit() + db_session.close() + except InvalidRequestError: + log.warning( + "DB session cannot be operated on any longer. Closing it and removing it", + exc_info=True, + ) + get_db_session.remove() + except InvalidRequestError: + log.warning( + "DB session cannot be operated on any longer. Closing it and removing it", + exc_info=True, + ) + get_db_session.remove() + + def on_retry(self, exc, task_id, args, kwargs, einfo): + res = super().on_retry(exc, task_id, args, kwargs, einfo) + self.task_retry_counter.inc() + return res + + def on_success(self, retval, task_id, args, kwargs): + res = super().on_success(retval, task_id, args, kwargs) + self.task_success_counter.inc() + return res + + def on_failure(self, exc, task_id, args, kwargs, einfo): + """ + Includes SoftTimeLimitExceeded, for example + """ + res = super().on_failure(exc, task_id, args, kwargs, einfo) + self.task_failure_counter.inc() + + if UploadFlow.has_begun(): + UploadFlow.log(UploadFlow.CELERY_FAILURE) + if TestResultsFlow.has_begun(): + TestResultsFlow.log(TestResultsFlow.CELERY_FAILURE) + + return res + + def get_repo_provider_service( + self, + repository: Repository, + installation_name_to_use: str = GITHUB_APP_INSTALLATION_DEFAULT_NAME, + additional_data: AdditionalData = None, + commit: Commit = None, + ) -> TorngitBaseAdapter | None: + try: + return get_repo_provider_service( + repository, installation_name_to_use, additional_data + ) + except RepositoryWithoutValidBotError: + save_commit_error( + commit, + error_code=CommitErrorTypes.REPO_BOT_INVALID.value, + error_params=dict(repoid=repository.repoid), + ) + log.warning( + "Unable to reach git provider because repo doesn't have a valid bot" + ) + except NoConfiguredAppsAvailable as exp: + if exp.rate_limited_count > 0: + # There's at least 1 app that we can use to communicate with GitHub, + # but this app happens to be rate limited now. We try again later. + # Min wait time of 1 minute + retry_delay_seconds = max(60, get_seconds_to_next_hour()) + log.warning( + "Unable to get repo provider service due to rate limits. Retrying again later.", + extra=dict( + apps_available=exp.apps_count, + apps_rate_limited=exp.rate_limited_count, + apps_suspended=exp.suspended_count, + countdown_seconds=retry_delay_seconds, + ), + ) + self._retry(countdown=retry_delay_seconds) + else: + log.warning( + "Unable to get repo provider service. Apps appear to be suspended.", + extra=dict( + apps_available=exp.apps_count, + apps_rate_limited=exp.rate_limited_count, + apps_suspended=exp.suspended_count, + ), + ) + except Exception as e: + log.exception("Uncaught exception when trying to get repository service") diff --git a/apps/worker/tasks/brolly_stats_rollup.py b/apps/worker/tasks/brolly_stats_rollup.py new file mode 100644 index 0000000000..7778aee605 --- /dev/null +++ b/apps/worker/tasks/brolly_stats_rollup.py @@ -0,0 +1,125 @@ +import datetime +import json +import logging + +import httpx +from shared.celery_config import brolly_stats_rollup_task_name +from shared.config import get_config + +from app import celery_app +from database.models import Commit, Constants, Repository, Upload, User +from tasks.crontasks import CodecovCronTask + +log = logging.getLogger(__name__) + +DEFAULT_BROLLY_ENDPOINT = "https://codecov.io/self-hosted/telemetry" + + +class BrollyStatsRollupTask(CodecovCronTask, name=brolly_stats_rollup_task_name): + """ + By default, this cron task collects anonymous statistics about the Codecov instance + and submits them to Codecov to give us insight into the size of our self-hosted, + open-source userbase. + + Installations can configure the behavior in `codecov.yml`: + - `setup.telemetry.enabled` (default True): Control whether stats are collected at all. + - `setup.telemetry.endpoint_override`: Control where stats are sent. + - `setup.telemetry.anonymous` (default True): Request that brolly not save identifiable + information such as the IP address of the installation that submitted the stats. + - `setup.telemetry.admin_email`: Contact information for the installation owner. + """ + + @classmethod + def get_min_seconds_interval_between_executions(cls): + return 72000 # 20h + + def _get_install_id(self, db_session): + """ + Anonymous, randomly-generated UUID created during DB setup. + """ + return db_session.query(Constants).get("install_id").value + + def _get_users_count(self, db_session): + return db_session.query(User.id_).count() + + def _get_repos_count(self, db_session): + return db_session.query(Repository.repoid).count() + + def _get_commits_count(self, db_session): + return db_session.query(Commit.id_).count() + + def _get_uploads_count_last_24h(self, db_session): + time_24h_ago = datetime.datetime.now() - datetime.timedelta(days=1) + return db_session.query(Upload).filter(Upload.created_at > time_24h_ago).count() + + def _get_anonymous(self): + """ + If this is true, brolly will refrain from logging things like the IP address used to + submit the stats. + """ + return get_config("setup", "telemetry", "anonymous", default=True) + + def _get_version(self, db_session): + return db_session.query(Constants).get("version").value + + def _get_endpoint_url(self): + """ + Where to send stats. + """ + return get_config( + "setup", "telemetry", "endpoint_override", default=DEFAULT_BROLLY_ENDPOINT + ) + + def _get_admin_email(self): + """ + Contact information for the owner of the installation. + If not populated, it will be omitted from the payload. + """ + return get_config("setup", "telemetry", "admin_email", default=None) + + def run_cron_task(self, db_session, *args, **kwargs): + # We shouldn't even schedule this task if it's not enabled, but + # let's double-check that we're supposed to collect stats. + if not get_config("setup", "telemetry", "enabled", default=True): + return dict(uploaded=False, reason="telemetry disabled in codecov.yml") + + payload = dict( + install_id=self._get_install_id(db_session), + users=self._get_users_count(db_session), + repos=self._get_repos_count(db_session), + commits=self._get_commits_count(db_session), + uploads_24h=self._get_uploads_count_last_24h(db_session), + version=self._get_version(db_session), + anonymous=self._get_anonymous(), + ) + + admin_email = self._get_admin_email() + if admin_email: + payload["admin_email"] = admin_email + + # Perform the upload + brolly_endpoint = self._get_endpoint_url() + headers = { + "Accept": "application/json", + "Content-Type": "application/json", + } + res = httpx.Client().post( + url=brolly_endpoint, + content=json.dumps(payload), + headers=headers, + ) + + match res.status_code: + case httpx.codes.OK: + log.info( + "Successfully uploaded stats to brolly", extra=dict(response=res) + ) + case _: + log.error("Failed to upload stats to brolly", extra=dict(response=res)) + return dict(uploaded=False, payload=payload) + + return dict(uploaded=True, payload=payload) + + +RegisteredBrollyStatsRollupTask = celery_app.register_task(BrollyStatsRollupTask()) +brolly_stats_rollup_task = celery_app.tasks[RegisteredBrollyStatsRollupTask.name] diff --git a/apps/worker/tasks/bundle_analysis_notify.py b/apps/worker/tasks/bundle_analysis_notify.py new file mode 100644 index 0000000000..d57f5cd0b0 --- /dev/null +++ b/apps/worker/tasks/bundle_analysis_notify.py @@ -0,0 +1,133 @@ +import logging +from typing import Any, Dict + +import sentry_sdk +from shared.yaml import UserYaml + +from app import celery_app +from database.enums import ReportType +from database.models import Commit +from helpers.github_installation import get_installation_name_for_owner_for_task +from services.bundle_analysis.notify import BundleAnalysisNotifyService +from services.bundle_analysis.notify.types import NotificationSuccess +from services.lock_manager import LockManager, LockRetry, LockType +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + +bundle_analysis_notify_task_name = "app.tasks.bundle_analysis.BundleAnalysisNotify" + + +class BundleAnalysisNotifyTask(BaseCodecovTask, name=bundle_analysis_notify_task_name): + def run_impl( + self, + db_session, + # Celery `chain` injects this argument - it's the returned result + # from the prior task in the chain + previous_result: Dict[str, Any], + *, + repoid: int, + commitid: str, + commit_yaml: dict, + **kwargs, + ): + repoid = int(repoid) + commit_yaml = UserYaml.from_dict(commit_yaml) + + log.info( + "Starting bundle analysis notify", + extra=dict( + repoid=repoid, + commit=commitid, + commit_yaml=commit_yaml, + ), + ) + + lock_manager = LockManager( + repoid=repoid, + commitid=commitid, + report_type=ReportType.BUNDLE_ANALYSIS, + ) + + try: + with lock_manager.locked( + LockType.BUNDLE_ANALYSIS_NOTIFY, + retry_num=self.request.retries, + ): + return self.process_impl_within_lock( + db_session=db_session, + repoid=repoid, + commitid=commitid, + commit_yaml=commit_yaml, + previous_result=previous_result, + **kwargs, + ) + except LockRetry as retry: + self.retry(max_retries=5, countdown=retry.countdown) + + @sentry_sdk.trace + def process_impl_within_lock( + self, + *, + db_session, + repoid: int, + commitid: str, + commit_yaml: UserYaml, + previous_result: Dict[str, Any], + **kwargs, + ): + log.info( + "Running bundle analysis notify", + extra=dict( + repoid=repoid, + commit=commitid, + commit_yaml=commit_yaml, + parent_task=self.request.parent_id, + ), + ) + + commit = ( + db_session.query(Commit).filter_by(repoid=repoid, commitid=commitid).first() + ) + assert commit, "commit not found" + + # these are the task results from prior processor tasks in the chain + # (they get accumulated as we execute each task in succession) + processing_results = previous_result.get("results", []) + + if all((result["error"] is not None for result in processing_results)): + # every processor errored, nothing to notify on + return { + "notify_attempted": False, + "notify_succeeded": NotificationSuccess.ALL_ERRORED, + } + + installation_name_to_use = get_installation_name_for_owner_for_task( + self.name, commit.repository.owner + ) + notifier = BundleAnalysisNotifyService( + commit, commit_yaml, gh_app_installation_name=installation_name_to_use + ) + result = notifier.notify() + + log.info( + "Finished bundle analysis notify", + extra=dict( + repoid=repoid, + commit=commitid, + commit_yaml=commit_yaml, + parent_task=self.request.parent_id, + result=result, + ), + ) + + return { + "notify_attempted": True, + "notify_succeeded": result.to_NotificationSuccess(), + } + + +RegisteredBundleAnalysisNotifyTask = celery_app.register_task( + BundleAnalysisNotifyTask() +) +bundle_analysis_notify_task = celery_app.tasks[RegisteredBundleAnalysisNotifyTask.name] diff --git a/apps/worker/tasks/bundle_analysis_processor.py b/apps/worker/tasks/bundle_analysis_processor.py new file mode 100644 index 0000000000..de175d165c --- /dev/null +++ b/apps/worker/tasks/bundle_analysis_processor.py @@ -0,0 +1,254 @@ +import logging +from typing import Any + +from celery.exceptions import CeleryError, SoftTimeLimitExceeded +from shared.reports.enums import UploadState +from shared.yaml import UserYaml + +from app import celery_app +from database.enums import ReportType +from database.models import Commit, CommitReport, Upload +from services.bundle_analysis.report import ( + BundleAnalysisReportService, + ProcessingResult, +) +from services.lock_manager import LockManager, LockRetry, LockType +from services.processing.types import UploadArguments +from tasks.base import BaseCodecovTask +from tasks.bundle_analysis_save_measurements import ( + bundle_analysis_save_measurements_task_name, +) + +log = logging.getLogger(__name__) + +bundle_analysis_processor_task_name = ( + "app.tasks.bundle_analysis.BundleAnalysisProcessor" +) + + +class BundleAnalysisProcessorTask( + BaseCodecovTask, name=bundle_analysis_processor_task_name +): + max_retries = 5 + + def run_impl( + self, + db_session, + # Celery `chain` injects this argument - it's the returned result + # from the prior task in the chain + previous_result: dict[str, Any], + *args, + repoid: int, + commitid: str, + commit_yaml: dict, + params: UploadArguments, + **kwargs, + ): + repoid = int(repoid) + + log.info( + "Starting bundle analysis processor", + extra=dict( + repoid=repoid, + commit=commitid, + commit_yaml=commit_yaml, + params=params, + ), + ) + + lock_manager = LockManager( + repoid=repoid, + commitid=commitid, + report_type=ReportType.BUNDLE_ANALYSIS, + ) + + try: + with lock_manager.locked( + LockType.BUNDLE_ANALYSIS_PROCESSING, + retry_num=self.request.retries, + ): + return self.process_impl_within_lock( + db_session, + repoid, + commitid, + UserYaml.from_dict(commit_yaml), + params, + previous_result, + ) + except LockRetry as retry: + self.retry(countdown=retry.countdown) + + def process_impl_within_lock( + self, + db_session, + repoid: int, + commitid: str, + commit_yaml: UserYaml, + params: UploadArguments, + previous_result: dict[str, Any], + ): + log.info( + "Running bundle analysis processor", + extra=dict( + commit_yaml=commit_yaml, + params=params, + parent_task=self.request.parent_id, + ), + ) + + commit = ( + db_session.query(Commit).filter_by(repoid=repoid, commitid=commitid).first() + ) + assert commit, "commit not found" + + report_service = BundleAnalysisReportService(commit_yaml) + + # these are the task results from prior processor tasks in the chain + # (they get accumulated as we execute each task in succession) + processing_results = previous_result.get("results", []) + + # these are populated in the upload task + # unless when this task is called on a non-BA upload then we have to create an empty upload + upload_id, carriedforward = params.get("upload_id"), False + if upload_id is not None: + upload = db_session.query(Upload).filter_by(id_=upload_id).first() + else: + # This processor task handles caching for reports. When the 'upload' parameter is missing, + # it indicates this task was triggered by a non-BA upload. + # + # To prevent redundant caching of the same parent report: + # 1. We first check if a BA report already exists for this commit + # 2. We then verify there are uploads associated with it that aren't in an error state + # + # If both conditions are met, we can exit the task early since the caching was likely + # already handled. Otherwise, we need to: + # 1. Create a new BA report and upload + # 2. Proceed with caching data from the parent report + commit_report = ( + db_session.query(CommitReport) + .filter_by( + commit_id=commit.id, + report_type=ReportType.BUNDLE_ANALYSIS.value, + ) + .first() + ) + if commit_report: + upload_states = [upload.state for upload in commit_report.uploads] + if upload_states and any( + upload_state != "error" for upload_state in upload_states + ): + log.info( + "Bundle analysis report already exists for commit, skipping carryforward", + extra=dict( + repoid=commit.repoid, + commit=commit.commitid, + ), + ) + return processing_results + else: + # If the commit report does not exist, we will create a new one + commit_report = report_service.initialize_and_save_report(commit) + + upload = report_service.create_report_upload({"url": ""}, commit_report) + carriedforward = True + + assert upload is not None + + # Override base commit of comparisons with a custom commit SHA if applicable + compare_sha = params.get("bundle_analysis_compare_sha") + + try: + log.info( + "Processing bundle analysis upload", + extra=dict( + repoid=repoid, + commit=commitid, + commit_yaml=commit_yaml, + params=params, + upload_id=upload.id_, + parent_task=self.request.parent_id, + compare_sha=compare_sha, + ), + ) + assert params.get("commit") == commit.commitid + + result: ProcessingResult = report_service.process_upload( + commit, upload, compare_sha + ) + if ( + result.error + and result.error.is_retryable + and self.request.retries < self.max_retries + ): + log.warn( + "Attempting to retry bundle analysis upload", + extra=dict( + repoid=repoid, + commit=commitid, + commit_yaml=commit_yaml, + params=params, + result=result.as_dict(), + ), + ) + self.retry(countdown=30 * (2**self.request.retries)) + result.update_upload(carriedforward=carriedforward) + + processing_results.append(result.as_dict()) + except (CeleryError, SoftTimeLimitExceeded): + # This generally happens when the task needs to be retried because we attempt to access + # the upload before it is saved to GCS. Anecdotally, it takes around 30s for it to be + # saved, but if the BA processor runs before that we will error and need to retry. + raise + except Exception: + log.exception( + "Unable to process bundle analysis upload", + extra=dict( + repoid=repoid, + commit=commitid, + commit_yaml=commit_yaml, + params=params, + upload_id=upload.id_, + parent_task=self.request.parent_id, + ), + ) + upload.state_id = UploadState.ERROR.db_id + upload.state = "error" + raise + finally: + if result.bundle_report: + result.bundle_report.cleanup() + if result.previous_bundle_report: + result.previous_bundle_report.cleanup() + + # Create task to save bundle measurements + self.app.tasks[bundle_analysis_save_measurements_task_name].apply_async( + kwargs=dict( + commitid=commit.commitid, + repoid=commit.repoid, + uploadid=upload.id_, + commit_yaml=commit_yaml.to_dict(), + previous_result=processing_results, + ) + ) + + log.info( + "Finished bundle analysis processor", + extra=dict( + repoid=repoid, + commit=commitid, + commit_yaml=commit_yaml, + params=params, + results=processing_results, + parent_task=self.request.parent_id, + ), + ) + + return {"results": processing_results} + + +RegisteredBundleAnalysisProcessorTask = celery_app.register_task( + BundleAnalysisProcessorTask() +) +bundle_analysis_processor_task = celery_app.tasks[ + RegisteredBundleAnalysisProcessorTask.name +] diff --git a/apps/worker/tasks/bundle_analysis_save_measurements.py b/apps/worker/tasks/bundle_analysis_save_measurements.py new file mode 100644 index 0000000000..833fd7575d --- /dev/null +++ b/apps/worker/tasks/bundle_analysis_save_measurements.py @@ -0,0 +1,99 @@ +import logging +from typing import Any + +from shared.yaml import UserYaml +from sqlalchemy.orm.session import Session + +from app import celery_app +from database.models import Commit, Upload +from services.bundle_analysis.report import ( + BundleAnalysisReportService, + ProcessingResult, +) +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + +bundle_analysis_save_measurements_task_name = ( + "app.tasks.bundle_analysis.BundleAnalysisSaveMeasurements" +) + + +class BundleAnalysisSaveMeasurementsTask( + BaseCodecovTask, name=bundle_analysis_save_measurements_task_name +): + def run_impl( + self, + db_session: Session, + repoid: int, + commitid: str, + uploadid: int, + commit_yaml: dict, + previous_result: Any, + ): + repoid = int(repoid) + + log.info( + "Starting bundle analysis save measurements", + extra=dict( + repoid=repoid, + commit=commitid, + previous_result=previous_result, + ), + ) + + commit = ( + db_session.query(Commit).filter_by(repoid=repoid, commitid=commitid).first() + ) + assert commit, "commit not found" + + upload = db_session.query(Upload).filter_by(id_=uploadid).first() + if upload is None: + log.info( + "Skipping bundle analysis save measurements - cached bundle", + extra=dict( + repoid=repoid, + commit=commitid, + uploadid=uploadid, + success=True, + ), + ) + return {"successful": True} + + save_measurements = True + + if all((result["error"] is not None for result in previous_result)): + save_measurements = False + + bundle_name = None + for result in previous_result: + bundle_name = result.get("bundle_name") + + if save_measurements: + report_service = BundleAnalysisReportService( + UserYaml.from_dict(commit_yaml) + ) + result: ProcessingResult = report_service.save_measurements( + commit, upload, bundle_name + ) + save_measurements = result.error is None + + log.info( + "Finished bundle analysis save measurements", + extra=dict( + repoid=repoid, + commit=commitid, + uploadid=uploadid, + success=save_measurements, + ), + ) + + return {"successful": save_measurements} + + +RegisteredBundleAnalysisSaveMeasurementsTask = celery_app.register_task( + BundleAnalysisSaveMeasurementsTask() +) +bundle_analysis_save_measurements_task = celery_app.tasks[ + RegisteredBundleAnalysisSaveMeasurementsTask.name +] diff --git a/apps/worker/tasks/cache_rollup_cron_task.py b/apps/worker/tasks/cache_rollup_cron_task.py new file mode 100644 index 0000000000..507193801c --- /dev/null +++ b/apps/worker/tasks/cache_rollup_cron_task.py @@ -0,0 +1,35 @@ +import datetime as dt +import logging + +from shared.django_apps.reports.models import LastCacheRollupDate +from sqlalchemy.orm import Session + +from app import celery_app +from celery_config import cache_rollup_cron_task_name +from tasks.cache_test_rollups import cache_test_rollups_task_name +from tasks.crontasks import CodecovCronTask + +log = logging.getLogger(__name__) + + +class CacheRollupTask(CodecovCronTask, name=cache_rollup_cron_task_name): + def run_cron_task(self, _db_session: Session, *args, **kwargs): + # get repos that have not uploaded test results in the last 24 hours + out_of_date_repo_branches = LastCacheRollupDate.objects.all() + + for repo_branch in out_of_date_repo_branches: + repo = repo_branch.repository + branch = repo_branch.branch + + if repo_branch.last_rollup_date < (dt.date.today() - dt.timedelta(days=30)): + repo_branch.delete() + else: + self.app.tasks[cache_test_rollups_task_name].s( + repoid=repo.repoid, + branch=branch, + update_date=False, + ).apply_async() + + +RegisteredCacheRollupTask = celery_app.register_task(CacheRollupTask()) +cache_rollup_task = celery_app.tasks[RegisteredCacheRollupTask.name] diff --git a/apps/worker/tasks/cache_test_rollups.py b/apps/worker/tasks/cache_test_rollups.py new file mode 100644 index 0000000000..f194f5e57f --- /dev/null +++ b/apps/worker/tasks/cache_test_rollups.py @@ -0,0 +1,215 @@ +import datetime as dt +from typing import Literal + +import polars as pl +import shared.storage +from django.db import connections +from redis.exceptions import LockError +from shared.celery_config import cache_test_rollups_task_name +from shared.config import get_config +from shared.django_apps.reports.models import LastCacheRollupDate +from shared.helpers.redis import get_redis_connection + +from app import celery_app +from django_scaffold import settings +from services.test_analytics.ta_cache_rollups import cache_rollups +from services.test_analytics.ta_metrics import ( + read_rollups_from_db_summary, + rollup_size_summary, +) +from tasks.base import BaseCodecovTask + +# Reminder: `a BETWEEN x AND y` is equivalent to `a >= x AND a <= y` +# Since we are working with calendar days, using a range of `0..0` gives us *today*, +# and a range of `1..1` gives use *yesterday*. +BASE_SUBQUERY = """ +SELECT * +FROM reports_dailytestrollups +WHERE repoid = %(repoid)s + AND branch = %(branch)s + AND date BETWEEN + (CURRENT_DATE - INTERVAL %(interval_start)s) AND + (CURRENT_DATE - INTERVAL %(interval_end)s) +""" + +TEST_AGGREGATION_SUBQUERY = """ +SELECT test_id, + CASE + WHEN SUM(pass_count) + SUM(fail_count) = 0 THEN 0 + ELSE SUM(fail_count)::float / (SUM(pass_count) + SUM(fail_count)) + END AS failure_rate, + CASE + WHEN SUM(pass_count) + SUM(fail_count) = 0 THEN 0 + ELSE SUM(flaky_fail_count)::float / (SUM(pass_count) + SUM(fail_count)) + END AS flake_rate, + MAX(latest_run) AS updated_at, + AVG(avg_duration_seconds) AS avg_duration, + SUM(fail_count) AS total_fail_count, + SUM(flaky_fail_count) AS total_flaky_fail_count, + SUM(pass_count) AS total_pass_count, + SUM(skip_count) AS total_skip_count +FROM base_cte +GROUP BY test_id +""" + +COMMITS_FAILED_SUBQUERY = """ +SELECT test_id, + array_length((array_agg(DISTINCT unnested_cwf)), 1) AS failed_commits_count +FROM + (SELECT test_id, + commits_where_fail AS cwf + FROM base_cte + WHERE array_length(commits_where_fail, 1) > 0) AS tests_with_commits_that_failed, + unnest(cwf) AS unnested_cwf +GROUP BY test_id +""" + +LAST_DURATION_SUBQUERY = """ +SELECT base_cte.test_id, + last_duration_seconds +FROM base_cte +JOIN + (SELECT test_id, + max(created_at) AS created_at + FROM base_cte + GROUP BY test_id) AS latest_rollups ON base_cte.created_at = latest_rollups.created_at +AND base_cte.test_id = latest_rollups.test_id +""" + +TEST_FLAGS_SUBQUERY = """ +SELECT test_id, + array_agg(DISTINCT flag_name) AS flags +FROM reports_test_results_flag_bridge tfb +JOIN reports_test rt ON rt.id = tfb.test_id +JOIN reports_repositoryflag rr ON tfb.flag_id = rr.id +WHERE rt.repoid = %(repoid)s +GROUP BY test_id +""" + +ROLLUP_QUERY = f""" +WITH + base_cte AS ({BASE_SUBQUERY}), + failure_rate_cte AS ({TEST_AGGREGATION_SUBQUERY}), + commits_where_fail_cte AS ({COMMITS_FAILED_SUBQUERY}), + last_duration_cte AS ({LAST_DURATION_SUBQUERY}), + flags_cte AS ({TEST_FLAGS_SUBQUERY}) + +SELECT COALESCE(rt.computed_name, rt.name) AS name, + rt.testsuite, + flags_cte.flags, + results.* +FROM + (SELECT failure_rate_cte.*, + coalesce(commits_where_fail_cte.failed_commits_count, 0) AS commits_where_fail, + last_duration_cte.last_duration_seconds AS last_duration + FROM failure_rate_cte + FULL OUTER JOIN commits_where_fail_cte USING (test_id) + FULL OUTER JOIN last_duration_cte USING (test_id)) AS results +JOIN reports_test rt ON results.test_id = rt.id +LEFT JOIN flags_cte USING (test_id) +""" + + +class CacheTestRollupsTask(BaseCodecovTask, name=cache_test_rollups_task_name): + def run_impl( + self, + _db_session, + branch: str, + repo_id: int, + update_date: bool = True, + impl_type: Literal["old", "new", "both"] = "old", + **kwargs, + ): + redis_conn = get_redis_connection() + try: + with redis_conn.lock( + f"rollups:{repo_id}:{branch}", timeout=300, blocking_timeout=2 + ): + if impl_type == "new" or impl_type == "both": + cache_rollups(repo_id, branch) + cache_rollups(repo_id, None) + if impl_type == "new": + return {"success": True} + + self.run_impl_within_lock(repo_id, branch) + + if update_date: + LastCacheRollupDate.objects.update_or_create( + repository_id=repo_id, + branch=branch, + defaults={"last_rollup_date": dt.date.today()}, + ) + return {"success": True} + except LockError: + return {"in_progress": True} + + def run_impl_within_lock(self, repo_id: int, branch: str): + storage_service = shared.storage.get_appropriate_storage_service(repo_id) + + if get_config("setup", "database", "read_replica_enabled", default=False): + connection = connections["default_read"] + else: + connection = connections["default"] + + with connection.cursor() as cursor: + for interval_start, interval_end in [ + # NOTE: working with calendar days and intervals, + # `(CURRENT_DATE - INTERVAL '1 days')` means *yesterday*, + # and `2..1` matches *the day before yesterday*. + (1, None), + (2, 1), + (7, None), + (14, 7), + (30, None), + (60, 30), + ]: + query_params = { + "repoid": repo_id, # query is expecting repoid + "branch": branch, + "interval_start": f"{interval_start} days", + # SQL `BETWEEN` syntax is equivalent to `<= end`, with an inclusive end date, + # thats why we do a `+1` here: + "interval_end": f"{interval_end + 1 if interval_end else 0} days", + } + + with read_rollups_from_db_summary.labels("old").time(): + cursor.execute(ROLLUP_QUERY, query_params) + aggregation_of_test_results = cursor.fetchall() + + df = pl.DataFrame( + aggregation_of_test_results, + [ + "name", + "testsuite", + ("flags", pl.List(pl.String)), + "test_id", + "failure_rate", + "flake_rate", + ("updated_at", pl.Datetime(time_zone=dt.UTC)), + "avg_duration", + "total_fail_count", + "total_flaky_fail_count", + "total_pass_count", + "total_skip_count", + "commits_where_fail", + "last_duration", + ], + orient="row", + ) + + serialized_table = df.write_ipc(None) + serialized_table.seek(0) # avoids Stream must be at beginning errors + + storage_key = ( + f"test_results/rollups/{repo_id}/{branch}/{interval_start}" + if interval_end is None + else f"test_results/rollups/{repo_id}/{branch}/{interval_start}_{interval_end}" + ) + storage_service.write_file( + settings.GCS_BUCKET_NAME, storage_key, serialized_table + ) + rollup_size_summary.labels("old").observe(serialized_table.tell()) + + +RegisteredCacheTestRollupTask = celery_app.register_task(CacheTestRollupsTask()) +cache_test_rollups_task = celery_app.tasks[RegisteredCacheTestRollupTask.name] diff --git a/apps/worker/tasks/cache_test_rollups_redis.py b/apps/worker/tasks/cache_test_rollups_redis.py new file mode 100644 index 0000000000..132109413f --- /dev/null +++ b/apps/worker/tasks/cache_test_rollups_redis.py @@ -0,0 +1,70 @@ +import datetime as dt + +import shared.storage +from redis.exceptions import LockError +from shared.celery_config import cache_test_rollups_redis_task_name +from shared.helpers.redis import get_redis_connection +from shared.storage.exceptions import FileNotInStorageError + +from app import celery_app +from django_scaffold import settings +from tasks.base import BaseCodecovTask + + +class CacheTestRollupsRedisTask( + BaseCodecovTask, name=cache_test_rollups_redis_task_name +): + def run_impl( + self, _db_session, repoid: int, branch: str, **kwargs + ) -> dict[str, bool]: + redis_conn = get_redis_connection() + try: + with redis_conn.lock( + f"rollups:{repoid}:{branch}", timeout=300, blocking_timeout=2 + ): + self.run_impl_within_lock(repoid, branch) + return {"success": True} + except LockError: + return {"in_progress": True} + + def run_impl_within_lock(self, repoid, branch) -> None: + storage_service = shared.storage.get_appropriate_storage_service(repoid) + redis_conn = get_redis_connection() + + for interval_start, interval_end in [ + (1, None), + (7, None), + (30, None), + (2, 1), + (14, 7), + (60, 30), + ]: + storage_key = ( + f"test_results/rollups/{repoid}/{branch}/{interval_start}" + if interval_end is None + else f"test_results/rollups/{repoid}/{branch}/{interval_start}_{interval_end}" + ) + try: + file: bytes = storage_service.read_file( + settings.GCS_BUCKET_NAME, storage_key + ) + except FileNotInStorageError: + pass + + redis_key = ( + f"ta_roll:{repoid}:{branch}:{interval_start}" + if interval_end is None + else f"ta_roll:{repoid}:{branch}:{interval_start}_{interval_end}" + ) + + redis_conn.set(redis_key, file, ex=dt.timedelta(hours=1).seconds) + + return + + +RegisteredCacheTestRollupsRedisTask = celery_app.register_task( + CacheTestRollupsRedisTask() +) +cache_test_rollups_redis_task = celery_app.tasks[ + RegisteredCacheTestRollupsRedisTask.name +] diff --git a/apps/worker/tasks/commit_update.py b/apps/worker/tasks/commit_update.py new file mode 100644 index 0000000000..71b343373e --- /dev/null +++ b/apps/worker/tasks/commit_update.py @@ -0,0 +1,147 @@ +import datetime as dt +import logging + +from shared.celery_config import commit_update_task_name +from shared.torngit.exceptions import TorngitClientError, TorngitRepoNotFoundError + +from app import celery_app +from database.models import Branch, Commit, Pull +from helpers.exceptions import RepositoryWithoutValidBotError +from helpers.github_installation import get_installation_name_for_owner_for_task +from services.repository import ( + get_repo_provider_service, + possibly_update_commit_from_provider_info, +) +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class CommitUpdateTask(BaseCodecovTask, name=commit_update_task_name): + def run_impl( + self, + db_session, + repoid: int, + commitid: str, + **kwargs, + ): + commit = None + commits = db_session.query(Commit).filter( + Commit.repoid == repoid, Commit.commitid == commitid + ) + commit = commits.first() + assert commit, "Commit not found in database." + repository = commit.repository + repository_service = None + was_updated = False + try: + installation_name_to_use = get_installation_name_for_owner_for_task( + self.name, repository.owner + ) + repository_service = get_repo_provider_service( + repository, installation_name_to_use=installation_name_to_use + ) + was_updated = possibly_update_commit_from_provider_info( + commit, repository_service + ) + + if isinstance(commit.timestamp, str): + commit.timestamp = dt.datetime.fromisoformat(commit.timestamp).replace( + tzinfo=None + ) + + if commit.pullid is not None: + # upsert pull + pull = ( + db_session.query(Pull) + .filter(Pull.repoid == repoid, Pull.pullid == commit.pullid) + .first() + ) + + if pull is None: + pull = Pull( + repoid=repoid, + pullid=commit.pullid, + author_id=commit.author_id, + head=commit.commitid, + ) + db_session.add(pull) + else: + previous_pull_head = ( + db_session.query(Commit) + .filter(Commit.repoid == repoid, Commit.commitid == pull.head) + .first() + ) + if ( + previous_pull_head is None + or previous_pull_head.deleted == True + or previous_pull_head.timestamp < commit.timestamp + ): + pull.head = commit.commitid + + db_session.flush() + + if commit.branch is not None: + # upsert branch + branch = ( + db_session.query(Branch) + .filter(Branch.repoid == repoid, Branch.branch == commit.branch) + .first() + ) + + if branch is None: + branch = Branch( + repoid=repoid, + branch=commit.branch, + head=commit.commitid, + authors=[commit.author_id], + ) + db_session.add(branch) + else: + if commit.author_id is not None: + if branch.authors is None: + branch.authors = [commit.author_id] + elif commit.author_id not in branch.authors: + branch.authors.append(commit.author_id) + + previous_branch_head = ( + db_session.query(Commit) + .filter(Commit.repoid == repoid, Commit.commitid == branch.head) + .first() + ) + + if ( + previous_branch_head is None + or previous_branch_head.deleted == True + or previous_branch_head.timestamp < commit.timestamp + ): + branch.head = commit.commitid + + db_session.flush() + + except RepositoryWithoutValidBotError: + log.warning( + "Unable to reach git provider because repo doesn't have a valid bot", + extra=dict(repoid=repoid, commit=commitid), + ) + except TorngitRepoNotFoundError: + log.warning( + "Unable to reach git provider because this specific bot/integration can't see that repository", + extra=dict(repoid=repoid, commit=commitid), + ) + except TorngitClientError: + log.warning( + "Unable to reach git provider because there was a 4xx error", + extra=dict(repoid=repoid, commit=commitid), + exc_info=True, + ) + if was_updated: + log.info( + "Commit updated successfully", + extra=dict(commitid=commitid, repoid=repoid), + ) + return {"was_updated": was_updated} + + +RegisteredCommitUpdateTask = celery_app.register_task(CommitUpdateTask()) +commit_update_task = celery_app.tasks[RegisteredCommitUpdateTask.name] diff --git a/apps/worker/tasks/compute_comparison.py b/apps/worker/tasks/compute_comparison.py new file mode 100644 index 0000000000..8c00581070 --- /dev/null +++ b/apps/worker/tasks/compute_comparison.py @@ -0,0 +1,323 @@ +import logging +from typing import Literal, TypedDict + +import sentry_sdk +from asgiref.sync import async_to_sync +from celery import group +from shared.celery_config import compute_comparison_task_name +from shared.components import Component +from shared.helpers.flag import Flag +from shared.torngit.exceptions import TorngitRateLimitError +from shared.yaml import UserYaml + +from app import celery_app +from database.enums import CompareCommitError, CompareCommitState +from database.models import CompareCommit, CompareComponent, CompareFlag +from database.models.reports import ReportLevelTotals, RepositoryFlag +from helpers.comparison import minimal_totals +from helpers.github_installation import get_installation_name_for_owner_for_task +from rollouts import PARALLEL_COMPONENT_COMPARISON +from services.archive import ArchiveService +from services.comparison import ComparisonProxy, FilteredComparison +from services.comparison_utils import get_comparison_proxy +from services.report import ReportService +from services.yaml import get_current_yaml, get_repo_yaml +from tasks.base import BaseCodecovTask +from tasks.compute_component_comparison import compute_component_comparison_task + +log = logging.getLogger(__name__) + + +ComputeComparisonTaskErrors = ( + Literal["missing_head_report"] + | Literal["missing_base_report"] + | Literal["torngit_rate_limit"] +) + + +class ComputeComparisonTaskReturn(TypedDict): + success: bool + error: ComputeComparisonTaskErrors | None + + +class ComputeComparisonTask(BaseCodecovTask, name=compute_comparison_task_name): + def run_impl( + self, db_session, comparison_id, *args, **kwargs + ) -> ComputeComparisonTaskReturn: + comparison: CompareCommit = db_session.query(CompareCommit).get(comparison_id) + repo = comparison.compare_commit.repository + log_extra = dict( + comparison_id=comparison_id, + repoid=repo.repoid, + commit=comparison.compare_commit.commitid, + ) + log.info("Computing comparison", extra=log_extra) + current_yaml = get_repo_yaml(repo) + installation_name_to_use = get_installation_name_for_owner_for_task( + self.name, repo.owner + ) + report_service = ReportService( + current_yaml, gh_app_installation_name=installation_name_to_use + ) + + comparison_proxy = get_comparison_proxy(comparison, report_service) + if not comparison_proxy.has_head_report(): + comparison.error = CompareCommitError.missing_head_report.value + comparison.state = CompareCommitState.error.value + log.warning("Comparison doesn't have HEAD report", extra=log_extra) + return {"successful": False, "error": "missing_head_report"} + + # At this point we can calculate the patch coverage + # Because we have a HEAD report and a base commit to get the diff from + patch_totals = comparison_proxy.get_patch_totals() + comparison.patch_totals = minimal_totals(patch_totals) + db_session.commit() + + if not comparison_proxy.has_project_coverage_base_report(): + comparison.error = CompareCommitError.missing_base_report.value + log.warning( + "Comparison doesn't have BASE report", + extra={"base_commit": comparison.base_commit.commitid, **log_extra}, + ) + comparison.state = CompareCommitState.error.value + return {"successful": False, "error": "missing_base_report"} + else: + comparison.error = None + + try: + impacted_files = comparison_proxy.get_impacted_files() + except TorngitRateLimitError: + log.warning( + "Unable to compute comparison due to rate limit error", + extra=dict( + comparison_id=comparison_id, repoid=comparison.compare_commit.repoid + ), + ) + comparison.state = CompareCommitState.error.value + return {"successful": False, "error": "torngit_rate_limit"} + + log.info("Files impact calculated", extra=log_extra) + path = self.store_results(comparison, impacted_files) + + comparison.report_storage_path = path + db_session.commit() + + comparison.state = CompareCommitState.processed.value + log.info("Computing comparison successful", extra=log_extra) + db_session.commit() + + self.compute_flag_comparison(db_session, comparison, comparison_proxy) + db_session.commit() + self.compute_component_comparisons(db_session, comparison, comparison_proxy) + db_session.commit() + + return {"successful": True} + + def compute_flag_comparison(self, db_session, comparison, comparison_proxy): + log_extra = dict(comparison_id=comparison.id) + log.info("Computing flag comparisons", extra=log_extra) + head_report_flags = comparison_proxy.comparison.head.report.flags + if not head_report_flags: + log.info("Head report does not have any flags", extra=log_extra) + return + self.create_or_update_flag_comparisons( + db_session, + head_report_flags, + comparison, + comparison_proxy, + ) + + @sentry_sdk.trace + def create_or_update_flag_comparisons( + self, + db_session, + head_report_flags: dict[str, Flag], + comparison: CompareCommit, + comparison_proxy: ComparisonProxy, + ): + repository_id = comparison.compare_commit.repository.repoid + for flag_name in head_report_flags.keys(): + totals = self.get_flag_comparison_totals(flag_name, comparison_proxy) + repositoryflag = ( + db_session.query(RepositoryFlag) + .filter_by( + flag_name=flag_name, + repository_id=repository_id, + ) + .first() + ) + if not repositoryflag: + log.warning( + "Repository flag not found for flag. Created repository flag.", + extra=dict(repoid=repository_id, flag_name=flag_name), + ) + repositoryflag = RepositoryFlag( + repository_id=repository_id, + flag_name=flag_name, + ) + db_session.add(repositoryflag) + db_session.flush() + + flag_comparison_entry = ( + db_session.query(CompareFlag) + .filter_by( + commit_comparison_id=comparison.id, + repositoryflag_id=repositoryflag.id, + ) + .first() + ) + + if not flag_comparison_entry: + log.debug( + "No previous flag comparisons; adding flag comparisons", + extra=dict(repoid=repository_id), + ) + self.store_flag_comparison( + db_session, comparison, repositoryflag, totals + ) + else: + log.debug( + "Updating totals for existing flag comparison entry", + extra=dict(repoid=repository_id), + ) + flag_comparison_entry.head_totals = totals["head_totals"] + flag_comparison_entry.base_totals = totals["base_totals"] + flag_comparison_entry.patch_totals = totals["patch_totals"] + log.info( + "Flag comparisons stored successfully", + extra=dict(number_stored=len(head_report_flags)), + ) + + def get_flag_comparison_totals( + self, + flag_name: str, + comparison_proxy: ComparisonProxy, + ): + flag_head_report = comparison_proxy.comparison.head.report.flags.get(flag_name) + flag_base_report = ( + comparison_proxy.comparison.project_coverage_base.report.flags.get( + flag_name + ) + ) + head_totals = None if not flag_head_report else flag_head_report.totals.asdict() + base_totals = None if not flag_base_report else flag_base_report.totals.asdict() + totals = dict( + head_totals=head_totals, base_totals=base_totals, patch_totals=None + ) + diff = comparison_proxy.get_diff() + if diff: + patch_totals = flag_head_report.apply_diff(diff) + if patch_totals: + totals["patch_totals"] = patch_totals.asdict() + return totals + + def store_flag_comparison( + self, + db_session, + comparison: CompareCommit, + repositoryflag: RepositoryFlag, + totals: ReportLevelTotals, + ): + flag_comparison = CompareFlag( + commit_comparison=comparison, + repositoryflag=repositoryflag, + patch_totals=totals["patch_totals"], + head_totals=totals["head_totals"], + base_totals=totals["base_totals"], + ) + db_session.add(flag_comparison) + db_session.flush() + + @sentry_sdk.trace + def compute_component_comparisons( + self, db_session, comparison: CompareCommit, comparison_proxy: ComparisonProxy + ): + head_commit = comparison_proxy.comparison.head.commit + yaml: UserYaml = async_to_sync(get_current_yaml)( + head_commit, comparison_proxy.repository_service + ) + components = yaml.get_components() + log.info( + "Computing component comparisons", + extra=dict( + comparison_id=comparison.id, + component_count=len(components), + ), + ) + if PARALLEL_COMPONENT_COMPARISON.check_value( + comparison.compare_commit.repoid, default=False + ): + self.parallel_compute_component_comparison(comparison.id, components) + else: + for component in components: + self.compute_component_comparison( + db_session, comparison, comparison_proxy, component + ) + + @sentry_sdk.trace + def parallel_compute_component_comparison( + self, + comparison_id: int, + components: list[Component], + ): + task_group = group( + [ + compute_component_comparison_task.s( + comparison_id, component.component_id + ) + for component in components + ] + ) + task_group.apply_async() + + def compute_component_comparison( + self, + db_session, + comparison: CompareCommit, + comparison_proxy: ComparisonProxy, + component: Component, + ): + component_comparison = ( + db_session.query(CompareComponent) + .filter_by( + commit_comparison_id=comparison.id, + component_id=component.component_id, + ) + .first() + ) + if not component_comparison: + component_comparison = CompareComponent( + commit_comparison=comparison, + component_id=component.component_id, + ) + + # filter comparison by component + head_report = comparison_proxy.comparison.head.report + flags = component.get_matching_flags(head_report.flags.keys()) + filtered: FilteredComparison = comparison_proxy.get_filtered_comparison( + flags=flags, path_patterns=component.paths + ) + + # component comparison totals + component_comparison.base_totals = ( + filtered.project_coverage_base.report.totals.asdict() + ) + component_comparison.head_totals = filtered.head.report.totals.asdict() + diff = comparison_proxy.get_diff() + if diff: + patch_totals = filtered.head.report.apply_diff(diff) + if patch_totals: + component_comparison.patch_totals = patch_totals.asdict() + + db_session.add(component_comparison) + db_session.flush() + + @sentry_sdk.trace + def store_results(self, comparison, impacted_files): + repository = comparison.compare_commit.repository + storage_service = ArchiveService(repository) + return storage_service.write_computed_comparison(comparison, impacted_files) + + +RegisteredComputeComparisonTask = celery_app.register_task(ComputeComparisonTask()) +compute_comparison_task = celery_app.tasks[RegisteredComputeComparisonTask.name] diff --git a/apps/worker/tasks/compute_component_comparison.py b/apps/worker/tasks/compute_component_comparison.py new file mode 100644 index 0000000000..fd58833399 --- /dev/null +++ b/apps/worker/tasks/compute_component_comparison.py @@ -0,0 +1,115 @@ +import logging + +from asgiref.sync import async_to_sync +from shared.components import Component +from shared.utils.enums import TaskConfigGroup +from shared.yaml import UserYaml +from sqlalchemy.orm import Session + +from app import celery_app +from database.models import CompareCommit, CompareComponent +from helpers.github_installation import get_installation_name_for_owner_for_task +from services.comparison import ComparisonProxy, FilteredComparison +from services.comparison_utils import get_comparison_proxy +from services.report import ReportService +from services.yaml import get_current_yaml, get_repo_yaml +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + +task_name = ( + f"app.tasks.{TaskConfigGroup.compute_comparison.value}.ComputeComponentComparison" +) + + +def compute_component_comparison( + db_session: Session, + comparison: CompareCommit, + comparison_proxy: ComparisonProxy, + component: Component, +): + component_comparison = ( + db_session.query(CompareComponent) + .filter_by( + commit_comparison_id=comparison.id, + component_id=component.component_id, + ) + .first() + ) + if not component_comparison: + component_comparison = CompareComponent( + commit_comparison=comparison, + component_id=component.component_id, + ) + + # filter comparison by component + head_report = comparison_proxy.comparison.head.report + flags = component.get_matching_flags(head_report.flags.keys()) + filtered: FilteredComparison = comparison_proxy.get_filtered_comparison( + flags=flags, path_patterns=component.paths + ) + + # component comparison totals + component_comparison.base_totals = ( + filtered.project_coverage_base.report.totals.asdict() + ) + component_comparison.head_totals = filtered.head.report.totals.asdict() + diff = comparison_proxy.get_diff() + if diff: + patch_totals = filtered.head.report.apply_diff(diff) + if patch_totals: + component_comparison.patch_totals = patch_totals.asdict() + + db_session.add(component_comparison) + db_session.flush() + + +class ComputeComponentComparisonTask(BaseCodecovTask, name=task_name): + def run_impl( + self, + db_session: Session, + comparison_id: int, + component_id: str, + *args, + **kwargs, + ): + comparison: CompareCommit = db_session.query(CompareCommit).get(comparison_id) + repo = comparison.compare_commit.repository + + log_extra = dict( + comparison_id=comparison_id, + repoid=repo.repoid, + commit=comparison.compare_commit.commitid, + ) + log.info("Computing component comparison", extra=log_extra) + + current_yaml = get_repo_yaml(repo) + installation_name_to_use = get_installation_name_for_owner_for_task( + self.name, repo.owner + ) + report_service = ReportService( + current_yaml, gh_app_installation_name=installation_name_to_use + ) + comparison_proxy = get_comparison_proxy(comparison, report_service) + head_commit = comparison_proxy.comparison.head.commit + + yaml: UserYaml = async_to_sync(get_current_yaml)( + head_commit, comparison_proxy.repository_service + ) + + components = yaml.get_components() + + component_dict = {c.component_id: c for c in components} + compute_component_comparison( + db_session, comparison, comparison_proxy, component_dict[component_id] + ) + + log.info("Finished computing component comparison", extra=log_extra) + + +RegisteredComputeComponentComparisonTask = celery_app.register_task( + ComputeComponentComparisonTask() +) +compute_component_comparison_task = celery_app.tasks[ + RegisteredComputeComponentComparisonTask.name +] diff --git a/apps/worker/tasks/crontasks.py b/apps/worker/tasks/crontasks.py new file mode 100644 index 0000000000..960ec692d2 --- /dev/null +++ b/apps/worker/tasks/crontasks.py @@ -0,0 +1,65 @@ +import logging +from datetime import datetime, timedelta, timezone + +from redis.exceptions import LockError +from shared.helpers.redis import get_redis_connection + +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class CodecovCronTask(BaseCodecovTask): + @classmethod + def get_min_seconds_interval_between_executions(cls) -> int: + """ + Ensures the task never runs twice inside a certain time interval. + + This is just a mechanism of protection in addition to the lock. + The lock guarantees two tasks don't run at the same time in case + they are scheduled by two different workers. But if one task finishes + too quickly before the other one even starts (due to queue timing), + they will both run. + + So this task gives a little buffer on that. So if a task is meant to run + every hour or so, giving it a 50-55 minutes buffer ensure that the tasks + coming right after it won't run (unless there is some crazy queue that makes + a task take 50 minute to run) while still making sure the task arriving + on the next hour can still run + + Returns: + int: Number of seconds to wait before the task is run again + """ + raise NotImplementedError() + + def run_impl(self, db_session, *args, cron_task_generation_time_iso, **kwargs): + lock_name = f"worker.executionlock.{self.name}" + redis_connection = get_redis_connection() + generation_time = datetime.fromisoformat(cron_task_generation_time_iso) + try: + with redis_connection.lock( + lock_name, + timeout=max(60 * 5, self.hard_time_limit_task), + blocking_timeout=1, + ): + min_seconds_interval = ( + self.get_min_seconds_interval_between_executions() + ) + last_executed_key = f"worker.last_execution_on.{self.name}" + last_execution_on = redis_connection.get(last_executed_key) + if last_execution_on is not None and generation_time - timedelta( + seconds=min_seconds_interval + ) < datetime.fromtimestamp( + int(float(last_execution_on)), tz=timezone.utc + ): + log.info("Cron task executed very recently. Skipping") + return {"executed": False} + redis_connection.setex( + last_executed_key, min_seconds_interval, generation_time.timestamp() + ) + log.info("Executing cron task") + result = self.run_cron_task(db_session, *args, **kwargs) + return {"executed": True, "result": result} + except LockError: + log.info("Not executing cron task since another one is already running it") + return {"executed": False} diff --git a/apps/worker/tasks/delete_owner.py b/apps/worker/tasks/delete_owner.py new file mode 100644 index 0000000000..0a9b95f217 --- /dev/null +++ b/apps/worker/tasks/delete_owner.py @@ -0,0 +1,26 @@ +import logging + +from celery.exceptions import SoftTimeLimitExceeded +from shared.celery_config import delete_owner_task_name + +from app import celery_app +from services.cleanup.owner import cleanup_owner +from services.cleanup.utils import CleanupSummary +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class DeleteOwnerTask(BaseCodecovTask, name=delete_owner_task_name): + acks_late = True # retry the task when the worker dies for whatever reason + max_retries = None # aka, no limit on retries + + def run_impl(self, _db_session, ownerid: int) -> CleanupSummary: + try: + return cleanup_owner(ownerid) + except SoftTimeLimitExceeded: + raise self.retry() + + +RegisteredDeleteOwnerTask = celery_app.register_task(DeleteOwnerTask()) +delete_owner_task = celery_app.tasks[DeleteOwnerTask.name] diff --git a/apps/worker/tasks/flare_cleanup.py b/apps/worker/tasks/flare_cleanup.py new file mode 100644 index 0000000000..b0d73a5792 --- /dev/null +++ b/apps/worker/tasks/flare_cleanup.py @@ -0,0 +1,98 @@ +import logging + +from shared.celery_config import flare_cleanup_task_name +from shared.django_apps.core.models import Pull, PullStates + +from app import celery_app +from services.cleanup.models import cleanup_files_batched +from services.cleanup.utils import cleanup_context +from tasks.crontasks import CodecovCronTask + +log = logging.getLogger(__name__) + + +class FlareCleanupTask(CodecovCronTask, name=flare_cleanup_task_name): + """ + Flare is a field on a Pull object. + Flare is used to draw static graphs (see GraphHandler view in api) and can be large. + The majority of flare graphs are used in pr comments, so we keep the (maybe large) flare "available" + in either the db or Archive storage while the pull is OPEN. + If the pull is not OPEN, we dump the flare to save space. + If we need to generate a flare graph for a non-OPEN pull, we build_report_from_commit + and generate fresh flare from that report (see GraphHandler view in api). + """ + + @classmethod + def get_min_seconds_interval_between_executions(cls): + return 72000 # 20h + + def run_cron_task(self, db_session, batch_size=1000, limit=10000, *args, **kwargs): + # for any Pull that is not OPEN, clear the flare field(s), targeting older data + non_open_pulls = Pull.objects.exclude(state=PullStates.OPEN.value).order_by( + "updatestamp" + ) + + log.info("Starting FlareCleanupTask") + + # clear in db + non_open_pulls_with_flare_in_db = non_open_pulls.filter( + _flare__isnull=False + ).exclude(_flare={}) + + # Process in batches + total_updated = 0 + start = 0 + while start < limit: + stop = start + batch_size if start + batch_size < limit else limit + batch = non_open_pulls_with_flare_in_db.values_list("id", flat=True)[ + start:stop + ] + if not batch: + break + n_updated = non_open_pulls_with_flare_in_db.filter(id__in=batch).update( + _flare=None + ) + total_updated += n_updated + start = stop + + log.info(f"FlareCleanupTask cleared {total_updated} database flares") + + # clear in Archive + non_open_pulls_with_flare_in_archive = non_open_pulls.filter( + _flare_storage_path__isnull=False + ) + + # Process archive deletions in batches + total_updated = 0 + start = 0 + with cleanup_context() as context: + while start < limit: + stop = start + batch_size if start + batch_size < limit else limit + batch = non_open_pulls_with_flare_in_archive.values_list( + "id", flat=True + )[start:stop] + if not batch: + break + + flare_paths_from_batch = Pull.objects.filter(id__in=batch).values_list( + "_flare_storage_path", flat=True + ) + cleanup_files_batched( + context, {context.default_bucket: flare_paths_from_batch} + ) + + # Update the _flare_storage_path field for successfully processed pulls + n_updated = Pull.objects.filter(id__in=batch).update( + _flare_storage_path=None + ) + total_updated += n_updated + start = stop + + log.info(f"FlareCleanupTask cleared {total_updated} Archive flares") + + def manual_run(self, db_session=None, limit=1000, *args, **kwargs): + self.run_cron_task(db_session, limit=limit, *args, **kwargs) + + +RegisteredFlareCleanupTask = celery_app.register_task(FlareCleanupTask()) +flare_cleanup_task = celery_app.tasks[RegisteredFlareCleanupTask.name] diff --git a/apps/worker/tasks/flush_repo.py b/apps/worker/tasks/flush_repo.py new file mode 100644 index 0000000000..a8b64cdf5a --- /dev/null +++ b/apps/worker/tasks/flush_repo.py @@ -0,0 +1,25 @@ +import logging + +from celery.exceptions import SoftTimeLimitExceeded + +from app import celery_app +from services.cleanup.repository import cleanup_repo +from services.cleanup.utils import CleanupSummary +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class FlushRepoTask(BaseCodecovTask, name="app.tasks.flush_repo.FlushRepo"): + acks_late = True # retry the task when the worker dies for whatever reason + max_retries = None # aka, no limit on retries + + def run_impl(self, _db_session, repoid: int) -> CleanupSummary: + try: + return cleanup_repo(repoid) + except SoftTimeLimitExceeded: + raise self.retry() + + +FlushRepo = celery_app.register_task(FlushRepoTask()) +flush_repo = celery_app.tasks[FlushRepo.name] diff --git a/apps/worker/tasks/github_app_webhooks_check.py b/apps/worker/tasks/github_app_webhooks_check.py new file mode 100644 index 0000000000..56753fb04b --- /dev/null +++ b/apps/worker/tasks/github_app_webhooks_check.py @@ -0,0 +1,220 @@ +import asyncio +import logging +from datetime import datetime, timedelta +from itertools import groupby +from typing import Iterable, List + +from asgiref.sync import async_to_sync +from shared.celery_config import gh_app_webhook_check_task_name +from shared.config import get_config +from shared.metrics import Counter +from shared.torngit import Github +from shared.torngit.exceptions import ( + TorngitRateLimitError, + TorngitServer5xxCodeError, + TorngitUnauthorizedError, +) + +from app import celery_app +from helpers.environment import is_enterprise +from services.github import get_github_integration_token +from tasks.crontasks import CodecovCronTask + +log = logging.getLogger(__name__) + +WEBHOOK_DELIVERY_FAILURES = Counter( + "worker_webhooks_deliveries_failed", + "Count of how many incoming webhooks failed to be delivered to our service, broken down by git provider and webhook event. This metric overcounts: the task runs every 6 hours but it considers failures from the last 8 hours.", + [ + "service", + "event", + ], +) + +WEBHOOK_REDELIVERY_REQUESTS = Counter( + "worker_webhooks_redelivery_requests", + "Count of how many webhooks we requested redelivery for, broken down by git provider and whether the redelivery request was successful. Note that we don't necessarily request redelivery for every failure.", + [ + "service", + "result", + ], +) + + +class GitHubAppWebhooksCheckTask(CodecovCronTask, name=gh_app_webhook_check_task_name): + @classmethod + def get_min_seconds_interval_between_executions(cls): + return 18000 # 5h + + def _apply_time_filter(self, deliveries: List[object]) -> Iterable[object]: + """ + Apply a time filter to the deliveries, so that we only consider webhook deliveries from the past 25h. + So that we skip deliveries that we probably already analysed in the past. + """ + now = datetime.now() + eight_hours_ago = now - timedelta(hours=8) + + def time_filter(item: object) -> bool: + return ( + datetime.strptime(item["delivered_at"], "%Y-%m-%dT%H:%M:%SZ") + >= eight_hours_ago + ) + + return filter(time_filter, deliveries) + + def _apply_status_filter(self, deliveries: List[object]) -> Iterable[object]: + """ + Apply a filter to the status code of the delivery so that we ignore successful deliveries. + """ + + def status_filter(item: object) -> bool: + return item["status_code"] != 200 + + return filter(status_filter, deliveries) + + def _apply_event_filter(self, deliveries: List[object]) -> Iterable[object]: + """ + Apply a status filter. We really only care about installation webhooks for now. + events: installation, installation_repositories + """ + + def event_filter(item: object) -> bool: + return item["event"].startswith("installation") + + return filter(event_filter, deliveries) + + async def process_delivery_page( + self, gh_handler: Github, deliveries: List[object] + ) -> List[object]: + # Beware - filter objects are single-use iterables. If you iterate or + # take their length, they're consumed and can't be used again. + deliveries = self._apply_time_filter(deliveries) + deliveries = self._apply_status_filter(deliveries) + + # Sort by the webhook event so we can record how many failures there + # were for each different event. `sorted` returns a list even if the + # input was a single-use iterator. + deliveries = sorted(deliveries, key=lambda item: item.get("event")) + for event, items in groupby(deliveries, key=lambda item: item.get("event")): + WEBHOOK_DELIVERY_FAILURES.labels(service="github", event=event).inc( + len(list(items)) + ) + + # We don't necessarily want to request redelivery for every failure. + # Apply a filter and materialize it as a list. + deliveries = list(self._apply_event_filter(deliveries)) + redeliveries_requested = len(deliveries) + + successful_request_count = await self.request_redeliveries( + gh_handler, deliveries + ) + WEBHOOK_REDELIVERY_REQUESTS.labels(service="github", result="success").inc( + successful_request_count + ) + WEBHOOK_REDELIVERY_REQUESTS.labels(service="github", result="failure").inc( + redeliveries_requested - successful_request_count + ) + + return successful_request_count, redeliveries_requested + + async def request_redeliveries( + self, gh_handler: Github, deliveries_to_request: List[object] + ) -> int: + """ + Requests re-delivery of failed webhooks to GitHub. + Returns the number of successful redelivery requests. + """ + if len(deliveries_to_request) == 0: + return 0 + redelivery_coroutines = map( + lambda item: gh_handler.request_webhook_redelivery(item["id"]), + deliveries_to_request, + ) + results = await asyncio.gather(*redelivery_coroutines) + return sum(results) + + def run_cron_task(self, db_session, *args, **kwargs): + if is_enterprise(): + return dict(checked=False, reason="Enterprise env") + + gh_app_token = get_github_integration_token( + service="github", installation_id=None + ) + gh_handler = Github( + token=dict(key=gh_app_token), + oauth_consumer_token=dict( + key=get_config("github", "client_id"), + secret=get_config("github", "client_secret"), + ), + ) + redeliveries_requested = 0 + successful_redeliveries = 0 + all_deliveries = 0 + pages_processed = 0 + + async def process_deliveries(): + async for deliveries in gh_handler.list_webhook_deliveries(): + nonlocal all_deliveries + nonlocal pages_processed + nonlocal redeliveries_requested + nonlocal successful_redeliveries + all_deliveries += len(deliveries) + pages_processed += 1 + ( + curr_successful_redeliveries, + curr_redeliveries_requested, + ) = await self.process_delivery_page( + gh_handler, + deliveries, + ) + successful_redeliveries += curr_successful_redeliveries + redeliveries_requested += curr_redeliveries_requested + log.info( + "Processed page of webhook redelivery requests", + extra=dict( + deliveries_to_request=curr_redeliveries_requested, + successful_redeliveries=curr_successful_redeliveries, + acc_successful_redeliveries=successful_redeliveries, + acc_redeliveries_requested=redeliveries_requested, + ), + ) + + try: + async_to_sync(process_deliveries)() + except ( + TorngitUnauthorizedError, + TorngitServer5xxCodeError, + TorngitRateLimitError, + ) as exp: + log.error( + "Failed to check github app webhooks", + extra=dict( + reason="Failed with exception. Ending task immediately", + exception=str(exp), + redeliveries_requested=redeliveries_requested, + deliveries_processed=all_deliveries, + pages_processed=pages_processed, + ), + ) + return dict( + checked=False, + reason="Failed with exception. Ending task immediately", + exception=str(exp), + redeliveries_requested=redeliveries_requested, + successful_redeliveries=successful_redeliveries, + deliveries_processed=all_deliveries, + pages_processed=pages_processed, + ) + return dict( + checked=True, + redeliveries_requested=redeliveries_requested, + deliveries_processed=all_deliveries, + pages_processed=pages_processed, + successful_redeliveries=successful_redeliveries, + ) + + +RegisteredGitHubAppWebhooksCheckTask = celery_app.register_task( + GitHubAppWebhooksCheckTask() +) +gh_webhook_check_task = celery_app.tasks[RegisteredGitHubAppWebhooksCheckTask.name] diff --git a/apps/worker/tasks/github_marketplace.py b/apps/worker/tasks/github_marketplace.py new file mode 100644 index 0000000000..52e2f219c5 --- /dev/null +++ b/apps/worker/tasks/github_marketplace.py @@ -0,0 +1,264 @@ +import logging +from datetime import datetime + +import requests +from shared.celery_config import ghm_sync_plans_task_name +from shared.plan.constants import DEFAULT_FREE_PLAN + +from app import celery_app +from database.models import Owner, Repository +from services.github_marketplace import GitHubMarketplaceService +from services.stripe import stripe +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class SyncPlansTask(BaseCodecovTask, name=ghm_sync_plans_task_name): + """ + Sync GitHub marketplace plans + """ + + def run_impl(self, db_session, sender=None, account=None, action=None): + """ + Sender: The person who took the action that triggered the webhook. Ex: + { "login":"username", "id":3877742, "type":"User", ..., "email":"username@email.com" } + Account: The organization or user account associated with the subscription. + Action: The action performed to generate the webhook. Can be `purchased`, + `cancelled`, `pending_change`, `pending_change_cancelled`, or `changed`. + """ + log.info( + "GitHub marketplace sync plans", + extra=dict(sender=sender, account=account, action=action), + ) + + # make sure sender and account owner entries exist + if sender: + self.upsert_owner(db_session, sender["id"], sender["login"]) + + if account: + self.upsert_owner(db_session, account["id"], account["login"]) + + ghm_service = GitHubMarketplaceService() + + if account: + # TODO sync all team members - 3 year old todo from legacy... + try: + plans = ghm_service.get_account_plans(account["id"]) + except requests.exceptions.HTTPError as e: + if e.response.status_code == 404: + # account has not purchased the listing + return self.sync_plan( + db_session, ghm_service, account["id"], None, action=action + ) + else: + raise + + return self.sync_plan( + db_session, + ghm_service, + account["id"], + plans["marketplace_purchase"], + action=action, + ) + else: + log.warning( + "No account provided for GitHub Marketplace sync", + extra=dict(sender=sender, account=account, action=action), + ) + + def sync_plan( + self, db_session, ghm_service, service_id, purchase_object, action=None + ): + log.info( + "Sync plan", + extra=dict( + service_id=service_id, purchase_object=purchase_object, action=action + ), + ) + + if ( + action != "cancelled" + and purchase_object + and purchase_object["plan"]["id"] in ghm_service.plan_ids + ): + self.create_or_update_plan( + db_session, ghm_service, service_id, purchase_object + ) + plan_type_synced = "paid" + else: + self.create_or_update_to_free_plan(db_session, ghm_service, service_id) + plan_type_synced = "free" + + return dict(plan_type_synced=plan_type_synced) + + def sync_all(self, db_session, ghm_service, action): + """ + This is carried over from legacy to sync all plan accounts - it is not currently used + """ + log.info("Sync all", extra=dict(action=action)) + + has_a_plan = [] + + # get codecov plans + plans = ghm_service.get_codecov_plans() + plan_ids = [plan["id"] for plan in plans] + + for plan_id in plan_ids: + page = 0 + # loop through all plan accounts + while True: + page = page + 1 + accounts = ghm_service.get_plan_accounts(page, plan_id) + + if len(accounts) == 0: + # next plan + break + + # sync each plan + for customers in accounts: + has_a_plan.append(str(customers["id"])) + self.sync_plan( + db_session, + ghm_service, + customers["id"], + customers["marketplace_purchase"], + action=action, + ) + + self.disable_all_inactive(db_session, has_a_plan) + + def upsert_owner(self, db_session, service_id, username): + log.info("Upsert owner", extra=dict(service_id=service_id, username=username)) + + owner = ( + db_session.query(Owner) + .filter(Owner.service == "github", Owner.service_id == str(service_id)) + .first() + ) + + if owner: + owner.username = username + owner.updatestamp = datetime.now() + else: + owner = self.create_owner(db_session, service_id, username) + + return owner.ownerid + + def create_owner(self, db_session, service_id, username, name=None, email=None): + owner = Owner( + service="github", + service_id=service_id, + username=username, + name=name, + email=email, + plan_provider="github", + createstamp=datetime.now(), + ) + db_session.add(owner) + db_session.flush() + return owner + + def disable_all_inactive(self, db_session, active_account_ids): + """ + Disable all plans that are no longer active + """ + active_account_ids = list(map(str, active_account_ids)) + + db_session.query(Owner).filter( + Owner.service == "github", + Owner.plan == "users", + Owner.plan_provider == "github", + Owner.service_id.notin_(active_account_ids), + ).update({Owner.plan: None}, synchronize_session=False) + + def deactivate_repos(self, db_session, ownerid): + """ + Deactivate all repos for given ownerid + """ + db_session.query(Repository).filter(Repository.ownerid == ownerid).update( + {Repository.activated: False}, synchronize_session=False + ) + + def create_or_update_plan( + self, db_session, ghm_service, service_id, purchase_object + ): + """ + Create or update plan from GitHub marketplace purchase info. Cancel Stripe + subscription if owner currently has one. + """ + log.info( + "Github Marketplace - Create or update plan", + extra=dict(service_id=service_id), + ) + + owner = ( + db_session.query(Owner) + .filter(Owner.service == "github", Owner.service_id == str(service_id)) + .first() + ) + + if owner: + owner.plan = "users" + owner.plan_provider = "github" + owner.plan_auto_activate = True + owner.plan_activated_users = None + owner.plan_user_count = purchase_object["unit_count"] + + if owner.stripe_customer_id and owner.stripe_subscription_id: + # cancel stripe subscription immediately + stripe.Subscription.cancel( + owner.stripe_subscription_id, + prorate=True, + ) + owner.stripe_subscription_id = None + else: + # create the user + user_data = ghm_service.get_user(service_id) + new_owner = self.create_owner( + db_session, + service_id, + user_data["login"], + user_data["name"], + user_data["email"], + ) + + # set plan info + new_owner.plan = "users" + new_owner.plan_provider = "github" + new_owner.plan_auto_activate = True + new_owner.plan_user_count = purchase_object["unit_count"] + + def create_or_update_to_free_plan(self, db_session, ghm_service, service_id): + """ + Create or update user to free plan for when plan isn't known or the action is "cancelled" + """ + log.info("Create or update to free plan", extra=dict(service_id=service_id)) + + owner = ( + db_session.query(Owner) + .filter(Owner.service == "github", Owner.service_id == str(service_id)) + .first() + ) + + if owner: + # deactivate repos and remove all activated users for this owner + owner.plan = DEFAULT_FREE_PLAN + owner.plan_user_count = 1 + owner.plan_activated_users = None + + self.deactivate_repos(db_session, owner.ownerid) + else: + # create the user + user_data = ghm_service.get_user(service_id) + self.create_owner( + db_session, + service_id, + user_data["login"], + user_data["name"], + user_data["email"], + ) + + +RegisteredGHMSyncPlansTask = celery_app.register_task(SyncPlansTask()) +ghm_sync_plans_task = celery_app.tasks[SyncPlansTask.name] diff --git a/apps/worker/tasks/health_check.py b/apps/worker/tasks/health_check.py new file mode 100644 index 0000000000..51ba15b191 --- /dev/null +++ b/apps/worker/tasks/health_check.py @@ -0,0 +1,58 @@ +import logging +from typing import Set + +import shared.helpers.redis as redis_service +from shared.celery_config import health_check_task_name +from shared.config import get_config + +from app import celery_app +from helpers.metrics import metrics +from tasks.crontasks import CodecovCronTask + +log = logging.getLogger(__name__) + + +class HealthCheckTask(CodecovCronTask, name=health_check_task_name): + @classmethod + def get_min_seconds_interval_between_executions(cls): + return 8 # This task should run every 10s, so this time should be small. + + def _get_all_queue_names_from_config(self) -> Set[str]: + """ + Gets all queue names defined in the *install* codecov.yaml. + EXCEPT the healthcheck queue itself that's hardcoded in celery_config.py. + """ + tasks_config = get_config("setup", "tasks", default={}) + default_queue_name = get_config( + "setup", "tasks", "celery", "default_queue", default="celery" + ) + queue_names_in_config = set( + [ + item["queue"] + for _, item in tasks_config.items() + if item.get("queue") is not None + ] + ) + queue_names_in_config.add(default_queue_name) + enterprise_queues = set( + map(lambda queue: "enterprise_" + queue, queue_names_in_config) + ) + return queue_names_in_config | enterprise_queues + + def _get_correct_redis_connection(self): + if get_config("services", "celery_broker"): + return redis_service._get_redis_instance_from_url( + get_config("services", "celery_broker") + ) + else: + return redis_service.get_redis_connection() + + def run_cron_task(self, db_session, *args, **kwargs): + queue_names = self._get_all_queue_names_from_config() + redis = self._get_correct_redis_connection() + for q in queue_names: + metrics.gauge("celery.queue.%s.len" % q, redis.llen(q)) + + +RegisteredHealthCheckTask = celery_app.register_task(HealthCheckTask()) +health_check_task = celery_app.tasks[RegisteredHealthCheckTask.name] diff --git a/apps/worker/tasks/hourly_check.py b/apps/worker/tasks/hourly_check.py new file mode 100644 index 0000000000..55819e7cbe --- /dev/null +++ b/apps/worker/tasks/hourly_check.py @@ -0,0 +1,23 @@ +import logging + +from app import celery_app +from celery_config import hourly_check_task_name +from helpers.metrics import metrics +from tasks.crontasks import CodecovCronTask + +log = logging.getLogger(__name__) + + +class HourlyCheckTask(CodecovCronTask, name=hourly_check_task_name): + @classmethod + def get_min_seconds_interval_between_executions(cls): + return 3300 # 55 minutes + + def run_cron_task(self, db_session, *args, **kwargs): + log.info("Doing hourly check") + metrics.incr(f"{self.metrics_prefix}.checks") + return {"checked": True} + + +RegisteredHourlyCheckTask = celery_app.register_task(HourlyCheckTask()) +hourly_check_task = celery_app.tasks[RegisteredHourlyCheckTask.name] diff --git a/apps/worker/tasks/http_request.py b/apps/worker/tasks/http_request.py new file mode 100644 index 0000000000..f9d8a4dd79 --- /dev/null +++ b/apps/worker/tasks/http_request.py @@ -0,0 +1,72 @@ +import logging + +import httpx +from shared.config import get_config + +from app import celery_app +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class HTTPRequestTask(BaseCodecovTask, name="app.tasks.http_request.HTTPRequest"): + """ + Task for making generic HTTP requests. + """ + + def run_impl( + self, + db_session, + url, + method="POST", + headers=None, + timeout=None, + data=None, + *args, + **kwargs, + ): + if timeout is None: + timeout = get_config("setup", "http", "timeouts", "external", default=10) + + params = dict( + url=url, + method=method, + headers=headers, + data=data, + timeout=timeout, + ) + + log.info("HTTP request", extra=params) + + try: + with httpx.Client() as client: + res = client.request(**params) + + if res.status_code >= 500: + # server error, we can retry later + self._retry_task() + + if res.status_code >= 400: + # malformed request, do not retry + return { + "successful": False, + "status_code": res.status_code, + "response": res.text, + } + + return { + "successful": True, + "status_code": res.status_code, + "response": res.text, + } + except httpx.HTTPError: + log.warning("HTTP request error", exc_info=True) + self._retry_task() + + def _retry_task(self): + # retry w/ exponential backoff + self.retry(max_retries=5, countdown=20 * (2**self.request.retries)) + + +RegisteredHTTPRequestTask = celery_app.register_task(HTTPRequestTask()) +http_request_task = celery_app.tasks[RegisteredHTTPRequestTask.name] diff --git a/apps/worker/tasks/label_analysis.py b/apps/worker/tasks/label_analysis.py new file mode 100644 index 0000000000..d26d1308b7 --- /dev/null +++ b/apps/worker/tasks/label_analysis.py @@ -0,0 +1,562 @@ +import logging +from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union + +import sentry_sdk +from asgiref.sync import async_to_sync +from shared.celery_config import label_analysis_task_name +from shared.labelanalysis import LabelAnalysisRequestState +from sqlalchemy.orm import Session + +from app import celery_app +from database.models.labelanalysis import ( + LabelAnalysisProcessingError, + LabelAnalysisProcessingErrorCode, + LabelAnalysisRequest, +) +from database.models.staticanalysis import StaticAnalysisSuite +from helpers.labels import get_all_report_labels, get_labels_per_session +from helpers.metrics import metrics +from services.report import Report, ReportService +from services.report.report_builder import SpecialLabelsEnum +from services.repository import get_repo_provider_service +from services.static_analysis import StaticAnalysisComparisonService +from services.static_analysis.git_diff_parser import DiffChange, parse_git_diff_json +from services.yaml import get_repo_yaml +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +GLOBAL_LEVEL_LABEL = ( + SpecialLabelsEnum.CODECOV_ALL_LABELS_PLACEHOLDER.corresponding_label +) + +GLOBAL_LEVEL_LABEL_IDX = ( + SpecialLabelsEnum.CODECOV_ALL_LABELS_PLACEHOLDER.corresponding_index +) + + +class LinesRelevantToChangeInFile(TypedDict): + all: bool + lines: Set[int] + + +class LinesRelevantToChange(TypedDict): + all: bool + files: Dict[str, Optional[LinesRelevantToChangeInFile]] + + +class ExistingLabelSetsEncoded(NamedTuple): + all_report_labels: Set[int] + executable_lines_labels: Set[int] + global_level_labels: Set[int] + are_labels_encoded: bool = True + + +class ExistingLabelSetsNotEncoded(NamedTuple): + all_report_labels: Set[str] + executable_lines_labels: Set[str] + global_level_labels: Set[str] + are_labels_encoded: bool = False + + +ExistingLabelSets = Union[ExistingLabelSetsEncoded, ExistingLabelSetsNotEncoded] +PossiblyEncodedLabelSet = Union[Set[str], Set[int]] + + +class LabelAnalysisRequestProcessingTask( + BaseCodecovTask, name=label_analysis_task_name +): + errors: List[LabelAnalysisProcessingError] = None + dbsession: Session = None + + def reset_task_context(self): + """Resets the task's attributes to None to avoid spilling information + between task calls in the same process. + https://docs.celeryq.dev/en/latest/userguide/tasks.html#instantiation + """ + self.errors = None + self.dbsession = None + + def run_impl(self, db_session, request_id, *args, **kwargs): + self.errors = [] + self.dbsession = db_session + label_analysis_request = ( + db_session.query(LabelAnalysisRequest) + .filter(LabelAnalysisRequest.id_ == request_id) + .first() + ) + if label_analysis_request is None: + metrics.incr("label_analysis_task.failed_to_calculate.larq_not_found") + log.error( + "LabelAnalysisRequest not found", extra=dict(request_id=request_id) + ) + self.add_processing_error( + larq_id=request_id, + error_code=LabelAnalysisProcessingErrorCode.NOT_FOUND, + error_msg="LabelAnalysisRequest not found", + error_extra=dict(), + ) + response = { + "success": False, + "present_report_labels": [], + "present_diff_labels": [], + "absent_labels": [], + "global_level_labels": [], + "errors": self.errors, + } + self.reset_task_context() + return response + log.info( + "Starting label analysis request", + extra=dict( + request_id=request_id, + external_id=label_analysis_request.external_id, + commit=label_analysis_request.head_commit.commitid, + ), + ) + + if label_analysis_request.state_id == LabelAnalysisRequestState.FINISHED.db_id: + # Indicates that this request has been calculated already + # We might need to update the requested labels + response = self._handle_larq_already_calculated(label_analysis_request) + self.reset_task_context() + return response + + try: + lines_relevant_to_diff: Optional[LinesRelevantToChange] = ( + self._get_lines_relevant_to_diff(label_analysis_request) + ) + base_report = self._get_base_report(label_analysis_request) + + if lines_relevant_to_diff and base_report: + existing_labels: ExistingLabelSets = self._get_existing_labels( + base_report, lines_relevant_to_diff + ) + if existing_labels.are_labels_encoded: + # Translate label_ids + def partial_fn_to_apply(label_id_set): + return self._lookup_label_ids( + report=base_report, label_ids=label_id_set + ) + + existing_labels = ExistingLabelSetsNotEncoded( + all_report_labels=partial_fn_to_apply( + existing_labels.all_report_labels + ), + executable_lines_labels=partial_fn_to_apply( + existing_labels.executable_lines_labels + ), + global_level_labels=partial_fn_to_apply( + existing_labels.global_level_labels + ), + are_labels_encoded=False, + ) + + requested_labels = self._get_requested_labels(label_analysis_request) + result = self.calculate_final_result( + requested_labels=requested_labels, + existing_labels=existing_labels, + commit_sha=label_analysis_request.head_commit.commitid, + ) + label_analysis_request.result = result + label_analysis_request.state_id = ( + LabelAnalysisRequestState.FINISHED.db_id + ) + metrics.incr("label_analysis_task.success") + response = { + "success": True, + "present_report_labels": result["present_report_labels"], + "present_diff_labels": result["present_diff_labels"], + "absent_labels": result["absent_labels"], + "global_level_labels": result["global_level_labels"], + "errors": self.errors, + } + self.reset_task_context() + return response + except Exception: + # temporary general catch while we find possible problems on this + metrics.incr("label_analysis_task.failed_to_calculate.exception") + log.exception( + "Label analysis failed to calculate", + extra=dict( + request_id=request_id, + commit=label_analysis_request.head_commit.commitid, + external_id=label_analysis_request.external_id, + ), + ) + label_analysis_request.result = None + label_analysis_request.state_id = LabelAnalysisRequestState.ERROR.db_id + self.add_processing_error( + larq_id=request_id, + error_code=LabelAnalysisProcessingErrorCode.FAILED, + error_msg="Failed to calculate", + error_extra=dict(), + ) + response = { + "success": False, + "present_report_labels": [], + "present_diff_labels": [], + "absent_labels": [], + "global_level_labels": [], + "errors": self.errors, + } + self.reset_task_context() + return response + metrics.incr("label_analysis_task.failed_to_calculate.missing_info") + log.warning( + "We failed to get some information that was important to label analysis", + extra=dict( + has_relevant_lines=(lines_relevant_to_diff is not None), + has_base_report=(base_report is not None), + commit=label_analysis_request.head_commit.commitid, + external_id=label_analysis_request.external_id, + request_id=request_id, + ), + ) + label_analysis_request.state_id = LabelAnalysisRequestState.FINISHED.db_id + result_to_save = { + "success": True, + "present_report_labels": [], + "present_diff_labels": [], + "absent_labels": label_analysis_request.requested_labels, + "global_level_labels": [], + } + label_analysis_request.result = result_to_save + result_to_return = {**result_to_save, "errors": self.errors} + self.reset_task_context() + return result_to_return + + def add_processing_error( + self, + larq_id: int, + error_code: LabelAnalysisProcessingErrorCode, + error_msg: str, + error_extra: dict, + ): + error = LabelAnalysisProcessingError( + label_analysis_request_id=larq_id, + error_code=error_code.value, + error_params=dict(message=error_msg, extra=error_extra), + ) + self.errors.append(error.to_representation()) + self.dbsession.add(error) + + def _handle_larq_already_calculated(self, larq: LabelAnalysisRequest): + # This means we already calculated everything + # Except possibly the absent labels + log.info( + "Label analysis request was already calculated", + extra=dict( + request_id=larq.id, + external_id=larq.external_id, + commit=larq.head_commit.commitid, + ), + ) + if larq.requested_labels: + saved_result = larq.result + all_saved_labels = set( + saved_result.get("present_report_labels", []) + + saved_result.get("present_diff_labels", []) + + saved_result.get("global_level_labels", []) + ) + executable_lines_saved_labels = set( + saved_result.get("present_diff_labels", []) + ) + global_saved_labels = set(saved_result.get("global_level_labels", [])) + result = self.calculate_final_result( + requested_labels=larq.requested_labels, + existing_labels=ExistingLabelSetsNotEncoded( + all_saved_labels, executable_lines_saved_labels, global_saved_labels + ), + commit_sha=larq.head_commit.commitid, + ) + larq.result = result # Save the new result + metrics.incr("label_analysis_task.already_calculated.new_result") + return {**result, "success": True, "errors": []} + # No requested labels mean we don't have any new information + # So we don't need to calculate again + # This shouldn't actually happen + metrics.incr("label_analysis_task.already_calculated.same_result") + return {**larq.result, "success": True, "errors": []} + + def _lookup_label_ids(self, report: Report, label_ids: Set[int]) -> Set[str]: + labels: Set[str] = set() + for label_id in label_ids: + # This can raise shared.reports.exceptions.LabelNotFoundError + # But (1) we shouldn't let that happen and (2) there's no recovering from it + # So we should let that happen to surface bugs to us + labels.add(report.lookup_label_by_id(label_id)) + return labels + + def _get_requested_labels(self, label_analysis_request: LabelAnalysisRequest): + if label_analysis_request.requested_labels: + return label_analysis_request.requested_labels + # This is the case where the CLI PATCH the requested labels after collecting them + self.dbsession.refresh(label_analysis_request, ["requested_labels"]) + return label_analysis_request.requested_labels + + @sentry_sdk.trace + def _get_existing_labels( + self, report: Report, lines_relevant_to_diff: LinesRelevantToChange + ) -> ExistingLabelSets: + all_report_labels = self.get_all_report_labels(report) + ( + executable_lines_labels, + global_level_labels, + ) = self.get_executable_lines_labels(report, lines_relevant_to_diff) + + if len(all_report_labels) > 0: + # Check if report labels are encoded or not + test_label = all_report_labels.pop() + are_labels_encoded = isinstance(test_label, int) + all_report_labels.add(test_label) + else: + # There are no labels in the report + are_labels_encoded = False + + class_to_use = ( + ExistingLabelSetsEncoded + if are_labels_encoded + else ExistingLabelSetsNotEncoded + ) + + return class_to_use( + all_report_labels=all_report_labels, + executable_lines_labels=executable_lines_labels, + global_level_labels=global_level_labels, + ) + + @sentry_sdk.trace + def _get_lines_relevant_to_diff(self, label_analysis_request: LabelAnalysisRequest): + parsed_git_diff = self._get_parsed_git_diff(label_analysis_request) + if parsed_git_diff: + executable_lines_relevant_to_diff = self.get_relevant_executable_lines( + label_analysis_request, parsed_git_diff + ) + # This line will be useful for debugging + # And to tweak the heuristics + log.info( + "Lines relevant to diff", + extra=dict( + lines_relevant_to_diff=executable_lines_relevant_to_diff, + commit=label_analysis_request.head_commit.commitid, + external_id=label_analysis_request.external_id, + request_id=label_analysis_request.id_, + ), + ) + return executable_lines_relevant_to_diff + return None + + @sentry_sdk.trace + def _get_parsed_git_diff( + self, label_analysis_request: LabelAnalysisRequest + ) -> Optional[List[DiffChange]]: + try: + repo_service = get_repo_provider_service( + label_analysis_request.head_commit.repository + ) + git_diff = async_to_sync(repo_service.get_compare)( + label_analysis_request.base_commit.commitid, + label_analysis_request.head_commit.commitid, + ) + return list(parse_git_diff_json(git_diff)) + except Exception: + # temporary general catch while we find possible problems on this + log.exception( + "Label analysis failed to parse git diff", + extra=dict( + request_id=label_analysis_request.id, + external_id=label_analysis_request.external_id, + commit=label_analysis_request.head_commit.commitid, + ), + ) + self.add_processing_error( + larq_id=label_analysis_request.id, + error_code=LabelAnalysisProcessingErrorCode.FAILED, + error_msg="Failed to parse git diff", + error_extra=dict( + head_commit=label_analysis_request.head_commit.commitid, + base_commit=label_analysis_request.base_commit.commitid, + ), + ) + return None + + @sentry_sdk.trace + def _get_base_report( + self, label_analysis_request: LabelAnalysisRequest + ) -> Optional[Report]: + base_commit = label_analysis_request.base_commit + current_yaml = get_repo_yaml(base_commit.repository) + report_service = ReportService(current_yaml) + report: Report = report_service.get_existing_report_for_commit(base_commit) + if report is None: + log.warning( + "No report found for label analysis", + extra=dict( + request_id=label_analysis_request.id, + commit=label_analysis_request.head_commit.commitid, + ), + ) + self.add_processing_error( + larq_id=label_analysis_request.id, + error_code=LabelAnalysisProcessingErrorCode.MISSING_DATA, + error_msg="Missing base report", + error_extra=dict( + head_commit=label_analysis_request.head_commit.commitid, + base_commit=label_analysis_request.base_commit.commitid, + ), + ) + return report + + @sentry_sdk.trace + def calculate_final_result( + self, + *, + requested_labels: Optional[List[str]], + existing_labels: ExistingLabelSetsNotEncoded, + commit_sha: str, + ): + all_report_labels = existing_labels.all_report_labels + executable_lines_labels = existing_labels.executable_lines_labels + global_level_labels = existing_labels.global_level_labels + log.info( + "Final info", + extra=dict( + executable_lines_labels=sorted(executable_lines_labels), + all_report_labels=all_report_labels, + requested_labels=requested_labels, + global_level_labels=sorted(global_level_labels), + commit=commit_sha, + ), + ) + if requested_labels is not None: + requested_labels = set(requested_labels) + ans = { + "present_report_labels": sorted(all_report_labels & requested_labels), + "present_diff_labels": sorted( + executable_lines_labels & requested_labels + ), + "absent_labels": sorted(requested_labels - all_report_labels), + "global_level_labels": sorted(global_level_labels & requested_labels), + } + return ans + return { + "present_report_labels": sorted(all_report_labels), + "present_diff_labels": sorted(executable_lines_labels), + "absent_labels": [], + "global_level_labels": sorted(global_level_labels), + } + + @sentry_sdk.trace + def get_relevant_executable_lines( + self, label_analysis_request: LabelAnalysisRequest, parsed_git_diff + ): + db_session = label_analysis_request.get_db_session() + base_static_analysis: StaticAnalysisSuite = ( + db_session.query(StaticAnalysisSuite) + .filter( + StaticAnalysisSuite.commit_id == label_analysis_request.base_commit_id, + ) + .first() + ) + head_static_analysis: StaticAnalysisSuite = ( + db_session.query(StaticAnalysisSuite) + .filter( + StaticAnalysisSuite.commit_id == label_analysis_request.head_commit_id, + ) + .first() + ) + if not base_static_analysis or not head_static_analysis: + # TODO : Proper handling of this case + log.info( + "Trying to make prediction where there are no static analyses", + extra=dict( + base_static_analysis=base_static_analysis.id_ + if base_static_analysis is not None + else None, + head_static_analysis=head_static_analysis.id_ + if head_static_analysis is not None + else None, + commit=label_analysis_request.head_commit.commitid, + ), + ) + self.add_processing_error( + larq_id=label_analysis_request.id, + error_code=LabelAnalysisProcessingErrorCode.MISSING_DATA, + error_msg="Missing static analysis info", + error_extra=dict( + head_commit=label_analysis_request.head_commit.commitid, + base_commit=label_analysis_request.base_commit.commitid, + has_base_static_analysis=(base_static_analysis is not None), + has_head_static_analysis=(head_static_analysis is not None), + ), + ) + return None + static_analysis_comparison_service = StaticAnalysisComparisonService( + base_static_analysis, + head_static_analysis, + parsed_git_diff, + ) + return static_analysis_comparison_service.get_base_lines_relevant_to_change() + + @sentry_sdk.trace + def get_executable_lines_labels( + self, report: Report, executable_lines: LinesRelevantToChange + ) -> Tuple[PossiblyEncodedLabelSet, PossiblyEncodedLabelSet]: + if executable_lines["all"]: + return (self.get_all_report_labels(report), set()) + full_sessions = set() + labels: PossiblyEncodedLabelSet = set() + global_level_labels = set() + # Prime piece of code to be rust-ifyied + for name, file_executable_lines in executable_lines["files"].items(): + rf = report.get(name) + if rf and file_executable_lines: + if file_executable_lines["all"]: + for line_number, line in rf.lines: + if line and line.datapoints: + for datapoint in line.datapoints: + dp_labels = datapoint.label_ids or [] + labels.update(dp_labels) + if ( + # If labels are encoded + GLOBAL_LEVEL_LABEL_IDX in dp_labels + # If labels are NOT encoded + or GLOBAL_LEVEL_LABEL in dp_labels + ): + full_sessions.add(datapoint.sessionid) + else: + for line_number in file_executable_lines["lines"]: + line = rf.get(line_number) + if line and line.datapoints: + for datapoint in line.datapoints: + dp_labels = datapoint.label_ids or [] + labels.update(dp_labels) + if ( + # If labels are encoded + GLOBAL_LEVEL_LABEL_IDX in dp_labels + # If labels are NOT encoded + or GLOBAL_LEVEL_LABEL in dp_labels + ): + full_sessions.add(datapoint.sessionid) + for sess_id in full_sessions: + global_level_labels.update(self.get_labels_per_session(report, sess_id)) + return ( + labels - set([GLOBAL_LEVEL_LABEL_IDX, GLOBAL_LEVEL_LABEL]), + global_level_labels, + ) + + def get_labels_per_session(self, report: Report, sess_id: int): + return get_labels_per_session(report, sess_id) + + def get_all_report_labels(self, report: Report) -> set: + return get_all_report_labels(report) + + +RegisteredLabelAnalysisRequestProcessingTask = celery_app.register_task( + LabelAnalysisRequestProcessingTask() +) +label_analysis_task = celery_app.tasks[ + RegisteredLabelAnalysisRequestProcessingTask.name +] diff --git a/apps/worker/tasks/manual_trigger.py b/apps/worker/tasks/manual_trigger.py new file mode 100644 index 0000000000..fc50c6efec --- /dev/null +++ b/apps/worker/tasks/manual_trigger.py @@ -0,0 +1,189 @@ +import logging + +from celery.exceptions import MaxRetriesExceededError +from redis.exceptions import LockError +from shared.celery_config import ( + compute_comparison_task_name, + manual_upload_completion_trigger_task_name, + notify_task_name, + pulls_task_name, +) +from shared.helpers.redis import get_redis_connection +from shared.reports.enums import UploadState + +from app import celery_app +from database.enums import ReportType +from database.models import Commit, Pull +from database.models.reports import CommitReport, Upload +from services.comparison import get_or_create_comparison +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class ManualTriggerTask( + BaseCodecovTask, name=manual_upload_completion_trigger_task_name +): + def run_impl( + self, + db_session, + *, + repoid: int, + commitid: str, + report_code: str, + current_yaml=None, + **kwargs, + ): + log.info( + "Received manual trigger task", + extra=dict(repoid=repoid, commit=commitid, report_code=report_code), + ) + repoid = int(repoid) + lock_name = f"manual_trigger_lock_{repoid}_{commitid}" + redis_connection = get_redis_connection() + try: + with redis_connection.lock( + lock_name, + timeout=60 * 5, + blocking_timeout=5, + ): + return self.process_impl_within_lock( + db_session=db_session, + repoid=repoid, + commitid=commitid, + commit_yaml=current_yaml, + report_code=report_code, + **kwargs, + ) + except LockError: + log.warning( + "Unable to acquire lock", + extra=dict( + commit=commitid, + repoid=repoid, + number_retries=self.request.retries, + lock_name=lock_name, + ), + ) + return {"notifications_called": False, "message": "Unable to acquire lock"} + + def process_impl_within_lock( + self, + *, + db_session, + repoid, + commitid, + commit_yaml, + report_code, + **kwargs, + ): + commit = ( + db_session.query(Commit) + .filter( + Commit.repoid == repoid, + Commit.commitid == commitid, + ) + .first() + ) + uploads = ( + db_session.query(Upload) + .join(CommitReport) + .filter( + CommitReport.code == report_code, + CommitReport.commit == commit, + (CommitReport.report_type == None) # noqa: E711 + | (CommitReport.report_type == ReportType.COVERAGE.value), + ) + ) + still_processing = 0 + for upload in uploads: + if not upload.state or upload.state_id == UploadState.UPLOADED.db_id: + still_processing += 1 + if still_processing == 0: + self.trigger_notifications(repoid, commitid, commit_yaml) + if commit.pullid: + self.trigger_pull_sync(db_session, repoid, commit) + return { + "notifications_called": True, + "message": "All uploads are processed. Triggering notifications.", + } + else: + # reschedule the task + try: + log.info( + "Retrying ManualTriggerTask. Some uploads are still being processed." + ) + retry_in = 60 * 3**self.request.retries + self.retry(max_retries=5, countdown=retry_in) + except MaxRetriesExceededError: + log.warning( + "Not attempting to wait for all uploads to get processed since we already retried too many times", + extra=dict( + repoid=commit.repoid, + commit=commit.commitid, + max_retries=5, + next_countdown_would_be=retry_in, + ), + ) + return { + "notifications_called": False, + "message": "Uploads are still in process and the task got retired so many times. Not triggering notifications.", + } + + def trigger_notifications(self, repoid, commitid, commit_yaml): + log.info( + "Scheduling notify task", + extra=dict( + repoid=repoid, + commit=commitid, + commit_yaml=commit_yaml.to_dict() if commit_yaml else None, + ), + ) + self.app.tasks[notify_task_name].apply_async( + kwargs=dict( + repoid=repoid, + commitid=commitid, + current_yaml=commit_yaml.to_dict() if commit_yaml else None, + ) + ) + + def trigger_pull_sync(self, db_session, repoid, commit): + pull = ( + db_session.query(Pull) + .filter_by(repoid=commit.repoid, pullid=commit.pullid) + .first() + ) + + if pull: + head = pull.get_head_commit() + if head is None or head.timestamp <= commit.timestamp: + pull.head = commit.commitid + if pull.head == commit.commitid: + db_session.commit() + log.info( + "Scheduling pulls syc task", + extra=dict( + repoid=repoid, + pullid=pull.pullid, + ), + ) + self.app.tasks[pulls_task_name].apply_async( + kwargs=dict( + repoid=repoid, + pullid=pull.pullid, + should_send_notifications=False, + ) + ) + compared_to = pull.get_comparedto_commit() + if compared_to: + comparison = get_or_create_comparison( + db_session, compared_to, commit + ) + db_session.commit() + self.app.tasks[compute_comparison_task_name].apply_async( + kwargs=dict(comparison_id=comparison.id) + ) + + +RegisteredManualTriggerTask = celery_app.register_task(ManualTriggerTask()) +manual_trigger_task = celery_app.tasks[RegisteredManualTriggerTask.name] diff --git a/apps/worker/tasks/new_user_activated.py b/apps/worker/tasks/new_user_activated.py new file mode 100644 index 0000000000..02eb427d6b --- /dev/null +++ b/apps/worker/tasks/new_user_activated.py @@ -0,0 +1,139 @@ +import logging +from datetime import datetime, timedelta +from typing import Iterator + +from shared.celery_config import new_user_activated_task_name, notify_task_name +from shared.plan.service import PlanService + +from app import celery_app +from database.enums import Decoration +from database.models import Owner, Pull, Repository +from helpers.metrics import metrics +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class NewUserActivatedTask(BaseCodecovTask, name=new_user_activated_task_name): + """ + This task resends notifications for pull requests that were authored by a newly activated + user for an org that is on a PR-author billing plan ('users-pr-inapp*'). We do this so that + pulls that received "ugprade" decoration will now be updated with the standard decoration. + + The steps are: + - Ensure we are dealing with an activation for an org on a PR-author based plan + - Get `pull` entries authored by the user that meet the following criteria: + - Pull is for a repo owned by the provided `org_ownerid` + - Pull is in the 'open' state + - Pull was updated within the previous 10 days (Note: the `pull` table does NOT have + a createstamp so we have to go by the updatestamp for now) + - Schedule notify tasks to run again for the pulls that previously ran with just the + "upgrade" decoration + """ + + def run_impl(self, db_session, org_ownerid, user_ownerid, *args, **kwargs): + log.info( + "New user activated", + extra=dict(org_ownerid=org_ownerid, user_ownerid=user_ownerid), + ) + pulls_notified = [] + + if not self.is_org_on_pr_plan(db_session, org_ownerid): + return { + "notifies_scheduled": False, + "pulls_notified": pulls_notified, + "reason": "org not on pr author billing plan", + } + + pulls = self.get_pulls_authored_by_user(db_session, org_ownerid, user_ownerid) + + # NOTE: we could also notify through pulls_sync task but we will notify directly here + for pull in pulls: + pull_commit_notifications = pull.get_head_commit_notifications() + + if not pull_commit_notifications: + # don't know decoration type used so skip + log.info( + "Skipping pull", + extra=dict( + org_ownerid=org_ownerid, + user_ownerid=user_ownerid, + repoid=pull.repoid, + pullid=pull.pullid, + ), + ) + continue + + if self.possibly_resend_notifications(pull_commit_notifications, pull): + pulls_notified.append( + dict(repoid=pull.repoid, pullid=pull.pullid, commitid=pull.head) + ) + + return { + "notifies_scheduled": bool(pulls_notified), + "pulls_notified": pulls_notified, + "reason": None + if pulls_notified + else "no pulls/pull notifications met criteria", + } + + def is_org_on_pr_plan(self, db_session, ownerid: int) -> bool: + owner = db_session.query(Owner).filter(Owner.ownerid == ownerid).first() + + if not owner: + log.info("Org not found", extra=dict(org_ownerid=ownerid)) + return False + + # do not access plan directly - only through PlanService + plan = PlanService(current_org=owner) + + return plan.is_pr_billing_plan + + @metrics.timer("worker.task.new_user_activated.get_pulls_authored_by_user") + def get_pulls_authored_by_user( + self, db_session, org_ownerid: int, user_ownerid: int + ) -> Iterator[Pull]: + ten_days_ago = datetime.now() - timedelta(days=10) + + pulls = ( + db_session.query(Pull) + .join(Pull.repository) + .join(Repository.owner) + .filter( + Pull.updatestamp > ten_days_ago, + Repository.ownerid == org_ownerid, + Pull.author_id == user_ownerid, + Pull.state == "open", + ) + .all() + ) + + return pulls + + def possibly_resend_notifications( + self, pull_commit_notifications, pull: Pull + ) -> bool: + was_notification_scheduled = False + should_notify = any( + commit_notification.decoration_type == Decoration.upgrade + for commit_notification in pull_commit_notifications + ) + + if should_notify: + repoid = pull.repoid + pullid = pull.pullid + commitid = pull.head + log.info( + "Scheduling notify task", + extra=dict(repoid=repoid, pullid=pullid, commitid=pull.head), + ) + self.app.tasks[notify_task_name].apply_async( + kwargs=dict(repoid=repoid, commitid=pull.head) + ) + was_notification_scheduled = True + + return was_notification_scheduled + + +RegisteredNewUserActivatedTask = celery_app.register_task(NewUserActivatedTask()) +new_user_activated_task = celery_app.tasks[NewUserActivatedTask.name] diff --git a/apps/worker/tasks/notify.py b/apps/worker/tasks/notify.py new file mode 100644 index 0000000000..1f80aa2c62 --- /dev/null +++ b/apps/worker/tasks/notify.py @@ -0,0 +1,932 @@ +import logging +from typing import Optional + +import sentry_sdk +from asgiref.sync import async_to_sync +from celery.exceptions import MaxRetriesExceededError, SoftTimeLimitExceeded +from shared.bots.github_apps import ( + get_github_app_token, + get_specific_github_app_details, +) +from shared.celery_config import ( + activate_account_user_task_name, + new_user_activated_task_name, + notify_task_name, + status_set_error_task_name, +) +from shared.config import get_config +from shared.django_apps.codecov_auth.models import Service +from shared.helpers.redis import Redis, get_redis_connection +from shared.reports.readonly import ReadOnlyReport +from shared.torngit.base import TokenType, TorngitBaseAdapter +from shared.torngit.exceptions import TorngitClientError, TorngitServerFailureError +from shared.typings.torngit import OwnerInfo, RepoInfo, TorngitInstanceData +from shared.yaml import UserYaml +from sqlalchemy import and_ +from sqlalchemy.orm.session import Session + +from app import celery_app +from database.enums import CommitErrorTypes, Decoration, NotificationState, ReportType +from database.models import ( + Commit, + CommitReport, + Pull, + TestResultReportTotals, + Upload, + UploadError, +) +from database.models.core import GITHUB_APP_INSTALLATION_DEFAULT_NAME, CompareCommit +from helpers.checkpoint_logger.flows import UploadFlow +from helpers.clock import get_seconds_to_next_hour +from helpers.comparison import minimal_totals +from helpers.exceptions import NoConfiguredAppsAvailable, RepositoryWithoutValidBotError +from helpers.github_installation import get_installation_name_for_owner_for_task +from helpers.save_commit_error import save_commit_error +from services.activation import activate_user +from services.commit_status import RepositoryCIFilter +from services.comparison import ( + ComparisonContext, + ComparisonProxy, + get_or_create_comparison, +) +from services.comparison.types import Comparison, FullCommit +from services.decoration import determine_decoration_details +from services.github import get_github_app_for_commit, set_github_app_for_commit +from services.lock_manager import LockManager, LockRetry, LockType +from services.notification import NotificationService +from services.report import ReportService +from services.repository import ( + EnrichedPull, + _get_repo_provider_service_instance, + fetch_and_update_pull_request_information_from_commit, + get_repo_provider_service, +) +from services.yaml import get_current_yaml, read_yaml_field +from tasks.base import BaseCodecovTask +from tasks.upload_processor import UPLOAD_PROCESSING_LOCK_NAME + +log = logging.getLogger(__name__) + +GENERIC_TA_ERROR_MSG = ":x: We are unable to process any of the uploaded JUnit XML files. Please ensure your files are in the right format." + + +class NotifyTask(BaseCodecovTask, name=notify_task_name): + throws = (SoftTimeLimitExceeded,) + + def run_impl( + self, + db_session: Session, + *, + repoid: int, + commitid: str, + current_yaml=None, + empty_upload=None, + **kwargs, + ): + redis_connection = get_redis_connection() + if self.has_upcoming_notifies_according_to_redis( + redis_connection, repoid, commitid + ): + log.info( + "Not notifying because there are seemingly other jobs being processed yet", + extra=dict(repoid=repoid, commitid=commitid), + ) + self.log_checkpoint(UploadFlow.SKIPPING_NOTIFICATION) + return { + "notified": False, + "notifications": None, + "reason": "has_other_notifies_coming", + } + + lock_manager = LockManager( + repoid=repoid, + commitid=commitid, + report_type=ReportType.COVERAGE, + lock_timeout=max(80, self.hard_time_limit_task), + ) + + try: + lock_acquired = False + with lock_manager.locked( + lock_type=LockType.NOTIFICATION, retry_num=self.request.retries + ): + lock_acquired = True + return self.run_impl_within_lock( + db_session, + repoid=repoid, + commitid=commitid, + current_yaml=current_yaml, + empty_upload=empty_upload, + **kwargs, + ) + except LockRetry as err: + ( + log.info( + "Not notifying because there is another notification already happening", + extra=dict( + repoid=repoid, + commitid=commitid, + error_type=type(err), + lock_acquired=lock_acquired, + ), + ), + ) + self.log_checkpoint(UploadFlow.NOTIF_LOCK_ERROR) + return { + "notified": False, + "notifications": None, + "reason": "unobtainable_lock", + } + + def log_checkpoint(self, checkpoint): + """ + Only log a checkpoint if whoever scheduled us sent checkpoints data from + the same flow. + + The notify task is an important part of `UploadFlow`, but it's also used + elsewhere. If this instance of the notify task wasn't scheduled as part + of upload processing, attempting to log `UploadFlow` checkpoints for it + will pollute our metrics. + """ + if UploadFlow.has_begun(): + UploadFlow.log(checkpoint) + + def _attempt_retry( + self, + max_retries: int, + countdown: int, + commit: Commit, + current_yaml: Optional[UserYaml], + *args, + **kwargs, + ) -> None: + try: + self.retry(max_retries=max_retries, countdown=countdown) + except MaxRetriesExceededError: + log.warning( + "Not attempting to retry notifications since we already retried too many times", + extra=dict( + repoid=commit.repoid, + commit=commit.commitid, + max_retries=max_retries, + next_countdown_would_be=countdown, + current_yaml=current_yaml.to_dict(), + ), + ) + self.log_checkpoint(UploadFlow.NOTIF_TOO_MANY_RETRIES) + return { + "notified": False, + "notifications": None, + "reason": "too_many_retries", + } + + def run_impl_within_lock( + self, + db_session: Session, + *, + repoid: int, + commitid: str, + current_yaml=None, + empty_upload=None, + **kwargs, + ): + log.info("Starting notifications", extra=dict(commit=commitid, repoid=repoid)) + commits_query = db_session.query(Commit).filter( + Commit.repoid == repoid, Commit.commitid == commitid + ) + commit: Commit = commits_query.first() + assert commit, "Commit not found in database." + + test_result_commit_report = commit.commit_report(ReportType.TEST_RESULTS) + if ( + test_result_commit_report is not None + and test_result_commit_report.test_result_totals is not None + and not test_result_commit_report.test_result_totals.error + and test_result_commit_report.test_result_totals.failed > 0 + ): + return { + "notify_attempted": False, + "notifications": None, + "reason": "test_failures", + } + + try: + installation_name_to_use = get_installation_name_for_owner_for_task( + self.name, commit.repository.owner + ) + repository_service = get_repo_provider_service_for_specific_commit( + commit, installation_name_to_use + ) + except RepositoryWithoutValidBotError: + save_commit_error( + commit, error_code=CommitErrorTypes.REPO_BOT_INVALID.value + ) + + log.warning( + "Unable to start notifications because repo doesn't have a valid bot", + extra=dict(repoid=repoid, commit=commitid), + ) + self.log_checkpoint(UploadFlow.NOTIF_NO_VALID_INTEGRATION) + return {"notified": False, "notifications": None, "reason": "no_valid_bot"} + except NoConfiguredAppsAvailable as exp: + if exp.rate_limited_count > 0: + # There's at least 1 app that we can use to communicate with GitHub, + # but this app happens to be rate limited now. We try again later. + # Min wait time of 1 minute + retry_delay_seconds = max(60, get_seconds_to_next_hour()) + log.warning( + "Unable to start notifications. Retrying again later.", + extra=dict( + repoid=repoid, + commit=commitid, + apps_available=exp.apps_count, + apps_rate_limited=exp.rate_limited_count, + apps_suspended=exp.suspended_count, + countdown_seconds=retry_delay_seconds, + ), + ) + return self._attempt_retry( + max_retries=10, + countdown=retry_delay_seconds, + current_yaml=current_yaml, + commit=commit, + **kwargs, + ) + # Maybe we have apps that are suspended. We can't communicate with github. + log.warning( + "We can't find an app to communicate with GitHub. Not notifying.", + extra=dict( + repoid=repoid, + commit=commitid, + apps_available=exp.apps_count, + apps_suspended=exp.suspended_count, + ), + ) + self.log_checkpoint(UploadFlow.NOTIF_NO_APP_INSTALLATION) + return { + "notified": False, + "notifications": None, + "reason": "no_valid_github_app_found", + } + + if current_yaml is None: + current_yaml = async_to_sync(get_current_yaml)(commit, repository_service) + else: + current_yaml = UserYaml.from_dict(current_yaml) + + try: + ci_results = self.fetch_and_update_whether_ci_passed( + repository_service, commit, current_yaml + ) + except TorngitClientError as ex: + log.info( + "Unable to fetch CI results due to a client problem. Not notifying user", + extra=dict(repoid=commit.repoid, commit=commit.commitid, code=ex.code), + ) + self.log_checkpoint(UploadFlow.NOTIF_GIT_CLIENT_ERROR) + return { + "notified": False, + "notifications": None, + "reason": "not_able_fetch_ci_result", + } + except TorngitServerFailureError: + log.info( + "Unable to fetch CI results due to server issues. Not notifying user", + extra=dict(repoid=commit.repoid, commit=commit.commitid), + ) + self.log_checkpoint(UploadFlow.NOTIF_GIT_SERVICE_ERROR) + return { + "notified": False, + "notifications": None, + "reason": "server_issues_ci_result", + } + if self.should_wait_longer(current_yaml, commit, ci_results): + log.info( + "Not sending notifications yet because we are waiting for CI to finish", + extra=dict(repoid=commit.repoid, commit=commit.commitid), + ) + ghapp_default_installations = list( + filter( + lambda obj: obj.name == installation_name_to_use + and obj.is_configured(), + commit.repository.owner.github_app_installations or [], + ) + ) + rely_on_webhook_ghapp = ghapp_default_installations != [] and any( + obj.is_repo_covered_by_integration(commit.repository) + for obj in ghapp_default_installations + ) + rely_on_webhook_legacy = commit.repository.using_integration + if ( + rely_on_webhook_ghapp + or rely_on_webhook_legacy + or commit.repository.hookid + ): + # rely on the webhook, but still retry in case we miss the webhook + max_retries = 5 + countdown = (60 * 3) * 2**self.request.retries + else: + max_retries = 10 + countdown = 15 * 2**self.request.retries + return self._attempt_retry( + max_retries=max_retries, + countdown=countdown, + current_yaml=current_yaml, + commit=commit, + **kwargs, + ) + + report_service = ReportService( + current_yaml, gh_app_installation_name=installation_name_to_use + ) + head_report = report_service.get_existing_report_for_commit( + commit, report_class=ReadOnlyReport + ) + if self.should_send_notifications( + current_yaml, commit, ci_results, head_report + ): + enriched_pull = async_to_sync( + fetch_and_update_pull_request_information_from_commit + )(repository_service, commit, current_yaml) + if enriched_pull and enriched_pull.database_pull: + pull = enriched_pull.database_pull + base_commit = self.fetch_pull_request_base(pull) + else: + pull = None + base_commit = self.fetch_parent(commit) + + if ( + enriched_pull + and not self.send_notifications_if_commit_differs_from_pulls_head( + commit, enriched_pull, current_yaml + ) + and empty_upload is None + ): + log.info( + "Not sending notifications for commit when it differs from pull's most recent head", + extra=dict( + commit=commit.commitid, + repoid=commit.repoid, + current_yaml=current_yaml.to_dict(), + pull_head=enriched_pull.provider_pull["head"]["commitid"], + ), + ) + self.log_checkpoint(UploadFlow.NOTIF_STALE_HEAD) + return { + "notified": False, + "notifications": None, + "reason": "User doesnt want notifications warning them that current head differs from pull request most recent head.", + } + + if base_commit is not None: + base_report = report_service.get_existing_report_for_commit( + base_commit, report_class=ReadOnlyReport + ) + else: + base_report = None + if head_report is None and empty_upload is None: + self.log_checkpoint(UploadFlow.NOTIF_ERROR_NO_REPORT) + return { + "notified": False, + "notifications": None, + "reason": "no_head_report", + } + + if commit.repository.service == "gitlab": + gitlab_extra_shas_to_notify = self.get_gitlab_extra_shas_to_notify( + commit, repository_service + ) + else: + gitlab_extra_shas_to_notify = None + + log.info( + "We are going to be sending notifications", + extra=dict( + commit=commit.commitid, + repoid=commit.repoid, + current_yaml=current_yaml.to_dict(), + ), + ) + + all_tests_passed, ta_error_msg = get_ta_relevant_context( + db_session, test_result_commit_report + ) + + notifications = self.submit_third_party_notifications( + current_yaml, + base_commit, + commit, + base_report, + head_report, + enriched_pull, + repository_service, + empty_upload, + all_tests_passed=all_tests_passed, + test_results_error=ta_error_msg, + installation_name_to_use=installation_name_to_use, + gh_is_using_codecov_commenter=self.is_using_codecov_commenter( + repository_service + ), + gitlab_extra_shas_to_notify=gitlab_extra_shas_to_notify, + ) + self.log_checkpoint(UploadFlow.NOTIFIED) + log.info( + "Notifications done", + extra=dict( + notifications=notifications, + notification_count=len(notifications), + commit=commit.commitid, + repoid=commit.repoid, + pullid=pull.pullid if pull is not None else None, + ), + ) + db_session.commit() + return {"notified": True, "notifications": notifications} + else: + log.info( + "Not sending notifications at all", + extra=dict(commit=commit.commitid, repoid=commit.repoid), + ) + self.log_checkpoint(UploadFlow.SKIPPING_NOTIFICATION) + return {"notified": False, "notifications": None} + + def is_using_codecov_commenter( + self, repository_service: TorngitBaseAdapter + ) -> bool: + """Returns a boolean indicating if the message will be sent by codecov-commenter. + If the user doesn't have an installation, and if the token type for the repo is codecov-commenter, + then it's likely that they're using the commenter bot. + """ + commenter_bot_token = get_config(repository_service.service, "bots", "comment") + return ( + repository_service.service == "github" + and repository_service.data.get("installation") is None + and commenter_bot_token is not None + and repository_service.get_token_by_type(TokenType.comment) + == commenter_bot_token + ) + + def has_upcoming_notifies_according_to_redis( + self, redis_connection: Redis, repoid: int, commitid: str + ) -> bool: + """Checks whether there are any jobs processing according to Redis right now and, + therefore, whether more up-to-date notifications will come after this anyway + + It's very important to have this code be conservative against saying + there are upcoming notifies already. The point of this code is to + avoid extra notifications for efficiency purposes, but it is better + to send extra notifications than to lack notifications + + Args: + redis_connection (Redis): The redis connection we check against + repoid (int): The repoid of the commit + commitid (str): The commitid of the commit + """ + upload_processing_lock_name = UPLOAD_PROCESSING_LOCK_NAME(repoid, commitid) + if redis_connection.get(upload_processing_lock_name): + return True + return False + + @sentry_sdk.trace + def save_patch_totals(self, comparison: ComparisonProxy) -> None: + """Saves patch coverage to the CompareCommit, if it exists. + This is done to make sure the patch coverage reported by notifications and UI is the same + (because they come from the same source) + """ + if ( + comparison.comparison.patch_coverage_base_commitid is None + or not comparison.has_head_report() + ): + # Can't get diff to calculate patch totals + return + head_commit = comparison.head.commit + db_session = head_commit.get_db_session() + if db_session is None: + log.warning("Failed to save patch_totals. dbsession is None") + return + patch_coverage = comparison.get_patch_totals() + if ( + comparison.project_coverage_base is not None + and comparison.project_coverage_base.commit is not None + ): + # Update existing Comparison + statement = ( + CompareCommit.__table__.update() + .where( + and_( + CompareCommit.compare_commit_id == head_commit.id, + CompareCommit.base_commit_id + == comparison.project_coverage_base.commit.id, + ) + ) + .values(patch_totals=minimal_totals(patch_coverage)) + ) + db_session.execute(statement) + elif ( + patch_coverage is not None + and comparison.comparison.patch_coverage_base_commitid is not None + ): + # We calculated patch coverage, but there's no project base + # So we will create a comparison to save the patch_totals, to make sure + # the UI and the PR have the same information + base_commit = ( + db_session.query(Commit) + .filter( + Commit.commitid + == comparison.comparison.patch_coverage_base_commitid, + Commit.repository == head_commit.repository, + ) + .first() + ) + if base_commit: + compare_commit = get_or_create_comparison( + db_session, base_commit, head_commit + ) + compare_commit.patch_totals = minimal_totals(patch_coverage) + + db_session.commit() + + @sentry_sdk.trace + def get_gitlab_extra_shas_to_notify( + self, commit: Commit, repository_service: TorngitBaseAdapter + ) -> set[str]: + """ " + Fetches extra commit SHAs we should send statuses too for GitLab. + + GitLab has a "merge results pipeline" (see https://docs.gitlab.com/ee/ci/pipelines/merged_results_pipelines.html) + This runs on an "internal" commit that is the merge from the PR HEAD and the target branch. This commit only exists in GitLab. + We need to send commit statuses to this other commit, to guarantee that the check is not ignored (see https://docs.gitlab.com/ee/ci/pipelines/merged_results_pipelines.html#successful-merged-results-pipeline-overrides-a-failed-branch-pipeline) + """ + log.info( + "Checking if we need to send notification to more commits", + extra=dict(commit=commit.commitid), + ) + report = commit.commit_report(ReportType.COVERAGE) + if report is None: + log.info( + "No coverage report found. Skipping extra shas for GitLab", + extra=dict(commit=commit.commitid), + ) + return set() + project_id = commit.repository.service_id + job_ids = ( + upload.job_code for upload in report.uploads if upload.job_code is not None + ) + _get_pipeline_details = async_to_sync(repository_service.get_pipeline_details) + results = [_get_pipeline_details(project_id, job_id) for job_id in job_ids] + return set( + filter(lambda sha: sha is not None and sha != commit.commitid, results) + ) + + @sentry_sdk.trace + def submit_third_party_notifications( + self, + current_yaml: UserYaml, + base_commit: Commit | None, + commit: Commit, + base_report: ReadOnlyReport | None, + head_report: ReadOnlyReport | None, + enriched_pull: EnrichedPull, + repository_service: TorngitBaseAdapter, + empty_upload=None, + all_tests_passed: bool = False, + test_results_error: str | None = None, + installation_name_to_use: str = GITHUB_APP_INSTALLATION_DEFAULT_NAME, + gh_is_using_codecov_commenter: bool = False, + gitlab_extra_shas_to_notify: set[str] | None = None, + ): + # base_commit is an "adjusted" base commit; for project coverage, we + # compare a PR head's report against its base's report, or if the base + # doesn't exist in our database, the next-oldest commit that does. That + # is unnecessary/incorrect for patch coverage, for which we want to + # compare against the original PR base. + pull = enriched_pull.database_pull if enriched_pull else None + if pull: + patch_coverage_base_commitid = pull.base + elif base_commit is not None: + patch_coverage_base_commitid = base_commit.commitid + else: + log.warning( + "Neither the original nor updated base commit are known", + extra=dict(repoid=commit.repository.repoid, commit=commit.commitid), + ) + patch_coverage_base_commitid = None + + # FIXME: Both the `commit` as well as the `report` on `FullCommit` + # (both `head` and `project_coverage_base`) are declared to be non-`None`. + # Though you will see type errors below because they indeed can be `None`. + # Downstream code seems to be very fragile in this regard. + # Some code wrongly assumes things are non-`None` and will error. + # Other code checks `report` and then errors on `commit`. + + comparison = ComparisonProxy( + Comparison( + head=FullCommit(commit=commit, report=head_report), + project_coverage_base=FullCommit( + commit=base_commit, report=base_report + ), + patch_coverage_base_commitid=patch_coverage_base_commitid, + enriched_pull=enriched_pull, + current_yaml=current_yaml, + ), + context=ComparisonContext( + repository_service=repository_service, + all_tests_passed=all_tests_passed, + test_results_error=test_results_error, + gh_app_installation_name=installation_name_to_use, + gh_is_using_codecov_commenter=gh_is_using_codecov_commenter, + gitlab_extra_shas=gitlab_extra_shas_to_notify, + ), + ) + + self.save_patch_totals(comparison) + + decoration_type = self.determine_decoration_type_from_pull( + enriched_pull, empty_upload + ) + + notifications_service = NotificationService( + commit.repository, + current_yaml, + repository_service, + decoration_type, + gh_installation_name_to_use=installation_name_to_use, + ) + return notifications_service.notify(comparison) + + def send_notifications_if_commit_differs_from_pulls_head( + self, commit, enriched_pull, current_yaml + ): + if ( + enriched_pull.provider_pull is not None + and commit.commitid != enriched_pull.provider_pull["head"]["commitid"] + ): + wait_for_ci = read_yaml_field( + current_yaml, ("codecov", "notify", "wait_for_ci"), True + ) + manual_trigger = read_yaml_field( + current_yaml, ("codecov", "notify", "manual_trigger") + ) + after_n_builds = read_yaml_field( + current_yaml, ("codecov", "notify", "after_n_builds") + ) + if wait_for_ci or manual_trigger or after_n_builds: + return False + return True + + def fetch_pull_request_base(self, pull: Pull) -> Commit: + return pull.get_comparedto_commit() + + def fetch_parent(self, commit): + db_session = commit.get_db_session() + return ( + db_session.query(Commit) + .filter_by(commitid=commit.parent_commit_id, repoid=commit.repoid) + .first() + ) + + def should_send_notifications(self, current_yaml, commit, ci_passed, report): + if ( + read_yaml_field(current_yaml, ("codecov", "require_ci_to_pass"), True) + and ci_passed is False + ): + # we can exit, ci failed. + self.app.tasks[status_set_error_task_name].apply_async( + args=None, + kwargs=dict( + repoid=commit.repoid, commitid=commit.commitid, message="CI failed." + ), + ) + log.info( + "Not sending notifications because CI failed", + extra=dict(repoid=commit.repoid, commit=commit.commitid), + ) + return False + + # check the nuber of builds + after_n_builds = read_yaml_field( + current_yaml, ("codecov", "notify", "after_n_builds") + ) + if after_n_builds: + number_sessions = len(report.sessions) if report is not None else 0 + if after_n_builds > number_sessions: + log.info( + "Not sending notifications because there arent enough builds", + extra=dict( + repoid=commit.repoid, + commit=commit.commitid, + after_n_builds=after_n_builds, + number_sessions=number_sessions, + ), + ) + return False + return True + + def should_wait_longer(self, current_yaml, commit, ci_results): + return ( + read_yaml_field(current_yaml, ("codecov", "notify", "wait_for_ci"), True) + and ci_results is None + ) + + def determine_decoration_type_from_pull( + self, + enriched_pull: EnrichedPull, + empty_upload=None, + ) -> Decoration: + """ + Get and process decoration details and attempt auto activation if necessary + """ + decoration_details = determine_decoration_details(enriched_pull, empty_upload) + decoration_type = decoration_details.decoration_type + + if decoration_details.should_attempt_author_auto_activation: + successful_activation = activate_user( + enriched_pull.database_pull.get_db_session(), + decoration_details.activation_org_ownerid, + decoration_details.activation_author_ownerid, + ) + if successful_activation: + self.schedule_new_user_activated_task( + decoration_details.activation_org_ownerid, + decoration_details.activation_author_ownerid, + ) + decoration_type = Decoration.standard + return decoration_type + + def schedule_new_user_activated_task(self, org_ownerid, user_ownerid): + celery_app.send_task( + new_user_activated_task_name, + args=None, + kwargs=dict(org_ownerid=org_ownerid, user_ownerid=user_ownerid), + ) + # Activate the account user if it exists. + self.app.tasks[activate_account_user_task_name].apply_async( + kwargs=dict( + user_ownerid=user_ownerid, + org_ownerid=org_ownerid, + ), + ) + + @sentry_sdk.trace + def fetch_and_update_whether_ci_passed( + self, repository_service: TorngitBaseAdapter, commit, current_yaml + ): + all_statuses = async_to_sync(repository_service.get_commit_statuses)( + commit.commitid + ) + ci_state = all_statuses.filter(RepositoryCIFilter(current_yaml)) + if ci_state: + # cannot use instead of "codecov/*" because + # [bitbucket] appends the extra "codecov-" to the status + # which becomes "codecov-codecov/patch" + ci_state = ci_state - "codecov*" + ci_passed = ( + True if ci_state.is_success else False if ci_state.is_failure else None + ) + if ci_passed != commit.ci_passed: + commit.ci_passed = ci_passed + return ci_passed + + +RegisteredNotifyTask = celery_app.register_task(NotifyTask()) +notify_task = celery_app.tasks[RegisteredNotifyTask.name] + + +def _possibly_refresh_previous_selection(commit: Commit) -> bool: + installation_cached = get_github_app_for_commit(commit) + app_id_used_in_successful_comment: int | None = next( + ( + obj.gh_app_id + for obj in commit.notifications + if obj.gh_app_id is not None and obj.state == NotificationState.success + ), + None, + ) + if installation_cached or app_id_used_in_successful_comment: + id_to_cache = installation_cached or app_id_used_in_successful_comment + # Some app is already set for this commit, so we renew the caching of the app. + # It's OK if this app is not the same as the one chosen by torngit (argument), because the + # different notifiers have their own torngit adapter and will look at the pinned app first. + set_github_app_for_commit(id_to_cache, commit) + return True + return False + + +def _possibly_pin_commit_to_github_app( + commit: Commit, torngit: TorngitBaseAdapter +) -> int | str | None: + """Pin the GitHub app to use when emitting notifications for this commit, as needed. + + For non-GitHub, do nothing. + For situations that we don't use a GithubAppInstance to communicate, do nothing. + + If there is already an app cached in redis for this commit, OR a CommitNotification that was + successful with an app, renew that app's caching (it might be different from our selection, but that's ok) + + Returns: + the cached app's id (int | str | None) - to make it easier to test + """ + is_github = commit.repository.service in ["github", "github_enterprise"] + if not is_github: + return None + refreshed_previous_selection = _possibly_refresh_previous_selection(commit) + if refreshed_previous_selection: + # If a selection was already made we shouldn't overwrite it + return None + torngit_installation = torngit.data.get("installation") + selected_installation_id = ( + torngit_installation.get("id") if torngit_installation else None + ) + if selected_installation_id is not None: + # Here we pin our selection to be the app to use + set_github_app_for_commit(selected_installation_id, commit) + return selected_installation_id + return None + + +@sentry_sdk.trace +def get_repo_provider_service_for_specific_commit( + commit: Commit, + fallback_installation_name: str = GITHUB_APP_INSTALLATION_DEFAULT_NAME, +) -> TorngitBaseAdapter: + """Gets a Torngit adapter (potentially) using a specific GitHub app as the authentication source. + If the commit doesn't have a particular app assigned to it, return regular `get_repo_provider_service` choice + + This is done specifically after emitting checks for a PR using GitHub apps, because only the app + that posted the check can edit it later on. The "app for a commit" info is saved in Redis by the NotifyTask. + """ + repository = commit.repository + installation_for_commit = get_github_app_for_commit(commit) + if installation_for_commit is None: + repository_provider = get_repo_provider_service( + repository, fallback_installation_name + ) + _possibly_pin_commit_to_github_app(commit, repository_provider) + return repository_provider + + ghapp_details = get_specific_github_app_details( + repository.owner, int(installation_for_commit), commit.commitid + ) + token, _ = get_github_app_token(Service(repository.service), ghapp_details) + + data = TorngitInstanceData( + repo=RepoInfo( + name=repository.name, + using_integration=True, + service_id=repository.service_id, + repoid=repository.repoid, + ), + owner=OwnerInfo( + service_id=repository.owner.service_id, + ownerid=repository.ownerid, + username=repository.owner.username, + ), + installation=ghapp_details, + fallback_installations=None, + ) + + adapter_params = dict( + token=token, + token_type_mapping=None, + on_token_refresh=None, + **data, + ) + return _get_repo_provider_service_instance(repository.service, adapter_params) + + +def get_ta_relevant_context( + db_session: Session, ta_commit_report: CommitReport | None +) -> tuple[bool, str | None]: + all_tests_passed: bool = False + ta_error_msg: str | None = None + + if ta_commit_report: + ta_upload_ids = ( + db_session.query(Upload.id_) + .filter(Upload.report_id == ta_commit_report.id_) + .subquery() + ) + upload_error = ( + db_session.query(UploadError) + .filter(UploadError.upload_id.in_(ta_upload_ids)) + .first() + ) + + totals: TestResultReportTotals | None = ta_commit_report.test_result_totals + + if upload_error: + ta_error_msg = upload_error.error_params["error_message"] + elif totals and totals.error: + # this branch covers old behavior of setting the error on the totals + # TODO: remove this in the future + ta_error_msg = GENERIC_TA_ERROR_MSG + else: + ta_error_msg = None + + all_tests_passed = False + if totals: + no_error = ta_error_msg is None + no_test_failures = totals.failed == 0 + all_tests_passed = no_error and no_test_failures + + return all_tests_passed, ta_error_msg diff --git a/apps/worker/tasks/notify_error.py b/apps/worker/tasks/notify_error.py new file mode 100644 index 0000000000..8a7d01c330 --- /dev/null +++ b/apps/worker/tasks/notify_error.py @@ -0,0 +1,117 @@ +import logging +from dataclasses import dataclass + +from sqlalchemy.orm import Session + +from celery_config import notify_error_task_name +from database.enums import ReportType +from database.models import Commit, CommitReport, Upload +from helpers.checkpoint_logger.flows import UploadFlow +from helpers.notifier import BaseNotifier, NotifierResult +from services.yaml import UserYaml +from tasks.base import BaseCodecovTask, celery_app + +log = logging.getLogger(__name__) + + +@dataclass +class ErrorNotifier(BaseNotifier): + failed_upload: int = 0 + total_upload: int = 0 + + def build_message( + self, + ) -> str: + error_message = f"❗️ We couldn't process [{self.failed_upload}] out of [{self.total_upload}] uploads. Codecov cannot generate a coverage report with partially processed data. Please review the upload errors on the commit page." + return error_message + + +class NotifyErrorTask(BaseCodecovTask, name=notify_error_task_name): + def run_impl( + self, + db_session: Session, + *, + repoid: int, + commitid: str, + current_yaml=None, + **kwargs, + ): + log.info( + "Starting notify error task", + extra=dict(commit=commitid, repoid=repoid, commit_yaml=current_yaml), + ) + + # get all upload errors for this commit + commit_yaml = UserYaml.from_dict(current_yaml) + + commits_query = db_session.query(Commit).filter( # type:ignore + Commit.repoid == repoid, + Commit.commitid == commitid, # type:ignore + ) + + commit: Commit = commits_query.first() + assert commit, "Commit not found in database." + + report: CommitReport = commit.commit_report(ReportType.COVERAGE) # type:ignore + + uploads = db_session.query(Upload).filter(Upload.report_id == report.id).all() + + num_total_upload = len(uploads) + + def is_failed(upload): + if upload.state == "error": + return True + else: + return False + + num_failed_upload = len(list(filter(is_failed, list(uploads)))) + + log.info( + "Notifying upload errors", + extra=dict( + repoid=repoid, + commitid=commitid, + num_failed_upload=num_failed_upload, + num_total_upload=num_total_upload, + ), + ) + + error_notifier = ErrorNotifier( + commit, + commit_yaml, + failed_upload=num_failed_upload, + total_upload=num_total_upload, + ) + notification_result: NotifierResult = error_notifier.notify() + match notification_result: + case NotifierResult.COMMENT_POSTED: + log.info( + "Finished notify error task", + extra=dict( + commit=commitid, + repoid=repoid, + commit_yaml=current_yaml, + num_failed_upload=num_failed_upload, + num_total_upload=num_total_upload, + ), + ) + UploadFlow.log(UploadFlow.NOTIFIED_ERROR) + return {"success": True} + case NotifierResult.NO_PULL | NotifierResult.TORNGIT_ERROR: + UploadFlow.log(UploadFlow.ERROR_NOTIFYING_ERROR) + log.info( + "Failed to comment in notify error task", + extra=dict( + commit=commitid, + repoid=repoid, + commit_yaml=current_yaml, + num_failed_upload=num_failed_upload, + num_total_upload=num_total_upload, + notification_result=notification_result.value, + ), + ) + return {"success": False} + + +RegisteredNotifyErrorTask = celery_app.register_task(NotifyErrorTask()) +notify_error_task = celery_app.tasks[RegisteredNotifyErrorTask.name] diff --git a/apps/worker/tasks/plan_manager_task.py b/apps/worker/tasks/plan_manager_task.py new file mode 100644 index 0000000000..221185e730 --- /dev/null +++ b/apps/worker/tasks/plan_manager_task.py @@ -0,0 +1,47 @@ +import logging + +from shared.plan.constants import PlanName +from sqlalchemy.orm import Session + +from app import celery_app +from celery_config import daily_plan_manager_task_name +from database.models.core import OrganizationLevelToken, Owner +from tasks.crontasks import CodecovCronTask + +log = logging.getLogger(__name__) + + +# This is currently disabled, as we decided to support the org wide token for all org types +# TODO: Move to shared (celery_config) +class DailyPlanManagerTask(CodecovCronTask, name=daily_plan_manager_task_name): + PLANS_THAT_CAN_HAVE_ORG_LEVEL_TOKENS = [ + PlanName.ENTERPRISE_CLOUD_MONTHLY.value, + PlanName.ENTERPRISE_CLOUD_YEARLY.value, + ] + + @classmethod + def get_min_seconds_interval_between_executions(cls): + return 86100 # 1 day - 5 minutes + + def run_cron_task(self, db_session: Session, *args, **kwargs): + # Query all org-wide tokens + tokens_to_delete = ( + db_session.query(OrganizationLevelToken.id_) + .join(Owner) + .filter(Owner.plan.notin_(self.PLANS_THAT_CAN_HAVE_ORG_LEVEL_TOKENS)) + ) + log.info( + "Deleting OrganizationLevelTokens that belong to invalid plans", + extra=dict(deleted_tokens_ids=tokens_to_delete.all()), + ) + deleted_count = ( + db_session.query(OrganizationLevelToken) + .filter(OrganizationLevelToken.id_.in_(tokens_to_delete.subquery())) + .delete(synchronize_session=False) + ) + db_session.commit() + return dict(checked=True, deleted=deleted_count) + + +RegistedDailyPlanManagerTask = celery_app.register_task(DailyPlanManagerTask()) +daily_plan_manager_task = celery_app.tasks[RegistedDailyPlanManagerTask.name] diff --git a/apps/worker/tasks/preprocess_upload.py b/apps/worker/tasks/preprocess_upload.py new file mode 100644 index 0000000000..566da2a086 --- /dev/null +++ b/apps/worker/tasks/preprocess_upload.py @@ -0,0 +1,151 @@ +import logging +from typing import Optional + +from redis.exceptions import LockError +from shared.helpers.redis import get_redis_connection +from shared.torngit.base import TorngitBaseAdapter + +from app import celery_app +from database.enums import CommitErrorTypes +from database.models import Commit +from helpers.exceptions import RepositoryWithoutValidBotError +from helpers.github_installation import get_installation_name_for_owner_for_task +from helpers.save_commit_error import save_commit_error +from services.report import ReportService +from services.repository import ( + fetch_commit_yaml_and_possibly_store, + get_repo_provider_service, + possibly_update_commit_from_provider_info, +) +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class PreProcessUpload(BaseCodecovTask, name="app.tasks.upload.PreProcessUpload"): + """ + The main goal for this task is to carry forward flags from previous uploads + and save the new carried-forawrded upload in the db,as a pre-step for + uploading a report to codecov + """ + + def run_impl( + self, + db_session, + *, + repoid: int, + commitid: str, + report_code: Optional[str] = None, + **kwargs, + ): + log.info( + "Received preprocess upload task", + extra=dict(repoid=repoid, commit=commitid, report_code=report_code), + ) + lock_name = f"preprocess_upload_lock_{repoid}_{commitid}_{report_code}" + redis_connection = get_redis_connection() + # This task only needs to run once per commit (per report_code) + # To generate the report. So if one is already running we don't need another + if redis_connection.get(lock_name): + log.info( + "PreProcess task is already running", + extra=dict(commit=commitid, repoid=repoid), + ) + return {"preprocessed_upload": False, "reason": "already_running"} + try: + with redis_connection.lock( + lock_name, + timeout=60 * 5, + blocking_timeout=None, + ): + return self.process_impl_within_lock( + db_session=db_session, + repoid=repoid, + commitid=commitid, + report_code=report_code, + ) + except LockError: + log.warning( + "Unable to acquire lock", + extra=dict( + commit=commitid, + repoid=repoid, + number_retries=self.request.retries, + lock_name=lock_name, + ), + ) + return {"preprocessed_upload": False, "reason": "unable_to_acquire_lock"} + + def process_impl_within_lock( + self, + db_session, + repoid, + commitid, + report_code, + ): + commit = ( + db_session.query(Commit) + .filter(Commit.repoid == repoid, Commit.commitid == commitid) + .first() + ) + assert commit, "Commit not found in database." + installation_name_to_use = get_installation_name_for_owner_for_task( + self.name, commit.repository.owner + ) + repository_service = self.get_repo_service(commit, installation_name_to_use) + if repository_service is None: + log.warning( + "Failed to get repository_service", + extra=dict(commit=commitid, repo=repoid), + ) + return { + "preprocessed_upload": False, + "updated_commit": False, + "error": "Failed to get repository_service", + } + # Makes sure that we can properly carry forward reports + # By populating the commit info (if needed) + updated_commit = possibly_update_commit_from_provider_info( + commit=commit, repository_service=repository_service + ) + commit_yaml = fetch_commit_yaml_and_possibly_store(commit, repository_service) + report_service = ReportService( + commit_yaml, gh_app_installation_name=installation_name_to_use + ) + # For parallel upload processing experiment, saving the report to GCS happens here + commit_report = report_service.initialize_and_save_report(commit, report_code) + # Persist changes from within the lock + db_session.flush() + return { + "preprocessed_upload": True, + "reportid": str(commit_report.external_id), + "updated_commit": updated_commit, + } + + def get_repo_service( + self, commit: Commit, installation_name_to_use: str + ) -> Optional[TorngitBaseAdapter]: + repository_service = None + try: + repository_service = get_repo_provider_service( + commit.repository, + installation_name_to_use=installation_name_to_use, + ) + except RepositoryWithoutValidBotError: + save_commit_error( + commit, + error_code=CommitErrorTypes.REPO_BOT_INVALID.value, + error_params=dict( + repoid=commit.repoid, repository_service=repository_service + ), + ) + log.warning( + "Unable to reach git provider because repo doesn't have a valid bot", + extra=dict(repoid=commit.repoid, commit=commit.commitid), + ) + + return repository_service + + +RegisteredUploadTask = celery_app.register_task(PreProcessUpload()) +preprocess_upload_task = celery_app.tasks[RegisteredUploadTask.name] diff --git a/apps/worker/tasks/process_flakes.py b/apps/worker/tasks/process_flakes.py new file mode 100644 index 0000000000..75adb1b08d --- /dev/null +++ b/apps/worker/tasks/process_flakes.py @@ -0,0 +1,102 @@ +import logging +from typing import Any, Literal, cast + +from redis import Redis +from redis.exceptions import LockError +from shared.celery_config import process_flakes_task_name +from shared.helpers.redis import get_redis_connection +from sqlalchemy.orm import Session + +from app import celery_app +from services.processing.flake_processing import process_flake_for_repo_commit +from services.test_analytics.ta_process_flakes import process_flakes_for_repo +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +FLAKE_EXPIRY_COUNT = 30 +LOCK_NAME = "flake_lock:{}" +NEW_KEY = "flake_uploads_list:{}" +OLD_KEY = "flake_uploads:{}" + + +def get_redis_val(redis_client: Redis, repo_id: int) -> list[bytes]: + commit_ids = cast(list[bytes], redis_client.lpop(NEW_KEY.format(repo_id), 10)) + if commit_ids is None: + commit_ids = [] + + return commit_ids + + +class ProcessFlakesTask(BaseCodecovTask, name=process_flakes_task_name): + """ + This task is currently called in the test results finisher task and in the sync pulls task + """ + + def run_impl( + self, + _db_session: Session, + *, + repo_id: int, + impl_type: Literal["old", "new", "both"] = "old", + **kwargs: Any, + ): + """ + This task wants to iterate through uploads for a given commit that have yet to be + "flake processed". + + For each of those uploads it wants to iterate through its test instances and + update existing flakes' count, recent_passes_count, fail_count, and end_date fields + depending on whether the test instance passed or failed. + + For each upload it wants to keep track of newly created flakes and keep those in a separate + collection than the existing flakes, so at the end it can bulk create the new flakes and + bulk update the existing flakes. + + It also wants to increment the flaky_fail_count of the relevant DailyTestRollup when it creates + a new flake so it keeps track of those changes and bulk updates those as well. + + When it's done with an upload it merges the new flakes dictionary into the existing flakes dictionary + and then clears the new flakes dictionary so the following upload considers the flakes created during the previous + iteration as existing. + + The redis locking is to prevent mutliple instances of the task running at the same time for the same repo. + The locking scheme is set up such that no upload will be unprocessed. Before queuing up the process flakes task the + test results finisher and sync pulls tasks will set the flake_uploads key in redis for that repo. + """ + log.info("Received process flakes task") + + if impl_type == "new" or impl_type == "both": + process_flakes_for_repo(repo_id) + if impl_type == "new": + return {"successful": True} + + redis_client = get_redis_connection() + lock_name = LOCK_NAME.format(repo_id) + + process_func = process_flake_for_repo_commit + + try: + with redis_client.lock( + lock_name, + timeout=max(300, self.hard_time_limit_task), + blocking_timeout=3, + ): + while True: + commit_shas = get_redis_val(redis_client, repo_id) + if not commit_shas: + break + + for commit_sha in commit_shas: + process_func(repo_id, commit_sha.decode()) + + except LockError: + log.warning("Unable to acquire process flakeslock for key %s.", lock_name) + return {"successful": False} + + return {"successful": True} + + +RegisteredProcessFlakesTask = celery_app.register_task(ProcessFlakesTask()) +process_flakes_task = celery_app.tasks[RegisteredProcessFlakesTask.name] diff --git a/apps/worker/tasks/regular_cleanup.py b/apps/worker/tasks/regular_cleanup.py new file mode 100644 index 0000000000..bdb6d997bb --- /dev/null +++ b/apps/worker/tasks/regular_cleanup.py @@ -0,0 +1,26 @@ +import logging + +from celery.exceptions import SoftTimeLimitExceeded + +from app import celery_app +from celery_config import regular_cleanup_cron_task_name +from services.cleanup.regular import run_regular_cleanup +from services.cleanup.utils import CleanupSummary +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class RegularCleanupTask(BaseCodecovTask, name=regular_cleanup_cron_task_name): + acks_late = True # retry the task when the worker dies for whatever reason + max_retries = None # aka, no limit on retries + + def run_impl(self, _db_session, *args, **kwargs) -> CleanupSummary: + try: + return run_regular_cleanup() + except SoftTimeLimitExceeded: + raise self.retry() + + +RegisteredRegularCleanupTask = celery_app.register_task(RegularCleanupTask()) +regular_cleanup_task = celery_app.tasks[RegisteredRegularCleanupTask.name] diff --git a/apps/worker/tasks/save_commit_measurements.py b/apps/worker/tasks/save_commit_measurements.py new file mode 100644 index 0000000000..94a0ed7198 --- /dev/null +++ b/apps/worker/tasks/save_commit_measurements.py @@ -0,0 +1,122 @@ +import logging +from typing import Sequence + +from celery import group +from shared.celery_config import timeseries_save_commit_measurements_task_name +from shared.reports.readonly import ReadOnlyReport +from sqlalchemy.orm import Session + +from app import celery_app +from database.models import Commit, MeasurementName +from rollouts import PARALLEL_COMPONENT_COMPARISON +from services.report import ReportService +from services.timeseries import ( + get_relevant_components, + maybe_upsert_coverage_measurement, + maybe_upsert_flag_measurements, + repository_datasets_query, + upsert_components_measurements, +) +from services.yaml import get_repo_yaml +from tasks.base import BaseCodecovTask +from tasks.upsert_component import upsert_component_task + +log = logging.getLogger(__name__) + + +def save_commit_measurements(commit: Commit, dataset_names: Sequence[str]) -> None: + db_session = commit.get_db_session() + + current_yaml = get_repo_yaml(commit.repository) + report_service = ReportService(current_yaml) + report = report_service.get_existing_report_for_commit( + commit, report_class=ReadOnlyReport + ) + + if report is None: + log.warning( + "No report found for commit", + extra=dict(commitid=commit.commitid, repoid=commit.repoid), + ) + return + + maybe_upsert_coverage_measurement(commit, dataset_names, db_session, report) + + if MeasurementName.component_coverage.value in dataset_names: + report_flags = list(report.flags.keys()) + components = get_relevant_components(current_yaml, report_flags) + if components: + if PARALLEL_COMPONENT_COMPARISON.check_value(commit.repository.repoid): + task_signatures = [ + upsert_component_task.s( + commit.commitid, + commit.repoid, + component_id=component.component_id, + flags=component.flags, + paths=component.paths, + ) + for component in components + ] + group(task_signatures).apply_async() + else: + upsert_components_measurements(commit, report, components) + + maybe_upsert_flag_measurements(commit, dataset_names, db_session, report) + + +class SaveCommitMeasurementsTask( + BaseCodecovTask, name=timeseries_save_commit_measurements_task_name +): + def run_impl( + self, + db_session: Session, + commitid: str, + repoid: int, + dataset_names: Sequence[str] | None, + *args, + **kwargs, + ): + log.info( + "Received save_commit_measurements task", + extra=dict( + repoid=repoid, + commit=commitid, + dataset_names=dataset_names, + parent_task=self.request.parent_id, + ), + ) + + commit = ( + db_session.query(Commit) + .filter(Commit.repoid == repoid, Commit.commitid == commitid) + .first() + ) + + if commit is None: + return {"successful": False, "error": "no_commit_in_db"} + + if dataset_names is None: + dataset_names = [ + dataset.name for dataset in repository_datasets_query(commit.repository) + ] + if len(dataset_names) == 0: + return + + try: + # TODO: We should improve on the error handling/logs inside this fn + save_commit_measurements(commit=commit, dataset_names=dataset_names) + return {"successful": True} + except Exception: + log.exception( + "An error happened while saving commit measurements", + extra=dict(commitid=commitid, task_args=args, task_kwargs=kwargs), + ) + return {"successful": False, "error": "exception"} + + +RegisteredSaveCommitMeasurementsTask = celery_app.register_task( + SaveCommitMeasurementsTask() +) +save_commit_measurements_task = celery_app.tasks[ + RegisteredSaveCommitMeasurementsTask.name +] diff --git a/apps/worker/tasks/save_report_results.py b/apps/worker/tasks/save_report_results.py new file mode 100644 index 0000000000..eee45fc405 --- /dev/null +++ b/apps/worker/tasks/save_report_results.py @@ -0,0 +1,174 @@ +import logging + +from asgiref.sync import async_to_sync +from shared.reports.readonly import ReadOnlyReport +from shared.yaml import UserYaml + +from app import celery_app +from database.enums import ReportType +from database.models import Commit, Pull +from database.models.reports import CommitReport, ReportResults +from helpers.exceptions import RepositoryWithoutValidBotError +from helpers.github_installation import get_installation_name_for_owner_for_task +from services.comparison import ComparisonContext, ComparisonProxy +from services.comparison.types import Comparison, FullCommit +from services.notification.notifiers.status.patch import PatchStatusNotifier +from services.report import ReportService +from services.repository import ( + fetch_and_update_pull_request_information_from_commit, + get_repo_provider_service, +) +from services.yaml import get_current_yaml +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class SaveReportResultsTask( + BaseCodecovTask, name="app.tasks.reports.save_report_results" +): + def run_impl( + self, db_session, *, repoid, commitid, report_code, current_yaml, **kwargs + ): + commit = self.fetch_commit(db_session, repoid, commitid) + + try: + installation_name_to_use = get_installation_name_for_owner_for_task( + self.name, commit.repository.owner + ) + repository_service = get_repo_provider_service( + commit.repository, installation_name_to_use=installation_name_to_use + ) + except RepositoryWithoutValidBotError: + return { + "report_results_saved": False, + "reason": "repository without valid bot", + } + + current_yaml = self.fetch_yaml_dict(current_yaml, commit, repository_service) + enriched_pull = async_to_sync( + fetch_and_update_pull_request_information_from_commit + )(repository_service, commit, current_yaml) + base_commit = self.fetch_base_commit(commit, enriched_pull) + base_report, head_report = self.fetch_base_and_head_reports( + current_yaml, commit, base_commit, report_code + ) + + if enriched_pull and enriched_pull.database_pull: + patch_coverage_base_commitid = enriched_pull.database_pull.base + else: + patch_coverage_base_commitid = base_commit.commitid if base_commit else None + + if head_report is None: + log.warning( + "Not saving report results because no head report found.", + extra=dict(repoid=repoid, commit_id=commitid), + ) + return {"report_results_saved": False, "reason": "No head report found."} + + comparison = ComparisonProxy( + Comparison( + head=FullCommit(commit=commit, report=head_report), + project_coverage_base=FullCommit( + commit=base_commit, report=base_report + ), + patch_coverage_base_commitid=patch_coverage_base_commitid, + enriched_pull=enriched_pull, + ), + ComparisonContext(repository_service=repository_service), + ) + + notifier = PatchStatusNotifier( + repository=comparison.head.commit.repository, + title="title", + notifier_yaml_settings={}, + notifier_site_settings=True, + current_yaml=current_yaml, + repository_service=repository_service, + ) + result = notifier.build_payload(comparison) + report = self.fetch_report(commit, report_code) + log.info( + "Saving report results into the db", + extra=dict(repoid=repoid, commitid=commitid, report_id=report.id), + ) + self.save_results_into_db(result, report) + return {"report_results_saved": True, "reason": "success"} + + def fetch_base_and_head_reports( + self, current_yaml, commit, base_commit, report_code + ): + report_service = ReportService(current_yaml) + if base_commit is not None: + base_report = report_service.get_existing_report_for_commit( + base_commit, report_class=ReadOnlyReport + ) + else: + base_report = None + head_report = report_service.get_existing_report_for_commit( + commit, report_class=ReadOnlyReport, report_code=report_code + ) + + return base_report, head_report + + def fetch_base_commit(self, commit, enriched_pull): + if enriched_pull and enriched_pull.database_pull: + pull = enriched_pull.database_pull + base_commit = self.fetch_pull_request_base(pull) + else: + pull = None + base_commit = self.fetch_parent(commit) + return base_commit + + def fetch_yaml_dict(self, current_yaml, commit, repository_service): + if current_yaml is None: + current_yaml = async_to_sync(get_current_yaml)(commit, repository_service) + else: + current_yaml = UserYaml.from_dict(current_yaml) + return current_yaml + + def fetch_commit(self, db_session, repoid, commitid) -> Commit | None: + commits_query = db_session.query(Commit).filter( + Commit.repoid == repoid, Commit.commitid == commitid + ) + commit = commits_query.first() + return commit + + def save_results_into_db(self, result, report): + db_session = report.get_db_session() + report_results = ( + db_session.query(ReportResults) + .filter(ReportResults.report == report) + .first() + ) + report_results.state = "Completed" + report_results.result = result + db_session.add(report_results) + db_session.flush() + + def fetch_report(self, commit: Commit, report_code: str) -> CommitReport: + db_session = commit.get_db_session() + return ( + db_session.query(CommitReport) + .filter_by(commit_id=commit.id_, code=report_code) + .filter( + (CommitReport.report_type == None) # noqa: E711 + | (CommitReport.report_type == ReportType.COVERAGE.value) + ) + .first() + ) + + def fetch_pull_request_base(self, pull: Pull) -> Commit: + return pull.get_comparedto_commit() + + def fetch_parent(self, commit): + db_session = commit.get_db_session() + return ( + db_session.query(Commit) + .filter_by(commitid=commit.parent_commit_id, repoid=commit.repoid) + .first() + ) + + +RegisteredSaveReportResultsTask = celery_app.register_task(SaveReportResultsTask()) +save_report_results_task = celery_app.tasks[RegisteredSaveReportResultsTask.name] diff --git a/apps/worker/tasks/send_email.py b/apps/worker/tasks/send_email.py new file mode 100644 index 0000000000..08e3728dc7 --- /dev/null +++ b/apps/worker/tasks/send_email.py @@ -0,0 +1,81 @@ +import logging + +from shared.celery_config import send_email_task_name +from shared.config import get_config + +import services.smtp +from app import celery_app +from helpers.email import Email +from helpers.metrics import metrics +from services.template import TemplateService +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class SendEmailTask(BaseCodecovTask, name=send_email_task_name): + def run_impl( + self, db_session, to_addr, subject, template_name, from_addr=None, **kwargs + ): + with metrics.timer("worker.tasks.send_email"): + if from_addr is None: + from_addr = get_config( + "services", + "smtp", + "from_address", + default="Codecov ", + ) + + log_extra_dict = { + "to_addr": to_addr, + "from_addr": from_addr, + "template_name": template_name, + "template_kwargs": kwargs, + } + + log.info( + "Received send email task", + extra=log_extra_dict, + ) + + smtp_service = services.smtp.SMTPService() + + if not smtp_service.active(): + log.warning( + "Cannot send email because SMTP is not configured for this installation of codecov" + ) + return { + "email_successful": False, + "err_msg": "Cannot send email because SMTP is not configured for this installation of codecov", + } + template_service = TemplateService() + + with metrics.timer("worker.tasks.send_email.render_templates"): + text_template = template_service.get_template(f"{template_name}.txt") + text = text_template.render(**kwargs) + + html_template = template_service.get_template(f"{template_name}.html") + html = html_template.render(**kwargs) + + email_wrapper = Email(to_addr, from_addr, subject, text, html) + + err_msg = None + metrics.incr("worker.tasks.send_email.attempt") + with metrics.timer("worker.tasks.send_email.send"): + try: + smtp_service.send(email_wrapper) + except services.smtp.SMTPServiceError as exc: + err_msg = str(exc) + + if err_msg is not None: + log.warning(f"Failed to send email: {err_msg}", extra=log_extra_dict) + metrics.incr("worker.tasks.send_email.fail") + return {"email_successful": False, "err_msg": err_msg} + + log.info("Sent email", extra=log_extra_dict) + metrics.incr("worker.tasks.send_email.succeed") + return {"email_successful": True, "err_msg": None} + + +RegisteredSendEmailTask = celery_app.register_task(SendEmailTask()) +send_email = celery_app.tasks[RegisteredSendEmailTask.name] diff --git a/apps/worker/tasks/static_analysis_suite_check.py b/apps/worker/tasks/static_analysis_suite_check.py new file mode 100644 index 0000000000..10168431b6 --- /dev/null +++ b/apps/worker/tasks/static_analysis_suite_check.py @@ -0,0 +1,75 @@ +import logging +from typing import Optional + +from shared.celery_config import static_analysis_task_name +from shared.staticanalysis import StaticAnalysisSingleFileSnapshotState +from shared.storage.exceptions import FileNotInStorageError + +from app import celery_app +from database.models.staticanalysis import ( + StaticAnalysisSingleFileSnapshot, + StaticAnalysisSuite, + StaticAnalysisSuiteFilepath, +) +from services.archive import ArchiveService +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class StaticAnalysisSuiteCheckTask(BaseCodecovTask, name=static_analysis_task_name): + def run_impl( + self, + db_session, + *, + suite_id, + **kwargs, + ): + suite: Optional[StaticAnalysisSuite] = ( + db_session.query(StaticAnalysisSuite).filter_by(id_=suite_id).first() + ) + if suite is None: + log.warning("Checking Static Analysis that does not exist yet") + return {"successful": False, "changed_count": None} + log.info("Checking static analysis suite", extra=dict(suite_id=suite_id)) + query = ( + db_session.query( + StaticAnalysisSingleFileSnapshot, + StaticAnalysisSingleFileSnapshot.content_location, + ) + .join( + StaticAnalysisSuiteFilepath, + StaticAnalysisSuiteFilepath.file_snapshot_id + == StaticAnalysisSingleFileSnapshot.id_, + ) + .filter( + StaticAnalysisSuiteFilepath.analysis_suite_id == suite_id, + StaticAnalysisSingleFileSnapshot.state_id + == StaticAnalysisSingleFileSnapshotState.CREATED.db_id, + ) + ) + archive_service = ArchiveService(suite.commit.repository) + # purposefully iteration when an update would suffice, + # because we actually want to validate different stuff + changed_count = 0 + for elem, content_location in query: + try: + _ = archive_service.read_file(content_location) + elem.state_id = StaticAnalysisSingleFileSnapshotState.VALID.db_id + changed_count += 1 + except FileNotInStorageError: + log.warning( + "File not found to be analyzed", + extra=dict(filepath_id=elem.id, suite_id=suite_id), + ) + + db_session.commit() + return {"successful": True, "changed_count": changed_count} + + +RegisteredStaticAnalysisSuiteCheckTask = celery_app.register_task( + StaticAnalysisSuiteCheckTask() +) +static_analysis_suite_check_task = celery_app.tasks[ + RegisteredStaticAnalysisSuiteCheckTask.name +] diff --git a/apps/worker/tasks/status_set_error.py b/apps/worker/tasks/status_set_error.py new file mode 100644 index 0000000000..ff3ef87499 --- /dev/null +++ b/apps/worker/tasks/status_set_error.py @@ -0,0 +1,77 @@ +import logging + +from asgiref.sync import async_to_sync +from shared.celery_config import status_set_error_task_name +from shared.helpers.yaml import default_if_true +from shared.utils.urls import make_url + +from app import celery_app +from database.models import Commit +from services.repository import get_repo_provider_service +from services.yaml import get_current_yaml +from services.yaml.reader import read_yaml_field +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class StatusSetErrorTask(BaseCodecovTask, name=status_set_error_task_name): + """ + Set commit status upon error + """ + + def run_impl(self, db_session, repoid, commitid, *, message=None, **kwargs): + log.info( + "Set error", + extra=dict(repoid=repoid, commitid=commitid, description=message), + ) + + # TODO: need to check for enterprise license once licences are implemented + # assert license.LICENSE['valid'], ('Notifications disabled. '+(license.LICENSE['warning'] or '')) + + commits = db_session.query(Commit).filter( + Commit.repoid == repoid, Commit.commitid == commitid + ) + commit = commits.first() + assert commit, "Commit not found in database." + repo_service = get_repo_provider_service(commit.repository) + current_yaml = async_to_sync(get_current_yaml)(commit, repo_service) + settings = read_yaml_field(current_yaml, ("coverage", "status")) + + status_set = False + + if settings and any(settings.values()): + statuses = async_to_sync(repo_service.get_commit_statuses)(commitid) + url = make_url(repo_service, "commit", commitid) + for context in ("project", "patch", "changes"): + if settings.get(context): + for key, data in default_if_true(settings[context]): + context = "codecov/%s%s" % ( + context, + ("/" + key if key != "default" else ""), + ) + state = ( + "success" + if data.get("informational") + else data.get("if_ci_failed", "error") + ) + message = ( + message or "Coverage not measured fully because CI failed" + ) + if context in statuses: + async_to_sync(repo_service.set_commit_status)( + commitid, state, context, message, url + ) + status_set = True + log.info( + "Status set", + extra=dict( + context=context, description=message, state=state + ), + ) + + return {"status_set": status_set} + + +RegisteredStatusSetErrorTask = celery_app.register_task(StatusSetErrorTask()) +status_set_error_task = celery_app.tasks[StatusSetErrorTask.name] diff --git a/apps/worker/tasks/status_set_pending.py b/apps/worker/tasks/status_set_pending.py new file mode 100644 index 0000000000..2611e7040a --- /dev/null +++ b/apps/worker/tasks/status_set_pending.py @@ -0,0 +1,103 @@ +import logging + +from asgiref.sync import async_to_sync +from shared.celery_config import status_set_pending_task_name +from shared.helpers.redis import get_redis_connection +from shared.helpers.yaml import default_if_true +from shared.utils.match import match +from shared.utils.urls import make_url + +from app import celery_app +from database.models import Commit +from services.repository import get_repo_provider_service +from services.yaml import get_current_yaml +from services.yaml.reader import read_yaml_field +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class StatusSetPendingTask(BaseCodecovTask, name=status_set_pending_task_name): + """ + Set commit status to pending + """ + + throws = (AssertionError,) + + def run_impl( + self, db_session, repoid, commitid, branch, on_a_pull_request, *args, **kwargs + ): + log.info( + "Set pending", + extra=dict( + repoid=repoid, + commit=commitid, + branch=branch, + on_a_pull_request=on_a_pull_request, + ), + ) + + # TODO: need to check for enterprise license once licences are implemented + # assert license.LICENSE['valid'], ('Notifications disabled. '+(license.LICENSE['warning'] or '')) + + # check that repo is in beta + redis_connection = get_redis_connection() + assert redis_connection.sismember("beta.pending", repoid), ( + "Pending disabled. Please request to be in beta." + ) + + commits = db_session.query(Commit).filter( + Commit.repoid == repoid, Commit.commitid == commitid + ) + commit = commits.first() + assert commit, "Commit not found in database." + repo_service = get_repo_provider_service(commit.repository) + current_yaml = async_to_sync(get_current_yaml)(commit, repo_service) + settings = read_yaml_field(current_yaml, ("coverage", "status")) + + status_set = False + + if settings and any(settings.values()): + statuses = async_to_sync(repo_service.get_commit_statuses)(commitid) + url = make_url(repo_service, "commit", commitid) + + for context in ("project", "patch", "changes"): + if settings.get(context): + for key, config in default_if_true(settings[context]): + try: + title = "codecov/%s%s" % ( + context, + ("/" + key if key != "default" else ""), + ) + assert match(config.get("branches"), branch or ""), ( + "Ignore setting pending status on branch" + ) + assert ( + on_a_pull_request + if config.get("only_pulls", False) + else True + ), "Set pending only on pulls" + assert config.get("set_pending", True), ( + "Pending status disabled in YAML" + ) + assert title not in statuses, "Pending status already set" + + async_to_sync(repo_service.set_commit_status)( + commitid, + "pending", + title, + "Collecting reports and waiting for CI to complete", + url, + ) + status_set = True + log.info( + "Status set", extra=dict(context=title, state="pending") + ) + except AssertionError as e: + log.warning(str(e), extra=dict(context=context)) + + return {"status_set": status_set} + + +RegisteredStatusSetPendingTask = celery_app.register_task(StatusSetPendingTask()) +status_set_pending_task = celery_app.tasks[StatusSetPendingTask.name] diff --git a/apps/worker/tasks/sync_pull.py b/apps/worker/tasks/sync_pull.py new file mode 100644 index 0000000000..187cf4daa1 --- /dev/null +++ b/apps/worker/tasks/sync_pull.py @@ -0,0 +1,576 @@ +import json +import logging +import os +import time +from collections import deque +from datetime import datetime +from typing import Any, Dict, List, Sequence + +import sqlalchemy.orm +from asgiref.sync import async_to_sync +from redis.exceptions import LockError +from shared.celery_config import notify_task_name, pulls_task_name +from shared.helpers.redis import get_redis_connection +from shared.metrics import Counter, inc_counter +from shared.reports.types import Change +from shared.torngit.exceptions import TorngitClientError +from shared.yaml import UserYaml +from shared.yaml.user_yaml import OwnerContext + +from app import celery_app +from database.models import Commit, Pull, Repository +from helpers.exceptions import NoConfiguredAppsAvailable, RepositoryWithoutValidBotError +from helpers.github_installation import get_installation_name_for_owner_for_task +from helpers.metrics import metrics +from rollouts import SYNC_PULL_USE_MERGE_COMMIT_SHA +from services.comparison.changes import get_changes +from services.report import Report, ReportService +from services.repository import ( + EnrichedPull, + fetch_and_update_pull_request_information, + get_repo_provider_service, +) +from services.test_results import should_do_flaky_detection +from services.yaml.reader import read_yaml_field +from tasks.base import BaseCodecovTask +from tasks.process_flakes import process_flakes_task_name + +log = logging.getLogger(__name__) + +SYNC_PULL_MERGE_COMMIT_SHA_COUNTER = Counter( + "sync_pull_merge_commit_sha", + "Number of sync pull using merge commit SHA", + ["success"], +) + + +class PullSyncTask(BaseCodecovTask, name=pulls_task_name): + """ + This is the task that syncs pull with the information the Git Provider gives us + + The most characteristic piece of this task is that it centers around the PR. + We receive a (repoid, pullid) pair. And we fetch all the information + from the git provider to update it as needed. + + This mostly includes + - Updating basic database fields around the PR, like author + - Updating `base` and `head` of the PR + - Updating the `diff` and `flare` information of the PR using the `report` of its head + - Updating all the commits that point to this pull in case the pull is being merged + - Clear the caches we have around this PR + + At the end we call the notify task to do notifications with the new information we have + """ + + def run_impl( + self, + db_session: sqlalchemy.orm.Session, + *, + repoid: int = None, + pullid: int = None, + should_send_notifications: bool = True, + **kwargs, + ): + redis_connection = get_redis_connection() + pullid = int(pullid) + repoid = int(repoid) + lock_name = f"pullsync_{repoid}_{pullid}" + start_wait = time.monotonic() + try: + with redis_connection.lock( + lock_name, + timeout=60 * 5, + blocking_timeout=0.5, + ): + return self.run_impl_within_lock( + db_session, + redis_connection, + repoid=repoid, + pullid=pullid, + should_send_notifications=should_send_notifications, + **kwargs, + ) + except LockError: + log.info( + "Unable to acquire PullSync lock. Not retrying because pull is being synced already", + extra=dict(pullid=pullid, repoid=repoid), + ) + return { + "notifier_called": False, + "commit_updates_done": {"merged_count": 0, "soft_deleted_count": 0}, + "pull_updated": False, + "reason": "unable_fetch_lock", + } + + def run_impl_within_lock( + self, + db_session: sqlalchemy.orm.Session, + redis_connection, + *, + repoid: int = None, + pullid: int = None, + should_send_notifications: bool = True, + **kwargs, + ): + commit_updates_done = {"merged_count": 0, "soft_deleted_count": 0} + repository = db_session.query(Repository).filter_by(repoid=repoid).first() + assert repository + extra_info = dict(pullid=pullid, repoid=repoid) + try: + installation_name_to_use = get_installation_name_for_owner_for_task( + self.name, repository.owner + ) + repository_service = get_repo_provider_service( + repository, installation_name_to_use=installation_name_to_use + ) + except RepositoryWithoutValidBotError: + log.warning( + "Could not sync pull because there is no valid bot found for that repo", + extra=extra_info, + exc_info=True, + ) + return { + "notifier_called": False, + "commit_updates_done": {"merged_count": 0, "soft_deleted_count": 0}, + "pull_updated": False, + "reason": "no_bot", + } + except NoConfiguredAppsAvailable as err: + log.error( + "Could not sync pull because there are no configured apps available", + extra={ + **extra_info, + "suspended_app_count": err.suspended_count, + "rate_limited_count": err.rate_limited_count, + }, + ) + if err.rate_limited_count > 0: + log.info("Apps are rate limited. Retrying in 60s", extra=extra_info) + self.retry(max_retries=1, countdown=60) + return { + "notifier_called": False, + "commit_updates_done": {"merged_count": 0, "soft_deleted_count": 0}, + "pull_updated": False, + "reason": "no_configured_apps_available", + } + context = OwnerContext( + owner_onboarding_date=repository.owner.createstamp, + owner_plan=repository.owner.plan, + ownerid=repository.ownerid, + ) + current_yaml = UserYaml.get_final_yaml( + owner_yaml=repository.owner.yaml, + repo_yaml=repository.yaml, + owner_context=context, + ) + with metrics.timer(f"{self.metrics_prefix}.fetch_pull"): + enriched_pull = async_to_sync(fetch_and_update_pull_request_information)( + repository_service, db_session, repoid, pullid, current_yaml + ) + pull = enriched_pull.database_pull + if pull is None: + log.info( + "Not syncing pull since we can't find it in the database nor in the provider", + extra=extra_info, + ) + return { + "notifier_called": False, + "commit_updates_done": {"merged_count": 0, "soft_deleted_count": 0}, + "pull_updated": False, + "reason": "no_db_pull", + } + if enriched_pull.provider_pull is None: + log.info( + "Not syncing pull since we can't find it in the provider. There is nothing to sync", + extra=extra_info, + ) + return { + "notifier_called": False, + "commit_updates_done": {"merged_count": 0, "soft_deleted_count": 0}, + "pull_updated": False, + "reason": "not_in_provider", + } + self.trigger_ai_pr_review(enriched_pull, current_yaml) + report_service = ReportService( + current_yaml, gh_app_installation_name=installation_name_to_use + ) + head_commit = pull.get_head_commit() + if head_commit is None: + log.info( + "Not syncing pull since there is no head in our database", + extra=dict(pullid=pullid, repoid=repoid), + ) + return { + "notifier_called": False, + "commit_updates_done": {"merged_count": 0, "soft_deleted_count": 0}, + "pull_updated": False, + "reason": "no_head", + } + compared_to = pull.get_comparedto_commit() + head_report = report_service.get_existing_report_for_commit(head_commit) + if compared_to is not None: + base_report = report_service.get_existing_report_for_commit(compared_to) + else: + base_report = None + commits = None + db_session.commit() + try: + commits = async_to_sync(repository_service.get_pull_request_commits)( + pull.pullid + ) + base_ancestors_tree = async_to_sync(repository_service.get_ancestors_tree)( + enriched_pull.provider_pull["base"]["branch"] + ) + commit_updates_done = self.update_pull_commits( + repository_service, + enriched_pull, + commits, + base_ancestors_tree, + current_yaml, + repository, + ) + db_session.commit() + except TorngitClientError: + log.warning( + "Unable to fetch information about pull commits", + extra=dict(pullid=pullid, repoid=repoid), + ) + self.update_pull_from_reports( + pull, repository_service, base_report, head_report, current_yaml + ) + db_session.commit() + notifier_was_called = False + if should_send_notifications: + notifier_was_called = True + self.app.tasks[notify_task_name].apply_async( + kwargs=dict(repoid=repoid, commitid=pull.head) + ) + self.clear_pull_related_caches(redis_connection, enriched_pull) + return { + "notifier_called": notifier_was_called, + "commit_updates_done": commit_updates_done, + "pull_updated": True, + "reason": "success", + } + + def cache_changes(self, pull: Pull, changes: List[Change]): + """ + Caches the list of files with changes for a given comparison. + This information will be used API-side to speed up responses. + """ + owners_with_cached_changes = [ + int(ownerid.strip()) + for ownerid in os.getenv("OWNERS_WITH_CACHED_CHANGES", "").split(",") + if ownerid != "" + ] + if pull.repository.owner.ownerid in owners_with_cached_changes: + log.info( + "Caching files with changes", + extra=dict(pullid=pull.pullid, repoid=pull.repoid), + ) + redis = get_redis_connection() + key = "/".join( + ( + "compare-changed-files", + pull.repository.owner.service, + pull.repository.owner.username, + pull.repository.name, + f"{pull.pullid}", + ) + ) + redis.set( + key, + json.dumps([change.path for change in changes]), + ex=86400, # 1 day in seconds + ) + log.info( + "Finished caching files with changes", + extra=dict(pullid=pull.pullid, repoid=pull.repoid), + ) + + def update_pull_from_reports( + self, + pull: Pull, + repository_service, + base_report: Report, + head_report: Report, + current_yaml, + ): + try: + compare_dict = async_to_sync(repository_service.get_compare)( + pull.base, pull.head, with_commits=False + ) + diff = compare_dict["diff"] + changes = get_changes(base_report, head_report, diff) + if changes: + self.cache_changes(pull, changes) + if head_report: + color = read_yaml_field(current_yaml, ("coverage", "range")) + pull.diff = head_report.apply_diff(diff) + pull.flare = ( + head_report.flare(changes, color=color) if head_report else None + ) + return True + except TorngitClientError: + log.warning( + "Unable to fetch information about diff", + extra=dict(pullid=pull.pullid, repoid=pull.repoid), + ) + return False + + def clear_pull_related_caches(self, redis_connection, enriched_pull: EnrichedPull): + pull = enriched_pull.database_pull + pull_dict = enriched_pull.provider_pull + repository = pull.repository + key = ":".join((repository.service, repository.owner.username, repository.name)) + if pull.state == "merged": + base_branch = pull_dict["base"]["branch"] + if base_branch: + redis_connection.hdel("badge", (f"{key}:{base_branch}").lower()) + if base_branch == repository.branch: + redis_connection.hdel("badge", (f"{key}:").lower()) + + def update_pull_commits( + self, + repository_service, + enriched_pull: EnrichedPull, + commits_on_pr: Sequence, + ancestors_tree_on_base: Dict[str, Any], + current_yaml, + repository: Repository, + ) -> dict: + """Updates commits considering what the new PR situation is. + + For example, if a pull is merged, it makes sense that their commits switch to + `merged` mode and start being part of the `base` branch. + + On squash merge we have a bit of an issue, since we can't move the commits to the + new branch (they didn't go there), but they don't go deleted. So they just hang + there for a while. Theoretically a branch deletion should trigger something + and the system will deal with those commits, but I dont think this is implemented + yet + + Args: + enriched_pull (EnrichedPull): The pull that changed state + commits_on_pr (Sequence): The commits that were on the PR we might want to update + ancestors_tree_on_base (Dict[str, Any]): Ancestor tree of commits starting at the base + + Returns: + dict: A dict of the changes that were made + """ + pull = enriched_pull.database_pull + pull_dict = enriched_pull.provider_pull + repoid = pull.repoid + pullid = pull.pullid + db_session = pull.get_db_session() + merged_count, deleted_count = 0, 0 + if commits_on_pr: + if pull.state == "merged": + is_squash_merge = self.was_pr_merged_with_squash( + repoid, + pullid, + pull_dict, + repository_service, + commits_on_pr, + ancestors_tree_on_base, + ) + if not is_squash_merge: + log.info( + "Moving commits to base branch", + extra=dict( + commits_on_pr=commits_on_pr, + repoid=repoid, + pullid=pullid, + new_branch=pull_dict["base"]["branch"], + ), + ) + merged_count = ( + db_session.query(Commit) + .filter( + Commit.repoid == repoid, + Commit.pullid == pullid, + Commit.commitid.in_(commits_on_pr + [pull.base, pull.head]), + ~Commit.merged, + ) + .update( + { + Commit.branch: pull_dict["base"]["branch"], + Commit.updatestamp: datetime.now(), + Commit.merged: True, + Commit.deleted: False, + }, + synchronize_session=False, + ) + ) + + self.trigger_process_flakes( + db_session, + repository, + pull.head, + current_yaml, + ) + + # set the rest of the commits to deleted (do not show in the UI) + deleted_count = ( + db_session.query(Commit) + .filter( + Commit.repoid == repoid, + Commit.pullid == pullid, + ~Commit.commitid.in_(commits_on_pr + [pull.base, pull.head]), + ) + .update({Commit.deleted: True}, synchronize_session=False) + ) + return {"soft_deleted_count": deleted_count, "merged_count": merged_count} + + def was_squash_via_merge_commit( + self, repoid, pullid, repository_service, pull_dict + ): + # if the merge commit exists for this PR, and that commit + # has multiple parents, then it's a regular merge commit + # otherwise it's a squash + + merge_commit_sha = pull_dict.get("merge_commit_sha") + log.info( + "Sync Pull using merge commit sha experiment running", + extra=dict(repoid=repoid, pullid=pullid, merge_commit_sha=merge_commit_sha), + ) + + if merge_commit_sha is None: + return None + + merge_commit = repository_service.get_commit(merge_commit_sha) + return len(merge_commit["parents"] <= 1) + + def was_squash_via_ancestor_tree(self, commits_on_pr, base_ancestors_tree): + """ + Determines if commit was merged with squash merge or not, by looking at the commits + that were on the commit and the commits that are on the base branch now + + The logic here is that, if at least one commit of the PR made it into the base branch, + then we know no ways of merging just that one commit without the other ones. So it + is probable that the commit was merged with merge-commit (or some other strategy + that moves all commits to the base branch). + + Notice that the reason we don't require all commits to be on the base branch now is + because it's possible that the base branch moves so fast that by the time + we check it part of the PR commits could have been merged via merge-commit, but + are already out of the first page of commit listing. + + This also brings us the the limitation of this logic: if the base branch moves so fast + that ALL the commits are out of the first page of listing when we check, we + will assume (wrongly) that this was a squash commit + + Args: + commits_on_pr (Sequence[str]): Description + base_ancestors_tree (Dict[str, Any]): Description + """ + commits_on_pr_set = set(commits_on_pr) + current_level = deque([base_ancestors_tree]) + while current_level: + el = current_level.popleft() + if el["commitid"] in commits_on_pr_set: + log.info( + "Commit currently on base tree was also on PR. Calling it a normal merge", + extra=dict( + commits_on_pr=commits_on_pr, + commit_on_base=el["commitid"], + base_head=base_ancestors_tree["commitid"], + ), + ) + return False + for p in el.get("parents", []): + current_level.append(p) + log.info( + "Commits from PR not found on base tree. Calling it a squash merge", + extra=dict( + commits_on_pr=commits_on_pr, base_head=base_ancestors_tree["commitid"] + ), + ) + return True + + def was_pr_merged_with_squash( + self, + repoid: int, + pullid: int, + repository_service, + pull_dict: dict[str, Any], + commits_on_pr: Sequence[str], + base_ancestors_tree: Dict[str, Any], + ) -> bool: + experiment_was_squash = None + if SYNC_PULL_USE_MERGE_COMMIT_SHA.check_value(repoid): + experiment_was_squash = self.was_squash_via_merge_commit( + repoid, pullid, repository_service, pull_dict + ) + + regular_was_squash = self.was_squash_via_ancestor_tree( + commits_on_pr, base_ancestors_tree + ) + + if regular_was_squash == experiment_was_squash: + inc_counter( + SYNC_PULL_MERGE_COMMIT_SHA_COUNTER, + labels=dict(success="true"), + ) + log.info( + "Sync Pull merge commit sha experiment succeeded", + extra=dict(repoid=repoid, pullid=pullid), + ) + else: + inc_counter( + SYNC_PULL_MERGE_COMMIT_SHA_COUNTER, + labels=dict(success="false"), + ) + log.info( + "Sync Pull merge commit sha experiment failed", + extra=dict(repoid=repoid, pullid=pullid), + ) + + return regular_was_squash + + def trigger_process_flakes( + self, + db_session: sqlalchemy.orm.Session, + repository: Repository, + pull_head: str, + current_yaml: UserYaml, + ): + if should_do_flaky_detection(repository, current_yaml): + redis_client = get_redis_connection() + redis_client.set(f"flake_uploads:{repository.repoid}", 0) + self.app.tasks[process_flakes_task_name].apply_async( + kwargs=dict(repo_id=repository.repoid, commit_id=pull_head) + ) + + def trigger_ai_pr_review(self, enriched_pull: EnrichedPull, current_yaml: UserYaml): + pull = enriched_pull.database_pull + if pull.state == "open" and read_yaml_field( + current_yaml, ("ai_pr_review", "enabled"), False + ): + review_method = read_yaml_field( + current_yaml, ("ai_pr_review", "method"), "auto" + ) + label_name = read_yaml_field( + current_yaml, ("ai_pr_review", "label_name"), None + ) + pull_labels = enriched_pull.provider_pull.get("labels", []) + if review_method == "auto" or ( + review_method == "label" and label_name in pull_labels + ): + log.info( + "Triggering AI PR review task", + extra=dict( + repoid=pull.repoid, + pullid=pull.pullid, + review_method=review_method, + lbale_name=label_name, + pull_labels=pull_labels, + ), + ) + self.app.tasks["app.tasks.ai_pr_review.AiPrReview"].apply_async( + kwargs=dict(repoid=pull.repoid, pullid=pull.pullid) + ) + + +RegisteredPullSyncTask = celery_app.register_task(PullSyncTask()) +pull_sync_task = celery_app.tasks[RegisteredPullSyncTask.name] diff --git a/apps/worker/tasks/sync_repo_languages.py b/apps/worker/tasks/sync_repo_languages.py new file mode 100644 index 0000000000..cf4627d2b9 --- /dev/null +++ b/apps/worker/tasks/sync_repo_languages.py @@ -0,0 +1,95 @@ +import logging + +from asgiref.sync import async_to_sync +from shared.celery_config import sync_repo_languages_task_name +from shared.torngit.exceptions import TorngitError +from sqlalchemy.orm.session import Session + +from app import celery_app +from database.models.core import Repository +from helpers.clock import get_utc_now +from helpers.github_installation import get_installation_name_for_owner_for_task +from services.repository import get_repo_provider_service +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + +BUNDLE_ANALYSIS_LANGUAGES = ["javascript", "typescript"] + +# We sync on repos that don't have the desired BA languages every 7 days +REPOSITORY_LANGUAGE_SYNC_THRESHOLD = 7 + + +class SyncRepoLanguagesTask(BaseCodecovTask, name=sync_repo_languages_task_name): + def run_impl( + self, db_session: Session, repoid: int, manual_trigger=False, *args, **kwargs + ): + repository = db_session.query(Repository).get(repoid) + if repository is None: + return {"successful": False, "error": "no_repo_in_db"} + + now = get_utc_now() + days_since_sync = REPOSITORY_LANGUAGE_SYNC_THRESHOLD + + if repository.languages_last_updated: + days_since_sync = abs( + (now.replace(tzinfo=None) - repository.languages_last_updated).days + ) + + desired_languages_intersection = set(BUNDLE_ANALYSIS_LANGUAGES).intersection( + repository.languages or {} + ) + + should_sync_languages = ( + days_since_sync >= REPOSITORY_LANGUAGE_SYNC_THRESHOLD + and len(desired_languages_intersection) == 0 + ) or manual_trigger + + if not should_sync_languages: + return {"successful": True, "synced": False} + + log_extra = dict( + owner_id=repository.ownerid or "", + repository_id=repository.repoid, + ) + log.info("Syncing repository languages", extra=log_extra) + + installation_name_to_use = get_installation_name_for_owner_for_task( + self.name, repository.owner + ) + repository_service = get_repo_provider_service( + repository, installation_name_to_use=installation_name_to_use + ) + if not repository_service: + log.warning( + "Failed to create repository_service. Exiting", + extra=log_extra, + ) + return {"successful": False, "error": "no_repo_service"} + + is_bitbucket_call = ( + repository.owner.service == "bitbucket" + or repository.owner.service == "bitbucket_server" + ) + try: + if is_bitbucket_call: + languages = async_to_sync(repository_service.get_repo_languages)( + token=None, language=repository.language + ) + else: + languages = async_to_sync(repository_service.get_repo_languages)() + repository.languages = languages + repository.languages_last_updated = now + except TorngitError: + log.warning( + "Unable to find languages for this repository", + extra=dict(repoid=repoid), + ) + return {"successful": False, "error": "no_repo_in_provider"} + + db_session.flush() + return {"successful": True} + + +RegisteredSyncRepoLanguagesTask = celery_app.register_task(SyncRepoLanguagesTask()) +sync_repo_language_task = celery_app.tasks[RegisteredSyncRepoLanguagesTask.name] diff --git a/apps/worker/tasks/sync_repo_languages_gql.py b/apps/worker/tasks/sync_repo_languages_gql.py new file mode 100644 index 0000000000..31dc8597f9 --- /dev/null +++ b/apps/worker/tasks/sync_repo_languages_gql.py @@ -0,0 +1,91 @@ +import logging +from typing import List, Optional + +from asgiref.sync import async_to_sync +from shared.celery_config import sync_repo_languages_gql_task_name +from shared.torngit.exceptions import TorngitError, TorngitRateLimitError +from sqlalchemy import String +from sqlalchemy.orm.session import Session + +from app import celery_app +from database.models.core import Owner, Repository +from helpers.clock import get_utc_now +from services.owner import get_owner_provider_service +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class SyncRepoLanguagesGQLTask(BaseCodecovTask, name=sync_repo_languages_gql_task_name): + def run_impl( + self, + db_session: Session, + org_username: String, + current_owner_id=int, + *args, + **kwargs, + ): + # Fetch current owner and org of interest from DB + current_owner: Owner = ( + db_session.query(Owner).filter(Owner.ownerid == current_owner_id).first() + ) + org: Owner = ( + db_session.query(Owner) + .filter(Owner.username == org_username, Owner.service == "github") + .first() + ) + if current_owner is None or org is None: + return {"successful": False, "error": "no_owner_in_db"} + + org_db_repositories: List[Repository] = ( + db_session.query(Repository).filter(Repository.ownerid == org.ownerid).all() + ) + owner_service = get_owner_provider_service(owner=current_owner) + + try: + repos_in_github: dict[str, List[str]] = async_to_sync( + owner_service.get_repos_with_languages_graphql + )(owner_username=org_username) + except TorngitRateLimitError: + log.warning( + "Unable to fetch repositories due to rate limit error", + extra=dict(current_owner_id=current_owner_id, org_id=org.ownerid), + ) + return {"successful": False, "error": "torngit_rate_limit_error"} + except TorngitError: + log.warning( + "There was an error in torngit", + extra=dict(current_owner_id=current_owner_id, org_id=org.ownerid), + ) + return {"successful": False, "error": "torngit_error"} + + updated_repoids_for_logging = [] + updated_repos = [] + for db_repo in org_db_repositories: + repo_langs_from_github: Optional[List[str]] = repos_in_github.get( + db_repo.name + ) + if repo_langs_from_github is not None: + updated_repoids_for_logging.append(db_repo.repoid) + updated_repo = { + "repoid": db_repo.repoid, + "languages": repo_langs_from_github, + "languages_last_updated": get_utc_now(), + } + updated_repos.append(updated_repo) + + db_session.bulk_update_mappings(Repository, updated_repos) + db_session.commit() + + log.info( + "Repo languages sync done", + extra=dict(username=org_username, repoids=updated_repoids_for_logging), + ) + + return {"successful": True} + + +RegisteredSyncRepoLanguagesGQLTask = celery_app.register_task( + SyncRepoLanguagesGQLTask() +) +sync_repo_languages_gql_task = celery_app.tasks[RegisteredSyncRepoLanguagesGQLTask.name] diff --git a/apps/worker/tasks/sync_repos.py b/apps/worker/tasks/sync_repos.py new file mode 100644 index 0000000000..970d6f3bb9 --- /dev/null +++ b/apps/worker/tasks/sync_repos.py @@ -0,0 +1,672 @@ +import logging +from datetime import datetime +from typing import List, Optional, Tuple + +from asgiref.sync import async_to_sync +from celery.exceptions import SoftTimeLimitExceeded +from redis.exceptions import LockError +from shared.celery_config import ( + sync_repo_languages_gql_task_name, + sync_repo_languages_task_name, + sync_repos_task_name, +) +from shared.config import get_config +from shared.helpers.redis import get_redis_connection +from shared.torngit.base import TorngitBaseAdapter +from shared.torngit.exceptions import TorngitClientError, TorngitServerFailureError +from sqlalchemy import and_ +from sqlalchemy.orm.session import Session + +from app import celery_app +from database.models import Owner, Repository +from services.owner import get_owner_provider_service +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) +metrics_scope = "worker.SyncReposTask" + + +class SyncReposTask(BaseCodecovTask, name=sync_repos_task_name): + """This task syncs the repos for a user in the same way as the legacy "refresh" task. + + High-level steps: + + 1. Get repos for the user. This is all of the repos owned by the user and those + in other teams/orgs/groups that the user has permission for. If using a GitHub + integration, we get all the repos included in the integration. + + 2. Loop over repos and upsert the owner, repo, and fork (if any) into the database. + + 3. Update the permissions for the user (permissions col in the owners table). + + 4. Set the bot for owners (teams/orgs/groups) that have private repos. This is so + we have a link to a valid token through the bot user for calls to the provider + service (GitHub, Gitlab, Bitbucket, ...). + + 5. Fire off a task to sync every repository's available languages with its provider + after finishing the sync. + + # About the `using_integration` argument + `using_integration` is specific to GitHub users. When `using_integration==True` then this refresh + task came from receiving some INSTALLATION event from GitHub indicating that the app installation + for the user suffered some change. + + In this case we use the installation token to list repos from github, as opposed to the owner's token + (there's possibly a difference in what repos the owner can see and what repos the app installation can see) + """ + + ignore_result = False + + def run_impl( + self, + db_session: Session, + # `previous_results`` is added by celery if the task is chained. + # It contains the results of tasks that came before this one in the chain + previous_results=None, + *, + ownerid: int, + username: Optional[str] = None, + using_integration=False, + manual_trigger=False, + # `repository_service_ids` is optionally passed to the task + # when using_integration=True so we know what are the repos affected. + # Speeds up getting info from the git provider, but not required + # objects are (service_id, node_id) + repository_service_ids: Optional[List[Tuple[str, str]]] = None, + **kwargs, + ): + log.info( + "Sync repos", + extra=dict( + ownerid=ownerid, + username=username, + using_integration=using_integration, + manual_trigger=manual_trigger, + repository_service_ids=repository_service_ids, + ), + ) + owner = db_session.query(Owner).filter(Owner.ownerid == ownerid).first() + + assert owner, "Owner not found" + + lock_name = f"syncrepos_lock_{ownerid}_{using_integration}" + redis_connection = get_redis_connection() + try: + with redis_connection.lock( + lock_name, + timeout=max(300, self.hard_time_limit_task), + blocking_timeout=5, + ): + git = get_owner_provider_service( + owner, + ignore_installation=(not using_integration), + ) + sync_repos_output = {} + if using_integration: + sync_repos_output = async_to_sync( + self.sync_repos_using_integration + )( + db_session, + git, + owner, + username, + repository_service_ids=repository_service_ids, + ) + else: + sync_repos_output = async_to_sync(self.sync_repos)( + db_session, git, owner, username, using_integration + ) + + if get_config( + "setup", "tasks", "sync_repo_languages", "enabled", default=True + ): + self.sync_repos_languages( + sync_repos_output=sync_repos_output, + manual_trigger=manual_trigger, + current_owner=owner, + ) + except LockError: + log.warning("Unable to sync repos because another task is already doing it") + + async def sync_repos_affected_repos_known( + self, + db_session: Session, + git: TorngitBaseAdapter, + owner: Owner, + repository_service_ids: List[Tuple[int, str]] | None, + ): + repoids_added = [] + # Casting to str in case celery interprets the service ID as a integer for some reason + # As that has caused issues with testing locally + service_ids = set(str(x[0]) for x in repository_service_ids) + # Check what repos we already have in the DB + existing_repos = set( + map( + lambda row_result: row_result[0], + db_session.query(Repository.service_id) + .filter(Repository.service_id.in_(service_ids)) + .all(), + ) + ) + missing_repo_service_ids = service_ids.difference(existing_repos) + + log.info( + "Sync missing repos if any", + extra=dict( + ownerid=owner.ownerid, + missing_repo_service_ids=missing_repo_service_ids, + num_missing_repos=len(missing_repo_service_ids), + existing_repos=existing_repos, + repository_service_ids=repository_service_ids, + ), + ) + + # Get info from provider on the repos we don't have + repos_to_search = [ + x[1] + for x in repository_service_ids + if str(x[0]) in missing_repo_service_ids + ] + async for repo_data in git.get_repos_from_nodeids_generator( + repos_to_search, owner.username + ): + # Get or create owner + if repo_data["owner"]["is_expected_owner"]: + new_repo_ownerid = owner.ownerid + else: + upserted_owner_id = self.upsert_owner( + db_session, + git.service, + repo_data["owner"]["service_id"], + repo_data["owner"]["username"], + ) + new_repo_ownerid = upserted_owner_id + # Get or create repo + # Yes we had issues trying to insert a repeated repo at this point. + # Maybe race condition? + repoid = self.upsert_repo( + db_session=db_session, + service=git.service, + ownerid=new_repo_ownerid, + repo_data={**repo_data, "service_id": str(repo_data["service_id"])}, + using_integration=True, + ) + repoids_added.append(repoid) + return repoids_added + + def _possibly_update_ghinstallation_covered_repos( + self, + git: TorngitBaseAdapter, + owner: Owner, + service_ids_listed: List[str], + ): + installation_used = git.data.get("installation") + if installation_used is None: + log.warning( + "Failed to update ghapp covered repos. We don't know which installation is being used" + ) + if ( + owner.github_app_installations is None + or owner.github_app_installations == [] + ): + log.warning( + "Failed to possibly update ghapp covered repos. Owner has no installations", + ) + return + ghapp = next( + filter( + lambda obj: ( + obj.installation_id == installation_used.get("installation_id") + and obj.app_id == installation_used.get("app_id") + ), + owner.github_app_installations, + ), + None, + ) + if ghapp and ghapp.repository_service_ids is not None: + covered_repos = set(ghapp.repository_service_ids) + service_ids_listed_set = set(service_ids_listed) + log.info( + "Updating list of repos covered", + extra=dict( + owner=owner.ownerid, + installation=ghapp.installation_id, + ghapp_id=ghapp.id, + added_repos_service_ids=covered_repos.difference( + service_ids_listed_set + ), + ), + ) + ghapp.repository_service_ids = list(covered_repos | service_ids_listed_set) + + async def sync_repos_using_integration( + self, + db_session: Session, + git: TorngitBaseAdapter, + owner: Owner, + username: str, + repository_service_ids: Optional[List[Tuple[int, str]]] = None, + ): + ownerid = owner.ownerid + log.info( + "Syncing repos using integration", + extra=dict(ownerid=ownerid, username=username), + ) + + repoids = [] + + # We're testing processing repos a page at a time and this helper + # function avoids duplicating the code in the old and new paths + def process_repos(repos): + service_ids = {repo["repo"]["service_id"] for repo in repos} + self._possibly_update_ghinstallation_covered_repos(git, owner, service_ids) + if service_ids: + # Querying through the `Repository` model is cleaner, but we + # need to go through the table object instead if we want to + # use a Postgres `RETURNING` clause like this. + table = Repository.__table__ + update_statement = ( + table.update() + .returning(table.columns.service_id) + .where( + and_( + table.columns.ownerid == ownerid, + table.columns.service_id.in_(service_ids), + ) + ) + .values(using_integration=True) + ) + result = db_session.execute(update_statement) + updated_service_ids = {r[0] for r in result.fetchall()} + + # The set of repos our app can see minus the set of repos we + # just updated = the set of repos we need to insert. + missing_service_ids = service_ids - updated_service_ids + missing_repos = [ + repo + for repo in repos + if repo["repo"]["service_id"] in missing_service_ids + ] + + for repo in missing_repos: + repo_data = repo["repo"] + new_repo = Repository( + ownerid=ownerid, + service_id=repo_data["service_id"], + name=repo_data["name"], + language=repo_data["language"], + private=repo_data["private"], + branch=repo_data["branch"], + using_integration=True, + ) + db_session.add(new_repo) + db_session.flush() + repoids.append(new_repo.repoid) + + # Here comes the actual function + received_repos = False + if repository_service_ids: + # This flow is different from the ones below because the API already informed us the repos affected + # So we can update those values directly + repoids_added = await self.sync_repos_affected_repos_known( + db_session, git, owner, repository_service_ids + ) + repoids = repoids_added + # Below logic may not be needed if repository_service_ids exist, but + # we have run into issues related to the sync task when repos are known + # So we should still run it just in case and possibly update GithubInstallation.repository_service_ids + # Instead of relying exclusively on the webhooks to do that + # TODO: Maybe we don't need to run this every time, but once in a while just in case... + async for page in git.list_repos_using_installation_generator(username): + if page: + received_repos = True + process_repos(page) + + # If the installation returned no repos, we were probably disabled and + # should indicate as much on this owner's repositories. + if not received_repos: + db_session.query(Repository).filter( + Repository.ownerid == ownerid, Repository.using_integration.is_(True) + ).update({Repository.using_integration: False}, synchronize_session=False) + + log.info( + "Repo sync using integration done", + extra=dict(repoids_created=repoids, repoids_created_count=len(repoids)), + ) + + return { + "service": git.service, + "org_usernames": [owner.username], + "repoids": repoids, + } + + async def sync_repos( + self, + db_session: Session, + git, + owner: Owner, + username: Optional[str], + using_integration: bool, + ): + service = owner.service + ownerid = owner.ownerid + private_project_ids = [] + + log.info( + "Syncing repos without integration", + extra=dict(ownerid=ownerid, username=username, service=service), + ) + + repoids = [] + owners_by_id = {} + + # We're testing processing repos a page at a time and this helper + # function avoids duplicating the code in the old and new paths + def process_repos(repos): + for repo in repos: + _ownerid = owners_by_id.get( + ( + service, + repo["owner"]["service_id"], + repo["owner"]["username"], + ) + ) + if not _ownerid: + _ownerid = self.upsert_owner( + db_session, + service, + repo["owner"]["service_id"], + repo["owner"]["username"], + ) + owners_by_id[ + ( + service, + repo["owner"]["service_id"], + repo["owner"]["username"], + ) + ] = _ownerid + + repoid = self.upsert_repo( + db_session, service, _ownerid, repo["repo"], using_integration + ) + + repoids.append(repoid) + + if repo["repo"].get("fork"): + _ownerid = self.upsert_owner( + db_session, + service, + repo["repo"]["fork"]["owner"]["service_id"], + repo["repo"]["fork"]["owner"]["username"], + ) + + _repoid = self.upsert_repo( + db_session, service, _ownerid, repo["repo"]["fork"]["repo"] + ) + + repoids.append(_repoid) + + if repo["repo"]["fork"]["repo"]["private"]: + private_project_ids.append(int(_repoid)) + if repo["repo"]["private"]: + private_project_ids.append(int(repoid)) + db_session.commit() + + try: + async for page in git.list_repos_generator(): + process_repos(page) + + except ( + SoftTimeLimitExceeded, + TorngitClientError, + TorngitServerFailureError, + ) as e: + old_permissions = owner.permission or [] + if isinstance(e, SoftTimeLimitExceeded): + error_string = "System timed out while listing repos" + else: + error_string = "Torngit failure while listing repos" + + log.error( + f"{error_string}. Permissions list may be incomplete", + exc_info=True, + extra=dict( + ownerid=owner.ownerid, + number_old_permissions=len(old_permissions), + number_new_permissions=len(set(private_project_ids)), + ), + ) + + log.info( + "Updating permissions", + extra=dict( + ownerid=ownerid, username=username, privaterepoids=private_project_ids + ), + ) + old_permissions = owner.permission or [] + removed_permissions = set(old_permissions) - set(private_project_ids) + if removed_permissions: + log.warning( + "Owner had permissions that are being removed", + extra=dict( + old_permissions=old_permissions[:100], + number_old_permissions=len(old_permissions), + new_permissions=private_project_ids[:100], + number_new_permissions=len(private_project_ids), + removed_permissions=sorted(removed_permissions), + ownerid=ownerid, + username=owner.username, + ), + ) + + # update user permissions + owner.permission = sorted(set(private_project_ids)) + + log.info( + "Repo sync done", + extra=dict(ownerid=ownerid, username=username, repoids=repoids), + ) + + return { + "service": git.service, + "org_usernames": [item[2] for item in owners_by_id.keys()], + "repoids": repoids, + } + + def upsert_owner( + self, db_session: Session, service: str, service_id: int, username: str + ): + log.info( + "Upserting owner", + extra=dict(git_service=service, service_id=service_id, username=username), + ) + owner = ( + db_session.query(Owner) + .filter(Owner.service == service, Owner.service_id == service_id) + .first() + ) + + if owner: + if (owner.username or "").lower() != username.lower(): + owner.username = username + else: + owner = Owner( + service=service, + service_id=service_id, + username=username, + createstamp=datetime.now(), + ) + db_session.add(owner) + db_session.flush() + + return owner.ownerid + + def upsert_repo( + self, + db_session: Session, + service: str, + ownerid: int, + repo_data, + using_integration: Optional[bool] = None, + ): + log.info( + "Upserting repo", + extra=dict(ownerid=ownerid, repo_data=repo_data), + ) + repo = ( + db_session.query(Repository) + .filter( + Repository.ownerid == ownerid, + Repository.service_id == repo_data["service_id"], + ) + .first() + ) + + if repo: + # Found the exact repo. Let's just update + has_changes = False + if repo.private != repo_data["private"]: + repo.private = repo_data["private"] + has_changes = True + if repo.language != repo_data["language"]: + repo.language = repo_data["language"] + has_changes = True + if repo.name != repo_data["name"]: + repo.name = repo_data["name"] + has_changes = True + if repo.deleted is not False: + repo.deleted = False + has_changes = True + if has_changes: + repo.updatestamp = datetime.now() + repo_id = repo.repoid + return repo_id + # repo was not found, could be a different owner + repo_correct_serviceid_wrong_owner = ( + db_session.query(Repository) + .join(Owner, Repository.ownerid == Owner.ownerid) + .filter( + Repository.deleted == False, + Repository.service_id == repo_data["service_id"], + Owner.service == service, + ) + .first() + ) + # repo was not found, could be a different service_id + repo_correct_owner_wrong_service_id = ( + db_session.query(Repository) + .filter(Repository.ownerid == ownerid, Repository.name == repo_data["name"]) + .first() + ) + if ( + repo_correct_serviceid_wrong_owner is not None + and repo_correct_owner_wrong_service_id is not None + ): + # But it cannot be both different owner and different service_id + log.warning( + "There is a repo with the right service_id and a repo with the right slug, but they are not the same", + extra=dict( + repo_data=repo_data, + repo_correct_serviceid_wrong_owner=dict( + repoid=repo_correct_serviceid_wrong_owner.repoid, + slug=repo_correct_serviceid_wrong_owner.slug, + service_id=repo_correct_serviceid_wrong_owner.service_id, + ), + repo_correct_owner_wrong_service_id=dict( + repoid=repo_correct_owner_wrong_service_id.repoid, + slug=repo_correct_owner_wrong_service_id.slug, + service_id=repo_correct_owner_wrong_service_id.service_id, + ), + ), + ) + # We will have to assume the user has access to the service_id one, since + # the service_id is the Github identity value + return repo_correct_serviceid_wrong_owner.repoid + + if repo_correct_serviceid_wrong_owner: + repo_id = repo_correct_serviceid_wrong_owner.repoid + # repo exists, but wrong owner + repo_wrong_owner = repo_correct_serviceid_wrong_owner + log.info( + "Updating repo - wrong owner", + extra=dict( + ownerid=ownerid, + repo_id=repo_wrong_owner.repoid, + repo_name=repo_data["name"], + ), + ) + repo_wrong_owner.ownerid = ownerid + repo_wrong_owner.private = repo_data["private"] + repo_wrong_owner.language = repo_data["language"] + repo_wrong_owner.name = repo_data["name"] + repo_wrong_owner.deleted = False + repo_wrong_owner.updatestamp = datetime.now() + db_session.flush() + return repo_wrong_owner.repoid + # could be correct owner but wrong service_id (repo deleted and recreated) + if repo_correct_owner_wrong_service_id: + log.info( + "Updating repo - correct owner, wrong service_id", + extra=dict( + ownerid=ownerid, + repo_id=repo_correct_owner_wrong_service_id.service_id, + repo_name=repo_data["name"], + ), + ) + repo_correct_owner_wrong_service_id.service_id = repo_data["service_id"] + repo_correct_owner_wrong_service_id.name = repo_data["name"] + repo_correct_owner_wrong_service_id.language = repo_data["language"] + repo_correct_owner_wrong_service_id.private = repo_data["private"] + repo_correct_owner_wrong_service_id.using_integration = using_integration + repo_correct_owner_wrong_service_id.updatestamp = datetime.now() + db_session.flush() + return repo_correct_owner_wrong_service_id.repoid + # repo does not exist, create it + new_repo = Repository( + ownerid=ownerid, + service_id=repo_data["service_id"], + name=repo_data["name"], + language=repo_data["language"], + private=repo_data["private"], + branch=repo_data["branch"], + using_integration=using_integration, + ) + log.info( + "Inserting repo", + extra=dict( + ownerid=ownerid, repo_id=new_repo.repoid, repo_name=new_repo.name + ), + ) + db_session.add(new_repo) + db_session.flush() + return new_repo.repoid + + def sync_repos_languages( + self, sync_repos_output: dict, manual_trigger: bool, current_owner: Owner + ): + log.info( + "Syncing repos languages", + extra=dict( + ownerid=current_owner.ownerid, + sync_repos_output=sync_repos_output, + manual_trigger=manual_trigger, + ), + ) + if sync_repos_output: + if sync_repos_output["service"] == "github": + for owner_username in sync_repos_output["org_usernames"]: + self.app.tasks[sync_repo_languages_gql_task_name].apply_async( + kwargs=dict( + org_username=owner_username, + current_owner_id=current_owner.ownerid, + ) + ) + else: + for repoid in sync_repos_output["repoids"]: + self.app.tasks[sync_repo_languages_task_name].apply_async( + kwargs=dict(repoid=repoid, manual_trigger=manual_trigger) + ) + + +RegisteredSyncReposTask = celery_app.register_task(SyncReposTask()) +sync_repos_task = celery_app.tasks[SyncReposTask.name] diff --git a/apps/worker/tasks/sync_teams.py b/apps/worker/tasks/sync_teams.py new file mode 100644 index 0000000000..1f0e42016b --- /dev/null +++ b/apps/worker/tasks/sync_teams.py @@ -0,0 +1,101 @@ +import logging +from datetime import datetime + +from asgiref.sync import async_to_sync +from shared.celery_config import sync_teams_task_name + +from app import celery_app +from database.models import Owner +from services.owner import get_owner_provider_service +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class SyncTeamsTask(BaseCodecovTask, name=sync_teams_task_name): + """This task syncs the orgs/teams that a user belongs to""" + + ignore_result = False + + def run_impl(self, db_session, ownerid, *, username=None, **kwargs): + log.info("Sync teams", extra=dict(ownerid=ownerid, username=username)) + owner = db_session.query(Owner).filter(Owner.ownerid == ownerid).first() + + assert owner, "Owner not found" + service = owner.service + + git = get_owner_provider_service(owner, ignore_installation=True) + + # get list of teams with username, name, email, id (service_id), etc + teams = async_to_sync(git.list_teams)() + + updated_teams = [] + + for team in teams: + team_data = dict( + username=team["username"], + name=team["name"], + email=team.get("email"), + avatar_url=team.get("avatar_url"), + parent_service_id=team.get("parent_id"), + ) + team_ownerid = self.upsert_team( + db_session, service, str(team["id"]), team_data + ) + team_data["ownerid"] = team_ownerid + updated_teams.append(team_data) + + team_ids = [team["ownerid"] for team in updated_teams] + + removed_orgs = set(owner.organizations or []) - set(team_ids) + if removed_orgs: + log.warning( + "Owner had access to organization that are being removed", + extra=dict( + old_orgs=owner.organizations, + new_orgs=team_ids, + removed_orgs=sorted(removed_orgs), + ownerid=ownerid, + ), + ) + + owner.updatestamp = datetime.now() + owner.organizations = team_ids + + def upsert_team(self, db_session, service, service_id, data): + log.info( + "Upserting team", + extra=dict(git_service=service, service_id=service_id, data=data), + ) + team = ( + db_session.query(Owner) + .filter(Owner.service == service, Owner.service_id == str(service_id)) + .first() + ) + + if team: + team.username = data["username"] + team.name = data["name"] + team.email = data.get("email") + team.avatar_url = data.get("avatar_url") + team.parent_service_id = data.get("parent_service_id") + team.updatestamp = datetime.now() + else: + team = Owner( + service=service, + service_id=service_id, + username=data["username"], + name=data["name"], + email=data.get("email"), + avatar_url=data.get("avatar_url"), + parent_service_id=data.get("parent_service_id"), + createstamp=datetime.now(), + ) + db_session.add(team) + db_session.flush() + + return team.ownerid + + +RegisteredSyncTeamsTask = celery_app.register_task(SyncTeamsTask()) +sync_teams_task = celery_app.tasks[SyncTeamsTask.name] diff --git a/apps/worker/tasks/test_results_finisher.py b/apps/worker/tasks/test_results_finisher.py new file mode 100644 index 0000000000..b11d92a9a0 --- /dev/null +++ b/apps/worker/tasks/test_results_finisher.py @@ -0,0 +1,399 @@ +import logging +from typing import Any, Literal + +from asgiref.sync import async_to_sync +from shared.helpers.redis import get_redis_connection +from shared.reports.types import UploadType +from shared.typings.torngit import AdditionalData +from shared.yaml import UserYaml +from sqlalchemy.orm import Session + +from app import celery_app +from database.enums import ReportType +from database.models import Commit, Flake, Repository, TestResultReportTotals +from helpers.checkpoint_logger.flows import TestResultsFlow +from helpers.notifier import NotifierResult +from helpers.string import EscapeEnum, Replacement, StringEscaper, shorten_file_paths +from services.activation import activate_user, schedule_new_user_activated_task +from services.lock_manager import LockManager, LockRetry, LockType +from services.repository import ( + fetch_and_update_pull_request_information_from_commit, + get_repo_provider_service, +) +from services.seats import ShouldActivateSeat, determine_seat_activation +from services.test_analytics.ta_metrics import ( + read_failures_summary, + read_tests_totals_summary, +) +from services.test_results import ( + FinisherResult, + FlakeInfo, + TACommentInDepthInfo, + TestResultsNotificationFailure, + TestResultsNotificationPayload, + TestResultsNotifier, + get_test_summary_for_commit, + latest_failures_for_commit, + should_do_flaky_detection, +) +from tasks.base import BaseCodecovTask +from tasks.cache_test_rollups import cache_test_rollups_task_name +from tasks.notify import notify_task_name +from tasks.process_flakes import NEW_KEY, process_flakes_task_name + +log = logging.getLogger(__name__) + +test_results_finisher_task_name = "app.tasks.test_results.TestResultsFinisherTask" + +ESCAPE_FAILURE_MESSAGE_DEFN = [ + Replacement(["\r"], "", EscapeEnum.REPLACE), +] + + +class TestResultsFinisherTask(BaseCodecovTask, name=test_results_finisher_task_name): + def run_impl( + self, + db_session: Session, + chain_result: bool, + *, + repoid: int, + commitid: str, + commit_yaml: dict, + impl_type: Literal["old", "new", "both"] = "old", + **kwargs, + ): + repoid = int(repoid) + + self.extra_dict: dict[str, Any] = { + "commit_yaml": commit_yaml, + "impl_type": impl_type, + } + log.info("Starting test results finisher task", extra=self.extra_dict) + + lock_manager = LockManager( + repoid=repoid, + commitid=commitid, + report_type=ReportType.COVERAGE, + lock_timeout=max(80, self.hard_time_limit_task), + ) + + try: + # this needs to be the coverage notification lock + # since both tests post/edit the same comment + with lock_manager.locked( + LockType.NOTIFICATION, + retry_num=self.request.retries, + ): + finisher_result = self.process_impl_within_lock( + db_session=db_session, + repoid=repoid, + commitid=commitid, + commit_yaml=UserYaml.from_dict(commit_yaml), + chain_result=chain_result, + impl_type=impl_type, + **kwargs, + ) + if finisher_result["queue_notify"]: + self.app.tasks[notify_task_name].apply_async( + args=None, + kwargs=dict( + repoid=repoid, + commitid=commitid, + current_yaml=commit_yaml, + ), + ) + + return finisher_result + + except LockRetry as retry: + self.retry(max_retries=5, countdown=retry.countdown) + + def process_impl_within_lock( + self, + *, + db_session: Session, + repoid: int, + commitid: str, + commit_yaml: UserYaml, + chain_result: bool, + impl_type: Literal["old", "both", "new"], + **kwargs, + ) -> FinisherResult: + log.info("Running test results finishers", extra=self.extra_dict) + TestResultsFlow.log(TestResultsFlow.TEST_RESULTS_FINISHER_BEGIN) + + commit: Commit = ( + db_session.query(Commit).filter_by(repoid=repoid, commitid=commitid).first() + ) + assert commit, "commit not found" + repo = commit.repository + + return self.old_impl( + db_session, repo, commit, chain_result, commit_yaml, impl_type + ) + + def old_impl( + self, + db_session: Session, + repo: Repository, + commit: Commit, + chain_result: bool, + commit_yaml: UserYaml, + impl_type: Literal["old", "both", "new"], + ) -> FinisherResult: + repoid = repo.repoid + commitid = commit.commitid + redis_client = get_redis_connection() + + if should_do_flaky_detection(repo, commit_yaml): + if commit.merged is True or commit.branch == repo.branch: + redis_client.lpush(NEW_KEY.format(repo.repoid), commit.commitid) + self.app.tasks[process_flakes_task_name].apply_async( + kwargs=dict( + repo_id=repoid, + impl_type=impl_type, + ) + ) + + if commit.branch is not None: + self.app.tasks[cache_test_rollups_task_name].apply_async( + kwargs=dict( + repo_id=repoid, + branch=commit.branch, + impl_type=impl_type, + ), + ) + + commit_report = commit.commit_report(ReportType.TEST_RESULTS) + + totals = commit_report.test_result_totals + if totals is None: + totals = TestResultReportTotals( + report_id=commit_report.id, + ) + totals.passed = 0 + totals.skipped = 0 + totals.failed = 0 + totals.error = "failure" + db_session.add(totals) + db_session.flush() + + if not chain_result: + # every processor errored, nothing to notify on + queue_notify = False + + # if error is None this whole process should be a noop + if totals.error is not None: + # make an attempt to make test results comment + notifier = TestResultsNotifier(commit, commit_yaml) + success, reason = notifier.error_comment() + + # also make attempt to make coverage comment + queue_notify = True + + return { + "notify_attempted": False, + "notify_succeeded": False, + "queue_notify": queue_notify, + } + + # if we succeed once, error should be None for this commit forever + if totals.error is not None: + totals.error = None + db_session.flush() + + cached_uploads: dict[int, dict] = dict() + escaper = StringEscaper(ESCAPE_FAILURE_MESSAGE_DEFN) + shorten_paths = commit_yaml.read_yaml_field( + "test_analytics", "shorten_paths", _else=True + ) + + with read_tests_totals_summary.labels("old").time(): + test_summary = get_test_summary_for_commit(db_session, repoid, commitid) + failed_tests = test_summary.get("error", 0) + test_summary.get("failure", 0) + passed_tests = test_summary.get("pass", 0) + skipped_tests = test_summary.get("skip", 0) + + failures = [] + if failed_tests: + with read_failures_summary.labels("old").time(): + failed_test_instances = latest_failures_for_commit( + db_session, repoid, commitid + ) + + for test_instance in failed_test_instances: + failure_message = test_instance.failure_message + if failure_message is not None: + if shorten_paths: + failure_message = shorten_file_paths(failure_message) + failure_message = escaper.replace(failure_message) + + if test_instance.upload_id not in cached_uploads: + upload = test_instance.upload + cached_uploads[test_instance.upload_id] = { + "flag_names": sorted(upload.flag_names), + "build_url": upload.build_url, + } + upload = cached_uploads[test_instance.upload_id] + + failures.append( + TestResultsNotificationFailure( + display_name=test_instance.test.computed_name + if test_instance.test.computed_name is not None + else test_instance.test.name, + failure_message=failure_message, + test_id=test_instance.test_id, + envs=upload["flag_names"], + duration_seconds=test_instance.duration_seconds, + build_url=upload["build_url"], + ) + ) + + totals.passed = passed_tests + totals.skipped = skipped_tests + totals.failed = failed_tests + db_session.flush() + + if failed_tests == 0: + return { + "notify_attempted": False, + "notify_succeeded": False, + "queue_notify": True, + } + + additional_data: AdditionalData = {"upload_type": UploadType.TEST_RESULTS} + repo_service = get_repo_provider_service(repo, additional_data=additional_data) + pull = async_to_sync(fetch_and_update_pull_request_information_from_commit)( + repo_service, commit, commit_yaml + ) + + if not pull: + success = False + attempted = False + notifier_result = NotifierResult.NO_PULL + elif not commit_yaml.read_yaml_field("comment", _else=True): + success = False + attempted = False + notifier_result = NotifierResult.NO_COMMENT + else: + activate_seat_info = determine_seat_activation(pull) + + should_show_upgrade_message = True + + match activate_seat_info.should_activate_seat: + case ShouldActivateSeat.AUTO_ACTIVATE: + assert activate_seat_info.owner_id + assert activate_seat_info.author_id + successful_activation = activate_user( + db_session=db_session, + org_ownerid=activate_seat_info.owner_id, + user_ownerid=activate_seat_info.author_id, + ) + if successful_activation: + schedule_new_user_activated_task( + activate_seat_info.owner_id, + activate_seat_info.author_id, + ) + should_show_upgrade_message = False + case ShouldActivateSeat.MANUAL_ACTIVATE: + pass + case ShouldActivateSeat.NO_ACTIVATE: + should_show_upgrade_message = False + + if should_show_upgrade_message: + notifier = TestResultsNotifier( + commit, commit_yaml, _pull=pull, _repo_service=repo_service + ) + success, reason = notifier.upgrade_comment() + + self.extra_dict["success"] = success + self.extra_dict["reason"] = reason + log.info("Made upgrade comment", extra=self.extra_dict) + + return { + "notify_attempted": True, + "notify_succeeded": success, + "queue_notify": False, + } + + flaky_tests = dict() + if should_do_flaky_detection(repo, commit_yaml): + flaky_tests = self.get_flaky_tests(db_session, repoid, failures) + + failures = sorted(failures, key=lambda x: x.duration_seconds)[:3] + info = TACommentInDepthInfo(failures, flaky_tests) + payload = TestResultsNotificationPayload( + failed_tests, passed_tests, skipped_tests, info + ) + notifier = TestResultsNotifier( + commit, + commit_yaml, + payload=payload, + _pull=pull, + _repo_service=repo_service, + ) + + if repo.private == False: + log.info( + "making TA comment", + extra=dict( + pullid=pull.database_pull.pullid, + service=repo.service, + slug=f"{repo.owner.username}/{repo.name}", + ), + ) + notifier_result = notifier.notify() + success = ( + True if notifier_result is NotifierResult.COMMENT_POSTED else False + ) + TestResultsFlow.log(TestResultsFlow.TEST_RESULTS_NOTIFY) + if len(flaky_tests): + log.info( + "Detected failure on test that has been identified as flaky", + extra=dict( + success=success, + notifier_result=notifier_result.value, + test_ids=list(flaky_tests.keys()), + ), + ) + attempted = True + + self.extra_dict["success"] = success + self.extra_dict["notifier_result"] = notifier_result.value + log.info("Finished test results notify", extra=self.extra_dict) + + return { + "notify_attempted": attempted, + "notify_succeeded": success, + "queue_notify": False, + } + + def get_flaky_tests( + self, + db_session: Session, + repoid: int, + failures: list[TestResultsNotificationFailure[str]], + ) -> dict[str, FlakeInfo]: + failure_test_ids = [failure.test_id for failure in failures] + + matching_flakes = list( + db_session.query(Flake) + .filter( + Flake.repoid == repoid, + Flake.testid.in_(failure_test_ids), + Flake.end_date.is_(None), + Flake.count != (Flake.recent_passes_count + Flake.fail_count), + ) + .limit(100) + .all() + ) + + flaky_test_ids = { + flake.testid: FlakeInfo(flake.fail_count, flake.count) + for flake in matching_flakes + } + return flaky_test_ids + + +RegisteredTestResultsFinisherTask = celery_app.register_task(TestResultsFinisherTask()) +test_results_finisher_task = celery_app.tasks[RegisteredTestResultsFinisherTask.name] diff --git a/apps/worker/tasks/test_results_processor.py b/apps/worker/tasks/test_results_processor.py new file mode 100644 index 0000000000..1b52a8ef24 --- /dev/null +++ b/apps/worker/tasks/test_results_processor.py @@ -0,0 +1,503 @@ +from __future__ import annotations + +import logging +from dataclasses import dataclass +from datetime import date, datetime +from typing import Literal, TypedDict + +import sentry_sdk +import test_results_parser +from shared.celery_config import test_results_processor_task_name +from shared.config import get_config +from shared.yaml import UserYaml +from sqlalchemy.dialects.postgresql import insert +from sqlalchemy.orm import Session + +from app import celery_app +from database.models import ( + DailyTestRollup, + Flake, + Repository, + RepositoryFlag, + Test, + TestFlagBridge, + TestInstance, + Upload, +) +from services.archive import ArchiveService +from services.processing.types import UploadArguments +from services.test_analytics.ta_metrics import write_tests_summary +from services.test_analytics.ta_processor import ta_processor_impl +from services.test_results import generate_flags_hash, generate_test_id +from services.yaml import read_yaml_field +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +@dataclass +class ReadableFile: + path: str + contents: bytes + + +def get_repo_flag_ids(db_session: Session, repoid: int, flags: list[str]) -> set[int]: + if not flags: + return set() + + return set( + db_session.query(RepositoryFlag.id_) + .filter( + RepositoryFlag.repository_id == repoid, + RepositoryFlag.flag_name.in_(flags), + ) + .all() + ) + + +def create_daily_totals( + daily_totals: dict, + test_id: str, + repoid: int, + duration_seconds: float, + outcome: Literal["pass", "failure", "error", "skip"], + branch: str, + commitid: str, + flaky_test_set: set[str], +): + daily_totals[test_id] = { + "test_id": test_id, + "repoid": repoid, + "last_duration_seconds": duration_seconds, + "avg_duration_seconds": duration_seconds, + "pass_count": 1 if outcome == "pass" else 0, + "fail_count": 1 if outcome == "failure" or outcome == "error" else 0, + "skip_count": 1 if outcome == "skip" else 0, + "flaky_fail_count": 1 + if test_id in flaky_test_set and (outcome == "failure" or outcome == "error") + else 0, + "branch": branch, + "date": date.today(), + "latest_run": datetime.now(), + "commits_where_fail": [commitid] + if outcome == "failure" or outcome == "error" + else [], + } + + +def update_daily_totals( + daily_totals: dict, + test_id: str, + duration_seconds: float, + outcome: Literal["pass", "failure", "error", "skip"], +): + daily_totals[test_id]["last_duration_seconds"] = duration_seconds + + # logic below is a little complicated but we're basically doing: + + # (old_avg * num of values used to compute old avg) + new value + # ------------------------------------------------------------- + # num of values used to compute old avg + 1 + daily_totals[test_id]["avg_duration_seconds"] = ( + daily_totals[test_id]["avg_duration_seconds"] + * (daily_totals[test_id]["pass_count"] + daily_totals[test_id]["fail_count"]) + + duration_seconds + ) / (daily_totals[test_id]["pass_count"] + daily_totals[test_id]["fail_count"] + 1) + + if outcome == "pass": + daily_totals[test_id]["pass_count"] += 1 + elif outcome == "failure" or outcome == "error": + daily_totals[test_id]["fail_count"] += 1 + elif outcome == "skip": + daily_totals[test_id]["skip_count"] += 1 + + +@dataclass +class PytestName: + actual_class_name: str + test_file_path: str + + +class DailyTotals(TypedDict): + test_id: str + repoid: int + pass_count: int + fail_count: int + skip_count: int + flaky_fail_count: int + branch: str + date: date + latest_run: datetime + commits_where_fail: list[str] + last_duration_seconds: float + avg_duration_seconds: float + + +class TestResultsProcessorTask(BaseCodecovTask, name=test_results_processor_task_name): + __test__ = False + + def run_impl( + self, + db_session, + previous_result: bool, + *args, + repoid: int, + commitid: str, + commit_yaml, + arguments_list: list[UploadArguments], + impl_type: Literal["old", "new", "both"] = "old", + **kwargs, + ) -> bool: + if impl_type == "new" or impl_type == "both": + running_alone = impl_type == "new" + for argument in arguments_list: + ta_processor_impl( + repoid=repoid, + commitid=commitid, + commit_yaml=commit_yaml, + argument=argument, + update_state=running_alone, + ) + + if running_alone: + return True + + commit_yaml = UserYaml(commit_yaml) + repoid = int(repoid) + + results = [] + + repo_flakes = ( + db_session.query(Flake.testid) + .filter(Flake.repoid == repoid, Flake.end_date.is_(None)) + .all() + ) + flaky_test_set = {flake.testid for flake in repo_flakes} + repository = ( + db_session.query(Repository) + .filter(Repository.repoid == int(repoid)) + .first() + ) + + should_delete_archive = self.should_delete_archive(commit_yaml) + archive_service = ArchiveService(repository) + + # process each report session's test information + for arguments in arguments_list: + upload = ( + db_session.query(Upload).filter_by(id_=arguments["upload_id"]).first() + ) + result = self.process_individual_upload( + db_session, + archive_service, + repository, + commitid, + upload, + flaky_test_set, + should_delete_archive, + ) + + results.append(result) + + return previous_result or any(result.get("successful") for result in results) + + @sentry_sdk.trace + def _bulk_write_tests_to_db( + self, + db_session: Session, + repoid: int, + commitid: str, + upload_id: int, + branch: str, + parsing_results: list[test_results_parser.ParsingInfo], + flaky_test_set: set[str], + flags: list[str], + ): + log.info("Writing tests to database", extra=dict(upload_id=upload_id)) + test_data = {} + test_instance_data = [] + test_flag_bridge_data: list[dict] = [] + daily_totals: dict[str, DailyTotals] = dict() + + flags_hash = generate_flags_hash(flags) + repo_flag_ids = get_repo_flag_ids(db_session, repoid, flags) + + for p in parsing_results: + framework = p["framework"] + + for testrun in p["testruns"]: + # Build up the data for bulk insert + name: str = f"{testrun['classname']}\x1f{testrun['name']}" + testsuite: str = testrun["testsuite"] + outcome = testrun["outcome"] + duration_seconds: float = ( + testrun["duration"] if testrun["duration"] is not None else 0.0 + ) + failure_message: str | None = testrun["failure_message"] + test_id: str = generate_test_id(repoid, testsuite, name, flags_hash) + computed_name = testrun["computed_name"] + filename: str | None = testrun["filename"] + + test_data[(repoid, name, testsuite, flags_hash)] = dict( + id=test_id, + repoid=repoid, + name=name, + testsuite=testsuite, + flags_hash=flags_hash, + framework=framework, + filename=filename, + computed_name=computed_name, + ) + + if repo_flag_ids: + test_flag_bridge_data.extend( + {"test_id": test_id, "flag_id": flag_id} + for flag_id in repo_flag_ids + ) + + test_instance_data.append( + dict( + test_id=test_id, + upload_id=upload_id, + duration_seconds=duration_seconds, + outcome=outcome, + failure_message=failure_message, + commitid=commitid, + branch=branch, + reduced_error_id=None, + repoid=repoid, + ) + ) + + if outcome != "skip": + if test_id in daily_totals: + update_daily_totals( + daily_totals, test_id, duration_seconds, outcome + ) + else: + create_daily_totals( + daily_totals, + test_id, + repoid, + duration_seconds, + outcome, + branch, + commitid, + flaky_test_set, + ) + + # Upsert Tests + if len(test_data) > 0: + sorted_tests = sorted( + test_data.values(), + key=lambda x: str(x["id"]), + ) + self.save_tests(db_session, sorted_tests) + + log.info("Upserted tests to database", extra=dict(upload_id=upload_id)) + + if len(test_flag_bridge_data) > 0: + self.save_test_flag_bridges(db_session, test_flag_bridge_data) + + log.info( + "Inserted new test flag bridges to database", + extra=dict(upload_id=upload_id), + ) + + if len(daily_totals) > 0: + sorted_rollups = sorted( + daily_totals.values(), key=lambda x: str(x["test_id"]) + ) + self.save_daily_test_rollups(db_session, sorted_rollups) + + log.info( + "Upserted daily test rollups to database", + extra=dict(upload_id=upload_id), + ) + + # Save TestInstances + if len(test_instance_data) > 0: + self.save_test_instances(db_session, test_instance_data) + + log.info( + "Inserted test instances to database", extra=dict(upload_id=upload_id) + ) + + def save_tests(self, db_session: Session, test_data: list[dict]): + test_insert = insert(Test.__table__).values(test_data) + insert_on_conflict_do_update = test_insert.on_conflict_do_update( + index_elements=["id"], + set_={ + "framework": test_insert.excluded.framework, + "computed_name": test_insert.excluded.computed_name, + "filename": test_insert.excluded.filename, + }, + ) + db_session.execute(insert_on_conflict_do_update) + db_session.commit() + + def save_daily_test_rollups( + self, db_session: Session, daily_rollups: list[DailyTotals] + ): + rollup_table = DailyTestRollup.__table__ + stmt = insert(rollup_table).values(daily_rollups) + stmt = stmt.on_conflict_do_update( + index_elements=[ + "repoid", + "branch", + "test_id", + "date", + ], + set_={ + "last_duration_seconds": stmt.excluded.last_duration_seconds, + "avg_duration_seconds": ( + rollup_table.c.avg_duration_seconds + * (rollup_table.c.pass_count + rollup_table.c.fail_count) + + stmt.excluded.avg_duration_seconds + ) + / (rollup_table.c.pass_count + rollup_table.c.fail_count + 1), + "latest_run": stmt.excluded.latest_run, + "pass_count": rollup_table.c.pass_count + stmt.excluded.pass_count, + "skip_count": rollup_table.c.skip_count + stmt.excluded.skip_count, + "fail_count": rollup_table.c.fail_count + stmt.excluded.fail_count, + "flaky_fail_count": rollup_table.c.flaky_fail_count + + stmt.excluded.flaky_fail_count, + "commits_where_fail": rollup_table.c.commits_where_fail + + stmt.excluded.commits_where_fail, + }, + ) + + db_session.execute(stmt) + db_session.commit() + + def save_test_instances(self, db_session: Session, test_instance_data: list[dict]): + insert_test_instances = insert(TestInstance.__table__).values( + test_instance_data + ) + db_session.execute(insert_test_instances) + db_session.commit() + + def save_test_flag_bridges( + self, db_session: Session, test_flag_bridge_data: list[dict] + ): + insert_on_conflict_do_nothing_flags = ( + insert(TestFlagBridge.__table__) + .values(test_flag_bridge_data) + .on_conflict_do_nothing(index_elements=["test_id", "flag_id"]) + ) + db_session.execute(insert_on_conflict_do_nothing_flags) + db_session.commit() + + def parse_file( + self, + file_bytes: bytes, + upload: Upload, + ) -> tuple[list[test_results_parser.ParsingInfo], bytes] | None: + try: + parsing_infos, readable_files = test_results_parser.parse_raw_upload( + file_bytes + ) + return parsing_infos, readable_files + except RuntimeError as exc: + log.error( + "Error parsing file", + extra=dict( + repoid=upload.report.commit.repoid, + commitid=upload.report.commit_id, + uploadid=upload.id, + parser_err_msg=str(exc), + upload_state=upload.state, + ), + ) + sentry_sdk.capture_exception(exc, tags={"upload_state": upload.state}) + return None + + def process_individual_upload( + self, + db_session, + archive_service: ArchiveService, + repository: Repository, + commitid, + upload: Upload, + flaky_test_set: set[str], + should_delete_archive: bool, + ): + upload_id = upload.id + + log.info("Processing individual upload", extra=dict(upload_id=upload_id)) + if upload.state == "processed": + return {"successful": True} + elif upload.state == "has_failed": + return {"successful": False} + + payload_bytes = archive_service.read_file(upload.storage_path) + parsing_results: list[test_results_parser.ParsingInfo] = [] + report_contents: list[ReadableFile] = [] + + result = self.parse_file(payload_bytes, upload) + if result is None: + upload.state = "has_failed" + db_session.commit() + return {"successful": False} + + parsing_results, readable_files = result + + if all(len(result["testruns"]) == 0 for result in parsing_results): + successful = False + log.error( + "No test result files were successfully parsed for this upload", + extra=dict(upload_id=upload_id), + ) + else: + successful = True + + with write_tests_summary.labels("old").time(): + self._bulk_write_tests_to_db( + db_session, + repository.repoid, + commitid, + upload_id, + upload.report.commit.branch, + parsing_results, + flaky_test_set, + upload.flag_names, + ) + + upload.state = "processed" + db_session.commit() + + log.info( + "Finished processing individual upload", extra=dict(upload_id=upload_id) + ) + + if should_delete_archive: + self.delete_archive(archive_service, upload) + else: + log.info( + "Writing readable files to archive", extra=dict(upload_id=upload_id) + ) + archive_service.write_file(upload.storage_path, readable_files) + + return {"successful": successful} + + def should_delete_archive(self, commit_yaml): + if get_config("services", "minio", "expire_raw_after_n_days"): + return True + return not read_yaml_field( + commit_yaml, ("codecov", "archive", "uploads"), _else=True + ) + + def delete_archive(self, archive_service: ArchiveService, upload: Upload): + archive_url = upload.storage_path + if archive_url and not archive_url.startswith("http"): + log.info( + "Deleting uploaded file as requested", + extra=dict(archive_url=archive_url, upload=upload.external_id), + ) + archive_service.delete_file(archive_url) + + +RegisteredTestResultsProcessorTask = celery_app.register_task( + TestResultsProcessorTask() +) +test_results_processor_task = celery_app.tasks[RegisteredTestResultsProcessorTask.name] diff --git a/apps/worker/tasks/tests/__init__.py b/apps/worker/tasks/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/tasks/tests/integration/__init__.py b/apps/worker/tasks/tests/integration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/tasks/tests/integration/cassetes/test_ghm_sync_plans/TestGHMarketplaceSyncPlansTask/test_cancelled.yaml b/apps/worker/tasks/tests/integration/cassetes/test_ghm_sync_plans/TestGHMarketplaceSyncPlansTask/test_cancelled.yaml new file mode 100644 index 0000000000..9dbd2f6c5c --- /dev/null +++ b/apps/worker/tasks/tests/integration/cassetes/test_ghm_sync_plans/TestGHMarketplaceSyncPlansTask/test_cancelled.yaml @@ -0,0 +1,74 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/vnd.github.valkyrie-preview+json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - Codecov + method: GET + uri: https://api.github.com/marketplace_listing/stubbed/accounts/3877742 + response: + body: + string: !!binary | + H4sIAAAAAAAAA31STW/cIBD9KxZne413K7XrU6KcckoPOTWKEIaxjYIB8SHVWe1/z2A7m0ZpVuIA + M8N7b+bNiSSvSUvGGF1o65o7tRtUHFO3E3aqrR9Cvb5JSeLsAGsf/MCNeuVRWYNRJUn7oyTaDspg + 9lINE1cIbZLWJUGgyx/WKa2VGdhWQbb3zQcxwk7cv0B0mgtgDozMH8TIzYASVsxPFcljMmDu9A7H + xCx01jtZE0c9I6aBv/HCLnnM2T3d04ruK9o8UtouZ0cp/YPl1rDeA7DoFcdOeq4DlOQjxFBWYDiE + TVAyKjJhk4mXiMssknGMbExN1Ry+MGGbiHK6bsa//WoVIk6kzh9DfUSxXCzMgV019ApG/Y6weXrE + eaWpA0/aA175lKf121tMSwjCK7csQEtuC+dtDyHgk+tq8FxCcXdfBKvTtiObBcx5hXYqwwSgVNI2 + 9Ig0M3Cv56/J5tdPWhK09X82rFCTlZD3t9c8Vj47WpIQV2dd6nBMI0iMLdasPazL0+FaQpbwRB5H + FQo8cYSiVz7EYk0Wtl9i2HOxGFR+Kg0grJHf1T6fz+c3q9/G6F0DAAA= + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - public, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Sat, 25 Jan 2020 18:42:13 GMT + ETag: + - W/"275358a665064f66410393b9de134be6" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=valkyrie-preview; format=json + X-GitHub-Request-Id: + - A594:2048:1713F8F:2E697B7:5E2C8C05 + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4997' + X-RateLimit-Reset: + - '1579978815' + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/apps/worker/tasks/tests/integration/cassetes/test_ghm_sync_plans/TestGHMarketplaceSyncPlansTask/test_purchase_by_existing_owner.yaml b/apps/worker/tasks/tests/integration/cassetes/test_ghm_sync_plans/TestGHMarketplaceSyncPlansTask/test_purchase_by_existing_owner.yaml new file mode 100644 index 0000000000..30d34d160f --- /dev/null +++ b/apps/worker/tasks/tests/integration/cassetes/test_ghm_sync_plans/TestGHMarketplaceSyncPlansTask/test_purchase_by_existing_owner.yaml @@ -0,0 +1,65 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/vnd.github.valkyrie-preview+json + Connection: + - keep-alive + User-Agent: + - Codecov + method: GET + uri: https://api.github.com/marketplace_listing/stubbed/accounts/3877742 + response: + body: + string: '{"url":"https://api.github.com/orgs/github","type":"Organization","id":3877742,"login":"cc-test","email":"cc-test@codecov.io","organization_billing_email":"billing@github.com","marketplace_pending_change":null,"marketplace_purchase":{"billing_cycle":"monthly","next_billing_date":"2020-02-01T00:00:00.000Z","on_free_trial":false,"free_trial_ends_on":null,"unit_count":10,"updated_at":"2020-01-13T00:00:00.000Z","plan":{"url":"https://api.github.com/marketplace_listing/plans/3267","accounts_url":"https://api.github.com/marketplace_listing/plans/3267/accounts","id":3267,"number":3,"name":"Pro","description":"A + professional-grade CI solution","monthly_price_in_cents":1099,"yearly_price_in_cents":11870,"has_free_trial":false,"price_model":"flat-rate","state":"published","unit_name":null,"bullets":["This + is the first bullet of the Pro plan","This is the second bullet of the Pro + plan"]}}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - public, max-age=60, s-maxage=60 + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 24 Jan 2020 00:13:35 GMT + ETag: + - W/"275358a665064f66410393b9de134be6" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=valkyrie-preview; format=json + X-GitHub-Request-Id: + - B4A6:56CB:126D6B:2D5556:5E2A36AF + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4992' + X-RateLimit-Reset: + - '1579825791' + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/apps/worker/tasks/tests/integration/cassetes/test_ghm_sync_plans/TestGHMarketplaceSyncPlansTask/test_purchase_listing_not_found.yaml b/apps/worker/tasks/tests/integration/cassetes/test_ghm_sync_plans/TestGHMarketplaceSyncPlansTask/test_purchase_listing_not_found.yaml new file mode 100644 index 0000000000..a031a9d315 --- /dev/null +++ b/apps/worker/tasks/tests/integration/cassetes/test_ghm_sync_plans/TestGHMarketplaceSyncPlansTask/test_purchase_listing_not_found.yaml @@ -0,0 +1,63 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/vnd.github.valkyrie-preview+json + Connection: + - keep-alive + User-Agent: + - Codecov + method: GET + uri: https://api.github.com/marketplace_listing/stubbed/accounts/123456 + response: + body: + string: '{"message":"Not Found"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - public, max-age=60, s-maxage=60 + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Sat, 25 Jan 2020 18:18:57 GMT + ETag: + - W/"275358a665064f66410393b9de134be6" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept + - Accept-Encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=valkyrie-preview; format=json + X-GitHub-Request-Id: + - EDD2:0DE2:12F2066:2B23392:5E2C8691 + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4998' + X-RateLimit-Reset: + - '1579978815' + X-XSS-Protection: + - 1; mode=block + status: + code: 404 + message: Not Found +version: 1 diff --git a/apps/worker/tasks/tests/integration/cassetes/test_ghm_sync_plans/TestGHMarketplaceSyncPlansTask/test_purchase_new_owner.yaml b/apps/worker/tasks/tests/integration/cassetes/test_ghm_sync_plans/TestGHMarketplaceSyncPlansTask/test_purchase_new_owner.yaml new file mode 100644 index 0000000000..30d34d160f --- /dev/null +++ b/apps/worker/tasks/tests/integration/cassetes/test_ghm_sync_plans/TestGHMarketplaceSyncPlansTask/test_purchase_new_owner.yaml @@ -0,0 +1,65 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/vnd.github.valkyrie-preview+json + Connection: + - keep-alive + User-Agent: + - Codecov + method: GET + uri: https://api.github.com/marketplace_listing/stubbed/accounts/3877742 + response: + body: + string: '{"url":"https://api.github.com/orgs/github","type":"Organization","id":3877742,"login":"cc-test","email":"cc-test@codecov.io","organization_billing_email":"billing@github.com","marketplace_pending_change":null,"marketplace_purchase":{"billing_cycle":"monthly","next_billing_date":"2020-02-01T00:00:00.000Z","on_free_trial":false,"free_trial_ends_on":null,"unit_count":10,"updated_at":"2020-01-13T00:00:00.000Z","plan":{"url":"https://api.github.com/marketplace_listing/plans/3267","accounts_url":"https://api.github.com/marketplace_listing/plans/3267/accounts","id":3267,"number":3,"name":"Pro","description":"A + professional-grade CI solution","monthly_price_in_cents":1099,"yearly_price_in_cents":11870,"has_free_trial":false,"price_model":"flat-rate","state":"published","unit_name":null,"bullets":["This + is the first bullet of the Pro plan","This is the second bullet of the Pro + plan"]}}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - public, max-age=60, s-maxage=60 + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 24 Jan 2020 00:13:35 GMT + ETag: + - W/"275358a665064f66410393b9de134be6" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=valkyrie-preview; format=json + X-GitHub-Request-Id: + - B4A6:56CB:126D6B:2D5556:5E2A36AF + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4992' + X-RateLimit-Reset: + - '1579825791' + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/apps/worker/tasks/tests/integration/cassetes/test_ghm_sync_plans/TestGHMarketplaceSyncPlansTask/test_sync_all_plans.yaml b/apps/worker/tasks/tests/integration/cassetes/test_ghm_sync_plans/TestGHMarketplaceSyncPlansTask/test_sync_all_plans.yaml new file mode 100644 index 0000000000..3a0a58afbf --- /dev/null +++ b/apps/worker/tasks/tests/integration/cassetes/test_ghm_sync_plans/TestGHMarketplaceSyncPlansTask/test_sync_all_plans.yaml @@ -0,0 +1,348 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/vnd.github.valkyrie-preview+json + Connection: + - keep-alive + User-Agent: + - Codecov + method: GET + uri: https://api.github.com/marketplace_listing/stubbed/plans + response: + body: + string: '[{"url":"https://api.github.com/marketplace_listing/plans/3267","accounts_url":"https://api.github.com/marketplace_listing/plans/3267/accounts","id":3267,"number":3,"name":"Pro","description":"A + professional-grade CI solution","monthly_price_in_cents":1099,"yearly_price_in_cents":11870,"has_free_trial":false,"price_model":"flat-rate","state":"published","unit_name":null,"bullets":["This + is the first bullet of the Pro plan","This is the second bullet of the Pro + plan"]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - public, max-age=60, s-maxage=60 + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Sat, 25 Jan 2020 18:52:32 GMT + ETag: + - W/"f730eb63a60ae54aa937da5aa61c0d54" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=valkyrie-preview; format=json + X-GitHub-Request-Id: + - 9E24:7972:48F77:AD2EF:5E2C8E70 + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4996' + X-RateLimit-Reset: + - '1579978815' + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/vnd.github.valkyrie-preview+json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - Codecov + method: GET + uri: https://api.github.com/marketplace_listing/stubbed/plans/3267/accounts?page=1 + response: + body: + string: '[{"url":"https://api.github.com/orgs/github","type":"Organization","id":4,"login":"github","email":null,"organization_billing_email":"billing@github.com","marketplace_pending_change":null,"marketplace_purchase":{"billing_cycle":"monthly","next_billing_date":"2020-02-01T00:00:00.000Z","on_free_trial":false,"free_trial_ends_on":null,"unit_count":12,"updated_at":"2020-01-13T00:00:00.000Z","plan":{"url":"https://api.github.com/marketplace_listing/plans/3267","accounts_url":"https://api.github.com/marketplace_listing/plans/3267/accounts","id":3267,"number":3,"name":"Pro","description":"A + professional-grade CI solution","monthly_price_in_cents":1099,"yearly_price_in_cents":11870,"has_free_trial":false,"price_model":"flat-rate","state":"published","unit_name":null,"bullets":["This + is the first bullet of the Pro plan","This is the second bullet of the Pro + plan"]}}},{"url":"https://api.github.com/users/test","type":"User","id":2,"login":"test","email":"test@example.com","marketplace_pending_change":{"id":12,"unit_count":12,"plan":{"url":"https://api.github.com/marketplace_listing/plans/2147","accounts_url":"https://api.github.com/marketplace_listing/plans/2147/accounts","id":2147,"number":2,"name":"Hobby","description":"A + hobby-grade CI solution","monthly_price_in_cents":1099,"yearly_price_in_cents":11870,"has_free_trial":false,"price_model":"per-unit","state":"published","unit_name":"seat","bullets":["This + is the first bullet of the Hobby plan","This is the second bullet of the Hobby + plan"]},"effective_date":"2020-02-01T00:00:00.000Z"},"marketplace_purchase":{"billing_cycle":"monthly","next_billing_date":"2020-02-01T00:00:00.000Z","on_free_trial":false,"free_trial_ends_on":null,"unit_count":12,"updated_at":"2020-01-13T00:00:00.000Z","plan":{"url":"https://api.github.com/marketplace_listing/plans/2147","accounts_url":"https://api.github.com/marketplace_listing/plans/2147/accounts","id":2147,"number":2,"name":"Hobby","description":"A + hobby-grade CI solution","monthly_price_in_cents":1099,"yearly_price_in_cents":11870,"has_free_trial":false,"price_model":"per-unit","state":"published","unit_name":"seat","bullets":["This + is the first bullet of the Hobby plan","This is the second bullet of the Hobby + plan"]}}}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - public, max-age=60, s-maxage=60 + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Sat, 25 Jan 2020 20:57:30 GMT + ETag: + - W/"6ac56d589bf99d5d30030d9c200d5ea5" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=valkyrie-preview; format=json + X-GitHub-Request-Id: + - 8CEA:380D:A89C66:183FF07:5E2CABB9 + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '3200' + X-RateLimit-Reset: + - '1579987188' + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/vnd.github.valkyrie-preview+json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - Codecov + method: GET + uri: https://api.github.com/marketplace_listing/stubbed/plans/3267/accounts?page=2 + response: + body: + string: '[]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - public, max-age=60, s-maxage=60 + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Sat, 25 Jan 2020 20:57:30 GMT + ETag: + - W/"6ac56d589bf99d5d30030d9c200d5ea5" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=valkyrie-preview; format=json + X-GitHub-Request-Id: + - 8CEC:714A:20FF6A0:3BF8875:5E2CABBA + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '3199' + X-RateLimit-Reset: + - '1579987188' + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/vnd.github.valkyrie-preview+json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - Codecov + method: GET + uri: https://api.github.com/user/4?client_id=testiouu71gdynyqxzk4&client_secret=3b4ab5b18be7155fdbb739e7f1ae277222fb12db + response: + body: + string: !!binary | + H4sIAAAAAAAAA5VUXWvbMBT9K0HPaeQ4H+0EpX0YgzE2KMtG15cgy6qtTZaMJDu4pv99R7YbusAe + /GT56p5zT47vSU+0LZQhjJw6wYMnS6JywrZLYmwuj/FMvn582P98/KbF74db3POWB+6OjdO4K0Oo + PaN0LPpkVahQNlnjpRPWBGnCStiKNnR7195ugS7chB+oUbjgqdVEMeLA4+lZWhkqfTF4nDc0n9ue + rdb2BOSlyP+T0zMGksazMsVsPDA9taGU8AfSX+MPVj7METL09zQ+YH9k8LDbyXyGmAkBKScDFT11 + srYDVZN54VQdlDVzRP2DA491BTfqhc/lAS6uWJQzZ/zQD5xssVBzgCOgp7VTLRddtMBJIVULO2eT + XSDBFbpaYot/4FNHc1WQR55XMU7PXHuJEPEqNvySZZPzxRceXtCHXa256VA/KJ3L5eKzESvUM0Rx + ihQS1Q2YP4DE5ca1tggovhtavnOz+OS4EcoLG32puIppHBNwX8TXCVUqJ3mmocI0WmOIsm/Husm0 + EsfRXJamuyWZSsMOEna9W7+FAWEi7MN2f/0uHcPfhAB9gJs8YH6aJDdXyfpqnR6SHdvcsM3mCfKa + On/fkyaxJ00OacqSPUvXT+T1L90oNXiIBAAA + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - public, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Sat, 25 Jan 2020 20:57:29 GMT + ETag: + - W/"0fe5c11b8816ba2e6280232d653389b9" + Last-Modified: + - Mon, 20 Jan 2020 22:06:21 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=valkyrie-preview; format=json + X-GitHub-Request-Id: + - 8CD0:174F:137AB68:2BEAA82:5E2CABB8 + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4975' + X-RateLimit-Reset: + - '1579988452' + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/vnd.github.valkyrie-preview+json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - Codecov + method: GET + uri: https://api.github.com/user/2?client_id=testiouu71gdynyqxzk4&client_secret=3b4ab5b18be7155fdbb739e7f1ae277222fb12db + response: + body: + string: !!binary | + H4sIAAAAAAAAA5WTzW6cMBSFX6XyeiY2NJlJkKIu2k0XrVQp/VE2I2M84NbYyD+MpiiP0F33fcU+ + Qo+Byc8sKrECzP3OPVzuGYi2tTKkIJXcR/MjkBVRFSnyFTG2krt0Tz68+7T58u2jFt/f3+I973ng + bhedxrsmhM4XlE6Hnl3UKjSxjF46YU2QJlwI29JI8zf97SXo2s38KI2DM51OzRITBx1Pn7w1odVn + naeGY/VT3d5qbQ9gz23+R54+QnA13StTLxcANFAbGokZwf5D+mjlwyIrIzDQdME/SBIeM3eyWmJn + RmDmYOBjoE52dtSKpRdOdUFZs8jWCxBC1tXcqJ98sRBADz4ZWmRgBADKHpu1iJyIgXZO9Vwc0xic + FFL1mOlytTMUYuHYSSz0Z/zxNGEV5I5XbYrWnmsvkSfepoK3jVP+1VdufHA8NKjF5nbcHElhotYr + UiKRc7CQK5HKD6fqccuBaCvGmZ8Y2XKFNE4CjXKSlxrNZkFloff3z6/fILtYaiV20xyLjG0fj8aV + Q/K3r0/bj/jgmV1fb57lAScZg2n0CBgdD9DOGduuM7bO2R27KvLLIru5R6/YVc9rspt1lq1Zdpdn + xdWmYOyePPwDTU2zRYAEAAA= + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - public, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Sat, 25 Jan 2020 20:57:29 GMT + ETag: + - W/"56aa96e0a95e5e099af95553b894cf5e" + Last-Modified: + - Fri, 01 Nov 2019 21:56:00 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=valkyrie-preview; format=json + X-GitHub-Request-Id: + - 8CD2:5922:13298D6:2B0DDA4:5E2CABB9 + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4974' + X-RateLimit-Reset: + - '1579988452' + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/apps/worker/tasks/tests/integration/cassetes/test_http_request_task/TestHTTPRequestTask/test_http_request_run_async_200.yaml b/apps/worker/tasks/tests/integration/cassetes/test_http_request_task/TestHTTPRequestTask/test_http_request_run_async_200.yaml new file mode 100644 index 0000000000..dcb07af4da --- /dev/null +++ b/apps/worker/tasks/tests/integration/cassetes/test_http_request_task/TestHTTPRequestTask/test_http_request_run_async_200.yaml @@ -0,0 +1,52 @@ +interactions: +- request: + body: '{"testing": 123}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '16' + content-type: + - application/json + host: + - mockbin.org + user-agent: + - Codecov + method: POST + uri: http://mockbin.org/bin/a1316495-ee65-4eab-b8e3-d5cb7cfc7519?foo=bar&foo=baz + response: + content: ok + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 7a54589b3c2422c9-ORD + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - text/html; charset=utf-8 + Date: + - Thu, 09 Mar 2023 15:28:09 GMT + NEL: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=0KCDrzTnQ3oxmF6BTY6m1zb1wpY%2FXqSwIhaF4qNExnFLq1iY8Y5QtCO9sDC3%2FBEKJ8S9rWX3z%2FFl1tLJJmmm9pSpJvfOPHekn52h8%2FOXhBG%2BlSyD3AQTR82QwEVFMg%3D%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Transfer-Encoding: + - chunked + Vary: + - X-HTTP-Method-Override, Accept-Encoding + Via: + - 1.1 vegur + alt-svc: + - h3=":443"; ma=86400, h3-29=":443"; ma=86400 + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/apps/worker/tasks/tests/integration/cassetes/test_http_request_task/TestHTTPRequestTask/test_http_request_run_async_400.yaml b/apps/worker/tasks/tests/integration/cassetes/test_http_request_task/TestHTTPRequestTask/test_http_request_run_async_400.yaml new file mode 100644 index 0000000000..b0a47058e4 --- /dev/null +++ b/apps/worker/tasks/tests/integration/cassetes/test_http_request_task/TestHTTPRequestTask/test_http_request_run_async_400.yaml @@ -0,0 +1,50 @@ +interactions: +- request: + body: '{"testing": 123}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '16' + content-type: + - application/json + host: + - mockbin.org + user-agent: + - Codecov + method: POST + uri: http://mockbin.org/bin/e4e9db83-b7b9-4a50-b929-d672bcc8d075?foo=bar&foo=baz + response: + content: bad request + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 7a545aec4f3efdcd-ORD + Connection: + - keep-alive + Content-Type: + - text/html; charset=utf-8 + Date: + - Thu, 09 Mar 2023 15:29:44 GMT + NEL: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=RHosuVenJ2SPTXip%2BnAlbNsFhjndBvxIWI06Q9oZk6XEApDzyTCzpyNpiD%2BjAPj8PSef8AOywK%2FsMiwKaD%2FgjvA3nngx1ZAAg%2FptdnspIztaFMtShvRlcWlGSi2ZSg%3D%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Transfer-Encoding: + - chunked + Vary: + - X-HTTP-Method-Override, Accept-Encoding + Via: + - 1.1 vegur + alt-svc: + - h3=":443"; ma=86400, h3-29=":443"; ma=86400 + http_version: HTTP/1.1 + status_code: 400 +version: 1 diff --git a/apps/worker/tasks/tests/integration/cassetes/test_http_request_task/TestHTTPRequestTask/test_http_request_run_async_500.yaml b/apps/worker/tasks/tests/integration/cassetes/test_http_request_task/TestHTTPRequestTask/test_http_request_run_async_500.yaml new file mode 100644 index 0000000000..7087f26da7 --- /dev/null +++ b/apps/worker/tasks/tests/integration/cassetes/test_http_request_task/TestHTTPRequestTask/test_http_request_run_async_500.yaml @@ -0,0 +1,50 @@ +interactions: +- request: + body: '{"testing": 123}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '16' + content-type: + - application/json + host: + - mockbin.org + user-agent: + - Codecov + method: POST + uri: http://mockbin.org/bin/c0052243-3391-4a30-bed7-066e4cd04074?foo=bar&foo=baz + response: + content: server error + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 7a545c78ec752a78-ORD + Connection: + - keep-alive + Content-Type: + - text/html; charset=utf-8 + Date: + - Thu, 09 Mar 2023 15:30:47 GMT + NEL: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=B7w04pQyMSLh7hb7HL5vdqe2ksEZLYq6k6yAbNYCM4JzBi2m%2F7PFsJg7jAZbxRFMiwM38zlkcc3qlyMiwvDkG1lVxIWR0U5mHqtZxeQjZ%2BV%2Baq4kqtbpa1dd9A8f%2BA%3D%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Transfer-Encoding: + - chunked + Vary: + - X-HTTP-Method-Override, Accept-Encoding + Via: + - 1.1 vegur + alt-svc: + - h3=":443"; ma=86400, h3-29=":443"; ma=86400 + http_version: HTTP/1.1 + status_code: 500 +version: 1 diff --git a/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_notifier_call_no_head_commit_report.yaml b/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_notifier_call_no_head_commit_report.yaml new file mode 100644 index 0000000000..999ebcc1e1 --- /dev/null +++ b/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_notifier_call_no_head_commit_report.yaml @@ -0,0 +1,222 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a/status?page=1&per_page=100 + response: + content: '{"state":"success","statuses":[{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/649eaaf2924e92dc7fd8d370ddb857033231e67a","avatar_url":"https://avatars1.githubusercontent.com/oa/166618?v=4","id":8231102724,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ4MjMxMTAyNzI0","state":"success","description":"Coverage + not affected when comparing 17a71a9...649eaaf","target_url":"http://localhost/gh/ThiagoCodecov/example-python/compare/17a71a9a2f5335ed4d00496c7bbc6405f547a527...649eaaf2924e92dc7fd8d370ddb857033231e67a","context":"codecov/patch","created_at":"2019-11-27T00:01:14Z","updated_at":"2019-11-27T00:01:14Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/649eaaf2924e92dc7fd8d370ddb857033231e67a","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":8459091358,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ4NDU5MDkxMzU4","state":"success","description":"No + unexpected coverage changes found","target_url":"https://codecov.io/gh/ThiagoCodecov/example-python/commit/649eaaf2924e92dc7fd8d370ddb857033231e67a","context":"codecov/changes","created_at":"2019-12-30T11:15:30Z","updated_at":"2019-12-30T11:15:30Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/649eaaf2924e92dc7fd8d370ddb857033231e67a","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9333173070,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzMzMTczMDcw","state":"success","description":"85.00% + (+0.00%) compared to 17a71a9","target_url":"https://codecov.io/gh/ThiagoCodecov/example-python/compare/17a71a9a2f5335ed4d00496c7bbc6405f547a527...649eaaf2924e92dc7fd8d370ddb857033231e67a","context":"codecov/project","created_at":"2020-04-09T16:02:50Z","updated_at":"2020-04-09T16:02:50Z"}],"sha":"649eaaf2924e92dc7fd8d370ddb857033231e67a","total_count":3,"repository":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments"},"commit_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 30 Apr 2020 17:13:30 GMT + Etag: + - W/"acd2c609a1b52d50bf51328ff3634f60" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - repo, repo:status + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - CA9F:16E0:6F216:9760E:5EAB073A + X-Http-Reason: + - OK + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4999' + X-Ratelimit-Reset: + - '1588270410' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a/status?page=1&per_page=100 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/search/issues?q=649eaaf2924e92dc7fd8d370ddb857033231e67a+repo%3AThiagoCodecov%2Fexample-python+type%3Apr+state%3Aopen + response: + content: '{"total_count":0,"incomplete_results":false,"items":[]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - no-cache + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 30 Apr 2020 17:13:30 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - CA9F:16E0:6F21C:97618:5EAB073A + X-Http-Reason: + - OK + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '30' + X-Ratelimit-Remaining: + - '29' + X-Ratelimit-Reset: + - '1588266870' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/search/issues?q=649eaaf2924e92dc7fd8d370ddb857033231e67a+repo%3AThiagoCodecov%2Fexample-python+type%3Apr+state%3Aopen +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a/pulls + response: + content: '[{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11","id":331918181,"node_id":"MDExOlB1bGxSZXF1ZXN0MzMxOTE4MTgx","html_url":"https://github.com/ThiagoCodecov/example-python/pull/11","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/11.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/11.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11","number":11,"state":"closed","locked":false,"title":"Adding + requirements","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"","created_at":"2019-10-24T08:23:32Z","updated_at":"2019-12-09T12:13:28Z","closed_at":"2019-12-06T15:46:04Z","merged_at":"2019-12-06T15:46:04Z","merge_commit_sha":"5936002ddf11f01a9fc1641a5bbb620691443239","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/10063a8d1d721d8d6c1b731f5273754a3675962d","head":{"label":"ThiagoCodecov:test-branch-1","ref":"test-branch-1","sha":"10063a8d1d721d8d6c1b731f5273754a3675962d","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-12-04T04:21:29Z","pushed_at":"2022-08-17T20:09:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":2,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":2,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"ThiagoCodecov:master","ref":"master","sha":"17a71a9a2f5335ed4d00496c7bbc6405f547a527","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-12-04T04:21:29Z","pushed_at":"2022-08-17T20:09:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":2,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":2,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/11"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/10063a8d1d721d8d6c1b731f5273754a3675962d"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 17 Aug 2022 20:11:17 GMT + ETag: + - W/"b4d81d734af35a265542d18515a95a2a9501e5f1c8e2834da0c8c6c3f84d915b" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D73C:1104:EF131:FA8D5:62FD4B65 + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4998' + X-RateLimit-Reset: + - '1660770602' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '2' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-08-24 01:18:21 UTC + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_simple_call_no_notifiers.yaml b/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_simple_call_no_notifiers.yaml new file mode 100644 index 0000000000..eae1e80184 --- /dev/null +++ b/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_simple_call_no_notifiers.yaml @@ -0,0 +1,457 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a/statuses?page=1&per_page=100 + response: + content: '[{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/649eaaf2924e92dc7fd8d370ddb857033231e67a","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":8296741626,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ4Mjk2NzQxNjI2","state":"success","description":"85.00000% + (+0.00%) compared to 17a71a9","target_url":"https://codecov.io/gh/ThiagoCodecov/example-python/compare/17a71a9a2f5335ed4d00496c7bbc6405f547a527...649eaaf2924e92dc7fd8d370ddb857033231e67a","context":"codecov/project","created_at":"2019-12-05T13:44:00Z","updated_at":"2019-12-05T13:44:00Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/649eaaf2924e92dc7fd8d370ddb857033231e67a","avatar_url":"https://avatars1.githubusercontent.com/oa/166618?v=4","id":8231102724,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ4MjMxMTAyNzI0","state":"success","description":"Coverage + not affected when comparing 17a71a9...649eaaf","target_url":"http://localhost/gh/ThiagoCodecov/example-python/compare/17a71a9a2f5335ed4d00496c7bbc6405f547a527...649eaaf2924e92dc7fd8d370ddb857033231e67a","context":"codecov/patch","created_at":"2019-11-27T00:01:14Z","updated_at":"2019-11-27T00:01:14Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/649eaaf2924e92dc7fd8d370ddb857033231e67a","avatar_url":"https://avatars1.githubusercontent.com/oa/166618?v=4","id":8231102539,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ4MjMxMTAyNTM5","state":"success","description":"85.29% + remains the same compared to 17a71a9","target_url":"http://localhost/gh/ThiagoCodecov/example-python/compare/17a71a9a2f5335ed4d00496c7bbc6405f547a527...649eaaf2924e92dc7fd8d370ddb857033231e67a","context":"codecov/project","created_at":"2019-11-27T00:01:13Z","updated_at":"2019-11-27T00:01:13Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 05 Dec 2019 21:18:31 GMT + Etag: + - W/"9455b4560a3c620d81b2c308f88894f5" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - repo, repo:status + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 3830:1515:E1CBD4:241D791:5DE97427 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4999' + X-Ratelimit-Reset: + - '1575584311' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a/statuses?page=1&per_page=100 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/search/issues?q=649eaaf2924e92dc7fd8d370ddb857033231e67a+repo%3AThiagoCodecov%2Fexample-python+type%3Apr+state%3Aopen + response: + content: '{"total_count":1,"incomplete_results":false,"items":[{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11","repository_url":"https://api.github.com/repos/ThiagoCodecov/example-python","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11/labels{/name}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11/comments","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11/events","html_url":"https://github.com/ThiagoCodecov/example-python/pull/11","id":511786496,"node_id":"MDExOlB1bGxSZXF1ZXN0MzMxOTE4MTgx","number":11,"title":"Adding + requirements","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":1,"created_at":"2019-10-24T08:23:32Z","updated_at":"2019-12-03T21:15:27Z","closed_at":null,"author_association":"OWNER","pull_request":{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11","html_url":"https://github.com/ThiagoCodecov/example-python/pull/11","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/11.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/11.patch"},"body":"","score":100.0}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - no-cache + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 05 Dec 2019 21:18:32 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 3831:049F:144F0AE:2FEFE28:5DE97428 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '30' + X-Ratelimit-Remaining: + - '29' + X-Ratelimit-Reset: + - '1575580772' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/search/issues?q=649eaaf2924e92dc7fd8d370ddb857033231e67a+repo%3AThiagoCodecov%2Fexample-python+type%3Apr+state%3Aopen +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11","id":331918181,"node_id":"MDExOlB1bGxSZXF1ZXN0MzMxOTE4MTgx","html_url":"https://github.com/ThiagoCodecov/example-python/pull/11","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/11.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/11.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11","number":11,"state":"open","locked":false,"title":"Adding + requirements","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"","created_at":"2019-10-24T08:23:32Z","updated_at":"2019-12-03T21:15:27Z","closed_at":null,"merged_at":null,"merge_commit_sha":"ebebc454bc399c79fa5efbd7e2f1662a43bb4196","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/10063a8d1d721d8d6c1b731f5273754a3675962d","head":{"label":"ThiagoCodecov:test-branch-1","ref":"test-branch-1","sha":"10063a8d1d721d8d6c1b731f5273754a3675962d","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2019-10-05T18:21:23Z","pushed_at":"2019-12-03T21:15:06Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":119,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":7,"license":null,"forks":0,"open_issues":7,"watchers":0,"default_branch":"master"}},"base":{"label":"ThiagoCodecov:master","ref":"master","sha":"17a71a9a2f5335ed4d00496c7bbc6405f547a527","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2019-10-05T18:21:23Z","pushed_at":"2019-12-03T21:15:06Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":119,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":7,"license":null,"forks":0,"open_issues":7,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/11"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/10063a8d1d721d8d6c1b731f5273754a3675962d"}},"author_association":"OWNER","merged":false,"mergeable":true,"rebaseable":true,"mergeable_state":"clean","merged_by":null,"comments":1,"review_comments":0,"maintainer_can_modify":false,"commits":6,"additions":31,"deletions":6,"changed_files":3}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 05 Dec 2019 21:18:33 GMT + Etag: + - W/"df979824ea38e80a32607ac5f2bb3a69" + Last-Modified: + - Tue, 03 Dec 2019 21:15:27 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 3832:1F57:8FDE45:17368D2:5DE97428 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4998' + X-Ratelimit-Reset: + - '1575584311' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/commits + response: + content: '[{"sha":"ab89bd097cd5e1af391dc52daccaa4e6c6579039","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmFiODliZDA5N2NkNWUxYWYzOTFkYzUyZGFjY2FhNGU2YzY1NzkwMzk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-10-24T08:21:45Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-10-24T08:21:45Z"},"message":"Adding + requirements","tree":{"sha":"c8091d748268a03a5886b5959cb0ae5e25d0c9d8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c8091d748268a03a5886b5959cb0ae5e25d0c9d8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ab89bd097cd5e1af391dc52daccaa4e6c6579039","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ab89bd097cd5e1af391dc52daccaa4e6c6579039","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ab89bd097cd5e1af391dc52daccaa4e6c6579039","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ab89bd097cd5e1af391dc52daccaa4e6c6579039/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"17a71a9a2f5335ed4d00496c7bbc6405f547a527","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/17a71a9a2f5335ed4d00496c7bbc6405f547a527","html_url":"https://github.com/ThiagoCodecov/example-python/commit/17a71a9a2f5335ed4d00496c7bbc6405f547a527"}]},{"sha":"649eaaf2924e92dc7fd8d370ddb857033231e67a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjY0OWVhYWYyOTI0ZTkyZGM3ZmQ4ZDM3MGRkYjg1NzAzMzIzMWU2N2E=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-11-26T23:54:15Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-11-26T23:54:15Z"},"message":"Trying + stuff","tree":{"sha":"2e9aa67eafb7410ed9ecb73e5a60462452bfdd17","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2e9aa67eafb7410ed9ecb73e5a60462452bfdd17"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/649eaaf2924e92dc7fd8d370ddb857033231e67a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ab89bd097cd5e1af391dc52daccaa4e6c6579039","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ab89bd097cd5e1af391dc52daccaa4e6c6579039","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ab89bd097cd5e1af391dc52daccaa4e6c6579039"}]},{"sha":"8b190323a5dc374b02892c07136c3bd8893b2a04","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjhiMTkwMzIzYTVkYzM3NGIwMjg5MmMwNzEzNmMzYmQ4ODkzYjJhMDQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-03T21:13:27Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-03T21:13:27Z"},"message":"Doing + some tests","tree":{"sha":"78cc08e9be6359826ccf03fb72a4ce3fcb25d7a2","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/78cc08e9be6359826ccf03fb72a4ce3fcb25d7a2"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8b190323a5dc374b02892c07136c3bd8893b2a04","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8b190323a5dc374b02892c07136c3bd8893b2a04","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8b190323a5dc374b02892c07136c3bd8893b2a04","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8b190323a5dc374b02892c07136c3bd8893b2a04/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"649eaaf2924e92dc7fd8d370ddb857033231e67a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/649eaaf2924e92dc7fd8d370ddb857033231e67a"}]},{"sha":"fa3df832e28febb2611ab8e4aa550a178dc5d733","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmZhM2RmODMyZTI4ZmViYjI2MTFhYjhlNGFhNTUwYTE3OGRjNWQ3MzM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-03T21:13:51Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-03T21:13:51Z"},"message":"Doing + some tests 2","tree":{"sha":"c1f9c27a1e3c1c70ae53e7d68a0f8d9a0535eee3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c1f9c27a1e3c1c70ae53e7d68a0f8d9a0535eee3"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/fa3df832e28febb2611ab8e4aa550a178dc5d733","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fa3df832e28febb2611ab8e4aa550a178dc5d733","html_url":"https://github.com/ThiagoCodecov/example-python/commit/fa3df832e28febb2611ab8e4aa550a178dc5d733","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fa3df832e28febb2611ab8e4aa550a178dc5d733/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"8b190323a5dc374b02892c07136c3bd8893b2a04","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8b190323a5dc374b02892c07136c3bd8893b2a04","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8b190323a5dc374b02892c07136c3bd8893b2a04"}]},{"sha":"844f31d1edcb864aaaa1447e185dec294134241e","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg0NGYzMWQxZWRjYjg2NGFhYWExNDQ3ZTE4NWRlYzI5NDEzNDI0MWU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-03T21:14:40Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-03T21:14:40Z"},"message":"Doing + some tests 2","tree":{"sha":"c88eb829331ec8401da569fe2fc44c2cc32219f7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c88eb829331ec8401da569fe2fc44c2cc32219f7"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/844f31d1edcb864aaaa1447e185dec294134241e","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/844f31d1edcb864aaaa1447e185dec294134241e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/844f31d1edcb864aaaa1447e185dec294134241e","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/844f31d1edcb864aaaa1447e185dec294134241e/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"fa3df832e28febb2611ab8e4aa550a178dc5d733","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fa3df832e28febb2611ab8e4aa550a178dc5d733","html_url":"https://github.com/ThiagoCodecov/example-python/commit/fa3df832e28febb2611ab8e4aa550a178dc5d733"}]},{"sha":"10063a8d1d721d8d6c1b731f5273754a3675962d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjEwMDYzYThkMWQ3MjFkOGQ2YzFiNzMxZjUyNzM3NTRhMzY3NTk2MmQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-03T21:14:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-03T21:14:57Z"},"message":"Doing + some tests 3","tree":{"sha":"9ac5df663e620e8f966964865cca4507f62619a6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9ac5df663e620e8f966964865cca4507f62619a6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/10063a8d1d721d8d6c1b731f5273754a3675962d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/10063a8d1d721d8d6c1b731f5273754a3675962d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/10063a8d1d721d8d6c1b731f5273754a3675962d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/10063a8d1d721d8d6c1b731f5273754a3675962d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"844f31d1edcb864aaaa1447e185dec294134241e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/844f31d1edcb864aaaa1447e185dec294134241e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/844f31d1edcb864aaaa1447e185dec294134241e"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 10 Jan 2020 03:14:48 GMT + Etag: + - W/"05b6236c2d4f303962bb91ebdebff6e3" + Last-Modified: + - Thu, 09 Jan 2020 19:23:25 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 13EB:208D:2AE416:3CF25D:5E17EC28 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4999' + X-Ratelimit-Reset: + - '1578629688' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/commits +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a/status?page=1&per_page=100 + response: + content: '{"state":"success","statuses":[{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/649eaaf2924e92dc7fd8d370ddb857033231e67a","avatar_url":"https://avatars1.githubusercontent.com/oa/166618?v=4","id":8231102724,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ4MjMxMTAyNzI0","state":"success","description":"Coverage + not affected when comparing 17a71a9...649eaaf","target_url":"http://localhost/gh/ThiagoCodecov/example-python/compare/17a71a9a2f5335ed4d00496c7bbc6405f547a527...649eaaf2924e92dc7fd8d370ddb857033231e67a","context":"codecov/patch","created_at":"2019-11-27T00:01:14Z","updated_at":"2019-11-27T00:01:14Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/649eaaf2924e92dc7fd8d370ddb857033231e67a","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":8296741626,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ4Mjk2NzQxNjI2","state":"success","description":"85.00000% + (+0.00%) compared to 17a71a9","target_url":"https://codecov.io/gh/ThiagoCodecov/example-python/compare/17a71a9a2f5335ed4d00496c7bbc6405f547a527...649eaaf2924e92dc7fd8d370ddb857033231e67a","context":"codecov/project","created_at":"2019-12-05T13:44:00Z","updated_at":"2019-12-05T13:44:00Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/649eaaf2924e92dc7fd8d370ddb857033231e67a","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":8459091358,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ4NDU5MDkxMzU4","state":"success","description":"No + unexpected coverage changes found","target_url":"https://codecov.io/gh/ThiagoCodecov/example-python/commit/649eaaf2924e92dc7fd8d370ddb857033231e67a","context":"codecov/changes","created_at":"2019-12-30T11:15:30Z","updated_at":"2019-12-30T11:15:30Z"}],"sha":"649eaaf2924e92dc7fd8d370ddb857033231e67a","total_count":3,"repository":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments"},"commit_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:02:49 GMT + Etag: + - W/"bebc0296cfb273d3427ec5d041a76079" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - repo, repo:status + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EB78:4EFD:254A59:3285AE:5E8F4729 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4999' + X-Ratelimit-Reset: + - '1586451769' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a/status?page=1&per_page=100 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a/pulls + response: + content: '[{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11","id":331918181,"node_id":"MDExOlB1bGxSZXF1ZXN0MzMxOTE4MTgx","html_url":"https://github.com/ThiagoCodecov/example-python/pull/11","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/11.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/11.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11","number":11,"state":"closed","locked":false,"title":"Adding + requirements","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"","created_at":"2019-10-24T08:23:32Z","updated_at":"2019-12-09T12:13:28Z","closed_at":"2019-12-06T15:46:04Z","merged_at":"2019-12-06T15:46:04Z","merge_commit_sha":"5936002ddf11f01a9fc1641a5bbb620691443239","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/10063a8d1d721d8d6c1b731f5273754a3675962d","head":{"label":"ThiagoCodecov:test-branch-1","ref":"test-branch-1","sha":"10063a8d1d721d8d6c1b731f5273754a3675962d","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-12-04T04:21:29Z","pushed_at":"2020-12-04T04:21:26Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":2,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":2,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"ThiagoCodecov:master","ref":"master","sha":"17a71a9a2f5335ed4d00496c7bbc6405f547a527","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-12-04T04:21:29Z","pushed_at":"2020-12-04T04:21:26Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":2,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":2,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/11"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/10063a8d1d721d8d6c1b731f5273754a3675962d"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 17 Aug 2022 01:19:10 GMT + ETag: + - W/"876ae794efdbbd6cd22b6d72b188cbf4fb0026cb0f076bcfb912360db3c0f21f" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C607:3B85:7D8367:85AEFD:62FC420E + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4987' + X-RateLimit-Reset: + - '1660701033' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '13' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-08-24 01:18:21 UTC + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_simple_call_only_status_notifiers.yaml b/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_simple_call_only_status_notifiers.yaml new file mode 100644 index 0000000000..6efa59d457 --- /dev/null +++ b/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_simple_call_only_status_notifiers.yaml @@ -0,0 +1,317 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a/status?page=1&per_page=100 + response: + content: '{"state":"pending","statuses":[],"sha":"649eaaf2924e92dc7fd8d370ddb857033231e67a","total_count":0,"repository":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments"},"commit_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Length: + - '95' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 03 Sep 2024 13:17:10 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - github.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E8CF:37895C:261D361:4949EA9:66D70C56 + X-RateLimit-Limit: + - '60' + X-RateLimit-Remaining: + - '50' + X-RateLimit-Reset: + - '1725372143' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '10' + X-XSS-Protection: + - '0' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a/pulls + response: + content: '[{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11","id":331918181,"node_id":"MDExOlB1bGxSZXF1ZXN0MzMxOTE4MTgx","html_url":"https://github.com/ThiagoCodecov/example-python/pull/11","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/11.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/11.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11","number":11,"state":"closed","locked":true,"title":"Adding requirements","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"","created_at":"2019-10-24T08:23:32Z","updated_at":"2019-12-09T12:13:28Z","closed_at":"2019-12-06T15:46:04Z","merged_at":"2019-12-06T15:46:04Z","merge_commit_sha":"5936002ddf11f01a9fc1641a5bbb620691443239","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/10063a8d1d721d8d6c1b731f5273754a3675962d","head":{"label":"ThiagoCodecov:test-branch-1","ref":"test-branch-1","sha":"10063a8d1d721d8d6c1b731f5273754a3675962d","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2023-07-04T20:51:23Z","pushed_at":"2022-08-17T20:09:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":3,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"ThiagoCodecov:master","ref":"master","sha":"17a71a9a2f5335ed4d00496c7bbc6405f547a527","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2023-07-04T20:51:23Z","pushed_at":"2022-08-17T20:09:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":3,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/11"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/11/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/11/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/10063a8d1d721d8d6c1b731f5273754a3675962d"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Length: + - '95' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 03 Sep 2024 13:17:10 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - github.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E8CF:37895C:261D361:4949EA9:66D70C56 + X-RateLimit-Limit: + - '60' + X-RateLimit-Remaining: + - '50' + X-RateLimit-Reset: + - '1725372143' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '10' + X-XSS-Protection: + - '0' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/compare/17a71a9a2f5335ed4d00496c7bbc6405f547a527...649eaaf2924e92dc7fd8d370ddb857033231e67a + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/17a71a9a2f5335ed4d00496c7bbc6405f547a527...649eaaf2924e92dc7fd8d370ddb857033231e67a","html_url":"https://github.com/ThiagoCodecov/example-python/compare/17a71a9a2f5335ed4d00496c7bbc6405f547a527...649eaaf2924e92dc7fd8d370ddb857033231e67a","permalink_url":"https://github.com/ThiagoCodecov/example-python/compare/ThiagoCodecov:17a71a9...ThiagoCodecov:649eaaf","diff_url":"https://github.com/ThiagoCodecov/example-python/compare/17a71a9a2f5335ed4d00496c7bbc6405f547a527...649eaaf2924e92dc7fd8d370ddb857033231e67a.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/compare/17a71a9a2f5335ed4d00496c7bbc6405f547a527...649eaaf2924e92dc7fd8d370ddb857033231e67a.patch","base_commit":{"sha":"17a71a9a2f5335ed4d00496c7bbc6405f547a527","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjE3YTcxYTlhMmY1MzM1ZWQ0ZDAwNDk2YzdiYmM2NDA1ZjU0N2E1Mjc=","commit":{"author":{"name":"Thiago Ramos","email":"thiago@codecov.io","date":"2019-10-05T18:21:13Z"},"committer":{"name":"Thiago Ramos","email":"thiago@codecov.io","date":"2019-10-05T18:21:13Z"},"message":"Adding file 20191005-4f786e3e-a901-4816-890a-295fa4aca6f7","tree":{"sha":"c78a6415857cd33367f26e78c4be1fcf3ca4d05f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c78a6415857cd33367f26e78c4be1fcf3ca4d05f"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/17a71a9a2f5335ed4d00496c7bbc6405f547a527","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/17a71a9a2f5335ed4d00496c7bbc6405f547a527","html_url":"https://github.com/ThiagoCodecov/example-python/commit/17a71a9a2f5335ed4d00496c7bbc6405f547a527","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/17a71a9a2f5335ed4d00496c7bbc6405f547a527/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ffcb4efe52c91dc1f29030d1fb1a07ff85b38dd9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ffcb4efe52c91dc1f29030d1fb1a07ff85b38dd9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ffcb4efe52c91dc1f29030d1fb1a07ff85b38dd9"}]},"merge_base_commit":{"sha":"17a71a9a2f5335ed4d00496c7bbc6405f547a527","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjE3YTcxYTlhMmY1MzM1ZWQ0ZDAwNDk2YzdiYmM2NDA1ZjU0N2E1Mjc=","commit":{"author":{"name":"Thiago Ramos","email":"thiago@codecov.io","date":"2019-10-05T18:21:13Z"},"committer":{"name":"Thiago Ramos","email":"thiago@codecov.io","date":"2019-10-05T18:21:13Z"},"message":"Adding file 20191005-4f786e3e-a901-4816-890a-295fa4aca6f7","tree":{"sha":"c78a6415857cd33367f26e78c4be1fcf3ca4d05f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c78a6415857cd33367f26e78c4be1fcf3ca4d05f"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/17a71a9a2f5335ed4d00496c7bbc6405f547a527","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/17a71a9a2f5335ed4d00496c7bbc6405f547a527","html_url":"https://github.com/ThiagoCodecov/example-python/commit/17a71a9a2f5335ed4d00496c7bbc6405f547a527","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/17a71a9a2f5335ed4d00496c7bbc6405f547a527/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ffcb4efe52c91dc1f29030d1fb1a07ff85b38dd9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ffcb4efe52c91dc1f29030d1fb1a07ff85b38dd9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ffcb4efe52c91dc1f29030d1fb1a07ff85b38dd9"}]},"status":"ahead","ahead_by":2,"behind_by":0,"total_commits":2,"commits":[{"sha":"ab89bd097cd5e1af391dc52daccaa4e6c6579039","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmFiODliZDA5N2NkNWUxYWYzOTFkYzUyZGFjY2FhNGU2YzY1NzkwMzk=","commit":{"author":{"name":"Thiago Ramos","email":"thiago@codecov.io","date":"2019-10-24T08:21:45Z"},"committer":{"name":"Thiago Ramos","email":"thiago@codecov.io","date":"2019-10-24T08:21:45Z"},"message":"Adding requirements","tree":{"sha":"c8091d748268a03a5886b5959cb0ae5e25d0c9d8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c8091d748268a03a5886b5959cb0ae5e25d0c9d8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ab89bd097cd5e1af391dc52daccaa4e6c6579039","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ab89bd097cd5e1af391dc52daccaa4e6c6579039","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ab89bd097cd5e1af391dc52daccaa4e6c6579039","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ab89bd097cd5e1af391dc52daccaa4e6c6579039/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"17a71a9a2f5335ed4d00496c7bbc6405f547a527","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/17a71a9a2f5335ed4d00496c7bbc6405f547a527","html_url":"https://github.com/ThiagoCodecov/example-python/commit/17a71a9a2f5335ed4d00496c7bbc6405f547a527"}]},{"sha":"649eaaf2924e92dc7fd8d370ddb857033231e67a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjY0OWVhYWYyOTI0ZTkyZGM3ZmQ4ZDM3MGRkYjg1NzAzMzIzMWU2N2E=","commit":{"author":{"name":"Thiago Ramos","email":"thiago@codecov.io","date":"2019-11-26T23:54:15Z"},"committer":{"name":"Thiago Ramos","email":"thiago@codecov.io","date":"2019-11-26T23:54:15Z"},"message":"Trying stuff","tree":{"sha":"2e9aa67eafb7410ed9ecb73e5a60462452bfdd17","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2e9aa67eafb7410ed9ecb73e5a60462452bfdd17"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/649eaaf2924e92dc7fd8d370ddb857033231e67a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ab89bd097cd5e1af391dc52daccaa4e6c6579039","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ab89bd097cd5e1af391dc52daccaa4e6c6579039","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ab89bd097cd5e1af391dc52daccaa4e6c6579039"}]}],"files":[{"sha":"ab0a8735f8dfaef9044d2092e6de622052069b1d","filename":"requirements.txt","status":"added","additions":20,"deletions":0,"changes":20,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/649eaaf2924e92dc7fd8d370ddb857033231e67a/requirements.txt","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/649eaaf2924e92dc7fd8d370ddb857033231e67a/requirements.txt","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/requirements.txt?ref=649eaaf2924e92dc7fd8d370ddb857033231e67a","patch":"@@ -0,0 +1,20 @@\n+atomicwrites==1.3.0\n+attrs==19.3.0\n+certifi==2019.9.11\n+chardet==3.0.4\n+codecov==2.0.15\n+coverage==4.5.4\n+idna==2.8\n+importlib-metadata==0.23\n+more-itertools==7.2.0\n+packaging==19.2\n+pluggy==0.13.0\n+py==1.8.0\n+pyparsing==2.4.2\n+pytest==5.2.1\n+pytest-cov==2.8.1\n+requests==2.22.0\n+six==1.12.0\n+urllib3==1.25.6\n+wcwidth==0.1.7\n+zipp==0.6.0"},{"sha":"aaaabbbbf8dfaef9044d2092e6de622052069b1d","filename":"awesome/__init__.py","status":"added","additions":20,"deletions":0,"changes":20,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/649eaaf2924e92dc7fd8d370ddb857033231e67a/requirements.txt","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/649eaaf2924e92dc7fd8d370ddb857033231e67a/requirements.txt","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/requirements.txt?ref=649eaaf2924e92dc7fd8d370ddb857033231e67a","patch":"diff --git a/awesome/__init__.py b/awesome/__init__.py\nindex bc9e176b..4cf0cf64 100644\n--- a/awesome/__init__.py\n+++ b/awesome/__init__.py\n@@ -4,3 +4,4 @@ test:\n some line\n-line modified (old)\n+line modified (new)\n+line added\n some line"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Length: + - '95' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 03 Sep 2024 13:17:10 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - github.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E8CF:37895C:261D361:4949EA9:66D70C56 + X-RateLimit-Limit: + - '60' + X-RateLimit-Remaining: + - '50' + X-RateLimit-Reset: + - '1725372143' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '10' + X-XSS-Protection: + - '0' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a/status?page=1&per_page=100 + response: + content: '{"state":"pending","statuses":[],"sha":"649eaaf2924e92dc7fd8d370ddb857033231e67a","total_count":0,"repository":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments"},"commit_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/649eaaf2924e92dc7fd8d370ddb857033231e67a/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Length: + - '95' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 03 Sep 2024 13:17:10 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - github.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E8CF:37895C:261D361:4949EA9:66D70C56 + X-RateLimit-Limit: + - '60' + X-RateLimit-Remaining: + - '50' + X-RateLimit-Reset: + - '1725372143' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '10' + X-XSS-Protection: + - '0' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/649eaaf2924e92dc7fd8d370ddb857033231e67a + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/649eaaf2924e92dc7fd8d370ddb857033231e67a","avatar_url":"https://github.com/images/error/hubot_happy.gif","id":1,"node_id":"MDY6U3RhdHVzMQ==","state":"success","description":"Build has completed successfully","target_url":"https://ci.example.com/1000/output","context":"continuous-integration/jenkins","created_at":"2012-07-20T01:19:13Z","updated_at":"2012-07-20T01:19:13Z","creator":{"login":"octocat","id":1,"node_id":"MDQ6VXNlcjE=","avatar_url":"https://github.com/images/error/octocat_happy.gif","gravatar_id":"","url":"https://api.github.com/users/octocat","html_url":"https://github.com/octocat","followers_url":"https://api.github.com/users/octocat/followers","following_url":"https://api.github.com/users/octocat/following{/other_user}","gists_url":"https://api.github.com/users/octocat/gists{/gist_id}","starred_url":"https://api.github.com/users/octocat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/octocat/subscriptions","organizations_url":"https://api.github.com/users/octocat/orgs","repos_url":"https://api.github.com/users/octocat/repos","events_url":"https://api.github.com/users/octocat/events{/privacy}","received_events_url":"https://api.github.com/users/octocat/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Length: + - '95' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 03 Sep 2024 13:17:10 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - github.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E8CF:37895C:261D361:4949EA9:66D70C56 + X-RateLimit-Limit: + - '60' + X-RateLimit-Remaining: + - '50' + X-RateLimit-Reset: + - '1725372143' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '10' + X-XSS-Protection: + - '0' + http_version: HTTP/1.1 + status_code: 201 +version: 1 \ No newline at end of file diff --git a/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_simple_call_only_status_notifiers_no_pull_request.yaml b/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_simple_call_only_status_notifiers_no_pull_request.yaml new file mode 100644 index 0000000000..185e85ac12 --- /dev/null +++ b/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_simple_call_only_status_notifiers_no_pull_request.yaml @@ -0,0 +1,890 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/status?page=1&per_page=100 + response: + content: '{"state":"failure","statuses":[{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324244409,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ0NDA5","state":"pending","description":"pending + - 4 - turtle","target_url":"https://localhost:50036/github/codecov","context":"turtle","created_at":"2020-04-08T20:49:44Z","updated_at":"2020-04-08T20:49:44Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324244483,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ0NDgz","state":"failure","description":"failure + - 5 - bird","target_url":"https://localhost:50036/github/codecov","context":"bird","created_at":"2020-04-08T20:49:44Z","updated_at":"2020-04-08T20:49:44Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324244621,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ0NjIx","state":"error","description":"error + - 6 - pig","target_url":"https://localhost:50036/github/codecov","context":"pig","created_at":"2020-04-08T20:49:45Z","updated_at":"2020-04-08T20:49:45Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324244913,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ0OTEz","state":"success","description":"success + - 9 - giant","target_url":"https://localhost:50036/github/codecov","context":"giant","created_at":"2020-04-08T20:49:46Z","updated_at":"2020-04-08T20:49:46Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324245017,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ1MDE3","state":"success","description":"success + - 10 - capybara","target_url":"https://localhost:50036/github/codecov","context":"capybara","created_at":"2020-04-08T20:49:47Z","updated_at":"2020-04-08T20:49:47Z"}],"sha":"f0895290dc26668faeeb20ee5ccd4cc995925775","total_count":5,"repository":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments"},"commit_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:12:07 GMT + Etag: + - W/"7b5cf4b2890fc250c0b842b249fb140e" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - repo, repo:status + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EBD0:05F2:2169DC:2EEB68:5E8F4957 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4981' + X-Ratelimit-Reset: + - '1586451769' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/status?page=1&per_page=100 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/search/issues?q=f0895290dc26668faeeb20ee5ccd4cc995925775+repo%3AThiagoCodecov%2Fexample-python+type%3Apr+state%3Aopen + response: + content: '{"total_count":0,"incomplete_results":false,"items":[]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - no-cache + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:12:07 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EBD1:0F75:1A8057:269E76:5E8F4957 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '30' + X-Ratelimit-Remaining: + - '29' + X-Ratelimit-Reset: + - '1586448787' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/search/issues?q=f0895290dc26668faeeb20ee5ccd4cc995925775+repo%3AThiagoCodecov%2Fexample-python+type%3Apr+state%3Aopen +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/status?page=1&per_page=100 + response: + content: '{"state":"failure","statuses":[{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324244409,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ0NDA5","state":"pending","description":"pending + - 4 - turtle","target_url":"https://localhost:50036/github/codecov","context":"turtle","created_at":"2020-04-08T20:49:44Z","updated_at":"2020-04-08T20:49:44Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324244483,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ0NDgz","state":"failure","description":"failure + - 5 - bird","target_url":"https://localhost:50036/github/codecov","context":"bird","created_at":"2020-04-08T20:49:44Z","updated_at":"2020-04-08T20:49:44Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324244621,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ0NjIx","state":"error","description":"error + - 6 - pig","target_url":"https://localhost:50036/github/codecov","context":"pig","created_at":"2020-04-08T20:49:45Z","updated_at":"2020-04-08T20:49:45Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324244913,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ0OTEz","state":"success","description":"success + - 9 - giant","target_url":"https://localhost:50036/github/codecov","context":"giant","created_at":"2020-04-08T20:49:46Z","updated_at":"2020-04-08T20:49:46Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324245017,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ1MDE3","state":"success","description":"success + - 10 - capybara","target_url":"https://localhost:50036/github/codecov","context":"capybara","created_at":"2020-04-08T20:49:47Z","updated_at":"2020-04-08T20:49:47Z"}],"sha":"f0895290dc26668faeeb20ee5ccd4cc995925775","total_count":5,"repository":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments"},"commit_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:12:08 GMT + Etag: + - W/"7b5cf4b2890fc250c0b842b249fb140e" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - repo, repo:status + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EBD4:05F3:1E35DC:2A2F0A:5E8F4957 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4980' + X-Ratelimit-Reset: + - '1586451770' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/status?page=1&per_page=100 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/compare/081d91921f05a8a39d39aef667eddb88e96300c7...f0895290dc26668faeeb20ee5ccd4cc995925775 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/081d91921f05a8a39d39aef667eddb88e96300c7...f0895290dc26668faeeb20ee5ccd4cc995925775","html_url":"https://github.com/ThiagoCodecov/example-python/compare/081d91921f05a8a39d39aef667eddb88e96300c7...f0895290dc26668faeeb20ee5ccd4cc995925775","permalink_url":"https://github.com/ThiagoCodecov/example-python/compare/ThiagoCodecov:081d919...ThiagoCodecov:f089529","diff_url":"https://github.com/ThiagoCodecov/example-python/compare/081d91921f05a8a39d39aef667eddb88e96300c7...f0895290dc26668faeeb20ee5ccd4cc995925775.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/compare/081d91921f05a8a39d39aef667eddb88e96300c7...f0895290dc26668faeeb20ee5ccd4cc995925775.patch","base_commit":{"sha":"081d91921f05a8a39d39aef667eddb88e96300c7","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA4MWQ5MTkyMWYwNWE4YTM5ZDM5YWVmNjY3ZWRkYjg4ZTk2MzAwYzc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"message":"182B7277-7D2C-420B-B005-92418CBD6F09","tree":{"sha":"e6af1eb1d589c63cf9cc5caf7b80024a30c2e892","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e6af1eb1d589c63cf9cc5caf7b80024a30c2e892"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/081d91921f05a8a39d39aef667eddb88e96300c7","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/081d91921f05a8a39d39aef667eddb88e96300c7","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f60187a642531c18d7af0b0f1d37294b809081fb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f60187a642531c18d7af0b0f1d37294b809081fb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f60187a642531c18d7af0b0f1d37294b809081fb"}]},"merge_base_commit":{"sha":"081d91921f05a8a39d39aef667eddb88e96300c7","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA4MWQ5MTkyMWYwNWE4YTM5ZDM5YWVmNjY3ZWRkYjg4ZTk2MzAwYzc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"message":"182B7277-7D2C-420B-B005-92418CBD6F09","tree":{"sha":"e6af1eb1d589c63cf9cc5caf7b80024a30c2e892","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e6af1eb1d589c63cf9cc5caf7b80024a30c2e892"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/081d91921f05a8a39d39aef667eddb88e96300c7","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/081d91921f05a8a39d39aef667eddb88e96300c7","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f60187a642531c18d7af0b0f1d37294b809081fb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f60187a642531c18d7af0b0f1d37294b809081fb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f60187a642531c18d7af0b0f1d37294b809081fb"}]},"status":"ahead","ahead_by":4,"behind_by":0,"total_commits":4,"commits":[{"sha":"3ebc2022c1f17cd84ea800f1fad20ab5161e48d8","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjNlYmMyMDIyYzFmMTdjZDg0ZWE4MDBmMWZhZDIwYWI1MTYxZTQ4ZDg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T11:04:30Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T11:39:43Z"},"message":"Cleaning + old code","tree":{"sha":"32d44fc0901bd32b47e4556230e5a0a0983ab76b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/32d44fc0901bd32b47e4556230e5a0a0983ab76b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3ebc2022c1f17cd84ea800f1fad20ab5161e48d8","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3ebc2022c1f17cd84ea800f1fad20ab5161e48d8","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3ebc2022c1f17cd84ea800f1fad20ab5161e48d8","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3ebc2022c1f17cd84ea800f1fad20ab5161e48d8/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"d645e75c4c398fab6f7052bcd4654402aff0fbb1","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d645e75c4c398fab6f7052bcd4654402aff0fbb1","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d645e75c4c398fab6f7052bcd4654402aff0fbb1"}]},{"sha":"27f89d73aa6d8232664bfedd9a11700932b9795d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjI3Zjg5ZDczYWE2ZDgyMzI2NjRiZmVkZDlhMTE3MDA5MzJiOTc5NWQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T11:10:13Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T11:40:21Z"},"message":"Second + commit incoming","tree":{"sha":"9c2626d04097fd60190eb6c5bb630a11e0f6c929","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9c2626d04097fd60190eb6c5bb630a11e0f6c929"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/27f89d73aa6d8232664bfedd9a11700932b9795d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/27f89d73aa6d8232664bfedd9a11700932b9795d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/27f89d73aa6d8232664bfedd9a11700932b9795d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/27f89d73aa6d8232664bfedd9a11700932b9795d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"3ebc2022c1f17cd84ea800f1fad20ab5161e48d8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3ebc2022c1f17cd84ea800f1fad20ab5161e48d8","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3ebc2022c1f17cd84ea800f1fad20ab5161e48d8"}]},{"sha":"89f3d20f2be3fb2d098e544814f8ce3636dc78c4","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg5ZjNkMjBmMmJlM2ZiMmQwOThlNTQ0ODE0ZjhjZTM2MzZkYzc4YzQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T11:13:17Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T11:40:21Z"},"message":"Adding + class code","tree":{"sha":"3a8dce85ba7351a8b72890f2f76dc833d03659ab","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3a8dce85ba7351a8b72890f2f76dc833d03659ab"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/89f3d20f2be3fb2d098e544814f8ce3636dc78c4","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/89f3d20f2be3fb2d098e544814f8ce3636dc78c4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/89f3d20f2be3fb2d098e544814f8ce3636dc78c4","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/89f3d20f2be3fb2d098e544814f8ce3636dc78c4/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"27f89d73aa6d8232664bfedd9a11700932b9795d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/27f89d73aa6d8232664bfedd9a11700932b9795d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/27f89d73aa6d8232664bfedd9a11700932b9795d"}]},{"sha":"f0895290dc26668faeeb20ee5ccd4cc995925775","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmYwODk1MjkwZGMyNjY2OGZhZWViMjBlZTVjY2Q0Y2M5OTU5MjU3NzU=","commit":{"author":{"name":"Thiago","email":"44376991+ThiagoCodecov@users.noreply.github.com","date":"2020-03-24T22:01:33Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2020-03-24T22:01:33Z"},"message":"Merge + pull request #12 from ThiagoCodecov/thiago/f/cool-branch\n\nThiago/f/cool branch","tree":{"sha":"cda64d688c17ae3fcf03b22ee869238002e410f0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/cda64d688c17ae3fcf03b22ee869238002e410f0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f0895290dc26668faeeb20ee5ccd4cc995925775","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJeeoM+CRBK7hj4Ov3rIwAAdHIIAA7IxV7nFuDJIEFyQpE9XGAA\nMI6C/eXQp7/K6J2xwbFhUUDhkuXlbiiVvPw59STKg6qvlZ937FZBc4BfzM5OJIhY\n2fQcZnqJATD7cskHakK1+X6fmDKjN9u/KR7EmWfgjzst+cFYExptPJ7z3XktMKu9\nS7WaomPPF4lAAtDScCYb1IUILDKK5oiQMeUVJVzXyiuD9jPwjE/xo8a6Hpkvir0Q\npfjFWMuIgj9MnEv7+IZIOL//upOljWKpgoZfMBszinrF1okEtsFgeRpFXkmyVLGd\nmJa+ElmnO0TpIBtV8t/O6+eAFoc9pW3hFCuWNsIBQBhW2py5Q3gQ1hPyHdGzpUY=\n=P/w8\n-----END + PGP SIGNATURE-----\n","payload":"tree cda64d688c17ae3fcf03b22ee869238002e410f0\nparent + 081d91921f05a8a39d39aef667eddb88e96300c7\nparent 89f3d20f2be3fb2d098e544814f8ce3636dc78c4\nauthor + Thiago <44376991+ThiagoCodecov@users.noreply.github.com> 1585087293 -0300\ncommitter + GitHub 1585087293 -0300\n\nMerge pull request #12 from + ThiagoCodecov/thiago/f/cool-branch\n\nThiago/f/cool branch"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f0895290dc26668faeeb20ee5ccd4cc995925775","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars3.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"081d91921f05a8a39d39aef667eddb88e96300c7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/081d91921f05a8a39d39aef667eddb88e96300c7"},{"sha":"89f3d20f2be3fb2d098e544814f8ce3636dc78c4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/89f3d20f2be3fb2d098e544814f8ce3636dc78c4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/89f3d20f2be3fb2d098e544814f8ce3636dc78c4"}]}],"files":[{"sha":"326dc8b55279ac0c4796cb803520b1486ff7a778","filename":"awesome/__init__.py","status":"modified","additions":5,"deletions":6,"changes":11,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/f0895290dc26668faeeb20ee5ccd4cc995925775/awesome/__init__.py","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/f0895290dc26668faeeb20ee5ccd4cc995925775/awesome/__init__.py","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/__init__.py?ref=f0895290dc26668faeeb20ee5ccd4cc995925775","patch":"@@ + -10,12 +10,6 @@ def shieeee(g):\n return f\"\\\\{g}/\"\n \n \n-def fib(n):\n- if + n < 2:\n- return 1\n- return fib(n - 2) + fib(n - 1)\n-\n-\n def coala(k):\n return + k * k\n \n@@ -31,3 +25,8 @@ def sample_function():\n def ha_number_2():\n return + \"HA\" + str(2)\n \n+\n+class ClassCode(object):\n+\n+ def __init__(self, + context):\n+ self.context = context"},{"sha":"7fb3c3fbd71a6d3f4b98964c0130f7e083505fcd","filename":"awesome/code_fib.py","status":"modified","additions":3,"deletions":1,"changes":4,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/f0895290dc26668faeeb20ee5ccd4cc995925775/awesome/code_fib.py","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/f0895290dc26668faeeb20ee5ccd4cc995925775/awesome/code_fib.py","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/code_fib.py?ref=f0895290dc26668faeeb20ee5ccd4cc995925775","patch":"@@ + -1,6 +1,8 @@\n def fib(n):\n- if n <= 1:\n+ if n < 0:\n return + 0\n+ if n <= 1:\n+ return 1\n return fib(n - 1) + fib(n - 2)\n + \n "},{"sha":"8f1219afdad18e39608abf7a20a264e9421bfd13","filename":"tests/test_number_two.py","status":"modified","additions":8,"deletions":1,"changes":9,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/f0895290dc26668faeeb20ee5ccd4cc995925775/tests/test_number_two.py","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/f0895290dc26668faeeb20ee5ccd4cc995925775/tests/test_number_two.py","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests/test_number_two.py?ref=f0895290dc26668faeeb20ee5ccd4cc995925775","patch":"@@ + -1,3 +1,5 @@\n+import pytest\n+\n import awesome\n from awesome.code_fib import + fib\n \n@@ -7,8 +9,13 @@ def test_something():\n \n \n def test_a():\n- assert + fib(2) == 0\n+ assert fib(2) == 2\n \n \n def test_nothing():\n assert + True\n+\n+\n+def test_untested():\n+ with pytest.raises(Exception):\n+ awesome.untested_code()"},{"sha":"5334cacf77d9166ad5e70c552077a72f9ffb10d2","filename":"tests/test_sample.py","status":"modified","additions":4,"deletions":3,"changes":7,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/f0895290dc26668faeeb20ee5ccd4cc995925775/tests/test_sample.py","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/f0895290dc26668faeeb20ee5ccd4cc995925775/tests/test_sample.py","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests/test_sample.py?ref=f0895290dc26668faeeb20ee5ccd4cc995925775","patch":"@@ + -1,17 +1,18 @@\n import awesome\n+from awesome.code_fib import fib\n \n \n def + test_something():\n assert awesome.smile() == '':)''\n \n \n def test_fib():\n- assert + awesome.fib(1) == 1\n+ assert fib(1) == 1\n \n \n def test_fib_second():\n- assert + awesome.fib(3) == 3\n- assert awesome.fib(5) == 8\n+ assert fib(3) == + 3\n+ assert fib(5) == 8\n \n \n def test_something_wrong():"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:12:08 GMT + Etag: + - W/"21d9209aa0bebbcb7c80cf4911297b26" + Last-Modified: + - Tue, 24 Mar 2020 22:01:33 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EBD2:05F3:1E35DC:2A2F09:5E8F4957 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4979' + X-Ratelimit-Reset: + - '1586451770' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/compare/081d91921f05a8a39d39aef667eddb88e96300c7...f0895290dc26668faeeb20ee5ccd4cc995925775 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/compare/081d91921f05a8a39d39aef667eddb88e96300c7...f0895290dc26668faeeb20ee5ccd4cc995925775 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/081d91921f05a8a39d39aef667eddb88e96300c7...f0895290dc26668faeeb20ee5ccd4cc995925775","html_url":"https://github.com/ThiagoCodecov/example-python/compare/081d91921f05a8a39d39aef667eddb88e96300c7...f0895290dc26668faeeb20ee5ccd4cc995925775","permalink_url":"https://github.com/ThiagoCodecov/example-python/compare/ThiagoCodecov:081d919...ThiagoCodecov:f089529","diff_url":"https://github.com/ThiagoCodecov/example-python/compare/081d91921f05a8a39d39aef667eddb88e96300c7...f0895290dc26668faeeb20ee5ccd4cc995925775.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/compare/081d91921f05a8a39d39aef667eddb88e96300c7...f0895290dc26668faeeb20ee5ccd4cc995925775.patch","base_commit":{"sha":"081d91921f05a8a39d39aef667eddb88e96300c7","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA4MWQ5MTkyMWYwNWE4YTM5ZDM5YWVmNjY3ZWRkYjg4ZTk2MzAwYzc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"message":"182B7277-7D2C-420B-B005-92418CBD6F09","tree":{"sha":"e6af1eb1d589c63cf9cc5caf7b80024a30c2e892","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e6af1eb1d589c63cf9cc5caf7b80024a30c2e892"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/081d91921f05a8a39d39aef667eddb88e96300c7","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/081d91921f05a8a39d39aef667eddb88e96300c7","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f60187a642531c18d7af0b0f1d37294b809081fb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f60187a642531c18d7af0b0f1d37294b809081fb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f60187a642531c18d7af0b0f1d37294b809081fb"}]},"merge_base_commit":{"sha":"081d91921f05a8a39d39aef667eddb88e96300c7","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA4MWQ5MTkyMWYwNWE4YTM5ZDM5YWVmNjY3ZWRkYjg4ZTk2MzAwYzc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"message":"182B7277-7D2C-420B-B005-92418CBD6F09","tree":{"sha":"e6af1eb1d589c63cf9cc5caf7b80024a30c2e892","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e6af1eb1d589c63cf9cc5caf7b80024a30c2e892"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/081d91921f05a8a39d39aef667eddb88e96300c7","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/081d91921f05a8a39d39aef667eddb88e96300c7","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f60187a642531c18d7af0b0f1d37294b809081fb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f60187a642531c18d7af0b0f1d37294b809081fb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f60187a642531c18d7af0b0f1d37294b809081fb"}]},"status":"ahead","ahead_by":4,"behind_by":0,"total_commits":4,"commits":[{"sha":"3ebc2022c1f17cd84ea800f1fad20ab5161e48d8","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjNlYmMyMDIyYzFmMTdjZDg0ZWE4MDBmMWZhZDIwYWI1MTYxZTQ4ZDg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T11:04:30Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T11:39:43Z"},"message":"Cleaning + old code","tree":{"sha":"32d44fc0901bd32b47e4556230e5a0a0983ab76b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/32d44fc0901bd32b47e4556230e5a0a0983ab76b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3ebc2022c1f17cd84ea800f1fad20ab5161e48d8","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3ebc2022c1f17cd84ea800f1fad20ab5161e48d8","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3ebc2022c1f17cd84ea800f1fad20ab5161e48d8","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3ebc2022c1f17cd84ea800f1fad20ab5161e48d8/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"d645e75c4c398fab6f7052bcd4654402aff0fbb1","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d645e75c4c398fab6f7052bcd4654402aff0fbb1","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d645e75c4c398fab6f7052bcd4654402aff0fbb1"}]},{"sha":"27f89d73aa6d8232664bfedd9a11700932b9795d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjI3Zjg5ZDczYWE2ZDgyMzI2NjRiZmVkZDlhMTE3MDA5MzJiOTc5NWQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T11:10:13Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T11:40:21Z"},"message":"Second + commit incoming","tree":{"sha":"9c2626d04097fd60190eb6c5bb630a11e0f6c929","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9c2626d04097fd60190eb6c5bb630a11e0f6c929"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/27f89d73aa6d8232664bfedd9a11700932b9795d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/27f89d73aa6d8232664bfedd9a11700932b9795d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/27f89d73aa6d8232664bfedd9a11700932b9795d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/27f89d73aa6d8232664bfedd9a11700932b9795d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"3ebc2022c1f17cd84ea800f1fad20ab5161e48d8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3ebc2022c1f17cd84ea800f1fad20ab5161e48d8","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3ebc2022c1f17cd84ea800f1fad20ab5161e48d8"}]},{"sha":"89f3d20f2be3fb2d098e544814f8ce3636dc78c4","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg5ZjNkMjBmMmJlM2ZiMmQwOThlNTQ0ODE0ZjhjZTM2MzZkYzc4YzQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T11:13:17Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T11:40:21Z"},"message":"Adding + class code","tree":{"sha":"3a8dce85ba7351a8b72890f2f76dc833d03659ab","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3a8dce85ba7351a8b72890f2f76dc833d03659ab"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/89f3d20f2be3fb2d098e544814f8ce3636dc78c4","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/89f3d20f2be3fb2d098e544814f8ce3636dc78c4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/89f3d20f2be3fb2d098e544814f8ce3636dc78c4","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/89f3d20f2be3fb2d098e544814f8ce3636dc78c4/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"27f89d73aa6d8232664bfedd9a11700932b9795d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/27f89d73aa6d8232664bfedd9a11700932b9795d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/27f89d73aa6d8232664bfedd9a11700932b9795d"}]},{"sha":"f0895290dc26668faeeb20ee5ccd4cc995925775","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmYwODk1MjkwZGMyNjY2OGZhZWViMjBlZTVjY2Q0Y2M5OTU5MjU3NzU=","commit":{"author":{"name":"Thiago","email":"44376991+ThiagoCodecov@users.noreply.github.com","date":"2020-03-24T22:01:33Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2020-03-24T22:01:33Z"},"message":"Merge + pull request #12 from ThiagoCodecov/thiago/f/cool-branch\n\nThiago/f/cool branch","tree":{"sha":"cda64d688c17ae3fcf03b22ee869238002e410f0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/cda64d688c17ae3fcf03b22ee869238002e410f0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f0895290dc26668faeeb20ee5ccd4cc995925775","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJeeoM+CRBK7hj4Ov3rIwAAdHIIAA7IxV7nFuDJIEFyQpE9XGAA\nMI6C/eXQp7/K6J2xwbFhUUDhkuXlbiiVvPw59STKg6qvlZ937FZBc4BfzM5OJIhY\n2fQcZnqJATD7cskHakK1+X6fmDKjN9u/KR7EmWfgjzst+cFYExptPJ7z3XktMKu9\nS7WaomPPF4lAAtDScCYb1IUILDKK5oiQMeUVJVzXyiuD9jPwjE/xo8a6Hpkvir0Q\npfjFWMuIgj9MnEv7+IZIOL//upOljWKpgoZfMBszinrF1okEtsFgeRpFXkmyVLGd\nmJa+ElmnO0TpIBtV8t/O6+eAFoc9pW3hFCuWNsIBQBhW2py5Q3gQ1hPyHdGzpUY=\n=P/w8\n-----END + PGP SIGNATURE-----\n","payload":"tree cda64d688c17ae3fcf03b22ee869238002e410f0\nparent + 081d91921f05a8a39d39aef667eddb88e96300c7\nparent 89f3d20f2be3fb2d098e544814f8ce3636dc78c4\nauthor + Thiago <44376991+ThiagoCodecov@users.noreply.github.com> 1585087293 -0300\ncommitter + GitHub 1585087293 -0300\n\nMerge pull request #12 from + ThiagoCodecov/thiago/f/cool-branch\n\nThiago/f/cool branch"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f0895290dc26668faeeb20ee5ccd4cc995925775","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars3.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"081d91921f05a8a39d39aef667eddb88e96300c7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/081d91921f05a8a39d39aef667eddb88e96300c7"},{"sha":"89f3d20f2be3fb2d098e544814f8ce3636dc78c4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/89f3d20f2be3fb2d098e544814f8ce3636dc78c4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/89f3d20f2be3fb2d098e544814f8ce3636dc78c4"}]}],"files":[{"sha":"326dc8b55279ac0c4796cb803520b1486ff7a778","filename":"awesome/__init__.py","status":"modified","additions":5,"deletions":6,"changes":11,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/f0895290dc26668faeeb20ee5ccd4cc995925775/awesome/__init__.py","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/f0895290dc26668faeeb20ee5ccd4cc995925775/awesome/__init__.py","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/__init__.py?ref=f0895290dc26668faeeb20ee5ccd4cc995925775","patch":"@@ + -10,12 +10,6 @@ def shieeee(g):\n return f\"\\\\{g}/\"\n \n \n-def fib(n):\n- if + n < 2:\n- return 1\n- return fib(n - 2) + fib(n - 1)\n-\n-\n def coala(k):\n return + k * k\n \n@@ -31,3 +25,8 @@ def sample_function():\n def ha_number_2():\n return + \"HA\" + str(2)\n \n+\n+class ClassCode(object):\n+\n+ def __init__(self, + context):\n+ self.context = context"},{"sha":"7fb3c3fbd71a6d3f4b98964c0130f7e083505fcd","filename":"awesome/code_fib.py","status":"modified","additions":3,"deletions":1,"changes":4,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/f0895290dc26668faeeb20ee5ccd4cc995925775/awesome/code_fib.py","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/f0895290dc26668faeeb20ee5ccd4cc995925775/awesome/code_fib.py","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/code_fib.py?ref=f0895290dc26668faeeb20ee5ccd4cc995925775","patch":"@@ + -1,6 +1,8 @@\n def fib(n):\n- if n <= 1:\n+ if n < 0:\n return + 0\n+ if n <= 1:\n+ return 1\n return fib(n - 1) + fib(n - 2)\n + \n "},{"sha":"8f1219afdad18e39608abf7a20a264e9421bfd13","filename":"tests/test_number_two.py","status":"modified","additions":8,"deletions":1,"changes":9,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/f0895290dc26668faeeb20ee5ccd4cc995925775/tests/test_number_two.py","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/f0895290dc26668faeeb20ee5ccd4cc995925775/tests/test_number_two.py","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests/test_number_two.py?ref=f0895290dc26668faeeb20ee5ccd4cc995925775","patch":"@@ + -1,3 +1,5 @@\n+import pytest\n+\n import awesome\n from awesome.code_fib import + fib\n \n@@ -7,8 +9,13 @@ def test_something():\n \n \n def test_a():\n- assert + fib(2) == 0\n+ assert fib(2) == 2\n \n \n def test_nothing():\n assert + True\n+\n+\n+def test_untested():\n+ with pytest.raises(Exception):\n+ awesome.untested_code()"},{"sha":"5334cacf77d9166ad5e70c552077a72f9ffb10d2","filename":"tests/test_sample.py","status":"modified","additions":4,"deletions":3,"changes":7,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/f0895290dc26668faeeb20ee5ccd4cc995925775/tests/test_sample.py","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/f0895290dc26668faeeb20ee5ccd4cc995925775/tests/test_sample.py","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests/test_sample.py?ref=f0895290dc26668faeeb20ee5ccd4cc995925775","patch":"@@ + -1,17 +1,18 @@\n import awesome\n+from awesome.code_fib import fib\n \n \n def + test_something():\n assert awesome.smile() == '':)''\n \n \n def test_fib():\n- assert + awesome.fib(1) == 1\n+ assert fib(1) == 1\n \n \n def test_fib_second():\n- assert + awesome.fib(3) == 3\n- assert awesome.fib(5) == 8\n+ assert fib(3) == + 3\n+ assert fib(5) == 8\n \n \n def test_something_wrong():"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:12:08 GMT + Etag: + - W/"21d9209aa0bebbcb7c80cf4911297b26" + Last-Modified: + - Tue, 24 Mar 2020 22:01:33 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EBD3:0F75:1A805B:269E7D:5E8F4957 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4978' + X-Ratelimit-Reset: + - '1586451770' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/compare/081d91921f05a8a39d39aef667eddb88e96300c7...f0895290dc26668faeeb20ee5ccd4cc995925775 +- request: + body: '{"state": "success", "target_url": "https://myexamplewebsite.io/gh/ThiagoCodecov/example-python/commit/f0895290dc26668faeeb20ee5ccd4cc995925775", + "context": "codecov/project", "description": "85.00% (+0.00%) compared to 081d919"}' + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9333281614,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzMzMjgxNjE0","state":"success","description":"85.00% + (+0.00%) compared to 081d919","target_url":"https://myexamplewebsite.io/gh/ThiagoCodecov/example-python/commit/f0895290dc26668faeeb20ee5ccd4cc995925775","context":"codecov/project","created_at":"2020-04-09T16:12:08Z","updated_at":"2020-04-09T16:12:08Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Length: + - '1523' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:12:08 GMT + Etag: + - '"731de6d02eba962df2bf8ca0dc9ae3d1"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EBD5:4EFE:1B5027:271077:5E8F4958 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4977' + X-Ratelimit-Reset: + - '1586451769' + X-Xss-Protection: + - 1; mode=block + status: + code: 201 + message: Created + status_code: 201 + url: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/status?page=1&per_page=100 + response: + content: '{"state":"failure","statuses":[{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324244409,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ0NDA5","state":"pending","description":"pending + - 4 - turtle","target_url":"https://localhost:50036/github/codecov","context":"turtle","created_at":"2020-04-08T20:49:44Z","updated_at":"2020-04-08T20:49:44Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324244483,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ0NDgz","state":"failure","description":"failure + - 5 - bird","target_url":"https://localhost:50036/github/codecov","context":"bird","created_at":"2020-04-08T20:49:44Z","updated_at":"2020-04-08T20:49:44Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324244621,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ0NjIx","state":"error","description":"error + - 6 - pig","target_url":"https://localhost:50036/github/codecov","context":"pig","created_at":"2020-04-08T20:49:45Z","updated_at":"2020-04-08T20:49:45Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324244913,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ0OTEz","state":"success","description":"success + - 9 - giant","target_url":"https://localhost:50036/github/codecov","context":"giant","created_at":"2020-04-08T20:49:46Z","updated_at":"2020-04-08T20:49:46Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324245017,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ1MDE3","state":"success","description":"success + - 10 - capybara","target_url":"https://localhost:50036/github/codecov","context":"capybara","created_at":"2020-04-08T20:49:47Z","updated_at":"2020-04-08T20:49:47Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9333281614,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzMzMjgxNjE0","state":"success","description":"85.00% + (+0.00%) compared to 081d919","target_url":"https://myexamplewebsite.io/gh/ThiagoCodecov/example-python/commit/f0895290dc26668faeeb20ee5ccd4cc995925775","context":"codecov/project","created_at":"2020-04-09T16:12:08Z","updated_at":"2020-04-09T16:12:08Z"}],"sha":"f0895290dc26668faeeb20ee5ccd4cc995925775","total_count":6,"repository":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments"},"commit_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:12:08 GMT + Etag: + - W/"fa60044ba8fa2bded529108a809b0aeb" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - repo, repo:status + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EBD7:4EFE:1B502A:27107D:5E8F4958 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4976' + X-Ratelimit-Reset: + - '1586451769' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/status?page=1&per_page=100 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/status?page=1&per_page=100 + response: + content: '{"state":"failure","statuses":[{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324244409,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ0NDA5","state":"pending","description":"pending + - 4 - turtle","target_url":"https://localhost:50036/github/codecov","context":"turtle","created_at":"2020-04-08T20:49:44Z","updated_at":"2020-04-08T20:49:44Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324244483,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ0NDgz","state":"failure","description":"failure + - 5 - bird","target_url":"https://localhost:50036/github/codecov","context":"bird","created_at":"2020-04-08T20:49:44Z","updated_at":"2020-04-08T20:49:44Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324244621,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ0NjIx","state":"error","description":"error + - 6 - pig","target_url":"https://localhost:50036/github/codecov","context":"pig","created_at":"2020-04-08T20:49:45Z","updated_at":"2020-04-08T20:49:45Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324244913,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ0OTEz","state":"success","description":"success + - 9 - giant","target_url":"https://localhost:50036/github/codecov","context":"giant","created_at":"2020-04-08T20:49:46Z","updated_at":"2020-04-08T20:49:46Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9324245017,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MjQ1MDE3","state":"success","description":"success + - 10 - capybara","target_url":"https://localhost:50036/github/codecov","context":"capybara","created_at":"2020-04-08T20:49:47Z","updated_at":"2020-04-08T20:49:47Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9333281614,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzMzMjgxNjE0","state":"success","description":"85.00% + (+0.00%) compared to 081d919","target_url":"https://myexamplewebsite.io/gh/ThiagoCodecov/example-python/commit/f0895290dc26668faeeb20ee5ccd4cc995925775","context":"codecov/project","created_at":"2020-04-09T16:12:08Z","updated_at":"2020-04-09T16:12:08Z"}],"sha":"f0895290dc26668faeeb20ee5ccd4cc995925775","total_count":6,"repository":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments"},"commit_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:12:08 GMT + Etag: + - W/"fa60044ba8fa2bded529108a809b0aeb" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - repo, repo:status + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EBD6:05F3:1E35E6:2A2F18:5E8F4958 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4975' + X-Ratelimit-Reset: + - '1586451769' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/status?page=1&per_page=100 +- request: + body: '{"state": "success", "target_url": "https://myexamplewebsite.io/gh/ThiagoCodecov/example-python/commit/f0895290dc26668faeeb20ee5ccd4cc995925775", + "context": "codecov/patch", "description": "Coverage not affected when comparing + 081d919...f089529"}' + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9333281697,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzMzMjgxNjk3","state":"success","description":"Coverage + not affected when comparing 081d919...f089529","target_url":"https://myexamplewebsite.io/gh/ThiagoCodecov/example-python/commit/f0895290dc26668faeeb20ee5ccd4cc995925775","context":"codecov/patch","created_at":"2020-04-09T16:12:08Z","updated_at":"2020-04-09T16:12:08Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Length: + - '1540' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:12:09 GMT + Etag: + - '"5125c0192a2899ec83518df88ca09f59"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EBD9:0F74:1E9B6D:2BAF15:5E8F4958 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4974' + X-Ratelimit-Reset: + - '1586451770' + X-Xss-Protection: + - 1; mode=block + status: + code: 201 + message: Created + status_code: 201 + url: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775 +- request: + body: '{"state": "failure", "target_url": "https://myexamplewebsite.io/gh/ThiagoCodecov/example-python/commit/f0895290dc26668faeeb20ee5ccd4cc995925775", + "context": "codecov/changes", "description": "1 file has unexpected coverage + changes not visible in diff"}' + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9333281703,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzMzMjgxNzAz","state":"failure","description":"1 + file has unexpected coverage changes not visible in diff","target_url":"https://myexamplewebsite.io/gh/ThiagoCodecov/example-python/commit/f0895290dc26668faeeb20ee5ccd4cc995925775","context":"codecov/changes","created_at":"2020-04-09T16:12:08Z","updated_at":"2020-04-09T16:12:08Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Length: + - '1546' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:12:09 GMT + Etag: + - '"7a180dbe2132fb9df3a5e5e6f24ef9d5"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EBD8:4EFE:1B502F:271086:5E8F4958 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4973' + X-Ratelimit-Reset: + - '1586451770' + X-Xss-Protection: + - 1; mode=block + status: + code: 201 + message: Created + status_code: 201 + url: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/f0895290dc26668faeeb20ee5ccd4cc995925775 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/pulls + response: + content: '[{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/12","id":350655906,"node_id":"MDExOlB1bGxSZXF1ZXN0MzUwNjU1OTA2","html_url":"https://github.com/ThiagoCodecov/example-python/pull/12","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/12.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/12.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/12","number":12,"state":"closed","locked":false,"title":"Thiago/f/cool + branch","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"","created_at":"2019-12-09T11:11:24Z","updated_at":"2022-08-17T20:09:03Z","closed_at":"2020-03-24T22:01:34Z","merged_at":"2020-03-24T22:01:34Z","merge_commit_sha":"f0895290dc26668faeeb20ee5ccd4cc995925775","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/12/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/12/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/12/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/89f3d20f2be3fb2d098e544814f8ce3636dc78c4","head":{"label":"ThiagoCodecov:thiago/f/cool-branch","ref":"thiago/f/cool-branch","sha":"89f3d20f2be3fb2d098e544814f8ce3636dc78c4","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-12-04T04:21:29Z","pushed_at":"2022-08-17T20:09:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":2,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":2,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"ThiagoCodecov:master","ref":"master","sha":"d645e75c4c398fab6f7052bcd4654402aff0fbb1","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-12-04T04:21:29Z","pushed_at":"2022-08-17T20:09:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":2,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":2,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/12"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/12"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/12"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/12/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/12/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/12/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/89f3d20f2be3fb2d098e544814f8ce3636dc78c4"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 17 Aug 2022 20:10:02 GMT + ETag: + - W/"7223ee50580c39a1bfa244d1a60c64a04c2e2aecbb66a536cf6113214e9fbe48" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D736:1C33:EB318:F68CE:62FD4B1A + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1660770602' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-08-24 01:18:21 UTC + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_simple_call_only_status_notifiers_with_pull_request.yaml b/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_simple_call_only_status_notifiers_with_pull_request.yaml new file mode 100644 index 0000000000..84e9380b48 --- /dev/null +++ b/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_simple_call_only_status_notifiers_with_pull_request.yaml @@ -0,0 +1,1107 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c/status?page=1&per_page=100 + response: + content: '{"state":"pending","statuses":[],"sha":"11daa27b1b74fd181836a64106f936a16404089c","total_count":0,"repository":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments"},"commit_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:19:21 GMT + Etag: + - W/"3836d1bda5994807246356db5ad950cc" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - repo, repo:status + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EC48:2F61:1F517F:2C84C2:5E8F4B09 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4972' + X-Ratelimit-Reset: + - '1586451769' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c/status?page=1&per_page=100 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/search/issues?q=11daa27b1b74fd181836a64106f936a16404089c+repo%3AThiagoCodecov%2Fexample-python+type%3Apr+state%3Aopen + response: + content: '{"total_count":1,"incomplete_results":false,"items":[{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/18","repository_url":"https://api.github.com/repos/ThiagoCodecov/example-python","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/18/labels{/name}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/18/comments","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/18/events","html_url":"https://github.com/ThiagoCodecov/example-python/pull/18","id":575148804,"node_id":"MDExOlB1bGxSZXF1ZXN0MzgzMzQ4Nzc1","number":18,"title":"Thiago/base + no base","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":6,"created_at":"2020-03-04T05:32:53Z","updated_at":"2020-04-08T20:50:42Z","closed_at":null,"author_association":"OWNER","draft":false,"pull_request":{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18","html_url":"https://github.com/ThiagoCodecov/example-python/pull/18","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/18.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/18.patch"},"body":"","score":1.0}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - no-cache + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:19:22 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EC49:6512:1E94D5:2BDA11:5E8F4B0A + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '30' + X-Ratelimit-Remaining: + - '29' + X-Ratelimit-Reset: + - '1586449222' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/search/issues?q=11daa27b1b74fd181836a64106f936a16404089c+repo%3AThiagoCodecov%2Fexample-python+type%3Apr+state%3Aopen +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18","id":383348775,"node_id":"MDExOlB1bGxSZXF1ZXN0MzgzMzQ4Nzc1","html_url":"https://github.com/ThiagoCodecov/example-python/pull/18","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/18.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/18.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/18","number":18,"state":"open","locked":false,"title":"Thiago/base + no base","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"","created_at":"2020-03-04T05:32:53Z","updated_at":"2020-04-08T20:50:42Z","closed_at":null,"merged_at":null,"merge_commit_sha":"63c31ff299fa85e990a635e7fe47debc242b027a","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/18/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e999aac5b33acbca52601d2a655ab0ac46a1ffdf","head":{"label":"ThiagoCodecov:thiago/base-no-base","ref":"thiago/base-no-base","sha":"e999aac5b33acbca52601d2a655ab0ac46a1ffdf","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-03-24T22:01:40Z","pushed_at":"2020-04-08T20:50:43Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":266,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":null,"forks":0,"open_issues":2,"watchers":0,"default_branch":"master"}},"base":{"label":"ThiagoCodecov:master","ref":"master","sha":"081d91921f05a8a39d39aef667eddb88e96300c7","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-03-24T22:01:40Z","pushed_at":"2020-04-08T20:50:43Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":266,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":null,"forks":0,"open_issues":2,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/18"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/18"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/18/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e999aac5b33acbca52601d2a655ab0ac46a1ffdf"}},"author_association":"OWNER","merged":false,"mergeable":true,"rebaseable":true,"mergeable_state":"unstable","merged_by":null,"comments":6,"review_comments":0,"maintainer_can_modify":false,"commits":18,"additions":65,"deletions":3506,"changed_files":8}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:19:22 GMT + Etag: + - W/"bb2f1399b8dfebff6cf73e465029facc" + Last-Modified: + - Thu, 09 Apr 2020 16:02:08 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EC4A:6512:1E94DD:2BDA1F:5E8F4B0A + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4971' + X-Ratelimit-Reset: + - '1586451769' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18/commits + response: + content: '[{"sha":"11daa27b1b74fd181836a64106f936a16404089c","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjExZGFhMjdiMWI3NGZkMTgxODM2YTY0MTA2ZjkzNmExNjQwNDA4OWM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:31:33Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:31:33Z"},"message":"F8926640-91E9-4A1E-B209-0101666AB0BB","tree":{"sha":"6be9ab3da2917081449644850f6ecf058d0feded","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6be9ab3da2917081449644850f6ecf058d0feded"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/11daa27b1b74fd181836a64106f936a16404089c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/11daa27b1b74fd181836a64106f936a16404089c","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"081d91921f05a8a39d39aef667eddb88e96300c7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/081d91921f05a8a39d39aef667eddb88e96300c7"}]},{"sha":"13a3976301d1cfcb934f11d12e57dbf23f4b353c","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjEzYTM5NzYzMDFkMWNmY2I5MzRmMTFkMTJlNTdkYmYyM2Y0YjM1M2M=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:31:33Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-17T20:42:44Z"},"message":"AAA","tree":{"sha":"65ed3234d14c3b5aac6eaf99880bfaa7008696a9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/65ed3234d14c3b5aac6eaf99880bfaa7008696a9"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/13a3976301d1cfcb934f11d12e57dbf23f4b353c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/13a3976301d1cfcb934f11d12e57dbf23f4b353c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/13a3976301d1cfcb934f11d12e57dbf23f4b353c","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/13a3976301d1cfcb934f11d12e57dbf23f4b353c/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"11daa27b1b74fd181836a64106f936a16404089c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/11daa27b1b74fd181836a64106f936a16404089c"}]},{"sha":"658a6602a7af9cabc24dac621065bbe4c6af3143","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjY1OGE2NjAyYTdhZjljYWJjMjRkYWM2MjEwNjViYmU0YzZhZjMxNDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-17T20:44:12Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-17T20:44:12Z"},"message":"3B2313E3-F5B7-4157-B446-AA7AA180CE26","tree":{"sha":"91936ef02e92450ca2d4ce3c2ad1b286a4ff586c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/91936ef02e92450ca2d4ce3c2ad1b286a4ff586c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/658a6602a7af9cabc24dac621065bbe4c6af3143","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/658a6602a7af9cabc24dac621065bbe4c6af3143","html_url":"https://github.com/ThiagoCodecov/example-python/commit/658a6602a7af9cabc24dac621065bbe4c6af3143","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/658a6602a7af9cabc24dac621065bbe4c6af3143/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"13a3976301d1cfcb934f11d12e57dbf23f4b353c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/13a3976301d1cfcb934f11d12e57dbf23f4b353c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/13a3976301d1cfcb934f11d12e57dbf23f4b353c"}]},{"sha":"60e64b877c2d799ffe05d0acc3ecfaf9400a4823","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjYwZTY0Yjg3N2MyZDc5OWZmZTA1ZDBhY2MzZWNmYWY5NDAwYTQ4MjM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-17T20:44:12Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-17T20:44:12Z"},"message":"F8F6009F-3C7F-491D-BBD3-609F681D6359","tree":{"sha":"2c2dbfc01e0ba0824904cd2bfd19ef206fd4301e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2c2dbfc01e0ba0824904cd2bfd19ef206fd4301e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/60e64b877c2d799ffe05d0acc3ecfaf9400a4823","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/60e64b877c2d799ffe05d0acc3ecfaf9400a4823","html_url":"https://github.com/ThiagoCodecov/example-python/commit/60e64b877c2d799ffe05d0acc3ecfaf9400a4823","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/60e64b877c2d799ffe05d0acc3ecfaf9400a4823/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"658a6602a7af9cabc24dac621065bbe4c6af3143","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/658a6602a7af9cabc24dac621065bbe4c6af3143","html_url":"https://github.com/ThiagoCodecov/example-python/commit/658a6602a7af9cabc24dac621065bbe4c6af3143"}]},{"sha":"d903a9181301c5fb228560cd6310637bfe25351a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmQ5MDNhOTE4MTMwMWM1ZmIyMjg1NjBjZDYzMTA2MzdiZmUyNTM1MWE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-17T23:03:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-17T23:03:00Z"},"message":"DA17062D-CCB7-4703-99BE-D7F5377D1A8C","tree":{"sha":"d41120716111617289d590eb0a248fbfabbb130f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/d41120716111617289d590eb0a248fbfabbb130f"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d903a9181301c5fb228560cd6310637bfe25351a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d903a9181301c5fb228560cd6310637bfe25351a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d903a9181301c5fb228560cd6310637bfe25351a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d903a9181301c5fb228560cd6310637bfe25351a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"60e64b877c2d799ffe05d0acc3ecfaf9400a4823","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/60e64b877c2d799ffe05d0acc3ecfaf9400a4823","html_url":"https://github.com/ThiagoCodecov/example-python/commit/60e64b877c2d799ffe05d0acc3ecfaf9400a4823"}]},{"sha":"877f0ef734dbd423fe8f9be3b6d8c1af966b21e4","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg3N2YwZWY3MzRkYmQ0MjNmZThmOWJlM2I2ZDhjMWFmOTY2YjIxZTQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-17T23:03:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-17T23:03:00Z"},"message":"698AF9F4-898B-421D-AF90-A11377A866F9","tree":{"sha":"c2a74ec9c361da2e09cd45548d076fc1f0f57e55","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c2a74ec9c361da2e09cd45548d076fc1f0f57e55"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/877f0ef734dbd423fe8f9be3b6d8c1af966b21e4","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/877f0ef734dbd423fe8f9be3b6d8c1af966b21e4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/877f0ef734dbd423fe8f9be3b6d8c1af966b21e4","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/877f0ef734dbd423fe8f9be3b6d8c1af966b21e4/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"d903a9181301c5fb228560cd6310637bfe25351a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d903a9181301c5fb228560cd6310637bfe25351a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d903a9181301c5fb228560cd6310637bfe25351a"}]},{"sha":"a36c4f0a4137a0c56b13a3a0734531bb82f2f073","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmEzNmM0ZjBhNDEzN2EwYzU2YjEzYTNhMDczNDUzMWJiODJmMmYwNzM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-20T23:09:54Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-20T23:09:54Z"},"message":"3DF19566-C212-4CC5-AACF-AF743C3A85EC","tree":{"sha":"b12a1b53f04f96ec28a1fab0e27f0004594a36e0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b12a1b53f04f96ec28a1fab0e27f0004594a36e0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/a36c4f0a4137a0c56b13a3a0734531bb82f2f073","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a36c4f0a4137a0c56b13a3a0734531bb82f2f073","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a36c4f0a4137a0c56b13a3a0734531bb82f2f073","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a36c4f0a4137a0c56b13a3a0734531bb82f2f073/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"877f0ef734dbd423fe8f9be3b6d8c1af966b21e4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/877f0ef734dbd423fe8f9be3b6d8c1af966b21e4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/877f0ef734dbd423fe8f9be3b6d8c1af966b21e4"}]},{"sha":"04ce0ba971c3151f915ea5c361cd719a535be596","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA0Y2UwYmE5NzFjMzE1MWY5MTVlYTVjMzYxY2Q3MTlhNTM1YmU1OTY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-20T23:09:54Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-20T23:09:54Z"},"message":"2D7C68B0-4508-4F7B-A513-D176B82DCB20","tree":{"sha":"915a9d7d1ef209813f7e44d42567e2bf86aac244","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/915a9d7d1ef209813f7e44d42567e2bf86aac244"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/04ce0ba971c3151f915ea5c361cd719a535be596","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/04ce0ba971c3151f915ea5c361cd719a535be596","html_url":"https://github.com/ThiagoCodecov/example-python/commit/04ce0ba971c3151f915ea5c361cd719a535be596","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/04ce0ba971c3151f915ea5c361cd719a535be596/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"a36c4f0a4137a0c56b13a3a0734531bb82f2f073","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a36c4f0a4137a0c56b13a3a0734531bb82f2f073","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a36c4f0a4137a0c56b13a3a0734531bb82f2f073"}]},{"sha":"13508ffaf17c00af1a6fe801cd43e5d8aa4decb1","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjEzNTA4ZmZhZjE3YzAwYWYxYTZmZTgwMWNkNDNlNWQ4YWE0ZGVjYjE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-23T19:25:43Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-23T19:25:43Z"},"message":"EA63D277-9D87-40C2-B53F-E9D005FC3A38","tree":{"sha":"7362a1fd3ec02e15ef83392b3c3eed548a0f551e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7362a1fd3ec02e15ef83392b3c3eed548a0f551e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/13508ffaf17c00af1a6fe801cd43e5d8aa4decb1","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/13508ffaf17c00af1a6fe801cd43e5d8aa4decb1","html_url":"https://github.com/ThiagoCodecov/example-python/commit/13508ffaf17c00af1a6fe801cd43e5d8aa4decb1","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/13508ffaf17c00af1a6fe801cd43e5d8aa4decb1/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"04ce0ba971c3151f915ea5c361cd719a535be596","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/04ce0ba971c3151f915ea5c361cd719a535be596","html_url":"https://github.com/ThiagoCodecov/example-python/commit/04ce0ba971c3151f915ea5c361cd719a535be596"}]},{"sha":"20a07fc2da0b80f3553fc1522643a21ed43e3b68","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjIwYTA3ZmMyZGEwYjgwZjM1NTNmYzE1MjI2NDNhMjFlZDQzZTNiNjg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-23T19:25:43Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-23T19:25:43Z"},"message":"D5AACB51-9166-457E-8B52-A18C7DF79B95","tree":{"sha":"4c4046075d9a279c6284db0ce12828d29752ff87","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4c4046075d9a279c6284db0ce12828d29752ff87"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/20a07fc2da0b80f3553fc1522643a21ed43e3b68","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/20a07fc2da0b80f3553fc1522643a21ed43e3b68","html_url":"https://github.com/ThiagoCodecov/example-python/commit/20a07fc2da0b80f3553fc1522643a21ed43e3b68","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/20a07fc2da0b80f3553fc1522643a21ed43e3b68/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"13508ffaf17c00af1a6fe801cd43e5d8aa4decb1","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/13508ffaf17c00af1a6fe801cd43e5d8aa4decb1","html_url":"https://github.com/ThiagoCodecov/example-python/commit/13508ffaf17c00af1a6fe801cd43e5d8aa4decb1"}]},{"sha":"23db2f02970a01f5448acc35e0ea8b001d138ef8","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjIzZGIyZjAyOTcwYTAxZjU0NDhhY2MzNWUwZWE4YjAwMWQxMzhlZjg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-30T21:26:15Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-30T21:26:15Z"},"message":"685B5EDF-EB21-49D4-AAF8-663F1F2612C9","tree":{"sha":"355ae851269e8725589d57436b49d1a5bcf115c3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/355ae851269e8725589d57436b49d1a5bcf115c3"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/23db2f02970a01f5448acc35e0ea8b001d138ef8","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/23db2f02970a01f5448acc35e0ea8b001d138ef8","html_url":"https://github.com/ThiagoCodecov/example-python/commit/23db2f02970a01f5448acc35e0ea8b001d138ef8","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/23db2f02970a01f5448acc35e0ea8b001d138ef8/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"20a07fc2da0b80f3553fc1522643a21ed43e3b68","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/20a07fc2da0b80f3553fc1522643a21ed43e3b68","html_url":"https://github.com/ThiagoCodecov/example-python/commit/20a07fc2da0b80f3553fc1522643a21ed43e3b68"}]},{"sha":"9ac06606c144c81650b9a480b64ec551e439f0bd","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjlhYzA2NjA2YzE0NGM4MTY1MGI5YTQ4MGI2NGVjNTUxZTQzOWYwYmQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-30T21:26:15Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-30T21:26:15Z"},"message":"E4F041FF-D124-4180-9AA4-FE126E4FEE2B","tree":{"sha":"92c14915bb0050c395808cb91d885d1bdc392141","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/92c14915bb0050c395808cb91d885d1bdc392141"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/9ac06606c144c81650b9a480b64ec551e439f0bd","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9ac06606c144c81650b9a480b64ec551e439f0bd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9ac06606c144c81650b9a480b64ec551e439f0bd","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9ac06606c144c81650b9a480b64ec551e439f0bd/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"23db2f02970a01f5448acc35e0ea8b001d138ef8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/23db2f02970a01f5448acc35e0ea8b001d138ef8","html_url":"https://github.com/ThiagoCodecov/example-python/commit/23db2f02970a01f5448acc35e0ea8b001d138ef8"}]},{"sha":"cde8db59f2effe26b92af8a659ff1c30468b3696","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmNkZThkYjU5ZjJlZmZlMjZiOTJhZjhhNjU5ZmYxYzMwNDY4YjM2OTY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-30T21:27:43Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-30T21:27:43Z"},"message":"2BB57C56-4554-402C-923C-5CFD5182A4D3","tree":{"sha":"29df4f19ec3283defd955af54ad1b0ba6491f6a0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/29df4f19ec3283defd955af54ad1b0ba6491f6a0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/cde8db59f2effe26b92af8a659ff1c30468b3696","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cde8db59f2effe26b92af8a659ff1c30468b3696","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cde8db59f2effe26b92af8a659ff1c30468b3696","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cde8db59f2effe26b92af8a659ff1c30468b3696/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"9ac06606c144c81650b9a480b64ec551e439f0bd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9ac06606c144c81650b9a480b64ec551e439f0bd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9ac06606c144c81650b9a480b64ec551e439f0bd"}]},{"sha":"cb9f3ff711ecb4cb7767ff520bc8d8e498490d7a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmNiOWYzZmY3MTFlY2I0Y2I3NzY3ZmY1MjBiYzhkOGU0OTg0OTBkN2E=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-30T21:27:43Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-30T21:27:43Z"},"message":"18729778-26D6-4979-8AA3-51CC87559CC6","tree":{"sha":"2c6ac0c9c3866dc32e3eaa7ad8bcb4a26d90f28f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2c6ac0c9c3866dc32e3eaa7ad8bcb4a26d90f28f"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/cb9f3ff711ecb4cb7767ff520bc8d8e498490d7a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cb9f3ff711ecb4cb7767ff520bc8d8e498490d7a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cb9f3ff711ecb4cb7767ff520bc8d8e498490d7a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cb9f3ff711ecb4cb7767ff520bc8d8e498490d7a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"cde8db59f2effe26b92af8a659ff1c30468b3696","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cde8db59f2effe26b92af8a659ff1c30468b3696","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cde8db59f2effe26b92af8a659ff1c30468b3696"}]},{"sha":"0c5f4f4149645fa2270ecbabec7091758a0c3919","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjBjNWY0ZjQxNDk2NDVmYTIyNzBlY2JhYmVjNzA5MTc1OGEwYzM5MTk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-04-06T20:27:40Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-04-06T20:27:40Z"},"message":"EFDAF948-2DC8-4023-B0F6-9E0B0431F1BC","tree":{"sha":"12b1c72a55ecb2b35efb2c42fb3917e730b80923","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/12b1c72a55ecb2b35efb2c42fb3917e730b80923"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/0c5f4f4149645fa2270ecbabec7091758a0c3919","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0c5f4f4149645fa2270ecbabec7091758a0c3919","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0c5f4f4149645fa2270ecbabec7091758a0c3919","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0c5f4f4149645fa2270ecbabec7091758a0c3919/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"cb9f3ff711ecb4cb7767ff520bc8d8e498490d7a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cb9f3ff711ecb4cb7767ff520bc8d8e498490d7a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cb9f3ff711ecb4cb7767ff520bc8d8e498490d7a"}]},{"sha":"2d6580c19ab627a54c9642d21203700ef7f26424","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJkNjU4MGMxOWFiNjI3YTU0Yzk2NDJkMjEyMDM3MDBlZjdmMjY0MjQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-04-06T20:27:40Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-04-06T20:27:40Z"},"message":"94927F15-E9A1-4235-81F5-0893924B618A","tree":{"sha":"b68bd222bd3fbcf4b1b99065db3a3b697204564b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b68bd222bd3fbcf4b1b99065db3a3b697204564b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2d6580c19ab627a54c9642d21203700ef7f26424","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d6580c19ab627a54c9642d21203700ef7f26424","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2d6580c19ab627a54c9642d21203700ef7f26424","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d6580c19ab627a54c9642d21203700ef7f26424/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"0c5f4f4149645fa2270ecbabec7091758a0c3919","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0c5f4f4149645fa2270ecbabec7091758a0c3919","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0c5f4f4149645fa2270ecbabec7091758a0c3919"}]},{"sha":"aca3a08bd7313dfdc7834cdd86bba9debcf19464","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmFjYTNhMDhiZDczMTNkZmRjNzgzNGNkZDg2YmJhOWRlYmNmMTk0NjQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-04-08T20:50:36Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-04-08T20:50:36Z"},"message":"5A779B8B-1311-486F-84E5-CA1FCB3CEC89","tree":{"sha":"9d92d6061a13804ef5c25869ac7cfae2446926c8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9d92d6061a13804ef5c25869ac7cfae2446926c8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/aca3a08bd7313dfdc7834cdd86bba9debcf19464","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/aca3a08bd7313dfdc7834cdd86bba9debcf19464","html_url":"https://github.com/ThiagoCodecov/example-python/commit/aca3a08bd7313dfdc7834cdd86bba9debcf19464","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/aca3a08bd7313dfdc7834cdd86bba9debcf19464/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2d6580c19ab627a54c9642d21203700ef7f26424","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d6580c19ab627a54c9642d21203700ef7f26424","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2d6580c19ab627a54c9642d21203700ef7f26424"}]},{"sha":"e999aac5b33acbca52601d2a655ab0ac46a1ffdf","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmU5OTlhYWM1YjMzYWNiY2E1MjYwMWQyYTY1NWFiMGFjNDZhMWZmZGY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-04-08T20:50:36Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-04-08T20:50:36Z"},"message":"71347B77-57D6-461C-BF40-69BB1369FEC0","tree":{"sha":"531f3876de58892b7c8f18f07b5fd7608f471a88","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/531f3876de58892b7c8f18f07b5fd7608f471a88"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e999aac5b33acbca52601d2a655ab0ac46a1ffdf","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e999aac5b33acbca52601d2a655ab0ac46a1ffdf","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e999aac5b33acbca52601d2a655ab0ac46a1ffdf","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e999aac5b33acbca52601d2a655ab0ac46a1ffdf/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"aca3a08bd7313dfdc7834cdd86bba9debcf19464","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/aca3a08bd7313dfdc7834cdd86bba9debcf19464","html_url":"https://github.com/ThiagoCodecov/example-python/commit/aca3a08bd7313dfdc7834cdd86bba9debcf19464"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:19:23 GMT + Etag: + - W/"5203500fd5e214d262b8c225aa15ca62" + Last-Modified: + - Thu, 09 Apr 2020 16:02:08 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EC4B:552C:1C5D5D:285D01:5E8F4B0B + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4970' + X-Ratelimit-Reset: + - '1586451770' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18/commits +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7 + response: + content: '{"sha":"081d91921f05a8a39d39aef667eddb88e96300c7","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA4MWQ5MTkyMWYwNWE4YTM5ZDM5YWVmNjY3ZWRkYjg4ZTk2MzAwYzc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"message":"182B7277-7D2C-420B-B005-92418CBD6F09","tree":{"sha":"e6af1eb1d589c63cf9cc5caf7b80024a30c2e892","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e6af1eb1d589c63cf9cc5caf7b80024a30c2e892"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/081d91921f05a8a39d39aef667eddb88e96300c7","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/081d91921f05a8a39d39aef667eddb88e96300c7","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f60187a642531c18d7af0b0f1d37294b809081fb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f60187a642531c18d7af0b0f1d37294b809081fb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f60187a642531c18d7af0b0f1d37294b809081fb"}],"stats":{"total":1,"additions":1,"deletions":0},"files":[{"sha":"f2a9f3e932bb13cb506de777ef2c0940feb9ee1c","filename":"README.md","status":"modified","additions":1,"deletions":0,"changes":1,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/081d91921f05a8a39d39aef667eddb88e96300c7/README.md","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/081d91921f05a8a39d39aef667eddb88e96300c7/README.md","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.md?ref=081d91921f05a8a39d39aef667eddb88e96300c7","patch":"@@ + -71,3 +71,4 @@ FCFA1979-26B8-4048-AF74-FE6DA53C96B6\n EC5B1F9F-2293-4947-8E01-62F6F828E139\n + A8FC2A63-27CD-4C2F-868C-28F728EB8EEF\n 8E855C85-E884-4187-A673-0D77F3F379A1\n+182B7277-7D2C-420B-B005-92418CBD6F09"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:19:23 GMT + Etag: + - W/"52734ef5046099acb1dd626aaeb9ad62" + Last-Modified: + - Wed, 04 Mar 2020 05:30:27 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EC4C:2F62:1B58A5:27648E:5E8F4B0B + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4969' + X-Ratelimit-Reset: + - '1586451769' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c","html_url":"https://github.com/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c","permalink_url":"https://github.com/ThiagoCodecov/example-python/compare/ThiagoCodecov:f089529...ThiagoCodecov:11daa27","diff_url":"https://github.com/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c.patch","base_commit":{"sha":"f0895290dc26668faeeb20ee5ccd4cc995925775","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmYwODk1MjkwZGMyNjY2OGZhZWViMjBlZTVjY2Q0Y2M5OTU5MjU3NzU=","commit":{"author":{"name":"Thiago","email":"44376991+ThiagoCodecov@users.noreply.github.com","date":"2020-03-24T22:01:33Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2020-03-24T22:01:33Z"},"message":"Merge + pull request #12 from ThiagoCodecov/thiago/f/cool-branch\n\nThiago/f/cool branch","tree":{"sha":"cda64d688c17ae3fcf03b22ee869238002e410f0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/cda64d688c17ae3fcf03b22ee869238002e410f0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f0895290dc26668faeeb20ee5ccd4cc995925775","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJeeoM+CRBK7hj4Ov3rIwAAdHIIAA7IxV7nFuDJIEFyQpE9XGAA\nMI6C/eXQp7/K6J2xwbFhUUDhkuXlbiiVvPw59STKg6qvlZ937FZBc4BfzM5OJIhY\n2fQcZnqJATD7cskHakK1+X6fmDKjN9u/KR7EmWfgjzst+cFYExptPJ7z3XktMKu9\nS7WaomPPF4lAAtDScCYb1IUILDKK5oiQMeUVJVzXyiuD9jPwjE/xo8a6Hpkvir0Q\npfjFWMuIgj9MnEv7+IZIOL//upOljWKpgoZfMBszinrF1okEtsFgeRpFXkmyVLGd\nmJa+ElmnO0TpIBtV8t/O6+eAFoc9pW3hFCuWNsIBQBhW2py5Q3gQ1hPyHdGzpUY=\n=P/w8\n-----END + PGP SIGNATURE-----\n","payload":"tree cda64d688c17ae3fcf03b22ee869238002e410f0\nparent + 081d91921f05a8a39d39aef667eddb88e96300c7\nparent 89f3d20f2be3fb2d098e544814f8ce3636dc78c4\nauthor + Thiago <44376991+ThiagoCodecov@users.noreply.github.com> 1585087293 -0300\ncommitter + GitHub 1585087293 -0300\n\nMerge pull request #12 from + ThiagoCodecov/thiago/f/cool-branch\n\nThiago/f/cool branch"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f0895290dc26668faeeb20ee5ccd4cc995925775","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars3.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"081d91921f05a8a39d39aef667eddb88e96300c7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/081d91921f05a8a39d39aef667eddb88e96300c7"},{"sha":"89f3d20f2be3fb2d098e544814f8ce3636dc78c4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/89f3d20f2be3fb2d098e544814f8ce3636dc78c4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/89f3d20f2be3fb2d098e544814f8ce3636dc78c4"}]},"merge_base_commit":{"sha":"081d91921f05a8a39d39aef667eddb88e96300c7","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA4MWQ5MTkyMWYwNWE4YTM5ZDM5YWVmNjY3ZWRkYjg4ZTk2MzAwYzc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"message":"182B7277-7D2C-420B-B005-92418CBD6F09","tree":{"sha":"e6af1eb1d589c63cf9cc5caf7b80024a30c2e892","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e6af1eb1d589c63cf9cc5caf7b80024a30c2e892"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/081d91921f05a8a39d39aef667eddb88e96300c7","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/081d91921f05a8a39d39aef667eddb88e96300c7","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f60187a642531c18d7af0b0f1d37294b809081fb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f60187a642531c18d7af0b0f1d37294b809081fb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f60187a642531c18d7af0b0f1d37294b809081fb"}]},"status":"diverged","ahead_by":1,"behind_by":4,"total_commits":1,"commits":[{"sha":"11daa27b1b74fd181836a64106f936a16404089c","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjExZGFhMjdiMWI3NGZkMTgxODM2YTY0MTA2ZjkzNmExNjQwNDA4OWM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:31:33Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:31:33Z"},"message":"F8926640-91E9-4A1E-B209-0101666AB0BB","tree":{"sha":"6be9ab3da2917081449644850f6ecf058d0feded","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6be9ab3da2917081449644850f6ecf058d0feded"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/11daa27b1b74fd181836a64106f936a16404089c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/11daa27b1b74fd181836a64106f936a16404089c","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"081d91921f05a8a39d39aef667eddb88e96300c7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/081d91921f05a8a39d39aef667eddb88e96300c7"}]}],"files":[{"sha":"c2814252ce16c397051c3be74c373b2ed1573e26","filename":"README.md","status":"modified","additions":1,"deletions":0,"changes":1,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/11daa27b1b74fd181836a64106f936a16404089c/README.md","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/11daa27b1b74fd181836a64106f936a16404089c/README.md","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.md?ref=11daa27b1b74fd181836a64106f936a16404089c","patch":"@@ + -72,3 +72,4 @@ EC5B1F9F-2293-4947-8E01-62F6F828E139\n A8FC2A63-27CD-4C2F-868C-28F728EB8EEF\n + 8E855C85-E884-4187-A673-0D77F3F379A1\n 182B7277-7D2C-420B-B005-92418CBD6F09\n+F8926640-91E9-4A1E-B209-0101666AB0BB"},{"sha":"32f5e02a889149106e00e801896b26e561aee9ba","filename":"flagone.coverage.xml","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/11daa27b1b74fd181836a64106f936a16404089c/flagone.coverage.xml","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/11daa27b1b74fd181836a64106f936a16404089c/flagone.coverage.xml","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/flagone.coverage.xml?ref=11daa27b1b74fd181836a64106f936a16404089c","patch":"@@ + -1,5 +1,5 @@\n \n-\n+\n \t\n \t\n \t"},{"sha":"fe0ba5359a3cc58e01c4ead0174e1de49370dd10","filename":"flagtwo.coverage.xml","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/11daa27b1b74fd181836a64106f936a16404089c/flagtwo.coverage.xml","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/11daa27b1b74fd181836a64106f936a16404089c/flagtwo.coverage.xml","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/flagtwo.coverage.xml?ref=11daa27b1b74fd181836a64106f936a16404089c","patch":"@@ + -1,5 +1,5 @@\n \n-\n+\n \t\n \t\n \t"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:19:24 GMT + Etag: + - W/"289ae1132650fcac2003b69af072aa86" + Last-Modified: + - Tue, 24 Mar 2020 22:01:33 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EC4F:5531:1EF723:2C9A50:5E8F4B0B + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4968' + X-Ratelimit-Reset: + - '1586451770' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c","html_url":"https://github.com/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c","permalink_url":"https://github.com/ThiagoCodecov/example-python/compare/ThiagoCodecov:f089529...ThiagoCodecov:11daa27","diff_url":"https://github.com/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c.patch","base_commit":{"sha":"f0895290dc26668faeeb20ee5ccd4cc995925775","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmYwODk1MjkwZGMyNjY2OGZhZWViMjBlZTVjY2Q0Y2M5OTU5MjU3NzU=","commit":{"author":{"name":"Thiago","email":"44376991+ThiagoCodecov@users.noreply.github.com","date":"2020-03-24T22:01:33Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2020-03-24T22:01:33Z"},"message":"Merge + pull request #12 from ThiagoCodecov/thiago/f/cool-branch\n\nThiago/f/cool branch","tree":{"sha":"cda64d688c17ae3fcf03b22ee869238002e410f0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/cda64d688c17ae3fcf03b22ee869238002e410f0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f0895290dc26668faeeb20ee5ccd4cc995925775","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJeeoM+CRBK7hj4Ov3rIwAAdHIIAA7IxV7nFuDJIEFyQpE9XGAA\nMI6C/eXQp7/K6J2xwbFhUUDhkuXlbiiVvPw59STKg6qvlZ937FZBc4BfzM5OJIhY\n2fQcZnqJATD7cskHakK1+X6fmDKjN9u/KR7EmWfgjzst+cFYExptPJ7z3XktMKu9\nS7WaomPPF4lAAtDScCYb1IUILDKK5oiQMeUVJVzXyiuD9jPwjE/xo8a6Hpkvir0Q\npfjFWMuIgj9MnEv7+IZIOL//upOljWKpgoZfMBszinrF1okEtsFgeRpFXkmyVLGd\nmJa+ElmnO0TpIBtV8t/O6+eAFoc9pW3hFCuWNsIBQBhW2py5Q3gQ1hPyHdGzpUY=\n=P/w8\n-----END + PGP SIGNATURE-----\n","payload":"tree cda64d688c17ae3fcf03b22ee869238002e410f0\nparent + 081d91921f05a8a39d39aef667eddb88e96300c7\nparent 89f3d20f2be3fb2d098e544814f8ce3636dc78c4\nauthor + Thiago <44376991+ThiagoCodecov@users.noreply.github.com> 1585087293 -0300\ncommitter + GitHub 1585087293 -0300\n\nMerge pull request #12 from + ThiagoCodecov/thiago/f/cool-branch\n\nThiago/f/cool branch"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f0895290dc26668faeeb20ee5ccd4cc995925775","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars3.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"081d91921f05a8a39d39aef667eddb88e96300c7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/081d91921f05a8a39d39aef667eddb88e96300c7"},{"sha":"89f3d20f2be3fb2d098e544814f8ce3636dc78c4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/89f3d20f2be3fb2d098e544814f8ce3636dc78c4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/89f3d20f2be3fb2d098e544814f8ce3636dc78c4"}]},"merge_base_commit":{"sha":"081d91921f05a8a39d39aef667eddb88e96300c7","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA4MWQ5MTkyMWYwNWE4YTM5ZDM5YWVmNjY3ZWRkYjg4ZTk2MzAwYzc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"message":"182B7277-7D2C-420B-B005-92418CBD6F09","tree":{"sha":"e6af1eb1d589c63cf9cc5caf7b80024a30c2e892","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e6af1eb1d589c63cf9cc5caf7b80024a30c2e892"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/081d91921f05a8a39d39aef667eddb88e96300c7","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/081d91921f05a8a39d39aef667eddb88e96300c7","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f60187a642531c18d7af0b0f1d37294b809081fb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f60187a642531c18d7af0b0f1d37294b809081fb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f60187a642531c18d7af0b0f1d37294b809081fb"}]},"status":"diverged","ahead_by":1,"behind_by":4,"total_commits":1,"commits":[{"sha":"11daa27b1b74fd181836a64106f936a16404089c","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjExZGFhMjdiMWI3NGZkMTgxODM2YTY0MTA2ZjkzNmExNjQwNDA4OWM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:31:33Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:31:33Z"},"message":"F8926640-91E9-4A1E-B209-0101666AB0BB","tree":{"sha":"6be9ab3da2917081449644850f6ecf058d0feded","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6be9ab3da2917081449644850f6ecf058d0feded"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/11daa27b1b74fd181836a64106f936a16404089c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/11daa27b1b74fd181836a64106f936a16404089c","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"081d91921f05a8a39d39aef667eddb88e96300c7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/081d91921f05a8a39d39aef667eddb88e96300c7"}]}],"files":[{"sha":"c2814252ce16c397051c3be74c373b2ed1573e26","filename":"README.md","status":"modified","additions":1,"deletions":0,"changes":1,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/11daa27b1b74fd181836a64106f936a16404089c/README.md","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/11daa27b1b74fd181836a64106f936a16404089c/README.md","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.md?ref=11daa27b1b74fd181836a64106f936a16404089c","patch":"@@ + -72,3 +72,4 @@ EC5B1F9F-2293-4947-8E01-62F6F828E139\n A8FC2A63-27CD-4C2F-868C-28F728EB8EEF\n + 8E855C85-E884-4187-A673-0D77F3F379A1\n 182B7277-7D2C-420B-B005-92418CBD6F09\n+F8926640-91E9-4A1E-B209-0101666AB0BB"},{"sha":"32f5e02a889149106e00e801896b26e561aee9ba","filename":"flagone.coverage.xml","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/11daa27b1b74fd181836a64106f936a16404089c/flagone.coverage.xml","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/11daa27b1b74fd181836a64106f936a16404089c/flagone.coverage.xml","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/flagone.coverage.xml?ref=11daa27b1b74fd181836a64106f936a16404089c","patch":"@@ + -1,5 +1,5 @@\n \n-\n+\n \t\n \t\n \t"},{"sha":"fe0ba5359a3cc58e01c4ead0174e1de49370dd10","filename":"flagtwo.coverage.xml","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/11daa27b1b74fd181836a64106f936a16404089c/flagtwo.coverage.xml","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/11daa27b1b74fd181836a64106f936a16404089c/flagtwo.coverage.xml","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/flagtwo.coverage.xml?ref=11daa27b1b74fd181836a64106f936a16404089c","patch":"@@ + -1,5 +1,5 @@\n \n-\n+\n \t\n \t\n \t"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:19:24 GMT + Etag: + - W/"289ae1132650fcac2003b69af072aa86" + Last-Modified: + - Tue, 24 Mar 2020 22:01:33 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EC4E:552B:5A0C0:7E75D:5E8F4B0B + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4967' + X-Ratelimit-Reset: + - '1586451770' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c/status?page=1&per_page=100 + response: + content: '{"state":"pending","statuses":[],"sha":"11daa27b1b74fd181836a64106f936a16404089c","total_count":0,"repository":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments"},"commit_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:19:24 GMT + Etag: + - W/"3836d1bda5994807246356db5ad950cc" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - repo, repo:status + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EC4D:552B:5A0C0:7E75E:5E8F4B0B + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4966' + X-Ratelimit-Reset: + - '1586451770' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c/status?page=1&per_page=100 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c/status?page=1&per_page=100 + response: + content: '{"state":"pending","statuses":[],"sha":"11daa27b1b74fd181836a64106f936a16404089c","total_count":0,"repository":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments"},"commit_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:19:24 GMT + Etag: + - W/"3836d1bda5994807246356db5ad950cc" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - repo, repo:status + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EC50:2F61:1F5211:2C856B:5E8F4B0C + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4965' + X-Ratelimit-Reset: + - '1586451770' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c/status?page=1&per_page=100 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c/status?page=1&per_page=100 + response: + content: '{"state":"pending","statuses":[],"sha":"11daa27b1b74fd181836a64106f936a16404089c","total_count":0,"repository":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments"},"commit_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:19:24 GMT + Etag: + - W/"3836d1bda5994807246356db5ad950cc" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - repo, repo:status + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EC51:6512:1E94FF:2BDA52:5E8F4B0C + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4964' + X-Ratelimit-Reset: + - '1586451770' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c/status?page=1&per_page=100 +- request: + body: '{"state": "success", "target_url": "https://myexamplewebsite.io/gh/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c", + "context": "codecov/project", "description": "85.00% (+0.00%) compared to f089529"}' + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/11daa27b1b74fd181836a64106f936a16404089c + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/11daa27b1b74fd181836a64106f936a16404089c","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9333363767,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzMzMzYzNzY3","state":"success","description":"85.00% + (+0.00%) compared to f089529","target_url":"https://myexamplewebsite.io/gh/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c","context":"codecov/project","created_at":"2020-04-09T16:19:24Z","updated_at":"2020-04-09T16:19:24Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Length: + - '1567' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:19:24 GMT + Etag: + - '"810fabf231fed01d2bec5f794c58dba4"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/11daa27b1b74fd181836a64106f936a16404089c + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EC52:6512:1E9503:2BDA55:5E8F4B0C + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4963' + X-Ratelimit-Reset: + - '1586451769' + X-Xss-Protection: + - 1; mode=block + status: + code: 201 + message: Created + status_code: 201 + url: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/11daa27b1b74fd181836a64106f936a16404089c +- request: + body: '{"state": "success", "target_url": "https://myexamplewebsite.io/gh/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c", + "context": "codecov/changes", "description": "No unexpected coverage changes + found"}' + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/11daa27b1b74fd181836a64106f936a16404089c + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/11daa27b1b74fd181836a64106f936a16404089c","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9333363778,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzMzMzYzNzc4","state":"success","description":"No + unexpected coverage changes found","target_url":"https://myexamplewebsite.io/gh/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c","context":"codecov/changes","created_at":"2020-04-09T16:19:24Z","updated_at":"2020-04-09T16:19:24Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Length: + - '1568' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:19:24 GMT + Etag: + - '"bae05226b4cb11bd85e9fc91e02a496d"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/11daa27b1b74fd181836a64106f936a16404089c + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EC54:6512:1E9503:2BDA59:5E8F4B0C + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4962' + X-Ratelimit-Reset: + - '1586451769' + X-Xss-Protection: + - 1; mode=block + status: + code: 201 + message: Created + status_code: 201 + url: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/11daa27b1b74fd181836a64106f936a16404089c +- request: + body: '{"state": "success", "target_url": "https://myexamplewebsite.io/gh/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c", + "context": "codecov/patch", "description": "Coverage not affected when comparing + f089529...11daa27"}' + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/11daa27b1b74fd181836a64106f936a16404089c + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/11daa27b1b74fd181836a64106f936a16404089c","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9333363801,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzMzMzYzODAx","state":"success","description":"Coverage + not affected when comparing f089529...11daa27","target_url":"https://myexamplewebsite.io/gh/ThiagoCodecov/example-python/compare/f0895290dc26668faeeb20ee5ccd4cc995925775...11daa27b1b74fd181836a64106f936a16404089c","context":"codecov/patch","created_at":"2020-04-09T16:19:24Z","updated_at":"2020-04-09T16:19:24Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Length: + - '1584' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:19:24 GMT + Etag: + - '"52a8b12a2b55c4ebacaf752fdbed891d"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/11daa27b1b74fd181836a64106f936a16404089c + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EC53:6512:1E9503:2BDA58:5E8F4B0C + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4961' + X-Ratelimit-Reset: + - '1586451769' + X-Xss-Protection: + - 1; mode=block + status: + code: 201 + message: Created + status_code: 201 + url: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/11daa27b1b74fd181836a64106f936a16404089c +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/11daa27b1b74fd181836a64106f936a16404089c/pulls + response: + content: '[]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '2' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 17 Aug 2022 01:19:13 GMT + ETag: + - '"9df3c60ee9918425c5450f4f3dc01f2dfd32b2a251792108bb6907a0d6b21a92"' + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C60B:0EF5:79E56B:8213D3:62FC4210 + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4982' + X-RateLimit-Reset: + - '1660701033' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '18' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-08-24 01:18:21 UTC + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_simple_call_status_and_notifiers.yaml b/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_simple_call_status_and_notifiers.yaml new file mode 100644 index 0000000000..7c3730caed --- /dev/null +++ b/apps/worker/tasks/tests/integration/cassetes/test_notify_task/TestNotifyTask/test_simple_call_status_and_notifiers.yaml @@ -0,0 +1,1837 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/test-acc9/test_example/branches?per_page=100&page=1 + response: + content: '[{"name":"featureA","commit":{"sha":"a2d3e3c30547a000f026daa47610bb3f7b63aece","url":"https://api.github.com/repos/test-acc9/test_example/commits/a2d3e3c30547a000f026daa47610bb3f7b63aece"},"protected":false},{"name":"main","commit":{"sha":"ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","url":"https://api.github.com/repos/test-acc9/test_example/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619"},"protected":false}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 29 Jun 2023 16:40:54 GMT + ETag: + - W/"e8806c7436fe3bbbbc43660e85532d37d5c6d546af9bd7752e5d0bdffde7c987" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - EDA3:5DA6:61F153:C6B35C:649DB416 + X-OAuth-Scopes: + - public_repo, repo:status, repo_deployment + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4993' + X-RateLimit-Reset: + - '1688060452' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '7' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-07-06 14:34:07 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/test-acc9/test_example/compare/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619...ef6edf5ae6643d53a7971fb8823d3f7b2ac65619 + response: + content: '{"url":"https://api.github.com/repos/test-acc9/test_example/compare/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619...ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","html_url":"https://github.com/test-acc9/test_example/compare/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619...ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","permalink_url":"https://github.com/test-acc9/test_example/compare/test-acc9:ef6edf5...test-acc9:ef6edf5","diff_url":"https://github.com/test-acc9/test_example/compare/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619...ef6edf5ae6643d53a7971fb8823d3f7b2ac65619.diff","patch_url":"https://github.com/test-acc9/test_example/compare/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619...ef6edf5ae6643d53a7971fb8823d3f7b2ac65619.patch","base_commit":{"sha":"ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","node_id":"C_kwDOHP_eEdoAKGVmNmVkZjVhZTY2NDNkNTNhNzk3MWZiODgyM2QzZjdiMmFjNjU2MTk","commit":{"author":{"name":"test-acc9","email":"104562106+test-acc9@users.noreply.github.com","date":"2022-04-28T09:35:11Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-04-28T09:35:11Z"},"message":"Create + read.me","tree":{"sha":"5e836bead2ad06cc76854f5f8d3f4ab983ec60a1","url":"https://api.github.com/repos/test-acc9/test_example/git/trees/5e836bead2ad06cc76854f5f8d3f4ab983ec60a1"},"url":"https://api.github.com/repos/test-acc9/test_example/git/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJial/QCRBK7hj4Ov3rIwAA29QIAAdOIKu3w9SwC5g7UPjXj8xi\nYOt30BqPzSY/ktq7scRf/uOiO97uAC0XANXwmfkWCgI6YZSijysJdHEALFg7NwXp\n/aF8u9fFk+nHbhfTsCtM0jgabWRt0FSMsO0VreWn5ExiqWqPnDMTydbyCEzkHwsz\nMvVUGj8uqP4P7NM6DvCsOSM0m9Xg6IEFNqpsMd8yfvP5fF57YXnpixhISAUkGjuq\nJx/ouGxFircoRLTlJi5ZfmFNGFsXfoqvCyBMZ62lkg81lUYXEGKhDvFWnz6OR9sZ\n+4SxRgaFLz05xUKl3EEAhnWHjzrBkQP7gJBIz2MJ1VvFmH3h/hH86NtaN+TMCKU=\n=k3dl\n-----END + PGP SIGNATURE-----\n","payload":"tree 5e836bead2ad06cc76854f5f8d3f4ab983ec60a1\nauthor + test-acc9 <104562106+test-acc9@users.noreply.github.com> 1651138511 +0300\ncommitter + GitHub 1651138511 +0300\n\nCreate read.me"}},"url":"https://api.github.com/repos/test-acc9/test_example/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","html_url":"https://github.com/test-acc9/test_example/commit/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","comments_url":"https://api.github.com/repos/test-acc9/test_example/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619/comments","author":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[]},"merge_base_commit":{"sha":"ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","node_id":"C_kwDOHP_eEdoAKGVmNmVkZjVhZTY2NDNkNTNhNzk3MWZiODgyM2QzZjdiMmFjNjU2MTk","commit":{"author":{"name":"test-acc9","email":"104562106+test-acc9@users.noreply.github.com","date":"2022-04-28T09:35:11Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-04-28T09:35:11Z"},"message":"Create + read.me","tree":{"sha":"5e836bead2ad06cc76854f5f8d3f4ab983ec60a1","url":"https://api.github.com/repos/test-acc9/test_example/git/trees/5e836bead2ad06cc76854f5f8d3f4ab983ec60a1"},"url":"https://api.github.com/repos/test-acc9/test_example/git/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJial/QCRBK7hj4Ov3rIwAA29QIAAdOIKu3w9SwC5g7UPjXj8xi\nYOt30BqPzSY/ktq7scRf/uOiO97uAC0XANXwmfkWCgI6YZSijysJdHEALFg7NwXp\n/aF8u9fFk+nHbhfTsCtM0jgabWRt0FSMsO0VreWn5ExiqWqPnDMTydbyCEzkHwsz\nMvVUGj8uqP4P7NM6DvCsOSM0m9Xg6IEFNqpsMd8yfvP5fF57YXnpixhISAUkGjuq\nJx/ouGxFircoRLTlJi5ZfmFNGFsXfoqvCyBMZ62lkg81lUYXEGKhDvFWnz6OR9sZ\n+4SxRgaFLz05xUKl3EEAhnWHjzrBkQP7gJBIz2MJ1VvFmH3h/hH86NtaN+TMCKU=\n=k3dl\n-----END + PGP SIGNATURE-----\n","payload":"tree 5e836bead2ad06cc76854f5f8d3f4ab983ec60a1\nauthor + test-acc9 <104562106+test-acc9@users.noreply.github.com> 1651138511 +0300\ncommitter + GitHub 1651138511 +0300\n\nCreate read.me"}},"url":"https://api.github.com/repos/test-acc9/test_example/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","html_url":"https://github.com/test-acc9/test_example/commit/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","comments_url":"https://api.github.com/repos/test-acc9/test_example/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619/comments","author":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[]},"status":"identical","ahead_by":0,"behind_by":0,"total_commits":0,"commits":[],"files":[]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 29 Jun 2023 16:40:55 GMT + ETag: + - W/"20de48b515b79760b935addb0188e5011b2bcf19e20a3a63f88d7de98f2c19e9" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - EDA4:37DA:5DA894:BE2F4D:649DB416 + X-OAuth-Scopes: + - public_repo, repo:status, repo_deployment + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4992' + X-RateLimit-Reset: + - '1688060452' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '8' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-07-06 14:34:07 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c/status?page=1&per_page=100 + response: + content: '{"state":"success","statuses":[{"url":"https://api.github.com/repos/test-acc9/test_example/statuses/610ada9fa2bbc49f1a08917da3f73bef2d03709c","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","id":18593754782,"node_id":"SC_kwDOHP_eEc8AAAAEVEYung","state":"success","description":"85.00% + (+0.00%) compared to ef6edf5","target_url":"https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1","context":"codecov/project","created_at":"2022-08-02T17:58:52Z","updated_at":"2022-08-02T17:58:52Z"},{"url":"https://api.github.com/repos/test-acc9/test_example/statuses/610ada9fa2bbc49f1a08917da3f73bef2d03709c","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","id":18593755520,"node_id":"SC_kwDOHP_eEc8AAAAEVEYxgA","state":"success","description":"No + unexpected coverage changes found","target_url":"https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1","context":"codecov/changes","created_at":"2022-08-02T17:58:55Z","updated_at":"2022-08-02T17:58:55Z"},{"url":"https://api.github.com/repos/test-acc9/test_example/statuses/610ada9fa2bbc49f1a08917da3f73bef2d03709c","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","id":18593755542,"node_id":"SC_kwDOHP_eEc8AAAAEVEYxlg","state":"success","description":"Coverage + not affected when comparing ef6edf5...610ada9","target_url":"https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1","context":"codecov/patch","created_at":"2022-08-02T17:58:55Z","updated_at":"2022-08-02T17:58:55Z"}],"sha":"610ada9fa2bbc49f1a08917da3f73bef2d03709c","total_count":3,"repository":{"id":486530577,"node_id":"R_kgDOHP_eEQ","name":"test_example","full_name":"test-acc9/test_example","private":false,"owner":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"html_url":"https://github.com/test-acc9/test_example","description":null,"fork":false,"url":"https://api.github.com/repos/test-acc9/test_example","forks_url":"https://api.github.com/repos/test-acc9/test_example/forks","keys_url":"https://api.github.com/repos/test-acc9/test_example/keys{/key_id}","collaborators_url":"https://api.github.com/repos/test-acc9/test_example/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/test-acc9/test_example/teams","hooks_url":"https://api.github.com/repos/test-acc9/test_example/hooks","issue_events_url":"https://api.github.com/repos/test-acc9/test_example/issues/events{/number}","events_url":"https://api.github.com/repos/test-acc9/test_example/events","assignees_url":"https://api.github.com/repos/test-acc9/test_example/assignees{/user}","branches_url":"https://api.github.com/repos/test-acc9/test_example/branches{/branch}","tags_url":"https://api.github.com/repos/test-acc9/test_example/tags","blobs_url":"https://api.github.com/repos/test-acc9/test_example/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/test-acc9/test_example/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/test-acc9/test_example/git/refs{/sha}","trees_url":"https://api.github.com/repos/test-acc9/test_example/git/trees{/sha}","statuses_url":"https://api.github.com/repos/test-acc9/test_example/statuses/{sha}","languages_url":"https://api.github.com/repos/test-acc9/test_example/languages","stargazers_url":"https://api.github.com/repos/test-acc9/test_example/stargazers","contributors_url":"https://api.github.com/repos/test-acc9/test_example/contributors","subscribers_url":"https://api.github.com/repos/test-acc9/test_example/subscribers","subscription_url":"https://api.github.com/repos/test-acc9/test_example/subscription","commits_url":"https://api.github.com/repos/test-acc9/test_example/commits{/sha}","git_commits_url":"https://api.github.com/repos/test-acc9/test_example/git/commits{/sha}","comments_url":"https://api.github.com/repos/test-acc9/test_example/comments{/number}","issue_comment_url":"https://api.github.com/repos/test-acc9/test_example/issues/comments{/number}","contents_url":"https://api.github.com/repos/test-acc9/test_example/contents/{+path}","compare_url":"https://api.github.com/repos/test-acc9/test_example/compare/{base}...{head}","merges_url":"https://api.github.com/repos/test-acc9/test_example/merges","archive_url":"https://api.github.com/repos/test-acc9/test_example/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/test-acc9/test_example/downloads","issues_url":"https://api.github.com/repos/test-acc9/test_example/issues{/number}","pulls_url":"https://api.github.com/repos/test-acc9/test_example/pulls{/number}","milestones_url":"https://api.github.com/repos/test-acc9/test_example/milestones{/number}","notifications_url":"https://api.github.com/repos/test-acc9/test_example/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/test-acc9/test_example/labels{/name}","releases_url":"https://api.github.com/repos/test-acc9/test_example/releases{/id}","deployments_url":"https://api.github.com/repos/test-acc9/test_example/deployments"},"commit_url":"https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c","url":"https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 02 Aug 2022 18:15:03 GMT + ETag: + - W/"d2b162701dbf6a7a8eff7d2ccbe045ed2e3639a937946c5029cef4c0b8a98243" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo, repo:status + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - EF69:1DBF:29A34E:2BA21F:62E969A6 + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4987' + X-RateLimit-Reset: + - '1659466112' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '13' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-09-01 17:26:32 UTC + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/search/issues?q=610ada9fa2bbc49f1a08917da3f73bef2d03709c+repo%3Atest-acc9%2Ftest_example+type%3Apr+state%3Aopen + response: + content: '{"total_count":1,"incomplete_results":false,"items":[{"url":"https://api.github.com/repos/test-acc9/test_example/issues/1","repository_url":"https://api.github.com/repos/test-acc9/test_example","labels_url":"https://api.github.com/repos/test-acc9/test_example/issues/1/labels{/name}","comments_url":"https://api.github.com/repos/test-acc9/test_example/issues/1/comments","events_url":"https://api.github.com/repos/test-acc9/test_example/issues/1/events","html_url":"https://github.com/test-acc9/test_example/pull/1","id":1218464748,"node_id":"PR_kwDOHP_eEc427r9u","number":1,"title":"Create + random-commit.me","user":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":3,"created_at":"2022-04-28T09:37:10Z","updated_at":"2022-08-02T17:58:55Z","closed_at":null,"author_association":"OWNER","active_lock_reason":null,"draft":false,"pull_request":{"url":"https://api.github.com/repos/test-acc9/test_example/pulls/1","html_url":"https://github.com/test-acc9/test_example/pull/1","diff_url":"https://github.com/test-acc9/test_example/pull/1.diff","patch_url":"https://github.com/test-acc9/test_example/pull/1.patch","merged_at":null},"body":null,"reactions":{"url":"https://api.github.com/repos/test-acc9/test_example/issues/1/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"timeline_url":"https://api.github.com/repos/test-acc9/test_example/issues/1/timeline","performed_via_github_app":null,"state_reason":null,"score":1.0}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - no-cache + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 02 Aug 2022 18:15:04 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - EF6A:0950:943B0:D6558:62E969A7 + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '30' + X-RateLimit-Remaining: + - '29' + X-RateLimit-Reset: + - '1659464164' + X-RateLimit-Resource: + - search + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-09-01 17:26:32 UTC + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/test-acc9/test_example/pulls/1 + response: + content: '{"url":"https://api.github.com/repos/test-acc9/test_example/pulls/1","id":921616238,"node_id":"PR_kwDOHP_eEc427r9u","html_url":"https://github.com/test-acc9/test_example/pull/1","diff_url":"https://github.com/test-acc9/test_example/pull/1.diff","patch_url":"https://github.com/test-acc9/test_example/pull/1.patch","issue_url":"https://api.github.com/repos/test-acc9/test_example/issues/1","number":1,"state":"open","locked":false,"title":"Create + random-commit.me","user":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"body":null,"created_at":"2022-04-28T09:37:10Z","updated_at":"2022-08-02T17:58:55Z","closed_at":null,"merged_at":null,"merge_commit_sha":"188bca08574e22fdc3cbdbde0bb98bfcf7e64425","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/test-acc9/test_example/pulls/1/commits","review_comments_url":"https://api.github.com/repos/test-acc9/test_example/pulls/1/comments","review_comment_url":"https://api.github.com/repos/test-acc9/test_example/pulls/comments{/number}","comments_url":"https://api.github.com/repos/test-acc9/test_example/issues/1/comments","statuses_url":"https://api.github.com/repos/test-acc9/test_example/statuses/a2d3e3c30547a000f026daa47610bb3f7b63aece","head":{"label":"test-acc9:featureA","ref":"featureA","sha":"a2d3e3c30547a000f026daa47610bb3f7b63aece","user":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"repo":{"id":486530577,"node_id":"R_kgDOHP_eEQ","name":"test_example","full_name":"test-acc9/test_example","private":false,"owner":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"html_url":"https://github.com/test-acc9/test_example","description":null,"fork":false,"url":"https://api.github.com/repos/test-acc9/test_example","forks_url":"https://api.github.com/repos/test-acc9/test_example/forks","keys_url":"https://api.github.com/repos/test-acc9/test_example/keys{/key_id}","collaborators_url":"https://api.github.com/repos/test-acc9/test_example/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/test-acc9/test_example/teams","hooks_url":"https://api.github.com/repos/test-acc9/test_example/hooks","issue_events_url":"https://api.github.com/repos/test-acc9/test_example/issues/events{/number}","events_url":"https://api.github.com/repos/test-acc9/test_example/events","assignees_url":"https://api.github.com/repos/test-acc9/test_example/assignees{/user}","branches_url":"https://api.github.com/repos/test-acc9/test_example/branches{/branch}","tags_url":"https://api.github.com/repos/test-acc9/test_example/tags","blobs_url":"https://api.github.com/repos/test-acc9/test_example/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/test-acc9/test_example/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/test-acc9/test_example/git/refs{/sha}","trees_url":"https://api.github.com/repos/test-acc9/test_example/git/trees{/sha}","statuses_url":"https://api.github.com/repos/test-acc9/test_example/statuses/{sha}","languages_url":"https://api.github.com/repos/test-acc9/test_example/languages","stargazers_url":"https://api.github.com/repos/test-acc9/test_example/stargazers","contributors_url":"https://api.github.com/repos/test-acc9/test_example/contributors","subscribers_url":"https://api.github.com/repos/test-acc9/test_example/subscribers","subscription_url":"https://api.github.com/repos/test-acc9/test_example/subscription","commits_url":"https://api.github.com/repos/test-acc9/test_example/commits{/sha}","git_commits_url":"https://api.github.com/repos/test-acc9/test_example/git/commits{/sha}","comments_url":"https://api.github.com/repos/test-acc9/test_example/comments{/number}","issue_comment_url":"https://api.github.com/repos/test-acc9/test_example/issues/comments{/number}","contents_url":"https://api.github.com/repos/test-acc9/test_example/contents/{+path}","compare_url":"https://api.github.com/repos/test-acc9/test_example/compare/{base}...{head}","merges_url":"https://api.github.com/repos/test-acc9/test_example/merges","archive_url":"https://api.github.com/repos/test-acc9/test_example/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/test-acc9/test_example/downloads","issues_url":"https://api.github.com/repos/test-acc9/test_example/issues{/number}","pulls_url":"https://api.github.com/repos/test-acc9/test_example/pulls{/number}","milestones_url":"https://api.github.com/repos/test-acc9/test_example/milestones{/number}","notifications_url":"https://api.github.com/repos/test-acc9/test_example/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/test-acc9/test_example/labels{/name}","releases_url":"https://api.github.com/repos/test-acc9/test_example/releases{/id}","deployments_url":"https://api.github.com/repos/test-acc9/test_example/deployments","created_at":"2022-04-28T09:34:42Z","updated_at":"2022-07-27T08:02:03Z","pushed_at":"2022-07-27T06:13:31Z","git_url":"git://github.com/test-acc9/test_example.git","ssh_url":"git@github.com:test-acc9/test_example.git","clone_url":"https://github.com/test-acc9/test_example.git","svn_url":"https://github.com/test-acc9/test_example","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main"}},"base":{"label":"test-acc9:main","ref":"main","sha":"ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","user":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"repo":{"id":486530577,"node_id":"R_kgDOHP_eEQ","name":"test_example","full_name":"test-acc9/test_example","private":false,"owner":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"html_url":"https://github.com/test-acc9/test_example","description":null,"fork":false,"url":"https://api.github.com/repos/test-acc9/test_example","forks_url":"https://api.github.com/repos/test-acc9/test_example/forks","keys_url":"https://api.github.com/repos/test-acc9/test_example/keys{/key_id}","collaborators_url":"https://api.github.com/repos/test-acc9/test_example/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/test-acc9/test_example/teams","hooks_url":"https://api.github.com/repos/test-acc9/test_example/hooks","issue_events_url":"https://api.github.com/repos/test-acc9/test_example/issues/events{/number}","events_url":"https://api.github.com/repos/test-acc9/test_example/events","assignees_url":"https://api.github.com/repos/test-acc9/test_example/assignees{/user}","branches_url":"https://api.github.com/repos/test-acc9/test_example/branches{/branch}","tags_url":"https://api.github.com/repos/test-acc9/test_example/tags","blobs_url":"https://api.github.com/repos/test-acc9/test_example/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/test-acc9/test_example/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/test-acc9/test_example/git/refs{/sha}","trees_url":"https://api.github.com/repos/test-acc9/test_example/git/trees{/sha}","statuses_url":"https://api.github.com/repos/test-acc9/test_example/statuses/{sha}","languages_url":"https://api.github.com/repos/test-acc9/test_example/languages","stargazers_url":"https://api.github.com/repos/test-acc9/test_example/stargazers","contributors_url":"https://api.github.com/repos/test-acc9/test_example/contributors","subscribers_url":"https://api.github.com/repos/test-acc9/test_example/subscribers","subscription_url":"https://api.github.com/repos/test-acc9/test_example/subscription","commits_url":"https://api.github.com/repos/test-acc9/test_example/commits{/sha}","git_commits_url":"https://api.github.com/repos/test-acc9/test_example/git/commits{/sha}","comments_url":"https://api.github.com/repos/test-acc9/test_example/comments{/number}","issue_comment_url":"https://api.github.com/repos/test-acc9/test_example/issues/comments{/number}","contents_url":"https://api.github.com/repos/test-acc9/test_example/contents/{+path}","compare_url":"https://api.github.com/repos/test-acc9/test_example/compare/{base}...{head}","merges_url":"https://api.github.com/repos/test-acc9/test_example/merges","archive_url":"https://api.github.com/repos/test-acc9/test_example/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/test-acc9/test_example/downloads","issues_url":"https://api.github.com/repos/test-acc9/test_example/issues{/number}","pulls_url":"https://api.github.com/repos/test-acc9/test_example/pulls{/number}","milestones_url":"https://api.github.com/repos/test-acc9/test_example/milestones{/number}","notifications_url":"https://api.github.com/repos/test-acc9/test_example/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/test-acc9/test_example/labels{/name}","releases_url":"https://api.github.com/repos/test-acc9/test_example/releases{/id}","deployments_url":"https://api.github.com/repos/test-acc9/test_example/deployments","created_at":"2022-04-28T09:34:42Z","updated_at":"2022-07-27T08:02:03Z","pushed_at":"2022-07-27T06:13:31Z","git_url":"git://github.com/test-acc9/test_example.git","ssh_url":"git@github.com:test-acc9/test_example.git","clone_url":"https://github.com/test-acc9/test_example.git","svn_url":"https://github.com/test-acc9/test_example","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main"}},"_links":{"self":{"href":"https://api.github.com/repos/test-acc9/test_example/pulls/1"},"html":{"href":"https://github.com/test-acc9/test_example/pull/1"},"issue":{"href":"https://api.github.com/repos/test-acc9/test_example/issues/1"},"comments":{"href":"https://api.github.com/repos/test-acc9/test_example/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/test-acc9/test_example/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/test-acc9/test_example/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/test-acc9/test_example/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/test-acc9/test_example/statuses/a2d3e3c30547a000f026daa47610bb3f7b63aece"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null,"merged":false,"mergeable":true,"rebaseable":true,"mergeable_state":"clean","merged_by":null,"comments":3,"review_comments":0,"maintainer_can_modify":false,"commits":2,"additions":2,"deletions":0,"changed_files":1}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 02 Aug 2022 18:15:05 GMT + ETag: + - W/"170e8feebca292fb56faaf757d32171edca8ee01664007928d374afdbb1e6105" + Last-Modified: + - Tue, 02 Aug 2022 17:58:55 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - EF6B:913A:13F3E16:145DED3:62E969A8 + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4986' + X-RateLimit-Reset: + - '1659466112' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '14' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-09-01 17:26:32 UTC + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/test-acc9/test_example/pulls/1/commits?per_page=250 + response: + content: '[{"sha":"610ada9fa2bbc49f1a08917da3f73bef2d03709c","node_id":"C_kwDOHP_eEdoAKDYxMGFkYTlmYTJiYmM0OWYxYTA4OTE3ZGEzZjczYmVmMmQwMzcwOWM","commit":{"author":{"name":"test-acc9","email":"104562106+test-acc9@users.noreply.github.com","date":"2022-04-28T09:37:05Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-04-28T09:37:05Z"},"message":"Create + random-commit.me","tree":{"sha":"729da23e42745fea6aff7dfdf1679146fe919620","url":"https://api.github.com/repos/test-acc9/test_example/git/trees/729da23e42745fea6aff7dfdf1679146fe919620"},"url":"https://api.github.com/repos/test-acc9/test_example/git/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJiamBBCRBK7hj4Ov3rIwAARu8IAFSGwf1o4Qrp71vxruVbUE0a\nBtA7q0ViOvbK7xwFP6mxJ8cbtKT/0MyYREdgvnw2qzc8KjoQjK0PCZBpHq3BP1WQ\nCkeW1S8xuCn4tnVJQeq+hA9NETt/YGY9cbZHv8o7636vZc7HhxKMiUXF2ppBJYNi\nsQk6W5LgPrb+su5WfZ299yH40b+DivZsoK+aAguhnlXzL4Tvznml78k9x4aO7KPM\nP/FGSzGrUQqNdlFI1xShhqAjwlLpAF2CE7CJ0CZ7q9PmYfYpLfB3bxWAv49+ozk0\nrJP1fdKMJvCmR0spPIjr/3bDhKoMVGB67sj5afkFchSNv0AkNMooVjSa5F2ABP4=\n=ALiA\n-----END + PGP SIGNATURE-----\n","payload":"tree 729da23e42745fea6aff7dfdf1679146fe919620\nparent + ef6edf5ae6643d53a7971fb8823d3f7b2ac65619\nauthor test-acc9 <104562106+test-acc9@users.noreply.github.com> + 1651138625 +0300\ncommitter GitHub 1651138625 +0300\n\nCreate + random-commit.me"}},"url":"https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c","html_url":"https://github.com/test-acc9/test_example/commit/610ada9fa2bbc49f1a08917da3f73bef2d03709c","comments_url":"https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c/comments","author":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","url":"https://api.github.com/repos/test-acc9/test_example/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","html_url":"https://github.com/test-acc9/test_example/commit/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619"}]},{"sha":"a2d3e3c30547a000f026daa47610bb3f7b63aece","node_id":"C_kwDOHP_eEdoAKGEyZDNlM2MzMDU0N2EwMDBmMDI2ZGFhNDc2MTBiYjNmN2I2M2FlY2U","commit":{"author":{"name":"test-acc9","email":"104562106+test-acc9@users.noreply.github.com","date":"2022-07-27T06:13:31Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-07-27T06:13:31Z"},"message":"random-commit-msg","tree":{"sha":"d9899981dca2af8bfa20263c9c97413f017e7602","url":"https://api.github.com/repos/test-acc9/test_example/git/trees/d9899981dca2af8bfa20263c9c97413f017e7602"},"url":"https://api.github.com/repos/test-acc9/test_example/git/commits/a2d3e3c30547a000f026daa47610bb3f7b63aece","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJi4NeLCRBK7hj4Ov3rIwAAHFIIAD0YVuosBePsrNall2Qti6RF\nO4+4QzF5wK6WBgkuCX5mPaGUZMfwYohYUf6iipVVPJHhnxwGA/MIj5pt+muChJCx\nziP6+QfEg8uHR+od0MyG4atpojM2WpBa/gqNzTWnk1Ti7PCyMlueZQGVWLkTuU+Y\ng0fbKg7FFTAxWBhm4sTnb+8vFfwFiuHef7J2NpIdBVAUh4lmfk/hsqKlLz7s6xxS\n8qWz6BhCZSjXUjG+i4/DbwezV9YqsZMz73TbE04TX/pZvqXPoTyLQfCg1mpL2hrI\ndGJBHDZfr0CB8a+33e9TWT80U8TD4g6gjtwrwrZTVzwMfJVVSQcxIfVndQ7ihDg=\n=qgU3\n-----END + PGP SIGNATURE-----\n","payload":"tree d9899981dca2af8bfa20263c9c97413f017e7602\nparent + 610ada9fa2bbc49f1a08917da3f73bef2d03709c\nauthor test-acc9 <104562106+test-acc9@users.noreply.github.com> + 1658902411 +0300\ncommitter GitHub 1658902411 +0300\n\nrandom-commit-msg"}},"url":"https://api.github.com/repos/test-acc9/test_example/commits/a2d3e3c30547a000f026daa47610bb3f7b63aece","html_url":"https://github.com/test-acc9/test_example/commit/a2d3e3c30547a000f026daa47610bb3f7b63aece","comments_url":"https://api.github.com/repos/test-acc9/test_example/commits/a2d3e3c30547a000f026daa47610bb3f7b63aece/comments","author":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"610ada9fa2bbc49f1a08917da3f73bef2d03709c","url":"https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c","html_url":"https://github.com/test-acc9/test_example/commit/610ada9fa2bbc49f1a08917da3f73bef2d03709c"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 02 Aug 2022 18:15:05 GMT + ETag: + - W/"43360268807303f590e2ed5222a10b2b756a7db82e2fb6bcf790d1dcf23153b2" + Last-Modified: + - Tue, 02 Aug 2022 17:58:55 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - EF6B:913A:13F3F6D:145E05C:62E969A9 + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4985' + X-RateLimit-Reset: + - '1659466112' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '15' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-09-01 17:26:32 UTC + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c/status?page=1&per_page=100 + response: + content: '{"state":"success","statuses":[{"url":"https://api.github.com/repos/test-acc9/test_example/statuses/610ada9fa2bbc49f1a08917da3f73bef2d03709c","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","id":18593754782,"node_id":"SC_kwDOHP_eEc8AAAAEVEYung","state":"success","description":"85.00% + (+0.00%) compared to ef6edf5","target_url":"https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1","context":"codecov/project","created_at":"2022-08-02T17:58:52Z","updated_at":"2022-08-02T17:58:52Z"},{"url":"https://api.github.com/repos/test-acc9/test_example/statuses/610ada9fa2bbc49f1a08917da3f73bef2d03709c","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","id":18593755520,"node_id":"SC_kwDOHP_eEc8AAAAEVEYxgA","state":"success","description":"No + unexpected coverage changes found","target_url":"https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1","context":"codecov/changes","created_at":"2022-08-02T17:58:55Z","updated_at":"2022-08-02T17:58:55Z"},{"url":"https://api.github.com/repos/test-acc9/test_example/statuses/610ada9fa2bbc49f1a08917da3f73bef2d03709c","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","id":18593755542,"node_id":"SC_kwDOHP_eEc8AAAAEVEYxlg","state":"success","description":"Coverage + not affected when comparing ef6edf5...610ada9","target_url":"https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1","context":"codecov/patch","created_at":"2022-08-02T17:58:55Z","updated_at":"2022-08-02T17:58:55Z"}],"sha":"610ada9fa2bbc49f1a08917da3f73bef2d03709c","total_count":3,"repository":{"id":486530577,"node_id":"R_kgDOHP_eEQ","name":"test_example","full_name":"test-acc9/test_example","private":false,"owner":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"html_url":"https://github.com/test-acc9/test_example","description":null,"fork":false,"url":"https://api.github.com/repos/test-acc9/test_example","forks_url":"https://api.github.com/repos/test-acc9/test_example/forks","keys_url":"https://api.github.com/repos/test-acc9/test_example/keys{/key_id}","collaborators_url":"https://api.github.com/repos/test-acc9/test_example/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/test-acc9/test_example/teams","hooks_url":"https://api.github.com/repos/test-acc9/test_example/hooks","issue_events_url":"https://api.github.com/repos/test-acc9/test_example/issues/events{/number}","events_url":"https://api.github.com/repos/test-acc9/test_example/events","assignees_url":"https://api.github.com/repos/test-acc9/test_example/assignees{/user}","branches_url":"https://api.github.com/repos/test-acc9/test_example/branches{/branch}","tags_url":"https://api.github.com/repos/test-acc9/test_example/tags","blobs_url":"https://api.github.com/repos/test-acc9/test_example/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/test-acc9/test_example/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/test-acc9/test_example/git/refs{/sha}","trees_url":"https://api.github.com/repos/test-acc9/test_example/git/trees{/sha}","statuses_url":"https://api.github.com/repos/test-acc9/test_example/statuses/{sha}","languages_url":"https://api.github.com/repos/test-acc9/test_example/languages","stargazers_url":"https://api.github.com/repos/test-acc9/test_example/stargazers","contributors_url":"https://api.github.com/repos/test-acc9/test_example/contributors","subscribers_url":"https://api.github.com/repos/test-acc9/test_example/subscribers","subscription_url":"https://api.github.com/repos/test-acc9/test_example/subscription","commits_url":"https://api.github.com/repos/test-acc9/test_example/commits{/sha}","git_commits_url":"https://api.github.com/repos/test-acc9/test_example/git/commits{/sha}","comments_url":"https://api.github.com/repos/test-acc9/test_example/comments{/number}","issue_comment_url":"https://api.github.com/repos/test-acc9/test_example/issues/comments{/number}","contents_url":"https://api.github.com/repos/test-acc9/test_example/contents/{+path}","compare_url":"https://api.github.com/repos/test-acc9/test_example/compare/{base}...{head}","merges_url":"https://api.github.com/repos/test-acc9/test_example/merges","archive_url":"https://api.github.com/repos/test-acc9/test_example/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/test-acc9/test_example/downloads","issues_url":"https://api.github.com/repos/test-acc9/test_example/issues{/number}","pulls_url":"https://api.github.com/repos/test-acc9/test_example/pulls{/number}","milestones_url":"https://api.github.com/repos/test-acc9/test_example/milestones{/number}","notifications_url":"https://api.github.com/repos/test-acc9/test_example/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/test-acc9/test_example/labels{/name}","releases_url":"https://api.github.com/repos/test-acc9/test_example/releases{/id}","deployments_url":"https://api.github.com/repos/test-acc9/test_example/deployments"},"commit_url":"https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c","url":"https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 02 Aug 2022 18:15:07 GMT + ETag: + - W/"d2b162701dbf6a7a8eff7d2ccbe045ed2e3639a937946c5029cef4c0b8a98243" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo, repo:status + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - EF6D:9853:34EC6:7638E:62E969AA + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4984' + X-RateLimit-Reset: + - '1659466112' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '16' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-09-01 17:26:32 UTC + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"text": "Coverage for + *no change* `` + on `featureA` is `85.00000%` via ``", + "author_name": "Codecov", "author_link": "https://myexamplewebsite.io/gh/test-acc9/test_example/commit/610ada9fa2bbc49f1a08917da3f73bef2d03709c", + "attachments": []}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '546' + content-type: + - application/json + host: + - hooks.slack.com + user-agent: + - Codecov + method: POST + uri: https://hooks.slack.com/services/testkylhk/test01hg7/testohfnij1e83uy4xt8sxml + response: + content: ok + headers: + access-control-allow-origin: + - '*' + content-encoding: + - gzip + content-length: + - '22' + content-type: + - text/html + date: + - Tue, 02 Aug 2022 18:15:07 GMT + referrer-policy: + - no-referrer + server: + - Apache + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + vary: + - Accept-Encoding + via: + - envoy-www-iad-of0d, envoy-edge-fra-rnek + x-backend: + - main_normal main_bedrock_normal_with_overflow main_canary_with_overflow main_bedrock_canary_with_overflow + main_control_with_overflow main_bedrock_control_with_overflow + x-edge-backend: + - envoy-www + x-envoy-upstream-service-time: + - '129' + x-frame-options: + - SAMEORIGIN + x-powered-by: + - HHVM/4.153.1 + x-server: + - slack-www-hhvm-main-iad-ssoj + x-slack-backend: + - r + x-slack-edge-shared-secret-outcome: + - no-match + x-slack-shared-secret-outcome: + - no-match + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"repo": {"url": "https://myexamplewebsite.io/gh/test-acc9/test_example", + "service_id": "id_422", "name": "test_example", "private": true}, "head": {"author": + {"username": "test-acc9", "service_id": "104562106", "email": "atest7321@gmail.com", + "service": "github", "name": "Paul Arellano"}, "url": "https://myexamplewebsite.io/gh/test-acc9/test_example/commit/610ada9fa2bbc49f1a08917da3f73bef2d03709c", + "timestamp": "2019-02-01T17:59:47", "totals": {"files": 3, "lines": 20, "hits": + 17, "misses": 3, "partials": 0, "coverage": "85.00000", "branches": 0, "methods": + 0, "messages": 0, "sessions": 1, "complexity": 0, "complexity_total": 0, "diff": + [1, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0]}, "commitid": "610ada9fa2bbc49f1a08917da3f73bef2d03709c", + "service_url": "https://github.com/test-acc9/test_example/commit/610ada9fa2bbc49f1a08917da3f73bef2d03709c", + "branch": "featureA", "message": ""}, "base": {"author": {"username": "test-acc9", + "service_id": "104562106", "email": "atest7321@gmail.com", "service": "github", + "name": "Paul Arellano"}, "url": "https://myexamplewebsite.io/gh/test-acc9/test_example/commit/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619", + "timestamp": "2019-02-01T17:59:47", "totals": {"files": 3, "lines": 20, "hits": + 17, "misses": 3, "partials": 0, "coverage": "85.00000", "branches": 0, "methods": + 0, "messages": 0, "sessions": 1, "complexity": 0, "complexity_total": 0, "diff": + [1, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0]}, "commitid": "ef6edf5ae6643d53a7971fb8823d3f7b2ac65619", + "service_url": "https://github.com/test-acc9/test_example/commit/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619", + "branch": "master", "message": ""}, "compare": {"url": "https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1", + "message": "no change", "coverage": "0.00", "notation": ""}, "owner": {"username": + "test-acc9", "service_id": "104562106", "service": "github"}, "pull": {"head": + {"commit": "610ada9fa2bbc49f1a08917da3f73bef2d03709c", "branch": "master"}, + "number": "1", "base": {"commit": "ef6edf5ae6643d53a7971fb8823d3f7b2ac65619", + "branch": "master"}, "open": true, "id": 1, "merged": false}}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '2116' + content-type: + - application/json + host: + - 6da6786648c8a8e5b8b09bc6562af8b4.m.pipedream.net + user-agent: + - Codecov + method: POST + uri: https://6da6786648c8a8e5b8b09bc6562af8b4.m.pipedream.net + response: + content: '' + headers: + Access-Control-Allow-Origin: + - '*' + Connection: + - keep-alive + Content-Length: + - '0' + Date: + - Tue, 02 Aug 2022 18:15:08 GMT + X-Powered-By: + - Express + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/test-acc9/test_example/compare/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619...610ada9fa2bbc49f1a08917da3f73bef2d03709c + response: + content: '{"url":"https://api.github.com/repos/test-acc9/test_example/compare/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619...610ada9fa2bbc49f1a08917da3f73bef2d03709c","html_url":"https://github.com/test-acc9/test_example/compare/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619...610ada9fa2bbc49f1a08917da3f73bef2d03709c","permalink_url":"https://github.com/test-acc9/test_example/compare/test-acc9:ef6edf5...test-acc9:610ada9","diff_url":"https://github.com/test-acc9/test_example/compare/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619...610ada9fa2bbc49f1a08917da3f73bef2d03709c.diff","patch_url":"https://github.com/test-acc9/test_example/compare/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619...610ada9fa2bbc49f1a08917da3f73bef2d03709c.patch","base_commit":{"sha":"ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","node_id":"C_kwDOHP_eEdoAKGVmNmVkZjVhZTY2NDNkNTNhNzk3MWZiODgyM2QzZjdiMmFjNjU2MTk","commit":{"author":{"name":"test-acc9","email":"104562106+test-acc9@users.noreply.github.com","date":"2022-04-28T09:35:11Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-04-28T09:35:11Z"},"message":"Create + read.me","tree":{"sha":"5e836bead2ad06cc76854f5f8d3f4ab983ec60a1","url":"https://api.github.com/repos/test-acc9/test_example/git/trees/5e836bead2ad06cc76854f5f8d3f4ab983ec60a1"},"url":"https://api.github.com/repos/test-acc9/test_example/git/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJial/QCRBK7hj4Ov3rIwAA29QIAAdOIKu3w9SwC5g7UPjXj8xi\nYOt30BqPzSY/ktq7scRf/uOiO97uAC0XANXwmfkWCgI6YZSijysJdHEALFg7NwXp\n/aF8u9fFk+nHbhfTsCtM0jgabWRt0FSMsO0VreWn5ExiqWqPnDMTydbyCEzkHwsz\nMvVUGj8uqP4P7NM6DvCsOSM0m9Xg6IEFNqpsMd8yfvP5fF57YXnpixhISAUkGjuq\nJx/ouGxFircoRLTlJi5ZfmFNGFsXfoqvCyBMZ62lkg81lUYXEGKhDvFWnz6OR9sZ\n+4SxRgaFLz05xUKl3EEAhnWHjzrBkQP7gJBIz2MJ1VvFmH3h/hH86NtaN+TMCKU=\n=k3dl\n-----END + PGP SIGNATURE-----\n","payload":"tree 5e836bead2ad06cc76854f5f8d3f4ab983ec60a1\nauthor + test-acc9 <104562106+test-acc9@users.noreply.github.com> 1651138511 +0300\ncommitter + GitHub 1651138511 +0300\n\nCreate read.me"}},"url":"https://api.github.com/repos/test-acc9/test_example/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","html_url":"https://github.com/test-acc9/test_example/commit/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","comments_url":"https://api.github.com/repos/test-acc9/test_example/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619/comments","author":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[]},"merge_base_commit":{"sha":"ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","node_id":"C_kwDOHP_eEdoAKGVmNmVkZjVhZTY2NDNkNTNhNzk3MWZiODgyM2QzZjdiMmFjNjU2MTk","commit":{"author":{"name":"test-acc9","email":"104562106+test-acc9@users.noreply.github.com","date":"2022-04-28T09:35:11Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-04-28T09:35:11Z"},"message":"Create + read.me","tree":{"sha":"5e836bead2ad06cc76854f5f8d3f4ab983ec60a1","url":"https://api.github.com/repos/test-acc9/test_example/git/trees/5e836bead2ad06cc76854f5f8d3f4ab983ec60a1"},"url":"https://api.github.com/repos/test-acc9/test_example/git/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJial/QCRBK7hj4Ov3rIwAA29QIAAdOIKu3w9SwC5g7UPjXj8xi\nYOt30BqPzSY/ktq7scRf/uOiO97uAC0XANXwmfkWCgI6YZSijysJdHEALFg7NwXp\n/aF8u9fFk+nHbhfTsCtM0jgabWRt0FSMsO0VreWn5ExiqWqPnDMTydbyCEzkHwsz\nMvVUGj8uqP4P7NM6DvCsOSM0m9Xg6IEFNqpsMd8yfvP5fF57YXnpixhISAUkGjuq\nJx/ouGxFircoRLTlJi5ZfmFNGFsXfoqvCyBMZ62lkg81lUYXEGKhDvFWnz6OR9sZ\n+4SxRgaFLz05xUKl3EEAhnWHjzrBkQP7gJBIz2MJ1VvFmH3h/hH86NtaN+TMCKU=\n=k3dl\n-----END + PGP SIGNATURE-----\n","payload":"tree 5e836bead2ad06cc76854f5f8d3f4ab983ec60a1\nauthor + test-acc9 <104562106+test-acc9@users.noreply.github.com> 1651138511 +0300\ncommitter + GitHub 1651138511 +0300\n\nCreate read.me"}},"url":"https://api.github.com/repos/test-acc9/test_example/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","html_url":"https://github.com/test-acc9/test_example/commit/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","comments_url":"https://api.github.com/repos/test-acc9/test_example/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619/comments","author":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[]},"status":"ahead","ahead_by":1,"behind_by":0,"total_commits":1,"commits":[{"sha":"610ada9fa2bbc49f1a08917da3f73bef2d03709c","node_id":"C_kwDOHP_eEdoAKDYxMGFkYTlmYTJiYmM0OWYxYTA4OTE3ZGEzZjczYmVmMmQwMzcwOWM","commit":{"author":{"name":"test-acc9","email":"104562106+test-acc9@users.noreply.github.com","date":"2022-04-28T09:37:05Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-04-28T09:37:05Z"},"message":"Create + random-commit.me","tree":{"sha":"729da23e42745fea6aff7dfdf1679146fe919620","url":"https://api.github.com/repos/test-acc9/test_example/git/trees/729da23e42745fea6aff7dfdf1679146fe919620"},"url":"https://api.github.com/repos/test-acc9/test_example/git/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJiamBBCRBK7hj4Ov3rIwAARu8IAFSGwf1o4Qrp71vxruVbUE0a\nBtA7q0ViOvbK7xwFP6mxJ8cbtKT/0MyYREdgvnw2qzc8KjoQjK0PCZBpHq3BP1WQ\nCkeW1S8xuCn4tnVJQeq+hA9NETt/YGY9cbZHv8o7636vZc7HhxKMiUXF2ppBJYNi\nsQk6W5LgPrb+su5WfZ299yH40b+DivZsoK+aAguhnlXzL4Tvznml78k9x4aO7KPM\nP/FGSzGrUQqNdlFI1xShhqAjwlLpAF2CE7CJ0CZ7q9PmYfYpLfB3bxWAv49+ozk0\nrJP1fdKMJvCmR0spPIjr/3bDhKoMVGB67sj5afkFchSNv0AkNMooVjSa5F2ABP4=\n=ALiA\n-----END + PGP SIGNATURE-----\n","payload":"tree 729da23e42745fea6aff7dfdf1679146fe919620\nparent + ef6edf5ae6643d53a7971fb8823d3f7b2ac65619\nauthor test-acc9 <104562106+test-acc9@users.noreply.github.com> + 1651138625 +0300\ncommitter GitHub 1651138625 +0300\n\nCreate + random-commit.me"}},"url":"https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c","html_url":"https://github.com/test-acc9/test_example/commit/610ada9fa2bbc49f1a08917da3f73bef2d03709c","comments_url":"https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c/comments","author":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","url":"https://api.github.com/repos/test-acc9/test_example/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","html_url":"https://github.com/test-acc9/test_example/commit/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619"}]}],"files":[{"sha":"ce013625030ba8dba906f756967f9e9ca394464a","filename":"random-commit.me","status":"added","additions":1,"deletions":0,"changes":1,"blob_url":"https://github.com/test-acc9/test_example/blob/610ada9fa2bbc49f1a08917da3f73bef2d03709c/random-commit.me","raw_url":"https://github.com/test-acc9/test_example/raw/610ada9fa2bbc49f1a08917da3f73bef2d03709c/random-commit.me","contents_url":"https://api.github.com/repos/test-acc9/test_example/contents/random-commit.me?ref=610ada9fa2bbc49f1a08917da3f73bef2d03709c","patch":"@@ + -0,0 +1 @@\n+hello"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 02 Aug 2022 18:15:09 GMT + ETag: + - W/"13c61d1887620d3242b3cad23be06746a4d08c5264b577e40633f95b85ffc8a8" + Last-Modified: + - Thu, 28 Apr 2022 09:37:05 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - EF72:7D69:C53C2D:CAE737:62E969AD + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4983' + X-RateLimit-Reset: + - '1659466112' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '17' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-09-01 17:26:32 UTC + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"body": "## [Codecov](https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1?src=pr&el=h1) + Report\n> Merging [#1](https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1?src=pr&el=desc) + (610ada9) into [main](https://myexamplewebsite.io/gh/test-acc9/test_example/commit/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619?el=desc) + (ef6edf5) will **not change** coverage.\n> The diff coverage is `n/a`.\n\n> + :exclamation: Current head 610ada9 differs from pull request most recent head + a2d3e3c. Consider uploading reports for the commit a2d3e3c to get more accurate + results\n\n[![Impacted file tree graph](https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij)](https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1?src=pr&el=tree)\n\n```diff\n@@ Coverage + Diff @@\n## main #1 +/- ##\n=======================================\n Coverage 85.00% 85.00% \n=======================================\n Files 3 3 \n Lines 20 20 \n=======================================\n Hits 17 17 \n Misses 3 3 \n```\n\n| + Flag | Coverage \u0394 | |\n|---|---|---|\n| unit | `85.00% <\u00f8> (\u00f8)` + | |\n\nFlags with carried forward coverage won''t be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Paul+Arellano#carryforward-flags-in-the-pull-request-comment) + to find out more.\n\n\n------\n\n[Continue to review full report in Codecov + by Sentry](https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1?src=pr&el=continue).\n> + **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Paul+Arellano)\n> + `\u0394 = absolute (impact)`, `\u00f8 = not affected`, `? = missing + data`\n> Powered by [Codecov](https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1?src=pr&el=footer). + Last update [ef6edf5...a2d3e3c](https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1?src=pr&el=lastupdated). + Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Paul+Arellano).\n"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '2436' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/test-acc9/test_example/issues/1/comments + response: + content: "{\"url\":\"https://api.github.com/repos/test-acc9/test_example/issues/comments/1203062549\"\ + ,\"html_url\":\"https://github.com/test-acc9/test_example/pull/1#issuecomment-1203062549\"\ + ,\"issue_url\":\"https://api.github.com/repos/test-acc9/test_example/issues/1\"\ + ,\"id\":1203062549,\"node_id\":\"IC_kwDOHP_eEc5HtUcV\",\"user\":{\"login\":\"\ + test-acc9\",\"id\":104562106,\"node_id\":\"U_kgDOBjt9ug\",\"avatar_url\":\"\ + https://avatars.githubusercontent.com/u/104562106?v=4\",\"gravatar_id\":\"\"\ + ,\"url\":\"https://api.github.com/users/test-acc9\",\"html_url\":\"https://github.com/test-acc9\"\ + ,\"followers_url\":\"https://api.github.com/users/test-acc9/followers\",\"following_url\"\ + :\"https://api.github.com/users/test-acc9/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/test-acc9/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/test-acc9/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/test-acc9/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/test-acc9/orgs\",\"repos_url\":\"https://api.github.com/users/test-acc9/repos\"\ + ,\"events_url\":\"https://api.github.com/users/test-acc9/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/test-acc9/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"created_at\":\"2022-08-02T18:15:10Z\"\ + ,\"updated_at\":\"2022-08-02T18:15:10Z\",\"author_association\":\"OWNER\",\"\ + body\":\"# [Codecov](https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1?src=pr&el=h1)\ + \ Report\\n> Merging [#1](https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1?src=pr&el=desc)\ + \ (610ada9) into [main](https://myexamplewebsite.io/gh/test-acc9/test_example/commit/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619?el=desc)\ + \ (ef6edf5) will **not change** coverage.\\n> The diff coverage is `n/a`.\\\ + n\\n> :exclamation: Current head 610ada9 differs from pull request most recent\ + \ head a2d3e3c. Consider uploading reports for the commit a2d3e3c to get more\ + \ accurate results\\n\\n[![Impacted file tree graph](https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij)](https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1?src=pr&el=tree)\\\ + n\\n```diff\\n@@ Coverage Diff @@\\n## main\ + \ #1 +/- ##\\n=======================================\\n Coverage\ + \ 85.00% 85.00% \\n=======================================\\n\ + \ Files 3 3 \\n Lines 20 20 \ + \ \\n=======================================\\n Hits 17 \ + \ 17 \\n Misses 3 3 \\n```\\n\\n| Flag\ + \ | Coverage \u0394 | |\\n|---|---|---|\\n| unit | `85.00% <\xF8> (\xF8)` |\ + \ |\\n\\nFlags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Paul+Arellano#carryforward-flags-in-the-pull-request-comment)\ + \ to find out more.\\n\\n\\n------\\n\\n[Continue to review full report in Codecov\ + \ by Sentry](https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1?src=pr&el=continue).\\\ + n> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Paul+Arellano)\\\ + n> `\u0394 = absolute (impact)`, `\xF8 = not affected`, `? = missing\ + \ data`\\n> Powered by [Codecov](https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1?src=pr&el=footer).\ + \ Last update [ef6edf5...a2d3e3c](https://myexamplewebsite.io/gh/test-acc9/test_example/pull/1?src=pr&el=lastupdated).\ + \ Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Paul+Arellano).\\\ + n\",\"reactions\":{\"url\":\"https://api.github.com/repos/test-acc9/test_example/issues/comments/1203062549/reactions\"\ + ,\"total_count\":0,\"+1\":0,\"-1\":0,\"laugh\":0,\"hooray\":0,\"confused\":0,\"\ + heart\":0,\"rocket\":0,\"eyes\":0},\"performed_via_github_app\":null}" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '3970' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 02 Aug 2022 18:15:11 GMT + ETag: + - '"cf1d0508937b1741624bab77f3a77e7fd2f1c4ea5d5e5be7032fb21b2906216d"' + Location: + - https://api.github.com/repos/test-acc9/test_example/issues/comments/1203062549 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - EF73:11A39:1493081:14FDED9:62E969AE + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4982' + X-RateLimit-Reset: + - '1659466112' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '18' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-09-01 17:26:32 UTC + http_version: HTTP/1.1 + status_code: 201 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c/pulls + response: + content: '[{"url":"https://api.github.com/repos/test-acc9/test_example/pulls/1","id":921616238,"node_id":"PR_kwDOHP_eEc427r9u","html_url":"https://github.com/test-acc9/test_example/pull/1","diff_url":"https://github.com/test-acc9/test_example/pull/1.diff","patch_url":"https://github.com/test-acc9/test_example/pull/1.patch","issue_url":"https://api.github.com/repos/test-acc9/test_example/issues/1","number":1,"state":"open","locked":false,"title":"Create + random-commit.me","user":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"body":null,"created_at":"2022-04-28T09:37:10Z","updated_at":"2022-08-02T18:15:11Z","closed_at":null,"merged_at":null,"merge_commit_sha":"188bca08574e22fdc3cbdbde0bb98bfcf7e64425","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/test-acc9/test_example/pulls/1/commits","review_comments_url":"https://api.github.com/repos/test-acc9/test_example/pulls/1/comments","review_comment_url":"https://api.github.com/repos/test-acc9/test_example/pulls/comments{/number}","comments_url":"https://api.github.com/repos/test-acc9/test_example/issues/1/comments","statuses_url":"https://api.github.com/repos/test-acc9/test_example/statuses/a2d3e3c30547a000f026daa47610bb3f7b63aece","head":{"label":"test-acc9:featureA","ref":"featureA","sha":"a2d3e3c30547a000f026daa47610bb3f7b63aece","user":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"repo":{"id":486530577,"node_id":"R_kgDOHP_eEQ","name":"test_example","full_name":"test-acc9/test_example","private":false,"owner":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"html_url":"https://github.com/test-acc9/test_example","description":null,"fork":false,"url":"https://api.github.com/repos/test-acc9/test_example","forks_url":"https://api.github.com/repos/test-acc9/test_example/forks","keys_url":"https://api.github.com/repos/test-acc9/test_example/keys{/key_id}","collaborators_url":"https://api.github.com/repos/test-acc9/test_example/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/test-acc9/test_example/teams","hooks_url":"https://api.github.com/repos/test-acc9/test_example/hooks","issue_events_url":"https://api.github.com/repos/test-acc9/test_example/issues/events{/number}","events_url":"https://api.github.com/repos/test-acc9/test_example/events","assignees_url":"https://api.github.com/repos/test-acc9/test_example/assignees{/user}","branches_url":"https://api.github.com/repos/test-acc9/test_example/branches{/branch}","tags_url":"https://api.github.com/repos/test-acc9/test_example/tags","blobs_url":"https://api.github.com/repos/test-acc9/test_example/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/test-acc9/test_example/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/test-acc9/test_example/git/refs{/sha}","trees_url":"https://api.github.com/repos/test-acc9/test_example/git/trees{/sha}","statuses_url":"https://api.github.com/repos/test-acc9/test_example/statuses/{sha}","languages_url":"https://api.github.com/repos/test-acc9/test_example/languages","stargazers_url":"https://api.github.com/repos/test-acc9/test_example/stargazers","contributors_url":"https://api.github.com/repos/test-acc9/test_example/contributors","subscribers_url":"https://api.github.com/repos/test-acc9/test_example/subscribers","subscription_url":"https://api.github.com/repos/test-acc9/test_example/subscription","commits_url":"https://api.github.com/repos/test-acc9/test_example/commits{/sha}","git_commits_url":"https://api.github.com/repos/test-acc9/test_example/git/commits{/sha}","comments_url":"https://api.github.com/repos/test-acc9/test_example/comments{/number}","issue_comment_url":"https://api.github.com/repos/test-acc9/test_example/issues/comments{/number}","contents_url":"https://api.github.com/repos/test-acc9/test_example/contents/{+path}","compare_url":"https://api.github.com/repos/test-acc9/test_example/compare/{base}...{head}","merges_url":"https://api.github.com/repos/test-acc9/test_example/merges","archive_url":"https://api.github.com/repos/test-acc9/test_example/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/test-acc9/test_example/downloads","issues_url":"https://api.github.com/repos/test-acc9/test_example/issues{/number}","pulls_url":"https://api.github.com/repos/test-acc9/test_example/pulls{/number}","milestones_url":"https://api.github.com/repos/test-acc9/test_example/milestones{/number}","notifications_url":"https://api.github.com/repos/test-acc9/test_example/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/test-acc9/test_example/labels{/name}","releases_url":"https://api.github.com/repos/test-acc9/test_example/releases{/id}","deployments_url":"https://api.github.com/repos/test-acc9/test_example/deployments","created_at":"2022-04-28T09:34:42Z","updated_at":"2022-07-27T08:02:03Z","pushed_at":"2022-07-27T06:13:31Z","git_url":"git://github.com/test-acc9/test_example.git","ssh_url":"git@github.com:test-acc9/test_example.git","clone_url":"https://github.com/test-acc9/test_example.git","svn_url":"https://github.com/test-acc9/test_example","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main"}},"base":{"label":"test-acc9:main","ref":"main","sha":"ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","user":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"repo":{"id":486530577,"node_id":"R_kgDOHP_eEQ","name":"test_example","full_name":"test-acc9/test_example","private":false,"owner":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"html_url":"https://github.com/test-acc9/test_example","description":null,"fork":false,"url":"https://api.github.com/repos/test-acc9/test_example","forks_url":"https://api.github.com/repos/test-acc9/test_example/forks","keys_url":"https://api.github.com/repos/test-acc9/test_example/keys{/key_id}","collaborators_url":"https://api.github.com/repos/test-acc9/test_example/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/test-acc9/test_example/teams","hooks_url":"https://api.github.com/repos/test-acc9/test_example/hooks","issue_events_url":"https://api.github.com/repos/test-acc9/test_example/issues/events{/number}","events_url":"https://api.github.com/repos/test-acc9/test_example/events","assignees_url":"https://api.github.com/repos/test-acc9/test_example/assignees{/user}","branches_url":"https://api.github.com/repos/test-acc9/test_example/branches{/branch}","tags_url":"https://api.github.com/repos/test-acc9/test_example/tags","blobs_url":"https://api.github.com/repos/test-acc9/test_example/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/test-acc9/test_example/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/test-acc9/test_example/git/refs{/sha}","trees_url":"https://api.github.com/repos/test-acc9/test_example/git/trees{/sha}","statuses_url":"https://api.github.com/repos/test-acc9/test_example/statuses/{sha}","languages_url":"https://api.github.com/repos/test-acc9/test_example/languages","stargazers_url":"https://api.github.com/repos/test-acc9/test_example/stargazers","contributors_url":"https://api.github.com/repos/test-acc9/test_example/contributors","subscribers_url":"https://api.github.com/repos/test-acc9/test_example/subscribers","subscription_url":"https://api.github.com/repos/test-acc9/test_example/subscription","commits_url":"https://api.github.com/repos/test-acc9/test_example/commits{/sha}","git_commits_url":"https://api.github.com/repos/test-acc9/test_example/git/commits{/sha}","comments_url":"https://api.github.com/repos/test-acc9/test_example/comments{/number}","issue_comment_url":"https://api.github.com/repos/test-acc9/test_example/issues/comments{/number}","contents_url":"https://api.github.com/repos/test-acc9/test_example/contents/{+path}","compare_url":"https://api.github.com/repos/test-acc9/test_example/compare/{base}...{head}","merges_url":"https://api.github.com/repos/test-acc9/test_example/merges","archive_url":"https://api.github.com/repos/test-acc9/test_example/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/test-acc9/test_example/downloads","issues_url":"https://api.github.com/repos/test-acc9/test_example/issues{/number}","pulls_url":"https://api.github.com/repos/test-acc9/test_example/pulls{/number}","milestones_url":"https://api.github.com/repos/test-acc9/test_example/milestones{/number}","notifications_url":"https://api.github.com/repos/test-acc9/test_example/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/test-acc9/test_example/labels{/name}","releases_url":"https://api.github.com/repos/test-acc9/test_example/releases{/id}","deployments_url":"https://api.github.com/repos/test-acc9/test_example/deployments","created_at":"2022-04-28T09:34:42Z","updated_at":"2022-07-27T08:02:03Z","pushed_at":"2022-07-27T06:13:31Z","git_url":"git://github.com/test-acc9/test_example.git","ssh_url":"git@github.com:test-acc9/test_example.git","clone_url":"https://github.com/test-acc9/test_example.git","svn_url":"https://github.com/test-acc9/test_example","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main"}},"_links":{"self":{"href":"https://api.github.com/repos/test-acc9/test_example/pulls/1"},"html":{"href":"https://github.com/test-acc9/test_example/pull/1"},"issue":{"href":"https://api.github.com/repos/test-acc9/test_example/issues/1"},"comments":{"href":"https://api.github.com/repos/test-acc9/test_example/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/test-acc9/test_example/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/test-acc9/test_example/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/test-acc9/test_example/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/test-acc9/test_example/statuses/a2d3e3c30547a000f026daa47610bb3f7b63aece"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 17 Aug 2022 01:19:13 GMT + ETag: + - W/"e9554c5b3f4def89e40ff4318b9cca6b121fccf1f7f2e13d8d54ed159ebd26f8" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C60C:5D66:400A79:43C910:62FC4211 + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4981' + X-RateLimit-Reset: + - '1660701033' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '19' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-08-24 01:18:21 UTC + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d/status?page=1&per_page=100 + response: + content: '{"state":"success","statuses":[{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/statuses/5601846871b8142ab0df1e0b8774756c658bcc7d","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","id":24842855406,"node_id":"SC_kwDOJu7MkM8AAAAFyL_j7g","state":"success","description":"No + unexpected coverage changes found","target_url":"https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9","context":"codecov/changes","created_at":"2023-08-30T16:03:04Z","updated_at":"2023-08-30T16:03:04Z"},{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/statuses/5601846871b8142ab0df1e0b8774756c658bcc7d","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","id":24843016906,"node_id":"SC_kwDOJu7MkM8AAAAFyMJayg","state":"success","description":"Coverage + not affected when comparing 38c2d02...5601846","target_url":"https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9","context":"codecov/patch","created_at":"2023-08-30T16:10:45Z","updated_at":"2023-08-30T16:10:45Z"},{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/statuses/5601846871b8142ab0df1e0b8774756c658bcc7d","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","id":24844653606,"node_id":"SC_kwDOJu7MkM8AAAAFyNtUJg","state":"success","description":"85.00% + (+0.00%) compared to 5b174c2","target_url":"https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9","context":"codecov/project","created_at":"2023-08-30T17:32:52Z","updated_at":"2023-08-30T17:32:52Z"}],"sha":"5601846871b8142ab0df1e0b8774756c658bcc7d","total_count":3,"repository":{"id":653184144,"node_id":"R_kgDOJu7MkA","name":"codecov-demo","full_name":"joseph-sentry/codecov-demo","private":false,"owner":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"html_url":"https://github.com/joseph-sentry/codecov-demo","description":null,"fork":true,"url":"https://api.github.com/repos/joseph-sentry/codecov-demo","forks_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/forks","keys_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/teams","hooks_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/hooks","issue_events_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/events{/number}","events_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/events","assignees_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/assignees{/user}","branches_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/branches{/branch}","tags_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/tags","blobs_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/refs{/sha}","trees_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/statuses/{sha}","languages_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/languages","stargazers_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/stargazers","contributors_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/contributors","subscribers_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/subscribers","subscription_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/subscription","commits_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits{/sha}","git_commits_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits{/sha}","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/comments{/number}","issue_comment_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments{/number}","contents_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/contents/{+path}","compare_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/merges","archive_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/downloads","issues_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/issues{/number}","pulls_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/pulls{/number}","milestones_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/milestones{/number}","notifications_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/labels{/name}","releases_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/releases{/id}","deployments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/deployments"},"commit_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:59 GMT + ETag: + - W/"a8ba6029cdf0ed9473d1b0c81a03c7bc00d743b518b84fffbfe5f117a3da131f" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo, repo:status + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E74D:052D:10DCAE:223F5B:64EF8E2B + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4975' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '25' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:50:41 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/pulls/9 + response: + content: '{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/pulls/9","id":1494493271,"node_id":"PR_kwDOJu7MkM5ZFChX","html_url":"https://github.com/joseph-sentry/codecov-demo/pull/9","diff_url":"https://github.com/joseph-sentry/codecov-demo/pull/9.diff","patch_url":"https://github.com/joseph-sentry/codecov-demo/pull/9.patch","issue_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/9","number":9,"state":"open","locked":false,"title":"make + change","user":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"body":null,"created_at":"2023-08-29T21:33:06Z","updated_at":"2023-08-30T18:44:47Z","closed_at":null,"merged_at":null,"merge_commit_sha":"cda7a5ab6d25555ff6503973ce16ef7c5fb1cd37","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/pulls/9/commits","review_comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/pulls/9/comments","review_comment_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/pulls/comments{/number}","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/9/comments","statuses_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/statuses/5601846871b8142ab0df1e0b8774756c658bcc7d","head":{"label":"joseph-sentry:test","ref":"test","sha":"5601846871b8142ab0df1e0b8774756c658bcc7d","user":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"repo":{"id":653184144,"node_id":"R_kgDOJu7MkA","name":"codecov-demo","full_name":"joseph-sentry/codecov-demo","private":false,"owner":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"html_url":"https://github.com/joseph-sentry/codecov-demo","description":null,"fork":true,"url":"https://api.github.com/repos/joseph-sentry/codecov-demo","forks_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/forks","keys_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/teams","hooks_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/hooks","issue_events_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/events{/number}","events_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/events","assignees_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/assignees{/user}","branches_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/branches{/branch}","tags_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/tags","blobs_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/refs{/sha}","trees_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/statuses/{sha}","languages_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/languages","stargazers_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/stargazers","contributors_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/contributors","subscribers_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/subscribers","subscription_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/subscription","commits_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits{/sha}","git_commits_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits{/sha}","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/comments{/number}","issue_comment_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments{/number}","contents_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/contents/{+path}","compare_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/merges","archive_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/downloads","issues_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/issues{/number}","pulls_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/pulls{/number}","milestones_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/milestones{/number}","notifications_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/labels{/name}","releases_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/releases{/id}","deployments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/deployments","created_at":"2023-06-13T14:58:33Z","updated_at":"2023-06-13T15:45:15Z","pushed_at":"2023-08-29T21:33:07Z","git_url":"git://github.com/joseph-sentry/codecov-demo.git","ssh_url":"git@github.com:joseph-sentry/codecov-demo.git","clone_url":"https://github.com/joseph-sentry/codecov-demo.git","svn_url":"https://github.com/joseph-sentry/codecov-demo","homepage":null,"size":3500,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main"}},"base":{"label":"joseph-sentry:main","ref":"main","sha":"38c2d0214f2a48c9212a140f5311977059a15b35","user":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"repo":{"id":653184144,"node_id":"R_kgDOJu7MkA","name":"codecov-demo","full_name":"joseph-sentry/codecov-demo","private":false,"owner":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"html_url":"https://github.com/joseph-sentry/codecov-demo","description":null,"fork":true,"url":"https://api.github.com/repos/joseph-sentry/codecov-demo","forks_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/forks","keys_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/teams","hooks_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/hooks","issue_events_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/events{/number}","events_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/events","assignees_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/assignees{/user}","branches_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/branches{/branch}","tags_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/tags","blobs_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/refs{/sha}","trees_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/statuses/{sha}","languages_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/languages","stargazers_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/stargazers","contributors_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/contributors","subscribers_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/subscribers","subscription_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/subscription","commits_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits{/sha}","git_commits_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits{/sha}","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/comments{/number}","issue_comment_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments{/number}","contents_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/contents/{+path}","compare_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/merges","archive_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/downloads","issues_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/issues{/number}","pulls_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/pulls{/number}","milestones_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/milestones{/number}","notifications_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/labels{/name}","releases_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/releases{/id}","deployments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/deployments","created_at":"2023-06-13T14:58:33Z","updated_at":"2023-06-13T15:45:15Z","pushed_at":"2023-08-29T21:33:07Z","git_url":"git://github.com/joseph-sentry/codecov-demo.git","ssh_url":"git@github.com:joseph-sentry/codecov-demo.git","clone_url":"https://github.com/joseph-sentry/codecov-demo.git","svn_url":"https://github.com/joseph-sentry/codecov-demo","homepage":null,"size":3500,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main"}},"_links":{"self":{"href":"https://api.github.com/repos/joseph-sentry/codecov-demo/pulls/9"},"html":{"href":"https://github.com/joseph-sentry/codecov-demo/pull/9"},"issue":{"href":"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/9"},"comments":{"href":"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/9/comments"},"review_comments":{"href":"https://api.github.com/repos/joseph-sentry/codecov-demo/pulls/9/comments"},"review_comment":{"href":"https://api.github.com/repos/joseph-sentry/codecov-demo/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/joseph-sentry/codecov-demo/pulls/9/commits"},"statuses":{"href":"https://api.github.com/repos/joseph-sentry/codecov-demo/statuses/5601846871b8142ab0df1e0b8774756c658bcc7d"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null,"merged":false,"mergeable":true,"rebaseable":true,"mergeable_state":"clean","merged_by":null,"comments":6,"review_comments":0,"maintainer_can_modify":false,"commits":1,"additions":1,"deletions":3,"changed_files":1}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:59 GMT + ETag: + - W/"bcd044d2f0b0d1685fee462d0406adcdf71bf58d7ed6e4318a371a0548c311ef" + Last-Modified: + - Wed, 30 Aug 2023 18:44:47 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E74E:11AB:100C7C:209FCB:64EF8E2B + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4974' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '26' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:50:41 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/pulls/9/commits?per_page=250 + response: + content: '[{"sha":"5601846871b8142ab0df1e0b8774756c658bcc7d","node_id":"C_kwDOJu7MkNoAKDU2MDE4NDY4NzFiODE0MmFiMGRmMWUwYjg3NzQ3NTZjNjU4YmNjN2Q","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:16:17Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:23:50Z"},"message":"make + change","tree":{"sha":"3ca6632aeb641e579bea4391294d9ebe55c062c7","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/3ca6632aeb641e579bea4391294d9ebe55c062c7"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5601846871b8142ab0df1e0b8774756c658bcc7d","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQAmeUxSTgmd3FlcaYtRmVpuCh0n6L0/2Oy7wnsoGF6zjLoBO+aDz22vOG+bywNp4bc\nlmOl+Tu5Jk9woOhH2Olg8=\n-----END + SSH SIGNATURE-----","payload":"tree 3ca6632aeb641e579bea4391294d9ebe55c062c7\nparent + 5b174c2b40d501a70c479e91025d5109b1ad5c1b\nauthor joseph-sentry + 1692026177 -0400\ncommitter joseph-sentry 1692026630 + -0400\n\nmake change\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5601846871b8142ab0df1e0b8774756c658bcc7d","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:44:59 GMT + ETag: + - W/"502ab99dca2a22266f17c9b4931ebcc7979de45c6e6bf4cdd99ad419bae1024c" + Last-Modified: + - Wed, 30 Aug 2023 18:44:47 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E74E:11AB:100CD6:20A07F:64EF8E2B + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4973' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '27' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:50:41 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d + response: + content: '{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d","html_url":"https://github.com/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d","permalink_url":"https://github.com/joseph-sentry/codecov-demo/compare/joseph-sentry:5b174c2...joseph-sentry:5601846","diff_url":"https://github.com/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d.diff","patch_url":"https://github.com/joseph-sentry/codecov-demo/compare/5b174c2b40d501a70c479e91025d5109b1ad5c1b...5601846871b8142ab0df1e0b8774756c658bcc7d.patch","base_commit":{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","node_id":"C_kwDOJu7MkNoAKDViMTc0YzJiNDBkNTAxYTcwYzQ3OWU5MTAyNWQ1MTA5YjFhZDVjMWI","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"message":"make + change on main","tree":{"sha":"a9d6c66e50012a6d42f3f4c4e6d6de845230e73b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/a9d6c66e50012a6d42f3f4c4e6d6de845230e73b"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQCSXJE75ylD7PwNrXf6ITnsvh9gIP5b/JASjGlKqB7f37XPt9wLvT2YaV3HcSyTRyt\nCY9zBOZT+tELiyZcpqKwY=\n-----END + SSH SIGNATURE-----","payload":"tree a9d6c66e50012a6d42f3f4c4e6d6de845230e73b\nparent + 3fe10c352faae434e0cca7c1c5984c1c98000288\nauthor joseph-sentry + 1692026348 -0400\ncommitter joseph-sentry 1692026348 + -0400\n\nmake change on main\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"3fe10c352faae434e0cca7c1c5984c1c98000288","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/3fe10c352faae434e0cca7c1c5984c1c98000288","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/3fe10c352faae434e0cca7c1c5984c1c98000288"}]},"merge_base_commit":{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","node_id":"C_kwDOJu7MkNoAKDViMTc0YzJiNDBkNTAxYTcwYzQ3OWU5MTAyNWQ1MTA5YjFhZDVjMWI","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"message":"make + change on main","tree":{"sha":"a9d6c66e50012a6d42f3f4c4e6d6de845230e73b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/a9d6c66e50012a6d42f3f4c4e6d6de845230e73b"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQCSXJE75ylD7PwNrXf6ITnsvh9gIP5b/JASjGlKqB7f37XPt9wLvT2YaV3HcSyTRyt\nCY9zBOZT+tELiyZcpqKwY=\n-----END + SSH SIGNATURE-----","payload":"tree a9d6c66e50012a6d42f3f4c4e6d6de845230e73b\nparent + 3fe10c352faae434e0cca7c1c5984c1c98000288\nauthor joseph-sentry + 1692026348 -0400\ncommitter joseph-sentry 1692026348 + -0400\n\nmake change on main\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"3fe10c352faae434e0cca7c1c5984c1c98000288","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/3fe10c352faae434e0cca7c1c5984c1c98000288","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/3fe10c352faae434e0cca7c1c5984c1c98000288"}]},"status":"ahead","ahead_by":1,"behind_by":0,"total_commits":1,"commits":[{"sha":"5601846871b8142ab0df1e0b8774756c658bcc7d","node_id":"C_kwDOJu7MkNoAKDU2MDE4NDY4NzFiODE0MmFiMGRmMWUwYjg3NzQ3NTZjNjU4YmNjN2Q","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:16:17Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:23:50Z"},"message":"make + change","tree":{"sha":"3ca6632aeb641e579bea4391294d9ebe55c062c7","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/3ca6632aeb641e579bea4391294d9ebe55c062c7"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5601846871b8142ab0df1e0b8774756c658bcc7d","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQAmeUxSTgmd3FlcaYtRmVpuCh0n6L0/2Oy7wnsoGF6zjLoBO+aDz22vOG+bywNp4bc\nlmOl+Tu5Jk9woOhH2Olg8=\n-----END + SSH SIGNATURE-----","payload":"tree 3ca6632aeb641e579bea4391294d9ebe55c062c7\nparent + 5b174c2b40d501a70c479e91025d5109b1ad5c1b\nauthor joseph-sentry + 1692026177 -0400\ncommitter joseph-sentry 1692026630 + -0400\n\nmake change\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5601846871b8142ab0df1e0b8774756c658bcc7d","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b"}]}],"files":[{"sha":"8664ca881cd0536ab32eac8dfee923a2b304167d","filename":"api/calculator/calculator.py","status":"modified","additions":1,"deletions":3,"changes":4,"blob_url":"https://github.com/joseph-sentry/codecov-demo/blob/5601846871b8142ab0df1e0b8774756c658bcc7d/api%2Fcalculator%2Fcalculator.py","raw_url":"https://github.com/joseph-sentry/codecov-demo/raw/5601846871b8142ab0df1e0b8774756c658bcc7d/api%2Fcalculator%2Fcalculator.py","contents_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/contents/api%2Fcalculator%2Fcalculator.py?ref=5601846871b8142ab0df1e0b8774756c658bcc7d","patch":"@@ + -9,6 +9,4 @@ def multiply(x, y):\n return x * y\n \n def divide(x, + y):\n- if y == 0:\n- return ''Cannot divide by 0''\n- return + x * 1.0 / y\n\\ No newline at end of file\n+ return x * 1.0 / y"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:45:00 GMT + ETag: + - W/"649a89b4b5093758f87a21af1f020ef04640b12c19dde25535fd5962d059092d" + Last-Modified: + - Mon, 14 Aug 2023 15:23:50 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E74F:5672:1000AA:209300:64EF8E2B + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4972' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '28' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:50:41 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"state": "success", "target_url": "https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9", + "context": "codecov/patch", "description": "Coverage not affected when comparing + 5b174c2...5601846"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '203' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/statuses/5601846871b8142ab0df1e0b8774756c658bcc7d + response: + content: '{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/statuses/5601846871b8142ab0df1e0b8774756c658bcc7d","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","id":24846000025,"node_id":"SC_kwDOJu7MkM8AAAAFyO_fmQ","state":"success","description":"Coverage + not affected when comparing 5b174c2...5601846","target_url":"https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9","context":"codecov/patch","created_at":"2023-08-30T18:45:00Z","updated_at":"2023-08-30T18:45:00Z","creator":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '1478' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:45:00 GMT + ETag: + - '"6082b52302afdc137a43de77d677c72bca91f142539336b3dd79fe2658a7ce2c"' + Location: + - https://api.github.com/repos/joseph-sentry/codecov-demo/statuses/5601846871b8142ab0df1e0b8774756c658bcc7d + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E750:2FFC:F1072:1EA6B3:64EF8E2C + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4971' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '29' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:50:41 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 201 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/branches/main + response: + content: '{"name":"main","commit":{"sha":"38c2d0214f2a48c9212a140f5311977059a15b35","node_id":"C_kwDOJu7MkNoAKDM4YzJkMDIxNGYyYTQ4YzkyMTJhMTQwZjUzMTE5NzcwNTlhMTViMzU","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"message":"make + change to app\n\nSigned-off-by: joseph-sentry ","tree":{"sha":"9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/38c2d0214f2a48c9212a140f5311977059a15b35","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQJe3OYXkY7fA1+RZ130Zrab06lH36l2XC9HMqjHDAW8iCW5VUbvNhOxNpKSokDItdd\nroqDRnazyFj3+oWVCvxgM=\n-----END + SSH SIGNATURE-----","payload":"tree 9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043\nparent + 1713006782821e36912c23baa98901c7361ee83c\nauthor joseph-sentry + 1692377290 -0400\ncommitter joseph-sentry 1692377290 + -0400\n\nmake change to app\n\nSigned-off-by: joseph-sentry \n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/38c2d0214f2a48c9212a140f5311977059a15b35","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"1713006782821e36912c23baa98901c7361ee83c","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/1713006782821e36912c23baa98901c7361ee83c","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/1713006782821e36912c23baa98901c7361ee83c"}]},"_links":{"self":"https://api.github.com/repos/joseph-sentry/codecov-demo/branches/main","html":"https://github.com/joseph-sentry/codecov-demo/tree/main"},"protected":false,"protection":{"enabled":false,"required_status_checks":{"enforcement_level":"off","contexts":[],"checks":[]}},"protection_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/branches/main/protection"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:45:00 GMT + ETag: + - W/"03c43fb66e2e87ed9b951aaf1a14277a1108501b49f79548c81f11090224b5ba" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E751:13D8:117A2A:2379C9:64EF8E2C + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4970' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '30' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:50:41 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b + response: + content: '{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b","permalink_url":"https://github.com/joseph-sentry/codecov-demo/compare/joseph-sentry:38c2d02...joseph-sentry:5b174c2","diff_url":"https://github.com/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b.diff","patch_url":"https://github.com/joseph-sentry/codecov-demo/compare/38c2d0214f2a48c9212a140f5311977059a15b35...5b174c2b40d501a70c479e91025d5109b1ad5c1b.patch","base_commit":{"sha":"38c2d0214f2a48c9212a140f5311977059a15b35","node_id":"C_kwDOJu7MkNoAKDM4YzJkMDIxNGYyYTQ4YzkyMTJhMTQwZjUzMTE5NzcwNTlhMTViMzU","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"message":"make + change to app\n\nSigned-off-by: joseph-sentry ","tree":{"sha":"9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/38c2d0214f2a48c9212a140f5311977059a15b35","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQJe3OYXkY7fA1+RZ130Zrab06lH36l2XC9HMqjHDAW8iCW5VUbvNhOxNpKSokDItdd\nroqDRnazyFj3+oWVCvxgM=\n-----END + SSH SIGNATURE-----","payload":"tree 9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043\nparent + 1713006782821e36912c23baa98901c7361ee83c\nauthor joseph-sentry + 1692377290 -0400\ncommitter joseph-sentry 1692377290 + -0400\n\nmake change to app\n\nSigned-off-by: joseph-sentry \n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/38c2d0214f2a48c9212a140f5311977059a15b35","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"1713006782821e36912c23baa98901c7361ee83c","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/1713006782821e36912c23baa98901c7361ee83c","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/1713006782821e36912c23baa98901c7361ee83c"}]},"merge_base_commit":{"sha":"5b174c2b40d501a70c479e91025d5109b1ad5c1b","node_id":"C_kwDOJu7MkNoAKDViMTc0YzJiNDBkNTAxYTcwYzQ3OWU5MTAyNWQ1MTA5YjFhZDVjMWI","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-14T15:19:08Z"},"message":"make + change on main","tree":{"sha":"a9d6c66e50012a6d42f3f4c4e6d6de845230e73b","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/a9d6c66e50012a6d42f3f4c4e6d6de845230e73b"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQCSXJE75ylD7PwNrXf6ITnsvh9gIP5b/JASjGlKqB7f37XPt9wLvT2YaV3HcSyTRyt\nCY9zBOZT+tELiyZcpqKwY=\n-----END + SSH SIGNATURE-----","payload":"tree a9d6c66e50012a6d42f3f4c4e6d6de845230e73b\nparent + 3fe10c352faae434e0cca7c1c5984c1c98000288\nauthor joseph-sentry + 1692026348 -0400\ncommitter joseph-sentry 1692026348 + -0400\n\nmake change on main\n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5b174c2b40d501a70c479e91025d5109b1ad5c1b/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"3fe10c352faae434e0cca7c1c5984c1c98000288","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/3fe10c352faae434e0cca7c1c5984c1c98000288","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/3fe10c352faae434e0cca7c1c5984c1c98000288"}]},"status":"behind","ahead_by":0,"behind_by":2,"total_commits":0,"commits":[],"files":[]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:45:00 GMT + ETag: + - W/"c000dea59114b2e2fa8073bc817bc28ac3ae901be2bdbb129bd291c73eda0211" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E752:44A9:123CC6:24FB74:64EF8E2C + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4969' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '31' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:50:41 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"body": "## [Codecov](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=h1) + Report\n> Merging [#9](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=desc) + (5601846) into [main](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b?el=desc) + (5b174c2) will **not change** coverage.\n> Report is 2 commits behind head on + main.\n> The diff coverage is `n/a`.\n\n:exclamation: Your organization is not + using the GitHub App Integration. As a result you may experience degraded service + beginning May 15th. Please [install the GitHub App Integration](https://github.com/apps/codecov) + for your organization. [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sheena+Parker).\n\n[![Impacted + file tree graph](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij)](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=tree)\n\n```diff\n@@ Coverage + Diff @@\n## main #9 +/- ##\n=======================================\n Coverage 85.00% 85.00% \n=======================================\n Files 3 3 \n Lines 20 20 \n=======================================\n Hits 17 17 \n Misses 3 3 \n```\n\n| + [Flag](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flags) + | Coverage \u0394 | |\n|---|---|---|\n| [unit](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag) + | `85.00% <\u00f8> (\u00f8)` | |\n\nFlags with carried forward coverage won''t + be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sheena+Parker#carryforward-flags-in-the-pull-request-comment) + to find out more.\n\n\n------\n\n[Continue to review full report in Codecov + by Sentry](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=continue).\n> + **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sheena+Parker)\n> + `\u0394 = absolute (impact)`, `\u00f8 = not affected`, `? = missing + data`\n> Powered by [Codecov](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=footer). + Last update [5b174c2...5601846](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=lastupdated). + Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sheena+Parker).\n"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '2969' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/issues/9/comments + response: + content: "{\"url\":\"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments/1699669573\"\ + ,\"html_url\":\"https://github.com/joseph-sentry/codecov-demo/pull/9#issuecomment-1699669573\"\ + ,\"issue_url\":\"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/9\"\ + ,\"id\":1699669573,\"node_id\":\"IC_kwDOJu7MkM5lTuZF\",\"user\":{\"login\":\"\ + joseph-sentry\",\"id\":136376984,\"node_id\":\"U_kgDOCCDymA\",\"avatar_url\"\ + :\"https://avatars.githubusercontent.com/u/136376984?u=8154f2067e6b4966acba9c27358d6e49cfbbf45d&v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/joseph-sentry\"\ + ,\"html_url\":\"https://github.com/joseph-sentry\",\"followers_url\":\"https://api.github.com/users/joseph-sentry/followers\"\ + ,\"following_url\":\"https://api.github.com/users/joseph-sentry/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/joseph-sentry/gists{/gist_id}\"\ + ,\"starred_url\":\"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/joseph-sentry/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/joseph-sentry/orgs\",\"\ + repos_url\":\"https://api.github.com/users/joseph-sentry/repos\",\"events_url\"\ + :\"https://api.github.com/users/joseph-sentry/events{/privacy}\",\"received_events_url\"\ + :\"https://api.github.com/users/joseph-sentry/received_events\",\"type\":\"\ + User\",\"site_admin\":false},\"created_at\":\"2023-08-30T18:45:01Z\",\"updated_at\"\ + :\"2023-08-30T18:45:01Z\",\"author_association\":\"OWNER\",\"body\":\"## [Codecov](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=h1)\ + \ Report\\n> Merging [#9](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=desc)\ + \ (5601846) into [main](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b?el=desc)\ + \ (5b174c2) will **not change** coverage.\\n> Report is 2 commits behind head\ + \ on main.\\n> The diff coverage is `n/a`.\\n\\n:exclamation: Your organization\ + \ is not using the GitHub App Integration. As a result you may experience degraded\ + \ service beginning May 15th. Please [install the GitHub App Integration](https://github.com/apps/codecov)\ + \ for your organization. [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sheena+Parker).\\\ + n\\n[![Impacted file tree graph](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij)](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=tree)\\\ + n\\n```diff\\n@@ Coverage Diff @@\\n## main\ + \ #9 +/- ##\\n=======================================\\n Coverage\ + \ 85.00% 85.00% \\n=======================================\\n\ + \ Files 3 3 \\n Lines 20 20 \ + \ \\n=======================================\\n Hits 17 \ + \ 17 \\n Misses 3 3 \\n```\\n\\n| [Flag](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flags)\ + \ | Coverage \u0394 | |\\n|---|---|---|\\n| [unit](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag)\ + \ | `85.00% <\xF8> (\xF8)` | |\\n\\nFlags with carried forward coverage won't\ + \ be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sheena+Parker#carryforward-flags-in-the-pull-request-comment)\ + \ to find out more.\\n\\n\\n------\\n\\n[Continue to review full report in Codecov\ + \ by Sentry](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=continue).\\\ + n> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sheena+Parker)\\\ + n> `\u0394 = absolute (impact)`, `\xF8 = not affected`, `? = missing\ + \ data`\\n> Powered by [Codecov](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=footer).\ + \ Last update [5b174c2...5601846](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=lastupdated).\ + \ Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sheena+Parker).\\\ + n\",\"reactions\":{\"url\":\"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments/1699669573/reactions\"\ + ,\"total_count\":0,\"+1\":0,\"-1\":0,\"laugh\":0,\"hooray\":0,\"confused\":0,\"\ + heart\":0,\"rocket\":0,\"eyes\":0},\"performed_via_github_app\":null}" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '4610' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:45:01 GMT + ETag: + - '"ad3f2b237aded0e1db3d185e2c0668ad471be827d9159b129b0087cdcfdff72d"' + Location: + - https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments/1699669573 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E753:7D95:14DBB6:2A469F:64EF8E2C + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4968' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '32' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:50:41 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 201 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d/status?page=1&per_page=100 + response: + content: '{"state":"success","statuses":[{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/statuses/5601846871b8142ab0df1e0b8774756c658bcc7d","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","id":24842855406,"node_id":"SC_kwDOJu7MkM8AAAAFyL_j7g","state":"success","description":"No + unexpected coverage changes found","target_url":"https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9","context":"codecov/changes","created_at":"2023-08-30T16:03:04Z","updated_at":"2023-08-30T16:03:04Z"},{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/statuses/5601846871b8142ab0df1e0b8774756c658bcc7d","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","id":24844653606,"node_id":"SC_kwDOJu7MkM8AAAAFyNtUJg","state":"success","description":"85.00% + (+0.00%) compared to 5b174c2","target_url":"https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9","context":"codecov/project","created_at":"2023-08-30T17:32:52Z","updated_at":"2023-08-30T17:32:52Z"},{"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/statuses/5601846871b8142ab0df1e0b8774756c658bcc7d","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","id":24846000025,"node_id":"SC_kwDOJu7MkM8AAAAFyO_fmQ","state":"success","description":"Coverage + not affected when comparing 5b174c2...5601846","target_url":"https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9","context":"codecov/patch","created_at":"2023-08-30T18:45:00Z","updated_at":"2023-08-30T18:45:00Z"}],"sha":"5601846871b8142ab0df1e0b8774756c658bcc7d","total_count":3,"repository":{"id":653184144,"node_id":"R_kgDOJu7MkA","name":"codecov-demo","full_name":"joseph-sentry/codecov-demo","private":false,"owner":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"html_url":"https://github.com/joseph-sentry/codecov-demo","description":null,"fork":true,"url":"https://api.github.com/repos/joseph-sentry/codecov-demo","forks_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/forks","keys_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/teams","hooks_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/hooks","issue_events_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/events{/number}","events_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/events","assignees_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/assignees{/user}","branches_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/branches{/branch}","tags_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/tags","blobs_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/refs{/sha}","trees_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/statuses/{sha}","languages_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/languages","stargazers_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/stargazers","contributors_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/contributors","subscribers_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/subscribers","subscription_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/subscription","commits_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits{/sha}","git_commits_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits{/sha}","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/comments{/number}","issue_comment_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/issues/comments{/number}","contents_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/contents/{+path}","compare_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/merges","archive_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/downloads","issues_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/issues{/number}","pulls_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/pulls{/number}","milestones_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/milestones{/number}","notifications_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/labels{/name}","releases_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/releases{/id}","deployments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/deployments"},"commit_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/5601846871b8142ab0df1e0b8774756c658bcc7d/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 18:50:27 GMT + ETag: + - W/"4ba0e4b8ad7e097c9f90d3f1f8e4a5d3c5abd99c161ec3597f00e3b678f2fa30" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo, repo:status + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E81A:1F44:10C6EC:22160D:64EF8F72 + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4967' + X-RateLimit-Reset: + - '1693424656' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '33' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 15:50:41 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/apps/worker/tasks/tests/integration/cassetes/test_status_set_error_task/TestStatusSetErrorTask/test_set_error.yaml b/apps/worker/tasks/tests/integration/cassetes/test_status_set_error_task/TestStatusSetErrorTask/test_set_error.yaml new file mode 100644 index 0000000000..fab8c53c43 --- /dev/null +++ b/apps/worker/tasks/tests/integration/cassetes/test_status_set_error_task/TestStatusSetErrorTask/test_set_error.yaml @@ -0,0 +1,363 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/contents?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab + response: + content: '[{"name":".gitignore","path":".gitignore","sha":"e9428a03c7fc4ce77bba5997760efb3bb6ec42ee","size":1765,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.gitignore?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/.gitignore","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e9428a03c7fc4ce77bba5997760efb3bb6ec42ee","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/.gitignore","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.gitignore?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e9428a03c7fc4ce77bba5997760efb3bb6ec42ee","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/.gitignore"}},{"name":"Makefile","path":"Makefile","sha":"8feacc0b967da9f0be4d936d2e43a9d9c23ade8e","size":854,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/Makefile?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/Makefile","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/8feacc0b967da9f0be4d936d2e43a9d9c23ade8e","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/Makefile","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/Makefile?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/8feacc0b967da9f0be4d936d2e43a9d9c23ade8e","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/Makefile"}},{"name":"README.md","path":"README.md","sha":"2b0f425f0087389722de7ec07251431b5970a431","size":897,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.md?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/README.md","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/2b0f425f0087389722de7ec07251431b5970a431","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/README.md","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.md?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/2b0f425f0087389722de7ec07251431b5970a431","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/README.md"}},{"name":"awesome","path":"awesome","sha":"391a13a04a02e4d75b6b51291a5114ae3b359793","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/tree/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/awesome","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/391a13a04a02e4d75b6b51291a5114ae3b359793","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/391a13a04a02e4d75b6b51291a5114ae3b359793","html":"https://github.com/ThiagoCodecov/example-python/tree/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/awesome"}},{"name":"codecov.yaml","path":"codecov.yaml","sha":"e7bb8d289876a0b30be8fa103999f40dc3e0375e","size":354,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/codecov.yaml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e7bb8d289876a0b30be8fa103999f40dc3e0375e","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/codecov.yaml","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e7bb8d289876a0b30be8fa103999f40dc3e0375e","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/codecov.yaml"}},{"name":"pull12.txt","path":"pull12.txt","sha":"2ba366f955c486a371c866fc96244be8a1dede1b","size":2006,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/pull12.txt?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull12.txt","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/2ba366f955c486a371c866fc96244be8a1dede1b","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull12.txt","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/pull12.txt?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/2ba366f955c486a371c866fc96244be8a1dede1b","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull12.txt"}},{"name":"pull14.txt","path":"pull14.txt","sha":"a4d347e8697c8ab14338c781d7add6208312532b","size":1990,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/pull14.txt?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull14.txt","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/a4d347e8697c8ab14338c781d7add6208312532b","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull14.txt","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/pull14.txt?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/a4d347e8697c8ab14338c781d7add6208312532b","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull14.txt"}},{"name":"pull15.txt","path":"pull15.txt","sha":"fe9b857ea9e474bbec99b691dc9e07251a1e4109","size":1952,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/pull15.txt?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull15.txt","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/fe9b857ea9e474bbec99b691dc9e07251a1e4109","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull15.txt","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/pull15.txt?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/fe9b857ea9e474bbec99b691dc9e07251a1e4109","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull15.txt"}},{"name":"requirements.txt","path":"requirements.txt","sha":"ab0a8735f8dfaef9044d2092e6de622052069b1d","size":319,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/requirements.txt?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/requirements.txt","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/ab0a8735f8dfaef9044d2092e6de622052069b1d","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/requirements.txt","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/requirements.txt?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/ab0a8735f8dfaef9044d2092e6de622052069b1d","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/requirements.txt"}},{"name":"tests","path":"tests","sha":"e011fd105f2273c900b1ef4ec81438884a63f633","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/tree/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/tests","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e011fd105f2273c900b1ef4ec81438884a63f633","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e011fd105f2273c900b1ef4ec81438884a63f633","html":"https://github.com/ThiagoCodecov/example-python/tree/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/tests"}}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:50:29 GMT + Etag: + - W/"f6f559b56da125de3c7cad84e9055c8ae5a29d22" + Last-Modified: + - Tue, 24 Mar 2020 22:01:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EDE9:6512:1F529B:2CEAC5:5E8F5254 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4934' + X-Ratelimit-Reset: + - '1586451769' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/contents?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab + response: + content: '{"name":"codecov.yaml","path":"codecov.yaml","sha":"e7bb8d289876a0b30be8fa103999f40dc3e0375e","size":354,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/codecov.yaml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e7bb8d289876a0b30be8fa103999f40dc3e0375e","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/codecov.yaml","type":"file","content":"Y29kZWNvdjoKICBub3RpZnk6CiAgICByZXF1aXJlX2NpX3RvX3Bhc3M6IHll\ncwoKY292ZXJhZ2U6CiAgcHJlY2lzaW9uOiAyCiAgcm91bmQ6IGRvd24KICBy\nYW5nZTogIjcwLi4uMTAwIgoKICBzdGF0dXM6CiAgICBwcm9qZWN0OiB5ZXMK\nICAgIHBhdGNoOiB5ZXMKICAgIGNoYW5nZXM6IG5vCgpwYXJzZXJzOgogIGdj\nb3Y6CiAgICBicmFuY2hfZGV0ZWN0aW9uOgogICAgICBjb25kaXRpb25hbDog\neWVzCiAgICAgIGxvb3A6IHllcwogICAgICBtZXRob2Q6IG5vCiAgICAgIG1h\nY3JvOiBubwoKY29tbWVudDoKICBsYXlvdXQ6ICJoZWFkZXIsIGRpZmYiCiAg\nYmVoYXZpb3I6IGRlZmF1bHQKICByZXF1aXJlX2NoYW5nZXM6IG5v\n","encoding":"base64","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e7bb8d289876a0b30be8fa103999f40dc3e0375e","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/codecov.yaml"}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:50:29 GMT + Etag: + - W/"e7bb8d289876a0b30be8fa103999f40dc3e0375e" + Last-Modified: + - Thu, 12 Dec 2019 00:29:55 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EDEA:6511:1CA8F8:293105:5E8F5255 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4933' + X-Ratelimit-Reset: + - '1586451769' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/status?page=1&per_page=100 + response: + content: '{"state":"pending","statuses":[{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9333691206,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzMzNjkxMjA2","state":"pending","description":"Collecting + reports and waiting for CI to complete","target_url":"https://codecov.io/gh/ThiagoCodecov/example-python/commit/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","context":"codecov/project","created_at":"2020-04-09T16:47:43Z","updated_at":"2020-04-09T16:47:43Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9333691298,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzMzNjkxMjk4","state":"pending","description":"Collecting + reports and waiting for CI to complete","target_url":"https://codecov.io/gh/ThiagoCodecov/example-python/commit/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","context":"codecov/patch","created_at":"2020-04-09T16:47:43Z","updated_at":"2020-04-09T16:47:43Z"}],"sha":"e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","total_count":2,"repository":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments"},"commit_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:50:30 GMT + Etag: + - W/"bf42fcd8c5a13aac2f48cf07d6aac52c" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - repo, repo:status + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EDEB:6512:1F52AA:2CEADB:5E8F5255 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4932' + X-Ratelimit-Reset: + - '1586451770' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/status?page=1&per_page=100 +- request: + body: '{"state": "error", "target_url": "https://codecov.io/gh/ThiagoCodecov/example-python/commit/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab", + "context": "codecov/project", "description": "Test err message"}' + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9333724379,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzMzNzI0Mzc5","state":"error","description":"Test + err message","target_url":"https://codecov.io/gh/ThiagoCodecov/example-python/commit/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","context":"codecov/project","created_at":"2020-04-09T16:50:30Z","updated_at":"2020-04-09T16:50:30Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Length: + - '1493' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:50:30 GMT + Etag: + - '"a5c799bdbe4a9da7515f79599f7de1a8"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EDEC:6511:1CA900:293114:5E8F5256 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4931' + X-Ratelimit-Reset: + - '1586451769' + X-Xss-Protection: + - 1; mode=block + status: + code: 201 + message: Created + status_code: 201 + url: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab +- request: + body: '{"state": "error", "target_url": "https://codecov.io/gh/ThiagoCodecov/example-python/commit/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab", + "context": "codecov/patch", "description": "Test err message"}' + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9333724490,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzMzNzI0NDkw","state":"error","description":"Test + err message","target_url":"https://codecov.io/gh/ThiagoCodecov/example-python/commit/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","context":"codecov/patch","created_at":"2020-04-09T16:50:30Z","updated_at":"2020-04-09T16:50:30Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Length: + - '1491' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:50:31 GMT + Etag: + - '"aa5d5cdcc095765ca540e0852fea1412"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EDED:2F62:1C06E8:2859C4:5E8F5256 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4930' + X-Ratelimit-Reset: + - '1586451770' + X-Xss-Protection: + - 1; mode=block + status: + code: 201 + message: Created + status_code: 201 + url: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab +version: 1 diff --git a/apps/worker/tasks/tests/integration/cassetes/test_status_set_pending_task/TestStatusSetPendingTask/test_set_pending.yaml b/apps/worker/tasks/tests/integration/cassetes/test_status_set_pending_task/TestStatusSetPendingTask/test_set_pending.yaml new file mode 100644 index 0000000000..0147e2f868 --- /dev/null +++ b/apps/worker/tasks/tests/integration/cassetes/test_status_set_pending_task/TestStatusSetPendingTask/test_set_pending.yaml @@ -0,0 +1,363 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/contents?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab + response: + content: '[{"name":".gitignore","path":".gitignore","sha":"e9428a03c7fc4ce77bba5997760efb3bb6ec42ee","size":1765,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.gitignore?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/.gitignore","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e9428a03c7fc4ce77bba5997760efb3bb6ec42ee","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/.gitignore","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.gitignore?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e9428a03c7fc4ce77bba5997760efb3bb6ec42ee","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/.gitignore"}},{"name":"Makefile","path":"Makefile","sha":"8feacc0b967da9f0be4d936d2e43a9d9c23ade8e","size":854,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/Makefile?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/Makefile","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/8feacc0b967da9f0be4d936d2e43a9d9c23ade8e","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/Makefile","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/Makefile?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/8feacc0b967da9f0be4d936d2e43a9d9c23ade8e","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/Makefile"}},{"name":"README.md","path":"README.md","sha":"2b0f425f0087389722de7ec07251431b5970a431","size":897,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.md?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/README.md","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/2b0f425f0087389722de7ec07251431b5970a431","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/README.md","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.md?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/2b0f425f0087389722de7ec07251431b5970a431","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/README.md"}},{"name":"awesome","path":"awesome","sha":"391a13a04a02e4d75b6b51291a5114ae3b359793","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/tree/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/awesome","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/391a13a04a02e4d75b6b51291a5114ae3b359793","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/391a13a04a02e4d75b6b51291a5114ae3b359793","html":"https://github.com/ThiagoCodecov/example-python/tree/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/awesome"}},{"name":"codecov.yaml","path":"codecov.yaml","sha":"e7bb8d289876a0b30be8fa103999f40dc3e0375e","size":354,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/codecov.yaml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e7bb8d289876a0b30be8fa103999f40dc3e0375e","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/codecov.yaml","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e7bb8d289876a0b30be8fa103999f40dc3e0375e","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/codecov.yaml"}},{"name":"pull12.txt","path":"pull12.txt","sha":"2ba366f955c486a371c866fc96244be8a1dede1b","size":2006,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/pull12.txt?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull12.txt","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/2ba366f955c486a371c866fc96244be8a1dede1b","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull12.txt","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/pull12.txt?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/2ba366f955c486a371c866fc96244be8a1dede1b","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull12.txt"}},{"name":"pull14.txt","path":"pull14.txt","sha":"a4d347e8697c8ab14338c781d7add6208312532b","size":1990,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/pull14.txt?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull14.txt","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/a4d347e8697c8ab14338c781d7add6208312532b","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull14.txt","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/pull14.txt?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/a4d347e8697c8ab14338c781d7add6208312532b","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull14.txt"}},{"name":"pull15.txt","path":"pull15.txt","sha":"fe9b857ea9e474bbec99b691dc9e07251a1e4109","size":1952,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/pull15.txt?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull15.txt","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/fe9b857ea9e474bbec99b691dc9e07251a1e4109","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull15.txt","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/pull15.txt?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/fe9b857ea9e474bbec99b691dc9e07251a1e4109","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/pull15.txt"}},{"name":"requirements.txt","path":"requirements.txt","sha":"ab0a8735f8dfaef9044d2092e6de622052069b1d","size":319,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/requirements.txt?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/requirements.txt","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/ab0a8735f8dfaef9044d2092e6de622052069b1d","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/requirements.txt","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/requirements.txt?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/ab0a8735f8dfaef9044d2092e6de622052069b1d","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/requirements.txt"}},{"name":"tests","path":"tests","sha":"e011fd105f2273c900b1ef4ec81438884a63f633","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/tree/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/tests","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e011fd105f2273c900b1ef4ec81438884a63f633","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e011fd105f2273c900b1ef4ec81438884a63f633","html":"https://github.com/ThiagoCodecov/example-python/tree/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/tests"}}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:47:42 GMT + Etag: + - W/"f6f559b56da125de3c7cad84e9055c8ae5a29d22" + Last-Modified: + - Tue, 24 Mar 2020 22:01:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EDC3:05F2:223775:3014CC:5E8F51AE + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4939' + X-Ratelimit-Reset: + - '1586451770' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/contents?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab + response: + content: '{"name":"codecov.yaml","path":"codecov.yaml","sha":"e7bb8d289876a0b30be8fa103999f40dc3e0375e","size":354,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/codecov.yaml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e7bb8d289876a0b30be8fa103999f40dc3e0375e","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/codecov.yaml","type":"file","content":"Y29kZWNvdjoKICBub3RpZnk6CiAgICByZXF1aXJlX2NpX3RvX3Bhc3M6IHll\ncwoKY292ZXJhZ2U6CiAgcHJlY2lzaW9uOiAyCiAgcm91bmQ6IGRvd24KICBy\nYW5nZTogIjcwLi4uMTAwIgoKICBzdGF0dXM6CiAgICBwcm9qZWN0OiB5ZXMK\nICAgIHBhdGNoOiB5ZXMKICAgIGNoYW5nZXM6IG5vCgpwYXJzZXJzOgogIGdj\nb3Y6CiAgICBicmFuY2hfZGV0ZWN0aW9uOgogICAgICBjb25kaXRpb25hbDog\neWVzCiAgICAgIGxvb3A6IHllcwogICAgICBtZXRob2Q6IG5vCiAgICAgIG1h\nY3JvOiBubwoKY29tbWVudDoKICBsYXlvdXQ6ICJoZWFkZXIsIGRpZmYiCiAg\nYmVoYXZpb3I6IGRlZmF1bHQKICByZXF1aXJlX2NoYW5nZXM6IG5v\n","encoding":"base64","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e7bb8d289876a0b30be8fa103999f40dc3e0375e","html":"https://github.com/ThiagoCodecov/example-python/blob/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/codecov.yaml"}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:47:42 GMT + Etag: + - W/"e7bb8d289876a0b30be8fa103999f40dc3e0375e" + Last-Modified: + - Thu, 12 Dec 2019 00:29:55 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EDC4:0F75:1B4017:27B611:5E8F51AE + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4938' + X-Ratelimit-Reset: + - '1586451769' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=e3b6c976efe88b2a3781dc8157485e46bf2ac7ab +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/status?page=1&per_page=100 + response: + content: '{"state":"pending","statuses":[],"sha":"e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","total_count":0,"repository":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments"},"commit_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:47:42 GMT + Etag: + - W/"bdda6a550951f06b521ebc9838932182" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - repo, repo:status + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EDC5:05F2:22378B:3014E7:5E8F51AE + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4937' + X-Ratelimit-Reset: + - '1586451769' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/status?page=1&per_page=100 +- request: + body: '{"state": "pending", "target_url": "https://codecov.io/gh/ThiagoCodecov/example-python/commit/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab", + "context": "codecov/project", "description": "Collecting reports and waiting + for CI to complete"}' + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9333691206,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzMzNjkxMjA2","state":"pending","description":"Collecting + reports and waiting for CI to complete","target_url":"https://codecov.io/gh/ThiagoCodecov/example-python/commit/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","context":"codecov/project","created_at":"2020-04-09T16:47:43Z","updated_at":"2020-04-09T16:47:43Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Length: + - '1528' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:47:43 GMT + Etag: + - '"caece04984e5de4b88a3d4366633e3b3"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EDC6:0F73:69F5F:93565:5E8F51AE + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4936' + X-Ratelimit-Reset: + - '1586451770' + X-Xss-Protection: + - 1; mode=block + status: + code: 201 + message: Created + status_code: 201 + url: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab +- request: + body: '{"state": "pending", "target_url": "https://codecov.io/gh/ThiagoCodecov/example-python/commit/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab", + "context": "codecov/patch", "description": "Collecting reports and waiting for + CI to complete"}' + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":9333691298,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzMzNjkxMjk4","state":"pending","description":"Collecting + reports and waiting for CI to complete","target_url":"https://codecov.io/gh/ThiagoCodecov/example-python/commit/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","context":"codecov/patch","created_at":"2020-04-09T16:47:43Z","updated_at":"2020-04-09T16:47:43Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Length: + - '1526' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 09 Apr 2020 16:47:43 GMT + Etag: + - '"63b8fcacebb25247e8f68ca39b72c58f"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - EDC7:05F2:223796:3014F8:5E8F51AF + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4935' + X-Ratelimit-Reset: + - '1586451769' + X-Xss-Protection: + - 1; mode=block + status: + code: 201 + message: Created + status_code: 201 + url: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab +version: 1 diff --git a/apps/worker/tasks/tests/integration/cassetes/test_sync_pull/TestPullSyncTask/test_call_task.yaml b/apps/worker/tasks/tests/integration/cassetes/test_sync_pull/TestPullSyncTask/test_call_task.yaml new file mode 100644 index 0000000000..e3ebecf5e3 --- /dev/null +++ b/apps/worker/tasks/tests/integration/cassetes/test_sync_pull/TestPullSyncTask/test_call_task.yaml @@ -0,0 +1,746 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17","id":360077097,"node_id":"MDExOlB1bGxSZXF1ZXN0MzYwMDc3MDk3","html_url":"https://github.com/ThiagoCodecov/example-python/pull/17","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/17.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/17.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/17","number":17,"state":"closed","locked":false,"title":"Thiago/f/something","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"","created_at":"2020-01-07T16:47:39Z","updated_at":"2020-02-07T18:50:34Z","closed_at":"2020-02-07T18:50:34Z","merged_at":"2020-02-07T18:50:34Z","merge_commit_sha":"92e005b57ad5a3b68dbb757ed436756826a397c7","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/17/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/c9fb9262268f11b53c6b0682d4f9acac89bdeee5","head":{"label":"ThiagoCodecov:thiago/f/something","ref":"thiago/f/something","sha":"c9fb9262268f11b53c6b0682d4f9acac89bdeee5","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-02-09T13:51:32Z","pushed_at":"2020-02-09T13:51:30Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":174,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":4,"license":null,"forks":0,"open_issues":4,"watchers":0,"default_branch":"master"}},"base":{"label":"ThiagoCodecov:master","ref":"master","sha":"6dc3afd80a8deea5ea949d284d996d58811cd01d","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-02-09T13:51:32Z","pushed_at":"2020-02-09T13:51:30Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":174,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":4,"license":null,"forks":0,"open_issues":4,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/17"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/17"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/17/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/c9fb9262268f11b53c6b0682d4f9acac89bdeee5"}},"author_association":"OWNER","merged":true,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"comments":5,"review_comments":0,"maintainer_can_modify":false,"commits":3,"additions":24,"deletions":0,"changed_files":2}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 18 Feb 2020 18:29:08 GMT + Etag: + - W/"2ca126a031f469a3d39b98287323a98f" + Last-Modified: + - Fri, 07 Feb 2020 18:50:34 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 1380:3C7C:1D3ED9:2A56C0:5E4C2CF3 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4996' + X-Ratelimit-Reset: + - '1582053098' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17/commits + response: + content: '[{"sha":"e2de2b261cfd934b45babfe241012ddd129ff8ed","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmUyZGUyYjI2MWNmZDkzNGI0NWJhYmZlMjQxMDEyZGRkMTI5ZmY4ZWQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-07T16:47:05Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-07T16:47:05Z"},"message":"5609B3F3-74E2-42AE-B298-51B8280371C6","tree":{"sha":"50f132271f74df3d310ab2eef0eb6f8b0e993c9f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/50f132271f74df3d310ab2eef0eb6f8b0e993c9f"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e2de2b261cfd934b45babfe241012ddd129ff8ed","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e2de2b261cfd934b45babfe241012ddd129ff8ed","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e2de2b261cfd934b45babfe241012ddd129ff8ed","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e2de2b261cfd934b45babfe241012ddd129ff8ed/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6dc3afd80a8deea5ea949d284d996d58811cd01d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6dc3afd80a8deea5ea949d284d996d58811cd01d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6dc3afd80a8deea5ea949d284d996d58811cd01d"}]},{"sha":"7a7153d24f76c9ad58f421bcac8276203d589b1a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjdhNzE1M2QyNGY3NmM5YWQ1OGY0MjFiY2FjODI3NjIwM2Q1ODliMWE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-07T16:47:05Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-07T16:47:05Z"},"message":"C1B0D231-650E-4BA2-A5F1-A6EDB515B073","tree":{"sha":"e7541dee241f0084972697991609f7dfc4e2adac","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e7541dee241f0084972697991609f7dfc4e2adac"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/7a7153d24f76c9ad58f421bcac8276203d589b1a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7a7153d24f76c9ad58f421bcac8276203d589b1a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7a7153d24f76c9ad58f421bcac8276203d589b1a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7a7153d24f76c9ad58f421bcac8276203d589b1a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e2de2b261cfd934b45babfe241012ddd129ff8ed","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e2de2b261cfd934b45babfe241012ddd129ff8ed","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e2de2b261cfd934b45babfe241012ddd129ff8ed"}]},{"sha":"c9fb9262268f11b53c6b0682d4f9acac89bdeee5","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmM5ZmI5MjYyMjY4ZjExYjUzYzZiMDY4MmQ0ZjlhY2FjODliZGVlZTU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-10T22:13:45Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-07T18:49:56Z"},"message":"Adding + carryforward flag","tree":{"sha":"2138c0d1229898b4ee4269cc95e3837af4386bc9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2138c0d1229898b4ee4269cc95e3837af4386bc9"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c9fb9262268f11b53c6b0682d4f9acac89bdeee5","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c9fb9262268f11b53c6b0682d4f9acac89bdeee5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c9fb9262268f11b53c6b0682d4f9acac89bdeee5","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c9fb9262268f11b53c6b0682d4f9acac89bdeee5/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"7a7153d24f76c9ad58f421bcac8276203d589b1a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7a7153d24f76c9ad58f421bcac8276203d589b1a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7a7153d24f76c9ad58f421bcac8276203d589b1a"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 18 Feb 2020 18:29:08 GMT + Etag: + - W/"96fcb95adc6219c9c9ad009879639161" + Last-Modified: + - Fri, 07 Feb 2020 18:50:34 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 1381:3C56:1E06BA:2AD069:5E4C2CF4 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4995' + X-Ratelimit-Reset: + - '1582053097' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17/commits +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/contents?ref=thiago%2Ff%2Fsomething + response: + content: '[{"name":".gitignore","path":".gitignore","sha":"e9428a03c7fc4ce77bba5997760efb3bb6ec42ee","size":1765,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.gitignore?ref=thiago/f/something","html_url":"https://github.com/ThiagoCodecov/example-python/blob/thiago/f/something/.gitignore","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e9428a03c7fc4ce77bba5997760efb3bb6ec42ee","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/thiago/f/something/.gitignore","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.gitignore?ref=thiago/f/something","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e9428a03c7fc4ce77bba5997760efb3bb6ec42ee","html":"https://github.com/ThiagoCodecov/example-python/blob/thiago/f/something/.gitignore"}},{"name":"Makefile","path":"Makefile","sha":"8feacc0b967da9f0be4d936d2e43a9d9c23ade8e","size":854,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/Makefile?ref=thiago/f/something","html_url":"https://github.com/ThiagoCodecov/example-python/blob/thiago/f/something/Makefile","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/8feacc0b967da9f0be4d936d2e43a9d9c23ade8e","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/thiago/f/something/Makefile","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/Makefile?ref=thiago/f/something","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/8feacc0b967da9f0be4d936d2e43a9d9c23ade8e","html":"https://github.com/ThiagoCodecov/example-python/blob/thiago/f/something/Makefile"}},{"name":"README.md","path":"README.md","sha":"c95c940fe9dc269c9f4d150c4e795221476428a3","size":1304,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.md?ref=thiago/f/something","html_url":"https://github.com/ThiagoCodecov/example-python/blob/thiago/f/something/README.md","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/c95c940fe9dc269c9f4d150c4e795221476428a3","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/thiago/f/something/README.md","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.md?ref=thiago/f/something","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/c95c940fe9dc269c9f4d150c4e795221476428a3","html":"https://github.com/ThiagoCodecov/example-python/blob/thiago/f/something/README.md"}},{"name":"awesome","path":"awesome","sha":"391a13a04a02e4d75b6b51291a5114ae3b359793","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=thiago/f/something","html_url":"https://github.com/ThiagoCodecov/example-python/tree/thiago/f/something/awesome","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/391a13a04a02e4d75b6b51291a5114ae3b359793","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=thiago/f/something","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/391a13a04a02e4d75b6b51291a5114ae3b359793","html":"https://github.com/ThiagoCodecov/example-python/tree/thiago/f/something/awesome"}},{"name":"codecov.yaml","path":"codecov.yaml","sha":"842e646f82cbcbb30cd0da6ede1f417a32939182","size":396,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=thiago/f/something","html_url":"https://github.com/ThiagoCodecov/example-python/blob/thiago/f/something/codecov.yaml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/842e646f82cbcbb30cd0da6ede1f417a32939182","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/thiago/f/something/codecov.yaml","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=thiago/f/something","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/842e646f82cbcbb30cd0da6ede1f417a32939182","html":"https://github.com/ThiagoCodecov/example-python/blob/thiago/f/something/codecov.yaml"}},{"name":"requirements.txt","path":"requirements.txt","sha":"ab0a8735f8dfaef9044d2092e6de622052069b1d","size":319,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/requirements.txt?ref=thiago/f/something","html_url":"https://github.com/ThiagoCodecov/example-python/blob/thiago/f/something/requirements.txt","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/ab0a8735f8dfaef9044d2092e6de622052069b1d","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/thiago/f/something/requirements.txt","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/requirements.txt?ref=thiago/f/something","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/ab0a8735f8dfaef9044d2092e6de622052069b1d","html":"https://github.com/ThiagoCodecov/example-python/blob/thiago/f/something/requirements.txt"}},{"name":"tests","path":"tests","sha":"e011fd105f2273c900b1ef4ec81438884a63f633","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=thiago/f/something","html_url":"https://github.com/ThiagoCodecov/example-python/tree/thiago/f/something/tests","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e011fd105f2273c900b1ef4ec81438884a63f633","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=thiago/f/something","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e011fd105f2273c900b1ef4ec81438884a63f633","html":"https://github.com/ThiagoCodecov/example-python/tree/thiago/f/something/tests"}}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 18 Feb 2020 18:29:09 GMT + Etag: + - W/"2138c0d1229898b4ee4269cc95e3837af4386bc9" + Last-Modified: + - Sun, 09 Feb 2020 13:51:32 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 1382:3C56:1E06C6:2AD077:5E4C2CF5 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4994' + X-Ratelimit-Reset: + - '1582053098' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/contents?ref=thiago%2Ff%2Fsomething +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=thiago%2Ff%2Fsomething + response: + content: '{"name":"codecov.yaml","path":"codecov.yaml","sha":"842e646f82cbcbb30cd0da6ede1f417a32939182","size":396,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=thiago/f/something","html_url":"https://github.com/ThiagoCodecov/example-python/blob/thiago/f/something/codecov.yaml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/842e646f82cbcbb30cd0da6ede1f417a32939182","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/thiago/f/something/codecov.yaml","type":"file","content":"Y29kZWNvdjoKICBub3RpZnk6CiAgICByZXF1aXJlX2NpX3RvX3Bhc3M6IHll\ncwoKY292ZXJhZ2U6CiAgcHJlY2lzaW9uOiAyCiAgcm91bmQ6IGRvd24KICBy\nYW5nZTogIjcwLi4uMTAwIgoKICBzdGF0dXM6CiAgICBwcm9qZWN0OiB5ZXMK\nICAgIHBhdGNoOiB5ZXMKICAgIGNoYW5nZXM6IG5vCgpwYXJzZXJzOgogIGdj\nb3Y6CiAgICBicmFuY2hfZGV0ZWN0aW9uOgogICAgICBjb25kaXRpb25hbDog\neWVzCiAgICAgIGxvb3A6IHllcwogICAgICBtZXRob2Q6IG5vCiAgICAgIG1h\nY3JvOiBubwoKZmxhZ3M6CiAgZmxhZ29uZToKICAgIGNhcnJ5Zm9yd2FyZDog\ndHJ1ZQoKY29tbWVudDoKICBsYXlvdXQ6ICJoZWFkZXIsIGRpZmYiCiAgYmVo\nYXZpb3I6IGRlZmF1bHQKICByZXF1aXJlX2NoYW5nZXM6IG5v\n","encoding":"base64","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=thiago/f/something","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/842e646f82cbcbb30cd0da6ede1f417a32939182","html":"https://github.com/ThiagoCodecov/example-python/blob/thiago/f/something/codecov.yaml"}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 18 Feb 2020 18:29:09 GMT + Etag: + - W/"842e646f82cbcbb30cd0da6ede1f417a32939182" + Last-Modified: + - Fri, 07 Feb 2020 18:49:56 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 1383:3C52:1950AF:240811:5E4C2CF5 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4993' + X-Ratelimit-Reset: + - '1582053098' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=thiago%2Ff%2Fsomething +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17","id":360077097,"node_id":"MDExOlB1bGxSZXF1ZXN0MzYwMDc3MDk3","html_url":"https://github.com/ThiagoCodecov/example-python/pull/17","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/17.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/17.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/17","number":17,"state":"closed","locked":false,"title":"Thiago/f/something","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"","created_at":"2020-01-07T16:47:39Z","updated_at":"2020-02-07T18:50:34Z","closed_at":"2020-02-07T18:50:34Z","merged_at":"2020-02-07T18:50:34Z","merge_commit_sha":"92e005b57ad5a3b68dbb757ed436756826a397c7","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/17/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/c9fb9262268f11b53c6b0682d4f9acac89bdeee5","head":{"label":"ThiagoCodecov:thiago/f/something","ref":"thiago/f/something","sha":"c9fb9262268f11b53c6b0682d4f9acac89bdeee5","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-02-09T13:51:32Z","pushed_at":"2020-02-09T13:51:30Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":174,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":4,"license":null,"forks":0,"open_issues":4,"watchers":0,"default_branch":"master"}},"base":{"label":"ThiagoCodecov:master","ref":"master","sha":"6dc3afd80a8deea5ea949d284d996d58811cd01d","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-02-09T13:51:32Z","pushed_at":"2020-02-09T13:51:30Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":174,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":4,"license":null,"forks":0,"open_issues":4,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/17"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/17"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/17/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/c9fb9262268f11b53c6b0682d4f9acac89bdeee5"}},"author_association":"OWNER","merged":true,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"comments":5,"review_comments":0,"maintainer_can_modify":false,"commits":3,"additions":24,"deletions":0,"changed_files":2}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 18 Feb 2020 18:29:10 GMT + Etag: + - W/"2ca126a031f469a3d39b98287323a98f" + Last-Modified: + - Fri, 07 Feb 2020 18:50:34 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 1384:3C4F:7A798:9DE20:5E4C2CF5 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4992' + X-Ratelimit-Reset: + - '1582053098' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17/commits + response: + content: '[{"sha":"e2de2b261cfd934b45babfe241012ddd129ff8ed","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmUyZGUyYjI2MWNmZDkzNGI0NWJhYmZlMjQxMDEyZGRkMTI5ZmY4ZWQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-07T16:47:05Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-07T16:47:05Z"},"message":"5609B3F3-74E2-42AE-B298-51B8280371C6","tree":{"sha":"50f132271f74df3d310ab2eef0eb6f8b0e993c9f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/50f132271f74df3d310ab2eef0eb6f8b0e993c9f"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e2de2b261cfd934b45babfe241012ddd129ff8ed","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e2de2b261cfd934b45babfe241012ddd129ff8ed","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e2de2b261cfd934b45babfe241012ddd129ff8ed","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e2de2b261cfd934b45babfe241012ddd129ff8ed/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6dc3afd80a8deea5ea949d284d996d58811cd01d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6dc3afd80a8deea5ea949d284d996d58811cd01d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6dc3afd80a8deea5ea949d284d996d58811cd01d"}]},{"sha":"7a7153d24f76c9ad58f421bcac8276203d589b1a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjdhNzE1M2QyNGY3NmM5YWQ1OGY0MjFiY2FjODI3NjIwM2Q1ODliMWE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-07T16:47:05Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-07T16:47:05Z"},"message":"C1B0D231-650E-4BA2-A5F1-A6EDB515B073","tree":{"sha":"e7541dee241f0084972697991609f7dfc4e2adac","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e7541dee241f0084972697991609f7dfc4e2adac"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/7a7153d24f76c9ad58f421bcac8276203d589b1a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7a7153d24f76c9ad58f421bcac8276203d589b1a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7a7153d24f76c9ad58f421bcac8276203d589b1a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7a7153d24f76c9ad58f421bcac8276203d589b1a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e2de2b261cfd934b45babfe241012ddd129ff8ed","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e2de2b261cfd934b45babfe241012ddd129ff8ed","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e2de2b261cfd934b45babfe241012ddd129ff8ed"}]},{"sha":"c9fb9262268f11b53c6b0682d4f9acac89bdeee5","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmM5ZmI5MjYyMjY4ZjExYjUzYzZiMDY4MmQ0ZjlhY2FjODliZGVlZTU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-10T22:13:45Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-07T18:49:56Z"},"message":"Adding + carryforward flag","tree":{"sha":"2138c0d1229898b4ee4269cc95e3837af4386bc9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2138c0d1229898b4ee4269cc95e3837af4386bc9"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c9fb9262268f11b53c6b0682d4f9acac89bdeee5","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c9fb9262268f11b53c6b0682d4f9acac89bdeee5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c9fb9262268f11b53c6b0682d4f9acac89bdeee5","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c9fb9262268f11b53c6b0682d4f9acac89bdeee5/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"7a7153d24f76c9ad58f421bcac8276203d589b1a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7a7153d24f76c9ad58f421bcac8276203d589b1a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7a7153d24f76c9ad58f421bcac8276203d589b1a"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 18 Feb 2020 18:29:10 GMT + Etag: + - W/"96fcb95adc6219c9c9ad009879639161" + Last-Modified: + - Fri, 07 Feb 2020 18:50:34 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 1385:50C3:1EA2D7:2BCF16:5E4C2CF6 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4991' + X-Ratelimit-Reset: + - '1582053098' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17/commits +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/compare/6dc3afd80a8deea5ea949d284d996d58811cd01d...6dc3afd80a8deea5ea949d284d996d58811cd01d + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/6dc3afd80a8deea5ea949d284d996d58811cd01d...6dc3afd80a8deea5ea949d284d996d58811cd01d","html_url":"https://github.com/ThiagoCodecov/example-python/compare/6dc3afd80a8deea5ea949d284d996d58811cd01d...6dc3afd80a8deea5ea949d284d996d58811cd01d","permalink_url":"https://github.com/ThiagoCodecov/example-python/compare/ThiagoCodecov:6dc3afd...ThiagoCodecov:6dc3afd","diff_url":"https://github.com/ThiagoCodecov/example-python/compare/6dc3afd80a8deea5ea949d284d996d58811cd01d...6dc3afd80a8deea5ea949d284d996d58811cd01d.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/compare/6dc3afd80a8deea5ea949d284d996d58811cd01d...6dc3afd80a8deea5ea949d284d996d58811cd01d.patch","base_commit":{"sha":"6dc3afd80a8deea5ea949d284d996d58811cd01d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjZkYzNhZmQ4MGE4ZGVlYTVlYTk0OWQyODRkOTk2ZDU4ODExY2QwMWQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-07T04:55:14Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-07T04:55:14Z"},"message":"8C93D40F-9BD0-4C70-A474-3DE83B491330","tree":{"sha":"777f74a8f509fb4aa173678312251ca2f9d01698","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/777f74a8f509fb4aa173678312251ca2f9d01698"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6dc3afd80a8deea5ea949d284d996d58811cd01d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6dc3afd80a8deea5ea949d284d996d58811cd01d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6dc3afd80a8deea5ea949d284d996d58811cd01d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6dc3afd80a8deea5ea949d284d996d58811cd01d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"10af123faca189b7589f068ec80ce39356a6b2c0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/10af123faca189b7589f068ec80ce39356a6b2c0","html_url":"https://github.com/ThiagoCodecov/example-python/commit/10af123faca189b7589f068ec80ce39356a6b2c0"}]},"merge_base_commit":{"sha":"6dc3afd80a8deea5ea949d284d996d58811cd01d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjZkYzNhZmQ4MGE4ZGVlYTVlYTk0OWQyODRkOTk2ZDU4ODExY2QwMWQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-07T04:55:14Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-07T04:55:14Z"},"message":"8C93D40F-9BD0-4C70-A474-3DE83B491330","tree":{"sha":"777f74a8f509fb4aa173678312251ca2f9d01698","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/777f74a8f509fb4aa173678312251ca2f9d01698"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6dc3afd80a8deea5ea949d284d996d58811cd01d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6dc3afd80a8deea5ea949d284d996d58811cd01d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6dc3afd80a8deea5ea949d284d996d58811cd01d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6dc3afd80a8deea5ea949d284d996d58811cd01d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"10af123faca189b7589f068ec80ce39356a6b2c0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/10af123faca189b7589f068ec80ce39356a6b2c0","html_url":"https://github.com/ThiagoCodecov/example-python/commit/10af123faca189b7589f068ec80ce39356a6b2c0"}]},"status":"identical","ahead_by":0,"behind_by":0,"total_commits":0,"commits":[],"files":[]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 18 Feb 2020 18:29:11 GMT + Etag: + - W/"7338d2a0b4ec6206ecce8d1382ed7e4e" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 1387:50C3:1EA2E9:2BCF2A:5E4C2CF6 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4990' + X-Ratelimit-Reset: + - '1582053098' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/compare/6dc3afd80a8deea5ea949d284d996d58811cd01d...6dc3afd80a8deea5ea949d284d996d58811cd01d +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17/commits + response: + content: '[{"sha":"e2de2b261cfd934b45babfe241012ddd129ff8ed","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmUyZGUyYjI2MWNmZDkzNGI0NWJhYmZlMjQxMDEyZGRkMTI5ZmY4ZWQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-07T16:47:05Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-07T16:47:05Z"},"message":"5609B3F3-74E2-42AE-B298-51B8280371C6","tree":{"sha":"50f132271f74df3d310ab2eef0eb6f8b0e993c9f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/50f132271f74df3d310ab2eef0eb6f8b0e993c9f"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e2de2b261cfd934b45babfe241012ddd129ff8ed","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e2de2b261cfd934b45babfe241012ddd129ff8ed","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e2de2b261cfd934b45babfe241012ddd129ff8ed","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e2de2b261cfd934b45babfe241012ddd129ff8ed/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6dc3afd80a8deea5ea949d284d996d58811cd01d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6dc3afd80a8deea5ea949d284d996d58811cd01d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6dc3afd80a8deea5ea949d284d996d58811cd01d"}]},{"sha":"7a7153d24f76c9ad58f421bcac8276203d589b1a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjdhNzE1M2QyNGY3NmM5YWQ1OGY0MjFiY2FjODI3NjIwM2Q1ODliMWE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-07T16:47:05Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-07T16:47:05Z"},"message":"C1B0D231-650E-4BA2-A5F1-A6EDB515B073","tree":{"sha":"e7541dee241f0084972697991609f7dfc4e2adac","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e7541dee241f0084972697991609f7dfc4e2adac"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/7a7153d24f76c9ad58f421bcac8276203d589b1a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7a7153d24f76c9ad58f421bcac8276203d589b1a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7a7153d24f76c9ad58f421bcac8276203d589b1a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7a7153d24f76c9ad58f421bcac8276203d589b1a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e2de2b261cfd934b45babfe241012ddd129ff8ed","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e2de2b261cfd934b45babfe241012ddd129ff8ed","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e2de2b261cfd934b45babfe241012ddd129ff8ed"}]},{"sha":"c9fb9262268f11b53c6b0682d4f9acac89bdeee5","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmM5ZmI5MjYyMjY4ZjExYjUzYzZiMDY4MmQ0ZjlhY2FjODliZGVlZTU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-01-10T22:13:45Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-07T18:49:56Z"},"message":"Adding + carryforward flag","tree":{"sha":"2138c0d1229898b4ee4269cc95e3837af4386bc9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2138c0d1229898b4ee4269cc95e3837af4386bc9"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c9fb9262268f11b53c6b0682d4f9acac89bdeee5","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c9fb9262268f11b53c6b0682d4f9acac89bdeee5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c9fb9262268f11b53c6b0682d4f9acac89bdeee5","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c9fb9262268f11b53c6b0682d4f9acac89bdeee5/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"7a7153d24f76c9ad58f421bcac8276203d589b1a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7a7153d24f76c9ad58f421bcac8276203d589b1a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7a7153d24f76c9ad58f421bcac8276203d589b1a"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 18 Feb 2020 18:29:11 GMT + Etag: + - W/"96fcb95adc6219c9c9ad009879639161" + Last-Modified: + - Fri, 07 Feb 2020 18:50:34 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 1388:50C3:1EA2FA:2BCF40:5E4C2CF7 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4989' + X-Ratelimit-Reset: + - '1582053098' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/17/commits +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits?sha=master + response: + content: '[{"sha":"f0895290dc26668faeeb20ee5ccd4cc995925775","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmYwODk1MjkwZGMyNjY2OGZhZWViMjBlZTVjY2Q0Y2M5OTU5MjU3NzU=","commit":{"author":{"name":"Thiago","email":"44376991+ThiagoCodecov@users.noreply.github.com","date":"2020-03-24T22:01:33Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2020-03-24T22:01:33Z"},"message":"Merge + pull request #12 from ThiagoCodecov/thiago/f/cool-branch\n\nThiago/f/cool branch","tree":{"sha":"cda64d688c17ae3fcf03b22ee869238002e410f0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/cda64d688c17ae3fcf03b22ee869238002e410f0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f0895290dc26668faeeb20ee5ccd4cc995925775","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJeeoM+CRBK7hj4Ov3rIwAAdHIIAA7IxV7nFuDJIEFyQpE9XGAA\nMI6C/eXQp7/K6J2xwbFhUUDhkuXlbiiVvPw59STKg6qvlZ937FZBc4BfzM5OJIhY\n2fQcZnqJATD7cskHakK1+X6fmDKjN9u/KR7EmWfgjzst+cFYExptPJ7z3XktMKu9\nS7WaomPPF4lAAtDScCYb1IUILDKK5oiQMeUVJVzXyiuD9jPwjE/xo8a6Hpkvir0Q\npfjFWMuIgj9MnEv7+IZIOL//upOljWKpgoZfMBszinrF1okEtsFgeRpFXkmyVLGd\nmJa+ElmnO0TpIBtV8t/O6+eAFoc9pW3hFCuWNsIBQBhW2py5Q3gQ1hPyHdGzpUY=\n=P/w8\n-----END + PGP SIGNATURE-----\n","payload":"tree cda64d688c17ae3fcf03b22ee869238002e410f0\nparent + 081d91921f05a8a39d39aef667eddb88e96300c7\nparent 89f3d20f2be3fb2d098e544814f8ce3636dc78c4\nauthor + Thiago <44376991+ThiagoCodecov@users.noreply.github.com> 1585087293 -0300\ncommitter + GitHub 1585087293 -0300\n\nMerge pull request #12 from + ThiagoCodecov/thiago/f/cool-branch\n\nThiago/f/cool branch"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f0895290dc26668faeeb20ee5ccd4cc995925775","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars3.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"081d91921f05a8a39d39aef667eddb88e96300c7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/081d91921f05a8a39d39aef667eddb88e96300c7"},{"sha":"89f3d20f2be3fb2d098e544814f8ce3636dc78c4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/89f3d20f2be3fb2d098e544814f8ce3636dc78c4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/89f3d20f2be3fb2d098e544814f8ce3636dc78c4"}]},{"sha":"081d91921f05a8a39d39aef667eddb88e96300c7","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA4MWQ5MTkyMWYwNWE4YTM5ZDM5YWVmNjY3ZWRkYjg4ZTk2MzAwYzc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"message":"182B7277-7D2C-420B-B005-92418CBD6F09","tree":{"sha":"e6af1eb1d589c63cf9cc5caf7b80024a30c2e892","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e6af1eb1d589c63cf9cc5caf7b80024a30c2e892"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/081d91921f05a8a39d39aef667eddb88e96300c7","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/081d91921f05a8a39d39aef667eddb88e96300c7","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/081d91921f05a8a39d39aef667eddb88e96300c7/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f60187a642531c18d7af0b0f1d37294b809081fb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f60187a642531c18d7af0b0f1d37294b809081fb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f60187a642531c18d7af0b0f1d37294b809081fb"}]},{"sha":"f60187a642531c18d7af0b0f1d37294b809081fb","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmY2MDE4N2E2NDI1MzFjMThkN2FmMGIwZjFkMzcyOTRiODA5MDgxZmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-04T05:30:27Z"},"message":"8E855C85-E884-4187-A673-0D77F3F379A1","tree":{"sha":"a7faf41fa25aaae304a34ff7b5bca7631fc4cb53","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/a7faf41fa25aaae304a34ff7b5bca7631fc4cb53"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f60187a642531c18d7af0b0f1d37294b809081fb","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f60187a642531c18d7af0b0f1d37294b809081fb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f60187a642531c18d7af0b0f1d37294b809081fb","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f60187a642531c18d7af0b0f1d37294b809081fb/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"3c5cd56dab46b4f8b79fd6e737ed9c94b1825b92","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3c5cd56dab46b4f8b79fd6e737ed9c94b1825b92","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3c5cd56dab46b4f8b79fd6e737ed9c94b1825b92"}]},{"sha":"3c5cd56dab46b4f8b79fd6e737ed9c94b1825b92","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjNjNWNkNTZkYWI0NmI0ZjhiNzlmZDZlNzM3ZWQ5Yzk0YjE4MjViOTI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-03T18:14:52Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-03T18:14:52Z"},"message":"A8FC2A63-27CD-4C2F-868C-28F728EB8EEF","tree":{"sha":"62b8621466436112d5c2132e8976bceea45fb9d5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/62b8621466436112d5c2132e8976bceea45fb9d5"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3c5cd56dab46b4f8b79fd6e737ed9c94b1825b92","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3c5cd56dab46b4f8b79fd6e737ed9c94b1825b92","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3c5cd56dab46b4f8b79fd6e737ed9c94b1825b92","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3c5cd56dab46b4f8b79fd6e737ed9c94b1825b92/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"c51de784dc9bb8299601dc7c3b9962823d1caed7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c51de784dc9bb8299601dc7c3b9962823d1caed7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c51de784dc9bb8299601dc7c3b9962823d1caed7"}]},{"sha":"c51de784dc9bb8299601dc7c3b9962823d1caed7","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmM1MWRlNzg0ZGM5YmI4Mjk5NjAxZGM3YzNiOTk2MjgyM2QxY2FlZDc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-03T18:14:52Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-03T18:14:52Z"},"message":"EC5B1F9F-2293-4947-8E01-62F6F828E139","tree":{"sha":"e38ea4781a8b53bcc7c47d8ce64d19b1b2e0d6d6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e38ea4781a8b53bcc7c47d8ce64d19b1b2e0d6d6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c51de784dc9bb8299601dc7c3b9962823d1caed7","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c51de784dc9bb8299601dc7c3b9962823d1caed7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c51de784dc9bb8299601dc7c3b9962823d1caed7","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c51de784dc9bb8299601dc7c3b9962823d1caed7/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"eb3442f75fdf8851ec17315d8dad49f848f178ba","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/eb3442f75fdf8851ec17315d8dad49f848f178ba","html_url":"https://github.com/ThiagoCodecov/example-python/commit/eb3442f75fdf8851ec17315d8dad49f848f178ba"}]},{"sha":"eb3442f75fdf8851ec17315d8dad49f848f178ba","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmViMzQ0MmY3NWZkZjg4NTFlYzE3MzE1ZDhkYWQ0OWY4NDhmMTc4YmE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-03T18:08:26Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-03T18:08:26Z"},"message":"44521915-9E24-4E78-A553-8696280B1CE2","tree":{"sha":"c0acf59027eac0249987aef73281fc9d87186109","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c0acf59027eac0249987aef73281fc9d87186109"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/eb3442f75fdf8851ec17315d8dad49f848f178ba","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/eb3442f75fdf8851ec17315d8dad49f848f178ba","html_url":"https://github.com/ThiagoCodecov/example-python/commit/eb3442f75fdf8851ec17315d8dad49f848f178ba","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/eb3442f75fdf8851ec17315d8dad49f848f178ba/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","html_url":"https://github.com/ThiagoCodecov/example-python/commit/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382"}]},{"sha":"702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjcwMmQwNWZkM2U1N2ExZDdkMWU0YTVlM2UzYTAwMTdmZTI1NzEzODI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-03T18:08:26Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-03T18:08:26Z"},"message":"5B7972E5-0E8A-4134-B6F4-6AD44FE97085","tree":{"sha":"36b7bd556feb199e5d51903a59d10c877b8b4fdf","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/36b7bd556feb199e5d51903a59d10c877b8b4fdf"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","html_url":"https://github.com/ThiagoCodecov/example-python/commit/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"3c8f4787ff8538512561e3cc9bf379a6e9a65497","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3c8f4787ff8538512561e3cc9bf379a6e9a65497","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3c8f4787ff8538512561e3cc9bf379a6e9a65497"}]},{"sha":"3c8f4787ff8538512561e3cc9bf379a6e9a65497","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjNjOGY0Nzg3ZmY4NTM4NTEyNTYxZTNjYzliZjM3OWE2ZTlhNjU0OTc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-03T02:26:19Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-03T02:26:19Z"},"message":"FCFA1979-26B8-4048-AF74-FE6DA53C96B6","tree":{"sha":"df69d1a03da414dc772a805e65c753c143320b3c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/df69d1a03da414dc772a805e65c753c143320b3c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3c8f4787ff8538512561e3cc9bf379a6e9a65497","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3c8f4787ff8538512561e3cc9bf379a6e9a65497","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3c8f4787ff8538512561e3cc9bf379a6e9a65497","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3c8f4787ff8538512561e3cc9bf379a6e9a65497/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f433b24f93523ec2057bbe5f1e8af5eed963d052","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f433b24f93523ec2057bbe5f1e8af5eed963d052","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f433b24f93523ec2057bbe5f1e8af5eed963d052"}]},{"sha":"f433b24f93523ec2057bbe5f1e8af5eed963d052","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmY0MzNiMjRmOTM1MjNlYzIwNTdiYmU1ZjFlOGFmNWVlZDk2M2QwNTI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-03T02:26:19Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-03T02:26:19Z"},"message":"23F7D7DC-F29C-47A5-BEA9-D2C600255F32","tree":{"sha":"e0f4a39de24c662ed47aba05d6fad8eccec0618a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e0f4a39de24c662ed47aba05d6fad8eccec0618a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f433b24f93523ec2057bbe5f1e8af5eed963d052","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f433b24f93523ec2057bbe5f1e8af5eed963d052","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f433b24f93523ec2057bbe5f1e8af5eed963d052","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f433b24f93523ec2057bbe5f1e8af5eed963d052/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ab7623620dd24527474ba691f3f589be256d360a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ab7623620dd24527474ba691f3f589be256d360a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ab7623620dd24527474ba691f3f589be256d360a"}]},{"sha":"ab7623620dd24527474ba691f3f589be256d360a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmFiNzYyMzYyMGRkMjQ1Mjc0NzRiYTY5MWYzZjU4OWJlMjU2ZDM2MGE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-03T02:24:35Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-03T02:24:35Z"},"message":"F205752F-E9E5-4B41-8334-8366A8F44EA6","tree":{"sha":"1ce2c5a2b1679092b0adb0ab2fb505a2ff402976","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/1ce2c5a2b1679092b0adb0ab2fb505a2ff402976"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ab7623620dd24527474ba691f3f589be256d360a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ab7623620dd24527474ba691f3f589be256d360a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ab7623620dd24527474ba691f3f589be256d360a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ab7623620dd24527474ba691f3f589be256d360a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"9c76dc6ee797cc0bf3fd9e2888b550c7b146e606","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9c76dc6ee797cc0bf3fd9e2888b550c7b146e606","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9c76dc6ee797cc0bf3fd9e2888b550c7b146e606"}]},{"sha":"9c76dc6ee797cc0bf3fd9e2888b550c7b146e606","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjljNzZkYzZlZTc5N2NjMGJmM2ZkOWUyODg4YjU1MGM3YjE0NmU2MDY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-03T02:24:35Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-03-03T02:24:35Z"},"message":"C989EE78-765E-479B-B8D5-8EDF99EAB6D0","tree":{"sha":"61b70727b2e043b194b2d88fd26ff92e931d7284","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/61b70727b2e043b194b2d88fd26ff92e931d7284"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/9c76dc6ee797cc0bf3fd9e2888b550c7b146e606","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9c76dc6ee797cc0bf3fd9e2888b550c7b146e606","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9c76dc6ee797cc0bf3fd9e2888b550c7b146e606","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9c76dc6ee797cc0bf3fd9e2888b550c7b146e606/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"c18fde3266d9fc4a392d8f9d86d2e2740c08260e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c18fde3266d9fc4a392d8f9d86d2e2740c08260e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c18fde3266d9fc4a392d8f9d86d2e2740c08260e"}]},{"sha":"c18fde3266d9fc4a392d8f9d86d2e2740c08260e","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmMxOGZkZTMyNjZkOWZjNGEzOTJkOGY5ZDg2ZDJlMjc0MGMwODI2MGU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:16:23Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:16:23Z"},"message":"63C8D1C3-703F-40A1-8F7B-580A4D85D632","tree":{"sha":"2e6f8e361c3fba208fcf3da3ecc0c8e906539564","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2e6f8e361c3fba208fcf3da3ecc0c8e906539564"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c18fde3266d9fc4a392d8f9d86d2e2740c08260e","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c18fde3266d9fc4a392d8f9d86d2e2740c08260e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c18fde3266d9fc4a392d8f9d86d2e2740c08260e","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c18fde3266d9fc4a392d8f9d86d2e2740c08260e/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"38cc591258c811d996c78b48e4340b12f939e824","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/38cc591258c811d996c78b48e4340b12f939e824","html_url":"https://github.com/ThiagoCodecov/example-python/commit/38cc591258c811d996c78b48e4340b12f939e824"}]},{"sha":"38cc591258c811d996c78b48e4340b12f939e824","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjM4Y2M1OTEyNThjODExZDk5NmM3OGI0OGU0MzQwYjEyZjkzOWU4MjQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:16:23Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:16:23Z"},"message":"E97F66AF-BBF5-47E8-8576-9148110A1E57","tree":{"sha":"6f91699cc5e79ba506b3a5259d28ea961a40593c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6f91699cc5e79ba506b3a5259d28ea961a40593c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/38cc591258c811d996c78b48e4340b12f939e824","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/38cc591258c811d996c78b48e4340b12f939e824","html_url":"https://github.com/ThiagoCodecov/example-python/commit/38cc591258c811d996c78b48e4340b12f939e824","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/38cc591258c811d996c78b48e4340b12f939e824/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6eedb865917b52cbf4a7f47d246046adf63a4cd6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6eedb865917b52cbf4a7f47d246046adf63a4cd6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6eedb865917b52cbf4a7f47d246046adf63a4cd6"}]},{"sha":"6eedb865917b52cbf4a7f47d246046adf63a4cd6","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjZlZWRiODY1OTE3YjUyY2JmNGE3ZjQ3ZDI0NjA0NmFkZjYzYTRjZDY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:14:33Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:14:33Z"},"message":"F904B847-3311-42AC-A0A0-36A40EA7D488","tree":{"sha":"46d929062474504be967b226d6bfc81057925cf3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/46d929062474504be967b226d6bfc81057925cf3"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6eedb865917b52cbf4a7f47d246046adf63a4cd6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6eedb865917b52cbf4a7f47d246046adf63a4cd6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6eedb865917b52cbf4a7f47d246046adf63a4cd6","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6eedb865917b52cbf4a7f47d246046adf63a4cd6/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"d4a7881ffd98a2aae71c9f747197a06ec4860c24","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d4a7881ffd98a2aae71c9f747197a06ec4860c24","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d4a7881ffd98a2aae71c9f747197a06ec4860c24"}]},{"sha":"d4a7881ffd98a2aae71c9f747197a06ec4860c24","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmQ0YTc4ODFmZmQ5OGEyYWFlNzFjOWY3NDcxOTdhMDZlYzQ4NjBjMjQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:14:33Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:14:33Z"},"message":"EAF7489D-C2DD-4CB4-B53D-DD7781CE329E","tree":{"sha":"4f9cd50f931796983e9173d3cc21026f80e8f25f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4f9cd50f931796983e9173d3cc21026f80e8f25f"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d4a7881ffd98a2aae71c9f747197a06ec4860c24","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d4a7881ffd98a2aae71c9f747197a06ec4860c24","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d4a7881ffd98a2aae71c9f747197a06ec4860c24","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d4a7881ffd98a2aae71c9f747197a06ec4860c24/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"9857a3abe369d6d9fc7f895e8e714c0b597ce377","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9857a3abe369d6d9fc7f895e8e714c0b597ce377","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9857a3abe369d6d9fc7f895e8e714c0b597ce377"}]},{"sha":"9857a3abe369d6d9fc7f895e8e714c0b597ce377","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojk4NTdhM2FiZTM2OWQ2ZDlmYzdmODk1ZThlNzE0YzBiNTk3Y2UzNzc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:10:41Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:10:41Z"},"message":"7BBD2F88-35FB-4788-80DA-1A253AD2570B","tree":{"sha":"badefa0f823391e90cdade3159b818740e043dd0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/badefa0f823391e90cdade3159b818740e043dd0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/9857a3abe369d6d9fc7f895e8e714c0b597ce377","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9857a3abe369d6d9fc7f895e8e714c0b597ce377","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9857a3abe369d6d9fc7f895e8e714c0b597ce377","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9857a3abe369d6d9fc7f895e8e714c0b597ce377/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"087c0ea26d8d27fe05dc17f6c10bb53ac20963d9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087c0ea26d8d27fe05dc17f6c10bb53ac20963d9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/087c0ea26d8d27fe05dc17f6c10bb53ac20963d9"}]},{"sha":"087c0ea26d8d27fe05dc17f6c10bb53ac20963d9","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA4N2MwZWEyNmQ4ZDI3ZmUwNWRjMTdmNmMxMGJiNTNhYzIwOTYzZDk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:10:41Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:10:41Z"},"message":"7574F3EA-731E-4605-A7E7-31DC93690FF5","tree":{"sha":"ccc4165e2dbd703b90e538ae15e3890356621b08","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ccc4165e2dbd703b90e538ae15e3890356621b08"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/087c0ea26d8d27fe05dc17f6c10bb53ac20963d9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087c0ea26d8d27fe05dc17f6c10bb53ac20963d9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/087c0ea26d8d27fe05dc17f6c10bb53ac20963d9","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087c0ea26d8d27fe05dc17f6c10bb53ac20963d9/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2b60969699d3cd96552fb2a90280abead590800f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2b60969699d3cd96552fb2a90280abead590800f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2b60969699d3cd96552fb2a90280abead590800f"}]},{"sha":"2b60969699d3cd96552fb2a90280abead590800f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJiNjA5Njk2OTlkM2NkOTY1NTJmYjJhOTAyODBhYmVhZDU5MDgwMGY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:10:17Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:10:17Z"},"message":"76E3FB43-76B6-4ED3-9315-818C4BB073D2","tree":{"sha":"9b685756c2df783cbb2fafc09ac54d9906bfd9b0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9b685756c2df783cbb2fafc09ac54d9906bfd9b0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2b60969699d3cd96552fb2a90280abead590800f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2b60969699d3cd96552fb2a90280abead590800f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2b60969699d3cd96552fb2a90280abead590800f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2b60969699d3cd96552fb2a90280abead590800f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"98910948cfae2f37a6d8445f182b5f5146d265d3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/98910948cfae2f37a6d8445f182b5f5146d265d3","html_url":"https://github.com/ThiagoCodecov/example-python/commit/98910948cfae2f37a6d8445f182b5f5146d265d3"}]},{"sha":"98910948cfae2f37a6d8445f182b5f5146d265d3","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojk4OTEwOTQ4Y2ZhZTJmMzdhNmQ4NDQ1ZjE4MmI1ZjUxNDZkMjY1ZDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:10:17Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:10:17Z"},"message":"96F6A299-FA1B-40E7-97BD-BB78805B17E4","tree":{"sha":"b60e09486223889dd7d4653cbf0ab9eeb21f42a0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b60e09486223889dd7d4653cbf0ab9eeb21f42a0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/98910948cfae2f37a6d8445f182b5f5146d265d3","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/98910948cfae2f37a6d8445f182b5f5146d265d3","html_url":"https://github.com/ThiagoCodecov/example-python/commit/98910948cfae2f37a6d8445f182b5f5146d265d3","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/98910948cfae2f37a6d8445f182b5f5146d265d3/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2155c4c9623e18693c75c84aa5c0bf73476c28ed","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2155c4c9623e18693c75c84aa5c0bf73476c28ed","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2155c4c9623e18693c75c84aa5c0bf73476c28ed"}]},{"sha":"2155c4c9623e18693c75c84aa5c0bf73476c28ed","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjIxNTVjNGM5NjIzZTE4NjkzYzc1Yzg0YWE1YzBiZjczNDc2YzI4ZWQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:07:50Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:07:50Z"},"message":"86238B48-1C1A-404D-84F6-540BA12026D0","tree":{"sha":"035f3077941d62925add2a12c57778b3e713a4ab","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/035f3077941d62925add2a12c57778b3e713a4ab"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2155c4c9623e18693c75c84aa5c0bf73476c28ed","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2155c4c9623e18693c75c84aa5c0bf73476c28ed","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2155c4c9623e18693c75c84aa5c0bf73476c28ed","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2155c4c9623e18693c75c84aa5c0bf73476c28ed/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"a3dcd9163c20a16a44aa1df5c3ca48fca5f7655e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a3dcd9163c20a16a44aa1df5c3ca48fca5f7655e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a3dcd9163c20a16a44aa1df5c3ca48fca5f7655e"}]},{"sha":"a3dcd9163c20a16a44aa1df5c3ca48fca5f7655e","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmEzZGNkOTE2M2MyMGExNmE0NGFhMWRmNWMzY2E0OGZjYTVmNzY1NWU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:07:50Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:07:50Z"},"message":"D91343F0-6020-451C-AE34-AE13C54768B7","tree":{"sha":"e95f6a925e7d9d994aa0eede18d1ab049bf31332","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e95f6a925e7d9d994aa0eede18d1ab049bf31332"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/a3dcd9163c20a16a44aa1df5c3ca48fca5f7655e","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a3dcd9163c20a16a44aa1df5c3ca48fca5f7655e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a3dcd9163c20a16a44aa1df5c3ca48fca5f7655e","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a3dcd9163c20a16a44aa1df5c3ca48fca5f7655e/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"a4ea5016273c9cfa8e869cc41ab75a69180c5d08","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a4ea5016273c9cfa8e869cc41ab75a69180c5d08","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a4ea5016273c9cfa8e869cc41ab75a69180c5d08"}]},{"sha":"a4ea5016273c9cfa8e869cc41ab75a69180c5d08","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmE0ZWE1MDE2MjczYzljZmE4ZTg2OWNjNDFhYjc1YTY5MTgwYzVkMDg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:06:26Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:06:26Z"},"message":"75FF7739-474E-4F40-A1F5-DA644247777C","tree":{"sha":"0a224aaebf416022b485d1ff5eb0a590c284a8c0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/0a224aaebf416022b485d1ff5eb0a590c284a8c0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/a4ea5016273c9cfa8e869cc41ab75a69180c5d08","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a4ea5016273c9cfa8e869cc41ab75a69180c5d08","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a4ea5016273c9cfa8e869cc41ab75a69180c5d08","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a4ea5016273c9cfa8e869cc41ab75a69180c5d08/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"44958d0eedde4628323c6cf4f663e3ebc1f526b4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/44958d0eedde4628323c6cf4f663e3ebc1f526b4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/44958d0eedde4628323c6cf4f663e3ebc1f526b4"}]},{"sha":"44958d0eedde4628323c6cf4f663e3ebc1f526b4","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjQ0OTU4ZDBlZWRkZTQ2MjgzMjNjNmNmNGY2NjNlM2ViYzFmNTI2YjQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:06:26Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:06:26Z"},"message":"49C38017-A85B-4A35-9891-A9A8C929EBD1","tree":{"sha":"b58f70513693758466bda80729e62c846351d93d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b58f70513693758466bda80729e62c846351d93d"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/44958d0eedde4628323c6cf4f663e3ebc1f526b4","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/44958d0eedde4628323c6cf4f663e3ebc1f526b4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/44958d0eedde4628323c6cf4f663e3ebc1f526b4","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/44958d0eedde4628323c6cf4f663e3ebc1f526b4/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"4ffecf59d2eebc39dddbb7976c977258a7dd1059","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4ffecf59d2eebc39dddbb7976c977258a7dd1059","html_url":"https://github.com/ThiagoCodecov/example-python/commit/4ffecf59d2eebc39dddbb7976c977258a7dd1059"}]},{"sha":"4ffecf59d2eebc39dddbb7976c977258a7dd1059","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjRmZmVjZjU5ZDJlZWJjMzlkZGRiYjc5NzZjOTc3MjU4YTdkZDEwNTk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:04:30Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:04:30Z"},"message":"6F2AA94B-B8B7-4E5D-89F2-4B781B58FE20","tree":{"sha":"ed751f8f3ffbcf308e91698187ae1ce052e09ca0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ed751f8f3ffbcf308e91698187ae1ce052e09ca0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/4ffecf59d2eebc39dddbb7976c977258a7dd1059","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4ffecf59d2eebc39dddbb7976c977258a7dd1059","html_url":"https://github.com/ThiagoCodecov/example-python/commit/4ffecf59d2eebc39dddbb7976c977258a7dd1059","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4ffecf59d2eebc39dddbb7976c977258a7dd1059/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"96592e2e1b9e6bd5870bf8d8e6d5de0d1b840f0f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/96592e2e1b9e6bd5870bf8d8e6d5de0d1b840f0f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/96592e2e1b9e6bd5870bf8d8e6d5de0d1b840f0f"}]},{"sha":"96592e2e1b9e6bd5870bf8d8e6d5de0d1b840f0f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojk2NTkyZTJlMWI5ZTZiZDU4NzBiZjhkOGU2ZDVkZTBkMWI4NDBmMGY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:04:30Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:04:30Z"},"message":"90E63F6E-2D82-4C95-B711-55F48F203062","tree":{"sha":"42acd54abb2fe082a660df562a192b73ae772ace","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/42acd54abb2fe082a660df562a192b73ae772ace"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/96592e2e1b9e6bd5870bf8d8e6d5de0d1b840f0f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/96592e2e1b9e6bd5870bf8d8e6d5de0d1b840f0f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/96592e2e1b9e6bd5870bf8d8e6d5de0d1b840f0f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/96592e2e1b9e6bd5870bf8d8e6d5de0d1b840f0f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"30c20136235b395cd81e761d7feafc01289c8b41","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30c20136235b395cd81e761d7feafc01289c8b41","html_url":"https://github.com/ThiagoCodecov/example-python/commit/30c20136235b395cd81e761d7feafc01289c8b41"}]},{"sha":"30c20136235b395cd81e761d7feafc01289c8b41","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjMwYzIwMTM2MjM1YjM5NWNkODFlNzYxZDdmZWFmYzAxMjg5YzhiNDE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:02:17Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:02:17Z"},"message":"BC8B58F8-D84F-41DA-8411-D145DFBBCDF7","tree":{"sha":"23a131aeb83e70c162a84b136024890a429e8e8c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/23a131aeb83e70c162a84b136024890a429e8e8c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/30c20136235b395cd81e761d7feafc01289c8b41","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30c20136235b395cd81e761d7feafc01289c8b41","html_url":"https://github.com/ThiagoCodecov/example-python/commit/30c20136235b395cd81e761d7feafc01289c8b41","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30c20136235b395cd81e761d7feafc01289c8b41/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"3faf3f0d5723063344ed3fd3e766e4382e7eb430","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3faf3f0d5723063344ed3fd3e766e4382e7eb430","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3faf3f0d5723063344ed3fd3e766e4382e7eb430"}]},{"sha":"3faf3f0d5723063344ed3fd3e766e4382e7eb430","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjNmYWYzZjBkNTcyMzA2MzM0NGVkM2ZkM2U3NjZlNDM4MmU3ZWI0MzA=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:02:17Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-20T19:02:17Z"},"message":"8AAB2B79-2543-4769-B4E5-C7F8DF996128","tree":{"sha":"9a34bde5d65c43435f6bc8b2f04fbf7553a008b0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9a34bde5d65c43435f6bc8b2f04fbf7553a008b0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3faf3f0d5723063344ed3fd3e766e4382e7eb430","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3faf3f0d5723063344ed3fd3e766e4382e7eb430","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3faf3f0d5723063344ed3fd3e766e4382e7eb430","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3faf3f0d5723063344ed3fd3e766e4382e7eb430/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"d4dd0902e335216ca911cc5eddd54c0590154f67","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d4dd0902e335216ca911cc5eddd54c0590154f67","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d4dd0902e335216ca911cc5eddd54c0590154f67"}]},{"sha":"d4dd0902e335216ca911cc5eddd54c0590154f67","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmQ0ZGQwOTAyZTMzNTIxNmNhOTExY2M1ZWRkZDU0YzA1OTAxNTRmNjc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-09T13:51:24Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-09T13:51:24Z"},"message":"36BED83E-FC17-46A2-825D-BCCB5783ECE9","tree":{"sha":"2ed4dec721ef85e0e244e671bf1a2bdb6c2dff2a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2ed4dec721ef85e0e244e671bf1a2bdb6c2dff2a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d4dd0902e335216ca911cc5eddd54c0590154f67","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d4dd0902e335216ca911cc5eddd54c0590154f67","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d4dd0902e335216ca911cc5eddd54c0590154f67","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d4dd0902e335216ca911cc5eddd54c0590154f67/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ff12f781cc4db8a6560c54723a8e2131d1ab7b9a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ff12f781cc4db8a6560c54723a8e2131d1ab7b9a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ff12f781cc4db8a6560c54723a8e2131d1ab7b9a"}]},{"sha":"ff12f781cc4db8a6560c54723a8e2131d1ab7b9a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmZmMTJmNzgxY2M0ZGI4YTY1NjBjNTQ3MjNhOGUyMTMxZDFhYjdiOWE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-09T13:51:24Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-09T13:51:24Z"},"message":"F0F82276-44D0-4A89-92D0-32BB652160EB","tree":{"sha":"62ffebfb87ec4cbb6e11103cc1df7d3366718bd3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/62ffebfb87ec4cbb6e11103cc1df7d3366718bd3"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ff12f781cc4db8a6560c54723a8e2131d1ab7b9a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ff12f781cc4db8a6560c54723a8e2131d1ab7b9a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ff12f781cc4db8a6560c54723a8e2131d1ab7b9a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ff12f781cc4db8a6560c54723a8e2131d1ab7b9a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"b83a1dde30dff054775ad5a05a96070c991ac0e4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b83a1dde30dff054775ad5a05a96070c991ac0e4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b83a1dde30dff054775ad5a05a96070c991ac0e4"}]},{"sha":"b83a1dde30dff054775ad5a05a96070c991ac0e4","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmI4M2ExZGRlMzBkZmYwNTQ3NzVhZDVhMDVhOTYwNzBjOTkxYWMwZTQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-09T13:49:17Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-02-09T13:49:17Z"},"message":"768A0A2C-4E79-4BB5-9815-C45316D88265","tree":{"sha":"3c4155f1a78526623030df2d659c198cc009aab3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3c4155f1a78526623030df2d659c198cc009aab3"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b83a1dde30dff054775ad5a05a96070c991ac0e4","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b83a1dde30dff054775ad5a05a96070c991ac0e4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b83a1dde30dff054775ad5a05a96070c991ac0e4","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b83a1dde30dff054775ad5a05a96070c991ac0e4/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"fc06ce6861db737cd2720071cf0e971bdc9e9811","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fc06ce6861db737cd2720071cf0e971bdc9e9811","html_url":"https://github.com/ThiagoCodecov/example-python/commit/fc06ce6861db737cd2720071cf0e971bdc9e9811"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Sat, 25 Apr 2020 14:43:33 GMT + Etag: + - W/"871c5f02ab292f08a0b44b5b6711bd2b" + Last-Modified: + - Tue, 24 Mar 2020 22:01:33 GMT + Link: + - ; + rel="next", ; + rel="last" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - E877:72BB:4B6800:66A8BC:5EA44C94 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4999' + X-Ratelimit-Reset: + - '1587829412' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits?sha=master +version: 1 diff --git a/apps/worker/tasks/tests/integration/test_ai_pr_review.py b/apps/worker/tasks/tests/integration/test_ai_pr_review.py new file mode 100644 index 0000000000..f83abddf01 --- /dev/null +++ b/apps/worker/tasks/tests/integration/test_ai_pr_review.py @@ -0,0 +1,29 @@ +import pytest + +from database.tests.factories import OwnerFactory, RepositoryFactory +from tasks.ai_pr_review import AiPrReviewTask + + +@pytest.mark.integration +def test_ai_pr_review_task( + mocker, + dbsession, +): + owner = OwnerFactory(service="github") + repository = RepositoryFactory(owner=owner) + dbsession.add(owner) + dbsession.add(repository) + dbsession.flush() + + perform_review = mocker.patch("tasks.ai_pr_review.perform_review") + + task = AiPrReviewTask() + + result = task.run_impl( + dbsession, + repoid=repository.repoid, + pullid=123, + ) + + assert result == {"successful": True} + perform_review.assert_called_once_with(repository, 123) diff --git a/apps/worker/tasks/tests/integration/test_ghm_sync_plans.py b/apps/worker/tasks/tests/integration/test_ghm_sync_plans.py new file mode 100644 index 0000000000..6dba9b3a8d --- /dev/null +++ b/apps/worker/tasks/tests/integration/test_ghm_sync_plans.py @@ -0,0 +1,277 @@ +import pytest +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName + +from database.models import Owner, Repository +from database.tests.factories import OwnerFactory, RepositoryFactory +from services.github_marketplace import GitHubMarketplaceService +from tasks.github_marketplace import SyncPlansTask + +# DONT WORRY, this is generated for the purposes of validation +# and is not the real one on which the code ran +fake_private_key = """-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDCFqq2ygFh9UQU/6PoDJ6L9e4ovLPCHtlBt7vzDwyfwr3XGxln +0VbfycVLc6unJDVEGZ/PsFEuS9j1QmBTTEgvCLR6RGpfzmVuMO8wGVEO52pH73h9 +rviojaheX/u3ZqaA0di9RKy8e3L+T0ka3QYgDx5wiOIUu1wGXCs6PhrtEwICBAEC +gYBu9jsi0eVROozSz5dmcZxUAzv7USiUcYrxX007SUpm0zzUY+kPpWLeWWEPaddF +VONCp//0XU8hNhoh0gedw7ZgUTG6jYVOdGlaV95LhgY6yXaQGoKSQNNTY+ZZVT61 +zvHOlPynt3GZcaRJOlgf+3hBF5MCRoWKf+lDA5KiWkqOYQJBAMQp0HNVeTqz+E0O +6E0neqQDQb95thFmmCI7Kgg4PvkS5mz7iAbZa5pab3VuyfmvnVvYLWejOwuYSp0U +9N8QvUsCQQD9StWHaVNM4Lf5zJnB1+lJPTXQsmsuzWvF3HmBkMHYWdy84N/TdCZX +Cxve1LR37lM/Vijer0K77wAx2RAN/ppZAkB8+GwSh5+mxZKydyPaPN29p6nC6aLx +3DV2dpzmhD0ZDwmuk8GN+qc0YRNOzzJ/2UbHH9L/lvGqui8I6WLOi8nDAkEA9CYq +ewfdZ9LcytGz7QwPEeWVhvpm0HQV9moetFWVolYecqBP4QzNyokVnpeUOqhIQAwe +Z0FJEQ9VWsG+Df0noQJBALFjUUZEtv4x31gMlV24oiSWHxIRX4fEND/6LpjleDZ5 +C/tY+lZIEO1Gg/FxSMB+hwwhwfSuE3WohZfEcSy+R48= +-----END RSA PRIVATE KEY-----""" + + +@pytest.mark.integration +class TestGHMarketplaceSyncPlansTask(object): + def test_purchase_by_existing_owner( + self, dbsession, mocker, mock_configuration, codecov_vcr + ): + mock_configuration.loaded_files[("github", "integration", "pem")] = ( + fake_private_key + ) + + mock_configuration.params["github"] = { + "integration": { + "pem": "/home/src/certs/github.pem", + "id": 51984, # Fake integration id, tested with a real one + } + } + mock_configuration.params["services"]["github_marketplace"] = dict( + use_stubbed=True + ) + + owner = OwnerFactory.create( + username="cc-test", + service="github", + service_id="3877742", + plan=None, + plan_provider=None, + plan_auto_activate=None, + plan_user_count=None, + ) + dbsession.add(owner) + dbsession.flush() + + sender = {"login": "cc-test", "id": 3877742} + account = {"type": "User", "id": 3877742, "login": "cc-test"} + action = "purchased" + + task = SyncPlansTask() + result = task.run_impl(dbsession, sender=sender, account=account, action=action) + assert result["plan_type_synced"] == "paid" + + assert owner.plan == PlanName.GHM_PLAN_NAME.value + assert owner.plan_provider == "github" + assert owner.plan_auto_activate is True + assert owner.plan_user_count == 10 + + def test_purchase_new_owner( + self, dbsession, mocker, mock_configuration, codecov_vcr + ): + mock_configuration.loaded_files[("github", "integration", "pem")] = ( + fake_private_key + ) + + mock_configuration.params["github"] = { + "integration": { + "pem": "/home/src/certs/github.pem", + "id": 51984, # Fake integration id, tested with a real one + } + } + mock_configuration.params["services"]["github_marketplace"] = dict( + use_stubbed=True + ) + + sender = {"login": "cc-test", "id": 3877742} + account = {"type": "User", "id": 3877742, "login": "cc-test"} + action = "purchased" + + task = SyncPlansTask() + result = task.run_impl(dbsession, sender=sender, account=account, action=action) + assert result["plan_type_synced"] == "paid" + + owner = ( + dbsession.query(Owner) + .filter(Owner.service == "github", Owner.service_id == "3877742") + .first() + ) + + assert owner is not None + assert owner.username == "cc-test" + assert owner.plan == PlanName.GHM_PLAN_NAME.value + assert owner.plan_provider == "github" + assert owner.plan_auto_activate is True + assert owner.plan_user_count == 10 + + def test_purchase_listing_not_found( + self, dbsession, mocker, mock_configuration, codecov_vcr + ): + mock_configuration.loaded_files[("github", "integration", "pem")] = ( + fake_private_key + ) + + mock_configuration.params["github"] = { + "integration": { + "pem": "/home/src/certs/github.pem", + "id": 51984, # Fake integration id, tested with a real one + } + } + mock_configuration.params["services"]["github_marketplace"] = dict( + use_stubbed=True + ) + + sender = {"login": "cc-test", "id": 3877742} + account = {"type": "Organization", "id": 123456, "login": "some-org"} + action = "purchased" + + task = SyncPlansTask() + result = task.run_impl(dbsession, sender=sender, account=account, action=action) + assert result["plan_type_synced"] == "free" + + owner = ( + dbsession.query(Owner) + .filter(Owner.service == "github", Owner.service_id == "123456") + .first() + ) + + assert owner is not None + assert owner.username == "some-org" + assert owner.plan_provider == "github" + assert owner.plan == DEFAULT_FREE_PLAN + assert owner.plan_user_count == 1 + assert owner.plan_activated_users is None + + def test_cancelled(self, dbsession, mocker, mock_configuration, codecov_vcr): + mock_configuration.loaded_files[("github", "integration", "pem")] = ( + fake_private_key + ) + + mock_configuration.params["github"] = { + "integration": { + "pem": "/home/src/certs/github.pem", + "id": 51984, # Fake integration id, tested with a real one + } + } + mock_configuration.params["services"]["github_marketplace"] = dict( + use_stubbed=True + ) + + owner = OwnerFactory.create( + username="cc-test", + service="github", + service_id="3877742", + plan=PlanName.GHM_PLAN_NAME.value, + plan_provider="github", + plan_auto_activate=True, + plan_user_count=10, + ) + dbsession.add(owner) + repo_pub = RepositoryFactory.create( + private=False, + name="pub", + using_integration=False, + service_id="159090647", + activated=True, + owner=owner, + ) + repo_pytest = RepositoryFactory.create( + private=False, + name="pytest", + using_integration=False, + service_id="159089634", + activated=True, + owner=owner, + ) + repo_spack = RepositoryFactory.create( + private=False, + name="spack", + using_integration=False, + service_id="164948070", + activated=True, + owner=owner, + ) + dbsession.add(repo_pub) + dbsession.add(repo_pytest) + dbsession.add(repo_spack) + dbsession.flush() + + sender = {"login": "cc-test", "id": 3877742} + account = {"type": "User", "id": 3877742, "login": "cc-test"} + action = "cancelled" + + task = SyncPlansTask() + result = task.run_impl(dbsession, sender=sender, account=account, action=action) + assert result["plan_type_synced"] == "free" + + dbsession.commit() + owner = ( + dbsession.query(Owner) + .filter(Owner.service == "github", Owner.service_id == "3877742") + .first() + ) + assert owner is not None + assert owner.username == "cc-test" + assert owner.plan_provider == "github" + assert owner.plan == DEFAULT_FREE_PLAN + assert owner.plan_user_count == 1 + assert owner.plan_activated_users is None + + repos = ( + dbsession.query(Repository) + .filter(Repository.ownerid == owner.ownerid) + .all() + ) + assert len(repos) == 3 + for repo in repos: + assert repo.activated is False + + def test_sync_all_plans(self, dbsession, mocker, mock_configuration, codecov_vcr): + mock_configuration.loaded_files[("github", "integration", "pem")] = ( + fake_private_key + ) + mock_configuration.params["github"] = { + "integration": { + "pem": "/home/src/certs/github.pem", + "id": 51984, # Fake integration id, tested with a real one + }, + "client_id": "testiouu71gdynyqxzk4", + "client_secret": "3b4ab5b18be7155fdbb739e7f1ae277222fb12db", + } + mock_configuration.params["services"]["github_marketplace"] = dict( + use_stubbed=True + ) + + # create owner whose plan is actually inactive + owner = OwnerFactory.create( + username="test2", + service="github", + service_id="781233", + plan=PlanName.GHM_PLAN_NAME.value, + plan_provider="github", + plan_auto_activate=True, + plan_user_count=10, + ) + dbsession.add(owner) + dbsession.flush() + + action = "purchased" + ghm_service = GitHubMarketplaceService() + + SyncPlansTask().sync_all(dbsession, ghm_service=ghm_service, action=action) + + # inactive plan disabled + dbsession.commit() + assert owner.plan is None + + # active plans - service ids 2 and 4 + owners = dbsession.query(Owner).filter(Owner.service_id.in_(["2", "4"])).all() + assert owners is not None + for owner in owners: + assert owner.plan == PlanName.GHM_PLAN_NAME.value + assert owner.plan_provider == "github" + assert owner.plan_auto_activate == True + assert owner.plan_user_count == 12 diff --git a/apps/worker/tasks/tests/integration/test_http_request_task.py b/apps/worker/tasks/tests/integration/test_http_request_task.py new file mode 100644 index 0000000000..25e94eee6b --- /dev/null +++ b/apps/worker/tasks/tests/integration/test_http_request_task.py @@ -0,0 +1,69 @@ +import json + +import pytest +from celery.exceptions import Retry + +from tasks.http_request import HTTPRequestTask + + +@pytest.mark.integration +class TestHTTPRequestTask: + def test_http_request_run_async_200(self, dbsession, codecov_vcr): + task = HTTPRequestTask() + res = task.run_impl( + dbsession, + url="http://mockbin.org/bin/a1316495-ee65-4eab-b8e3-d5cb7cfc7519?foo=bar&foo=baz", + method="POST", + headers={ + "Content-Type": "application/json", + "User-Agent": "Codecov", + }, + data=json.dumps({"testing": 123}), + ) + assert res == {"response": "ok", "status_code": 200, "successful": True} + + def test_http_request_run_async_400(self, dbsession, codecov_vcr): + task = HTTPRequestTask() + res = task.run_impl( + dbsession, + url="http://mockbin.org/bin/e4e9db83-b7b9-4a50-b929-d672bcc8d075?foo=bar&foo=baz", + method="POST", + headers={ + "Content-Type": "application/json", + "User-Agent": "Codecov", + }, + data=json.dumps({"testing": 123}), + ) + assert res == { + "response": "bad request", + "status_code": 400, + "successful": False, + } + + def test_http_request_run_async_500(self, dbsession, codecov_vcr): + task = HTTPRequestTask() + with pytest.raises(Retry): + task.run_impl( + dbsession, + url="http://mockbin.org/bin/c0052243-3391-4a30-bed7-066e4cd04074?foo=bar&foo=baz", + method="POST", + headers={ + "Content-Type": "application/json", + "User-Agent": "Codecov", + }, + data=json.dumps({"testing": 123}), + ) + + def test_http_request_run_async_connection_error(self, dbsession, codecov_vcr): + task = HTTPRequestTask() + with pytest.raises(Retry): + task.run_impl( + dbsession, + url="http://probablynotavaliddomain.com", + method="POST", + headers={ + "Content-Type": "application/json", + "User-Agent": "Codecov", + }, + data=json.dumps({"testing": 123}), + ) diff --git a/apps/worker/tasks/tests/integration/test_notify_task.py b/apps/worker/tasks/tests/integration/test_notify_task.py new file mode 100644 index 0000000000..f8722443b5 --- /dev/null +++ b/apps/worker/tasks/tests/integration/test_notify_task.py @@ -0,0 +1,1401 @@ +from decimal import Decimal +from unittest.mock import patch + +import pytest +from mock import AsyncMock, PropertyMock +from shared.validation.types import CoverageCommentRequiredChanges + +from database.models import Pull +from database.models.core import CompareCommit +from database.tests.factories import CommitFactory, PullFactory, RepositoryFactory +from services.archive import ArchiveService +from services.comparison import get_or_create_comparison +from services.notification.notifiers.base import NotificationResult +from services.repository import EnrichedPull +from tasks.notify import NotifyTask +from tests.helpers import mock_all_plans_and_tiers + +sample_token = "ghp_test6ldgmyaglf73gcnbi0kprz7dyjz6nzgn" + + +@pytest.fixture +def is_not_first_pull(mocker): + mocker.patch( + "database.models.core.Pull.is_first_coverage_pull", + return_value=False, + new_callable=PropertyMock, + ) + + +@pytest.mark.integration +class TestNotifyTask(object): + @pytest.fixture(autouse=True) + def setup(self): + mock_all_plans_and_tiers() + + @patch("requests.post") + @pytest.mark.django_db + def test_simple_call_no_notifiers( + self, + mock_requests_post, + dbsession, + mocker, + codecov_vcr, + mock_storage, + mock_configuration, + mock_redis, + ): + mock_requests_post.return_value.status_code = 200 + mock_redis.get.return_value = None + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codecov.io" + ) + mocked_app = mocker.patch.object(NotifyTask, "app") + repository = RepositoryFactory.create( + owner__unencrypted_oauth_token=sample_token, + owner__username="ThiagoCodecov", + owner__service_id="44376991", + owner__service="github", + yaml={"codecov": {"max_report_age": "1y ago"}}, + name="example-python", + ) + dbsession.add(repository) + dbsession.flush() + master_commit = CommitFactory.create( + message="", + pullid=None, + branch="master", + commitid="17a71a9a2f5335ed4d00496c7bbc6405f547a527", + repository=repository, + author__username="christina84", + ) + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + repository=repository, + author__username="christina84", + ) + dbsession.add(commit) + dbsession.add(master_commit) + dbsession.flush() + with open("tasks/tests/samples/sample_chunks_1.txt") as f: + content = f.read().encode() + archive_hash = ArchiveService.get_archive_hash(commit.repository) + chunks_url = f"v4/repos/{archive_hash}/commits/{commit.commitid}/chunks.txt" + mock_storage.write_file("archive", chunks_url, content) + master_chunks_url = ( + f"v4/repos/{archive_hash}/commits/{master_commit.commitid}/chunks.txt" + ) + mock_storage.write_file("archive", master_chunks_url, content) + task = NotifyTask() + result = task.run_impl( + dbsession, repoid=commit.repoid, commitid=commit.commitid, current_yaml={} + ) + + assert result == { + "notified": True, + "notifications": [ + { + "notifier": "codecov-slack-app", + "title": "codecov-slack-app", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation="Successfully notified slack app", + data_sent={ + "repository": "example-python", + "owner": "ThiagoCodecov", + "comparison": { + "url": None, + "message": "unknown", + "coverage": None, + "notation": "", + "head_commit": { + "commitid": "649eaaf2924e92dc7fd8d370ddb857033231e67a", + "branch": "test-branch-1", + "message": "", + "author": "christina84", + "timestamp": "2019-02-01T17:59:47+00:00", + "ci_passed": True, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "85.00000", + "d": 0, + "diff": [ + 1, + 2, + 1, + 1, + 0, + "50.00000", + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "f": 3, + "h": 17, + "m": 3, + "n": 20, + "p": 0, + "s": 1, + }, + "pull": None, + }, + "base_commit": None, + "head_totals_c": "85.00000", + }, + }, + data_received=None, + ), + } + ], + } + + @patch("requests.post") + @pytest.mark.django_db + def test_simple_call_only_status_notifiers( + self, + mock_post_request, + dbsession, + mocker, + codecov_vcr, + mock_storage, + mock_configuration, + ): + mock_post_request.return_value.status_code = 200 + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codecov.io" + ) + mocked_app = mocker.patch.object(NotifyTask, "app") + repository = RepositoryFactory.create( + owner__unencrypted_oauth_token=sample_token, + owner__username="ThiagoCodecov", + owner__service="github", + owner__service_id="44376991", + name="example-python", + ) + dbsession.add(repository) + dbsession.flush() + master_commit = CommitFactory.create( + message="", + pullid=None, + branch="master", + commitid="17a71a9a2f5335ed4d00496c7bbc6405f547a527", + repository=repository, + author__username="bateslouis", + ) + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + repository=repository, + parent_commit_id="17a71a9a2f5335ed4d00496c7bbc6405f547a527", + author__username="bateslouis", + ) + dbsession.add(commit) + dbsession.add(master_commit) + get_or_create_comparison(dbsession, master_commit, commit) + dbsession.flush() + task = NotifyTask() + with open("tasks/tests/samples/sample_chunks_1.txt") as f: + content = f.read().encode() + archive_hash = ArchiveService.get_archive_hash(commit.repository) + chunks_url = f"v4/repos/{archive_hash}/commits/{commit.commitid}/chunks.txt" + mock_storage.write_file("archive", chunks_url, content) + master_chunks_url = ( + f"v4/repos/{archive_hash}/commits/{master_commit.commitid}/chunks.txt" + ) + mock_storage.write_file("archive", master_chunks_url, content) + result = task.run_impl_within_lock( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml={"coverage": {"status": {"project": True}}}, + ) + + assert result == { + "notified": True, + "notifications": [ + { + "notifier": "status-project", + "title": "default", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + data_sent={ + "title": "codecov/project", + "state": "success", + "message": "85.00% (+0.00%) compared to 17a71a9", + }, + data_received={"id": 1}, + ), + }, + { + "notifier": "codecov-slack-app", + "title": "codecov-slack-app", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation="Successfully notified slack app", + data_sent={ + "repository": "example-python", + "owner": "ThiagoCodecov", + "comparison": { + "url": "https://codecov.io/gh/ThiagoCodecov/example-python/commit/649eaaf2924e92dc7fd8d370ddb857033231e67a", + "message": "no change", + "coverage": "0.00", + "notation": "", + "head_commit": { + "commitid": "649eaaf2924e92dc7fd8d370ddb857033231e67a", + "branch": "test-branch-1", + "message": "", + "author": "bateslouis", + "timestamp": "2019-02-01T17:59:47", + "ci_passed": True, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "85.00000", + "d": 0, + "diff": [ + 1, + 2, + 1, + 1, + 0, + "50.00000", + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "f": 3, + "h": 17, + "m": 3, + "n": 20, + "p": 0, + "s": 1, + }, + "pull": None, + }, + "base_commit": { + "commitid": "17a71a9a2f5335ed4d00496c7bbc6405f547a527", + "branch": "master", + "message": "", + "author": "bateslouis", + "timestamp": "2019-02-01T17:59:47", + "ci_passed": True, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "85.00000", + "d": 0, + "diff": [ + 1, + 2, + 1, + 1, + 0, + "50.00000", + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "f": 3, + "h": 17, + "m": 3, + "n": 20, + "p": 0, + "s": 1, + }, + "pull": None, + }, + "head_totals_c": "85.00000", + }, + }, + data_received=None, + ), + }, + ], + } + + comparison = ( + dbsession.query(CompareCommit) + .filter( + CompareCommit.compare_commit_id == commit.id, + CompareCommit.base_commit_id == master_commit.id, + ) + .first() + ) + assert comparison is not None + assert comparison.patch_totals is not None + assert comparison.patch_totals == { + "hits": 2, + "misses": 0, + "partials": 0, + "coverage": 1.0, + } + + @patch("requests.post") + @pytest.mark.django_db + def test_simple_call_only_status_notifiers_no_pull_request( + self, + mock_post_request, + dbsession, + mocker, + codecov_vcr, + mock_storage, + mock_configuration, + ): + mock_post_request.return_value.status_code = 200 + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://myexamplewebsite.io" + ) + repository = RepositoryFactory.create( + owner__unencrypted_oauth_token=sample_token, + owner__service="github", + owner__username="ThiagoCodecov", + owner__service_id="44376991", + name="example-python", + ) + dbsession.add(repository) + dbsession.flush() + parent_commit_id = "081d91921f05a8a39d39aef667eddb88e96300c7" + commitid = "f0895290dc26668faeeb20ee5ccd4cc995925775" + master_commit = CommitFactory.create( + message="", + pullid=None, + branch="master", + commitid=parent_commit_id, + repository=repository, + author__username="bateslouis", + ) + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid=commitid, + parent_commit_id=master_commit.commitid, + repository=repository, + author__username="rolabuhasna", + ) + dbsession.add(commit) + dbsession.add(master_commit) + dbsession.flush() + task = NotifyTask() + with open("tasks/tests/samples/sample_chunks_1.txt") as f: + content = f.read().encode() + archive_hash = ArchiveService.get_archive_hash(commit.repository) + chunks_url = f"v4/repos/{archive_hash}/commits/{commit.commitid}/chunks.txt" + mock_storage.write_file("archive", chunks_url, content) + master_chunks_url = ( + f"v4/repos/{archive_hash}/commits/{master_commit.commitid}/chunks.txt" + ) + mock_storage.write_file("archive", master_chunks_url, content) + result = task.run_impl_within_lock( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml={ + "coverage": { + "status": {"project": True, "patch": True, "changes": True} + } + }, + ) + + assert result == { + "notified": True, + "notifications": [ + { + "notifier": "status-project", + "title": "default", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "title": "codecov/project", + "state": "success", + "message": "85.00% (+0.00%) compared to 081d919", + }, + data_received={"id": 9333281614}, + ), + }, + { + "notifier": "status-patch", + "title": "default", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "title": "codecov/patch", + "state": "success", + "message": "Coverage not affected when comparing 081d919...f089529", + }, + data_received={"id": 9333281697}, + ), + }, + { + "notifier": "status-changes", + "title": "default", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "title": "codecov/changes", + "state": "failure", + "message": "1 file has indirect coverage changes not visible in diff", + "included_helper_text": { + "indirect_changes_helper_text": "Your changes status has failed because you have indirect coverage changes. Learn more about [Unexpected Coverage Changes](https://docs.codecov.com/docs/unexpected-coverage-changes) and [reasons for indirect coverage changes](https://docs.codecov.com/docs/unexpected-coverage-changes#reasons-for-indirect-changes)." + }, + }, + data_received={"id": 9333281703}, + ), + }, + { + "notifier": "codecov-slack-app", + "title": "codecov-slack-app", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation="Successfully notified slack app", + data_sent={ + "repository": "example-python", + "owner": "ThiagoCodecov", + "comparison": { + "url": "https://myexamplewebsite.io/gh/ThiagoCodecov/example-python/commit/f0895290dc26668faeeb20ee5ccd4cc995925775", + "message": "no change", + "coverage": "0.00", + "notation": "", + "head_commit": { + "commitid": "f0895290dc26668faeeb20ee5ccd4cc995925775", + "branch": "test-branch-1", + "message": "", + "author": "rolabuhasna", + "timestamp": "2019-02-01T17:59:47", + "ci_passed": True, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "85.00000", + "d": 0, + "diff": [ + 1, + 2, + 1, + 1, + 0, + "50.00000", + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "f": 3, + "h": 17, + "m": 3, + "n": 20, + "p": 0, + "s": 1, + }, + "pull": None, + }, + "base_commit": { + "commitid": "081d91921f05a8a39d39aef667eddb88e96300c7", + "branch": "master", + "message": "", + "author": "bateslouis", + "timestamp": "2019-02-01T17:59:47", + "ci_passed": True, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "85.00000", + "d": 0, + "diff": [ + 1, + 2, + 1, + 1, + 0, + "50.00000", + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "f": 3, + "h": 17, + "m": 3, + "n": 20, + "p": 0, + "s": 1, + }, + "pull": None, + }, + "head_totals_c": "85.00000", + }, + }, + data_received=None, + ), + }, + ], + } + + @patch("requests.post") + @pytest.mark.django_db + def test_simple_call_only_status_notifiers_with_pull_request( + self, + mock_post_request, + dbsession, + mocker, + codecov_vcr, + mock_storage, + mock_configuration, + ): + mock_post_request.return_value.status_code = 200 + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://myexamplewebsite.io" + ) + mocked_app = mocker.patch.object(NotifyTask, "app") + repository = RepositoryFactory.create( + owner__unencrypted_oauth_token=sample_token, + owner__service="github", + owner__username="ThiagoCodecov", + owner__service_id="44376991", + name="example-python", + ) + dbsession.add(repository) + dbsession.flush() + head_commitid = "11daa27b1b74fd181836a64106f936a16404089c" + master_sha = "f0895290dc26668faeeb20ee5ccd4cc995925775" + master_commit = CommitFactory.create( + message="", + pullid=None, + branch="master", + commitid=master_sha, + repository=repository, + author__username="bateslouis", + ) + commit = CommitFactory.create( + message="", + pullid=None, + branch="thiago/base-no-base", + commitid=head_commitid, + parent_commit_id=master_commit.commitid, + repository=repository, + author__username="rolabuhasna", + ) + dbsession.add(commit) + dbsession.add(master_commit) + dbsession.flush() + task = NotifyTask() + with open("tasks/tests/samples/sample_chunks_1.txt") as f: + content = f.read().encode() + archive_hash = ArchiveService.get_archive_hash(commit.repository) + chunks_url = f"v4/repos/{archive_hash}/commits/{commit.commitid}/chunks.txt" + mock_storage.write_file("archive", chunks_url, content) + master_chunks_url = ( + f"v4/repos/{archive_hash}/commits/{master_commit.commitid}/chunks.txt" + ) + mock_storage.write_file("archive", master_chunks_url, content) + result = task.run_impl_within_lock( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml={ + "coverage": { + "status": {"project": True, "patch": True, "changes": True} + } + }, + ) + + assert result == { + "notified": True, + "notifications": [ + { + "notifier": "status-project", + "title": "default", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "title": "codecov/project", + "state": "success", + "message": "85.00% (+0.00%) compared to f089529", + }, + data_received={"id": 9333363767}, + ), + }, + { + "notifier": "status-patch", + "title": "default", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "title": "codecov/patch", + "state": "success", + "message": "Coverage not affected when comparing f089529...11daa27", + }, + data_received={"id": 9333363778}, + ), + }, + { + "notifier": "status-changes", + "title": "default", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "title": "codecov/changes", + "state": "success", + "message": "No indirect coverage changes found", + }, + data_received={"id": 9333363801}, + ), + }, + { + "notifier": "codecov-slack-app", + "title": "codecov-slack-app", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation="Successfully notified slack app", + data_sent={ + "repository": "example-python", + "owner": "ThiagoCodecov", + "comparison": { + "url": "https://myexamplewebsite.io/gh/ThiagoCodecov/example-python/commit/11daa27b1b74fd181836a64106f936a16404089c", + "message": "no change", + "coverage": "0.00", + "notation": "", + "head_commit": { + "commitid": "11daa27b1b74fd181836a64106f936a16404089c", + "branch": "thiago/base-no-base", + "message": "", + "author": "rolabuhasna", + "timestamp": "2019-02-01T17:59:47", + "ci_passed": True, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "85.00000", + "d": 0, + "diff": [ + 1, + 2, + 1, + 1, + 0, + "50.00000", + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "f": 3, + "h": 17, + "m": 3, + "n": 20, + "p": 0, + "s": 1, + }, + "pull": None, + }, + "base_commit": { + "commitid": "f0895290dc26668faeeb20ee5ccd4cc995925775", + "branch": "master", + "message": "", + "author": "bateslouis", + "timestamp": "2019-02-01T17:59:47", + "ci_passed": True, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "85.00000", + "d": 0, + "diff": [ + 1, + 2, + 1, + 1, + 0, + "50.00000", + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "f": 3, + "h": 17, + "m": 3, + "n": 20, + "p": 0, + "s": 1, + }, + "pull": None, + }, + "head_totals_c": "85.00000", + }, + }, + data_received=None, + ), + }, + ], + } + + @patch("requests.post") + @pytest.mark.django_db + def test_simple_call_status_and_notifiers( + self, + mock_post_request, + dbsession, + mocker, + codecov_vcr, + mock_storage, + mock_configuration, + is_not_first_pull, + ): + mock_post_request.return_value.status_code = 200 + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://myexamplewebsite.io" + ) + mocker.patch.object(NotifyTask, "app") + repository = RepositoryFactory.create( + owner__unencrypted_oauth_token=sample_token, + owner__username="joseph-sentry", + owner__service="github", + owner__service_id="136376984", + owner__email="joseph.sawaya@sentry.io", + name="codecov-demo", + image_token="abcdefghij", + ) + dbsession.add(repository) + dbsession.flush() + repository.owner.plan_activated_users = [repository.owner.ownerid] + dbsession.add(repository) + dbsession.flush() + head_commitid = "5601846871b8142ab0df1e0b8774756c658bcc7d" + master_sha = "5b174c2b40d501a70c479e91025d5109b1ad5c1b" + master_commit = CommitFactory.create( + message="", + pullid=None, + branch="main", + commitid=master_sha, + repository=repository, + author=repository.owner, + ) + # create another pull so that we don't trigger the 1st time comment message + dbsession.add(PullFactory.create(repository=repository, pullid=8)) + commit = CommitFactory.create( + message="", + pullid=9, + branch="test", + commitid=head_commitid, + parent_commit_id=master_commit.commitid, + repository=repository, + author=repository.owner, + ) + dbsession.add(commit) + dbsession.add(master_commit) + dbsession.flush() + task = NotifyTask() + with open("tasks/tests/samples/sample_chunks_1.txt") as f: + content = f.read().encode() + archive_hash = ArchiveService.get_archive_hash(commit.repository) + chunks_url = f"v4/repos/{archive_hash}/commits/{commit.commitid}/chunks.txt" + mock_storage.write_file("archive", chunks_url, content) + master_chunks_url = ( + f"v4/repos/{archive_hash}/commits/{master_commit.commitid}/chunks.txt" + ) + mock_storage.write_file("archive", master_chunks_url, content) + result = task.run_impl_within_lock( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml={ + "comment": { + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + "require_changes": [ + CoverageCommentRequiredChanges.no_requirements.value + ], + "require_base": False, + "require_head": True, + }, + "coverage": { + "status": {"project": True, "patch": True, "changes": True}, + "notify": { + "webhook": { + "default": { + "url": "https://6da6786648c8a8e5b8b09bc6562af8b4.m.pipedream.net" + } + }, + "slack": { + "default": { + "url": "https://hooks.slack.com/services/testkylhk/test01hg7/testohfnij1e83uy4xt8sxml" + } + }, + }, + }, + }, + ) + expected_author_dict = { + "username": "joseph-sentry", + "service_id": repository.owner.service_id, + "email": repository.owner.email, + "service": "github", + "name": repository.owner.name, + } + expected_result = { + "notified": True, + "notifications": [ + { + "notifier": "WebhookNotifier", + "title": "default", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "repo": { + "url": "https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo", + "service_id": repository.service_id, + "name": "codecov-demo", + "private": True, + }, + "head": { + "author": expected_author_dict, + "url": "https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/commit/5601846871b8142ab0df1e0b8774756c658bcc7d", + "timestamp": "2019-02-01T17:59:47", + "totals": { + "files": 3, + "lines": 20, + "hits": 17, + "misses": 3, + "partials": 0, + "coverage": "85.00000", + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 0, + "complexity_total": 0, + "diff": [ + 1, + 2, + 1, + 1, + 0, + "50.00000", + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + "commitid": head_commitid, + "service_url": f"https://github.com/joseph-sentry/codecov-demo/commit/{head_commitid}", + "branch": "test", + "message": "", + }, + "base": { + "author": expected_author_dict, + "url": "https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b", + "timestamp": "2019-02-01T17:59:47", + "totals": { + "files": 3, + "lines": 20, + "hits": 17, + "misses": 3, + "partials": 0, + "coverage": "85.00000", + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 0, + "complexity_total": 0, + "diff": [ + 1, + 2, + 1, + 1, + 0, + "50.00000", + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + "commitid": "5b174c2b40d501a70c479e91025d5109b1ad5c1b", + "service_url": "https://github.com/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b", + "branch": "main", + "message": "", + }, + "compare": { + "url": "https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9", + "message": "no change", + "coverage": Decimal("0.00"), + "notation": "", + }, + "owner": { + "username": "joseph-sentry", + "service_id": repository.owner.service_id, + "service": "github", + }, + "pull": { + "head": { + "commit": "5601846871b8142ab0df1e0b8774756c658bcc7d", + "branch": "master", + }, + "number": "9", + "base": { + "commit": "5b174c2b40d501a70c479e91025d5109b1ad5c1b", + "branch": "master", + }, + "open": True, + "id": 9, + "merged": False, + }, + }, + data_received=None, + ), + }, + { + "notifier": "SlackNotifier", + "title": "default", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "text": "Coverage for *no change* `` on `test` is `85.00000%` via ``", + "author_name": "Codecov", + "author_link": "https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/commit/5601846871b8142ab0df1e0b8774756c658bcc7d", + "attachments": [], + }, + data_received=None, + ), + }, + { + "notifier": "status-project", + "title": "default", + "result": NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="already_done", + data_sent={ + "title": "codecov/project", + "state": "success", + "message": f"85.00% (+0.00%) compared to {master_sha[:7]}", + }, + data_received=None, + ), + }, + { + "notifier": "status-patch", + "title": "default", + "result": NotificationResult( + notification_attempted=False, + notification_successful=None, + explanation="already_done", + data_sent={ + "title": "codecov/patch", + "state": "success", + "message": f"Coverage not affected when comparing {master_sha[:7]}...{head_commitid[:7]}", + }, + data_received=None, + ), + }, + { + "notifier": "status-changes", + "title": "default", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "title": "codecov/changes", + "state": "success", + "message": "No indirect coverage changes found", + }, + data_received={"id": 24846000025}, + ), + }, + { + "notifier": "codecov-slack-app", + "title": "codecov-slack-app", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation="Successfully notified slack app", + data_sent={ + "repository": "codecov-demo", + "owner": "joseph-sentry", + "comparison": { + "url": "https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9", + "message": "no change", + "coverage": "0.00", + "notation": "", + "head_commit": { + "commitid": "5601846871b8142ab0df1e0b8774756c658bcc7d", + "branch": "test", + "message": "", + "author": "joseph-sentry", + "timestamp": "2019-02-01T17:59:47", + "ci_passed": True, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "85.00000", + "d": 0, + "diff": [ + 1, + 2, + 1, + 1, + 0, + "50.00000", + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "f": 3, + "h": 17, + "m": 3, + "n": 20, + "p": 0, + "s": 1, + }, + "pull": 9, + }, + "base_commit": { + "commitid": "5b174c2b40d501a70c479e91025d5109b1ad5c1b", + "branch": "main", + "message": "", + "author": "joseph-sentry", + "timestamp": "2019-02-01T17:59:47", + "ci_passed": True, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "85.00000", + "d": 0, + "diff": [ + 1, + 2, + 1, + 1, + 0, + "50.00000", + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "f": 3, + "h": 17, + "m": 3, + "n": 20, + "p": 0, + "s": 1, + }, + "pull": None, + }, + "head_totals_c": "85.00000", + }, + }, + data_received=None, + ), + }, + { + "notifier": "comment", + "title": "comment", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation=None, + data_sent={ + "message": [ + "## [Codecov](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9?dropdown=coverage&src=pr&el=h1) Report", + "All modified and coverable lines are covered by tests :white_check_mark:", + "> Project coverage is 85.00%. Comparing base [(`5b174c2`)](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/commit/5b174c2b40d501a70c479e91025d5109b1ad5c1b?dropdown=coverage&el=desc) to head [(`5601846`)](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/commit/5601846871b8142ab0df1e0b8774756c658bcc7d?dropdown=coverage&el=desc).", + "> Report is 2 commits behind head on main.", + "", + ":exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.", + "", + "[![Impacted file tree graph](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9/graphs/tree.svg?width=650&height=150&src=pr&token=abcdefghij)](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9?src=pr&el=tree)", + "", + "```diff", + "@@ Coverage Diff @@", + "## main #9 +/- ##", + "=======================================", + " Coverage 85.00% 85.00% ", + "=======================================", + " Files 3 3 ", + " Lines 20 20 ", + "=======================================", + " Hits 17 17 ", + " Misses 3 3 ", + "```", + "", + "| [Flag](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flags) | Coverage Δ | |", + "|---|---|---|", + "| [unit](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9/flags?src=pr&el=flag) | `85.00% <ø> (ø)` | |", + "", + "Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more." + "", + "", + "------", + "", + "[Continue to review full report in Codecov by Sentry](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9?dropdown=coverage&src=pr&el=continue).", + "> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)", + "> `Δ = absolute (impact)`, `ø = not affected`, `? = missing data`", + "> Powered by [Codecov](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9?dropdown=coverage&src=pr&el=footer). Last update [5b174c2...5601846](https://myexamplewebsite.io/gh/joseph-sentry/codecov-demo/pull/9?dropdown=coverage&src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).", + "", + ], + "commentid": None, + "pullid": 9, + }, + data_received={"id": 1699669573}, + ), + }, + ], + } + + assert len(result["notifications"]) == len(expected_result["notifications"]) + for expected, actual in zip( + sorted(result["notifications"], key=lambda x: x["notifier"]), + sorted(expected_result["notifications"], key=lambda x: x["notifier"]), + ): + assert ( + expected["result"].notification_attempted + == actual["result"].notification_attempted + ) + assert ( + expected["result"].notification_successful + == actual["result"].notification_successful + ) + assert expected["result"].explanation == actual["result"].explanation + assert expected["result"].data_sent.get("message") == actual[ + "result" + ].data_sent.get("message") + assert expected["result"].data_sent == actual["result"].data_sent + assert expected["result"].data_received == actual["result"].data_received + assert expected["result"] == actual["result"] + assert expected == actual + + sorted_result = sorted(result["notifications"], key=lambda x: x["notifier"]) + sorted_expected_result = sorted( + expected_result["notifications"], key=lambda x: x["notifier"] + ) + assert len(sorted_result) == len(sorted_expected_result) == 7 + assert sorted_result == sorted_expected_result + + result["notifications"] = sorted_result + expected_result["notifications"] = sorted_expected_result + assert result == expected_result + + pull = dbsession.query(Pull).filter_by(pullid=9, repoid=commit.repoid).first() + assert pull.commentid == "1699669573" + + @pytest.mark.django_db + def test_notifier_call_no_head_commit_report( + self, dbsession, mocker, codecov_vcr, mock_storage, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codecov.io" + ) + mocked_app = mocker.patch.object(NotifyTask, "app") + repository = RepositoryFactory.create( + owner__unencrypted_oauth_token=sample_token, + owner__username="ThiagoCodecov", + owner__service="github", + owner__service_id="44376991", + name="example-python", + ) + dbsession.add(repository) + dbsession.flush() + master_commit = CommitFactory.create( + message="", + pullid=None, + branch="master", + commitid="17a71a9a2f5335ed4d00496c7bbc6405f547a527", + repository=repository, + ) + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + repository=repository, + _report_json=None, + ) + dbsession.add(commit) + dbsession.add(master_commit) + dbsession.flush() + task = NotifyTask() + with open("tasks/tests/samples/sample_chunks_1.txt") as f: + content = f.read().encode() + archive_hash = ArchiveService.get_archive_hash(commit.repository) + master_chunks_url = ( + f"v4/repos/{archive_hash}/commits/{master_commit.commitid}/chunks.txt" + ) + mock_storage.write_file("archive", master_chunks_url, content) + result = task.run_impl_within_lock( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml={"coverage": {"status": {"project": True}}}, + ) + expected_result = { + "notified": False, + "notifications": None, + "reason": "no_head_report", + } + assert result == expected_result + + @pytest.mark.django_db + @patch("requests.post") + def test_notifier_call_no_head_commit_report_empty_upload( + self, + mock_post_request, + dbsession, + mocker, + codecov_vcr, + mock_storage, + mock_configuration, + mock_repo_provider, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codecov.io" + ) + mocked_app = mocker.patch.object(NotifyTask, "app") + repository = RepositoryFactory.create( + owner__unencrypted_oauth_token=sample_token, + owner__username="ThiagoCodecov", + owner__service="github", + owner__service_id="44376991", + name="example-python", + ) + dbsession.add(repository) + dbsession.flush() + master_commit = CommitFactory.create( + message="", + pullid=None, + branch="master", + commitid="17a71a9a2f5335ed4d00496c7bbc6405f547a527", + repository=repository, + ) + commit = CommitFactory.create( + message="", + pullid=1234, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + repository=repository, + _report_json=None, + ) + dbsession.add(commit) + dbsession.add(master_commit) + dbsession.flush() + pull = dbsession.query(Pull).filter_by(pullid=1234).first() + pull.repository = repository + pull.head = commit.commitid + pull.base = master_commit.commitid + dbsession.add(pull) + dbsession.flush() + + task = NotifyTask() + with open("tasks/tests/samples/sample_chunks_1.txt") as f: + content = f.read().encode() + archive_hash = ArchiveService.get_archive_hash(commit.repository) + master_chunks_url = ( + f"v4/repos/{archive_hash}/commits/{master_commit.commitid}/chunks.txt" + ) + mock_storage.write_file("archive", master_chunks_url, content) + + mocker.patch("tasks.notify.NotifyTask.fetch_and_update_whether_ci_passed") + mocker.patch( + "tasks.notify.fetch_and_update_pull_request_information_from_commit", + return_value=EnrichedPull(database_pull=pull, provider_pull=None), + ) + mock_repo_provider.get_commit_statuses = AsyncMock(return_value=None) + + result = task.run_impl_within_lock( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml={"coverage": {"status": {"project": True}}}, + empty_upload="pass", + ) + + assert result["notified"] + assert len(result["notifications"]) == 2 + assert result["notifications"][0]["result"] is not None + assert result["notifications"][1]["result"] is not None diff --git a/apps/worker/tasks/tests/integration/test_send_email_task.py b/apps/worker/tasks/tests/integration/test_send_email_task.py new file mode 100644 index 0000000000..4a1e4ec035 --- /dev/null +++ b/apps/worker/tasks/tests/integration/test_send_email_task.py @@ -0,0 +1,104 @@ +import pytest +import requests + +from database.tests.factories import OwnerFactory +from services.smtp import SMTPService +from tasks.send_email import SendEmailTask + +mock_smtp_config = {} + +to_addr = "test_to@codecov.io" +username = "test_username" + + +@pytest.mark.integration +class TestSendEmailTask: + def test_send_email_integration( + self, + mocker, + dbsession, + mock_storage, + mock_redis, + mock_configuration, + ): + SMTPService.connection = None + tls = mocker.patch("smtplib.SMTP.starttls") + login = mocker.patch("smtplib.SMTP.login") + owner = OwnerFactory.create(email=to_addr, username=username) + dbsession.add(owner) + dbsession.flush() + task = SendEmailTask() + + # make sure mailhog is not storing any other messages before + # running this test + res = requests.delete( + "http://mailhog:8025/api/v1/messages", + ) + + assert res.status_code == 200 + + result = task.run_impl( + dbsession, + owner.email, + "TestSubject", + "test", + "test@codecov.io", + username=owner.username, + ) + + assert result["email_successful"] == True + assert result["err_msg"] is None + + res = requests.get( + "http://mailhog:8025/api/v2/messages", + ) + + res = res.json() + mail = res["items"][0] + assert mail["To"] == [ + { + "Domain": "codecov.io", + "Mailbox": "test_to", + "Params": "", + "Relays": None, + } + ] + assert mail["From"] == { + "Domain": "codecov.io", + "Mailbox": "test", + "Params": "", + "Relays": None, + } + + mail_body = mail["Content"]["Body"].splitlines() + assert mail_body[1:6] == [ + 'Content-Type: text/plain; charset="utf-8"', + "Content-Transfer-Encoding: 7bit", + "", + "Test template test_username", + "", + ] + assert mail_body[7:-1] == [ + 'Content-Type: text/html; charset="utf-8"', + "Content-Transfer-Encoding: 7bit", + "MIME-Version: 1.0", + "", + "", + '', + "", + "", + ' ', + ' ', + " Document", + "", + "", + "", + "

    ", + " test template test_username", + "

    ", + "", + "", + "", + "", + ] + assert res["count"] == 1 diff --git a/apps/worker/tasks/tests/integration/test_status_set_error_task.py b/apps/worker/tasks/tests/integration/test_status_set_error_task.py new file mode 100644 index 0000000000..2d787967c2 --- /dev/null +++ b/apps/worker/tasks/tests/integration/test_status_set_error_task.py @@ -0,0 +1,33 @@ +import pytest + +from database.tests.factories import CommitFactory, RepositoryFactory +from tasks.status_set_error import StatusSetErrorTask + + +@pytest.mark.integration +class TestStatusSetErrorTask(object): + def test_set_error(self, dbsession, mocker, mock_configuration, codecov_vcr): + repository = RepositoryFactory.create( + owner__username="ThiagoCodecov", + owner__service="github", + name="example-python", + owner__unencrypted_oauth_token="909b86f2e90668589666e2b5b76966797cee4b24", + yaml={"coverage": {"status": {"project": {"default": {"target": 100}}}}}, + ) + dbsession.add(repository) + dbsession.flush() + + commit = CommitFactory.create( + message="", + branch="thiago/test-1", + commitid="e3b6c976efe88b2a3781dc8157485e46bf2ac7ab", + repository=repository, + ) + dbsession.add(commit) + + task = StatusSetErrorTask() + result = task.run_impl( + dbsession, repository.repoid, commit.commitid, message="Test err message" + ) + expected_result = {"status_set": True} + assert result == expected_result diff --git a/apps/worker/tasks/tests/integration/test_status_set_pending_task.py b/apps/worker/tasks/tests/integration/test_status_set_pending_task.py new file mode 100644 index 0000000000..048e006399 --- /dev/null +++ b/apps/worker/tasks/tests/integration/test_status_set_pending_task.py @@ -0,0 +1,37 @@ +import pytest + +from database.tests.factories import CommitFactory, RepositoryFactory +from tasks.status_set_pending import StatusSetPendingTask + + +@pytest.mark.integration +class TestStatusSetPendingTask(object): + def test_set_pending( + self, dbsession, mocker, mock_configuration, codecov_vcr, mock_redis + ): + repository = RepositoryFactory.create( + owner__username="ThiagoCodecov", + owner__service="github", + name="example-python", + owner__unencrypted_oauth_token="909b86f2e90668589666e2b5b76966797cee4b24", + yaml={"coverage": {"status": {"project": {"default": {"target": 100}}}}}, + ) + dbsession.add(repository) + dbsession.flush() + + commit = CommitFactory.create( + message="", + branch="some-branch", + commitid="e3b6c976efe88b2a3781dc8157485e46bf2ac7ab", + repository=repository, + ) + dbsession.add(commit) + + mock_redis.sismember.side_effect = [True] + + task = StatusSetPendingTask() + result = task.run_impl( + dbsession, repository.repoid, commit.commitid, commit.branch, True + ) + expected_result = {"status_set": True} + assert result == expected_result diff --git a/apps/worker/tasks/tests/integration/test_sync_pull.py b/apps/worker/tasks/tests/integration/test_sync_pull.py new file mode 100644 index 0000000000..1b138fc065 --- /dev/null +++ b/apps/worker/tasks/tests/integration/test_sync_pull.py @@ -0,0 +1,135 @@ +from pathlib import Path + +from database.tests.factories import CommitFactory, PullFactory, RepositoryFactory +from services.archive import ArchiveService +from tasks.sync_pull import PullSyncTask + +here = Path(__file__) + + +class TestPullSyncTask(object): + def test_call_task(self, dbsession, codecov_vcr, mock_storage, mocker, mock_redis): + mocker.patch.object(PullSyncTask, "app") + task = PullSyncTask() + repository = RepositoryFactory.create( + owner__username="ThiagoCodecov", + owner__service="github", + owner__service_id="44376991", + name="example-python", + owner__unencrypted_oauth_token="testduhiiri16grurxduwjexioy26ohqhaxvk67z", + ) + report_json = { + "files": { + "README.md": [ + 2, + [0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0], + [[0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0]], + [0, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + ], + "codecov.yaml": [ + 0, + [0, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0], + [[0, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "tests/test_sample.py": [ + 1, + [0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + [[0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + }, + "sessions": { + "0": { + "N": None, + "a": "v4/raw/2019-01-10/4434BC2A2EC4FCA57F77B473D83F928C/abf6d4df662c47e32460020ab14abf9303581429/9ccc55a1-8b41-4bb1-a946-ee7a33a7fb56.txt", + "c": None, + "d": 1547084427, + "e": None, + "f": ["unit"], + "j": None, + "n": None, + "p": None, + "t": [3, 20, 17, 3, 0, "85.00000", 0, 0, 0, 0, 0, 0, 0], + "": None, + } + }, + } + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create( + repository=repository, commitid="7a7153d24f76c9ad58f421bcac8276203d589b1a" + ) + head_commit = CommitFactory.create( + repository=repository, + commitid="6dc3afd80a8deea5ea949d284d996d58811cd01d", + branch="new_branch", + _report_json=report_json, + ) + archive_hash = ArchiveService.get_archive_hash(repository) + with open(here.parent.parent / "samples" / "sample_chunks_1.txt") as f: + head_chunks_url = ( + f"v4/repos/{archive_hash}/commits/{head_commit.commitid}/chunks.txt" + ) + content = f.read() + mock_storage.write_file("archive", head_chunks_url, content) + pull = PullFactory.create( + pullid=17, + repository=repository, + base=base_commit.commitid, + head=head_commit.commitid, + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + res = task.run_impl(dbsession, repoid=pull.repoid, pullid=pull.pullid) + assert { + "notifier_called": True, + "commit_updates_done": {"merged_count": 0, "soft_deleted_count": 0}, + "pull_updated": True, + "reason": "success", + } == res + assert len(pull.flare) == 1 + expected_flare = { + "_class": None, + "children": [ + { + "_class": None, + "color": "red", + "coverage": -1, + "lines": 10, + "name": "README.md", + }, + { + "_class": None, + "color": "#e1e1e1", + "coverage": 0, + "lines": 3, + "name": "codecov.yaml", + }, + { + "_class": None, + "children": [ + { + "_class": None, + "color": "#e1e1e1", + "coverage": 0, + "lines": 7, + "name": "test_sample.py", + } + ], + "color": "#e1e1e1", + "coverage": 0.0, + "lines": 7, + "name": "tests", + }, + ], + "color": "#e1e1e1", + "coverage": 0.0, + "lines": 20, + "name": "", + } + assert expected_flare["children"] == pull.flare[0]["children"] + assert expected_flare == pull.flare[0] + assert pull.diff is None diff --git a/apps/worker/tasks/tests/integration/test_timeseries_backfill.py b/apps/worker/tasks/tests/integration/test_timeseries_backfill.py new file mode 100644 index 0000000000..c44530e58f --- /dev/null +++ b/apps/worker/tasks/tests/integration/test_timeseries_backfill.py @@ -0,0 +1,70 @@ +import pytest +from shared.celery_config import timeseries_save_commit_measurements_task_name + +from database.models import MeasurementName +from database.tests.factories import CommitFactory, RepositoryFactory +from database.tests.factories.timeseries import DatasetFactory +from services.archive import ArchiveService +from tasks.timeseries_backfill import TimeseriesBackfillCommitsTask + + +@pytest.mark.integration +def test_backfill_dataset_run_impl(dbsession, mocker, mock_storage): + mocker.patch("tasks.timeseries_backfill.is_timeseries_enabled", return_value=True) + mocked_app = mocker.patch.object( + TimeseriesBackfillCommitsTask, + "app", + tasks={ + timeseries_save_commit_measurements_task_name: mocker.MagicMock(), + }, + ) + + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + coverage_dataset = DatasetFactory.create( + name=MeasurementName.coverage.value, repository_id=repository.repoid + ) + dbsession.add(coverage_dataset) + flag_coverage_dataset = DatasetFactory.create( + name=MeasurementName.flag_coverage.value, repository_id=repository.repoid + ) + dbsession.add(flag_coverage_dataset) + dbsession.flush() + + commit = CommitFactory.create( + repository=repository, + ) + dbsession.add(commit) + dbsession.flush() + + with open("tasks/tests/samples/sample_chunks_1.txt") as f: + content = f.read().encode() + archive_hash = ArchiveService.get_archive_hash(commit.repository) + chunks_url = f"v4/repos/{archive_hash}/commits/{commit.commitid}/chunks.txt" + mock_storage.write_file("archive", chunks_url, content) + master_chunks_url = ( + f"v4/repos/{archive_hash}/commits/{commit.commitid}/chunks.txt" + ) + mock_storage.write_file("archive", master_chunks_url, content) + + task = TimeseriesBackfillCommitsTask() + dataset_names = [ + MeasurementName.coverage.value, + MeasurementName.flag_coverage.value, + ] + res = task.run_impl( + dbsession, + commit_ids=[commit.id_], + dataset_names=dataset_names, + ) + assert res == {"successful": True} + mocked_app.tasks[ + timeseries_save_commit_measurements_task_name + ].apply_async.assert_called_once_with( + kwargs={ + "commitid": commit.commitid, + "repoid": commit.repoid, + "dataset_names": dataset_names, + } + ) diff --git a/apps/worker/tasks/tests/integration/test_upload_e2e.py b/apps/worker/tasks/tests/integration/test_upload_e2e.py new file mode 100644 index 0000000000..9112e659be --- /dev/null +++ b/apps/worker/tasks/tests/integration/test_upload_e2e.py @@ -0,0 +1,631 @@ +import json +import random +from functools import partial +from typing import Iterable +from uuid import uuid4 + +import pytest +from redis import Redis +from shared.helpers.redis import get_redis_connection +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ReportLine +from shared.utils.sessions import SessionType +from shared.yaml import UserYaml +from sqlalchemy.orm import Session as DbSession + +from database.models.core import Commit, CompareCommit, Repository +from database.models.reports import Upload +from database.tests.factories import CommitFactory, RepositoryFactory +from database.tests.factories.core import PullFactory +from services.archive import ArchiveService +from services.report import ReportService +from tasks.tests.utils import hook_repo_provider, hook_session, run_tasks +from tasks.upload import upload_task +from tests.helpers import mock_all_plans_and_tiers + + +def write_raw_upload( + redis: Redis, + archive_service: ArchiveService, + repoid: int, + commitid: str, + contents: bytes, + upload_json: dict | None = None, +): + report_id = uuid4().hex + written_path = f"upload/{report_id}.txt" + archive_service.write_file(written_path, contents) + + upload_json = upload_json or {} + upload_json.update({"reportid": report_id, "url": written_path}) + upload = json.dumps(upload_json) + + redis_key = f"uploads/{repoid}/{commitid}" + redis.lpush(redis_key, upload) + + return upload_json + + +def lines(lines: Iterable[tuple[int, ReportLine]]) -> list[tuple[int, int]]: + return list(((lineno, line.coverage) for lineno, line in lines)) + + +def get_base_report(): + file_a = ReportFile("a.rs") + file_a.append(1, ReportLine.create(coverage=1, sessions=[[0, 1]])) + file_a.append(1, ReportLine.create(coverage=2, sessions=[[1, 2]])) + + file_b = ReportFile("b.rs") + file_b.append(1, ReportLine.create(coverage=3, sessions=[[0, 3]])) + file_b.append(2, ReportLine.create(coverage=5, sessions=[[1, 5]])) + report = Report() + report.append(file_a) + report.append(file_b) + return report + + +def setup_base_commit(repository: Repository, dbsession: DbSession) -> Commit: + base_report = get_base_report() + commit = CommitFactory(repository=repository) + dbsession.add(commit) + dbsession.flush() + report_service = ReportService({}) + report_service.save_full_report(commit, base_report) + return commit + + +def setup_mock_get_compare( + base_commit: Commit, head_commit: Commit, mock_repo_provider +): + get_compare = { + "diff": { + "files": { + "a.rs": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["1", "3", "1", "4"], + "lines": [ + " fn main() {", + '- println!("Salve!");', + '+ println!("Hello World!");', + '+ println!(":wink:");', + " }", + ], + } + ], + } + } + }, + "commits": [ + { + "commitid": base_commit.commitid, + "message": "BASE commit", + "timestamp": base_commit.timestamp, + "author": { + "id": base_commit.author.service_id, + "username": base_commit.author.username, + }, + }, + { + "commitid": head_commit.commitid, + "message": "HEAD commit", + "timestamp": head_commit.timestamp, + "author": { + "id": head_commit.author.service_id, + "username": head_commit.author.username, + }, + }, + ], + } + mock_repo_provider.get_compare.return_value = get_compare + + +def setup_mocks( + mocker, + dbsession: DbSession, + mock_configuration, + mock_repo_provider, + user_yaml=None, +): + # patch various `get_db_session` imports + hook_session(mocker, dbsession) + # to not close the session after each task + mocker.patch("tasks.base.BaseCodecovTask.wrap_up_dbsession") + # patch various `get_repo_provider_service` imports + hook_repo_provider(mocker, mock_repo_provider) + # avoid some calls reaching out to git providers + mocker.patch("tasks.upload.UploadTask.possibly_setup_webhooks", return_value=True) + mocker.patch( + "tasks.upload.fetch_commit_yaml_and_possibly_store", + return_value=UserYaml(user_yaml or {}), + ) + mocker.patch( + "tasks.compute_comparison.get_current_yaml", + return_value=UserYaml(user_yaml or {}), + ) + # disable all the tasks being emitted from `UploadFinisher`. + # ideally, we would really want to test their outcomes as well. + mocker.patch("tasks.notify.NotifyTask.run_impl") + mocker.patch("tasks.sync_pull.PullSyncTask.run_impl") + mocker.patch("tasks.save_commit_measurements.SaveCommitMeasurementsTask.run_impl") + + # force `report_json` to be written out to storage + mock_configuration.set_params( + { + "setup": { + "save_report_data_in_storage": { + "commit_report": "general_access", + }, + } + } + ) + + +@pytest.mark.integration +@pytest.mark.django_db(databases={"default"}, transaction=True) +def test_full_upload( + dbsession: DbSession, + mocker, + mock_repo_provider, + mock_storage, + mock_configuration, +): + mock_all_plans_and_tiers() + setup_mocks(mocker, dbsession, mock_configuration, mock_repo_provider) + + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + + # setup a base commit (with a report) to compare against the current one + base_commit = setup_base_commit(repository, dbsession) + + repoid = repository.repoid + commitid = uuid4().hex + # BASE and HEAD are connected in a PR + pull = PullFactory( + pullid=12, + repository=repository, + compared_to=base_commit.commitid, + ) + commit = CommitFactory.create( + repository=repository, commitid=commitid, pullid=12, _report_json=None + ) + dbsession.add(pull) + dbsession.flush() + + dbsession.add(commit) + dbsession.flush() + + setup_mock_get_compare(base_commit, commit, mock_repo_provider) + + archive_service = ArchiveService(repository) + do_upload = partial( + write_raw_upload, + get_redis_connection(), + archive_service, + repoid, + commitid, + ) + + report_service = ReportService({}) + commit_report = report_service.initialize_and_save_report(commit) + + upload_id = 2**33 + int(random.random() * 2**15) + + first_upload_json = do_upload( + b""" +a.rs +<<<<<< network +# path=coverage.lcov +SF:a.rs +DA:1,1 +end_of_record +""", + {"flags": [], "upload_id": upload_id}, + ) + + first_upload = report_service.create_report_upload(first_upload_json, commit_report) + first_upload.flags = [] + dbsession.flush() + + # force the upload to have a really high ID: + dbsession.execute( + f"UPDATE reports_upload SET id={upload_id} WHERE id={first_upload.id}" + ) + + with run_tasks(): + upload_task.apply_async( + kwargs={ + "repoid": repoid, + "commitid": commitid, + } + ) + + do_upload( + b""" +a.rs +<<<<<< network +# path=coverage.lcov +SF:a.rs +DA:2,2 +DA:3,1 +end_of_record +""" + ) + do_upload( + b""" +b.rs +<<<<<< network +# path=coverage.lcov +SF:b.rs +DA:1,3 +end_of_record +""" + ) + do_upload( + b""" +b.rs +<<<<<< network +# path=coverage.lcov +SF:b.rs +DA:2,5 +end_of_record +""" + ) + + with run_tasks(): + upload_task.apply_async( + kwargs={ + "repoid": repoid, + "commitid": commitid, + } + ) + + # we expect the following files: + # chunks+json for the base commit + # 4 * raw uploads + # chunks+json, and `comparison` for the finished upload + archive = mock_storage.storage["archive"] + assert len(archive) == 2 + 4 + 3 + + report_service = ReportService(UserYaml({})) + report = report_service.get_existing_report_for_commit(commit, report_code=None) + + assert report + assert set(report.files) == {"a.rs", "b.rs"} + + a = report.get("a.rs") + assert a + assert lines(a.lines) == [ + (1, 1), + (2, 2), + (3, 1), + ] + + b = report.get("b.rs") + assert b + assert lines(b.lines) == [ + (1, 3), + (2, 5), + ] + + # Adding one more upload + + do_upload( + b""" +c.rs +<<<<<< network +# path=coverage.lcov +SF:c.rs +DA:2,4 +end_of_record +""" + ) + + with run_tasks(): + upload_task.apply_async( + kwargs={ + "repoid": repoid, + "commitid": commitid, + } + ) + + report = report_service.get_existing_report_for_commit(commit, report_code=None) + + assert report + assert set(report.files) == {"a.rs", "b.rs", "c.rs"} + + c = report.get("c.rs") + assert c + assert lines(c.lines) == [ + (2, 4), + ] + + assert len(archive) == 2 + 5 + 3 + repo_hash = ArchiveService.get_archive_hash(repository) + raw_chunks_path = f"v4/repos/{repo_hash}/commits/{commitid}/chunks.txt" + assert raw_chunks_path in archive + raw_files_sessions_path = f"v4/repos/{repo_hash}/commits/{commitid}/json_data/commits/report_json/{commitid}.json" + assert raw_files_sessions_path in archive + + comparison: CompareCommit = ( + dbsession.query(CompareCommit) + .filter( + CompareCommit.base_commit_id == base_commit.id, + CompareCommit.compare_commit_id == commit.id, + ) + .first() + ) + assert comparison is not None + assert comparison.error is None + assert comparison.state == "processed" + assert comparison.patch_totals == { + "hits": 2, + "misses": 0, + "coverage": 1, + "partials": 0, + } + + +@pytest.mark.integration +@pytest.mark.django_db(databases={"default"}, transaction=True) +def test_full_carryforward( + dbsession: DbSession, + mocker, + mock_repo_provider, + mock_storage, + mock_configuration, +): + mock_all_plans_and_tiers() + user_yaml = {"flag_management": {"default_rules": {"carryforward": True}}} + setup_mocks( + mocker, dbsession, mock_configuration, mock_repo_provider, user_yaml=user_yaml + ) + mocker.patch("tasks.compute_comparison.ComputeComparisonTask.run_impl") + + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + + repoid = repository.repoid + commitid = uuid4().hex + base_commit = CommitFactory.create(repository=repository, commitid=commitid) + dbsession.add(base_commit) + dbsession.flush() + + archive_service = ArchiveService(repository) + do_upload = partial( + write_raw_upload, + get_redis_connection(), + archive_service, + repoid, + commitid, + ) + + do_upload( + b""" +a.rs +<<<<<< network +# path=coverage.lcov +SF:a.rs +DA:1,1 +end_of_record +""", + {"flags": "a"}, + ) + do_upload( + b""" +a.rs +<<<<<< network +# path=coverage.lcov +SF:a.rs +DA:2,2 +DA:3,1 +end_of_record +""", + {"flags": "a"}, + ) + do_upload( + b""" +b.rs +<<<<<< network +# path=coverage.lcov +SF:b.rs +DA:1,3 +end_of_record +""", + {"flags": "b"}, + ) + do_upload( + b""" +b.rs +<<<<<< network +# path=coverage.lcov +SF:b.rs +DA:2,5 +end_of_record +""", + {"flags": "b"}, + ) + + with run_tasks(): + upload_task.apply_async( + kwargs={ + "repoid": repoid, + "commitid": commitid, + } + ) + + report_service = ReportService(UserYaml({})) + report = report_service.get_existing_report_for_commit( + base_commit, report_code=None + ) + assert report + + base_sessions = report.sessions + + assert set(report.files) == {"a.rs", "b.rs"} + + a = report.get("a.rs") + assert a + assert lines(a.lines) == [ + (1, 1), + (2, 2), + (3, 1), + ] + + b = report.get("b.rs") + assert b + assert lines(b.lines) == [ + (1, 3), + (2, 5), + ] + + # Then, upload only *half* of the reports using carry-forward logic: + + commitid = uuid4().hex + commit = CommitFactory.create( + repository=repository, + commitid=commitid, + _report_json=None, + parent_commit_id=base_commit.commitid, + ) + dbsession.add(commit) + dbsession.flush() + + # BASE and HEAD are connected in a PR + pull = PullFactory( + pullid=12, + repository=repository, + compared_to=base_commit.commitid, + ) + dbsession.add(pull) + dbsession.flush() + setup_mock_get_compare(base_commit, commit, mock_repo_provider) + + do_upload = partial( + write_raw_upload, + get_redis_connection(), + archive_service, + repoid, + commitid, + ) + + do_upload( + b""" +a.rs +<<<<<< network +# path=coverage.lcov +SF:a.rs +DA:1,1 +end_of_record +""", + {"flags": "a"}, + ) + + with run_tasks(): + upload_task.apply_async( + kwargs={ + "repoid": repoid, + "commitid": commitid, + } + ) + + # with only one upload being processed so far, we still expect all "b" sessions to still exist + report = report_service.get_existing_report_for_commit(commit, report_code=None) + + assert report + assert set(report.files) == {"a.rs", "b.rs"} + + a = report.get("a.rs") + assert a + assert lines(a.lines) == [ + (1, 1), + ] + + b = report.get("b.rs") + assert b + assert lines(b.lines) == [ + (1, 3), + (2, 5), + ] + + sessions = report.sessions + # we expect there to be a total of 3 sessions, two of which are carriedforward + assert len(sessions) == 3 + carriedforward_sessions = sum( + 1 for s in sessions.values() if s.session_type == SessionType.carriedforward + ) + assert carriedforward_sessions == 2 + + # the `Upload`s in the database should match the `sessions` in the report: + uploads = ( + dbsession.query(Upload).filter(Upload.report_id == commit.report.id_).all() + ) + assert {upload.order_number for upload in uploads} == { + session.id for session in sessions.values() + } + + # and then overwrite data related to "b" as well + do_upload( + b""" +b.rs +<<<<<< network +# path=coverage.lcov +SF:b.rs +DA:1,3 +end_of_record +""", + {"flags": "b"}, + ) + + with run_tasks(): + upload_task.apply_async( + kwargs={ + "repoid": repoid, + "commitid": commitid, + } + ) + report = report_service.get_existing_report_for_commit(commit, report_code=None) + + assert report + assert set(report.files) == {"a.rs", "b.rs"} + + a = report.get("a.rs") + assert a + assert lines(a.lines) == [ + (1, 1), + ] + + b = report.get("b.rs") + assert b + assert lines(b.lines) == [ + (1, 3), + ] + + assert len(report.sessions) == 2 + uploads = ( + dbsession.query(Upload).filter(Upload.report_id == commit.report.id_).all() + ) + assert {upload.order_number for upload in uploads} == { + session.id for session in report.sessions.values() + } + + # just as a sanity check: any cleanup for the followup commit did not touch + # data of the base commit: + uploads = ( + dbsession.query(Upload).filter(Upload.report_id == base_commit.report.id_).all() + ) + assert {upload.order_number for upload in uploads} == { + session.id for session in base_sessions.values() + } + + # we expect the following files: + # chunks+json for the base commit + # 6 * raw uploads + # chunks+json for the carryforwarded commit (no `comparison`) + archive = mock_storage.storage["archive"] + assert len(archive) == 2 + 6 + 2 diff --git a/apps/worker/tasks/tests/samples/sample_chunks_1.txt b/apps/worker/tasks/tests/samples/sample_chunks_1.txt new file mode 100644 index 0000000000..a6ffbaa997 --- /dev/null +++ b/apps/worker/tasks/tests/samples/sample_chunks_1.txt @@ -0,0 +1,39 @@ +{} +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[0, null, [[0, 0], [1, 0]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 1]]] +[1, null, [[0, 1], [1, 1]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 1]]] +[1, null, [[0, 0], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[0, null, [[0, 0], [1, 0]]] \ No newline at end of file diff --git a/apps/worker/tasks/tests/samples/sample_chunks_4_sessions.txt b/apps/worker/tasks/tests/samples/sample_chunks_4_sessions.txt new file mode 100644 index 0000000000..50f3cc7796 --- /dev/null +++ b/apps/worker/tasks/tests/samples/sample_chunks_4_sessions.txt @@ -0,0 +1,237 @@ +{} +[1, null, [[0, 0, null, null, null], [2, 1, null, null, null]]] +["2/2", null, [[2, 1, null, null, null], [0, "1/2", null, null, null]]] +["1/2", null, [[0, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["2/2", null, [[0, 0, null, null, null], [1, 1, null, null, null], [3, "1/2", null, null, null]]] +["2/2", null, [[3, 0, null, null, null], [0, 1, null, null, null], [1, "1/2", null, null, null]]] +["3/3", null, [[2, 0, null, null, null], [1, 1, null, null, null], [0, "1/3", null, null, null]]] +["3/3", null, [[3, 0, null, null, null], [0, 1, null, null, null], [1, "1/3", null, null, null]]] +["3/3", null, [[3, 0, null, null, null], [1, 1, null, null, null], [0, "1/3", null, null, null]]] +["3/3", null, [[3, 0, null, null, null], [1, 1, null, null, null], [2, "1/3", null, null, null]]] +["1/2", null, [[2, 0, null, null, null], [1, "1/2", null, null, null], [0, "1/3", null, null, null]]] +["2/2", null, [[0, 1, null, null, null], [2, "1/2", null, null, null], [1, "1/3", null, null, null]]] +["2/2", null, [[2, 1, null, null, null], [3, "1/2", null, null, null], [0, "1/3", null, null, null]]] +["2/2", null, [[0, 0, null, null, null], [3, 1, null, null, null], [2, "1/2", null, null, null], [1, "1/3", null, null, null]]] +["2/2", null, [[3, 0, null, null, null], [1, 1, null, null, null], [0, "1/2", null, null, null], [2, "1/3", null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +["1/3", null, [[0, "1/3", null, null, null]]] +["1/3", null, [[2, 0, null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[0, 1, null, null, null], [3, "1/2", null, null, null]]] +["2/2", null, [[1, 0, null, null, null], [0, 1, null, null, null], [3, "1/2", null, null, null]]] +["3/3", null, [[2, 0, null, null, null], [0, 1, null, null, null], [3, "1/3", null, null, null]]] +["1/2", null, [[0, 0, null, null, null], [3, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["2/2", null, [[0, 1, null, null, null], [3, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["2/2", null, [[2, 1, null, null, null], [0, "1/2", null, null, null], [1, "1/3", null, null, null]]] +["2/2", null, [[2, 1, null, null, null], [1, "1/2", null, null, null], [0, "1/3", null, null, null]]] +["2/2", null, [[3, 0, null, null, null], [1, 1, null, null, null], [2, "1/2", null, null, null], [0, "1/3", null, null, null]]] +["2/2", null, [[3, 0, null, null, null], [2, 1, null, null, null], [1, "1/2", null, null, null], [0, "1/3", null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[2, 1, null, null, null]]] +["1/3", null, [[3, "1/3", null, null, null]]] +[1, null, [[0, 0, null, null, null], [1, 1, null, null, null]]] +["1/2", null, [[3, 0, null, null, null], [2, "1/2", null, null, null]]] +["2/2", null, [[3, 1, null, null, null], [1, "1/2", null, null, null]]] +["3/3", null, [[1, 1, null, null, null], [2, "1/3", null, null, null]]] +["1/2", null, [[1, "1/2", null, null, null], [0, "1/3", null, null, null]]] +["3/3", null, [[0, 0, null, null, null], [2, 1, null, null, null], [1, "1/3", null, null, null]]] +["3/3", null, [[0, 0, null, null, null], [3, 1, null, null, null], [2, "1/3", null, null, null]]] +["3/3", null, [[3, 0, null, null, null], [0, 1, null, null, null], [2, "1/3", null, null, null]]] +["1/2", null, [[1, 0, null, null, null], [2, "1/2", null, null, null], [0, "1/3", null, null, null]]] +["2/2", null, [[1, 0, null, null, null], [2, 1, null, null, null], [3, "1/2", null, null, null], [0, "1/3", null, null, null]]] +["2/2", null, [[3, 0, null, null, null], [0, 1, null, null, null], [1, "1/2", null, null, null], [2, "1/3", null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[1, 1, null, null, null]]] +[1, null, [[3, 0, null, null, null], [2, 1, null, null, null]]] +["1/2", null, [[1, 0, null, null, null], [3, "1/2", null, null, null]]] +["1/2", null, [[3, 0, null, null, null], [1, "1/2", null, null, null]]] +["1/3", null, [[0, 0, null, null, null], [2, "1/3", null, null, null]]] +["1/2", null, [[1, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[1, 0, null, null, null], [3, 1, null, null, null], [2, "1/2", null, null, null]]] +["2/2", null, [[3, 0, null, null, null], [1, 1, null, null, null], [0, "1/2", null, null, null]]] +["3/3", null, [[1, 0, null, null, null], [0, 1, null, null, null], [3, "1/3", null, null, null]]] +["1/2", null, [[0, 0, null, null, null], [1, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["1/2", null, [[0, 0, null, null, null], [2, "1/2", null, null, null], [1, "1/3", null, null, null]]] +["1/2", null, [[0, 0, null, null, null], [3, "1/2", null, null, null], [1, "1/3", null, null, null]]] +["1/2", null, [[2, 0, null, null, null], [1, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[1, 1, null, null, null], [3, "1/2", null, null, null], [0, "1/3", null, null, null]]] +["2/2", null, [[2, 1, null, null, null], [1, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[2, 1, null, null, null], [3, "1/2", null, null, null], [1, "1/3", null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +["1/3", null, [[2, "1/3", null, null, null]]] +[1, null, [[3, 0, null, null, null], [0, 1, null, null, null]]] +["1/2", null, [[0, 0, null, null, null], [2, "1/2", null, null, null]]] +["2/2", null, [[1, 1, null, null, null], [2, "1/2", null, null, null]]] +["2/2", null, [[3, 1, null, null, null], [2, "1/2", null, null, null]]] +["1/2", null, [[3, "1/2", null, null, null], [0, "1/3", null, null, null]]] +["2/2", null, [[3, 0, null, null, null], [2, 1, null, null, null], [0, "1/2", null, null, null]]] +["3/3", null, [[0, 0, null, null, null], [2, 1, null, null, null], [3, "1/3", null, null, null]]] +["1/2", null, [[1, 0, null, null, null], [0, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["2/2", null, [[1, 1, null, null, null], [2, "1/2", null, null, null], [0, "1/3", null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +["1/3", null, [[1, "1/3", null, null, null]]] +[1, null, [[2, 0, null, null, null], [0, 1, null, null, null]]] +["1/2", null, [[1, 0, null, null, null], [2, "1/2", null, null, null]]] +["1/2", null, [[3, 0, null, null, null], [0, "1/2", null, null, null]]] +["3/3", null, [[1, 1, null, null, null], [3, "1/3", null, null, null]]] +["3/3", null, [[3, 1, null, null, null], [2, "1/3", null, null, null]]] +["1/2", null, [[0, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[0, 0, null, null, null], [2, 1, null, null, null], [3, "1/2", null, null, null]]] +["3/3", null, [[0, 0, null, null, null], [1, 1, null, null, null], [2, "1/3", null, null, null]]] +["3/3", null, [[2, 0, null, null, null], [1, 1, null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[0, 0, null, null, null], [2, 1, null, null, null], [1, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[1, 0, null, null, null], [0, 1, null, null, null], [2, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[1, 0, null, null, null], [0, 1, null, null, null], [3, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["2/2", null, [[2, 0, null, null, null], [0, 1, null, null, null], [3, "1/2", null, null, null], [1, "1/3", null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +[0, null, [[1, 0, null, null, null]]] +["1/2", null, [[1, 0, null, null, null], [0, "1/2", null, null, null]]] +["2/2", null, [[1, 1, null, null, null], [3, "1/2", null, null, null]]] +["2/2", null, [[3, 1, null, null, null], [0, "1/2", null, null, null]]] +["3/3", null, [[3, 1, null, null, null], [1, "1/3", null, null, null]]] +["2/2", null, [[0, 0, null, null, null], [2, 1, null, null, null], [1, "1/2", null, null, null]]] +["3/3", null, [[1, 0, null, null, null], [3, 1, null, null, null], [0, "1/3", null, null, null]]] +["2/2", null, [[0, 0, null, null, null], [2, 1, null, null, null], [3, "1/2", null, null, null], [1, "1/3", null, null, null]]] +["2/2", null, [[3, 0, null, null, null], [0, 1, null, null, null], [2, "1/2", null, null, null], [1, "1/3", null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[0, 0, null, null, null], [3, 1, null, null, null]]] +[1, null, [[2, 0, null, null, null], [3, 1, null, null, null]]] +["2/2", null, [[2, 1, null, null, null], [1, "1/2", null, null, null]]] +["1/2", null, [[2, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[0, 0, null, null, null], [3, 1, null, null, null], [2, "1/2", null, null, null]]] +["2/2", null, [[2, 0, null, null, null], [1, 1, null, null, null], [0, "1/2", null, null, null]]] +["3/3", null, [[0, 0, null, null, null], [1, 1, null, null, null], [3, "1/3", null, null, null]]] +["1/2", null, [[1, 0, null, null, null], [2, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[1, 1, null, null, null], [0, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["2/2", null, [[2, 1, null, null, null], [0, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[2, 0, null, null, null], [1, 1, null, null, null], [3, "1/2", null, null, null], [0, "1/3", null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[3, 0, null, null, null], [1, 1, null, null, null]]] +["1/2", null, [[2, 0, null, null, null], [0, "1/2", null, null, null]]] +["1/2", null, [[2, 0, null, null, null], [1, "1/2", null, null, null]]] +["1/3", null, [[1, 0, null, null, null], [2, "1/3", null, null, null]]] +["1/2", null, [[3, "1/2", null, null, null], [1, "1/3", null, null, null]]] +["2/2", null, [[2, 0, null, null, null], [0, 1, null, null, null], [1, "1/2", null, null, null]]] +["2/2", null, [[2, 0, null, null, null], [3, 1, null, null, null], [0, "1/2", null, null, null]]] +["3/3", null, [[3, 0, null, null, null], [2, 1, null, null, null], [1, "1/3", null, null, null]]] +["1/2", null, [[1, 0, null, null, null], [3, "1/2", null, null, null], [0, "1/3", null, null, null]]] +["2/2", null, [[1, 1, null, null, null], [3, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["2/2", null, [[2, 0, null, null, null], [0, 1, null, null, null], [1, "1/2", null, null, null], [3, "1/3", null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +[0, null, [[2, 0, null, null, null]]] +["1/2", null, [[1, "1/2", null, null, null]]] +["1/3", null, [[0, 0, null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[1, 1, null, null, null], [0, "1/2", null, null, null]]] +["3/3", null, [[1, 1, null, null, null], [0, "1/3", null, null, null]]] +["3/3", null, [[2, 1, null, null, null], [3, "1/3", null, null, null]]] +["1/2", null, [[2, "1/2", null, null, null], [1, "1/3", null, null, null]]] +["2/2", null, [[1, 0, null, null, null], [0, 1, null, null, null], [2, "1/2", null, null, null]]] +["2/2", null, [[1, 0, null, null, null], [2, 1, null, null, null], [0, "1/2", null, null, null]]] +["2/2", null, [[2, 0, null, null, null], [3, 1, null, null, null], [1, "1/2", null, null, null]]] +["2/2", null, [[0, 1, null, null, null], [1, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["2/2", null, [[0, 1, null, null, null], [1, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[2, 0, null, null, null], [3, 1, null, null, null], [1, "1/2", null, null, null], [0, "1/3", null, null, null]]] +["2/2", null, [[3, 0, null, null, null], [2, 1, null, null, null], [0, "1/2", null, null, null], [1, "1/3", null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +[0, null, [[0, 0, null, null, null]]] +[1, null, [[3, 1, null, null, null]]] +["1/2", null, [[2, 0, null, null, null], [3, "1/2", null, null, null]]] +["2/2", null, [[0, 1, null, null, null], [2, "1/2", null, null, null]]] +["1/2", null, [[0, "1/2", null, null, null], [1, "1/3", null, null, null]]] +["2/2", null, [[2, 0, null, null, null], [1, 1, null, null, null], [3, "1/2", null, null, null]]] +["3/3", null, [[0, 0, null, null, null], [3, 1, null, null, null], [1, "1/3", null, null, null]]] +["1/2", null, [[3, 0, null, null, null], [2, "1/2", null, null, null], [0, "1/3", null, null, null]]] +["2/2", null, [[1, 1, null, null, null], [2, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[0, 0, null, null, null], [3, 1, null, null, null], [1, "1/2", null, null, null], [2, "1/3", null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +[0, null, [[3, 0, null, null, null]]] +[1, null, [[0, 1, null, null, null]]] +["1/2", null, [[2, "1/2", null, null, null]]] +["1/2", null, [[3, "1/2", null, null, null]]] +[1, null, [[2, 0, null, null, null], [1, 1, null, null, null]]] +["1/3", null, [[3, 0, null, null, null], [1, "1/3", null, null, null]]] +["3/3", null, [[0, 1, null, null, null], [2, "1/3", null, null, null]]] +["3/3", null, [[2, 1, null, null, null], [1, "1/3", null, null, null]]] +["1/2", null, [[2, "1/2", null, null, null], [0, "1/3", null, null, null]]] +["2/2", null, [[0, 0, null, null, null], [3, 1, null, null, null], [1, "1/2", null, null, null]]] +["2/2", null, [[1, 0, null, null, null], [2, 1, null, null, null], [3, "1/2", null, null, null]]] +["2/2", null, [[1, 0, null, null, null], [3, 1, null, null, null], [0, "1/2", null, null, null]]] +["2/2", null, [[3, 0, null, null, null], [0, 1, null, null, null], [2, "1/2", null, null, null]]] +["2/2", null, [[3, 0, null, null, null], [1, 1, null, null, null], [2, "1/2", null, null, null]]] +["3/3", null, [[2, 0, null, null, null], [0, 1, null, null, null], [1, "1/3", null, null, null]]] +["3/3", null, [[2, 0, null, null, null], [3, 1, null, null, null], [0, "1/3", null, null, null]]] +["1/2", null, [[1, 0, null, null, null], [3, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["1/2", null, [[2, 0, null, null, null], [3, "1/2", null, null, null], [1, "1/3", null, null, null]]] +["1/2", null, [[3, 0, null, null, null], [0, "1/2", null, null, null], [1, "1/3", null, null, null]]] +["2/2", null, [[3, 1, null, null, null], [0, "1/2", null, null, null], [1, "1/3", null, null, null]]] +["2/2", null, [[3, 1, null, null, null], [2, "1/2", null, null, null], [0, "1/3", null, null, null]]] +["2/2", null, [[1, 0, null, null, null], [3, 1, null, null, null], [0, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["2/2", null, [[2, 0, null, null, null], [1, 1, null, null, null], [0, "1/2", null, null, null], [3, "1/3", null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[1, 0, null, null, null], [0, 1, null, null, null]]] +["1/2", null, [[0, 0, null, null, null], [3, "1/2", null, null, null]]] +["1/3", null, [[1, 0, null, null, null], [3, "1/3", null, null, null]]] +["1/3", null, [[2, 0, null, null, null], [1, "1/3", null, null, null]]] +["1/3", null, [[3, 0, null, null, null], [0, "1/3", null, null, null]]] +["3/3", null, [[0, 1, null, null, null], [1, "1/3", null, null, null]]] +["3/3", null, [[3, 1, null, null, null], [0, "1/3", null, null, null]]] +["1/2", null, [[3, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["2/2", null, [[2, 0, null, null, null], [0, 1, null, null, null], [3, "1/2", null, null, null]]] +["1/2", null, [[3, 0, null, null, null], [1, "1/2", null, null, null], [0, "1/3", null, null, null]]] +["2/2", null, [[1, 1, null, null, null], [0, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[3, 1, null, null, null], [0, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["2/2", null, [[1, 0, null, null, null], [2, 1, null, null, null], [0, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[1, 0, null, null, null], [3, 1, null, null, null], [2, "1/2", null, null, null], [0, "1/3", null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +["1/2", null, [[0, "1/2", null, null, null]]] +[1, null, [[1, 0, null, null, null], [3, 1, null, null, null]]] +["1/2", null, [[0, 0, null, null, null], [1, "1/2", null, null, null]]] +["1/3", null, [[0, 0, null, null, null], [1, "1/3", null, null, null]]] +["1/3", null, [[1, 0, null, null, null], [0, "1/3", null, null, null]]] +["2/2", null, [[3, 0, null, null, null], [2, 1, null, null, null], [1, "1/2", null, null, null]]] +["3/3", null, [[1, 0, null, null, null], [0, 1, null, null, null], [2, "1/3", null, null, null]]] +["3/3", null, [[1, 0, null, null, null], [2, 1, null, null, null], [3, "1/3", null, null, null]]] +["3/3", null, [[3, 0, null, null, null], [2, 1, null, null, null], [0, "1/3", null, null, null]]] +["1/2", null, [[0, 0, null, null, null], [2, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["1/2", null, [[2, 0, null, null, null], [0, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[0, 1, null, null, null], [2, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[0, 1, null, null, null], [3, "1/2", null, null, null], [1, "1/3", null, null, null]]] +["2/2", null, [[3, 1, null, null, null], [1, "1/2", null, null, null], [0, "1/3", null, null, null]]] +["2/2", null, [[3, 1, null, null, null], [2, "1/2", null, null, null], [1, "1/3", null, null, null]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[1, 0, null, null, null], [2, 1, null, null, null]]] +["1/3", null, [[2, 0, null, null, null], [0, "1/3", null, null, null]]] +["1/3", null, [[3, 0, null, null, null], [2, "1/3", null, null, null]]] +["2/2", null, [[0, 1, null, null, null], [1, "1/2", null, null, null]]] +["2/2", null, [[2, 1, null, null, null], [3, "1/2", null, null, null]]] +["3/3", null, [[0, 1, null, null, null], [3, "1/3", null, null, null]]] +["3/3", null, [[2, 1, null, null, null], [0, "1/3", null, null, null]]] +["1/2", null, [[1, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["2/2", null, [[0, 0, null, null, null], [1, 1, null, null, null], [2, "1/2", null, null, null]]] +["3/3", null, [[1, 0, null, null, null], [2, 1, null, null, null], [0, "1/3", null, null, null]]] +["3/3", null, [[1, 0, null, null, null], [3, 1, null, null, null], [2, "1/3", null, null, null]]] +["3/3", null, [[2, 0, null, null, null], [3, 1, null, null, null], [1, "1/3", null, null, null]]] +["1/2", null, [[0, 0, null, null, null], [1, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["1/2", null, [[1, 0, null, null, null], [0, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["1/2", null, [[2, 0, null, null, null], [0, "1/2", null, null, null], [1, "1/3", null, null, null]]] +["1/2", null, [[2, 0, null, null, null], [3, "1/2", null, null, null], [0, "1/3", null, null, null]]] +["1/2", null, [[3, 0, null, null, null], [0, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["1/2", null, [[3, 0, null, null, null], [1, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["1/2", null, [[3, 0, null, null, null], [2, "1/2", null, null, null], [1, "1/3", null, null, null]]] +["2/2", null, [[3, 1, null, null, null], [1, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["2/2", null, [[0, 0, null, null, null], [1, 1, null, null, null], [2, "1/2", null, null, null], [3, "1/3", null, null, null]]] +["2/2", null, [[0, 0, null, null, null], [1, 1, null, null, null], [3, "1/2", null, null, null], [2, "1/3", null, null, null]]] +["2/2", null, [[2, 0, null, null, null], [3, 1, null, null, null], [0, "1/2", null, null, null], [1, "1/3", null, null, null]]] \ No newline at end of file diff --git a/apps/worker/tasks/tests/samples/sample_chunks_with_header.txt b/apps/worker/tasks/tests/samples/sample_chunks_with_header.txt new file mode 100644 index 0000000000..6f00874881 --- /dev/null +++ b/apps/worker/tasks/tests/samples/sample_chunks_with_header.txt @@ -0,0 +1,54 @@ +{"labels_index": {"0": "Th2dMtk4M_codecov", "1": "core/tests/test_menu_interface.py::TestMenuInterface::test_init", "2": "core/tests/test_main.py::TestMainMenu::test_init_values", "3": "core/tests/test_main.py::TestMainMenu::test_invalid_menu_choice", "4": "core/tests/test_menu_interface.py::TestMenuInterface::test_menu_options", "5": "core/tests/test_menu_interface.py::TestMenuInterface::test_set_loop", "6": "core/tests/test_main.py::TestMainMenu::test_menu_choice_emotions", "7": "core/tests/test_menu_interface.py::TestMenuInterface::test_name", "8": "core/tests/test_menu_interface.py::TestMenuInterface::test_parent", "9": "core/tests/test_main.py::TestMainMenu::test_menu_choice_fruits", "10": "core/tests/test_main.py::TestMainMenu::test_menu_options"}} +<<<<< end_of_header >>>>> +{"present_sessions":[0]} +[0, null, [[0, 0]], null, null, [[0, 0, null, []]]] + +[0, null, [[0, 0]], null, null, [[0, 0, null, []]]] +[0, null, [[0, 0]], null, null, [[0, 0, null, []]]] +[0, null, [[0, 0]], null, null, [[0, 0, null, []]]] +<<<<< end_of_chunk >>>>> +{"present_sessions":[0]} +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] + + +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] + + +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] + +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]]] + +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [1, 3, 5, 6, 9]]]] + +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [1, 2, 8]]]] + +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [1, 2, 7]]]] + +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [3, 5, 6, 9]]]] + +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [3, 4, 6, 9, 10]]]] +[1, null, [[0, 1]], null, null, [[0, 1, null, [4]]]] + +[1, null, [[0, 1]], null, null, [[0, 1, null, [3, 4, 6, 9, 10]]]] + +[1, null, [[0, 1]], null, null, [[0, 1, null, [0]]]] +[0, null, [[0, 0]], null, null, [[0, 0, null, []]]] \ No newline at end of file diff --git a/apps/worker/tasks/tests/samples/sample_multi_test_part_1.json b/apps/worker/tasks/tests/samples/sample_multi_test_part_1.json new file mode 100644 index 0000000000..fc0a34ff91 --- /dev/null +++ b/apps/worker/tasks/tests/samples/sample_multi_test_part_1.json @@ -0,0 +1,11 @@ +{ + "test_results_files": [ + { + "filename": "codecov-demo/multi-test.xml", + "format": "base64+compressed", + "data": "eJy1U01vgkAQvfsrJtvEaCywi18FBdOkNqaHnpp6bDawICkCYZe2/vsOi1aqxvTSTYDM25n3HrM788XXNoUPUcokzzzCTEpAZEEeJlnskUpFxh1Z+J25ElLJKsGP3wFcRwAyvhUeKXY1gsVlmZfSI8gT8SStSoEBIyDfk6IQod7QtRpVSV1LTWpPiOatVw1KxbeFR2xqjww6NWznhQ1daruMmZOpMxk6Bh25FLk2uVSNg9fH1fP66WG9csw0D3hK/B9G7TbgUkCQcimbfO3Cqt9vUZUFChtgFjsCx90fvGWUshav5t7/JqBpyWPMQgVRKmDQjdXMg3H3htEZDAA+NwLzGHhQ8izEJxY9eguM9okfiugXrW5E20Ov754l1AKwlzulPKifFS3h1OCFlMEVs52LRs9b6Y5duNdSGC/rezG39s1qHY11OJv/P64/a0VVmu4gyHEuRHiQgBO00Zia9hWFJtZzgkNktafoG8+FAfk=", + "labels": "" + } + ], + "metadata": {} +} diff --git a/apps/worker/tasks/tests/samples/sample_multi_test_part_2.json b/apps/worker/tasks/tests/samples/sample_multi_test_part_2.json new file mode 100644 index 0000000000..14aff5bb74 --- /dev/null +++ b/apps/worker/tasks/tests/samples/sample_multi_test_part_2.json @@ -0,0 +1,11 @@ +{ + "test_results_files": [ + { + "filename": "codecov-demo/multi-test.xml", + "format": "base64+compressed", + "data": "eJzNU8tugzAQvOcrVq4UJUoBm7wKCUSVmirqoaeqOVYWGIJKAGHTNn/fxeRBk7TnWgK0492ZweudL762KXyIUiZ55hFmUgIiC/IwyWKPVCoy7sjC78yVkEpWCX78DuA6AZDxrfBIsasRLC7LvJQeQZ6IJ2lVCgwYAfmeFIUI9Yau1ahK6lpqUntCNG+9alAqvi08YlN7ZNCpYTsvbOhS22XMnEydydAx6MilyLXJpWocvD6untdPD+uVY6Z5wFPiHxm124BLAUHKpWzytQurfr9FVRYoPACz2BE47R7xllFqt3mtA/E/0oqqNN1BkGNTRXiQgDO00Riak5aCZtw3DbAFkseYg7yiVMCgG6uZB+PuDaMzGAB8bpAKcQ9KnoX4xKJHb4HRPvFDEf2g1W1t/2Wv714k1AKwlzunPKhfFC3h3OCVlMEfZjtXjV42yx27cK+lMF7Wt3xu7Q/r9yY1sZ4THCKrPUXfoXQB+w==", + "labels": "" + } + ], + "metadata": {} +} \ No newline at end of file diff --git a/apps/worker/tasks/tests/samples/sample_ta_file.xml b/apps/worker/tasks/tests/samples/sample_ta_file.xml new file mode 100644 index 0000000000..74f0f066da --- /dev/null +++ b/apps/worker/tasks/tests/samples/sample_ta_file.xml @@ -0,0 +1,17 @@ + + + + + + + + def + test_divide():\n> assert Calculator.divide(1, 2) == 0.5\nE assert 1.0 == 0.5\nE + + where 1.0 = <function Calculator.divide at 0x104c9eb90>(1, 2)\nE + where + <function Calculator.divide at 0x104c9eb90> = + Calculator.divide\n\napi/temp/calculator/test_calculator.py:30: AssertionError + + + \ No newline at end of file diff --git a/apps/worker/tasks/tests/samples/sample_test.json b/apps/worker/tasks/tests/samples/sample_test.json new file mode 100644 index 0000000000..1b05856e84 --- /dev/null +++ b/apps/worker/tasks/tests/samples/sample_test.json @@ -0,0 +1,11 @@ +{ + "test_results_files": [ + { + "filename": "codecov-demo/temp.junit.xml", + "format": "base64+compressed", + "data": "eJy1VMluwjAQvfMVI1dCoBbHZiklJEFVS4V66Kkqx8okBqw6i2KHwt/XWSChnCrRXDLjefNm8Uuc2T6UsOOpEnHkIooJAh75cSCijYsyve49oJnnaK60yoR5NWyIWMhdlBzyE5OWpnGqXGQY1kzILOXGoQjUl0gSHhSBItdFQ2OJPJdgMuqXjtIsTFzUJ/1Bj9IeuX+n1KZjmwwxoZSMDWwbK13W/HhZvC1fn5eLCZaxzyQq2/KZ4uBLplQJY4nAmocJNhA/k0zHKc5xn7WPqimKYxYEjc6Iad66DrHKVjplvv4f9jCTWiTy0GQnV2MPxE4E/Lxzz6muGMzFKbbJaZXiqQajIHBdIHjUvqFkCrcA31tugEUA2lJP11nkayM3eKobKIsA00D2lAz9CV9NSHujpx16B/3uievI9mceU/sChryAr6ExZKdrtwpw+VQjXeSVPVVjtubn6HoBp8iVlnDGd9VFtFpGFFYuCqsWgfVLFDg52ANiw2Mxpyk3zz94x6qU4DnWUW2VWfwkmrbyfgBbcXMH", + "labels": "" + } + ], + "metadata": {} +} \ No newline at end of file diff --git a/apps/worker/tasks/tests/samples/sample_test_missing_network.json b/apps/worker/tasks/tests/samples/sample_test_missing_network.json new file mode 100644 index 0000000000..39125ce7d8 --- /dev/null +++ b/apps/worker/tasks/tests/samples/sample_test_missing_network.json @@ -0,0 +1,11 @@ +{ + "test_results_files": [ + { + "filename": "codecov-demo/temp.junit.xml", + "format": "base64+compressed", + "data": "eJzNlE1P4zAQhu/5FSMjVSC2jh3Ksk2bVAhYIQ6c0PaI3MQp1jofsp1C//3aSWgCueyuesCXxOOZZ16PxrNcveUSdlxpURYRopgg4EVSpqLYRqg22fQHWsXe0nBtdC3sJ/bArt4ABct5hKq9s9hgpUqlI2Q5GROyVtxuKAL9W1QVT5uDJjZCM/snXCzB5DJADdctZ9SG5VWEAhJcTCmdku9PlIb0KiQzTCglVwGCl1KbNvevn/eP64fb9f0cyzJhEsUHVqMzYZpDIpnWrT+rBDY8r7D1TWrJTKmw83vu96i7VmNmaTqQSmgv1a1MyJbpO6bfM/xPTFztEfhH1qbrjVEsMV9WYF5LIyq5HwokX0lgKnYi5ceqX/whdNm9gQ9Gt2yHa7Z1WK25MmAfHkQREHw5OaFkAecAry9c8fYAJtIssrpIjH2lcNMnbLUDM0DeKJklc76Zk8nWLE7pNwjODqx32j9zbO6RG4pTno1uNKjl6VnoNcHd5UaEVl13Ye8ORkWwtvNRhiMUxHFbzoj+H6UZMUZhnvd3bRNekBCumyrY9Hduhi79rncG7e6/93s3hP3DFLYj2h/O6D/18sx5", + "labels": "" + } + ], + "metadata": {} +} \ No newline at end of file diff --git a/apps/worker/tasks/tests/samples/sample_test_network.json b/apps/worker/tasks/tests/samples/sample_test_network.json new file mode 100644 index 0000000000..25db673864 --- /dev/null +++ b/apps/worker/tasks/tests/samples/sample_test_network.json @@ -0,0 +1,14 @@ +{ + "network_files": [ + "api/temp/calculator/test_calculator.py" + ], + "test_results_files": [ + { + "filename": "codecov-demo/temp.junit.xml", + "format": "base64+compressed", + "data": "eJy1VMluwjAQvfMVI1dCoBbHZiklJEFVS4V66Kkqx8okBqw6i2KHwt/XWSChnCrRXDLjefNm8Uuc2T6UsOOpEnHkIooJAh75cSCijYsyve49oJnnaK60yoR5NWyIWMhdlBzyE5OWpnGqXGQY1kzILOXGoQjUl0gSHhSBItdFQ2OJPJdgMuqXjtIsTFzUJ/1Bj9IeuX+n1KZjmwwxoZSMDWwbK13W/HhZvC1fn5eLCZaxzyQq2/KZ4uBLplQJY4nAmocJNhA/k0zHKc5xn7WPqimKYxYEjc6Iad66DrHKVjplvv4f9jCTWiTy0GQnV2MPxE4E/Lxzz6muGMzFKbbJaZXiqQajIHBdIHjUvqFkCrcA31tugEUA2lJP11nkayM3eKobKIsA00D2lAz9CV9NSHujpx16B/3uievI9mceU/sChryAr6ExZKdrtwpw+VQjXeSVPVVjtubn6HoBp8iVlnDGd9VFtFpGFFYuCqsWgfVLFDg52ANiw2Mxpyk3zz94x6qU4DnWUW2VWfwkmrbyfgBbcXMH", + "labels": "" + } + ], + "metadata": {} +} \ No newline at end of file diff --git a/apps/worker/tasks/tests/samples/sample_uploaded_report_1.txt b/apps/worker/tasks/tests/samples/sample_uploaded_report_1.txt new file mode 100644 index 0000000000..5494e680e6 --- /dev/null +++ b/apps/worker/tasks/tests/samples/sample_uploaded_report_1.txt @@ -0,0 +1,99 @@ +codecov.yaml +.coverage +README.rst +awesome/__init__.py +codecov +coverage.xml +tests/__init__.py +tests/test_sample.py +<<<<<< network + +# path=coverage.xml + + + + + + /home/thiagorramos/Projects/codecov/example-python + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +<<<<<< EOF + +# path=codecov.yaml +codecov: + notify: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: yes + patch: yes + changes: no + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "header, diff" + behavior: default + require_changes: no +<<<<<< EOF + diff --git a/apps/worker/tasks/tests/unit/__init__.py b/apps/worker/tasks/tests/unit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_add_to_list_invalid_list_with_list_type.yaml b/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_add_to_list_invalid_list_with_list_type.yaml new file mode 100644 index 0000000000..ab370bd5c8 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_add_to_list_invalid_list_with_list_type.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_add_to_list_invalid_owner.yaml b/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_add_to_list_invalid_owner.yaml new file mode 100644 index 0000000000..ab370bd5c8 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_add_to_list_invalid_owner.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_add_to_list_invalid_owner_no_list_type.yaml b/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_add_to_list_invalid_owner_no_list_type.yaml new file mode 100644 index 0000000000..ab370bd5c8 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_add_to_list_invalid_owner_no_list_type.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_add_to_list_no_list.yaml b/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_add_to_list_no_list.yaml new file mode 100644 index 0000000000..ab370bd5c8 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_add_to_list_no_list.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_end_of_trial_email.yaml b/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_end_of_trial_email.yaml new file mode 100644 index 0000000000..1266b32011 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_end_of_trial_email.yaml @@ -0,0 +1,59 @@ +interactions: +- request: + body: '{"list_ids": ["ff08b4ae-367e-49ac-9d5a-d6e0e004d19f"], "contacts": [{"email": + "tom@codecov.io"}]}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '100' + Content-Type: + - application/json + User-Agent: + - python-requests/2.22.0 + method: PUT + uri: https://api.sendgrid.com/v3/marketing/contacts + response: + body: + string: '{"job_id":"9791f6a7-3d3b-4ae9-8f71-67bd98f33008"} + + ' + headers: + Connection: + - keep-alive + Content-Length: + - '50' + Content-Type: + - application/json + Date: + - Wed, 22 Jan 2020 21:33:47 GMT + Server: + - nginx + access-control-allow-headers: + - AUTHORIZATION, Content-Type, On-behalf-of, x-sg-elas-acl, X-Recaptcha, X-Request-Source + access-control-allow-methods: + - PUT,DELETE,OPTIONS + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Link, Location + referrer-policy: + - strict-origin-when-cross-origin + x-amz-apigw-id: + - GuLlIG2RPHcFtbA= + x-amzn-requestid: + - d4e105c8-b312-4585-97b4-f67d7f463ccf + x-amzn-trace-id: + - Root=1-5e28bfba-8aff770e542798e4a788763c;Sampled=0 + x-content-type-options: + - nosniff + x-envoy-upstream-service-time: + - '1554' + status: + code: 202 + message: Accepted +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_new_oauthed_users_email_with_email_type.yaml b/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_new_oauthed_users_email_with_email_type.yaml new file mode 100644 index 0000000000..17b68227c3 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_new_oauthed_users_email_with_email_type.yaml @@ -0,0 +1,36 @@ +interactions: +- request: + body: '{"list_ids": ["283c715a-48e0-4c35-9dbd-374760900ee2"], "contacts": [{"email": + "tom@codecov.io"}]}' + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + Content-Length: ['100'] + Content-Type: [application/json] + User-Agent: [python-requests/2.22.0] + method: PUT + uri: https://api.sendgrid.com/v3/marketing/contacts + response: + body: {string: '{"job_id":"9791f6a7-3d3b-4ae9-8f71-67bd98f33008"} + + '} + headers: + Connection: [keep-alive] + Content-Length: ['50'] + Content-Type: [application/json] + Date: ['Wed, 22 Jan 2020 21:33:47 GMT'] + Server: [nginx] + access-control-allow-headers: ['AUTHORIZATION, Content-Type, On-behalf-of, x-sg-elas-acl, + X-Recaptcha, X-Request-Source'] + access-control-allow-methods: ['PUT,DELETE,OPTIONS'] + access-control-allow-origin: ['*'] + access-control-expose-headers: ['Link, Location'] + referrer-policy: [strict-origin-when-cross-origin] + x-amz-apigw-id: [GuLlIG2RPHcFtbA=] + x-amzn-requestid: [d4e105c8-b312-4585-97b4-f67d7f463ccf] + x-amzn-trace-id: [Root=1-5e28bfba-8aff770e542798e4a788763c;Sampled=0] + x-content-type-options: [nosniff] + x-envoy-upstream-service-time: ['1554'] + status: {code: 202, message: Accepted} +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_new_oauthed_users_email_with_list_type.yaml b/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_new_oauthed_users_email_with_list_type.yaml new file mode 100644 index 0000000000..adbee1f3c6 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_add_to_sendgrid_list/TestAddToSendgridListTask/test_new_oauthed_users_email_with_list_type.yaml @@ -0,0 +1,59 @@ +interactions: +- request: + body: '{"list_ids": ["283c715a-48e0-4c35-9dbd-374760900ee2"], "contacts": [{"email": + "tom@codecov.io"}]}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '100' + Content-Type: + - application/json + User-Agent: + - python-requests/2.22.0 + method: PUT + uri: https://api.sendgrid.com/v3/marketing/contacts + response: + body: + string: '{"job_id":"9791f6a7-3d3b-4ae9-8f71-67bd98f33008"} + + ' + headers: + Connection: + - keep-alive + Content-Length: + - '50' + Content-Type: + - application/json + Date: + - Wed, 22 Jan 2020 21:33:47 GMT + Server: + - nginx + access-control-allow-headers: + - AUTHORIZATION, Content-Type, On-behalf-of, x-sg-elas-acl, X-Recaptcha, X-Request-Source + access-control-allow-methods: + - PUT,DELETE,OPTIONS + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Link, Location + referrer-policy: + - strict-origin-when-cross-origin + x-amz-apigw-id: + - GuLlIG2RPHcFtbA= + x-amzn-requestid: + - d4e105c8-b312-4585-97b4-f67d7f463ccf + x-amzn-trace-id: + - Root=1-5e28bfba-8aff770e542798e4a788763c;Sampled=0 + x-content-type-options: + - nosniff + x-envoy-upstream-service-time: + - '1554' + status: + code: 202 + message: Accepted +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_commit_update/TestCommitUpdate/test_update_commit.yaml b/apps/worker/tasks/tests/unit/cassetes/test_commit_update/TestCommitUpdate/test_update_commit.yaml new file mode 100644 index 0000000000..9f232a04a6 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_commit_update/TestCommitUpdate/test_update_commit.yaml @@ -0,0 +1,345 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/test-acc9/test_example/commits/a2d3e3c30547a000f026daa47610bb3f7b63aece + response: + content: '{"sha":"a2d3e3c30547a000f026daa47610bb3f7b63aece","node_id":"C_kwDOHP_eEdoAKGEyZDNlM2MzMDU0N2EwMDBmMDI2ZGFhNDc2MTBiYjNmN2I2M2FlY2U","commit":{"author":{"name":"test-acc9","email":"104562106+test-acc9@users.noreply.github.com","date":"2022-07-27T06:13:31Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-07-27T06:13:31Z"},"message":"random-commit-msg","tree":{"sha":"d9899981dca2af8bfa20263c9c97413f017e7602","url":"https://api.github.com/repos/test-acc9/test_example/git/trees/d9899981dca2af8bfa20263c9c97413f017e7602"},"url":"https://api.github.com/repos/test-acc9/test_example/git/commits/a2d3e3c30547a000f026daa47610bb3f7b63aece","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJi4NeLCRBK7hj4Ov3rIwAAHFIIAD0YVuosBePsrNall2Qti6RF\nO4+4QzF5wK6WBgkuCX5mPaGUZMfwYohYUf6iipVVPJHhnxwGA/MIj5pt+muChJCx\nziP6+QfEg8uHR+od0MyG4atpojM2WpBa/gqNzTWnk1Ti7PCyMlueZQGVWLkTuU+Y\ng0fbKg7FFTAxWBhm4sTnb+8vFfwFiuHef7J2NpIdBVAUh4lmfk/hsqKlLz7s6xxS\n8qWz6BhCZSjXUjG+i4/DbwezV9YqsZMz73TbE04TX/pZvqXPoTyLQfCg1mpL2hrI\ndGJBHDZfr0CB8a+33e9TWT80U8TD4g6gjtwrwrZTVzwMfJVVSQcxIfVndQ7ihDg=\n=qgU3\n-----END + PGP SIGNATURE-----\n","payload":"tree d9899981dca2af8bfa20263c9c97413f017e7602\nparent + 610ada9fa2bbc49f1a08917da3f73bef2d03709c\nauthor test-acc9 <104562106+test-acc9@users.noreply.github.com> + 1658902411 +0300\ncommitter GitHub 1658902411 +0300\n\nrandom-commit-msg"}},"url":"https://api.github.com/repos/test-acc9/test_example/commits/a2d3e3c30547a000f026daa47610bb3f7b63aece","html_url":"https://github.com/test-acc9/test_example/commit/a2d3e3c30547a000f026daa47610bb3f7b63aece","comments_url":"https://api.github.com/repos/test-acc9/test_example/commits/a2d3e3c30547a000f026daa47610bb3f7b63aece/comments","author":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"610ada9fa2bbc49f1a08917da3f73bef2d03709c","url":"https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c","html_url":"https://github.com/test-acc9/test_example/commit/610ada9fa2bbc49f1a08917da3f73bef2d03709c"}],"stats":{"total":1,"additions":1,"deletions":0},"files":[{"sha":"b2b9cc97bb0f4415ce6fcb16d7a40ccd60fb4a40","filename":"random-commit.me","status":"modified","additions":1,"deletions":0,"changes":1,"blob_url":"https://github.com/test-acc9/test_example/blob/a2d3e3c30547a000f026daa47610bb3f7b63aece/random-commit.me","raw_url":"https://github.com/test-acc9/test_example/raw/a2d3e3c30547a000f026daa47610bb3f7b63aece/random-commit.me","contents_url":"https://api.github.com/repos/test-acc9/test_example/contents/random-commit.me?ref=a2d3e3c30547a000f026daa47610bb3f7b63aece","patch":"@@ + -1 +1,2 @@\n hello\n+test"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 27 Jul 2022 09:29:49 GMT + ETag: + - W/"3056cfcf8f1a5c76c44ffdb23e495b3796fa612e43a0fbea79bd02a3956c563f" + Last-Modified: + - Wed, 27 Jul 2022 06:13:31 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C498:390D:8282C9:86FE59:62E1058C + X-OAuth-Scopes: + - admin:repo_hook, repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1658917789' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-08-25 14:53:43 UTC + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/test-acc9/test_example/pulls/1 + response: + content: '{"url":"https://api.github.com/repos/test-acc9/test_example/pulls/1","id":921616238,"node_id":"PR_kwDOHP_eEc427r9u","html_url":"https://github.com/test-acc9/test_example/pull/1","diff_url":"https://github.com/test-acc9/test_example/pull/1.diff","patch_url":"https://github.com/test-acc9/test_example/pull/1.patch","issue_url":"https://api.github.com/repos/test-acc9/test_example/issues/1","number":1,"state":"open","locked":false,"title":"Create + random-commit.me","user":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"body":null,"created_at":"2022-04-28T09:37:10Z","updated_at":"2022-07-27T06:13:31Z","closed_at":null,"merged_at":null,"merge_commit_sha":"188bca08574e22fdc3cbdbde0bb98bfcf7e64425","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/test-acc9/test_example/pulls/1/commits","review_comments_url":"https://api.github.com/repos/test-acc9/test_example/pulls/1/comments","review_comment_url":"https://api.github.com/repos/test-acc9/test_example/pulls/comments{/number}","comments_url":"https://api.github.com/repos/test-acc9/test_example/issues/1/comments","statuses_url":"https://api.github.com/repos/test-acc9/test_example/statuses/a2d3e3c30547a000f026daa47610bb3f7b63aece","head":{"label":"test-acc9:featureA","ref":"featureA","sha":"a2d3e3c30547a000f026daa47610bb3f7b63aece","user":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"repo":{"id":486530577,"node_id":"R_kgDOHP_eEQ","name":"test_example","full_name":"test-acc9/test_example","private":false,"owner":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"html_url":"https://github.com/test-acc9/test_example","description":null,"fork":false,"url":"https://api.github.com/repos/test-acc9/test_example","forks_url":"https://api.github.com/repos/test-acc9/test_example/forks","keys_url":"https://api.github.com/repos/test-acc9/test_example/keys{/key_id}","collaborators_url":"https://api.github.com/repos/test-acc9/test_example/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/test-acc9/test_example/teams","hooks_url":"https://api.github.com/repos/test-acc9/test_example/hooks","issue_events_url":"https://api.github.com/repos/test-acc9/test_example/issues/events{/number}","events_url":"https://api.github.com/repos/test-acc9/test_example/events","assignees_url":"https://api.github.com/repos/test-acc9/test_example/assignees{/user}","branches_url":"https://api.github.com/repos/test-acc9/test_example/branches{/branch}","tags_url":"https://api.github.com/repos/test-acc9/test_example/tags","blobs_url":"https://api.github.com/repos/test-acc9/test_example/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/test-acc9/test_example/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/test-acc9/test_example/git/refs{/sha}","trees_url":"https://api.github.com/repos/test-acc9/test_example/git/trees{/sha}","statuses_url":"https://api.github.com/repos/test-acc9/test_example/statuses/{sha}","languages_url":"https://api.github.com/repos/test-acc9/test_example/languages","stargazers_url":"https://api.github.com/repos/test-acc9/test_example/stargazers","contributors_url":"https://api.github.com/repos/test-acc9/test_example/contributors","subscribers_url":"https://api.github.com/repos/test-acc9/test_example/subscribers","subscription_url":"https://api.github.com/repos/test-acc9/test_example/subscription","commits_url":"https://api.github.com/repos/test-acc9/test_example/commits{/sha}","git_commits_url":"https://api.github.com/repos/test-acc9/test_example/git/commits{/sha}","comments_url":"https://api.github.com/repos/test-acc9/test_example/comments{/number}","issue_comment_url":"https://api.github.com/repos/test-acc9/test_example/issues/comments{/number}","contents_url":"https://api.github.com/repos/test-acc9/test_example/contents/{+path}","compare_url":"https://api.github.com/repos/test-acc9/test_example/compare/{base}...{head}","merges_url":"https://api.github.com/repos/test-acc9/test_example/merges","archive_url":"https://api.github.com/repos/test-acc9/test_example/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/test-acc9/test_example/downloads","issues_url":"https://api.github.com/repos/test-acc9/test_example/issues{/number}","pulls_url":"https://api.github.com/repos/test-acc9/test_example/pulls{/number}","milestones_url":"https://api.github.com/repos/test-acc9/test_example/milestones{/number}","notifications_url":"https://api.github.com/repos/test-acc9/test_example/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/test-acc9/test_example/labels{/name}","releases_url":"https://api.github.com/repos/test-acc9/test_example/releases{/id}","deployments_url":"https://api.github.com/repos/test-acc9/test_example/deployments","created_at":"2022-04-28T09:34:42Z","updated_at":"2022-07-27T08:02:03Z","pushed_at":"2022-07-27T06:13:31Z","git_url":"git://github.com/test-acc9/test_example.git","ssh_url":"git@github.com:test-acc9/test_example.git","clone_url":"https://github.com/test-acc9/test_example.git","svn_url":"https://github.com/test-acc9/test_example","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main"}},"base":{"label":"test-acc9:main","ref":"main","sha":"ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","user":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"repo":{"id":486530577,"node_id":"R_kgDOHP_eEQ","name":"test_example","full_name":"test-acc9/test_example","private":false,"owner":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"html_url":"https://github.com/test-acc9/test_example","description":null,"fork":false,"url":"https://api.github.com/repos/test-acc9/test_example","forks_url":"https://api.github.com/repos/test-acc9/test_example/forks","keys_url":"https://api.github.com/repos/test-acc9/test_example/keys{/key_id}","collaborators_url":"https://api.github.com/repos/test-acc9/test_example/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/test-acc9/test_example/teams","hooks_url":"https://api.github.com/repos/test-acc9/test_example/hooks","issue_events_url":"https://api.github.com/repos/test-acc9/test_example/issues/events{/number}","events_url":"https://api.github.com/repos/test-acc9/test_example/events","assignees_url":"https://api.github.com/repos/test-acc9/test_example/assignees{/user}","branches_url":"https://api.github.com/repos/test-acc9/test_example/branches{/branch}","tags_url":"https://api.github.com/repos/test-acc9/test_example/tags","blobs_url":"https://api.github.com/repos/test-acc9/test_example/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/test-acc9/test_example/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/test-acc9/test_example/git/refs{/sha}","trees_url":"https://api.github.com/repos/test-acc9/test_example/git/trees{/sha}","statuses_url":"https://api.github.com/repos/test-acc9/test_example/statuses/{sha}","languages_url":"https://api.github.com/repos/test-acc9/test_example/languages","stargazers_url":"https://api.github.com/repos/test-acc9/test_example/stargazers","contributors_url":"https://api.github.com/repos/test-acc9/test_example/contributors","subscribers_url":"https://api.github.com/repos/test-acc9/test_example/subscribers","subscription_url":"https://api.github.com/repos/test-acc9/test_example/subscription","commits_url":"https://api.github.com/repos/test-acc9/test_example/commits{/sha}","git_commits_url":"https://api.github.com/repos/test-acc9/test_example/git/commits{/sha}","comments_url":"https://api.github.com/repos/test-acc9/test_example/comments{/number}","issue_comment_url":"https://api.github.com/repos/test-acc9/test_example/issues/comments{/number}","contents_url":"https://api.github.com/repos/test-acc9/test_example/contents/{+path}","compare_url":"https://api.github.com/repos/test-acc9/test_example/compare/{base}...{head}","merges_url":"https://api.github.com/repos/test-acc9/test_example/merges","archive_url":"https://api.github.com/repos/test-acc9/test_example/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/test-acc9/test_example/downloads","issues_url":"https://api.github.com/repos/test-acc9/test_example/issues{/number}","pulls_url":"https://api.github.com/repos/test-acc9/test_example/pulls{/number}","milestones_url":"https://api.github.com/repos/test-acc9/test_example/milestones{/number}","notifications_url":"https://api.github.com/repos/test-acc9/test_example/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/test-acc9/test_example/labels{/name}","releases_url":"https://api.github.com/repos/test-acc9/test_example/releases{/id}","deployments_url":"https://api.github.com/repos/test-acc9/test_example/deployments","created_at":"2022-04-28T09:34:42Z","updated_at":"2022-07-27T08:02:03Z","pushed_at":"2022-07-27T06:13:31Z","git_url":"git://github.com/test-acc9/test_example.git","ssh_url":"git@github.com:test-acc9/test_example.git","clone_url":"https://github.com/test-acc9/test_example.git","svn_url":"https://github.com/test-acc9/test_example","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main"}},"_links":{"self":{"href":"https://api.github.com/repos/test-acc9/test_example/pulls/1"},"html":{"href":"https://github.com/test-acc9/test_example/pull/1"},"issue":{"href":"https://api.github.com/repos/test-acc9/test_example/issues/1"},"comments":{"href":"https://api.github.com/repos/test-acc9/test_example/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/test-acc9/test_example/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/test-acc9/test_example/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/test-acc9/test_example/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/test-acc9/test_example/statuses/a2d3e3c30547a000f026daa47610bb3f7b63aece"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null,"merged":false,"mergeable":true,"rebaseable":true,"mergeable_state":"clean","merged_by":null,"comments":2,"review_comments":0,"maintainer_can_modify":false,"commits":2,"additions":2,"deletions":0,"changed_files":1}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 27 Jul 2022 09:29:49 GMT + ETag: + - W/"d4757c718be43abb3e8db24d40502c26274048223d41bf9d8f427979a1c513ef" + Last-Modified: + - Wed, 27 Jul 2022 06:13:31 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C49A:B717:360AA:6E243:62E1058D + X-OAuth-Scopes: + - admin:repo_hook, repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4998' + X-RateLimit-Reset: + - '1658917789' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '2' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-08-25 14:53:43 UTC + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/test-acc9/test_example/pulls/1/commits?per_page=250 + response: + content: '[{"sha":"610ada9fa2bbc49f1a08917da3f73bef2d03709c","node_id":"C_kwDOHP_eEdoAKDYxMGFkYTlmYTJiYmM0OWYxYTA4OTE3ZGEzZjczYmVmMmQwMzcwOWM","commit":{"author":{"name":"test-acc9","email":"104562106+test-acc9@users.noreply.github.com","date":"2022-04-28T09:37:05Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-04-28T09:37:05Z"},"message":"Create + random-commit.me","tree":{"sha":"729da23e42745fea6aff7dfdf1679146fe919620","url":"https://api.github.com/repos/test-acc9/test_example/git/trees/729da23e42745fea6aff7dfdf1679146fe919620"},"url":"https://api.github.com/repos/test-acc9/test_example/git/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJiamBBCRBK7hj4Ov3rIwAARu8IAFSGwf1o4Qrp71vxruVbUE0a\nBtA7q0ViOvbK7xwFP6mxJ8cbtKT/0MyYREdgvnw2qzc8KjoQjK0PCZBpHq3BP1WQ\nCkeW1S8xuCn4tnVJQeq+hA9NETt/YGY9cbZHv8o7636vZc7HhxKMiUXF2ppBJYNi\nsQk6W5LgPrb+su5WfZ299yH40b+DivZsoK+aAguhnlXzL4Tvznml78k9x4aO7KPM\nP/FGSzGrUQqNdlFI1xShhqAjwlLpAF2CE7CJ0CZ7q9PmYfYpLfB3bxWAv49+ozk0\nrJP1fdKMJvCmR0spPIjr/3bDhKoMVGB67sj5afkFchSNv0AkNMooVjSa5F2ABP4=\n=ALiA\n-----END + PGP SIGNATURE-----\n","payload":"tree 729da23e42745fea6aff7dfdf1679146fe919620\nparent + ef6edf5ae6643d53a7971fb8823d3f7b2ac65619\nauthor test-acc9 <104562106+test-acc9@users.noreply.github.com> + 1651138625 +0300\ncommitter GitHub 1651138625 +0300\n\nCreate + random-commit.me"}},"url":"https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c","html_url":"https://github.com/test-acc9/test_example/commit/610ada9fa2bbc49f1a08917da3f73bef2d03709c","comments_url":"https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c/comments","author":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","url":"https://api.github.com/repos/test-acc9/test_example/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","html_url":"https://github.com/test-acc9/test_example/commit/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619"}]},{"sha":"a2d3e3c30547a000f026daa47610bb3f7b63aece","node_id":"C_kwDOHP_eEdoAKGEyZDNlM2MzMDU0N2EwMDBmMDI2ZGFhNDc2MTBiYjNmN2I2M2FlY2U","commit":{"author":{"name":"test-acc9","email":"104562106+test-acc9@users.noreply.github.com","date":"2022-07-27T06:13:31Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-07-27T06:13:31Z"},"message":"random-commit-msg","tree":{"sha":"d9899981dca2af8bfa20263c9c97413f017e7602","url":"https://api.github.com/repos/test-acc9/test_example/git/trees/d9899981dca2af8bfa20263c9c97413f017e7602"},"url":"https://api.github.com/repos/test-acc9/test_example/git/commits/a2d3e3c30547a000f026daa47610bb3f7b63aece","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJi4NeLCRBK7hj4Ov3rIwAAHFIIAD0YVuosBePsrNall2Qti6RF\nO4+4QzF5wK6WBgkuCX5mPaGUZMfwYohYUf6iipVVPJHhnxwGA/MIj5pt+muChJCx\nziP6+QfEg8uHR+od0MyG4atpojM2WpBa/gqNzTWnk1Ti7PCyMlueZQGVWLkTuU+Y\ng0fbKg7FFTAxWBhm4sTnb+8vFfwFiuHef7J2NpIdBVAUh4lmfk/hsqKlLz7s6xxS\n8qWz6BhCZSjXUjG+i4/DbwezV9YqsZMz73TbE04TX/pZvqXPoTyLQfCg1mpL2hrI\ndGJBHDZfr0CB8a+33e9TWT80U8TD4g6gjtwrwrZTVzwMfJVVSQcxIfVndQ7ihDg=\n=qgU3\n-----END + PGP SIGNATURE-----\n","payload":"tree d9899981dca2af8bfa20263c9c97413f017e7602\nparent + 610ada9fa2bbc49f1a08917da3f73bef2d03709c\nauthor test-acc9 <104562106+test-acc9@users.noreply.github.com> + 1658902411 +0300\ncommitter GitHub 1658902411 +0300\n\nrandom-commit-msg"}},"url":"https://api.github.com/repos/test-acc9/test_example/commits/a2d3e3c30547a000f026daa47610bb3f7b63aece","html_url":"https://github.com/test-acc9/test_example/commit/a2d3e3c30547a000f026daa47610bb3f7b63aece","comments_url":"https://api.github.com/repos/test-acc9/test_example/commits/a2d3e3c30547a000f026daa47610bb3f7b63aece/comments","author":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"610ada9fa2bbc49f1a08917da3f73bef2d03709c","url":"https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c","html_url":"https://github.com/test-acc9/test_example/commit/610ada9fa2bbc49f1a08917da3f73bef2d03709c"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 27 Jul 2022 09:29:50 GMT + ETag: + - W/"4fbff312c8532e6eed0db3fe61f0501d53497520e0033e6e9b9eb33da576b415" + Last-Modified: + - Wed, 27 Jul 2022 06:13:31 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C49A:B717:360AE:6E247:62E1058D + X-OAuth-Scopes: + - admin:repo_hook, repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4997' + X-RateLimit-Reset: + - '1658917789' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '3' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-08-25 14:53:43 UTC + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/test-acc9/test_example/commits?sha=a2d3e3c30547a000f026daa47610bb3f7b63aece + response: + content: '[{"sha":"a2d3e3c30547a000f026daa47610bb3f7b63aece","node_id":"C_kwDOHP_eEdoAKGEyZDNlM2MzMDU0N2EwMDBmMDI2ZGFhNDc2MTBiYjNmN2I2M2FlY2U","commit":{"author":{"name":"test-acc9","email":"104562106+test-acc9@users.noreply.github.com","date":"2022-07-27T06:13:31Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-07-27T06:13:31Z"},"message":"random-commit-msg","tree":{"sha":"d9899981dca2af8bfa20263c9c97413f017e7602","url":"https://api.github.com/repos/test-acc9/test_example/git/trees/d9899981dca2af8bfa20263c9c97413f017e7602"},"url":"https://api.github.com/repos/test-acc9/test_example/git/commits/a2d3e3c30547a000f026daa47610bb3f7b63aece","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJi4NeLCRBK7hj4Ov3rIwAAHFIIAD0YVuosBePsrNall2Qti6RF\nO4+4QzF5wK6WBgkuCX5mPaGUZMfwYohYUf6iipVVPJHhnxwGA/MIj5pt+muChJCx\nziP6+QfEg8uHR+od0MyG4atpojM2WpBa/gqNzTWnk1Ti7PCyMlueZQGVWLkTuU+Y\ng0fbKg7FFTAxWBhm4sTnb+8vFfwFiuHef7J2NpIdBVAUh4lmfk/hsqKlLz7s6xxS\n8qWz6BhCZSjXUjG+i4/DbwezV9YqsZMz73TbE04TX/pZvqXPoTyLQfCg1mpL2hrI\ndGJBHDZfr0CB8a+33e9TWT80U8TD4g6gjtwrwrZTVzwMfJVVSQcxIfVndQ7ihDg=\n=qgU3\n-----END + PGP SIGNATURE-----\n","payload":"tree d9899981dca2af8bfa20263c9c97413f017e7602\nparent + 610ada9fa2bbc49f1a08917da3f73bef2d03709c\nauthor test-acc9 <104562106+test-acc9@users.noreply.github.com> + 1658902411 +0300\ncommitter GitHub 1658902411 +0300\n\nrandom-commit-msg"}},"url":"https://api.github.com/repos/test-acc9/test_example/commits/a2d3e3c30547a000f026daa47610bb3f7b63aece","html_url":"https://github.com/test-acc9/test_example/commit/a2d3e3c30547a000f026daa47610bb3f7b63aece","comments_url":"https://api.github.com/repos/test-acc9/test_example/commits/a2d3e3c30547a000f026daa47610bb3f7b63aece/comments","author":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"610ada9fa2bbc49f1a08917da3f73bef2d03709c","url":"https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c","html_url":"https://github.com/test-acc9/test_example/commit/610ada9fa2bbc49f1a08917da3f73bef2d03709c"}]},{"sha":"610ada9fa2bbc49f1a08917da3f73bef2d03709c","node_id":"C_kwDOHP_eEdoAKDYxMGFkYTlmYTJiYmM0OWYxYTA4OTE3ZGEzZjczYmVmMmQwMzcwOWM","commit":{"author":{"name":"test-acc9","email":"104562106+test-acc9@users.noreply.github.com","date":"2022-04-28T09:37:05Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-04-28T09:37:05Z"},"message":"Create + random-commit.me","tree":{"sha":"729da23e42745fea6aff7dfdf1679146fe919620","url":"https://api.github.com/repos/test-acc9/test_example/git/trees/729da23e42745fea6aff7dfdf1679146fe919620"},"url":"https://api.github.com/repos/test-acc9/test_example/git/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJiamBBCRBK7hj4Ov3rIwAARu8IAFSGwf1o4Qrp71vxruVbUE0a\nBtA7q0ViOvbK7xwFP6mxJ8cbtKT/0MyYREdgvnw2qzc8KjoQjK0PCZBpHq3BP1WQ\nCkeW1S8xuCn4tnVJQeq+hA9NETt/YGY9cbZHv8o7636vZc7HhxKMiUXF2ppBJYNi\nsQk6W5LgPrb+su5WfZ299yH40b+DivZsoK+aAguhnlXzL4Tvznml78k9x4aO7KPM\nP/FGSzGrUQqNdlFI1xShhqAjwlLpAF2CE7CJ0CZ7q9PmYfYpLfB3bxWAv49+ozk0\nrJP1fdKMJvCmR0spPIjr/3bDhKoMVGB67sj5afkFchSNv0AkNMooVjSa5F2ABP4=\n=ALiA\n-----END + PGP SIGNATURE-----\n","payload":"tree 729da23e42745fea6aff7dfdf1679146fe919620\nparent + ef6edf5ae6643d53a7971fb8823d3f7b2ac65619\nauthor test-acc9 <104562106+test-acc9@users.noreply.github.com> + 1651138625 +0300\ncommitter GitHub 1651138625 +0300\n\nCreate + random-commit.me"}},"url":"https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c","html_url":"https://github.com/test-acc9/test_example/commit/610ada9fa2bbc49f1a08917da3f73bef2d03709c","comments_url":"https://api.github.com/repos/test-acc9/test_example/commits/610ada9fa2bbc49f1a08917da3f73bef2d03709c/comments","author":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","url":"https://api.github.com/repos/test-acc9/test_example/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","html_url":"https://github.com/test-acc9/test_example/commit/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619"}]},{"sha":"ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","node_id":"C_kwDOHP_eEdoAKGVmNmVkZjVhZTY2NDNkNTNhNzk3MWZiODgyM2QzZjdiMmFjNjU2MTk","commit":{"author":{"name":"test-acc9","email":"104562106+test-acc9@users.noreply.github.com","date":"2022-04-28T09:35:11Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-04-28T09:35:11Z"},"message":"Create + read.me","tree":{"sha":"5e836bead2ad06cc76854f5f8d3f4ab983ec60a1","url":"https://api.github.com/repos/test-acc9/test_example/git/trees/5e836bead2ad06cc76854f5f8d3f4ab983ec60a1"},"url":"https://api.github.com/repos/test-acc9/test_example/git/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJial/QCRBK7hj4Ov3rIwAA29QIAAdOIKu3w9SwC5g7UPjXj8xi\nYOt30BqPzSY/ktq7scRf/uOiO97uAC0XANXwmfkWCgI6YZSijysJdHEALFg7NwXp\n/aF8u9fFk+nHbhfTsCtM0jgabWRt0FSMsO0VreWn5ExiqWqPnDMTydbyCEzkHwsz\nMvVUGj8uqP4P7NM6DvCsOSM0m9Xg6IEFNqpsMd8yfvP5fF57YXnpixhISAUkGjuq\nJx/ouGxFircoRLTlJi5ZfmFNGFsXfoqvCyBMZ62lkg81lUYXEGKhDvFWnz6OR9sZ\n+4SxRgaFLz05xUKl3EEAhnWHjzrBkQP7gJBIz2MJ1VvFmH3h/hH86NtaN+TMCKU=\n=k3dl\n-----END + PGP SIGNATURE-----\n","payload":"tree 5e836bead2ad06cc76854f5f8d3f4ab983ec60a1\nauthor + test-acc9 <104562106+test-acc9@users.noreply.github.com> 1651138511 +0300\ncommitter + GitHub 1651138511 +0300\n\nCreate read.me"}},"url":"https://api.github.com/repos/test-acc9/test_example/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","html_url":"https://github.com/test-acc9/test_example/commit/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619","comments_url":"https://api.github.com/repos/test-acc9/test_example/commits/ef6edf5ae6643d53a7971fb8823d3f7b2ac65619/comments","author":{"login":"test-acc9","id":104562106,"node_id":"U_kgDOBjt9ug","avatar_url":"https://avatars.githubusercontent.com/u/104562106?v=4","gravatar_id":"","url":"https://api.github.com/users/test-acc9","html_url":"https://github.com/test-acc9","followers_url":"https://api.github.com/users/test-acc9/followers","following_url":"https://api.github.com/users/test-acc9/following{/other_user}","gists_url":"https://api.github.com/users/test-acc9/gists{/gist_id}","starred_url":"https://api.github.com/users/test-acc9/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/test-acc9/subscriptions","organizations_url":"https://api.github.com/users/test-acc9/orgs","repos_url":"https://api.github.com/users/test-acc9/repos","events_url":"https://api.github.com/users/test-acc9/events{/privacy}","received_events_url":"https://api.github.com/users/test-acc9/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 27 Jul 2022 09:29:50 GMT + ETag: + - W/"3fcf9a21d550ea3db4cb763923cd67d98acd60ede6ed033bbed7dc71cd223287" + Last-Modified: + - Wed, 27 Jul 2022 06:13:31 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C49C:2EAF:7C1481:808E1E:62E1058E + X-OAuth-Scopes: + - admin:repo_hook, repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4996' + X-RateLimit-Reset: + - '1658917789' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '4' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-08-25 14:53:43 UTC + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_end_of_trial_email_with_email_type.yaml b/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_end_of_trial_email_with_email_type.yaml new file mode 100644 index 0000000000..24025c1c4b --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_end_of_trial_email_with_email_type.yaml @@ -0,0 +1,59 @@ +interactions: +- request: + body: '{"list_ids": ["ff08b4ae-367e-49ac-9d5a-d6e0e004d19f"], "contacts": [{"email": + "felipe@codecov.io"}]}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '100' + Content-Type: + - application/json + User-Agent: + - python-requests/2.22.0 + method: PUT + uri: https://api.sendgrid.com/v3/marketing/contacts + response: + body: + string: '{"job_id":"9791f6a7-3d3b-4ae9-8f71-67bd98f33008"} + + ' + headers: + Connection: + - keep-alive + Content-Length: + - '50' + Content-Type: + - application/json + Date: + - Wed, 22 Jan 2020 21:33:47 GMT + Server: + - nginx + access-control-allow-headers: + - AUTHORIZATION, Content-Type, On-behalf-of, x-sg-elas-acl, X-Recaptcha, X-Request-Source + access-control-allow-methods: + - PUT,DELETE,OPTIONS + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Link, Location + referrer-policy: + - strict-origin-when-cross-origin + x-amz-apigw-id: + - GuLlIG2RPHcFtbA= + x-amzn-requestid: + - d4e105c8-b312-4585-97b4-f67d7f463ccf + x-amzn-trace-id: + - Root=1-5e28bfba-8aff770e542798e4a788763c;Sampled=0 + x-content-type-options: + - nosniff + x-envoy-upstream-service-time: + - '1554' + status: + code: 202 + message: Accepted +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_end_of_trial_email_with_list_type.yaml b/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_end_of_trial_email_with_list_type.yaml new file mode 100644 index 0000000000..24025c1c4b --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_end_of_trial_email_with_list_type.yaml @@ -0,0 +1,59 @@ +interactions: +- request: + body: '{"list_ids": ["ff08b4ae-367e-49ac-9d5a-d6e0e004d19f"], "contacts": [{"email": + "felipe@codecov.io"}]}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '100' + Content-Type: + - application/json + User-Agent: + - python-requests/2.22.0 + method: PUT + uri: https://api.sendgrid.com/v3/marketing/contacts + response: + body: + string: '{"job_id":"9791f6a7-3d3b-4ae9-8f71-67bd98f33008"} + + ' + headers: + Connection: + - keep-alive + Content-Length: + - '50' + Content-Type: + - application/json + Date: + - Wed, 22 Jan 2020 21:33:47 GMT + Server: + - nginx + access-control-allow-headers: + - AUTHORIZATION, Content-Type, On-behalf-of, x-sg-elas-acl, X-Recaptcha, X-Request-Source + access-control-allow-methods: + - PUT,DELETE,OPTIONS + access-control-allow-origin: + - '*' + access-control-expose-headers: + - Link, Location + referrer-policy: + - strict-origin-when-cross-origin + x-amz-apigw-id: + - GuLlIG2RPHcFtbA= + x-amzn-requestid: + - d4e105c8-b312-4585-97b4-f67d7f463ccf + x-amzn-trace-id: + - Root=1-5e28bfba-8aff770e542798e4a788763c;Sampled=0 + x-content-type-options: + - nosniff + x-envoy-upstream-service-time: + - '1554' + status: + code: 202 + message: Accepted +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_send_email_invalid_list_with_email_type.yaml b/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_send_email_invalid_list_with_email_type.yaml new file mode 100644 index 0000000000..ab370bd5c8 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_send_email_invalid_list_with_email_type.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_send_email_invalid_list_with_list_type.yaml b/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_send_email_invalid_list_with_list_type.yaml new file mode 100644 index 0000000000..ab370bd5c8 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_send_email_invalid_list_with_list_type.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_send_email_invalid_owner.yaml b/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_send_email_invalid_owner.yaml new file mode 100644 index 0000000000..ab370bd5c8 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_send_email_invalid_owner.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_send_email_invalid_owner_no_list_type.yaml b/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_send_email_invalid_owner_no_list_type.yaml new file mode 100644 index 0000000000..ab370bd5c8 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_send_email_invalid_owner_no_list_type.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_send_email_no_list.yaml b/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_send_email_no_list.yaml new file mode 100644 index 0000000000..ab370bd5c8 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_send_email/TestSendEmailTask/test_send_email_no_list.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_only_public_repos_already_in_db.yaml b/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_only_public_repos_already_in_db.yaml new file mode 100644 index 0000000000..57dd848a12 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_only_public_repos_already_in_db.yaml @@ -0,0 +1,233 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/user/repos?per_page=100&page=1 + response: + content: '[{"id":159090647,"node_id":"MDEwOlJlcG9zaXRvcnkxNTkwOTA2NDc=","name":"pub","full_name":"1nf1n1t3l00p/pub","private":false,"owner":{"login":"1nf1n1t3l00p","id":45343385,"node_id":"MDQ6VXNlcjQ1MzQzMzg1","avatar_url":"https://avatars1.githubusercontent.com/u/45343385?v=4","gravatar_id":"","url":"https://api.github.com/users/1nf1n1t3l00p","html_url":"https://github.com/1nf1n1t3l00p","followers_url":"https://api.github.com/users/1nf1n1t3l00p/followers","following_url":"https://api.github.com/users/1nf1n1t3l00p/following{/other_user}","gists_url":"https://api.github.com/users/1nf1n1t3l00p/gists{/gist_id}","starred_url":"https://api.github.com/users/1nf1n1t3l00p/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/1nf1n1t3l00p/subscriptions","organizations_url":"https://api.github.com/users/1nf1n1t3l00p/orgs","repos_url":"https://api.github.com/users/1nf1n1t3l00p/repos","events_url":"https://api.github.com/users/1nf1n1t3l00p/events{/privacy}","received_events_url":"https://api.github.com/users/1nf1n1t3l00p/received_events","type":"User","site_admin":false},"html_url":"https://github.com/1nf1n1t3l00p/pub","description":null,"fork":false,"url":"https://api.github.com/repos/1nf1n1t3l00p/pub","forks_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/forks","keys_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/keys{/key_id}","collaborators_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/teams","hooks_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/hooks","issue_events_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/issues/events{/number}","events_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/events","assignees_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/assignees{/user}","branches_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/branches{/branch}","tags_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/tags","blobs_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/git/refs{/sha}","trees_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/git/trees{/sha}","statuses_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/statuses/{sha}","languages_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/languages","stargazers_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/stargazers","contributors_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/contributors","subscribers_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/subscribers","subscription_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/subscription","commits_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/commits{/sha}","git_commits_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/git/commits{/sha}","comments_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/comments{/number}","issue_comment_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/issues/comments{/number}","contents_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/contents/{+path}","compare_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/compare/{base}...{head}","merges_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/merges","archive_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/downloads","issues_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/issues{/number}","pulls_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/pulls{/number}","milestones_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/milestones{/number}","notifications_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/labels{/name}","releases_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/releases{/id}","deployments_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/deployments","created_at":"2018-11-26T01:00:55Z","updated_at":"2018-11-26T01:00:58Z","pushed_at":"2018-11-26T01:00:57Z","git_url":"git://github.com/1nf1n1t3l00p/pub.git","ssh_url":"git@github.com:1nf1n1t3l00p/pub.git","clone_url":"https://github.com/1nf1n1t3l00p/pub.git","svn_url":"https://github.com/1nf1n1t3l00p/pub","homepage":null,"size":0,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master","permissions":{"admin":true,"push":true,"pull":true}},{"id":159089634,"node_id":"MDEwOlJlcG9zaXRvcnkxNTkwODk2MzQ=","name":"pytest","full_name":"1nf1n1t3l00p/pytest","private":false,"owner":{"login":"1nf1n1t3l00p","id":45343385,"node_id":"MDQ6VXNlcjQ1MzQzMzg1","avatar_url":"https://avatars1.githubusercontent.com/u/45343385?v=4","gravatar_id":"","url":"https://api.github.com/users/1nf1n1t3l00p","html_url":"https://github.com/1nf1n1t3l00p","followers_url":"https://api.github.com/users/1nf1n1t3l00p/followers","following_url":"https://api.github.com/users/1nf1n1t3l00p/following{/other_user}","gists_url":"https://api.github.com/users/1nf1n1t3l00p/gists{/gist_id}","starred_url":"https://api.github.com/users/1nf1n1t3l00p/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/1nf1n1t3l00p/subscriptions","organizations_url":"https://api.github.com/users/1nf1n1t3l00p/orgs","repos_url":"https://api.github.com/users/1nf1n1t3l00p/repos","events_url":"https://api.github.com/users/1nf1n1t3l00p/events{/privacy}","received_events_url":"https://api.github.com/users/1nf1n1t3l00p/received_events","type":"User","site_admin":false},"html_url":"https://github.com/1nf1n1t3l00p/pytest","description":"The + pytest framework makes it easy to write small tests, yet scales to support complex + functional testing","fork":true,"url":"https://api.github.com/repos/1nf1n1t3l00p/pytest","forks_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/forks","keys_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/keys{/key_id}","collaborators_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/teams","hooks_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/hooks","issue_events_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/issues/events{/number}","events_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/events","assignees_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/assignees{/user}","branches_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/branches{/branch}","tags_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/tags","blobs_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/refs{/sha}","trees_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/trees{/sha}","statuses_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/statuses/{sha}","languages_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/languages","stargazers_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/stargazers","contributors_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/contributors","subscribers_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/subscribers","subscription_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/subscription","commits_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/commits{/sha}","git_commits_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/commits{/sha}","comments_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/comments{/number}","issue_comment_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/issues/comments{/number}","contents_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/contents/{+path}","compare_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/compare/{base}...{head}","merges_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/merges","archive_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/downloads","issues_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/issues{/number}","pulls_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/pulls{/number}","milestones_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/milestones{/number}","notifications_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/labels{/name}","releases_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/releases{/id}","deployments_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/deployments","created_at":"2018-11-26T00:46:41Z","updated_at":"2018-11-26T00:46:44Z","pushed_at":"2018-11-25T19:30:27Z","git_url":"git://github.com/1nf1n1t3l00p/pytest.git","ssh_url":"git@github.com:1nf1n1t3l00p/pytest.git","clone_url":"https://github.com/1nf1n1t3l00p/pytest.git","svn_url":"https://github.com/1nf1n1t3l00p/pytest","homepage":"https://pytest.org","size":15137,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master","permissions":{"admin":true,"push":true,"pull":true}},{"id":164948070,"node_id":"MDEwOlJlcG9zaXRvcnkxNjQ5NDgwNzA=","name":"spack","full_name":"1nf1n1t3l00p/spack","private":false,"owner":{"login":"1nf1n1t3l00p","id":45343385,"node_id":"MDQ6VXNlcjQ1MzQzMzg1","avatar_url":"https://avatars1.githubusercontent.com/u/45343385?v=4","gravatar_id":"","url":"https://api.github.com/users/1nf1n1t3l00p","html_url":"https://github.com/1nf1n1t3l00p","followers_url":"https://api.github.com/users/1nf1n1t3l00p/followers","following_url":"https://api.github.com/users/1nf1n1t3l00p/following{/other_user}","gists_url":"https://api.github.com/users/1nf1n1t3l00p/gists{/gist_id}","starred_url":"https://api.github.com/users/1nf1n1t3l00p/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/1nf1n1t3l00p/subscriptions","organizations_url":"https://api.github.com/users/1nf1n1t3l00p/orgs","repos_url":"https://api.github.com/users/1nf1n1t3l00p/repos","events_url":"https://api.github.com/users/1nf1n1t3l00p/events{/privacy}","received_events_url":"https://api.github.com/users/1nf1n1t3l00p/received_events","type":"User","site_admin":false},"html_url":"https://github.com/1nf1n1t3l00p/spack","description":"A + flexible package manager that supports multiple versions, configurations, platforms, + and compilers.","fork":true,"url":"https://api.github.com/repos/1nf1n1t3l00p/spack","forks_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/forks","keys_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/keys{/key_id}","collaborators_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/teams","hooks_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/hooks","issue_events_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/issues/events{/number}","events_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/events","assignees_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/assignees{/user}","branches_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/branches{/branch}","tags_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/tags","blobs_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/refs{/sha}","trees_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/trees{/sha}","statuses_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/statuses/{sha}","languages_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/languages","stargazers_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/stargazers","contributors_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/contributors","subscribers_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/subscribers","subscription_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/subscription","commits_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/commits{/sha}","git_commits_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/commits{/sha}","comments_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/comments{/number}","issue_comment_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/issues/comments{/number}","contents_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/contents/{+path}","compare_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/compare/{base}...{head}","merges_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/merges","archive_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/downloads","issues_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/issues{/number}","pulls_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/pulls{/number}","milestones_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/milestones{/number}","notifications_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/labels{/name}","releases_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/releases{/id}","deployments_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/deployments","created_at":"2019-01-09T22:27:51Z","updated_at":"2019-01-09T22:28:01Z","pushed_at":"2019-01-09T22:19:36Z","git_url":"git://github.com/1nf1n1t3l00p/spack.git","ssh_url":"git@github.com:1nf1n1t3l00p/spack.git","clone_url":"https://github.com/1nf1n1t3l00p/spack.git","svn_url":"https://github.com/1nf1n1t3l00p/spack","homepage":"https://spack.io","size":50539,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":0,"open_issues":0,"watchers":0,"default_branch":"develop","permissions":{"admin":true,"push":true,"pull":true}}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Sun, 27 Oct 2019 03:14:12 GMT + Etag: + - W/"e3801bcbeed09428da1710250fede463" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - C030:746A:10DBEF9:2327992:5DB50B84 + X-Oauth-Scopes: + - repo, user + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4999' + X-Ratelimit-Reset: + - '1572149652' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/user/repos?per_page=100&page=1 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/1nf1n1t3l00p/pytest + response: + content: '{"id":159089634,"node_id":"MDEwOlJlcG9zaXRvcnkxNTkwODk2MzQ=","name":"pytest","full_name":"1nf1n1t3l00p/pytest","private":false,"owner":{"login":"1nf1n1t3l00p","id":45343385,"node_id":"MDQ6VXNlcjQ1MzQzMzg1","avatar_url":"https://avatars1.githubusercontent.com/u/45343385?v=4","gravatar_id":"","url":"https://api.github.com/users/1nf1n1t3l00p","html_url":"https://github.com/1nf1n1t3l00p","followers_url":"https://api.github.com/users/1nf1n1t3l00p/followers","following_url":"https://api.github.com/users/1nf1n1t3l00p/following{/other_user}","gists_url":"https://api.github.com/users/1nf1n1t3l00p/gists{/gist_id}","starred_url":"https://api.github.com/users/1nf1n1t3l00p/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/1nf1n1t3l00p/subscriptions","organizations_url":"https://api.github.com/users/1nf1n1t3l00p/orgs","repos_url":"https://api.github.com/users/1nf1n1t3l00p/repos","events_url":"https://api.github.com/users/1nf1n1t3l00p/events{/privacy}","received_events_url":"https://api.github.com/users/1nf1n1t3l00p/received_events","type":"User","site_admin":false},"html_url":"https://github.com/1nf1n1t3l00p/pytest","description":"The + pytest framework makes it easy to write small tests, yet scales to support complex + functional testing","fork":true,"url":"https://api.github.com/repos/1nf1n1t3l00p/pytest","forks_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/forks","keys_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/keys{/key_id}","collaborators_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/teams","hooks_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/hooks","issue_events_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/issues/events{/number}","events_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/events","assignees_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/assignees{/user}","branches_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/branches{/branch}","tags_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/tags","blobs_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/refs{/sha}","trees_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/trees{/sha}","statuses_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/statuses/{sha}","languages_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/languages","stargazers_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/stargazers","contributors_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/contributors","subscribers_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/subscribers","subscription_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/subscription","commits_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/commits{/sha}","git_commits_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/commits{/sha}","comments_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/comments{/number}","issue_comment_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/issues/comments{/number}","contents_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/contents/{+path}","compare_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/compare/{base}...{head}","merges_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/merges","archive_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/downloads","issues_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/issues{/number}","pulls_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/pulls{/number}","milestones_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/milestones{/number}","notifications_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/labels{/name}","releases_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/releases{/id}","deployments_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/deployments","created_at":"2018-11-26T00:46:41Z","updated_at":"2018-11-26T00:46:44Z","pushed_at":"2018-11-25T19:30:27Z","git_url":"git://github.com/1nf1n1t3l00p/pytest.git","ssh_url":"git@github.com:1nf1n1t3l00p/pytest.git","clone_url":"https://github.com/1nf1n1t3l00p/pytest.git","svn_url":"https://github.com/1nf1n1t3l00p/pytest","homepage":"https://pytest.org","size":15137,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master","permissions":{"admin":true,"push":true,"pull":true},"allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"parent":{"id":37489525,"node_id":"MDEwOlJlcG9zaXRvcnkzNzQ4OTUyNQ==","name":"pytest","full_name":"pytest-dev/pytest","private":false,"owner":{"login":"pytest-dev","id":8897583,"node_id":"MDEyOk9yZ2FuaXphdGlvbjg4OTc1ODM=","avatar_url":"https://avatars2.githubusercontent.com/u/8897583?v=4","gravatar_id":"","url":"https://api.github.com/users/pytest-dev","html_url":"https://github.com/pytest-dev","followers_url":"https://api.github.com/users/pytest-dev/followers","following_url":"https://api.github.com/users/pytest-dev/following{/other_user}","gists_url":"https://api.github.com/users/pytest-dev/gists{/gist_id}","starred_url":"https://api.github.com/users/pytest-dev/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pytest-dev/subscriptions","organizations_url":"https://api.github.com/users/pytest-dev/orgs","repos_url":"https://api.github.com/users/pytest-dev/repos","events_url":"https://api.github.com/users/pytest-dev/events{/privacy}","received_events_url":"https://api.github.com/users/pytest-dev/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/pytest-dev/pytest","description":"The + pytest framework makes it easy to write small tests, yet scales to support complex + functional testing","fork":false,"url":"https://api.github.com/repos/pytest-dev/pytest","forks_url":"https://api.github.com/repos/pytest-dev/pytest/forks","keys_url":"https://api.github.com/repos/pytest-dev/pytest/keys{/key_id}","collaborators_url":"https://api.github.com/repos/pytest-dev/pytest/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/pytest-dev/pytest/teams","hooks_url":"https://api.github.com/repos/pytest-dev/pytest/hooks","issue_events_url":"https://api.github.com/repos/pytest-dev/pytest/issues/events{/number}","events_url":"https://api.github.com/repos/pytest-dev/pytest/events","assignees_url":"https://api.github.com/repos/pytest-dev/pytest/assignees{/user}","branches_url":"https://api.github.com/repos/pytest-dev/pytest/branches{/branch}","tags_url":"https://api.github.com/repos/pytest-dev/pytest/tags","blobs_url":"https://api.github.com/repos/pytest-dev/pytest/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/pytest-dev/pytest/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/pytest-dev/pytest/git/refs{/sha}","trees_url":"https://api.github.com/repos/pytest-dev/pytest/git/trees{/sha}","statuses_url":"https://api.github.com/repos/pytest-dev/pytest/statuses/{sha}","languages_url":"https://api.github.com/repos/pytest-dev/pytest/languages","stargazers_url":"https://api.github.com/repos/pytest-dev/pytest/stargazers","contributors_url":"https://api.github.com/repos/pytest-dev/pytest/contributors","subscribers_url":"https://api.github.com/repos/pytest-dev/pytest/subscribers","subscription_url":"https://api.github.com/repos/pytest-dev/pytest/subscription","commits_url":"https://api.github.com/repos/pytest-dev/pytest/commits{/sha}","git_commits_url":"https://api.github.com/repos/pytest-dev/pytest/git/commits{/sha}","comments_url":"https://api.github.com/repos/pytest-dev/pytest/comments{/number}","issue_comment_url":"https://api.github.com/repos/pytest-dev/pytest/issues/comments{/number}","contents_url":"https://api.github.com/repos/pytest-dev/pytest/contents/{+path}","compare_url":"https://api.github.com/repos/pytest-dev/pytest/compare/{base}...{head}","merges_url":"https://api.github.com/repos/pytest-dev/pytest/merges","archive_url":"https://api.github.com/repos/pytest-dev/pytest/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/pytest-dev/pytest/downloads","issues_url":"https://api.github.com/repos/pytest-dev/pytest/issues{/number}","pulls_url":"https://api.github.com/repos/pytest-dev/pytest/pulls{/number}","milestones_url":"https://api.github.com/repos/pytest-dev/pytest/milestones{/number}","notifications_url":"https://api.github.com/repos/pytest-dev/pytest/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/pytest-dev/pytest/labels{/name}","releases_url":"https://api.github.com/repos/pytest-dev/pytest/releases{/id}","deployments_url":"https://api.github.com/repos/pytest-dev/pytest/deployments","created_at":"2015-06-15T20:28:27Z","updated_at":"2019-10-26T17:20:00Z","pushed_at":"2019-10-27T02:02:44Z","git_url":"git://github.com/pytest-dev/pytest.git","ssh_url":"git@github.com:pytest-dev/pytest.git","clone_url":"https://github.com/pytest-dev/pytest.git","svn_url":"https://github.com/pytest-dev/pytest","homepage":"https://pytest.org","size":18966,"stargazers_count":4986,"watchers_count":4986,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1194,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":608,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":1194,"open_issues":608,"watchers":4986,"default_branch":"master"},"source":{"id":37489525,"node_id":"MDEwOlJlcG9zaXRvcnkzNzQ4OTUyNQ==","name":"pytest","full_name":"pytest-dev/pytest","private":false,"owner":{"login":"pytest-dev","id":8897583,"node_id":"MDEyOk9yZ2FuaXphdGlvbjg4OTc1ODM=","avatar_url":"https://avatars2.githubusercontent.com/u/8897583?v=4","gravatar_id":"","url":"https://api.github.com/users/pytest-dev","html_url":"https://github.com/pytest-dev","followers_url":"https://api.github.com/users/pytest-dev/followers","following_url":"https://api.github.com/users/pytest-dev/following{/other_user}","gists_url":"https://api.github.com/users/pytest-dev/gists{/gist_id}","starred_url":"https://api.github.com/users/pytest-dev/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pytest-dev/subscriptions","organizations_url":"https://api.github.com/users/pytest-dev/orgs","repos_url":"https://api.github.com/users/pytest-dev/repos","events_url":"https://api.github.com/users/pytest-dev/events{/privacy}","received_events_url":"https://api.github.com/users/pytest-dev/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/pytest-dev/pytest","description":"The + pytest framework makes it easy to write small tests, yet scales to support complex + functional testing","fork":false,"url":"https://api.github.com/repos/pytest-dev/pytest","forks_url":"https://api.github.com/repos/pytest-dev/pytest/forks","keys_url":"https://api.github.com/repos/pytest-dev/pytest/keys{/key_id}","collaborators_url":"https://api.github.com/repos/pytest-dev/pytest/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/pytest-dev/pytest/teams","hooks_url":"https://api.github.com/repos/pytest-dev/pytest/hooks","issue_events_url":"https://api.github.com/repos/pytest-dev/pytest/issues/events{/number}","events_url":"https://api.github.com/repos/pytest-dev/pytest/events","assignees_url":"https://api.github.com/repos/pytest-dev/pytest/assignees{/user}","branches_url":"https://api.github.com/repos/pytest-dev/pytest/branches{/branch}","tags_url":"https://api.github.com/repos/pytest-dev/pytest/tags","blobs_url":"https://api.github.com/repos/pytest-dev/pytest/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/pytest-dev/pytest/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/pytest-dev/pytest/git/refs{/sha}","trees_url":"https://api.github.com/repos/pytest-dev/pytest/git/trees{/sha}","statuses_url":"https://api.github.com/repos/pytest-dev/pytest/statuses/{sha}","languages_url":"https://api.github.com/repos/pytest-dev/pytest/languages","stargazers_url":"https://api.github.com/repos/pytest-dev/pytest/stargazers","contributors_url":"https://api.github.com/repos/pytest-dev/pytest/contributors","subscribers_url":"https://api.github.com/repos/pytest-dev/pytest/subscribers","subscription_url":"https://api.github.com/repos/pytest-dev/pytest/subscription","commits_url":"https://api.github.com/repos/pytest-dev/pytest/commits{/sha}","git_commits_url":"https://api.github.com/repos/pytest-dev/pytest/git/commits{/sha}","comments_url":"https://api.github.com/repos/pytest-dev/pytest/comments{/number}","issue_comment_url":"https://api.github.com/repos/pytest-dev/pytest/issues/comments{/number}","contents_url":"https://api.github.com/repos/pytest-dev/pytest/contents/{+path}","compare_url":"https://api.github.com/repos/pytest-dev/pytest/compare/{base}...{head}","merges_url":"https://api.github.com/repos/pytest-dev/pytest/merges","archive_url":"https://api.github.com/repos/pytest-dev/pytest/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/pytest-dev/pytest/downloads","issues_url":"https://api.github.com/repos/pytest-dev/pytest/issues{/number}","pulls_url":"https://api.github.com/repos/pytest-dev/pytest/pulls{/number}","milestones_url":"https://api.github.com/repos/pytest-dev/pytest/milestones{/number}","notifications_url":"https://api.github.com/repos/pytest-dev/pytest/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/pytest-dev/pytest/labels{/name}","releases_url":"https://api.github.com/repos/pytest-dev/pytest/releases{/id}","deployments_url":"https://api.github.com/repos/pytest-dev/pytest/deployments","created_at":"2015-06-15T20:28:27Z","updated_at":"2019-10-26T17:20:00Z","pushed_at":"2019-10-27T02:02:44Z","git_url":"git://github.com/pytest-dev/pytest.git","ssh_url":"git@github.com:pytest-dev/pytest.git","clone_url":"https://github.com/pytest-dev/pytest.git","svn_url":"https://github.com/pytest-dev/pytest","homepage":"https://pytest.org","size":18966,"stargazers_count":4986,"watchers_count":4986,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1194,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":608,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":1194,"open_issues":608,"watchers":4986,"default_branch":"master"},"network_count":1194,"subscribers_count":0}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Sun, 27 Oct 2019 03:14:12 GMT + Etag: + - W/"5b77b875c911b58b9136a0e9569432a5" + Last-Modified: + - Mon, 26 Nov 2018 00:46:44 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - repo + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - C034:59D8:133AC85:26F6B8C:5DB50B84 + X-Oauth-Scopes: + - repo, user + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4997' + X-Ratelimit-Reset: + - '1572149652' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/1nf1n1t3l00p/pytest +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/1nf1n1t3l00p/spack + response: + content: '{"id":164948070,"node_id":"MDEwOlJlcG9zaXRvcnkxNjQ5NDgwNzA=","name":"spack","full_name":"1nf1n1t3l00p/spack","private":false,"owner":{"login":"1nf1n1t3l00p","id":45343385,"node_id":"MDQ6VXNlcjQ1MzQzMzg1","avatar_url":"https://avatars1.githubusercontent.com/u/45343385?v=4","gravatar_id":"","url":"https://api.github.com/users/1nf1n1t3l00p","html_url":"https://github.com/1nf1n1t3l00p","followers_url":"https://api.github.com/users/1nf1n1t3l00p/followers","following_url":"https://api.github.com/users/1nf1n1t3l00p/following{/other_user}","gists_url":"https://api.github.com/users/1nf1n1t3l00p/gists{/gist_id}","starred_url":"https://api.github.com/users/1nf1n1t3l00p/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/1nf1n1t3l00p/subscriptions","organizations_url":"https://api.github.com/users/1nf1n1t3l00p/orgs","repos_url":"https://api.github.com/users/1nf1n1t3l00p/repos","events_url":"https://api.github.com/users/1nf1n1t3l00p/events{/privacy}","received_events_url":"https://api.github.com/users/1nf1n1t3l00p/received_events","type":"User","site_admin":false},"html_url":"https://github.com/1nf1n1t3l00p/spack","description":"A + flexible package manager that supports multiple versions, configurations, platforms, + and compilers.","fork":true,"url":"https://api.github.com/repos/1nf1n1t3l00p/spack","forks_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/forks","keys_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/keys{/key_id}","collaborators_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/teams","hooks_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/hooks","issue_events_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/issues/events{/number}","events_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/events","assignees_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/assignees{/user}","branches_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/branches{/branch}","tags_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/tags","blobs_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/refs{/sha}","trees_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/trees{/sha}","statuses_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/statuses/{sha}","languages_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/languages","stargazers_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/stargazers","contributors_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/contributors","subscribers_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/subscribers","subscription_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/subscription","commits_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/commits{/sha}","git_commits_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/commits{/sha}","comments_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/comments{/number}","issue_comment_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/issues/comments{/number}","contents_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/contents/{+path}","compare_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/compare/{base}...{head}","merges_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/merges","archive_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/downloads","issues_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/issues{/number}","pulls_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/pulls{/number}","milestones_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/milestones{/number}","notifications_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/labels{/name}","releases_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/releases{/id}","deployments_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/deployments","created_at":"2019-01-09T22:27:51Z","updated_at":"2019-01-09T22:28:01Z","pushed_at":"2019-01-09T22:19:36Z","git_url":"git://github.com/1nf1n1t3l00p/spack.git","ssh_url":"git@github.com:1nf1n1t3l00p/spack.git","clone_url":"https://github.com/1nf1n1t3l00p/spack.git","svn_url":"https://github.com/1nf1n1t3l00p/spack","homepage":"https://spack.io","size":50539,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":0,"open_issues":0,"watchers":0,"default_branch":"develop","permissions":{"admin":true,"push":true,"pull":true},"allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"parent":{"id":15730865,"node_id":"MDEwOlJlcG9zaXRvcnkxNTczMDg2NQ==","name":"spack","full_name":"spack/spack","private":false,"owner":{"login":"spack","id":25539161,"node_id":"MDEyOk9yZ2FuaXphdGlvbjI1NTM5MTYx","avatar_url":"https://avatars2.githubusercontent.com/u/25539161?v=4","gravatar_id":"","url":"https://api.github.com/users/spack","html_url":"https://github.com/spack","followers_url":"https://api.github.com/users/spack/followers","following_url":"https://api.github.com/users/spack/following{/other_user}","gists_url":"https://api.github.com/users/spack/gists{/gist_id}","starred_url":"https://api.github.com/users/spack/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/spack/subscriptions","organizations_url":"https://api.github.com/users/spack/orgs","repos_url":"https://api.github.com/users/spack/repos","events_url":"https://api.github.com/users/spack/events{/privacy}","received_events_url":"https://api.github.com/users/spack/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/spack/spack","description":"A + flexible package manager that supports multiple versions, configurations, platforms, + and compilers.","fork":false,"url":"https://api.github.com/repos/spack/spack","forks_url":"https://api.github.com/repos/spack/spack/forks","keys_url":"https://api.github.com/repos/spack/spack/keys{/key_id}","collaborators_url":"https://api.github.com/repos/spack/spack/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/spack/spack/teams","hooks_url":"https://api.github.com/repos/spack/spack/hooks","issue_events_url":"https://api.github.com/repos/spack/spack/issues/events{/number}","events_url":"https://api.github.com/repos/spack/spack/events","assignees_url":"https://api.github.com/repos/spack/spack/assignees{/user}","branches_url":"https://api.github.com/repos/spack/spack/branches{/branch}","tags_url":"https://api.github.com/repos/spack/spack/tags","blobs_url":"https://api.github.com/repos/spack/spack/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/spack/spack/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/spack/spack/git/refs{/sha}","trees_url":"https://api.github.com/repos/spack/spack/git/trees{/sha}","statuses_url":"https://api.github.com/repos/spack/spack/statuses/{sha}","languages_url":"https://api.github.com/repos/spack/spack/languages","stargazers_url":"https://api.github.com/repos/spack/spack/stargazers","contributors_url":"https://api.github.com/repos/spack/spack/contributors","subscribers_url":"https://api.github.com/repos/spack/spack/subscribers","subscription_url":"https://api.github.com/repos/spack/spack/subscription","commits_url":"https://api.github.com/repos/spack/spack/commits{/sha}","git_commits_url":"https://api.github.com/repos/spack/spack/git/commits{/sha}","comments_url":"https://api.github.com/repos/spack/spack/comments{/number}","issue_comment_url":"https://api.github.com/repos/spack/spack/issues/comments{/number}","contents_url":"https://api.github.com/repos/spack/spack/contents/{+path}","compare_url":"https://api.github.com/repos/spack/spack/compare/{base}...{head}","merges_url":"https://api.github.com/repos/spack/spack/merges","archive_url":"https://api.github.com/repos/spack/spack/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/spack/spack/downloads","issues_url":"https://api.github.com/repos/spack/spack/issues{/number}","pulls_url":"https://api.github.com/repos/spack/spack/pulls{/number}","milestones_url":"https://api.github.com/repos/spack/spack/milestones{/number}","notifications_url":"https://api.github.com/repos/spack/spack/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/spack/spack/labels{/name}","releases_url":"https://api.github.com/repos/spack/spack/releases{/id}","deployments_url":"https://api.github.com/repos/spack/spack/deployments","created_at":"2014-01-08T09:22:12Z","updated_at":"2019-10-27T02:03:43Z","pushed_at":"2019-10-27T02:03:41Z","git_url":"git://github.com/spack/spack.git","ssh_url":"git@github.com:spack/spack.git","clone_url":"https://github.com/spack/spack.git","svn_url":"https://github.com/spack/spack","homepage":"https://spack.io","size":66728,"stargazers_count":1252,"watchers_count":1252,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":783,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1383,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":783,"open_issues":1383,"watchers":1252,"default_branch":"develop"},"source":{"id":15730865,"node_id":"MDEwOlJlcG9zaXRvcnkxNTczMDg2NQ==","name":"spack","full_name":"spack/spack","private":false,"owner":{"login":"spack","id":25539161,"node_id":"MDEyOk9yZ2FuaXphdGlvbjI1NTM5MTYx","avatar_url":"https://avatars2.githubusercontent.com/u/25539161?v=4","gravatar_id":"","url":"https://api.github.com/users/spack","html_url":"https://github.com/spack","followers_url":"https://api.github.com/users/spack/followers","following_url":"https://api.github.com/users/spack/following{/other_user}","gists_url":"https://api.github.com/users/spack/gists{/gist_id}","starred_url":"https://api.github.com/users/spack/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/spack/subscriptions","organizations_url":"https://api.github.com/users/spack/orgs","repos_url":"https://api.github.com/users/spack/repos","events_url":"https://api.github.com/users/spack/events{/privacy}","received_events_url":"https://api.github.com/users/spack/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/spack/spack","description":"A + flexible package manager that supports multiple versions, configurations, platforms, + and compilers.","fork":false,"url":"https://api.github.com/repos/spack/spack","forks_url":"https://api.github.com/repos/spack/spack/forks","keys_url":"https://api.github.com/repos/spack/spack/keys{/key_id}","collaborators_url":"https://api.github.com/repos/spack/spack/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/spack/spack/teams","hooks_url":"https://api.github.com/repos/spack/spack/hooks","issue_events_url":"https://api.github.com/repos/spack/spack/issues/events{/number}","events_url":"https://api.github.com/repos/spack/spack/events","assignees_url":"https://api.github.com/repos/spack/spack/assignees{/user}","branches_url":"https://api.github.com/repos/spack/spack/branches{/branch}","tags_url":"https://api.github.com/repos/spack/spack/tags","blobs_url":"https://api.github.com/repos/spack/spack/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/spack/spack/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/spack/spack/git/refs{/sha}","trees_url":"https://api.github.com/repos/spack/spack/git/trees{/sha}","statuses_url":"https://api.github.com/repos/spack/spack/statuses/{sha}","languages_url":"https://api.github.com/repos/spack/spack/languages","stargazers_url":"https://api.github.com/repos/spack/spack/stargazers","contributors_url":"https://api.github.com/repos/spack/spack/contributors","subscribers_url":"https://api.github.com/repos/spack/spack/subscribers","subscription_url":"https://api.github.com/repos/spack/spack/subscription","commits_url":"https://api.github.com/repos/spack/spack/commits{/sha}","git_commits_url":"https://api.github.com/repos/spack/spack/git/commits{/sha}","comments_url":"https://api.github.com/repos/spack/spack/comments{/number}","issue_comment_url":"https://api.github.com/repos/spack/spack/issues/comments{/number}","contents_url":"https://api.github.com/repos/spack/spack/contents/{+path}","compare_url":"https://api.github.com/repos/spack/spack/compare/{base}...{head}","merges_url":"https://api.github.com/repos/spack/spack/merges","archive_url":"https://api.github.com/repos/spack/spack/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/spack/spack/downloads","issues_url":"https://api.github.com/repos/spack/spack/issues{/number}","pulls_url":"https://api.github.com/repos/spack/spack/pulls{/number}","milestones_url":"https://api.github.com/repos/spack/spack/milestones{/number}","notifications_url":"https://api.github.com/repos/spack/spack/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/spack/spack/labels{/name}","releases_url":"https://api.github.com/repos/spack/spack/releases{/id}","deployments_url":"https://api.github.com/repos/spack/spack/deployments","created_at":"2014-01-08T09:22:12Z","updated_at":"2019-10-27T02:03:43Z","pushed_at":"2019-10-27T02:03:41Z","git_url":"git://github.com/spack/spack.git","ssh_url":"git@github.com:spack/spack.git","clone_url":"https://github.com/spack/spack.git","svn_url":"https://github.com/spack/spack","homepage":"https://spack.io","size":66728,"stargazers_count":1252,"watchers_count":1252,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":783,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1383,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":783,"open_issues":1383,"watchers":1252,"default_branch":"develop"},"network_count":783,"subscribers_count":0}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Sun, 27 Oct 2019 03:14:13 GMT + Etag: + - W/"469e9879690659a462c89604c730d161" + Last-Modified: + - Wed, 09 Jan 2019 22:28:01 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - repo + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - C036:0DDA:11E82F3:24C6EEF:5DB50B85 + X-Oauth-Scopes: + - repo, user + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4996' + X-Ratelimit-Reset: + - '1572149652' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/1nf1n1t3l00p/spack +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_only_public_repos_not_in_db.yaml b/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_only_public_repos_not_in_db.yaml new file mode 100644 index 0000000000..4183e6c50a --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_only_public_repos_not_in_db.yaml @@ -0,0 +1,71 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/user/repos?per_page=100&page=1 + response: + content: '[{"id":159090647,"node_id":"MDEwOlJlcG9zaXRvcnkxNTkwOTA2NDc=","name":"pub","full_name":"1nf1n1t3l00p/pub","private":false,"owner":{"login":"1nf1n1t3l00p","id":45343385,"node_id":"MDQ6VXNlcjQ1MzQzMzg1","avatar_url":"https://avatars1.githubusercontent.com/u/45343385?v=4","gravatar_id":"","url":"https://api.github.com/users/1nf1n1t3l00p","html_url":"https://github.com/1nf1n1t3l00p","followers_url":"https://api.github.com/users/1nf1n1t3l00p/followers","following_url":"https://api.github.com/users/1nf1n1t3l00p/following{/other_user}","gists_url":"https://api.github.com/users/1nf1n1t3l00p/gists{/gist_id}","starred_url":"https://api.github.com/users/1nf1n1t3l00p/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/1nf1n1t3l00p/subscriptions","organizations_url":"https://api.github.com/users/1nf1n1t3l00p/orgs","repos_url":"https://api.github.com/users/1nf1n1t3l00p/repos","events_url":"https://api.github.com/users/1nf1n1t3l00p/events{/privacy}","received_events_url":"https://api.github.com/users/1nf1n1t3l00p/received_events","type":"User","site_admin":false},"html_url":"https://github.com/1nf1n1t3l00p/pub","description":null,"fork":false,"url":"https://api.github.com/repos/1nf1n1t3l00p/pub","forks_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/forks","keys_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/keys{/key_id}","collaborators_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/teams","hooks_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/hooks","issue_events_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/issues/events{/number}","events_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/events","assignees_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/assignees{/user}","branches_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/branches{/branch}","tags_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/tags","blobs_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/git/refs{/sha}","trees_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/git/trees{/sha}","statuses_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/statuses/{sha}","languages_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/languages","stargazers_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/stargazers","contributors_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/contributors","subscribers_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/subscribers","subscription_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/subscription","commits_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/commits{/sha}","git_commits_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/git/commits{/sha}","comments_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/comments{/number}","issue_comment_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/issues/comments{/number}","contents_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/contents/{+path}","compare_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/compare/{base}...{head}","merges_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/merges","archive_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/downloads","issues_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/issues{/number}","pulls_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/pulls{/number}","milestones_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/milestones{/number}","notifications_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/labels{/name}","releases_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/releases{/id}","deployments_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/deployments","created_at":"2018-11-26T01:00:55Z","updated_at":"2018-11-26T01:00:58Z","pushed_at":"2018-11-26T01:00:57Z","git_url":"git://github.com/1nf1n1t3l00p/pub.git","ssh_url":"git@github.com:1nf1n1t3l00p/pub.git","clone_url":"https://github.com/1nf1n1t3l00p/pub.git","svn_url":"https://github.com/1nf1n1t3l00p/pub","homepage":null,"size":0,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master","permissions":{"admin":true,"push":true,"pull":true}}]' + headers: + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Sun, 27 Oct 2019 03:14:12 GMT + Etag: + - W/"e3801bcbeed09428da1710250fede463" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - "" + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - C030:746A:10DBEF9:2327992:5DB50B84 + X-Oauth-Scopes: + - repo, user + X-Ratelimit-Limit: + - "5000" + X-Ratelimit-Remaining: + - "4999" + X-Ratelimit-Reset: + - "1572149652" + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/user/repos?per_page=100&page=1 +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_private_repos_set_bot.yaml b/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_private_repos_set_bot.yaml new file mode 100644 index 0000000000..c527445fa2 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_private_repos_set_bot.yaml @@ -0,0 +1,198 @@ +interactions: +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://api.github.com/user/repos?per_page=100&page=1 + response: + content: '[{"id":168373402,"node_id":"MDEwOlJlcG9zaXRvcnkxNjgzNzM0MDI=","name":"neomake","full_name":"1nf1n1t3l00p/neomake","private":false,"owner":{"login":"1nf1n1t3l00p","id":45343385,"node_id":"MDQ6VXNlcjQ1MzQzMzg1","avatar_url":"https://avatars1.githubusercontent.com/u/45343385?v=4","gravatar_id":"","url":"https://api.github.com/users/1nf1n1t3l00p","html_url":"https://github.com/1nf1n1t3l00p","followers_url":"https://api.github.com/users/1nf1n1t3l00p/followers","following_url":"https://api.github.com/users/1nf1n1t3l00p/following{/other_user}","gists_url":"https://api.github.com/users/1nf1n1t3l00p/gists{/gist_id}","starred_url":"https://api.github.com/users/1nf1n1t3l00p/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/1nf1n1t3l00p/subscriptions","organizations_url":"https://api.github.com/users/1nf1n1t3l00p/orgs","repos_url":"https://api.github.com/users/1nf1n1t3l00p/repos","events_url":"https://api.github.com/users/1nf1n1t3l00p/events{/privacy}","received_events_url":"https://api.github.com/users/1nf1n1t3l00p/received_events","type":"User","site_admin":false},"html_url":"https://github.com/1nf1n1t3l00p/neomake","description":"Asynchronous + linting and make framework for Neovim/Vim","fork":true,"url":"https://api.github.com/repos/1nf1n1t3l00p/neomake","forks_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/forks","keys_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/keys{/key_id}","collaborators_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/teams","hooks_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/hooks","issue_events_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/issues/events{/number}","events_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/events","assignees_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/assignees{/user}","branches_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/branches{/branch}","tags_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/tags","blobs_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/git/refs{/sha}","trees_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/git/trees{/sha}","statuses_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/statuses/{sha}","languages_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/languages","stargazers_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/stargazers","contributors_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/contributors","subscribers_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/subscribers","subscription_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/subscription","commits_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/commits{/sha}","git_commits_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/git/commits{/sha}","comments_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/comments{/number}","issue_comment_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/issues/comments{/number}","contents_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/contents/{+path}","compare_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/compare/{base}...{head}","merges_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/merges","archive_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/downloads","issues_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/issues{/number}","pulls_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/pulls{/number}","milestones_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/milestones{/number}","notifications_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/labels{/name}","releases_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/releases{/id}","deployments_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/deployments","created_at":"2019-01-30T16:13:56Z","updated_at":"2019-01-30T16:13:59Z","pushed_at":"2019-01-27T15:56:53Z","git_url":"git://github.com/1nf1n1t3l00p/neomake.git","ssh_url":"git@github.com:1nf1n1t3l00p/neomake.git","clone_url":"https://github.com/1nf1n1t3l00p/neomake.git","svn_url":"https://github.com/1nf1n1t3l00p/neomake","homepage":"","size":4862,"stargazers_count":0,"watchers_count":0,"language":"Vim + script","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master","permissions":{"admin":true,"push":true,"pull":true}},{"id":159090505,"node_id":"MDEwOlJlcG9zaXRvcnkxNTkwOTA1MDU=","name":"priv_example","full_name":"1nf1n1t3l00p/priv_example","private":true,"owner":{"login":"1nf1n1t3l00p","id":45343385,"node_id":"MDQ6VXNlcjQ1MzQzMzg1","avatar_url":"https://avatars1.githubusercontent.com/u/45343385?v=4","gravatar_id":"","url":"https://api.github.com/users/1nf1n1t3l00p","html_url":"https://github.com/1nf1n1t3l00p","followers_url":"https://api.github.com/users/1nf1n1t3l00p/followers","following_url":"https://api.github.com/users/1nf1n1t3l00p/following{/other_user}","gists_url":"https://api.github.com/users/1nf1n1t3l00p/gists{/gist_id}","starred_url":"https://api.github.com/users/1nf1n1t3l00p/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/1nf1n1t3l00p/subscriptions","organizations_url":"https://api.github.com/users/1nf1n1t3l00p/orgs","repos_url":"https://api.github.com/users/1nf1n1t3l00p/repos","events_url":"https://api.github.com/users/1nf1n1t3l00p/events{/privacy}","received_events_url":"https://api.github.com/users/1nf1n1t3l00p/received_events","type":"User","site_admin":false},"html_url":"https://github.com/1nf1n1t3l00p/priv_example","description":"Example + private repo","fork":false,"url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example","forks_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/forks","keys_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/keys{/key_id}","collaborators_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/teams","hooks_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/hooks","issue_events_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/issues/events{/number}","events_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/events","assignees_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/assignees{/user}","branches_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/branches{/branch}","tags_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/tags","blobs_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/git/refs{/sha}","trees_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/git/trees{/sha}","statuses_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/statuses/{sha}","languages_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/languages","stargazers_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/stargazers","contributors_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/contributors","subscribers_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/subscribers","subscription_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/subscription","commits_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/commits{/sha}","git_commits_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/git/commits{/sha}","comments_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/comments{/number}","issue_comment_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/issues/comments{/number}","contents_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/contents/{+path}","compare_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/compare/{base}...{head}","merges_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/merges","archive_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/downloads","issues_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/issues{/number}","pulls_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/pulls{/number}","milestones_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/milestones{/number}","notifications_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/labels{/name}","releases_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/releases{/id}","deployments_url":"https://api.github.com/repos/1nf1n1t3l00p/priv_example/deployments","created_at":"2018-11-26T00:59:07Z","updated_at":"2018-11-26T00:59:12Z","pushed_at":"2018-11-26T00:59:10Z","git_url":"git://github.com/1nf1n1t3l00p/priv_example.git","ssh_url":"git@github.com:1nf1n1t3l00p/priv_example.git","clone_url":"https://github.com/1nf1n1t3l00p/priv_example.git","svn_url":"https://github.com/1nf1n1t3l00p/priv_example","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master","permissions":{"admin":true,"push":true,"pull":true}},{"id":159090647,"node_id":"MDEwOlJlcG9zaXRvcnkxNTkwOTA2NDc=","name":"pub","full_name":"1nf1n1t3l00p/pub","private":false,"owner":{"login":"1nf1n1t3l00p","id":45343385,"node_id":"MDQ6VXNlcjQ1MzQzMzg1","avatar_url":"https://avatars1.githubusercontent.com/u/45343385?v=4","gravatar_id":"","url":"https://api.github.com/users/1nf1n1t3l00p","html_url":"https://github.com/1nf1n1t3l00p","followers_url":"https://api.github.com/users/1nf1n1t3l00p/followers","following_url":"https://api.github.com/users/1nf1n1t3l00p/following{/other_user}","gists_url":"https://api.github.com/users/1nf1n1t3l00p/gists{/gist_id}","starred_url":"https://api.github.com/users/1nf1n1t3l00p/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/1nf1n1t3l00p/subscriptions","organizations_url":"https://api.github.com/users/1nf1n1t3l00p/orgs","repos_url":"https://api.github.com/users/1nf1n1t3l00p/repos","events_url":"https://api.github.com/users/1nf1n1t3l00p/events{/privacy}","received_events_url":"https://api.github.com/users/1nf1n1t3l00p/received_events","type":"User","site_admin":false},"html_url":"https://github.com/1nf1n1t3l00p/pub","description":null,"fork":false,"url":"https://api.github.com/repos/1nf1n1t3l00p/pub","forks_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/forks","keys_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/keys{/key_id}","collaborators_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/teams","hooks_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/hooks","issue_events_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/issues/events{/number}","events_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/events","assignees_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/assignees{/user}","branches_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/branches{/branch}","tags_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/tags","blobs_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/git/refs{/sha}","trees_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/git/trees{/sha}","statuses_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/statuses/{sha}","languages_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/languages","stargazers_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/stargazers","contributors_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/contributors","subscribers_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/subscribers","subscription_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/subscription","commits_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/commits{/sha}","git_commits_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/git/commits{/sha}","comments_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/comments{/number}","issue_comment_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/issues/comments{/number}","contents_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/contents/{+path}","compare_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/compare/{base}...{head}","merges_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/merges","archive_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/downloads","issues_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/issues{/number}","pulls_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/pulls{/number}","milestones_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/milestones{/number}","notifications_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/labels{/name}","releases_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/releases{/id}","deployments_url":"https://api.github.com/repos/1nf1n1t3l00p/pub/deployments","created_at":"2018-11-26T01:00:55Z","updated_at":"2018-11-26T01:00:58Z","pushed_at":"2018-11-26T01:00:57Z","git_url":"git://github.com/1nf1n1t3l00p/pub.git","ssh_url":"git@github.com:1nf1n1t3l00p/pub.git","clone_url":"https://github.com/1nf1n1t3l00p/pub.git","svn_url":"https://github.com/1nf1n1t3l00p/pub","homepage":null,"size":0,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master","permissions":{"admin":true,"push":true,"pull":true}},{"id":159089634,"node_id":"MDEwOlJlcG9zaXRvcnkxNTkwODk2MzQ=","name":"pytest","full_name":"1nf1n1t3l00p/pytest","private":false,"owner":{"login":"1nf1n1t3l00p","id":45343385,"node_id":"MDQ6VXNlcjQ1MzQzMzg1","avatar_url":"https://avatars1.githubusercontent.com/u/45343385?v=4","gravatar_id":"","url":"https://api.github.com/users/1nf1n1t3l00p","html_url":"https://github.com/1nf1n1t3l00p","followers_url":"https://api.github.com/users/1nf1n1t3l00p/followers","following_url":"https://api.github.com/users/1nf1n1t3l00p/following{/other_user}","gists_url":"https://api.github.com/users/1nf1n1t3l00p/gists{/gist_id}","starred_url":"https://api.github.com/users/1nf1n1t3l00p/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/1nf1n1t3l00p/subscriptions","organizations_url":"https://api.github.com/users/1nf1n1t3l00p/orgs","repos_url":"https://api.github.com/users/1nf1n1t3l00p/repos","events_url":"https://api.github.com/users/1nf1n1t3l00p/events{/privacy}","received_events_url":"https://api.github.com/users/1nf1n1t3l00p/received_events","type":"User","site_admin":false},"html_url":"https://github.com/1nf1n1t3l00p/pytest","description":"The + pytest framework makes it easy to write small tests, yet scales to support complex + functional testing","fork":true,"url":"https://api.github.com/repos/1nf1n1t3l00p/pytest","forks_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/forks","keys_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/keys{/key_id}","collaborators_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/teams","hooks_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/hooks","issue_events_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/issues/events{/number}","events_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/events","assignees_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/assignees{/user}","branches_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/branches{/branch}","tags_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/tags","blobs_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/refs{/sha}","trees_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/trees{/sha}","statuses_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/statuses/{sha}","languages_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/languages","stargazers_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/stargazers","contributors_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/contributors","subscribers_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/subscribers","subscription_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/subscription","commits_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/commits{/sha}","git_commits_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/commits{/sha}","comments_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/comments{/number}","issue_comment_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/issues/comments{/number}","contents_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/contents/{+path}","compare_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/compare/{base}...{head}","merges_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/merges","archive_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/downloads","issues_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/issues{/number}","pulls_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/pulls{/number}","milestones_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/milestones{/number}","notifications_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/labels{/name}","releases_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/releases{/id}","deployments_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/deployments","created_at":"2018-11-26T00:46:41Z","updated_at":"2018-11-26T00:46:44Z","pushed_at":"2018-11-25T19:30:27Z","git_url":"git://github.com/1nf1n1t3l00p/pytest.git","ssh_url":"git@github.com:1nf1n1t3l00p/pytest.git","clone_url":"https://github.com/1nf1n1t3l00p/pytest.git","svn_url":"https://github.com/1nf1n1t3l00p/pytest","homepage":"https://pytest.org","size":15137,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master","permissions":{"admin":true,"push":true,"pull":true}},{"id":164948070,"node_id":"MDEwOlJlcG9zaXRvcnkxNjQ5NDgwNzA=","name":"spack","full_name":"1nf1n1t3l00p/spack","private":false,"owner":{"login":"1nf1n1t3l00p","id":45343385,"node_id":"MDQ6VXNlcjQ1MzQzMzg1","avatar_url":"https://avatars1.githubusercontent.com/u/45343385?v=4","gravatar_id":"","url":"https://api.github.com/users/1nf1n1t3l00p","html_url":"https://github.com/1nf1n1t3l00p","followers_url":"https://api.github.com/users/1nf1n1t3l00p/followers","following_url":"https://api.github.com/users/1nf1n1t3l00p/following{/other_user}","gists_url":"https://api.github.com/users/1nf1n1t3l00p/gists{/gist_id}","starred_url":"https://api.github.com/users/1nf1n1t3l00p/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/1nf1n1t3l00p/subscriptions","organizations_url":"https://api.github.com/users/1nf1n1t3l00p/orgs","repos_url":"https://api.github.com/users/1nf1n1t3l00p/repos","events_url":"https://api.github.com/users/1nf1n1t3l00p/events{/privacy}","received_events_url":"https://api.github.com/users/1nf1n1t3l00p/received_events","type":"User","site_admin":false},"html_url":"https://github.com/1nf1n1t3l00p/spack","description":"A + flexible package manager that supports multiple versions, configurations, platforms, + and compilers.","fork":true,"url":"https://api.github.com/repos/1nf1n1t3l00p/spack","forks_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/forks","keys_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/keys{/key_id}","collaborators_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/teams","hooks_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/hooks","issue_events_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/issues/events{/number}","events_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/events","assignees_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/assignees{/user}","branches_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/branches{/branch}","tags_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/tags","blobs_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/refs{/sha}","trees_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/trees{/sha}","statuses_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/statuses/{sha}","languages_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/languages","stargazers_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/stargazers","contributors_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/contributors","subscribers_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/subscribers","subscription_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/subscription","commits_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/commits{/sha}","git_commits_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/commits{/sha}","comments_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/comments{/number}","issue_comment_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/issues/comments{/number}","contents_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/contents/{+path}","compare_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/compare/{base}...{head}","merges_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/merges","archive_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/downloads","issues_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/issues{/number}","pulls_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/pulls{/number}","milestones_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/milestones{/number}","notifications_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/labels{/name}","releases_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/releases{/id}","deployments_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/deployments","created_at":"2019-01-09T22:27:51Z","updated_at":"2019-01-09T22:28:01Z","pushed_at":"2019-01-09T22:19:36Z","git_url":"git://github.com/1nf1n1t3l00p/spack.git","ssh_url":"git@github.com:1nf1n1t3l00p/spack.git","clone_url":"https://github.com/1nf1n1t3l00p/spack.git","svn_url":"https://github.com/1nf1n1t3l00p/spack","homepage":"https://spack.io","size":50539,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":0,"open_issues":0,"watchers":0,"default_branch":"develop","permissions":{"admin":true,"push":true,"pull":true}},{"id":100391333,"node_id":"MDEwOlJlcG9zaXRvcnkxMDAzOTEzMzM=","name":"superfluid-dashboard","full_name":"TJBIII/superfluid-dashboard","private":true,"owner":{"login":"TJBIII","id":13630281,"node_id":"MDQ6VXNlcjEzNjMwMjgx","avatar_url":"https://avatars0.githubusercontent.com/u/13630281?v=4","gravatar_id":"","url":"https://api.github.com/users/TJBIII","html_url":"https://github.com/TJBIII","followers_url":"https://api.github.com/users/TJBIII/followers","following_url":"https://api.github.com/users/TJBIII/following{/other_user}","gists_url":"https://api.github.com/users/TJBIII/gists{/gist_id}","starred_url":"https://api.github.com/users/TJBIII/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/TJBIII/subscriptions","organizations_url":"https://api.github.com/users/TJBIII/orgs","repos_url":"https://api.github.com/users/TJBIII/repos","events_url":"https://api.github.com/users/TJBIII/events{/privacy}","received_events_url":"https://api.github.com/users/TJBIII/received_events","type":"User","site_admin":false},"html_url":"https://github.com/TJBIII/superfluid-dashboard","description":null,"fork":false,"url":"https://api.github.com/repos/TJBIII/superfluid-dashboard","forks_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/forks","keys_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/keys{/key_id}","collaborators_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/teams","hooks_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/hooks","issue_events_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/issues/events{/number}","events_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/events","assignees_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/assignees{/user}","branches_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/branches{/branch}","tags_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/tags","blobs_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/git/refs{/sha}","trees_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/git/trees{/sha}","statuses_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/statuses/{sha}","languages_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/languages","stargazers_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/stargazers","contributors_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/contributors","subscribers_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/subscribers","subscription_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/subscription","commits_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/commits{/sha}","git_commits_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/git/commits{/sha}","comments_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/comments{/number}","issue_comment_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/issues/comments{/number}","contents_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/contents/{+path}","compare_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/compare/{base}...{head}","merges_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/merges","archive_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/downloads","issues_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/issues{/number}","pulls_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/pulls{/number}","milestones_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/milestones{/number}","notifications_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/labels{/name}","releases_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/releases{/id}","deployments_url":"https://api.github.com/repos/TJBIII/superfluid-dashboard/deployments","created_at":"2017-08-15T15:25:30Z","updated_at":"2017-08-15T20:58:45Z","pushed_at":"2017-08-16T00:23:38Z","git_url":"git://github.com/TJBIII/superfluid-dashboard.git","ssh_url":"git@github.com:TJBIII/superfluid-dashboard.git","clone_url":"https://github.com/TJBIII/superfluid-dashboard.git","svn_url":"https://github.com/TJBIII/superfluid-dashboard","homepage":null,"size":16,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master","permissions":{"admin":false,"push":true,"pull":true}}]' + headers: + Access-Control-Allow-Origin: ['*'] + Access-Control-Expose-Headers: ['ETag, Link, Location, Retry-After, X-GitHub-OTP, + X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type'] + Cache-Control: ['private, max-age=60, s-maxage=60'] + Connection: [close] + Content-Security-Policy: [default-src 'none'] + Content-Type: [application/json; charset=utf-8] + Date: ['Thu, 07 Nov 2019 21:22:11 GMT'] + Etag: [W/"2cd75938e8a1d346cfdf8aa060783e5e"] + Referrer-Policy: ['origin-when-cross-origin, strict-origin-when-cross-origin'] + Server: [GitHub.com] + Status: [200 OK] + Strict-Transport-Security: [max-age=31536000; includeSubdomains; preload] + Transfer-Encoding: [chunked] + Vary: ['Accept, Authorization, Cookie, X-GitHub-OTP'] + X-Accepted-Oauth-Scopes: [''] + X-Consumed-Content-Encoding: [gzip] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [deny] + X-Github-Media-Type: [github.v3] + X-Github-Request-Id: ['E370:1D2F:2DC418:719FA0:5DC48B03'] + X-Oauth-Scopes: ['repo, user'] + X-Ratelimit-Limit: ['5000'] + X-Ratelimit-Remaining: ['4987'] + X-Ratelimit-Reset: ['1573165331'] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} + status_code: 200 + url: https://api.github.com/user/repos?per_page=100&page=1 +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://api.github.com/repos/1nf1n1t3l00p/neomake + response: + content: '{"id":168373402,"node_id":"MDEwOlJlcG9zaXRvcnkxNjgzNzM0MDI=","name":"neomake","full_name":"1nf1n1t3l00p/neomake","private":false,"owner":{"login":"1nf1n1t3l00p","id":45343385,"node_id":"MDQ6VXNlcjQ1MzQzMzg1","avatar_url":"https://avatars1.githubusercontent.com/u/45343385?v=4","gravatar_id":"","url":"https://api.github.com/users/1nf1n1t3l00p","html_url":"https://github.com/1nf1n1t3l00p","followers_url":"https://api.github.com/users/1nf1n1t3l00p/followers","following_url":"https://api.github.com/users/1nf1n1t3l00p/following{/other_user}","gists_url":"https://api.github.com/users/1nf1n1t3l00p/gists{/gist_id}","starred_url":"https://api.github.com/users/1nf1n1t3l00p/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/1nf1n1t3l00p/subscriptions","organizations_url":"https://api.github.com/users/1nf1n1t3l00p/orgs","repos_url":"https://api.github.com/users/1nf1n1t3l00p/repos","events_url":"https://api.github.com/users/1nf1n1t3l00p/events{/privacy}","received_events_url":"https://api.github.com/users/1nf1n1t3l00p/received_events","type":"User","site_admin":false},"html_url":"https://github.com/1nf1n1t3l00p/neomake","description":"Asynchronous + linting and make framework for Neovim/Vim","fork":true,"url":"https://api.github.com/repos/1nf1n1t3l00p/neomake","forks_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/forks","keys_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/keys{/key_id}","collaborators_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/teams","hooks_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/hooks","issue_events_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/issues/events{/number}","events_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/events","assignees_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/assignees{/user}","branches_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/branches{/branch}","tags_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/tags","blobs_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/git/refs{/sha}","trees_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/git/trees{/sha}","statuses_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/statuses/{sha}","languages_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/languages","stargazers_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/stargazers","contributors_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/contributors","subscribers_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/subscribers","subscription_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/subscription","commits_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/commits{/sha}","git_commits_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/git/commits{/sha}","comments_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/comments{/number}","issue_comment_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/issues/comments{/number}","contents_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/contents/{+path}","compare_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/compare/{base}...{head}","merges_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/merges","archive_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/downloads","issues_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/issues{/number}","pulls_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/pulls{/number}","milestones_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/milestones{/number}","notifications_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/labels{/name}","releases_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/releases{/id}","deployments_url":"https://api.github.com/repos/1nf1n1t3l00p/neomake/deployments","created_at":"2019-01-30T16:13:56Z","updated_at":"2019-01-30T16:13:59Z","pushed_at":"2019-01-27T15:56:53Z","git_url":"git://github.com/1nf1n1t3l00p/neomake.git","ssh_url":"git@github.com:1nf1n1t3l00p/neomake.git","clone_url":"https://github.com/1nf1n1t3l00p/neomake.git","svn_url":"https://github.com/1nf1n1t3l00p/neomake","homepage":"","size":4862,"stargazers_count":0,"watchers_count":0,"language":"Vim + script","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master","permissions":{"admin":true,"push":true,"pull":true},"allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"parent":{"id":26522697,"node_id":"MDEwOlJlcG9zaXRvcnkyNjUyMjY5Nw==","name":"neomake","full_name":"neomake/neomake","private":false,"owner":{"login":"neomake","id":19256388,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE5MjU2Mzg4","avatar_url":"https://avatars2.githubusercontent.com/u/19256388?v=4","gravatar_id":"","url":"https://api.github.com/users/neomake","html_url":"https://github.com/neomake","followers_url":"https://api.github.com/users/neomake/followers","following_url":"https://api.github.com/users/neomake/following{/other_user}","gists_url":"https://api.github.com/users/neomake/gists{/gist_id}","starred_url":"https://api.github.com/users/neomake/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/neomake/subscriptions","organizations_url":"https://api.github.com/users/neomake/orgs","repos_url":"https://api.github.com/users/neomake/repos","events_url":"https://api.github.com/users/neomake/events{/privacy}","received_events_url":"https://api.github.com/users/neomake/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/neomake/neomake","description":"Asynchronous + linting and make framework for Neovim/Vim","fork":false,"url":"https://api.github.com/repos/neomake/neomake","forks_url":"https://api.github.com/repos/neomake/neomake/forks","keys_url":"https://api.github.com/repos/neomake/neomake/keys{/key_id}","collaborators_url":"https://api.github.com/repos/neomake/neomake/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/neomake/neomake/teams","hooks_url":"https://api.github.com/repos/neomake/neomake/hooks","issue_events_url":"https://api.github.com/repos/neomake/neomake/issues/events{/number}","events_url":"https://api.github.com/repos/neomake/neomake/events","assignees_url":"https://api.github.com/repos/neomake/neomake/assignees{/user}","branches_url":"https://api.github.com/repos/neomake/neomake/branches{/branch}","tags_url":"https://api.github.com/repos/neomake/neomake/tags","blobs_url":"https://api.github.com/repos/neomake/neomake/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/neomake/neomake/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/neomake/neomake/git/refs{/sha}","trees_url":"https://api.github.com/repos/neomake/neomake/git/trees{/sha}","statuses_url":"https://api.github.com/repos/neomake/neomake/statuses/{sha}","languages_url":"https://api.github.com/repos/neomake/neomake/languages","stargazers_url":"https://api.github.com/repos/neomake/neomake/stargazers","contributors_url":"https://api.github.com/repos/neomake/neomake/contributors","subscribers_url":"https://api.github.com/repos/neomake/neomake/subscribers","subscription_url":"https://api.github.com/repos/neomake/neomake/subscription","commits_url":"https://api.github.com/repos/neomake/neomake/commits{/sha}","git_commits_url":"https://api.github.com/repos/neomake/neomake/git/commits{/sha}","comments_url":"https://api.github.com/repos/neomake/neomake/comments{/number}","issue_comment_url":"https://api.github.com/repos/neomake/neomake/issues/comments{/number}","contents_url":"https://api.github.com/repos/neomake/neomake/contents/{+path}","compare_url":"https://api.github.com/repos/neomake/neomake/compare/{base}...{head}","merges_url":"https://api.github.com/repos/neomake/neomake/merges","archive_url":"https://api.github.com/repos/neomake/neomake/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/neomake/neomake/downloads","issues_url":"https://api.github.com/repos/neomake/neomake/issues{/number}","pulls_url":"https://api.github.com/repos/neomake/neomake/pulls{/number}","milestones_url":"https://api.github.com/repos/neomake/neomake/milestones{/number}","notifications_url":"https://api.github.com/repos/neomake/neomake/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/neomake/neomake/labels{/name}","releases_url":"https://api.github.com/repos/neomake/neomake/releases{/id}","deployments_url":"https://api.github.com/repos/neomake/neomake/deployments","created_at":"2014-11-12T06:34:12Z","updated_at":"2019-11-07T10:12:16Z","pushed_at":"2019-11-07T19:47:04Z","git_url":"git://github.com/neomake/neomake.git","ssh_url":"git@github.com:neomake/neomake.git","clone_url":"https://github.com/neomake/neomake.git","svn_url":"https://github.com/neomake/neomake","homepage":"","size":5294,"stargazers_count":2235,"watchers_count":2235,"language":"Vim + script","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":355,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":175,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":355,"open_issues":175,"watchers":2235,"default_branch":"master"},"source":{"id":26522697,"node_id":"MDEwOlJlcG9zaXRvcnkyNjUyMjY5Nw==","name":"neomake","full_name":"neomake/neomake","private":false,"owner":{"login":"neomake","id":19256388,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE5MjU2Mzg4","avatar_url":"https://avatars2.githubusercontent.com/u/19256388?v=4","gravatar_id":"","url":"https://api.github.com/users/neomake","html_url":"https://github.com/neomake","followers_url":"https://api.github.com/users/neomake/followers","following_url":"https://api.github.com/users/neomake/following{/other_user}","gists_url":"https://api.github.com/users/neomake/gists{/gist_id}","starred_url":"https://api.github.com/users/neomake/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/neomake/subscriptions","organizations_url":"https://api.github.com/users/neomake/orgs","repos_url":"https://api.github.com/users/neomake/repos","events_url":"https://api.github.com/users/neomake/events{/privacy}","received_events_url":"https://api.github.com/users/neomake/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/neomake/neomake","description":"Asynchronous + linting and make framework for Neovim/Vim","fork":false,"url":"https://api.github.com/repos/neomake/neomake","forks_url":"https://api.github.com/repos/neomake/neomake/forks","keys_url":"https://api.github.com/repos/neomake/neomake/keys{/key_id}","collaborators_url":"https://api.github.com/repos/neomake/neomake/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/neomake/neomake/teams","hooks_url":"https://api.github.com/repos/neomake/neomake/hooks","issue_events_url":"https://api.github.com/repos/neomake/neomake/issues/events{/number}","events_url":"https://api.github.com/repos/neomake/neomake/events","assignees_url":"https://api.github.com/repos/neomake/neomake/assignees{/user}","branches_url":"https://api.github.com/repos/neomake/neomake/branches{/branch}","tags_url":"https://api.github.com/repos/neomake/neomake/tags","blobs_url":"https://api.github.com/repos/neomake/neomake/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/neomake/neomake/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/neomake/neomake/git/refs{/sha}","trees_url":"https://api.github.com/repos/neomake/neomake/git/trees{/sha}","statuses_url":"https://api.github.com/repos/neomake/neomake/statuses/{sha}","languages_url":"https://api.github.com/repos/neomake/neomake/languages","stargazers_url":"https://api.github.com/repos/neomake/neomake/stargazers","contributors_url":"https://api.github.com/repos/neomake/neomake/contributors","subscribers_url":"https://api.github.com/repos/neomake/neomake/subscribers","subscription_url":"https://api.github.com/repos/neomake/neomake/subscription","commits_url":"https://api.github.com/repos/neomake/neomake/commits{/sha}","git_commits_url":"https://api.github.com/repos/neomake/neomake/git/commits{/sha}","comments_url":"https://api.github.com/repos/neomake/neomake/comments{/number}","issue_comment_url":"https://api.github.com/repos/neomake/neomake/issues/comments{/number}","contents_url":"https://api.github.com/repos/neomake/neomake/contents/{+path}","compare_url":"https://api.github.com/repos/neomake/neomake/compare/{base}...{head}","merges_url":"https://api.github.com/repos/neomake/neomake/merges","archive_url":"https://api.github.com/repos/neomake/neomake/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/neomake/neomake/downloads","issues_url":"https://api.github.com/repos/neomake/neomake/issues{/number}","pulls_url":"https://api.github.com/repos/neomake/neomake/pulls{/number}","milestones_url":"https://api.github.com/repos/neomake/neomake/milestones{/number}","notifications_url":"https://api.github.com/repos/neomake/neomake/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/neomake/neomake/labels{/name}","releases_url":"https://api.github.com/repos/neomake/neomake/releases{/id}","deployments_url":"https://api.github.com/repos/neomake/neomake/deployments","created_at":"2014-11-12T06:34:12Z","updated_at":"2019-11-07T10:12:16Z","pushed_at":"2019-11-07T19:47:04Z","git_url":"git://github.com/neomake/neomake.git","ssh_url":"git@github.com:neomake/neomake.git","clone_url":"https://github.com/neomake/neomake.git","svn_url":"https://github.com/neomake/neomake","homepage":"","size":5294,"stargazers_count":2235,"watchers_count":2235,"language":"Vim + script","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":355,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":175,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":355,"open_issues":175,"watchers":2235,"default_branch":"master"},"network_count":355,"subscribers_count":0}' + headers: + Access-Control-Allow-Origin: ['*'] + Access-Control-Expose-Headers: ['ETag, Link, Location, Retry-After, X-GitHub-OTP, + X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type'] + Cache-Control: ['private, max-age=60, s-maxage=60'] + Connection: [close] + Content-Security-Policy: [default-src 'none'] + Content-Type: [application/json; charset=utf-8] + Date: ['Thu, 07 Nov 2019 21:22:12 GMT'] + Etag: [W/"89a732fa4b2704063a7d943b38ab941d"] + Last-Modified: ['Wed, 30 Jan 2019 16:13:59 GMT'] + Referrer-Policy: ['origin-when-cross-origin, strict-origin-when-cross-origin'] + Server: [GitHub.com] + Status: [200 OK] + Strict-Transport-Security: [max-age=31536000; includeSubdomains; preload] + Transfer-Encoding: [chunked] + Vary: ['Accept, Authorization, Cookie, X-GitHub-OTP'] + X-Accepted-Oauth-Scopes: [repo] + X-Consumed-Content-Encoding: [gzip] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [deny] + X-Github-Media-Type: [github.v3] + X-Github-Request-Id: ['E372:3FBA:57BBF9:B74978:5DC48B03'] + X-Oauth-Scopes: ['repo, user'] + X-Ratelimit-Limit: ['5000'] + X-Ratelimit-Remaining: ['4986'] + X-Ratelimit-Reset: ['1573165331'] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} + status_code: 200 + url: https://api.github.com/repos/1nf1n1t3l00p/neomake +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://api.github.com/repos/1nf1n1t3l00p/pytest + response: + content: '{"id":159089634,"node_id":"MDEwOlJlcG9zaXRvcnkxNTkwODk2MzQ=","name":"pytest","full_name":"1nf1n1t3l00p/pytest","private":false,"owner":{"login":"1nf1n1t3l00p","id":45343385,"node_id":"MDQ6VXNlcjQ1MzQzMzg1","avatar_url":"https://avatars1.githubusercontent.com/u/45343385?v=4","gravatar_id":"","url":"https://api.github.com/users/1nf1n1t3l00p","html_url":"https://github.com/1nf1n1t3l00p","followers_url":"https://api.github.com/users/1nf1n1t3l00p/followers","following_url":"https://api.github.com/users/1nf1n1t3l00p/following{/other_user}","gists_url":"https://api.github.com/users/1nf1n1t3l00p/gists{/gist_id}","starred_url":"https://api.github.com/users/1nf1n1t3l00p/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/1nf1n1t3l00p/subscriptions","organizations_url":"https://api.github.com/users/1nf1n1t3l00p/orgs","repos_url":"https://api.github.com/users/1nf1n1t3l00p/repos","events_url":"https://api.github.com/users/1nf1n1t3l00p/events{/privacy}","received_events_url":"https://api.github.com/users/1nf1n1t3l00p/received_events","type":"User","site_admin":false},"html_url":"https://github.com/1nf1n1t3l00p/pytest","description":"The + pytest framework makes it easy to write small tests, yet scales to support complex + functional testing","fork":true,"url":"https://api.github.com/repos/1nf1n1t3l00p/pytest","forks_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/forks","keys_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/keys{/key_id}","collaborators_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/teams","hooks_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/hooks","issue_events_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/issues/events{/number}","events_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/events","assignees_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/assignees{/user}","branches_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/branches{/branch}","tags_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/tags","blobs_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/refs{/sha}","trees_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/trees{/sha}","statuses_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/statuses/{sha}","languages_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/languages","stargazers_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/stargazers","contributors_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/contributors","subscribers_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/subscribers","subscription_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/subscription","commits_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/commits{/sha}","git_commits_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/git/commits{/sha}","comments_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/comments{/number}","issue_comment_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/issues/comments{/number}","contents_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/contents/{+path}","compare_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/compare/{base}...{head}","merges_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/merges","archive_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/downloads","issues_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/issues{/number}","pulls_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/pulls{/number}","milestones_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/milestones{/number}","notifications_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/labels{/name}","releases_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/releases{/id}","deployments_url":"https://api.github.com/repos/1nf1n1t3l00p/pytest/deployments","created_at":"2018-11-26T00:46:41Z","updated_at":"2018-11-26T00:46:44Z","pushed_at":"2018-11-25T19:30:27Z","git_url":"git://github.com/1nf1n1t3l00p/pytest.git","ssh_url":"git@github.com:1nf1n1t3l00p/pytest.git","clone_url":"https://github.com/1nf1n1t3l00p/pytest.git","svn_url":"https://github.com/1nf1n1t3l00p/pytest","homepage":"https://pytest.org","size":15137,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master","permissions":{"admin":true,"push":true,"pull":true},"allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"parent":{"id":37489525,"node_id":"MDEwOlJlcG9zaXRvcnkzNzQ4OTUyNQ==","name":"pytest","full_name":"pytest-dev/pytest","private":false,"owner":{"login":"pytest-dev","id":8897583,"node_id":"MDEyOk9yZ2FuaXphdGlvbjg4OTc1ODM=","avatar_url":"https://avatars2.githubusercontent.com/u/8897583?v=4","gravatar_id":"","url":"https://api.github.com/users/pytest-dev","html_url":"https://github.com/pytest-dev","followers_url":"https://api.github.com/users/pytest-dev/followers","following_url":"https://api.github.com/users/pytest-dev/following{/other_user}","gists_url":"https://api.github.com/users/pytest-dev/gists{/gist_id}","starred_url":"https://api.github.com/users/pytest-dev/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pytest-dev/subscriptions","organizations_url":"https://api.github.com/users/pytest-dev/orgs","repos_url":"https://api.github.com/users/pytest-dev/repos","events_url":"https://api.github.com/users/pytest-dev/events{/privacy}","received_events_url":"https://api.github.com/users/pytest-dev/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/pytest-dev/pytest","description":"The + pytest framework makes it easy to write small tests, yet scales to support complex + functional testing","fork":false,"url":"https://api.github.com/repos/pytest-dev/pytest","forks_url":"https://api.github.com/repos/pytest-dev/pytest/forks","keys_url":"https://api.github.com/repos/pytest-dev/pytest/keys{/key_id}","collaborators_url":"https://api.github.com/repos/pytest-dev/pytest/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/pytest-dev/pytest/teams","hooks_url":"https://api.github.com/repos/pytest-dev/pytest/hooks","issue_events_url":"https://api.github.com/repos/pytest-dev/pytest/issues/events{/number}","events_url":"https://api.github.com/repos/pytest-dev/pytest/events","assignees_url":"https://api.github.com/repos/pytest-dev/pytest/assignees{/user}","branches_url":"https://api.github.com/repos/pytest-dev/pytest/branches{/branch}","tags_url":"https://api.github.com/repos/pytest-dev/pytest/tags","blobs_url":"https://api.github.com/repos/pytest-dev/pytest/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/pytest-dev/pytest/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/pytest-dev/pytest/git/refs{/sha}","trees_url":"https://api.github.com/repos/pytest-dev/pytest/git/trees{/sha}","statuses_url":"https://api.github.com/repos/pytest-dev/pytest/statuses/{sha}","languages_url":"https://api.github.com/repos/pytest-dev/pytest/languages","stargazers_url":"https://api.github.com/repos/pytest-dev/pytest/stargazers","contributors_url":"https://api.github.com/repos/pytest-dev/pytest/contributors","subscribers_url":"https://api.github.com/repos/pytest-dev/pytest/subscribers","subscription_url":"https://api.github.com/repos/pytest-dev/pytest/subscription","commits_url":"https://api.github.com/repos/pytest-dev/pytest/commits{/sha}","git_commits_url":"https://api.github.com/repos/pytest-dev/pytest/git/commits{/sha}","comments_url":"https://api.github.com/repos/pytest-dev/pytest/comments{/number}","issue_comment_url":"https://api.github.com/repos/pytest-dev/pytest/issues/comments{/number}","contents_url":"https://api.github.com/repos/pytest-dev/pytest/contents/{+path}","compare_url":"https://api.github.com/repos/pytest-dev/pytest/compare/{base}...{head}","merges_url":"https://api.github.com/repos/pytest-dev/pytest/merges","archive_url":"https://api.github.com/repos/pytest-dev/pytest/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/pytest-dev/pytest/downloads","issues_url":"https://api.github.com/repos/pytest-dev/pytest/issues{/number}","pulls_url":"https://api.github.com/repos/pytest-dev/pytest/pulls{/number}","milestones_url":"https://api.github.com/repos/pytest-dev/pytest/milestones{/number}","notifications_url":"https://api.github.com/repos/pytest-dev/pytest/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/pytest-dev/pytest/labels{/name}","releases_url":"https://api.github.com/repos/pytest-dev/pytest/releases{/id}","deployments_url":"https://api.github.com/repos/pytest-dev/pytest/deployments","created_at":"2015-06-15T20:28:27Z","updated_at":"2019-11-07T17:41:47Z","pushed_at":"2019-11-07T21:13:28Z","git_url":"git://github.com/pytest-dev/pytest.git","ssh_url":"git@github.com:pytest-dev/pytest.git","clone_url":"https://github.com/pytest-dev/pytest.git","svn_url":"https://github.com/pytest-dev/pytest","homepage":"https://pytest.org","size":19189,"stargazers_count":5050,"watchers_count":5050,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1205,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":582,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":1205,"open_issues":582,"watchers":5050,"default_branch":"master"},"source":{"id":37489525,"node_id":"MDEwOlJlcG9zaXRvcnkzNzQ4OTUyNQ==","name":"pytest","full_name":"pytest-dev/pytest","private":false,"owner":{"login":"pytest-dev","id":8897583,"node_id":"MDEyOk9yZ2FuaXphdGlvbjg4OTc1ODM=","avatar_url":"https://avatars2.githubusercontent.com/u/8897583?v=4","gravatar_id":"","url":"https://api.github.com/users/pytest-dev","html_url":"https://github.com/pytest-dev","followers_url":"https://api.github.com/users/pytest-dev/followers","following_url":"https://api.github.com/users/pytest-dev/following{/other_user}","gists_url":"https://api.github.com/users/pytest-dev/gists{/gist_id}","starred_url":"https://api.github.com/users/pytest-dev/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pytest-dev/subscriptions","organizations_url":"https://api.github.com/users/pytest-dev/orgs","repos_url":"https://api.github.com/users/pytest-dev/repos","events_url":"https://api.github.com/users/pytest-dev/events{/privacy}","received_events_url":"https://api.github.com/users/pytest-dev/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/pytest-dev/pytest","description":"The + pytest framework makes it easy to write small tests, yet scales to support complex + functional testing","fork":false,"url":"https://api.github.com/repos/pytest-dev/pytest","forks_url":"https://api.github.com/repos/pytest-dev/pytest/forks","keys_url":"https://api.github.com/repos/pytest-dev/pytest/keys{/key_id}","collaborators_url":"https://api.github.com/repos/pytest-dev/pytest/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/pytest-dev/pytest/teams","hooks_url":"https://api.github.com/repos/pytest-dev/pytest/hooks","issue_events_url":"https://api.github.com/repos/pytest-dev/pytest/issues/events{/number}","events_url":"https://api.github.com/repos/pytest-dev/pytest/events","assignees_url":"https://api.github.com/repos/pytest-dev/pytest/assignees{/user}","branches_url":"https://api.github.com/repos/pytest-dev/pytest/branches{/branch}","tags_url":"https://api.github.com/repos/pytest-dev/pytest/tags","blobs_url":"https://api.github.com/repos/pytest-dev/pytest/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/pytest-dev/pytest/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/pytest-dev/pytest/git/refs{/sha}","trees_url":"https://api.github.com/repos/pytest-dev/pytest/git/trees{/sha}","statuses_url":"https://api.github.com/repos/pytest-dev/pytest/statuses/{sha}","languages_url":"https://api.github.com/repos/pytest-dev/pytest/languages","stargazers_url":"https://api.github.com/repos/pytest-dev/pytest/stargazers","contributors_url":"https://api.github.com/repos/pytest-dev/pytest/contributors","subscribers_url":"https://api.github.com/repos/pytest-dev/pytest/subscribers","subscription_url":"https://api.github.com/repos/pytest-dev/pytest/subscription","commits_url":"https://api.github.com/repos/pytest-dev/pytest/commits{/sha}","git_commits_url":"https://api.github.com/repos/pytest-dev/pytest/git/commits{/sha}","comments_url":"https://api.github.com/repos/pytest-dev/pytest/comments{/number}","issue_comment_url":"https://api.github.com/repos/pytest-dev/pytest/issues/comments{/number}","contents_url":"https://api.github.com/repos/pytest-dev/pytest/contents/{+path}","compare_url":"https://api.github.com/repos/pytest-dev/pytest/compare/{base}...{head}","merges_url":"https://api.github.com/repos/pytest-dev/pytest/merges","archive_url":"https://api.github.com/repos/pytest-dev/pytest/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/pytest-dev/pytest/downloads","issues_url":"https://api.github.com/repos/pytest-dev/pytest/issues{/number}","pulls_url":"https://api.github.com/repos/pytest-dev/pytest/pulls{/number}","milestones_url":"https://api.github.com/repos/pytest-dev/pytest/milestones{/number}","notifications_url":"https://api.github.com/repos/pytest-dev/pytest/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/pytest-dev/pytest/labels{/name}","releases_url":"https://api.github.com/repos/pytest-dev/pytest/releases{/id}","deployments_url":"https://api.github.com/repos/pytest-dev/pytest/deployments","created_at":"2015-06-15T20:28:27Z","updated_at":"2019-11-07T17:41:47Z","pushed_at":"2019-11-07T21:13:28Z","git_url":"git://github.com/pytest-dev/pytest.git","ssh_url":"git@github.com:pytest-dev/pytest.git","clone_url":"https://github.com/pytest-dev/pytest.git","svn_url":"https://github.com/pytest-dev/pytest","homepage":"https://pytest.org","size":19189,"stargazers_count":5050,"watchers_count":5050,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1205,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":582,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":1205,"open_issues":582,"watchers":5050,"default_branch":"master"},"network_count":1205,"subscribers_count":0}' + headers: + Access-Control-Allow-Origin: ['*'] + Access-Control-Expose-Headers: ['ETag, Link, Location, Retry-After, X-GitHub-OTP, + X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type'] + Cache-Control: ['private, max-age=60, s-maxage=60'] + Connection: [close] + Content-Security-Policy: [default-src 'none'] + Content-Type: [application/json; charset=utf-8] + Date: ['Thu, 07 Nov 2019 21:22:12 GMT'] + Etag: [W/"d0ebe5d798bb9318f7edcb2c99fba06e"] + Last-Modified: ['Mon, 26 Nov 2018 00:46:44 GMT'] + Referrer-Policy: ['origin-when-cross-origin, strict-origin-when-cross-origin'] + Server: [GitHub.com] + Status: [200 OK] + Strict-Transport-Security: [max-age=31536000; includeSubdomains; preload] + Transfer-Encoding: [chunked] + Vary: ['Accept, Authorization, Cookie, X-GitHub-OTP'] + X-Accepted-Oauth-Scopes: [repo] + X-Consumed-Content-Encoding: [gzip] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [deny] + X-Github-Media-Type: [github.v3] + X-Github-Request-Id: ['E374:1A80:515F7C:AD552C:5DC48B04'] + X-Oauth-Scopes: ['repo, user'] + X-Ratelimit-Limit: ['5000'] + X-Ratelimit-Remaining: ['4985'] + X-Ratelimit-Reset: ['1573165331'] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} + status_code: 200 + url: https://api.github.com/repos/1nf1n1t3l00p/pytest +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://api.github.com/repos/1nf1n1t3l00p/spack + response: + content: '{"id":164948070,"node_id":"MDEwOlJlcG9zaXRvcnkxNjQ5NDgwNzA=","name":"spack","full_name":"1nf1n1t3l00p/spack","private":false,"owner":{"login":"1nf1n1t3l00p","id":45343385,"node_id":"MDQ6VXNlcjQ1MzQzMzg1","avatar_url":"https://avatars1.githubusercontent.com/u/45343385?v=4","gravatar_id":"","url":"https://api.github.com/users/1nf1n1t3l00p","html_url":"https://github.com/1nf1n1t3l00p","followers_url":"https://api.github.com/users/1nf1n1t3l00p/followers","following_url":"https://api.github.com/users/1nf1n1t3l00p/following{/other_user}","gists_url":"https://api.github.com/users/1nf1n1t3l00p/gists{/gist_id}","starred_url":"https://api.github.com/users/1nf1n1t3l00p/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/1nf1n1t3l00p/subscriptions","organizations_url":"https://api.github.com/users/1nf1n1t3l00p/orgs","repos_url":"https://api.github.com/users/1nf1n1t3l00p/repos","events_url":"https://api.github.com/users/1nf1n1t3l00p/events{/privacy}","received_events_url":"https://api.github.com/users/1nf1n1t3l00p/received_events","type":"User","site_admin":false},"html_url":"https://github.com/1nf1n1t3l00p/spack","description":"A + flexible package manager that supports multiple versions, configurations, platforms, + and compilers.","fork":true,"url":"https://api.github.com/repos/1nf1n1t3l00p/spack","forks_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/forks","keys_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/keys{/key_id}","collaborators_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/teams","hooks_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/hooks","issue_events_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/issues/events{/number}","events_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/events","assignees_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/assignees{/user}","branches_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/branches{/branch}","tags_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/tags","blobs_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/refs{/sha}","trees_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/trees{/sha}","statuses_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/statuses/{sha}","languages_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/languages","stargazers_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/stargazers","contributors_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/contributors","subscribers_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/subscribers","subscription_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/subscription","commits_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/commits{/sha}","git_commits_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/git/commits{/sha}","comments_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/comments{/number}","issue_comment_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/issues/comments{/number}","contents_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/contents/{+path}","compare_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/compare/{base}...{head}","merges_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/merges","archive_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/downloads","issues_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/issues{/number}","pulls_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/pulls{/number}","milestones_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/milestones{/number}","notifications_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/labels{/name}","releases_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/releases{/id}","deployments_url":"https://api.github.com/repos/1nf1n1t3l00p/spack/deployments","created_at":"2019-01-09T22:27:51Z","updated_at":"2019-01-09T22:28:01Z","pushed_at":"2019-01-09T22:19:36Z","git_url":"git://github.com/1nf1n1t3l00p/spack.git","ssh_url":"git@github.com:1nf1n1t3l00p/spack.git","clone_url":"https://github.com/1nf1n1t3l00p/spack.git","svn_url":"https://github.com/1nf1n1t3l00p/spack","homepage":"https://spack.io","size":50539,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":0,"open_issues":0,"watchers":0,"default_branch":"develop","permissions":{"admin":true,"push":true,"pull":true},"allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"parent":{"id":15730865,"node_id":"MDEwOlJlcG9zaXRvcnkxNTczMDg2NQ==","name":"spack","full_name":"spack/spack","private":false,"owner":{"login":"spack","id":25539161,"node_id":"MDEyOk9yZ2FuaXphdGlvbjI1NTM5MTYx","avatar_url":"https://avatars2.githubusercontent.com/u/25539161?v=4","gravatar_id":"","url":"https://api.github.com/users/spack","html_url":"https://github.com/spack","followers_url":"https://api.github.com/users/spack/followers","following_url":"https://api.github.com/users/spack/following{/other_user}","gists_url":"https://api.github.com/users/spack/gists{/gist_id}","starred_url":"https://api.github.com/users/spack/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/spack/subscriptions","organizations_url":"https://api.github.com/users/spack/orgs","repos_url":"https://api.github.com/users/spack/repos","events_url":"https://api.github.com/users/spack/events{/privacy}","received_events_url":"https://api.github.com/users/spack/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/spack/spack","description":"A + flexible package manager that supports multiple versions, configurations, platforms, + and compilers.","fork":false,"url":"https://api.github.com/repos/spack/spack","forks_url":"https://api.github.com/repos/spack/spack/forks","keys_url":"https://api.github.com/repos/spack/spack/keys{/key_id}","collaborators_url":"https://api.github.com/repos/spack/spack/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/spack/spack/teams","hooks_url":"https://api.github.com/repos/spack/spack/hooks","issue_events_url":"https://api.github.com/repos/spack/spack/issues/events{/number}","events_url":"https://api.github.com/repos/spack/spack/events","assignees_url":"https://api.github.com/repos/spack/spack/assignees{/user}","branches_url":"https://api.github.com/repos/spack/spack/branches{/branch}","tags_url":"https://api.github.com/repos/spack/spack/tags","blobs_url":"https://api.github.com/repos/spack/spack/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/spack/spack/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/spack/spack/git/refs{/sha}","trees_url":"https://api.github.com/repos/spack/spack/git/trees{/sha}","statuses_url":"https://api.github.com/repos/spack/spack/statuses/{sha}","languages_url":"https://api.github.com/repos/spack/spack/languages","stargazers_url":"https://api.github.com/repos/spack/spack/stargazers","contributors_url":"https://api.github.com/repos/spack/spack/contributors","subscribers_url":"https://api.github.com/repos/spack/spack/subscribers","subscription_url":"https://api.github.com/repos/spack/spack/subscription","commits_url":"https://api.github.com/repos/spack/spack/commits{/sha}","git_commits_url":"https://api.github.com/repos/spack/spack/git/commits{/sha}","comments_url":"https://api.github.com/repos/spack/spack/comments{/number}","issue_comment_url":"https://api.github.com/repos/spack/spack/issues/comments{/number}","contents_url":"https://api.github.com/repos/spack/spack/contents/{+path}","compare_url":"https://api.github.com/repos/spack/spack/compare/{base}...{head}","merges_url":"https://api.github.com/repos/spack/spack/merges","archive_url":"https://api.github.com/repos/spack/spack/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/spack/spack/downloads","issues_url":"https://api.github.com/repos/spack/spack/issues{/number}","pulls_url":"https://api.github.com/repos/spack/spack/pulls{/number}","milestones_url":"https://api.github.com/repos/spack/spack/milestones{/number}","notifications_url":"https://api.github.com/repos/spack/spack/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/spack/spack/labels{/name}","releases_url":"https://api.github.com/repos/spack/spack/releases{/id}","deployments_url":"https://api.github.com/repos/spack/spack/deployments","created_at":"2014-01-08T09:22:12Z","updated_at":"2019-11-07T19:47:17Z","pushed_at":"2019-11-07T20:54:26Z","git_url":"git://github.com/spack/spack.git","ssh_url":"git@github.com:spack/spack.git","clone_url":"https://github.com/spack/spack.git","svn_url":"https://github.com/spack/spack","homepage":"https://spack.io","size":67170,"stargazers_count":1277,"watchers_count":1277,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":793,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1385,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":793,"open_issues":1385,"watchers":1277,"default_branch":"develop"},"source":{"id":15730865,"node_id":"MDEwOlJlcG9zaXRvcnkxNTczMDg2NQ==","name":"spack","full_name":"spack/spack","private":false,"owner":{"login":"spack","id":25539161,"node_id":"MDEyOk9yZ2FuaXphdGlvbjI1NTM5MTYx","avatar_url":"https://avatars2.githubusercontent.com/u/25539161?v=4","gravatar_id":"","url":"https://api.github.com/users/spack","html_url":"https://github.com/spack","followers_url":"https://api.github.com/users/spack/followers","following_url":"https://api.github.com/users/spack/following{/other_user}","gists_url":"https://api.github.com/users/spack/gists{/gist_id}","starred_url":"https://api.github.com/users/spack/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/spack/subscriptions","organizations_url":"https://api.github.com/users/spack/orgs","repos_url":"https://api.github.com/users/spack/repos","events_url":"https://api.github.com/users/spack/events{/privacy}","received_events_url":"https://api.github.com/users/spack/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/spack/spack","description":"A + flexible package manager that supports multiple versions, configurations, platforms, + and compilers.","fork":false,"url":"https://api.github.com/repos/spack/spack","forks_url":"https://api.github.com/repos/spack/spack/forks","keys_url":"https://api.github.com/repos/spack/spack/keys{/key_id}","collaborators_url":"https://api.github.com/repos/spack/spack/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/spack/spack/teams","hooks_url":"https://api.github.com/repos/spack/spack/hooks","issue_events_url":"https://api.github.com/repos/spack/spack/issues/events{/number}","events_url":"https://api.github.com/repos/spack/spack/events","assignees_url":"https://api.github.com/repos/spack/spack/assignees{/user}","branches_url":"https://api.github.com/repos/spack/spack/branches{/branch}","tags_url":"https://api.github.com/repos/spack/spack/tags","blobs_url":"https://api.github.com/repos/spack/spack/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/spack/spack/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/spack/spack/git/refs{/sha}","trees_url":"https://api.github.com/repos/spack/spack/git/trees{/sha}","statuses_url":"https://api.github.com/repos/spack/spack/statuses/{sha}","languages_url":"https://api.github.com/repos/spack/spack/languages","stargazers_url":"https://api.github.com/repos/spack/spack/stargazers","contributors_url":"https://api.github.com/repos/spack/spack/contributors","subscribers_url":"https://api.github.com/repos/spack/spack/subscribers","subscription_url":"https://api.github.com/repos/spack/spack/subscription","commits_url":"https://api.github.com/repos/spack/spack/commits{/sha}","git_commits_url":"https://api.github.com/repos/spack/spack/git/commits{/sha}","comments_url":"https://api.github.com/repos/spack/spack/comments{/number}","issue_comment_url":"https://api.github.com/repos/spack/spack/issues/comments{/number}","contents_url":"https://api.github.com/repos/spack/spack/contents/{+path}","compare_url":"https://api.github.com/repos/spack/spack/compare/{base}...{head}","merges_url":"https://api.github.com/repos/spack/spack/merges","archive_url":"https://api.github.com/repos/spack/spack/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/spack/spack/downloads","issues_url":"https://api.github.com/repos/spack/spack/issues{/number}","pulls_url":"https://api.github.com/repos/spack/spack/pulls{/number}","milestones_url":"https://api.github.com/repos/spack/spack/milestones{/number}","notifications_url":"https://api.github.com/repos/spack/spack/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/spack/spack/labels{/name}","releases_url":"https://api.github.com/repos/spack/spack/releases{/id}","deployments_url":"https://api.github.com/repos/spack/spack/deployments","created_at":"2014-01-08T09:22:12Z","updated_at":"2019-11-07T19:47:17Z","pushed_at":"2019-11-07T20:54:26Z","git_url":"git://github.com/spack/spack.git","ssh_url":"git@github.com:spack/spack.git","clone_url":"https://github.com/spack/spack.git","svn_url":"https://github.com/spack/spack","homepage":"https://spack.io","size":67170,"stargazers_count":1277,"watchers_count":1277,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":793,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1385,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":793,"open_issues":1385,"watchers":1277,"default_branch":"develop"},"network_count":793,"subscribers_count":0}' + headers: + Access-Control-Allow-Origin: ['*'] + Access-Control-Expose-Headers: ['ETag, Link, Location, Retry-After, X-GitHub-OTP, + X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type'] + Cache-Control: ['private, max-age=60, s-maxage=60'] + Connection: [close] + Content-Security-Policy: [default-src 'none'] + Content-Type: [application/json; charset=utf-8] + Date: ['Thu, 07 Nov 2019 21:22:12 GMT'] + Etag: [W/"45973422cbdb9b27c39d3aa8d1ff79ba"] + Last-Modified: ['Wed, 09 Jan 2019 22:28:01 GMT'] + Referrer-Policy: ['origin-when-cross-origin, strict-origin-when-cross-origin'] + Server: [GitHub.com] + Status: [200 OK] + Strict-Transport-Security: [max-age=31536000; includeSubdomains; preload] + Transfer-Encoding: [chunked] + Vary: ['Accept, Authorization, Cookie, X-GitHub-OTP'] + X-Accepted-Oauth-Scopes: [repo] + X-Consumed-Content-Encoding: [gzip] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [deny] + X-Github-Media-Type: [github.v3] + X-Github-Request-Id: ['E376:134F:6AF489:D20851:5DC48B04'] + X-Oauth-Scopes: ['repo, user'] + X-Ratelimit-Limit: ['5000'] + X-Ratelimit-Remaining: ['4984'] + X-Ratelimit-Reset: ['1573165331'] + X-Xss-Protection: [1; mode=block] + status: {code: 200, message: OK} + status_code: 200 + url: https://api.github.com/repos/1nf1n1t3l00p/spack +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_set_bot_gitlab_subgroups.yaml b/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_set_bot_gitlab_subgroups.yaml new file mode 100644 index 0000000000..1c3cd111f2 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_set_bot_gitlab_subgroups.yaml @@ -0,0 +1,1284 @@ +interactions: +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://gitlab.com/api/v4/user + response: + body: {string: '{"id":3215137,"name":"Infinite Loop","username":"1nf1n1t3l00p","state":"active","avatar_url":"https://assets.gitlab-static.net/uploads/-/system/user/avatar/3215137/avatar.png","web_url":"https://gitlab.com/1nf1n1t3l00p","created_at":"2018-12-01T19:43:16.121Z","bio":"","location":"","public_email":"","skype":"","linkedin":"","twitter":"","website_url":"","organization":"","last_sign_in_at":"2019-10-20T22:24:28.741Z","confirmed_at":"2018-12-01T19:43:33.397Z","last_activity_on":"2019-11-07","email":"tjbiii.photo@gmail.com","theme_id":1,"color_scheme_id":1,"projects_limit":100000,"current_sign_in_at":"2019-11-07T22:23:23.652Z","identities":[],"can_create_group":true,"can_create_project":true,"two_factor_enabled":false,"external":false,"private_profile":false,"shared_runners_minutes_limit":null,"extra_shared_runners_minutes_limit":null}'} + headers: + - !!python/tuple + - Server + - [nginx] + - !!python/tuple + - Date + - ['Thu, 07 Nov 2019 22:24:48 GMT'] + - !!python/tuple + - Content-Type + - [application/json] + - !!python/tuple + - Content-Length + - ['843'] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Cache-Control + - ['max-age=0, private, must-revalidate'] + - !!python/tuple + - Etag + - [W/"2b4a569af963e5456798e07f62cbb19c"] + - !!python/tuple + - Vary + - [Origin] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Frame-Options + - [SAMEORIGIN] + - !!python/tuple + - X-Request-Id + - [Tu1X3IMfcQ] + - !!python/tuple + - X-Runtime + - ['0.033973'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000] + - !!python/tuple + - Referrer-Policy + - [strict-origin-when-cross-origin] + - !!python/tuple + - Ratelimit-Limit + - ['600'] + - !!python/tuple + - Ratelimit-Observed + - ['1'] + - !!python/tuple + - Ratelimit-Remaining + - ['599'] + - !!python/tuple + - Ratelimit-Reset + - ['1573165548'] + - !!python/tuple + - Ratelimit-Resettime + - ['Thu, 07 Nov 2019 22:25:48 GMT'] + - !!python/tuple + - Gitlab-Lb + - [fe-22-lb-gprd] + - !!python/tuple + - Gitlab-Sv + - [localhost] + status: {code: 200, message: OK} + url: https://gitlab.com/api/v4/user +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://gitlab.com/api/v4/groups?per_page=100 + response: + body: {string: '[{"id":5608536,"web_url":"https://gitlab.com/groups/bevera","name":"Bevera","path":"bevera","description":"Bevera + Code and Issues.","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"Bevera","full_path":"bevera","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":4165904,"web_url":"https://gitlab.com/groups/l00p_group_1","name":"My + Awesome Group","path":"l00p_group_1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group","full_path":"l00p_group_1","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":5542118,"web_url":"https://gitlab.com/groups/sm-package-zen","name":"Package + Zen","path":"sm-package-zen","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"Package + Zen","full_path":"sm-package-zen","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":4570068,"web_url":"https://gitlab.com/groups/falco-group-1","name":"falco-group-1","path":"falco-group-1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"falco-group-1","full_path":"falco-group-1","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":4570071,"web_url":"https://gitlab.com/groups/falco-group-1/falco-subgroup-1","name":"falco-subgroup-1","path":"falco-subgroup-1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"falco-group-1 + / falco-subgroup-1","full_path":"falco-group-1/falco-subgroup-1","parent_id":4570068,"ldap_cn":null,"ldap_access":null},{"id":4165905,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup1","name":"subgroup1","path":"subgroup1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup1","full_path":"l00p_group_1/subgroup1","parent_id":4165904,"ldap_cn":null,"ldap_access":null},{"id":4165907,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup2","name":"subgroup2","path":"subgroup2","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup2","full_path":"l00p_group_1/subgroup2","parent_id":4165904,"ldap_cn":null,"ldap_access":null},{"id":4255344,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup2/subsub","name":"subsub","path":"subsub","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup2 / subsub","full_path":"l00p_group_1/subgroup2/subsub","parent_id":4165907,"ldap_cn":null,"ldap_access":null}]'} + headers: + - !!python/tuple + - Server + - [nginx] + - !!python/tuple + - Date + - ['Thu, 07 Nov 2019 22:24:49 GMT'] + - !!python/tuple + - Content-Type + - [application/json] + - !!python/tuple + - Content-Length + - ['4554'] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Cache-Control + - ['max-age=0, private, must-revalidate'] + - !!python/tuple + - Etag + - [W/"52ac61adbb1454a98c89845aa24cd882"] + - !!python/tuple + - Link + - ['; + rel="first", ; + rel="last"'] + - !!python/tuple + - Vary + - [Origin] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Frame-Options + - [SAMEORIGIN] + - !!python/tuple + - X-Next-Page + - [''] + - !!python/tuple + - X-Page + - ['1'] + - !!python/tuple + - X-Per-Page + - ['100'] + - !!python/tuple + - X-Prev-Page + - [''] + - !!python/tuple + - X-Request-Id + - [xwbyurHqRK9] + - !!python/tuple + - X-Runtime + - ['0.081314'] + - !!python/tuple + - X-Total + - ['8'] + - !!python/tuple + - X-Total-Pages + - ['1'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000] + - !!python/tuple + - Referrer-Policy + - [strict-origin-when-cross-origin] + - !!python/tuple + - Ratelimit-Limit + - ['600'] + - !!python/tuple + - Ratelimit-Observed + - ['2'] + - !!python/tuple + - Ratelimit-Remaining + - ['598'] + - !!python/tuple + - Ratelimit-Reset + - ['1573165549'] + - !!python/tuple + - Ratelimit-Resettime + - ['Thu, 07 Nov 2019 22:25:49 GMT'] + - !!python/tuple + - Gitlab-Lb + - [fe-20-lb-gprd] + - !!python/tuple + - Gitlab-Sv + - [localhost] + status: {code: 200, message: OK} + url: https://gitlab.com/api/v4/groups?per_page=100 +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://gitlab.com/api/v4/groups/5608536/projects?per_page=50&page=1 + response: + body: {string: '[{"id":13258489,"description":"by Michael G. Scott","name":"BeVera","name_with_namespace":"Bevera + / BeVera","path":"BeVera","path_with_namespace":"bevera/BeVera","created_at":"2019-07-10T15:35:54.122Z","default_branch":"0_0_dev","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:bevera/BeVera.git","http_url_to_repo":"https://gitlab.com/bevera/BeVera.git","web_url":"https://gitlab.com/bevera/BeVera","readme_url":"https://gitlab.com/bevera/BeVera/blob/0_0_dev/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-07-10T20:30:54.406Z","namespace":{"id":5608536,"name":"Bevera","path":"bevera","kind":"group","full_path":"bevera","parent_id":null,"avatar_url":null,"web_url":"https://gitlab.com/groups/bevera"},"_links":{"self":"https://gitlab.com/api/v4/projects/13258489","issues":"https://gitlab.com/api/v4/projects/13258489/issues","merge_requests":"https://gitlab.com/api/v4/projects/13258489/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/13258489/repository/branches","labels":"https://gitlab.com/api/v4/projects/13258489/labels","events":"https://gitlab.com/api/v4/projects/13258489/events","members":"https://gitlab.com/api/v4/projects/13258489/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":4097368,"import_status":"finished","open_issues_count":10,"ci_default_git_depth":50,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","mirror":false,"external_authorization_classification_label":""}]'} + headers: + - !!python/tuple + - Server + - [nginx] + - !!python/tuple + - Date + - ['Thu, 07 Nov 2019 22:24:49 GMT'] + - !!python/tuple + - Content-Type + - [application/json] + - !!python/tuple + - Content-Length + - ['2347'] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Cache-Control + - ['max-age=0, private, must-revalidate'] + - !!python/tuple + - Etag + - [W/"534deb57432ec2fc7653caf5920639ac"] + - !!python/tuple + - Link + - ['; + rel="first", ; + rel="last"'] + - !!python/tuple + - Vary + - [Origin] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Frame-Options + - [SAMEORIGIN] + - !!python/tuple + - X-Next-Page + - [''] + - !!python/tuple + - X-Page + - ['1'] + - !!python/tuple + - X-Per-Page + - ['50'] + - !!python/tuple + - X-Prev-Page + - [''] + - !!python/tuple + - X-Request-Id + - [NzUp1dkiVL9] + - !!python/tuple + - X-Runtime + - ['0.125837'] + - !!python/tuple + - X-Total + - ['1'] + - !!python/tuple + - X-Total-Pages + - ['1'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000] + - !!python/tuple + - Referrer-Policy + - [strict-origin-when-cross-origin] + - !!python/tuple + - Ratelimit-Limit + - ['600'] + - !!python/tuple + - Ratelimit-Observed + - ['3'] + - !!python/tuple + - Ratelimit-Remaining + - ['597'] + - !!python/tuple + - Ratelimit-Reset + - ['1573165549'] + - !!python/tuple + - Ratelimit-Resettime + - ['Thu, 07 Nov 2019 22:25:49 GMT'] + - !!python/tuple + - Gitlab-Lb + - [fe-06-lb-gprd] + - !!python/tuple + - Gitlab-Sv + - [localhost] + status: {code: 200, message: OK} + url: https://gitlab.com/api/v4/groups/5608536/projects?per_page=50&page=1 +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://gitlab.com/api/v4/groups/4165904/projects?per_page=50&page=1 + response: + body: {string: '[{"id":9715859,"description":"","name":"loop proj","name_with_namespace":"My + Awesome Group / loop proj","path":"loop-proj","path_with_namespace":"l00p_group_1/loop-proj","created_at":"2018-12-01T19:48:07.749Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/loop-proj.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/loop-proj.git","web_url":"https://gitlab.com/l00p_group_1/loop-proj","readme_url":"https://gitlab.com/l00p_group_1/loop-proj/blob/master/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-03-01T01:09:11.939Z","namespace":{"id":4165904,"name":"My + Awesome Group","path":"l00p_group_1","kind":"group","full_path":"l00p_group_1","parent_id":null,"avatar_url":null,"web_url":"https://gitlab.com/groups/l00p_group_1"},"_links":{"self":"https://gitlab.com/api/v4/projects/9715859","issues":"https://gitlab.com/api/v4/projects/9715859/issues","merge_requests":"https://gitlab.com/api/v4/projects/9715859/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9715859/repository/branches","labels":"https://gitlab.com/api/v4/projects/9715859/labels","events":"https://gitlab.com/api/v4/projects/9715859/events","members":"https://gitlab.com/api/v4/projects/9715859/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","mirror":false,"external_authorization_classification_label":""}]'} + headers: + - !!python/tuple + - Server + - [nginx] + - !!python/tuple + - Date + - ['Thu, 07 Nov 2019 22:24:49 GMT'] + - !!python/tuple + - Content-Type + - [application/json] + - !!python/tuple + - Content-Length + - ['2407'] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Cache-Control + - ['max-age=0, private, must-revalidate'] + - !!python/tuple + - Etag + - [W/"37b44e954132f63b61f0e7e8f7e91c8d"] + - !!python/tuple + - Link + - ['; + rel="first", ; + rel="last"'] + - !!python/tuple + - Vary + - [Origin] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Frame-Options + - [SAMEORIGIN] + - !!python/tuple + - X-Next-Page + - [''] + - !!python/tuple + - X-Page + - ['1'] + - !!python/tuple + - X-Per-Page + - ['50'] + - !!python/tuple + - X-Prev-Page + - [''] + - !!python/tuple + - X-Request-Id + - [kOkvJLi5D74] + - !!python/tuple + - X-Runtime + - ['0.101962'] + - !!python/tuple + - X-Total + - ['1'] + - !!python/tuple + - X-Total-Pages + - ['1'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000] + - !!python/tuple + - Referrer-Policy + - [strict-origin-when-cross-origin] + - !!python/tuple + - Ratelimit-Limit + - ['600'] + - !!python/tuple + - Ratelimit-Observed + - ['4'] + - !!python/tuple + - Ratelimit-Remaining + - ['596'] + - !!python/tuple + - Ratelimit-Reset + - ['1573165549'] + - !!python/tuple + - Ratelimit-Resettime + - ['Thu, 07 Nov 2019 22:25:49 GMT'] + - !!python/tuple + - Gitlab-Lb + - [fe-22-lb-gprd] + - !!python/tuple + - Gitlab-Sv + - [localhost] + status: {code: 200, message: OK} + url: https://gitlab.com/api/v4/groups/4165904/projects?per_page=50&page=1 +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://gitlab.com/api/v4/groups/5542118/projects?per_page=50&page=1 + response: + body: {string: '[{"id":13279607,"description":"","name":"match","name_with_namespace":"Package + Zen / match","path":"match","path_with_namespace":"sm-package-zen/match","created_at":"2019-07-11T22:39:19.412Z","default_branch":null,"tag_list":[],"ssh_url_to_repo":"git@gitlab.com:sm-package-zen/match.git","http_url_to_repo":"https://gitlab.com/sm-package-zen/match.git","web_url":"https://gitlab.com/sm-package-zen/match","readme_url":null,"avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-07-11T22:39:19.412Z","namespace":{"id":5542118,"name":"Package + Zen","path":"sm-package-zen","kind":"group","full_path":"sm-package-zen","parent_id":null,"avatar_url":null,"web_url":"https://gitlab.com/groups/sm-package-zen"},"_links":{"self":"https://gitlab.com/api/v4/projects/13279607","issues":"https://gitlab.com/api/v4/projects/13279607/issues","merge_requests":"https://gitlab.com/api/v4/projects/13279607/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/13279607/repository/branches","labels":"https://gitlab.com/api/v4/projects/13279607/labels","events":"https://gitlab.com/api/v4/projects/13279607/events","members":"https://gitlab.com/api/v4/projects/13279607/members"},"empty_repo":true,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"ci_default_git_depth":50,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","mirror":false,"external_authorization_classification_label":""},{"id":13264968,"description":"","name":"matcher","name_with_namespace":"Package + Zen / matcher","path":"matcher","path_with_namespace":"sm-package-zen/matcher","created_at":"2019-07-11T03:07:21.270Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:sm-package-zen/matcher.git","http_url_to_repo":"https://gitlab.com/sm-package-zen/matcher.git","web_url":"https://gitlab.com/sm-package-zen/matcher","readme_url":"https://gitlab.com/sm-package-zen/matcher/blob/master/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-08-28T00:30:16.095Z","namespace":{"id":5542118,"name":"Package + Zen","path":"sm-package-zen","kind":"group","full_path":"sm-package-zen","parent_id":null,"avatar_url":null,"web_url":"https://gitlab.com/groups/sm-package-zen"},"_links":{"self":"https://gitlab.com/api/v4/projects/13264968","issues":"https://gitlab.com/api/v4/projects/13264968/issues","merge_requests":"https://gitlab.com/api/v4/projects/13264968/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/13264968/repository/branches","labels":"https://gitlab.com/api/v4/projects/13264968/labels","events":"https://gitlab.com/api/v4/projects/13264968/events","members":"https://gitlab.com/api/v4/projects/13264968/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"ci_default_git_depth":50,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","mirror":false,"external_authorization_classification_label":""},{"id":13122097,"description":"","name":"Matcherino + - ML","name_with_namespace":"Package Zen / Matcherino - ML","path":"pz-matcherino-ml","path_with_namespace":"sm-package-zen/pz-matcherino-ml","created_at":"2019-07-01T19:58:31.736Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:sm-package-zen/pz-matcherino-ml.git","http_url_to_repo":"https://gitlab.com/sm-package-zen/pz-matcherino-ml.git","web_url":"https://gitlab.com/sm-package-zen/pz-matcherino-ml","readme_url":null,"avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-07-01T19:58:31.736Z","namespace":{"id":5542118,"name":"Package + Zen","path":"sm-package-zen","kind":"group","full_path":"sm-package-zen","parent_id":null,"avatar_url":null,"web_url":"https://gitlab.com/groups/sm-package-zen"},"_links":{"self":"https://gitlab.com/api/v4/projects/13122097","issues":"https://gitlab.com/api/v4/projects/13122097/issues","merge_requests":"https://gitlab.com/api/v4/projects/13122097/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/13122097/repository/branches","labels":"https://gitlab.com/api/v4/projects/13122097/labels","events":"https://gitlab.com/api/v4/projects/13122097/events","members":"https://gitlab.com/api/v4/projects/13122097/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":4097368,"import_status":"finished","open_issues_count":0,"ci_default_git_depth":50,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","mirror":false,"external_authorization_classification_label":""}]'} + headers: + - !!python/tuple + - Server + - [nginx] + - !!python/tuple + - Date + - ['Thu, 07 Nov 2019 22:24:49 GMT'] + - !!python/tuple + - Content-Type + - [application/json] + - !!python/tuple + - Content-Length + - ['7131'] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Cache-Control + - ['max-age=0, private, must-revalidate'] + - !!python/tuple + - Etag + - [W/"e6060726c18bdefaf574e7bff2b230ed"] + - !!python/tuple + - Link + - ['; + rel="first", ; + rel="last"'] + - !!python/tuple + - Vary + - [Origin] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Frame-Options + - [SAMEORIGIN] + - !!python/tuple + - X-Next-Page + - [''] + - !!python/tuple + - X-Page + - ['1'] + - !!python/tuple + - X-Per-Page + - ['50'] + - !!python/tuple + - X-Prev-Page + - [''] + - !!python/tuple + - X-Request-Id + - [MrGPWpWuzM1] + - !!python/tuple + - X-Runtime + - ['0.183865'] + - !!python/tuple + - X-Total + - ['3'] + - !!python/tuple + - X-Total-Pages + - ['1'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000] + - !!python/tuple + - Referrer-Policy + - [strict-origin-when-cross-origin] + - !!python/tuple + - Ratelimit-Limit + - ['600'] + - !!python/tuple + - Ratelimit-Observed + - ['5'] + - !!python/tuple + - Ratelimit-Remaining + - ['595'] + - !!python/tuple + - Ratelimit-Reset + - ['1573165549'] + - !!python/tuple + - Ratelimit-Resettime + - ['Thu, 07 Nov 2019 22:25:49 GMT'] + - !!python/tuple + - Gitlab-Lb + - [fe-20-lb-gprd] + - !!python/tuple + - Gitlab-Sv + - [localhost] + status: {code: 200, message: OK} + url: https://gitlab.com/api/v4/groups/5542118/projects?per_page=50&page=1 +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://gitlab.com/api/v4/groups/4570068/projects?per_page=50&page=1 + response: + body: {string: '[]'} + headers: + - !!python/tuple + - Server + - [nginx] + - !!python/tuple + - Date + - ['Thu, 07 Nov 2019 22:24:50 GMT'] + - !!python/tuple + - Content-Type + - [application/json] + - !!python/tuple + - Content-Length + - ['2'] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Cache-Control + - ['max-age=0, private, must-revalidate'] + - !!python/tuple + - Etag + - [W/"4f53cda18c2baa0c0354bb5f9a3ecbe5"] + - !!python/tuple + - Link + - ['; + rel="first", ; + rel="last"'] + - !!python/tuple + - Vary + - [Origin] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Frame-Options + - [SAMEORIGIN] + - !!python/tuple + - X-Next-Page + - [''] + - !!python/tuple + - X-Page + - ['1'] + - !!python/tuple + - X-Per-Page + - ['50'] + - !!python/tuple + - X-Prev-Page + - [''] + - !!python/tuple + - X-Request-Id + - [VVsLlWfpEe2] + - !!python/tuple + - X-Runtime + - ['0.053084'] + - !!python/tuple + - X-Total + - ['0'] + - !!python/tuple + - X-Total-Pages + - ['1'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000] + - !!python/tuple + - Referrer-Policy + - [strict-origin-when-cross-origin] + - !!python/tuple + - Ratelimit-Limit + - ['600'] + - !!python/tuple + - Ratelimit-Observed + - ['6'] + - !!python/tuple + - Ratelimit-Remaining + - ['594'] + - !!python/tuple + - Ratelimit-Reset + - ['1573165550'] + - !!python/tuple + - Ratelimit-Resettime + - ['Thu, 07 Nov 2019 22:25:50 GMT'] + - !!python/tuple + - Gitlab-Lb + - [fe-11-lb-gprd] + - !!python/tuple + - Gitlab-Sv + - [localhost] + status: {code: 200, message: OK} + url: https://gitlab.com/api/v4/groups/4570068/projects?per_page=50&page=1 +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://gitlab.com/api/v4/groups/4570071/projects?per_page=50&page=1 + response: + body: {string: '[{"id":10754567,"description":"","name":"falco-subgroup-1-proj","name_with_namespace":"falco-group-1 + / falco-subgroup-1 / falco-subgroup-1-proj","path":"falco-subgroup-1-proj","path_with_namespace":"falco-group-1/falco-subgroup-1/falco-subgroup-1-proj","created_at":"2019-02-08T15:21:22.591Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:falco-group-1/falco-subgroup-1/falco-subgroup-1-proj.git","http_url_to_repo":"https://gitlab.com/falco-group-1/falco-subgroup-1/falco-subgroup-1-proj.git","web_url":"https://gitlab.com/falco-group-1/falco-subgroup-1/falco-subgroup-1-proj","readme_url":"https://gitlab.com/falco-group-1/falco-subgroup-1/falco-subgroup-1-proj/blob/master/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-02-08T15:21:22.591Z","namespace":{"id":4570071,"name":"falco-subgroup-1","path":"falco-subgroup-1","kind":"group","full_path":"falco-group-1/falco-subgroup-1","parent_id":4570068,"avatar_url":null,"web_url":"https://gitlab.com/groups/falco-group-1/falco-subgroup-1"},"_links":{"self":"https://gitlab.com/api/v4/projects/10754567","issues":"https://gitlab.com/api/v4/projects/10754567/issues","merge_requests":"https://gitlab.com/api/v4/projects/10754567/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/10754567/repository/branches","labels":"https://gitlab.com/api/v4/projects/10754567/labels","events":"https://gitlab.com/api/v4/projects/10754567/events","members":"https://gitlab.com/api/v4/projects/10754567/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3235271,"import_status":"none","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","mirror":false,"external_authorization_classification_label":""}]'} + headers: + - !!python/tuple + - Server + - [nginx] + - !!python/tuple + - Date + - ['Thu, 07 Nov 2019 22:24:50 GMT'] + - !!python/tuple + - Content-Type + - [application/json] + - !!python/tuple + - Content-Length + - ['2660'] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Cache-Control + - ['max-age=0, private, must-revalidate'] + - !!python/tuple + - Etag + - [W/"bf2d147c7eadd4e264368ae31cef5d9f"] + - !!python/tuple + - Link + - ['; + rel="first", ; + rel="last"'] + - !!python/tuple + - Vary + - [Origin] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Frame-Options + - [SAMEORIGIN] + - !!python/tuple + - X-Next-Page + - [''] + - !!python/tuple + - X-Page + - ['1'] + - !!python/tuple + - X-Per-Page + - ['50'] + - !!python/tuple + - X-Prev-Page + - [''] + - !!python/tuple + - X-Request-Id + - [dCjTwhJi9T6] + - !!python/tuple + - X-Runtime + - ['0.127661'] + - !!python/tuple + - X-Total + - ['1'] + - !!python/tuple + - X-Total-Pages + - ['1'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000] + - !!python/tuple + - Referrer-Policy + - [strict-origin-when-cross-origin] + - !!python/tuple + - Ratelimit-Limit + - ['600'] + - !!python/tuple + - Ratelimit-Observed + - ['7'] + - !!python/tuple + - Ratelimit-Remaining + - ['593'] + - !!python/tuple + - Ratelimit-Reset + - ['1573165550'] + - !!python/tuple + - Ratelimit-Resettime + - ['Thu, 07 Nov 2019 22:25:50 GMT'] + - !!python/tuple + - Gitlab-Lb + - [fe-16-lb-gprd] + - !!python/tuple + - Gitlab-Sv + - [localhost] + status: {code: 200, message: OK} + url: https://gitlab.com/api/v4/groups/4570071/projects?per_page=50&page=1 +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://gitlab.com/api/v4/groups/4165905/projects?per_page=50&page=1 + response: + body: {string: '[{"id":12071992,"description":"","name":"proj-c","name_with_namespace":"My + Awesome Group / subgroup1 / proj-c","path":"proj-c","path_with_namespace":"l00p_group_1/subgroup1/proj-c","created_at":"2019-04-28T17:45:14.077Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/subgroup1/proj-c.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/subgroup1/proj-c.git","web_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-c","readme_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-c/blob/master/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-04-28T17:45:14.077Z","namespace":{"id":4165905,"name":"subgroup1","path":"subgroup1","kind":"group","full_path":"l00p_group_1/subgroup1","parent_id":4165904,"avatar_url":null,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup1"},"_links":{"self":"https://gitlab.com/api/v4/projects/12071992","issues":"https://gitlab.com/api/v4/projects/12071992/issues","merge_requests":"https://gitlab.com/api/v4/projects/12071992/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/12071992/repository/branches","labels":"https://gitlab.com/api/v4/projects/12071992/labels","events":"https://gitlab.com/api/v4/projects/12071992/events","members":"https://gitlab.com/api/v4/projects/12071992/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","mirror":false,"external_authorization_classification_label":""},{"id":11087339,"description":"","name":"proj-b","name_with_namespace":"My + Awesome Group / subgroup1 / proj-b","path":"proj-b","path_with_namespace":"l00p_group_1/subgroup1/proj-b","created_at":"2019-03-01T01:27:29.582Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/subgroup1/proj-b.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/subgroup1/proj-b.git","web_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-b","readme_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-b/blob/master/README.md","avatar_url":null,"star_count":0,"forks_count":1,"last_activity_at":"2019-04-28T17:48:06.317Z","namespace":{"id":4165905,"name":"subgroup1","path":"subgroup1","kind":"group","full_path":"l00p_group_1/subgroup1","parent_id":4165904,"avatar_url":null,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup1"},"_links":{"self":"https://gitlab.com/api/v4/projects/11087339","issues":"https://gitlab.com/api/v4/projects/11087339/issues","merge_requests":"https://gitlab.com/api/v4/projects/11087339/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/11087339/repository/branches","labels":"https://gitlab.com/api/v4/projects/11087339/labels","events":"https://gitlab.com/api/v4/projects/11087339/events","members":"https://gitlab.com/api/v4/projects/11087339/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"private","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","mirror":false,"external_authorization_classification_label":""},{"id":9715852,"description":"","name":"proj-A","name_with_namespace":"My + Awesome Group / subgroup1 / proj-A","path":"proj-a","path_with_namespace":"l00p_group_1/subgroup1/proj-a","created_at":"2018-12-01T19:47:18.634Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/subgroup1/proj-a.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/subgroup1/proj-a.git","web_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-a","readme_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-a/blob/master/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-03-01T01:16:00.572Z","namespace":{"id":4165905,"name":"subgroup1","path":"subgroup1","kind":"group","full_path":"l00p_group_1/subgroup1","parent_id":4165904,"avatar_url":null,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup1"},"_links":{"self":"https://gitlab.com/api/v4/projects/9715852","issues":"https://gitlab.com/api/v4/projects/9715852/issues","merge_requests":"https://gitlab.com/api/v4/projects/9715852/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9715852/repository/branches","labels":"https://gitlab.com/api/v4/projects/9715852/labels","events":"https://gitlab.com/api/v4/projects/9715852/events","members":"https://gitlab.com/api/v4/projects/9715852/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","mirror":false,"external_authorization_classification_label":""}]'} + headers: + - !!python/tuple + - Server + - [nginx] + - !!python/tuple + - Date + - ['Thu, 07 Nov 2019 22:24:50 GMT'] + - !!python/tuple + - Content-Type + - [application/json] + - !!python/tuple + - Content-Length + - ['7388'] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Cache-Control + - ['max-age=0, private, must-revalidate'] + - !!python/tuple + - Etag + - [W/"db77be3ee470349828761cf7c06240a4"] + - !!python/tuple + - Link + - ['; + rel="first", ; + rel="last"'] + - !!python/tuple + - Vary + - [Origin] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Frame-Options + - [SAMEORIGIN] + - !!python/tuple + - X-Next-Page + - [''] + - !!python/tuple + - X-Page + - ['1'] + - !!python/tuple + - X-Per-Page + - ['50'] + - !!python/tuple + - X-Prev-Page + - [''] + - !!python/tuple + - X-Request-Id + - [nVk7owu1012] + - !!python/tuple + - X-Runtime + - ['0.249399'] + - !!python/tuple + - X-Total + - ['3'] + - !!python/tuple + - X-Total-Pages + - ['1'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000] + - !!python/tuple + - Referrer-Policy + - [strict-origin-when-cross-origin] + - !!python/tuple + - Ratelimit-Limit + - ['600'] + - !!python/tuple + - Ratelimit-Observed + - ['8'] + - !!python/tuple + - Ratelimit-Remaining + - ['592'] + - !!python/tuple + - Ratelimit-Reset + - ['1573165550'] + - !!python/tuple + - Ratelimit-Resettime + - ['Thu, 07 Nov 2019 22:25:50 GMT'] + - !!python/tuple + - Gitlab-Lb + - [fe-18-lb-gprd] + - !!python/tuple + - Gitlab-Sv + - [localhost] + status: {code: 200, message: OK} + url: https://gitlab.com/api/v4/groups/4165905/projects?per_page=50&page=1 +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://gitlab.com/api/v4/groups/4165907/projects?per_page=50&page=1 + response: + body: {string: '[]'} + headers: + - !!python/tuple + - Server + - [nginx] + - !!python/tuple + - Date + - ['Thu, 07 Nov 2019 22:24:50 GMT'] + - !!python/tuple + - Content-Type + - [application/json] + - !!python/tuple + - Content-Length + - ['2'] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Cache-Control + - ['max-age=0, private, must-revalidate'] + - !!python/tuple + - Etag + - [W/"4f53cda18c2baa0c0354bb5f9a3ecbe5"] + - !!python/tuple + - Link + - ['; + rel="first", ; + rel="last"'] + - !!python/tuple + - Vary + - [Origin] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Frame-Options + - [SAMEORIGIN] + - !!python/tuple + - X-Next-Page + - [''] + - !!python/tuple + - X-Page + - ['1'] + - !!python/tuple + - X-Per-Page + - ['50'] + - !!python/tuple + - X-Prev-Page + - [''] + - !!python/tuple + - X-Request-Id + - [gAtHhP3pV47] + - !!python/tuple + - X-Runtime + - ['0.054930'] + - !!python/tuple + - X-Total + - ['0'] + - !!python/tuple + - X-Total-Pages + - ['1'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000] + - !!python/tuple + - Referrer-Policy + - [strict-origin-when-cross-origin] + - !!python/tuple + - Ratelimit-Limit + - ['600'] + - !!python/tuple + - Ratelimit-Observed + - ['9'] + - !!python/tuple + - Ratelimit-Remaining + - ['591'] + - !!python/tuple + - Ratelimit-Reset + - ['1573165550'] + - !!python/tuple + - Ratelimit-Resettime + - ['Thu, 07 Nov 2019 22:25:50 GMT'] + - !!python/tuple + - Gitlab-Lb + - [fe-19-lb-gprd] + - !!python/tuple + - Gitlab-Sv + - [localhost] + status: {code: 200, message: OK} + url: https://gitlab.com/api/v4/groups/4165907/projects?per_page=50&page=1 +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://gitlab.com/api/v4/groups/4255344/projects?per_page=50&page=1 + response: + body: {string: '[{"id":9950231,"description":"","name":"subsub-proj","name_with_namespace":"My + Awesome Group / subgroup2 / subsub / subsub-proj","path":"subsub-proj","path_with_namespace":"l00p_group_1/subgroup2/subsub/subsub-proj","created_at":"2018-12-16T17:02:52.619Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/subgroup2/subsub/subsub-proj.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/subgroup2/subsub/subsub-proj.git","web_url":"https://gitlab.com/l00p_group_1/subgroup2/subsub/subsub-proj","readme_url":"https://gitlab.com/l00p_group_1/subgroup2/subsub/subsub-proj/blob/master/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2018-12-16T17:02:52.619Z","namespace":{"id":4255344,"name":"subsub","path":"subsub","kind":"group","full_path":"l00p_group_1/subgroup2/subsub","parent_id":4165907,"avatar_url":null,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup2/subsub"},"_links":{"self":"https://gitlab.com/api/v4/projects/9950231","issues":"https://gitlab.com/api/v4/projects/9950231/issues","merge_requests":"https://gitlab.com/api/v4/projects/9950231/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9950231/repository/branches","labels":"https://gitlab.com/api/v4/projects/9950231/labels","events":"https://gitlab.com/api/v4/projects/9950231/events","members":"https://gitlab.com/api/v4/projects/9950231/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","mirror":false,"external_authorization_classification_label":""}]'} + headers: + - !!python/tuple + - Server + - [nginx] + - !!python/tuple + - Date + - ['Thu, 07 Nov 2019 22:24:51 GMT'] + - !!python/tuple + - Content-Type + - [application/json] + - !!python/tuple + - Content-Length + - ['2550'] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Cache-Control + - ['max-age=0, private, must-revalidate'] + - !!python/tuple + - Etag + - [W/"19d9f6aac48ea8194ee887f78ec0cebd"] + - !!python/tuple + - Link + - ['; + rel="first", ; + rel="last"'] + - !!python/tuple + - Vary + - [Origin] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Frame-Options + - [SAMEORIGIN] + - !!python/tuple + - X-Next-Page + - [''] + - !!python/tuple + - X-Page + - ['1'] + - !!python/tuple + - X-Per-Page + - ['50'] + - !!python/tuple + - X-Prev-Page + - [''] + - !!python/tuple + - X-Request-Id + - [XWMjJLM4n51] + - !!python/tuple + - X-Runtime + - ['0.143526'] + - !!python/tuple + - X-Total + - ['1'] + - !!python/tuple + - X-Total-Pages + - ['1'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000] + - !!python/tuple + - Referrer-Policy + - [strict-origin-when-cross-origin] + - !!python/tuple + - Ratelimit-Limit + - ['600'] + - !!python/tuple + - Ratelimit-Observed + - ['10'] + - !!python/tuple + - Ratelimit-Remaining + - ['590'] + - !!python/tuple + - Ratelimit-Reset + - ['1573165551'] + - !!python/tuple + - Ratelimit-Resettime + - ['Thu, 07 Nov 2019 22:25:51 GMT'] + - !!python/tuple + - Gitlab-Lb + - [fe-14-lb-gprd] + - !!python/tuple + - Gitlab-Sv + - [localhost] + status: {code: 200, message: OK} + url: https://gitlab.com/api/v4/groups/4255344/projects?per_page=50&page=1 +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://gitlab.com/api/v4/projects?owned=true&per_page=50&page=1 + response: + body: {string: '[{"id":12071992,"description":"","name":"proj-c","name_with_namespace":"My + Awesome Group / subgroup1 / proj-c","path":"proj-c","path_with_namespace":"l00p_group_1/subgroup1/proj-c","created_at":"2019-04-28T17:45:14.077Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/subgroup1/proj-c.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/subgroup1/proj-c.git","web_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-c","readme_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-c/blob/master/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-04-28T17:45:14.077Z","namespace":{"id":4165905,"name":"subgroup1","path":"subgroup1","kind":"group","full_path":"l00p_group_1/subgroup1","parent_id":4165904,"avatar_url":null,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup1"},"_links":{"self":"https://gitlab.com/api/v4/projects/12071992","issues":"https://gitlab.com/api/v4/projects/12071992/issues","merge_requests":"https://gitlab.com/api/v4/projects/12071992/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/12071992/repository/branches","labels":"https://gitlab.com/api/v4/projects/12071992/labels","events":"https://gitlab.com/api/v4/projects/12071992/events","members":"https://gitlab.com/api/v4/projects/12071992/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}},"mirror":false,"external_authorization_classification_label":""},{"id":11087339,"description":"","name":"proj-b","name_with_namespace":"My + Awesome Group / subgroup1 / proj-b","path":"proj-b","path_with_namespace":"l00p_group_1/subgroup1/proj-b","created_at":"2019-03-01T01:27:29.582Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/subgroup1/proj-b.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/subgroup1/proj-b.git","web_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-b","readme_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-b/blob/master/README.md","avatar_url":null,"star_count":0,"forks_count":1,"last_activity_at":"2019-04-28T17:48:06.317Z","namespace":{"id":4165905,"name":"subgroup1","path":"subgroup1","kind":"group","full_path":"l00p_group_1/subgroup1","parent_id":4165904,"avatar_url":null,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup1"},"_links":{"self":"https://gitlab.com/api/v4/projects/11087339","issues":"https://gitlab.com/api/v4/projects/11087339/issues","merge_requests":"https://gitlab.com/api/v4/projects/11087339/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/11087339/repository/branches","labels":"https://gitlab.com/api/v4/projects/11087339/labels","events":"https://gitlab.com/api/v4/projects/11087339/events","members":"https://gitlab.com/api/v4/projects/11087339/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"private","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}},"mirror":false,"external_authorization_classification_label":""},{"id":10797587,"description":"","name":"test-ngrok","name_with_namespace":"Infinite + Loop / test-ngrok","path":"test-ngrok","path_with_namespace":"1nf1n1t3l00p/test-ngrok","created_at":"2019-02-11T23:57:46.167Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:1nf1n1t3l00p/test-ngrok.git","http_url_to_repo":"https://gitlab.com/1nf1n1t3l00p/test-ngrok.git","web_url":"https://gitlab.com/1nf1n1t3l00p/test-ngrok","readme_url":"https://gitlab.com/1nf1n1t3l00p/test-ngrok/blob/master/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-02-11T23:57:46.167Z","namespace":{"id":4165899,"name":"Infinite + Loop","path":"1nf1n1t3l00p","kind":"user","full_path":"1nf1n1t3l00p","parent_id":null,"avatar_url":"/uploads/-/system/user/avatar/3215137/avatar.png","web_url":"https://gitlab.com/1nf1n1t3l00p"},"_links":{"self":"https://gitlab.com/api/v4/projects/10797587","issues":"https://gitlab.com/api/v4/projects/10797587/issues","merge_requests":"https://gitlab.com/api/v4/projects/10797587/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/10797587/repository/branches","labels":"https://gitlab.com/api/v4/projects/10797587/labels","events":"https://gitlab.com/api/v4/projects/10797587/events","members":"https://gitlab.com/api/v4/projects/10797587/members"},"empty_repo":false,"archived":false,"visibility":"private","owner":{"id":3215137,"name":"Infinite + Loop","username":"1nf1n1t3l00p","state":"active","avatar_url":"https://assets.gitlab-static.net/uploads/-/system/user/avatar/3215137/avatar.png","web_url":"https://gitlab.com/1nf1n1t3l00p"},"resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","permissions":{"project_access":{"access_level":40,"notification_level":3},"group_access":null},"mirror":false,"external_authorization_classification_label":""},{"id":10337453,"description":"The + pytest framework makes it easy to write small tests, yet scales to support + complex functional testing","name":"pytest","name_with_namespace":"Infinite + Loop / pytest","path":"pytest","path_with_namespace":"1nf1n1t3l00p/pytest","created_at":"2019-01-12T23:04:33.294Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:1nf1n1t3l00p/pytest.git","http_url_to_repo":"https://gitlab.com/1nf1n1t3l00p/pytest.git","web_url":"https://gitlab.com/1nf1n1t3l00p/pytest","readme_url":"https://gitlab.com/1nf1n1t3l00p/pytest/blob/master/README.rst","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-01-30T01:35:29.409Z","namespace":{"id":4165899,"name":"Infinite + Loop","path":"1nf1n1t3l00p","kind":"user","full_path":"1nf1n1t3l00p","parent_id":null,"avatar_url":"/uploads/-/system/user/avatar/3215137/avatar.png","web_url":"https://gitlab.com/1nf1n1t3l00p"},"_links":{"self":"https://gitlab.com/api/v4/projects/10337453","issues":"https://gitlab.com/api/v4/projects/10337453/issues","merge_requests":"https://gitlab.com/api/v4/projects/10337453/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/10337453/repository/branches","labels":"https://gitlab.com/api/v4/projects/10337453/labels","events":"https://gitlab.com/api/v4/projects/10337453/events","members":"https://gitlab.com/api/v4/projects/10337453/members"},"empty_repo":false,"archived":false,"visibility":"public","owner":{"id":3215137,"name":"Infinite + Loop","username":"1nf1n1t3l00p","state":"active","avatar_url":"https://assets.gitlab-static.net/uploads/-/system/user/avatar/3215137/avatar.png","web_url":"https://gitlab.com/1nf1n1t3l00p"},"resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"forked_from_project":{"id":9422960,"description":"The + pytest framework makes it easy to write small tests, yet scales to support + complex functional testing","name":"pytest","name_with_namespace":"Eli Hooten + / pytest","path":"pytest","path_with_namespace":"hootener/pytest","created_at":"2018-11-15T16:52:55.486Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:hootener/pytest.git","http_url_to_repo":"https://gitlab.com/hootener/pytest.git","web_url":"https://gitlab.com/hootener/pytest","readme_url":"https://gitlab.com/hootener/pytest/blob/master/README.rst","avatar_url":null,"star_count":0,"forks_count":2,"last_activity_at":"2018-11-15T22:43:29.619Z","namespace":{"id":3969537,"name":"Eli + Hooten","path":"hootener","kind":"user","full_path":"hootener","parent_id":null,"avatar_url":"https://secure.gravatar.com/avatar/ab022b5adf4e3edc9ea77fe7fd561837?s=80\u0026d=identicon","web_url":"https://gitlab.com/hootener"}},"import_status":"finished","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","permissions":{"project_access":{"access_level":40,"notification_level":3},"group_access":null},"approvals_before_merge":0,"mirror":false,"external_authorization_classification_label":"","packages_enabled":true},{"id":9950231,"description":"","name":"subsub-proj","name_with_namespace":"My + Awesome Group / subgroup2 / subsub / subsub-proj","path":"subsub-proj","path_with_namespace":"l00p_group_1/subgroup2/subsub/subsub-proj","created_at":"2018-12-16T17:02:52.619Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/subgroup2/subsub/subsub-proj.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/subgroup2/subsub/subsub-proj.git","web_url":"https://gitlab.com/l00p_group_1/subgroup2/subsub/subsub-proj","readme_url":"https://gitlab.com/l00p_group_1/subgroup2/subsub/subsub-proj/blob/master/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2018-12-16T17:02:52.619Z","namespace":{"id":4255344,"name":"subsub","path":"subsub","kind":"group","full_path":"l00p_group_1/subgroup2/subsub","parent_id":4165907,"avatar_url":null,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup2/subsub"},"_links":{"self":"https://gitlab.com/api/v4/projects/9950231","issues":"https://gitlab.com/api/v4/projects/9950231/issues","merge_requests":"https://gitlab.com/api/v4/projects/9950231/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9950231/repository/branches","labels":"https://gitlab.com/api/v4/projects/9950231/labels","events":"https://gitlab.com/api/v4/projects/9950231/events","members":"https://gitlab.com/api/v4/projects/9950231/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","permissions":{"project_access":null,"group_access":null},"mirror":false,"external_authorization_classification_label":""},{"id":9862840,"description":"","name":"assume-flag","name_with_namespace":"Infinite + Loop / assume-flag","path":"assume-flag","path_with_namespace":"1nf1n1t3l00p/assume-flag","created_at":"2018-12-10T23:24:07.415Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:1nf1n1t3l00p/assume-flag.git","http_url_to_repo":"https://gitlab.com/1nf1n1t3l00p/assume-flag.git","web_url":"https://gitlab.com/1nf1n1t3l00p/assume-flag","readme_url":"https://gitlab.com/1nf1n1t3l00p/assume-flag/blob/master/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2018-12-17T02:31:08.694Z","namespace":{"id":4165899,"name":"Infinite + Loop","path":"1nf1n1t3l00p","kind":"user","full_path":"1nf1n1t3l00p","parent_id":null,"avatar_url":"/uploads/-/system/user/avatar/3215137/avatar.png","web_url":"https://gitlab.com/1nf1n1t3l00p"},"_links":{"self":"https://gitlab.com/api/v4/projects/9862840","issues":"https://gitlab.com/api/v4/projects/9862840/issues","merge_requests":"https://gitlab.com/api/v4/projects/9862840/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9862840/repository/branches","labels":"https://gitlab.com/api/v4/projects/9862840/labels","events":"https://gitlab.com/api/v4/projects/9862840/events","members":"https://gitlab.com/api/v4/projects/9862840/members"},"empty_repo":false,"archived":false,"visibility":"private","owner":{"id":3215137,"name":"Infinite + Loop","username":"1nf1n1t3l00p","state":"active","avatar_url":"https://assets.gitlab-static.net/uploads/-/system/user/avatar/3215137/avatar.png","web_url":"https://gitlab.com/1nf1n1t3l00p"},"resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","permissions":{"project_access":{"access_level":40,"notification_level":3},"group_access":null},"mirror":false,"external_authorization_classification_label":""},{"id":9715862,"description":"","name":"inf + proj","name_with_namespace":"Infinite Loop / inf proj","path":"inf-proj","path_with_namespace":"1nf1n1t3l00p/inf-proj","created_at":"2018-12-01T19:48:26.216Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:1nf1n1t3l00p/inf-proj.git","http_url_to_repo":"https://gitlab.com/1nf1n1t3l00p/inf-proj.git","web_url":"https://gitlab.com/1nf1n1t3l00p/inf-proj","readme_url":"https://gitlab.com/1nf1n1t3l00p/inf-proj/blob/master/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2018-12-01T19:48:26.216Z","namespace":{"id":4165899,"name":"Infinite + Loop","path":"1nf1n1t3l00p","kind":"user","full_path":"1nf1n1t3l00p","parent_id":null,"avatar_url":"/uploads/-/system/user/avatar/3215137/avatar.png","web_url":"https://gitlab.com/1nf1n1t3l00p"},"_links":{"self":"https://gitlab.com/api/v4/projects/9715862","issues":"https://gitlab.com/api/v4/projects/9715862/issues","merge_requests":"https://gitlab.com/api/v4/projects/9715862/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9715862/repository/branches","labels":"https://gitlab.com/api/v4/projects/9715862/labels","events":"https://gitlab.com/api/v4/projects/9715862/events","members":"https://gitlab.com/api/v4/projects/9715862/members"},"empty_repo":false,"archived":false,"visibility":"private","owner":{"id":3215137,"name":"Infinite + Loop","username":"1nf1n1t3l00p","state":"active","avatar_url":"https://assets.gitlab-static.net/uploads/-/system/user/avatar/3215137/avatar.png","web_url":"https://gitlab.com/1nf1n1t3l00p"},"resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","permissions":{"project_access":{"access_level":40,"notification_level":3},"group_access":null},"mirror":false,"external_authorization_classification_label":""},{"id":9715859,"description":"","name":"loop + proj","name_with_namespace":"My Awesome Group / loop proj","path":"loop-proj","path_with_namespace":"l00p_group_1/loop-proj","created_at":"2018-12-01T19:48:07.749Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/loop-proj.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/loop-proj.git","web_url":"https://gitlab.com/l00p_group_1/loop-proj","readme_url":"https://gitlab.com/l00p_group_1/loop-proj/blob/master/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-03-01T01:09:11.939Z","namespace":{"id":4165904,"name":"My + Awesome Group","path":"l00p_group_1","kind":"group","full_path":"l00p_group_1","parent_id":null,"avatar_url":null,"web_url":"https://gitlab.com/groups/l00p_group_1"},"_links":{"self":"https://gitlab.com/api/v4/projects/9715859","issues":"https://gitlab.com/api/v4/projects/9715859/issues","merge_requests":"https://gitlab.com/api/v4/projects/9715859/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9715859/repository/branches","labels":"https://gitlab.com/api/v4/projects/9715859/labels","events":"https://gitlab.com/api/v4/projects/9715859/events","members":"https://gitlab.com/api/v4/projects/9715859/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}},"mirror":false,"external_authorization_classification_label":""},{"id":9715852,"description":"","name":"proj-A","name_with_namespace":"My + Awesome Group / subgroup1 / proj-A","path":"proj-a","path_with_namespace":"l00p_group_1/subgroup1/proj-a","created_at":"2018-12-01T19:47:18.634Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/subgroup1/proj-a.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/subgroup1/proj-a.git","web_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-a","readme_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-a/blob/master/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-03-01T01:16:00.572Z","namespace":{"id":4165905,"name":"subgroup1","path":"subgroup1","kind":"group","full_path":"l00p_group_1/subgroup1","parent_id":4165904,"avatar_url":null,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup1"},"_links":{"self":"https://gitlab.com/api/v4/projects/9715852","issues":"https://gitlab.com/api/v4/projects/9715852/issues","merge_requests":"https://gitlab.com/api/v4/projects/9715852/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9715852/repository/branches","labels":"https://gitlab.com/api/v4/projects/9715852/labels","events":"https://gitlab.com/api/v4/projects/9715852/events","members":"https://gitlab.com/api/v4/projects/9715852/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}},"mirror":false,"external_authorization_classification_label":""}]'} + headers: + - !!python/tuple + - Server + - [nginx] + - !!python/tuple + - Date + - ['Thu, 07 Nov 2019 22:24:51 GMT'] + - !!python/tuple + - Content-Type + - [application/json] + - !!python/tuple + - Transfer-Encoding + - [chunked] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Vary + - [Accept-Encoding, Origin] + - !!python/tuple + - Cache-Control + - ['max-age=0, private, must-revalidate'] + - !!python/tuple + - Etag + - [W/"10256f8121b8fac2f9050e5ab329c827"] + - !!python/tuple + - Link + - ['; + rel="first", ; + rel="last"'] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Frame-Options + - [SAMEORIGIN] + - !!python/tuple + - X-Next-Page + - [''] + - !!python/tuple + - X-Page + - ['1'] + - !!python/tuple + - X-Per-Page + - ['50'] + - !!python/tuple + - X-Prev-Page + - [''] + - !!python/tuple + - X-Request-Id + - [uEI6ejunue6] + - !!python/tuple + - X-Runtime + - ['0.508830'] + - !!python/tuple + - X-Total + - ['9'] + - !!python/tuple + - X-Total-Pages + - ['1'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000] + - !!python/tuple + - Referrer-Policy + - [strict-origin-when-cross-origin] + - !!python/tuple + - Ratelimit-Limit + - ['600'] + - !!python/tuple + - Ratelimit-Observed + - ['11'] + - !!python/tuple + - Ratelimit-Remaining + - ['589'] + - !!python/tuple + - Ratelimit-Reset + - ['1573165551'] + - !!python/tuple + - Ratelimit-Resettime + - ['Thu, 07 Nov 2019 22:25:51 GMT'] + - !!python/tuple + - Gitlab-Lb + - [fe-18-lb-gprd] + - !!python/tuple + - Gitlab-Sv + - [localhost] + - !!python/tuple + - X-Consumed-Content-Encoding + - [gzip] + status: {code: 200, message: OK} + url: https://gitlab.com/api/v4/projects?owned=true&per_page=50&page=1 +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://gitlab.com/api/v4/users?username=hootener + response: + body: {string: '[{"id":3108129,"name":"Eli Hooten","username":"hootener","state":"active","avatar_url":"https://secure.gravatar.com/avatar/ab022b5adf4e3edc9ea77fe7fd561837?s=80\u0026d=identicon","web_url":"https://gitlab.com/hootener"}]'} + headers: + - !!python/tuple + - Server + - [nginx] + - !!python/tuple + - Date + - ['Thu, 07 Nov 2019 22:24:51 GMT'] + - !!python/tuple + - Content-Type + - [application/json] + - !!python/tuple + - Content-Length + - ['220'] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Cache-Control + - ['max-age=0, private, must-revalidate'] + - !!python/tuple + - Etag + - [W/"55b9059f7864ce342c8bd96c5c8e5b5a"] + - !!python/tuple + - Link + - ['; + rel="first", ; + rel="last"'] + - !!python/tuple + - Vary + - [Origin] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Frame-Options + - [SAMEORIGIN] + - !!python/tuple + - X-Next-Page + - [''] + - !!python/tuple + - X-Page + - ['1'] + - !!python/tuple + - X-Per-Page + - ['20'] + - !!python/tuple + - X-Prev-Page + - [''] + - !!python/tuple + - X-Request-Id + - [ZyuoVpLDm2] + - !!python/tuple + - X-Runtime + - ['0.024654'] + - !!python/tuple + - X-Total + - ['1'] + - !!python/tuple + - X-Total-Pages + - ['1'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000] + - !!python/tuple + - Referrer-Policy + - [strict-origin-when-cross-origin] + - !!python/tuple + - Ratelimit-Limit + - ['600'] + - !!python/tuple + - Ratelimit-Observed + - ['12'] + - !!python/tuple + - Ratelimit-Remaining + - ['588'] + - !!python/tuple + - Ratelimit-Reset + - ['1573165551'] + - !!python/tuple + - Ratelimit-Resettime + - ['Thu, 07 Nov 2019 22:25:51 GMT'] + - !!python/tuple + - Gitlab-Lb + - [fe-14-lb-gprd] + - !!python/tuple + - Gitlab-Sv + - [localhost] + status: {code: 200, message: OK} + url: https://gitlab.com/api/v4/users?username=hootener +- request: + body: null + headers: + Accept: [application/json] + User-Agent: [Default] + method: GET + uri: https://gitlab.com/api/v4/projects/9422960 + response: + body: {string: '{"id":9422960,"description":"The pytest framework makes it easy + to write small tests, yet scales to support complex functional testing","name":"pytest","name_with_namespace":"Eli + Hooten / pytest","path":"pytest","path_with_namespace":"hootener/pytest","created_at":"2018-11-15T16:52:55.486Z","default_branch":"master","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:hootener/pytest.git","http_url_to_repo":"https://gitlab.com/hootener/pytest.git","web_url":"https://gitlab.com/hootener/pytest","readme_url":"https://gitlab.com/hootener/pytest/blob/master/README.rst","avatar_url":null,"star_count":0,"forks_count":2,"last_activity_at":"2018-11-15T22:43:29.619Z","namespace":{"id":3969537,"name":"Eli + Hooten","path":"hootener","kind":"user","full_path":"hootener","parent_id":null,"avatar_url":"https://secure.gravatar.com/avatar/ab022b5adf4e3edc9ea77fe7fd561837?s=80\u0026d=identicon","web_url":"https://gitlab.com/hootener"},"_links":{"self":"https://gitlab.com/api/v4/projects/9422960","issues":"https://gitlab.com/api/v4/projects/9422960/issues","merge_requests":"https://gitlab.com/api/v4/projects/9422960/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9422960/repository/branches","labels":"https://gitlab.com/api/v4/projects/9422960/labels","events":"https://gitlab.com/api/v4/projects/9422960/events","members":"https://gitlab.com/api/v4/projects/9422960/members"},"empty_repo":false,"archived":false,"visibility":"public","owner":{"id":3108129,"name":"Eli + Hooten","username":"hootener","state":"active","avatar_url":"https://secure.gravatar.com/avatar/ab022b5adf4e3edc9ea77fe7fd561837?s=80\u0026d=identicon","web_url":"https://gitlab.com/hootener"},"resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3108129,"import_status":"finished","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","permissions":{"project_access":null,"group_access":null},"approvals_before_merge":0,"mirror":false,"external_authorization_classification_label":"","packages_enabled":true}'} + headers: + - !!python/tuple + - Server + - [nginx] + - !!python/tuple + - Date + - ['Thu, 07 Nov 2019 22:24:52 GMT'] + - !!python/tuple + - Content-Type + - [application/json] + - !!python/tuple + - Content-Length + - ['2861'] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Cache-Control + - ['max-age=0, private, must-revalidate'] + - !!python/tuple + - Etag + - [W/"cf19f5c5b0edd5ce2cb42ae8697c628b"] + - !!python/tuple + - Vary + - [Origin] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Frame-Options + - [SAMEORIGIN] + - !!python/tuple + - X-Request-Id + - [56Ue1EoORk2] + - !!python/tuple + - X-Runtime + - ['0.078179'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000] + - !!python/tuple + - Referrer-Policy + - [strict-origin-when-cross-origin] + - !!python/tuple + - Ratelimit-Limit + - ['600'] + - !!python/tuple + - Ratelimit-Observed + - ['13'] + - !!python/tuple + - Ratelimit-Remaining + - ['587'] + - !!python/tuple + - Ratelimit-Reset + - ['1573165552'] + - !!python/tuple + - Ratelimit-Resettime + - ['Thu, 07 Nov 2019 22:25:52 GMT'] + - !!python/tuple + - Gitlab-Lb + - [fe-09-lb-gprd] + - !!python/tuple + - Gitlab-Sv + - [localhost] + status: {code: 200, message: OK} + url: https://gitlab.com/api/v4/projects/9422960 +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_sync_repos_using_integration.yaml b/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_sync_repos_using_integration.yaml new file mode 100644 index 0000000000..a997d5e408 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_sync_repos_using_integration.yaml @@ -0,0 +1,107 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/vnd.github.machine-man-preview+json + User-Agent: + - Default + method: GET + uri: https://api.github.com/installation/repositories?per_page=100&page=1 + response: + content: '{ "total_count": 3, "repositories": [ { "id": 159089634, "node_id": + "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", "name": "pytest", "full_name": "1nf1n1t3l00p/pytest", + "owner": { "login": "1nf1n1t3l00p", "id": 45343385, "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", "gravatar_id": + "", "url": "https://api.github.com/users/octocat", "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", "following_url": + "https://api.github.com/users/octocat/following{/other_user}", "gists_url": + "https://api.github.com/users/octocat/gists{/gist_id}", "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", "organizations_url": + "https://api.github.com/users/octocat/orgs", "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", "received_events_url": + "https://api.github.com/users/octocat/received_events", "type": "User", "site_admin": + false }, "private": false, "html_url": "https://github.com/octocat/Hello-World", + "description": "This your first repo!", "fork": false, "url": "https://api.github.com/repos/octocat/Hello-World" + }, { "id": 164948070, "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", "name": "spack", + "full_name": "1nf1n1t3l00p/spack", "owner": { "login": "1nf1n1t3l00p", "id": + 45343385, "node_id": "MDQ6VXNlcjE=", "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", "url": "https://api.github.com/users/octocat", "html_url": + "https://github.com/octocat", "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", "starred_url": + "https://api.github.com/users/octocat/starred{/owner}{/repo}", "subscriptions_url": + "https://api.github.com/users/octocat/subscriptions", "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", "site_admin": false }, "private": false, "html_url": "https://github.com/octocat/Hello-World", + "description": "This your first repo!", "fork": false, "url": "https://api.github.com/repos/octocat/Hello-World" + }, { "id": 213786132, "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", "name": "pub", + "full_name": "1nf1n1t3l00p/pub", "owner": { "login": "1nf1n1t3l00p", "id": 45343385, + "node_id": "MDQ6VXNlcjE=", "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", "url": "https://api.github.com/users/octocat", "html_url": + "https://github.com/octocat", "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", "starred_url": + "https://api.github.com/users/octocat/starred{/owner}{/repo}", "subscriptions_url": + "https://api.github.com/users/octocat/subscriptions", "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", "site_admin": false }, "private": false, "html_url": "https://github.com/octocat/Hello-World", + "description": "This your first repo!", "fork": false, "url": "https://api.github.com/repos/octocat/Hello-World" + } ] }' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 20 Feb 2020 03:21:56 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 403 Forbidden + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.machine-man-preview; format=json + X-Github-Request-Id: + - B880:4CF4:AD5BEB:1A3160D:5E4DFB54 + X-Oauth-Scopes: + - repo, user + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4999' + X-Ratelimit-Reset: + - '1582172516' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/installation/repositories?per_page=100&page=1 +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_sync_repos_using_integration_no_repos.yaml b/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_sync_repos_using_integration_no_repos.yaml new file mode 100644 index 0000000000..5f41c7433e --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_sync_repos_using_integration_no_repos.yaml @@ -0,0 +1,67 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/vnd.github.machine-man-preview+json + User-Agent: + - Default + method: GET + uri: https://api.github.com/installation/repositories?per_page=100&page=1 + response: + content: '{ "total_count": 0, "repositories": [] }' + headers: + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 20 Feb 2020 03:21:56 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 403 Forbidden + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-Oauth-Scopes: + - "" + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.machine-man-preview; format=json + X-Github-Request-Id: + - B880:4CF4:AD5BEB:1A3160D:5E4DFB54 + X-Oauth-Scopes: + - repo, user + X-Ratelimit-Limit: + - "5000" + X-Ratelimit-Remaining: + - "4999" + X-Ratelimit-Reset: + - "1582172516" + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/installation/repositories?per_page=100&page=1 +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_sync_repos_with_feature_flag_django_call.yaml b/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_sync_repos_with_feature_flag_django_call.yaml new file mode 100644 index 0000000000..a566a2d422 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_sync_repos_with_feature_flag_django_call.yaml @@ -0,0 +1,65 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/user/repos?per_page=100&page=1 + response: + content: '{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Length: + - '80' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 14 Mar 2024 18:39:08 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - 4242:0F3D:2B2A4D:523B83:65F3444C + X-RateLimit-Limit: + - '60' + X-RateLimit-Remaining: + - '51' + X-RateLimit-Reset: + - '1710444668' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '9' + X-XSS-Protection: + - '0' + http_version: HTTP/1.1 + status_code: 401 +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_sync_teams_task/TestSyncTeamsTaskUnit/test_gitlab_subgroups.yaml b/apps/worker/tasks/tests/unit/cassetes/test_sync_teams_task/TestSyncTeamsTaskUnit/test_gitlab_subgroups.yaml new file mode 100644 index 0000000000..a3911064ce --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_sync_teams_task/TestSyncTeamsTaskUnit/test_gitlab_subgroups.yaml @@ -0,0 +1,83 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups?per_page=100 + response: + content: '[{"id":4165904,"web_url":"https://gitlab.com/groups/l00p_group_1","name":"My + Awesome Group","path":"l00p_group_1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group","full_path":"l00p_group_1","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":4570068,"web_url":"https://gitlab.com/groups/falco-group-1","name":"falco-group-1","path":"falco-group-1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"falco-group-1","full_path":"falco-group-1","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":4570071,"web_url":"https://gitlab.com/groups/falco-group-1/falco-subgroup-1","name":"falco-subgroup-1","path":"falco-subgroup-1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"falco-group-1 + / falco-subgroup-1","full_path":"falco-group-1/falco-subgroup-1","parent_id":4570068,"ldap_cn":null,"ldap_access":null},{"id":4165905,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup1","name":"subgroup1","path":"subgroup1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup1","full_path":"l00p_group_1/subgroup1","parent_id":4165904,"ldap_cn":null,"ldap_access":null},{"id":4165907,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup2","name":"subgroup2","path":"subgroup2","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup2","full_path":"l00p_group_1/subgroup2","parent_id":4165904,"ldap_cn":null,"ldap_access":null},{"id":4255344,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup2/subsub","name":"subsub","path":"subsub","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup2 / subsub","full_path":"l00p_group_1/subgroup2/subsub","parent_id":4165907,"ldap_cn":null,"ldap_access":null}]' + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Connection: + - close + Content-Length: + - '4554' + Content-Type: + - application/json + Date: + - Thu, 24 Oct 2019 21:44:59 GMT + Etag: + - W/"52ac61adbb1454a98c89845aa24cd882" + Gitlab-Lb: + - fe-15-lb-gprd + Gitlab-Sv: + - localhost + Link: + - ; + rel="first", ; + rel="last" + Ratelimit-Limit: + - '600' + Ratelimit-Observed: + - '1' + Ratelimit-Remaining: + - '599' + Ratelimit-Reset: + - '1571953559' + Ratelimit-Resettime: + - Thu, 24 Oct 2019 21:45:59 GMT + Referrer-Policy: + - strict-origin-when-cross-origin + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Next-Page: + - '' + X-Page: + - '1' + X-Per-Page: + - '100' + X-Prev-Page: + - '' + X-Request-Id: + - f9jlP4u8pu4 + X-Runtime: + - '0.077474' + X-Total: + - '8' + X-Total-Pages: + - '1' + status: + code: 200 + message: OK + status_code: 200 + url: https://gitlab.com/api/v4/groups?per_page=100 +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_sync_teams_task/TestSyncTeamsTaskUnit/test_no_teams.yaml b/apps/worker/tasks/tests/unit/cassetes/test_sync_teams_task/TestSyncTeamsTaskUnit/test_no_teams.yaml new file mode 100644 index 0000000000..74fcfedeed --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_sync_teams_task/TestSyncTeamsTaskUnit/test_no_teams.yaml @@ -0,0 +1,73 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/user/memberships/orgs?page=1 + response: + content: '[]' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + cache-control: + - private, max-age=60, s-maxage=60 + content-length: + - '2' + content-security-policy: + - default-src 'none' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 29 Oct 2020 20:53:21 GMT + etag: + - '"6f7b0ea21eaeada815b32d296452cb826d6e059d8e60febd69f199af03450eed"' + referrer-policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + server: + - GitHub.com + status: + - 200 OK + strict-transport-security: + - max-age=31536000; includeSubdomains; preload + vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + x-accepted-oauth-scopes: + - admin:org, read:org, repo, user, write:org + x-content-type-options: + - nosniff + x-frame-options: + - deny + x-github-media-type: + - github.v3 + x-github-request-id: + - 138F:21E9:14389E:2C122A:5F9B2BC1 + x-oauth-scopes: + - repo, user + x-ratelimit-limit: + - '5000' + x-ratelimit-remaining: + - '4999' + x-ratelimit-reset: + - '1604008401' + x-ratelimit-used: + - '1' + x-xss-protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_sync_teams_task/TestSyncTeamsTaskUnit/test_team_data_updated.yaml b/apps/worker/tasks/tests/unit/cassetes/test_sync_teams_task/TestSyncTeamsTaskUnit/test_team_data_updated.yaml new file mode 100644 index 0000000000..c3f10da32e --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_sync_teams_task/TestSyncTeamsTaskUnit/test_team_data_updated.yaml @@ -0,0 +1,227 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/user/memberships/orgs?page=1 + response: + content: '[{"url":"https://api.github.com/orgs/codecov/memberships/ThiagoCodecov","state":"active","role":"member","organization_url":"https://api.github.com/orgs/codecov","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"organization":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","url":"https://api.github.com/orgs/codecov","repos_url":"https://api.github.com/orgs/codecov/repos","events_url":"https://api.github.com/orgs/codecov/events","hooks_url":"https://api.github.com/orgs/codecov/hooks","issues_url":"https://api.github.com/orgs/codecov/issues","members_url":"https://api.github.com/orgs/codecov/members{/member}","public_members_url":"https://api.github.com/orgs/codecov/public_members{/member}","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","description":"Empower + developers with tools to improve code quality and testing."}},{"url":"https://api.github.com/orgs/ThiagoCodecovTeam/memberships/ThiagoCodecov","state":"active","role":"admin","organization_url":"https://api.github.com/orgs/ThiagoCodecovTeam","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"organization":{"login":"ThiagoCodecovTeam","id":57222756,"node_id":"MDEyOk9yZ2FuaXphdGlvbjU3MjIyNzU2","url":"https://api.github.com/orgs/ThiagoCodecovTeam","repos_url":"https://api.github.com/orgs/ThiagoCodecovTeam/repos","events_url":"https://api.github.com/orgs/ThiagoCodecovTeam/events","hooks_url":"https://api.github.com/orgs/ThiagoCodecovTeam/hooks","issues_url":"https://api.github.com/orgs/ThiagoCodecovTeam/issues","members_url":"https://api.github.com/orgs/ThiagoCodecovTeam/members{/member}","public_members_url":"https://api.github.com/orgs/ThiagoCodecovTeam/public_members{/member}","avatar_url":"https://avatars0.githubusercontent.com/u/57222756?v=4","description":null}}]' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + cache-control: + - private, max-age=60, s-maxage=60 + content-encoding: + - gzip + content-security-policy: + - default-src 'none' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 29 Oct 2020 20:54:49 GMT + etag: + - W/"b239a71030388622987b9d420e2ce04b583f3e139c278acbf24001de841cddd7" + referrer-policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + server: + - GitHub.com + status: + - 200 OK + strict-transport-security: + - max-age=31536000; includeSubdomains; preload + transfer-encoding: + - chunked + vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + x-accepted-oauth-scopes: + - admin:org, read:org, repo, user, write:org + x-content-type-options: + - nosniff + x-frame-options: + - deny + x-github-media-type: + - github.v3 + x-github-request-id: + - 121B:46D9:4E122:D6EC6:5F9B2C19 + x-oauth-scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, workflow, write:discussion + x-ratelimit-limit: + - '5000' + x-ratelimit-remaining: + - '4987' + x-ratelimit-reset: + - '1604007913' + x-ratelimit-used: + - '13' + x-xss-protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/users/codecov + response: + content: '{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false,"name":"Codecov","company":null,"blog":"https://codecov.io/","location":null,"email":"hello@codecov.io","hireable":null,"bio":"Empower + developers with tools to improve code quality and testing.","twitter_username":null,"public_repos":98,"public_gists":0,"followers":0,"following":0,"created_at":"2014-07-21T16:22:31Z","updated_at":"2020-10-28T19:29:26Z"}' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + cache-control: + - private, max-age=60, s-maxage=60 + content-encoding: + - gzip + content-security-policy: + - default-src 'none' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 29 Oct 2020 20:54:49 GMT + etag: + - W/"c24e96a03914be633678cca4f59e1941e1b1ecbdd5be7f45527a0b98e71e15a8" + last-modified: + - Wed, 28 Oct 2020 19:29:26 GMT + referrer-policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + server: + - GitHub.com + status: + - 200 OK + strict-transport-security: + - max-age=31536000; includeSubdomains; preload + transfer-encoding: + - chunked + vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + x-accepted-oauth-scopes: + - '' + x-content-type-options: + - nosniff + x-frame-options: + - deny + x-github-media-type: + - github.v3 + x-github-request-id: + - 121B:46D9:4E128:D6ECA:5F9B2C19 + x-oauth-scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, workflow, write:discussion + x-ratelimit-limit: + - '5000' + x-ratelimit-remaining: + - '4986' + x-ratelimit-reset: + - '1604007913' + x-ratelimit-used: + - '14' + x-xss-protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/users/ThiagoCodecovTeam + response: + content: '{"login":"ThiagoCodecovTeam","id":57222756,"node_id":"MDEyOk9yZ2FuaXphdGlvbjU3MjIyNzU2","avatar_url":"https://avatars0.githubusercontent.com/u/57222756?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecovTeam","html_url":"https://github.com/ThiagoCodecovTeam","followers_url":"https://api.github.com/users/ThiagoCodecovTeam/followers","following_url":"https://api.github.com/users/ThiagoCodecovTeam/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecovTeam/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecovTeam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecovTeam/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecovTeam/orgs","repos_url":"https://api.github.com/users/ThiagoCodecovTeam/repos","events_url":"https://api.github.com/users/ThiagoCodecovTeam/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecovTeam/received_events","type":"Organization","site_admin":false,"name":null,"company":null,"blog":"","location":null,"email":null,"hireable":null,"bio":null,"twitter_username":null,"public_repos":0,"public_gists":0,"followers":0,"following":0,"created_at":"2019-10-31T13:07:24Z","updated_at":"2019-10-31T13:07:24Z"}' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + cache-control: + - private, max-age=60, s-maxage=60 + content-encoding: + - gzip + content-security-policy: + - default-src 'none' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 29 Oct 2020 20:54:50 GMT + etag: + - W/"9fba591d890817ee6895ef095a4eae5de9577c942e16afc4c9c856a00c9a91b0" + last-modified: + - Thu, 31 Oct 2019 13:07:24 GMT + referrer-policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + server: + - GitHub.com + status: + - 200 OK + strict-transport-security: + - max-age=31536000; includeSubdomains; preload + transfer-encoding: + - chunked + vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + x-accepted-oauth-scopes: + - '' + x-content-type-options: + - nosniff + x-frame-options: + - deny + x-github-media-type: + - github.v3 + x-github-request-id: + - 121B:46D9:4E12B:D6ED6:5F9B2C19 + x-oauth-scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, workflow, write:discussion + x-ratelimit-limit: + - '5000' + x-ratelimit-remaining: + - '4985' + x-ratelimit-reset: + - '1604007913' + x-ratelimit-used: + - '15' + x-xss-protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_sync_teams_task/TestSyncTeamsTaskUnit/test_team_removed.yaml b/apps/worker/tasks/tests/unit/cassetes/test_sync_teams_task/TestSyncTeamsTaskUnit/test_team_removed.yaml new file mode 100644 index 0000000000..9738553db2 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_sync_teams_task/TestSyncTeamsTaskUnit/test_team_removed.yaml @@ -0,0 +1,73 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/user/memberships/orgs?page=1 + response: + content: '[]' + headers: + access-control-allow-origin: + - '*' + access-control-expose-headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + cache-control: + - private, max-age=60, s-maxage=60 + content-length: + - '2' + content-security-policy: + - default-src 'none' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 29 Oct 2020 20:53:22 GMT + etag: + - '"6f7b0ea21eaeada815b32d296452cb826d6e059d8e60febd69f199af03450eed"' + referrer-policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + server: + - GitHub.com + status: + - 200 OK + strict-transport-security: + - max-age=31536000; includeSubdomains; preload + vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + x-accepted-oauth-scopes: + - admin:org, read:org, repo, user, write:org + x-content-type-options: + - nosniff + x-frame-options: + - deny + x-github-media-type: + - github.v3 + x-github-request-id: + - 1395:772F:895BC:16B873:5F9B2BC2 + x-oauth-scopes: + - repo, user + x-ratelimit-limit: + - '5000' + x-ratelimit-remaining: + - '4998' + x-ratelimit-reset: + - '1604008401' + x-ratelimit-used: + - '2' + x-xss-protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_upload_finisher_task/TestUploadFinisherTask/test_upload_finisher_task_call.yaml b/apps/worker/tasks/tests/unit/cassetes/test_upload_finisher_task/TestUploadFinisherTask/test_upload_finisher_task_call.yaml new file mode 100644 index 0000000000..ab370bd5c8 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_upload_finisher_task/TestUploadFinisherTask/test_upload_finisher_task_call.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_processor_task_call.yaml b/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_processor_task_call.yaml new file mode 100644 index 0000000000..37121719a9 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_processor_task_call.yaml @@ -0,0 +1,84 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/vnd.github.v3.diff + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 + response: + content: "diff --git a/awesome/__init__.py b/awesome/__init__.py\nindex 1da48d3..770f2e8 + 100644\n--- a/awesome/__init__.py\n+++ b/awesome/__init__.py\n@@ -10,3 +10,7 + @@ def fib(n):\n if n < 2:\n return 1\n return fib(n - 2) + + fib(n - 1)\n+\n+\n+def coala(k):\n+ return k * k\ndiff --git a/coverage.xml + b/coverage.xml\nindex c9d2dd5..d407637 100644\n--- a/coverage.xml\n+++ b/coverage.xml\n@@ + -1,5 +1,5 @@\n \n-\n+\n \t\n \t\n \t\n" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Length: + - '965' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/vnd.github.v3.diff; charset=utf-8 + Date: + - Thu, 08 Aug 2019 03:15:12 GMT + Etag: + - '"2501945a0ce1d37f67c174508ab4e027"' + Last-Modified: + - Thu, 10 Jan 2019 01:39:55 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3; param=diff + X-Github-Request-Id: + - 24E1:6A14:2BE8:4190:5D4B93BF + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4999' + X-Ratelimit-Reset: + - '1565237712' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_processor_task_call_should_delete.yaml b/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_processor_task_call_should_delete.yaml new file mode 100644 index 0000000000..4ef50c63a2 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_processor_task_call_should_delete.yaml @@ -0,0 +1,65 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/vnd.github.v3.diff + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 + response: + content: '{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Length: + - '80' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 19 Jun 2023 12:23:50 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=diff + X-GitHub-Request-Id: + - FFA6:B799:A03D1D:A15442:649048D6 + X-RateLimit-Limit: + - '60' + X-RateLimit-Remaining: + - '53' + X-RateLimit-Reset: + - '1687179699' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '7' + X-XSS-Protection: + - '0' + http_version: HTTP/1.1 + status_code: 401 +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_task_call.yaml b/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_task_call.yaml new file mode 100644 index 0000000000..c4e5306bc3 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_task_call.yaml @@ -0,0 +1,103 @@ +interactions: +- request: + body: null + headers: + Accept: [application/vnd.github.v3.diff] + User-Agent: [Default] + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 + response: + body: {string: "diff --git a/awesome/__init__.py b/awesome/__init__.py\nindex + 1da48d3..770f2e8 100644\n--- a/awesome/__init__.py\n+++ b/awesome/__init__.py\n@@ + -10,3 +10,7 @@ def fib(n):\n if n < 2:\n return 1\n return + fib(n - 2) + fib(n - 1)\n+\n+\n+def coala(k):\n+ return k * k\ndiff --git + a/coverage.xml b/coverage.xml\nindex c9d2dd5..d407637 100644\n--- a/coverage.xml\n+++ + b/coverage.xml\n@@ -1,5 +1,5 @@\n \n-\n+\n \t\n \t\n \t\n"} + headers: + - !!python/tuple + - Server + - [GitHub.com] + - !!python/tuple + - Date + - ['Thu, 08 Aug 2019 00:11:48 GMT'] + - !!python/tuple + - Content-Type + - [application/vnd.github.v3.diff; charset=utf-8] + - !!python/tuple + - Content-Length + - ['965'] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Status + - [200 OK] + - !!python/tuple + - X-Ratelimit-Limit + - ['5000'] + - !!python/tuple + - X-Ratelimit-Remaining + - ['4999'] + - !!python/tuple + - X-Ratelimit-Reset + - ['1565226708'] + - !!python/tuple + - Cache-Control + - ['private, max-age=60, s-maxage=60'] + - !!python/tuple + - Vary + - ['Accept, Authorization, Cookie, X-GitHub-OTP'] + - !!python/tuple + - Etag + - ['"2501945a0ce1d37f67c174508ab4e027"'] + - !!python/tuple + - Last-Modified + - ['Thu, 10 Jan 2019 01:39:55 GMT'] + - !!python/tuple + - X-Oauth-Scopes + - ['admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion'] + - !!python/tuple + - X-Accepted-Oauth-Scopes + - [''] + - !!python/tuple + - X-Github-Media-Type + - [github.v3; param=diff] + - !!python/tuple + - Access-Control-Expose-Headers + - ['ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type'] + - !!python/tuple + - Access-Control-Allow-Origin + - ['*'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000; includeSubdomains; preload] + - !!python/tuple + - X-Frame-Options + - [deny] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Xss-Protection + - [1; mode=block] + - !!python/tuple + - Referrer-Policy + - ['origin-when-cross-origin, strict-origin-when-cross-origin'] + - !!python/tuple + - Content-Security-Policy + - [default-src 'none'] + - !!python/tuple + - X-Github-Request-Id + - ['25D8:31F4:6B067:98187:5D4B68C3'] + status: {code: 200, message: OK} + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_task_call_exception_within_individual_upload.yaml b/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_task_call_exception_within_individual_upload.yaml new file mode 100644 index 0000000000..ab370bd5c8 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_task_call_exception_within_individual_upload.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_task_call_existing_chunks.yaml b/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_task_call_existing_chunks.yaml new file mode 100644 index 0000000000..86ade56a7f --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_task_call_existing_chunks.yaml @@ -0,0 +1,84 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/vnd.github.v3.diff + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 + response: + content: "diff --git a/awesome/__init__.py b/awesome/__init__.py\nindex 1da48d3..770f2e8 + 100644\n--- a/awesome/__init__.py\n+++ b/awesome/__init__.py\n@@ -10,3 +10,7 + @@ def fib(n):\n if n < 2:\n return 1\n return fib(n - 2) + + fib(n - 1)\n+\n+\n+def coala(k):\n+ return k * k\ndiff --git a/coverage.xml + b/coverage.xml\nindex c9d2dd5..d407637 100644\n--- a/coverage.xml\n+++ b/coverage.xml\n@@ + -1,5 +1,5 @@\n \n-\n+\n \t\n \t\n \t\n" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Length: + - '965' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/vnd.github.v3.diff; charset=utf-8 + Date: + - Thu, 08 Aug 2019 00:11:48 GMT + Etag: + - '"2501945a0ce1d37f67c174508ab4e027"' + Last-Modified: + - Thu, 10 Jan 2019 01:39:55 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3; param=diff + X-Github-Request-Id: + - 25D9:0C3B:10430C:1852FE:5D4B68C4 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4998' + X-Ratelimit-Reset: + - '1565226708' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_task_call_with_try_later.yaml b/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_task_call_with_try_later.yaml new file mode 100644 index 0000000000..3d09911c1a --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_upload_processing_task/TestUploadProcessorTask/test_upload_task_call_with_try_later.yaml @@ -0,0 +1,103 @@ +interactions: +- request: + body: null + headers: + Accept: [application/vnd.github.v3.diff] + User-Agent: [Default] + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 + response: + body: {string: "diff --git a/awesome/__init__.py b/awesome/__init__.py\nindex + 1da48d3..770f2e8 100644\n--- a/awesome/__init__.py\n+++ b/awesome/__init__.py\n@@ + -10,3 +10,7 @@ def fib(n):\n if n < 2:\n return 1\n return + fib(n - 2) + fib(n - 1)\n+\n+\n+def coala(k):\n+ return k * k\ndiff --git + a/coverage.xml b/coverage.xml\nindex c9d2dd5..d407637 100644\n--- a/coverage.xml\n+++ + b/coverage.xml\n@@ -1,5 +1,5 @@\n \n-\n+\n \t\n \t\n \t\n"} + headers: + - !!python/tuple + - Date + - ['Thu, 08 Aug 2019 00:11:49 GMT'] + - !!python/tuple + - Content-Type + - [application/vnd.github.v3.diff; charset=utf-8] + - !!python/tuple + - Content-Length + - ['965'] + - !!python/tuple + - Connection + - [close] + - !!python/tuple + - Server + - [GitHub.com] + - !!python/tuple + - Status + - [200 OK] + - !!python/tuple + - X-Ratelimit-Limit + - ['5000'] + - !!python/tuple + - X-Ratelimit-Remaining + - ['4997'] + - !!python/tuple + - X-Ratelimit-Reset + - ['1565226708'] + - !!python/tuple + - Cache-Control + - ['private, max-age=60, s-maxage=60'] + - !!python/tuple + - Vary + - ['Accept, Authorization, Cookie, X-GitHub-OTP', Accept-Encoding] + - !!python/tuple + - Etag + - ['"2501945a0ce1d37f67c174508ab4e027"'] + - !!python/tuple + - Last-Modified + - ['Thu, 10 Jan 2019 01:39:55 GMT'] + - !!python/tuple + - X-Oauth-Scopes + - ['admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion'] + - !!python/tuple + - X-Accepted-Oauth-Scopes + - [''] + - !!python/tuple + - X-Github-Media-Type + - [github.v3; param=diff] + - !!python/tuple + - Access-Control-Expose-Headers + - ['ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type'] + - !!python/tuple + - Access-Control-Allow-Origin + - ['*'] + - !!python/tuple + - Strict-Transport-Security + - [max-age=31536000; includeSubdomains; preload] + - !!python/tuple + - X-Frame-Options + - [deny] + - !!python/tuple + - X-Content-Type-Options + - [nosniff] + - !!python/tuple + - X-Xss-Protection + - [1; mode=block] + - !!python/tuple + - Referrer-Policy + - ['origin-when-cross-origin, strict-origin-when-cross-origin'] + - !!python/tuple + - Content-Security-Policy + - [default-src 'none'] + - !!python/tuple + - X-Github-Request-Id + - ['25DA:3129:C4415:11AFBC:5D4B68C5'] + status: {code: 200, message: OK} + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call.yaml b/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call.yaml new file mode 100644 index 0000000000..5a4865875b --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call.yaml @@ -0,0 +1,586 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 + response: + content: '{"sha":"abf6d4df662c47e32460020ab14abf9303581429","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmFiZjZkNGRmNjYyYzQ3ZTMyNDYwMDIwYWIxNGFiZjkzMDM1ODE0Mjk=","commit":{"author":{"name":"Thiago + Ribeiro Ramos","email":"thiago@ribeiroramos.com","date":"2019-01-10T01:39:55Z"},"committer":{"name":"Thiago + Ribeiro Ramos","email":"thiago@ribeiroramos.com","date":"2019-01-10T01:39:55Z"},"message":"dsidsahdsahdsa","tree":{"sha":"88796ed5686cfd7dffdb2db34b6b12ccaefde90e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/88796ed5686cfd7dffdb2db34b6b12ccaefde90e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/abf6d4df662c47e32460020ab14abf9303581429","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/commit/abf6d4df662c47e32460020ab14abf9303581429","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429/comments","author":null,"committer":null,"parents":[{"sha":"c5b67303452bbff57cc1f49984339cde39eb1db5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5b67303452bbff57cc1f49984339cde39eb1db5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c5b67303452bbff57cc1f49984339cde39eb1db5"}],"stats":{"total":6,"additions":5,"deletions":1},"files":[{"sha":"770f2e8c26296c665565d336490a5306380b61d4","filename":"awesome/__init__.py","status":"modified","additions":4,"deletions":0,"changes":4,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/awesome/__init__.py","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/abf6d4df662c47e32460020ab14abf9303581429/awesome/__init__.py","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/__init__.py?ref=abf6d4df662c47e32460020ab14abf9303581429","patch":"@@ + -10,3 +10,7 @@ def fib(n):\n if n < 2:\n return 1\n return fib(n + - 2) + fib(n - 1)\n+\n+\n+def coala(k):\n+ return k * k"},{"sha":"d4076376a0834a54939fe778da2f0524e10bace0","filename":"coverage.xml","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/coverage.xml?ref=abf6d4df662c47e32460020ab14abf9303581429","patch":"@@ + -1,5 +1,5 @@\n \n-\n+\n \t\n \t\n \t"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 20 Sep 2019 20:13:19 GMT + Etag: + - W/"0d16d1ae4309299074cb54f04bc3b9f8" + Last-Modified: + - Thu, 10 Jan 2019 01:39:55 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 14D9:02DA:8EFDE6:14CBD25:5D8532DF + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4999' + X-Ratelimit-Reset: + - '1569013999' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1","id":229193531,"node_id":"MDExOlB1bGxSZXF1ZXN0MjI5MTkzNTMx","html_url":"https://github.com/ThiagoCodecov/example-python/pull/1","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/1.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/1.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1","number":1,"state":"closed","locked":false,"title":"Creating + new code for reasons no one knows","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"Why + you ask?\r\n\r\nI dont know","created_at":"2018-11-07T22:44:49Z","updated_at":"2019-09-09T22:23:11Z","closed_at":"2019-09-09T22:23:11Z","merged_at":"2019-09-09T22:23:11Z","merge_commit_sha":"038ac8ac2127baa19a927c67f0d5168d9928abf3","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/119c1907fb266f374b8440bbd70dccbea54daf8f","head":{"label":"ThiagoCodecov:reason/some-testing","ref":"reason/some-testing","sha":"119c1907fb266f374b8440bbd70dccbea54daf8f","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2019-09-20T17:23:07Z","pushed_at":"2019-09-20T17:23:05Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":114,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":6,"license":null,"forks":0,"open_issues":6,"watchers":0,"default_branch":"master"}},"base":{"label":"ThiagoCodecov:master","ref":"master","sha":"68946ef98daec68c7798459150982fc799c87d85","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2019-09-20T17:23:07Z","pushed_at":"2019-09-20T17:23:05Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":114,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":6,"license":null,"forks":0,"open_issues":6,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/1"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/119c1907fb266f374b8440bbd70dccbea54daf8f"}},"author_association":"OWNER","merged":true,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"comments":3,"review_comments":0,"maintainer_can_modify":false,"commits":10,"additions":48,"deletions":6,"changed_files":5}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 20 Sep 2019 20:13:20 GMT + Etag: + - W/"d0859ba060eec061163eb69a9dd154ee" + Last-Modified: + - Thu, 12 Sep 2019 21:16:16 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 14DB:5A2B:EE381A:1E925A2:5D8532E0 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4998' + X-Ratelimit-Reset: + - '1569013999' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits?sha=abf6d4df662c47e32460020ab14abf9303581429 + response: + content: "[{\"sha\":\"abf6d4df662c47e32460020ab14abf9303581429\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmFiZjZkNGRmNjYyYzQ3ZTMyNDYwMDIwYWIxNGFiZjkzMDM1ODE0Mjk=\",\"commit\":{\"author\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:39:55Z\"},\"committer\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:39:55Z\"},\"message\":\"dsidsahdsahdsa\",\"tree\":{\"sha\":\"88796ed5686cfd7dffdb2db34b6b12ccaefde90e\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/88796ed5686cfd7dffdb2db34b6b12ccaefde90e\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/abf6d4df662c47e32460020ab14abf9303581429\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/abf6d4df662c47e32460020ab14abf9303581429\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429/comments\",\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"c5b67303452bbff57cc1f49984339cde39eb1db5\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5b67303452bbff57cc1f49984339cde39eb1db5\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/c5b67303452bbff57cc1f49984339cde39eb1db5\"}]},{\"sha\":\"c5b67303452bbff57cc1f49984339cde39eb1db5\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmM1YjY3MzAzNDUyYmJmZjU3Y2MxZjQ5OTg0MzM5Y2RlMzllYjFkYjU=\",\"commit\":{\"author\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:39:10Z\"},\"committer\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:39:10Z\"},\"message\":\"KLKLK\",\"tree\":{\"sha\":\"7568f07accc434ad1be86a9ca05830ab6926687c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7568f07accc434ad1be86a9ca05830ab6926687c\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c5b67303452bbff57cc1f49984339cde39eb1db5\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5b67303452bbff57cc1f49984339cde39eb1db5\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/c5b67303452bbff57cc1f49984339cde39eb1db5\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5b67303452bbff57cc1f49984339cde39eb1db5/comments\",\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/a11ba05c2991a945048d26bf84511b7a3fb2d82a\"}]},{\"sha\":\"a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmExMWJhMDVjMjk5MWE5NDUwNDhkMjZiZjg0NTExYjdhM2ZiMmQ4MmE=\",\"commit\":{\"author\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:32:13Z\"},\"committer\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:32:13Z\"},\"message\":\"BGSFDS\",\"tree\":{\"sha\":\"08a1a5a77f43b2c7b8b7d007f8f14114db0785bf\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/08a1a5a77f43b2c7b8b7d007f8f14114db0785bf\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a11ba05c2991a945048d26bf84511b7a3fb2d82a/comments\",\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76cf63501628eec3b450290b0fcf3019f9f13b1f\"}]},{\"sha\":\"76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojc2Y2Y2MzUwMTYyOGVlYzNiNDUwMjkwYjBmY2YzMDE5ZjlmMTNiMWY=\",\"commit\":{\"author\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-09T21:36:23Z\"},\"committer\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-09T21:36:23Z\"},\"message\":\"AAAA\",\"tree\":{\"sha\":\"79cc86f88de848974928a905905fea36ddd8089e\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/79cc86f88de848974928a905905fea36ddd8089e\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76cf63501628eec3b450290b0fcf3019f9f13b1f/comments\",\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\"}]},{\"sha\":\"b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmI0N2UwYWM3YTJmN2FiYWE0Y2NkYzYxNDYyYzRlZTM5NjQ4OGVmMjA=\",\"commit\":{\"author\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-09T21:02:43Z\"},\"committer\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-09T21:02:43Z\"},\"message\":\"New + commit man\",\"tree\":{\"sha\":\"5d69d4a04e76adff772f68a92b7042fe195a57b0\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/5d69d4a04e76adff772f68a92b7042fe195a57b0\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20/comments\",\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b92edba44fdd29fcc506317cc3ddeae1a723dd08\"}]},{\"sha\":\"b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmI5MmVkYmE0NGZkZDI5ZmNjNTA2MzE3Y2MzZGRlYWUxYTcyM2RkMDg=\",\"commit\":{\"author\":{\"name\":\"Jerrod\",\"email\":\"jerrod@fundersclub.com\",\"date\":\"2018-07-09T23:51:16Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-07-09T23:51:16Z\"},\"message\":\"Update + README.rst\",\"tree\":{\"sha\":\"0cbac4b22e6b7a239338e6550a59553c9bc76eb0\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/0cbac4b22e6b7a239338e6550a59553c9bc76eb0\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJbQ/T0CRBK7hj4Ov3rIwAAdHIIAGISA3RET3zrQdUjtrsxVc8K\\nGjR/6NYt0xJxRA+tJ5JuRGplJJuVECOADr52eXaRMw+3jvfsqZOt7oKAnU/Q490u\\nwb8V8Y7vOo9doxqrJY6vQKCddjbiRZKD/clwAlBFFO0UJJtRWWANqeD0PHnDyzIG\\nIasWMQyRb1RSMBAg7tIGsBwxzXKBaMr9Y6IVuh2HSLS/mOg124vy9hHKx5L60IyJ\\nvOlcFiEQpWYtFDn9hc+BvEgdaIcKP6mkOo+AGz6uYJ8149ukTwpGZQr8NJgxl4Yx\\nY+gBGy7CurVoFZ4N3JOY94H9RffoYKXJwJmZS01n0y9ar8CG2YjSniFY7x7hJNQ=\\n=SnzP\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree 0cbac4b22e6b7a239338e6550a59553c9bc76eb0\\nparent + c7f608036a3d2e89f8c59989ee213900c1ef39d1\\nauthor Jerrod + 1531180276 -0700\\ncommitter GitHub 1531180276 -0700\\n\\nUpdate + README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08/comments\",\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/c7f608036a3d2e89f8c59989ee213900c1ef39d1\"}]},{\"sha\":\"c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmM3ZjYwODAzNmEzZDJlODlmOGM1OTk4OWVlMjEzOTAwYzFlZjM5ZDE=\",\"commit\":{\"author\":{\"name\":\"Jerrod\",\"email\":\"jerrod@fundersclub.com\",\"date\":\"2018-07-09T23:48:34Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-07-09T23:48:34Z\"},\"message\":\"Update + README.rst\",\"tree\":{\"sha\":\"67a425bb5cdf5dba974649a92b9bd1b332ccbada\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/67a425bb5cdf5dba974649a92b9bd1b332ccbada\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJbQ/RSCRBK7hj4Ov3rIwAAdHIIADaMP9S0JFvlxV7y32ytgpMy\\nHHzFrThiO4KivcY0JNiTLPTD9zdKYKeczLw2fV2GLZU/3Ho/msh9gk+GB07yxJiK\\nSxQxW78XRBNeXMNtN1gQHTB/1XpDMk//uZRFD4CAY3Rf9n8MxKhtLV66vmvmInsu\\n/pErsBSOyZH1plHejRJFloQCbHjwzVB8/OrtoV1P/woVsX6nmX59NHWsMo5rY80W\\n/AEr58FzjXV0b0mQ05q9VjHVhFZqOwh981YWHrgv0Ujxu0z9qpbTAhZx5+JOjAuX\\nR9zILOWUgZ6w7YUXhAgXfqYztYfCZyPiaDPOxgl1RWMPtAh9KvYZzriKuEZgTdk=\\n=utzN\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree 67a425bb5cdf5dba974649a92b9bd1b332ccbada\\nparent + 6895b6479dbe12b5cb3baa02416c6343ddb888b4\\nauthor Jerrod + 1531180114 -0700\\ncommitter GitHub 1531180114 -0700\\n\\nUpdate + README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1/comments\",\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6895b6479dbe12b5cb3baa02416c6343ddb888b4\"}]},{\"sha\":\"6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjY4OTViNjQ3OWRiZTEyYjVjYjNiYWEwMjQxNmM2MzQzZGRiODg4YjQ=\",\"commit\":{\"author\":{\"name\":\"Jerrod\",\"email\":\"jerrod@fundersclub.com\",\"date\":\"2018-07-09T23:39:20Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-07-09T23:39:20Z\"},\"message\":\"Adding + 'include' term if multiple sources\\n\\nbased on a support ticket around multiple + sources\\r\\n\\r\\nhttps://codecov.freshdesk.com/a/tickets/87\",\"tree\":{\"sha\":\"3c47e2b9d9791503b56f0e4f78e76b9d061ad529\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3c47e2b9d9791503b56f0e4f78e76b9d061ad529\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJbQ/IoCRBK7hj4Ov3rIwAAdHIIAGm5AdlM8E0E7TyFKWgwPpjO\\nsxiQswFXWosTZnJAn2NN/JF5aNqxUFLa9mo7Z+jztQuxrWsAFQsNFHf/t90iZi4w\\ne0CkIHJdI8ukcae5/3eP+9h8GyqEq/RcvxYtvW6zYkWAK3Pyqwrs+qwH1MuLsl6E\\n02fgD6T99Pq2V+3S1+dfgU6ot4IrMwT7aR+u9fCM8G4tF4y/5znIzuke6amVt52S\\nUfjnHOHbDxdD4Mkxn8107zX1XmQ4BEzhh1kjTVd3Mean6ye7xsFxFGYHA5Zd1iyM\\nCsmW5waqonRf03m1bQ9pYleufcwpr72iARLiBFhTOcAF6vpdoshO1qmTtsweFno=\\n=vKnQ\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree 3c47e2b9d9791503b56f0e4f78e76b9d061ad529\\nparent + adb252173d2107fad86bcdcbc149884c2dd4c609\\nauthor Jerrod + 1531179560 -0700\\ncommitter GitHub 1531179560 -0700\\n\\nAdding + 'include' term if multiple sources\\n\\nbased on a support ticket around multiple + sources\\r\\n\\r\\nhttps://codecov.freshdesk.com/a/tickets/87\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4/comments\",\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"adb252173d2107fad86bcdcbc149884c2dd4c609\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/adb252173d2107fad86bcdcbc149884c2dd4c609\"}]},{\"sha\":\"adb252173d2107fad86bcdcbc149884c2dd4c609\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmFkYjI1MjE3M2QyMTA3ZmFkODZiY2RjYmMxNDk4ODRjMmRkNGM2MDk=\",\"commit\":{\"author\":{\"name\":\"Thomas + Pedbereznak\",\"email\":\"tom@tomped.com\",\"date\":\"2018-04-26T08:39:32Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-04-26T08:39:32Z\"},\"message\":\"Update + README.rst\",\"tree\":{\"sha\":\"26b90aa0aa92d1d873342d7b95df4ad79f7db8b9\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/26b90aa0aa92d1d873342d7b95df4ad79f7db8b9\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/adb252173d2107fad86bcdcbc149884c2dd4c609\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJa4ZBECRBK7hj4Ov3rIwAAdHIIAEoo6hDo1yVW2e9pe5R6cesa\\nzQrd0cjAMvcjwvdDRAbAHkiNuJtJElO41xjyC4sAthl9zM1Wx1Jo4lc8+4CeJ2Vs\\n3b3PDbwp6MLBJcwfhC/mox0PYPzTFO56r61HJI7T2CkBh9GXHAifXMHhkmYP0y5A\\nGzeOE7FlP7Mz3N7NaXzlSPJbIZPD4X9swR0cqDZCFuD1R48QXi3+IbREUzO4KneM\\nS4KwJQNPWRefH8pEkZBLZ8KFPL0ftXr6YuCKE7ySwoer7uQ0AXVHY5HcLSZD/js/\\n9R7G+7CWkyBivTJAUFzql3j+A/ZiDTnXbEO6lty5K4LpvGD8kbkXTZB6QsIPC+g=\\n=fcYU\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree 26b90aa0aa92d1d873342d7b95df4ad79f7db8b9\\nparent + 6ae5f1795a441884ed2847bb31154814ac01ef38\\nauthor Thomas Pedbereznak + 1524731972 +0200\\ncommitter GitHub 1524731972 +0200\\n\\nUpdate + README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/adb252173d2107fad86bcdcbc149884c2dd4c609\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609/comments\",\"author\":{\"login\":\"TomPed\",\"id\":11602092,\"node_id\":\"MDQ6VXNlcjExNjAyMDky\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/11602092?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/TomPed\",\"html_url\":\"https://github.com/TomPed\",\"followers_url\":\"https://api.github.com/users/TomPed/followers\",\"following_url\":\"https://api.github.com/users/TomPed/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/TomPed/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/TomPed/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/TomPed/subscriptions\",\"organizations_url\":\"https://api.github.com/users/TomPed/orgs\",\"repos_url\":\"https://api.github.com/users/TomPed/repos\",\"events_url\":\"https://api.github.com/users/TomPed/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/TomPed/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"6ae5f1795a441884ed2847bb31154814ac01ef38\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38\"}]},{\"sha\":\"6ae5f1795a441884ed2847bb31154814ac01ef38\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjZhZTVmMTc5NWE0NDE4ODRlZDI4NDdiYjMxMTU0ODE0YWMwMWVmMzg=\",\"commit\":{\"author\":{\"name\":\"Thomas + Pedbereznak\",\"email\":\"tom@tomped.com\",\"date\":\"2018-04-26T08:35:58Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-04-26T08:35:58Z\"},\"message\":\"Update + README.rst\",\"tree\":{\"sha\":\"b5592410a15d7a596a8eaea6399766fbbbe0366c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b5592410a15d7a596a8eaea6399766fbbbe0366c\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6ae5f1795a441884ed2847bb31154814ac01ef38\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJa4Y9uCRBK7hj4Ov3rIwAAdHIIAAyJAC1mOPnKmkDSzraV47Wq\\nXma/2QidKpXnRqKgY6XFBXAH0RIpHpZ3NwGR/L2GH1l7xLjXtOMTvXOCjFBZUwRE\\nLlM9IdoUFyPU2E9P0z0vfGR/nk5QC8PY9lzDwe/N8ZhR0j4M2rTM2ue97om9nJ4e\\nmD+HR2ZwjKA9Z9zFeALgBjokKs44F6oN6lLuPYn06oiCnYB3ytlWJy+vpmEGLhoM\\nL+a/ct2e6O5MmlpbRlKVME4FL0O4wDBMrAaFeeZgQTCl2LKfdsfYScJnypkB7X06\\n6cDtC/TJ436n4PCTBRHVMDNGxzmgMgMFYbCPkJ27BeWlTuKVDcJ2msOV7ZJKqqs=\\n=oGiR\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree b5592410a15d7a596a8eaea6399766fbbbe0366c\\nparent + 8631ea09b9b689de0a348d5abf70bdd7273d2ae3\\nauthor Thomas Pedbereznak + 1524731758 +0200\\ncommitter GitHub 1524731758 +0200\\n\\nUpdate + README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38/comments\",\"author\":{\"login\":\"TomPed\",\"id\":11602092,\"node_id\":\"MDQ6VXNlcjExNjAyMDky\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/11602092?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/TomPed\",\"html_url\":\"https://github.com/TomPed\",\"followers_url\":\"https://api.github.com/users/TomPed/followers\",\"following_url\":\"https://api.github.com/users/TomPed/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/TomPed/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/TomPed/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/TomPed/subscriptions\",\"organizations_url\":\"https://api.github.com/users/TomPed/orgs\",\"repos_url\":\"https://api.github.com/users/TomPed/repos\",\"events_url\":\"https://api.github.com/users/TomPed/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/TomPed/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"}]},{\"sha\":\"8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg2MzFlYTA5YjliNjg5ZGUwYTM0OGQ1YWJmNzBiZGQ3MjczZDJhZTM=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2018-02-13T09:13:36Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-02-13T09:13:36Z\"},\"message\":\"Merge + pull request #31 from Gabswim/fix/typo\\n\\nfixing a typo in the README\",\"tree\":{\"sha\":\"e08452bb815b0a8039d5c326e126798aeaf8898f\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e08452bb815b0a8039d5c326e126798aeaf8898f\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJagqxACRBK7hj4Ov3rIwAAdHIIAHiUfENfdOLuNZ/2IvhBe6oJ\\nkkRIlb0iVOaYEtRm7zaEdj8Jt08IFL2C/jztCnE0Osx1r0K/qGXd2gVmBX6mBlda\\n+NSIdLBOdtTmdJQv/zN9ddht4uGakPOHsHTrodFaMN/nWRn9pDUBu1kQNK664zWo\\nSOBwmDU/zMPDUOpYoC2dmorA1Xze0aaKaA11zDO42jqfyHWmlqYoa6Eaf+TYC4Or\\nT8vjYyIJ6TC27XWWvqW1Rk/lZYGbx6QneIT5XLBmyHBCYt+RAYVB09mrrTYBqwVI\\nY7ZqJubRfkzWwAif6vS1jb1U7iisP1oQep+9j6p8RsViVx6s8qGIKea4HGjxm/8=\\n=7ECB\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree e08452bb815b0a8039d5c326e126798aeaf8898f\\nparent + 48f47f9d1b58ba418fdcd50117fc9781c10a27fb\\nparent 087ede6771099a66dccb968c8aacfa04e9ba27a8\\nauthor + Steve Peak 1518513216 +0100\\ncommitter GitHub + 1518513216 +0100\\n\\nMerge pull request #31 from Gabswim/fix/typo\\n\\nfixing + a typo in the README\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"},{\"sha\":\"087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/087ede6771099a66dccb968c8aacfa04e9ba27a8\"}]},{\"sha\":\"087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjA4N2VkZTY3NzEwOTlhNjZkY2NiOTY4YzhhYWNmYTA0ZTliYTI3YTg=\",\"commit\":{\"author\":{\"name\":\"Gabriel + Legault\",\"email\":\"gablegault1@hotmail.com\",\"date\":\"2018-02-13T01:06:57Z\"},\"committer\":{\"name\":\"Gabriel + Legault\",\"email\":\"gablegault1@hotmail.com\",\"date\":\"2018-02-13T01:06:57Z\"},\"message\":\"fixing + a typo in the README\",\"tree\":{\"sha\":\"e08452bb815b0a8039d5c326e126798aeaf8898f\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e08452bb815b0a8039d5c326e126798aeaf8898f\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8/comments\",\"author\":{\"login\":\"Gabswim\",\"id\":2859712,\"node_id\":\"MDQ6VXNlcjI4NTk3MTI=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2859712?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/Gabswim\",\"html_url\":\"https://github.com/Gabswim\",\"followers_url\":\"https://api.github.com/users/Gabswim/followers\",\"following_url\":\"https://api.github.com/users/Gabswim/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/Gabswim/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/Gabswim/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/Gabswim/subscriptions\",\"organizations_url\":\"https://api.github.com/users/Gabswim/orgs\",\"repos_url\":\"https://api.github.com/users/Gabswim/repos\",\"events_url\":\"https://api.github.com/users/Gabswim/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/Gabswim/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"Gabswim\",\"id\":2859712,\"node_id\":\"MDQ6VXNlcjI4NTk3MTI=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2859712?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/Gabswim\",\"html_url\":\"https://github.com/Gabswim\",\"followers_url\":\"https://api.github.com/users/Gabswim/followers\",\"following_url\":\"https://api.github.com/users/Gabswim/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/Gabswim/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/Gabswim/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/Gabswim/subscriptions\",\"organizations_url\":\"https://api.github.com/users/Gabswim/orgs\",\"repos_url\":\"https://api.github.com/users/Gabswim/repos\",\"events_url\":\"https://api.github.com/users/Gabswim/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/Gabswim/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"}]},{\"sha\":\"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjQ4ZjQ3ZjlkMWI1OGJhNDE4ZmRjZDUwMTE3ZmM5NzgxYzEwYTI3ZmI=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2018-01-30T11:57:49Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-01-30T11:57:49Z\"},\"message\":\"Merge + pull request #30 from Jay54520/master\\n\\n#29/Pytest doc error\",\"tree\":{\"sha\":\"3cdd37198ec70196f94aaae638599c10b95be8a0\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3cdd37198ec70196f94aaae638599c10b95be8a0\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJacF29CRBK7hj4Ov3rIwAAdHIIABlpiW1RCdcVH3F1mUGSPHBa\\n6htdwCoISAHOoiJpYXHv6C3ad0eBX6NCsyhcKw0IFGotqvoMvJo6/vS8cJnvdrkj\\nKhOr478QT2M50XYx+izaey55ckSMG2VFU/0rlnoGgnLsqQ5+tLt8xKU5PjCnYleF\\nSK7l5D8pb2vvMesGDHHrESaHup//flHCLvYCJsCVslhVU4+iAE7xx/s0ln8gVg9K\\n0r+2IKjsleoHxjiHWibWOqaDH6z/WUIE7RrO7JsitFg7/4aX4/JXIzDp+EI8wU8a\\npqvjtl+2Qj5hgr5qa6Wj5Qi4vh+wwhNR/Ujv6eK0hFZwMv5wMQ656eSRTeQH//U=\\n=V6j3\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree 3cdd37198ec70196f94aaae638599c10b95be8a0\\nparent + 76003ff147414ce80d2a14ab5f1b78d165e9a468\\nparent d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\\nauthor + Steve Peak 1517313469 +0100\\ncommitter GitHub + 1517313469 +0100\\n\\nMerge pull request #30 from Jay54520/master\\n\\n#29/Pytest + doc error\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\"},{\"sha\":\"d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"}]},{\"sha\":\"d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmQyY2QyOGQ1M2I3MmQ5OTllOWExOGQ2NjMxOWRmZTRkOWI3MTU1Yzc=\",\"commit\":{\"author\":{\"name\":\"\u63ED\u601D\u654F\",\"email\":\"jsm0834@175game.com\",\"date\":\"2018-01-28T07:12:57Z\"},\"committer\":{\"name\":\"\u63ED\u601D\u654F\",\"email\":\"jsm0834@175game.com\",\"date\":\"2018-01-28T07:12:57Z\"},\"message\":\"#29/Pytest + doc error\",\"tree\":{\"sha\":\"3cdd37198ec70196f94aaae638599c10b95be8a0\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3cdd37198ec70196f94aaae638599c10b95be8a0\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7/comments\",\"author\":{\"login\":\"Jay54520\",\"id\":13315364,\"node_id\":\"MDQ6VXNlcjEzMzE1MzY0\",\"avatar_url\":\"https://avatars2.githubusercontent.com/u/13315364?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/Jay54520\",\"html_url\":\"https://github.com/Jay54520\",\"followers_url\":\"https://api.github.com/users/Jay54520/followers\",\"following_url\":\"https://api.github.com/users/Jay54520/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/Jay54520/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/Jay54520/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/Jay54520/subscriptions\",\"organizations_url\":\"https://api.github.com/users/Jay54520/orgs\",\"repos_url\":\"https://api.github.com/users/Jay54520/repos\",\"events_url\":\"https://api.github.com/users/Jay54520/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/Jay54520/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"Jay54520\",\"id\":13315364,\"node_id\":\"MDQ6VXNlcjEzMzE1MzY0\",\"avatar_url\":\"https://avatars2.githubusercontent.com/u/13315364?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/Jay54520\",\"html_url\":\"https://github.com/Jay54520\",\"followers_url\":\"https://api.github.com/users/Jay54520/followers\",\"following_url\":\"https://api.github.com/users/Jay54520/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/Jay54520/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/Jay54520/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/Jay54520/subscriptions\",\"organizations_url\":\"https://api.github.com/users/Jay54520/orgs\",\"repos_url\":\"https://api.github.com/users/Jay54520/repos\",\"events_url\":\"https://api.github.com/users/Jay54520/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/Jay54520/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\"}]},{\"sha\":\"76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojc2MDAzZmYxNDc0MTRjZTgwZDJhMTRhYjVmMWI3OGQxNjVlOWE0Njg=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2018-01-22T13:40:28Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-01-22T13:40:28Z\"},\"message\":\"Update + README.rst\",\"tree\":{\"sha\":\"eac434ca240c7045eaf41e16b70a56e256467621\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/eac434ca240c7045eaf41e16b70a56e256467621\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJaZenMCRBK7hj4Ov3rIwAAdHIIAEKcjkLuqsx9HOj8kr7APqrJ\\nOm2KpXC0WCvn4QaqDzA2hNDoE7rwYvS+MEV308/39AKAg9GBv9T1GwzyeU6krnBn\\nVWfo6Ee3r+/H61GOEZmqHWoQ140LtvC0Z6wCG5xNTfmrr3eUizqPPi9ePnyTeoHQ\\nBvNEvI3FCqmudfBAWPfCWp4zDKp25siRLJ+jCEV3FZ8OmZ8kE5EvPspZDUFgCTzs\\nNaGYfpPOZoxSPrC3Th9ujHCEEontzTzTDHCBsTJLoZeoWrM25R4kcaBoHzggHHBn\\nvMLwN+kwoq4bdbIhTxoeFqc9eVPtVtG9D7jv0+oheBiu5nKkx44SinVBHpXgQ5w=\\n=yptQ\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree eac434ca240c7045eaf41e16b70a56e256467621\\nparent + f9253b0bf56d0e808ab01fdcc70412ac010f5c34\\nauthor Steve Peak + 1516628428 +0100\\ncommitter GitHub 1516628428 +0100\\n\\nUpdate + README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"}]},{\"sha\":\"f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmY5MjUzYjBiZjU2ZDBlODA4YWIwMWZkY2M3MDQxMmFjMDEwZjVjMzQ=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2017-12-11T10:13:41Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-12-11T10:13:41Z\"},\"message\":\"Merge + pull request #24 from gundalow/README_md_to_rst\\n\\nReadme md to rst\",\"tree\":{\"sha\":\"f80cce2672a08f03dbcd902468341e4c71727a2c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f80cce2672a08f03dbcd902468341e4c71727a2c\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJaLlpVCRBK7hj4Ov3rIwAAdHIIAKPJ0bOZe6GVHVPSbpxQctA9\\nCX3gxTFNxaWR9UW0ikeIYLPQBrvzyTYx0HDIErnSfktBFl02cGKm020SvU2qBWFs\\nwtOWnvxVILMH1xV30Q8n/pqLUomaQkSCu9LO/0w1QDF0BgBEP1F5dJb6lzU5uL7t\\nA4FllQ+UKpebhnI3PBCLK6pEUUN/2S6x76pXRvK6j56c+mHfeuOm66R663ZuTnDa\\neYis+Y4R4AYVMrZ12FCkGRly1BVZGgNtrmYQw0pG3DVplp8k9vjw7WaUbxaPsYFj\\nQ33FRzKIqOdjiXH5AdSX1NIsuJo1Fq459i/utW4rfT5EEDuK8V2ZHe4qUzggjmA=\\n=F9Iq\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree f80cce2672a08f03dbcd902468341e4c71727a2c\\nparent + 1e906160c09128765a75afbcbd60d1cbd3c8d10a\\nparent 2903ade6074f09319c1854850ffee2c254c3e17c\\nauthor + Steve Peak 1512987221 +0100\\ncommitter GitHub + 1512987221 +0100\\n\\nMerge pull request #24 from gundalow/README_md_to_rst\\n\\nReadme + md to rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"},{\"sha\":\"2903ade6074f09319c1854850ffee2c254c3e17c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2903ade6074f09319c1854850ffee2c254c3e17c\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2903ade6074f09319c1854850ffee2c254c3e17c\"}]},{\"sha\":\"2903ade6074f09319c1854850ffee2c254c3e17c\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjI5MDNhZGU2MDc0ZjA5MzE5YzE4NTQ4NTBmZmVlMmMyNTRjM2UxN2M=\",\"commit\":{\"author\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:35:07Z\"},\"committer\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:35:07Z\"},\"message\":\"typo\",\"tree\":{\"sha\":\"f80cce2672a08f03dbcd902468341e4c71727a2c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f80cce2672a08f03dbcd902468341e4c71727a2c\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2903ade6074f09319c1854850ffee2c254c3e17c\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2903ade6074f09319c1854850ffee2c254c3e17c\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2903ade6074f09319c1854850ffee2c254c3e17c\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2903ade6074f09319c1854850ffee2c254c3e17c/comments\",\"author\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/0073e9e074081bc2588b9ee311fc01bc9adfa967\"}]},{\"sha\":\"0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjAwNzNlOWUwNzQwODFiYzI1ODhiOWVlMzExZmMwMWJjOWFkZmE5Njc=\",\"commit\":{\"author\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:33:54Z\"},\"committer\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:33:54Z\"},\"message\":\"Valid + badge\",\"tree\":{\"sha\":\"c6dad62f00e288976a31e2c6c189f18fc23881e4\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c6dad62f00e288976a31e2c6c189f18fc23881e4\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967/comments\",\"author\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"8d437d531af955c068c03f35a1f6f19667c6d215\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d437d531af955c068c03f35a1f6f19667c6d215\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8d437d531af955c068c03f35a1f6f19667c6d215\"}]},{\"sha\":\"8d437d531af955c068c03f35a1f6f19667c6d215\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjhkNDM3ZDUzMWFmOTU1YzA2OGMwM2YzNWExZjZmMTk2NjdjNmQyMTU=\",\"commit\":{\"author\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:28:02Z\"},\"committer\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:28:02Z\"},\"message\":\"Convert + README.md to RST\\n\\n* Update formatting fixes #3\\n* Add example badge (and + how to use)\\n* Make it cleared that bash uploader should be used\",\"tree\":{\"sha\":\"3bdc420145fb2cc4232a0ba454675b84311c4bc4\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3bdc420145fb2cc4232a0ba454675b84311c4bc4\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8d437d531af955c068c03f35a1f6f19667c6d215\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d437d531af955c068c03f35a1f6f19667c6d215\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8d437d531af955c068c03f35a1f6f19667c6d215\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d437d531af955c068c03f35a1f6f19667c6d215/comments\",\"author\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"}]},{\"sha\":\"1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjFlOTA2MTYwYzA5MTI4NzY1YTc1YWZiY2JkNjBkMWNiZDNjOGQxMGE=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2017-10-04T09:43:08Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-10-04T09:43:08Z\"},\"message\":\"Merge + pull request #22 from IanLee1521/patch-1\\n\\nFixed minor typo\",\"tree\":{\"sha\":\"4b1780a3cac3fcfe3356b1da70346f19f63106b7\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4b1780a3cac3fcfe3356b1da70346f19f63106b7\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\"},{\"sha\":\"b066c93c2676bc957d971d2c4188e77b3e383b77\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b066c93c2676bc957d971d2c4188e77b3e383b77\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b066c93c2676bc957d971d2c4188e77b3e383b77\"}]},{\"sha\":\"b066c93c2676bc957d971d2c4188e77b3e383b77\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmIwNjZjOTNjMjY3NmJjOTU3ZDk3MWQyYzQxODhlNzdiM2UzODNiNzc=\",\"commit\":{\"author\":{\"name\":\"Ian + Lee\",\"email\":\"ianlee1521@gmail.com\",\"date\":\"2017-09-29T19:29:42Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-09-29T19:29:42Z\"},\"message\":\"Fixed + minor typo\",\"tree\":{\"sha\":\"4b1780a3cac3fcfe3356b1da70346f19f63106b7\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4b1780a3cac3fcfe3356b1da70346f19f63106b7\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b066c93c2676bc957d971d2c4188e77b3e383b77\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b066c93c2676bc957d971d2c4188e77b3e383b77\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b066c93c2676bc957d971d2c4188e77b3e383b77\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b066c93c2676bc957d971d2c4188e77b3e383b77/comments\",\"author\":{\"login\":\"IanLee1521\",\"id\":828452,\"node_id\":\"MDQ6VXNlcjgyODQ1Mg==\",\"avatar_url\":\"https://avatars0.githubusercontent.com/u/828452?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/IanLee1521\",\"html_url\":\"https://github.com/IanLee1521\",\"followers_url\":\"https://api.github.com/users/IanLee1521/followers\",\"following_url\":\"https://api.github.com/users/IanLee1521/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/IanLee1521/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/IanLee1521/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/IanLee1521/subscriptions\",\"organizations_url\":\"https://api.github.com/users/IanLee1521/orgs\",\"repos_url\":\"https://api.github.com/users/IanLee1521/repos\",\"events_url\":\"https://api.github.com/users/IanLee1521/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/IanLee1521/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\"}]},{\"sha\":\"3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjMyMzA1OTRhN2FhODc4MmZiY2Y1MTMyOWIyMzk1MTE4ZjdjZjBkMTU=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2017-08-30T14:02:20Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-08-30T14:02:20Z\"},\"message\":\"Update + README.md\",\"tree\":{\"sha\":\"2ba18d3d080f553b23501cd8485eae3c50589e6a\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2ba18d3d080f553b23501cd8485eae3c50589e6a\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"}]},{\"sha\":\"6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjY1NjBjYmZmMzNmYmZmODc0MGYwNDA3ZjRjYWMxMDkxYmMyNWU2YWU=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2017-02-02T19:10:16Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-02-02T19:10:16Z\"},\"message\":\"Update + README.md\",\"tree\":{\"sha\":\"39e2c45b98a62d06d6d84222d5a8c244150ec3c4\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/39e2c45b98a62d06d6d84222d5a8c244150ec3c4\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"feb5100831541db79eb83a263986df129573f3de\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/feb5100831541db79eb83a263986df129573f3de\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/feb5100831541db79eb83a263986df129573f3de\"}]},{\"sha\":\"feb5100831541db79eb83a263986df129573f3de\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmZlYjUxMDA4MzE1NDFkYjc5ZWI4M2EyNjM5ODZkZjEyOTU3M2YzZGU=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2017-02-02T19:09:38Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-02-02T19:09:38Z\"},\"message\":\"Merge + pull request #20 from briandant/master\\n\\nAdd further instructions for using + env vars\",\"tree\":{\"sha\":\"be98fa09eb1a8343b44071cdae6aac15e61d0cc9\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/be98fa09eb1a8343b44071cdae6aac15e61d0cc9\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/feb5100831541db79eb83a263986df129573f3de\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/feb5100831541db79eb83a263986df129573f3de\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/feb5100831541db79eb83a263986df129573f3de\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/feb5100831541db79eb83a263986df129573f3de/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"},{\"sha\":\"6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6802411a35f438bf62ee8a1c1928cd36ca50d534\"}]},{\"sha\":\"6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjY4MDI0MTFhMzVmNDM4YmY2MmVlOGExYzE5MjhjZDM2Y2E1MGQ1MzQ=\",\"commit\":{\"author\":{\"name\":\"Brian + Dant\",\"email\":\"briandant@users.noreply.github.com\",\"date\":\"2017-02-02T18:37:42Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-02-02T18:37:42Z\"},\"message\":\"Add + further instructions for using env vars\",\"tree\":{\"sha\":\"be98fa09eb1a8343b44071cdae6aac15e61d0cc9\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/be98fa09eb1a8343b44071cdae6aac15e61d0cc9\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534/comments\",\"author\":{\"login\":\"briandant\",\"id\":1884902,\"node_id\":\"MDQ6VXNlcjE4ODQ5MDI=\",\"avatar_url\":\"https://avatars2.githubusercontent.com/u/1884902?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/briandant\",\"html_url\":\"https://github.com/briandant\",\"followers_url\":\"https://api.github.com/users/briandant/followers\",\"following_url\":\"https://api.github.com/users/briandant/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/briandant/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/briandant/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/briandant/subscriptions\",\"organizations_url\":\"https://api.github.com/users/briandant/orgs\",\"repos_url\":\"https://api.github.com/users/briandant/repos\",\"events_url\":\"https://api.github.com/users/briandant/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/briandant/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"}]},{\"sha\":\"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjNmMjhlOTNjYmMxZDI2NmMzMGE1YmRmNDMxMjZlZDQ0ZmJlYjAwOWQ=\",\"commit\":{\"author\":{\"name\":\"Codecov + Test Bot\",\"email\":\"hello@codecov.io\",\"date\":\"2016-09-29T10:37:07Z\"},\"committer\":{\"name\":\"Codecov + Test Bot\",\"email\":\"hello@codecov.io\",\"date\":\"2016-09-29T10:37:07Z\"},\"message\":\"Circle + build #355\\nhttps://circleci.com/gh/codecov/testsuite/355\\nbash <(curl -s + https://raw.githubusercontent.com/codecov/codecov-bash/master/codecov) -v -u + https://codecov.io\",\"tree\":{\"sha\":\"80c741dbd6916ef10fadd2357efdcac15402af49\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/80c741dbd6916ef10fadd2357efdcac15402af49\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d/comments\",\"author\":{\"login\":\"codecov-test\",\"id\":8485477,\"node_id\":\"MDQ6VXNlcjg0ODU0Nzc=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8485477?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov-test\",\"html_url\":\"https://github.com/codecov-test\",\"followers_url\":\"https://api.github.com/users/codecov-test/followers\",\"following_url\":\"https://api.github.com/users/codecov-test/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/codecov-test/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/codecov-test/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/codecov-test/subscriptions\",\"organizations_url\":\"https://api.github.com/users/codecov-test/orgs\",\"repos_url\":\"https://api.github.com/users/codecov-test/repos\",\"events_url\":\"https://api.github.com/users/codecov-test/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/codecov-test/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"codecov-test\",\"id\":8485477,\"node_id\":\"MDQ6VXNlcjg0ODU0Nzc=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8485477?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov-test\",\"html_url\":\"https://github.com/codecov-test\",\"followers_url\":\"https://api.github.com/users/codecov-test/followers\",\"following_url\":\"https://api.github.com/users/codecov-test/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/codecov-test/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/codecov-test/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/codecov-test/subscriptions\",\"organizations_url\":\"https://api.github.com/users/codecov-test/orgs\",\"repos_url\":\"https://api.github.com/users/codecov-test/repos\",\"events_url\":\"https://api.github.com/users/codecov-test/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/codecov-test/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/63f5740c33f2aac68fd0757f471ab741f9e20a05\"}]},{\"sha\":\"63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjYzZjU3NDBjMzNmMmFhYzY4ZmQwNzU3ZjQ3MWFiNzQxZjllMjBhMDU=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2016-08-26T19:21:23Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2016-08-26T19:21:23Z\"},\"message\":\"Update + README.md\",\"tree\":{\"sha\":\"80c741dbd6916ef10fadd2357efdcac15402af49\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/80c741dbd6916ef10fadd2357efdcac15402af49\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"d61bb41b849de7125ca17fbd37292479648e7fa7\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d61bb41b849de7125ca17fbd37292479648e7fa7\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d61bb41b849de7125ca17fbd37292479648e7fa7\"}]},{\"sha\":\"d61bb41b849de7125ca17fbd37292479648e7fa7\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmQ2MWJiNDFiODQ5ZGU3MTI1Y2ExN2ZiZDM3MjkyNDc5NjQ4ZTdmYTc=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2016-08-16T15:47:11Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2016-08-16T15:47:11Z\"},\"message\":\"Merge + pull request #18 from yurovant/patch-1\\n\\nUpdate README.md\",\"tree\":{\"sha\":\"6f202265ab6e9bbe035e9567729cef9d042faa2d\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6f202265ab6e9bbe035e9567729cef9d042faa2d\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d61bb41b849de7125ca17fbd37292479648e7fa7\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d61bb41b849de7125ca17fbd37292479648e7fa7\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d61bb41b849de7125ca17fbd37292479648e7fa7\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d61bb41b849de7125ca17fbd37292479648e7fa7/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"},{\"sha\":\"2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2d1b772e138f05dbbd577ce0dcf3633577629a76\"}]},{\"sha\":\"2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjJkMWI3NzJlMTM4ZjA1ZGJiZDU3N2NlMGRjZjM2MzM1Nzc2MjlhNzY=\",\"commit\":{\"author\":{\"name\":\"Anton + Yurovskykh\",\"email\":\"anton.yurovskykh@gmail.com\",\"date\":\"2016-08-16T07:35:54Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2016-08-16T07:35:54Z\"},\"message\":\"Update + README.md\\n\\ntypo: priveta --> private\",\"tree\":{\"sha\":\"6f202265ab6e9bbe035e9567729cef9d042faa2d\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6f202265ab6e9bbe035e9567729cef9d042faa2d\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76/comments\",\"author\":{\"login\":\"yurovant\",\"id\":11337124,\"node_id\":\"MDQ6VXNlcjExMzM3MTI0\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/11337124?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/yurovant\",\"html_url\":\"https://github.com/yurovant\",\"followers_url\":\"https://api.github.com/users/yurovant/followers\",\"following_url\":\"https://api.github.com/users/yurovant/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/yurovant/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/yurovant/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/yurovant/subscriptions\",\"organizations_url\":\"https://api.github.com/users/yurovant/orgs\",\"repos_url\":\"https://api.github.com/users/yurovant/repos\",\"events_url\":\"https://api.github.com/users/yurovant/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/yurovant/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"}]},{\"sha\":\"84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg0ZWE4YjliYTBmODEzNGJlMDQ3Nzk3MWU3MmIyMzM5NTlmNWQzYjY=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2016-08-05T16:46:35Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2016-08-05T16:46:35Z\"},\"message\":\"Update + README.md\",\"tree\":{\"sha\":\"9b948b519e86c5089a2c9c00d0a8f326282bc5cd\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9b948b519e86c5089a2c9c00d0a8f326282bc5cd\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"e051e55647e0ecc27539b2dc40fb9b2839383060\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e051e55647e0ecc27539b2dc40fb9b2839383060\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/e051e55647e0ecc27539b2dc40fb9b2839383060\"}]}]" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 20 Sep 2019 20:13:21 GMT + Etag: + - W/"63475b9f822f78699bcbdd5ba8efd3b2" + Last-Modified: + - Thu, 10 Jan 2019 01:39:55 GMT + Link: + - ; + rel="next", ; + rel="last" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 14DC:0EA9:12A3C6D:23DDC35:5D8532E1 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4997' + X-Ratelimit-Reset: + - '1569013999' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits?sha=abf6d4df662c47e32460020ab14abf9303581429 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/contents?ref=abf6d4df662c47e32460020ab14abf9303581429 + response: + content: '[{"name":".coverage","path":".coverage","sha":"23e6e577d9e906f1fa619e8a8672d9537ff27326","size":335,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.coverage?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.coverage","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/23e6e577d9e906f1fa619e8a8672d9537ff27326","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/.coverage","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.coverage?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/23e6e577d9e906f1fa619e8a8672d9537ff27326","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.coverage"}},{"name":".travis.yml","path":".travis.yml","sha":"11d295c04e2feac0a533bf5b1df52b84f8076eec","size":144,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.travis.yml?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.travis.yml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/11d295c04e2feac0a533bf5b1df52b84f8076eec","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/.travis.yml","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.travis.yml?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/11d295c04e2feac0a533bf5b1df52b84f8076eec","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.travis.yml"}},{"name":"README.rst","path":"README.rst","sha":"405d834472636bc2c83dd8bd6818e4b2c2871e86","size":6377,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.rst?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/README.rst","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/405d834472636bc2c83dd8bd6818e4b2c2871e86","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/README.rst","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.rst?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/405d834472636bc2c83dd8bd6818e4b2c2871e86","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/README.rst"}},{"name":"awesome","path":"awesome","sha":"b25dbf08d0d6d35990c0fcec549b91f96cace181","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/awesome","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b25dbf08d0d6d35990c0fcec549b91f96cace181","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b25dbf08d0d6d35990c0fcec549b91f96cace181","html":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/awesome"}},{"name":"coverage.xml","path":"coverage.xml","sha":"d4076376a0834a54939fe778da2f0524e10bace0","size":1846,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/coverage.xml?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/d4076376a0834a54939fe778da2f0524e10bace0","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/coverage.xml?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/d4076376a0834a54939fe778da2f0524e10bace0","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml"}},{"name":"tests","path":"tests","sha":"2b1270d7021356b875c31501edb897038ddd589c","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/tests","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2b1270d7021356b875c31501edb897038ddd589c","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2b1270d7021356b875c31501edb897038ddd589c","html":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/tests"}}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 20 Sep 2019 20:13:22 GMT + Etag: + - W/"88796ed5686cfd7dffdb2db34b6b12ccaefde90e" + Last-Modified: + - Fri, 20 Sep 2019 17:23:07 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 14DD:5F03:12F4BCB:241E55A:5D8532E1 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4996' + X-Ratelimit-Reset: + - '1569013999' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/contents?ref=abf6d4df662c47e32460020ab14abf9303581429 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits + response: + content: '[{"sha":"587662b6e5403ae0d126e0c7839a8d98382c4760","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjU4NzY2MmI2ZTU0MDNhZTBkMTI2ZTBjNzgzOWE4ZDk4MzgyYzQ3NjA=","commit":{"author":{"name":"Thiago + Ribeiro Ramos","email":"thiago@ribeiroramos.com","date":"2018-11-07T22:43:54Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:34:45Z"},"message":"Creating + new code for reasons no one knows","tree":{"sha":"ec56802a37b981f13bdc3c9a56ae68ef82ab424a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ec56802a37b981f13bdc3c9a56ae68ef82ab424a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","html_url":"https://github.com/ThiagoCodecov/example-python/commit/587662b6e5403ae0d126e0c7839a8d98382c4760","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760/comments","author":null,"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"68946ef98daec68c7798459150982fc799c87d85","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/68946ef98daec68c7798459150982fc799c87d85","html_url":"https://github.com/ThiagoCodecov/example-python/commit/68946ef98daec68c7798459150982fc799c87d85"}]},{"sha":"03a8b737cb9d8585076ebdbac7b7235c8da0620d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjAzYThiNzM3Y2I5ZDg1ODUwNzZlYmRiYWM3YjcyMzVjOGRhMDYyMGQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-03-12T02:37:19Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:36:02Z"},"message":"Now + what","tree":{"sha":"51a385e1f575447b0b70fd597596c32c4f5bd172","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/51a385e1f575447b0b70fd597596c32c4f5bd172"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/03a8b737cb9d8585076ebdbac7b7235c8da0620d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"587662b6e5403ae0d126e0c7839a8d98382c4760","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","html_url":"https://github.com/ThiagoCodecov/example-python/commit/587662b6e5403ae0d126e0c7839a8d98382c4760"}]},{"sha":"bf9b57cf7b169806ae2d18d7671aba3825b99203","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmJmOWI1N2NmN2IxNjk4MDZhZTJkMThkNzY3MWFiYTM4MjViOTkyMDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-03-12T02:42:33Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:36:02Z"},"message":"Adding + untested code","tree":{"sha":"ce5383a6feb3e0bf20a4df46ae6c67ec3955723e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ce5383a6feb3e0bf20a4df46ae6c67ec3955723e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bf9b57cf7b169806ae2d18d7671aba3825b99203","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"03a8b737cb9d8585076ebdbac7b7235c8da0620d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/03a8b737cb9d8585076ebdbac7b7235c8da0620d"}]},{"sha":"cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmNlZGUxOWNiMzEwY2Q0Y2RkZmI1ZDg5MjFjYjhkMGNjN2M3YzE1MDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-16T22:02:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-16T22:11:24Z"},"message":"asdadafdsfdsfds","tree":{"sha":"e614247adf8a0705575e9c2170fad7c2848870a0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e614247adf8a0705575e9c2170fad7c2848870a0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"bf9b57cf7b169806ae2d18d7671aba3825b99203","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bf9b57cf7b169806ae2d18d7671aba3825b99203"}]},{"sha":"ea3ada938db123368d62b0133e7c5bb54b5292b9","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmVhM2FkYTkzOGRiMTIzMzY4ZDYyYjAxMzNlN2M1YmI1NGI1MjkyYjk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-19T18:48:19Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-19T18:48:19Z"},"message":"Adding + file t2 haha","tree":{"sha":"9ac6564d515ed2630026080e7cbdad4edfa9eca6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9ac6564d515ed2630026080e7cbdad4edfa9eca6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ea3ada938db123368d62b0133e7c5bb54b5292b9","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503"}]},{"sha":"2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjIwNDhiMjc3ZGQ2NTQyZjE4NGM2YTMwYzNlMmIwZjNlZTVlZWFmNGI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:43:42Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:43:42Z"},"message":"Adding + file t2 haha oooggg","tree":{"sha":"8b8d478591c3125af92ac395e87ddfb37fec5086","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/8b8d478591c3125af92ac395e87ddfb37fec5086"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ea3ada938db123368d62b0133e7c5bb54b5292b9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ea3ada938db123368d62b0133e7c5bb54b5292b9"}]},{"sha":"119de54e3cfdf8227a8556b9f5730c328a1390cd","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjExOWRlNTRlM2NmZGY4MjI3YTg1NTZiOWY1NzMwYzMyOGExMzkwY2Q=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:46:16Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:46:16Z"},"message":"Adding + file t2 haha oooggdsadsdsag","tree":{"sha":"d3868402c41afd8dcafb50e5bfa0e023f35c307e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/d3868402c41afd8dcafb50e5bfa0e023f35c307e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119de54e3cfdf8227a8556b9f5730c328a1390cd","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b"}]},{"sha":"2d55e8501b058b6f25382c4e287f022e8938461f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJkNTVlODUwMWIwNThiNmYyNTM4MmM0ZTI4N2YwMjJlODkzODQ2MWY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-24T21:32:08Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-24T21:32:08Z"},"message":"Adding + file t4 unpredictable","tree":{"sha":"a87f6d6ddd74d6df712bad79cc65d040c408efe8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/a87f6d6ddd74d6df712bad79cc65d040c408efe8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2d55e8501b058b6f25382c4e287f022e8938461f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2d55e8501b058b6f25382c4e287f022e8938461f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"119de54e3cfdf8227a8556b9f5730c328a1390cd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119de54e3cfdf8227a8556b9f5730c328a1390cd"}]},{"sha":"364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjM2NGJkZmJjNzJkNWUwNWI1MjBmMDMyMGIwZDhiMzlmZDllYTY5MmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-28T22:50:25Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-28T22:50:25Z"},"message":"Adding + Makefile","tree":{"sha":"452c48e858913bacb4be63a8e2351c98719406dd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/452c48e858913bacb4be63a8e2351c98719406dd"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2d55e8501b058b6f25382c4e287f022e8938461f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2d55e8501b058b6f25382c4e287f022e8938461f"}]},{"sha":"119c1907fb266f374b8440bbd70dccbea54daf8f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjExOWMxOTA3ZmIyNjZmMzc0Yjg0NDBiYmQ3MGRjY2JlYTU0ZGFmOGY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-09-02T23:07:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-09-02T23:07:56Z"},"message":"Cleaning + some stuff","tree":{"sha":"4995d75a388061164491217b50ee296137150f89","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4995d75a388061164491217b50ee296137150f89"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/119c1907fb266f374b8440bbd70dccbea54daf8f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119c1907fb266f374b8440bbd70dccbea54daf8f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119c1907fb266f374b8440bbd70dccbea54daf8f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119c1907fb266f374b8440bbd70dccbea54daf8f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 10 Jan 2020 03:16:25 GMT + Etag: + - W/"6a255c332b90087940cdb3a3dcd00be9" + Last-Modified: + - Thu, 09 Jan 2020 19:23:25 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 121E:2F1A:23F86D:333A32:5E17EC88 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4996' + X-Ratelimit-Reset: + - '1578629688' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits +- request: + body: '{"name": "web", "active": true, "events": ["pull_request", "delete", "push", + "public", "status", "repository"], "config": {"url": "https://codecov.io/webhooks/github", + "secret": "test46nudghi6oft49bay37keyiuhok7", "content_type": "json"}}' + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/hooks + response: + content: '{"message":"Validation Failed","errors":[{"resource":"Hook","code":"custom","message":"Hook + already exists on this repository"}],"documentation_url":"https://developer.github.com/v3/repos/hooks/#create-a-hook"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Connection: + - close + Content-Length: + - '210' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 10 Jan 2020 03:16:25 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 422 Unprocessable Entity + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + X-Accepted-Oauth-Scopes: + - admin:repo_hook, public_repo, repo, write:repo_hook + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 1220:2F19:2E446B:40EB3E:5E17EC89 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4995' + X-Ratelimit-Reset: + - '1578629688' + X-Xss-Protection: + - 1; mode=block + status: + code: 422 + message: Unprocessable Entity + status_code: 422 + url: https://api.github.com/repos/ThiagoCodecov/example-python/hooks +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call_bundle_analysis.yaml b/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call_bundle_analysis.yaml new file mode 100644 index 0000000000..5a4865875b --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call_bundle_analysis.yaml @@ -0,0 +1,586 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 + response: + content: '{"sha":"abf6d4df662c47e32460020ab14abf9303581429","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmFiZjZkNGRmNjYyYzQ3ZTMyNDYwMDIwYWIxNGFiZjkzMDM1ODE0Mjk=","commit":{"author":{"name":"Thiago + Ribeiro Ramos","email":"thiago@ribeiroramos.com","date":"2019-01-10T01:39:55Z"},"committer":{"name":"Thiago + Ribeiro Ramos","email":"thiago@ribeiroramos.com","date":"2019-01-10T01:39:55Z"},"message":"dsidsahdsahdsa","tree":{"sha":"88796ed5686cfd7dffdb2db34b6b12ccaefde90e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/88796ed5686cfd7dffdb2db34b6b12ccaefde90e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/abf6d4df662c47e32460020ab14abf9303581429","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/commit/abf6d4df662c47e32460020ab14abf9303581429","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429/comments","author":null,"committer":null,"parents":[{"sha":"c5b67303452bbff57cc1f49984339cde39eb1db5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5b67303452bbff57cc1f49984339cde39eb1db5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c5b67303452bbff57cc1f49984339cde39eb1db5"}],"stats":{"total":6,"additions":5,"deletions":1},"files":[{"sha":"770f2e8c26296c665565d336490a5306380b61d4","filename":"awesome/__init__.py","status":"modified","additions":4,"deletions":0,"changes":4,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/awesome/__init__.py","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/abf6d4df662c47e32460020ab14abf9303581429/awesome/__init__.py","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/__init__.py?ref=abf6d4df662c47e32460020ab14abf9303581429","patch":"@@ + -10,3 +10,7 @@ def fib(n):\n if n < 2:\n return 1\n return fib(n + - 2) + fib(n - 1)\n+\n+\n+def coala(k):\n+ return k * k"},{"sha":"d4076376a0834a54939fe778da2f0524e10bace0","filename":"coverage.xml","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/coverage.xml?ref=abf6d4df662c47e32460020ab14abf9303581429","patch":"@@ + -1,5 +1,5 @@\n \n-\n+\n \t\n \t\n \t"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 20 Sep 2019 20:13:19 GMT + Etag: + - W/"0d16d1ae4309299074cb54f04bc3b9f8" + Last-Modified: + - Thu, 10 Jan 2019 01:39:55 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 14D9:02DA:8EFDE6:14CBD25:5D8532DF + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4999' + X-Ratelimit-Reset: + - '1569013999' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1","id":229193531,"node_id":"MDExOlB1bGxSZXF1ZXN0MjI5MTkzNTMx","html_url":"https://github.com/ThiagoCodecov/example-python/pull/1","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/1.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/1.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1","number":1,"state":"closed","locked":false,"title":"Creating + new code for reasons no one knows","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"Why + you ask?\r\n\r\nI dont know","created_at":"2018-11-07T22:44:49Z","updated_at":"2019-09-09T22:23:11Z","closed_at":"2019-09-09T22:23:11Z","merged_at":"2019-09-09T22:23:11Z","merge_commit_sha":"038ac8ac2127baa19a927c67f0d5168d9928abf3","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/119c1907fb266f374b8440bbd70dccbea54daf8f","head":{"label":"ThiagoCodecov:reason/some-testing","ref":"reason/some-testing","sha":"119c1907fb266f374b8440bbd70dccbea54daf8f","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2019-09-20T17:23:07Z","pushed_at":"2019-09-20T17:23:05Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":114,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":6,"license":null,"forks":0,"open_issues":6,"watchers":0,"default_branch":"master"}},"base":{"label":"ThiagoCodecov:master","ref":"master","sha":"68946ef98daec68c7798459150982fc799c87d85","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2019-09-20T17:23:07Z","pushed_at":"2019-09-20T17:23:05Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":114,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":6,"license":null,"forks":0,"open_issues":6,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/1"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/119c1907fb266f374b8440bbd70dccbea54daf8f"}},"author_association":"OWNER","merged":true,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"comments":3,"review_comments":0,"maintainer_can_modify":false,"commits":10,"additions":48,"deletions":6,"changed_files":5}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 20 Sep 2019 20:13:20 GMT + Etag: + - W/"d0859ba060eec061163eb69a9dd154ee" + Last-Modified: + - Thu, 12 Sep 2019 21:16:16 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 14DB:5A2B:EE381A:1E925A2:5D8532E0 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4998' + X-Ratelimit-Reset: + - '1569013999' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits?sha=abf6d4df662c47e32460020ab14abf9303581429 + response: + content: "[{\"sha\":\"abf6d4df662c47e32460020ab14abf9303581429\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmFiZjZkNGRmNjYyYzQ3ZTMyNDYwMDIwYWIxNGFiZjkzMDM1ODE0Mjk=\",\"commit\":{\"author\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:39:55Z\"},\"committer\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:39:55Z\"},\"message\":\"dsidsahdsahdsa\",\"tree\":{\"sha\":\"88796ed5686cfd7dffdb2db34b6b12ccaefde90e\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/88796ed5686cfd7dffdb2db34b6b12ccaefde90e\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/abf6d4df662c47e32460020ab14abf9303581429\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/abf6d4df662c47e32460020ab14abf9303581429\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429/comments\",\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"c5b67303452bbff57cc1f49984339cde39eb1db5\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5b67303452bbff57cc1f49984339cde39eb1db5\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/c5b67303452bbff57cc1f49984339cde39eb1db5\"}]},{\"sha\":\"c5b67303452bbff57cc1f49984339cde39eb1db5\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmM1YjY3MzAzNDUyYmJmZjU3Y2MxZjQ5OTg0MzM5Y2RlMzllYjFkYjU=\",\"commit\":{\"author\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:39:10Z\"},\"committer\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:39:10Z\"},\"message\":\"KLKLK\",\"tree\":{\"sha\":\"7568f07accc434ad1be86a9ca05830ab6926687c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7568f07accc434ad1be86a9ca05830ab6926687c\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c5b67303452bbff57cc1f49984339cde39eb1db5\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5b67303452bbff57cc1f49984339cde39eb1db5\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/c5b67303452bbff57cc1f49984339cde39eb1db5\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5b67303452bbff57cc1f49984339cde39eb1db5/comments\",\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/a11ba05c2991a945048d26bf84511b7a3fb2d82a\"}]},{\"sha\":\"a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmExMWJhMDVjMjk5MWE5NDUwNDhkMjZiZjg0NTExYjdhM2ZiMmQ4MmE=\",\"commit\":{\"author\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:32:13Z\"},\"committer\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:32:13Z\"},\"message\":\"BGSFDS\",\"tree\":{\"sha\":\"08a1a5a77f43b2c7b8b7d007f8f14114db0785bf\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/08a1a5a77f43b2c7b8b7d007f8f14114db0785bf\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a11ba05c2991a945048d26bf84511b7a3fb2d82a/comments\",\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76cf63501628eec3b450290b0fcf3019f9f13b1f\"}]},{\"sha\":\"76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojc2Y2Y2MzUwMTYyOGVlYzNiNDUwMjkwYjBmY2YzMDE5ZjlmMTNiMWY=\",\"commit\":{\"author\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-09T21:36:23Z\"},\"committer\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-09T21:36:23Z\"},\"message\":\"AAAA\",\"tree\":{\"sha\":\"79cc86f88de848974928a905905fea36ddd8089e\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/79cc86f88de848974928a905905fea36ddd8089e\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76cf63501628eec3b450290b0fcf3019f9f13b1f/comments\",\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\"}]},{\"sha\":\"b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmI0N2UwYWM3YTJmN2FiYWE0Y2NkYzYxNDYyYzRlZTM5NjQ4OGVmMjA=\",\"commit\":{\"author\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-09T21:02:43Z\"},\"committer\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-09T21:02:43Z\"},\"message\":\"New + commit man\",\"tree\":{\"sha\":\"5d69d4a04e76adff772f68a92b7042fe195a57b0\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/5d69d4a04e76adff772f68a92b7042fe195a57b0\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20/comments\",\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b92edba44fdd29fcc506317cc3ddeae1a723dd08\"}]},{\"sha\":\"b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmI5MmVkYmE0NGZkZDI5ZmNjNTA2MzE3Y2MzZGRlYWUxYTcyM2RkMDg=\",\"commit\":{\"author\":{\"name\":\"Jerrod\",\"email\":\"jerrod@fundersclub.com\",\"date\":\"2018-07-09T23:51:16Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-07-09T23:51:16Z\"},\"message\":\"Update + README.rst\",\"tree\":{\"sha\":\"0cbac4b22e6b7a239338e6550a59553c9bc76eb0\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/0cbac4b22e6b7a239338e6550a59553c9bc76eb0\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJbQ/T0CRBK7hj4Ov3rIwAAdHIIAGISA3RET3zrQdUjtrsxVc8K\\nGjR/6NYt0xJxRA+tJ5JuRGplJJuVECOADr52eXaRMw+3jvfsqZOt7oKAnU/Q490u\\nwb8V8Y7vOo9doxqrJY6vQKCddjbiRZKD/clwAlBFFO0UJJtRWWANqeD0PHnDyzIG\\nIasWMQyRb1RSMBAg7tIGsBwxzXKBaMr9Y6IVuh2HSLS/mOg124vy9hHKx5L60IyJ\\nvOlcFiEQpWYtFDn9hc+BvEgdaIcKP6mkOo+AGz6uYJ8149ukTwpGZQr8NJgxl4Yx\\nY+gBGy7CurVoFZ4N3JOY94H9RffoYKXJwJmZS01n0y9ar8CG2YjSniFY7x7hJNQ=\\n=SnzP\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree 0cbac4b22e6b7a239338e6550a59553c9bc76eb0\\nparent + c7f608036a3d2e89f8c59989ee213900c1ef39d1\\nauthor Jerrod + 1531180276 -0700\\ncommitter GitHub 1531180276 -0700\\n\\nUpdate + README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08/comments\",\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/c7f608036a3d2e89f8c59989ee213900c1ef39d1\"}]},{\"sha\":\"c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmM3ZjYwODAzNmEzZDJlODlmOGM1OTk4OWVlMjEzOTAwYzFlZjM5ZDE=\",\"commit\":{\"author\":{\"name\":\"Jerrod\",\"email\":\"jerrod@fundersclub.com\",\"date\":\"2018-07-09T23:48:34Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-07-09T23:48:34Z\"},\"message\":\"Update + README.rst\",\"tree\":{\"sha\":\"67a425bb5cdf5dba974649a92b9bd1b332ccbada\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/67a425bb5cdf5dba974649a92b9bd1b332ccbada\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJbQ/RSCRBK7hj4Ov3rIwAAdHIIADaMP9S0JFvlxV7y32ytgpMy\\nHHzFrThiO4KivcY0JNiTLPTD9zdKYKeczLw2fV2GLZU/3Ho/msh9gk+GB07yxJiK\\nSxQxW78XRBNeXMNtN1gQHTB/1XpDMk//uZRFD4CAY3Rf9n8MxKhtLV66vmvmInsu\\n/pErsBSOyZH1plHejRJFloQCbHjwzVB8/OrtoV1P/woVsX6nmX59NHWsMo5rY80W\\n/AEr58FzjXV0b0mQ05q9VjHVhFZqOwh981YWHrgv0Ujxu0z9qpbTAhZx5+JOjAuX\\nR9zILOWUgZ6w7YUXhAgXfqYztYfCZyPiaDPOxgl1RWMPtAh9KvYZzriKuEZgTdk=\\n=utzN\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree 67a425bb5cdf5dba974649a92b9bd1b332ccbada\\nparent + 6895b6479dbe12b5cb3baa02416c6343ddb888b4\\nauthor Jerrod + 1531180114 -0700\\ncommitter GitHub 1531180114 -0700\\n\\nUpdate + README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1/comments\",\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6895b6479dbe12b5cb3baa02416c6343ddb888b4\"}]},{\"sha\":\"6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjY4OTViNjQ3OWRiZTEyYjVjYjNiYWEwMjQxNmM2MzQzZGRiODg4YjQ=\",\"commit\":{\"author\":{\"name\":\"Jerrod\",\"email\":\"jerrod@fundersclub.com\",\"date\":\"2018-07-09T23:39:20Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-07-09T23:39:20Z\"},\"message\":\"Adding + 'include' term if multiple sources\\n\\nbased on a support ticket around multiple + sources\\r\\n\\r\\nhttps://codecov.freshdesk.com/a/tickets/87\",\"tree\":{\"sha\":\"3c47e2b9d9791503b56f0e4f78e76b9d061ad529\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3c47e2b9d9791503b56f0e4f78e76b9d061ad529\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJbQ/IoCRBK7hj4Ov3rIwAAdHIIAGm5AdlM8E0E7TyFKWgwPpjO\\nsxiQswFXWosTZnJAn2NN/JF5aNqxUFLa9mo7Z+jztQuxrWsAFQsNFHf/t90iZi4w\\ne0CkIHJdI8ukcae5/3eP+9h8GyqEq/RcvxYtvW6zYkWAK3Pyqwrs+qwH1MuLsl6E\\n02fgD6T99Pq2V+3S1+dfgU6ot4IrMwT7aR+u9fCM8G4tF4y/5znIzuke6amVt52S\\nUfjnHOHbDxdD4Mkxn8107zX1XmQ4BEzhh1kjTVd3Mean6ye7xsFxFGYHA5Zd1iyM\\nCsmW5waqonRf03m1bQ9pYleufcwpr72iARLiBFhTOcAF6vpdoshO1qmTtsweFno=\\n=vKnQ\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree 3c47e2b9d9791503b56f0e4f78e76b9d061ad529\\nparent + adb252173d2107fad86bcdcbc149884c2dd4c609\\nauthor Jerrod + 1531179560 -0700\\ncommitter GitHub 1531179560 -0700\\n\\nAdding + 'include' term if multiple sources\\n\\nbased on a support ticket around multiple + sources\\r\\n\\r\\nhttps://codecov.freshdesk.com/a/tickets/87\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4/comments\",\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"adb252173d2107fad86bcdcbc149884c2dd4c609\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/adb252173d2107fad86bcdcbc149884c2dd4c609\"}]},{\"sha\":\"adb252173d2107fad86bcdcbc149884c2dd4c609\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmFkYjI1MjE3M2QyMTA3ZmFkODZiY2RjYmMxNDk4ODRjMmRkNGM2MDk=\",\"commit\":{\"author\":{\"name\":\"Thomas + Pedbereznak\",\"email\":\"tom@tomped.com\",\"date\":\"2018-04-26T08:39:32Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-04-26T08:39:32Z\"},\"message\":\"Update + README.rst\",\"tree\":{\"sha\":\"26b90aa0aa92d1d873342d7b95df4ad79f7db8b9\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/26b90aa0aa92d1d873342d7b95df4ad79f7db8b9\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/adb252173d2107fad86bcdcbc149884c2dd4c609\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJa4ZBECRBK7hj4Ov3rIwAAdHIIAEoo6hDo1yVW2e9pe5R6cesa\\nzQrd0cjAMvcjwvdDRAbAHkiNuJtJElO41xjyC4sAthl9zM1Wx1Jo4lc8+4CeJ2Vs\\n3b3PDbwp6MLBJcwfhC/mox0PYPzTFO56r61HJI7T2CkBh9GXHAifXMHhkmYP0y5A\\nGzeOE7FlP7Mz3N7NaXzlSPJbIZPD4X9swR0cqDZCFuD1R48QXi3+IbREUzO4KneM\\nS4KwJQNPWRefH8pEkZBLZ8KFPL0ftXr6YuCKE7ySwoer7uQ0AXVHY5HcLSZD/js/\\n9R7G+7CWkyBivTJAUFzql3j+A/ZiDTnXbEO6lty5K4LpvGD8kbkXTZB6QsIPC+g=\\n=fcYU\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree 26b90aa0aa92d1d873342d7b95df4ad79f7db8b9\\nparent + 6ae5f1795a441884ed2847bb31154814ac01ef38\\nauthor Thomas Pedbereznak + 1524731972 +0200\\ncommitter GitHub 1524731972 +0200\\n\\nUpdate + README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/adb252173d2107fad86bcdcbc149884c2dd4c609\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609/comments\",\"author\":{\"login\":\"TomPed\",\"id\":11602092,\"node_id\":\"MDQ6VXNlcjExNjAyMDky\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/11602092?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/TomPed\",\"html_url\":\"https://github.com/TomPed\",\"followers_url\":\"https://api.github.com/users/TomPed/followers\",\"following_url\":\"https://api.github.com/users/TomPed/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/TomPed/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/TomPed/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/TomPed/subscriptions\",\"organizations_url\":\"https://api.github.com/users/TomPed/orgs\",\"repos_url\":\"https://api.github.com/users/TomPed/repos\",\"events_url\":\"https://api.github.com/users/TomPed/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/TomPed/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"6ae5f1795a441884ed2847bb31154814ac01ef38\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38\"}]},{\"sha\":\"6ae5f1795a441884ed2847bb31154814ac01ef38\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjZhZTVmMTc5NWE0NDE4ODRlZDI4NDdiYjMxMTU0ODE0YWMwMWVmMzg=\",\"commit\":{\"author\":{\"name\":\"Thomas + Pedbereznak\",\"email\":\"tom@tomped.com\",\"date\":\"2018-04-26T08:35:58Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-04-26T08:35:58Z\"},\"message\":\"Update + README.rst\",\"tree\":{\"sha\":\"b5592410a15d7a596a8eaea6399766fbbbe0366c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b5592410a15d7a596a8eaea6399766fbbbe0366c\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6ae5f1795a441884ed2847bb31154814ac01ef38\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJa4Y9uCRBK7hj4Ov3rIwAAdHIIAAyJAC1mOPnKmkDSzraV47Wq\\nXma/2QidKpXnRqKgY6XFBXAH0RIpHpZ3NwGR/L2GH1l7xLjXtOMTvXOCjFBZUwRE\\nLlM9IdoUFyPU2E9P0z0vfGR/nk5QC8PY9lzDwe/N8ZhR0j4M2rTM2ue97om9nJ4e\\nmD+HR2ZwjKA9Z9zFeALgBjokKs44F6oN6lLuPYn06oiCnYB3ytlWJy+vpmEGLhoM\\nL+a/ct2e6O5MmlpbRlKVME4FL0O4wDBMrAaFeeZgQTCl2LKfdsfYScJnypkB7X06\\n6cDtC/TJ436n4PCTBRHVMDNGxzmgMgMFYbCPkJ27BeWlTuKVDcJ2msOV7ZJKqqs=\\n=oGiR\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree b5592410a15d7a596a8eaea6399766fbbbe0366c\\nparent + 8631ea09b9b689de0a348d5abf70bdd7273d2ae3\\nauthor Thomas Pedbereznak + 1524731758 +0200\\ncommitter GitHub 1524731758 +0200\\n\\nUpdate + README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38/comments\",\"author\":{\"login\":\"TomPed\",\"id\":11602092,\"node_id\":\"MDQ6VXNlcjExNjAyMDky\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/11602092?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/TomPed\",\"html_url\":\"https://github.com/TomPed\",\"followers_url\":\"https://api.github.com/users/TomPed/followers\",\"following_url\":\"https://api.github.com/users/TomPed/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/TomPed/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/TomPed/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/TomPed/subscriptions\",\"organizations_url\":\"https://api.github.com/users/TomPed/orgs\",\"repos_url\":\"https://api.github.com/users/TomPed/repos\",\"events_url\":\"https://api.github.com/users/TomPed/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/TomPed/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"}]},{\"sha\":\"8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg2MzFlYTA5YjliNjg5ZGUwYTM0OGQ1YWJmNzBiZGQ3MjczZDJhZTM=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2018-02-13T09:13:36Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-02-13T09:13:36Z\"},\"message\":\"Merge + pull request #31 from Gabswim/fix/typo\\n\\nfixing a typo in the README\",\"tree\":{\"sha\":\"e08452bb815b0a8039d5c326e126798aeaf8898f\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e08452bb815b0a8039d5c326e126798aeaf8898f\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJagqxACRBK7hj4Ov3rIwAAdHIIAHiUfENfdOLuNZ/2IvhBe6oJ\\nkkRIlb0iVOaYEtRm7zaEdj8Jt08IFL2C/jztCnE0Osx1r0K/qGXd2gVmBX6mBlda\\n+NSIdLBOdtTmdJQv/zN9ddht4uGakPOHsHTrodFaMN/nWRn9pDUBu1kQNK664zWo\\nSOBwmDU/zMPDUOpYoC2dmorA1Xze0aaKaA11zDO42jqfyHWmlqYoa6Eaf+TYC4Or\\nT8vjYyIJ6TC27XWWvqW1Rk/lZYGbx6QneIT5XLBmyHBCYt+RAYVB09mrrTYBqwVI\\nY7ZqJubRfkzWwAif6vS1jb1U7iisP1oQep+9j6p8RsViVx6s8qGIKea4HGjxm/8=\\n=7ECB\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree e08452bb815b0a8039d5c326e126798aeaf8898f\\nparent + 48f47f9d1b58ba418fdcd50117fc9781c10a27fb\\nparent 087ede6771099a66dccb968c8aacfa04e9ba27a8\\nauthor + Steve Peak 1518513216 +0100\\ncommitter GitHub + 1518513216 +0100\\n\\nMerge pull request #31 from Gabswim/fix/typo\\n\\nfixing + a typo in the README\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"},{\"sha\":\"087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/087ede6771099a66dccb968c8aacfa04e9ba27a8\"}]},{\"sha\":\"087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjA4N2VkZTY3NzEwOTlhNjZkY2NiOTY4YzhhYWNmYTA0ZTliYTI3YTg=\",\"commit\":{\"author\":{\"name\":\"Gabriel + Legault\",\"email\":\"gablegault1@hotmail.com\",\"date\":\"2018-02-13T01:06:57Z\"},\"committer\":{\"name\":\"Gabriel + Legault\",\"email\":\"gablegault1@hotmail.com\",\"date\":\"2018-02-13T01:06:57Z\"},\"message\":\"fixing + a typo in the README\",\"tree\":{\"sha\":\"e08452bb815b0a8039d5c326e126798aeaf8898f\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e08452bb815b0a8039d5c326e126798aeaf8898f\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8/comments\",\"author\":{\"login\":\"Gabswim\",\"id\":2859712,\"node_id\":\"MDQ6VXNlcjI4NTk3MTI=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2859712?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/Gabswim\",\"html_url\":\"https://github.com/Gabswim\",\"followers_url\":\"https://api.github.com/users/Gabswim/followers\",\"following_url\":\"https://api.github.com/users/Gabswim/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/Gabswim/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/Gabswim/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/Gabswim/subscriptions\",\"organizations_url\":\"https://api.github.com/users/Gabswim/orgs\",\"repos_url\":\"https://api.github.com/users/Gabswim/repos\",\"events_url\":\"https://api.github.com/users/Gabswim/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/Gabswim/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"Gabswim\",\"id\":2859712,\"node_id\":\"MDQ6VXNlcjI4NTk3MTI=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2859712?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/Gabswim\",\"html_url\":\"https://github.com/Gabswim\",\"followers_url\":\"https://api.github.com/users/Gabswim/followers\",\"following_url\":\"https://api.github.com/users/Gabswim/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/Gabswim/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/Gabswim/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/Gabswim/subscriptions\",\"organizations_url\":\"https://api.github.com/users/Gabswim/orgs\",\"repos_url\":\"https://api.github.com/users/Gabswim/repos\",\"events_url\":\"https://api.github.com/users/Gabswim/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/Gabswim/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"}]},{\"sha\":\"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjQ4ZjQ3ZjlkMWI1OGJhNDE4ZmRjZDUwMTE3ZmM5NzgxYzEwYTI3ZmI=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2018-01-30T11:57:49Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-01-30T11:57:49Z\"},\"message\":\"Merge + pull request #30 from Jay54520/master\\n\\n#29/Pytest doc error\",\"tree\":{\"sha\":\"3cdd37198ec70196f94aaae638599c10b95be8a0\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3cdd37198ec70196f94aaae638599c10b95be8a0\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJacF29CRBK7hj4Ov3rIwAAdHIIABlpiW1RCdcVH3F1mUGSPHBa\\n6htdwCoISAHOoiJpYXHv6C3ad0eBX6NCsyhcKw0IFGotqvoMvJo6/vS8cJnvdrkj\\nKhOr478QT2M50XYx+izaey55ckSMG2VFU/0rlnoGgnLsqQ5+tLt8xKU5PjCnYleF\\nSK7l5D8pb2vvMesGDHHrESaHup//flHCLvYCJsCVslhVU4+iAE7xx/s0ln8gVg9K\\n0r+2IKjsleoHxjiHWibWOqaDH6z/WUIE7RrO7JsitFg7/4aX4/JXIzDp+EI8wU8a\\npqvjtl+2Qj5hgr5qa6Wj5Qi4vh+wwhNR/Ujv6eK0hFZwMv5wMQ656eSRTeQH//U=\\n=V6j3\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree 3cdd37198ec70196f94aaae638599c10b95be8a0\\nparent + 76003ff147414ce80d2a14ab5f1b78d165e9a468\\nparent d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\\nauthor + Steve Peak 1517313469 +0100\\ncommitter GitHub + 1517313469 +0100\\n\\nMerge pull request #30 from Jay54520/master\\n\\n#29/Pytest + doc error\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\"},{\"sha\":\"d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"}]},{\"sha\":\"d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmQyY2QyOGQ1M2I3MmQ5OTllOWExOGQ2NjMxOWRmZTRkOWI3MTU1Yzc=\",\"commit\":{\"author\":{\"name\":\"\u63ED\u601D\u654F\",\"email\":\"jsm0834@175game.com\",\"date\":\"2018-01-28T07:12:57Z\"},\"committer\":{\"name\":\"\u63ED\u601D\u654F\",\"email\":\"jsm0834@175game.com\",\"date\":\"2018-01-28T07:12:57Z\"},\"message\":\"#29/Pytest + doc error\",\"tree\":{\"sha\":\"3cdd37198ec70196f94aaae638599c10b95be8a0\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3cdd37198ec70196f94aaae638599c10b95be8a0\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7/comments\",\"author\":{\"login\":\"Jay54520\",\"id\":13315364,\"node_id\":\"MDQ6VXNlcjEzMzE1MzY0\",\"avatar_url\":\"https://avatars2.githubusercontent.com/u/13315364?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/Jay54520\",\"html_url\":\"https://github.com/Jay54520\",\"followers_url\":\"https://api.github.com/users/Jay54520/followers\",\"following_url\":\"https://api.github.com/users/Jay54520/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/Jay54520/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/Jay54520/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/Jay54520/subscriptions\",\"organizations_url\":\"https://api.github.com/users/Jay54520/orgs\",\"repos_url\":\"https://api.github.com/users/Jay54520/repos\",\"events_url\":\"https://api.github.com/users/Jay54520/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/Jay54520/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"Jay54520\",\"id\":13315364,\"node_id\":\"MDQ6VXNlcjEzMzE1MzY0\",\"avatar_url\":\"https://avatars2.githubusercontent.com/u/13315364?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/Jay54520\",\"html_url\":\"https://github.com/Jay54520\",\"followers_url\":\"https://api.github.com/users/Jay54520/followers\",\"following_url\":\"https://api.github.com/users/Jay54520/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/Jay54520/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/Jay54520/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/Jay54520/subscriptions\",\"organizations_url\":\"https://api.github.com/users/Jay54520/orgs\",\"repos_url\":\"https://api.github.com/users/Jay54520/repos\",\"events_url\":\"https://api.github.com/users/Jay54520/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/Jay54520/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\"}]},{\"sha\":\"76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojc2MDAzZmYxNDc0MTRjZTgwZDJhMTRhYjVmMWI3OGQxNjVlOWE0Njg=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2018-01-22T13:40:28Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-01-22T13:40:28Z\"},\"message\":\"Update + README.rst\",\"tree\":{\"sha\":\"eac434ca240c7045eaf41e16b70a56e256467621\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/eac434ca240c7045eaf41e16b70a56e256467621\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJaZenMCRBK7hj4Ov3rIwAAdHIIAEKcjkLuqsx9HOj8kr7APqrJ\\nOm2KpXC0WCvn4QaqDzA2hNDoE7rwYvS+MEV308/39AKAg9GBv9T1GwzyeU6krnBn\\nVWfo6Ee3r+/H61GOEZmqHWoQ140LtvC0Z6wCG5xNTfmrr3eUizqPPi9ePnyTeoHQ\\nBvNEvI3FCqmudfBAWPfCWp4zDKp25siRLJ+jCEV3FZ8OmZ8kE5EvPspZDUFgCTzs\\nNaGYfpPOZoxSPrC3Th9ujHCEEontzTzTDHCBsTJLoZeoWrM25R4kcaBoHzggHHBn\\nvMLwN+kwoq4bdbIhTxoeFqc9eVPtVtG9D7jv0+oheBiu5nKkx44SinVBHpXgQ5w=\\n=yptQ\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree eac434ca240c7045eaf41e16b70a56e256467621\\nparent + f9253b0bf56d0e808ab01fdcc70412ac010f5c34\\nauthor Steve Peak + 1516628428 +0100\\ncommitter GitHub 1516628428 +0100\\n\\nUpdate + README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"}]},{\"sha\":\"f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmY5MjUzYjBiZjU2ZDBlODA4YWIwMWZkY2M3MDQxMmFjMDEwZjVjMzQ=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2017-12-11T10:13:41Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-12-11T10:13:41Z\"},\"message\":\"Merge + pull request #24 from gundalow/README_md_to_rst\\n\\nReadme md to rst\",\"tree\":{\"sha\":\"f80cce2672a08f03dbcd902468341e4c71727a2c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f80cce2672a08f03dbcd902468341e4c71727a2c\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJaLlpVCRBK7hj4Ov3rIwAAdHIIAKPJ0bOZe6GVHVPSbpxQctA9\\nCX3gxTFNxaWR9UW0ikeIYLPQBrvzyTYx0HDIErnSfktBFl02cGKm020SvU2qBWFs\\nwtOWnvxVILMH1xV30Q8n/pqLUomaQkSCu9LO/0w1QDF0BgBEP1F5dJb6lzU5uL7t\\nA4FllQ+UKpebhnI3PBCLK6pEUUN/2S6x76pXRvK6j56c+mHfeuOm66R663ZuTnDa\\neYis+Y4R4AYVMrZ12FCkGRly1BVZGgNtrmYQw0pG3DVplp8k9vjw7WaUbxaPsYFj\\nQ33FRzKIqOdjiXH5AdSX1NIsuJo1Fq459i/utW4rfT5EEDuK8V2ZHe4qUzggjmA=\\n=F9Iq\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree f80cce2672a08f03dbcd902468341e4c71727a2c\\nparent + 1e906160c09128765a75afbcbd60d1cbd3c8d10a\\nparent 2903ade6074f09319c1854850ffee2c254c3e17c\\nauthor + Steve Peak 1512987221 +0100\\ncommitter GitHub + 1512987221 +0100\\n\\nMerge pull request #24 from gundalow/README_md_to_rst\\n\\nReadme + md to rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"},{\"sha\":\"2903ade6074f09319c1854850ffee2c254c3e17c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2903ade6074f09319c1854850ffee2c254c3e17c\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2903ade6074f09319c1854850ffee2c254c3e17c\"}]},{\"sha\":\"2903ade6074f09319c1854850ffee2c254c3e17c\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjI5MDNhZGU2MDc0ZjA5MzE5YzE4NTQ4NTBmZmVlMmMyNTRjM2UxN2M=\",\"commit\":{\"author\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:35:07Z\"},\"committer\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:35:07Z\"},\"message\":\"typo\",\"tree\":{\"sha\":\"f80cce2672a08f03dbcd902468341e4c71727a2c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f80cce2672a08f03dbcd902468341e4c71727a2c\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2903ade6074f09319c1854850ffee2c254c3e17c\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2903ade6074f09319c1854850ffee2c254c3e17c\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2903ade6074f09319c1854850ffee2c254c3e17c\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2903ade6074f09319c1854850ffee2c254c3e17c/comments\",\"author\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/0073e9e074081bc2588b9ee311fc01bc9adfa967\"}]},{\"sha\":\"0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjAwNzNlOWUwNzQwODFiYzI1ODhiOWVlMzExZmMwMWJjOWFkZmE5Njc=\",\"commit\":{\"author\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:33:54Z\"},\"committer\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:33:54Z\"},\"message\":\"Valid + badge\",\"tree\":{\"sha\":\"c6dad62f00e288976a31e2c6c189f18fc23881e4\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c6dad62f00e288976a31e2c6c189f18fc23881e4\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967/comments\",\"author\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"8d437d531af955c068c03f35a1f6f19667c6d215\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d437d531af955c068c03f35a1f6f19667c6d215\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8d437d531af955c068c03f35a1f6f19667c6d215\"}]},{\"sha\":\"8d437d531af955c068c03f35a1f6f19667c6d215\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjhkNDM3ZDUzMWFmOTU1YzA2OGMwM2YzNWExZjZmMTk2NjdjNmQyMTU=\",\"commit\":{\"author\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:28:02Z\"},\"committer\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:28:02Z\"},\"message\":\"Convert + README.md to RST\\n\\n* Update formatting fixes #3\\n* Add example badge (and + how to use)\\n* Make it cleared that bash uploader should be used\",\"tree\":{\"sha\":\"3bdc420145fb2cc4232a0ba454675b84311c4bc4\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3bdc420145fb2cc4232a0ba454675b84311c4bc4\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8d437d531af955c068c03f35a1f6f19667c6d215\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d437d531af955c068c03f35a1f6f19667c6d215\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8d437d531af955c068c03f35a1f6f19667c6d215\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d437d531af955c068c03f35a1f6f19667c6d215/comments\",\"author\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"}]},{\"sha\":\"1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjFlOTA2MTYwYzA5MTI4NzY1YTc1YWZiY2JkNjBkMWNiZDNjOGQxMGE=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2017-10-04T09:43:08Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-10-04T09:43:08Z\"},\"message\":\"Merge + pull request #22 from IanLee1521/patch-1\\n\\nFixed minor typo\",\"tree\":{\"sha\":\"4b1780a3cac3fcfe3356b1da70346f19f63106b7\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4b1780a3cac3fcfe3356b1da70346f19f63106b7\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\"},{\"sha\":\"b066c93c2676bc957d971d2c4188e77b3e383b77\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b066c93c2676bc957d971d2c4188e77b3e383b77\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b066c93c2676bc957d971d2c4188e77b3e383b77\"}]},{\"sha\":\"b066c93c2676bc957d971d2c4188e77b3e383b77\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmIwNjZjOTNjMjY3NmJjOTU3ZDk3MWQyYzQxODhlNzdiM2UzODNiNzc=\",\"commit\":{\"author\":{\"name\":\"Ian + Lee\",\"email\":\"ianlee1521@gmail.com\",\"date\":\"2017-09-29T19:29:42Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-09-29T19:29:42Z\"},\"message\":\"Fixed + minor typo\",\"tree\":{\"sha\":\"4b1780a3cac3fcfe3356b1da70346f19f63106b7\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4b1780a3cac3fcfe3356b1da70346f19f63106b7\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b066c93c2676bc957d971d2c4188e77b3e383b77\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b066c93c2676bc957d971d2c4188e77b3e383b77\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b066c93c2676bc957d971d2c4188e77b3e383b77\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b066c93c2676bc957d971d2c4188e77b3e383b77/comments\",\"author\":{\"login\":\"IanLee1521\",\"id\":828452,\"node_id\":\"MDQ6VXNlcjgyODQ1Mg==\",\"avatar_url\":\"https://avatars0.githubusercontent.com/u/828452?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/IanLee1521\",\"html_url\":\"https://github.com/IanLee1521\",\"followers_url\":\"https://api.github.com/users/IanLee1521/followers\",\"following_url\":\"https://api.github.com/users/IanLee1521/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/IanLee1521/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/IanLee1521/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/IanLee1521/subscriptions\",\"organizations_url\":\"https://api.github.com/users/IanLee1521/orgs\",\"repos_url\":\"https://api.github.com/users/IanLee1521/repos\",\"events_url\":\"https://api.github.com/users/IanLee1521/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/IanLee1521/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\"}]},{\"sha\":\"3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjMyMzA1OTRhN2FhODc4MmZiY2Y1MTMyOWIyMzk1MTE4ZjdjZjBkMTU=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2017-08-30T14:02:20Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-08-30T14:02:20Z\"},\"message\":\"Update + README.md\",\"tree\":{\"sha\":\"2ba18d3d080f553b23501cd8485eae3c50589e6a\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2ba18d3d080f553b23501cd8485eae3c50589e6a\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"}]},{\"sha\":\"6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjY1NjBjYmZmMzNmYmZmODc0MGYwNDA3ZjRjYWMxMDkxYmMyNWU2YWU=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2017-02-02T19:10:16Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-02-02T19:10:16Z\"},\"message\":\"Update + README.md\",\"tree\":{\"sha\":\"39e2c45b98a62d06d6d84222d5a8c244150ec3c4\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/39e2c45b98a62d06d6d84222d5a8c244150ec3c4\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"feb5100831541db79eb83a263986df129573f3de\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/feb5100831541db79eb83a263986df129573f3de\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/feb5100831541db79eb83a263986df129573f3de\"}]},{\"sha\":\"feb5100831541db79eb83a263986df129573f3de\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmZlYjUxMDA4MzE1NDFkYjc5ZWI4M2EyNjM5ODZkZjEyOTU3M2YzZGU=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2017-02-02T19:09:38Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-02-02T19:09:38Z\"},\"message\":\"Merge + pull request #20 from briandant/master\\n\\nAdd further instructions for using + env vars\",\"tree\":{\"sha\":\"be98fa09eb1a8343b44071cdae6aac15e61d0cc9\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/be98fa09eb1a8343b44071cdae6aac15e61d0cc9\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/feb5100831541db79eb83a263986df129573f3de\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/feb5100831541db79eb83a263986df129573f3de\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/feb5100831541db79eb83a263986df129573f3de\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/feb5100831541db79eb83a263986df129573f3de/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"},{\"sha\":\"6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6802411a35f438bf62ee8a1c1928cd36ca50d534\"}]},{\"sha\":\"6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjY4MDI0MTFhMzVmNDM4YmY2MmVlOGExYzE5MjhjZDM2Y2E1MGQ1MzQ=\",\"commit\":{\"author\":{\"name\":\"Brian + Dant\",\"email\":\"briandant@users.noreply.github.com\",\"date\":\"2017-02-02T18:37:42Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-02-02T18:37:42Z\"},\"message\":\"Add + further instructions for using env vars\",\"tree\":{\"sha\":\"be98fa09eb1a8343b44071cdae6aac15e61d0cc9\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/be98fa09eb1a8343b44071cdae6aac15e61d0cc9\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534/comments\",\"author\":{\"login\":\"briandant\",\"id\":1884902,\"node_id\":\"MDQ6VXNlcjE4ODQ5MDI=\",\"avatar_url\":\"https://avatars2.githubusercontent.com/u/1884902?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/briandant\",\"html_url\":\"https://github.com/briandant\",\"followers_url\":\"https://api.github.com/users/briandant/followers\",\"following_url\":\"https://api.github.com/users/briandant/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/briandant/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/briandant/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/briandant/subscriptions\",\"organizations_url\":\"https://api.github.com/users/briandant/orgs\",\"repos_url\":\"https://api.github.com/users/briandant/repos\",\"events_url\":\"https://api.github.com/users/briandant/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/briandant/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"}]},{\"sha\":\"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjNmMjhlOTNjYmMxZDI2NmMzMGE1YmRmNDMxMjZlZDQ0ZmJlYjAwOWQ=\",\"commit\":{\"author\":{\"name\":\"Codecov + Test Bot\",\"email\":\"hello@codecov.io\",\"date\":\"2016-09-29T10:37:07Z\"},\"committer\":{\"name\":\"Codecov + Test Bot\",\"email\":\"hello@codecov.io\",\"date\":\"2016-09-29T10:37:07Z\"},\"message\":\"Circle + build #355\\nhttps://circleci.com/gh/codecov/testsuite/355\\nbash <(curl -s + https://raw.githubusercontent.com/codecov/codecov-bash/master/codecov) -v -u + https://codecov.io\",\"tree\":{\"sha\":\"80c741dbd6916ef10fadd2357efdcac15402af49\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/80c741dbd6916ef10fadd2357efdcac15402af49\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d/comments\",\"author\":{\"login\":\"codecov-test\",\"id\":8485477,\"node_id\":\"MDQ6VXNlcjg0ODU0Nzc=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8485477?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov-test\",\"html_url\":\"https://github.com/codecov-test\",\"followers_url\":\"https://api.github.com/users/codecov-test/followers\",\"following_url\":\"https://api.github.com/users/codecov-test/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/codecov-test/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/codecov-test/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/codecov-test/subscriptions\",\"organizations_url\":\"https://api.github.com/users/codecov-test/orgs\",\"repos_url\":\"https://api.github.com/users/codecov-test/repos\",\"events_url\":\"https://api.github.com/users/codecov-test/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/codecov-test/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"codecov-test\",\"id\":8485477,\"node_id\":\"MDQ6VXNlcjg0ODU0Nzc=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8485477?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov-test\",\"html_url\":\"https://github.com/codecov-test\",\"followers_url\":\"https://api.github.com/users/codecov-test/followers\",\"following_url\":\"https://api.github.com/users/codecov-test/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/codecov-test/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/codecov-test/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/codecov-test/subscriptions\",\"organizations_url\":\"https://api.github.com/users/codecov-test/orgs\",\"repos_url\":\"https://api.github.com/users/codecov-test/repos\",\"events_url\":\"https://api.github.com/users/codecov-test/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/codecov-test/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/63f5740c33f2aac68fd0757f471ab741f9e20a05\"}]},{\"sha\":\"63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjYzZjU3NDBjMzNmMmFhYzY4ZmQwNzU3ZjQ3MWFiNzQxZjllMjBhMDU=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2016-08-26T19:21:23Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2016-08-26T19:21:23Z\"},\"message\":\"Update + README.md\",\"tree\":{\"sha\":\"80c741dbd6916ef10fadd2357efdcac15402af49\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/80c741dbd6916ef10fadd2357efdcac15402af49\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"d61bb41b849de7125ca17fbd37292479648e7fa7\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d61bb41b849de7125ca17fbd37292479648e7fa7\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d61bb41b849de7125ca17fbd37292479648e7fa7\"}]},{\"sha\":\"d61bb41b849de7125ca17fbd37292479648e7fa7\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmQ2MWJiNDFiODQ5ZGU3MTI1Y2ExN2ZiZDM3MjkyNDc5NjQ4ZTdmYTc=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2016-08-16T15:47:11Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2016-08-16T15:47:11Z\"},\"message\":\"Merge + pull request #18 from yurovant/patch-1\\n\\nUpdate README.md\",\"tree\":{\"sha\":\"6f202265ab6e9bbe035e9567729cef9d042faa2d\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6f202265ab6e9bbe035e9567729cef9d042faa2d\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d61bb41b849de7125ca17fbd37292479648e7fa7\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d61bb41b849de7125ca17fbd37292479648e7fa7\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d61bb41b849de7125ca17fbd37292479648e7fa7\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d61bb41b849de7125ca17fbd37292479648e7fa7/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"},{\"sha\":\"2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2d1b772e138f05dbbd577ce0dcf3633577629a76\"}]},{\"sha\":\"2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjJkMWI3NzJlMTM4ZjA1ZGJiZDU3N2NlMGRjZjM2MzM1Nzc2MjlhNzY=\",\"commit\":{\"author\":{\"name\":\"Anton + Yurovskykh\",\"email\":\"anton.yurovskykh@gmail.com\",\"date\":\"2016-08-16T07:35:54Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2016-08-16T07:35:54Z\"},\"message\":\"Update + README.md\\n\\ntypo: priveta --> private\",\"tree\":{\"sha\":\"6f202265ab6e9bbe035e9567729cef9d042faa2d\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6f202265ab6e9bbe035e9567729cef9d042faa2d\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76/comments\",\"author\":{\"login\":\"yurovant\",\"id\":11337124,\"node_id\":\"MDQ6VXNlcjExMzM3MTI0\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/11337124?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/yurovant\",\"html_url\":\"https://github.com/yurovant\",\"followers_url\":\"https://api.github.com/users/yurovant/followers\",\"following_url\":\"https://api.github.com/users/yurovant/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/yurovant/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/yurovant/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/yurovant/subscriptions\",\"organizations_url\":\"https://api.github.com/users/yurovant/orgs\",\"repos_url\":\"https://api.github.com/users/yurovant/repos\",\"events_url\":\"https://api.github.com/users/yurovant/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/yurovant/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"}]},{\"sha\":\"84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg0ZWE4YjliYTBmODEzNGJlMDQ3Nzk3MWU3MmIyMzM5NTlmNWQzYjY=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2016-08-05T16:46:35Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2016-08-05T16:46:35Z\"},\"message\":\"Update + README.md\",\"tree\":{\"sha\":\"9b948b519e86c5089a2c9c00d0a8f326282bc5cd\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9b948b519e86c5089a2c9c00d0a8f326282bc5cd\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"e051e55647e0ecc27539b2dc40fb9b2839383060\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e051e55647e0ecc27539b2dc40fb9b2839383060\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/e051e55647e0ecc27539b2dc40fb9b2839383060\"}]}]" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 20 Sep 2019 20:13:21 GMT + Etag: + - W/"63475b9f822f78699bcbdd5ba8efd3b2" + Last-Modified: + - Thu, 10 Jan 2019 01:39:55 GMT + Link: + - ; + rel="next", ; + rel="last" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 14DC:0EA9:12A3C6D:23DDC35:5D8532E1 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4997' + X-Ratelimit-Reset: + - '1569013999' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits?sha=abf6d4df662c47e32460020ab14abf9303581429 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/contents?ref=abf6d4df662c47e32460020ab14abf9303581429 + response: + content: '[{"name":".coverage","path":".coverage","sha":"23e6e577d9e906f1fa619e8a8672d9537ff27326","size":335,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.coverage?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.coverage","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/23e6e577d9e906f1fa619e8a8672d9537ff27326","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/.coverage","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.coverage?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/23e6e577d9e906f1fa619e8a8672d9537ff27326","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.coverage"}},{"name":".travis.yml","path":".travis.yml","sha":"11d295c04e2feac0a533bf5b1df52b84f8076eec","size":144,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.travis.yml?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.travis.yml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/11d295c04e2feac0a533bf5b1df52b84f8076eec","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/.travis.yml","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.travis.yml?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/11d295c04e2feac0a533bf5b1df52b84f8076eec","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.travis.yml"}},{"name":"README.rst","path":"README.rst","sha":"405d834472636bc2c83dd8bd6818e4b2c2871e86","size":6377,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.rst?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/README.rst","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/405d834472636bc2c83dd8bd6818e4b2c2871e86","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/README.rst","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.rst?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/405d834472636bc2c83dd8bd6818e4b2c2871e86","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/README.rst"}},{"name":"awesome","path":"awesome","sha":"b25dbf08d0d6d35990c0fcec549b91f96cace181","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/awesome","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b25dbf08d0d6d35990c0fcec549b91f96cace181","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b25dbf08d0d6d35990c0fcec549b91f96cace181","html":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/awesome"}},{"name":"coverage.xml","path":"coverage.xml","sha":"d4076376a0834a54939fe778da2f0524e10bace0","size":1846,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/coverage.xml?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/d4076376a0834a54939fe778da2f0524e10bace0","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/coverage.xml?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/d4076376a0834a54939fe778da2f0524e10bace0","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml"}},{"name":"tests","path":"tests","sha":"2b1270d7021356b875c31501edb897038ddd589c","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/tests","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2b1270d7021356b875c31501edb897038ddd589c","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2b1270d7021356b875c31501edb897038ddd589c","html":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/tests"}}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 20 Sep 2019 20:13:22 GMT + Etag: + - W/"88796ed5686cfd7dffdb2db34b6b12ccaefde90e" + Last-Modified: + - Fri, 20 Sep 2019 17:23:07 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 14DD:5F03:12F4BCB:241E55A:5D8532E1 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4996' + X-Ratelimit-Reset: + - '1569013999' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/contents?ref=abf6d4df662c47e32460020ab14abf9303581429 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits + response: + content: '[{"sha":"587662b6e5403ae0d126e0c7839a8d98382c4760","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjU4NzY2MmI2ZTU0MDNhZTBkMTI2ZTBjNzgzOWE4ZDk4MzgyYzQ3NjA=","commit":{"author":{"name":"Thiago + Ribeiro Ramos","email":"thiago@ribeiroramos.com","date":"2018-11-07T22:43:54Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:34:45Z"},"message":"Creating + new code for reasons no one knows","tree":{"sha":"ec56802a37b981f13bdc3c9a56ae68ef82ab424a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ec56802a37b981f13bdc3c9a56ae68ef82ab424a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","html_url":"https://github.com/ThiagoCodecov/example-python/commit/587662b6e5403ae0d126e0c7839a8d98382c4760","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760/comments","author":null,"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"68946ef98daec68c7798459150982fc799c87d85","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/68946ef98daec68c7798459150982fc799c87d85","html_url":"https://github.com/ThiagoCodecov/example-python/commit/68946ef98daec68c7798459150982fc799c87d85"}]},{"sha":"03a8b737cb9d8585076ebdbac7b7235c8da0620d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjAzYThiNzM3Y2I5ZDg1ODUwNzZlYmRiYWM3YjcyMzVjOGRhMDYyMGQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-03-12T02:37:19Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:36:02Z"},"message":"Now + what","tree":{"sha":"51a385e1f575447b0b70fd597596c32c4f5bd172","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/51a385e1f575447b0b70fd597596c32c4f5bd172"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/03a8b737cb9d8585076ebdbac7b7235c8da0620d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"587662b6e5403ae0d126e0c7839a8d98382c4760","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","html_url":"https://github.com/ThiagoCodecov/example-python/commit/587662b6e5403ae0d126e0c7839a8d98382c4760"}]},{"sha":"bf9b57cf7b169806ae2d18d7671aba3825b99203","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmJmOWI1N2NmN2IxNjk4MDZhZTJkMThkNzY3MWFiYTM4MjViOTkyMDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-03-12T02:42:33Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:36:02Z"},"message":"Adding + untested code","tree":{"sha":"ce5383a6feb3e0bf20a4df46ae6c67ec3955723e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ce5383a6feb3e0bf20a4df46ae6c67ec3955723e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bf9b57cf7b169806ae2d18d7671aba3825b99203","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"03a8b737cb9d8585076ebdbac7b7235c8da0620d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/03a8b737cb9d8585076ebdbac7b7235c8da0620d"}]},{"sha":"cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmNlZGUxOWNiMzEwY2Q0Y2RkZmI1ZDg5MjFjYjhkMGNjN2M3YzE1MDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-16T22:02:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-16T22:11:24Z"},"message":"asdadafdsfdsfds","tree":{"sha":"e614247adf8a0705575e9c2170fad7c2848870a0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e614247adf8a0705575e9c2170fad7c2848870a0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"bf9b57cf7b169806ae2d18d7671aba3825b99203","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bf9b57cf7b169806ae2d18d7671aba3825b99203"}]},{"sha":"ea3ada938db123368d62b0133e7c5bb54b5292b9","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmVhM2FkYTkzOGRiMTIzMzY4ZDYyYjAxMzNlN2M1YmI1NGI1MjkyYjk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-19T18:48:19Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-19T18:48:19Z"},"message":"Adding + file t2 haha","tree":{"sha":"9ac6564d515ed2630026080e7cbdad4edfa9eca6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9ac6564d515ed2630026080e7cbdad4edfa9eca6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ea3ada938db123368d62b0133e7c5bb54b5292b9","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503"}]},{"sha":"2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjIwNDhiMjc3ZGQ2NTQyZjE4NGM2YTMwYzNlMmIwZjNlZTVlZWFmNGI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:43:42Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:43:42Z"},"message":"Adding + file t2 haha oooggg","tree":{"sha":"8b8d478591c3125af92ac395e87ddfb37fec5086","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/8b8d478591c3125af92ac395e87ddfb37fec5086"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ea3ada938db123368d62b0133e7c5bb54b5292b9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ea3ada938db123368d62b0133e7c5bb54b5292b9"}]},{"sha":"119de54e3cfdf8227a8556b9f5730c328a1390cd","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjExOWRlNTRlM2NmZGY4MjI3YTg1NTZiOWY1NzMwYzMyOGExMzkwY2Q=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:46:16Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:46:16Z"},"message":"Adding + file t2 haha oooggdsadsdsag","tree":{"sha":"d3868402c41afd8dcafb50e5bfa0e023f35c307e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/d3868402c41afd8dcafb50e5bfa0e023f35c307e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119de54e3cfdf8227a8556b9f5730c328a1390cd","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b"}]},{"sha":"2d55e8501b058b6f25382c4e287f022e8938461f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJkNTVlODUwMWIwNThiNmYyNTM4MmM0ZTI4N2YwMjJlODkzODQ2MWY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-24T21:32:08Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-24T21:32:08Z"},"message":"Adding + file t4 unpredictable","tree":{"sha":"a87f6d6ddd74d6df712bad79cc65d040c408efe8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/a87f6d6ddd74d6df712bad79cc65d040c408efe8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2d55e8501b058b6f25382c4e287f022e8938461f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2d55e8501b058b6f25382c4e287f022e8938461f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"119de54e3cfdf8227a8556b9f5730c328a1390cd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119de54e3cfdf8227a8556b9f5730c328a1390cd"}]},{"sha":"364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjM2NGJkZmJjNzJkNWUwNWI1MjBmMDMyMGIwZDhiMzlmZDllYTY5MmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-28T22:50:25Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-28T22:50:25Z"},"message":"Adding + Makefile","tree":{"sha":"452c48e858913bacb4be63a8e2351c98719406dd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/452c48e858913bacb4be63a8e2351c98719406dd"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2d55e8501b058b6f25382c4e287f022e8938461f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2d55e8501b058b6f25382c4e287f022e8938461f"}]},{"sha":"119c1907fb266f374b8440bbd70dccbea54daf8f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjExOWMxOTA3ZmIyNjZmMzc0Yjg0NDBiYmQ3MGRjY2JlYTU0ZGFmOGY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-09-02T23:07:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-09-02T23:07:56Z"},"message":"Cleaning + some stuff","tree":{"sha":"4995d75a388061164491217b50ee296137150f89","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4995d75a388061164491217b50ee296137150f89"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/119c1907fb266f374b8440bbd70dccbea54daf8f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119c1907fb266f374b8440bbd70dccbea54daf8f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119c1907fb266f374b8440bbd70dccbea54daf8f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119c1907fb266f374b8440bbd70dccbea54daf8f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 10 Jan 2020 03:16:25 GMT + Etag: + - W/"6a255c332b90087940cdb3a3dcd00be9" + Last-Modified: + - Thu, 09 Jan 2020 19:23:25 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 121E:2F1A:23F86D:333A32:5E17EC88 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4996' + X-Ratelimit-Reset: + - '1578629688' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits +- request: + body: '{"name": "web", "active": true, "events": ["pull_request", "delete", "push", + "public", "status", "repository"], "config": {"url": "https://codecov.io/webhooks/github", + "secret": "test46nudghi6oft49bay37keyiuhok7", "content_type": "json"}}' + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/hooks + response: + content: '{"message":"Validation Failed","errors":[{"resource":"Hook","code":"custom","message":"Hook + already exists on this repository"}],"documentation_url":"https://developer.github.com/v3/repos/hooks/#create-a-hook"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Connection: + - close + Content-Length: + - '210' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 10 Jan 2020 03:16:25 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 422 Unprocessable Entity + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + X-Accepted-Oauth-Scopes: + - admin:repo_hook, public_repo, repo, write:repo_hook + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 1220:2F19:2E446B:40EB3E:5E17EC89 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4995' + X-Ratelimit-Reset: + - '1578629688' + X-Xss-Protection: + - 1; mode=block + status: + code: 422 + message: Unprocessable Entity + status_code: 422 + url: https://api.github.com/repos/ThiagoCodecov/example-python/hooks +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call_bundle_analysis_no_upload.yaml b/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call_bundle_analysis_no_upload.yaml new file mode 100644 index 0000000000..904d529ba6 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call_bundle_analysis_no_upload.yaml @@ -0,0 +1,254 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 + response: + content: '{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest","status":"401"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Length: + - '95' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 27 Aug 2024 20:45:35 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - github.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - EE09:10B931:BA26EB:1650A97:66CE3AEF + X-RateLimit-Limit: + - '60' + X-RateLimit-Remaining: + - '52' + X-RateLimit-Reset: + - '1724794701' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '8' + X-XSS-Protection: + - '0' + http_version: HTTP/1.1 + status_code: 401 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 + response: + content: '{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest","status":"401"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Length: + - '95' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 27 Aug 2024 20:45:35 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - github.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - EE09:10B931:BA2723:1650AD6:66CE3AEF + X-RateLimit-Limit: + - '60' + X-RateLimit-Remaining: + - '51' + X-RateLimit-Reset: + - '1724794701' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '9' + X-XSS-Protection: + - '0' + http_version: HTTP/1.1 + status_code: 401 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 + response: + content: '{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest","status":"401"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Length: + - '95' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 27 Aug 2024 20:45:35 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - github.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - EE09:10B931:BA2748:1650B11:66CE3AEF + X-RateLimit-Limit: + - '60' + X-RateLimit-Remaining: + - '50' + X-RateLimit-Reset: + - '1724794701' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '10' + X-XSS-Protection: + - '0' + http_version: HTTP/1.1 + status_code: 401 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/contents?ref=abf6d4df662c47e32460020ab14abf9303581429 + response: + content: '{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest","status":"401"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Length: + - '95' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 27 Aug 2024 20:45:35 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - github.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - A42E:33658C:BC8457:1691FEA:66CE3AEF + X-RateLimit-Limit: + - '60' + X-RateLimit-Remaining: + - '49' + X-RateLimit-Reset: + - '1724794701' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '11' + X-XSS-Protection: + - '0' + http_version: HTTP/1.1 + status_code: 401 +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call_multiple_processors.yaml b/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call_multiple_processors.yaml new file mode 100644 index 0000000000..7dad646330 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call_multiple_processors.yaml @@ -0,0 +1,586 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 + response: + content: '{"sha":"abf6d4df662c47e32460020ab14abf9303581429","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmFiZjZkNGRmNjYyYzQ3ZTMyNDYwMDIwYWIxNGFiZjkzMDM1ODE0Mjk=","commit":{"author":{"name":"Thiago + Ribeiro Ramos","email":"thiago@ribeiroramos.com","date":"2019-01-10T01:39:55Z"},"committer":{"name":"Thiago + Ribeiro Ramos","email":"thiago@ribeiroramos.com","date":"2019-01-10T01:39:55Z"},"message":"dsidsahdsahdsa","tree":{"sha":"88796ed5686cfd7dffdb2db34b6b12ccaefde90e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/88796ed5686cfd7dffdb2db34b6b12ccaefde90e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/abf6d4df662c47e32460020ab14abf9303581429","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/commit/abf6d4df662c47e32460020ab14abf9303581429","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429/comments","author":null,"committer":null,"parents":[{"sha":"c5b67303452bbff57cc1f49984339cde39eb1db5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5b67303452bbff57cc1f49984339cde39eb1db5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c5b67303452bbff57cc1f49984339cde39eb1db5"}],"stats":{"total":6,"additions":5,"deletions":1},"files":[{"sha":"770f2e8c26296c665565d336490a5306380b61d4","filename":"awesome/__init__.py","status":"modified","additions":4,"deletions":0,"changes":4,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/awesome/__init__.py","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/abf6d4df662c47e32460020ab14abf9303581429/awesome/__init__.py","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/__init__.py?ref=abf6d4df662c47e32460020ab14abf9303581429","patch":"@@ + -10,3 +10,7 @@ def fib(n):\n if n < 2:\n return 1\n return fib(n + - 2) + fib(n - 1)\n+\n+\n+def coala(k):\n+ return k * k"},{"sha":"d4076376a0834a54939fe778da2f0524e10bace0","filename":"coverage.xml","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/coverage.xml?ref=abf6d4df662c47e32460020ab14abf9303581429","patch":"@@ + -1,5 +1,5 @@\n \n-\n+\n \t\n \t\n \t"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 20 Sep 2019 20:13:22 GMT + Etag: + - W/"0d16d1ae4309299074cb54f04bc3b9f8" + Last-Modified: + - Thu, 10 Jan 2019 01:39:55 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 14DE:0A30:4A17F5:BFA428:5D8532E2 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4995' + X-Ratelimit-Reset: + - '1569013999' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1","id":229193531,"node_id":"MDExOlB1bGxSZXF1ZXN0MjI5MTkzNTMx","html_url":"https://github.com/ThiagoCodecov/example-python/pull/1","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/1.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/1.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1","number":1,"state":"closed","locked":false,"title":"Creating + new code for reasons no one knows","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"Why + you ask?\r\n\r\nI dont know","created_at":"2018-11-07T22:44:49Z","updated_at":"2019-09-09T22:23:11Z","closed_at":"2019-09-09T22:23:11Z","merged_at":"2019-09-09T22:23:11Z","merge_commit_sha":"038ac8ac2127baa19a927c67f0d5168d9928abf3","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/119c1907fb266f374b8440bbd70dccbea54daf8f","head":{"label":"ThiagoCodecov:reason/some-testing","ref":"reason/some-testing","sha":"119c1907fb266f374b8440bbd70dccbea54daf8f","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2019-09-20T17:23:07Z","pushed_at":"2019-09-20T17:23:05Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":114,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":6,"license":null,"forks":0,"open_issues":6,"watchers":0,"default_branch":"master"}},"base":{"label":"ThiagoCodecov:master","ref":"master","sha":"68946ef98daec68c7798459150982fc799c87d85","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2019-09-20T17:23:07Z","pushed_at":"2019-09-20T17:23:05Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":114,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":6,"license":null,"forks":0,"open_issues":6,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/1"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/119c1907fb266f374b8440bbd70dccbea54daf8f"}},"author_association":"OWNER","merged":true,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"comments":3,"review_comments":0,"maintainer_can_modify":false,"commits":10,"additions":48,"deletions":6,"changed_files":5}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 20 Sep 2019 20:13:23 GMT + Etag: + - W/"d0859ba060eec061163eb69a9dd154ee" + Last-Modified: + - Thu, 12 Sep 2019 21:16:16 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 14DF:2B61:82DAF3:13E6C14:5D8532E2 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4994' + X-Ratelimit-Reset: + - '1569013999' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits?sha=abf6d4df662c47e32460020ab14abf9303581429 + response: + content: "[{\"sha\":\"abf6d4df662c47e32460020ab14abf9303581429\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmFiZjZkNGRmNjYyYzQ3ZTMyNDYwMDIwYWIxNGFiZjkzMDM1ODE0Mjk=\",\"commit\":{\"author\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:39:55Z\"},\"committer\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:39:55Z\"},\"message\":\"dsidsahdsahdsa\",\"tree\":{\"sha\":\"88796ed5686cfd7dffdb2db34b6b12ccaefde90e\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/88796ed5686cfd7dffdb2db34b6b12ccaefde90e\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/abf6d4df662c47e32460020ab14abf9303581429\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/abf6d4df662c47e32460020ab14abf9303581429\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429/comments\",\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"c5b67303452bbff57cc1f49984339cde39eb1db5\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5b67303452bbff57cc1f49984339cde39eb1db5\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/c5b67303452bbff57cc1f49984339cde39eb1db5\"}]},{\"sha\":\"c5b67303452bbff57cc1f49984339cde39eb1db5\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmM1YjY3MzAzNDUyYmJmZjU3Y2MxZjQ5OTg0MzM5Y2RlMzllYjFkYjU=\",\"commit\":{\"author\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:39:10Z\"},\"committer\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:39:10Z\"},\"message\":\"KLKLK\",\"tree\":{\"sha\":\"7568f07accc434ad1be86a9ca05830ab6926687c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7568f07accc434ad1be86a9ca05830ab6926687c\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c5b67303452bbff57cc1f49984339cde39eb1db5\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5b67303452bbff57cc1f49984339cde39eb1db5\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/c5b67303452bbff57cc1f49984339cde39eb1db5\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5b67303452bbff57cc1f49984339cde39eb1db5/comments\",\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/a11ba05c2991a945048d26bf84511b7a3fb2d82a\"}]},{\"sha\":\"a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmExMWJhMDVjMjk5MWE5NDUwNDhkMjZiZjg0NTExYjdhM2ZiMmQ4MmE=\",\"commit\":{\"author\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:32:13Z\"},\"committer\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:32:13Z\"},\"message\":\"BGSFDS\",\"tree\":{\"sha\":\"08a1a5a77f43b2c7b8b7d007f8f14114db0785bf\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/08a1a5a77f43b2c7b8b7d007f8f14114db0785bf\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a11ba05c2991a945048d26bf84511b7a3fb2d82a/comments\",\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76cf63501628eec3b450290b0fcf3019f9f13b1f\"}]},{\"sha\":\"76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojc2Y2Y2MzUwMTYyOGVlYzNiNDUwMjkwYjBmY2YzMDE5ZjlmMTNiMWY=\",\"commit\":{\"author\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-09T21:36:23Z\"},\"committer\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-09T21:36:23Z\"},\"message\":\"AAAA\",\"tree\":{\"sha\":\"79cc86f88de848974928a905905fea36ddd8089e\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/79cc86f88de848974928a905905fea36ddd8089e\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76cf63501628eec3b450290b0fcf3019f9f13b1f/comments\",\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\"}]},{\"sha\":\"b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmI0N2UwYWM3YTJmN2FiYWE0Y2NkYzYxNDYyYzRlZTM5NjQ4OGVmMjA=\",\"commit\":{\"author\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-09T21:02:43Z\"},\"committer\":{\"name\":\"Thiago + Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-09T21:02:43Z\"},\"message\":\"New + commit man\",\"tree\":{\"sha\":\"5d69d4a04e76adff772f68a92b7042fe195a57b0\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/5d69d4a04e76adff772f68a92b7042fe195a57b0\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20/comments\",\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b92edba44fdd29fcc506317cc3ddeae1a723dd08\"}]},{\"sha\":\"b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmI5MmVkYmE0NGZkZDI5ZmNjNTA2MzE3Y2MzZGRlYWUxYTcyM2RkMDg=\",\"commit\":{\"author\":{\"name\":\"Jerrod\",\"email\":\"jerrod@fundersclub.com\",\"date\":\"2018-07-09T23:51:16Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-07-09T23:51:16Z\"},\"message\":\"Update + README.rst\",\"tree\":{\"sha\":\"0cbac4b22e6b7a239338e6550a59553c9bc76eb0\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/0cbac4b22e6b7a239338e6550a59553c9bc76eb0\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJbQ/T0CRBK7hj4Ov3rIwAAdHIIAGISA3RET3zrQdUjtrsxVc8K\\nGjR/6NYt0xJxRA+tJ5JuRGplJJuVECOADr52eXaRMw+3jvfsqZOt7oKAnU/Q490u\\nwb8V8Y7vOo9doxqrJY6vQKCddjbiRZKD/clwAlBFFO0UJJtRWWANqeD0PHnDyzIG\\nIasWMQyRb1RSMBAg7tIGsBwxzXKBaMr9Y6IVuh2HSLS/mOg124vy9hHKx5L60IyJ\\nvOlcFiEQpWYtFDn9hc+BvEgdaIcKP6mkOo+AGz6uYJ8149ukTwpGZQr8NJgxl4Yx\\nY+gBGy7CurVoFZ4N3JOY94H9RffoYKXJwJmZS01n0y9ar8CG2YjSniFY7x7hJNQ=\\n=SnzP\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree 0cbac4b22e6b7a239338e6550a59553c9bc76eb0\\nparent + c7f608036a3d2e89f8c59989ee213900c1ef39d1\\nauthor Jerrod + 1531180276 -0700\\ncommitter GitHub 1531180276 -0700\\n\\nUpdate + README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08/comments\",\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/c7f608036a3d2e89f8c59989ee213900c1ef39d1\"}]},{\"sha\":\"c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmM3ZjYwODAzNmEzZDJlODlmOGM1OTk4OWVlMjEzOTAwYzFlZjM5ZDE=\",\"commit\":{\"author\":{\"name\":\"Jerrod\",\"email\":\"jerrod@fundersclub.com\",\"date\":\"2018-07-09T23:48:34Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-07-09T23:48:34Z\"},\"message\":\"Update + README.rst\",\"tree\":{\"sha\":\"67a425bb5cdf5dba974649a92b9bd1b332ccbada\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/67a425bb5cdf5dba974649a92b9bd1b332ccbada\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJbQ/RSCRBK7hj4Ov3rIwAAdHIIADaMP9S0JFvlxV7y32ytgpMy\\nHHzFrThiO4KivcY0JNiTLPTD9zdKYKeczLw2fV2GLZU/3Ho/msh9gk+GB07yxJiK\\nSxQxW78XRBNeXMNtN1gQHTB/1XpDMk//uZRFD4CAY3Rf9n8MxKhtLV66vmvmInsu\\n/pErsBSOyZH1plHejRJFloQCbHjwzVB8/OrtoV1P/woVsX6nmX59NHWsMo5rY80W\\n/AEr58FzjXV0b0mQ05q9VjHVhFZqOwh981YWHrgv0Ujxu0z9qpbTAhZx5+JOjAuX\\nR9zILOWUgZ6w7YUXhAgXfqYztYfCZyPiaDPOxgl1RWMPtAh9KvYZzriKuEZgTdk=\\n=utzN\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree 67a425bb5cdf5dba974649a92b9bd1b332ccbada\\nparent + 6895b6479dbe12b5cb3baa02416c6343ddb888b4\\nauthor Jerrod + 1531180114 -0700\\ncommitter GitHub 1531180114 -0700\\n\\nUpdate + README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1/comments\",\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6895b6479dbe12b5cb3baa02416c6343ddb888b4\"}]},{\"sha\":\"6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjY4OTViNjQ3OWRiZTEyYjVjYjNiYWEwMjQxNmM2MzQzZGRiODg4YjQ=\",\"commit\":{\"author\":{\"name\":\"Jerrod\",\"email\":\"jerrod@fundersclub.com\",\"date\":\"2018-07-09T23:39:20Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-07-09T23:39:20Z\"},\"message\":\"Adding + 'include' term if multiple sources\\n\\nbased on a support ticket around multiple + sources\\r\\n\\r\\nhttps://codecov.freshdesk.com/a/tickets/87\",\"tree\":{\"sha\":\"3c47e2b9d9791503b56f0e4f78e76b9d061ad529\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3c47e2b9d9791503b56f0e4f78e76b9d061ad529\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJbQ/IoCRBK7hj4Ov3rIwAAdHIIAGm5AdlM8E0E7TyFKWgwPpjO\\nsxiQswFXWosTZnJAn2NN/JF5aNqxUFLa9mo7Z+jztQuxrWsAFQsNFHf/t90iZi4w\\ne0CkIHJdI8ukcae5/3eP+9h8GyqEq/RcvxYtvW6zYkWAK3Pyqwrs+qwH1MuLsl6E\\n02fgD6T99Pq2V+3S1+dfgU6ot4IrMwT7aR+u9fCM8G4tF4y/5znIzuke6amVt52S\\nUfjnHOHbDxdD4Mkxn8107zX1XmQ4BEzhh1kjTVd3Mean6ye7xsFxFGYHA5Zd1iyM\\nCsmW5waqonRf03m1bQ9pYleufcwpr72iARLiBFhTOcAF6vpdoshO1qmTtsweFno=\\n=vKnQ\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree 3c47e2b9d9791503b56f0e4f78e76b9d061ad529\\nparent + adb252173d2107fad86bcdcbc149884c2dd4c609\\nauthor Jerrod + 1531179560 -0700\\ncommitter GitHub 1531179560 -0700\\n\\nAdding + 'include' term if multiple sources\\n\\nbased on a support ticket around multiple + sources\\r\\n\\r\\nhttps://codecov.freshdesk.com/a/tickets/87\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4/comments\",\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"adb252173d2107fad86bcdcbc149884c2dd4c609\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/adb252173d2107fad86bcdcbc149884c2dd4c609\"}]},{\"sha\":\"adb252173d2107fad86bcdcbc149884c2dd4c609\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmFkYjI1MjE3M2QyMTA3ZmFkODZiY2RjYmMxNDk4ODRjMmRkNGM2MDk=\",\"commit\":{\"author\":{\"name\":\"Thomas + Pedbereznak\",\"email\":\"tom@tomped.com\",\"date\":\"2018-04-26T08:39:32Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-04-26T08:39:32Z\"},\"message\":\"Update + README.rst\",\"tree\":{\"sha\":\"26b90aa0aa92d1d873342d7b95df4ad79f7db8b9\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/26b90aa0aa92d1d873342d7b95df4ad79f7db8b9\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/adb252173d2107fad86bcdcbc149884c2dd4c609\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJa4ZBECRBK7hj4Ov3rIwAAdHIIAEoo6hDo1yVW2e9pe5R6cesa\\nzQrd0cjAMvcjwvdDRAbAHkiNuJtJElO41xjyC4sAthl9zM1Wx1Jo4lc8+4CeJ2Vs\\n3b3PDbwp6MLBJcwfhC/mox0PYPzTFO56r61HJI7T2CkBh9GXHAifXMHhkmYP0y5A\\nGzeOE7FlP7Mz3N7NaXzlSPJbIZPD4X9swR0cqDZCFuD1R48QXi3+IbREUzO4KneM\\nS4KwJQNPWRefH8pEkZBLZ8KFPL0ftXr6YuCKE7ySwoer7uQ0AXVHY5HcLSZD/js/\\n9R7G+7CWkyBivTJAUFzql3j+A/ZiDTnXbEO6lty5K4LpvGD8kbkXTZB6QsIPC+g=\\n=fcYU\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree 26b90aa0aa92d1d873342d7b95df4ad79f7db8b9\\nparent + 6ae5f1795a441884ed2847bb31154814ac01ef38\\nauthor Thomas Pedbereznak + 1524731972 +0200\\ncommitter GitHub 1524731972 +0200\\n\\nUpdate + README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/adb252173d2107fad86bcdcbc149884c2dd4c609\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609/comments\",\"author\":{\"login\":\"TomPed\",\"id\":11602092,\"node_id\":\"MDQ6VXNlcjExNjAyMDky\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/11602092?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/TomPed\",\"html_url\":\"https://github.com/TomPed\",\"followers_url\":\"https://api.github.com/users/TomPed/followers\",\"following_url\":\"https://api.github.com/users/TomPed/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/TomPed/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/TomPed/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/TomPed/subscriptions\",\"organizations_url\":\"https://api.github.com/users/TomPed/orgs\",\"repos_url\":\"https://api.github.com/users/TomPed/repos\",\"events_url\":\"https://api.github.com/users/TomPed/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/TomPed/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"6ae5f1795a441884ed2847bb31154814ac01ef38\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38\"}]},{\"sha\":\"6ae5f1795a441884ed2847bb31154814ac01ef38\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjZhZTVmMTc5NWE0NDE4ODRlZDI4NDdiYjMxMTU0ODE0YWMwMWVmMzg=\",\"commit\":{\"author\":{\"name\":\"Thomas + Pedbereznak\",\"email\":\"tom@tomped.com\",\"date\":\"2018-04-26T08:35:58Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-04-26T08:35:58Z\"},\"message\":\"Update + README.rst\",\"tree\":{\"sha\":\"b5592410a15d7a596a8eaea6399766fbbbe0366c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b5592410a15d7a596a8eaea6399766fbbbe0366c\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6ae5f1795a441884ed2847bb31154814ac01ef38\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJa4Y9uCRBK7hj4Ov3rIwAAdHIIAAyJAC1mOPnKmkDSzraV47Wq\\nXma/2QidKpXnRqKgY6XFBXAH0RIpHpZ3NwGR/L2GH1l7xLjXtOMTvXOCjFBZUwRE\\nLlM9IdoUFyPU2E9P0z0vfGR/nk5QC8PY9lzDwe/N8ZhR0j4M2rTM2ue97om9nJ4e\\nmD+HR2ZwjKA9Z9zFeALgBjokKs44F6oN6lLuPYn06oiCnYB3ytlWJy+vpmEGLhoM\\nL+a/ct2e6O5MmlpbRlKVME4FL0O4wDBMrAaFeeZgQTCl2LKfdsfYScJnypkB7X06\\n6cDtC/TJ436n4PCTBRHVMDNGxzmgMgMFYbCPkJ27BeWlTuKVDcJ2msOV7ZJKqqs=\\n=oGiR\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree b5592410a15d7a596a8eaea6399766fbbbe0366c\\nparent + 8631ea09b9b689de0a348d5abf70bdd7273d2ae3\\nauthor Thomas Pedbereznak + 1524731758 +0200\\ncommitter GitHub 1524731758 +0200\\n\\nUpdate + README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38/comments\",\"author\":{\"login\":\"TomPed\",\"id\":11602092,\"node_id\":\"MDQ6VXNlcjExNjAyMDky\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/11602092?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/TomPed\",\"html_url\":\"https://github.com/TomPed\",\"followers_url\":\"https://api.github.com/users/TomPed/followers\",\"following_url\":\"https://api.github.com/users/TomPed/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/TomPed/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/TomPed/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/TomPed/subscriptions\",\"organizations_url\":\"https://api.github.com/users/TomPed/orgs\",\"repos_url\":\"https://api.github.com/users/TomPed/repos\",\"events_url\":\"https://api.github.com/users/TomPed/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/TomPed/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"}]},{\"sha\":\"8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg2MzFlYTA5YjliNjg5ZGUwYTM0OGQ1YWJmNzBiZGQ3MjczZDJhZTM=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2018-02-13T09:13:36Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-02-13T09:13:36Z\"},\"message\":\"Merge + pull request #31 from Gabswim/fix/typo\\n\\nfixing a typo in the README\",\"tree\":{\"sha\":\"e08452bb815b0a8039d5c326e126798aeaf8898f\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e08452bb815b0a8039d5c326e126798aeaf8898f\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJagqxACRBK7hj4Ov3rIwAAdHIIAHiUfENfdOLuNZ/2IvhBe6oJ\\nkkRIlb0iVOaYEtRm7zaEdj8Jt08IFL2C/jztCnE0Osx1r0K/qGXd2gVmBX6mBlda\\n+NSIdLBOdtTmdJQv/zN9ddht4uGakPOHsHTrodFaMN/nWRn9pDUBu1kQNK664zWo\\nSOBwmDU/zMPDUOpYoC2dmorA1Xze0aaKaA11zDO42jqfyHWmlqYoa6Eaf+TYC4Or\\nT8vjYyIJ6TC27XWWvqW1Rk/lZYGbx6QneIT5XLBmyHBCYt+RAYVB09mrrTYBqwVI\\nY7ZqJubRfkzWwAif6vS1jb1U7iisP1oQep+9j6p8RsViVx6s8qGIKea4HGjxm/8=\\n=7ECB\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree e08452bb815b0a8039d5c326e126798aeaf8898f\\nparent + 48f47f9d1b58ba418fdcd50117fc9781c10a27fb\\nparent 087ede6771099a66dccb968c8aacfa04e9ba27a8\\nauthor + Steve Peak 1518513216 +0100\\ncommitter GitHub + 1518513216 +0100\\n\\nMerge pull request #31 from Gabswim/fix/typo\\n\\nfixing + a typo in the README\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"},{\"sha\":\"087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/087ede6771099a66dccb968c8aacfa04e9ba27a8\"}]},{\"sha\":\"087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjA4N2VkZTY3NzEwOTlhNjZkY2NiOTY4YzhhYWNmYTA0ZTliYTI3YTg=\",\"commit\":{\"author\":{\"name\":\"Gabriel + Legault\",\"email\":\"gablegault1@hotmail.com\",\"date\":\"2018-02-13T01:06:57Z\"},\"committer\":{\"name\":\"Gabriel + Legault\",\"email\":\"gablegault1@hotmail.com\",\"date\":\"2018-02-13T01:06:57Z\"},\"message\":\"fixing + a typo in the README\",\"tree\":{\"sha\":\"e08452bb815b0a8039d5c326e126798aeaf8898f\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e08452bb815b0a8039d5c326e126798aeaf8898f\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8/comments\",\"author\":{\"login\":\"Gabswim\",\"id\":2859712,\"node_id\":\"MDQ6VXNlcjI4NTk3MTI=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2859712?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/Gabswim\",\"html_url\":\"https://github.com/Gabswim\",\"followers_url\":\"https://api.github.com/users/Gabswim/followers\",\"following_url\":\"https://api.github.com/users/Gabswim/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/Gabswim/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/Gabswim/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/Gabswim/subscriptions\",\"organizations_url\":\"https://api.github.com/users/Gabswim/orgs\",\"repos_url\":\"https://api.github.com/users/Gabswim/repos\",\"events_url\":\"https://api.github.com/users/Gabswim/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/Gabswim/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"Gabswim\",\"id\":2859712,\"node_id\":\"MDQ6VXNlcjI4NTk3MTI=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2859712?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/Gabswim\",\"html_url\":\"https://github.com/Gabswim\",\"followers_url\":\"https://api.github.com/users/Gabswim/followers\",\"following_url\":\"https://api.github.com/users/Gabswim/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/Gabswim/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/Gabswim/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/Gabswim/subscriptions\",\"organizations_url\":\"https://api.github.com/users/Gabswim/orgs\",\"repos_url\":\"https://api.github.com/users/Gabswim/repos\",\"events_url\":\"https://api.github.com/users/Gabswim/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/Gabswim/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"}]},{\"sha\":\"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjQ4ZjQ3ZjlkMWI1OGJhNDE4ZmRjZDUwMTE3ZmM5NzgxYzEwYTI3ZmI=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2018-01-30T11:57:49Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-01-30T11:57:49Z\"},\"message\":\"Merge + pull request #30 from Jay54520/master\\n\\n#29/Pytest doc error\",\"tree\":{\"sha\":\"3cdd37198ec70196f94aaae638599c10b95be8a0\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3cdd37198ec70196f94aaae638599c10b95be8a0\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJacF29CRBK7hj4Ov3rIwAAdHIIABlpiW1RCdcVH3F1mUGSPHBa\\n6htdwCoISAHOoiJpYXHv6C3ad0eBX6NCsyhcKw0IFGotqvoMvJo6/vS8cJnvdrkj\\nKhOr478QT2M50XYx+izaey55ckSMG2VFU/0rlnoGgnLsqQ5+tLt8xKU5PjCnYleF\\nSK7l5D8pb2vvMesGDHHrESaHup//flHCLvYCJsCVslhVU4+iAE7xx/s0ln8gVg9K\\n0r+2IKjsleoHxjiHWibWOqaDH6z/WUIE7RrO7JsitFg7/4aX4/JXIzDp+EI8wU8a\\npqvjtl+2Qj5hgr5qa6Wj5Qi4vh+wwhNR/Ujv6eK0hFZwMv5wMQ656eSRTeQH//U=\\n=V6j3\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree 3cdd37198ec70196f94aaae638599c10b95be8a0\\nparent + 76003ff147414ce80d2a14ab5f1b78d165e9a468\\nparent d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\\nauthor + Steve Peak 1517313469 +0100\\ncommitter GitHub + 1517313469 +0100\\n\\nMerge pull request #30 from Jay54520/master\\n\\n#29/Pytest + doc error\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\"},{\"sha\":\"d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"}]},{\"sha\":\"d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmQyY2QyOGQ1M2I3MmQ5OTllOWExOGQ2NjMxOWRmZTRkOWI3MTU1Yzc=\",\"commit\":{\"author\":{\"name\":\"\u63ED\u601D\u654F\",\"email\":\"jsm0834@175game.com\",\"date\":\"2018-01-28T07:12:57Z\"},\"committer\":{\"name\":\"\u63ED\u601D\u654F\",\"email\":\"jsm0834@175game.com\",\"date\":\"2018-01-28T07:12:57Z\"},\"message\":\"#29/Pytest + doc error\",\"tree\":{\"sha\":\"3cdd37198ec70196f94aaae638599c10b95be8a0\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3cdd37198ec70196f94aaae638599c10b95be8a0\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7/comments\",\"author\":{\"login\":\"Jay54520\",\"id\":13315364,\"node_id\":\"MDQ6VXNlcjEzMzE1MzY0\",\"avatar_url\":\"https://avatars2.githubusercontent.com/u/13315364?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/Jay54520\",\"html_url\":\"https://github.com/Jay54520\",\"followers_url\":\"https://api.github.com/users/Jay54520/followers\",\"following_url\":\"https://api.github.com/users/Jay54520/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/Jay54520/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/Jay54520/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/Jay54520/subscriptions\",\"organizations_url\":\"https://api.github.com/users/Jay54520/orgs\",\"repos_url\":\"https://api.github.com/users/Jay54520/repos\",\"events_url\":\"https://api.github.com/users/Jay54520/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/Jay54520/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"Jay54520\",\"id\":13315364,\"node_id\":\"MDQ6VXNlcjEzMzE1MzY0\",\"avatar_url\":\"https://avatars2.githubusercontent.com/u/13315364?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/Jay54520\",\"html_url\":\"https://github.com/Jay54520\",\"followers_url\":\"https://api.github.com/users/Jay54520/followers\",\"following_url\":\"https://api.github.com/users/Jay54520/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/Jay54520/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/Jay54520/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/Jay54520/subscriptions\",\"organizations_url\":\"https://api.github.com/users/Jay54520/orgs\",\"repos_url\":\"https://api.github.com/users/Jay54520/repos\",\"events_url\":\"https://api.github.com/users/Jay54520/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/Jay54520/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\"}]},{\"sha\":\"76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojc2MDAzZmYxNDc0MTRjZTgwZDJhMTRhYjVmMWI3OGQxNjVlOWE0Njg=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2018-01-22T13:40:28Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2018-01-22T13:40:28Z\"},\"message\":\"Update + README.rst\",\"tree\":{\"sha\":\"eac434ca240c7045eaf41e16b70a56e256467621\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/eac434ca240c7045eaf41e16b70a56e256467621\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJaZenMCRBK7hj4Ov3rIwAAdHIIAEKcjkLuqsx9HOj8kr7APqrJ\\nOm2KpXC0WCvn4QaqDzA2hNDoE7rwYvS+MEV308/39AKAg9GBv9T1GwzyeU6krnBn\\nVWfo6Ee3r+/H61GOEZmqHWoQ140LtvC0Z6wCG5xNTfmrr3eUizqPPi9ePnyTeoHQ\\nBvNEvI3FCqmudfBAWPfCWp4zDKp25siRLJ+jCEV3FZ8OmZ8kE5EvPspZDUFgCTzs\\nNaGYfpPOZoxSPrC3Th9ujHCEEontzTzTDHCBsTJLoZeoWrM25R4kcaBoHzggHHBn\\nvMLwN+kwoq4bdbIhTxoeFqc9eVPtVtG9D7jv0+oheBiu5nKkx44SinVBHpXgQ5w=\\n=yptQ\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree eac434ca240c7045eaf41e16b70a56e256467621\\nparent + f9253b0bf56d0e808ab01fdcc70412ac010f5c34\\nauthor Steve Peak + 1516628428 +0100\\ncommitter GitHub 1516628428 +0100\\n\\nUpdate + README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"}]},{\"sha\":\"f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmY5MjUzYjBiZjU2ZDBlODA4YWIwMWZkY2M3MDQxMmFjMDEwZjVjMzQ=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2017-12-11T10:13:41Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-12-11T10:13:41Z\"},\"message\":\"Merge + pull request #24 from gundalow/README_md_to_rst\\n\\nReadme md to rst\",\"tree\":{\"sha\":\"f80cce2672a08f03dbcd902468341e4c71727a2c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f80cce2672a08f03dbcd902468341e4c71727a2c\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\",\"signature\":\"-----BEGIN + PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJaLlpVCRBK7hj4Ov3rIwAAdHIIAKPJ0bOZe6GVHVPSbpxQctA9\\nCX3gxTFNxaWR9UW0ikeIYLPQBrvzyTYx0HDIErnSfktBFl02cGKm020SvU2qBWFs\\nwtOWnvxVILMH1xV30Q8n/pqLUomaQkSCu9LO/0w1QDF0BgBEP1F5dJb6lzU5uL7t\\nA4FllQ+UKpebhnI3PBCLK6pEUUN/2S6x76pXRvK6j56c+mHfeuOm66R663ZuTnDa\\neYis+Y4R4AYVMrZ12FCkGRly1BVZGgNtrmYQw0pG3DVplp8k9vjw7WaUbxaPsYFj\\nQ33FRzKIqOdjiXH5AdSX1NIsuJo1Fq459i/utW4rfT5EEDuK8V2ZHe4qUzggjmA=\\n=F9Iq\\n-----END + PGP SIGNATURE-----\\n\",\"payload\":\"tree f80cce2672a08f03dbcd902468341e4c71727a2c\\nparent + 1e906160c09128765a75afbcbd60d1cbd3c8d10a\\nparent 2903ade6074f09319c1854850ffee2c254c3e17c\\nauthor + Steve Peak 1512987221 +0100\\ncommitter GitHub + 1512987221 +0100\\n\\nMerge pull request #24 from gundalow/README_md_to_rst\\n\\nReadme + md to rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"},{\"sha\":\"2903ade6074f09319c1854850ffee2c254c3e17c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2903ade6074f09319c1854850ffee2c254c3e17c\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2903ade6074f09319c1854850ffee2c254c3e17c\"}]},{\"sha\":\"2903ade6074f09319c1854850ffee2c254c3e17c\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjI5MDNhZGU2MDc0ZjA5MzE5YzE4NTQ4NTBmZmVlMmMyNTRjM2UxN2M=\",\"commit\":{\"author\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:35:07Z\"},\"committer\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:35:07Z\"},\"message\":\"typo\",\"tree\":{\"sha\":\"f80cce2672a08f03dbcd902468341e4c71727a2c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f80cce2672a08f03dbcd902468341e4c71727a2c\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2903ade6074f09319c1854850ffee2c254c3e17c\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2903ade6074f09319c1854850ffee2c254c3e17c\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2903ade6074f09319c1854850ffee2c254c3e17c\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2903ade6074f09319c1854850ffee2c254c3e17c/comments\",\"author\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/0073e9e074081bc2588b9ee311fc01bc9adfa967\"}]},{\"sha\":\"0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjAwNzNlOWUwNzQwODFiYzI1ODhiOWVlMzExZmMwMWJjOWFkZmE5Njc=\",\"commit\":{\"author\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:33:54Z\"},\"committer\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:33:54Z\"},\"message\":\"Valid + badge\",\"tree\":{\"sha\":\"c6dad62f00e288976a31e2c6c189f18fc23881e4\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c6dad62f00e288976a31e2c6c189f18fc23881e4\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967/comments\",\"author\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"8d437d531af955c068c03f35a1f6f19667c6d215\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d437d531af955c068c03f35a1f6f19667c6d215\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8d437d531af955c068c03f35a1f6f19667c6d215\"}]},{\"sha\":\"8d437d531af955c068c03f35a1f6f19667c6d215\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjhkNDM3ZDUzMWFmOTU1YzA2OGMwM2YzNWExZjZmMTk2NjdjNmQyMTU=\",\"commit\":{\"author\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:28:02Z\"},\"committer\":{\"name\":\"John + Barker\",\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:28:02Z\"},\"message\":\"Convert + README.md to RST\\n\\n* Update formatting fixes #3\\n* Add example badge (and + how to use)\\n* Make it cleared that bash uploader should be used\",\"tree\":{\"sha\":\"3bdc420145fb2cc4232a0ba454675b84311c4bc4\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3bdc420145fb2cc4232a0ba454675b84311c4bc4\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8d437d531af955c068c03f35a1f6f19667c6d215\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d437d531af955c068c03f35a1f6f19667c6d215\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8d437d531af955c068c03f35a1f6f19667c6d215\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d437d531af955c068c03f35a1f6f19667c6d215/comments\",\"author\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"}]},{\"sha\":\"1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjFlOTA2MTYwYzA5MTI4NzY1YTc1YWZiY2JkNjBkMWNiZDNjOGQxMGE=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2017-10-04T09:43:08Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-10-04T09:43:08Z\"},\"message\":\"Merge + pull request #22 from IanLee1521/patch-1\\n\\nFixed minor typo\",\"tree\":{\"sha\":\"4b1780a3cac3fcfe3356b1da70346f19f63106b7\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4b1780a3cac3fcfe3356b1da70346f19f63106b7\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\"},{\"sha\":\"b066c93c2676bc957d971d2c4188e77b3e383b77\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b066c93c2676bc957d971d2c4188e77b3e383b77\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b066c93c2676bc957d971d2c4188e77b3e383b77\"}]},{\"sha\":\"b066c93c2676bc957d971d2c4188e77b3e383b77\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmIwNjZjOTNjMjY3NmJjOTU3ZDk3MWQyYzQxODhlNzdiM2UzODNiNzc=\",\"commit\":{\"author\":{\"name\":\"Ian + Lee\",\"email\":\"ianlee1521@gmail.com\",\"date\":\"2017-09-29T19:29:42Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-09-29T19:29:42Z\"},\"message\":\"Fixed + minor typo\",\"tree\":{\"sha\":\"4b1780a3cac3fcfe3356b1da70346f19f63106b7\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4b1780a3cac3fcfe3356b1da70346f19f63106b7\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b066c93c2676bc957d971d2c4188e77b3e383b77\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b066c93c2676bc957d971d2c4188e77b3e383b77\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b066c93c2676bc957d971d2c4188e77b3e383b77\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b066c93c2676bc957d971d2c4188e77b3e383b77/comments\",\"author\":{\"login\":\"IanLee1521\",\"id\":828452,\"node_id\":\"MDQ6VXNlcjgyODQ1Mg==\",\"avatar_url\":\"https://avatars0.githubusercontent.com/u/828452?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/IanLee1521\",\"html_url\":\"https://github.com/IanLee1521\",\"followers_url\":\"https://api.github.com/users/IanLee1521/followers\",\"following_url\":\"https://api.github.com/users/IanLee1521/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/IanLee1521/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/IanLee1521/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/IanLee1521/subscriptions\",\"organizations_url\":\"https://api.github.com/users/IanLee1521/orgs\",\"repos_url\":\"https://api.github.com/users/IanLee1521/repos\",\"events_url\":\"https://api.github.com/users/IanLee1521/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/IanLee1521/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\"}]},{\"sha\":\"3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjMyMzA1OTRhN2FhODc4MmZiY2Y1MTMyOWIyMzk1MTE4ZjdjZjBkMTU=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2017-08-30T14:02:20Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-08-30T14:02:20Z\"},\"message\":\"Update + README.md\",\"tree\":{\"sha\":\"2ba18d3d080f553b23501cd8485eae3c50589e6a\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2ba18d3d080f553b23501cd8485eae3c50589e6a\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"}]},{\"sha\":\"6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjY1NjBjYmZmMzNmYmZmODc0MGYwNDA3ZjRjYWMxMDkxYmMyNWU2YWU=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2017-02-02T19:10:16Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-02-02T19:10:16Z\"},\"message\":\"Update + README.md\",\"tree\":{\"sha\":\"39e2c45b98a62d06d6d84222d5a8c244150ec3c4\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/39e2c45b98a62d06d6d84222d5a8c244150ec3c4\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"feb5100831541db79eb83a263986df129573f3de\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/feb5100831541db79eb83a263986df129573f3de\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/feb5100831541db79eb83a263986df129573f3de\"}]},{\"sha\":\"feb5100831541db79eb83a263986df129573f3de\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmZlYjUxMDA4MzE1NDFkYjc5ZWI4M2EyNjM5ODZkZjEyOTU3M2YzZGU=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2017-02-02T19:09:38Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-02-02T19:09:38Z\"},\"message\":\"Merge + pull request #20 from briandant/master\\n\\nAdd further instructions for using + env vars\",\"tree\":{\"sha\":\"be98fa09eb1a8343b44071cdae6aac15e61d0cc9\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/be98fa09eb1a8343b44071cdae6aac15e61d0cc9\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/feb5100831541db79eb83a263986df129573f3de\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/feb5100831541db79eb83a263986df129573f3de\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/feb5100831541db79eb83a263986df129573f3de\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/feb5100831541db79eb83a263986df129573f3de/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"},{\"sha\":\"6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6802411a35f438bf62ee8a1c1928cd36ca50d534\"}]},{\"sha\":\"6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjY4MDI0MTFhMzVmNDM4YmY2MmVlOGExYzE5MjhjZDM2Y2E1MGQ1MzQ=\",\"commit\":{\"author\":{\"name\":\"Brian + Dant\",\"email\":\"briandant@users.noreply.github.com\",\"date\":\"2017-02-02T18:37:42Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2017-02-02T18:37:42Z\"},\"message\":\"Add + further instructions for using env vars\",\"tree\":{\"sha\":\"be98fa09eb1a8343b44071cdae6aac15e61d0cc9\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/be98fa09eb1a8343b44071cdae6aac15e61d0cc9\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534/comments\",\"author\":{\"login\":\"briandant\",\"id\":1884902,\"node_id\":\"MDQ6VXNlcjE4ODQ5MDI=\",\"avatar_url\":\"https://avatars2.githubusercontent.com/u/1884902?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/briandant\",\"html_url\":\"https://github.com/briandant\",\"followers_url\":\"https://api.github.com/users/briandant/followers\",\"following_url\":\"https://api.github.com/users/briandant/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/briandant/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/briandant/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/briandant/subscriptions\",\"organizations_url\":\"https://api.github.com/users/briandant/orgs\",\"repos_url\":\"https://api.github.com/users/briandant/repos\",\"events_url\":\"https://api.github.com/users/briandant/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/briandant/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"}]},{\"sha\":\"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjNmMjhlOTNjYmMxZDI2NmMzMGE1YmRmNDMxMjZlZDQ0ZmJlYjAwOWQ=\",\"commit\":{\"author\":{\"name\":\"Codecov + Test Bot\",\"email\":\"hello@codecov.io\",\"date\":\"2016-09-29T10:37:07Z\"},\"committer\":{\"name\":\"Codecov + Test Bot\",\"email\":\"hello@codecov.io\",\"date\":\"2016-09-29T10:37:07Z\"},\"message\":\"Circle + build #355\\nhttps://circleci.com/gh/codecov/testsuite/355\\nbash <(curl -s + https://raw.githubusercontent.com/codecov/codecov-bash/master/codecov) -v -u + https://codecov.io\",\"tree\":{\"sha\":\"80c741dbd6916ef10fadd2357efdcac15402af49\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/80c741dbd6916ef10fadd2357efdcac15402af49\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d/comments\",\"author\":{\"login\":\"codecov-test\",\"id\":8485477,\"node_id\":\"MDQ6VXNlcjg0ODU0Nzc=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8485477?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov-test\",\"html_url\":\"https://github.com/codecov-test\",\"followers_url\":\"https://api.github.com/users/codecov-test/followers\",\"following_url\":\"https://api.github.com/users/codecov-test/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/codecov-test/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/codecov-test/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/codecov-test/subscriptions\",\"organizations_url\":\"https://api.github.com/users/codecov-test/orgs\",\"repos_url\":\"https://api.github.com/users/codecov-test/repos\",\"events_url\":\"https://api.github.com/users/codecov-test/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/codecov-test/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"codecov-test\",\"id\":8485477,\"node_id\":\"MDQ6VXNlcjg0ODU0Nzc=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8485477?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov-test\",\"html_url\":\"https://github.com/codecov-test\",\"followers_url\":\"https://api.github.com/users/codecov-test/followers\",\"following_url\":\"https://api.github.com/users/codecov-test/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/codecov-test/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/codecov-test/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/codecov-test/subscriptions\",\"organizations_url\":\"https://api.github.com/users/codecov-test/orgs\",\"repos_url\":\"https://api.github.com/users/codecov-test/repos\",\"events_url\":\"https://api.github.com/users/codecov-test/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/codecov-test/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/63f5740c33f2aac68fd0757f471ab741f9e20a05\"}]},{\"sha\":\"63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjYzZjU3NDBjMzNmMmFhYzY4ZmQwNzU3ZjQ3MWFiNzQxZjllMjBhMDU=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2016-08-26T19:21:23Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2016-08-26T19:21:23Z\"},\"message\":\"Update + README.md\",\"tree\":{\"sha\":\"80c741dbd6916ef10fadd2357efdcac15402af49\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/80c741dbd6916ef10fadd2357efdcac15402af49\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"d61bb41b849de7125ca17fbd37292479648e7fa7\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d61bb41b849de7125ca17fbd37292479648e7fa7\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d61bb41b849de7125ca17fbd37292479648e7fa7\"}]},{\"sha\":\"d61bb41b849de7125ca17fbd37292479648e7fa7\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmQ2MWJiNDFiODQ5ZGU3MTI1Y2ExN2ZiZDM3MjkyNDc5NjQ4ZTdmYTc=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2016-08-16T15:47:11Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2016-08-16T15:47:11Z\"},\"message\":\"Merge + pull request #18 from yurovant/patch-1\\n\\nUpdate README.md\",\"tree\":{\"sha\":\"6f202265ab6e9bbe035e9567729cef9d042faa2d\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6f202265ab6e9bbe035e9567729cef9d042faa2d\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d61bb41b849de7125ca17fbd37292479648e7fa7\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d61bb41b849de7125ca17fbd37292479648e7fa7\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d61bb41b849de7125ca17fbd37292479648e7fa7\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d61bb41b849de7125ca17fbd37292479648e7fa7/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"},{\"sha\":\"2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2d1b772e138f05dbbd577ce0dcf3633577629a76\"}]},{\"sha\":\"2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjJkMWI3NzJlMTM4ZjA1ZGJiZDU3N2NlMGRjZjM2MzM1Nzc2MjlhNzY=\",\"commit\":{\"author\":{\"name\":\"Anton + Yurovskykh\",\"email\":\"anton.yurovskykh@gmail.com\",\"date\":\"2016-08-16T07:35:54Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2016-08-16T07:35:54Z\"},\"message\":\"Update + README.md\\n\\ntypo: priveta --> private\",\"tree\":{\"sha\":\"6f202265ab6e9bbe035e9567729cef9d042faa2d\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6f202265ab6e9bbe035e9567729cef9d042faa2d\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76/comments\",\"author\":{\"login\":\"yurovant\",\"id\":11337124,\"node_id\":\"MDQ6VXNlcjExMzM3MTI0\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/11337124?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/yurovant\",\"html_url\":\"https://github.com/yurovant\",\"followers_url\":\"https://api.github.com/users/yurovant/followers\",\"following_url\":\"https://api.github.com/users/yurovant/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/yurovant/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/yurovant/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/yurovant/subscriptions\",\"organizations_url\":\"https://api.github.com/users/yurovant/orgs\",\"repos_url\":\"https://api.github.com/users/yurovant/repos\",\"events_url\":\"https://api.github.com/users/yurovant/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/yurovant/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"}]},{\"sha\":\"84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg0ZWE4YjliYTBmODEzNGJlMDQ3Nzk3MWU3MmIyMzM5NTlmNWQzYjY=\",\"commit\":{\"author\":{\"name\":\"Steve + Peak\",\"email\":\"steve@codecov.io\",\"date\":\"2016-08-05T16:46:35Z\"},\"committer\":{\"name\":\"GitHub\",\"email\":\"noreply@github.com\",\"date\":\"2016-08-05T16:46:35Z\"},\"message\":\"Update + README.md\",\"tree\":{\"sha\":\"9b948b519e86c5089a2c9c00d0a8f326282bc5cd\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9b948b519e86c5089a2c9c00d0a8f326282bc5cd\"},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\",\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6/comments\",\"author\":{\"login\":\"stevepeak\",\"id\":2041757,\"node_id\":\"MDQ6VXNlcjIwNDE3NTc=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2041757?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/stevepeak\",\"html_url\":\"https://github.com/stevepeak\",\"followers_url\":\"https://api.github.com/users/stevepeak/followers\",\"following_url\":\"https://api.github.com/users/stevepeak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/stevepeak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/stevepeak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/stevepeak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/stevepeak/orgs\",\"repos_url\":\"https://api.github.com/users/stevepeak/repos\",\"events_url\":\"https://api.github.com/users/stevepeak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/stevepeak/received_events\",\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\":\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\",\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\",\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\":\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\",\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"e051e55647e0ecc27539b2dc40fb9b2839383060\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e051e55647e0ecc27539b2dc40fb9b2839383060\",\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/e051e55647e0ecc27539b2dc40fb9b2839383060\"}]}]" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 20 Sep 2019 20:13:23 GMT + Etag: + - W/"63475b9f822f78699bcbdd5ba8efd3b2" + Last-Modified: + - Thu, 10 Jan 2019 01:39:55 GMT + Link: + - ; + rel="next", ; + rel="last" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 14E0:5F67:8B061E:1437B55:5D8532E3 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4993' + X-Ratelimit-Reset: + - '1569013999' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits?sha=abf6d4df662c47e32460020ab14abf9303581429 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/contents?ref=abf6d4df662c47e32460020ab14abf9303581429 + response: + content: '[{"name":".coverage","path":".coverage","sha":"23e6e577d9e906f1fa619e8a8672d9537ff27326","size":335,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.coverage?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.coverage","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/23e6e577d9e906f1fa619e8a8672d9537ff27326","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/.coverage","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.coverage?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/23e6e577d9e906f1fa619e8a8672d9537ff27326","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.coverage"}},{"name":".travis.yml","path":".travis.yml","sha":"11d295c04e2feac0a533bf5b1df52b84f8076eec","size":144,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.travis.yml?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.travis.yml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/11d295c04e2feac0a533bf5b1df52b84f8076eec","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/.travis.yml","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.travis.yml?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/11d295c04e2feac0a533bf5b1df52b84f8076eec","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.travis.yml"}},{"name":"README.rst","path":"README.rst","sha":"405d834472636bc2c83dd8bd6818e4b2c2871e86","size":6377,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.rst?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/README.rst","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/405d834472636bc2c83dd8bd6818e4b2c2871e86","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/README.rst","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.rst?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/405d834472636bc2c83dd8bd6818e4b2c2871e86","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/README.rst"}},{"name":"awesome","path":"awesome","sha":"b25dbf08d0d6d35990c0fcec549b91f96cace181","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/awesome","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b25dbf08d0d6d35990c0fcec549b91f96cace181","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b25dbf08d0d6d35990c0fcec549b91f96cace181","html":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/awesome"}},{"name":"coverage.xml","path":"coverage.xml","sha":"d4076376a0834a54939fe778da2f0524e10bace0","size":1846,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/coverage.xml?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/d4076376a0834a54939fe778da2f0524e10bace0","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/coverage.xml?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/d4076376a0834a54939fe778da2f0524e10bace0","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml"}},{"name":"tests","path":"tests","sha":"2b1270d7021356b875c31501edb897038ddd589c","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/tests","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2b1270d7021356b875c31501edb897038ddd589c","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2b1270d7021356b875c31501edb897038ddd589c","html":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/tests"}}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 20 Sep 2019 20:13:24 GMT + Etag: + - W/"88796ed5686cfd7dffdb2db34b6b12ccaefde90e" + Last-Modified: + - Fri, 20 Sep 2019 17:23:07 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 14E1:0EA9:12A3E3F:23DDF50:5D8532E4 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4992' + X-Ratelimit-Reset: + - '1569013999' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/contents?ref=abf6d4df662c47e32460020ab14abf9303581429 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits + response: + content: '[{"sha":"587662b6e5403ae0d126e0c7839a8d98382c4760","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjU4NzY2MmI2ZTU0MDNhZTBkMTI2ZTBjNzgzOWE4ZDk4MzgyYzQ3NjA=","commit":{"author":{"name":"Thiago + Ribeiro Ramos","email":"thiago@ribeiroramos.com","date":"2018-11-07T22:43:54Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:34:45Z"},"message":"Creating + new code for reasons no one knows","tree":{"sha":"ec56802a37b981f13bdc3c9a56ae68ef82ab424a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ec56802a37b981f13bdc3c9a56ae68ef82ab424a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","html_url":"https://github.com/ThiagoCodecov/example-python/commit/587662b6e5403ae0d126e0c7839a8d98382c4760","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760/comments","author":null,"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"68946ef98daec68c7798459150982fc799c87d85","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/68946ef98daec68c7798459150982fc799c87d85","html_url":"https://github.com/ThiagoCodecov/example-python/commit/68946ef98daec68c7798459150982fc799c87d85"}]},{"sha":"03a8b737cb9d8585076ebdbac7b7235c8da0620d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjAzYThiNzM3Y2I5ZDg1ODUwNzZlYmRiYWM3YjcyMzVjOGRhMDYyMGQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-03-12T02:37:19Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:36:02Z"},"message":"Now + what","tree":{"sha":"51a385e1f575447b0b70fd597596c32c4f5bd172","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/51a385e1f575447b0b70fd597596c32c4f5bd172"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/03a8b737cb9d8585076ebdbac7b7235c8da0620d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"587662b6e5403ae0d126e0c7839a8d98382c4760","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","html_url":"https://github.com/ThiagoCodecov/example-python/commit/587662b6e5403ae0d126e0c7839a8d98382c4760"}]},{"sha":"bf9b57cf7b169806ae2d18d7671aba3825b99203","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmJmOWI1N2NmN2IxNjk4MDZhZTJkMThkNzY3MWFiYTM4MjViOTkyMDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-03-12T02:42:33Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:36:02Z"},"message":"Adding + untested code","tree":{"sha":"ce5383a6feb3e0bf20a4df46ae6c67ec3955723e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ce5383a6feb3e0bf20a4df46ae6c67ec3955723e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bf9b57cf7b169806ae2d18d7671aba3825b99203","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"03a8b737cb9d8585076ebdbac7b7235c8da0620d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/03a8b737cb9d8585076ebdbac7b7235c8da0620d"}]},{"sha":"cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmNlZGUxOWNiMzEwY2Q0Y2RkZmI1ZDg5MjFjYjhkMGNjN2M3YzE1MDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-16T22:02:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-16T22:11:24Z"},"message":"asdadafdsfdsfds","tree":{"sha":"e614247adf8a0705575e9c2170fad7c2848870a0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e614247adf8a0705575e9c2170fad7c2848870a0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"bf9b57cf7b169806ae2d18d7671aba3825b99203","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bf9b57cf7b169806ae2d18d7671aba3825b99203"}]},{"sha":"ea3ada938db123368d62b0133e7c5bb54b5292b9","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmVhM2FkYTkzOGRiMTIzMzY4ZDYyYjAxMzNlN2M1YmI1NGI1MjkyYjk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-19T18:48:19Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-19T18:48:19Z"},"message":"Adding + file t2 haha","tree":{"sha":"9ac6564d515ed2630026080e7cbdad4edfa9eca6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9ac6564d515ed2630026080e7cbdad4edfa9eca6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ea3ada938db123368d62b0133e7c5bb54b5292b9","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503"}]},{"sha":"2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjIwNDhiMjc3ZGQ2NTQyZjE4NGM2YTMwYzNlMmIwZjNlZTVlZWFmNGI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:43:42Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:43:42Z"},"message":"Adding + file t2 haha oooggg","tree":{"sha":"8b8d478591c3125af92ac395e87ddfb37fec5086","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/8b8d478591c3125af92ac395e87ddfb37fec5086"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ea3ada938db123368d62b0133e7c5bb54b5292b9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ea3ada938db123368d62b0133e7c5bb54b5292b9"}]},{"sha":"119de54e3cfdf8227a8556b9f5730c328a1390cd","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjExOWRlNTRlM2NmZGY4MjI3YTg1NTZiOWY1NzMwYzMyOGExMzkwY2Q=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:46:16Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:46:16Z"},"message":"Adding + file t2 haha oooggdsadsdsag","tree":{"sha":"d3868402c41afd8dcafb50e5bfa0e023f35c307e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/d3868402c41afd8dcafb50e5bfa0e023f35c307e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119de54e3cfdf8227a8556b9f5730c328a1390cd","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b"}]},{"sha":"2d55e8501b058b6f25382c4e287f022e8938461f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJkNTVlODUwMWIwNThiNmYyNTM4MmM0ZTI4N2YwMjJlODkzODQ2MWY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-24T21:32:08Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-24T21:32:08Z"},"message":"Adding + file t4 unpredictable","tree":{"sha":"a87f6d6ddd74d6df712bad79cc65d040c408efe8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/a87f6d6ddd74d6df712bad79cc65d040c408efe8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2d55e8501b058b6f25382c4e287f022e8938461f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2d55e8501b058b6f25382c4e287f022e8938461f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"119de54e3cfdf8227a8556b9f5730c328a1390cd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119de54e3cfdf8227a8556b9f5730c328a1390cd"}]},{"sha":"364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjM2NGJkZmJjNzJkNWUwNWI1MjBmMDMyMGIwZDhiMzlmZDllYTY5MmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-28T22:50:25Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-28T22:50:25Z"},"message":"Adding + Makefile","tree":{"sha":"452c48e858913bacb4be63a8e2351c98719406dd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/452c48e858913bacb4be63a8e2351c98719406dd"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2d55e8501b058b6f25382c4e287f022e8938461f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2d55e8501b058b6f25382c4e287f022e8938461f"}]},{"sha":"119c1907fb266f374b8440bbd70dccbea54daf8f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjExOWMxOTA3ZmIyNjZmMzc0Yjg0NDBiYmQ3MGRjY2JlYTU0ZGFmOGY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-09-02T23:07:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-09-02T23:07:56Z"},"message":"Cleaning + some stuff","tree":{"sha":"4995d75a388061164491217b50ee296137150f89","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4995d75a388061164491217b50ee296137150f89"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/119c1907fb266f374b8440bbd70dccbea54daf8f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119c1907fb266f374b8440bbd70dccbea54daf8f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119c1907fb266f374b8440bbd70dccbea54daf8f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119c1907fb266f374b8440bbd70dccbea54daf8f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 10 Jan 2020 03:16:26 GMT + Etag: + - W/"6a255c332b90087940cdb3a3dcd00be9" + Last-Modified: + - Thu, 09 Jan 2020 19:23:25 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 1221:785C:245926:33A9B8:5E17EC8A + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4994' + X-Ratelimit-Reset: + - '1578629688' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits +- request: + body: '{"name": "web", "active": true, "events": ["pull_request", "delete", "push", + "public", "status", "repository"], "config": {"url": "https://codecov.io/webhooks/github", + "secret": "test46nudghi6oft49bay37keyiuhok7", "content_type": "json"}}' + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/hooks + response: + content: '{"message":"Validation Failed","errors":[{"resource":"Hook","code":"custom","message":"Hook + already exists on this repository"}],"documentation_url":"https://developer.github.com/v3/repos/hooks/#create-a-hook"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Connection: + - close + Content-Length: + - '210' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 10 Jan 2020 03:16:26 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 422 Unprocessable Entity + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + X-Accepted-Oauth-Scopes: + - admin:repo_hook, public_repo, repo, write:repo_hook + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 1222:2560:244F1C:3387EA:5E17EC8A + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4993' + X-Ratelimit-Reset: + - '1578629688' + X-Xss-Protection: + - 1; mode=block + status: + code: 422 + message: Unprocessable Entity + status_code: 422 + url: https://api.github.com/repos/ThiagoCodecov/example-python/hooks +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call_no_jobs.yaml b/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call_no_jobs.yaml new file mode 100644 index 0000000000..ab370bd5c8 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call_no_jobs.yaml @@ -0,0 +1,2 @@ +interactions: [] +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call_test_results.yaml b/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call_test_results.yaml new file mode 100644 index 0000000000..116386e4c6 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_call_test_results.yaml @@ -0,0 +1,1412 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 + response: + content: '{"sha":"abf6d4df662c47e32460020ab14abf9303581429","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmFiZjZkNGRmNjYyYzQ3ZTMyNDYwMDIwYWIxNGFiZjkzMDM1ODE0Mjk=","commit":{"author":{"name":"Thiago + Ribeiro Ramos","email":"thiago@ribeiroramos.com","date":"2019-01-10T01:39:55Z"},"committer":{"name":"Thiago + Ribeiro Ramos","email":"thiago@ribeiroramos.com","date":"2019-01-10T01:39:55Z"},"message":"dsidsahdsahdsa","tree":{"sha":"88796ed5686cfd7dffdb2db34b6b12ccaefde90e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/88796ed5686cfd7dffdb2db34b6b12ccaefde90e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/abf6d4df662c47e32460020ab14abf9303581429","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/commit/abf6d4df662c47e32460020ab14abf9303581429","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429/comments","author":null,"committer":null,"parents":[{"sha":"c5b67303452bbff57cc1f49984339cde39eb1db5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5b67303452bbff57cc1f49984339cde39eb1db5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c5b67303452bbff57cc1f49984339cde39eb1db5"}],"stats":{"total":6,"additions":5,"deletions":1},"files":[{"sha":"770f2e8c26296c665565d336490a5306380b61d4","filename":"awesome/__init__.py","status":"modified","additions":4,"deletions":0,"changes":4,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/awesome%2F__init__.py","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/abf6d4df662c47e32460020ab14abf9303581429/awesome%2F__init__.py","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome%2F__init__.py?ref=abf6d4df662c47e32460020ab14abf9303581429","patch":"@@ + -10,3 +10,7 @@ def fib(n):\n if n < 2:\n return 1\n return fib(n + - 2) + fib(n - 1)\n+\n+\n+def coala(k):\n+ return k * k"},{"sha":"d4076376a0834a54939fe778da2f0524e10bace0","filename":"coverage.xml","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/coverage.xml?ref=abf6d4df662c47e32460020ab14abf9303581429","patch":"@@ + -1,5 +1,5 @@\n \n-\n+\n \t\n \t\n \t"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 03 Jan 2024 18:11:50 GMT + ETag: + - W/"4ab1bbacf914f9985dc1d8e75d169bc8b1ddfceee3ee4e34bd08ecd28294408f" + Last-Modified: + - Thu, 10 Jan 2019 01:39:55 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF68:1D9A:B95E888:1830B419:6595A366 + X-OAuth-Scopes: + - '' + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1704309110' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2024-02-02 18:10:44 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1","id":229193531,"node_id":"MDExOlB1bGxSZXF1ZXN0MjI5MTkzNTMx","html_url":"https://github.com/ThiagoCodecov/example-python/pull/1","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/1.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/1.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1","number":1,"state":"closed","locked":true,"title":"Creating + new code for reasons no one knows","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"Why + you ask?\r\n\r\nI dont know","created_at":"2018-11-07T22:44:49Z","updated_at":"2020-10-14T21:28:41Z","closed_at":"2019-09-09T22:23:11Z","merged_at":"2019-09-09T22:23:11Z","merge_commit_sha":"038ac8ac2127baa19a927c67f0d5168d9928abf3","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/119c1907fb266f374b8440bbd70dccbea54daf8f","head":{"label":"ThiagoCodecov:reason/some-testing","ref":"reason/some-testing","sha":"119c1907fb266f374b8440bbd70dccbea54daf8f","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2023-07-04T20:51:23Z","pushed_at":"2022-08-17T20:09:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":3,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"ThiagoCodecov:master","ref":"master","sha":"68946ef98daec68c7798459150982fc799c87d85","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2023-07-04T20:51:23Z","pushed_at":"2022-08-17T20:09:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":3,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/1"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/119c1907fb266f374b8440bbd70dccbea54daf8f"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null,"merged":true,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"comments":4,"review_comments":0,"maintainer_can_modify":false,"commits":10,"additions":48,"deletions":6,"changed_files":5}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 03 Jan 2024 18:11:50 GMT + ETag: + - W/"54809781a79d5b91deb75a71602908896318ce5076389cef66df9d70d910b399" + Last-Modified: + - Mon, 01 Jan 2024 12:42:06 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF69:8C6B:B7AE04E:17FB5738:6595A366 + X-OAuth-Scopes: + - '' + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4998' + X-RateLimit-Reset: + - '1704309110' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '2' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2024-02-02 18:10:44 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits?per_page=100&page=1 + response: + content: '[{"sha":"587662b6e5403ae0d126e0c7839a8d98382c4760","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjU4NzY2MmI2ZTU0MDNhZTBkMTI2ZTBjNzgzOWE4ZDk4MzgyYzQ3NjA=","commit":{"author":{"name":"Thiago + Ribeiro Ramos","email":"thiago@ribeiroramos.com","date":"2018-11-07T22:43:54Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:34:45Z"},"message":"Creating + new code for reasons no one knows","tree":{"sha":"ec56802a37b981f13bdc3c9a56ae68ef82ab424a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ec56802a37b981f13bdc3c9a56ae68ef82ab424a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","html_url":"https://github.com/ThiagoCodecov/example-python/commit/587662b6e5403ae0d126e0c7839a8d98382c4760","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760/comments","author":null,"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"68946ef98daec68c7798459150982fc799c87d85","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/68946ef98daec68c7798459150982fc799c87d85","html_url":"https://github.com/ThiagoCodecov/example-python/commit/68946ef98daec68c7798459150982fc799c87d85"}]},{"sha":"03a8b737cb9d8585076ebdbac7b7235c8da0620d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjAzYThiNzM3Y2I5ZDg1ODUwNzZlYmRiYWM3YjcyMzVjOGRhMDYyMGQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-03-12T02:37:19Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:36:02Z"},"message":"Now + what","tree":{"sha":"51a385e1f575447b0b70fd597596c32c4f5bd172","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/51a385e1f575447b0b70fd597596c32c4f5bd172"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/03a8b737cb9d8585076ebdbac7b7235c8da0620d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"587662b6e5403ae0d126e0c7839a8d98382c4760","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","html_url":"https://github.com/ThiagoCodecov/example-python/commit/587662b6e5403ae0d126e0c7839a8d98382c4760"}]},{"sha":"bf9b57cf7b169806ae2d18d7671aba3825b99203","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmJmOWI1N2NmN2IxNjk4MDZhZTJkMThkNzY3MWFiYTM4MjViOTkyMDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-03-12T02:42:33Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:36:02Z"},"message":"Adding + untested code","tree":{"sha":"ce5383a6feb3e0bf20a4df46ae6c67ec3955723e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ce5383a6feb3e0bf20a4df46ae6c67ec3955723e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bf9b57cf7b169806ae2d18d7671aba3825b99203","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"03a8b737cb9d8585076ebdbac7b7235c8da0620d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/03a8b737cb9d8585076ebdbac7b7235c8da0620d"}]},{"sha":"cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmNlZGUxOWNiMzEwY2Q0Y2RkZmI1ZDg5MjFjYjhkMGNjN2M3YzE1MDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-16T22:02:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-16T22:11:24Z"},"message":"asdadafdsfdsfds","tree":{"sha":"e614247adf8a0705575e9c2170fad7c2848870a0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e614247adf8a0705575e9c2170fad7c2848870a0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"bf9b57cf7b169806ae2d18d7671aba3825b99203","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bf9b57cf7b169806ae2d18d7671aba3825b99203"}]},{"sha":"ea3ada938db123368d62b0133e7c5bb54b5292b9","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmVhM2FkYTkzOGRiMTIzMzY4ZDYyYjAxMzNlN2M1YmI1NGI1MjkyYjk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-19T18:48:19Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-19T18:48:19Z"},"message":"Adding + file t2 haha","tree":{"sha":"9ac6564d515ed2630026080e7cbdad4edfa9eca6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9ac6564d515ed2630026080e7cbdad4edfa9eca6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ea3ada938db123368d62b0133e7c5bb54b5292b9","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503"}]},{"sha":"2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjIwNDhiMjc3ZGQ2NTQyZjE4NGM2YTMwYzNlMmIwZjNlZTVlZWFmNGI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:43:42Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:43:42Z"},"message":"Adding + file t2 haha oooggg","tree":{"sha":"8b8d478591c3125af92ac395e87ddfb37fec5086","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/8b8d478591c3125af92ac395e87ddfb37fec5086"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ea3ada938db123368d62b0133e7c5bb54b5292b9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ea3ada938db123368d62b0133e7c5bb54b5292b9"}]},{"sha":"119de54e3cfdf8227a8556b9f5730c328a1390cd","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjExOWRlNTRlM2NmZGY4MjI3YTg1NTZiOWY1NzMwYzMyOGExMzkwY2Q=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:46:16Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:46:16Z"},"message":"Adding + file t2 haha oooggdsadsdsag","tree":{"sha":"d3868402c41afd8dcafb50e5bfa0e023f35c307e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/d3868402c41afd8dcafb50e5bfa0e023f35c307e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119de54e3cfdf8227a8556b9f5730c328a1390cd","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b"}]},{"sha":"2d55e8501b058b6f25382c4e287f022e8938461f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJkNTVlODUwMWIwNThiNmYyNTM4MmM0ZTI4N2YwMjJlODkzODQ2MWY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-24T21:32:08Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-24T21:32:08Z"},"message":"Adding + file t4 unpredictable","tree":{"sha":"a87f6d6ddd74d6df712bad79cc65d040c408efe8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/a87f6d6ddd74d6df712bad79cc65d040c408efe8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2d55e8501b058b6f25382c4e287f022e8938461f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2d55e8501b058b6f25382c4e287f022e8938461f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"119de54e3cfdf8227a8556b9f5730c328a1390cd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119de54e3cfdf8227a8556b9f5730c328a1390cd"}]},{"sha":"364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjM2NGJkZmJjNzJkNWUwNWI1MjBmMDMyMGIwZDhiMzlmZDllYTY5MmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-28T22:50:25Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-28T22:50:25Z"},"message":"Adding + Makefile","tree":{"sha":"452c48e858913bacb4be63a8e2351c98719406dd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/452c48e858913bacb4be63a8e2351c98719406dd"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2d55e8501b058b6f25382c4e287f022e8938461f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2d55e8501b058b6f25382c4e287f022e8938461f"}]},{"sha":"119c1907fb266f374b8440bbd70dccbea54daf8f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjExOWMxOTA3ZmIyNjZmMzc0Yjg0NDBiYmQ3MGRjY2JlYTU0ZGFmOGY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-09-02T23:07:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-09-02T23:07:56Z"},"message":"Cleaning + some stuff","tree":{"sha":"4995d75a388061164491217b50ee296137150f89","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4995d75a388061164491217b50ee296137150f89"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/119c1907fb266f374b8440bbd70dccbea54daf8f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119c1907fb266f374b8440bbd70dccbea54daf8f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119c1907fb266f374b8440bbd70dccbea54daf8f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119c1907fb266f374b8440bbd70dccbea54daf8f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 03 Jan 2024 18:11:51 GMT + ETag: + - W/"baef9979462bf58b4528c5088ab956470e8fa00dbb4701da675ee46bae31f211" + Last-Modified: + - Mon, 01 Jan 2024 12:42:06 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF6A:40D7:BE75E37:18D18DA0:6595A367 + X-OAuth-Scopes: + - '' + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4997' + X-RateLimit-Reset: + - '1704309110' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '3' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2024-02-02 18:10:44 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits?sha=abf6d4df662c47e32460020ab14abf9303581429 + response: + content: "[{\"sha\":\"abf6d4df662c47e32460020ab14abf9303581429\",\"node_id\":\"\ + MDY6Q29tbWl0MTU2NjE3Nzc3OmFiZjZkNGRmNjYyYzQ3ZTMyNDYwMDIwYWIxNGFiZjkzMDM1ODE0Mjk=\"\ + ,\"commit\":{\"author\":{\"name\":\"Thiago Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\"\ + ,\"date\":\"2019-01-10T01:39:55Z\"},\"committer\":{\"name\":\"Thiago Ribeiro\ + \ Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:39:55Z\"\ + },\"message\":\"dsidsahdsahdsa\",\"tree\":{\"sha\":\"88796ed5686cfd7dffdb2db34b6b12ccaefde90e\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/88796ed5686cfd7dffdb2db34b6b12ccaefde90e\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/abf6d4df662c47e32460020ab14abf9303581429\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/abf6d4df662c47e32460020ab14abf9303581429\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429/comments\"\ + ,\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"c5b67303452bbff57cc1f49984339cde39eb1db5\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5b67303452bbff57cc1f49984339cde39eb1db5\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/c5b67303452bbff57cc1f49984339cde39eb1db5\"\ + }]},{\"sha\":\"c5b67303452bbff57cc1f49984339cde39eb1db5\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmM1YjY3MzAzNDUyYmJmZjU3Y2MxZjQ5OTg0MzM5Y2RlMzllYjFkYjU=\"\ + ,\"commit\":{\"author\":{\"name\":\"Thiago Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\"\ + ,\"date\":\"2019-01-10T01:39:10Z\"},\"committer\":{\"name\":\"Thiago Ribeiro\ + \ Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:39:10Z\"\ + },\"message\":\"KLKLK\",\"tree\":{\"sha\":\"7568f07accc434ad1be86a9ca05830ab6926687c\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7568f07accc434ad1be86a9ca05830ab6926687c\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c5b67303452bbff57cc1f49984339cde39eb1db5\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5b67303452bbff57cc1f49984339cde39eb1db5\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/c5b67303452bbff57cc1f49984339cde39eb1db5\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5b67303452bbff57cc1f49984339cde39eb1db5/comments\"\ + ,\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"a11ba05c2991a945048d26bf84511b7a3fb2d82a\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a11ba05c2991a945048d26bf84511b7a3fb2d82a\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/a11ba05c2991a945048d26bf84511b7a3fb2d82a\"\ + }]},{\"sha\":\"a11ba05c2991a945048d26bf84511b7a3fb2d82a\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmExMWJhMDVjMjk5MWE5NDUwNDhkMjZiZjg0NTExYjdhM2ZiMmQ4MmE=\"\ + ,\"commit\":{\"author\":{\"name\":\"Thiago Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\"\ + ,\"date\":\"2019-01-10T01:32:13Z\"},\"committer\":{\"name\":\"Thiago Ribeiro\ + \ Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-10T01:32:13Z\"\ + },\"message\":\"BGSFDS\",\"tree\":{\"sha\":\"08a1a5a77f43b2c7b8b7d007f8f14114db0785bf\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/08a1a5a77f43b2c7b8b7d007f8f14114db0785bf\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/a11ba05c2991a945048d26bf84511b7a3fb2d82a\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a11ba05c2991a945048d26bf84511b7a3fb2d82a\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/a11ba05c2991a945048d26bf84511b7a3fb2d82a\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a11ba05c2991a945048d26bf84511b7a3fb2d82a/comments\"\ + ,\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"76cf63501628eec3b450290b0fcf3019f9f13b1f\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76cf63501628eec3b450290b0fcf3019f9f13b1f\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76cf63501628eec3b450290b0fcf3019f9f13b1f\"\ + }]},{\"sha\":\"76cf63501628eec3b450290b0fcf3019f9f13b1f\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojc2Y2Y2MzUwMTYyOGVlYzNiNDUwMjkwYjBmY2YzMDE5ZjlmMTNiMWY=\"\ + ,\"commit\":{\"author\":{\"name\":\"Thiago Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\"\ + ,\"date\":\"2019-01-09T21:36:23Z\"},\"committer\":{\"name\":\"Thiago Ribeiro\ + \ Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-09T21:36:23Z\"\ + },\"message\":\"AAAA\",\"tree\":{\"sha\":\"79cc86f88de848974928a905905fea36ddd8089e\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/79cc86f88de848974928a905905fea36ddd8089e\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/76cf63501628eec3b450290b0fcf3019f9f13b1f\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76cf63501628eec3b450290b0fcf3019f9f13b1f\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76cf63501628eec3b450290b0fcf3019f9f13b1f\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76cf63501628eec3b450290b0fcf3019f9f13b1f/comments\"\ + ,\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\"\ + }]},{\"sha\":\"b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmI0N2UwYWM3YTJmN2FiYWE0Y2NkYzYxNDYyYzRlZTM5NjQ4OGVmMjA=\"\ + ,\"commit\":{\"author\":{\"name\":\"Thiago Ribeiro Ramos\",\"email\":\"thiago@ribeiroramos.com\"\ + ,\"date\":\"2019-01-09T21:02:43Z\"},\"committer\":{\"name\":\"Thiago Ribeiro\ + \ Ramos\",\"email\":\"thiago@ribeiroramos.com\",\"date\":\"2019-01-09T21:02:43Z\"\ + },\"message\":\"New commit man\",\"tree\":{\"sha\":\"5d69d4a04e76adff772f68a92b7042fe195a57b0\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/5d69d4a04e76adff772f68a92b7042fe195a57b0\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b47e0ac7a2f7abaa4ccdc61462c4ee396488ef20/comments\"\ + ,\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"b92edba44fdd29fcc506317cc3ddeae1a723dd08\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b92edba44fdd29fcc506317cc3ddeae1a723dd08\"\ + }]},{\"sha\":\"b92edba44fdd29fcc506317cc3ddeae1a723dd08\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmI5MmVkYmE0NGZkZDI5ZmNjNTA2MzE3Y2MzZGRlYWUxYTcyM2RkMDg=\"\ + ,\"commit\":{\"author\":{\"name\":\"Jerrod\",\"email\":\"jerrod@fundersclub.com\"\ + ,\"date\":\"2018-07-09T23:51:16Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2018-07-09T23:51:16Z\"},\"message\":\"Update\ + \ README.rst\",\"tree\":{\"sha\":\"0cbac4b22e6b7a239338e6550a59553c9bc76eb0\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/0cbac4b22e6b7a239338e6550a59553c9bc76eb0\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\"\ + ,\"signature\":\"-----BEGIN PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJbQ/T0CRBK7hj4Ov3rIwAAdHIIAGISA3RET3zrQdUjtrsxVc8K\\\ + nGjR/6NYt0xJxRA+tJ5JuRGplJJuVECOADr52eXaRMw+3jvfsqZOt7oKAnU/Q490u\\nwb8V8Y7vOo9doxqrJY6vQKCddjbiRZKD/clwAlBFFO0UJJtRWWANqeD0PHnDyzIG\\\ + nIasWMQyRb1RSMBAg7tIGsBwxzXKBaMr9Y6IVuh2HSLS/mOg124vy9hHKx5L60IyJ\\nvOlcFiEQpWYtFDn9hc+BvEgdaIcKP6mkOo+AGz6uYJ8149ukTwpGZQr8NJgxl4Yx\\\ + nY+gBGy7CurVoFZ4N3JOY94H9RffoYKXJwJmZS01n0y9ar8CG2YjSniFY7x7hJNQ=\\n=SnzP\\\ + n-----END PGP SIGNATURE-----\\n\",\"payload\":\"tree 0cbac4b22e6b7a239338e6550a59553c9bc76eb0\\\ + nparent c7f608036a3d2e89f8c59989ee213900c1ef39d1\\nauthor Jerrod \ + \ 1531180276 -0700\\ncommitter GitHub 1531180276 -0700\\\ + n\\nUpdate README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b92edba44fdd29fcc506317cc3ddeae1a723dd08\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"c7f608036a3d2e89f8c59989ee213900c1ef39d1\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/c7f608036a3d2e89f8c59989ee213900c1ef39d1\"\ + }]},{\"sha\":\"c7f608036a3d2e89f8c59989ee213900c1ef39d1\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmM3ZjYwODAzNmEzZDJlODlmOGM1OTk4OWVlMjEzOTAwYzFlZjM5ZDE=\"\ + ,\"commit\":{\"author\":{\"name\":\"Jerrod\",\"email\":\"jerrod@fundersclub.com\"\ + ,\"date\":\"2018-07-09T23:48:34Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2018-07-09T23:48:34Z\"},\"message\":\"Update\ + \ README.rst\",\"tree\":{\"sha\":\"67a425bb5cdf5dba974649a92b9bd1b332ccbada\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/67a425bb5cdf5dba974649a92b9bd1b332ccbada\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\"\ + ,\"signature\":\"-----BEGIN PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJbQ/RSCRBK7hj4Ov3rIwAAdHIIADaMP9S0JFvlxV7y32ytgpMy\\\ + nHHzFrThiO4KivcY0JNiTLPTD9zdKYKeczLw2fV2GLZU/3Ho/msh9gk+GB07yxJiK\\nSxQxW78XRBNeXMNtN1gQHTB/1XpDMk//uZRFD4CAY3Rf9n8MxKhtLV66vmvmInsu\\\ + n/pErsBSOyZH1plHejRJFloQCbHjwzVB8/OrtoV1P/woVsX6nmX59NHWsMo5rY80W\\n/AEr58FzjXV0b0mQ05q9VjHVhFZqOwh981YWHrgv0Ujxu0z9qpbTAhZx5+JOjAuX\\\ + nR9zILOWUgZ6w7YUXhAgXfqYztYfCZyPiaDPOxgl1RWMPtAh9KvYZzriKuEZgTdk=\\n=utzN\\\ + n-----END PGP SIGNATURE-----\\n\",\"payload\":\"tree 67a425bb5cdf5dba974649a92b9bd1b332ccbada\\\ + nparent 6895b6479dbe12b5cb3baa02416c6343ddb888b4\\nauthor Jerrod \ + \ 1531180114 -0700\\ncommitter GitHub 1531180114 -0700\\\ + n\\nUpdate README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/c7f608036a3d2e89f8c59989ee213900c1ef39d1\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"6895b6479dbe12b5cb3baa02416c6343ddb888b4\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6895b6479dbe12b5cb3baa02416c6343ddb888b4\"\ + }]},{\"sha\":\"6895b6479dbe12b5cb3baa02416c6343ddb888b4\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjY4OTViNjQ3OWRiZTEyYjVjYjNiYWEwMjQxNmM2MzQzZGRiODg4YjQ=\"\ + ,\"commit\":{\"author\":{\"name\":\"Jerrod\",\"email\":\"jerrod@fundersclub.com\"\ + ,\"date\":\"2018-07-09T23:39:20Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2018-07-09T23:39:20Z\"},\"message\":\"Adding\ + \ 'include' term if multiple sources\\n\\nbased on a support ticket around multiple\ + \ sources\\r\\n\\r\\nhttps://codecov.freshdesk.com/a/tickets/87\",\"tree\":{\"\ + sha\":\"3c47e2b9d9791503b56f0e4f78e76b9d061ad529\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3c47e2b9d9791503b56f0e4f78e76b9d061ad529\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\"\ + ,\"signature\":\"-----BEGIN PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJbQ/IoCRBK7hj4Ov3rIwAAdHIIAGm5AdlM8E0E7TyFKWgwPpjO\\\ + nsxiQswFXWosTZnJAn2NN/JF5aNqxUFLa9mo7Z+jztQuxrWsAFQsNFHf/t90iZi4w\\ne0CkIHJdI8ukcae5/3eP+9h8GyqEq/RcvxYtvW6zYkWAK3Pyqwrs+qwH1MuLsl6E\\\ + n02fgD6T99Pq2V+3S1+dfgU6ot4IrMwT7aR+u9fCM8G4tF4y/5znIzuke6amVt52S\\nUfjnHOHbDxdD4Mkxn8107zX1XmQ4BEzhh1kjTVd3Mean6ye7xsFxFGYHA5Zd1iyM\\\ + nCsmW5waqonRf03m1bQ9pYleufcwpr72iARLiBFhTOcAF6vpdoshO1qmTtsweFno=\\n=vKnQ\\\ + n-----END PGP SIGNATURE-----\\n\",\"payload\":\"tree 3c47e2b9d9791503b56f0e4f78e76b9d061ad529\\\ + nparent adb252173d2107fad86bcdcbc149884c2dd4c609\\nauthor Jerrod \ + \ 1531179560 -0700\\ncommitter GitHub 1531179560 -0700\\\ + n\\nAdding 'include' term if multiple sources\\n\\nbased on a support ticket\ + \ around multiple sources\\r\\n\\r\\nhttps://codecov.freshdesk.com/a/tickets/87\"\ + }},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6895b6479dbe12b5cb3baa02416c6343ddb888b4\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"adb252173d2107fad86bcdcbc149884c2dd4c609\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/adb252173d2107fad86bcdcbc149884c2dd4c609\"\ + }]},{\"sha\":\"adb252173d2107fad86bcdcbc149884c2dd4c609\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmFkYjI1MjE3M2QyMTA3ZmFkODZiY2RjYmMxNDk4ODRjMmRkNGM2MDk=\"\ + ,\"commit\":{\"author\":{\"name\":\"Thomas Pedbereznak\",\"email\":\"tom@tomped.com\"\ + ,\"date\":\"2018-04-26T08:39:32Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2018-04-26T08:39:32Z\"},\"message\":\"Update\ + \ README.rst\",\"tree\":{\"sha\":\"26b90aa0aa92d1d873342d7b95df4ad79f7db8b9\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/26b90aa0aa92d1d873342d7b95df4ad79f7db8b9\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/adb252173d2107fad86bcdcbc149884c2dd4c609\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\"\ + ,\"signature\":\"-----BEGIN PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJa4ZBECRBK7hj4Ov3rIwAAdHIIAEoo6hDo1yVW2e9pe5R6cesa\\\ + nzQrd0cjAMvcjwvdDRAbAHkiNuJtJElO41xjyC4sAthl9zM1Wx1Jo4lc8+4CeJ2Vs\\n3b3PDbwp6MLBJcwfhC/mox0PYPzTFO56r61HJI7T2CkBh9GXHAifXMHhkmYP0y5A\\\ + nGzeOE7FlP7Mz3N7NaXzlSPJbIZPD4X9swR0cqDZCFuD1R48QXi3+IbREUzO4KneM\\nS4KwJQNPWRefH8pEkZBLZ8KFPL0ftXr6YuCKE7ySwoer7uQ0AXVHY5HcLSZD/js/\\\ + n9R7G+7CWkyBivTJAUFzql3j+A/ZiDTnXbEO6lty5K4LpvGD8kbkXTZB6QsIPC+g=\\n=fcYU\\\ + n-----END PGP SIGNATURE-----\\n\",\"payload\":\"tree 26b90aa0aa92d1d873342d7b95df4ad79f7db8b9\\\ + nparent 6ae5f1795a441884ed2847bb31154814ac01ef38\\nauthor Thomas Pedbereznak\ + \ 1524731972 +0200\\ncommitter GitHub \ + \ 1524731972 +0200\\n\\nUpdate README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/adb252173d2107fad86bcdcbc149884c2dd4c609\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609/comments\"\ + ,\"author\":{\"login\":\"TomPed\",\"id\":11602092,\"node_id\":\"MDQ6VXNlcjExNjAyMDky\"\ + ,\"avatar_url\":\"https://avatars.githubusercontent.com/u/11602092?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/TomPed\",\"html_url\"\ + :\"https://github.com/TomPed\",\"followers_url\":\"https://api.github.com/users/TomPed/followers\"\ + ,\"following_url\":\"https://api.github.com/users/TomPed/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/TomPed/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/TomPed/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/TomPed/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/TomPed/orgs\",\"repos_url\":\"https://api.github.com/users/TomPed/repos\"\ + ,\"events_url\":\"https://api.github.com/users/TomPed/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/TomPed/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\"\ + ,\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"6ae5f1795a441884ed2847bb31154814ac01ef38\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38\"\ + }]},{\"sha\":\"6ae5f1795a441884ed2847bb31154814ac01ef38\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjZhZTVmMTc5NWE0NDE4ODRlZDI4NDdiYjMxMTU0ODE0YWMwMWVmMzg=\"\ + ,\"commit\":{\"author\":{\"name\":\"Thomas Pedbereznak\",\"email\":\"tom@tomped.com\"\ + ,\"date\":\"2018-04-26T08:35:58Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2018-04-26T08:35:58Z\"},\"message\":\"Update\ + \ README.rst\",\"tree\":{\"sha\":\"b5592410a15d7a596a8eaea6399766fbbbe0366c\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b5592410a15d7a596a8eaea6399766fbbbe0366c\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6ae5f1795a441884ed2847bb31154814ac01ef38\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\"\ + ,\"signature\":\"-----BEGIN PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJa4Y9uCRBK7hj4Ov3rIwAAdHIIAAyJAC1mOPnKmkDSzraV47Wq\\\ + nXma/2QidKpXnRqKgY6XFBXAH0RIpHpZ3NwGR/L2GH1l7xLjXtOMTvXOCjFBZUwRE\\nLlM9IdoUFyPU2E9P0z0vfGR/nk5QC8PY9lzDwe/N8ZhR0j4M2rTM2ue97om9nJ4e\\\ + nmD+HR2ZwjKA9Z9zFeALgBjokKs44F6oN6lLuPYn06oiCnYB3ytlWJy+vpmEGLhoM\\nL+a/ct2e6O5MmlpbRlKVME4FL0O4wDBMrAaFeeZgQTCl2LKfdsfYScJnypkB7X06\\\ + n6cDtC/TJ436n4PCTBRHVMDNGxzmgMgMFYbCPkJ27BeWlTuKVDcJ2msOV7ZJKqqs=\\n=oGiR\\\ + n-----END PGP SIGNATURE-----\\n\",\"payload\":\"tree b5592410a15d7a596a8eaea6399766fbbbe0366c\\\ + nparent 8631ea09b9b689de0a348d5abf70bdd7273d2ae3\\nauthor Thomas Pedbereznak\ + \ 1524731758 +0200\\ncommitter GitHub \ + \ 1524731758 +0200\\n\\nUpdate README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38/comments\"\ + ,\"author\":{\"login\":\"TomPed\",\"id\":11602092,\"node_id\":\"MDQ6VXNlcjExNjAyMDky\"\ + ,\"avatar_url\":\"https://avatars.githubusercontent.com/u/11602092?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/TomPed\",\"html_url\"\ + :\"https://github.com/TomPed\",\"followers_url\":\"https://api.github.com/users/TomPed/followers\"\ + ,\"following_url\":\"https://api.github.com/users/TomPed/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/TomPed/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/TomPed/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/TomPed/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/TomPed/orgs\",\"repos_url\":\"https://api.github.com/users/TomPed/repos\"\ + ,\"events_url\":\"https://api.github.com/users/TomPed/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/TomPed/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\"\ + ,\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"\ + }]},{\"sha\":\"8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg2MzFlYTA5YjliNjg5ZGUwYTM0OGQ1YWJmNzBiZGQ3MjczZDJhZTM=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2018-02-13T09:13:36Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2018-02-13T09:13:36Z\"},\"message\":\"Merge\ + \ pull request #31 from Gabswim/fix/typo\\n\\nfixing a typo in the README\"\ + ,\"tree\":{\"sha\":\"e08452bb815b0a8039d5c326e126798aeaf8898f\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e08452bb815b0a8039d5c326e126798aeaf8898f\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\"\ + ,\"signature\":\"-----BEGIN PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJagqxACRBK7hj4Ov3rIwAAdHIIAHiUfENfdOLuNZ/2IvhBe6oJ\\\ + nkkRIlb0iVOaYEtRm7zaEdj8Jt08IFL2C/jztCnE0Osx1r0K/qGXd2gVmBX6mBlda\\n+NSIdLBOdtTmdJQv/zN9ddht4uGakPOHsHTrodFaMN/nWRn9pDUBu1kQNK664zWo\\\ + nSOBwmDU/zMPDUOpYoC2dmorA1Xze0aaKaA11zDO42jqfyHWmlqYoa6Eaf+TYC4Or\\nT8vjYyIJ6TC27XWWvqW1Rk/lZYGbx6QneIT5XLBmyHBCYt+RAYVB09mrrTYBqwVI\\\ + nY7ZqJubRfkzWwAif6vS1jb1U7iisP1oQep+9j6p8RsViVx6s8qGIKea4HGjxm/8=\\n=7ECB\\\ + n-----END PGP SIGNATURE-----\\n\",\"payload\":\"tree e08452bb815b0a8039d5c326e126798aeaf8898f\\\ + nparent 48f47f9d1b58ba418fdcd50117fc9781c10a27fb\\nparent 087ede6771099a66dccb968c8aacfa04e9ba27a8\\\ + nauthor Steve Peak 1518513216 +0100\\ncommitter GitHub \ + \ 1518513216 +0100\\n\\nMerge pull request #31 from Gabswim/fix/typo\\n\\nfixing\ + \ a typo in the README\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + },{\"sha\":\"087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/087ede6771099a66dccb968c8aacfa04e9ba27a8\"\ + }]},{\"sha\":\"087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjA4N2VkZTY3NzEwOTlhNjZkY2NiOTY4YzhhYWNmYTA0ZTliYTI3YTg=\"\ + ,\"commit\":{\"author\":{\"name\":\"Gabriel Legault\",\"email\":\"gablegault1@hotmail.com\"\ + ,\"date\":\"2018-02-13T01:06:57Z\"},\"committer\":{\"name\":\"Gabriel Legault\"\ + ,\"email\":\"gablegault1@hotmail.com\",\"date\":\"2018-02-13T01:06:57Z\"},\"\ + message\":\"fixing a typo in the README\",\"tree\":{\"sha\":\"e08452bb815b0a8039d5c326e126798aeaf8898f\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e08452bb815b0a8039d5c326e126798aeaf8898f\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/087ede6771099a66dccb968c8aacfa04e9ba27a8\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8/comments\"\ + ,\"author\":{\"login\":\"Gabswim\",\"id\":2859712,\"node_id\":\"MDQ6VXNlcjI4NTk3MTI=\"\ + ,\"avatar_url\":\"https://avatars.githubusercontent.com/u/2859712?v=4\",\"gravatar_id\"\ + :\"\",\"url\":\"https://api.github.com/users/Gabswim\",\"html_url\":\"https://github.com/Gabswim\"\ + ,\"followers_url\":\"https://api.github.com/users/Gabswim/followers\",\"following_url\"\ + :\"https://api.github.com/users/Gabswim/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/Gabswim/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/Gabswim/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/Gabswim/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/Gabswim/orgs\",\"repos_url\":\"https://api.github.com/users/Gabswim/repos\"\ + ,\"events_url\":\"https://api.github.com/users/Gabswim/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/Gabswim/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"Gabswim\"\ + ,\"id\":2859712,\"node_id\":\"MDQ6VXNlcjI4NTk3MTI=\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/2859712?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/Gabswim\",\"html_url\"\ + :\"https://github.com/Gabswim\",\"followers_url\":\"https://api.github.com/users/Gabswim/followers\"\ + ,\"following_url\":\"https://api.github.com/users/Gabswim/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/Gabswim/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/Gabswim/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/Gabswim/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/Gabswim/orgs\",\"repos_url\":\"https://api.github.com/users/Gabswim/repos\"\ + ,\"events_url\":\"https://api.github.com/users/Gabswim/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/Gabswim/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + }]},{\"sha\":\"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjQ4ZjQ3ZjlkMWI1OGJhNDE4ZmRjZDUwMTE3ZmM5NzgxYzEwYTI3ZmI=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2018-01-30T11:57:49Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2018-01-30T11:57:49Z\"},\"message\":\"Merge\ + \ pull request #30 from Jay54520/master\\n\\n#29/Pytest doc error\",\"tree\"\ + :{\"sha\":\"3cdd37198ec70196f94aaae638599c10b95be8a0\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3cdd37198ec70196f94aaae638599c10b95be8a0\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\"\ + ,\"signature\":\"-----BEGIN PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJacF29CRBK7hj4Ov3rIwAAdHIIABlpiW1RCdcVH3F1mUGSPHBa\\\ + n6htdwCoISAHOoiJpYXHv6C3ad0eBX6NCsyhcKw0IFGotqvoMvJo6/vS8cJnvdrkj\\nKhOr478QT2M50XYx+izaey55ckSMG2VFU/0rlnoGgnLsqQ5+tLt8xKU5PjCnYleF\\\ + nSK7l5D8pb2vvMesGDHHrESaHup//flHCLvYCJsCVslhVU4+iAE7xx/s0ln8gVg9K\\n0r+2IKjsleoHxjiHWibWOqaDH6z/WUIE7RrO7JsitFg7/4aX4/JXIzDp+EI8wU8a\\\ + npqvjtl+2Qj5hgr5qa6Wj5Qi4vh+wwhNR/Ujv6eK0hFZwMv5wMQ656eSRTeQH//U=\\n=V6j3\\\ + n-----END PGP SIGNATURE-----\\n\",\"payload\":\"tree 3cdd37198ec70196f94aaae638599c10b95be8a0\\\ + nparent 76003ff147414ce80d2a14ab5f1b78d165e9a468\\nparent d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\\\ + nauthor Steve Peak 1517313469 +0100\\ncommitter GitHub \ + \ 1517313469 +0100\\n\\nMerge pull request #30 from Jay54520/master\\n\\n#29/Pytest\ + \ doc error\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + },{\"sha\":\"d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"\ + }]},{\"sha\":\"d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmQyY2QyOGQ1M2I3MmQ5OTllOWExOGQ2NjMxOWRmZTRkOWI3MTU1Yzc=\"\ + ,\"commit\":{\"author\":{\"name\":\"\u63ED\u601D\u654F\",\"email\":\"jsm0834@175game.com\"\ + ,\"date\":\"2018-01-28T07:12:57Z\"},\"committer\":{\"name\":\"\u63ED\u601D\u654F\ + \",\"email\":\"jsm0834@175game.com\",\"date\":\"2018-01-28T07:12:57Z\"},\"message\"\ + :\"#29/Pytest doc error\",\"tree\":{\"sha\":\"3cdd37198ec70196f94aaae638599c10b95be8a0\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3cdd37198ec70196f94aaae638599c10b95be8a0\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7/comments\"\ + ,\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + }]},{\"sha\":\"76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojc2MDAzZmYxNDc0MTRjZTgwZDJhMTRhYjVmMWI3OGQxNjVlOWE0Njg=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2018-01-22T13:40:28Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2018-01-22T13:40:28Z\"},\"message\":\"Update\ + \ README.rst\",\"tree\":{\"sha\":\"eac434ca240c7045eaf41e16b70a56e256467621\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/eac434ca240c7045eaf41e16b70a56e256467621\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\"\ + ,\"signature\":\"-----BEGIN PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJaZenMCRBK7hj4Ov3rIwAAdHIIAEKcjkLuqsx9HOj8kr7APqrJ\\\ + nOm2KpXC0WCvn4QaqDzA2hNDoE7rwYvS+MEV308/39AKAg9GBv9T1GwzyeU6krnBn\\nVWfo6Ee3r+/H61GOEZmqHWoQ140LtvC0Z6wCG5xNTfmrr3eUizqPPi9ePnyTeoHQ\\\ + nBvNEvI3FCqmudfBAWPfCWp4zDKp25siRLJ+jCEV3FZ8OmZ8kE5EvPspZDUFgCTzs\\nNaGYfpPOZoxSPrC3Th9ujHCEEontzTzTDHCBsTJLoZeoWrM25R4kcaBoHzggHHBn\\\ + nvMLwN+kwoq4bdbIhTxoeFqc9eVPtVtG9D7jv0+oheBiu5nKkx44SinVBHpXgQ5w=\\n=yptQ\\\ + n-----END PGP SIGNATURE-----\\n\",\"payload\":\"tree eac434ca240c7045eaf41e16b70a56e256467621\\\ + nparent f9253b0bf56d0e808ab01fdcc70412ac010f5c34\\nauthor Steve Peak \ + \ 1516628428 +0100\\ncommitter GitHub 1516628428 +0100\\\ + n\\nUpdate README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + }]},{\"sha\":\"f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmY5MjUzYjBiZjU2ZDBlODA4YWIwMWZkY2M3MDQxMmFjMDEwZjVjMzQ=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2017-12-11T10:13:41Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2017-12-11T10:13:41Z\"},\"message\":\"Merge\ + \ pull request #24 from gundalow/README_md_to_rst\\n\\nReadme md to rst\",\"\ + tree\":{\"sha\":\"f80cce2672a08f03dbcd902468341e4c71727a2c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f80cce2672a08f03dbcd902468341e4c71727a2c\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\"\ + ,\"signature\":\"-----BEGIN PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJaLlpVCRBK7hj4Ov3rIwAAdHIIAKPJ0bOZe6GVHVPSbpxQctA9\\\ + nCX3gxTFNxaWR9UW0ikeIYLPQBrvzyTYx0HDIErnSfktBFl02cGKm020SvU2qBWFs\\nwtOWnvxVILMH1xV30Q8n/pqLUomaQkSCu9LO/0w1QDF0BgBEP1F5dJb6lzU5uL7t\\\ + nA4FllQ+UKpebhnI3PBCLK6pEUUN/2S6x76pXRvK6j56c+mHfeuOm66R663ZuTnDa\\neYis+Y4R4AYVMrZ12FCkGRly1BVZGgNtrmYQw0pG3DVplp8k9vjw7WaUbxaPsYFj\\\ + nQ33FRzKIqOdjiXH5AdSX1NIsuJo1Fq459i/utW4rfT5EEDuK8V2ZHe4qUzggjmA=\\n=F9Iq\\\ + n-----END PGP SIGNATURE-----\\n\",\"payload\":\"tree f80cce2672a08f03dbcd902468341e4c71727a2c\\\ + nparent 1e906160c09128765a75afbcbd60d1cbd3c8d10a\\nparent 2903ade6074f09319c1854850ffee2c254c3e17c\\\ + nauthor Steve Peak 1512987221 +0100\\ncommitter GitHub \ + \ 1512987221 +0100\\n\\nMerge pull request #24 from gundalow/README_md_to_rst\\\ + n\\nReadme md to rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + },{\"sha\":\"2903ade6074f09319c1854850ffee2c254c3e17c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2903ade6074f09319c1854850ffee2c254c3e17c\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2903ade6074f09319c1854850ffee2c254c3e17c\"\ + }]},{\"sha\":\"2903ade6074f09319c1854850ffee2c254c3e17c\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjI5MDNhZGU2MDc0ZjA5MzE5YzE4NTQ4NTBmZmVlMmMyNTRjM2UxN2M=\"\ + ,\"commit\":{\"author\":{\"name\":\"John Barker\",\"email\":\"john@johnrbarker.com\"\ + ,\"date\":\"2017-12-09T13:35:07Z\"},\"committer\":{\"name\":\"John Barker\"\ + ,\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:35:07Z\"},\"message\"\ + :\"typo\",\"tree\":{\"sha\":\"f80cce2672a08f03dbcd902468341e4c71727a2c\",\"\ + url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f80cce2672a08f03dbcd902468341e4c71727a2c\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2903ade6074f09319c1854850ffee2c254c3e17c\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2903ade6074f09319c1854850ffee2c254c3e17c\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2903ade6074f09319c1854850ffee2c254c3e17c\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2903ade6074f09319c1854850ffee2c254c3e17c/comments\"\ + ,\"author\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\"\ + ,\"avatar_url\":\"https://avatars.githubusercontent.com/u/940557?v=4\",\"gravatar_id\"\ + :\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\"\ + ,\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\"\ + :\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\"\ + ,\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/gundalow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"gundalow\"\ + ,\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/940557?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\"\ + :\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + }]},{\"sha\":\"0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjAwNzNlOWUwNzQwODFiYzI1ODhiOWVlMzExZmMwMWJjOWFkZmE5Njc=\"\ + ,\"commit\":{\"author\":{\"name\":\"John Barker\",\"email\":\"john@johnrbarker.com\"\ + ,\"date\":\"2017-12-09T13:33:54Z\"},\"committer\":{\"name\":\"John Barker\"\ + ,\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:33:54Z\"},\"message\"\ + :\"Valid badge\",\"tree\":{\"sha\":\"c6dad62f00e288976a31e2c6c189f18fc23881e4\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c6dad62f00e288976a31e2c6c189f18fc23881e4\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967/comments\"\ + ,\"author\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\"\ + ,\"avatar_url\":\"https://avatars.githubusercontent.com/u/940557?v=4\",\"gravatar_id\"\ + :\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\"\ + ,\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\"\ + :\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\"\ + ,\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/gundalow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"gundalow\"\ + ,\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/940557?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\"\ + :\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"8d437d531af955c068c03f35a1f6f19667c6d215\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d437d531af955c068c03f35a1f6f19667c6d215\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8d437d531af955c068c03f35a1f6f19667c6d215\"\ + }]},{\"sha\":\"8d437d531af955c068c03f35a1f6f19667c6d215\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjhkNDM3ZDUzMWFmOTU1YzA2OGMwM2YzNWExZjZmMTk2NjdjNmQyMTU=\"\ + ,\"commit\":{\"author\":{\"name\":\"John Barker\",\"email\":\"john@johnrbarker.com\"\ + ,\"date\":\"2017-12-09T13:28:02Z\"},\"committer\":{\"name\":\"John Barker\"\ + ,\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:28:02Z\"},\"message\"\ + :\"Convert README.md to RST\\n\\n* Update formatting fixes #3\\n* Add example\ + \ badge (and how to use)\\n* Make it cleared that bash uploader should be used\"\ + ,\"tree\":{\"sha\":\"3bdc420145fb2cc4232a0ba454675b84311c4bc4\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3bdc420145fb2cc4232a0ba454675b84311c4bc4\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8d437d531af955c068c03f35a1f6f19667c6d215\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d437d531af955c068c03f35a1f6f19667c6d215\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8d437d531af955c068c03f35a1f6f19667c6d215\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d437d531af955c068c03f35a1f6f19667c6d215/comments\"\ + ,\"author\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\"\ + ,\"avatar_url\":\"https://avatars.githubusercontent.com/u/940557?v=4\",\"gravatar_id\"\ + :\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\"\ + ,\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\"\ + :\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\"\ + ,\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/gundalow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"gundalow\"\ + ,\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/940557?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\"\ + :\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + }]},{\"sha\":\"1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjFlOTA2MTYwYzA5MTI4NzY1YTc1YWZiY2JkNjBkMWNiZDNjOGQxMGE=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2017-10-04T09:43:08Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2017-10-04T09:43:08Z\"},\"message\":\"Merge\ + \ pull request #22 from IanLee1521/patch-1\\n\\nFixed minor typo\",\"tree\"\ + :{\"sha\":\"4b1780a3cac3fcfe3356b1da70346f19f63106b7\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4b1780a3cac3fcfe3356b1da70346f19f63106b7\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + },{\"sha\":\"b066c93c2676bc957d971d2c4188e77b3e383b77\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b066c93c2676bc957d971d2c4188e77b3e383b77\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b066c93c2676bc957d971d2c4188e77b3e383b77\"\ + }]},{\"sha\":\"b066c93c2676bc957d971d2c4188e77b3e383b77\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmIwNjZjOTNjMjY3NmJjOTU3ZDk3MWQyYzQxODhlNzdiM2UzODNiNzc=\"\ + ,\"commit\":{\"author\":{\"name\":\"Ian Lee\",\"email\":\"IanLee1521@gmail.com\"\ + ,\"date\":\"2017-09-29T19:29:42Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2017-09-29T19:29:42Z\"},\"message\":\"Fixed\ + \ minor typo\",\"tree\":{\"sha\":\"4b1780a3cac3fcfe3356b1da70346f19f63106b7\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4b1780a3cac3fcfe3356b1da70346f19f63106b7\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b066c93c2676bc957d971d2c4188e77b3e383b77\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b066c93c2676bc957d971d2c4188e77b3e383b77\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b066c93c2676bc957d971d2c4188e77b3e383b77\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b066c93c2676bc957d971d2c4188e77b3e383b77/comments\"\ + ,\"author\":{\"login\":\"IanLee1521\",\"id\":828452,\"node_id\":\"MDQ6VXNlcjgyODQ1Mg==\"\ + ,\"avatar_url\":\"https://avatars.githubusercontent.com/u/828452?v=4\",\"gravatar_id\"\ + :\"\",\"url\":\"https://api.github.com/users/IanLee1521\",\"html_url\":\"https://github.com/IanLee1521\"\ + ,\"followers_url\":\"https://api.github.com/users/IanLee1521/followers\",\"\ + following_url\":\"https://api.github.com/users/IanLee1521/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/IanLee1521/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/IanLee1521/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/IanLee1521/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/IanLee1521/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/IanLee1521/repos\",\"events_url\":\"https://api.github.com/users/IanLee1521/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/IanLee1521/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\"\ + ,\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + }]},{\"sha\":\"3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjMyMzA1OTRhN2FhODc4MmZiY2Y1MTMyOWIyMzk1MTE4ZjdjZjBkMTU=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2017-08-30T14:02:20Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2017-08-30T14:02:20Z\"},\"message\":\"Update\ + \ README.md\",\"tree\":{\"sha\":\"2ba18d3d080f553b23501cd8485eae3c50589e6a\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2ba18d3d080f553b23501cd8485eae3c50589e6a\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + }]},{\"sha\":\"6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjY1NjBjYmZmMzNmYmZmODc0MGYwNDA3ZjRjYWMxMDkxYmMyNWU2YWU=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2017-02-02T19:10:16Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2017-02-02T19:10:16Z\"},\"message\":\"Update\ + \ README.md\",\"tree\":{\"sha\":\"39e2c45b98a62d06d6d84222d5a8c244150ec3c4\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/39e2c45b98a62d06d6d84222d5a8c244150ec3c4\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"feb5100831541db79eb83a263986df129573f3de\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/feb5100831541db79eb83a263986df129573f3de\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/feb5100831541db79eb83a263986df129573f3de\"\ + }]},{\"sha\":\"feb5100831541db79eb83a263986df129573f3de\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmZlYjUxMDA4MzE1NDFkYjc5ZWI4M2EyNjM5ODZkZjEyOTU3M2YzZGU=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2017-02-02T19:09:38Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2017-02-02T19:09:38Z\"},\"message\":\"Merge\ + \ pull request #20 from briandant/master\\n\\nAdd further instructions for using\ + \ env vars\",\"tree\":{\"sha\":\"be98fa09eb1a8343b44071cdae6aac15e61d0cc9\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/be98fa09eb1a8343b44071cdae6aac15e61d0cc9\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/feb5100831541db79eb83a263986df129573f3de\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/feb5100831541db79eb83a263986df129573f3de\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/feb5100831541db79eb83a263986df129573f3de\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/feb5100831541db79eb83a263986df129573f3de/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + },{\"sha\":\"6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6802411a35f438bf62ee8a1c1928cd36ca50d534\"\ + }]},{\"sha\":\"6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjY4MDI0MTFhMzVmNDM4YmY2MmVlOGExYzE5MjhjZDM2Y2E1MGQ1MzQ=\"\ + ,\"commit\":{\"author\":{\"name\":\"Brian Dant\",\"email\":\"briandant@users.noreply.github.com\"\ + ,\"date\":\"2017-02-02T18:37:42Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2017-02-02T18:37:42Z\"},\"message\":\"Add\ + \ further instructions for using env vars\",\"tree\":{\"sha\":\"be98fa09eb1a8343b44071cdae6aac15e61d0cc9\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/be98fa09eb1a8343b44071cdae6aac15e61d0cc9\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6802411a35f438bf62ee8a1c1928cd36ca50d534\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534/comments\"\ + ,\"author\":{\"login\":\"briandant\",\"id\":1884902,\"node_id\":\"MDQ6VXNlcjE4ODQ5MDI=\"\ + ,\"avatar_url\":\"https://avatars.githubusercontent.com/u/1884902?v=4\",\"gravatar_id\"\ + :\"\",\"url\":\"https://api.github.com/users/briandant\",\"html_url\":\"https://github.com/briandant\"\ + ,\"followers_url\":\"https://api.github.com/users/briandant/followers\",\"following_url\"\ + :\"https://api.github.com/users/briandant/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/briandant/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/briandant/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/briandant/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/briandant/orgs\",\"repos_url\":\"https://api.github.com/users/briandant/repos\"\ + ,\"events_url\":\"https://api.github.com/users/briandant/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/briandant/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\"\ + ,\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + }]},{\"sha\":\"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjNmMjhlOTNjYmMxZDI2NmMzMGE1YmRmNDMxMjZlZDQ0ZmJlYjAwOWQ=\"\ + ,\"commit\":{\"author\":{\"name\":\"Codecov Test Bot\",\"email\":\"hello@codecov.io\"\ + ,\"date\":\"2016-09-29T10:37:07Z\"},\"committer\":{\"name\":\"Codecov Test Bot\"\ + ,\"email\":\"hello@codecov.io\",\"date\":\"2016-09-29T10:37:07Z\"},\"message\"\ + :\"Circle build #355\\nhttps://circleci.com/gh/codecov/testsuite/355\\nbash\ + \ <(curl -s https://raw.githubusercontent.com/codecov/codecov-bash/master/codecov)\ + \ -v -u https://codecov.io\",\"tree\":{\"sha\":\"80c741dbd6916ef10fadd2357efdcac15402af49\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/80c741dbd6916ef10fadd2357efdcac15402af49\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d/comments\"\ + ,\"author\":{\"login\":\"codecov-test\",\"id\":8485477,\"node_id\":\"MDQ6VXNlcjg0ODU0Nzc=\"\ + ,\"avatar_url\":\"https://avatars.githubusercontent.com/u/8485477?v=4\",\"gravatar_id\"\ + :\"\",\"url\":\"https://api.github.com/users/codecov-test\",\"html_url\":\"\ + https://github.com/codecov-test\",\"followers_url\":\"https://api.github.com/users/codecov-test/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov-test/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov-test/gists{/gist_id}\"\ + ,\"starred_url\":\"https://api.github.com/users/codecov-test/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/codecov-test/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/codecov-test/orgs\",\"\ + repos_url\":\"https://api.github.com/users/codecov-test/repos\",\"events_url\"\ + :\"https://api.github.com/users/codecov-test/events{/privacy}\",\"received_events_url\"\ + :\"https://api.github.com/users/codecov-test/received_events\",\"type\":\"User\"\ + ,\"site_admin\":false},\"committer\":{\"login\":\"codecov-test\",\"id\":8485477,\"\ + node_id\":\"MDQ6VXNlcjg0ODU0Nzc=\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/8485477?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov-test\"\ + ,\"html_url\":\"https://github.com/codecov-test\",\"followers_url\":\"https://api.github.com/users/codecov-test/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov-test/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov-test/gists{/gist_id}\"\ + ,\"starred_url\":\"https://api.github.com/users/codecov-test/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/codecov-test/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/codecov-test/orgs\",\"\ + repos_url\":\"https://api.github.com/users/codecov-test/repos\",\"events_url\"\ + :\"https://api.github.com/users/codecov-test/events{/privacy}\",\"received_events_url\"\ + :\"https://api.github.com/users/codecov-test/received_events\",\"type\":\"User\"\ + ,\"site_admin\":false},\"parents\":[{\"sha\":\"63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + }]},{\"sha\":\"63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjYzZjU3NDBjMzNmMmFhYzY4ZmQwNzU3ZjQ3MWFiNzQxZjllMjBhMDU=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2016-08-26T19:21:23Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2016-08-26T19:21:23Z\"},\"message\":\"Update\ + \ README.md\",\"tree\":{\"sha\":\"80c741dbd6916ef10fadd2357efdcac15402af49\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/80c741dbd6916ef10fadd2357efdcac15402af49\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + }]},{\"sha\":\"d61bb41b849de7125ca17fbd37292479648e7fa7\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmQ2MWJiNDFiODQ5ZGU3MTI1Y2ExN2ZiZDM3MjkyNDc5NjQ4ZTdmYTc=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2016-08-16T15:47:11Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2016-08-16T15:47:11Z\"},\"message\":\"Merge\ + \ pull request #18 from yurovant/patch-1\\n\\nUpdate README.md\",\"tree\":{\"\ + sha\":\"6f202265ab6e9bbe035e9567729cef9d042faa2d\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6f202265ab6e9bbe035e9567729cef9d042faa2d\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d61bb41b849de7125ca17fbd37292479648e7fa7/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + },{\"sha\":\"2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2d1b772e138f05dbbd577ce0dcf3633577629a76\"\ + }]},{\"sha\":\"2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjJkMWI3NzJlMTM4ZjA1ZGJiZDU3N2NlMGRjZjM2MzM1Nzc2MjlhNzY=\"\ + ,\"commit\":{\"author\":{\"name\":\"Anton Yurovskykh\",\"email\":\"anton.yurovskykh@gmail.com\"\ + ,\"date\":\"2016-08-16T07:35:54Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2016-08-16T07:35:54Z\"},\"message\":\"Update\ + \ README.md\\n\\ntypo: priveta --> private\",\"tree\":{\"sha\":\"6f202265ab6e9bbe035e9567729cef9d042faa2d\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6f202265ab6e9bbe035e9567729cef9d042faa2d\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2d1b772e138f05dbbd577ce0dcf3633577629a76\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76/comments\"\ + ,\"author\":{\"login\":\"yurovant\",\"id\":11337124,\"node_id\":\"MDQ6VXNlcjExMzM3MTI0\"\ + ,\"avatar_url\":\"https://avatars.githubusercontent.com/u/11337124?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/yurovant\",\"html_url\"\ + :\"https://github.com/yurovant\",\"followers_url\":\"https://api.github.com/users/yurovant/followers\"\ + ,\"following_url\":\"https://api.github.com/users/yurovant/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/yurovant/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/yurovant/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/yurovant/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/yurovant/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/yurovant/repos\",\"events_url\":\"https://api.github.com/users/yurovant/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/yurovant/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\"\ + ,\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + }]},{\"sha\":\"84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg0ZWE4YjliYTBmODEzNGJlMDQ3Nzk3MWU3MmIyMzM5NTlmNWQzYjY=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2016-08-05T16:46:35Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2016-08-05T16:46:35Z\"},\"message\":\"Update\ + \ README.md\",\"tree\":{\"sha\":\"9b948b519e86c5089a2c9c00d0a8f326282bc5cd\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9b948b519e86c5089a2c9c00d0a8f326282bc5cd\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"e051e55647e0ecc27539b2dc40fb9b2839383060\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e051e55647e0ecc27539b2dc40fb9b2839383060\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/e051e55647e0ecc27539b2dc40fb9b2839383060\"\ + }]}]" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 03 Jan 2024 18:11:51 GMT + ETag: + - W/"19b5433da36c257c2c6186fe10a5f3da805b681c6c41af436acca69689fbea75" + Last-Modified: + - Thu, 10 Jan 2019 01:39:55 GMT + Link: + - ; + rel="next", ; + rel="last" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF6B:3F66:BDF9207:18C52D70:6595A367 + X-OAuth-Scopes: + - '' + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4996' + X-RateLimit-Reset: + - '1704309110' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '4' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2024-02-02 18:10:44 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"name": "web", "active": true, "events": ["pull_request", "delete", "push", + "public", "status", "repository"], "config": {"url": "https://codecov.io/webhooks/github", + "secret": "ab164bf3f7d947f2a0681b215404873e", "content_type": "json"}}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '238' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/hooks + response: + content: '{"message":"Not Found","documentation_url":"https://docs.github.com/rest/repos/webhooks#create-a-repository-webhook"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 03 Jan 2024 18:11:51 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - admin:repo_hook, public_repo, repo, write:repo_hook + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF6D:1613:C048B28:190BD8E5:6595A367 + X-OAuth-Scopes: + - '' + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4995' + X-RateLimit-Reset: + - '1704309110' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '5' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2024-02-02 18:10:44 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 404 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/contents?ref=abf6d4df662c47e32460020ab14abf9303581429 + response: + content: '[{"name":".coverage","path":".coverage","sha":"23e6e577d9e906f1fa619e8a8672d9537ff27326","size":335,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.coverage?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.coverage","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/23e6e577d9e906f1fa619e8a8672d9537ff27326","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/.coverage","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.coverage?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/23e6e577d9e906f1fa619e8a8672d9537ff27326","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.coverage"}},{"name":".travis.yml","path":".travis.yml","sha":"11d295c04e2feac0a533bf5b1df52b84f8076eec","size":144,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.travis.yml?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.travis.yml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/11d295c04e2feac0a533bf5b1df52b84f8076eec","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/.travis.yml","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.travis.yml?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/11d295c04e2feac0a533bf5b1df52b84f8076eec","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.travis.yml"}},{"name":"README.rst","path":"README.rst","sha":"405d834472636bc2c83dd8bd6818e4b2c2871e86","size":6377,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.rst?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/README.rst","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/405d834472636bc2c83dd8bd6818e4b2c2871e86","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/README.rst","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.rst?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/405d834472636bc2c83dd8bd6818e4b2c2871e86","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/README.rst"}},{"name":"awesome","path":"awesome","sha":"b25dbf08d0d6d35990c0fcec549b91f96cace181","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/awesome","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b25dbf08d0d6d35990c0fcec549b91f96cace181","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b25dbf08d0d6d35990c0fcec549b91f96cace181","html":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/awesome"}},{"name":"coverage.xml","path":"coverage.xml","sha":"d4076376a0834a54939fe778da2f0524e10bace0","size":1846,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/coverage.xml?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/d4076376a0834a54939fe778da2f0524e10bace0","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/coverage.xml?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/d4076376a0834a54939fe778da2f0524e10bace0","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml"}},{"name":"tests","path":"tests","sha":"2b1270d7021356b875c31501edb897038ddd589c","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/tests","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2b1270d7021356b875c31501edb897038ddd589c","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2b1270d7021356b875c31501edb897038ddd589c","html":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/tests"}}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 03 Jan 2024 18:11:51 GMT + ETag: + - W/"88796ed5686cfd7dffdb2db34b6b12ccaefde90e" + Last-Modified: + - Tue, 04 Jul 2023 20:51:23 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF6E:7FE2:BC59D3F:188F1D6F:6595A367 + X-OAuth-Scopes: + - '' + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4994' + X-RateLimit-Reset: + - '1704309110' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '6' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2024-02-02 18:10:44 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_proper_parent.yaml b/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_proper_parent.yaml new file mode 100644 index 0000000000..e334abbd46 --- /dev/null +++ b/apps/worker/tasks/tests/unit/cassetes/test_upload_task/TestUploadTaskIntegration/test_upload_task_proper_parent.yaml @@ -0,0 +1,400 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 + response: + content: '{"sha":"abf6d4df662c47e32460020ab14abf9303581429","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmFiZjZkNGRmNjYyYzQ3ZTMyNDYwMDIwYWIxNGFiZjkzMDM1ODE0Mjk=","commit":{"author":{"name":"Thiago + Ribeiro Ramos","email":"thiago@ribeiroramos.com","date":"2019-01-10T01:39:55Z"},"committer":{"name":"Thiago + Ribeiro Ramos","email":"thiago@ribeiroramos.com","date":"2019-01-10T01:39:55Z"},"message":"dsidsahdsahdsa","tree":{"sha":"88796ed5686cfd7dffdb2db34b6b12ccaefde90e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/88796ed5686cfd7dffdb2db34b6b12ccaefde90e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/abf6d4df662c47e32460020ab14abf9303581429","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/commit/abf6d4df662c47e32460020ab14abf9303581429","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429/comments","author":null,"committer":null,"parents":[{"sha":"c5b67303452bbff57cc1f49984339cde39eb1db5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5b67303452bbff57cc1f49984339cde39eb1db5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c5b67303452bbff57cc1f49984339cde39eb1db5"}],"stats":{"total":6,"additions":5,"deletions":1},"files":[{"sha":"770f2e8c26296c665565d336490a5306380b61d4","filename":"awesome/__init__.py","status":"modified","additions":4,"deletions":0,"changes":4,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/awesome/__init__.py","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/abf6d4df662c47e32460020ab14abf9303581429/awesome/__init__.py","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/__init__.py?ref=abf6d4df662c47e32460020ab14abf9303581429","patch":"@@ + -10,3 +10,7 @@ def fib(n):\n if n < 2:\n return 1\n return fib(n + - 2) + fib(n - 1)\n+\n+\n+def coala(k):\n+ return k * k"},{"sha":"d4076376a0834a54939fe778da2f0524e10bace0","filename":"coverage.xml","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/coverage.xml?ref=abf6d4df662c47e32460020ab14abf9303581429","patch":"@@ + -1,5 +1,5 @@\n \n-\n+\n \t\n \t\n \t"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 11 Dec 2019 19:42:45 GMT + Etag: + - W/"e7b5f4329ef951f5157b29b27f6f5602" + Last-Modified: + - Thu, 10 Jan 2019 01:39:55 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 3842:3AD1:203F89:2E1361:5DF146B4 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4999' + X-Ratelimit-Reset: + - '1576096965' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abf6d4df662c47e32460020ab14abf9303581429 +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1","id":229193531,"node_id":"MDExOlB1bGxSZXF1ZXN0MjI5MTkzNTMx","html_url":"https://github.com/ThiagoCodecov/example-python/pull/1","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/1.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/1.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1","number":1,"state":"closed","locked":false,"title":"Creating + new code for reasons no one knows","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"Why + you ask?\r\n\r\nI dont know","created_at":"2018-11-07T22:44:49Z","updated_at":"2019-12-09T12:13:24Z","closed_at":"2019-09-09T22:23:11Z","merged_at":"2019-09-09T22:23:11Z","merge_commit_sha":"038ac8ac2127baa19a927c67f0d5168d9928abf3","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/119c1907fb266f374b8440bbd70dccbea54daf8f","head":{"label":"ThiagoCodecov:reason/some-testing","ref":"reason/some-testing","sha":"119c1907fb266f374b8440bbd70dccbea54daf8f","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2019-12-09T12:18:20Z","pushed_at":"2019-12-09T12:19:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":65,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":3,"license":null,"forks":0,"open_issues":3,"watchers":0,"default_branch":"master"}},"base":{"label":"ThiagoCodecov:master","ref":"master","sha":"68946ef98daec68c7798459150982fc799c87d85","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2019-12-09T12:18:20Z","pushed_at":"2019-12-09T12:19:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":65,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":3,"license":null,"forks":0,"open_issues":3,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/1"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/119c1907fb266f374b8440bbd70dccbea54daf8f"}},"author_association":"OWNER","merged":true,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"comments":3,"review_comments":0,"maintainer_can_modify":false,"commits":10,"additions":48,"deletions":6,"changed_files":5}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 11 Dec 2019 19:42:45 GMT + Etag: + - W/"e7aaf1e04a864807d6a7a10735b429e2" + Last-Modified: + - Mon, 09 Dec 2019 12:13:24 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 3843:3AD3:13D597:1D8E07:5DF146B5 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4998' + X-Ratelimit-Reset: + - '1576096965' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1 +- request: + body: '{"name": "web", "active": true, "events": ["pull_request", "delete", "push", + "public", "status", "repository"], "config": {"url": "https://codecov.io/webhooks/github", + "secret": "test46nudghi6oft49bay37keyiuhok7", "content_type": "json"}}' + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/hooks + response: + content: '{"message":"Validation Failed","errors":[{"resource":"Hook","code":"custom","message":"Hook + already exists on this repository"}],"documentation_url":"https://developer.github.com/v3/repos/hooks/#create-a-hook"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Connection: + - close + Content-Length: + - '210' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 11 Dec 2019 19:42:46 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 422 Unprocessable Entity + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + X-Accepted-Oauth-Scopes: + - admin:repo_hook, public_repo, repo, write:repo_hook + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 384F:3AD1:203F96:2E1371:5DF146B5 + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4997' + X-Ratelimit-Reset: + - '1576096965' + X-Xss-Protection: + - 1; mode=block + status: + code: 422 + message: Unprocessable Entity + status_code: 422 + url: https://api.github.com/repos/ThiagoCodecov/example-python/hooks +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits + response: + content: '[{"sha":"587662b6e5403ae0d126e0c7839a8d98382c4760","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjU4NzY2MmI2ZTU0MDNhZTBkMTI2ZTBjNzgzOWE4ZDk4MzgyYzQ3NjA=","commit":{"author":{"name":"Thiago + Ribeiro Ramos","email":"thiago@ribeiroramos.com","date":"2018-11-07T22:43:54Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:34:45Z"},"message":"Creating + new code for reasons no one knows","tree":{"sha":"ec56802a37b981f13bdc3c9a56ae68ef82ab424a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ec56802a37b981f13bdc3c9a56ae68ef82ab424a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","html_url":"https://github.com/ThiagoCodecov/example-python/commit/587662b6e5403ae0d126e0c7839a8d98382c4760","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760/comments","author":null,"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"68946ef98daec68c7798459150982fc799c87d85","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/68946ef98daec68c7798459150982fc799c87d85","html_url":"https://github.com/ThiagoCodecov/example-python/commit/68946ef98daec68c7798459150982fc799c87d85"}]},{"sha":"03a8b737cb9d8585076ebdbac7b7235c8da0620d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjAzYThiNzM3Y2I5ZDg1ODUwNzZlYmRiYWM3YjcyMzVjOGRhMDYyMGQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-03-12T02:37:19Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:36:02Z"},"message":"Now + what","tree":{"sha":"51a385e1f575447b0b70fd597596c32c4f5bd172","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/51a385e1f575447b0b70fd597596c32c4f5bd172"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/03a8b737cb9d8585076ebdbac7b7235c8da0620d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"587662b6e5403ae0d126e0c7839a8d98382c4760","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","html_url":"https://github.com/ThiagoCodecov/example-python/commit/587662b6e5403ae0d126e0c7839a8d98382c4760"}]},{"sha":"bf9b57cf7b169806ae2d18d7671aba3825b99203","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmJmOWI1N2NmN2IxNjk4MDZhZTJkMThkNzY3MWFiYTM4MjViOTkyMDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-03-12T02:42:33Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:36:02Z"},"message":"Adding + untested code","tree":{"sha":"ce5383a6feb3e0bf20a4df46ae6c67ec3955723e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ce5383a6feb3e0bf20a4df46ae6c67ec3955723e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bf9b57cf7b169806ae2d18d7671aba3825b99203","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"03a8b737cb9d8585076ebdbac7b7235c8da0620d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/03a8b737cb9d8585076ebdbac7b7235c8da0620d"}]},{"sha":"cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmNlZGUxOWNiMzEwY2Q0Y2RkZmI1ZDg5MjFjYjhkMGNjN2M3YzE1MDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-16T22:02:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-16T22:11:24Z"},"message":"asdadafdsfdsfds","tree":{"sha":"e614247adf8a0705575e9c2170fad7c2848870a0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e614247adf8a0705575e9c2170fad7c2848870a0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"bf9b57cf7b169806ae2d18d7671aba3825b99203","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bf9b57cf7b169806ae2d18d7671aba3825b99203"}]},{"sha":"ea3ada938db123368d62b0133e7c5bb54b5292b9","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmVhM2FkYTkzOGRiMTIzMzY4ZDYyYjAxMzNlN2M1YmI1NGI1MjkyYjk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-19T18:48:19Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-19T18:48:19Z"},"message":"Adding + file t2 haha","tree":{"sha":"9ac6564d515ed2630026080e7cbdad4edfa9eca6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9ac6564d515ed2630026080e7cbdad4edfa9eca6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ea3ada938db123368d62b0133e7c5bb54b5292b9","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503"}]},{"sha":"2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjIwNDhiMjc3ZGQ2NTQyZjE4NGM2YTMwYzNlMmIwZjNlZTVlZWFmNGI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:43:42Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:43:42Z"},"message":"Adding + file t2 haha oooggg","tree":{"sha":"8b8d478591c3125af92ac395e87ddfb37fec5086","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/8b8d478591c3125af92ac395e87ddfb37fec5086"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ea3ada938db123368d62b0133e7c5bb54b5292b9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ea3ada938db123368d62b0133e7c5bb54b5292b9"}]},{"sha":"119de54e3cfdf8227a8556b9f5730c328a1390cd","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjExOWRlNTRlM2NmZGY4MjI3YTg1NTZiOWY1NzMwYzMyOGExMzkwY2Q=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:46:16Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:46:16Z"},"message":"Adding + file t2 haha oooggdsadsdsag","tree":{"sha":"d3868402c41afd8dcafb50e5bfa0e023f35c307e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/d3868402c41afd8dcafb50e5bfa0e023f35c307e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119de54e3cfdf8227a8556b9f5730c328a1390cd","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b"}]},{"sha":"2d55e8501b058b6f25382c4e287f022e8938461f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJkNTVlODUwMWIwNThiNmYyNTM4MmM0ZTI4N2YwMjJlODkzODQ2MWY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-24T21:32:08Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-24T21:32:08Z"},"message":"Adding + file t4 unpredictable","tree":{"sha":"a87f6d6ddd74d6df712bad79cc65d040c408efe8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/a87f6d6ddd74d6df712bad79cc65d040c408efe8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2d55e8501b058b6f25382c4e287f022e8938461f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2d55e8501b058b6f25382c4e287f022e8938461f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"119de54e3cfdf8227a8556b9f5730c328a1390cd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119de54e3cfdf8227a8556b9f5730c328a1390cd"}]},{"sha":"364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjM2NGJkZmJjNzJkNWUwNWI1MjBmMDMyMGIwZDhiMzlmZDllYTY5MmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-28T22:50:25Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-28T22:50:25Z"},"message":"Adding + Makefile","tree":{"sha":"452c48e858913bacb4be63a8e2351c98719406dd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/452c48e858913bacb4be63a8e2351c98719406dd"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2d55e8501b058b6f25382c4e287f022e8938461f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2d55e8501b058b6f25382c4e287f022e8938461f"}]},{"sha":"119c1907fb266f374b8440bbd70dccbea54daf8f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjExOWMxOTA3ZmIyNjZmMzc0Yjg0NDBiYmQ3MGRjY2JlYTU0ZGFmOGY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-09-02T23:07:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-09-02T23:07:56Z"},"message":"Cleaning + some stuff","tree":{"sha":"4995d75a388061164491217b50ee296137150f89","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4995d75a388061164491217b50ee296137150f89"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/119c1907fb266f374b8440bbd70dccbea54daf8f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119c1907fb266f374b8440bbd70dccbea54daf8f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119c1907fb266f374b8440bbd70dccbea54daf8f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119c1907fb266f374b8440bbd70dccbea54daf8f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - private, max-age=60, s-maxage=60 + Connection: + - close + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 10 Jan 2020 03:16:27 GMT + Etag: + - W/"6a255c332b90087940cdb3a3dcd00be9" + Last-Modified: + - Thu, 09 Jan 2020 19:23:25 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - 1223:785F:2E2C60:40DDCD:5E17EC8A + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4992' + X-Ratelimit-Reset: + - '1578629688' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/contents?ref=abf6d4df662c47e32460020ab14abf9303581429 + response: + content: '[{"name":".coverage","path":".coverage","sha":"23e6e577d9e906f1fa619e8a8672d9537ff27326","size":335,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.coverage?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.coverage","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/23e6e577d9e906f1fa619e8a8672d9537ff27326","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/.coverage","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.coverage?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/23e6e577d9e906f1fa619e8a8672d9537ff27326","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.coverage"}},{"name":".travis.yml","path":".travis.yml","sha":"11d295c04e2feac0a533bf5b1df52b84f8076eec","size":144,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.travis.yml?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.travis.yml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/11d295c04e2feac0a533bf5b1df52b84f8076eec","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/.travis.yml","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.travis.yml?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/11d295c04e2feac0a533bf5b1df52b84f8076eec","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/.travis.yml"}},{"name":"README.rst","path":"README.rst","sha":"405d834472636bc2c83dd8bd6818e4b2c2871e86","size":6377,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.rst?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/README.rst","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/405d834472636bc2c83dd8bd6818e4b2c2871e86","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/README.rst","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.rst?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/405d834472636bc2c83dd8bd6818e4b2c2871e86","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/README.rst"}},{"name":"awesome","path":"awesome","sha":"b25dbf08d0d6d35990c0fcec549b91f96cace181","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/awesome","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b25dbf08d0d6d35990c0fcec549b91f96cace181","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b25dbf08d0d6d35990c0fcec549b91f96cace181","html":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/awesome"}},{"name":"coverage.xml","path":"coverage.xml","sha":"d4076376a0834a54939fe778da2f0524e10bace0","size":1846,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/coverage.xml?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/d4076376a0834a54939fe778da2f0524e10bace0","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/coverage.xml?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/d4076376a0834a54939fe778da2f0524e10bace0","html":"https://github.com/ThiagoCodecov/example-python/blob/abf6d4df662c47e32460020ab14abf9303581429/coverage.xml"}},{"name":"tests","path":"tests","sha":"2b1270d7021356b875c31501edb897038ddd589c","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=abf6d4df662c47e32460020ab14abf9303581429","html_url":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/tests","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2b1270d7021356b875c31501edb897038ddd589c","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=abf6d4df662c47e32460020ab14abf9303581429","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2b1270d7021356b875c31501edb897038ddd589c","html":"https://github.com/ThiagoCodecov/example-python/tree/abf6d4df662c47e32460020ab14abf9303581429/tests"}}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 31 Jul 2020 16:34:55 GMT + Etag: + - W/"88796ed5686cfd7dffdb2db34b6b12ccaefde90e" + Last-Modified: + - Tue, 24 Mar 2020 22:01:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3 + X-Github-Request-Id: + - F4FE:2CF6:C69A44:11BB1F7:5F24482E + X-Http-Reason: + - OK + X-Oauth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-Ratelimit-Limit: + - '5000' + X-Ratelimit-Remaining: + - '4999' + X-Ratelimit-Reset: + - '1596216895' + X-Xss-Protection: + - 1; mode=block + status: + code: 200 + message: OK + status_code: 200 + url: https://api.github.com/repos/ThiagoCodecov/example-python/contents?ref=abf6d4df662c47e32460020ab14abf9303581429 +version: 1 diff --git a/apps/worker/tasks/tests/unit/conftest.py b/apps/worker/tasks/tests/unit/conftest.py new file mode 100644 index 0000000000..052bd27286 --- /dev/null +++ b/apps/worker/tasks/tests/unit/conftest.py @@ -0,0 +1,158 @@ +# TODO: Clean this + +import pytest +from shared.reports.readonly import ReadOnlyReport +from shared.reports.reportfile import ReportFile +from shared.reports.resources import Report +from shared.reports.types import ReportLine +from shared.utils.sessions import Session + +from database.tests.factories import CommitFactory, PullFactory, RepositoryFactory +from services.comparison import ComparisonProxy +from services.comparison.types import Comparison, FullCommit +from services.repository import EnrichedPull + + +def get_small_report(flags=None): + if flags is None: + flags = ["integration"] + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(11, 20)) + ) + first_file.append(3, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append(51, ReportLine.create(coverage=0, sessions=[[0, 1]])) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=flags)) + return report + + +@pytest.fixture +def sample_report(): + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=["unit"])) + return report + + +@pytest.fixture +def sample_report_with_multiple_flags(): + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=["unit"])) + report.add_session(Session(flags=["integration"])) + return report + + +@pytest.fixture +def sample_report_without_flags(): + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, ReportLine.create(coverage=1, sessions=[[0, 1]], complexity=(10, 2)) + ) + first_file.append(2, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(3, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(5, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(6, ReportLine.create(coverage=0, sessions=[[0, 1]])) + first_file.append(8, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(9, ReportLine.create(coverage=1, sessions=[[0, 1]])) + first_file.append(10, ReportLine.create(coverage=0, sessions=[[0, 1]])) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, 1]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session(Session(flags=None)) + return report + + +@pytest.fixture +def sample_comparison(dbsession, request, sample_report): + repository = RepositoryFactory.create( + owner__username=request.node.name, owner__service="github" + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository, author__service="github") + head_commit = CommitFactory.create( + repository=repository, branch="new_branch", author__service="github" + ) + pull = PullFactory.create( + repository=repository, base=base_commit.commitid, head=head_commit.commitid + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + repository = base_commit.repository + base_full_commit = FullCommit( + commit=base_commit, report=ReadOnlyReport.create_from_report(get_small_report()) + ) + head_full_commit = FullCommit( + commit=head_commit, report=ReadOnlyReport.create_from_report(sample_report) + ) + return ComparisonProxy( + Comparison( + head=head_full_commit, + project_coverage_base=base_full_commit, + patch_coverage_base_commitid=base_commit.commitid, + enriched_pull=EnrichedPull( + database_pull=pull, + provider_pull={ + "author": {"id": "12345", "username": "codecov-test-user"}, + "base": {"branch": "master", "commitid": base_commit.commitid}, + "head": { + "branch": "reason/some-testing", + "commitid": head_commit.commitid, + }, + "number": str(pull.pullid), + "id": str(pull.pullid), + "state": "open", + "title": "Creating new code for reasons no one knows", + }, + ), + ) + ) diff --git a/apps/worker/tasks/tests/unit/samples/sample_opentelem_collected.json b/apps/worker/tasks/tests/unit/samples/sample_opentelem_collected.json new file mode 100644 index 0000000000..7640b2cc2c --- /dev/null +++ b/apps/worker/tasks/tests/unit/samples/sample_opentelem_collected.json @@ -0,0 +1,5176 @@ +{ + "files": [ + { + "filename": "helpers/logging_config.py", + "ln_ex_ct": [ + [ + 11, + 5 + ], + [ + 12, + 5 + ], + [ + 13, + 5 + ], + [ + 14, + 5 + ], + [ + 15, + 5 + ], + [ + 24, + 5 + ], + [ + 25, + 5 + ], + [ + 26, + 5 + ], + [ + 27, + 5 + ], + [ + 28, + 5 + ], + [ + 30, + 5 + ] + ] + }, + { + "filename": "services/redis.py", + "ln_ex_ct": [ + [ + 12, + 5 + ], + [ + 13, + 5 + ], + [ + 14, + 5 + ], + [ + 21, + 5 + ], + [ + 22, + 5 + ], + [ + 26, + 5 + ] + ] + }, + { + "filename": "tasks/base.py", + "ln_ex_ct": [ + [ + 39, + 5 + ], + [ + 43, + 5 + ], + [ + 45, + 5 + ], + [ + 47, + 5 + ], + [ + 50, + 5 + ], + [ + 51, + 5 + ], + [ + 52, + 5 + ], + [ + 53, + 5 + ], + [ + 54, + 5 + ], + [ + 55, + 2 + ], + [ + 62, + 2 + ], + [ + 81, + 5 + ], + [ + 98, + 5 + ], + [ + 99, + 5 + ], + [ + 100, + 5 + ], + [ + 123, + 2 + ], + [ + 124, + 2 + ], + [ + 125, + 2 + ], + [ + 128, + 3 + ], + [ + 129, + 3 + ], + [ + 130, + 3 + ] + ] + }, + { + "filename": "tasks/upload.py", + "ln_ex_ct": [ + [ + 99, + 3 + ], + [ + 102, + 3 + ], + [ + 103, + 3 + ], + [ + 104, + 2 + ], + [ + 105, + 1 + ], + [ + 133, + 3 + ], + [ + 134, + 3 + ], + [ + 136, + 3 + ], + [ + 139, + 3 + ], + [ + 140, + 3 + ], + [ + 141, + 3 + ], + [ + 142, + 3 + ], + [ + 143, + 3 + ], + [ + 158, + 3 + ], + [ + 159, + 3 + ], + [ + 164, + 3 + ], + [ + 167, + 2 + ], + [ + 199, + 3 + ], + [ + 202, + 3 + ], + [ + 203, + 1 + ], + [ + 208, + 2 + ], + [ + 209, + 2 + ], + [ + 210, + 2 + ], + [ + 211, + 2 + ], + [ + 214, + 2 + ], + [ + 215, + 2 + ], + [ + 216, + 2 + ], + [ + 220, + 2 + ], + [ + 224, + 2 + ] + ] + }, + { + "filename": "database/base.py", + "ln_ex_ct": [ + [ + 17, + 2 + ], + [ + 32, + 2 + ] + ] + }, + { + "filename": "database/engine.py", + "ln_ex_ct": [ + [ + 18, + 2 + ], + [ + 19, + 2 + ], + [ + 24, + 2 + ] + ] + }, + { + "filename": "database/models/core.py", + "ln_ex_ct": [ + [ + 123, + 2 + ], + [ + 168, + 2 + ] + ] + }, + { + "filename": "database/models/reports.py", + "ln_ex_ct": [ + [ + 85, + 2 + ], + [ + 116, + 2 + ], + [ + 118, + 2 + ], + [ + 119, + 2 + ], + [ + 120, + 2 + ], + [ + 121, + 2 + ], + [ + 122, + 2 + ], + [ + 123, + 2 + ], + [ + 124, + 2 + ] + ] + }, + { + "filename": "helpers/cache.py", + "ln_ex_ct": [ + [ + 29, + 2 + ], + [ + 30, + 2 + ], + [ + 31, + 2 + ], + [ + 38, + 2 + ], + [ + 39, + 2 + ], + [ + 40, + 2 + ], + [ + 41, + 2 + ], + [ + 42, + 2 + ], + [ + 44, + 2 + ], + [ + 98, + 2 + ], + [ + 99, + 2 + ], + [ + 103, + 2 + ], + [ + 105, + 2 + ], + [ + 106, + 2 + ], + [ + 161, + 2 + ], + [ + 188, + 2 + ], + [ + 189, + 2 + ], + [ + 190, + 2 + ], + [ + 191, + 2 + ], + [ + 192, + 2 + ], + [ + 202, + 2 + ], + [ + 203, + 2 + ], + [ + 204, + 2 + ], + [ + 205, + 2 + ] + ] + }, + { + "filename": "helpers/pathmap/pathmap.py", + "ln_ex_ct": [ + [ + 9, + 2 + ], + [ + 16, + 2 + ], + [ + 23, + 2 + ], + [ + 24, + 2 + ], + [ + 25, + 2 + ], + [ + 26, + 2 + ], + [ + 42, + 2 + ], + [ + 44, + 2 + ], + [ + 46, + 2 + ], + [ + 47, + 2 + ], + [ + 51, + 2 + ] + ] + }, + { + "filename": "helpers/pathmap/tree.py", + "ln_ex_ct": [ + [ + 10, + 2 + ], + [ + 13, + 2 + ], + [ + 16, + 2 + ], + [ + 25, + 2 + ], + [ + 26, + 2 + ], + [ + 27, + 2 + ], + [ + 28, + 2 + ], + [ + 29, + 2 + ], + [ + 30, + 2 + ], + [ + 83, + 2 + ], + [ + 85, + 2 + ], + [ + 86, + 2 + ], + [ + 88, + 2 + ], + [ + 89, + 2 + ], + [ + 90, + 2 + ], + [ + 91, + 2 + ], + [ + 92, + 2 + ], + [ + 96, + 2 + ], + [ + 100, + 2 + ], + [ + 110, + 2 + ], + [ + 111, + 2 + ], + [ + 112, + 2 + ], + [ + 114, + 2 + ], + [ + 117, + 2 + ], + [ + 118, + 2 + ], + [ + 127, + 2 + ], + [ + 156, + 2 + ], + [ + 157, + 2 + ], + [ + 158, + 2 + ], + [ + 160, + 2 + ], + [ + 161, + 2 + ], + [ + 162, + 2 + ], + [ + 174, + 2 + ], + [ + 175, + 2 + ] + ] + }, + { + "filename": "services/archive.py", + "ln_ex_ct": [ + [ + 28, + 2 + ], + [ + 58, + 2 + ], + [ + 59, + 2 + ], + [ + 62, + 2 + ], + [ + 63, + 2 + ], + [ + 64, + 2 + ], + [ + 84, + 2 + ], + [ + 85, + 2 + ], + [ + 86, + 2 + ], + [ + 97, + 2 + ], + [ + 98, + 2 + ], + [ + 115, + 2 + ], + [ + 192, + 2 + ], + [ + 196, + 2 + ], + [ + 197, + 2 + ], + [ + 204, + 2 + ], + [ + 205, + 2 + ], + [ + 206, + 2 + ], + [ + 209, + 2 + ], + [ + 233, + 2 + ], + [ + 237, + 2 + ] + ] + }, + { + "filename": "services/bots.py", + "ln_ex_ct": [ + [ + 17, + 2 + ], + [ + 18, + 2 + ], + [ + 21, + 2 + ], + [ + 43, + 2 + ], + [ + 44, + 2 + ] + ] + }, + { + "filename": "services/repository.py", + "ln_ex_ct": [ + [ + 29, + 2 + ], + [ + 33, + 2 + ], + [ + 34, + 2 + ], + [ + 35, + 2 + ], + [ + 56, + 2 + ], + [ + 60, + 2 + ] + ] + }, + { + "filename": "services/storage.py", + "ln_ex_ct": [ + [ + 12, + 2 + ], + [ + 17, + 2 + ], + [ + 20, + 2 + ] + ] + }, + { + "filename": "services/path_fixer/__init__.py", + "ln_ex_ct": [ + [ + 41, + 2 + ], + [ + 44, + 2 + ], + [ + 45, + 2 + ], + [ + 46, + 2 + ], + [ + 47, + 2 + ], + [ + 50, + 2 + ], + [ + 51, + 2 + ], + [ + 54, + 2 + ], + [ + 64, + 2 + ], + [ + 65, + 2 + ], + [ + 66, + 2 + ], + [ + 67, + 2 + ], + [ + 68, + 2 + ], + [ + 71, + 2 + ], + [ + 72, + 2 + ], + [ + 73, + 2 + ], + [ + 74, + 2 + ], + [ + 75, + 2 + ], + [ + 78, + 2 + ], + [ + 80, + 2 + ], + [ + 81, + 2 + ], + [ + 84, + 2 + ], + [ + 85, + 2 + ], + [ + 86, + 2 + ], + [ + 90, + 2 + ], + [ + 93, + 2 + ], + [ + 96, + 2 + ], + [ + 99, + 2 + ], + [ + 102, + 2 + ], + [ + 103, + 2 + ], + [ + 104, + 2 + ], + [ + 107, + 2 + ], + [ + 112, + 2 + ], + [ + 113, + 2 + ], + [ + 118, + 2 + ], + [ + 121, + 2 + ], + [ + 122, + 2 + ], + [ + 127, + 2 + ], + [ + 152, + 2 + ], + [ + 165, + 2 + ] + ] + }, + { + "filename": "services/path_fixer/fixpaths.py", + "ln_ex_ct": [ + [ + 98, + 2 + ], + [ + 99, + 2 + ], + [ + 101, + 2 + ], + [ + 105, + 2 + ], + [ + 107, + 2 + ], + [ + 109, + 2 + ], + [ + 110, + 2 + ], + [ + 115, + 2 + ], + [ + 119, + 2 + ], + [ + 121, + 2 + ] + ] + }, + { + "filename": "services/path_fixer/user_path_fixes.py", + "ln_ex_ct": [ + [ + 25, + 2 + ], + [ + 26, + 2 + ], + [ + 29, + 2 + ], + [ + 30, + 2 + ], + [ + 31, + 2 + ], + [ + 35, + 2 + ], + [ + 44, + 2 + ] + ] + }, + { + "filename": "services/path_fixer/user_path_includes.py", + "ln_ex_ct": [ + [ + 27, + 2 + ], + [ + 28, + 2 + ], + [ + 29, + 2 + ], + [ + 56, + 2 + ], + [ + 57, + 2 + ] + ] + }, + { + "filename": "services/report/__init__.py", + "ln_ex_ct": [ + [ + 48, + 2 + ], + [ + 55, + 2 + ], + [ + 80, + 2 + ], + [ + 228, + 2 + ], + [ + 229, + 2 + ], + [ + 230, + 2 + ], + [ + 231, + 2 + ], + [ + 233, + 2 + ], + [ + 236, + 2 + ], + [ + 241, + 2 + ], + [ + 249, + 2 + ], + [ + 250, + 2 + ], + [ + 252, + 2 + ], + [ + 253, + 2 + ], + [ + 254, + 2 + ], + [ + 261, + 2 + ], + [ + 263, + 2 + ], + [ + 264, + 2 + ], + [ + 265, + 2 + ], + [ + 266, + 2 + ], + [ + 269, + 2 + ], + [ + 414, + 2 + ], + [ + 415, + 2 + ], + [ + 416, + 2 + ], + [ + 417, + 2 + ], + [ + 418, + 2 + ], + [ + 419, + 2 + ], + [ + 420, + 2 + ], + [ + 421, + 2 + ], + [ + 422, + 2 + ], + [ + 424, + 2 + ], + [ + 425, + 2 + ], + [ + 435, + 2 + ], + [ + 436, + 2 + ], + [ + 437, + 2 + ], + [ + 438, + 2 + ], + [ + 451, + 2 + ], + [ + 452, + 2 + ], + [ + 453, + 2 + ], + [ + 454, + 2 + ], + [ + 457, + 2 + ], + [ + 470, + 2 + ], + [ + 497, + 2 + ], + [ + 498, + 2 + ], + [ + 499, + 2 + ], + [ + 500, + 2 + ], + [ + 501, + 2 + ], + [ + 502, + 2 + ], + [ + 503, + 2 + ], + [ + 504, + 2 + ], + [ + 515, + 2 + ], + [ + 516, + 2 + ], + [ + 517, + 2 + ], + [ + 530, + 2 + ], + [ + 532, + 2 + ], + [ + 533, + 2 + ], + [ + 534, + 2 + ], + [ + 535, + 2 + ], + [ + 536, + 2 + ], + [ + 537, + 2 + ], + [ + 538, + 2 + ], + [ + 539, + 2 + ], + [ + 540, + 2 + ], + [ + 547, + 2 + ], + [ + 548, + 2 + ], + [ + 558, + 2 + ], + [ + 559, + 2 + ], + [ + 560, + 2 + ], + [ + 561, + 2 + ], + [ + 564, + 2 + ], + [ + 565, + 2 + ], + [ + 566, + 2 + ], + [ + 575, + 2 + ] + ] + }, + { + "filename": "services/report/parser.py", + "ln_ex_ct": [ + [ + 10, + 2 + ], + [ + 11, + 2 + ], + [ + 12, + 2 + ], + [ + 16, + 2 + ], + [ + 19, + 2 + ], + [ + 30, + 2 + ], + [ + 31, + 2 + ], + [ + 32, + 2 + ], + [ + 33, + 2 + ], + [ + 36, + 2 + ], + [ + 39, + 2 + ], + [ + 42, + 2 + ], + [ + 46, + 2 + ], + [ + 71, + 2 + ], + [ + 72, + 2 + ], + [ + 73, + 2 + ], + [ + 74, + 2 + ], + [ + 75, + 2 + ], + [ + 76, + 2 + ], + [ + 77, + 2 + ], + [ + 78, + 2 + ], + [ + 81, + 2 + ], + [ + 82, + 2 + ], + [ + 83, + 2 + ], + [ + 85, + 2 + ], + [ + 98, + 2 + ], + [ + 99, + 2 + ], + [ + 100, + 2 + ], + [ + 101, + 2 + ], + [ + 102, + 2 + ], + [ + 103, + 2 + ], + [ + 130, + 2 + ], + [ + 131, + 2 + ], + [ + 132, + 2 + ], + [ + 133, + 2 + ], + [ + 134, + 2 + ], + [ + 135, + 2 + ], + [ + 136, + 2 + ], + [ + 137, + 2 + ], + [ + 138, + 2 + ], + [ + 139, + 2 + ], + [ + 140, + 2 + ], + [ + 141, + 2 + ], + [ + 142, + 2 + ], + [ + 143, + 2 + ], + [ + 144, + 2 + ], + [ + 145, + 2 + ], + [ + 146, + 2 + ], + [ + 148, + 2 + ], + [ + 157, + 2 + ], + [ + 158, + 2 + ], + [ + 166, + 2 + ], + [ + 167, + 2 + ], + [ + 168, + 2 + ], + [ + 169, + 2 + ], + [ + 170, + 2 + ], + [ + 171, + 2 + ], + [ + 172, + 2 + ], + [ + 173, + 2 + ], + [ + 176, + 2 + ], + [ + 179, + 2 + ], + [ + 185, + 2 + ] + ] + }, + { + "filename": "services/report/raw_upload_processor.py", + "ln_ex_ct": [ + [ + 31, + 2 + ], + [ + 36, + 2 + ], + [ + 37, + 2 + ], + [ + 38, + 2 + ], + [ + 39, + 2 + ], + [ + 45, + 2 + ], + [ + 48, + 2 + ], + [ + 55, + 2 + ], + [ + 60, + 2 + ], + [ + 65, + 2 + ], + [ + 66, + 2 + ], + [ + 67, + 2 + ], + [ + 70, + 2 + ], + [ + 71, + 2 + ], + [ + 73, + 2 + ], + [ + 76, + 2 + ], + [ + 77, + 2 + ], + [ + 79, + 2 + ], + [ + 80, + 2 + ], + [ + 81, + 2 + ], + [ + 82, + 2 + ], + [ + 90, + 2 + ], + [ + 91, + 2 + ], + [ + 92, + 2 + ], + [ + 93, + 2 + ], + [ + 96, + 2 + ], + [ + 99, + 2 + ], + [ + 106, + 2 + ], + [ + 107, + 2 + ], + [ + 108, + 2 + ], + [ + 109, + 2 + ], + [ + 120, + 2 + ], + [ + 121, + 2 + ], + [ + 122, + 2 + ], + [ + 126, + 2 + ], + [ + 131, + 2 + ], + [ + 141, + 2 + ] + ] + }, + { + "filename": "services/report/report_processor.py", + "ln_ex_ct": [ + [ + 48, + 2 + ], + [ + 49, + 2 + ], + [ + 50, + 2 + ], + [ + 51, + 2 + ], + [ + 64, + 2 + ], + [ + 69, + 2 + ], + [ + 73, + 2 + ], + [ + 75, + 2 + ], + [ + 76, + 2 + ], + [ + 77, + 2 + ], + [ + 80, + 2 + ], + [ + 81, + 2 + ], + [ + 82, + 2 + ], + [ + 84, + 2 + ], + [ + 85, + 2 + ], + [ + 86, + 2 + ], + [ + 87, + 2 + ], + [ + 88, + 2 + ], + [ + 95, + 2 + ], + [ + 130, + 2 + ], + [ + 136, + 2 + ], + [ + 137, + 2 + ], + [ + 138, + 2 + ], + [ + 139, + 2 + ], + [ + 142, + 2 + ], + [ + 143, + 2 + ], + [ + 144, + 2 + ], + [ + 145, + 2 + ], + [ + 148, + 2 + ], + [ + 149, + 2 + ], + [ + 157, + 2 + ], + [ + 160, + 2 + ] + ] + }, + { + "filename": "services/report/languages/base.py", + "ln_ex_ct": [ + [ + 9, + 2 + ], + [ + 12, + 2 + ], + [ + 68, + 2 + ] + ] + }, + { + "filename": "services/report/languages/clover.py", + "ln_ex_ct": [ + [ + 12, + 2 + ] + ] + }, + { + "filename": "services/report/languages/cobertura.py", + "ln_ex_ct": [ + [ + 19, + 2 + ], + [ + 20, + 2 + ], + [ + 24, + 2 + ], + [ + 28, + 2 + ], + [ + 29, + 2 + ], + [ + 35, + 2 + ], + [ + 36, + 2 + ], + [ + 41, + 2 + ], + [ + 66, + 2 + ], + [ + 68, + 2 + ], + [ + 69, + 2 + ], + [ + 70, + 2 + ], + [ + 72, + 2 + ], + [ + 73, + 2 + ], + [ + 74, + 2 + ], + [ + 75, + 2 + ], + [ + 77, + 2 + ], + [ + 78, + 2 + ], + [ + 79, + 2 + ], + [ + 80, + 2 + ], + [ + 81, + 2 + ], + [ + 84, + 2 + ], + [ + 85, + 2 + ], + [ + 86, + 2 + ], + [ + 93, + 2 + ], + [ + 96, + 2 + ], + [ + 97, + 2 + ], + [ + 106, + 2 + ], + [ + 111, + 2 + ], + [ + 126, + 2 + ], + [ + 128, + 2 + ], + [ + 138, + 2 + ], + [ + 158, + 2 + ], + [ + 161, + 2 + ], + [ + 162, + 2 + ], + [ + 163, + 2 + ], + [ + 164, + 2 + ], + [ + 165, + 2 + ], + [ + 166, + 2 + ], + [ + 168, + 2 + ], + [ + 169, + 2 + ], + [ + 173, + 2 + ], + [ + 174, + 2 + ] + ] + }, + { + "filename": "services/report/languages/csharp.py", + "ln_ex_ct": [ + [ + 12, + 2 + ] + ] + }, + { + "filename": "services/report/languages/helpers.py", + "ln_ex_ct": [ + [ + 24, + 2 + ] + ] + }, + { + "filename": "services/report/languages/jacoco.py", + "ln_ex_ct": [ + [ + 14, + 2 + ] + ] + }, + { + "filename": "services/report/languages/jetbrainsxml.py", + "ln_ex_ct": [ + [ + 9, + 2 + ] + ] + }, + { + "filename": "services/report/languages/mono.py", + "ln_ex_ct": [ + [ + 9, + 2 + ] + ] + }, + { + "filename": "services/report/languages/scoverage.py", + "ln_ex_ct": [ + [ + 10, + 2 + ] + ] + }, + { + "filename": "services/report/languages/vb.py", + "ln_ex_ct": [ + [ + 9, + 2 + ] + ] + }, + { + "filename": "services/report/languages/vb2.py", + "ln_ex_ct": [ + [ + 9, + 2 + ] + ] + }, + { + "filename": "services/yaml/reader.py", + "ln_ex_ct": [ + [ + 16, + 2 + ], + [ + 17, + 2 + ], + [ + 18, + 2 + ], + [ + 19, + 2 + ], + [ + 20, + 2 + ], + [ + 23, + 2 + ], + [ + 24, + 2 + ], + [ + 25, + 2 + ] + ] + }, + { + "filename": "tasks/upload_processor.py", + "ln_ex_ct": [ + [ + 69, + 2 + ], + [ + 70, + 2 + ], + [ + 71, + 2 + ], + [ + 72, + 2 + ], + [ + 73, + 2 + ], + [ + 74, + 2 + ], + [ + 79, + 2 + ], + [ + 80, + 2 + ], + [ + 112, + 2 + ], + [ + 113, + 2 + ], + [ + 114, + 2 + ], + [ + 115, + 2 + ], + [ + 116, + 2 + ], + [ + 117, + 2 + ], + [ + 120, + 2 + ], + [ + 121, + 2 + ], + [ + 122, + 2 + ], + [ + 123, + 2 + ], + [ + 124, + 2 + ], + [ + 125, + 2 + ], + [ + 126, + 2 + ], + [ + 128, + 2 + ], + [ + 129, + 2 + ], + [ + 130, + 2 + ], + [ + 132, + 2 + ], + [ + 133, + 2 + ], + [ + 134, + 2 + ], + [ + 135, + 2 + ], + [ + 145, + 2 + ], + [ + 146, + 2 + ], + [ + 147, + 2 + ], + [ + 148, + 2 + ], + [ + 149, + 2 + ], + [ + 150, + 2 + ], + [ + 155, + 2 + ], + [ + 158, + 2 + ], + [ + 166, + 2 + ], + [ + 181, + 2 + ], + [ + 182, + 2 + ], + [ + 183, + 2 + ], + [ + 184, + 2 + ], + [ + 185, + 2 + ], + [ + 190, + 2 + ], + [ + 191, + 2 + ], + [ + 194, + 2 + ], + [ + 204, + 2 + ], + [ + 220, + 2 + ], + [ + 223, + 2 + ], + [ + 236, + 2 + ], + [ + 239, + 2 + ], + [ + 251, + 2 + ], + [ + 252, + 2 + ], + [ + 253, + 2 + ], + [ + 261, + 2 + ], + [ + 264, + 2 + ], + [ + 266, + 2 + ], + [ + 280, + 2 + ], + [ + 281, + 2 + ], + [ + 282, + 2 + ], + [ + 283, + 2 + ], + [ + 284, + 2 + ], + [ + 301, + 2 + ], + [ + 311, + 2 + ], + [ + 312, + 2 + ], + [ + 313, + 2 + ] + ] + } + ], + "groups": [ + { + "count": 3, + "files": [ + { + "filename": "helpers/logging_config.py", + "ln_ex_ct": [ + [ + 11, + 3 + ], + [ + 12, + 3 + ], + [ + 13, + 3 + ], + [ + 14, + 3 + ], + [ + 15, + 3 + ], + [ + 24, + 3 + ], + [ + 25, + 3 + ], + [ + 26, + 3 + ], + [ + 27, + 3 + ], + [ + 28, + 3 + ], + [ + 30, + 3 + ] + ] + }, + { + "filename": "services/redis.py", + "ln_ex_ct": [ + [ + 12, + 3 + ], + [ + 13, + 3 + ], + [ + 14, + 3 + ], + [ + 21, + 3 + ], + [ + 22, + 3 + ], + [ + 26, + 3 + ] + ] + }, + { + "filename": "tasks/base.py", + "ln_ex_ct": [ + [ + 39, + 3 + ], + [ + 43, + 3 + ], + [ + 45, + 3 + ], + [ + 47, + 3 + ], + [ + 50, + 3 + ], + [ + 51, + 3 + ], + [ + 52, + 3 + ], + [ + 53, + 3 + ], + [ + 54, + 3 + ], + [ + 55, + 2 + ], + [ + 62, + 2 + ], + [ + 81, + 3 + ], + [ + 98, + 3 + ], + [ + 99, + 3 + ], + [ + 100, + 3 + ], + [ + 123, + 2 + ], + [ + 124, + 2 + ], + [ + 125, + 2 + ], + [ + 128, + 1 + ], + [ + 129, + 1 + ], + [ + 130, + 1 + ] + ] + }, + { + "filename": "tasks/upload.py", + "ln_ex_ct": [ + [ + 99, + 3 + ], + [ + 102, + 3 + ], + [ + 103, + 3 + ], + [ + 104, + 2 + ], + [ + 105, + 1 + ], + [ + 133, + 3 + ], + [ + 134, + 3 + ], + [ + 136, + 3 + ], + [ + 139, + 3 + ], + [ + 140, + 3 + ], + [ + 141, + 3 + ], + [ + 142, + 3 + ], + [ + 143, + 3 + ], + [ + 158, + 3 + ], + [ + 159, + 3 + ], + [ + 164, + 3 + ], + [ + 167, + 2 + ], + [ + 199, + 3 + ], + [ + 202, + 3 + ], + [ + 203, + 1 + ], + [ + 208, + 2 + ], + [ + 209, + 2 + ], + [ + 210, + 2 + ], + [ + 211, + 2 + ], + [ + 214, + 2 + ], + [ + 215, + 2 + ], + [ + 216, + 2 + ], + [ + 220, + 2 + ], + [ + 224, + 2 + ] + ] + } + ], + "group_name": "run/app.tasks.upload.Upload" + }, + { + "count": 2, + "files": [ + { + "filename": "database/base.py", + "ln_ex_ct": [ + [ + 17, + 2 + ], + [ + 32, + 2 + ] + ] + }, + { + "filename": "database/engine.py", + "ln_ex_ct": [ + [ + 18, + 2 + ], + [ + 19, + 2 + ], + [ + 24, + 2 + ] + ] + }, + { + "filename": "database/models/core.py", + "ln_ex_ct": [ + [ + 123, + 2 + ], + [ + 168, + 2 + ] + ] + }, + { + "filename": "database/models/reports.py", + "ln_ex_ct": [ + [ + 85, + 2 + ], + [ + 116, + 2 + ], + [ + 118, + 2 + ], + [ + 119, + 2 + ], + [ + 120, + 2 + ], + [ + 121, + 2 + ], + [ + 122, + 2 + ], + [ + 123, + 2 + ], + [ + 124, + 2 + ] + ] + }, + { + "filename": "helpers/cache.py", + "ln_ex_ct": [ + [ + 29, + 2 + ], + [ + 30, + 2 + ], + [ + 31, + 2 + ], + [ + 38, + 2 + ], + [ + 39, + 2 + ], + [ + 40, + 2 + ], + [ + 41, + 2 + ], + [ + 42, + 2 + ], + [ + 44, + 2 + ], + [ + 98, + 2 + ], + [ + 99, + 2 + ], + [ + 103, + 2 + ], + [ + 105, + 2 + ], + [ + 106, + 2 + ], + [ + 161, + 2 + ], + [ + 188, + 2 + ], + [ + 189, + 2 + ], + [ + 190, + 2 + ], + [ + 191, + 2 + ], + [ + 192, + 2 + ], + [ + 202, + 2 + ], + [ + 203, + 2 + ], + [ + 204, + 2 + ], + [ + 205, + 2 + ] + ] + }, + { + "filename": "helpers/logging_config.py", + "ln_ex_ct": [ + [ + 11, + 2 + ], + [ + 12, + 2 + ], + [ + 13, + 2 + ], + [ + 14, + 2 + ], + [ + 15, + 2 + ], + [ + 24, + 2 + ], + [ + 25, + 2 + ], + [ + 26, + 2 + ], + [ + 27, + 2 + ], + [ + 28, + 2 + ], + [ + 30, + 2 + ] + ] + }, + { + "filename": "helpers/pathmap/pathmap.py", + "ln_ex_ct": [ + [ + 9, + 2 + ], + [ + 16, + 2 + ], + [ + 23, + 2 + ], + [ + 24, + 2 + ], + [ + 25, + 2 + ], + [ + 26, + 2 + ], + [ + 42, + 2 + ], + [ + 44, + 2 + ], + [ + 46, + 2 + ], + [ + 47, + 2 + ], + [ + 51, + 2 + ] + ] + }, + { + "filename": "helpers/pathmap/tree.py", + "ln_ex_ct": [ + [ + 10, + 2 + ], + [ + 13, + 2 + ], + [ + 16, + 2 + ], + [ + 25, + 2 + ], + [ + 26, + 2 + ], + [ + 27, + 2 + ], + [ + 28, + 2 + ], + [ + 29, + 2 + ], + [ + 30, + 2 + ], + [ + 83, + 2 + ], + [ + 85, + 2 + ], + [ + 86, + 2 + ], + [ + 88, + 2 + ], + [ + 89, + 2 + ], + [ + 90, + 2 + ], + [ + 91, + 2 + ], + [ + 92, + 2 + ], + [ + 96, + 2 + ], + [ + 100, + 2 + ], + [ + 110, + 2 + ], + [ + 111, + 2 + ], + [ + 112, + 2 + ], + [ + 114, + 2 + ], + [ + 117, + 2 + ], + [ + 118, + 2 + ], + [ + 127, + 2 + ], + [ + 156, + 2 + ], + [ + 157, + 2 + ], + [ + 158, + 2 + ], + [ + 160, + 2 + ], + [ + 161, + 2 + ], + [ + 162, + 2 + ], + [ + 174, + 2 + ], + [ + 175, + 2 + ] + ] + }, + { + "filename": "services/archive.py", + "ln_ex_ct": [ + [ + 28, + 2 + ], + [ + 58, + 2 + ], + [ + 59, + 2 + ], + [ + 62, + 2 + ], + [ + 63, + 2 + ], + [ + 64, + 2 + ], + [ + 84, + 2 + ], + [ + 85, + 2 + ], + [ + 86, + 2 + ], + [ + 97, + 2 + ], + [ + 98, + 2 + ], + [ + 115, + 2 + ], + [ + 192, + 2 + ], + [ + 196, + 2 + ], + [ + 197, + 2 + ], + [ + 204, + 2 + ], + [ + 205, + 2 + ], + [ + 206, + 2 + ], + [ + 209, + 2 + ], + [ + 233, + 2 + ], + [ + 237, + 2 + ] + ] + }, + { + "filename": "services/bots.py", + "ln_ex_ct": [ + [ + 17, + 2 + ], + [ + 18, + 2 + ], + [ + 21, + 2 + ], + [ + 43, + 2 + ], + [ + 44, + 2 + ] + ] + }, + { + "filename": "services/redis.py", + "ln_ex_ct": [ + [ + 12, + 2 + ], + [ + 13, + 2 + ], + [ + 14, + 2 + ], + [ + 21, + 2 + ], + [ + 22, + 2 + ], + [ + 26, + 2 + ] + ] + }, + { + "filename": "services/repository.py", + "ln_ex_ct": [ + [ + 29, + 2 + ], + [ + 33, + 2 + ], + [ + 34, + 2 + ], + [ + 35, + 2 + ], + [ + 56, + 2 + ], + [ + 60, + 2 + ] + ] + }, + { + "filename": "services/storage.py", + "ln_ex_ct": [ + [ + 12, + 2 + ], + [ + 17, + 2 + ], + [ + 20, + 2 + ] + ] + }, + { + "filename": "services/path_fixer/__init__.py", + "ln_ex_ct": [ + [ + 41, + 2 + ], + [ + 44, + 2 + ], + [ + 45, + 2 + ], + [ + 46, + 2 + ], + [ + 47, + 2 + ], + [ + 50, + 2 + ], + [ + 51, + 2 + ], + [ + 54, + 2 + ], + [ + 64, + 2 + ], + [ + 65, + 2 + ], + [ + 66, + 2 + ], + [ + 67, + 2 + ], + [ + 68, + 2 + ], + [ + 71, + 2 + ], + [ + 72, + 2 + ], + [ + 73, + 2 + ], + [ + 74, + 2 + ], + [ + 75, + 2 + ], + [ + 78, + 2 + ], + [ + 80, + 2 + ], + [ + 81, + 2 + ], + [ + 84, + 2 + ], + [ + 85, + 2 + ], + [ + 86, + 2 + ], + [ + 90, + 2 + ], + [ + 93, + 2 + ], + [ + 96, + 2 + ], + [ + 99, + 2 + ], + [ + 102, + 2 + ], + [ + 103, + 2 + ], + [ + 104, + 2 + ], + [ + 107, + 2 + ], + [ + 112, + 2 + ], + [ + 113, + 2 + ], + [ + 118, + 2 + ], + [ + 121, + 2 + ], + [ + 122, + 2 + ], + [ + 127, + 2 + ], + [ + 152, + 2 + ], + [ + 165, + 2 + ] + ] + }, + { + "filename": "services/path_fixer/fixpaths.py", + "ln_ex_ct": [ + [ + 98, + 2 + ], + [ + 99, + 2 + ], + [ + 101, + 2 + ], + [ + 105, + 2 + ], + [ + 107, + 2 + ], + [ + 109, + 2 + ], + [ + 110, + 2 + ], + [ + 115, + 2 + ], + [ + 119, + 2 + ], + [ + 121, + 2 + ] + ] + }, + { + "filename": "services/path_fixer/user_path_fixes.py", + "ln_ex_ct": [ + [ + 25, + 2 + ], + [ + 26, + 2 + ], + [ + 29, + 2 + ], + [ + 30, + 2 + ], + [ + 31, + 2 + ], + [ + 35, + 2 + ], + [ + 44, + 2 + ] + ] + }, + { + "filename": "services/path_fixer/user_path_includes.py", + "ln_ex_ct": [ + [ + 27, + 2 + ], + [ + 28, + 2 + ], + [ + 29, + 2 + ], + [ + 56, + 2 + ], + [ + 57, + 2 + ] + ] + }, + { + "filename": "services/report/__init__.py", + "ln_ex_ct": [ + [ + 48, + 2 + ], + [ + 55, + 2 + ], + [ + 80, + 2 + ], + [ + 228, + 2 + ], + [ + 229, + 2 + ], + [ + 230, + 2 + ], + [ + 231, + 2 + ], + [ + 233, + 2 + ], + [ + 236, + 2 + ], + [ + 241, + 2 + ], + [ + 249, + 2 + ], + [ + 250, + 2 + ], + [ + 252, + 2 + ], + [ + 253, + 2 + ], + [ + 254, + 2 + ], + [ + 261, + 2 + ], + [ + 263, + 2 + ], + [ + 264, + 2 + ], + [ + 265, + 2 + ], + [ + 266, + 2 + ], + [ + 269, + 2 + ], + [ + 414, + 2 + ], + [ + 415, + 2 + ], + [ + 416, + 2 + ], + [ + 417, + 2 + ], + [ + 418, + 2 + ], + [ + 419, + 2 + ], + [ + 420, + 2 + ], + [ + 421, + 2 + ], + [ + 422, + 2 + ], + [ + 424, + 2 + ], + [ + 425, + 2 + ], + [ + 435, + 2 + ], + [ + 436, + 2 + ], + [ + 437, + 2 + ], + [ + 438, + 2 + ], + [ + 451, + 2 + ], + [ + 452, + 2 + ], + [ + 453, + 2 + ], + [ + 454, + 2 + ], + [ + 457, + 2 + ], + [ + 470, + 2 + ], + [ + 497, + 2 + ], + [ + 498, + 2 + ], + [ + 499, + 2 + ], + [ + 500, + 2 + ], + [ + 501, + 2 + ], + [ + 502, + 2 + ], + [ + 503, + 2 + ], + [ + 504, + 2 + ], + [ + 515, + 2 + ], + [ + 516, + 2 + ], + [ + 517, + 2 + ], + [ + 530, + 2 + ], + [ + 532, + 2 + ], + [ + 533, + 2 + ], + [ + 534, + 2 + ], + [ + 535, + 2 + ], + [ + 536, + 2 + ], + [ + 537, + 2 + ], + [ + 538, + 2 + ], + [ + 539, + 2 + ], + [ + 540, + 2 + ], + [ + 547, + 2 + ], + [ + 548, + 2 + ], + [ + 558, + 2 + ], + [ + 559, + 2 + ], + [ + 560, + 2 + ], + [ + 561, + 2 + ], + [ + 564, + 2 + ], + [ + 565, + 2 + ], + [ + 566, + 2 + ], + [ + 575, + 2 + ] + ] + }, + { + "filename": "services/report/parser.py", + "ln_ex_ct": [ + [ + 10, + 2 + ], + [ + 11, + 2 + ], + [ + 12, + 2 + ], + [ + 16, + 2 + ], + [ + 19, + 2 + ], + [ + 30, + 2 + ], + [ + 31, + 2 + ], + [ + 32, + 2 + ], + [ + 33, + 2 + ], + [ + 36, + 2 + ], + [ + 39, + 2 + ], + [ + 42, + 2 + ], + [ + 46, + 2 + ], + [ + 71, + 2 + ], + [ + 72, + 2 + ], + [ + 73, + 2 + ], + [ + 74, + 2 + ], + [ + 75, + 2 + ], + [ + 76, + 2 + ], + [ + 77, + 2 + ], + [ + 78, + 2 + ], + [ + 81, + 2 + ], + [ + 82, + 2 + ], + [ + 83, + 2 + ], + [ + 85, + 2 + ], + [ + 98, + 2 + ], + [ + 99, + 2 + ], + [ + 100, + 2 + ], + [ + 101, + 2 + ], + [ + 102, + 2 + ], + [ + 103, + 2 + ], + [ + 130, + 2 + ], + [ + 131, + 2 + ], + [ + 132, + 2 + ], + [ + 133, + 2 + ], + [ + 134, + 2 + ], + [ + 135, + 2 + ], + [ + 136, + 2 + ], + [ + 137, + 2 + ], + [ + 138, + 2 + ], + [ + 139, + 2 + ], + [ + 140, + 2 + ], + [ + 141, + 2 + ], + [ + 142, + 2 + ], + [ + 143, + 2 + ], + [ + 144, + 2 + ], + [ + 145, + 2 + ], + [ + 146, + 2 + ], + [ + 148, + 2 + ], + [ + 157, + 2 + ], + [ + 158, + 2 + ], + [ + 166, + 2 + ], + [ + 167, + 2 + ], + [ + 168, + 2 + ], + [ + 169, + 2 + ], + [ + 170, + 2 + ], + [ + 171, + 2 + ], + [ + 172, + 2 + ], + [ + 173, + 2 + ], + [ + 176, + 2 + ], + [ + 179, + 2 + ], + [ + 185, + 2 + ] + ] + }, + { + "filename": "services/report/raw_upload_processor.py", + "ln_ex_ct": [ + [ + 31, + 2 + ], + [ + 36, + 2 + ], + [ + 37, + 2 + ], + [ + 38, + 2 + ], + [ + 39, + 2 + ], + [ + 45, + 2 + ], + [ + 48, + 2 + ], + [ + 55, + 2 + ], + [ + 60, + 2 + ], + [ + 65, + 2 + ], + [ + 66, + 2 + ], + [ + 67, + 2 + ], + [ + 70, + 2 + ], + [ + 71, + 2 + ], + [ + 73, + 2 + ], + [ + 76, + 2 + ], + [ + 77, + 2 + ], + [ + 79, + 2 + ], + [ + 80, + 2 + ], + [ + 81, + 2 + ], + [ + 82, + 2 + ], + [ + 90, + 2 + ], + [ + 91, + 2 + ], + [ + 92, + 2 + ], + [ + 93, + 2 + ], + [ + 96, + 2 + ], + [ + 99, + 2 + ], + [ + 106, + 2 + ], + [ + 107, + 2 + ], + [ + 108, + 2 + ], + [ + 109, + 2 + ], + [ + 120, + 2 + ], + [ + 121, + 2 + ], + [ + 122, + 2 + ], + [ + 126, + 2 + ], + [ + 131, + 2 + ], + [ + 141, + 2 + ] + ] + }, + { + "filename": "services/report/report_processor.py", + "ln_ex_ct": [ + [ + 48, + 2 + ], + [ + 49, + 2 + ], + [ + 50, + 2 + ], + [ + 51, + 2 + ], + [ + 64, + 2 + ], + [ + 69, + 2 + ], + [ + 73, + 2 + ], + [ + 75, + 2 + ], + [ + 76, + 2 + ], + [ + 77, + 2 + ], + [ + 80, + 2 + ], + [ + 81, + 2 + ], + [ + 82, + 2 + ], + [ + 84, + 2 + ], + [ + 85, + 2 + ], + [ + 86, + 2 + ], + [ + 87, + 2 + ], + [ + 88, + 2 + ], + [ + 95, + 2 + ], + [ + 130, + 2 + ], + [ + 136, + 2 + ], + [ + 137, + 2 + ], + [ + 138, + 2 + ], + [ + 139, + 2 + ], + [ + 142, + 2 + ], + [ + 143, + 2 + ], + [ + 144, + 2 + ], + [ + 145, + 2 + ], + [ + 148, + 2 + ], + [ + 149, + 2 + ], + [ + 157, + 2 + ], + [ + 160, + 2 + ] + ] + }, + { + "filename": "services/report/languages/base.py", + "ln_ex_ct": [ + [ + 9, + 2 + ], + [ + 12, + 2 + ], + [ + 68, + 2 + ] + ] + }, + { + "filename": "services/report/languages/clover.py", + "ln_ex_ct": [ + [ + 12, + 2 + ] + ] + }, + { + "filename": "services/report/languages/cobertura.py", + "ln_ex_ct": [ + [ + 19, + 2 + ], + [ + 20, + 2 + ], + [ + 24, + 2 + ], + [ + 28, + 2 + ], + [ + 29, + 2 + ], + [ + 35, + 2 + ], + [ + 36, + 2 + ], + [ + 41, + 2 + ], + [ + 66, + 2 + ], + [ + 68, + 2 + ], + [ + 69, + 2 + ], + [ + 70, + 2 + ], + [ + 72, + 2 + ], + [ + 73, + 2 + ], + [ + 74, + 2 + ], + [ + 75, + 2 + ], + [ + 77, + 2 + ], + [ + 78, + 2 + ], + [ + 79, + 2 + ], + [ + 80, + 2 + ], + [ + 81, + 2 + ], + [ + 84, + 2 + ], + [ + 85, + 2 + ], + [ + 86, + 2 + ], + [ + 93, + 2 + ], + [ + 96, + 2 + ], + [ + 97, + 2 + ], + [ + 106, + 2 + ], + [ + 111, + 2 + ], + [ + 126, + 2 + ], + [ + 128, + 2 + ], + [ + 138, + 2 + ], + [ + 158, + 2 + ], + [ + 161, + 2 + ], + [ + 162, + 2 + ], + [ + 163, + 2 + ], + [ + 164, + 2 + ], + [ + 165, + 2 + ], + [ + 166, + 2 + ], + [ + 168, + 2 + ], + [ + 169, + 2 + ], + [ + 173, + 2 + ], + [ + 174, + 2 + ] + ] + }, + { + "filename": "services/report/languages/csharp.py", + "ln_ex_ct": [ + [ + 12, + 2 + ] + ] + }, + { + "filename": "services/report/languages/helpers.py", + "ln_ex_ct": [ + [ + 24, + 2 + ] + ] + }, + { + "filename": "services/report/languages/jacoco.py", + "ln_ex_ct": [ + [ + 14, + 2 + ] + ] + }, + { + "filename": "services/report/languages/jetbrainsxml.py", + "ln_ex_ct": [ + [ + 9, + 2 + ] + ] + }, + { + "filename": "services/report/languages/mono.py", + "ln_ex_ct": [ + [ + 9, + 2 + ] + ] + }, + { + "filename": "services/report/languages/scoverage.py", + "ln_ex_ct": [ + [ + 10, + 2 + ] + ] + }, + { + "filename": "services/report/languages/vb.py", + "ln_ex_ct": [ + [ + 9, + 2 + ] + ] + }, + { + "filename": "services/report/languages/vb2.py", + "ln_ex_ct": [ + [ + 9, + 2 + ] + ] + }, + { + "filename": "services/yaml/reader.py", + "ln_ex_ct": [ + [ + 16, + 2 + ], + [ + 17, + 2 + ], + [ + 18, + 2 + ], + [ + 19, + 2 + ], + [ + 20, + 2 + ], + [ + 23, + 2 + ], + [ + 24, + 2 + ], + [ + 25, + 2 + ] + ] + }, + { + "filename": "tasks/base.py", + "ln_ex_ct": [ + [ + 39, + 2 + ], + [ + 43, + 2 + ], + [ + 45, + 2 + ], + [ + 47, + 2 + ], + [ + 50, + 2 + ], + [ + 51, + 2 + ], + [ + 52, + 2 + ], + [ + 53, + 2 + ], + [ + 54, + 2 + ], + [ + 81, + 2 + ], + [ + 98, + 2 + ], + [ + 99, + 2 + ], + [ + 100, + 2 + ], + [ + 128, + 2 + ], + [ + 129, + 2 + ], + [ + 130, + 2 + ] + ] + }, + { + "filename": "tasks/upload_processor.py", + "ln_ex_ct": [ + [ + 69, + 2 + ], + [ + 70, + 2 + ], + [ + 71, + 2 + ], + [ + 72, + 2 + ], + [ + 73, + 2 + ], + [ + 74, + 2 + ], + [ + 79, + 2 + ], + [ + 80, + 2 + ], + [ + 112, + 2 + ], + [ + 113, + 2 + ], + [ + 114, + 2 + ], + [ + 115, + 2 + ], + [ + 116, + 2 + ], + [ + 117, + 2 + ], + [ + 120, + 2 + ], + [ + 121, + 2 + ], + [ + 122, + 2 + ], + [ + 123, + 2 + ], + [ + 124, + 2 + ], + [ + 125, + 2 + ], + [ + 126, + 2 + ], + [ + 128, + 2 + ], + [ + 129, + 2 + ], + [ + 130, + 2 + ], + [ + 132, + 2 + ], + [ + 133, + 2 + ], + [ + 134, + 2 + ], + [ + 135, + 2 + ], + [ + 145, + 2 + ], + [ + 146, + 2 + ], + [ + 147, + 2 + ], + [ + 148, + 2 + ], + [ + 149, + 2 + ], + [ + 150, + 2 + ], + [ + 155, + 2 + ], + [ + 158, + 2 + ], + [ + 166, + 2 + ], + [ + 181, + 2 + ], + [ + 182, + 2 + ], + [ + 183, + 2 + ], + [ + 184, + 2 + ], + [ + 185, + 2 + ], + [ + 190, + 2 + ], + [ + 191, + 2 + ], + [ + 194, + 2 + ], + [ + 204, + 2 + ], + [ + 220, + 2 + ], + [ + 223, + 2 + ], + [ + 236, + 2 + ], + [ + 239, + 2 + ], + [ + 251, + 2 + ], + [ + 252, + 2 + ], + [ + 253, + 2 + ], + [ + 261, + 2 + ], + [ + 264, + 2 + ], + [ + 266, + 2 + ], + [ + 280, + 2 + ], + [ + 281, + 2 + ], + [ + 282, + 2 + ], + [ + 283, + 2 + ], + [ + 284, + 2 + ], + [ + 301, + 2 + ], + [ + 311, + 2 + ], + [ + 312, + 2 + ], + [ + 313, + 2 + ] + ] + } + ], + "group_name": "run/app.tasks.upload_processor.UploadProcessorTask" + } + ], + "metadata": { + "version": "v1" + } +} \ No newline at end of file diff --git a/apps/worker/tasks/tests/unit/samples/sample_opentelem_input.json b/apps/worker/tasks/tests/unit/samples/sample_opentelem_input.json new file mode 100644 index 0000000000..3771a3144b --- /dev/null +++ b/apps/worker/tasks/tests/unit/samples/sample_opentelem_input.json @@ -0,0 +1,417 @@ +{ + "spans": [ + { + "name": "run/app.tasks.upload.Upload", + "context": { + "trace_id": "0x1d6924e922a74221ae650dce3405bdd4", + "span_id": "0x604468a7a3675e67", + "trace_state": "[]" + }, + "kind": "SpanKind.CONSUMER", + "parent_id": null, + "start_time": "2021-09-05T23:52:43.686041Z", + "end_time": "2021-09-05T23:52:43.836428Z", + "status": { + "status_code": "UNSET" + }, + "attributes": { + "celery.retry.reason": "Retry in 30s", + "celery.action": "run", + "celery.state": "RETRY", + "messaging.conversation_id": "99420cd6-01db-48b9-ac7b-6ddca0c8aec5", + "messaging.destination": "celery", + "celery.delivery_info": "{'exchange': '', 'routing_key': 'celery', 'priority': 0, 'redelivered': None}", + "celery.eta": "2021-09-05T23:52:43.517824+00:00", + "messaging.message_id": "99420cd6-01db-48b9-ac7b-6ddca0c8aec5", + "celery.reply_to": "d437a05e-3a88-3f40-a482-2f944088928e", + "celery.hostname": "gen9@2bb889310bc2", + "celery.task_name": "app.tasks.upload.Upload" + }, + "events": [], + "links": [], + "resource": { + "telemetry.sdk.language": "python", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.4.1", + "service.name": "unknown_service" + }, + "codecov": { + "type": "bytes", + "coverage": "PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8Y292ZXJhZ2UgdmVyc2lvbj0iNS41IiB0aW1lc3RhbXA9IjE2MzA4ODU5OTg1MTMiIGxpbmVzLXZhbGlkPSIzMzEiIGxpbmVzLWNvdmVyZWQ9IjYyIiBsaW5lLXJhdGU9IjAuMTg3MyIgYnJhbmNoZXMtY292ZXJlZD0iMCIgYnJhbmNoZXMtdmFsaWQ9IjAiIGJyYW5jaC1yYXRlPSIwIiBjb21wbGV4aXR5PSIwIj4KCTwhLS0gR2VuZXJhdGVkIGJ5IGNvdmVyYWdlLnB5OiBodHRwczovL2NvdmVyYWdlLnJlYWR0aGVkb2NzLmlvIC0tPgoJPCEtLSBCYXNlZCBvbiBodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vY29iZXJ0dXJhL3dlYi9tYXN0ZXIvaHRkb2NzL3htbC9jb3ZlcmFnZS0wNC5kdGQgLS0+Cgk8c291cmNlcz4KCQk8c291cmNlPi93b3JrZXI8L3NvdXJjZT4KCTwvc291cmNlcz4KCTxwYWNrYWdlcz4KCQk8cGFja2FnZSBuYW1lPSJoZWxwZXJzIiBsaW5lLXJhdGU9IjAuMjg5NSIgYnJhbmNoLXJhdGU9IjAiIGNvbXBsZXhpdHk9IjAiPgoJCQk8Y2xhc3Nlcz4KCQkJCTxjbGFzcyBuYW1lPSJsb2dnaW5nX2NvbmZpZy5weSIgZmlsZW5hbWU9ImhlbHBlcnMvbG9nZ2luZ19jb25maWcucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4yODk1IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MiIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQk8L2NsYXNzZXM+CgkJPC9wYWNrYWdlPgoJCTxwYWNrYWdlIG5hbWU9InNlcnZpY2VzIiBsaW5lLXJhdGU9IjAuMjMwOCIgYnJhbmNoLXJhdGU9IjAiIGNvbXBsZXhpdHk9IjAiPgoJCQk8Y2xhc3Nlcz4KCQkJCTxjbGFzcyBuYW1lPSJyZWRpcy5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlZGlzLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuMjMwOCIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQk8L2NsYXNzZXM+CgkJPC9wYWNrYWdlPgoJCTxwYWNrYWdlIG5hbWU9InRhc2tzIiBsaW5lLXJhdGU9IjAuMTY4NSIgYnJhbmNoLXJhdGU9IjAiIGNvbXBsZXhpdHk9IjAiPgoJCQk8Y2xhc3Nlcz4KCQkJCTxjbGFzcyBuYW1lPSJiYXNlLnB5IiBmaWxlbmFtZT0idGFza3MvYmFzZS5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjIzMDgiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyMyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyNCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyNSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzOCIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQkJPGNsYXNzIG5hbWU9InVwbG9hZC5weSIgZmlsZW5hbWU9InRhc2tzL3VwbG9hZC5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjE0MjkiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTczIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTk0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTk2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTk5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjAyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjAzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjExIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjUwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjUxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjU2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjU3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjY2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjY3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjY5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjcwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjc0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjc1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjc3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjg0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjg1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjg2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjg3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjg5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjk3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjk5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzA1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzA2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzM2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQ3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzU0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzU1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzU2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzY3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzc4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzc5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzgxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzg4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzg5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzk0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzk1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzk2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzk3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDA2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDA4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDM5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQ0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQ4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDU0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDU1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDU2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDU5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDYwIiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCTwvY2xhc3Nlcz4KCQk8L3BhY2thZ2U+Cgk8L3BhY2thZ2VzPgo8L2NvdmVyYWdlPgo=" + } + }, + { + "name": "run/app.tasks.upload.Upload", + "context": { + "trace_id": "0x8e5a3d8ef716ced564988acc2344eb19", + "span_id": "0xacfb96cbb3f2f6ba", + "trace_state": "[]" + }, + "kind": "SpanKind.CONSUMER", + "parent_id": null, + "start_time": "2021-09-05T23:52:44.784940Z", + "end_time": "2021-09-05T23:52:44.810924Z", + "status": { + "status_code": "UNSET" + }, + "attributes": { + "celery.retry.reason": "Retry in 30s", + "celery.action": "run", + "celery.state": "RETRY", + "messaging.conversation_id": "029ea054-6864-4129-b968-c7148b3ec816", + "messaging.destination": "celery", + "celery.delivery_info": "{'exchange': '', 'routing_key': 'celery', 'priority': 0, 'redelivered': None}", + "celery.eta": "2021-09-05T23:52:44.488725+00:00", + "messaging.message_id": "029ea054-6864-4129-b968-c7148b3ec816", + "celery.reply_to": "d437a05e-3a88-3f40-a482-2f944088928e", + "celery.hostname": "gen9@2bb889310bc2", + "celery.task_name": "app.tasks.upload.Upload" + }, + "events": [], + "links": [], + "resource": { + "telemetry.sdk.language": "python", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.4.1", + "service.name": "unknown_service" + }, + "codecov": { + "type": "bytes", + "coverage": "PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8Y292ZXJhZ2UgdmVyc2lvbj0iNS41IiB0aW1lc3RhbXA9IjE2MzA4ODU5OTg1NzUiIGxpbmVzLXZhbGlkPSIzMzEiIGxpbmVzLWNvdmVyZWQ9IjYyIiBsaW5lLXJhdGU9IjAuMTg3MyIgYnJhbmNoZXMtY292ZXJlZD0iMCIgYnJhbmNoZXMtdmFsaWQ9IjAiIGJyYW5jaC1yYXRlPSIwIiBjb21wbGV4aXR5PSIwIj4KCTwhLS0gR2VuZXJhdGVkIGJ5IGNvdmVyYWdlLnB5OiBodHRwczovL2NvdmVyYWdlLnJlYWR0aGVkb2NzLmlvIC0tPgoJPCEtLSBCYXNlZCBvbiBodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vY29iZXJ0dXJhL3dlYi9tYXN0ZXIvaHRkb2NzL3htbC9jb3ZlcmFnZS0wNC5kdGQgLS0+Cgk8c291cmNlcz4KCQk8c291cmNlPi93b3JrZXI8L3NvdXJjZT4KCTwvc291cmNlcz4KCTxwYWNrYWdlcz4KCQk8cGFja2FnZSBuYW1lPSJoZWxwZXJzIiBsaW5lLXJhdGU9IjAuMjg5NSIgYnJhbmNoLXJhdGU9IjAiIGNvbXBsZXhpdHk9IjAiPgoJCQk8Y2xhc3Nlcz4KCQkJCTxjbGFzcyBuYW1lPSJsb2dnaW5nX2NvbmZpZy5weSIgZmlsZW5hbWU9ImhlbHBlcnMvbG9nZ2luZ19jb25maWcucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4yODk1IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MiIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQk8L2NsYXNzZXM+CgkJPC9wYWNrYWdlPgoJCTxwYWNrYWdlIG5hbWU9InNlcnZpY2VzIiBsaW5lLXJhdGU9IjAuMjMwOCIgYnJhbmNoLXJhdGU9IjAiIGNvbXBsZXhpdHk9IjAiPgoJCQk8Y2xhc3Nlcz4KCQkJCTxjbGFzcyBuYW1lPSJyZWRpcy5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlZGlzLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuMjMwOCIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQk8L2NsYXNzZXM+CgkJPC9wYWNrYWdlPgoJCTxwYWNrYWdlIG5hbWU9InRhc2tzIiBsaW5lLXJhdGU9IjAuMTY4NSIgYnJhbmNoLXJhdGU9IjAiIGNvbXBsZXhpdHk9IjAiPgoJCQk8Y2xhc3Nlcz4KCQkJCTxjbGFzcyBuYW1lPSJiYXNlLnB5IiBmaWxlbmFtZT0idGFza3MvYmFzZS5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjIzMDgiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyMyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyNCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyNSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzOCIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQkJPGNsYXNzIG5hbWU9InVwbG9hZC5weSIgZmlsZW5hbWU9InRhc2tzL3VwbG9hZC5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjE0MjkiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTczIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTk0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTk2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTk5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjAyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjAzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjExIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjUwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjUxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjU2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjU3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjY2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjY3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjY5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjcwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjc0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjc1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjc3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjg0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjg1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjg2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjg3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjg5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjk3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjk5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzA1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzA2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzM2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQ3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzU0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzU1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzU2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzY3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzc4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzc5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzgxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzg4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzg5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzk0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzk1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzk2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzk3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDA2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDA4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDM5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQ0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQ4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDU0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDU1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDU2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDU5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDYwIiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCTwvY2xhc3Nlcz4KCQk8L3BhY2thZ2U+Cgk8L3BhY2thZ2VzPgo8L2NvdmVyYWdlPgo=" + } + }, + { + "name": "apply_async/app.tasks.upload.Upload", + "context": { + "trace_id": "0x45d7a32f4e35e5b0edf26d8aea2a9d76", + "span_id": "0x03bfb63118e29712", + "trace_state": "[]" + }, + "kind": "SpanKind.PRODUCER", + "parent_id": "0x29de28566ab7bc38", + "start_time": "2021-09-05T23:52:47.705160Z", + "end_time": "2021-09-05T23:52:47.716828Z", + "status": { + "status_code": "UNSET" + }, + "attributes": { + "celery.action": "apply_async", + "messaging.message_id": "d6e12f84-4934-485a-a696-49db319955b2", + "celery.task_name": "app.tasks.upload.Upload", + "messaging.destination_kind": "queue", + "messaging.destination": "celery" + }, + "events": [], + "links": [], + "resource": { + "telemetry.sdk.language": "python", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.4.1", + "service.name": "unknown_service" + }, + "codecov": {} + }, + { + "name": "apply_async/app.tasks.upload.Upload", + "context": { + "trace_id": "0x5af123764b7a856d4a931e3221d0aef9", + "span_id": "0xbd986ecba100b41c", + "trace_state": "[]" + }, + "kind": "SpanKind.PRODUCER", + "parent_id": "0x8b52243ed07a3cec", + "start_time": "2021-09-05T23:52:49.675992Z", + "end_time": "2021-09-05T23:52:49.687046Z", + "status": { + "status_code": "UNSET" + }, + "attributes": { + "celery.action": "apply_async", + "messaging.message_id": "0e28f3bf-de27-47dc-9315-2de2c0b0547f", + "celery.task_name": "app.tasks.upload.Upload", + "messaging.destination_kind": "queue", + "messaging.destination": "celery" + }, + "events": [], + "links": [], + "resource": { + "telemetry.sdk.language": "python", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.4.1", + "service.name": "unknown_service" + }, + "codecov": {} + }, + { + "name": "apply_async/app.tasks.upload_processor.UploadProcessorTask", + "context": { + "trace_id": "0x84950fb14140c6eca1c0084d6c8f0528", + "span_id": "0x91a0f32e89fa138b", + "trace_state": "[]" + }, + "kind": "SpanKind.PRODUCER", + "parent_id": "0x4012342bf99a992a", + "start_time": "2021-09-05T23:52:54.077506Z", + "end_time": "2021-09-05T23:52:54.088480Z", + "status": { + "status_code": "UNSET" + }, + "attributes": { + "celery.action": "apply_async", + "messaging.message_id": "9c67af1b-526d-408a-a33c-49afffeb4110", + "celery.task_name": "app.tasks.upload_processor.UploadProcessorTask", + "messaging.destination_kind": "queue", + "messaging.destination": "celery" + }, + "events": [], + "links": [], + "resource": { + "telemetry.sdk.language": "python", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.4.1", + "service.name": "unknown_service" + }, + "codecov": {} + }, + { + "name": "apply_async/app.tasks.upload_processor.UploadProcessorTask", + "context": { + "trace_id": "0x84950fb14140c6eca1c0084d6c8f0528", + "span_id": "0x050baee558e92804", + "trace_state": "[]" + }, + "kind": "SpanKind.PRODUCER", + "parent_id": "0xc882a1af4088166e", + "start_time": "2021-09-05T23:52:55.508580Z", + "end_time": "2021-09-05T23:52:55.522330Z", + "status": { + "status_code": "UNSET" + }, + "attributes": { + "celery.action": "apply_async", + "messaging.message_id": "6b09023e-92bc-4f6f-a012-c897b1d9f3e9", + "celery.task_name": "app.tasks.upload_processor.UploadProcessorTask", + "messaging.destination_kind": "queue", + "messaging.destination": "celery" + }, + "events": [], + "links": [], + "resource": { + "telemetry.sdk.language": "python", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.4.1", + "service.name": "unknown_service" + }, + "codecov": {} + }, + { + "name": "run/app.tasks.upload_processor.UploadProcessorTask", + "context": { + "trace_id": "0x84950fb14140c6eca1c0084d6c8f0528", + "span_id": "0xc882a1af4088166e", + "trace_state": "[]" + }, + "kind": "SpanKind.CONSUMER", + "parent_id": "0x8a52b4c5c123e8c2", + "start_time": "2021-09-05T23:52:54.855820Z", + "end_time": "2021-09-05T23:52:55.524058Z", + "status": { + "status_code": "UNSET" + }, + "attributes": { + "celery.action": "run", + "celery.state": "SUCCESS", + "messaging.conversation_id": "79eb3f01-c0c0-4a7e-9f12-9ad2f15816cc", + "messaging.destination": "celery", + "celery.delivery_info": "{'exchange': '', 'routing_key': 'celery', 'priority': 0, 'redelivered': None}", + "messaging.message_id": "79eb3f01-c0c0-4a7e-9f12-9ad2f15816cc", + "celery.reply_to": "66db7ac1-c08d-3bdd-9f1b-753ac0bbe1a1", + "celery.hostname": "gen13@4f4dceccd966", + "celery.task_name": "app.tasks.upload_processor.UploadProcessorTask" + }, + "events": [], + "links": [], + "resource": { + "telemetry.sdk.language": "python", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.4.1", + "service.name": "unknown_service" + }, + "codecov": { + "type": "bytes", + "coverage": "PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8Y292ZXJhZ2UgdmVyc2lvbj0iNS41IiB0aW1lc3RhbXA9IjE2MzA4ODU5OTg2OTQiIGxpbmVzLXZhbGlkPSIyMzA1IiBsaW5lcy1jb3ZlcmVkPSI1NDciIGxpbmUtcmF0ZT0iMC4yMzczIiBicmFuY2hlcy1jb3ZlcmVkPSIwIiBicmFuY2hlcy12YWxpZD0iMCIgYnJhbmNoLXJhdGU9IjAiIGNvbXBsZXhpdHk9IjAiPgoJPCEtLSBHZW5lcmF0ZWQgYnkgY292ZXJhZ2UucHk6IGh0dHBzOi8vY292ZXJhZ2UucmVhZHRoZWRvY3MuaW8gLS0+Cgk8IS0tIEJhc2VkIG9uIGh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9jb2JlcnR1cmEvd2ViL21hc3Rlci9odGRvY3MveG1sL2NvdmVyYWdlLTA0LmR0ZCAtLT4KCTxzb3VyY2VzPgoJCTxzb3VyY2U+L3dvcmtlcjwvc291cmNlPgoJPC9zb3VyY2VzPgoJPHBhY2thZ2VzPgoJCTxwYWNrYWdlIG5hbWU9ImRhdGFiYXNlIiBsaW5lLXJhdGU9IjAuMTI1IiBicmFuY2gtcmF0ZT0iMCIgY29tcGxleGl0eT0iMCI+CgkJCTxjbGFzc2VzPgoJCQkJPGNsYXNzIG5hbWU9ImJhc2UucHkiIGZpbGVuYW1lPSJkYXRhYmFzZS9iYXNlLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuMTA1MyIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMiIgaGl0cz0iMSIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQkJPGNsYXNzIG5hbWU9ImVuZ2luZS5weSIgZmlsZW5hbWU9ImRhdGFiYXNlL2VuZ2luZS5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjE0MjkiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM2IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCTwvY2xhc3Nlcz4KCQk8L3BhY2thZ2U+CgkJPHBhY2thZ2UgbmFtZT0iZGF0YWJhc2UubW9kZWxzIiBsaW5lLXJhdGU9IjAuMDQyOCIgYnJhbmNoLXJhdGU9IjAiIGNvbXBsZXhpdHk9IjAiPgoJCQk8Y2xhc3Nlcz4KCQkJCTxjbGFzcyBuYW1lPSJjb3JlLnB5IiBmaWxlbmFtZT0iZGF0YWJhc2UvbW9kZWxzL2NvcmUucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4wMTEzIiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyMyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2OCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI0MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI0OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzMiIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQkJPGNsYXNzIG5hbWU9InJlcG9ydHMucHkiIGZpbGVuYW1lPSJkYXRhYmFzZS9tb2RlbHMvcmVwb3J0cy5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjExMjUiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjczIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM5IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCTwvY2xhc3Nlcz4KCQk8L3BhY2thZ2U+CgkJPHBhY2thZ2UgbmFtZT0iaGVscGVycyIgbGluZS1yYXRlPSIwLjI0MTQiIGJyYW5jaC1yYXRlPSIwIiBjb21wbGV4aXR5PSIwIj4KCQkJPGNsYXNzZXM+CgkJCQk8Y2xhc3MgbmFtZT0iY2FjaGUucHkiIGZpbGVuYW1lPSJoZWxwZXJzL2NhY2hlLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuMjI0MyIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjQiIGhpdHM9IjAiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJsb2dnaW5nX2NvbmZpZy5weSIgZmlsZW5hbWU9ImhlbHBlcnMvbG9nZ2luZ19jb25maWcucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4yODk1IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MiIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQk8L2NsYXNzZXM+CgkJPC9wYWNrYWdlPgoJCTxwYWNrYWdlIG5hbWU9ImhlbHBlcnMucGF0aG1hcCIgbGluZS1yYXRlPSIwLjQ0MTIiIGJyYW5jaC1yYXRlPSIwIiBjb21wbGV4aXR5PSIwIj4KCQkJPGNsYXNzZXM+CgkJCQk8Y2xhc3MgbmFtZT0icGF0aG1hcC5weSIgZmlsZW5hbWU9ImhlbHBlcnMvcGF0aG1hcC9wYXRobWFwLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuNTIzOCIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTQiIGhpdHM9IjAiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJ0cmVlLnB5IiBmaWxlbmFtZT0iaGVscGVycy9wYXRobWFwL3RyZWUucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC40MTk4IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4OCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzUiIGhpdHM9IjEiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJPC9jbGFzc2VzPgoJCTwvcGFja2FnZT4KCQk8cGFja2FnZSBuYW1lPSJzZXJ2aWNlcyIgbGluZS1yYXRlPSIwLjExMjMiIGJyYW5jaC1yYXRlPSIwIiBjb21wbGV4aXR5PSIwIj4KCQkJPGNsYXNzZXM+CgkJCQk8Y2xhc3MgbmFtZT0iYXJjaGl2ZS5weSIgZmlsZW5hbWU9InNlcnZpY2VzL2FyY2hpdmUucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4yMjM0IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1OCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTczIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTk2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTk3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjAzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQ0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQ2IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0iYm90cy5weSIgZmlsZW5hbWU9InNlcnZpY2VzL2JvdHMucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4wODMzMyIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNiIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQkJPGNsYXNzIG5hbWU9InJlZGlzLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcmVkaXMucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4yMzA4IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM4IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0icmVwb3NpdG9yeS5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlcG9zaXRvcnkucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4wMzQ2OCIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTcxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTcyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTczIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjAzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzA2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzM2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzM4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQ0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQ5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzU0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzU5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzYwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzY1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzY2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzY3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzY5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzc1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzc3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzc4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzc5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzgxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzgyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzg2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzg5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzk0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzk1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzk5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDA2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDA4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDM4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQ0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQ3IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0ic3RvcmFnZS5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3N0b3JhZ2UucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4yNSIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMSIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQk8L2NsYXNzZXM+CgkJPC9wYWNrYWdlPgoJCTxwYWNrYWdlIG5hbWU9InNlcnZpY2VzLnBhdGhfZml4ZXIiIGxpbmUtcmF0ZT0iMC4zMjQ2IiBicmFuY2gtcmF0ZT0iMCIgY29tcGxleGl0eT0iMCI+CgkJCTxjbGFzc2VzPgoJCQkJPGNsYXNzIG5hbWU9Il9faW5pdF9fLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcGF0aF9maXhlci9fX2luaXRfXy5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjQ0OTQiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjUiIGhpdHM9IjEiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJmaXhwYXRocy5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3BhdGhfZml4ZXIvZml4cGF0aHMucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4yMjczIiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDIiIGhpdHM9IjAiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJ1c2VyX3BhdGhfZml4ZXMucHkiIGZpbGVuYW1lPSJzZXJ2aWNlcy9wYXRoX2ZpeGVyL3VzZXJfcGF0aF9maXhlcy5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjI5MTciIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU3IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0idXNlcl9wYXRoX2luY2x1ZGVzLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcGF0aF9maXhlci91c2VyX3BhdGhfaW5jbHVkZXMucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4xNDcxIiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc0IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCTwvY2xhc3Nlcz4KCQk8L3BhY2thZ2U+CgkJPHBhY2thZ2UgbmFtZT0ic2VydmljZXMucmVwb3J0IiBsaW5lLXJhdGU9IjAuNDE1NSIgYnJhbmNoLXJhdGU9IjAiIGNvbXBsZXhpdHk9IjAiPgoJCQk8Y2xhc3Nlcz4KCQkJCTxjbGFzcyBuYW1lPSJfX2luaXRfXy5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlcG9ydC9fX2luaXRfXy5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjI4MjkiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMzEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMzMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMzYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNDEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNDkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNTIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNTMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNjMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNjQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNjUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyODAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyODMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyODQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyODUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyODYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyODciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMDUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNDUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzODEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MTUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MTYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MTgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MjAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MjIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MjQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MjUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MzUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MzYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MzciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MzgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NDIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NTEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NTIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NTMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0ODIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0ODMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0ODgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OTgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MDAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MDEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MDIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MDMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MDQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MTUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MTYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NDAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NDciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NDgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NTgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NjAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NjQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NjUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NzUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1ODkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1OTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1OTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1OTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MTYiIGhpdHM9IjAiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJwYXJzZXIucHkiIGZpbGVuYW1lPSJzZXJ2aWNlcy9yZXBvcnQvcGFyc2VyLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuNjEzOSIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjczIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODUiIGhpdHM9IjEiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJyYXdfdXBsb2FkX3Byb2Nlc3Nvci5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlcG9ydC9yYXdfdXBsb2FkX3Byb2Nlc3Nvci5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjU0NDEiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDEiIGhpdHM9IjEiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJyZXBvcnRfcHJvY2Vzc29yLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcmVwb3J0L3JlcG9ydF9wcm9jZXNzb3IucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC41IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTcxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTcyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTczIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgzIiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCTwvY2xhc3Nlcz4KCQk8L3BhY2thZ2U+CgkJPHBhY2thZ2UgbmFtZT0ic2VydmljZXMucmVwb3J0Lmxhbmd1YWdlcyIgbGluZS1yYXRlPSIwLjExNzgiIGJyYW5jaC1yYXRlPSIwIiBjb21wbGV4aXR5PSIwIj4KCQkJPGNsYXNzZXM+CgkJCQk8Y2xhc3MgbmFtZT0iYmFzZS5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlcG9ydC9sYW5ndWFnZXMvYmFzZS5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjIiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OCIgaGl0cz0iMSIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQkJPGNsYXNzIG5hbWU9ImNsb3Zlci5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlcG9ydC9sYW5ndWFnZXMvY2xvdmVyLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuMDE1MzgiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE0IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0iY29iZXJ0dXJhLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcmVwb3J0L2xhbmd1YWdlcy9jb2JlcnR1cmEucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC40NTc0IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjczIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTExIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTczIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc0IiBoaXRzPSIxIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0iY3NoYXJwLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcmVwb3J0L2xhbmd1YWdlcy9jc2hhcnAucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4wMTYzOSIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzQiIGhpdHM9IjAiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJoZWxwZXJzLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcmVwb3J0L2xhbmd1YWdlcy9oZWxwZXJzLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuMTI1IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjEiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJqYWNvY28ucHkiIGZpbGVuYW1lPSJzZXJ2aWNlcy9yZXBvcnQvbGFuZ3VhZ2VzL2phY29jby5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjAxNDI5IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjczIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExOCIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQkJPGNsYXNzIG5hbWU9ImpldGJyYWluc3htbC5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlcG9ydC9sYW5ndWFnZXMvamV0YnJhaW5zeG1sLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuMDMyMjYiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU4IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0ibW9uby5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlcG9ydC9sYW5ndWFnZXMvbW9uby5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjA0MzQ4IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ0IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0ic2NvdmVyYWdlLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcmVwb3J0L2xhbmd1YWdlcy9zY292ZXJhZ2UucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4wMjEyOCIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MiIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQkJPGNsYXNzIG5hbWU9InZiLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcmVwb3J0L2xhbmd1YWdlcy92Yi5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjAzNTcxIiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NiIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQkJPGNsYXNzIG5hbWU9InZiMi5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlcG9ydC9sYW5ndWFnZXMvdmIyLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuMDQiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM4IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCTwvY2xhc3Nlcz4KCQk8L3BhY2thZ2U+CgkJPHBhY2thZ2UgbmFtZT0ic2VydmljZXMueWFtbCIgbGluZS1yYXRlPSIwLjI0MjQiIGJyYW5jaC1yYXRlPSIwIiBjb21wbGV4aXR5PSIwIj4KCQkJPGNsYXNzZXM+CgkJCQk8Y2xhc3MgbmFtZT0icmVhZGVyLnB5IiBmaWxlbmFtZT0ic2VydmljZXMveWFtbC9yZWFkZXIucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4yNDI0IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUyIiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCTwvY2xhc3Nlcz4KCQk8L3BhY2thZ2U+CgkJPHBhY2thZ2UgbmFtZT0idGFza3MiIGxpbmUtcmF0ZT0iMC4zNzg1IiBicmFuY2gtcmF0ZT0iMCIgY29tcGxleGl0eT0iMCI+CgkJCTxjbGFzc2VzPgoJCQkJPGNsYXNzIG5hbWU9ImJhc2UucHkiIGZpbGVuYW1lPSJ0YXNrcy9iYXNlLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuMjA1MSIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM4IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0idXBsb2FkX3Byb2Nlc3Nvci5weSIgZmlsZW5hbWU9InRhc2tzL3VwbG9hZF9wcm9jZXNzb3IucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC40Nzc5IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTcwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTk0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjUxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjUyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjUzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjU0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjU4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjU5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjY0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjY1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjY2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjcwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjg0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjg1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjk1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjk2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzA0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzA1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzExIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzEyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzEzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE3IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCTwvY2xhc3Nlcz4KCQk8L3BhY2thZ2U+Cgk8L3BhY2thZ2VzPgo8L2NvdmVyYWdlPgo=" + } + }, + { + "name": "apply_async/app.tasks.upload_processor.UploadProcessorTask", + "context": { + "trace_id": "0x84950fb14140c6eca1c0084d6c8f0528", + "span_id": "0xbfd7bf84fdc61466", + "trace_state": "[]" + }, + "kind": "SpanKind.PRODUCER", + "parent_id": "0x985e4685e1628a27", + "start_time": "2021-09-05T23:52:56.080787Z", + "end_time": "2021-09-05T23:52:56.093910Z", + "status": { + "status_code": "UNSET" + }, + "attributes": { + "celery.action": "apply_async", + "messaging.message_id": "461e653e-b535-4253-a787-9f20358225b7", + "celery.task_name": "app.tasks.upload_processor.UploadProcessorTask", + "messaging.destination_kind": "queue", + "messaging.destination": "celery" + }, + "events": [], + "links": [], + "resource": { + "telemetry.sdk.language": "python", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.4.1", + "service.name": "unknown_service" + }, + "codecov": {} + }, + { + "name": "run/app.tasks.upload_processor.UploadProcessorTask", + "context": { + "trace_id": "0x84950fb14140c6eca1c0084d6c8f0528", + "span_id": "0x985e4685e1628a27", + "trace_state": "[]" + }, + "kind": "SpanKind.CONSUMER", + "parent_id": "0x050baee558e92804", + "start_time": "2021-09-05T23:52:55.526019Z", + "end_time": "2021-09-05T23:52:56.096127Z", + "status": { + "status_code": "UNSET" + }, + "attributes": { + "celery.action": "run", + "celery.state": "SUCCESS", + "messaging.conversation_id": "6b09023e-92bc-4f6f-a012-c897b1d9f3e9", + "messaging.destination": "celery", + "celery.delivery_info": "{'exchange': '', 'routing_key': 'celery', 'priority': 0, 'redelivered': None}", + "messaging.message_id": "6b09023e-92bc-4f6f-a012-c897b1d9f3e9", + "celery.reply_to": "66db7ac1-c08d-3bdd-9f1b-753ac0bbe1a1", + "celery.hostname": "gen10@4f4dceccd966", + "celery.task_name": "app.tasks.upload_processor.UploadProcessorTask" + }, + "events": [], + "links": [], + "resource": { + "telemetry.sdk.language": "python", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.4.1", + "service.name": "unknown_service" + }, + "codecov": { + "type": "bytes", + "coverage": "PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8Y292ZXJhZ2UgdmVyc2lvbj0iNS41IiB0aW1lc3RhbXA9IjE2MzA4ODU5OTkwOTEiIGxpbmVzLXZhbGlkPSIyMzA1IiBsaW5lcy1jb3ZlcmVkPSI1NDciIGxpbmUtcmF0ZT0iMC4yMzczIiBicmFuY2hlcy1jb3ZlcmVkPSIwIiBicmFuY2hlcy12YWxpZD0iMCIgYnJhbmNoLXJhdGU9IjAiIGNvbXBsZXhpdHk9IjAiPgoJPCEtLSBHZW5lcmF0ZWQgYnkgY292ZXJhZ2UucHk6IGh0dHBzOi8vY292ZXJhZ2UucmVhZHRoZWRvY3MuaW8gLS0+Cgk8IS0tIEJhc2VkIG9uIGh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9jb2JlcnR1cmEvd2ViL21hc3Rlci9odGRvY3MveG1sL2NvdmVyYWdlLTA0LmR0ZCAtLT4KCTxzb3VyY2VzPgoJCTxzb3VyY2U+L3dvcmtlcjwvc291cmNlPgoJPC9zb3VyY2VzPgoJPHBhY2thZ2VzPgoJCTxwYWNrYWdlIG5hbWU9ImRhdGFiYXNlIiBsaW5lLXJhdGU9IjAuMTI1IiBicmFuY2gtcmF0ZT0iMCIgY29tcGxleGl0eT0iMCI+CgkJCTxjbGFzc2VzPgoJCQkJPGNsYXNzIG5hbWU9ImJhc2UucHkiIGZpbGVuYW1lPSJkYXRhYmFzZS9iYXNlLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuMTA1MyIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMiIgaGl0cz0iMSIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQkJPGNsYXNzIG5hbWU9ImVuZ2luZS5weSIgZmlsZW5hbWU9ImRhdGFiYXNlL2VuZ2luZS5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjE0MjkiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM2IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCTwvY2xhc3Nlcz4KCQk8L3BhY2thZ2U+CgkJPHBhY2thZ2UgbmFtZT0iZGF0YWJhc2UubW9kZWxzIiBsaW5lLXJhdGU9IjAuMDQyOCIgYnJhbmNoLXJhdGU9IjAiIGNvbXBsZXhpdHk9IjAiPgoJCQk8Y2xhc3Nlcz4KCQkJCTxjbGFzcyBuYW1lPSJjb3JlLnB5IiBmaWxlbmFtZT0iZGF0YWJhc2UvbW9kZWxzL2NvcmUucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4wMTEzIiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyMyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2OCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI0MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI0OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzMiIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQkJPGNsYXNzIG5hbWU9InJlcG9ydHMucHkiIGZpbGVuYW1lPSJkYXRhYmFzZS9tb2RlbHMvcmVwb3J0cy5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjExMjUiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjczIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM5IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCTwvY2xhc3Nlcz4KCQk8L3BhY2thZ2U+CgkJPHBhY2thZ2UgbmFtZT0iaGVscGVycyIgbGluZS1yYXRlPSIwLjI0MTQiIGJyYW5jaC1yYXRlPSIwIiBjb21wbGV4aXR5PSIwIj4KCQkJPGNsYXNzZXM+CgkJCQk8Y2xhc3MgbmFtZT0iY2FjaGUucHkiIGZpbGVuYW1lPSJoZWxwZXJzL2NhY2hlLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuMjI0MyIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjQiIGhpdHM9IjAiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJsb2dnaW5nX2NvbmZpZy5weSIgZmlsZW5hbWU9ImhlbHBlcnMvbG9nZ2luZ19jb25maWcucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4yODk1IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MiIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQk8L2NsYXNzZXM+CgkJPC9wYWNrYWdlPgoJCTxwYWNrYWdlIG5hbWU9ImhlbHBlcnMucGF0aG1hcCIgbGluZS1yYXRlPSIwLjQ0MTIiIGJyYW5jaC1yYXRlPSIwIiBjb21wbGV4aXR5PSIwIj4KCQkJPGNsYXNzZXM+CgkJCQk8Y2xhc3MgbmFtZT0icGF0aG1hcC5weSIgZmlsZW5hbWU9ImhlbHBlcnMvcGF0aG1hcC9wYXRobWFwLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuNTIzOCIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTQiIGhpdHM9IjAiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJ0cmVlLnB5IiBmaWxlbmFtZT0iaGVscGVycy9wYXRobWFwL3RyZWUucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC40MTk4IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4OCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzUiIGhpdHM9IjEiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJPC9jbGFzc2VzPgoJCTwvcGFja2FnZT4KCQk8cGFja2FnZSBuYW1lPSJzZXJ2aWNlcyIgbGluZS1yYXRlPSIwLjExMjMiIGJyYW5jaC1yYXRlPSIwIiBjb21wbGV4aXR5PSIwIj4KCQkJPGNsYXNzZXM+CgkJCQk8Y2xhc3MgbmFtZT0iYXJjaGl2ZS5weSIgZmlsZW5hbWU9InNlcnZpY2VzL2FyY2hpdmUucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4yMjM0IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1OCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTczIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTk2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTk3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjAzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQ0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQ2IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0iYm90cy5weSIgZmlsZW5hbWU9InNlcnZpY2VzL2JvdHMucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4wODMzMyIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNiIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQkJPGNsYXNzIG5hbWU9InJlZGlzLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcmVkaXMucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4yMzA4IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM4IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0icmVwb3NpdG9yeS5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlcG9zaXRvcnkucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4wMzQ2OCIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTcxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTcyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTczIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjAzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzA2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzM2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzM4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQ0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQ5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzU0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzU5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzYwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzY1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzY2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzY3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzY5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzc1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzc3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzc4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzc5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzgxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzgyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzg2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzg5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzk0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzk1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzk5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDA2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDA4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDM4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQ0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQ3IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0ic3RvcmFnZS5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3N0b3JhZ2UucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4yNSIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMSIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQk8L2NsYXNzZXM+CgkJPC9wYWNrYWdlPgoJCTxwYWNrYWdlIG5hbWU9InNlcnZpY2VzLnBhdGhfZml4ZXIiIGxpbmUtcmF0ZT0iMC4zMjQ2IiBicmFuY2gtcmF0ZT0iMCIgY29tcGxleGl0eT0iMCI+CgkJCTxjbGFzc2VzPgoJCQkJPGNsYXNzIG5hbWU9Il9faW5pdF9fLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcGF0aF9maXhlci9fX2luaXRfXy5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjQ0OTQiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjUiIGhpdHM9IjEiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJmaXhwYXRocy5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3BhdGhfZml4ZXIvZml4cGF0aHMucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4yMjczIiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDIiIGhpdHM9IjAiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJ1c2VyX3BhdGhfZml4ZXMucHkiIGZpbGVuYW1lPSJzZXJ2aWNlcy9wYXRoX2ZpeGVyL3VzZXJfcGF0aF9maXhlcy5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjI5MTciIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU3IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0idXNlcl9wYXRoX2luY2x1ZGVzLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcGF0aF9maXhlci91c2VyX3BhdGhfaW5jbHVkZXMucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4xNDcxIiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc0IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCTwvY2xhc3Nlcz4KCQk8L3BhY2thZ2U+CgkJPHBhY2thZ2UgbmFtZT0ic2VydmljZXMucmVwb3J0IiBsaW5lLXJhdGU9IjAuNDE1NSIgYnJhbmNoLXJhdGU9IjAiIGNvbXBsZXhpdHk9IjAiPgoJCQk8Y2xhc3Nlcz4KCQkJCTxjbGFzcyBuYW1lPSJfX2luaXRfXy5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlcG9ydC9fX2luaXRfXy5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjI4MjkiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMzEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMzMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMzYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNDEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNDkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNTIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNTMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNjMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNjQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNjUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyODAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyODMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyODQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyODUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyODYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyODciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMDUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNDUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzODEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MTUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MTYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MTgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MjAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MjIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MjQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MjUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MzUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MzYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MzciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MzgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NDIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NTEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NTIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NTMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0ODIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0ODMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0ODgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OTgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MDAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MDEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MDIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MDMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MDQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MTUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MTYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MzkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NDAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NDciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NDgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NTgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NjAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NjQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NjUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NzUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1ODkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1OTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1OTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1OTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MTYiIGhpdHM9IjAiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJwYXJzZXIucHkiIGZpbGVuYW1lPSJzZXJ2aWNlcy9yZXBvcnQvcGFyc2VyLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuNjEzOSIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjczIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNTgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNzkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxODUiIGhpdHM9IjEiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJyYXdfdXBsb2FkX3Byb2Nlc3Nvci5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlcG9ydC9yYXdfdXBsb2FkX3Byb2Nlc3Nvci5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjU0NDEiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMDkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNDEiIGhpdHM9IjEiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJyZXBvcnRfcHJvY2Vzc29yLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcmVwb3J0L3JlcG9ydF9wcm9jZXNzb3IucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC41IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTcxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTcyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTczIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgzIiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCTwvY2xhc3Nlcz4KCQk8L3BhY2thZ2U+CgkJPHBhY2thZ2UgbmFtZT0ic2VydmljZXMucmVwb3J0Lmxhbmd1YWdlcyIgbGluZS1yYXRlPSIwLjExNzgiIGJyYW5jaC1yYXRlPSIwIiBjb21wbGV4aXR5PSIwIj4KCQkJPGNsYXNzZXM+CgkJCQk8Y2xhc3MgbmFtZT0iYmFzZS5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlcG9ydC9sYW5ndWFnZXMvYmFzZS5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjIiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OCIgaGl0cz0iMSIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQkJPGNsYXNzIG5hbWU9ImNsb3Zlci5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlcG9ydC9sYW5ndWFnZXMvY2xvdmVyLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuMDE1MzgiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE0IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0iY29iZXJ0dXJhLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcmVwb3J0L2xhbmd1YWdlcy9jb2JlcnR1cmEucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC40NTc0IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjczIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODUiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTExIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTczIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTc0IiBoaXRzPSIxIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0iY3NoYXJwLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcmVwb3J0L2xhbmd1YWdlcy9jc2hhcnAucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4wMTYzOSIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMzQiIGhpdHM9IjAiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJoZWxwZXJzLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcmVwb3J0L2xhbmd1YWdlcy9oZWxwZXJzLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuMTI1IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjEiLz4KCQkJCQk8L2xpbmVzPgoJCQkJPC9jbGFzcz4KCQkJCTxjbGFzcyBuYW1lPSJqYWNvY28ucHkiIGZpbGVuYW1lPSJzZXJ2aWNlcy9yZXBvcnQvbGFuZ3VhZ2VzL2phY29jby5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjAxNDI5IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjczIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijg5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExOCIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQkJPGNsYXNzIG5hbWU9ImpldGJyYWluc3htbC5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlcG9ydC9sYW5ndWFnZXMvamV0YnJhaW5zeG1sLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuMDMyMjYiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU4IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0ibW9uby5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlcG9ydC9sYW5ndWFnZXMvbW9uby5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjA0MzQ4IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ0IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0ic2NvdmVyYWdlLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcmVwb3J0L2xhbmd1YWdlcy9zY292ZXJhZ2UucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4wMjEyOCIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MiIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQkJPGNsYXNzIG5hbWU9InZiLnB5IiBmaWxlbmFtZT0ic2VydmljZXMvcmVwb3J0L2xhbmd1YWdlcy92Yi5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjAzNTcxIiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NiIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQkJPGNsYXNzIG5hbWU9InZiMi5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlcG9ydC9sYW5ndWFnZXMvdmIyLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuMDQiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM4IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCTwvY2xhc3Nlcz4KCQk8L3BhY2thZ2U+CgkJPHBhY2thZ2UgbmFtZT0ic2VydmljZXMueWFtbCIgbGluZS1yYXRlPSIwLjI0MjQiIGJyYW5jaC1yYXRlPSIwIiBjb21wbGV4aXR5PSIwIj4KCQkJPGNsYXNzZXM+CgkJCQk8Y2xhc3MgbmFtZT0icmVhZGVyLnB5IiBmaWxlbmFtZT0ic2VydmljZXMveWFtbC9yZWFkZXIucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4yNDI0IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTYiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUyIiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCTwvY2xhc3Nlcz4KCQk8L3BhY2thZ2U+CgkJPHBhY2thZ2UgbmFtZT0idGFza3MiIGxpbmUtcmF0ZT0iMC4zNzg1IiBicmFuY2gtcmF0ZT0iMCIgY29tcGxleGl0eT0iMCI+CgkJCTxjbGFzc2VzPgoJCQkJPGNsYXNzIG5hbWU9ImJhc2UucHkiIGZpbGVuYW1lPSJ0YXNrcy9iYXNlLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuMjA1MSIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzkiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM4IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCQk8Y2xhc3MgbmFtZT0idXBsb2FkX3Byb2Nlc3Nvci5weSIgZmlsZW5hbWU9InRhc2tzL3VwbG9hZF9wcm9jZXNzb3IucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC40Nzc5IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjcyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzMiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjkwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTAxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTE3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTIzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTI5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTMzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTM1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ3IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQ5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTU4IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTY5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTcwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTg1IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTk0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjA5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjExIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjM5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjUxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjUyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjUzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjU0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjU4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjU5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjYzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjY0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjY1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjY2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjcwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjg0IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjg1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjk1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjk2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzA0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzA1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzExIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzEyIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzEzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzE3IiBoaXRzPSIwIi8+CgkJCQkJPC9saW5lcz4KCQkJCTwvY2xhc3M+CgkJCTwvY2xhc3Nlcz4KCQk8L3BhY2thZ2U+Cgk8L3BhY2thZ2VzPgo8L2NvdmVyYWdlPgo=" + } + }, + { + "name": "apply_async/app.tasks.upload_finisher.UploadFinisherTask", + "context": { + "trace_id": "0x84950fb14140c6eca1c0084d6c8f0528", + "span_id": "0xbb8a9ef0aca2385a", + "trace_state": "[]" + }, + "kind": "SpanKind.PRODUCER", + "parent_id": "0x990a6fbf5f60c679", + "start_time": "2021-09-05T23:52:56.577402Z", + "end_time": "2021-09-05T23:52:56.588826Z", + "status": { + "status_code": "UNSET" + }, + "attributes": { + "celery.action": "apply_async", + "messaging.message_id": "314a9a4e-ecdf-4eba-8eed-13cf38a487c4", + "celery.task_name": "app.tasks.upload_finisher.UploadFinisherTask", + "messaging.destination_kind": "queue", + "messaging.destination": "celery" + }, + "events": [], + "links": [], + "resource": { + "telemetry.sdk.language": "python", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.4.1", + "service.name": "unknown_service" + }, + "codecov": {} + }, + { + "name": "apply_async/app.tasks.notify.Notify", + "context": { + "trace_id": "0x84950fb14140c6eca1c0084d6c8f0528", + "span_id": "0x740038cf07cc97b0", + "trace_state": "[]" + }, + "kind": "SpanKind.PRODUCER", + "parent_id": "0x44558e56011f42b8", + "start_time": "2021-09-05T23:52:56.609546Z", + "end_time": "2021-09-05T23:52:56.620396Z", + "status": { + "status_code": "UNSET" + }, + "attributes": { + "celery.action": "apply_async", + "messaging.message_id": "a3989468-f677-4439-9bc1-4f5ee826fb4f", + "celery.task_name": "app.tasks.notify.Notify", + "messaging.destination_kind": "queue", + "messaging.destination": "celery" + }, + "events": [], + "links": [], + "resource": { + "telemetry.sdk.language": "python", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.4.1", + "service.name": "unknown_service" + }, + "codecov": {} + }, + { + "name": "run/app.tasks.upload.Upload", + "context": { + "trace_id": "0x613d3010e38b46e2a374666a5bf08823", + "span_id": "0xc3574dc89f744f33", + "trace_state": "[]" + }, + "kind": "SpanKind.CONSUMER", + "parent_id": "0x80764b2892ff6471", + "start_time": "2021-09-05T23:53:17.262768Z", + "end_time": "2021-09-05T23:53:17.278853Z", + "status": { + "status_code": "UNSET" + }, + "attributes": { + "celery.action": "run", + "celery.state": "SUCCESS", + "messaging.conversation_id": "40a283eb-df95-48fb-bff1-46547b5bc5df", + "messaging.destination": "celery", + "celery.delivery_info": "{'exchange': '', 'routing_key': 'celery', 'priority': 0, 'redelivered': None}", + "celery.eta": "2021-09-05T23:53:16.982457+00:00", + "messaging.message_id": "40a283eb-df95-48fb-bff1-46547b5bc5df", + "celery.reply_to": "d437a05e-3a88-3f40-a482-2f944088928e", + "celery.retries": 1, + "celery.hostname": "gen10@4f4dceccd966", + "celery.task_name": "app.tasks.upload.Upload" + }, + "events": [], + "links": [], + "resource": { + "telemetry.sdk.language": "python", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.4.1", + "service.name": "unknown_service" + }, + "codecov": { + "type": "bytes", + "coverage": "PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8Y292ZXJhZ2UgdmVyc2lvbj0iNS41IiB0aW1lc3RhbXA9IjE2MzA4ODU5OTk0ODkiIGxpbmVzLXZhbGlkPSIzMzEiIGxpbmVzLWNvdmVyZWQ9IjUxIiBsaW5lLXJhdGU9IjAuMTU0MSIgYnJhbmNoZXMtY292ZXJlZD0iMCIgYnJhbmNoZXMtdmFsaWQ9IjAiIGJyYW5jaC1yYXRlPSIwIiBjb21wbGV4aXR5PSIwIj4KCTwhLS0gR2VuZXJhdGVkIGJ5IGNvdmVyYWdlLnB5OiBodHRwczovL2NvdmVyYWdlLnJlYWR0aGVkb2NzLmlvIC0tPgoJPCEtLSBCYXNlZCBvbiBodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vY29iZXJ0dXJhL3dlYi9tYXN0ZXIvaHRkb2NzL3htbC9jb3ZlcmFnZS0wNC5kdGQgLS0+Cgk8c291cmNlcz4KCQk8c291cmNlPi93b3JrZXI8L3NvdXJjZT4KCTwvc291cmNlcz4KCTxwYWNrYWdlcz4KCQk8cGFja2FnZSBuYW1lPSJoZWxwZXJzIiBsaW5lLXJhdGU9IjAuMjg5NSIgYnJhbmNoLXJhdGU9IjAiIGNvbXBsZXhpdHk9IjAiPgoJCQk8Y2xhc3Nlcz4KCQkJCTxjbGFzcyBuYW1lPSJsb2dnaW5nX2NvbmZpZy5weSIgZmlsZW5hbWU9ImhlbHBlcnMvbG9nZ2luZ19jb25maWcucHkiIGNvbXBsZXhpdHk9IjAiIGxpbmUtcmF0ZT0iMC4yODk1IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzAiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iODEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MiIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQk8L2NsYXNzZXM+CgkJPC9wYWNrYWdlPgoJCTxwYWNrYWdlIG5hbWU9InNlcnZpY2VzIiBsaW5lLXJhdGU9IjAuMjMwOCIgYnJhbmNoLXJhdGU9IjAiIGNvbXBsZXhpdHk9IjAiPgoJCQk8Y2xhc3Nlcz4KCQkJCTxjbGFzcyBuYW1lPSJyZWRpcy5weSIgZmlsZW5hbWU9InNlcnZpY2VzL3JlZGlzLnB5IiBjb21wbGV4aXR5PSIwIiBsaW5lLXJhdGU9IjAuMjMwOCIgYnJhbmNoLXJhdGU9IjAiPgoJCQkJCTxtZXRob2RzLz4KCQkJCQk8bGluZXM+CgkJCQkJCTxsaW5lIG51bWJlcj0iMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxMiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQk8L2NsYXNzZXM+CgkJPC9wYWNrYWdlPgoJCTxwYWNrYWdlIG5hbWU9InRhc2tzIiBsaW5lLXJhdGU9IjAuMTI3MyIgYnJhbmNoLXJhdGU9IjAiIGNvbXBsZXhpdHk9IjAiPgoJCQk8Y2xhc3Nlcz4KCQkJCTxjbGFzcyBuYW1lPSJiYXNlLnB5IiBmaWxlbmFtZT0idGFza3MvYmFzZS5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjIwNTEiIGJyYW5jaC1yYXRlPSIwIj4KCQkJCQk8bWV0aG9kcy8+CgkJCQkJPGxpbmVzPgoJCQkJCQk8bGluZSBudW1iZXI9IjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5IiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDQiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDciIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUwIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTEiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1MiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjUzIiBoaXRzPSIxIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNTQiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI1NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjU2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjAiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjYyIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI2NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjY4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNjkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI3NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijc4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNzkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjgzIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTgiIGhpdHM9IjEiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjExNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyOCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyOSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzMCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzOCIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQkJPGNsYXNzIG5hbWU9InVwbG9hZC5weSIgZmlsZW5hbWU9InRhc2tzL3VwbG9hZC5weSIgY29tcGxleGl0eT0iMCIgbGluZS1yYXRlPSIwLjA5NTI0IiBicmFuY2gtcmF0ZT0iMCI+CgkJCQkJPG1ldGhvZHMvPgoJCQkJCTxsaW5lcz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjciIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI4IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIxNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMTkiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIyMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMjUiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxIiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzIiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iMzYiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSIzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iNDEiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI0NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9Ijk2IiBoaXRzPSIwIi8+CgkJCQkJCTxsaW5lIG51bWJlcj0iOTgiIGhpdHM9IjAiLz4KCQkJCQkJPGxpbmUgbnVtYmVyPSI5OSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwMyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEwNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzMyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzNCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzNiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjEzOSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0MCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0MSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0MiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0MyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE0NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1OCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE1OSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2NCIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE2OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE3OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE4OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjE5OSIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwMiIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwMyIgaGl0cz0iMSIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIwOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIxNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIyOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjIzOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI0MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI0NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI1NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI2OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI3NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI4OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjI5OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMwNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMxOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMyNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjMzNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM0NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM1NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM2NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM2OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM3NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM3OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM3OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM4MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM4MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM4OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM4OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjM5NyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQwMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQwMyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQwNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQwNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQwOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQwOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQxMCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQxNCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQxNSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQxNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQxNyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyMSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyMiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyNiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQyOCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQzOSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ0MCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ0MSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ0MiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ0MyIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ0NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ0NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ0OCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ1NCIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ1NSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ1NiIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ1OSIgaGl0cz0iMCIvPgoJCQkJCQk8bGluZSBudW1iZXI9IjQ2MCIgaGl0cz0iMCIvPgoJCQkJCTwvbGluZXM+CgkJCQk8L2NsYXNzPgoJCQk8L2NsYXNzZXM+CgkJPC9wYWNrYWdlPgoJPC9wYWNrYWdlcz4KPC9jb3ZlcmFnZT4K" + } + } + ] +} \ No newline at end of file diff --git a/apps/worker/tasks/tests/unit/samples/sample_opentelem_normalized.json b/apps/worker/tasks/tests/unit/samples/sample_opentelem_normalized.json new file mode 100644 index 0000000000..2d45bc6151 --- /dev/null +++ b/apps/worker/tasks/tests/unit/samples/sample_opentelem_normalized.json @@ -0,0 +1,8205 @@ +{ + "runs": [ + { + "grouping_attributes": [ + [ + "celery.state", + "RETRY" + ], + [ + "http.method", + null + ] + ], + "group": "run/app.tasks.upload.Upload", + "execs": [ + { + "filename": "helpers/logging_config.py", + "lines": [ + [ + 11, + 1 + ], + [ + 12, + 1 + ], + [ + 13, + 1 + ], + [ + 14, + 1 + ], + [ + 15, + 1 + ], + [ + 24, + 1 + ], + [ + 25, + 1 + ], + [ + 26, + 1 + ], + [ + 27, + 1 + ], + [ + 28, + 1 + ], + [ + 30, + 1 + ] + ] + }, + { + "filename": "services/redis.py", + "lines": [ + [ + 12, + 1 + ], + [ + 13, + 1 + ], + [ + 14, + 1 + ], + [ + 21, + 1 + ], + [ + 22, + 1 + ], + [ + 26, + 1 + ] + ] + }, + { + "filename": "tasks/base.py", + "lines": [ + [ + 39, + 1 + ], + [ + 43, + 1 + ], + [ + 45, + 1 + ], + [ + 47, + 1 + ], + [ + 50, + 1 + ], + [ + 51, + 1 + ], + [ + 52, + 1 + ], + [ + 53, + 1 + ], + [ + 54, + 1 + ], + [ + 55, + 1 + ], + [ + 62, + 1 + ], + [ + 81, + 1 + ], + [ + 98, + 1 + ], + [ + 99, + 1 + ], + [ + 100, + 1 + ], + [ + 123, + 1 + ], + [ + 124, + 1 + ], + [ + 125, + 1 + ] + ] + }, + { + "filename": "tasks/upload.py", + "lines": [ + [ + 99, + 1 + ], + [ + 102, + 1 + ], + [ + 103, + 1 + ], + [ + 104, + 1 + ], + [ + 133, + 1 + ], + [ + 134, + 1 + ], + [ + 136, + 1 + ], + [ + 139, + 1 + ], + [ + 140, + 1 + ], + [ + 141, + 1 + ], + [ + 142, + 1 + ], + [ + 143, + 1 + ], + [ + 158, + 1 + ], + [ + 159, + 1 + ], + [ + 164, + 1 + ], + [ + 167, + 1 + ], + [ + 199, + 1 + ], + [ + 202, + 1 + ], + [ + 208, + 1 + ], + [ + 209, + 1 + ], + [ + 210, + 1 + ], + [ + 211, + 1 + ], + [ + 214, + 1 + ], + [ + 215, + 1 + ], + [ + 216, + 1 + ], + [ + 220, + 1 + ], + [ + 224, + 1 + ] + ] + } + ] + }, + { + "grouping_attributes": [ + [ + "celery.state", + "RETRY" + ], + [ + "http.method", + null + ] + ], + "group": "run/app.tasks.upload.Upload", + "execs": [ + { + "filename": "helpers/logging_config.py", + "lines": [ + [ + 11, + 1 + ], + [ + 12, + 1 + ], + [ + 13, + 1 + ], + [ + 14, + 1 + ], + [ + 15, + 1 + ], + [ + 24, + 1 + ], + [ + 25, + 1 + ], + [ + 26, + 1 + ], + [ + 27, + 1 + ], + [ + 28, + 1 + ], + [ + 30, + 1 + ] + ] + }, + { + "filename": "services/redis.py", + "lines": [ + [ + 12, + 1 + ], + [ + 13, + 1 + ], + [ + 14, + 1 + ], + [ + 21, + 1 + ], + [ + 22, + 1 + ], + [ + 26, + 1 + ] + ] + }, + { + "filename": "tasks/base.py", + "lines": [ + [ + 39, + 1 + ], + [ + 43, + 1 + ], + [ + 45, + 1 + ], + [ + 47, + 1 + ], + [ + 50, + 1 + ], + [ + 51, + 1 + ], + [ + 52, + 1 + ], + [ + 53, + 1 + ], + [ + 54, + 1 + ], + [ + 55, + 1 + ], + [ + 62, + 1 + ], + [ + 81, + 1 + ], + [ + 98, + 1 + ], + [ + 99, + 1 + ], + [ + 100, + 1 + ], + [ + 123, + 1 + ], + [ + 124, + 1 + ], + [ + 125, + 1 + ] + ] + }, + { + "filename": "tasks/upload.py", + "lines": [ + [ + 99, + 1 + ], + [ + 102, + 1 + ], + [ + 103, + 1 + ], + [ + 104, + 1 + ], + [ + 133, + 1 + ], + [ + 134, + 1 + ], + [ + 136, + 1 + ], + [ + 139, + 1 + ], + [ + 140, + 1 + ], + [ + 141, + 1 + ], + [ + 142, + 1 + ], + [ + 143, + 1 + ], + [ + 158, + 1 + ], + [ + 159, + 1 + ], + [ + 164, + 1 + ], + [ + 167, + 1 + ], + [ + 199, + 1 + ], + [ + 202, + 1 + ], + [ + 208, + 1 + ], + [ + 209, + 1 + ], + [ + 210, + 1 + ], + [ + 211, + 1 + ], + [ + 214, + 1 + ], + [ + 215, + 1 + ], + [ + 216, + 1 + ], + [ + 220, + 1 + ], + [ + 224, + 1 + ] + ] + } + ] + }, + { + "grouping_attributes": [ + [ + "celery.state", + "SUCCESS" + ], + [ + "http.method", + null + ] + ], + "group": "run/app.tasks.upload_processor.UploadProcessorTask", + "execs": [ + { + "filename": "database/base.py", + "lines": [ + [ + 17, + 1 + ], + [ + 32, + 1 + ] + ] + }, + { + "filename": "database/engine.py", + "lines": [ + [ + 18, + 1 + ], + [ + 19, + 1 + ], + [ + 24, + 1 + ] + ] + }, + { + "filename": "database/models/core.py", + "lines": [ + [ + 123, + 1 + ], + [ + 168, + 1 + ] + ] + }, + { + "filename": "database/models/reports.py", + "lines": [ + [ + 85, + 1 + ], + [ + 116, + 1 + ], + [ + 118, + 1 + ], + [ + 119, + 1 + ], + [ + 120, + 1 + ], + [ + 121, + 1 + ], + [ + 122, + 1 + ], + [ + 123, + 1 + ], + [ + 124, + 1 + ] + ] + }, + { + "filename": "helpers/cache.py", + "lines": [ + [ + 29, + 1 + ], + [ + 30, + 1 + ], + [ + 31, + 1 + ], + [ + 38, + 1 + ], + [ + 39, + 1 + ], + [ + 40, + 1 + ], + [ + 41, + 1 + ], + [ + 42, + 1 + ], + [ + 44, + 1 + ], + [ + 98, + 1 + ], + [ + 99, + 1 + ], + [ + 103, + 1 + ], + [ + 105, + 1 + ], + [ + 106, + 1 + ], + [ + 161, + 1 + ], + [ + 188, + 1 + ], + [ + 189, + 1 + ], + [ + 190, + 1 + ], + [ + 191, + 1 + ], + [ + 192, + 1 + ], + [ + 202, + 1 + ], + [ + 203, + 1 + ], + [ + 204, + 1 + ], + [ + 205, + 1 + ] + ] + }, + { + "filename": "helpers/logging_config.py", + "lines": [ + [ + 11, + 1 + ], + [ + 12, + 1 + ], + [ + 13, + 1 + ], + [ + 14, + 1 + ], + [ + 15, + 1 + ], + [ + 24, + 1 + ], + [ + 25, + 1 + ], + [ + 26, + 1 + ], + [ + 27, + 1 + ], + [ + 28, + 1 + ], + [ + 30, + 1 + ] + ] + }, + { + "filename": "helpers/pathmap/pathmap.py", + "lines": [ + [ + 9, + 1 + ], + [ + 16, + 1 + ], + [ + 23, + 1 + ], + [ + 24, + 1 + ], + [ + 25, + 1 + ], + [ + 26, + 1 + ], + [ + 42, + 1 + ], + [ + 44, + 1 + ], + [ + 46, + 1 + ], + [ + 47, + 1 + ], + [ + 51, + 1 + ] + ] + }, + { + "filename": "helpers/pathmap/tree.py", + "lines": [ + [ + 10, + 1 + ], + [ + 13, + 1 + ], + [ + 16, + 1 + ], + [ + 25, + 1 + ], + [ + 26, + 1 + ], + [ + 27, + 1 + ], + [ + 28, + 1 + ], + [ + 29, + 1 + ], + [ + 30, + 1 + ], + [ + 83, + 1 + ], + [ + 85, + 1 + ], + [ + 86, + 1 + ], + [ + 88, + 1 + ], + [ + 89, + 1 + ], + [ + 90, + 1 + ], + [ + 91, + 1 + ], + [ + 92, + 1 + ], + [ + 96, + 1 + ], + [ + 100, + 1 + ], + [ + 110, + 1 + ], + [ + 111, + 1 + ], + [ + 112, + 1 + ], + [ + 114, + 1 + ], + [ + 117, + 1 + ], + [ + 118, + 1 + ], + [ + 127, + 1 + ], + [ + 156, + 1 + ], + [ + 157, + 1 + ], + [ + 158, + 1 + ], + [ + 160, + 1 + ], + [ + 161, + 1 + ], + [ + 162, + 1 + ], + [ + 174, + 1 + ], + [ + 175, + 1 + ] + ] + }, + { + "filename": "services/archive.py", + "lines": [ + [ + 28, + 1 + ], + [ + 58, + 1 + ], + [ + 59, + 1 + ], + [ + 62, + 1 + ], + [ + 63, + 1 + ], + [ + 64, + 1 + ], + [ + 84, + 1 + ], + [ + 85, + 1 + ], + [ + 86, + 1 + ], + [ + 97, + 1 + ], + [ + 98, + 1 + ], + [ + 115, + 1 + ], + [ + 192, + 1 + ], + [ + 196, + 1 + ], + [ + 197, + 1 + ], + [ + 204, + 1 + ], + [ + 205, + 1 + ], + [ + 206, + 1 + ], + [ + 209, + 1 + ], + [ + 233, + 1 + ], + [ + 237, + 1 + ] + ] + }, + { + "filename": "services/bots.py", + "lines": [ + [ + 17, + 1 + ], + [ + 18, + 1 + ], + [ + 21, + 1 + ], + [ + 43, + 1 + ], + [ + 44, + 1 + ] + ] + }, + { + "filename": "services/redis.py", + "lines": [ + [ + 12, + 1 + ], + [ + 13, + 1 + ], + [ + 14, + 1 + ], + [ + 21, + 1 + ], + [ + 22, + 1 + ], + [ + 26, + 1 + ] + ] + }, + { + "filename": "services/repository.py", + "lines": [ + [ + 29, + 1 + ], + [ + 33, + 1 + ], + [ + 34, + 1 + ], + [ + 35, + 1 + ], + [ + 56, + 1 + ], + [ + 60, + 1 + ] + ] + }, + { + "filename": "services/storage.py", + "lines": [ + [ + 12, + 1 + ], + [ + 17, + 1 + ], + [ + 20, + 1 + ] + ] + }, + { + "filename": "services/path_fixer/__init__.py", + "lines": [ + [ + 41, + 1 + ], + [ + 44, + 1 + ], + [ + 45, + 1 + ], + [ + 46, + 1 + ], + [ + 47, + 1 + ], + [ + 50, + 1 + ], + [ + 51, + 1 + ], + [ + 54, + 1 + ], + [ + 64, + 1 + ], + [ + 65, + 1 + ], + [ + 66, + 1 + ], + [ + 67, + 1 + ], + [ + 68, + 1 + ], + [ + 71, + 1 + ], + [ + 72, + 1 + ], + [ + 73, + 1 + ], + [ + 74, + 1 + ], + [ + 75, + 1 + ], + [ + 78, + 1 + ], + [ + 80, + 1 + ], + [ + 81, + 1 + ], + [ + 84, + 1 + ], + [ + 85, + 1 + ], + [ + 86, + 1 + ], + [ + 90, + 1 + ], + [ + 93, + 1 + ], + [ + 96, + 1 + ], + [ + 99, + 1 + ], + [ + 102, + 1 + ], + [ + 103, + 1 + ], + [ + 104, + 1 + ], + [ + 107, + 1 + ], + [ + 112, + 1 + ], + [ + 113, + 1 + ], + [ + 118, + 1 + ], + [ + 121, + 1 + ], + [ + 122, + 1 + ], + [ + 127, + 1 + ], + [ + 152, + 1 + ], + [ + 165, + 1 + ] + ] + }, + { + "filename": "services/path_fixer/fixpaths.py", + "lines": [ + [ + 98, + 1 + ], + [ + 99, + 1 + ], + [ + 101, + 1 + ], + [ + 105, + 1 + ], + [ + 107, + 1 + ], + [ + 109, + 1 + ], + [ + 110, + 1 + ], + [ + 115, + 1 + ], + [ + 119, + 1 + ], + [ + 121, + 1 + ] + ] + }, + { + "filename": "services/path_fixer/user_path_fixes.py", + "lines": [ + [ + 25, + 1 + ], + [ + 26, + 1 + ], + [ + 29, + 1 + ], + [ + 30, + 1 + ], + [ + 31, + 1 + ], + [ + 35, + 1 + ], + [ + 44, + 1 + ] + ] + }, + { + "filename": "services/path_fixer/user_path_includes.py", + "lines": [ + [ + 27, + 1 + ], + [ + 28, + 1 + ], + [ + 29, + 1 + ], + [ + 56, + 1 + ], + [ + 57, + 1 + ] + ] + }, + { + "filename": "services/report/__init__.py", + "lines": [ + [ + 48, + 1 + ], + [ + 55, + 1 + ], + [ + 80, + 1 + ], + [ + 228, + 1 + ], + [ + 229, + 1 + ], + [ + 230, + 1 + ], + [ + 231, + 1 + ], + [ + 233, + 1 + ], + [ + 236, + 1 + ], + [ + 241, + 1 + ], + [ + 249, + 1 + ], + [ + 250, + 1 + ], + [ + 252, + 1 + ], + [ + 253, + 1 + ], + [ + 254, + 1 + ], + [ + 261, + 1 + ], + [ + 263, + 1 + ], + [ + 264, + 1 + ], + [ + 265, + 1 + ], + [ + 266, + 1 + ], + [ + 269, + 1 + ], + [ + 414, + 1 + ], + [ + 415, + 1 + ], + [ + 416, + 1 + ], + [ + 417, + 1 + ], + [ + 418, + 1 + ], + [ + 419, + 1 + ], + [ + 420, + 1 + ], + [ + 421, + 1 + ], + [ + 422, + 1 + ], + [ + 424, + 1 + ], + [ + 425, + 1 + ], + [ + 435, + 1 + ], + [ + 436, + 1 + ], + [ + 437, + 1 + ], + [ + 438, + 1 + ], + [ + 451, + 1 + ], + [ + 452, + 1 + ], + [ + 453, + 1 + ], + [ + 454, + 1 + ], + [ + 457, + 1 + ], + [ + 470, + 1 + ], + [ + 497, + 1 + ], + [ + 498, + 1 + ], + [ + 499, + 1 + ], + [ + 500, + 1 + ], + [ + 501, + 1 + ], + [ + 502, + 1 + ], + [ + 503, + 1 + ], + [ + 504, + 1 + ], + [ + 515, + 1 + ], + [ + 516, + 1 + ], + [ + 517, + 1 + ], + [ + 530, + 1 + ], + [ + 532, + 1 + ], + [ + 533, + 1 + ], + [ + 534, + 1 + ], + [ + 535, + 1 + ], + [ + 536, + 1 + ], + [ + 537, + 1 + ], + [ + 538, + 1 + ], + [ + 539, + 1 + ], + [ + 540, + 1 + ], + [ + 547, + 1 + ], + [ + 548, + 1 + ], + [ + 558, + 1 + ], + [ + 559, + 1 + ], + [ + 560, + 1 + ], + [ + 561, + 1 + ], + [ + 564, + 1 + ], + [ + 565, + 1 + ], + [ + 566, + 1 + ], + [ + 575, + 1 + ] + ] + }, + { + "filename": "services/report/parser.py", + "lines": [ + [ + 10, + 1 + ], + [ + 11, + 1 + ], + [ + 12, + 1 + ], + [ + 16, + 1 + ], + [ + 19, + 1 + ], + [ + 30, + 1 + ], + [ + 31, + 1 + ], + [ + 32, + 1 + ], + [ + 33, + 1 + ], + [ + 36, + 1 + ], + [ + 39, + 1 + ], + [ + 42, + 1 + ], + [ + 46, + 1 + ], + [ + 71, + 1 + ], + [ + 72, + 1 + ], + [ + 73, + 1 + ], + [ + 74, + 1 + ], + [ + 75, + 1 + ], + [ + 76, + 1 + ], + [ + 77, + 1 + ], + [ + 78, + 1 + ], + [ + 81, + 1 + ], + [ + 82, + 1 + ], + [ + 83, + 1 + ], + [ + 85, + 1 + ], + [ + 98, + 1 + ], + [ + 99, + 1 + ], + [ + 100, + 1 + ], + [ + 101, + 1 + ], + [ + 102, + 1 + ], + [ + 103, + 1 + ], + [ + 130, + 1 + ], + [ + 131, + 1 + ], + [ + 132, + 1 + ], + [ + 133, + 1 + ], + [ + 134, + 1 + ], + [ + 135, + 1 + ], + [ + 136, + 1 + ], + [ + 137, + 1 + ], + [ + 138, + 1 + ], + [ + 139, + 1 + ], + [ + 140, + 1 + ], + [ + 141, + 1 + ], + [ + 142, + 1 + ], + [ + 143, + 1 + ], + [ + 144, + 1 + ], + [ + 145, + 1 + ], + [ + 146, + 1 + ], + [ + 148, + 1 + ], + [ + 157, + 1 + ], + [ + 158, + 1 + ], + [ + 166, + 1 + ], + [ + 167, + 1 + ], + [ + 168, + 1 + ], + [ + 169, + 1 + ], + [ + 170, + 1 + ], + [ + 171, + 1 + ], + [ + 172, + 1 + ], + [ + 173, + 1 + ], + [ + 176, + 1 + ], + [ + 179, + 1 + ], + [ + 185, + 1 + ] + ] + }, + { + "filename": "services/report/raw_upload_processor.py", + "lines": [ + [ + 31, + 1 + ], + [ + 36, + 1 + ], + [ + 37, + 1 + ], + [ + 38, + 1 + ], + [ + 39, + 1 + ], + [ + 45, + 1 + ], + [ + 48, + 1 + ], + [ + 55, + 1 + ], + [ + 60, + 1 + ], + [ + 65, + 1 + ], + [ + 66, + 1 + ], + [ + 67, + 1 + ], + [ + 70, + 1 + ], + [ + 71, + 1 + ], + [ + 73, + 1 + ], + [ + 76, + 1 + ], + [ + 77, + 1 + ], + [ + 79, + 1 + ], + [ + 80, + 1 + ], + [ + 81, + 1 + ], + [ + 82, + 1 + ], + [ + 90, + 1 + ], + [ + 91, + 1 + ], + [ + 92, + 1 + ], + [ + 93, + 1 + ], + [ + 96, + 1 + ], + [ + 99, + 1 + ], + [ + 106, + 1 + ], + [ + 107, + 1 + ], + [ + 108, + 1 + ], + [ + 109, + 1 + ], + [ + 120, + 1 + ], + [ + 121, + 1 + ], + [ + 122, + 1 + ], + [ + 126, + 1 + ], + [ + 131, + 1 + ], + [ + 141, + 1 + ] + ] + }, + { + "filename": "services/report/report_processor.py", + "lines": [ + [ + 48, + 1 + ], + [ + 49, + 1 + ], + [ + 50, + 1 + ], + [ + 51, + 1 + ], + [ + 64, + 1 + ], + [ + 69, + 1 + ], + [ + 73, + 1 + ], + [ + 75, + 1 + ], + [ + 76, + 1 + ], + [ + 77, + 1 + ], + [ + 80, + 1 + ], + [ + 81, + 1 + ], + [ + 82, + 1 + ], + [ + 84, + 1 + ], + [ + 85, + 1 + ], + [ + 86, + 1 + ], + [ + 87, + 1 + ], + [ + 88, + 1 + ], + [ + 95, + 1 + ], + [ + 130, + 1 + ], + [ + 136, + 1 + ], + [ + 137, + 1 + ], + [ + 138, + 1 + ], + [ + 139, + 1 + ], + [ + 142, + 1 + ], + [ + 143, + 1 + ], + [ + 144, + 1 + ], + [ + 145, + 1 + ], + [ + 148, + 1 + ], + [ + 149, + 1 + ], + [ + 157, + 1 + ], + [ + 160, + 1 + ] + ] + }, + { + "filename": "services/report/languages/base.py", + "lines": [ + [ + 9, + 1 + ], + [ + 12, + 1 + ], + [ + 68, + 1 + ] + ] + }, + { + "filename": "services/report/languages/clover.py", + "lines": [ + [ + 12, + 1 + ] + ] + }, + { + "filename": "services/report/languages/cobertura.py", + "lines": [ + [ + 19, + 1 + ], + [ + 20, + 1 + ], + [ + 24, + 1 + ], + [ + 28, + 1 + ], + [ + 29, + 1 + ], + [ + 35, + 1 + ], + [ + 36, + 1 + ], + [ + 41, + 1 + ], + [ + 66, + 1 + ], + [ + 68, + 1 + ], + [ + 69, + 1 + ], + [ + 70, + 1 + ], + [ + 72, + 1 + ], + [ + 73, + 1 + ], + [ + 74, + 1 + ], + [ + 75, + 1 + ], + [ + 77, + 1 + ], + [ + 78, + 1 + ], + [ + 79, + 1 + ], + [ + 80, + 1 + ], + [ + 81, + 1 + ], + [ + 84, + 1 + ], + [ + 85, + 1 + ], + [ + 86, + 1 + ], + [ + 93, + 1 + ], + [ + 96, + 1 + ], + [ + 97, + 1 + ], + [ + 106, + 1 + ], + [ + 111, + 1 + ], + [ + 126, + 1 + ], + [ + 128, + 1 + ], + [ + 138, + 1 + ], + [ + 158, + 1 + ], + [ + 161, + 1 + ], + [ + 162, + 1 + ], + [ + 163, + 1 + ], + [ + 164, + 1 + ], + [ + 165, + 1 + ], + [ + 166, + 1 + ], + [ + 168, + 1 + ], + [ + 169, + 1 + ], + [ + 173, + 1 + ], + [ + 174, + 1 + ] + ] + }, + { + "filename": "services/report/languages/csharp.py", + "lines": [ + [ + 12, + 1 + ] + ] + }, + { + "filename": "services/report/languages/helpers.py", + "lines": [ + [ + 24, + 1 + ] + ] + }, + { + "filename": "services/report/languages/jacoco.py", + "lines": [ + [ + 14, + 1 + ] + ] + }, + { + "filename": "services/report/languages/jetbrainsxml.py", + "lines": [ + [ + 9, + 1 + ] + ] + }, + { + "filename": "services/report/languages/mono.py", + "lines": [ + [ + 9, + 1 + ] + ] + }, + { + "filename": "services/report/languages/scoverage.py", + "lines": [ + [ + 10, + 1 + ] + ] + }, + { + "filename": "services/report/languages/vb.py", + "lines": [ + [ + 9, + 1 + ] + ] + }, + { + "filename": "services/report/languages/vb2.py", + "lines": [ + [ + 9, + 1 + ] + ] + }, + { + "filename": "services/yaml/reader.py", + "lines": [ + [ + 16, + 1 + ], + [ + 17, + 1 + ], + [ + 18, + 1 + ], + [ + 19, + 1 + ], + [ + 20, + 1 + ], + [ + 23, + 1 + ], + [ + 24, + 1 + ], + [ + 25, + 1 + ] + ] + }, + { + "filename": "tasks/base.py", + "lines": [ + [ + 39, + 1 + ], + [ + 43, + 1 + ], + [ + 45, + 1 + ], + [ + 47, + 1 + ], + [ + 50, + 1 + ], + [ + 51, + 1 + ], + [ + 52, + 1 + ], + [ + 53, + 1 + ], + [ + 54, + 1 + ], + [ + 81, + 1 + ], + [ + 98, + 1 + ], + [ + 99, + 1 + ], + [ + 100, + 1 + ], + [ + 128, + 1 + ], + [ + 129, + 1 + ], + [ + 130, + 1 + ] + ] + }, + { + "filename": "tasks/upload_processor.py", + "lines": [ + [ + 69, + 1 + ], + [ + 70, + 1 + ], + [ + 71, + 1 + ], + [ + 72, + 1 + ], + [ + 73, + 1 + ], + [ + 74, + 1 + ], + [ + 79, + 1 + ], + [ + 80, + 1 + ], + [ + 112, + 1 + ], + [ + 113, + 1 + ], + [ + 114, + 1 + ], + [ + 115, + 1 + ], + [ + 116, + 1 + ], + [ + 117, + 1 + ], + [ + 120, + 1 + ], + [ + 121, + 1 + ], + [ + 122, + 1 + ], + [ + 123, + 1 + ], + [ + 124, + 1 + ], + [ + 125, + 1 + ], + [ + 126, + 1 + ], + [ + 128, + 1 + ], + [ + 129, + 1 + ], + [ + 130, + 1 + ], + [ + 132, + 1 + ], + [ + 133, + 1 + ], + [ + 134, + 1 + ], + [ + 135, + 1 + ], + [ + 145, + 1 + ], + [ + 146, + 1 + ], + [ + 147, + 1 + ], + [ + 148, + 1 + ], + [ + 149, + 1 + ], + [ + 150, + 1 + ], + [ + 155, + 1 + ], + [ + 158, + 1 + ], + [ + 166, + 1 + ], + [ + 181, + 1 + ], + [ + 182, + 1 + ], + [ + 183, + 1 + ], + [ + 184, + 1 + ], + [ + 185, + 1 + ], + [ + 190, + 1 + ], + [ + 191, + 1 + ], + [ + 194, + 1 + ], + [ + 204, + 1 + ], + [ + 220, + 1 + ], + [ + 223, + 1 + ], + [ + 236, + 1 + ], + [ + 239, + 1 + ], + [ + 251, + 1 + ], + [ + 252, + 1 + ], + [ + 253, + 1 + ], + [ + 261, + 1 + ], + [ + 264, + 1 + ], + [ + 266, + 1 + ], + [ + 280, + 1 + ], + [ + 281, + 1 + ], + [ + 282, + 1 + ], + [ + 283, + 1 + ], + [ + 284, + 1 + ], + [ + 301, + 1 + ], + [ + 311, + 1 + ], + [ + 312, + 1 + ], + [ + 313, + 1 + ] + ] + } + ] + }, + { + "grouping_attributes": [ + [ + "celery.state", + "SUCCESS" + ], + [ + "http.method", + null + ] + ], + "group": "run/app.tasks.upload_processor.UploadProcessorTask", + "execs": [ + { + "filename": "database/base.py", + "lines": [ + [ + 17, + 1 + ], + [ + 32, + 1 + ] + ] + }, + { + "filename": "database/engine.py", + "lines": [ + [ + 18, + 1 + ], + [ + 19, + 1 + ], + [ + 24, + 1 + ] + ] + }, + { + "filename": "database/models/core.py", + "lines": [ + [ + 123, + 1 + ], + [ + 168, + 1 + ] + ] + }, + { + "filename": "database/models/reports.py", + "lines": [ + [ + 85, + 1 + ], + [ + 116, + 1 + ], + [ + 118, + 1 + ], + [ + 119, + 1 + ], + [ + 120, + 1 + ], + [ + 121, + 1 + ], + [ + 122, + 1 + ], + [ + 123, + 1 + ], + [ + 124, + 1 + ] + ] + }, + { + "filename": "helpers/cache.py", + "lines": [ + [ + 29, + 1 + ], + [ + 30, + 1 + ], + [ + 31, + 1 + ], + [ + 38, + 1 + ], + [ + 39, + 1 + ], + [ + 40, + 1 + ], + [ + 41, + 1 + ], + [ + 42, + 1 + ], + [ + 44, + 1 + ], + [ + 98, + 1 + ], + [ + 99, + 1 + ], + [ + 103, + 1 + ], + [ + 105, + 1 + ], + [ + 106, + 1 + ], + [ + 161, + 1 + ], + [ + 188, + 1 + ], + [ + 189, + 1 + ], + [ + 190, + 1 + ], + [ + 191, + 1 + ], + [ + 192, + 1 + ], + [ + 202, + 1 + ], + [ + 203, + 1 + ], + [ + 204, + 1 + ], + [ + 205, + 1 + ] + ] + }, + { + "filename": "helpers/logging_config.py", + "lines": [ + [ + 11, + 1 + ], + [ + 12, + 1 + ], + [ + 13, + 1 + ], + [ + 14, + 1 + ], + [ + 15, + 1 + ], + [ + 24, + 1 + ], + [ + 25, + 1 + ], + [ + 26, + 1 + ], + [ + 27, + 1 + ], + [ + 28, + 1 + ], + [ + 30, + 1 + ] + ] + }, + { + "filename": "helpers/pathmap/pathmap.py", + "lines": [ + [ + 9, + 1 + ], + [ + 16, + 1 + ], + [ + 23, + 1 + ], + [ + 24, + 1 + ], + [ + 25, + 1 + ], + [ + 26, + 1 + ], + [ + 42, + 1 + ], + [ + 44, + 1 + ], + [ + 46, + 1 + ], + [ + 47, + 1 + ], + [ + 51, + 1 + ] + ] + }, + { + "filename": "helpers/pathmap/tree.py", + "lines": [ + [ + 10, + 1 + ], + [ + 13, + 1 + ], + [ + 16, + 1 + ], + [ + 25, + 1 + ], + [ + 26, + 1 + ], + [ + 27, + 1 + ], + [ + 28, + 1 + ], + [ + 29, + 1 + ], + [ + 30, + 1 + ], + [ + 83, + 1 + ], + [ + 85, + 1 + ], + [ + 86, + 1 + ], + [ + 88, + 1 + ], + [ + 89, + 1 + ], + [ + 90, + 1 + ], + [ + 91, + 1 + ], + [ + 92, + 1 + ], + [ + 96, + 1 + ], + [ + 100, + 1 + ], + [ + 110, + 1 + ], + [ + 111, + 1 + ], + [ + 112, + 1 + ], + [ + 114, + 1 + ], + [ + 117, + 1 + ], + [ + 118, + 1 + ], + [ + 127, + 1 + ], + [ + 156, + 1 + ], + [ + 157, + 1 + ], + [ + 158, + 1 + ], + [ + 160, + 1 + ], + [ + 161, + 1 + ], + [ + 162, + 1 + ], + [ + 174, + 1 + ], + [ + 175, + 1 + ] + ] + }, + { + "filename": "services/archive.py", + "lines": [ + [ + 28, + 1 + ], + [ + 58, + 1 + ], + [ + 59, + 1 + ], + [ + 62, + 1 + ], + [ + 63, + 1 + ], + [ + 64, + 1 + ], + [ + 84, + 1 + ], + [ + 85, + 1 + ], + [ + 86, + 1 + ], + [ + 97, + 1 + ], + [ + 98, + 1 + ], + [ + 115, + 1 + ], + [ + 192, + 1 + ], + [ + 196, + 1 + ], + [ + 197, + 1 + ], + [ + 204, + 1 + ], + [ + 205, + 1 + ], + [ + 206, + 1 + ], + [ + 209, + 1 + ], + [ + 233, + 1 + ], + [ + 237, + 1 + ] + ] + }, + { + "filename": "services/bots.py", + "lines": [ + [ + 17, + 1 + ], + [ + 18, + 1 + ], + [ + 21, + 1 + ], + [ + 43, + 1 + ], + [ + 44, + 1 + ] + ] + }, + { + "filename": "services/redis.py", + "lines": [ + [ + 12, + 1 + ], + [ + 13, + 1 + ], + [ + 14, + 1 + ], + [ + 21, + 1 + ], + [ + 22, + 1 + ], + [ + 26, + 1 + ] + ] + }, + { + "filename": "services/repository.py", + "lines": [ + [ + 29, + 1 + ], + [ + 33, + 1 + ], + [ + 34, + 1 + ], + [ + 35, + 1 + ], + [ + 56, + 1 + ], + [ + 60, + 1 + ] + ] + }, + { + "filename": "services/storage.py", + "lines": [ + [ + 12, + 1 + ], + [ + 17, + 1 + ], + [ + 20, + 1 + ] + ] + }, + { + "filename": "services/path_fixer/__init__.py", + "lines": [ + [ + 41, + 1 + ], + [ + 44, + 1 + ], + [ + 45, + 1 + ], + [ + 46, + 1 + ], + [ + 47, + 1 + ], + [ + 50, + 1 + ], + [ + 51, + 1 + ], + [ + 54, + 1 + ], + [ + 64, + 1 + ], + [ + 65, + 1 + ], + [ + 66, + 1 + ], + [ + 67, + 1 + ], + [ + 68, + 1 + ], + [ + 71, + 1 + ], + [ + 72, + 1 + ], + [ + 73, + 1 + ], + [ + 74, + 1 + ], + [ + 75, + 1 + ], + [ + 78, + 1 + ], + [ + 80, + 1 + ], + [ + 81, + 1 + ], + [ + 84, + 1 + ], + [ + 85, + 1 + ], + [ + 86, + 1 + ], + [ + 90, + 1 + ], + [ + 93, + 1 + ], + [ + 96, + 1 + ], + [ + 99, + 1 + ], + [ + 102, + 1 + ], + [ + 103, + 1 + ], + [ + 104, + 1 + ], + [ + 107, + 1 + ], + [ + 112, + 1 + ], + [ + 113, + 1 + ], + [ + 118, + 1 + ], + [ + 121, + 1 + ], + [ + 122, + 1 + ], + [ + 127, + 1 + ], + [ + 152, + 1 + ], + [ + 165, + 1 + ] + ] + }, + { + "filename": "services/path_fixer/fixpaths.py", + "lines": [ + [ + 98, + 1 + ], + [ + 99, + 1 + ], + [ + 101, + 1 + ], + [ + 105, + 1 + ], + [ + 107, + 1 + ], + [ + 109, + 1 + ], + [ + 110, + 1 + ], + [ + 115, + 1 + ], + [ + 119, + 1 + ], + [ + 121, + 1 + ] + ] + }, + { + "filename": "services/path_fixer/user_path_fixes.py", + "lines": [ + [ + 25, + 1 + ], + [ + 26, + 1 + ], + [ + 29, + 1 + ], + [ + 30, + 1 + ], + [ + 31, + 1 + ], + [ + 35, + 1 + ], + [ + 44, + 1 + ] + ] + }, + { + "filename": "services/path_fixer/user_path_includes.py", + "lines": [ + [ + 27, + 1 + ], + [ + 28, + 1 + ], + [ + 29, + 1 + ], + [ + 56, + 1 + ], + [ + 57, + 1 + ] + ] + }, + { + "filename": "services/report/__init__.py", + "lines": [ + [ + 48, + 1 + ], + [ + 55, + 1 + ], + [ + 80, + 1 + ], + [ + 228, + 1 + ], + [ + 229, + 1 + ], + [ + 230, + 1 + ], + [ + 231, + 1 + ], + [ + 233, + 1 + ], + [ + 236, + 1 + ], + [ + 241, + 1 + ], + [ + 249, + 1 + ], + [ + 250, + 1 + ], + [ + 252, + 1 + ], + [ + 253, + 1 + ], + [ + 254, + 1 + ], + [ + 261, + 1 + ], + [ + 263, + 1 + ], + [ + 264, + 1 + ], + [ + 265, + 1 + ], + [ + 266, + 1 + ], + [ + 269, + 1 + ], + [ + 414, + 1 + ], + [ + 415, + 1 + ], + [ + 416, + 1 + ], + [ + 417, + 1 + ], + [ + 418, + 1 + ], + [ + 419, + 1 + ], + [ + 420, + 1 + ], + [ + 421, + 1 + ], + [ + 422, + 1 + ], + [ + 424, + 1 + ], + [ + 425, + 1 + ], + [ + 435, + 1 + ], + [ + 436, + 1 + ], + [ + 437, + 1 + ], + [ + 438, + 1 + ], + [ + 451, + 1 + ], + [ + 452, + 1 + ], + [ + 453, + 1 + ], + [ + 454, + 1 + ], + [ + 457, + 1 + ], + [ + 470, + 1 + ], + [ + 497, + 1 + ], + [ + 498, + 1 + ], + [ + 499, + 1 + ], + [ + 500, + 1 + ], + [ + 501, + 1 + ], + [ + 502, + 1 + ], + [ + 503, + 1 + ], + [ + 504, + 1 + ], + [ + 515, + 1 + ], + [ + 516, + 1 + ], + [ + 517, + 1 + ], + [ + 530, + 1 + ], + [ + 532, + 1 + ], + [ + 533, + 1 + ], + [ + 534, + 1 + ], + [ + 535, + 1 + ], + [ + 536, + 1 + ], + [ + 537, + 1 + ], + [ + 538, + 1 + ], + [ + 539, + 1 + ], + [ + 540, + 1 + ], + [ + 547, + 1 + ], + [ + 548, + 1 + ], + [ + 558, + 1 + ], + [ + 559, + 1 + ], + [ + 560, + 1 + ], + [ + 561, + 1 + ], + [ + 564, + 1 + ], + [ + 565, + 1 + ], + [ + 566, + 1 + ], + [ + 575, + 1 + ] + ] + }, + { + "filename": "services/report/parser.py", + "lines": [ + [ + 10, + 1 + ], + [ + 11, + 1 + ], + [ + 12, + 1 + ], + [ + 16, + 1 + ], + [ + 19, + 1 + ], + [ + 30, + 1 + ], + [ + 31, + 1 + ], + [ + 32, + 1 + ], + [ + 33, + 1 + ], + [ + 36, + 1 + ], + [ + 39, + 1 + ], + [ + 42, + 1 + ], + [ + 46, + 1 + ], + [ + 71, + 1 + ], + [ + 72, + 1 + ], + [ + 73, + 1 + ], + [ + 74, + 1 + ], + [ + 75, + 1 + ], + [ + 76, + 1 + ], + [ + 77, + 1 + ], + [ + 78, + 1 + ], + [ + 81, + 1 + ], + [ + 82, + 1 + ], + [ + 83, + 1 + ], + [ + 85, + 1 + ], + [ + 98, + 1 + ], + [ + 99, + 1 + ], + [ + 100, + 1 + ], + [ + 101, + 1 + ], + [ + 102, + 1 + ], + [ + 103, + 1 + ], + [ + 130, + 1 + ], + [ + 131, + 1 + ], + [ + 132, + 1 + ], + [ + 133, + 1 + ], + [ + 134, + 1 + ], + [ + 135, + 1 + ], + [ + 136, + 1 + ], + [ + 137, + 1 + ], + [ + 138, + 1 + ], + [ + 139, + 1 + ], + [ + 140, + 1 + ], + [ + 141, + 1 + ], + [ + 142, + 1 + ], + [ + 143, + 1 + ], + [ + 144, + 1 + ], + [ + 145, + 1 + ], + [ + 146, + 1 + ], + [ + 148, + 1 + ], + [ + 157, + 1 + ], + [ + 158, + 1 + ], + [ + 166, + 1 + ], + [ + 167, + 1 + ], + [ + 168, + 1 + ], + [ + 169, + 1 + ], + [ + 170, + 1 + ], + [ + 171, + 1 + ], + [ + 172, + 1 + ], + [ + 173, + 1 + ], + [ + 176, + 1 + ], + [ + 179, + 1 + ], + [ + 185, + 1 + ] + ] + }, + { + "filename": "services/report/raw_upload_processor.py", + "lines": [ + [ + 31, + 1 + ], + [ + 36, + 1 + ], + [ + 37, + 1 + ], + [ + 38, + 1 + ], + [ + 39, + 1 + ], + [ + 45, + 1 + ], + [ + 48, + 1 + ], + [ + 55, + 1 + ], + [ + 60, + 1 + ], + [ + 65, + 1 + ], + [ + 66, + 1 + ], + [ + 67, + 1 + ], + [ + 70, + 1 + ], + [ + 71, + 1 + ], + [ + 73, + 1 + ], + [ + 76, + 1 + ], + [ + 77, + 1 + ], + [ + 79, + 1 + ], + [ + 80, + 1 + ], + [ + 81, + 1 + ], + [ + 82, + 1 + ], + [ + 90, + 1 + ], + [ + 91, + 1 + ], + [ + 92, + 1 + ], + [ + 93, + 1 + ], + [ + 96, + 1 + ], + [ + 99, + 1 + ], + [ + 106, + 1 + ], + [ + 107, + 1 + ], + [ + 108, + 1 + ], + [ + 109, + 1 + ], + [ + 120, + 1 + ], + [ + 121, + 1 + ], + [ + 122, + 1 + ], + [ + 126, + 1 + ], + [ + 131, + 1 + ], + [ + 141, + 1 + ] + ] + }, + { + "filename": "services/report/report_processor.py", + "lines": [ + [ + 48, + 1 + ], + [ + 49, + 1 + ], + [ + 50, + 1 + ], + [ + 51, + 1 + ], + [ + 64, + 1 + ], + [ + 69, + 1 + ], + [ + 73, + 1 + ], + [ + 75, + 1 + ], + [ + 76, + 1 + ], + [ + 77, + 1 + ], + [ + 80, + 1 + ], + [ + 81, + 1 + ], + [ + 82, + 1 + ], + [ + 84, + 1 + ], + [ + 85, + 1 + ], + [ + 86, + 1 + ], + [ + 87, + 1 + ], + [ + 88, + 1 + ], + [ + 95, + 1 + ], + [ + 130, + 1 + ], + [ + 136, + 1 + ], + [ + 137, + 1 + ], + [ + 138, + 1 + ], + [ + 139, + 1 + ], + [ + 142, + 1 + ], + [ + 143, + 1 + ], + [ + 144, + 1 + ], + [ + 145, + 1 + ], + [ + 148, + 1 + ], + [ + 149, + 1 + ], + [ + 157, + 1 + ], + [ + 160, + 1 + ] + ] + }, + { + "filename": "services/report/languages/base.py", + "lines": [ + [ + 9, + 1 + ], + [ + 12, + 1 + ], + [ + 68, + 1 + ] + ] + }, + { + "filename": "services/report/languages/clover.py", + "lines": [ + [ + 12, + 1 + ] + ] + }, + { + "filename": "services/report/languages/cobertura.py", + "lines": [ + [ + 19, + 1 + ], + [ + 20, + 1 + ], + [ + 24, + 1 + ], + [ + 28, + 1 + ], + [ + 29, + 1 + ], + [ + 35, + 1 + ], + [ + 36, + 1 + ], + [ + 41, + 1 + ], + [ + 66, + 1 + ], + [ + 68, + 1 + ], + [ + 69, + 1 + ], + [ + 70, + 1 + ], + [ + 72, + 1 + ], + [ + 73, + 1 + ], + [ + 74, + 1 + ], + [ + 75, + 1 + ], + [ + 77, + 1 + ], + [ + 78, + 1 + ], + [ + 79, + 1 + ], + [ + 80, + 1 + ], + [ + 81, + 1 + ], + [ + 84, + 1 + ], + [ + 85, + 1 + ], + [ + 86, + 1 + ], + [ + 93, + 1 + ], + [ + 96, + 1 + ], + [ + 97, + 1 + ], + [ + 106, + 1 + ], + [ + 111, + 1 + ], + [ + 126, + 1 + ], + [ + 128, + 1 + ], + [ + 138, + 1 + ], + [ + 158, + 1 + ], + [ + 161, + 1 + ], + [ + 162, + 1 + ], + [ + 163, + 1 + ], + [ + 164, + 1 + ], + [ + 165, + 1 + ], + [ + 166, + 1 + ], + [ + 168, + 1 + ], + [ + 169, + 1 + ], + [ + 173, + 1 + ], + [ + 174, + 1 + ] + ] + }, + { + "filename": "services/report/languages/csharp.py", + "lines": [ + [ + 12, + 1 + ] + ] + }, + { + "filename": "services/report/languages/helpers.py", + "lines": [ + [ + 24, + 1 + ] + ] + }, + { + "filename": "services/report/languages/jacoco.py", + "lines": [ + [ + 14, + 1 + ] + ] + }, + { + "filename": "services/report/languages/jetbrainsxml.py", + "lines": [ + [ + 9, + 1 + ] + ] + }, + { + "filename": "services/report/languages/mono.py", + "lines": [ + [ + 9, + 1 + ] + ] + }, + { + "filename": "services/report/languages/scoverage.py", + "lines": [ + [ + 10, + 1 + ] + ] + }, + { + "filename": "services/report/languages/vb.py", + "lines": [ + [ + 9, + 1 + ] + ] + }, + { + "filename": "services/report/languages/vb2.py", + "lines": [ + [ + 9, + 1 + ] + ] + }, + { + "filename": "services/yaml/reader.py", + "lines": [ + [ + 16, + 1 + ], + [ + 17, + 1 + ], + [ + 18, + 1 + ], + [ + 19, + 1 + ], + [ + 20, + 1 + ], + [ + 23, + 1 + ], + [ + 24, + 1 + ], + [ + 25, + 1 + ] + ] + }, + { + "filename": "tasks/base.py", + "lines": [ + [ + 39, + 1 + ], + [ + 43, + 1 + ], + [ + 45, + 1 + ], + [ + 47, + 1 + ], + [ + 50, + 1 + ], + [ + 51, + 1 + ], + [ + 52, + 1 + ], + [ + 53, + 1 + ], + [ + 54, + 1 + ], + [ + 81, + 1 + ], + [ + 98, + 1 + ], + [ + 99, + 1 + ], + [ + 100, + 1 + ], + [ + 128, + 1 + ], + [ + 129, + 1 + ], + [ + 130, + 1 + ] + ] + }, + { + "filename": "tasks/upload_processor.py", + "lines": [ + [ + 69, + 1 + ], + [ + 70, + 1 + ], + [ + 71, + 1 + ], + [ + 72, + 1 + ], + [ + 73, + 1 + ], + [ + 74, + 1 + ], + [ + 79, + 1 + ], + [ + 80, + 1 + ], + [ + 112, + 1 + ], + [ + 113, + 1 + ], + [ + 114, + 1 + ], + [ + 115, + 1 + ], + [ + 116, + 1 + ], + [ + 117, + 1 + ], + [ + 120, + 1 + ], + [ + 121, + 1 + ], + [ + 122, + 1 + ], + [ + 123, + 1 + ], + [ + 124, + 1 + ], + [ + 125, + 1 + ], + [ + 126, + 1 + ], + [ + 128, + 1 + ], + [ + 129, + 1 + ], + [ + 130, + 1 + ], + [ + 132, + 1 + ], + [ + 133, + 1 + ], + [ + 134, + 1 + ], + [ + 135, + 1 + ], + [ + 145, + 1 + ], + [ + 146, + 1 + ], + [ + 147, + 1 + ], + [ + 148, + 1 + ], + [ + 149, + 1 + ], + [ + 150, + 1 + ], + [ + 155, + 1 + ], + [ + 158, + 1 + ], + [ + 166, + 1 + ], + [ + 181, + 1 + ], + [ + 182, + 1 + ], + [ + 183, + 1 + ], + [ + 184, + 1 + ], + [ + 185, + 1 + ], + [ + 190, + 1 + ], + [ + 191, + 1 + ], + [ + 194, + 1 + ], + [ + 204, + 1 + ], + [ + 220, + 1 + ], + [ + 223, + 1 + ], + [ + 236, + 1 + ], + [ + 239, + 1 + ], + [ + 251, + 1 + ], + [ + 252, + 1 + ], + [ + 253, + 1 + ], + [ + 261, + 1 + ], + [ + 264, + 1 + ], + [ + 266, + 1 + ], + [ + 280, + 1 + ], + [ + 281, + 1 + ], + [ + 282, + 1 + ], + [ + 283, + 1 + ], + [ + 284, + 1 + ], + [ + 301, + 1 + ], + [ + 311, + 1 + ], + [ + 312, + 1 + ], + [ + 313, + 1 + ] + ] + } + ] + }, + { + "grouping_attributes": [ + [ + "celery.state", + "SUCCESS" + ], + [ + "http.method", + null + ] + ], + "group": "run/app.tasks.upload.Upload", + "execs": [ + { + "filename": "helpers/logging_config.py", + "lines": [ + [ + 11, + 1 + ], + [ + 12, + 1 + ], + [ + 13, + 1 + ], + [ + 14, + 1 + ], + [ + 15, + 1 + ], + [ + 24, + 1 + ], + [ + 25, + 1 + ], + [ + 26, + 1 + ], + [ + 27, + 1 + ], + [ + 28, + 1 + ], + [ + 30, + 1 + ] + ] + }, + { + "filename": "services/redis.py", + "lines": [ + [ + 12, + 1 + ], + [ + 13, + 1 + ], + [ + 14, + 1 + ], + [ + 21, + 1 + ], + [ + 22, + 1 + ], + [ + 26, + 1 + ] + ] + }, + { + "filename": "tasks/base.py", + "lines": [ + [ + 39, + 1 + ], + [ + 43, + 1 + ], + [ + 45, + 1 + ], + [ + 47, + 1 + ], + [ + 50, + 1 + ], + [ + 51, + 1 + ], + [ + 52, + 1 + ], + [ + 53, + 1 + ], + [ + 54, + 1 + ], + [ + 81, + 1 + ], + [ + 98, + 1 + ], + [ + 99, + 1 + ], + [ + 100, + 1 + ], + [ + 128, + 1 + ], + [ + 129, + 1 + ], + [ + 130, + 1 + ] + ] + }, + { + "filename": "tasks/upload.py", + "lines": [ + [ + 99, + 1 + ], + [ + 102, + 1 + ], + [ + 103, + 1 + ], + [ + 105, + 1 + ], + [ + 133, + 1 + ], + [ + 134, + 1 + ], + [ + 136, + 1 + ], + [ + 139, + 1 + ], + [ + 140, + 1 + ], + [ + 141, + 1 + ], + [ + 142, + 1 + ], + [ + 143, + 1 + ], + [ + 158, + 1 + ], + [ + 159, + 1 + ], + [ + 164, + 1 + ], + [ + 199, + 1 + ], + [ + 202, + 1 + ], + [ + 203, + 1 + ] + ] + } + ] + } + ], + "files": { + "helpers/logging_config.py": { + "executable_lines": [ + 1, + 3, + 4, + 6, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 17, + 18, + 21, + 22, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 33, + 34, + 35, + 38, + 39, + 40, + 41, + 42, + 43, + 45, + 48, + 78, + 79, + 80, + 81, + 82 + ] + }, + "services/redis.py": { + "executable_lines": [ + 1, + 2, + 3, + 5, + 6, + 8, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 20, + 21, + 22, + 25, + 26, + 29, + 32, + 33, + 34, + 35, + 36, + 37, + 38 + ] + }, + "tasks/base.py": { + "executable_lines": [ + 1, + 2, + 4, + 5, + 6, + 7, + 14, + 15, + 16, + 18, + 21, + 22, + 23, + 24, + 26, + 27, + 28, + 29, + 30, + 31, + 34, + 35, + 37, + 38, + 39, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 60, + 61, + 62, + 63, + 64, + 68, + 69, + 74, + 78, + 79, + 81, + 83, + 98, + 99, + 100, + 101, + 102, + 106, + 107, + 108, + 109, + 110, + 114, + 115, + 116, + 120, + 122, + 123, + 124, + 125, + 127, + 128, + 129, + 130, + 132, + 136, + 137, + 138 + ] + }, + "tasks/upload.py": { + "executable_lines": [ + 1, + 2, + 3, + 4, + 5, + 7, + 8, + 9, + 10, + 11, + 16, + 17, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 30, + 31, + 32, + 33, + 34, + 36, + 38, + 39, + 41, + 44, + 96, + 98, + 99, + 102, + 103, + 104, + 105, + 107, + 122, + 125, + 126, + 127, + 128, + 129, + 130, + 132, + 133, + 134, + 135, + 136, + 138, + 139, + 140, + 141, + 142, + 143, + 147, + 157, + 158, + 159, + 164, + 167, + 168, + 173, + 174, + 178, + 183, + 184, + 188, + 194, + 196, + 199, + 202, + 203, + 208, + 209, + 210, + 211, + 214, + 215, + 216, + 220, + 224, + 225, + 226, + 229, + 230, + 231, + 232, + 233, + 234, + 235, + 236, + 239, + 240, + 241, + 245, + 246, + 250, + 251, + 256, + 257, + 261, + 266, + 267, + 268, + 269, + 270, + 274, + 275, + 276, + 277, + 280, + 283, + 284, + 285, + 286, + 287, + 289, + 297, + 299, + 300, + 301, + 302, + 305, + 306, + 307, + 316, + 317, + 318, + 323, + 324, + 330, + 331, + 332, + 333, + 334, + 335, + 336, + 345, + 346, + 347, + 354, + 355, + 356, + 367, + 368, + 376, + 378, + 379, + 380, + 381, + 388, + 389, + 390, + 391, + 392, + 393, + 394, + 395, + 396, + 397, + 401, + 403, + 406, + 407, + 408, + 409, + 410, + 414, + 415, + 416, + 417, + 421, + 422, + 426, + 428, + 439, + 440, + 441, + 442, + 443, + 444, + 445, + 448, + 454, + 455, + 456, + 459, + 460 + ] + }, + "database/base.py": { + "executable_lines": [ + 1, + 2, + 4, + 5, + 6, + 7, + 9, + 12, + 14, + 16, + 17, + 20, + 21, + 22, + 25, + 26, + 30, + 31, + 32 + ] + }, + "database/engine.py": { + "executable_lines": [ + 1, + 2, + 3, + 5, + 6, + 7, + 9, + 12, + 13, + 16, + 17, + 18, + 19, + 20, + 23, + 24, + 27, + 28, + 33, + 34, + 36 + ] + }, + "database/models/core.py": { + "executable_lines": [ + 1, + 2, + 3, + 5, + 6, + 7, + 8, + 10, + 11, + 19, + 20, + 21, + 22, + 23, + 25, + 26, + 27, + 28, + 33, + 34, + 35, + 36, + 39, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 61, + 62, + 70, + 75, + 76, + 77, + 79, + 80, + 83, + 85, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 103, + 104, + 105, + 106, + 107, + 109, + 110, + 112, + 117, + 118, + 119, + 121, + 122, + 123, + 125, + 126, + 129, + 131, + 133, + 134, + 135, + 136, + 137, + 138, + 139, + 140, + 141, + 142, + 143, + 144, + 145, + 146, + 147, + 148, + 149, + 151, + 152, + 153, + 159, + 167, + 168, + 170, + 171, + 172, + 179, + 181, + 183, + 184, + 185, + 186, + 187, + 188, + 190, + 192, + 194, + 195, + 198, + 200, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 209, + 212, + 214, + 216, + 217, + 218, + 219, + 222, + 223, + 224, + 225, + 226, + 227, + 228, + 229, + 230, + 231, + 233, + 234, + 236, + 238, + 239, + 241, + 242, + 249, + 250, + 257, + 258, + 259, + 260, + 266, + 269, + 271, + 273, + 274, + 275, + 279, + 282, + 283, + 284, + 290, + 292, + 301, + 302, + 305, + 306, + 308, + 309, + 310, + 311, + 312, + 313, + 319, + 331, + 332 + ] + }, + "database/models/reports.py": { + "executable_lines": [ + 1, + 2, + 4, + 5, + 6, + 8, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 21, + 22, + 23, + 24, + 30, + 37, + 44, + 49, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 69, + 70, + 71, + 72, + 73, + 80, + 81, + 83, + 84, + 85, + 88, + 89, + 90, + 91, + 92, + 93, + 96, + 97, + 98, + 99, + 102, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 115, + 116, + 118, + 119, + 120, + 121, + 122, + 123, + 124, + 126, + 127, + 130, + 131, + 132, + 133, + 136, + 137, + 138, + 139 + ] + }, + "helpers/cache.py": { + "executable_lines": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 13, + 15, + 17, + 20, + 29, + 30, + 31, + 34, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 47, + 58, + 67, + 69, + 70, + 73, + 83, + 84, + 86, + 87, + 90, + 92, + 94, + 95, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 110, + 111, + 112, + 113, + 114, + 115, + 118, + 154, + 155, + 157, + 158, + 160, + 161, + 163, + 172, + 175, + 176, + 177, + 178, + 180, + 181, + 182, + 183, + 185, + 186, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 199, + 201, + 202, + 203, + 204, + 205, + 207, + 208, + 209, + 210, + 211, + 212, + 213, + 214, + 215, + 216, + 217, + 218, + 219, + 221, + 224 + ] + }, + "helpers/pathmap/pathmap.py": { + "executable_lines": [ + 3, + 5, + 8, + 9, + 16, + 19, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 32, + 42, + 44, + 46, + 47, + 49, + 51, + 54 + ] + }, + "helpers/pathmap/tree.py": { + "executable_lines": [ + 1, + 2, + 3, + 5, + 8, + 9, + 10, + 13, + 16, + 18, + 25, + 26, + 27, + 28, + 29, + 30, + 32, + 41, + 46, + 48, + 50, + 57, + 59, + 60, + 62, + 63, + 65, + 66, + 68, + 70, + 83, + 85, + 86, + 88, + 89, + 90, + 91, + 92, + 96, + 97, + 98, + 99, + 100, + 102, + 110, + 111, + 112, + 114, + 115, + 117, + 118, + 120, + 121, + 122, + 123, + 125, + 127, + 129, + 135, + 136, + 137, + 138, + 140, + 141, + 142, + 143, + 144, + 146, + 147, + 149, + 156, + 157, + 158, + 160, + 161, + 162, + 164, + 165, + 167, + 174, + 175 + ] + }, + "services/archive.py": { + "executable_lines": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 10, + 11, + 13, + 14, + 16, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 27, + 28, + 33, + 40, + 45, + 50, + 55, + 57, + 58, + 59, + 61, + 62, + 63, + 64, + 66, + 67, + 74, + 75, + 82, + 83, + 84, + 85, + 86, + 97, + 98, + 105, + 106, + 114, + 115, + 128, + 131, + 141, + 143, + 145, + 146, + 149, + 150, + 152, + 153, + 154, + 161, + 162, + 164, + 165, + 166, + 173, + 174, + 176, + 177, + 178, + 184, + 185, + 191, + 192, + 196, + 197, + 203, + 204, + 205, + 206, + 209, + 215, + 216, + 222, + 223, + 224, + 225, + 226, + 232, + 233, + 237, + 243, + 244, + 246 + ] + }, + "services/bots.py": { + "executable_lines": [ + 1, + 2, + 4, + 5, + 7, + 8, + 9, + 10, + 11, + 13, + 16, + 17, + 18, + 21, + 22, + 25, + 26, + 27, + 31, + 32, + 35, + 36, + 37, + 38, + 39, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 55, + 63, + 64, + 65, + 69, + 70, + 71, + 79, + 80, + 81, + 85, + 86, + 89, + 90, + 91, + 92, + 93, + 96, + 97, + 98, + 102, + 103, + 104, + 105, + 106 + ] + }, + "services/repository.py": { + "executable_lines": [ + 1, + 2, + 3, + 4, + 5, + 7, + 8, + 9, + 14, + 15, + 17, + 18, + 19, + 21, + 23, + 26, + 29, + 33, + 34, + 35, + 56, + 59, + 60, + 63, + 66, + 67, + 68, + 69, + 70, + 71, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 95, + 96, + 97, + 98, + 107, + 108, + 109, + 110, + 114, + 117, + 122, + 123, + 124, + 126, + 127, + 132, + 133, + 134, + 135, + 136, + 145, + 155, + 156, + 161, + 162, + 165, + 166, + 168, + 171, + 172, + 173, + 175, + 176, + 177, + 180, + 181, + 182, + 184, + 185, + 186, + 187, + 188, + 189, + 192, + 203, + 206, + 209, + 210, + 211, + 213, + 214, + 215, + 222, + 223, + 224, + 225, + 226, + 230, + 231, + 232, + 235, + 240, + 243, + 293, + 306, + 307, + 309, + 311, + 314, + 315, + 316, + 317, + 320, + 323, + 324, + 325, + 326, + 327, + 330, + 331, + 336, + 337, + 338, + 341, + 342, + 343, + 344, + 345, + 346, + 349, + 352, + 353, + 354, + 359, + 360, + 365, + 366, + 367, + 368, + 369, + 375, + 376, + 377, + 378, + 379, + 380, + 381, + 382, + 386, + 389, + 392, + 393, + 394, + 395, + 399, + 400, + 401, + 402, + 406, + 407, + 408, + 409, + 427, + 428, + 429, + 430, + 431, + 434, + 435, + 437, + 438, + 444, + 445, + 447 + ] + }, + "services/storage.py": { + "executable_lines": [ + 1, + 3, + 4, + 6, + 8, + 11, + 12, + 15, + 17, + 18, + 19, + 20 + ] + }, + "services/path_fixer/__init__.py": { + "executable_lines": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 9, + 10, + 11, + 12, + 13, + 14, + 16, + 19, + 20, + 21, + 23, + 26, + 32, + 33, + 41, + 44, + 45, + 46, + 47, + 50, + 51, + 54, + 61, + 64, + 65, + 66, + 67, + 68, + 70, + 71, + 72, + 73, + 74, + 75, + 77, + 78, + 79, + 80, + 81, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 98, + 99, + 101, + 102, + 103, + 104, + 106, + 107, + 110, + 111, + 112, + 113, + 118, + 120, + 121, + 122, + 127, + 128, + 129, + 132, + 133, + 134, + 135, + 136, + 143, + 144, + 146, + 152, + 153, + 164, + 165 + ] + }, + "services/path_fixer/fixpaths.py": { + "executable_lines": [ + 1, + 2, + 3, + 4, + 6, + 8, + 57, + 68, + 69, + 70, + 71, + 72, + 75, + 76, + 78, + 79, + 81, + 82, + 85, + 86, + 88, + 91, + 98, + 99, + 101, + 102, + 105, + 107, + 109, + 110, + 115, + 116, + 119, + 121, + 124, + 127, + 128, + 129, + 132, + 135, + 136, + 139, + 141, + 142 + ] + }, + "services/path_fixer/user_path_fixes.py": { + "executable_lines": [ + 1, + 2, + 4, + 7, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 35, + 37, + 40, + 44, + 46, + 47, + 48, + 49, + 50, + 51, + 56, + 57 + ] + }, + "services/path_fixer/user_path_includes.py": { + "executable_lines": [ + 1, + 2, + 4, + 7, + 26, + 27, + 28, + 29, + 31, + 32, + 35, + 37, + 38, + 39, + 40, + 42, + 43, + 45, + 46, + 48, + 55, + 56, + 57, + 58, + 59, + 61, + 63, + 65, + 67, + 69, + 70, + 72, + 73, + 74 + ] + }, + "services/report/__init__.py": { + "executable_lines": [ + 1, + 2, + 3, + 4, + 5, + 6, + 8, + 9, + 10, + 11, + 12, + 13, + 15, + 16, + 23, + 24, + 25, + 26, + 27, + 30, + 31, + 32, + 33, + 34, + 36, + 37, + 40, + 41, + 42, + 43, + 44, + 46, + 48, + 49, + 55, + 61, + 64, + 65, + 68, + 70, + 79, + 80, + 82, + 91, + 93, + 113, + 114, + 117, + 120, + 121, + 122, + 123, + 128, + 129, + 132, + 133, + 134, + 135, + 138, + 139, + 140, + 144, + 147, + 148, + 149, + 150, + 151, + 153, + 154, + 156, + 171, + 172, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 195, + 205, + 206, + 207, + 208, + 209, + 214, + 215, + 218, + 219, + 220, + 221, + 222, + 223, + 225, + 228, + 229, + 230, + 231, + 232, + 233, + 236, + 240, + 241, + 243, + 244, + 246, + 249, + 250, + 251, + 252, + 253, + 254, + 255, + 256, + 260, + 261, + 262, + 263, + 264, + 265, + 266, + 269, + 271, + 272, + 273, + 274, + 275, + 277, + 280, + 283, + 284, + 285, + 286, + 287, + 292, + 293, + 297, + 305, + 306, + 317, + 318, + 319, + 320, + 328, + 329, + 330, + 340, + 341, + 343, + 344, + 345, + 349, + 350, + 351, + 352, + 353, + 354, + 355, + 359, + 360, + 361, + 362, + 370, + 371, + 372, + 373, + 374, + 375, + 376, + 377, + 378, + 381, + 392, + 399, + 414, + 415, + 416, + 417, + 418, + 419, + 420, + 421, + 422, + 424, + 425, + 435, + 436, + 437, + 438, + 441, + 442, + 451, + 452, + 453, + 454, + 457, + 470, + 471, + 472, + 477, + 482, + 483, + 488, + 494, + 497, + 498, + 499, + 500, + 501, + 502, + 503, + 504, + 515, + 516, + 517, + 519, + 520, + 521, + 526, + 527, + 529, + 530, + 531, + 532, + 533, + 534, + 535, + 536, + 537, + 538, + 539, + 540, + 546, + 547, + 548, + 558, + 559, + 560, + 561, + 562, + 563, + 564, + 565, + 566, + 575, + 577, + 589, + 590, + 591, + 592, + 609, + 610, + 611, + 612, + 613, + 614, + 615, + 616 + ] + }, + "services/report/parser.py": { + "executable_lines": [ + 1, + 2, + 3, + 5, + 8, + 9, + 10, + 11, + 12, + 14, + 15, + 16, + 18, + 19, + 22, + 23, + 30, + 31, + 32, + 33, + 35, + 36, + 38, + 39, + 41, + 42, + 44, + 45, + 46, + 49, + 51, + 52, + 53, + 55, + 61, + 62, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 81, + 82, + 83, + 85, + 87, + 88, + 98, + 99, + 100, + 101, + 102, + 103, + 109, + 111, + 112, + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 138, + 139, + 140, + 141, + 142, + 143, + 144, + 145, + 146, + 147, + 148, + 154, + 155, + 156, + 157, + 158, + 160, + 161, + 162, + 164, + 165, + 166, + 167, + 168, + 169, + 170, + 171, + 172, + 173, + 174, + 176, + 177, + 179, + 185 + ] + }, + "services/report/raw_upload_processor.py": { + "executable_lines": [ + 3, + 4, + 5, + 7, + 8, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 18, + 21, + 22, + 23, + 25, + 28, + 31, + 36, + 37, + 38, + 39, + 40, + 45, + 46, + 48, + 55, + 56, + 60, + 65, + 66, + 67, + 68, + 70, + 71, + 73, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 86, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 99, + 106, + 107, + 108, + 109, + 110, + 111, + 120, + 121, + 122, + 124, + 126, + 131, + 132, + 141 + ] + }, + "services/report/report_processor.py": { + "executable_lines": [ + 3, + 4, + 5, + 6, + 8, + 9, + 11, + 12, + 13, + 41, + 42, + 44, + 47, + 48, + 49, + 50, + 51, + 64, + 69, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 94, + 95, + 130, + 133, + 136, + 137, + 138, + 139, + 141, + 142, + 143, + 144, + 145, + 148, + 149, + 157, + 160, + 161, + 162, + 171, + 172, + 173, + 176, + 177, + 183 + ] + }, + "services/report/languages/base.py": { + "executable_lines": [ + 1, + 3, + 6, + 7, + 8, + 9, + 11, + 12, + 14, + 35, + 37, + 64, + 66, + 67, + 68 + ] + }, + "services/report/languages/clover.py": { + "executable_lines": [ + 1, + 2, + 3, + 5, + 6, + 7, + 10, + 11, + 12, + 14, + 17, + 20, + 26, + 27, + 28, + 29, + 30, + 31, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 45, + 46, + 47, + 49, + 50, + 51, + 54, + 59, + 61, + 62, + 64, + 67, + 70, + 71, + 72, + 73, + 76, + 77, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 88, + 90, + 91, + 92, + 93, + 97, + 98, + 101, + 108, + 109, + 110, + 111, + 112, + 114 + ] + }, + "services/report/languages/cobertura.py": { + "executable_lines": [ + 1, + 2, + 3, + 4, + 6, + 7, + 8, + 10, + 11, + 12, + 14, + 17, + 18, + 19, + 20, + 21, + 23, + 24, + 27, + 28, + 29, + 30, + 31, + 34, + 35, + 36, + 39, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 50, + 51, + 52, + 53, + 54, + 55, + 57, + 64, + 66, + 68, + 69, + 70, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 84, + 85, + 86, + 90, + 91, + 93, + 96, + 97, + 98, + 99, + 101, + 102, + 106, + 111, + 121, + 126, + 127, + 128, + 138, + 140, + 141, + 142, + 143, + 144, + 145, + 150, + 158, + 161, + 162, + 163, + 164, + 165, + 166, + 168, + 169, + 173, + 174 + ] + }, + "services/report/languages/csharp.py": { + "executable_lines": [ + 1, + 2, + 4, + 5, + 7, + 10, + 11, + 12, + 14, + 15, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 29, + 30, + 33, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 66, + 67, + 68, + 69, + 70, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 85, + 87, + 90, + 91, + 92, + 112, + 130, + 131, + 132, + 134 + ] + }, + "services/report/languages/helpers.py": { + "executable_lines": [ + 1, + 6, + 7, + 8, + 16, + 18, + 21, + 24 + ] + }, + "services/report/languages/jacoco.py": { + "executable_lines": [ + 1, + 3, + 4, + 5, + 7, + 8, + 9, + 12, + 13, + 14, + 16, + 17, + 20, + 28, + 29, + 30, + 31, + 35, + 37, + 38, + 40, + 42, + 43, + 45, + 46, + 48, + 49, + 50, + 53, + 54, + 55, + 58, + 60, + 61, + 63, + 65, + 66, + 67, + 68, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 81, + 82, + 83, + 84, + 85, + 87, + 89, + 91, + 92, + 93, + 94, + 95, + 97, + 98, + 99, + 102, + 103, + 105, + 106, + 108, + 116, + 118 + ] + }, + "services/report/languages/jetbrainsxml.py": { + "executable_lines": [ + 1, + 2, + 4, + 7, + 8, + 9, + 11, + 14, + 17, + 19, + 20, + 21, + 22, + 23, + 24, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 49, + 53, + 54, + 55, + 56, + 58 + ] + }, + "services/report/languages/mono.py": { + "executable_lines": [ + 1, + 2, + 4, + 7, + 8, + 9, + 11, + 14, + 17, + 18, + 21, + 23, + 24, + 25, + 28, + 29, + 30, + 33, + 34, + 35, + 37, + 42, + 44 + ] + }, + "services/report/languages/scoverage.py": { + "executable_lines": [ + 1, + 2, + 3, + 5, + 8, + 9, + 10, + 12, + 15, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 26, + 27, + 28, + 30, + 32, + 36, + 37, + 39, + 40, + 43, + 46, + 47, + 48, + 49, + 50, + 51, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 62, + 63, + 64, + 66, + 67, + 69, + 70, + 72 + ] + }, + "services/report/languages/vb.py": { + "executable_lines": [ + 1, + 2, + 4, + 7, + 8, + 9, + 11, + 12, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 37, + 38, + 43, + 44, + 46 + ] + }, + "services/report/languages/vb2.py": { + "executable_lines": [ + 1, + 2, + 4, + 7, + 8, + 9, + 11, + 12, + 15, + 16, + 17, + 18, + 19, + 20, + 24, + 25, + 26, + 28, + 29, + 30, + 33, + 35, + 36, + 37, + 38 + ] + }, + "services/yaml/reader.py": { + "executable_lines": [ + 1, + 2, + 3, + 5, + 7, + 15, + 16, + 17, + 18, + 19, + 20, + 22, + 23, + 24, + 25, + 28, + 29, + 30, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 43, + 44, + 45, + 46, + 47, + 50, + 52 + ] + }, + "tasks/upload_processor.py": { + "executable_lines": [ + 1, + 2, + 3, + 4, + 5, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 26, + 28, + 29, + 30, + 33, + 52, + 54, + 55, + 56, + 58, + 69, + 70, + 71, + 72, + 73, + 74, + 79, + 80, + 89, + 90, + 97, + 98, + 99, + 101, + 112, + 113, + 114, + 115, + 116, + 117, + 120, + 121, + 122, + 123, + 124, + 125, + 126, + 128, + 129, + 130, + 131, + 132, + 133, + 134, + 135, + 145, + 146, + 147, + 148, + 149, + 150, + 155, + 158, + 166, + 167, + 168, + 169, + 170, + 180, + 181, + 182, + 183, + 184, + 185, + 190, + 191, + 194, + 204, + 207, + 208, + 209, + 210, + 211, + 215, + 217, + 220, + 223, + 228, + 235, + 236, + 239, + 241, + 251, + 252, + 253, + 254, + 258, + 259, + 260, + 261, + 263, + 264, + 265, + 266, + 270, + 280, + 281, + 282, + 283, + 284, + 285, + 290, + 295, + 296, + 301, + 302, + 303, + 304, + 305, + 311, + 312, + 313, + 316, + 317 + ] + } + } +} \ No newline at end of file diff --git a/apps/worker/tasks/tests/unit/snapshots/cache_test_rollups__TestCacheTestRollupsTask__cache_test_rollups_use_timeseries__0.json b/apps/worker/tasks/tests/unit/snapshots/cache_test_rollups__TestCacheTestRollupsTask__cache_test_rollups_use_timeseries__0.json new file mode 100644 index 0000000000..bf6dba038d --- /dev/null +++ b/apps/worker/tasks/tests/unit/snapshots/cache_test_rollups__TestCacheTestRollupsTask__cache_test_rollups_use_timeseries__0.json @@ -0,0 +1,50 @@ +{ + "computed_name": [ + "computed_name", + "computed_name2" + ], + "flags": [ + [ + "test-rollups" + ], + [ + "test-rollups2" + ] + ], + "failing_commits": [ + 1, + 2 + ], + "last_duration": [ + 100.0, + 200.0 + ], + "avg_duration": [ + 100.0, + 200.0 + ], + "pass_count": [ + 0, + 0 + ], + "fail_count": [ + 1, + 2 + ], + "flaky_fail_count": [ + 0, + 0 + ], + "skip_count": [ + 0, + 0 + ], + "updated_at": [ + "2025-01-01T00:00:00+00:00", + "2025-01-01T00:00:00+00:00" + ], + "timestamp_bin": [ + "2024-12-31", + "2024-12-31" + ] +} diff --git a/apps/worker/tasks/tests/unit/snapshots/cache_test_rollups__TestCacheTestRollupsTask__cache_test_rollups_use_timeseries_branch__0.json b/apps/worker/tasks/tests/unit/snapshots/cache_test_rollups__TestCacheTestRollupsTask__cache_test_rollups_use_timeseries_branch__0.json new file mode 100644 index 0000000000..b69323b72d --- /dev/null +++ b/apps/worker/tasks/tests/unit/snapshots/cache_test_rollups__TestCacheTestRollupsTask__cache_test_rollups_use_timeseries_branch__0.json @@ -0,0 +1,37 @@ +{ + "computed_name": [ + "computed_name" + ], + "flags": [ + [ + "test-rollups" + ] + ], + "failing_commits": [ + 0 + ], + "last_duration": [ + 100.0 + ], + "avg_duration": [ + 100.0 + ], + "pass_count": [ + 1 + ], + "fail_count": [ + 0 + ], + "flaky_fail_count": [ + 0 + ], + "skip_count": [ + 0 + ], + "updated_at": [ + "2024-12-31T00:00:00+00:00" + ], + "timestamp_bin": [ + "2024-12-31" + ] +} diff --git a/apps/worker/tasks/tests/unit/snapshots/cache_test_rollups__TestCacheTestRollupsTask__cache_test_rollups_use_timeseries_main__0.json b/apps/worker/tasks/tests/unit/snapshots/cache_test_rollups__TestCacheTestRollupsTask__cache_test_rollups_use_timeseries_main__0.json new file mode 100644 index 0000000000..bf6dba038d --- /dev/null +++ b/apps/worker/tasks/tests/unit/snapshots/cache_test_rollups__TestCacheTestRollupsTask__cache_test_rollups_use_timeseries_main__0.json @@ -0,0 +1,50 @@ +{ + "computed_name": [ + "computed_name", + "computed_name2" + ], + "flags": [ + [ + "test-rollups" + ], + [ + "test-rollups2" + ] + ], + "failing_commits": [ + 1, + 2 + ], + "last_duration": [ + 100.0, + 200.0 + ], + "avg_duration": [ + 100.0, + 200.0 + ], + "pass_count": [ + 0, + 0 + ], + "fail_count": [ + 1, + 2 + ], + "flaky_fail_count": [ + 0, + 0 + ], + "skip_count": [ + 0, + 0 + ], + "updated_at": [ + "2025-01-01T00:00:00+00:00", + "2025-01-01T00:00:00+00:00" + ], + "timestamp_bin": [ + "2024-12-31", + "2024-12-31" + ] +} diff --git a/apps/worker/tasks/tests/unit/test_activate_account_user.py b/apps/worker/tasks/tests/unit/test_activate_account_user.py new file mode 100644 index 0000000000..0e77e2f809 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_activate_account_user.py @@ -0,0 +1,96 @@ +from typing import Any + +import pytest +from pytest import LogCaptureFixture +from pytest_mock import MockFixture +from shared.django_apps.codecov_auth.models import AccountsUsers +from shared.django_apps.codecov_auth.tests.factories import ( + AccountFactory, + OwnerFactory, + UserFactory, +) + +from tasks.activate_account_user import ActivateAccountUserTask + + +@pytest.fixture +def mock_db_session(mocker: MockFixture) -> Any: + return mocker.Mock() + + +@pytest.mark.django_db +def test_activate_account_user_skip_no_account( + caplog: LogCaptureFixture, mock_db_session: Any +) -> None: + user = OwnerFactory() + org = OwnerFactory() + org.account = None + ActivateAccountUserTask().run_impl( + mock_db_session, user_ownerid=user.ownerid, org_ownerid=org.ownerid + ) + assert len(caplog.records) == 2 + assert ( + caplog.records[1].message + == "Organization does not have an account. Skipping account user activation." + ) + + +@pytest.mark.parametrize( + "plan_seat_count,free_seat_count,is_user_student,expected_user_count", + [ + pytest.param(0, 0, False, 0, id="cannot_activate_no_seats_available"), + pytest.param(1, 0, False, 1, id="activate_with_seats_available"), + pytest.param(0, 1, False, 1, id="activate_with_free_seats_available"), + pytest.param(2, 0, True, 1, id="activate_github_student"), + pytest.param(2, 1, True, 1, id="activate_github_student"), + ], +) +@pytest.mark.django_db +def test_activate_account_user( + plan_seat_count: int, + free_seat_count: int, + is_user_student: bool, + expected_user_count: int, + mock_db_session: Any, +) -> None: + user = OwnerFactory() + user.student = is_user_student + user.user = UserFactory() + org = OwnerFactory() + account = AccountFactory( + plan_seat_count=plan_seat_count, free_seat_count=free_seat_count + ) + org.account = account + org.save() + assert AccountsUsers.objects.count() == 0 + + ActivateAccountUserTask().run_impl( + mock_db_session, user_ownerid=user.ownerid, org_ownerid=org.ownerid + ) + assert AccountsUsers.objects.count() == expected_user_count + if expected_user_count > 0: + assert AccountsUsers.objects.first().account == account + assert AccountsUsers.objects.first().user.owners.first() == user + + +@pytest.mark.django_db +def test_activate_account_user_already_exists(mock_db_session: Any) -> None: + user = OwnerFactory() + user.user = UserFactory() + org = OwnerFactory() + account = AccountFactory() + org.account = account + org.save() + + account.users.add(user.user) + account.save() + user.save() + + assert AccountsUsers.objects.filter(account=account, user=user.user).count() == 1 + + ActivateAccountUserTask().run_impl( + mock_db_session, user_ownerid=user.ownerid, org_ownerid=org.ownerid + ) + + # Nothing happens... user already exists. + assert AccountsUsers.objects.filter(account=account, user=user.user).count() == 1 diff --git a/apps/worker/tasks/tests/unit/test_backfill_existing_gh_app_installations.py b/apps/worker/tasks/tests/unit/test_backfill_existing_gh_app_installations.py new file mode 100644 index 0000000000..35f552edad --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_backfill_existing_gh_app_installations.py @@ -0,0 +1,191 @@ +from sqlalchemy.orm.session import Session + +from database.models.core import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, +) +from database.tests.factories.core import OwnerFactory, RepositoryFactory +from tasks.backfill_existing_gh_app_installations import ( + BackfillExistingIndividualGHAppInstallationTask, +) + + +def repo_obj(service_id, name, language, private, branch, using_integration): + return { + "owner": { + "service_id": "test-owner-service-id", + "username": "test-owner-username", + }, + "repo": { + "service_id": service_id, + "name": name, + "language": language, + "private": private, + "branch": branch, + }, + "_using_integration": using_integration, + } + + +class TestBackfillWithPreviousGHAppInstallation(object): + def test_gh_app_with_selection_all( + self, mocker, mock_repo_provider, dbsession: Session + ): + owner = OwnerFactory(service="github", integration_id=12345) + gh_app_installation = GithubAppInstallation( + owner=owner, + repository_service_ids=None, + installation_id=owner.integration_id, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + ) + + dbsession.add_all([owner, gh_app_installation]) + dbsession.commit() + + # Mock fn return values + mock_repo_provider.get_gh_app_installation.return_value = { + "repository_selection": "all" + } + mocker.patch( + "tasks.backfill_existing_gh_app_installations.get_owner_provider_service", + return_value=mock_repo_provider, + ) + + task = BackfillExistingIndividualGHAppInstallationTask() + assert task.run_impl( + dbsession, gh_app_installation_id=gh_app_installation.id + ) == { + "successful": True, + "reason": "backfill task finished", + } + + gh_app_installation = ( + dbsession.query(GithubAppInstallation) + .filter_by(ownerid=owner.ownerid) + .first() + ) + assert gh_app_installation.owner == owner + assert gh_app_installation.repository_service_ids is None + + def test_gh_app_with_specific_owner_ids( + self, mocker, mock_repo_provider, dbsession: Session + ): + owner = OwnerFactory(service="github", integration_id=123) + gh_app_installation = GithubAppInstallation( + owner=owner, + repository_service_ids=[237], + installation_id=owner.integration_id, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + ) + + owner_two = OwnerFactory(service="github", integration_id=456) + gh_app_installation_two = GithubAppInstallation( + owner=owner_two, + repository_service_ids=[748], + installation_id=owner_two.integration_id, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + ) + + dbsession.add_all( + [owner, gh_app_installation, owner_two, gh_app_installation_two] + ) + dbsession.commit() + + # Mock fn return values + mock_repo_provider.get_gh_app_installation.return_value = { + "repository_selection": "all" + } + mocker.patch( + "tasks.backfill_existing_gh_app_installations.get_owner_provider_service", + return_value=mock_repo_provider, + ) + + task = BackfillExistingIndividualGHAppInstallationTask() + assert task.run_impl( + dbsession, gh_app_installation_id=gh_app_installation.id + ) == { + "successful": True, + "reason": "backfill task finished", + } + + db_gh_app_installation_one = ( + dbsession.query(GithubAppInstallation) + .filter_by(ownerid=owner.ownerid) + .first() + ) + assert db_gh_app_installation_one.owner == owner + assert db_gh_app_installation_one.repository_service_ids is None + + # This one should have the same values as when it started + db_gh_app_installation_two = ( + dbsession.query(GithubAppInstallation) + .filter_by(ownerid=owner_two.ownerid) + .first() + ) + assert db_gh_app_installation_two.owner == owner_two + assert ( + db_gh_app_installation_two.repository_service_ids + == gh_app_installation_two.repository_service_ids + ) + + def test_gh_app_without_all_repo_selection( + self, mocker, mock_repo_provider, dbsession: Session + ): + owner = OwnerFactory(service="github", integration_id=12345) + gh_app_installation = GithubAppInstallation( + owner=owner, + repository_service_ids=None, + installation_id=owner.integration_id, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + ) + + # Create repos for mock endpoint and for DB + mock_repos = [ + repo_obj("159089634", "pytest", "python", False, "main", True), + repo_obj("164948070", "spack", "python", False, "develop", False), + repo_obj("213786132", "pub", "dart", False, "master", None), + repo_obj("555555555", "soda", "python", False, "main", None), + ] + for repo in mock_repos: + repo_data = repo["repo"] + dbsession.add( + RepositoryFactory( + owner=owner, + name=repo_data["name"], + service_id=repo_data["service_id"], + ) + ) + + dbsession.add_all([owner, gh_app_installation]) + dbsession.commit() + + # Mock fn return values + mock_repo_provider.get_gh_app_installation.return_value = { + "repository_selection": "selected" + } + mock_repo_provider.list_repos_using_installation.return_value = mock_repos + mocker.patch( + "tasks.backfill_existing_gh_app_installations.get_owner_provider_service", + return_value=mock_repo_provider, + ) + + task = BackfillExistingIndividualGHAppInstallationTask() + assert task.run_impl( + dbsession, gh_app_installation_id=gh_app_installation.id + ) == { + "successful": True, + "reason": "backfill task finished", + } + + gh_app_installation = ( + dbsession.query(GithubAppInstallation) + .filter_by(ownerid=owner.ownerid) + .first() + ) + assert gh_app_installation.owner == owner + assert len(gh_app_installation.repository_service_ids) == len(mock_repos) + + for repo in mock_repos: + assert ( + repo["repo"]["service_id"] in gh_app_installation.repository_service_ids + ) diff --git a/apps/worker/tasks/tests/unit/test_backfill_owners_without_gh_app_installations.py b/apps/worker/tasks/tests/unit/test_backfill_owners_without_gh_app_installations.py new file mode 100644 index 0000000000..d66c83f291 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_backfill_owners_without_gh_app_installations.py @@ -0,0 +1,124 @@ +from sqlalchemy.orm.session import Session + +from database.models.core import GithubAppInstallation +from database.tests.factories.core import OwnerFactory, RepositoryFactory +from tasks.backfill_owners_without_gh_app_installations import ( + BackfillOwnersWithoutGHAppInstallationIndividual, +) + + +def repo_obj(service_id, name, language, private, branch, using_integration): + return { + "owner": { + "service_id": "test-owner-service-id", + "username": "test-owner-username", + }, + "repo": { + "service_id": service_id, + "name": name, + "language": language, + "private": private, + "branch": branch, + }, + "_using_integration": using_integration, + } + + +class TestBackfillOwnersWithIntegrationWithoutGHApp(object): + # @patch("tasks.backfill_owners_without_gh_app_installations.yield_amount", 1) + def test_no_previous_app_existing_repos_only( + self, mocker, mock_repo_provider, dbsession: Session + ): + owner = OwnerFactory(service="github", integration_id=12345) + dbsession.add(owner) + + # Create repos for mock endpoint and for DB + mock_repos = [ + repo_obj("159089634", "pytest", "python", False, "main", True), + repo_obj("164948070", "spack", "python", False, "develop", False), + repo_obj("213786132", "pub", "dart", False, "master", None), + repo_obj("555555555", "soda", "python", False, "main", None), + ] + for repo in mock_repos: + repo_data = repo["repo"] + dbsession.add( + RepositoryFactory( + owner=owner, + name=repo_data["name"], + service_id=repo_data["service_id"], + ) + ) + + dbsession.commit() + + # Mock fn return values + mock_repo_provider.list_repos_using_installation.return_value = mock_repos + mocker.patch( + "tasks.backfill_owners_without_gh_app_installations.get_owner_provider_service", + return_value=mock_repo_provider, + ) + + task = BackfillOwnersWithoutGHAppInstallationIndividual() + assert task.run_impl(dbsession, ownerid=owner.ownerid) == { + "successful": True, + "reason": "backfill task finished", + } + + new_gh_app_installation = ( + dbsession.query(GithubAppInstallation) + .filter_by(ownerid=owner.ownerid) + .first() + ) + assert new_gh_app_installation.owner == owner + assert new_gh_app_installation.installation_id == owner.integration_id + assert len(new_gh_app_installation.repository_service_ids) == len(mock_repos) + + for repo in mock_repos: + assert ( + repo["repo"]["service_id"] + in new_gh_app_installation.repository_service_ids + ) + + def test_no_previous_app_some_existing_repos( + self, mocker, mock_repo_provider, dbsession: Session + ): + owner = OwnerFactory(service="github", integration_id=12345) + + # Only one of the two mock repos is stored in the DB + repo_name = "test-456" + repo_service_id = "164948070" + repo = RepositoryFactory( + owner=owner, name=repo_name, service_id=repo_service_id + ) + + mock_repos = [ + repo_obj("159089634", "pytest", "python", False, "main", True), + repo_obj(repo_service_id, repo_name, "python", False, "develop", False), + ] + + dbsession.add_all([owner, repo]) + dbsession.commit() + + # Mock fn return values + mock_repo_provider.list_repos_using_installation.return_value = mock_repos + mocker.patch( + "tasks.backfill_owners_without_gh_app_installations.get_owner_provider_service", + return_value=mock_repo_provider, + ) + + task = BackfillOwnersWithoutGHAppInstallationIndividual() + assert task.run_impl(dbsession, ownerid=owner.ownerid) == { + "successful": True, + "reason": "backfill task finished", + } + + new_gh_app_installation = ( + dbsession.query(GithubAppInstallation) + .filter_by(ownerid=owner.ownerid) + .first() + ) + assert new_gh_app_installation.owner == owner + assert new_gh_app_installation.installation_id == owner.integration_id + # Only added the service_id as long as the repository exists in our system as well + assert len(new_gh_app_installation.repository_service_ids) == 1 + assert new_gh_app_installation.repository_service_ids[0] == repo_service_id diff --git a/apps/worker/tasks/tests/unit/test_base.py b/apps/worker/tasks/tests/unit/test_base.py new file mode 100644 index 0000000000..9389444acf --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_base.py @@ -0,0 +1,695 @@ +from datetime import datetime +from pathlib import Path +from unittest.mock import patch + +import psycopg2 +import pytest +from celery import chain +from celery.contrib.testing.mocks import TaskMessage +from celery.exceptions import Retry, SoftTimeLimitExceeded +from mock import call +from prometheus_client import REGISTRY +from shared.celery_config import sync_repos_task_name, upload_task_name +from shared.plan.constants import PlanName +from sqlalchemy.exc import ( + DBAPIError, + IntegrityError, + InvalidRequestError, + StatementError, +) + +from database.enums import CommitErrorTypes +from database.models.core import GITHUB_APP_INSTALLATION_DEFAULT_NAME +from database.tests.factories.core import OwnerFactory, RepositoryFactory +from helpers.exceptions import NoConfiguredAppsAvailable, RepositoryWithoutValidBotError +from tasks.base import BaseCodecovRequest, BaseCodecovTask +from tasks.base import celery_app as base_celery_app +from tests.helpers import mock_all_plans_and_tiers + +here = Path(__file__) + + +class MockDateTime(datetime): + """ + `@pytest.mark.freeze_time()` is convenient but will freeze time for + everything, including timeseries metrics for which a timestamp is + a primary key. + + This class can be used to mock time more narrowly. + """ + + @classmethod + def now(cls): + return datetime.fromisoformat("2023-06-13T10:01:01.000123") + + +class SampleTask(BaseCodecovTask, name="test.SampleTask"): + def run_impl(self, dbsession): + return {"unusual": "return", "value": ["There"]} + + +class SampleTaskWithArbitraryError( + BaseCodecovTask, name="test.SampleTaskWithArbitraryError" +): + def __init__(self, error): + self.error = error + + def run_impl(self, dbsession): + raise self.error + + def retry(self, countdown=None): + # Fake retry method + raise Retry() + + +class SampleTaskWithArbitraryPostgresError( + BaseCodecovTask, name="test.SampleTaskWithArbitraryPostgresError" +): + def __init__(self, error): + self.error = error + + def run_impl(self, dbsession): + raise DBAPIError("statement", "params", self.error) + + def retry(self, countdown=None): + # Fake retry method + raise Retry() + + +class SampleTaskWithSoftTimeout(BaseCodecovTask, name="test.SampleTaskWithSoftTimeout"): + def run_impl(self, dbsession): + raise SoftTimeLimitExceeded() + + +class FailureSampleTask(BaseCodecovTask, name="test.FailureSampleTask"): + def run_impl(self, *args, **kwargs): + raise Exception("Whhhhyyyyyyy") + + +class RetrySampleTask(BaseCodecovTask, name="test.RetrySampleTask"): + def run(self, *args, **kwargs): + self.retry() + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +class TestBaseCodecovTask(object): + def test_hard_time_limit_task_with_request_data(self, mocker): + mocker.patch.object(SampleTask, "request", timelimit=[200, 123]) + r = SampleTask() + assert r.hard_time_limit_task == 200 + + def test_hard_time_limit_task_from_default_app(self, mocker): + mocker.patch.object(SampleTask, "request", timelimit=None) + r = SampleTask() + assert r.hard_time_limit_task == 480 + + @patch("tasks.base.datetime", MockDateTime) + def test_sample_run(self, mocker, dbsession): + mocked_get_db_session = mocker.patch("tasks.base.get_db_session") + mock_task_request = mocker.patch("tasks.base.BaseCodecovTask.request") + fake_request_values = dict( + created_timestamp="2023-06-13 10:00:00.000000", + delivery_info={"routing_key": "my-queue"}, + ) + mock_task_request.get.side_effect = ( + lambda key, default: fake_request_values.get(key, default) + ) + mocked_get_db_session.return_value = dbsession + task_instance = SampleTask() + result = task_instance.run() + assert result == {"unusual": "return", "value": ["There"]} + assert ( + REGISTRY.get_sample_value( + "worker_tasks_timers_time_in_queue_seconds_sum", + labels={"task": SampleTask.name, "queue": "my-queue"}, + ) + == 61.000123 + ) + + @patch("tasks.base.BaseCodecovTask._emit_queue_metrics") + def test_sample_run_db_exception(self, mocker, dbsession): + mocked_get_db_session = mocker.patch("tasks.base.get_db_session") + mocked_get_db_session.return_value = dbsession + with pytest.raises(Retry): + SampleTaskWithArbitraryError( + DBAPIError("statement", "params", "orig") + ).run() + + @patch("tasks.base.BaseCodecovTask._emit_queue_metrics") + def test_sample_run_integrity_error(self, mocker, dbsession): + mocked_get_db_session = mocker.patch("tasks.base.get_db_session") + mocked_get_db_session.return_value = dbsession + with pytest.raises(Retry): + SampleTaskWithArbitraryError( + IntegrityError("statement", "params", "orig") + ).run() + + @patch("tasks.base.BaseCodecovTask._emit_queue_metrics") + def test_sample_run_deadlock_exception(self, mocker, dbsession): + mocked_get_db_session = mocker.patch("tasks.base.get_db_session") + mocked_get_db_session.return_value = dbsession + with pytest.raises(Retry): + SampleTaskWithArbitraryPostgresError( + psycopg2.errors.DeadlockDetected() + ).run() + + @patch("tasks.base.BaseCodecovTask._emit_queue_metrics") + def test_sample_run_operationalerror_exception(self, mocker, dbsession): + mocked_get_db_session = mocker.patch("tasks.base.get_db_session") + mocked_get_db_session.return_value = dbsession + with pytest.raises(Retry): + SampleTaskWithArbitraryPostgresError(psycopg2.OperationalError()).run() + + @patch("tasks.base.BaseCodecovTask._emit_queue_metrics") + def test_sample_run_softimeout(self, mocker, dbsession): + mocked_get_db_session = mocker.patch("tasks.base.get_db_session") + mocked_get_db_session.return_value = dbsession + with pytest.raises(SoftTimeLimitExceeded): + SampleTaskWithSoftTimeout().run() + + def test_wrap_up_dbsession_success(self, mocker): + task = BaseCodecovTask() + fake_session = mocker.MagicMock() + task.wrap_up_dbsession(fake_session) + assert fake_session.commit.call_count == 1 + assert fake_session.close.call_count == 1 + + def test_wrap_up_dbsession_timeout_but_ok(self, mocker): + task = BaseCodecovTask() + fake_session = mocker.MagicMock( + commit=mocker.MagicMock(side_effect=[SoftTimeLimitExceeded(), 1]) + ) + task.wrap_up_dbsession(fake_session) + assert fake_session.commit.call_count == 2 + assert fake_session.close.call_count == 1 + + def test_wrap_up_dbsession_timeout_nothing_works(self, mocker): + mocked_get_db_session = mocker.patch("tasks.base.get_db_session") + task = BaseCodecovTask() + fake_session = mocker.MagicMock( + commit=mocker.MagicMock( + side_effect=[SoftTimeLimitExceeded(), InvalidRequestError()] + ) + ) + task.wrap_up_dbsession(fake_session) + assert fake_session.commit.call_count == 2 + assert fake_session.close.call_count == 0 + assert mocked_get_db_session.remove.call_count == 1 + + def test_wrap_up_dbsession_invalid_nothing_works(self, mocker): + mocked_get_db_session = mocker.patch("tasks.base.get_db_session") + task = BaseCodecovTask() + fake_session = mocker.MagicMock( + commit=mocker.MagicMock(side_effect=[InvalidRequestError()]) + ) + task.wrap_up_dbsession(fake_session) + assert fake_session.commit.call_count == 1 + assert fake_session.close.call_count == 0 + assert mocked_get_db_session.remove.call_count == 1 + + def test_run_success_commits_sqlalchemy(self, mocker, dbsession): + mock_wrap_up = mocker.patch("tasks.base.BaseCodecovTask.wrap_up_dbsession") + mock_dbsession_rollback = mocker.patch.object(dbsession, "rollback") + mock_get_db_session = mocker.patch( + "tasks.base.get_db_session", return_value=dbsession + ) + + task = SampleTask() + task.run() + + assert mock_wrap_up.call_args_list == [call(dbsession)] + + assert mock_dbsession_rollback.call_count == 0 + + def test_run_db_errors_rollback(self, mocker, dbsession, celery_app): + mock_dbsession_rollback = mocker.patch.object(dbsession, "rollback") + mock_wrap_up = mocker.patch("tasks.base.BaseCodecovTask.wrap_up_dbsession") + mock_get_db_session = mocker.patch( + "tasks.base.get_db_session", return_value=dbsession + ) + + # IntegrityError and DataError are subclasses of SQLAlchemyError that + # have their own `except` clause. + task = SampleTaskWithArbitraryError(IntegrityError("", {}, None)) + registered_task = celery_app.register_task(task) + task = celery_app.tasks[registered_task.name] + task.apply() + + assert mock_dbsession_rollback.call_args_list == [call()] + + assert mock_wrap_up.call_args_list == [call(dbsession)] + + def test_run_sqlalchemy_error_rollback(self, mocker, dbsession, celery_app): + mock_dbsession_rollback = mocker.patch.object(dbsession, "rollback") + mock_wrap_up = mocker.patch("tasks.base.BaseCodecovTask.wrap_up_dbsession") + mock_get_db_session = mocker.patch( + "tasks.base.get_db_session", return_value=dbsession + ) + + # StatementError is a subclass of SQLAlchemyError just like + # IntegrityError and DataError, but this test case is different because + # it is caught by a different except clause. + task = SampleTaskWithArbitraryError(StatementError("", "", None, None)) + registered_task = celery_app.register_task(task) + task = celery_app.tasks[registered_task.name] + task.apply() + + assert mock_dbsession_rollback.call_args_list == [call()] + + assert mock_wrap_up.call_args_list == [call(dbsession)] + + def test_get_repo_provider_service_working(self, mocker): + mock_repo_provider = mocker.MagicMock() + mock_get_repo_provider_service = mocker.patch( + "tasks.base.get_repo_provider_service", return_value=mock_repo_provider + ) + + task = BaseCodecovTask() + mock_repo = mocker.MagicMock() + assert task.get_repo_provider_service(mock_repo) == mock_repo_provider + mock_get_repo_provider_service.assert_called_with( + mock_repo, GITHUB_APP_INSTALLATION_DEFAULT_NAME, None + ) + + def test_get_repo_provider_service_rate_limited(self, mocker): + mocker.patch( + "tasks.base.get_repo_provider_service", + side_effect=NoConfiguredAppsAvailable( + apps_count=2, + rate_limited_count=2, + suspended_count=0, + ), + ) + mocker.patch("tasks.base.get_seconds_to_next_hour", return_value=120) + + task = BaseCodecovTask() + mock_retry = mocker.patch.object(task, "retry") + mock_repo = mocker.MagicMock() + assert task.get_repo_provider_service(mock_repo) is None + task.retry.assert_called_with(countdown=120) + + def test_get_repo_provider_service_suspended(self, mocker): + mocker.patch( + "tasks.base.get_repo_provider_service", + side_effect=NoConfiguredAppsAvailable( + apps_count=2, + rate_limited_count=0, + suspended_count=2, + ), + ) + mocker.patch("tasks.base.get_seconds_to_next_hour", return_value=120) + + task = BaseCodecovTask() + mock_repo = mocker.MagicMock() + assert task.get_repo_provider_service(mock_repo) is None + + def test_get_repo_provider_service_no_valid_bot(self, mocker): + mocker.patch( + "tasks.base.get_repo_provider_service", + side_effect=RepositoryWithoutValidBotError(), + ) + mock_save_commit_error = mocker.patch("tasks.base.save_commit_error") + + task = BaseCodecovTask() + mock_repo = mocker.MagicMock() + mock_repo.repoid = 5 + mock_commit = mocker.MagicMock() + assert task.get_repo_provider_service(mock_repo, commit=mock_commit) is None + mock_save_commit_error.assert_called_with( + mock_commit, + error_code=CommitErrorTypes.REPO_BOT_INVALID.value, + error_params=dict(repoid=5), + ) + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +class TestBaseCodecovTaskHooks(object): + def test_sample_task_success(self, celery_app): + class SampleTask(BaseCodecovTask, name="test.SampleTask"): + def run_impl(self, dbsession): + return {"unusual": "return", "value": ["There"]} + + DTask = celery_app.register_task(SampleTask()) + task = celery_app.tasks[DTask.name] + + prom_run_counter_before = REGISTRY.get_sample_value( + "worker_task_counts_runs_total", labels={"task": DTask.name} + ) + prom_success_counter_before = REGISTRY.get_sample_value( + "worker_task_counts_successes_total", labels={"task": DTask.name} + ) + k = task.apply() + prom_run_counter_after = REGISTRY.get_sample_value( + "worker_task_counts_runs_total", labels={"task": DTask.name} + ) + prom_success_counter_after = REGISTRY.get_sample_value( + "worker_task_counts_successes_total", labels={"task": DTask.name} + ) + + res = k.get() + assert res == {"unusual": "return", "value": ["There"]} + assert prom_run_counter_after - prom_run_counter_before == 1 + assert prom_success_counter_after - prom_success_counter_before == 1 + + def test_sample_task_failure(self, celery_app): + class FailureSampleTask(BaseCodecovTask, name="test.FailureSampleTask"): + def run_impl(self, *args, **kwargs): + raise Exception("Whhhhyyyyyyy") + + DTask = celery_app.register_task(FailureSampleTask()) + task = celery_app.tasks[DTask.name] + with pytest.raises(Exception) as exc: + prom_run_counter_before = REGISTRY.get_sample_value( + "worker_task_counts_runs_total", labels={"task": DTask.name} + ) + prom_failure_counter_before = REGISTRY.get_sample_value( + "worker_task_counts_failures_total", labels={"task": DTask.name} + ) + task.apply().get() + prom_run_counter_after = REGISTRY.get_sample_value( + "worker_task_counts_runs_total", labels={"task": DTask.name} + ) + prom_failure_counter_after = REGISTRY.get_sample_value( + "worker_task_counts_failures_total", labels={"task": DTask.name} + ) + assert prom_run_counter_after - prom_run_counter_before == 1 + assert prom_failure_counter_after - prom_failure_counter_before == 1 + assert exc.value.args == ("Whhhhyyyyyyy",) + + def test_sample_task_retry(self): + # Unfortunately we cant really call the task with apply().get() + # Something happens inside celery as of version 4.3 that makes them + # not call on_Retry at all. + # best we can do is to call on_retry ourselves and ensure this makes the + # metric be called + task = RetrySampleTask() + prom_retry_counter_before = REGISTRY.get_sample_value( + "worker_task_counts_retries_total", labels={"task": task.name} + ) + task.on_retry("exc", "task_id", ("args",), {"kwargs": "foo"}, "einfo") + prom_retry_counter_after = REGISTRY.get_sample_value( + "worker_task_counts_retries_total", labels={"task": task.name} + ) + assert prom_retry_counter_after - prom_retry_counter_before == 1 + + +class TestBaseCodecovRequest(object): + """ + All in all, this is a really weird class + + We are trying here to test some of the hooks celery providers for requests + + It's not easy to generate a situation where they can be called without intensely + faking the situation + + If you every find a better way to test this, delete this class + + If things start going badly because of those tests, delete this class + """ + + def xRequest(self, mocker, name, celery_app): + # I dont even know what I am doing here. Just trying to create a sample request Copied from + # https://github.com/celery/celery/blob/4e4d308db88e60afeec97479a5a133671c671fce/t/unit/worker/test_request.py#L54 + id = None + args = [1] + kwargs = {"f": "x"} + on_ack = mocker.Mock(name="on_ack") + on_reject = mocker.Mock(name="on_reject") + message = TaskMessage(name, id, args=args, kwargs=kwargs) + return BaseCodecovRequest( + message, app=celery_app, on_ack=on_ack, on_reject=on_reject + ) + + def test_sample_task_timeout(self, celery_app, mocker): + class SampleTask(BaseCodecovTask, name="test.SampleTask"): + pass + + DTask = celery_app.register_task(SampleTask()) + request = self.xRequest(mocker, DTask.name, celery_app) + prom_timeout_counter_before = ( + REGISTRY.get_sample_value( + "worker_task_counts_timeouts_total", labels={"task": DTask.name} + ) + or 0 + ) + request.on_timeout(True, 10) + prom_timeout_counter_after = REGISTRY.get_sample_value( + "worker_task_counts_timeouts_total", labels={"task": DTask.name} + ) + assert prom_timeout_counter_after - prom_timeout_counter_before == 1 + + def test_sample_task_hard_timeout(self, celery_app, mocker): + class SampleTask(BaseCodecovTask, name="test.SampleTask"): + pass + + DTask = celery_app.register_task(SampleTask()) + request = self.xRequest(mocker, DTask.name, celery_app) + prom_timeout_counter_before = ( + REGISTRY.get_sample_value( + "worker_task_counts_timeouts_total", labels={"task": DTask.name} + ) + or 0 + ) + prom_hard_timeout_counter_before = ( + REGISTRY.get_sample_value( + "worker_task_counts_hard_timeouts_total", labels={"task": DTask.name} + ) + or 0 + ) + request.on_timeout(False, 10) + prom_timeout_counter_after = REGISTRY.get_sample_value( + "worker_task_counts_timeouts_total", labels={"task": DTask.name} + ) + prom_hard_timeout_counter_after = REGISTRY.get_sample_value( + "worker_task_counts_hard_timeouts_total", labels={"task": DTask.name} + ) + assert prom_timeout_counter_after - prom_timeout_counter_before == 1 + assert prom_hard_timeout_counter_after - prom_hard_timeout_counter_before == 1 + + +class TestBaseCodecovTaskApplyAsyncOverride(object): + @pytest.fixture + def fake_owners(self, dbsession): + owner = OwnerFactory.create(plan=PlanName.CODECOV_PRO_MONTHLY.value) + owner_enterprise_cloud = OwnerFactory.create( + plan=PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + dbsession.add(owner) + dbsession.add(owner_enterprise_cloud) + dbsession.flush() + return (owner, owner_enterprise_cloud) + + @pytest.fixture + def fake_repos(self, dbsession, fake_owners): + (owner, owner_enterprise_cloud) = fake_owners + repo = RepositoryFactory.create(owner=owner) + repo_enterprise_cloud = RepositoryFactory.create(owner=owner_enterprise_cloud) + dbsession.add(repo) + dbsession.add(repo_enterprise_cloud) + dbsession.flush() + return (repo, repo_enterprise_cloud) + + @pytest.mark.freeze_time("2023-06-13T10:01:01.000123") + def test_apply_async_override(self, mocker): + mock_get_db_session = mocker.patch("tasks.base.get_db_session") + mock_celery_task_router = mocker.patch("tasks.base._get_user_plan_from_task") + mock_route_tasks = mocker.patch( + "tasks.base.route_tasks_based_on_user_plan", + return_value=dict( + queue="some_queue", + extra_config=dict(soft_timelimit=200, hard_timelimit=400), + ), + ) + + task = BaseCodecovTask() + task.name = "app.tasks.upload.FakeTask" + mocked_apply_async = mocker.patch.object(base_celery_app.Task, "apply_async") + + kwargs = dict(n=10) + task.apply_async(kwargs=kwargs) + assert mock_get_db_session.call_count == 1 + assert mock_celery_task_router.call_count == 1 + assert mock_route_tasks.call_count == 1 + mocked_apply_async.assert_called_with( + args=None, + kwargs=kwargs, + headers=dict(created_timestamp="2023-06-13T10:01:01.000123"), + time_limit=400, + soft_time_limit=200, + user_plan=mock_celery_task_router(), + ) + + @pytest.mark.freeze_time("2023-06-13T10:01:01.000123") + def test_apply_async_override_with_chain(self, mocker): + mock_get_db_session = mocker.patch("tasks.base.get_db_session") + mock_celery_task_router = mocker.patch("tasks.base._get_user_plan_from_task") + mock_route_tasks = mocker.patch( + "tasks.base.route_tasks_based_on_user_plan", + return_value=dict( + queue="some_queue", + extra_config=dict(soft_timelimit=200, hard_timelimit=400), + ), + ) + + task = BaseCodecovTask() + task.name = "app.tasks.upload.FakeTask" + mocked_apply_async = mocker.patch.object(base_celery_app.Task, "apply_async") + + chain( + [task.signature(kwargs=dict(n=1)), task.signature(kwargs=dict(n=10))] + ).apply_async() + assert mock_get_db_session.call_count == 1 + assert mock_celery_task_router.call_count == 1 + assert mock_route_tasks.call_count == 1 + assert mocked_apply_async.call_count == 1 + _, kwargs = mocked_apply_async.call_args + assert "soft_time_limit" in kwargs and kwargs.get("soft_time_limit") == 200 + assert "time_limit" in kwargs and kwargs.get("time_limit") == 400 + assert "kwargs" in kwargs and kwargs.get("kwargs") == {"n": 1} + assert "chain" in kwargs and len(kwargs.get("chain")) == 1 + assert "task_id" in kwargs + assert "headers" in kwargs + assert kwargs.get("headers") == dict( + created_timestamp="2023-06-13T10:01:01.000123" + ) + + @pytest.mark.freeze_time("2023-06-13T10:01:01.000123") + @pytest.mark.django_db(databases={"default"}) + def test_real_example_no_override( + self, mocker, dbsession, mock_configuration, fake_repos + ): + mock_all_plans_and_tiers() + mock_configuration.set_params( + { + "setup": { + "tasks": { + "celery": { + "enterprise": { + "soft_timelimit": 500, + "hard_timelimit": 600, + }, + }, + "upload": { + "enterprise": {"soft_timelimit": 400, "hard_timelimit": 450} + }, + } + } + } + ) + mock_get_db_session = mocker.patch( + "tasks.base.get_db_session", return_value=dbsession + ) + task = BaseCodecovTask() + mocker.patch.object(task, "run", return_value="success") + task.name = sync_repos_task_name + + mocked_super_apply_async = mocker.patch.object( + base_celery_app.Task, "apply_async" + ) + repo, _ = fake_repos + + kwargs = dict(ownerid=repo.ownerid) + task.apply_async(kwargs=kwargs) + assert mock_get_db_session.call_count == 1 + mocked_super_apply_async.assert_called_with( + args=None, + kwargs=kwargs, + soft_time_limit=None, + headers=dict(created_timestamp="2023-06-13T10:01:01.000123"), + time_limit=None, + user_plan="users-pr-inappm", + ) + + @pytest.mark.freeze_time("2023-06-13T10:01:01.000123") + @pytest.mark.django_db(databases={"default"}) + def test_real_example_override_from_celery( + self, mocker, dbsession, mock_configuration, fake_repos + ): + mock_all_plans_and_tiers() + mock_configuration.set_params( + { + "setup": { + "tasks": { + "celery": { + "enterprise": { + "soft_timelimit": 500, + "hard_timelimit": 600, + }, + }, + "upload": { + "enterprise": {"soft_timelimit": 400, "hard_timelimit": 450} + }, + } + } + } + ) + mock_get_db_session = mocker.patch( + "tasks.base.get_db_session", return_value=dbsession + ) + task = BaseCodecovTask() + mocker.patch.object(task, "run", return_value="success") + task.name = sync_repos_task_name + + mocked_super_apply_async = mocker.patch.object( + base_celery_app.Task, "apply_async" + ) + _, repo_enterprise_cloud = fake_repos + + kwargs = dict(ownerid=repo_enterprise_cloud.ownerid) + task.apply_async(kwargs=kwargs) + assert mock_get_db_session.call_count == 1 + mocked_super_apply_async.assert_called_with( + args=None, + kwargs=kwargs, + soft_time_limit=500, + headers=dict(created_timestamp="2023-06-13T10:01:01.000123"), + time_limit=600, + user_plan="users-enterprisey", + ) + + @pytest.mark.freeze_time("2023-06-13T10:01:01.000123") + @pytest.mark.django_db(databases={"default"}) + def test_real_example_override_from_upload( + self, mocker, dbsession, mock_configuration, fake_repos + ): + mock_all_plans_and_tiers() + mock_configuration.set_params( + { + "setup": { + "tasks": { + "celery": { + "enterprise": { + "soft_timelimit": 500, + "hard_timelimit": 600, + }, + }, + "upload": { + "enterprise": {"soft_timelimit": 400, "hard_timelimit": 450} + }, + } + } + } + ) + mock_get_db_session = mocker.patch( + "tasks.base.get_db_session", return_value=dbsession + ) + task = BaseCodecovTask() + mocker.patch.object(task, "run", return_value="success") + task.name = upload_task_name + + mocked_super_apply_async = mocker.patch.object( + base_celery_app.Task, "apply_async" + ) + _, repo_enterprise_cloud = fake_repos + + kwargs = dict(repoid=repo_enterprise_cloud.repoid) + task.apply_async(kwargs=kwargs) + assert mock_get_db_session.call_count == 1 + mocked_super_apply_async.assert_called_with( + args=None, + kwargs=kwargs, + soft_time_limit=400, + headers=dict(created_timestamp="2023-06-13T10:01:01.000123"), + time_limit=450, + user_plan="users-enterprisey", + ) diff --git a/apps/worker/tasks/tests/unit/test_brolly_stats_rollup.py b/apps/worker/tasks/tests/unit/test_brolly_stats_rollup.py new file mode 100644 index 0000000000..65293e7ba7 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_brolly_stats_rollup.py @@ -0,0 +1,182 @@ +import asyncio +import datetime +import uuid + +import httpx +import pytest +import respx +from mock import Mock, patch + +from database.models import Constants +from database.tests.factories import ( + CommitFactory, + ConstantsFactory, + ReportFactory, + RepositoryFactory, + UploadFactory, + UserFactory, +) +from tasks.brolly_stats_rollup import DEFAULT_BROLLY_ENDPOINT, BrollyStatsRollupTask + + +@pytest.fixture +def version(dbsession) -> str: + version = dbsession.query(Constants).filter_by(key="version").scalar() + if version is None: + version = ConstantsFactory.create(key="version", value="hello") + dbsession.add(version) + dbsession.flush() + return version + + +@pytest.fixture +def install_id(dbsession) -> int: + install_id = dbsession.query(Constants).filter_by(key="install_id").scalar() + if install_id is None: + install_id = ConstantsFactory.create(key="install_id", value=str(uuid.uuid4())) + dbsession.add(install_id) + dbsession.flush() + return install_id + + +def _get_n_hours_ago(n): + return datetime.datetime.now() - datetime.timedelta(hours=n) + + +def _mock_response(): + f = asyncio.Future() + f.set_result = Mock(status_code=200) + return f + + +class TestBrollyStatsRollupTask(object): + def test_get_min_seconds_interval_between_executions(self, dbsession): + assert isinstance( + BrollyStatsRollupTask.get_min_seconds_interval_between_executions(), + int, + ) + assert ( + BrollyStatsRollupTask.get_min_seconds_interval_between_executions() == 72000 + ) + + @patch("tasks.brolly_stats_rollup.get_config", return_value=False) + def test_run_cron_task_while_disabled(self, dbsession): + result = BrollyStatsRollupTask().run_cron_task(dbsession) + assert result == { + "uploaded": False, + "reason": "telemetry disabled in codecov.yml", + } + + @respx.mock + def test_run_cron_task_http_ok(self, dbsession, install_id, version): + users = [UserFactory.create(name=name) for name in ("foo", "bar", "baz")] + for user in users: + dbsession.add(user) + + repos = [ + RepositoryFactory.create( + name=name, + ) + for name in ("abc", "def", "ghi", "jkl") + ] + for repo in repos: + dbsession.add(repo) + + commits = [ + CommitFactory.create( + message="", + commitid=commitid, + repository=repos[0], + ) + for commitid in ("deadbeef", "cafebabe", "eggdad") + ] + for commit in commits: + dbsession.add(commit) + + report = ReportFactory.create(commit=commits[0]) + uploads = [ + UploadFactory.create(created_at=created_at, report=report) + for created_at in ( + _get_n_hours_ago(5), + _get_n_hours_ago(16), + _get_n_hours_ago(30), + ) + ] + for upload in uploads: + dbsession.add(upload) + + dbsession.flush() + + install_id_val = dbsession.query(Constants).get("install_id").value + version_val = dbsession.query(Constants).get("version").value + + mock_request = respx.post(DEFAULT_BROLLY_ENDPOINT).mock( + return_value=httpx.Response(200) + ) + + task = BrollyStatsRollupTask() + result = task.run_cron_task(dbsession) + + assert mock_request.called + assert result == { + "uploaded": True, + "payload": { + "install_id": install_id.value, + "users": 3, + "repos": 4, + "commits": 3, + "uploads_24h": 2, + "anonymous": True, + "version": version.value, + }, + } + + @respx.mock + def test_run_cron_task_not_ok(self, dbsession, install_id, version): + mock_request = respx.post(DEFAULT_BROLLY_ENDPOINT).mock( + return_value=httpx.Response(500) + ) + task = BrollyStatsRollupTask() + result = task.run_cron_task(dbsession) + assert mock_request.called + assert result == { + "uploaded": False, + "payload": { + "install_id": install_id.value, + "users": 0, + "repos": 0, + "commits": 0, + "uploads_24h": 0, + "anonymous": True, + "version": version.value, + }, + } + + @respx.mock + def test_run_cron_task_include_admin_email_if_populated( + self, mocker, dbsession, install_id, version + ): + mock_request = respx.post(DEFAULT_BROLLY_ENDPOINT).mock( + return_value=httpx.Response(200) + ) + + mocker.patch.object( + BrollyStatsRollupTask, "_get_admin_email", return_value="hello" + ) + + task = BrollyStatsRollupTask() + result = task.run_cron_task(dbsession) + assert mock_request.called + assert result == { + "uploaded": True, + "payload": { + "install_id": install_id.value, + "users": 0, + "repos": 0, + "commits": 0, + "uploads_24h": 0, + "anonymous": True, + "version": version.value, + "admin_email": "hello", + }, + } diff --git a/apps/worker/tasks/tests/unit/test_bundle_analysis_notify_task.py b/apps/worker/tasks/tests/unit/test_bundle_analysis_notify_task.py new file mode 100644 index 0000000000..be86afdc18 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_bundle_analysis_notify_task.py @@ -0,0 +1,58 @@ +from database.tests.factories import CommitFactory +from services.bundle_analysis.notify import BundleAnalysisNotifyReturn +from services.bundle_analysis.notify.types import ( + NotificationSuccess, + NotificationType, +) +from tasks.bundle_analysis_notify import BundleAnalysisNotifyTask + + +def test_bundle_analysis_notify_task( + mocker, + dbsession, + celery_app, + mock_redis, +): + mocker.patch.object(BundleAnalysisNotifyTask, "app", celery_app) + + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + + mocker.patch( + "services.bundle_analysis.notify.BundleAnalysisNotifyService.notify", + return_value=BundleAnalysisNotifyReturn( + notifications_configured=(NotificationType.PR_COMMENT,), + notifications_attempted=(NotificationType.PR_COMMENT,), + notifications_successful=(NotificationType.PR_COMMENT,), + ), + ) + + result = BundleAnalysisNotifyTask().run_impl( + dbsession, + {"results": [{"error": None}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + ) + assert result == { + "notify_attempted": True, + "notify_succeeded": NotificationSuccess.FULL_SUCCESS, + } + + +def test_bundle_analysis_notify_skips_if_all_processing_fail(dbsession): + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + result = BundleAnalysisNotifyTask().run_impl( + dbsession, + {"results": [{"error": True}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + ) + assert result == { + "notify_attempted": False, + "notify_succeeded": NotificationSuccess.ALL_ERRORED, + } diff --git a/apps/worker/tasks/tests/unit/test_bundle_analysis_processor_task.py b/apps/worker/tasks/tests/unit/test_bundle_analysis_processor_task.py new file mode 100644 index 0000000000..cf9daae544 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_bundle_analysis_processor_task.py @@ -0,0 +1,1415 @@ +from typing import Optional +from unittest.mock import ANY + +import pytest +from redis.exceptions import LockError +from shared.bundle_analysis.storage import get_bucket_name +from shared.django_apps.bundle_analysis.models import CacheConfig +from shared.storage.exceptions import PutRequestRateLimitError + +from database.enums import ReportType +from database.models import CommitReport, Upload +from database.tests.factories import CommitFactory, RepositoryFactory, UploadFactory +from services.archive import ArchiveService +from tasks.bundle_analysis_processor import BundleAnalysisProcessorTask +from tasks.bundle_analysis_save_measurements import ( + bundle_analysis_save_measurements_task_name, +) + + +class MockBundleReport: + def __init__(self, bundle_name, size): + self.bundle_name = bundle_name + self.size = size + + @property + def name(self): + return self.bundle_name + + +class MockBundleAnalysisReport: + def bundle_reports(self): + return [ + MockBundleReport("BundleA", 1111), + ] + + def ingest(self, path, compare_sha: Optional[str] = None): + return 123, "BundleA" + + def cleanup(self): + pass + + def delete_bundle_by_name(self, bundle_name): + pass + + def update_is_cached(self, d): + pass + + def associate_previous_assets(self, prev_bar): + pass + + def metadata(self): + return {} + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_bundle_analysis_processor_task_success( + mocker, + dbsession, + mock_storage, +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + commit = CommitFactory.create(state="pending") + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.flush() + + ingest = mocker.patch("shared.bundle_analysis.BundleAnalysisReport.ingest") + ingest.return_value = (123, "bundle1") # session_id + + result = BundleAnalysisProcessorTask().run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": upload.id_, + "commit": commit.commitid, + }, + ) + assert result == { + "results": [ + {"previous": "result"}, + { + "error": None, + "session_id": 123, + "upload_id": upload.id_, + "bundle_name": "bundle1", + }, + ], + } + + assert commit.state == "complete" + assert upload.state == "processed" + + +def test_bundle_analysis_processor_task_error( + mocker, + dbsession, + mock_storage, +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + commit = CommitFactory.create(state="pending") + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create( + storage_path="invalid-storage-path", report=commit_report + ) + dbsession.add(upload) + dbsession.flush() + + task = BundleAnalysisProcessorTask() + retry = mocker.patch.object(task, "retry") + + result = task.run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": upload.id_, + "commit": commit.commitid, + }, + ) + assert result == { + "results": [ + {"previous": "result"}, + { + "error": { + "code": "file_not_in_storage", + "params": {"location": "invalid-storage-path"}, + }, + "session_id": None, + "upload_id": upload.id_, + "bundle_name": None, + }, + ], + } + + assert commit.state == "error" + assert upload.state == "error" + retry.assert_called_once_with(countdown=30) + + +def test_bundle_analysis_processor_task_general_error( + mocker, + dbsession, + mock_storage, +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + process_upload = mocker.patch( + "services.bundle_analysis.report.BundleAnalysisReportService.process_upload" + ) + process_upload.side_effect = Exception() + + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create( + state="started", + storage_path="invalid-storage-path", + report=commit_report, + ) + dbsession.add(upload) + dbsession.flush() + + task = BundleAnalysisProcessorTask() + retry = mocker.patch.object(task, "retry") + + with pytest.raises(Exception): + task.run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": upload.id_, + "commit": commit.commitid, + }, + ) + + assert upload.state == "error" + assert not retry.called + + +def test_bundle_analysis_process_upload_general_error( + mocker, + dbsession, + mock_storage, +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + commit = CommitFactory.create(state="pending") + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.flush() + + ingest = mocker.patch("shared.bundle_analysis.BundleAnalysisReport.ingest") + ingest.side_effect = Exception() + + task = BundleAnalysisProcessorTask() + retry = mocker.patch.object(task, "retry") + + result = BundleAnalysisProcessorTask().run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": upload.id_, + "commit": commit.commitid, + }, + ) + + assert result == { + "results": [ + {"previous": "result"}, + { + "error": { + "code": "parser_error", + "params": { + "location": "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite", + "plugin_name": "unknown", + }, + }, + "session_id": None, + "upload_id": upload.id_, + "bundle_name": None, + }, + ], + } + + assert not retry.called + assert upload.state == "error" + assert commit.state == "error" + + +def test_bundle_analysis_processor_task_locked( + mocker, + dbsession, + mock_storage, + mock_redis, +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + mock_redis.lock.return_value.__enter__.side_effect = LockError() + + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create( + state="started", + storage_path=storage_path, + report=commit_report, + ) + dbsession.add(upload) + dbsession.flush() + + task = BundleAnalysisProcessorTask() + retry = mocker.patch.object(task, "retry") + + result = task.run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": upload.id_, + "commit": commit.commitid, + }, + ) + assert result is None + + assert upload.state == "started" + retry.assert_called_once_with(countdown=ANY) + + +def test_bundle_analysis_process_upload_rate_limit_error( + mocker, + dbsession, + mock_storage, +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + commit = CommitFactory.create(state="pending") + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.flush() + + task = BundleAnalysisProcessorTask() + retry = mocker.patch.object(task, "retry") + + ingest = mocker.patch("shared.bundle_analysis.BundleAnalysisReport.ingest") + ingest.side_effect = PutRequestRateLimitError() + + result = task.run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": upload.id_, + "commit": commit.commitid, + }, + ) + assert result == { + "results": [ + {"previous": "result"}, + { + "error": { + "code": "rate_limit_error", + "params": { + "location": "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + }, + }, + "session_id": None, + "upload_id": upload.id_, + "bundle_name": None, + }, + ], + } + + assert commit.state == "error" + assert upload.state == "error" + retry.assert_called_once_with(countdown=30) + + +def test_bundle_analysis_process_associate_no_parent_commit_id( + mocker, + dbsession, + mock_storage, +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + parent_commit = CommitFactory.create(state="complete") + dbsession.add(parent_commit) + dbsession.flush() + + commit = CommitFactory.create(state="pending", parent_commit_id=None) + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.flush() + + ingest = mocker.patch("shared.bundle_analysis.BundleAnalysisReport.ingest") + ingest.return_value = (123, "bundle1") # session_id + + BundleAnalysisProcessorTask().run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": upload.id_, + "commit": commit.commitid, + }, + ) + + assert commit.state == "complete" + assert upload.state == "processed" + + +def test_bundle_analysis_process_associate_no_parent_commit_object( + mocker, + dbsession, + mock_storage, +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + parent_commit = CommitFactory.create(state="complete") + + commit = CommitFactory.create( + state="pending", parent_commit_id=parent_commit.commitid + ) + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.flush() + + ingest = mocker.patch("shared.bundle_analysis.BundleAnalysisReport.ingest") + ingest.return_value = (123, "bundle1") # session_id + + BundleAnalysisProcessorTask().run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": upload.id_, + "commit": commit.commitid, + }, + ) + + assert commit.state == "complete" + assert upload.state == "processed" + + +def test_bundle_analysis_process_associate_no_parent_commit_report_object( + mocker, + dbsession, + mock_storage, +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + parent_commit = CommitFactory.create(state="complete") + dbsession.add(parent_commit) + dbsession.flush() + + commit = CommitFactory.create( + state="pending", + parent_commit_id=parent_commit.commitid, + repository=parent_commit.repository, + ) + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.flush() + + ingest = mocker.patch("shared.bundle_analysis.BundleAnalysisReport.ingest") + ingest.return_value = (123, "bundle1") # session_id + + BundleAnalysisProcessorTask().run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": upload.id_, + "commit": commit.commitid, + }, + ) + + assert commit.state == "complete" + assert upload.state == "processed" + + +def test_bundle_analysis_process_associate_called( + mocker, + dbsession, + mock_storage, +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + parent_commit = CommitFactory.create(state="complete") + dbsession.add(parent_commit) + dbsession.flush() + + parent_commit_report = CommitReport( + commit_id=parent_commit.id_, report_type="bundle_analysis" + ) + dbsession.add(parent_commit_report) + dbsession.flush() + + commit = CommitFactory.create( + state="pending", + parent_commit_id=parent_commit.commitid, + repository=parent_commit.repository, + ) + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.flush() + + ingest = mocker.patch("shared.bundle_analysis.BundleAnalysisReport.ingest") + ingest.return_value = (123, "bundle1") # session_id + + BundleAnalysisProcessorTask().run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": upload.id_, + "commit": commit.commitid, + }, + ) + + assert commit.state == "complete" + assert upload.state == "processed" + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_bundle_analysis_process_associate_called_two( + mocker, + dbsession, + mock_storage, +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + parent_commit = CommitFactory.create(state="complete") + dbsession.add(parent_commit) + dbsession.flush() + + parent_commit_report = CommitReport( + commit_id=parent_commit.id_, report_type="bundle_analysis" + ) + dbsession.add(parent_commit_report) + dbsession.flush() + + commit = CommitFactory.create( + state="pending", + parent_commit_id=parent_commit.commitid, + repository=parent_commit.repository, + ) + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.flush() + + ingest = mocker.patch("shared.bundle_analysis.BundleAnalysisReport.ingest") + ingest.return_value = (123, "bundle1") # session_id + + associate = mocker.patch( + "shared.bundle_analysis.BundleAnalysisReport.associate_previous_assets" + ) + associate.return_value = None + + prev_bundle_report = mocker.patch( + "services.bundle_analysis.report.BundleAnalysisReportService._previous_bundle_analysis_report" + ) + prev_bundle_report.side_effect = [None, MockBundleAnalysisReport()] + + BundleAnalysisProcessorTask().run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": upload.id_, + "commit": commit.commitid, + }, + ) + + assert commit.state == "complete" + assert upload.state == "processed" + associate.assert_called_once() + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_bundle_analysis_processor_associate_custom_compare_sha( + mocker, + dbsession, + mock_storage, +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + parent_commit = CommitFactory.create(state="complete") + dbsession.add(parent_commit) + dbsession.flush() + + parent_commit_report = CommitReport( + commit_id=parent_commit.id_, report_type="bundle_analysis" + ) + dbsession.add(parent_commit_report) + dbsession.flush() + + commit = CommitFactory.create( + state="pending", + parent_commit_id=parent_commit.commitid, + repository=parent_commit.repository, + ) + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.flush() + + ingest = mocker.patch("shared.bundle_analysis.BundleAnalysisReport.ingest") + ingest.return_value = (123, "bundle1") # session_id + + _get_parent_commit = mocker.patch( + "services.bundle_analysis.report.BundleAnalysisReportService._get_parent_commit" + ) + _get_parent_commit.side_effect = [None, None] + + BundleAnalysisProcessorTask().run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": upload.id_, + "commit": commit.commitid, + }, + ) + + assert commit.state == "complete" + assert upload.state == "processed" + + assert _get_parent_commit.call_count == 2 + args = _get_parent_commit.call_args_list + + assert args[0][1]["head_commit"] == commit + assert args[1][1]["head_commit"] == commit + + assert args[0][1]["head_bundle_report"] is None + assert args[1][1]["head_bundle_report"] is not None + + +def test_bundle_analysis_processor_task_cache_config_not_saved( + mocker, + dbsession, + mock_storage, +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + mocker.patch( + "shared.bundle_analysis.BundleAnalysisReportLoader.load", + return_value=MockBundleAnalysisReport(), + ) + + bundle_load_mock_save = mocker.patch( + "shared.bundle_analysis.BundleAnalysisReportLoader.save", + return_value=MockBundleAnalysisReport(), + ) + bundle_load_mock_save.return_value = None + + bundle_config_mock = mocker.patch( + "shared.django_apps.bundle_analysis.service.bundle_analysis.BundleAnalysisCacheConfigService.update_cache_option" + ) + + commit = CommitFactory.create(state="pending") + + # Using main branch as default and commit is in feat + commit.branch = "feat" + commit.repository.branch = "main" + + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.flush() + + result = BundleAnalysisProcessorTask().run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": upload.id_, + "commit": commit.commitid, + }, + ) + assert result == { + "results": [ + {"previous": "result"}, + { + "error": None, + "session_id": 123, + "upload_id": upload.id_, + "bundle_name": "BundleA", + }, + ], + } + + assert commit.state == "complete" + assert upload.state == "processed" + + bundle_config_mock.assert_not_called() + + +def test_bundle_analysis_processor_task_cache_config_saved( + mocker, + dbsession, + mock_storage, +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + mocker.patch( + "shared.bundle_analysis.BundleAnalysisReportLoader.load", + return_value=MockBundleAnalysisReport(), + ) + + bundle_load_mock_save = mocker.patch( + "shared.bundle_analysis.BundleAnalysisReportLoader.save", + return_value=MockBundleAnalysisReport(), + ) + bundle_load_mock_save.return_value = None + + bundle_config_mock = mocker.patch( + "shared.django_apps.bundle_analysis.service.bundle_analysis.BundleAnalysisCacheConfigService.create_if_not_exists" + ) + + commit = CommitFactory.create(state="pending") + + # Using main branch as default and commit is in main + commit.branch = "main" + commit.repository.branch = "main" + + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.flush() + + result = BundleAnalysisProcessorTask().run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": upload.id_, + "commit": commit.commitid, + }, + ) + assert result == { + "results": [ + {"previous": "result"}, + { + "error": None, + "session_id": 123, + "upload_id": upload.id_, + "bundle_name": "BundleA", + }, + ], + } + + assert commit.state == "complete" + assert upload.state == "processed" + + bundle_config_mock.assert_called_with(commit.repository.repoid, "BundleA") + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_bundle_analysis_processor_not_caching_previous_report( + mocker, + dbsession, + mock_storage, +): + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + repository = RepositoryFactory() + + prev_commit = CommitFactory.create(repository=repository) + dbsession.add(prev_commit) + dbsession.flush() + + prev_report = CommitReport( + commit_id=prev_commit.id_, report_type=ReportType.BUNDLE_ANALYSIS.value + ) + dbsession.add(prev_report) + dbsession.flush() + + commit = CommitFactory.create( + state="pending", repository=repository, parent_commit_id=prev_commit.commitid + ) + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + repo_key = ArchiveService.get_archive_hash(prev_commit.repository) + storage_path = ( + f"v1/repos/{repo_key}/{prev_report.external_id}/bundle_report.sqlite", + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.flush() + + loader_mock = mocker.patch( + "shared.bundle_analysis.BundleAnalysisReportLoader.load", + ) + loader_mock.side_effect = [None, MockBundleAnalysisReport(), None] + + saver_mock = mocker.patch( + "shared.bundle_analysis.BundleAnalysisReportLoader.save", + ) + saver_mock.return_value = None + + ingest = mocker.patch("shared.bundle_analysis.BundleAnalysisReport.ingest") + ingest.return_value = (123, "bundle1") # session_id + + result = BundleAnalysisProcessorTask().run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": upload.id_, + "commit": commit.commitid, + }, + ) + assert result == { + "results": [ + {"previous": "result"}, + { + "error": None, + "session_id": 123, + "upload_id": upload.id_, + "bundle_name": "BundleA", + }, + ], + } + + assert commit.state == "complete" + assert upload.state == "processed" + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_bundle_analysis_processor_not_caching_previous_report_two( + mocker, + dbsession, + mock_storage, +): + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + repository = RepositoryFactory() + + prev_commit = CommitFactory.create(repository=repository) + dbsession.add(prev_commit) + dbsession.flush() + + prev_report = CommitReport( + commit_id=prev_commit.id_, report_type=ReportType.BUNDLE_ANALYSIS.value + ) + dbsession.add(prev_report) + dbsession.flush() + + commit = CommitFactory.create( + state="pending", repository=repository, parent_commit_id=prev_commit.commitid + ) + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + CacheConfig.objects.create( + repo_id=commit.repoid, bundle_name="BundleA", is_caching=False + ) + + repo_key = ArchiveService.get_archive_hash(prev_commit.repository) + storage_path = ( + f"v1/repos/{repo_key}/{prev_report.external_id}/bundle_report.sqlite", + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.flush() + + loader_mock = mocker.patch( + "shared.bundle_analysis.BundleAnalysisReportLoader.load", + ) + loader_mock.side_effect = [None, MockBundleAnalysisReport(), None] + + saver_mock = mocker.patch( + "shared.bundle_analysis.BundleAnalysisReportLoader.save", + ) + saver_mock.return_value = None + + ingest = mocker.patch("shared.bundle_analysis.BundleAnalysisReport.ingest") + ingest.return_value = (123, "bundle1") # session_id + + result = BundleAnalysisProcessorTask().run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": upload.id_, + "commit": commit.commitid, + }, + ) + assert result == { + "results": [ + {"previous": "result"}, + { + "error": None, + "session_id": 123, + "upload_id": upload.id_, + "bundle_name": "BundleA", + }, + ], + } + + assert commit.state == "complete" + assert upload.state == "processed" + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_bundle_analysis_processor_caching_previous_report( + mocker, + dbsession, + mock_storage, +): + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + repository = RepositoryFactory() + + prev_commit = CommitFactory.create(repository=repository) + dbsession.add(prev_commit) + dbsession.flush() + + prev_report = CommitReport( + commit_id=prev_commit.id_, report_type=ReportType.BUNDLE_ANALYSIS.value + ) + dbsession.add(prev_report) + dbsession.flush() + + commit = CommitFactory.create( + state="pending", repository=repository, parent_commit_id=prev_commit.commitid + ) + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + CacheConfig.objects.create( + repo_id=commit.repoid, bundle_name="BundleA", is_caching=True + ) + + repo_key = ArchiveService.get_archive_hash(prev_commit.repository) + storage_path = ( + f"v1/repos/{repo_key}/{prev_report.external_id}/bundle_report.sqlite", + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.flush() + + loader_mock = mocker.patch( + "shared.bundle_analysis.BundleAnalysisReportLoader.load", + ) + loader_mock.side_effect = [None, MockBundleAnalysisReport(), None] + + saver_mock = mocker.patch( + "shared.bundle_analysis.BundleAnalysisReportLoader.save", + ) + saver_mock.return_value = None + + ingest = mocker.patch("shared.bundle_analysis.BundleAnalysisReport.ingest") + ingest.return_value = (123, "bundle1") # session_id + + result = BundleAnalysisProcessorTask().run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": upload.id_, + "commit": commit.commitid, + }, + ) + assert result == { + "results": [ + {"previous": "result"}, + { + "error": None, + "session_id": 123, + "upload_id": upload.id_, + "bundle_name": "BundleA", + }, + ], + } + + assert commit.state == "complete" + assert upload.state == "processed" + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_bundle_analysis_processor_task_no_upload( + mocker, + dbsession, + mock_storage, +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + commit = CommitFactory.create(state="pending") + dbsession.add(commit) + dbsession.flush() + + result = BundleAnalysisProcessorTask().run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": None, + "commit": commit.commitid, + }, + ) + + commit_report = dbsession.query(CommitReport).filter_by(commit_id=commit.id).first() + assert commit_report is not None + + upload = dbsession.query(Upload).filter_by(report_id=commit_report.id).first() + assert upload is not None + + assert result == { + "results": [ + {"previous": "result"}, + { + "error": None, + "session_id": None, + "upload_id": upload.id_, + "bundle_name": None, + }, + ], + } + + assert commit.state == "complete" + assert upload.state == "processed" + assert upload.upload_type == "carriedforward" + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_bundle_analysis_processor_task_carryforward( + mocker, + dbsession, + mock_storage, +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + commit = CommitFactory.create(state="pending") + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport( + commit_id=commit.id_, report_type=ReportType.BUNDLE_ANALYSIS.value + ) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create( + storage_path=storage_path, report=commit_report, state="processed" + ) + dbsession.add(upload) + dbsession.flush() + + BundleAnalysisProcessorTask().run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": None, + "commit": commit.commitid, + }, + ) + + # A new upload wasn't created because the caching was skipped + total_uploads = ( + dbsession.query(Upload).filter_by(report_id=commit_report.id).count() + ) + assert total_uploads == 1 + + # A new report wasn't created either + total_ba_reports = ( + dbsession.query(CommitReport).filter_by(commit_id=commit.id).count() + ) + assert total_ba_reports == 1 + + +@pytest.mark.django_db(databases={"default", "timeseries"}) +def test_bundle_analysis_processor_task_carryforward_error( + mocker, + dbsession, + mock_storage, +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object( + BundleAnalysisProcessorTask, + "app", + tasks={ + bundle_analysis_save_measurements_task_name: mocker.MagicMock(), + }, + ) + + commit = CommitFactory.create(state="pending") + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport( + commit_id=commit.id_, report_type=ReportType.BUNDLE_ANALYSIS.value + ) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create( + storage_path=storage_path, report=commit_report, state="error" + ) + dbsession.add(upload) + dbsession.flush() + + BundleAnalysisProcessorTask().run_impl( + dbsession, + {"results": [{"previous": "result"}]}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + params={ + "upload_id": None, + "commit": commit.commitid, + }, + ) + + # A new upload was created because all the previous uploads were in error states + total_uploads = ( + dbsession.query(Upload).filter_by(report_id=commit_report.id).count() + ) + assert total_uploads == 2 + + # There should still only be 1 BA report + total_ba_reports = ( + dbsession.query(CommitReport).filter_by(commit_id=commit.id).count() + ) + assert total_ba_reports == 1 diff --git a/apps/worker/tasks/tests/unit/test_bundle_analysis_save_measurements_task.py b/apps/worker/tasks/tests/unit/test_bundle_analysis_save_measurements_task.py new file mode 100644 index 0000000000..89c05bbd29 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_bundle_analysis_save_measurements_task.py @@ -0,0 +1,169 @@ +from shared.bundle_analysis.storage import get_bucket_name + +from database.models import CommitReport +from database.tests.factories import CommitFactory, UploadFactory +from services.bundle_analysis.report import ProcessingResult +from tasks.bundle_analysis_save_measurements import BundleAnalysisSaveMeasurementsTask + + +def test_bundle_analysis_save_measurements_task_success( + mocker, dbsession, mock_storage, celery_app +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object(BundleAnalysisSaveMeasurementsTask, "app", celery_app) + + commit = CommitFactory.create(state="complete") + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.flush() + + save_measurements_mock = mocker.patch( + "services.bundle_analysis.report.BundleAnalysisReportService.save_measurements" + ) + save_measurements_mock.return_value = ProcessingResult( + upload=upload, commit=commit, error=None + ) + + result = BundleAnalysisSaveMeasurementsTask().run_impl( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + uploadid=upload.id_, + commit_yaml={}, + previous_result=[ + { + "upload_id": upload.id_, + "session_id": 28, + "error": None, + "bundle_name": "BundleA", + } + ], + ) + assert result == {"successful": True} + + +def test_bundle_analysis_save_measurements_task_no_uploads_success( + mocker, dbsession, mock_storage, celery_app +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object(BundleAnalysisSaveMeasurementsTask, "app", celery_app) + + commit = CommitFactory.create(state="complete") + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + save_measurements_mock = mocker.patch( + "services.bundle_analysis.report.BundleAnalysisReportService.save_measurements" + ) + save_measurements_mock.return_value = ProcessingResult( + upload=None, commit=commit, error=None + ) + + result = BundleAnalysisSaveMeasurementsTask().run_impl( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + uploadid=None, + commit_yaml={}, + previous_result=[{"upload_id": None, "session_id": 28, "error": None}], + ) + assert result == {"successful": True} + + +def test_bundle_analysis_save_measurements_task_error_from_save_service( + mocker, dbsession, mock_storage, celery_app +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object(BundleAnalysisSaveMeasurementsTask, "app", celery_app) + + commit = CommitFactory.create(state="complete") + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.flush() + + save_measurements_mock = mocker.patch( + "services.bundle_analysis.report.BundleAnalysisReportService.save_measurements" + ) + save_measurements_mock.return_value = ProcessingResult( + upload=upload, commit=commit, error=True + ) + + result = BundleAnalysisSaveMeasurementsTask().run_impl( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + uploadid=upload.id_, + commit_yaml={}, + previous_result=[{"upload_id": upload.id_, "session_id": 28, "error": False}], + ) + assert result == {"successful": False} + + +def test_bundle_analysis_save_measurements_task_error_from_processor_task( + mocker, dbsession, mock_storage, celery_app +): + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + mock_storage.write_file(get_bucket_name(), storage_path, "test-content") + + mocker.patch.object(BundleAnalysisSaveMeasurementsTask, "app", celery_app) + + commit = CommitFactory.create(state="complete") + dbsession.add(commit) + dbsession.flush() + + commit_report = CommitReport(commit_id=commit.id_) + dbsession.add(commit_report) + dbsession.flush() + + upload = UploadFactory.create(storage_path=storage_path, report=commit_report) + dbsession.add(upload) + dbsession.flush() + + save_measurements_mock = mocker.patch( + "services.bundle_analysis.report.BundleAnalysisReportService.save_measurements" + ) + save_measurements_mock.return_value = ProcessingResult( + upload=upload, commit=commit, error=None + ) + + result = BundleAnalysisSaveMeasurementsTask().run_impl( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + uploadid=upload.id_, + commit_yaml={}, + previous_result=[{"upload_id": upload.id_, "session_id": 28, "error": True}], + ) + assert result == {"successful": False} diff --git a/apps/worker/tasks/tests/unit/test_cache_rollup_cron_task.py b/apps/worker/tasks/tests/unit/test_cache_rollup_cron_task.py new file mode 100644 index 0000000000..888b5595df --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_cache_rollup_cron_task.py @@ -0,0 +1,52 @@ +import datetime as dt + +from shared.django_apps.reports.models import LastCacheRollupDate +from shared.django_apps.reports.tests.factories import LastCacheRollupDateFactory + +from tasks.cache_rollup_cron_task import CacheRollupTask +from tasks.cache_test_rollups import cache_test_rollups_task_name + + +def test_cache_rollup_cron_task(mock_storage, transactional_db, mocker): + mocked_app = mocker.patch.object( + CacheRollupTask, + "app", + tasks={ + cache_test_rollups_task_name: mocker.MagicMock(), + }, + ) + rollup_date = LastCacheRollupDateFactory( + last_rollup_date=dt.date.today() - dt.timedelta(days=1), + ) + rollup_date.save() + + CacheRollupTask().run_cron_task( + _db_session=None, + ) + + mocked_app.tasks[cache_test_rollups_task_name].s.assert_called_once_with( + repoid=rollup_date.repository_id, + branch=rollup_date.branch, + update_date=False, + ) + + +def test_cache_rollup_cron_task_delete(mock_storage, transactional_db, mocker): + mocked_app = mocker.patch.object( + CacheRollupTask, + "app", + tasks={ + cache_test_rollups_task_name: mocker.MagicMock(), + }, + ) + rollup_date = LastCacheRollupDateFactory( + last_rollup_date=dt.date.today() - dt.timedelta(days=31), + ) + + CacheRollupTask().run_cron_task( + _db_session=None, + ) + + mocked_app.tasks[cache_test_rollups_task_name].s.assert_not_called() + + assert LastCacheRollupDate.objects.filter(id=rollup_date.id).first() is None diff --git a/apps/worker/tasks/tests/unit/test_cache_test_rollups.py b/apps/worker/tasks/tests/unit/test_cache_test_rollups.py new file mode 100644 index 0000000000..a43c483653 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_cache_test_rollups.py @@ -0,0 +1,265 @@ +import datetime as dt + +import polars as pl +import time_machine +from shared.django_apps.core.tests.factories import RepositoryFactory +from shared.django_apps.reports.models import LastCacheRollupDate +from shared.django_apps.reports.tests.factories import ( + DailyTestRollupFactory, + LastCacheRollupDateFactory, + RepositoryFlagFactory, + TestFactory, + TestFlagBridgeFactory, +) + +from tasks.cache_test_rollups import CacheTestRollupsTask + + +class TestCacheTestRollupsTask: + def read_table(self, mock_storage, storage_path: str): + decompressed_table: bytes = mock_storage.read_file("archive", storage_path) + return pl.read_ipc(decompressed_table) + + def test_cache_test_rollups(self, mock_storage, transactional_db): + with time_machine.travel(dt.datetime.now(dt.timezone.utc), tick=False): + self.repo = RepositoryFactory() + self.flag = RepositoryFlagFactory( + repository=self.repo, + flag_name="test-rollups", + ) + self.flag2 = RepositoryFlagFactory( + repository=self.repo, + flag_name="test-rollups2", + ) + self.test = TestFactory(repository=self.repo, testsuite="testsuite1") + self.test2 = TestFactory(repository=self.repo, testsuite="testsuite2") + self.test3 = TestFactory(repository=self.repo, testsuite="testsuite3") + + _ = TestFlagBridgeFactory( + test=self.test, + flag=self.flag, + ) + _ = TestFlagBridgeFactory( + test=self.test2, + flag=self.flag2, + ) + + _ = DailyTestRollupFactory( + test=self.test, + commits_where_fail=["123", "456"], + repoid=self.repo.repoid, + branch="main", + pass_count=1, + date=dt.date.today(), + latest_run=dt.datetime.now(dt.timezone.utc), + ) + r = DailyTestRollupFactory( + test=self.test2, + repoid=self.repo.repoid, + branch="main", + pass_count=1, + fail_count=1, + date=dt.date.today() - dt.timedelta(days=6), + commits_where_fail=["123"], + latest_run=dt.datetime.now(dt.timezone.utc), + ) + r.created_at = dt.datetime.now(dt.timezone.utc) - dt.timedelta(seconds=1) + r.save() + _ = DailyTestRollupFactory( + test=self.test2, + repoid=self.repo.repoid, + branch="main", + pass_count=0, + fail_count=10, + date=dt.date.today() - dt.timedelta(days=29), + commits_where_fail=["123", "789"], + latest_run=dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=29), + ) + _ = DailyTestRollupFactory( + test=self.test3, + repoid=self.repo.repoid, + branch="main", + pass_count=0, + fail_count=10, + date=dt.date.today() - dt.timedelta(days=50), + commits_where_fail=["123", "789"], + latest_run=dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=50), + ) + + task = CacheTestRollupsTask() + result = task.run_impl( + _db_session=None, repo_id=self.repo.repoid, branch="main" + ) + assert result == {"success": True} + + storage_key = f"test_results/rollups/{self.repo.repoid}/main/1" + table = self.read_table(mock_storage, storage_key) + + assert table.to_dict(as_series=False) == { + "avg_duration": [0.0], + "commits_where_fail": [2], + "failure_rate": [0.0], + "flags": [["test-rollups"]], + "flake_rate": [0.0], + "last_duration": [0.0], + "name": [self.test.name], + "test_id": [self.test.id], + "testsuite": [self.test.testsuite], + "total_fail_count": [0], + "total_flaky_fail_count": [0], + "total_pass_count": [1], + "total_skip_count": [0], + "updated_at": [dt.datetime.now(dt.timezone.utc)], + } + + storage_key = f"test_results/rollups/{self.repo.repoid}/main/7" + table = self.read_table(mock_storage, storage_key) + + assert table.to_dict(as_series=False) == { + "avg_duration": [0.0, 0.0], + "commits_where_fail": [2, 1], + "failure_rate": [0.0, 0.5], + "flags": [["test-rollups"], ["test-rollups2"]], + "flake_rate": [0.0, 0.0], + "last_duration": [0.0, 0.0], + "name": [self.test.name, self.test2.name], + "test_id": [self.test.id, self.test2.id], + "testsuite": [self.test.testsuite, self.test2.testsuite], + "total_fail_count": [0, 1], + "total_flaky_fail_count": [0, 0], + "total_pass_count": [1, 1], + "total_skip_count": [0, 0], + "updated_at": [ + dt.datetime.now(dt.timezone.utc), + dt.datetime.now(dt.timezone.utc), + ], + } + + storage_key = f"test_results/rollups/{self.repo.repoid}/main/30" + table = self.read_table(mock_storage, storage_key) + + assert table.to_dict(as_series=False) == { + "avg_duration": [0.0, 0.0], + "commits_where_fail": [2, 2], + "failure_rate": [0.0, 0.9166666666666666], + "flags": [["test-rollups"], ["test-rollups2"]], + "flake_rate": [0.0, 0.0], + "last_duration": [0.0, 0.0], + "name": [self.test.name, self.test2.name], + "test_id": [self.test.id, self.test2.id], + "testsuite": [self.test.testsuite, self.test2.testsuite], + "total_fail_count": [0, 11], + "total_flaky_fail_count": [0, 0], + "total_pass_count": [1, 1], + "total_skip_count": [0, 0], + "updated_at": [ + dt.datetime.now(dt.timezone.utc), + dt.datetime.now(dt.timezone.utc), + ], + } + + storage_key = f"test_results/rollups/{self.repo.repoid}/main/60_30" + table = self.read_table(mock_storage, storage_key) + + assert table.to_dict(as_series=False) == { + "name": [self.test3.name], + "test_id": [self.test3.id], + "testsuite": [self.test3.testsuite], + "flags": [None], + "failure_rate": [1.0], + "flake_rate": [0.0], + "updated_at": [ + dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=50) + ], + "avg_duration": [0.0], + "total_fail_count": [10], + "total_flaky_fail_count": [0], + "total_pass_count": [0], + "total_skip_count": [0], + "commits_where_fail": [2], + "last_duration": [0.0], + } + + def test_cache_test_rollups_no_update_date(self, mock_storage, transactional_db): + with time_machine.travel(dt.datetime.now(dt.UTC), tick=False): + self.repo = RepositoryFactory() + rollup_date = LastCacheRollupDateFactory( + repository=self.repo, + last_rollup_date=dt.date.today() - dt.timedelta(days=30), + ) + + task = CacheTestRollupsTask() + _ = task.run_impl( + _db_session=None, + repo_id=rollup_date.repository_id, + branch=rollup_date.branch, + update_date=False, + ) + + obj = LastCacheRollupDate.objects.filter( + repository_id=self.repo.repoid, branch="main" + ).first() + assert obj.last_rollup_date == dt.date.today() - dt.timedelta(days=30) + + def test_cache_test_rollups_update_date(self, mock_storage, transactional_db): + with time_machine.travel(dt.datetime.now(dt.UTC), tick=False): + self.repo = RepositoryFactory() + + rollup_date = LastCacheRollupDateFactory( + repository=self.repo, + last_rollup_date=dt.date.today() - dt.timedelta(days=1), + ) + + task = CacheTestRollupsTask() + _ = task.run_impl( + _db_session=None, + repo_id=rollup_date.repository_id, + branch="main", + update_date=True, + ) + + obj = LastCacheRollupDate.objects.filter( + repository_id=self.repo.repoid, branch="main" + ).first() + assert obj.last_rollup_date == dt.date.today() + + def test_cache_test_rollups_update_date_does_not_exist( + self, mock_storage, transactional_db + ): + self.repo = RepositoryFactory() + with time_machine.travel(dt.datetime.now(dt.UTC), tick=False): + task = CacheTestRollupsTask() + _ = task.run_impl( + _db_session=None, + repo_id=self.repo.repoid, + branch="main", + update_date=True, + ) + + obj = LastCacheRollupDate.objects.filter( + repository_id=self.repo.repoid, branch="main" + ).first() + assert obj.last_rollup_date == dt.date.today() + + def test_cache_test_rollups_both(self, mock_storage, transactional_db, mocker): + mock_cache_rollups = mocker.patch("tasks.cache_test_rollups.cache_rollups") + task = CacheTestRollupsTask() + mocker.patch.object(task, "run_impl_within_lock") + self.repo = RepositoryFactory() + with time_machine.travel(dt.datetime.now(dt.UTC), tick=False): + _ = task.run_impl( + _db_session=None, + repo_id=self.repo.repoid, + branch="main", + update_date=True, + impl_type="both", + ) + + mock_cache_rollups.assert_has_calls( + [ + mocker.call(self.repo.repoid, "main"), + mocker.call(self.repo.repoid, None), + ] + ) + + task.run_impl_within_lock.assert_called_once() diff --git a/apps/worker/tasks/tests/unit/test_cache_test_rollups_redis.py b/apps/worker/tasks/tests/unit/test_cache_test_rollups_redis.py new file mode 100644 index 0000000000..5cb4e57f81 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_cache_test_rollups_redis.py @@ -0,0 +1,34 @@ +import polars as pl +import shared.storage +from shared.django_apps.core.tests.factories import RepositoryFactory +from shared.helpers.redis import get_redis_connection +from shared.storage.exceptions import BucketAlreadyExistsError + +from tasks.cache_test_rollups_redis import CacheTestRollupsRedisTask + + +class TestCacheTestRollupsTask: + def read_table(self, mock_storage, storage_path: str): + decompressed_table: bytes = mock_storage.read_file("archive", storage_path) + return pl.read_ipc(decompressed_table) + + def test_cache_test_rollups(self, mock_storage, transactional_db): + repo = RepositoryFactory() + + redis = get_redis_connection() + storage_service = shared.storage.get_appropriate_storage_service(repo.repoid) + storage_key = f"test_results/rollups/{repo.repoid}/main/1" + try: + storage_service.create_root_storage("archive") + except BucketAlreadyExistsError: + pass + + storage_service.write_file("archive", storage_key, b"hello world") + + task = CacheTestRollupsRedisTask() + result = task.run_impl(_db_session=None, repoid=repo.repoid, branch="main") + assert result == {"success": True} + + redis_key = f"ta_roll:{repo.repoid}:main:1" + + assert redis.get(redis_key) == storage_service.read_file("archive", storage_key) diff --git a/apps/worker/tasks/tests/unit/test_check_static_analysis.py b/apps/worker/tasks/tests/unit/test_check_static_analysis.py new file mode 100644 index 0000000000..418d3401e4 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_check_static_analysis.py @@ -0,0 +1,89 @@ +from shared.staticanalysis import StaticAnalysisSingleFileSnapshotState + +from database.tests.factories.staticanalysis import ( + StaticAnalysisSuiteFactory, + StaticAnalysisSuiteFilepathFactory, +) +from tasks.static_analysis_suite_check import StaticAnalysisSuiteCheckTask + + +class TestStaticAnalysisCheckTask(object): + def test_simple_call_no_object_saved(self, dbsession): + task = StaticAnalysisSuiteCheckTask() + res = task.run_impl(dbsession, suite_id=987654321 * 7) + assert res == {"changed_count": None, "successful": False} + + def test_simple_call_with_suite_all_created( + self, dbsession, mock_storage, mock_configuration, mocker + ): + obj = StaticAnalysisSuiteFactory.create() + dbsession.add(obj) + dbsession.flush() + task = StaticAnalysisSuiteCheckTask() + for i in range(8): + fp_obj = StaticAnalysisSuiteFilepathFactory.create( + analysis_suite=obj, + file_snapshot__state_id=StaticAnalysisSingleFileSnapshotState.CREATED.db_id, + ) + mock_storage.write_file( + mock_configuration.params["services"]["minio"]["bucket"], + fp_obj.file_snapshot.content_location, + "aaaa", + ) + dbsession.add(fp_obj) + # adding one without writing + fp_obj = StaticAnalysisSuiteFilepathFactory.create( + analysis_suite=obj, + file_snapshot__state_id=StaticAnalysisSingleFileSnapshotState.CREATED.db_id, + ) + dbsession.add(fp_obj) + dbsession.flush() + res = task.run_impl(dbsession, suite_id=obj.id_) + assert res == {"changed_count": 8, "successful": True} + + def test_simple_call_with_suite_mix_from_other( + self, dbsession, mock_storage, mock_configuration, mocker + ): + obj = StaticAnalysisSuiteFactory.create() + another_obj_same_repo = StaticAnalysisSuiteFactory.create( + commit__repository=obj.commit.repository + ) + dbsession.add(obj) + dbsession.flush() + task = StaticAnalysisSuiteCheckTask() + for i in range(17): + fp_obj = StaticAnalysisSuiteFilepathFactory.create( + analysis_suite=another_obj_same_repo, + file_snapshot__state_id=StaticAnalysisSingleFileSnapshotState.CREATED.db_id, + ) + mock_storage.write_file( + mock_configuration.params["services"]["minio"]["bucket"], + fp_obj.file_snapshot.content_location, + "aaaa", + ) + dbsession.add(fp_obj) + for i in range(23): + fp_obj = StaticAnalysisSuiteFilepathFactory.create( + analysis_suite=obj, + file_snapshot__state_id=StaticAnalysisSingleFileSnapshotState.CREATED.db_id, + ) + mock_storage.write_file( + mock_configuration.params["services"]["minio"]["bucket"], + fp_obj.file_snapshot.content_location, + "aaaa", + ) + dbsession.add(fp_obj) + for i in range(2): + fp_obj = StaticAnalysisSuiteFilepathFactory.create( + analysis_suite=obj, + file_snapshot__state_id=StaticAnalysisSingleFileSnapshotState.VALID.db_id, + ) + mock_storage.write_file( + mock_configuration.params["services"]["minio"]["bucket"], + fp_obj.file_snapshot.content_location, + "aaaa", + ) + dbsession.add(fp_obj) + dbsession.flush() + res = task.run_impl(dbsession, suite_id=obj.id_) + assert res == {"changed_count": 23, "successful": True} diff --git a/apps/worker/tasks/tests/unit/test_commit_update.py b/apps/worker/tasks/tests/unit/test_commit_update.py new file mode 100644 index 0000000000..3dc588bb7c --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_commit_update.py @@ -0,0 +1,263 @@ +import datetime as dt + +import pytest +from shared.torngit.exceptions import ( + TorngitClientError, + TorngitObjectNotFoundError, + TorngitRepoNotFoundError, +) + +from database.models import Branch +from database.tests.factories import BranchFactory, CommitFactory, PullFactory +from helpers.exceptions import RepositoryWithoutValidBotError +from tasks.commit_update import CommitUpdateTask + + +@pytest.mark.integration +class TestCommitUpdate(object): + def test_update_commit( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_redis, + celery_app, + ): + mocker.patch.object(CommitUpdateTask, "app", celery_app) + + commit = CommitFactory.create( + message="", + commitid="a2d3e3c30547a000f026daa47610bb3f7b63aece", + repository__owner__unencrypted_oauth_token="ghp_test3c8iyfspq6h4s9ugpmq19qp7826rv20o", + repository__owner__username="test-acc9", + repository__owner__service="github", + repository__owner__service_id="104562106", + repository__name="test_example", + pullid=1, + ) + dbsession.add(commit) + dbsession.flush() + + result = CommitUpdateTask().run_impl(dbsession, commit.repoid, commit.commitid) + expected_result = {"was_updated": True} + assert expected_result == result + assert commit.message == "random-commit-msg" + assert commit.parent_commit_id is None + assert commit.branch == "featureA" + assert commit.pullid == 1 + + def test_update_commit_bot_unauthorized( + self, + mocker, + mock_configuration, + dbsession, + mock_redis, + mock_repo_provider, + mock_storage, + ): + mock_repo_provider.get_commit.side_effect = TorngitClientError( + 401, "response", "message" + ) + commit = CommitFactory.create( + message="", + parent_commit_id=None, + repository__owner__unencrypted_oauth_token="un-authorized", + repository__owner__username="test-acc9", + repository__yaml={"codecov": {"max_report_age": "764y ago"}}, + ) + mock_repo_provider.data = dict( + repo=dict(repoid=commit.repoid, commit=commit.commitid) + ) + dbsession.add(commit) + dbsession.flush() + + result = CommitUpdateTask().run_impl(dbsession, commit.repoid, commit.commitid) + assert {"was_updated": False} == result + assert commit.message == "" + assert commit.parent_commit_id is None + + def test_update_commit_no_bot( + self, + mocker, + mock_configuration, + dbsession, + mock_redis, + mock_repo_provider, + mock_storage, + ): + mock_get_repo_service = mocker.patch( + "tasks.commit_update.get_repo_provider_service" + ) + mock_get_repo_service.side_effect = RepositoryWithoutValidBotError() + commit = CommitFactory.create( + message="", + parent_commit_id=None, + commitid="a2d3e3c30547a000f026daa47610bb3f7b63aece", + repository__owner__unencrypted_oauth_token="ghp_test3c8iyfspq6h4s9ugpmq19qp7826rv20o", + repository__owner__username="test-acc9", + repository__yaml={"codecov": {"max_report_age": "764y ago"}}, + repository__name="test_example", + ) + dbsession.add(commit) + dbsession.flush() + result = CommitUpdateTask().run_impl(dbsession, commit.repoid, commit.commitid) + expected_result = {"was_updated": False} + assert expected_result == result + assert commit.message == "" + assert commit.parent_commit_id is None + + def test_update_commit_repo_not_found( + self, + mocker, + mock_configuration, + dbsession, + mock_redis, + mock_repo_provider, + mock_storage, + ): + mock_get_repo_service = mocker.patch( + "tasks.commit_update.get_repo_provider_service" + ) + mock_get_repo_service.side_effect = TorngitRepoNotFoundError( + "fake_response", "message" + ) + commit = CommitFactory.create( + message="", + parent_commit_id=None, + repository__owner__unencrypted_oauth_token="ghp_test3c8iyfspq6h4s9ugpmq19qp7826rv20o", + repository__owner__username="test-acc9", + repository__yaml={"codecov": {"max_report_age": "764y ago"}}, + repository__name="test_example", + ) + dbsession.add(commit) + dbsession.flush() + + result = CommitUpdateTask().run_impl(dbsession, commit.repoid, commit.commitid) + expected_result = {"was_updated": False} + assert expected_result == result + assert commit.message == "" + assert commit.parent_commit_id is None + + def test_update_commit_not_found( + self, + mocker, + mock_configuration, + dbsession, + mock_redis, + mock_repo_provider, + mock_storage, + ): + mock_update_commit_from_provider = mocker.patch( + "tasks.commit_update.possibly_update_commit_from_provider_info" + ) + mock_update_commit_from_provider.side_effect = TorngitObjectNotFoundError( + "fake_response", "message" + ) + commit = CommitFactory.create( + message="", + parent_commit_id=None, + repository__owner__unencrypted_oauth_token="ghp_test3c8iyfspq6h4s9ugpmq19qp7826rv20o", + repository__owner__username="test-acc9", + repository__yaml={"codecov": {"max_report_age": "764y ago"}}, + repository__name="test_example", + ) + dbsession.add(commit) + dbsession.flush() + + result = CommitUpdateTask().run_impl(dbsession, commit.repoid, commit.commitid) + expected_result = {"was_updated": False} + assert expected_result == result + assert commit.message == "" + assert commit.parent_commit_id is None + + @pytest.mark.parametrize("branch_authors", [None, False, True]) + @pytest.mark.parametrize("prev_head", ["old_head", "new_head"]) + @pytest.mark.parametrize("deleted", [False, True]) + def test_update_commit_already_populated( + self, + mocker, + mock_configuration, + dbsession, + mock_redis, + mock_repo_provider, + mock_storage, + branch_authors, + prev_head, + deleted, + ): + commit = CommitFactory.create( + message="commit_msg", + parent_commit_id=None, + repository__owner__unencrypted_oauth_token="ghp_test3c8iyfspq6h4s9ugpmq19qp7826rv20o", + repository__owner__username="test-acc9", + repository__yaml={"codecov": {"max_report_age": "764y ago"}}, + repository__name="test_example", + timestamp=dt.datetime.fromisoformat("2019-02-01T17:59:47"), + ) + dbsession.add(commit) + dbsession.flush() + + commit.branch = "featureA" + commit.pullid = 1 + dbsession.flush() + + old_head = CommitFactory.create( + message="", + commitid="b2d3e3c30547a000f026daa47610bb3f7b63aece", + repository=commit.repository, + timestamp=dt.datetime.fromisoformat("2019-01-01T17:59:47"), + ) + dbsession.add(old_head) + dbsession.flush() + + old_head.branch = "featureA" + old_head.pullid = 1 + dbsession.flush() + + pull = PullFactory( + repository=commit.repository, pullid=1, head=old_head.commitid + ) + dbsession.add(pull) + dbsession.flush() + + if branch_authors is False: + branch_authors = [] + elif branch_authors is True: + branch_authors = [commit.author_id] + + if prev_head == "old_head": + prev_head = old_head + elif prev_head == "new_head": + prev_head = commit + + if deleted: + prev_head.deleted = True + dbsession.flush() + + b = dbsession.query(Branch).first() + dbsession.delete(b) + dbsession.flush() + + branch = BranchFactory( + repository=commit.repository, + branch="featureA", + head=prev_head.commitid, + authors=branch_authors, + ) + dbsession.add(branch) + dbsession.flush() + + result = CommitUpdateTask().run_impl(dbsession, commit.repoid, commit.commitid) + expected_result = {"was_updated": False} + assert expected_result == result + assert commit.message == "commit_msg" + assert commit.parent_commit_id is None + assert commit.timestamp == dt.datetime.fromisoformat("2019-02-01T17:59:47") + assert commit.branch == "featureA" + + dbsession.refresh(pull) + assert pull.head == commit.commitid + + dbsession.refresh(branch) + assert branch.head == commit.commitid diff --git a/apps/worker/tasks/tests/unit/test_compute_comparison.py b/apps/worker/tasks/tests/unit/test_compute_comparison.py new file mode 100644 index 0000000000..da75a3d6af --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_compute_comparison.py @@ -0,0 +1,781 @@ +import json + +from celery import group +from shared.reports.readonly import ReadOnlyReport +from shared.reports.resources import Report +from shared.reports.types import ReportTotals +from shared.torngit.exceptions import TorngitRateLimitError +from shared.yaml import UserYaml + +from database.enums import CompareCommitError, CompareCommitState +from database.models import CompareComponent, CompareFlag, RepositoryFlag +from database.tests.factories import CompareCommitFactory +from rollouts import PARALLEL_COMPONENT_COMPARISON +from services.report import ReportService +from tasks.compute_comparison import ComputeComparisonTask + + +class TestComputeComparisonTask(object): + def test_set_state_to_processed( + self, dbsession, mocker, mock_repo_provider, mock_storage + ): + mocker.patch.object( + PARALLEL_COMPONENT_COMPARISON, "check_value", return_value=False + ) + comparison = CompareCommitFactory.create() + dbsession.add(comparison) + dbsession.flush() + task = ComputeComparisonTask() + mocker.patch.object( + ReportService, + "get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report(Report()), + ) + mock_repo_provider.get_compare.return_value = { + "diff": { + "files": { + "file_2.py": { + "type": "modified", + "before": None, + "segments": [ + {"header": ["2", "5", "2", "5"], "lines": ["+", "-", "-"]} + ], + } + } + } + } + get_current_yaml = mocker.patch("tasks.compute_comparison.get_current_yaml") + get_current_yaml.return_value = UserYaml({"coverage": {"status": None}}) + + task.run_impl(dbsession, comparison.id) + dbsession.flush() + assert comparison.state == CompareCommitState.processed.value + data_in_storage = mock_storage.read_file( + "archive", comparison.report_storage_path + ) + assert comparison.patch_totals == { + "hits": 0, + "misses": 0, + "partials": 0, + "coverage": None, + } + assert json.loads(data_in_storage) == { + "files": [], + "changes_summary": { + "patch_totals": { + "hits": 0, + "misses": 0, + "partials": 0, + "coverage": None, + } + }, + } + + def test_set_state_to_processed_non_empty_report_with_flag_comparisons( + self, + dbsession, + mocker, + mock_repo_provider, + mock_storage, + sample_report_with_multiple_flags, + ): + mocker.patch.object( + PARALLEL_COMPONENT_COMPARISON, "check_value", return_value=False + ) + comparison = CompareCommitFactory.create() + dbsession.add(comparison) + dbsession.flush() + task = ComputeComparisonTask() + mocker.patch.object( + ReportService, + "get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report( + sample_report_with_multiple_flags + ), + ) + mock_repo_provider.get_compare.return_value = { + "diff": { + "files": { + "file_2.py": { + "type": "modified", + "before": None, + "segments": [ + {"header": ["2", "5", "2", "5"], "lines": ["+", "-", "-"]} + ], + } + } + } + } + get_current_yaml = mocker.patch("tasks.compute_comparison.get_current_yaml") + get_current_yaml.return_value = UserYaml({"coverage": {"status": None}}) + + unit_repositoryflag = RepositoryFlag( + repository_id=comparison.compare_commit.repository.repoid, flag_name="unit" + ) + dbsession.add(unit_repositoryflag) + task.run_impl(dbsession, comparison.id) + dbsession.flush() + assert comparison.state == CompareCommitState.processed.value + compare_flag_records = dbsession.query(CompareFlag).all() + assert len(compare_flag_records) == 2 + assert compare_flag_records[0].repositoryflag_id == unit_repositoryflag.id_ + + data_in_storage = mock_storage.read_file( + "archive", comparison.report_storage_path + ) + assert json.loads(data_in_storage) == { + "files": [ + { + "base_name": "file_2.py", + "head_name": "file_2.py", + "file_was_added_by_diff": False, + "file_was_removed_by_diff": False, + "base_coverage": { + "hits": 1, + "misses": 0, + "partials": 1, + "branches": 1, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 0, + }, + "head_coverage": { + "hits": 1, + "misses": 0, + "partials": 1, + "branches": 1, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 0, + }, + "removed_diff_coverage": [], + "added_diff_coverage": [], + "unexpected_line_changes": [ + [[12, "h"], [11, None]], + [[13, None], [12, "h"]], + [[51, "p"], [50, None]], + [[52, None], [51, "p"]], + ], + "lines_only_on_base": [2, 3], + "lines_only_on_head": [2], + } + ], + "changes_summary": { + "patch_totals": { + "hits": 0, + "misses": 0, + "partials": 0, + "coverage": None, + } + }, + } + + def test_flag_comparisons_without_head_report( + self, + dbsession, + mocker, + mock_repo_provider, + mock_storage, + sample_report_without_flags, + ): + mocker.patch.object( + PARALLEL_COMPONENT_COMPARISON, "check_value", return_value=False + ) + comparison = CompareCommitFactory.create() + dbsession.add(comparison) + dbsession.flush() + task = ComputeComparisonTask() + mocker.patch.object( + ReportService, + "get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report(sample_report_without_flags), + ) + mock_repo_provider.get_compare.return_value = { + "diff": { + "files": { + "file_2.py": { + "type": "modified", + "before": None, + "segments": [ + {"header": ["2", "5", "2", "5"], "lines": ["+", "-", "-"]} + ], + } + } + } + } + get_current_yaml = mocker.patch("tasks.compute_comparison.get_current_yaml") + get_current_yaml.return_value = UserYaml({"coverage": {"status": None}}) + + task.run_impl(dbsession, comparison.id) + dbsession.flush() + assert comparison.state == CompareCommitState.processed.value + data_in_storage = mock_storage.read_file( + "archive", comparison.report_storage_path + ) + assert json.loads(data_in_storage) == { + "files": [ + { + "base_name": "file_2.py", + "head_name": "file_2.py", + "file_was_added_by_diff": False, + "file_was_removed_by_diff": False, + "base_coverage": { + "hits": 1, + "misses": 0, + "partials": 1, + "branches": 1, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 0, + }, + "head_coverage": { + "hits": 1, + "misses": 0, + "partials": 1, + "branches": 1, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 0, + }, + "removed_diff_coverage": [], + "added_diff_coverage": [], + "unexpected_line_changes": [ + [[12, "h"], [11, None]], + [[13, None], [12, "h"]], + [[51, "p"], [50, None]], + [[52, None], [51, "p"]], + ], + "lines_only_on_base": [2, 3], + "lines_only_on_head": [2], + } + ], + "changes_summary": { + "patch_totals": { + "hits": 0, + "misses": 0, + "partials": 0, + "coverage": None, + } + }, + } + + def test_update_existing_flag_comparisons( + self, dbsession, mocker, mock_repo_provider, mock_storage, sample_report + ): + mocker.patch.object( + PARALLEL_COMPONENT_COMPARISON, "check_value", return_value=False + ) + comparison = CompareCommitFactory.create() + dbsession.add(comparison) + dbsession.flush() + task = ComputeComparisonTask() + mocker.patch.object( + ReportService, + "get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report(sample_report), + ) + mock_repo_provider.get_compare.return_value = { + "diff": { + "files": { + "file_2.py": { + "type": "modified", + "before": None, + "segments": [ + {"header": ["2", "5", "2", "5"], "lines": ["+", "-", "-"]} + ], + } + } + } + } + get_current_yaml = mocker.patch("tasks.compute_comparison.get_current_yaml") + get_current_yaml.return_value = UserYaml({"coverage": {"status": None}}) + + repositoryflag = RepositoryFlag( + repository_id=comparison.compare_commit.repository.repoid, flag_name="unit" + ) + dbsession.add(repositoryflag) + existing_flag_comparison = CompareFlag( + commit_comparison=comparison, + repositoryflag=repositoryflag, + patch_totals=None, + head_totals=None, + base_totals=None, + ) + dbsession.add(existing_flag_comparison) + task.run_impl(dbsession, comparison.id) + dbsession.flush() + assert comparison.state == CompareCommitState.processed.value + compare_flag_records = dbsession.query(CompareFlag).all() + assert len(compare_flag_records) == 1 + assert compare_flag_records[0].repositoryflag_id == repositoryflag.id_ + assert compare_flag_records[0].patch_totals is not None + + def test_set_state_to_error_missing_base_report( + self, dbsession, mocker, sample_report + ): + mocker.patch.object( + PARALLEL_COMPONENT_COMPARISON, "check_value", return_value=False + ) + comparison = CompareCommitFactory.create() + # We need a head report, but no base report + head_commit = comparison.compare_commit + mocker.patch.object( + ReportService, + "get_existing_report_for_commit", + side_effect=lambda commit, + *args, + **kwargs: ReadOnlyReport.create_from_report(sample_report) + if commit == head_commit + else None, + ) + patch_totals = ReportTotals( + files=3, lines=200, hits=100, misses=100, coverage="10.5" + ) + mocker.patch( + "tasks.compute_comparison.ComparisonProxy.get_patch_totals", + return_value=patch_totals, + ) + dbsession.add(comparison) + dbsession.flush() + task = ComputeComparisonTask() + result = task.run_impl(dbsession, comparison.id) + dbsession.flush() + assert result == {"successful": False, "error": "missing_base_report"} + assert comparison.state == CompareCommitState.error.value + assert comparison.patch_totals == { + "hits": 100, + "misses": 100, + "partials": 0, + "coverage": 0.105, + } + assert comparison.error == CompareCommitError.missing_base_report.value + + def test_set_state_to_error_missing_head_report( + self, dbsession, mocker, sample_report + ): + mocker.patch.object( + PARALLEL_COMPONENT_COMPARISON, "check_value", return_value=False + ) + comparison = CompareCommitFactory.create() + dbsession.add(comparison) + dbsession.flush() + task = ComputeComparisonTask() + mocker.patch.object( + ReportService, + "get_existing_report_for_commit", + side_effect=(ReadOnlyReport.create_from_report(sample_report), None), + ) + task.run_impl(dbsession, comparison.id) + dbsession.flush() + assert comparison.state == CompareCommitState.error.value + assert comparison.error == CompareCommitError.missing_head_report.value + + def test_run_task_ratelimit_error(self, dbsession, mocker, sample_report): + mocker.patch.object( + PARALLEL_COMPONENT_COMPARISON, "check_value", return_value=False + ) + comparison = CompareCommitFactory.create() + dbsession.add(comparison) + dbsession.flush() + patch_totals = ReportTotals( + files=3, lines=200, hits=100, misses=100, coverage="50.00" + ) + mocker.patch( + "tasks.compute_comparison.ComparisonProxy.get_patch_totals", + return_value=patch_totals, + ) + mocker.patch( + "tasks.compute_comparison.ComparisonProxy.get_impacted_files", + side_effect=TorngitRateLimitError("response_data", "message", "reset"), + ) + task = ComputeComparisonTask() + mocker.patch.object( + ReportService, + "get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report(sample_report), + ) + res = task.run_impl(dbsession, comparison.id) + assert res == {"successful": False, "error": "torngit_rate_limit"} + dbsession.flush() + assert comparison.state == CompareCommitState.error.value + assert comparison.patch_totals == { + "hits": 100, + "misses": 100, + "partials": 0, + "coverage": 0.5, + } + assert comparison.error is None + + def test_compute_component_comparisons( + self, dbsession, mocker, mock_repo_provider, mock_storage, sample_report + ): + mocker.patch.object( + PARALLEL_COMPONENT_COMPARISON, "check_value", return_value=False + ) + mocker.patch.object( + ReportService, + "get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report(sample_report), + ) + mock_repo_provider.get_compare.return_value = { + "diff": { + "files": { + "file_2.py": { + "type": "modified", + "before": None, + "segments": [ + {"header": ["2", "5", "2", "5"], "lines": ["+", "-", "-"]} + ], + } + } + } + } + get_current_yaml = mocker.patch("tasks.compute_comparison.get_current_yaml") + get_current_yaml.return_value = UserYaml( + { + "component_management": { + "individual_components": [ + {"component_id": "go_files", "paths": [r".*\.go"]}, + {"component_id": "unit_flags", "flag_regexes": [r"unit.*"]}, + ] + } + } + ) + + comparison = CompareCommitFactory.create() + dbsession.add(comparison) + dbsession.flush() + + task = ComputeComparisonTask() + res = task.run_impl(dbsession, comparison.id) + assert res == {"successful": True} + + component_comparisons = ( + dbsession.query(CompareComponent) + .filter_by(commit_comparison_id=comparison.id) + .all() + ) + assert len(component_comparisons) == 2 + + go_comparison = component_comparisons[0] + assert go_comparison.component_id == "go_files" + assert go_comparison.base_totals == { + "files": 1, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": "62.50000", + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10, + "complexity_total": 2, + "diff": 0, + } + assert go_comparison.head_totals == { + "files": 1, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": "62.50000", + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10, + "complexity_total": 2, + "diff": 0, + } + assert go_comparison.patch_totals == { + "files": 0, + "lines": 0, + "hits": 0, + "misses": 0, + "partials": 0, + "coverage": None, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": None, + "complexity_total": None, + "diff": 0, + } + + unit_comparison = component_comparisons[1] + assert unit_comparison.component_id == "unit_flags" + assert unit_comparison.base_totals == { + "files": 2, + "lines": 10, + "hits": 10, + "misses": 0, + "partials": 0, + "coverage": "100", + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 0, + "complexity_total": 0, + "diff": 0, + } + assert unit_comparison.head_totals == { + "files": 2, + "lines": 10, + "hits": 10, + "misses": 0, + "partials": 0, + "coverage": "100", + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 0, + "complexity_total": 0, + "diff": 0, + } + assert unit_comparison.patch_totals == { + "files": 1, + "lines": 0, + "hits": 0, + "misses": 0, + "partials": 0, + "coverage": None, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": None, + "complexity_total": None, + "diff": 0, + } + + def test_compute_component_comparisons_parallel( + self, dbsession, mocker, mock_repo_provider, mock_storage, sample_report + ): + mocker.patch("tasks.base.get_db_session", return_value=dbsession) + + mocker.patch.object(group, "apply_async", group.apply) + mocker.patch.object( + PARALLEL_COMPONENT_COMPARISON, "check_value", return_value=True + ) + mocker.patch.object( + ReportService, + "get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report(sample_report), + ) + mock_repo_provider.get_compare.return_value = { + "diff": { + "files": { + "file_2.py": { + "type": "modified", + "before": None, + "segments": [ + {"header": ["2", "5", "2", "5"], "lines": ["+", "-", "-"]} + ], + } + } + } + } + mocker.patch( + "services.comparison.get_repo_provider_service", + return_value=mock_repo_provider, + ) + get_current_yaml = mocker.patch("tasks.compute_comparison.get_current_yaml") + get_current_yaml.return_value = UserYaml( + { + "component_management": { + "individual_components": [ + {"component_id": "go_files", "paths": [r".*\.go"]}, + {"component_id": "unit_flags", "flag_regexes": [r"unit.*"]}, + ] + } + } + ) + + get_current_yaml = mocker.patch( + "tasks.compute_component_comparison.get_current_yaml" + ) + get_current_yaml.return_value = UserYaml( + { + "component_management": { + "individual_components": [ + {"component_id": "go_files", "paths": [r".*\.go"]}, + {"component_id": "unit_flags", "flag_regexes": [r"unit.*"]}, + ] + } + } + ) + + comparison = CompareCommitFactory.create() + dbsession.add(comparison) + dbsession.flush() + comparison_id = comparison.id + + task = ComputeComparisonTask() + res = task.run_impl(dbsession, comparison.id) + assert res == {"successful": True} + + component_comparisons = ( + dbsession.query(CompareComponent) + .filter_by(commit_comparison_id=comparison_id) + .all() + ) + assert len(component_comparisons) == 2 + + go_comparison = component_comparisons[0] + assert go_comparison.component_id == "go_files" + assert go_comparison.base_totals == { + "files": 1, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": "62.50000", + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10, + "complexity_total": 2, + "diff": 0, + } + assert go_comparison.head_totals == { + "files": 1, + "lines": 8, + "hits": 5, + "misses": 3, + "partials": 0, + "coverage": "62.50000", + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 10, + "complexity_total": 2, + "diff": 0, + } + assert go_comparison.patch_totals == { + "files": 0, + "lines": 0, + "hits": 0, + "misses": 0, + "partials": 0, + "coverage": None, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": None, + "complexity_total": None, + "diff": 0, + } + + unit_comparison = component_comparisons[1] + assert unit_comparison.component_id == "unit_flags" + assert unit_comparison.base_totals == { + "files": 2, + "lines": 10, + "hits": 10, + "misses": 0, + "partials": 0, + "coverage": "100", + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 0, + "complexity_total": 0, + "diff": 0, + } + assert unit_comparison.head_totals == { + "files": 2, + "lines": 10, + "hits": 10, + "misses": 0, + "partials": 0, + "coverage": "100", + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 0, + "complexity_total": 0, + "diff": 0, + } + assert unit_comparison.patch_totals == { + "files": 1, + "lines": 0, + "hits": 0, + "misses": 0, + "partials": 0, + "coverage": None, + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 0, + "complexity": None, + "complexity_total": None, + "diff": 0, + } + + def test_compute_component_comparisons_empty_diff( + self, + dbsession, + mocker, + mock_repo_provider, + mock_storage, + sample_report_with_multiple_flags, + ): + mocker.patch.object( + PARALLEL_COMPONENT_COMPARISON, "check_value", return_value=False + ) + mocker.patch.object( + ReportService, + "get_existing_report_for_commit", + return_value=ReadOnlyReport.create_from_report( + sample_report_with_multiple_flags + ), + ) + mock_repo_provider.get_compare.return_value = {"diff": {"files": {}}} + + get_current_yaml = mocker.patch("tasks.compute_comparison.get_current_yaml") + get_current_yaml.return_value = UserYaml( + { + "component_management": { + "individual_components": [ + {"component_id": "go_files", "paths": [r".*\.go"]}, + {"component_id": "unit_flags", "flag_regexes": [r"unit.*"]}, + ] + } + } + ) + + comparison = CompareCommitFactory.create() + dbsession.add(comparison) + dbsession.flush() + + task = ComputeComparisonTask() + res = task.run_impl(dbsession, comparison.id) + assert res == {"successful": True} + + component_comparisons = ( + dbsession.query(CompareComponent) + .filter_by(commit_comparison_id=comparison.id) + .all() + ) + assert len(component_comparisons) == 2 + for comparison in component_comparisons: + assert comparison.patch_totals is None + + flag_comparisons = dbsession.query(CompareFlag).all() + assert len(flag_comparisons) == 2 + for comparison in flag_comparisons: + assert comparison.patch_totals is None diff --git a/apps/worker/tasks/tests/unit/test_crontasks.py b/apps/worker/tasks/tests/unit/test_crontasks.py new file mode 100644 index 0000000000..311f5c75e7 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_crontasks.py @@ -0,0 +1,52 @@ +from datetime import datetime, timedelta, timezone + +from redis.exceptions import LockError + +from tasks.crontasks import CodecovCronTask + + +class SampleCronTask(CodecovCronTask): + name = "test.SampleCronTask" + + def get_min_seconds_interval_between_executions(self): + return 234 + + @property + def hard_time_limit_task(self): + return 100 + + def run_cron_task(self, dbsession): + return {"unusual": "return", "value": ["something"]} + + +class TestCrontasks(object): + def test_simple_run(self, dbsession, mock_redis): + generation_time = datetime(2021, 1, 2, 0, 3, 4).replace(tzinfo=timezone.utc) + task = SampleCronTask() + res = task.run_impl( + dbsession, cron_task_generation_time_iso=generation_time.isoformat() + ) + assert res == { + "executed": True, + "result": {"unusual": "return", "value": ["something"]}, + } + + def test_simple_run_with_too_recent_call(self, dbsession, mock_redis): + generation_time = datetime(2021, 1, 2, 0, 3, 4).replace(tzinfo=timezone.utc) + mock_redis.get.return_value = ( + generation_time - timedelta(seconds=5) + ).timestamp() + task = SampleCronTask() + res = task.run_impl( + dbsession, cron_task_generation_time_iso=generation_time.isoformat() + ) + assert res == {"executed": False} + + def test_simple_run_with_lock_error(self, dbsession, mock_redis): + generation_time = datetime(2021, 1, 2, 0, 3, 4).replace(tzinfo=timezone.utc) + mock_redis.lock.side_effect = LockError + task = SampleCronTask() + res = task.run_impl( + dbsession, cron_task_generation_time_iso=generation_time.isoformat() + ) + assert res == {"executed": False} diff --git a/apps/worker/tasks/tests/unit/test_delete_owner.py b/apps/worker/tasks/tests/unit/test_delete_owner.py new file mode 100644 index 0000000000..e1350b3017 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_delete_owner.py @@ -0,0 +1,132 @@ +from pathlib import Path + +import pytest +from shared.django_apps.codecov_auth.models import Owner +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory +from shared.django_apps.compare.models import CommitComparison +from shared.django_apps.compare.tests.factories import CommitComparisonFactory +from shared.django_apps.core.models import Branch, Commit, Pull, Repository +from shared.django_apps.core.tests.factories import ( + CommitFactory, + RepositoryFactory, +) +from shared.django_apps.reports.models import ( + CommitReport, + DailyTestRollup, + Test, + TestInstance, +) +from shared.django_apps.reports.models import ReportSession as Upload +from shared.django_apps.reports.tests.factories import ( + CommitReportFactory, + DailyTestRollupFactory, + TestFactory, + TestInstanceFactory, + UploadFactory, +) + +from services.cleanup.utils import CleanupResult, CleanupSummary +from tasks.delete_owner import DeleteOwnerTask + +here = Path(__file__) + + +@pytest.mark.django_db(databases=["timeseries", "default"], transaction=True) +def test_delete_owner_deletes_owner_with_ownerid(mock_storage): + user = OwnerFactory() + repo = RepositoryFactory(author=user) + CommitFactory(repository=repo, author=user) + # NOTE: the commit creates an implicit `Branch` and `Pull` + + res = DeleteOwnerTask().run_impl({}, user.ownerid) + + assert res == CleanupSummary( + CleanupResult(5), + { + Branch: CleanupResult(1), + Commit: CleanupResult(1), + Owner: CleanupResult(1), + Pull: CleanupResult(1), + Repository: CleanupResult(1), + }, + ) + + assert Branch.objects.count() == 0 + assert Commit.objects.count() == 0 + assert Owner.objects.count() == 0 + assert Pull.objects.count() == 0 + assert Repository.objects.count() == 0 + + +@pytest.mark.django_db(databases=["timeseries", "default"], transaction=True) +def test_delete_owner_deletes_owner_with_commit_compares(mock_storage): + user = OwnerFactory() + repo = RepositoryFactory(author=user) + + base_commit = CommitFactory(repository=repo, author=user) + compare_commit = CommitFactory(repository=repo, author=user) + CommitComparisonFactory(base_commit=base_commit, compare_commit=compare_commit) + + report = CommitReportFactory(commit=base_commit) + upload = UploadFactory(report=report) + test = TestFactory(repository=repo) + TestInstanceFactory(test=test, upload=upload) + DailyTestRollupFactory(test=test, repoid=repo.repoid) + + # This test factory implicitly creates: + # - Test with a Repository and an Owner, + # - An Upload with a CommitReport, a Commit that has a different Owner and + # one more Repository with yet another different Owner. + # And then also a Branch and a Pull via DB triggers because of the Commit. + remaining = TestInstanceFactory() + + res = DeleteOwnerTask().run_impl({}, user.ownerid) + + assert res == CleanupSummary( + CleanupResult(12), + { + Branch: CleanupResult(1), + Commit: CleanupResult(2), + CommitComparison: CleanupResult(1), + Owner: CleanupResult(1), + Pull: CleanupResult(1), + Repository: CleanupResult(1), + CommitReport: CleanupResult(1), + Upload: CleanupResult(1), + Test: CleanupResult(1), + TestInstance: CleanupResult(1), + DailyTestRollup: CleanupResult(1), + }, + ) + + assert list(TestInstance.objects.all()) == [remaining] + # See the comment above why we have all of these objects + assert Branch.objects.count() == 1 + assert Commit.objects.count() == 1 + assert CommitComparison.objects.count() == 0 + assert Owner.objects.count() == 3 + assert Pull.objects.count() == 1 + assert Repository.objects.count() == 2 + assert CommitReport.objects.count() == 1 + assert Upload.objects.count() == 1 + assert Test.objects.count() == 1 + assert DailyTestRollup.objects.count() == 0 + + +@pytest.mark.django_db(databases=["timeseries", "default"], transaction=True) +def test_delete_owner_from_orgs_removes_ownerid_from_organizations_of_related_owners( + mock_storage, +): + org = OwnerFactory() + + user_1 = OwnerFactory(organizations=[org.ownerid]) + user_2 = OwnerFactory(organizations=[org.ownerid, user_1.ownerid]) + + res = DeleteOwnerTask().run_impl({}, org.ownerid) + + assert res.summary[Owner] == CleanupResult(1) + + user_1 = Owner.objects.get(pk=user_1.ownerid) + assert user_1.organizations == [] + user_2 = Owner.objects.get(pk=user_2.ownerid) + assert user_2.organizations == [user_1.ownerid] diff --git a/apps/worker/tasks/tests/unit/test_flare_cleanup.py b/apps/worker/tasks/tests/unit/test_flare_cleanup.py new file mode 100644 index 0000000000..7d66fc2ff3 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_flare_cleanup.py @@ -0,0 +1,198 @@ +from unittest.mock import call + +from shared.django_apps.core.models import Pull, PullStates +from shared.django_apps.core.tests.factories import PullFactory, RepositoryFactory + +from tasks.flare_cleanup import FlareCleanupTask + + +class TestFlareCleanupTask(object): + def test_get_min_seconds_interval_between_executions(self): + assert isinstance( + FlareCleanupTask.get_min_seconds_interval_between_executions(), + int, + ) + assert FlareCleanupTask.get_min_seconds_interval_between_executions() > 17000 + + def test_successful_run(self, transactional_db, mocker, mock_archive_storage): + mock_logs = mocker.patch("logging.Logger.info") + archive_value_for_flare = {"some": "data"} + local_value_for_flare = {"test": "test"} + + open_pull_with_local_flare = PullFactory( + state=PullStates.OPEN.value, + _flare=local_value_for_flare, + repository=RepositoryFactory(), + ) + assert open_pull_with_local_flare.flare == local_value_for_flare + assert open_pull_with_local_flare._flare == local_value_for_flare + assert open_pull_with_local_flare._flare_storage_path is None + + closed_pull_with_local_flare = PullFactory( + state=PullStates.CLOSED.value, + _flare=local_value_for_flare, + repository=RepositoryFactory(), + ) + assert closed_pull_with_local_flare.flare == local_value_for_flare + assert closed_pull_with_local_flare._flare == local_value_for_flare + assert closed_pull_with_local_flare._flare_storage_path is None + + open_pull_with_archive_flare = PullFactory( + state=PullStates.OPEN.value, + _flare=None, + repository=RepositoryFactory(), + ) + open_pull_with_archive_flare.flare = archive_value_for_flare + open_pull_with_archive_flare.save() + open_pull_with_archive_flare.refresh_from_db() + assert open_pull_with_archive_flare.flare == archive_value_for_flare + assert open_pull_with_archive_flare._flare is None + assert open_pull_with_archive_flare._flare_storage_path is not None + + merged_pull_with_archive_flare = PullFactory( + state=PullStates.MERGED.value, + _flare=None, + repository=RepositoryFactory(), + ) + merged_pull_with_archive_flare.flare = archive_value_for_flare + merged_pull_with_archive_flare.save() + merged_pull_with_archive_flare.refresh_from_db() + assert merged_pull_with_archive_flare.flare == archive_value_for_flare + assert merged_pull_with_archive_flare._flare is None + assert merged_pull_with_archive_flare._flare_storage_path is not None + + task = FlareCleanupTask() + task.manual_run() + + mock_logs.assert_has_calls( + [ + call("Starting FlareCleanupTask"), + call("FlareCleanupTask cleared 1 database flares"), + call("FlareCleanupTask cleared 1 Archive flares"), + ] + ) + + # there is a cache for flare on the object (all ArchiveFields have this), + # so get a fresh copy of each object without the cached value + open_pull_with_local_flare = Pull.objects.get(id=open_pull_with_local_flare.id) + assert open_pull_with_local_flare.flare == local_value_for_flare + assert open_pull_with_local_flare._flare == local_value_for_flare + assert open_pull_with_local_flare._flare_storage_path is None + + closed_pull_with_local_flare = Pull.objects.get( + id=closed_pull_with_local_flare.id + ) + assert closed_pull_with_local_flare.flare == {} + assert closed_pull_with_local_flare._flare is None + assert closed_pull_with_local_flare._flare_storage_path is None + + open_pull_with_archive_flare = Pull.objects.get( + id=open_pull_with_archive_flare.id + ) + assert open_pull_with_archive_flare.flare == archive_value_for_flare + assert open_pull_with_archive_flare._flare is None + assert open_pull_with_archive_flare._flare_storage_path is not None + + merged_pull_with_archive_flare = Pull.objects.get( + id=merged_pull_with_archive_flare.id + ) + assert merged_pull_with_archive_flare.flare == {} + assert merged_pull_with_archive_flare._flare is None + assert merged_pull_with_archive_flare._flare_storage_path is None + + mock_logs.reset_mock() + # check that once these pulls are corrected they are not corrected again + task = FlareCleanupTask() + task.manual_run() + + mock_logs.assert_has_calls( + [ + call("Starting FlareCleanupTask"), + call("FlareCleanupTask cleared 0 database flares"), + call("FlareCleanupTask cleared 0 Archive flares"), + ] + ) + + def test_limits_on_manual_run(self, transactional_db, mocker, mock_archive_storage): + mock_logs = mocker.patch("logging.Logger.info") + local_value_for_flare = {"test": "test"} + archive_value_for_flare = {"some": "data"} + + oldest_to_newest_pulls_with_local_flare = [] + for i in range(5): + merged_pull_with_local_flare = PullFactory( + state=PullStates.MERGED.value, + _flare=local_value_for_flare, + repository=RepositoryFactory(), + ) + assert merged_pull_with_local_flare.flare == local_value_for_flare + assert merged_pull_with_local_flare._flare == local_value_for_flare + assert merged_pull_with_local_flare._flare_storage_path is None + oldest_to_newest_pulls_with_local_flare.append( + merged_pull_with_local_flare.id + ) + + oldest_to_newest_pulls_with_archive_flare = [] + for i in range(5): + merged_pull_with_archive_flare = PullFactory( + state=PullStates.MERGED.value, + _flare=None, + repository=RepositoryFactory(), + ) + merged_pull_with_archive_flare.flare = archive_value_for_flare + merged_pull_with_archive_flare.save() + assert merged_pull_with_archive_flare.flare == archive_value_for_flare + assert merged_pull_with_archive_flare._flare is None + assert merged_pull_with_archive_flare._flare_storage_path is not None + oldest_to_newest_pulls_with_archive_flare.append( + merged_pull_with_archive_flare.id + ) + + everything_in_archive_storage = mock_archive_storage.storage["archive"] + assert len(everything_in_archive_storage) == 5 + + task = FlareCleanupTask() + task.manual_run(limit=3) + + mock_logs.assert_has_calls( + [ + call("Starting FlareCleanupTask"), + call("FlareCleanupTask cleared 3 database flares"), + call("FlareCleanupTask cleared 3 Archive flares"), + ] + ) + + # there is a cache for flare on the object (all ArchiveFields have this), + # so get a fresh copy of each object without the cached value + should_be_cleared = oldest_to_newest_pulls_with_local_flare[:3] + should_not_be_cleared = oldest_to_newest_pulls_with_local_flare[3:] + for pull_id in should_be_cleared: + pull = Pull.objects.get(id=pull_id) + assert pull.flare == {} + assert pull._flare is None + assert pull._flare_storage_path is None + + for pull_id in should_not_be_cleared: + pull = Pull.objects.get(id=pull_id) + assert pull.flare == local_value_for_flare + assert pull._flare == local_value_for_flare + assert pull._flare_storage_path is None + + everything_in_archive_storage = mock_archive_storage.storage["archive"] + assert len(everything_in_archive_storage) == 2 + file_names_in_archive_storage = set(everything_in_archive_storage.keys()) + + should_be_cleared = oldest_to_newest_pulls_with_archive_flare[:3] + should_not_be_cleared = oldest_to_newest_pulls_with_archive_flare[3:] + for pull_id in should_be_cleared: + pull = Pull.objects.get(id=pull_id) + assert pull.flare == {} + assert pull._flare is None + assert pull._flare_storage_path is None + + for pull_id in should_not_be_cleared: + pull = Pull.objects.get(id=pull_id) + assert pull.flare == archive_value_for_flare + assert pull._flare is None + assert pull._flare_storage_path is not None + assert pull._flare_storage_path in file_names_in_archive_storage diff --git a/apps/worker/tasks/tests/unit/test_flush_repo.py b/apps/worker/tasks/tests/unit/test_flush_repo.py new file mode 100644 index 0000000000..8db8a8c600 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_flush_repo.py @@ -0,0 +1,140 @@ +import pytest +from shared.bundle_analysis import StoragePaths +from shared.django_apps.compare.models import CommitComparison, FlagComparison +from shared.django_apps.compare.tests.factories import ( + CommitComparisonFactory, + FlagComparisonFactory, +) +from shared.django_apps.core.models import Branch, Commit, Pull, Repository +from shared.django_apps.core.tests.factories import ( + BranchFactory, + CommitFactory, + PullFactory, + RepositoryFactory, +) +from shared.django_apps.reports.models import CommitReport, RepositoryFlag +from shared.django_apps.reports.models import ReportSession as Upload +from shared.django_apps.reports.tests.factories import ( + CommitReportFactory, + RepositoryFlagFactory, + UploadFactory, +) + +from services.archive import ArchiveService +from services.cleanup.utils import CleanupResult, CleanupSummary +from tasks.flush_repo import FlushRepoTask + + +@pytest.mark.django_db(databases=["timeseries", "default"]) +def test_flush_repo_nothing(mock_storage): + repo = RepositoryFactory() + + task = FlushRepoTask() + res = task.run_impl({}, repoid=repo.repoid) + + assert res == CleanupSummary( + CleanupResult(1), + { + Repository: CleanupResult(1), + }, + ) + + +@pytest.mark.django_db(databases=["timeseries", "default"]) +def test_flush_repo_few_of_each_only_db_objects(mock_storage): + repo = RepositoryFactory() + flag = RepositoryFlagFactory(repository=repo) + + for i in range(8): + CommitFactory(repository=repo) + + for i in range(4): + base_commit = CommitFactory(repository=repo) + head_commit = CommitFactory(repository=repo) + comparison = CommitComparisonFactory( + base_commit=base_commit, compare_commit=head_commit + ) + + FlagComparisonFactory(commit_comparison=comparison, repositoryflag=flag) + + # NOTE: The `CommitFactary` defaults to `branch: main, pullid: 1` + # This default seems to create models for + # `Pull` and `Branch` automatically through some kind of trigger? + + for i in range(17): + PullFactory(repository=repo, pullid=i + 100) + + for i in range(23): + BranchFactory(repository=repo) + + task = FlushRepoTask() + res = task.run_impl({}, repoid=repo.repoid) + + assert res == CleanupSummary( + CleanupResult(24 + 16 + 4 + 4 + 18 + 1 + 1), + { + Branch: CleanupResult(24), + Commit: CleanupResult(16), + CommitComparison: CleanupResult(4), + FlagComparison: CleanupResult(4), + Pull: CleanupResult(18), + Repository: CleanupResult(1), + RepositoryFlag: CleanupResult(1), + }, + ) + + +@pytest.mark.django_db(databases=["timeseries", "default"]) +def test_flush_repo_little_bit_of_everything(mocker, mock_storage): + repo = RepositoryFactory() + archive_service = ArchiveService(repo) + + for i in range(8): + # NOTE: `CommitWithReportFactory` exists, but its only usable from `api`, + # because of unresolved imports + commit = CommitFactory(repository=repo) + commit_report = CommitReportFactory(commit=commit) + upload = UploadFactory(report=commit_report, storage_path=f"upload{i}") + + archive_service.write_chunks(commit.commitid, f"chunks_data{i}") + archive_service.write_file(upload.storage_path, f"upload_data{i}") + + ba_report = CommitReportFactory(commit=commit, report_type="bundle_analysis") + ba_upload = UploadFactory(report=ba_report, storage_path=f"ba_upload{i}") + ba_report_path = StoragePaths.bundle_report.path( + repo_key=archive_service.storage_hash, report_key=ba_report.external_id + ) + archive_service.storage.write_file( + "bundle-analysis", ba_report_path, f"ba_report_data{i}" + ) + archive_service.storage.write_file( + "bundle-analysis", ba_upload.storage_path, f"ba_upload_data{i}" + ) + + for i in range(17): + PullFactory(repository=repo, pullid=i + 100) + + for i in range(23): + BranchFactory(repository=repo) + + archive = mock_storage.storage["archive"] + ba_archive = mock_storage.storage["bundle-analysis"] + assert len(archive) == 16 + assert len(ba_archive) == 16 + + task = FlushRepoTask() + res = task.run_impl({}, repoid=repo.repoid) + + assert res == CleanupSummary( + CleanupResult(24 + 8 + 16 + 18 + 1 + 16, 16 + 16), + { + Branch: CleanupResult(24), + Commit: CleanupResult(8), + CommitReport: CleanupResult(16, 16), + Pull: CleanupResult(18), + Repository: CleanupResult(1), + Upload: CleanupResult(16, 16), + }, + ) + assert len(archive) == 0 + assert len(ba_archive) == 0 diff --git a/apps/worker/tasks/tests/unit/test_ghm_sync_plans.py b/apps/worker/tasks/tests/unit/test_ghm_sync_plans.py new file mode 100644 index 0000000000..ba2672ff0c --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_ghm_sync_plans.py @@ -0,0 +1,170 @@ +from freezegun import freeze_time +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName + +from database.models import Owner, Repository +from database.tests.factories import OwnerFactory, RepositoryFactory +from tasks.github_marketplace import SyncPlansTask + + +class TestGHMarketplaceSyncPlansTaskUnit(object): + def test_create_or_update_to_free_plan_known_user(self, dbsession, mocker): + owner = OwnerFactory.create( + service="github", + plan=PlanName.GHM_PLAN_NAME.value, + plan_user_count=2, + plan_activated_users=[1, 2], + ) + dbsession.add(owner) + repo = RepositoryFactory.create( + private=True, service_id="12071992", activated=True, owner=owner + ) + dbsession.add(repo) + dbsession.flush() + + ghm_service = mocker.MagicMock(get_user=mocker.MagicMock()) + SyncPlansTask().create_or_update_to_free_plan( + dbsession, ghm_service, owner.service_id + ) + + assert not ghm_service.get_user.called + assert owner.plan == DEFAULT_FREE_PLAN + assert owner.plan_user_count == 1 + assert owner.plan_activated_users is None + + dbsession.commit() + # their repos should also be deactivated + repos = ( + dbsession.query(Repository) + .filter(Repository.ownerid == owner.ownerid) + .all() + ) + + for repo in repos: + assert repo.activated is False + + @freeze_time("2024-03-28T00:00:00") + def test_create_or_update_to_free_plan_unknown_user(self, dbsession, mocker): + service_id = "12345" + username = "tomcat" + name = "Tom Cat" + email = "tom@cat.com" + ghm_service = mocker.MagicMock( + get_user=mocker.MagicMock( + return_value=dict(login=username, name=name, email=email) + ) + ) + SyncPlansTask().create_or_update_to_free_plan( + dbsession, ghm_service, service_id + ) + + assert ghm_service.get_user.called + + owner = ( + dbsession.query(Owner) + .filter(Owner.service_id == service_id, Owner.service == "github") + .first() + ) + assert owner.username == username + assert owner.name == name + assert owner.email == email + assert owner.createstamp.isoformat() == "2024-03-28T00:00:00+00:00" + + def test_create_or_update_plan_known_user_with_plan(self, dbsession, mocker): + owner = OwnerFactory.create( + service="github", + plan=DEFAULT_FREE_PLAN, + plan_user_count=10, + plan_activated_users=[34123, 231, 2314212], + stripe_customer_id="cus_123", + stripe_subscription_id="sub_123", + ) + dbsession.add(owner) + repo = RepositoryFactory.create( + private=True, service_id="12071992", activated=True, owner=owner + ) + dbsession.add(repo) + dbsession.flush() + + stripe_mock = mocker.patch( + "tasks.github_marketplace.stripe.Subscription.cancel" + ) + ghm_service = mocker.MagicMock(get_user=mocker.MagicMock()) + SyncPlansTask().create_or_update_plan( + dbsession, ghm_service, owner.service_id, dict(unit_count=5) + ) + + assert not ghm_service.get_user.called + assert owner.plan == PlanName.GHM_PLAN_NAME.value + assert owner.plan_provider == "github" + assert owner.plan_auto_activate == True + assert owner.plan_activated_users is None + assert owner.plan_user_count == 5 + + # stripe subscription should be canceled but not customer id + stripe_mock.assert_called_with("sub_123", prorate=True) + assert owner.stripe_subscription_id is None + assert owner.stripe_customer_id == "cus_123" + + def test_create_or_update_plan_known_user_without_plan(self, dbsession, mocker): + owner = OwnerFactory.create( + service="github", + plan=None, + plan_user_count=None, + plan_activated_users=None, + stripe_customer_id=None, + stripe_subscription_id=None, + ) + dbsession.add(owner) + repo = RepositoryFactory.create( + private=True, service_id="12071992", activated=True, owner=owner + ) + dbsession.add(repo) + dbsession.flush() + + stripe_mock = mocker.patch( + "tasks.github_marketplace.stripe.Subscription.cancel" + ) + ghm_service = mocker.MagicMock(get_user=mocker.MagicMock()) + SyncPlansTask().create_or_update_plan( + dbsession, ghm_service, owner.service_id, dict(unit_count=5) + ) + + assert not ghm_service.get_user.called + assert owner.plan == PlanName.GHM_PLAN_NAME.value + assert owner.plan_provider == "github" + assert owner.plan_auto_activate == True + assert owner.plan_activated_users is None + assert owner.plan_user_count == 5 + + stripe_mock.assert_not_called() + assert owner.stripe_subscription_id is None + assert owner.stripe_customer_id is None + + def test_create_or_update_plan_unknown_user(self, dbsession, mocker): + service_id = "12345" + username = "tomcat" + name = "Tom Cat" + email = "tom@cat.com" + ghm_service = mocker.MagicMock( + get_user=mocker.MagicMock( + return_value=dict(login=username, name=name, email=email) + ) + ) + SyncPlansTask().create_or_update_plan( + dbsession, ghm_service, service_id, dict(unit_count=5) + ) + + assert ghm_service.get_user.called + + owner = ( + dbsession.query(Owner) + .filter(Owner.service_id == service_id, Owner.service == "github") + .first() + ) + assert owner.username == username + assert owner.name == name + assert owner.email == email + assert owner.plan == PlanName.GHM_PLAN_NAME.value + assert owner.plan_provider == "github" + assert owner.plan_auto_activate == True + assert owner.plan_user_count == 5 diff --git a/apps/worker/tasks/tests/unit/test_github_app_webhooks_check.py b/apps/worker/tasks/tests/unit/test_github_app_webhooks_check.py new file mode 100644 index 0000000000..85a0232193 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_github_app_webhooks_check.py @@ -0,0 +1,289 @@ +from datetime import datetime, timedelta +from unittest.mock import AsyncMock + +import pytest +from shared.torngit.exceptions import TorngitUnauthorizedError + +from tasks.github_app_webhooks_check import Github, GitHubAppWebhooksCheckTask + + +@pytest.fixture +def sample_deliveries(): + sample_deliveries = [ + # time filter: passes, because the `delivered_at` is updated below to be recent + # status filter: fails, because this was a successful delivery + # event filter: passes, because it's an installation event + { + "id": 17324040107, + "guid": "53c93580-7a6e-11ed-96c9-5e1ce3e5574e", + "delivered_at": "2022-12-12T22:42:59Z", + "redelivery": False, + "duration": 0.37, + "status": "OK", + "status_code": 200, + "event": "installation_repositories", + "action": "added", + "installation_id": None, + "repository_id": None, + "url": "", + }, + # time filter: fails, because the `delivered_at` is old and not updated below + # status filter: fails, because this was a successful delivery + # event filter: passes, because it's an installation event + { + "id": 17324018336, + "guid": "40d7f830-7a6e-11ed-8b90-0777e88b1858", + "delivered_at": "2022-12-12T22:42:30Z", + "redelivery": False, + "duration": 2.31, + "status": "OK", + "status_code": 200, + "event": "installation_repositories", + "action": "removed", + "installation_id": None, + "repository_id": None, + "url": "", + }, + # time filter: passes, because the `delivered_at` is updated below to be recent + # status filter: passes, because this was a failed delivery + # event filter: passes, because it's an installation even + { + "id": 17323292984, + "guid": "0498e8e0-7a6c-11ed-8834-c5eb5a4b102a", + "delivered_at": "2022-12-12T22:26:28Z", + "redelivery": False, + "duration": 0.69, + "status": "Invalid HTTP Response: 400", + "status_code": 400, + "event": "installation", + "action": "created", + "installation_id": None, + "repository_id": None, + "url": "", + }, + # time filter: fails, because the `delivered_at` is old and not updated below + # status filter: passes, because this was a failed delivery + # event filter: passes, because it's an installation even + { + "id": 17323228732, + "guid": "d41fa780-7a6b-11ed-8890-0619085a3f97", + "delivered_at": "2022-12-12T22:25:07Z", + "redelivery": False, + "duration": 0.74, + "status": "Invalid HTTP Response: 400", + "status_code": 400, + "event": "installation", + "action": "deleted", + "installation_id": None, + "repository_id": None, + "url": "", + }, + # time filter: passes, because the `delivered_at` is updated below to be recent + # status filter: passes, because this was a failed delivery + # event filter: fails, because it isn't an installation event + { + "id": 17323228732, + "guid": "d41fa780-7a6b-11ed-8890-0619085a3f97", + "delivered_at": "2022-12-12T22:25:07Z", + "redelivery": False, + "duration": 0.74, + "status": "Invalid HTTP Response: 400", + "status_code": 400, + "event": "unknown event", + "action": "deleted", + "installation_id": None, + "repository_id": None, + "url": "", + }, + # time filter: fails, because the `delivered_at` is old and not updated below + # status filter: fails, because this was a successful delivery + # event filter: fails, because it isn't an installation event + { + "id": 17323228732, + "guid": "d41fa780-7a6b-11ed-8890-0619085a3f97", + "delivered_at": "2022-12-12T22:25:07Z", + "redelivery": False, + "duration": 0.74, + "status": "Invalid HTTP Response: 400", + "status_code": 200, + "event": "unknown event", + "action": "deleted", + "installation_id": None, + "repository_id": None, + "url": "", + }, + ] + now = datetime.now() + few_hours_ago = now - timedelta(hours=6) + sample_deliveries[0]["delivered_at"] = few_hours_ago.strftime("%Y-%m-%dT%H:%M:%SZ") + sample_deliveries[2]["delivered_at"] = few_hours_ago.strftime("%Y-%m-%dT%H:%M:%SZ") + sample_deliveries[4]["delivered_at"] = few_hours_ago.strftime("%Y-%m-%dT%H:%M:%SZ") + return sample_deliveries + + +class TestGHAppWebhooksTask(object): + def test_get_min_seconds_interval_between_executions(self, dbsession): + assert isinstance( + GitHubAppWebhooksCheckTask.get_min_seconds_interval_between_executions(), + int, + ) + assert ( + GitHubAppWebhooksCheckTask.get_min_seconds_interval_between_executions() + > 17000 + ) + + def test_apply_time_filter(self, sample_deliveries): + deliveries_to_test_with = sample_deliveries[0:3] + # Fix time so the test doesn't break eventually + now = datetime.now() + few_hours_ago = now - timedelta(hours=6) + many_hours_ago_in_range = now - timedelta(hours=7, minutes=50) + many_hours_ago = now - timedelta(days=2) + deliveries_to_test_with[0]["delivered_at"] = few_hours_ago.strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + deliveries_to_test_with[1]["delivered_at"] = many_hours_ago.strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + deliveries_to_test_with[2]["delivered_at"] = many_hours_ago_in_range.strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + task = GitHubAppWebhooksCheckTask() + filtered_deliveries = list(task._apply_time_filter(deliveries_to_test_with)) + assert len(filtered_deliveries) == 2 + assert filtered_deliveries == [ + deliveries_to_test_with[0], + deliveries_to_test_with[2], + ] + + def test_apply_event_filter(self, sample_deliveries): + task = GitHubAppWebhooksCheckTask() + filtered_deliveries = list(task._apply_event_filter(sample_deliveries)) + assert len(filtered_deliveries) == 4 + assert filtered_deliveries == sample_deliveries[:4] + + def test_apply_status_filter(self, sample_deliveries): + task = GitHubAppWebhooksCheckTask() + filtered_deliveries = list(task._apply_status_filter(sample_deliveries)) + assert len(filtered_deliveries) == 3 + assert filtered_deliveries == sample_deliveries[2:5] + + @pytest.mark.asyncio + async def test_process_delivery_page(self, mocker, sample_deliveries): + gh_handler = mocker.MagicMock() + gh_handler.request_webhook_redelivery = AsyncMock(return_value=True) + task = GitHubAppWebhooksCheckTask() + ( + successful_redelivery_count, + redeliveries_requested, + ) = await task.process_delivery_page(gh_handler, sample_deliveries) + assert redeliveries_requested == 1 + assert successful_redelivery_count == 1 + + @pytest.mark.asyncio + async def test_request_redeliveries_return_early(self, mocker): + fake_redelivery = mocker.patch.object( + Github, + "request_webhook_redelivery", + return_value=True, + ) + task = GitHubAppWebhooksCheckTask() + assert await task.request_redeliveries(mocker.MagicMock(), []) == 0 + fake_redelivery.assert_not_called() + + def test_skip_check_if_enterprise(self, dbsession, mocker): + mock_is_enterprise = mocker.patch( + "tasks.github_app_webhooks_check.is_enterprise", return_value=True + ) + task = GitHubAppWebhooksCheckTask() + ans = task.run_cron_task(dbsession) + assert ans == dict(checked=False, reason="Enterprise env") + mock_is_enterprise.assert_called() + + def test_return_on_exception(self, dbsession, mocker): + def throw_exception(*args, **kwargs): + raise TorngitUnauthorizedError( + response_data="error error", message="error error" + ) + + fake_list_deliveries = mocker.patch.object( + Github, + "list_webhook_deliveries", + side_effect=throw_exception, + ) + fake_redelivery = mocker.patch.object( + Github, + "request_webhook_redelivery", + return_value=True, + ) + + fake_get_token = mocker.patch( + "tasks.github_app_webhooks_check.get_github_integration_token", + return_value="integration_jwt_token", + ) + task = GitHubAppWebhooksCheckTask() + ans = task.run_cron_task(dbsession) + assert ans == dict( + checked=False, + reason="Failed with exception. Ending task immediately", + exception=str( + TorngitUnauthorizedError( + response_data="error error", message="error error" + ) + ), + redeliveries_requested=0, + deliveries_processed=0, + successful_redeliveries=0, + pages_processed=0, + ) + fake_list_deliveries.assert_called() + fake_get_token.assert_called() + fake_redelivery.assert_not_called() + + def test_successful_run(self, dbsession, mocker, sample_deliveries): + fake_list_deliveries = mocker.patch.object( + Github, + "list_webhook_deliveries", + ) + fake_list_deliveries.return_value.__aiter__.return_value = [sample_deliveries] + + fake_get_token = mocker.patch( + "tasks.github_app_webhooks_check.get_github_integration_token", + return_value="integration_jwt_token", + ) + fake_redelivery = mocker.patch.object( + Github, + "request_webhook_redelivery", + return_value=True, + ) + task = GitHubAppWebhooksCheckTask() + ans = task.run_cron_task(dbsession) + assert ans == dict( + checked=True, + redeliveries_requested=1, + deliveries_processed=6, + successful_redeliveries=1, + pages_processed=1, + ) + fake_list_deliveries.assert_called() + fake_get_token.assert_called() + fake_redelivery.assert_called() + + def test_redelivery_counters(self, dbsession, mocker, sample_deliveries): + fake_list_deliveries = mocker.patch.object( + Github, + "list_webhook_deliveries", + ) + fake_list_deliveries.return_value.__aiter__.return_value = [sample_deliveries] + + fake_get_token = mocker.patch( + "tasks.github_app_webhooks_check.get_github_integration_token", + return_value="integration_jwt_token", + ) + fake_redelivery = mocker.patch.object( + Github, + "request_webhook_redelivery", + return_value=True, + ) + task = GitHubAppWebhooksCheckTask() + _ = task.run_cron_task(dbsession) diff --git a/apps/worker/tasks/tests/unit/test_healthcheck_task.py b/apps/worker/tasks/tests/unit/test_healthcheck_task.py new file mode 100644 index 0000000000..b4d2cedcb1 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_healthcheck_task.py @@ -0,0 +1,117 @@ +from unittest.mock import MagicMock + +from tasks.health_check import HealthCheckTask + + +class TestHealthCheckTask(object): + def test_get_queue_names_default(self, mock_configuration): + health_check_task = HealthCheckTask() + queue_names = health_check_task._get_all_queue_names_from_config() + assert queue_names == set(["celery", "enterprise_celery"]) + + def test_get_queue_names_some_config(self, mock_configuration): + mock_configuration.set_params( + { + "setup": { + "debug": True, + "tasks": { + "celery": {"default_queue": "custom_celery"}, + "notify": {"queue": "notify_queue"}, + "pulls": {"queue": "pulls_queue"}, + "synchronize": {"queue": "synchronize_queue"}, + "flush_repo": {"queue": "flush_repo_queue"}, + "comment": {}, + }, + } + } + ) + health_check_task = HealthCheckTask() + queue_names = health_check_task._get_all_queue_names_from_config() + assert queue_names == set( + [ + "custom_celery", + "enterprise_custom_celery", + "notify_queue", + "enterprise_notify_queue", + "pulls_queue", + "enterprise_pulls_queue", + "synchronize_queue", + "enterprise_synchronize_queue", + "flush_repo_queue", + "enterprise_flush_repo_queue", + ] + ) + + def test_get_redis_config_celery_broker(self, mocker, mock_configuration): + mock_redis_instance_from_url = mocker.patch( + "tasks.health_check.redis_service._get_redis_instance_from_url" + ) + mock_redis_connection = mocker.patch( + "tasks.health_check.redis_service.get_redis_connection" + ) + mock_configuration.set_params( + {"services": {"celery_broker": "redis://redis-celery-broker"}} + ) + health_check_task = HealthCheckTask() + health_check_task._get_correct_redis_connection() # should come from services.celery_broker + mock_redis_instance_from_url.assert_called_with("redis://redis-celery-broker") + mock_redis_connection.assert_not_called() + + def test_get_redis_no_celery_broker(self, mocker, mock_configuration): + mock_redis_instance_from_url = mocker.patch( + "tasks.health_check.redis_service._get_redis_instance_from_url" + ) + mock_redis_connection = mocker.patch( + "tasks.health_check.redis_service.get_redis_connection" + ) + mock_configuration.set_params( + {"services": {"redis_url": "redis://redis-celery-broker"}} + ) + health_check_task = HealthCheckTask() + health_check_task._get_correct_redis_connection() + mock_redis_instance_from_url.assert_not_called() + mock_redis_connection.assert_called() + + def test_run_impl(self, mocker, mock_redis, dbsession): + mock_metrics = mocker.patch("tasks.health_check.metrics.gauge") + mock_redis.llen.return_value = 10 + mock_redis.return_value = MagicMock() + health_check_task = HealthCheckTask() + health_check_task.run_cron_task(dbsession) + mock_metrics.assert_any_call("celery.queue.celery.len", 10) + mock_metrics.assert_any_call("celery.queue.enterprise_celery.len", 10) + + def test_run_impl_with_configs( + self, mocker, mock_redis, mock_configuration, dbsession + ): + mock_configuration.set_params( + { + "setup": { + "debug": True, + "tasks": { + "celery": {"default_queue": "custom_celery"}, + "notify": {"queue": "notify_queue"}, + "pulls": {"queue": "pulls_queue"}, + "synchronize": {"queue": "synchronize_queue"}, + "flush_repo": {"queue": "flush_repo_queue"}, + "comment": {}, + }, + } + } + ) + mock_metrics = mocker.patch("tasks.health_check.metrics.gauge") + mock_redis.llen.return_value = 10 + mock_redis.return_value = MagicMock() + health_check_task = HealthCheckTask() + health_check_task.run_cron_task(dbsession) + mock_metrics.assert_any_call("celery.queue.custom_celery.len", 10) + mock_metrics.assert_any_call("celery.queue.notify_queue.len", 10) + mock_metrics.assert_any_call("celery.queue.pulls_queue.len", 10) + mock_metrics.assert_any_call("celery.queue.synchronize_queue.len", 10) + mock_metrics.assert_any_call("celery.queue.flush_repo_queue.len", 10) + + def test_get_min_seconds_interval_between_executions(self, dbsession): + assert isinstance( + HealthCheckTask.get_min_seconds_interval_between_executions(), int + ) + assert HealthCheckTask.get_min_seconds_interval_between_executions() < 10 diff --git a/apps/worker/tasks/tests/unit/test_hourly_check.py b/apps/worker/tasks/tests/unit/test_hourly_check.py new file mode 100644 index 0000000000..d6b024153f --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_hourly_check.py @@ -0,0 +1,14 @@ +from tasks.hourly_check import HourlyCheckTask + + +class TestHourlyCheck(object): + def test_simple_case(self, dbsession): + task = HourlyCheckTask() + assert task.run_cron_task(dbsession) == {"checked": True} + + def test_get_min_seconds_interval_between_executions(self, dbsession): + assert isinstance( + HourlyCheckTask.get_min_seconds_interval_between_executions(), int + ) + # The specifics don't matter, but the number needs to be somewhat big + assert HourlyCheckTask.get_min_seconds_interval_between_executions() > 600 diff --git a/apps/worker/tasks/tests/unit/test_label_analysis.py b/apps/worker/tasks/tests/unit/test_label_analysis.py new file mode 100644 index 0000000000..5ebae7a286 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_label_analysis.py @@ -0,0 +1,964 @@ +import json + +import pytest +from mock import patch +from shared.reports.reportfile import ReportFile +from shared.reports.resources import Report +from shared.reports.types import CoverageDatapoint, LineSession, ReportLine + +from database.models.labelanalysis import LabelAnalysisRequest +from database.tests.factories import RepositoryFactory +from database.tests.factories.labelanalysis import LabelAnalysisRequestFactory +from database.tests.factories.staticanalysis import ( + StaticAnalysisSingleFileSnapshotFactory, + StaticAnalysisSuiteFactory, + StaticAnalysisSuiteFilepathFactory, +) +from services.report import ReportService +from services.static_analysis import StaticAnalysisComparisonService +from tasks.label_analysis import ( + LabelAnalysisRequestProcessingTask, + LabelAnalysisRequestState, +) + +sample_head_static_analysis_dict = { + "empty_lines": [2, 3, 11], + "warnings": [], + "filename": "source.py", + "functions": [ + { + "identifier": "some_function", + "start_line": 6, + "end_line": 10, + "code_hash": "e69c18eff7d24f8bad3370db87f64333", + "complexity_metrics": { + "conditions": 1, + "mccabe_cyclomatic_complexity": 2, + "returns": 1, + "max_nested_conditional": 1, + }, + } + ], + "hash": "84d371ab1c57d2349038ac3671428803", + "language": "python", + "number_lines": 11, + "statements": [ + ( + 1, + { + "line_surety_ancestorship": None, + "start_column": 0, + "line_hash": "55c30cf01e202728b6952e9cba304798", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 5, + { + "line_surety_ancestorship": None, + "start_column": 4, + "line_hash": "1d7be9f2145760a59513a4049fcd0d1c", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 6, + { + "line_surety_ancestorship": 5, + "start_column": 4, + "line_hash": "f802087a854c26782ee8d4ece7214425", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 7, + { + "line_surety_ancestorship": None, + "start_column": 8, + "line_hash": "6ae3393fa7880fe8a844c03256cac37b", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 8, + { + "line_surety_ancestorship": 6, + "start_column": 4, + "line_hash": "5b099d1822e9236c540a5701a657225e", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 9, + { + "line_surety_ancestorship": 8, + "start_column": 4, + "line_hash": "e5d4915bb7dddeb18f53dc9fde9a3064", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 10, + { + "line_surety_ancestorship": 9, + "start_column": 4, + "line_hash": "e70ce43136171575ee525375b10f91a1", + "len": 0, + "extra_connected_lines": (), + }, + ), + ], + "definition_lines": [(4, 6)], + "import_lines": [], +} + +sample_base_static_analysis_dict = { + "empty_lines": [2, 3, 11], + "warnings": [], + "filename": "source.py", + "functions": [ + { + "identifier": "some_function", + "start_line": 6, + "end_line": 10, + "code_hash": "e4b52b6da12184142fcd7ff2c8412662", + "complexity_metrics": { + "conditions": 1, + "mccabe_cyclomatic_complexity": 2, + "returns": 1, + "max_nested_conditional": 1, + }, + } + ], + "hash": "811d0016249a5b1400a685164e5295de", + "language": "python", + "number_lines": 11, + "statements": [ + ( + 1, + { + "line_surety_ancestorship": None, + "start_column": 0, + "line_hash": "55c30cf01e202728b6952e9cba304798", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 5, + { + "line_surety_ancestorship": None, + "start_column": 4, + "line_hash": "1d7be9f2145760a59513a4049fcd0d1c", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 6, + { + "line_surety_ancestorship": 5, + "start_column": 4, + "line_hash": "52f98812dca4687f18373b87433df695", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 7, + { + "line_surety_ancestorship": None, + "start_column": 8, + "line_hash": "6ae3393fa7880fe8a844c03256cac37b", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 8, + { + "line_surety_ancestorship": 7, + "start_column": 8, + "line_hash": "5b099d1822e9236c540a5701a657225e", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 9, + { + "line_surety_ancestorship": 6, + "start_column": 4, + "line_hash": "e5d4915bb7dddeb18f53dc9fde9a3064", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 10, + { + "line_surety_ancestorship": 9, + "start_column": 4, + "line_hash": "e70ce43136171575ee525375b10f91a1", + "len": 0, + "extra_connected_lines": (), + }, + ), + ], + "definition_lines": [(4, 6)], + "import_lines": [], +} + + +@pytest.fixture +def sample_report_with_labels(): + r = Report() + first_rf = ReportFile("source.py") + first_rf.append( + 5, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + ( + LineSession( + id=1, + coverage=1, + ) + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=1, + coverage=1, + coverage_type=None, + label_ids=["apple", "label_one", "pineapple", "banana"], + ) + ], + complexity=None, + ), + ) + first_rf.append( + 6, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + ( + LineSession( + id=1, + coverage=1, + ) + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=1, + coverage=1, + coverage_type=None, + label_ids=["label_one", "pineapple", "banana"], + ) + ], + complexity=None, + ), + ) + first_rf.append( + 7, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + ( + LineSession( + id=1, + coverage=1, + ) + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=1, + coverage=1, + coverage_type=None, + label_ids=["banana"], + ) + ], + complexity=None, + ), + ) + first_rf.append( + 8, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + ( + LineSession( + id=1, + coverage=1, + ) + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=1, + coverage=1, + coverage_type=None, + label_ids=["banana"], + ), + CoverageDatapoint( + sessionid=5, + coverage=1, + coverage_type=None, + label_ids=["orangejuice"], + ), + ], + complexity=None, + ), + ) + first_rf.append( + 99, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + ( + LineSession( + id=5, + coverage=1, + ) + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=5, + coverage=1, + coverage_type=None, + label_ids=["justjuice"], + ), + ], + complexity=None, + ), + ) + first_rf.append( + 8, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + ( + LineSession( + id=1, + coverage=1, + ) + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=1, + coverage=1, + coverage_type=None, + label_ids=["label_one", "pineapple", "banana"], + ), + CoverageDatapoint( + sessionid=5, + coverage=1, + coverage_type=None, + label_ids=["Th2dMtk4M_codecov", "applejuice"], + ), + ], + complexity=None, + ), + ) + second_rf = ReportFile("path/from/additionsonly.py") + second_rf.append( + 6, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + ( + LineSession( + id=1, + coverage=1, + ) + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=1, + coverage=1, + coverage_type=None, + label_ids=["whatever", "here"], + ) + ], + complexity=None, + ), + ) + random_rf = ReportFile("path/from/randomfile_no_static_analysis.html") + random_rf.append( + 1, + ReportLine.create( + coverage=1, + type=None, + sessions=[(LineSession(id=1, coverage=1))], + datapoints=None, + complexity=None, + ), + ) + r.append(first_rf) + r.append(second_rf) + r.append(random_rf) + + return r + + +def test_simple_call_without_requested_labels_then_with_requested_labels( + dbsession, mock_storage, mocker, sample_report_with_labels, mock_repo_provider +): + mock_metrics = mocker.patch("tasks.label_analysis.metrics") + mocker.patch.object( + LabelAnalysisRequestProcessingTask, + "_get_lines_relevant_to_diff", + return_value={ + "all": False, + "files": {"source.py": {"all": False, "lines": {8, 6}}}, + }, + ) + mocker.patch.object( + ReportService, + "get_existing_report_for_commit", + return_value=sample_report_with_labels, + ) + repository = RepositoryFactory.create() + larf = LabelAnalysisRequestFactory.create( + base_commit__repository=repository, head_commit__repository=repository + ) + dbsession.add(larf) + dbsession.flush() + base_sasf = StaticAnalysisSuiteFactory.create(commit=larf.base_commit) + head_sasf = StaticAnalysisSuiteFactory.create(commit=larf.head_commit) + dbsession.add(base_sasf) + dbsession.add(head_sasf) + dbsession.flush() + first_path = "abdkasdauchudh.txt" + second_path = "0diao9u3qdsdu.txt" + mock_storage.write_file( + "archive", + first_path, + json.dumps(sample_base_static_analysis_dict), + ) + mock_storage.write_file( + "archive", + second_path, + json.dumps(sample_head_static_analysis_dict), + ) + first_snapshot = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository, content_location=first_path + ) + second_snapshot = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository, content_location=second_path + ) + dbsession.add(first_snapshot) + dbsession.add(second_snapshot) + dbsession.flush() + first_base_file = StaticAnalysisSuiteFilepathFactory.create( + file_snapshot=first_snapshot, + analysis_suite=base_sasf, + filepath="source.py", + ) + first_head_file = StaticAnalysisSuiteFilepathFactory.create( + file_snapshot=second_snapshot, + analysis_suite=head_sasf, + filepath="source.py", + ) + dbsession.add(first_base_file) + dbsession.add(first_head_file) + dbsession.flush() + + task = LabelAnalysisRequestProcessingTask() + res = task.run_impl(dbsession, larf.id) + expected_present_report_labels = [ + "apple", + "applejuice", + "banana", + "here", + "justjuice", + "label_one", + "orangejuice", + "pineapple", + "whatever", + ] + expected_present_diff_labels = sorted( + ["applejuice", "banana", "label_one", "orangejuice", "pineapple"] + ) + expected_result = { + "absent_labels": [], + "present_diff_labels": expected_present_diff_labels, + "present_report_labels": expected_present_report_labels, + "global_level_labels": ["applejuice", "justjuice", "orangejuice"], + "success": True, + "errors": [], + } + assert res == expected_result + mock_metrics.incr.assert_called_with("label_analysis_task.success") + dbsession.flush() + dbsession.refresh(larf) + assert larf.state_id == LabelAnalysisRequestState.FINISHED.db_id + assert larf.result == { + "absent_labels": [], + "present_diff_labels": expected_present_diff_labels, + "present_report_labels": expected_present_report_labels, + "global_level_labels": ["applejuice", "justjuice", "orangejuice"], + } + # Now we call the task again, this time with the requested labels. + # This illustrates what should happen if we patch the labels after calculating + # And trigger the task again to save the new results + larf.requested_labels = ["tangerine", "pear", "banana", "apple"] + dbsession.flush() + res = task.run_impl(dbsession, larf.id) + expected_present_diff_labels = ["banana"] + expected_present_report_labels = ["apple", "banana"] + expected_absent_labels = ["pear", "tangerine"] + assert res == { + "absent_labels": expected_absent_labels, + "present_diff_labels": expected_present_diff_labels, + "present_report_labels": expected_present_report_labels, + "success": True, + "global_level_labels": [], + "errors": [], + } + assert larf.result == { + "absent_labels": expected_absent_labels, + "present_diff_labels": expected_present_diff_labels, + "present_report_labels": expected_present_report_labels, + "global_level_labels": [], + } + mock_metrics.incr.assert_called_with( + "label_analysis_task.already_calculated.new_result" + ) + + +def test_simple_call_with_requested_labels( + dbsession, mock_storage, mocker, sample_report_with_labels, mock_repo_provider +): + mock_metrics = mocker.patch("tasks.label_analysis.metrics") + mocker.patch.object( + LabelAnalysisRequestProcessingTask, + "_get_lines_relevant_to_diff", + return_value={ + "all": False, + "files": {"source.py": {"all": False, "lines": {8, 6}}}, + }, + ) + mocker.patch.object( + ReportService, + "get_existing_report_for_commit", + return_value=sample_report_with_labels, + ) + larf = LabelAnalysisRequestFactory.create( + requested_labels=["tangerine", "pear", "banana", "apple"] + ) + dbsession.add(larf) + dbsession.flush() + task = LabelAnalysisRequestProcessingTask() + res = task.run_impl(dbsession, larf.id) + expected_present_diff_labels = ["banana"] + expected_present_report_labels = ["apple", "banana"] + expected_absent_labels = ["pear", "tangerine"] + assert res == { + "absent_labels": expected_absent_labels, + "present_diff_labels": expected_present_diff_labels, + "present_report_labels": expected_present_report_labels, + "success": True, + "global_level_labels": [], + "errors": [], + } + dbsession.flush() + dbsession.refresh(larf) + assert larf.state_id == LabelAnalysisRequestState.FINISHED.db_id + assert larf.result == { + "absent_labels": expected_absent_labels, + "present_diff_labels": expected_present_diff_labels, + "present_report_labels": expected_present_report_labels, + "global_level_labels": [], + } + mock_metrics.incr.assert_called_with("label_analysis_task.success") + + +def test_get_requested_labels(dbsession, mocker): + larf = LabelAnalysisRequestFactory.create(requested_labels=[]) + + def side_effect(*args, **kwargs): + larf.requested_labels = ["tangerine", "pear", "banana", "apple"] + + mock_refresh = mocker.patch.object(dbsession, "refresh", side_effect=side_effect) + dbsession.add(larf) + dbsession.flush() + task = LabelAnalysisRequestProcessingTask() + task.dbsession = dbsession + labels = task._get_requested_labels(larf) + mock_refresh.assert_called() + assert labels == ["tangerine", "pear", "banana", "apple"] + + +def test_call_label_analysis_no_request_object(dbsession, mocker): + task = LabelAnalysisRequestProcessingTask() + mock_metrics = mocker.patch("tasks.label_analysis.metrics") + res = task.run_impl(db_session=dbsession, request_id=-1) + assert res == { + "success": False, + "present_report_labels": [], + "present_diff_labels": [], + "absent_labels": [], + "global_level_labels": [], + "errors": [ + { + "error_code": "not found", + "error_params": { + "extra": {}, + "message": "LabelAnalysisRequest not found", + }, + } + ], + } + mock_metrics.incr.assert_called_with( + "label_analysis_task.failed_to_calculate.larq_not_found" + ) + + +def test_get_executable_lines_labels_all_labels(sample_report_with_labels): + executable_lines = {"all": True} + task = LabelAnalysisRequestProcessingTask() + assert task.get_executable_lines_labels( + sample_report_with_labels, executable_lines + ) == ( + { + "banana", + "justjuice", + "here", + "pineapple", + "applejuice", + "apple", + "whatever", + "label_one", + "orangejuice", + }, + set(), + ) + assert task.get_executable_lines_labels( + sample_report_with_labels, executable_lines + ) == (task.get_all_report_labels(sample_report_with_labels), set()) + + +def test_get_executable_lines_labels_all_labels_in_one_file(sample_report_with_labels): + executable_lines = {"all": False, "files": {"source.py": {"all": True}}} + task = LabelAnalysisRequestProcessingTask() + assert task.get_executable_lines_labels( + sample_report_with_labels, executable_lines + ) == ( + { + "apple", + "justjuice", + "applejuice", + "label_one", + "banana", + "orangejuice", + "pineapple", + }, + {"orangejuice", "justjuice", "applejuice"}, + ) + + +def test_get_executable_lines_labels_some_labels_in_one_file(sample_report_with_labels): + executable_lines = { + "all": False, + "files": {"source.py": {"all": False, "lines": set([5, 6])}}, + } + task = LabelAnalysisRequestProcessingTask() + assert task.get_executable_lines_labels( + sample_report_with_labels, executable_lines + ) == ( + {"apple", "label_one", "pineapple", "banana"}, + set(), + ) + + +def test_get_executable_lines_labels_some_labels_in_one_file_with_globals( + sample_report_with_labels, +): + executable_lines = { + "all": False, + "files": {"source.py": {"all": False, "lines": set([6, 8])}}, + } + task = LabelAnalysisRequestProcessingTask() + assert task.get_executable_lines_labels( + sample_report_with_labels, executable_lines + ) == ( + {"label_one", "pineapple", "banana", "orangejuice", "applejuice"}, + {"applejuice", "justjuice", "orangejuice"}, + ) + + +def test_get_executable_lines_labels_some_labels_in_one_file_other_null( + sample_report_with_labels, +): + executable_lines = { + "all": False, + "files": { + "source.py": {"all": False, "lines": set([5, 6])}, + "path/from/randomfile_no_static_analysis.html": None, + }, + } + task = LabelAnalysisRequestProcessingTask() + assert task.get_executable_lines_labels( + sample_report_with_labels, executable_lines + ) == ( + {"apple", "label_one", "pineapple", "banana"}, + set(), + ) + + +def test_get_all_labels_one_session(sample_report_with_labels): + task = LabelAnalysisRequestProcessingTask() + assert task.get_labels_per_session(sample_report_with_labels, 1) == { + "apple", + "banana", + "here", + "label_one", + "pineapple", + "whatever", + } + assert task.get_labels_per_session(sample_report_with_labels, 2) == set() + assert task.get_labels_per_session(sample_report_with_labels, 5) == { + "orangejuice", + "justjuice", + "applejuice", + } + + +def test_get_relevant_executable_lines_nothing_found(dbsession, mocker): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + larf = LabelAnalysisRequestFactory.create( + base_commit__repository=repository, head_commit__repository=repository + ) + dbsession.add(larf) + dbsession.flush() + task = LabelAnalysisRequestProcessingTask() + task.errors = [] + task.dbsession = dbsession + parsed_git_diff = [] + assert task.get_relevant_executable_lines(larf, parsed_git_diff) is None + + +def test_get_relevant_executable_lines_with_static_analyses(dbsession, mocker): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + larf = LabelAnalysisRequestFactory.create( + base_commit__repository=repository, head_commit__repository=repository + ) + dbsession.add(larf) + dbsession.flush() + base_sasf = StaticAnalysisSuiteFactory.create(commit=larf.base_commit) + head_sasf = StaticAnalysisSuiteFactory.create(commit=larf.head_commit) + dbsession.add(base_sasf) + dbsession.add(head_sasf) + dbsession.flush() + task = LabelAnalysisRequestProcessingTask() + parsed_git_diff = [] + mocked_res = mocker.patch.object( + StaticAnalysisComparisonService, "get_base_lines_relevant_to_change" + ) + assert ( + task.get_relevant_executable_lines(larf, parsed_git_diff) + == mocked_res.return_value + ) + + +def test_run_impl_with_error( + dbsession, mock_storage, mocker, sample_report_with_labels, mock_repo_provider +): + mock_metrics = mocker.patch("tasks.label_analysis.metrics") + mocker.patch.object( + LabelAnalysisRequestProcessingTask, + "_get_lines_relevant_to_diff", + side_effect=Exception("Oh no"), + ) + larf = LabelAnalysisRequestFactory.create( + requested_labels=["tangerine", "pear", "banana", "apple"] + ) + dbsession.add(larf) + dbsession.flush() + task = LabelAnalysisRequestProcessingTask() + res = task.run_impl(dbsession, larf.id) + expected_result = { + "absent_labels": [], + "present_diff_labels": [], + "present_report_labels": [], + "success": False, + "global_level_labels": [], + "errors": [ + { + "error_code": "failed", + "error_params": {"extra": {}, "message": "Failed to calculate"}, + } + ], + } + assert res == expected_result + dbsession.flush() + dbsession.refresh(larf) + assert larf.state_id == LabelAnalysisRequestState.ERROR.db_id + assert larf.result is None + mock_metrics.incr.assert_called_with( + "label_analysis_task.failed_to_calculate.exception" + ) + + +def test_calculate_result_no_report( + dbsession, mock_storage, mocker, sample_report_with_labels, mock_repo_provider +): + mock_metrics = mocker.patch("tasks.label_analysis.metrics") + larf: LabelAnalysisRequest = LabelAnalysisRequestFactory.create( + # This being not-ordered is important in the test + # TO make sure we go through the warning at the bottom of run_impl + requested_labels=["tangerine", "pear", "banana", "apple"] + ) + dbsession.add(larf) + dbsession.flush() + mocker.patch.object( + ReportService, + "get_existing_report_for_commit", + return_value=None, + ) + mocker.patch.object( + LabelAnalysisRequestProcessingTask, + "_get_lines_relevant_to_diff", + return_value=(set(), set(), set()), + ) + task = LabelAnalysisRequestProcessingTask() + res = task.run_impl(dbsession, larf.id) + assert res == { + "success": True, + "absent_labels": larf.requested_labels, + "present_diff_labels": [], + "present_report_labels": [], + "global_level_labels": [], + "errors": [ + { + "error_code": "missing data", + "error_params": { + "extra": { + "base_commit": larf.base_commit.commitid, + "head_commit": larf.head_commit.commitid, + }, + "message": "Missing base report", + }, + } + ], + } + mock_metrics.incr.assert_called_with( + "label_analysis_task.failed_to_calculate.missing_info" + ) + + +@patch("tasks.label_analysis.parse_git_diff_json", return_value=["parsed_git_diff"]) +def test__get_parsed_git_diff(mock_parse_diff, dbsession, mock_repo_provider): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + larq = LabelAnalysisRequestFactory.create( + base_commit__repository=repository, head_commit__repository=repository + ) + dbsession.add(larq) + dbsession.flush() + mock_repo_provider.get_compare.return_value = {"diff": "json"} + task = LabelAnalysisRequestProcessingTask() + task.errors = [] + parsed_diff = task._get_parsed_git_diff(larq) + assert parsed_diff == ["parsed_git_diff"] + mock_parse_diff.assert_called_with({"diff": "json"}) + mock_repo_provider.get_compare.assert_called_with( + larq.base_commit.commitid, larq.head_commit.commitid + ) + + +@patch("tasks.label_analysis.parse_git_diff_json", return_value=["parsed_git_diff"]) +def test__get_parsed_git_diff_error(mock_parse_diff, dbsession, mock_repo_provider): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + larq = LabelAnalysisRequestFactory.create( + base_commit__repository=repository, head_commit__repository=repository + ) + dbsession.add(larq) + dbsession.flush() + mock_repo_provider.get_compare.side_effect = Exception("Oh no") + task = LabelAnalysisRequestProcessingTask() + task.errors = [] + task.dbsession = dbsession + parsed_diff = task._get_parsed_git_diff(larq) + assert parsed_diff is None + mock_parse_diff.assert_not_called() + mock_repo_provider.get_compare.assert_called_with( + larq.base_commit.commitid, larq.head_commit.commitid + ) + + +@patch( + "tasks.label_analysis.LabelAnalysisRequestProcessingTask.get_relevant_executable_lines", + return_value=[{"all": False, "files": {}}], +) +@patch( + "tasks.label_analysis.LabelAnalysisRequestProcessingTask._get_parsed_git_diff", + return_value=["parsed_git_diff"], +) +def test__get_lines_relevant_to_diff( + mock_parse_diff, mock_get_relevant_lines, dbsession +): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + larq = LabelAnalysisRequestFactory.create( + base_commit__repository=repository, head_commit__repository=repository + ) + dbsession.add(larq) + dbsession.flush() + task = LabelAnalysisRequestProcessingTask() + lines = task._get_lines_relevant_to_diff(larq) + assert lines == [{"all": False, "files": {}}] + mock_parse_diff.assert_called_with(larq) + mock_get_relevant_lines.assert_called_with(larq, ["parsed_git_diff"]) + + +@patch( + "tasks.label_analysis.LabelAnalysisRequestProcessingTask.get_relevant_executable_lines" +) +@patch( + "tasks.label_analysis.LabelAnalysisRequestProcessingTask._get_parsed_git_diff", + return_value=None, +) +def test__get_lines_relevant_to_diff_error( + mock_parse_diff, mock_get_relevant_lines, dbsession +): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + larq = LabelAnalysisRequestFactory.create( + base_commit__repository=repository, head_commit__repository=repository + ) + dbsession.add(larq) + dbsession.flush() + task = LabelAnalysisRequestProcessingTask() + lines = task._get_lines_relevant_to_diff(larq) + assert lines is None + mock_parse_diff.assert_called_with(larq) + mock_get_relevant_lines.assert_not_called() diff --git a/apps/worker/tasks/tests/unit/test_label_analysis_encoded_labels.py b/apps/worker/tasks/tests/unit/test_label_analysis_encoded_labels.py new file mode 100644 index 0000000000..56d78984ca --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_label_analysis_encoded_labels.py @@ -0,0 +1,1010 @@ +import json + +import pytest +from mock import MagicMock, patch +from shared.reports.reportfile import ReportFile +from shared.reports.resources import Report +from shared.reports.types import CoverageDatapoint, LineSession, ReportLine + +from database.models.labelanalysis import LabelAnalysisRequest +from database.tests.factories import RepositoryFactory +from database.tests.factories.core import ReportFactory +from database.tests.factories.labelanalysis import LabelAnalysisRequestFactory +from database.tests.factories.staticanalysis import ( + StaticAnalysisSingleFileSnapshotFactory, + StaticAnalysisSuiteFactory, + StaticAnalysisSuiteFilepathFactory, +) +from helpers.labels import SpecialLabelsEnum +from services.report import ReportService +from services.static_analysis import StaticAnalysisComparisonService +from tasks.label_analysis import ( + ExistingLabelSetsNotEncoded, + LabelAnalysisRequestProcessingTask, + LabelAnalysisRequestState, +) + +sample_head_static_analysis_dict = { + "empty_lines": [2, 3, 11], + "warnings": [], + "filename": "source.py", + "functions": [ + { + "identifier": "some_function", + "start_line": 6, + "end_line": 10, + "code_hash": "e69c18eff7d24f8bad3370db87f64333", + "complexity_metrics": { + "conditions": 1, + "mccabe_cyclomatic_complexity": 2, + "returns": 1, + "max_nested_conditional": 1, + }, + } + ], + "hash": "84d371ab1c57d2349038ac3671428803", + "language": "python", + "number_lines": 11, + "statements": [ + ( + 1, + { + "line_surety_ancestorship": None, + "start_column": 0, + "line_hash": "55c30cf01e202728b6952e9cba304798", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 5, + { + "line_surety_ancestorship": None, + "start_column": 4, + "line_hash": "1d7be9f2145760a59513a4049fcd0d1c", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 6, + { + "line_surety_ancestorship": 5, + "start_column": 4, + "line_hash": "f802087a854c26782ee8d4ece7214425", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 7, + { + "line_surety_ancestorship": None, + "start_column": 8, + "line_hash": "6ae3393fa7880fe8a844c03256cac37b", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 8, + { + "line_surety_ancestorship": 6, + "start_column": 4, + "line_hash": "5b099d1822e9236c540a5701a657225e", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 9, + { + "line_surety_ancestorship": 8, + "start_column": 4, + "line_hash": "e5d4915bb7dddeb18f53dc9fde9a3064", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 10, + { + "line_surety_ancestorship": 9, + "start_column": 4, + "line_hash": "e70ce43136171575ee525375b10f91a1", + "len": 0, + "extra_connected_lines": (), + }, + ), + ], + "definition_lines": [(4, 6)], + "import_lines": [], +} + +sample_base_static_analysis_dict = { + "empty_lines": [2, 3, 11], + "warnings": [], + "filename": "source.py", + "functions": [ + { + "identifier": "some_function", + "start_line": 6, + "end_line": 10, + "code_hash": "e4b52b6da12184142fcd7ff2c8412662", + "complexity_metrics": { + "conditions": 1, + "mccabe_cyclomatic_complexity": 2, + "returns": 1, + "max_nested_conditional": 1, + }, + } + ], + "hash": "811d0016249a5b1400a685164e5295de", + "language": "python", + "number_lines": 11, + "statements": [ + ( + 1, + { + "line_surety_ancestorship": None, + "start_column": 0, + "line_hash": "55c30cf01e202728b6952e9cba304798", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 5, + { + "line_surety_ancestorship": None, + "start_column": 4, + "line_hash": "1d7be9f2145760a59513a4049fcd0d1c", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 6, + { + "line_surety_ancestorship": 5, + "start_column": 4, + "line_hash": "52f98812dca4687f18373b87433df695", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 7, + { + "line_surety_ancestorship": None, + "start_column": 8, + "line_hash": "6ae3393fa7880fe8a844c03256cac37b", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 8, + { + "line_surety_ancestorship": 7, + "start_column": 8, + "line_hash": "5b099d1822e9236c540a5701a657225e", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 9, + { + "line_surety_ancestorship": 6, + "start_column": 4, + "line_hash": "e5d4915bb7dddeb18f53dc9fde9a3064", + "len": 0, + "extra_connected_lines": (), + }, + ), + ( + 10, + { + "line_surety_ancestorship": 9, + "start_column": 4, + "line_hash": "e70ce43136171575ee525375b10f91a1", + "len": 0, + "extra_connected_lines": (), + }, + ), + ], + "definition_lines": [(4, 6)], + "import_lines": [], +} + + +@pytest.fixture +def sample_report_with_labels(): + r = Report() + report_labels_index = { + 0: SpecialLabelsEnum.CODECOV_ALL_LABELS_PLACEHOLDER.corresponding_label, + 1: "apple", + 2: "label_one", + 3: "pineapple", + 4: "banana", + 5: "orangejuice", + 6: "justjuice", + 7: "whatever", + 8: "here", + 9: "applejuice", + } + first_rf = ReportFile("source.py") + first_rf.append( + 5, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + ( + LineSession( + id=1, + coverage=1, + ) + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=1, + coverage=1, + coverage_type=None, + label_ids=[1, 2, 3, 4], + ) + ], + complexity=None, + ), + ) + first_rf.append( + 6, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + ( + LineSession( + id=1, + coverage=1, + ) + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=1, + coverage=1, + coverage_type=None, + label_ids=[2, 3, 4], + ) + ], + complexity=None, + ), + ) + first_rf.append( + 7, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + ( + LineSession( + id=1, + coverage=1, + ) + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=1, + coverage=1, + coverage_type=None, + label_ids=[4], + ) + ], + complexity=None, + ), + ) + first_rf.append( + 8, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + ( + LineSession( + id=1, + coverage=1, + ) + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=1, + coverage=1, + coverage_type=None, + label_ids=[4], + ), + CoverageDatapoint( + sessionid=5, + coverage=1, + coverage_type=None, + label_ids=[5], + ), + ], + complexity=None, + ), + ) + first_rf.append( + 99, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + ( + LineSession( + id=5, + coverage=1, + ) + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=5, + coverage=1, + coverage_type=None, + label_ids=[6], + ), + ], + complexity=None, + ), + ) + first_rf.append( + 8, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + ( + LineSession( + id=1, + coverage=1, + ) + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=1, + coverage=1, + coverage_type=None, + label_ids=[2, 3, 4], + ), + CoverageDatapoint( + sessionid=5, + coverage=1, + coverage_type=None, + label_ids=[0, 9], + ), + ], + complexity=None, + ), + ) + second_rf = ReportFile("path/from/additionsonly.py") + second_rf.append( + 6, + ReportLine.create( + coverage=1, + type=None, + sessions=[ + ( + LineSession( + id=1, + coverage=1, + ) + ) + ], + datapoints=[ + CoverageDatapoint( + sessionid=1, + coverage=1, + coverage_type=None, + label_ids=[7, 8], + ) + ], + complexity=None, + ), + ) + random_rf = ReportFile("path/from/randomfile_no_static_analysis.html") + random_rf.append( + 1, + ReportLine.create( + coverage=1, + type=None, + sessions=[(LineSession(id=1, coverage=1))], + datapoints=None, + complexity=None, + ), + ) + r.append(first_rf) + r.append(second_rf) + r.append(random_rf) + r.labels_index = report_labels_index + return r + + +def test_simple_call_without_requested_labels_then_with_requested_labels( + dbsession, mock_storage, mocker, sample_report_with_labels, mock_repo_provider +): + mock_metrics = mocker.patch("tasks.label_analysis.metrics") + mocker.patch.object( + LabelAnalysisRequestProcessingTask, + "_get_lines_relevant_to_diff", + return_value={ + "all": False, + "files": {"source.py": {"all": False, "lines": {8, 6}}}, + }, + ) + mocker.patch.object( + ReportService, + "get_existing_report_for_commit", + return_value=sample_report_with_labels, + ) + repository = RepositoryFactory.create() + larf = LabelAnalysisRequestFactory.create( + base_commit__repository=repository, head_commit__repository=repository + ) + dbsession.add(larf) + dbsession.flush() + base_sasf = StaticAnalysisSuiteFactory.create(commit=larf.base_commit) + head_sasf = StaticAnalysisSuiteFactory.create(commit=larf.head_commit) + dbsession.add(base_sasf) + dbsession.add(head_sasf) + dbsession.flush() + first_path = "abdkasdauchudh.txt" + second_path = "0diao9u3qdsdu.txt" + mock_storage.write_file( + "archive", + first_path, + json.dumps(sample_base_static_analysis_dict), + ) + mock_storage.write_file( + "archive", + second_path, + json.dumps(sample_head_static_analysis_dict), + ) + first_snapshot = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository, content_location=first_path + ) + second_snapshot = StaticAnalysisSingleFileSnapshotFactory.create( + repository=repository, content_location=second_path + ) + dbsession.add(first_snapshot) + dbsession.add(second_snapshot) + dbsession.flush() + first_base_file = StaticAnalysisSuiteFilepathFactory.create( + file_snapshot=first_snapshot, + analysis_suite=base_sasf, + filepath="source.py", + ) + first_head_file = StaticAnalysisSuiteFilepathFactory.create( + file_snapshot=second_snapshot, + analysis_suite=head_sasf, + filepath="source.py", + ) + dbsession.add(first_base_file) + dbsession.add(first_head_file) + dbsession.flush() + + task = LabelAnalysisRequestProcessingTask() + assert sample_report_with_labels.labels_index is not None + res = task.run_impl(dbsession, larf.id) + expected_present_report_labels = [ + "apple", + "applejuice", + "banana", + "here", + "justjuice", + "label_one", + "orangejuice", + "pineapple", + "whatever", + ] + expected_present_diff_labels = sorted( + ["applejuice", "banana", "label_one", "orangejuice", "pineapple"] + ) + expected_result = { + "absent_labels": [], + "present_diff_labels": expected_present_diff_labels, + "present_report_labels": expected_present_report_labels, + "global_level_labels": ["applejuice", "justjuice", "orangejuice"], + "success": True, + "errors": [], + } + assert res == expected_result + mock_metrics.incr.assert_called_with("label_analysis_task.success") + # It's zero because the report has the _labels_index already + dbsession.flush() + dbsession.refresh(larf) + assert larf.state_id == LabelAnalysisRequestState.FINISHED.db_id + assert larf.result == { + "absent_labels": [], + "present_diff_labels": expected_present_diff_labels, + "present_report_labels": expected_present_report_labels, + "global_level_labels": ["applejuice", "justjuice", "orangejuice"], + } + # Now we call the task again, this time with the requested labels. + # This illustrates what should happen if we patch the labels after calculating + # And trigger the task again to save the new results + larf.requested_labels = ["tangerine", "pear", "banana", "apple"] + dbsession.flush() + res = task.run_impl(dbsession, larf.id) + expected_present_diff_labels = ["banana"] + expected_present_report_labels = ["apple", "banana"] + expected_absent_labels = ["pear", "tangerine"] + assert res == { + "absent_labels": expected_absent_labels, + "present_diff_labels": expected_present_diff_labels, + "present_report_labels": expected_present_report_labels, + "success": True, + "global_level_labels": [], + "errors": [], + } + assert larf.result == { + "absent_labels": expected_absent_labels, + "present_diff_labels": expected_present_diff_labels, + "present_report_labels": expected_present_report_labels, + "global_level_labels": [], + } + mock_metrics.incr.assert_called_with( + "label_analysis_task.already_calculated.new_result" + ) + mock_metrics.incr.assert_called_with( + "label_analysis_task.already_calculated.new_result" + ) + + +def test_simple_call_with_requested_labels( + dbsession, mock_storage, mocker, sample_report_with_labels, mock_repo_provider +): + mock_metrics = mocker.patch("tasks.label_analysis.metrics") + mocker.patch.object( + LabelAnalysisRequestProcessingTask, + "_get_lines_relevant_to_diff", + return_value={ + "all": False, + "files": {"source.py": {"all": False, "lines": {8, 6}}}, + }, + ) + mocker.patch.object( + ReportService, + "get_existing_report_for_commit", + return_value=sample_report_with_labels, + ) + larf = LabelAnalysisRequestFactory.create( + requested_labels=["tangerine", "pear", "banana", "apple"] + ) + ReportFactory(commit=larf.base_commit) + dbsession.add(larf) + dbsession.flush() + task = LabelAnalysisRequestProcessingTask() + res = task.run_impl(dbsession, larf.id) + expected_present_diff_labels = ["banana"] + expected_present_report_labels = ["apple", "banana"] + expected_absent_labels = ["pear", "tangerine"] + assert res == { + "absent_labels": expected_absent_labels, + "present_diff_labels": expected_present_diff_labels, + "present_report_labels": expected_present_report_labels, + "success": True, + "global_level_labels": [], + "errors": [], + } + dbsession.flush() + dbsession.refresh(larf) + assert larf.state_id == LabelAnalysisRequestState.FINISHED.db_id + assert larf.result == { + "absent_labels": expected_absent_labels, + "present_diff_labels": expected_present_diff_labels, + "present_report_labels": expected_present_report_labels, + "global_level_labels": [], + } + mock_metrics.incr.assert_called_with("label_analysis_task.success") + mock_metrics.incr.assert_called_with("label_analysis_task.success") + + +def test_get_requested_labels(dbsession, mocker): + larf = LabelAnalysisRequestFactory.create(requested_labels=[]) + + def side_effect(*args, **kwargs): + larf.requested_labels = ["tangerine", "pear", "banana", "apple"] + + mock_refresh = mocker.patch.object(dbsession, "refresh", side_effect=side_effect) + dbsession.add(larf) + dbsession.flush() + task = LabelAnalysisRequestProcessingTask() + task.dbsession = dbsession + labels = task._get_requested_labels(larf) + mock_refresh.assert_called() + assert labels == ["tangerine", "pear", "banana", "apple"] + + +def test_call_label_analysis_no_request_object(dbsession, mocker): + task = LabelAnalysisRequestProcessingTask() + mock_metrics = mocker.patch("tasks.label_analysis.metrics") + res = task.run_impl(db_session=dbsession, request_id=-1) + assert res == { + "success": False, + "present_report_labels": [], + "present_diff_labels": [], + "absent_labels": [], + "global_level_labels": [], + "errors": [ + { + "error_code": "not found", + "error_params": { + "extra": {}, + "message": "LabelAnalysisRequest not found", + }, + } + ], + } + mock_metrics.incr.assert_called_with( + "label_analysis_task.failed_to_calculate.larq_not_found" + ) + + +def test_get_executable_lines_labels_all_labels(sample_report_with_labels): + executable_lines = {"all": True} + task = LabelAnalysisRequestProcessingTask() + assert task.get_executable_lines_labels( + sample_report_with_labels, executable_lines + ) == ( + { + 4, + 6, + 8, + 3, + 9, + 1, + 7, + 2, + 5, + }, + set(), + ) + assert task.get_executable_lines_labels( + sample_report_with_labels, executable_lines + ) == (task.get_all_report_labels(sample_report_with_labels), set()) + + +def test_get_executable_lines_labels_all_labels_in_one_file(sample_report_with_labels): + executable_lines = {"all": False, "files": {"source.py": {"all": True}}} + task = LabelAnalysisRequestProcessingTask() + assert task.get_executable_lines_labels( + sample_report_with_labels, executable_lines + ) == ( + { + 1, + 6, + 9, + 2, + 4, + 5, + 3, + }, + {5, 6, 9}, + ) + + +def test_get_executable_lines_labels_some_labels_in_one_file(sample_report_with_labels): + executable_lines = { + "all": False, + "files": {"source.py": {"all": False, "lines": set([5, 6])}}, + } + task = LabelAnalysisRequestProcessingTask() + assert task.get_executable_lines_labels( + sample_report_with_labels, executable_lines + ) == ( + {1, 2, 3, 4}, + set(), + ) + + +def test_get_executable_lines_labels_some_labels_in_one_file_with_globals( + sample_report_with_labels, +): + executable_lines = { + "all": False, + "files": {"source.py": {"all": False, "lines": set([6, 8])}}, + } + task = LabelAnalysisRequestProcessingTask() + assert task.get_executable_lines_labels( + sample_report_with_labels, executable_lines + ) == ( + {2, 3, 4, 5, 9}, + {9, 6, 5}, + ) + + +def test_get_executable_lines_labels_some_labels_in_one_file_other_null( + sample_report_with_labels, +): + executable_lines = { + "all": False, + "files": { + "source.py": {"all": False, "lines": set([5, 6])}, + "path/from/randomfile_no_static_analysis.html": None, + }, + } + task = LabelAnalysisRequestProcessingTask() + assert task.get_executable_lines_labels( + sample_report_with_labels, executable_lines + ) == ( + {1, 2, 3, 4}, + set(), + ) + + +def test_get_all_labels_one_session(sample_report_with_labels): + task = LabelAnalysisRequestProcessingTask() + assert task.get_labels_per_session(sample_report_with_labels, 1) == { + 1, + 4, + 8, + 2, + 3, + 7, + } + assert task.get_labels_per_session(sample_report_with_labels, 2) == set() + assert task.get_labels_per_session(sample_report_with_labels, 5) == { + 5, + 6, + 9, + } + + +def test_get_relevant_executable_lines_nothing_found(dbsession, mocker): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + larf = LabelAnalysisRequestFactory.create( + base_commit__repository=repository, head_commit__repository=repository + ) + dbsession.add(larf) + dbsession.flush() + task = LabelAnalysisRequestProcessingTask() + task.errors = [] + task.dbsession = dbsession + parsed_git_diff = [] + assert task.get_relevant_executable_lines(larf, parsed_git_diff) is None + + +def test_get_relevant_executable_lines_with_static_analyses(dbsession, mocker): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + larf = LabelAnalysisRequestFactory.create( + base_commit__repository=repository, head_commit__repository=repository + ) + dbsession.add(larf) + dbsession.flush() + base_sasf = StaticAnalysisSuiteFactory.create(commit=larf.base_commit) + head_sasf = StaticAnalysisSuiteFactory.create(commit=larf.head_commit) + dbsession.add(base_sasf) + dbsession.add(head_sasf) + dbsession.flush() + task = LabelAnalysisRequestProcessingTask() + parsed_git_diff = [] + mocked_res = mocker.patch.object( + StaticAnalysisComparisonService, "get_base_lines_relevant_to_change" + ) + assert ( + task.get_relevant_executable_lines(larf, parsed_git_diff) + == mocked_res.return_value + ) + + +def test_run_impl_with_error( + dbsession, mock_storage, mocker, sample_report_with_labels, mock_repo_provider +): + mock_metrics = mocker.patch("tasks.label_analysis.metrics") + mocker.patch.object( + LabelAnalysisRequestProcessingTask, + "_get_lines_relevant_to_diff", + side_effect=Exception("Oh no"), + ) + larf = LabelAnalysisRequestFactory.create( + requested_labels=["tangerine", "pear", "banana", "apple"] + ) + dbsession.add(larf) + dbsession.flush() + task = LabelAnalysisRequestProcessingTask() + res = task.run_impl(dbsession, larf.id) + expected_result = { + "absent_labels": [], + "present_diff_labels": [], + "present_report_labels": [], + "success": False, + "global_level_labels": [], + "errors": [ + { + "error_code": "failed", + "error_params": {"extra": {}, "message": "Failed to calculate"}, + } + ], + } + assert res == expected_result + dbsession.flush() + dbsession.refresh(larf) + assert larf.state_id == LabelAnalysisRequestState.ERROR.db_id + assert larf.result is None + mock_metrics.incr.assert_called_with( + "label_analysis_task.failed_to_calculate.exception" + ) + + +def test_calculate_result_no_report( + dbsession, mock_storage, mocker, sample_report_with_labels, mock_repo_provider +): + mock_metrics = mocker.patch("tasks.label_analysis.metrics") + larf: LabelAnalysisRequest = LabelAnalysisRequestFactory.create( + # This being not-ordered is important in the test + # TO make sure we go through the warning at the bottom of run_impl + requested_labels=["tangerine", "pear", "banana", "apple"] + ) + dbsession.add(larf) + dbsession.flush() + mocker.patch.object( + ReportService, + "get_existing_report_for_commit", + return_value=None, + ) + mocker.patch.object( + LabelAnalysisRequestProcessingTask, + "_get_lines_relevant_to_diff", + return_value=(set(), set(), set()), + ) + task = LabelAnalysisRequestProcessingTask() + res = task.run_impl(dbsession, larf.id) + assert res == { + "success": True, + "absent_labels": larf.requested_labels, + "present_diff_labels": [], + "present_report_labels": [], + "global_level_labels": [], + "errors": [ + { + "error_code": "missing data", + "error_params": { + "extra": { + "base_commit": larf.base_commit.commitid, + "head_commit": larf.head_commit.commitid, + }, + "message": "Missing base report", + }, + } + ], + } + mock_metrics.incr.assert_called_with( + "label_analysis_task.failed_to_calculate.missing_info" + ) + + +@patch("tasks.label_analysis.parse_git_diff_json", return_value=["parsed_git_diff"]) +def test__get_parsed_git_diff(mock_parse_diff, dbsession, mock_repo_provider): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + larq = LabelAnalysisRequestFactory.create( + base_commit__repository=repository, head_commit__repository=repository + ) + dbsession.add(larq) + dbsession.flush() + mock_repo_provider.get_compare.return_value = {"diff": "json"} + task = LabelAnalysisRequestProcessingTask() + task.errors = [] + parsed_diff = task._get_parsed_git_diff(larq) + assert parsed_diff == ["parsed_git_diff"] + mock_parse_diff.assert_called_with({"diff": "json"}) + mock_repo_provider.get_compare.assert_called_with( + larq.base_commit.commitid, larq.head_commit.commitid + ) + + +@patch("tasks.label_analysis.parse_git_diff_json", return_value=["parsed_git_diff"]) +def test__get_parsed_git_diff_error(mock_parse_diff, dbsession, mock_repo_provider): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + larq = LabelAnalysisRequestFactory.create( + base_commit__repository=repository, head_commit__repository=repository + ) + dbsession.add(larq) + dbsession.flush() + mock_repo_provider.get_compare.side_effect = Exception("Oh no") + task = LabelAnalysisRequestProcessingTask() + task.errors = [] + task.dbsession = dbsession + parsed_diff = task._get_parsed_git_diff(larq) + assert parsed_diff is None + mock_parse_diff.assert_not_called() + mock_repo_provider.get_compare.assert_called_with( + larq.base_commit.commitid, larq.head_commit.commitid + ) + + +@patch( + "tasks.label_analysis.LabelAnalysisRequestProcessingTask.get_relevant_executable_lines", + return_value=[{"all": False, "files": {}}], +) +@patch( + "tasks.label_analysis.LabelAnalysisRequestProcessingTask._get_parsed_git_diff", + return_value=["parsed_git_diff"], +) +def test__get_lines_relevant_to_diff( + mock_parse_diff, mock_get_relevant_lines, dbsession +): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + larq = LabelAnalysisRequestFactory.create( + base_commit__repository=repository, head_commit__repository=repository + ) + dbsession.add(larq) + dbsession.flush() + task = LabelAnalysisRequestProcessingTask() + lines = task._get_lines_relevant_to_diff(larq) + assert lines == [{"all": False, "files": {}}] + mock_parse_diff.assert_called_with(larq) + mock_get_relevant_lines.assert_called_with(larq, ["parsed_git_diff"]) + + +@patch( + "tasks.label_analysis.LabelAnalysisRequestProcessingTask.get_relevant_executable_lines" +) +@patch( + "tasks.label_analysis.LabelAnalysisRequestProcessingTask._get_parsed_git_diff", + return_value=None, +) +def test__get_lines_relevant_to_diff_error( + mock_parse_diff, mock_get_relevant_lines, dbsession +): + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + larq = LabelAnalysisRequestFactory.create( + base_commit__repository=repository, head_commit__repository=repository + ) + dbsession.add(larq) + dbsession.flush() + task = LabelAnalysisRequestProcessingTask() + lines = task._get_lines_relevant_to_diff(larq) + assert lines is None + mock_parse_diff.assert_called_with(larq) + mock_get_relevant_lines.assert_not_called() + + +@patch( + "tasks.label_analysis.LabelAnalysisRequestProcessingTask.get_all_report_labels", + return_value=set(), +) +@patch( + "tasks.label_analysis.LabelAnalysisRequestProcessingTask.get_executable_lines_labels", + return_value=(set(), set()), +) +def test___get_existing_labels_no_labels_in_report( + mock_get_executable_lines_labels, mock_get_all_report_labels +): + report = MagicMock(name="fake_report") + lines_relevant = MagicMock(name="fake_lines_relevant_to_diff") + task = LabelAnalysisRequestProcessingTask() + res = task._get_existing_labels(report, lines_relevant) + expected = ExistingLabelSetsNotEncoded( + all_report_labels=set(), + executable_lines_labels=set(), + global_level_labels=set(), + ) + assert isinstance(res, ExistingLabelSetsNotEncoded) + assert res == expected diff --git a/apps/worker/tasks/tests/unit/test_manual_trigger.py b/apps/worker/tasks/tests/unit/test_manual_trigger.py new file mode 100644 index 0000000000..66ef4283c8 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_manual_trigger.py @@ -0,0 +1,110 @@ +import pytest +from celery.exceptions import Retry +from shared.reports.enums import UploadState + +from database.tests.factories import CommitFactory, PullFactory +from database.tests.factories.core import UploadFactory +from tasks.manual_trigger import ManualTriggerTask + + +class TestUploadCompletionTask(object): + def test_manual_upload_completion_trigger( + self, + mocker, + mock_configuration, + dbsession, + mock_storage, + mock_redis, + celery_app, + ): + mocked_app = mocker.patch.object( + ManualTriggerTask, + "app", + tasks={ + "app.tasks.notify.Notify": mocker.MagicMock(), + "app.tasks.pulls.Sync": mocker.MagicMock(), + "app.tasks.compute_comparison.ComputeComparison": mocker.MagicMock(), + }, + ) + commit = CommitFactory.create(pullid=None) + pull = PullFactory.create(repository=commit.repository, head=commit.commitid) + commit.pullid = pull.pullid + dbsession.add(pull) + dbsession.flush() + + dbsession.add(commit) + + upload = UploadFactory.create(report__commit=commit) + compared_to = CommitFactory.create(repository=commit.repository) + pull.compared_to = compared_to.commitid + + dbsession.add(upload) + dbsession.add(compared_to) + dbsession.add(pull) + dbsession.flush() + result = ManualTriggerTask().run_impl( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + report_code=None, + current_yaml={}, + ) + assert { + "notifications_called": True, + "message": "All uploads are processed. Triggering notifications.", + } == result + mocked_app.tasks["app.tasks.notify.Notify"].apply_async.assert_called_with( + kwargs=dict( + commitid=commit.commitid, current_yaml=None, repoid=commit.repoid + ) + ) + mocked_app.tasks["app.tasks.pulls.Sync"].apply_async.assert_called_with( + kwargs={ + "pullid": commit.pullid, + "repoid": commit.repoid, + "should_send_notifications": False, + } + ) + assert mocked_app.send_task.call_count == 0 + + mocked_app.tasks[ + "app.tasks.compute_comparison.ComputeComparison" + ].apply_async.assert_called_once() + + def test_manual_upload_completion_trigger_uploads_still_processing( + self, + mocker, + mock_configuration, + dbsession, + mock_storage, + mock_redis, + celery_app, + ): + mocker.patch.object( + ManualTriggerTask, + "app", + celery_app, + ) + commit = CommitFactory.create() + upload = UploadFactory.create(report__commit=commit, state="", state_id=None) + upload2 = UploadFactory.create( + report__commit=commit, + state="started", + state_id=UploadState.UPLOADED.db_id, + ) + dbsession.add(commit) + dbsession.add(upload) + dbsession.add(upload2) + dbsession.flush() + with pytest.raises(Retry): + result = ManualTriggerTask().run_impl( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + report_code=None, + current_yaml={}, + ) + assert { + "notifications_called": False, + "message": "Uploads are still in process and the task got retired so many times. Not triggering notifications.", + } == result diff --git a/apps/worker/tasks/tests/unit/test_new_user_activated.py b/apps/worker/tasks/tests/unit/test_new_user_activated.py new file mode 100644 index 0000000000..3bd3fdeb21 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_new_user_activated.py @@ -0,0 +1,228 @@ +from datetime import datetime + +import pytest + +from database.enums import Decoration, Notification, NotificationState +from database.tests.factories import ( + CommitFactory, + CommitNotificationFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) +from tasks.new_user_activated import NewUserActivatedTask +from tests.helpers import mock_all_plans_and_tiers + + +@pytest.fixture +def pull(dbsession): + repository = RepositoryFactory.create( + owner__username="codecov-test", + owner__unencrypted_oauth_token="testtlxuu2kfef3km1fbecdlmnb2nvpikvmoadi3bb", + owner__plan="users-pr-inappm", + name="example-python", + image_token="abcdefghij", + private=True, + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository) + head_commit = CommitFactory.create(repository=repository) + pull = PullFactory.create( + repository=repository, + base=base_commit.commitid, + head=head_commit.commitid, + updatestamp=datetime.now(), + state="open", + author__username="tjbiii", + author__unencrypted_oauth_token="testmlqkug1uo08z1ic8kq4gkivba2owf538c7mz", + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + return pull + + +class TestNewUserActivatedTaskUnit(object): + @pytest.fixture(autouse=True) + def mock_all_plans_and_tiers(self): + mock_all_plans_and_tiers() + + @pytest.mark.django_db + def test_get_pulls_authored_by_user_none(self, dbsession, pull): + org_ownerid = pull.repository.ownerid + user_ownerid_with_no_pulls = 12312412 + res = NewUserActivatedTask().get_pulls_authored_by_user( + dbsession, org_ownerid, user_ownerid_with_no_pulls + ) + assert res == [] + + @pytest.mark.django_db + def test_get_pulls_authored_by_user(self, dbsession, pull): + pull_by_other_author = PullFactory.create( + repository=pull.repository, + updatestamp=datetime.now(), + state="open", + author__username="1nf1nt3l00p", + author__unencrypted_oauth_token="testolcdo9icfq7lgpumzd2xq3aln6z4kxe6", + ) + dbsession.add(pull_by_other_author) + dbsession.flush() + org_ownerid = pull.repository.ownerid + user_ownerid = pull.author.ownerid + res = NewUserActivatedTask().get_pulls_authored_by_user( + dbsession, org_ownerid, user_ownerid + ) + assert len(res) == 1 + authored_pull = res[0] + assert authored_pull.state == "open" + assert authored_pull.author.ownerid == user_ownerid + + @pytest.mark.django_db + def test_is_org_on_pr_plan_gitlab_subgroup(self, dbsession, with_sql_functions): + root_group = OwnerFactory.create( + username="root_group", + service="gitlab", + unencrypted_oauth_token="testtlxuu2kfef3km1fbecdlmnb2nvpikvmoadi3", + plan="users-pr-inappm", + plan_activated_users=[], + ) + subgroup = OwnerFactory.create( + username="subgroup", + service="gitlab", + unencrypted_oauth_token="testtlxuu2kfef3km1fbecdlmnb2nvpikvmoadi3", + plan=None, + parent_service_id=root_group.service_id, + ) + dbsession.add(subgroup) + dbsession.add(root_group) + dbsession.flush() + + res = NewUserActivatedTask().is_org_on_pr_plan(dbsession, subgroup.ownerid) + assert res is True + + @pytest.mark.django_db + def test_org_not_found(self, mocker, dbsession): + unknown_org_ownerid = 404123 + user_ownerid = 123 + res = NewUserActivatedTask().run_impl( + dbsession, unknown_org_ownerid, user_ownerid + ) + assert res == { + "notifies_scheduled": False, + "pulls_notified": [], + "reason": "org not on pr author billing plan", + } + + @pytest.mark.django_db + def test_org_not_on_pr_plan(self, mocker, dbsession, pull): + pull.repository.owner.plan = "users-inappm" + dbsession.flush() + res = NewUserActivatedTask().run_impl( + dbsession, pull.repository.owner.ownerid, pull.author.ownerid + ) + assert res == { + "notifies_scheduled": False, + "pulls_notified": [], + "reason": "org not on pr author billing plan", + } + + @pytest.mark.django_db + def test_no_commit_notifications_found(self, mocker, dbsession, pull): + mocked_possibly_resend_notifications = mocker.patch( + "tasks.new_user_activated.NewUserActivatedTask.possibly_resend_notifications" + ) + res = NewUserActivatedTask().run_impl( + dbsession, pull.repository.owner.ownerid, pull.author.ownerid + ) + assert res == { + "notifies_scheduled": False, + "pulls_notified": [], + "reason": "no pulls/pull notifications met criteria", + } + assert not mocked_possibly_resend_notifications.called + + @pytest.mark.django_db + def test_no_head_commit_on_pull(self, mocker, dbsession, pull): + pull.head = None + mocked_possibly_resend_notifications = mocker.patch( + "tasks.new_user_activated.NewUserActivatedTask.possibly_resend_notifications" + ) + res = NewUserActivatedTask().run_impl( + dbsession, pull.repository.owner.ownerid, pull.author.ownerid + ) + assert res == { + "notifies_scheduled": False, + "pulls_notified": [], + "reason": "no pulls/pull notifications met criteria", + } + assert not mocked_possibly_resend_notifications.called + + @pytest.mark.django_db + def test_commit_notifications_all_standard(self, mocker, dbsession, pull): + pull_head_commit = pull.get_head_commit() + cn1 = CommitNotificationFactory.create( + commit=pull_head_commit, + notification_type=Notification.comment, + decoration_type=Decoration.standard, + state=NotificationState.pending, + ) + cn2 = CommitNotificationFactory.create( + commit=pull_head_commit, + notification_type=Notification.status_changes, + decoration_type=Decoration.standard, + state=NotificationState.pending, + ) + dbsession.add(cn1) + dbsession.add(cn2) + dbsession.flush() + + res = NewUserActivatedTask().run_impl( + dbsession, pull.repository.owner.ownerid, pull.author.ownerid + ) + assert res == { + "notifies_scheduled": False, + "pulls_notified": [], + "reason": "no pulls/pull notifications met criteria", + } + + @pytest.mark.django_db + def test_commit_notifications_resend_single_pull(self, mocker, dbsession, pull): + pull_head_commit = pull.get_head_commit() + cn1 = CommitNotificationFactory.create( + commit=pull_head_commit, + notification_type=Notification.comment, + decoration_type=Decoration.upgrade, + state=NotificationState.pending, + ) + cn2 = CommitNotificationFactory.create( + commit=pull_head_commit, + notification_type=Notification.status_changes, + decoration_type=Decoration.upgrade, + state=NotificationState.pending, + ) + dbsession.add(cn1) + dbsession.add(cn2) + dbsession.flush() + + mocked_app = mocker.patch.object( + NewUserActivatedTask, + "app", + tasks={"app.tasks.notify.Notify": mocker.MagicMock()}, + ) + + res = NewUserActivatedTask().run_impl( + dbsession, pull.repository.owner.ownerid, pull.author.ownerid + ) + + assert res == { + "notifies_scheduled": True, + "pulls_notified": [ + {"repoid": pull.repoid, "pullid": pull.pullid, "commitid": pull.head} + ], + "reason": None, + } + mocked_app.tasks["app.tasks.notify.Notify"].apply_async.assert_called_with( + kwargs=dict(commitid=pull.head, repoid=pull.repoid) + ) diff --git a/apps/worker/tasks/tests/unit/test_notify_error_task.py b/apps/worker/tasks/tests/unit/test_notify_error_task.py new file mode 100644 index 0000000000..fdea541b2c --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_notify_error_task.py @@ -0,0 +1,108 @@ +import pytest +from mock import AsyncMock +from shared.torngit.exceptions import TorngitClientError + +from database.tests.factories import ( + CommitFactory, + PullFactory, + ReportFactory, + RepositoryFactory, + UploadFactory, +) +from services.repository import EnrichedPull +from tasks.notify_error import ErrorNotifier, NotifyErrorTask + + +@pytest.fixture +def mock_repo_provider_comments(mocker): + m = mocker.MagicMock( + edit_comment=AsyncMock(return_value=True), + post_comment=AsyncMock(return_value={"id": 1}), + ) + _ = mocker.patch( + "helpers.notifier.get_repo_provider_service", + return_value=m, + ) + return m + + +def test_error_notifier(): + commit = CommitFactory() + failed_upload = 1 + total_upload = 2 + + e = ErrorNotifier( + commit, None, failed_upload=failed_upload, total_upload=total_upload + ) + + assert ( + e.build_message() + == f"❗️ We couldn't process [{failed_upload}] out of [{total_upload}] uploads. Codecov cannot generate a coverage report with partially processed data. Please review the upload errors on the commit page." + ) + + +def test_notify_error_task(mocker, dbsession, mock_repo_provider_comments): + repo = RepositoryFactory() + dbsession.add(repo) + commit = CommitFactory(repository=repo) + dbsession.add(commit) + report = ReportFactory(commit=commit) + dbsession.add(report) + upload1 = UploadFactory(report=report, state="complete") + upload2 = UploadFactory(report=report, state="error") + dbsession.add(upload1) + dbsession.add(upload2) + dbsession.flush() + + pull = PullFactory.create(repository=commit.repository, head=commit.commitid) + + _ = mocker.patch( + "helpers.notifier.fetch_and_update_pull_request_information_from_commit", + return_value=EnrichedPull( + database_pull=pull, + provider_pull={}, + ), + ) + + result = NotifyErrorTask().run_impl( + dbsession, + commitid=commit.commitid, + repoid=commit.repoid, + current_yaml={}, + ) + + assert result["success"] == True + + +def test_notify_error_task_failure(mocker, dbsession, mock_repo_provider_comments): + mock_repo_provider_comments.post_comment.side_effect = TorngitClientError + + repo = RepositoryFactory() + dbsession.add(repo) + commit = CommitFactory(repository=repo) + dbsession.add(commit) + report = ReportFactory(commit=commit) + dbsession.add(report) + upload1 = UploadFactory(report=report, state="complete") + upload2 = UploadFactory(report=report, state="error") + dbsession.add(upload1) + dbsession.add(upload2) + dbsession.flush() + + pull = PullFactory.create(repository=commit.repository, head=commit.commitid) + + _ = mocker.patch( + "helpers.notifier.fetch_and_update_pull_request_information_from_commit", + return_value=EnrichedPull( + database_pull=pull, + provider_pull={}, + ), + ) + + result = NotifyErrorTask().run_impl( + dbsession, + commitid=commit.commitid, + repoid=commit.repoid, + current_yaml={}, + ) + assert result["success"] == False diff --git a/apps/worker/tasks/tests/unit/test_notify_task.py b/apps/worker/tasks/tests/unit/test_notify_task.py new file mode 100644 index 0000000000..593f5b8a1f --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_notify_task.py @@ -0,0 +1,1311 @@ +from datetime import datetime, timezone +from typing import Any +from unittest.mock import MagicMock, call + +import httpx +import pytest +import respx +from celery.exceptions import MaxRetriesExceededError, Retry +from freezegun import freeze_time +from shared.celery_config import ( + activate_account_user_task_name, + new_user_activated_task_name, +) +from shared.reports.resources import Report +from shared.torngit.base import TorngitBaseAdapter +from shared.torngit.exceptions import ( + TorngitClientGeneralError, + TorngitServer5xxCodeError, +) +from shared.torngit.gitlab import Gitlab +from shared.typings.oauth_token_types import Token +from shared.typings.torngit import GithubInstallationInfo, TorngitInstanceData +from shared.yaml import UserYaml + +from database.enums import Decoration, Notification, NotificationState +from database.models.core import CommitNotification, GithubAppInstallation +from database.tests.factories import ( + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, + UploadErrorFactory, +) +from database.tests.factories.core import ReportFactory, UploadFactory +from database.tests.factories.reports import TestResultReportTotalsFactory +from helpers.checkpoint_logger import _kwargs_key +from helpers.checkpoint_logger.flows import UploadFlow +from helpers.exceptions import NoConfiguredAppsAvailable, RepositoryWithoutValidBotError +from services.decoration import DecorationDetails +from services.lock_manager import LockRetry +from services.notification import NotificationService +from services.notification.notifiers.base import ( + AbstractBaseNotifier, + NotificationResult, +) +from services.report import ReportService +from services.repository import EnrichedPull +from tasks.notify import ( + NotifyTask, + _possibly_pin_commit_to_github_app, + _possibly_refresh_previous_selection, + get_ta_relevant_context, +) +from tests.helpers import mock_all_plans_and_tiers + + +def _start_upload_flow(mocker): + mocker.patch( + "helpers.checkpoint_logger._get_milli_timestamp", + side_effect=[1337, 9001, 10000, 15000, 20000, 25000], + ) + UploadFlow.log(UploadFlow.UPLOAD_TASK_BEGIN) + UploadFlow.log(UploadFlow.PROCESSING_BEGIN) + UploadFlow.log(UploadFlow.INITIAL_PROCESSING_COMPLETE) + UploadFlow.log(UploadFlow.BATCH_PROCESSING_COMPLETE) + UploadFlow.log(UploadFlow.PROCESSING_COMPLETE) + + +@pytest.fixture +def enriched_pull(dbsession): + repository = RepositoryFactory.create( + owner__username="codecov", + owner__unencrypted_oauth_token="testtlxuu2kfef3km1fbecdlmnb2nvpikvmoadi3", + owner__plan="users-pr-inappm", + name="example-python", + image_token="abcdefghij", + private=True, + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository) + head_commit = CommitFactory.create(repository=repository) + pull = PullFactory.create( + repository=repository, + base=base_commit.commitid, + head=head_commit.commitid, + state="merged", + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + provider_pull = { + "author": {"id": "7123", "username": "tomcat"}, + "base": { + "branch": "master", + "commitid": "b92edba44fdd29fcc506317cc3ddeae1a723dd08", + }, + "head": { + "branch": "reason/some-testing", + "commitid": "a06aef4356ca35b34c5486269585288489e578db", + }, + "number": "1", + "id": "1", + "state": "open", + "title": "Creating new code for reasons no one knows", + } + return EnrichedPull(database_pull=pull, provider_pull=provider_pull) + + +class TestNotifyTaskHelpers(object): + def test_fetch_parent(self, dbsession): + task = NotifyTask() + owner = OwnerFactory.create( + unencrypted_oauth_token="testlln8sdeec57lz83oe3l8y9qq4lhqat2f1kzm", + username="ThiagoCodecov", + ) + repository = RepositoryFactory.create( + owner=owner, yaml={"codecov": {"max_report_age": "1y ago"}} + ) + different_repository = RepositoryFactory.create( + owner=owner, yaml={"codecov": {"max_report_age": "1y ago"}} + ) + dbsession.add(repository) + right_parent_commit = CommitFactory.create( + message="", + pullid=None, + branch="master", + commitid="17a71a9a2f5335ed4d00496c7bbc6405f547a527", + repository=repository, + ) + wrong_parent_commit = CommitFactory.create( + message="", + pullid=None, + branch="master", + commitid="17a71a9a2f5335ed4d00496c7bbc6405f547a527", + repository=different_repository, + ) + another_wrong_parent_commit = CommitFactory.create( + message="", + pullid=None, + branch="master", + commitid="bf303450570d7a84f8c3cdedac5ac23e27a64c19", + repository=repository, + ) + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + parent_commit_id="17a71a9a2f5335ed4d00496c7bbc6405f547a527", + repository=repository, + ) + dbsession.add(commit) + dbsession.add(another_wrong_parent_commit) + dbsession.add(repository) + dbsession.add(different_repository) + dbsession.add(right_parent_commit) + dbsession.add(wrong_parent_commit) + dbsession.flush() + assert task.fetch_parent(commit) == right_parent_commit + + def test_determine_decoration_type_from_pull_does_not_attempt_activation( + self, dbsession, mocker, enriched_pull + ): + mock_activate_user = mocker.patch("tasks.notify.activate_user") + decoration_details = DecorationDetails( + decoration_type=Decoration.standard, + reason="Auto activate not needed", + should_attempt_author_auto_activation=False, + ) + mock_determine_decoration_details = mocker.patch( + "tasks.notify.determine_decoration_details", return_value=decoration_details + ) + task = NotifyTask() + res = task.determine_decoration_type_from_pull(enriched_pull) + assert res == Decoration.standard + mock_determine_decoration_details.assert_called_with(enriched_pull, None) + assert not mock_activate_user.called + + def test_determine_decoration_type_from_pull_auto_activation_fails( + self, dbsession, mocker, enriched_pull, with_sql_functions + ): + pr_author = OwnerFactory.create( + username=enriched_pull.provider_pull["author"]["username"], + service_id=enriched_pull.provider_pull["author"]["id"], + ) + dbsession.add(pr_author) + dbsession.flush() + mock_activate_user = mocker.patch( + "tasks.notify.activate_user", return_value=False + ) + mock_schedule_new_user_activated_task = mocker.patch( + "tasks.notify.NotifyTask.schedule_new_user_activated_task" + ) + decoration_details = DecorationDetails( + decoration_type=Decoration.upgrade, + reason="User must be activated", + should_attempt_author_auto_activation=True, + activation_org_ownerid=enriched_pull.database_pull.repository.owner.ownerid, + activation_author_ownerid=pr_author.ownerid, + ) + mock_determine_decoration_details = mocker.patch( + "tasks.notify.determine_decoration_details", return_value=decoration_details + ) + task = NotifyTask() + res = task.determine_decoration_type_from_pull(enriched_pull) + assert res == Decoration.upgrade + mock_determine_decoration_details.assert_called_with(enriched_pull, None) + mock_activate_user.assert_called_with( + dbsession, + enriched_pull.database_pull.repository.owner.ownerid, + pr_author.ownerid, + ) + assert not mock_schedule_new_user_activated_task.called + + @pytest.mark.django_db(databases={"default"}) + def test_determine_decoration_type_from_pull_attempt_activation( + self, dbsession, mocker, enriched_pull, with_sql_functions + ): + mock_all_plans_and_tiers() + pr_author = OwnerFactory.create( + username=enriched_pull.provider_pull["author"]["username"], + service_id=enriched_pull.provider_pull["author"]["id"], + ) + dbsession.add(pr_author) + dbsession.flush() + mock_activate_user = mocker.patch( + "tasks.notify.activate_user", return_value=True + ) + decoration_details = DecorationDetails( + decoration_type=Decoration.upgrade, + reason="User must be activated", + should_attempt_author_auto_activation=True, + activation_org_ownerid=enriched_pull.database_pull.repository.owner.ownerid, + activation_author_ownerid=pr_author.ownerid, + ) + mock_determine_decoration_details = mocker.patch( + "tasks.notify.determine_decoration_details", return_value=decoration_details + ) + mocked_send_task = mocker.patch( + "tasks.notify.celery_app.send_task", return_value=None + ) + task = NotifyTask() + res = task.determine_decoration_type_from_pull(enriched_pull) + assert res == Decoration.standard + mock_determine_decoration_details.assert_called_with(enriched_pull, None) + mock_activate_user.assert_called_with( + dbsession, + enriched_pull.database_pull.repository.owner.ownerid, + pr_author.ownerid, + ) + assert mocked_send_task.call_count == 2 + + new_user_activation_call = mocked_send_task.call_args_list[0] + account_user_activation_call = mocked_send_task.call_args_list[1] + assert new_user_activation_call == call( + new_user_activated_task_name, + args=None, + kwargs=dict( + org_ownerid=enriched_pull.database_pull.repository.owner.ownerid, + user_ownerid=pr_author.ownerid, + ), + ) + assert account_user_activation_call[0] == ( + activate_account_user_task_name, + None, + { + "org_ownerid": enriched_pull.database_pull.repository.owner.ownerid, + "user_ownerid": pr_author.ownerid, + }, + ) + + @pytest.mark.parametrize("cached_id, app_to_save", [("24", "24"), (None, 12)]) + def test__possibly_refresh_previous_selection( + self, cached_id, app_to_save, mocker, dbsession + ): + commit = CommitFactory(repository__owner__service="github") + app = GithubAppInstallation( + id_=12, owner=commit.repository.owner, installation_id=123 + ) + other_app = GithubAppInstallation( + id_=24, owner=commit.repository.owner, installation_id=123 + ) + commit_notifications = [ + CommitNotification( + commit=commit, + notification_type=Notification.checks_project, + state=NotificationState.success, + ), + CommitNotification( + commit=commit, + notification_type=Notification.checks_patch, + state=NotificationState.error, + gh_app_id=other_app.id, + ), + CommitNotification( + commit=commit, + notification_type=Notification.checks_changes, + state=NotificationState.success, + gh_app_id=app.id, + ), + ] + dbsession.add_all([commit, app, other_app] + commit_notifications) + dbsession.flush() + mock_set_gh_app_for_commit = mocker.patch( + "tasks.notify.set_github_app_for_commit" + ) + mocker.patch("tasks.notify.get_github_app_for_commit", return_value=cached_id) + assert _possibly_refresh_previous_selection(commit) == True + mock_set_gh_app_for_commit.assert_called_with(app_to_save, commit) + + def test__possibly_refresh_previous_selection_false(self, mocker, dbsession): + commit = CommitFactory(repository__owner__service="github") + dbsession.add(commit) + dbsession.flush() + mocker.patch("tasks.notify.get_github_app_for_commit", return_value=None) + mock_set_gh_app_for_commit = mocker.patch( + "tasks.notify.set_github_app_for_commit" + ) + assert _possibly_refresh_previous_selection(commit) == False + mock_set_gh_app_for_commit.assert_not_called() + + def test_possibly_pin_commit_to_github_app_not_github_or_no_installation( + self, mocker, dbsession + ): + commit = CommitFactory(repository__owner__service="gitlab") + commit_from_gh = CommitFactory(repository__owner__service="github") + dbsession.add_all([commit, commit_from_gh]) + dbsession.flush() + mock_refresh_selection = mocker.patch( + "tasks.notify._possibly_refresh_previous_selection", return_value=None + ) + torngit = MagicMock(data=TorngitInstanceData()) + torngit_with_installation = MagicMock( + data=TorngitInstanceData(installation=GithubInstallationInfo(id=12)) + ) + assert ( + _possibly_pin_commit_to_github_app(commit, torngit_with_installation) + is None + ) + mock_refresh_selection.assert_not_called() + assert _possibly_pin_commit_to_github_app(commit_from_gh, torngit) is None + mock_refresh_selection.assert_called_with(commit_from_gh) + + def test_possibly_pin_commit_to_github_app_new_selection(self, mocker, dbsession): + commit = CommitFactory(repository__owner__service="github") + dbsession.add(commit) + dbsession.flush() + mock_refresh_selection = mocker.patch( + "tasks.notify._possibly_refresh_previous_selection", return_value=None + ) + mock_set_gh_app_for_commit = mocker.patch( + "tasks.notify.set_github_app_for_commit" + ) + torngit = MagicMock( + data=TorngitInstanceData(installation=GithubInstallationInfo(id=12)) + ) + assert _possibly_pin_commit_to_github_app(commit, torngit) == 12 + mock_refresh_selection.assert_called_with(commit) + mock_set_gh_app_for_commit.assert_called_with(12, commit) + + def test_get_gitlab_extra_shas(self, dbsession): + commit = CommitFactory( + repository__owner__service="gitlab", repository__service_id=1000 + ) + dbsession.add(commit) + report = ReportFactory(commit=commit) + dbsession.add(report) + uploads = [UploadFactory(report=report, job_code=i) for i in range(3)] + dbsession.add_all(uploads) + dbsession.flush() + assert len(commit.report.uploads) == 3 + with respx.mock: + respx.get("https://gitlab.com/api/v4/projects/1000/jobs/0").mock( + return_value=httpx.Response( + status_code=200, + json={ + "pipeline": { + "id": 0, + "project_id": 1000, + "sha": commit.commitid, + } + }, + ) + ) + respx.get("https://gitlab.com/api/v4/projects/1000/jobs/1").mock( + return_value=httpx.Response( + status_code=200, + json={ + "pipeline": { + "id": 1, + "project_id": 1000, + "sha": "508c25daba5bbc77d8e7cf3c1917d5859153cfd3", + } + }, + ) + ) + respx.get("https://gitlab.com/api/v4/projects/1000/jobs/2").mock( + return_value=httpx.Response(status_code=400, json={}) + ) + repository_service = Gitlab(token={"key": "some_token"}) + task = NotifyTask() + assert task.get_gitlab_extra_shas_to_notify(commit, repository_service) == { + "508c25daba5bbc77d8e7cf3c1917d5859153cfd3", + } + + +class TestNotifyTask(object): + def test_simple_call_no_notifications( + self, dbsession, mocker, mock_storage, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codecov.io" + ) + mocker.patch.object(NotifyTask, "app") + mocked_should_send_notifications = mocker.patch.object( + NotifyTask, "should_send_notifications", return_value=False + ) + fetch_and_update_whether_ci_passed_result = {} + mocker.patch.object( + NotifyTask, + "fetch_and_update_whether_ci_passed", + return_value=fetch_and_update_whether_ci_passed_result, + ) + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + ) + dbsession.add(commit) + dbsession.flush() + task = NotifyTask() + result = task.run_impl_within_lock( + dbsession, repoid=commit.repoid, commitid=commit.commitid, current_yaml={} + ) + assert result == {"notified": False, "notifications": None} + mocked_should_send_notifications.assert_called_with( + UserYaml({}), commit, fetch_and_update_whether_ci_passed_result, None + ) + + def test_simple_call_no_notifications_no_yaml_given( + self, dbsession, mocker, mock_storage, mock_configuration, mock_repo_provider + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codecov.io" + ) + mocker.patch.object(NotifyTask, "app") + mocked_should_send_notifications = mocker.patch.object( + NotifyTask, "should_send_notifications", return_value=False + ) + fetch_and_update_whether_ci_passed_result = {} + mocker.patch.object( + NotifyTask, + "fetch_and_update_whether_ci_passed", + return_value=fetch_and_update_whether_ci_passed_result, + ) + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + repository__owner__createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + ) + mocked_fetch_yaml = mocker.patch( + "services.yaml.fetch_commit_yaml_from_provider" + ) + mocked_fetch_yaml.return_value = {} + dbsession.add(commit) + dbsession.flush() + task = NotifyTask() + result = task.run_impl_within_lock( + dbsession, repoid=commit.repoid, commitid=commit.commitid, current_yaml=None + ) + assert result == {"notified": False, "notifications": None} + mocked_should_send_notifications.assert_called_with( + UserYaml({}), commit, fetch_and_update_whether_ci_passed_result, None + ) + mocked_fetch_yaml.assert_called_with(commit, mock_repo_provider) + + def test_simple_call_no_notifications_commit_differs_from_pulls_head( + self, + dbsession, + mocker, + mock_storage, + mock_configuration, + mock_repo_provider, + enriched_pull, + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codecov.io" + ) + mocker.patch.object(NotifyTask, "app") + mocked_should_send_notifications = mocker.patch.object( + NotifyTask, "should_send_notifications", return_value=True + ) + fetch_and_update_whether_ci_passed_result = {} + mocker.patch.object( + NotifyTask, + "fetch_and_update_whether_ci_passed", + return_value=fetch_and_update_whether_ci_passed_result, + ) + mocked_fetch_pull = mocker.patch( + "tasks.notify.fetch_and_update_pull_request_information_from_commit" + ) + head_report = Report() + mocker.patch.object( + ReportService, "get_existing_report_for_commit", return_value=head_report + ) + mocked_fetch_pull.return_value = enriched_pull + # commit different from provider pull recent head + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + ) + dbsession.add(commit) + dbsession.flush() + task = NotifyTask() + result = task.run_impl_within_lock( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml={"codecov": {"notify": {"manual_trigger": True}}}, + ) + assert result == { + "notified": False, + "notifications": None, + "reason": "User doesnt want notifications warning them that current head differs from pull request most recent head.", + } + mocked_should_send_notifications.assert_called_with( + UserYaml({"codecov": {"notify": {"manual_trigger": True}}}), + commit, + fetch_and_update_whether_ci_passed_result, + head_report, + ) + + def test_simple_call_yes_notifications_no_base( + self, + dbsession, + mocker, + mock_storage, + mock_configuration, + mock_checkpoint_submit, + ): + fake_notifier = mocker.MagicMock( + AbstractBaseNotifier, + is_enabled=mocker.MagicMock(return_value=True), + title="the_title", + notification_type=Notification.comment, + decoration_type=Decoration.standard, + ) + fake_notifier.name = "fake_hahaha" + fake_notifier.notify.return_value = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation="", + data_sent={"all": ["The", 1, "data"]}, + ) + mocker.patch.object( + NotificationService, "get_notifiers_instances", return_value=[fake_notifier] + ) + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codecov.io" + ) + mocker.patch.object(NotifyTask, "app") + mocker.patch.object(NotifyTask, "save_patch_totals") + mocker.patch.object(NotifyTask, "should_send_notifications", return_value=True) + fetch_and_update_whether_ci_passed_result = {} + mocker.patch.object( + NotifyTask, + "fetch_and_update_whether_ci_passed", + return_value=fetch_and_update_whether_ci_passed_result, + ) + mocked_fetch_pull = mocker.patch( + "tasks.notify.fetch_and_update_pull_request_information_from_commit" + ) + mocker.patch.object( + ReportService, "get_existing_report_for_commit", return_value=Report() + ) + mocked_fetch_pull.return_value = None + commit = CommitFactory.create( + message="", pullid=None, repository__owner__service="github" + ) + dbsession.add(commit) + dbsession.flush() + + _start_upload_flow(mocker) + kwargs = UploadFlow.save_to_kwargs({}) + + task = NotifyTask() + result = task.run_impl_within_lock( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml={"coverage": {"status": {"patch": True}}}, + **kwargs, + ) + expected_result = { + "notified": True, + "notifications": [ + { + "notifier": "fake_hahaha", + "title": "the_title", + "result": NotificationResult( + data_sent={"all": ["The", 1, "data"]}, + notification_successful=True, + notification_attempted=True, + data_received=None, + explanation="", + ), + } + ], + } + assert result["notifications"] == expected_result["notifications"] + assert result["notifications"][0] == expected_result["notifications"][0] + assert result["notifications"] == expected_result["notifications"] + assert result == expected_result + + checkpoints_data = UploadFlow._data_from_log_context() + mock_checkpoint_submit.assert_any_call( + "notification_latency", + UploadFlow.UPLOAD_TASK_BEGIN, + UploadFlow.NOTIFIED, + data=checkpoints_data, + ) + + def test_simple_call_no_pullrequest_found( + self, dbsession, mocker, mock_storage, mock_configuration + ): + mocked_submit_third_party_notifications = mocker.patch.object( + NotifyTask, "submit_third_party_notifications" + ) + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codecov.io" + ) + mocker.patch.object(NotifyTask, "app") + mocker.patch.object(NotifyTask, "should_send_notifications", return_value=True) + fetch_and_update_whether_ci_passed_result = {} + mocker.patch.object( + NotifyTask, + "fetch_and_update_whether_ci_passed", + return_value=fetch_and_update_whether_ci_passed_result, + ) + mocked_fetch_pull = mocker.patch( + "tasks.notify.fetch_and_update_pull_request_information_from_commit" + ) + mocker.patch.object( + ReportService, "get_existing_report_for_commit", return_value=Report() + ) + mocked_fetch_pull.return_value = EnrichedPull(None, None) + commit = CommitFactory.create(message="", pullid=None) + dbsession.add(commit) + dbsession.flush() + task = NotifyTask() + result = task.run_impl_within_lock( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml={"coverage": {"status": {"patch": True}}}, + ) + assert result == { + "notified": True, + "notifications": mocked_submit_third_party_notifications.return_value, + } + + def test_simple_call_should_delay( + self, dbsession, mocker, mock_storage, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codecov.io" + ) + mocker.patch.object(NotifyTask, "app") + mocked_should_wait_longer = mocker.patch.object( + NotifyTask, "should_wait_longer", return_value=True + ) + mocked_retry = mocker.patch.object(NotifyTask, "retry", side_effect=Retry()) + fetch_and_update_whether_ci_passed_result = {} + mocker.patch.object( + NotifyTask, + "fetch_and_update_whether_ci_passed", + return_value=fetch_and_update_whether_ci_passed_result, + ) + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + ) + dbsession.add(commit) + dbsession.flush() + task = NotifyTask() + with pytest.raises(Retry): + task.run_impl_within_lock( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml={}, + ) + mocked_retry.assert_called_with(countdown=15, max_retries=10) + mocked_should_wait_longer.assert_called_with( + UserYaml({}), commit, fetch_and_update_whether_ci_passed_result + ) + + def test_simple_call_should_delay_using_integration( + self, dbsession, mocker, mock_storage, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codecov.io" + ) + mocker.patch.object(NotifyTask, "app") + mocked_should_wait_longer = mocker.patch.object( + NotifyTask, "should_wait_longer", return_value=True + ) + mocked_retry = mocker.patch.object(NotifyTask, "retry", side_effect=Retry()) + fetch_and_update_whether_ci_passed_result = {} + mocker.patch.object( + NotifyTask, + "fetch_and_update_whether_ci_passed", + return_value=fetch_and_update_whether_ci_passed_result, + ) + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + repository__using_integration=True, + ) + dbsession.add(commit) + dbsession.flush() + task = NotifyTask() + with pytest.raises(Retry): + task.run_impl_within_lock( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml={}, + ) + mocked_retry.assert_called_with(countdown=180, max_retries=5) + mocked_should_wait_longer.assert_called_with( + UserYaml({}), commit, fetch_and_update_whether_ci_passed_result + ) + + def test_simple_call_not_able_fetch_ci( + self, dbsession, mocker, mock_storage, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codecov.io" + ) + mocker.patch.object(NotifyTask, "app") + mocker.patch.object( + NotifyTask, + "fetch_and_update_whether_ci_passed", + side_effect=TorngitClientGeneralError(401, "response", "message"), + ) + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + repository__using_integration=True, + ) + dbsession.add(commit) + dbsession.flush() + task = NotifyTask() + res = task.run_impl_within_lock( + dbsession, repoid=commit.repoid, commitid=commit.commitid, current_yaml={} + ) + expected_result = { + "notifications": None, + "notified": False, + "reason": "not_able_fetch_ci_result", + } + assert expected_result == res + + def test_simple_call_not_able_fetch_ci_server_issues( + self, dbsession, mocker, mock_storage, mock_configuration + ): + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codecov.io" + ) + mocker.patch.object(NotifyTask, "app") + mocker.patch.object( + NotifyTask, + "fetch_and_update_whether_ci_passed", + side_effect=TorngitServer5xxCodeError(), + ) + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + repository__using_integration=True, + ) + dbsession.add(commit) + dbsession.flush() + task = NotifyTask() + res = task.run_impl_within_lock( + dbsession, repoid=commit.repoid, commitid=commit.commitid, current_yaml={} + ) + expected_result = { + "notifications": None, + "notified": False, + "reason": "server_issues_ci_result", + } + assert expected_result == res + + def test_should_send_notifications_ci_did_not_pass(self, dbsession, mocker): + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + repository__using_integration=True, + ) + mocked_app = mocker.patch.object(NotifyTask, "app") + set_error_task_caller = mocker.MagicMock() + mocked_app.tasks = {"app.tasks.status.SetError": set_error_task_caller} + dbsession.add(commit) + dbsession.flush() + ci_passed = False + mock_report = mocker.MagicMock(sessions=[mocker.MagicMock()]) # 1 session + current_yaml = {"codecov": {"require_ci_to_pass": True}} + task = NotifyTask() + res = task.should_send_notifications( + current_yaml, commit, ci_passed, mock_report + ) + assert not res + set_error_task_caller.apply_async.assert_called_with( + args=None, + kwargs=dict( + repoid=commit.repoid, commitid=commit.commitid, message="CI failed." + ), + ) + + def test_should_send_notifications_after_n_builds(self, dbsession, mocker): + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + + mock_report = mocker.MagicMock(sessions=[mocker.MagicMock()]) # 1 session + + task = NotifyTask() + current_yaml = {"codecov": {"notify": {"after_n_builds": 2}}} + res = task.should_send_notifications(current_yaml, commit, True, mock_report) + assert not res + + def test_notify_task_no_bot(self, dbsession, mocker): + get_repo_provider_service = mocker.patch( + "tasks.notify.get_repo_provider_service" + ) + get_repo_provider_service.side_effect = RepositoryWithoutValidBotError() + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + repository__using_integration=True, + ) + dbsession.add(commit) + dbsession.flush() + current_yaml = {"codecov": {"require_ci_to_pass": True}} + task = NotifyTask() + res = task.run_impl_within_lock( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml=current_yaml, + ) + expected_result = { + "notifications": None, + "notified": False, + "reason": "no_valid_bot", + } + assert expected_result == res + + @freeze_time("2024-04-22T11:15:00") + def test_notify_task_no_ghapp_available_one_rate_limited(self, dbsession, mocker): + get_repo_provider_service = mocker.patch( + "tasks.notify.get_repo_provider_service" + ) + mock_retry = mocker.patch.object(NotifyTask, "retry", return_value=None) + get_repo_provider_service.side_effect = NoConfiguredAppsAvailable( + apps_count=2, rate_limited_count=1, suspended_count=1 + ) + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + repository__using_integration=True, + ) + dbsession.add(commit) + dbsession.flush() + current_yaml = {"codecov": {"require_ci_to_pass": True}} + task = NotifyTask() + res = task.run_impl_within_lock( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml=current_yaml, + ) + assert res is None + mock_retry.assert_called_with(max_retries=10, countdown=45 * 60) + + @freeze_time("2024-04-22T11:15:00") + def test_notify_task_no_ghapp_available_all_suspended(self, dbsession, mocker): + get_repo_provider_service = mocker.patch( + "tasks.notify.get_repo_provider_service" + ) + mock_retry = mocker.patch.object(NotifyTask, "retry", return_value=None) + get_repo_provider_service.side_effect = NoConfiguredAppsAvailable( + apps_count=1, rate_limited_count=0, suspended_count=1 + ) + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + repository__using_integration=True, + ) + dbsession.add(commit) + dbsession.flush() + current_yaml = {"codecov": {"require_ci_to_pass": True}} + task = NotifyTask() + res = task.run_impl_within_lock( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml=current_yaml, + ) + assert res == { + "notified": False, + "notifications": None, + "reason": "no_valid_github_app_found", + } + mock_retry.assert_not_called() + + def test_submit_third_party_notifications_exception(self, mocker, dbsession): + current_yaml = {} + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create(repository=repository) + head_commit = CommitFactory.create(repository=repository, branch="new_branch") + pull = PullFactory.create( + repository=repository, base=base_commit.commitid, head=head_commit.commitid + ) + enrichedPull = EnrichedPull(database_pull=pull, provider_pull={}) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + repository = base_commit.repository + base_report = Report() + head_report = Report() + good_notifier = mocker.MagicMock( + AbstractBaseNotifier, + is_enabled=mocker.MagicMock(return_value=True), + title="good_notifier", + notification_type=Notification.comment, + decoration_type=Decoration.standard, + ) + bad_notifier = mocker.MagicMock( + AbstractBaseNotifier, + is_enabled=mocker.MagicMock(return_value=True), + title="bad_notifier", + notification_type=Notification.comment, + decoration_type=Decoration.standard, + ) + disabled_notifier = mocker.MagicMock( + AbstractBaseNotifier, + is_enabled=mocker.MagicMock(return_value=False), + title="disabled_notifier", + notification_type=Notification.comment, + decoration_type=Decoration.standard, + ) + good_notifier.notify.return_value = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation="", + data_sent={"some": "data"}, + ) + good_notifier.name = "good_name" + bad_notifier.name = "bad_name" + disabled_notifier.name = "disabled_notifier_name" + bad_notifier.notify.side_effect = Exception("This is bad") + mocker.patch.object( + NotificationService, + "get_notifiers_instances", + return_value=[bad_notifier, good_notifier, disabled_notifier], + ) + mocker.patch.object(NotifyTask, "save_patch_totals") + task = NotifyTask() + expected_result = [ + {"notifier": "bad_name", "title": "bad_notifier", "result": None}, + { + "notifier": "good_name", + "title": "good_notifier", + "result": NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation="", + data_sent={"some": "data"}, + data_received=None, + ), + }, + ] + res = task.submit_third_party_notifications( + current_yaml, + base_commit, + head_commit, + base_report, + head_report, + enrichedPull, + mocker.MagicMock(), + ) + assert expected_result == res + + def test_notify_task_max_retries_exceeded( + self, dbsession, mocker, mock_repo_provider + ): + mocker.patch.object(NotifyTask, "should_wait_longer", return_value=True) + mocker.patch.object(NotifyTask, "retry", side_effect=MaxRetriesExceededError()) + mocked_fetch_and_update_whether_ci_passed = mocker.patch.object( + NotifyTask, "fetch_and_update_whether_ci_passed" + ) + mocked_fetch_and_update_whether_ci_passed.return_value = True + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + repository__using_integration=True, + ) + dbsession.add(commit) + dbsession.flush() + current_yaml = {"codecov": {"require_ci_to_pass": True}} + task = NotifyTask() + res = task.run_impl_within_lock( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml=current_yaml, + ) + expected_result = { + "notifications": None, + "notified": False, + "reason": "too_many_retries", + } + assert expected_result == res + + def test_run_impl_unobtainable_lock(self, dbsession, mock_redis, mocker): + mocked_run_impl_within_lock = mocker.patch.object( + NotifyTask, "run_impl_within_lock" + ) + mocked_has_upcoming_notifies_according_to_redis = mocker.patch.object( + NotifyTask, "has_upcoming_notifies_according_to_redis", return_value=False + ) + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + current_yaml = {"codecov": {"require_ci_to_pass": True}} + task = NotifyTask() + m = mocker.MagicMock() + m.return_value.locked.return_value.__enter__.side_effect = LockRetry(60) + mocker.patch("tasks.notify.LockManager", m) + + res = task.run_impl( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml=current_yaml, + ) + + assert res == { + "notifications": None, + "notified": False, + "reason": "unobtainable_lock", + } + assert not mocked_run_impl_within_lock.called + + def test_run_impl_other_jobs_coming(self, dbsession, mock_redis, mocker): + mocked_run_impl_within_lock = mocker.patch.object( + NotifyTask, "run_impl_within_lock" + ) + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + current_yaml = {"codecov": {"require_ci_to_pass": True}} + task = NotifyTask() + mock_redis.get.return_value = True + res = task.run_impl( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml=current_yaml, + ) + assert res == { + "notifications": None, + "notified": False, + "reason": "has_other_notifies_coming", + } + assert not mocked_run_impl_within_lock.called + + def test_run_impl_can_run_logic(self, dbsession, mock_redis, mocker): + mocked_run_impl_within_lock = mocker.patch.object( + NotifyTask, "run_impl_within_lock" + ) + mocked_run_impl_within_lock.return_value = { + "notifications": [], + "notified": True, + "reason": "yay", + } + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + current_yaml = {"codecov": {"require_ci_to_pass": True}} + task = NotifyTask() + mock_redis.get.return_value = False + _start_upload_flow(mocker) + kwargs = UploadFlow.save_to_kwargs({}) + res = task.run_impl( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml=current_yaml, + **kwargs, + ) + assert res == {"notifications": [], "notified": True, "reason": "yay"} + kwargs = {_kwargs_key(UploadFlow): mocker.ANY} + mocked_run_impl_within_lock.assert_called_with( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml=current_yaml, + empty_upload=None, + **kwargs, + ) + + def test_checkpoints_not_logged_outside_upload_flow( + self, dbsession, mock_redis, mocker, mock_checkpoint_submit, mock_configuration + ): + fake_notifier = mocker.MagicMock( + AbstractBaseNotifier, + is_enabled=mocker.MagicMock(return_value=True), + title="the_title", + notification_type=Notification.comment, + decoration_type=Decoration.standard, + ) + fake_notifier.name = "fake_hahaha" + fake_notifier.notify.return_value = NotificationResult( + notification_attempted=True, + notification_successful=True, + explanation="", + data_sent={"all": ["The", 1, "data"]}, + ) + mocker.patch.object( + NotificationService, "get_notifiers_instances", return_value=[fake_notifier] + ) + mock_configuration.params["setup"]["codecov_dashboard_url"] = ( + "https://codecov.io" + ) + mocker.patch("tasks.notify.get_repo_provider_service_for_specific_commit") + mocker.patch.object(NotifyTask, "app") + mocker.patch.object(NotifyTask, "save_patch_totals") + mocker.patch.object(NotifyTask, "should_send_notifications", return_value=True) + fetch_and_update_whether_ci_passed_result = {} + mocker.patch.object( + NotifyTask, + "fetch_and_update_whether_ci_passed", + return_value=fetch_and_update_whether_ci_passed_result, + ) + mocked_fetch_pull = mocker.patch( + "tasks.notify.fetch_and_update_pull_request_information_from_commit" + ) + mocker.patch.object( + ReportService, "get_existing_report_for_commit", return_value=Report() + ) + mocked_fetch_pull.return_value = None + commit = CommitFactory.create(message="", pullid=None) + dbsession.add(commit) + dbsession.flush() + + task = NotifyTask() + result = task.run_impl_within_lock( + dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + current_yaml={"coverage": {"status": {"patch": True}}}, + ) + assert not mock_checkpoint_submit.called + + @pytest.mark.parametrize( + "service,data,bot_token,expected_response", + [ + pytest.param( + "github", + {}, + Token(username="test-codecov-commenter-bot"), + True, + id="expected_to_be_true", + ), + pytest.param( + "bitbucket", + {}, + Token(username="test-codecov-commenter-bot"), + False, + id="not_github", + ), + pytest.param( + "github", + {"installation": GithubInstallationInfo(installation_id="some_id")}, + Token(username="test-codecov-commenter-bot"), + False, + id="has_installation", + ), + pytest.param( + "github", + {"installation": GithubInstallationInfo(installation_id="some_id")}, + None, + False, + id="not_using_commenter_bot", + ), + ], + ) + def test_is_using_codecov_commenter( + self, + mocker, + mock_configuration: dict[str, Any], + service: str, + data: dict[str, Any], + bot_token: Token | None, + expected_response, + ) -> None: + mock_repository_service: Any = mocker.MagicMock(spec=TorngitBaseAdapter) + mock_repository_service.data = data + mock_repository_service.service = service + mock_repository_service.get_token_by_type.return_value = bot_token + + mock_configuration.params["github"] = { + "bots": {"comment": {"username": "test-codecov-commenter-bot"}} + } + + task = NotifyTask() + assert ( + task.is_using_codecov_commenter(mock_repository_service) + == expected_response + ) + + def test_ta_relevant_context(self, mocker, dbsession): + report = ReportFactory(report_type="test_results") + dbsession.add(report) + dbsession.flush() + + upload = UploadFactory(report=report) + dbsession.add(upload) + dbsession.flush() + + upload_error = UploadErrorFactory(report_upload=upload) + dbsession.add(upload_error) + dbsession.flush() + + all_tests_passed, ta_error_msg = get_ta_relevant_context(dbsession, report) + + assert all_tests_passed is False + assert ta_error_msg == "error message" + + def test_ta_relevant_context_no_error(self, mocker, dbsession): + report = ReportFactory(report_type="test_results") + dbsession.add(report) + dbsession.flush() + + upload = UploadFactory(report=report) + dbsession.add(upload) + dbsession.flush() + + all_tests_passed, ta_error_msg = get_ta_relevant_context(dbsession, report) + + assert all_tests_passed is False + assert ta_error_msg is None + + def test_ta_relevant_context_totals(self, mocker, dbsession): + report = ReportFactory(report_type="test_results") + dbsession.add(report) + dbsession.flush() + + totals = TestResultReportTotalsFactory(report=report, failed=1) + dbsession.add(totals) + dbsession.flush() + + all_tests_passed, ta_error_msg = get_ta_relevant_context(dbsession, report) + + assert all_tests_passed is False + assert ta_error_msg is None + + def test_ta_relevant_context_totals_passed(self, mocker, dbsession): + report = ReportFactory(report_type="test_results") + dbsession.add(report) + dbsession.flush() + + totals = TestResultReportTotalsFactory(report=report, failed=0) + dbsession.add(totals) + dbsession.flush() + + all_tests_passed, ta_error_msg = get_ta_relevant_context(dbsession, report) + + assert all_tests_passed is True + assert ta_error_msg is None diff --git a/apps/worker/tasks/tests/unit/test_planmanager_task.py b/apps/worker/tasks/tests/unit/test_planmanager_task.py new file mode 100644 index 0000000000..5760c13d23 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_planmanager_task.py @@ -0,0 +1,47 @@ +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName + +from database.models.core import OrganizationLevelToken +from database.tests.factories.core import OrgLevelTokenFactory, OwnerFactory +from tasks.plan_manager_task import DailyPlanManagerTask + + +class TestDailyPlanManagerTask(object): + def test_simple_case(self, dbsession): + task = DailyPlanManagerTask() + # Populate DB + owner_in_enterprise_plan = OwnerFactory.create( + service="github", plan=PlanName.ENTERPRISE_CLOUD_MONTHLY.value + ) + owner_not_in_enterprise_plan = OwnerFactory.create( + service="github", plan=DEFAULT_FREE_PLAN + ) + + valid_token = OrgLevelTokenFactory.create(owner=owner_in_enterprise_plan) + invalid_token = OrgLevelTokenFactory.create(owner=owner_not_in_enterprise_plan) + invalid_token_2 = OrgLevelTokenFactory.create( + owner=owner_not_in_enterprise_plan + ) + + dbsession.add(valid_token) + dbsession.add(invalid_token) + dbsession.add(invalid_token_2) + dbsession.flush() + assert dbsession.query(OrganizationLevelToken).count() == 3 + + result = task.run_cron_task(dbsession) + assert result.get("checked") == True + assert result.get("deleted") == 2 + assert dbsession.query(OrganizationLevelToken).count() == 1 + assert ( + dbsession.query(OrganizationLevelToken).first().owner.ownerid + == owner_in_enterprise_plan.ownerid + ) + + def test_get_min_seconds_interval_between_executions(self, dbsession): + assert isinstance( + DailyPlanManagerTask.get_min_seconds_interval_between_executions(), int + ) + # The specifics don't matter, but the number needs to be big + assert ( + DailyPlanManagerTask.get_min_seconds_interval_between_executions() > 86000 + ) diff --git a/apps/worker/tasks/tests/unit/test_preprocess_upload.py b/apps/worker/tasks/tests/unit/test_preprocess_upload.py new file mode 100644 index 0000000000..a99b29b5c9 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_preprocess_upload.py @@ -0,0 +1,178 @@ +import pytest +from redis.exceptions import LockError + +from database.models.reports import Upload +from database.tests.factories.core import ( + CommitFactory, + ReportFactory, + RepositoryFactory, +) +from helpers.exceptions import OwnerWithoutValidBotError, RepositoryWithoutValidBotError +from services.report import ReportService +from tasks.preprocess_upload import PreProcessUpload + + +class TestPreProcessUpload(object): + @pytest.mark.django_db(databases={"default"}) + def test_preprocess_task( + self, + mocker, + mock_configuration, + dbsession, + mock_storage, + mock_redis, + celery_app, + sample_report, + ): + # get_existing_report_for_commit gets called for the parent commit + mocker.patch.object( + ReportService, + "get_existing_report_for_commit", + return_value=sample_report, + ) + commit_yaml = { + "flag_management": { + "individual_flags": [ + { + "name": "unit", + "carryforward": True, + } + ] + } + } + mocker.patch( + "services.repository.fetch_commit_yaml_from_provider", + return_value=commit_yaml, + ) + mock_save_commit = mocker.patch( + "services.repository.save_repo_yaml_to_database_if_needed" + ) + + def fake_possibly_shift(report, base, head): + return report + + mock_possibly_shift = mocker.patch.object( + ReportService, + "_possibly_shift_carryforward_report", + side_effect=fake_possibly_shift, + ) + commit, report = self.create_commit_and_report(dbsession) + + result = PreProcessUpload().process_impl_within_lock( + dbsession, + repoid=commit.repository.repoid, + commitid=commit.commitid, + report_code=None, + ) + for sess_id in sample_report.sessions.keys(): + upload = ( + dbsession.query(Upload) + .filter_by(report_id=commit.report.id_, order_number=sess_id) + .first() + ) + assert upload + assert upload.flag_names == ["unit"] + assert result == { + "preprocessed_upload": True, + "reportid": str(report.external_id), + "updated_commit": False, + } + mock_save_commit.assert_called_with(commit, commit_yaml) + mock_possibly_shift.assert_called() + + def create_commit_and_report(self, dbsession): + repository = RepositoryFactory() + parent_commit = CommitFactory(repository=repository) + parent_commit_report = ReportFactory(commit=parent_commit) + commit = CommitFactory( + _report_json=None, + parent_commit_id=parent_commit.commitid, + repository=repository, + ) + report = ReportFactory(commit=commit) + dbsession.add(parent_commit) + dbsession.add(parent_commit_report) + dbsession.add(commit) + dbsession.add(report) + dbsession.flush() + return commit, report + + def test_run_impl_already_running(self, dbsession, mock_redis): + mock_redis.get = lambda _name: True + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + result = PreProcessUpload().run_impl( + dbsession, + repoid=commit.repository.repoid, + commitid=commit.commitid, + report_code=None, + ) + assert result == {"preprocessed_upload": False, "reason": "already_running"} + + def test_run_impl_unobtainable_lock(self, dbsession, mock_redis): + mock_redis.get = lambda _name: False + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + mock_redis.lock.side_effect = LockError() + result = PreProcessUpload().run_impl( + dbsession, + repoid=commit.repository.repoid, + commitid=commit.commitid, + report_code=None, + ) + assert result == { + "preprocessed_upload": False, + "reason": "unable_to_acquire_lock", + } + + def test_get_repo_service_repo_and_owner_lack_bot(self, dbsession, mocker): + mock_owner_bot = mocker.patch( + "shared.bots.repo_bots.get_owner_or_appropriate_bot" + ) + mock_owner_bot.side_effect = OwnerWithoutValidBotError() + + mock_github_installations = mocker.patch( + "shared.bots.github_apps.get_github_app_info_for_owner" + ) + mock_github_installations.return_value = [] + + mock_save_error = mocker.patch("tasks.preprocess_upload.save_commit_error") + + commit = CommitFactory.create(repository__private=True, repository__bot=None) + repo_service = PreProcessUpload().get_repo_service(commit, None) + + assert repo_service is None + mock_save_error.assert_called() + + def test_get_repo_provider_service_no_bot(self, dbsession, mocker): + mocker.patch("tasks.preprocess_upload.save_commit_error") + mock_get_repo_service = mocker.patch( + "tasks.preprocess_upload.get_repo_provider_service" + ) + mock_get_repo_service.side_effect = RepositoryWithoutValidBotError() + commit = CommitFactory.create() + repo_provider = PreProcessUpload().get_repo_service(commit, None) + assert repo_provider is None + + def test_preprocess_upload_fail_no_provider_service(self, dbsession, mocker): + mocker.patch("tasks.preprocess_upload.save_commit_error") + mock_get_repo_service = mocker.patch( + "tasks.preprocess_upload.get_repo_provider_service" + ) + mock_get_repo_service.side_effect = RepositoryWithoutValidBotError() + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + res = PreProcessUpload().process_impl_within_lock( + db_session=dbsession, + repoid=commit.repoid, + commitid=commit.commitid, + report_code=None, + ) + assert res == { + "preprocessed_upload": False, + "updated_commit": False, + "error": "Failed to get repository_service", + } diff --git a/apps/worker/tasks/tests/unit/test_process_flakes.py b/apps/worker/tasks/tests/unit/test_process_flakes.py new file mode 100644 index 0000000000..287cd75219 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_process_flakes.py @@ -0,0 +1,515 @@ +import datetime as dt +from collections import defaultdict + +import pytest +import time_machine +from shared.django_apps.core.models import Commit +from shared.django_apps.core.tests.factories import CommitFactory, RepositoryFactory +from shared.django_apps.reports.models import ( + CommitReport, + DailyTestRollup, + Flake, + TestInstance, +) +from shared.django_apps.reports.tests.factories import ( + DailyTestRollupFactory, + FlakeFactory, + TestFactory, + TestInstanceFactory, + UploadFactory, +) +from shared.helpers.redis import get_redis_connection + +from services.processing.flake_processing import ( + create_flake, + fetch_curr_flakes, + get_test_instances, + update_flake, +) +from tasks.process_flakes import ( + NEW_KEY, + OLD_KEY, + ProcessFlakesTask, + process_flakes_task, +) +from tests.helpers import mock_all_plans_and_tiers + + +class RepoSimulator: + def __init__(self): + self.repo = RepositoryFactory() + self.repo.save() + self.test_count = 0 + self.branch_number = 0 + + self.redis = get_redis_connection() + + self.test_map = defaultdict(lambda: TestFactory(id=self.test_count)) + + def run_task(self, repo_id: int, commit_id: str): + self.redis.delete(NEW_KEY.format(repo_id)) + self.redis.delete(OLD_KEY.format(repo_id)) + self.redis.rpush(NEW_KEY.format(repo_id), commit_id) + ProcessFlakesTask().run_impl(None, repo_id=repo_id, commit_id=commit_id) + + def create_commit(self) -> Commit: + c = CommitFactory( + repository=self.repo, merged=False, branch=str(self.branch_number) + ) + c.save() + self.branch_number += 1 + self.test_count = 0 + return c + + def add_test_instance( + self, + c: Commit, + outcome: str = TestInstance.Outcome.PASS.value, + state: str = "processed", + ) -> TestInstance: + upload = UploadFactory( + report__commit=c, + report__report_type=CommitReport.ReportType.TEST_RESULTS.value, + state=state, + ) + upload.save() + ti = TestInstanceFactory( + commitid=c.commitid, + repoid=self.repo.repoid, + branch=c.branch, + outcome=outcome, + test=self.test_map[self.test_count], + upload=upload, + ) + ti.save() + + rollup, _ = DailyTestRollup.objects.get_or_create( + repoid=self.repo.repoid, + date=dt.date.today(), + test=self.test_map[self.test_count], + branch=c.branch, + defaults={ + "pass_count": 0, + "skip_count": 0, + "fail_count": 0, + "flaky_fail_count": 0, + "avg_duration_seconds": 0.0, + "last_duration_seconds": 0.0, + "latest_run": dt.datetime.now(tz=dt.timezone.utc), + "commits_where_fail": [], + }, + ) + + match outcome: + case TestInstance.Outcome.PASS.value: + rollup.pass_count += 1 + case TestInstance.Outcome.SKIP.value: + rollup.skip_count += 1 + case _: + rollup.fail_count += 1 + + s = set(rollup.commits_where_fail) + s.add(c.commitid) + rollup.commits_where_fail = list(s) + + flake = Flake.objects.filter( + repository_id=self.repo.repoid, + test=self.test_map[self.test_count], + ).first() + + if flake: + rollup.flaky_fail_count += 1 + + rollup.save() + + self.test_count += 1 + + return ti + + def reset(self): + self.repo = RepositoryFactory() + self.repo.save() + self.test_count = 0 + + +@pytest.mark.django_db(transaction=True) +def test_generate_flake_dict(): + repo = RepositoryFactory() + + flake_dict = fetch_curr_flakes(repo.repoid) + + assert len(flake_dict) == 0 + + f = FlakeFactory(repository=repo, test__id="id") + f.save() + + flake_dict = fetch_curr_flakes(repo.repoid) + + assert len(flake_dict) == 1 + assert "id" in flake_dict + + +@pytest.mark.django_db(transaction=True) +def test_get_test_instances_when_test_is_flaky(): + repo = RepositoryFactory() + commit = CommitFactory() + upload = UploadFactory(report__commit=commit) + + ti = TestInstanceFactory( + commitid=commit.commitid, + repoid=repo.repoid, + branch="main", + outcome=TestInstance.Outcome.FAILURE.value, + upload=upload, + ) + ti.save() + + tis = get_test_instances(upload, flaky_tests=[ti.test_id]) + assert len(tis) == 1 + assert tis[0].commitid + + +@pytest.mark.django_db(transaction=True) +def test_get_test_instances_when_instance_is_failure(): + repo = RepositoryFactory() + commit = CommitFactory() + upload = UploadFactory(report__commit=commit) + + ti = TestInstanceFactory( + commitid=commit.commitid, + repoid=repo.repoid, + branch="main", + outcome=TestInstance.Outcome.FAILURE.value, + upload=upload, + ) + ti.save() + + tis = get_test_instances(upload, flaky_tests=[]) + assert len(tis) == 1 + assert tis[0].commitid + + +@pytest.mark.django_db(transaction=True) +def test_get_test_instances_when_test_is_flaky_and_instance_is_skip(): + repo = RepositoryFactory() + commit = CommitFactory() + upload = UploadFactory(report__commit=commit) + + ti = TestInstanceFactory( + commitid=commit.commitid, + repoid=repo.repoid, + branch="main", + outcome=TestInstance.Outcome.SKIP.value, + upload=upload, + ) + ti.save() + + tis = get_test_instances(upload, flaky_tests=[ti.test_id]) + assert len(tis) == 0 + + +@pytest.mark.django_db(transaction=True) +def test_get_test_instances_when_instance_is_pass(): + repo = RepositoryFactory() + commit = CommitFactory() + upload = UploadFactory(report__commit=commit) + + ti = TestInstanceFactory( + commitid=commit.commitid, + repoid=repo.repoid, + branch="main", + outcome=TestInstance.Outcome.PASS.value, + upload=upload, + ) + ti.save() + + tis = get_test_instances(upload, flaky_tests=[]) + assert len(tis) == 0 + + +@pytest.mark.django_db(transaction=True) +def test_update_flake_pass(): + rs = RepoSimulator() + c = rs.create_commit() + ti = rs.add_test_instance(c, outcome=TestInstance.Outcome.PASS.value) + + f = FlakeFactory(test=ti.test, repository=rs.repo) + f.save() + assert f.count == 0 + assert f.recent_passes_count == 0 + + update_flake(f, ti) + + assert f.count == 1 + assert f.recent_passes_count == 1 + + +@pytest.mark.django_db(transaction=True) +def test_update_flake_fail(): + rs = RepoSimulator() + c = rs.create_commit() + ti = rs.add_test_instance(c, outcome=TestInstance.Outcome.FAILURE.value) + + f = FlakeFactory(test=ti.test, repository=rs.repo) + f.save() + assert f.count == 0 + assert f.recent_passes_count == 0 + + update_flake(f, ti) + + assert f.count == 1 + assert f.recent_passes_count == 0 + assert f.fail_count == 1 + + +@pytest.mark.django_db(transaction=True) +def test_upsert_failed_flakes(): + repo = RepositoryFactory() + repo.save() + commit = CommitFactory() + commit.save() + ti = TestInstanceFactory( + commitid=commit.commitid, repoid=repo.repoid, branch="main" + ) + ti.save() + + rollup = DailyTestRollupFactory( + repoid=repo.repoid, + test=ti.test, + branch="main", + date=dt.date.today(), + flaky_fail_count=0, + ) + rollup.save() + + f, r = create_flake(ti, repo.repoid) + assert f.count == 1 + assert f.fail_count == 1 + assert f.recent_passes_count == 0 + assert f.test == ti.test + + assert r is not None + assert r.flaky_fail_count == 1 + + +@pytest.mark.django_db(transaction=False) +def test_upsert_failed_flakes_rollup_is_none(): + repo = RepositoryFactory() + repo.save() + commit = CommitFactory() + commit.save() + ti = TestInstanceFactory( + commitid=commit.commitid, repoid=repo.repoid, branch="main" + ) + ti.save() + + f, r = create_flake(ti, repo.repoid) + assert f.count == 1 + assert f.fail_count == 1 + assert f.recent_passes_count == 0 + assert f.test == ti.test + + assert r is None + + +@pytest.mark.django_db(transaction=True) +def test_it_handles_only_passes(): + rs = RepoSimulator() + c1 = rs.create_commit() + rs.add_test_instance(c1) + rs.add_test_instance(c1) + + rs.run_task(rs.repo.repoid, c1.commitid) + + assert len(Flake.objects.all()) == 0 + + +@time_machine.travel(dt.datetime.now(tz=dt.UTC), tick=False) +@pytest.mark.django_db(transaction=True) +def test_it_creates_flakes_from_processed_uploads(): + rs = RepoSimulator() + c1 = rs.create_commit() + rs.add_test_instance(c1, state="finished") + rs.add_test_instance( + c1, outcome=TestInstance.Outcome.FAILURE.value, state="processed" + ) + + rs.run_task(rs.repo.repoid, c1.commitid) + + assert len(Flake.objects.all()) == 1 + flake = Flake.objects.first() + + assert flake is not None + assert flake.count == 1 + assert flake.fail_count == 1 + assert flake.start_date == dt.datetime.now(tz=dt.UTC) + + +@time_machine.travel(dt.datetime.now(tz=dt.UTC), tick=False) +@pytest.mark.django_db(transaction=True) +def test_it_does_not_create_flakes_from_flake_processed_uploads(): + rs = RepoSimulator() + c1 = rs.create_commit() + rs.add_test_instance(c1, state="processed") + rs.add_test_instance( + c1, outcome=TestInstance.Outcome.FAILURE.value, state="flake_processed" + ) + + rs.run_task(rs.repo.repoid, c1.commitid) + + assert len(Flake.objects.all()) == 0 + + +@time_machine.travel(dt.datetime.now(tz=dt.UTC), tick=False) +@pytest.mark.django_db(transaction=True) +def test_it_processes_two_commits_separately(): + rs = RepoSimulator() + c1 = rs.create_commit() + rs.add_test_instance(c1, outcome=TestInstance.Outcome.FAILURE.value) + + rs.run_task(rs.repo.repoid, c1.commitid) + + c2 = rs.create_commit() + rs.add_test_instance(c2, outcome=TestInstance.Outcome.FAILURE.value) + + rs.run_task(rs.repo.repoid, c2.commitid) + + assert len(Flake.objects.all()) == 1 + flake = Flake.objects.first() + + assert flake is not None + assert flake.recent_passes_count == 0 + assert flake.count == 2 + assert flake.fail_count == 2 + assert flake.start_date == dt.datetime.now(dt.UTC) + + +@pytest.mark.django_db(transaction=True) +def test_it_creates_flakes_expires(): + with time_machine.travel(dt.datetime.now(tz=dt.UTC), tick=False) as traveller: + rs = RepoSimulator() + commits: list[str] = [] + c1 = rs.create_commit() + rs.add_test_instance(c1, outcome=TestInstance.Outcome.FAILURE.value) + + rs.run_task(rs.repo.repoid, c1.commitid) + + old_time = dt.datetime.now(dt.UTC) + traveller.shift(dt.timedelta(seconds=100)) + new_time = dt.datetime.now(dt.UTC) + + for _ in range(0, 29): + c = rs.create_commit() + rs.add_test_instance(c, outcome=TestInstance.Outcome.PASS.value) + commits.append(c.commitid) + + rs.run_task(rs.repo.repoid, c.commitid) + + assert len(Flake.objects.all()) == 1 + flake = Flake.objects.first() + + assert flake is not None + assert flake.recent_passes_count == 29 + assert flake.count == 30 + assert flake.fail_count == 1 + assert flake.start_date == old_time + assert flake.end_date is None + + c = rs.create_commit() + rs.add_test_instance(c, outcome=TestInstance.Outcome.PASS.value) + + rs.run_task(rs.repo.repoid, c.commitid) + + assert len(Flake.objects.all()) == 1 + flake = Flake.objects.first() + + assert flake is not None + assert flake.recent_passes_count == 30 + assert flake.count == 31 + assert flake.fail_count == 1 + assert flake.start_date == old_time + assert flake.end_date == new_time + + +@pytest.mark.django_db(transaction=True) +def test_it_creates_rollups(): + with time_machine.travel("1970-1-1T00:00:00Z"): + rs = RepoSimulator() + c1 = rs.create_commit() + rs.add_test_instance(c1, outcome=TestInstance.Outcome.FAILURE.value) + rs.add_test_instance(c1, outcome=TestInstance.Outcome.FAILURE.value) + + rs.run_task(rs.repo.repoid, c1.commitid) + + with time_machine.travel("1970-1-2T00:00:00Z"): + c2 = rs.create_commit() + rs.add_test_instance(c2, outcome=TestInstance.Outcome.FAILURE.value) + rs.add_test_instance(c2, outcome=TestInstance.Outcome.FAILURE.value) + + rs.run_task(rs.repo.repoid, c2.commitid) + + rollups = DailyTestRollup.objects.all().order_by("date") + + assert len(rollups) == 4 + + assert rollups[0].fail_count == 1 + assert rollups[0].flaky_fail_count == 1 + assert rollups[0].date == dt.date.today() - dt.timedelta(days=1) + + assert rollups[1].fail_count == 1 + assert rollups[1].flaky_fail_count == 1 + assert rollups[1].date == dt.date.today() - dt.timedelta(days=1) + + assert rollups[2].fail_count == 1 + assert rollups[2].flaky_fail_count == 1 + assert rollups[2].date == dt.date.today() + + assert rollups[3].fail_count == 1 + assert rollups[3].flaky_fail_count == 1 + assert rollups[3].date == dt.date.today() + + +@pytest.mark.django_db(transaction=False) +def test_it_locks(mocker): + mock_all_plans_and_tiers() + result2 = None + + def first_call(repo_id: int, commit_id: str): + nonlocal result2 + if result2 is None: + result2 = process_flakes_task.s( + repo_id=repo_id, + commit_id=commit_id, + use_timeseries=False, + ).apply() + return None + + mock_process = mocker.patch( + "tasks.process_flakes.process_flake_for_repo_commit", + side_effect=first_call, + ) + + redis_client = get_redis_connection() + repo_id = 1 + commit_ids = ["abc123", "def456", "ghi789"] + + redis_client.delete(OLD_KEY.format(repo_id)) + redis_client.delete(NEW_KEY.format(repo_id)) + for commit_id in commit_ids: + redis_client.rpush(NEW_KEY.format(repo_id), commit_id) + + result1 = process_flakes_task.s( + repo_id=repo_id, + commit_id=commit_ids[0], + use_timeseries=False, + ).apply() + + assert result1 is not None + assert result2 is not None + assert result1.get() == {"successful": True} + assert result2.get() == {"successful": False} + + assert mock_process.call_count == len(commit_ids) + mock_process.assert_has_calls( + [mocker.call(repo_id, commit_id) for commit_id in commit_ids] + ) diff --git a/apps/worker/tasks/tests/unit/test_save_commit_measurements.py b/apps/worker/tasks/tests/unit/test_save_commit_measurements.py new file mode 100644 index 0000000000..8c20600e76 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_save_commit_measurements.py @@ -0,0 +1,83 @@ +from database.tests.factories.core import CommitFactory, OwnerFactory, RepositoryFactory +from services.timeseries import MeasurementName +from tasks.save_commit_measurements import SaveCommitMeasurementsTask + + +class TestSaveCommitMeasurements(object): + def test_save_commit_measurements_success(self, dbsession, mocker): + save_commit_measurements_mock = mocker.patch( + "tasks.save_commit_measurements.save_commit_measurements" + ) + owner = OwnerFactory.create(service="github") + dbsession.add(owner) + + repository = RepositoryFactory.create( + owner=owner, languages_last_updated=None, languages=[] + ) + dbsession.add(repository) + + commit = CommitFactory.create(repository=repository) + dbsession.add(commit) + dbsession.flush() + + task = SaveCommitMeasurementsTask() + assert task.run_impl( + dbsession, + commitid=commit.commitid, + repoid=commit.repoid, + dataset_names=[ + MeasurementName.coverage.value, + MeasurementName.flag_coverage.value, + ], + ) == {"successful": True} + save_commit_measurements_mock.assert_called_with( + commit=commit, + dataset_names=[ + MeasurementName.coverage.value, + MeasurementName.flag_coverage.value, + ], + ) + + def test_save_commit_measurements_no_commit(self, dbsession): + owner = OwnerFactory.create(service="github") + dbsession.add(owner) + dbsession.flush() + + task = SaveCommitMeasurementsTask() + assert task.run_impl( + dbsession, commitid="123asdf", repoid=123, dataset_names=[] + ) == { + "successful": False, + "error": "no_commit_in_db", + } + + def test_save_commit_measurements_exception(self, mocker, dbsession): + save_commit_measurements_mock = mocker.patch( + "tasks.save_commit_measurements.save_commit_measurements" + ) + save_commit_measurements_mock.side_effect = Exception("Muy malo") + owner = OwnerFactory.create(service="github") + dbsession.add(owner) + + repository = RepositoryFactory.create( + owner=owner, languages_last_updated=None, languages=[] + ) + dbsession.add(repository) + + commit = CommitFactory.create(repository=repository) + dbsession.add(commit) + dbsession.flush() + + task = SaveCommitMeasurementsTask() + assert task.run_impl( + dbsession, + commitid=commit.commitid, + repoid=commit.repoid, + dataset_names=[ + MeasurementName.coverage.value, + MeasurementName.flag_coverage.value, + ], + ) == { + "successful": False, + "error": "exception", + } diff --git a/apps/worker/tasks/tests/unit/test_save_report_results_task.py b/apps/worker/tasks/tests/unit/test_save_report_results_task.py new file mode 100644 index 0000000000..b86ec19cc6 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_save_report_results_task.py @@ -0,0 +1,359 @@ +import pytest +from shared.reports.resources import Report + +from database.tests.factories import ( + CommitFactory, + OwnerFactory, + PullFactory, + RepositoryFactory, +) +from database.tests.factories.core import ReportFactory, ReportResultsFactory +from helpers.exceptions import RepositoryWithoutValidBotError +from services.notification.notifiers.status.patch import PatchStatusNotifier +from services.report import ReportService +from services.repository import EnrichedPull +from tasks.save_report_results import SaveReportResultsTask + + +@pytest.fixture +def enriched_pull(dbsession, request): + repository = RepositoryFactory.create( + owner__username="codecov", + owner__service="github", + owner__unencrypted_oauth_token="testtlxuu2kfef3km1fbecdlmnb2nvpikvmoadi3", + owner__plan="users-pr-inappm", + name="example-python", + image_token="abcdefghij", + private=True, + ) + dbsession.add(repository) + dbsession.flush() + base_commit = CommitFactory.create( + repository=repository, + author__username=f"base{request.node.name[-20:]}", + author__service="github", + ) + head_commit = CommitFactory.create( + repository=repository, + author__username=f"head{request.node.name[-20:]}", + author__service="github", + ) + pull = PullFactory.create( + author__service="github", + repository=repository, + base=base_commit.commitid, + head=head_commit.commitid, + state="merged", + ) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.add(pull) + dbsession.flush() + provider_pull = { + "author": {"id": "7123", "username": "tomcat"}, + "base": { + "branch": "master", + "commitid": "b92edba44fdd29fcc506317cc3ddeae1a723dd08", + }, + "head": { + "branch": "reason/some-testing", + "commitid": "a06aef4356ca35b34c5486269585288489e578db", + }, + "number": "1", + "id": "1", + "state": "open", + "title": "Creating new code for reasons no one knows", + } + return EnrichedPull(database_pull=pull, provider_pull=provider_pull) + + +class TestSaveReportResultsTaskHelpers(object): + def test_fetch_parent(self, dbsession): + task = SaveReportResultsTask() + owner = OwnerFactory.create( + unencrypted_oauth_token="testlln8sdeec57lz83oe3l8y9qq4lhqat2f1kzm", + username="ThiagoCodecov", + ) + repository = RepositoryFactory.create( + owner=owner, yaml={"codecov": {"max_report_age": "1y ago"}} + ) + different_repository = RepositoryFactory.create( + owner=owner, yaml={"codecov": {"max_report_age": "1y ago"}} + ) + dbsession.add(repository) + right_parent_commit = CommitFactory.create( + message="", + pullid=None, + branch="master", + commitid="17a71a9a2f5335ed4d00496c7bbc6405f547a527", + repository=repository, + ) + wrong_parent_commit = CommitFactory.create( + message="", + pullid=None, + branch="master", + commitid="17a71a9a2f5335ed4d00496c7bbc6405f547a527", + repository=different_repository, + ) + another_wrong_parent_commit = CommitFactory.create( + message="", + pullid=None, + branch="master", + commitid="bf303450570d7a84f8c3cdedac5ac23e27a64c19", + repository=repository, + ) + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + parent_commit_id="17a71a9a2f5335ed4d00496c7bbc6405f547a527", + repository=repository, + ) + dbsession.add(commit) + dbsession.add(another_wrong_parent_commit) + dbsession.add(repository) + dbsession.add(different_repository) + dbsession.add(right_parent_commit) + dbsession.add(wrong_parent_commit) + dbsession.flush() + assert task.fetch_parent(commit) == right_parent_commit + + def test_fetch_report(self, dbsession): + task = SaveReportResultsTask() + owner = OwnerFactory.create( + unencrypted_oauth_token="testlln8sdeec57lz83oe3l8y9qq4lhqat2f1kzm", + username="ThiagoCodecov", + ) + repository = RepositoryFactory.create( + owner=owner, yaml={"codecov": {"max_report_age": "1y ago"}} + ) + different_repository = RepositoryFactory.create( + owner=owner, yaml={"codecov": {"max_report_age": "1y ago"}} + ) + dbsession.add(repository) + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + repository=repository, + ) + different_commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + repository=different_repository, + ) + # two reports with the same commit-sha and code, but different repos + report = ReportFactory.create(commit=commit, code="report1") + another_report = ReportFactory.create(commit=different_commit, code="report1") + dbsession.add(commit) + dbsession.add(repository) + dbsession.add(different_repository) + dbsession.add(different_commit) + dbsession.add(report) + dbsession.add(another_report) + dbsession.flush() + assert task.fetch_report(commit, "report1") == report + + def test_fetch_commit(self, dbsession): + task = SaveReportResultsTask() + owner = OwnerFactory.create( + unencrypted_oauth_token="testlln8sdeec57lz83oe3l8y9qq4lhqat2f1kzm", + username="ThiagoCodecov", + ) + repository = RepositoryFactory.create( + owner=owner, yaml={"codecov": {"max_report_age": "1y ago"}} + ) + different_repository = RepositoryFactory.create( + owner=owner, yaml={"codecov": {"max_report_age": "1y ago"}} + ) + dbsession.add(repository) + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + repository=repository, + ) + different_commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + repository=different_repository, + ) + dbsession.add(commit) + dbsession.add(repository) + dbsession.add(different_repository) + dbsession.add(different_commit) + dbsession.flush() + assert ( + task.fetch_commit(dbsession, repository.repoid, commit.commitid) == commit + ) + + def test_fetch_base_commit(self, dbsession, enriched_pull): + task = SaveReportResultsTask() + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + parent_commit_id="17a71a9a2f5335ed4d00496c7bbc6405f547a527", + ) + task.fetch_base_commit( + commit, enriched_pull + ) == enriched_pull.database_pull.base + + def test_fetch_base_and_head_reports(self, dbsession, enriched_pull, mocker): + mocked_reports = mocker.patch.object( + ReportService, "get_existing_report_for_commit", return_value=Report() + ) + task = SaveReportResultsTask() + base_report, head_report = task.fetch_base_and_head_reports( + {}, + enriched_pull.database_pull.head, + enriched_pull.database_pull.base, + "report_code", + ) + mocked_reports.assert_called() + assert base_report is not None + assert head_report is not None + + def test_fetch_yaml_dict(self, dbsession, mocker, mock_repo_provider): + task = SaveReportResultsTask() + mocked_fetch_yaml = mocker.patch("tasks.save_report_results.get_current_yaml") + mocked_fetch_yaml.return_value = {"coverage": {"status": {"patch": True}}} + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + ) + dbsession.add(commit) + dbsession.flush() + yaml = task.fetch_yaml_dict(None, commit, mock_repo_provider) + mocked_fetch_yaml.assert_called_with(commit, mock_repo_provider) + assert yaml == {"coverage": {"status": {"patch": True}}} + + def test_save_report_results_into_db(self, dbsession): + report = ReportFactory.create() + report_results = ReportResultsFactory.create(report=report) + dbsession.add(report) + dbsession.add(report_results) + dbsession.flush() + result = { + "state": "completed", + "message": "Coverage not affected when comparing aba5300...014b924", + } + task = SaveReportResultsTask() + task.save_results_into_db(result, report) + assert report_results + assert report_results.result == result + + +class TestSaveReportResultsTask(object): + def test_save_patch_results_successful(self, dbsession, mocker): + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + ) + report = ReportFactory.create(commit=commit, code="report1") + report_results = ReportResultsFactory.create(report=report) + dbsession.add(report) + dbsession.add(report_results) + dbsession.add(commit) + dbsession.flush() + + mocked_fetch_pull = mocker.patch( + "tasks.save_report_results.fetch_and_update_pull_request_information_from_commit" + ) + mocker.patch.object( + PatchStatusNotifier, + "build_payload", + return_value={"state": "success", "message": "somemessage"}, + ) + mocker.patch.object( + ReportService, "get_existing_report_for_commit", return_value=Report() + ) + mocked_fetch_pull.return_value = None + task = SaveReportResultsTask() + result = task.run_impl( + dbsession, + repoid=commit.repository.repoid, + commitid=commit.commitid, + report_code="report1", + current_yaml={}, + ) + assert result == {"report_results_saved": True, "reason": "success"} + + def test_save_patch_results_no_valid_bot(self, dbsession, mocker): + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + ) + report = ReportFactory.create(commit=commit, code="report1") + dbsession.add(report) + dbsession.add(commit) + dbsession.flush() + + mocked_fetch_pull = mocker.patch( + "tasks.save_report_results.fetch_and_update_pull_request_information_from_commit" + ) + mocker.patch.object( + ReportService, "get_existing_report_for_commit", return_value=Report() + ) + mocked_fetch_pull.return_value = None + mock_get_repo_service = mocker.patch( + "tasks.save_report_results.get_repo_provider_service" + ) + mock_get_repo_service.side_effect = RepositoryWithoutValidBotError() + task = SaveReportResultsTask() + result = task.run_impl( + dbsession, + repoid=commit.repository.repoid, + commitid=commit.commitid, + report_code="report1", + current_yaml={}, + ) + assert result == { + "report_results_saved": False, + "reason": "repository without valid bot", + } + + def test_save_patch_results_no_head_report(self, dbsession, mocker): + commit = CommitFactory.create( + message="", + pullid=None, + branch="test-branch-1", + commitid="649eaaf2924e92dc7fd8d370ddb857033231e67a", + ) + report = ReportFactory.create(commit=commit, code="report1") + dbsession.add(report) + dbsession.add(commit) + dbsession.flush() + mocked_fetch_pull = mocker.patch( + "tasks.save_report_results.fetch_and_update_pull_request_information_from_commit" + ) + mocker.patch.object( + ReportService, "get_existing_report_for_commit", return_value=None + ) + mocked_fetch_pull.return_value = None + task = SaveReportResultsTask() + result = task.run_impl( + dbsession, + repoid=commit.repository.repoid, + commitid=commit.commitid, + report_code="report1", + current_yaml={}, + ) + assert result == { + "report_results_saved": False, + "reason": "No head report found.", + } diff --git a/apps/worker/tasks/tests/unit/test_send_email_task.py b/apps/worker/tasks/tests/unit/test_send_email_task.py new file mode 100644 index 0000000000..9a1a0d6ca8 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_send_email_task.py @@ -0,0 +1,216 @@ +from pathlib import Path + +import pytest +from jinja2 import TemplateNotFound, UndefinedError +from shared.config import ConfigHelper + +from database.tests.factories import OwnerFactory +from services.smtp import SMTPService, SMTPServiceError +from tasks.send_email import SendEmailTask + +here = Path(__file__) + +to_addr = "test_to@codecov.io" + + +@pytest.fixture +def mock_configuration_no_smtp(mocker): + m = mocker.patch("shared.config._get_config_instance") + mock_config = ConfigHelper() + m.return_value = mock_config + our_config = { + "bitbucket": {"bot": {"username": "codecov-io"}}, + "services": { + "minio": { + "access_key_id": "codecov-default-key", + "bucket": "archive", + "hash_key": "88f572f4726e4971827415efa8867978", + "secret_access_key": "codecov-default-secret", + "verify_ssl": False, + }, + }, + "setup": { + "codecov_url": "https://codecov.io", + "encryption_secret": "zp^P9*i8aR3", + "telemetry": { + "endpoint_override": "abcde", + }, + }, + } + mock_config.set_params(our_config) + return mock_config + + +class TestSendEmailTask(object): + def test_send_email( + self, + mocker, + mock_configuration, + dbsession, + mock_storage, + mock_smtp, + mock_redis, + ): + mock_smtp.configure_mock(**{"send.return_value": None}) + owner = OwnerFactory.create(email=to_addr) + dbsession.add(owner) + dbsession.flush() + result = SendEmailTask().run_impl( + db_session=dbsession, + to_addr=owner.email, + subject="Test", + template_name="test", + from_addr="test_from@codecov.io", + username="test_username", + ) + + assert result == {"email_successful": True, "err_msg": None} + + def test_send_email_uses_default_from_addr( + self, mocker, mock_configuration, dbsession, mock_smtp + ): + mock_smtp.configure_mock(**{"send.return_value": None}) + owner = OwnerFactory.create(email=to_addr) + dbsession.add(owner) + dbsession.flush() + result = SendEmailTask().run_impl( + db_session=dbsession, + to_addr=owner.email, + subject="Test", + template_name="test", + username="test_username", + ) + assert result == { + "email_successful": True, + "err_msg": None, + } + + def test_send_email_non_existent_template( + self, mocker, mock_configuration, dbsession, mock_smtp + ): + owner = OwnerFactory.create(email=to_addr) + dbsession.add(owner) + dbsession.flush() + with pytest.raises(TemplateNotFound): + _ = SendEmailTask().run_impl( + db_session=dbsession, + to_addr=owner.email, + subject="Test", + template_name="non_existent", + username="test_username", + ) + + def test_send_email_missing_kwargs( + self, mocker, mock_configuration, dbsession, mock_smtp + ): + owner = OwnerFactory.create(email=to_addr) + dbsession.add(owner) + dbsession.flush() + with pytest.raises(UndefinedError): + result = SendEmailTask().run_impl( + db_session=dbsession, + to_addr=owner.email, + subject="Test", + template_name="test", + ) + + def test_send_email_recipients_refused( + self, mocker, mock_configuration, dbsession, mock_smtp + ): + mock_smtp.configure_mock( + **{"send.side_effect": SMTPServiceError("All recipients were refused")} + ) + + owner = OwnerFactory.create(email=to_addr) + dbsession.add(owner) + dbsession.flush() + result = SendEmailTask().run_impl( + db_session=dbsession, + to_addr=owner.email, + subject="Test", + template_name="test", + username="test_username", + ) + + assert result["email_successful"] == False + assert result["err_msg"] == "All recipients were refused" + + def test_send_email_sender_refused( + self, mocker, mock_configuration, dbsession, mock_smtp + ): + mock_smtp.configure_mock( + **{"send.side_effect": SMTPServiceError("Sender was refused")} + ) + owner = OwnerFactory.create(email=to_addr) + dbsession.add(owner) + dbsession.flush() + result = SendEmailTask().run_impl( + db_session=dbsession, + to_addr=owner.email, + subject="Test", + template_name="test", + username="test_username", + ) + assert result["email_successful"] == False + assert result["err_msg"] == "Sender was refused" + + def test_send_email_data_error( + self, mocker, mock_configuration, dbsession, mock_smtp + ): + mock_smtp.configure_mock( + **{ + "send.side_effect": SMTPServiceError( + "The SMTP server did not accept the data" + ) + } + ) + owner = OwnerFactory.create(email=to_addr) + dbsession.add(owner) + dbsession.flush() + result = SendEmailTask().run_impl( + db_session=dbsession, + to_addr=owner.email, + subject="Test", + template_name="test", + username="test_username", + ) + assert result["email_successful"] == False + assert result["err_msg"] == "The SMTP server did not accept the data" + + def test_send_email_sends_errs( + self, mocker, mock_configuration, dbsession, mock_smtp + ): + mock_smtp.configure_mock( + **{"send.side_effect": SMTPServiceError("123 abc 456 def")} + ) + owner = OwnerFactory.create(email=to_addr) + dbsession.add(owner) + dbsession.flush() + result = SendEmailTask().run_impl( + db_session=dbsession, + to_addr=owner.email, + subject="Test", + template_name="test", + username="test_username", + ) + assert result["email_successful"] == False + assert result["err_msg"] == "123 abc 456 def" + + def test_send_email_no_smtp_config( + self, mocker, mock_configuration_no_smtp, dbsession + ): + SMTPService.connection = None + owner = OwnerFactory.create(email=to_addr) + dbsession.add(owner) + dbsession.flush() + result = SendEmailTask().run_impl( + db_session=dbsession, + to_addr=owner.email, + subject="Test", + template_name="test", + username="test_username", + ) + assert result == { + "email_successful": False, + "err_msg": "Cannot send email because SMTP is not configured for this installation of codecov", + } diff --git a/apps/worker/tasks/tests/unit/test_status_set_error.py b/apps/worker/tasks/tests/unit/test_status_set_error.py new file mode 100644 index 0000000000..f10e4c1714 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_status_set_error.py @@ -0,0 +1,161 @@ +from pathlib import Path + +import mock +import pytest +from shared.torngit.status import Status +from shared.yaml import UserYaml + +from database.tests.factories import CommitFactory +from tasks.status_set_error import StatusSetErrorTask + +here = Path(__file__) + + +class TestSetErrorTaskUnit(object): + def test_no_status(self, mocker, mock_configuration, dbsession): + mocked_1 = mocker.patch("tasks.status_set_error.get_repo_provider_service") + repo = mocker.MagicMock( + service="github", + data=dict(repo=dict(repoid=123)), + set_commit_status=mock.AsyncMock(return_value=None), + ) + mocked_1.return_value = repo + + mocked_2 = mocker.patch("tasks.status_set_error.get_current_yaml") + fetch_current_yaml = {"coverage": {"status": None}} + mocked_2.return_value = UserYaml(fetch_current_yaml) + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + repoid = commit.repoid + commitid = commit.commitid + res = StatusSetErrorTask().run_impl(dbsession, repoid, commitid) + assert not repo.set_commit_status.called + assert res == {"status_set": False} + + @pytest.mark.parametrize( + "context, cc_status_exists", + [ + ("patch", True), + ("project", True), + ("changes", True), + ("patch", False), + ("project", False), + ("changes", False), + ], + ) + def test_set_error( + self, context, cc_status_exists, mocker, mock_configuration, dbsession + ): + statuses = [ + { + "url": None, + "state": "pending", + "context": "ci", + "time": "2015-12-21T16:54:13Z", + } + ] + ( + [ + { + "url": None, + "state": "pending", + "context": "codecov/" + context, + "time": "2015-12-21T16:54:13Z", + } + ] + if cc_status_exists + else [] + ) + get_commit_statuses = Status(statuses) + set_commit_status = None + + mocked_1 = mocker.patch("tasks.status_set_error.get_repo_provider_service") + repo = mocker.MagicMock( + service="github", + slug="owner/repo", + token={"username": "bot"}, + data=dict(repo=dict(repoid=123)), + get_commit_statuses=mock.AsyncMock(return_value=get_commit_statuses), + set_commit_status=mock.AsyncMock(return_value=set_commit_status), + ) + mocked_1.return_value = repo + + mocked_2 = mocker.patch("tasks.status_set_error.get_current_yaml") + fetch_current_yaml = { + "coverage": {"status": {context: {"default": {"target": 80}}}} + } + mocked_2.return_value = UserYaml(fetch_current_yaml) + + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + repoid = commit.repoid + commitid = commit.commitid + StatusSetErrorTask().run_impl(dbsession, repoid, commitid) + if cc_status_exists: + repo.set_commit_status.assert_called_with( + commitid, + "error", + "codecov/" + context, + "Coverage not measured fully because CI failed", + f"https://codecov.io/gh/owner/repo/commit/{commitid}", + ) + else: + assert not repo.set_commit_status.called + + def test_set_error_custom_message(self, mocker, mock_configuration, dbsession): + context = "project" + statuses = [ + { + "url": None, + "state": "pending", + "context": "ci", + "time": "2015-12-21T16:54:13Z", + } + ] + ( + [ + { + "url": None, + "state": "pending", + "context": "codecov/" + context, + "time": "2015-12-21T16:54:13Z", + } + ] + ) + get_commit_statuses = Status(statuses) + set_commit_status = None + + mocked_1 = mocker.patch("tasks.status_set_error.get_repo_provider_service") + repo = mocker.MagicMock( + service="github", + slug="owner/repo", + token={"username": "bot"}, + data=dict(repo=dict(repoid=123)), + get_commit_statuses=mock.AsyncMock(return_value=get_commit_statuses), + set_commit_status=mock.AsyncMock(return_value=set_commit_status), + ) + mocked_1.return_value = repo + + mocked_2 = mocker.patch("tasks.status_set_error.get_current_yaml") + fetch_current_yaml = { + "coverage": {"status": {context: {"default": {"target": 80}}}} + } + mocked_2.return_value = UserYaml(fetch_current_yaml) + + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + repoid = commit.repoid + commitid = commit.commitid + custom_message = "Uh-oh. This is bad." + StatusSetErrorTask().run_impl( + dbsession, repoid, commitid, message=custom_message + ) + + repo.set_commit_status.assert_called_with( + commitid, + "error", + "codecov/" + context, + custom_message, + f"https://codecov.io/gh/owner/repo/commit/{commitid}", + ) diff --git a/apps/worker/tasks/tests/unit/test_status_set_pending.py b/apps/worker/tasks/tests/unit/test_status_set_pending.py new file mode 100644 index 0000000000..ffb2673fdd --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_status_set_pending.py @@ -0,0 +1,234 @@ +from pathlib import Path + +import mock +import pytest +from shared.torngit.status import Status + +from database.tests.factories import CommitFactory +from tasks.status_set_pending import StatusSetPendingTask + +here = Path(__file__) + + +class TestSetPendingTaskUnit(object): + def test_no_status(self, mocker, mock_configuration, dbsession, mock_redis): + mocked_1 = mocker.patch("tasks.status_set_pending.get_repo_provider_service") + repo = mocker.MagicMock( + service="github", + data=dict(repo=dict(repoid=123)), + set_commit_status=mock.AsyncMock(return_value=None), + ) + mocked_1.return_value = repo + + mocked_2 = mocker.patch("tasks.status_set_pending.get_current_yaml") + fetch_current_yaml = {"coverage": {"status": None}} + mocked_2.return_value = fetch_current_yaml + + mock_redis.sismember.side_effect = [True] + + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + repoid = commit.repoid + commitid = commit.commitid + branch = "master" + on_a_pull_request = False + res = StatusSetPendingTask().run_impl( + dbsession, repoid, commitid, branch, on_a_pull_request + ) + assert res["status_set"] == False + assert not repo.set_commit_status.called + + def test_not_in_beta(self, mocker, mock_configuration, dbsession, mock_redis): + mocked_1 = mocker.patch("tasks.status_set_pending.get_repo_provider_service") + repo = mocker.MagicMock( + service="github", + data=dict(repo=dict(repoid=123)), + set_commit_status=mock.AsyncMock(return_value=None), + ) + mocked_1.return_value = repo + + mocked_2 = mocker.patch("tasks.status_set_pending.get_current_yaml") + fetch_current_yaml = {"coverage": {"status": None}} + mocked_2.return_value = fetch_current_yaml + + mock_redis.sismember.side_effect = [False] + + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + repoid = commit.repoid + commitid = commit.commitid + branch = "master" + on_a_pull_request = False + with pytest.raises( + AssertionError, match="Pending disabled. Please request to be in beta." + ): + StatusSetPendingTask().run_impl( + dbsession, repoid, commitid, branch, on_a_pull_request + ) + mock_redis.sismember.assert_called_with("beta.pending", repoid) + + def test_skip_set_pending(self, mocker, mock_configuration, dbsession, mock_redis): + mocked_1 = mocker.patch("tasks.status_set_pending.get_repo_provider_service") + get_commit_statuses = Status([]) + set_commit_status = None + repo = mocker.MagicMock( + service="github", + slug="owner/repo", + data=dict(repo=dict(repoid=123)), + get_commit_statuses=mock.AsyncMock(return_value=get_commit_statuses), + set_commit_status=mock.AsyncMock(return_value=set_commit_status), + ) + mocked_1.return_value = repo + + mocked_2 = mocker.patch("tasks.status_set_pending.get_current_yaml") + fetch_current_yaml = { + "coverage": { + "status": {"project": {"custom": {"target": 80, "set_pending": False}}} + } + } + mocked_2.return_value = fetch_current_yaml + + mock_redis.sismember.side_effect = [True] + + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + repoid = commit.repoid + commitid = commit.commitid + branch = "master" + on_a_pull_request = False + res = StatusSetPendingTask().run_impl( + dbsession, repoid, commitid, branch, on_a_pull_request + ) + assert not repo.set_commit_status.called + assert res["status_set"] == False + + def test_skip_set_pending_unknown_branch( + self, mocker, mock_configuration, dbsession, mock_redis + ): + mocked_1 = mocker.patch("tasks.status_set_pending.get_repo_provider_service") + get_commit_statuses = Status([]) + set_commit_status = None + repo = mocker.MagicMock( + service="github", + slug="owner/repo", + data=dict(repo=dict(repoid=123)), + get_commit_statuses=mock.AsyncMock(return_value=get_commit_statuses), + set_commit_status=mock.AsyncMock(return_value=set_commit_status), + ) + mocked_1.return_value = repo + + mocked_2 = mocker.patch("tasks.status_set_pending.get_current_yaml") + fetch_current_yaml = { + "coverage": { + "status": { + "project": {"custom": {"target": 80, "branches": ["master"]}} + } + } + } + mocked_2.return_value = fetch_current_yaml + + mock_redis.sismember.side_effect = [True] + + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + repoid = commit.repoid + commitid = commit.commitid + branch = None + on_a_pull_request = False + res = StatusSetPendingTask().run_impl( + dbsession, repoid, commitid, branch, on_a_pull_request + ) + assert not repo.set_commit_status.called + assert res["status_set"] == False + + @pytest.mark.parametrize( + "context, branch, cc_status_exists", + [ + ("patch", "master", False), + ("patch", "master", False), + ("patch", "master", True), + ("patch", "skip", False), + ("project", "master", False), + ("project", "skip", False), + ("project", "master", True), + ("changes", "master", False), + ("changes", "master", False), + ("changes", "master", True), + ("changes", "skip", False), + ], + ) + def test_set_pending( + self, + context, + branch, + cc_status_exists, + mocker, + mock_configuration, + dbsession, + mock_redis, + ): + statuses = [ + { + "url": None, + "state": "pending", + "context": "ci", + "time": "2015-12-21T16:54:13Z", + } + ] + ( + [ + { + "url": None, + "state": "pending", + "context": "codecov/" + context + "/custom", + "time": "2015-12-21T16:54:13Z", + } + ] + if cc_status_exists + else [] + ) + + mocked_1 = mocker.patch("tasks.status_set_pending.get_repo_provider_service") + get_commit_statuses = Status(statuses) + set_commit_status = None + repo = mocker.MagicMock( + service="github", + slug="owner/repo", + data=dict(repo=dict(repoid=123)), + get_commit_statuses=mock.AsyncMock(return_value=get_commit_statuses), + set_commit_status=mock.AsyncMock(return_value=set_commit_status), + ) + mocked_1.return_value = repo + + mocked_2 = mocker.patch("tasks.status_set_pending.get_current_yaml") + fetch_current_yaml = { + "coverage": { + "status": {context: {"custom": {"target": 80, "branches": ["!skip"]}}} + } + } + mocked_2.return_value = fetch_current_yaml + + mock_redis.sismember.side_effect = [True] + + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + repoid = commit.repoid + commitid = commit.commitid + on_a_pull_request = False + StatusSetPendingTask().run_impl( + dbsession, repoid, commitid, branch, on_a_pull_request + ) + if branch == "master" and not cc_status_exists: + repo.set_commit_status.assert_called_with( + commitid, + "pending", + "codecov/" + context + "/custom", + "Collecting reports and waiting for CI to complete", + f"https://codecov.io/gh/owner/repo/commit/{commitid}", + ) + else: + assert not repo.set_commit_status.called diff --git a/apps/worker/tasks/tests/unit/test_sync_pull.py b/apps/worker/tasks/tests/unit/test_sync_pull.py new file mode 100644 index 0000000000..1e819bc8ee --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_sync_pull.py @@ -0,0 +1,555 @@ +import json +import os +from pathlib import Path + +import pytest +from celery.exceptions import Retry +from mock.mock import MagicMock +from redis.exceptions import LockError +from shared.reports.types import Change +from shared.torngit.exceptions import TorngitClientError + +from database.models import Commit, Pull, Repository +from database.tests.factories import CommitFactory, PullFactory, RepositoryFactory +from database.tests.factories.reports import TestFactory +from helpers.exceptions import NoConfiguredAppsAvailable, RepositoryWithoutValidBotError +from services.repository import EnrichedPull +from services.yaml import UserYaml +from tasks.sync_pull import PullSyncTask +from tests.helpers import mock_all_plans_and_tiers + +here = Path(__file__) + + +@pytest.fixture +def repository(dbsession) -> Repository: + repository = RepositoryFactory.create(owner__plan="users-inappm") + dbsession.add(repository) + dbsession.flush() + return repository + + +@pytest.fixture +def base_commit(dbsession, repository) -> Commit: + commit = CommitFactory.create(repository=repository) + dbsession.add(commit) + dbsession.flush() + return commit + + +@pytest.fixture +def head_commit(dbsession, repository) -> Commit: + commit = CommitFactory.create(repository=repository) + dbsession.add(commit) + dbsession.flush() + return commit + + +@pytest.fixture +def pull(dbsession, repository, base_commit, head_commit) -> Pull: + pull = PullFactory.create( + repository=repository, + base=base_commit.commitid, + head=head_commit.commitid, + ) + dbsession.add(pull) + dbsession.flush() + return pull + + +@pytest.mark.parametrize( + "tests_exist", + [True, False], +) +@pytest.mark.django_db +def test_update_pull_commits_merged( + dbsession, + mocker, + tests_exist, + repository, + head_commit, + base_commit, + pull, +): + mock_all_plans_and_tiers() + + pull.state = "merged" + pullid = pull.pullid + base_commit.pullid = pullid + head_commit.pullid = pullid + first_commit = CommitFactory.create( + repository=repository, pullid=pullid, merged=False + ) + second_commit = CommitFactory.create( + repository=repository, pullid=pullid, merged=False + ) + third_commit = CommitFactory.create( + repository=repository, pullid=pullid, merged=False + ) + fourth_commit = CommitFactory.create( + repository=repository, pullid=pullid, merged=False + ) + dbsession.add(pull) + dbsession.add(first_commit) + dbsession.add(second_commit) + dbsession.add(third_commit) + dbsession.add(fourth_commit) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.flush() + task = PullSyncTask() + enriched_pull = EnrichedPull( + database_pull=pull, + provider_pull=dict(base=dict(branch="lookatthis"), head=dict(branch="thing")), + ) + commits = [first_commit.commitid, third_commit.commitid] + commits_at_base = { + "commitid": first_commit.commitid, + "parents": [{"commitid": third_commit.commitid, "parents": []}], + } + + apply_async: MagicMock = mocker.patch.object( + task.app.tasks["app.tasks.flakes.ProcessFlakesTask"], "apply_async" + ) + + current_yaml = UserYaml.from_dict( + { + "test_analytics": { + "flake_detection": True, + } + } + ) + mock_repo_provider = MagicMock( + get_commit=MagicMock(return_value=dict(parents=["1", "2"])) + ) + res = task.update_pull_commits( + mock_repo_provider, + enriched_pull, + commits, + commits_at_base, + current_yaml, + repository, + ) + + apply_async.assert_called_once_with( + kwargs=dict( + repo_id=repository.repoid, + commit_id=head_commit.commitid, + ) + ) + + assert res == {"merged_count": 2, "soft_deleted_count": 2} + dbsession.refresh(first_commit) + dbsession.refresh(second_commit) + dbsession.refresh(third_commit) + dbsession.refresh(fourth_commit) + assert not first_commit.deleted + assert second_commit.deleted + assert not third_commit.deleted + assert fourth_commit.deleted + assert first_commit.merged + assert not second_commit.merged + assert third_commit.merged + assert not fourth_commit.merged + assert first_commit.branch == "lookatthis" + assert third_commit.branch == "lookatthis" + + +def test_update_pull_commits_not_merged( + dbsession, repository, base_commit, head_commit, pull +): + pull.state = "open" + pullid = pull.pullid + base_commit.pullid = pullid + head_commit.pullid = pullid + first_commit = CommitFactory.create( + repository=repository, pullid=pullid, merged=False + ) + second_commit = CommitFactory.create( + repository=repository, pullid=pullid, merged=False + ) + third_commit = CommitFactory.create( + repository=repository, pullid=pullid, merged=False + ) + fourth_commit = CommitFactory.create( + repository=repository, pullid=pullid, merged=False + ) + dbsession.add(pull) + dbsession.add(first_commit) + dbsession.add(second_commit) + dbsession.add(third_commit) + dbsession.add(fourth_commit) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.flush() + task = PullSyncTask() + enriched_pull = EnrichedPull( + database_pull=pull, provider_pull=dict(base=dict(branch="lookatthis")) + ) + commits = [first_commit.commitid, third_commit.commitid] + commits_at_base = { + "commitid": first_commit.commitid, + "parents": [{"commitid": third_commit.commitid, "parents": []}], + } + current_yaml = UserYaml.from_dict(dict()) + mock_repo_provider = MagicMock( + get_commit=MagicMock(return_value=dict(parents=["1", "2"])) + ) + res = task.update_pull_commits( + mock_repo_provider, + enriched_pull, + commits, + commits_at_base, + current_yaml, + repository, + ) + assert res == {"merged_count": 0, "soft_deleted_count": 2} + dbsession.refresh(first_commit) + dbsession.refresh(second_commit) + dbsession.refresh(third_commit) + dbsession.refresh(fourth_commit) + assert not first_commit.deleted + assert second_commit.deleted + assert not third_commit.deleted + assert fourth_commit.deleted + assert not first_commit.merged + assert not second_commit.merged + assert not third_commit.merged + assert not fourth_commit.merged + + +def test_call_pullsync_task_no_head_commit(dbsession, mocker, mock_redis): + task = PullSyncTask() + pull = PullFactory.create(head="head_commit_nonexistent_sha", state="open") + dbsession.add(pull) + dbsession.flush() + mocked_fetch_pr = mocker.patch( + "tasks.sync_pull.fetch_and_update_pull_request_information" + ) + mocked_fetch_pr.return_value = EnrichedPull(database_pull=pull, provider_pull={}) + res = task.run_impl(dbsession, repoid=pull.repoid, pullid=pull.pullid) + assert res == { + "commit_updates_done": {"merged_count": 0, "soft_deleted_count": 0}, + "notifier_called": False, + "pull_updated": False, + "reason": "no_head", + } + + +def test_call_pullsync_task_nolock(dbsession, mock_redis, pull): + task = PullSyncTask() + mock_redis.lock.return_value.__enter__.side_effect = LockError + res = task.run_impl(dbsession, repoid=pull.repoid, pullid=pull.pullid) + assert res == { + "commit_updates_done": {"merged_count": 0, "soft_deleted_count": 0}, + "notifier_called": False, + "pull_updated": False, + "reason": "unable_fetch_lock", + } + + +def test_call_pullsync_task_no_database_pull(dbsession, mocker, mock_redis, repository): + task = PullSyncTask() + mocked_fetch_pr = mocker.patch( + "tasks.sync_pull.fetch_and_update_pull_request_information" + ) + mocked_fetch_pr.return_value = EnrichedPull(database_pull=None, provider_pull=None) + res = task.run_impl(dbsession, repoid=repository.repoid, pullid=99) + assert res == { + "commit_updates_done": {"merged_count": 0, "soft_deleted_count": 0}, + "notifier_called": False, + "pull_updated": False, + "reason": "no_db_pull", + } + + +def test_call_pullsync_task_no_provider_pull_only( + dbsession, mocker, mock_redis, repository, pull +): + task = PullSyncTask() + mocked_fetch_pr = mocker.patch( + "tasks.sync_pull.fetch_and_update_pull_request_information" + ) + mocked_fetch_pr.return_value = EnrichedPull(database_pull=pull, provider_pull=None) + res = task.run_impl(dbsession, repoid=repository.repoid, pullid=99) + assert res == { + "commit_updates_done": {"merged_count": 0, "soft_deleted_count": 0}, + "notifier_called": False, + "pull_updated": False, + "reason": "not_in_provider", + } + + +def test_call_pullsync_no_bot(dbsession, mock_redis, mocker, pull): + task = PullSyncTask() + mocker.patch( + "tasks.sync_pull.get_repo_provider_service", + side_effect=RepositoryWithoutValidBotError(), + ) + res = task.run_impl(dbsession, repoid=pull.repoid, pullid=pull.pullid) + assert res == { + "commit_updates_done": {"merged_count": 0, "soft_deleted_count": 0}, + "notifier_called": False, + "pull_updated": False, + "reason": "no_bot", + } + + +def test_call_pullsync_no_apps_available_rate_limit( + dbsession, mock_redis, mocker, pull +): + task = PullSyncTask() + mocker.patch( + "tasks.sync_pull.get_repo_provider_service", + side_effect=NoConfiguredAppsAvailable( + apps_count=1, rate_limited_count=1, suspended_count=0 + ), + ) + with pytest.raises(Retry): + task.run_impl(dbsession, repoid=pull.repoid, pullid=pull.pullid) + + +def test_call_pullsync_no_apps_available_suspended(dbsession, mock_redis, mocker, pull): + task = PullSyncTask() + mocker.patch( + "tasks.sync_pull.get_repo_provider_service", + side_effect=NoConfiguredAppsAvailable( + apps_count=1, rate_limited_count=0, suspended_count=1 + ), + ) + res = task.run_impl(dbsession, repoid=pull.repoid, pullid=pull.pullid) + assert res == { + "commit_updates_done": {"merged_count": 0, "soft_deleted_count": 0}, + "notifier_called": False, + "pull_updated": False, + "reason": "no_configured_apps_available", + } + + +def test_call_pullsync_no_permissions_get_compare( + dbsession, + mock_redis, + mocker, + mock_repo_provider, + mock_storage, + repository, + base_commit, + head_commit, + pull, +): + mocker.patch.object(PullSyncTask, "app") + task = PullSyncTask() + mocked_fetch_pr = mocker.patch( + "tasks.sync_pull.fetch_and_update_pull_request_information" + ) + mocked_fetch_pr.return_value = EnrichedPull( + database_pull=pull, provider_pull={"head"} + ) + mock_repo_provider.get_compare.side_effect = TorngitClientError( + 403, "response", "message" + ) + mock_repo_provider.get_pull_request_commits.side_effect = TorngitClientError( + 403, "response", "message" + ) + res = task.run_impl(dbsession, repoid=pull.repoid, pullid=pull.pullid) + assert res == { + "commit_updates_done": {"merged_count": 0, "soft_deleted_count": 0}, + "notifier_called": True, + "pull_updated": True, + "reason": "success", + } + + +def test_run_impl_unobtainable_lock(dbsession, mock_redis, pull): + mock_redis.lock.side_effect = LockError() + mock_redis.exists.return_value = True + task = PullSyncTask() + task.request.retries = 0 + res = task.run_impl(dbsession, repoid=pull.repoid, pullid=pull.pullid) + assert res == { + "commit_updates_done": {"merged_count": 0, "soft_deleted_count": 0}, + "notifier_called": False, + "pull_updated": False, + "reason": "unable_fetch_lock", + } + + +def test_was_pr_merged_with_squash(): + ancestors_tree = { + "commitid": "c739768fcac68144a3a6d82305b9c4106934d31a", + "parents": [ + { + "commitid": "b33e12816cc3f386dae8add4968cedeff5155021", + "parents": [ + { + "commitid": "743b04806ea677403aa2ff26c6bdeb85005de658", + "parents": [], + }, + { + "commitid": "some_commit", + "parents": [{"commitid": "paaaaaaaaaaa", "parents": []}], + }, + ], + } + ], + } + task = PullSyncTask() + assert not task.was_squash_via_ancestor_tree( + ["c739768fcac68144a3a6d82305b9c4106934d31a"], ancestors_tree + ) + assert task.was_squash_via_ancestor_tree(["some_other_stuff"], ancestors_tree) + assert not task.was_squash_via_ancestor_tree( + ["some_other_stuff", "some_commit"], ancestors_tree + ) + + +def test_cache_changes_stores_changed_files_in_redis_if_owner_is_whitelisted( + dbsession, + mock_redis, + mock_repo_provider, + mocker, + repository, + base_commit, + head_commit, + pull, +): + os.environ["OWNERS_WITH_CACHED_CHANGES"] = f"{pull.repository.owner.ownerid}" + + changes = [Change(path="f.py")] + mocker.patch( + "tasks.sync_pull.get_changes", + lambda base_report, head_report, diff: changes, + ) + + task = PullSyncTask() + task.cache_changes(pull, changes) + + mock_redis.set.assert_called_once_with( + "/".join( + ( + "compare-changed-files", + pull.repository.owner.service, + pull.repository.owner.username, + pull.repository.name, + f"{pull.pullid}", + ) + ), + json.dumps(["f.py"]), + ex=86400, + ) + + +def test_trigger_ai_pr_review( + dbsession, mocker, repository, base_commit, head_commit, pull +): + pullid = pull.pullid + base_commit.pullid = pullid + head_commit.pullid = pullid + dbsession.add(pull) + dbsession.add(base_commit) + dbsession.add(head_commit) + dbsession.flush() + task = PullSyncTask() + apply_async = mocker.patch.object( + task.app.tasks["app.tasks.ai_pr_review.AiPrReview"], "apply_async" + ) + enriched_pull = EnrichedPull( + database_pull=pull, + provider_pull=dict(base=dict(branch="lookatthis"), labels=["ai-pr-review"]), + ) + current_yaml = UserYaml.from_dict( + { + "ai_pr_review": { + "enabled": True, + "method": "label", + "label_name": "ai-pr-review", + } + } + ) + task.trigger_ai_pr_review(enriched_pull, current_yaml) + apply_async.assert_called_once_with( + kwargs=dict(repoid=pull.repoid, pullid=pull.pullid) + ) + + apply_async.reset_mock() + current_yaml = UserYaml.from_dict( + { + "ai_pr_review": { + "enabled": True, + } + } + ) + task.trigger_ai_pr_review(enriched_pull, current_yaml) + apply_async.assert_called_once_with( + kwargs=dict(repoid=pull.repoid, pullid=pull.pullid) + ) + + apply_async.reset_mock() + current_yaml = UserYaml.from_dict( + { + "ai_pr_review": { + "enabled": True, + "method": "auto", + } + } + ) + task.trigger_ai_pr_review(enriched_pull, current_yaml) + apply_async.assert_called_once_with( + kwargs=dict(repoid=pull.repoid, pullid=pull.pullid) + ) + + apply_async.reset_mock() + current_yaml = UserYaml.from_dict( + { + "ai_pr_review": { + "enabled": True, + "method": "label", + "label_name": "other", + } + } + ) + task.trigger_ai_pr_review(enriched_pull, current_yaml) + assert not apply_async.called + + +@pytest.mark.parametrize("flake_detection", [False, True]) +@pytest.mark.django_db +def test_trigger_process_flakes(dbsession, mocker, flake_detection, repository): + mock_all_plans_and_tiers() + + current_yaml = UserYaml.from_dict( + { + "test_analytics": { + "flake_detection": flake_detection, + } + } + ) + + commit = CommitFactory.create(repository=repository) + dbsession.add(commit) + dbsession.flush() + + task = PullSyncTask() + apply_async: MagicMock = mocker.patch.object( + task.app.tasks["app.tasks.flakes.ProcessFlakesTask"], "apply_async" + ) + + if flake_detection: + TestFactory.create(repository=repository) + dbsession.flush() + + task.trigger_process_flakes( + dbsession, + repository, + commit.commitid, + current_yaml, + ) + if flake_detection: + apply_async.assert_called_once_with( + kwargs=dict( + repo_id=repository.repoid, + commit_id=commit.commitid, + ) + ) + else: + apply_async.assert_not_called() diff --git a/apps/worker/tasks/tests/unit/test_sync_repo_languages.py b/apps/worker/tasks/tests/unit/test_sync_repo_languages.py new file mode 100644 index 0000000000..3e17c326f5 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_sync_repo_languages.py @@ -0,0 +1,245 @@ +from datetime import datetime, timedelta +from unittest.mock import Mock + +import pytest +from shared.torngit.exceptions import TorngitError +from shared.utils.enums import TaskConfigGroup + +from database.tests.factories.core import OwnerFactory, RepositoryFactory +from tasks.sync_repo_languages import SyncRepoLanguagesTask + +MOCKED_NOW = datetime(2024, 7, 3, 6, 8, 12) +LIST_WITH_INTERSECTION = ["python", "go", "javascript"] + + +def setup_now(mocker): + mocker.patch( + f"tasks.{TaskConfigGroup.sync_repo_languages.value}.get_utc_now", + return_value=MOCKED_NOW, + ) + + +@pytest.fixture +def setup_with_languages(mocker, mock_repo_provider): + setup_now(mocker) + + mock_repo_provider.get_repo_languages.return_value = LIST_WITH_INTERSECTION + mocker.patch( + "tasks.base.get_repo_provider_service", + return_value=mock_repo_provider, + ) + + +@pytest.fixture +def setup_with_null_languages(mocker, mock_repo_provider): + setup_now(mocker) + + mock_repo_provider.get_repo_languages.return_value = None + mocker.patch( + "tasks.base.get_repo_provider_service", + return_value=mock_repo_provider, + ) + + +@pytest.fixture +def setup_with_languages_bitbucket(mocker, mock_repo_provider): + setup_now(mocker) + + mock_repo_provider.get_repo_languages.return_value = ["javascript"] + mocker.patch( + "tasks.base.get_repo_provider_service", + return_value=mock_repo_provider, + ) + + +@pytest.fixture +def setup_with_torngit_error(mocker, mock_repo_provider): + setup_now(mocker) + + mock_repo_provider.get_repo_languages = Mock(side_effect=TorngitError()) + + +class TestSyncRepoLanguages(object): + def test_languages_no_intersection_and_not_synced_github( + self, dbsession, setup_with_languages + ): + owner = OwnerFactory.create(service="github") + dbsession.add(owner) + repo = RepositoryFactory.create( + owner=owner, languages_last_updated=None, languages=[] + ) + dbsession.add(repo) + dbsession.flush() + + task = SyncRepoLanguagesTask() + assert task.run_impl(dbsession, repoid=repo.repoid, manual_trigger=False) == { + "successful": True + } + assert repo.languages == LIST_WITH_INTERSECTION + assert repo.languages_last_updated == MOCKED_NOW + + def test_languages_no_intersection_and_not_synced_gitlab( + self, dbsession, setup_with_languages + ): + owner = OwnerFactory.create(service="gitlab") + dbsession.add(owner) + repo = RepositoryFactory.create( + owner=owner, languages_last_updated=None, languages=[] + ) + dbsession.add(repo) + dbsession.flush() + + task = SyncRepoLanguagesTask() + assert task.run_impl(dbsession, repoid=repo.repoid, manual_trigger=False) == { + "successful": True + } + assert repo.languages == LIST_WITH_INTERSECTION + assert repo.languages_last_updated == MOCKED_NOW + + def test_languages_no_intersection_and_not_synced_bitbucket( + self, dbsession, setup_with_languages_bitbucket + ): + owner = OwnerFactory.create(service="bitbucket") + dbsession.add(owner) + repo = RepositoryFactory.create( + owner=owner, + languages_last_updated=None, + languages=[], + language="javascript", + ) + dbsession.add(repo) + dbsession.flush() + + task = SyncRepoLanguagesTask() + assert task.run_impl(dbsession, repoid=repo.repoid, manual_trigger=False) == { + "successful": True + } + assert repo.languages == ["javascript"] + assert repo.languages_last_updated == MOCKED_NOW + + def test_languages_no_intersection_and_synced_below_threshold( + self, dbsession, setup_with_languages + ): + mocked_below_threshold = MOCKED_NOW + timedelta(days=-3) + + repo = RepositoryFactory.create( + languages_last_updated=mocked_below_threshold, languages=[] + ) + dbsession.add(repo) + dbsession.flush() + + task = SyncRepoLanguagesTask() + assert task.run_impl(dbsession, repoid=repo.repoid, manual_trigger=False) == { + "successful": True, + "synced": False, + } + + def test_languages_no_intersection_and_synced_beyond_threshold( + self, dbsession, setup_with_languages + ): + mocked_beyond_threshold = MOCKED_NOW + timedelta(days=-10) + + repo = RepositoryFactory.create( + languages_last_updated=mocked_beyond_threshold, languages=[] + ) + dbsession.add(repo) + dbsession.flush() + + task = SyncRepoLanguagesTask() + assert task.run_impl(dbsession, repoid=repo.repoid, manual_trigger=False) == { + "successful": True + } + assert repo.languages == LIST_WITH_INTERSECTION + assert repo.languages_last_updated == MOCKED_NOW + + def test_languages_intersection_and_synced_below_threshold( + self, dbsession, setup_with_languages + ): + mocked_below_threshold = MOCKED_NOW + timedelta(days=-3) + + repo = RepositoryFactory.create( + languages_last_updated=mocked_below_threshold, languages=["javascript"] + ) + dbsession.add(repo) + dbsession.flush() + + task = SyncRepoLanguagesTask() + assert task.run_impl(dbsession, repoid=repo.repoid, manual_trigger=False) == { + "successful": True, + "synced": False, + } + + def test_languages_intersection_and_synced_beyond_threshold( + self, dbsession, setup_with_null_languages + ): + mocked_beyond_threshold = MOCKED_NOW + timedelta(days=-10) + + repo = RepositoryFactory.create( + languages_last_updated=mocked_beyond_threshold, languages=["javascript"] + ) + dbsession.add(repo) + dbsession.flush() + + task = SyncRepoLanguagesTask() + assert task.run_impl(dbsession, repoid=repo.repoid, manual_trigger=False) == { + "successful": True, + "synced": False, + } + + def test_languages_intersection_and_synced_beyond_threshold_with_languages( + self, dbsession, setup_with_languages + ): + mocked_beyond_threshold = MOCKED_NOW + timedelta(days=-10) + + repo = RepositoryFactory.create( + languages_last_updated=mocked_beyond_threshold, languages=["javascript"] + ) + dbsession.add(repo) + dbsession.flush() + + task = SyncRepoLanguagesTask() + assert task.run_impl(dbsession, repoid=repo.repoid, manual_trigger=False) == { + "successful": True, + "synced": False, + } + + def test_languages_intersection_and_synced_with_manual_trigger( + self, dbsession, setup_with_languages + ): + mocked_beyond_threshold = MOCKED_NOW + timedelta(days=-10) + + repo = RepositoryFactory.create( + languages_last_updated=mocked_beyond_threshold, languages=["javascript"] + ) + dbsession.add(repo) + dbsession.flush() + + task = SyncRepoLanguagesTask() + assert task.run_impl(dbsession, repoid=repo.repoid, manual_trigger=True) == { + "successful": True + } + assert repo.languages == LIST_WITH_INTERSECTION + assert repo.languages_last_updated == MOCKED_NOW + + def test_languages_torngit_error(self, dbsession, setup_with_torngit_error): + repo = RepositoryFactory.create( + languages_last_updated=None, languages=["javascript"] + ) + dbsession.add(repo) + dbsession.flush() + + task = SyncRepoLanguagesTask() + res = task.run_impl(dbsession, repoid=repo.repoid, manual_trigger=True) + assert res["successful"] == False + assert res["error"] == "no_repo_in_provider" + + def test_languages_no_repository(self, dbsession): + owner = OwnerFactory.create(service="github") + dbsession.add(owner) + dbsession.flush() + + task = SyncRepoLanguagesTask() + assert task.run_impl(dbsession, repoid=123, manual_trigger=False) == { + "successful": False, + "error": "no_repo_in_db", + } diff --git a/apps/worker/tasks/tests/unit/test_sync_repo_languages_gql.py b/apps/worker/tasks/tests/unit/test_sync_repo_languages_gql.py new file mode 100644 index 0000000000..8d6b840e14 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_sync_repo_languages_gql.py @@ -0,0 +1,121 @@ +from datetime import datetime +from unittest.mock import Mock + +from shared.torngit.exceptions import TorngitError, TorngitRateLimitError +from shared.utils.enums import TaskConfigGroup + +from database.models.core import Repository +from database.tests.factories.core import OwnerFactory, RepositoryFactory +from tasks.sync_repo_languages_gql import SyncRepoLanguagesGQLTask + + +class TestSyncRepoLanguagesGQL(object): + def test_get_repo_languages_without_org_or_current_owner(self, dbsession): + task = SyncRepoLanguagesGQLTask() + + assert task.run_impl(dbsession, org_username="asdf", current_owner_id=123) == { + "successful": False, + "error": "no_owner_in_db", + } + + def test_get_repo_languages_with_torngit_rate_limit_error( + self, dbsession, mocker, mock_repo_provider + ): + current_owner = OwnerFactory.create(service="github") + org = OwnerFactory.create(service="github") + + dbsession.add_all([current_owner, org]) + dbsession.flush() + + mocker.patch( + f"tasks.{TaskConfigGroup.sync_repo_languages_gql.value}.get_owner_provider_service", + return_value=mock_repo_provider, + ) + mock_repo_provider.get_repos_with_languages_graphql = Mock( + side_effect=TorngitRateLimitError("response_data", "message", "reset") + ) + + task = SyncRepoLanguagesGQLTask() + + assert task.run_impl( + dbsession, org_username=org.username, current_owner_id=current_owner.ownerid + ) == {"successful": False, "error": "torngit_rate_limit_error"} + + def test_get_repo_languages_with_torngit_error( + self, dbsession, mocker, mock_repo_provider + ): + current_owner = OwnerFactory.create(service="github") + org = OwnerFactory.create(service="github") + + dbsession.add_all([current_owner, org]) + dbsession.flush() + + mocker.patch( + f"tasks.{TaskConfigGroup.sync_repo_languages_gql.value}.get_owner_provider_service", + return_value=mock_repo_provider, + ) + mock_repo_provider.get_repos_with_languages_graphql = Mock( + side_effect=TorngitError() + ) + + task = SyncRepoLanguagesGQLTask() + + assert task.run_impl( + dbsession, org_username=org.username, current_owner_id=current_owner.ownerid + ) == {"successful": False, "error": "torngit_error"} + + def test_get_repo_languages_expected_response( + self, dbsession, mocker, mock_repo_provider + ): + current_owner = OwnerFactory.create(service="github") + org = OwnerFactory.create(service="github") + + repo_one_name = "test-one" + repo_two_name = "test-two" + repo_three_name = "test-three" + + repo_one = RepositoryFactory.create(name=repo_one_name, owner=org) + repo_two = RepositoryFactory.create( + name=repo_two_name, languages_last_updated=None, owner=org + ) + repo_three = RepositoryFactory.create(name=repo_three_name, owner=org) + + dbsession.add_all([current_owner, org, repo_one, repo_two, repo_three]) + dbsession.flush() + + MOCKED_NOW = datetime(2024, 7, 3, 6, 8, 12) + + mock_return_value = { + repo_one_name: ["javascript", "typescript"], + repo_two_name: ["swift", "html"], + repo_three_name: [], + "random": ["python"], + } + + mock_repo_provider.get_repos_with_languages_graphql.return_value = ( + mock_return_value + ) + mocker.patch( + f"tasks.{TaskConfigGroup.sync_repo_languages_gql.value}.get_owner_provider_service", + return_value=mock_repo_provider, + ) + mocker.patch( + f"tasks.{TaskConfigGroup.sync_repo_languages_gql.value}.get_utc_now", + return_value=MOCKED_NOW, + ) + + task = SyncRepoLanguagesGQLTask() + + assert task.run_impl( + dbsession, org_username=org.username, current_owner_id=current_owner.ownerid + ) == {"successful": True} + + all_repos = ( + dbsession.query(Repository) + .filter(Repository.name.startswith("test-")) + .all() + ) + + for repo in all_repos: + assert repo.languages == mock_return_value[repo.name] + assert repo.languages_last_updated == MOCKED_NOW diff --git a/apps/worker/tasks/tests/unit/test_sync_repos_task.py b/apps/worker/tasks/tests/unit/test_sync_repos_task.py new file mode 100644 index 0000000000..3e0204b33f --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_sync_repos_task.py @@ -0,0 +1,1094 @@ +from datetime import datetime +from pathlib import Path +from unittest.mock import call + +import pytest +import respx +import vcr +from celery.exceptions import SoftTimeLimitExceeded +from freezegun import freeze_time +from redis.exceptions import LockError +from shared.celery_config import ( + sync_repo_languages_gql_task_name, + sync_repo_languages_task_name, +) +from shared.torngit.exceptions import TorngitClientError, TorngitServer5xxCodeError + +from database.models import Owner, Repository +from database.models.core import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, +) +from database.tests.factories import OwnerFactory, RepositoryFactory +from tasks.sync_repo_languages_gql import SyncRepoLanguagesGQLTask +from tasks.sync_repos import SyncReposTask +from tests.helpers import mock_all_plans_and_tiers + +here = Path(__file__) + + +def reuse_cassette(filepath): + return vcr.use_cassette( + filepath, + record_mode="new_episodes", + filter_headers=["authorization"], + match_on=["method", "scheme", "host", "port", "path"], + ) + + +class TestSyncReposTaskUnit(object): + def test_unknown_owner(self, dbsession): + unknown_ownerid = 10404 + with pytest.raises(AssertionError, match="Owner not found"): + SyncReposTask().run_impl( + dbsession, + ownerid=unknown_ownerid, + username=None, + using_integration=False, + ) + + @freeze_time("2024-03-28T00:00:00") + def test_upsert_owner_add_new(self, dbsession): + service = "github" + service_id = "123456" + username = "some_org" + prev_entry = ( + dbsession.query(Owner) + .filter(Owner.service == service, Owner.service_id == service_id) + .first() + ) + assert prev_entry is None + + upserted_ownerid = SyncReposTask().upsert_owner( + dbsession, service, service_id, username + ) + + assert isinstance(upserted_ownerid, int) + new_entry = ( + dbsession.query(Owner) + .filter(Owner.service == service, Owner.service_id == service_id) + .first() + ) + assert new_entry is not None + assert new_entry.username == username + assert new_entry.createstamp.isoformat() == "2024-03-28T00:00:00+00:00" + + def test_upsert_owner_update_existing(self, dbsession): + ownerid = 1 + service = "github" + service_id = "123456" + old_username = "codecov_org" + new_username = "Codecov" + now = datetime.utcnow() + existing_owner = OwnerFactory.create( + ownerid=ownerid, + organizations=[], + service=service, + username=old_username, + permission=[], + createstamp=now, + service_id=service_id, + ) + dbsession.add(existing_owner) + dbsession.flush() + + upserted_ownerid = SyncReposTask().upsert_owner( + dbsession, service, service_id, new_username + ) + + assert upserted_ownerid == ownerid + updated_owner = ( + dbsession.query(Owner) + .filter(Owner.service == service, Owner.service_id == service_id) + .first() + ) + assert updated_owner is not None + assert updated_owner.username == new_username + assert updated_owner.createstamp == now + + def test_upsert_repo_update_existing( + self, + dbsession, + ): + service = "gitlab" + repo_service_id = "12071992" + repo_data = { + "service_id": repo_service_id, + "name": "new-name", + "fork": None, + "private": True, + "language": None, + "branch": b"master", + } + + # add existing to db + user = OwnerFactory.create( + organizations=[], + service=service, + username="1nf1n1t3l00p", + permission=[], + service_id="45343385", + ) + dbsession.add(user) + old_repo = RepositoryFactory.create( + private=True, + name="old-name", + using_integration=False, + service_id="12071992", + owner=user, + ) + dbsession.add(old_repo) + dbsession.flush() + + upserted_repoid = SyncReposTask().upsert_repo( + dbsession, service, user.ownerid, repo_data + ) + + assert upserted_repoid == old_repo.repoid + updated_repo = ( + dbsession.query(Repository) + .filter( + Repository.ownerid == user.ownerid, + Repository.service_id == str(repo_service_id), + ) + .first() + ) + assert updated_repo is not None + assert updated_repo.private is True + assert updated_repo.name == repo_data.get("name") + assert updated_repo.updatestamp is not None + assert updated_repo.deleted is False + + def test_upsert_repo_exists_but_wrong_owner(self, dbsession): + service = "gitlab" + repo_service_id = "12071992" + repo_data = { + "service_id": repo_service_id, + "name": "pytest", + "fork": None, + "private": True, + "language": None, + "branch": b"master", + } + + # setup db + correct_owner = OwnerFactory.create( + organizations=[], + service=service, + username="1nf1n1t3l00p", + permission=[], + service_id="45343385", + ) + dbsession.add(correct_owner) + wrong_owner = OwnerFactory.create( + organizations=[], + service=service, + username="cc", + permission=[], + service_id="40404", + ) + dbsession.add(wrong_owner) + old_repo = RepositoryFactory.create( + private=True, + name="pytest", + using_integration=False, + service_id="12071992", + owner=wrong_owner, + ) + dbsession.add(old_repo) + dbsession.flush() + + upserted_repoid = SyncReposTask().upsert_repo( + dbsession, service, correct_owner.ownerid, repo_data + ) + + assert upserted_repoid == old_repo.repoid + updated_repo = ( + dbsession.query(Repository) + .filter( + Repository.ownerid == correct_owner.ownerid, + Repository.service_id == str(repo_service_id), + ) + .first() + ) + assert updated_repo is not None + assert updated_repo.deleted is False + assert updated_repo.updatestamp is not None + + def test_upsert_repo_exists_both_wrong_owner_and_service_id(self, dbsession): + # It is unclear what situation leads to this + # The most likely situation is that there was a repo abc on both owners kay and jay + # Then kay deleted its own repo, and jay moved its own repo to kay ownership + # Now the system sees that there is already a repo under kay with the right username + # (kay) but the wrong service_id (since that's an old repo), and another repo + # with the right service_id (because moved repos keep their service_ids) but wrong owner (jay) + service = "gitlab" + repository_name = "repository_name_hahahaha" + repo_service_id = "12071992" + wrong_service_id = "123498765482" + repo_data = { + "service_id": repo_service_id, + "name": repository_name, + "fork": None, + "private": True, + "language": None, + "branch": b"master", + } + correct_owner = OwnerFactory.create( + organizations=[], service=service, username="1nf1n1t3l00p", permission=[] + ) + dbsession.add(correct_owner) + dbsession.flush() + repo_same_name = RepositoryFactory.create( + private=True, + name=repository_name, + using_integration=False, + service_id=wrong_service_id, + owner=correct_owner, + ) + dbsession.add(repo_same_name) + wrong_owner = OwnerFactory.create( + organizations=[], service=service, username="cc", permission=[] + ) + dbsession.add(wrong_owner) + right_service_id_repo = RepositoryFactory.create( + private=True, + name=repository_name, + using_integration=False, + service_id=repo_service_id, + owner=wrong_owner, + ) + dbsession.add(right_service_id_repo) + dbsession.flush() + + upserted_repoid = SyncReposTask().upsert_repo( + dbsession, service, correct_owner.ownerid, repo_data + ) + + assert upserted_repoid == right_service_id_repo.repoid + assert ( + dbsession.query(Repository) + .filter( + Repository.ownerid == correct_owner.ownerid, + Repository.service_id == repo_service_id, + ) + .count() + ) == 0 # We didn't move any repos or anything + dbsession.refresh(right_service_id_repo) + assert right_service_id_repo.name == repository_name + assert right_service_id_repo.service_id == repo_service_id + assert right_service_id_repo.ownerid == wrong_owner.ownerid + dbsession.refresh(repo_same_name) + assert repo_same_name.name == repository_name + assert repo_same_name.service_id == wrong_service_id + assert repo_same_name.ownerid == correct_owner.ownerid + + def test_upsert_repo_exists_but_wrong_service_id(self, dbsession): + service = "gitlab" + repo_service_id = "12071992" + repo_wrong_service_id = "40404" + repo_data = { + "service_id": repo_service_id, + "name": "pytest", + "fork": None, + "private": True, + "language": None, + "branch": b"master", + } + + # setup db + user = OwnerFactory.create( + organizations=[], + service=service, + username="1nf1n1t3l00p", + permission=[], + service_id="45343385", + ) + dbsession.add(user) + + old_repo = RepositoryFactory.create( + private=True, + name="pytest", + using_integration=False, + service_id=repo_wrong_service_id, + owner=user, + ) + dbsession.add(old_repo) + dbsession.flush() + + upserted_repoid = SyncReposTask().upsert_repo( + dbsession, service, user.ownerid, repo_data + ) + + assert upserted_repoid == old_repo.repoid + + updated_repo = ( + dbsession.query(Repository) + .filter( + Repository.ownerid == user.ownerid, + Repository.service_id == str(repo_service_id), + ) + .first() + ) + assert updated_repo is not None + assert updated_repo.service_id == str(repo_service_id) + assert updated_repo.name == "pytest" + + bad_service_id_repo = ( + dbsession.query(Repository) + .filter( + Repository.ownerid == user.ownerid, + Repository.service_id == str(repo_wrong_service_id), + ) + .first() + ) + assert bad_service_id_repo is None + + def test_upsert_repo_create_new(self, dbsession): + service = "gitlab" + repo_service_id = "12071992" + repo_data = { + "service_id": repo_service_id, + "name": "pytest", + "fork": None, + "private": True, + "language": None, + "branch": "master", + } + + # setup db + user = OwnerFactory.create( + organizations=[], + service=service, + username="1nf1n1t3l00p", + permission=[], + service_id="45343385", + ) + dbsession.add(user) + dbsession.flush() + + upserted_repoid = SyncReposTask().upsert_repo( + dbsession, service, user.ownerid, repo_data + ) + + assert isinstance(upserted_repoid, int) + new_repo = ( + dbsession.query(Repository) + .filter( + Repository.ownerid == user.ownerid, + Repository.service_id == str(repo_service_id), + ) + .first() + ) + assert new_repo is not None + assert new_repo.name == repo_data.get("name") + assert new_repo.language == repo_data.get("language") + assert new_repo.branch == repo_data.get("branch") + assert new_repo.private is True + + @pytest.mark.django_db(databases={"default"}) + def test_only_public_repos_already_in_db(self, dbsession): + token = "ecd73a086eadc85db68747a66bdbd662a785a072" + user = OwnerFactory.create( + organizations=[], + service="github", + username="1nf1n1t3l00p", + unencrypted_oauth_token=token, + permission=[], + service_id="45343385", + ) + dbsession.add(user) + + repo_pub = RepositoryFactory.create( + private=False, + name="pub", + using_integration=False, + service_id="159090647", + owner=user, + ) + repo_pytest = RepositoryFactory.create( + private=False, + name="pytest", + using_integration=False, + service_id="159089634", + owner=user, + ) + repo_spack = RepositoryFactory.create( + private=False, + name="spack", + using_integration=False, + service_id="164948070", + owner=user, + ) + dbsession.add(repo_pub) + dbsession.add(repo_pytest) + dbsession.add(repo_spack) + dbsession.flush() + + SyncReposTask().run_impl( + dbsession, ownerid=user.ownerid, using_integration=False + ) + repos = ( + dbsession.query(Repository) + .filter(Repository.service_id.in_(("159090647", "159089634", "164948070"))) + .all() + ) + + assert user.permission == [] # there were no private repos to add + assert len(repos) == 3 + + def test_sync_repos_lock_error(self, dbsession, mock_redis): + user = OwnerFactory.create( + organizations=[], + service="github", + username="1nf1n1t3l00p", + permission=[], + service_id="45343385", + ) + dbsession.add(user) + dbsession.flush() + mock_redis.lock.side_effect = LockError + SyncReposTask().run_impl( + dbsession, ownerid=user.ownerid, using_integration=False + ) + assert user.permission == [] # there were no private repos to add + + @reuse_cassette( + "tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_only_public_repos_not_in_db.yaml" + ) + @respx.mock + @pytest.mark.django_db(databases={"default"}) + def test_only_public_repos_not_in_db(self, dbsession): + mock_all_plans_and_tiers() + token = "ecd73a086eadc85db68747a66bdbd662a785a072" + user = OwnerFactory.create( + organizations=[], + service="github", + username="1nf1n1t3l00p", + unencrypted_oauth_token=token, + permission=[], + service_id="45343385", + ) + dbsession.add(user) + dbsession.flush() + SyncReposTask().run_impl( + dbsession, ownerid=user.ownerid, using_integration=False + ) + + public_repo_service_id = "159090647" + expected_repo_service_ids = (public_repo_service_id,) + assert user.permission == [] # there were no private repos to add + repos = ( + dbsession.query(Repository) + .filter(Repository.service_id.in_(expected_repo_service_ids)) + .all() + ) + assert len(repos) == 1 + assert repos[0].service_id == public_repo_service_id + assert repos[0].ownerid == user.ownerid + + @respx.mock + @reuse_cassette( + "tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_sync_repos_using_integration.yaml" + ) + @pytest.mark.django_db(databases={"default"}) + def test_sync_repos_using_integration( + self, + mocker, + dbsession, + mock_owner_provider, + mock_redis, + ): + mock_all_plans_and_tiers() + user = OwnerFactory.create( + organizations=[], + service="github", + username="1nf1n1t3l00p", + permission=[], + service_id="45343385", + ) + dbsession.add(user) + + mock_redis.exists.return_value = False + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + return_value="installation_token", + ) + + ghapp = GithubAppInstallation( + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + installation_id=1822, + # inaccurate because this integration should be able to list all repos in the test + # but the sync_repos should fix this too + repository_service_ids=["555555555"], + owner=user, + ) + dbsession.add(ghapp) + user.github_app_installations = [ghapp] + + dbsession.flush() + + def repo_obj(service_id, name, language, private, branch, using_integration): + return { + "owner": { + "service_id": "test-owner-service-id", + "username": "test-owner-username", + }, + "repo": { + "service_id": service_id, + "name": name, + "language": language, + "private": private, + "branch": branch, + }, + "_using_integration": using_integration, + } + + mock_repos = [ + repo_obj("159089634", "pytest", "python", False, "main", True), + repo_obj("164948070", "spack", "python", False, "develop", False), + repo_obj("213786132", "pub", "dart", False, "master", None), + repo_obj("555555555", "soda", "python", False, "main", None), + ] + + # Mock GitHub response for repos that are visible to our app + mock_owner_provider.list_repos_using_installation_generator.return_value.__aiter__.return_value = [ + mock_repos + ] + + # Three of the four repositories we can see are already in the database. + # Will we update `using_integration` correctly? + preseeded_repos = [ + RepositoryFactory.create( + private=repo["repo"]["private"], + name=repo["repo"]["name"], + using_integration=repo["_using_integration"], + service_id=repo["repo"]["service_id"], + owner=user, + ) + for repo in mock_repos[:-1] + ] + + for repo in preseeded_repos: + dbsession.add(repo) + dbsession.flush() + + SyncReposTask().run_impl( + dbsession, ownerid=user.ownerid, using_integration=True + ) + dbsession.commit() + + repos = ( + dbsession.query(Repository) + .filter( + Repository.service_id.in_( + (repo["repo"]["service_id"] for repo in mock_repos) + ) + ) + .all() + ) + + # We pre-seeded 3 repos in the database, but we should have added the + # 4th based on our GitHub response + assert len(repos) == 4 + + assert user.permission == [] # there were no private repos + for repo in repos: + assert repo.using_integration is True + + @respx.mock + @reuse_cassette( + "tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_sync_repos_using_integration_no_repos.yaml" + ) + @pytest.mark.django_db(databases={"default"}) + def test_sync_repos_using_integration_no_repos( + self, + dbsession, + ): + mock_all_plans_and_tiers() + token = "ecd73a086eadc85db68747a66bdbd662a785a072" + user = OwnerFactory.create( + organizations=[], + service="github", + username="1nf1n1t3l00p", + unencrypted_oauth_token=token, + permission=[], + service_id="45343385", + ) + dbsession.add(user) + + repo_pytest = RepositoryFactory.create( + private=False, + name="pytest", + using_integration=True, + service_id="159089634", + owner=user, + ) + repo_spack = RepositoryFactory.create( + private=False, + name="spack", + using_integration=True, + service_id="164948070", + owner=user, + ) + dbsession.add(repo_pytest) + dbsession.add(repo_spack) + dbsession.flush() + + SyncReposTask().run_impl( + dbsession, ownerid=user.ownerid, using_integration=True + ) + + dbsession.commit() + + repos = ( + dbsession.query(Repository) + .filter( + Repository.service_id.in_( + (repo_pytest.service_id, repo_spack.service_id) + ) + ) + .all() + ) + assert len(repos) == 2 + + assert user.permission == [] # there were no private repos + for repo in repos: + # repos are no longer using integration + assert repo.using_integration is False + + @pytest.mark.parametrize( + "list_repos_error", + [ + TorngitClientError("code", "response", "message"), + TorngitServer5xxCodeError("5xx error"), + SoftTimeLimitExceeded(), + ], + ) + @pytest.mark.django_db(databases={"default"}) + def test_sync_repos_list_repos_error( + self, + dbsession, + mock_owner_provider, + list_repos_error, + ): + mock_all_plans_and_tiers() + token = "ecd73a086eadc85db68747a66bdbd662a785a072" + user = OwnerFactory.create( + organizations=[], + service="github", + username="1nf1n1t3l00p", + unencrypted_oauth_token=token, + service_id="45343385", + ) + dbsession.add(user) + dbsession.flush() + + repos = [RepositoryFactory.create(private=True, owner=user) for _ in range(10)] + dbsession.add_all(repos) + dbsession.flush() + + user.permission = sorted([r.repoid for r in repos]) + assert len(user.permission) > 0 + dbsession.flush() + + list_repos_result = [ + dict( + owner=dict( + service_id=repo.owner.service_id, + username=repo.owner.username, + ), + repo=dict( + service_id=repo.service_id, + name=repo.name, + language=repo.language, + private=repo.private, + branch=repo.branch or "master", + ), + ) + for repo in repos + ] + + # Yield the first page of repos and then throw an error + async def mock_list_repos_generator(*args, **kwargs): + yield list_repos_result[:5] + raise list_repos_error + + mock_owner_provider.list_repos_generator = mock_list_repos_generator + + SyncReposTask().run_impl( + dbsession, ownerid=user.ownerid, using_integration=False + ) + + # `list_repos()` raised an error so we couldn't finish every repo, but the first page was finished and should show up here. + assert user.permission == sorted([r.repoid for r in repos[:5]]) + + @reuse_cassette( + "tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_only_public_repos_not_in_db.yaml" + ) + @respx.mock + @pytest.mark.django_db(databases={"default"}) + def test_insert_repo_and_call_repo_sync_languages(self, dbsession): + mock_all_plans_and_tiers() + token = "ecd73a086eadc85db68747a66bdbd662a785a072" + user = OwnerFactory.create( + organizations=[], + service="github", + username="1nf1n1t3l00p", + unencrypted_oauth_token=token, + permission=[], + service_id="45343385", + ) + dbsession.add(user) + dbsession.flush() + SyncReposTask().run_impl( + dbsession, ownerid=user.ownerid, using_integration=False + ) + + public_repo_service_id = "159090647" + expected_repo_service_ids = (public_repo_service_id,) + assert user.permission == [] # there were no private repos to add + repos = ( + dbsession.query(Repository) + .filter(Repository.service_id.in_(expected_repo_service_ids)) + .all() + ) + assert len(repos) == 1 + assert repos[0].service_id == public_repo_service_id + assert repos[0].ownerid == user.ownerid + + @respx.mock + @reuse_cassette( + "tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_sync_repos_using_integration.yaml" + ) + def test_insert_repo_and_call_repo_sync_languages_using_integration( + self, + mocker, + dbsession, + mock_owner_provider, + ): + mocked_app = mocker.patch.object( + SyncReposTask, + "app", + tasks={ + sync_repo_languages_task_name: mocker.MagicMock(), + }, + ) + + token = "ecd73a086eadc85db68747a66bdbd662a785a072" + user = OwnerFactory.create( + organizations=[], + service="github", + username="1nf1n1t3l00p", + unencrypted_oauth_token=token, + permission=[], + service_id="45343385", + ) + dbsession.add(user) + + def repo_obj(service_id, name, language, private, branch, using_integration): + return { + "owner": { + "service_id": "test-owner-service-id", + "username": "test-owner-username", + }, + "repo": { + "service_id": service_id, + "name": name, + "language": language, + "private": private, + "branch": branch, + }, + "_using_integration": using_integration, + } + + mock_repos = [ + repo_obj("159089634", "pytest", "python", False, "main", True), + repo_obj("164948070", "spack", "python", False, "develop", False), + repo_obj("213786132", "pub", "dart", False, "master", None), + repo_obj("555555555", "soda", "python", False, "main", None), + ] + + # Mock GitHub response for repos that are visible to our app + mock_owner_provider.list_repos_using_installation_generator.return_value.__aiter__.return_value = [ + mock_repos + ] + + # Three of the four repositories we can see are already in the database. + # Will we update `using_integration` correctly? + preseeded_repos = [ + RepositoryFactory.create( + private=repo["repo"]["private"], + name=repo["repo"]["name"], + using_integration=repo["_using_integration"], + service_id=repo["repo"]["service_id"], + owner=user, + ) + for repo in mock_repos[:-1] + ] + + for repo in preseeded_repos: + dbsession.add(repo) + dbsession.flush() + + SyncReposTask().run_impl( + dbsession, ownerid=user.ownerid, using_integration=True + ) + dbsession.commit() + + repos = ( + dbsession.query(Repository) + .filter( + Repository.service_id.in_( + (repo["repo"]["service_id"] for repo in mock_repos) + ) + ) + .all() + ) + + # We pre-seeded 3 repos in the database, but we should have added the + # 4th based on our GitHub response + assert len(repos) == 4 + + assert user.permission == [] # there were no private repos + for repo in repos: + assert repo.using_integration is True + + new_repo_list = list(set(repos) - set(preseeded_repos)) + + mocked_app.tasks[sync_repo_languages_task_name].apply_async.assert_any_call( + kwargs={"repoid": new_repo_list[0].repoid, "manual_trigger": False} + ) + + @respx.mock + @reuse_cassette( + "tasks/tests/unit/cassetes/test_sync_repos_task/TestSyncReposTaskUnit/test_sync_repos_using_integration.yaml" + ) + def test_insert_repo_and_not_call_repo_sync_languages_using_integration( + self, + mocker, + dbsession, + mock_owner_provider, + ): + mocked_app = mocker.patch.object( + SyncReposTask, + "app", + tasks={ + sync_repo_languages_task_name: mocker.MagicMock(), + }, + ) + + mocker.patch("tasks.sync_repos.get_config", return_value=False) + + token = "ecd73a086eadc85db68747a66bdbd662a785a072" + user = OwnerFactory.create( + organizations=[], + service="github", + username="1nf1n1t3l00p", + unencrypted_oauth_token=token, + permission=[], + service_id="45343385", + ) + dbsession.add(user) + + def repo_obj(service_id, name, language, private, branch, using_integration): + return { + "owner": { + "service_id": "test-owner-service-id", + "username": "test-owner-username", + }, + "repo": { + "service_id": service_id, + "name": name, + "language": language, + "private": private, + "branch": branch, + }, + "_using_integration": using_integration, + } + + mock_repos = [ + repo_obj("159089634", "pytest", "python", False, "main", True), + ] + mock_owner_provider.list_repos_using_installation_generator.return_value.__aiter__.return_value = [ + mock_repos + ] + + preseeded_repos = [ + RepositoryFactory.create( + private=repo["repo"]["private"], + name=repo["repo"]["name"], + using_integration=repo["_using_integration"], + service_id=repo["repo"]["service_id"], + owner=user, + ) + for repo in mock_repos[:-1] + ] + + for repo in preseeded_repos: + dbsession.add(repo) + dbsession.flush() + + SyncReposTask().run_impl( + dbsession, ownerid=user.ownerid, using_integration=True + ) + + mocked_app.tasks[sync_repo_languages_task_name].apply_async.assert_not_called() + + @pytest.mark.django_db(databases={"default"}) + def test_sync_repos_using_integration_affected_repos_known( + self, + mocker, + dbsession, + mock_owner_provider, + mock_redis, + ): + mock_all_plans_and_tiers() + user = OwnerFactory.create( + organizations=[], + service="github", + username="1nf1n1t3l00p", + unencrypted_oauth_token="sometesttoken", + permission=[], + service_id="45343385", + ) + dbsession.add(user) + + mocked_app = mocker.patch.object( + SyncRepoLanguagesGQLTask, + "app", + tasks={ + sync_repo_languages_gql_task_name: mocker.MagicMock(), + }, + ) + repository_service_ids = [ + ("460565350", "R_kgDOG3OrZg"), + ("665728948", "R_kgDOJ643tA"), + ("553624697", "R_kgDOIP-keQ"), + ("631985885", "R_kgDOJatW3Q"), # preseeded + ("623359086", "R_kgDOJSe0bg"), # preseeded + ] + service_ids = [x[0] for x in repository_service_ids] + service_ids_to_add = service_ids[:3] + + def repo_obj(service_id, name, language, private, branch, using_integration): + return { + "owner": { + "service_id": "test-owner-service-id", + "username": "test-owner-username", + }, + "repo": { + "service_id": service_id, + "name": name, + "language": language, + "private": private, + "branch": branch, + }, + "_using_integration": using_integration, + } + + preseeded_repos = [ + repo_obj("631985885", "example-python", "python", False, "main", True), + repo_obj("623359086", "sentry", "python", False, "main", True), + ] + + for repo in preseeded_repos: + new_repo = RepositoryFactory.create( + private=repo["repo"]["private"], + name=repo["repo"]["name"], + using_integration=repo["_using_integration"], + service_id=repo["repo"]["service_id"], + owner=user, + ) + dbsession.add(new_repo) + dbsession.flush() + + # These are the repos we're supposed to query from the service provider + async def side_effect(*args, **kwargs): + results = [ + { + "branch": "main", + "language": "python", + "name": "codecov-cli", + "owner": { + "is_expected_owner": False, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=", + "service_id": "8226205", + "username": "codecov", + }, + "service_id": 460565350, + "private": False, + }, + { + "branch": "main", + "language": "python", + "name": "worker", + "owner": { + "is_expected_owner": False, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=", + "service_id": "8226205", + "username": "codecov", + }, + "service_id": 665728948, + "private": False, + }, + { + "branch": "main", + "language": "python", + "name": "components-demo", + "owner": { + "is_expected_owner": True, + "node_id": "U_kgDOBfIxWg", + "username": "giovanni-guidini", + }, + "service_id": 553624697, + "private": False, + }, + ] + for r in results: + yield r + + mock_owner_provider.get_repos_from_nodeids_generator.side_effect = side_effect + mock_owner_provider.service = "github" + + SyncReposTask().run_impl( + dbsession, + ownerid=user.ownerid, + using_integration=True, + repository_service_ids=repository_service_ids, + ) + dbsession.commit() + + mock_owner_provider.get_repos_from_nodeids_generator.assert_called_with( + ["R_kgDOG3OrZg", "R_kgDOJ643tA", "R_kgDOIP-keQ"], user.username + ) + + repos = ( + dbsession.query(Repository) + .filter(Repository.service_id.in_(service_ids)) + .all() + ) + repos_added = list( + filter(lambda repo: repo.service_id in service_ids_to_add, repos) + ) + assert len(repos) == 5 + + mocked_app.tasks[sync_repo_languages_gql_task_name].apply_async.calls( + [ + call( + kwargs={ + "current_owner_id": user.ownerid, + "org_username": user.ownerid, + } + ) + for repo in repos_added + ] + ) + + upserted_owner = ( + dbsession.query(Owner) + .filter(Owner.service == "github", Owner.service_id == "8226205") + .first() + ) + assert upserted_owner is not None + assert upserted_owner.username == "codecov" diff --git a/apps/worker/tasks/tests/unit/test_sync_teams_task.py b/apps/worker/tasks/tests/unit/test_sync_teams_task.py new file mode 100644 index 0000000000..8b7056e337 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_sync_teams_task.py @@ -0,0 +1,135 @@ +from pathlib import Path + +import pytest +from freezegun import freeze_time + +from database.models import Owner +from database.tests.factories import OwnerFactory +from tasks.sync_teams import SyncTeamsTask + +here = Path(__file__) + + +class TestSyncTeamsTaskUnit(object): + def test_unknown_owner(self, mocker, mock_configuration, dbsession): + unknown_ownerid = 10404 + with pytest.raises(AssertionError, match="Owner not found"): + SyncTeamsTask().run_impl( + dbsession, unknown_ownerid, username=None, using_integration=False + ) + + def test_no_teams(self, mocker, mock_configuration, dbsession, codecov_vcr): + token = "testv2ztxs03zwys22v36ama292esl13swroe6dj" + user = OwnerFactory.create( + organizations=[], service="github", unencrypted_oauth_token=token + ) + dbsession.add(user) + dbsession.flush() + SyncTeamsTask().run_impl(dbsession, user.ownerid, using_integration=False) + assert user.organizations == [] + + def test_team_removed(self, mocker, mock_configuration, dbsession, codecov_vcr): + token = "testv2ztxs03zwys22v36ama292esl13swroe6dj" + prev_team = OwnerFactory.create(service="github", username="Evil_Corp") + dbsession.add(prev_team) + user = OwnerFactory.create( + organizations=[prev_team.ownerid], + service="github", + unencrypted_oauth_token=token, + ) + dbsession.add(user) + dbsession.flush() + SyncTeamsTask().run_impl(dbsession, user.ownerid, using_integration=False) + assert prev_team.ownerid not in user.organizations + + def test_team_data_updated( + self, mocker, mock_configuration, dbsession, codecov_vcr + ): + token = "testh0ry1fe5tiysbtbh6x47fdwotcsoyv7orqrd" + last_updated = "2018-06-01 01:02:30" + old_team = OwnerFactory.create( + service="github", + service_id="8226205", + username="cc_old", + name="CODECOV_OLD", + email="old@codecov.io", + updatestamp=last_updated, + ) + dbsession.add(old_team) + user = OwnerFactory.create( + organizations=[], service="github", unencrypted_oauth_token=token + ) + dbsession.add(user) + dbsession.flush() + + SyncTeamsTask().run_impl(dbsession, user.ownerid, using_integration=False) + assert old_team.ownerid in user.organizations + + # old team in db should have its data updated + assert old_team.email is None + assert old_team.username == "codecov" + assert old_team.name == "codecov" + assert str(old_team.updatestamp) > last_updated + + @freeze_time("2024-03-28T00:00:00") + def test_gitlab_subgroups( + self, mocker, mock_configuration, dbsession, codecov_vcr, caplog + ): + import logging + + caplog.set_level(logging.DEBUG) + token = "testenll80qbqhofao65" + user = OwnerFactory.create( + organizations=[], service="gitlab", unencrypted_oauth_token=token + ) + dbsession.add(user) + dbsession.flush() + SyncTeamsTask().run_impl(dbsession, user.ownerid, using_integration=False) + + assert len(user.organizations) == 6 + gitlab_groups = ( + dbsession.query(Owner).filter(Owner.ownerid.in_(user.organizations)).all() + ) + expected_owner_ids = [g.ownerid for g in gitlab_groups] + assert sorted(user.organizations) == sorted(expected_owner_ids) + + expected_groups = { + "4165904": { + "username": "l00p_group_1", + "name": "My Awesome Group", + "parent_service_id": None, + }, + "4570068": { + "username": "falco-group-1", + "name": "falco-group-1", + "parent_service_id": None, + }, + "4570071": { + "username": "falco-group-1:falco-subgroup-1", + "name": "falco-subgroup-1", + "parent_service_id": "4570068", + }, + "4165905": { + "username": "l00p_group_1:subgroup1", + "name": "subgroup1", + "parent_service_id": "4165904", + }, + "4165907": { + "username": "l00p_group_1:subgroup2", + "name": "subgroup2", + "parent_service_id": "4165904", + }, + "4255344": { + "username": "l00p_group_1:subgroup2:subsub", + "name": "subsub", + "parent_service_id": "4165907", + }, + } + for g in gitlab_groups: + service_id = g.service_id + expected_data = expected_groups.get(service_id, {}) + assert g.username == expected_data.get("username") + assert g.name == expected_data["name"] + assert g.createstamp.isoformat() == "2024-03-28T00:00:00+00:00" + if expected_data["parent_service_id"]: + assert g.parent_service_id == expected_data["parent_service_id"] diff --git a/apps/worker/tasks/tests/unit/test_test_results_finisher.py b/apps/worker/tasks/tests/unit/test_test_results_finisher.py new file mode 100644 index 0000000000..1ac4d62209 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_test_results_finisher.py @@ -0,0 +1,1205 @@ +from datetime import datetime, timedelta +from pathlib import Path + +import pytest +from mock import AsyncMock +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName +from shared.torngit.exceptions import TorngitClientError + +from database.enums import ReportType +from database.models import ( + CommitReport, + Flake, + ReducedError, + Repository, + RepositoryFlag, + Test, + TestInstance, +) +from database.tests.factories import ( + CommitFactory, + OwnerFactory, + PullFactory, + UploadFactory, +) +from services.repository import EnrichedPull +from services.test_results import generate_test_id +from services.urls import get_members_url +from tasks.test_results_finisher import TestResultsFinisherTask +from tests.helpers import mock_all_plans_and_tiers + +here = Path(__file__) + + +@pytest.fixture +def test_results_mock_app(mocker): + mocked_app = mocker.patch.object( + TestResultsFinisherTask, + "app", + tasks={ + "app.tasks.notify.Notify": mocker.MagicMock(), + "app.tasks.flakes.ProcessFlakesTask": mocker.MagicMock(), + "app.tasks.cache_rollup.CacheTestRollupsTask": mocker.MagicMock(), + }, + ) + return mocked_app + + +@pytest.fixture +def mock_repo_provider_comments(mocker): + m = mocker.MagicMock( + edit_comment=AsyncMock(return_value=True), + post_comment=AsyncMock(return_value={"id": 1}), + ) + _ = mocker.patch( + "helpers.notifier.get_repo_provider_service", + return_value=m, + ) + _ = mocker.patch( + "tasks.test_results_finisher.get_repo_provider_service", + return_value=m, + ) + return m + + +@pytest.fixture +def test_results_setup(mocker, dbsession): + mocker.patch.object(TestResultsFinisherTask, "hard_time_limit_task", 0) + + commit = CommitFactory.create( + message="hello world", + commitid="cd76b0821854a780b60012aed85af0a8263004ad", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="test-username", + repository__owner__service="github", + repository__owner__plan=PlanName.CODECOV_PRO_MONTHLY.value, + repository__name="test-repo-name", + ) + commit.branch = "main" + dbsession.add(commit) + dbsession.flush() + + commit.repository.branch = "main" + dbsession.flush() + + repoid = commit.repoid + commitid = commit.commitid + + current_report_row = CommitReport( + commit_id=commit.id_, report_type=ReportType.TEST_RESULTS.value + ) + dbsession.add(current_report_row) + dbsession.flush() + + pull = PullFactory.create(repository=commit.repository, head=commit.commitid) + dbsession.add(pull) + dbsession.flush() + + enriched_pull = EnrichedPull( + database_pull=pull, + provider_pull={}, + ) + + _ = mocker.patch( + "helpers.notifier.fetch_and_update_pull_request_information_from_commit", + return_value=enriched_pull, + ) + _ = mocker.patch( + "tasks.test_results_finisher.fetch_and_update_pull_request_information_from_commit", + return_value=enriched_pull, + ) + + uploads = [UploadFactory.create() for _ in range(4)] + uploads[3].created_at += timedelta(0, 3) + + for i, upload in enumerate(uploads): + upload.report = current_report_row + upload.report.commit.repoid = repoid + upload.build_url = f"https://example.com/build_url_{i}" + dbsession.add(upload) + dbsession.flush() + + flags = [RepositoryFlag(repository_id=repoid, flag_name=str(i)) for i in range(2)] + for flag in flags: + dbsession.add(flag) + dbsession.flush() + + uploads[0].flags = [flags[0]] + uploads[1].flags = [flags[1]] + uploads[2].flags = [] + uploads[3].flags = [flags[0]] + dbsession.flush() + + test_name = "test_name" + test_suite = "test_testsuite" + + test_id1 = generate_test_id(repoid, test_name + "0", test_suite, "a") + test1 = Test( + id_=test_id1, + repoid=repoid, + name="Class Name\x1f" + test_name + "0", + testsuite=test_suite, + flags_hash="a", + ) + dbsession.add(test1) + + test_id2 = generate_test_id(repoid, test_name + "1", test_suite, "b") + test2 = Test( + id_=test_id2, + repoid=repoid, + name=test_name + "1", + testsuite=test_suite, + flags_hash="b", + ) + dbsession.add(test2) + + test_id3 = generate_test_id(repoid, test_name + "2", test_suite, "") + test3 = Test( + id_=test_id3, + repoid=repoid, + name="Other Class Name\x1f" + test_name + "2", + testsuite=test_suite, + flags_hash="", + ) + dbsession.add(test3) + + test_id4 = generate_test_id(repoid, test_name + "3", test_suite, "") + test4 = Test( + id_=test_id4, + repoid=repoid, + name=test_name + "3", + testsuite=test_suite, + flags_hash="", + ) + dbsession.add(test4) + + dbsession.flush() + + test_instances = [ + TestInstance( + test_id=test_id1, + outcome="failure", + failure_message="This should not be in the comment, it will get overwritten by the last test instance", + duration_seconds=1.0, + upload_id=uploads[0].id, + repoid=repoid, + commitid=commitid, + ), + TestInstance( + test_id=test_id2, + outcome="failure", + failure_message="Shared \n\n\n\n
     ````````\n \r\n\r\n | test | test | test 
    failure message", + duration_seconds=2.0, + upload_id=uploads[1].id, + repoid=repoid, + commitid=commitid, + ), + TestInstance( + test_id=test_id3, + outcome="failure", + failure_message="Shared \n\n\n\n
     \n  ````````  \n \r\n\r\n | test | test | test 
    failure message", + duration_seconds=3.0, + upload_id=uploads[2].id, + repoid=repoid, + commitid=commitid, + ), + TestInstance( + test_id=test_id1, + outcome="failure", + failure_message="
    Fourth \r\n\r\n
    | test | instance |", + duration_seconds=4.0, + upload_id=uploads[3].id, + repoid=repoid, + commitid=commitid, + ), + TestInstance( + test_id=test_id4, + outcome="failure", + failure_message=None, + duration_seconds=5.0, + upload_id=uploads[3].id, + repoid=repoid, + commitid=commitid, + ), + ] + for instance in test_instances: + dbsession.add(instance) + dbsession.flush() + + return (repoid, commit, pull, test_instances) + + +@pytest.fixture +def test_results_setup_no_instances(mocker, dbsession): + mocker.patch.object(TestResultsFinisherTask, "hard_time_limit_task", 0) + + commit = CommitFactory.create( + message="hello world", + commitid="cd76b0821854a780b60012aed85af0a8263004ad", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="joseph-sentry", + repository__owner__service="github", + repository__owner__plan=PlanName.CODECOV_PRO_MONTHLY.value, + repository__name="codecov-demo", + ) + commit.branch = "main" + dbsession.add(commit) + dbsession.flush() + + repoid = commit.repoid + + current_report_row = CommitReport( + commit_id=commit.id_, report_type=ReportType.TEST_RESULTS.value + ) + dbsession.add(current_report_row) + dbsession.flush() + + pull = PullFactory.create(repository=commit.repository, head=commit.commitid) + + enriched_pull = EnrichedPull( + database_pull=pull, + provider_pull={}, + ) + + _ = mocker.patch( + "helpers.notifier.fetch_and_update_pull_request_information_from_commit", + return_value=enriched_pull, + ) + _ = mocker.patch( + "tasks.test_results_finisher.fetch_and_update_pull_request_information_from_commit", + return_value=enriched_pull, + ) + + uploads = [UploadFactory.create() for _ in range(4)] + uploads[3].created_at += timedelta(0, 3) + + for upload in uploads: + upload.report = current_report_row + upload.report.commit.repoid = repoid + dbsession.add(upload) + dbsession.flush() + + flags = [RepositoryFlag(repository_id=repoid, flag_name=str(i)) for i in range(2)] + for flag in flags: + dbsession.add(flag) + dbsession.flush() + + uploads[0].flags = [flags[0]] + uploads[1].flags = [flags[1]] + uploads[2].flags = [] + uploads[3].flags = [flags[0]] + dbsession.flush() + + test_name = "test_name" + test_suite = "test_testsuite" + + test_id1 = generate_test_id(repoid, test_name + "0", test_suite, "a") + test1 = Test( + id_=test_id1, + repoid=repoid, + name=test_name + "0", + testsuite=test_suite, + flags_hash="a", + ) + dbsession.add(test1) + + test_id2 = generate_test_id(repoid, test_name + "1", test_suite, "b") + test2 = Test( + id_=test_id2, + repoid=repoid, + name=test_name + "1", + testsuite=test_suite, + flags_hash="b", + ) + dbsession.add(test2) + + test_id3 = generate_test_id(repoid, test_name + "2", test_suite, "") + test3 = Test( + id_=test_id3, + repoid=repoid, + name=test_name + "2", + testsuite=test_suite, + flags_hash="", + ) + dbsession.add(test3) + + test_id4 = generate_test_id(repoid, test_name + "3", test_suite, "") + test4 = Test( + id_=test_id4, + repoid=repoid, + name=test_name + "3", + testsuite=test_suite, + flags_hash="", + ) + dbsession.add(test4) + + dbsession.flush() + + return (repoid, commit, pull, None) + + +class TestUploadTestFinisherTask(object): + @pytest.fixture(autouse=True) + def setup(self): + mock_all_plans_and_tiers() + + @pytest.mark.integration + @pytest.mark.django_db + def test_upload_finisher_task_call( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + test_results_mock_app, + mock_repo_provider_comments, + test_results_setup, + ): + repoid, commit, pull, _ = test_results_setup + + result = TestResultsFinisherTask().run_impl( + dbsession, + True, + repoid=repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + impl_type="both", + ) + + expected_result = { + "notify_attempted": True, + "notify_succeeded": True, + "queue_notify": False, + } + + test_results_mock_app.tasks[ + "app.tasks.cache_rollup.CacheTestRollupsTask" + ].apply_async.assert_called_with( + kwargs={ + "repo_id": repoid, + "branch": "main", + "impl_type": "both", + }, + ) + + assert expected_result == result + mock_repo_provider_comments.post_comment.assert_called_with( + pull.pullid, + """### :x: 4 Tests Failed: +| Tests completed | Failed | Passed | Skipped | +|---|---|---|---| +| 4 | 4 | 0 | 0 | +
    View the top 3 failed test(s) by shortest run time + +> +> ```python +> test_name1 +> ``` +> +>
    Stack Traces | 2s run time +> +> > `````````python +> > Shared +> > +> > +> > +> >
     ````````
    +> >  
    +> > 
    +> >  | test | test | test 
    failure message +> > ````````` +> > [View](https://example.com/build_url_1) the CI Build +> +>
    + + +> +> ```python +> Other Class Name test_name2 +> ``` +> +>
    Stack Traces | 3s run time +> +> > `````````python +> > Shared +> > +> > +> > +> >
     
    +> >   ````````  
    +> >  
    +> > 
    +> >  | test | test | test 
    failure message +> > ````````` +> > [View](https://example.com/build_url_2) the CI Build +> +>
    + + +> +> ```python +> Class Name test_name0 +> ``` +> +>
    Stack Traces | 4s run time +> +> > +> > ```python +> >
    Fourth 
    +> > 
    +> > 
    | test | instance | +> > ``` +> > +> > [View](https://example.com/build_url_3) the CI Build +> +>
    + +
    + +To view more test analytics, go to the [Test Analytics Dashboard](https://app.codecov.io/gh/test-username/test-repo-name/tests/main) +📋 Got 3 mins? [Take this short survey](https://forms.gle/BpocVj23nhr2Y45G7) to help us improve Test Analytics.""", + ) + + @pytest.mark.integration + @pytest.mark.django_db + def test_upload_finisher_task_call_no_failures( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + test_results_mock_app, + mock_repo_provider_comments, + test_results_setup, + ): + repoid, commit, _, test_instances = test_results_setup + + for instance in test_instances: + instance.outcome = "pass" + dbsession.flush() + + result = TestResultsFinisherTask().run_impl( + dbsession, + True, + repoid=repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + ) + + expected_result = { + "notify_attempted": False, + "notify_succeeded": False, + "queue_notify": True, + } + test_results_mock_app.tasks[ + "app.tasks.notify.Notify" + ].apply_async.assert_called_with( + args=None, + kwargs={ + "commitid": commit.commitid, + "current_yaml": {"codecov": {"max_report_age": False}}, + "repoid": repoid, + }, + ) + + test_results_mock_app.tasks[ + "app.tasks.cache_rollup.CacheTestRollupsTask" + ].apply_async.assert_called_with( + kwargs={ + "repo_id": repoid, + "branch": "main", + "impl_type": "old", + }, + ) + + assert expected_result == result + + @pytest.mark.integration + @pytest.mark.django_db + def test_upload_finisher_task_call_no_pull( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + test_results_mock_app, + mock_repo_provider_comments, + test_results_setup, + ): + repoid, commit, pull, _ = test_results_setup + + _ = mocker.patch( + "tasks.test_results_finisher.fetch_and_update_pull_request_information_from_commit", + return_value=None, + ) + + result = TestResultsFinisherTask().run_impl( + dbsession, + True, + repoid=repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + ) + + expected_result = { + "notify_attempted": False, + "notify_succeeded": False, + "queue_notify": False, + } + + assert expected_result == result + + @pytest.mark.django_db + @pytest.mark.integration + def test_upload_finisher_task_call_no_success( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + test_results_mock_app, + mock_repo_provider_comments, + test_results_setup_no_instances, + ): + repoid, commit, pull, _ = test_results_setup_no_instances + + result = TestResultsFinisherTask().run_impl( + dbsession, + False, + repoid=repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + ) + + expected_result = { + "notify_attempted": False, + "notify_succeeded": False, + "queue_notify": True, + } + + assert expected_result == result + + mock_repo_provider_comments.post_comment.assert_called_with( + pull.pullid, + ":x: We are unable to process any of the uploaded JUnit XML files. Please ensure your files are in the right format.", + ) + + test_results_mock_app.tasks[ + "app.tasks.notify.Notify" + ].apply_async.assert_called_with( + args=None, + kwargs={ + "commitid": commit.commitid, + "current_yaml": {"codecov": {"max_report_age": False}}, + "repoid": repoid, + }, + ) + + test_results_mock_app.tasks[ + "app.tasks.cache_rollup.CacheTestRollupsTask" + ].apply_async.assert_called_with( + kwargs={ + "repo_id": repoid, + "branch": "main", + "impl_type": "old", + }, + ) + + @pytest.mark.integration + @pytest.mark.django_db + def test_upload_finisher_task_call_upgrade_comment( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + test_results_mock_app, + mock_repo_provider_comments, + test_results_setup, + ): + repoid, commit, pull, _ = test_results_setup + + repo = dbsession.query(Repository).filter(Repository.repoid == repoid).first() + repo.owner.plan_activated_users = [] + repo.owner.plan = PlanName.CODECOV_PRO_MONTHLY.value + repo.private = True + dbsession.flush() + + pr_author = OwnerFactory(service="github", service_id=100) + dbsession.add(pr_author) + dbsession.flush() + + enriched_pull = EnrichedPull( + database_pull=pull, + provider_pull={"author": {"id": "100", "username": "test_username"}}, + ) + _ = mocker.patch( + "helpers.notifier.fetch_and_update_pull_request_information_from_commit", + return_value=enriched_pull, + ) + _ = mocker.patch( + "tasks.test_results_finisher.fetch_and_update_pull_request_information_from_commit", + return_value=enriched_pull, + ) + + result = TestResultsFinisherTask().run_impl( + dbsession, + True, + repoid=repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + ) + + assert result == { + "notify_attempted": True, + "notify_succeeded": True, + "queue_notify": False, + } + + mock_repo_provider_comments.post_comment.assert_called_with( + pull.pullid, + f"The author of this PR, test_username, is not an activated member of this organization on Codecov.\nPlease [activate this user on Codecov]({get_members_url(pull)}) to display this PR comment.\nCoverage data is still being uploaded to Codecov.io for purposes of overall coverage calculations.\nPlease don't hesitate to email us at support@codecov.io with any questions.", + ) + + test_results_mock_app.tasks["app.tasks.notify.Notify"].assert_not_called() + + test_results_mock_app.tasks[ + "app.tasks.cache_rollup.CacheTestRollupsTask" + ].apply_async.assert_called_with( + kwargs={ + "repo_id": repoid, + "branch": "main", + "impl_type": "old", + }, + ) + + @pytest.mark.integration + @pytest.mark.django_db + def test_upload_finisher_task_call_existing_comment( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + test_results_mock_app, + mock_repo_provider_comments, + test_results_setup, + ): + repoid, commit, pull, _ = test_results_setup + + pull.commentid = 1 + dbsession.flush() + + result = TestResultsFinisherTask().run_impl( + dbsession, + True, + repoid=repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + ) + + expected_result = { + "notify_attempted": True, + "notify_succeeded": True, + "queue_notify": False, + } + + test_results_mock_app.tasks[ + "app.tasks.cache_rollup.CacheTestRollupsTask" + ].apply_async.assert_called_with( + kwargs={ + "repo_id": repoid, + "branch": "main", + "impl_type": "old", + }, + ) + + mock_repo_provider_comments.edit_comment.assert_called_with( + pull.pullid, + 1, + """### :x: 4 Tests Failed: +| Tests completed | Failed | Passed | Skipped | +|---|---|---|---| +| 4 | 4 | 0 | 0 | +
    View the top 3 failed test(s) by shortest run time + +> +> ```python +> test_name1 +> ``` +> +>
    Stack Traces | 2s run time +> +> > `````````python +> > Shared +> > +> > +> > +> >
     ````````
    +> >  
    +> > 
    +> >  | test | test | test 
    failure message +> > ````````` +> > [View](https://example.com/build_url_1) the CI Build +> +>
    + + +> +> ```python +> Other Class Name test_name2 +> ``` +> +>
    Stack Traces | 3s run time +> +> > `````````python +> > Shared +> > +> > +> > +> >
     
    +> >   ````````  
    +> >  
    +> > 
    +> >  | test | test | test 
    failure message +> > ````````` +> > [View](https://example.com/build_url_2) the CI Build +> +>
    + + +> +> ```python +> Class Name test_name0 +> ``` +> +>
    Stack Traces | 4s run time +> +> > +> > ```python +> >
    Fourth 
    +> > 
    +> > 
    | test | instance | +> > ``` +> > +> > [View](https://example.com/build_url_3) the CI Build +> +>
    + +
    + +To view more test analytics, go to the [Test Analytics Dashboard](https://app.codecov.io/gh/test-username/test-repo-name/tests/main) +📋 Got 3 mins? [Take this short survey](https://forms.gle/BpocVj23nhr2Y45G7) to help us improve Test Analytics.""", + ) + + assert expected_result == result + + @pytest.mark.integration + @pytest.mark.django_db + def test_upload_finisher_task_call_comment_fails( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + test_results_mock_app, + mock_repo_provider_comments, + test_results_setup, + ): + repoid, commit, _, _ = test_results_setup + + mock_repo_provider_comments.post_comment.side_effect = TorngitClientError + + result = TestResultsFinisherTask().run_impl( + dbsession, + True, + repoid=repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + ) + + expected_result = { + "notify_attempted": True, + "notify_succeeded": False, + "queue_notify": False, + } + + test_results_mock_app.tasks[ + "app.tasks.cache_rollup.CacheTestRollupsTask" + ].apply_async.assert_called_with( + kwargs={ + "repo_id": repoid, + "branch": "main", + "impl_type": "old", + }, + ) + + assert expected_result == result + + @pytest.mark.parametrize( + "fail_count,count,recent_passes_count", [(2, 15, 13), (50, 150, 10)] + ) + @pytest.mark.integration + @pytest.mark.django_db + def test_upload_finisher_task_call_with_flaky( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + test_results_mock_app, + mock_repo_provider_comments, + test_results_setup, + fail_count, + count, + recent_passes_count, + ): + repoid, commit, pull, test_instances = test_results_setup + + for i, instance in enumerate(test_instances): + if i != 2: + dbsession.delete(instance) + dbsession.flush() + + r = ReducedError() + r.message = "failure_message" + + dbsession.add(r) + dbsession.flush() + + f = Flake() + f.repoid = repoid + f.testid = test_instances[2].test_id + f.reduced_error = r + f.count = count + f.fail_count = fail_count + f.recent_passes_count = recent_passes_count + f.start_date = datetime.now() + f.end_date = None + + dbsession.add(f) + dbsession.flush() + + result = TestResultsFinisherTask().run_impl( + dbsession, + True, + repoid=repoid, + commitid=commit.commitid, + commit_yaml={ + "codecov": {"max_report_age": False}, + "test_analytics": {"flake_detection": True}, + }, + ) + + expected_result = { + "notify_attempted": True, + "notify_succeeded": True, + "queue_notify": False, + } + + assert expected_result == result + + test_results_mock_app.tasks[ + "app.tasks.cache_rollup.CacheTestRollupsTask" + ].apply_async.assert_called_with( + kwargs={ + "repo_id": repoid, + "branch": "main", + "impl_type": "old", + }, + ) + + mock_repo_provider_comments.post_comment.assert_called_with( + pull.pullid, + f"""### :x: 1 Tests Failed: +| Tests completed | Failed | Passed | Skipped | +|---|---|---|---| +| 1 | 1 | 0 | 0 | +
    {"View the top 1 failed test(s) by shortest run time" if (count - fail_count) == recent_passes_count else "View the full list of 1 :snowflake: flaky tests"} + +> +> ```python +> Other Class Name test_name2 +> ``` +> {f"\n> **Flake rate in main:** 33.33% (Passed {count - fail_count} times, Failed {fail_count} times)" if (count - fail_count) != recent_passes_count else ""} +>
    Stack Traces | 3s run time +> +> > `````````python +> > Shared +> > +> > +> > +> >
     
    +> >   ````````  
    +> >  
    +> > 
    +> >  | test | test | test 
    failure message +> > ````````` +> > [View](https://example.com/build_url_2) the CI Build +> +>
    + +
    + +To view more test analytics, go to the [Test Analytics Dashboard](https://app.codecov.io/gh/test-username/test-repo-name/tests/main) +📋 Got 3 mins? [Take this short survey](https://forms.gle/BpocVj23nhr2Y45G7) to help us improve Test Analytics.""", + ) + + @pytest.mark.integration + @pytest.mark.django_db + def test_upload_finisher_task_call_main_branch( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + test_results_mock_app, + mock_repo_provider_comments, + test_results_setup, + ): + commit_yaml = { + "codecov": {"max_report_age": False}, + "test_analytics": {"flake_detection": True}, + } + + repoid, commit, pull, test_instances = test_results_setup + + commit.merged = True + + result = TestResultsFinisherTask().run_impl( + dbsession, + True, + repoid=repoid, + commitid=commit.commitid, + commit_yaml=commit_yaml, + ) + + expected_result = { + "notify_attempted": True, + "notify_succeeded": True, + "queue_notify": False, + } + + assert expected_result == result + + test_results_mock_app.tasks[ + "app.tasks.flakes.ProcessFlakesTask" + ].apply_async.assert_called_with( + kwargs={ + "repo_id": repoid, + "impl_type": "old", + }, + ) + test_results_mock_app.tasks[ + "app.tasks.cache_rollup.CacheTestRollupsTask" + ].apply_async.assert_called_with( + kwargs={ + "repo_id": repoid, + "branch": "main", + "impl_type": "old", + }, + ) + + @pytest.mark.integration + @pytest.mark.django_db + def test_upload_finisher_task_call_computed_name( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + test_results_mock_app, + mock_repo_provider_comments, + test_results_setup, + ): + repoid, commit, pull, test_instances = test_results_setup + + for instance in test_instances: + instance.test.computed_name = f"hello_{instance.test.name}" + + dbsession.flush() + + result = TestResultsFinisherTask().run_impl( + dbsession, + True, + repoid=repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + ) + + expected_result = { + "notify_attempted": True, + "notify_succeeded": True, + "queue_notify": False, + } + + assert expected_result == result + mock_repo_provider_comments.post_comment.assert_called_with( + pull.pullid, + """### :x: 4 Tests Failed: +| Tests completed | Failed | Passed | Skipped | +|---|---|---|---| +| 4 | 4 | 0 | 0 | +
    View the top 3 failed test(s) by shortest run time + +> +> ```python +> hello_test_name1 +> ``` +> +>
    Stack Traces | 2s run time +> +> > `````````python +> > Shared +> > +> > +> > +> >
     ````````
    +> >  
    +> > 
    +> >  | test | test | test 
    failure message +> > ````````` +> > [View](https://example.com/build_url_1) the CI Build +> +>
    + + +> +> ```python +> hello_Other Class Name test_name2 +> ``` +> +>
    Stack Traces | 3s run time +> +> > `````````python +> > Shared +> > +> > +> > +> >
     
    +> >   ````````  
    +> >  
    +> > 
    +> >  | test | test | test 
    failure message +> > ````````` +> > [View](https://example.com/build_url_2) the CI Build +> +>
    + + +> +> ```python +> hello_Class Name test_name0 +> ``` +> +>
    Stack Traces | 4s run time +> +> > +> > ```python +> >
    Fourth 
    +> > 
    +> > 
    | test | instance | +> > ``` +> > +> > [View](https://example.com/build_url_3) the CI Build +> +>
    + +
    + +To view more test analytics, go to the [Test Analytics Dashboard](https://app.codecov.io/gh/test-username/test-repo-name/tests/main) +📋 Got 3 mins? [Take this short survey](https://forms.gle/BpocVj23nhr2Y45G7) to help us improve Test Analytics.""", + ) + + @pytest.mark.integration + @pytest.mark.django_db + @pytest.mark.parametrize("plan", [DEFAULT_FREE_PLAN, "users-pr-inappm"]) + def test_upload_finisher_task_call_main_with_plan( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + test_results_mock_app, + mock_repo_provider_comments, + test_results_setup, + plan, + ): + mocker.patch.object(TestResultsFinisherTask, "get_flaky_tests") + + commit_yaml = { + "codecov": { + "max_report_age": False, + }, + "test_analytics": {"flake_detection": True}, + } + + repoid, commit, pull, test_instances = test_results_setup + + commit.merged = True + + repo = dbsession.query(Repository).filter_by(repoid=repoid).first() + repo.owner.plan = plan + dbsession.flush() + result = TestResultsFinisherTask().run_impl( + dbsession, + True, + repoid=repoid, + commitid=commit.commitid, + commit_yaml=commit_yaml, + ) + + expected_result = { + "notify_attempted": True, + "notify_succeeded": True, + "queue_notify": False, + } + + assert expected_result == result + + if plan == PlanName.CODECOV_PRO_MONTHLY.value: + test_results_mock_app.tasks[ + "app.tasks.flakes.ProcessFlakesTask" + ].apply_async.assert_called_with( + kwargs={ + "repo_id": repoid, + "impl_type": "old", + }, + ) + else: + test_results_mock_app.tasks[ + "app.tasks.flakes.ProcessFlakesTask" + ].apply_async.assert_not_called() diff --git a/apps/worker/tasks/tests/unit/test_test_results_processor_task.py b/apps/worker/tasks/tests/unit/test_test_results_processor_task.py new file mode 100644 index 0000000000..41b2d018ee --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_test_results_processor_task.py @@ -0,0 +1,704 @@ +from datetime import date, datetime, timedelta, timezone +from itertools import chain +from pathlib import Path + +import pytest +from shared.storage.exceptions import FileNotInStorageError +from time_machine import travel + +from database.models import CommitReport, RepositoryFlag +from database.models.reports import DailyTestRollup, Test, TestFlagBridge, TestInstance +from database.tests.factories import CommitFactory, UploadFactory +from database.tests.factories.reports import FlakeFactory +from services.test_results import generate_flags_hash, generate_test_id +from tasks.test_results_processor import ( + TestResultsProcessorTask, +) + +here = Path(__file__) + + +class TestUploadTestProcessorTask(object): + @pytest.mark.integration + def test_upload_processor_task_call( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + ): + tests = dbsession.query(Test).all() + test_instances = dbsession.query(TestInstance).all() + assert len(tests) == 0 + assert len(test_instances) == 0 + + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + with open(here.parent.parent / "samples" / "sample_test.json") as f: + content = f.read() + mock_storage.write_file("archive", url, content) + upload = UploadFactory.create(storage_path=url) + dbsession.add(upload) + dbsession.flush() + redis_queue = [{"url": url, "upload_id": upload.id_}] + mocker.patch.object(TestResultsProcessorTask, "app", celery_app) + + commit = CommitFactory.create( + message="hello world", + commitid="cd76b0821854a780b60012aed85af0a8263004ad", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="joseph-sentry", + repository__owner__service="github", + repository__name="codecov-demo", + ) + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + result = TestResultsProcessorTask().run_impl( + dbsession, + previous_result=False, + repoid=upload.report.commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + arguments_list=redis_queue, + ) + expected_result = True + tests = dbsession.query(Test).all() + test_instances = dbsession.query(TestInstance).all() + failures = dbsession.query(TestInstance).filter_by(outcome="failure").all() + + assert len(tests) == 4 + assert len(test_instances) == 4 + assert len(failures) == 1 + + assert ( + failures[0].failure_message + == """def test_divide():\n> assert Calculator.divide(1, 2) == 0.5\nE assert 1.0 == 0.5\nE + where 1.0 = (1, 2)\nE + where = Calculator.divide\n\napi/temp/calculator/test_calculator.py:30: AssertionError""" + ) + assert ( + failures[0].test.name + == "api.temp.calculator.test_calculator\x1ftest_divide" + ) + assert expected_result == result + assert commit.message == "hello world" + assert ( + mock_storage.read_file("archive", url) + == b"""# path=codecov-demo/temp.junit.xml +def test_divide(): +> assert Calculator.divide(1, 2) == 0.5 +E assert 1.0 == 0.5 +E + where 1.0 = <function Calculator.divide at 0x104c9eb90>(1, 2) +E + where <function Calculator.divide at 0x104c9eb90> = Calculator.divide + +api/temp/calculator/test_calculator.py:30: AssertionError +<<<<<< EOF +""" + ) + + @pytest.mark.integration + @pytest.mark.integration + def test_test_result_processor_task_error_parsing_file( + self, + caplog, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + ): + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + with open(here.parent.parent / "samples" / "sample_test.json") as f: + content = f.read() + mock_storage.write_file("archive", url, content) + upload = UploadFactory.create(storage_path=url) + dbsession.add(upload) + dbsession.flush() + redis_queue = [{"url": url, "upload_id": upload.id_}] + mocker.patch.object(TestResultsProcessorTask, "app", celery_app) + mocker.patch( + "tasks.test_results_processor.test_results_parser.parse_raw_upload", + side_effect=RuntimeError("Error parsing file"), + ) + + commit = CommitFactory.create( + message="hello world", + commitid="cd76b0821854a780b60012aed85af0a8263004ad", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="joseph-sentry", + repository__owner__service="github", + repository__name="codecov-demo", + ) + + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + + result = TestResultsProcessorTask().run_impl( + dbsession, + previous_result=False, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + arguments_list=redis_queue, + ) + + assert "Error parsing file" in caplog.text + assert result == False + + @pytest.mark.integration + def test_test_result_processor_task_delete_archive( + self, + caplog, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + ): + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + with open(here.parent.parent / "samples" / "sample_test.json") as f: + content = f.read() + mock_storage.write_file("archive", url, content) + upload = UploadFactory.create(storage_path=url) + dbsession.add(upload) + dbsession.flush() + redis_queue = [{"url": url, "upload_id": upload.id_}] + mocker.patch.object(TestResultsProcessorTask, "app", celery_app) + mocker.patch.object( + TestResultsProcessorTask, "should_delete_archive", return_value=True + ) + + commit = CommitFactory.create( + message="hello world", + commitid="cd76b0821854a780b60012aed85af0a8263004ad", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="joseph-sentry", + repository__owner__service="github", + repository__name="codecov-demo", + ) + + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + result = TestResultsProcessorTask().run_impl( + dbsession, + previous_result=False, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + arguments_list=redis_queue, + ) + expected_result = True + + tests = dbsession.query(Test).all() + test_instances = dbsession.query(TestInstance).all() + failures = dbsession.query(TestInstance).filter_by(outcome="failure").all() + + assert result == expected_result + + assert len(tests) == 4 + assert len(test_instances) == 4 + assert len(failures) == 1 + + assert set([test.flags_hash for test in tests]) == { + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + assert set([test_instance.test.id for test_instance in test_instances]) == set( + [test.id_ for test in tests] + ) + assert "Deleting uploaded file as requested" in caplog.text + with pytest.raises(FileNotInStorageError): + mock_storage.read_file("archive", url) + + @pytest.mark.integration + def test_test_result_processor_task_bad_file( + self, + caplog, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + ): + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + mock_storage.write_file( + "archive", + url, + b'{"test_results_files": [{"filename": "blah", "format": "blah", "data": "eJxLyknMSIJiAB8CBMY="}]}', + ) + upload = UploadFactory.create(storage_path=url) + dbsession.add(upload) + dbsession.flush() + redis_queue = [{"url": url, "upload_id": upload.id_}] + mocker.patch.object(TestResultsProcessorTask, "app", celery_app) + + commit = CommitFactory.create( + message="hello world", + commitid="cd76b0821854a780b60012aed85af0a8263004ad", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="joseph-sentry", + repository__owner__service="github", + repository__name="codecov-demo", + ) + + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + result = TestResultsProcessorTask().run_impl( + dbsession, + previous_result=False, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + arguments_list=redis_queue, + ) + expected_result = False + + assert expected_result == result + assert ( + "No test result files were successfully parsed for this upload" + in caplog.text + ) + + @pytest.mark.integration + @travel("2025-01-01T00:00:00Z", tick=False) + def test_upload_processor_task_call_existing_test( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + ): + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + with open(here.parent.parent / "samples" / "sample_test.json") as f: + content = f.read() + mock_storage.write_file("archive", url, content) + upload = UploadFactory.create( + storage_path=url, + ) + dbsession.add(upload) + dbsession.flush() + repoid = upload.report.commit.repoid + redis_queue = [{"url": url, "upload_id": upload.id_}] + mocker.patch.object(TestResultsProcessorTask, "app", celery_app) + + commit = CommitFactory.create( + message="hello world", + commitid="cd76b0821854a780b60012aed85af0a8263004ad", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="joseph-sentry", + repository__owner__service="github", + repository__name="codecov-demo", + ) + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + + test_id = generate_test_id( + repoid, + "pytest", + "api.temp.calculator.test_calculator\x1ftest_divide", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + ) + existing_test = Test( + repoid=repoid, + flags_hash="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + name="api.temp.calculator.test_calculator\x1ftest_divide", + testsuite="pytest", + id_=test_id, + ) + dbsession.add(existing_test) + dbsession.flush() + + result = TestResultsProcessorTask().run_impl( + dbsession, + previous_result=False, + repoid=repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + arguments_list=redis_queue, + ) + expected_result = True + tests = dbsession.query(Test).all() + test_instances = dbsession.query(TestInstance).all() + failures = dbsession.query(TestInstance).filter_by(outcome="failure").all() + + assert len(tests) == 4 + assert len(test_instances) == 4 + assert len(failures) == 1 + + assert ( + failures[0].failure_message + == """def test_divide():\n> assert Calculator.divide(1, 2) == 0.5\nE assert 1.0 == 0.5\nE + where 1.0 = (1, 2)\nE + where = Calculator.divide\n\napi/temp/calculator/test_calculator.py:30: AssertionError""" + ) + assert ( + failures[0].test.name + == "api.temp.calculator.test_calculator\x1ftest_divide" + ) + assert expected_result == result + assert commit.message == "hello world" + + @pytest.mark.integration + def test_upload_processor_task_call_existing_test_diff_flags_hash( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + ): + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + with open(here.parent.parent / "samples" / "sample_test.json") as f: + content = f.read() + mock_storage.write_file("archive", url, content) + upload = UploadFactory.create( + storage_path=url, + ) + dbsession.add(upload) + dbsession.flush() + repoid = upload.report.commit.repoid + repo_flag = RepositoryFlag( + repository=upload.report.commit.repository, flag_name="hello_world" + ) + upload.flags = [repo_flag] + dbsession.flush() + + redis_queue = [{"url": url, "upload_id": upload.id_}] + mocker.patch.object(TestResultsProcessorTask, "app", celery_app) + + commit = CommitFactory.create( + message="hello world", + commitid="cd76b0821854a780b60012aed85af0a8263004ad", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="joseph-sentry", + repository__owner__service="github", + repository__name="codecov-demo", + ) + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + + flags_hash = generate_flags_hash(upload.flag_names) + test_id = generate_test_id( + repoid, + "pytest", + "api.temp.calculator.test_calculator\x1ftest_divide", + flags_hash, + ) + existing_test = Test( + repoid=repoid, + flags_hash=flags_hash, + name="api.temp.calculator.test_calculator\x1ftest_divide", + testsuite="pytest", + id_=test_id, + ) + dbsession.add(existing_test) + dbsession.flush() + + result = TestResultsProcessorTask().run_impl( + dbsession, + previous_result=False, + repoid=repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + arguments_list=redis_queue, + ) + expected_result = True + tests = dbsession.query(Test).all() + test_instances = dbsession.query(TestInstance).all() + failures = dbsession.query(TestInstance).filter_by(outcome="failure").all() + + test_flag_bridges = dbsession.query(TestFlagBridge).all() + + assert set(bridge.test_id for bridge in test_flag_bridges) == set( + instance.test_id for instance in test_instances + ) + for bridge in test_flag_bridges: + assert bridge.flag == repo_flag + + assert len(tests) == 4 + assert len(test_instances) == 4 + assert len(failures) == 1 + + assert ( + failures[0].failure_message + == """def test_divide():\n> assert Calculator.divide(1, 2) == 0.5\nE assert 1.0 == 0.5\nE + where 1.0 = (1, 2)\nE + where = Calculator.divide\n\napi/temp/calculator/test_calculator.py:30: AssertionError""" + ) + assert ( + failures[0].test.name + == "api.temp.calculator.test_calculator\x1ftest_divide" + ) + assert expected_result == result + assert commit.message == "hello world" + + @pytest.mark.integration + def test_upload_processor_task_call_daily_test_totals( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + ): + with travel("1970-1-1T00:00:00Z", tick=False): + first_url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + with open( + here.parent.parent / "samples" / "sample_multi_test_part_1.json" + ) as f: + content = f.read() + mock_storage.write_file("archive", first_url, content) + + first_commit = CommitFactory.create( + message="hello world", + commitid="cd76b0821854a780b60012aed85af0a8263004ad", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="joseph-sentry", + repository__owner__service="github", + repository__name="codecov-demo", + branch="first_branch", + ) + dbsession.add(first_commit) + dbsession.flush() + + first_report_row = CommitReport(commit_id=first_commit.id_) + dbsession.add(first_report_row) + dbsession.flush() + + upload = UploadFactory.create( + storage_path=first_url, report=first_report_row + ) + dbsession.add(upload) + dbsession.flush() + + repoid = upload.report.commit.repoid + redis_queue = [{"url": first_url, "upload_id": upload.id_}] + mocker.patch.object(TestResultsProcessorTask, "app", celery_app) + + result = TestResultsProcessorTask().run_impl( + dbsession, + previous_result=False, + repoid=repoid, + commitid=first_commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + arguments_list=redis_queue, + ) + expected_result = True + + rollups = dbsession.query(DailyTestRollup).all() + + assert [r.branch for r in rollups] == [ + "first_branch", + "first_branch", + ] + + assert [r.date for r in rollups] == [ + date.today(), + date.today(), + ] + + with travel("1970-1-2T00:00:00Z", tick=False): + second_commit = CommitFactory.create( + message="hello world 2", + commitid="bd76b0821854a780b60012aed85af0a8263004ad", + repository=first_commit.repository, + branch="second_branch", + ) + dbsession.add(second_commit) + dbsession.flush() + + second_report_row = CommitReport(commit_id=second_commit.id_) + dbsession.add(second_report_row) + dbsession.flush() + + second_url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/b84d445c-9c1e-434f-8275-f18f1f320f81.txt" + with open( + here.parent.parent / "samples" / "sample_multi_test_part_2.json" + ) as f: + content = f.read() + mock_storage.write_file("archive", second_url, content) + upload = UploadFactory.create( + storage_path=second_url, report=second_report_row + ) + dbsession.add(upload) + dbsession.flush() + + tests = dbsession.query(Test).all() + for test in tests: + flake = FlakeFactory.create(test=test) + dbsession.add(flake) + dbsession.flush() + + redis_queue = [{"url": second_url, "upload_id": upload.id_}] + + result = TestResultsProcessorTask().run_impl( + dbsession, + previous_result=False, + repoid=repoid, + commitid=second_commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + arguments_list=redis_queue, + ) + expected_result = True + + assert result == expected_result + + rollups_first_branch: list[DailyTestRollup] = ( + dbsession.query(DailyTestRollup).filter_by(branch="first_branch").all() + ) + + assert set(r.date for r in rollups_first_branch) == { + date.today() - timedelta(days=1) + } + assert set(r.fail_count for r in rollups_first_branch) == {0, 1} + assert set(r.pass_count for r in rollups_first_branch) == {1} + assert set(r.skip_count for r in rollups_first_branch) == {0} + assert set(r.flaky_fail_count for r in rollups_first_branch) == {0} + assert set( + chain.from_iterable(r.commits_where_fail for r in rollups_first_branch) + ) == { + "cd76b0821854a780b60012aed85af0a8263004ad", + } + assert set(r.latest_run for r in rollups_first_branch) == { + datetime(1970, 1, 1, 0, 0, tzinfo=timezone.utc) + } + assert set(r.avg_duration_seconds for r in rollups_first_branch) == { + 7.2, + 0.001, + } + assert set(r.last_duration_seconds for r in rollups_first_branch) == { + 7.2, + 0.001, + } + + rollups_second_branch: list[DailyTestRollup] = ( + dbsession.query(DailyTestRollup).filter_by(branch="second_branch").all() + ) + + assert set(r.date for r in rollups_second_branch) == {date.today()} + assert set(r.fail_count for r in rollups_second_branch) == {0, 1} + assert set(r.pass_count for r in rollups_second_branch) == {0, 2} + assert set(r.skip_count for r in rollups_second_branch) == {0} + assert set(r.flaky_fail_count for r in rollups_second_branch) == {0, 1} + assert set( + chain.from_iterable(r.commits_where_fail for r in rollups_second_branch) + ) == { + "bd76b0821854a780b60012aed85af0a8263004ad", + } + assert set(r.latest_run for r in rollups_second_branch) == { + datetime(1970, 1, 2, 0, 0, tzinfo=timezone.utc) + } + assert set(r.avg_duration_seconds for r in rollups_second_branch) == { + 3.6, + 0.002, + } + assert set(r.last_duration_seconds for r in rollups_second_branch) == { + 3.6, + 0.002, + } + + @pytest.mark.integration + def test_upload_processor_task_call_network( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + ): + tests = dbsession.query(Test).all() + test_instances = dbsession.query(TestInstance).all() + assert len(tests) == 0 + assert len(test_instances) == 0 + + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + with open( + here.parent.parent / "samples" / "sample_test_missing_network.json" + ) as f: + content = f.read() + mock_storage.write_file("archive", url, content) + upload = UploadFactory.create(storage_path=url) + dbsession.add(upload) + dbsession.flush() + redis_queue = [{"url": url, "upload_id": upload.id_}] + mocker.patch.object(TestResultsProcessorTask, "app", celery_app) + + commit = CommitFactory.create( + message="hello world", + commitid="cd76b0821854a780b60012aed85af0a8263004ad", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="joseph-sentry", + repository__owner__service="github", + repository__name="codecov-demo", + ) + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + result = TestResultsProcessorTask().run_impl( + dbsession, + previous_result=False, + repoid=upload.report.commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + arguments_list=redis_queue, + ) + expected_result = True + tests = dbsession.query(Test).all() + test_instances = dbsession.query(TestInstance).all() + failures = dbsession.query(TestInstance).filter_by(outcome="failure").all() + + assert len(tests) == 4 + assert len(test_instances) == 4 + assert len(failures) == 1 + + for test in tests: + assert test.framework == "Pytest" + assert test.computed_name.startswith( + "api/temp/calculator/test_calculator.py::" + ) + + assert ( + failures[0].failure_message.replace(" ", "").replace("\n", "") + == """deftest_divide():>assertCalculator.divide(1,2)==0.5Eassert1.0==0.5E+where1.0=(1,2)E+where=Calculator.divideapi/temp/calculator/test_calculator.py:30:AssertionError""" + ) + assert ( + failures[0].test.name + == "api.temp.calculator.test_calculator\x1ftest_divide" + ) + assert expected_result == result + assert commit.message == "hello world" + + assert mock_storage.read_file("archive", url).startswith( + b"""# path=codecov-demo/temp.junit.xml +""" + ) diff --git a/apps/worker/tasks/tests/unit/test_timeseries_backfill_commits.py b/apps/worker/tasks/tests/unit/test_timeseries_backfill_commits.py new file mode 100644 index 0000000000..ff05f5343d --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_timeseries_backfill_commits.py @@ -0,0 +1,76 @@ +from shared.celery_config import timeseries_save_commit_measurements_task_name + +from database.models import MeasurementName +from database.tests.factories import RepositoryFactory +from database.tests.factories.core import CommitFactory +from database.tests.factories.timeseries import DatasetFactory +from tasks.timeseries_backfill import TimeseriesBackfillCommitsTask + + +def test_backfill_commits_run_impl(dbsession, mocker): + mocker.patch("tasks.timeseries_backfill.is_timeseries_enabled", return_value=True) + mocked_app = mocker.patch.object( + TimeseriesBackfillCommitsTask, + "app", + tasks={ + timeseries_save_commit_measurements_task_name: mocker.MagicMock(), + }, + ) + + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + + commit1 = CommitFactory(repository=repository) + dbsession.add(commit1) + commit2 = CommitFactory(repository=repository) + dbsession.add(commit2) + + dataset = DatasetFactory.create( + name=MeasurementName.flag_coverage.value, + repository_id=repository.repoid, + ) + dbsession.add(dataset) + dbsession.flush() + + task = TimeseriesBackfillCommitsTask() + res = task.run_impl( + dbsession, + commit_ids=[commit1.id_, commit2.id_], + dataset_names=[dataset.name], + ) + assert res == {"successful": True} + + mocked_app.tasks[ + timeseries_save_commit_measurements_task_name + ].apply_async.assert_any_call( + kwargs={ + "commitid": commit1.commitid, + "repoid": commit1.repoid, + "dataset_names": [dataset.name], + } + ) + mocked_app.tasks[ + timeseries_save_commit_measurements_task_name + ].apply_async.assert_any_call( + kwargs={ + "commitid": commit2.commitid, + "repoid": commit2.repoid, + "dataset_names": [dataset.name], + } + ) + + +def test_backfill_commits_run_impl_timeseries_not_enabled(dbsession, mocker): + mocker.patch("tasks.timeseries_backfill.is_timeseries_enabled", return_value=False) + mock_group = mocker.patch("tasks.timeseries_backfill.group") + + task = TimeseriesBackfillCommitsTask() + res = task.run_impl( + dbsession, + commit_ids=[1, 2, 3], + dataset_names=["testing"], + ) + assert res == {"successful": False} + + assert not mock_group.called diff --git a/apps/worker/tasks/tests/unit/test_timeseries_backfill_dataset.py b/apps/worker/tasks/tests/unit/test_timeseries_backfill_dataset.py new file mode 100644 index 0000000000..b6ede3d726 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_timeseries_backfill_dataset.py @@ -0,0 +1,193 @@ +from datetime import datetime + +from database.models import MeasurementName +from database.tests.factories import RepositoryFactory +from database.tests.factories.core import CommitFactory +from database.tests.factories.timeseries import DatasetFactory +from tasks.timeseries_backfill import ( + TimeseriesBackfillDatasetTask, + timeseries_backfill_commits_task, +) + + +def test_backfill_dataset_run_impl(dbsession, mocker): + mocker.patch("tasks.timeseries_backfill.is_timeseries_enabled", return_value=True) + mock_group = mocker.patch("tasks.timeseries_backfill.group") + + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + + commit1 = CommitFactory( + repository=repository, timestamp=datetime(2022, 1, 1, 0, 0, 0) + ) + dbsession.add(commit1) + commit2 = CommitFactory( + repository=repository, timestamp=datetime(2022, 2, 1, 0, 0, 0) + ) + dbsession.add(commit2) + commit3 = CommitFactory( + repository=repository, timestamp=datetime(2022, 3, 1, 0, 0, 0) + ) + dbsession.add(commit3) + commit4 = CommitFactory( + repository=repository, timestamp=datetime(2022, 4, 1, 0, 0, 0) + ) + dbsession.add(commit4) + + dataset = DatasetFactory.create( + name=MeasurementName.flag_coverage.value, + repository_id=repository.repoid, + ) + dbsession.add(dataset) + dbsession.flush() + + task = TimeseriesBackfillDatasetTask() + res = task.run_impl( + dbsession, + dataset_id=dataset.id_, + start_date="2022-01-01T00:00:00", + end_date="2022-03-15T00:00:00", + batch_size=2, + ) + assert res == {"successful": True} + + expected_signatures = [ + timeseries_backfill_commits_task.signature( + kwargs=dict( + commit_ids=[commit3.id_, commit2.id_], + dataset_names=[dataset.name], + ), + ), + timeseries_backfill_commits_task.signature( + kwargs=dict( + commit_ids=[commit1.id_], + dataset_names=[dataset.name], + ), + ), + ] + mock_group.assert_called_once_with(expected_signatures) + + +def test_backfill_dataset_run_impl_invalid_dataset(dbsession, mocker): + mocker.patch("tasks.timeseries_backfill.is_timeseries_enabled", return_value=True) + mock_group = mocker.patch("tasks.timeseries_backfill.group") + + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + + dataset = DatasetFactory.create( + name=MeasurementName.flag_coverage.value, + repository_id=repository.repoid, + ) + dbsession.add(dataset) + dbsession.flush() + + task = TimeseriesBackfillDatasetTask() + res = task.run_impl( + dbsession, + dataset_id=9999, + start_date="2022-01-01T00:00:00", + end_date="2022-12-31T00:00:00", + ) + assert res == {"successful": False} + + assert not mock_group.called + + +def test_backfill_dataset_run_impl_invalid_repository(dbsession, mocker): + mocker.patch("tasks.timeseries_backfill.is_timeseries_enabled", return_value=True) + mock_group = mocker.patch("tasks.timeseries_backfill.group") + + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + + dataset = DatasetFactory.create( + name=MeasurementName.flag_coverage.value, + repository_id=9999, + ) + dbsession.add(dataset) + dbsession.flush() + + task = TimeseriesBackfillDatasetTask() + res = task.run_impl( + dbsession, + dataset_id=dataset.id_, + start_date="2022-01-01T00:00:00", + end_date="2022-12-31T00:00:00", + ) + assert res == {"successful": False} + + assert not mock_group.called + + +def test_backfill_dataset_run_impl_invalid_start_date(dbsession, mocker): + mocker.patch("tasks.timeseries_backfill.is_timeseries_enabled", return_value=True) + mock_group = mocker.patch("tasks.timeseries_backfill.group") + + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + + dataset = DatasetFactory.create( + name=MeasurementName.flag_coverage.value, + repository_id=repository.repoid, + ) + dbsession.add(dataset) + dbsession.flush() + + task = TimeseriesBackfillDatasetTask() + res = task.run_impl( + dbsession, + dataset_id=dataset.id_, + start_date="invalid", + end_date="2022-12-31T00:00:00", + ) + assert res == {"successful": False} + + assert not mock_group.called + + +def test_backfill_dataset_run_impl_invalid_end_date(dbsession, mocker): + mocker.patch("tasks.timeseries_backfill.is_timeseries_enabled", return_value=True) + mock_group = mocker.patch("tasks.timeseries_backfill.group") + + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + + dataset = DatasetFactory.create( + name=MeasurementName.flag_coverage.value, + repository_id=repository.repoid, + ) + dbsession.add(dataset) + dbsession.flush() + + task = TimeseriesBackfillDatasetTask() + res = task.run_impl( + dbsession, + dataset_id=dataset.id_, + start_date="2022-01-01T00:00:00", + end_date="invalid", + ) + assert res == {"successful": False} + + assert not mock_group.called + + +def test_backfill_dataset_run_impl_timeseries_not_enabled(dbsession, mocker): + mocker.patch("tasks.timeseries_backfill.is_timeseries_enabled", return_value=False) + mock_group = mocker.patch("tasks.timeseries_backfill.group") + + task = TimeseriesBackfillDatasetTask() + res = task.run_impl( + dbsession, + dataset_id=9999, + start_date="2022-01-01T00:00:00", + end_date="2022-12-31T00:00:00", + ) + assert res == {"successful": False} + + assert not mock_group.called diff --git a/apps/worker/tasks/tests/unit/test_timeseries_delete.py b/apps/worker/tasks/tests/unit/test_timeseries_delete.py new file mode 100644 index 0000000000..4bbfbcd06b --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_timeseries_delete.py @@ -0,0 +1,96 @@ +from database.models.timeseries import MeasurementName +from database.tests.factories import RepositoryFactory +from tasks.timeseries_delete import TimeseriesDeleteTask + + +def test_timeseries_delete_run_impl(dbsession, mocker): + mocker.patch("tasks.timeseries_delete.is_timeseries_enabled", return_value=True) + delete_repository_data = mocker.patch( + "tasks.timeseries_delete.delete_repository_data" + ) + + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + + task = TimeseriesDeleteTask() + res = task.run_impl( + dbsession, + repository_id=repository.repoid, + ) + assert res == {"successful": True} + + delete_repository_data.assert_called_once_with(repository) + + +def test_timeseries_delete_run_impl_invalid_repository(dbsession, mocker): + mocker.patch("tasks.timeseries_delete.is_timeseries_enabled", return_value=True) + + task = TimeseriesDeleteTask() + res = task.run_impl( + dbsession, + repository_id=9999, + ) + assert res == {"successful": False, "reason": "Repository not found"} + + +def test_timeseries_delete_run_impl_timeseries_not_enabled(dbsession, mocker): + mocker.patch("tasks.timeseries_delete.is_timeseries_enabled", return_value=False) + + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + + task = TimeseriesDeleteTask() + res = task.run_impl( + dbsession, + repository_id=repository.repoid, + ) + assert res == {"successful": False, "reason": "Timeseries not enabled"} + + +def test_timeseries_delete_measurements_only(dbsession, mocker): + mocker.patch("tasks.timeseries_delete.is_timeseries_enabled", return_value=True) + delete_repository_measurements = mocker.patch( + "tasks.timeseries_delete.delete_repository_measurements" + ) + + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + + task = TimeseriesDeleteTask() + res = task.run_impl( + dbsession, + repository_id=repository.repoid, + measurement_only=True, + measurement_type=MeasurementName.coverage.value, + measurement_id=f"{repository.repoid}", + ) + assert res == {"successful": True} + + delete_repository_measurements.assert_called_once() + + +def test_timeseries_delete_measurements_only_unsuccessful(dbsession, mocker): + mocker.patch("tasks.timeseries_delete.is_timeseries_enabled", return_value=True) + delete_repository_measurements = mocker.patch( + "tasks.timeseries_delete.delete_repository_measurements" + ) + + repository = RepositoryFactory.create() + dbsession.add(repository) + dbsession.flush() + + task = TimeseriesDeleteTask() + res = task.run_impl( + dbsession, + repository_id=repository.repoid, + measurement_only=True, + ) + assert res == { + "successful": False, + "reason": "Measurement type and ID required to delete measurements only", + } + + delete_repository_measurements.assert_not_called() diff --git a/apps/worker/tasks/tests/unit/test_trial_expiration.py b/apps/worker/tasks/tests/unit/test_trial_expiration.py new file mode 100644 index 0000000000..c5d3bee22f --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_trial_expiration.py @@ -0,0 +1,97 @@ +import pytest +from shared.django_apps.codecov_auth.tests.factories import ( + OwnerFactory, + PlanFactory, + TierFactory, +) +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName, TierName + +from database.enums import TrialStatus +from tasks.trial_expiration import TrialExpirationTask + + +@pytest.mark.django_db +class TestTrialExpiration(object): + @pytest.fixture(autouse=True) + def setup(self): + trial_tier = TierFactory(tier_name=TierName.TRIAL.value) + PlanFactory( + tier=trial_tier, + name=PlanName.TRIAL_PLAN_NAME.value, + paid_plan=False, + marketing_name="Developer", + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + ) + basic_tier = TierFactory(tier_name=TierName.BASIC.value) + PlanFactory( + name=DEFAULT_FREE_PLAN, + tier=basic_tier, + marketing_name="Developer", + benefits=[ + "Up to 1 user", + "Unlimited public repositories", + "Unlimited private repositories", + ], + monthly_uploads_limit=250, + ) + + def test_trial_expiration_task_with_pretrial_users_count(self, db, mocker): + """ + We used to save the plan_user_count as pretrial_users_count, and reinstate the original plan_user_count at the end of the trial. + We no longer do this - when we cancel a trial, we set_default_plan_data(), which sets plan_user_count as 1 + """ + owner = OwnerFactory( + pretrial_users_count=5, + plan=PlanName.TRIAL_PLAN_NAME.value, + trial_status=TrialStatus.ONGOING.value, + ) + + task = TrialExpirationTask() + assert task.run_impl(db, owner.ownerid) == {"successful": True} + + owner.refresh_from_db() + assert owner.plan == DEFAULT_FREE_PLAN + assert owner.plan_activated_users is None + assert owner.plan_user_count == 1 + assert owner.stripe_subscription_id is None + assert owner.trial_status == TrialStatus.EXPIRED.value + + def test_trial_expiration_task_without_pretrial_users_count(self, db, mocker): + owner = OwnerFactory( + plan=PlanName.TRIAL_PLAN_NAME.value, trial_status=TrialStatus.ONGOING.value + ) + + task = TrialExpirationTask() + assert task.run_impl(db, owner.ownerid) == {"successful": True} + + owner.refresh_from_db() + assert owner.plan == DEFAULT_FREE_PLAN + assert owner.plan_activated_users is None + assert owner.plan_user_count == 1 + assert owner.stripe_subscription_id is None + assert owner.trial_status == TrialStatus.EXPIRED.value + + def test_trial_expiration_task_with_trial_fired_by(self, db, mocker): + """ + We used to set the trial_fired_by owner as the only plan_activated_users as part of expiring the trial. + We no longer do this - when we cancel a trial, we set_default_plan_data(), which clears plan_activated_users. + """ + owner = OwnerFactory( + trial_fired_by=9, + plan=PlanName.TRIAL_PLAN_NAME.value, + trial_status=TrialStatus.ONGOING.value, + ) + + task = TrialExpirationTask() + assert task.run_impl(db, owner.ownerid) == {"successful": True} + + owner.refresh_from_db() + assert owner.plan == DEFAULT_FREE_PLAN + assert owner.plan_activated_users is None + assert owner.stripe_subscription_id is None + assert owner.trial_status == TrialStatus.EXPIRED.value diff --git a/apps/worker/tasks/tests/unit/test_trial_expiration_cron.py b/apps/worker/tasks/tests/unit/test_trial_expiration_cron.py new file mode 100644 index 0000000000..0b07ca041e --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_trial_expiration_cron.py @@ -0,0 +1,96 @@ +from datetime import datetime, timedelta +from unittest.mock import patch + +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName + +from celery_config import trial_expiration_task_name +from database.enums import TrialStatus +from database.tests.factories.core import OwnerFactory +from tasks.trial_expiration_cron import TrialExpirationCronTask + + +class TestTrialExpirationCheck(object): + @patch("tasks.trial_expiration_cron.yield_amount", 1) + def test_enqueue_trial_expiration_task(self, dbsession, mocker): + mocked_now = datetime(2023, 7, 3, 6, 8, 12) + mocker.patch( + "tasks.trial_expiration_cron.get_utc_now", + return_value=mocked_now, + ) + yesterday = mocked_now + timedelta(days=-1) + tomorrow = mocked_now + timedelta(days=10) + + ongoing_owner_that_should_expire = OwnerFactory.create( + username="one", + trial_status=TrialStatus.ONGOING.value, + trial_end_date=yesterday, + plan=PlanName.TRIAL_PLAN_NAME.value, + ) + second_ongoing_owner_that_should_expire = OwnerFactory.create( + username="two", + trial_status=TrialStatus.ONGOING.value, + trial_end_date=yesterday, + plan=PlanName.TRIAL_PLAN_NAME.value, + ) + # These are to represent other types of owners we could face that we should not see + ongoing_owner_that_should_not_expire = OwnerFactory.create( + username="three", + trial_status=TrialStatus.ONGOING.value, + trial_end_date=tomorrow, + plan=PlanName.TRIAL_PLAN_NAME.value, + ) + expired_basic_owner = OwnerFactory.create( + trial_status=TrialStatus.EXPIRED.value, + trial_end_date=yesterday, + plan=DEFAULT_FREE_PLAN, + ) + expired_paid_owner = OwnerFactory.create( + trial_status=TrialStatus.EXPIRED.value, + trial_end_date=yesterday, + plan=PlanName.CODECOV_PRO_MONTHLY.value, + ) + cannot_trial_paid_owner = OwnerFactory.create( + trial_status=TrialStatus.CANNOT_TRIAL.value, + trial_end_date=yesterday, + plan=PlanName.CODECOV_PRO_MONTHLY.value, + ) + not_started_basic_owner = OwnerFactory.create( + trial_status=TrialStatus.NOT_STARTED.value, + trial_end_date=None, + plan=DEFAULT_FREE_PLAN, + ) + dbsession.add(ongoing_owner_that_should_expire) + dbsession.add(second_ongoing_owner_that_should_expire) + dbsession.add(ongoing_owner_that_should_not_expire) + dbsession.add(expired_basic_owner) + dbsession.add(expired_paid_owner) + dbsession.add(cannot_trial_paid_owner) + dbsession.add(not_started_basic_owner) + dbsession.flush() + + mocked_app = mocker.patch.object( + TrialExpirationCronTask, + "app", + tasks={ + trial_expiration_task_name: mocker.MagicMock(), + }, + ) + task = TrialExpirationCronTask() + + assert task.run_cron_task(dbsession) == {"successful": True} + mocked_app.tasks[trial_expiration_task_name].apply_async.assert_any_call( + kwargs={"ownerid": second_ongoing_owner_that_should_expire.ownerid} + ) + mocked_app.tasks[trial_expiration_task_name].apply_async.assert_any_call( + kwargs={"ownerid": ongoing_owner_that_should_expire.ownerid} + ) + # TODO: couldn't find a way to assert I didn't call the other owners + + def test_get_min_seconds_interval_between_executions(self, dbsession): + assert isinstance( + TrialExpirationCronTask.get_min_seconds_interval_between_executions(), int + ) + # The specifics don't matter, but the number needs to be somewhat big + assert ( + TrialExpirationCronTask.get_min_seconds_interval_between_executions() > 600 + ) diff --git a/apps/worker/tasks/tests/unit/test_upload_finisher_task.py b/apps/worker/tasks/tests/unit/test_upload_finisher_task.py new file mode 100644 index 0000000000..6c11a27985 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_upload_finisher_task.py @@ -0,0 +1,696 @@ +from pathlib import Path +from unittest.mock import ANY + +import pytest +from celery.exceptions import Retry +from redis.exceptions import LockError +from shared.celery_config import timeseries_save_commit_measurements_task_name +from shared.torngit.exceptions import TorngitObjectNotFoundError +from shared.yaml import UserYaml + +from database.models.reports import CommitReport +from database.tests.factories import CommitFactory, PullFactory, RepositoryFactory +from database.tests.factories.core import UploadFactory +from database.tests.factories.timeseries import DatasetFactory +from helpers.checkpoint_logger import _kwargs_key +from helpers.checkpoint_logger.flows import UploadFlow +from helpers.exceptions import RepositoryWithoutValidBotError +from helpers.log_context import LogContext, set_log_context +from services.processing.merging import get_joined_flag, update_uploads +from services.processing.types import MergeResult, ProcessingResult +from services.timeseries import MeasurementName +from tasks.upload_finisher import ( + ReportService, + ShouldCallNotifyResult, + UploadFinisherTask, + load_commit_diff, +) + +here = Path(__file__) + + +def _start_upload_flow(mocker): + mocker.patch( + "helpers.checkpoint_logger._get_milli_timestamp", + side_effect=[1337, 9001, 10000, 15000, 20000, 25000], + ) + set_log_context(LogContext()) + UploadFlow.log(UploadFlow.UPLOAD_TASK_BEGIN) + UploadFlow.log(UploadFlow.PROCESSING_BEGIN) + UploadFlow.log(UploadFlow.INITIAL_PROCESSING_COMPLETE) + + +def test_load_commit_diff_no_diff(mock_configuration, dbsession, mock_repo_provider): + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + mock_repo_provider.get_commit_diff.side_effect = TorngitObjectNotFoundError( + "response", "message" + ) + diff = load_commit_diff(commit) + assert diff is None + + +def test_load_commit_diff_no_bot(mocker, mock_configuration, dbsession): + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + mock_get_repo_service = mocker.patch( + "tasks.upload_finisher.get_repo_provider_service" + ) + mock_get_repo_service.side_effect = RepositoryWithoutValidBotError() + diff = load_commit_diff(commit) + assert diff is None + + +def test_mark_uploads_as_failed(dbsession): + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + report = CommitReport(commit_id=commit.id_) + dbsession.add(report) + dbsession.flush() + upload_1 = UploadFactory.create(report=report, state="started", storage_path="url") + upload_2 = UploadFactory.create(report=report, state="started", storage_path="url2") + dbsession.add(upload_1) + dbsession.add(upload_2) + dbsession.flush() + + results: list[ProcessingResult] = [ + { + "upload_id": upload_1.id, + "successful": False, + "error": {"code": "report_empty", "params": {}}, + }, + { + "upload_id": upload_2.id, + "successful": False, + "error": {"code": "report_expired", "params": {}}, + }, + ] + + update_uploads(dbsession, UserYaml({}), results, [], MergeResult({}, set())) + dbsession.expire_all() + + assert upload_1.state == "error" + assert len(upload_1.errors) == 1 + assert upload_1.errors[0].error_code == "report_empty" + assert upload_1.errors[0].error_params == {} + assert upload_1.errors[0].report_upload == upload_1 + + assert upload_2.state == "error" + assert len(upload_2.errors) == 1 + assert upload_2.errors[0].error_code == "report_expired" + assert upload_2.errors[0].error_params == {} + assert upload_2.errors[0].report_upload == upload_2 + + +@pytest.mark.parametrize( + "flag, joined", + [("nightly", False), ("unittests", True), ("ui", True), ("other", True)], +) +def test_not_joined_flag(flag, joined): + yaml = UserYaml( + { + "flags": { + "nightly": {"joined": False}, + "unittests": {"joined": True}, + "ui": {"paths": ["ui/"]}, + } + } + ) + assert get_joined_flag(yaml, [flag]) == joined + + +class TestUploadFinisherTask(object): + @pytest.mark.django_db(databases={"default"}) + def test_upload_finisher_task_call( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_checkpoint_submit, + mock_repo_provider, + ): + mocker.patch("tasks.upload_finisher.load_intermediate_reports", return_value=[]) + mocker.patch("tasks.upload_finisher.update_uploads") + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + mocked_3 = mocker.patch.object( + UploadFinisherTask, "app", conf=mocker.MagicMock(task_time_limit=123) + ) + mocked_3.send_task.return_value = True + + commit = CommitFactory.create( + message="dsidsahdsahdsa", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + branch="thisbranch", + ci_passed=True, + repository__branch="thisbranch", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__owner__service="github", + author__service="github", + notified=True, + repository__yaml={ + "codecov": {"max_report_age": "1y ago"} + }, # Sorry, this is a timebomb now + ) + dbsession.add(commit) + dbsession.flush() + previous_results = [ + {"upload_id": 0, "arguments": {"url": url}, "successful": True} + ] + + _start_upload_flow(mocker) + result = UploadFinisherTask().run_impl( + dbsession, + previous_results, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + ) + + assert result == {"notifications_called": True} + dbsession.refresh(commit) + assert commit.message == "dsidsahdsahdsa" + + mock_checkpoint_submit.assert_any_call( + "batch_processing_duration", + UploadFlow.INITIAL_PROCESSING_COMPLETE, + UploadFlow.BATCH_PROCESSING_COMPLETE, + data={ + UploadFlow.UPLOAD_TASK_BEGIN: 1337, + UploadFlow.PROCESSING_BEGIN: 9001, + UploadFlow.INITIAL_PROCESSING_COMPLETE: 10000, + UploadFlow.BATCH_PROCESSING_COMPLETE: 15000, + UploadFlow.PROCESSING_COMPLETE: 20000, + }, + ) + mock_checkpoint_submit.assert_any_call( + "total_processing_duration", + UploadFlow.PROCESSING_BEGIN, + UploadFlow.PROCESSING_COMPLETE, + data={ + UploadFlow.UPLOAD_TASK_BEGIN: 1337, + UploadFlow.PROCESSING_BEGIN: 9001, + UploadFlow.INITIAL_PROCESSING_COMPLETE: 10000, + UploadFlow.BATCH_PROCESSING_COMPLETE: 15000, + UploadFlow.PROCESSING_COMPLETE: 20000, + }, + ) + + @pytest.mark.django_db(databases={"default"}) + def test_upload_finisher_task_call_no_author( + self, mocker, mock_configuration, dbsession, mock_storage, mock_repo_provider + ): + mocker.patch("tasks.upload_finisher.load_intermediate_reports", return_value=[]) + mocker.patch("tasks.upload_finisher.update_uploads") + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + mocked_3 = mocker.patch.object( + UploadFinisherTask, "app", conf=mocker.MagicMock(task_time_limit=123) + ) + mock_finish_reports_processing = mocker.patch.object( + UploadFinisherTask, "finish_reports_processing" + ) + mock_finish_reports_processing.return_value = {"notifications_called": True} + mocked_3.send_task.return_value = True + + commit = CommitFactory.create( + message="dsidsahdsahdsa", + author=None, + branch="thisbranch", + ci_passed=True, + repository__branch="thisbranch", + repository__owner__username="ThiagoCodecov", + repository__yaml={ + "codecov": {"max_report_age": "1y ago"} + }, # Sorry, this is a timebomb now + ) + dbsession.add(commit) + dbsession.flush() + previous_results = [ + {"upload_id": 0, "arguments": {"url": url}, "successful": True} + ] + result = UploadFinisherTask().run_impl( + dbsession, + previous_results, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + ) + expected_result = {"notifications_called": True} + assert expected_result == result + dbsession.refresh(commit) + assert commit.message == "dsidsahdsahdsa" + + @pytest.mark.django_db + def test_upload_finisher_task_call_different_branch( + self, mocker, mock_configuration, dbsession, mock_storage, mock_repo_provider + ): + mocker.patch("tasks.upload_finisher.load_intermediate_reports", return_value=[]) + mocker.patch("tasks.upload_finisher.update_uploads") + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + mocked_3 = mocker.patch.object( + UploadFinisherTask, "app", conf=mocker.MagicMock(task_time_limit=123) + ) + mock_finish_reports_processing = mocker.patch.object( + UploadFinisherTask, "finish_reports_processing" + ) + mock_finish_reports_processing.return_value = {"notifications_called": True} + mocked_3.send_task.return_value = True + + commit = CommitFactory.create( + message="dsidsahdsahdsa", + branch="other_branch", + ci_passed=True, + repository__branch="thisbranch", + repository__owner__username="ThiagoCodecov", + repository__yaml={ + "codecov": {"max_report_age": "1y ago"} + }, # Sorry, this is a timebomb now + ) + dbsession.add(commit) + dbsession.flush() + previous_results = [ + {"upload_id": 0, "arguments": {"url": url}, "successful": True} + ] + result = UploadFinisherTask().run_impl( + dbsession, + previous_results, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + ) + expected_result = {"notifications_called": True} + assert expected_result == result + dbsession.refresh(commit) + assert commit.message == "dsidsahdsahdsa" + + def test_should_call_notifications(self, dbsession): + commit_yaml = {"codecov": {"max_report_age": "1y ago"}} + commit = CommitFactory.create( + message="dsidsahdsahdsa", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__yaml=commit_yaml, + ) + dbsession.add(commit) + dbsession.flush() + + assert ( + UploadFinisherTask().should_call_notifications( + commit, + commit_yaml, + [{"arguments": {"url": "url"}, "successful": True}], + None, + ) + == ShouldCallNotifyResult.NOTIFY + ) + + def test_should_call_notifications_local_upload(self, dbsession): + commit_yaml = {"codecov": {"max_report_age": "1y ago"}} + commit = CommitFactory.create( + message="dsidsahdsahdsa", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__yaml=commit_yaml, + ) + dbsession.add(commit) + dbsession.flush() + + assert ( + UploadFinisherTask().should_call_notifications( + commit, commit_yaml, [], "local_report1" + ) + == ShouldCallNotifyResult.DO_NOT_NOTIFY + ) + + def test_should_call_notifications_manual_trigger(self, dbsession): + commit_yaml = {"codecov": {"notify": {"manual_trigger": True}}} + commit = CommitFactory.create( + message="dsidsahdsahdsa", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="aabbcc", + repository__owner__username="Codecov", + repository__yaml=commit_yaml, + ) + dbsession.add(commit) + dbsession.flush() + + assert ( + UploadFinisherTask().should_call_notifications( + commit, commit_yaml, [], None + ) + == ShouldCallNotifyResult.DO_NOT_NOTIFY + ) + + def test_should_call_notifications_manual_trigger_off(self, dbsession): + commit_yaml = { + "codecov": {"max_report_age": "1y ago", "notify": {"manual_trigger": False}} + } + commit = CommitFactory.create( + message="dsidsahdsahdsa", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__yaml=commit_yaml, + ) + dbsession.add(commit) + dbsession.flush() + + assert ( + UploadFinisherTask().should_call_notifications( + commit, + commit_yaml, + [{"arguments": {"url": "url"}, "successful": True}], + None, + ) + == ShouldCallNotifyResult.NOTIFY + ) + + @pytest.mark.parametrize( + "notify_error,result", + [ + (True, ShouldCallNotifyResult.NOTIFY_ERROR), + (False, ShouldCallNotifyResult.DO_NOT_NOTIFY), + ], + ) + def test_should_call_notifications_no_successful_reports( + self, dbsession, notify_error, result + ): + commit_yaml = { + "codecov": { + "max_report_age": "1y ago", + "notify": {"notify_error": notify_error}, + } + } + commit = CommitFactory.create( + message="dsidsahdsahdsa", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__yaml=commit_yaml, + ) + dbsession.add(commit) + dbsession.flush() + + assert ( + UploadFinisherTask().should_call_notifications( + commit, + commit_yaml, + 12 * [{"arguments": {"url": "url"}, "successful": False}], + None, + ) + == result + ) + + def test_should_call_notifications_not_enough_builds(self, dbsession, mocker): + commit_yaml = {"codecov": {"notify": {"after_n_builds": 9}}} + commit = CommitFactory.create( + message="dsidsahdsahdsa", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__yaml=commit_yaml, + ) + dbsession.add(commit) + + mocked_report = mocker.patch.object( + ReportService, "get_existing_report_for_commit" + ) + mocked_report.return_value = mocker.MagicMock( + sessions=[mocker.MagicMock()] * 8 + ) # 8 sessions + + assert ( + UploadFinisherTask().should_call_notifications( + commit, + commit_yaml, + 9 * [{"arguments": {"url": "url"}, "successful": True}], + None, + ) + == ShouldCallNotifyResult.DO_NOT_NOTIFY + ) + + def test_should_call_notifications_more_than_enough_builds(self, dbsession, mocker): + commit_yaml = {"codecov": {"notify": {"after_n_builds": 9}}} + commit = CommitFactory.create( + message="dsidsahdsahdsa", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__yaml=commit_yaml, + ) + dbsession.add(commit) + + mocked_report = mocker.patch.object( + ReportService, "get_existing_report_for_commit" + ) + mocked_report.return_value = mocker.MagicMock( + sessions=[mocker.MagicMock()] * 10 + ) # 10 sessions + + assert ( + UploadFinisherTask().should_call_notifications( + commit, + commit_yaml, + 2 * [{"arguments": {"url": "url"}, "successful": True}], + None, + ) + == ShouldCallNotifyResult.NOTIFY + ) + + def test_finish_reports_processing(self, dbsession, mocker): + commit_yaml = {} + mocked_app = mocker.patch.object(UploadFinisherTask, "app") + commit = CommitFactory.create( + message="dsidsahdsahdsa", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__yaml=commit_yaml, + ) + dbsession.add(commit) + dbsession.flush() + + _start_upload_flow(mocker) + res = UploadFinisherTask().finish_reports_processing( + dbsession, + commit, + UserYaml(commit_yaml), + [{"successful": True}], + None, + ) + assert res == {"notifications_called": True} + mocked_app.tasks["app.tasks.notify.Notify"].apply_async.assert_called_with( + kwargs={ + "commitid": commit.commitid, + "current_yaml": commit_yaml, + "repoid": commit.repoid, + _kwargs_key(UploadFlow): ANY, + }, + ) + assert mocked_app.send_task.call_count == 0 + + def test_finish_reports_processing_with_pull(self, dbsession, mocker): + commit_yaml = {} + mocked_app = mocker.patch.object( + UploadFinisherTask, + "app", + tasks={ + "app.tasks.notify.Notify": mocker.MagicMock(), + "app.tasks.pulls.Sync": mocker.MagicMock(), + "app.tasks.compute_comparison.ComputeComparison": mocker.MagicMock(), + "app.tasks.upload.UploadCleanLabelsIndex": mocker.MagicMock(), + }, + ) + repository = RepositoryFactory.create( + owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + owner__username="ThiagoCodecov", + yaml=commit_yaml, + ) + pull = PullFactory.create(repository=repository) + + dbsession.add(repository) + dbsession.add(pull) + dbsession.flush() + + compared_to = CommitFactory.create(repository=repository) + pull.compared_to = compared_to.commitid + commit = CommitFactory.create( + message="dsidsahdsahdsa", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository=repository, + pullid=pull.pullid, + ) + dbsession.add(commit) + dbsession.add(compared_to) + dbsession.flush() + + _start_upload_flow(mocker) + res = UploadFinisherTask().finish_reports_processing( + dbsession, + commit, + UserYaml(commit_yaml), + [{"successful": True}], + None, + ) + assert res == {"notifications_called": True} + mocked_app.tasks["app.tasks.notify.Notify"].apply_async.assert_called_with( + kwargs={ + "commitid": commit.commitid, + "current_yaml": commit_yaml, + "repoid": commit.repoid, + _kwargs_key(UploadFlow): ANY, + }, + ) + mocked_app.tasks["app.tasks.pulls.Sync"].apply_async.assert_called_with( + kwargs={ + "pullid": pull.pullid, + "repoid": pull.repoid, + "should_send_notifications": False, + } + ) + assert mocked_app.send_task.call_count == 0 + + mocked_app.tasks[ + "app.tasks.compute_comparison.ComputeComparison" + ].apply_async.assert_called_once() + mocked_app.tasks[ + "app.tasks.upload.UploadCleanLabelsIndex" + ].apply_async.assert_not_called() + + @pytest.mark.parametrize( + "notify_error", + [True, False], + ) + def test_finish_reports_processing_no_notification( + self, dbsession, mocker, notify_error + ): + commit_yaml = {"codecov": {"notify": {"notify_error": notify_error}}} + mocked_app = mocker.patch.object( + UploadFinisherTask, + "app", + tasks={ + "app.tasks.notify.NotifyErrorTask": mocker.MagicMock(), + "app.tasks.notify.Notify": mocker.MagicMock(), + }, + ) + commit = CommitFactory.create( + message="dsidsahdsahdsa", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__yaml=commit_yaml, + ) + dbsession.add(commit) + dbsession.flush() + + _start_upload_flow(mocker) + res = UploadFinisherTask().finish_reports_processing( + dbsession, + commit, + UserYaml(commit_yaml), + [{"successful": False}], + None, + ) + assert res == {"notifications_called": False} + if notify_error: + assert mocked_app.send_task.call_count == 0 + mocked_app.tasks[ + "app.tasks.notify.NotifyErrorTask" + ].apply_async.assert_called_once() + mocked_app.tasks["app.tasks.notify.Notify"].apply_async.assert_not_called() + else: + assert mocked_app.send_task.call_count == 0 + mocked_app.tasks[ + "app.tasks.notify.NotifyErrorTask" + ].apply_async.assert_not_called() + mocked_app.tasks["app.tasks.notify.Notify"].apply_async.assert_not_called() + + @pytest.mark.django_db + def test_upload_finisher_task_calls_save_commit_measurements_task( + self, mocker, dbsession, mock_storage, mock_repo_provider + ): + mocker.patch("tasks.upload_finisher.load_intermediate_reports", return_value=[]) + mocker.patch("tasks.upload_finisher.update_uploads") + + mocker.patch("tasks.upload_finisher.is_timeseries_enabled", return_value=True) + mocked_app = mocker.patch.object( + UploadFinisherTask, + "app", + tasks={ + timeseries_save_commit_measurements_task_name: mocker.MagicMock(), + "app.tasks.notify.Notify": mocker.MagicMock(), + }, + conf=mocker.MagicMock(task_time_limit=123), + ) + + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + + mocker.patch( + "tasks.upload_finisher.repository_datasets_query", + return_value=[ + DatasetFactory.create( + repository_id=commit.repository.repoid, + name=MeasurementName.coverage.value, + ), + DatasetFactory.create( + repository_id=commit.repository.repoid, + name=MeasurementName.flag_coverage.value, + ), + DatasetFactory.create( + repository_id=commit.repository.repoid, + name=MeasurementName.component_coverage.value, + ), + ], + ) + + previous_results = [{"upload_id": 0, "arguments": {}, "successful": True}] + UploadFinisherTask().run_impl( + dbsession, + previous_results, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + ) + + mocked_app.tasks[ + timeseries_save_commit_measurements_task_name + ].apply_async.assert_called_once_with( + kwargs={ + "commitid": commit.commitid, + "repoid": commit.repoid, + "dataset_names": [ + MeasurementName.coverage.value, + MeasurementName.flag_coverage.value, + MeasurementName.component_coverage.value, + ], + } + ) + + @pytest.mark.django_db() + def test_retry_on_report_lock(self, dbsession, mock_redis): + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + + mock_redis.lock.side_effect = LockError() + + task = UploadFinisherTask() + task.request.retries = 0 + + with pytest.raises(Retry): + task.run_impl( + dbsession, + [{"upload_id": 0, "successful": True, "arguments": {}}], + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + ) diff --git a/apps/worker/tasks/tests/unit/test_upload_processing_task.py b/apps/worker/tasks/tests/unit/test_upload_processing_task.py new file mode 100644 index 0000000000..4e428816b0 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_upload_processing_task.py @@ -0,0 +1,819 @@ +from pathlib import Path + +import celery +import pytest +from celery.exceptions import Retry +from shared.config import get_config +from shared.reports.reportfile import ReportFile +from shared.reports.resources import Report +from shared.reports.types import ReportLine, ReportTotals +from shared.storage.exceptions import FileNotInStorageError +from shared.upload.constants import UploadErrorCode +from shared.yaml import UserYaml + +from database.models import CommitReport +from database.tests.factories import CommitFactory, UploadFactory +from helpers.exceptions import ReportEmptyError, ReportExpiredException +from services.archive import ArchiveService +from services.processing.processing import process_upload +from services.report import ProcessingError, RawReportInfo, ReportService +from services.report.parser.legacy import LegacyReportParser +from tasks.upload_processor import UploadProcessorTask + +here = Path(__file__) + + +def test_default_acks_late() -> None: + task = UploadProcessorTask() + # task.acks_late is defined at import time, so it's difficult to test + # This test ensures that, in the absence of config the default is False + # So we need to explicitly set acks_late + assert get_config("setup", "tasks", "upload", "acks_late", default=None) is None + assert task.acks_late == False + + +class TestUploadProcessorTask(object): + @pytest.mark.integration + @pytest.mark.django_db(databases={"default"}) + def test_upload_processor_task_call( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + celery_app, + ): + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + with open(here.parent.parent / "samples" / "sample_uploaded_report_1.txt") as f: + content = f.read() + mock_storage.write_file("archive", url, content) + upload = UploadFactory.create(storage_path=url) + dbsession.add(upload) + dbsession.flush() + mocker.patch.object(UploadProcessorTask, "app", celery_app) + + commit = CommitFactory.create( + message="dsidsahdsahdsa", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__owner__service="github", + repository__name="example-python", + ) + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + result = UploadProcessorTask().run_impl( + dbsession, + {}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + arguments={"url": url, "upload_id": upload.id_}, + ) + + assert result == { + "upload_id": upload.id_, + "arguments": {"upload_id": upload.id_, "url": url}, + "successful": True, + } + + @pytest.mark.integration + @pytest.mark.django_db + def test_upload_processor_task_call_should_delete( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + celery_app, + ): + mock_configuration.set_params( + {"services": {"minio": {"expire_raw_after_n_days": True}}} + ) + mock_delete_file = mocker.patch.object(ArchiveService, "delete_file") + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7F/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + with open(here.parent.parent / "samples" / "sample_uploaded_report_1.txt") as f: + content = f.read() + mock_storage.write_file("archive", url, content) + upload = UploadFactory.create(storage_path=url) + dbsession.add(upload) + dbsession.flush() + mocker.patch.object(UploadProcessorTask, "app", celery_app) + + commit = CommitFactory.create( + message="dsidsahdsahdsa", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__owner__service="github", + repository__name="example-python", + ) + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + result = UploadProcessorTask().run_impl( + dbsession, + {}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": False}}, + arguments={"url": url, "upload_id": upload.id_}, + ) + + mock_delete_file.assert_called() + assert ( + upload.storage_path + == "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7F/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + ) + assert result == { + "upload_id": upload.id_, + "arguments": {"upload_id": upload.id_, "url": url}, + "successful": True, + } + + @pytest.mark.django_db + def test_upload_processor_call_with_upload_obj( + self, mocker, dbsession, mock_storage + ): + commit = CommitFactory.create( + message="dsidsahdsahdsa", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + author__service="github", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__service="github", + repository__owner__username="ThiagoCodecov", + repository__name="example-python", + ) + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + upload = UploadFactory.create( + report=current_report_row, state="started", storage_path=url + ) + dbsession.add(upload) + dbsession.flush() + with open( + here.parent.parent / "samples" / "sample_uploaded_report_1.txt", "rb" + ) as f: + content = f.read() + mock_storage.write_file("archive", url, content) + result = process_upload( + lambda _e: None, + db_session=dbsession, + repo_id=commit.repoid, + commit_sha=commit.commitid, + commit_yaml=UserYaml({"codecov": {"max_report_age": False}}), + arguments={"url": url, "upload_id": upload.id_}, + ) + + assert result == { + "upload_id": upload.id_, + "arguments": {"url": url, "upload_id": upload.id_}, + "successful": True, + } + + # storage is overwritten with parsed contents + data = mock_storage.read_file("archive", url) + parsed = LegacyReportParser().parse_raw_report_from_bytes(content) + assert data == parsed.content().getvalue() + + @pytest.mark.django_db(databases={"default"}) + def test_upload_task_call_exception_within_individual_upload( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + celery_app, + ): + commit = CommitFactory.create( + message="", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__yaml={"codecov": {"max_report_age": False}}, + ) + dbsession.add(commit) + dbsession.flush() + upload = UploadFactory.create( + report__commit=commit, state="started", storage_path="url" + ) + dbsession.add(upload) + dbsession.flush() + + mocker.patch( + "services.report.process_raw_upload", + side_effect=Exception("first", "aruba", "digimon"), + ) + mocker.patch.object( + ReportService, + "parse_raw_report_from_storage", + return_value="ParsedRawReport()", + ) + + mocked_post_process = mocker.patch( + "services.processing.processing.rewrite_or_delete_upload" + ) + + result = UploadProcessorTask().run_impl( + dbsession, + {}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + arguments={"url": "url", "upload_id": upload.id_}, + ) + + assert result == { + "upload_id": upload.id_, + "arguments": {"upload_id": upload.id, "url": "url"}, + "successful": False, + "error": { + "code": UploadErrorCode.UNKNOWN_PROCESSING, + "params": {"location": "url"}, + }, + } + + mocked_post_process.assert_called_with( + mocker.ANY, + mocker.ANY, + RawReportInfo( + raw_report="ParsedRawReport()", + archive_url="url", + upload=upload.external_id, + error=ProcessingError( + code=UploadErrorCode.UNKNOWN_PROCESSING, + params={"location": "url"}, + is_retryable=False, + ), + ), + ) + + @pytest.mark.django_db(databases={"default"}) + def test_upload_task_call_with_expired_report( + self, + mocker, + mock_configuration, + dbsession, + mock_repo_provider, + mock_storage, + celery_app, + ): + mocked_1 = mocker.patch.object(ArchiveService, "read_chunks") + mocked_1.return_value = None + mocker.patch.object(ArchiveService, "read_file", return_value=b"") + mocked_2 = mocker.patch("services.report.process_raw_upload") + false_report = Report() + false_report_file = ReportFile("file.c") + false_report_file.append(18, ReportLine.create(1, [])) + false_report.append(false_report_file) + mocked_2.side_effect = [ + false_report, + ReportExpiredException(), + ] + # Mocking retry to also raise the exception so we can see how it is called + mocker.patch.object(UploadProcessorTask, "app", celery_app) + commit = CommitFactory.create( + message="", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__yaml={ + "codecov": {"max_report_age": "1y ago"} + }, # Sorry for the timebomb + ) + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + upload_1 = UploadFactory.create( + report=current_report_row, state="started", storage_path="url" + ) + upload_2 = UploadFactory.create( + report=current_report_row, state="started", storage_path="url2" + ) + dbsession.add(upload_1) + dbsession.add(upload_2) + dbsession.flush() + + result = UploadProcessorTask().run_impl( + dbsession, + {}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + arguments={"url": "url", "what": "huh", "upload_id": upload_1.id_}, + ) + + assert result == { + "upload_id": upload_1.id_, + "arguments": {"url": "url", "what": "huh", "upload_id": upload_1.id_}, + "successful": True, + } + + result = UploadProcessorTask().run_impl( + dbsession, + {}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + arguments={"url": "url2", "extra_param": 45, "upload_id": upload_2.id_}, + ) + + assert result == { + "upload_id": upload_2.id_, + "arguments": { + "extra_param": 45, + "url": "url2", + "upload_id": upload_2.id_, + }, + "successful": False, + "error": {"code": "report_expired", "params": {}}, + } + + assert commit.state == "complete" + + def test_upload_task_process_individual_report_with_notfound_report( + self, + mocker, + mock_configuration, + dbsession, + mock_repo_provider, + mock_storage, + ): + mocked_1 = mocker.patch.object(ArchiveService, "read_chunks") + mocked_1.return_value = None + # Mocking retry to also raise the exception so we can see how it is called + mocked_4 = mocker.patch.object(UploadProcessorTask, "app") + mocked_4.send_task.return_value = True + commit = CommitFactory.create( + message="", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__yaml={"codecov": {"max_report_age": False}}, + ) + dbsession.add(commit) + dbsession.flush() + upload = UploadFactory.create( + report__commit=commit, storage_path="locationlocation" + ) + dbsession.add(upload) + dbsession.flush() + + result = process_upload( + lambda error: None, + dbsession, + commit.repoid, + commit.commitid, + UserYaml({"codecov": {"max_report_age": False}}), + {"upload_id": upload.id_}, + ) + + assert result == { + "upload_id": upload.id_, + "arguments": {"upload_id": upload.id_}, + "successful": False, + "error": { + "code": "file_not_in_storage", + "params": {"location": "locationlocation"}, + }, + } + + assert commit.state == "complete" + + def test_upload_task_process_individual_report_with_notfound_report_no_retries_yet( + self, dbsession, mocker + ): + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + upload = UploadFactory.create(report__commit=commit) + dbsession.add(upload) + dbsession.flush() + + # throw an error thats retryable: + mocker.patch.object( + ReportService, + "parse_raw_report_from_storage", + side_effect=FileNotInStorageError(), + ) + + def on_error(_error): + raise Retry() + + with pytest.raises(Retry): + process_upload( + on_error, + dbsession, + commit.repoid, + commit.commitid, + UserYaml({}), + {"upload_id": upload.id_}, + ) + + @pytest.mark.django_db(databases={"default"}) + def test_upload_task_call_with_empty_report( + self, + mocker, + mock_configuration, + dbsession, + mock_repo_provider, + mock_storage, + celery_app, + ): + mocked_1 = mocker.patch.object(ArchiveService, "read_chunks") + mocked_1.return_value = None + mocker.patch.object(ArchiveService, "read_file", return_value=b"") + mocked_2 = mocker.patch("services.report.process_raw_upload") + false_report = Report() + false_report_file = ReportFile("file.c") + false_report_file.append(18, ReportLine.create(1, [])) + false_report.append(false_report_file) + mocked_2.side_effect = [ + false_report, + ReportEmptyError(), + ] + mocker.patch.object(UploadProcessorTask, "app", celery_app) + commit = CommitFactory.create( + message="", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__yaml={ + "codecov": {"max_report_age": "1y ago"} + }, # Sorry for the timebomb + ) + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + upload_1 = UploadFactory.create( + report=current_report_row, state="started", storage_path="url" + ) + upload_2 = UploadFactory.create( + report=current_report_row, state="started", storage_path="url2" + ) + dbsession.add(upload_1) + dbsession.add(upload_2) + dbsession.flush() + + result = UploadProcessorTask().run_impl( + dbsession, + {}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + arguments={"url": "url", "what": "huh", "upload_id": upload_1.id_}, + ) + assert result == { + "upload_id": upload_1.id_, + "arguments": {"url": "url", "what": "huh", "upload_id": upload_1.id_}, + "successful": True, + } + + result = UploadProcessorTask().run_impl( + dbsession, + {}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + arguments={"url": "url2", "extra_param": 45, "upload_id": upload_2.id_}, + ) + assert result == { + "upload_id": upload_2.id_, + "arguments": { + "extra_param": 45, + "url": "url2", + "upload_id": upload_2.id_, + }, + "successful": False, + "error": {"code": "report_empty", "params": {}}, + } + + assert commit.state == "complete" + + @pytest.mark.django_db(databases={"default"}) + def test_upload_task_call_no_successful_report( + self, + mocker, + mock_configuration, + dbsession, + mock_repo_provider, + mock_storage, + celery_app, + ): + mocked_1 = mocker.patch.object(ArchiveService, "read_chunks") + mocked_1.return_value = None + mocked_2 = mocker.patch("services.report.process_raw_upload") + mocked_2.side_effect = [ReportEmptyError(), ReportExpiredException()] + mocker.patch.object(ArchiveService, "read_file", return_value=b"") + # Mocking retry to also raise the exception so we can see how it is called + mocker.patch.object(UploadProcessorTask, "app", celery_app) + commit = CommitFactory.create( + message="", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__yaml={ + "codecov": {"max_report_age": "1y ago"} + }, # Sorry for the timebomb + ) + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + upload_1 = UploadFactory.create( + report=current_report_row, state="started", storage_path="url" + ) + upload_2 = UploadFactory.create( + report=current_report_row, state="started", storage_path="url2" + ) + dbsession.add(upload_1) + dbsession.add(upload_2) + dbsession.flush() + + result = UploadProcessorTask().run_impl( + dbsession, + {}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + arguments={"url": "url", "what": "huh", "upload_id": upload_1.id_}, + ) + + assert result == { + "upload_id": upload_1.id_, + "arguments": {"url": "url", "what": "huh", "upload_id": upload_1.id_}, + "successful": False, + "error": {"code": "report_empty", "params": {}}, + } + + result = UploadProcessorTask().run_impl( + dbsession, + {}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + arguments={"url": "url2", "extra_param": 45, "upload_id": upload_2.id_}, + ) + + assert result == { + "upload_id": upload_2.id_, + "arguments": { + "extra_param": 45, + "url": "url2", + "upload_id": upload_2.id_, + }, + "successful": False, + "error": {"code": "report_expired", "params": {}}, + } + + @pytest.mark.django_db(databases={"default"}) + def test_upload_task_call_softtimelimit( + self, + mocker, + mock_configuration, + dbsession, + mock_repo_provider, + mock_storage, + celery_app, + ): + mocked_1 = mocker.patch.object(ArchiveService, "read_chunks") + mocked_1.return_value = None + mocked_2 = mocker.patch.object(ReportService, "build_report_from_raw_content") + mocked_2.side_effect = celery.exceptions.SoftTimeLimitExceeded("banana") + # Mocking retry to also raise the exception so we can see how it is called + mocker.patch.object(UploadProcessorTask, "app", celery_app) + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + upload_1 = UploadFactory.create( + report=current_report_row, state="started", storage_path="url" + ) + dbsession.add(upload_1) + dbsession.flush() + with pytest.raises(celery.exceptions.SoftTimeLimitExceeded, match="banana"): + UploadProcessorTask().run_impl( + dbsession, + {}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + arguments={"url": "url", "what": "huh", "upload_id": upload_1.id_}, + ) + assert commit.state == "error" + + @pytest.mark.django_db(databases={"default"}) + def test_upload_task_call_celeryerror( + self, + mocker, + mock_configuration, + dbsession, + mock_repo_provider, + mock_storage, + celery_app, + ): + mocked_1 = mocker.patch.object(ArchiveService, "read_chunks") + mocked_1.return_value = None + mocked_2 = mocker.patch.object(ReportService, "build_report_from_raw_content") + mocked_2.side_effect = celery.exceptions.Retry("banana") + # Mocking retry to also raise the exception so we can see how it is called + mocker.patch.object(UploadProcessorTask, "app", celery_app) + commit = CommitFactory.create(state="pending") + dbsession.add(commit) + dbsession.flush() + current_report_row = CommitReport(commit_id=commit.id_) + dbsession.add(current_report_row) + dbsession.flush() + upload_1 = UploadFactory.create( + report=current_report_row, state="started", storage_path="url" + ) + dbsession.add(upload_1) + dbsession.flush() + with pytest.raises(celery.exceptions.Retry, match="banana"): + UploadProcessorTask().run_impl( + dbsession, + {}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={}, + arguments={"url": "url", "what": "huh", "upload_id": upload_1.id_}, + ) + assert commit.state == "pending" + + def test_save_report_apply_diff_not_there( + self, mocker, mock_configuration, dbsession, mock_storage + ): + commit = CommitFactory.create( + message="", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__yaml={ + "codecov": {"max_report_age": "1y ago"} + }, # Sorry for the timebomb + ) + dbsession.add(commit) + dbsession.flush() + report = Report() + report_file_1 = ReportFile("path/to/first.py") + report_file_2 = ReportFile("to/second/path.py") + report_line_1 = ReportLine.create(coverage=1, sessions=[[0, 1]]) + report_line_2 = ReportLine.create(coverage=0, sessions=[[0, 0]]) + report_line_3 = ReportLine.create(coverage=1, sessions=[[0, 1]]) + report_file_1.append(10, report_line_1) + report_file_1.append(12, report_line_2) + report_file_2.append(12, report_line_3) + report.append(report_file_1) + report.append(report_file_2) + chunks_archive_service = ArchiveService(commit.repository) + result = ReportService({}).save_report(commit, report) + + assert result == { + "url": f"v4/repos/{chunks_archive_service.storage_hash}/commits/{commit.commitid}/chunks.txt" + } + assert report.diff_totals is None + + def test_save_report_apply_diff_valid( + self, mocker, mock_configuration, dbsession, mock_storage + ): + commit = CommitFactory.create( + message="", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__yaml={ + "codecov": {"max_report_age": "1y ago"} + }, # Sorry for the timebomb + ) + dbsession.add(commit) + dbsession.flush() + report = Report() + report_file_1 = ReportFile("path/to/first.py") + report_file_2 = ReportFile("to/second/path.py") + report_line_1 = ReportLine.create(coverage=1, sessions=[[0, 1]]) + report_line_2 = ReportLine.create(coverage=0, sessions=[[0, 0]]) + report_line_3 = ReportLine.create(coverage=1, sessions=[[0, 1]]) + report_file_1.append(10, report_line_1) + report_file_1.append(12, report_line_2) + report_file_2.append(12, report_line_3) + report.append(report_file_1) + report.append(report_file_2) + chunks_archive_service = ArchiveService(commit.repository) + diff = { + "files": { + "path/to/first.py": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["9", "3", "9", "5"], + "lines": [ + "+sudo: false", + "+", + " language: python", + " ", + " python:", + ], + } + ], + "stats": {"added": 2, "removed": 0}, + } + } + } + report.apply_diff(diff) + result = ReportService({}).save_report(commit, report) + + assert result == { + "url": f"v4/repos/{chunks_archive_service.storage_hash}/commits/{commit.commitid}/chunks.txt" + } + assert report.diff_totals == ReportTotals( + files=1, + lines=1, + hits=1, + misses=0, + partials=0, + coverage="100", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + + def test_save_report_empty_report( + self, mocker, mock_configuration, dbsession, mock_storage + ): + commit = CommitFactory.create( + message="", + repository__owner__unencrypted_oauth_token="testulk3d54rlhxkjyzomq2wh8b7np47xabcrkx8", + repository__owner__username="ThiagoCodecov", + repository__yaml={ + "codecov": {"max_report_age": "1y ago"} + }, # Sorry for the timebomb + ) + dbsession.add(commit) + dbsession.flush() + report = Report() + chunks_archive_service = ArchiveService(commit.repository) + diff = { + "files": { + "path/to/first.py": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["9", "3", "9", "5"], + "lines": [ + "+sudo: false", + "+", + " language: python", + " ", + " python:", + ], + } + ], + "stats": {"added": 2, "removed": 0}, + } + } + } + report.apply_diff(diff) + result = ReportService({}).save_report(commit, report) + + assert result == { + "url": f"v4/repos/{chunks_archive_service.storage_hash}/commits/{commit.commitid}/chunks.txt" + } + assert report.diff_totals == ReportTotals( + files=0, + lines=0, + hits=0, + misses=0, + partials=0, + coverage=None, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=None, + complexity_total=None, + diff=0, + ) + assert commit.state == "error" diff --git a/apps/worker/tasks/tests/unit/test_upload_task.py b/apps/worker/tasks/tests/unit/test_upload_task.py new file mode 100644 index 0000000000..5e6ae8b0f7 --- /dev/null +++ b/apps/worker/tasks/tests/unit/test_upload_task.py @@ -0,0 +1,1620 @@ +import json +import uuid +from datetime import datetime, timedelta, timezone +from pathlib import Path + +import mock +import pytest +from celery.exceptions import Retry +from mock import AsyncMock, call +from redis.exceptions import LockError +from shared.helpers.redis import get_redis_connection +from shared.reports.enums import UploadState, UploadType +from shared.torngit import GitlabEnterprise +from shared.torngit.exceptions import TorngitClientError, TorngitRepoNotFoundError +from shared.torngit.gitlab import Gitlab +from shared.utils.sessions import SessionType + +from database.enums import ReportType +from database.models import Upload +from database.models.core import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, +) +from database.models.reports import CommitReport +from database.tests.factories import CommitFactory, OwnerFactory, RepositoryFactory +from database.tests.factories.core import ReportFactory +from helpers.checkpoint_logger import _kwargs_key +from helpers.checkpoint_logger.flows import TestResultsFlow, UploadFlow +from helpers.exceptions import RepositoryWithoutValidBotError +from helpers.log_context import LogContext, set_log_context +from services.report import NotReadyToBuildReportYetError, ReportService +from tasks.bundle_analysis_notify import bundle_analysis_notify_task +from tasks.bundle_analysis_processor import bundle_analysis_processor_task +from tasks.test_results_finisher import test_results_finisher_task +from tasks.test_results_processor import test_results_processor_task +from tasks.upload import UploadContext, UploadTask +from tasks.upload_finisher import upload_finisher_task +from tasks.upload_processor import upload_processor_task + +here = Path(__file__) + + +def _start_upload_flow(mocker): + mocker.patch( + "helpers.checkpoint_logger._get_milli_timestamp", + side_effect=[1337, 9001, 10000, 15000, 20000, 25000], + ) + set_log_context(LogContext()) + UploadFlow.log(UploadFlow.UPLOAD_TASK_BEGIN) + + +class FakeRedis(object): + """ + This is a fake, very rudimentary redis implementation to ease the managing + of mocking `exists`, `lpop` and whatnot in the context of Upload jobs + """ + + def __init__(self, mocker): + self.lists = {} + self.keys = {} + self.lock = mocker.MagicMock() + self.delete = mocker.MagicMock() + self.sismember = mocker.MagicMock() + self.hdel = mocker.MagicMock() + + def exists(self, key): + if self.lists.get(key): + return True + if self.keys.get(key) is not None: + return True + return False + + def get(self, key): + res = None + if self.keys.get(key) is not None: + res = self.keys.get(key) + if self.lists.get(key): + res = self.lists.get(key) + if res is None: + return None + if not isinstance(res, (str, bytes)): + return str(res).encode() + if not isinstance(res, bytes): + return res.encode() + return res + + def lpop(self, key, count=None): + list = self.lists.get(key) + if not list: + return None + + res = None + if count: + res = [] + for _ in range(count): + res.append(list.pop(0)) + if list == []: + del self.lists[key] + break + else: + res = list.pop(0) + if list == []: + del self.lists[key] + + return res + + def delete(self, key): + del self.lists[key] + + +@pytest.fixture +def mock_redis(mocker): + m = mocker.patch("shared.helpers.redis._get_redis_instance_from_url") + redis_server = FakeRedis(mocker) + m.return_value = redis_server + yield redis_server + + +@pytest.fixture(scope="function", autouse=True) +def clear_log_context(mocker): + set_log_context(LogContext()) + + +@pytest.mark.integration +class TestUploadTaskIntegration(object): + @pytest.mark.django_db(databases={"default"}, transaction=True) + def test_upload_task_call( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + celery_app, + mock_checkpoint_submit, + ): + _start_upload_flow(mocker) + mocked_chord = mocker.patch("tasks.upload.chord") + url = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + mocker.patch.object(UploadTask, "app", celery_app) + + commit = CommitFactory.create( + message="", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="ThiagoCodecov", + repository__owner__service="github", + repository__yaml={"codecov": {"max_report_age": "1y ago"}}, + repository__name="example-python", + pullid=1, + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + repository__owner__createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + ) + dbsession.add(commit) + dbsession.flush() + dbsession.refresh(commit) + repo_updatestamp = commit.repository.updatestamp + redis = get_redis_connection() + redis.lpush( + f"uploads/{commit.repoid}/{commit.commitid}", + json.dumps({"url": url, "build": "some_random_build"}), + ) + kwargs = UploadFlow.save_to_kwargs({}) + result = UploadTask().run_impl( + dbsession, + commit.repoid, + commit.commitid, + kwargs=kwargs, + ) + expected_result = {"was_setup": False, "was_updated": True} + assert expected_result == result + assert commit.message == "dsidsahdsahdsa" + assert commit.parent_commit_id is None + assert commit.report is not None + sessions = commit.report.uploads + assert len(sessions) == 1 + first_session = ( + dbsession.query(Upload) + .filter_by(report_id=commit.report.id, build_code="some_random_build") + .first() + ) + processor = upload_processor_task.s( + repoid=commit.repoid, + commitid="abf6d4df662c47e32460020ab14abf9303581429", + commit_yaml={"codecov": {"max_report_age": "1y ago"}}, + arguments={ + "url": url, + "flags": [], + "build": "some_random_build", + "upload_id": first_session.id, + "upload_pk": first_session.id, + }, + ) + kwargs = dict( + repoid=commit.repoid, + commitid="abf6d4df662c47e32460020ab14abf9303581429", + commit_yaml={"codecov": {"max_report_age": "1y ago"}}, + report_code=None, + ) + kwargs[_kwargs_key(UploadFlow)] = mocker.ANY + finisher = upload_finisher_task.signature(kwargs=kwargs) + mocked_chord.assert_called_with([processor], finisher) + calls = [ + mock.call( + "time_before_processing", + UploadFlow.UPLOAD_TASK_BEGIN, + UploadFlow.PROCESSING_BEGIN, + data={ + UploadFlow.UPLOAD_TASK_BEGIN: 1337, + UploadFlow.PROCESSING_BEGIN: 9001, + UploadFlow.INITIAL_PROCESSING_COMPLETE: 10000, + }, + ), + mock.call( + "initial_processing_duration", + UploadFlow.PROCESSING_BEGIN, + UploadFlow.INITIAL_PROCESSING_COMPLETE, + data={ + UploadFlow.UPLOAD_TASK_BEGIN: 1337, + UploadFlow.PROCESSING_BEGIN: 9001, + UploadFlow.INITIAL_PROCESSING_COMPLETE: 10000, + }, + ), + ] + mock_checkpoint_submit.assert_has_calls(calls) + + @pytest.mark.django_db(databases={"default"}, transaction=True) + def test_upload_task_call_bundle_analysis( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + ): + chain = mocker.patch("tasks.upload.chain") + storage_path = ( + "v1/repos/testing/ed1bdd67-8fd2-4cdb-ac9e-39b99e4a3892/bundle_report.sqlite" + ) + redis_queue = [{"url": storage_path, "build_code": "some_random_build"}] + jsonified_redis_queue = [json.dumps(x) for x in redis_queue] + mocker.patch.object(UploadTask, "app", celery_app) + + commit = CommitFactory.create( + message="", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__oauth_token="GHTZB+Mi+kbl/ubudnSKTJYb/fgN4hRJVJYSIErtidEsCLDJBb8DZzkbXqLujHAnv28aKShXddE/OffwRuwKug==", + repository__owner__username="ThiagoCodecov", + repository__owner__service="github", + repository__yaml={"codecov": {"max_report_age": "1y ago"}}, + repository__name="example-python", + pullid=1, + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + repository__owner__createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + ) + dbsession.add(commit) + dbsession.flush() + dbsession.refresh(commit) + + mock_redis.lists[ + f"uploads/{commit.repoid}/{commit.commitid}/bundle_analysis" + ] = jsonified_redis_queue + + UploadTask().run_impl( + dbsession, + commit.repoid, + commit.commitid, + report_type="bundle_analysis", + ) + commit_report = commit.commit_report(report_type=ReportType.BUNDLE_ANALYSIS) + assert commit_report + uploads = commit_report.uploads + assert len(uploads) == 1 + upload = dbsession.query(Upload).filter_by(report_id=commit_report.id).first() + processor_sig = bundle_analysis_processor_task.s( + {}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": "1y ago"}}, + params={ + "url": storage_path, + "flags": [], + "build_code": "some_random_build", + "upload_id": upload.id, + "upload_pk": upload.id, + }, + ) + notify_sig = bundle_analysis_notify_task.s( + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": "1y ago"}}, + ) + chain.assert_called_with([processor_sig, notify_sig]) + + @pytest.mark.django_db(databases={"default"}, transaction=True) + def test_upload_task_call_bundle_analysis_no_upload( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + ): + chain = mocker.patch("tasks.upload.chain") + storage_path = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + redis_queue = [{"url": storage_path, "build_code": "some_random_build"}] + jsonified_redis_queue = [json.dumps(x) for x in redis_queue] + mocker.patch.object(UploadTask, "app", celery_app) + + commit = CommitFactory.create( + message="", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__oauth_token="GHTZB+Mi+kbl/ubudnSKTJYb/fgN4hRJVJYSIErtidEsCLDJBb8DZzkbXqLujHAnv28aKShXddE/OffwRuwKug==", + repository__owner__username="ThiagoCodecov", + repository__owner__service="github", + repository__yaml={"codecov": {"max_report_age": "1y ago"}}, + repository__name="example-python", + pullid=1, + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + repository__owner__createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + ) + dbsession.add(commit) + dbsession.flush() + dbsession.refresh(commit) + + repository = commit.repository + repository.bundle_analysis_enabled = True + dbsession.add(repository) + dbsession.flush() + dbsession.refresh(repository) + + mock_redis.lists[f"uploads/{commit.repoid}/{commit.commitid}/test_results"] = ( + jsonified_redis_queue + ) + + UploadTask().run_impl( + dbsession, + commit.repoid, + commit.commitid, + report_type="test_results", + ) + + commit_report = commit.commit_report(report_type=ReportType.BUNDLE_ANALYSIS) + assert commit_report is None + + processor_sig = bundle_analysis_processor_task.s( + {}, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": "1y ago"}}, + params={ + "url": storage_path, + "flags": [], + "build_code": "some_random_build", + "upload_pk": None, + }, + ) + assert call([processor_sig]) in chain.mock_calls + + @pytest.mark.django_db(databases={"default"}, transaction=True) + def test_upload_task_call_test_results( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + ): + chain = mocker.patch("tasks.upload.chain") + storage_path = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + redis_queue = [{"url": storage_path, "build_code": "some_random_build"}] + jsonified_redis_queue = [json.dumps(x) for x in redis_queue] + mocker.patch.object(UploadTask, "app", celery_app) + + commit = CommitFactory.create( + message="", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__oauth_token="GHTZB+Mi+kbl/ubudnSKTJYb/fgN4hRJVJYSIErtidEsCLDJBb8DZzkbXqLujHAnv28aKShXddE/OffwRuwKug==", + repository__owner__username="ThiagoCodecov", + repository__owner__service="github", + repository__yaml={"codecov": {"max_report_age": "1y ago"}}, + repository__name="example-python", + pullid=1, + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + repository__owner__createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + ) + dbsession.add(commit) + dbsession.flush() + dbsession.refresh(commit) + + mock_redis.lists[f"uploads/{commit.repoid}/{commit.commitid}/test_results"] = ( + jsonified_redis_queue + ) + + UploadTask().run_impl( + dbsession, + commit.repoid, + commit.commitid, + report_type="test_results", + ) + commit_report = commit.commit_report(report_type=ReportType.TEST_RESULTS) + assert commit_report + uploads = commit_report.uploads + assert len(uploads) == 1 + upload = dbsession.query(Upload).filter_by(report_id=commit_report.id).first() + processor_sig = test_results_processor_task.s( + False, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": "1y ago"}}, + arguments_list=[ + { + "url": storage_path, + "flags": [], + "build_code": "some_random_build", + "upload_id": upload.id, + "upload_pk": upload.id, + } + ], + report_code=None, + impl_type="old", + ) + kwargs = dict( + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": "1y ago"}}, + checkpoints_TestResultsFlow=None, + impl_type="old", + ) + + kwargs[_kwargs_key(TestResultsFlow)] = mocker.ANY + notify_sig = test_results_finisher_task.signature(kwargs=kwargs) + chain.assert_called_with(*[processor_sig, notify_sig]) + + @pytest.mark.django_db(databases={"default"}, transaction=True) + def test_upload_task_call_new_ta_tasks( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + ): + chain = mocker.patch("tasks.upload.chain") + _ = mocker.patch("tasks.upload.NEW_TA_TASKS.check_value", return_value="both") + storage_path = "v4/raw/2019-05-22/C3C4715CA57C910D11D5EB899FC86A7E/4c4e4654ac25037ae869caeb3619d485970b6304/a84d445c-9c1e-434f-8275-f18f1f320f81.txt" + redis_queue = [{"url": storage_path, "build_code": "some_random_build"}] + jsonified_redis_queue = [json.dumps(x) for x in redis_queue] + mocker.patch.object(UploadTask, "app", celery_app) + + mock_repo_provider_service = AsyncMock() + mock_repo_provider_service.get_commit.return_value = { + "author": { + "id": "123", + "username": "456", + "email": "789", + "name": "101", + }, + "message": "hello world", + "parents": [], + "timestamp": str(datetime.now()), + } + mock_repo_provider_service.get_ancestors_tree.return_value = {"parents": []} + mock_repo_provider_service.get_pull_request.return_value = { + "head": {"branch": "main"}, + "base": {}, + } + mock_repo_provider_service.list_top_level_files.return_value = [ + {"name": "codecov.yml", "path": "codecov.yml"} + ] + mock_repo_provider_service.get_source.return_value = { + "content": """ + codecov: + max_report_age: 1y ago + """ + } + + mocker.patch( + "tasks.base.get_repo_provider_service", + return_value=mock_repo_provider_service, + ) + mocker.patch("tasks.upload.hasattr", return_value=False) + commit = CommitFactory.create( + message="", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__oauth_token="GHTZB+Mi+kbl/ubudnSKTJYb/fgN4hRJVJYSIErtidEsCLDJBb8DZzkbXqLujHAnv28aKShXddE/OffwRuwKug==", + repository__owner__username="ThiagoCodecov", + repository__owner__service="github", + repository__yaml={"codecov": {"max_report_age": "1y ago"}}, + repository__name="example-python", + pullid=1, + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + repository__owner__createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + branch="main", + ) + dbsession.add(commit) + dbsession.flush() + dbsession.refresh(commit) + + mock_redis.lists[f"uploads/{commit.repoid}/{commit.commitid}/test_results"] = ( + jsonified_redis_queue + ) + + UploadTask().run_impl( + dbsession, + commit.repoid, + commit.commitid, + report_type="test_results", + ) + commit_report = commit.commit_report(report_type=ReportType.TEST_RESULTS) + assert commit_report + uploads = commit_report.uploads + assert len(uploads) == 1 + upload = dbsession.query(Upload).filter_by(report_id=commit_report.id).first() + processor_sig = test_results_processor_task.s( + False, + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": "1y ago"}}, + arguments_list=[ + { + "url": storage_path, + "flags": [], + "build_code": "some_random_build", + "upload_id": upload.id, + "upload_pk": upload.id, + } + ], + report_code=None, + impl_type="both", + ) + kwargs = dict( + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml={"codecov": {"max_report_age": "1y ago"}}, + checkpoints_TestResultsFlow=None, + impl_type="both", + ) + + kwargs[_kwargs_key(TestResultsFlow)] = mocker.ANY + notify_sig = test_results_finisher_task.signature(kwargs=kwargs) + chain.assert_has_calls([call(processor_sig, notify_sig)], any_order=True) + + def test_upload_task_call_no_jobs( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + ): + mocker.patch.object(UploadTask, "app", celery_app) + + commit = CommitFactory.create( + parent_commit_id=None, + message="", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="ThiagoCodecov", + repository__yaml={"codecov": {"max_report_age": "1y ago"}}, + repository__name="example-python", + pullid=1, + ) + dbsession.add(commit) + dbsession.flush() + mock_redis.lists[f"uploads/{commit.repoid}/{commit.commitid}"] = [] + result = UploadTask().run_impl(dbsession, commit.repoid, commit.commitid) + expected_result = { + "was_setup": False, + "was_updated": False, + "tasks_were_scheduled": False, + } + assert expected_result == result + assert commit.message == "" + assert commit.parent_commit_id is None + + @pytest.mark.django_db(databases={"default"}, transaction=True) + def test_upload_task_upload_processing_delay_not_enough_delay( + self, + mocker, + mock_configuration, + dbsession, + mock_storage, + celery_app, + ): + mock_possibly_update_commit_from_provider_info = mocker.patch( + "tasks.upload.possibly_update_commit_from_provider_info", return_value=True + ) + mocker.patch.object(UploadTask, "possibly_setup_webhooks", return_value=True) + mock_configuration.set_params({"setup": {"upload_processing_delay": 1000}}) + mocker.patch.object(UploadTask, "app", celery_app) + + commit = CommitFactory.create( + parent_commit_id=None, + message="", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="ThiagoCodecov", + repository__yaml={"codecov": {"max_report_age": "1y ago"}}, + repository__name="example-python", + pullid=1, + ) + dbsession.add(commit) + dbsession.flush() + + redis = get_redis_connection() + redis.lpush( + f"uploads/{commit.repoid}/{commit.commitid}", + '{"build": "part1", "url": "someurl1"}', + ) + redis.lpush( + f"uploads/{commit.repoid}/{commit.commitid}", + '{"build": "part2", "url": "someurl2"}', + ) + redis.set( + f"latest_upload/{commit.repoid}/{commit.commitid}", + (datetime.now() - timedelta(seconds=10)).timestamp(), + ) + + with pytest.raises(Retry): + UploadTask().run_impl(dbsession, commit.repoid, commit.commitid) + + assert commit.message == "" + assert commit.parent_commit_id is None + assert redis.exists(f"uploads/{commit.repoid}/{commit.commitid}") + assert not mock_possibly_update_commit_from_provider_info.called + + @pytest.mark.django_db(databases={"default"}, transaction=True) + def test_upload_task_upload_processing_delay_enough_delay( + self, + mocker, + mock_configuration, + dbsession, + mock_storage, + celery_app, + ): + mocker.patch( + "tasks.upload.possibly_update_commit_from_provider_info", return_value=True + ) + mocker.patch.object(UploadTask, "possibly_setup_webhooks", return_value=True) + commit = CommitFactory.create( + parent_commit_id=None, + message="", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="ThiagoCodecov", + repository__yaml={"codecov": {"max_report_age": "1y ago"}}, + repository__name="example-python", + pullid=1, + ) + mock_configuration.set_params({"setup": {"upload_processing_delay": 1000}}) + mocker.patch.object(UploadTask, "app", celery_app) + mocker.patch.object(UploadTask, "possibly_setup_webhooks", return_value=True) + mocked_chord = mocker.patch("tasks.upload.chord") + dbsession.add(commit) + dbsession.flush() + + redis = get_redis_connection() + redis.set( + f"latest_upload/{commit.repoid}/{commit.commitid}", + (datetime.now() - timedelta(seconds=1200)).timestamp(), + ) + redis.lpush( + f"uploads/{commit.repoid}/{commit.commitid}", + '{"build": "part1", "url": "someurl1"}', + ) + redis.lpush( + f"uploads/{commit.repoid}/{commit.commitid}", + '{"build": "part2", "url": "someurl2"}', + ) + + result = UploadTask().run_impl(dbsession, commit.repoid, commit.commitid) + + assert result == {"was_setup": True, "was_updated": True} + assert commit.message == "" + assert commit.parent_commit_id is None + assert not redis.exists(f"uploads/{commit.repoid}/{commit.commitid}") + mocked_chord.assert_called_with([mocker.ANY, mocker.ANY], mocker.ANY) + + @pytest.mark.django_db(databases={"default"}, transaction=True) + @pytest.mark.skip(reason="Bitbucket down is breaking this test") + def test_upload_task_upload_processing_delay_upload_is_none( + self, + mocker, + mock_configuration, + dbsession, + mock_storage, + celery_app, + ): + mock_configuration.set_params({"setup": {"upload_processing_delay": 1000}}) + mocker.patch.object(UploadTask, "app", celery_app) + mocker.patch( + "tasks.upload.possibly_update_commit_from_provider_info", return_value=True + ) + mocker.patch.object(UploadTask, "possibly_setup_webhooks", return_value=True) + mocked_chord = mocker.patch("tasks.upload.chord") + commit = CommitFactory.create( + parent_commit_id=None, + message="", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="ThiagoCodecov", + repository__yaml={"codecov": {"max_report_age": "1y ago"}}, + repository__name="example-python", + pullid=1, + ) + dbsession.add(commit) + dbsession.flush() + + redis = get_redis_connection() + redis.lpush( + f"uploads/{commit.repoid}/{commit.commitid}", + '{"build": "part1", "url": "someurl1"}', + ) + redis.lpush( + f"uploads/{commit.repoid}/{commit.commitid}", + '{"build": "part2", "url": "someurl2"}', + ) + + result = UploadTask().run_impl(dbsession, commit.repoid, commit.commitid) + + assert result == {"was_setup": True, "was_updated": True} + assert commit.message == "" + assert commit.parent_commit_id is None + assert not redis.exists(f"uploads/{commit.repoid}/{commit.commitid}") + mocked_chord.assert_called_with([mocker.ANY, mocker.ANY], mocker.ANY) + + @pytest.mark.django_db(databases={"default"}, transaction=True) + def test_upload_task_call_multiple_processors( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + celery_app, + ): + mocked_chord = mocker.patch("tasks.upload.chord") + mocker.patch.object(UploadTask, "app", celery_app) + + commit = CommitFactory.create( + message="", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="ThiagoCodecov", + repository__owner__service="github", + repository__yaml={"codecov": {"max_report_age": "1y ago"}}, + repository__name="example-python", + pullid=1, + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + repository__owner__createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + ) + dbsession.add(commit) + dbsession.flush() + + redis = get_redis_connection() + + redis_queue = [ + {"build": "part1", "url": "someurl1"}, + {"build": "part2", "url": "someurl2"}, + {"build": "part3", "url": "someurl3"}, + {"build": "part4", "url": "someurl4"}, + {"build": "part5", "url": "someurl5"}, + {"build": "part6", "url": "someurl6"}, + {"build": "part7", "url": "someurl7"}, + {"build": "part8", "url": "someurl8"}, + ] + for arguments in redis_queue: + redis.lpush( + f"uploads/{commit.repoid}/{commit.commitid}", json.dumps(arguments) + ) + + result = UploadTask().run_impl(dbsession, commit.repoid, commit.commitid) + + assert result == {"was_setup": False, "was_updated": True} + assert commit.message == "dsidsahdsahdsa" + assert commit.parent_commit_id is None + processors = [ + upload_processor_task.s( + repoid=commit.repoid, + commitid="abf6d4df662c47e32460020ab14abf9303581429", + commit_yaml={"codecov": {"max_report_age": "1y ago"}}, + arguments={ + **arguments, + "flags": [], + "upload_id": mocker.ANY, + "upload_pk": mocker.ANY, + }, + ) + for arguments in redis_queue + ] + processors.reverse() # whatever the reason + kwargs = dict( + repoid=commit.repoid, + commitid="abf6d4df662c47e32460020ab14abf9303581429", + commit_yaml={"codecov": {"max_report_age": "1y ago"}}, + report_code=None, + ) + kwargs[_kwargs_key(UploadFlow)] = mocker.ANY + t_final = upload_finisher_task.signature(kwargs=kwargs) + mocked_chord.assert_called_with(processors, t_final) + + def test_upload_task_proper_parent( + self, + mocker, + mock_configuration, + dbsession, + codecov_vcr, + mock_storage, + mock_redis, + celery_app, + ): + mocked_1 = mocker.patch("tasks.upload.chain") + mocker.patch.object(UploadTask, "app", celery_app) + mocked_3 = mocker.patch.object(UploadContext, "arguments_list", return_value=[]) + + owner = OwnerFactory.create( + service="github", + username="ThiagoCodecov", + unencrypted_oauth_token="test76zow6xgh7modd88noxr245j2z25t4ustoff", + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + ) + dbsession.add(owner) + + repo = RepositoryFactory.create( + owner=owner, + yaml={"codecov": {"max_report_age": "1y ago"}}, + name="example-python", + ) + dbsession.add(repo) + + parent_commit = CommitFactory.create( + message="", + commitid="c5b67303452bbff57cc1f49984339cde39eb1db5", + repository=repo, + pullid=1, + ) + + commit = CommitFactory.create( + message="", + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository=repo, + pullid=1, + ) + dbsession.add(parent_commit) + dbsession.add(commit) + dbsession.flush() + redis_queue = [{"build": "part1"}] + jsonified_redis_queue = [json.dumps(x) for x in redis_queue] + mock_redis.lists[f"uploads/{commit.repoid}/{commit.commitid}"] = ( + jsonified_redis_queue + ) + + result = UploadTask().run_impl(dbsession, commit.repoid, commit.commitid) + expected_result = {"was_setup": False, "was_updated": True} + assert expected_result == result + assert commit.message == "dsidsahdsahdsa" + assert commit.parent_commit_id == "c5b67303452bbff57cc1f49984339cde39eb1db5" + assert not mocked_1.called + mock_redis.lock.assert_any_call( + f"upload_lock_{commit.repoid}_{commit.commitid}", + blocking_timeout=5, + timeout=300, + ) + + @pytest.mark.django_db(databases={"default"}, transaction=True) + def test_upload_task_no_bot( + self, + mocker, + mock_configuration, + dbsession, + mock_redis, + mock_storage, + celery_app, + ): + mocked_schedule_task = mocker.patch.object(UploadTask, "schedule_task") + mocker.patch.object(UploadTask, "app", celery_app) + mocked_fetch_yaml = mocker.patch( + "services.repository.fetch_commit_yaml_and_possibly_store" + ) + redis_queue = [ + {"build": "part1", "url": "url1"}, + {"build": "part2", "url": "url2"}, + ] + jsonified_redis_queue = [json.dumps(x) for x in redis_queue] + mock_get_repo_service = mocker.patch("tasks.base.get_repo_provider_service") + mock_get_repo_service.side_effect = RepositoryWithoutValidBotError() + commit = CommitFactory.create( + message="", + parent_commit_id=None, + commitid="abf6d4df662c47e32460020ab14abf9303581429", + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="ThiagoCodecov", + repository__yaml={"codecov": {"max_report_age": "764y ago"}}, + repository__name="example-python", + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + repository__owner__createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + ) + dbsession.add(commit) + dbsession.flush() + mock_redis.lists[f"uploads/{commit.repoid}/{commit.commitid}"] = ( + jsonified_redis_queue + ) + result = UploadTask().run_impl(dbsession, commit.repoid, commit.commitid) + expected_result = {"was_setup": False, "was_updated": False} + assert expected_result == result + assert commit.message == "" + assert commit.parent_commit_id is None + mocked_schedule_task.assert_called_with( + commit, + {"codecov": {"max_report_age": "764y ago"}}, + [ + { + "build": "part1", + "url": "url1", + "flags": [], + "upload_id": mocker.ANY, + "upload_pk": mocker.ANY, + }, + { + "build": "part2", + "url": "url2", + "flags": [], + "upload_id": mocker.ANY, + "upload_pk": mocker.ANY, + }, + ], + commit.report, + mocker.ANY, + ) + assert not mocked_fetch_yaml.called + + @pytest.mark.django_db(databases={"default"}, transaction=True) + def test_upload_task_bot_no_permissions( + self, + mocker, + mock_configuration, + dbsession, + mock_redis, + mock_storage, + celery_app, + ): + mocked_schedule_task = mocker.patch.object(UploadTask, "schedule_task") + mocker.patch.object(UploadTask, "app", celery_app) + mocked_fetch_yaml = mocker.patch( + "services.repository.fetch_commit_yaml_and_possibly_store" + ) + redis_queue = [ + {"build": "part1", "url": "url1"}, + {"build": "part2", "url": "url2"}, + ] + jsonified_redis_queue = [json.dumps(x) for x in redis_queue] + mock_get_repo_service = mocker.patch("tasks.base.get_repo_provider_service") + mock_get_repo_service.side_effect = TorngitRepoNotFoundError( + "fake_response", "message" + ) + commit = CommitFactory.create( + message="", + parent_commit_id=None, + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="ThiagoCodecov", + repository__yaml={"codecov": {"max_report_age": "764y ago"}}, + repository__name="example-python", + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + repository__owner__createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + ) + dbsession.add(commit) + dbsession.flush() + mock_redis.lists[f"uploads/{commit.repoid}/{commit.commitid}"] = ( + jsonified_redis_queue + ) + result = UploadTask().run_impl(dbsession, commit.repoid, commit.commitid) + expected_result = {"was_setup": False, "was_updated": False} + assert expected_result == result + assert commit.message == "" + assert commit.parent_commit_id is None + mocked_schedule_task.assert_called_with( + commit, + {"codecov": {"max_report_age": "764y ago"}}, + [ + { + "build": "part1", + "url": "url1", + "flags": [], + "upload_id": mocker.ANY, + "upload_pk": mocker.ANY, + }, + { + "build": "part2", + "url": "url2", + "flags": [], + "upload_id": mocker.ANY, + "upload_pk": mocker.ANY, + }, + ], + commit.report, + mocker.ANY, + ) + assert not mocked_fetch_yaml.called + + @pytest.mark.django_db(databases={"default"}, transaction=True) + def test_upload_task_bot_unauthorized( + self, + mocker, + mock_configuration, + dbsession, + mock_redis, + mock_repo_provider, + mock_storage, + ): + mocked_schedule_task = mocker.patch.object(UploadTask, "schedule_task") + mock_app = mocker.patch.object(UploadTask, "app") + mock_app.send_task.return_value = True + redis_queue = [ + {"build": "part1", "url": "url1"}, + {"build": "part2", "url": "url2"}, + ] + jsonified_redis_queue = [json.dumps(x) for x in redis_queue] + mock_repo_provider.get_commit.side_effect = TorngitClientError( + 401, "response", "message" + ) + mock_repo_provider.list_top_level_files.side_effect = TorngitClientError( + 401, "response", "message" + ) + commit = CommitFactory.create( + message="", + parent_commit_id=None, + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="ThiagoCodecov", + repository__yaml={"codecov": {"max_report_age": "764y ago"}}, + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + repository__owner__createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + ) + mock_repo_provider.data = dict(repo=dict(repoid=commit.repoid)) + dbsession.add(commit) + dbsession.flush() + mock_redis.lists[f"uploads/{commit.repoid}/{commit.commitid}"] = ( + jsonified_redis_queue + ) + upload_args = UploadContext( + repoid=commit.repoid, + commitid=commit.commitid, + redis_connection=mock_redis, + ) + result = UploadTask().run_impl_within_lock(dbsession, upload_args, kwargs={}) + assert {"was_setup": False, "was_updated": False} == result + assert commit.message == "" + assert commit.parent_commit_id is None + assert commit.report is not None + sessions = commit.report.uploads + assert len(sessions) == 2 + first_session = ( + dbsession.query(Upload) + .filter_by(report_id=commit.report.id, build_code="part1") + .first() + ) + second_session = ( + dbsession.query(Upload) + .filter_by(report_id=commit.report.id, build_code="part2") + .first() + ) + mocked_schedule_task.assert_called_with( + commit, + {"codecov": {"max_report_age": "764y ago"}}, + [ + { + "build": "part1", + "url": "url1", + "flags": [], + "upload_id": first_session.id, + "upload_pk": first_session.id, + }, + { + "build": "part2", + "url": "url2", + "flags": [], + "upload_id": second_session.id, + "upload_pk": second_session.id, + }, + ], + commit.report, + mocker.ANY, + ) + + def test_upload_task_upload_already_created( + self, + mocker, + mock_configuration, + dbsession, + mock_redis, + mock_repo_provider, + mock_storage, + ): + mocked_schedule_task = mocker.patch.object(UploadTask, "schedule_task") + mocker.patch( + "tasks.upload.possibly_update_commit_from_provider_info", return_value=True + ) + mock_create_upload = mocker.patch.object(ReportService, "create_report_upload") + + def fail_if_try_to_create_upload(*args, **kwargs): + raise Exception("tried to create Upload") + + mock_create_upload.side_effect = fail_if_try_to_create_upload + mocker.patch.object(UploadTask, "possibly_setup_webhooks", return_value=True) + mock_app = mocker.patch.object(UploadTask, "app") + mock_app.send_task.return_value = True + reportid = "5fbeee8b-5a41-4925-b59d-470b9d171235" + commit = CommitFactory.create( + message="", + parent_commit_id=None, + repository__owner__unencrypted_oauth_token="test7lk5ndmtqzxlx06rip65nac9c7epqopclnoy", + repository__owner__username="ThiagoCodecov", + repository__yaml={"codecov": {"max_report_age": "764y ago"}}, + # Setting the time to _before_ patch centric default YAMLs start date of 2024-04-30 + repository__owner__createstamp=datetime(2023, 1, 1, tzinfo=timezone.utc), + ) + report = CommitReport(commit_id=commit.id_) + upload = Upload( + external_id=reportid, + build_code="part1", + build_url="build_url", + env=None, + report=report, + job_code="job", + name="name", + provider="service", + state="started", + storage_path="url", + order_number=None, + upload_extras={}, + upload_type=SessionType.uploaded.value, + state_id=UploadState.UPLOADED.db_id, + upload_type_id=UploadType.UPLOADED.db_id, + ) + mock_repo_provider.data = dict(repo=dict(repoid=commit.repoid)) + dbsession.add(commit) + dbsession.add(report) + dbsession.add(upload) + dbsession.flush() + + redis_queue = [ + {"build": "part1", "url": "url1", "upload_id": upload.id_}, + ] + jsonified_redis_queue = [json.dumps(x) for x in redis_queue] + mock_redis.lists[f"uploads/{commit.repoid}/{commit.commitid}"] = ( + jsonified_redis_queue + ) + upload_args = UploadContext( + repoid=commit.repoid, + commitid=commit.commitid, + redis_connection=mock_redis, + ) + result = UploadTask().run_impl_within_lock(dbsession, upload_args, kwargs={}) + assert {"was_setup": True, "was_updated": True} == result + assert commit.message == "" + assert commit.parent_commit_id is None + assert commit.report is not None + mocked_schedule_task.assert_called_with( + commit, + {"codecov": {"max_report_age": "764y ago"}}, + [ + { + "build": "part1", + "url": "url1", + "flags": [], + "upload_id": upload.id_, + "upload_pk": upload.id_, + } + ], + report, + mocker.ANY, + ) + + +class TestUploadTaskUnit(object): + def test_list_of_arguments(self, mock_redis): + upload_args = UploadContext( + repoid=542, + commitid="commitid", + redis_connection=mock_redis, + ) + first_redis_queue = [ + {"url": "http://example.first.com"}, + {"and_another": "one"}, + ] + mock_redis.lists["uploads/542/commitid"] = [ + json.dumps(x) for x in first_redis_queue + ] + res = list(upload_args.arguments_list()) + assert res == [{"url": "http://example.first.com"}, {"and_another": "one"}] + + @pytest.mark.django_db + def test_schedule_task_with_one_task(self, dbsession, mocker, mock_repo_provider): + _start_upload_flow(mocker) + mocker.patch( + "tasks.base.get_repo_provider_service", + return_value=mock_repo_provider, + ) + mocker.patch( + "tasks.upload.UploadTask.possibly_setup_webhooks", return_value=True + ) + mocked_chord = mocker.patch("tasks.upload.chord") + commit = CommitFactory.create() + commit_yaml = {"codecov": {"max_report_age": "100y ago"}} + dbsession.add(commit) + dbsession.flush() + upload_args = UploadContext( + repoid=commit.repoid, + commitid=commit.commitid, + redis_connection=mock_redis, + ) + result = UploadTask().schedule_task( + commit, + commit_yaml, + [{"upload_id": 1, "upload_pk": 1}], + ReportFactory.create(), + upload_args, + ) + assert result == mocked_chord.return_value.apply_async.return_value + processor = upload_processor_task.s( + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml=commit_yaml, + arguments={"upload_id": 1, "upload_pk": 1}, + ) + finisher = upload_finisher_task.signature( + kwargs={ + "repoid": commit.repoid, + "commitid": commit.commitid, + "commit_yaml": commit_yaml, + "report_code": None, + _kwargs_key(UploadFlow): mocker.ANY, + } + ) + mocked_chord.assert_called_with([processor], finisher) + + def test_run_impl_unobtainable_lock_no_pending_jobs( + self, dbsession, mocker, mock_redis + ): + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + mock_redis.lock.side_effect = LockError() + result = UploadTask().run_impl(dbsession, commit.repoid, commit.commitid) + assert result == { + "tasks_were_scheduled": False, + "was_setup": False, + "was_updated": False, + } + + def test_run_impl_unobtainable_lock_too_many_retries( + self, dbsession, mocker, mock_redis + ): + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + mock_redis.lock.side_effect = LockError() + mock_redis.keys[f"uploads/{commit.repoid}/{commit.commitid}"] = ["something"] + task = UploadTask() + task.request.retries = 3 + result = task.run_impl(dbsession, commit.repoid, commit.commitid) + assert result == { + "tasks_were_scheduled": False, + "was_setup": False, + "was_updated": False, + "reason": "too_many_retries", + } + + def test_run_impl_currently_processing(self, dbsession, mocker, mock_redis): + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + mocked_is_currently_processing = mocker.patch.object( + UploadContext, "is_currently_processing", return_value=True + ) + mocked_run_impl_within_lock = mocker.patch.object( + UploadTask, "run_impl_within_lock", return_value=True + ) + mock_redis.keys[f"uploads/{commit.repoid}/{commit.commitid}"] = ["something"] + task = UploadTask() + task.request.retries = 0 + with pytest.raises(Retry): + task.run_impl(dbsession, commit.repoid, commit.commitid) + mocked_is_currently_processing.assert_called_with() + assert not mocked_run_impl_within_lock.called + + def test_run_impl_currently_processing_second_retry( + self, dbsession, mocker, mock_redis + ): + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + mocked_is_currently_processing = mocker.patch.object( + UploadContext, "is_currently_processing", return_value=True + ) + mocked_run_impl_within_lock = mocker.patch.object( + UploadTask, "run_impl_within_lock", return_value={"some": "value"} + ) + mock_redis.keys[f"uploads/{commit.repoid}/{commit.commitid}"] = ["something"] + task = UploadTask() + task.request.retries = 1 + result = task.run_impl(dbsession, commit.repoid, commit.commitid) + mocked_is_currently_processing.assert_called_with() + assert mocked_run_impl_within_lock.called + assert result == {"some": "value"} + + def test_is_currently_processing(self, mock_redis): + repoid = 1 + commitid = "adsdadsadfdsjnskgiejrw" + lock_name = f"upload_processing_lock_{repoid}_{commitid}" + mock_redis.keys[lock_name] = "val" + upload_args = UploadContext( + repoid=repoid, + commitid=commitid, + redis_connection=mock_redis, + ) + assert upload_args.is_currently_processing() + upload_args = UploadContext( + repoid=repoid, + commitid="pol", + redis_connection=mock_redis, + ) + assert not upload_args.is_currently_processing() + + def test_run_impl_unobtainable_lock_retry(self, dbsession, mocker, mock_redis): + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + mock_redis.lock.side_effect = LockError() + mock_redis.keys[f"uploads/{commit.repoid}/{commit.commitid}"] = ["something"] + task = UploadTask() + task.request.retries = 0 + with pytest.raises(Retry): + task.run_impl(dbsession, commit.repoid, commit.commitid) + + def test_possibly_setup_webhooks_public_repo( + self, mocker, mock_configuration, mock_repo_provider + ): + mock_configuration.set_params({"github": {"bot": {"key": "somekey"}}}) + commit = CommitFactory.create( + repository__private=False, + repository__owner__unencrypted_oauth_token="aaaaabbbbhhhh", + ) + task = UploadTask() + mock_repo_provider.data = mocker.MagicMock() + mock_repo_provider.service = "github" + res = task.possibly_setup_webhooks(commit, mock_repo_provider) + assert res is True + mock_repo_provider.post_webhook.assert_called_with( + "Codecov Webhook. None", + "None/webhooks/github", + ["pull_request", "delete", "push", "public", "status", "repository"], + "ab164bf3f7d947f2a0681b215404873e", + token=None, + ) + + def test_possibly_setup_webhooks_owner_has_ghapp( + self, mocker, dbsession, mock_configuration, mock_repo_provider + ): + mock_configuration.set_params({"github": {"bot": {"key": "somekey"}}}) + commit = CommitFactory.create( + repository__using_integration=False, repository__hookid=None + ) + ghapp = GithubAppInstallation( + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + installation_id=1234, + repository_service_ids=None, + owner=commit.repository.owner, + ) + dbsession.add(ghapp) + dbsession.flush() + task = UploadTask() + mock_repo_provider.data = mocker.MagicMock() + mock_repo_provider.service = "github" + res = task.possibly_setup_webhooks(commit, mock_repo_provider) + assert res is False + mock_repo_provider.post_webhook.assert_not_called() + + def test_possibly_setup_webhooks_gitlab( + self, dbsession, mocker, mock_configuration + ): + mock_configuration.set_params({"gitlab": {"bot": {"key": "somekey"}}}) + repository = RepositoryFactory.create() + dbsession.add(repository) + commit = CommitFactory.create(repository=repository) + dbsession.add(commit) + + gitlab_provider = mocker.MagicMock( + Gitlab, get_commit_diff=mock.AsyncMock(return_value={}) + ) + mock_repo_provider = mocker.patch( + "services.repository._get_repo_provider_service_instance" + ) + mock_repo_provider.return_value = gitlab_provider + gitlab_provider.data = mocker.MagicMock() + gitlab_provider.service = "gitlab" + + task = UploadTask() + res = task.possibly_setup_webhooks(commit, gitlab_provider) + assert res is True + + assert repository.webhook_secret is not None + gitlab_provider.post_webhook.assert_called_with( + "Codecov Webhook. None", + "None/webhooks/gitlab", + { + "push_events": True, + "issues_events": False, + "merge_requests_events": True, + "tag_push_events": False, + "note_events": False, + "job_events": False, + "build_events": True, + "pipeline_events": True, + "wiki_events": False, + }, + commit.repository.webhook_secret, + token=None, + ) + + def test_needs_webhook_secret_backfill(self, dbsession, mocker, mock_configuration): + mock_configuration.set_params({"gitlab": {"bot": {"key": "somekey"}}}) + repository = RepositoryFactory.create( + repoid="5678", hookid="1234", webhook_secret=None + ) + dbsession.add(repository) + commit = CommitFactory.create(repository=repository) + dbsession.add(commit) + gitlab_provider = mocker.MagicMock( + Gitlab, get_commit_diff=mock.AsyncMock(return_value={}) + ) + mock_repo_provider = mocker.patch( + "services.repository._get_repo_provider_service_instance" + ) + mock_repo_provider.return_value = gitlab_provider + gitlab_provider.data = mocker.MagicMock() + gitlab_provider.service = "gitlab" + task = UploadTask() + res = task.possibly_setup_webhooks(commit, gitlab_provider) + assert res is False + + assert repository.webhook_secret is not None + gitlab_provider.edit_webhook.assert_called_with( + hookid=repository.hookid, + name="Codecov Webhook. None", + url="None/webhooks/gitlab", + events={ + "push_events": True, + "issues_events": False, + "merge_requests_events": True, + "tag_push_events": False, + "note_events": False, + "job_events": False, + "build_events": True, + "pipeline_events": True, + "wiki_events": False, + }, + secret=commit.repository.webhook_secret, + ) + + def test_needs_webhook_secret_backfill_gle( + self, dbsession, mocker, mock_configuration + ): + mock_configuration.set_params( + {"gitlab_enterprise": {"bot": {"key": "somekey"}}} + ) + repository = RepositoryFactory.create( + repoid="5678", hookid="1234", webhook_secret=None + ) + dbsession.add(repository) + commit = CommitFactory.create(repository=repository) + dbsession.add(commit) + gitlab_e_provider = mocker.MagicMock( + GitlabEnterprise, get_commit_diff=mock.AsyncMock(return_value={}) + ) + mock_repo_provider = mocker.patch( + "services.repository._get_repo_provider_service_instance" + ) + mock_repo_provider.return_value = gitlab_e_provider + gitlab_e_provider.data = mocker.MagicMock() + gitlab_e_provider.service = "gitlab" + task = UploadTask() + res = task.possibly_setup_webhooks(commit, gitlab_e_provider) + assert res is False + + assert repository.webhook_secret is not None + gitlab_e_provider.edit_webhook.assert_called_with( + hookid=repository.hookid, + name="Codecov Webhook. None", + url="None/webhooks/gitlab", + events={ + "push_events": True, + "issues_events": False, + "merge_requests_events": True, + "tag_push_events": False, + "note_events": False, + "job_events": False, + "build_events": True, + "pipeline_events": True, + "wiki_events": False, + }, + secret=commit.repository.webhook_secret, + ) + + def test_doesnt_need_webhook_secret_backfill( + self, dbsession, mocker, mock_configuration + ): + mock_configuration.set_params({"gitlab": {"bot": {"key": "somekey"}}}) + secret = str(uuid.uuid4()) + repository = RepositoryFactory.create( + repoid="5678", hookid="1234", webhook_secret=secret + ) + dbsession.add(repository) + commit = CommitFactory.create(repository=repository) + dbsession.add(commit) + gitlab_provider = mocker.MagicMock( + Gitlab, get_commit_diff=mock.AsyncMock(return_value={}) + ) + mock_repo_provider = mocker.patch( + "services.repository._get_repo_provider_service_instance" + ) + mock_repo_provider.return_value = gitlab_provider + gitlab_provider.data = mocker.MagicMock() + gitlab_provider.service = "gitlab" + task = UploadTask() + res = task.possibly_setup_webhooks(commit, gitlab_provider) + assert res is False + + assert repository.webhook_secret is secret + gitlab_provider.edit_webhook.assert_not_called() + + def test_doesnt_need_webhook_secret_backfill_no_hookid( + self, dbsession, mocker, mock_configuration + ): + mock_configuration.set_params({"gitlab": {"bot": {"key": "somekey"}}}) + repository = RepositoryFactory.create( + repoid="5678", hookid=None, webhook_secret=None, using_integration=True + ) + dbsession.add(repository) + commit = CommitFactory.create(repository=repository) + dbsession.add(commit) + gitlab_provider = mocker.MagicMock( + Gitlab, get_commit_diff=mock.AsyncMock(return_value={}) + ) + mock_repo_provider = mocker.patch( + "services.repository._get_repo_provider_service_instance" + ) + mock_repo_provider.return_value = gitlab_provider + gitlab_provider.data = mocker.MagicMock() + gitlab_provider.service = "gitlab" + task = UploadTask() + res = task.possibly_setup_webhooks(commit, gitlab_provider) + assert res is False + + assert repository.webhook_secret is None + gitlab_provider.edit_webhook.assert_not_called() + + def test_needs_webhook_secret_backfill_error( + self, dbsession, mocker, mock_configuration + ): + mock_configuration.set_params( + {"gitlab_enterprise": {"bot": {"key": "somekey"}}} + ) + repository = RepositoryFactory.create( + repoid="5678", hookid="1234", webhook_secret=None + ) + dbsession.add(repository) + commit = CommitFactory.create(repository=repository) + dbsession.add(commit) + gitlab_e_provider = mocker.MagicMock( + GitlabEnterprise, get_commit_diff=mock.AsyncMock(return_value={}) + ) + mock_repo_provider = mocker.patch( + "services.repository._get_repo_provider_service_instance" + ) + mock_repo_provider.return_value = gitlab_e_provider + gitlab_e_provider.data = mocker.MagicMock() + gitlab_e_provider.service = "gitlab" + gitlab_e_provider.edit_webhook.side_effect = TorngitClientError + task = UploadTask() + + res = task.possibly_setup_webhooks(commit, gitlab_e_provider) + assert res is False + assert repository.webhook_secret is None + gitlab_e_provider.edit_webhook.assert_called_once() + + def test_upload_not_ready_to_build_report( + self, dbsession, mocker, mock_configuration, mock_repo_provider, mock_redis + ): + mock_configuration.set_params({"github": {"bot": {"key": "somekey"}}}) + commit = CommitFactory.create() + dbsession.add(commit) + dbsession.flush() + mocker.patch.object(UploadContext, "has_pending_jobs", return_value=True) + task = UploadTask() + mock_repo_provider.data = mocker.MagicMock() + mock_repo_provider.service = "github" + mocker.patch.object( + ReportService, + "initialize_and_save_report", + side_effect=NotReadyToBuildReportYetError(), + ) + upload_args = UploadContext( + repoid=commit.repoid, + commitid=commit.commitid, + redis_connection=mock_redis, + ) + with pytest.raises(Retry): + task.run_impl_within_lock(dbsession, upload_args, kwargs={}) diff --git a/apps/worker/tasks/tests/utils.py b/apps/worker/tasks/tests/utils.py new file mode 100644 index 0000000000..01eb25df12 --- /dev/null +++ b/apps/worker/tasks/tests/utils.py @@ -0,0 +1,54 @@ +import contextlib +from typing import Generator + +from sqlalchemy.orm import Session + +from app import celery_app + + +@contextlib.contextmanager +def run_tasks() -> Generator[None, None, None]: + prev = celery_app.conf.task_always_eager + celery_app.conf.update(task_always_eager=True) + try: + yield + finally: + celery_app.conf.update(task_always_eager=prev) + + +GLOBALS_USING_SESSION = [ + "celery_task_router.get_db_session", + "database.engine.get_db_session", + "tasks.base.get_db_session", +] + + +def hook_session(mocker, dbsession: Session): + """ + This patches various module-local imports related to `get_db_session`. + """ + mocker.patch("shared.metrics") + for path in GLOBALS_USING_SESSION: + mocker.patch(path, return_value=dbsession) + + +GLOBALS_USING_REPO_PROVIDER = [ + "services.comparison.get_repo_provider_service", + "services.report.get_repo_provider_service", + "tasks.notify.get_repo_provider_service", + "tasks.upload_finisher.get_repo_provider_service", + "tasks.base.get_repo_provider_service", +] + + +def hook_repo_provider(mocker, mock_repo_provider): + """ + Hooks / mocks various `get_repo_provider_service` locals. + Due to how import resolution works in python, we have to patch this + *everywhere* that is *imported* into, instead of patching the function where + it is defined. + The reason is that imports are resolved at import time, and overriding the + function definition after the fact does not work. + """ + for path in GLOBALS_USING_REPO_PROVIDER: + mocker.patch(path, return_value=mock_repo_provider) diff --git a/apps/worker/tasks/timeseries_backfill.py b/apps/worker/tasks/timeseries_backfill.py new file mode 100644 index 0000000000..c9b934899c --- /dev/null +++ b/apps/worker/tasks/timeseries_backfill.py @@ -0,0 +1,148 @@ +import logging +from datetime import datetime +from typing import Iterable, Optional + +from celery import group +from celery.canvas import Signature +from shared.celery_config import ( + timeseries_backfill_commits_task_name, + timeseries_backfill_dataset_task_name, + timeseries_save_commit_measurements_task_name, +) +from shared.timeseries.helpers import is_timeseries_enabled +from sqlalchemy.orm.session import Session + +from app import celery_app +from database.models import Commit, Repository +from database.models.timeseries import Dataset +from services.timeseries import backfill_batch_size, repository_commits_query +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class TimeseriesBackfillCommitsTask( + BaseCodecovTask, name=timeseries_backfill_commits_task_name +): + def run_impl( + self, + db_session: Session, + *, + commit_ids: Iterable[int], + dataset_names: Iterable[str], + **kwargs, + ): + if not is_timeseries_enabled(): + log.warning("Timeseries not enabled") + return {"successful": False} + + commits = db_session.query(Commit).filter(Commit.id_.in_(commit_ids)) + for commit in commits: + self.app.tasks[timeseries_save_commit_measurements_task_name].apply_async( + kwargs=dict( + commitid=commit.commitid, + repoid=commit.repoid, + dataset_names=dataset_names, + ) + ) + return {"successful": True} + + +RegisteredTimeseriesBackfillCommitsTask = celery_app.register_task( + TimeseriesBackfillCommitsTask() +) +timeseries_backfill_commits_task = celery_app.tasks[ + RegisteredTimeseriesBackfillCommitsTask.name +] + + +class TimeseriesBackfillDatasetTask( + BaseCodecovTask, name=timeseries_backfill_dataset_task_name +): + def run_impl( + self, + db_session: Session, + *, + dataset_id: int, + start_date: str, + end_date: str, + batch_size: Optional[int] = None, + **kwargs, + ): + if not is_timeseries_enabled(): + log.warning("Timeseries not enabled") + return {"successful": False} + + dataset = db_session.query(Dataset).filter(Dataset.id_ == dataset_id).first() + if not dataset: + log.error( + "Dataset not found", + extra=dict(dataset_id=dataset_id), + ) + return {"successful": False} + + repository = ( + db_session.query(Repository).filter_by(repoid=dataset.repository_id).first() + ) + if not repository: + log.error( + "Repository not found", + extra=dict(repoid=dataset.repository_id), + ) + return {"successful": False} + + if batch_size is None: + batch_size = backfill_batch_size(repository, dataset) + + try: + start_date = datetime.fromisoformat(start_date) + end_date = datetime.fromisoformat(end_date) + except ValueError: + log.error( + "Invalid date range", + extra=dict(start_date=start_date, end_date=end_date), + ) + return {"successful": False} + + # all commits in given time range + commits = repository_commits_query(repository, start_date, end_date) + + # split commits into batches of equal size + signatures = self._commit_batch_signatures(dataset, commits, batch_size) + + # enqueue task for each batch (to be run in parallel) + group(signatures).apply_async() + + return {"successful": True} + + def _commit_batch_signatures( + self, dataset: Dataset, commits: Iterable[Commit], batch_size: int + ) -> Iterable[Signature]: + commit_ids = [] + signatures = [] + for commit in commits: + commit_ids.append(commit.id_) + if len(commit_ids) == batch_size: + signatures.append(self._backfill_commits_signature(dataset, commit_ids)) + commit_ids = [] + if len(commit_ids) > 0: + signatures.append(self._backfill_commits_signature(dataset, commit_ids)) + return signatures + + def _backfill_commits_signature( + self, dataset: Dataset, commit_ids: Iterable[int] + ) -> Signature: + return timeseries_backfill_commits_task.signature( + kwargs=dict( + commit_ids=commit_ids, + dataset_names=[dataset.name], + ), + ) + + +RegisteredTimeseriesBackfillDatasetTask = celery_app.register_task( + TimeseriesBackfillDatasetTask() +) +timeseries_backfill_dataset_task = celery_app.tasks[ + RegisteredTimeseriesBackfillDatasetTask.name +] diff --git a/apps/worker/tasks/timeseries_delete.py b/apps/worker/tasks/timeseries_delete.py new file mode 100644 index 0000000000..4f5526ccb1 --- /dev/null +++ b/apps/worker/tasks/timeseries_delete.py @@ -0,0 +1,53 @@ +import logging +from typing import Optional + +from shared.celery_config import timeseries_delete_task_name +from shared.timeseries.helpers import is_timeseries_enabled +from sqlalchemy.orm.session import Session + +from app import celery_app +from database.models import Repository +from services.timeseries import delete_repository_data, delete_repository_measurements +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class TimeseriesDeleteTask(BaseCodecovTask, name=timeseries_delete_task_name): + def run_impl( + self, + db_session: Session, + *, + repository_id: int, + measurement_only: Optional[bool] = False, + measurement_type: Optional[str] = None, + measurement_id: Optional[str] = None, + **kwargs, + ): + if not is_timeseries_enabled(): + log.warning("Timeseries not enabled") + return {"successful": False, "reason": "Timeseries not enabled"} + + repo = db_session.query(Repository).filter_by(repoid=repository_id).first() + if not repo: + log.warning("Repository not found") + return {"successful": False, "reason": "Repository not found"} + + if measurement_only: + if measurement_type is None or measurement_id is None: + log.warning( + "Measurement type and ID required to delete measurements only" + ) + return { + "successful": False, + "reason": "Measurement type and ID required to delete measurements only", + } + delete_repository_measurements(repo, measurement_type, measurement_id) + else: + delete_repository_data(repo) + + return {"successful": True} + + +RegisteredTimeseriesDeleteTask = celery_app.register_task(TimeseriesDeleteTask()) +timeseries_delete_task = celery_app.tasks[RegisteredTimeseriesDeleteTask.name] diff --git a/apps/worker/tasks/trial_expiration.py b/apps/worker/tasks/trial_expiration.py new file mode 100644 index 0000000000..137f9d379a --- /dev/null +++ b/apps/worker/tasks/trial_expiration.py @@ -0,0 +1,29 @@ +import logging + +from shared.django_apps.codecov_auth.models import Owner +from shared.plan.service import PlanService + +from app import celery_app +from celery_config import trial_expiration_task_name +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class TrialExpirationTask(BaseCodecovTask, name=trial_expiration_task_name): + def run_impl(self, db_session, ownerid, *args, **kwargs): + owner = Owner.objects.get(ownerid=ownerid) + log_extra = dict( + owner_id=ownerid, + trial_end_date=owner.trial_end_date, + ) + log.info( + "Expiring owner's trial and setting back to basic plan", extra=log_extra + ) + owner_plan = PlanService(current_org=owner) + owner_plan.cancel_trial() + return {"successful": True} + + +RegisteredTrialExpirationTask = celery_app.register_task(TrialExpirationTask()) +trial_expiration_task = celery_app.tasks[RegisteredTrialExpirationTask.name] diff --git a/apps/worker/tasks/trial_expiration_cron.py b/apps/worker/tasks/trial_expiration_cron.py new file mode 100644 index 0000000000..8216d0001a --- /dev/null +++ b/apps/worker/tasks/trial_expiration_cron.py @@ -0,0 +1,45 @@ +import logging + +from shared.plan.constants import PlanName + +from app import celery_app +from celery_config import trial_expiration_cron_task_name, trial_expiration_task_name +from database.enums import TrialStatus +from database.models.core import Owner +from helpers.clock import get_utc_now +from tasks.crontasks import CodecovCronTask + +log = logging.getLogger(__name__) + +yield_amount = 100 + + +class TrialExpirationCronTask(CodecovCronTask, name=trial_expiration_cron_task_name): + @classmethod + def get_min_seconds_interval_between_executions(cls): + return 86100 # 23 hours and 55 minutes + + def run_cron_task(self, db_session, *args, **kwargs): + log.info("Doing trial expiration check") + now = get_utc_now() + + ongoing_trial_owners_that_should_be_expired = ( + db_session.query(Owner.ownerid) + .filter( + Owner.plan == PlanName.TRIAL_PLAN_NAME.value, + Owner.trial_status == TrialStatus.ONGOING.value, + Owner.trial_end_date <= now, + ) + .yield_per(yield_amount) + ) + + for owner in ongoing_trial_owners_that_should_be_expired: + self.app.tasks[trial_expiration_task_name].apply_async( + kwargs=dict(ownerid=owner.ownerid) + ) + + return {"successful": True} + + +RegisteredTrialExpirationCronTask = celery_app.register_task(TrialExpirationCronTask()) +trial_expiration_cron_task = celery_app.tasks[RegisteredTrialExpirationCronTask.name] diff --git a/apps/worker/tasks/update_branches.py b/apps/worker/tasks/update_branches.py new file mode 100644 index 0000000000..14a644e1ba --- /dev/null +++ b/apps/worker/tasks/update_branches.py @@ -0,0 +1,116 @@ +import logging + +from sqlalchemy import desc + +from app import celery_app +from celery_config import update_branches_task_name +from database.models.core import Branch, Commit +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + + +class UpdateBranchesTask(BaseCodecovTask, name=update_branches_task_name): + def run_impl( + self, + db_session, + *args, + branch_name=None, + incorrect_commitid=None, + dry_run=True, + **kwargs, + ): + if branch_name is None: + log.warning("No branch name specified, not updating any branches") + return {"attempted": False} + + log.info( + "Doing update branches for branch", + extra=dict(branch_name=branch_name, incorrect_commitid=incorrect_commitid), + ) + + branches_to_update = ( + db_session.query(Branch) + .filter( + Branch.branch == branch_name, + Branch.head == incorrect_commitid, + ) + .all() + ) + + chunk_size = 1000 + chunks = [ + branches_to_update[i : i + chunk_size] + for i in range(0, len(branches_to_update), chunk_size) + ] + + for chunk in chunks: + relevant_repos = [branch.repoid for branch in chunk] + # query similar to what we do to fetch the latest test instances + # this time there is no need to join + # this will fetch the commits in all the repos and group them together + # and order them by timestamp descending + # then only select one commit per repo starting with the first one + # it sees, thus it will select the latest commit for that repo + relevant_commits = ( + db_session.query(Commit) + .filter( + Commit.branch == branch_name, + Commit.repoid.in_(relevant_repos), + ) + .order_by(Commit.repoid) + .order_by(desc(Commit.timestamp)) + .distinct(Commit.repoid) + .all() + ) + commit_dict = {commit.repoid: commit for commit in relevant_commits} + for branch in chunk: + log.info( + "Updating branch on repo", + extra=dict( + branch_name=branch_name, + repoid=branch.repoid, + incorrect_commitid=incorrect_commitid, + ), + ) + + latest_commit_on_branch = commit_dict.get(branch.repoid, None) + if latest_commit_on_branch is None: + log.info( + "No existing commits on this branch in this repo", + extra=dict( + branch_name=branch_name, + repoid=branch.repoid, + incorrect_commitid=incorrect_commitid, + ), + ) + continue + + new_branch_head = latest_commit_on_branch.commitid + log.info( + "Found latest commit on branch and updating branch head to", + extra=dict( + branch_name=branch_name, + repoid=branch.repoid, + latest_commit=new_branch_head, + incorrect_commitid=incorrect_commitid, + ), + ) + + if not dry_run: + branch.head = new_branch_head + + if not dry_run: + log.info( + "flushing and commiting changes to chunk", + extra=dict( + branch_name=branch_name, incorrect_commitid=incorrect_commitid + ), + ) + db_session.commit() + + return {"successful": True} + + +RegisteredTrialExpirationCronTask = celery_app.register_task(UpdateBranchesTask()) +update_branches_task = celery_app.tasks[UpdateBranchesTask.name] diff --git a/apps/worker/tasks/upload.py b/apps/worker/tasks/upload.py new file mode 100644 index 0000000000..e766421302 --- /dev/null +++ b/apps/worker/tasks/upload.py @@ -0,0 +1,946 @@ +import itertools +import logging +import time +import uuid +from copy import deepcopy +from typing import Optional, TypedDict + +import orjson +import sentry_sdk +from asgiref.sync import async_to_sync +from celery import chain, chord +from django.conf import settings +from django.db import transaction as django_transaction +from django.utils import timezone +from redis import Redis +from redis.exceptions import LockError +from shared.celery_config import upload_task_name +from shared.config import get_config +from shared.django_apps.user_measurements.models import UserMeasurement +from shared.helpers.redis import get_redis_connection +from shared.metrics import Histogram +from shared.torngit.exceptions import TorngitClientError, TorngitRepoNotFoundError +from shared.upload.utils import UploaderType, bulk_insert_coverage_measurements +from shared.yaml import UserYaml +from shared.yaml.user_yaml import OwnerContext +from sqlalchemy.orm import Session + +from app import celery_app +from database.enums import ReportType +from database.models import Commit, CommitReport, Repository, RepositoryFlag, Upload +from database.models.core import GITHUB_APP_INSTALLATION_DEFAULT_NAME +from helpers.checkpoint_logger.flows import TestResultsFlow, UploadFlow +from helpers.github_installation import get_installation_name_for_owner_for_task +from rollouts import NEW_TA_TASKS +from services.bundle_analysis.report import BundleAnalysisReportService +from services.processing.state import ProcessingState +from services.processing.types import UploadArguments +from services.report import ( + BaseReportService, + NotReadyToBuildReportYetError, + ReportService, +) +from services.repository import ( + create_webhook_on_provider, + fetch_commit_yaml_and_possibly_store, + gitlab_webhook_update, + possibly_update_commit_from_provider_info, +) +from services.test_results import TestResultsReportService +from tasks.base import BaseCodecovTask +from tasks.bundle_analysis_notify import bundle_analysis_notify_task +from tasks.bundle_analysis_processor import bundle_analysis_processor_task +from tasks.test_results_finisher import test_results_finisher_task +from tasks.test_results_processor import test_results_processor_task +from tasks.upload_finisher import upload_finisher_task +from tasks.upload_processor import UPLOAD_PROCESSING_LOCK_NAME, upload_processor_task + +log = logging.getLogger(__name__) + +CHUNK_SIZE = 3 + +UPLOADS_PER_TASK_SCHEDULE = Histogram( + "worker_uploads_per_schedule", + "The number of individual uploads scheduled for processing", + ["report_type"], + buckets=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 20, 25, 30, 40, 50], +) + + +class UploadContext: + """ + Encapsulates the arguments passed to an upload task. This includes both the + Celery task arguments as well as the arguments list passed via Redis. + """ + + def __init__( + self, + repoid: int, + commitid: str, + report_type: ReportType = ReportType.COVERAGE, + report_code: str | None = None, + redis_connection: Redis | None = None, + ): + self.repoid = repoid + self.commitid = commitid + self.report_type = report_type + self.report_code = report_code + self.redis_connection = redis_connection or get_redis_connection() + + def log_extra(self, **kwargs) -> dict: + return dict( + report_type=self.report_type.value, + report_code=self.report_code, + **kwargs, + ) + + def lock_name(self, lock_type: str): + if self.report_type == ReportType.COVERAGE: + # for backward compat this does not include the report type + if lock_type == "upload_processing": + return UPLOAD_PROCESSING_LOCK_NAME(self.repoid, self.commitid) + else: + return f"{lock_type}_lock_{self.repoid}_{self.commitid}" + else: + return f"{lock_type}_lock_{self.repoid}_{self.commitid}_{self.report_type.value}" + + @property + def upload_location(self): + if self.report_type == ReportType.COVERAGE: + # for backward compat this does not include the report type + return f"uploads/{self.repoid}/{self.commitid}" + else: + return f"uploads/{self.repoid}/{self.commitid}/{self.report_type.value}" + + def is_locked(self, lock_type: str) -> bool: + lock_name = self.lock_name(lock_type) + return bool(self.redis_connection.get(lock_name)) + + def is_currently_processing(self) -> bool: + return self.is_locked("upload_processing") + + def has_pending_jobs(self) -> bool: + return bool(self.redis_connection.exists(self.upload_location)) + + def last_upload_timestamp(self): + if self.report_type == ReportType.COVERAGE: + # for backward compat this does not include the report type + redis_key = f"latest_upload/{self.repoid}/{self.commitid}" + else: + redis_key = ( + f"latest_upload/{self.repoid}/{self.commitid}/{self.report_type.value}" + ) + return self.redis_connection.get(redis_key) + + def kwargs_for_retry(self, kwargs: dict) -> dict: + return dict( + **kwargs, + repoid=self.repoid, + commitid=self.commitid, + report_type=self.report_type.value, + report_code=self.report_code, + ) + + def arguments_list(self): + """ + Retrieves a list of arguments from redis, parses them + and feeds them to the processing code. + + This function doesn't go infinite because it keeps emptying the respective key on redis. + It will only go arbitrarily long if someone else keeps uploading more and more arguments + to such list + + Yields: + dict: A dict with the parameters to be passed + """ + uploads_list_key = self.upload_location + log.debug("Fetching arguments from redis %s", uploads_list_key) + while arguments := self.redis_connection.lpop(uploads_list_key, count=50): + for arg in arguments: + yield orjson.loads(arg) + + +def normalize_flags(arguments: UploadArguments): + flags: list | str | None = arguments.get("flags") + if not flags: + flags = [] + elif isinstance(flags, str): + flags = [flag.strip() for flag in flags.split(",")] + arguments["flags"] = flags + + +def _should_debounce_processing(upload_context: UploadContext) -> Optional[float]: + """ + Queries the `UploadContext`s `last_upload_timestamp` and determines if + another upload should be debounced by some time. + """ + upload_processing_delay = get_config("setup", "upload_processing_delay") + if upload_processing_delay is None: + return None + + upload_processing_delay = float(upload_processing_delay) + last_upload_timestamp = upload_context.last_upload_timestamp() + if last_upload_timestamp is None: + return None + + last_upload_delta = time.time() - float(last_upload_timestamp) + if last_upload_delta < upload_processing_delay: + return max(30, upload_processing_delay - last_upload_delta) + return None + + +class CreateUploadResponse(TypedDict): + argument_list: list[UploadArguments] + measurements_list: list[UserMeasurement] + upload_flag_map: dict[Upload, list | str | None] + + +class UploadTask(BaseCodecovTask, name=upload_task_name): + """The first of a series of tasks designed to process an `upload` made by the user + + This task is the first of three tasks, which run whenever a user makes + an upload to `UploadHandler` (on the main app code) + + - UploadTask + - UploadProcessorTask + - UploadFinisherTask + + Each task has a purpose + + - UploadTask (this one) + - Prepares the ground for the other tasks to run (view it as a compatibility layer between + the old code and new) + - Does things that only need to happen once per commit, and not per upload, + like populating commit info and webhooks + - UploadProcessorTask + - Process each individual upload the user did (with some possible batching) + - UploadFinisherTask + - Does the finishing steps of processing, like deciding what tasks + to schedule next (notifications) + + Now a little about this individual task. + + UploadTask has a specific purpose, it does all the 'pre-processing', for things that should be + run outside the individual `upload` context, and is also the starter + of the other tasks. + + The preprocessing tasks it does are: + - Populating commit's info, in case this is the first time this commit is uploaded to our + servers + - Setup webhooks, in case this is the first time this repo has an upload on our servers + - Fetch commit yaml from git provider, and possibly store it on the db (in case this + is a commit on the repo default branch). This yaml is also passed and used on + the other tasks, so they don't need to fetch it again + + The last thing this task does is schedule the other tasks. It works as a compatibility layer + because the `UploadHandler` (on the main app code) pushes some important info to + redis to be read here, and this task already takes all the relevant info from redis + and pass them directly as parameters to the other tasks, so they don't have to manually + deal with redis (since celery kind of automatically does the same behavior already) + + On the scheduling, this task does the following logic: + - After fetching all uploads metadata (from redis), it splits the uploads in chunks of 3. + - Each chunk goes to a `UploadProcessorTask`, and they are chained (as in `celery chain`) + - At the end of the celery chain, we add one `UploadFinisherTask`. So after all processing, + the finisher task does the finishing steps + - In the end, the tasks are scheduled (sent to celery), and this task finishes + + """ + + def run_impl( + self, + db_session: Session, + repoid: int, + commitid: str, + report_type: str = "coverage", + report_code: str | None = None, + *args, + **kwargs, + ): + upload_context = UploadContext( + repoid=int(repoid), + commitid=commitid, + report_type=ReportType(report_type), + report_code=report_code, + ) + if report_code: + sentry_sdk.capture_message( + "Customer is using non-default `report_code`", + tags={"report_type": report_type, "report_code": report_code}, + ) + + # If we're a retry, kwargs will already have our first checkpoint. + # If not, log it directly into kwargs so we can pass it onto other tasks + if upload_context.report_type == ReportType.COVERAGE: + UploadFlow.log( + UploadFlow.UPLOAD_TASK_BEGIN, kwargs=kwargs, ignore_repeat=True + ) + elif upload_context.report_type == ReportType.TEST_RESULTS: + TestResultsFlow.log( + TestResultsFlow.TEST_RESULTS_BEGIN, kwargs=kwargs, ignore_repeat=True + ) + + log.info("Received upload task", extra=upload_context.log_extra()) + + if not upload_context.has_pending_jobs(): + log.info("No pending jobs. Upload task is done.") + self.maybe_log_upload_checkpoint(UploadFlow.NO_PENDING_JOBS) + return { + "was_setup": False, + "was_updated": False, + "tasks_were_scheduled": False, + } + + if upload_context.is_currently_processing() and self.request.retries == 0: + log.info( + "Currently processing upload. Retrying in 60s.", + extra=upload_context.log_extra(), + ) + self.retry(countdown=60, kwargs=upload_context.kwargs_for_retry(kwargs)) + + if retry_countdown := _should_debounce_processing(upload_context): + log.info( + "Retrying due to very recent uploads.", + extra=upload_context.log_extra( + countdown=retry_countdown, + ), + ) + self.retry( + countdown=retry_countdown, + kwargs=upload_context.kwargs_for_retry(kwargs), + ) + + lock_name = upload_context.lock_name("upload") + try: + with upload_context.redis_connection.lock( + lock_name, + timeout=max(300, self.hard_time_limit_task), + blocking_timeout=5, + ): + # Check whether a different `Upload` task has "stolen" our uploads + if not upload_context.has_pending_jobs(): + log.info("No pending jobs. Upload task is done.") + self.maybe_log_upload_checkpoint(UploadFlow.NO_PENDING_JOBS) + return { + "was_setup": False, + "was_updated": False, + "tasks_were_scheduled": False, + } + + return self.run_impl_within_lock( + db_session, + upload_context, + kwargs, + ) + except LockError: + log.warning( + "Unable to acquire lock for key %s.", + lock_name, + extra=dict(commit=commitid, repoid=repoid, report_type=report_type), + ) + if not upload_context.has_pending_jobs(): + log.info( + "Not retrying since there are likely no jobs that need scheduling", + extra=upload_context.log_extra(), + ) + self.maybe_log_upload_checkpoint(UploadFlow.NO_PENDING_JOBS) + return { + "was_setup": False, + "was_updated": False, + "tasks_were_scheduled": False, + } + if self.request.retries > 1: + log.info( + "Not retrying since we already had too many retries", + extra=upload_context.log_extra(), + ) + self.maybe_log_upload_checkpoint(UploadFlow.TOO_MANY_RETRIES) + return { + "was_setup": False, + "was_updated": False, + "tasks_were_scheduled": False, + "reason": "too_many_retries", + } + retry_countdown = 20 * 2**self.request.retries + log.warning( + "Retrying upload", + extra=upload_context.log_extra(countdown=retry_countdown), + ) + self.retry( + max_retries=3, + countdown=retry_countdown, + kwargs=upload_context.kwargs_for_retry(kwargs), + ) + + @sentry_sdk.trace + def run_impl_within_lock( + self, + db_session: Session, + upload_context: UploadContext, + kwargs: dict, + ): + log.info("Starting processing of report", extra=upload_context.log_extra()) + repoid = upload_context.repoid + report_type = upload_context.report_type + + if report_type == ReportType.COVERAGE: + try: + UploadFlow.log(UploadFlow.PROCESSING_BEGIN) + except ValueError as e: + log.warning( + "CheckpointLogger failed to log/submit", extra=dict(error=e) + ) + + commit = ( + db_session.query(Commit) + .filter(Commit.repoid == repoid, Commit.commitid == upload_context.commitid) + .first() + ) + assert commit, "Commit not found in database." + repository = commit.repository + repository_service = None + + was_updated, was_setup = False, False + installation_name_to_use = get_installation_name_for_owner_for_task( + self.name, repository.owner + ) + repository_service = self.get_repo_provider_service( + repository, installation_name_to_use=installation_name_to_use, commit=commit + ) + + if repository_service: + commit_yaml = fetch_commit_yaml_and_possibly_store( + commit, repository_service + ) + try: + was_updated = possibly_update_commit_from_provider_info( + commit, repository_service + ) + was_setup = self.possibly_setup_webhooks(commit, repository_service) + except TorngitRepoNotFoundError: + log.warning( + "Unable to reach git provider because this specific bot/integration can't see that repository", + extra=upload_context.log_extra(), + ) + except TorngitClientError: + log.warning( + "Unable to reach git provider because there was a 4xx error", + extra=upload_context.log_extra(), + exc_info=True, + ) + else: + context = OwnerContext( + owner_onboarding_date=repository.owner.createstamp, + owner_plan=repository.owner.plan, + ownerid=repository.ownerid, + ) + commit_yaml = UserYaml.get_final_yaml( + owner_yaml=repository.owner.yaml, + repo_yaml=repository.yaml, + commit_yaml=None, + owner_context=context, + ) + + report_service: BaseReportService + if report_type == ReportType.COVERAGE: + # TODO: consider renaming class to `CoverageReportService` + report_service = ReportService( + commit_yaml, gh_app_installation_name=installation_name_to_use + ) + elif report_type == ReportType.BUNDLE_ANALYSIS: + report_service = BundleAnalysisReportService(commit_yaml) + elif report_type == ReportType.TEST_RESULTS: + report_service = TestResultsReportService(commit_yaml) + else: + raise NotImplementedError(f"no report service for: {report_type.value}") + + try: + log.info("Initializing and saving report", extra=upload_context.log_extra()) + commit_report = report_service.initialize_and_save_report( + commit, upload_context.report_code + ) + except NotReadyToBuildReportYetError: + log.warning( + "Commit not yet ready to build its initial report. Retrying in 60s.", + extra=upload_context.log_extra(), + ) + self.retry(countdown=60, kwargs=upload_context.kwargs_for_retry(kwargs)) + + upload_argument_list = self.possibly_insert_uploads_and_side_effects( + db_session=db_session, + upload_context=upload_context, + commit=commit, + commit_report=commit_report, + report_service=report_service, + ) + + if upload_argument_list: + db_session.commit() + + UPLOADS_PER_TASK_SCHEDULE.labels(report_type=report_type.value).observe( + len(upload_argument_list) + ) + scheduled_tasks = self.schedule_task( + commit, + commit_yaml.to_dict(), + upload_argument_list, + commit_report, + upload_context, + ) + + log.info( + f"Scheduling {upload_context.report_type.value} processing tasks", + extra=upload_context.log_extra( + argument_list=upload_argument_list, + number_arguments=len(upload_argument_list), + scheduled_task_ids=scheduled_tasks.as_tuple(), + ), + ) + + else: + self.maybe_log_upload_checkpoint(UploadFlow.INITIAL_PROCESSING_COMPLETE) + self.maybe_log_upload_checkpoint(UploadFlow.NO_REPORTS_FOUND) + log.info( + "Not scheduling task because there were no arguments found on redis", + extra=upload_context.log_extra(), + ) + return {"was_setup": was_setup, "was_updated": was_updated} + + def possibly_insert_uploads_and_side_effects( + self, + db_session: Session, + upload_context: UploadContext, + commit: Commit, + commit_report: CommitReport, + report_service: ReportService, + ) -> list[UploadArguments]: + """ + This method possibly batch inserts uploads, flags and user measurements. + This only happens for v4 uploads as CLI uploads create the records mentioned + above in the uploads codecov-api route. + """ + repository: Repository = commit.repository + + # Possibly batch insert uploads + create_upload_res = self._possibly_create_uploads_to_insert( + db_session=db_session, + commit=commit, + repository=repository, + commit_report=commit_report, + upload_context=upload_context, + report_service=report_service, + ) + + # Bulk insert flags + if uploads_flag_map := create_upload_res["upload_flag_map"]: + self._bulk_insert_flags( + db_session=db_session, + repoid=repository.repoid, + upload_flag_map=uploads_flag_map, + ) + + # Bulk insert coverage measurements + if measurements := create_upload_res["measurements_list"]: + self._bulk_insert_coverage_measurements(measurements=measurements) + + return create_upload_res["argument_list"] + + def _possibly_create_uploads_to_insert( + self, + db_session: Session, + commit: Commit, + repository: Repository, + upload_context: UploadContext, + report_service: ReportService, + commit_report: CommitReport, + ) -> CreateUploadResponse: + # List to track arguments for the rest of uploads + argument_list: list[UploadArguments] = [] + + # List to track possible measurements to insert later + measurements_list: list[UserMeasurement] = [] + created_at = timezone.now() + + # List + helper mapping to track possible upload + flags to insert later + upload_flag_map: dict[Upload, list | str | None] = {} + + for arguments in upload_context.arguments_list(): + arguments.pop("token", None) + normalize_flags(arguments) + + if "upload_id" not in arguments: + upload = report_service.create_report_upload(arguments, commit_report) + arguments["upload_id"] = upload.id_ + # Adds objects to insert later in bulk + upload_flag_map[upload] = arguments.get("flags", []) + measurements_list.append( + UserMeasurement( + owner_id=repository.owner.ownerid, + repo_id=repository.repoid, + commit_id=commit.id, + upload_id=upload.id, + # CLI precreates the upload in API so this defaults to Legacy + uploader_used=UploaderType.LEGACY.value, + private_repo=repository.private, + report_type=commit_report.report_type, + created_at=created_at, + ) + ) + + # TODO(swatinem): eventually migrate from `upload_pk` to `upload_id`: + arguments["upload_pk"] = arguments["upload_id"] + argument_list.append(arguments) + + db_session.commit() + return CreateUploadResponse( + argument_list=argument_list, + measurements_list=measurements_list, + upload_flag_map=upload_flag_map, + ) + + def _bulk_insert_flags( + self, + db_session: Session, + repoid: int, + upload_flag_map: dict[Upload, list | str | None] = None, + ): + """ + This function possibly inserts flags in bulk for a Repository if these + don't exist already + """ + if upload_flag_map is None: + upload_flag_map = {} + + # Fetch all RepositoryFlags per repo + existing_flags = self._fetch_all_repo_flags( + db_session=db_session, repoid=repoid + ) + + # Prepare new flags and map relationships + flags_to_create: list[RepositoryFlag] = [] + upload_flag_links = {} + + # Loops through upload_flag_map dict, possibly creates flags and maps them to their uploads + for upload, flag_names in upload_flag_map.items(): + upload_flags = [] + + for flag_name in flag_names: + # Check for existing flag, create if missing + flag = existing_flags.get(flag_name) + if not flag: + flag = RepositoryFlag(repository_id=repoid, flag_name=flag_name) + flags_to_create.append(flag) + existing_flags[flag_name] = flag + + upload_flags.append(flag) + + # Save the relationship mapping without causing additional queries + upload_flag_links[upload] = upload_flags + + if flags_to_create: + db_session.add_all(flags_to_create) + db_session.commit() + + # Assign flags to uploads + for upload, upload_flags in upload_flag_links.items(): + upload.flags = upload_flags + + db_session.commit() + + def _fetch_all_repo_flags( + self, db_session: Session, repoid: int + ) -> Optional[dict[str, RepositoryFlag] | dict]: + """ + Fetches all flags on a repository + """ + flags = db_session.query(RepositoryFlag).filter_by(repository_id=repoid).all() + return {flag.flag_name: flag for flag in flags} if flags else {} + + def _bulk_insert_coverage_measurements(self, measurements: list[UserMeasurement]): + bulk_insert_coverage_measurements(measurements=measurements) + django_transaction.commit() + + def schedule_task( + self, + commit: Commit, + commit_yaml: dict, + argument_list: list[UploadArguments], + commit_report: CommitReport, + upload_context: UploadContext, + ): + # Carryforward the parent BA report for the current commit's BA report when handling uploads + # that's not bundle analysis type. + self.possibly_carryforward_bundle_report( + commit, commit_report, commit_yaml, argument_list + ) + + if upload_context.report_type == ReportType.COVERAGE: + assert ( + commit_report.report_type is None + or commit_report.report_type == ReportType.COVERAGE.value + ) + return self._schedule_coverage_processing_task( + commit, + commit_yaml, + argument_list, + commit_report, + ) + elif upload_context.report_type == ReportType.BUNDLE_ANALYSIS: + assert commit_report.report_type == ReportType.BUNDLE_ANALYSIS.value + return self._schedule_bundle_analysis_processing_task( + commit, + commit_yaml, + argument_list, + ) + elif upload_context.report_type == ReportType.TEST_RESULTS: + assert commit_report.report_type == ReportType.TEST_RESULTS.value + return self._schedule_ta_processing_task( + commit, commit_yaml, argument_list, commit_report + ) + + def _schedule_coverage_processing_task( + self, + commit: Commit, + commit_yaml: dict, + argument_list: list[UploadArguments], + commit_report: CommitReport, + ): + self.maybe_log_upload_checkpoint(UploadFlow.INITIAL_PROCESSING_COMPLETE) + + state = ProcessingState(commit.repoid, commit.commitid) + state.mark_uploads_as_processing( + [int(upload["upload_id"]) for upload in argument_list] + ) + + parallel_processing_tasks = [ + upload_processor_task.s( + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml=commit_yaml, + arguments=arguments, + ) + for arguments in argument_list + ] + + finisher_kwargs = { + "repoid": commit.repoid, + "commitid": commit.commitid, + "commit_yaml": commit_yaml, + "report_code": commit_report.code, + } + finisher_kwargs = UploadFlow.save_to_kwargs(finisher_kwargs) + finish_parallel_sig = upload_finisher_task.signature(kwargs=finisher_kwargs) + + parallel_tasks = chord(parallel_processing_tasks, finish_parallel_sig) + return parallel_tasks.apply_async() + + def _schedule_bundle_analysis_processing_task( + self, + commit: Commit, + commit_yaml: dict, + argument_list: list[UploadArguments], + do_notify: Optional[bool] = True, + ): + task_signatures = [ + bundle_analysis_processor_task.s( + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml=commit_yaml, + params=params, + ) + for params in argument_list + ] + task_signatures[0].args = ({},) # this is the first `previous_result` + + # it might make sense to eventually have a "finisher" task that + # does whatever extra stuff + enqueues a notify + if do_notify: + task_signatures.append( + bundle_analysis_notify_task.s( + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml=commit_yaml, + ) + ) + + return chain(task_signatures).apply_async() + + def _schedule_ta_processing_task( + self, + commit: Commit, + commit_yaml: dict, + argument_list: list[UploadArguments], + commit_report: CommitReport, + ): + new_ta_tasks = NEW_TA_TASKS.check_value(commit.repoid, default="old") + if not settings.TA_TIMESERIES_ENABLED: + new_ta_tasks = "old" + + task_group = [ + test_results_processor_task.s( + repoid=commit.repoid, + commitid=commit.commitid, + commit_yaml=commit_yaml, + arguments_list=list(chunk), + report_code=commit_report.code, + impl_type=new_ta_tasks, + ) + for chunk in itertools.batched(argument_list, CHUNK_SIZE) + ] + + task_group[0].args = (False,) + + finisher_kwargs = { + "repoid": commit.repoid, + "commitid": commit.commitid, + "commit_yaml": commit_yaml, + "impl_type": new_ta_tasks, + } + finisher_kwargs = TestResultsFlow.save_to_kwargs(finisher_kwargs) + task_group.append( + test_results_finisher_task.signature(kwargs=finisher_kwargs), + ) + chain_result = chain(*task_group).apply_async() + + return chain_result + + def possibly_carryforward_bundle_report( + self, + commit: Commit, + commit_report: CommitReport, + commit_yaml: dict, + argument_list: list[UploadArguments], + ): + """ + If an upload is not of bundle analysis type we will create an additional BA report and upload for it. + The reason this is done is because when doing BA comparisons if the base report does not have a proper + BA upload then the head can not be compared. So to prevent that we will always create a BA report on + all upload types, if the upload is not a BA upload then we will copy the parent's report to it. + This implementation is similar to carryforward flag mechanism in coverage, note the the difference it + that instead of traversing the commit tree during fetch, we always create a permanent report on every upload. + """ + if ( + commit_report.report_type != ReportType.BUNDLE_ANALYSIS.value + and commit.repository.bundle_analysis_enabled + ): + # Override upload_id from other upload types and create the BA uploads in the + # BA processor task + ba_argument_list = [] + for arg in argument_list: + ba_arg = deepcopy(arg) + del ba_arg["upload_id"] + ba_arg["upload_pk"] = None + ba_argument_list.append(ba_arg) + + self._schedule_bundle_analysis_processing_task( + commit, + commit_yaml, + ba_argument_list, + do_notify=False, + ) + + def possibly_setup_webhooks(self, commit: Commit, repository_service): + repository = commit.repository + repo_data = repository_service.data + + ghapp_default_installations = list( + filter( + lambda obj: obj.name == GITHUB_APP_INSTALLATION_DEFAULT_NAME, + commit.repository.owner.github_app_installations or [], + ) + ) + should_post_ghapp = not ( + ghapp_default_installations != [] + and ghapp_default_installations[0].is_repo_covered_by_integration( + commit.repository + ) + ) + should_post_legacy = not repository.using_integration + + should_post_webhook = ( + should_post_legacy + and should_post_ghapp + and not repository.hookid + and hasattr(repository_service, "post_webhook") + ) + + needs_webhook_secret_backfill = ( + repository_service.service in ["gitlab", "gitlab_enterprise"] + and repository.hookid + and not repository.webhook_secret + and hasattr(repository_service, "edit_webhook") + ) + + # try to add webhook + if should_post_webhook or needs_webhook_secret_backfill: + log.info( + "Setting or editing webhook", + extra=dict( + repoid=repository.repoid, + commit=commit.commitid, + action="SET" if should_post_webhook else "EDIT", + ), + ) + try: + if repository_service.service in ["gitlab", "gitlab_enterprise"]: + # we use per-repo webhook secrets in this case + webhook_secret = repository.webhook_secret or str(uuid.uuid4()) + else: + # service-level config value will be used instead in this case + webhook_secret = None + + if should_post_webhook: + hook_result = async_to_sync(create_webhook_on_provider)( + repository_service, webhook_secret=webhook_secret + ) + hookid = hook_result["id"] + log.info( + "Registered hook", + extra=dict( + repoid=commit.repoid, + commit=commit.commitid, + hookid=hookid, + action="SET", + ), + ) + repository.hookid = hookid + repo_data["repo"]["hookid"] = hookid + repository.webhook_secret = webhook_secret + return True # was_setup + else: + async_to_sync(gitlab_webhook_update)( + repository_service=repository_service, + hookid=repository.hookid, + secret=webhook_secret, + ) + repository.webhook_secret = webhook_secret + log.info( + "Updated hook", + extra=dict( + repository_service=repository_service.service, + repoid=repository.repoid, + commit=commit.commitid, + hookid=repository.hookid, + action="EDIT", + ), + ) + return False # was_setup + except TorngitClientError: + log.warning( + "Failed to create or update project webhook", + extra=dict( + repoid=repository.repoid, + commit=commit.commitid, + action="SET" if should_post_webhook else "EDIT", + ), + exc_info=True, + ) + return False + + def maybe_log_upload_checkpoint(self, checkpoint): + if UploadFlow.has_begun(): + UploadFlow.log(checkpoint) + + +RegisteredUploadTask = celery_app.register_task(UploadTask()) +upload_task = celery_app.tasks[RegisteredUploadTask.name] diff --git a/apps/worker/tasks/upload_finisher.py b/apps/worker/tasks/upload_finisher.py new file mode 100644 index 0000000000..24c678d8dd --- /dev/null +++ b/apps/worker/tasks/upload_finisher.py @@ -0,0 +1,460 @@ +import logging +import random +import re +from datetime import datetime, timedelta, timezone +from enum import Enum + +import sentry_sdk +from asgiref.sync import async_to_sync +from redis.exceptions import LockError +from redis.lock import Lock +from shared.celery_config import ( + compute_comparison_task_name, + notify_task_name, + pulls_task_name, + timeseries_save_commit_measurements_task_name, + upload_finisher_task_name, +) +from shared.helpers.cache import cache +from shared.helpers.redis import get_redis_connection +from shared.reports.resources import Report +from shared.timeseries.helpers import is_timeseries_enabled +from shared.torngit.exceptions import TorngitError +from shared.yaml import UserYaml + +from app import celery_app +from celery_config import notify_error_task_name +from database.enums import CommitErrorTypes +from database.models import Commit, Pull +from database.models.core import GITHUB_APP_INSTALLATION_DEFAULT_NAME +from helpers.checkpoint_logger.flows import UploadFlow +from helpers.exceptions import RepositoryWithoutValidBotError +from helpers.github_installation import get_installation_name_for_owner_for_task +from helpers.save_commit_error import save_commit_error +from services.comparison import get_or_create_comparison +from services.processing.intermediate import ( + cleanup_intermediate_reports, + load_intermediate_reports, +) +from services.processing.merging import merge_reports, update_uploads +from services.processing.state import ProcessingState, should_trigger_postprocessing +from services.processing.types import ProcessingResult +from services.report import ReportService +from services.repository import get_repo_provider_service +from services.timeseries import repository_datasets_query +from services.yaml import read_yaml_field +from tasks.base import BaseCodecovTask +from tasks.upload_processor import MAX_RETRIES, UPLOAD_PROCESSING_LOCK_NAME + +log = logging.getLogger(__name__) + +regexp_ci_skip = re.compile(r"\[(ci|skip| |-){3,}\]") + + +class ShouldCallNotifyResult(Enum): + DO_NOT_NOTIFY = "do_not_notify" + NOTIFY_ERROR = "notify_error" + NOTIFY = "notify" + + +class UploadFinisherTask(BaseCodecovTask, name=upload_finisher_task_name): + """This is the third task of the series of tasks designed to process an `upload` made + by the user + + To see more about the whole picture, see `tasks.upload.UploadTask` + + This task does the finishing steps after a group of uploads is processed + + The steps are: + - Schedule the set_pending task, depending on the case + - Schedule notification tasks, depending on the case + - Invalidating whatever cache is done + """ + + def run_impl( + self, + db_session, + processing_results: list[ProcessingResult], + *args, + repoid: int, + commitid: str, + commit_yaml, + report_code: str | None = None, + **kwargs, + ): + try: + UploadFlow.log(UploadFlow.BATCH_PROCESSING_COMPLETE) + except ValueError as e: + log.warning("CheckpointLogger failed to log/submit", extra=dict(error=e)) + + log.info( + "Received upload_finisher task", + extra={"processing_results": processing_results}, + ) + + repoid = int(repoid) + commit_yaml = UserYaml(commit_yaml) + + commit = ( + db_session.query(Commit) + .filter(Commit.repoid == repoid, Commit.commitid == commitid) + .first() + ) + assert commit, "Commit not found in database." + repository = commit.repository + + state = ProcessingState(repoid, commitid) + + upload_ids = [upload["upload_id"] for upload in processing_results] + diff = load_commit_diff(commit, self.name) + + try: + with get_report_lock(repoid, commitid, self.hard_time_limit_task): + report_service = ReportService(commit_yaml) + report = perform_report_merging( + report_service, commit_yaml, commit, processing_results + ) + + log.info( + "Saving combined report", + extra={"processing_results": processing_results}, + ) + + if diff: + report.apply_diff(diff) + report_service.save_report(commit, report, report_code) + + db_session.commit() + state.mark_uploads_as_merged(upload_ids) + + except LockError: + max_retry = 200 * 3**self.request.retries + retry_in = min(random.randint(max_retry // 2, max_retry), 60 * 60 * 5) + log.warning( + "Unable to acquire report lock. Retrying", + extra=dict(countdown=retry_in, number_retries=self.request.retries), + ) + self.retry(max_retries=MAX_RETRIES, countdown=retry_in) + + cleanup_intermediate_reports(upload_ids) + + if not should_trigger_postprocessing(state.get_upload_numbers()): + UploadFlow.log(UploadFlow.PROCESSING_COMPLETE) + UploadFlow.log(UploadFlow.SKIPPING_NOTIFICATION) + return + + lock_name = f"upload_finisher_lock_{repoid}_{commitid}" + redis_connection = get_redis_connection() + try: + with redis_connection.lock(lock_name, timeout=60 * 5, blocking_timeout=5): + result = self.finish_reports_processing( + db_session, + commit, + commit_yaml, + processing_results, + report_code, + ) + if is_timeseries_enabled(): + dataset_names = [ + dataset.name + for dataset in repository_datasets_query(repository) + ] + if dataset_names: + self.app.tasks[ + timeseries_save_commit_measurements_task_name + ].apply_async( + kwargs=dict( + commitid=commitid, + repoid=repoid, + dataset_names=dataset_names, + ) + ) + + # Mark the repository as updated so it will appear earlier in the list + # of recently-active repositories + now = datetime.now(tz=timezone.utc) + threshold = now - timedelta(minutes=30) + if not repository.updatestamp or repository.updatestamp < threshold: + repository.updatestamp = now + db_session.commit() + + self.invalidate_caches(redis_connection, commit) + log.info("Finished upload_finisher task") + return result + except LockError: + log.warning("Unable to acquire lock", extra=dict(lock_name=lock_name)) + UploadFlow.log(UploadFlow.FINISHER_LOCK_ERROR) + + def finish_reports_processing( + self, + db_session, + commit: Commit, + commit_yaml: UserYaml, + processing_results: list[ProcessingResult], + report_code, + ): + log.debug("In finish_reports_processing for commit: %s" % commit) + commitid = commit.commitid + repoid = commit.repoid + + # always notify, let the notify handle if it should submit + notifications_called = False + if not regexp_ci_skip.search(commit.message or ""): + match self.should_call_notifications( + commit, commit_yaml, processing_results, report_code + ): + case ShouldCallNotifyResult.NOTIFY: + notifications_called = True + notify_kwargs = { + "repoid": repoid, + "commitid": commitid, + "current_yaml": commit_yaml.to_dict(), + } + notify_kwargs = UploadFlow.save_to_kwargs(notify_kwargs) + task = self.app.tasks[notify_task_name].apply_async( + kwargs=notify_kwargs + ) + log.info( + "Scheduling notify task", + extra=dict( + repoid=repoid, + commit=commitid, + commit_yaml=commit_yaml.to_dict(), + processing_results=processing_results, + notify_task_id=task.id, + parent_task=self.request.parent_id, + ), + ) + if commit.pullid: + pull = ( + db_session.query(Pull) + .filter_by(repoid=commit.repoid, pullid=commit.pullid) + .first() + ) + if pull: + head = pull.get_head_commit() + if head is None or head.timestamp <= commit.timestamp: + pull.head = commit.commitid + if pull.head == commit.commitid: + db_session.commit() + self.app.tasks[pulls_task_name].apply_async( + kwargs=dict( + repoid=repoid, + pullid=pull.pullid, + should_send_notifications=False, + ) + ) + compared_to = pull.get_comparedto_commit() + if compared_to: + comparison = get_or_create_comparison( + db_session, compared_to, commit + ) + db_session.commit() + self.app.tasks[ + compute_comparison_task_name + ].apply_async( + kwargs=dict(comparison_id=comparison.id) + ) + case ShouldCallNotifyResult.DO_NOT_NOTIFY: + notifications_called = False + log.info( + "Skipping notify task", + extra=dict( + repoid=repoid, + commit=commitid, + commit_yaml=commit_yaml.to_dict(), + processing_results=processing_results, + parent_task=self.request.parent_id, + ), + ) + case ShouldCallNotifyResult.NOTIFY_ERROR: + notifications_called = False + notify_error_kwargs = { + "repoid": repoid, + "commitid": commitid, + "current_yaml": commit_yaml.to_dict(), + } + notify_error_kwargs = UploadFlow.save_to_kwargs(notify_error_kwargs) + task = self.app.tasks[notify_error_task_name].apply_async( + kwargs=notify_error_kwargs + ) + else: + commit.state = "skipped" + + UploadFlow.log(UploadFlow.PROCESSING_COMPLETE) + if not notifications_called: + UploadFlow.log(UploadFlow.SKIPPING_NOTIFICATION) + + return {"notifications_called": notifications_called} + + def should_call_notifications( + self, + commit: Commit, + commit_yaml: UserYaml, + processing_results: list[ProcessingResult], + report_code, + ) -> ShouldCallNotifyResult: + extra_dict = { + "repoid": commit.repoid, + "commitid": commit.commitid, + "commit_yaml": commit_yaml, + "processing_results": processing_results, + "report_code": report_code, + "parent_task": self.request.parent_id, + } + + manual_trigger = read_yaml_field( + commit_yaml, ("codecov", "notify", "manual_trigger") + ) + if manual_trigger: + log.info( + "Not scheduling notify because manual trigger is used", + extra=extra_dict, + ) + return ShouldCallNotifyResult.DO_NOT_NOTIFY + # Notifications should be off in case of local uploads, and report code wouldn't be null in that case + if report_code is not None: + log.info( + "Not scheduling notify because it's a local upload", + extra=extra_dict, + ) + return ShouldCallNotifyResult.DO_NOT_NOTIFY + + after_n_builds = ( + read_yaml_field(commit_yaml, ("codecov", "notify", "after_n_builds")) or 0 + ) + if after_n_builds > 0: + report = ReportService(commit_yaml).get_existing_report_for_commit(commit) + number_sessions = len(report.sessions) if report is not None else 0 + if after_n_builds > number_sessions: + log.info( + "Not scheduling notify because `after_n_builds` is %s and we only found %s builds", + after_n_builds, + number_sessions, + extra=extra_dict, + ) + return ShouldCallNotifyResult.DO_NOT_NOTIFY + + processing_successses = [x["successful"] for x in processing_results] + + if read_yaml_field( + commit_yaml, + ("codecov", "notify", "notify_error"), + _else=False, + ): + if len(processing_successses) == 0 or not all(processing_successses): + log.info( + "Not scheduling notify because there is a non-successful processing result", + extra=extra_dict, + ) + + return ShouldCallNotifyResult.NOTIFY_ERROR + else: + if not any(processing_successses): + return ShouldCallNotifyResult.DO_NOT_NOTIFY + + return ShouldCallNotifyResult.NOTIFY + + def invalidate_caches(self, redis_connection, commit: Commit): + redis_connection.delete("cache/{}/tree/{}".format(commit.repoid, commit.branch)) + redis_connection.delete( + "cache/{0}/tree/{1}".format(commit.repoid, commit.commitid) + ) + repository = commit.repository + key = ":".join((repository.service, repository.owner.username, repository.name)) + if commit.branch: + redis_connection.hdel("badge", ("%s:%s" % (key, (commit.branch))).lower()) + if commit.branch == repository.branch: + redis_connection.hdel("badge", ("%s:" % key).lower()) + + +RegisteredUploadTask = celery_app.register_task(UploadFinisherTask()) +upload_finisher_task = celery_app.tasks[RegisteredUploadTask.name] + + +def get_report_lock(repoid: int, commitid: str, hard_time_limit: int) -> Lock: + lock_name = UPLOAD_PROCESSING_LOCK_NAME(repoid, commitid) + redis_connection = get_redis_connection() + + timeout = 60 * 5 + if hard_time_limit: + timeout = max(timeout, hard_time_limit) + + return redis_connection.lock( + lock_name, + timeout=timeout, + blocking_timeout=5, + ) + + +@sentry_sdk.trace +def perform_report_merging( + report_service: ReportService, + commit_yaml: UserYaml, + commit: Commit, + processing_results: list[ProcessingResult], +) -> Report: + master_report = report_service.get_existing_report_for_commit(commit) + if master_report is None: + master_report = Report() + + upload_ids = [ + upload["upload_id"] for upload in processing_results if upload["successful"] + ] + intermediate_reports = load_intermediate_reports(upload_ids) + + master_report, merge_result = merge_reports( + commit_yaml, master_report, intermediate_reports + ) + + # Update the `Upload` in the database with the final session_id + # (aka `order_number`) and other statuses + update_uploads( + commit.get_db_session(), + commit_yaml, + processing_results, + intermediate_reports, + merge_result, + ) + + return master_report + + +@sentry_sdk.trace +@cache.cache_function(ttl=60 * 60) # the commit diff is immutable +def load_commit_diff(commit: Commit, task_name: str | None = None) -> dict | None: + repository = commit.repository + commitid = commit.commitid + try: + installation_name_to_use = ( + get_installation_name_for_owner_for_task(task_name, repository.owner) + if task_name + else GITHUB_APP_INSTALLATION_DEFAULT_NAME + ) + repository_service = get_repo_provider_service( + repository, installation_name_to_use=installation_name_to_use + ) + return async_to_sync(repository_service.get_commit_diff)(commitid) + + # TODO(swatinem): can we maybe get rid of all this logging? + except TorngitError: + # When this happens, we have that commit.totals["diff"] is not available. + # Since there is no way to calculate such diff without the git commit, + # then we assume having the rest of the report saved there is better than the + # alternative of refusing an otherwise "good" report because of the lack of diff + log.warning( + "Could not apply diff to report because there was an error fetching diff from provider", + exc_info=True, + ) + except RepositoryWithoutValidBotError: + save_commit_error( + commit, + error_code=CommitErrorTypes.REPO_BOT_INVALID.value, + ) + + log.warning( + "Could not apply diff to report because there is no valid bot found for that repo", + exc_info=True, + ) + + return None diff --git a/apps/worker/tasks/upload_processor.py b/apps/worker/tasks/upload_processor.py new file mode 100644 index 0000000000..8f8d5b03b9 --- /dev/null +++ b/apps/worker/tasks/upload_processor.py @@ -0,0 +1,86 @@ +import logging + +from shared.celery_config import upload_processor_task_name +from shared.config import get_config +from shared.yaml import UserYaml +from sqlalchemy.orm import Session as DbSession + +from app import celery_app +from services.processing.processing import UploadArguments, process_upload +from services.report import ProcessingError +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + +MAX_RETRIES = 5 +FIRST_RETRY_DELAY = 20 + + +def UPLOAD_PROCESSING_LOCK_NAME(repoid: int, commitid: str) -> str: + """The upload_processing_lock. + Only a single processing task may possess this lock at a time, because merging + reports requires exclusive access to the report. + + This is used by the Upload, Notify and UploadCleanLabelsIndex tasks as well to + verify if an upload for the commit is currently being processed. + """ + return f"upload_processing_lock_{repoid}_{commitid}" + + +class UploadProcessorTask(BaseCodecovTask, name=upload_processor_task_name): + """This is the second task of the series of tasks designed to process an `upload` made + by the user + + To see more about the whole picture, see `tasks.upload.UploadTask` + + This task processes each user `upload`, and saves the results to db and minio storage + + The steps are: + - Fetching the user uploaded report (from minio, or sometimes redis) + - Running them through the language processors, and obtaining reports from that + - Merging the generated reports to the already existing commit processed reports + - Saving all that info to the database + + This task doesn't limit how many individual reports it receives for processing. It deals + with as many as possible. But it is not expected that this task will receive a big + number of `uploads` to be processed + """ + + acks_late = get_config("setup", "tasks", "upload", "acks_late", default=False) + + def run_impl( + self, + db_session: DbSession, + *args, + repoid: int, + commitid: str, + commit_yaml: dict, + arguments: UploadArguments, + **kwargs, + ): + log.info( + "Received upload processor task", + extra={"arguments": arguments, "commit_yaml": commit_yaml}, + ) + + def on_processing_error(error: ProcessingError): + # the error is only retried on the first pass + if error.is_retryable and self.request.retries == 0: + log.info( + "Scheduling a retry due to retryable error", + extra={"error": error.as_dict()}, + ) + self.retry(max_retries=MAX_RETRIES, countdown=FIRST_RETRY_DELAY) + + return process_upload( + on_processing_error, + db_session, + int(repoid), + commitid, + UserYaml(commit_yaml), + arguments, + ) + + +RegisteredUploadTask = celery_app.register_task(UploadProcessorTask()) +upload_processor_task = celery_app.tasks[RegisteredUploadTask.name] diff --git a/apps/worker/tasks/upsert_component.py b/apps/worker/tasks/upsert_component.py new file mode 100644 index 0000000000..267b0f4b3d --- /dev/null +++ b/apps/worker/tasks/upsert_component.py @@ -0,0 +1,52 @@ +import logging + +from shared.reports.readonly import ReadOnlyReport +from shared.utils.enums import TaskConfigGroup +from sqlalchemy.orm import Session + +from app import celery_app +from database.models import Commit +from services.report import ReportService +from services.timeseries import ComponentForMeasurement, upsert_components_measurements +from services.yaml import get_repo_yaml +from tasks.base import BaseCodecovTask + +log = logging.getLogger(__name__) + +task_name = f"app.tasks.{TaskConfigGroup.timeseries.value}.UpsertComponentTask" + + +class UpsertComponentTask(BaseCodecovTask, name=task_name): + def run_impl( + self, + db_session: Session, + commitid: str, + repoid: int, + component_id: str, + flags: list[str], + paths: list[str], + *args, + **kwargs, + ): + log.info("Upserting component", extra=dict(commitid=commitid, repoid=repoid)) + + commit = ( + db_session.query(Commit) + .filter(Commit.repoid == repoid, Commit.commitid == commitid) + .first() + ) + + current_yaml = get_repo_yaml(commit.repository) + report_service = ReportService(current_yaml) + report = report_service.get_existing_report_for_commit( + commit, report_class=ReadOnlyReport + ) + assert report, "expected a `Report` to exist" + + upsert_components_measurements( + commit, report, [ComponentForMeasurement(component_id, flags, paths)] + ) + + +registered_task = celery_app.register_task(UpsertComponentTask()) +upsert_component_task = celery_app.tasks[registered_task.name] diff --git a/apps/worker/templates/auto-refund.html b/apps/worker/templates/auto-refund.html new file mode 100644 index 0000000000..8504981ee2 --- /dev/null +++ b/apps/worker/templates/auto-refund.html @@ -0,0 +1,25 @@ + + +{% extends "base.html" %} {% block main %} +

    Sorry to see you go, {{ name }}

    +
    + + + + +

    + A ${{ amount }} auto refund has been processed and will be credited to your {% if card_type %}{{ card_type | capitalize }}{% else %}card{% endif %}{% if last_four %} ending in {{ last_four }}{% endif %} shortly. +

    +
    +

    + Your subscription has been successfully canceled. Your account has been returned to our one-seat developer plan. We appreciate your business and welcome you back anytime. +

    +View invoice +

    + If you have any questions or need help, please contact us at + support@codecov.io +

    +{% endblock %} {% block aside %} +

    Total: USD ${{ amount }}

    +

    {{ date }}

    +{% endblock %} diff --git a/apps/worker/templates/auto-refund.txt b/apps/worker/templates/auto-refund.txt new file mode 100644 index 0000000000..8ad0b20e64 --- /dev/null +++ b/apps/worker/templates/auto-refund.txt @@ -0,0 +1,5 @@ +Your subscription has been successfully canceled. Your account has been returned to our one-seat developer plan. We appreciate your business and welcome you back anytime. + +A ${{ amount }} auto refund has been processed and will be credited to your {% if card_type %}{{ card_type | capitalize }}{% else %}card{% endif %}{% if last_four %} ending in {{ last_four }}{% endif %} shortly. + +If you have any questions or need help, please contact us at support@codecov.io diff --git a/apps/worker/templates/base.html b/apps/worker/templates/base.html new file mode 100644 index 0000000000..ef43d8218f --- /dev/null +++ b/apps/worker/templates/base.html @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + +
    + + Codecov logo + + +

    + Codecov +

    +
    +
    +
    + + {% block main %}{% endblock %} + +
    + + + + + + + + + + + + + +
    +

    + Codecov +

    +

    + Functional Software, dba Sentry +

    +

    + 45 Fremont St. 8th Floor, San Francisco, CA 94105 +

    +
    + + + + + + + + + + + + + + + + + + + + +
    +
    + + diff --git a/apps/worker/templates/failed-payment.html b/apps/worker/templates/failed-payment.html new file mode 100644 index 0000000000..626be55735 --- /dev/null +++ b/apps/worker/templates/failed-payment.html @@ -0,0 +1,101 @@ +{% extends "base.html" %} {% block main %} +

    + Oops! Let's fix this, {{ name }} +

    +

    + Total: USD + ${{ amount }} + | {{ date }} +

    +
    +

    + Your ${{ amount }} payment to Functional Software, Inc, dba + Sentry has failed +

    +
    +

    + We were unable to process your {% if card_type %}{{ card_type | + capitalize }}{% else %}card{% endif %}{% if last_four %} ending in + {{ last_four }}{% endif %}. Please take a moment to double check + your payment information to ensure your account continues to run + smoothly. +

    +

    + Update your billing info +

    +
    +

    Why did the payment fail?

    +

    In most cases, a payment might fail due to:

    +
      +
    • An expired card
    • +
    • Insufficient funds
    • +
    • Requiring additional authentication
    • +
    +

    + If you have any questions or need help, please contact us at + support@codecov.io +

    +
    +{% endblock %} diff --git a/apps/worker/templates/failed-payment.txt b/apps/worker/templates/failed-payment.txt new file mode 100644 index 0000000000..7110ed0f3e --- /dev/null +++ b/apps/worker/templates/failed-payment.txt @@ -0,0 +1,10 @@ +Your ${{ amount }} payment to Functional Software, Inc, dba Sentry has failed. + +We were unable to process your {% if card_type %}{{ card_type | capitalize }}{% else %}card{% endif %}{% if last_four %} ending in {{ last_four }}{% endif %}. Please take a moment to double check your payment information to ensure your account continues to run smoothly. + +In most cases, a payment might fail due to: +- An expired card +- Insufficient funds +- Requiring additional authentication + +If you have any questions or need help, please contact us at support@codecov.io diff --git a/apps/worker/templates/success-after-failed-payment.html b/apps/worker/templates/success-after-failed-payment.html new file mode 100644 index 0000000000..aad9203dd9 --- /dev/null +++ b/apps/worker/templates/success-after-failed-payment.html @@ -0,0 +1,80 @@ +{% extends "base.html" %} {% block main %} +

    + You're all set! +

    +

    + Total: USD + ${{ amount }} + | {{ date }} +

    +
    +

    + Your ${{ amount }} payment to Functional Software, Inc, dba + Sentry has been received +

    +
    +

    + To view your recent payment, click the link below. Thank you for choosing Codecov. +

    +

    + View invoice +

    +

    + If you have any questions or need help, please contact us at + support@codecov.io +

    +{% endblock %} diff --git a/apps/worker/templates/success-after-failed-payment.txt b/apps/worker/templates/success-after-failed-payment.txt new file mode 100644 index 0000000000..869b1e9a74 --- /dev/null +++ b/apps/worker/templates/success-after-failed-payment.txt @@ -0,0 +1,7 @@ +Your ${{ amount }} payment to Functional Software, Inc, dba Sentry has been received. + +To view your recent payment, click the link below. Thank you for choosing Codecov. + +{{ cta_link }} + +If you have any questions or need help, please contact us at support@codecov.io diff --git a/apps/worker/templates/test.html b/apps/worker/templates/test.html new file mode 100644 index 0000000000..7d05a97c52 --- /dev/null +++ b/apps/worker/templates/test.html @@ -0,0 +1,16 @@ + + + + + + + Document + + + +

    + test template {{ username }} +

    + + + \ No newline at end of file diff --git a/apps/worker/templates/test.txt b/apps/worker/templates/test.txt new file mode 100644 index 0000000000..65033ccec0 --- /dev/null +++ b/apps/worker/templates/test.txt @@ -0,0 +1 @@ +Test template {{ username }} \ No newline at end of file diff --git a/apps/worker/test_utils/base.py b/apps/worker/test_utils/base.py new file mode 100644 index 0000000000..d17e7c710a --- /dev/null +++ b/apps/worker/test_utils/base.py @@ -0,0 +1,49 @@ +import dataclasses +from json import loads + +from services.report import legacy_totals + + +class BaseTestCase(object): + def convert_report_to_better_readable(self, report): + report_json, _chunks, _totals = report.serialize() + + totals_dict = legacy_totals(report) + report_dict = loads(report_json) + report_dict.pop("totals") + archive_dict = {} + for filename in report.files: + file_report = report.get(filename) + lines = [] + for line_number, line in file_report.lines: + ( + coverage, + line_type, + sessions, + messages, + complexity, + datapoints, + ) = dataclasses.astuple(line) + sessions = [list(s) for s in sessions] + lines.append( + ( + line_number, + coverage, + line_type, + sessions, + messages, + complexity, + datapoints, + ) + if datapoints is not None + else ( + line_number, + coverage, + line_type, + sessions, + messages, + complexity, + ) + ) + archive_dict[filename] = lines + return {"archive": archive_dict, "report": report_dict, "totals": totals_dict} diff --git a/apps/worker/tests/__init__.py b/apps/worker/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/worker/tests/helpers.py b/apps/worker/tests/helpers.py new file mode 100644 index 0000000000..be3df03f2e --- /dev/null +++ b/apps/worker/tests/helpers.py @@ -0,0 +1,165 @@ +from shared.django_apps.codecov_auth.models import BillingRate +from shared.django_apps.codecov_auth.tests.factories import PlanFactory, TierFactory +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName, PlanPrice, TierName + + +def mock_all_plans_and_tiers(): + trial_tier = TierFactory(tier_name=TierName.TRIAL.value) + PlanFactory( + tier=trial_tier, + name=PlanName.TRIAL_PLAN_NAME.value, + paid_plan=False, + marketing_name="Developer", + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + ) + + basic_tier = TierFactory(tier_name=TierName.BASIC.value) + PlanFactory( + name=DEFAULT_FREE_PLAN, + tier=basic_tier, + marketing_name="Developer", + benefits=[ + "Up to 1 user", + "Unlimited public repositories", + "Unlimited private repositories", + ], + monthly_uploads_limit=250, + ) + pro_tier = TierFactory(tier_name=TierName.PRO.value) + PlanFactory( + name=PlanName.CODECOV_PRO_MONTHLY.value, + tier=pro_tier, + marketing_name="Pro", + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + billing_rate=BillingRate.MONTHLY.value, + base_unit_price=PlanPrice.MONTHLY.value, + paid_plan=True, + ) + PlanFactory( + name=PlanName.CODECOV_PRO_YEARLY.value, + tier=pro_tier, + marketing_name="Pro", + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + billing_rate=BillingRate.ANNUALLY.value, + base_unit_price=PlanPrice.YEARLY.value, + paid_plan=True, + ) + PlanFactory( + name=PlanName.CODECOV_PRO_MONTHLY_LEGACY.value, + tier=pro_tier, + marketing_name="Pro", + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + ) + PlanFactory( + name=PlanName.CODECOV_PRO_YEARLY_LEGACY.value, + tier=pro_tier, + marketing_name="Pro", + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + ) + + team_tier = TierFactory(tier_name=TierName.TEAM.value) + PlanFactory( + name=PlanName.TEAM_MONTHLY.value, + tier=team_tier, + marketing_name="Team", + benefits=[ + "Up to 10 users", + "Unlimited repositories", + "2500 private repo uploads", + "Patch coverage analysis", + ], + billing_rate=BillingRate.MONTHLY.value, + base_unit_price=PlanPrice.TEAM_MONTHLY.value, + monthly_uploads_limit=2500, + paid_plan=True, + ) + PlanFactory( + name=PlanName.TEAM_YEARLY.value, + tier=team_tier, + marketing_name="Team", + benefits=[ + "Up to 10 users", + "Unlimited repositories", + "2500 private repo uploads", + "Patch coverage analysis", + ], + billing_rate=BillingRate.ANNUALLY.value, + base_unit_price=PlanPrice.TEAM_YEARLY.value, + monthly_uploads_limit=2500, + paid_plan=True, + ) + + sentry_tier = TierFactory(tier_name=TierName.SENTRY.value) + PlanFactory( + name=PlanName.SENTRY_MONTHLY.value, + tier=sentry_tier, + marketing_name="Sentry Pro", + billing_rate=BillingRate.MONTHLY.value, + base_unit_price=PlanPrice.MONTHLY.value, + paid_plan=True, + benefits=[ + "Includes 5 seats", + "$12 per additional seat", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + ) + PlanFactory( + name=PlanName.SENTRY_YEARLY.value, + tier=sentry_tier, + marketing_name="Sentry Pro", + billing_rate=BillingRate.ANNUALLY.value, + base_unit_price=PlanPrice.YEARLY.value, + paid_plan=True, + benefits=[ + "Includes 5 seats", + "$10 per additional seat", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + ) + + enterprise_tier = TierFactory(tier_name=TierName.ENTERPRISE.value) + PlanFactory( + name=PlanName.ENTERPRISE_CLOUD_MONTHLY.value, + tier=enterprise_tier, + marketing_name="Enterprise", + billing_rate=BillingRate.MONTHLY.value, + base_unit_price=PlanPrice.MONTHLY.value, + paid_plan=True, + ) + PlanFactory( + name=PlanName.ENTERPRISE_CLOUD_YEARLY.value, + tier=enterprise_tier, + marketing_name="Enterprise", + billing_rate=BillingRate.ANNUALLY.value, + base_unit_price=PlanPrice.YEARLY.value, + paid_plan=True, + ) diff --git a/apps/worker/tests/services/notification/notifiers/comment/test_conditions.py b/apps/worker/tests/services/notification/notifiers/comment/test_conditions.py new file mode 100644 index 0000000000..34008a198f --- /dev/null +++ b/apps/worker/tests/services/notification/notifiers/comment/test_conditions.py @@ -0,0 +1,42 @@ +import unittest +from unittest.mock import MagicMock, patch + +from services.comparison import ComparisonProxy +from services.notification.notifiers.base import AbstractBaseNotifier +from services.notification.notifiers.comment.conditions import ( + HasEnoughRequiredChanges, +) + + +class TestHasEnoughRequiredChanges(unittest.TestCase): + def setUp(self): + self.notifier = MagicMock(spec=AbstractBaseNotifier) + self.comparison = MagicMock(spec=ComparisonProxy) + + def test_check_coverage_change_with_none_diff(self): + self.comparison.get_diff.return_value = None + result = HasEnoughRequiredChanges._check_coverage_change(self.comparison) + self.assertFalse(result) + self.comparison.get_diff.assert_called_once() + self.comparison.head.report.calculate_diff.assert_not_called() + + def test_check_uncovered_patch_with_none_diff(self): + self.comparison.get_diff.return_value = None + result = HasEnoughRequiredChanges._check_uncovered_patch(self.comparison) + self.assertFalse(result) + self.comparison.get_diff.assert_called_once_with(use_original_base=True) + self.comparison.head.report.apply_diff.assert_not_called() + + def test_check_any_change_with_none_diff(self): + self.comparison.get_diff.return_value = None + + # Mock _check_unexpected_changes to return False + with patch.object( + HasEnoughRequiredChanges, "_check_unexpected_changes", return_value=False + ): + # Act + result = HasEnoughRequiredChanges._check_any_change(self.comparison) + + # Assert + self.assertFalse(result) + self.comparison.get_diff.assert_called_once() diff --git a/apps/worker/tests/test_debug.py b/apps/worker/tests/test_debug.py new file mode 100644 index 0000000000..b067052609 --- /dev/null +++ b/apps/worker/tests/test_debug.py @@ -0,0 +1,51 @@ +import orjson +import pytest +from shared.reports.resources import Report +from shared.utils.sessions import Session + +from services.report import raw_upload_processor as process +from services.report.parser.legacy import LegacyReportParser + +# The intention of these tests is to easily reproduce production problems with real reports. +# +# In order to run them, comment out the `skip` annotation. +# +# For both tests, download the raw upload, or the `report_json`/`chunks` from storage, +# and paste in the filename before running the test directly. +# +# As these tests do not depend on any external service, you can run them directly with: +# > pytest -vvs "tests/test_debug.py::test_process_upload" +# +# Then either hook up an interactive debugger, or do "print-debugging" to your liking. + + +@pytest.mark.skip(reason="this is supposed to be invoked manually") +def test_process_upload(): + upload_file = "..." + with open(upload_file, "rb") as d: + contents = d.read() + + parsed_upload = LegacyReportParser().parse_raw_report_from_bytes(contents) + report = process.process_raw_upload(None, parsed_upload, Session()) + + file = report.get("interesting_file") + + +@pytest.mark.skip(reason="this is supposed to be invoked manually") +def test_inspect_report(): + report_json_file = "..." + chunks_file = "..." + with open(report_json_file, "rb") as d: + report_json = d.read() + with open(chunks_file, "rb") as d: + chunks = d.read() + + report_json = orjson.loads(report_json) + report = Report.from_chunks( + chunks=chunks, + files=report_json["files"], + sessions=report_json["sessions"], + totals=report_json.get("totals"), + ) + + file = report.get("interesting_file") diff --git a/apps/worker/tests/unit/test_main.py b/apps/worker/tests/unit/test_main.py new file mode 100644 index 0000000000..4ad629e8ee --- /dev/null +++ b/apps/worker/tests/unit/test_main.py @@ -0,0 +1,342 @@ +import os +import sys +from unittest import mock + +from click.testing import CliRunner +from shared.celery_config import BaseCeleryConfig + +from main import _get_queues_param_from_queue_input, cli, main, setup_worker, test, web + + +def test_get_queues_param_from_queue_input(): + assert ( + _get_queues_param_from_queue_input(["worker,profiling,notify"]) + == f"worker,profiling,notify,enterprise_worker,enterprise_profiling,enterprise_notify,{BaseCeleryConfig.health_check_default_queue}" + ) + assert ( + _get_queues_param_from_queue_input(["worker", "profiling", "notify"]) + == f"worker,profiling,notify,enterprise_worker,enterprise_profiling,enterprise_notify,{BaseCeleryConfig.health_check_default_queue}" + ) + + +@mock.patch("main.startup_license_logging") +@mock.patch("main.start_prometheus") +def test_run_empty_config( + mock_prometheus, mock_license_logging, mock_storage, mock_configuration +): + assert not mock_storage.root_storage_created + res = setup_worker() + assert res is None + assert not mock_storage.root_storage_created + assert mock_storage.config == {} + mock_license_logging.assert_called_once() + + +@mock.patch("main.startup_license_logging") +@mock.patch("main.start_prometheus") +def test_sys_path_append_on_enterprise( + mock_prometheus, mock_license_logging, mock_storage, mock_configuration +): + sys.frozen = True + res = setup_worker() + assert res is None + assert "./external_deps" in sys.path + mock_license_logging.assert_called_once() + + +@mock.patch("main.startup_license_logging") +@mock.patch("main.start_prometheus") +def test_run_already_existing_root_storage( + mock_prometheus, mock_license_logging, mock_storage, mock_configuration +): + mock_storage.root_storage_created = True + res = setup_worker() + assert res is None + assert mock_storage.config == {} + assert mock_storage.root_storage_created + mock_license_logging.assert_called_once() + + +@mock.patch("main.startup_license_logging") +@mock.patch("main.start_prometheus") +def test_get_cli_help(mocker, mock_license_logging): + runner = CliRunner() + res = runner.invoke(cli, ["--help"]) + expected_output = "\n".join( + [ + "Usage: cli [OPTIONS] COMMAND [ARGS]...", + "", + "Options:", + " --help Show this message and exit.", + "", + "Commands:", + " test", + " web", + " worker", + "", + ] + ) + + assert res.output == expected_output + mock_license_logging.assert_not_called() + + +@mock.patch("main.startup_license_logging") +@mock.patch("main.start_prometheus") +def test_deal_unsupported_commands(mocker, mock_license_logging): + runner = CliRunner() + test_res = runner.invoke(test, []) + assert test_res.output == "Error: System not suitable to run TEST mode\n" + web_res = runner.invoke(web, []) + assert web_res.output == "Error: System not suitable to run WEB mode\n" + mock_license_logging.assert_not_called() + + +@mock.patch("main.startup_license_logging") +@mock.patch("main.start_prometheus") +def test_deal_worker_command_default( + mock_prometheus, mock_license_logging, mocker, mock_storage +): + mocker.patch.dict(os.environ, {"HOSTNAME": "simpleworker"}) + mocked_get_current_version = mocker.patch( + "main.get_current_version", return_value="some_version_12.3" + ) + mock_app = mocker.patch("main.app") + runner = CliRunner() + res = runner.invoke(cli, ["worker"]) + expected_output = "\n".join( + [ + "", + " _____ _", + " / ____| | |", + "| | ___ __| | ___ ___ _____ __", + "| | / _ \\ / _` |/ _ \\/ __/ _ \\ \\ / /", + "| |___| (_) | (_| | __/ (_| (_) \\ V /", + " \\_____\\___/ \\__,_|\\___|\\___\\___/ \\_/", + " some_version_12.3", + "", + "", + "", + ] + ) + assert res.output == expected_output + mocked_get_current_version.assert_called_with() + mock_app.celery_app.worker_main.assert_called_with( + argv=[ + "worker", + "-n", + "simpleworker", + "-c", + 2, + "-l", + "info", + "-Q", + f"celery,enterprise_celery,{BaseCeleryConfig.health_check_default_queue}", + "-B", + "-s", + "/home/codecov/celerybeat-schedule", + ] + ) + mock_license_logging.assert_called_once() + + +@mock.patch("main.startup_license_logging") +@mock.patch("main.start_prometheus") +def test_deal_worker_command( + mock_prometheus, mock_license_logging, mocker, mock_storage +): + mocker.patch.dict(os.environ, {"HOSTNAME": "simpleworker"}) + mocked_get_current_version = mocker.patch( + "main.get_current_version", return_value="some_version_12.3" + ) + mock_app = mocker.patch("main.app") + runner = CliRunner() + res = runner.invoke(cli, ["worker", "--queue", "simple,one,two", "--queue", "some"]) + expected_output = "\n".join( + [ + "", + " _____ _", + " / ____| | |", + "| | ___ __| | ___ ___ _____ __", + "| | / _ \\ / _` |/ _ \\/ __/ _ \\ \\ / /", + "| |___| (_) | (_| | __/ (_| (_) \\ V /", + " \\_____\\___/ \\__,_|\\___|\\___\\___/ \\_/", + " some_version_12.3", + "", + "", + "", + ] + ) + assert res.output == expected_output + mocked_get_current_version.assert_called_with() + mock_app.celery_app.worker_main.assert_called_with( + argv=[ + "worker", + "-n", + "simpleworker", + "-c", + 2, + "-l", + "info", + "-Q", + f"simple,one,two,some,enterprise_simple,enterprise_one,enterprise_two,enterprise_some,{BaseCeleryConfig.health_check_default_queue}", + "-B", + "-s", + "/home/codecov/celerybeat-schedule", + ] + ) + mock_license_logging.assert_called_once() + + +@mock.patch("main.startup_license_logging") +@mock.patch("main.start_prometheus") +def test_deal_worker_no_beat( + mock_prometheus, mock_license_logging, mocker, mock_storage, empty_configuration +): + mocker.patch.dict( + os.environ, {"HOSTNAME": "simpleworker", "SETUP__CELERY_BEAT_ENABLED": "False"} + ) + mocked_get_current_version = mocker.patch( + "main.get_current_version", return_value="some_version_12.3" + ) + mock_app = mocker.patch("main.app") + runner = CliRunner() + res = runner.invoke(cli, ["worker", "--queue", "simple,one,two", "--queue", "some"]) + expected_output = "\n".join( + [ + "", + " _____ _", + " / ____| | |", + "| | ___ __| | ___ ___ _____ __", + "| | / _ \\ / _` |/ _ \\/ __/ _ \\ \\ / /", + "| |___| (_) | (_| | __/ (_| (_) \\ V /", + " \\_____\\___/ \\__,_|\\___|\\___\\___/ \\_/", + " some_version_12.3", + "", + "", + "", + ] + ) + assert res.output == expected_output + mocked_get_current_version.assert_called_with() + mock_app.celery_app.worker_main.assert_called_with( + argv=[ + "worker", + "-n", + "simpleworker", + "-c", + 2, + "-l", + "info", + "-Q", + f"simple,one,two,some,enterprise_simple,enterprise_one,enterprise_two,enterprise_some,{BaseCeleryConfig.health_check_default_queue}", + ] + ) + mock_license_logging.assert_called_once() + + +@mock.patch("main.startup_license_logging") +@mock.patch("main.start_prometheus") +def test_deal_worker_no_queues( + mock_prometheus, mock_license_logging, mocker, mock_storage, empty_configuration +): + mocker.patch.dict( + os.environ, + {"HOSTNAME": "simpleworker", "SETUP__CELERY_QUEUES_ENABLED": "False"}, + ) + mocked_get_current_version = mocker.patch( + "main.get_current_version", return_value="some_version_12.3" + ) + mock_app = mocker.patch("main.app") + runner = CliRunner() + res = runner.invoke(cli, ["worker", "--queue", "simple,one,two", "--queue", "some"]) + expected_output = "\n".join( + [ + "", + " _____ _", + " / ____| | |", + "| | ___ __| | ___ ___ _____ __", + "| | / _ \\ / _` |/ _ \\/ __/ _ \\ \\ / /", + "| |___| (_) | (_| | __/ (_| (_) \\ V /", + " \\_____\\___/ \\__,_|\\___|\\___\\___/ \\_/", + " some_version_12.3", + "", + "", + "", + ] + ) + assert res.output == expected_output + mocked_get_current_version.assert_called_with() + mock_app.celery_app.worker_main.assert_called_with( + argv=[ + "worker", + "-n", + "simpleworker", + "-c", + 2, + "-l", + "info", + "-B", + "-s", + "/home/codecov/celerybeat-schedule", + ] + ) + mock_license_logging.assert_called_once() + + +@mock.patch("main.startup_license_logging") +@mock.patch("main.start_prometheus") +def test_deal_worker_no_queues_or_beat( + mock_prometheus, mock_license_logging, mocker, mock_storage, empty_configuration +): + env = { + "HOSTNAME": "simpleworker", + "SETUP__CELERY_QUEUES_ENABLED": "False", + "SETUP__CELERY_BEAT_ENABLED": "False", + } + mocked_get_current_version = mocker.patch( + "main.get_current_version", return_value="some_version_12.3" + ) + mock_app = mocker.patch("main.app") + runner = CliRunner() + res = runner.invoke( + cli, ["worker", "--queue", "simple,one,two", "--queue", "some"], env=env + ) + expected_output = "\n".join( + [ + "", + " _____ _", + " / ____| | |", + "| | ___ __| | ___ ___ _____ __", + "| | / _ \\ / _` |/ _ \\/ __/ _ \\ \\ / /", + "| |___| (_) | (_| | __/ (_| (_) \\ V /", + " \\_____\\___/ \\__,_|\\___|\\___\\___/ \\_/", + " some_version_12.3", + "", + "", + "", + ] + ) + assert res.output == expected_output + mocked_get_current_version.assert_called_with() + mock_app.celery_app.worker_main.assert_called_with( + argv=[ + "worker", + "-n", + "simpleworker", + "-c", + 2, + "-l", + "info", + ] + ) + mock_license_logging.assert_called_once() + + +@mock.patch("main.startup_license_logging") +@mock.patch("main.start_prometheus") +def test_main(mock_prometheus, mock_license_logging, mocker): + mock_cli = mocker.patch("main.cli") + assert main() is None + mock_cli.assert_called_with(obj={}) + mock_license_logging.assert_not_called() diff --git a/apps/worker/tests/unit/test_task_router.py b/apps/worker/tests/unit/test_task_router.py new file mode 100644 index 0000000000..26f86861fc --- /dev/null +++ b/apps/worker/tests/unit/test_task_router.py @@ -0,0 +1,270 @@ +import pytest +import shared.celery_config as shared_celery_config +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName + +from celery_task_router import ( + _get_user_plan_from_comparison_id, + _get_user_plan_from_label_request_id, + _get_user_plan_from_org_ownerid, + _get_user_plan_from_ownerid, + _get_user_plan_from_repoid, + _get_user_plan_from_suite_id, + _get_user_plan_from_task, + route_task, +) +from database.tests.factories.core import ( + CommitFactory, + CompareCommitFactory, + OwnerFactory, + RepositoryFactory, +) +from database.tests.factories.labelanalysis import LabelAnalysisRequestFactory +from database.tests.factories.staticanalysis import StaticAnalysisSuiteFactory + + +@pytest.fixture +def fake_owners(dbsession): + owner = OwnerFactory.create(plan=PlanName.CODECOV_PRO_MONTHLY.value) + owner_enterprise_cloud = OwnerFactory.create( + plan=PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + dbsession.add(owner) + dbsession.add(owner_enterprise_cloud) + dbsession.flush() + return (owner, owner_enterprise_cloud) + + +@pytest.fixture +def fake_repos(dbsession, fake_owners): + (owner, owner_enterprise_cloud) = fake_owners + repo = RepositoryFactory.create(owner=owner) + repo_enterprise_cloud = RepositoryFactory.create(owner=owner_enterprise_cloud) + dbsession.add(repo) + dbsession.add(repo_enterprise_cloud) + dbsession.flush() + return (repo, repo_enterprise_cloud) + + +@pytest.fixture +def fake_comparison_commit(dbsession, fake_repos): + (repo, repo_enterprise_cloud) = fake_repos + + commmit = CommitFactory.create(repository=repo) + commmit_enterprise = CommitFactory.create(repository=repo_enterprise_cloud) + dbsession.add(commmit) + dbsession.add(commmit_enterprise) + dbsession.flush() + compare_commit = CompareCommitFactory(compare_commit=commmit) + compare_commit_enterprise = CompareCommitFactory(compare_commit=commmit_enterprise) + dbsession.add(compare_commit) + dbsession.add(compare_commit_enterprise) + dbsession.flush() + return (compare_commit, compare_commit_enterprise) + + +@pytest.fixture +def fake_label_analysis_request(dbsession, fake_repos): + (repo, repo_enterprise_cloud) = fake_repos + + commmit = CommitFactory.create(repository=repo) + commmit_enterprise = CommitFactory.create(repository=repo_enterprise_cloud) + dbsession.add(commmit) + dbsession.add(commmit_enterprise) + dbsession.flush() + label_analysis_request = LabelAnalysisRequestFactory(head_commit=commmit) + label_analysis_request_enterprise = LabelAnalysisRequestFactory( + head_commit=commmit_enterprise + ) + dbsession.add(label_analysis_request) + dbsession.add(label_analysis_request_enterprise) + dbsession.flush() + return (label_analysis_request, label_analysis_request_enterprise) + + +@pytest.fixture +def fake_static_analysis_suite(dbsession, fake_repos): + (repo, repo_enterprise_cloud) = fake_repos + + commmit = CommitFactory.create(repository=repo) + commmit_enterprise = CommitFactory.create(repository=repo_enterprise_cloud) + dbsession.add(commmit) + dbsession.add(commmit_enterprise) + dbsession.flush() + static_analysis_suite = StaticAnalysisSuiteFactory(commit=commmit) + static_analysis_suite_enterprise = StaticAnalysisSuiteFactory( + commit=commmit_enterprise + ) + dbsession.add(static_analysis_suite) + dbsession.add(static_analysis_suite_enterprise) + dbsession.flush() + return (static_analysis_suite, static_analysis_suite_enterprise) + + +def test_get_owner_plan_from_id(dbsession, fake_owners): + (owner, owner_enterprise_cloud) = fake_owners + assert ( + _get_user_plan_from_ownerid(dbsession, owner.ownerid) + == PlanName.CODECOV_PRO_MONTHLY.value + ) + assert ( + _get_user_plan_from_ownerid(dbsession, owner_enterprise_cloud.ownerid) + == PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + assert _get_user_plan_from_ownerid(dbsession, 10000000) == DEFAULT_FREE_PLAN + + +def test_get_user_plan_from_org_ownerid(dbsession, fake_owners): + (owner, owner_enterprise_cloud) = fake_owners + assert ( + _get_user_plan_from_org_ownerid(dbsession, owner.ownerid) + == PlanName.CODECOV_PRO_MONTHLY.value + ) + assert ( + _get_user_plan_from_org_ownerid(dbsession, owner_enterprise_cloud.ownerid) + == PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + + +def test_get_owner_plan_from_repoid(dbsession, fake_repos): + (repo, repo_enterprise_cloud) = fake_repos + assert ( + _get_user_plan_from_repoid(dbsession, repo.repoid) + == PlanName.CODECOV_PRO_MONTHLY.value + ) + assert ( + _get_user_plan_from_repoid(dbsession, repo_enterprise_cloud.repoid) + == PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + assert _get_user_plan_from_repoid(dbsession, 10000000) == DEFAULT_FREE_PLAN + + +def test_get_user_plan_from_comparison_id(dbsession, fake_comparison_commit): + (compare_commit, compare_commit_enterprise) = fake_comparison_commit + assert ( + _get_user_plan_from_comparison_id(dbsession, comparison_id=compare_commit.id) + == PlanName.CODECOV_PRO_MONTHLY.value + ) + assert ( + _get_user_plan_from_comparison_id( + dbsession, comparison_id=compare_commit_enterprise.id + ) + == PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + assert _get_user_plan_from_comparison_id(dbsession, 10000000) == DEFAULT_FREE_PLAN + + +def test_get_user_plan_from_label_request_id(dbsession, fake_label_analysis_request): + ( + label_analysis_request, + label_analysis_request_enterprise, + ) = fake_label_analysis_request + assert ( + _get_user_plan_from_label_request_id( + dbsession, request_id=label_analysis_request.id + ) + == PlanName.CODECOV_PRO_MONTHLY.value + ) + assert ( + _get_user_plan_from_label_request_id( + dbsession, request_id=label_analysis_request_enterprise.id + ) + == PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + assert ( + _get_user_plan_from_label_request_id(dbsession, 10000000) == DEFAULT_FREE_PLAN + ) + + +def test_get_user_plan_from_static_analysis_suite( + dbsession, fake_static_analysis_suite +): + ( + static_analysis_suite, + static_analysis_suite_enterprise, + ) = fake_static_analysis_suite + assert ( + _get_user_plan_from_suite_id(dbsession, suite_id=static_analysis_suite.id) + == PlanName.CODECOV_PRO_MONTHLY.value + ) + assert ( + _get_user_plan_from_suite_id( + dbsession, suite_id=static_analysis_suite_enterprise.id + ) + == PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + assert _get_user_plan_from_suite_id(dbsession, 10000000) == DEFAULT_FREE_PLAN + + +def test_get_user_plan_from_task( + dbsession, + fake_repos, + fake_comparison_commit, +): + (repo, repo_enterprise_cloud) = fake_repos + compare_commit = fake_comparison_commit[0] + task_kwargs = dict(repoid=repo.repoid, commitid=0, debug=False, rebuild=False) + assert ( + _get_user_plan_from_task( + dbsession, shared_celery_config.upload_task_name, task_kwargs + ) + == PlanName.CODECOV_PRO_MONTHLY.value + ) + + task_kwargs = dict( + repoid=repo_enterprise_cloud.repoid, commitid=0, debug=False, rebuild=False + ) + assert ( + _get_user_plan_from_task( + dbsession, shared_celery_config.upload_task_name, task_kwargs + ) + == PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + + task_kwargs = dict(ownerid=repo.ownerid) + assert ( + _get_user_plan_from_task( + dbsession, shared_celery_config.delete_owner_task_name, task_kwargs + ) + == PlanName.CODECOV_PRO_MONTHLY.value + ) + + task_kwargs = dict(org_ownerid=repo.ownerid, user_ownerid=20) + assert ( + _get_user_plan_from_task( + dbsession, shared_celery_config.new_user_activated_task_name, task_kwargs + ) + == PlanName.CODECOV_PRO_MONTHLY.value + ) + + task_kwargs = dict(comparison_id=compare_commit.id) + assert ( + _get_user_plan_from_task( + dbsession, shared_celery_config.compute_comparison_task_name, task_kwargs + ) + == PlanName.CODECOV_PRO_MONTHLY.value + ) + + task_kwargs = dict( + repoid=repo_enterprise_cloud.repoid, commitid=0, debug=False, rebuild=False + ) + assert ( + _get_user_plan_from_task(dbsession, "unknown task", task_kwargs) + == DEFAULT_FREE_PLAN + ) + + +def test_route_task(mocker, dbsession, fake_repos): + mock_get_db_session = mocker.patch("celery_task_router.get_db_session") + mock_route_tasks_shared = mocker.patch( + "celery_task_router.route_tasks_based_on_user_plan" + ) + mock_get_db_session.return_value = dbsession + mock_route_tasks_shared.return_value = {"queue": "correct queue"} + repo = fake_repos[0] + task_kwargs = dict(repoid=repo.repoid, commitid=0, debug=False, rebuild=False) + response = route_task(shared_celery_config.upload_task_name, [], task_kwargs, {}) + assert response == {"queue": "correct queue"} + mock_get_db_session.assert_called() + mock_route_tasks_shared.assert_called_with( + shared_celery_config.upload_task_name, PlanName.CODECOV_PRO_MONTHLY.value + ) diff --git a/apps/worker/uv.lock b/apps/worker/uv.lock new file mode 100644 index 0000000000..01802ef5d1 --- /dev/null +++ b/apps/worker/uv.lock @@ -0,0 +1,2228 @@ +version = 1 +requires-python = "==3.13.*" +resolution-markers = [ + "platform_python_implementation != 'PyPy'", + "platform_python_implementation == 'PyPy'", +] + +[[package]] +name = "amplitude-analytics" +version = "1.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/e1/9af6812e54cc53e6be090d1324cf7b11ebe93f9613345959f16b4844fed3/amplitude-analytics-1.1.4.tar.gz", hash = "sha256:9f05dc461459cfef15df8795895971745193fb74ab4e8a561e96bb208f11860e", size = 21193 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/d9/9c1b286a2bb83d081b5f142e6992f7f0a8d7c229d10897c040988026e95e/amplitude_analytics-1.1.4-py3-none-any.whl", hash = "sha256:802d9b3a20d095d49074610dcd7e8834e5fcaff5c99d25d3153c03a163d73889", size = 24037 }, +] + +[[package]] +name = "amqp" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944 }, +] + +[[package]] +name = "analytics-python" +version = "1.3.0b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backoff" }, + { name = "monotonic" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/b0/192e68610548b2b06fe87a2adaf3c1589bf2257f9002b177778ec4d719bb/analytics-python-1.3.0b1.tar.gz", hash = "sha256:1110fc4da4611429af6aa76d9f4662e22fb624861f788615bafc4cc7e9263989", size = 13380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e8/816c1f8e0e54c0f1cbb3945e2f7037521aa2b9adacb279e3f275fcba5591/analytics_python-1.3.0b1-py2.py3-none-any.whl", hash = "sha256:82bac51c20afe7f6f65457b88c60da7eae791c4c7ab362b3b72ec030b2d0698b", size = 14887 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, +] + +[[package]] +name = "argon2-cffi" +version = "23.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/fa/57ec2c6d16ecd2ba0cf15f3c7d1c3c2e7b5fcb83555ff56d7ab10888ec8f/argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08", size = 42798 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea", size = 15124 }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", size = 1779911 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/13/838ce2620025e9666aa8f686431f67a29052241692a3dd1ae9d3692a89d3/argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", size = 29658 }, + { url = "https://files.pythonhosted.org/packages/b3/02/f7f7bb6b6af6031edb11037639c697b912e1dea2db94d436e681aea2f495/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", size = 80583 }, + { url = "https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", size = 86168 }, + { url = "https://files.pythonhosted.org/packages/74/f6/4a34a37a98311ed73bb80efe422fed95f2ac25a4cacc5ae1d7ae6a144505/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", size = 82709 }, + { url = "https://files.pythonhosted.org/packages/74/2b/73d767bfdaab25484f7e7901379d5f8793cccbb86c6e0cbc4c1b96f63896/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", size = 83613 }, + { url = "https://files.pythonhosted.org/packages/4f/fd/37f86deef67ff57c76f137a67181949c2d408077e2e3dd70c6c42912c9bf/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", size = 84583 }, + { url = "https://files.pythonhosted.org/packages/6f/52/5a60085a3dae8fded8327a4f564223029f5f54b0cb0455a31131b5363a01/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", size = 88475 }, + { url = "https://files.pythonhosted.org/packages/8b/95/143cd64feb24a15fa4b189a3e1e7efbaeeb00f39a51e99b26fc62fbacabd/argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", size = 27698 }, + { url = "https://files.pythonhosted.org/packages/37/2c/e34e47c7dee97ba6f01a6203e0383e15b60fb85d78ac9a15cd066f6fe28b/argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", size = 30817 }, + { url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104 }, +] + +[[package]] +name = "asgiref" +version = "3.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 }, +] + +[[package]] +name = "backoff" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/00/1aa1ffe4668ddee7a381144bcf953835500387301a3202465e023ea44bcb/backoff-1.6.0.tar.gz", hash = "sha256:e3df718a774c456a516f7c88516e47a9f2d02aa562943cdfa274c439e9dbbfde", size = 10474 } + +[[package]] +name = "billiard" +version = "4.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/58/1546c970afcd2a2428b1bfafecf2371d8951cc34b46701bea73f4280989e/billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f", size = 155031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/da/43b15f28fe5f9e027b41c539abc5469052e9d48fd75f8ff094ba2a0ae767/billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb", size = 86766 }, +] + +[[package]] +name = "boto3" +version = "1.36.23" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/54/a91d274f50bbe8fbd16ecea8bfd60249d0dc1ca50874e3a06119c6e5723a/boto3-1.36.23.tar.gz", hash = "sha256:006800604c34382873521b20890b758eea7109d699696ece932131259d0a4658", size = 111094 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/b5/2217a8ebb59bbc5f28dc10d3184b1f7e238f1929d211e69298421f912411/boto3-1.36.23-py3-none-any.whl", hash = "sha256:d59642672b1f35f55f47b317693241ce53333816f47c9e72fcc8fd0e9adc6a87", size = 139179 }, +] + +[[package]] +name = "botocore" +version = "1.36.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/ce/f3b193407b6bb1be04a3e7c75a8fd2ddc52d08dd5514180010b65ab006bf/botocore-1.36.24.tar.gz", hash = "sha256:7d35ba92ccbed7aa7e1563b12bb339bde612d5f845c89bfdd79a6db8c26b9f2e", size = 13572742 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f6/8a93f52b9e53962ecf0cd2b673d598d918f4383796dfe3b3408829c57e45/botocore-1.36.24-py3-none-any.whl", hash = "sha256:b8b2ad60e6545aaef3a40163793c39555fcfd67fb081a38695018026c4f4db25", size = 13359496 }, +] + +[[package]] +name = "cachetools" +version = "5.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/74/57df1ab0ce6bc5f6fa868e08de20df8ac58f9c44330c7671ad922d2bbeae/cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95", size = 28044 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/4e/de4ff18bcf55857ba18d3a4bd48c8a9fde6bb0980c9d20b263f05387fd88/cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb", size = 9530 }, +] + +[[package]] +name = "celery" +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "billiard" }, + { name = "click" }, + { name = "click-didyoumean" }, + { name = "click-plugins" }, + { name = "click-repl" }, + { name = "kombu" }, + { name = "python-dateutil" }, + { name = "tzdata" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/9c/cf0bce2cc1c8971bf56629d8f180e4ca35612c7e79e6e432e785261a8be4/celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706", size = 1575692 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/c4/6a4d3772e5407622feb93dd25c86ce3c0fee746fa822a777a627d56b4f2a/celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64", size = 425983 }, +] + +[[package]] +name = "cerberus" +version = "1.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/92/6d861524d97a2c4913816309ca12afe313b32c8efc3ec641de98b890834b/cerberus-1.3.7.tar.gz", hash = "sha256:ecf249665400a0b7a9d5e4ee1ffc234fd5d003186d3e1904f70bc14038642c13", size = 29651 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/ce/e3abf3fd04da28978eefb06ea906549f20f23f2ec6df8873ede6b62c8a8c/Cerberus-1.3.7-py3-none-any.whl", hash = "sha256:180e7d1fa1a5765cbff7b5c716e52fddddfab859dc8f625b0d563ace4b7a7ab3", size = 30508 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "click-didyoumean" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631 }, +] + +[[package]] +name = "click-plugins" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/1d/45434f64ed749540af821fd7e42b8e4d23ac04b1eda7c26613288d6cd8a8/click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", size = 8164 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/da/824b92d9942f4e472702488857914bdd50f73021efea15b4cad9aca8ecef/click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8", size = 7497 }, +] + +[[package]] +name = "click-repl" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289 }, +] + +[[package]] +name = "codecov-ribs" +version = "0.1.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/0c/da1d2367c86ccebbcd60d77a697a816b9b226031528b92c5e751a82b498d/codecov_ribs-0.1.18.tar.gz", hash = "sha256:004590ee88515aa2ee3e1ad0a05ed616a6caffcedd9effe3454efe9d927c2a46", size = 31935 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/bc/e39377099e929bd4da67e9dde23e9e4b1f765e80174a4fcbd76066ab85bc/codecov_ribs-0.1.18-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:00fe34c01f72cfb967d9abfa0a19ac65485564f1cdfc7ea634362f0f8031a57a", size = 449013 }, + { url = "https://files.pythonhosted.org/packages/cd/26/27b1404572ece21c3cfcaebcacc77accdef87fd7f3799ae75ec9a3a986a6/codecov_ribs-0.1.18-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:509c3b5ffedb75590e5efff9fe44fa38fb9a330f3cfd2cfdbffe03ba9baf2c6f", size = 435904 }, + { url = "https://files.pythonhosted.org/packages/90/e4/aac82889bf1233e65c05d3182f0f009f9fec7c3938143a627082a4f470dd/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:94676137c0a84fe434b6fa9d2e51e8301839b704f0561642e3104def7fdd186a", size = 1277625 }, + { url = "https://files.pythonhosted.org/packages/46/b3/5b9677fa973a25942538c143bb1184bfd13ee838ade1f62836bae1ab4f8e/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:078a3c5b23852142ce0c362e5e61dc3e76be28820000cf8d62f0dfa02e0b9fd1", size = 1230640 }, + { url = "https://files.pythonhosted.org/packages/48/c6/85c6d4ad866d3ed860ec4d371eaa4d55a3937dd21315d7258d6968623748/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ba82a37648c42b568513c5b8c4044c45f1dc4240a9c53259687cc2e869f415b", size = 1243114 }, + { url = "https://files.pythonhosted.org/packages/15/30/919304e23d9599674887ae112d362e6a0b96cdb120a1766c2c0ea30bff17/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb7aeb1fce52ae4e61ad4fa1f16d9d6c5429b77c037800eec8a58586020a3b1", size = 1340437 }, + { url = "https://files.pythonhosted.org/packages/53/55/d71a0ebf5363af6eacaf60cbf2db33d4c2e7962c8734172e6f37e378b153/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7f87052b56e5346ab2d89cad8cb801074faaca4cb9d351097e5a7a54a2af424", size = 1432376 }, + { url = "https://files.pythonhosted.org/packages/44/26/ee24c4f22de09d04d242e21dfb4877469735a6a7d55c052b3bbcb782e541/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08db4118a7350f76c53fc0ba04110b606a3178fdd1acb6946cd1c0b9fea777b5", size = 1242811 }, + { url = "https://files.pythonhosted.org/packages/00/ad/e86b5967ee91e93604554a5c9ea35175330d84e52f781a8d1295d63150e9/codecov_ribs-0.1.18-cp38-abi3-win32.whl", hash = "sha256:a7db0d0afa593f155e34ae722703b8b21840c85a94cc90e8550173610d346d0f", size = 290807 }, + { url = "https://files.pythonhosted.org/packages/d1/e3/b1e2b06b1d7e047318608fb2b7268d594da0798b166eb6b38ae7d8c3f41f/codecov_ribs-0.1.18-cp38-abi3-win_amd64.whl", hash = "sha256:dd7a799c1eea76caebf59f4a9a3a5cda0fff7999d49452235c1db5f1d654d4b8", size = 305137 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "colour" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/d4/5911a7618acddc3f594ddf144ecd8a03c29074a540f4494670ad8f153efe/colour-0.1.5.tar.gz", hash = "sha256:af20120fefd2afede8b001fbef2ea9da70ad7d49fafdb6489025dae8745c3aee", size = 24776 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/46/e81907704ab203206769dee1385dc77e1407576ff8f50a0681d0a6b541be/colour-0.1.5-py2.py3-none-any.whl", hash = "sha256:33f6db9d564fadc16e59921a56999b79571160ce09916303d35346dddc17978c", size = 23772 }, +] + +[[package]] +name = "coverage" +version = "7.6.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 }, + { url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 }, + { url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 }, + { url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 }, + { url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 }, + { url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 }, + { url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 }, + { url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 }, + { url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 }, + { url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 }, + { url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 }, + { url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 }, + { url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 }, + { url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 }, + { url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 }, + { url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 }, + { url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 }, + { url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 }, + { url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 }, + { url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 }, + { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 }, +] + +[[package]] +name = "cryptography" +version = "44.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/67/545c79fe50f7af51dbad56d16b23fe33f63ee6a5d956b3cb68ea110cbe64/cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14", size = 710819 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/27/5e3524053b4c8889da65cf7814a9d0d8514a05194a25e1e34f46852ee6eb/cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009", size = 6642022 }, + { url = "https://files.pythonhosted.org/packages/34/b9/4d1fa8d73ae6ec350012f89c3abfbff19fc95fe5420cf972e12a8d182986/cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f", size = 3943865 }, + { url = "https://files.pythonhosted.org/packages/6e/57/371a9f3f3a4500807b5fcd29fec77f418ba27ffc629d88597d0d1049696e/cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2", size = 4162562 }, + { url = "https://files.pythonhosted.org/packages/c5/1d/5b77815e7d9cf1e3166988647f336f87d5634a5ccecec2ffbe08ef8dd481/cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911", size = 3951923 }, + { url = "https://files.pythonhosted.org/packages/28/01/604508cd34a4024467cd4105887cf27da128cba3edd435b54e2395064bfb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69", size = 3685194 }, + { url = "https://files.pythonhosted.org/packages/c6/3d/d3c55d4f1d24580a236a6753902ef6d8aafd04da942a1ee9efb9dc8fd0cb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026", size = 4187790 }, + { url = "https://files.pythonhosted.org/packages/ea/a6/44d63950c8588bfa8594fd234d3d46e93c3841b8e84a066649c566afb972/cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd", size = 3951343 }, + { url = "https://files.pythonhosted.org/packages/c1/17/f5282661b57301204cbf188254c1a0267dbd8b18f76337f0a7ce1038888c/cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0", size = 4187127 }, + { url = "https://files.pythonhosted.org/packages/f3/68/abbae29ed4f9d96596687f3ceea8e233f65c9645fbbec68adb7c756bb85a/cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf", size = 4070666 }, + { url = "https://files.pythonhosted.org/packages/0f/10/cf91691064a9e0a88ae27e31779200b1505d3aee877dbe1e4e0d73b4f155/cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864", size = 4288811 }, + { url = "https://files.pythonhosted.org/packages/38/78/74ea9eb547d13c34e984e07ec8a473eb55b19c1451fe7fc8077c6a4b0548/cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a", size = 2771882 }, + { url = "https://files.pythonhosted.org/packages/cf/6c/3907271ee485679e15c9f5e93eac6aa318f859b0aed8d369afd636fafa87/cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00", size = 3206989 }, + { url = "https://files.pythonhosted.org/packages/9f/f1/676e69c56a9be9fd1bffa9bc3492366901f6e1f8f4079428b05f1414e65c/cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008", size = 6643714 }, + { url = "https://files.pythonhosted.org/packages/ba/9f/1775600eb69e72d8f9931a104120f2667107a0ee478f6ad4fe4001559345/cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862", size = 3943269 }, + { url = "https://files.pythonhosted.org/packages/25/ba/e00d5ad6b58183829615be7f11f55a7b6baa5a06910faabdc9961527ba44/cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3", size = 4166461 }, + { url = "https://files.pythonhosted.org/packages/b3/45/690a02c748d719a95ab08b6e4decb9d81e0ec1bac510358f61624c86e8a3/cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7", size = 3950314 }, + { url = "https://files.pythonhosted.org/packages/e6/50/bf8d090911347f9b75adc20f6f6569ed6ca9b9bff552e6e390f53c2a1233/cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a", size = 3686675 }, + { url = "https://files.pythonhosted.org/packages/e1/e7/cfb18011821cc5f9b21efb3f94f3241e3a658d267a3bf3a0f45543858ed8/cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c", size = 4190429 }, + { url = "https://files.pythonhosted.org/packages/07/ef/77c74d94a8bfc1a8a47b3cafe54af3db537f081742ee7a8a9bd982b62774/cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62", size = 3950039 }, + { url = "https://files.pythonhosted.org/packages/6d/b9/8be0ff57c4592382b77406269b1e15650c9f1a167f9e34941b8515b97159/cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41", size = 4189713 }, + { url = "https://files.pythonhosted.org/packages/78/e1/4b6ac5f4100545513b0847a4d276fe3c7ce0eacfa73e3b5ebd31776816ee/cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b", size = 4071193 }, + { url = "https://files.pythonhosted.org/packages/3d/cb/afff48ceaed15531eab70445abe500f07f8f96af2bb35d98af6bfa89ebd4/cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7", size = 4289566 }, + { url = "https://files.pythonhosted.org/packages/30/6f/4eca9e2e0f13ae459acd1ca7d9f0257ab86e68f44304847610afcb813dc9/cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9", size = 2772371 }, + { url = "https://files.pythonhosted.org/packages/d2/05/5533d30f53f10239616a357f080892026db2d550a40c393d0a8a7af834a9/cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f", size = 3207303 }, +] + +[[package]] +name = "debugpy" +version = "1.8.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/25/c74e337134edf55c4dfc9af579eccb45af2393c40960e2795a94351e8140/debugpy-1.8.12.tar.gz", hash = "sha256:646530b04f45c830ceae8e491ca1c9320a2d2f0efea3141487c82130aba70dce", size = 1641122 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/4d/7c3896619a8791effd5d8c31f0834471fc8f8fb3047ec4f5fc69dd1393dd/debugpy-1.8.12-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:696d8ae4dff4cbd06bf6b10d671e088b66669f110c7c4e18a44c43cf75ce966f", size = 2485246 }, + { url = "https://files.pythonhosted.org/packages/99/46/bc6dcfd7eb8cc969a5716d858e32485eb40c72c6a8dc88d1e3a4d5e95813/debugpy-1.8.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:898fba72b81a654e74412a67c7e0a81e89723cfe2a3ea6fcd3feaa3395138ca9", size = 4218616 }, + { url = "https://files.pythonhosted.org/packages/03/dd/d7fcdf0381a9b8094da1f6a1c9f19fed493a4f8576a2682349b3a8b20ec7/debugpy-1.8.12-cp313-cp313-win32.whl", hash = "sha256:22a11c493c70413a01ed03f01c3c3a2fc4478fc6ee186e340487b2edcd6f4180", size = 5226540 }, + { url = "https://files.pythonhosted.org/packages/25/bd/ecb98f5b5fc7ea0bfbb3c355bc1dd57c198a28780beadd1e19915bf7b4d9/debugpy-1.8.12-cp313-cp313-win_amd64.whl", hash = "sha256:fdb3c6d342825ea10b90e43d7f20f01535a72b3a1997850c0c3cefa5c27a4a2c", size = 5267134 }, + { url = "https://files.pythonhosted.org/packages/38/c4/5120ad36405c3008f451f94b8f92ef1805b1e516f6ff870f331ccb3c4cc0/debugpy-1.8.12-py2.py3-none-any.whl", hash = "sha256:274b6a2040349b5c9864e475284bce5bb062e63dce368a394b8cc865ae3b00c6", size = 5229490 }, +] + +[[package]] +name = "deprecated" +version = "1.2.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, +] + +[[package]] +name = "django" +version = "4.2.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/49/8e71f8a30aecc8e236bbb0ce056ff705a4782db4ab836e588c1a0e0a26aa/Django-4.2.19.tar.gz", hash = "sha256:6c833be4b0ca614f0a919472a1028a3bbdeb6f056fa04023aeb923346ba2c306", size = 10426865 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/db/6fc8c55f3b482776ac245623cd713b00ba894ad8f32cf438a40d9f1ea29e/Django-4.2.19-py3-none-any.whl", hash = "sha256:a104e13f219fc55996a4e416ef7d18ab4eeb44e0aa95174c192f16cda9f94e75", size = 7993670 }, +] + +[[package]] +name = "django-better-admin-arrayfield" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/4f/e8bdf86d5bba2622d585bec983a0b59f373f4ed00eb948a6907ba35e5585/django-better-admin-arrayfield-1.4.2.tar.gz", hash = "sha256:b45423e51bbc0aa31ef658248c058ca8b533a541be4dee9fb8bcd059f8a10a58", size = 13857 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/75/8fc6cfc73c456283c89a80bd8319df61aabcd273cfa37697bc31a00e387b/django_better_admin_arrayfield-1.4.2-py2.py3-none-any.whl", hash = "sha256:bfeaa0fa8210a7ea95ee996a6caaa59ecd0c923269f573e6d8319c28dcac5c88", size = 13931 }, +] + +[[package]] +name = "django-model-utils" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/60/5e232c32a2c977cc1af8c70a38ef436598bc649ad89c2c4568454edde2c9/django_model_utils-5.0.0.tar.gz", hash = "sha256:041cdd6230d2fbf6cd943e1969318bce762272077f4ecd333ab2263924b4e5eb", size = 80559 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/13/87a42048700c54bfce35900a34e2031245132775fb24363fc0e33664aa9c/django_model_utils-5.0.0-py3-none-any.whl", hash = "sha256:fec78e6c323d565a221f7c4edc703f4567d7bb1caeafe1acd16a80c5ff82056b", size = 42630 }, +] + +[[package]] +name = "django-postgres-extra" +version = "2.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/8a/2bf9f570e73058569e48cc3208f007fd7ff45cb28e178ea1da2b188f14e2/django-postgres-extra-2.0.8.tar.gz", hash = "sha256:9efa08c6f18ed34460af41c6f679bb375b93d12544b1105aa348b787a30b46eb", size = 48572 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/22/559559d7894648bd7f2bbfdeedd5ccba4f8d9de68855ced522011d608af5/django_postgres_extra-2.0.8-py3-none-any.whl", hash = "sha256:447d5a971759943ee63a9d4cef9c6c1fa290e518611ea521a38b6732681d2f3a", size = 75205 }, +] + +[[package]] +name = "django-prometheus" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prometheus-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/51/485b4122e00f2b8efec8a6d718ef4ce6b150231e49398e554ce1151f65c3/django-prometheus-2.3.1.tar.gz", hash = "sha256:f9c8b6c780c9419ea01043c63a437d79db2c33353451347894408184ad9c3e1e", size = 24718 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/75/fb3d4f056f9ed4f8848817d5afd7a1d949632ab117452ccd179e3839cfc4/django_prometheus-2.3.1-py2.py3-none-any.whl", hash = "sha256:cf9b26f7ba2e4568f08f8f91480a2882023f5908579681bcf06a4d2465f12168", size = 29081 }, +] + +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774 }, +] + +[[package]] +name = "factory-boy" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "faker" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/98/75cacae9945f67cfe323829fc2ac451f64517a8a330b572a06a323997065/factory_boy-3.3.3.tar.gz", hash = "sha256:866862d226128dfac7f2b4160287e899daf54f2612778327dd03d0e2cb1e3d03", size = 164146 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/8d/2bc5f5546ff2ccb3f7de06742853483ab75bf74f36a92254702f8baecc79/factory_boy-3.3.3-py2.py3-none-any.whl", hash = "sha256:1c39e3289f7e667c4285433f305f8d506efc2fe9c73aaea4151ebd5cdea394fc", size = 37036 }, +] + +[[package]] +name = "faker" +version = "36.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/8f/40d002bed58bd6b79bf970505582b769fc975afcacc62c2fe1518d5729c2/faker-36.1.1.tar.gz", hash = "sha256:7cb2bbd4c8f040e4a340ae4019e9a48b6cf1db6a71bda4e5a61d8d13b7bef28d", size = 1874935 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/79/e13ae542f63ce40d02b0fe63809563b102f19ffa3b94e6062ee9286a7801/Faker-36.1.1-py3-none-any.whl", hash = "sha256:ad1f1be7fd692ec0256517404a9d7f007ab36ac5d4674082fa72404049725eaa", size = 1917865 }, +] + +[[package]] +name = "filelock" +version = "3.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e", size = 18027 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 }, +] + +[[package]] +name = "freezegun" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/ef/722b8d71ddf4d48f25f6d78aa2533d505bf3eec000a7cacb8ccc8de61f2f/freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9", size = 33697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/0b/0d7fee5919bccc1fdc1c2a7528b98f65c6f69b223a3fd8f809918c142c36/freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1", size = 17569 }, +] + +[[package]] +name = "google-api-core" +version = "2.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/b7/481c83223d7b4f02c7651713fceca648fa3336e1571b9804713f66bca2d8/google_api_core-2.24.1.tar.gz", hash = "sha256:f8b36f5456ab0dd99a1b693a40a31d1e7757beea380ad1b38faaf8941eae9d8a", size = 163508 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/a6/8e30ddfd3d39ee6d2c76d3d4f64a83f77ac86a4cab67b286ae35ce9e4369/google_api_core-2.24.1-py3-none-any.whl", hash = "sha256:bc78d608f5a5bf853b80bd70a795f703294de656c096c0968320830a4bc280f1", size = 160059 }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-auth" +version = "2.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/eb/d504ba1daf190af6b204a9d4714d457462b486043744901a6eeea711f913/google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4", size = 270866 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/47/603554949a37bca5b7f894d51896a9c534b9eab808e2520a748e081669d0/google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a", size = 210770 }, +] + +[[package]] +name = "google-cloud-core" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/1f/9d1e0ba6919668608570418a9a51e47070ac15aeff64261fb092d8be94c0/google-cloud-core-2.4.1.tar.gz", hash = "sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073", size = 35587 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/0f/2e2061e3fbcb9d535d5da3f58cc8de4947df1786fe6a1355960feb05a681/google_cloud_core-2.4.1-py2.py3-none-any.whl", hash = "sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61", size = 29233 }, +] + +[[package]] +name = "google-cloud-pubsub" +version = "2.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "grpcio-status" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/91/e5ed3f50d543cd752efc6957c0b196e5dcdc5ffc60956a8efe566c81e3ff/google_cloud_pubsub-2.28.0.tar.gz", hash = "sha256:904e894b4e15121521077ac85c9aa8f4e7b8517bc5fb409ddb2aac8df1a02b3c", size = 371127 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/33/07636ce3dd59016ab88b98fbb9e614c76d8f210f5c8feec2db5891f6dc5d/google_cloud_pubsub-2.28.0-py2.py3-none-any.whl", hash = "sha256:76b41a322b43bc845fb06ffe238758726324d957d0161bae3ff4b14339da144b", size = 301549 }, +] + +[[package]] +name = "google-cloud-storage" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "google-resumable-media" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/d7/dfa74049c4faa3b4d68fa1a10a7eab5a76c57d0788b47c27f927bedc606d/google_cloud_storage-3.0.0.tar.gz", hash = "sha256:2accb3e828e584888beff1165e5f3ac61aa9088965eb0165794a82d8c7f95297", size = 7665253 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/ae/1a50f07161301e40a30b2e40744a7b85ffab7add16e044417925eccf9bbf/google_cloud_storage-3.0.0-py2.py3-none-any.whl", hash = "sha256:f85fd059650d2dbb0ac158a9a6b304b66143b35ed2419afec2905ca522eb2c6a", size = 173860 }, +] + +[[package]] +name = "google-crc32c" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/72/c3298da1a3773102359c5a78f20dae8925f5ea876e37354415f68594a6fb/google_crc32c-1.6.0.tar.gz", hash = "sha256:6eceb6ad197656a1ff49ebfbbfa870678c75be4344feb35ac1edf694309413dc", size = 14472 } + +[[package]] +name = "google-resumable-media" +version = "2.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-crc32c" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/5a/0efdc02665dca14e0837b62c8a1a93132c264bd02054a15abb2218afe0ae/google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0", size = 2163099 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/35/b8d3baf8c46695858cb9d8835a53baa1eeb9906ddaf2f728a5f5b640fd1e/google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa", size = 81251 }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.67.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/e1/fbffb85a624f1404133b5bb624834e77e0f549e2b8548146fe18c56e1411/googleapis_common_protos-1.67.0.tar.gz", hash = "sha256:21398025365f138be356d5923e9168737d94d46a72aefee4a6110a1f23463c86", size = 57344 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/30/2bd0eb03a7dee7727cd2ec643d1e992979e62d5e7443507381cce0455132/googleapis_common_protos-1.67.0-py2.py3-none-any.whl", hash = "sha256:579de760800d13616f51cf8be00c876f00a9f146d3e6510e19d1f4111758b741", size = 164985 }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, +] + +[[package]] +name = "grpc-google-iam-v1" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", extra = ["grpc"] }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/2f/68e43b0e551974fa7dd18798a5974710586a72dc484ecaa2fc023d961342/grpc_google_iam_v1-0.14.0.tar.gz", hash = "sha256:c66e07aa642e39bb37950f9e7f491f70dad150ac9801263b42b2814307c2df99", size = 18327 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/b4/ab54f7fda4af43ca5c094bc1d6341780fd669c44ae18952b5337029b1d98/grpc_google_iam_v1-0.14.0-py2.py3-none-any.whl", hash = "sha256:fb4a084b30099ba3ab07d61d620a0d4429570b13ff53bd37bac75235f98b7da4", size = 27276 }, +] + +[[package]] +name = "grpcio" +version = "1.70.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/e1/4b21b5017c33f3600dcc32b802bb48fe44a4d36d6c066f52650c7c2690fa/grpcio-1.70.0.tar.gz", hash = "sha256:8d1584a68d5922330025881e63a6c1b54cc8117291d382e4fa69339b6d914c56", size = 12788932 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/38/66d0f32f88feaf7d83f8559cd87d899c970f91b1b8a8819b58226de0a496/grpcio-1.70.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:aa573896aeb7d7ce10b1fa425ba263e8dddd83d71530d1322fd3a16f31257b4a", size = 5199218 }, + { url = "https://files.pythonhosted.org/packages/c1/96/947df763a0b18efb5cc6c2ae348e56d97ca520dc5300c01617b234410173/grpcio-1.70.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:d405b005018fd516c9ac529f4b4122342f60ec1cee181788249372524e6db429", size = 11445983 }, + { url = "https://files.pythonhosted.org/packages/fd/5b/f3d4b063e51b2454bedb828e41f3485800889a3609c49e60f2296cc8b8e5/grpcio-1.70.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f32090238b720eb585248654db8e3afc87b48d26ac423c8dde8334a232ff53c9", size = 5663954 }, + { url = "https://files.pythonhosted.org/packages/bd/0b/dab54365fcedf63e9f358c1431885478e77d6f190d65668936b12dd38057/grpcio-1.70.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfa089a734f24ee5f6880c83d043e4f46bf812fcea5181dcb3a572db1e79e01c", size = 6304323 }, + { url = "https://files.pythonhosted.org/packages/76/a8/8f965a7171ddd336ce32946e22954aa1bbc6f23f095e15dadaa70604ba20/grpcio-1.70.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f19375f0300b96c0117aca118d400e76fede6db6e91f3c34b7b035822e06c35f", size = 5910939 }, + { url = "https://files.pythonhosted.org/packages/1b/05/0bbf68be8b17d1ed6f178435a3c0c12e665a1e6054470a64ce3cb7896596/grpcio-1.70.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:7c73c42102e4a5ec76608d9b60227d917cea46dff4d11d372f64cbeb56d259d0", size = 6631405 }, + { url = "https://files.pythonhosted.org/packages/79/6a/5df64b6df405a1ed1482cb6c10044b06ec47fd28e87c2232dbcf435ecb33/grpcio-1.70.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:0a5c78d5198a1f0aa60006cd6eb1c912b4a1520b6a3968e677dbcba215fabb40", size = 6190982 }, + { url = "https://files.pythonhosted.org/packages/42/aa/aeaac87737e6d25d1048c53b8ec408c056d3ed0c922e7c5efad65384250c/grpcio-1.70.0-cp313-cp313-win32.whl", hash = "sha256:fe9dbd916df3b60e865258a8c72ac98f3ac9e2a9542dcb72b7a34d236242a5ce", size = 3598359 }, + { url = "https://files.pythonhosted.org/packages/1f/79/8edd2442d2de1431b4a3de84ef91c37002f12de0f9b577fb07b452989dbc/grpcio-1.70.0-cp313-cp313-win_amd64.whl", hash = "sha256:4119fed8abb7ff6c32e3d2255301e59c316c22d31ab812b3fbcbaf3d0d87cc68", size = 4293938 }, +] + +[[package]] +name = "grpcio-status" +version = "1.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/d1/2397797c810020eac424e1aac10fbdc5edb6b9b4ad6617e0ed53ca907653/grpcio_status-1.70.0.tar.gz", hash = "sha256:0e7b42816512433b18b9d764285ff029bde059e9d41f8fe10a60631bd8348101", size = 13681 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/49e558040e069feebac70cdd1b605f38738c0277ac5d38e2ce3d03e1b1ec/grpcio_status-1.70.0-py3-none-any.whl", hash = "sha256:fc5a2ae2b9b1c1969cc49f3262676e6854aa2398ec69cb5bd6c47cd501904a85", size = 14429 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "identify" +version = "2.6.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/d1/524aa3350f78bcd714d148ade6133d67d6b7de2cdbae7d99039c024c9a25/identify-2.6.7.tar.gz", hash = "sha256:3fa266b42eba321ee0b2bb0936a6a6b9e36a1351cbb69055b3082f4193035684", size = 99260 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/00/1fd4a117c6c93f2dcc5b7edaeaf53ea45332ef966429be566ca16c2beb94/identify-2.6.7-py2.py3-none-any.whl", hash = "sha256:155931cb617a401807b09ecec6635d6c692d180090a1cedca8ef7d58ba5b6aa0", size = 99097 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "ijson" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/83/28e9e93a3a61913e334e3a2e78ea9924bb9f9b1ac45898977f9d9dd6133f/ijson-3.3.0.tar.gz", hash = "sha256:7f172e6ba1bee0d4c8f8ebd639577bfe429dee0f3f96775a067b8bae4492d8a0", size = 60079 } + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jinja2" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, +] + +[[package]] +name = "jiter" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/70/90bc7bd3932e651486861df5c8ffea4ca7c77d28e8532ddefe2abc561a53/jiter-0.8.2.tar.gz", hash = "sha256:cd73d3e740666d0e639f678adb176fad25c1bcbdae88d8d7b857e1783bb4212d", size = 163007 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/b0/bfa1f6f2c956b948802ef5a021281978bf53b7a6ca54bb126fd88a5d014e/jiter-0.8.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ca1f08b8e43dc3bd0594c992fb1fd2f7ce87f7bf0d44358198d6da8034afdf84", size = 301190 }, + { url = "https://files.pythonhosted.org/packages/a4/8f/396ddb4e292b5ea57e45ade5dc48229556b9044bad29a3b4b2dddeaedd52/jiter-0.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5672a86d55416ccd214c778efccf3266b84f87b89063b582167d803246354be4", size = 309334 }, + { url = "https://files.pythonhosted.org/packages/7f/68/805978f2f446fa6362ba0cc2e4489b945695940656edd844e110a61c98f8/jiter-0.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58dc9bc9767a1101f4e5e22db1b652161a225874d66f0e5cb8e2c7d1c438b587", size = 333918 }, + { url = "https://files.pythonhosted.org/packages/b3/99/0f71f7be667c33403fa9706e5b50583ae5106d96fab997fa7e2f38ee8347/jiter-0.8.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b2998606d6dadbb5ccda959a33d6a5e853252d921fec1792fc902351bb4e2c", size = 356057 }, + { url = "https://files.pythonhosted.org/packages/8d/50/a82796e421a22b699ee4d2ce527e5bcb29471a2351cbdc931819d941a167/jiter-0.8.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ab9a87f3784eb0e098f84a32670cfe4a79cb6512fd8f42ae3d0709f06405d18", size = 379790 }, + { url = "https://files.pythonhosted.org/packages/3c/31/10fb012b00f6d83342ca9e2c9618869ab449f1aa78c8f1b2193a6b49647c/jiter-0.8.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:79aec8172b9e3c6d05fd4b219d5de1ac616bd8da934107325a6c0d0e866a21b6", size = 388285 }, + { url = "https://files.pythonhosted.org/packages/c8/81/f15ebf7de57be488aa22944bf4274962aca8092e4f7817f92ffa50d3ee46/jiter-0.8.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:711e408732d4e9a0208008e5892c2966b485c783cd2d9a681f3eb147cf36c7ef", size = 344764 }, + { url = "https://files.pythonhosted.org/packages/b3/e8/0cae550d72b48829ba653eb348cdc25f3f06f8a62363723702ec18e7be9c/jiter-0.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:653cf462db4e8c41995e33d865965e79641ef45369d8a11f54cd30888b7e6ff1", size = 376620 }, + { url = "https://files.pythonhosted.org/packages/b8/50/e5478ff9d82534a944c03b63bc217c5f37019d4a34d288db0f079b13c10b/jiter-0.8.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:9c63eaef32b7bebac8ebebf4dabebdbc6769a09c127294db6babee38e9f405b9", size = 510402 }, + { url = "https://files.pythonhosted.org/packages/8e/1e/3de48bbebbc8f7025bd454cedc8c62378c0e32dd483dece5f4a814a5cb55/jiter-0.8.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:eb21aaa9a200d0a80dacc7a81038d2e476ffe473ffdd9c91eb745d623561de05", size = 503018 }, + { url = "https://files.pythonhosted.org/packages/d5/cd/d5a5501d72a11fe3e5fd65c78c884e5164eefe80077680533919be22d3a3/jiter-0.8.2-cp313-cp313-win32.whl", hash = "sha256:789361ed945d8d42850f919342a8665d2dc79e7e44ca1c97cc786966a21f627a", size = 203190 }, + { url = "https://files.pythonhosted.org/packages/51/bf/e5ca301245ba951447e3ad677a02a64a8845b185de2603dabd83e1e4b9c6/jiter-0.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:ab7f43235d71e03b941c1630f4b6e3055d46b6cb8728a17663eaac9d8e83a865", size = 203551 }, + { url = "https://files.pythonhosted.org/packages/2f/3c/71a491952c37b87d127790dd7a0b1ebea0514c6b6ad30085b16bbe00aee6/jiter-0.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b426f72cd77da3fec300ed3bc990895e2dd6b49e3bfe6c438592a3ba660e41ca", size = 308347 }, + { url = "https://files.pythonhosted.org/packages/a0/4c/c02408042e6a7605ec063daed138e07b982fdb98467deaaf1c90950cf2c6/jiter-0.8.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2dd880785088ff2ad21ffee205e58a8c1ddabc63612444ae41e5e4b321b39c0", size = 342875 }, + { url = "https://files.pythonhosted.org/packages/91/61/c80ef80ed8a0a21158e289ef70dac01e351d929a1c30cb0f49be60772547/jiter-0.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:3ac9f578c46f22405ff7f8b1f5848fb753cc4b8377fbec8470a7dc3997ca7566", size = 202374 }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, +] + +[[package]] +name = "kombu" +version = "5.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "amqp" }, + { name = "tzdata" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/4d/b93fcb353d279839cc35d0012bee805ed0cf61c07587916bfc35dbfddaf1/kombu-5.4.2.tar.gz", hash = "sha256:eef572dd2fd9fc614b37580e3caeafdd5af46c1eff31e7fba89138cdb406f2cf", size = 442858 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/ec/7811a3cf9fdfee3ee88e54d08fcbc3fabe7c1b6e4059826c59d7b795651c/kombu-5.4.2-py3-none-any.whl", hash = "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763", size = 201349 }, +] + +[[package]] +name = "lxml" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/f6/c15ca8e5646e937c148e147244817672cf920b56ac0bf2cc1512ae674be8/lxml-5.3.1.tar.gz", hash = "sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8", size = 3678591 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/1c/724931daa1ace168e0237b929e44062545bf1551974102a5762c349c668d/lxml-5.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c093c7088b40d8266f57ed71d93112bd64c6724d31f0794c1e52cc4857c28e0e", size = 8171881 }, + { url = "https://files.pythonhosted.org/packages/67/0c/857b8fb6010c4246e66abeebb8639eaabba60a6d9b7c606554ecc5cbf1ee/lxml-5.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0884e3f22d87c30694e625b1e62e6f30d39782c806287450d9dc2fdf07692fd", size = 4440394 }, + { url = "https://files.pythonhosted.org/packages/61/72/c9e81de6a000f9682ccdd13503db26e973b24c68ac45a7029173237e3eed/lxml-5.3.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1637fa31ec682cd5760092adfabe86d9b718a75d43e65e211d5931809bc111e7", size = 5037860 }, + { url = "https://files.pythonhosted.org/packages/24/26/942048c4b14835711b583b48cd7209bd2b5f0b6939ceed2381a494138b14/lxml-5.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a364e8e944d92dcbf33b6b494d4e0fb3499dcc3bd9485beb701aa4b4201fa414", size = 4782513 }, + { url = "https://files.pythonhosted.org/packages/e2/65/27792339caf00f610cc5be32b940ba1e3009b7054feb0c4527cebac228d4/lxml-5.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:779e851fd0e19795ccc8a9bb4d705d6baa0ef475329fe44a13cf1e962f18ff1e", size = 5305227 }, + { url = "https://files.pythonhosted.org/packages/18/e1/25f7aa434a4d0d8e8420580af05ea49c3e12db6d297cf5435ac0a054df56/lxml-5.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4393600915c308e546dc7003d74371744234e8444a28622d76fe19b98fa59d1", size = 4829846 }, + { url = "https://files.pythonhosted.org/packages/fe/ed/faf235e0792547d24f61ee1448159325448a7e4f2ab706503049d8e5df19/lxml-5.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:673b9d8e780f455091200bba8534d5f4f465944cbdd61f31dc832d70e29064a5", size = 4949495 }, + { url = "https://files.pythonhosted.org/packages/e5/e1/8f572ad9ed6039ba30f26dd4c2c58fb90f79362d2ee35ca3820284767672/lxml-5.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2e4a570f6a99e96c457f7bec5ad459c9c420ee80b99eb04cbfcfe3fc18ec6423", size = 4773415 }, + { url = "https://files.pythonhosted.org/packages/a3/75/6b57166b9d1983dac8f28f354e38bff8d6bcab013a241989c4d54c72701b/lxml-5.3.1-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:71f31eda4e370f46af42fc9f264fafa1b09f46ba07bdbee98f25689a04b81c20", size = 5337710 }, + { url = "https://files.pythonhosted.org/packages/cc/71/4aa56e2daa83bbcc66ca27b5155be2f900d996f5d0c51078eaaac8df9547/lxml-5.3.1-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:42978a68d3825eaac55399eb37a4d52012a205c0c6262199b8b44fcc6fd686e8", size = 4897362 }, + { url = "https://files.pythonhosted.org/packages/65/10/3fa2da152cd9b49332fd23356ed7643c9b74cad636ddd5b2400a9730d12b/lxml-5.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8b1942b3e4ed9ed551ed3083a2e6e0772de1e5e3aca872d955e2e86385fb7ff9", size = 4977795 }, + { url = "https://files.pythonhosted.org/packages/de/d2/e1da0f7b20827e7b0ce934963cb6334c1b02cf1bb4aecd218c4496880cb3/lxml-5.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85c4f11be9cf08917ac2a5a8b6e1ef63b2f8e3799cec194417e76826e5f1de9c", size = 4858104 }, + { url = "https://files.pythonhosted.org/packages/a5/35/063420e1b33d3308f5aa7fcbdd19ef6c036f741c9a7a4bd5dc8032486b27/lxml-5.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:231cf4d140b22a923b1d0a0a4e0b4f972e5893efcdec188934cc65888fd0227b", size = 5416531 }, + { url = "https://files.pythonhosted.org/packages/c3/83/93a6457d291d1e37adfb54df23498101a4701834258c840381dd2f6a030e/lxml-5.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5865b270b420eda7b68928d70bb517ccbe045e53b1a428129bb44372bf3d7dd5", size = 5273040 }, + { url = "https://files.pythonhosted.org/packages/39/25/ad4ac8fac488505a2702656550e63c2a8db3a4fd63db82a20dad5689cecb/lxml-5.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7bebc2275016cddf3c997bf8a0f7044160714c64a9b83975670a04e6d2252", size = 5050951 }, + { url = "https://files.pythonhosted.org/packages/82/74/f7d223c704c87e44b3d27b5e0dde173a2fcf2e89c0524c8015c2b3554876/lxml-5.3.1-cp313-cp313-win32.whl", hash = "sha256:d0751528b97d2b19a388b302be2a0ee05817097bab46ff0ed76feeec24951f78", size = 3485357 }, + { url = "https://files.pythonhosted.org/packages/80/83/8c54533b3576f4391eebea88454738978669a6cad0d8e23266224007939d/lxml-5.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:91fb6a43d72b4f8863d21f347a9163eecbf36e76e2f51068d59cd004c506f332", size = 3814484 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "minio" +version = "7.2.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi" }, + { name = "certifi" }, + { name = "pycryptodome" }, + { name = "typing-extensions" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/68/86a1cef80396e6a35a6fc4fafee5d28578c1a137bddd3ca2aa86f9b26a22/minio-7.2.15.tar.gz", hash = "sha256:5247df5d4dca7bfa4c9b20093acd5ad43e82d8710ceb059d79c6eea970f49f79", size = 138040 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/6f/3690028e846fe432bfa5ba724a0dc37ec9c914965b7733e19d8ca2c4c48d/minio-7.2.15-py3-none-any.whl", hash = "sha256:c06ef7a43e5d67107067f77b6c07ebdd68733e5aa7eed03076472410ca19d876", size = 95075 }, +] + +[[package]] +name = "mmh3" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/1b/1fc6888c74cbd8abad1292dde2ddfcf8fc059e114c97dd6bf16d12f36293/mmh3-5.1.0.tar.gz", hash = "sha256:136e1e670500f177f49ec106a4ebf0adf20d18d96990cc36ea492c651d2b406c", size = 33728 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/06/a098a42870db16c0a54a82c56a5bdc873de3165218cd5b3ca59dbc0d31a7/mmh3-5.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a523899ca29cfb8a5239618474a435f3d892b22004b91779fcb83504c0d5b8c", size = 56165 }, + { url = "https://files.pythonhosted.org/packages/5a/65/eaada79a67fde1f43e1156d9630e2fb70655e1d3f4e8f33d7ffa31eeacfd/mmh3-5.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:17cef2c3a6ca2391ca7171a35ed574b5dab8398163129a3e3a4c05ab85a4ff40", size = 40569 }, + { url = "https://files.pythonhosted.org/packages/36/7e/2b6c43ed48be583acd68e34d16f19209a9f210e4669421b0321e326d8554/mmh3-5.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52e12895b30110f3d89dae59a888683cc886ed0472dd2eca77497edef6161997", size = 40104 }, + { url = "https://files.pythonhosted.org/packages/11/2b/1f9e962fdde8e41b0f43d22c8ba719588de8952f9376df7d73a434827590/mmh3-5.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d6719045cda75c3f40397fc24ab67b18e0cb8f69d3429ab4c39763c4c608dd", size = 102497 }, + { url = "https://files.pythonhosted.org/packages/46/94/d6c5c3465387ba077cccdc028ab3eec0d86eed1eebe60dcf4d15294056be/mmh3-5.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d19fa07d303a91f8858982c37e6939834cb11893cb3ff20e6ee6fa2a7563826a", size = 108834 }, + { url = "https://files.pythonhosted.org/packages/34/1e/92c212bb81796b69dddfd50a8a8f4b26ab0d38fdaf1d3e8628a67850543b/mmh3-5.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31b47a620d622fbde8ca1ca0435c5d25de0ac57ab507209245e918128e38e676", size = 106936 }, + { url = "https://files.pythonhosted.org/packages/f4/41/f2f494bbff3aad5ffd2085506255049de76cde51ddac84058e32768acc79/mmh3-5.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00f810647c22c179b6821079f7aa306d51953ac893587ee09cf1afb35adf87cb", size = 93709 }, + { url = "https://files.pythonhosted.org/packages/9e/a9/a2cc4a756d73d9edf4fb85c76e16fd56b0300f8120fd760c76b28f457730/mmh3-5.1.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6128b610b577eed1e89ac7177ab0c33d06ade2aba93f5c89306032306b5f1c6", size = 101623 }, + { url = "https://files.pythonhosted.org/packages/5e/6f/b9d735533b6a56b2d56333ff89be6a55ac08ba7ff33465feb131992e33eb/mmh3-5.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1e550a45d2ff87a1c11b42015107f1778c93f4c6f8e731bf1b8fa770321b8cc4", size = 98521 }, + { url = "https://files.pythonhosted.org/packages/99/47/dff2b54fac0d421c1e6ecbd2d9c85b2d0e6f6ee0d10b115d9364116a511e/mmh3-5.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:785ae09276342f79fd8092633e2d52c0f7c44d56e8cfda8274ccc9b76612dba2", size = 96696 }, + { url = "https://files.pythonhosted.org/packages/be/43/9e205310f47c43ddf1575bb3a1769c36688f30f1ac105e0f0c878a29d2cd/mmh3-5.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0f4be3703a867ef976434afd3661a33884abe73ceb4ee436cac49d3b4c2aaa7b", size = 105234 }, + { url = "https://files.pythonhosted.org/packages/6b/44/90b11fd2b67dcb513f5bfe9b476eb6ca2d5a221c79b49884dc859100905e/mmh3-5.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e513983830c4ff1f205ab97152a0050cf7164f1b4783d702256d39c637b9d107", size = 98449 }, + { url = "https://files.pythonhosted.org/packages/f0/d0/25c4b0c7b8e49836541059b28e034a4cccd0936202800d43a1cc48495ecb/mmh3-5.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9135c300535c828c0bae311b659f33a31c941572eae278568d1a953c4a57b59", size = 97796 }, + { url = "https://files.pythonhosted.org/packages/23/fa/cbbb7fcd0e287a715f1cd28a10de94c0535bd94164e38b852abc18da28c6/mmh3-5.1.0-cp313-cp313-win32.whl", hash = "sha256:c65dbd12885a5598b70140d24de5839551af5a99b29f9804bb2484b29ef07692", size = 40828 }, + { url = "https://files.pythonhosted.org/packages/09/33/9fb90ef822f7b734955a63851907cf72f8a3f9d8eb3c5706bfa6772a2a77/mmh3-5.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:10db7765201fc65003fa998faa067417ef6283eb5f9bba8f323c48fd9c33e91f", size = 41504 }, + { url = "https://files.pythonhosted.org/packages/16/71/4ad9a42f2772793a03cb698f0fc42499f04e6e8d2560ba2f7da0fb059a8e/mmh3-5.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:b22fe2e54be81f6c07dcb36b96fa250fb72effe08aa52fbb83eade6e1e2d5fd7", size = 38890 }, +] + +[[package]] +name = "mock" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/ab/41d09a46985ead5839d8be987acda54b5bb93f713b3969cc0be4f81c455b/mock-5.1.0.tar.gz", hash = "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d", size = 80232 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/20/471f41173930550f279ccb65596a5ac19b9ac974a8d93679bcd3e0c31498/mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744", size = 30938 }, +] + +[[package]] +name = "monotonic" +version = "1.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/ca/8e91948b782ddfbd194f323e7e7d9ba12e5877addf04fb2bf8fca38e86ac/monotonic-1.6.tar.gz", hash = "sha256:3a55207bcfed53ddd5c5bae174524062935efed17792e9de2ad0205ce9ad63f7", size = 7615 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/67/7e8406a29b6c45be7af7740456f7f37025f0506ae2e05fb9009a53946860/monotonic-1.6-py2.py3-none-any.whl", hash = "sha256:68687e19a14f11f26d140dd5c86f3dba4bf5df58003000ed467e0e2a69bca96c", size = 8154 }, +] + +[[package]] +name = "multidict" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, + { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, + { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, + { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, + { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, + { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, + { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, + { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, + { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, + { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, + { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, + { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, + { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, + { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, + { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688 }, +] + +[[package]] +name = "openai" +version = "1.63.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/1c/11b520deb71f9ea54ced3c52cd6a5f7131215deba63ad07f23982e328141/openai-1.63.2.tar.gz", hash = "sha256:aeabeec984a7d2957b4928ceaa339e2ead19c61cfcf35ae62b7c363368d26360", size = 356902 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/64/db3462b358072387b8e93e6e6a38d3c741a17b4a84171ef01d6c85c63f25/openai-1.63.2-py3-none-any.whl", hash = "sha256:1f38b27b5a40814c2b7d8759ec78110df58c4a614c25f182809ca52b080ff4d4", size = 472282 }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "importlib-metadata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/6d/bbbf879826b7f3c89a45252010b5796fb1f1a0d45d9dc4709db0ef9a06c8/opentelemetry_api-1.30.0.tar.gz", hash = "sha256:375893400c1435bf623f7dfb3bcd44825fe6b56c34d0667c542ea8257b1a1240", size = 63703 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/0a/eea862fae6413d8181b23acf8e13489c90a45f17986ee9cf4eab8a0b9ad9/opentelemetry_api-1.30.0-py3-none-any.whl", hash = "sha256:d5f5284890d73fdf47f843dda3210edf37a38d66f44f2b5aedc1e89ed455dc09", size = 64955 }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/ee/d710062e8a862433d1be0b85920d0c653abe318878fef2d14dfe2c62ff7b/opentelemetry_sdk-1.30.0.tar.gz", hash = "sha256:c9287a9e4a7614b9946e933a67168450b9ab35f08797eb9bc77d998fa480fa18", size = 158633 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/28/64d781d6adc6bda2260067ce2902bd030cf45aec657e02e28c5b4480b976/opentelemetry_sdk-1.30.0-py3-none-any.whl", hash = "sha256:14fe7afc090caad881addb6926cec967129bd9260c4d33ae6a217359f6b61091", size = 118717 }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.51b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "opentelemetry-api" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/c0/0f9ef4605fea7f2b83d55dd0b0d7aebe8feead247cd6facd232b30907b4f/opentelemetry_semantic_conventions-0.51b0.tar.gz", hash = "sha256:3fabf47f35d1fd9aebcdca7e6802d86bd5ebc3bc3408b7e3248dde6e87a18c47", size = 107191 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/75/d7bdbb6fd8630b4cafb883482b75c4fc276b6426619539d266e32ac53266/opentelemetry_semantic_conventions-0.51b0-py3-none-any.whl", hash = "sha256:fdc777359418e8d06c86012c3dc92c88a6453ba662e941593adb062e48c2eeae", size = 177416 }, +] + +[[package]] +name = "orjson" +version = "3.10.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/5dea21763eeff8c1590076918a446ea3d6140743e0e36f58f369928ed0f4/orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e", size = 5282482 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/10/fe7d60b8da538e8d3d3721f08c1b7bff0491e8fa4dd3bf11a17e34f4730e/orjson-3.10.15-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bae0e6ec2b7ba6895198cd981b7cca95d1487d0147c8ed751e5632ad16f031a6", size = 249399 }, + { url = "https://files.pythonhosted.org/packages/6b/83/52c356fd3a61abd829ae7e4366a6fe8e8863c825a60d7ac5156067516edf/orjson-3.10.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93ce145b2db1252dd86af37d4165b6faa83072b46e3995ecc95d4b2301b725a", size = 125044 }, + { url = "https://files.pythonhosted.org/packages/55/b2/d06d5901408e7ded1a74c7c20d70e3a127057a6d21355f50c90c0f337913/orjson-3.10.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c203f6f969210128af3acae0ef9ea6aab9782939f45f6fe02d05958fe761ef9", size = 150066 }, + { url = "https://files.pythonhosted.org/packages/75/8c/60c3106e08dc593a861755781c7c675a566445cc39558677d505878d879f/orjson-3.10.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8918719572d662e18b8af66aef699d8c21072e54b6c82a3f8f6404c1f5ccd5e0", size = 139737 }, + { url = "https://files.pythonhosted.org/packages/6a/8c/ae00d7d0ab8a4490b1efeb01ad4ab2f1982e69cc82490bf8093407718ff5/orjson-3.10.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f71eae9651465dff70aa80db92586ad5b92df46a9373ee55252109bb6b703307", size = 154804 }, + { url = "https://files.pythonhosted.org/packages/22/86/65dc69bd88b6dd254535310e97bc518aa50a39ef9c5a2a5d518e7a223710/orjson-3.10.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e117eb299a35f2634e25ed120c37c641398826c2f5a3d3cc39f5993b96171b9e", size = 130583 }, + { url = "https://files.pythonhosted.org/packages/bb/00/6fe01ededb05d52be42fabb13d93a36e51f1fd9be173bd95707d11a8a860/orjson-3.10.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13242f12d295e83c2955756a574ddd6741c81e5b99f2bef8ed8d53e47a01e4b7", size = 138465 }, + { url = "https://files.pythonhosted.org/packages/db/2f/4cc151c4b471b0cdc8cb29d3eadbce5007eb0475d26fa26ed123dca93b33/orjson-3.10.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7946922ada8f3e0b7b958cc3eb22cfcf6c0df83d1fe5521b4a100103e3fa84c8", size = 130742 }, + { url = "https://files.pythonhosted.org/packages/9f/13/8a6109e4b477c518498ca37963d9c0eb1508b259725553fb53d53b20e2ea/orjson-3.10.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b7155eb1623347f0f22c38c9abdd738b287e39b9982e1da227503387b81b34ca", size = 414669 }, + { url = "https://files.pythonhosted.org/packages/22/7b/1d229d6d24644ed4d0a803de1b0e2df832032d5beda7346831c78191b5b2/orjson-3.10.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:208beedfa807c922da4e81061dafa9c8489c6328934ca2a562efa707e049e561", size = 141043 }, + { url = "https://files.pythonhosted.org/packages/cc/d3/6dc91156cf12ed86bed383bcb942d84d23304a1e57b7ab030bf60ea130d6/orjson-3.10.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eca81f83b1b8c07449e1d6ff7074e82e3fd6777e588f1a6632127f286a968825", size = 129826 }, + { url = "https://files.pythonhosted.org/packages/b3/38/c47c25b86f6996f1343be721b6ea4367bc1c8bc0fc3f6bbcd995d18cb19d/orjson-3.10.15-cp313-cp313-win32.whl", hash = "sha256:c03cd6eea1bd3b949d0d007c8d57049aa2b39bd49f58b4b2af571a5d3833d890", size = 142542 }, + { url = "https://files.pythonhosted.org/packages/27/f1/1d7ec15b20f8ce9300bc850de1e059132b88990e46cd0ccac29cbf11e4f9/orjson-3.10.15-cp313-cp313-win_amd64.whl", hash = "sha256:fd56a26a04f6ba5fb2045b0acc487a63162a958ed837648c5781e1fe3316cfbf", size = 133444 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "polars" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/df/55127a3099e990b45ce3a29ab6789a083451e76e7109fb754aad5525360b/polars-1.12.0.tar.gz", hash = "sha256:fb5c92de1a8f7d0a3f923fe48ea89eb518bdf55315ae917012350fa072bd64f4", size = 4090738 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/ae/77c7ec395d9361ae2086693af1947c9a2b21346ba3faf092bb154b735227/polars-1.12.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f3c4e4e423c373dda07b4c8a7ff12aa02094b524767d0ca306b1eba67f2d99e", size = 32923786 }, + { url = "https://files.pythonhosted.org/packages/97/1c/60736d5588309eb528c52538e116593cb275310bab82ba28702cd87a76d1/polars-1.12.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:aa6f9862f0cec6353243920d9b8d858c21ec8f25f91af203dea6ff91980e140d", size = 28887255 }, + { url = "https://files.pythonhosted.org/packages/5a/3e/31257118e7e087fa27c230b8fadf8ff15d521140bf58558dc889ee0c9c5e/polars-1.12.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afb03647b5160737d2119532ee8ffe825de1d19d87f81bbbb005131786f7d59b", size = 34126501 }, + { url = "https://files.pythonhosted.org/packages/ad/e6/d03053e6064d262f2ec41172a5092b08fc20d10c059dda6c9460371cfd7e/polars-1.12.0-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:ea96aba5eb3dab8f0e6abf05ab3fc2136b329261860ef8661d20f5456a2d78e0", size = 30479546 }, + { url = "https://files.pythonhosted.org/packages/d5/28/3d44ddf56a5c95272b202ce8aa0e9b818a1310e83525c4c29176b538ae7c/polars-1.12.0-cp39-abi3-win_amd64.whl", hash = "sha256:a228a4b320a36d03a9ec9dfe7241b6d80a2f119b2dceb1da953166655e4cf43c", size = 33790337 }, +] + +[[package]] +name = "pre-commit" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/13/b62d075317d8686071eb843f0bb1f195eb332f48869d3c31a4c6f1e063ac/pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4", size = 193330 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/b3/df14c580d82b9627d173ceea305ba898dca135feb360b6d84019d0803d3b/pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b", size = 220560 }, +] + +[[package]] +name = "prometheus-client" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/14/7d0f567991f3a9af8d1cd4f619040c93b68f09a02b6d0b6ab1b2d1ded5fe/prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb", size = 78551 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/c2/ab7d37426c179ceb9aeb109a85cda8948bb269b7561a0be870cc656eefe4/prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301", size = 54682 }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.50" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 }, +] + +[[package]] +name = "propcache" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/c8/2a13f78d82211490855b2fb303b6721348d0787fdd9a12ac46d99d3acde1/propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64", size = 41735 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/2a/329e0547cf2def8857157f9477669043e75524cc3e6251cef332b3ff256f/propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc", size = 77002 }, + { url = "https://files.pythonhosted.org/packages/12/2d/c4df5415e2382f840dc2ecbca0eeb2293024bc28e57a80392f2012b4708c/propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9", size = 44639 }, + { url = "https://files.pythonhosted.org/packages/d0/5a/21aaa4ea2f326edaa4e240959ac8b8386ea31dedfdaa636a3544d9e7a408/propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439", size = 44049 }, + { url = "https://files.pythonhosted.org/packages/4e/3e/021b6cd86c0acc90d74784ccbb66808b0bd36067a1bf3e2deb0f3845f618/propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536", size = 224819 }, + { url = "https://files.pythonhosted.org/packages/3c/57/c2fdeed1b3b8918b1770a133ba5c43ad3d78e18285b0c06364861ef5cc38/propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629", size = 229625 }, + { url = "https://files.pythonhosted.org/packages/9d/81/70d4ff57bf2877b5780b466471bebf5892f851a7e2ca0ae7ffd728220281/propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b", size = 232934 }, + { url = "https://files.pythonhosted.org/packages/3c/b9/bb51ea95d73b3fb4100cb95adbd4e1acaf2cbb1fd1083f5468eeb4a099a8/propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052", size = 227361 }, + { url = "https://files.pythonhosted.org/packages/f1/20/3c6d696cd6fd70b29445960cc803b1851a1131e7a2e4ee261ee48e002bcd/propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce", size = 213904 }, + { url = "https://files.pythonhosted.org/packages/a1/cb/1593bfc5ac6d40c010fa823f128056d6bc25b667f5393781e37d62f12005/propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d", size = 212632 }, + { url = "https://files.pythonhosted.org/packages/6d/5c/e95617e222be14a34c709442a0ec179f3207f8a2b900273720501a70ec5e/propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce", size = 207897 }, + { url = "https://files.pythonhosted.org/packages/8e/3b/56c5ab3dc00f6375fbcdeefdede5adf9bee94f1fab04adc8db118f0f9e25/propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95", size = 208118 }, + { url = "https://files.pythonhosted.org/packages/86/25/d7ef738323fbc6ebcbce33eb2a19c5e07a89a3df2fded206065bd5e868a9/propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf", size = 217851 }, + { url = "https://files.pythonhosted.org/packages/b3/77/763e6cef1852cf1ba740590364ec50309b89d1c818e3256d3929eb92fabf/propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f", size = 222630 }, + { url = "https://files.pythonhosted.org/packages/4f/e9/0f86be33602089c701696fbed8d8c4c07b6ee9605c5b7536fd27ed540c5b/propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30", size = 216269 }, + { url = "https://files.pythonhosted.org/packages/cc/02/5ac83217d522394b6a2e81a2e888167e7ca629ef6569a3f09852d6dcb01a/propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6", size = 39472 }, + { url = "https://files.pythonhosted.org/packages/f4/33/d6f5420252a36034bc8a3a01171bc55b4bff5df50d1c63d9caa50693662f/propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1", size = 43363 }, + { url = "https://files.pythonhosted.org/packages/41/b6/c5319caea262f4821995dca2107483b94a3345d4607ad797c76cb9c36bcc/propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54", size = 11818 }, +] + +[[package]] +name = "proto-plus" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/79/a5c6cbb42268cfd3ddc652dc526889044a8798c688a03ff58e5e92b743c8/proto_plus-1.26.0.tar.gz", hash = "sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22", size = 56136 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/c3/59308ccc07b34980f9d532f7afc718a9f32b40e52cde7a740df8d55632fb/proto_plus-1.26.0-py3-none-any.whl", hash = "sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7", size = 50166 }, +] + +[[package]] +name = "protobuf" +version = "5.29.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/d1/e0a911544ca9993e0f17ce6d3cc0932752356c1b0a834397f28e63479344/protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620", size = 424945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/7a/1e38f3cafa022f477ca0f57a1f49962f21ad25850c3ca0acd3b9d0091518/protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888", size = 422708 }, + { url = "https://files.pythonhosted.org/packages/61/fa/aae8e10512b83de633f2646506a6d835b151edf4b30d18d73afd01447253/protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a", size = 434508 }, + { url = "https://files.pythonhosted.org/packages/dd/04/3eaedc2ba17a088961d0e3bd396eac764450f431621b58a04ce898acd126/protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e", size = 417825 }, + { url = "https://files.pythonhosted.org/packages/4f/06/7c467744d23c3979ce250397e26d8ad8eeb2bea7b18ca12ad58313c1b8d5/protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84", size = 319573 }, + { url = "https://files.pythonhosted.org/packages/a8/45/2ebbde52ad2be18d3675b6bee50e68cd73c9e0654de77d595540b5129df8/protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f", size = 319672 }, + { url = "https://files.pythonhosted.org/packages/fd/b2/ab07b09e0f6d143dfb839693aa05765257bceaa13d03bf1a696b78323e7a/protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f", size = 172550 }, +] + +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699 }, + { url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245 }, + { url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631 }, + { url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140 }, + { url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762 }, + { url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967 }, + { url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326 }, + { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712 }, + { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155 }, + { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356 }, + { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/67/6afbf0d507f73c32d21084a79946bfcfca5fbc62a72057e9c23797a737c9/pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c", size = 310028 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/89/bc88a6711935ba795a679ea6ebee07e128050d6382eaa35a0a47c8032bdc/pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", size = 181537 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pycryptodome" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/52/13b9db4a913eee948152a079fe58d035bd3d1a519584155da8e786f767e6/pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297", size = 4818071 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/88/5e83de10450027c96c79dc65ac45e9d0d7a7fef334f39d3789a191f33602/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4", size = 2495937 }, + { url = "https://files.pythonhosted.org/packages/66/e1/8f28cd8cf7f7563319819d1e172879ccce2333781ae38da61c28fe22d6ff/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b", size = 1634629 }, + { url = "https://files.pythonhosted.org/packages/6a/c1/f75a1aaff0c20c11df8dc8e2bf8057e7f73296af7dfd8cbb40077d1c930d/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e", size = 2168708 }, + { url = "https://files.pythonhosted.org/packages/ea/66/6f2b7ddb457b19f73b82053ecc83ba768680609d56dd457dbc7e902c41aa/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8", size = 2254555 }, + { url = "https://files.pythonhosted.org/packages/2c/2b/152c330732a887a86cbf591ed69bd1b489439b5464806adb270f169ec139/pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1", size = 2294143 }, + { url = "https://files.pythonhosted.org/packages/55/92/517c5c498c2980c1b6d6b9965dffbe31f3cd7f20f40d00ec4069559c5902/pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a", size = 2160509 }, + { url = "https://files.pythonhosted.org/packages/39/1f/c74288f54d80a20a78da87df1818c6464ac1041d10988bb7d982c4153fbc/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2", size = 2329480 }, + { url = "https://files.pythonhosted.org/packages/39/1b/d0b013bf7d1af7cf0a6a4fce13f5fe5813ab225313755367b36e714a63f8/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93", size = 2254397 }, + { url = "https://files.pythonhosted.org/packages/14/71/4cbd3870d3e926c34706f705d6793159ac49d9a213e3ababcdade5864663/pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764", size = 1775641 }, + { url = "https://files.pythonhosted.org/packages/43/1d/81d59d228381576b92ecede5cd7239762c14001a828bdba30d64896e9778/pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53", size = 1812863 }, +] + +[[package]] +name = "pydantic" +version = "2.10.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, +] + +[[package]] +name = "pyparsing" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/1a/3544f4f299a47911c2ab3710f534e52fea62a633c96806995da5d25be4b2/pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a", size = 1067694 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/a7/c8a2d361bf89c0d9577c934ebb7421b25dc84bf3a8e3ac0a40aed9acc547/pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1", size = 107716 }, +] + +[[package]] +name = "pytest" +version = "8.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.25.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/a8/ecbc8ede70921dd2f544ab1cadd3ff3bf842af27f87bbdea774c7baa1d38/pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a", size = 54239 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/17/3493c5624e48fd97156ebaec380dcaafee9506d7e2c46218ceebbb57d7de/pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3", size = 19467 }, +] + +[[package]] +name = "pytest-celery" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "celery" }, + { name = "debugpy" }, + { name = "docker" }, + { name = "psutil" }, + { name = "pytest-docker-tools" }, + { name = "setuptools" }, + { name = "tenacity" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/ed/cf73ad1119f2c9cc009d10a806d21aa165361e4bd04076c76e4f75d51789/pytest_celery-1.1.3.tar.gz", hash = "sha256:ac7eee546b4d9fb5c742eaaece98187f1f5e5f5622fbaa8e7729bb46923c54fc", size = 29770 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/a8/13f300c73143caaf22806953ba0e18b1aef1425357a9070cc2de99101b7c/pytest_celery-1.1.3-py3-none-any.whl", hash = "sha256:4cdb5f658dc472509e8be71f745d26bcb8246397661534f5709d2a55edc43286", size = 49032 }, +] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + +[[package]] +name = "pytest-django" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/10/a096573b4b896f18a8390d9dafaffc054c1f613c60bf838300732e538890/pytest_django-4.10.0.tar.gz", hash = "sha256:1091b20ea1491fd04a310fc9aaff4c01b4e8450e3b157687625e16a6b5f3a366", size = 84710 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/4c/a4fe18205926216e1aebe1f125cba5bce444f91b6e4de4f49fa87e322775/pytest_django-4.10.0-py3-none-any.whl", hash = "sha256:57c74ef3aa9d89cae5a5d73fbb69a720a62673ade7ff13b9491872409a3f5918", size = 23975 }, +] + +[[package]] +name = "pytest-docker-tools" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docker" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/a2/620ff42d20a2c2b107805a12633a2cb9eb01db3a4eb371a6bc1f71728217/pytest_docker_tools-3.1.3.tar.gz", hash = "sha256:c7e28841839d67b3ac80ad7b345b953701d5ae61ffda97586114244292aeacc0", size = 37136 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/f6/961e9b5c6a3006be78d2725713e0d6b2811dc20ae78b2b21b575185b448d/pytest_docker_tools-3.1.3-py3-none-any.whl", hash = "sha256:63e659043160f41d89f94ea42616102594bcc85682aac394fcbc14f14cd1b189", size = 24807 }, +] + +[[package]] +name = "pytest-freezegun" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "freezegun" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/e3/c39d7c3d3afef5652f19323f3483267d7e6b0d9911c3867e10d6e2d3c9ae/pytest-freezegun-0.4.2.zip", hash = "sha256:19c82d5633751bf3ec92caa481fb5cffaac1787bd485f0df6436fd6242176949", size = 9059 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/09/0bdd7d24b9d21453ad3364ae1efbd65082045bb6081b5fd5eade91a9b644/pytest_freezegun-0.4.2-py2.py3-none-any.whl", hash = "sha256:5318a6bfb8ba4b709c8471c94d0033113877b3ee02da5bfcd917c1889cde99a7", size = 4590 }, +] + +[[package]] +name = "pytest-insta" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/5b/6ca4baca60c3f8361415501668cde3abd94dbad44293325833fd89d1a7c1/pytest_insta-0.3.0.tar.gz", hash = "sha256:9e6e1c70a021f68ccc4643360b2c2f8326cf3befba85f942c1da17b9caf713f7", size = 14960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/d5/1459b2861cf703cf49d96b6f29731ee74f4ac7e34b0c60b0ff75bdd318bc/pytest_insta-0.3.0-py3-none-any.whl", hash = "sha256:93a105e3850f2887b120a581923b10bb313d722e00d369377a1d91aa535df704", size = 13660 }, +] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, +] + +[[package]] +name = "pytest-sqlalchemy" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "sqlalchemy" }, + { name = "sqlalchemy-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/b3/7f959345fe74c6e471e3edf302ea26822e3ecd33fe3ab6f713f737d8b82f/pytest-sqlalchemy-0.2.1.tar.gz", hash = "sha256:b5c51b7160713b330c0e8f8cef61a00ffca2ccd582357a13be38fe4d9ed67d7e", size = 2865 } + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-json-logger" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/c4/358cd13daa1d912ef795010897a483ab2f0b41c9ea1b35235a8b2f7d15a7/python_json_logger-3.2.1.tar.gz", hash = "sha256:8eb0554ea17cb75b05d2848bc14fb02fbdbd9d6972120781b974380bfa162008", size = 16287 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/72/2f30cf26664fcfa0bd8ec5ee62ec90c03bd485e4a294d92aabc76c5203a5/python_json_logger-3.2.1-py3-none-any.whl", hash = "sha256:cdc17047eb5374bd311e748b42f99d71223f3b0e186f4206cc5d52aefe85b090", size = 14924 }, +] + +[[package]] +name = "python-redis-lock" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/d7/a2a97c73d39e68aacce02667885b9e0b575eb9082866a04fbf098b4c4d99/python-redis-lock-4.0.0.tar.gz", hash = "sha256:4abd0bcf49136acad66727bf5486dd2494078ca55e49efa693f794077319091a", size = 162533 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/70/c5dfaec2085d9be10792704f108543ba1802e228bf040632c673066d8e78/python_redis_lock-4.0.0-py3-none-any.whl", hash = "sha256:ff786e587569415f31e64ca9337fce47c4206e832776e9e42b83bfb9ee1af4bd", size = 12165 }, +] + +[[package]] +name = "pytz" +version = "2025.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/57/df1c9157c8d5a05117e455d66fd7cf6dbc46974f832b1058ed4856785d8a/pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e", size = 319617 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930 }, +] + +[[package]] +name = "pywin32" +version = "308" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/a4/aa562d8935e3df5e49c161b427a3a2efad2ed4e9cf81c3de636f1fdddfd0/pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed", size = 5938579 }, + { url = "https://files.pythonhosted.org/packages/c7/50/b0efb8bb66210da67a53ab95fd7a98826a97ee21f1d22949863e6d588b22/pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4", size = 6542056 }, + { url = "https://files.pythonhosted.org/packages/26/df/2b63e3e4f2df0224f8aaf6d131f54fe4e8c96400eb9df563e2aae2e1a1f9/pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", size = 7974986 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "redis" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 }, +] + +[[package]] +name = "regex" +version = "2024.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "respx" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127 }, +] + +[[package]] +name = "rsa" +version = "4.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21", size = 29711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315 }, +] + +[[package]] +name = "ruff" +version = "0.9.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/59/ac745a2492986a4c900c73a7a3a10eb4d7a3853e43443519bceecae5eefc/ruff-0.9.8.tar.gz", hash = "sha256:12d455f2be6fe98accbea2487bbb8eaec716c760bf60b45e7e13f76f913f56e9", size = 3715230 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/1c/9de3a463279e9a203104fe80881d7dcfd8377eb52b3d5608770ea6ff3dc6/ruff-0.9.8-py3-none-linux_armv6l.whl", hash = "sha256:d236f0ce0190bbc6fa9b4c4b85e916fb4c50fd087e6558af1bf5a45eb20e374d", size = 10036520 }, + { url = "https://files.pythonhosted.org/packages/35/10/a4eda083ad0b60a4c16bc9a68c6eda59de69a3a58913a0b62541f5c551cd/ruff-0.9.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:59fac6922b336d0c38df199761ade561563e1b7636e3a2b767b9ee5a68aa9cbf", size = 10827099 }, + { url = "https://files.pythonhosted.org/packages/57/34/cf7e18f2315926ee2c98f931717e1302f8c3face189f5b99352eb48c5373/ruff-0.9.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a82082ec72bde2166ec138055307396c4d4e543fd97266dc2bfa24284cb30af6", size = 10161605 }, + { url = "https://files.pythonhosted.org/packages/f3/08/5e7e8fc08d193e3520b9227249a00bc9b8da9e0a20bf97bef03a9a9f0d38/ruff-0.9.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e06635d12321605d1d11226c7d3c6b1245a0df498099868d14b4e353b3f0ac22", size = 10338840 }, + { url = "https://files.pythonhosted.org/packages/54/c0/df2187618b87334867ea7942f6d2d79ea3e5cb3ed709cfa5c8df115d3715/ruff-0.9.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:65961815bb35d427e957940d13b2a1d0a67d8b245d3a7e0b5a4a2058536d3532", size = 9891009 }, + { url = "https://files.pythonhosted.org/packages/fb/39/8fc50b87203e71e6f3281111813ab0f3d6095cb1129efc2cf4c33e977657/ruff-0.9.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c18356beaef174797ad83f11debc5569e96afa73a549b2d073912565cfc4cfd1", size = 11413420 }, + { url = "https://files.pythonhosted.org/packages/6a/7b/53cd91b99a1cef31126859fb98fdc347c47e0047a9ec51391ea28f08284d/ruff-0.9.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a1dfc443bee0288ea926a4d9ecfd858bf94ddf0a03a256c63e81b2b6dccdfc7d", size = 12138017 }, + { url = "https://files.pythonhosted.org/packages/1a/d4/949a328934202a2d2641dcd759761d8ed806e672cbbad0a88e20a46c43ba/ruff-0.9.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc86d5a85cd5ab1d5aff1650f038aa34681d0692cc2467aa9ddef37bd56ea3f9", size = 11592548 }, + { url = "https://files.pythonhosted.org/packages/c6/8e/8520a4d97eefedb8472811fd5144fcb1fcbb29f83bb9bb4356a468e7eeac/ruff-0.9.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:66662aa19535d58fe6d04e5b59a39e495b102f2f5a2a1b9698e240eb78f429ef", size = 13787277 }, + { url = "https://files.pythonhosted.org/packages/24/68/f1629e00dbc5c9adcd31f12f9438b68c50ab0eefca8b07e11b6c94f11b09/ruff-0.9.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:733647b2fe9367e1aa049c0eba296363746f3bc0dbfd454b0bc4b7b46cdf0146", size = 11275421 }, + { url = "https://files.pythonhosted.org/packages/28/65/c133462f179b925e49910532c7d7b5a244df5995c155cd2ab9452545926f/ruff-0.9.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:100031be9777f67af7f61b4d4eea2a0531ed6788940aca4360f6b9aae317c53b", size = 10220273 }, + { url = "https://files.pythonhosted.org/packages/d8/1e/9339aef1896470380838385dbdc91f62998c37d406009f05ff3b810265f3/ruff-0.9.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f090758d58b4667d9022eee1085a854db93d800279e5a177ebda5adc1faf639", size = 9860266 }, + { url = "https://files.pythonhosted.org/packages/ca/33/2a2934860df6bd3665776ec686fc33910e7a1b793bdd2f000aea3e8f0b65/ruff-0.9.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f774998b9c9a062510533aba9b53085de6be6d41e13a7a0bd086af8a40e838c3", size = 10831947 }, + { url = "https://files.pythonhosted.org/packages/74/66/0a7677b1cda4b2367a654f9af57f1dbe58f38c6704da88aee9bbf3941197/ruff-0.9.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6ef7cc80626264ab8ab4d68b359ba867b8a52b0830a9643cd31289146dd40892", size = 11306767 }, + { url = "https://files.pythonhosted.org/packages/c4/90/6c98f94e036c8acdf19bd8f3f84d246e43cbcc950e24dc7ff85d2f2735ba/ruff-0.9.8-py3-none-win32.whl", hash = "sha256:54b57b623a683e696a1ede99db95500763c1badafe105b6ad8d8e9d96e385ae2", size = 10234107 }, + { url = "https://files.pythonhosted.org/packages/f5/e7/35877491b4b64daa35cbd7dc06aa5969e7bb1cd6f69e5594e4376dfbc16d/ruff-0.9.8-py3-none-win_amd64.whl", hash = "sha256:b0878103b2fb8af55ad701308a69ce713108ad346c3a3a143ebcd1e13829c9a7", size = 11357825 }, + { url = "https://files.pythonhosted.org/packages/6e/98/de77a972b2e9ded804dea5d4e6fbfa093d99e81092602567787ea87979af/ruff-0.9.8-py3-none-win_arm64.whl", hash = "sha256:e459a4fc4150fcc60da26c59a6a4b70878c60a99df865a71cf6f958dc68c419a", size = 10435420 }, +] + +[[package]] +name = "s3transfer" +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/45/2323b5928f86fd29f9afdcef4659f68fa73eaa5356912b774227f5cf46b5/s3transfer-0.11.2.tar.gz", hash = "sha256:3b39185cb72f5acc77db1a58b6e25b977f28d20496b6e58d6813d75f464d632f", size = 147885 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/ac/e7dc469e49048dc57f62e0c555d2ee3117fa30813d2a1a2962cce3a2a82a/s3transfer-0.11.2-py3-none-any.whl", hash = "sha256:be6ecb39fadd986ef1701097771f87e4d2f821f27f6071c872143884d2950fbc", size = 84151 }, +] + +[[package]] +name = "sentry-sdk" +version = "2.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/b6/662988ecd2345bf6c3a5c306a9a3590852742eff91d0a78a143398b816f3/sentry_sdk-2.22.0.tar.gz", hash = "sha256:b4bf43bb38f547c84b2eadcefbe389b36ef75f3f38253d7a74d6b928c07ae944", size = 303539 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/7f/0e4459173e9671ba5f75a48dda2442bcc48a12c79e54e5789381c8c6a9bc/sentry_sdk-2.22.0-py2.py3-none-any.whl", hash = "sha256:3d791d631a6c97aad4da7074081a57073126c69487560c6f8bffcf586461de66", size = 325815 }, +] + +[[package]] +name = "setuptools" +version = "75.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 }, +] + +[[package]] +name = "shared" +version = "0.1.0" +source = { directory = "../../libs/shared" } +dependencies = [ + { name = "amplitude-analytics" }, + { name = "boto3" }, + { name = "cachetools" }, + { name = "cerberus" }, + { name = "codecov-ribs" }, + { name = "colour" }, + { name = "cryptography" }, + { name = "django" }, + { name = "django-better-admin-arrayfield" }, + { name = "django-model-utils" }, + { name = "django-postgres-extra" }, + { name = "django-prometheus" }, + { name = "google-auth" }, + { name = "google-cloud-pubsub" }, + { name = "google-cloud-storage" }, + { name = "httpx" }, + { name = "ijson" }, + { name = "minio" }, + { name = "mmh3" }, + { name = "oauthlib" }, + { name = "orjson" }, + { name = "prometheus-client" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "pyparsing" }, + { name = "python-redis-lock" }, + { name = "pyyaml" }, + { name = "redis" }, + { name = "requests" }, + { name = "sentry-sdk" }, + { name = "sqlalchemy" }, + { name = "zstandard" }, +] + +[package.metadata] +requires-dist = [ + { name = "amplitude-analytics", specifier = ">=1.1.4" }, + { name = "boto3", specifier = ">=1.20.25" }, + { name = "cachetools", specifier = ">=4.1.1" }, + { name = "cerberus", specifier = ">=1.3.5" }, + { name = "codecov-ribs", specifier = ">=0.1.18" }, + { name = "colour", specifier = ">=0.1.5" }, + { name = "cryptography", specifier = ">=43.0.1" }, + { name = "django", specifier = "<5" }, + { name = "django-better-admin-arrayfield", specifier = ">=1.4.2" }, + { name = "django-model-utils", specifier = ">=4.5.1" }, + { name = "django-postgres-extra", specifier = ">=2.0.8" }, + { name = "django-prometheus", specifier = ">=2.3.1" }, + { name = "google-auth", specifier = ">=2.21.0" }, + { name = "google-cloud-pubsub", specifier = ">=2.18.4" }, + { name = "google-cloud-storage", specifier = ">=2.18.2" }, + { name = "httpx", specifier = ">=0.23.0" }, + { name = "ijson", specifier = ">=3.2.3" }, + { name = "minio", specifier = ">=7.1.13" }, + { name = "mmh3", specifier = ">=4.0.1" }, + { name = "oauthlib", specifier = ">=3.1.0" }, + { name = "orjson", specifier = ">=3.10.9" }, + { name = "prometheus-client", specifier = ">=0.17.1" }, + { name = "pydantic", specifier = ">=2.10.4" }, + { name = "pyjwt", specifier = ">=2.8.0" }, + { name = "pyparsing", specifier = ">=2.4.7" }, + { name = "python-redis-lock", specifier = ">=4.0.0" }, + { name = "pyyaml", specifier = ">=6.0.1" }, + { name = "redis", specifier = ">=4.4.4" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "sentry-sdk", specifier = ">=2.13.0" }, + { name = "sqlalchemy", specifier = "<2" }, + { name = "zstandard", specifier = ">=0.23.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "factory-boy", specifier = ">=3.2.0" }, + { name = "freezegun", specifier = ">=1.1.0" }, + { name = "mock", specifier = ">=4.0.3" }, + { name = "mypy", specifier = ">=1.13.0" }, + { name = "pre-commit", specifier = ">=2.11.1" }, + { name = "psycopg2-binary", specifier = ">=2.9.2" }, + { name = "pytest", specifier = ">=8.1.1" }, + { name = "pytest-asyncio", specifier = ">=0.14.0" }, + { name = "pytest-codspeed", specifier = ">=3.2.0" }, + { name = "pytest-cov", specifier = ">=5.0.0" }, + { name = "pytest-django", specifier = ">=4.7.0" }, + { name = "pytest-mock", specifier = ">=1.13.0" }, + { name = "respx", specifier = ">=0.20.2" }, + { name = "ruff", specifier = ">=0.9.0" }, + { name = "types-mock", specifier = ">=5.1.0.20240425" }, + { name = "types-requests", specifier = ">=2.31.0.6" }, + { name = "urllib3", specifier = "==1.26.19" }, + { name = "vcrpy", specifier = ">=4.1.1" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "sqlalchemy" +version = "1.3.24" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/ab/81bef2f960abf3cdaf32fbf1994f0c6f5e6a5f1667b5713ed6ebf162b6a2/SQLAlchemy-1.3.24.tar.gz", hash = "sha256:ebbb777cbf9312359b897bf81ba00dae0f5cb69fba2a18265dcc18a6f5ef7519", size = 6353598 } + +[[package]] +name = "sqlalchemy-utils" +version = "0.41.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/bf/abfd5474cdd89ddd36dbbde9c6efba16bfa7f5448913eba946fed14729da/SQLAlchemy-Utils-0.41.2.tar.gz", hash = "sha256:bc599c8c3b3319e53ce6c5c3c471120bd325d0071fb6f38a10e924e3d07b9990", size = 138017 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/f0/dc4757b83ac1ab853cf222df8535ed73973e0c203d983982ba7b8bc60508/SQLAlchemy_Utils-0.41.2-py3-none-any.whl", hash = "sha256:85cf3842da2bf060760f955f8467b87983fb2e30f1764fd0e24a48307dc8ec6e", size = 93083 }, +] + +[[package]] +name = "sqlparse" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/26/5da251cd090ccd580f5cfaa7d36cdd8b2471e49fffce60ed520afc27f4bc/sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93", size = 83475 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/5d/a0fdd88fd486b39ae1fd1a75ff75b4e29a0df96c0304d462fd407b82efe0/sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663", size = 43971 }, +] + +[[package]] +name = "statsd" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/29/05e9f50946f4cf2ed182726c60d9c0ae523bb3f180588c574dd9746de557/statsd-4.0.1.tar.gz", hash = "sha256:99763da81bfea8daf6b3d22d11aaccb01a8d0f52ea521daab37e758a4ca7d128", size = 27814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/d0/c9543b52c067a390ae6ae632d7fd1b97a35cdc8d69d40c0b7d334b326410/statsd-4.0.1-py2.py3-none-any.whl", hash = "sha256:c2676519927f7afade3723aca9ca8ea986ef5b059556a980a867721ca69df093", size = 13118 }, +] + +[[package]] +name = "stripe" +version = "11.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/49/902b7cb88e788754c529710598a5110242f788c8694ffbbacbf58a3c6882/stripe-11.5.0.tar.gz", hash = "sha256:bc3e0358ffc23d5ecfa8aafec1fa4f048ee8107c3237bcb00003e68c8c96fa02", size = 1386601 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/76/a5a7b407da86ed47c41e88e8450ea4965e0a49a9cc301602edc6733382d7/stripe-11.5.0-py2.py3-none-any.whl", hash = "sha256:3b2cd47ed3002328249bff5cacaee38d5e756c3899ab425d3bd07acdaf32534a", size = 1633231 }, +] + +[[package]] +name = "tenacity" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/94/91fccdb4b8110642462e653d5dcb27e7b674742ad68efd146367da7bdb10/tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b", size = 47421 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/cb/b86984bed139586d01532a587464b5805f12e397594f19f931c4c2fbfa61/tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539", size = 28169 }, +] + +[[package]] +name = "test-results-parser" +version = "0.5.1" +source = { git = "https://github.com/codecov/test-results-parser?rev=190bbc8a911099749928e13d5fe57f6027ca1e74#190bbc8a911099749928e13d5fe57f6027ca1e74" } + +[[package]] +name = "time-machine" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/dd/5022939b9cadefe3af04f4012186c29b8afbe858b1ec2cfa38baeec94dab/time_machine-2.16.0.tar.gz", hash = "sha256:4a99acc273d2f98add23a89b94d4dd9e14969c01214c8514bfa78e4e9364c7e2", size = 24626 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/18/3087d0eb185cedbc82385f46bf16032ec7102a0e070205a2c88c4ecf9952/time_machine-2.16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7751bf745d54e9e8b358c0afa332815da9b8a6194b26d0fd62876ab6c4d5c9c0", size = 20209 }, + { url = "https://files.pythonhosted.org/packages/03/a3/fcc3eaf69390402ecf491d718e533b6d0e06d944d77fc8d87be3a2839102/time_machine-2.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1784edf173ca840ba154de6eed000b5727f65ab92972c2f88cec5c4d6349c5f2", size = 16681 }, + { url = "https://files.pythonhosted.org/packages/a2/96/8b76d264014bf9dc21873218de50d67223c71736f87fe6c65e582f7c29ac/time_machine-2.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f5876a5682ce1f517e55d7ace2383432627889f6f7e338b961f99d684fd9e8d", size = 33768 }, + { url = "https://files.pythonhosted.org/packages/5c/13/59ae8259be02b6c657ef6e3b6952bf274b43849f6f35cc61a576c68ce301/time_machine-2.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:806672529a2e255cd901f244c9033767dc1fa53466d0d3e3e49565a1572a64fe", size = 31685 }, + { url = "https://files.pythonhosted.org/packages/3e/c1/9f142beb4d373a2a01ebb58d5117289315baa5131d880ec804db49e94bf7/time_machine-2.16.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:667b150fedb54acdca2a4bea5bf6da837b43e6dd12857301b48191f8803ba93f", size = 33447 }, + { url = "https://files.pythonhosted.org/packages/95/f7/ed9ecd93c2d38dca77d0a28e070020f3ce0fb23e0d4a6edb14bcfffa5526/time_machine-2.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:da3ae1028af240c0c46c79adf9c1acffecc6ed1701f2863b8132f5ceae6ae4b5", size = 33408 }, + { url = "https://files.pythonhosted.org/packages/91/40/d0d274d70fa2c4cad531745deb8c81346365beb0a2736be05a3acde8b94a/time_machine-2.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:520a814ea1b2706c89ab260a54023033d3015abef25c77873b83e3d7c1fafbb2", size = 31526 }, + { url = "https://files.pythonhosted.org/packages/1d/ba/a27cdbb324d9a6d779cde0d514d47b696b5a6a653705d4b511fd65ef1514/time_machine-2.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8243664438bb468408b29c6865958662d75e51f79c91842d2794fa22629eb697", size = 33042 }, + { url = "https://files.pythonhosted.org/packages/72/63/64e9156c9e38c18720d0cc41378168635241de44013ffe3dd5b099447eb0/time_machine-2.16.0-cp313-cp313-win32.whl", hash = "sha256:32d445ce20d25c60ab92153c073942b0bac9815bfbfd152ce3dcc225d15ce988", size = 19108 }, + { url = "https://files.pythonhosted.org/packages/3d/40/27f5738fbd50b78dcc0682c14417eac5a49ccf430525dd0c5a058be125a2/time_machine-2.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:f6927dda86425f97ffda36131f297b1a601c64a6ee6838bfa0e6d3149c2f0d9f", size = 19935 }, + { url = "https://files.pythonhosted.org/packages/35/75/c4d8b2f0fe7dac22854d88a9c509d428e78ac4bf284bc54cfe83f75cc13b/time_machine-2.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:4d3843143c46dddca6491a954bbd0abfd435681512ac343169560e9bab504129", size = 18047 }, +] + +[[package]] +name = "timestring" +version = "1.6.4" +source = { git = "https://github.com/codecov/timestring?rev=d37ceacc5954dff3b5bd2f887936a98a668dda42#d37ceacc5954dff3b5bd2f887936a98a668dda42" } +dependencies = [ + { name = "pytz" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "tzdata" +version = "2025.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/0f/fa4723f22942480be4ca9527bbde8d43f6c3f2fe8412f00e7f5f6746bc8b/tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", size = 194950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 }, +] + +[[package]] +name = "urllib3" +version = "1.26.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/93/65e479b023bbc46dab3e092bda6b0005424ea3217d711964ccdede3f9b1b/urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429", size = 306068 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/6a/99eaaeae8becaa17a29aeb334a18e5d582d873b6f084c11f02581b8d7f7f/urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3", size = 143933 }, +] + +[[package]] +name = "vcrpy" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "urllib3" }, + { name = "wrapt" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/d3/856e06184d4572aada1dd559ddec3bedc46df1f2edc5ab2c91121a2cccdb/vcrpy-7.0.0.tar.gz", hash = "sha256:176391ad0425edde1680c5b20738ea3dc7fb942520a48d2993448050986b3a50", size = 85502 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/5d/1f15b252890c968d42b348d1e9b0aa12d5bf3e776704178ec37cceccdb63/vcrpy-7.0.0-py2.py3-none-any.whl", hash = "sha256:55791e26c18daa363435054d8b35bd41a4ac441b6676167635d1b37a71dbe124", size = 42321 }, +] + +[[package]] +name = "vine" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636 }, +] + +[[package]] +name = "virtualenv" +version = "20.29.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/88/dacc875dd54a8acadb4bcbfd4e3e86df8be75527116c91d8f9784f5e9cab/virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728", size = 4320272 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/fa/849483d56773ae29740ae70043ad88e068f98a6401aa819b5d6bee604683/virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a", size = 4301478 }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +] + +[[package]] +name = "worker" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "analytics-python" }, + { name = "asgiref" }, + { name = "billiard" }, + { name = "boto3" }, + { name = "celery" }, + { name = "click" }, + { name = "codecov-ribs" }, + { name = "django" }, + { name = "django-postgres-extra" }, + { name = "google-cloud-pubsub" }, + { name = "google-cloud-storage" }, + { name = "grpcio" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "lxml" }, + { name = "mmh3" }, + { name = "multidict" }, + { name = "openai" }, + { name = "orjson" }, + { name = "polars" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "psycopg2-binary" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "python-dateutil" }, + { name = "python-json-logger" }, + { name = "python-redis-lock" }, + { name = "pyyaml" }, + { name = "redis" }, + { name = "regex" }, + { name = "requests" }, + { name = "sentry-sdk" }, + { name = "shared" }, + { name = "sqlalchemy" }, + { name = "sqlparse" }, + { name = "statsd" }, + { name = "stripe" }, + { name = "test-results-parser" }, + { name = "timestring" }, + { name = "zstandard" }, +] + +[package.dev-dependencies] +dev = [ + { name = "coverage" }, + { name = "factory-boy" }, + { name = "mock" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-celery" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "pytest-freezegun" }, + { name = "pytest-insta" }, + { name = "pytest-mock" }, + { name = "pytest-sqlalchemy" }, + { name = "respx" }, + { name = "ruff" }, + { name = "sqlalchemy-utils" }, + { name = "time-machine" }, + { name = "urllib3" }, + { name = "vcrpy" }, +] + +[package.metadata] +requires-dist = [ + { name = "analytics-python", specifier = "==1.3.0b1" }, + { name = "asgiref", specifier = ">=3.7.2" }, + { name = "billiard", specifier = ">=4.2.1" }, + { name = "boto3", specifier = ">=1.34" }, + { name = "celery", specifier = ">=5.3.6" }, + { name = "click", specifier = ">=8.1.7" }, + { name = "codecov-ribs", specifier = "==0.1.18" }, + { name = "django", specifier = ">=4.2.16" }, + { name = "django-postgres-extra", specifier = ">=2.0.8" }, + { name = "google-cloud-pubsub", specifier = ">=2.27.1" }, + { name = "google-cloud-storage", specifier = ">=2.10.0" }, + { name = "grpcio", specifier = ">=1.66.2" }, + { name = "httpx", specifier = ">0.23.1" }, + { name = "jinja2", specifier = ">=3.1.3" }, + { name = "lxml", specifier = ">=5.3.0" }, + { name = "mmh3", specifier = ">=5.0.1" }, + { name = "multidict", specifier = ">=6.1.0" }, + { name = "openai", specifier = ">=1.2.4" }, + { name = "orjson", specifier = ">=3.10.11" }, + { name = "polars", specifier = "==1.12.0" }, + { name = "proto-plus", specifier = ">=1.25.0" }, + { name = "protobuf", specifier = ">=5.29.2" }, + { name = "psycopg2-binary", specifier = ">=2.9.10" }, + { name = "pydantic", specifier = ">=2.9.0" }, + { name = "pyjwt", specifier = ">=2.4.0" }, + { name = "python-dateutil", specifier = ">=2.9.0.post0" }, + { name = "python-json-logger", specifier = ">=0.1.11" }, + { name = "python-redis-lock", specifier = ">=4.0.0" }, + { name = "pyyaml", specifier = ">=6.0.1" }, + { name = "redis", specifier = ">=4.4.4" }, + { name = "regex", specifier = ">=2023.12.25" }, + { name = "requests", specifier = ">=2.32.0" }, + { name = "sentry-sdk", specifier = ">=2.13.0" }, + { name = "shared", directory = "../../libs/shared" }, + { name = "sqlalchemy", specifier = "==1.3.*" }, + { name = "sqlparse", specifier = "==0.5.0" }, + { name = "statsd", specifier = ">=3.3.0" }, + { name = "stripe", specifier = ">=11.4.1" }, + { name = "test-results-parser", git = "https://github.com/codecov/test-results-parser?rev=190bbc8a911099749928e13d5fe57f6027ca1e74" }, + { name = "timestring", git = "https://github.com/codecov/timestring?rev=d37ceacc5954dff3b5bd2f887936a98a668dda42" }, + { name = "zstandard", specifier = ">=0.23.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage", specifier = ">=7.5.0" }, + { name = "factory-boy", specifier = ">=3.2.0" }, + { name = "mock", specifier = ">=4.0.3" }, + { name = "pre-commit", specifier = ">=3.4.0" }, + { name = "pytest", specifier = ">=8.1.1" }, + { name = "pytest-asyncio", specifier = ">=0.14.0" }, + { name = "pytest-celery", specifier = ">=0.0.0" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "pytest-django", specifier = ">=4.7.0" }, + { name = "pytest-freezegun", specifier = ">=0.4.2" }, + { name = "pytest-insta", specifier = ">=0.3.0" }, + { name = "pytest-mock", specifier = ">=1.13.0" }, + { name = "pytest-sqlalchemy", specifier = ">=0.2.1" }, + { name = "respx", specifier = ">=0.20.2" }, + { name = "ruff", specifier = ">=0.9.8" }, + { name = "sqlalchemy-utils", specifier = ">=0.41.2" }, + { name = "time-machine", specifier = ">=2.16.0" }, + { name = "urllib3", specifier = "==1.26.19" }, + { name = "vcrpy", specifier = ">=6.0.0" }, +] + +[[package]] +name = "wrapt" +version = "1.17.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800 }, + { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824 }, + { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920 }, + { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690 }, + { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861 }, + { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174 }, + { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721 }, + { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763 }, + { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585 }, + { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676 }, + { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871 }, + { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312 }, + { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062 }, + { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155 }, + { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471 }, + { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208 }, + { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339 }, + { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232 }, + { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476 }, + { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 }, + { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 }, + { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 }, + { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, +] + +[[package]] +name = "yarl" +version = "1.18.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789 }, + { url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144 }, + { url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974 }, + { url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587 }, + { url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386 }, + { url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421 }, + { url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384 }, + { url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689 }, + { url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453 }, + { url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872 }, + { url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497 }, + { url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981 }, + { url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229 }, + { url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383 }, + { url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152 }, + { url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723 }, + { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, +] + +[[package]] +name = "zipp" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, +] + +[[package]] +name = "zstandard" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation == 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975 }, + { url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448 }, + { url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269 }, + { url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228 }, + { url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891 }, + { url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310 }, + { url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912 }, + { url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946 }, + { url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994 }, + { url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681 }, + { url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239 }, + { url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149 }, + { url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392 }, + { url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299 }, + { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862 }, + { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578 }, +] diff --git a/apps/worker/worker.sh b/apps/worker/worker.sh new file mode 100755 index 0000000000..41e8e1015d --- /dev/null +++ b/apps/worker/worker.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +if [ -n "$PROMETHEUS_MULTIPROC_DIR" ]; then + rm -r "$PROMETHEUS_MULTIPROC_DIR" 2> /dev/null + mkdir "$PROMETHEUS_MULTIPROC_DIR" +fi + +queues="" +if [ "$CODECOV_WORKER_QUEUES" ]; then + queues="--queue $CODECOV_WORKER_QUEUES" +fi + +if [ "$RUN_ENV" = "ENTERPRISE" ] || [ "$RUN_ENV" = "DEV" ]; then + python manage.py migrate + python manage.py migrate --database "timeseries" + python manage.py migrate --database "ta_timeseries" +fi + +if [ -z "$1" ]; +then + python main.py worker ${queues} +else + exec "$@" +fi diff --git a/docker-compose.yml b/docker-compose.yml index f168da37ca..80db8c4aa3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,7 @@ services: - codecov api: - image: codecov/api + image: codecov/api-umbrella entrypoint: sh -c "/devenv-scripts/start-api.sh" environment: - RUN_ENV=DEV @@ -53,7 +53,7 @@ services: - redis worker: - image: codecov/worker + image: codecov/worker-umbrella entrypoint: sh -c "/devenv-scripts/start-worker.sh" environment: - RUN_ENV=DEV @@ -124,7 +124,7 @@ services: - codecov minio: - image: minio/minio:RELEASE.2020-04-15T00-39-01Z + image: minio/minio:latest command: server /export ports: - "${CODECOV_MINIO_PORT-9000}:9000" diff --git a/docker/Dockerfile.requirements b/docker/Dockerfile.requirements index c27b8369d1..9eec9d3793 100644 --- a/docker/Dockerfile.requirements +++ b/docker/Dockerfile.requirements @@ -1,16 +1,14 @@ # syntax=docker/dockerfile:1.4 -ARG PYTHON_IMAGE=python:3.12-slim-bookworm +ARG PYTHON_IMAGE=ghcr.io/astral-sh/uv:python3.13-bookworm-slim -# BUILD STAGE - Download dependencies from GitHub that require SSH access -FROM $PYTHON_IMAGE as build +ARG APP_DIR +# BUILD STAGE +FROM $PYTHON_IMAGE as build -# Pinning a specific nightly version so that builds don't suddenly break if a -# "this feature is now stabilized" warning is promoted to an error or something. -# We would like to keep up with nightly if we can. -ARG RUST_VERSION=nightly-2024-02-22 -ENV RUST_VERSION=${RUST_VERSION} +ARG APP_DIR +# Install all system packages needed by either worker OR codecov-api RUN apt-get update RUN apt-get install -y \ build-essential \ @@ -18,37 +16,64 @@ RUN apt-get install -y \ libpq-dev \ libxml2-dev \ libxslt-dev \ + git \ curl -# Install Rust +# Install the Rust toolchain which we need to build test-results-parser +ARG RUST_VERSION=stable +ENV RUST_VERSION=${RUST_VERSION} RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | bash -s -- -y --default-toolchain $RUST_VERSION + | bash -s -- -y --profile minimal --default-toolchain $RUST_VERSION ENV PATH="/root/.cargo/bin:$PATH" -COPY requirements.txt / -WORKDIR /pip-packages/ -RUN pip wheel -r /requirements.txt -RUN rm -rf /pip-packages/src +ENV UV_LINK_MODE=copy \ + UV_COMPILE_BYTECODE=1 \ + UV_PYTHON_DOWNLOADS=never \ + UV_PYTHON=python \ + UV_PROJECT_ENVIRONMENT=/app + +# Export a requirements.txt +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=${APP_DIR}/pyproject.toml,target=${APP_DIR}/pyproject.toml \ + --mount=type=bind,source=${APP_DIR}/uv.lock,target=${APP_DIR}/uv.lock \ + --mount=type=bind,source=libs/shared,target=libs/shared \ + uv --directory ${APP_DIR} export --no-hashes --frozen --format requirements-txt > requirements.txt -ENV PYCURL_SSL_LIBRARY=openssl +# The resulting requirements.txt includes ourselves as a dependency we should install +# "editably". Filter that out, leaving only external dependencies. +RUN grep -v '^-e ' requirements.txt > ${APP_DIR}/requirements.remote.txt -# RUNTIME STAGE - Copy packages from build stage and install runtime dependencies +# Fetch/build wheels for all external dependencies. +RUN --mount=type=bind,source=libs/shared,target=libs/shared \ + (cd ${APP_DIR} && pip wheel -w /wheels --find-links /wheels -r requirements.remote.txt) + +# Build a wheel for ourselves +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=${APP_DIR}/pyproject.toml,target=${APP_DIR}/pyproject.toml \ + --mount=type=bind,source=${APP_DIR}/uv.lock,target=${APP_DIR}/uv.lock \ + --mount=type=bind,source=libs/shared,target=libs/shared \ + uv build --directory ${APP_DIR} --all-packages --wheel -o /wheels + +# RUNTIME STAGE FROM $PYTHON_IMAGE -# Our postgres driver psycopg2 requires libpq-dev as a runtime dependency +ARG APP_DIR + RUN apt-get update RUN apt-get install -y \ - libpq-dev \ libxml2-dev \ libxslt-dev \ - make \ - curl \ - && pip install --upgrade pip + libpq-dev \ + libexpat1 \ + make -WORKDIR /pip-packages/ -COPY --from=build /pip-packages/ /pip-packages/ +COPY --from=build /wheels/ /wheels/ -RUN pip install --no-deps --no-index --find-links=/pip-packages/ /pip-packages/* +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=${APP_DIR}/pyproject.toml,target=${APP_DIR}/pyproject.toml \ + --mount=type=bind,source=${APP_DIR}/uv.lock,target=${APP_DIR}/uv.lock \ + --mount=type=bind,source=libs/shared,target=libs/shared \ + uv --directory ${APP_DIR} pip install --no-deps --no-index --find-links=/wheels /wheels/* --system RUN addgroup --system application \ - && adduser --system codecov --ingroup application --home /home/codecov \ No newline at end of file + && adduser --system codecov --ingroup application --home /home/codecov diff --git a/libs/shared b/libs/shared deleted file mode 160000 index d591317cb5..0000000000 --- a/libs/shared +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d591317cb558fa499e81e52d9eacf61a2ff52744 diff --git a/libs/shared/.coveragerc b/libs/shared/.coveragerc new file mode 100644 index 0000000000..37a0bfdf95 --- /dev/null +++ b/libs/shared/.coveragerc @@ -0,0 +1,12 @@ +[run] +branch = True + +[report] +exclude_lines = + pragma: no cover + raise NotImplementedError + if __name__ == .__main__.: +ignore_errors = True +omit = + tests/* + setup.py diff --git a/libs/shared/.dockerignore b/libs/shared/.dockerignore new file mode 100644 index 0000000000..21d0b898ff --- /dev/null +++ b/libs/shared/.dockerignore @@ -0,0 +1 @@ +.venv/ diff --git a/libs/shared/.envrc b/libs/shared/.envrc new file mode 100644 index 0000000000..7fab97e472 --- /dev/null +++ b/libs/shared/.envrc @@ -0,0 +1,2 @@ +uv sync +source .venv/bin/activate diff --git a/libs/shared/.github/CODEOWNERS b/libs/shared/.github/CODEOWNERS new file mode 100644 index 0000000000..872347a7f8 --- /dev/null +++ b/libs/shared/.github/CODEOWNERS @@ -0,0 +1 @@ +**/migrations/ @codecov/database-migration-reviewers \ No newline at end of file diff --git a/libs/shared/.github/ISSUE_TEMPLATE/bug_report.md b/libs/shared/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..07f3be3e12 --- /dev/null +++ b/libs/shared/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,16 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + + + +# 📣 Feedback / 🐛 Bugs + +Do you want to file a bug report and/or feature request for Codecov? [Please use our feedback repo instead](https://github.com/codecov/feedback/issues). diff --git a/libs/shared/.github/ISSUE_TEMPLATE/feature_request.md b/libs/shared/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..03313301fe --- /dev/null +++ b/libs/shared/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,16 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + + + +# 📣 Feedback / 🐛 Bugs + +Do you want to file a bug report and/or feature request for Codecov? [Please use our feedback repo instead](https://github.com/codecov/feedback/issues). diff --git a/libs/shared/.github/pull_request_template.md b/libs/shared/.github/pull_request_template.md new file mode 100644 index 0000000000..9fcafcedc1 --- /dev/null +++ b/libs/shared/.github/pull_request_template.md @@ -0,0 +1,13 @@ + + + + + + +### Legal Boilerplate + +Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. In 2022 this entity acquired Codecov and as result Sentry is going to need some rights from me in order to utilize my contributions in this PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms. \ No newline at end of file diff --git a/libs/shared/.github/workflows/ci.yml b/libs/shared/.github/workflows/ci.yml new file mode 100644 index 0000000000..d38a7d34f6 --- /dev/null +++ b/libs/shared/.github/workflows/ci.yml @@ -0,0 +1,88 @@ +name: Shared CI + +on: + push: + branches: + - main + pull_request: + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + lint: + name: Run Lint + uses: codecov/gha-workflows/.github/workflows/lint.yml@v1.2.19 + + codecovstartup: + name: Codecov Startup + uses: codecov/gha-workflows/.github/workflows/codecov-startup.yml@v1.2.19 + secrets: inherit + + benchmark: + name: Benchmarks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: astral-sh/setup-uv@v5 + - uses: actions/setup-python@v5 + - run: uv sync --all-extras --dev + + - uses: CodSpeedHQ/action@v3 + with: + run: uv run pytest tests/ --codspeed + token: ${{ secrets.CODSPEED_TOKEN }} + + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + - name: Install docker compose + run: | + sudo curl -SL https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + - name: Build + run: | + make test_env.build + - name: Bring containers up + run: | + make test_env.up + - name: Run tests + run: | + make test_env.test + + - name: Install codecovcli + run: | + make test_env.install_cli + + # Don't upload on forks for now. + - name: upload using codecovcli + if: ${{ !cancelled() && !github.event.pull_request.head.repo.fork && github.repository_owner == 'codecov' }} + run: | + codecovcli -v upload-process --flag shared-docker-uploader --file tests/unit.coverage.xml --token ${{ secrets.CODECOV_ORG_TOKEN }} --fail-on-error + codecovcli -v do-upload --report-type "test_results" --flag shared-docker-uploader --file tests/unit.junit.xml --token ${{ secrets.CODECOV_ORG_TOKEN }} --fail-on-error + + - name: upload using codecovcli staging + if: ${{ !cancelled() && !github.event.pull_request.head.repo.fork && github.repository_owner == 'codecov' }} + run: | + codecovcli -v -u ${{ secrets.CODECOV_STAGING_URL }} upload-process --flag shared-docker-uploader --file tests/unit.coverage.xml --token ${{ secrets.CODECOV_ORG_TOKEN_STAGING }} --fail-on-error + codecovcli -v -u ${{ secrets.CODECOV_STAGING_URL }} do-upload --report-type "test_results" --flag shared-docker-uploader --file tests/unit.junit.xml --token ${{ secrets.CODECOV_ORG_TOKEN_STAGING }} || true + + - name: upload using codecovcli qa + if: ${{ !cancelled() && !github.event.pull_request.head.repo.fork && github.repository_owner == 'codecov' }} + run: | + codecovcli -v -u ${{ secrets.CODECOV_QA_URL }} upload-process --flag shared-docker-uploader --file tests/unit.coverage.xml --token ${{ secrets.CODECOV_QA_TOKEN }} --fail-on-error + codecovcli -v -u ${{ secrets.CODECOV_QA_URL }} do-upload --report-type "test_results" --flag shared-docker-uploader --file tests/unit.junit.xml --token ${{ secrets.CODECOV_QA_TOKEN }} --fail-on-error + + - name: upload using codecovcli public qa + if: ${{ !cancelled() && !github.event.pull_request.head.repo.fork && github.repository_owner == 'codecov' }} + run: | + codecovcli -v -u ${{ secrets.CODECOV_PUBLIC_QA_URL }} upload-process --flag shared-docker-uploader --file tests/unit.coverage.xml --token ${{ secrets.CODECOV_PUBLIC_QA_TOKEN }} --fail-on-error + codecovcli -v -u ${{ secrets.CODECOV_PUBLIC_QA_URL }} do-upload --report-type "test_results" --flag shared-docker-uploader --file tests/unit.junit.xml --token ${{ secrets.CODECOV_PUBLIC_QA_TOKEN }} || true diff --git a/libs/shared/.github/workflows/enforce-license-compliance.yml b/libs/shared/.github/workflows/enforce-license-compliance.yml new file mode 100644 index 0000000000..86be74100e --- /dev/null +++ b/libs/shared/.github/workflows/enforce-license-compliance.yml @@ -0,0 +1,14 @@ +name: Enforce License Compliance + +on: + pull_request: + branches: [main, master] + +jobs: + enforce-license-compliance: + runs-on: ubuntu-latest + steps: + - name: 'Enforce License Compliance' + uses: getsentry/action-enforce-license-compliance@57ba820387a1a9315a46115ee276b2968da51f3d # main + with: + fossa_api_key: ${{ secrets.FOSSA_API_KEY }} diff --git a/libs/shared/.gitignore b/libs/shared/.gitignore new file mode 100644 index 0000000000..5d3c251a13 --- /dev/null +++ b/libs/shared/.gitignore @@ -0,0 +1,150 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ +.codspeed +*.junit.xml +*.coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +*.sqlite + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Editor junk +.*.sw? +.idea/ + +.ruff_cache/ + +/target + +# Make working with Sapling a little easier +.git +.sl diff --git a/libs/shared/.pre-commit-config.yaml b/libs/shared/.pre-commit-config.yaml new file mode 100644 index 0000000000..3dfd578331 --- /dev/null +++ b/libs/shared/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: +- repo: local + hooks: + - id: lint + name: lint + description: "Lint and sort" + entry: make lint + pass_filenames: false + require_serial: true + language: system diff --git a/libs/shared/.python-version b/libs/shared/.python-version new file mode 100644 index 0000000000..24ee5b1be9 --- /dev/null +++ b/libs/shared/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/libs/shared/.vscode/README.md b/libs/shared/.vscode/README.md new file mode 100644 index 0000000000..2c266ab448 --- /dev/null +++ b/libs/shared/.vscode/README.md @@ -0,0 +1,11 @@ +As everyone will have their personal vscode preferences, +and might want to override settings inside of the workspace, +here is how you can make changes locally, without git prompting you to commit +your changes to these settings files: + +``` +git update-index --assume-unchanged .vscode/settings.json +``` + +This way, git will ignore your local changes, and you are free to change these +files as you see fit. diff --git a/libs/shared/.vscode/extensions.json b/libs/shared/.vscode/extensions.json new file mode 100644 index 0000000000..1fc832ae26 --- /dev/null +++ b/libs/shared/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "ms-python.python", + "ms-python.mypy-type-checker", + "charliermarsh.ruff" + ] +} diff --git a/libs/shared/.vscode/settings.json b/libs/shared/.vscode/settings.json new file mode 100644 index 0000000000..21a2e64474 --- /dev/null +++ b/libs/shared/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "[python]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + } + } +} diff --git a/libs/shared/CHANGELOG.md b/libs/shared/CHANGELOG.md new file mode 100644 index 0000000000..87d0b75b02 --- /dev/null +++ b/libs/shared/CHANGELOG.md @@ -0,0 +1,140 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [Unreleased] + +### Added + +### Changed +- [internal] Upgraded RIBS + +### Deprecated + +### Removed + +### Fixed + +### Security + +## [v0.5.10] + +### Changed +- Limited the situations where the ruby-related line deleting happened + +## [v0.5.9] + +### Changed +- [internal] Enabled Lazy loading of rust reports + +## [v0.5.8] + +### Changed +- [internal] Upgraded RIBS + +### Added +- Ability to load PEMS from base64 encoded envvars + +### Fixed +- Allowed empty layout values +- Fixed corner case in Node where some partials could have been counted as hits + +## [v0.5.7] + +### Changed +- [internal] Upgraded RIBS + +## [v0.5.6] + +### Changed +- [internal] Changed logic for single-session totals calculation + +## [v0.5.5] + +### Changed +- Upgraded RIBS for performance reasons + +## [v0.5.4] + +### Fixed +- Fixed pickling of TorngitClientError +- Fixed complexity calculation on filtered reports +- Fixed bug on YAML being not a dict + +## [v0.5.3] + +### Changed +- [INTERNAL] Changed location of celery config in order to have consistent celery rules + +### Fixed +- [CE-3240] Fixed Bitbucket API calls on what pertains to teams + +## [v0.5.2] + +### Fixed +- Corrected some exceptions on YAML errors +- Made ignore_lines clear less caches +- Made `threshold` nullable on some fields + +## [v0.5.1] + +### Added + +### Changed +- Replaced library for validating YAMLs + +### Deprecated + +### Removed +- [INTERNAL] Removed pycurl + +### Fixed +- Fixed small cache discrepancy for when ignore-lines was used + +### Security + +## [v0.5.0] + +### Changed +- Now `totals.coverage` can be None if the totals have no lines +- `totals.files` will only count files that have at least some coverage in them under that given analysis + +### Fixed +- `emails` are properly collected using `get_authenticated_user` +- Issues on `analytics` call will no longer crash the rest of the code if they crash + +## [v0.4.15] + +### Added + +### Changed +- Changed prefix-matching behavior on flags filtering to exact-matching. Old behavior can be achiaved on setting `get_config("compatibility", "flag_pattern_matching")` to True + +### Deprecated + +### Removed + +### Fixed +- [CE-3152] Fixed bug where a loss of integrity on some commits could cause descendant commits to not carryforward properly from it + +### Security +- Loosened dependencies so package upgrades can happen more easily + +## [v0.4.12] + +### Added +- Implemented Bitbucket Oauth1 functions + +## [v0.4.10] + +### Added +- Added `flag_management` field and subfields to user YAML + + +[unreleased]: https://github.com/codecov/shared/compare/v0.4.13...HEAD +[v0.4.13]: https://github.com/codecov/shared/compare/v0.4.12...v0.4.13 +[v0.4.12]: https://github.com/codecov/shared/compare/v0.4.11...v0.4.12 +[v0.4.11]: https://github.com/codecov/shared/compare/v0.4.10...v0.4.11 +[v0.4.10]: https://github.com/codecov/shared/compare/v0.4.9...v0.4.10 diff --git a/libs/shared/LICENSE.md b/libs/shared/LICENSE.md new file mode 100644 index 0000000000..8be662980b --- /dev/null +++ b/libs/shared/LICENSE.md @@ -0,0 +1,105 @@ +# Functional Source License, Version 1.1, Apache 2.0 Future License + +## Abbreviation + +FSL-1.1-Apache-2.0 + +## Notice + +Copyright 2015-2024 Functional Software, Inc. dba Sentry + +## Terms and Conditions + +### Licensor ("We") + +The party offering the Software under these Terms and Conditions. + +### The Software + +The "Software" is each version of the software that we make available under +these Terms and Conditions, as indicated by our inclusion of these Terms and +Conditions with the Software. + +### License Grant + +Subject to your compliance with this License Grant and the Patents, +Redistribution and Trademark clauses below, we hereby grant you the right to +use, copy, modify, create derivative works, publicly perform, publicly display +and redistribute the Software for any Permitted Purpose identified below. + +### Permitted Purpose + +A Permitted Purpose is any purpose other than a Competing Use. A Competing Use +means making the Software available to others in a commercial product or +service that: + +1. substitutes for the Software; + +2. substitutes for any other product or service we offer using the Software + that exists as of the date we make the Software available; or + +3. offers the same or substantially similar functionality as the Software. + +Permitted Purposes specifically include using the Software: + +1. for your internal use and access; + +2. for non-commercial education; + +3. for non-commercial research; and + +4. in connection with professional services that you provide to a licensee + using the Software in accordance with these Terms and Conditions. + +### Patents + +To the extent your use for a Permitted Purpose would necessarily infringe our +patents, the license grant above includes a license under our patents. If you +make a claim against any party that the Software infringes or contributes to +the infringement of any patent, then your patent license to the Software ends +immediately. + +### Redistribution + +The Terms and Conditions apply to all copies, modifications and derivatives of +the Software. + +If you redistribute any copies, modifications or derivatives of the Software, +you must include a copy of or a link to these Terms and Conditions and not +remove any copyright notices provided in or with the Software. + +### Disclaimer + +THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR +PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT. + +IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE +SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, +EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE. + +### Trademarks + +Except for displaying the License Details and identifying us as the origin of +the Software, you have no right under these Terms and Conditions to use our +trademarks, trade names, service marks or product names. + +## Grant of Future License + +We hereby irrevocably grant you an additional license to use the Software under +the Apache License, Version 2.0 that is effective on the second anniversary of +the date we make the Software available. On or after that date, you may use the +Software under the Apache License, Version 2.0, in which case the following +will apply: + +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. diff --git a/libs/shared/Makefile b/libs/shared/Makefile new file mode 100644 index 0000000000..1acfa6740d --- /dev/null +++ b/libs/shared/Makefile @@ -0,0 +1,57 @@ +CODECOV_UPLOAD_TOKEN ?= "notset" +CODECOV_URL ?= "https://api.codecov.io" +CODECOV_FLAG ?= "" +full_sha := $(shell git rev-parse HEAD) + +# We allow this to be overridden so that we can run `pytest` from this directory +# but have the junit file use paths relative to a parent directory. This will +# help us move to a monorepo. +PYTEST_ROOTDIR ?= "." + +export CODECOV_TOKEN=${CODECOV_UPLOAD_TOKEN} +.ONESHELL: + +test: + # Emit coverage/junit files inside the bind-mounted `test` directory + docker compose exec shared uv run pytest --cov-report=xml:tests/unit.coverage.xml --junitxml=tests/unit.junit.xml -o junit_family=legacy -c pytest.ini --rootdir=${PYTEST_ROOTDIR} + +test.path: + docker compose exec shared uv run pytest $(TEST_PATH) + +lint: + make lint.install + make lint.run + +lint.install: + echo "Installing..." + pip install -Iv ruff + +lint.run: + ruff check + ruff format + +lint.check: + echo "Linting..." + ruff check --fix + echo "Formatting..." + ruff format --check + +requirements.install: + uv sync + . .venv/bin/activate + +test_env.install_cli: + pip install codecov-cli + +test_env.build: + docker compose build + +test_env.up: + docker compose up -d + +test_env.test: + # Emit coverage/junit files inside the bind-mounted `test` directory + docker compose exec shared uv run pytest -c pytest.ini --rootdir=${PYTEST_ROOTDIR} --cov ./shared --cov-report=xml:tests/unit.coverage.xml --junitxml=tests/unit.junit.xml -o junit_family=legacy + +test_env.down: + docker compose down diff --git a/libs/shared/README.md b/libs/shared/README.md new file mode 100644 index 0000000000..bfb74141fe --- /dev/null +++ b/libs/shared/README.md @@ -0,0 +1,86 @@ +# shared +[![Shared CI](https://github.com/codecov/shared/actions/workflows/ci.yml/badge.svg)](https://github.com/codecov/shared/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/codecov/shared/graph/badge.svg?token=IL64imgbOu)](https://codecov.io/gh/codecov/shared) + +Shared is a place for code that is common to multiple python repositories on `codecov`. + +> We believe that everyone should have access to quality software (like Sentry), that’s why we have always offered Codecov for free to open source maintainers. +> +> By making our code public, we’re not only joining the community that’s supported us from the start — but also want to make sure that every developer can contribute to and build on the Codecov experience. + +## How does shared get into production + +`shared` is a repository of its own, so it needs to be installed as a dependency on the repositories that might use it. + +The current repositories using `shared` are `codecov/worker` and `codecov/codecov-api`. + +Whenever getting new code into `shared`, one needs to wait for a new version to be released (or release it themselves, see below), and update the `requirements.in` file in `codecov/worker` and `codecov/codecov-api` to use the newly released version of `shared`. + +## Getting started + +To get started, ensure that you have: + +1. Docker installed on your machine +2. Run +``` +docker compose up +``` + +## Releasing a new version on shared + +To release a new version, you need to: + +1) Check what the next version should be. + - You can check the latest version on https://github.com/codecov/shared/releases + - As a rule of thumb, just add one to the micro version (number most to the right) +2) Create a new PR: +- Changing the `version` field on https://github.com/codecov/shared/blob/main/setup.py#L12 to that new version +- Change https://github.com/codecov/shared/blob/main/CHANGELOG.md unreleased header name to that version, and create a new _unreleased_ section with the same subsections. +3) Merge that PR +4) Create a new release on https://github.com/codecov/shared/releases/new + +## Running tests + +In order to run tests from within your docker container, run: + +``` +make test +``` + +To run a specific test file, run for example: +``` +make test-path TEST_PATH=tests/unit/bundle_analysis/test_bundle_analysis.py +``` + +## Running migrations + +If you make changes to the models in `shared/django_apps/` you will need to create migrations to reflect those changes in the database. + +Make sure the shared container is running and shell into it +```bash +$ docker compose up +$ docker compose exec -it shared /bin/bash +``` + +Now you can create a migration (from within the container) + +```bash +$ cd shared/django_apps/ +$ python manage.py makemigrations +``` + +To learn more about migrations visit [Django Docs](https://docs.djangoproject.com/en/5.0/topics/migrations/) + +## Managing shared dependencies + +As a normal python package, `shared` can include dependencies of its own. + +Updating them should be done at the `setup.py` file. + +Remember to add dependencies as loosely as possible. Only make sure to include what the minimum version is, and only include a maximum version if you do know that higher versions will break. + +Remember that multiple packages, on different contexts of their own requirements, will have to install this. So keeping the requirements loose allow them to avoid version clashes and eases upgrades whenever they need to. + +## Contributing + +This repository, like all of Codecov's repositories, strives to follow our general [Contributing guidlines](https://github.com/codecov/contributing). If you're considering making a contribution to this repository, we encourage review of our Contributing guidelines first. diff --git a/libs/shared/codecov.yml b/libs/shared/codecov.yml new file mode 100644 index 0000000000..ae63c2d0ff --- /dev/null +++ b/libs/shared/codecov.yml @@ -0,0 +1,12 @@ +codecov: + require_ci_to_pass: false + notify: + wait_for_ci: false +ignore: + - conftest.py + - "**/conftest.py" + - "**tests**/test_*.py" + - "tests**" + +test_analytics: + flake_detection: true diff --git a/libs/shared/docker-compose.yml b/libs/shared/docker-compose.yml new file mode 100644 index 0000000000..a82eef679e --- /dev/null +++ b/libs/shared/docker-compose.yml @@ -0,0 +1,62 @@ +volumes: + postgres-volume: + timescale-volume: + redis-volume: + archive-volume: + +services: + shared: + build: + context: . + dockerfile: docker/Dockerfile + tty: true + depends_on: + - minio + - postgres + - redis + - timescale + volumes: + - ./shared/:/app/libs/shared/shared + - ./tests/:/app/libs/shared/tests + - ./.coveragerc:/app/libs/shared/.coveragerc + + postgres: + image: postgres:14-alpine + environment: + - POSTGRES_DB=postgres + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + - POSTGRES_HOST_AUTH_METHOD=trust + volumes: + - type: tmpfs + target: /var/lib/postgresql/data + tmpfs: + size: 1024M + + redis: + image: redis:6-alpine + volumes: + - redis-volume:/data + + timescale: + image: timescale/timescaledb:latest-pg14 + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_HOST_AUTH_METHOD=trust + volumes: + - type: tmpfs + target: /var/lib/postgresql/data + tmpfs: + size: 1024M + + minio: + image: minio/minio:latest + command: server /export + ports: + - "${MINIO_PORT:-9000}:9000" + environment: + - MINIO_ACCESS_KEY=codecov-default-key + - MINIO_SECRET_KEY=codecov-default-secret + volumes: + - archive-volume:/export diff --git a/libs/shared/docker/Dockerfile b/libs/shared/docker/Dockerfile new file mode 100644 index 0000000000..c7537c27f0 --- /dev/null +++ b/libs/shared/docker/Dockerfile @@ -0,0 +1,32 @@ +# syntax=docker/dockerfile:1.4 +ARG PYTHON_IMAGE=ghcr.io/astral-sh/uv:python3.12-bookworm-slim + +FROM $PYTHON_IMAGE as build + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + git + +ENV UV_LINK_MODE=copy +ENV PATH=/root/.local/bin:/app/libs/shared/.venv/bin:$PATH + +RUN --mount=type=cache,target=/root/.cache/uv \ + uv tool install codecov-cli + +# Change the working directory to the `app/libs/shared` directory +WORKDIR /app/libs/shared + +# Install dependencies +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --frozen --no-install-project + +# Copy the project into the image +ADD . /app/libs/shared + +# Sync the project +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen + +CMD ["uv", "run", "bash"] diff --git a/libs/shared/pyproject.toml b/libs/shared/pyproject.toml new file mode 100644 index 0000000000..32fbb554fd --- /dev/null +++ b/libs/shared/pyproject.toml @@ -0,0 +1,69 @@ +[project] +name = "shared" +version = "0.1.0" +description = "Shared code used in codecov API and worker" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "boto3>=1.20.25", + "cachetools>=4.1.1", + "cerberus>=1.3.5", + "codecov-ribs>=0.1.18", + "colour>=0.1.5", + "cryptography>=43.0.1", + "django-better-admin-arrayfield>=1.4.2", + "django-model-utils>=4.5.1", + "django-postgres-extra>=2.0.8", + "django-prometheus>=2.3.1", + "django<5", + "google-auth>=2.21.0", + "google-cloud-pubsub>=2.18.4", + "google-cloud-storage>=2.18.2", + "httpx>=0.23.0", + "ijson>=3.2.3", + "minio>=7.1.13", + "mmh3>=4.0.1", + "oauthlib>=3.1.0", + "orjson>=3.10.9", + "prometheus-client>=0.17.1", + "pyjwt>=2.8.0", + "pyparsing>=2.4.7", + "python-redis-lock>=4.0.0", + "pyyaml>=6.0.1", + "redis>=4.4.4", + "requests>=2.32.3", + "sentry-sdk>=2.13.0", + "sqlalchemy<2", + "zstandard>=0.23.0", + "pydantic>=2.10.4", + "amplitude-analytics>=1.1.4", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.uv] +required-version = ">=0.6.0" +dev-dependencies = [ + "factory-boy>=3.2.0", + "freezegun>=1.1.0", + "mock>=4.0.3", + "mypy>=1.13.0", + "pre-commit>=2.11.1", + "psycopg2-binary>=2.9.2", + "pytest-asyncio>=0.14.0", + "pytest-codspeed>=3.2.0", + "pytest-cov>=5.0.0", + "pytest-django>=4.7.0", + "pytest-mock>=1.13.0", + "pytest>=8.1.1", + "respx>=0.20.2", + "ruff>=0.9.0", + "types-mock>=5.1.0.20240425", + # NOTE: some weird interaction between existing `vcrpy` snapshots and the way + # `oauth2` / `minio` deal with requests forces us to downgrade `urllib3`: + "urllib3==1.26.19", + "vcrpy>=4.1.1", + "types-requests>=2.31.0.6", +] diff --git a/libs/shared/pytest.ini b/libs/shared/pytest.ini new file mode 100644 index 0000000000..afc7caa361 --- /dev/null +++ b/libs/shared/pytest.ini @@ -0,0 +1,7 @@ +[pytest] +DJANGO_SETTINGS_MODULE = shared.django_apps.dummy_settings +python_files=tests/*/*.py +mock_use_standalone_module = true +markers = + unit: mark a test as a unit test + integration: mark a test as an integration test diff --git a/libs/shared/ruff.toml b/libs/shared/ruff.toml new file mode 100644 index 0000000000..4280aae727 --- /dev/null +++ b/libs/shared/ruff.toml @@ -0,0 +1,64 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.12 +target-version = "py312" + +[lint] +# Currently only enabled for F (Pyflakes), I (isort), E,W (pycodestyle:Error/Warning), PLC/PLE/PLW (Pylint:Convention/Error/Warning), +# PERF (Perflint), and T20 (Flake8-print) rules: https://docs.astral.sh/ruff/rules/ +select = ["F", "I", "E", "W", "PLC", "PLE", "PLW", "PERF", "T20"] +ignore = ["F403", "F405", "E501", "E712"] + +# Allow fix for all enabled rules (when `--fix`) is provided. +# The preferred method (for now) w.r.t. fixable rules is to manually update the makefile +# with --fix and re-run 'make lint' +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" \ No newline at end of file diff --git a/libs/shared/shared/__init__.py b/libs/shared/shared/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/analytics_tracking/__init__.py b/libs/shared/shared/analytics_tracking/__init__.py new file mode 100644 index 0000000000..bcb98c787d --- /dev/null +++ b/libs/shared/shared/analytics_tracking/__init__.py @@ -0,0 +1,32 @@ +import logging +from typing import List + +from shared.analytics_tracking.base import BaseAnalyticsTool +from shared.analytics_tracking.manager import AnalyticsToolManager +from shared.analytics_tracking.marketo import Marketo +from shared.analytics_tracking.noop import NoopTool +from shared.analytics_tracking.pubsub import PubSub + +log = logging.getLogger("__name__") + +__all__ = ["analytics_manager"] + + +def get_list_of_analytic_tools() -> List[BaseAnalyticsTool]: + return [PubSub(), Marketo()] + + +def get_tools_manager(): + tool_manager = AnalyticsToolManager() + available_tools = get_list_of_analytic_tools() + for tool in available_tools: + tool_manager.add_tool(tool) + + # Noop shouldn't be added unless there are no tracking tools used + if not available_tools: + log.warning("Analytics tool is not enabled. Please check your configuration.") + tool_manager.add_tool(NoopTool) + return tool_manager + + +analytics_manager = get_tools_manager() diff --git a/libs/shared/shared/analytics_tracking/base.py b/libs/shared/shared/analytics_tracking/base.py new file mode 100644 index 0000000000..d2bdcd0459 --- /dev/null +++ b/libs/shared/shared/analytics_tracking/base.py @@ -0,0 +1,15 @@ +from abc import ABC, abstractmethod + +from shared.analytics_tracking.events import Event + + +class BaseAnalyticsTool(ABC): + BLANK_USER_ID = -1 + + @classmethod + @abstractmethod + def is_enabled(cls): + raise NotImplementedError() + + def track_event(self, event: Event, *, is_enterprise, context: None): + raise NotImplementedError() diff --git a/libs/shared/shared/analytics_tracking/events.py b/libs/shared/shared/analytics_tracking/events.py new file mode 100644 index 0000000000..4e5158680b --- /dev/null +++ b/libs/shared/shared/analytics_tracking/events.py @@ -0,0 +1,40 @@ +import logging +from base64 import b64encode +from datetime import datetime, timezone +from enum import Enum +from typing import Any, Mapping +from uuid import uuid1 + +log = logging.getLogger("__name__") + + +class Events(Enum): + ACCOUNT_ACTIVATED_REPOSITORY_ON_UPLOAD = ( + "codecov.account.activated_repository_on_upload" + ) + ACCOUNT_ACTIVATED_REPOSITORY = "codecov.account.activated_repository" + ACCOUNT_UPLOADED_COVERAGE_REPORT = "codecov.account.uploaded_coverage_report" + USER_SIGNED_IN = "codecov.user.signed_in" + USER_SIGNED_UP = "codecov.user.signed_up" + GDPR_OPT_IN = "codecov.email.gdpr_opt_in" + + +class Event: + def __init__(self, event_name: str, dt: datetime | None = None, **data) -> None: + self.uuid = uuid1() + self.datetime = dt or datetime.now(timezone.utc) + self.name = self._get_event_name(event_name) + self.data = data + + def _get_event_name(self, event_name: str): + if event_name not in list(event.value for event in Events): + raise ValueError("Invalid event name: " + event_name) + return event_name + + def serialize(self) -> Mapping[str, Any]: + return { + "uuid": b64encode(self.uuid.bytes).decode(), + "timestamp": self.datetime.timestamp(), + "type": self.name, + "data": self.data, + } diff --git a/libs/shared/shared/analytics_tracking/manager.py b/libs/shared/shared/analytics_tracking/manager.py new file mode 100644 index 0000000000..f98dffab2c --- /dev/null +++ b/libs/shared/shared/analytics_tracking/manager.py @@ -0,0 +1,41 @@ +import logging +from typing import Optional + +from shared.analytics_tracking.base import BaseAnalyticsTool +from shared.analytics_tracking.events import Event + +log = logging.getLogger("__name__") + + +class AnalyticsToolManager: + def __init__(self): + self.tools = [] + + def add_tool(self, tracking_tool: BaseAnalyticsTool): + self.tools.append(tracking_tool) + + def remove_tool(self, tracking_tool: BaseAnalyticsTool): + self.tools.remove(tracking_tool) + + def track_event( + self, + event_name, + *, + is_enterprise=False, + event_data: Optional[dict] = None, + context=None, + ): + if event_data is None: + event_data = {} + + event = Event(event_name, **event_data) + for tool in self.tools: + if tool.is_enabled(): + try: + tool.track_event( + event, is_enterprise=is_enterprise, context=context + ) + except Exception as exc: + log.error( + "Got an error sending events", extra=dict(tool=tool, error=exc) + ) diff --git a/libs/shared/shared/analytics_tracking/marketo.py b/libs/shared/shared/analytics_tracking/marketo.py new file mode 100644 index 0000000000..66625ff453 --- /dev/null +++ b/libs/shared/shared/analytics_tracking/marketo.py @@ -0,0 +1,79 @@ +import httpx + +from shared.analytics_tracking.base import BaseAnalyticsTool +from shared.analytics_tracking.events import Event, Events +from shared.config import get_config + +marketo_events = [ + Events.USER_SIGNED_IN.value, + Events.USER_SIGNED_UP.value, + Events.ACCOUNT_UPLOADED_COVERAGE_REPORT.value, + Events.GDPR_OPT_IN.value, +] + + +class MarketoError(Exception): + def __init__(self, error): + self.code = error["code"] + self.message = error["message"] + + def __str__(self): + return f"MarketoError: {self.code} - {self.message}" + + +class Marketo(BaseAnalyticsTool): + OAUTH_URL = "/identity/oauth/token" + LEAD_URL = "/rest/v1/leads.json" + + def __init__(self) -> None: + self.client_id = get_config("setup", "marketo", "client_id", default=None) + self.client_secret = get_config( + "setup", "marketo", "client_secret", default=None + ) + self.base_url = get_config("setup", "marketo", "base_url", default=None) + self.client = httpx.Client() + + @property + def token(self): + resp = self.retrieve_token() + return resp["access_token"] + + @classmethod + def is_enabled(cls): + return bool(get_config("setup", "marketo", "enabled", default=False)) + + def track_event(self, event: Event, *, is_enterprise, context: None): + if event.name in marketo_events: + body = { + "input": [event.serialize()], + } + return self.make_rest_request(self.LEAD_URL, method="POST", json=body) + + def make_request(self, url, *args, **kwargs): + full_url = self.base_url + url + method = kwargs.pop("method", "GET") + res = self.client.request(method, full_url, **kwargs) + return res.json() + + def make_rest_request(self, url, *args, **kwargs): + headers = kwargs.pop("headers", {}) + headers["Authorization"] = f"Bearer {self.token}" + headers["Content-Type"] = "application/json" + data = self.make_request(url, *args, headers=headers, **kwargs) + + if not data.get("success"): + # just use the first error + error = data["errors"][0] + raise MarketoError(error) + + # we might have success=True but field level erros + results = data.get("result", []) + for result in results: + if result.get("status") == "skipped": + error = result["reasons"][0] + raise MarketoError(error) + return data + + def retrieve_token(self): + url = f"{self.OAUTH_URL}?grant_type=client_credentials&client_id={self.client_id}&client_secret={self.client_secret}" + return self.make_request(url) diff --git a/libs/shared/shared/analytics_tracking/noop.py b/libs/shared/shared/analytics_tracking/noop.py new file mode 100644 index 0000000000..7e4ca50c18 --- /dev/null +++ b/libs/shared/shared/analytics_tracking/noop.py @@ -0,0 +1,15 @@ +import logging + +from shared.analytics_tracking.base import BaseAnalyticsTool +from shared.analytics_tracking.events import Event + +log = logging.getLogger("__name__") + + +class NoopTool(BaseAnalyticsTool): + @classmethod + def is_enabled(cls): + return False + + def track_event(self, event: Event, *, is_enterprise, context: None): + return diff --git a/libs/shared/shared/analytics_tracking/pubsub.py b/libs/shared/shared/analytics_tracking/pubsub.py new file mode 100644 index 0000000000..6925f06f89 --- /dev/null +++ b/libs/shared/shared/analytics_tracking/pubsub.py @@ -0,0 +1,63 @@ +import json +import logging +from datetime import datetime + +from google.auth.exceptions import GoogleAuthError +from google.cloud import pubsub_v1 + +from shared.analytics_tracking.base import BaseAnalyticsTool +from shared.analytics_tracking.events import Event +from shared.config import get_config + +log = logging.getLogger("__name__") + + +class CustomJSONEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, datetime): + return obj.isoformat() + return super().default(obj) + + +class PubSub(BaseAnalyticsTool): + def __init__( + self, + batch_max_bytes: int = 1024 * 1024 * 5, + batch_max_latency: float = 0.05, + batch_max_messages: int = 1000, + ) -> None: + settings = pubsub_v1.types.BatchSettings( + max_bytes=batch_max_bytes, + max_latency=batch_max_latency, + max_messages=batch_max_messages, + ) + self.project = get_config("setup", "pubsub", "project_id") + topic_name = get_config("setup", "pubsub", "topic") + self.publisher = self.get_publisher(settings) + if self.publisher: + self.topic = self.publisher.topic_path(self.project, topic_name) + + @classmethod + def is_enabled(cls): + return bool(get_config("setup", "pubsub", "enabled", default=False)) + + def get_publisher(self, settings): + if not self.is_enabled(): + return None + try: + publisher = pubsub_v1.PublisherClient(settings) + except GoogleAuthError: + log.warning("Unable to initialize PubSub, no auth found") + publisher = None + return publisher + + def track_event(self, event: Event, *, is_enterprise=False, context=None): + if is_enterprise: + return + if self.publisher is not None: + self.publisher.publish( + self.topic, + data=json.dumps(event.serialize(), cls=CustomJSONEncoder).encode( + "utf-8" + ), + ) diff --git a/libs/shared/shared/api_archive/__init__.py b/libs/shared/shared/api_archive/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/api_archive/archive.py b/libs/shared/shared/api_archive/archive.py new file mode 100644 index 0000000000..4c7d0f507c --- /dev/null +++ b/libs/shared/shared/api_archive/archive.py @@ -0,0 +1,174 @@ +import json +import logging +from base64 import b16encode +from enum import Enum +from hashlib import md5 + +import sentry_sdk + +import shared.storage +from shared.config import get_config +from shared.utils.ReportEncoder import ReportEncoder + +log = logging.getLogger(__name__) + + +# TODO deduplicate this logic from worker and shared +class MinioEndpoints(Enum): + chunks = "{version}/repos/{repo_hash}/commits/{commitid}/chunks.txt" + json_data = "{version}/repos/{repo_hash}/commits/{commitid}/json_data/{table}/{field}/{external_id}.json" + json_data_no_commit = ( + "{version}/repos/{repo_hash}/json_data/{table}/{field}/{external_id}.json" + ) + raw = "v4/raw/{date}/{repo_hash}/{commit_sha}/{reportid}.txt" + raw_with_upload_id = ( + "v4/raw/{date}/{repo_hash}/{commit_sha}/{reportid}/{uploadid}.txt" + ) + profiling_upload = ( + "{version}/repos/{repo_hash}/profilinguploads/{profiling_version}/{location}" + ) + static_analysis_single_file = ( + "{version}/repos/{repo_hash}/static_analysis/files/{location}" + ) + test_results = "test_results/v1/raw/{date}/{repo_hash}/{commit_sha}/{uploadid}.txt" + + def get_path(self, **kwaargs): + return self.value.format(**kwaargs) + + +class ArchiveService(object): + """ + Service class for performing archive operations. + Meant to work against the underlying `StorageService`. + """ + + root: str + """ + The root level of the archive. + In s3 terms, this would be the name of the bucket + """ + + storage_hash: str | None + """ + A hash key of the repo for internal storage + """ + + ttl = 10 + """ + Time to life, how long presigned PUTs/GETs should live + """ + + def __init__(self, repository, ttl=None): + self.root = get_config("services", "minio", "bucket", default="archive") + # Set TTL from config and default to existing value + self.ttl = ttl or int(get_config("services", "minio", "ttl", default=self.ttl)) + + # The `api_archive.ArchiveService` is always using `minio` + self.storage = shared.storage.get_appropriate_storage_service( + repository.repoid if repository else None + ) + + self.storage_hash = self.get_archive_hash(repository) if repository else None + + @classmethod + def get_archive_hash(cls, repository) -> str: + """ + Generates a hash key from repo specific information. + Provides slight obfuscation of data in minio storage + """ + _hash = md5() + hash_key = get_config("services", "minio", "hash_key", default="") + val = "".join( + map( + str, + ( + repository.repoid, + repository.service, + repository.service_id, + hash_key, + ), + ) + ).encode() + _hash.update(val) + return b16encode(_hash.digest()).decode() + + def write_json_data_to_storage( + self, + commit_id, + table: str, + field: str, + external_id: str, + data: dict, + *, + encoder=ReportEncoder, + ): + if not self.storage_hash: + raise ValueError("No hash key provided") + if commit_id is None: + # Some classes don't have a commit associated with them + # For example Pull belongs to multiple commits. + path = MinioEndpoints.json_data_no_commit.get_path( + version="v4", + repo_hash=self.storage_hash, + table=table, + field=field, + external_id=external_id, + ) + else: + path = MinioEndpoints.json_data.get_path( + version="v4", + repo_hash=self.storage_hash, + commitid=commit_id, + table=table, + field=field, + external_id=external_id, + ) + stringified_data = json.dumps(data, cls=encoder) + self.write_file(path, stringified_data) + return path + + @sentry_sdk.trace + def write_file( + self, path, data, reduced_redundancy=False, is_already_gzipped=False + ): + """ + Writes a generic file to the archive -- it's typically recommended to + not use this in lieu of the convenience methods write_raw_upload and + write_chunks + """ + self.storage.write_file( + self.root, + path, + data, + reduced_redundancy=reduced_redundancy, + is_already_gzipped=is_already_gzipped, + ) + + @sentry_sdk.trace + def read_file(self, path: str) -> str: + """ + Generic method to read a file from the archive + """ + contents = self.storage.read_file(self.root, path) + return contents.decode() + + @sentry_sdk.trace + def delete_file(self, path: str) -> None: + """ + Generic method to delete a file from the archive. + """ + self.storage.delete_file(self.root, path) + + def read_chunks(self, commit_sha: str) -> str: + """ + Convenience method to read a chunks file from the archive. + """ + if not self.storage_hash: + raise ValueError("No hash key provided") + path = MinioEndpoints.chunks.get_path( + version="v4", repo_hash=self.storage_hash, commitid=commit_sha + ) + return self.read_file(path) + + def create_presigned_put(self, path: str) -> str: + return self.storage.create_presigned_put(self.root, path, self.ttl) diff --git a/libs/shared/shared/bots/__init__.py b/libs/shared/shared/bots/__init__.py new file mode 100644 index 0000000000..8eef7168d5 --- /dev/null +++ b/libs/shared/shared/bots/__init__.py @@ -0,0 +1,70 @@ +import logging +from typing import Any, List + +from shared.bots.github_apps import get_github_app_info_for_owner +from shared.bots.owner_bots import get_owner_appropriate_bot_token +from shared.bots.public_bots import get_token_type_mapping +from shared.bots.repo_bots import get_repo_appropriate_bot_token +from shared.bots.types import AdapterAuthInformation +from shared.django_apps.codecov_auth.models import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + Owner, + Service, +) +from shared.django_apps.core.models import Repository +from shared.typings.torngit import GithubInstallationInfo + +log = logging.getLogger(__name__) + +SQLAlchemyOwner = Any +SQLAlchemyRepository = Any + + +def get_adapter_auth_information( + owner: Owner | SQLAlchemyOwner, + repository: Repository | SQLAlchemyRepository | None = None, + *, + ignore_installations: bool = False, + installation_name_to_use: str = GITHUB_APP_INSTALLATION_DEFAULT_NAME, +) -> AdapterAuthInformation: + """ + Gets all the auth information needed to send requests to the provider. + This logic is used by the worker to get the data needed to create a torngit.BaseAdapter. + :warning: Api should use the `owner.oauth_token` of the user making the request. + """ + installation_info: GithubInstallationInfo | None = None + token_type_mapping = None + fallback_installations: List[GithubInstallationInfo] | None = None + if ( + Service(owner.service) in [Service.GITHUB, Service.GITHUB_ENTERPRISE] + # in sync_teams and sync_repos we might prefer to use the owner's OAuthToken instead of installation + and not ignore_installations + ): + installations_available_info = get_github_app_info_for_owner( + owner, + repository=repository, + installation_name=installation_name_to_use, + ) + if installations_available_info != []: + installation_info, *fallback_installations = installations_available_info + + if repository: + token, token_owner = get_repo_appropriate_bot_token( + repository, installation_info + ) + if installation_info is None: + # the admin_bot_token should be associated with an Owner so we know that it was + # actually configured for this Repository. + # The exception would be GH installation tokens, but in that case we don't use token_type_mapping + token_type_mapping = get_token_type_mapping( + repository, admin_bot_token=(token if token_owner else None) + ) + else: + token, token_owner = get_owner_appropriate_bot_token(owner, installation_info) + return AdapterAuthInformation( + token=token, + token_owner=token_owner, + selected_installation_info=installation_info, + fallback_installations=fallback_installations, + token_type_mapping=token_type_mapping, + ) diff --git a/libs/shared/shared/bots/exceptions.py b/libs/shared/shared/bots/exceptions.py new file mode 100644 index 0000000000..1b2f8fea39 --- /dev/null +++ b/libs/shared/shared/bots/exceptions.py @@ -0,0 +1,19 @@ +class RequestedGithubAppNotFound(Exception): + pass + + +class OwnerWithoutValidBotError(Exception): + pass + + +class NoConfiguredAppsAvailable(Exception): + def __init__( + self, apps_count: int, rate_limited_count: int, suspended_count: int + ) -> None: + self.apps_count = apps_count + self.rate_limited_count = rate_limited_count + self.suspended_count = suspended_count + + +class RepositoryWithoutValidBotError(Exception): + pass diff --git a/libs/shared/shared/bots/github_apps.py b/libs/shared/shared/bots/github_apps.py new file mode 100644 index 0000000000..717fdc689a --- /dev/null +++ b/libs/shared/shared/bots/github_apps.py @@ -0,0 +1,351 @@ +import logging +import random +from datetime import datetime, timezone +from typing import Dict, List + +from shared.bots.exceptions import NoConfiguredAppsAvailable, RequestedGithubAppNotFound +from shared.bots.types import TokenWithOwner +from shared.django_apps.codecov_auth.models import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, + Owner, + Service, +) +from shared.django_apps.core.models import Repository +from shared.github import InvalidInstallationError, get_github_integration_token +from shared.helpers.redis import get_redis_connection +from shared.orms.owner_helper import DjangoSQLAlchemyOwnerWrapper +from shared.rate_limits import determine_if_entity_is_rate_limited, gh_app_key_name +from shared.typings.oauth_token_types import Token +from shared.typings.torngit import GithubInstallationInfo + +log = logging.getLogger(__name__) + + +MAX_GITHUB_APP_SELECTION_WEIGHT = 1200 + + +def _get_installation_weight(installation: GithubAppInstallation) -> int: + """The weight for a given app installation. + Establishes an exponential ramp-up period for installations after being updated. + """ + age = datetime.now(timezone.utc) - installation.created_at + if age.days >= 10: + return MAX_GITHUB_APP_SELECTION_WEIGHT + seconds_in_hour = 3600 + age_hours = (age.seconds // seconds_in_hour) + age.days * 24 + # Prevent clock differences from making the weight negative + return max(1, age_hours + 2**age.days) + + +def _can_use_this_app( + app: GithubAppInstallation, installation_name: str, repository: Repository | None +) -> bool: + return ( + app.name == installation_name + # We ignore apps that are not configured because those can't be used + and app.is_configured() + # If repository is provided, the installation needs to cover it + and ((not repository) or app.is_repo_covered_by_integration(repository)) + ) + + +def _get_apps_from_weighted_selection( + owner: Owner, installation_name: str, repository: Repository | None +) -> List[GithubAppInstallation]: + """This function returns an ordered list of GithubAppInstallations that can be used to communicate with GitHub + in behalf of the owner. The list is ordered in such a way that the 1st element is the app to be used in Torngit, + and the subsequent apps are selected as fallbacks. + + IF the repository is provided, the selected apps also cover the repo. + IF installation_name is not the default one, than the default codecov installation + is also selected as a possible fallback app. + + Apps are selected randomly but assigned weights based on how recently they were created. + This means that older apps are selected more frequently as the main app than newer ones. + (up to 10 days, when the probability of being chosen is the same) + The random selection is done so we can distribute request load more evenly among apps. + """ + # Map GithubAppInstallation.id --> GithubAppInstallation + ghapp_installations_filter: Dict[int, GithubAppInstallation] = { + obj.id: obj + for obj in filter( + lambda obj: _can_use_this_app(obj, installation_name, repository), + DjangoSQLAlchemyOwnerWrapper.get_github_app_installations(owner) or [], + ) + } + # We assign weights to the apps based on how long ago they were created. + # The idea is that there's a greater chance that a change misconfigured the app, + # So apps recently created are selected less frequently than older apps + keys = list(ghapp_installations_filter.keys()) + weights = [ + min( + MAX_GITHUB_APP_SELECTION_WEIGHT, + _get_installation_weight(ghapp_installations_filter[key]), + ) + for key in keys + ] + # We pick apps one by one until all apps have been selected + # Obviously apps with a higher weight have a higher change of being selected as the main app (1st selection) + # But it's important that others are also selected so we can use them as fallbacks + apps_to_consider = [] + apps_to_select = len(keys) + selections = 0 + while selections < apps_to_select: + selected_app_id = random.choices(keys, weights, k=1)[0] + apps_to_consider.append(ghapp_installations_filter[selected_app_id]) + # random.choices chooses with replacement + # which we are trying to avoid here. So we remove the key selected and its weight from the population. + key_idx = keys.index(selected_app_id) + keys.pop(key_idx) + weights.pop(key_idx) + selections += 1 + if installation_name != GITHUB_APP_INSTALLATION_DEFAULT_NAME: + # Add the default app as the last fallback if the owner is using a different app for the task + default_apps = filter( + lambda obj: _can_use_this_app( + obj, GITHUB_APP_INSTALLATION_DEFAULT_NAME, repository + ), + DjangoSQLAlchemyOwnerWrapper.get_github_app_installations(owner), + ) + if default_apps: + apps_to_consider.extend(default_apps) + return apps_to_consider + + +def handle_invalid_installation( + installation_info: GithubInstallationInfo, error: InvalidInstallationError +) -> None: + """Handles the InvalidInstallationError, syncing our info with GitHub's , so that we don't have the same error again in the future. + + possible side effects: + * marking GithubAppInstallation as suspended; + * deleting GithubAppInstallations; + * clearing out Owner.integration_id + """ + if "id" in installation_info: + match error.error_cause: + case "installation_suspended": + # Mark the installation as suspended so we don't keep trying to get the token for it + GithubAppInstallation.objects.filter(id=installation_info["id"]).update( + is_suspended=True + ) + case "installation_not_found": + GithubAppInstallation.objects.filter( + id=installation_info["id"] + ).delete() + else: + # This comes from the legacy Owner.integration_id. Clear it. + # installation_id should be unique among Owners + Owner.objects.filter( + integration_id=installation_info["installation_id"] + ).update(integration_id=None) + + +def get_github_app_token( + service: Service, installation_info: GithubInstallationInfo +) -> TokenWithOwner: + """Get an access_token from GitHub that we can use to authenticate as the installation + See https://docs.github.com/en/enterprise-server@3.9/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation#generating-an-installation-access-token + + ⚠️ side effect: handle_invalid_installation has potential side effects to GithubAppInstallation and Owner models + + Raises: + InvalidInstallationError: if we can't get the installation's access_token + """ + try: + app_id = installation_info.get("app_id", None) + installation_id = installation_info["installation_id"] + github_token = get_github_integration_token( + service.value, + installation_id, + app_id=str(app_id) if app_id else None, + pem_path=installation_info.get("pem_path", None), + ) + installation_token = Token( + key=github_token, + username=f"installation_{installation_id}", + entity_name=gh_app_key_name( + installation_id=installation_id, + app_id=app_id, + ), + ) + # The token is not owned by an Owner object, so 2nd arg is None + return installation_token, None + except InvalidInstallationError as err: + handle_invalid_installation(installation_info, err) + raise err + + +def get_specific_github_app_details( + owner: Owner, github_app_id: int, commitid: str +) -> GithubInstallationInfo: + """Gets the GithubInstallationInfo for GithubAppInstallation with id github_app_id. + + Args: + owner (Owner): the owner of the app. We look only in the apps for this owner. + github_app_id (int): the ID of the GithubAppInstallation we're looking for + commitid (str): Commit.commitid, used for logging purposes + + Raises: + RequestedGithubAppNotFound - if the app is not found in the given owner raise exception. + The assumption is that we need this specific app for a reason, and if we can't find the app + it's better to just fail + """ + app: GithubAppInstallation | None = next( + ( + obj + for obj in DjangoSQLAlchemyOwnerWrapper.get_github_app_installations(owner) + if obj.id == github_app_id + ), + None, + ) + if app is None: + log.exception( + "Can't find requested app", + extra=dict(ghapp_id=github_app_id, commitid=commitid), + ) + raise RequestedGithubAppNotFound() + if not app.is_configured(): + log.warning( + "Request for specific app that is not configured", + extra=dict(ghapp_id=id, commitid=commitid), + ) + return GithubInstallationInfo( + id=app.id, + installation_id=app.installation_id, + app_id=app.app_id, + pem_path=app.pem_path, + ) + + +def _filter_rate_limited_apps( + apps_to_consider: List[GithubAppInstallation], +) -> List[GithubAppInstallation]: + redis_connection = get_redis_connection() + return list( + filter( + lambda obj: not determine_if_entity_is_rate_limited( + redis_connection, + gh_app_key_name(app_id=obj.app_id, installation_id=obj.installation_id), + ), + apps_to_consider, + ) + ) + + +def _filter_suspended_apps( + apps_to_consider: List[GithubAppInstallation], +) -> List[GithubAppInstallation]: + return list(filter(lambda obj: not obj.is_suspended, apps_to_consider)) + + +def get_github_app_info_for_owner( + owner: Owner, + *, + repository: Repository | None = None, + installation_name: str = GITHUB_APP_INSTALLATION_DEFAULT_NAME, +) -> List[GithubInstallationInfo]: + """Gets the GitHub app info needed to communicate with GitHub using an app for this owner. + If multiple apps are available for this owner a selection is done to have 1 main app, and the others + are listed as fallback options. + + ⚠️ The return of this function is NOT enough to actually send requests to GitHub using the app. For that you need to call 'get_github_integration_token' + with an installation info, to get a token. This token is used to send requests to GitHub _as the app_ + (i.e. authenticating as an app installation, see https://docs.github.com/en/enterprise-server@3.9/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app#authentication-as-an-app-installation) + + GitHub App information can be: + 1. A GithubAppInstallation that belongs to the owner + 2. (deprecated) Owner.integration_id + + Args: + owner (Owner): The owner to get GitHub App info for. + repository (Repository | None): The repo that we will interact with. + Any GitHub App info returned needs to cover this repo + installation_name (str): The installation name to search for in the available apps. + GitHubAppInstallation.name must be equal to installation_name for it to be returned. + + Returns: + (ordered) List[GithubInstallationInfo]: where index 0 is the main app and the others are fallback options + + Raises: + NoConfiguredAppsAvailable: Owner has app installations available, but they are all currently rate limited. + """ + extra_info_to_log = dict( + ownerid=owner.ownerid, + repoid=getattr(repository, "repoid", None), + ) + log.info( + "Getting owner's GitHub Apps info", + extra=dict( + installation_name=installation_name, + **extra_info_to_log, + ), + ) + owner_service = Service(owner.service) + if owner_service not in [Service.GITHUB, Service.GITHUB_ENTERPRISE]: + log.info( + "Owner's service not GitHub", + extra=dict(service=owner_service, **extra_info_to_log), + ) + return [] + + # Get the apps available for the owner with the given 'installation_name' + # AND that cover 'repository' (if provided) + apps_to_consider = _get_apps_from_weighted_selection( + owner, installation_name, repository + ) + apps_matching_criteria_count = len(apps_to_consider) + # We can't use apps that are rate limited + apps_to_consider = _filter_rate_limited_apps(apps_to_consider) + rate_limited_apps_count = apps_matching_criteria_count - len(apps_to_consider) + # We can't use apps that are suspended (by the user) + apps_to_consider = _filter_suspended_apps(apps_to_consider) + suspended_apps_count = ( + apps_matching_criteria_count - rate_limited_apps_count - len(apps_to_consider) + ) + + if apps_to_consider: + # There's at least 1 app that matches all the criteria and can be used to communicate with GitHub + main_name = apps_to_consider[0].name + info_to_get_tokens = list( + map( + lambda obj: GithubInstallationInfo( + id=obj.id, + installation_id=obj.installation_id, + app_id=obj.app_id, + pem_path=obj.pem_path, + ), + apps_to_consider, + ) + ) + log.info( + "Selected installation to communicate with github", + extra=dict( + installation_id=info_to_get_tokens[0]["installation_id"], + installation_name=main_name, + fallback_installations=[ + obj["installation_id"] for obj in info_to_get_tokens + ], + ), + ) + return info_to_get_tokens + elif apps_matching_criteria_count > 0: + # There are apps that match the criteria, but we can't use them. + # Either they are currently rate limited or they have been suspended. + raise NoConfiguredAppsAvailable( + apps_count=apps_matching_criteria_count, + rate_limited_count=rate_limited_apps_count, + suspended_count=suspended_apps_count, + ) + # DEPRECATED FLOW - begin + if owner.integration_id and ( + (repository and repository.using_integration) or (repository is None) + ): + log.info( + "Selected deprecated owner.integration_id to communicate with github", + extra=extra_info_to_log, + ) + return [GithubInstallationInfo(installation_id=owner.integration_id)] + # DEPRECATED FLOW - end + return [] diff --git a/libs/shared/shared/bots/helpers.py b/libs/shared/shared/bots/helpers.py new file mode 100644 index 0000000000..41743c6754 --- /dev/null +++ b/libs/shared/shared/bots/helpers.py @@ -0,0 +1,86 @@ +import logging +from typing import Self + +from pydantic import BaseModel, ValidationError + +from shared.bots.exceptions import RepositoryWithoutValidBotError +from shared.config import get_config +from shared.github import InvalidInstallationError +from shared.github import get_github_integration_token as _get_github_integration_token +from shared.torngit.base import TokenType +from shared.typings.oauth_token_types import Token + +log = logging.getLogger(__name__) + + +def get_github_integration_token( + service: str, + installation_id: int | None = None, + app_id: str | None = None, + pem_path: str | None = None, +): + try: + return _get_github_integration_token( + service, + integration_id=installation_id, + app_id=app_id, + pem_path=pem_path, + ) + except InvalidInstallationError: + log.warning("Failed to get installation token") + raise RepositoryWithoutValidBotError() + + +class DedicatedApp(BaseModel): + id: str | int + installation_id: int + pem: str + + @classmethod + def validate_or_none(cls, value: object) -> Self | None: + try: + return cls.model_validate(value) + except ValidationError: + return None + + def pem_path(self, service: str, token_type: TokenType) -> str: + return f"yaml+file://{service}.dedicated_apps.{token_type.value}.pem" + + +def get_dedicated_app_token_from_config( + service: str, token_type: TokenType +) -> Token | None: + # GitHub can have 'dedicated_apps', and those are preferred + dedicated_app = DedicatedApp.validate_or_none( + get_config(service, "dedicated_apps", token_type.value, default={}) + ) + if dedicated_app is None: + return None + + actual_token = get_github_integration_token( + service, + app_id=str(dedicated_app.id), + installation_id=dedicated_app.installation_id, + pem_path=dedicated_app.pem_path(service, token_type), + ) + return Token( + key=actual_token, + username=f"{token_type.value}_dedicated_app", + entity_name=token_type.value, + ) + + +def get_token_type_from_config(service: str, token_type: TokenType) -> Token | None: + """Gets the TokenType credentials configured for this `service` in the install config (YAML). + [All providers] Configuration are defined as a `bot` per TokenType. + [GitHub] Can also have a `dedicated_app`. `dedicated_app` is preferred. + """ + if service in ["github", "github_enterprise"]: + dedicated_app_config = get_dedicated_app_token_from_config(service, token_type) + if dedicated_app_config: + return dedicated_app_config + + token_from_config = get_config(service, "bots", token_type.value) + if token_from_config: + token_from_config["entity_name"] = token_type.value + return token_from_config diff --git a/libs/shared/shared/bots/owner_bots.py b/libs/shared/shared/bots/owner_bots.py new file mode 100644 index 0000000000..538b871813 --- /dev/null +++ b/libs/shared/shared/bots/owner_bots.py @@ -0,0 +1,42 @@ +import logging + +from shared.bots.exceptions import OwnerWithoutValidBotError +from shared.bots.github_apps import get_github_app_token +from shared.bots.types import TokenWithOwner +from shared.django_apps.codecov_auth.models import Owner, Service +from shared.encryption.oauth import get_encryptor_from_configuration +from shared.rate_limits import owner_key_name +from shared.typings.oauth_token_types import Token +from shared.typings.torngit import GithubInstallationInfo + +encryptor = get_encryptor_from_configuration() + +log = logging.getLogger(__name__) + + +def get_owner_or_appropriate_bot(owner: Owner, repoid: int | None = None) -> Owner: + if owner.bot is not None and owner.bot.oauth_token is not None: + log.info( + "Owner has specific bot", + extra=dict(botid=owner.bot.ownerid, ownerid=owner.ownerid, repoid=repoid), + ) + return owner.bot + elif owner.oauth_token is not None: + log.info( + "No bot, using owner", extra=dict(ownerid=owner.ownerid, repoid=repoid) + ) + return owner + raise OwnerWithoutValidBotError() + + +def get_owner_appropriate_bot_token( + owner, installation_info: GithubInstallationInfo | None = None +) -> TokenWithOwner: + if installation_info: + result = get_github_app_token(Service(owner.service), installation_info) + return result + + token_owner = get_owner_or_appropriate_bot(owner) + token: Token = encryptor.decrypt_token(token_owner.oauth_token) + token["entity_name"] = owner_key_name(owner_id=token_owner.ownerid) + return token, token_owner diff --git a/libs/shared/shared/bots/public_bots.py b/libs/shared/shared/bots/public_bots.py new file mode 100644 index 0000000000..ab14954d27 --- /dev/null +++ b/libs/shared/shared/bots/public_bots.py @@ -0,0 +1,79 @@ +import logging + +from shared.bots.exceptions import RepositoryWithoutValidBotError +from shared.bots.helpers import get_token_type_from_config +from shared.bots.types import TokenTypeMapping, TokenWithOwner +from shared.config import get_config +from shared.django_apps.codecov_auth.models import Service +from shared.django_apps.core.models import Repository +from shared.torngit.base import TokenType +from shared.typings.oauth_token_types import Token + +log = logging.getLogger(__name__) + + +def get_public_bot_token(service: Service, repoid: int) -> TokenWithOwner: + """Gets the configured public bot for a service. + + These bots are declared in the install YAML per service. + They can only access public repositories (in general) + """ + # Generic bot for this service + public_bot_dict = get_config(service.value, "bot") + # Function-specific bots for this service + # In this case we want the 'tokenless' function + tokenless_bot_dict = get_config( + service.value, "bots", "tokenless", default=public_bot_dict + ) + + if tokenless_bot_dict and tokenless_bot_dict.get("key"): + log.info( + "Using tokenless bot as bot fallback", + extra=dict(repoid=repoid, botname=tokenless_bot_dict.get("username")), + ) + tokenless_bot_dict["entity_name"] = "tokenless_bot" + # Once again token not owned by an Owner. + return tokenless_bot_dict, None + + log.error( + "No tokenless bot dict in get_public_bot_token", + extra=dict(repoid=repoid), + ) + raise RepositoryWithoutValidBotError() + + +def get_token_type_mapping( + repo: Repository, admin_bot_token: Token | None = None +) -> TokenTypeMapping | None: + """Gets the fallback tokens configured via install YAML per function. + + This only affects _public_ repos, as private ones need a token defined. + A public repo might have a token defined (the admin_bot), in which case it is used for all functions, + except comment. + """ + if repo.private: + return None + + if admin_bot_token is None: + log.warning( + "No admin_bot_token provided, but still continuing operations in case it is not doing an admin call anyway", + extra=dict(repoid=repo.repoid), + ) + + mapping = { + TokenType.admin: admin_bot_token, + # [GitHub] Only legacy Personal Access Tokens (PAT) can post statuses and comment to all public repos, + # so there can't be a dedicated_app for this + TokenType.comment: get_config(repo.service, "bots", "comment"), + TokenType.status: admin_bot_token or get_config(repo.service, "bots", "status"), + } + for token_type in [ + TokenType.read, + TokenType.tokenless, + TokenType.commit, + TokenType.pull, + ]: + mapping[token_type] = admin_bot_token or get_token_type_from_config( + repo.service, token_type + ) + return mapping diff --git a/libs/shared/shared/bots/repo_bots.py b/libs/shared/shared/bots/repo_bots.py new file mode 100644 index 0000000000..260b914bbf --- /dev/null +++ b/libs/shared/shared/bots/repo_bots.py @@ -0,0 +1,79 @@ +import logging + +from shared.bots.exceptions import ( + OwnerWithoutValidBotError, + RepositoryWithoutValidBotError, +) +from shared.bots.github_apps import get_github_app_token +from shared.bots.owner_bots import get_owner_or_appropriate_bot +from shared.bots.public_bots import get_public_bot_token +from shared.bots.types import TokenWithOwner +from shared.config import get_config +from shared.django_apps.codecov_auth.models import Owner, Service +from shared.django_apps.core.models import Repository +from shared.encryption.oauth import get_encryptor_from_configuration +from shared.environment.environment import is_enterprise +from shared.orms.repository_helper import DjangoSQLAlchemyRepositoryWrapper +from shared.rate_limits import owner_key_name +from shared.typings.torngit import GithubInstallationInfo + +encryptor = get_encryptor_from_configuration() + +log = logging.getLogger(__name__) + + +def get_repo_particular_bot_token(repo) -> TokenWithOwner: + appropriate_bot = get_repo_appropriate_bot(repo) + token_dict = encryptor.decrypt_token(appropriate_bot.oauth_token) + token_dict["username"] = appropriate_bot.username + token_dict["entity_name"] = owner_key_name(appropriate_bot.ownerid) + return token_dict, appropriate_bot + + +def get_repo_appropriate_bot(repo: Repository) -> Owner: + if repo.bot is not None and repo.bot.oauth_token is not None: + log.info( + "Repo has specific bot", + extra=dict(repoid=repo.repoid, botid=repo.bot.ownerid), + ) + return repo.bot + try: + return get_owner_or_appropriate_bot( + DjangoSQLAlchemyRepositoryWrapper.get_repo_owner(repo) + ) + except OwnerWithoutValidBotError: + raise RepositoryWithoutValidBotError() + + +def get_repo_appropriate_bot_token( + repo: Repository, + installation_info: GithubInstallationInfo | None = None, +) -> TokenWithOwner: + extra_info_to_log = dict( + repoid=repo.repoid, is_private=repo.private, service=repo.service + ) + log.info( + "Get repo appropriate bot token", + extra={"installation_info": installation_info, **extra_info_to_log}, + ) + + service = Service(repo.service) + + if is_enterprise() and get_config(repo.service, "bot"): + log.info( + "Using enterprise-configured bot for the service", extra=extra_info_to_log + ) + return get_public_bot_token(service, repo.repoid) + + if installation_info: + log.info("Using github installation", extra=extra_info_to_log) + return get_github_app_token(service, installation_info) + try: + token_dict, appropriate_bot = get_repo_particular_bot_token(repo) + log.info("Using repo particular bot", extra=extra_info_to_log) + return token_dict, appropriate_bot + except RepositoryWithoutValidBotError as e: + if not repo.private: + log.info("Using YAML-configured public bot", extra=extra_info_to_log) + return get_public_bot_token(service, repo.repoid) + raise e diff --git a/libs/shared/shared/bots/types.py b/libs/shared/shared/bots/types.py new file mode 100644 index 0000000000..59a9320f0d --- /dev/null +++ b/libs/shared/shared/bots/types.py @@ -0,0 +1,34 @@ +from typing import Any, Dict, List, Optional, Tuple, TypedDict + +from shared.django_apps.codecov_auth.models import Owner +from shared.torngit.base import TokenType +from shared.typings.oauth_token_types import Token +from shared.typings.torngit import GithubInstallationInfo + +# A Token and its Owner +# If a Token doesn't belong to Owner (i.e. it's a GitHubAppInstallation Token), second value is None +TokenWithOwner = Tuple[Token, Optional[Owner]] + +TokenTypeMapping = Dict[TokenType, Token] + + +class AdapterAuthInformation(TypedDict): + """This class is just a type annotation for the return value of services.bots.get_adapter_auth_information + It is a container with all the information we need to authenticate a given repo/owner with the git provider. + Specific fields have comments to document them further + """ + + # This is the Authentication used with the git provider + token: Token + # token_owner is used to decide on token_refresh functions. Could be SQLAlchemy | Django Owner - leaving Any here + token_owner: Owner | Any | None + # GitHub app info - exclusive for GitHub (duh) + # Preferred method of authentication (if available) + # selected_installation_info is the installation being used to communicate with github. We save this info in the TorngitAdapter. + # If this installation becomes rate-limited the TorngitAdapter uses the info to mark it so (so we don't select it for a while) + # fallback_installations are used if multi-apps are available and the selected one becomes rate-limited + selected_installation_info: GithubInstallationInfo | None + fallback_installations: List[GithubInstallationInfo] | None + # TokenTypeMapping + # exclusive for public repos not using an installation. Fallback tokens per action + token_type_mapping: TokenTypeMapping | None diff --git a/libs/shared/shared/bundle_analysis/__init__.py b/libs/shared/shared/bundle_analysis/__init__.py new file mode 100644 index 0000000000..5d03540b98 --- /dev/null +++ b/libs/shared/shared/bundle_analysis/__init__.py @@ -0,0 +1,38 @@ +from shared.bundle_analysis import models +from shared.bundle_analysis.comparison import ( + AssetChange, + BundleAnalysisComparison, + BundleChange, + BundleComparison, + MissingBaseReportError, + MissingBundleError, + MissingHeadReportError, + RouteChange, +) +from shared.bundle_analysis.parser import Parser +from shared.bundle_analysis.report import ( + AssetReport, + BundleAnalysisReport, + BundleReport, + ModuleReport, +) +from shared.bundle_analysis.storage import BundleAnalysisReportLoader, StoragePaths + +__all__ = [ + "models", + "AssetChange", + "BundleAnalysisComparison", + "BundleChange", + "BundleComparison", + "MissingBaseReportError", + "MissingBundleError", + "MissingHeadReportError", + "Parser", + "AssetReport", + "BundleAnalysisReport", + "BundleReport", + "ModuleReport", + "BundleAnalysisReportLoader", + "StoragePaths", + "RouteChange", +] diff --git a/libs/shared/shared/bundle_analysis/comparison.py b/libs/shared/shared/bundle_analysis/comparison.py new file mode 100644 index 0000000000..3a690fecdb --- /dev/null +++ b/libs/shared/shared/bundle_analysis/comparison.py @@ -0,0 +1,498 @@ +import logging +from collections import defaultdict +from dataclasses import dataclass +from enum import Enum +from functools import cached_property +from typing import Dict, Iterator, List, MutableSet, Optional, Tuple + +import sentry_sdk + +from shared.bundle_analysis.models import MetadataKey +from shared.bundle_analysis.report import ( + AssetReport, + BundleAnalysisReport, + BundleReport, + BundleRouteReport, + ModuleReport, +) +from shared.bundle_analysis.storage import BundleAnalysisReportLoader +from shared.django_apps.core.models import Repository +from shared.django_apps.reports.models import CommitReport + +log = logging.getLogger(__name__) + + +class MissingBaseReportError(Exception): + pass + + +class MissingHeadReportError(Exception): + pass + + +class MissingBundleError(Exception): + pass + + +@dataclass(frozen=True) +class BaseChange: + """ + Base class for representing changes between two different reports. + """ + + class ChangeType(Enum): + ADDED = "added" + REMOVED = "removed" + CHANGED = "changed" + + change_type: ChangeType + size_delta: int + + +@dataclass(frozen=True) +class BundleChange(BaseChange): + """ + Info about how a bundle has changed between two different reports. + """ + + bundle_name: str + percentage_delta: float + + +@dataclass(frozen=True) +class RouteChange(BaseChange): + """ + Info about how a bundle route has changed between two different reports. + """ + + route_name: str + percentage_delta: float + size_base: int + size_head: int + + +@dataclass(frozen=True) +class AssetChange(BaseChange): + """ + Info about how an asset has changed between two different reports. + """ + + asset_name: str + percentage_delta: float + size_base: int + size_head: int + + +AssetMatch = Tuple[Optional[AssetReport], Optional[AssetReport]] + + +class AssetComparison: + def __init__( + self, + base_asset_report: Optional[AssetReport] = None, + head_asset_report: Optional[AssetReport] = None, + ): + self.base_asset_report = base_asset_report + self.head_asset_report = head_asset_report + + @sentry_sdk.trace + def asset_change(self) -> AssetChange: + if self.base_asset_report is None: + return AssetChange( + asset_name=self.head_asset_report.name, + change_type=AssetChange.ChangeType.ADDED, + size_delta=self.head_asset_report.size, + percentage_delta=100.0, + size_base=0, + size_head=self.head_asset_report.size, + ) + elif self.head_asset_report is None: + return AssetChange( + asset_name=self.base_asset_report.name, + change_type=AssetChange.ChangeType.REMOVED, + size_delta=-self.base_asset_report.size, + percentage_delta=-100.0, + size_base=self.base_asset_report.size, + size_head=0, + ) + else: + size_delta = self.head_asset_report.size - self.base_asset_report.size + if size_delta == 0: + percentage_delta = 0 + elif self.base_asset_report.size == 0: + percentage_delta = 100.0 + else: + percentage_delta = round( + (size_delta / self.base_asset_report.size) * 100, 2 + ) + return AssetChange( + asset_name=self.head_asset_report.name, + change_type=AssetChange.ChangeType.CHANGED, + size_delta=size_delta, + percentage_delta=percentage_delta, + size_base=self.base_asset_report.size, + size_head=self.head_asset_report.size, + ) + + def contributing_modules( + self, pr_changed_files: Optional[List[str]] = None + ) -> List[ModuleReport]: + asset_report = self.head_asset_report + if asset_report is None: + return [] + return asset_report.modules(pr_changed_files) + + +class BundleComparison: + def __init__( + self, base_bundle_report: BundleReport, head_bundle_report: BundleReport + ): + self.base_bundle_report = base_bundle_report + self.head_bundle_report = head_bundle_report + + def total_size_delta(self) -> int: + base_size = self.base_bundle_report.total_size() + head_size = self.head_bundle_report.total_size() + return head_size - base_size + + @sentry_sdk.trace + def asset_comparisons(self) -> List[AssetComparison]: + # this groups assets by name + # there can be multiple assets with the same name and we + # need to try and match them across base and head reports + base_asset_reports = defaultdict(set) + for asset_report in self.base_bundle_report.asset_reports(): + base_asset_reports[asset_report.name].add(asset_report) + head_asset_reports = defaultdict(set) + for asset_report in self.head_bundle_report.asset_reports(): + head_asset_reports[asset_report.name].add(asset_report) + + # match bundles across base and head + # (A, B) means that bundle A transformed to bundle B + # (X, None) means that bundle X was deleted + # (None, X) means that bundle X was added + matches: List[AssetMatch] = [] + asset_names = [] + for asset_name, asset_reports in head_asset_reports.items(): + asset_names.append(asset_name) + matches += self._match_assets(base_asset_reports[asset_name], asset_reports) + for asset_name, asset_reports in base_asset_reports.items(): + if asset_name not in asset_names: + matches += self._match_assets(asset_reports, []) + + return [ + AssetComparison(base_asset_report, head_asset_report) + for base_asset_report, head_asset_report in matches + ] + + def _match_assets( + self, + base_asset_reports: MutableSet[AssetReport], + head_asset_reports: MutableSet[AssetReport], + ) -> List[AssetMatch]: + """ + The given base assets and head assets all have the same name. + This method attempts to pick the most likely matching of assets between + base and head (so as to track their changes through time). + + Current approach: + 1. Pick asset with the same UUID. This means the base and head assets have either of: + - same hashed name + - same modules by name + 2. Pick asset with the closest size + """ + n = max([len(base_asset_reports), len(head_asset_reports)]) + matches: List[AssetMatch] = [] + + while len(matches) < n: + if len(head_asset_reports) > 0: + # we have an unmatched head asset + head_asset_report = head_asset_reports.pop() + + if len(base_asset_reports) == 0: + # no more base assets to match against + matches.append((None, head_asset_report)) + else: + # 1. Pick asset with the same UUID + base_asset_report_uuids = { + base_bundle.uuid: base_bundle + for base_bundle in base_asset_reports + } + if head_asset_report.uuid in base_asset_report_uuids: + base_asset_report = base_asset_report_uuids[ + head_asset_report.uuid + ] + + # 2. Pick asset with the closest size + else: + size_deltas = { + abs(head_asset_report.size - base_bundle.size): base_bundle + for base_bundle in base_asset_reports + } + min_delta = min(size_deltas.keys()) + base_asset_report = size_deltas[min_delta] + + matches.append((base_asset_report, head_asset_report)) + base_asset_reports.remove(base_asset_report) + elif len(base_asset_reports) > 0: + # we have unmatched base assets and no more head assets + base_asset_report = base_asset_reports.pop() + matches.append((base_asset_report, None)) + else: + # shouldn't ever get here + raise Exception("incorrect asset matching logic") # pragma: no cover + + return matches + + +class BundleRoutesComparison: + """ + Compares all routes of two bundle route reports for a given bundle + """ + + def __init__( + self, + base_report: BundleRouteReport, + head_report: BundleRouteReport, + ): + self.base_report = base_report + self.head_report = head_report + + @sentry_sdk.trace + def size_changes(self) -> List[RouteChange]: + """ + Returns a list of changes for each unique route that exists between the base and head. + If a route exists on base but not head that is considered "removed" and -100% percentage delta + If a route exists on head but not base that is considered "added" and +100% percentage delta + Otherwise it is considered "changed" and percentage delta = (diff_size / base_size) * 100 + """ + base_sizes = self.base_report.get_sizes() + head_sizes = self.head_report.get_sizes() + + all_routes, results = base_sizes.keys() | head_sizes.keys(), [] + for route_name in all_routes: + # Added new route + if route_name not in base_sizes or base_sizes[route_name] == 0: + results.append( + RouteChange( + route_name=route_name, + change_type=RouteChange.ChangeType.ADDED, + size_delta=head_sizes[route_name], + percentage_delta=100, + size_base=0, + size_head=head_sizes[route_name], + ) + ) + # Removed old route + elif route_name not in head_sizes: + results.append( + RouteChange( + route_name=route_name, + change_type=RouteChange.ChangeType.REMOVED, + size_delta=-base_sizes[route_name], + percentage_delta=-100.0, + size_base=base_sizes[route_name], + size_head=0, + ) + ) + # Changed + else: + size_delta = head_sizes[route_name] - base_sizes[route_name] + percentage_delta = round((size_delta / base_sizes[route_name]) * 100, 2) + results.append( + RouteChange( + route_name=route_name, + change_type=RouteChange.ChangeType.CHANGED, + size_delta=size_delta, + percentage_delta=percentage_delta, + size_base=base_sizes[route_name], + size_head=head_sizes[route_name], + ) + ) + + return results + + +class BundleAnalysisComparison: + """ + Compares two different bundle analysis reports. + """ + + def __init__( + self, + loader: BundleAnalysisReportLoader, + base_report_key: str, + head_report_key: str, + repository: Optional[Repository] = None, + ): + self.loader = loader + self.base_report_key = base_report_key + self.head_report_key = head_report_key + + compare_sha_external_id = self._check_compare_sha(repository) + if compare_sha_external_id: + self.base_report_key = compare_sha_external_id + + def _check_compare_sha(self, repository: Repository) -> Optional[str]: + """ + When doing comparisons first check if there is a compare_sha set in the head report, + if there is use that commitid to load the base commit report to compare the head to. + """ + try: + head_report_compare_sha = self.head_report.metadata().get( + MetadataKey.COMPARE_SHA + ) + if head_report_compare_sha and repository: + base_report = CommitReport.objects.filter( + commit__commitid=head_report_compare_sha, + commit__repository=repository, + report_type=CommitReport.ReportType.BUNDLE_ANALYSIS, + ).first() + if base_report: + return base_report.external_id + else: + log.warning( + f"Bundle Analysis compare SHA not found in reports for {head_report_compare_sha}" + ) + except MissingHeadReportError: + pass + + @cached_property + def base_report(self) -> BundleAnalysisReport: + base_report = self.loader.load(self.base_report_key) + if base_report is None: + raise MissingBaseReportError() + return base_report + + @cached_property + def head_report(self) -> BundleAnalysisReport: + head_report = self.loader.load(self.head_report_key) + if head_report is None: + raise MissingHeadReportError() + return head_report + + @sentry_sdk.trace + def bundle_changes(self) -> Iterator[BundleChange]: + """ + Returns a list of changes across the bundles in the base and head reports. + """ + base_bundle_reports = { + bundle_report.name: bundle_report + for bundle_report in self.base_report.bundle_reports() + } + head_bundle_reports = { + bundle_report.name: bundle_report + for bundle_report in self.head_report.bundle_reports() + } + + for bundle_name, head_bundle_report in head_bundle_reports.items(): + if bundle_name not in base_bundle_reports: + yield BundleChange( + bundle_name=bundle_name, + change_type=BundleChange.ChangeType.ADDED, + size_delta=head_bundle_report.total_size(), + percentage_delta=100, + ) + else: + base_bundle_report = base_bundle_reports[bundle_name] + del base_bundle_reports[bundle_name] + size_delta = ( + head_bundle_report.total_size() - base_bundle_report.total_size() + ) + percentage_delta = round( + (size_delta / base_bundle_report.total_size()) * 100, 2 + ) + yield BundleChange( + bundle_name=bundle_name, + change_type=BundleChange.ChangeType.CHANGED, + size_delta=size_delta, + percentage_delta=percentage_delta, + ) + + for bundle_name, base_bundle_report in base_bundle_reports.items(): + yield BundleChange( + bundle_name=bundle_name, + change_type=BundleChange.ChangeType.REMOVED, + size_delta=-base_bundle_report.total_size(), + percentage_delta=-100.0, + ) + + @property + def total_size_delta(self) -> int: + return sum(bundle_change.size_delta for bundle_change in self.bundle_changes()) + + @property + def percentage_delta(self) -> float: + """Returns the size delta as a percentage of BASE report total size. + For example, base_bundle_reports have a total size of 1MB + and the total_size_delta is 100kB then percentage_delta is 10.0% + + Percentage is returned as a float 0-100, rounded to 2 decimal places + """ + base_size = sum( + report.total_size() for report in self.base_report.bundle_reports() + ) + if base_size == 0: + return 100.0 + return round((self.total_size_delta / base_size) * 100, 2) + + @sentry_sdk.trace + def bundle_comparison(self, bundle_name: str) -> BundleComparison: + """ + More detailed comparison (about asset changes) for a particular bundle that + exists both in the base and head reports. + """ + base_bundle_report = self.base_report.bundle_report(bundle_name) + head_bundle_report = self.head_report.bundle_report(bundle_name) + if base_bundle_report is None or head_bundle_report is None: + raise MissingBundleError() + return BundleComparison(base_bundle_report, head_bundle_report) + + @sentry_sdk.trace + def bundle_routes_changes(self) -> Dict[str, List[RouteChange]]: + """ + Comparison for all the routes available to a pair of bundles. + """ + comparison_mapping = {} + base_bundle_reports = { + bundle_report.name: bundle_report.full_route_report() + for bundle_report in self.base_report.bundle_reports() + } + head_bundle_reports = { + bundle_report.name: bundle_report.full_route_report() + for bundle_report in self.head_report.bundle_reports() + } + + # Combine all bundle route reports with base and head. If either don't exist + # then it will be set as None in the comparison param. + bundle_names = base_bundle_reports.keys() | head_bundle_reports.keys() + comparison_mapping = { + name: BundleRoutesComparison( + base_bundle_reports.get( + name, BundleRouteReport(self.base_report.db_path, {}) + ), + head_bundle_reports.get( + name, BundleRouteReport(self.head_report.db_path, {}) + ), + ).size_changes() + for name in bundle_names + } + + return comparison_mapping + + @sentry_sdk.trace + def bundle_routes_changes_by_bundle(self, bundle_name: str) -> List[RouteChange]: + """ + Comparison for all the routes available to a pair of bundles. + """ + base_bundle_report = self.base_report.bundle_report(bundle_name) + head_bundle_report = self.head_report.bundle_report(bundle_name) + if base_bundle_report is None or head_bundle_report is None: + raise MissingBundleError() + + base_route_report = base_bundle_report.full_route_report() + head_route_report = head_bundle_report.full_route_report() + + return BundleRoutesComparison( + base_route_report, head_route_report + ).size_changes() diff --git a/libs/shared/shared/bundle_analysis/db_migrations.py b/libs/shared/shared/bundle_analysis/db_migrations.py new file mode 100644 index 0000000000..403834fe4d --- /dev/null +++ b/libs/shared/shared/bundle_analysis/db_migrations.py @@ -0,0 +1,53 @@ +from collections.abc import Callable + +import sentry_sdk +from sqlalchemy import text +from sqlalchemy.orm import Session + +from shared.bundle_analysis.migrations.v001_add_gzip_size import add_gzip_size +from shared.bundle_analysis.migrations.v002_bundle_is_cached import add_is_cached +from shared.bundle_analysis.migrations.v003_modify_gzip_size_nullable import ( + modify_gzip_size_nullable, +) +from shared.bundle_analysis.migrations.v004_add_dynamic_imports import ( + add_dynamic_imports, +) + + +class BundleAnalysisMigration: + """ + Keeps track of DB schema migrations for the Bundle Analysis Report SQLite file + When updating the tables/models in assets.py, the SCHEMA_VERSION needs to be + incremented by 1, and an entry in self.migrations needs to correspond the new version + and the SQL changes that needs to execute to update to the latest changes. + When new bundle analysis reports are processed they will be on the latest version and + no migration will be needed, however for commits with reports of older versions, when + they are fetched the migrations will be applied to prevent errors with unexpected DB schemas. + """ + + def __init__(self, db_session: Session, from_version: int, to_version: int): + self.db_session = db_session + self.from_version = from_version + self.to_version = to_version + + # Mapping of the schema_version number to the migration function that needs to run + # {x: fcn} means to bring version x-1 to x, fcn must be ran + self.migrations: dict[int, Callable[[Session], None]] = { + 2: add_gzip_size, + 3: add_is_cached, + 4: modify_gzip_size_nullable, + 5: add_dynamic_imports, + } + + def update_schema_version(self, version): + stmt = f""" + UPDATE "metadata" SET "value"={version} WHERE "key"='schema_version' + """ + self.db_session.execute(text(stmt)) + + @sentry_sdk.trace + def migrate(self): + for version in range(self.from_version + 1, self.to_version + 1): + self.migrations[version](self.db_session) + self.update_schema_version(version) + self.db_session.commit() diff --git a/libs/shared/shared/bundle_analysis/migrations/__init__.py b/libs/shared/shared/bundle_analysis/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/bundle_analysis/migrations/v001_add_gzip_size.py b/libs/shared/shared/bundle_analysis/migrations/v001_add_gzip_size.py new file mode 100644 index 0000000000..014c3d6c47 --- /dev/null +++ b/libs/shared/shared/bundle_analysis/migrations/v001_add_gzip_size.py @@ -0,0 +1,21 @@ +from sqlalchemy import text +from sqlalchemy.orm import Session + + +def add_gzip_size(db_session: Session): + # Inserts gzip_size column to assets table + # then sets value to 1/1000 of the uncompressed asset size + # using this arbitrary number because that's what we've + # historically been computing it as in the API + + # Create new column + stmt = """ + ALTER TABLE "assets" ADD COLUMN "gzip_size" integer NOT NULL DEFAULT 0 + """ + db_session.execute(text(stmt)) + + # Set default value to assets.size / 1000 + stmt = """ + UPDATE "assets" SET "gzip_size"="size"/1000 + """ + db_session.execute(text(stmt)) diff --git a/libs/shared/shared/bundle_analysis/migrations/v002_bundle_is_cached.py b/libs/shared/shared/bundle_analysis/migrations/v002_bundle_is_cached.py new file mode 100644 index 0000000000..ab37c80e98 --- /dev/null +++ b/libs/shared/shared/bundle_analysis/migrations/v002_bundle_is_cached.py @@ -0,0 +1,17 @@ +from sqlalchemy import text +from sqlalchemy.orm import Session + + +def add_is_cached(db_session: Session): + """ + Inserts is_cached column to bundles table + then sets value default value to False because if there was + no column for this it means it was created before caching + mechanism existed + """ + + # Create new column + stmt = """ + ALTER TABLE "bundles" ADD COLUMN "is_cached" boolean NOT NULL DEFAULT 0 + """ + db_session.execute(text(stmt)) diff --git a/libs/shared/shared/bundle_analysis/migrations/v003_modify_gzip_size_nullable.py b/libs/shared/shared/bundle_analysis/migrations/v003_modify_gzip_size_nullable.py new file mode 100644 index 0000000000..795a7b486d --- /dev/null +++ b/libs/shared/shared/bundle_analysis/migrations/v003_modify_gzip_size_nullable.py @@ -0,0 +1,59 @@ +from sqlalchemy import text +from sqlalchemy.exc import OperationalError +from sqlalchemy.orm import Session + + +def modify_gzip_size_nullable(db_session: Session): + """ + Modify gzip_size column of assets table to be a nullable value + Because SQLite does not have a "alter column" command we need to + rename the existing table, create the new table, and migrate all the data over + """ + + # Fixes an issue where old bundle reports that were created before the inception of + # DB migrations would error because UUID column does not exist. + try: + db_session.execute( + text(""" + ALTER TABLE "assets" ADD COLUMN "uuid" text DEFAULT '' + """) + ) + except OperationalError: + # Ignore the case where uuid column already exists + pass + + stmts = [ + """ + PRAGMA foreign_keys=off; + """, + """ + ALTER TABLE assets RENAME TO assets_old; + """, + """ + CREATE TABLE assets ( + id integer primary key, + session_id integer not null, + name text not null, + normalized_name text not null, + size integer not null, + gzip_size integer, + uuid text not null, + asset_type text not null, + foreign key (session_id) references sessions (id) + ); + """, + """ + INSERT INTO assets (id, session_id, name, normalized_name, size, gzip_size, uuid, asset_type) + SELECT id, session_id, name, normalized_name, size, gzip_size, uuid, asset_type + FROM assets_old; + """, + """ + DROP TABLE assets_old; + """, + """ + PRAGMA foreign_keys=on; + """, + ] + + for stmt in stmts: + db_session.execute(text(stmt)) diff --git a/libs/shared/shared/bundle_analysis/migrations/v004_add_dynamic_imports.py b/libs/shared/shared/bundle_analysis/migrations/v004_add_dynamic_imports.py new file mode 100644 index 0000000000..ce9af1cac4 --- /dev/null +++ b/libs/shared/shared/bundle_analysis/migrations/v004_add_dynamic_imports.py @@ -0,0 +1,27 @@ +from sqlalchemy import text +from sqlalchemy.orm import Session + + +def add_dynamic_imports(db_session: Session): + """ + Adds a table called dynamic_imports (DynamicImport model name) + This table represents for a given Chunk what are its dynamically + imported Assets, if applicable. + + There is no data available to migrate, any older versions of bundle + reports will be considered to not have dynamic imports + """ + stmts = [ + """ + CREATE TABLE dynamic_imports ( + chunk_id integer not null, + asset_id integer not null, + primary key (chunk_id, asset_id), + foreign key (chunk_id) references chunks (id), + foreign key (asset_id) references assets (id) + ); + """, + ] + + for stmt in stmts: + db_session.execute(text(stmt)) diff --git a/libs/shared/shared/bundle_analysis/models.py b/libs/shared/shared/bundle_analysis/models.py new file mode 100644 index 0000000000..6d74ad696a --- /dev/null +++ b/libs/shared/shared/bundle_analysis/models.py @@ -0,0 +1,311 @@ +import logging +from enum import Enum +from typing import Optional + +import sqlalchemy +from sqlalchemy import Column, ForeignKey, Table, create_engine, types +from sqlalchemy import Enum as SQLAlchemyEnum +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import Session as DbSession +from sqlalchemy.orm import backref, relationship, sessionmaker + +log = logging.getLogger(__name__) + + +SCHEMA = """ +create table bundles ( + id integer primary key, + name text, + is_cached boolean not null default 0 +); + +--- only allow 1 null name (the default bundle) +create unique index bundles_name_index on bundles (ifnull(name, 'codecov-default-bundle-name')); + +create table sessions ( + id integer primary key, + info text not null, + bundle_id integer, --- this is nullable just temporarily while parsing + foreign key (bundle_id) references bundles (id) +); + +create index sessions_bundle_id_index on sessions (bundle_id); + +create table metadata ( + key text primary key, + value text not null +); + +create table assets ( + id integer primary key, + session_id integer not null, + name text not null, + normalized_name text not null, + size integer not null, + gzip_size integer, + uuid text not null, + asset_type text not null, + foreign key (session_id) references sessions (id) +); + +create index assets_session_id_index on assets (session_id); +create index assets_name_index on assets (name); + +create table chunks ( + id integer primary key, + session_id integer not null, + external_id text not null, + unique_external_id text not null, + entry boolean not null, + initial boolean not null, + foreign key (session_id) references sessions (id) +); + +create table assets_chunks ( + asset_id integer not null, + chunk_id integer not null, + primary key (asset_id, chunk_id), + foreign key (asset_id) references assets (id), + foreign key (chunk_id) references chunks (id) +); + +create table modules ( + id integer primary key, + session_id integer not null, + name text not null, + size integer not null, + foreign key (session_id) references sessions (id) +); + +create table chunks_modules ( + chunk_id integer not null, + module_id integer not null, + primary key (chunk_id, module_id), + foreign key (chunk_id) references chunks (id), + foreign key (module_id) references modules (id) +); + +create table dynamic_imports ( + chunk_id integer not null, + asset_id integer not null, + primary key (chunk_id, asset_id), + foreign key (chunk_id) references chunks (id), + foreign key (asset_id) references assets (id) +); +""" + +SCHEMA_VERSION = 5 + +Base = declarative_base() + +""" +Create a custom context manager for SQLAlchemy session because worker is currently +stuck on SQLAlchemy version <1.4, and built in context manager for session is introduced +in 1.4 (which is also what codecov-api uses). +It is a lot of work to upgrade worker's SQLAlchemy version because it is too closely tied +into its postgres DB support. +When worker fully migrates to Django for postgres, we can simply upgrade the SQLAlchemy +version to support modern functionalities and delete this legacy code. +For now if the SQLAlchemy version is <1.4 it will go through the custom LegacySessionManager +context manager object to handle opening and closing its sessions. +""" + + +class LegacySessionManager: + def __init__(self, session: DbSession): + self.session = session + + def __enter__(self): + return self.session + + def __exit__(self, type, value, traceback): + self.session.close() + + +def _use_modern_sqlalchemy_session_manager(): + try: + version = sqlalchemy.__version__ + major = int(version.split(".")[0]) + minor = int(version.split(".")[1]) + return major >= 2 or (major == 1 and minor >= 4) + except Exception as e: + log.info( + f"Can't determine which SQLAlchemy session manager to use, falling back to legacy: {e}" + ) + return False + + +use_modern_sqlalchemy_session_manager = _use_modern_sqlalchemy_session_manager() + + +def get_db_session(path: str, auto_close: Optional[bool] = True) -> DbSession: + engine = create_engine(f"sqlite:///{path}") + Session = sessionmaker() + Session.configure(bind=engine) + session = Session() + if not auto_close or use_modern_sqlalchemy_session_manager: + return session + else: + return LegacySessionManager(session) + + +# table definitions for many-to-many joins +# (we're not creating models for these tables since they can be manipulated through each side of the join) + +assets_chunks = Table( + "assets_chunks", + Base.metadata, + Column("asset_id", ForeignKey("assets.id")), + Column("chunk_id", ForeignKey("chunks.id")), +) + +chunks_modules = Table( + "chunks_modules", + Base.metadata, + Column("chunk_id", ForeignKey("chunks.id")), + Column("module_id", ForeignKey("modules.id")), +) + +# model definitions + + +class Bundle(Base): + """ + A bundle is a top-level wrapper of various assets. A large application + may have multiple bundles being built and we'd like to track all of them + separately. + """ + + __tablename__ = "bundles" + + id = Column(types.Integer, primary_key=True) + name = Column(types.Text, nullable=False) + is_cached = Column(types.Boolean, nullable=False, default=False) + + +class Session(Base): + """ + A session represents a single bundle stats file that we ingest. + Multiple sessions are combined into a single database to form a full + bundle report. + """ + + __tablename__ = "sessions" + + id = Column(types.Integer, primary_key=True) + bundle_id = Column(types.Integer, ForeignKey("bundles.id"), nullable=False) + info = Column(types.JSON) + + bundle = relationship("Bundle", backref=backref("sessions")) + + +class Metadata(Base): + """ + Metadata about the bundle report. + """ + + __tablename__ = "metadata" + + key = Column(types.Text, primary_key=True) + value = Column(types.JSON) + + +class AssetType(Enum): + JAVASCRIPT = "javascript" + STYLESHEET = "stylesheet" + FONT = "font" + IMAGE = "image" + UNKNOWN = "unknown" + + +class Asset(Base): + """ + These are the top-level artifacts that the bundling process produces. + """ + + __tablename__ = "assets" + + id = Column(types.Integer, primary_key=True) + session_id = Column(types.Integer, ForeignKey("sessions.id"), nullable=False) + name = Column(types.Text, nullable=False) + normalized_name = Column(types.Text, nullable=False) + size = Column(types.Integer, nullable=False) + gzip_size = Column(types.Integer) + uuid = Column(types.Text, nullable=False) + asset_type = Column(SQLAlchemyEnum(AssetType)) + session = relationship("Session", backref=backref("assets")) + chunks = relationship( + "Chunk", secondary=assets_chunks, back_populates="assets", cascade="all, delete" + ) + + def __repr__(self): + """Returns a string representation of the Asset with key information""" + return f"Asset(name='{self.name}', type={self.asset_type}, size={self.size})" + + +class Chunk(Base): + """ + These are an intermediate form that I don't totally understand yet. + """ + + __tablename__ = "chunks" + + id = Column(types.Integer, primary_key=True) + session_id = Column(types.Integer, ForeignKey("sessions.id"), nullable=False) + external_id = Column(types.Text, nullable=False) + unique_external_id = Column(types.Text, nullable=False) + entry = Column(types.Boolean, nullable=False) + initial = Column(types.Boolean, nullable=False) + + session = relationship("Session", backref=backref("chunks")) + assets = relationship("Asset", secondary=assets_chunks, back_populates="chunks") + modules = relationship( + "Module", + secondary=chunks_modules, + back_populates="chunks", + cascade="all, delete", + ) + + +class Module(Base): + """ + These are the constituent modules that comprise an asset. + """ + + __tablename__ = "modules" + + id = Column(types.Integer, primary_key=True) + session_id = Column(types.Integer, ForeignKey("sessions.id"), nullable=False) + name = Column(types.Text, nullable=False) + size = Column(types.Integer, nullable=False) + + session = relationship("Session", backref=backref("modules")) + chunks = relationship( + "Chunk", + secondary=chunks_modules, + back_populates="modules", + ) + + +class DynamicImport(Base): + """ + These represents a mapping of each chunk's dynamically imported assets + """ + + __tablename__ = "dynamic_imports" + + chunk_id = Column(types.Integer, ForeignKey("chunks.id"), primary_key=True) + asset_id = Column(types.Integer, ForeignKey("assets.id"), primary_key=True) + + # Relationships + chunk = relationship( + "Chunk", backref=backref("dynamic_imports", cascade="all, delete-orphan") + ) + asset = relationship( + "Asset", backref=backref("dynamic_imports", cascade="all, delete-orphan") + ) + + +class MetadataKey(Enum): + SCHEMA_VERSION = "schema_version" + COMPARE_SHA = "compare_sha" diff --git a/libs/shared/shared/bundle_analysis/parser.py b/libs/shared/shared/bundle_analysis/parser.py new file mode 100644 index 0000000000..7feef8fc1d --- /dev/null +++ b/libs/shared/shared/bundle_analysis/parser.py @@ -0,0 +1,47 @@ +import logging + +import ijson +from sqlalchemy.orm import Session as DbSession + +from shared.bundle_analysis.parsers import ParserInterface, ParserV1, ParserV2, ParserV3 +from shared.bundle_analysis.parsers.base import ParserTrait + +log = logging.getLogger(__name__) + + +PARSER_VERSION_MAPPING: dict[str, type[ParserTrait]] = { + "1": ParserV1, + "2": ParserV2, + "3": ParserV3, +} + + +class Parser: + """ + # Retrieve bundle stats file version and return an associated instance of its parser + """ + + def __init__(self, path: str, db_session: DbSession): + self.path = path + self.db_session = db_session + + def get_proper_parser(self) -> ParserTrait: + error = None + try: + with open(self.path, "rb") as f: + for event in ijson.parse(f): + prefix, _, value = event + if prefix == "version": + selected_parser = PARSER_VERSION_MAPPING.get(value) + if selected_parser is None: + error = f"parser not implemented for version {value}" + elif not issubclass(selected_parser, ParserInterface): + error = "invalid parser implementation" + else: + return selected_parser(self.db_session) + error = "version does not exist in bundle file" + except IOError: + error = "unable to open file" + if error: + raise Exception(f"Couldn't parse bundle: {error}") + raise Exception("Couldn't parse bundle: unknown error") diff --git a/libs/shared/shared/bundle_analysis/parsers/__init__.py b/libs/shared/shared/bundle_analysis/parsers/__init__.py new file mode 100644 index 0000000000..98c77c4d50 --- /dev/null +++ b/libs/shared/shared/bundle_analysis/parsers/__init__.py @@ -0,0 +1,11 @@ +from shared.bundle_analysis.parsers.base import ParserInterface +from shared.bundle_analysis.parsers.v1 import ParserV1 +from shared.bundle_analysis.parsers.v2 import ParserV2 +from shared.bundle_analysis.parsers.v3 import ParserV3 + +__all__ = [ + "ParserInterface", + "ParserV1", + "ParserV2", + "ParserV3", +] diff --git a/libs/shared/shared/bundle_analysis/parsers/base.py b/libs/shared/shared/bundle_analysis/parsers/base.py new file mode 100644 index 0000000000..13f31bfc14 --- /dev/null +++ b/libs/shared/shared/bundle_analysis/parsers/base.py @@ -0,0 +1,20 @@ +import abc +from typing import Tuple + +from sqlalchemy.orm import Session + + +class ParserInterface(metaclass=abc.ABCMeta): + @classmethod + def __subclasshook__(cls, subclass): + return hasattr(subclass, "parse") and callable(subclass.parse) + + +class ParserTrait: + @abc.abstractmethod + def __init__(self, db_session: Session): + pass + + @abc.abstractmethod + def parse(self, path: str) -> Tuple[int, str]: + pass diff --git a/libs/shared/shared/bundle_analysis/parsers/v1.py b/libs/shared/shared/bundle_analysis/parsers/v1.py new file mode 100644 index 0000000000..ae85ac6ba6 --- /dev/null +++ b/libs/shared/shared/bundle_analysis/parsers/v1.py @@ -0,0 +1,376 @@ +import json +import logging +import re +import uuid +from typing import Tuple + +import ijson +import sentry_sdk +from sqlalchemy.orm import Session as DbSession + +from shared.bundle_analysis.models import ( + Asset, + AssetType, + Bundle, + Chunk, + Module, + Session, + assets_chunks, + chunks_modules, +) +from shared.bundle_analysis.parsers.base import ParserTrait +from shared.bundle_analysis.utils import get_extension + +log = logging.getLogger(__name__) + + +""" +Version 1 Schema +{ + "version": "1", + "plugin": { + "name": str + "version": str + }, + "builtAt": int, + "duration": int, + "bundler": { "name": str, "version": str }, + "bundleName": str, + "assets": [{ + "name": str", + "size": int, + "normalized": str + }], + "chunks": [{ + "id": str, + "uniqueId": str, + "entry": bool, + "initial": bool, + "files": [str], + "names": [str] + }], + "modules": [{ + "name": str, + "size": int, + "chunkUniqueIds": [str] + }] +} +""" + + +class ParserV1(ParserTrait): + """ + This does a streaming JSON parse of the stats JSON file referenced by `path`. + It's more complicated that just doing a `json.loads` but should keep our memory + usage constrained. + """ + + def __init__(self, db_session: DbSession): + self.db_session = db_session + + def reset(self): + """ + Resets temporary parser state in order to parse a new file path. + """ + # chunk unique id -> asset name list + self.chunk_asset_names_index = {} + + # module name -> chunk external id list + self.module_chunk_unique_external_ids_index = {} + + # misc. top-level info from the stats data (i.e. bundler version, bundle time, etc.) + self.info = {} + + # temporary parser state + self.session = None + self.asset = None + self.chunk = None + self.chunk_asset_names = [] + self.module = None + self.module_chunk_unique_external_ids = [] + + self.asset_list = [] + self.chunk_list = [] + self.module_list = [] + + @sentry_sdk.trace + def parse(self, path: str) -> Tuple[int, str]: + try: + self.reset() + + # Retrieve the info section first before parsing all the other things + # this way when an error is raised we know which bundle plugin caused it + with open(path, "rb") as f: + for event in ijson.parse(f): + self._parse_info(event) + + self.session = Session(info={}) + self.db_session.add(self.session) + self.db_session.flush() + + with open(path, "rb") as f: + for event in ijson.parse(f): + self._parse_event(event) + + if self.asset_list: + insert_asset = Asset.__table__.insert().values(self.asset_list) + self.db_session.execute(insert_asset) + + if self.chunk_list: + insert_chunks = Chunk.__table__.insert().values(self.chunk_list) + self.db_session.execute(insert_chunks) + + if self.module_list: + insert_modules = Module.__table__.insert().values(self.module_list) + self.db_session.execute(insert_modules) + + self.db_session.flush() + + # Delete old session/asset/chunk/module with the same bundle name if applicable + old_session = ( + self.db_session.query(Session) + .filter( + Session.bundle == self.session.bundle, + Session.id != self.session.id, + ) + .one_or_none() + ) + if old_session: + for model in [Asset, Chunk, Module]: + to_be_deleted = self.db_session.query(model).filter( + model.session == old_session + ) + for item in to_be_deleted: + self.db_session.delete(item) + self.db_session.flush() + self.db_session.delete(old_session) + self.db_session.flush() + + # save top level bundle stats info + self.session.info = json.dumps(self.info) + + # this happens last so that we could potentially handle any ordering + # of top-level keys inside the JSON (i.e. we couldn't associate a chunk + # to an asset above if we parse the chunk before the asset) + self._create_associations() + + assert self.session.bundle is not None + return self.session.id, self.session.bundle.name + except Exception as e: + # Inject the plugin name to the Exception object so we have visibilitity on which plugin + # is causing the trouble. + e.bundle_analysis_plugin_name = self.info.get("plugin_name", "unknown") + raise e + + def _asset_type(self, name: str) -> AssetType: + extension = get_extension(name) + + if extension in ["js", "mjs", "cjs"]: + return AssetType.JAVASCRIPT + if extension in ["css"]: + return AssetType.STYLESHEET + if extension in ["woff", "woff2", "ttf", "otf", "eot"]: + return AssetType.FONT + if extension in ["jpg", "jpeg", "png", "gif", "svg", "webp", "apng", "avif"]: + return AssetType.IMAGE + + return AssetType.UNKNOWN + + def _parse_info(self, event: Tuple[str, str, str]): + prefix, _, value = event + + # session info + if prefix == "version": + self.info["version"] = value + elif prefix == "bundler.name": + self.info["bundler_name"] = value + elif prefix == "bundler.version": + self.info["bundler_version"] = value + elif prefix == "builtAt": + self.info["built_at"] = value + elif prefix == "plugin.name": + self.info["plugin_name"] = value + elif prefix == "plugin.version": + self.info["plugin_version"] = value + elif prefix == "duration": + self.info["duration"] = value + + def _parse_event(self, event: Tuple[str, str, str]): + prefix, _, value = event + prefix_path = prefix.split(".") + + # asset / chunks / modules + if prefix_path[0] == "assets": + self._parse_assets_event(*event) + elif prefix_path[0] == "chunks": + self._parse_chunks_event(*event) + elif prefix_path[0] == "modules": + self._parse_modules_event(*event) + + # bundle name + elif prefix == "bundleName": + if not re.fullmatch(r"^[\w\d_:/@\.{}\[\]$-]+$", value): + log.info(f'bundle name does not match regex: "{value}"') + raise Exception("invalid bundle name") + bundle = self.db_session.query(Bundle).filter_by(name=value).first() + if bundle is None: + bundle = Bundle(name=value) + self.db_session.add(bundle) + bundle.is_cached = False + self.session.bundle = bundle + + def _parse_assets_event(self, prefix: str, event: str, value: str): + if (prefix, event) == ("assets.item", "start_map"): + # new asset + assert self.asset is None + self.asset = Asset(session_id=self.session.id) + elif prefix == "assets.item.name": + self.asset.name = value + elif prefix == "assets.item.normalized": + self.asset.normalized_name = value + elif prefix == "assets.item.size": + self.asset.size = int(value) + elif (prefix, event) == ("assets.item", "end_map"): + self.asset_list.append( + dict( + session_id=self.asset.session_id, + name=self.asset.name, + normalized_name=self.asset.normalized_name, + size=self.asset.size, + gzip_size=self.asset.size // 1000, + uuid=str(uuid.uuid4()), + asset_type=self._asset_type(self.asset.name), + ) + ) + + # reset parser state + self.asset = None + + def _parse_chunks_event(self, prefix: str, event: str, value: str): + if (prefix, event) == ("chunks.item", "start_map"): + # new chunk + assert self.chunk is None + self.chunk = Chunk(session_id=self.session.id) + elif prefix == "chunks.item.id": + self.chunk.external_id = value + elif prefix == "chunks.item.uniqueId": + self.chunk.unique_external_id = value + elif prefix == "chunks.item.initial": + self.chunk.initial = value + elif prefix == "chunks.item.entry": + self.chunk.entry = value + elif prefix == "chunks.item.files.item": + self.chunk_asset_names.append(value) + elif (prefix, event) == ("chunks.item", "end_map"): + self.chunk_list.append( + dict( + session_id=self.chunk.session_id, + external_id=self.chunk.external_id, + unique_external_id=self.chunk.unique_external_id, + initial=self.chunk.initial, + entry=self.chunk.entry, + ) + ) + + self.chunk_asset_names_index[self.chunk.unique_external_id] = ( + self.chunk_asset_names + ) + # reset parser state + self.chunk = None + self.chunk_asset_names = [] + + def _parse_modules_event(self, prefix: str, event: str, value: str): + if (prefix, event) == ("modules.item", "start_map"): + # new module + assert self.module is None + self.module = Module(session_id=self.session.id) + elif prefix == "modules.item.name": + self.module.name = value + elif prefix == "modules.item.size": + self.module.size = int(value) + elif prefix == "modules.item.chunkUniqueIds.item": + self.module_chunk_unique_external_ids.append(value) + elif (prefix, event) == ("modules.item", "end_map"): + self.module_list.append( + dict( + session_id=self.module.session_id, + name=self.module.name, + size=self.module.size, + ) + ) + + self.module_chunk_unique_external_ids_index[self.module.name] = ( + self.module_chunk_unique_external_ids + ) + # reset parser state + self.module = None + self.module_chunk_unique_external_ids = [] + + def _create_associations(self): + # associate chunks to assets + inserts = [] + assets: list[Asset] = ( + self.db_session.query(Asset) + .filter( + Asset.session_id == self.session.id, + ) + .all() + ) + + asset_name_to_id = {asset.name: asset.id for asset in assets} + + chunks: list[Chunk] = ( + self.db_session.query(Chunk) + .filter( + Chunk.session_id == self.session.id, + ) + .all() + ) + + chunk_unique_id_to_id = {chunk.unique_external_id: chunk.id for chunk in chunks} + + modules = ( + self.db_session.query(Module) + .filter( + Module.session_id == self.session.id, + ) + .all() + ) + + for chunk in chunks: + chunk_id = chunk.id + asset_names = self.chunk_asset_names_index[chunk.unique_external_id] + inserts.extend( + [ + dict(asset_id=asset_name_to_id.get(asset_name), chunk_id=chunk_id) + for asset_name in asset_names + if asset_name_to_id.get(asset_name) is not None + ] + ) + if inserts: + self.db_session.execute(assets_chunks.insert(), inserts) + + # associate modules to chunks + # FIXME: this isn't quite right - need to sort out how non-JS assets reference chunks + inserts = [] + + modules: list[Module] = self.db_session.query(Module).filter( + Module.session_id == self.session.id, + ) + for module in modules: + module_id = module.id + chunk_unique_external_ids = self.module_chunk_unique_external_ids_index[ + module.name + ] + + inserts.extend( + [ + dict( + chunk_id=chunk_unique_id_to_id[unique_external_id], + module_id=module_id, + ) + for unique_external_id in chunk_unique_external_ids + ] + ) + if inserts: + self.db_session.execute(chunks_modules.insert(), inserts) diff --git a/libs/shared/shared/bundle_analysis/parsers/v2.py b/libs/shared/shared/bundle_analysis/parsers/v2.py new file mode 100644 index 0000000000..0c6f23bff4 --- /dev/null +++ b/libs/shared/shared/bundle_analysis/parsers/v2.py @@ -0,0 +1,379 @@ +import json +import logging +import re +import uuid +from typing import Tuple + +import ijson +import sentry_sdk +from sqlalchemy.orm import Session as DbSession + +from shared.bundle_analysis.models import ( + Asset, + AssetType, + Bundle, + Chunk, + Module, + Session, + assets_chunks, + chunks_modules, +) +from shared.bundle_analysis.parsers.base import ParserTrait +from shared.bundle_analysis.utils import get_extension + +log = logging.getLogger(__name__) + + +""" +Version 2 Schema +{ + "version": "2", + "plugin": { + "name": str + "version": str + }, + "builtAt": int, + "duration": int, + "bundler": { "name": str, "version": str }, + "bundleName": str, + "assets": [{ + "name": str", + "size": int, + "gzipSize": int, + "normalized": str + }], + "chunks": [{ + "id": str, + "uniqueId": str, + "entry": bool, + "initial": bool, + "files": [str], + "names": [str] + }], + "modules": [{ + "name": str, + "size": int, + "chunkUniqueIds": [str] + }] +} +""" + + +class ParserV2(ParserTrait): + """ + This does a streaming JSON parse of the stats JSON file referenced by `path`. + It's more complicated that just doing a `json.loads` but should keep our memory + usage constrained. + """ + + def __init__(self, db_session: DbSession): + self.db_session = db_session + + def reset(self): + """ + Resets temporary parser state in order to parse a new file path. + """ + # chunk unique id -> asset name list + self.chunk_asset_names_index = {} + + # module name -> chunk external id list + self.module_chunk_unique_external_ids_index = {} + + # misc. top-level info from the stats data (i.e. bundler version, bundle time, etc.) + self.info = {} + + # temporary parser state + self.session = None + self.asset = None + self.chunk = None + self.chunk_asset_names = [] + self.module = None + self.module_chunk_unique_external_ids = [] + + self.asset_list = [] + self.chunk_list = [] + self.module_list = [] + + @sentry_sdk.trace + def parse(self, path: str) -> Tuple[int, str]: + try: + self.reset() + + # Retrieve the info section first before parsing all the other things + # this way when an error is raised we know which bundle plugin caused it + with open(path, "rb") as f: + for event in ijson.parse(f): + self._parse_info(event) + + self.session = Session(info={}) + self.db_session.add(self.session) + self.db_session.flush() + + with open(path, "rb") as f: + for event in ijson.parse(f): + self._parse_event(event) + + # Delete old session/asset/chunk/module with the same bundle name if applicable + old_session = ( + self.db_session.query(Session) + .filter( + Session.bundle == self.session.bundle, + Session.id != self.session.id, + ) + .one_or_none() + ) + if old_session: + for model in [Asset, Chunk, Module]: + to_be_deleted = self.db_session.query(model).filter( + model.session == old_session + ) + for item in to_be_deleted: + self.db_session.delete(item) + self.db_session.flush() + self.db_session.delete(old_session) + self.db_session.flush() + + if self.asset_list: + insert_asset = Asset.__table__.insert().values(self.asset_list) + self.db_session.execute(insert_asset) + + if self.chunk_list: + insert_chunks = Chunk.__table__.insert().values(self.chunk_list) + self.db_session.execute(insert_chunks) + + if self.module_list: + insert_modules = Module.__table__.insert().values(self.module_list) + self.db_session.execute(insert_modules) + + self.db_session.flush() + + # save top level bundle stats info + self.session.info = json.dumps(self.info) + + # this happens last so that we could potentially handle any ordering + # of top-level keys inside the JSON (i.e. we couldn't associate a chunk + # to an asset above if we parse the chunk before the asset) + self._create_associations() + + assert self.session.bundle is not None + return self.session.id, self.session.bundle.name + except Exception as e: + # Inject the plugin name to the Exception object so we have visibilitity on which plugin + # is causing the trouble. + e.bundle_analysis_plugin_name = self.info.get("plugin_name", "unknown") + raise e + + def _asset_type(self, name: str) -> AssetType: + extension = get_extension(name) + + if extension in ["js", "mjs", "cjs"]: + return AssetType.JAVASCRIPT + if extension in ["css"]: + return AssetType.STYLESHEET + if extension in ["woff", "woff2", "ttf", "otf", "eot"]: + return AssetType.FONT + if extension in ["jpg", "jpeg", "png", "gif", "svg", "webp", "apng", "avif"]: + return AssetType.IMAGE + + return AssetType.UNKNOWN + + def _parse_info(self, event: Tuple[str, str, str]): + prefix, _, value = event + + # session info + if prefix == "version": + self.info["version"] = value + elif prefix == "bundler.name": + self.info["bundler_name"] = value + elif prefix == "bundler.version": + self.info["bundler_version"] = value + elif prefix == "builtAt": + self.info["built_at"] = value + elif prefix == "plugin.name": + self.info["plugin_name"] = value + elif prefix == "plugin.version": + self.info["plugin_version"] = value + elif prefix == "duration": + self.info["duration"] = value + + def _parse_event(self, event: Tuple[str, str, str]): + prefix, _, value = event + prefix_path = prefix.split(".") + + # asset / chunks / modules + if prefix_path[0] == "assets": + self._parse_assets_event(*event) + elif prefix_path[0] == "chunks": + self._parse_chunks_event(*event) + elif prefix_path[0] == "modules": + self._parse_modules_event(*event) + + # bundle name + elif prefix == "bundleName": + if not re.fullmatch(r"^[\w\d_:/@\.{}\[\]$-]+$", value): + log.info(f'bundle name does not match regex: "{value}"') + raise Exception("invalid bundle name") + bundle = self.db_session.query(Bundle).filter_by(name=value).first() + if bundle is None: + bundle = Bundle(name=value) + self.db_session.add(bundle) + bundle.is_cached = False + self.session.bundle = bundle + + def _parse_assets_event(self, prefix: str, event: str, value: str): + if (prefix, event) == ("assets.item", "start_map"): + # new asset + assert self.asset is None + self.asset = Asset(session_id=self.session.id) + elif prefix == "assets.item.name": + self.asset.name = value + elif prefix == "assets.item.normalized": + self.asset.normalized_name = value + elif prefix == "assets.item.size": + self.asset.size = int(value) + elif prefix == "assets.item.gzipSize" and value is not None: + self.asset.gzip_size = int(value) + elif (prefix, event) == ("assets.item", "end_map"): + self.asset_list.append( + dict( + session_id=self.asset.session_id, + name=self.asset.name, + normalized_name=self.asset.normalized_name, + size=self.asset.size, + gzip_size=self.asset.gzip_size, + uuid=str(uuid.uuid4()), + asset_type=self._asset_type(self.asset.name), + ) + ) + + # reset parser state + self.asset = None + + def _parse_chunks_event(self, prefix: str, event: str, value: str): + if (prefix, event) == ("chunks.item", "start_map"): + # new chunk + assert self.chunk is None + self.chunk = Chunk(session_id=self.session.id) + elif prefix == "chunks.item.id": + self.chunk.external_id = value + elif prefix == "chunks.item.uniqueId": + self.chunk.unique_external_id = value + elif prefix == "chunks.item.initial": + self.chunk.initial = value + elif prefix == "chunks.item.entry": + self.chunk.entry = value + elif prefix == "chunks.item.files.item": + self.chunk_asset_names.append(value) + elif (prefix, event) == ("chunks.item", "end_map"): + self.chunk_list.append( + dict( + session_id=self.chunk.session_id, + external_id=self.chunk.external_id, + unique_external_id=self.chunk.unique_external_id, + initial=self.chunk.initial, + entry=self.chunk.entry, + ) + ) + + self.chunk_asset_names_index[self.chunk.unique_external_id] = ( + self.chunk_asset_names + ) + # reset parser state + self.chunk = None + self.chunk_asset_names = [] + + def _parse_modules_event(self, prefix: str, event: str, value: str): + if (prefix, event) == ("modules.item", "start_map"): + # new module + assert self.module is None + self.module = Module(session_id=self.session.id) + elif prefix == "modules.item.name": + self.module.name = value + elif prefix == "modules.item.size": + self.module.size = int(value) + elif prefix == "modules.item.chunkUniqueIds.item": + self.module_chunk_unique_external_ids.append(value) + elif (prefix, event) == ("modules.item", "end_map"): + self.module_list.append( + dict( + session_id=self.module.session_id, + name=self.module.name, + size=self.module.size, + ) + ) + + self.module_chunk_unique_external_ids_index[self.module.name] = ( + self.module_chunk_unique_external_ids + ) + # reset parser state + self.module = None + self.module_chunk_unique_external_ids = [] + + def _create_associations(self): + # associate chunks to assets + inserts = [] + assets: list[Asset] = ( + self.db_session.query(Asset) + .filter( + Asset.session_id == self.session.id, + ) + .all() + ) + + asset_name_to_id = {asset.name: asset.id for asset in assets} + + chunks: list[Chunk] = ( + self.db_session.query(Chunk) + .filter( + Chunk.session_id == self.session.id, + ) + .all() + ) + + chunk_unique_id_to_id = {chunk.unique_external_id: chunk.id for chunk in chunks} + + modules = ( + self.db_session.query(Module) + .filter( + Module.session_id == self.session.id, + ) + .all() + ) + + for chunk in chunks: + chunk_id = chunk.id + asset_names = self.chunk_asset_names_index[chunk.unique_external_id] + inserts.extend( + [ + dict(asset_id=asset_name_to_id.get(asset_name), chunk_id=chunk_id) + for asset_name in asset_names + if asset_name_to_id.get(asset_name) is not None + ] + ) + if inserts: + self.db_session.execute(assets_chunks.insert(), inserts) + + # associate modules to chunks + # FIXME: this isn't quite right - need to sort out how non-JS assets reference chunks + inserts = [] + + modules: list[Module] = self.db_session.query(Module).filter( + Module.session_id == self.session.id, + ) + for module in modules: + module_id = module.id + chunk_unique_external_ids = self.module_chunk_unique_external_ids_index[ + module.name + ] + + inserts.extend( + [ + dict( + chunk_id=chunk_unique_id_to_id[unique_external_id], + module_id=module_id, + ) + for unique_external_id in chunk_unique_external_ids + ] + ) + if inserts: + self.db_session.execute(chunks_modules.insert(), inserts) diff --git a/libs/shared/shared/bundle_analysis/parsers/v3.py b/libs/shared/shared/bundle_analysis/parsers/v3.py new file mode 100644 index 0000000000..c7c7a195f4 --- /dev/null +++ b/libs/shared/shared/bundle_analysis/parsers/v3.py @@ -0,0 +1,451 @@ +import json +import logging +import re +import uuid +from collections import defaultdict +from typing import Dict, List, Tuple + +import ijson +import sentry_sdk +from sqlalchemy import tuple_ +from sqlalchemy.orm import Session as DbSession +from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound + +from shared.bundle_analysis.models import ( + Asset, + AssetType, + Bundle, + Chunk, + DynamicImport, + Module, + Session, + assets_chunks, + chunks_modules, +) +from shared.bundle_analysis.parsers.base import ParserTrait +from shared.bundle_analysis.utils import get_extension + +log = logging.getLogger(__name__) + + +""" +Version 3 Schema +{ + "version": "3", + "plugin": { + "name": str + "version": str + }, + "builtAt": int, + "duration": int, + "bundler": { "name": str, "version": str }, + "bundleName": str, + "assets": [{ + "name": str", + "size": int, + "gzipSize": int, + "normalized": str + }], + "chunks": [{ + "id": str, + "uniqueId": str, + "entry": bool, + "initial": bool, + "files": [str], + "names": [str], + "dynamicImports": [str] + }], + "modules": [{ + "name": str, + "size": int, + "chunkUniqueIds": [str] + }] +} +""" + + +class ParserV3(ParserTrait): + """ + This does a streaming JSON parse of the stats JSON file referenced by `path`. + It's more complicated that just doing a `json.loads` but should keep our memory + usage constrained. + """ + + def __init__(self, db_session: DbSession): + self.db_session = db_session + + def reset(self): + """ + Resets temporary parser state in order to parse a new file path. + """ + # chunk unique id -> asset name list + self.chunk_asset_names_index = {} + + # module name -> chunk external id list + self.module_chunk_unique_external_ids_index = {} + + # misc. top-level info from the stats data (i.e. bundler version, bundle time, etc.) + self.info = {} + + # temporary parser state + self.session = None + self.asset = None + self.chunk = None + self.chunk_asset_names = [] + self.module = None + self.module_chunk_unique_external_ids = [] + + self.asset_list = [] + self.chunk_list = [] + self.module_list = [] + + # dynamic imports: mapping between Chunk and each file name of its dynamic imports + self.dynamic_import_file_names_by_chunk = defaultdict( + list + ) # typing: Dict[Chunk, List[str]] + + @sentry_sdk.trace + def parse(self, path: str) -> Tuple[int, str]: + try: + self.reset() + + # Retrieve the info section first before parsing all the other things + # this way when an error is raised we know which bundle plugin caused it + with open(path, "rb") as f: + for event in ijson.parse(f): + self._parse_info(event) + + self.session = Session(info={}) + self.db_session.add(self.session) + self.db_session.flush() + + with open(path, "rb") as f: + for event in ijson.parse(f): + self._parse_event(event) + + # Delete old session/asset/chunk/module with the same bundle name if applicable + old_session = ( + self.db_session.query(Session) + .filter( + Session.bundle == self.session.bundle, + Session.id != self.session.id, + ) + .one_or_none() + ) + if old_session: + for model in [Asset, Chunk, Module]: + to_be_deleted = self.db_session.query(model).filter( + model.session == old_session + ) + for item in to_be_deleted: + self.db_session.delete(item) + self.db_session.flush() + self.db_session.delete(old_session) + self.db_session.flush() + + if self.asset_list: + insert_asset = Asset.__table__.insert().values(self.asset_list) + self.db_session.execute(insert_asset) + + # Needs to use ORM-style insert to update the models since they + # will be used later in dynamic import processing + self.db_session.add_all(self.chunk_list) + + if self.module_list: + insert_modules = Module.__table__.insert().values(self.module_list) + self.db_session.execute(insert_modules) + + self.db_session.flush() + + # Insert into dynamic imports table the Chunk.id and Asset.id + # but first we need to find the Asset by the hashed file name + dynamic_imports_list = self._parse_dynamic_imports() + if dynamic_imports_list: + self.db_session.execute( + DynamicImport.__table__.delete().where( + tuple_(DynamicImport.chunk_id, DynamicImport.asset_id).in_( + [ + (item["chunk_id"], item["asset_id"]) + for item in dynamic_imports_list + ] + ) + ) + ) + insert_dynamic_imports = DynamicImport.__table__.insert().values( + dynamic_imports_list + ) + self.db_session.execute(insert_dynamic_imports) + self.db_session.flush() + + # save top level bundle stats info + self.session.info = json.dumps(self.info) + + # this happens last so that we could potentially handle any ordering + # of top-level keys inside the JSON (i.e. we couldn't associate a chunk + # to an asset above if we parse the chunk before the asset) + self._create_associations() + + assert self.session.bundle is not None + return self.session.id, self.session.bundle.name + except Exception as e: + # Inject the plugin name to the Exception object so we have visibility on which plugin + # is causing the trouble. + e.bundle_analysis_plugin_name = self.info.get("plugin_name", "unknown") + raise e + + def _asset_type(self, name: str) -> AssetType: + extension = get_extension(name) + + if extension in ["js", "mjs", "cjs"]: + return AssetType.JAVASCRIPT + if extension in ["css"]: + return AssetType.STYLESHEET + if extension in ["woff", "woff2", "ttf", "otf", "eot"]: + return AssetType.FONT + if extension in ["jpg", "jpeg", "png", "gif", "svg", "webp", "apng", "avif"]: + return AssetType.IMAGE + + return AssetType.UNKNOWN + + def _parse_info(self, event: Tuple[str, str, str]): + prefix, _, value = event + + # session info + if prefix == "version": + self.info["version"] = value + elif prefix == "bundler.name": + self.info["bundler_name"] = value + elif prefix == "bundler.version": + self.info["bundler_version"] = value + elif prefix == "builtAt": + self.info["built_at"] = value + elif prefix == "plugin.name": + self.info["plugin_name"] = value + elif prefix == "plugin.version": + self.info["plugin_version"] = value + elif prefix == "duration": + self.info["duration"] = value + + def _parse_event(self, event: Tuple[str, str, str]): + prefix, _, value = event + prefix_path = prefix.split(".") + + # asset / chunks / modules + if prefix_path[0] == "assets": + self._parse_assets_event(*event) + elif prefix_path[0] == "chunks": + self._parse_chunks_event(*event) + elif prefix_path[0] == "modules": + self._parse_modules_event(*event) + + # bundle name + elif prefix == "bundleName": + if not re.fullmatch(r"^[\w\d_:/@\.{}\[\]$-]+$", value): + log.info(f'bundle name does not match regex: "{value}"') + raise Exception("invalid bundle name") + bundle = self.db_session.query(Bundle).filter_by(name=value).first() + if bundle is None: + bundle = Bundle(name=value) + self.db_session.add(bundle) + bundle.is_cached = False + self.session.bundle = bundle + + def _parse_assets_event(self, prefix: str, event: str, value: str): + if (prefix, event) == ("assets.item", "start_map"): + # new asset + assert self.asset is None + self.asset = Asset(session_id=self.session.id) + elif prefix == "assets.item.name": + self.asset.name = value + elif prefix == "assets.item.normalized": + self.asset.normalized_name = value + elif prefix == "assets.item.size": + self.asset.size = int(value) + elif prefix == "assets.item.gzipSize" and value is not None: + self.asset.gzip_size = int(value) + elif (prefix, event) == ("assets.item", "end_map"): + self.asset_list.append( + dict( + session_id=self.asset.session_id, + name=self.asset.name, + normalized_name=self.asset.normalized_name, + size=self.asset.size, + gzip_size=self.asset.gzip_size, + uuid=str(uuid.uuid4()), + asset_type=self._asset_type(self.asset.name), + ) + ) + + # reset parser state + self.asset = None + + def _parse_chunks_event(self, prefix: str, event: str, value: str): + if (prefix, event) == ("chunks.item", "start_map"): + # new chunk + assert self.chunk is None + self.chunk = Chunk(session_id=self.session.id) + elif prefix == "chunks.item.id": + self.chunk.external_id = value + elif prefix == "chunks.item.uniqueId": + self.chunk.unique_external_id = value + elif prefix == "chunks.item.initial": + self.chunk.initial = value + elif prefix == "chunks.item.entry": + self.chunk.entry = value + elif prefix == "chunks.item.files.item": + self.chunk_asset_names.append(value) + elif prefix == "chunks.item.dynamicImports.item": + self.dynamic_import_file_names_by_chunk[self.chunk].append(value) + elif (prefix, event) == ("chunks.item", "end_map"): + self.chunk_list.append(self.chunk) + + self.chunk_asset_names_index[self.chunk.unique_external_id] = ( + self.chunk_asset_names + ) + # reset parser state + self.chunk = None + self.chunk_asset_names = [] + + def _parse_modules_event(self, prefix: str, event: str, value: str): + if (prefix, event) == ("modules.item", "start_map"): + # new module + assert self.module is None + self.module = Module(session_id=self.session.id) + elif prefix == "modules.item.name": + self.module.name = value + elif prefix == "modules.item.size": + self.module.size = int(value) + elif prefix == "modules.item.chunkUniqueIds.item": + self.module_chunk_unique_external_ids.append(value) + elif (prefix, event) == ("modules.item", "end_map"): + self.module_list.append( + dict( + session_id=self.module.session_id, + name=self.module.name, + size=self.module.size, + ) + ) + + self.module_chunk_unique_external_ids_index[self.module.name] = ( + self.module_chunk_unique_external_ids + ) + # reset parser state + self.module = None + self.module_chunk_unique_external_ids = [] + + def _parse_dynamic_imports(self) -> List[Dict[str, int]]: + """ + Computes all the dynamic imports that needs to be inserted to the DB + Returns a list of dictionary objects representing the insert params + [{ + "chunk_id": chunk.id, + "asset_id": asset.id, + }] + """ + dynamic_imports_list = [] + for chunk, filenames in self.dynamic_import_file_names_by_chunk.items(): + imported_assets = {} + for filename in filenames: + try: + asset = ( + self.db_session.query(Asset) + .join(Asset.session) # Join Asset to Session + .join(Session.bundle) # Join Session to Bundle + .filter( + Asset.name == filename, + Bundle.name == self.session.bundle.name, + ) + .one() + ) + imported_assets[filename] = asset + except NoResultFound: + # TODO: Ignore this behavior for now, we'll handle it in the future + # https://github.com/codecov/engineering-team/issues/3512 + log.warn( + f'Asset not found for dynamic import: "{filename}". Skipping...', + ) + except MultipleResultsFound: + log.error( + f'Multiple assets found for dynamic import: "{filename}"', + exc_info=True, + ) + raise + + dynamic_imports_list.extend( + [ + dict(chunk_id=chunk.id, asset_id=asset.id) + for asset in imported_assets.values() + ] + ) + + return dynamic_imports_list + + def _create_associations(self): + # associate chunks to assets + inserts = [] + assets: list[Asset] = ( + self.db_session.query(Asset) + .filter( + Asset.session_id == self.session.id, + ) + .all() + ) + + asset_name_to_id = {asset.name: asset.id for asset in assets} + + chunks: list[Chunk] = ( + self.db_session.query(Chunk) + .filter( + Chunk.session_id == self.session.id, + ) + .all() + ) + + chunk_unique_id_to_id = {chunk.unique_external_id: chunk.id for chunk in chunks} + + modules = ( + self.db_session.query(Module) + .filter( + Module.session_id == self.session.id, + ) + .all() + ) + + for chunk in chunks: + chunk_id = chunk.id + asset_names = self.chunk_asset_names_index[chunk.unique_external_id] + + inserts.extend( + [ + dict(asset_id=asset_name_to_id.get(asset_name), chunk_id=chunk_id) + for asset_name in asset_names + if asset_name_to_id.get(asset_name) is not None + ] + ) + if inserts: + self.db_session.execute(assets_chunks.insert(), inserts) + + # associate modules to chunks + # FIXME: this isn't quite right - need to sort out how non-JS assets reference chunks + inserts = [] + + modules: list[Module] = self.db_session.query(Module).filter( + Module.session_id == self.session.id, + ) + for module in modules: + module_id = module.id + chunk_unique_external_ids = self.module_chunk_unique_external_ids_index[ + module.name + ] + + inserts.extend( + [ + dict( + chunk_id=chunk_unique_id_to_id[unique_external_id], + module_id=module_id, + ) + for unique_external_id in chunk_unique_external_ids + ] + ) + if inserts: + self.db_session.execute(chunks_modules.insert(), inserts) diff --git a/libs/shared/shared/bundle_analysis/report.py b/libs/shared/shared/bundle_analysis/report.py new file mode 100644 index 0000000000..8e889afeec --- /dev/null +++ b/libs/shared/shared/bundle_analysis/report.py @@ -0,0 +1,635 @@ +import json +import logging +import os +import sqlite3 +import tempfile +from collections import defaultdict, deque +from typing import Any, Dict, Iterator, List, Optional, Set, Tuple + +import sentry_sdk +from sqlalchemy import asc, desc, text +from sqlalchemy.exc import OperationalError +from sqlalchemy.orm import Session as DbSession +from sqlalchemy.orm import aliased +from sqlalchemy.orm.query import Query +from sqlalchemy.sql import func +from sqlalchemy.sql.functions import coalesce + +from shared.bundle_analysis.db_migrations import BundleAnalysisMigration +from shared.bundle_analysis.models import ( + SCHEMA, + SCHEMA_VERSION, + Asset, + AssetType, + Bundle, + Chunk, + DynamicImport, + Metadata, + MetadataKey, + Module, + Session, + get_db_session, +) +from shared.bundle_analysis.parser import Parser +from shared.bundle_analysis.utils import AssetRoute, AssetRoutePluginName + +log = logging.getLogger(__name__) + + +class ModuleReport: + """ + Report wrapper around a single module (many of which can exist in a single Asset via Chunks) + """ + + def __init__(self, db_path: str, module: Module) -> None: + self.db_path = db_path + self.module = module + + @property + def name(self) -> str: + return self.module.name + + @property + def size(self) -> int: + return self.module.size + + +class AssetReport: + """ + Report wrapper around a single asset (many of which can exist in a single bundle). + """ + + def __init__(self, db_path: str, asset: Asset, bundle_info: dict = {}) -> None: + self.db_path = db_path + self.asset = asset + self.bundle_info = bundle_info + + @property + def id(self) -> int: + return self.asset.id + + @property + def name(self) -> str: + return self.asset.normalized_name + + @property + def hashed_name(self) -> str: + return self.asset.name + + @property + def size(self) -> int: + return self.asset.size + + @property + def gzip_size(self) -> int: + return self.asset.gzip_size + + @property + def uuid(self) -> str: + return self.asset.uuid + + @property + def asset_type(self) -> AssetType: + return self.asset.asset_type + + def modules( + self, pr_changed_files: Optional[List[str]] = None + ) -> List[ModuleReport]: + with get_db_session(self.db_path) as session: + query = ( + session.query(Module) + .join(Module.chunks) + .join(Chunk.assets) + .filter(Asset.id == self.asset.id) + ) + + if pr_changed_files is None: + return [ModuleReport(self.db_path, module) for module in query] + + """ + Apply filter on modules where the module name has to be part of the PR's changed file list. + However we can't simply do a simple equality match because the module names we store is relative + to the root of the app while the PR's file path is relative to the root of the repo. So we will + need to check that any of the PR's file lists ends with any of the modules for the given asset. + For example, + PR changed files: ["abc/def.ts", "ghi/jkl.ts"], + modules: ["def.ts", "mno.ts"] + -> ["def.ts"] + """ + normalized_changed_files = [ + os.path.normpath(path[2:] if path.startswith("./") else path) + for path in pr_changed_files + ] + filtered_modules = set() + for file in normalized_changed_files: + for module in query: + normalized_module = os.path.normpath( + module.name[1:] if file.startswith(".") else module.name + ) + if file.endswith(normalized_module): + filtered_modules.add(module) + + return [ModuleReport(self.db_path, module) for module in filtered_modules] + + def routes(self) -> Optional[List[str]]: + plugin_name = self.bundle_info.get("plugin_name") + if plugin_name not in [item.value for item in AssetRoutePluginName]: + return None + + asset_route_compute = AssetRoute(AssetRoutePluginName(plugin_name)) + module_names, routes = [m.name for m in self.modules()], set() + for module_name in module_names: + route = asset_route_compute.get_from_filename(module_name) + if route is not None: + routes.add(route) + + return list(routes) + + def dynamically_imported_assets(self) -> List["AssetReport"]: + """ + Returns all dynamically imported assets of the current Asset. + This is retrieving by querying all unique Assets in the DynamicImport + model for each Chunk of the current Asset. + """ + with get_db_session(self.db_path) as session: + # Reattach self.asset to the current session to avoid DetachedInstanceError + asset = session.merge(self.asset) + + # Alias the chunks table for the Asset.chunks relationship + asset_chunks = aliased(Chunk) + + assets = ( + session.query(Asset) + .distinct() + .join(DynamicImport, DynamicImport.asset_id == Asset.id) + .join(Chunk, DynamicImport.chunk_id == Chunk.id) + .join(asset_chunks, asset_chunks.id == DynamicImport.chunk_id) + .filter(asset_chunks.id.in_([chunk.id for chunk in asset.chunks])) + ) + + return ( + AssetReport(self.db_path, asset, self.bundle_info) + for asset in assets.all() + ) + + +class BundleRouteReport: + """ + Report wrapper for asset route analytics. Mainly used for BundleRouteComparison + Stores a dictionary + keys: all routes of the bundle + values: a list of distinct Assets (as AssetReports) that is associated with the route + """ + + def __init__(self, db_path: str, data: Dict[str, List[AssetReport]]): + self.db_path = db_path + self.data = data + + def get_sizes(self) -> Dict[str, int]: + results = {} + for route, asset_reports in self.data.items(): + results[route] = sum([asset.size for asset in asset_reports]) + return results + + def get_size(self, route: str) -> Optional[int]: + if route not in self.data: + return None + return sum([asset.size for asset in self.data[route]]) + + +class BundleReport: + """ + Report wrapper around a single bundle (many of which can exist in a single analysis report). + """ + + def __init__(self, db_path: str, bundle: Bundle): + self.db_path = db_path + self.bundle = bundle + + @property + def name(self): + return self.bundle.name + + def _asset_filter( + self, + query: Query, + asset_types: Optional[List[AssetType]] = None, + chunk_entry: Optional[bool] = None, + chunk_initial: Optional[bool] = None, + ) -> Query: + # Filter in assets having chunks with requested initial value + if chunk_initial is not None: + query = query.join(Asset.chunks).filter(Chunk.initial == chunk_initial) + # Filter in assets having chunks with requested entry value + if chunk_entry is not None: + query = query.join(Asset.chunks).filter(Chunk.entry == chunk_entry) + # Filter in assets belonging to requested asset types + if asset_types is not None: + query = query.filter(Asset.asset_type.in_(asset_types)) + return query + + @sentry_sdk.trace + def asset_report_by_name(self, name: str) -> Optional[AssetReport]: + with get_db_session(self.db_path) as session: + asset = session.query(Asset).filter(Asset.name == name).one_or_none() + return AssetReport(self.db_path, asset, self.info()) if asset else None + + @sentry_sdk.trace + def asset_reports( + self, + asset_types: Optional[List[AssetType]] = None, + chunk_entry: Optional[bool] = None, + chunk_initial: Optional[bool] = None, + ordering_column: str = "size", + ordering_desc: Optional[bool] = True, + ) -> Iterator[AssetReport]: + with get_db_session(self.db_path) as session: + ordering = desc if ordering_desc else asc + assets = ( + session.query(Asset) + .join(Asset.session) + .join(Session.bundle) + .filter(Bundle.id == self.bundle.id) + ) + assets = self._asset_filter( + assets, + asset_types, + chunk_entry, + chunk_initial, + ).order_by(ordering(getattr(Asset, ordering_column))) + return ( + AssetReport(self.db_path, asset, self.info()) for asset in assets.all() + ) + + def total_size( + self, + asset_types: Optional[List[AssetType]] = None, + chunk_entry: Optional[bool] = None, + chunk_initial: Optional[bool] = None, + ) -> int: + with get_db_session(self.db_path) as session: + assets = ( + session.query(func.sum(Asset.size).label("asset_size")) + .join(Asset.session) + .join(Session.bundle) + .filter(Bundle.id == self.bundle.id) + ) + assets = self._asset_filter( + assets, + asset_types, + chunk_entry, + chunk_initial, + ) + return assets.scalar() or 0 + + def total_gzip_size( + self, + asset_types: Optional[List[AssetType]] = None, + chunk_entry: Optional[bool] = None, + chunk_initial: Optional[bool] = None, + ) -> int: + """ + Returns the sum of all assets' gzip_size if present plus + the sum of all assets' size if they do not have gzip_size value. + This simulates the amount of data transfer in a realistic setting, + for those assets that are not compressible we will use its uncompressed size. + """ + with get_db_session(self.db_path) as session: + assets = ( + session.query( + func.sum(coalesce(Asset.gzip_size, Asset.size)).label("size") + ) + .join(Asset.session) + .join(Session.bundle) + .filter(Bundle.id == self.bundle.id) + ) + assets = self._asset_filter( + assets, + asset_types, + chunk_entry, + chunk_initial, + ) + return assets.scalar() or 0 + + def info(self) -> dict: + with get_db_session(self.db_path) as session: + result = ( + session.query(Session) + .filter(Session.bundle_id == self.bundle.id) + .first() + ) + return json.loads(result.info) + + def is_cached(self) -> bool: + with get_db_session(self.db_path) as session: + result = session.query(Bundle).filter(Bundle.id == self.bundle.id).first() + return result.is_cached + + def routes(self) -> Dict[str, List[AssetReport]]: + """ + Returns a mapping of routes and all Assets (as AssetReports) that belongs to it + Note that this ignores dynamically imported Assets (ie only the direct asset) + """ + route_map = defaultdict(list) + for asset_report in self.asset_reports(): + routes = asset_report.routes() + if routes is not None: + for route in routes: + route_map[route].append(asset_report) + return route_map + + @sentry_sdk.trace + def full_route_report(self) -> BundleRouteReport: + """ + A more powerful routes function that will additionally associate dynamically + imported Assets into the belonging route. Also this function returns a + BundleRouteReport object as this will be used for comparison and additional + data manipulation. + """ + return_data = defaultdict(list) # typing: Dict[str, List[AssetReport]] + for route, asset_reports in self.routes().items(): + # Implements a graph traversal algorithm to get all nodes (Asset) linked by edges + # represented as DynamicImport. + visited_asset_ids = set() + unique_assets = [] + + # For each Asset get all the dynamic imported Asset that we will need to traverse into + to_be_processed_asset = deque(asset_reports) + while to_be_processed_asset: + current_asset = to_be_processed_asset.popleft() + if current_asset.id not in visited_asset_ids: + visited_asset_ids.add(current_asset.id) + unique_assets.append(current_asset) + to_be_processed_asset += current_asset.dynamically_imported_assets() + + # Add all the assets found to the route we were processing + return_data[route] = unique_assets + return BundleRouteReport(self.db_path, return_data) + + +class BundleAnalysisReport: + """ + Report wrapper around multiple bundles for a single commit report. + """ + + db_path: str + + def __init__(self, db_path: str | None = None): + if db_path is None: + _, self.db_path = tempfile.mkstemp(prefix="bundle_analysis_") + else: + self.db_path = db_path + with get_db_session(self.db_path) as db_session: + self._setup(db_session) + + @sentry_sdk.trace + def _setup(self, db_session: DbSession) -> None: + """ + Creates the schema for a new bundle report database. + """ + try: + schema_version = ( + db_session.query(Metadata) + .filter_by(key=MetadataKey.SCHEMA_VERSION.value) + .first() + ).value + if schema_version < SCHEMA_VERSION: + log.info( + f"Migrating Bundle Analysis DB schema from {schema_version} to {SCHEMA_VERSION}" + ) + BundleAnalysisMigration( + db_session, schema_version, SCHEMA_VERSION + ).migrate() + except OperationalError: + # schema does not exist + try: + con = sqlite3.connect(self.db_path) + con.executescript(SCHEMA) + con.commit() + finally: + con.close() + schema_version = Metadata( + key=MetadataKey.SCHEMA_VERSION.value, + value=SCHEMA_VERSION, + ) + db_session.add(schema_version) + db_session.commit() + + def cleanup(self): + os.unlink(self.db_path) + + @sentry_sdk.trace + def ingest(self, path: str, compare_sha: Optional[str] = None) -> Tuple[int, str]: + """ + Ingest the bundle stats JSON at the given file path. + Returns session ID of ingested data. + """ + with get_db_session(self.db_path) as session: + # Normally Assets/Chunks/Modules are cascade deleted and the many-to-many table entries + # would be deleted as well as they are foreign keys. However some rare cases occurs + # where the chunks_modules and assets_chunks table IDs doesn't exist in its + # associated Assets/Chunks/Modules table even though they are foreign keys. + # Fix: before each ingestion we make sure these rows are deleted. + for params in [ + ["chunks_modules", "chunk_id", "chunks", "module_id", "modules"], + ["assets_chunks", "asset_id", "assets", "chunk_id", "chunks"], + ]: + sql = text( + f""" + DELETE FROM {params[0]} + WHERE + {params[1]} NOT IN (SELECT id FROM {params[2]}) + OR {params[3]} NOT IN (SELECT id FROM {params[4]}) + """ + ) + result = session.execute(sql) + session.commit() + rows_deleted = result.rowcount + if rows_deleted > 0: + log.warning( + f"Integrity error detected, deleted {rows_deleted} corrupted rows from {params[0]}" + ) + + parser = Parser(path, session).get_proper_parser() + session_id, bundle_name = parser.parse(path) + + # Save custom base commit SHA for doing comparisons if available + if compare_sha: + sql = text( + """ + INSERT OR REPLACE INTO metadata (key, value) + VALUES (:key, :value) + """ + ) + session.execute( + sql, {"key": "compare_sha", "value": json.dumps(compare_sha)} + ) + session.commit() + + session.commit() + return session_id, bundle_name + + def _associate_bundle_report_assets_by_name( + self, curr_bundle_report: BundleReport, prev_bundle_report: BundleReport + ) -> Set[Tuple[str, str]]: + """ + Rule 1 + Returns a set of pairs of UUIDs (the current asset UUID and prev asset UUID) + representing that the curr asset UUID should be updated to the prev asset UUID + because the curr asset has the same hashed name as the previous asset + """ + ret = set() + prev_asset_hashed_names = { + a.hashed_name: a.uuid for a in prev_bundle_report.asset_reports() + } + for curr_asset in curr_bundle_report.asset_reports(): + if curr_asset.asset_type == AssetType.JAVASCRIPT: + if curr_asset.hashed_name in prev_asset_hashed_names: + ret.add( + ( + prev_asset_hashed_names[curr_asset.hashed_name], + curr_asset.uuid, + ) + ) + return ret + + def _associate_bundle_report_assets_by_module_names( + self, curr_bundle_report: BundleReport, prev_bundle_report: BundleReport + ) -> Set[Tuple[str, str]]: + """ + Rule 2 + Returns a set of pairs of UUIDs (the current asset UUID and prev asset UUID) + representing that the curr asset UUID should be updated to the prev asset UUID + because there exists a prev asset where all its module names are the same as the + curr asset module names + """ + ret = set() + prev_module_asset_mapping = {} + for prev_asset in prev_bundle_report.asset_reports(): + if prev_asset.asset_type == AssetType.JAVASCRIPT: + prev_modules = tuple( + sorted(frozenset([m.name for m in prev_asset.modules()])) + ) + # NOTE: Assume two non-related assets CANNOT have the same set of modules + # though in reality there can be rare cases of this but we + # will deal with that later if it becomes a prevalent problem + prev_module_asset_mapping[prev_modules] = prev_asset.uuid + + for curr_asset in curr_bundle_report.asset_reports(): + if curr_asset.asset_type == AssetType.JAVASCRIPT: + curr_modules = tuple( + sorted(frozenset([m.name for m in curr_asset.modules()])) + ) + if curr_modules in prev_module_asset_mapping: + ret.add( + ( + prev_module_asset_mapping[curr_modules], + curr_asset.uuid, + ) + ) + return ret + + @sentry_sdk.trace + def associate_previous_assets( + self, prev_bundle_analysis_report: "BundleAnalysisReport" + ) -> None: + """ + Only associate past asset if it is Javascript or Typescript types + and belonging to the same bundle name + Associated if one of the following is true + Rule 1. Previous and current asset have the same hashed name + Rule 2. Previous and current asset shared the same set of module names + """ + associated_assets_found = set() + + prev_bundle_reports = list(prev_bundle_analysis_report.bundle_reports()) + for curr_bundle_report in self.bundle_reports(): + for prev_bundle_report in prev_bundle_reports: + if curr_bundle_report.name == prev_bundle_report.name: + # Rule 1 check + associated_assets_found |= ( + self._associate_bundle_report_assets_by_name( + curr_bundle_report, prev_bundle_report + ) + ) + + # Rule 2 check + associated_assets_found |= ( + self._associate_bundle_report_assets_by_module_names( + curr_bundle_report, prev_bundle_report + ) + ) + + with get_db_session(self.db_path) as session: + # Update the Assets table for the bundle correct uuid + for pair in associated_assets_found: + prev_uuid, curr_uuid = pair + session.query(Asset).filter(Asset.uuid == curr_uuid).update( + {Asset.uuid: prev_uuid} + ) + session.commit() + + def metadata(self) -> Dict[MetadataKey, Any]: + with get_db_session(self.db_path) as session: + metadata = session.query(Metadata).all() + return {MetadataKey(item.key): item.value for item in metadata} + + def bundle_reports(self) -> Iterator[BundleReport]: + with get_db_session(self.db_path) as session: + bundles = session.query(Bundle).all() + return (BundleReport(self.db_path, bundle) for bundle in bundles) + + def bundle_report(self, bundle_name: str) -> Optional[BundleReport]: + with get_db_session(self.db_path) as session: + bundle = session.query(Bundle).filter_by(name=bundle_name).first() + if bundle is None: + return None + return BundleReport(self.db_path, bundle) + + def session_count(self) -> int: + with get_db_session(self.db_path) as session: + return session.query(Session).count() + + def update_is_cached(self, data: Dict[str, bool]) -> None: + with get_db_session(self.db_path) as session: + for bundle_name, value in data.items(): + session.query(Bundle).filter(Bundle.name == bundle_name).update( + {Bundle.is_cached: value} + ) + session.commit() + + def is_cached(self) -> bool: + with get_db_session(self.db_path) as session: + cached_bundles = session.query(Bundle).filter_by(is_cached=True) + return cached_bundles.count() > 0 + + @sentry_sdk.trace + def delete_bundle_by_name(self, bundle_name: str) -> None: + with get_db_session(self.db_path) as session: + bundle_to_be_deleted = ( + session.query(Bundle).filter_by(name=bundle_name).one_or_none() + ) + if bundle_to_be_deleted is None: + return + + # Deletes Asset, Chunk, Module + session_to_be_deleted = ( + session.query(Session) + .filter(Session.bundle == bundle_to_be_deleted) + .one_or_none() + ) + if session_to_be_deleted is None: + raise Exception( + "Data integrity error - cannot have Bundles without Sessions" + ) + for model in [Asset, Chunk, Module]: + stmt = model.__table__.delete().where( + model.session == session_to_be_deleted + ) + session.execute(stmt) + + # Deletes Session and Bundle + session.delete(session_to_be_deleted) + session.delete(bundle_to_be_deleted) + + session.commit() diff --git a/libs/shared/shared/bundle_analysis/storage.py b/libs/shared/shared/bundle_analysis/storage.py new file mode 100644 index 0000000000..5a4ca7c833 --- /dev/null +++ b/libs/shared/shared/bundle_analysis/storage.py @@ -0,0 +1,74 @@ +import logging +import tempfile +from enum import Enum +from typing import Optional + +import sentry_sdk + +from shared.bundle_analysis.report import BundleAnalysisReport +from shared.config import get_config +from shared.storage.base import BaseStorageService +from shared.storage.exceptions import FileNotInStorageError, PutRequestRateLimitError + +log = logging.getLogger(__name__) + + +def get_bucket_name() -> str: + return get_config("bundle_analysis", "bucket_name", default="bundle-analysis") + + +class StoragePaths(Enum): + bundle_report = "v1/repos/{repo_key}/{report_key}/bundle_report.sqlite" + upload = "v1/uploads/{upload_key}.json" + + def path(self, **kwargs): + return self.value.format(**kwargs) + + +class BundleAnalysisReportLoader: + """ + Loads and saves `BundleAnalysisReport`s into the underlying storage service. + Requires a `repo_key` that uniquely and permanently (i.e. maybe not the name/slug) + that identifies a repo in the storage layer. + """ + + def __init__(self, storage_service: BaseStorageService, repo_key: str): + self.storage_service = storage_service + self.repo_key = repo_key + self.bucket_name = get_bucket_name() + + @sentry_sdk.trace + def load(self, report_key: str) -> Optional[BundleAnalysisReport]: + """ + Loads the `BundleAnalysisReport` for the given report key from storage + or returns `None` if no such report exists. + """ + path = StoragePaths.bundle_report.path( + repo_key=self.repo_key, report_key=report_key + ) + _, db_path = tempfile.mkstemp(prefix="bundle_analysis_") + + with open(db_path, "w+b") as f: + try: + self.storage_service.read_file(self.bucket_name, path, file_obj=f) + except FileNotInStorageError: + return None + return BundleAnalysisReport(db_path) + + @sentry_sdk.trace + def save(self, report: BundleAnalysisReport, report_key: str): + """ + Saves a `BundleAnalysisReport` for the given report key into storage. + """ + storage_path = StoragePaths.bundle_report.path( + repo_key=self.repo_key, report_key=report_key + ) + try: + with open(report.db_path, "rb") as f: + self.storage_service.write_file(self.bucket_name, storage_path, f) + except Exception as e: + log.info(f"Bundle analysis GCS save file error: {e}") + if "TooManyRequests" in str(e): + raise PutRequestRateLimitError("GCS Rate Limit Error for Saving File") + else: + raise e diff --git a/libs/shared/shared/bundle_analysis/utils.py b/libs/shared/shared/bundle_analysis/utils.py new file mode 100644 index 0000000000..c89e439286 --- /dev/null +++ b/libs/shared/shared/bundle_analysis/utils.py @@ -0,0 +1,374 @@ +import logging +import os +import re +from enum import Enum +from pathlib import Path +from typing import List, Optional + +log = logging.getLogger(__name__) + + +class AssetRoutePluginName(Enum): + REMIX_VITE = "@codecov/remix-vite-plugin" + NEXTJS_WEBPACK = "@codecov/nextjs-webpack-plugin" + NUXT = "@codecov/nuxt-plugin" + SOLIDSTART = "@codecov/solidstart-plugin" + SVELTEKIT = "@codecov/sveltekit-plugin" + ASTRO = "@codecov/astro-plugin" + + +class AssetRoute: + def __init__( + self, + plugin: AssetRoutePluginName, + configured_route_prefix: Optional[str] = None, + ) -> None: + self._from_filename_map = { + AssetRoutePluginName.REMIX_VITE: (self._compute_remix, ["app", "routes"]), + AssetRoutePluginName.NEXTJS_WEBPACK: (self._compute_nextjs_webpack, "app"), + AssetRoutePluginName.NUXT: (self._compute_nuxt, "pages"), + AssetRoutePluginName.SOLIDSTART: ( + self._compute_solidstart, + ["src", "routes"], + ), + AssetRoutePluginName.SVELTEKIT: ( + self._compute_sveltekit, + ["src", "routes"], + ), + AssetRoutePluginName.ASTRO: ( + self._compute_astro, + ["src", "pages"], + ), + } + self._compute_from_filename = self._from_filename_map[plugin][0] + + if configured_route_prefix is not None: + self._prefix = configured_route_prefix + else: + self._prefix = self._from_filename_map[plugin][1] + + def _is_file(self, s: str, extensions: Optional[List[str]] = None) -> bool: + """ + Determines if the passed string represents a file with one or more dots, + and optionally verifies if it ends with a specific extension. + + Args: + s (str): The string to check. + extension (Optional[str]): The file extension to validate (e.g., "vue"). + + Returns: + bool: True if the string represents a valid file, False otherwise. + """ + # If a list of extensions is provided, check if the string ends with it + if extensions is not None and not any( + [s.endswith(f".{e}") for e in extensions] + ): + return False + + # Matches strings with at least one non-dot character before the first dot + # and at least one non-dot character after the last dot. + file_regex = re.compile(r"^[^/\\]+?\.[^/\\]+$") + return bool(file_regex.match(s)) + + def _compute_remix(self, filename: str) -> Optional[str]: + """ + Computes the route for Next.js Webpack plugin. + Doc: https://remix.run/docs/en/main/file-conventions/routes + """ + path_items = Path(filename).parts + + # Check if contains at least 3 parts (2 prefix and suffix) + if len(path_items) < 3: + return None + + # Check if 2 prefix is present + if path_items[0] != self._prefix[0] or path_items[1] != self._prefix[1]: + return None + + # Remove parameters after extension + file = path_items[-1] + if file.rfind("?") >= 0: + file = file[: file.rfind("?")] + + # Check if suffix is a file that with valid extensions + if not self._is_file(file, extensions=["tsx", "ts", "jsx", "js"]): + return None + + # Get the file name without extension + file = path_items[-1] + file = file[: file.rfind(".")] + + returned_path = list(path_items[2:-1]) + + # Split the file by . to build the route with special rules + file_items = split_by_delimiter( + file, delimiter=".", escape_open="[", escape_close="]" + ) + for item in file_items: + if not item.startswith("_"): + if item.endswith("_"): + returned_path.append(item[:-1]) + else: + returned_path.append(item) + + # Build path from items excluding prefix and suffix + return "/" + "/".join(returned_path) + + def _compute_nextjs_webpack(self, filename: str) -> Optional[str]: + """ + Computes the route for Next.js Webpack plugin. + Doc: https://nextjs.org/docs/app/building-your-application/routing + """ + path_items = Path(filename).parts + + # Check if contains at least 2 parts (prefix and suffix) + if len(path_items) < 2: + return None + + # Check if prefix is present and suffix is a file type + if path_items[0] != self._prefix or not self._is_file(path_items[-1]): + return None + + # Build path from items excluding prefix and suffix + return "/" + "/".join(path_items[1:-1]) + + def _compute_nuxt(self, filename: str) -> Optional[str]: + """ + Computes the route for Nuxt plugin. + Doc: https://nuxt.com/docs/getting-started/routing + """ + path_items = Path(filename).parts + + # Check if contains at least 2 parts (prefix and suffix) + if len(path_items) < 2: + return None + + # Check if prefix is present and suffix is a file type that has .vue extension + if path_items[0] != self._prefix or not self._is_file(path_items[-1], ["vue"]): + return None + + # Remove .vue from last path item + path_items = list(path_items) + path_items[-1] = path_items[-1][:-4] + + # Drop file index if exists + if path_items[-1] == "index": + path_items.pop() + + # Build path from items excluding prefix + return "/" + "/".join(path_items[1:]) + + def _compute_solidstart(self, filename: str) -> Optional[str]: + """ + Computes the route for SolidtStart plugin. + Doc: https://docs.solidjs.com/solid-start/building-your-application/routing#file-based-routing + """ + path_items = Path(filename).parts + + # Check if contains at least 3 parts (2 prefix and suffix) + if len(path_items) < 3: + return None + + # Check if 2 prefix is present + if path_items[0] != self._prefix[0] or path_items[1] != self._prefix[1]: + return None + + # Check if suffix is a file that with valid extensions + if not self._is_file(path_items[-1], extensions=["tsx", "ts", "jsx", "js"]): + return None + + # Remove route groups and renamed indices, ie remove character inside parenthesis and itself + returned_items = [re.sub(r"\(.*?\)", "", item) for item in path_items] + + # Get the file name without extension + file = returned_items[-1] + file = file[: file.rfind(".")] + returned_items[-1] = file + + # Remove index file if exists + if returned_items[-1] == "index": + returned_items.pop() + + # Build path from items excluding prefix and suffix + return "/" + "/".join([item for item in returned_items[2:] if item != ""]) + + def _compute_sveltekit(self, filename: str) -> Optional[str]: + """ + Computes the route for SvelteKit plugin. + Doc: https://svelte.dev/docs/kit/routing + """ + path_items = Path(filename).parts + + # Check if contains at least 3 parts (2 prefix and suffix) + if len(path_items) < 3: + return None + + # Check if 2 prefix is present + if path_items[0] != self._prefix[0] or path_items[1] != self._prefix[1]: + return None + + # Check if suffix is a file that starts with "+" + if not self._is_file(path_items[-1]) or not path_items[-1].startswith("+"): + return None + + # Build path from items excluding 2 prefix and suffix + return "/" + "/".join(path_items[2:-1]) + + def _compute_astro(self, filename: str) -> Optional[str]: + """ + Computes the route for Astro plugin. + Doc: https://docs.astro.build/en/guides/routing + """ + path_items = Path(filename).parts + + # Check if contains at least 3 parts (2 prefix and suffix) + if len(path_items) < 3: + return None + + # Check if 2 prefix is present + if path_items[0] != self._prefix[0] or path_items[1] != self._prefix[1]: + return None + + path_items = list(path_items) + file = path_items[-1] + + # Remove parameters after extension + if file.rfind("?") >= 0: + file = file[: file.rfind("?")] + + # Check if suffix is a file that with valid extensions + if not self._is_file( + file, extensions=["astro", "md", "mdx", "html", "js", "ts"] + ): + return None + + # Excludes pages if anywhere in the path it starts with _ + if any([path.startswith("_") for path in path_items]): + return None + + # Get the file name without extension + file = file[: file.rfind(".")] + + path_items[-1] = file + + # Drop file index if exists + if file == "index": + path_items.pop() + + # Build path from items excluding 2 prefix + return "/" + "/".join(path_items[2:]) + + def get_from_filename(self, filename: str) -> Optional[str]: + """ + Computes the route. + Args: + filename (str): The file path to compute the route from. + + Returns: + Optional[str]: The computed route or None if invalid. + """ + try: + return self._compute_from_filename(filename) + except Exception as e: + log.error( + f"Uncaught error during AssetRoute path compute: {e}", exc_info=True + ) + return None + + +def get_extension(filename: str) -> str: + """ + Gets the file extension of the file without the dot + """ + # At times file can be something like './index.js + 12 modules', only keep the real filepath + filename = filename.split(" ")[0] + # Retrieve the file extension with the dot + _, file_extension = os.path.splitext(filename) + # Return empty string if file has no extension + if not file_extension or file_extension[0] != ".": + return "" + # Remove the dot in the extension + file_extension = file_extension[1:] + # At times file can be something like './index.js?module', remove the ? + file_extension = file_extension.split("?")[0] + + return file_extension + + +def split_by_delimiter( + s: str, + delimiter: str, + escape_open: Optional[str] = None, + escape_close: Optional[str] = None, +) -> List[str]: + """ + Splits a string based on a specified delimiter character, optionally respecting escape delimiters. + + Parameters: + ---------- + s : str + The input string to split. + delimiter : str + The character used to split the string. Must be a single character. + escape_open : Optional[str], default=None + The character indicating the start of an escaped section. + If provided, must be a single character. + escape_close : Optional[str], default=None + The character indicating the end of an escaped section. + If provided, must be a single character. + + Returns: + ------- + List[str] + A list of substrings obtained by splitting `s` at occurrences of `delimiter`, + unless the delimiter is within an escaped section. + Returns an empty list if input parameters are invalid. + """ + # Error handling for invalid parameters + if not s: + return [] + if not isinstance(delimiter, str) or len(delimiter) != 1: + return [] + if ( + escape_open is not None + and (not isinstance(escape_open, str) or len(escape_open) != 1) + ) or ( + escape_close is not None + and (not isinstance(escape_close, str) or len(escape_close) != 1) + ): + return [] + if (escape_open is None) != (escape_close is None): # Only one of them is None + return [] + if delimiter == escape_open or delimiter == escape_close: + return [] + + result = [] + buffer = [] + inside_escape = 0 + + for char in s: + if char == escape_open: + inside_escape += 1 + if inside_escape == 1: + continue # Skip adding the opening escape character + elif char == escape_close: + inside_escape -= 1 + if inside_escape == 0: + continue # Skip adding the closing escape character + elif inside_escape < 0: + return [] + elif char == delimiter and inside_escape == 0: + # Split here if not inside escape brackets + result.append("".join(buffer)) + buffer = [] + continue + + buffer.append(char) + + if buffer or s[-1] == delimiter: + result.append("".join(buffer)) + + if inside_escape != 0: + return [] + + return result diff --git a/libs/shared/shared/celery_config.py b/libs/shared/shared/celery_config.py new file mode 100644 index 0000000000..ef714fd07e --- /dev/null +++ b/libs/shared/shared/celery_config.py @@ -0,0 +1,473 @@ +# http://docs.celeryq.org/en/latest/configuration.html#configuration +from typing import Optional + +from shared.config import get_config +from shared.utils.enums import TaskConfigGroup + +# Task name follows the following convention: +# task_name |- app... +# can be "tasks" or "cron" +# is the task's TaskConfigGroup +# is the task name (usually same as task class) +sync_teams_task_name = f"app.tasks.{TaskConfigGroup.sync_teams.value}.SyncTeams" +sync_repos_task_name = f"app.tasks.{TaskConfigGroup.sync_repos.value}.SyncRepos" +sync_repo_languages_task_name = ( + f"app.tasks.{TaskConfigGroup.sync_repo_languages.value}.SyncLanguages" +) +sync_repo_languages_gql_task_name = ( + f"app.tasks.{TaskConfigGroup.sync_repo_languages_gql.value}.SyncLanguagesGQL" +) +delete_owner_task_name = f"app.tasks.{TaskConfigGroup.delete_owner.value}.DeleteOwner" +activate_account_user_task_name = ( + f"app.tasks.{TaskConfigGroup.sync_account.value}.ActivateAccountUser" +) +notify_task_name = f"app.tasks.{TaskConfigGroup.notify.value}.Notify" +pulls_task_name = f"app.tasks.{TaskConfigGroup.pulls.value}.Sync" +status_set_error_task_name = f"app.tasks.{TaskConfigGroup.status.value}.SetError" +status_set_pending_task_name = f"app.tasks.{TaskConfigGroup.status.value}.SetPending" +pre_process_upload_task_name = ( + f"app.tasks.{TaskConfigGroup.upload.value}.PreProcessUpload" +) +upload_task_name = f"app.tasks.{TaskConfigGroup.upload.value}.Upload" +upload_processor_task_name = f"app.tasks.{TaskConfigGroup.upload.value}.UploadProcessor" +upload_finisher_task_name = f"app.tasks.{TaskConfigGroup.upload.value}.UploadFinisher" +parallel_verification_task_name = ( + f"app.tasks.{TaskConfigGroup.upload.value}.ParallelVerification" +) +test_results_processor_task_name = ( + f"app.tasks.{TaskConfigGroup.test_results.value}.TestResultsProcessor" +) + +test_results_finisher_task_name = ( + f"app.tasks.{TaskConfigGroup.test_results.value}.TestResultsFinisherTask" +) + +sync_test_results_task_name = ( + f"app.tasks.{TaskConfigGroup.test_results.value}.SyncTestResultsTask" +) + +cache_test_rollups_task_name = ( + f"app.tasks.{TaskConfigGroup.cache_rollup.value}.CacheTestRollupsTask" +) + +cache_test_rollups_redis_task_name = ( + f"app.tasks.{TaskConfigGroup.cache_rollup.value}.CacheTestRollupsRedisTask" +) + +process_flakes_task_name = f"app.tasks.{TaskConfigGroup.flakes.value}.ProcessFlakesTask" + +manual_upload_completion_trigger_task_name = ( + f"app.tasks.{TaskConfigGroup.upload.value}.ManualUploadCompletionTrigger" +) +comment_task_name = f"app.tasks.{TaskConfigGroup.comment.value}.Comment" +flush_repo_task_name = f"app.tasks.{TaskConfigGroup.flush_repo.value}.FlushRepo" +ghm_sync_plans_task_name = f"app.tasks.{TaskConfigGroup.sync_plans.value}.SyncPlans" +send_email_task_name = f"app.tasks.{TaskConfigGroup.send_email.value}.SendEmail" +new_user_activated_task_name = ( + f"app.tasks.{TaskConfigGroup.new_user_activated.value}.NewUserActivated" +) +compute_comparison_task_name = ( + f"app.tasks.{TaskConfigGroup.compute_comparison.value}.ComputeComparison" +) +commit_update_task_name = ( + f"app.tasks.{TaskConfigGroup.commit_update.value}.CommitUpdate" +) + +profiling_finding_task_name = ( + f"app.cron.{TaskConfigGroup.profiling.value}.findinguncollected" +) +profiling_summarization_task_name = ( + f"app.tasks.{TaskConfigGroup.profiling.value}.summarization" +) +profiling_collection_task_name = ( + f"app.tasks.{TaskConfigGroup.profiling.value}.collection" +) +profiling_normalization_task_name = ( + f"app.tasks.{TaskConfigGroup.profiling.value}.normalizer" +) + +# Timeseries tasks +timeseries_backfill_task_name = f"app.tasks.{TaskConfigGroup.timeseries.value}.backfill" +timeseries_backfill_dataset_task_name = ( + f"app.tasks.{TaskConfigGroup.timeseries.value}.backfill_dataset" +) +timeseries_backfill_commits_task_name = ( + f"app.tasks.{TaskConfigGroup.timeseries.value}.backfill_commits" +) +timeseries_delete_task_name = f"app.tasks.{TaskConfigGroup.timeseries.value}.delete" +timeseries_save_commit_measurements_task_name = ( + f"app.tasks.{TaskConfigGroup.timeseries.value}.save_commit_measurements" +) + +static_analysis_task_name = ( + f"app.tasks.{TaskConfigGroup.static_analysis.value}.check_suite" +) +label_analysis_task_name = ( + f"app.tasks.{TaskConfigGroup.label_analysis.value}.process_request" +) + +health_check_task_name = f"app.cron.{TaskConfigGroup.healthcheck.value}.HealthCheckTask" +gh_app_webhook_check_task_name = ( + f"app.cron.{TaskConfigGroup.daily.value}.GitHubAppWebhooksCheckTask" +) +brolly_stats_rollup_task_name = ( + f"app.cron.{TaskConfigGroup.daily.value}.BrollyStatsRollupTask" +) +flare_cleanup_task_name = f"app.cron.{TaskConfigGroup.daily.value}.FlareCleanupTask" + + +def get_task_group(task_name: str) -> Optional[str]: + task_parts = task_name.split(".") + if len(task_parts) != 4: + return None + return task_parts[2] + + +class BaseCeleryConfig(object): + broker_url = get_config("services", "celery_broker") or get_config( + "services", "redis_url" + ) + result_backend = get_config("services", "celery_broker") or get_config( + "services", "redis_url" + ) + + broker_transport_options = {"visibility_timeout": (60 * 60 * 6)} # 6 hours + result_extended = True + task_default_queue = get_config( + "setup", "tasks", "celery", "default_queue", default="celery" + ) + health_check_default_queue = "healthcheck" + + # Import jobs + imports = ("tasks",) + + task_serializer = "json" + + accept_content = ["json"] + + worker_max_memory_per_child = int( + get_config( + "setup", "tasks", "celery", "worker_max_memory_per_child", default=1500000 + ) + ) # 1.5GB + + # http://docs.celeryproject.org/en/latest/configuration.html?highlight=celery_redirect_stdouts#celeryd-hijack-root-logger + worker_hijack_root_logger = False + + timezone = "UTC" + enable_utc = True + + # http://docs.celeryproject.org/en/latest/configuration.html#celery-ignore-result + task_ignore_result = True + + # http://celery.readthedocs.org/en/latest/userguide/tasks.html#disable-rate-limits-if-they-re-not-used + worker_disable_rate_limits = True + + # http://celery.readthedocs.org/en/latest/faq.html#should-i-use-retry-or-acks-late + task_acks_late = bool(get_config("setup", "tasks", "celery", "acks_late")) + + # http://celery.readthedocs.org/en/latest/userguide/optimizing.html#prefetch-limits + worker_prefetch_multiplier = int( + get_config("setup", "tasks", "celery", "prefetch", default=1) + ) + # !!! NEVER 0 !!! 0 == infinite + + # http://celery.readthedocs.org/en/latest/configuration.html#celeryd-task-soft-time-limit + task_soft_time_limit = int( + get_config("setup", "tasks", "celery", "soft_timelimit", default=400) + ) + + # http://celery.readthedocs.org/en/latest/configuration.html#std:setting-CELERYD_TASK_TIME_LIMIT + task_time_limit = int( + get_config("setup", "tasks", "celery", "hard_timelimit", default=480) + ) + + notify_soft_time_limit = int( + get_config( + "setup", "tasks", TaskConfigGroup.notify.value, "timeout", default=120 + ) + ) + timeseries_soft_time_limit = get_config( + "setup", + "tasks", + TaskConfigGroup.timeseries.value, + "soft_timelimit", + default=400, + ) + timeseries_hard_time_limit = get_config( + "setup", + "tasks", + TaskConfigGroup.timeseries.value, + "hard_timelimit", + default=480, + ) + + gh_webhook_retry_soft_time_limit = get_config( + "setup", "tasks", TaskConfigGroup.daily.value, "soft_timelimit", default=600 + ) + + gh_webhook_retry_hard_time_limit = get_config( + "setup", "tasks", TaskConfigGroup.daily.value, "hard_timelimit", default=680 + ) + + task_annotations = { + delete_owner_task_name: { + "soft_time_limit": 2 * task_soft_time_limit, + "time_limit": 2 * task_time_limit, + }, + notify_task_name: { + "soft_time_limit": notify_soft_time_limit, + "time_limit": notify_soft_time_limit + 20, + }, + sync_repos_task_name: { + "soft_time_limit": 2 * task_soft_time_limit, + "time_limit": 2 * task_time_limit, + }, + timeseries_backfill_dataset_task_name: { + "soft_time_limit": timeseries_soft_time_limit, + "time_limit": timeseries_hard_time_limit, + }, + timeseries_backfill_commits_task_name: { + "soft_time_limit": timeseries_soft_time_limit, + "time_limit": timeseries_hard_time_limit, + }, + timeseries_save_commit_measurements_task_name: { + "soft_time_limit": timeseries_soft_time_limit, + "time_limit": timeseries_hard_time_limit, + }, + gh_app_webhook_check_task_name: { + "soft_time_limit": gh_webhook_retry_soft_time_limit, + "time_limit": gh_webhook_retry_hard_time_limit, + }, + } + + task_routes = { + sync_teams_task_name: { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.sync_teams.value, + "queue", + default=task_default_queue, + ) + }, + sync_repos_task_name: { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.sync_repos.value, + "queue", + default=task_default_queue, + ) + }, + sync_repo_languages_task_name: { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.sync_repo_languages.value, + "queue", + default=task_default_queue, + ) + }, + sync_repo_languages_gql_task_name: { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.sync_repo_languages_gql.value, + "queue", + default=task_default_queue, + ) + }, + delete_owner_task_name: { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.delete_owner.value, + "queue", + default=task_default_queue, + ) + }, + activate_account_user_task_name: { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.sync_account.value, + "queue", + default=task_default_queue, + ) + }, + notify_task_name: { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.notify.value, + "queue", + default=task_default_queue, + ) + }, + pulls_task_name: { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.pulls.value, + "queue", + default=task_default_queue, + ) + }, + f"app.tasks.{TaskConfigGroup.status.value}.*": { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.status.value, + "queue", + default=task_default_queue, + ) + }, + f"app.tasks.{TaskConfigGroup.upload.value}.*": { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.upload.value, + "queue", + default=task_default_queue, + ) + }, + f"app.tasks.{TaskConfigGroup.test_results.value}.*": { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.test_results.value, + "queue", + default=task_default_queue, + ) + }, + f"app.tasks.{TaskConfigGroup.flakes.value}.*": { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.flakes.value, + "queue", + default=task_default_queue, + ) + }, + f"app.tasks.{TaskConfigGroup.cache_rollup.value}.*": { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.cache_rollup.value, + "queue", + default=task_default_queue, + ) + }, + f"app.tasks.{TaskConfigGroup.archive.value}.*": { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.archive.value, + "queue", + default=task_default_queue, + ) + }, + comment_task_name: { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.comment.value, + "queue", + default=task_default_queue, + ) + }, + flush_repo_task_name: { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.flush_repo.value, + "queue", + default=task_default_queue, + ) + }, + ghm_sync_plans_task_name: { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.sync_plans.value, + "queue", + default=task_default_queue, + ) + }, + new_user_activated_task_name: { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.new_user_activated.value, + "queue", + default=task_default_queue, + ) + }, + f"app.tasks.{TaskConfigGroup.profiling.value}.*": { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.profiling.value, + "queue", + default=task_default_queue, + ) + }, + f"app.cron.{TaskConfigGroup.profiling.value}.*": { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.profiling.value, + "queue", + default=task_default_queue, + ) + }, + commit_update_task_name: { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.commit_update.value, + "queue", + default=task_default_queue, + ) + }, + f"app.tasks.{TaskConfigGroup.timeseries.value}.*": { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.timeseries.value, + "queue", + default=task_default_queue, + ) + }, + compute_comparison_task_name: { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.compute_comparison.value, + "queue", + default=task_default_queue, + ) + }, + health_check_task_name: { + "queue": health_check_default_queue, + }, + f"app.tasks.{TaskConfigGroup.label_analysis.value}.*": { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.label_analysis.value, + "queue", + default=task_default_queue, + ) + }, + f"app.tasks.{TaskConfigGroup.static_analysis.value}.*": { + "queue": get_config( + "setup", + "tasks", + TaskConfigGroup.static_analysis.value, + "queue", + default=task_default_queue, + ) + }, + } diff --git a/libs/shared/shared/celery_router.py b/libs/shared/shared/celery_router.py new file mode 100644 index 0000000000..da9867cca6 --- /dev/null +++ b/libs/shared/shared/celery_router.py @@ -0,0 +1,67 @@ +import fnmatch +import re +from collections import OrderedDict +from collections.abc import Mapping + +from shared.celery_config import BaseCeleryConfig, get_task_group +from shared.config import get_config +from shared.django_apps.codecov_auth.models import Plan + +Pattern = re.Pattern + + +# based on code from https://github.com/celery/celery/blob/main/celery/app/routes.py +class MapRoute: + def __init__(self, map): + map = map.items() if isinstance(map, Mapping) else map + self.map = {} + self.patterns = OrderedDict() + for k, v in map: + if isinstance(k, Pattern): + self.patterns[k] = v + elif "*" in k: + self.patterns[re.compile(fnmatch.translate(k))] = v + else: + self.map[k] = v + + def __call__(self, name, *args, **kwargs): + try: + return dict(self.map[name]) + except KeyError: + pass + except ValueError: + return {"queue": self.map[name]} + for regex, route in self.patterns.items(): + if regex.match(name): + try: + return dict(route) + except ValueError: + return {"queue": route} + + +def route_tasks_based_on_user_plan(task_name: str, user_plan: str): + """Helper function to dynamically route tasks based on the user plan. + This cannot be used as a celery router function directly. + Returns extra config for the queue, if any. + """ + route = MapRoute(BaseCeleryConfig.task_routes) + default_task_queue = ( + route(task_name) or dict(queue=BaseCeleryConfig.task_default_queue) + )["queue"] + plan = Plan.objects.get(name=user_plan) + if plan.is_enterprise_plan: + default_enterprise_queue_specific_config = get_config( + "setup", "tasks", "celery", "enterprise", default=dict() + ) + this_queue_specific_config = get_config( + "setup", + "tasks", + get_task_group(task_name), + "enterprise", + default=default_enterprise_queue_specific_config, + ) + return { + "queue": "enterprise_" + default_task_queue, + "extra_config": this_queue_specific_config, + } + return {"queue": default_task_queue, "extra_config": {}} diff --git a/libs/shared/shared/components/__init__.py b/libs/shared/shared/components/__init__.py new file mode 100644 index 0000000000..bea35cdc72 --- /dev/null +++ b/libs/shared/shared/components/__init__.py @@ -0,0 +1,42 @@ +import re +from dataclasses import dataclass +from typing import List + + +@dataclass +class Component: + """ + Virtual representation of components defined in the user_schema yaml. + Definition: https://github.com/codecov/shared/pull/312/commits/c7bd48173da914bb16137526015791cb5a3c931c + """ + + component_id: str + name: str + flag_regexes: List[str] + paths: List[str] + statuses: List[dict] + + @classmethod + def from_dict(cls, component_dict): + return Component( + component_id=component_dict.get("component_id", ""), + name=component_dict.get("name", ""), + flag_regexes=component_dict.get("flag_regexes", []), + paths=component_dict.get("paths", []), + statuses=component_dict.get("statuses", []), + ) + + def get_display_name(self) -> str: + return self.name or self.component_id or "default_component" + + def get_matching_flags(self, current_flags: List[str]) -> List[str]: + ans = set() + compiled_regexes = map( + lambda flag_regex: re.compile(flag_regex), self.flag_regexes + ) + for regex_to_match in compiled_regexes: + matches_to_this_regex = filter( + lambda flag: regex_to_match.match(flag), current_flags + ) + ans.update(matches_to_this_regex) + return list(ans) diff --git a/libs/shared/shared/config/__init__.py b/libs/shared/shared/config/__init__.py new file mode 100644 index 0000000000..ad70ee4f01 --- /dev/null +++ b/libs/shared/shared/config/__init__.py @@ -0,0 +1,248 @@ +import collections +import json +import logging +import os +import re +from base64 import b64decode +from copy import deepcopy +from datetime import datetime +from typing import Any, List, Tuple + +from yaml import safe_load as yaml_load + +from shared.validation.install import validate_install_configuration + + +class MissingConfigException(Exception): + pass + + +log = logging.getLogger(__name__) + +LEGACY_DEFAULT_SITE_CONFIG = { + "codecov": {"require_ci_to_pass": True, "notify": {"wait_for_ci": True}}, + "coverage": { + "precision": 2, + "round": "down", + "range": "60...80", + "status": { + "project": True, + "patch": True, + "changes": False, + "default_rules": {"flag_coverage_not_uploaded_behavior": "include"}, + }, + }, + "comment": { + "layout": "reach,diff,flags,tree,reach", + "behavior": "default", + "show_carryforward_flags": False, + }, + "slack_app": True, + "github_checks": {"annotations": True}, +} + +PATCH_CENTRIC_DEFAULT_TIME_START = datetime.fromisoformat( + "2024-04-30 00:00:00.000+00:00" +) + +PATCH_CENTRIC_DEFAULT_CONFIG = { + **LEGACY_DEFAULT_SITE_CONFIG, + "coverage": { + "precision": 2, + "round": "down", + # The range is created with the transformed version (in the legacy it's transformed by validation) + # Because this bit of dict will not be validated. + "range": [60.0, 80.0], + "status": { + "project": False, + "patch": True, + "changes": False, + "default_rules": {"flag_coverage_not_uploaded_behavior": "include"}, + }, + }, + "comment": { + "layout": "condensed_header, flags, tree, component", + "hide_project_coverage": True, + "behavior": "default", + "show_carryforward_flags": False, + }, +} + +default_config = { + "services": { + "minio": { + "host": "minio", + "access_key_id": "codecov-default-key", + "secret_access_key": "codecov-default-secret", + "verify_ssl": False, + "iam_auth": False, + "iam_endpoint": None, + "hash_key": "ab164bf3f7d947f2a0681b215404873e", + }, + "database_url": "postgresql://postgres:@postgres:5432/postgres", + }, + "site": LEGACY_DEFAULT_SITE_CONFIG, + "setup": { + "timeseries": {"enabled": False}, + }, +} + + +def update(d, u): + d = deepcopy(d) + for k, v in u.items(): + if isinstance(v, collections.abc.Mapping) and isinstance( + d.get(k), collections.abc.Mapping + ): + d[k] = update(d.get(k, {}), v) + else: + d[k] = v + return d + + +class ConfigHelper(object): + def __init__(self): + self._params = None + self.loaded_files = {} + + # Load config values from environment variables + def load_env_var(self): + val = {} + for env_var in os.environ: + if not env_var.startswith("__") and "__" in env_var: + multiple_level_vars, data = self._parse_path_and_value_from_envvar( + env_var + ) + current = val + for c in multiple_level_vars[:-1]: + current = current.setdefault(c.lower(), {}) + current[multiple_level_vars[-1].lower()] = data + return val + + def _env_var_value_cast(self, data): + if isinstance(data, str): + if data in ("true", "True", "TRUE", "on", "On", "ON"): + return True + elif data in ("false", "False", "FALSE", "off", "Off", "OFF"): + return False + elif re.match(r"^-?\d+$", data): + return int(data) + elif re.match(r"^-?\d+\.\d+$", data): + try: + return float(data) + except ValueError: + pass + + return data + + def _parse_path_and_value_from_envvar( + self, env_var_name: str + ) -> Tuple[List[str], Any]: + """ + Given an envvar, calculate both the data that needs to be put in the config and + the location in the config where it needs to be set. + + For example: + ONE__TWO__THREE='value' --> { 'one': { 'two': { 'three': 'value' }}} + + Args: + env_var_name (str): The envvar we want to load data from + + Returns: + Tuple[List[str], Any]: Two elements: + - The path where the data needs to be set + - The actual data + """ + # Split env variables on "__" to get values for nested config fields + should_load_from_json = env_var_name.startswith("JSONCONFIG___") + path_to_use = env_var_name if not should_load_from_json else env_var_name[13:] + data = os.getenv(env_var_name) + data = data if not should_load_from_json else json.loads(data) + data = self._env_var_value_cast(data) + return (path_to_use.split("__"), data) + + @property + def params(self): + """ + Construct the config by combining default values, yaml config, and env vars. + An env var overrides a yaml config value, which overrides the default values. + """ + if self._params is None: + content = self.yaml_content() + env_vars = self.load_env_var() + temp_result = update(default_config, content) + unvalidated_final_result = update(temp_result, env_vars) + final_result = validate_install_configuration(unvalidated_final_result) + self.set_params(final_result) + return self._params + + def set_params(self, val): + self._params = val + + def get(self, *args, **kwargs): + current_p = self.params + for el in args: + try: + current_p = current_p[el] + except (KeyError, TypeError): + raise MissingConfigException(args) + return current_p + + def load_yaml_file(self): + yaml_path = os.getenv("CODECOV_YML", "/config/codecov.yml") + with open(yaml_path, "r") as c: + return c.read() + + def yaml_content(self): + try: + return yaml_load(self.load_yaml_file()) + except FileNotFoundError: + return {} + + def load_filename_from_path(self, *args): + if args not in self.loaded_files: + location = self.get(*args) + if isinstance(location, dict): + if location.get("source_type") == "base64env": + self.loaded_files[args] = b64decode(location.get("value")).decode() + return self.loaded_files[args] + else: + assert location.get("source_type") == "filepath" + location = location.get("value") + try: + with open(location, "r") as _file: + self.loaded_files[args] = _file.read() + except FileNotFoundError: + log.exception( + "Unable to read file specified in config", + extra=dict(file_location=location, path_args=list(args)), + ) + raise + return self.loaded_files[args] + + +config_class_instance = ConfigHelper() + + +def _get_config_instance(): + return config_class_instance + + +def get_config(*path, default=None): + config = _get_config_instance() + try: + return config.get(*path) + except MissingConfigException: + return default + + +def load_file_from_path_at_config(*args): + config = _get_config_instance() + return config.load_filename_from_path(*args) + + +def get_verify_ssl(service): + verify = get_config(service, "verify_ssl") + if verify is False: + return False + return get_config(service, "ssl_pem") or os.getenv("REQUESTS_CA_BUNDLE") diff --git a/libs/shared/shared/django_apps/__init__.py b/libs/shared/shared/django_apps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/bundle_analysis/__init__.py b/libs/shared/shared/django_apps/bundle_analysis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/bundle_analysis/migrations/0001_initial.py b/libs/shared/shared/django_apps/bundle_analysis/migrations/0001_initial.py new file mode 100644 index 0000000000..271ae4fd26 --- /dev/null +++ b/libs/shared/shared/django_apps/bundle_analysis/migrations/0001_initial.py @@ -0,0 +1,51 @@ +# Generated by Django 4.2.11 on 2024-07-10 14:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Create model CacheConfig + -- + CREATE TABLE "bundle_analysis_cacheconfig" ( + "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + "repo_id" integer NOT NULL, + "bundle_name" varchar NOT NULL, + "is_caching" boolean NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL + ); + -- + -- Create constraint unique_repo_bundle_pair on model cacheconfig + -- + ALTER TABLE "bundle_analysis_cacheconfig" ADD CONSTRAINT "unique_repo_bundle_pair" UNIQUE ( + "repo_id", "bundle_name" + ); + COMMIT; + """ + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="CacheConfig", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("repo_id", models.IntegerField()), + ("bundle_name", models.CharField()), + ("is_caching", models.BooleanField(default=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + ), + migrations.AddConstraint( + model_name="cacheconfig", + constraint=models.UniqueConstraint( + fields=("repo_id", "bundle_name"), name="unique_repo_bundle_pair" + ), + ), + ] diff --git a/libs/shared/shared/django_apps/bundle_analysis/migrations/__init__.py b/libs/shared/shared/django_apps/bundle_analysis/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/bundle_analysis/models.py b/libs/shared/shared/django_apps/bundle_analysis/models.py new file mode 100644 index 0000000000..3d0cd3bd41 --- /dev/null +++ b/libs/shared/shared/django_apps/bundle_analysis/models.py @@ -0,0 +1,21 @@ +from django.db import models + +BUNDLE_ANALYSIS_LABEL = "bundle_analysis" + + +class CacheConfig(models.Model): + id = models.BigAutoField(primary_key=True) + repo_id = models.IntegerField() + bundle_name = models.CharField() + is_caching = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + app_label = BUNDLE_ANALYSIS_LABEL + constraints = [ + models.UniqueConstraint( + name="unique_repo_bundle_pair", + fields=["repo_id", "bundle_name"], + ) + ] diff --git a/libs/shared/shared/django_apps/bundle_analysis/service/__init__.py b/libs/shared/shared/django_apps/bundle_analysis/service/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/bundle_analysis/service/bundle_analysis.py b/libs/shared/shared/django_apps/bundle_analysis/service/bundle_analysis.py new file mode 100644 index 0000000000..070dbebc6d --- /dev/null +++ b/libs/shared/shared/django_apps/bundle_analysis/service/bundle_analysis.py @@ -0,0 +1,22 @@ +from shared.django_apps.bundle_analysis.models import CacheConfig + + +class BundleAnalysisCacheConfigService: + @staticmethod + def update_cache_option(repo_id: int, name: str, is_caching: bool = True) -> None: + CacheConfig.objects.update_or_create( + repo_id=repo_id, bundle_name=name, defaults={"is_caching": is_caching} + ) + + @staticmethod + def create_if_not_exists(repo_id: int, name: str, is_caching: bool = True) -> None: + CacheConfig.objects.get_or_create( + repo_id=repo_id, bundle_name=name, defaults={"is_caching": is_caching} + ) + + @staticmethod + def get_cache_option(repo_id: int, name: str) -> bool: + cache_option = CacheConfig.objects.filter( + repo_id=repo_id, bundle_name=name + ).first() + return cache_option.is_caching if cache_option else False diff --git a/libs/shared/shared/django_apps/bundle_analysis/tests/service/test_bundle_config.py b/libs/shared/shared/django_apps/bundle_analysis/tests/service/test_bundle_config.py new file mode 100644 index 0000000000..4e9e1e3ce2 --- /dev/null +++ b/libs/shared/shared/django_apps/bundle_analysis/tests/service/test_bundle_config.py @@ -0,0 +1,138 @@ +from django.test import TestCase + +from shared.django_apps.bundle_analysis.models import CacheConfig +from shared.django_apps.bundle_analysis.service.bundle_analysis import ( + BundleAnalysisCacheConfigService, +) + + +class BundleAnalysisCacheConfigServiceTest(TestCase): + def test_bundle_config_create_then_update(self): + # Create + BundleAnalysisCacheConfigService.update_cache_option( + repo_id=1, name="bundle1", is_caching=True + ) + + query_results = CacheConfig.objects.all() + assert len(query_results) == 1 + + data = query_results[0] + create_stamp, update_stamp = data.created_at, data.updated_at + + assert data.repo_id == 1 + assert data.bundle_name == "bundle1" + assert data.is_caching == True + assert create_stamp is not None + assert update_stamp is not None + + # Update + BundleAnalysisCacheConfigService.update_cache_option( + repo_id=1, name="bundle1", is_caching=False + ) + + query_results = CacheConfig.objects.all() + assert len(query_results) == 1 + + data = query_results[0] + create_stamp_updated, update_stamp_updated = data.created_at, data.updated_at + + assert data.repo_id == 1 + assert data.bundle_name == "bundle1" + assert data.is_caching == False + assert create_stamp_updated == create_stamp + assert update_stamp_updated != update_stamp + + def test_bundle_config_create_multiple(self): + # Create 1 + BundleAnalysisCacheConfigService.update_cache_option( + repo_id=1, name="bundleA", is_caching=False + ) + + # Create 2 + BundleAnalysisCacheConfigService.update_cache_option( + repo_id=1, name="bundleB", is_caching=True + ) + + # Create 3 + BundleAnalysisCacheConfigService.update_cache_option( + repo_id=2, name="bundleA", is_caching=False + ) + + # Create 4 + BundleAnalysisCacheConfigService.update_cache_option( + repo_id=2, name="bundleB", is_caching=True + ) + + query_results = CacheConfig.objects.all() + assert len(query_results) == 4 + + def test_bundle_config_get_or_create(self): + # Create 1 -- default as is_caching=True + BundleAnalysisCacheConfigService.create_if_not_exists(repo_id=1, name="bundleA") + query_results = CacheConfig.objects.all() + assert len(query_results) == 1 + assert query_results[0].repo_id == 1 + assert query_results[0].bundle_name == "bundleA" + assert query_results[0].is_caching == True + + # Create 2 -- already exist don't change is_caching value + BundleAnalysisCacheConfigService.create_if_not_exists( + repo_id=1, name="bundleA", is_caching=False + ) + query_results = CacheConfig.objects.all() + assert len(query_results) == 1 + assert query_results[0].repo_id == 1 + assert query_results[0].bundle_name == "bundleA" + assert query_results[0].is_caching == True + + # Create 3 -- new bundle + BundleAnalysisCacheConfigService.create_if_not_exists( + repo_id=1, name="bundleB", is_caching=False + ) + query_results = CacheConfig.objects.all() + assert len(query_results) == 2 + query_results = CacheConfig.objects.filter(bundle_name="bundleA").all() + assert len(query_results) == 1 + assert query_results[0].repo_id == 1 + assert query_results[0].bundle_name == "bundleA" + assert query_results[0].is_caching == True + query_results = CacheConfig.objects.filter(bundle_name="bundleB").all() + assert len(query_results) == 1 + assert query_results[0].repo_id == 1 + assert query_results[0].bundle_name == "bundleB" + assert query_results[0].is_caching == False + + def test_bundle_config_get_cache_option(self): + # Does not exist + assert ( + BundleAnalysisCacheConfigService.get_cache_option(repo_id=1, name="bundleA") + == False + ) + + # Create + BundleAnalysisCacheConfigService.create_if_not_exists(repo_id=1, name="bundleA") + assert ( + BundleAnalysisCacheConfigService.get_cache_option(repo_id=1, name="bundleA") + == True + ) + + # Update + BundleAnalysisCacheConfigService.update_cache_option( + repo_id=1, name="bundleA", is_caching=False + ) + assert ( + BundleAnalysisCacheConfigService.get_cache_option(repo_id=1, name="bundleA") + == False + ) + + # Create another 2 bundles + BundleAnalysisCacheConfigService.create_if_not_exists( + repo_id=1, name="bundleB", is_caching=True + ) + BundleAnalysisCacheConfigService.create_if_not_exists( + repo_id=2, name="bundleA", is_caching=True + ) + assert ( + BundleAnalysisCacheConfigService.get_cache_option(repo_id=1, name="bundleA") + == False + ) diff --git a/libs/shared/shared/django_apps/codecov/__init__.py b/libs/shared/shared/django_apps/codecov/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/codecov/commands/__init__.py b/libs/shared/shared/django_apps/codecov/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/codecov/commands/exceptions.py b/libs/shared/shared/django_apps/codecov/commands/exceptions.py new file mode 100644 index 0000000000..965450c02c --- /dev/null +++ b/libs/shared/shared/django_apps/codecov/commands/exceptions.py @@ -0,0 +1,24 @@ +class BaseException(Exception): + pass + + +class Unauthenticated(BaseException): + message = "You are not authenticated" + + +class ValidationError(BaseException): + @property + def message(self): + return str(self) + + +class Unauthorized(BaseException): + message = "You are not authorized" + + +class NotFound(BaseException): + message = "Cant find the requested resource" + + +class MissingService(BaseException): + message = "Missing required service" diff --git a/libs/shared/shared/django_apps/codecov/models.py b/libs/shared/shared/django_apps/codecov/models.py new file mode 100644 index 0000000000..195b1eb6ef --- /dev/null +++ b/libs/shared/shared/django_apps/codecov/models.py @@ -0,0 +1,22 @@ +import uuid + +from django.db import models + + +class BaseCodecovModel(models.Model): + id = models.BigAutoField(primary_key=True) + external_id = models.UUIDField(default=uuid.uuid4, editable=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + + +class BaseModel(models.Model): + id = models.BigAutoField(primary_key=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True diff --git a/libs/shared/shared/django_apps/codecov_auth/__init__.py b/libs/shared/shared/django_apps/codecov_auth/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/codecov_auth/constants.py b/libs/shared/shared/django_apps/codecov_auth/constants.py new file mode 100644 index 0000000000..a27a4f230a --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/constants.py @@ -0,0 +1,5 @@ +AVATAR_GITHUB_BASE_URL = "https://avatars0.githubusercontent.com" +BITBUCKET_BASE_URL = "https://bitbucket.org" +GITLAB_BASE_URL = "https://gitlab.com" +GRAVATAR_BASE_URL = "https://www.gravatar.com" +AVATARIO_BASE_URL = "https://avatars.io" diff --git a/libs/shared/shared/django_apps/codecov_auth/helpers.py b/libs/shared/shared/django_apps/codecov_auth/helpers.py new file mode 100644 index 0000000000..e7aa2e7b3a --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/helpers.py @@ -0,0 +1,61 @@ +from traceback import format_stack + +import requests +from django.contrib.admin.models import CHANGE, LogEntry +from django.contrib.contenttypes.models import ContentType + +from shared.django_apps.codecov_auth.constants import GITLAB_BASE_URL + +GITLAB_PAYLOAD_AVATAR_URL_KEY = "avatar_url" + + +def get_gitlab_url(email, size): + res = requests.get( + "{}/api/v4/avatar?email={}&size={}".format(GITLAB_BASE_URL, email, size) + ) + url = "" + if res.status_code == 200: + data = res.json() + try: + url = data[GITLAB_PAYLOAD_AVATAR_URL_KEY] + except KeyError: + pass + + return url + + +# https://stackoverflow.com/questions/7905106/adding-a-log-entry-for-an-action-by-a-user-in-a-django-ap + + +class History: + @staticmethod + def log(objects, message, user, action_flag=None, add_traceback=False): + """ + Log an action in the admin log + :param objects: Objects being operated on + :param message: Message to log + :param user: User performing action + :param action_flag: Type of action being performed + :param add_traceback: Add the stack trace to the message + """ + if action_flag is None: + action_flag = CHANGE + + if not isinstance(objects, list): + objects = [objects] + + if add_traceback: + message = f"{message}: {format_stack()}" + + for obj in objects: + if not obj: + continue + + LogEntry.objects.log_action( + user_id=user.pk, + content_type_id=ContentType.objects.get_for_model(obj).pk, + object_repr=str(obj), + object_id=obj.ownerid, + change_message=message, + action_flag=action_flag, + ) diff --git a/libs/shared/shared/django_apps/codecov_auth/managers.py b/libs/shared/shared/django_apps/codecov_auth/managers.py new file mode 100644 index 0000000000..67dfff0bd8 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/managers.py @@ -0,0 +1,83 @@ +from django.db.models import Exists, Func, Manager, OuterRef, Q, QuerySet, Subquery +from django.db.models.functions import Coalesce + +from shared.django_apps.core.models import Pull + + +class OwnerQuerySet(QuerySet): + def users_of(self, owner): + """ + Returns users of "owner", which is defined as Owner objects + whose "organizations" field contains the "owner"s ownerid + or is one of the "owner"s "plan_activated_users". + """ + filters = Q(organizations__contains=[owner.ownerid]) + if owner.plan_activated_users: + filters = filters | Q(ownerid__in=owner.plan_activated_users) + + return self.filter(filters) + + def annotate_activated_in(self, owner): + """ + Annotates queryset with "activated" field, which is True + if a given user is activated in organization "owner", false + otherwise. + """ + from shared.django_apps.codecov_auth.models import Owner + + return self.annotate( + activated=Coalesce( + Exists( + Owner.objects.filter( + ownerid=owner.ownerid, + plan_activated_users__contains=Func( + OuterRef("ownerid"), + function="ARRAY", + template="%(function)s[%(expressions)s]", + ), + ) + ), + False, + ) + ) + + def annotate_is_admin_in(self, owner): + """ + Annotates queryset with "is_admin" field, which is True + if a given user is an admin in organization "owner", and + false otherwise. + """ + from shared.django_apps.codecov_auth.models import Owner + + return self.annotate( + is_admin=Coalesce( + Exists( + Owner.objects.filter( + ownerid=owner.ownerid, + admins__contains=Func( + OuterRef("ownerid"), + function="ARRAY", + template="%(function)s[%(expressions)s]", + ), + ) + ), + False, + ) + ) + + def annotate_last_pull_timestamp(self): + pulls = Pull.objects.filter(author=OuterRef("pk")).order_by("-updatestamp") + return self.annotate( + last_pull_timestamp=Subquery(pulls.values("updatestamp")[:1]), + ) + + +# We cannot use `QuerySet.as_manager()` since it relies on the `inspect` module and will +# not play nicely with Cython (which we use for self-hosted): +# https://cython.readthedocs.io/en/latest/src/userguide/limitations.html#inspect-support +class OwnerManager(Manager): + def get_queryset(self): + return OwnerQuerySet(self.model, using=self._db) + + def users_of(self, *args, **kwargs): + return self.get_queryset().users_of(*args, **kwargs) diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0001_initial.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0001_initial.py new file mode 100644 index 0000000000..daddd5cb52 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0001_initial.py @@ -0,0 +1,168 @@ +# Generated by Django 3.1.6 on 2021-04-08 19:21 + +import datetime +import uuid + +import django.contrib.postgres.fields +import django.contrib.postgres.fields.citext +import django.db.models.deletion +from django.contrib.postgres.operations import CITextExtension +from django.db import migrations, models + +from shared.django_apps.core.models import DateTimeWithoutTZField + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + CITextExtension(), + migrations.CreateModel( + name="User", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("email", django.contrib.postgres.fields.citext.CITextField(null=True)), + ("name", models.TextField(null=True)), + ("is_staff", models.BooleanField(default=False, null=True)), + ("is_superuser", models.BooleanField(default=False, null=True)), + ( + "external_id", + models.UUIDField(default=uuid.uuid4, editable=False, unique=True), + ), + ], + options={ + "db_table": "users", + }, + ), + migrations.CreateModel( + name="Owner", + fields=[ + ("ownerid", models.AutoField(primary_key=True, serialize=False)), + ( + "service", + models.TextField( + choices=[ + ("github", "Github"), + ("gitlab", "Gitlab"), + ("bitbucket", "Bitbucket"), + ("github_enterprise", "Github Enterprise"), + ("gitlab_enterprise", "Gitlab Enterprise"), + ("bitbucket_server", "Bitbucket Server"), + ] + ), + ), + ( + "username", + django.contrib.postgres.fields.citext.CITextField( + null=True, unique=True + ), + ), + ("email", models.TextField(null=True)), + ("name", models.TextField(null=True)), + ("oauth_token", models.TextField(null=True)), + ("stripe_customer_id", models.TextField(null=True)), + ("stripe_subscription_id", models.TextField(null=True)), + ("createstamp", models.DateTimeField(null=True)), + ("service_id", models.TextField()), + ("parent_service_id", models.TextField(null=True)), + ("root_parent_service_id", models.TextField(null=True)), + ("private_access", models.BooleanField(null=True)), + ("staff", models.BooleanField(default=False, null=True)), + ("cache", models.JSONField(null=True)), + ("plan", models.TextField(default="users-free", null=True)), + ("plan_provider", models.TextField(null=True)), + ("plan_user_count", models.SmallIntegerField(default=5, null=True)), + ("plan_auto_activate", models.BooleanField(default=True, null=True)), + ( + "plan_activated_users", + django.contrib.postgres.fields.ArrayField( + base_field=models.IntegerField(null=True), null=True, size=None + ), + ), + ("did_trial", models.BooleanField(null=True)), + ("free", models.SmallIntegerField(default=0)), + ("invoice_details", models.TextField(null=True)), + ("delinquent", models.BooleanField(null=True)), + ("yaml", models.JSONField(null=True)), + ( + "updatestamp", + DateTimeWithoutTZField(default=datetime.datetime.now), + ), + ( + "organizations", + django.contrib.postgres.fields.ArrayField( + base_field=models.IntegerField(null=True), null=True, size=None + ), + ), + ( + "admins", + django.contrib.postgres.fields.ArrayField( + base_field=models.IntegerField(null=True), null=True, size=None + ), + ), + ("integration_id", models.IntegerField(null=True)), + ( + "permission", + django.contrib.postgres.fields.ArrayField( + base_field=models.IntegerField(null=True), null=True, size=None + ), + ), + ("student", models.BooleanField(default=False)), + ("student_created_at", DateTimeWithoutTZField(null=True)), + ("student_updated_at", DateTimeWithoutTZField(null=True)), + ( + "bot", + models.ForeignKey( + db_column="bot", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="codecov_auth.owner", + ), + ), + ], + options={"db_table": "owners", "ordering": ["ownerid"]}, + ), + migrations.CreateModel( + name="Session", + fields=[ + ("sessionid", models.AutoField(primary_key=True, serialize=False)), + ( + "token", + models.UUIDField(default=uuid.uuid4, editable=False, unique=True), + ), + ("name", models.TextField(null=True)), + ("useragent", models.TextField(null=True)), + ("ip", models.TextField(null=True)), + ("lastseen", models.DateTimeField(null=True)), + ( + "type", + models.TextField(choices=[("api", "Api"), ("login", "Login")]), + ), + ( + "owner", + models.ForeignKey( + db_column="ownerid", + on_delete=django.db.models.deletion.CASCADE, + to="codecov_auth.owner", + ), + ), + ], + options={"db_table": "sessions", "ordering": ["-lastseen"]}, + ), + migrations.AddConstraint( + model_name="owner", + constraint=models.UniqueConstraint( + fields=("service", "username"), name="owner_service_username" + ), + ), + migrations.AddConstraint( + model_name="owner", + constraint=models.UniqueConstraint( + fields=("service", "service_id"), name="owner_service_ids" + ), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0002_auto_20210817_1346.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0002_auto_20210817_1346.py new file mode 100644 index 0000000000..2f7d93e731 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0002_auto_20210817_1346.py @@ -0,0 +1,43 @@ +# Generated by Django 3.1.6 on 2021-08-17 13:46 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0004_pull_user_provided_base_sha"), + ("codecov_auth", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="owner", + name="plan_provider", + field=models.TextField(choices=[("github", "Github")], null=True), + ), + migrations.CreateModel( + name="RepositoryToken", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("token_type", models.CharField(max_length=50)), + ("valid_until", models.DateTimeField(null=True)), + ("key", models.CharField(max_length=40, unique=True)), + ( + "repository", + models.ForeignKey( + db_column="repoid", + on_delete=django.db.models.deletion.CASCADE, + related_name="tokens", + to="core.repository", + ), + ), + ], + options={"abstract": False}, + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0003_auto_20210924_1003.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0003_auto_20210924_1003.py new file mode 100644 index 0000000000..c304ef4d9e --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0003_auto_20210924_1003.py @@ -0,0 +1,70 @@ +# Generated by Django 3.1.13 on 2021-09-24 10:03 + +import uuid + +import django.contrib.postgres.fields +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("codecov_auth", "0002_auto_20210817_1346")] + + operations = [ + migrations.AddField( + model_name="owner", name="business_email", field=models.TextField(null=True) + ), + migrations.AddField( + model_name="owner", + name="onboarding_completed", + field=models.BooleanField(default=False), + ), + migrations.CreateModel( + name="OwnerProfile", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "type_projects", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField( + choices=[ + ("PERSONAL", "Personal"), + ("YOUR_ORG", "Your Org"), + ("OPEN_SOURCE", "Open Source"), + ("EDUCATIONAL", "Educational"), + ] + ), + default=list, + size=None, + ), + ), + ( + "goals", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField( + choices=[ + ("STARTING_WITH_TESTS", "Starting With Tests"), + ("IMPROVE_COVERAGE", "Improve Coverage"), + ("MAINTAIN_COVERAGE", "Maintain Coverage"), + ("OTHER", "Other"), + ] + ), + default=list, + size=None, + ), + ), + ("other_goal", models.TextField(null=True)), + ( + "owner", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to="codecov_auth.owner", + ), + ), + ], + options={"abstract": False}, + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0004_auto_20210930_1429.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0004_auto_20210930_1429.py new file mode 100644 index 0000000000..566668d5e7 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0004_auto_20210930_1429.py @@ -0,0 +1,28 @@ +# Generated by Django 3.1.13 on 2021-09-30 14:29 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("codecov_auth", "0003_auto_20210924_1003")] + + operations = [ + migrations.AlterField( + model_name="ownerprofile", + name="goals", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.TextField( + choices=[ + ("STARTING_WITH_TESTS", "Starting With Tests"), + ("IMPROVE_COVERAGE", "Improve Coverage"), + ("MAINTAIN_COVERAGE", "Maintain Coverage"), + ("TEAM_REQUIREMENTS", "Team Requirements"), + ("OTHER", "Other"), + ] + ), + default=list, + size=None, + ), + ) + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0005_auto_20211029_1709.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0005_auto_20211029_1709.py new file mode 100644 index 0000000000..3e496e9f96 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0005_auto_20211029_1709.py @@ -0,0 +1,13 @@ +# Generated by Django 3.1.13 on 2021-10-29 17:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [("codecov_auth", "0004_auto_20210930_1429")] + + operations = [ + migrations.RunSQL("ALTER TYPE plans ADD VALUE IF NOT EXISTS 'users-basic';") + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0006_auto_20211123_1535.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0006_auto_20211123_1535.py new file mode 100644 index 0000000000..819dfc3a25 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0006_auto_20211123_1535.py @@ -0,0 +1,42 @@ +# Generated by Django 3.1.13 on 2021-11-23 15:35 + +import django.db.models.deletion +from django.db import migrations, models + +from shared.django_apps.codecov_auth.models import _generate_key + + +class Migration(migrations.Migration): + dependencies = [("codecov_auth", "0005_auto_20211029_1709")] + + operations = [ + migrations.AlterField( + model_name="owner", + name="plan", + field=models.TextField(default="users-basic", null=True), + ), + migrations.AlterField( + model_name="ownerprofile", + name="owner", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="profile", + to="codecov_auth.owner", + ), + ), + migrations.AlterField( + model_name="repositorytoken", + name="key", + field=models.CharField( + default=_generate_key, + editable=False, + max_length=40, + unique=True, + ), + ), + migrations.AlterField( + model_name="repositorytoken", + name="valid_until", + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0007_auto_20211129_1228.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0007_auto_20211129_1228.py new file mode 100644 index 0000000000..1df753536f --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0007_auto_20211129_1228.py @@ -0,0 +1,13 @@ +# Generated by Django 3.1.13 on 2021-11-29 12:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [("codecov_auth", "0006_auto_20211123_1535")] + + operations = [ + migrations.RunSQL( + "ALTER TABLE owners ALTER COLUMN plan SET DEFAULT 'users-basic';" + ) + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0008_auto_20220119_1811.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0008_auto_20220119_1811.py new file mode 100644 index 0000000000..c11fb67268 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0008_auto_20220119_1811.py @@ -0,0 +1,13 @@ +# Generated by Django 3.1.13 on 2022-01-19 18:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [("codecov_auth", "0007_auto_20211129_1228")] + + operations = [ + migrations.RunSQL( + "ALTER TABLE owners ALTER COLUMN onboarding_completed SET DEFAULT FALSE;" + ) + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0009_auto_20220511_1313.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0009_auto_20220511_1313.py new file mode 100644 index 0000000000..5f63bca6a1 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0009_auto_20220511_1313.py @@ -0,0 +1,76 @@ +# Generated by Django 3.1.13 on 2022-05-11 13:13 + +import django.contrib.postgres.fields +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0008_auto_20220119_1811"), + ] + + operations = [ + migrations.RunSQL( + """-- + -- Alter field bot on Owner + -- + COMMIT; + """, + state_operations=[ + migrations.AlterField( + model_name="owner", + name="bot", + field=models.ForeignKey( + blank=True, + db_column="bot", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="codecov_auth.owner", + ), + ), + ], + ), + migrations.AlterField( + model_name="owner", + name="integration_id", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name="owner", + name="plan", + field=models.TextField(blank=True, default="users-basic", null=True), + ), + migrations.AlterField( + model_name="owner", + name="plan_activated_users", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.IntegerField(null=True), + blank=True, + null=True, + size=None, + ), + ), + migrations.AlterField( + model_name="owner", + name="plan_provider", + field=models.TextField( + blank=True, choices=[("github", "Github")], null=True + ), + ), + migrations.AlterField( + model_name="owner", + name="plan_user_count", + field=models.SmallIntegerField(blank=True, default=5, null=True), + ), + migrations.AlterField( + model_name="owner", + name="stripe_customer_id", + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name="owner", + name="stripe_subscription_id", + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0010_owner_is_superuser.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0010_owner_is_superuser.py new file mode 100644 index 0000000000..ad85c08ae5 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0010_owner_is_superuser.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.13 on 2022-05-24 16:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0009_auto_20220511_1313"), + ] + + operations = [ + migrations.AddField( + model_name="owner", + name="is_superuser", + field=models.BooleanField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0011_new_enterprise_plans.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0011_new_enterprise_plans.py new file mode 100644 index 0000000000..af6836d471 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0011_new_enterprise_plans.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.12 on 2022-05-09 14:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("codecov_auth", "0010_owner_is_superuser"), + ] + + operations = [ + migrations.RunSQL( + "ALTER TYPE plans ADD VALUE IF NOT EXISTS 'users-enterprisey';" + ), + migrations.RunSQL( + "ALTER TYPE plans ADD VALUE IF NOT EXISTS 'users-enterprisem';" + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0012_auto_20220531_1452.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0012_auto_20220531_1452.py new file mode 100644 index 0000000000..26828385fd --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0012_auto_20220531_1452.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.13 on 2022-05-31 14:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0011_new_enterprise_plans"), + ] + + operations = [ + migrations.AlterField( + model_name="owner", + name="is_superuser", + field=models.BooleanField(default=False, null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0013_alter_owner_organizations.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0013_alter_owner_organizations.py new file mode 100644 index 0000000000..19f02eecb9 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0013_alter_owner_organizations.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.12 on 2022-06-22 12:05 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0012_auto_20220531_1452"), + ] + + operations = [ + migrations.AlterField( + model_name="owner", + name="organizations", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.IntegerField(null=True), + blank=True, + null=True, + size=None, + ), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0014_alter_repositorytoken_token_type.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0014_alter_repositorytoken_token_type.py new file mode 100644 index 0000000000..28204b6cd9 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0014_alter_repositorytoken_token_type.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.12 on 2022-08-16 17:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0013_alter_owner_organizations"), + ] + + operations = [ + migrations.AlterField( + model_name="repositorytoken", + name="token_type", + field=models.CharField( + choices=[("upload", "Upload"), ("profiling", "Profiling")], + max_length=50, + ), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0015_organizationleveltoken.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0015_organizationleveltoken.py new file mode 100644 index 0000000000..b710f0f96c --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0015_organizationleveltoken.py @@ -0,0 +1,42 @@ +# Generated by Django 3.2.12 on 2022-08-17 18:35 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0014_alter_repositorytoken_token_type"), + ] + + operations = [ + migrations.CreateModel( + name="OrganizationLevelToken", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("token", models.UUIDField(default=uuid.uuid4, unique=True)), + ("valid_until", models.DateTimeField(blank=True, null=True)), + ( + "token_type", + models.CharField(choices=[("upload", "Upload")], max_length=50), + ), + ( + "owner", + models.ForeignKey( + db_column="ownerid", + on_delete=django.db.models.deletion.CASCADE, + related_name="organization_tokens", + to="codecov_auth.owner", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0016_alter_owner_admins.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0016_alter_owner_admins.py new file mode 100644 index 0000000000..c9763bf2b2 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0016_alter_owner_admins.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.12 on 2022-08-22 09:43 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0015_organizationleveltoken"), + ] + + operations = [ + migrations.AlterField( + model_name="owner", + name="admins", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.IntegerField(null=True), + blank=True, + null=True, + size=None, + ), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0017_alter_organizationleveltoken_token_type.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0017_alter_organizationleveltoken_token_type.py new file mode 100644 index 0000000000..2aa73b0c00 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0017_alter_organizationleveltoken_token_type.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.12 on 2022-08-19 14:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0016_alter_owner_admins"), + ] + + operations = [ + migrations.AlterField( + model_name="organizationleveltoken", + name="token_type", + field=models.CharField( + choices=[("upload", "Upload")], default="upload", max_length=50 + ), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0018_usertoken.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0018_usertoken.py new file mode 100644 index 0000000000..31e9ed4ee1 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0018_usertoken.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.12 on 2022-09-07 17:38 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0017_alter_organizationleveltoken_token_type"), + ] + + operations = [ + migrations.CreateModel( + name="UserToken", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("name", models.CharField(max_length=100)), + ("token", models.UUIDField(default=uuid.uuid4, unique=True)), + ("valid_until", models.DateTimeField(blank=True, null=True)), + ( + "token_type", + models.CharField( + choices=[("api", "Api")], default="api", max_length=50 + ), + ), + ( + "owner", + models.ForeignKey( + db_column="ownerid", + on_delete=django.db.models.deletion.CASCADE, + related_name="user_tokens", + to="codecov_auth.owner", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0019_alter_repositorytoken_token_type.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0019_alter_repositorytoken_token_type.py new file mode 100644 index 0000000000..8190d81720 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0019_alter_repositorytoken_token_type.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.12 on 2022-12-06 04:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0018_usertoken"), + ] + + operations = [ + migrations.AlterField( + model_name="repositorytoken", + name="token_type", + field=models.CharField( + choices=[ + ("upload", "Upload"), + ("profiling", "Profiling"), + ("static_analysis", "Static Analysis"), + ], + max_length=50, + ), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0020_ownerprofile_default_org.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0020_ownerprofile_default_org.py new file mode 100644 index 0000000000..ce7d55b420 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0020_ownerprofile_default_org.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.12 on 2023-01-19 19:06 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + # BEGIN; + # -- + # -- Add field default_org to ownerprofile + # -- + # ALTER TABLE "codecov_auth_ownerprofile" ADD COLUMN "default_org_id" integer NULL CONSTRAINT "codecov_auth_ownerpr_default_org_id_da545ea8_fk_owners_ow" REFERENCES "owners"("ownerid") DEFERRABLE INITIALLY DEFERRED; SET CONSTRAINTS "codecov_auth_ownerpr_default_org_id_da545ea8_fk_owners_ow" IMMEDIATE; + # CREATE INDEX "codecov_auth_ownerprofile_default_org_id_da545ea8" ON "codecov_auth_ownerprofile" ("default_org_id"); + # COMMIT; + + dependencies = [ + ("codecov_auth", "0019_alter_repositorytoken_token_type"), + ] + + operations = [ + migrations.AddField( + model_name="ownerprofile", + name="default_org", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="profiles_with_default", + to="codecov_auth.owner", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0021_owner_max_upload_limit.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0021_owner_max_upload_limit.py new file mode 100644 index 0000000000..c6ce8eaabf --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0021_owner_max_upload_limit.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.12 on 2023-02-13 19:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0020_ownerprofile_default_org"), + ] + + operations = [ + migrations.AddField( + model_name="owner", + name="max_upload_limit", + field=models.IntegerField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0022_alter_owner_max_upload_limit.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0022_alter_owner_max_upload_limit.py new file mode 100644 index 0000000000..f6a494e1aa --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0022_alter_owner_max_upload_limit.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.12 on 2023-02-13 20:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0021_owner_max_upload_limit"), + ] + + operations = [ + migrations.AlterField( + model_name="owner", + name="max_upload_limit", + field=models.IntegerField(default=150, null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0023_auto_20230214_1129.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0023_auto_20230214_1129.py new file mode 100644 index 0000000000..2209237ccf --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0023_auto_20230214_1129.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.12 on 2023-02-14 11:29 + +from django.db import migrations + +from shared.django_apps.migration_utils import RiskyRunSQL + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0022_alter_owner_max_upload_limit"), + ] + + operations = [ + migrations.RunSQL( + "ALTER TABLE owners ALTER COLUMN max_upload_limit SET DEFAULT 150;" + ), + RiskyRunSQL( + "UPDATE owners SET max_upload_limit=150 WHERE max_upload_limit is null;" + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0024_alter_owner_max_upload_limit.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0024_alter_owner_max_upload_limit.py new file mode 100644 index 0000000000..fe960ef400 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0024_alter_owner_max_upload_limit.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.12 on 2023-02-23 11:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0023_auto_20230214_1129"), + ] + + operations = [ + migrations.AlterField( + model_name="owner", + name="max_upload_limit", + field=models.IntegerField(blank=True, default=150, null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0025_owner_stripe_coupon_id.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0025_owner_stripe_coupon_id.py new file mode 100644 index 0000000000..c60a2606ff --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0025_owner_stripe_coupon_id.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.12 on 2023-02-22 19:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Add field stripe_coupon_id to owner + -- + ALTER TABLE "owners" ADD COLUMN "stripe_coupon_id" text NULL; + COMMIT; + """ + + dependencies = [ + ("codecov_auth", "0024_alter_owner_max_upload_limit"), + ] + + operations = [ + migrations.AddField( + model_name="owner", + name="stripe_coupon_id", + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0026_alter_owner_plan_user_count.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0026_alter_owner_plan_user_count.py new file mode 100644 index 0000000000..1c49b2586a --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0026_alter_owner_plan_user_count.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.7 on 2023-03-09 20:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0025_owner_stripe_coupon_id"), + ] + + operations = [ + migrations.AlterField( + model_name="owner", + name="plan_user_count", + field=models.SmallIntegerField(blank=True, default=1, null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0027_auto_20230307_1751.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0027_auto_20230307_1751.py new file mode 100644 index 0000000000..f69089b05a --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0027_auto_20230307_1751.py @@ -0,0 +1,14 @@ +# Generated by Django 4.1.7 on 2023-03-07 17:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0026_alter_owner_plan_user_count"), + ] + + operations = [ + migrations.RunSQL("ALTER TYPE plans ADD VALUE IF NOT EXISTS 'users-sentrym';"), + migrations.RunSQL("ALTER TYPE plans ADD VALUE IF NOT EXISTS 'users-sentryy';"), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0028_owner_sentry_user_data_owner_sentry_user_id.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0028_owner_sentry_user_data_owner_sentry_user_id.py new file mode 100644 index 0000000000..3d13a5379d --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0028_owner_sentry_user_data_owner_sentry_user_id.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1.7 on 2023-03-07 22:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0027_auto_20230307_1751"), + ] + + operations = [ + migrations.AddField( + model_name="owner", + name="sentry_user_data", + field=models.JSONField(null=True), + ), + migrations.AddField( + model_name="owner", + name="sentry_user_id", + field=models.TextField(blank=True, null=True, unique=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0029_ownerprofile_terms_agreement_and_more.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0029_ownerprofile_terms_agreement_and_more.py new file mode 100644 index 0000000000..b40f23f8d2 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0029_ownerprofile_terms_agreement_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.7 on 2023-03-17 22:01 + +from django.db import migrations, models + +from shared.django_apps.core.models import DateTimeWithoutTZField + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0028_owner_sentry_user_data_owner_sentry_user_id"), + ] + + operations = [ + migrations.AddField( + model_name="ownerprofile", + name="terms_agreement", + field=models.BooleanField(null=True), + ), + migrations.AddField( + model_name="ownerprofile", + name="terms_agreement_at", + field=DateTimeWithoutTZField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0030_owner_trial_end_date_owner_trial_start_date.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0030_owner_trial_end_date_owner_trial_start_date.py new file mode 100644 index 0000000000..02b636b4ee --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0030_owner_trial_end_date_owner_trial_start_date.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.7 on 2023-06-20 17:14 + +from django.db import migrations + +from shared.django_apps.core.models import DateTimeWithoutTZField + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0029_ownerprofile_terms_agreement_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="owner", + name="trial_end_date", + field=DateTimeWithoutTZField(null=True), + ), + migrations.AddField( + model_name="owner", + name="trial_start_date", + field=DateTimeWithoutTZField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0031_user_owner_user.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0031_user_owner_user.py new file mode 100644 index 0000000000..e80b4c91cc --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0031_user_owner_user.py @@ -0,0 +1,67 @@ +# Generated by Django 4.1.7 on 2023-05-22 17:53 + + +import django.contrib.postgres.fields.citext +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0030_owner_trial_end_date_owner_trial_start_date"), + ] + + operations = [ + # NOTE: this migration had to be moved to the `0001_initial` migration + # since there are internal Django migrations that need to refer to this + # model via AUTH_USER_MODEL. + # It is not actually applied there since our `legacy_migrations` override + # the `migrate` command and mark 0001_initial migrations as fake. The + # raw SQL to create this table is in `legacy_migrations` and needs to be applied + # manually before running this migration. We have a raw SQL migration below to + # create the table if it does not already exist. + # + # migrations.CreateModel( + # name='User', + # fields=[ + # ('id', models.BigAutoField(primary_key=True, serialize=False)), + # ('created_at', models.DateTimeField(auto_now_add=True)), + # ('updated_at', models.DateTimeField(auto_now=True)), + # ('email', django.contrib.postgres.fields.citext.CITextField(null=True)), + # ('name', models.TextField(null=True)), + # ('is_staff', models.BooleanField(default=False, null=True)), + # ('is_superuser', models.BooleanField(default=False, null=True)), + # ('external_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + # ], + # options={ + # 'db_table': 'users', + # }, + # ), + migrations.RunSQL( + """ + CREATE TABLE IF NOT EXISTS "users" ( + "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + "external_id" uuid NOT NULL UNIQUE, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "email" citext NULL, + "name" text NULL, + "is_staff" boolean NULL, + "is_superuser" boolean NULL + ); + """, + reverse_sql="DROP TABLE users", + ), + migrations.AddField( + model_name="owner", + name="user", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="owners", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0032_owner_trial_status.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0032_owner_trial_status.py new file mode 100644 index 0000000000..8f9d485a11 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0032_owner_trial_status.py @@ -0,0 +1,27 @@ +# Generated by Django 4.1.7 on 2023-07-20 00:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0031_user_owner_user"), + ] + + operations = [ + migrations.RunSQL("ALTER TYPE plans ADD VALUE IF NOT EXISTS 'users-trial';"), + migrations.AddField( + model_name="owner", + name="trial_status", + field=models.CharField( + choices=[ + ("not_started", "Not Started"), + ("ongoing", "Ongoing"), + ("expired", "Expired"), + ("cannot_trial", "Cannot Trial"), + ], + max_length=50, + null=True, + ), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0033_sentryuser.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0033_sentryuser.py new file mode 100644 index 0000000000..6ac44a00cb --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0033_sentryuser.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.2 on 2023-07-06 16:05 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0032_owner_trial_status"), + ] + + operations = [ + migrations.CreateModel( + name="SentryUser", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("access_token", models.TextField(null=True)), + ("refresh_token", models.TextField(null=True)), + ("sentry_id", models.TextField(unique=True)), + ("email", models.TextField(null=True)), + ("name", models.TextField(null=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="sentry_user", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0034_alter_owner_trial_status.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0034_alter_owner_trial_status.py new file mode 100644 index 0000000000..8478f1f41f --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0034_alter_owner_trial_status.py @@ -0,0 +1,41 @@ +# Generated by Django 4.1.7 on 2023-07-27 00:38 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyRunSQL + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Alter field trial_status on owner + -- + -- (no-op) + COMMIT; + """ + + dependencies = [ + ("codecov_auth", "0033_sentryuser"), + ] + + operations = [ + migrations.AlterField( + model_name="owner", + name="trial_status", + field=models.CharField( + choices=[ + ("not_started", "Not Started"), + ("ongoing", "Ongoing"), + ("expired", "Expired"), + ("cannot_trial", "Cannot Trial"), + ], + default="not_started", + max_length=50, + null=True, + ), + ), + RiskyRunSQL( + "alter table owners alter column trial_status set default 'not_started';" + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0035_owner_pretrial_users_count.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0035_owner_pretrial_users_count.py new file mode 100644 index 0000000000..73b7e217b3 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0035_owner_pretrial_users_count.py @@ -0,0 +1,28 @@ +# Generated by Django 4.1.7 on 2023-07-27 23:40 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddField + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Add field pretrial_users_count to owner + -- + ALTER TABLE "owners" ADD COLUMN "pretrial_users_count" smallint NULL; + COMMIT; + """ + + dependencies = [ + ("codecov_auth", "0034_alter_owner_trial_status"), + ] + + operations = [ + RiskyAddField( + model_name="owner", + name="pretrial_users_count", + field=models.SmallIntegerField(blank=True, null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0036_add_user_terms_agreement.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0036_add_user_terms_agreement.py new file mode 100644 index 0000000000..7bb058dc17 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0036_add_user_terms_agreement.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.2 on 2023-08-30 13:27 + +from django.db import migrations, models + +from shared.django_apps.core.models import DateTimeWithoutTZField + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Add field terms_agreement to user + -- + ALTER TABLE "users" ADD COLUMN "terms_agreement" boolean NULL; + -- + -- Add field terms_agreement_at to user + -- + ALTER TABLE "users" ADD COLUMN "terms_agreement_at" timestamp NULL; + COMMIT; + """ + + dependencies = [ + ("codecov_auth", "0035_owner_pretrial_users_count"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="terms_agreement", + field=models.BooleanField(null=True), + ), + migrations.AddField( + model_name="user", + name="terms_agreement_at", + field=DateTimeWithoutTZField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0037_owner_uses_invoice.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0037_owner_uses_invoice.py new file mode 100644 index 0000000000..61eae6e752 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0037_owner_uses_invoice.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.2 on 2023-08-17 20:59 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddField + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0036_add_user_terms_agreement"), + ] + + operations = [ + RiskyAddField( + model_name="owner", + name="uses_invoice", + field=models.BooleanField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0038_alter_owner_uses_invoice.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0038_alter_owner_uses_invoice.py new file mode 100644 index 0000000000..b8b77a0797 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0038_alter_owner_uses_invoice.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.2 on 2023-08-28 18:27 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAlterField, RiskyRunSQL + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0037_owner_uses_invoice"), + ] + + operations = [ + RiskyAlterField( + model_name="owner", + name="uses_invoice", + field=models.BooleanField(default=False, null=True), + ), + RiskyRunSQL( + """ + UPDATE "owners" SET "uses_invoice" = false WHERE "uses_invoice" IS NULL; + ALTER TABLE "owners" ALTER COLUMN "uses_invoice" SET DEFAULT false; + """ + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0039_alter_owner_uses_invoice.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0039_alter_owner_uses_invoice.py new file mode 100644 index 0000000000..949d491406 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0039_alter_owner_uses_invoice.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.2 on 2023-08-28 17:42 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAlterField + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0038_alter_owner_uses_invoice"), + ] + + operations = [ + RiskyAlterField( + model_name="owner", + name="uses_invoice", + field=models.BooleanField(default=False, null=False), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0040_oktauser.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0040_oktauser.py new file mode 100644 index 0000000000..2d1b8e051c --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0040_oktauser.py @@ -0,0 +1,40 @@ +# Generated by Django 4.2.2 on 2023-07-25 18:08 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0039_alter_owner_uses_invoice"), + ] + + operations = [ + migrations.CreateModel( + name="OktaUser", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("access_token", models.TextField(null=True)), + ("okta_id", models.TextField(unique=True)), + ("email", models.TextField(null=True)), + ("name", models.TextField(null=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="okta_user", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0041_auto_20230918_1825.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0041_auto_20230918_1825.py new file mode 100644 index 0000000000..cfd30b7ab2 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0041_auto_20230918_1825.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.2 on 2023-09-18 18:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + # BEGIN; + # -- + # -- Raw SQL operation + # -- + # ALTER TYPE plans ADD VALUE IF NOT EXISTS 'users-litem'; + # -- + # -- Raw SQL operation + # -- + # ALTER TYPE plans ADD VALUE IF NOT EXISTS 'users-litey'; + # COMMIT; + + dependencies = [ + ("codecov_auth", "0040_oktauser"), + ] + + operations = [ + migrations.RunSQL("ALTER TYPE plans ADD VALUE IF NOT EXISTS 'users-litem';"), + migrations.RunSQL("ALTER TYPE plans ADD VALUE IF NOT EXISTS 'users-litey';"), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0042_owner_trial_fired_by.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0042_owner_trial_fired_by.py new file mode 100644 index 0000000000..caa927264e --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0042_owner_trial_fired_by.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.3 on 2023-09-19 09:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0041_auto_20230918_1825"), + ] + + operations = [ + migrations.AddField( + model_name="owner", + name="trial_fired_by", + field=models.IntegerField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0043_sync_user_terms_agreement.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0043_sync_user_terms_agreement.py new file mode 100644 index 0000000000..4e59160e25 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0043_sync_user_terms_agreement.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.2 on 2023-09-18 14:51 + + +from django.db import migrations + +from shared.django_apps.migration_utils import RiskyRunSQL + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0042_owner_trial_fired_by"), + ] + + operations = [ + RiskyRunSQL( + sql=""" + UPDATE users + SET + terms_agreement = subquery.terms_agreement, + terms_agreement_at = subquery.terms_agreement_at + FROM ( + SELECT + owners.user_id, + codecov_auth_ownerprofile.terms_agreement, + codecov_auth_ownerprofile.terms_agreement_at + FROM owners + INNER JOIN codecov_auth_ownerprofile + ON codecov_auth_ownerprofile.owner_id = owners.ownerid + ) subquery + WHERE subquery.user_id = users.id; + """, + reverse_sql=migrations.RunSQL.noop, + ) + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0044_remove_owner_agreements_and_alter_user_agreements.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0044_remove_owner_agreements_and_alter_user_agreements.py new file mode 100644 index 0000000000..4868d1cee5 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0044_remove_owner_agreements_and_alter_user_agreements.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.3 on 2023-09-19 20:52 + +from django.db import migrations, models + +from shared.django_apps.core.models import DateTimeWithoutTZField + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0043_sync_user_terms_agreement"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="terms_agreement", + field=models.BooleanField(blank=True, default=False, null=True), + ), + migrations.AlterField( + model_name="user", + name="terms_agreement_at", + field=DateTimeWithoutTZField(blank=True, null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0045_remove_ownerprofile_terms_agreement.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0045_remove_ownerprofile_terms_agreement.py new file mode 100644 index 0000000000..fa9f21a232 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0045_remove_ownerprofile_terms_agreement.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.3 on 2023-09-21 14:24 + +from django.db import migrations + +from shared.django_apps.migration_utils import RiskyRemoveField + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Remove field terms_agreement from ownerprofile + -- + ALTER TABLE "codecov_auth_ownerprofile" DROP COLUMN "terms_agreement" CASCADE; + -- + -- Remove field terms_agreement_at from ownerprofile + -- + ALTER TABLE "codecov_auth_ownerprofile" DROP COLUMN "terms_agreement_at" CASCADE; + COMMIT; + """ + + dependencies = [ + ("codecov_auth", "0044_remove_owner_agreements_and_alter_user_agreements"), + ] + + operations = [ + RiskyRemoveField( + model_name="ownerprofile", + name="terms_agreement", + ), + RiskyRemoveField( + model_name="ownerprofile", + name="terms_agreement_at", + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0046_dedupe_owner_admin_values.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0046_dedupe_owner_admin_values.py new file mode 100644 index 0000000000..3e02addeb9 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0046_dedupe_owner_admin_values.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.3 on 2023-09-19 19:48 + + +from django.db import migrations + +from shared.django_apps.migration_utils import RiskyRunSQL + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0045_remove_ownerprofile_terms_agreement"), + ] + + operations = [ + RiskyRunSQL( + sql=""" + UPDATE owners + SET admins = ARRAY ( + SELECT v + FROM unnest(admins) WITH ORDINALITY t(v,ord) + GROUP BY 1 + ORDER BY min(ord) + ); + """, + reverse_sql=migrations.RunSQL.noop, + ) + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0047_auto_20231009_1257.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0047_auto_20231009_1257.py new file mode 100644 index 0000000000..b598e96e12 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0047_auto_20231009_1257.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.3 on 2023-10-09 12:57 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0046_dedupe_owner_admin_values"), + ] + + operations = [ + migrations.RunSQL( + "ALTER TYPE plans ADD VALUE IF NOT EXISTS 'users-teamm';", + reverse_sql=migrations.RunSQL.noop, + ), + migrations.RunSQL( + "ALTER TYPE plans ADD VALUE IF NOT EXISTS 'users-teamy';", + reverse_sql=migrations.RunSQL.noop, + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0048_githubappinstallation.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0048_githubappinstallation.py new file mode 100644 index 0000000000..e95e4966f1 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0048_githubappinstallation.py @@ -0,0 +1,60 @@ +# Generated by Django 4.2.7 on 2024-01-17 13:37 + +import uuid + +import django.contrib.postgres.fields +import django.db.models.deletion +import django_prometheus.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0047_auto_20231009_1257"), + ] + + # BEGIN; + # -- + # -- Create model GithubAppInstallation + # -- + # CREATE TABLE "codecov_auth_githubappinstallation" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "external_id" uuid NOT NULL, "created_at" timestamp with time zone NOT NULL, "updated_at" timestamp with time zone NOT NULL, "installation_id" integer NOT NULL, "name" text NOT NULL, "repository_service_ids" text[] NULL, "owner_id" integer NOT NULL); + # ALTER TABLE "codecov_auth_githubappinstallation" ADD CONSTRAINT "codecov_auth_githuba_owner_id_82ba29b1_fk_owners_ow" FOREIGN KEY ("owner_id") REFERENCES "owners" ("ownerid") DEFERRABLE INITIALLY DEFERRED; + # CREATE INDEX "codecov_auth_githubappinstallation_owner_id_82ba29b1" ON "codecov_auth_githubappinstallation" ("owner_id"); + # COMMIT; + + operations = [ + migrations.CreateModel( + name="GithubAppInstallation", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("installation_id", models.IntegerField()), + ("name", models.TextField(default="codecov_app_installation")), + ( + "repository_service_ids", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), null=True, size=None + ), + ), + ( + "owner", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="github_app_installations", + to="codecov_auth.owner", + ), + ), + ], + options={ + "abstract": False, + }, + bases=( + django_prometheus.models.ExportModelOperationsMixin( + "codecov_auth.github_app_installation" + ), + models.Model, + ), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0049_ownerprofile_customer_intent.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0049_ownerprofile_customer_intent.py new file mode 100644 index 0000000000..6069105f49 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0049_ownerprofile_customer_intent.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.7 on 2024-02-09 19:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0048_githubappinstallation"), + ] + + # BEGIN; + # -- + # -- Add field customer_intent to ownerprofile + # -- + # ALTER TABLE "codecov_auth_ownerprofile" ADD COLUMN "customer_intent" text NULL; + # COMMIT; + operations = [ + migrations.AddField( + model_name="ownerprofile", + name="customer_intent", + field=models.TextField( + choices=[("BUSINESS", "Business"), ("PERSONAL", "Personal")], null=True + ), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0050_remove_ownerprofile_customer_intent.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0050_remove_ownerprofile_customer_intent.py new file mode 100644 index 0000000000..cbd2e5b806 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0050_remove_ownerprofile_customer_intent.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.7 on 2024-02-13 21:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0049_ownerprofile_customer_intent"), + ] + + # BEGIN; + # -- + # -- Remove field customer_intent from ownerprofile + # -- + # ALTER TABLE "codecov_auth_ownerprofile" DROP COLUMN "customer_intent" CASCADE; + # COMMIT; + operations = [ + migrations.RemoveField( + model_name="ownerprofile", + name="customer_intent", + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0051_user_customer_intent.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0051_user_customer_intent.py new file mode 100644 index 0000000000..9ee1b42880 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0051_user_customer_intent.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.7 on 2024-02-14 14:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0050_remove_ownerprofile_customer_intent"), + ] + + # BEGIN; + # -- + # -- Add field customer_intent to user + # -- + # ALTER TABLE "users" ADD COLUMN "customer_intent" text NULL; + # COMMIT; + operations = [ + migrations.AddField( + model_name="user", + name="customer_intent", + field=models.TextField( + choices=[("BUSINESS", "Business"), ("PERSONAL", "Personal")], null=True + ), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0052_githubappinstallation_app_id_and_more.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0052_githubappinstallation_app_id_and_more.py new file mode 100644 index 0000000000..699c00cc5f --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0052_githubappinstallation_app_id_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.7 on 2024-02-19 14:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + # BEGIN; + # -- + # -- Add field app_id to githubappinstallation + # -- + # ALTER TABLE "codecov_auth_githubappinstallation" ADD COLUMN "app_id" integer NULL; + # -- + # -- Add field pem_path to githubappinstallation + # -- + # ALTER TABLE "codecov_auth_githubappinstallation" ADD COLUMN "pem_path" text NULL; + # COMMIT; + + dependencies = [ + ("codecov_auth", "0051_user_customer_intent"), + ] + + operations = [ + migrations.AddField( + model_name="githubappinstallation", + name="app_id", + field=models.IntegerField(null=True), + ), + migrations.AddField( + model_name="githubappinstallation", + name="pem_path", + field=models.TextField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0053_ownerinstallationnametousefortask_and_more.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0053_ownerinstallationnametousefortask_and_more.py new file mode 100644 index 0000000000..e37358e508 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0053_ownerinstallationnametousefortask_and_more.py @@ -0,0 +1,62 @@ +# Generated by Django 4.2.7 on 2024-02-21 16:03 + +import uuid + +import django.db.models.deletion +import django_prometheus.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0052_githubappinstallation_app_id_and_more"), + ] + + # BEGIN; + # -- + # -- Create model OwnerInstallationNameToUseForTask + # -- + # CREATE TABLE "codecov_auth_ownerinstallationnametousefortask" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "external_id" uuid NOT NULL, "created_at" timestamp with time zone NOT NULL, "updated_at" timestamp with time zone NOT NULL, "installation_name" text NOT NULL, "task_name" text NOT NULL, "owner_id" integer NOT NULL); + # -- + # -- Create constraint single_task_name_per_owner on model ownerinstallationnametousefortask + # -- + # CREATE UNIQUE INDEX "single_task_name_per_owner" ON "codecov_auth_ownerinstallationnametousefortask" ("owner_id", "task_name"); + # ALTER TABLE "codecov_auth_ownerinstallationnametousefortask" ADD CONSTRAINT "codecov_auth_ownerin_owner_id_8bf0ce9b_fk_owners_ow" FOREIGN KEY ("owner_id") REFERENCES "owners" ("ownerid") DEFERRABLE INITIALLY DEFERRED; + # CREATE INDEX "codecov_auth_ownerinstalla_owner_id_8bf0ce9b" ON "codecov_auth_ownerinstallationnametousefortask" ("owner_id"); + # COMMIT; + + operations = [ + migrations.CreateModel( + name="OwnerInstallationNameToUseForTask", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("installation_name", models.TextField()), + ("task_name", models.TextField()), + ( + "owner", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="installation_name_to_use_for_tasks", + to="codecov_auth.owner", + ), + ), + ], + bases=( + django_prometheus.models.ExportModelOperationsMixin( + "codecov_auth.github_app_installation" + ), + models.Model, + ), + ), + migrations.AddConstraint( + model_name="ownerinstallationnametousefortask", + constraint=models.UniqueConstraint( + models.F("owner_id"), + models.F("task_name"), + name="single_task_name_per_owner", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0054_update_owners_column_defaults.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0054_update_owners_column_defaults.py new file mode 100644 index 0000000000..c657c4212b --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0054_update_owners_column_defaults.py @@ -0,0 +1,42 @@ +# Generated by Django 4.2.11 on 2024-03-28 19:25 + +from django.db import migrations + +from shared.django_apps.migration_utils import RiskyRunSQL + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0053_ownerinstallationnametousefortask_and_more"), + ] + + operations = [ + RiskyRunSQL( + "ALTER TABLE owners ALTER COLUMN plan_user_count SET DEFAULT 1;", + reverse_sql="ALTER TABLE owners ALTER COLUMN plan_user_count SET DEFAULT NULL;", + ), + RiskyRunSQL( + "ALTER TABLE owners ALTER COLUMN updatestamp SET DEFAULT now();", + reverse_sql="ALTER TABLE owners ALTER COLUMN updatestamp SET DEFAULT NULL;", + ), + RiskyRunSQL( + "ALTER TABLE owners ALTER COLUMN is_superuser SET DEFAULT false;", + reverse_sql="ALTER TABLE owners ALTER COLUMN is_superuser SET DEFAULT NULL;", + ), + RiskyRunSQL( + "ALTER TABLE owners ALTER COLUMN createstamp SET DEFAULT now();", + reverse_sql="ALTER TABLE owners ALTER COLUMN createstamp SET DEFAULT NULL;", + ), + RiskyRunSQL( + "UPDATE owners SET plan_user_count=1 WHERE plan_user_count IS NULL;", + reverse_sql=migrations.RunSQL.noop, + ), + RiskyRunSQL( + "UPDATE owners SET updatestamp=now() WHERE updatestamp IS NULL;", + reverse_sql=migrations.RunSQL.noop, + ), + RiskyRunSQL( + "UPDATE owners SET is_superuser=false WHERE is_superuser IS NULL;", + reverse_sql=migrations.RunSQL.noop, + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0055_session_login_session.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0055_session_login_session.py new file mode 100644 index 0000000000..094ae27043 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0055_session_login_session.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.11 on 2024-04-11 18:45 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Add field login_session to session + -- + ALTER TABLE "sessions" ADD COLUMN "login_session_id" varchar(40) NULL CONSTRAINT "sessions_login_session_id_4c8453b4_fk_django_se" REFERENCES "django_session"("session_key") DEFERRABLE INITIALLY DEFERRED; SET CONSTRAINTS "sessions_login_session_id_4c8453b4_fk_django_se" IMMEDIATE; + CREATE INDEX "sessions_login_session_id_4c8453b4" ON "sessions" ("login_session_id"); + CREATE INDEX "sessions_login_session_id_4c8453b4_like" ON "sessions" ("login_session_id" varchar_pattern_ops); + COMMIT; + """ + + dependencies = [ + ("sessions", "0001_initial"), + ("codecov_auth", "0054_update_owners_column_defaults"), + ] + + operations = [ + migrations.AddField( + model_name="session", + name="login_session", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="sessions.session", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0056_githubappinstallation_is_suspended.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0056_githubappinstallation_is_suspended.py new file mode 100644 index 0000000000..dd21063192 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0056_githubappinstallation_is_suspended.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.13 on 2024-06-03 13:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0055_session_login_session"), + ] + + # BEGIN; + # -- + # -- Add field is_suspended to githubappinstallation + # -- + # ALTER TABLE "codecov_auth_githubappinstallation" ADD COLUMN "is_suspended" boolean DEFAULT false NOT NULL; + # ALTER TABLE "codecov_auth_githubappinstallation" ALTER COLUMN "is_suspended" DROP DEFAULT; + # COMMIT; + + operations = [ + migrations.AddField( + model_name="githubappinstallation", + name="is_suspended", + field=models.BooleanField(default=False), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0057_account_stripebilling_oktasettings_invoicebilling_and_more.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0057_account_stripebilling_oktasettings_invoicebilling_and_more.py new file mode 100644 index 0000000000..1fdda7e867 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0057_account_stripebilling_oktasettings_invoicebilling_and_more.py @@ -0,0 +1,164 @@ +# Generated by Django 4.2.13 on 2024-06-18 00:16 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0056_githubappinstallation_is_suspended"), + ] + + operations = [ + migrations.CreateModel( + name="Account", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("name", models.CharField(max_length=100, unique=True)), + ("is_active", models.BooleanField(blank=True, default=True)), + ( + "plan", + models.CharField( + choices=[ + ("users-basic", "BASIC_PLAN_NAME"), + ("users-trial", "TRIAL_PLAN_NAME"), + ("users-pr-inappm", "CODECOV_PRO_MONTHLY"), + ("users-pr-inappy", "CODECOV_PRO_YEARLY"), + ("users-sentrym", "SENTRY_MONTHLY"), + ("users-sentryy", "SENTRY_YEARLY"), + ("users-teamm", "TEAM_MONTHLY"), + ("users-teamy", "TEAM_YEARLY"), + ("users", "GHM_PLAN_NAME"), + ("users-free", "FREE_PLAN_NAME"), + ("users-inappm", "CODECOV_PRO_MONTHLY_LEGACY"), + ("users-inappy", "CODECOV_PRO_YEARLY_LEGACY"), + ("users-enterprisem", "ENTERPRISE_CLOUD_MONTHLY"), + ("users-enterprisey", "ENTERPRISE_CLOUD_YEARLY"), + ], + default="users-basic", + max_length=50, + ), + ), + ("plan_seat_count", models.SmallIntegerField(blank=True, default=1)), + ("free_seat_count", models.SmallIntegerField(blank=True, default=0)), + ("plan_auto_activate", models.BooleanField(blank=True, default=True)), + ("is_delinquent", models.BooleanField(blank=True, default=False)), + ], + options={ + "ordering": ["-updated_at"], + }, + ), + migrations.CreateModel( + name="StripeBilling", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("customer_id", models.CharField(max_length=255, unique=True)), + ( + "subscription_id", + models.CharField(blank=True, max_length=255, null=True), + ), + ("is_active", models.BooleanField(blank=True, default=True)), + ( + "account", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="stripe_billing", + to="codecov_auth.account", + unique=True, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="OktaSettings", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("client_id", models.CharField(max_length=255)), + ("client_secret", models.CharField(max_length=255)), + ("url", models.CharField(max_length=255)), + ("enabled", models.BooleanField(blank=True, default=True)), + ("enforced", models.BooleanField(blank=True, default=True)), + ( + "account", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="okta_settings", + to="codecov_auth.account", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="InvoiceBilling", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "account_manager", + models.CharField(blank=True, max_length=255, null=True), + ), + ("invoice_notes", models.TextField(blank=True, null=True)), + ("is_active", models.BooleanField(blank=True, default=True)), + ( + "account", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="invoice_billing", + to="codecov_auth.account", + unique=True, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="AccountsUsers", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "account", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="codecov_auth.account", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "unique_together": {("user", "account")}, + }, + ), + migrations.AddField( + model_name="account", + name="users", + field=models.ManyToManyField( + related_name="accounts", + through="codecov_auth.AccountsUsers", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0058_owner_account.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0058_owner_account.py new file mode 100644 index 0000000000..e24882d265 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0058_owner_account.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.13 on 2024-06-25 18:19 + +import django.db.models.deletion +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddField + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Add field account to owner + -- + ALTER TABLE "owners" ADD COLUMN "account_id" bigint NULL CONSTRAINT "owners_account_id_850ef729_fk_codecov_auth_account_id" REFERENCES "codecov_auth_account"("id") DEFERRABLE INITIALLY DEFERRED; SET CONSTRAINTS "owners_account_id_850ef729_fk_codecov_auth_account_id" IMMEDIATE; + CREATE INDEX "owners_account_id_850ef729" ON "owners" ("account_id"); + COMMIT; + """ + + dependencies = [ + ( + "codecov_auth", + "0057_account_stripebilling_oktasettings_invoicebilling_and_more", + ), + ] + + operations = [ + RiskyAddField( + model_name="owner", + name="account", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="organizations", + to="codecov_auth.account", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0059_alter_accountsusers_options.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0059_alter_accountsusers_options.py new file mode 100644 index 0000000000..87dea052d9 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0059_alter_accountsusers_options.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.14 on 2024-07-30 23:37 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0058_owner_account"), + ] + + operations = [ + migrations.AlterModelOptions( + name="accountsusers", + options={"verbose_name_plural": "Accounts Users"}, + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0060_owner_upload_token_required_for_public_repos.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0060_owner_upload_token_required_for_public_repos.py new file mode 100644 index 0000000000..a1e889d9b1 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0060_owner_upload_token_required_for_public_repos.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.16 on 2024-10-02 00:21 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddField + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Add field upload_token_required_for_public_repos to owner + -- + ALTER TABLE "owners" ADD COLUMN "upload_token_required_for_public_repos" boolean DEFAULT true NOT NULL; + ALTER TABLE "owners" ALTER COLUMN "upload_token_required_for_public_repos" DROP DEFAULT; + COMMIT; + """ + + dependencies = [ + ("codecov_auth", "0059_alter_accountsusers_options"), + ] + + operations = [ + RiskyAddField( + model_name="owner", + name="upload_token_required_for_public_repos", + field=models.BooleanField(default=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0061_user_email_opt_in.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0061_user_email_opt_in.py new file mode 100644 index 0000000000..ab188f6168 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0061_user_email_opt_in.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.16 on 2024-10-03 11:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Add field email_opt_in to user + -- + ALTER TABLE "users" ADD COLUMN "email_opt_in" boolean DEFAULT false NOT NULL; + ALTER TABLE "users" ALTER COLUMN "email_opt_in" DROP DEFAULT; + COMMIT; + """ + + dependencies = [ + ("codecov_auth", "0060_owner_upload_token_required_for_public_repos"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="email_opt_in", + field=models.BooleanField(default=False), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0062_alter_owner_upload_token_required_for_public_repos.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0062_alter_owner_upload_token_required_for_public_repos.py new file mode 100644 index 0000000000..0d7ca1aac8 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0062_alter_owner_upload_token_required_for_public_repos.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.16 on 2024-11-07 22:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0061_user_email_opt_in"), + ] + + operations = [ + migrations.AlterField( + model_name="owner", + name="upload_token_required_for_public_repos", + field=models.BooleanField(default=False), + ), # this is a no-op on the db + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0063_tier_plan.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0063_tier_plan.py new file mode 100644 index 0000000000..af7a5c6854 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0063_tier_plan.py @@ -0,0 +1,91 @@ +# Generated by Django 4.2.16 on 2025-01-14 18:09 + +import django.contrib.postgres.fields +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Create model Tier + -- + CREATE TABLE "codecov_auth_tier" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "created_at" timestamp with time zone NOT NULL, "updated_at" timestamp with time zone NOT NULL, "tier_name" varchar(255) NOT NULL UNIQUE, "bundle_analysis" boolean NOT NULL, "test_analytics" boolean NOT NULL, "flaky_test_detection" boolean NOT NULL, "project_coverage" boolean NOT NULL, "private_repo_support" boolean NOT NULL); + -- + -- Create model Plan + -- + CREATE TABLE "codecov_auth_plan" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "created_at" timestamp with time zone NOT NULL, "updated_at" timestamp with time zone NOT NULL, "base_unit_price" integer NOT NULL, "benefits" text[] NOT NULL, "billing_rate" text NULL, "is_active" boolean NOT NULL, "marketing_name" varchar(255) NOT NULL, "max_seats" integer NULL, "monthly_uploads_limit" integer NULL, "paid_plan" boolean NOT NULL, "name" varchar(255) NOT NULL UNIQUE, "tier_id" bigint NOT NULL); + CREATE INDEX "codecov_auth_tier_tier_name_ec8e98b1_like" ON "codecov_auth_tier" ("tier_name" varchar_pattern_ops); + ALTER TABLE "codecov_auth_plan" ADD CONSTRAINT "codecov_auth_plan_tier_id_7847a9eb_fk_codecov_auth_tier_id" FOREIGN KEY ("tier_id") REFERENCES "codecov_auth_tier" ("id") DEFERRABLE INITIALLY DEFERRED; + CREATE INDEX "codecov_auth_plan_name_e2fccac6_like" ON "codecov_auth_plan" ("name" varchar_pattern_ops); + CREATE INDEX "codecov_auth_plan_tier_id_7847a9eb" ON "codecov_auth_plan" ("tier_id"); + COMMIT; + """ + + dependencies = [ + ("codecov_auth", "0062_alter_owner_upload_token_required_for_public_repos"), + ] + + operations = [ + migrations.CreateModel( + name="Tier", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("tier_name", models.CharField(max_length=255, unique=True)), + ("bundle_analysis", models.BooleanField(default=False)), + ("test_analytics", models.BooleanField(default=False)), + ("flaky_test_detection", models.BooleanField(default=False)), + ("project_coverage", models.BooleanField(default=False)), + ("private_repo_support", models.BooleanField(default=False)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="Plan", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("base_unit_price", models.IntegerField(blank=True, default=0)), + ( + "benefits", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), + blank=True, + default=list, + size=None, + ), + ), + ( + "billing_rate", + models.TextField( + blank=True, + choices=[("monthly", "Monthly"), ("annually", "Annually")], + null=True, + ), + ), + ("is_active", models.BooleanField(default=True)), + ("marketing_name", models.CharField(max_length=255)), + ("max_seats", models.IntegerField(blank=True, null=True)), + ("monthly_uploads_limit", models.IntegerField(blank=True, null=True)), + ("paid_plan", models.BooleanField(default=False)), + ("name", models.CharField(max_length=255, unique=True)), + ( + "tier", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="plans", + to="codecov_auth.tier", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0064_plan_stripe_id.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0064_plan_stripe_id.py new file mode 100644 index 0000000000..0fce6c58e8 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0064_plan_stripe_id.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.16 on 2025-01-16 22:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0063_tier_plan"), + ] + + operations = [ + migrations.AddField( + model_name="plan", + name="stripe_id", + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0065_alter_account_plan_alter_owner_plan.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0065_alter_account_plan_alter_owner_plan.py new file mode 100644 index 0000000000..a20058c819 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0065_alter_account_plan_alter_owner_plan.py @@ -0,0 +1,62 @@ +# Generated by Django 4.2.16 on 2025-01-30 13:24 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyRunSQL + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Alter field plan on account + -- + -- (no-op) + -- + -- Raw SQL operation + -- + ALTER TYPE plans ADD VALUE IF NOT EXISTS 'users-developer'; + -- + -- Alter field plan on owner + -- + -- (no-op) + COMMIT; + """ + + dependencies = [ + ("codecov_auth", "0064_plan_stripe_id"), + ] + + operations = [ + migrations.AlterField( + model_name="account", + name="plan", + field=models.CharField( + choices=[ + ("users-basic", "BASIC_PLAN_NAME"), + ("users-trial", "TRIAL_PLAN_NAME"), + ("users-pr-inappm", "CODECOV_PRO_MONTHLY"), + ("users-pr-inappy", "CODECOV_PRO_YEARLY"), + ("users-sentrym", "SENTRY_MONTHLY"), + ("users-sentryy", "SENTRY_YEARLY"), + ("users-teamm", "TEAM_MONTHLY"), + ("users-teamy", "TEAM_YEARLY"), + ("users", "GHM_PLAN_NAME"), + ("users-free", "FREE_PLAN_NAME"), + ("users-inappm", "CODECOV_PRO_MONTHLY_LEGACY"), + ("users-inappy", "CODECOV_PRO_YEARLY_LEGACY"), + ("users-enterprisem", "ENTERPRISE_CLOUD_MONTHLY"), + ("users-enterprisey", "ENTERPRISE_CLOUD_YEARLY"), + ("users-developer", "USERS_DEVELOPER"), + ], + default="users-developer", + max_length=50, + ), + ), + RiskyRunSQL("ALTER TYPE plans ADD VALUE 'users-developer';"), + migrations.AlterField( + model_name="owner", + name="plan", + field=models.TextField(blank=True, default="users-developer", null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/0066_add_pro_plan.py b/libs/shared/shared/django_apps/codecov_auth/migrations/0066_add_pro_plan.py new file mode 100644 index 0000000000..df713c680c --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/migrations/0066_add_pro_plan.py @@ -0,0 +1,61 @@ +from django.db import migrations + +from shared.django_apps.utils.config import RUN_ENV + + +def add_pro_plan(apps, schema_editor): + if RUN_ENV != "ENTERPRISE": + return + + Plan = apps.get_model("codecov_auth", "Plan") + Tier = apps.get_model("codecov_auth", "Tier") + Owner = apps.get_model("codecov_auth", "Owner") + Account = apps.get_model("codecov_auth", "Account") + + defaults = { + "bundle_analysis": True, + "test_analytics": True, + "flaky_test_detection": True, + "project_coverage": True, + "private_repo_support": True, + } + pro_tier, _ = Tier.objects.update_or_create( + tier_name="pro", + defaults=defaults, + ) + + plan_defaults = { + "tier": pro_tier, + "base_unit_price": 10, + "benefits": [ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + "billing_rate": "annually", + "is_active": True, + "marketing_name": "Pro", + "max_seats": None, + "monthly_uploads_limit": None, + "paid_plan": True, + } + + Plan.objects.update_or_create( + name="users-pr-inappy", + defaults=plan_defaults, + ) + + Owner.objects.all().update(plan="users-pr-inappy") + + Account.objects.all().update(plan="users-pr-inappy") + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0065_alter_account_plan_alter_owner_plan"), + ] + + operations = [ + migrations.RunPython(add_pro_plan), + ] diff --git a/libs/shared/shared/django_apps/codecov_auth/migrations/__init__.py b/libs/shared/shared/django_apps/codecov_auth/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/codecov_auth/models.py b/libs/shared/shared/django_apps/codecov_auth/models.py new file mode 100644 index 0000000000..124e1fe77c --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/models.py @@ -0,0 +1,1094 @@ +import binascii +import logging +import os +import uuid +from datetime import datetime +from hashlib import md5 +from typing import Optional, Self + +from django.contrib.postgres.fields import ArrayField, CITextField +from django.contrib.sessions.models import Session as DjangoSession +from django.db import models +from django.db.models import Case, QuerySet, Sum, When +from django.db.models.fields import AutoField, BooleanField, IntegerField +from django.db.models.manager import BaseManager +from django.forms import ValidationError +from django.utils import timezone +from django_prometheus.models import ExportModelOperationsMixin +from model_utils import FieldTracker + +from shared.config import get_config +from shared.django_apps.codecov.models import BaseCodecovModel, BaseModel +from shared.django_apps.codecov_auth.constants import ( + AVATAR_GITHUB_BASE_URL, + AVATARIO_BASE_URL, + BITBUCKET_BASE_URL, + GRAVATAR_BASE_URL, +) +from shared.django_apps.codecov_auth.helpers import get_gitlab_url +from shared.django_apps.codecov_auth.managers import OwnerManager +from shared.django_apps.core.managers import RepositoryManager +from shared.django_apps.core.models import DateTimeWithoutTZField, Repository +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName, TierName, TrialDaysAmount + +# Added to avoid 'doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS' error\ +# Needs to be called the same as the API app +CODECOV_AUTH_APP_LABEL = "codecov_auth" + +# Large number to represent Infinity as float('int') is not JSON serializable +INFINITY = 99999999 + +SERVICE_GITHUB = "github" +SERVICE_GITHUB_ENTERPRISE = "github_enterprise" +SERVICE_BITBUCKET = "bitbucket" +SERVICE_BITBUCKET_SERVER = "bitbucket_server" +SERVICE_GITLAB = "gitlab" +SERVICE_CODECOV_ENTERPRISE = "enterprise" + + +DEFAULT_AVATAR_SIZE = 55 + + +log = logging.getLogger(__name__) + + +# TODO use this to refactor avatar_url +class Service(models.TextChoices): + GITHUB = "github" + GITLAB = "gitlab" + BITBUCKET = "bitbucket" + GITHUB_ENTERPRISE = "github_enterprise" + GITLAB_ENTERPRISE = "gitlab_enterprise" + BITBUCKET_SERVER = "bitbucket_server" + + +class PlanProviders(models.TextChoices): + GITHUB = "github" + + +# Follow the shape of TrialStatus in plan folder +class TrialStatus(models.TextChoices): + NOT_STARTED = "not_started" + ONGOING = "ongoing" + EXPIRED = "expired" + CANNOT_TRIAL = "cannot_trial" + + +class User(ExportModelOperationsMixin("codecov_auth.user"), BaseCodecovModel): + class CustomerIntent(models.TextChoices): + BUSINESS = "BUSINESS" + PERSONAL = "PERSONAL" + + email = CITextField(null=True) + name = models.TextField(null=True) + is_staff = models.BooleanField(null=True, default=False) + is_superuser = models.BooleanField(null=True, default=False) + external_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) + terms_agreement = models.BooleanField(null=True, default=False, blank=True) + terms_agreement_at = DateTimeWithoutTZField(null=True, blank=True) + customer_intent = models.TextField(choices=CustomerIntent.choices, null=True) + email_opt_in = models.BooleanField(default=False) + + REQUIRED_FIELDS = [] + USERNAME_FIELD = "external_id" + + _okta_loggedin_accounts: list["Account"] = [] + + class Meta: + db_table = "users" + app_label = CODECOV_AUTH_APP_LABEL + + @property + def is_active(self): + # Required to implement django's user-model interface + return True + + @property + def is_anonymous(self): + # Required to implement django's user-model interface + return False + + @property + def is_authenticated(self): + # Required to implement django's user-model interface + return True + + @property + def is_github_student(self) -> bool: + try: + github_owner: Owner = self.owners.get(service=Service.GITHUB) + except Owner.DoesNotExist: + return False + return github_owner.student + + def has_perm(self, perm, obj=None): + # Required to implement django's user-model interface + return self.is_staff + + def has_perms(self, *args, **kwargs): + # Required to implement django's user-model interface + return self.is_staff + + def has_module_perms(self, package_name): + # Required to implement django's user-model interface + return self.is_staff + + def get_username(self): + # Required to implement django's user-model interface + return self.external_id + + +class Account(BaseModel): + name = models.CharField(max_length=100, null=False, blank=False, unique=True) + is_active = models.BooleanField(default=True, null=False, blank=True) + plan = models.CharField( + max_length=50, + choices=PlanName.choices(), + null=False, + default=DEFAULT_FREE_PLAN, + ) + plan_seat_count = models.SmallIntegerField(default=1, null=False, blank=True) + free_seat_count = models.SmallIntegerField(default=0, null=False, blank=True) + plan_auto_activate = models.BooleanField(default=True, null=False, blank=True) + is_delinquent = models.BooleanField(default=False, null=False, blank=True) + users = models.ManyToManyField( + User, through="AccountsUsers", related_name="accounts" + ) + + class Meta: + ordering = ["-updated_at"] + app_label = CODECOV_AUTH_APP_LABEL + + def __str__(self): + str_representation_of_is_active = "Active" if self.is_active else "Inactive" + return f"{str_representation_of_is_active} Account: {self.name}" + + def _student_count_helper(self) -> QuerySet: + # This method creates the query to annotate a user as a student. + # To be used in conjunction with filter or exclude to count students and non-students + return ( + self.users.values("id") + .annotate( # count the number of student owners aggregated on users.id + owner_student_count=Sum( + Case( + When(owners__student=True, then=1), + default=0, + output_field=IntegerField(), + ) + ) + ) + .annotate( # if there are any associated student owner to this user, then it is marked as a student + is_student=Case( + When(owner_student_count__gt=0, then=True), + default=False, + output_field=BooleanField(), + ) + ) + ) + + @property + def activated_user_count(self) -> int: + """ + Return the number of activated users. An activated user is one that does not have any student associations. + """ + return self._student_count_helper().filter(is_student=False).count() + + @property + def activated_student_count(self) -> int: + return self._student_count_helper().filter(is_student=True).count() + + @property + def all_user_count(self) -> int: + return self.users.count() + + @property + def organizations_count(self) -> int: + return self.organizations.all().count() + + @property + def total_seat_count(self) -> int: + return self.plan_seat_count + self.free_seat_count + + @property + def available_seat_count(self) -> int: + count = self.total_seat_count - self.activated_user_count + return count if count > 0 else 0 + + @property + def pretty_plan(self) -> dict | None: + """ + This is how we represent the details of a plan to a user, see plan.constants.py + We inject quantity to make plan management easier on api, see PlanSerializer + """ + plan_details = Plan.objects.select_related("tier").get(name=self.plan) + if plan_details: + return { + "marketing_name": plan_details.marketing_name, + "value": plan_details.name, + "billing_rate": plan_details.billing_rate, + "base_unit_price": plan_details.base_unit_price, + "benefits": plan_details.benefits, + "tier_name": plan_details.tier.tier_name, + "monthly_uploads_limit": plan_details.monthly_uploads_limit, + "trial_days": TrialDaysAmount.CODECOV_SENTRY.value + if plan_details.name == PlanName.TRIAL_PLAN_NAME.value + else None, + "quantity": self.plan_seat_count, + } + return None + + def can_activate_user(self, user: User | None = None) -> bool: + """ + Check if account can activate a user. If no user is passed, + then only check for available seats. Otherwise, we can activate + a user if they're a student and if they haven't already been added. + """ + # User is already activated, meaning their occupancy is already counted in the plan seat count. + # Return True since activating them again costs 0 seats, so they will always fit. + if user and user in self.users.all(): + return True + + # User is a student, return True + if user and user.is_github_student: + return True + + total_seats_for_account = self.plan_seat_count + self.free_seat_count + return self.activated_user_count < total_seats_for_account + + def activate_user_onto_account(self, user: User) -> None: + self.users.add(user) + + def activate_owner_user_onto_account(self, owner_user: "Owner") -> None: + user: User = owner_user.user + if not user: + user = User.objects.create(name=owner_user.name, email=owner_user.email) + owner_user.user = user + owner_user.save() + self.activate_user_onto_account(user) + + def deactivate_owner_user_from_account(self, owner_user: "Owner") -> None: + if owner_user.user is None: + log.warning( + "Attempting to deactivate an owner without associated user. Skipping deactivation." + ) + return + + organizations_in_account: list[Owner] = self.organizations.all() + all_owner_users_for_account: set[AutoField] = set( + ownerid + for org in organizations_in_account + for ownerid in org.plan_activated_users + ) + if owner_user.ownerid not in all_owner_users_for_account: + self.users.remove(owner_user.user) + else: + log.info( + "User was not removed from account because they currently are " + "activated on another organization.", + extra=dict(owner_id=owner_user.ownerid, account_id=self.id), + ) + return + + +class Owner(ExportModelOperationsMixin("codecov_auth.owner"), models.Model): + class Meta: + db_table = "owners" + app_label = CODECOV_AUTH_APP_LABEL + ordering = ["ownerid"] + constraints = [ + models.UniqueConstraint( + fields=["service", "username"], name="owner_service_username" + ), + models.UniqueConstraint( + fields=["service", "service_id"], name="owner_service_ids" + ), + ] + + REQUIRED_FIELDS = [] + USERNAME_FIELD = "username" + + ownerid = models.AutoField(primary_key=True) + service = models.TextField(choices=Service.choices) # Really an ENUM in db + username = CITextField( + unique=True, null=True + ) # No actual unique constraint on this in the DB + email = models.TextField(null=True) + business_email = models.TextField(null=True) + name = models.TextField(null=True) + oauth_token = models.TextField(null=True) + stripe_customer_id = models.TextField(null=True, blank=True) + stripe_subscription_id = models.TextField(null=True, blank=True) + stripe_coupon_id = models.TextField(null=True, blank=True) + createstamp = models.DateTimeField(null=True) + service_id = models.TextField(null=False) + parent_service_id = models.TextField(null=True) + root_parent_service_id = models.TextField(null=True) + private_access = models.BooleanField(null=True) + staff = models.BooleanField(null=True, default=False) + cache = models.JSONField(null=True) + # Really an ENUM in db + plan = models.TextField(null=True, default=DEFAULT_FREE_PLAN, blank=True) + plan_provider = models.TextField( + null=True, choices=PlanProviders.choices, blank=True + ) # postgres enum containing only "github" + plan_user_count = models.SmallIntegerField(null=True, default=1, blank=True) + plan_auto_activate = models.BooleanField(null=True, default=True) + plan_activated_users = ArrayField( + models.IntegerField(null=True), null=True, blank=True + ) + did_trial = models.BooleanField(null=True) + trial_start_date = DateTimeWithoutTZField(null=True) + trial_end_date = DateTimeWithoutTZField(null=True) + trial_status = models.CharField( + max_length=50, + choices=TrialStatus.choices, + null=True, + default=TrialStatus.NOT_STARTED.value, + ) + trial_fired_by = models.IntegerField(null=True) + pretrial_users_count = models.SmallIntegerField(null=True, blank=True) + free = models.SmallIntegerField(default=0) + invoice_details = models.TextField(null=True) + uses_invoice = models.BooleanField(default=False, null=False) + delinquent = models.BooleanField(null=True) + yaml = models.JSONField(null=True) + updatestamp = DateTimeWithoutTZField(default=datetime.now) + organizations = ArrayField(models.IntegerField(null=True), null=True, blank=True) + admins = ArrayField(models.IntegerField(null=True), null=True, blank=True) + + # DEPRECATED - replaced by GithubAppInstallation model + integration_id = models.IntegerField(null=True, blank=True) + + permission = ArrayField(models.IntegerField(null=True), null=True) + bot = models.ForeignKey( + "Owner", db_column="bot", null=True, on_delete=models.SET_NULL, blank=True + ) + student = models.BooleanField(default=False) + student_created_at = DateTimeWithoutTZField(null=True) + student_updated_at = DateTimeWithoutTZField(null=True) + onboarding_completed = models.BooleanField(default=False) + is_superuser = models.BooleanField(null=True, default=False) + max_upload_limit = models.IntegerField(null=True, default=150, blank=True) + upload_token_required_for_public_repos = models.BooleanField(default=False) + + sentry_user_id = models.TextField(null=True, blank=True, unique=True) + sentry_user_data = models.JSONField(null=True) + + user = models.ForeignKey( + User, + null=True, + on_delete=models.SET_NULL, + blank=True, + related_name="owners", + ) + + account = models.ForeignKey( + Account, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="organizations", + ) + + objects = OwnerManager() + tracker = FieldTracker( + fields=["username", "service", "upload_token_required_for_public_repos"] + ) + repository_set = RepositoryManager() + + def __str__(self): + return f"Owner<{self.service}/{self.username}>" + + def save(self, *args, **kwargs): + self.updatestamp = timezone.now() + super().save(*args, **kwargs) + + @property + def has_yaml(self): + return self.yaml is not None + + @property + def default_org(self): + try: + if self.profile: + return self.profile.default_org + except OwnerProfile.DoesNotExist: + return None + + @property + def has_legacy_plan(self): + return self.plan is None or not self.plan.startswith("users") + + @property + def repo_total_credits(self): + # Returns the number of private repo credits remaining + # Only meaningful for legacy plans + V4_PLAN_PREFIX = "v4-" + if not self.has_legacy_plan: + return INFINITY + if self.plan is None: + return int(1 + self.free or 0) + elif self.plan.startswith(V4_PLAN_PREFIX): + return int(self.plan[3:-1]) + else: + return int(self.plan[:-1]) + + @property + def root_organization(self: "Owner") -> Optional["Owner"]: + """ + Find the root organization of Gitlab, by using the root_parent_service_id + if it exists, otherwise iterating through the parents and caches it in root_parent_service_id + """ + if self.root_parent_service_id: + return Owner.objects.get( + service_id=self.root_parent_service_id, service=self.service + ) + + root = None + if self.service == "gitlab" and self.parent_service_id: + root = self + while root.parent_service_id is not None: + root = Owner.objects.get( + service_id=root.parent_service_id, service=root.service + ) + self.root_parent_service_id = root.service_id + self.save() + return root + + @property + def nb_active_private_repos(self): + return self.repository_set.filter(active=True, private=True).count() + + @property + def has_public_repos(self): + return self.repository_set.filter(private=False).exists() + + @property + def has_private_repos(self): + return self.repository_set.filter(private=True).exists() + + @property + def has_active_repos(self): + return self.repository_set.filter(active=True).exists() + + @property + def repo_credits(self): + # Returns the number of private repo credits remaining + # Only meaningful for legacy plans + if not self.has_legacy_plan: + return INFINITY + return self.repo_total_credits - self.nb_active_private_repos + + @property + def orgs(self): + if self.organizations: + return Owner.objects.filter(ownerid__in=self.organizations) + return Owner.objects.none() + + @property + def active_repos(self): + return Repository.objects.filter(active=True, author=self.ownerid).order_by( + "-updatestamp" + ) + + @property + def activated_user_count(self): + if not self.plan_activated_users: + return 0 + return Owner.objects.filter( + ownerid__in=self.plan_activated_users, student=False + ).count() + + @property + def activated_student_count(self): + if not self.plan_activated_users: + return 0 + return Owner.objects.filter( + ownerid__in=self.plan_activated_users, student=True + ).count() + + @property + def student_count(self): + return Owner.objects.users_of(self).filter(student=True).count() + + @property + def inactive_user_count(self): + return ( + Owner.objects.users_of(self).filter(student=False).count() + - self.activated_user_count + ) + + def is_admin(self, owner): + return self.ownerid == owner.ownerid or ( + bool(self.admins) and owner.ownerid in self.admins + ) + + @property + def is_authenticated(self): + # NOTE: this is here to support `UserTokenAuthentication` which still returns + # an `Owner` as the authenticatable record. Since there is code that calls + # `request.user.is_authenticated` we need to support that here. + return True + + def clean(self): + if self.staff: + domain = self.email.split("@")[1] if self.email else "" + if domain not in ["codecov.io", "sentry.io"]: + raise ValidationError( + "User not part of Codecov or Sentry cannot be a staff member" + ) + if not self.plan: + self.plan = None + if not self.stripe_customer_id: + self.stripe_customer_id = None + if not self.stripe_subscription_id: + self.stripe_subscription_id = None + + @property + def avatar_url(self, size=DEFAULT_AVATAR_SIZE): + if self.service == SERVICE_GITHUB and self.service_id: + return "{}/u/{}?v=3&s={}".format( + AVATAR_GITHUB_BASE_URL, self.service_id, size + ) + + elif self.service == SERVICE_GITHUB_ENTERPRISE and self.service_id: + return "{}/avatars/u/{}?v=3&s={}".format( + get_config("github_enterprise", "url"), self.service_id, size + ) + + # Bitbucket + elif self.service == SERVICE_BITBUCKET and self.username: + return "{}/account/{}/avatar/{}".format( + BITBUCKET_BASE_URL, self.username, size + ) + + elif ( + self.service == SERVICE_BITBUCKET_SERVER + and self.service_id + and self.username + ): + if "U" in self.service_id: + return "{}/users/{}/avatar.png?s={}".format( + get_config("bitbucket_server", "url"), self.username, size + ) + else: + return "{}/projects/{}/avatar.png?s={}".format( + get_config("bitbucket_server", "url"), self.username, size + ) + + # Gitlab + elif self.service == SERVICE_GITLAB and self.email: + return get_gitlab_url(self.email, size) + + # Codecov config + elif get_config("services", "gravatar") and self.email: + return "{}/avatar/{}?s={}".format( + GRAVATAR_BASE_URL, md5(self.email.lower().encode()).hexdigest(), size + ) + + elif get_config("services", "avatars.io") and self.email: + return "{}/avatar/{}/{}".format( + AVATARIO_BASE_URL, md5(self.email.lower().encode()).hexdigest(), size + ) + + elif self.ownerid: + return "{}/users/{}.png?size={}".format( + get_config("setup", "codecov_url"), self.ownerid, size + ) + + elif os.getenv("APP_ENV") == SERVICE_CODECOV_ENTERPRISE: + return "{}/media/images/gafsi/avatar.svg".format( + get_config("setup", "codecov_url") + ) + + else: + return "{}/media/images/gafsi/avatar.svg".format( + get_config("setup", "media", "assets") + ) + + @property + def pretty_plan(self): + if self.account: + return self.account.pretty_plan + + plan_details = Plan.objects.select_related("tier").get(name=self.plan) + if plan_details: + return { + "marketing_name": plan_details.marketing_name, + "value": plan_details.name, + "billing_rate": plan_details.billing_rate, + "base_unit_price": plan_details.base_unit_price, + "benefits": plan_details.benefits, + "tier_name": plan_details.tier.tier_name, + "monthly_uploads_limit": plan_details.monthly_uploads_limit, + "trial_days": TrialDaysAmount.CODECOV_SENTRY.value + if plan_details.name == PlanName.TRIAL_PLAN_NAME.value + else None, + "quantity": self.plan_user_count, + } + return None + + def can_activate_user(self, owner_user: Self) -> bool: + owner_org = self + if owner_user.student: + return True + if owner_org.account: + return owner_org.account.can_activate_user(owner_user.user) + return ( + owner_org.activated_user_count < owner_org.plan_user_count + owner_org.free + ) + + def activate_user(self, owner_user: Self) -> None: + owner_org = self + log.info(f"Activating user {owner_user.ownerid} in ownerid {owner_org.ownerid}") + if isinstance(owner_org.plan_activated_users, list): + if owner_user.ownerid not in owner_org.plan_activated_users: + owner_org.plan_activated_users.append(owner_user.ownerid) + else: + owner_org.plan_activated_users = [owner_user.ownerid] + owner_org.save() + + if owner_org.account: + owner_org.account.activate_owner_user_onto_account(owner_user) + + def deactivate_user(self, owner_user: Self) -> None: + owner_org = self + log.info( + f"Deactivating user {owner_user.ownerid} in ownerid {owner_org.ownerid}" + ) + if isinstance(owner_org.plan_activated_users, list): + try: + owner_org.plan_activated_users.remove(owner_user.ownerid) + except ValueError: + pass + owner_org.save() + + if owner_org.account and owner_user.user: + owner_org.account.deactivate_owner_user_from_account(owner_user) + + def add_admin(self, user): + log.info( + f"Granting admin permissions to user {user.ownerid} within owner {self.ownerid}" + ) + if isinstance(self.admins, list): + if user.ownerid not in self.admins: + self.admins.append(user.ownerid) + else: + self.admins = [user.ownerid] + self.save() + + def remove_admin(self, user): + log.info( + f"Revoking admin permissions for user {user.ownerid} within owner {self.ownerid}" + ) + if isinstance(self.admins, list): + try: + self.admins.remove(user.ownerid) + except ValueError: + pass + self.save() + + +GITHUB_APP_INSTALLATION_DEFAULT_NAME = "codecov_app_installation" + + +class GithubAppInstallation( + ExportModelOperationsMixin("codecov_auth.github_app_installation"), BaseCodecovModel +): + # replacement for owner.integration_id + # installation id GitHub sends us in the installation-related webhook events + installation_id = models.IntegerField(null=False, blank=False) + name = models.TextField(default=GITHUB_APP_INSTALLATION_DEFAULT_NAME) + # if null, all repos are covered by this installation + # otherwise, it's a list of repo.id values + repository_service_ids = ArrayField(models.TextField(null=False), null=True) + + # Needed to get a JWT for the app + # NULL for the default app, which is configured in the install YAML + app_id = models.IntegerField(null=True, blank=False) + # Same comments for app_id apply + pem_path = models.TextField(null=True, blank=False) + + is_suspended = models.BooleanField(null=False, default=False) + + owner = models.ForeignKey( + Owner, + null=False, + on_delete=models.CASCADE, + blank=False, + related_name="github_app_installations", + ) + + class Meta: + app_label = CODECOV_AUTH_APP_LABEL + + def is_configured(self) -> bool: + """Returns whether this installation is properly configured and can be used""" + if self.app_id is not None and self.pem_path is not None: + return True + if self.name == "unconfigured_app": + return False + # The default app is configured in the installation YAML + installation_default_app_id = get_config("github", "integration", "id") + if installation_default_app_id is None: + log.error( + "Can't find default app ID in the YAML. Assuming installation is configured to prevent the app from breaking itself.", + extra=dict(installation_id=self.id, installation_name=self.name), + ) + return True + return str(self.app_id) == str(installation_default_app_id) + + def repository_queryset(self) -> BaseManager[Repository]: + """Returns a QuerySet of repositories covered by this installation""" + if self.repository_service_ids is None: + # All repos covered + return Repository.objects.filter(author=self.owner) + # Some repos covered + return Repository.objects.filter( + service_id__in=self.repository_service_ids, author=self.owner + ) + + def covers_all_repos(self) -> bool: + return self.repository_service_ids is None + + def is_repo_covered_by_integration(self, repo: Repository) -> bool: + if self.covers_all_repos(): + return repo.author.ownerid == self.owner.ownerid + return repo.service_id in self.repository_service_ids + + +class OwnerInstallationNameToUseForTask( + ExportModelOperationsMixin("codecov_auth.github_app_installation"), BaseCodecovModel +): + owner = models.ForeignKey( + Owner, + null=False, + on_delete=models.CASCADE, + blank=False, + related_name="installation_name_to_use_for_tasks", + ) + installation_name = models.TextField(null=False, blank=False) + task_name = models.TextField(null=False, blank=False) + + class Meta: + app_label = CODECOV_AUTH_APP_LABEL + constraints = [ + # Only 1 app name per task per owner_id + models.UniqueConstraint( + "owner_id", "task_name", name="single_task_name_per_owner" + ) + ] + + +class SentryUser( + ExportModelOperationsMixin("codecov_auth.sentry_user"), BaseCodecovModel +): + user = models.ForeignKey( + User, + null=False, + on_delete=models.CASCADE, + related_name="sentry_user", + ) + access_token = models.TextField(null=True) + refresh_token = models.TextField(null=True) + sentry_id = models.TextField(null=False, unique=True) + email = models.TextField(null=True) + name = models.TextField(null=True) + + class Meta: + app_label = CODECOV_AUTH_APP_LABEL + + +class OktaUser(ExportModelOperationsMixin("codecov_auth.okta_user"), BaseCodecovModel): + user = models.ForeignKey( + User, + null=False, + on_delete=models.CASCADE, + related_name="okta_user", + ) + access_token = models.TextField(null=True) + okta_id = models.TextField(null=False, unique=True) + email = models.TextField(null=True) + name = models.TextField(null=True) + + class Meta: + app_label = CODECOV_AUTH_APP_LABEL + + +class TokenTypeChoices(models.TextChoices): + UPLOAD = "upload" + + +class OrganizationLevelToken( + ExportModelOperationsMixin("codecov_auth.organization_level_token"), + BaseCodecovModel, +): + owner = models.ForeignKey( + "Owner", + db_column="ownerid", + related_name="organization_tokens", + on_delete=models.CASCADE, + ) + token = models.UUIDField(unique=True, default=uuid.uuid4) + valid_until = models.DateTimeField(blank=True, null=True) + token_type = models.CharField( + max_length=50, choices=TokenTypeChoices.choices, default=TokenTypeChoices.UPLOAD + ) + + class Meta: + app_label = CODECOV_AUTH_APP_LABEL + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + + +class OwnerProfile( + ExportModelOperationsMixin("codecov_auth.owner_profile"), BaseCodecovModel +): + class ProjectType(models.TextChoices): + PERSONAL = "PERSONAL" + YOUR_ORG = "YOUR_ORG" + OPEN_SOURCE = "OPEN_SOURCE" + EDUCATIONAL = "EDUCATIONAL" + + class Goal(models.TextChoices): + STARTING_WITH_TESTS = "STARTING_WITH_TESTS" + IMPROVE_COVERAGE = "IMPROVE_COVERAGE" + MAINTAIN_COVERAGE = "MAINTAIN_COVERAGE" + TEAM_REQUIREMENTS = "TEAM_REQUIREMENTS" + OTHER = "OTHER" + + owner = models.OneToOneField( + Owner, on_delete=models.CASCADE, unique=True, related_name="profile" + ) + type_projects = ArrayField( + models.TextField(choices=ProjectType.choices), default=list + ) + goals = ArrayField(models.TextField(choices=Goal.choices), default=list) + other_goal = models.TextField(null=True) + default_org = models.ForeignKey( + Owner, on_delete=models.CASCADE, null=True, related_name="profiles_with_default" + ) + + class Meta: + app_label = CODECOV_AUTH_APP_LABEL + + +class Session(ExportModelOperationsMixin("codecov_auth.session"), models.Model): + class Meta: + app_label = CODECOV_AUTH_APP_LABEL + db_table = "sessions" + ordering = ["-lastseen"] + + class SessionType(models.TextChoices): + API = "api" + LOGIN = "login" + + sessionid = models.AutoField(primary_key=True) + token = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + name = models.TextField(null=True) + useragent = models.TextField(null=True) + ip = models.TextField(null=True) + owner = models.ForeignKey(Owner, db_column="ownerid", on_delete=models.CASCADE) + lastseen = models.DateTimeField(null=True) + # Really an ENUM in db + type = models.TextField(choices=SessionType.choices) + login_session = models.ForeignKey( + DjangoSession, on_delete=models.CASCADE, blank=True, null=True + ) + + +def _generate_key(): + return binascii.hexlify(os.urandom(20)).decode() + + +class RepositoryToken( + ExportModelOperationsMixin("codecov_auth.repository_token"), BaseCodecovModel +): + class TokenType(models.TextChoices): + UPLOAD = "upload" + PROFILING = "profiling" + STATIC_ANALYSIS = "static_analysis" + + repository = models.ForeignKey( + "core.Repository", + db_column="repoid", + on_delete=models.CASCADE, + related_name="tokens", + ) + token_type = models.CharField(max_length=50, choices=TokenType.choices) + valid_until = models.DateTimeField(blank=True, null=True) + key = models.CharField( + max_length=40, unique=True, editable=False, default=_generate_key + ) + + class Meta: + app_label = CODECOV_AUTH_APP_LABEL + + @classmethod + def generate_key(cls): + return _generate_key() + + +class UserToken( + ExportModelOperationsMixin("codecov_auth.user_token"), BaseCodecovModel +): + class TokenType(models.TextChoices): + API = "api" + + class Meta: + app_label = CODECOV_AUTH_APP_LABEL + + name = models.CharField(max_length=100, null=False, blank=False) + owner = models.ForeignKey( + "Owner", + db_column="ownerid", + related_name="user_tokens", + on_delete=models.CASCADE, + ) + token = models.UUIDField(unique=True, default=uuid.uuid4) + valid_until = models.DateTimeField(blank=True, null=True) + token_type = models.CharField( + max_length=50, choices=TokenType.choices, default=TokenType.API + ) + + +class AccountsUsers(BaseModel): + user = models.ForeignKey(User, on_delete=models.CASCADE) + account = models.ForeignKey(Account, on_delete=models.CASCADE) + + class Meta: + unique_together = ("user", "account") + app_label = CODECOV_AUTH_APP_LABEL + verbose_name_plural = "Accounts Users" + + +class OktaSettings(BaseModel): + account = models.ForeignKey( + Account, on_delete=models.CASCADE, related_name="okta_settings" + ) + client_id = models.CharField(max_length=255) + client_secret = models.CharField(max_length=255) + url = models.CharField(max_length=255) + enabled = models.BooleanField(default=True, blank=True) + enforced = models.BooleanField(default=True, blank=True) + + class Meta: + app_label = CODECOV_AUTH_APP_LABEL + + +class StripeBilling(BaseModel): + account = models.ForeignKey( + Account, on_delete=models.CASCADE, related_name="stripe_billing", unique=True + ) + customer_id = models.CharField(max_length=255, unique=True) + subscription_id = models.CharField(max_length=255, null=True, blank=True) + is_active = models.BooleanField(default=True, null=False, blank=True) + + class Meta: + app_label = CODECOV_AUTH_APP_LABEL + + def save(self, *args, **kwargs): + if self.is_active: + # inactivate all other billing methods + StripeBilling.objects.filter(account=self.account, is_active=True).exclude( + id=self.id + ).update(is_active=False) + InvoiceBilling.objects.filter(account=self.account, is_active=True).update( + is_active=False + ) + return super().save(*args, **kwargs) + + +class InvoiceBilling(BaseModel): + account = models.ForeignKey( + Account, on_delete=models.CASCADE, related_name="invoice_billing", unique=True + ) + account_manager = models.CharField(max_length=255, null=True, blank=True) + invoice_notes = models.TextField(null=True, blank=True) + is_active = models.BooleanField(default=True, null=False, blank=True) + + class Meta: + app_label = CODECOV_AUTH_APP_LABEL + + def save(self, *args, **kwargs): + if self.is_active: + # inactivate all other billing methods + StripeBilling.objects.filter(account=self.account, is_active=True).update( + is_active=False + ) + InvoiceBilling.objects.filter(account=self.account, is_active=True).exclude( + id=self.id + ).update(is_active=False) + return super().save(*args, **kwargs) + + +class BillingRate(models.TextChoices): + MONTHLY = "monthly" + ANNUALLY = "annually" + + +class Plan(BaseModel): + tier = models.ForeignKey("Tier", on_delete=models.CASCADE, related_name="plans") + base_unit_price = models.IntegerField(default=0, blank=True) + benefits = ArrayField(models.TextField(), blank=True, default=list) + billing_rate = models.TextField( + choices=BillingRate.choices, + null=True, + blank=True, + ) + is_active = models.BooleanField(default=True) + marketing_name = models.CharField(max_length=255) + max_seats = models.IntegerField(null=True, blank=True) + monthly_uploads_limit = models.IntegerField(null=True, blank=True) + name = models.CharField(max_length=255, unique=True) + paid_plan = models.BooleanField(default=False) + stripe_id = models.CharField(max_length=255, null=True, blank=True) + + class Meta: + app_label = CODECOV_AUTH_APP_LABEL + + def __str__(self): + return self.name + + @property + def is_free_plan(self): + return not self.paid_plan + + @property + def is_pro_plan(self): + return ( + self.tier.tier_name == TierName.PRO.value + or self.tier.tier_name == TierName.SENTRY.value + ) + + @property + def is_team_plan(self): + return self.tier.tier_name == TierName.TEAM.value + + @property + def is_enterprise_plan(self): + return self.tier.tier_name == TierName.ENTERPRISE.value + + @property + def is_sentry_plan(self): + return self.tier.tier_name == TierName.SENTRY.value + + @property + def is_trial_plan(self): + return self.tier.tier_name == TierName.TRIAL.value + + +class Tier(BaseModel): + tier_name = models.CharField(max_length=255, unique=True) + bundle_analysis = models.BooleanField(default=False) + test_analytics = models.BooleanField(default=False) + flaky_test_detection = models.BooleanField(default=False) + project_coverage = models.BooleanField(default=False) + private_repo_support = models.BooleanField(default=False) + + class Meta: + app_label = CODECOV_AUTH_APP_LABEL + + def __str__(self): + return self.tier_name diff --git a/libs/shared/shared/django_apps/codecov_auth/services/org_level_token_service.py b/libs/shared/shared/django_apps/codecov_auth/services/org_level_token_service.py new file mode 100644 index 0000000000..a72617e22c --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/services/org_level_token_service.py @@ -0,0 +1,71 @@ +import logging +import uuid + +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.forms import ValidationError + +from shared.django_apps.codecov_auth.models import OrganizationLevelToken, Owner, Plan + +log = logging.getLogger(__name__) + + +class OrgLevelTokenService(object): + """ + Groups some basic CRUD functionality to create and delete OrganizationLevelToken. + Restrictions: + -- only 1 token per Owner + """ + + # MIGHT BE ABLE TO REMOVE THIS AND SUBSEQUENT DOWNSTREAM STUFF + @classmethod + def org_can_have_upload_token(cls, org: Owner): + return Plan.objects.filter(name=org.plan, is_active=True).exists() + + @classmethod + def get_or_create_org_token(cls, org: Owner): + if not cls.org_can_have_upload_token(org): + raise ValidationError( + "Organization-wide upload tokens are not available for your organization." + ) + token, created = OrganizationLevelToken.objects.get_or_create(owner=org) + if created: + log.info( + "New OrgLevelToken created", + extra=dict( + ownerid=org.ownerid, + valid_until=token.valid_until, + token_type=token.token_type, + ), + ) + return token + + @classmethod + def refresh_token(cls, tokenid: int): + try: + token = OrganizationLevelToken.objects.get(id=tokenid) + token.token = uuid.uuid4() + token.save() + except OrganizationLevelToken.DoesNotExist: + raise ValidationError( + "Token to refresh was not found", params=dict(tokenid=tokenid) + ) + + @classmethod + def delete_org_token_if_exists(cls, org: Owner): + try: + org_token = OrganizationLevelToken.objects.get(owner=org) + org_token.delete() + except OrganizationLevelToken.DoesNotExist: + pass + + +@receiver(post_save, sender=Owner) +def manage_org_tokens_if_owner_plan_changed(sender, instance: Owner, **kwargs): + """ + Gets executed after saving a Owner instance to DB. + Manages OrganizationLevelToken according to Owner plan, either creating or deleting them as necessary + """ + owner_can_have_org_token = OrgLevelTokenService.org_can_have_upload_token(instance) + if not owner_can_have_org_token: + OrgLevelTokenService.delete_org_token_if_exists(instance) diff --git a/libs/shared/shared/django_apps/codecov_auth/tests/__init__.py b/libs/shared/shared/django_apps/codecov_auth/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/codecov_auth/tests/factories.py b/libs/shared/shared/django_apps/codecov_auth/tests/factories.py new file mode 100644 index 0000000000..c3fe02b6bb --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_auth/tests/factories.py @@ -0,0 +1,202 @@ +from uuid import uuid4 + +import factory +from django.utils import timezone +from factory.django import DjangoModelFactory + +from shared.django_apps.codecov_auth.models import ( + Account, + AccountsUsers, + InvoiceBilling, + OktaSettings, + OktaUser, + OrganizationLevelToken, + Owner, + OwnerProfile, + Plan, + SentryUser, + Session, + StripeBilling, + Tier, + TokenTypeChoices, + User, + UserToken, +) +from shared.encryption.oauth import get_encryptor_from_configuration +from shared.plan.constants import PlanName, TierName, TrialStatus + +encryptor = get_encryptor_from_configuration() + + +class UserFactory(DjangoModelFactory): + class Meta: + model = User + + email = factory.Faker("email") + name = factory.Faker("name") + terms_agreement = False + terms_agreement_at = None + customer_intent = "Business" + + +class OwnerFactory(DjangoModelFactory): + class Meta: + model = Owner + exclude = ("unencrypted_oauth_token",) + + name = factory.Faker("name") + email = factory.Faker("email") + username = factory.Faker("user_name") + service = "github" + service_id = factory.Sequence(lambda n: f"{n}") + updatestamp = factory.LazyFunction(timezone.now) + plan_activated_users = [] + admins = [] + permission = [] + free = 0 + onboarding_completed = False + unencrypted_oauth_token = factory.LazyFunction(lambda: uuid4().hex) + cache = {"stats": {"repos": 1, "members": 2, "users": 1}} + oauth_token = factory.LazyAttribute( + lambda o: encryptor.encode(o.unencrypted_oauth_token).decode() + ) + student = False + user = factory.SubFactory(UserFactory) + trial_status = TrialStatus.NOT_STARTED.value + + +class SentryUserFactory(DjangoModelFactory): + class Meta: + model = SentryUser + + email = factory.Faker("email") + name = factory.Faker("name") + sentry_id = factory.LazyFunction(lambda: uuid4().hex) + access_token = factory.LazyFunction(lambda: uuid4().hex) + refresh_token = factory.LazyFunction(lambda: uuid4().hex) + user = factory.SubFactory(UserFactory) + + +class OktaUserFactory(DjangoModelFactory): + class Meta: + model = OktaUser + + email = factory.Faker("email") + name = factory.Faker("name") + okta_id = factory.LazyFunction(lambda: uuid4().hex) + access_token = factory.LazyFunction(lambda: uuid4().hex) + user = factory.SubFactory(UserFactory) + + +class OwnerProfileFactory(DjangoModelFactory): + class Meta: + model = OwnerProfile + + owner = factory.SubFactory(OwnerFactory) + default_org = factory.SubFactory(OwnerFactory) + + +class SessionFactory(DjangoModelFactory): + class Meta: + model = Session + + owner = factory.SubFactory(OwnerFactory) + lastseen = timezone.now() + type = Session.SessionType.API.value + token = factory.Faker("uuid4") + + +class OrganizationLevelTokenFactory(DjangoModelFactory): + class Meta: + model = OrganizationLevelToken + + owner = factory.SubFactory(OwnerFactory) + token = uuid4() + token_type = TokenTypeChoices.UPLOAD + + +class GetAdminProviderAdapter: + def __init__(self, result=False): + self.result = result + self.last_call_args = None + + async def get_is_admin(self, user): + self.last_call_args = user + return self.result + + +class UserTokenFactory(DjangoModelFactory): + class Meta: + model = UserToken + + owner = factory.SubFactory(OwnerFactory) + token = factory.LazyAttribute(lambda _: uuid4()) + + +class AccountFactory(DjangoModelFactory): + class Meta: + model = Account + + name = factory.Faker("name") + + +class AccountsUsersFactory(DjangoModelFactory): + class Meta: + model = AccountsUsers + + user = factory.SubFactory(UserFactory) + account = factory.SubFactory(Account) + + +class OktaSettingsFactory(DjangoModelFactory): + class Meta: + model = OktaSettings + + account = factory.SubFactory(Account) + client_id = factory.Faker("pyint") + client_secret = factory.Faker("pyint") + url = factory.Faker("pystr") + + +class StripeBillingFactory(DjangoModelFactory): + class Meta: + model = StripeBilling + + account = factory.SubFactory(Account) + customer_id = factory.Faker("pyint") + + +class InvoiceBillingFactory(DjangoModelFactory): + class Meta: + model = InvoiceBilling + + account = factory.SubFactory(Account) + + +class TierFactory(DjangoModelFactory): + class Meta: + model = Tier + + tier_name = TierName.BASIC.value + bundle_analysis = False + test_analytics = False + flaky_test_detection = False + project_coverage = False + private_repo_support = False + + +class PlanFactory(DjangoModelFactory): + class Meta: + model = Plan + + tier = factory.SubFactory(TierFactory) + base_unit_price = 0 + benefits = factory.LazyFunction(lambda: ["Benefit 1", "Benefit 2", "Benefit 3"]) + billing_rate = None + is_active = True + marketing_name = factory.Faker("catch_phrase") + max_seats = 1 + monthly_uploads_limit = None + name = PlanName.BASIC_PLAN_NAME.value + paid_plan = False + stripe_id = None diff --git a/libs/shared/shared/django_apps/codecov_metrics/__init__.py b/libs/shared/shared/django_apps/codecov_metrics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/codecov_metrics/migrations/0001_initial.py b/libs/shared/shared/django_apps/codecov_metrics/migrations/0001_initial.py new file mode 100644 index 0000000000..4214368f6f --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_metrics/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.13 on 2024-06-10 14:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + # BEGIN; + # -- + # -- Create model UserOnboardingLifeCycleMetrics + # -- + # CREATE TABLE "codecov_metrics_useronboardinglifecyclemetrics" + # ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "org_id" integer NOT NULL, "event" text NOT NULL, + # "timestamp" timestamp with time zone NOT NULL); + # COMMIT; + + operations = [ + migrations.CreateModel( + name="UserOnboardingLifeCycleMetrics", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("org_id", models.IntegerField()), + ("event", models.TextField()), + ("timestamp", models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_metrics/migrations/0002_useronboardinglifecyclemetrics_additional_data.py b/libs/shared/shared/django_apps/codecov_metrics/migrations/0002_useronboardinglifecyclemetrics_additional_data.py new file mode 100644 index 0000000000..b64eba3e1f --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_metrics/migrations/0002_useronboardinglifecyclemetrics_additional_data.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.13 on 2024-06-12 16:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_metrics", "0001_initial"), + ] + # BEGIN; + # -- + # -- Add field additional_data to useronboardinglifecyclemetrics + # -- + # ALTER TABLE "codecov_metrics_useronboardinglifecyclemetrics" ADD COLUMN "additional_data" jsonb NULL; + # COMMIT; + + operations = [ + migrations.AddField( + model_name="useronboardinglifecyclemetrics", + name="additional_data", + field=models.JSONField(blank=True, null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/codecov_metrics/migrations/__init__.py b/libs/shared/shared/django_apps/codecov_metrics/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/codecov_metrics/models.py b/libs/shared/shared/django_apps/codecov_metrics/models.py new file mode 100644 index 0000000000..98cac73ce0 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_metrics/models.py @@ -0,0 +1,13 @@ +from django.db import models + +# Added to avoid 'doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS' error\ +# Needs to be called the same as the API app +CODECOV_METRICS_APP_LABEL = "codecov_metrics" + + +class UserOnboardingLifeCycleMetrics(models.Model): + id = models.BigAutoField(primary_key=True) + org_id = models.IntegerField() + event = models.TextField() + timestamp = models.DateTimeField(auto_now_add=True) + additional_data = models.JSONField(blank=True, null=True) diff --git a/libs/shared/shared/django_apps/codecov_metrics/service/__init__.py b/libs/shared/shared/django_apps/codecov_metrics/service/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/codecov_metrics/service/codecov_metrics.py b/libs/shared/shared/django_apps/codecov_metrics/service/codecov_metrics.py new file mode 100644 index 0000000000..6b94f14e09 --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_metrics/service/codecov_metrics.py @@ -0,0 +1,31 @@ +import logging +from typing import Any, Dict + +from ..models import UserOnboardingLifeCycleMetrics + +log = logging.getLogger(__name__) + + +class UserOnboardingMetricsService: + ALLOWED_EVENTS = { + "VISITED_PAGE", + "CLICKED_BUTTON", + "COPIED_TEXT", + "COMPLETED_UPLOAD", + "INSTALLED_APP", + } + + @staticmethod + def create_user_onboarding_metric(org_id: int, event: str, payload: Dict[str, Any]): + if event not in UserOnboardingMetricsService.ALLOWED_EVENTS: + log.warning("Incompatible event type", extra=dict(event_name=event)) + return + + metric, created = UserOnboardingLifeCycleMetrics.objects.get_or_create( + org_id=org_id, + event=event, + additional_data=payload, + ) + if created: + return metric + return None diff --git a/libs/shared/shared/django_apps/codecov_metrics/tests/service/test_codecov_metrics.py b/libs/shared/shared/django_apps/codecov_metrics/tests/service/test_codecov_metrics.py new file mode 100644 index 0000000000..975b67faee --- /dev/null +++ b/libs/shared/shared/django_apps/codecov_metrics/tests/service/test_codecov_metrics.py @@ -0,0 +1,48 @@ +from django.test import TestCase +from django.utils import timezone + +from shared.django_apps.codecov_metrics.models import UserOnboardingLifeCycleMetrics +from shared.django_apps.codecov_metrics.service.codecov_metrics import ( + UserOnboardingMetricsService, +) + + +class UserOnboardingMetricsServiceTest(TestCase): + def setUp(self): + self.org_id = 1 + self.event = "VISITED_PAGE" + self.payload = { + "key1": "value1", + "key2": 123, + "key3": [1, 2, 3], + "key4": {"nested_key": "nested_value"}, + } + + def test_create_user_onboarding_metric_creates_metric(self): + metric = UserOnboardingMetricsService.create_user_onboarding_metric( + self.org_id, self.event, self.payload + ) + self.assertIsNotNone(metric) + self.assertEqual(metric.org_id, self.org_id) + self.assertEqual(metric.event, self.event) + self.assertIsInstance(metric.timestamp, timezone.datetime) + self.assertEqual(metric.additional_data, self.payload) + + def test_create_user_onboarding_metric_does_not_create_duplicate(self): + UserOnboardingLifeCycleMetrics.objects.create( + org_id=self.org_id, + event=self.event, + timestamp=timezone.now(), + additional_data=self.payload, + ) + metric = UserOnboardingMetricsService.create_user_onboarding_metric( + self.org_id, self.event, self.payload + ) + self.assertIsNone(metric) + + def test_create_user_onboarding_metric_with_invalid_event(self): + invalid_event = "INVALID_EVENT" + metric = UserOnboardingMetricsService.create_user_onboarding_metric( + self.org_id, invalid_event, self.payload + ) + self.assertIsNone(metric) diff --git a/libs/shared/shared/django_apps/compare/__init__.py b/libs/shared/shared/django_apps/compare/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/compare/migrations/0001_initial.py b/libs/shared/shared/django_apps/compare/migrations/0001_initial.py new file mode 100644 index 0000000000..ce53e0fcbb --- /dev/null +++ b/libs/shared/shared/django_apps/compare/migrations/0001_initial.py @@ -0,0 +1,62 @@ +# Generated by Django 3.1.6 on 2021-07-05 09:22 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [("core", "0004_pull_user_provided_base_sha")] + + operations = [ + migrations.CreateModel( + name="CommitComparison", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "state", + models.TextField( + choices=[ + ("pending", "Pending"), + ("error", "Error"), + ("processed", "Processed"), + ], + default="pending", + ), + ), + ( + "report_storage_path", + models.CharField(blank=True, max_length=150, null=True), + ), + ( + "base_commit", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="base_commit_comparisons", + to="core.commit", + ), + ), + ( + "compare_commit", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="compare_commit_comparisons", + to="core.commit", + ), + ), + ], + ), + migrations.AddConstraint( + model_name="commitcomparison", + constraint=models.UniqueConstraint( + fields=("base_commit", "compare_commit"), + name="unique_comparison_between_commit", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/compare/migrations/0002_commitcomparison_patch_totals.py b/libs/shared/shared/django_apps/compare/migrations/0002_commitcomparison_patch_totals.py new file mode 100644 index 0000000000..f84a27b9a8 --- /dev/null +++ b/libs/shared/shared/django_apps/compare/migrations/0002_commitcomparison_patch_totals.py @@ -0,0 +1,15 @@ +# Generated by Django 3.1.6 on 2021-09-02 04:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("compare", "0001_initial")] + + operations = [ + migrations.AddField( + model_name="commitcomparison", + name="patch_totals", + field=models.JSONField(null=True), + ) + ] diff --git a/libs/shared/shared/django_apps/compare/migrations/0003_commitcomparison_error.py b/libs/shared/shared/django_apps/compare/migrations/0003_commitcomparison_error.py new file mode 100644 index 0000000000..2cbd0d93c7 --- /dev/null +++ b/libs/shared/shared/django_apps/compare/migrations/0003_commitcomparison_error.py @@ -0,0 +1,21 @@ +# Generated by Django 3.1.13 on 2021-09-23 02:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("compare", "0002_commitcomparison_patch_totals")] + + operations = [ + migrations.AddField( + model_name="commitcomparison", + name="error", + field=models.TextField( + choices=[ + ("missing_base_report", "Missing Base Report"), + ("missing_head_report", "Missing Head Report"), + ], + null=True, + ), + ) + ] diff --git a/libs/shared/shared/django_apps/compare/migrations/0004_flagcomparison.py b/libs/shared/shared/django_apps/compare/migrations/0004_flagcomparison.py new file mode 100644 index 0000000000..0cd27b2e8a --- /dev/null +++ b/libs/shared/shared/django_apps/compare/migrations/0004_flagcomparison.py @@ -0,0 +1,46 @@ +# Generated by Django 3.2.12 on 2022-07-01 00:57 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0003_auto_20211118_1150"), + ("compare", "0003_commitcomparison_error"), + ] + + operations = [ + migrations.CreateModel( + name="FlagComparison", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("coverage_totals", models.JSONField(null=True)), + ("patch_totals", models.JSONField(null=True)), + ( + "commit_comparison", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="commit_comparisons", + to="compare.commitcomparison", + ), + ), + ( + "repositoryflag", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="flag_comparisons", + to="reports.repositoryflag", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/libs/shared/shared/django_apps/compare/migrations/0005_auto_20220713_2210.py b/libs/shared/shared/django_apps/compare/migrations/0005_auto_20220713_2210.py new file mode 100644 index 0000000000..4f71febec0 --- /dev/null +++ b/libs/shared/shared/django_apps/compare/migrations/0005_auto_20220713_2210.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.12 on 2022-07-13 22:10 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("compare", "0004_flagcomparison"), + ] + + operations = [ + migrations.RenameField( + model_name="flagcomparison", + old_name="coverage_totals", + new_name="head_totals", + ), + migrations.AddField( + model_name="flagcomparison", + name="base_totals", + field=models.JSONField(null=True), + ), + migrations.AlterField( + model_name="flagcomparison", + name="commit_comparison", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="flag_comparisons", + to="compare.commitcomparison", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/compare/migrations/0006_componentcomparison_and_more.py b/libs/shared/shared/django_apps/compare/migrations/0006_componentcomparison_and_more.py new file mode 100644 index 0000000000..f405d84749 --- /dev/null +++ b/libs/shared/shared/django_apps/compare/migrations/0006_componentcomparison_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.1.7 on 2023-04-20 14:33 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("compare", "0005_auto_20220713_2210"), + ] + + operations = [ + migrations.CreateModel( + name="ComponentComparison", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("component_id", models.TextField()), + ("head_totals", models.JSONField(null=True)), + ("base_totals", models.JSONField(null=True)), + ("patch_totals", models.JSONField(null=True)), + ( + "commit_comparison", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="component_comparisons", + to="compare.commitcomparison", + ), + ), + ], + ), + migrations.AddIndex( + model_name="componentcomparison", + index=models.Index( + fields=["commit_comparison_id", "component_id"], + name="component_comparison_component", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/compare/migrations/__init__.py b/libs/shared/shared/django_apps/compare/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/compare/models.py b/libs/shared/shared/django_apps/compare/models.py new file mode 100644 index 0000000000..92ad18bd46 --- /dev/null +++ b/libs/shared/shared/django_apps/compare/models.py @@ -0,0 +1,92 @@ +from django.db import models +from django_prometheus.models import ExportModelOperationsMixin + +from shared.django_apps.codecov.models import BaseCodecovModel +from shared.django_apps.core.models import Commit +from shared.django_apps.reports.models import RepositoryFlag + +# Added to avoid 'doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS' error\ +# Needs to be called the same as the API app +COMPARE_APP_LABEL = "compare" + + +class CommitComparison( + ExportModelOperationsMixin("compare.commit_comparison"), BaseCodecovModel +): + class CommitComparisonStates(models.TextChoices): + PENDING = "pending" + ERROR = "error" + PROCESSED = "processed" + + class CommitComparisonErrors(models.TextChoices): + MISSING_BASE_REPORT = "missing_base_report" + MISSING_HEAD_REPORT = "missing_head_report" + + base_commit = models.ForeignKey( + Commit, on_delete=models.CASCADE, related_name="base_commit_comparisons" + ) + compare_commit = models.ForeignKey( + Commit, on_delete=models.CASCADE, related_name="compare_commit_comparisons" + ) + state = models.TextField( + choices=CommitComparisonStates.choices, default=CommitComparisonStates.PENDING + ) + error = models.TextField(choices=CommitComparisonErrors.choices, null=True) + report_storage_path = models.CharField(max_length=150, null=True, blank=True) + patch_totals = models.JSONField(null=True) + + class Meta: + app_label = COMPARE_APP_LABEL + db_table = "compare_commitcomparison" + + constraints = [ + models.UniqueConstraint( + name="unique_comparison_between_commit", + fields=["base_commit", "compare_commit"], + ) + ] + + @property + def is_processed(self): + return self.state == CommitComparison.CommitComparisonStates.PROCESSED + + +class FlagComparison( + ExportModelOperationsMixin("compare.flag_comparison"), BaseCodecovModel +): + commit_comparison = models.ForeignKey( + CommitComparison, on_delete=models.CASCADE, related_name="flag_comparisons" + ) + repositoryflag = models.ForeignKey( + RepositoryFlag, on_delete=models.CASCADE, related_name="flag_comparisons" + ) + head_totals = models.JSONField(null=True) + base_totals = models.JSONField(null=True) + patch_totals = models.JSONField(null=True) + + class Meta: + app_label = COMPARE_APP_LABEL + db_table = "compare_flagcomparison" + + +class ComponentComparison( + ExportModelOperationsMixin("compare.component_comparison"), BaseCodecovModel +): + commit_comparison = models.ForeignKey( + CommitComparison, on_delete=models.CASCADE, related_name="component_comparisons" + ) + component_id = models.TextField(null=False, blank=False) + head_totals = models.JSONField(null=True) + base_totals = models.JSONField(null=True) + patch_totals = models.JSONField(null=True) + + class Meta: + app_label = COMPARE_APP_LABEL + db_table = "compare_componentcomparison" + + indexes = [ + models.Index( + fields=["commit_comparison_id", "component_id"], + name="component_comparison_component", + ), + ] diff --git a/libs/shared/shared/django_apps/compare/tests/__init__.py b/libs/shared/shared/django_apps/compare/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/compare/tests/factories.py b/libs/shared/shared/django_apps/compare/tests/factories.py new file mode 100644 index 0000000000..1ed3013f1e --- /dev/null +++ b/libs/shared/shared/django_apps/compare/tests/factories.py @@ -0,0 +1,120 @@ +import factory + +from shared.django_apps.core.tests.factories import CommitFactory +from shared.django_apps.reports.tests.factories import RepositoryFlagFactory + +from ..models import CommitComparison, ComponentComparison, FlagComparison + + +class CommitComparisonFactory(factory.django.DjangoModelFactory): + class Meta: + model = CommitComparison + + base_commit = factory.SubFactory(CommitFactory) + compare_commit = factory.SubFactory(CommitFactory) + + +class FlagComparisonFactory(factory.django.DjangoModelFactory): + class Meta: + model = FlagComparison + + commit_comparison = factory.SubFactory(CommitComparisonFactory) + repositoryflag = factory.SubFactory(RepositoryFlagFactory) + head_totals = { + "diff": 0, + "hits": 12, + "files": 1, + "lines": 14, + "misses": 1, + "methods": 5, + "branches": 3, + "coverage": "85.71429", + "messages": 0, + "partials": 1, + "sessions": 1, + "complexity": 0, + "complexity_total": 0, + } + patch_totals = { + "diff": 0, + "hits": 2, + "files": 2, + "lines": 7, + "misses": 4, + "methods": 2, + "branches": 2, + "coverage": "28.57143", + "messages": 0, + "partials": 1, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + } + base_totals = { + "diff": 0, + "hits": 2, + "files": 2, + "lines": 7, + "misses": 4, + "methods": 2, + "branches": 2, + "coverage": "72.92638", + "messages": 0, + "partials": 1, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + } + + +class ComponentComparisonFactory(factory.django.DjangoModelFactory): + class Meta: + model = ComponentComparison + + commit_comparison = factory.SubFactory(CommitComparisonFactory) + component_id = "test_component" + head_totals = { + "diff": 0, + "hits": 12, + "files": 1, + "lines": 14, + "misses": 1, + "methods": 5, + "branches": 3, + "coverage": "85.71429", + "messages": 0, + "partials": 1, + "sessions": 1, + "complexity": 0, + "complexity_total": 0, + } + patch_totals = { + "diff": 0, + "hits": 2, + "files": 2, + "lines": 7, + "misses": 4, + "methods": 2, + "branches": 2, + "coverage": "28.57143", + "messages": 0, + "partials": 1, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + } + base_totals = { + "diff": 0, + "hits": 2, + "files": 2, + "lines": 7, + "misses": 4, + "methods": 2, + "branches": 2, + "coverage": "72.92638", + "messages": 0, + "partials": 1, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + } diff --git a/libs/shared/shared/django_apps/core/__init__.py b/libs/shared/shared/django_apps/core/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/core/encoders.py b/libs/shared/shared/django_apps/core/encoders.py new file mode 100644 index 0000000000..2665a63418 --- /dev/null +++ b/libs/shared/shared/django_apps/core/encoders.py @@ -0,0 +1,10 @@ +from dataclasses import astuple, is_dataclass + +from django.core.serializers.json import DjangoJSONEncoder + + +class ReportJSONEncoder(DjangoJSONEncoder): + def default(self, obj): + if is_dataclass(obj): + return astuple(obj) + return super().default(self, obj) diff --git a/libs/shared/shared/django_apps/core/managers.py b/libs/shared/shared/django_apps/core/managers.py new file mode 100644 index 0000000000..f3ee8f33e0 --- /dev/null +++ b/libs/shared/shared/django_apps/core/managers.py @@ -0,0 +1,303 @@ +import datetime +import logging + +from dateutil import parser +from django.db import IntegrityError +from django.db.models import ( + F, + FloatField, + IntegerField, + Manager, + OuterRef, + Q, + QuerySet, + Subquery, + Value, +) +from django.db.models.fields.json import KeyTextTransform +from django.db.models.functions import Cast, Coalesce +from django.utils import timezone + +log = logging.getLogger("__name__") + + +class RepositoryQuerySet(QuerySet): + def viewable_repos(self, owner): + """ + Filters queryset so that result only includes repos viewable by the + given owner. + """ + filters = Q(private=False) + + if owner is not None: + filters = filters | Q(author__ownerid=owner.ownerid) + if owner.permission: + filters = filters | Q(repoid__in=owner.permission) + + filters &= ~Q(deleted=True) + + return self.filter(filters).exclude(name=None) + + def exclude_accounts_enforced_okta( + self, + authenticated_okta_account_ids: list[int], + ) -> QuerySet: + """Excludes any private repos for an organization that have a configured okta_setting and enforced=True. + We only show these private repos for users who have authenticated with Okta.""" + return self.exclude( + Q(private=True) + & Q(author__account_id__isnull=False) + & Q(author__account__okta_settings__isnull=False) + & Q(author__account__okta_settings__enforced=True) + & ~Q(author__account_id__in=authenticated_okta_account_ids), + ) + + def exclude_uncovered(self): + """ + Excludes repositories with no latest-commit val. Requires calling + 'with_latest_commit_totals_before' on queryset first. + """ + return self.exclude(latest_commit_totals__isnull=True) + + def with_recent_coverage(self) -> QuerySet: + """ + Annotates queryset with recent commit totals from latest commit + that is more than an hour old. This ensures that the coverage totals + are not changing as the most recent commit is uploading coverage + reports. + """ + from shared.django_apps.core.models import Commit + + timestamp = timezone.now() - timezone.timedelta(hours=1) + + commits_queryset = Commit.objects.filter( + repository_id=OuterRef("pk"), + state=Commit.CommitStates.COMPLETE, + branch=OuterRef("branch"), + timestamp__lte=timestamp, + ).order_by("-timestamp") + + coverage = Cast( + KeyTextTransform("c", "recent_commit_totals"), + output_field=FloatField(), + ) + hits = Cast( + KeyTextTransform("h", "recent_commit_totals"), + output_field=IntegerField(), + ) + misses = Cast( + KeyTextTransform("m", "recent_commit_totals"), + output_field=IntegerField(), + ) + lines = Cast( + KeyTextTransform("n", "recent_commit_totals"), + output_field=IntegerField(), + ) + + return self.annotate( + recent_commit_totals=Subquery(commits_queryset.values("totals")[:1]), + coverage_sha=Subquery(commits_queryset.values("commitid")[:1]), + recent_coverage=coverage, + coverage=Coalesce( + coverage, + Value(-1), + output_field=FloatField(), + ), + hits=hits, + misses=misses, + lines=lines, + ) + + def with_latest_commit_totals_before( + self, before_date, branch, include_previous_totals=False + ): + """ + Annotates queryset with coverage of latest commit totals before cerain date. + """ + from shared.django_apps.core.models import Commit + + # Parsing the date given in parameters so we receive a datetime rather than a string + timestamp = parser.parse(before_date) + + commit_query_set = Commit.objects.filter( + repository_id=OuterRef("repoid"), + state=Commit.CommitStates.COMPLETE, + branch=branch or OuterRef("branch"), + # The __date cast function will case the datetime based timestamp on the commit to a date object that only + # contains the year, month and day. This allows us to filter through a daily granularity rather than + # a second granularity since this is the level of granularity we get from other parts of the API. + timestamp__date__lte=timestamp, + ).order_by("-timestamp") + + queryset = self.annotate( + latest_commit_totals=Subquery(commit_query_set.values("totals")[:1]) + ) + + if include_previous_totals: + queryset = queryset.annotate( + prev_commit_totals=Subquery(commit_query_set.values("totals")[1:2]) + ) + return queryset + + def with_latest_coverage_change(self): + """ + Annotates the queryset with the latest "coverage change" (cov of last commit + made to default branch, minus cov of second-to-last commit made to default + branch) of each repository. Depends on having called "with_latest_commit_totals_before" with + "include_previous_totals=True". + """ + + return self.annotate( + latest_coverage=Cast( + KeyTextTransform("c", "latest_commit_totals"), output_field=FloatField() + ), + second_latest_coverage=Cast( + KeyTextTransform("c", "prev_commit_totals"), output_field=FloatField() + ), + ).annotate( + latest_coverage_change=F("latest_coverage") - F("second_latest_coverage") + ) + + def with_latest_commit_at(self): + """ + Annotates queryset with latest commit based on a Repository. We annotate: + - true_latest_commit_at as the real value from the table + - latest_commit_at as the true_coverage except NULL are transformed to 1/1/1900 + This make sure when we order the repo with no commit appears last. + """ + from shared.django_apps.core.models import Commit + + latest_commit_at = Subquery( + Commit.objects.filter(repository_id=OuterRef("pk")) + .order_by("-timestamp") + .values("timestamp")[:1] + ) + return self.annotate( + true_latest_commit_at=latest_commit_at, + latest_commit_at=Coalesce( + latest_commit_at, Value(datetime.datetime(1900, 1, 1)) + ), + ) + + def with_oldest_commit_at(self): + """ + Annotates the queryset with the oldest commit timestamp. + """ + from shared.django_apps.core.models import Commit + + commits = Commit.objects.filter(repository_id=OuterRef("pk")).order_by( + "timestamp" + ) + return self.annotate( + oldest_commit_at=Subquery(commits.values("timestamp")[:1]), + ) + + def get_or_create_from_git_repo(self, git_repo, owner): + from shared.django_apps.codecov_auth.models import Owner + + service_id = git_repo.get("service_id") or git_repo.get("id") + name = git_repo["name"] + + defaults = { + "private": git_repo["private"], + "branch": git_repo.get("branch") + or git_repo.get("default_branch") + or "main", + "name": name, + "service_id": service_id, + } + + try: + # covers renames, branch updates, public/private + repo, created = self.update_or_create( + author=owner, service_id=service_id, defaults=defaults + ) + + log.info( + "[GetOrCreateFromGitRepo] - Repo successfully updated or created", + extra=dict( + defaults=defaults, + author=owner.ownerid, + ), + ) + + except IntegrityError: + # if service_id changes / transfers(?) + repo, created = self.update_or_create( + author=owner, name=name, defaults=defaults + ) + + log.warning( + "[GetOrCreateFromGitRepo] - Integrity error, service id changed", + extra=dict( + defaults=defaults, + author=owner.ownerid, + ), + ) + + # If this is a fork, create the forked repo and save it to the new repo. + # Depending on the source of this data, 'fork' may either be a boolean or a dict + # containing data of the fork. In the case it is a boolean, the forked repo's data + # is contained in the 'parent' field. + fork = git_repo.get("fork") + if fork: + if isinstance(fork, dict): + git_repo_fork = git_repo["fork"]["repo"] + git_repo_fork_owner = git_repo["fork"]["owner"] + + elif isinstance(fork, bool): + # This is supposed to indicate that the repo json comes + # in the form of a github API repo + # (https://docs.github.com/en/rest/reference/repos#get-a-repository) + # but sometimes this will unexpectedly be missing the 'parent' field, + # which contains information about a fork's parent. So we check again + # below. + parent = git_repo.get("parent") + if parent: + git_repo_fork_owner = { + "service_id": parent["owner"]["id"], + "username": parent["owner"]["login"], + } + git_repo_fork = { + "service_id": parent["id"], + "private": parent["private"], + "language": parent["language"], + "branch": parent["default_branch"], + "name": parent["name"], + } + else: + # If the parent data doesn't exist, there is nothing else to do. + return repo, created + + fork_owner, _ = Owner.objects.get_or_create( + service=owner.service, + username=git_repo_fork_owner["username"], + service_id=git_repo_fork_owner["service_id"], + defaults={"createstamp": timezone.now()}, + ) + fork, _ = self.get_or_create( + author=fork_owner, + service_id=git_repo_fork["service_id"], + private=git_repo_fork["private"], + branch=git_repo_fork.get("branch") + or git_repo_fork.get("default_branch"), + name=git_repo_fork["name"], + ) + repo.fork = fork + repo.save() + + return repo, created + + +# We cannot use `QuerySet.as_manager()` since it relies on the `inspect` module and will +# not play nicely with Cython (which we use for self-hosted): +# https://cython.readthedocs.io/en/latest/src/userguide/limitations.html#inspect-support +class RepositoryManager(Manager): + def get_queryset(self): + return RepositoryQuerySet(self.model, using=self._db) + + def viewable_repos(self, *args, **kwargs): + return self.get_queryset().viewable_repos(*args, **kwargs) + + def get_or_create_from_git_repo(self, *args, **kwargs): + return self.get_queryset().get_or_create_from_git_repo(*args, **kwargs) diff --git a/libs/shared/shared/django_apps/core/migrations/0001_initial.py b/libs/shared/shared/django_apps/core/migrations/0001_initial.py new file mode 100644 index 0000000000..69953450c6 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0001_initial.py @@ -0,0 +1,332 @@ +# Generated by Django 3.1.6 on 2021-04-08 19:33 + +import datetime +import uuid + +import django.contrib.postgres.fields +import django.contrib.postgres.fields.citext +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + +from shared.django_apps.core.encoders import ReportJSONEncoder +from shared.django_apps.core.models import DateTimeWithoutTZField, _gen_image_token + + +class Migration(migrations.Migration): + initial = True + + dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] + + operations = [ + migrations.CreateModel( + name="Commit", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("commitid", models.TextField()), + ( + "timestamp", + DateTimeWithoutTZField(default=datetime.datetime.now), + ), + ( + "updatestamp", + DateTimeWithoutTZField(default=datetime.datetime.now), + ), + ("ci_passed", models.BooleanField(null=True)), + ("totals", models.JSONField(null=True)), + ( + "report", + models.JSONField(encoder=ReportJSONEncoder, null=True), + ), + ("merged", models.BooleanField(null=True)), + ("deleted", models.BooleanField(null=True)), + ("notified", models.BooleanField(null=True)), + ("branch", models.TextField(null=True)), + ("pullid", models.IntegerField(null=True)), + ("message", models.TextField(null=True)), + ("parent_commit_id", models.TextField(db_column="parent", null=True)), + ( + "state", + models.TextField( + choices=[ + ("complete", "Complete"), + ("pending", "Pending"), + ("error", "Error"), + ("skipped", "Skipped"), + ], + null=True, + ), + ), + ( + "author", + models.ForeignKey( + db_column="author", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="codecov_auth.owner", + ), + ), + ], + options={"db_table": "commits"}, + ), + migrations.CreateModel( + name="Version", + fields=[("version", models.TextField(primary_key=True, serialize=False))], + options={"db_table": "version"}, + ), + migrations.CreateModel( + name="Repository", + fields=[ + ("repoid", models.AutoField(primary_key=True, serialize=False)), + ("name", django.contrib.postgres.fields.citext.CITextField()), + ("service_id", models.TextField()), + ("private", models.BooleanField()), + ("updatestamp", models.DateTimeField(auto_now=True)), + ("active", models.BooleanField(null=True)), + ("language", models.TextField(blank=True, null=True)), + ("branch", models.TextField(default="master")), + ("upload_token", models.UUIDField(default=uuid.uuid4, unique=True)), + ("yaml", models.JSONField(null=True)), + ("cache", models.JSONField(null=True)), + ( + "image_token", + models.TextField(default=_gen_image_token, null=True), + ), + ("using_integration", models.BooleanField(null=True)), + ("hookid", models.TextField(null=True)), + ("activated", models.BooleanField(default=False, null=True)), + ("deleted", models.BooleanField(default=False)), + ( + "author", + models.ForeignKey( + db_column="ownerid", + on_delete=django.db.models.deletion.CASCADE, + to="codecov_auth.owner", + ), + ), + ( + "bot", + models.ForeignKey( + db_column="bot", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="bot_repos", + to="codecov_auth.owner", + ), + ), + ( + "fork", + models.ForeignKey( + blank=True, + db_column="forkid", + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + to="core.repository", + ), + ), + ], + options={"db_table": "repos", "ordering": ["-repoid"]}, + ), + migrations.CreateModel( + name="Pull", + fields=[ + ("pullid", models.IntegerField(primary_key=True, serialize=False)), + ("issueid", models.IntegerField(null=True)), + ( + "state", + models.TextField( + choices=[ + ("open", "Open"), + ("merged", "Merged"), + ("closed", "Closed"), + ], + default="open", + ), + ), + ("title", models.TextField(null=True)), + ("base", models.TextField(null=True)), + ("head", models.TextField(null=True)), + ("compared_to", models.TextField(null=True)), + ("commentid", models.TextField(null=True)), + ( + "updatestamp", + DateTimeWithoutTZField(default=datetime.datetime.now), + ), + ("diff", models.JSONField(null=True)), + ("flare", models.JSONField(null=True)), + ( + "author", + models.ForeignKey( + db_column="author", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="codecov_auth.owner", + ), + ), + ( + "repository", + models.ForeignKey( + db_column="repoid", + on_delete=django.db.models.deletion.CASCADE, + related_name="pull_requests", + to="core.repository", + ), + ), + ], + options={"db_table": "pulls", "ordering": ["-pullid"]}, + ), + migrations.CreateModel( + name="CommitNotification", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ( + "notification_type", + models.TextField( + choices=[ + ("comment", "Comment"), + ("gitter", "Gitter"), + ("hipchat", "Hipchat"), + ("irc", "Irc"), + ("slack", "Slack"), + ("status_changes", "Status Changes"), + ("status_patch", "Status Patch"), + ("status_project", "Status Project"), + ("webhook", "Webhook"), + ] + ), + ), + ( + "decoration_type", + models.TextField( + choices=[("standard", "Standard"), ("upgrade", "Upgrade")], + null=True, + ), + ), + ( + "state", + models.TextField( + choices=[ + ("pending", "Pending"), + ("success", "Success"), + ("error", "Error"), + ], + null=True, + ), + ), + ( + "created_at", + DateTimeWithoutTZField(default=datetime.datetime.now), + ), + ( + "updated_at", + DateTimeWithoutTZField(default=datetime.datetime.now), + ), + ( + "commit", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="notifications", + to="core.commit", + ), + ), + ], + options={"db_table": "commit_notifications"}, + ), + migrations.AddField( + model_name="commit", + name="repository", + field=models.ForeignKey( + db_column="repoid", + on_delete=django.db.models.deletion.CASCADE, + related_name="commits", + to="core.repository", + ), + ), + migrations.CreateModel( + name="Branch", + fields=[ + ( + "name", + models.TextField( + db_column="branch", primary_key=True, serialize=False + ), + ), + ( + "authors", + django.contrib.postgres.fields.ArrayField( + base_field=models.IntegerField(blank=True, null=True), + blank=True, + db_column="authors", + null=True, + size=None, + ), + ), + ("head", models.TextField()), + ("base", models.TextField(null=True)), + ("updatestamp", models.DateTimeField(auto_now=True)), + ( + "repository", + models.ForeignKey( + db_column="repoid", + on_delete=django.db.models.deletion.CASCADE, + related_name="branches", + to="core.repository", + ), + ), + ], + options={"db_table": "branches"}, + ), + migrations.AddConstraint( + model_name="repository", + constraint=models.UniqueConstraint( + fields=("author", "name"), name="repos_slug" + ), + ), + migrations.AddConstraint( + model_name="repository", + constraint=models.UniqueConstraint( + fields=("author", "service_id"), name="repos_service_ids" + ), + ), + migrations.AddIndex( + model_name="pull", + index=models.Index( + condition=models.Q(state="open"), + fields=["repository"], + name="pulls_repoid_state_open", + ), + ), + migrations.AddConstraint( + model_name="pull", + constraint=models.UniqueConstraint( + fields=("repository", "pullid"), name="pulls_repoid_pullid" + ), + ), + migrations.AddIndex( + model_name="commit", + index=models.Index( + fields=["repository", "-timestamp"], + name="commits_repoid_timestamp_desc", + ), + ), + migrations.AddIndex( + model_name="commit", + index=models.Index( + condition=models.Q(_negated=True, deleted=True), + fields=["repository", "pullid"], + name="commits_on_pull", + ), + ), + migrations.AddConstraint( + model_name="commit", + constraint=models.UniqueConstraint( + fields=("repository", "commitid"), name="commits_repoid_commitid" + ), + ), + migrations.AddConstraint( + model_name="branch", + constraint=models.UniqueConstraint( + fields=("name", "repository"), name="branches_repoid_branch" + ), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0002_auto_20210517_1223.py b/libs/shared/shared/django_apps/core/migrations/0002_auto_20210517_1223.py new file mode 100644 index 0000000000..1b4da8b7c9 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0002_auto_20210517_1223.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.6 on 2021-05-17 12:23 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyRunSQL + + +class Migration(migrations.Migration): + dependencies = [("core", "0001_initial")] + + operations = [ + migrations.AlterField( + model_name="repository", + name="active", + field=models.BooleanField(default=False, null=True), + ), + RiskyRunSQL("UPDATE repos SET active=false WHERE active is null;"), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0003_auto_20210520_0841.py b/libs/shared/shared/django_apps/core/migrations/0003_auto_20210520_0841.py new file mode 100644 index 0000000000..d36e8c5b19 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0003_auto_20210520_0841.py @@ -0,0 +1,44 @@ +# Generated by Django 3.1.6 on 2021-05-20 08:41 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyRunSQL + + +class Migration(migrations.Migration): + dependencies = [("core", "0002_auto_20210517_1223")] + + operations = [ + migrations.AlterField( + model_name="repository", + name="language", + field=models.TextField( + blank=True, + choices=[ + ("javascript", "Javascript"), + ("shell", "Shell"), + ("python", "Python"), + ("ruby", "Ruby"), + ("perl", "Perl"), + ("dart", "Dart"), + ("java", "Java"), + ("c", "C"), + ("clojure", "Clojure"), + ("d", "D"), + ("fortran", "Fortran"), + ("go", "Go"), + ("groovy", "Groovy"), + ("kotlin", "Kotlin"), + ("php", "Php"), + ("r", "R"), + ("scala", "Scala"), + ("swift", "Swift"), + ("objective-c", "Objective C"), + ("xtend", "Xtend"), + ], + null=True, + ), + ), + RiskyRunSQL("ALTER TABLE repos ALTER COLUMN active SET DEFAULT FALSE;"), + RiskyRunSQL("UPDATE repos SET active=false WHERE active is null;"), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0004_pull_user_provided_base_sha.py b/libs/shared/shared/django_apps/core/migrations/0004_pull_user_provided_base_sha.py new file mode 100644 index 0000000000..f308cf3f5a --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0004_pull_user_provided_base_sha.py @@ -0,0 +1,15 @@ +# Generated by Django 3.1.6 on 2021-06-23 19:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("core", "0003_auto_20210520_0841")] + + operations = [ + migrations.AddField( + model_name="pull", + name="user_provided_base_sha", + field=models.TextField(null=True), + ) + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0005_auto_20210916_0313.py b/libs/shared/shared/django_apps/core/migrations/0005_auto_20210916_0313.py new file mode 100644 index 0000000000..4df2656ab6 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0005_auto_20210916_0313.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.13 on 2021-09-16 03:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("core", "0004_pull_user_provided_base_sha")] + + operations = [ + migrations.AddField( + model_name="pull", + name="id", + field=models.BigAutoField(primary_key=True, serialize=False), + preserve_default=False, + ), + migrations.AlterField( + model_name="pull", name="pullid", field=models.IntegerField() + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0006_version_v4_6_2.py b/libs/shared/shared/django_apps/core/migrations/0006_version_v4_6_2.py new file mode 100644 index 0000000000..c416dada6e --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0006_version_v4_6_2.py @@ -0,0 +1,14 @@ +from django.db import migrations + + +def add_version(apps, schema): + version = apps.get_model("core", "Version") + version.objects.all().delete() + v = version(version="v4.6.2") + v.save() + + +class Migration(migrations.Migration): + dependencies = [("core", "0005_auto_20210916_0313")] + + operations = [migrations.RunPython(add_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0007_version_v4_6_3.py b/libs/shared/shared/django_apps/core/migrations/0007_version_v4_6_3.py new file mode 100644 index 0000000000..3f93c825de --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0007_version_v4_6_3.py @@ -0,0 +1,14 @@ +from django.db import migrations + + +def add_version(apps, schema): + version = apps.get_model("core", "Version") + version.objects.all().delete() + v = version(version="v4.6.3") + v.save() + + +class Migration(migrations.Migration): + dependencies = [("core", "0006_version_v4_6_2")] + + operations = [migrations.RunPython(add_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0008_version_v4_6_4.py b/libs/shared/shared/django_apps/core/migrations/0008_version_v4_6_4.py new file mode 100644 index 0000000000..c9a0128112 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0008_version_v4_6_4.py @@ -0,0 +1,14 @@ +from django.db import migrations + + +def add_version(apps, schema): + version = apps.get_model("core", "Version") + version.objects.all().delete() + v = version(version="v4.6.4") + v.save() + + +class Migration(migrations.Migration): + dependencies = [("core", "0007_version_v4_6_3")] + + operations = [migrations.RunPython(add_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0009_version_v4_6_5.py b/libs/shared/shared/django_apps/core/migrations/0009_version_v4_6_5.py new file mode 100644 index 0000000000..eec07e26d0 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0009_version_v4_6_5.py @@ -0,0 +1,14 @@ +from django.db import migrations + + +def add_version(apps, schema): + version = apps.get_model("core", "Version") + version.objects.all().delete() + v = version(version="v4.6.5") + v.save() + + +class Migration(migrations.Migration): + dependencies = [("core", "0008_version_v4_6_4")] + + operations = [migrations.RunPython(add_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0010_add_new_langs.py b/libs/shared/shared/django_apps/core/migrations/0010_add_new_langs.py new file mode 100644 index 0000000000..cb8229a7f8 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0010_add_new_langs.py @@ -0,0 +1,84 @@ +# Generated by Django 3.1.13 on 2022-04-06 17:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [("core", "0009_version_v4_6_5")] + + operations = [ + migrations.AlterModelOptions( + name="repository", + options={"ordering": ["-repoid"], "verbose_name_plural": "Repositories"}, + ), + migrations.AlterField( + model_name="repository", + name="language", + field=models.TextField( + blank=True, + choices=[ + ("javascript", "Javascript"), + ("shell", "Shell"), + ("python", "Python"), + ("ruby", "Ruby"), + ("perl", "Perl"), + ("dart", "Dart"), + ("java", "Java"), + ("c", "C"), + ("clojure", "Clojure"), + ("d", "D"), + ("fortran", "Fortran"), + ("go", "Go"), + ("groovy", "Groovy"), + ("kotlin", "Kotlin"), + ("php", "Php"), + ("r", "R"), + ("scala", "Scala"), + ("swift", "Swift"), + ("objective-c", "Objective C"), + ("xtend", "Xtend"), + ("typescript", "Typescript"), + ("haskell", "Haskell"), + ("rust", "Rust"), + ("lua", "Lua"), + ("matlab", "Matlab"), + ("assembly", "Assembly"), + ("scheme", "Scheme"), + ("powershell", "Powershell"), + ("apex", "Apex"), + ("verilog", "Verilog"), + ("common lisp", "Common Lisp"), + ("erlang", "Erlang"), + ("julia", "Julia"), + ("prolog", "Prolog"), + ("vue", "Vue"), + ("c++", "Cpp"), + ("c#", "C Sharp"), + ("f#", "F Sharp"), + ], + null=True, + ), + ), + migrations.RunSQL("ALTER TYPE languages ADD VALUE IF NOT exists 'typescript';"), + migrations.RunSQL("ALTER TYPE languages ADD VALUE IF NOT exists 'haskell';"), + migrations.RunSQL("ALTER TYPE languages ADD VALUE IF NOT exists 'rust';"), + migrations.RunSQL("ALTER TYPE languages ADD VALUE IF NOT exists 'lua';"), + migrations.RunSQL("ALTER TYPE languages ADD VALUE IF NOT exists 'matlab';"), + migrations.RunSQL("ALTER TYPE languages ADD VALUE IF NOT exists 'assembly';"), + migrations.RunSQL("ALTER TYPE languages ADD VALUE IF NOT exists 'scheme';"), + migrations.RunSQL("ALTER TYPE languages ADD VALUE IF NOT exists 'powershell';"), + migrations.RunSQL("ALTER TYPE languages ADD VALUE IF NOT exists 'apex';"), + migrations.RunSQL("ALTER TYPE languages ADD VALUE IF NOT exists 'verilog';"), + migrations.RunSQL( + "ALTER TYPE languages ADD VALUE IF NOT exists 'common lisp';" + ), + migrations.RunSQL("ALTER TYPE languages ADD VALUE IF NOT exists 'erlang';"), + migrations.RunSQL("ALTER TYPE languages ADD VALUE IF NOT exists 'julia';"), + migrations.RunSQL("ALTER TYPE languages ADD VALUE IF NOT exists 'prolog';"), + migrations.RunSQL("ALTER TYPE languages ADD VALUE IF NOT exists 'vue';"), + migrations.RunSQL("ALTER TYPE languages ADD VALUE IF NOT exists 'c++';"), + migrations.RunSQL("ALTER TYPE languages ADD VALUE IF NOT exists 'c#';"), + migrations.RunSQL("ALTER TYPE languages ADD VALUE IF NOT exists 'f#';"), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0011_add_decoration_type.py b/libs/shared/shared/django_apps/core/migrations/0011_add_decoration_type.py new file mode 100644 index 0000000000..6d0baccaa5 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0011_add_decoration_type.py @@ -0,0 +1,28 @@ +# Generated by Django 3.1.13 on 2022-04-27 10:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("core", "0010_add_new_langs"), + ] + + operations = [ + migrations.AlterField( + model_name="commitnotification", + name="decoration_type", + field=models.TextField( + choices=[ + ("standard", "Standard"), + ("upgrade", "Upgrade"), + ("upload_limit", "Upload Limit"), + ], + null=True, + ), + ), + migrations.RunSQL( + "ALTER TYPE decorations ADD VALUE IF NOT exists 'upload_limit';" + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0012_auto_20220511_1732.py b/libs/shared/shared/django_apps/core/migrations/0012_auto_20220511_1732.py new file mode 100644 index 0000000000..f6402f3af6 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0012_auto_20220511_1732.py @@ -0,0 +1,36 @@ +# Generated by Django 3.1.13 on 2022-05-11 17:32 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("core", "0011_add_decoration_type"), + ] + + operations = [ + migrations.RunSQL( + """-- + -- Alter field bot on Repository + -- + COMMIT; + """, + state_operations=[ + migrations.AlterField( + model_name="repository", + name="bot", + field=models.ForeignKey( + blank=True, + db_column="bot", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="bot_repos", + to="codecov_auth.owner", + ), + ), + ], + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0013_repository_repos_service_id_author.py b/libs/shared/shared/django_apps/core/migrations/0013_repository_repos_service_id_author.py new file mode 100644 index 0000000000..054d14d8fa --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0013_repository_repos_service_id_author.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.12 on 2022-05-03 02:57 + +from django.contrib.postgres.operations import AddIndexConcurrently +from django.db import migrations, models + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("core", "0012_auto_20220511_1732"), + ] + + operations = [ + AddIndexConcurrently( + model_name="repository", + index=models.Index( + fields=["service_id", "author"], name="repos_service_id_author" + ), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0014_pull_pulls_author_updatestamp.py b/libs/shared/shared/django_apps/core/migrations/0014_pull_pulls_author_updatestamp.py new file mode 100644 index 0000000000..4a661c5976 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0014_pull_pulls_author_updatestamp.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.12 on 2022-07-11 13:34 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddIndex + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Create index pulls_author_updatestamp on field(s) author, updatestamp of model pull + -- + CREATE INDEX "pulls_author_updatestamp" ON "pulls" ("author", "updatestamp"); + COMMIT; + """ + + dependencies = [ + ("core", "0013_repository_repos_service_id_author"), + ] + + operations = [ + RiskyAddIndex( + model_name="pull", + index=models.Index( + fields=["author", "updatestamp"], name="pulls_author_updatestamp" + ), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0015_commiterror.py b/libs/shared/shared/django_apps/core/migrations/0015_commiterror.py new file mode 100644 index 0000000000..b97c312e3f --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0015_commiterror.py @@ -0,0 +1,37 @@ +# Generated by Django 3.2.12 on 2022-08-09 15:14 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0014_pull_pulls_author_updatestamp"), + ] + + operations = [ + migrations.CreateModel( + name="CommitError", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("error_code", models.CharField(max_length=100)), + ("error_params", models.JSONField(default=dict)), + ( + "commit", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="errors", + to="core.commit", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0016_version_v4_6_6.py b/libs/shared/shared/django_apps/core/migrations/0016_version_v4_6_6.py new file mode 100644 index 0000000000..1a578b4d76 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0016_version_v4_6_6.py @@ -0,0 +1,14 @@ +from django.db import migrations + + +def add_version(apps, schema): + version = apps.get_model("core", "Version") + version.objects.all().delete() + v = version(version="v4.6.6") + v.save() + + +class Migration(migrations.Migration): + dependencies = [("core", "0015_commiterror")] + + operations = [migrations.RunPython(add_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0017_branch_branches_repoid_updatestamp.py b/libs/shared/shared/django_apps/core/migrations/0017_branch_branches_repoid_updatestamp.py new file mode 100644 index 0000000000..3551dfe9e1 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0017_branch_branches_repoid_updatestamp.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.12 on 2023-01-13 16:44 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddIndex + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Create index branches_repoid_updatestamp on field(s) repository, -updatestamp of model branch + -- + CREATE INDEX "branches_repoid_updatestamp" ON "branches" ("repoid", "updatestamp" DESC); + COMMIT; + """ + + dependencies = [ + ("core", "0016_version_v4_6_6"), + ] + + operations = [ + RiskyAddIndex( + model_name="branch", + index=models.Index( + fields=["repository", "-updatestamp"], + name="branches_repoid_updatestamp", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0018_commit_all_commits_on_pull.py b/libs/shared/shared/django_apps/core/migrations/0018_commit_all_commits_on_pull.py new file mode 100644 index 0000000000..0acfa75344 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0018_commit_all_commits_on_pull.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.12 on 2023-01-26 17:52 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddIndex + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Create index all_commits_on_pull on field(s) repository, pullid of model commit + -- + CREATE INDEX "all_commits_on_pull" ON "commits" ("repoid", "pullid"); + COMMIT; + """ + + dependencies = [ + ("core", "0017_branch_branches_repoid_updatestamp"), + ] + + operations = [ + RiskyAddIndex( + model_name="commit", + index=models.Index( + fields=["repository", "pullid"], name="all_commits_on_pull" + ), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0019_commit_commits_repoid_branch_state_ts.py b/libs/shared/shared/django_apps/core/migrations/0019_commit_commits_repoid_branch_state_ts.py new file mode 100644 index 0000000000..7fa0de61f9 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0019_commit_commits_repoid_branch_state_ts.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.12 on 2023-02-01 15:04 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddIndex + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Create index commits_repoid_branch_state_ts on field(s) repository, branch, state, -timestamp of model commit + -- + CREATE INDEX "commits_repoid_branch_state_ts" ON "commits" ("repoid", "branch", "state", "timestamp" DESC); + COMMIT; + """ + + dependencies = [ + ("core", "0018_commit_all_commits_on_pull"), + ] + + operations = [ + RiskyAddIndex( + model_name="commit", + index=models.Index( + fields=["repository", "branch", "state", "-timestamp"], + name="commits_repoid_branch_state_ts", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0020_commit_commits_repoid_commitid_short_and_more.py b/libs/shared/shared/django_apps/core/migrations/0020_commit_commits_repoid_commitid_short_and_more.py new file mode 100644 index 0000000000..fd403cc1bb --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0020_commit_commits_repoid_commitid_short_and_more.py @@ -0,0 +1,61 @@ +# Generated by Django 4.1.7 on 2023-03-10 18:24 + +import django.contrib.postgres.indexes +import django.db.models.functions.text +from django.contrib.postgres.operations import BtreeGinExtension, TrigramExtension +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddIndex + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Creates extension pg_trgm + -- + CREATE EXTENSION IF NOT EXISTS "pg_trgm"; + -- + -- Creates extension btree_gin + -- + CREATE EXTENSION IF NOT EXISTS "btree_gin"; + -- + -- Create index commits_repoid_commitid_short on F(repository), Substr(Lower(F(commitid)), Value(1), Value(7)) on model commit + -- + CREATE INDEX "commits_repoid_commitid_short" ON "commits" ("repoid", (SUBSTRING(LOWER("commitid"), 1, 7))); + -- + -- Create index commit_message_gin_trgm on F(repository), OpClass(Upper(F(message)), name=gin_trgm_ops) on model commit + -- + CREATE INDEX "commit_message_gin_trgm" ON "commits" USING gin ("repoid", (UPPER("message")) gin_trgm_ops); + COMMIT; + """ + + dependencies = [ + ("core", "0019_commit_commits_repoid_branch_state_ts"), + ] + + operations = [ + TrigramExtension(), + BtreeGinExtension(), + RiskyAddIndex( + model_name="commit", + index=models.Index( + models.F("repository"), + django.db.models.functions.text.Substr( + django.db.models.functions.text.Lower("commitid"), 1, 7 + ), + name="commits_repoid_commitid_short", + ), + ), + RiskyAddIndex( + model_name="commit", + index=django.contrib.postgres.indexes.GinIndex( + models.F("repository"), + django.contrib.postgres.indexes.OpClass( + django.db.models.functions.text.Upper("message"), + name="gin_trgm_ops", + ), + name="commit_message_gin_trgm", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0021_pull_behind_by_pull_behind_by_commit.py b/libs/shared/shared/django_apps/core/migrations/0021_pull_behind_by_pull_behind_by_commit.py new file mode 100644 index 0000000000..79eb3d82d1 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0021_pull_behind_by_pull_behind_by_commit.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1.7 on 2023-04-04 11:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0020_commit_commits_repoid_commitid_short_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="pull", + name="behind_by", + field=models.IntegerField(null=True), + ), + migrations.AddField( + model_name="pull", + name="behind_by_commit", + field=models.TextField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0022_pull_pulls_repoid_pullid_ts.py b/libs/shared/shared/django_apps/core/migrations/0022_pull_pulls_repoid_pullid_ts.py new file mode 100644 index 0000000000..3f8519f98d --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0022_pull_pulls_repoid_pullid_ts.py @@ -0,0 +1,30 @@ +# Generated by Django 4.1.7 on 2023-04-24 18:59 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddIndex + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Create index pulls_repoid_pullid_ts on field(s) repository, pullid, updatestamp of model pull + -- + CREATE INDEX "pulls_repoid_pullid_ts" ON "pulls" ("repoid", "pullid", "updatestamp"); + COMMIT; + """ + + dependencies = [ + ("core", "0021_pull_behind_by_pull_behind_by_commit"), + ] + + operations = [ + RiskyAddIndex( + model_name="pull", + index=models.Index( + fields=["repository", "pullid", "updatestamp"], + name="pulls_repoid_pullid_ts", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0023_alter_commitnotification_decoration_type.py b/libs/shared/shared/django_apps/core/migrations/0023_alter_commitnotification_decoration_type.py new file mode 100644 index 0000000000..792915fdfa --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0023_alter_commitnotification_decoration_type.py @@ -0,0 +1,46 @@ +# Generated by Django 4.1.7 on 2023-05-10 07:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + # -- + # -- Alter field decoration_type on commitnotification + # -- + # -- (no-op) + # -- + # -- Raw SQL operation + # -- + # ALTER TYPE decorations ADD VALUE IF NOT exists 'passing_empty_upload'; + # -- + # -- Raw SQL operation + # -- + # ALTER TYPE decorations ADD VALUE IF NOT exists 'failing_empty_upload'; + + atomic = False + dependencies = [ + ("core", "0022_pull_pulls_repoid_pullid_ts"), + ] + + operations = [ + migrations.AlterField( + model_name="commitnotification", + name="decoration_type", + field=models.TextField( + choices=[ + ("standard", "Standard"), + ("upgrade", "Upgrade"), + ("upload_limit", "Upload Limit"), + ("passing_empty_upload", "Passing Empty Upload"), + ("failing_empty_upload", "Failing Empty Upload"), + ], + null=True, + ), + ), + migrations.RunSQL( + "ALTER TYPE decorations ADD VALUE IF NOT exists 'passing_empty_upload';" + ), + migrations.RunSQL( + "ALTER TYPE decorations ADD VALUE IF NOT exists 'failing_empty_upload';" + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0024_alter_commit_timestamp_alter_commit_updatestamp_and_more.py b/libs/shared/shared/django_apps/core/migrations/0024_alter_commit_timestamp_alter_commit_updatestamp_and_more.py new file mode 100644 index 0000000000..01667ef402 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0024_alter_commit_timestamp_alter_commit_updatestamp_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 4.1.7 on 2023-05-29 15:24 + +import django.utils.timezone +from django.db import migrations, models + +from shared.django_apps.core.models import DateTimeWithoutTZField + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0023_alter_commitnotification_decoration_type"), + ] + + operations = [ + migrations.AlterField( + model_name="commit", + name="timestamp", + field=DateTimeWithoutTZField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name="commit", + name="updatestamp", + field=DateTimeWithoutTZField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name="commitnotification", + name="notification_type", + field=models.TextField( + choices=[ + ("comment", "Comment"), + ("gitter", "Gitter"), + ("hipchat", "Hipchat"), + ("irc", "Irc"), + ("slack", "Slack"), + ("status_changes", "Status Changes"), + ("status_patch", "Status Patch"), + ("status_project", "Status Project"), + ("webhook", "Webhook"), + ("codecov_slack_app", "Codecov Slack App"), + ] + ), + ), + migrations.AlterField( + model_name="pull", + name="updatestamp", + field=DateTimeWithoutTZField(default=django.utils.timezone.now), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0025_v5_0_1.py b/libs/shared/shared/django_apps/core/migrations/0025_v5_0_1.py new file mode 100644 index 0000000000..8464509af4 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0025_v5_0_1.py @@ -0,0 +1,16 @@ +from django.db import migrations + + +def add_version(apps, schema): + version = apps.get_model("core", "Version") + version.objects.all().delete() + v = version(version="v5.0.1") + v.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0024_alter_commit_timestamp_alter_commit_updatestamp_and_more") + ] + + operations = [migrations.RunPython(add_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0026_auto_20230605_1134.py b/libs/shared/shared/django_apps/core/migrations/0026_auto_20230605_1134.py new file mode 100644 index 0000000000..83a199172f --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0026_auto_20230605_1134.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.7 on 2023-06-05 11:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0025_v5_0_1"), + ] + + operations = [ + migrations.RunSQL( + """ + ALTER TYPE notifications ADD VALUE IF NOT exists 'codecov_slack_app'; + """ + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0027_alter_commit_report_rename_report_commit__report_and_more.py b/libs/shared/shared/django_apps/core/migrations/0027_alter_commit_report_rename_report_commit__report_and_more.py new file mode 100644 index 0000000000..a342d75c7a --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0027_alter_commit_report_rename_report_commit__report_and_more.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.2 on 2023-07-18 07:33 + +from django.db import migrations, models + +from shared.django_apps.core.encoders import ReportJSONEncoder + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0026_auto_20230605_1134"), + ] + + # BEGIN; + # -- + # -- Alter field report on commit + # -- + # -- (no-op) + # -- + # -- Rename field report on commit to _report + # -- + # -- (no-op) + # -- + # -- Add field _report_storage_path to commit + # -- + # ALTER TABLE "commits" ADD COLUMN "report_storage_path" varchar(200) NULL; + # COMMIT; + + operations = [ + migrations.AlterField( + model_name="commit", + name="report", + field=models.JSONField( + db_column="report", encoder=ReportJSONEncoder, null=True + ), + ), + migrations.RenameField( + model_name="commit", + old_name="report", + new_name="_report", + ), + migrations.AddField( + model_name="commit", + name="_report_storage_path", + field=models.URLField(db_column="report_storage_path", null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0028_repository_webhook_secret.py b/libs/shared/shared/django_apps/core/migrations/0028_repository_webhook_secret.py new file mode 100644 index 0000000000..b746412261 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0028_repository_webhook_secret.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.2 on 2023-07-24 16:38 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddField + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Add field webhook_secret to repository + -- + ALTER TABLE "repos" ADD COLUMN "webhook_secret" text NULL; + COMMIT; + """ + + dependencies = [ + ("core", "0027_alter_commit_report_rename_report_commit__report_and_more"), + ] + + operations = [ + RiskyAddField( + model_name="repository", + name="webhook_secret", + field=models.TextField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0029_constants_delete_version.py b/libs/shared/shared/django_apps/core/migrations/0029_constants_delete_version.py new file mode 100644 index 0000000000..34b2539cd4 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0029_constants_delete_version.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.2 on 2023-07-27 15:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0028_repository_webhook_secret"), + ] + + operations = [ + migrations.CreateModel( + name="Constants", + fields=[ + ("key", models.CharField(primary_key=True, serialize=False)), + ("value", models.CharField()), + ], + options={ + "db_table": "constants", + }, + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0030_auto_20230727_1539.py b/libs/shared/shared/django_apps/core/migrations/0030_auto_20230727_1539.py new file mode 100644 index 0000000000..9cc1af28cc --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0030_auto_20230727_1539.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.2 on 2023-07-27 15:39 + +from uuid import uuid4 + +from django.db import migrations + + +def generate_constants(apps, schema_editor): + Constants = apps.get_model("core", "Constants") + version = Constants(key="version", value="23.7.27") + install_id = Constants(key="install_id", value=uuid4()) + version.save() + install_id.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0029_constants_delete_version"), + ] + + operations = [migrations.RunPython(generate_constants)] diff --git a/libs/shared/shared/django_apps/core/migrations/0031_auto_20230731_1627.py b/libs/shared/shared/django_apps/core/migrations/0031_auto_20230731_1627.py new file mode 100644 index 0000000000..616ef65d2c --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0031_auto_20230731_1627.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.2 on 2023-07-31 16:27 + +from django.db import migrations + + +def update_version(apps, schema_editor): + version = apps.get_model("core", "Version") + version.objects.all().delete() + v = version(version="23.7.27") + v.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0030_auto_20230727_1539"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0032_auto_20230731_1641.py b/libs/shared/shared/django_apps/core/migrations/0032_auto_20230731_1641.py new file mode 100644 index 0000000000..f77c719fd6 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0032_auto_20230731_1641.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.2 on 2023-07-31 16:41 + +from django.db import migrations + +from shared.django_apps.migration_utils import RiskyRunSQL + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0031_auto_20230731_1627"), + ] + + operations = [ + RiskyRunSQL( + """ + create or replace function branches_update() returns trigger as $$ + declare _ownerid int; + begin + -- update repos cache if main branch + update repos + set updatestamp = now() + where repoid = new.repoid + and branch = new.branch + returning ownerid into _ownerid; + + if found then + -- default branch updated, so we can update the owners timestamp + -- to refresh the team list + update owners + set updatestamp=now() + where ownerid=_ownerid; + end if; + + return null; + end; + $$ language plpgsql; + """ + ) + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0033_alter_pull_flare_rename_flare_pull__flare_and_more.py b/libs/shared/shared/django_apps/core/migrations/0033_alter_pull_flare_rename_flare_pull__flare_and_more.py new file mode 100644 index 0000000000..59573ade70 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0033_alter_pull_flare_rename_flare_pull__flare_and_more.py @@ -0,0 +1,42 @@ +# Generated by Django 4.2.2 on 2023-08-03 09:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0032_auto_20230731_1641"), + ] + + # BEGIN; + # -- + # -- Alter field flare on pull + # -- + # -- (no-op) + # -- + # -- Rename field flare on pull to _flare + # -- + # -- (no-op) + # -- + # -- Add field _flare_storage_path to pull + # -- + # ALTER TABLE "pulls" ADD COLUMN "flare_storage_path" varchar(200) NULL; + # COMMIT; + + operations = [ + migrations.AlterField( + model_name="pull", + name="flare", + field=models.JSONField(db_column="flare", null=True), + ), + migrations.RenameField( + model_name="pull", + old_name="flare", + new_name="_flare", + ), + migrations.AddField( + model_name="pull", + name="_flare_storage_path", + field=models.URLField(db_column="flare_storage_path", null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0034_remove_repository_cache.py b/libs/shared/shared/django_apps/core/migrations/0034_remove_repository_cache.py new file mode 100644 index 0000000000..d0a454008f --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0034_remove_repository_cache.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.2 on 2023-08-14 13:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ( + "core", + "0033_alter_pull_flare_rename_flare_pull__flare_and_more", + ), + ] + + operations = [ + migrations.RunSQL( + sql=migrations.RunSQL.noop, + state_operations=[ + migrations.RemoveField( + model_name="repository", + name="cache", + ) + ], + ) + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0035_auto_20230907_2123.py b/libs/shared/shared/django_apps/core/migrations/0035_auto_20230907_2123.py new file mode 100644 index 0000000000..3b08bd7fdb --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0035_auto_20230907_2123.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.2 on 2023-09-07 21:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + def add_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "23.9.5" + version.save() + + dependencies = [ + ("core", "0034_remove_repository_cache"), + ] + + operations = [migrations.RunPython(add_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0036_auto_20231003_1342.py b/libs/shared/shared/django_apps/core/migrations/0036_auto_20231003_1342.py new file mode 100644 index 0000000000..1cfeb78ff2 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0036_auto_20231003_1342.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-10-03 13:42 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "23.10.2" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0035_auto_20230907_2123"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0037_alter_commitnotification_decoration_type.py b/libs/shared/shared/django_apps/core/migrations/0037_alter_commitnotification_decoration_type.py new file mode 100644 index 0000000000..ebf889692b --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0037_alter_commitnotification_decoration_type.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.3 on 2023-10-06 16:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0036_auto_20231003_1342"), + ] + + operations = [ + migrations.AlterField( + model_name="commitnotification", + name="decoration_type", + field=models.TextField( + choices=[ + ("standard", "Standard"), + ("upgrade", "Upgrade"), + ("upload_limit", "Upload Limit"), + ("passing_empty_upload", "Passing Empty Upload"), + ("failing_empty_upload", "Failing Empty Upload"), + ("processing_upload", "Processing Upload"), + ], + null=True, + ), + ), + migrations.RunSQL( + "ALTER TYPE decorations ADD VALUE IF NOT exists 'processing_upload';" + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0038_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0038_increment_version.py new file mode 100644 index 0000000000..21459c4938 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0038_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-11-03 13:24 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "23.11.2" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0037_alter_commitnotification_decoration_type"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0039_pull_pulls_repoid_id.py b/libs/shared/shared/django_apps/core/migrations/0039_pull_pulls_repoid_id.py new file mode 100644 index 0000000000..6d09ed6b7e --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0039_pull_pulls_repoid_id.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.3 on 2023-10-30 16:16 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddIndex + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Create index pulls_repoid_id on field(s) repository, id of model pull + -- + CREATE INDEX "pulls_repoid_id" ON "pulls" ("repoid", "id"); + COMMIT; + """ + + dependencies = [ + ("core", "0038_increment_version"), + ] + + operations = [ + RiskyAddIndex( + model_name="pull", + index=models.Index(fields=["repository", "id"], name="pulls_repoid_id"), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0040_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0040_increment_version.py new file mode 100644 index 0000000000..592154ca67 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0040_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.7 on 2023-12-04 21:13 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "23.12.4" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0039_pull_pulls_repoid_id"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0041_pull_bundle_analysis_commentid.py b/libs/shared/shared/django_apps/core/migrations/0041_pull_bundle_analysis_commentid.py new file mode 100644 index 0000000000..7fbfda6909 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0041_pull_bundle_analysis_commentid.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.7 on 2023-12-27 17:00 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddField + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Add field bundle_analysis_commentid to pull + -- + ALTER TABLE "pulls" ADD COLUMN "bundle_analysis_commentid" text NULL; + COMMIT; + """ + + dependencies = [ + ("core", "0040_increment_version"), + ] + + operations = [ + RiskyAddField( + model_name="pull", + name="bundle_analysis_commentid", + field=models.TextField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0042_repository_languages.py b/libs/shared/shared/django_apps/core/migrations/0042_repository_languages.py new file mode 100644 index 0000000000..875635bc32 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0042_repository_languages.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.7 on 2024-01-09 18:54 + +import django.contrib.postgres.fields +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddField + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Add field languages to repository + -- + ALTER TABLE "repos" ADD COLUMN "languages" varchar[] DEFAULT '{}' NOT NULL; + ALTER TABLE "repos" ALTER COLUMN "languages" DROP DEFAULT; + COMMIT; + """ + + dependencies = [ + ("core", "0041_pull_bundle_analysis_commentid"), + ] + + operations = [ + RiskyAddField( + model_name="repository", + name="languages", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(), blank=True, default=[], size=None + ), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0043_repository_bundle_analysis_enabled.py b/libs/shared/shared/django_apps/core/migrations/0043_repository_bundle_analysis_enabled.py new file mode 100644 index 0000000000..b051f3bf20 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0043_repository_bundle_analysis_enabled.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.7 on 2024-01-09 21:10 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddField + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Add field bundle_analysis_enabled to repository + -- + ALTER TABLE "repos" ADD COLUMN "bundle_analysis_enabled" boolean DEFAULT false NOT NULL; + ALTER TABLE "repos" ALTER COLUMN "bundle_analysis_enabled" DROP DEFAULT; + COMMIT; + """ + + dependencies = [ + ("core", "0042_repository_languages"), + ] + + operations = [ + RiskyAddField( + model_name="repository", + name="bundle_analysis_enabled", + field=models.BooleanField(default=False), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0044_alter_repository_bundle_analysis_enabled_and_more.py b/libs/shared/shared/django_apps/core/migrations/0044_alter_repository_bundle_analysis_enabled_and_more.py new file mode 100644 index 0000000000..2dff49fffd --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0044_alter_repository_bundle_analysis_enabled_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 4.2.7 on 2024-01-10 12:28 + +import django.contrib.postgres.fields +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAlterField + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Alter field bundle_analysis_enabled on repository + -- + ALTER TABLE "repos" ALTER COLUMN "bundle_analysis_enabled" DROP NOT NULL; + -- + -- Alter field languages on repository + -- + ALTER TABLE "repos" ALTER COLUMN "languages" DROP NOT NULL; + COMMIT; + """ + + dependencies = [ + ("core", "0043_repository_bundle_analysis_enabled"), + ] + + operations = [ + RiskyAlterField( + model_name="repository", + name="bundle_analysis_enabled", + field=models.BooleanField(default=False, null=True), + ), + RiskyAlterField( + model_name="repository", + name="languages", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(), + blank=True, + default=[], + null=True, + size=None, + ), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0045_repository_languages_last_updated.py b/libs/shared/shared/django_apps/core/migrations/0045_repository_languages_last_updated.py new file mode 100644 index 0000000000..30f35e9d09 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0045_repository_languages_last_updated.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.7 on 2024-01-11 05:32 + +from django.db import migrations + +from shared.django_apps.core.models import DateTimeWithoutTZField +from shared.django_apps.migration_utils import RiskyAddField + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Add field languages_last_updated to repository + -- + ALTER TABLE "repos" ADD COLUMN "languages_last_updated" timestamp NULL; + COMMIT; + """ + + dependencies = [ + ("core", "0044_alter_repository_bundle_analysis_enabled_and_more"), + ] + + operations = [ + RiskyAddField( + model_name="repository", + name="languages_last_updated", + field=DateTimeWithoutTZField(blank=True, null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0046_repository_coverage_enabled.py b/libs/shared/shared/django_apps/core/migrations/0046_repository_coverage_enabled.py new file mode 100644 index 0000000000..dc71c90e76 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0046_repository_coverage_enabled.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.7 on 2024-01-15 20:36 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddField, RiskyRunSQL + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Add field coverage_enabled to repository + -- + ALTER TABLE "repos" ADD COLUMN "coverage_enabled" boolean DEFAULT false NULL; + ALTER TABLE "repos" ALTER COLUMN "coverage_enabled" DROP DEFAULT; + -- + -- Raw SQL operation + -- + UPDATE repos SET coverage_enabled=true WHERE active=true; + COMMIT; + """ + + dependencies = [ + ("core", "0045_repository_languages_last_updated"), + ] + + operations = [ + RiskyAddField( + model_name="repository", + name="coverage_enabled", + field=models.BooleanField(default=False, null=True), + ), + RiskyRunSQL("UPDATE repos SET coverage_enabled=true WHERE active=true;"), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0047_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0047_increment_version.py new file mode 100644 index 0000000000..3740abc00d --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0047_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.7 on 2024-01-31 18:04 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "24.2.1" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0046_repository_coverage_enabled"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0048_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0048_increment_version.py new file mode 100644 index 0000000000..932c80b62a --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0048_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.11 on 2024-04-01 19:36 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "24.4.1" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0047_increment_version"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0049_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0049_increment_version.py new file mode 100644 index 0000000000..86e1a2eeb7 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0049_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.11 on 2024-05-01 18:34 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "24.5.1" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0048_increment_version"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0050_commitnotification_gh_app_and_more.py b/libs/shared/shared/django_apps/core/migrations/0050_commitnotification_gh_app_and_more.py new file mode 100644 index 0000000000..091d15a757 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0050_commitnotification_gh_app_and_more.py @@ -0,0 +1,57 @@ +# Generated by Django 4.2.13 on 2024-05-21 19:33 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("codecov_auth", "0055_session_login_session"), + ("core", "0049_increment_version"), + ] + + # BEGIN; + # -- + # -- Add field gh_app to commitnotification + # -- + # ALTER TABLE "commit_notifications" ADD COLUMN "gh_app_id" bigint NULL CONSTRAINT "commit_notifications_gh_app_id_8714fedd_fk_codecov_a" REFERENCES "codecov_auth_githubappinstallation"("id") DEFERRABLE INITIALLY DEFERRED; SET CONSTRAINTS "commit_notifications_gh_app_id_8714fedd_fk_codecov_a" IMMEDIATE; + # -- + # -- Alter field notification_type on commitnotification + # -- + # -- (no-op) + # CREATE INDEX "commit_notifications_gh_app_id_8714fedd" ON "commit_notifications" ("gh_app_id"); + # COMMIT; + + operations = [ + migrations.AddField( + model_name="commitnotification", + name="gh_app", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="commit_notifications", + to="codecov_auth.githubappinstallation", + ), + ), + migrations.AlterField( + model_name="commitnotification", + name="notification_type", + field=models.TextField( + choices=[ + ("comment", "Comment"), + ("gitter", "Gitter"), + ("hipchat", "Hipchat"), + ("irc", "Irc"), + ("slack", "Slack"), + ("status_changes", "Status Changes"), + ("status_patch", "Status Patch"), + ("status_project", "Status Project"), + ("webhook", "Webhook"), + ("codecov_slack_app", "Codecov Slack App"), + ("checks_project", "Checks Project"), + ("checks_changes", "Checks Changes"), + ("checks_patch", "Checks Patch"), + ] + ), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0051_repository_test_analytics_enabled.py b/libs/shared/shared/django_apps/core/migrations/0051_repository_test_analytics_enabled.py new file mode 100644 index 0000000000..1987e32ac4 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0051_repository_test_analytics_enabled.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.13 on 2024-05-27 17:30 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddField + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Add field test_analytics_enabled to repository + -- + ALTER TABLE "repos" ADD COLUMN "test_analytics_enabled" boolean DEFAULT false NULL; + ALTER TABLE "repos" ALTER COLUMN "test_analytics_enabled" DROP DEFAULT; + COMMIT; + """ + + dependencies = [ + ("core", "0050_commitnotification_gh_app_and_more"), + ] + + operations = [ + RiskyAddField( + model_name="repository", + name="test_analytics_enabled", + field=models.BooleanField(default=False, null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0052_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0052_increment_version.py new file mode 100644 index 0000000000..6568c92b6b --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0052_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.13 on 2024-06-03 18:44 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "24.6.1" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0051_repository_test_analytics_enabled"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0053_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0053_increment_version.py new file mode 100644 index 0000000000..a553e016e0 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0053_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.13 on 2024-07-01 22:57 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "24.7.1" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0052_increment_version"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0054_alter_repository_branch.py b/libs/shared/shared/django_apps/core/migrations/0054_alter_repository_branch.py new file mode 100644 index 0000000000..6618e88053 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0054_alter_repository_branch.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.13 on 2024-07-15 14:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0053_increment_version"), + ] + + operations = [ + migrations.AlterField( + model_name="repository", + name="branch", + field=models.TextField(default="main"), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0055_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0055_increment_version.py new file mode 100644 index 0000000000..f180e6c042 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0055_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.14 on 2024-08-05 18:05 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "24.8.1" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0054_alter_repository_branch"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0056_branch_name_trgm_idx.py b/libs/shared/shared/django_apps/core/migrations/0056_branch_name_trgm_idx.py new file mode 100644 index 0000000000..6ee9d8d6c2 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0056_branch_name_trgm_idx.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.14 on 2024-08-07 13:35 + +import django.contrib.postgres.indexes +import django.db.models.functions.text +from django.db import migrations + +from shared.django_apps.migration_utils import RiskyAddIndex + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Create index name_trgm_idx on OpClass(Upper(F(name)), name=gin_trgm_ops) on model branch + -- + CREATE INDEX "name_trgm_idx" ON "branches" USING gin ((UPPER("branch")) gin_trgm_ops); + COMMIT; + """ + + dependencies = [ + ("core", "0055_increment_version"), + ] + + operations = [ + RiskyAddIndex( + model_name="branch", + index=django.contrib.postgres.indexes.GinIndex( + django.contrib.postgres.indexes.OpClass( + django.db.models.functions.text.Upper("name"), name="gin_trgm_ops" + ), + name="name_trgm_idx", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0057_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0057_increment_version.py new file mode 100644 index 0000000000..4d01c29d98 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0057_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.15 on 2024-08-29 20:08 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "24.9.1" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0056_branch_name_trgm_idx"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0058_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0058_increment_version.py new file mode 100644 index 0000000000..2c4813042b --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0058_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2024-10-01 17:24 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "24.10.1" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0057_increment_version"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0059_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0059_increment_version.py new file mode 100644 index 0000000000..32dd49c687 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0059_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2024-11-04 17:16 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "24.11.1" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0058_increment_version"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0060_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0060_increment_version.py new file mode 100644 index 0000000000..527cf5af84 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0060_increment_version.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.16 on 2024-12-02 19:52 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "24.12.2" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0059_increment_version"), + ] + + operations = [ + migrations.RunPython( + code=update_version, + reverse_code=migrations.RunPython.noop, + ) + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0061_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0061_increment_version.py new file mode 100644 index 0000000000..d9f4c34bd0 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0061_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2025-01-03 15:10 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "25.1.3" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0060_increment_version"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0062_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0062_increment_version.py new file mode 100644 index 0000000000..cdeb9a524c --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0062_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2025-01-10 19:21 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "25.1.10" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0061_increment_version"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0063_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0063_increment_version.py new file mode 100644 index 0000000000..79327ec2ba --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0063_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2025-01-15 19:54 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "25.1.16" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0062_increment_version"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0064_missing_indices.py b/libs/shared/shared/django_apps/core/migrations/0064_missing_indices.py new file mode 100644 index 0000000000..199ce605dd --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0064_missing_indices.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.16 on 2025-01-27 13:18 + +import django.db.models.deletion +from django.contrib.postgres.operations import AddIndexConcurrently +from django.db import migrations, models + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("core", "0063_increment_version"), + ] + + operations = [ + migrations.AlterField( + model_name="repository", + name="fork", + field=models.ForeignKey( + blank=True, + db_column="forkid", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.repository", + ), + ), + migrations.AlterField( + model_name="repository", + name="languages", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(), + blank=True, + default=list, + null=True, + size=None, + ), + ), + AddIndexConcurrently( + model_name="commit", + index=models.Index(fields=["author"], name="commits_author_cd2f50_idx"), + ), + AddIndexConcurrently( + model_name="repository", + index=models.Index(fields=["fork"], name="repos_forkid_4cd440_idx"), + ), + ] diff --git a/libs/shared/shared/django_apps/core/migrations/0065_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0065_increment_version.py new file mode 100644 index 0000000000..b72f627714 --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0065_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2025-02-03 19:18 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "25.2.3" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0064_missing_indices"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0066_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0066_increment_version.py new file mode 100644 index 0000000000..87b3ffcc7b --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0066_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2025-02-07 20:22 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "25.2.7" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0065_increment_version"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0067_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0067_increment_version.py new file mode 100644 index 0000000000..b6edde6a5d --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0067_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2025-03-03 18:41 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "25.3.3" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0066_increment_version"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/0068_increment_version.py b/libs/shared/shared/django_apps/core/migrations/0068_increment_version.py new file mode 100644 index 0000000000..279657642f --- /dev/null +++ b/libs/shared/shared/django_apps/core/migrations/0068_increment_version.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2025-04-01 16:38 + +from django.db import migrations + + +def update_version(apps, schema): + Constants = apps.get_model("core", "Constants") + version = Constants.objects.get(key="version") + version.value = "25.4.1" + version.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0067_increment_version"), + ] + + operations = [migrations.RunPython(update_version)] diff --git a/libs/shared/shared/django_apps/core/migrations/__init__.py b/libs/shared/shared/django_apps/core/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/core/models.py b/libs/shared/shared/django_apps/core/models.py new file mode 100644 index 0000000000..69c632a81d --- /dev/null +++ b/libs/shared/shared/django_apps/core/models.py @@ -0,0 +1,519 @@ +# Create your models here. +import random +import string +import uuid +from datetime import datetime +from typing import Optional + +from django.contrib.postgres.fields import ArrayField, CITextField +from django.contrib.postgres.indexes import GinIndex, OpClass +from django.db import models +from django.db.models.functions import Lower, Substr, Upper +from django.forms import ValidationError +from django.utils import timezone +from django.utils.functional import cached_property +from django_prometheus.models import ExportModelOperationsMixin +from model_utils import FieldTracker + +from shared.django_apps.codecov.models import BaseCodecovModel +from shared.django_apps.core.encoders import ReportJSONEncoder +from shared.django_apps.core.managers import RepositoryManager +from shared.django_apps.utils.config import should_write_data_to_storage_config_check +from shared.django_apps.utils.model_utils import ArchiveField +from shared.reports.resources import Report + +# Added to avoid 'doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS' error\ +# Needs to be called the same as the API app +CORE_APP_LABEL = "core" + + +class DateTimeWithoutTZField(models.DateTimeField): + def db_type(self, connection): + return "timestamp" + + +class Version(ExportModelOperationsMixin("core.version"), models.Model): + version = models.TextField(primary_key=True) + + class Meta: + app_label = CORE_APP_LABEL + db_table = "version" + + +class Constants(ExportModelOperationsMixin("core.constants"), models.Model): + key = models.CharField(primary_key=True) + value = models.CharField() + + class Meta: + app_label = CORE_APP_LABEL + db_table = "constants" + + +def _gen_image_token(): + return "".join( + random.choice(string.ascii_letters + string.digits) for _ in range(10) + ) + + +class Repository(ExportModelOperationsMixin("core.repository"), models.Model): + class Languages(models.TextChoices): + JAVASCRIPT = "javascript" + SHELL = "shell" + PYTHON = "python" + RUBY = "ruby" + PERL = "perl" + DART = "dart" + JAVA = "java" + C = "c" + CLOJURE = "clojure" + D = "d" + FORTRAN = "fortran" + GO = "go" + GROOVY = "groovy" + KOTLIN = "kotlin" + PHP = "php" + R = "r" + SCALA = "scala" + SWIFT = "swift" + OBJECTIVE_C = "objective-c" + XTEND = "xtend" + TYPESCRIPT = "typescript" + HASKELL = "haskell" + RUST = "rust" + LUA = "lua" + MATLAB = "matlab" + ASSEMBLY = "assembly" + SCHEME = "scheme" + POWERSHELL = "powershell" + APEX = "apex" + VERILOG = "verilog" + COMMON_LISP = "common lisp" + ERLANG = "erlang" + JULIA = "julia" + PROLOG = "prolog" + VUE = "vue" + CPP = "c++" + C_SHARP = "c#" + F_SHARP = "f#" + + repoid = models.AutoField(primary_key=True) + name = CITextField() + author = models.ForeignKey( + "codecov_auth.Owner", db_column="ownerid", on_delete=models.CASCADE + ) + service_id = models.TextField() + private = models.BooleanField() + updatestamp = models.DateTimeField(auto_now=True) + active = models.BooleanField(null=True, default=False) + language = models.TextField( + null=True, blank=True, choices=Languages.choices + ) # Really an ENUM in db + languages = ArrayField(models.CharField(), default=list, blank=True, null=True) + languages_last_updated = DateTimeWithoutTZField(null=True, blank=True) + fork = models.ForeignKey( + "core.Repository", + db_column="forkid", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + branch = models.TextField(default="main") + upload_token = models.UUIDField(unique=True, default=uuid.uuid4) + yaml = models.JSONField(null=True) + image_token = models.TextField(null=True, default=_gen_image_token) + + # DEPRECATED - replaced by GithubAppInstallation model + using_integration = models.BooleanField(null=True) + + hookid = models.TextField(null=True) + webhook_secret = models.TextField(null=True) + bot = models.ForeignKey( + "codecov_auth.Owner", + db_column="bot", + null=True, + on_delete=models.SET_NULL, + related_name="bot_repos", + blank=True, + ) + activated = models.BooleanField(null=True, default=False) + deleted = models.BooleanField(default=False) + bundle_analysis_enabled = models.BooleanField(default=False, null=True) + coverage_enabled = models.BooleanField(default=False, null=True) + test_analytics_enabled = models.BooleanField(default=False, null=True) + + # tracks field changes being saved + tracker = FieldTracker() + + class Meta: + db_table = "repos" + app_label = CORE_APP_LABEL + ordering = ["-repoid"] + indexes = [ + models.Index(fields=["fork"]), + models.Index( + fields=["service_id", "author"], + name="repos_service_id_author", + ), + ] + constraints = [ + models.UniqueConstraint(fields=["author", "name"], name="repos_slug"), + models.UniqueConstraint( + fields=["author", "service_id"], name="repos_service_ids" + ), + ] + verbose_name_plural = "Repositories" + + objects = RepositoryManager() + + def __str__(self): + return f"Repo<{self.author}/{self.name}>" + + @property + def service(self): + return self.author.service + + def clean(self): + if self.using_integration is None: + raise ValidationError("using_integration cannot be null") + + +class Branch(ExportModelOperationsMixin("core.branch"), models.Model): + name = models.TextField(primary_key=True, db_column="branch") + repository = models.ForeignKey( + "core.Repository", + db_column="repoid", + on_delete=models.CASCADE, + related_name="branches", + ) + authors = ArrayField( + models.IntegerField(null=True, blank=True), + null=True, + blank=True, + db_column="authors", + ) + head = models.TextField() + base = models.TextField(null=True) + updatestamp = models.DateTimeField(auto_now=True) + + class Meta: + db_table = "branches" + app_label = CORE_APP_LABEL + constraints = [ + models.UniqueConstraint( + fields=["name", "repository"], name="branches_repoid_branch" + ) + ] + indexes = [ + models.Index( + fields=["repository", "-updatestamp"], + name="branches_repoid_updatestamp", + ), + GinIndex(OpClass(Upper("name"), name="gin_trgm_ops"), name="name_trgm_idx"), + ] + + +class Commit(ExportModelOperationsMixin("core.commit"), models.Model): + class CommitStates(models.TextChoices): + COMPLETE = "complete" + PENDING = "pending" + ERROR = "error" + SKIPPED = "skipped" + + id = models.BigAutoField(primary_key=True) + commitid = models.TextField() + timestamp = DateTimeWithoutTZField(default=timezone.now) + updatestamp = DateTimeWithoutTZField(default=timezone.now) + author = models.ForeignKey( + "codecov_auth.Owner", db_column="author", on_delete=models.SET_NULL, null=True + ) + repository = models.ForeignKey( + "core.Repository", + db_column="repoid", + on_delete=models.CASCADE, + related_name="commits", + ) + ci_passed = models.BooleanField(null=True) + totals = models.JSONField(null=True) + merged = models.BooleanField(null=True) + deleted = models.BooleanField(null=True) + notified = models.BooleanField(null=True) + branch = models.TextField(null=True) + pullid = models.IntegerField(null=True) + message = models.TextField(null=True) + parent_commit_id = models.TextField(null=True, db_column="parent") + state = models.TextField( + null=True, choices=CommitStates.choices + ) # Really an ENUM in db + + def save(self, *args, **kwargs): + self.updatestamp = timezone.now() + super().save(*args, **kwargs) + + @cached_property + def parent_commit(self): + return Commit.objects.filter( + repository=self.repository, commitid=self.parent_commit_id + ).first() + + @cached_property + def commitreport(self): + reports = list(self.reports.all()) + # This is almost always prefetched w/ `filter(code=None)` and + # `filter(Q(report_type=None) | Q(report_type=CommitReport.ReportType.COVERAGE))` + # (in which case `.all()` returns the already filtered results) + # In the case that the reports were not prefetched we'll filter again in memory. + reports = [ + report + for report in reports + if report.code is None + and (report.report_type is None or report.report_type == "coverage") + ] + return reports[0] if reports else None + + @cached_property + def full_report(self) -> Optional[Report]: + # TODO: we should probably remove use of this method since it inverts the + # dependency tree (services should be importing models and not the other + # way around). The caching should be preserved somehow though. + from shared.reports.api_report_service import build_report_from_commit + + return build_report_from_commit(self) + + class Meta: + db_table = "commits" + app_label = CORE_APP_LABEL + constraints = [ + models.UniqueConstraint( + fields=["repository", "commitid"], name="commits_repoid_commitid" + ) + ] + indexes = [ + models.Index(fields=["author"]), + models.Index( + fields=["repository", "-timestamp"], + name="commits_repoid_timestamp_desc", + ), + models.Index( + fields=["repository", "branch", "state", "-timestamp"], + name="commits_repoid_branch_state_ts", + ), + models.Index( + fields=["repository", "pullid"], + name="commits_on_pull", + condition=~models.Q(deleted=True), + ), + models.Index( + fields=["repository", "pullid"], + name="all_commits_on_pull", + ), + models.Index( + "repository", + Substr(Lower("commitid"), 1, 7), + name="commits_repoid_commitid_short", + ), + GinIndex( + "repository", + OpClass(Upper("message"), name="gin_trgm_ops"), + name="commit_message_gin_trgm", + ), + ] + + def get_repository(self): + return self.repository + + def get_commitid(self): + return self.commitid + + @property + def external_id(self): + return self.commitid + + def should_write_to_storage(self) -> bool: + if self.repository is None or self.repository.author is None: + return False + is_codecov_repo = self.repository.author.username == "codecov" + return should_write_data_to_storage_config_check( + "commit_report", is_codecov_repo, self.repository.repoid + ) + + # Use custom JSON to properly serialize custom data classes on reports + _report = models.JSONField(null=True, db_column="report", encoder=ReportJSONEncoder) + _report_storage_path = models.URLField(null=True, db_column="report_storage_path") + report = ArchiveField( + should_write_to_storage_fn=should_write_to_storage, + json_encoder=ReportJSONEncoder, + default_value_class=dict, + ) + + +class PullStates(models.TextChoices): + OPEN = "open" + MERGED = "merged" + CLOSED = "closed" + + +class Pull(ExportModelOperationsMixin("core.pull"), models.Model): + repository = models.ForeignKey( + "core.Repository", + db_column="repoid", + on_delete=models.CASCADE, + related_name="pull_requests", + ) + id = models.BigAutoField(primary_key=True) + pullid = models.IntegerField() + issueid = models.IntegerField(null=True) + state = models.TextField( + choices=PullStates.choices, default=PullStates.OPEN.value + ) # Really an ENUM in db + title = models.TextField(null=True) + base = models.TextField(null=True) + head = models.TextField(null=True) + user_provided_base_sha = models.TextField(null=True) + compared_to = models.TextField(null=True) + commentid = models.TextField(null=True) + bundle_analysis_commentid = models.TextField(null=True) + author = models.ForeignKey( + "codecov_auth.Owner", db_column="author", on_delete=models.SET_NULL, null=True + ) + updatestamp = DateTimeWithoutTZField(default=timezone.now) + diff = models.JSONField(null=True) + behind_by = models.IntegerField(null=True) + behind_by_commit = models.TextField(null=True) + + class Meta: + db_table = "pulls" + app_label = CORE_APP_LABEL + ordering = ["-pullid"] + constraints = [ + models.UniqueConstraint( + fields=["repository", "pullid"], name="pulls_repoid_pullid" + ) + ] + indexes = [ + models.Index( + fields=["repository"], + name="pulls_repoid_state_open", + condition=models.Q(state=PullStates.OPEN.value), + ), + models.Index( + fields=["author", "updatestamp"], + name="pulls_author_updatestamp", + ), + models.Index( + fields=["repository", "pullid", "updatestamp"], + name="pulls_repoid_pullid_ts", + ), + models.Index( + fields=["repository", "id"], + name="pulls_repoid_id", + ), + ] + + def get_repository(self): + return self.repository + + def get_commitid(self): + return None + + @property + def external_id(self): + return self.pullid + + def should_write_to_storage(self) -> bool: + """ + This only applies to the flare field. + Flare is used to draw static graphs (see GraphHandler view in api) and can be large. + Flare cleanup is handled by FlareCleanupTask in worker. + """ + if self.repository is None or self.repository.author is None: + return False + is_codecov_repo = self.repository.author.username == "codecov" + return should_write_data_to_storage_config_check( + master_switch_key="pull_flare", + is_codecov_repo=is_codecov_repo, + repoid=self.repository.repoid, + ) + + _flare = models.JSONField(db_column="flare", null=True) + _flare_storage_path = models.URLField(db_column="flare_storage_path", null=True) + flare = ArchiveField( + should_write_to_storage_fn=should_write_to_storage, + default_value_class=dict, + ) + + def save(self, *args, **kwargs): + self.updatestamp = timezone.now() + super().save(*args, **kwargs) + + +class CommitNotification( + ExportModelOperationsMixin("core.commit_notification"), models.Model +): + class NotificationTypes(models.TextChoices): + COMMENT = "comment" + GITTER = "gitter" + HIPCHAT = "hipchat" + IRC = "irc" + SLACK = "slack" + STATUS_CHANGES = "status_changes" + STATUS_PATCH = "status_patch" + STATUS_PROJECT = "status_project" + WEBHOOK = "webhook" + CODECOV_SLACK_APP = "codecov_slack_app" + CHECKS_PROJECT = "checks_project" + CHECKS_CHANGES = "checks_changes" + CHECKS_PATCH = "checks_patch" + + class DecorationTypes(models.TextChoices): + STANDARD = "standard" + UPGRADE = "upgrade" + UPLOAD_LIMIT = "upload_limit" + PASSING_EMPTY_UPLOAD = "passing_empty_upload" + FAILING_EMPTY_UPLOAD = "failing_empty_upload" + PROCESSING_UPLOAD = "processing_upload" + + class States(models.TextChoices): + PENDING = "pending" + SUCCESS = "success" + ERROR = "error" + + id = models.BigAutoField(primary_key=True) + commit = models.ForeignKey( + "core.Commit", on_delete=models.CASCADE, related_name="notifications" + ) + gh_app = models.ForeignKey( + "codecov_auth.GithubAppInstallation", + on_delete=models.CASCADE, + related_name="commit_notifications", + null=True, + ) + notification_type = models.TextField( + choices=NotificationTypes.choices + ) # Really an ENUM in db + decoration_type = models.TextField( + choices=DecorationTypes.choices, null=True + ) # Really an ENUM in db + state = models.TextField(choices=States.choices, null=True) # Really an ENUM in db + created_at = DateTimeWithoutTZField(default=datetime.now) + updated_at = DateTimeWithoutTZField(default=datetime.now) + + def save(self, *args, **kwargs): + self.updated_at = timezone.now() + super().save(*args, **kwargs) + + class Meta: + app_label = CORE_APP_LABEL + db_table = "commit_notifications" + + +class CommitError(ExportModelOperationsMixin("core.commit_error"), BaseCodecovModel): + commit = models.ForeignKey( + "Commit", + related_name="errors", + on_delete=models.CASCADE, + ) + error_code = models.CharField(max_length=100) + error_params = models.JSONField(default=dict) + + class Meta: + app_label = CORE_APP_LABEL diff --git a/libs/shared/shared/django_apps/core/tests/__init__.py b/libs/shared/shared/django_apps/core/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/core/tests/factories.py b/libs/shared/shared/django_apps/core/tests/factories.py new file mode 100644 index 0000000000..b2a475c8fa --- /dev/null +++ b/libs/shared/shared/django_apps/core/tests/factories.py @@ -0,0 +1,262 @@ +import random +from hashlib import sha1 + +import factory +from django.utils import timezone +from factory.django import DjangoModelFactory + +from shared.django_apps.codecov_auth.models import RepositoryToken +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory +from shared.django_apps.core import models + + +class RepositoryFactory(DjangoModelFactory): + class Meta: + model = models.Repository + + private = True + name = factory.Faker("slug") + service_id = factory.Sequence(lambda n: f"{n}") + author = factory.SubFactory(OwnerFactory) + language = factory.Iterator( + [language.value for language in models.Repository.Languages] + ) + languages = [] + fork = None + branch = "main" + upload_token = factory.Faker("uuid4") + image_token = factory.Faker("pystr", min_chars=10, max_chars=10) + using_integration = False + + +class CommitFactory(DjangoModelFactory): + class Meta: + model = models.Commit + + commitid = factory.LazyAttribute( + lambda o: sha1(o.message.encode("utf-8")).hexdigest() + ) + message = factory.Faker("sentence", nb_words=7) + ci_passed = True + pullid = 1 + author = factory.SubFactory(OwnerFactory) + repository = factory.SubFactory(RepositoryFactory) + branch = "main" + totals = { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "85.00000", + "d": 0, + "diff": [1, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + "f": 3, + "h": 17, + "m": 3, + "n": 20, + "p": 0, + "s": 1, + } + parent_commit_id = factory.LazyAttribute( + lambda o: sha1((o.message + "parent").encode("utf-8")).hexdigest() + ) + state = "complete" + + +class CommitWithReportFactory(CommitFactory): + @classmethod + def _create(cls, model_class, *args, **kwargs): + commit = super()._create( + model_class, + _report={ + "files": { + "awesome/__init__.py": [ + 2, + [0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "tests/__init__.py": [ + 0, + [0, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "tests/test_sample.py": [ + 1, + [0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + }, + "sessions": { + "0": { + "f": ["unittests"], + "st": "uploaded", + "se": None, + }, + "1": { + "f": ["integrations"], + "st": "uploaded", + "se": None, + }, + }, + }, + *args, + **kwargs, + ) + + # The following replaces the old `commits.report` JSON column + # TODO: we may want to find another way to create this since the imports below + # create a cyclic dependency + from shared.django_apps.reports.tests.factories import ( + CommitReportFactory, + ReportLevelTotalsFactory, + UploadFactory, + UploadFlagMembershipFactory, + UploadLevelTotalsFactory, + ) + + commit_report = CommitReportFactory(commit=commit) + ReportLevelTotalsFactory( + report=commit_report, + files=3, + lines=20, + hits=17, + misses=3, + partials=0, + coverage=85.0, + branches=0, + methods=0, + ) + + flag_unittests, created = commit.repository.flags.get_or_create( + flag_name="unittests" + ) + flag_integrations, created = commit.repository.flags.get_or_create( + flag_name="integrations" + ) + + upload1 = UploadFactory( + report=commit_report, + order_number=0, + storage_path="v4/raw/2019-01-10/4434BC2A2EC4FCA57F77B473D83F928C/abf6d4df662c47e32460020ab14abf9303581429/9ccc55a1-8b41-4bb1-a946-ee7a33a7fb56.txt", + ) + UploadLevelTotalsFactory( + report_session=upload1, + files=3, + lines=20, + hits=17, + misses=3, + partials=0, + coverage=85.0, + branches=0, + methods=0, + ) + UploadFlagMembershipFactory( + report_session=upload1, + flag=flag_unittests, + ) + + upload2 = UploadFactory( + report=commit_report, + order_number=1, + storage_path="v4/raw/2019-01-10/4434BC2A2EC4FCA57F77B473D83F928C/abf6d4df662c47e32460020ab14abf9303581429/9ccc55a1-8b41-4bb1-a946-ee7a33a7fb56.txt", + ) + UploadLevelTotalsFactory( + report_session=upload2, + files=3, + lines=20, + hits=17, + misses=3, + partials=0, + coverage=85.0, + branches=0, + methods=0, + ) + UploadFlagMembershipFactory( + report_session=upload2, + flag=flag_integrations, + ) + + return commit + + +class PullFactory(DjangoModelFactory): + class Meta: + model = models.Pull + + pullid = factory.Sequence(lambda n: n) + issueid = random.randint(1, 1000) + commentid = factory.LazyAttribute( + lambda o: sha1(o.title.encode("utf-8")).hexdigest() + ) + _flare = { + "name": "", + "color": "#e05d44", + "lines": 14, + "_class": None, + "children": [ + { + "name": "tests.py", + "color": "#baaf1b", + "lines": 7, + "_class": None, + "coverage": "85.71429", + } + ], + } + diff = [2, 3, 0, 3, 0, "0", 0, 0, 0, 0, 0, 0, 0] + title = factory.Faker("sentence", nb_words=7) + head = factory.LazyAttribute(lambda o: sha1(o.title.encode("utf-8")).hexdigest()) + base = factory.LazyAttribute(lambda o: sha1(o.title.encode("utf-8")).hexdigest()) + compared_to = factory.LazyAttribute( + lambda o: sha1(o.title.encode("utf-8")).hexdigest() + ) + updatestamp = factory.LazyFunction(timezone.now) + + +class BranchFactory(DjangoModelFactory): + class Meta: + model = models.Branch + + repository = factory.SubFactory(RepositoryFactory) + name = factory.Faker("slug") + head = factory.LazyAttribute(lambda o: sha1(o.name.encode("utf-8")).hexdigest()) + + +class ConstantsFactory(DjangoModelFactory): + class Meta: + model = models.Constants + + +class VersionFactory(DjangoModelFactory): + class Meta: + model = models.Version + + +class RepositoryTokenFactory(DjangoModelFactory): + repository = factory.SubFactory(RepositoryFactory) + key = factory.LazyFunction(RepositoryToken.generate_key) + token_type = "profiling" + + class Meta: + model = RepositoryToken + + +class CommitErrorFactory(DjangoModelFactory): + class Meta: + model = models.CommitError + + commit = factory.SubFactory(CommitFactory) + error_code = factory.Faker("") + + +class CommitNotificationFactory(DjangoModelFactory): + commit = factory.SubFactory(CommitFactory) + notification_type = models.CommitNotification.NotificationTypes.COMMENT + decoration_type = models.CommitNotification.DecorationTypes.STANDARD + state = models.CommitNotification.States.SUCCESS + + class Meta: + model = models.CommitNotification diff --git a/libs/shared/shared/django_apps/db_routers/__init__.py b/libs/shared/shared/django_apps/db_routers/__init__.py new file mode 100644 index 0000000000..751bce30ae --- /dev/null +++ b/libs/shared/shared/django_apps/db_routers/__init__.py @@ -0,0 +1,63 @@ +import logging + +from django.conf import settings + +log = logging.getLogger(__name__) + + +class MultiDatabaseRouter: + """ + A router to control all database operations on models across multiple databases. + https://docs.djangoproject.com/en/4.0/topics/db/multi-db/#automatic-database-routing + """ + + def db_for_read(self, model, **hints): + match model._meta.app_label: + case "timeseries": + if settings.TIMESERIES_DATABASE_READ_REPLICA_ENABLED: + return "timeseries_read" + else: + return "timeseries" + case "ta_timeseries": + return "ta_timeseries" + case _: + if settings.DATABASE_READ_REPLICA_ENABLED: + return "default_read" + else: + return "default" + + def db_for_write(self, model, **hints): + match model._meta.app_label: + case "timeseries": + return "timeseries" + case "ta_timeseries": + return "ta_timeseries" + case _: + return "default" + + def allow_migrate(self, db, app_label, model_name=None, **hints): + match db: + case "timeseries_read" | "default_read": + return False + case "timeseries": + if not settings.TIMESERIES_ENABLED: + return False + return app_label == "timeseries" + case "ta_timeseries": + if not settings.TA_TIMESERIES_ENABLED: + return False + return app_label == "ta_timeseries" + case _: + return app_label not in {"timeseries", "ta_timeseries"} + + def allow_relation(self, obj1, obj2, **hints): + obj1_app = obj1._meta.app_label + obj2_app = obj2._meta.app_label + + if obj1_app in {"timeseries", "ta_timeseries"} or obj2_app in { + "timeseries", + "ta_timeseries", + }: + return obj1_app == obj2_app + else: + return True diff --git a/libs/shared/shared/django_apps/db_settings.py b/libs/shared/shared/django_apps/db_settings.py new file mode 100644 index 0000000000..cf9163c7a6 --- /dev/null +++ b/libs/shared/shared/django_apps/db_settings.py @@ -0,0 +1,213 @@ +from urllib.parse import urlparse + +import django_prometheus + +from shared.config import get_config +from shared.timeseries.helpers import is_timeseries_enabled + +db_url = get_config("services", "database_url") +if db_url: + db_conf = urlparse(db_url) + DATABASE_USER = db_conf.username + DATABASE_NAME = db_conf.path.replace("/", "") + DATABASE_PASSWORD = db_conf.password + DATABASE_HOST = db_conf.hostname + DATABASE_PORT = db_conf.port +else: + DATABASE_USER = get_config("services", "database", "username", default="postgres") + DATABASE_NAME = get_config("services", "database", "name", default="postgres") + DATABASE_PASSWORD = get_config( + "services", "database", "password", default="postgres" + ) + DATABASE_HOST = get_config("services", "database", "host", default="postgres") + DATABASE_PORT = get_config("services", "database", "port", default=5432) + +DATABASE_READ_REPLICA_ENABLED = get_config( + "setup", "database", "read_replica_enabled", default=False +) + +db_read_url = get_config("services", "database_read_url") +if db_read_url: + db_conf = urlparse(db_read_url) + DATABASE_READ_USER = db_conf.username + DATABASE_READ_NAME = db_conf.path.replace("/", "") + DATABASE_READ_PASSWORD = db_conf.password + DATABASE_READ_HOST = db_conf.hostname + DATABASE_READ_PORT = db_conf.port +else: + DATABASE_READ_USER = get_config( + "services", "database_read", "username", default="postgres" + ) + DATABASE_READ_NAME = get_config( + "services", "database_read", "name", default="postgres" + ) + DATABASE_READ_PASSWORD = get_config( + "services", "database_read", "password", default="postgres" + ) + DATABASE_READ_HOST = get_config( + "services", "database_read", "host", default="postgres" + ) + DATABASE_READ_PORT = get_config("services", "database_read", "port", default=5432) + +TIMESERIES_ENABLED = is_timeseries_enabled() +TIMESERIES_REAL_TIME_AGGREGATES = get_config( + "setup", "timeseries", "real_time_aggregates", default=False +) + + +timeseries_database_url = get_config("services", "timeseries_database_url") +if timeseries_database_url: + timeseries_database_conf = urlparse(timeseries_database_url) + TIMESERIES_DATABASE_USER = timeseries_database_conf.username + TIMESERIES_DATABASE_NAME = timeseries_database_conf.path.replace("/", "") + TIMESERIES_DATABASE_PASSWORD = timeseries_database_conf.password + TIMESERIES_DATABASE_HOST = timeseries_database_conf.hostname + TIMESERIES_DATABASE_PORT = timeseries_database_conf.port +else: + TIMESERIES_DATABASE_USER = get_config( + "services", "timeseries_database", "username", default="postgres" + ) + TIMESERIES_DATABASE_NAME = get_config( + "services", "timeseries_database", "name", default="postgres" + ) + TIMESERIES_DATABASE_PASSWORD = get_config( + "services", "timeseries_database", "password", default="postgres" + ) + TIMESERIES_DATABASE_HOST = get_config( + "services", "timeseries_database", "host", default="timescale" + ) + TIMESERIES_DATABASE_PORT = get_config( + "services", "timeseries_database", "port", default=5432 + ) + +TIMESERIES_DATABASE_READ_REPLICA_ENABLED = get_config( + "setup", "timeseries", "read_replica_enabled", default=False +) + +timeseries_database_read_url = get_config("services", "timeseries_database_read_url") +if timeseries_database_read_url: + timeseries_database_conf = urlparse(timeseries_database_read_url) + TIMESERIES_DATABASE_READ_USER = timeseries_database_conf.username + TIMESERIES_DATABASE_READ_NAME = timeseries_database_conf.path.replace("/", "") + TIMESERIES_DATABASE_READ_PASSWORD = timeseries_database_conf.password + TIMESERIES_DATABASE_READ_HOST = timeseries_database_conf.hostname + TIMESERIES_DATABASE_READ_PORT = timeseries_database_conf.port +else: + TIMESERIES_DATABASE_READ_USER = get_config( + "services", "timeseries_database_read", "username", default="postgres" + ) + TIMESERIES_DATABASE_READ_NAME = get_config( + "services", "timeseries_database_read", "name", default="postgres" + ) + TIMESERIES_DATABASE_READ_PASSWORD = get_config( + "services", "timeseries_database_read", "password", default="postgres" + ) + TIMESERIES_DATABASE_READ_HOST = get_config( + "services", "timeseries_database_read", "host", default="timescale" + ) + TIMESERIES_DATABASE_READ_PORT = get_config( + "services", "timeseries_database_read", "port", default=5432 + ) + + +TA_TIMESERIES_ENABLED = get_config("setup", "ta_timeseries", "enabled", default=False) +ta_timeseries_database_url = get_config("services", "ta_timeseries_database_url") + +if ta_timeseries_database_url: + ta_timeseries_database_conf = urlparse(ta_timeseries_database_url) + TA_TIMESERIES_DATABASE_USER = ta_timeseries_database_conf.username + TA_TIMESERIES_DATABASE_NAME = ta_timeseries_database_conf.path.replace("/", "") + TA_TIMESERIES_DATABASE_PASSWORD = ta_timeseries_database_conf.password + TA_TIMESERIES_DATABASE_HOST = ta_timeseries_database_conf.hostname + TA_TIMESERIES_DATABASE_PORT = ta_timeseries_database_conf.port +else: + TA_TIMESERIES_DATABASE_USER = get_config( + "services", "ta_timeseries_database", "username", default="postgres" + ) + TA_TIMESERIES_DATABASE_NAME = get_config( + "services", "ta_timeseries_database", "name", default="test_analytics" + ) + TA_TIMESERIES_DATABASE_PASSWORD = get_config( + "services", "ta_timeseries_database", "password", default="postgres" + ) + TA_TIMESERIES_DATABASE_HOST = get_config( + "services", "ta_timeseries_database", "host", default="timescale" + ) + TA_TIMESERIES_DATABASE_PORT = get_config( + "services", "ta_timeseries_database", "port", default=5432 + ) + +# this is the time in seconds django decides to keep the connection open after the request +# the default is 0 seconds, meaning django closes the connection after every request +# https://docs.djangoproject.com/en/3.1/ref/settings/#conn-max-age +CONN_MAX_AGE = int(get_config("services", "database", "conn_max_age", default=0)) + +DATABASES = { + "default": { + "ENGINE": "psqlextra.backend", + "NAME": DATABASE_NAME, + "USER": DATABASE_USER, + "PASSWORD": DATABASE_PASSWORD, + "HOST": DATABASE_HOST, + "PORT": DATABASE_PORT, + "CONN_MAX_AGE": CONN_MAX_AGE, + } +} + +if DATABASE_READ_REPLICA_ENABLED: + DATABASES["default_read"] = { + "ENGINE": "psqlextra.backend", + "NAME": DATABASE_READ_NAME, + "USER": DATABASE_READ_USER, + "PASSWORD": DATABASE_READ_PASSWORD, + "HOST": DATABASE_READ_HOST, + "PORT": DATABASE_READ_PORT, + "CONN_MAX_AGE": CONN_MAX_AGE, + } + +if TIMESERIES_ENABLED: + DATABASES["timeseries"] = { + "ENGINE": "django_prometheus.db.backends.postgresql", + "NAME": TIMESERIES_DATABASE_NAME, + "USER": TIMESERIES_DATABASE_USER, + "PASSWORD": TIMESERIES_DATABASE_PASSWORD, + "HOST": TIMESERIES_DATABASE_HOST, + "PORT": TIMESERIES_DATABASE_PORT, + "CONN_MAX_AGE": CONN_MAX_AGE, + } + + if TIMESERIES_DATABASE_READ_REPLICA_ENABLED: + DATABASES["timeseries_read"] = { + "ENGINE": "django_prometheus.db.backends.postgresql", + "NAME": TIMESERIES_DATABASE_READ_NAME, + "USER": TIMESERIES_DATABASE_READ_USER, + "PASSWORD": TIMESERIES_DATABASE_READ_PASSWORD, + "HOST": TIMESERIES_DATABASE_READ_HOST, + "PORT": TIMESERIES_DATABASE_READ_PORT, + "CONN_MAX_AGE": CONN_MAX_AGE, + } + +if TA_TIMESERIES_ENABLED: + DATABASES["ta_timeseries"] = { + "ENGINE": "django_prometheus.db.backends.postgresql", + "NAME": TA_TIMESERIES_DATABASE_NAME, + "USER": TA_TIMESERIES_DATABASE_USER, + "PASSWORD": TA_TIMESERIES_DATABASE_PASSWORD, + "HOST": TA_TIMESERIES_DATABASE_HOST, + "PORT": TA_TIMESERIES_DATABASE_PORT, + "CONN_MAX_AGE": CONN_MAX_AGE, + } + +# See https://django-postgres-extra.readthedocs.io/en/main/settings.html +POSTGRES_EXTRA_DB_BACKEND_BASE: "django_prometheus.db.backends.postgresql" # type: ignore + +# Allows to use the pgpartition command +PSQLEXTRA_PARTITIONING_MANAGER = ( + "shared.django_apps.user_measurements.partitioning.manager" +) + +DATABASE_ROUTERS = [ + "shared.django_apps.db_routers.MultiDatabaseRouter", +] + +AUTH_USER_MODEL = "codecov_auth.User" diff --git a/libs/shared/shared/django_apps/dummy_settings.py b/libs/shared/shared/django_apps/dummy_settings.py new file mode 100644 index 0000000000..e60293e854 --- /dev/null +++ b/libs/shared/shared/django_apps/dummy_settings.py @@ -0,0 +1,121 @@ +from pathlib import Path + +from shared.django_apps.db_settings import * # noqa: F403 + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent + +ALLOWED_HOSTS = [] + +# Install apps so that you can make migrations for them +INSTALLED_APPS = [ + "shared.django_apps.legacy_migrations", + "shared.django_apps.pg_telemetry", + "shared.django_apps.rollouts", + "shared.django_apps.user_measurements", + "shared.django_apps.codecov_metrics", + # Needed for makemigrations to work + "django.contrib.auth", + "django.contrib.messages", + # partitions + "psqlextra", + "django_prometheus", + # API models + "django.contrib.admin", + "django.contrib.contenttypes", + "django.contrib.postgres", + "django.contrib.sessions", + "shared.django_apps.bundle_analysis", + "shared.django_apps.codecov_auth", + "shared.django_apps.compare", + "shared.django_apps.core", + "shared.django_apps.labelanalysis", + "shared.django_apps.profiling", + "shared.django_apps.reports", + "shared.django_apps.staticanalysis", + "shared.django_apps.ta_timeseries", + "shared.django_apps.test_analytics", + "shared.django_apps.timeseries", +] + +# Needed for makemigrations to work +MIDDLEWARE = [ + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", +] + +# Needed for makemigrations to work +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.request", + "django.contrib.messages.context_processors.messages", + ] + }, + } +] + +TELEMETRY_VANILLA_DB = "default" +TELEMETRY_TIMESCALE_DB = "timeseries" + +# Needed for migrations that depend on settings.auth_user_model +AUTH_USER_MODEL = "codecov_auth.User" + +# Needed as certain migrations refer to it +SKIP_RISKY_MIGRATION_STEPS = get_config("migrations", "skip_risky_steps", default=False) # noqa: F405 + + +TEST = True + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases +DATABASES = { + "default": { + "ENGINE": "psqlextra.backend", + "NAME": "postgres", + "USER": "postgres", + "PASSWORD": "password", + "HOST": "postgres", + "PORT": 5432, + }, + "timeseries": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "postgres", + "USER": "postgres", + "PASSWORD": "postgres", + "HOST": "timescale", + "PORT": 5432, + }, + "ta_timeseries": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "test_analytics", + "USER": "postgres", + "PASSWORD": "postgres", + "HOST": "timescale", + "PORT": 5432, + }, +} + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [] + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True diff --git a/libs/shared/shared/django_apps/labelanalysis/__init__.py b/libs/shared/shared/django_apps/labelanalysis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/labelanalysis/migrations/0001_initial.py b/libs/shared/shared/django_apps/labelanalysis/migrations/0001_initial.py new file mode 100644 index 0000000000..1dce4074a8 --- /dev/null +++ b/libs/shared/shared/django_apps/labelanalysis/migrations/0001_initial.py @@ -0,0 +1,59 @@ +# Generated by Django 3.2.12 on 2022-08-06 17:44 + +import uuid + +import django.contrib.postgres.fields +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("core", "0014_pull_pulls_author_updatestamp"), + ] + + operations = [ + migrations.CreateModel( + name="LabelAnalysisRequest", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "requested_labels", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), null=True, size=None + ), + ), + ( + "state_id", + models.IntegerField( + choices=[(1, "created"), (2, "finished"), (3, "error")] + ), + ), + ("result", models.JSONField(null=True)), + ( + "base_commit", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="label_requests_as_base", + to="core.commit", + ), + ), + ( + "head_commit", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="label_requests_as_head", + to="core.commit", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/libs/shared/shared/django_apps/labelanalysis/migrations/0002_auto_20230208_1712.py b/libs/shared/shared/django_apps/labelanalysis/migrations/0002_auto_20230208_1712.py new file mode 100644 index 0000000000..fc3de3606e --- /dev/null +++ b/libs/shared/shared/django_apps/labelanalysis/migrations/0002_auto_20230208_1712.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.12 on 2023-02-08 17:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("labelanalysis", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="labelanalysisrequest", + name="processing_params", + field=models.JSONField(null=True), + ), + migrations.AlterField( + model_name="labelanalysisrequest", + name="state_id", + field=models.IntegerField( + choices=[(1, "CREATED"), (2, "FINISHED"), (3, "ERROR")] + ), + ), + ] diff --git a/libs/shared/shared/django_apps/labelanalysis/migrations/0003_labelanalysisprocessingerror.py b/libs/shared/shared/django_apps/labelanalysis/migrations/0003_labelanalysisprocessingerror.py new file mode 100644 index 0000000000..567470725a --- /dev/null +++ b/libs/shared/shared/django_apps/labelanalysis/migrations/0003_labelanalysisprocessingerror.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.2 on 2023-07-18 09:57 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("labelanalysis", "0002_auto_20230208_1712"), + ] + + # BEGIN; + # -- + # -- Create model LabelAnalysisProcessingError + # -- + # CREATE TABLE "labelanalysis_labelanalysisprocessingerror" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "external_id" uuid NOT NULL, "created_at" timestamp with time zone NOT NULL, "updated_at" timestamp with time zone NOT NULL, "error_code" varchar(100) NOT NULL, "error_params" jsonb NOT NULL, "label_analysis_request_id" bigint NOT NULL); + # ALTER TABLE "labelanalysis_labelanalysisprocessingerror" ADD CONSTRAINT "labelanalysis_labela_label_analysis_reque_894742e5_fk_labelanal" FOREIGN KEY ("label_analysis_request_id") REFERENCES "labelanalysis_labelanalysisrequest" ("id") DEFERRABLE INITIALLY DEFERRED; + # CREATE INDEX "labelanalysis_labelanalysi_label_analysis_request_id_894742e5" ON "labelanalysis_labelanalysisprocessingerror" ("label_analysis_request_id"); + # COMMIT; + + operations = [ + migrations.CreateModel( + name="LabelAnalysisProcessingError", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("error_code", models.CharField(max_length=100)), + ("error_params", models.JSONField(default=dict)), + ( + "label_analysis_request", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="errors", + to="labelanalysis.labelanalysisrequest", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/libs/shared/shared/django_apps/labelanalysis/migrations/__init__.py b/libs/shared/shared/django_apps/labelanalysis/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/labelanalysis/models.py b/libs/shared/shared/django_apps/labelanalysis/models.py new file mode 100644 index 0000000000..9092f669ad --- /dev/null +++ b/libs/shared/shared/django_apps/labelanalysis/models.py @@ -0,0 +1,48 @@ +from django.contrib.postgres.fields import ArrayField +from django.db import models +from django_prometheus.models import ExportModelOperationsMixin + +from shared.django_apps.codecov.models import BaseCodecovModel +from shared.labelanalysis import LabelAnalysisRequestState + +# Added to avoid 'doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS' error\ +# Needs to be called the same as the API app +LABELANALYSIS_APP_LABEL = "labelanalysis" + + +class LabelAnalysisRequest( + ExportModelOperationsMixin("labelanalysis.label_analysis_request"), BaseCodecovModel +): + base_commit = models.ForeignKey( + "core.Commit", on_delete=models.CASCADE, related_name="label_requests_as_base" + ) + head_commit = models.ForeignKey( + "core.Commit", on_delete=models.CASCADE, related_name="label_requests_as_head" + ) + requested_labels = ArrayField(models.TextField(), null=True) + state_id = models.IntegerField( + null=False, choices=LabelAnalysisRequestState.choices() + ) + result = models.JSONField(null=True) + processing_params = models.JSONField(null=True) + + class Meta: + app_label = LABELANALYSIS_APP_LABEL + db_table = "labelanalysis_labelanalysisrequest" + + +class LabelAnalysisProcessingError( + ExportModelOperationsMixin("labelanalysis.label_analysis_processing_error"), + BaseCodecovModel, +): + label_analysis_request = models.ForeignKey( + "LabelAnalysisRequest", + related_name="errors", + on_delete=models.CASCADE, + ) + error_code = models.CharField(max_length=100) + error_params = models.JSONField(default=dict) + + class Meta: + app_label = LABELANALYSIS_APP_LABEL + db_table = "labelanalysis_labelanalysisprocessingerror" diff --git a/libs/shared/shared/django_apps/labelanalysis/tests/__init__.py b/libs/shared/shared/django_apps/labelanalysis/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/labelanalysis/tests/factories.py b/libs/shared/shared/django_apps/labelanalysis/tests/factories.py new file mode 100644 index 0000000000..ac71723704 --- /dev/null +++ b/libs/shared/shared/django_apps/labelanalysis/tests/factories.py @@ -0,0 +1,13 @@ +import factory + +from shared.django_apps.core.tests.factories import CommitFactory +from shared.django_apps.labelanalysis.models import LabelAnalysisRequest + + +class LabelAnalysisRequestFactory(factory.Factory): + class Meta: + model = LabelAnalysisRequest + + base_commit = factory.SubFactory(CommitFactory) + head_commit = factory.SubFactory(CommitFactory) + state_id = 1 diff --git a/libs/shared/shared/django_apps/legacy_migrations/__init__.py b/libs/shared/shared/django_apps/legacy_migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/legacy_migrations/management/__init__.py b/libs/shared/shared/django_apps/legacy_migrations/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/legacy_migrations/management/commands/__init__.py b/libs/shared/shared/django_apps/legacy_migrations/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/legacy_migrations/management/commands/migrate.py b/libs/shared/shared/django_apps/legacy_migrations/management/commands/migrate.py new file mode 100644 index 0000000000..a36f4d1c54 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/management/commands/migrate.py @@ -0,0 +1,150 @@ +import logging + +import redis_lock +from django.core.management.commands.migrate import Command as MigrateCommand +from django.db import connections +from django.db import transaction as django_transaction +from django.db.utils import ProgrammingError + +from shared.django_apps.utils.config import RUN_ENV +from shared.helpers.redis import get_redis_connection +from shared.timeseries.helpers import is_timeseries_enabled + +log = logging.getLogger(__name__) + +MIGRATION_LOCK_NAME = "djang-migrations-lock" + + +class MockLock: + def release(self): + pass + + +""" +We need to override the base Django migrate command to handle the legacy migrations we have in the "legacy_migrations" app. +Those migrations are the source of truth for the initial db state, which is captured in Django migrations 0001 for the +core, codecov_auth and reports apps. Thus we need to fake out the initial migrations for those apps to apply duplicate migration +steps eg. creating the same table twice. The source of truth for all other state is captured in the standard Django migrations +and can be safely applied after runnin the legacy migrations. +""" + + +class Command(MigrateCommand): + def _run_initial_codecov_migrations(self, args, options): + codecov_auth_options = {**options} + codecov_auth_options["fake"] = True + codecov_auth_options["app_label"] = "codecov_auth" + codecov_auth_options["migration_name"] = "0001" + + core_options = {**options} + core_options["fake"] = True + core_options["app_label"] = "core" + core_options["migration_name"] = "0001" + + reports_options = {**options} + reports_options["fake"] = True + reports_options["app_label"] = "reports" + reports_options["migration_name"] = "0001" + + legacy_options = {**options} + legacy_options["app_label"] = "legacy_migrations" + legacy_options["migration_name"] = None + + super().handle(*args, **codecov_auth_options) + super().handle(*args, **core_options) + super().handle(*args, **reports_options) + super().handle(*args, **legacy_options) + + def _run_initial_timeseries_migrations(self, args, options): + django_auth = {**options} + django_auth["fake"] = True + django_auth["app_label"] = "auth" + django_auth["migration_name"] = "0001" + + content_types = {**options} + content_types["fake"] = True + content_types["app_label"] = "contenttypes" + content_types["migration_name"] = "0001" + super().handle(*args, **django_auth) + super().handle(*args, **content_types) + + def _fake_initial_migrations(self, cursor, args, options): + try: + cursor.execute("SELECT * FROM django_migrations;") + except ProgrammingError: + self._run_initial_codecov_migrations(args=args, options=options) + + def _fake_initial_timeseries_migrations(self, cursor, args, options): + try: + # If this query doesn't recognize django_migration, nor has less than 2 entries in auth/contenttypes, + # it definitely doesn't have their initial migrations so we would run the initial timeseries migration + cursor.execute( + "SELECT COUNT(*) FROM django_migrations WHERE app = 'auth' or app = 'contenttypes';" + ) + result = cursor.fetchone() + if result[0] < 2: + self._run_initial_timeseries_migrations(args=args, options=options) + except Exception: + self._run_initial_codecov_migrations(args=args, options=options) + self._run_initial_timeseries_migrations(args=args, options=options) + + def _obtain_lock(self): + """ + In certain environments we might be running mutliple servers that will try and run the migrations at the same time. This is + not safe to do. So we have the command obtain a lock to try and run the migration. If it cannot get a lock, it will wait + until it is able to do so before continuing to run. We need to + wait for the lock instead of hard exiting on seeing another + server running the migrations because we write code in such a way that the server expects for migrations to be applied before + new code is deployed (but the opposite of new db with old code is fine). + """ + # If we're running in a non-server environment, we don't need to worry about acquiring a lock + if RUN_ENV == "DEV": + return MockLock() + + redis_connection = get_redis_connection() + lock = redis_lock.Lock( + redis_connection, MIGRATION_LOCK_NAME, expire=180, auto_renewal=True + ) + log.info("Trying to acquire migrations lock...") + acquired = lock.acquire(timeout=180) + + if not acquired: + return None + + return lock + + def handle(self, *args, **options): + log.info("Codecov is starting migrations...") + database = options["database"] + try: + db_connection = connections[database] + except Exception: + log.info( + f"Failed to establish connection with {database}. Cannot do migrations" + ) + return None + options["run_syncdb"] = False + + lock = self._obtain_lock() + + # Failed to acquire lock due to timeout + if not lock: + log.error("Potential deadlock detected in api migrations.") + raise Exception("Failed to obtain lock for api migration.") + + try: + with db_connection.cursor() as cursor: + self._fake_initial_migrations(cursor, args, options) + + if database == "timeseries" and is_timeseries_enabled(): + self._fake_initial_timeseries_migrations(cursor, args, options) + + super().handle(*args, **options) + django_transaction.commit(database) + except: + log.info("Codecov migrations failed.") + raise + else: + log.info("Codecov migrations succeeded.") + finally: + lock.release() diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/0001_initial.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/0001_initial.py new file mode 100644 index 0000000000..f6cabbed78 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# Generated by Django 3.1.6 on 2021-03-15 20:15 + +from django.db import migrations + +from shared.django_apps.legacy_migrations.migrations.legacy_sql.main.main import ( + run_sql as main_run_sql, +) +from shared.django_apps.legacy_migrations.migrations.legacy_sql.upgrades.main import ( + run_sql as upgrade_run_sql, +) + +BASE_VERSION = "base" + + +def forwards_func(apps, schema_editor): + Version = apps.get_model("core", "Version") + + schema_editor.execute("create table if not exists version (version text);") + + db_version = Version.objects.first() + current_version = db_version.version if db_version else BASE_VERSION + + if current_version == BASE_VERSION: + main_run_sql(schema_editor) + return + + upgrade_run_sql(schema_editor, current_version) + + +class Migration(migrations.Migration): + dependencies = [("core", "0001_initial")] + + operations = [migrations.RunPython(forwards_func)] diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/0002_yaml_history_table.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/0002_yaml_history_table.py new file mode 100644 index 0000000000..2157b57cfd --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/0002_yaml_history_table.py @@ -0,0 +1,95 @@ +# Generated by Django 3.2.12 on 2022-04-19 20:22 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("legacy_migrations", "0001_initial"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + database_operations=[ + migrations.RunSQL( + sql=""" + create table if not exists yaml_history + ( + id serial primary key, + ownerid integer not null + references owners + on delete cascade, + timestamp timestamp with time zone not null, + author integer + references owners + on delete cascade, + message text, + source text not null, + diff text + ); + """, + reverse_sql="drop table yaml_history;", + ), + migrations.RunSQL( + sql=""" + create index if not exists yaml_history_ownerid_timestamp + on yaml_history (ownerid, timestamp); + """, + reverse_sql="drop index if exists yaml_history_ownerid_timestamp;", + ), + ], + state_operations=[ + migrations.CreateModel( + name="YamlHistory", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("timestamp", models.DateTimeField()), + ("message", models.TextField(blank=True, null=True)), + ("source", models.TextField()), + ("diff", models.TextField(null=True)), + ( + "author", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="authors", + db_column="author", + to="codecov_auth.owner", + ), + ), + ( + "ownerid", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="ownerids", + db_column="ownerid", + to="codecov_auth.owner", + ), + ), + ], + options={ + "db_table": "yaml_history", + }, + ), + migrations.AddIndex( + model_name="yamlhistory", + index=models.Index( + fields=["ownerid", "timestamp"], + name="yaml_histor_ownerid_74e79b_idx", + ), + ), + ], + ) + ] diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/0003_auto_20230120_1837.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/0003_auto_20230120_1837.py new file mode 100644 index 0000000000..5e492a2d5d --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/0003_auto_20230120_1837.py @@ -0,0 +1,37 @@ +# Generated by Django 3.2.12 on 2023-01-20 18:37 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("legacy_migrations", "0002_yaml_history_table"), + ] + + # These 2 triggers were wrong in the database and did not match what is found + # in the codebase. They were casting the values to ::text types which was breaking + # the case-insensitive comparisons of ::citext. These migrations just drop and + # recreate the triggers exactly as they appear in the `legacy_sql` files. + + operations = [ + migrations.RunSQL( + """ + drop trigger owners_before_update on owners; + + create trigger owners_before_update before update on owners + for each row + when (new.username is not null and new.username is distinct from old.username) + execute procedure owners_before_insert_or_update(); + """ + ), + migrations.RunSQL( + """ + drop trigger repos_before_update on repos; + + create trigger repos_before_update before update on repos + for each row + when (new.name is not null and new.name is distinct from old.name) + execute procedure repos_before_insert_or_update(); + """ + ), + ] diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/0004_auto_20231024_1937.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/0004_auto_20231024_1937.py new file mode 100644 index 0000000000..b96bf31855 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/0004_auto_20231024_1937.py @@ -0,0 +1,78 @@ +# Generated by Django 4.2.3 on 2023-10-24 19:37 + +from django.db import migrations + +from shared.django_apps.migration_utils import RiskyRunSQL + +# from `legacy_migrations/migrations/legacy_sql/main/triggers/commits.py` +original_function = """ +create or replace function commits_insert_pr_branch() returns trigger as $$ +begin + if new.pullid is not null and new.merged is not true then + begin + insert into pulls (repoid, pullid, author, head) + values (new.repoid, new.pullid, new.author, new.commitid); + exception when unique_violation then + end; + end if; + + if new.branch is not null then + begin + insert into branches (repoid, updatestamp, branch, authors, head) + values (new.repoid, new.timestamp, + new.branch, + case when new.author is not null then array[new.author] else null end, + new.commitid); + exception when unique_violation then + end; + end if; + + update repos + set updatestamp=now() + where repoid=new.repoid; + + return null; +end; +$$ language plpgsql; +""" + +# we're removing the `update repos` part since it can be very slow +replacement_function = """ +create or replace function commits_insert_pr_branch() returns trigger as $$ +begin + if new.pullid is not null and new.merged is not true then + begin + insert into pulls (repoid, pullid, author, head) + values (new.repoid, new.pullid, new.author, new.commitid); + exception when unique_violation then + end; + end if; + + if new.branch is not null then + begin + insert into branches (repoid, updatestamp, branch, authors, head) + values (new.repoid, new.timestamp, + new.branch, + case when new.author is not null then array[new.author] else null end, + new.commitid); + exception when unique_violation then + end; + end if; + + return null; +end; +$$ language plpgsql; +""" + + +class Migration(migrations.Migration): + dependencies = [ + ("legacy_migrations", "0003_auto_20230120_1837"), + ] + + operations = [ + RiskyRunSQL( + replacement_function, + reverse_sql=original_function, + ), + ] diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/0005_delete_branch_update_db_trigger.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/0005_delete_branch_update_db_trigger.py new file mode 100644 index 0000000000..1c2553f862 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/0005_delete_branch_update_db_trigger.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.16 on 2024-11-12 23:06 + +from django.db import migrations + +from shared.django_apps.migration_utils import RiskyRunSQL + + +class Migration(migrations.Migration): + dependencies = [ + ("legacy_migrations", "0004_auto_20231024_1937"), + ] + + operations = [ + RiskyRunSQL( + """ + DROP TRIGGER IF EXISTS branch_update ON branches; + DROP FUNCTION IF EXISTS branches_update(); + """, + reverse_sql=migrations.RunSQL.noop, + ) + ] diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/0006_delete_many_owner_triggers.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/0006_delete_many_owner_triggers.py new file mode 100644 index 0000000000..0c2c9336e3 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/0006_delete_many_owner_triggers.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.16 on 2024-11-19 21:07 + +from django.db import migrations + +from shared.django_apps.migration_utils import RiskyRunSQL + + +class Migration(migrations.Migration): + dependencies = [ + ("legacy_migrations", "0005_delete_branch_update_db_trigger"), + ] + + operations = [ + RiskyRunSQL( + """ + DROP TRIGGER IF EXISTS owner_yaml_updated ON owners; + DROP TRIGGER IF EXISTS owner_cache_state_update ON owners; + DROP TRIGGER IF EXISTS owner_cache_state_insert ON owners; + DROP TRIGGER IF EXISTS owner_token_clered ON owners; + """, + reverse_sql=migrations.RunSQL.noop, + ) + ] diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/0007_delete_repo_trigger_stats.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/0007_delete_repo_trigger_stats.py new file mode 100644 index 0000000000..8b3e890189 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/0007_delete_repo_trigger_stats.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.16 on 2024-11-18 19:15 + +from django.db import migrations + +from shared.django_apps.migration_utils import RiskyRunSQL + + +class Migration(migrations.Migration): + dependencies = [ + ("legacy_migrations", "0006_delete_many_owner_triggers"), + ] + + operations = [ + RiskyRunSQL( + """ + DROP TRIGGER IF EXISTS repo_cache_state_update ON repos; + DROP FUNCTION IF EXISTS repo_cache_state_update(); + DROP TRIGGER IF EXISTS repo_yaml_update ON repos; + DROP FUNCTION IF EXISTS repo_yaml_update(); + """, + reverse_sql=migrations.RunSQL.noop, + ) + ] diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/0008_delete_pull_update_trigger.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/0008_delete_pull_update_trigger.py new file mode 100644 index 0000000000..a6dcc53d00 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/0008_delete_pull_update_trigger.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.16 on 2024-12-06 00:15 + +from django.db import migrations + +from shared.django_apps.migration_utils import RiskyRunSQL + + +class Migration(migrations.Migration): + dependencies = [ + ("legacy_migrations", "0007_delete_repo_trigger_stats"), + ] + + operations = [ + RiskyRunSQL( + """ + DROP TRIGGER IF EXISTS pulls_before_update_drop_flare ON pulls; + DROP FUNCTION IF EXISTS pulls_drop_flare(); + """, + reverse_sql=migrations.RunSQL.noop, + ) + ] diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/__init__.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/__init__.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/__init__.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/__init__.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/aggregates.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/aggregates.py new file mode 100644 index 0000000000..9422991852 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/aggregates.py @@ -0,0 +1,71 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + drop function if exists _pop_first_as_json(jsonb[]) cascade; + drop function if exists _max_coverage(jsonb[]) cascade; + drop function if exists _min_coverage(jsonb[]) cascade; + + create or replace function _pop_first_as_json(jsonb[]) returns jsonb as $$ + select $1[1]::jsonb; + $$ language sql immutable; + + + create or replace function _max_coverage(jsonb[], jsonb) returns jsonb[] as $$ + select case when $1 is null then array[$2] + when ($1[1]->>'c')::numeric > ($2->>'c')::numeric then $1 + else array[$2] end; + $$ language sql immutable; + + + create aggregate max_coverage(jsonb) ( + SFUNC = _max_coverage, + STYPE = jsonb[], + FINALFUNC = _pop_first_as_json + ); + + + create or replace function _min_coverage(jsonb[], jsonb) returns jsonb[] as $$ + select case when $1 is null then array[$2] + when ($1[1]->>'c')::numeric < ($2->>'c')::numeric then $1 + else array[$2] end; + $$ language sql immutable; + + + create aggregate min_coverage(jsonb) ( + SFUNC = _min_coverage, + STYPE = jsonb[], + FINALFUNC = _pop_first_as_json + ); + + + create or replace function ratio(int, int) returns text as $$ + select case when $2 = 0 then '0' else round(($1::numeric/$2::numeric)*100.0, 5)::text end; + $$ language sql immutable; + + + create or replace function _agg_report_totals(text[], jsonb) returns text[] as $$ + -- fnhmpcbdMs + select case when $1 is null + then array[$2->>0, $2->>1, $2->>2, $2->>3, + $2->>4, $2->>5, $2->>6, $2->>7, + $2->>8, $2->>9] + else array[($1[1]::int + ($2->>0)::int)::text, + ($1[2]::int + ($2->>1)::int)::text, + ($1[3]::int + ($2->>2)::int)::text, + ($1[4]::int + ($2->>3)::int)::text, + ($1[5]::int + ($2->>4)::int)::text, + ratio(($1[3]::int + ($2->>2)::int), ($1[2]::int + ($2->>1)::int)), + ($1[7]::int + ($2->>6)::int)::text, + ($1[8]::int + ($2->>7)::int)::text, + ($1[9]::int + ($2->>8)::int)::text, + ($1[10]::int + ($2->>9)::int)::text] end; + $$ language sql immutable; + + + create aggregate agg_totals(jsonb) ( + SFUNC = _agg_report_totals, + STYPE = text[] + ); + + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/array_append_unique.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/array_append_unique.py new file mode 100644 index 0000000000..85e6b7adca --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/array_append_unique.py @@ -0,0 +1,11 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create or replace function array_append_unique(anyarray, anyelement) returns anyarray as $$ + select case when $2 is null + then $1 + else array_remove($1, $2) || array[$2] + end; + $$ language sql immutable; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/coverage.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/coverage.py new file mode 100644 index 0000000000..4d3b737668 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/coverage.py @@ -0,0 +1,23 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create or replace function get_coverage(service, citext, citext, citext default null) returns jsonb as $$ + -- floor is temporary here + with d as ( + select floor((c.totals->>'c')::numeric) as c, + coalesce((r.yaml->'coverage'->'range')::jsonb, + (o.yaml->'coverage'->'range')::jsonb) as r, + case when r.private then r.image_token else null end as t + from repos r + inner join owners o using (ownerid) + left join branches b using (repoid) + inner join commits c on b.repoid=c.repoid and c.commitid=b.head + where o.service = $1 + and o.username = $2 + and r.name = $3 + and b.branch = coalesce($4, r.branch) + limit 1 + ) select to_jsonb(d) from d; + $$ language sql stable; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_access_token.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_access_token.py new file mode 100644 index 0000000000..9f0cd688cf --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_access_token.py @@ -0,0 +1,14 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create or replace function get_access_token(int) returns jsonb as $$ + with data as ( + select ownerid, oauth_token, username + from owners o + where ownerid = $1 + and oauth_token is not null + limit 1 + ) select to_jsonb(data) from data; + $$ language sql stable strict; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_author.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_author.py new file mode 100644 index 0000000000..1cd0686793 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_author.py @@ -0,0 +1,13 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create or replace function get_author(int) returns jsonb as $$ + with data as ( + select service, service_id, username, email, name + from owners + where ownerid=$1 + limit 1 + ) select to_jsonb(data) from data; + $$ language sql stable strict; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_commit.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_commit.py new file mode 100644 index 0000000000..ffa3633f9f --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_commit.py @@ -0,0 +1,139 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create or replace function get_commitid_from_short(int, text) returns text as $$ + select commitid + from commits + where repoid = $1 + and commitid like $2||'%%'; + $$ language sql immutable; + + + -- pull + create or replace function get_tip_of_pull(int, int) returns text as $$ + select head + from pulls + where repoid = $1 + and pullid = $2 + limit 1; + $$ language sql stable; + + + -- tips + create or replace function get_tip(int, text) returns text as $$ + select case when char_length($2) = 40 then $2 + else coalesce((select head from branches where repoid=$1 and branch=$2 limit 1), + (select commitid from commits where repoid=$1 and commitid like $2||'%%' limit 1)) end + limit 1; + $$ language sql stable; + + + -- branch + create or replace function get_tip_of_branch(int, text) returns text as $$ + select head + from branches + where repoid = $1 + and branch = $2 + limit 1; + $$ language sql stable; + + + create or replace function get_commit_totals(int, text) returns jsonb as $$ + select totals + from commits + where repoid = $1 + and commitid = $2 + limit 1; + $$ language sql stable; + + + create or replace function get_commit_totals(int, text, text) returns jsonb as $$ + select report->'files'->$3->1 + from commits + where repoid = $1 + and commitid = $2 + limit 1; + $$ language sql stable; + + + create or replace function get_commit(repoid integer, _commitid text) returns jsonb as $$ + with d as ( + select timestamp, commitid, branch, pullid::text, parent, + ci_passed, updatestamp, message, deleted, totals, + get_author(author) as author, state, merged, + get_commit_totals($1, c.parent) as parent_totals, notified, + report + from commits c + where c.repoid = $1 + and commitid = (case when char_length(_commitid) < 40 then get_commitid_from_short($1, _commitid) else _commitid end) + limit 1 + ) select to_jsonb(d) from d; + $$ language sql stable; + + + create or replace function get_commit_minimum(int, text) returns jsonb as $$ + with d as ( + select timestamp, commitid, ci_passed, message, + get_author(author) as author, totals + from commits + where repoid = $1 + and commitid = $2 + limit 1 + ) select to_jsonb(d) from d; + $$ language sql stable; + + + create or replace function get_commit_on_branch(int, text) returns jsonb as $$ + select get_commit($1, head) + from branches + where repoid = $1 and branch = $2 + limit 1; + $$ language sql stable; + + + create or replace function find_parent_commit(_repoid int, + _this_commitid text, + _this_timestamp timestamp, + _parent_commitids text[], + _branch text, + _pullid int) returns text as $$ + declare commitid_ text default null; + begin + if array_length(_parent_commitids, 1) > 0 then + -- first: find a direct decendant + select commitid into commitid_ + from commits + where repoid = _repoid + and array[commitid] <@ _parent_commitids + limit 1; + end if; + + if commitid_ is null then + -- second: find latest on branch + select commitid into commitid_ + from commits + where repoid = _repoid + and branch = _branch + and pullid is not distinct from _pullid + and commitid != _this_commitid + and ci_passed + and deleted is not true + and timestamp < _this_timestamp + order by timestamp desc + limit 1; + + if commitid_ is null then + -- third: use pull base + select base into commitid_ + from pulls + where repoid = _repoid + and pullid = _pullid + limit 1; + end if; + end if; + + return commitid_; + end; + $$ language plpgsql stable; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_customer.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_customer.py new file mode 100644 index 0000000000..4eeca2ede2 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_customer.py @@ -0,0 +1,133 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create or replace function get_gitlab_root_group(int) returns jsonb as $$ + /* get root group by following parent_service_id to highest level */ + with recursive tree as ( + select o.service_id, + o.parent_service_id, + o.ownerid, + 1 as depth + from owners o + where o.ownerid = $1 + and o.service = 'gitlab' + and o.parent_service_id is not null + + union all + + select o.service_id, + o.parent_service_id, + o.ownerid, + depth + 1 as depth + from tree t + join owners o + on o.service_id = t.parent_service_id + /* avoid infinite loop in case of cycling (2 > 5 > 3 > 2 > 5...) up to Gitlab max subgroup depth of 20 */ + where depth <= 20 + ), data as ( + select t.ownerid, + t.service_id + from tree t + where t.parent_service_id is null + ) + select to_jsonb(data) from data limit 1; + $$ language sql stable strict; + + create or replace function get_gitlab_repos_activated(int, text) returns int as $$ + declare _repos_activated int; + declare _decendents_owner_ids int[]; + begin + /* get array of owner ids for all subgroups under this group */ + select array( + with recursive tree as ( + /* seed the recursive query */ + select ownerid, + service_id, + array[]::text[] as ancestors_service_id, + 1 as depth + from owners + where parent_service_id is null + and service = 'gitlab' + and ownerid = $1 + + union all + + /* find the descendents */ + select owners.ownerid, + owners.service_id, + tree.ancestors_service_id || owners.parent_service_id, + depth + 1 as depth + from owners, tree + where owners.parent_service_id = tree.service_id + /* avoid infinite loop in case of cycling (2 > 5 > 3 > 2 > 5...) up to Gitlab max subgroup depth of 20 */ + and depth <= 20 + ) + select ownerid + from tree + where $2 = any(tree.ancestors_service_id) + ) into _decendents_owner_ids; + + /* get count of all repos that are active and private owned by this gitlab group and all of its subgroups */ + select count(*) into _repos_activated + from repos + where ownerid in (select unnest(array_append(_decendents_owner_ids, $1))) + and private + and activated; + + return _repos_activated; + end; + $$ language plpgsql stable; + + create or replace function get_repos_activated(int) returns int as $$ + declare _repos_activated int; + declare _service text; + declare _service_id text; + begin + select o.service, o.service_id into _service, _service_id + from owners o where o.ownerid = $1; + + if _service = 'gitlab' then + select get_gitlab_repos_activated($1, _service_id) into _repos_activated; + else + select count(*) into _repos_activated + from repos + where ownerid=$1 + and private + and activated; + end if; + + return _repos_activated; + end; + $$ language plpgsql stable; + + create or replace function get_customer(int) returns jsonb as $$ + with data as ( + select t.stripe_customer_id, + t.stripe_subscription_id, + t.ownerid::text, + t.service, + t.service_id, + t.plan_user_count, + t.plan_provider, + t.plan_auto_activate, + t.plan_activated_users, + t.plan, + t.email, + t.free, + t.did_trial, + t.invoice_details, + t.yaml, + t.student, + t.student_created_at, + t.student_updated_at, + b.username as bot_username, + get_users(t.admins) as admins, + get_repos_activated($1::int) as repos_activated + from owners t + LEFT JOIN owners b ON (b.ownerid = t.bot) + where t.ownerid = $1 + limit 1 + ) select to_jsonb(data) from data limit 1; + $$ language sql stable strict; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_graph_for.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_graph_for.py new file mode 100644 index 0000000000..2cc54f7f10 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_graph_for.py @@ -0,0 +1,240 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create or replace function sum_of_file_totals_filtering_sessionids(jsonb, int[]) returns text[] as $$ + -- sum totals for filtered flags + -- in [, , ], [1, 2] + -- out ( + ) = + with totals as ( + select $1->i as t from unnest($2) as i + ) select agg_totals(totals.t) from totals; + $$ language sql immutable; + + + create or replace function extract_totals(files jsonb, sessionids int[]) returns jsonb as $$ + -- return {"filename": , ...} + with files as ( + select case + when sessionids is not null then (select jsonb_agg(row(key, sum_of_file_totals_filtering_sessionids(value->2, sessionids))) from jsonb_each(files)) + else (select jsonb_agg(row(key, value->1)) from jsonb_each(files)) + end as data + ) select to_jsonb(data) from files; + $$ language sql immutable; + + + create or replace function list_sessionid_by_filtering_flags(sessions jsonb, flags text[]) returns int[] as $$ + -- return session index where flags overlap $1 + with indexes as ( + select (session.key)::int as key + from jsonb_each(sessions) as session + where (session.value->>'f')::text is not null + and flags <@ (select array_agg(trim(f::text, '"')) from jsonb_array_elements((session.value->'f')) f)::text[] + ) select array_agg(key) from indexes; + $$ language sql strict immutable; + + + create or replace function total_list_to_json(totals text[]) returns jsonb as $$ + select ('{"f":'||totals[1]||','|| + '"n":'||totals[2]||','|| + '"h":'||totals[3]||','|| + '"m":'||totals[4]||','|| + '"p":'||totals[5]||','|| + '"c":'||totals[6]||','|| + '"b":'||totals[7]||','|| + '"d":'||totals[8]||','|| + '"M":'||totals[9]||','|| + '"s":'||totals[10]||'}')::jsonb; + $$ language sql strict immutable; + + + create or replace function sum_session_totals(sessions jsonb, flags text[]) returns jsonb as $$ + -- sum totals for filtered flags + -- in {"0": {"t": }, "1": {"t": }, "2", {"t": }], [1, 2] + -- out ( + ) = + with totals as ( + select sessions->(i::text)->'t' as t from unnest(list_sessionid_by_filtering_flags(sessions, flags)) as i + ) select total_list_to_json(agg_totals(totals.t)) from totals; + $$ language sql strict immutable; + + + create or replace function get_graph_for_flare_pull(int, text, text, text[]) returns jsonb as $$ + with data as ( + select r.repoid, r.service_id, p.head as commitid, r.branch, + p.flare, + case when p.flare is null + then extract_totals(c.report->'files', list_sessionid_by_filtering_flags(c.report->'sessions', $4)) + else null + end as files_by_total, + coalesce((r.yaml->'coverage'->'range')::jsonb, + (o.yaml->'coverage'->'range')::jsonb) as coverage_range + from repos r + inner join owners o using (ownerid) + inner join pulls p using (repoid) + inner join commits c on c.repoid = r.repoid and c.commitid = p.head + where r.repoid = $1 + and p.pullid = $2::int + and (not r.private or r.image_token = $3) + limit 1 + ) select to_jsonb(data) from data limit 1; + $$ language sql stable; + + + create or replace function get_graph_for_flare_commit(int, text, text, text[]) returns jsonb as $$ + with data as ( + select r.repoid, r.service_id, c.commitid, r.branch, + extract_totals(c.report->'files', list_sessionid_by_filtering_flags(c.report->'sessions', $4)) as files_by_total, + coalesce((r.yaml->'coverage'->'range')::jsonb, + (o.yaml->'coverage'->'range')::jsonb) as coverage_range + from repos r + inner join owners o using (ownerid) + inner join commits c using (repoid) + where r.repoid = $1 + and c.commitid = $2 + and (not r.private or r.image_token = $3) + limit 1 + ) select to_jsonb(data) from data limit 1; + $$ language sql stable; + + + create or replace function get_graph_for_flare_branch(int, text, text, text[]) returns jsonb as $$ + with data as ( + select r.repoid, r.service_id, c.commitid, r.branch, + extract_totals(c.report->'files', list_sessionid_by_filtering_flags(c.report->'sessions', $4)) as files_by_total, + coalesce((r.yaml->'coverage'->'range')::jsonb, + (o.yaml->'coverage'->'range')::jsonb) as coverage_range + from repos r + inner join owners o using (ownerid) + inner join branches b using (repoid) + inner join commits c on c.repoid = r.repoid and c.commitid = b.head + where r.repoid = $1 + and b.branch = case when $2 is null then r.branch else $2 end + and (not r.private or r.image_token = $3) + limit 1 + ) select to_jsonb(data) from data limit 1; + $$ language sql stable; + + + create or replace function get_graph_for_totals_pull(int, text, text, text[]) returns jsonb as $$ + with data as ( + select r.repoid, r.service_id, r.branch, + p.base as base_commitid, + case when $4 is null + then (select totals from commits where repoid=p.repoid and commitid=p.base limit 1) + else (select sum_session_totals(report->'sessions', $4) + from commits + where repoid=$1 + and commitid=p.base + limit 1) + end as base_totals, + p.head as head_commitid, + case when $4 is null + then (select totals from commits where repoid=p.repoid and commitid=p.head limit 1) + else (select sum_session_totals(report->'sessions', $4) + from commits + where repoid=$1 + and commitid=p.head + limit 1) + end as head_totals, + coalesce((r.yaml->'coverage'->'range')::jsonb, + (o.yaml->'coverage'->'range')::jsonb) as coverage_range + from repos r + inner join owners o using (ownerid) + inner join pulls p using (repoid) + where r.repoid = $1 + and p.pullid = $2::int + and (not r.private or r.image_token = $3) + limit 1 + ) select to_jsonb(data) from data limit 1; + $$ language sql stable; + + + create or replace function get_graph_for_totals_commit(int, text, text, text[]) returns jsonb as $$ + with data as ( + select r.repoid, r.service_id, r.branch, + base.commitid as base_commitid, + case when $4 is null + then base.totals + else sum_session_totals(base.report->'sessions', $4) + end as base_totals, + head.commitid as head_commitid, + case when $4 is null + then head.totals + else sum_session_totals(head.report->'sessions', $4) + end as head_totals, + coalesce((r.yaml->'coverage'->'range')::jsonb, + (o.yaml->'coverage'->'range')::jsonb) as coverage_range + from repos r + inner join owners o using (ownerid) + inner join commits head using (repoid) + left join commits base on base.repoid = r.repoid + and base.commitid = head.parent + where r.repoid = $1 + and head.commitid = $2 + and (not r.private or r.image_token = $3) + limit 1 + ) select to_jsonb(data) from data limit 1; + $$ language sql stable; + + + create or replace function get_graph_for_totals_branch(int, text, text, text[]) returns jsonb as $$ + with data as ( + select r.repoid, r.service_id, r.branch, + base.commitid as base_commitid, + case when $4 is null + then base.totals + else sum_session_totals(base.report->'sessions', $4) + end as base_totals, + head.commitid as head_commitid, + case when $4 is null + then head.totals + else sum_session_totals(head.report->'sessions', $4) + end as head_totals, + coalesce((r.yaml->'coverage'->'range')::jsonb, + (o.yaml->'coverage'->'range')::jsonb) as coverage_range + from repos r + inner join owners o using (ownerid) + inner join branches b using (repoid) + left join commits base on base.repoid = r.repoid + and base.commitid = b.base + inner join commits head on head.repoid = r.repoid + and head.commitid = b.head + where r.repoid = $1 + and b.branch = case when $2 is null then r.branch else $2 end + and (not r.private or r.image_token = $3) + limit 1 + ) select to_jsonb(data) from data limit 1; + $$ language sql stable; + + + create or replace function get_graph_for_commits_pull(int, text, text, text[]) returns jsonb as $$ + with data as ( + select r.repoid, r.service_id, r.branch, + coalesce((r.yaml->'coverage'->'range')::jsonb, + (o.yaml->'coverage'->'range')::jsonb) as coverage_range + from repos r + inner join owners o using (ownerid) + inner join pulls p using (repoid) + where r.repoid = $1 + and p.pullid = $2::int + and (not r.private or r.image_token = $3) + limit 1 + ) select to_jsonb(data) from data limit 1; + $$ language sql stable; + + + create or replace function get_graph_for_commits_branch(int, text, text, text[]) returns jsonb as $$ + with data as ( + select r.repoid, r.service_id, r.branch, + coalesce((r.yaml->'coverage'->'range')::jsonb, + (o.yaml->'coverage'->'range')::jsonb) as coverage_range + from repos r + inner join owners o using (ownerid) + inner join branches b using (repoid) + where r.repoid = $1 + and b.branch = case when $2 is null then r.branch else $2 end + and (not r.private or r.image_token = $3) + limit 1 + ) select to_jsonb(data) from data limit 1; + $$ language sql stable; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_ownerid.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_ownerid.py new file mode 100644 index 0000000000..869f59cec0 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_ownerid.py @@ -0,0 +1,93 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create or replace function get_ownerid_if_member(service, citext, int) returns int as $$ + select ownerid + from owners + where service=$1 + and username=$2::citext + and array[$3] <@ organizations + and private_access is true + limit 1; + $$ language sql stable strict; + + + create or replace function get_ownerid(service, text, citext, text, text) returns int as $$ + declare _ownerid int; + begin + + select ownerid into _ownerid + from owners + where service=$1 + and service_id=$2 + limit 1; + + if not found and $2 is not null then + insert into owners (service, service_id, username, name, email) + values ($1, $2, $3::citext, $4, $5) + returning ownerid into _ownerid; + end if; + + return _ownerid; + end; + $$ language plpgsql; + + + create or replace function try_to_auto_activate(int, int) returns boolean as $$ + update owners + set plan_activated_users = ( + case when coalesce(array_length(plan_activated_users, 1), 0) < plan_user_count -- we have credits + then array_append_unique(plan_activated_users, $2) -- add user + else plan_activated_users + end) + where ownerid=$1 + returning (plan_activated_users @> array[$2]); + $$ language sql volatile strict; + + + create or replace function get_owner(service, citext) returns jsonb as $$ + with data as ( + select service_id, service, ownerid::text, username, avatar_url, + updatestamp, plan, name, integration_id, free, + plan_activated_users, plan_auto_activate, plan_user_count + from owners + where service=$1 + and username=$2::citext + limit 1 + ) select to_jsonb(data) + from data + limit 1; + $$ language sql stable strict; + + + create or replace function get_teams(service, integer[]) returns jsonb as $$ + with data as ( + select service_id, service, ownerid::text, username, name + from owners + where service=$1 + and array[ownerid] <@ $2 + ) select jsonb_agg(data) from data; + $$ language sql stable strict; + + + create or replace function get_or_create_owner(service, text, text, text, text) returns int as $$ + declare _ownerid int; + begin + update owners + set username = $3, avatar_url = $4, parent_service_id = $5 + where service = $1 + and service_id = $2 + returning ownerid into _ownerid; + + if not found then + insert into owners (service, service_id, username, avatar_url, parent_service_id) + values ($1, $2, $3, $4, $5) + returning ownerid into _ownerid; + end if; + + return _ownerid; + + end; + $$ language plpgsql volatile; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_repo.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_repo.py new file mode 100644 index 0000000000..2da4965f73 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_repo.py @@ -0,0 +1,79 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + -- used for app/tasks + create or replace function get_repo(int) returns jsonb as $$ + with d as (select o.service, o.username, o.service_id as owner_service_id, r.ownerid::text, + r.name, r.repoid::text, r.service_id, r.updatestamp, + r.branch, r.private, hookid, image_token, b.username as bot_username, + r.yaml, o.yaml as org_yaml, r.using_integration, o.plan, + (r.cache->>'yaml') as _yaml_location, + case when r.using_integration then o.integration_id else null end as integration_id, + get_access_token(coalesce(r.bot, o.bot, o.ownerid)) as token, + case when private and activated is not true and forkid is not null + then (select rr.activated from repos rr where rr.repoid = r.forkid limit 1) + else activated end as activated + from repos r + inner join owners o using (ownerid) + left join owners b ON (r.bot=b.ownerid) + where r.repoid = $1 + limit 1) select to_jsonb(d) from d; + $$ language sql stable strict; + + + -- used for app/handlers + create or replace function get_repo(int, citext) returns jsonb as $$ + with repo as ( + select r.yaml, r.name, "language", repoid::text, r.private, r.deleted, r.active, r.cache, b.username as bot_username, + r.branch, r.service_id, r.updatestamp, upload_token, image_token, hookid, using_integration, + case when private and activated is not true and forkid is not null + then (select rr.activated from repos rr where rr.repoid = r.forkid limit 1) + else activated end as activated + from repos r + left join owners b ON (r.bot=b.ownerid) + where r.ownerid = $1 and r.name = $2::citext + limit 1 + ) select to_jsonb(repo) from repo; + $$ language sql stable; + + + -- used for app/handlers/upload + create or replace function get_repo_by_token(uuid) returns jsonb as $$ + with d as ( + select get_repo(r.repoid) as repo, o.service + from repos r + inner join owners o using (ownerid) + where r.upload_token = $1 + limit 1 + ) select to_jsonb(d) from d limit 1; + $$ language sql stable; + + + -- used for app/handlers/teams + create or replace function get_repos(int, int default 0, int default 5) returns jsonb as $$ + with _repos as ( + select private, cache, name, updatestamp, upload_token, branch, + language, repoid::text, get_repo(forkid) as fork, yaml, + case when private and activated is not true and forkid is not null + then (select rr.activated from repos rr where rr.repoid = r.forkid limit 1) + else activated end as activated + from repos r + where ownerid = $1 + and active + offset $2 + limit $3 + ) select coalesce(jsonb_agg(_repos), '[]'::jsonb) from _repos; + $$ language sql stable; + + + create or replace function get_repoid(service, citext, citext) returns int as $$ + select repoid + from repos r + inner join owners o using (ownerid) + where o.service = $1 + and o.username = $2::citext + and r.name = $3::citext + limit 1 + $$ language sql stable; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_user.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_user.py new file mode 100644 index 0000000000..abdcba6bc1 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/get_user.py @@ -0,0 +1,33 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create or replace function get_user(int) returns jsonb as $$ + with data as ( + select ownerid::text, private_access, staff, service, service_id, + username, organizations, avatar_url, + oauth_token, plan, permission, + free, email, name, createstamp + from owners + where ownerid=$1 + limit 1 + ) select to_jsonb(data) from data; + $$ language sql stable; + + + create or replace function get_username(int) returns citext as $$ + select username from owners where ownerid=$1 limit 1; + $$ language sql stable strict; + + + create or replace function get_users(int[]) returns jsonb as $$ + with data as ( + select service, service_id::text, ownerid::text, username, name, email, avatar_url + from owners + where array[ownerid] <@ $1 + limit array_length($1, 1) + ) select jsonb_agg(data) + from data + limit array_length($1, 1); + $$ language sql stable strict; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/insert_commit.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/insert_commit.py new file mode 100644 index 0000000000..7bafbd6e32 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/insert_commit.py @@ -0,0 +1,29 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create or replace function insert_commit(int, text, text, int) returns void as $$ + begin + + update commits + set state='pending' + where repoid = $1 + and commitid = $2; + + if not found then + insert into commits (repoid, commitid, branch, pullid, merged, timestamp, state) + values ($1, $2, $3, $4, case when $4 is not null then false else null end, now(), 'pending') + on conflict (repoid, commitid) do update + set branch=$3, pullid=$4, + merged=(case when $4 is not null then false else null end), + state='pending'; + end if; + + update repos + set active=true, deleted=false, updatestamp=now() + where repoid = $1 + and (active is not true or deleted is true); + + end; + $$ language plpgsql volatile; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/main.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/main.py new file mode 100644 index 0000000000..f404a7902a --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/main.py @@ -0,0 +1,33 @@ +from .aggregates import run_sql as aggregates_run_sql +from .array_append_unique import run_sql as array_append_unique_run_sql +from .coverage import run_sql as coverage_run_sql +from .get_access_token import run_sql as get_access_token_run_sql +from .get_author import run_sql as get_author_run_sql +from .get_commit import run_sql as get_commit_run_sql +from .get_customer import run_sql as get_customer_run_sql +from .get_graph_for import run_sql as get_graph_for_run_sql +from .get_ownerid import run_sql as get_ownerid_run_sql +from .get_repo import run_sql as get_repo_run_sql +from .get_user import run_sql as get_user_run_sql +from .insert_commit import run_sql as insert_commit_run_sql +from .refresh_repos import run_sql as refresh_repos_run_sql +from .update_json import run_sql as update_json_run_sql +from .verify_session import run_sql as verify_session_run_sql + + +def run_sql(schema_editor): + aggregates_run_sql(schema_editor) + update_json_run_sql(schema_editor) + get_author_run_sql(schema_editor) + array_append_unique_run_sql(schema_editor) + coverage_run_sql(schema_editor) + get_access_token_run_sql(schema_editor) + get_repo_run_sql(schema_editor) + get_user_run_sql(schema_editor) + get_customer_run_sql(schema_editor) + get_commit_run_sql(schema_editor) + get_ownerid_run_sql(schema_editor) + verify_session_run_sql(schema_editor) + refresh_repos_run_sql(schema_editor) + insert_commit_run_sql(schema_editor) + get_graph_for_run_sql(schema_editor) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/refresh_repos.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/refresh_repos.py new file mode 100644 index 0000000000..42105e8d38 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/refresh_repos.py @@ -0,0 +1,160 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create or replace function refresh_teams(service, jsonb) returns int[] as $$ + declare ownerids int[]; + declare _ownerid int; + declare _team record; + begin + for _team in select d from jsonb_array_elements($2) d loop + update owners o + set username = (_team.d->>'username')::citext, + name = (_team.d->>'name')::text, + email = (_team.d->>'email')::text, + avatar_url = (_team.d->>'avatar_url')::text, + parent_service_id = (_team.d->>'parent_id')::text, + updatestamp = now() + where service = $1 + and service_id = (_team.d->>'id')::text + returning ownerid into _ownerid; + + if not found then + insert into owners (service, service_id, username, name, email, avatar_url, parent_service_id) + values ($1, + (_team.d->>'id')::text, + (_team.d->>'username')::citext, + (_team.d->>'name')::text, + (_team.d->>'email')::text, + (_team.d->>'avatar_url')::text, + (_team.d->>'parent_id')::text + ) + returning ownerid into _ownerid; + end if; + + select array_append(ownerids, _ownerid) into ownerids; + + end loop; + + return ownerids; + + end; + $$ language plpgsql volatile strict; + + + create or replace function refresh_repos(service, jsonb, int, boolean) returns text[] as $$ + declare _ text; + declare _branch text; + declare _forkid int; + declare _previous_ownerid int; + declare _ownerid int; + declare _repo record; + declare _repoid int; + declare _bot int; + declare repos text[]; + begin + + for _repo in select d from jsonb_array_elements($2) d loop + + select r.ownerid into _previous_ownerid + from repos r + inner join owners o using (ownerid) + where o.service = $1 + and r.service_id = (_repo.d->'repo'->>'service_id')::text + limit 1; + + -- owner + -- ===== + -- its import to check all three below. otherwise update the record. + select ownerid, bot, (yaml->'codecov'->>'branch')::text + into _ownerid, _bot, _branch + from owners + where service = $1 + and service_id = (_repo.d->'owner'->>'service_id')::text + and username = (_repo.d->'owner'->>'username')::citext + limit 1; + + if not found then + update owners + set username = (_repo.d->'owner'->>'username')::citext, + updatestamp = now() + where service = $1 + and service_id = (_repo.d->'owner'->>'service_id')::text + returning ownerid, bot, (yaml->'codecov'->>'branch')::text + into _ownerid, _bot, _branch; + + if not found then + insert into owners (service, service_id, username, bot) + values ($1, (_repo.d->'owner'->>'service_id')::text, (_repo.d->'owner'->>'username')::citext, $3) + returning ownerid, bot into _ownerid, _bot; + end if; + + end if; + + -- fork + -- ==== + if (_repo.d->'repo'->>'fork') is not null then + -- converts fork into array + select refresh_repos($1, (select jsonb_agg(d.d::jsonb)::jsonb + from (select (_repo.d->'repo'->>'fork')::jsonb d limit 1) d + limit 1), null, null) + into _ + limit 1; + + -- get owner + select r.repoid into _forkid + from repos r + inner join owners o using (ownerid) + where o.service = $1 + and o.username = (_repo.d->'repo'->'fork'->'owner'->>'username')::citext + and r.name = (_repo.d->'repo'->'fork'->'repo'->>'name')::citext + limit 1; + else + _forkid := null; + end if; + + -- update repo + -- =========== + if _previous_ownerid is not null then + -- repo already existed with this service_id, update it + update repos set + private = ((_repo.d)->'repo'->>'private')::boolean, + forkid = _forkid, + language = ((_repo.d)->'repo'->>'language')::languages, + ownerid = _ownerid, + using_integration=(using_integration or $4), + name = (_repo.d->'repo'->>'name')::citext, + deleted = false, + updatestamp=now() + where ownerid = _previous_ownerid + and service_id = (_repo.d->'repo'->>'service_id')::text + returning repoid + into _repoid; + + -- new repo + -- ======== + else + insert into repos (service_id, ownerid, private, forkid, name, branch, language, using_integration) + values ((_repo.d->'repo'->>'service_id')::text, + _ownerid, + (_repo.d->'repo'->>'private')::boolean, + _forkid, + (_repo.d->'repo'->>'name')::citext, + coalesce(_branch, (_repo.d->'repo'->>'branch')), + (_repo.d->'repo'->>'language')::languages, + $4) + returning repoid into _repoid; + + end if; + + -- return private repoids + if (_repo.d->'repo'->>'private')::boolean then + repos = array_append(repos, _repoid::text); + end if; + + end loop; + + return repos; + end; + $$ language plpgsql volatile; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/update_json.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/update_json.py new file mode 100644 index 0000000000..2e9dfe6fd5 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/update_json.py @@ -0,0 +1,57 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create or replace function add_key_to_json(jsonb, text, jsonb) returns jsonb as $$ + select case when $1 is null and $3 is null then ('{"'||$2||'":null}')::jsonb + when $1 is null or $1::text = '{}' then ('{"'||$2||'":'||$3||'}')::jsonb + when $3 is null then (left($1::text, -1)||',"'||$2||'":null}')::jsonb + else (left($1::text, -1)||',"'||$2||'":'||$3::text||'}')::jsonb end; + $$ language sql stable; + + + create or replace function add_key_to_json(jsonb, text, integer) returns jsonb as $$ + select case when $1 is null and $3 is null then ('{"'||$2||'":null}')::jsonb + when $1 is null or $1::text = '{}' then ('{"'||$2||'":'||$3||'}')::jsonb + when $3 is null then (left($1::text, -1)||',"'||$2||'":null}')::jsonb + else (left($1::text, -1)||',"'||$2||'":'||$3::text||'}')::jsonb end; + $$ language sql stable; + + + create or replace function add_key_to_json(jsonb, text, text) returns jsonb as $$ + select case when $1 is null and $3 is null then ('{"'||$2||'":null}')::jsonb + when $1 is null or $1::text = '{}' then ('{"'||$2||'":"'||$3||'"}')::jsonb + when $3 is null then (left($1::text, -1)||',"'||$2||'":null}')::jsonb + else (left($1::text, -1)||',"'||$2||'":"'||$3::text||'"}')::jsonb end; + $$ language sql stable; + + + create or replace function remove_key_from_json(jsonb, text) returns jsonb as $$ + with drop_key as ( + select key, value::text + from jsonb_each($1::jsonb) + where key != $2::text and value is not null + ) select ('{'||array_to_string((select array_agg('"'||key||'":'||value) from drop_key), ',')||'}')::jsonb; + $$ language sql stable; + + + create or replace function update_json(jsonb, text, jsonb) returns jsonb as $$ + select case when $1 is not null then add_key_to_json(coalesce(remove_key_from_json($1, $2), '{}'::jsonb), $2, $3) + when $3 is null then ('{"'||$2||'":null}')::jsonb + else ('{"'||$2||'":'||coalesce($3::text, 'null')::text||'}')::jsonb end; + $$ language sql stable; + + + create or replace function update_json(jsonb, text, integer) returns jsonb as $$ + select case when $1 is not null then add_key_to_json(coalesce(remove_key_from_json($1, $2), '{}'::jsonb), $2, $3) + when $3 is null then ('{"'||$2||'":null}')::jsonb + else ('{"'||$2||'":'||$3::text||'}')::jsonb end; + $$ language sql stable; + + + create or replace function update_json(jsonb, text, text) returns jsonb as $$ + select case when $1 is not null then add_key_to_json(coalesce(remove_key_from_json($1, $2), '{}'::jsonb), $2, $3) + when $3 is null then ('{"'||$2||'":null}')::jsonb + else ('{"'||$2||'":"'||$3||'"}')::jsonb end; + $$ language sql stable; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/verify_session.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/verify_session.py new file mode 100644 index 0000000000..d743f1e27a --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/functions/verify_session.py @@ -0,0 +1,15 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create or replace function verify_session(text, text, uuid, sessiontype) returns jsonb as $$ + -- try any members + update sessions + set lastseen = now(), + ip = $1, + useragent = $2 + where token = $3 + and type = $4 + returning get_user(ownerid); + $$ language sql volatile; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/main.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/main.py new file mode 100644 index 0000000000..2164ad1a8a --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/main.py @@ -0,0 +1,24 @@ +from .functions.main import run_sql as functions_run_sql +from .tables.main import run_sql as tables_run_sql +from .triggers.main import run_sql as triggers_run_sql +from .types import run_sql as types_run_sql + + +def run_sql(schema_editor): + schema_editor.execute( + """ + create extension if not exists "uuid-ossp"; + create extension if not exists "citext"; + + create table if not exists version (version text); + + create or replace function random_string(int) returns char as $$ + select string_agg(((string_to_array('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', null))[floor(random()*62)+1])::text, '') + from generate_series(1, $1); + $$ language sql; + """ + ) + types_run_sql(schema_editor) + tables_run_sql(schema_editor) + functions_run_sql(schema_editor) + triggers_run_sql(schema_editor) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/__init__.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/branches.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/branches.py new file mode 100644 index 0000000000..e34e535f63 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/branches.py @@ -0,0 +1,17 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create table branches( + repoid int references repos on delete cascade not null, + updatestamp timestamptz not null, + branch text not null, + base text, + head text not null, + authors int[] + ); + + create index branches_repoid on branches (repoid); + + create unique index branches_repoid_branch on branches (repoid, branch); + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/commit_notifications.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/commit_notifications.py new file mode 100644 index 0000000000..975dc80c64 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/commit_notifications.py @@ -0,0 +1,17 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create table commit_notifications( + id bigserial primary key, + commit_id bigint references commits(id) on delete cascade not null, + notification_type notifications not null, + decoration_type decorations, + created_at timestamp, + updated_at timestamp, + state commit_notification_state, + CONSTRAINT commit_notifications_commit_id_notification_type UNIQUE(commit_id, notification_type) + ); + + create index commit_notifications_commit_id on commit_notifications (commit_id); + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/commits.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/commits.py new file mode 100644 index 0000000000..8c2d80becc --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/commits.py @@ -0,0 +1,31 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create table commits( + commitid text not null, + id bigserial primary key, + timestamp timestamp not null, + repoid int references repos on delete cascade not null, + branch text, + pullid int, + author int references owners on delete set null, + ci_passed boolean, + updatestamp timestamp, + message text, + state commit_state, + merged boolean, + deleted boolean, + notified boolean, + version smallint, -- will be removed after migrations + parent text, + totals jsonb, + report jsonb + ); + + create unique index commits_repoid_commitid on commits (repoid, commitid); + + create index commits_repoid_timestamp_desc on commits (repoid, timestamp desc); + + create index commits_on_pull on commits (repoid, pullid) where deleted is not true; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/main.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/main.py new file mode 100644 index 0000000000..d1cc1224d9 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/main.py @@ -0,0 +1,21 @@ +from .branches import run_sql as branches_run_sql +from .commit_notifications import run_sql as commit_notifications_run_sql +from .commits import run_sql as commits_run_sql +from .owners import run_sql as owners_run_sql +from .pulls import run_sql as pulls_run_sql +from .reports import run_sql as reports_run_sql +from .repos import run_sql as repos_run_sql +from .sessions import run_sql as sessions_run_sql +from .users import run_sql as users_run_sql + + +def run_sql(schema_editor): + users_run_sql(schema_editor) + owners_run_sql(schema_editor) + sessions_run_sql(schema_editor) + repos_run_sql(schema_editor) + branches_run_sql(schema_editor) + pulls_run_sql(schema_editor) + commits_run_sql(schema_editor) + commit_notifications_run_sql(schema_editor) + reports_run_sql(schema_editor) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/owners.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/owners.py new file mode 100644 index 0000000000..346e65bb92 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/owners.py @@ -0,0 +1,51 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create table owners( + ownerid serial primary key, + service service not null, + username citext, + email text, + name text, + oauth_token text, + stripe_customer_id text, + stripe_subscription_id text, + createstamp timestamptz, + service_id text not null, + private_access boolean, + staff boolean default false, -- codecov staff + cache jsonb, -- {"stats": {}} + plan plans default null, + plan_provider plan_providers, + plan_user_count smallint, + plan_auto_activate boolean, + plan_activated_users int[], + did_trial boolean, + free smallint default 0 not null, + invoice_details text, + student boolean default false not null, + student_created_at timestamp default null, + student_updated_at timestamp default null, + -- bot int, SEE BELOW + delinquent boolean, + yaml jsonb, + updatestamp timestamp, + organizations int[], -- what teams I'm member of + admins int[], -- who can edit my billing + integration_id int, -- github integration id + permission int[] + ); + + create unique index owner_service_username on owners (service, username); + + create unique index owner_service_ids on owners (service, service_id); + + alter table owners add column bot int references owners on delete set null; + + alter table owners add column avatar_url text; + + alter table owners add column parent_service_id text; + + alter table owners add column root_parent_service_id text; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/pulls.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/pulls.py new file mode 100644 index 0000000000..ca326f120a --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/pulls.py @@ -0,0 +1,24 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create table pulls( + repoid int references repos on delete cascade not null, + pullid int not null, + issueid int, -- gitlab + updatestamp timestamp, + state pull_state not null default 'open', + title text, + base text, + compared_to text, + head text, + commentid text, + diff jsonb, + flare jsonb, -- only when pull is open + author int references owners on delete set null + ); + + create unique index pulls_repoid_pullid on pulls (repoid, pullid); + + create index pulls_repoid_state_open on pulls (repoid) where state = 'open'; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/reports.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/reports.py new file mode 100644 index 0000000000..84291c6439 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/reports.py @@ -0,0 +1,142 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + -- EOF + -- + -- Create model CommitReport + -- + CREATE TABLE "reports_commitreport" ( + "id" bigserial NOT NULL PRIMARY KEY, + "external_id" uuid NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "commit_id" bigint NOT NULL + ); + -- + -- Create model ReportDetails + -- + CREATE TABLE "reports_reportdetails" ( + "id" bigserial NOT NULL PRIMARY KEY, + "external_id" uuid NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "files_array" jsonb[] NOT NULL, + "report_id" bigint NOT NULL UNIQUE + ); + -- + -- Create model ReportLevelTotals + -- + CREATE TABLE "reports_reportleveltotals" ( + "id" bigserial NOT NULL PRIMARY KEY, + "external_id" uuid NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "branches" integer NOT NULL, + "coverage" numeric(7, 2) NOT NULL, + "hits" integer NOT NULL, + "lines" integer NOT NULL, + "methods" integer NOT NULL, + "misses" integer NOT NULL, + "partials" integer NOT NULL, + "files" integer NOT NULL, + "report_id" bigint NOT NULL UNIQUE + ); + -- + -- Create model ReportSession + -- + CREATE TABLE "reports_upload" ( + "id" bigserial NOT NULL PRIMARY KEY, + "external_id" uuid NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "build_code" text NULL, + "build_url" text NULL, + "env" jsonb NULL, + "job_code" text NULL, + "name" varchar(100) NULL, + "provider" varchar(50) NULL, + "state" varchar(100) NOT NULL, + "storage_path" text NOT NULL, + "order_number" integer NULL, + "upload_extras" jsonb NOT NULL, + "upload_type" varchar(100) NOT NULL + ); + -- + -- Create model ReportSessionError + -- + CREATE TABLE "reports_uploaderror" ( + "id" bigserial NOT NULL PRIMARY KEY, + "external_id" uuid NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "error_code" varchar(100) NOT NULL, + "error_params" jsonb NOT NULL, + "upload_id" bigint NOT NULL + ); + -- + -- Create model ReportSessionFlagMembership + -- + CREATE TABLE "reports_uploadflagmembership" ( + "id" bigserial NOT NULL PRIMARY KEY + ); + -- + -- Create model RepositoryFlag + -- + CREATE TABLE "reports_repositoryflag" ( + "id" bigserial NOT NULL PRIMARY KEY, + "external_id" uuid NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "flag_name" varchar(255) NOT NULL, + "repository_id" integer NOT NULL + ); + -- + -- Create model SessionLevelTotals + -- + CREATE TABLE "reports_uploadleveltotals" ( + "id" bigserial NOT NULL PRIMARY KEY, + "external_id" uuid NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "branches" integer NOT NULL, + "coverage" numeric(7, 2) NOT NULL, + "hits" integer NOT NULL, + "lines" integer NOT NULL, + "methods" integer NOT NULL, + "misses" integer NOT NULL, + "partials" integer NOT NULL, + "files" integer NOT NULL, + "upload_id" bigint NOT NULL UNIQUE + ); + -- + -- Add field flag to reportsessionflagmembership + -- + ALTER TABLE "reports_uploadflagmembership" ADD COLUMN "flag_id" bigint NOT NULL; + -- + -- Add field report_session to reportsessionflagmembership + -- + ALTER TABLE "reports_uploadflagmembership" ADD COLUMN "upload_id" bigint NOT NULL; + -- + -- Add field flags to reportsession + -- + -- + -- Add field report to reportsession + -- + ALTER TABLE "reports_upload" ADD COLUMN "report_id" bigint NOT NULL; + ALTER TABLE "reports_commitreport" ADD CONSTRAINT "reports_commitreport_commit_id_06d0bd39_fk_commits_id" FOREIGN KEY ("commit_id") REFERENCES "commits" ("id") DEFERRABLE INITIALLY DEFERRED; + CREATE INDEX "reports_commitreport_commit_id_06d0bd39" ON "reports_commitreport" ("commit_id"); + ALTER TABLE "reports_reportdetails" ADD CONSTRAINT "reports_reportdetail_report_id_4681bfd3_fk_reports_c" FOREIGN KEY ("report_id") REFERENCES "reports_commitreport" ("id") DEFERRABLE INITIALLY DEFERRED; + ALTER TABLE "reports_reportleveltotals" ADD CONSTRAINT "reports_reportlevelt_report_id_b690dffa_fk_reports_c" FOREIGN KEY ("report_id") REFERENCES "reports_commitreport" ("id") DEFERRABLE INITIALLY DEFERRED; + ALTER TABLE "reports_uploaderror" ADD CONSTRAINT "reports_reportsessio_report_session_id_bb6563f1_fk_reports_r" FOREIGN KEY ("upload_id") REFERENCES "reports_upload" ("id") DEFERRABLE INITIALLY DEFERRED; + CREATE INDEX "reports_uploaderror_report_session_id_bb6563f1" ON "reports_uploaderror" ("upload_id"); + ALTER TABLE "reports_repositoryflag" ADD CONSTRAINT "reports_repositoryflag_repository_id_9b64b64c_fk_repos_repoid" FOREIGN KEY ("repository_id") REFERENCES "repos" ("repoid") DEFERRABLE INITIALLY DEFERRED; + CREATE INDEX "reports_repositoryflag_repository_id_9b64b64c" ON "reports_repositoryflag" ("repository_id"); + ALTER TABLE "reports_uploadleveltotals" ADD CONSTRAINT "reports_sessionlevel_report_session_id_e2cd6669_fk_reports_r" FOREIGN KEY ("upload_id") REFERENCES "reports_upload" ("id") DEFERRABLE INITIALLY DEFERRED; + CREATE INDEX "reports_uploadflagmembership_flag_id_59edee69" ON "reports_uploadflagmembership" ("flag_id"); + ALTER TABLE "reports_uploadflagmembership" ADD CONSTRAINT "reports_reportsessio_flag_id_59edee69_fk_reports_r" FOREIGN KEY ("flag_id") REFERENCES "reports_repositoryflag" ("id") DEFERRABLE INITIALLY DEFERRED; + CREATE INDEX "reports_uploadflagmembership_report_session_id_7d7f9546" ON "reports_uploadflagmembership" ("upload_id"); + ALTER TABLE "reports_uploadflagmembership" ADD CONSTRAINT "reports_reportsessio_report_session_id_7d7f9546_fk_reports_r" FOREIGN KEY ("upload_id") REFERENCES "reports_upload" ("id") DEFERRABLE INITIALLY DEFERRED; + CREATE INDEX "reports_upload_report_id_f6b4ffae" ON "reports_upload" ("report_id"); + ALTER TABLE "reports_upload" ADD CONSTRAINT "reports_reportsessio_report_id_f6b4ffae_fk_reports_c" FOREIGN KEY ("report_id") REFERENCES "reports_commitreport" ("id") DEFERRABLE INITIALLY DEFERRED; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/repos.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/repos.py new file mode 100644 index 0000000000..2623af5035 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/repos.py @@ -0,0 +1,31 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create table repos( + repoid serial primary key, + ownerid int references owners on delete cascade not null, + service_id text not null, + name citext, + private boolean not null, + branch text default 'master' not null, + upload_token uuid unique default uuid_generate_v4(), + image_token text default random_string(10), + updatestamp timestamptz, + language languages, + active boolean, + deleted boolean default false not null, + activated boolean default false, + bot int references owners on delete set null, + yaml jsonb, + cache jsonb, -- {"totals": {}, "trends": [], "commit": {}, "yaml": ""} + hookid text, + using_integration boolean -- using github integration + ); + + create unique index repos_slug on repos (ownerid, name); + + create unique index repos_service_ids on repos (ownerid, service_id); + + alter table repos add column forkid int references repos; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/sessions.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/sessions.py new file mode 100644 index 0000000000..8cccb99874 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/sessions.py @@ -0,0 +1,15 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create table sessions( + sessionid serial primary key, + token uuid unique default uuid_generate_v4() not null, + name text, + ownerid int references owners on delete cascade not null, + type sessiontype not null, + lastseen timestamptz, + useragent text, + ip text + ); + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/users.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/users.py new file mode 100644 index 0000000000..2fd0f0d032 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/tables/users.py @@ -0,0 +1,15 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + CREATE TABLE IF NOT EXISTS "users" ( + "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + "external_id" uuid NOT NULL UNIQUE, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "email" citext NULL, + "name" text NULL, + "is_staff" boolean NULL, + "is_superuser" boolean NULL + ); + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/triggers/__init__.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/triggers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/triggers/commits.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/triggers/commits.py new file mode 100644 index 0000000000..231d9e1560 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/triggers/commits.py @@ -0,0 +1,108 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create or replace function commits_update_heads() returns trigger as $$ + begin + + if new.pullid is not null and new.merged is not true then + -- update head of pulls + update pulls p + set updatestamp = now(), + head = case when head is not null + and (select timestamp > new.timestamp + from commits c + where c.repoid=new.repoid + and c.commitid=p.head + and c.deleted is not true + limit 1) + then head + else new.commitid + end, + author = coalesce(author, new.author) + where repoid = new.repoid + and pullid = new.pullid; + + end if; + + -- update head of branches + if new.branch is not null then + update branches + set updatestamp = now(), + authors = array_append_unique(coalesce(authors, '{}'::int[]), new.author), + head = case + when head is null then new.commitid + when ( + head != new.commitid + and new.timestamp >= coalesce((select timestamp + from commits + where commitid=head + and deleted is not true + and repoid=new.repoid + limit 1), '-infinity'::timestamp) + ) then new.commitid + else head end + where repoid = new.repoid + and branch = new.branch; + if not found then + insert into branches (repoid, updatestamp, branch, head, authors) + values (new.repoid, new.timestamp, new.branch, new.commitid, + case when new.author is not null then array[new.author] else null end); + end if; + end if; + + return null; + end; + $$ language plpgsql; + + create trigger commits_update_heads after update on commits + for each row + when (( + new.deleted is distinct from old.deleted + ) or ( + new.state = 'complete'::commit_state + and new.deleted is not true + and + ( + new.state is distinct from old.state + or new.pullid is distinct from old.pullid + or new.merged is distinct from old.merged + or new.branch is distinct from old.branch + ) + )) + execute procedure commits_update_heads(); + + + create or replace function commits_insert_pr_branch() returns trigger as $$ + begin + if new.pullid is not null and new.merged is not true then + begin + insert into pulls (repoid, pullid, author, head) + values (new.repoid, new.pullid, new.author, new.commitid); + exception when unique_violation then + end; + end if; + + if new.branch is not null then + begin + insert into branches (repoid, updatestamp, branch, authors, head) + values (new.repoid, new.timestamp, + new.branch, + case when new.author is not null then array[new.author] else null end, + new.commitid); + exception when unique_violation then + end; + end if; + + update repos + set updatestamp=now() + where repoid=new.repoid; + + return null; + end; + $$ language plpgsql; + + create trigger commits_insert_pr_branch after insert on commits + for each row + execute procedure commits_insert_pr_branch(); + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/triggers/main.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/triggers/main.py new file mode 100644 index 0000000000..1fe503486a --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/triggers/main.py @@ -0,0 +1,9 @@ +from .commits import run_sql as commits_run_sql +from .owners import run_sql as owners_run_sql +from .repos import run_sql as repos_run_sql + + +def run_sql(schema_editor): + commits_run_sql(schema_editor) + owners_run_sql(schema_editor) + repos_run_sql(schema_editor) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/triggers/owners.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/triggers/owners.py new file mode 100644 index 0000000000..e470306463 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/triggers/owners.py @@ -0,0 +1,26 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create or replace function owners_before_insert_or_update() returns trigger as $$ + begin + -- user has changed name or deleted and invalidate sessions + with _owners as (update owners + set username = null + where service = new.service + and username = new.username::citext + returning ownerid) + delete from sessions where ownerid in (select ownerid from _owners); + return new; + end; + $$ language plpgsql; + + create trigger owners_before_insert before insert on owners + for each row + execute procedure owners_before_insert_or_update(); + + create trigger owners_before_update before update on owners + for each row + when (new.username is not null and new.username is distinct from old.username) + execute procedure owners_before_insert_or_update(); + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/triggers/repos.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/triggers/repos.py new file mode 100644 index 0000000000..341f64b89c --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/triggers/repos.py @@ -0,0 +1,27 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create or replace function repos_before_insert_or_update() returns trigger as $$ + begin + -- repo name changed or deleted + update repos + set name = null, + deleted = true, + active = false, + activated = false + where ownerid = new.ownerid + and name = new.name; + return new; + end; + $$ language plpgsql; + + create trigger repos_before_insert before insert on repos + for each row + execute procedure repos_before_insert_or_update(); + + create trigger repos_before_update before update on repos + for each row + when (new.name is not null and new.name is distinct from old.name) + execute procedure repos_before_insert_or_update(); + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/types.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/types.py new file mode 100644 index 0000000000..0e799c9991 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/main/types.py @@ -0,0 +1,26 @@ +def run_sql(schema_editor): + schema_editor.execute( + """ + create type service as enum ('github', 'bitbucket', 'gitlab', 'github_enterprise', 'gitlab_enterprise', 'bitbucket_server'); + + create type plans as enum('5m', '5y', '25m', '25y', '50m', '50y', '100m', '100y', '250m', '250y', '500m', '500y', '1000m', '1000y', '1m', '1y', + 'v4-10m', 'v4-10y', 'v4-20m', 'v4-20y', 'v4-50m', 'v4-50y', 'v4-125m', 'v4-125y', 'v4-300m', 'v4-300y', + 'users', 'users-inappm', 'users-inappy', 'users-pr-inappm', 'users-pr-inappy', 'users-free'); + + create type sessiontype as enum('api', 'login'); + + create type languages as enum('javascript', 'shell', 'python', 'ruby', 'perl', 'dart', 'java', 'c', 'clojure', 'd', 'fortran', 'go', 'groovy', 'kotlin', 'php', 'r', 'scala', 'swift', 'objective-c', 'xtend'); + + create type pull_state as enum('open', 'closed', 'merged'); + + create type commit_state as enum('pending', 'complete', 'error', 'skipped'); + + create type plan_providers as enum('github'); + + create type notifications as enum('comment', 'gitter', 'hipchat', 'irc', 'slack', 'status_changes', 'status_patch', 'status_project', 'webhook', 'checks_patch', 'checks_project', 'checks_changes'); + + create type decorations as enum('standard', 'upgrade'); + + create type commit_notification_state as enum('pending', 'success', 'error'); + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/__init__.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/main.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/main.py new file mode 100644 index 0000000000..33eac998f6 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/main.py @@ -0,0 +1,55 @@ +from .v440 import run_sql as v440_run_sql +from .v442 import run_sql as v442_run_sql +from .v443 import run_sql as v443_run_sql +from .v446 import run_sql as v446_run_sql +from .v447 import run_sql as v447_run_sql +from .v448 import run_sql as v448_run_sql +from .v449 import run_sql as v449_run_sql +from .v451 import run_sql as v451_run_sql +from .v452 import run_sql as v452_run_sql +from .v453 import run_sql as v453_run_sql +from .v454 import run_sql as v454_run_sql +from .v455 import run_sql as v455_run_sql +from .v461 import run_sql as v461_run_sql +from .v4410 import run_sql as v4410_run_sql +from .v4510 import run_sql as v4510_run_sql + +UPGRADE_MIGRATIONS_BY_VERSION = ( + ((4, 4, 0), v440_run_sql), + ((4, 4, 2), v442_run_sql), + ((4, 4, 3), v443_run_sql), + ((4, 4, 6), v446_run_sql), + ((4, 4, 7), v447_run_sql), + ((4, 4, 8), v448_run_sql), + ((4, 4, 9), v449_run_sql), + ((4, 4, 10), v4410_run_sql), + ((4, 5, 1), v451_run_sql), + ((4, 5, 2), v452_run_sql), + ((4, 5, 3), v453_run_sql), + ((4, 5, 4), v454_run_sql), + ((4, 5, 5), v455_run_sql), + ((4, 5, 10), v4510_run_sql), + ((4, 6, 1), v461_run_sql), +) + + +def _version_normalize(version): + return tuple(int(x or 0) for x in version.replace("v", "").split(".")) + + +def run_sql(schema_editor, current_version): + normalized_current_version = _version_normalize(current_version) + upgrade_migration_index_to_start_from = None + + for idx, (upgrade_version, _) in enumerate(UPGRADE_MIGRATIONS_BY_VERSION): + if upgrade_version > normalized_current_version: + upgrade_migration_index_to_start_from = idx + break + + if not upgrade_migration_index_to_start_from: + return + + for _, upgrade_migration in UPGRADE_MIGRATIONS_BY_VERSION[ + upgrade_migration_index_to_start_from: + ]: + upgrade_migration(schema_editor) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v440.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v440.py new file mode 100644 index 0000000000..4309aa3faf --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v440.py @@ -0,0 +1,335 @@ +from ..main.functions.aggregates import run_sql as aggregates_run_sql +from ..main.functions.coverage import run_sql as coverage_run_sql +from ..main.functions.get_access_token import run_sql as get_access_token_run_sql +from ..main.functions.get_author import run_sql as get_author_run_sql +from ..main.functions.get_commit import run_sql as get_commit_run_sql +from ..main.functions.get_customer import run_sql as get_customer_run_sql +from ..main.functions.get_graph_for import run_sql as get_graph_for_run_sql +from ..main.functions.get_ownerid import run_sql as get_ownerid_run_sql +from ..main.functions.get_repo import run_sql as get_repo_run_sql +from ..main.functions.get_user import run_sql as get_user_run_sql +from ..main.functions.insert_commit import run_sql as insert_commit_run_sql +from ..main.functions.refresh_repos import run_sql as refresh_repos_run_sql +from ..main.functions.update_json import run_sql as update_json_run_sql +from ..main.functions.verify_session import run_sql as verify_session_run_sql + + +# v4.4.0 +def run_sql(schema_editor): + schema_editor.execute( + """ + ---- Column Updates ----- + drop trigger repo_yaml_update on repos; + drop trigger owner_yaml_updated on owners; + + alter table owners drop column if exists errors; + alter table owners drop column if exists yaml_repoid; + alter table commits drop column if exists logs; + alter table commits drop column if exists archived; + alter table pulls rename column totals to diff; + alter table pulls drop column if exists changes; + alter table pulls drop column if exists base_branch; + alter table pulls drop column if exists head_branch; + alter table repos alter column yaml set data type jsonb; + alter table repos alter column cache set data type jsonb; + alter table owners alter column cache set data type jsonb; + alter table owners alter column yaml set data type jsonb; + alter table commits alter column totals set data type jsonb; + alter table commits alter column report set data type jsonb; + alter table pulls alter column diff set data type jsonb; + alter table pulls alter column flare set data type jsonb; + alter table owners alter column integration_id set data type integer; + + create trigger repo_yaml_update after update on repos + for each row + when ( + ((new.yaml->'codecov'->>'bot')::text is distinct from (old.yaml->'codecov'->>'bot')::text) + or ((new.yaml->'codecov'->>'branch')::text is distinct from (old.yaml->'codecov'->>'branch')::text) + ) + execute procedure repo_yaml_update(); + + + create trigger owner_yaml_updated before update on owners + for each row + when ( + ((new.yaml->'codecov'->>'bot')::text is distinct from (old.yaml->'codecov'->>'bot')::text) + or ((new.yaml->'codecov'->>'branch')::text is distinct from (old.yaml->'codecov'->>'branch')::text) + ) + execute procedure owner_yaml_updated(); + + ---- Function Changes ----- + drop function if exists get_new_repos(int); + drop function if exists get_pull(int, int); + drop function if exists coverage(service, text, text, text, text); + drop function if exists extract_totals(version smallint, files json, sessionids integer[]); + drop function if exists get_commit(repoid, _commitid, path, tree_only); + drop function if exists get_commit_on_branch(integer, text, text, boolean); + drop function if exists get_totals_for_file(smallint, json); + drop function if exists refresh_teams(service, json, integer); + drop function if exists get_commit(integer, text, text, boolean); + + -- insert_commit.sql + drop function if exists insert_commit(integer, text, text, integer, json); + """ + ) + insert_commit_run_sql(schema_editor) + + schema_editor.execute( + """ + -- aggregates.sql + drop function if exists _pop_first_as_json(json[]) cascade; + drop function if exists _max_coverage(json[]) cascade; + drop function if exists _min_coverage(json[]) cascade; + drop function _max_coverage(json[], json); + drop function _min_coverage(json[], json); + drop aggregate agg_totals(json); + drop function _agg_report_totals(text[], json); + """ + ) + aggregates_run_sql(schema_editor) + + schema_editor.execute( + """ + -- coverage.sql + drop function if exists get_coverage(service,citext,citext,citext); + """ + ) + coverage_run_sql(schema_editor) + + schema_editor.execute( + """ + -- get_access_token.sql + drop function if exists get_access_token(int); + """ + ) + get_access_token_run_sql(schema_editor) + + schema_editor.execute( + """ + -- get_author.sql + drop function if exists get_author(int); + """ + ) + get_author_run_sql(schema_editor) + + schema_editor.execute( + """ + -- get_commit.sql + drop function if exists get_commit_totals(int, text); + drop function if exists get_commit_totals(int, text, text); + drop function if exists get_commit(repoid integer, _commitid text); + drop function if exists get_commit_minimum(int, text); + drop function if exists get_ + commit_on_branch(int, text); + """ + ) + get_commit_run_sql(schema_editor) + + schema_editor.execute( + """ + -- get_customer.sql + drop function if exists get_customer(int); + """ + ) + get_customer_run_sql(schema_editor) + + schema_editor.execute( + """ + -- get_graph_for.sql + drop function if exists sum_of_file_totals_filtering_sessionids(json, int[]); + drop function if exists extract_totals(files json, sessionids int[]); + drop function if exists list_sessionid_by_filtering_flags(sessions json, flags text[]); + drop function if exists total_list_to_json(totals text[]); + drop function if exists sum_session_totals(sessions json, flags text[]); + drop function if exists get_graph_for_flare_pull(int, text, text, text[]); + drop function if exists get_graph_for_flare_commit(int, text, text, text[]); + drop function if exists get_graph_for_flare_branch(int, text, text, text[]); + drop function if exists get_graph_for_totals_pull(int, text, text, text[]); + drop function if exists get_graph_for_totals_commit(int, text, text, text[]); + drop function if exists get_graph_for_totals_branch(int, text, text, text[]); + drop function if exists get_graph_for_commits_pull(int, text, text, text[]); + drop function if exists get_graph_for_commits_branch(int, text, text, text[]); + """ + ) + get_graph_for_run_sql(schema_editor) + + schema_editor.execute( + """ + -- get_ownerid.sql + drop function if exists get_owner(service, citext); + drop function if exists get_teams(service, integer[]); + """ + ) + get_ownerid_run_sql(schema_editor) + + schema_editor.execute( + """ + -- get_repo.sql + drop function if exists get_repo(int); + drop function if exists get_repo(int, citext); + drop function if exists get_repo_by_token(uuid); + drop function if exists get_repos(int, int, int); + """ + ) + get_repo_run_sql(schema_editor) + + schema_editor.execute( + """ + -- get_user.sql + drop function if exists get_user(int); + drop function if exists get_username(int); + drop function if exists get_users(int[]); + """ + ) + get_user_run_sql(schema_editor) + + schema_editor.execute( + """ + -- refresh_repos.sql + drop function if exists refresh_teams(service, json); + drop function if exists refresh_repos(service, json, int, boolean); + """ + ) + refresh_repos_run_sql(schema_editor) + + schema_editor.execute( + """ + -- update_json.sql + drop function if exists add_key_to_json(json, text, json); + drop function if exists add_key_to_json(json, text, integer); + drop function if exists add_key_to_json(json, text, text); + drop function if exists remove_key_from_json(json, text); + drop function if exists update_json(json, text, json); + drop function if exists update_json(json, text, integer); + drop function if exists update_json(json, text, text); + """ + ) + update_json_run_sql(schema_editor) + + schema_editor.execute( + """ + -- verify_session.sql + drop function if exists verify_session(text, text, uuid, sessiontype); + """ + ) + verify_session_run_sql(schema_editor) + + schema_editor.execute( + """ + -- Trigger Changes -- + create or replace function commits_update_heads() returns trigger as $$ + begin + + if new.pullid is not null and new.merged is not true then + -- update head of pulls + update pulls p + set updatestamp = now(), + head = case when head is not null + and (select timestamp > new.timestamp + from commits c + where c.repoid=new.repoid + and c.commitid=p.head + and c.deleted is not true + limit 1) + then head + else new.commitid + end, + author = coalesce(author, new.author) + where repoid = new.repoid + and pullid = new.pullid; + + end if; + + -- update head of branches + if new.branch is not null then + update branches + set updatestamp = now(), + authors = array_append_unique(coalesce(authors, '{}'::int[]), new.author), + head = case + when head is null then new.commitid + when ( + head != new.commitid + and new.timestamp >= coalesce((select timestamp + from commits + where commitid=head + and deleted is not true + and repoid=new.repoid + limit 1), '-infinity'::timestamp) + ) then new.commitid + else head end + where repoid = new.repoid + and branch = new.branch; + if not found then + insert into branches (repoid, updatestamp, branch, head, authors) + values (new.repoid, new.timestamp, new.branch, new.commitid, + case when new.author is not null then array[new.author] else null end); + end if; + end if; + + return null; + end; + $$ language plpgsql; + + create or replace function branches_update() returns trigger as $$ + declare _ownerid int; + begin + -- update repos cache if main branch + update repos + set updatestamp = now(), + cache = update_json(cache::jsonb, 'commit', get_commit_minimum(new.repoid, new.head)::jsonb) + where repoid = new.repoid + and branch = new.branch + returning ownerid into _ownerid; + + if found then + -- default branch updated, so we can update the owners timestamp + -- to refresh the team list + update owners + set updatestamp=now() + where ownerid=_ownerid; + end if; + + return null; + end; + $$ language plpgsql; + + + create or replace function repos_before_insert_or_update() returns trigger as $$ + begin + -- repo name changed or deleted + update repos + set name = null, + deleted = true, + active = false, + activated = false + where ownerid = new.ownerid + and name = new.name; + return new; + end; + $$ language plpgsql; + + + create index commits_on_pull on commits (repoid, pullid) where deleted is not true; + + alter table commits drop column chunks; + + drop trigger commits_update_heads on commits; + + create trigger commits_update_heads after update on commits + for each row + when (( + new.deleted is distinct from old.deleted + ) or ( + new.state = 'complete'::commit_state + and new.deleted is not true + and + ( + new.state is distinct from old.state + or new.pullid is distinct from old.pullid + or new.merged is distinct from old.merged + or new.branch is distinct from old.branch + ) + )) + execute procedure commits_update_heads(); + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v4410.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v4410.py new file mode 100644 index 0000000000..4223b56e61 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v4410.py @@ -0,0 +1,29 @@ +# v4.4.10 +def run_sql(schema_editor): + schema_editor.execute( + """ + create or replace function owner_yaml_updated() returns trigger as $$ + begin + if (new.yaml->'codecov'->'bot')::citext is distinct from 'null' then + new.bot = coalesce( + get_ownerid_if_member( + new.service, + (new.yaml->'codecov'->>'bot')::citext, + new.ownerid + ), + old.bot + ); + else + new.bot = null; + end if; + + -- update repo branches + update repos r + set branch = coalesce((r.yaml->'codecov'->>'branch'), (new.yaml->'codecov'->>'branch'), branch) + where ownerid = new.ownerid; + + return new; + end; + $$ language plpgsql; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v442.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v442.py new file mode 100644 index 0000000000..7befcd0535 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v442.py @@ -0,0 +1,104 @@ +# v4.4.2 +def run_sql(schema_editor): + schema_editor.execute( + """ + ---- Column Updates ----- + alter table owners add column avatar_url text; + + + ---- Function Changes ----- + + -- get_ownerid.sql + create or replace function get_owner(service, citext) returns jsonb as $$ + with data as ( + select service_id, service, ownerid::text, username, avatar_url, + updatestamp, plan, name, integration_id, free, + plan_activated_users, plan_auto_activate, plan_user_count + from owners + where service=$1 + and username=$2::citext + limit 1 + ) select to_jsonb(data) + from data + limit 1; + $$ language sql stable strict; + + -- get_ownerid.sql + create or replace function get_or_create_owner(service, text, text, text) returns int as $$ + declare _ownerid int; + begin + update owners + set username = $3, avatar_url = $4 + where service = $1 + and service_id = $2 + returning ownerid into _ownerid; + + if not found then + insert into owners (service, service_id, username, avatar_url) + values ($1, $2, $3, $4) + returning ownerid into _ownerid; + end if; + + return _ownerid; + + end; + $$ language plpgsql volatile; + + -- get_user.sql + create or replace function get_user(int) returns jsonb as $$ + with data as ( + select ownerid::text, private_access, staff, service, service_id, + username, organizations, avatar_url, + oauth_token, plan, permission, + free, email, name, createstamp + from owners + where ownerid=$1 + limit 1 + ) select to_jsonb(data) from data; + $$ language sql stable; + + -- get_user.sql + create or replace function get_users(int[]) returns jsonb as $$ + with data as ( + select service, service_id::text, ownerid::text, username, name, email, avatar_url + from owners + where array[ownerid] <@ $1 + limit array_length($1, 1) + ) select jsonb_agg(data) + from data + limit array_length($1, 1); + $$ language sql stable strict; + + -- refresh_repos.sql + create or replace function refresh_teams(service, jsonb) returns int[] as $$ + declare ownerids int[]; + declare _ownerid int; + declare _team record; + begin + for _team in select d from jsonb_array_elements($2) d loop + update owners o + set username = (_team.d->>'username')::citext, + name = (_team.d->>'name')::text, + email = (_team.d->>'email')::text, + avatar_url = (_team.d->>'avatar_url')::text, + updatestamp = now() + where service = $1 + and service_id = (_team.d->>'id')::text + returning ownerid into _ownerid; + + if not found then + insert into owners (service, service_id, username, name, email, avatar_url) + values ($1, (_team.d->>'id')::text, (_team.d->>'username')::citext, (_team.d->>'name')::text, (_team.d->>'email')::text, (_team.d->>'avatar_url')::text) + returning ownerid into _ownerid; + end if; + + select array_append(ownerids, _ownerid) into ownerids; + + end loop; + + return ownerids; + + end; + $$ language plpgsql volatile strict; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v443.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v443.py new file mode 100644 index 0000000000..59c1945cfc --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v443.py @@ -0,0 +1,191 @@ +# v4.4.3 +def run_sql(schema_editor): + schema_editor.execute( + """ + ---- Table Changes ----- + alter table owners add column parent_service_id text; + + + ----- Functions Created ----- + + -- get_customer.sql + create or replace function get_gitlab_root_group(int) returns jsonb as $$ + with recursive tree as ( + select o.service_id, + o.parent_service_id, + o.ownerid, + 1 as depth + from owners o + where o.ownerid = $1 + and o.service = 'gitlab' + and o.parent_service_id is not null + union all + select o.service_id, + o.parent_service_id, + o.ownerid, + depth + 1 as depth + from tree t + join owners o + on o.service_id = t.parent_service_id + where depth <= 20 + ), data as ( + select t.ownerid, + t.service_id + from tree t + where t.parent_service_id is null + ) + select to_jsonb(data) from data limit 1; + $$ language sql stable strict; + + -- get_customer.sql + create or replace function get_gitlab_repos_activated(int, text) returns int as $$ + declare _repos_activated int; + declare _decendents_owner_ids int[]; + begin + select array( + with recursive tree as ( + select ownerid, + service_id, + array[]::text[] as ancestors_service_id, + 1 as depth + from owners + where parent_service_id is null + and service = 'gitlab' + and ownerid = $1 + union all + select owners.ownerid, + owners.service_id, + tree.ancestors_service_id || owners.parent_service_id, + depth + 1 as depth + from owners, tree + where owners.parent_service_id = tree.service_id + and depth <= 20 + ) + select ownerid + from tree + where $2 = any(tree.ancestors_service_id) + ) into _decendents_owner_ids; + + select count(*) into _repos_activated + from repos + where ownerid in (select unnest(array_append(_decendents_owner_ids, $1))) + and private + and activated; + + return _repos_activated; + end; + $$ language plpgsql stable; + + -- get_customer.sql + create or replace function get_repos_activated(int) returns int as $$ + declare _repos_activated int; + declare _service text; + declare _service_id text; + begin + select o.service, o.service_id into _service, _service_id + from owners o where o.ownerid = $1; + + if _service = 'gitlab' then + select get_gitlab_repos_activated($1, _service_id) into _repos_activated; + else + select count(*) into _repos_activated + from repos + where ownerid=$1 + and private + and activated; + end if; + + return _repos_activated; + end; + $$ language plpgsql stable; + + + ---- Functions Modified ----- + + drop function if exists get_or_create_owner(service, text, text, text); -- signature change + + -- get_customer.sql + create or replace function get_customer(int) returns jsonb as $$ + with data as ( + select t.stripe_customer_id, + t.stripe_subscription_id, + t.ownerid::text, + t.service, + t.service_id, + t.plan_user_count, + t.plan_provider, + t.plan_auto_activate, + t.plan_activated_users, + t.plan, t.email, + t.free, t.did_trial, + t.invoice_details, + get_users(t.admins) as admins, + get_repos_activated($1) as repos_activated + from owners t + where t.ownerid = $1 + limit 1 + ) select to_jsonb(data) from data limit 1; + $$ language sql stable strict; + + -- refresh_repos.sql + create or replace function refresh_teams(service, jsonb) returns int[] as $$ + declare ownerids int[]; + declare _ownerid int; + declare _team record; + begin + for _team in select d from jsonb_array_elements($2) d loop + update owners o + set username = (_team.d->>'username')::citext, + name = (_team.d->>'name')::text, + email = (_team.d->>'email')::text, + avatar_url = (_team.d->>'avatar_url')::text, + parent_service_id = (_team.d->>'parent_id')::text, + updatestamp = now() + where service = $1 + and service_id = (_team.d->>'id')::text + returning ownerid into _ownerid; + + if not found then + insert into owners (service, service_id, username, name, email, avatar_url, parent_service_id) + values ($1, + (_team.d->>'id')::text, + (_team.d->>'username')::citext, + (_team.d->>'name')::text, + (_team.d->>'email')::text, + (_team.d->>'avatar_url')::text, + (_team.d->>'parent_id')::text + ) + returning ownerid into _ownerid; + end if; + + select array_append(ownerids, _ownerid) into ownerids; + + end loop; + + return ownerids; + + end; + $$ language plpgsql volatile strict; + + -- get_ownerid.sql + create or replace function get_or_create_owner(service, text, text, text, text) returns int as $$ + declare _ownerid int; + begin + update owners + set username = $3, avatar_url = $4, parent_service_id = $5 + where service = $1 + and service_id = $2 + returning ownerid into _ownerid; + + if not found then + insert into owners (service, service_id, username, avatar_url, parent_service_id) + values ($1, $2, $3, $4, $5) + returning ownerid into _ownerid; + end if; + + return _ownerid; + + end; + $$ language plpgsql volatile; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v446.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v446.py new file mode 100644 index 0000000000..d7e4253e32 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v446.py @@ -0,0 +1,67 @@ +# v4.4.6 +def run_sql(schema_editor): + schema_editor.execute( + """ + -- used for app/tasks + create or replace function get_repo(int) returns jsonb as $$ + with d as (select o.service, o.username, o.service_id as owner_service_id, r.ownerid::text, + r.name, r.repoid::text, r.service_id, r.updatestamp, + r.branch, r.private, hookid, image_token, b.username as bot_username, + r.yaml, o.yaml as org_yaml, r.using_integration, o.plan, + (r.cache->>'yaml') as _yaml_location, + case when r.using_integration then o.integration_id else null end as integration_id, + get_access_token(coalesce(r.bot, o.bot, o.ownerid)) as token, + case when private and activated is not true and forkid is not null + then (select rr.activated from repos rr where rr.repoid = r.forkid limit 1) + else activated end as activated + from repos r + inner join owners o using (ownerid) + left join owners b ON (r.bot=b.ownerid) + where r.repoid = $1 + limit 1) select to_jsonb(d) from d; + $$ language sql stable strict; + + + -- used for app/handlers + create or replace function get_repo(int, citext) returns jsonb as $$ + with repo as ( + select r.yaml, r.name, "language", repoid::text, r.private, r.deleted, r.active, r.cache, b.username as bot_username, + r.branch, r.service_id, r.updatestamp, upload_token, image_token, hookid, using_integration, + case when private and activated is not true and forkid is not null + then (select rr.activated from repos rr where rr.repoid = r.forkid limit 1) + else activated end as activated + from repos r + left join owners b ON (r.bot=b.ownerid) + where r.ownerid = $1 and r.name = $2::citext + limit 1 + ) select to_jsonb(repo) from repo; + $$ language sql stable; + + create or replace function get_customer(int) returns jsonb as $$ + with data as ( + select t.stripe_customer_id, + t.stripe_subscription_id, + t.ownerid::text, + t.service, + t.service_id, + t.plan_user_count, + t.plan_provider, + t.plan_auto_activate, + t.plan_activated_users, + t.plan, + t.email, + t.free, + t.did_trial, + t.invoice_details, + t.yaml, + b.username as bot_username, + get_users(t.admins) as admins, + get_repos_activated($1::int) as repos_activated + from owners t + LEFT JOIN owners b ON (b.ownerid = t.bot) + where t.ownerid = $1 + limit 1 + ) select to_jsonb(data) from data limit 1; + $$ language sql stable strict; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v447.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v447.py new file mode 100644 index 0000000000..e1a7d256cf --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v447.py @@ -0,0 +1,25 @@ +# v4.4.7 +def run_sql(schema_editor): + schema_editor.execute( + """ + drop trigger repo_yaml_update on repos; + drop trigger owner_yaml_updated on owners; + + create trigger repo_yaml_update after update on repos + for each row + when ( + ((new.yaml->'codecov'->>'bot')::text is distinct from (old.yaml->'codecov'->>'bot')::text) + or ((new.yaml->'codecov'->>'branch')::text is distinct from (old.yaml->'codecov'->>'branch')::text) + ) + execute procedure repo_yaml_update(); + + + create trigger owner_yaml_updated before update on owners + for each row + when ( + ((new.yaml->'codecov'->>'bot')::text is distinct from (old.yaml->'codecov'->>'bot')::text) + or ((new.yaml->'codecov'->>'branch')::text is distinct from (old.yaml->'codecov'->>'branch')::text) + ) + execute procedure owner_yaml_updated(); + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v448.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v448.py new file mode 100644 index 0000000000..5a97b8b6a7 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v448.py @@ -0,0 +1,27 @@ +# v4.4.8 +def run_sql(schema_editor): + schema_editor.execute( + """ + --- transaction friendly enum column upates. See: https://stackoverflow.com/questions/1771543/adding-a-new-value-to-an-existing-enum-type#7834949 -- + + + -- rename the old enum -- rename the old enum + alter type plans rename to plans__; -- alter type plans rename to plans__; + -- create the new enum -- -- create the new enum + create type plans as enum('5m', '5y', '25m', '25y', '50m', '50y', '100m', '100y', '250m', '250y', '500m', '500y', '1000m', '1000y', '1m', '1y', -- create type plans as enum('5m', '5y', '25m', '25y', '50m', '50y', '100m', '100y', '250m', '250y', '500m', '500y', '1000m', '1000y', '1m', '1y', + 'v4-10m', 'v4-10y', 'v4-20m', 'v4-20y', 'v4-50m', 'v4-50y', 'v4-125m', 'v4-125y', 'v4-300m', 'v4-300y', -- 'v4-10m', 'v4-10y', 'v4-20m', 'v4-20y', 'v4-50m', 'v4-50y', 'v4-125m', 'v4-125y', 'v4-300m', 'v4-300y', + 'users', 'users-inappm', 'users-inappy', 'users-free'); -- 'users', 'users-inappm', 'users-inappy', 'users-free'); + -- alter all enum columns + alter table owner; + alter column plan type plans using plan::text::plans; + + + -- drop the old enum + drop type plans__; + + + ALTER TABLE ONLY owners ALTER COLUMN plan SET DEFAULT 'users-free'; -- ALTER TABLE ONLY owners ALTER COLUMN plan SET DEFAULT 'users-free'; + ALTER TABLE ONLY owners ALTER COLUMN plan_user_count SET DEFAULT 5; -- ALTER TABLE ONLY owners ALTER COLUMN plan_user_count SET DEFAULT 5; + ALTER TABLE ONLY owners ALTER COLUMN plan_auto_activate SET DEFAULT true; -- ALTER TABLE ONLY owners ALTER COLUMN plan_auto_activate SET DEFAULT true; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v449.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v449.py new file mode 100644 index 0000000000..7bf480d97b --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v449.py @@ -0,0 +1,41 @@ +# v4.4.9 +def run_sql(schema_editor): + schema_editor.execute( + """ + alter table owners add column student boolean null; + alter table owners add column student_updated_at timestamp; + alter table owners add column student_created_at timestamp; + + + -- new get customer to return student status + create or replace function get_customer(int) returns jsonb as $$ + with data as ( + select t.stripe_customer_id, + t.stripe_subscription_id, + t.ownerid::text, + t.service, + t.service_id, + t.plan_user_count, + t.plan_provider, + t.plan_auto_activate, + t.plan_activated_users, + t.plan, + t.email, + t.free, + t.did_trial, + t.invoice_details, + t.yaml, + t.student, + t.student_created_at, + t.student_updated_at, + b.username as bot_username, + get_users(t.admins) as admins, + get_repos_activated($1::int) as repos_activated + from owners t + LEFT JOIN owners b ON (b.ownerid = t.bot) + where t.ownerid = $1 + limit 1 + ) select to_jsonb(data) from data limit 1; + $$ language sql stable strict; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v451.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v451.py new file mode 100644 index 0000000000..0af6041576 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v451.py @@ -0,0 +1,43 @@ +# 4.5.1 +def run_sql(schema_editor): + schema_editor.execute( + """ + -- create enums used by commit_notifications table + create type notifications as enum('comment', 'gitter', 'hipchat', 'irc', 'slack', 'status_changes', 'status_patch', 'status_project', 'webhook', 'checks_patch', 'checks_project', 'checks_changes'); + create type decorations as enum('standard', 'upgrade'); + create type commit_notification_state as enum('pending', 'success', 'error'); + + -- Here we're commenting out all plan related migrations below because they break on enterprise + -- these migrations have been run already for production, but can break some production + -- deployments. Specifically the setting of the plan column to a new default causes problems with + -- web's ability to migrate effectively in some scenarios. + + -- If you're starting from scratch in dev, you will need to run the below migrations manually, + -- or comment out these migrations before starting up codecov.io for the first time. + + -- This isn't ideal, and will hopefully be addressed when we move all migrations to Django. + + -- Transaction friendly enum column upates. See: https://stackoverflow.com/questions/1771543/adding-a-new-value-to-an-existing-enum-type#7834949 + -- NOTE: we will not change the plan default yet + + -- first remove the default from plan column otherwise we'll get an error below with trying to cast the default + -- alter table owners alter column plan drop default; + + -- rename the old enum + -- alter type plans rename to plans__; + + -- create the new enum adding users-pr-inappm and users-pr-inappy plans + -- create type plans as enum('5m', '5y', '25m', '25y', '50m', '50y', '100m', '100y', '250m', '250y', '500m', '500y', '1000m', '1000y', '1m', '1y', + -- 'v4-10m', 'v4-10y', 'v4-20m', 'v4-20y', 'v4-50m', 'v4-50y', 'v4-125m', 'v4-125y', 'v4-300m', 'v4-300y', + -- 'users', 'users-inappm', 'users-inappy', 'users-pr-inappm', 'users-pr-inappy', 'users-free'); + + -- use the new enum + -- alter table owners alter column plan type plans using plan::text::plans; + + + --ALTER TABLE ONLY owners ALTER COLUMN plan SET DEFAULT 'users-free'; + + -- drop the old enum + -- drop type plans__; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v4510.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v4510.py new file mode 100644 index 0000000000..5659ee811a --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v4510.py @@ -0,0 +1,7 @@ +# v4.5.10 +def run_sql(schema_editor): + schema_editor.execute( + """ + alter table owners add column root_parent_service_id text; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v452.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v452.py new file mode 100644 index 0000000000..eec8e575d6 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v452.py @@ -0,0 +1,14 @@ +# v4.5.2 +def run_sql(schema_editor): + schema_editor.execute( + """ + ALTER TABLE commits ADD COLUMN id bigint; + COMMIT; + -- EOF + CREATE SEQUENCE commits_id_seq OWNED BY commits.id; + COMMIT; + -- EOF + ALTER TABLE commits ALTER COLUMN id SET DEFAULT nextval('commits_id_seq'); + COMMIT; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v453.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v453.py new file mode 100644 index 0000000000..21575d0aef --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v453.py @@ -0,0 +1,20 @@ +# v4.5.3 +def run_sql(schema_editor): + schema_editor.execute( + """ + CREATE UNIQUE INDEX IF NOT EXISTS commits_pkey on commits (id); + + create table commit_notifications( + id bigserial primary key, + commit_id bigint references commits(id) on delete cascade not null, + notification_type notifications not null, + decoration_type decorations, + created_at timestamp, + updated_at timestamp, + state commit_notification_state, + CONSTRAINT commit_notifications_commit_id_notification_type UNIQUE(commit_id, notification_type) + ); + + create index commit_notifications_commit_id on commit_notifications (commit_id); + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v454.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v454.py new file mode 100644 index 0000000000..cb9bb707b3 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v454.py @@ -0,0 +1,148 @@ +# v4.5.4 +def run_sql(schema_editor): + schema_editor.execute( + """ + ALTER TABLE commit_notifications drop CONSTRAINT IF EXISTS commit_notifications_commit_id_fkey; + ALTER TABLE commits drop CONSTRAINT IF EXISTS commits_pkey; + CREATE UNIQUE INDEX IF NOT EXISTS commits_pkey on commits (id); + ALTER TABLE commits ADD PRIMARY KEY USING INDEX commits_pkey; + ALTER TABLE commit_notifications ADD CONSTRAINT commit_notifications_commit_id_fkey FOREIGN KEY (commit_id) REFERENCES commits(id) ON DELETE CASCADE + -- EOF + BEGIN; + -- + -- Create model CommitReport + -- + CREATE TABLE "reports_commitreport" ( + "id" bigserial NOT NULL PRIMARY KEY, + "external_id" uuid NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "commit_id" bigint NOT NULL + ); + -- + -- Create model ReportDetails + -- + CREATE TABLE "reports_reportdetails" ( + "id" bigserial NOT NULL PRIMARY KEY, + "external_id" uuid NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "files_array" jsonb[] NOT NULL, + "report_id" bigint NOT NULL UNIQUE + ); + -- + -- Create model ReportLevelTotals + -- + CREATE TABLE "reports_reportleveltotals" ( + "id" bigserial NOT NULL PRIMARY KEY, + "external_id" uuid NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "branches" integer NOT NULL, + "coverage" numeric(7, 2) NOT NULL, + "hits" integer NOT NULL, + "lines" integer NOT NULL, + "methods" integer NOT NULL, + "misses" integer NOT NULL, + "partials" integer NOT NULL, + "files" integer NOT NULL, + "report_id" bigint NOT NULL UNIQUE + ); + -- + -- Create model ReportSession + -- + CREATE TABLE "reports_upload" ( + "id" bigserial NOT NULL PRIMARY KEY, + "external_id" uuid NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "build_code" text NULL, + "build_url" text NULL, + "env" jsonb NULL, + "job_code" text NULL, + "name" varchar(100) NULL, + "provider" varchar(50) NULL, + "state" varchar(100) NOT NULL, + "storage_path" text NOT NULL, + "order_number" integer NULL + ); + -- + -- Create model ReportSessionError + -- + CREATE TABLE "reports_uploaderror" ( + "id" bigserial NOT NULL PRIMARY KEY, + "external_id" uuid NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "error_code" varchar(100) NOT NULL, + "error_params" jsonb NOT NULL, + "report_session_id" bigint NOT NULL + ); + -- + -- Create model ReportSessionFlagMembership + -- + CREATE TABLE "reports_uploadflagmembership" ( + "id" bigserial NOT NULL PRIMARY KEY + ); + -- + -- Create model RepositoryFlag + -- + CREATE TABLE "reports_repositoryflag" ( + "id" bigserial NOT NULL PRIMARY KEY, + "external_id" uuid NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "flag_name" varchar(255) NOT NULL, + "repository_id" integer NOT NULL + ); + -- + -- Create model SessionLevelTotals + -- + CREATE TABLE "reports_sessionleveltotals" ( + "id" bigserial NOT NULL PRIMARY KEY, + "external_id" uuid NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "branches" integer NOT NULL, + "coverage" numeric(7, 2) NOT NULL, + "hits" integer NOT NULL, + "lines" integer NOT NULL, + "methods" integer NOT NULL, + "misses" integer NOT NULL, + "partials" integer NOT NULL, + "files" integer NOT NULL, + "report_session_id" bigint NOT NULL UNIQUE + ); + -- + -- Add field flag to reportsessionflagmembership + -- + ALTER TABLE "reports_uploadflagmembership" ADD COLUMN "flag_id" bigint NOT NULL; + -- + -- Add field report_session to reportsessionflagmembership + -- + ALTER TABLE "reports_uploadflagmembership" ADD COLUMN "report_session_id" bigint NOT NULL; + -- + -- Add field flags to reportsession + -- + -- + -- Add field report to reportsession + -- + ALTER TABLE "reports_upload" ADD COLUMN "report_id" bigint NOT NULL; + ALTER TABLE "reports_commitreport" ADD CONSTRAINT "reports_commitreport_commit_id_06d0bd39_fk_commits_id" FOREIGN KEY ("commit_id") REFERENCES "commits" ("id") DEFERRABLE INITIALLY DEFERRED; + CREATE INDEX "reports_commitreport_commit_id_06d0bd39" ON "reports_commitreport" ("commit_id"); + ALTER TABLE "reports_reportdetails" ADD CONSTRAINT "reports_reportdetail_report_id_4681bfd3_fk_reports_c" FOREIGN KEY ("report_id") REFERENCES "reports_commitreport" ("id") DEFERRABLE INITIALLY DEFERRED; + ALTER TABLE "reports_reportleveltotals" ADD CONSTRAINT "reports_reportlevelt_report_id_b690dffa_fk_reports_c" FOREIGN KEY ("report_id") REFERENCES "reports_commitreport" ("id") DEFERRABLE INITIALLY DEFERRED; + ALTER TABLE "reports_uploaderror" ADD CONSTRAINT "reports_reportsessio_report_session_id_bb6563f1_fk_reports_r" FOREIGN KEY ("report_session_id") REFERENCES "reports_upload" ("id") DEFERRABLE INITIALLY DEFERRED; + CREATE INDEX "reports_uploaderror_report_session_id_bb6563f1" ON "reports_uploaderror" ("report_session_id"); + ALTER TABLE "reports_repositoryflag" ADD CONSTRAINT "reports_repositoryflag_repository_id_9b64b64c_fk_repos_repoid" FOREIGN KEY ("repository_id") REFERENCES "repos" ("repoid") DEFERRABLE INITIALLY DEFERRED; + CREATE INDEX "reports_repositoryflag_repository_id_9b64b64c" ON "reports_repositoryflag" ("repository_id"); + ALTER TABLE "reports_sessionleveltotals" ADD CONSTRAINT "reports_sessionlevel_report_session_id_e2cd6669_fk_reports_r" FOREIGN KEY ("report_session_id") REFERENCES "reports_upload" ("id") DEFERRABLE INITIALLY DEFERRED; + CREATE INDEX "reports_uploadflagmembership_flag_id_59edee69" ON "reports_uploadflagmembership" ("flag_id"); + ALTER TABLE "reports_uploadflagmembership" ADD CONSTRAINT "reports_reportsessio_flag_id_59edee69_fk_reports_r" FOREIGN KEY ("flag_id") REFERENCES "reports_repositoryflag" ("id") DEFERRABLE INITIALLY DEFERRED; + CREATE INDEX "reports_uploadflagmembership_report_session_id_7d7f9546" ON "reports_uploadflagmembership" ("report_session_id"); + ALTER TABLE "reports_uploadflagmembership" ADD CONSTRAINT "reports_reportsessio_report_session_id_7d7f9546_fk_reports_r" FOREIGN KEY ("report_session_id") REFERENCES "reports_upload" ("id") DEFERRABLE INITIALLY DEFERRED; + CREATE INDEX "reports_upload_report_id_f6b4ffae" ON "reports_upload" ("report_id"); + ALTER TABLE "reports_upload" ADD CONSTRAINT "reports_reportsessio_report_id_f6b4ffae_fk_reports_c" FOREIGN KEY ("report_id") REFERENCES "reports_commitreport" ("id") DEFERRABLE INITIALLY DEFERRED; + COMMIT; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v455.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v455.py new file mode 100644 index 0000000000..dc5c815898 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v455.py @@ -0,0 +1,14 @@ +# v4.5.5 +def run_sql(schema_editor): + schema_editor.execute( + """ + ALTER TABLE "reports_uploaderror" RENAME COLUMN "report_session_id" TO "upload_id"; + ALTER TABLE "reports_uploadflagmembership" RENAME COLUMN "report_session_id" TO "upload_id"; + ALTER TABLE "reports_sessionleveltotals" RENAME COLUMN "report_session_id" TO "upload_id"; + + ALTER TABLE "reports_upload" ADD COLUMN "upload_extras" jsonb NOT NULL; + ALTER TABLE "reports_upload" ADD COLUMN "upload_type" varchar(100) NOT NULL; + + ALTER TABLE "reports_sessionleveltotals" RENAME TO "reports_uploadleveltotals"; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v461.py b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v461.py new file mode 100644 index 0000000000..a78b089085 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/migrations/legacy_sql/upgrades/v461.py @@ -0,0 +1,12 @@ +# v4.6.1 +def run_sql(schema_editor): + schema_editor.execute( + """ + ALTER TABLE reports_uploadleveltotals ALTER COLUMN coverage DROP NOT NULL; + ALTER TABLE reports_reportleveltotals ALTER COLUMN coverage DROP NOT NULL; + + ALTER TABLE owners ALTER COLUMN student SET DEFAULT FALSE; + + UPDATE owners SET student=false WHERE student is NULL; + """ + ) diff --git a/libs/shared/shared/django_apps/legacy_migrations/models.py b/libs/shared/shared/django_apps/legacy_migrations/models.py new file mode 100644 index 0000000000..25ce6d4438 --- /dev/null +++ b/libs/shared/shared/django_apps/legacy_migrations/models.py @@ -0,0 +1,30 @@ +from django.db import models +from django_prometheus.models import ExportModelOperationsMixin + +from shared.django_apps.codecov_auth.models import Owner + +# Added to avoid 'doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS' error\ +# Needs to be called the same as the API app +LEGACY_MIGRATIONS_APP_LABEL = "legacy_migrations" + + +# Create your models here. +class YamlHistory( + ExportModelOperationsMixin("legacy_migrations.yaml_history"), models.Model +): + id = models.AutoField(primary_key=True) + ownerid = models.ForeignKey( + Owner, on_delete=models.CASCADE, related_name="ownerids", db_column="ownerid" + ) + author = models.ForeignKey( + Owner, on_delete=models.CASCADE, related_name="authors", db_column="author" + ) + timestamp = models.DateTimeField() + message = models.TextField(blank=True, null=True) + source = models.TextField() + diff = models.TextField(null=True) + + class Meta: + db_table = "yaml_history" + app_label = LEGACY_MIGRATIONS_APP_LABEL + indexes = [models.Index(fields=["ownerid", "timestamp"])] diff --git a/libs/shared/shared/django_apps/manage.py b/libs/shared/shared/django_apps/manage.py new file mode 100755 index 0000000000..9d182095be --- /dev/null +++ b/libs/shared/shared/django_apps/manage.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" + +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dummy_settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/libs/shared/shared/django_apps/migration_utils.py b/libs/shared/shared/django_apps/migration_utils.py new file mode 100644 index 0000000000..5437b11620 --- /dev/null +++ b/libs/shared/shared/django_apps/migration_utils.py @@ -0,0 +1,169 @@ +from django.conf import settings +from django.db import migrations + +""" +These classes can be used to skip altering DB state while maintaining the state of migrations. +To use them you should manually replace the migration step in the migration file with its +corresponding "Risky" migration step. +Not all migration steps (such as AddField) are represented here because they cannot safely +exist in code while not being applied in the DB. +""" + + +class RiskyAddField(migrations.AddField): + """ + Consult https://www.notion.so/sentry/Database-Tips-and-Tricks-76df725ded264b2a8154c960d7ef3869?pvs=4 + for how to risky add field. + """ + + def database_forwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_forwards(app_label, schema_editor, from_state, to_state) + + def database_backwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_backwards(app_label, schema_editor, from_state, to_state) + + +class RiskyAlterField(migrations.AlterField): + def database_forwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_forwards(app_label, schema_editor, from_state, to_state) + + def database_backwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_backwards(app_label, schema_editor, from_state, to_state) + + +class RiskyRemoveField(migrations.RemoveField): + def database_forwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_forwards(app_label, schema_editor, from_state, to_state) + + def database_backwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_backwards(app_label, schema_editor, from_state, to_state) + + +class RiskyAlterUniqueTogether(migrations.AlterUniqueTogether): + def database_forwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_forwards(app_label, schema_editor, from_state, to_state) + + def database_backwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_backwards(app_label, schema_editor, from_state, to_state) + + +class RiskyAlterIndexTogether(migrations.AlterIndexTogether): + def database_forwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_forwards(app_label, schema_editor, from_state, to_state) + + def database_backwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_backwards(app_label, schema_editor, from_state, to_state) + + +class RiskyAddIndex(migrations.AddIndex): + def database_forwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_forwards(app_label, schema_editor, from_state, to_state) + + def database_backwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_backwards(app_label, schema_editor, from_state, to_state) + + +class RiskyRemoveIndex(migrations.RemoveIndex): + def database_forwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_forwards(app_label, schema_editor, from_state, to_state) + + def database_backwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_backwards(app_label, schema_editor, from_state, to_state) + + +class RiskyAddConstraint(migrations.AddConstraint): + def database_forwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_forwards(app_label, schema_editor, from_state, to_state) + + def database_backwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_backwards(app_label, schema_editor, from_state, to_state) + + +class RiskyRemoveConstraint(migrations.RemoveConstraint): + def database_forwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_forwards(app_label, schema_editor, from_state, to_state) + + def database_backwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_backwards(app_label, schema_editor, from_state, to_state) + + +class RiskyRunSQL(migrations.RunSQL): + def database_forwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_forwards(app_label, schema_editor, from_state, to_state) + + def database_backwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_backwards(app_label, schema_editor, from_state, to_state) + + +class RiskyRunPython(migrations.RunPython): + def database_forwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_forwards(app_label, schema_editor, from_state, to_state) + + def database_backwards(self, app_label, schema_editor, from_state, to_state): + if settings.SKIP_RISKY_MIGRATION_STEPS: + return + + super().database_backwards(app_label, schema_editor, from_state, to_state) diff --git a/libs/shared/shared/django_apps/pg_telemetry/__init__.py b/libs/shared/shared/django_apps/pg_telemetry/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/pg_telemetry/apps.py b/libs/shared/shared/django_apps/pg_telemetry/apps.py new file mode 100644 index 0000000000..cd2dac22fa --- /dev/null +++ b/libs/shared/shared/django_apps/pg_telemetry/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class PgTelemetryConfig(AppConfig): + name = "shared.django_apps.pg_telemetry" diff --git a/libs/shared/shared/django_apps/pg_telemetry/migrations/0001_initial.py b/libs/shared/shared/django_apps/pg_telemetry/migrations/0001_initial.py new file mode 100644 index 0000000000..f8fdba0300 --- /dev/null +++ b/libs/shared/shared/django_apps/pg_telemetry/migrations/0001_initial.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.6 on 2023-11-01 21:00 +# Modified by hand to suit timeseries data + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="SimpleMetric", + fields=[ + ("timestamp", models.DateTimeField(primary_key=True, serialize=False)), + ("repo_id", models.BigIntegerField(null=True)), + ("owner_id", models.BigIntegerField(null=True)), + ("commit_id", models.BigIntegerField(null=True)), + ("name", models.TextField()), + ("value", models.FloatField()), + ], + options={ + "db_table": "telemetry_simple", + "abstract": False, + }, + ), + # Django wants us to have a primary key. Closest we have is `timestamp`, + # but it isn't necessarily unique, so we drop the pkey constraint. + migrations.RunSQL( + "ALTER TABLE telemetry_simple DROP CONSTRAINT telemetry_simple_pkey;", + reverse_sql="", + ), + migrations.AddIndex( + model_name="SimpleMetric", + index=models.Index( + fields=[ + "name", + "timestamp", + "repo_id", + "owner_id", + "commit_id", + ], + name="telemetry_s_name_23f8a7_idx", + ), + ), + ] + + if hasattr(settings, "TEST") and settings.TEST: + # Skip steps that complicate tests + operations = [operations[0], operations[2]] diff --git a/libs/shared/shared/django_apps/pg_telemetry/migrations/__init__.py b/libs/shared/shared/django_apps/pg_telemetry/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/pg_telemetry/models.py b/libs/shared/shared/django_apps/pg_telemetry/models.py new file mode 100644 index 0000000000..fb04532206 --- /dev/null +++ b/libs/shared/shared/django_apps/pg_telemetry/models.py @@ -0,0 +1,58 @@ +from django.conf import settings +from django.db import models + + +class BaseModel(models.Model): + """ + Base model for timeseries metrics. It provides a timestamp field which + represents the time that the data sample was captured at and a few metadata + fields that we can filter or group by to investigate issues or identify + trends. + + This is the Postgres version. After data flows through both Postgres and + Timescale for a time, we'll pick one. + """ + + class Meta: + abstract = True + + timestamp = models.DateTimeField(null=False, primary_key=True) + + repo_id = models.BigIntegerField(null=True) + owner_id = models.BigIntegerField(null=True) + commit_id = models.BigIntegerField(null=True) + + def save(self, *args, **kwargs): + if settings.TELEMETRY_VANILLA_DB: + kwargs["using"] = settings.TELEMETRY_VANILLA_DB + super().save(*args, **kwargs) + + +class SimpleMetric(BaseModel): + """ + Model for the `telemetry_simple` table which houses many simple metrics. + Rather than create a bespoke model, table, and db migration for each timer + or quantity we want to measure, we put it in `telemetry_simple`. + + Examples could include `list_repos_duration_seconds` or `uploads_processed` + + This is the Postgres version. After data flows through both Postgres and + Timescale for a time, we'll pick one. + """ + + class Meta(BaseModel.Meta): + db_table = "telemetry_simple" + indexes = [ + models.Index( + fields=[ + "name", + "timestamp", + "repo_id", + "owner_id", + "commit_id", + ], + ), + ] + + name = models.TextField(null=False) + value = models.FloatField(null=False) diff --git a/libs/shared/shared/django_apps/profiling/__init__.py b/libs/shared/shared/django_apps/profiling/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/profiling/migrations/0001_initial.py b/libs/shared/shared/django_apps/profiling/migrations/0001_initial.py new file mode 100644 index 0000000000..d13715698c --- /dev/null +++ b/libs/shared/shared/django_apps/profiling/migrations/0001_initial.py @@ -0,0 +1,58 @@ +# Generated by Django 3.1.6 on 2021-07-29 21:15 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [("core", "0004_pull_user_provided_base_sha")] + + operations = [ + migrations.CreateModel( + name="ProfilingCommit", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("last_joined_uploads_at", models.DateTimeField(null=True)), + ("last_summarized_at", models.DateTimeField(null=True)), + ("joined_location", models.TextField(null=True)), + ("summarized_location", models.TextField(null=True)), + ("version_identifier", models.TextField()), + ( + "repository", + models.ForeignKey( + db_column="repoid", + on_delete=django.db.models.deletion.CASCADE, + related_name="profilings", + to="core.repository", + ), + ), + ], + options={"abstract": False}, + ), + migrations.CreateModel( + name="ProfilingUpload", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("raw_upload_location", models.TextField()), + ( + "profiling_commit", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="uploads", + to="profiling.profilingcommit", + ), + ), + ], + options={"abstract": False}, + ), + ] diff --git a/libs/shared/shared/django_apps/profiling/migrations/0002_auto_20210817_2007.py b/libs/shared/shared/django_apps/profiling/migrations/0002_auto_20210817_2007.py new file mode 100644 index 0000000000..7ce87e7613 --- /dev/null +++ b/libs/shared/shared/django_apps/profiling/migrations/0002_auto_20210817_2007.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1.6 on 2021-08-17 20:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("profiling", "0001_initial")] + + operations = [ + migrations.AddField( + model_name="profilingupload", + name="normalized_at", + field=models.DateTimeField(null=True), + ), + migrations.AddField( + model_name="profilingupload", + name="normalized_location", + field=models.TextField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/profiling/migrations/0003_profilingcommit_commit_sha.py b/libs/shared/shared/django_apps/profiling/migrations/0003_profilingcommit_commit_sha.py new file mode 100644 index 0000000000..2efb3ce613 --- /dev/null +++ b/libs/shared/shared/django_apps/profiling/migrations/0003_profilingcommit_commit_sha.py @@ -0,0 +1,15 @@ +# Generated by Django 3.1.13 on 2021-09-27 12:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("profiling", "0002_auto_20210817_2007")] + + operations = [ + migrations.AddField( + model_name="profilingcommit", + name="commit_sha", + field=models.TextField(null=True), + ) + ] diff --git a/libs/shared/shared/django_apps/profiling/migrations/0004_auto_20211011_2047.py b/libs/shared/shared/django_apps/profiling/migrations/0004_auto_20211011_2047.py new file mode 100644 index 0000000000..97b82acc2b --- /dev/null +++ b/libs/shared/shared/django_apps/profiling/migrations/0004_auto_20211011_2047.py @@ -0,0 +1,15 @@ +# Generated by Django 3.1.13 on 2021-10-11 20:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("profiling", "0003_profilingcommit_commit_sha")] + + operations = [ + migrations.AddField( + model_name="profilingcommit", + name="environment", + field=models.CharField(max_length=100, null=True), + ) + ] diff --git a/libs/shared/shared/django_apps/profiling/migrations/0005_auto_20211018_2158.py b/libs/shared/shared/django_apps/profiling/migrations/0005_auto_20211018_2158.py new file mode 100644 index 0000000000..4beea15ed4 --- /dev/null +++ b/libs/shared/shared/django_apps/profiling/migrations/0005_auto_20211018_2158.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.13 on 2021-10-18 21:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("profiling", "0004_auto_20211011_2047")] + + operations = [ + migrations.AddField( + model_name="profilingcommit", name="code", field=models.TextField(null=True) + ), + migrations.AddConstraint( + model_name="profilingcommit", + constraint=models.UniqueConstraint( + fields=("repository", "code"), name="uniquerepocode" + ), + ), + ] diff --git a/libs/shared/shared/django_apps/profiling/migrations/0006_rm_everything.py b/libs/shared/shared/django_apps/profiling/migrations/0006_rm_everything.py new file mode 100644 index 0000000000..d4b72534fd --- /dev/null +++ b/libs/shared/shared/django_apps/profiling/migrations/0006_rm_everything.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2025-03-31 13:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("profiling", "0005_auto_20211018_2158"), + ] + + operations = [ + migrations.DeleteModel( + name="ProfilingUpload", + ), + migrations.DeleteModel( + name="ProfilingCommit", + ), + ] diff --git a/libs/shared/shared/django_apps/profiling/migrations/__init__.py b/libs/shared/shared/django_apps/profiling/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/profiling/models.py b/libs/shared/shared/django_apps/profiling/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/reports/__init__.py b/libs/shared/shared/django_apps/reports/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/reports/managers.py b/libs/shared/shared/django_apps/reports/managers.py new file mode 100644 index 0000000000..0db5e5097e --- /dev/null +++ b/libs/shared/shared/django_apps/reports/managers.py @@ -0,0 +1,17 @@ +from django.db.models import Manager, Q, QuerySet + + +class CommitReportQuerySet(QuerySet): + def coverage_reports(self): + """ + Filters queryset such that only coverage reports are included. + """ + return self.filter(Q(report_type=None) | Q(report_type="coverage")) + + +class CommitReportManager(Manager): + def get_queryset(self): + return CommitReportQuerySet(self.model, using=self._db) + + def coverage_reports(self, *args, **kwargs): + return self.get_queryset().coverage_reports(*args, **kwargs) diff --git a/libs/shared/shared/django_apps/reports/migrations/0001_initial.py b/libs/shared/shared/django_apps/reports/migrations/0001_initial.py new file mode 100644 index 0000000000..b9dcf7b87c --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0001_initial.py @@ -0,0 +1,208 @@ +# Generated by Django 3.1.6 on 2021-04-08 19:33 + +import uuid + +import django.contrib.postgres.fields +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [("core", "0001_initial")] + + operations = [ + migrations.CreateModel( + name="CommitReport", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "commit", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="reports", + to="core.commit", + ), + ), + ], + options={"abstract": False}, + ), + migrations.CreateModel( + name="ReportSession", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("build_code", models.TextField(null=True)), + ("build_url", models.TextField(null=True)), + ("env", models.JSONField(null=True)), + ("job_code", models.TextField(null=True)), + ("name", models.CharField(max_length=100, null=True)), + ("provider", models.CharField(max_length=50, null=True)), + ("state", models.CharField(max_length=100)), + ("storage_path", models.TextField()), + ("order_number", models.IntegerField(null=True)), + ("upload_type", models.CharField(max_length=100, default="uploaded")), + ("upload_extras", models.JSONField(default=dict)), + ], + options={"db_table": "reports_upload"}, + ), + migrations.CreateModel( + name="SessionLevelTotals", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("branches", models.IntegerField()), + ("coverage", models.DecimalField(decimal_places=2, max_digits=7)), + ("hits", models.IntegerField()), + ("lines", models.IntegerField()), + ("methods", models.IntegerField()), + ("misses", models.IntegerField()), + ("partials", models.IntegerField()), + ("files", models.IntegerField()), + ( + "report_session", + models.OneToOneField( + db_column="upload_id", + on_delete=django.db.models.deletion.CASCADE, + to="reports.reportsession", + ), + ), + ], + options={"db_table": "reports_uploadleveltotals"}, + ), + migrations.CreateModel( + name="RepositoryFlag", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("flag_name", models.CharField(max_length=255)), + ( + "repository", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="flags", + to="core.repository", + ), + ), + ], + options={"abstract": False}, + ), + migrations.CreateModel( + name="ReportSessionFlagMembership", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ( + "flag", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="reports.repositoryflag", + ), + ), + ( + "report_session", + models.ForeignKey( + db_column="upload_id", + on_delete=django.db.models.deletion.CASCADE, + to="reports.reportsession", + ), + ), + ], + options={"db_table": "reports_uploadflagmembership"}, + ), + migrations.CreateModel( + name="ReportSessionError", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("error_code", models.CharField(max_length=100)), + ("error_params", models.JSONField(default=dict)), + ( + "report_session", + models.ForeignKey( + db_column="upload_id", + on_delete=django.db.models.deletion.CASCADE, + related_name="errors", + to="reports.reportsession", + ), + ), + ], + options={"db_table": "reports_uploaderror"}, + ), + migrations.AddField( + model_name="reportsession", + name="flags", + field=models.ManyToManyField( + through="reports.ReportSessionFlagMembership", + to="reports.RepositoryFlag", + ), + ), + migrations.AddField( + model_name="reportsession", + name="report", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="sessions", + to="reports.commitreport", + ), + ), + migrations.CreateModel( + name="ReportLevelTotals", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("branches", models.IntegerField()), + ("coverage", models.DecimalField(decimal_places=2, max_digits=7)), + ("hits", models.IntegerField()), + ("lines", models.IntegerField()), + ("methods", models.IntegerField()), + ("misses", models.IntegerField()), + ("partials", models.IntegerField()), + ("files", models.IntegerField()), + ( + "report", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to="reports.commitreport", + ), + ), + ], + options={"abstract": False}, + ), + migrations.CreateModel( + name="ReportDetails", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "files_array", + django.contrib.postgres.fields.ArrayField( + base_field=models.JSONField(), size=None + ), + ), + ( + "report", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to="reports.commitreport", + ), + ), + ], + options={"abstract": False}, + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0002_auto_20211006_2211.py b/libs/shared/shared/django_apps/reports/migrations/0002_auto_20211006_2211.py new file mode 100644 index 0000000000..7253189b39 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0002_auto_20211006_2211.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.13 on 2021-10-06 22:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [("reports", "0001_initial")] + + operations = [ + migrations.RenameModel(old_name="ReportSessionError", new_name="UploadError"), + migrations.RenameModel( + old_name="ReportSessionFlagMembership", new_name="UploadFlagMembership" + ), + migrations.RenameModel( + old_name="SessionLevelTotals", new_name="UploadLevelTotals" + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0003_auto_20211118_1150.py b/libs/shared/shared/django_apps/reports/migrations/0003_auto_20211118_1150.py new file mode 100644 index 0000000000..6befe13a8c --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0003_auto_20211118_1150.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1.13 on 2021-11-18 11:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("reports", "0002_auto_20211006_2211")] + + operations = [ + migrations.AddField( + model_name="reportsession", + name="state_id", + field=models.IntegerField( + choices=[(1, "uploaded"), (2, "processed"), (3, "error")], null=True + ), + ), + migrations.AddField( + model_name="reportsession", + name="upload_type_id", + field=models.IntegerField( + choices=[(1, "uploaded"), (2, "carryforwarded")], null=True + ), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0004_commitreport_code.py b/libs/shared/shared/django_apps/reports/migrations/0004_commitreport_code.py new file mode 100644 index 0000000000..4aece450bd --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0004_commitreport_code.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.12 on 2022-09-22 15:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0003_auto_20211118_1150"), + ] + + operations = [ + migrations.AddField( + model_name="commitreport", + name="code", + field=models.CharField(max_length=100, null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0005_auto_20221114_1428.py b/libs/shared/shared/django_apps/reports/migrations/0005_auto_20221114_1428.py new file mode 100644 index 0000000000..e5be5b19b3 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0005_auto_20221114_1428.py @@ -0,0 +1,42 @@ +# Generated by Django 3.2.12 on 2022-11-14 14:28 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0004_commitreport_code"), + ] + + operations = [ + migrations.CreateModel( + name="ReportResults", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "state", + models.TextField( + choices=[("created", "Created"), ("ready", "Ready")], null=True + ), + ), + ("completed_at", models.DateTimeField(null=True)), + ("result", models.JSONField(default=dict)), + ( + "report", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to="reports.commitreport", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0006_auto_20221212_1111.py b/libs/shared/shared/django_apps/reports/migrations/0006_auto_20221212_1111.py new file mode 100644 index 0000000000..60ee202bb2 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0006_auto_20221212_1111.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.12 on 2022-12-12 11:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("reports", "0005_auto_20221114_1428"), + ] + + operations = [ + migrations.RunSQL( + 'CREATE UNIQUE INDEX CONCURRENTLY unique_commit_id_code_idx ON reports_commitreport ("commit_id", "code");' + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0007_auto_20230220_1245.py b/libs/shared/shared/django_apps/reports/migrations/0007_auto_20230220_1245.py new file mode 100644 index 0000000000..dfec6f56db --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0007_auto_20230220_1245.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.12 on 2023-02-20 12:45 + +from django.db import migrations + +from shared.django_apps.migration_utils import RiskyRunSQL + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0006_auto_20221212_1111"), + ] + + operations = [ + RiskyRunSQL( + 'ALTER TABLE "reports_commitreport" ADD CONSTRAINT "unique_commit_id_code" UNIQUE USING INDEX unique_commit_id_code_idx;' + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0008_auto_20230228_1059.py b/libs/shared/shared/django_apps/reports/migrations/0008_auto_20230228_1059.py new file mode 100644 index 0000000000..fa7af32a3b --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0008_auto_20230228_1059.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.12 on 2023-02-28 10:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0007_auto_20230220_1245"), + ] + + operations = [ + migrations.AlterField( + model_name="reportresults", + name="state", + field=models.TextField( + choices=[ + ("pending", "Pending"), + ("completed", "Completed"), + ("error", "Error"), + ], + null=True, + ), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0009_auto_20230223_1624.py b/libs/shared/shared/django_apps/reports/migrations/0009_auto_20230223_1624.py new file mode 100644 index 0000000000..347fca49c9 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0009_auto_20230223_1624.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.12 on 2023-02-23 16:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0008_auto_20230228_1059"), + ] + + operations = [ + migrations.AddField( + model_name="repositoryflag", + name="deleted", + field=models.BooleanField(null=True), + ), + migrations.AlterField( + model_name="reportsession", + name="state_id", + field=models.IntegerField( + choices=[ + (1, "UPLOADED"), + (2, "PROCESSED"), + (3, "ERROR"), + (4, "FULLY_OVERWRITTEN"), + (5, "PARTIALLY_OVERWRITTEN"), + ], + null=True, + ), + ), + migrations.AlterField( + model_name="reportsession", + name="upload_type_id", + field=models.IntegerField( + choices=[(1, "UPLOADED"), (2, "CARRIEDFORWARD")], null=True + ), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0010_alter_reportdetails_files_array_and_more.py b/libs/shared/shared/django_apps/reports/migrations/0010_alter_reportdetails_files_array_and_more.py new file mode 100644 index 0000000000..187b3de92e --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0010_alter_reportdetails_files_array_and_more.py @@ -0,0 +1,60 @@ +# Generated by Django 4.1.7 on 2023-05-29 14:15 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + # Generated SQL + # BEGIN; + # -- + # -- Alter field files_array on reportdetails + # -- + # -- (no-op) + # -- + # -- Rename field files_array on reportdetails to _files_array + # -- + # -- (no-op) + # -- + # -- Add field _files_array_storage_path to reportdetails + # -- + # ALTER TABLE "reports_reportdetails" ADD COLUMN "files_array_storage_path" varchar(200) NULL; + # -- + # -- Alter field _files_array on reportdetails + # -- + # ALTER TABLE "reports_reportdetails" ALTER COLUMN "files_array" DROP NOT NULL; + # COMMIT; + + dependencies = [ + ("reports", "0009_auto_20230223_1624"), + ] + + operations = [ + migrations.AlterField( + model_name="reportdetails", + name="files_array", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.JSONField(), db_column="files_array", size=None + ), + ), + migrations.RenameField( + model_name="reportdetails", + old_name="files_array", + new_name="_files_array", + ), + migrations.AddField( + model_name="reportdetails", + name="_files_array_storage_path", + field=models.URLField(db_column="files_array_storage_path", null=True), + ), + migrations.AlterField( + model_name="reportdetails", + name="_files_array", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.JSONField(), + db_column="files_array", + null=True, + size=None, + ), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0011_commitreport_report_type.py b/libs/shared/shared/django_apps/reports/migrations/0011_commitreport_report_type.py new file mode 100644 index 0000000000..f35133697b --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0011_commitreport_report_type.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.7 on 2023-12-06 13:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Add field report_type to commitreport + -- + ALTER TABLE "reports_commitreport" ADD COLUMN "report_type" varchar(100) NULL; + COMMIT; + """ + + dependencies = [ + ("reports", "0010_alter_reportdetails_files_array_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="commitreport", + name="report_type", + field=models.CharField( + choices=[ + ("coverage", "Coverage"), + ("test_results", "Test Results"), + ("bundle_analysis", "Bundle Analysis"), + ], + max_length=100, + null=True, + ), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0012_alter_repositoryflag_flag_name.py b/libs/shared/shared/django_apps/reports/migrations/0012_alter_repositoryflag_flag_name.py new file mode 100644 index 0000000000..b5a00d44b1 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0012_alter_repositoryflag_flag_name.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.7 on 2023-12-12 00:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0011_commitreport_report_type"), + ] + + operations = [ + migrations.AlterField( + model_name="repositoryflag", + name="flag_name", + field=models.CharField(max_length=1024), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0013_test_testinstance.py b/libs/shared/shared/django_apps/reports/migrations/0013_test_testinstance.py new file mode 100644 index 0000000000..a04733b0a0 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0013_test_testinstance.py @@ -0,0 +1,94 @@ +# Generated by Django 4.2.7 on 2024-01-17 20:41 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Create model Test + -- + CREATE TABLE "reports_test" ("id" text NOT NULL PRIMARY KEY, "external_id" uuid NOT NULL, "created_at" timestamp with time zone NOT NULL, "updated_at" timestamp with time zone NOT NULL, "name" text NOT NULL, "testsuite" text NOT NULL, "env" text NOT NULL, "repoid" integer NOT NULL); + -- + -- Create model TestInstance + -- + CREATE TABLE "reports_testinstance" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "external_id" uuid NOT NULL, "created_at" timestamp with time zone NOT NULL, "updated_at" timestamp with time zone NOT NULL, "duration_seconds" double precision NOT NULL, "outcome" integer NOT NULL, "failure_message" text NULL, "test_id" text NOT NULL, "upload_id" bigint NOT NULL); + ALTER TABLE "reports_test" ADD CONSTRAINT "reports_test_repoid_445c33d7_fk_repos_repoid" FOREIGN KEY ("repoid") REFERENCES "repos" ("repoid") DEFERRABLE INITIALLY DEFERRED; + CREATE INDEX "reports_test_id_5c60c58c_like" ON "reports_test" ("id" text_pattern_ops); + CREATE INDEX "reports_test_repoid_445c33d7" ON "reports_test" ("repoid"); + ALTER TABLE "reports_testinstance" ADD CONSTRAINT "reports_testinstance_test_id_9c8dd6c1_fk_reports_test_id" FOREIGN KEY ("test_id") REFERENCES "reports_test" ("id") DEFERRABLE INITIALLY DEFERRED; + ALTER TABLE "reports_testinstance" ADD CONSTRAINT "reports_testinstance_upload_id_7350520f_fk_reports_upload_id" FOREIGN KEY ("upload_id") REFERENCES "reports_upload" ("id") DEFERRABLE INITIALLY DEFERRED; + CREATE INDEX "reports_testinstance_test_id_9c8dd6c1" ON "reports_testinstance" ("test_id"); + CREATE INDEX "reports_testinstance_test_id_9c8dd6c1_like" ON "reports_testinstance" ("test_id" text_pattern_ops); + CREATE INDEX "reports_testinstance_upload_id_7350520f" ON "reports_testinstance" ("upload_id"); + COMMIT; + """ + + dependencies = [ + ("core", "0045_repository_languages_last_updated"), + ("reports", "0012_alter_repositoryflag_flag_name"), + ] + + operations = [ + migrations.CreateModel( + name="Test", + fields=[ + ("id", models.TextField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("name", models.TextField()), + ("testsuite", models.TextField()), + ("env", models.TextField()), + ( + "repository", + models.ForeignKey( + db_column="repoid", + on_delete=django.db.models.deletion.CASCADE, + related_name="tests", + to="core.repository", + ), + ), + ], + options={ + "db_table": "reports_test", + }, + ), + migrations.CreateModel( + name="TestInstance", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("duration_seconds", models.FloatField()), + ("outcome", models.IntegerField()), + ("failure_message", models.TextField(null=True)), + ( + "test", + models.ForeignKey( + db_column="test_id", + on_delete=django.db.models.deletion.CASCADE, + related_name="testinstances", + to="reports.test", + ), + ), + ( + "upload", + models.ForeignKey( + db_column="upload_id", + on_delete=django.db.models.deletion.CASCADE, + related_name="testinstances", + to="reports.reportsession", + ), + ), + ], + options={ + "db_table": "reports_testinstance", + }, + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0014_rename_env_test_flags_hash_and_more.py b/libs/shared/shared/django_apps/reports/migrations/0014_rename_env_test_flags_hash_and_more.py new file mode 100644 index 0000000000..d42a2ccdbd --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0014_rename_env_test_flags_hash_and_more.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.7 on 2024-01-24 22:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Rename field env on test to flags_hash + -- + ALTER TABLE "reports_test" RENAME COLUMN "env" TO "flags_hash"; + -- + -- Alter field outcome on testinstance + -- + ALTER TABLE "reports_testinstance" ALTER COLUMN "outcome" TYPE varchar(100) USING "outcome"::varchar(100); + -- + -- Create constraint reports_test_repoid_name_testsuite_flags_hash on model test + -- + ALTER TABLE "reports_test" ADD CONSTRAINT "reports_test_repoid_name_testsuite_flags_hash" UNIQUE ("repoid", "name", "testsuite", "flags_hash"); + COMMIT; + """ + + dependencies = [ + ("reports", "0013_test_testinstance"), + ] + + operations = [ + migrations.RenameField( + model_name="test", + old_name="env", + new_name="flags_hash", + ), + migrations.AlterField( + model_name="testinstance", + name="outcome", + field=models.CharField( + choices=[ + ("failure", "Failure"), + ("skip", "Skip"), + ("error", "Error"), + ("pass", "Pass"), + ], + max_length=100, + ), + ), + migrations.AddConstraint( + model_name="test", + constraint=models.UniqueConstraint( + fields=("repository", "name", "testsuite", "flags_hash"), + name="reports_test_repoid_name_testsuite_flags_hash", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0015_testresultreporttotals.py b/libs/shared/shared/django_apps/reports/migrations/0015_testresultreporttotals.py new file mode 100644 index 0000000000..f383d2e68b --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0015_testresultreporttotals.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.7 on 2024-02-08 21:30 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0014_rename_env_test_flags_hash_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="TestResultReportTotals", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("passed", models.IntegerField()), + ("skipped", models.IntegerField()), + ("failed", models.IntegerField()), + ( + "report", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to="reports.commitreport", + ), + ), + ], + options={ + "db_table": "reports_testresultreporttotals", + }, + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0016_testresultreporttotals_error.py b/libs/shared/shared/django_apps/reports/migrations/0016_testresultreporttotals_error.py new file mode 100644 index 0000000000..7c26f7357b --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0016_testresultreporttotals_error.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.11 on 2024-04-12 17:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0015_testresultreporttotals"), + ] + + operations = [ + migrations.AddField( + model_name="testresultreporttotals", + name="error", + field=models.CharField( + choices=[("no_success", "No Success")], max_length=100, null=True + ), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0017_testinstance_flaky_status.py b/libs/shared/shared/django_apps/reports/migrations/0017_testinstance_flaky_status.py new file mode 100644 index 0000000000..a4d401e80b --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0017_testinstance_flaky_status.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.11 on 2024-04-12 19:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0016_testresultreporttotals_error"), + ] + + operations = [ + migrations.AddField( + model_name="testinstance", + name="flaky_status", + field=models.CharField( + choices=[ + ("failed_in_default_branch", "Failed In Default Branch"), + ("consecutive_diff_outcomes", "Consecutive Diff Outcomes"), + ("unrelated_matching_failures", "Unrelated Matching Failures"), + ], + max_length=100, + null=True, + ), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0018_testinstance_branch_testinstance_commitid.py b/libs/shared/shared/django_apps/reports/migrations/0018_testinstance_branch_testinstance_commitid.py new file mode 100644 index 0000000000..3d23862159 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0018_testinstance_branch_testinstance_commitid.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.11 on 2024-04-24 18:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0017_testinstance_flaky_status"), + ] + + operations = [ + migrations.AddField( + model_name="testinstance", + name="branch", + field=models.TextField(null=True), + ), + migrations.AddField( + model_name="testinstance", + name="commitid", + field=models.TextField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0019_auto_20240424_1824.py b/libs/shared/shared/django_apps/reports/migrations/0019_auto_20240424_1824.py new file mode 100644 index 0000000000..17df50983e --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0019_auto_20240424_1824.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.11 on 2024-04-24 16:02 + +from django.db import migrations + +from shared.django_apps.migration_utils import RiskyRunPython + +""" +BEGIN; +-- +-- Add field branch to testinstance +-- +ALTER TABLE "reports_testinstance" ADD COLUMN "branch" text NULL; +-- +-- Add field commitid to testinstance +-- +ALTER TABLE "reports_testinstance" ADD COLUMN "commitid" text NULL; +COMMIT; +""" + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0018_testinstance_branch_testinstance_commitid"), + ] + + def populate_test_instances(apps, schema_editor): + print("Not running due to performance") # noqa: T201 + return + TestInstance = apps.get_model("reports", "TestInstance") + + test_instances = TestInstance.objects.select_related( + "upload__report__commit" + ).all() + for test_instance in test_instances: + test_instance.branch = test_instance.upload.report.commit.branch + test_instance.commitid = test_instance.upload.report.commit.commitid + TestInstance.objects.bulk_update(test_instances, ["branch", "commitid"]) + + operations = [RiskyRunPython(populate_test_instances)] diff --git a/libs/shared/shared/django_apps/reports/migrations/0020_alter_reportleveltotals_coverage_and_more.py b/libs/shared/shared/django_apps/reports/migrations/0020_alter_reportleveltotals_coverage_and_more.py new file mode 100644 index 0000000000..adfaae9a4c --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0020_alter_reportleveltotals_coverage_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.11 on 2024-05-21 13:57 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAlterField + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0019_auto_20240424_1824"), + ] + + operations = [ + RiskyAlterField( + model_name="reportleveltotals", + name="coverage", + field=models.DecimalField(decimal_places=5, max_digits=8), + ), + RiskyAlterField( + model_name="uploadleveltotals", + name="coverage", + field=models.DecimalField(decimal_places=5, max_digits=8), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0021_remove_testinstance_flaky_status_and_more.py b/libs/shared/shared/django_apps/reports/migrations/0021_remove_testinstance_flaky_status_and_more.py new file mode 100644 index 0000000000..bad45d616b --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0021_remove_testinstance_flaky_status_and_more.py @@ -0,0 +1,83 @@ +# Generated by Django 4.2.11 on 2024-06-12 18:34 + +import django.contrib.postgres.fields +from django.db import migrations, models + +from shared.django_apps.migration_utils import ( + RiskyAddField, + RiskyAddIndex, + RiskyRemoveField, +) + +""" +BEGIN; +-- +-- Remove field flaky_status from testinstance +-- +ALTER TABLE "reports_testinstance" DROP COLUMN "flaky_status" CASCADE; +-- +-- Add field commits_where_fail to test +-- +ALTER TABLE "reports_test" ADD COLUMN "commits_where_fail" text[] NULL; +-- +-- Add field failure_rate to test +-- +ALTER TABLE "reports_test" ADD COLUMN "failure_rate" double precision NULL; +-- +-- Add field repoid to testinstance +-- +ALTER TABLE "reports_testinstance" ADD COLUMN "repoid" integer NULL; +-- +-- Create index reports_tes_commiti_b33542_idx on field(s) commitid, repoid, branch of model testinstance +-- +CREATE INDEX "reports_tes_commiti_b33542_idx" ON "reports_testinstance" ("commitid", "repoid", "branch"); +-- +-- Create index reports_tes_repoid_9e7bb3_idx on field(s) repoid, created_at, outcome of model testinstance +-- +CREATE INDEX "reports_tes_repoid_9e7bb3_idx" ON "reports_testinstance" ("repoid", "created_at", "outcome"); +COMMIT; +""" + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0020_alter_reportleveltotals_coverage_and_more"), + ] + + operations = [ + RiskyRemoveField( + model_name="testinstance", + name="flaky_status", + ), + RiskyAddField( + model_name="test", + name="commits_where_fail", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), null=True, size=None + ), + ), + RiskyAddField( + model_name="test", + name="failure_rate", + field=models.FloatField(null=True), + ), + RiskyAddField( + model_name="testinstance", + name="repoid", + field=models.IntegerField(null=True), + ), + RiskyAddIndex( + model_name="testinstance", + index=models.Index( + fields=["commitid", "repoid", "branch"], + name="reports_tes_commiti_b33542_idx", + ), + ), + RiskyAddIndex( + model_name="testinstance", + index=models.Index( + fields=["repoid", "created_at", "outcome"], + name="reports_tes_repoid_9e7bb3_idx", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0022_reducederror_flake_testinstance_reduced_error_and_more.py b/libs/shared/shared/django_apps/reports/migrations/0022_reducederror_flake_testinstance_reduced_error_and_more.py new file mode 100644 index 0000000000..edba55a782 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0022_reducederror_flake_testinstance_reduced_error_and_more.py @@ -0,0 +1,142 @@ +# Generated by Django 4.2.11 on 2024-06-20 18:21 + +import django.db.models.deletion +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddField + +""" +BEGIN; +-- +-- Create model ReducedError +-- +CREATE TABLE "reports_reducederror" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "created_at" timestamp with time zone NOT NULL, "updated_at" timestamp with time zone NOT NULL, "message" text NOT NULL, "repoid" integer NULL); +-- +-- Create model Flake +-- +CREATE TABLE "reports_flake" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "created_at" timestamp with time zone NOT NULL, "updated_at" timestamp with time zone NOT NULL, "recent_passes_count" integer NOT NULL, "count" integer NOT NULL, "fail_count" integer NOT NULL, "start_date" timestamp with time zone NOT NULL, "end_date" timestamp with time zone NULL, "reduced_error_id" bigint NULL, "repoid" integer NOT NULL, "testid" text NOT NULL); +-- +-- Add field reduced_error to testinstance +-- +ALTER TABLE "reports_testinstance" ADD COLUMN "reduced_error_id" bigint NULL CONSTRAINT "reports_testinstance_reduced_error_id_f90c8b72_fk_reports_r" REFERENCES "reports_reducederror"("id") DEFERRABLE INITIALLY DEFERRED; SET CONSTRAINTS "reports_testinstance_reduced_error_id_f90c8b72_fk_reports_r" IMMEDIATE; +-- +-- Create constraint reports_reducederror_message_constraint on model reducederror +-- +ALTER TABLE "reports_reducederror" ADD CONSTRAINT "reports_reducederror_message_constraint" UNIQUE ("message", "repoid"); +-- +-- Create index reports_fla_repoid_69b787_idx on field(s) repository, test, reduced_error, end_date of model flake +-- +CREATE INDEX "reports_fla_repoid_69b787_idx" ON "reports_flake" ("repoid", "testid", "reduced_error_id", "end_date"); +ALTER TABLE "reports_reducederror" ADD CONSTRAINT "reports_reducederror_repoid_3c055705_fk_repos_repoid" FOREIGN KEY ("repoid") REFERENCES "repos" ("repoid") DEFERRABLE INITIALLY DEFERRED; +CREATE INDEX "reports_reducederror_repoid_3c055705" ON "reports_reducederror" ("repoid"); +ALTER TABLE "reports_flake" ADD CONSTRAINT "reports_flake_reduced_error_id_1d102637_fk_reports_r" FOREIGN KEY ("reduced_error_id") REFERENCES "reports_reducederror" ("id") DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE "reports_flake" ADD CONSTRAINT "reports_flake_repoid_1454c21c_fk_repos_repoid" FOREIGN KEY ("repoid") REFERENCES "repos" ("repoid") DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE "reports_flake" ADD CONSTRAINT "reports_flake_testid_9873bd1c_fk_reports_test_id" FOREIGN KEY ("testid") REFERENCES "reports_test" ("id") DEFERRABLE INITIALLY DEFERRED; +CREATE INDEX "reports_flake_reduced_error_id_1d102637" ON "reports_flake" ("reduced_error_id"); +CREATE INDEX "reports_flake_repoid_1454c21c" ON "reports_flake" ("repoid"); +CREATE INDEX "reports_flake_testid_9873bd1c" ON "reports_flake" ("testid"); +CREATE INDEX "reports_flake_testid_9873bd1c_like" ON "reports_flake" ("testid" text_pattern_ops); +CREATE INDEX "reports_testinstance_reduced_error_id_f90c8b72" ON "reports_testinstance" ("reduced_error_id"); +COMMIT; +""" + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0052_increment_version"), + ("reports", "0021_remove_testinstance_flaky_status_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="ReducedError", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("message", models.TextField()), + ( + "repository", + models.ForeignKey( + db_column="repoid", + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="reduced_errors", + to="core.repository", + ), + ), + ], + options={ + "db_table": "reports_reducederror", + }, + ), + migrations.CreateModel( + name="Flake", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("recent_passes_count", models.IntegerField()), + ("count", models.IntegerField()), + ("fail_count", models.IntegerField()), + ("start_date", models.DateTimeField()), + ("end_date", models.DateTimeField(null=True)), + ( + "reduced_error", + models.ForeignKey( + db_column="reduced_error_id", + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="flakes", + to="reports.reducederror", + ), + ), + ( + "repository", + models.ForeignKey( + db_column="repoid", + on_delete=django.db.models.deletion.CASCADE, + related_name="flakes", + to="core.repository", + ), + ), + ( + "test", + models.ForeignKey( + db_column="testid", + on_delete=django.db.models.deletion.CASCADE, + related_name="flakes", + to="reports.test", + ), + ), + ], + options={ + "db_table": "reports_flake", + }, + ), + RiskyAddField( + model_name="testinstance", + name="reduced_error", + field=models.ForeignKey( + db_column="reduced_error_id", + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="testinstances", + to="reports.reducederror", + ), + ), + migrations.AddConstraint( + model_name="reducederror", + constraint=models.UniqueConstraint( + fields=("message", "repository"), + name="reports_reducederror_message_constraint", + ), + ), + migrations.AddIndex( + model_name="flake", + index=models.Index( + fields=["repository", "test", "reduced_error", "end_date"], + name="reports_fla_repoid_69b787_idx", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0023_auto_20240909_2002.py b/libs/shared/shared/django_apps/reports/migrations/0023_auto_20240909_2002.py new file mode 100644 index 0000000000..19b76ae674 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0023_auto_20240909_2002.py @@ -0,0 +1,99 @@ +# Generated by Django 4.2.15 on 2024-09-09 20:02 + +import django.contrib.postgres.fields +import django.db.models.deletion +import psqlextra.backend.migrations.operations.add_default_partition +import psqlextra.backend.migrations.operations.create_partitioned_model +import psqlextra.manager.manager +import psqlextra.models.partitioned +import psqlextra.types +from django.db import migrations, models + +""" +BEGIN; +-- +-- Create partitioned model DailyTestRollup +-- +CREATE TABLE "reports_dailytestrollups" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "created_at" timestamp with time zone NOT NULL, "updated_at" timestamp with time zone NOT NULL, "date" date NOT NULL, "repoid" integer NOT NULL, "branch" text NOT NULL, "fail_count" integer NOT NULL, "skip_count" integer NOT NULL, "pass_count" integer NOT NULL, "last_duration_seconds" double precision NOT NULL, "avg_duration_seconds" double precision NOT NULL, "latest_run" timestamp with time zone NOT NULL, "commits_where_fail" text[] NOT NULL, "test_id" text NOT NULL, PRIMARY KEY ("id", "date")) PARTITION BY RANGE ("date"); +-- +-- Creates default partition 'default' on DailyTestRollup +-- +CREATE TABLE "reports_dailytestrollups_default" PARTITION OF "reports_dailytestrollups" DEFAULT; +-- +-- Create constraint reports_dailytestrollups_repoid_date_branch_test on model dailytestrollup +-- +ALTER TABLE "reports_dailytestrollups" ADD CONSTRAINT "reports_dailytestrollups_repoid_date_branch_test" UNIQUE ("repoid", "date", "branch", "test_id"); +ALTER TABLE "reports_dailytestrollups" ADD CONSTRAINT "reports_dailytestrollups_test_id_bb017fbf_fk_reports_test_id" FOREIGN KEY ("test_id") REFERENCES "reports_test" ("id") DEFERRABLE INITIALLY DEFERRED; +CREATE INDEX "reports_dailytestrollups_test_id_bb017fbf" ON "reports_dailytestrollups" ("test_id"); +CREATE INDEX "reports_dailytestrollups_test_id_bb017fbf_like" ON "reports_dailytestrollups" ("test_id" text_pattern_ops); +CREATE INDEX "dailytestrollups_repoid_date" ON "reports_dailytestrollups" ("repoid", "date"); +COMMIT; +""" + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0022_reducederror_flake_testinstance_reduced_error_and_more"), + ] + + operations = [ + psqlextra.backend.migrations.operations.create_partitioned_model.PostgresCreatePartitionedModel( + name="DailyTestRollup", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("date", models.DateField()), + ("repoid", models.IntegerField()), + ("branch", models.TextField()), + ("fail_count", models.IntegerField()), + ("skip_count", models.IntegerField()), + ("pass_count", models.IntegerField()), + ("last_duration_seconds", models.FloatField()), + ("avg_duration_seconds", models.FloatField()), + ("latest_run", models.DateTimeField()), + ( + "commits_where_fail", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), size=None + ), + ), + ( + "test", + models.ForeignKey( + db_column="test_id", + on_delete=django.db.models.deletion.CASCADE, + related_name="daily_test_rollups", + to="reports.test", + ), + ), + ], + options={ + "db_table": "reports_dailytestrollups", + "indexes": [ + models.Index( + fields=["repoid", "date"], name="dailytestrollups_repoid_date" + ) + ], + }, + partitioning_options={ + "method": psqlextra.types.PostgresPartitioningMethod["RANGE"], + "key": ["date"], + }, + bases=(psqlextra.models.partitioned.PostgresPartitionedModel,), + managers=[ + ("objects", psqlextra.manager.manager.PostgresManager()), + ], + ), + psqlextra.backend.migrations.operations.add_default_partition.PostgresAddDefaultPartition( + model_name="DailyTestRollup", + name="default", + ), + migrations.AddConstraint( + model_name="dailytestrollup", + constraint=models.UniqueConstraint( + fields=("repoid", "date", "branch", "test"), + name="reports_dailytestrollups_repoid_date_branch_test", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0024_auto_20240917_2121.py b/libs/shared/shared/django_apps/reports/migrations/0024_auto_20240917_2121.py new file mode 100644 index 0000000000..e66e23b7c2 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0024_auto_20240917_2121.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.15 on 2024-09-17 21:21 + +from django.db import migrations +from psqlextra.backend.migrations.operations import PostgresAddRangePartition + +""" +BEGIN; +-- +-- Creates range partition reports_dailytestrollups_2024_jul on DailyTestRollup +-- +CREATE TABLE "reports_dailytestrollups_reports_dailytestrollups_2024_jul" PARTITION OF "reports_dailytestrollups" FOR VALUES FROM ('2024-07-01') TO ('2024-08-01'); +-- +-- Creates range partition reports_dailytestrollups_2024_aug on DailyTestRollup +-- +CREATE TABLE "reports_dailytestrollups_reports_dailytestrollups_2024_aug" PARTITION OF "reports_dailytestrollups" FOR VALUES FROM ('2024-08-01') TO ('2024-09-01'); +COMMIT; +""" + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0023_auto_20240909_2002"), + ] + + operations = [ + PostgresAddRangePartition( + model_name="DailyTestRollup", + name="reports_dailytestrollups_2024_jul", + from_values="2024-07-01", + to_values="2024-08-01", + ), + PostgresAddRangePartition( + model_name="DailyTestRollup", + name="reports_dailytestrollups_2024_aug", + from_values="2024-08-01", + to_values="2024-09-01", + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0025_dailytestrollup_flaky_fail_count.py b/libs/shared/shared/django_apps/reports/migrations/0025_dailytestrollup_flaky_fail_count.py new file mode 100644 index 0000000000..1b89fa9e00 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0025_dailytestrollup_flaky_fail_count.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.15 on 2024-09-12 21:11 + +from django.db import migrations, models + +""" +BEGIN; +-- +-- Add field flaky_fail_count to dailytestrollup +-- +ALTER TABLE "reports_dailytestrollups" ADD COLUMN "flaky_fail_count" integer DEFAULT 0 NOT NULL; +ALTER TABLE "reports_dailytestrollups" ALTER COLUMN "flaky_fail_count" DROP DEFAULT; +COMMIT; +""" + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0024_auto_20240917_2121"), + ] + + operations = [ + migrations.AddField( + model_name="dailytestrollup", + name="flaky_fail_count", + field=models.IntegerField(default=0), + preserve_default=False, + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0026_testflagbridge.py b/libs/shared/shared/django_apps/reports/migrations/0026_testflagbridge.py new file mode 100644 index 0000000000..4e5bf4fb3d --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0026_testflagbridge.py @@ -0,0 +1,62 @@ +# Generated by Django 4.2.15 on 2024-09-23 13:42 + +import django.db.models.deletion +from django.db import migrations, models + +""" +BEGIN; +-- +-- Create model TestFlagBridge +-- +CREATE TABLE "reports_test_results_flag_bridge" ("id" integer NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "flag_id" bigint NOT NULL, "test_id" text NOT NULL); +ALTER TABLE "reports_test_results_flag_bridge" ADD CONSTRAINT "reports_test_results_flag_id_741a460e_fk_reports_r" FOREIGN KEY ("flag_id") REFERENCES "reports_repositoryflag" ("id") DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE "reports_test_results_flag_bridge" ADD CONSTRAINT "reports_test_results_test_id_48eb4c8e_fk_reports_t" FOREIGN KEY ("test_id") REFERENCES "reports_test" ("id") DEFERRABLE INITIALLY DEFERRED; +CREATE INDEX "reports_test_results_flag_bridge_flag_id_741a460e" ON "reports_test_results_flag_bridge" ("flag_id"); +CREATE INDEX "reports_test_results_flag_bridge_test_id_48eb4c8e" ON "reports_test_results_flag_bridge" ("test_id"); +CREATE INDEX "reports_test_results_flag_bridge_test_id_48eb4c8e_like" ON "reports_test_results_flag_bridge" ("test_id" text_pattern_ops); +COMMIT; +""" + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0025_dailytestrollup_flaky_fail_count"), + ] + + operations = [ + migrations.CreateModel( + name="TestFlagBridge", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "flag", + models.ForeignKey( + db_column="flag_id", + on_delete=django.db.models.deletion.CASCADE, + related_name="test_flag_bridges", + to="reports.repositoryflag", + ), + ), + ( + "test", + models.ForeignKey( + db_column="test_id", + on_delete=django.db.models.deletion.CASCADE, + related_name="test_flag_bridges", + to="reports.test", + ), + ), + ], + options={ + "db_table": "reports_test_results_flag_bridge", + }, + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0027_test_computed_name_test_filename_test_framework.py b/libs/shared/shared/django_apps/reports/migrations/0027_test_computed_name_test_filename_test_framework.py new file mode 100644 index 0000000000..570fb8072c --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0027_test_computed_name_test_filename_test_framework.py @@ -0,0 +1,53 @@ +# Generated by Django 4.2.15 on 2024-09-25 20:25 + +from django.db import migrations, models + +""" +BEGIN; +-- +-- Add field computed_name to test +-- +ALTER TABLE "reports_test" ADD COLUMN "computed_name" text NULL; +-- +-- Add field filename to test +-- +ALTER TABLE "reports_test" ADD COLUMN "filename" text NULL; +-- +-- Add field framework to test +-- +ALTER TABLE "reports_test" ADD COLUMN "framework" varchar(100) NULL; +COMMIT; +""" + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0026_testflagbridge"), + ] + + operations = [ + migrations.AddField( + model_name="test", + name="computed_name", + field=models.TextField(null=True), + ), + migrations.AddField( + model_name="test", + name="filename", + field=models.TextField(null=True), + ), + migrations.AddField( + model_name="test", + name="framework", + field=models.CharField( + choices=[ + ("pytest", "Pytest"), + ("jest", "Jest"), + ("vitest", "Vitest"), + ("phpunit", "Phpunit"), + ], + max_length=100, + null=True, + ), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0028_remove_test_commits_where_fail_and_more.py b/libs/shared/shared/django_apps/reports/migrations/0028_remove_test_commits_where_fail_and_more.py new file mode 100644 index 0000000000..a539f4c401 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0028_remove_test_commits_where_fail_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.15 on 2024-10-15 16:42 + +""" +BEGIN; +-- +-- Remove field commits_where_fail from test +-- +ALTER TABLE "reports_test" DROP COLUMN "commits_where_fail" CASCADE; +-- +-- Remove field failure_rate from test +-- +ALTER TABLE "reports_test" DROP COLUMN "failure_rate" CASCADE; +COMMIT; +""" + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0027_test_computed_name_test_filename_test_framework"), + ] + + operations = [ + migrations.RemoveField( + model_name="test", + name="commits_where_fail", + ), + migrations.RemoveField( + model_name="test", + name="failure_rate", + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0029_alter_dailytestrollup_branch.py b/libs/shared/shared/django_apps/reports/migrations/0029_alter_dailytestrollup_branch.py new file mode 100644 index 0000000000..040a7e7ec6 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0029_alter_dailytestrollup_branch.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.15 on 2024-09-20 20:21 + +from django.db import migrations, models + +""" +BEGIN; +-- +-- Alter field branch on dailytestrollup +-- +ALTER TABLE "reports_dailytestrollups" ALTER COLUMN "branch" DROP NOT NULL; +COMMIT; +""" + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0028_remove_test_commits_where_fail_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="dailytestrollup", + name="branch", + field=models.TextField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0030_testflagbridge_reports_test_results_flag_bridge_flag_test.py b/libs/shared/shared/django_apps/reports/migrations/0030_testflagbridge_reports_test_results_flag_bridge_flag_test.py new file mode 100644 index 0000000000..809c5478ed --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0030_testflagbridge_reports_test_results_flag_bridge_flag_test.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.16 on 2024-11-05 20:47 + +from django.db import migrations, models + +""" +BEGIN; +-- +-- Create constraint reports_test_results_flag_bridge_flag_test on model testflagbridge +-- +ALTER TABLE "reports_test_results_flag_bridge" ADD CONSTRAINT "reports_test_results_flag_bridge_flag_test" UNIQUE ("flag_id", "test_id"); +COMMIT; +""" + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0029_alter_dailytestrollup_branch"), + ] + + operations = [ + migrations.AddConstraint( + model_name="testflagbridge", + constraint=models.UniqueConstraint( + fields=("flag", "test"), + name="reports_test_results_flag_bridge_flag_test", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0031_lastcacherollupdate_and_more.py b/libs/shared/shared/django_apps/reports/migrations/0031_lastcacherollupdate_and_more.py new file mode 100644 index 0000000000..58f2ff04eb --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0031_lastcacherollupdate_and_more.py @@ -0,0 +1,40 @@ +# Generated by Django 4.2.16 on 2024-11-06 19:05 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0059_increment_version"), + ("reports", "0030_testflagbridge_reports_test_results_flag_bridge_flag_test"), + ] + + operations = [ + migrations.CreateModel( + name="LastCacheRollupDate", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("branch", models.TextField()), + ("last_rollup_date", models.DateField()), + ( + "repository", + models.ForeignKey( + db_column="repoid", + on_delete=django.db.models.deletion.CASCADE, + to="core.repository", + ), + ), + ], + options={ + "db_table": "reports_lastrollupdate", + }, + ), + migrations.AddConstraint( + model_name="lastcacherollupdate", + constraint=models.UniqueConstraint( + fields=("repository", "branch"), + name="reports_lastrollupdate_repoid_branch", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0032_reportsession_reports_upload_order_number.py b/libs/shared/shared/django_apps/reports/migrations/0032_reportsession_reports_upload_order_number.py new file mode 100644 index 0000000000..e4da718c72 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0032_reportsession_reports_upload_order_number.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.16 on 2024-11-22 00:38 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddIndex + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0031_lastcacherollupdate_and_more"), + ] + + operations = [ + RiskyAddIndex( + model_name="reportsession", + index=models.Index( + fields=["report_id", "upload_type", "order_number"], + name="upload_index_id_type_number", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0033_alter_testinstance_duration_seconds.py b/libs/shared/shared/django_apps/reports/migrations/0033_alter_testinstance_duration_seconds.py new file mode 100644 index 0000000000..f6ed600181 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0033_alter_testinstance_duration_seconds.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.16 on 2024-11-27 14:46 + +from django.db import migrations, models + +""" +BEGIN; +-- +-- Alter field duration_seconds on testinstance +-- +ALTER TABLE "reports_testinstance" ALTER COLUMN "duration_seconds" DROP NOT NULL; +COMMIT; +""" + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0032_reportsession_reports_upload_order_number"), + ] + + operations = [ + migrations.AlterField( + model_name="testinstance", + name="duration_seconds", + field=models.FloatField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0034_remove_flake_created_at_remove_flake_updated_at.py b/libs/shared/shared/django_apps/reports/migrations/0034_remove_flake_created_at_remove_flake_updated_at.py new file mode 100644 index 0000000000..b50767b58a --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0034_remove_flake_created_at_remove_flake_updated_at.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.16 on 2024-12-13 15:44 + +from django.db import migrations + +from shared.django_apps.migration_utils import RiskyRemoveField + +""" +BEGIN; +-- +-- Remove field created_at from flake +-- +ALTER TABLE "reports_flake" DROP COLUMN "created_at" CASCADE; +-- +-- Remove field updated_at from flake +-- +ALTER TABLE "reports_flake" DROP COLUMN "updated_at" CASCADE; +COMMIT; +""" + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0033_alter_testinstance_duration_seconds"), + ] + + operations = [ + RiskyRemoveField( + model_name="flake", + name="created_at", + ), + RiskyRemoveField( + model_name="flake", + name="updated_at", + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0035_upload_indices_part1.py b/libs/shared/shared/django_apps/reports/migrations/0035_upload_indices_part1.py new file mode 100644 index 0000000000..4bb5e66719 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0035_upload_indices_part1.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.16 on 2024-11-28 12:26 + +from django.contrib.postgres.operations import AddIndexConcurrently +from django.db import migrations, models + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ( + "reports", + "0034_remove_flake_created_at_remove_flake_updated_at", + ), + ] + + operations = [ + AddIndexConcurrently( + model_name="reportsession", + index=models.Index( + name="upload_report_type_idx", + fields=["report_id", "upload_type"], + ), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0036_upload_indices_part2.py b/libs/shared/shared/django_apps/reports/migrations/0036_upload_indices_part2.py new file mode 100644 index 0000000000..d91f590aa1 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0036_upload_indices_part2.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.16 on 2024-11-28 12:37 + +from django.contrib.postgres.operations import RemoveIndexConcurrently +from django.db import migrations + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("reports", "0035_upload_indices_part1"), + ] + + operations = [ + # We drop these indices for the following reasons: + # - `upload_index_id_type_number`: This matches the above state migration, + # and it looks like the index does not actually exist in the production DB. + RemoveIndexConcurrently( + model_name="reportsession", + name="upload_index_id_type_number", + ), + # The following indices exist in the production DB, but not in django state/migrations: + # - `reports_upload_order_number_idx`: + # We never query by the `order_number` alone, so this index is likely unused. + # - `reports_upload_report_id_f6b4ffae`: Queries on `report_id` should already been covered by the + # newly added index on `report_id`+`upload_type`. + # - `reports_upload_report_id_upload_type_index_ccnew`: + # This seems to be a manually added variant of the `upload_report_type_idx` index and is thus duplicated. + # - `reports_upload_report_id_upload_type_order_number_index`: + # This is the same as the above, except with an additional `order_number`. + # We do use it in queries, but I doubt the index pulls its weight, as the `order_number` changes quite + # frequently so the index is costly to maintain. + *( + # Interestingly, we have to run these in individual `RunSQL` statements, otherwise django would create a + # transaction around them, which is not supported for these `DROP INDEX CONCURRENTLY` statements. + migrations.RunSQL( + sql=f"""DROP INDEX CONCURRENTLY IF EXISTS "{idx}";""", + hints={"tables": ["reports_upload"]}, + ) + for idx in [ + "reports_upload_order_number_idx", + "reports_upload_report_id_f6b4ffae", + "reports_upload_report_id_upload_type_index_ccnew", + "reports_upload_report_id_upload_type_order_number_index", + ] + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0037_alter_reportsession_storage_path.py b/libs/shared/shared/django_apps/reports/migrations/0037_alter_reportsession_storage_path.py new file mode 100644 index 0000000000..dea33ee54b --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0037_alter_reportsession_storage_path.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.16 on 2025-01-16 16:34 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAlterField + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0036_upload_indices_part2"), + ] + + operations = [ + RiskyAlterField( + model_name="reportsession", + name="storage_path", + field=models.TextField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0038_remove_test_reports_test_repoid_name_testsuite_flags_hash.py b/libs/shared/shared/django_apps/reports/migrations/0038_remove_test_reports_test_repoid_name_testsuite_flags_hash.py new file mode 100644 index 0000000000..05ecd39569 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0038_remove_test_reports_test_repoid_name_testsuite_flags_hash.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2025-01-20 17:39 + +from django.db import migrations + +from shared.django_apps.migration_utils import RiskyRemoveConstraint + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0037_alter_reportsession_storage_path"), + ] + + operations = [ + RiskyRemoveConstraint( + model_name="test", + name="reports_test_repoid_name_testsuite_flags_hash", + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0039_reportsession_upload_storage_path_idx.py b/libs/shared/shared/django_apps/reports/migrations/0039_reportsession_upload_storage_path_idx.py new file mode 100644 index 0000000000..7b1e43c496 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0039_reportsession_upload_storage_path_idx.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.16 on 2025-01-28 15:28 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddIndex + +""" +BEGIN; +-- +-- Create index upload_storage_path_idx on field(s) storage_path of model reportsession +-- +CREATE INDEX "upload_storage_path_idx" ON "reports_upload" ("storage_path"); +COMMIT; +""" + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0038_remove_test_reports_test_repoid_name_testsuite_flags_hash"), + ] + + operations = [ + RiskyAddIndex( + model_name="reportsession", + index=models.Index(fields=["storage_path"], name="upload_storage_path_idx"), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0040_testinstance_reports_tes_repoid_9cfd0a_idx.py b/libs/shared/shared/django_apps/reports/migrations/0040_testinstance_reports_tes_repoid_9cfd0a_idx.py new file mode 100644 index 0000000000..8549262dd4 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0040_testinstance_reports_tes_repoid_9cfd0a_idx.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.16 on 2025-03-10 17:11 + +from django.contrib.postgres.operations import AddIndexConcurrently +from django.db import migrations, models + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("reports", "0039_reportsession_upload_storage_path_idx"), + ] + + operations = [ + AddIndexConcurrently( + model_name="testinstance", + index=models.Index(fields=["repoid"], name="reports_tes_repoid_9cfd0a_idx"), + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/0041_rm_reportdetails.py b/libs/shared/shared/django_apps/reports/migrations/0041_rm_reportdetails.py new file mode 100644 index 0000000000..73bf18349f --- /dev/null +++ b/libs/shared/shared/django_apps/reports/migrations/0041_rm_reportdetails.py @@ -0,0 +1,15 @@ +# Generated by Django 4.2.16 on 2025-03-31 13:12 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("reports", "0040_testinstance_reports_tes_repoid_9cfd0a_idx"), + ] + + operations = [ + migrations.DeleteModel( + name="ReportDetails", + ), + ] diff --git a/libs/shared/shared/django_apps/reports/migrations/__init__.py b/libs/shared/shared/django_apps/reports/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/reports/models.py b/libs/shared/shared/django_apps/reports/models.py new file mode 100644 index 0000000000..2eab54766f --- /dev/null +++ b/libs/shared/shared/django_apps/reports/models.py @@ -0,0 +1,458 @@ +import logging +import uuid + +from django.contrib.postgres.fields import ArrayField +from django.db import models +from django_prometheus.models import ExportModelOperationsMixin +from psqlextra.models import PostgresPartitionedModel +from psqlextra.types import PostgresPartitioningMethod + +from shared.django_apps.codecov.models import BaseCodecovModel, BaseModel +from shared.django_apps.reports.managers import CommitReportManager +from shared.django_apps.utils.services import get_short_service_name +from shared.reports.enums import UploadState, UploadType +from shared.upload.constants import ci + +log = logging.getLogger(__name__) + +# Added to avoid 'doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS' error\ +# Needs to be called the same as the API app +REPORTS_APP_LABEL = "reports" + + +class ReportType(models.TextChoices): + COVERAGE = "coverage" + TEST_RESULTS = "test_results" + BUNDLE_ANALYSIS = "bundle_analysis" + + +class AbstractTotals( + ExportModelOperationsMixin("reports.abstract_totals"), BaseCodecovModel +): + branches = models.IntegerField() + coverage = models.DecimalField(max_digits=8, decimal_places=5) + hits = models.IntegerField() + lines = models.IntegerField() + methods = models.IntegerField() + misses = models.IntegerField() + partials = models.IntegerField() + files = models.IntegerField() + + class Meta: + abstract = True + + +class CommitReport( + ExportModelOperationsMixin("reports.commit_report"), BaseCodecovModel +): + class ReportType(models.TextChoices): + COVERAGE = "coverage" + TEST_RESULTS = "test_results" + BUNDLE_ANALYSIS = "bundle_analysis" + + commit = models.ForeignKey( + "core.Commit", related_name="reports", on_delete=models.CASCADE + ) + code = models.CharField(null=True, max_length=100) + report_type = models.CharField( + null=True, max_length=100, choices=ReportType.choices + ) + + class Meta: + app_label = REPORTS_APP_LABEL + + objects = CommitReportManager() + + +class ReportResults( + ExportModelOperationsMixin("reports.report_results"), BaseCodecovModel +): + class ReportResultsStates(models.TextChoices): + PENDING = "pending" + COMPLETED = "completed" + ERROR = "error" + + report = models.OneToOneField(CommitReport, on_delete=models.CASCADE) + state = models.TextField(null=True, choices=ReportResultsStates.choices) + completed_at = models.DateTimeField(null=True) + result = models.JSONField(default=dict) + + class Meta: + app_label = REPORTS_APP_LABEL + + +class ReportLevelTotals(AbstractTotals): + report = models.OneToOneField(CommitReport, on_delete=models.CASCADE) + + class Meta: + app_label = REPORTS_APP_LABEL + + +class UploadError(ExportModelOperationsMixin("reports.upload_error"), BaseCodecovModel): + report_session = models.ForeignKey( + "ReportSession", + db_column="upload_id", + related_name="errors", + on_delete=models.CASCADE, + ) + error_code = models.CharField(max_length=100) + error_params = models.JSONField(default=dict) + + class Meta: + app_label = REPORTS_APP_LABEL + db_table = "reports_uploaderror" + + +class UploadFlagMembership( + ExportModelOperationsMixin("reports.upload_flag_membership"), models.Model +): + report_session = models.ForeignKey( + "ReportSession", db_column="upload_id", on_delete=models.CASCADE + ) + flag = models.ForeignKey("RepositoryFlag", on_delete=models.CASCADE) + id = models.BigAutoField(primary_key=True) + + class Meta: + app_label = REPORTS_APP_LABEL + db_table = "reports_uploadflagmembership" + + +class RepositoryFlag( + ExportModelOperationsMixin("reports.repository_flag"), BaseCodecovModel +): + repository = models.ForeignKey( + "core.Repository", related_name="flags", on_delete=models.CASCADE + ) + flag_name = models.CharField(max_length=1024) + deleted = models.BooleanField(null=True) + + class Meta: + app_label = REPORTS_APP_LABEL + + +class ReportSession( + ExportModelOperationsMixin("reports.report_session"), BaseCodecovModel +): + # should be called Upload, but to do it we have to make the + # constraints be manually named, which take a bit + build_code = models.TextField(null=True) + build_url = models.TextField(null=True) + env = models.JSONField(null=True) + flags = models.ManyToManyField(RepositoryFlag, through=UploadFlagMembership) + job_code = models.TextField(null=True) + name = models.CharField(null=True, max_length=100) + provider = models.CharField(max_length=50, null=True) + report = models.ForeignKey( + "CommitReport", related_name="sessions", on_delete=models.CASCADE + ) + state = models.CharField(max_length=100) + storage_path = models.TextField(null=True) + order_number = models.IntegerField(null=True) + upload_type = models.CharField(max_length=100, default="uploaded") + upload_extras = models.JSONField(default=dict) + state_id = models.IntegerField(null=True, choices=UploadState.choices()) + upload_type_id = models.IntegerField(null=True, choices=UploadType.choices()) + + class Meta: + app_label = REPORTS_APP_LABEL + db_table = "reports_upload" + indexes = [ + models.Index( + name="upload_report_type_idx", + fields=["report_id", "upload_type"], + ), + models.Index( + name="upload_storage_path_idx", + fields=["storage_path"], + ), + ] + + @property + def ci_url(self): + if self.build_url: + # build_url was saved in the database + return self.build_url + + # otherwise we need to construct it ourself (if possible) + build_url = ci.get(self.provider, {}).get("build_url") + if not build_url: + return + repository = self.report.commit.repository + data = { + "service_short": get_short_service_name(repository.author.service), + "owner": repository.author, + "upload": self, + "repo": repository, + "commit": self.report.commit, + } + return build_url.format(**data) + + @property + def flag_names(self): + return [flag.flag_name for flag in self.flags.all()] + + +class UploadLevelTotals(AbstractTotals): + report_session = models.OneToOneField( + ReportSession, db_column="upload_id", on_delete=models.CASCADE + ) + + class Meta: + app_label = REPORTS_APP_LABEL + db_table = "reports_uploadleveltotals" + + +class Test(models.Model): + # the reason we aren't using the regular primary key + # in this case is because we want to be able to compute/predict + # the primary key of a Test object ourselves in the processor + # so we can easily do concurrent writes to the database + # this is a hash of the repoid, name, testsuite and flags_hash + id = models.TextField(primary_key=True) + + external_id = models.UUIDField(default=uuid.uuid4, editable=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + repository = models.ForeignKey( + "core.Repository", + db_column="repoid", + related_name="tests", + on_delete=models.CASCADE, + ) + name = models.TextField() + testsuite = models.TextField() + # this is a hash of the flags associated with this test + # users will use flags to distinguish the same test being run + # in a different environment + # for example: the same test being run on windows vs. mac + flags_hash = models.TextField() + + class Framework(models.TextChoices): + PYTEST = "pytest" + JEST = "jest" + VITEST = "vitest" + PHPUNIT = "phpunit" + + framework = models.CharField(max_length=100, choices=Framework.choices, null=True) + + computed_name = models.TextField(null=True) + filename = models.TextField(null=True) + + class Meta: + app_label = REPORTS_APP_LABEL + db_table = "reports_test" + + +class TestInstance(BaseCodecovModel): + test = models.ForeignKey( + "Test", + db_column="test_id", + related_name="testinstances", + on_delete=models.CASCADE, + ) + + class Outcome(models.TextChoices): + FAILURE = "failure" + SKIP = "skip" + ERROR = "error" + PASS = "pass" + + duration_seconds = models.FloatField(null=True) + outcome = models.CharField(max_length=100, choices=Outcome.choices) + upload = models.ForeignKey( + "ReportSession", + db_column="upload_id", + related_name="testinstances", + on_delete=models.CASCADE, + ) + failure_message = models.TextField(null=True) + + branch = models.TextField(null=True) + commitid = models.TextField(null=True) + repoid = models.IntegerField(null=True) + + reduced_error = models.ForeignKey( + "ReducedError", + db_column="reduced_error_id", + related_name="testinstances", + on_delete=models.CASCADE, + null=True, + ) + + class Meta: + app_label = REPORTS_APP_LABEL + db_table = "reports_testinstance" + indexes = [ + models.Index(fields=["commitid", "repoid", "branch"]), + models.Index(fields=["repoid", "created_at", "outcome"]), + models.Index(fields=["repoid"]), + ] + + +class TestResultReportTotals(BaseCodecovModel): + passed = models.IntegerField() + skipped = models.IntegerField() + failed = models.IntegerField() + + class TestResultsProcessingError(models.TextChoices): + NO_SUCCESS = "no_success" + + error = models.CharField( + null=True, max_length=100, choices=TestResultsProcessingError.choices + ) + + report = models.OneToOneField(CommitReport, on_delete=models.CASCADE) + + class Meta: + app_label = REPORTS_APP_LABEL + db_table = "reports_testresultreporttotals" + + +class ReducedError(BaseModel): + message = models.TextField() + repository = models.ForeignKey( + "core.Repository", + db_column="repoid", + related_name="reduced_errors", + on_delete=models.CASCADE, + null=True, + ) + + class Meta: + app_label = REPORTS_APP_LABEL + db_table = "reports_reducederror" + + constraints = [ + models.UniqueConstraint( + fields=["message", "repository"], + name="reports_reducederror_message_constraint", + ), + ] + + +class Flake(models.Model): + id = models.BigAutoField(primary_key=True) + repository = models.ForeignKey( + "core.Repository", + db_column="repoid", + related_name="flakes", + on_delete=models.CASCADE, + ) + test = models.ForeignKey( + "Test", db_column="testid", related_name="flakes", on_delete=models.CASCADE + ) + reduced_error = models.ForeignKey( + "ReducedError", + db_column="reduced_error_id", + related_name="flakes", + on_delete=models.CASCADE, + null=True, + ) + + recent_passes_count = models.IntegerField() + count = models.IntegerField() + fail_count = models.IntegerField() + start_date = models.DateTimeField() + end_date = models.DateTimeField(null=True) + + class Meta: + app_label = REPORTS_APP_LABEL + db_table = "reports_flake" + indexes = [ + models.Index(fields=["repository", "test", "reduced_error", "end_date"]) + ] + + +class DailyTestRollup(PostgresPartitionedModel, BaseModel): + class PartitioningMeta: + method = PostgresPartitioningMethod.RANGE + key = ["date"] + + test = models.ForeignKey( + "Test", + db_column="test_id", + related_name="daily_test_rollups", + on_delete=models.CASCADE, + ) + date = models.DateField() + repoid = models.IntegerField() + branch = models.TextField(null=True) + + fail_count = models.IntegerField() + flaky_fail_count = models.IntegerField() + skip_count = models.IntegerField() + pass_count = models.IntegerField() + last_duration_seconds = models.FloatField() + avg_duration_seconds = models.FloatField() + latest_run = models.DateTimeField() + commits_where_fail = ArrayField(models.TextField()) + + class Meta: + app_label = REPORTS_APP_LABEL + db_table = "reports_dailytestrollups" + constraints = [ + models.UniqueConstraint( + fields=[ + "repoid", + "date", + "branch", + "test", + ], + name="reports_dailytestrollups_repoid_date_branch_test", + ) + ] + indexes = [ + models.Index( + fields=[ + "repoid", + "date", + ], + name="dailytestrollups_repoid_date", + ) + ] + + +class TestFlagBridge(models.Model): + flag = models.ForeignKey( + "RepositoryFlag", + db_column="flag_id", + related_name="test_flag_bridges", + on_delete=models.CASCADE, + ) + + test = models.ForeignKey( + "Test", + db_column="test_id", + related_name="test_flag_bridges", + on_delete=models.CASCADE, + ) + + class Meta: + app_label = REPORTS_APP_LABEL + db_table = "reports_test_results_flag_bridge" + constraints = [ + models.UniqueConstraint( + fields=["flag", "test"], + name="reports_test_results_flag_bridge_flag_test", + ) + ] + + +class LastCacheRollupDate(models.Model): + id = models.BigAutoField(primary_key=True) + repository = models.ForeignKey( + "core.Repository", db_column="repoid", on_delete=models.CASCADE + ) + branch = models.TextField() + last_rollup_date = models.DateField() + + class Meta: + app_label = REPORTS_APP_LABEL + db_table = "reports_lastrollupdate" + constraints = [ + models.UniqueConstraint( + fields=["repository", "branch"], + name="reports_lastrollupdate_repoid_branch", + ) + ] diff --git a/libs/shared/shared/django_apps/reports/tests/__init__.py b/libs/shared/shared/django_apps/reports/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/reports/tests/factories.py b/libs/shared/shared/django_apps/reports/tests/factories.py new file mode 100644 index 0000000000..bae5125c4b --- /dev/null +++ b/libs/shared/shared/django_apps/reports/tests/factories.py @@ -0,0 +1,189 @@ +import datetime as dt +import enum + +import factory +from factory.django import DjangoModelFactory + +from shared.django_apps.core.tests.factories import CommitFactory, RepositoryFactory +from shared.django_apps.reports import models +from shared.django_apps.reports.models import ( + DailyTestRollup, + Flake, + LastCacheRollupDate, + ReducedError, + ReportResults, + Test, + TestFlagBridge, + TestInstance, +) + + +# TODO: deduplicate this from graphql_api.types.enums +class UploadErrorEnum(enum.Enum): + FILE_NOT_IN_STORAGE = "file_not_in_storage" + REPORT_EXPIRED = "report_expired" + REPORT_EMPTY = "report_empty" + UNSUPPORTED_FILE_FORMAT = "unsupported_file_format" + + +class CommitReportFactory(DjangoModelFactory): + class Meta: + model = models.CommitReport + + commit = factory.SubFactory(CommitFactory) + + +class UploadFactory(DjangoModelFactory): + class Meta: + model = models.ReportSession + + build_code = factory.Sequence(lambda n: f"{n}") + report = factory.SubFactory(CommitReportFactory) + state = "processed" + + +class RepositoryFlagFactory(DjangoModelFactory): + class Meta: + model = models.RepositoryFlag + + repository = factory.SubFactory(RepositoryFactory) + flag_name = factory.Faker("word") + + +class UploadFlagMembershipFactory(DjangoModelFactory): + class Meta: + model = models.UploadFlagMembership + + flag = factory.SubFactory(RepositoryFlagFactory) + report_session = factory.SubFactory(UploadFactory) + + +class ReportLevelTotalsFactory(DjangoModelFactory): + class Meta: + model = models.ReportLevelTotals + + report = factory.SubFactory(CommitReportFactory) + branches = factory.Faker("pyint") + coverage = factory.Faker("pydecimal", min_value=10, max_value=90, right_digits=2) + hits = factory.Faker("pyint") + lines = factory.Faker("pyint") + methods = factory.Faker("pyint") + misses = factory.Faker("pyint") + partials = factory.Faker("pyint") + files = factory.Faker("pyint") + + +class UploadLevelTotalsFactory(DjangoModelFactory): + class Meta: + model = models.UploadLevelTotals + + report_session = factory.SubFactory(UploadFactory) + + +class UploadErrorFactory(DjangoModelFactory): + class Meta: + model = models.UploadError + + report_session = factory.SubFactory(UploadFactory) + error_code = factory.Iterator( + [ + UploadErrorEnum.FILE_NOT_IN_STORAGE, + UploadErrorEnum.REPORT_EMPTY, + UploadErrorEnum.REPORT_EXPIRED, + UploadErrorEnum.UNSUPPORTED_FILE_FORMAT, + ] + ) + + +class ReportResultsFactory(DjangoModelFactory): + class Meta: + model = ReportResults + + report = factory.SubFactory(CommitReportFactory) + state = factory.Iterator( + [ + ReportResults.ReportResultsStates.PENDING, + ReportResults.ReportResultsStates.COMPLETED, + ] + ) + + +class ReducedErrorFactory(DjangoModelFactory): + class Meta: + model = ReducedError + + message = factory.Sequence(lambda n: f"message_{n}") + + +class TestFactory(DjangoModelFactory): + class Meta: + model = Test + + repository = factory.SubFactory(RepositoryFactory) + name = factory.Sequence(lambda n: f"test_{n}") + id = factory.Sequence(lambda n: f"test_{n}") + + +class TestInstanceFactory(DjangoModelFactory): + class Meta: + model = TestInstance + + test = factory.SubFactory(TestFactory) + upload = factory.SubFactory(UploadFactory) + duration_seconds = factory.Faker("pyint", min_value=0, max_value=1000) + + repoid = factory.SelfAttribute("test.repository.repoid") + commitid = factory.SelfAttribute("upload.report.commit.commitid") + + branch = "main" + + +class FlakeFactory(DjangoModelFactory): + class Meta: + model = Flake + + repository = factory.SubFactory(RepositoryFactory) + test = factory.SubFactory(TestFactory) + reduced_error = factory.SubFactory(ReducedErrorFactory) + + recent_passes_count = 0 + count = 0 + fail_count = 0 + start_date = dt.datetime.now() + + +class DailyTestRollupFactory(DjangoModelFactory): + class Meta: + model = DailyTestRollup + + test = factory.SubFactory(TestFactory) + date = dt.date.today() + repoid = factory.SelfAttribute("test.repository.repoid") + branch = "main" + + pass_count = 0 + fail_count = 0 + skip_count = 0 + flaky_fail_count = 0 + + last_duration_seconds = 0.0 + avg_duration_seconds = 0.0 + latest_run = dt.datetime.now() + commits_where_fail: list[str] = [] + + +class TestFlagBridgeFactory(DjangoModelFactory): + class Meta: + model = TestFlagBridge + + test = factory.SubFactory(TestFactory) + flag = factory.SubFactory(RepositoryFlagFactory) + + +class LastCacheRollupDateFactory(DjangoModelFactory): + class Meta: + model = LastCacheRollupDate + + repository = factory.SubFactory(RepositoryFactory) + branch = "main" + last_rollup_date = dt.date.today() diff --git a/libs/shared/shared/django_apps/reports/tests/fixtures.py b/libs/shared/shared/django_apps/reports/tests/fixtures.py new file mode 100644 index 0000000000..32b15209c3 --- /dev/null +++ b/libs/shared/shared/django_apps/reports/tests/fixtures.py @@ -0,0 +1,80 @@ +import pytest + +from shared.django_apps.core.tests.factories import ( + RepositoryFactory, +) +from shared.django_apps.reports.models import Test, TestInstance +from shared.django_apps.reports.tests.factories import UploadFactory + + +@pytest.fixture +def repo_fixture(): + return RepositoryFactory() + + +@pytest.fixture +def upload_fixture(): + return UploadFactory() + + +@pytest.fixture +def create_upload_func(): + def create_upload(): + return UploadFactory() + + return create_upload + + +@pytest.fixture +def create_test_func(repo_fixture): + test_i = 0 + + def create_test(): + nonlocal test_i + test_id = f"test_{test_i}" + test = Test( + id=test_id, + repository=repo_fixture, + testsuite="testsuite", + name=f"test_{test_i}", + flags_hash="", + ) + test.save() + test_i = test_i + 1 + + return test + + return create_test + + +@pytest.fixture +def create_test_instance_func(repo_fixture, upload_fixture): + def create_test_instance( + test, + outcome, + commitid=None, + branch=None, + repoid=None, + upload=upload_fixture, + duration=0, + created_at=None, + ): + ti = TestInstance( + test=test, + repoid=repo_fixture.repoid, + outcome=outcome, + upload=upload, + duration_seconds=duration, + ) + if created_at: + ti.created_at = created_at + if branch: + ti.branch = branch + if commitid: + ti.commitid = commitid + if repoid: + ti.repoid = repoid + ti.save() + return ti + + return create_test_instance diff --git a/libs/shared/shared/django_apps/rollouts/__init__.py b/libs/shared/shared/django_apps/rollouts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/rollouts/apps.py b/libs/shared/shared/django_apps/rollouts/apps.py new file mode 100644 index 0000000000..0ba1b6c68e --- /dev/null +++ b/libs/shared/shared/django_apps/rollouts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RolloutsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "shared.django_apps.rollouts" diff --git a/libs/shared/shared/django_apps/rollouts/migrations/0001_initial.py b/libs/shared/shared/django_apps/rollouts/migrations/0001_initial.py new file mode 100644 index 0000000000..466a1cdf79 --- /dev/null +++ b/libs/shared/shared/django_apps/rollouts/migrations/0001_initial.py @@ -0,0 +1,98 @@ +# Generated by Django 4.2.7 on 2024-02-26 18:57 + +import django.db.models.deletion +import django_better_admin_arrayfield.models.fields +from django.db import migrations, models + +import shared.django_apps.rollouts.models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + # BEGIN; + # -- + # -- Create model FeatureFlag + # -- + # CREATE TABLE "feature_flags" ("name" varchar(200) NOT NULL PRIMARY KEY, "proportion" decimal NOT NULL, "salt" varchar(32) NOT NULL); + # -- + # -- Create model FeatureFlagVariant + # -- + # CREATE TABLE "feature_flag_variants" ("variant_id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(200) NOT NULL, "proportion" decimal NOT NULL, "value" text NOT NULL CHECK ((JSON_VALID("value") OR "value" IS NULL)), "override_owner_ids" integer[] NOT NULL, "override_repo_ids" integer[] NOT NULL, "feature_flag_id" varchar(200) NOT NULL REFERENCES "feature_flags" ("name") DEFERRABLE INITIALLY DEFERRED); + # CREATE INDEX "feature_flag_variants_feature_flag_id_fa3a4c02" ON "feature_flag_variants" ("feature_flag_id"); + # CREATE INDEX "feature_fla_feature_15a078_idx" ON "feature_flag_variants" ("feature_flag_id"); + # COMMIT; + + operations = [ + migrations.CreateModel( + name="FeatureFlag", + fields=[ + ( + "name", + models.CharField(max_length=200, primary_key=True, serialize=False), + ), + ( + "proportion", + models.DecimalField(decimal_places=3, default=0, max_digits=4), + ), + ( + "salt", + models.CharField( + default=shared.django_apps.rollouts.models.default_random_salt, + max_length=32, + ), + ), + ], + options={ + "db_table": "feature_flags", + }, + ), + migrations.CreateModel( + name="FeatureFlagVariant", + fields=[ + ("variant_id", models.AutoField(primary_key=True, serialize=False)), + ("name", models.CharField(max_length=200)), + ( + "proportion", + models.DecimalField(decimal_places=3, default=0, max_digits=4), + ), + ("value", models.JSONField(default=False)), + ( + "override_owner_ids", + django_better_admin_arrayfield.models.fields.ArrayField( + base_field=models.IntegerField(), + blank=True, + default=list, + size=None, + ), + ), + ( + "override_repo_ids", + django_better_admin_arrayfield.models.fields.ArrayField( + base_field=models.IntegerField(), + blank=True, + default=list, + size=None, + ), + ), + ( + "feature_flag", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="variants", + to="rollouts.featureflag", + ), + ), + ], + options={ + "db_table": "feature_flag_variants", + "indexes": [ + models.Index( + fields=["feature_flag"], name="feature_fla_feature_15a078_idx" + ) + ], + }, + ), + ] diff --git a/libs/shared/shared/django_apps/rollouts/migrations/0002_auto_20240226_1858.py b/libs/shared/shared/django_apps/rollouts/migrations/0002_auto_20240226_1858.py new file mode 100644 index 0000000000..bb6be2cc5d --- /dev/null +++ b/libs/shared/shared/django_apps/rollouts/migrations/0002_auto_20240226_1858.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.7 on 2024-02-26 18:58 + +from django.db import migrations + + +def seed_initial_features(apps, schema_editor): + FeatureFlag = apps.get_model("rollouts", "FeatureFlag") + FeatureFlagVariant = apps.get_model("rollouts", "FeatureFlagVariant") + + list_repos_generator = FeatureFlag.objects.create( + name="list_repos_generator", proportion=0.0 + ) + FeatureFlagVariant.objects.create( + name="enabled", + feature_flag=list_repos_generator, + proportion=1.0, + value=True, + ) + + use_label_index_in_report_processing = FeatureFlag.objects.create( + name="use_label_index_in_report_processing", proportion=0.0 + ) + FeatureFlagVariant.objects.create( + name="enabled", + feature_flag=use_label_index_in_report_processing, + proportion=1.0, + value=True, + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("rollouts", "0001_initial"), + ] + + operations = [migrations.RunPython(seed_initial_features)] diff --git a/libs/shared/shared/django_apps/rollouts/migrations/0003_alter_featureflag_proportion_and_more.py b/libs/shared/shared/django_apps/rollouts/migrations/0003_alter_featureflag_proportion_and_more.py new file mode 100644 index 0000000000..9a8f32a5da --- /dev/null +++ b/libs/shared/shared/django_apps/rollouts/migrations/0003_alter_featureflag_proportion_and_more.py @@ -0,0 +1,55 @@ +# Generated by Django 4.2.7 on 2024-03-01 16:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("rollouts", "0002_auto_20240226_1858"), + ] + + # BEGIN; + # -- + # -- Alter field proportion on featureflag + # -- + # -- (no-op) + # -- + # -- Alter field proportion on featureflagvariant + # -- + # -- (no-op) + # -- + # -- Alter field value on featureflagvariant + # -- + # -- (no-op) + # COMMIT; + + operations = [ + migrations.AlterField( + model_name="featureflag", + name="proportion", + field=models.DecimalField( + decimal_places=3, + default=0, + help_text="Values are between 0 and 1. Eg: 0.5 means 50% of users", + max_digits=4, + ), + ), + migrations.AlterField( + model_name="featureflagvariant", + name="proportion", + field=models.DecimalField( + decimal_places=3, + default=0, + help_text="Values are between 0 and 1. Eg: 0.5 means 50% of users. The sum of all variants' proportions for a feature should equal to 1.", + max_digits=4, + ), + ), + migrations.AlterField( + model_name="featureflagvariant", + name="value", + field=models.JSONField( + default=False, + help_text="Accepts JSON values. Eg: `true`, `false`, `10`, `['abc', 'def']`, `{'k': 'v'}`", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/rollouts/migrations/0004_featureexposure.py b/libs/shared/shared/django_apps/rollouts/migrations/0004_featureexposure.py new file mode 100644 index 0000000000..a17258856d --- /dev/null +++ b/libs/shared/shared/django_apps/rollouts/migrations/0004_featureexposure.py @@ -0,0 +1,53 @@ +# Generated by Django 5.0.3 on 2024-03-12 20:06 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("rollouts", "0003_alter_featureflag_proportion_and_more"), + ] + + # BEGIN; + # -- + # -- Create model FeatureExposure + # -- + # CREATE TABLE "feature_exposures" ("exposure_id" integer NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "owner" integer NULL, "repo" integer NULL, "timestamp" timestamp with time zone NOT NULL, "feature_flag_id" varchar(200) NOT NULL, "feature_flag_variant_id" integer NOT NULL); + # ALTER TABLE "feature_exposures" ADD CONSTRAINT "feature_exposures_feature_flag_id_628f8212_fk_feature_f" FOREIGN KEY ("feature_flag_id") REFERENCES "feature_flags" ("name") DEFERRABLE INITIALLY DEFERRED; + # ALTER TABLE "feature_exposures" ADD CONSTRAINT "feature_exposures_feature_flag_variant_bfb854ff_fk_feature_f" FOREIGN KEY ("feature_flag_variant_id") REFERENCES "feature_flag_variants" ("variant_id") DEFERRABLE INITIALLY DEFERRED; + # CREATE INDEX "feature_exposures_feature_flag_id_628f8212" ON "feature_exposures" ("feature_flag_id"); + # CREATE INDEX "feature_exposures_feature_flag_id_628f8212_like" ON "feature_exposures" ("feature_flag_id" varchar_pattern_ops); + # CREATE INDEX "feature_exposures_feature_flag_variant_id_bfb854ff" ON "feature_exposures" ("feature_flag_variant_id"); + # COMMIT; + + operations = [ + migrations.CreateModel( + name="FeatureExposure", + fields=[ + ("exposure_id", models.AutoField(primary_key=True, serialize=False)), + ("owner", models.IntegerField(blank=True, null=True)), + ("repo", models.IntegerField(blank=True, null=True)), + ("timestamp", models.DateTimeField()), + ( + "feature_flag", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="exposures", + to="rollouts.featureflag", + ), + ), + ( + "feature_flag_variant", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="exposures", + to="rollouts.featureflagvariant", + ), + ), + ], + options={ + "db_table": "feature_exposures", + }, + ), + ] diff --git a/libs/shared/shared/django_apps/rollouts/migrations/0005_featureflag_is_active_featureflag_platform_and_more.py b/libs/shared/shared/django_apps/rollouts/migrations/0005_featureflag_is_active_featureflag_platform_and_more.py new file mode 100644 index 0000000000..c881c48a57 --- /dev/null +++ b/libs/shared/shared/django_apps/rollouts/migrations/0005_featureflag_is_active_featureflag_platform_and_more.py @@ -0,0 +1,84 @@ +# Generated by Django 4.2.11 on 2024-05-01 14:14 + +import django_better_admin_arrayfield.models.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("rollouts", "0004_featureexposure"), + ] + + # `BEGIN; + # -- + # -- Add field is_active to featureflag + # -- + # ALTER TABLE "feature_flags" ADD COLUMN "is_active" boolean DEFAULT true NOT NULL; + # ALTER TABLE "feature_flags" ALTER COLUMN "is_active" DROP DEFAULT; + # -- + # -- Add field platform to featureflag + # -- + # ALTER TABLE "feature_flags" ADD COLUMN "platform" varchar(1) DEFAULT 'B' NOT NULL; + # ALTER TABLE "feature_flags" ALTER COLUMN "platform" DROP DEFAULT; + # -- + # -- Add field rollout_universe to featureflag + # -- + # ALTER TABLE "feature_flags" ADD COLUMN "rollout_universe" varchar(30) DEFAULT 'OWNER_ID' NOT NULL; + # ALTER TABLE "feature_flags" ALTER COLUMN "rollout_universe" DROP DEFAULT; + # -- + # -- Add field override_emails to featureflagvariant + # -- + # ALTER TABLE "feature_flag_variants" ADD COLUMN "override_emails" varchar[] DEFAULT '{}' NOT NULL; + # ALTER TABLE "feature_flag_variants" ALTER COLUMN "override_emails" DROP DEFAULT; + # -- + # -- Add field override_org_ids to featureflagvariant + # -- + # ALTER TABLE "feature_flag_variants" ADD COLUMN "override_org_ids" integer[] DEFAULT '{}' NOT NULL; + # ALTER TABLE "feature_flag_variants" ALTER COLUMN "override_org_ids" DROP DEFAULT; + # COMMIT; + + operations = [ + migrations.AddField( + model_name="featureflag", + name="is_active", + field=models.BooleanField( + default=True, + help_text="This should be on if the experiment is currently running. Otherwise turn it off if the experiment has finished and is cleaned up", + ), + ), + migrations.AddField( + model_name="featureflag", + name="platform", + field=models.CharField( + choices=[("F", "Frontend"), ("B", "Backend")], default="B", max_length=1 + ), + ), + migrations.AddField( + model_name="featureflag", + name="rollout_universe", + field=models.CharField( + choices=[ + ("OWNER_ID", "Owner ID"), + ("REPO_ID", "Repo ID"), + ("ORG_ID", "Org ID"), + ("EMAIL", "Email"), + ], + default="OWNER_ID", + max_length=30, + ), + ), + migrations.AddField( + model_name="featureflagvariant", + name="override_emails", + field=django_better_admin_arrayfield.models.fields.ArrayField( + base_field=models.CharField(), blank=True, default=list, size=None + ), + ), + migrations.AddField( + model_name="featureflagvariant", + name="override_org_ids", + field=django_better_admin_arrayfield.models.fields.ArrayField( + base_field=models.IntegerField(), blank=True, default=list, size=None + ), + ), + ] diff --git a/libs/shared/shared/django_apps/rollouts/migrations/__init__.py b/libs/shared/shared/django_apps/rollouts/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/rollouts/models.py b/libs/shared/shared/django_apps/rollouts/models.py new file mode 100644 index 0000000000..5939400312 --- /dev/null +++ b/libs/shared/shared/django_apps/rollouts/models.py @@ -0,0 +1,160 @@ +from django.core.exceptions import ValidationError +from django.db import models +from django_better_admin_arrayfield.models.fields import ArrayField + + +# Defines whether flag is a front/back end feature flag. Backend +# flags benefit from experimentation via `telemetry_simple` SQL metrics +class Platform(models.TextChoices): + FRONTEND = "F", "Frontend" + BACKEND = "B", "Backend" + + +# Defines the possible identifiers you can perform a rollout over. Users +# who share the same identifier will always be assigned the same variant. +# EG: two users from the same org will receive the same variant when rolling +# out over ORG_ID +class RolloutUniverse(models.TextChoices): + OWNER_ID = "OWNER_ID", "Owner ID" + REPO_ID = "REPO_ID", "Repo ID" + ORG_ID = "ORG_ID", "Org ID" + EMAIL = "EMAIL", "Email" + + +def default_random_salt(): + # to resolve circular dependency + from shared.django_apps.utils.rollout_utils import default_random_salt + + return default_random_salt() + + +class FeatureFlag(models.Model): + """ + Represents a feature and its rollout parameters (see shared/rollouts/__init__.py). A + default salt will be created if one is not provided. + """ + + name = models.CharField(max_length=200, primary_key=True) + proportion = models.DecimalField( + default=0, + decimal_places=3, + max_digits=4, + help_text="Values are between 0 and 1. Eg: 0.5 means 50% of users", + ) + salt = models.CharField(max_length=32, default=default_random_salt) + platform = models.CharField( + max_length=1, choices=Platform.choices, default=Platform.BACKEND + ) + # Represents if an experiment has been cleaned up and + # is no longer running anymore + is_active = models.BooleanField( + default=True, + help_text="This should be on if the experiment is currently running. Otherwise turn it off if the experiment has finished and is cleaned up", + ) + + # The field we're rolling out over. Users with the same identifier + # will always receive the same variant. EG: if you rollout over org_id, + # then users in the same org see the same variant + rollout_universe = models.CharField( + max_length=30, + choices=RolloutUniverse.choices, + default=RolloutUniverse.OWNER_ID, + ) + + class Meta: + db_table = "feature_flags" + + def __str__(self): + return self.name + + +class FeatureFlagVariant(models.Model): + """ + Represents a variant of the feature being rolled out and the proportion of + the test population it should be rolled out to (see shared/rollouts/__init__.py). + The proportion should be a float between 0 and 1. A proportion of 0.5 means 50% of + the test population should receive this variant. Ensure that for any `FeatureFlag`, + the proportions of the corresponding `FeatureFlagVariant`s sum to 1. + """ + + variant_id = models.AutoField(primary_key=True) + name = models.CharField(max_length=200) + feature_flag = models.ForeignKey( + "FeatureFlag", on_delete=models.CASCADE, related_name="variants" + ) + proportion = models.DecimalField( + default=0, + decimal_places=3, + max_digits=4, + help_text="Values are between 0 and 1. Eg: 0.5 means 50% of users. The sum of all variants' proportions for a feature should equal to 1.", + ) + value = models.JSONField( + default=False, + help_text="Accepts JSON values. Eg: `true`, `false`, `10`, `['abc', 'def']`, `{'k': 'v'}`", + ) + + # Weak foreign keys to Owner and Respository models respectively. These + # same fields are also referenced for `telemetry_simple` metrics, and are + # connected via FeatureExposure to allow for experimentation. + override_owner_ids = ArrayField( + base_field=models.IntegerField(), default=list, blank=True + ) + override_repo_ids = ArrayField( + base_field=models.IntegerField(), default=list, blank=True + ) + + # Email field of Owner model + override_emails = ArrayField( + base_field=models.CharField(), default=list, blank=True + ) + # Foreign key to Owner model (orgs and users are both Owner model) + override_org_ids = ArrayField( + base_field=models.IntegerField(), default=list, blank=True + ) + + class Meta: + db_table = "feature_flag_variants" + indexes = [models.Index(fields=["feature_flag"])] + + def __str__(self): + return self.feature_flag.__str__() + ": " + self.name + + +class FeatureExposure(models.Model): + """ + Represents a feature variant being exposed to an entity (repo or owner) at + a point in time. Used to keep track of when features and variants have been enabled + and who they affected for experimentation purposes. + """ + + exposure_id = models.AutoField(primary_key=True) + feature_flag = models.ForeignKey( + "FeatureFlag", on_delete=models.CASCADE, related_name="exposures" + ) + feature_flag_variant = models.ForeignKey( + "FeatureFlagVariant", on_delete=models.CASCADE, related_name="exposures" + ) + + # Weak foreign keys to Owner and Respository models respectively + owner = models.IntegerField(null=True, blank=True) + repo = models.IntegerField(null=True, blank=True) + + timestamp = models.DateTimeField(null=False) + + def clean(self): + if not self.owner and not self.repo: + raise ValidationError( + "Exposure must have either a corresponding owner or repo" + ) + + super(FeatureExposure, self).clean() + + def save(self, *args, **kwargs): + self.full_clean() + super(FeatureExposure, self).save(*args, **kwargs) + + class Meta: + db_table = "feature_exposures" + # indexes = [ # don't use indexes for now + # models.Index(fields=['feature_flag', 'timestamp'], name='feature_flag_timestamp_idx'), + # ] diff --git a/libs/shared/shared/django_apps/staticanalysis/__init__.py b/libs/shared/shared/django_apps/staticanalysis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/staticanalysis/migrations/0001_initial.py b/libs/shared/shared/django_apps/staticanalysis/migrations/0001_initial.py new file mode 100644 index 0000000000..92a620184f --- /dev/null +++ b/libs/shared/shared/django_apps/staticanalysis/migrations/0001_initial.py @@ -0,0 +1,97 @@ +# Generated by Django 3.2.12 on 2022-08-06 17:25 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("core", "0014_pull_pulls_author_updatestamp"), + ] + + operations = [ + migrations.CreateModel( + name="StaticAnalysisSingleFileSnapshot", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("file_hash", models.UUIDField()), + ("content_location", models.TextField()), + ( + "state_id", + models.IntegerField( + choices=[(1, "created"), (2, "valid"), (3, "rejected")] + ), + ), + ( + "repository", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="core.repository", + ), + ), + ], + ), + migrations.CreateModel( + name="StaticAnalysisSuite", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "commit", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.commit" + ), + ), + ], + ), + migrations.CreateModel( + name="StaticAnalysisSuiteFilepath", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("external_id", models.UUIDField(default=uuid.uuid4, editable=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("filepath", models.TextField()), + ( + "analysis_suite", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="filepaths", + to="staticanalysis.staticanalysissuite", + ), + ), + ( + "file_snapshot", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="filepaths", + to="staticanalysis.staticanalysissinglefilesnapshot", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddConstraint( + model_name="staticanalysissuite", + constraint=models.UniqueConstraint( + fields=("external_id",), name="staticanalysis_external_id_uniq" + ), + ), + migrations.AddConstraint( + model_name="staticanalysissinglefilesnapshot", + constraint=models.UniqueConstraint( + fields=("repository", "file_hash"), name="staticanalysis_repo_filehash" + ), + ), + ] diff --git a/libs/shared/shared/django_apps/staticanalysis/migrations/__init__.py b/libs/shared/shared/django_apps/staticanalysis/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/staticanalysis/models.py b/libs/shared/shared/django_apps/staticanalysis/models.py new file mode 100644 index 0000000000..93e60b1632 --- /dev/null +++ b/libs/shared/shared/django_apps/staticanalysis/models.py @@ -0,0 +1,71 @@ +from django.db import models +from django_prometheus.models import ExportModelOperationsMixin + +from shared.django_apps.codecov.models import BaseCodecovModel +from shared.staticanalysis import StaticAnalysisSingleFileSnapshotState + +# Added to avoid 'doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS' error\ +# Needs to be called the same as the API app +STATICANALYSIS_APP_LABEL = "staticanalysis" + + +class StaticAnalysisSuite( + ExportModelOperationsMixin("staticanalysis.static_analysis_suite"), BaseCodecovModel +): + commit = models.ForeignKey("core.Commit", on_delete=models.CASCADE) + + class Meta: + app_label = STATICANALYSIS_APP_LABEL + db_table = "staticanalysis_staticanalysissuite" + + constraints = [ + models.UniqueConstraint( + fields=["external_id"], name="staticanalysis_external_id_uniq" + ), + ] + + +class StaticAnalysisSingleFileSnapshot( + ExportModelOperationsMixin("staticanalysis.static_analysis_single_file_snapshot"), + BaseCodecovModel, +): + repository = models.ForeignKey("core.Repository", on_delete=models.CASCADE) + file_hash = models.UUIDField(null=False) + content_location = models.TextField() + state_id = models.IntegerField( + choices=StaticAnalysisSingleFileSnapshotState.choices() + ) + + class Meta: + app_label = STATICANALYSIS_APP_LABEL + db_table = "staticanalysis_staticanalysissinglefilesnapshot" + + constraints = [ + models.UniqueConstraint( + fields=["repository", "file_hash"], name="staticanalysis_repo_filehash" + ), + ] + + +class StaticAnalysisSuiteFilepath( + ExportModelOperationsMixin("staticanalysis.static_analysis_suite_filepath"), + BaseCodecovModel, +): + analysis_suite = models.ForeignKey( + StaticAnalysisSuite, on_delete=models.CASCADE, related_name="filepaths" + ) + file_snapshot = models.ForeignKey( + StaticAnalysisSingleFileSnapshot, + on_delete=models.CASCADE, + related_name="filepaths", + ) + filepath = models.TextField() + + class Meta: + app_label = STATICANALYSIS_APP_LABEL + db_table = "staticanalysis_staticanalysissuitefilepath" + + @property + def file_hash(self): + # TODO: double check so serializer doesnt get N + 1 queries + return self.file_snapshot.file_hash diff --git a/libs/shared/shared/django_apps/staticanalysis/tests/__init__.py b/libs/shared/shared/django_apps/staticanalysis/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/staticanalysis/tests/factories.py b/libs/shared/shared/django_apps/staticanalysis/tests/factories.py new file mode 100644 index 0000000000..dcc5fd4523 --- /dev/null +++ b/libs/shared/shared/django_apps/staticanalysis/tests/factories.py @@ -0,0 +1,36 @@ +from uuid import uuid4 + +import factory +from factory.django import DjangoModelFactory + +from shared.django_apps.core.tests.factories import CommitFactory, RepositoryFactory +from shared.django_apps.staticanalysis.models import ( + StaticAnalysisSingleFileSnapshot, + StaticAnalysisSuite, + StaticAnalysisSuiteFilepath, +) + + +class StaticAnalysisSuiteFactory(DjangoModelFactory): + class Meta: + model = StaticAnalysisSuite + + commit = factory.SubFactory(CommitFactory) + + +class StaticAnalysisSingleFileSnapshotFactory(DjangoModelFactory): + class Meta: + model = StaticAnalysisSingleFileSnapshot + + repository = factory.SubFactory(RepositoryFactory) + file_hash = factory.LazyFunction(lambda: uuid4().hex) + content_location = "a/b/c.txt" + state_id = 1 + + +class StaticAnalysisSuiteFilepathFactory(DjangoModelFactory): + class Meta: + model = StaticAnalysisSuiteFilepath + + file_snapshot = factory.SubFactory(StaticAnalysisSingleFileSnapshotFactory) + analysis_suite = factory.SubFactory(StaticAnalysisSuiteFactory) diff --git a/libs/shared/shared/django_apps/ta_timeseries/__init__.py b/libs/shared/shared/django_apps/ta_timeseries/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/ta_timeseries/migrations/0001_initial.py b/libs/shared/shared/django_apps/ta_timeseries/migrations/0001_initial.py new file mode 100644 index 0000000000..86e87838fe --- /dev/null +++ b/libs/shared/shared/django_apps/ta_timeseries/migrations/0001_initial.py @@ -0,0 +1,108 @@ +# Generated by Django 4.2.16 on 2025-02-06 15:02 + +import django.contrib.postgres.fields +import django_prometheus.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Testrun", + fields=[ + ("timestamp", models.DateTimeField(primary_key=True, serialize=False)), + ("repo_id", models.BigIntegerField()), + ("test_id", models.BinaryField()), + ("testsuite", models.TextField(null=True)), + ("classname", models.TextField(null=True)), + ("name", models.TextField(null=True)), + ("computed_name", models.TextField(null=True)), + ("outcome", models.TextField()), + ("duration_seconds", models.FloatField(null=True)), + ("failure_message", models.TextField(null=True)), + ("framework", models.TextField(null=True)), + ("filename", models.TextField(null=True)), + ("commit_sha", models.TextField(null=True)), + ("branch", models.TextField(null=True)), + ( + "flags", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), null=True, size=None + ), + ), + ("upload_id", models.BigIntegerField(null=True)), + ], + bases=( + django_prometheus.models.ExportModelOperationsMixin( + "ta_timeseries.testrun" + ), + models.Model, + ), + ), + migrations.RunSQL( + "ALTER TABLE ta_timeseries_testrun DROP CONSTRAINT ta_timeseries_testrun_pkey;", + reverse_sql="", + ), + migrations.RunSQL( + "SELECT create_hypertable('ta_timeseries_testrun', 'timestamp');", + reverse_sql="", + ), + migrations.AddIndex( + model_name="testrun", + index=models.Index( + fields=[ + "repo_id", + "branch", + "timestamp", + ], + name="ta_ts__branch_i", + ), + ), + migrations.AddIndex( + model_name="testrun", + index=models.Index( + fields=["repo_id", "branch", "test_id", "timestamp"], + name="ta_ts__branch_test_i", + ), + ), + migrations.AddIndex( + model_name="testrun", + index=models.Index( + fields=["repo_id", "test_id", "timestamp"], + name="ta_ts__test_i", + ), + ), + migrations.AddIndex( + model_name="testrun", + index=models.Index( + fields=["repo_id", "commit_sha", "timestamp"], + name="ta_ts__commit_i", + ), + ), + migrations.RunSQL( + """ + CREATE OR REPLACE FUNCTION array_merge_dedup(anyarray, anyarray) + RETURNS anyarray LANGUAGE sql IMMUTABLE AS $$ + SELECT array_agg(DISTINCT x) + FROM ( + SELECT unnest($1) as x + UNION + SELECT unnest($2) + ) s; + $$; + CREATE OR REPLACE AGGREGATE array_merge_dedup_agg(anyarray) ( + SFUNC = array_merge_dedup, + STYPE = anyarray, + INITCOND = '{}' + ); + """, + reverse_sql=""" + DROP AGGREGATE array_merge_dedup_agg(anyarray); + DROP FUNCTION array_merge_dedup(anyarray, anyarray); + """, + ), + ] diff --git a/libs/shared/shared/django_apps/ta_timeseries/migrations/0002_testrun_summary_1day.py b/libs/shared/shared/django_apps/ta_timeseries/migrations/0002_testrun_summary_1day.py new file mode 100644 index 0000000000..b38ae52efc --- /dev/null +++ b/libs/shared/shared/django_apps/ta_timeseries/migrations/0002_testrun_summary_1day.py @@ -0,0 +1,71 @@ +# Generated by Django 4.2.16 on 2025-02-06 16:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + # cant create this views in a transaction + atomic = False + + dependencies = [ + ("ta_timeseries", "0001_initial"), + ] + + operations = [ + migrations.RunSQL( + """ + create materialized view ta_timeseries_testrun_summary_1day + with (timescaledb.continuous) as + select + repo_id, + testsuite, + classname, + name, + time_bucket(interval '1 days', timestamp) as timestamp_bin, + + min(computed_name) as computed_name, + COUNT(DISTINCT CASE WHEN outcome = 'failure' OR outcome = 'flaky_fail' THEN commit_sha ELSE NULL END) AS failing_commits, + last(duration_seconds, timestamp) as last_duration_seconds, + avg(duration_seconds) as avg_duration_seconds, + COUNT(*) FILTER (WHERE outcome = 'pass') AS pass_count, + COUNT(*) FILTER (WHERE outcome = 'failure') AS fail_count, + COUNT(*) FILTER (WHERE outcome = 'skip') AS skip_count, + COUNT(*) FILTER (WHERE outcome = 'flaky_fail') AS flaky_fail_count, + MAX(timestamp) AS updated_at, + array_merge_dedup_agg(flags) as flags + from ta_timeseries_testrun + group by + repo_id, testsuite, classname, name, timestamp_bin; + """, + reverse_sql="drop materialized view ta_timeseries_testrun_summary_1day;", + ), + migrations.RunSQL( + """ + create materialized view ta_timeseries_testrun_branch_summary_1day + with (timescaledb.continuous) as + select + repo_id, + branch, + testsuite, + classname, + name, + time_bucket(interval '1 days', timestamp) as timestamp_bin, + + min(computed_name) as computed_name, + COUNT(DISTINCT CASE WHEN outcome = 'failure' OR outcome = 'flaky_fail' THEN commit_sha ELSE NULL END) AS failing_commits, + last(duration_seconds, timestamp) as last_duration_seconds, + avg(duration_seconds) as avg_duration_seconds, + COUNT(*) FILTER (WHERE outcome = 'pass') AS pass_count, + COUNT(*) FILTER (WHERE outcome = 'failure') AS fail_count, + COUNT(*) FILTER (WHERE outcome = 'skip') AS skip_count, + COUNT(*) FILTER (WHERE outcome = 'flaky_fail') AS flaky_fail_count, + MAX(timestamp) AS updated_at, + array_merge_dedup_agg(flags) as flags + from ta_timeseries_testrun + where branch in ('main', 'master', 'develop') + group by + repo_id, branch, testsuite, classname, name, timestamp_bin; + """, + reverse_sql="drop materialized view ta_timeseries_testrun_branch_summary_1day;", + ), + ] diff --git a/libs/shared/shared/django_apps/ta_timeseries/migrations/0003_testrun_cagg_policy.py b/libs/shared/shared/django_apps/ta_timeseries/migrations/0003_testrun_cagg_policy.py new file mode 100644 index 0000000000..43f0b993aa --- /dev/null +++ b/libs/shared/shared/django_apps/ta_timeseries/migrations/0003_testrun_cagg_policy.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.16 on 2025-02-06 16:57 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("ta_timeseries", "0002_testrun_summary_1day"), + ] + + operations = [ + migrations.RunSQL( + """ + select add_continuous_aggregate_policy( + 'ta_timeseries_testrun_summary_1day', + start_offset => '7 days', + end_offset => '1 days', + schedule_interval => INTERVAL '1 days' + ); + """, + reverse_sql="select remove_continuous_aggregate_policy('ta_timeseries_testrun_summary_1day');", + ), + migrations.RunSQL( + """ + select add_continuous_aggregate_policy( + 'ta_timeseries_testrun_branch_summary_1day', + start_offset => '7 days', + end_offset => '1 days', + schedule_interval => INTERVAL '1 days' + ); + """, + reverse_sql="select remove_continuous_aggregate_policy('ta_timeseries_testrun_branch_summary_1day');", + ), + ] diff --git a/libs/shared/shared/django_apps/ta_timeseries/migrations/0004_testrun_summary_model.py b/libs/shared/shared/django_apps/ta_timeseries/migrations/0004_testrun_summary_model.py new file mode 100644 index 0000000000..3ec3e33642 --- /dev/null +++ b/libs/shared/shared/django_apps/ta_timeseries/migrations/0004_testrun_summary_model.py @@ -0,0 +1,90 @@ +# Generated by Django 4.2.16 on 2025-02-06 16:57 +import django.contrib.postgres.fields +import django_prometheus.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("ta_timeseries", "0003_testrun_cagg_policy"), + ] + + operations = [ + migrations.CreateModel( + name="TestrunSummary", + fields=[ + ( + "timestamp_bin", + models.DateTimeField(primary_key=True, serialize=False), + ), + ("repo_id", models.IntegerField()), + ("name", models.TextField()), + ("classname", models.TextField()), + ("testsuite", models.TextField()), + ("computed_name", models.TextField()), + ("failing_commits", models.IntegerField()), + ("avg_duration_seconds", models.FloatField()), + ("last_duration_seconds", models.FloatField()), + ("pass_count", models.IntegerField()), + ("fail_count", models.IntegerField()), + ("skip_count", models.IntegerField()), + ("flaky_fail_count", models.IntegerField()), + ("updated_at", models.DateTimeField()), + ( + "flags", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), null=True, size=None + ), + ), + ], + options={ + "db_table": "ta_timeseries_testrun_summary_1day", + "managed": False, + }, + bases=( + django_prometheus.models.ExportModelOperationsMixin( + "ta_timeseries.testrun_summary" + ), + models.Model, + ), + ), + migrations.CreateModel( + name="TestrunBranchSummary", + fields=[ + ( + "timestamp_bin", + models.DateTimeField(primary_key=True, serialize=False), + ), + ("repo_id", models.IntegerField()), + ("branch", models.TextField()), + ("name", models.TextField()), + ("classname", models.TextField()), + ("testsuite", models.TextField()), + ("computed_name", models.TextField()), + ("failing_commits", models.IntegerField()), + ("avg_duration_seconds", models.FloatField()), + ("last_duration_seconds", models.FloatField()), + ("pass_count", models.IntegerField()), + ("fail_count", models.IntegerField()), + ("skip_count", models.IntegerField()), + ("flaky_fail_count", models.IntegerField()), + ("updated_at", models.DateTimeField()), + ( + "flags", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), null=True, size=None + ), + ), + ], + options={ + "db_table": "ta_timeseries_testrun_branch_summary_1day", + "managed": False, + }, + bases=( + django_prometheus.models.ExportModelOperationsMixin( + "ta_timeseries.testrun_branch_summary" + ), + models.Model, + ), + ), + ] diff --git a/libs/shared/shared/django_apps/ta_timeseries/migrations/0005_testrun_real_time_agg.py b/libs/shared/shared/django_apps/ta_timeseries/migrations/0005_testrun_real_time_agg.py new file mode 100644 index 0000000000..b2c0311bcb --- /dev/null +++ b/libs/shared/shared/django_apps/ta_timeseries/migrations/0005_testrun_real_time_agg.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.16 on 2025-02-12 15:58 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("ta_timeseries", "0004_testrun_summary_model"), + ] + + operations = [ + migrations.RunSQL( + """ + alter materialized view ta_timeseries_testrun_summary_1day set (timescaledb.materialized_only = true); + """ + ) + if not settings.TIMESERIES_REAL_TIME_AGGREGATES + else migrations.RunSQL( + """ + alter materialized view ta_timeseries_testrun_summary_1day set (timescaledb.materialized_only = false); + """ + ), + migrations.RunSQL( + """ + alter materialized view ta_timeseries_testrun_branch_summary_1day set (timescaledb.materialized_only = true); + """ + ) + if not settings.TIMESERIES_REAL_TIME_AGGREGATES + else migrations.RunSQL( + """ + alter materialized view ta_timeseries_testrun_branch_summary_1day set (timescaledb.materialized_only = false); + """ + ), + ] diff --git a/libs/shared/shared/django_apps/ta_timeseries/migrations/__init__.py b/libs/shared/shared/django_apps/ta_timeseries/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/ta_timeseries/models.py b/libs/shared/shared/django_apps/ta_timeseries/models.py new file mode 100644 index 0000000000..25836d2e3d --- /dev/null +++ b/libs/shared/shared/django_apps/ta_timeseries/models.py @@ -0,0 +1,104 @@ +from django.contrib.postgres.fields import ArrayField +from django.db import models +from django_prometheus.models import ExportModelOperationsMixin + +TA_TIMESERIES_APP_LABEL = "ta_timeseries" + + +class Testrun(ExportModelOperationsMixin("ta_timeseries.testrun"), models.Model): + timestamp = models.DateTimeField(null=False, primary_key=True) + + test_id = models.BinaryField(null=False) + + name = models.TextField(null=True) + classname = models.TextField(null=True) + testsuite = models.TextField(null=True) + computed_name = models.TextField(null=True) + + outcome = models.TextField(null=False) + + duration_seconds = models.FloatField(null=True) + failure_message = models.TextField(null=True) + framework = models.TextField(null=True) + filename = models.TextField(null=True) + + repo_id = models.BigIntegerField(null=True) + commit_sha = models.TextField(null=True) + branch = models.TextField(null=True) + + flags = ArrayField(models.TextField(), null=True) + upload_id = models.BigIntegerField(null=True) + + class Meta: + app_label = TA_TIMESERIES_APP_LABEL + indexes = [ + models.Index( + name="ta_ts__branch_i", + fields=["repo_id", "branch", "timestamp"], + ), + models.Index( + name="ta_ts__branch_test_i", + fields=["repo_id", "branch", "test_id", "timestamp"], + ), + models.Index( + name="ta_ts__test_id_i", + fields=["repo_id", "test_id", "timestamp"], + ), + models.Index( + name="ta_ts__commit_i", + fields=["repo_id", "commit_sha", "timestamp"], + ), + ] + + +class TestrunBranchSummary( + ExportModelOperationsMixin("ta_timeseries.testrun_branch_summary"), + models.Model, +): + timestamp_bin = models.DateTimeField(primary_key=True) + repo_id = models.IntegerField() + branch = models.TextField() + name = models.TextField() + classname = models.TextField() + testsuite = models.TextField() + computed_name = models.TextField() + failing_commits = models.IntegerField() + avg_duration_seconds = models.FloatField() + last_duration_seconds = models.FloatField() + pass_count = models.IntegerField() + fail_count = models.IntegerField() + skip_count = models.IntegerField() + flaky_fail_count = models.IntegerField() + updated_at = models.DateTimeField() + flags = ArrayField(models.TextField(), null=True) + + class Meta: + app_label = TA_TIMESERIES_APP_LABEL + db_table = "ta_timeseries_testrun_branch_summary_1day" + managed = False + + +class TestrunSummary( + ExportModelOperationsMixin("ta_timeseries.testrun_summary"), + models.Model, +): + timestamp_bin = models.DateTimeField(primary_key=True) + repo_id = models.IntegerField() + name = models.TextField() + classname = models.TextField() + testsuite = models.TextField() + computed_name = models.TextField() + failing_commits = models.IntegerField() + avg_duration_seconds = models.FloatField() + last_duration_seconds = models.FloatField() + pass_count = models.IntegerField() + fail_count = models.IntegerField() + skip_count = models.IntegerField() + flaky_fail_count = models.IntegerField() + updated_at = models.DateTimeField() + flags = ArrayField(models.TextField(), null=True) + + class Meta: + app_label = TA_TIMESERIES_APP_LABEL + db_table = "ta_timeseries_testrun_summary_1day" + managed = False diff --git a/libs/shared/shared/django_apps/ta_timeseries/tests/factories.py b/libs/shared/shared/django_apps/ta_timeseries/tests/factories.py new file mode 100644 index 0000000000..1015dc8714 --- /dev/null +++ b/libs/shared/shared/django_apps/ta_timeseries/tests/factories.py @@ -0,0 +1,35 @@ +from datetime import datetime + +import factory +import factory.fuzzy +from factory.django import DjangoModelFactory + +from shared.django_apps.ta_timeseries import models + + +class TestrunFactory(DjangoModelFactory): + class Meta: + model = models.Testrun + + timestamp = datetime.now() + test_id = factory.Sequence(lambda n: f"test_{n}".encode()) + name = factory.Sequence(lambda n: f"test_{n}") + classname = factory.Sequence(lambda n: f"class_{n}") + testsuite = factory.Sequence(lambda n: f"suite_{n}") + computed_name = factory.Sequence(lambda n: f"computed_{n}") + outcome = factory.fuzzy.FuzzyChoice( + choices=["pass", "failure", "flaky_failure", "skip"] + ) + duration_seconds = factory.fuzzy.FuzzyFloat(low=0.0, high=100.0) + failure_message = factory.LazyAttribute( + lambda obj: f"failure_message_{obj.outcome}" + if obj.outcome == "failure" + else None + ) + framework = "Pytest" + filename = factory.Sequence(lambda n: f"test_{n}.py") + repo_id = 1 + commit_sha = "123" + branch = "main" + flags = [] + upload_id = 1 diff --git a/libs/shared/shared/django_apps/test_analytics/__init__.py b/libs/shared/shared/django_apps/test_analytics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/test_analytics/migrations/0001_initial.py b/libs/shared/shared/django_apps/test_analytics/migrations/0001_initial.py new file mode 100644 index 0000000000..8551573200 --- /dev/null +++ b/libs/shared/shared/django_apps/test_analytics/migrations/0001_initial.py @@ -0,0 +1,57 @@ +# Generated by Django 4.2.16 on 2025-01-17 22:07 + +from django.db import migrations, models + +""" +BEGIN; +-- +-- Create model Flake +-- +CREATE TABLE "test_analytics_flake" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "repoid" integer NOT NULL, "test_id" bytea NOT NULL, "recent_passes_count" integer NOT NULL, "count" integer NOT NULL, "fail_count" integer NOT NULL, "start_date" timestamp with time zone NOT NULL, "end_date" timestamp with time zone NULL); +CREATE INDEX "test_analyt_repoid_fcd881_idx" ON "test_analytics_flake" ("repoid"); +CREATE INDEX "test_analyt_test_id_f504a1_idx" ON "test_analytics_flake" ("test_id"); +CREATE INDEX "test_analyt_repoid_0690c3_idx" ON "test_analytics_flake" ("repoid", "test_id"); +CREATE INDEX "test_analyt_repoid_9e2402_idx" ON "test_analytics_flake" ("repoid", "end_date"); +COMMIT; +""" + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Flake", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("repoid", models.IntegerField()), + ("test_id", models.BinaryField()), + ("recent_passes_count", models.IntegerField()), + ("count", models.IntegerField()), + ("fail_count", models.IntegerField()), + ("start_date", models.DateTimeField()), + ("end_date", models.DateTimeField(null=True)), + ], + options={ + "db_table": "test_analytics_flake", + "indexes": [ + models.Index( + fields=["repoid"], name="test_analyt_repoid_fcd881_idx" + ), + models.Index( + fields=["test_id"], name="test_analyt_test_id_f504a1_idx" + ), + models.Index( + fields=["repoid", "test_id"], + name="test_analyt_repoid_0690c3_idx", + ), + models.Index( + fields=["repoid", "end_date"], + name="test_analyt_repoid_9e2402_idx", + ), + ], + }, + ), + ] diff --git a/libs/shared/shared/django_apps/test_analytics/migrations/0002_flake_flags_id.py b/libs/shared/shared/django_apps/test_analytics/migrations/0002_flake_flags_id.py new file mode 100644 index 0000000000..1689713a1f --- /dev/null +++ b/libs/shared/shared/django_apps/test_analytics/migrations/0002_flake_flags_id.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.16 on 2025-01-20 17:38 + +from django.db import migrations, models + +""" +BEGIN; +-- +-- Add field flags_id to flake +-- +ALTER TABLE "test_analytics_flake" ADD COLUMN "flags_id" bytea NULL; +COMMIT; +""" + + +class Migration(migrations.Migration): + dependencies = [ + ("test_analytics", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="flake", + name="flags_id", + field=models.BinaryField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/test_analytics/migrations/__init__.py b/libs/shared/shared/django_apps/test_analytics/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/test_analytics/models.py b/libs/shared/shared/django_apps/test_analytics/models.py new file mode 100644 index 0000000000..4d3010fc6d --- /dev/null +++ b/libs/shared/shared/django_apps/test_analytics/models.py @@ -0,0 +1,29 @@ +from django.db import models + +# Added to avoid 'doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS' error\ +# Needs to be called the same as the API app +TEST_ANALYTICS_APP_LABEL = "test_analytics" + + +class Flake(models.Model): + id = models.BigAutoField(primary_key=True) + + repoid = models.IntegerField() + test_id = models.BinaryField() + flags_id = models.BinaryField(null=True) + + recent_passes_count = models.IntegerField() + count = models.IntegerField() + fail_count = models.IntegerField() + start_date = models.DateTimeField() + end_date = models.DateTimeField(null=True) + + class Meta: + app_label = TEST_ANALYTICS_APP_LABEL + db_table = "test_analytics_flake" + indexes = [ + models.Index(fields=["repoid"]), + models.Index(fields=["test_id"]), + models.Index(fields=["repoid", "test_id"]), + models.Index(fields=["repoid", "end_date"]), + ] diff --git a/libs/shared/shared/django_apps/timeseries/__init__.py b/libs/shared/shared/django_apps/timeseries/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0001_initial.py b/libs/shared/shared/django_apps/timeseries/migrations/0001_initial.py new file mode 100644 index 0000000000..8a7e77314d --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# Generated by Django 3.1.13 on 2022-05-23 20:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Measurement", + fields=[ + ("timestamp", models.DateTimeField(primary_key=True, serialize=False)), + ("owner_id", models.BigIntegerField()), + ("repo_id", models.BigIntegerField()), + ("flag_id", models.BigIntegerField(null=True)), + ("branch", models.TextField(null=True)), + ("commit_sha", models.TextField(null=True)), + ("name", models.TextField()), + ("value", models.FloatField()), + ], + ), + migrations.RunSQL( + "ALTER TABLE timeseries_measurement DROP CONSTRAINT timeseries_measurement_pkey;", + reverse_sql="", + ), + migrations.AddIndex( + model_name="measurement", + index=models.Index( + fields=[ + "owner_id", + "repo_id", + "flag_id", + "branch", + "name", + "timestamp", + ], + name="timeseries__owner_i_2cc713_idx", + ), + ), + migrations.RunSQL( + "SELECT create_hypertable('timeseries_measurement', 'timestamp');", + reverse_sql="", + ), + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0002_continuous_aggregates.py b/libs/shared/shared/django_apps/timeseries/migrations/0002_continuous_aggregates.py new file mode 100644 index 0000000000..2cba7d1319 --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0002_continuous_aggregates.py @@ -0,0 +1,37 @@ +# Generated by Django 3.1.13 on 2022-05-23 20:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + # cant create this views in a transaction + atomic = False + + dependencies = [ + ("timeseries", "0001_initial"), + ] + + operations = [ + migrations.RunSQL( + f""" + create materialized view timeseries_measurement_summary_{days}day + with (timescaledb.continuous) as + select + owner_id, + repo_id, + flag_id, + branch, + name, + time_bucket(interval '{days} days', timestamp) as timestamp_bin, + avg(value) as value_avg, + max(value) as value_max, + min(value) as value_min, + count(value) as value_count + from timeseries_measurement + group by + owner_id, repo_id, flag_id, branch, name, timestamp_bin; + """, + reverse_sql=f"drop materialized view timeseries_measurement_summary_{days}day;", + ) + for days in [1, 7, 30] + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0003_cagg_policies.py b/libs/shared/shared/django_apps/timeseries/migrations/0003_cagg_policies.py new file mode 100644 index 0000000000..51d35611ec --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0003_cagg_policies.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1.13 on 2022-05-24 14:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("timeseries", "0002_continuous_aggregates"), + ] + + operations = [ + migrations.RunSQL( + f""" + select add_continuous_aggregate_policy( + 'timeseries_measurement_summary_{name}', + start_offset => NULL, + end_offset => NULL, + schedule_interval => INTERVAL '24 hours' + ); + """, + reverse_sql=f"select remove_continuous_aggregate_policy('timeseries_measurement_summary_{name}');", + ) + for name in ["1day", "7day", "30day"] + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0004_measurement_summaries.py b/libs/shared/shared/django_apps/timeseries/migrations/0004_measurement_summaries.py new file mode 100644 index 0000000000..c755684067 --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0004_measurement_summaries.py @@ -0,0 +1,84 @@ +# Generated by Django 3.1.13 on 2022-05-25 20:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("timeseries", "0003_cagg_policies"), + ] + + operations = [ + migrations.CreateModel( + name="MeasurementSummary1Day", + fields=[ + ( + "timestamp_bin", + models.DateTimeField(primary_key=True, serialize=False), + ), + ("owner_id", models.BigIntegerField()), + ("repo_id", models.BigIntegerField()), + ("flag_id", models.BigIntegerField()), + ("branch", models.TextField()), + ("name", models.TextField()), + ("value_avg", models.FloatField()), + ("value_max", models.FloatField()), + ("value_min", models.FloatField()), + ("value_count", models.FloatField()), + ], + options={ + "db_table": "timeseries_measurement_summary_1day", + "ordering": ["timestamp_bin"], + "abstract": False, + "managed": False, + }, + ), + migrations.CreateModel( + name="MeasurementSummary30Day", + fields=[ + ( + "timestamp_bin", + models.DateTimeField(primary_key=True, serialize=False), + ), + ("owner_id", models.BigIntegerField()), + ("repo_id", models.BigIntegerField()), + ("flag_id", models.BigIntegerField()), + ("branch", models.TextField()), + ("name", models.TextField()), + ("value_avg", models.FloatField()), + ("value_max", models.FloatField()), + ("value_min", models.FloatField()), + ("value_count", models.FloatField()), + ], + options={ + "db_table": "timeseries_measurement_summary_30day", + "ordering": ["timestamp_bin"], + "abstract": False, + "managed": False, + }, + ), + migrations.CreateModel( + name="MeasurementSummary7Day", + fields=[ + ( + "timestamp_bin", + models.DateTimeField(primary_key=True, serialize=False), + ), + ("owner_id", models.BigIntegerField()), + ("repo_id", models.BigIntegerField()), + ("flag_id", models.BigIntegerField()), + ("branch", models.TextField()), + ("name", models.TextField()), + ("value_avg", models.FloatField()), + ("value_max", models.FloatField()), + ("value_min", models.FloatField()), + ("value_count", models.FloatField()), + ], + options={ + "db_table": "timeseries_measurement_summary_7day", + "ordering": ["timestamp_bin"], + "abstract": False, + "managed": False, + }, + ), + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0005_uniqueness_constraints.py b/libs/shared/shared/django_apps/timeseries/migrations/0005_uniqueness_constraints.py new file mode 100644 index 0000000000..6aba3f03eb --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0005_uniqueness_constraints.py @@ -0,0 +1,35 @@ +# Generated by Django 3.1.13 on 2022-06-07 19:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("timeseries", "0004_measurement_summaries"), + ] + + operations = [ + migrations.AddConstraint( + model_name="measurement", + constraint=models.UniqueConstraint( + condition=models.Q(flag_id__isnull=False), + fields=( + "name", + "owner_id", + "repo_id", + "flag_id", + "commit_sha", + "timestamp", + ), + name="timeseries_measurement_flag_unique", + ), + ), + migrations.AddConstraint( + model_name="measurement", + constraint=models.UniqueConstraint( + condition=models.Q(flag_id__isnull=True), + fields=("name", "owner_id", "repo_id", "commit_sha", "timestamp"), + name="timeseries_measurement_noflag_unique", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0006_auto_20220718_1311.py b/libs/shared/shared/django_apps/timeseries/migrations/0006_auto_20220718_1311.py new file mode 100644 index 0000000000..3008c36396 --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0006_auto_20220718_1311.py @@ -0,0 +1,41 @@ +# Generated by Django 3.2.12 on 2022-07-18 13:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("timeseries", "0005_uniqueness_constraints"), + ] + + operations = [ + migrations.CreateModel( + name="Dataset", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.TextField()), + ("repository_id", models.IntegerField()), + ("backfilled", models.BooleanField(default=False)), + ], + ), + migrations.AddIndex( + model_name="dataset", + index=models.Index( + fields=["name", "repository_id"], name="timeseries__name_f96a15_idx" + ), + ), + migrations.AddConstraint( + model_name="dataset", + constraint=models.UniqueConstraint( + fields=("name", "repository_id"), name="name_repository_id_unique" + ), + ), + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0007_auto_20220727_2011.py b/libs/shared/shared/django_apps/timeseries/migrations/0007_auto_20220727_2011.py new file mode 100644 index 0000000000..e1f2fa7158 --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0007_auto_20220727_2011.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.12 on 2022-07-27 20:11 + +from django.db import migrations, models + +from shared.django_apps.core.models import DateTimeWithoutTZField + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Add field created_at to dataset + -- + ALTER TABLE "timeseries_dataset" ADD COLUMN "created_at" timestamp NULL; + -- + -- Add field updated_at to dataset + -- + ALTER TABLE "timeseries_dataset" ADD COLUMN "updated_at" timestamp NULL; + -- + -- Alter field id on dataset + -- + COMMIT; + """ + + dependencies = [ + ("timeseries", "0006_auto_20220718_1311"), + ] + + operations = [ + migrations.AddField( + model_name="dataset", + name="created_at", + field=DateTimeWithoutTZField(null=True), + ), + migrations.AddField( + model_name="dataset", + name="updated_at", + field=DateTimeWithoutTZField(null=True), + ), + migrations.AlterField( + model_name="dataset", + name="id", + field=models.AutoField(primary_key=True, serialize=False), + ), + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0008_auto_20220802_1838.py b/libs/shared/shared/django_apps/timeseries/migrations/0008_auto_20220802_1838.py new file mode 100644 index 0000000000..a717133089 --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0008_auto_20220802_1838.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.12 on 2022-08-02 18:38 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("timeseries", "0007_auto_20220727_2011"), + ] + + operations = [ + migrations.RunSQL( + f""" + select remove_continuous_aggregate_policy('timeseries_measurement_summary_{name}'); + select add_continuous_aggregate_policy( + 'timeseries_measurement_summary_{name}', + start_offset => NULL, + end_offset => NULL, + schedule_interval => INTERVAL '1 h' + ); + """, + ) + for name in ["1day", "7day", "30day"] + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0009_auto_20220804_1305.py b/libs/shared/shared/django_apps/timeseries/migrations/0009_auto_20220804_1305.py new file mode 100644 index 0000000000..af0c37d09f --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0009_auto_20220804_1305.py @@ -0,0 +1,37 @@ +# Generated by Django 3.2.12 on 2022-08-04 13:05 + +import datetime + +from django.db import migrations + +from shared.django_apps.core.models import DateTimeWithoutTZField + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Alter field created_at on dataset + -- + -- + -- Alter field updated_at on dataset + -- + COMMIT; + """ + + dependencies = [ + ("timeseries", "0008_auto_20220802_1838"), + ] + + operations = [ + migrations.AlterField( + model_name="dataset", + name="created_at", + field=DateTimeWithoutTZField(default=datetime.datetime.now, null=True), + ), + migrations.AlterField( + model_name="dataset", + name="updated_at", + field=DateTimeWithoutTZField(default=datetime.datetime.now, null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0010_auto_20230123_1453.py b/libs/shared/shared/django_apps/timeseries/migrations/0010_auto_20230123_1453.py new file mode 100644 index 0000000000..c725e7857a --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0010_auto_20230123_1453.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.12 on 2023-01-23 14:53 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("timeseries", "0009_auto_20220804_1305"), + ] + + # disable real time aggregates + # https://docs.timescale.com/timescaledb/latest/how-to-guides/continuous-aggregates/real-time-aggregates/#real-time-aggregates + + operations = [ + migrations.RunSQL( + f""" + alter materialized view timeseries_measurement_summary_{name} set (timescaledb.materialized_only = true); + """, + ) + for name in ["1day", "7day", "30day"] + if not settings.TIMESERIES_REAL_TIME_AGGREGATES + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0011_measurement_measurable_id.py b/libs/shared/shared/django_apps/timeseries/migrations/0011_measurement_measurable_id.py new file mode 100644 index 0000000000..8f21456df7 --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0011_measurement_measurable_id.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.7 on 2023-04-28 19:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("timeseries", "0010_auto_20230123_1453"), + ] + + operations = [ + migrations.AddField( + model_name="measurement", + name="measurable_id", + field=models.TextField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0012_auto_20230501_1929.py b/libs/shared/shared/django_apps/timeseries/migrations/0012_auto_20230501_1929.py new file mode 100644 index 0000000000..926ca1706f --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0012_auto_20230501_1929.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.7 on 2023-05-01 19:29 + +from django.db import migrations + +from shared.django_apps.migration_utils import RiskyRunSQL + + +class Migration(migrations.Migration): + dependencies = [ + ("timeseries", "0011_measurement_measurable_id"), + ] + + operations = [ + RiskyRunSQL( + "update timeseries_measurement set measurable_id = case when name = 'coverage' then repo_id::text when name = 'flag_coverage' then flag_id::text end where measurable_id is null;" + ), + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0013_measurable_indexes_caggs.py b/libs/shared/shared/django_apps/timeseries/migrations/0013_measurable_indexes_caggs.py new file mode 100644 index 0000000000..1792ecd5ed --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0013_measurable_indexes_caggs.py @@ -0,0 +1,220 @@ +# Generated by Django 4.1.7 on 2023-05-05 13:23 + +from django.conf import settings +from django.db import migrations, models + +from shared.django_apps.migration_utils import RiskyAddConstraint, RiskyAddIndex + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Alter field measurable_id on measurement + -- + ALTER TABLE "timeseries_measurement" ALTER COLUMN "measurable_id" SET NOT NULL; + -- + -- Remove index timeseries__owner_i_2cc713_idx from measurement + -- + DROP INDEX IF EXISTS "timeseries__owner_i_2cc713_idx"; + -- + -- Create index timeseries__owner_i_08d6fe_idx on field(s) owner_id, repo_id, measurable_id, branch, name, timestamp of model measurement + -- + CREATE INDEX "timeseries__owner_i_08d6fe_idx" ON "timeseries_measurement" ("owner_id", "repo_id", "measurable_id", "branch", "name", "timestamp"); + -- + -- Create constraint timeseries_measurement_unique on model measurement + -- + ALTER TABLE "timeseries_measurement" ADD CONSTRAINT "timeseries_measurement_unique" UNIQUE ("name", "owner_id", "repo_id", "measurable_id", "commit_sha", "timestamp"); + -- + -- Raw SQL operation + -- + + drop materialized view timeseries_measurement_summary_1day; + create materialized view timeseries_measurement_summary_1day + with (timescaledb.continuous) as + select + owner_id, + repo_id, + measurable_id, + branch, + name, + time_bucket(interval '1 days', timestamp) as timestamp_bin, + avg(value) as value_avg, + max(value) as value_max, + min(value) as value_min, + count(value) as value_count + from timeseries_measurement + group by + owner_id, repo_id, measurable_id, branch, name, timestamp_bin + with no data; + select add_continuous_aggregate_policy( + 'timeseries_measurement_summary_1day', + start_offset => NULL, + end_offset => NULL, + schedule_interval => INTERVAL '1 h' + ); + + -- + -- Raw SQL operation + -- + + drop materialized view timeseries_measurement_summary_7day; + create materialized view timeseries_measurement_summary_7day + with (timescaledb.continuous) as + select + owner_id, + repo_id, + measurable_id, + branch, + name, + time_bucket(interval '7 days', timestamp) as timestamp_bin, + avg(value) as value_avg, + max(value) as value_max, + min(value) as value_min, + count(value) as value_count + from timeseries_measurement + group by + owner_id, repo_id, measurable_id, branch, name, timestamp_bin + with no data; + select add_continuous_aggregate_policy( + 'timeseries_measurement_summary_7day', + start_offset => NULL, + end_offset => NULL, + schedule_interval => INTERVAL '1 h' + ); + + -- + -- Raw SQL operation + -- + + drop materialized view timeseries_measurement_summary_30day; + create materialized view timeseries_measurement_summary_30day + with (timescaledb.continuous) as + select + owner_id, + repo_id, + measurable_id, + branch, + name, + time_bucket(interval '30 days', timestamp) as timestamp_bin, + avg(value) as value_avg, + max(value) as value_max, + min(value) as value_min, + count(value) as value_count + from timeseries_measurement + group by + owner_id, repo_id, measurable_id, branch, name, timestamp_bin + with no data; + select add_continuous_aggregate_policy( + 'timeseries_measurement_summary_30day', + start_offset => NULL, + end_offset => NULL, + schedule_interval => INTERVAL '1 h' + ); + + -- + -- Raw SQL operation + -- + + alter materialized view timeseries_measurement_summary_1day set (timescaledb.materialized_only = true); + + -- + -- Raw SQL operation + -- + + alter materialized view timeseries_measurement_summary_7day set (timescaledb.materialized_only = true); + + -- + -- Raw SQL operation + -- + + alter materialized view timeseries_measurement_summary_30day set (timescaledb.materialized_only = true); + + COMMIT; + """ + + dependencies = [ + ("timeseries", "0012_auto_20230501_1929"), + ] + + operations = ( + [ + migrations.AlterField( + model_name="measurement", + name="measurable_id", + field=models.TextField(), + ), + migrations.RemoveIndex( + model_name="measurement", + name="timeseries__owner_i_2cc713_idx", + ), + RiskyAddIndex( + model_name="measurement", + index=models.Index( + fields=[ + "owner_id", + "repo_id", + "measurable_id", + "branch", + "name", + "timestamp", + ], + name="timeseries__owner_i_08d6fe_idx", + ), + ), + RiskyAddConstraint( + model_name="measurement", + constraint=models.UniqueConstraint( + fields=( + "name", + "owner_id", + "repo_id", + "measurable_id", + "commit_sha", + "timestamp", + ), + name="timeseries_measurement_unique", + ), + ), + ] + + [ + migrations.RunSQL( + f""" + drop materialized view timeseries_measurement_summary_{days}day; + create materialized view timeseries_measurement_summary_{days}day + with (timescaledb.continuous) as + select + owner_id, + repo_id, + measurable_id, + branch, + name, + time_bucket(interval '{days} days', timestamp) as timestamp_bin, + avg(value) as value_avg, + max(value) as value_max, + min(value) as value_min, + count(value) as value_count + from timeseries_measurement + group by + owner_id, repo_id, measurable_id, branch, name, timestamp_bin + with no data; + select add_continuous_aggregate_policy( + 'timeseries_measurement_summary_{days}day', + start_offset => NULL, + end_offset => NULL, + schedule_interval => INTERVAL '1 h' + ); + """ + ) + for days in [1, 7, 30] + ] + + [ + migrations.RunSQL( + f""" + alter materialized view timeseries_measurement_summary_{days}day set (timescaledb.materialized_only = true); + """ + ) + for days in [1, 7, 30] + if not settings.TIMESERIES_REAL_TIME_AGGREGATES + ] + ) diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0014_remove_measurement_timeseries_measurement_flag_unique_and_more.py b/libs/shared/shared/django_apps/timeseries/migrations/0014_remove_measurement_timeseries_measurement_flag_unique_and_more.py new file mode 100644 index 0000000000..c94dafb7bc --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0014_remove_measurement_timeseries_measurement_flag_unique_and_more.py @@ -0,0 +1,39 @@ +import django.utils.timezone +from django.db import migrations + +from shared.django_apps.core.models import DateTimeWithoutTZField +from shared.django_apps.migration_utils import ( # Generated by Django 4.1.7 on 2023-05-15 20:46 + RiskyRemoveConstraint, + RiskyRemoveField, +) + + +class Migration(migrations.Migration): + dependencies = [ + ("timeseries", "0013_measurable_indexes_caggs"), + ] + + operations = [ + RiskyRemoveConstraint( + model_name="measurement", + name="timeseries_measurement_flag_unique", + ), + RiskyRemoveConstraint( + model_name="measurement", + name="timeseries_measurement_noflag_unique", + ), + RiskyRemoveField( + model_name="measurement", + name="flag_id", + ), + migrations.AlterField( + model_name="dataset", + name="created_at", + field=DateTimeWithoutTZField(default=django.utils.timezone.now, null=True), + ), + migrations.AlterField( + model_name="dataset", + name="updated_at", + field=DateTimeWithoutTZField(default=django.utils.timezone.now, null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0015_testrun_model.py b/libs/shared/shared/django_apps/timeseries/migrations/0015_testrun_model.py new file mode 100644 index 0000000000..7f7620aed7 --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0015_testrun_model.py @@ -0,0 +1,111 @@ +# Generated by Django 4.2.16 on 2025-02-06 15:02 + +import django.contrib.postgres.fields +import django_prometheus.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "timeseries", + "0014_remove_measurement_timeseries_measurement_flag_unique_and_more", + ), + ] + + operations = [ + migrations.CreateModel( + name="Testrun", + fields=[ + ("timestamp", models.DateTimeField(primary_key=True, serialize=False)), + ("repo_id", models.BigIntegerField()), + ("test_id", models.BinaryField()), + ("testsuite", models.TextField(null=True)), + ("classname", models.TextField(null=True)), + ("name", models.TextField(null=True)), + ("computed_name", models.TextField(null=True)), + ("outcome", models.TextField()), + ("duration_seconds", models.FloatField(null=True)), + ("failure_message", models.TextField(null=True)), + ("framework", models.TextField(null=True)), + ("filename", models.TextField(null=True)), + ("commit_sha", models.TextField(null=True)), + ("branch", models.TextField(null=True)), + ( + "flags", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), null=True, size=None + ), + ), + ("upload_id", models.BigIntegerField(null=True)), + ], + bases=( + django_prometheus.models.ExportModelOperationsMixin( + "timeseries.testrun" + ), + models.Model, + ), + ), + migrations.RunSQL( + "ALTER TABLE timeseries_testrun DROP CONSTRAINT timeseries_testrun_pkey;", + reverse_sql="", + ), + migrations.RunSQL( + "SELECT create_hypertable('timeseries_testrun', 'timestamp');", + reverse_sql="", + ), + migrations.AddIndex( + model_name="testrun", + index=models.Index( + fields=[ + "repo_id", + "branch", + "timestamp", + ], + name="ts__repo_branch_time_i", + ), + ), + migrations.AddIndex( + model_name="testrun", + index=models.Index( + fields=["repo_id", "branch", "test_id", "timestamp"], + name="ts__repo_branch_test_time_i", + ), + ), + migrations.AddIndex( + model_name="testrun", + index=models.Index( + fields=["repo_id", "test_id", "timestamp"], + name="ts__repo_test_time_i", + ), + ), + migrations.AddIndex( + model_name="testrun", + index=models.Index( + fields=["repo_id", "commit_sha", "timestamp"], + name="ts__repo_commit_time_i", + ), + ), + migrations.RunSQL( + """ + CREATE OR REPLACE FUNCTION array_merge_dedup(anyarray, anyarray) + RETURNS anyarray LANGUAGE sql IMMUTABLE AS $$ + SELECT array_agg(DISTINCT x) + FROM ( + SELECT unnest($1) as x + UNION + SELECT unnest($2) + ) s; + $$; + CREATE AGGREGATE array_merge_dedup_agg(anyarray) ( + SFUNC = array_merge_dedup, + STYPE = anyarray, + INITCOND = '{}' + ); + """, + reverse_sql=""" + DROP AGGREGATE array_merge_dedup_agg(anyarray); + DROP FUNCTION array_merge_dedup(anyarray, anyarray); + """, + ), + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0016_testrun_summary_1day.py b/libs/shared/shared/django_apps/timeseries/migrations/0016_testrun_summary_1day.py new file mode 100644 index 0000000000..564476c005 --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0016_testrun_summary_1day.py @@ -0,0 +1,71 @@ +# Generated by Django 4.2.16 on 2025-02-06 16:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + # cant create this views in a transaction + atomic = False + + dependencies = [ + ("timeseries", "0015_testrun_model"), + ] + + operations = [ + migrations.RunSQL( + """ + create materialized view timeseries_testrun_summary_1day + with (timescaledb.continuous) as + select + repo_id, + testsuite, + classname, + name, + time_bucket(interval '1 days', timestamp) as timestamp_bin, + + min(computed_name) as computed_name, + COUNT(DISTINCT CASE WHEN outcome = 'failure' OR outcome = 'flaky_fail' THEN commit_sha ELSE NULL END) AS failing_commits, + last(duration_seconds, timestamp) as last_duration_seconds, + avg(duration_seconds) as avg_duration_seconds, + COUNT(*) FILTER (WHERE outcome = 'pass') AS pass_count, + COUNT(*) FILTER (WHERE outcome = 'failure') AS fail_count, + COUNT(*) FILTER (WHERE outcome = 'skip') AS skip_count, + COUNT(*) FILTER (WHERE outcome = 'flaky_fail') AS flaky_fail_count, + MAX(timestamp) AS updated_at, + array_merge_dedup_agg(flags) as flags + from timeseries_testrun + group by + repo_id, testsuite, classname, name, timestamp_bin; + """, + reverse_sql="drop materialized view timeseries_testrun_summary_1day;", + ), + migrations.RunSQL( + """ + create materialized view timeseries_testrun_branch_summary_1day + with (timescaledb.continuous) as + select + repo_id, + branch, + testsuite, + classname, + name, + time_bucket(interval '1 days', timestamp) as timestamp_bin, + + min(computed_name) as computed_name, + COUNT(DISTINCT CASE WHEN outcome = 'failure' OR outcome = 'flaky_fail' THEN commit_sha ELSE NULL END) AS failing_commits, + last(duration_seconds, timestamp) as last_duration_seconds, + avg(duration_seconds) as avg_duration_seconds, + COUNT(*) FILTER (WHERE outcome = 'pass') AS pass_count, + COUNT(*) FILTER (WHERE outcome = 'failure') AS fail_count, + COUNT(*) FILTER (WHERE outcome = 'skip') AS skip_count, + COUNT(*) FILTER (WHERE outcome = 'flaky_fail') AS flaky_fail_count, + MAX(timestamp) AS updated_at, + array_merge_dedup_agg(flags) as flags + from timeseries_testrun + where branch in ('main', 'master', 'develop') + group by + repo_id, branch, testsuite, classname, name, timestamp_bin; + """, + reverse_sql="drop materialized view timeseries_testrun_branch_summary_1day;", + ), + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0017_testrun_cagg_policy.py b/libs/shared/shared/django_apps/timeseries/migrations/0017_testrun_cagg_policy.py new file mode 100644 index 0000000000..beb413da6d --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0017_testrun_cagg_policy.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.16 on 2025-02-06 16:57 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("timeseries", "0016_testrun_summary_1day"), + ] + + operations = [ + migrations.RunSQL( + """ + select add_continuous_aggregate_policy( + 'timeseries_testrun_summary_1day', + start_offset => '7 days', + end_offset => '1 days', + schedule_interval => INTERVAL '1 days' + ); + """, + reverse_sql="select remove_continuous_aggregate_policy('timeseries_testrun_summary_1day');", + ), + migrations.RunSQL( + """ + select add_continuous_aggregate_policy( + 'timeseries_testrun_branch_summary_1day', + start_offset => '7 days', + end_offset => '1 days', + schedule_interval => INTERVAL '1 days' + ); + """, + reverse_sql="select remove_continuous_aggregate_policy('timeseries_testrun_branch_summary_1day');", + ), + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0018_testrun_summary_model.py b/libs/shared/shared/django_apps/timeseries/migrations/0018_testrun_summary_model.py new file mode 100644 index 0000000000..1e38b09c74 --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0018_testrun_summary_model.py @@ -0,0 +1,90 @@ +# Generated by Django 4.2.16 on 2025-02-06 16:57 +import django.contrib.postgres.fields +import django_prometheus.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("timeseries", "0017_testrun_cagg_policy"), + ] + + operations = [ + migrations.CreateModel( + name="TestrunSummary", + fields=[ + ( + "timestamp_bin", + models.DateTimeField(primary_key=True, serialize=False), + ), + ("repo_id", models.IntegerField()), + ("name", models.TextField()), + ("classname", models.TextField()), + ("testsuite", models.TextField()), + ("computed_name", models.TextField()), + ("failing_commits", models.IntegerField()), + ("avg_duration_seconds", models.FloatField()), + ("last_duration_seconds", models.FloatField()), + ("pass_count", models.IntegerField()), + ("fail_count", models.IntegerField()), + ("skip_count", models.IntegerField()), + ("flaky_fail_count", models.IntegerField()), + ("updated_at", models.DateTimeField()), + ( + "flags", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), null=True, size=None + ), + ), + ], + options={ + "db_table": "timeseries_testrun_summary_1day", + "managed": False, + }, + bases=( + django_prometheus.models.ExportModelOperationsMixin( + "timeseries.testrun_continuous_aggregate" + ), + models.Model, + ), + ), + migrations.CreateModel( + name="TestrunBranchSummary", + fields=[ + ( + "timestamp_bin", + models.DateTimeField(primary_key=True, serialize=False), + ), + ("repo_id", models.IntegerField()), + ("branch", models.TextField()), + ("name", models.TextField()), + ("classname", models.TextField()), + ("testsuite", models.TextField()), + ("computed_name", models.TextField()), + ("failing_commits", models.IntegerField()), + ("avg_duration_seconds", models.FloatField()), + ("last_duration_seconds", models.FloatField()), + ("pass_count", models.IntegerField()), + ("fail_count", models.IntegerField()), + ("skip_count", models.IntegerField()), + ("flaky_fail_count", models.IntegerField()), + ("updated_at", models.DateTimeField()), + ( + "flags", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), null=True, size=None + ), + ), + ], + options={ + "db_table": "timeseries_testrun_summary_1day", + "managed": False, + }, + bases=( + django_prometheus.models.ExportModelOperationsMixin( + "timeseries.testrun_continuous_aggregate" + ), + models.Model, + ), + ), + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/0019_testrun_real_time_agg.py b/libs/shared/shared/django_apps/timeseries/migrations/0019_testrun_real_time_agg.py new file mode 100644 index 0000000000..d5572e806e --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/migrations/0019_testrun_real_time_agg.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.16 on 2025-02-12 15:58 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("timeseries", "0018_testrun_summary_model"), + ] + + operations = [ + migrations.RunSQL( + """ + alter materialized view timeseries_testrun_summary_1day set (timescaledb.materialized_only = true); + """ + ) + if not settings.TIMESERIES_REAL_TIME_AGGREGATES + else migrations.RunSQL( + """ + alter materialized view timeseries_testrun_summary_1day set (timescaledb.materialized_only = false); + """ + ), + migrations.RunSQL( + """ + alter materialized view timeseries_testrun_branch_summary_1day set (timescaledb.materialized_only = true); + """ + ) + if not settings.TIMESERIES_REAL_TIME_AGGREGATES + else migrations.RunSQL( + """ + alter materialized view timeseries_testrun_branch_summary_1day set (timescaledb.materialized_only = false); + """ + ), + ] diff --git a/libs/shared/shared/django_apps/timeseries/migrations/__init__.py b/libs/shared/shared/django_apps/timeseries/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/timeseries/models.py b/libs/shared/shared/django_apps/timeseries/models.py new file mode 100644 index 0000000000..2f7a94376b --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/models.py @@ -0,0 +1,284 @@ +from datetime import datetime, timedelta +from enum import Enum + +import django.db.models as models +from django.contrib.postgres.fields import ArrayField +from django.utils import timezone +from django_prometheus.models import ExportModelOperationsMixin + +from shared.django_apps.core.models import DateTimeWithoutTZField + +TIMESERIES_APP_LABEL = "timeseries" + + +class Interval(Enum): + INTERVAL_1_DAY = 1 + INTERVAL_7_DAY = 7 + INTERVAL_30_DAY = 30 + + +class MeasurementName(Enum): + COVERAGE = "coverage" + FLAG_COVERAGE = "flag_coverage" + COMPONENT_COVERAGE = "component_coverage" + # For tracking the entire size of a bundle report by its name + BUNDLE_ANALYSIS_REPORT_SIZE = "bundle_analysis_report_size" + # For tracking the size of a category of assets of a bundle report by its name + BUNDLE_ANALYSIS_JAVASCRIPT_SIZE = "bundle_analysis_javascript_size" + BUNDLE_ANALYSIS_STYLESHEET_SIZE = "bundle_analysis_stylesheet_size" + BUNDLE_ANALYSIS_FONT_SIZE = "bundle_analysis_font_size" + BUNDLE_ANALYSIS_IMAGE_SIZE = "bundle_analysis_image_size" + # For tracking individual asset size via its UUID + BUNDLE_ANALYSIS_ASSET_SIZE = "bundle_analysis_asset_size" + + +class Measurement(ExportModelOperationsMixin("timeseries.measurement"), models.Model): + # TimescaleDB requires that `timestamp` be part of every index (since data is + # partitioned by `timestamp`). Since an auto-incrementing primary key would + # not satisfy this requirement we can make `timestamp` the primary key. + # `timestamp` may not be unique though so we drop the uniqueness constraint in + # a migration. + timestamp = models.DateTimeField(null=False, primary_key=True) + + owner_id = models.BigIntegerField(null=False) + repo_id = models.BigIntegerField(null=False) + measurable_id = models.TextField(null=False) + branch = models.TextField(null=True) + + # useful for updating a measurement if needed + commit_sha = models.TextField(null=True) + + # the name of the measurement (i.e. "coverage") + name = models.TextField(null=False, blank=False) + value = models.FloatField(null=False) + + class Meta: + app_label = TIMESERIES_APP_LABEL + indexes = [ + # for querying measurements + models.Index( + fields=[ + "owner_id", + "repo_id", + "measurable_id", + "branch", + "name", + "timestamp", + ] + ), + ] + constraints = [ + # for updating measurements + models.UniqueConstraint( + fields=[ + "name", + "owner_id", + "repo_id", + "measurable_id", + "commit_sha", + "timestamp", + ], + name="timeseries_measurement_unique", + ), + ] + + +class MeasurementSummary( + ExportModelOperationsMixin("timeseries.measurement_summary"), models.Model +): + timestamp_bin = models.DateTimeField(primary_key=True) + owner_id = models.BigIntegerField() + repo_id = models.BigIntegerField() + measurable_id = models.TextField() + branch = models.TextField() + name = models.TextField() + value_avg = models.FloatField() + value_max = models.FloatField() + value_min = models.FloatField() + value_count = models.FloatField() + + @classmethod + def agg_by(cls, interval: Interval) -> models.Manager: + model_classes = { + Interval.INTERVAL_1_DAY: MeasurementSummary1Day, + Interval.INTERVAL_7_DAY: MeasurementSummary7Day, + Interval.INTERVAL_30_DAY: MeasurementSummary30Day, + } + + model_class = model_classes.get(interval) + if not model_class: + raise ValueError(f"cannot aggregate by '{interval}'") + return model_class.objects + + class Meta: + app_label = TIMESERIES_APP_LABEL + abstract = True + # these are backed by TimescaleDB "continuous aggregates" + # (materialized views) + managed = False + ordering = ["timestamp_bin"] + + +class MeasurementSummary1Day(MeasurementSummary): + class Meta(MeasurementSummary.Meta): + db_table = "timeseries_measurement_summary_1day" + + +# Timescale's origin for time buckets is Monday 2000-01-03 +# Weekly aggregate bins will thus be Monday-Sunday +class MeasurementSummary7Day(MeasurementSummary): + class Meta(MeasurementSummary.Meta): + db_table = "timeseries_measurement_summary_7day" + + +# Timescale's origin for time buckets is 2000-01-03 +# 30 day offsets will be aligned on that origin +class MeasurementSummary30Day(MeasurementSummary): + class Meta(MeasurementSummary.Meta): + db_table = "timeseries_measurement_summary_30day" + + +class Dataset(ExportModelOperationsMixin("timeseries.dataset"), models.Model): + id = models.AutoField(primary_key=True) + + # this will likely correspond to a measurement name above + name = models.TextField(null=False, blank=False) + + # not a true foreign key since repositories are in a + # different database + repository_id = models.IntegerField(null=False) + + # indicates whether the backfill task has completed for this dataset + # TODO: We're not really using this field anymore as a backfill task takes very long for this to be populated when finished. + # The solution would be to somehow have a celery task return when it's done, hence the TODO + backfilled = models.BooleanField(null=False, default=False) + + created_at = DateTimeWithoutTZField(default=timezone.now, null=True) + updated_at = DateTimeWithoutTZField(default=timezone.now, null=True) + + class Meta: + app_label = TIMESERIES_APP_LABEL + indexes = [ + models.Index( + fields=[ + "name", + "repository_id", + ] + ), + ] + constraints = [ + models.UniqueConstraint( + fields=[ + "name", + "repository_id", + ], + name="name_repository_id_unique", + ), + ] + + def is_backfilled(self) -> bool: + """ + Returns `False` for an hour after creation. + + TODO: this should eventually read `self.backfilled` which will be updated via the worker + """ + if not self.created_at: + return False + return datetime.now() > self.created_at + timedelta(hours=1) + + +class Testrun(ExportModelOperationsMixin("timeseries.testrun"), models.Model): + timestamp = models.DateTimeField(null=False, primary_key=True) + + test_id = models.BinaryField(null=False) + + name = models.TextField(null=True) + classname = models.TextField(null=True) + testsuite = models.TextField(null=True) + computed_name = models.TextField(null=True) + + outcome = models.TextField(null=False) + + duration_seconds = models.FloatField(null=True) + failure_message = models.TextField(null=True) + framework = models.TextField(null=True) + filename = models.TextField(null=True) + + repo_id = models.BigIntegerField(null=True) + commit_sha = models.TextField(null=True) + branch = models.TextField(null=True) + + flags = ArrayField(models.TextField(), null=True) + upload_id = models.BigIntegerField(null=True) + + class Meta: + app_label = TIMESERIES_APP_LABEL + indexes = [ + models.Index( + name="ts__repo_branch_time_i", + fields=["repo_id", "branch", "timestamp"], + ), + models.Index( + name="ts__repo_branch_test_time_i", + fields=["repo_id", "branch", "test_id", "timestamp"], + ), + models.Index( + name="ts__repo_test_id_time_i", + fields=["repo_id", "test_id", "timestamp"], + ), + models.Index( + name="ts__repo_commit_time_i", + fields=["repo_id", "commit_sha", "timestamp"], + ), + ] + + +class TestrunBranchSummary( + ExportModelOperationsMixin("timeseries.testrun_continuous_aggregate"), models.Model +): + timestamp_bin = models.DateTimeField(primary_key=True) + repo_id = models.IntegerField() + branch = models.TextField() + name = models.TextField() + classname = models.TextField() + testsuite = models.TextField() + computed_name = models.TextField() + failing_commits = models.IntegerField() + avg_duration_seconds = models.FloatField() + last_duration_seconds = models.FloatField() + pass_count = models.IntegerField() + fail_count = models.IntegerField() + skip_count = models.IntegerField() + flaky_fail_count = models.IntegerField() + updated_at = models.DateTimeField() + flags = ArrayField(models.TextField(), null=True) + + class Meta: + app_label = TIMESERIES_APP_LABEL + db_table = "timeseries_testrun_branch_summary_1day" + managed = False + + +class TestrunSummary( + ExportModelOperationsMixin("timeseries.testrun_continuous_aggregate"), models.Model +): + timestamp_bin = models.DateTimeField(primary_key=True) + repo_id = models.IntegerField() + name = models.TextField() + classname = models.TextField() + testsuite = models.TextField() + computed_name = models.TextField() + failing_commits = models.IntegerField() + avg_duration_seconds = models.FloatField() + last_duration_seconds = models.FloatField() + pass_count = models.IntegerField() + fail_count = models.IntegerField() + skip_count = models.IntegerField() + flaky_fail_count = models.IntegerField() + updated_at = models.DateTimeField() + flags = ArrayField(models.TextField(), null=True) + + class Meta: + app_label = TIMESERIES_APP_LABEL + db_table = "timeseries_testrun_summary_1day" + managed = False diff --git a/libs/shared/shared/django_apps/timeseries/tests/__init__.py b/libs/shared/shared/django_apps/timeseries/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/timeseries/tests/factories.py b/libs/shared/shared/django_apps/timeseries/tests/factories.py new file mode 100644 index 0000000000..31371cbadf --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/tests/factories.py @@ -0,0 +1,28 @@ +import random +from datetime import datetime + +import factory +from factory.django import DjangoModelFactory + +from shared.django_apps.timeseries import models + + +class MeasurementFactory(DjangoModelFactory): + class Meta: + model = models.Measurement + + owner_id = 1 + repo_id = 1 + name = "testing" + branch = "master" + value = factory.LazyAttribute(lambda _: random.random() * 1000) + timestamp = factory.LazyAttribute(lambda _: datetime.now()) + + +class DatasetFactory(DjangoModelFactory): + class Meta: + model = models.Dataset + + repository_id = 1 + name = "testing" + backfilled = False diff --git a/libs/shared/shared/django_apps/timeseries/tests/test_db.py b/libs/shared/shared/django_apps/timeseries/tests/test_db.py new file mode 100644 index 0000000000..66d845e01e --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/tests/test_db.py @@ -0,0 +1,31 @@ +from unittest.mock import patch + +import pytest +from django.conf import settings +from django.db import connections +from django.test import TransactionTestCase + + +@pytest.mark.skipif( + not settings.TIMESERIES_ENABLED, reason="requires timeseries data storage" +) +class DatabaseTests(TransactionTestCase): + databases = {"timeseries"} + + @patch("django.db.backends.postgresql.base.DatabaseWrapper.is_usable") + def test_db_reconnect(self, is_usable): + timeseries_database_engine = settings.DATABASES["timeseries"]["ENGINE"] + settings.DATABASES["timeseries"]["ENGINE"] = "codecov.db" + + is_usable.return_value = True + + with connections["timeseries"].cursor() as cursor: + cursor.execute("SELECT 1") + + is_usable.return_value = False + + # it should reconnect and not raise an error + with connections["timeseries"].cursor() as cursor: + cursor.execute("SELECT 1") + + settings.DATABASES["timeseries"]["ENGINE"] = timeseries_database_engine diff --git a/libs/shared/shared/django_apps/timeseries/tests/test_models.py b/libs/shared/shared/django_apps/timeseries/tests/test_models.py new file mode 100644 index 0000000000..413ad3594c --- /dev/null +++ b/libs/shared/shared/django_apps/timeseries/tests/test_models.py @@ -0,0 +1,132 @@ +from datetime import datetime, timezone + +import pytest +from django.conf import settings +from django.test import TransactionTestCase +from freezegun import freeze_time + +from shared.django_apps.timeseries.models import Dataset, Interval, MeasurementSummary + +from .factories import DatasetFactory, MeasurementFactory + + +@pytest.mark.skipif( + not settings.TIMESERIES_ENABLED, reason="requires timeseries data storage" +) +class MeasurementTests(TransactionTestCase): + databases = {"timeseries"} + + def test_measurement_agg_1day(self): + MeasurementFactory( + timestamp=datetime(2022, 1, 1, 0, 0, 0, tzinfo=timezone.utc), value=1 + ) + MeasurementFactory( + timestamp=datetime(2022, 1, 1, 1, 0, 0, tzinfo=timezone.utc), value=2 + ) + MeasurementFactory( + timestamp=datetime(2022, 1, 1, 1, 0, 1, tzinfo=timezone.utc), value=3 + ) + MeasurementFactory( + timestamp=datetime(2022, 1, 2, 0, 0, 0, tzinfo=timezone.utc), value=4 + ) + MeasurementFactory( + timestamp=datetime(2022, 1, 2, 0, 1, 0, tzinfo=timezone.utc), value=5 + ) + + results = MeasurementSummary.agg_by(Interval.INTERVAL_1_DAY).all() + + assert len(results) == 2 + assert results[0].value_avg == 2 + assert results[0].value_min == 1 + assert results[0].value_max == 3 + assert results[0].value_count == 3 + assert results[1].value_avg == 4.5 + assert results[1].value_min == 4 + assert results[1].value_max == 5 + assert results[1].value_count == 2 + + def test_measurement_agg_7day(self): + # Week 1: Monday, Tuesday, Sunday + MeasurementFactory(timestamp=datetime(2022, 1, 3), value=1) + MeasurementFactory(timestamp=datetime(2022, 1, 4), value=2) + MeasurementFactory(timestamp=datetime(2022, 1, 9), value=3) + + # Week 2: Monday, Sunday + MeasurementFactory(timestamp=datetime(2022, 1, 10), value=4) + MeasurementFactory(timestamp=datetime(2022, 1, 16), value=5) + + results = MeasurementSummary.agg_by(Interval.INTERVAL_7_DAY).all() + + assert len(results) == 2 + assert results[0].value_avg == 2 + assert results[0].value_min == 1 + assert results[0].value_max == 3 + assert results[0].value_count == 3 + assert results[1].value_avg == 4.5 + assert results[1].value_min == 4 + assert results[1].value_max == 5 + assert results[1].value_count == 2 + + def test_measurement_agg_30day(self): + # Timescale's origin for time buckets is 2000-01-03 + # 30 day offsets will be aligned on that origin + + MeasurementFactory(timestamp=datetime(2000, 1, 3), value=1) + MeasurementFactory(timestamp=datetime(2000, 1, 4), value=2) + MeasurementFactory(timestamp=datetime(2000, 2, 1), value=3) + + MeasurementFactory(timestamp=datetime(2000, 2, 2), value=4) + MeasurementFactory(timestamp=datetime(2000, 2, 11), value=5) + + results = MeasurementSummary.agg_by(Interval.INTERVAL_30_DAY).all() + + assert len(results) == 2 + assert results[0].value_avg == 2 + assert results[0].value_min == 1 + assert results[0].value_max == 3 + assert results[0].value_count == 3 + assert results[1].value_avg == 4.5 + assert results[1].value_min == 4 + assert results[1].value_max == 5 + assert results[1].value_count == 2 + + def test_measurement_agg_invalid(self): + with self.assertRaises(ValueError): + MeasurementSummary.agg_by("invalid").all() + + +@pytest.mark.skipif( + not settings.TIMESERIES_ENABLED, reason="requires timeseries data storage" +) +class DatasetTests(TransactionTestCase): + databases = {"timeseries"} + + @freeze_time("2022-01-01T01:00:01+0000") + def test_is_backfilled_true(self): + dataset = DatasetFactory() + + Dataset.objects.filter(pk=dataset.pk).update( + created_at=datetime(2022, 1, 1, 0, 0, 0) + ) + + dataset.refresh_from_db() + assert dataset.is_backfilled() == True + + @freeze_time("2022-01-01T00:59:59+0000") + def test_is_backfilled_false(self): + dataset = DatasetFactory() + + Dataset.objects.filter(pk=dataset.pk).update( + created_at=datetime(2022, 1, 1, 0, 0, 0) + ) + + dataset.refresh_from_db() + assert dataset.is_backfilled() == False + + def test_is_backfilled_no_created_at(self): + dataset = DatasetFactory() + + Dataset.objects.filter(pk=dataset.pk).update(created_at=None) + + dataset.refresh_from_db() + assert dataset.is_backfilled() == False diff --git a/libs/shared/shared/django_apps/user_measurements/__init__.py b/libs/shared/shared/django_apps/user_measurements/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/user_measurements/migrations/0001_initial.py b/libs/shared/shared/django_apps/user_measurements/migrations/0001_initial.py new file mode 100644 index 0000000000..949e6db4be --- /dev/null +++ b/libs/shared/shared/django_apps/user_measurements/migrations/0001_initial.py @@ -0,0 +1,126 @@ +# Generated by Django 4.2.11 on 2024-04-10 21:40 + +import django.db.models.deletion +import psqlextra.backend.migrations.operations.add_default_partition +import psqlextra.backend.migrations.operations.create_partitioned_model +import psqlextra.manager.manager +import psqlextra.models.partitioned +import psqlextra.types +from django.db import migrations, models + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Create partitioned model UserMeasurement + -- + CREATE TABLE "user_measurements" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "created_at" timestamp with time zone NOT NULL, "uploader_used" varchar NOT NULL, "private_repo" boolean NOT NULL, "report_type" varchar(100) NULL, "commit_id" bigint NOT NULL, "owner_id" integer NOT NULL, "repo_id" integer NOT NULL, "upload_id" bigint NOT NULL, PRIMARY KEY ("id", "created_at")) PARTITION BY RANGE ("created_at"); + -- + -- Creates default partition 'default' on UserMeasurement + -- + CREATE TABLE "user_measurements_default" PARTITION OF "user_measurements" DEFAULT; + ALTER TABLE "user_measurements" ADD CONSTRAINT "user_measurements_commit_id_cebc077d_fk_commits_id" FOREIGN KEY ("commit_id") REFERENCES "commits" ("id") DEFERRABLE INITIALLY DEFERRED; + ALTER TABLE "user_measurements" ADD CONSTRAINT "user_measurements_owner_id_ef39e26d_fk_owners_ownerid" FOREIGN KEY ("owner_id") REFERENCES "owners" ("ownerid") DEFERRABLE INITIALLY DEFERRED; + ALTER TABLE "user_measurements" ADD CONSTRAINT "user_measurements_repo_id_88a7cde6_fk_repos_repoid" FOREIGN KEY ("repo_id") REFERENCES "repos" ("repoid") DEFERRABLE INITIALLY DEFERRED; + ALTER TABLE "user_measurements" ADD CONSTRAINT "user_measurements_upload_id_e18ce658_fk_reports_upload_id" FOREIGN KEY ("upload_id") REFERENCES "reports_upload" ("id") DEFERRABLE INITIALLY DEFERRED; + CREATE INDEX "user_measurements_commit_id_cebc077d" ON "user_measurements" ("commit_id"); + CREATE INDEX "user_measurements_owner_id_ef39e26d" ON "user_measurements" ("owner_id"); + CREATE INDEX "user_measurements_repo_id_88a7cde6" ON "user_measurements" ("repo_id"); + CREATE INDEX "user_measurements_upload_id_e18ce658" ON "user_measurements" ("upload_id"); + CREATE INDEX "i_owner" ON "user_measurements" ("owner_id"); + CREATE INDEX "owner_repo" ON "user_measurements" ("owner_id", "repo_id"); + CREATE INDEX "owner_private_repo" ON "user_measurements" ("owner_id", "private_repo"); + CREATE INDEX "owner_private_repo_report_type" ON "user_measurements" ("owner_id", "private_repo", "report_type"); + COMMIT; + """ + + initial = True + + dependencies = [ + ("codecov_auth", "0054_update_owners_column_defaults"), + ("reports", "0015_testresultreporttotals"), + ("core", "0048_increment_version"), + ] + + operations = [ + psqlextra.backend.migrations.operations.create_partitioned_model.PostgresCreatePartitionedModel( + name="UserMeasurement", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("uploader_used", models.CharField()), + ("private_repo", models.BooleanField()), + ( + "report_type", + models.CharField( + choices=[ + ("coverage", "Coverage"), + ("test_results", "Test Results"), + ("bundle_analysis", "Bundle Analysis"), + ], + max_length=100, + null=True, + ), + ), + ( + "commit", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="user_measurements", + to="core.commit", + ), + ), + ( + "owner", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="user_measurements", + to="codecov_auth.owner", + ), + ), + ( + "repo", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="user_measurements", + to="core.repository", + ), + ), + ( + "upload", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="user_measurements", + to="reports.reportsession", + ), + ), + ], + options={ + "db_table": "user_measurements", + "indexes": [ + models.Index(fields=["owner"], name="i_owner"), + models.Index(fields=["owner", "repo"], name="owner_repo"), + models.Index( + fields=["owner", "private_repo"], name="owner_private_repo" + ), + models.Index( + fields=["owner", "private_repo", "report_type"], + name="owner_private_repo_report_type", + ), + ], + }, + partitioning_options={ + "method": psqlextra.types.PostgresPartitioningMethod["RANGE"], + "key": ["created_at"], + }, + bases=(psqlextra.models.partitioned.PostgresPartitionedModel,), + managers=[ + ("objects", psqlextra.manager.manager.PostgresManager()), + ], + ), + psqlextra.backend.migrations.operations.add_default_partition.PostgresAddDefaultPartition( + model_name="UserMeasurement", + name="default", + ), + ] diff --git a/libs/shared/shared/django_apps/user_measurements/migrations/0002_remove_usermeasurement_i_owner_and_more.py b/libs/shared/shared/django_apps/user_measurements/migrations/0002_remove_usermeasurement_i_owner_and_more.py new file mode 100644 index 0000000000..c0c4efb24d --- /dev/null +++ b/libs/shared/shared/django_apps/user_measurements/migrations/0002_remove_usermeasurement_i_owner_and_more.py @@ -0,0 +1,161 @@ +# Generated by Django 4.2.11 on 2024-05-07 18:40 + +from django.db import migrations, models + +from shared.django_apps.migration_utils import ( + RiskyAddField, + RiskyAddIndex, + RiskyRemoveField, + RiskyRemoveIndex, +) + + +class Migration(migrations.Migration): + """ + BEGIN; + -- + -- Remove index i_owner from usermeasurement + -- + DROP INDEX IF EXISTS "i_owner"; + -- + -- Remove index owner_repo from usermeasurement + -- + DROP INDEX IF EXISTS "owner_repo"; + -- + -- Remove index owner_private_repo from usermeasurement + -- + DROP INDEX IF EXISTS "owner_private_repo"; + -- + -- Remove index owner_private_repo_report_type from usermeasurement + -- + DROP INDEX IF EXISTS "owner_private_repo_report_type"; + -- + -- Remove field commit from usermeasurement + -- + ALTER TABLE "user_measurements" DROP COLUMN "commit_id" CASCADE; + -- + -- Remove field owner from usermeasurement + -- + ALTER TABLE "user_measurements" DROP COLUMN "owner_id" CASCADE; + -- + -- Remove field repo from usermeasurement + -- + ALTER TABLE "user_measurements" DROP COLUMN "repo_id" CASCADE; + -- + -- Remove field upload from usermeasurement + -- + ALTER TABLE "user_measurements" DROP COLUMN "upload_id" CASCADE; + -- + -- Add field commit_id to usermeasurement + -- + ALTER TABLE "user_measurements" ADD COLUMN "commit_id" integer NULL; + -- + -- Add field owner_id to usermeasurement + -- + ALTER TABLE "user_measurements" ADD COLUMN "owner_id" integer NULL; + -- + -- Add field repo_id to usermeasurement + -- + ALTER TABLE "user_measurements" ADD COLUMN "repo_id" integer NULL; + -- + -- Add field upload_id to usermeasurement + -- + ALTER TABLE "user_measurements" ADD COLUMN "upload_id" integer NULL; + -- + -- Create index i_owner on field(s) owner_id of model usermeasurement + -- + CREATE INDEX "i_owner" ON "user_measurements" ("owner_id"); + -- + -- Create index owner_repo on field(s) owner_id, repo_id of model usermeasurement + -- + CREATE INDEX "owner_repo" ON "user_measurements" ("owner_id", "repo_id"); + -- + -- Create index owner_private_repo on field(s) owner_id, private_repo of model usermeasurement + -- + CREATE INDEX "owner_private_repo" ON "user_measurements" ("owner_id", "private_repo"); + -- + -- Create index owner_private_repo_report_type on field(s) owner_id, private_repo, report_type of model usermeasurement + -- + CREATE INDEX "owner_private_repo_report_type" ON "user_measurements" ("owner_id", "private_repo", "report_type"); + COMMIT; + """ + + dependencies = [ + ("user_measurements", "0001_initial"), + ] + + operations = [ + RiskyRemoveIndex( + model_name="usermeasurement", + name="i_owner", + ), + RiskyRemoveIndex( + model_name="usermeasurement", + name="owner_repo", + ), + RiskyRemoveIndex( + model_name="usermeasurement", + name="owner_private_repo", + ), + RiskyRemoveIndex( + model_name="usermeasurement", + name="owner_private_repo_report_type", + ), + RiskyRemoveField( + model_name="usermeasurement", + name="commit", + ), + RiskyRemoveField( + model_name="usermeasurement", + name="owner", + ), + RiskyRemoveField( + model_name="usermeasurement", + name="repo", + ), + RiskyRemoveField( + model_name="usermeasurement", + name="upload", + ), + RiskyAddField( + model_name="usermeasurement", + name="commit_id", + field=models.IntegerField(null=True), + ), + RiskyAddField( + model_name="usermeasurement", + name="owner_id", + field=models.IntegerField(null=True), + ), + RiskyAddField( + model_name="usermeasurement", + name="repo_id", + field=models.IntegerField(null=True), + ), + RiskyAddField( + model_name="usermeasurement", + name="upload_id", + field=models.IntegerField(null=True), + ), + RiskyAddIndex( + model_name="usermeasurement", + index=models.Index(fields=["owner_id"], name="i_owner"), + ), + RiskyAddIndex( + model_name="usermeasurement", + index=models.Index(fields=["owner_id", "repo_id"], name="owner_repo"), + ), + RiskyAddIndex( + model_name="usermeasurement", + index=models.Index( + fields=["owner_id", "private_repo"], name="owner_private_repo" + ), + ), + RiskyAddIndex( + model_name="usermeasurement", + index=models.Index( + fields=["owner_id", "private_repo", "report_type"], + name="owner_private_repo_report_type", + ), + ), + ] diff --git a/libs/shared/shared/django_apps/user_measurements/migrations/0003_alter_usermeasurement_upload_id.py b/libs/shared/shared/django_apps/user_measurements/migrations/0003_alter_usermeasurement_upload_id.py new file mode 100644 index 0000000000..72e08fbc2c --- /dev/null +++ b/libs/shared/shared/django_apps/user_measurements/migrations/0003_alter_usermeasurement_upload_id.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.13 on 2024-07-19 20:21 + +from django.db import migrations, models + +""" +BEGIN; +-- +-- Alter field upload_id on usermeasurement +-- +ALTER TABLE "user_measurements" ALTER COLUMN "upload_id" TYPE bigint USING "upload_id"::bigint; +COMMIT; +""" + + +class Migration(migrations.Migration): + dependencies = [ + ("user_measurements", "0002_remove_usermeasurement_i_owner_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="usermeasurement", + name="upload_id", + field=models.BigIntegerField(null=True), + ), + ] diff --git a/libs/shared/shared/django_apps/user_measurements/migrations/__init__.py b/libs/shared/shared/django_apps/user_measurements/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/user_measurements/models.py b/libs/shared/shared/django_apps/user_measurements/models.py new file mode 100644 index 0000000000..dfd1f09efb --- /dev/null +++ b/libs/shared/shared/django_apps/user_measurements/models.py @@ -0,0 +1,38 @@ +from django.db import models +from psqlextra.models import PostgresPartitionedModel +from psqlextra.types import PostgresPartitioningMethod + +from shared.django_apps.reports.models import ReportType + + +class UserMeasurement(PostgresPartitionedModel): + class PartitioningMeta: + method = PostgresPartitioningMethod.RANGE + key = ["created_at"] + + id = models.BigAutoField(primary_key=True) + repo_id = models.IntegerField(null=True) + commit_id = models.IntegerField(null=True) + upload_id = models.BigIntegerField(null=True) + owner_id = models.IntegerField(null=True) + created_at = models.DateTimeField(auto_now_add=True) + uploader_used = models.CharField() + private_repo = models.BooleanField() + report_type = models.CharField( + null=True, max_length=100, choices=ReportType.choices + ) + + class Meta: + db_table = "user_measurements" + indexes = [ + models.Index(fields=["owner_id"], name="i_owner"), + models.Index(fields=["owner_id", "repo_id"], name="owner_repo"), + models.Index( + fields=["owner_id", "private_repo"], + name="owner_private_repo", + ), + models.Index( + fields=["owner_id", "private_repo", "report_type"], + name="owner_private_repo_report_type", + ), + ] diff --git a/libs/shared/shared/django_apps/user_measurements/partitioning.py b/libs/shared/shared/django_apps/user_measurements/partitioning.py new file mode 100644 index 0000000000..76935821d3 --- /dev/null +++ b/libs/shared/shared/django_apps/user_measurements/partitioning.py @@ -0,0 +1,36 @@ +from dateutil.relativedelta import relativedelta +from psqlextra.partitioning import ( + PostgresCurrentTimePartitioningStrategy, + PostgresPartitioningManager, + PostgresTimePartitionSize, +) +from psqlextra.partitioning.config import PostgresPartitioningConfig + +from shared.django_apps.reports.models import DailyTestRollup +from shared.django_apps.user_measurements.models import UserMeasurement + +# Overlapping partitions will cause errors - https://www.postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE -> "create partitions" +manager = PostgresPartitioningManager( + [ + # 12 partitions ahead, each partition is 1 month + # Partitions can be deleted after 12 months of their starting date, not their creation, via the pgpartition command. + # They won't be automatically deleted though. + # Partitions will be named `[table_name]_[year]_[3-letter month name]`. + PostgresPartitioningConfig( + model=UserMeasurement, + strategy=PostgresCurrentTimePartitioningStrategy( + size=PostgresTimePartitionSize(months=1), + count=12, + max_age=relativedelta(months=12), + ), + ), + PostgresPartitioningConfig( + model=DailyTestRollup, + strategy=PostgresCurrentTimePartitioningStrategy( + size=PostgresTimePartitionSize(months=1), + count=3, + max_age=relativedelta(months=3), + ), + ), + ] +) diff --git a/libs/shared/shared/django_apps/utils/__init__.py b/libs/shared/shared/django_apps/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/django_apps/utils/config.py b/libs/shared/shared/django_apps/utils/config.py new file mode 100644 index 0000000000..dc7d416af6 --- /dev/null +++ b/libs/shared/shared/django_apps/utils/config.py @@ -0,0 +1,47 @@ +import os +from enum import Enum + +from shared.config import get_config + + +class SettingsModule(Enum): + DEV = "codecov.settings_dev" + STAGING = "codecov.settings_staging" + TESTING = "codecov.settings_test" + ENTERPRISE = "codecov.settings_enterprise" + PRODUCTION = "codecov.settings_prod" + + +RUN_ENV = os.environ.get("RUN_ENV", "PRODUCTION") + +if RUN_ENV == "DEV": + settings_module = SettingsModule.DEV.value +elif RUN_ENV == "STAGING": + settings_module = SettingsModule.STAGING.value +elif RUN_ENV == "TESTING": + settings_module = SettingsModule.TESTING.value +elif RUN_ENV == "ENTERPRISE": + settings_module = SettingsModule.ENTERPRISE.value +else: + settings_module = SettingsModule.PRODUCTION.value + + +def should_write_data_to_storage_config_check( + master_switch_key: str, is_codecov_repo: bool, repoid: int +) -> bool: + master_write_switch = get_config( + "setup", + "save_report_data_in_storage", + master_switch_key, + default=False, + ) + if master_write_switch == "restricted_access": + allowed_repo_ids = get_config( + "setup", "save_report_data_in_storage", "repo_ids", default=[] + ) + is_in_allowed_repoids = repoid in allowed_repo_ids + elif master_write_switch == "general_access": + is_in_allowed_repoids = True + else: + is_in_allowed_repoids = False + return master_write_switch and (is_codecov_repo or is_in_allowed_repoids) diff --git a/libs/shared/shared/django_apps/utils/model_utils.py b/libs/shared/shared/django_apps/utils/model_utils.py new file mode 100644 index 0000000000..03d30a8712 --- /dev/null +++ b/libs/shared/shared/django_apps/utils/model_utils.py @@ -0,0 +1,171 @@ +import json +import logging +from typing import Any, Callable, Optional + +from shared.api_archive.archive import ArchiveService +from shared.storage.exceptions import FileNotInStorageError +from shared.utils.ReportEncoder import ReportEncoder + +log = logging.getLogger(__name__) + + +class ArchiveFieldInterfaceMeta(type): + def __subclasscheck__(cls, subclass): + return ( + hasattr(subclass, "get_repository") + and callable(subclass.get_repository) + and hasattr(subclass, "get_commitid") + and callable(subclass.get_commitid) + and hasattr(subclass, "external_id") + ) + + +class ArchiveFieldInterface(metaclass=ArchiveFieldInterfaceMeta): + """Any class that uses ArchiveField must implement this interface""" + + external_id: str + + def get_repository(self): + """Returns the repository object associated with self""" + raise NotImplementedError() + + def get_commitid(self) -> Optional[str]: + """Returns the commitid associated with self. + If no commitid is associated return None. + """ + raise NotImplementedError() + + +class ArchiveField: + """This is a helper class that transparently handles models' fields that are saved in storage. + Classes that use the ArchiveField MUST implement ArchiveFieldInterface. It will throw an error otherwise. + It uses the Descriptor pattern: https://docs.python.org/3/howto/descriptor.html + + Arguments: + should_write_to_storage_fn: Callable function that decides if data should be written to storage. + It should take 1 argument: the object instance. + + rehydrate_fn: Callable function to allow you to decode your saved data into internal representations. + The default value does nothing. + Data retrieved both from DB and storage pass through this function to guarantee consistency. + It should take 2 arguments: the object instance and the encoded data. + + default_value: Any value that will be returned if we can't save the data for whatever reason + + Example: + archive_field = ArchiveField( + should_write_to_storage_fn=should_write_data, + rehydrate_fn=rehidrate_data, + default_value='default' + ) + For a full example check utils/tests/unit/test_model_utils.py in worker + """ + + def __init__( + self, + should_write_to_storage_fn: Callable[[object], bool], + rehydrate_fn: Callable[[object, object], Any] = lambda self, x: x, + json_encoder=ReportEncoder, + default_value_class=lambda: None, + ): + self.default_value_class = default_value_class + self.rehydrate_fn = rehydrate_fn + self.should_write_to_storage_fn = should_write_to_storage_fn + self.json_encoder = json_encoder + + def __set_name__(self, owner, name): + # Validate that the owner class has the methods we need + assert issubclass(owner, ArchiveFieldInterface), ( + "Missing some required methods to use AchiveField" + ) + self.public_name = name + self.db_field_name = "_" + name + self.archive_field_name = "_" + name + "_storage_path" + self.cached_value_property_name = f"__{self.public_name}_cached_value" + + def _get_value_from_archive(self, obj): + repository = obj.get_repository() + archive_service = ArchiveService(repository=repository) + archive_field = getattr(obj, self.archive_field_name) + if archive_field: + try: + file_str = archive_service.read_file(archive_field) + return self.rehydrate_fn(obj, json.loads(file_str)) + except FileNotInStorageError: + log.error( + "Archive enabled field not in storage", + extra=dict( + storage_path=archive_field, + object_id=obj.id, + commit=obj.get_commitid(), + ), + ) + else: + log.debug( + "Both db_field and archive_field are None", + extra=dict( + object_id=obj.id, + commit=obj.get_commitid(), + ), + ) + return self.default_value_class() + + def __get__(self, obj, objtype=None): + cached_value = getattr(obj, self.cached_value_property_name, None) + if cached_value: + return cached_value + db_field = getattr(obj, self.db_field_name) + if db_field is not None: + value = self.rehydrate_fn(obj, db_field) + else: + value = self._get_value_from_archive(obj) + setattr(obj, self.cached_value_property_name, value) + return value + + def __set__(self, obj, value): + # Set the new value + if self.should_write_to_storage_fn(obj): + repository = obj.get_repository() + archive_service = ArchiveService(repository=repository) + old_file_path = getattr(obj, self.archive_field_name) + table_name = obj._meta.db_table + path = archive_service.write_json_data_to_storage( + commit_id=obj.get_commitid(), + table=table_name, + field=self.public_name, + external_id=obj.external_id, + data=value, + encoder=self.json_encoder, + ) + if old_file_path is not None and path != old_file_path: + archive_service.delete_file(old_file_path) + setattr(obj, self.archive_field_name, path) + setattr(obj, self.db_field_name, None) + else: + setattr(obj, self.db_field_name, value) + setattr(obj, self.cached_value_property_name, value) + + +# This is the place for DB trigger logic that's been moved into code +# Owner +def get_ownerid_if_member( + service: str, owner_username: str, owner_id: int +) -> Optional[int]: + from shared.django_apps.codecov_auth.models import Owner + + """ + This is a Python representation of the get_ownerid_if_member DB function. + It expects a service, owner username and owner id, and returns the id of an + owner if the record exists. + """ + owner = ( + Owner.objects.filter( + service=service.lower(), + username=owner_username, + organizations__contains=[owner_id], + private_access=True, + ) + .values("ownerid") + .first() + ) + return owner.get("ownerid") if owner else None diff --git a/libs/shared/shared/django_apps/utils/rollout_utils.py b/libs/shared/shared/django_apps/utils/rollout_utils.py new file mode 100644 index 0000000000..f4a6356186 --- /dev/null +++ b/libs/shared/shared/django_apps/utils/rollout_utils.py @@ -0,0 +1,21 @@ +from random import choice + +from shared.django_apps.rollouts.models import RolloutUniverse + + +def default_random_salt(): + ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + return "".join([choice(ALPHABET) for _ in range(16)]) + + +def rollout_universe_to_override_string(rollout_universe: RolloutUniverse): + if rollout_universe == RolloutUniverse.OWNER_ID: + return "override_owner_ids" + elif rollout_universe == RolloutUniverse.REPO_ID: + return "override_repo_ids" + elif rollout_universe == RolloutUniverse.EMAIL: + return "override_emails" + elif rollout_universe == RolloutUniverse.ORG_ID: + return "override_org_ids" + else: + return "" diff --git a/libs/shared/shared/django_apps/utils/services.py b/libs/shared/shared/django_apps/utils/services.py new file mode 100644 index 0000000000..1c6edb2983 --- /dev/null +++ b/libs/shared/shared/django_apps/utils/services.py @@ -0,0 +1,21 @@ +short_services = { + "gh": "github", + "bb": "bitbucket", + "gl": "gitlab", + "ghe": "github_enterprise", + "gle": "gitlab_enterprise", + "bbs": "bitbucket_server", +} +long_services = {value: key for (key, value) in short_services.items()} + + +def get_long_service_name(service): + if service in short_services: + return short_services[service] + return service + + +def get_short_service_name(service): + if service in long_services: + return long_services[service] + return service diff --git a/libs/shared/shared/encryption/__init__.py b/libs/shared/shared/encryption/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/encryption/oauth.py b/libs/shared/shared/encryption/oauth.py new file mode 100644 index 0000000000..63e902f81f --- /dev/null +++ b/libs/shared/shared/encryption/oauth.py @@ -0,0 +1,97 @@ +import os +from itertools import product + +from shared.config import get_config +from shared.encryption.selector import DEFAULT_ENCRYPTOR_CONSTANT, EncryptorDivider +from shared.encryption.standard import StandardEncryptor + + +def get_encryptor_from_configuration() -> EncryptorDivider: + """Gets an EncryptorDivider capable of dealing with a bunch of possible keys + + First of all, if you don't know, we make an encryptor by concatenating a + bunch of given secret strings in a special way to make a key + + Here is how it works (if you change something, please make the effort to update the docs): + + The customer will have, in their INSTALL YAML, the following: + + ``` + setup: + encryption_secret: " + + With those values prepended, it's easy later to know which encryptor was used to generate what. + So, all we have to do is to pick the encryptor with the same identifier as + the encoded key gives. + + All the user has to do is to not change a key value after it is put in production. The same + way they already can't change encryption_secret + + Then, our system will produce a special Encryptor for the legacy key (which, notice, is the + only one that uses the envvar ENCRYPTION_SECRET). Everytime an encoded string arrive without + an identifier, we will know it's a legacy one, and pick the legacy generator for it. + + Returns: + EncryptorDivider: The encryption instance that will be used + """ + new_key_style = {"v1": "%_#v^tjq*$ggfn!s+q6&6b01rnm$i(yz8&5imgvt=m0g_g$z%9"} + current_hardcoded_key = "v1" + legacy_encryptor = StandardEncryptor( + get_config("setup", "encryption_secret", default=""), + os.getenv("ENCRYPTION_SECRET", ""), + "fYaA^Bj&h89,hs49iXyq]xARuCg", + ) + mapping = {DEFAULT_ENCRYPTOR_CONSTANT: legacy_encryptor} + user_given_encryption_secret_list = get_config( + "setup", "encryption", "keys", default={} + ) + user_given_encryption_secret_mapping = {} + for el in user_given_encryption_secret_list: + user_given_encryption_secret_mapping[el["name"]] = el["value"] + for hardcoded_key_pair, user_key_pair in product( + new_key_style.items(), user_given_encryption_secret_mapping.items() + ): + hardcoded_key_name, hardcoded_key_value = hardcoded_key_pair + user_key_name, user_key_value = user_key_pair + key_name = f"{hardcoded_key_name}_{user_key_name}" + encryptor = StandardEncryptor(hardcoded_key_value, user_key_value) + mapping[key_name] = encryptor + user_key_to_use = get_config("setup", "encryption", "write_key") + if user_key_to_use is None: + key_to_use = DEFAULT_ENCRYPTOR_CONSTANT + else: + key_to_use = f"{current_hardcoded_key}_{user_key_to_use}" + return EncryptorDivider(mapping, key_to_use) diff --git a/libs/shared/shared/encryption/selector.py b/libs/shared/shared/encryption/selector.py new file mode 100644 index 0000000000..6052dba6d2 --- /dev/null +++ b/libs/shared/shared/encryption/selector.py @@ -0,0 +1,40 @@ +import logging + +from shared.encryption.token import decode_token + +log = logging.getLogger(__name__) + +DEFAULT_ENCRYPTOR_CONSTANT = "default_enc" + + +class EncryptorDivider(object): + def __init__(self, encryptor_mapping, write_encryptor_code): + self._encryptor_mapping = encryptor_mapping + self.write_encryptor_code = write_encryptor_code + if self.write_encryptor_code not in self._encryptor_mapping: + log.error("Encryption does not seem to be properly configured") + raise Exception("Encryption misconfigured on write code") + + def get_encryptor_from_code(self, code): + return self._encryptor_mapping[code] + + def decode(self, string): + if isinstance(string, bytes): + string = string.decode() + if "::" not in string: + encryptor_code, code_to_decode = DEFAULT_ENCRYPTOR_CONSTANT, string + else: + encryptor_code, code_to_decode = string.rsplit("::", 1) + encryptor_to_use = self.get_encryptor_from_code(encryptor_code) + return encryptor_to_use.decode(code_to_decode) + + def encode(self, string): + write_encryptor = self.get_encryptor_from_code(self.write_encryptor_code) + result = write_encryptor.encode(string).decode() + if self.write_encryptor_code != DEFAULT_ENCRYPTOR_CONSTANT: + return f"{self.write_encryptor_code}::{result}".encode() + return result.encode() + + def decrypt_token(self, oauth_token): + _oauth: str = self.decode(oauth_token) + return decode_token(_oauth) diff --git a/libs/shared/shared/encryption/standard.py b/libs/shared/shared/encryption/standard.py new file mode 100644 index 0000000000..8198ddaa3b --- /dev/null +++ b/libs/shared/shared/encryption/standard.py @@ -0,0 +1,52 @@ +import hashlib +import os +from base64 import b64decode, b64encode + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + +from shared.encryption.token import decode_token + + +class StandardEncryptor(object): + def __init__(self, *keys, iv=None): + self.backend = default_backend() + self.key = self.generate_key(*keys) + self.bs = 16 + self.iv = iv + + def generate_key(self, *keys): + joined = "".join(keys) + return hashlib.sha256(joined.encode()).digest() + + def decode(self, string): + string = b64decode(string) + iv, to_decrypt = string[:16], string[16:] + cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv), backend=self.backend) + decryptor = cipher.decryptor() + return self._unpad(decryptor.update(to_decrypt) + decryptor.finalize()).decode() + + def encode(self, string): + if self.iv is None: + iv = os.urandom(self.bs) + else: + iv = self.iv + cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv), backend=self.backend) + encryptor = cipher.encryptor() + result = encryptor.update(self._pad(string).encode()) + encryptor.finalize() + return b64encode(iv + result) + + def _unpad(self, s): + return s[: -ord(s[len(s) - 1 :])] + + def _pad(self, s): + return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs) + + def decrypt_token(self, oauth_token): + _oauth: str = self.decode(oauth_token) + return decode_token(_oauth) + + +class EncryptorWithAlreadyGeneratedKey(StandardEncryptor): + def generate_key(self, *keys): + return keys[0] diff --git a/libs/shared/shared/encryption/token.py b/libs/shared/shared/encryption/token.py new file mode 100644 index 0000000000..0d59b2f5f1 --- /dev/null +++ b/libs/shared/shared/encryption/token.py @@ -0,0 +1,45 @@ +from shared.typings.oauth_token_types import OauthConsumerToken + + +def encode_token(token: OauthConsumerToken) -> str: + # Different git providers encode different information on the oauth_token column. + # Check decode_token function below. + if not token.get("secret") and not token.get("refresh_token"): + return token["key"] + + string_to_save = ( + token["key"] + + f":{token['secret'] if token.get('secret') else ' '}" + + (f":{token['refresh_token']}" if token.get("refresh_token") else "") + ) + return string_to_save + + +def decode_token(_oauth: str) -> OauthConsumerToken: + """ + This function decrypts a oauth_token into its different parts. + At the moment it does different things depending on the provider. + + - bitbucket + Encodes the token as f"{key}:{secret}" + - github + - gitlab + Encodes the token as f"{key}: :{refresh_token}" + (notice the space where {secret} should go to avoid having '::', used by decode function) + """ + token = {} + colon_count = _oauth.count(":") + if colon_count > 1: + # Github + Gitlab (post refresh tokens) + token["key"], token["secret"], token["refresh_token"] = _oauth.split(":", 2) + if token["secret"] == " ": + # We remove the secret if it's our placeholder value + token["secret"] = None + elif colon_count == 1: + # Bitbucket + token["key"], token["secret"] = _oauth.split(":", 1) + else: + # Github + Gitlab (pre refresh tokens) + token["key"] = _oauth + token["secret"] = None + return token diff --git a/libs/shared/shared/encryption/yaml_secret.py b/libs/shared/shared/encryption/yaml_secret.py new file mode 100644 index 0000000000..eca358c13d --- /dev/null +++ b/libs/shared/shared/encryption/yaml_secret.py @@ -0,0 +1,32 @@ +from shared.config import get_config +from shared.encryption.selector import DEFAULT_ENCRYPTOR_CONSTANT, EncryptorDivider +from shared.encryption.standard import ( + EncryptorWithAlreadyGeneratedKey, + StandardEncryptor, +) + + +def get_yaml_secret_encryptor(): + return EncryptorDivider( + encryptor_mapping={ + DEFAULT_ENCRYPTOR_CONSTANT: EncryptorWithAlreadyGeneratedKey( + b"]\xbb\x13\xf9}\xb3\xb7\x03)*0Kv\xb2\xcet" # Same secret as in the main app + ), + "v1": EncryptorWithAlreadyGeneratedKey( + b"\xc6f\x02\xf2Tg\x1d\xfa\x19\xe6\xc3 Environment: + """ + Gets the current environment of the system + + Returns: + Environment: The current environment + """ + return _get_cached_current_env() + + +def is_enterprise() -> bool: + """Tells whether the current environment is enterprise or not + + Returns: + bool: True if the current environment is enterprise, else False + """ + return get_current_env() == Environment.enterprise + + +@lru_cache() +def _get_cached_current_env() -> Environment: + return _calculate_current_env() + + +def _get_current_folder() -> str: + return getattr(sys, "_MEIPASS", os.getcwd()) + + +def _calculate_current_env() -> Environment: + run_env = os.getenv("RUN_ENV") + if run_env is not None: + if run_env == "ENTERPRISE": + return Environment.enterprise + elif run_env == "DEV": + return Environment.local + else: + return Environment.production + os.environ["CODECOV_HOME"] = _get_current_folder() + some_dir = Path(os.getenv("CODECOV_HOME")) + if os.path.exists(some_dir / "src/is_enterprise"): + return Environment.enterprise + if os.getenv("CURRENT_ENVIRONMENT", "production") == "local": + return Environment.local + return Environment.production + + +def get_external_dependencies_folder(): + return get_config( + "services", "external_dependencies_folder", default="./external_deps" + ) diff --git a/libs/shared/shared/events/amplitude/__init__.py b/libs/shared/shared/events/amplitude/__init__.py new file mode 100644 index 0000000000..72e5506b4a --- /dev/null +++ b/libs/shared/shared/events/amplitude/__init__.py @@ -0,0 +1,4 @@ +from shared.events.amplitude.constants import UNKNOWN_USER_OWNERID +from shared.events.amplitude.publisher import AmplitudeEventPublisher + +__all__ = ["AmplitudeEventPublisher", "UNKNOWN_USER_OWNERID"] diff --git a/libs/shared/shared/events/amplitude/constants.py b/libs/shared/shared/events/amplitude/constants.py new file mode 100644 index 0000000000..dfc250140e --- /dev/null +++ b/libs/shared/shared/events/amplitude/constants.py @@ -0,0 +1 @@ +UNKNOWN_USER_OWNERID = -1 # Amplitude User ID for unknown users. diff --git a/libs/shared/shared/events/amplitude/metrics.py b/libs/shared/shared/events/amplitude/metrics.py new file mode 100644 index 0000000000..a8b40cec35 --- /dev/null +++ b/libs/shared/shared/events/amplitude/metrics.py @@ -0,0 +1,18 @@ +from shared.metrics import Counter + +AMPLITUDE_PUBLISH_COUNTER = Counter( + "amplitude_publish", + "Total Amplitude publish calls", + [ + "event_type", # AmplitudeEventType + ], +) + +AMPLITUDE_PUBLISH_FAILURE_COUNTER = Counter( + "amplitude_publish_failure", + "Total Amplitude publish calls that failed", + [ + "event_type", # AmplitudeEventType + "error", # Exception class name + ], +) diff --git a/libs/shared/shared/events/amplitude/publisher.py b/libs/shared/shared/events/amplitude/publisher.py new file mode 100644 index 0000000000..eed4cc5bb8 --- /dev/null +++ b/libs/shared/shared/events/amplitude/publisher.py @@ -0,0 +1,173 @@ +import logging +from typing import Union + +from django.conf import settings + +from amplitude import Amplitude, BaseEvent, Config, EventOptions +from shared.environment.environment import Environment, get_current_env +from shared.events.amplitude import UNKNOWN_USER_OWNERID +from shared.events.amplitude.metrics import ( + AMPLITUDE_PUBLISH_COUNTER, + AMPLITUDE_PUBLISH_FAILURE_COUNTER, +) +from shared.events.amplitude.types import ( + AMPLITUDE_REQUIRED_PROPERTIES, + AmplitudeEventProperties, + AmplitudeEventType, +) +from shared.events.base import ( + EventPublisher, + MissingEventPropertyException, +) +from shared.metrics import inc_counter +from shared.utils.snake_to_camel_case import snake_to_camel_case + +log = logging.getLogger(__name__) + + +class AmplitudeEventPublisher(EventPublisher): + """ + + EventPublisher for Amplitude events. + + """ + + client: Amplitude + anon_user_id = "anon" + + def __init__(self, override_env=False): + if get_current_env() != Environment.production and not override_env: + log.info("RUN_ENV is not production. Amplitude events will not be tracked.") + self.client = StubbedAmplitudeClient() + return + + api_key = settings.AMPLITUDE_API_KEY + if api_key is None: + log.warning( + "AMPLITUDE_API_KEY is not defined. Amplitude events will not be tracked." + ) + self.client = StubbedAmplitudeClient() + else: + # min_id_length necessary to accommodate our ownerids + self.client = Amplitude(api_key, Config(min_id_length=1)) + + def publish( + self, event_type: AmplitudeEventType, event_properties: AmplitudeEventProperties + ): + inc_counter( + AMPLITUDE_PUBLISH_COUNTER, + labels={ + "event_type": event_type, + }, + ) + try: + self._unsafe_publish(event_type, event_properties) + except Exception as e: + inc_counter( + AMPLITUDE_PUBLISH_FAILURE_COUNTER, + labels={"event_type": event_type, "error": e.__class__.__name__}, + ) + log.error( + "Failed to publish Amplitude event", + extra=dict( + event_type=event_type, + error_name=e.__class__.__name__, + error_message=str(e), + ), + ) + + def _unsafe_publish( + self, event_type: AmplitudeEventType, event_properties: AmplitudeEventProperties + ): + user_id = event_properties["user_ownerid"] + + # Handle special set_orgs event + if event_type == "set_orgs": + if user_id == UNKNOWN_USER_OWNERID: + # We don't want to track these events for unknown users, + # shouldn't happen, but can't hurt to add this. + return + + if "org_ids" not in event_properties: + raise MissingEventPropertyException( + "Property 'org_ids' is required for event type 'set_orgs'" + ) + + self.client.set_group( + group_type="org", + group_name=[str(orgid) for orgid in event_properties["org_ids"]], + event_options=EventOptions( + user_id=str(event_properties["user_ownerid"]) + ), + ) + return + + # Handle normal events + structured_payload = self.__transform_properties(event_type, event_properties) + + # Track event with validated payload, we will raise an exception before + # this if bad payload. + org = structured_payload.get("ownerid", None) + self.client.track( + BaseEvent( + event_type, + user_id=str(user_id) + if user_id != UNKNOWN_USER_OWNERID + else self.anon_user_id, + event_properties=structured_payload, + groups={"org": org} if org is not None else {}, + ) + ) + return + + def __transform_properties( + self, event_type: AmplitudeEventType, event_properties: AmplitudeEventProperties + ) -> dict: + """ + + Helper function to validate all required properties exist for the provided + event_type and ensure only those properties are sent in the payload. + + """ + + payload = {} + + for property in AMPLITUDE_REQUIRED_PROPERTIES[event_type]: + if property not in event_properties: + raise MissingEventPropertyException( + f"Property {property} is required for event type {event_type}" + ) + + payload[snake_to_camel_case(property)] = event_properties.get(property) + + return payload + + +class StubbedAmplitudeClient(Amplitude): + """ + + Stubbed Amplitude client for use when no AMPLITUDE_API_KEY is defined. + + """ + + def __init__(self): + return + + def set_group( + self, + group_type: str, + group_name: Union[str, list[str]], + event_options: EventOptions, + ): + log.info( + f"StubbedAmplitudeClient set_group {group_type}: {group_name}", + extra=event_options.get_event_body(), + ) + return + + def track(self, event: BaseEvent): + log.info( + f"StubbedAmplitudeClient tracked event {event.event_type}", + extra=event.get_event_body(), + ) + return diff --git a/libs/shared/shared/events/amplitude/types.py b/libs/shared/shared/events/amplitude/types.py new file mode 100644 index 0000000000..ef5803513d --- /dev/null +++ b/libs/shared/shared/events/amplitude/types.py @@ -0,0 +1,73 @@ +""" + +Add new events as a string in the AmplitudeEventType type below! + +Adding event types in this way provides type safety for names and allows us to +specify required properties. +E.g., every 'App Installed' event must have the 'ownerid' property. + +Guidelines: + - Event names should: + - be of the form "[Noun] [Past-tense verb]" and + - have each word capitalized. + - Keep the event types very generic as we have a limited number of them. + Instead, add more detail in `properties` where possible. + - Try to keep event property names unique to the event type to avoid + accidental correlation of unrelated events. + - Never include names, only use ids. E.g., use repoid instead of repo name. + +""" + +from typing import Literal, TypedDict + +type AmplitudeEventType = Literal[ + "User Created", + "User Logged in", + "App Installed", + "Upload Received", + "set_orgs", # special event for setting a user's member orgs +] + +""" + +Add Event Properties here, define their types in AmplitudeEventProperties, +and finally add them as required properties where needed in +AMPLITUDE_REQUIRED_PROPERTIES. + +Note: these are converted to camel case before they're sent to Amplitude! + +""" +type AmplitudeEventProperty = Literal[ + "user_ownerid", + "ownerid", + "org_ids", + "repoid", + "commitid", + "pullid", + "upload_type", +] + + +# Separate type required to make user_ownerid mandatory with total=True +class BaseAmplitudeEventProperties(TypedDict, total=True): + user_ownerid: int # ownerid of user performing event action + + +class AmplitudeEventProperties(BaseAmplitudeEventProperties, total=False): + ownerid: int # ownerid of owner being acted upon + org_ids: list[int] + repoid: int + commitid: int # commit.id NOT commit.commitid. We do not want a commit SHA here! + pullid: int | None + upload_type: Literal["Coverage report", "Bundle", "Test results"] + + +# user_ownerid is always required, don't need to check here. +AMPLITUDE_REQUIRED_PROPERTIES: dict[ + AmplitudeEventType, list[AmplitudeEventProperty] +] = { + "User Created": [], + "User Logged in": [], + "App Installed": ["ownerid"], + "Upload Received": ["ownerid", "repoid", "commitid", "pullid", "upload_type"], +} diff --git a/libs/shared/shared/events/base.py b/libs/shared/shared/events/base.py new file mode 100644 index 0000000000..1b42dbcda8 --- /dev/null +++ b/libs/shared/shared/events/base.py @@ -0,0 +1,11 @@ +from abc import ABC, abstractmethod + + +class MissingEventPropertyException(Exception): + pass + + +class EventPublisher[T, P](ABC): + @abstractmethod + def publish(self, event_type: T, event_properties: P): + pass diff --git a/libs/shared/shared/github/__init__.py b/libs/shared/shared/github/__init__.py new file mode 100644 index 0000000000..ca494f42a2 --- /dev/null +++ b/libs/shared/shared/github/__init__.py @@ -0,0 +1,172 @@ +import logging +from time import time +from typing import Literal +from urllib.parse import urlparse + +import jwt +import requests + +import shared.torngit as torngit +from shared.config import get_config, load_file_from_path_at_config +from shared.helpers.cache import cache + +log = logging.getLogger(__name__) + +loaded_pems = None + +pem_paths = { + "github": ("github", "integration", "pem"), + "github_enterprise": ("github_enterprise", "integration", "pem"), +} + + +def load_pem_from_path(pem_path: str) -> str: + parsed_path = urlparse(pem_path) + if parsed_path.scheme == "yaml+file": + # The URL is a path to a key in the YAML + # The key's value points to a file mounted in the FS + path = parsed_path.netloc.split(".") + return load_file_from_path_at_config(*path) + raise Exception("Unknown schema to load PEM") + + +def get_pem(pem_name: str | None = None, pem_path: str | None = None) -> str: + if pem_path: + return load_pem_from_path(pem_path) + if pem_name: + path = pem_paths[pem_name] + return load_file_from_path_at_config(*path) + raise Exception("No PEM provided to get installation token") + + +InstallationErrorCause = Literal[ + "installation_not_found", + "permission_error", + "requires_authentication", + "installation_suspended", + "validation_failed_or_spammed", + "rate_limit", + "unknown_error", +] + + +class InvalidInstallationError(Exception): + def __init__(self, error_cause: InstallationErrorCause, *args: object) -> None: + super().__init__(error_cause, *args) + self.error_cause = error_cause + + +def decide_installation_error_cause( + response: requests.Response, +) -> InstallationErrorCause: + # https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app + is_suspended = ( + response.json().get("message") == "This installation has been suspended" + ) + is_rate_limit = ( + "X-RateLimit-Remaining" in response.headers or "Retry-After" in response.headers + ) + match response.status_code: + case 401: + return "requires_authentication" + case 404: + return "installation_not_found" + case 422: + return "validation_failed_or_spammed" + case 403: + if is_suspended: + return "installation_suspended" + elif is_rate_limit: + return "rate_limit" + else: + return "permission_error" + case _: + return "unknown_error" + + +def get_github_jwt_token( + service: str, app_id: str | None = None, pem_path: str | None = None +) -> str: + # https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/ + now = int(time()) + payload = { + # issued at time + "iat": now, + # JWT expiration time (max 10 minutes) + "exp": now + int(get_config(service, "integration", "expires", default=500)), + # Integration's GitHub identifier + "iss": app_id or get_config(service, "integration", "id"), + } + pem_kwargs = dict(pem_path=pem_path) if pem_path else dict(pem_name=service) + return jwt.encode(payload, get_pem(**pem_kwargs), algorithm="RS256") + + +# The integration tokens are valid for 1h +# We use 30min of that +@cache.cache_function(ttl=1800) +def get_github_integration_token( + service, + integration_id=None, + app_id: str | None = None, + pem_path: str | None = None, +) -> str: + # https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/ + token = get_github_jwt_token(service, app_id, pem_path) + if integration_id: + if service == "github": + api_endpoint = torngit.Github.get_api_url() + host_override = torngit.Github.get_api_host_header() + url = torngit.Github.count_and_get_url_template( + url_name="get_github_integration_token" + ).substitute(api_endpoint=api_endpoint, integration_id=integration_id) + else: + api_endpoint = torngit.GithubEnterprise.get_api_url() + host_override = torngit.GithubEnterprise.get_api_host_header() + url = torngit.GithubEnterprise.count_and_get_url_template( + url_name="get_github_integration_token" + ).substitute(api_endpoint=api_endpoint, integration_id=integration_id) + + headers = { + "Accept": "application/vnd.github.machine-man-preview+json", + "User-Agent": "Codecov", + "Authorization": "Bearer %s" % token, + } + if host_override is not None: + headers["Host"] = host_override + + res = requests.post(url, headers=headers) + if res.status_code in [401, 403, 404, 422]: + error_cause = decide_installation_error_cause(res) + log.warning( + "Integration could not be found to fetch token from or unauthorized", + extra=dict( + git_service=service, + integration_id=integration_id, + api_endpoint=api_endpoint, + error_cause=error_cause, + github_error=res.json().get("message"), + ), + ) + raise InvalidInstallationError(error_cause) + try: + res.raise_for_status() + except requests.exceptions.HTTPError: + log.exception( + "Github Integration Error on service %s", + service, + extra=dict(code=res.status_code, text=res.text), + ) + raise + res_json = res.json() + log.info( + "Requested and received a Github Integration token", + extra=dict( + expires_at=res_json.get("expires_at"), + permissions=res_json.get("permissions"), + repository_selection=res_json.get("repository_selection"), + integration_id=integration_id, + ), + ) + return res_json["token"] + else: + return token diff --git a/libs/shared/shared/helpers/__init__.py b/libs/shared/shared/helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/helpers/cache.py b/libs/shared/shared/helpers/cache.py new file mode 100644 index 0000000000..a172039be2 --- /dev/null +++ b/libs/shared/shared/helpers/cache.py @@ -0,0 +1,235 @@ +import asyncio +import base64 +import hashlib +import json +import logging +from functools import wraps +from typing import Any, Callable, Hashable + +from redis import Redis, RedisError + +log = logging.getLogger(__name__) + +NO_VALUE = object() + +DEFAULT_TTL = 120 + + +def attempt_json_dumps(value: Any) -> str: + def assert_string_keys(d: dict[Any, Any]) -> None: + for k, v in d.items(): + if not isinstance(k, str): + raise TypeError( + f"Attempted to JSON-serialize a dictionary with non-string key: {k}" + ) + if isinstance(v, dict): + assert_string_keys(v) + + if isinstance(value, dict): + assert_string_keys(value) + + return json.dumps(value) + + +def make_hash_sha256(o: Any) -> str: + """Provides a machine-independent, consistent hash value for any object + + Args: + o (Any): Any object we want + + Returns: + str: a sha256-based hash that is always the same for the same object + """ + hasher = hashlib.sha256() + hasher.update(repr(make_hashable(o)).encode()) + return base64.b64encode(hasher.digest()).decode() + + +def make_hashable(o: Any) -> Hashable: + """ + Converts any object into an object that will have a consistent hash + """ + if isinstance(o, (tuple, list)): + return tuple((make_hashable(e) for e in o)) + if isinstance(o, dict): + return tuple(sorted((k, make_hashable(v)) for k, v in o.items())) + if isinstance(o, (set, frozenset)): + return tuple(sorted(make_hashable(e) for e in o)) + return o + + +class BaseBackend(object): + """ + This is the interface a class needs to honor in order to work as a backend. + + The only two needed functions are `get` and `set`, which will fetch information from the + cache and send information to it, respectively. + + However the cache wants to work internally, it's their choice. They only need to be able to + `set` and `get` without raising any exceptions + """ + + def get(self, key: str) -> Any: + """Returns a cached value from the cache, or NO_VALUE, if no cache is set for that key + + Args: + key (str): The key that represents the objecr + + Returns: + Any: The object that is possibly cached, or NO_VALUE, if no cache was there + """ + raise NotImplementedError() + + def set(self, key: str, ttl: int, value: Any): + raise NotImplementedError() + + +class NullBackend(BaseBackend): + """ + This is the default implementation of BaseBackend that is used. + + It essentially `gets` as if nothing is cached, and does not cache anything when requested + to. + + This makes the cache virtually transparent. It acts as if no cache was there + """ + + def get(self, key: str) -> Any: + return NO_VALUE + + def set(self, key: str, ttl: int, value: Any): + pass + + +class RedisBackend(BaseBackend): + def __init__(self, redis_connection: Redis): + self.redis_connection = redis_connection + + def get(self, key: str) -> Any: + try: + serialized_value = self.redis_connection.get(key) + except RedisError: + log.warning("Unable to fetch from cache on redis", exc_info=True) + return NO_VALUE + if serialized_value is None: + return NO_VALUE + try: + return json.loads(serialized_value) + except ValueError: + return NO_VALUE + + def set(self, key: str, ttl: int, value: Any): + try: + serialized_value = attempt_json_dumps(value) + self.redis_connection.setex(key, ttl, serialized_value) + except RedisError: + log.warning("Unable to set cache on redis", exc_info=True) + except TypeError: + log.exception( + f"Attempted to cache a type that is not JSON-serializable: {value}" + ) + + +class OurOwnCache(object): + """ + This is codecov distributed cache's implementation. + + The tldr to use it is, given a function f: + + ``` + from shared.helpers.cache import cache + + @cache.cache_function() + def f(...): + ... + ``` + + Now to explain its internal workings. + + This is a configurable-at-runtime cache. Its whole idea is based on the fact that it does + not need information at import-time. This allows us to use it transparently and still + not have to change tests, for example, due to it. All tests occur as if the cache was + not there. + + All that is needed to configure the backend is to do + + ``` + cache.configure(any_backend) + ``` + + which we currently do at `worker_process_init` time with a RedisBackend instance. Other + instances can be plugged in easily, once needed. A backend is any implementation + of `BaseBackend`, which is described at their docstrings. + + When `cache.cache_function()` is called, a `FunctionCacher` is returned. They do the heavy + lifting of actually decorating the function properly, dealign with sync-async context. + + """ + + def __init__(self): + self._backend = NullBackend() + + def configure(self, backend: BaseBackend): + self._backend = backend + + def get_backend(self) -> BaseBackend: + return self._backend + + def cache_function(self, ttl: int = DEFAULT_TTL) -> "FunctionCacher": + """Creates a FunctionCacher with all the needed configuration to cache a function + + Args: + ttl (int, optional): The time-to-live of the cache + + Returns: + FunctionCacher: A FunctionCacher that can decorate any callable + """ + return FunctionCacher(self, ttl) + + +cache = OurOwnCache() +# TODO(swatinem): maybe initialize the cache directly at module load time? +# cache.configure(RedisBackend(get_redis_connection())) + + +class FunctionCacher(object): + def __init__(self, cache_instance: OurOwnCache, ttl: int): + self.cache_instance = cache_instance + self.ttl = ttl + + def __call__(self, func) -> Callable: + if asyncio.iscoroutinefunction(func): + return self.cache_async_function(func) + return self.cache_synchronous_function(func) + + def cache_synchronous_function(self, func: Callable) -> Callable: + @wraps(func) + def wrapped(*args, **kwargs): + key = self.generate_key(func, args, kwargs) + value = self.cache_instance.get_backend().get(key) + if value is not NO_VALUE: + return value + result = func(*args, **kwargs) + self.cache_instance.get_backend().set(key, self.ttl, result) + return result + + return wrapped + + def generate_key(self, func, args, kwargs) -> str: + func_name = make_hash_sha256(func.__name__) + tupled_args = make_hash_sha256(args) + frozen_kwargs = make_hash_sha256(kwargs) + return ":".join(["cache", func_name, tupled_args, frozen_kwargs]) + + def cache_async_function(self, func: Callable) -> Callable: + @wraps(func) + async def wrapped(*args, **kwargs): + key = self.generate_key(func, args, kwargs) + value = self.cache_instance.get_backend().get(key) + if value is not NO_VALUE: + return value + result = await func(*args, **kwargs) + self.cache_instance.get_backend().set(key, self.ttl, result) + return result + + return wrapped diff --git a/libs/shared/shared/helpers/color.py b/libs/shared/shared/helpers/color.py new file mode 100644 index 0000000000..a27d92007e --- /dev/null +++ b/libs/shared/shared/helpers/color.py @@ -0,0 +1,33 @@ +from itertools import chain + +from colour import Color + +colors = list( + chain( + Color("#e05d44").range_to("#fe7d37", 20), + Color("#fe7d37").range_to("#dfb317", 20), + Color("#dfb317").range_to("#a4a61d", 20), + Color("#a4a61d").range_to("#97CA00", 20), + Color("#97CA00").range_to("#4c1", 21), + ) +) + + +def coverage_to_color(range_low, range_high): + _div = float(range_high) - float(range_low) + + def _color(cov): + cov = float(cov or 0) + if int(cov) == 100: + return Color(colors[-1].hex) + + if cov <= range_low: + return Color(colors[0].hex) + + elif cov >= range_high: + return Color(colors[-1].hex) + + offset = ((float(cov - range_low)) / _div) * 100.0 + return Color(colors[int(offset)].hex) + + return _color diff --git a/libs/shared/shared/helpers/flag.py b/libs/shared/shared/helpers/flag.py new file mode 100644 index 0000000000..80992cd66d --- /dev/null +++ b/libs/shared/shared/helpers/flag.py @@ -0,0 +1,24 @@ +class Flag(object): + def __init__( + self, report, name, totals=None, carriedforward=False, carriedforward_from=None + ): + self._report = report + self.name = name + # TODO cache by storing in database + self._totals = totals + self.carriedforward = carriedforward + self.carriedforward_from = carriedforward_from + + @property + def report(self): + """returns the report filtered by this flag""" + return self._report.filter(paths=None, flags=[self.name]) + + @property + def totals(self): + if not self._totals: + self._totals = self.report.totals + return self._totals + + def apply_diff(self, diff): + return self.report.apply_diff(diff, _save=False) diff --git a/libs/shared/shared/helpers/numeric.py b/libs/shared/shared/helpers/numeric.py new file mode 100644 index 0000000000..0ba51d4cc2 --- /dev/null +++ b/libs/shared/shared/helpers/numeric.py @@ -0,0 +1,14 @@ +def maxint(string): + if len(string) > 5: + return 99999 + return int(string) + + +def ratio(x, y): + if x == y: + return "100" + + elif x == 0 or y == 0: + return "0" + + return "%.5f" % round((float(x) / float(y)) * 100, 5) diff --git a/libs/shared/shared/helpers/redis.py b/libs/shared/shared/helpers/redis.py new file mode 100644 index 0000000000..3ee5c1c82a --- /dev/null +++ b/libs/shared/shared/helpers/redis.py @@ -0,0 +1,21 @@ +from redis import Redis + +from shared.config import get_config + + +def get_redis_url() -> str: + url = get_config("services", "redis_url") + if url is not None: + return url + hostname = "redis" + port = 6379 + return f"redis://{hostname}:{port}" + + +def get_redis_connection() -> Redis: + url = get_redis_url() + return _get_redis_instance_from_url(url) + + +def _get_redis_instance_from_url(url): + return Redis.from_url(url) diff --git a/libs/shared/shared/helpers/yaml.py b/libs/shared/shared/helpers/yaml.py new file mode 100644 index 0000000000..1b7ce5c381 --- /dev/null +++ b/libs/shared/shared/helpers/yaml.py @@ -0,0 +1,28 @@ +def walk(_dict, keys, _else=None): + try: + for key in keys: + if hasattr(_dict, "_asdict"): + # namedtuples + _dict = getattr(_dict, key) + elif hasattr(_dict, "__getitem__"): + _dict = _dict[key] + else: + _dict = getattr(_dict, key) + return _dict + except Exception: + return _else + + +def default_if_true(value): + if value is True: + yield "default", {} + elif isinstance(value, dict): + for key, data in value.items(): + if data is False: + continue + elif data is True: + yield key, {} + elif not isinstance(data, dict) or data.get("enabled") is False: + continue + else: + yield key, data diff --git a/libs/shared/shared/helpers/zfill.py b/libs/shared/shared/helpers/zfill.py new file mode 100644 index 0000000000..c07b973e6e --- /dev/null +++ b/libs/shared/shared/helpers/zfill.py @@ -0,0 +1,6 @@ +def zfill(lst, index, value): + ll = len(lst) + if len(lst) <= index: + lst.extend([None] * (index - ll + 1)) + lst[index] = value + return lst diff --git a/libs/shared/shared/labelanalysis/__init__.py b/libs/shared/shared/labelanalysis/__init__.py new file mode 100644 index 0000000000..6e78e1fdc4 --- /dev/null +++ b/libs/shared/shared/labelanalysis/__init__.py @@ -0,0 +1,10 @@ +from shared.utils.enums import CodecovDatabaseEnum + + +class LabelAnalysisRequestState(CodecovDatabaseEnum): + CREATED = (1,) + FINISHED = (2,) + ERROR = (3,) + + def __init__(self, db_id): + self.db_id = db_id diff --git a/libs/shared/shared/license/__init__.py b/libs/shared/shared/license/__init__.py new file mode 100644 index 0000000000..eee648b467 --- /dev/null +++ b/libs/shared/shared/license/__init__.py @@ -0,0 +1,103 @@ +import binascii +from dataclasses import dataclass +from datetime import datetime +from json import loads + +from shared.config import get_config +from shared.encryption.standard import EncryptorWithAlreadyGeneratedKey + + +@dataclass +class LicenseInformation(object): + is_valid: bool = False + is_trial: bool = False + message: str = None + url: str = None + number_allowed_users: int = None + number_allowed_repos: int = None + expires: datetime = None + is_pr_billing: bool = False + + +LICENSE_ERRORS_MESSAGES = { + "invalid": "Enterprise license is invalid. Please contact Codecov team to renew license.", + "no-license": "No license key found. Please contact enterprise@codecov.io to issue a license key. Thank you!", + "unknown": "An unknown issue occured when checking your license. Please contact support.", + "expired": "License has expired.", + "demo-mode": "Currently in demo mode. No license key provided. Application restrictions apply.", + "url-mismatch": "License url mismatch. If you have changed your codecov_url please contact staff to generate a new license key.", + "users-exceeded": "Number of users exceeds license limit.", + "repos-exceeded": "Number of repositories exceeds license limit.", +} + + +def load_raw_license_into_dict(raw_license): + encryptor = EncryptorWithAlreadyGeneratedKey( + b"\xfb\xe9\x1b4`\xff\xe2\xa1\xfa\xe3\xd0\xf9\x8d\xa6%\x7f" + ) + return loads(encryptor.decode(raw_license)) + + +def get_current_license(): + current_license = get_config("setup", "enterprise_license") + if current_license is None: + return LicenseInformation( + is_valid=False, message=LICENSE_ERRORS_MESSAGES["no-license"] + ) + return parse_license(current_license) + + +def parse_license(raw_license): + try: + license_dict = load_raw_license_into_dict(raw_license) + except (binascii.Error, ValueError): + return LicenseInformation(is_valid=False) + number_allowed_users, number_allowed_repos = None, None + if license_dict.get("users"): + number_allowed_users = int(license_dict.get("users")) + if license_dict.get("repos"): + number_allowed_repos = int(license_dict.get("repos")) + if license_dict.get("pr_billing"): + is_pr_billing = bool(license_dict.get("pr_billing")) + else: + is_pr_billing = False + return LicenseInformation( + is_valid=True, + message=None, + url=license_dict.get("url"), + number_allowed_users=number_allowed_users, + number_allowed_repos=number_allowed_repos, + expires=datetime.strptime(license_dict["expires"], "%Y-%m-%d %H:%M:%S"), + is_trial=license_dict.get("trial"), + is_pr_billing=is_pr_billing, + ) + + +def startup_license_logging(): + """ + Makes troubleshooting license issues easier - called by startup process in worker and api + """ + if get_config("setup", "enterprise_license"): + statements_to_print = [ + "", # padding + "==> Checking License", + ] + + current_license = get_current_license() + is_valid = current_license.is_valid + statements_to_print.append( + f" License is {'valid' if is_valid else 'INVALID'}" + ) + + if current_license.message: + statements_to_print.append(f" Warning: {current_license.message}") + + exp_date = current_license.expires + statements_to_print.append( + f" License expires {datetime.strftime(exp_date, '%Y-%m-%d %H:%M:%S') if exp_date else 'NOT FOUND'} <==" + ) + statements_to_print.append("") # padding + + # printing the message in a single statement so the lines won't get split up + # among all the other messages during startup + print(*statements_to_print, sep="\n") # noqa: T201 diff --git a/libs/shared/shared/metrics/__init__.py b/libs/shared/shared/metrics/__init__.py new file mode 100644 index 0000000000..728c965ee0 --- /dev/null +++ b/libs/shared/shared/metrics/__init__.py @@ -0,0 +1,46 @@ +import logging + +from prometheus_client import Counter, Gauge, Histogram, Summary, start_http_server + +log = logging.getLogger(__name__) + +start_prometheus = start_http_server + + +__all__ = [ + "Counter", + "Gauge", + "Histogram", + "Summary", + "start_prometheus", +] + + +def inc_counter(counter: Counter, labels: dict | None = None) -> None: + try: + if labels: + counter.labels(**labels).inc() + else: + counter.inc() + except Exception as e: + log.warning(f"Error incrementing counter {counter._name}: {e}") + + +def set_gauge(gauge: Gauge, value, labels: dict | None = None) -> None: + try: + if labels: + gauge.labels(**labels).set(value) + else: + gauge.set(value) + except Exception as e: + log.warning(f"Error setting gauge {gauge._name}: {e}") + + +def set_summary(summary: Summary, value, labels: dict | None = None) -> None: + try: + if labels: + summary.labels(**labels).observe(value) + else: + summary.observe(value) + except Exception as e: + log.warning(f"Error observing summary {summary._name}: {e}") diff --git a/libs/shared/shared/orms/__init__.py b/libs/shared/shared/orms/__init__.py new file mode 100644 index 0000000000..83a179e442 --- /dev/null +++ b/libs/shared/shared/orms/__init__.py @@ -0,0 +1,5 @@ +from django.db import models + + +def _is_django_model(object): + return isinstance(object, models.Model) diff --git a/libs/shared/shared/orms/owner_helper.py b/libs/shared/shared/orms/owner_helper.py new file mode 100644 index 0000000000..2c886537c2 --- /dev/null +++ b/libs/shared/shared/orms/owner_helper.py @@ -0,0 +1,25 @@ +from typing import Any + +from shared.django_apps.codecov_auth.models import Owner +from shared.orms import _is_django_model + + +class DjangoSQLAlchemyOwnerWrapper: + # Owner type can be a Django Owner | SQLAlchemy Owner - added Any to help with IDE typing + @staticmethod + def get_github_app_installations(owner: Owner | Any): + if _is_django_model(owner): + return owner.github_app_installations.all() + else: + return owner.github_app_installations + + # TODO: leaving as a template for the future in case it works + # def template_thing_that_writes(self, owner): + # if self._is_django_model(owner): + # # stuff + # owner.save() + # else: + # db_session = owner.get_db_session() + # # stuff + # # TBD if we'd commit right away to follow sqlalchemy 'commit at the end' + # # db_session.commit() diff --git a/libs/shared/shared/orms/repository_helper.py b/libs/shared/shared/orms/repository_helper.py new file mode 100644 index 0000000000..9efadcda78 --- /dev/null +++ b/libs/shared/shared/orms/repository_helper.py @@ -0,0 +1,14 @@ +from typing import Any + +from shared.django_apps.core.models import Repository +from shared.orms import _is_django_model + + +class DjangoSQLAlchemyRepositoryWrapper: + # Repository type can be a Django Repository | SQLAlchemy Repository - added Any to help with IDE typing + @staticmethod + def get_repo_owner(repository: Repository | Any): + if _is_django_model(repository): + return repository.author + else: + return repository.owner diff --git a/libs/shared/shared/plan/__init__.py b/libs/shared/shared/plan/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/plan/constants.py b/libs/shared/shared/plan/constants.py new file mode 100644 index 0000000000..c533641f38 --- /dev/null +++ b/libs/shared/shared/plan/constants.py @@ -0,0 +1,85 @@ +import enum + +from shared.django_apps.utils.config import RUN_ENV + + +class MonthlyUploadLimits(enum.Enum): + CODECOV_FREE_PLAN = 250 + CODECOV_TEAM_PLAN = 2500 + + +class TrialDaysAmount(enum.Enum): + CODECOV_SENTRY = 14 + + +class PlanMarketingName(enum.Enum): + CODECOV_PRO = "Pro" + SENTRY_PRO = "Sentry Pro" + ENTERPRISE_CLOUD = "Enterprise Cloud" + GITHUB_MARKETPLACE = "Github Marketplace" + FREE = "Developer" + BASIC = "Developer" + TRIAL = "Developer" + TEAM = "Team" + + +DEFAULT_FREE_PLAN = "users-pr-inappy" if RUN_ENV == "ENTERPRISE" else "users-developer" + + +class PlanName(enum.Enum): + # If you add or remove, make a migration for Account table + BASIC_PLAN_NAME = "users-basic" + TRIAL_PLAN_NAME = "users-trial" + CODECOV_PRO_MONTHLY = "users-pr-inappm" + CODECOV_PRO_YEARLY = "users-pr-inappy" + SENTRY_MONTHLY = "users-sentrym" + SENTRY_YEARLY = "users-sentryy" + TEAM_MONTHLY = "users-teamm" + TEAM_YEARLY = "users-teamy" + GHM_PLAN_NAME = "users" + FREE_PLAN_NAME = "users-free" + CODECOV_PRO_MONTHLY_LEGACY = "users-inappm" + CODECOV_PRO_YEARLY_LEGACY = "users-inappy" + ENTERPRISE_CLOUD_MONTHLY = "users-enterprisem" + ENTERPRISE_CLOUD_YEARLY = "users-enterprisey" + USERS_DEVELOPER = "users-developer" + + @classmethod + def choices(cls): + return [(key.value, key.name) for key in cls] + + +class PlanBillingRate(enum.Enum): + MONTHLY = "monthly" + YEARLY = "annually" + + +class PlanPrice(enum.Enum): + MONTHLY = 12 + YEARLY = 10 + CODECOV_FREE = 0 + CODECOV_BASIC = 0 + CODECOV_TRIAL = 0 + TEAM_MONTHLY = 5 + TEAM_YEARLY = 4 + GHM_PRICE = 12 + + +class TrialStatus(enum.Enum): + NOT_STARTED = "not_started" + ONGOING = "ongoing" + EXPIRED = "expired" + CANNOT_TRIAL = "cannot_trial" + + +class TierName(enum.Enum): + BASIC = "basic" + TEAM = "team" + PRO = "pro" + ENTERPRISE = "enterprise" + SENTRY = "sentry" + TRIAL = "trial" + + +TRIAL_PLAN_SEATS = 1000 +TEAM_PLAN_MAX_USERS = 10 diff --git a/libs/shared/shared/plan/service.py b/libs/shared/shared/plan/service.py new file mode 100644 index 0000000000..61ad085021 --- /dev/null +++ b/libs/shared/shared/plan/service.py @@ -0,0 +1,359 @@ +import logging +from datetime import datetime, timedelta +from functools import cached_property +from typing import List, Optional + +from django.conf import settings + +from shared.config import get_config +from shared.django_apps.codecov.commands.exceptions import ValidationError +from shared.django_apps.codecov_auth.models import Owner, Plan, Service +from shared.license import get_current_license +from shared.plan.constants import ( + DEFAULT_FREE_PLAN, + TEAM_PLAN_MAX_USERS, + TRIAL_PLAN_SEATS, + PlanBillingRate, + PlanName, + TierName, + TrialDaysAmount, + TrialStatus, +) +from shared.self_hosted.service import enterprise_has_seats_left, license_seats + +log = logging.getLogger(__name__) + + +# This originally belongs to the sentry service in API but this is a temporary fn to avoid importing the whole service +def is_sentry_user(owner: Owner) -> bool: + """Returns true if the given owner has been linked with a Sentry user.""" + return owner.sentry_user_id is not None + + +# TODO: Consider moving some of these methods to the billing directory as they overlap billing functionality +class PlanService: + def __init__(self, current_org: Owner): + """ + Initializes a PlanService object for a specific organization. + + Args: + current_org (Owner): The organization for which the plan service is being initialized. + + Raises: + ValueError: If the organization's plan is unsupported. + """ + if ( + current_org.service == Service.GITLAB.value + and current_org.parent_service_id + ): + # for GitLab groups and subgroups, use the plan on the root org + self.current_org = current_org.root_organization + else: + self.current_org = current_org + + if not Plan.objects.filter(name=self.current_org.plan).exists(): + raise ValueError("Unsupported plan") + self._plan_data = None + + def update_plan(self, name: str, user_count: Optional[int]) -> None: + """Updates the organization's plan and user count.""" + if not Plan.objects.filter(name=name).exists(): + raise ValueError("Unsupported plan") + if not user_count: + raise ValueError("Quantity Needed") + self.current_org.plan = name + self.current_org.plan_user_count = user_count + self._plan_data = Plan.objects.select_related("tier").get(name=name) + self.current_org.delinquent = False + self.current_org.save() + + def current_org(self) -> Owner: + return self.current_org + + def set_default_plan_data(self) -> None: + """Sets the organization to the default developer plan.""" + log.info( + f"Setting plan to {DEFAULT_FREE_PLAN} for owner {self.current_org.ownerid}" + ) + self.current_org.plan = DEFAULT_FREE_PLAN + self.current_org.plan_activated_users = None + self.current_org.plan_user_count = 1 + self.current_org.stripe_subscription_id = None + self.current_org.save() + + @property + def has_account(self) -> bool: + """Returns whether the organization has an associated account.""" + return self.current_org.account is not None + + @cached_property + def plan_data(self) -> Plan: + """Returns the plan data for the organization, either from account or default.""" + if self._plan_data is None: + self._plan_data = Plan.objects.select_related("tier").get( + name=self.current_org.account.plan + if self.has_account + else self.current_org.plan + ) + return self._plan_data + + @property + def plan_name(self) -> str: + """Returns the name of the organization's current plan.""" + return self.plan_data.name + + @property + def plan_user_count(self) -> int: + """Returns the number of users allowed by the organization's plan.""" + if get_config("setup", "enterprise_license"): + return license_seats() + if self.has_account: + return self.current_org.account.total_seat_count + return self.current_org.plan_user_count + + @property + def plan_activated_users(self) -> Optional[List[int]]: + """Returns the list of activated users for the plan.""" + return self.current_org.plan_activated_users + + @property + def pretrial_users_count(self) -> int: + """Returns the number of pretrial users.""" + return self.current_org.pretrial_users_count or 1 + + @property + def marketing_name(self) -> str: + """Returns the marketing name of the plan.""" + return self.plan_data.marketing_name + + @property + def billing_rate(self) -> Optional[PlanBillingRate]: + """Returns the billing rate for the plan.""" + return self.plan_data.billing_rate + + @property + def base_unit_price(self) -> int: + """Returns the base unit price for the plan.""" + return self.plan_data.base_unit_price + + @property + def benefits(self) -> List[str]: + """Returns the benefits associated with the plan.""" + return self.plan_data.benefits + + @property + def monthly_uploads_limit(self) -> Optional[int]: + """ + Property that returns monthly uploads limit based on your trial status + + Returns: + Optional number of monthly uploads + """ + return self.plan_data.monthly_uploads_limit + + @property + def tier_name(self) -> TierName: + """Returns the tier name of the plan.""" + return self.plan_data.tier.tier_name + + def available_plans(self, owner: Owner) -> List[Plan]: + """Returns the available plans for the owner and organization.""" + available_plans = { + Plan.objects.select_related("tier").get(name=DEFAULT_FREE_PLAN) + } + curr_plan = self.plan_data + if not curr_plan.paid_plan: + available_plans.add(curr_plan) + + # Build list of available tiers based on conditions + available_tiers = [TierName.PRO.value] + + if is_sentry_user(owner) or curr_plan.is_sentry_plan: + available_tiers.append(TierName.SENTRY.value) + + if ( + not self.plan_activated_users + or len(self.plan_activated_users) <= TEAM_PLAN_MAX_USERS + ): + available_tiers.append(TierName.TEAM.value) + + available_plans.update( + Plan.objects.select_related("tier").filter( + tier__tier_name__in=available_tiers, is_active=True + ) + ) + + return list(available_plans) + + def _start_trial_helper( + self, + current_owner: Owner, + end_date: Optional[datetime] = None, + is_extension: bool = False, + ) -> None: + """Helper method to start or extend a trial for the organization.""" + start_date = datetime.now() + + if not is_extension: + self.current_org.trial_start_date = start_date + self.current_org.trial_status = TrialStatus.ONGOING.value + self.current_org.plan = PlanName.TRIAL_PLAN_NAME.value + self.current_org.pretrial_users_count = self.current_org.plan_user_count + self.current_org.plan_user_count = TRIAL_PLAN_SEATS + self.current_org.plan_auto_activate = True + + self.current_org.trial_end_date = ( + end_date + if end_date + else start_date + timedelta(days=TrialDaysAmount.CODECOV_SENTRY.value) + ) + self.current_org.trial_fired_by = current_owner.ownerid + self.current_org.save() + + # Trial Data + def start_trial(self, current_owner: Owner) -> None: + """ + Method that starts trial on an organization if the trial_start_date + is not empty. + + Returns: + No value + + Raises: + ValidationError: if trial has already started + """ + if self.trial_status != TrialStatus.NOT_STARTED.value: + raise ValidationError("Cannot start an existing trial") + if not Plan.objects.filter(name=self.plan_name, paid_plan=False).exists(): + raise ValidationError("Cannot trial from a paid plan") + + self._start_trial_helper(current_owner) + + def start_trial_manually(self, current_owner: Owner, end_date: datetime) -> None: + """ + Method that start trial immediately and ends at a predefined date for an organization + Used by administrators to manually start and extend trials + + Returns: + No value + """ + # Start a new trial plan for free users currently not on trial + + if self.plan_data.tier.tier_name == TierName.TRIAL.value: + self._start_trial_helper(current_owner, end_date, is_extension=True) + elif self.plan_data.paid_plan is False: + self._start_trial_helper(current_owner, end_date, is_extension=False) + # Extend an existing trial plan for users currently on trial + else: + raise ValidationError("Cannot trial from a paid plan") + + def cancel_trial(self) -> None: + """Cancels the ongoing trial for the organization.""" + if not self.is_org_trialing: + raise ValidationError("Cannot cancel a trial that is not ongoing") + now = datetime.now() + self.current_org.trial_status = TrialStatus.EXPIRED.value + self.current_org.trial_end_date = now + self.set_default_plan_data() + + def expire_trial_when_upgrading(self) -> None: + """ + Method that expires trial on an organization based on it's current trial status. + + + Returns: + No value + """ + if self.trial_status == TrialStatus.EXPIRED.value: + return + if self.trial_status != TrialStatus.CANNOT_TRIAL.value: + # Not adjusting the trial start/end dates here as some customers can + # directly purchase a plan without trialing first + self.current_org.trial_status = TrialStatus.EXPIRED.value + self.current_org.plan_activated_users = None + self.current_org.plan_user_count = ( + self.current_org.pretrial_users_count or 1 + ) + self.current_org.trial_end_date = datetime.now() + + self.current_org.save() + + @property + def trial_status(self) -> TrialStatus: + """Returns the trial status of the organization.""" + return self.current_org.trial_status + + @property + def trial_start_date(self) -> Optional[datetime]: + """Returns the trial start date.""" + return self.current_org.trial_start_date + + @property + def trial_end_date(self) -> Optional[datetime]: + """Returns the trial end date.""" + return self.current_org.trial_end_date + + @property + def trial_total_days(self) -> Optional[TrialDaysAmount]: + """Returns the total number of trial days.""" + return TrialDaysAmount.CODECOV_SENTRY.value + + @property + def is_org_trialing(self) -> bool: + return ( + self.trial_status == TrialStatus.ONGOING.value + and self.plan_name == PlanName.TRIAL_PLAN_NAME.value + ) + + @property + def has_trial_dates(self) -> bool: + return bool(self.trial_start_date and self.trial_end_date) + + @property + def has_seats_left(self) -> bool: + if get_config("setup", "enterprise_license"): + return enterprise_has_seats_left() + if self.has_account: + # edge case: IF the User is already a plan_activated_user on any of the Orgs in the Account, + # AND their Account is at capacity, + # AND they try to become a plan_activated_user on another Org in the Account, + # has_seats_left will evaluate as False even though the User should be allowed to activate on the Org. + return self.current_org.account.can_activate_user() + return ( + self.current_org.activated_user_count is None + or self.current_org.activated_user_count < self.plan_user_count + ) + + @property + def is_enterprise_plan(self) -> bool: + return self.plan_data.is_enterprise_plan + + @property + def is_free_plan(self) -> bool: + return self.plan_data.is_free_plan and not self.is_org_trialing + + @property + def is_pro_plan(self) -> bool: + return self.plan_data.is_pro_plan + + @property + def is_sentry_plan(self) -> bool: + return self.plan_data.is_sentry_plan + + @property + def is_team_plan(self) -> bool: + return self.plan_data.is_team_plan + + @property + def is_trial_plan(self) -> bool: + return self.plan_data.is_trial_plan + + @property + def is_pr_billing_plan(self) -> bool: + if not settings.IS_ENTERPRISE: + return self.plan_data.name not in [ + PlanName.CODECOV_PRO_MONTHLY_LEGACY.value, + PlanName.CODECOV_PRO_YEARLY_LEGACY.value, + ] + else: + return get_current_license().is_pr_billing diff --git a/libs/shared/shared/py.typed b/libs/shared/shared/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/rate_limits/__init__.py b/libs/shared/shared/rate_limits/__init__.py new file mode 100644 index 0000000000..60ca407c32 --- /dev/null +++ b/libs/shared/shared/rate_limits/__init__.py @@ -0,0 +1,115 @@ +import logging +from typing import Optional + +from redis import Redis, RedisError + +from shared.django_apps.codecov_auth.models import Owner +from shared.django_apps.core.models import Repository + +log = logging.getLogger(__name__) + +RATE_LIMIT_REDIS_KEY_PREFIX = "rate_limited_entity_" + + +def gh_app_key_name(installation_id: int, app_id: str | int | None = None) -> str: + if app_id is None: + return f"default_app_{installation_id}" + return f"{app_id}_{installation_id}" + + +def owner_key_name(owner_id: int) -> str: + return str(owner_id) + + +def default_bot_key_name() -> str: + return "github_bot" + + +def determine_entity_redis_key( + owner: Owner | None, repository: Repository | None +) -> Optional[str]: + """ + This function will determine the entity that uses a token to + communicate with third party services, currently Github. + + If no owner is provided, it returns a preset key for github bots. + Then, it gathers authentication information through the auth_info adapter method + and returns a GH app id + installation ids if an app exists, otherwise it will return + the owner who's token it uses. + + The entity can be any of the following: + _ for Github Apps + for Owners + for mapped Tokens if available + github_bot for Anonymous users + + It should only be used for github git instances + """ + from shared.bots import get_adapter_auth_information + from shared.bots.types import AdapterAuthInformation + + if not owner: + return default_bot_key_name() + + if repository: + auth_info: AdapterAuthInformation = get_adapter_auth_information( + owner=owner, repository=repository + ) + else: + auth_info: AdapterAuthInformation = get_adapter_auth_information(owner=owner) + + if ( + auth_info + and auth_info.get("token") + and auth_info.get("token").get("entity_name") + ): + return auth_info.get("token").get("entity_name") + + +def determine_if_entity_is_rate_limited(redis_connection: Redis, key_name: str) -> bool: + """ + This function will determine if a customer is rate limited. It will + return true if the record exists, false otherwise. + This will be used by API and Worker and should only be used for github git instances. + """ + try: + return redis_connection.exists(f"{RATE_LIMIT_REDIS_KEY_PREFIX}{key_name}") + except RedisError: + log.exception( + "Failed to check if the key name is rate_limited due to RedisError", + extra=dict(key_name=key_name), + ) + return False + + +def set_entity_to_rate_limited( + redis_connection: Redis, key_name: str, ttl_seconds: int +): + """ + Marks an entity as rate-limited in Redis. This will be mainly used + in worker during communication with Github 3rd party services + + @param `key_name` - name of the entity being rate limited. This is found in determine_entity_redis_key + and in the Token object definition + key_name can take the shapes of: + _ for Github Apps + for Owners + for Anonymous users + + @param `ttl_seconds` - Should come from GitHub (in the request that was rate limited) + """ + if ttl_seconds <= 0: + # ttl_seconds is the time until the RateLimit ends + # Makes no sense to mark an installation rate limited if it's not anymore + return + try: + redis_connection.set( + name=f"rate_limited_entity_{key_name}", + value=1, + ex=ttl_seconds, + ) + except RedisError: + log.exception( + "Failed to mark entity as rate_limited due to RedisError", + extra=dict(entity_id=key_name), + ) diff --git a/libs/shared/shared/reports/__init__.py b/libs/shared/shared/reports/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/reports/api_report_service.py b/libs/shared/shared/reports/api_report_service.py new file mode 100644 index 0000000000..17da3776ff --- /dev/null +++ b/libs/shared/shared/reports/api_report_service.py @@ -0,0 +1,69 @@ +import logging + +import sentry_sdk +from django.utils.functional import cached_property + +from shared.api_archive.archive import ArchiveService +from shared.django_apps.core.models import Commit +from shared.helpers.flag import Flag +from shared.reports.readonly import ReadOnlyReport as SharedReadOnlyReport +from shared.reports.resources import Report +from shared.storage.exceptions import FileNotInStorageError + +log = logging.getLogger(__name__) + + +class ReportMixin: + @cached_property + def flags(self): + """returns dict(:name=)""" + flags_dict = {} + for session in self.sessions.values(): + if session.flags is not None: + carriedforward = session.session_type.value == "carriedforward" + carriedforward_from = session.session_extras.get("carriedforward_from") + for flag in session.flags: + flags_dict[flag] = Flag( + self, + flag, + carriedforward=carriedforward, + carriedforward_from=carriedforward_from, + ) + return flags_dict + + +class SerializableReport(ReportMixin, Report): + pass + + +class ReadOnlyReport(ReportMixin, SharedReadOnlyReport): + pass + + +@sentry_sdk.trace +def build_report_from_commit(commit: Commit, report_class=None): + """ + Builds a `shared.reports.resources.Report` from a given commit. + """ + + if not commit.report: + return None + + files = commit.report["files"] + sessions = commit.report["sessions"] + totals = commit.totals + + try: + chunks = ArchiveService(commit.repository).read_chunks(commit.commitid) + except FileNotInStorageError: + log.warning( + "File for chunks not found in storage", + extra=dict(commit=commit.commitid, repo=commit.repository_id), + ) + return None + + if report_class is None: + report_class = SerializableReport + return report_class.from_chunks( + chunks=chunks, files=files, sessions=sessions, totals=totals + ) diff --git a/libs/shared/shared/reports/carryforward.py b/libs/shared/shared/reports/carryforward.py new file mode 100644 index 0000000000..5b73f5bf01 --- /dev/null +++ b/libs/shared/shared/reports/carryforward.py @@ -0,0 +1,72 @@ +import logging +import re +from typing import Mapping, Sequence + +from shared.reports.resources import Report +from shared.utils.match import Matcher +from shared.utils.sessions import SessionType + +log = logging.getLogger(__name__) + + +def carriedforward_session_name(original_session_name: str) -> str: + if not original_session_name: + return "Carriedforward" + elif original_session_name.startswith("CF "): + count = 0 + current_name = original_session_name + while current_name.startswith("CF "): + current_name = current_name.replace("CF ", "", 1) + count += 1 + return f"CF[{count + 1}] - {current_name}" + elif original_session_name.startswith("CF"): + regex = r"CF\[(\d*)\]" + res = re.match(regex, original_session_name) + if res: + number_so_far = int(res.group(1)) + return re.sub( + regex, f"CF[{number_so_far + 1}]", original_session_name, count=1 + ) + return f"CF[1] - {original_session_name}" + + +def generate_carryforward_report( + report: Report, + flags: Sequence[str], + paths: Sequence[str], + session_extras: Mapping[str, str] | None = None, +) -> Report: + """ + Generates a carriedforward report by filtering the given `report` in-place, + to only those files and sessions matching the given `flags` and `paths`. + + The sessions that are matching the `flags` are being flagged as `carriedforward`, + and other sessions are removed from the report.""" + if paths: + matcher = Matcher(paths) + files_to_delete = { + filename for filename in report._files.keys() if not matcher.match(filename) + } + for filename in files_to_delete: + del report[filename] + + sessions_to_delete = [] + for sid, session in report.sessions.items(): + if not contain_any_of_the_flags(flags, session.flags): + sessions_to_delete.append(int(sid)) + else: + session.session_extras = session_extras or session.session_extras + session.name = carriedforward_session_name(session.name) + session.session_type = SessionType.carriedforward + log.info( + "Removing sessions that are not supposed to carryforward", + extra=dict(deleted_sessions=sessions_to_delete), + ) + report.delete_multiple_sessions(sessions_to_delete) + return report + + +def contain_any_of_the_flags(expected_flags, actual_flags): + if expected_flags is None or actual_flags is None: + return False + return len(set(expected_flags) & set(actual_flags)) > 0 diff --git a/libs/shared/shared/reports/changes.py b/libs/shared/shared/reports/changes.py new file mode 100644 index 0000000000..5aac775332 --- /dev/null +++ b/libs/shared/shared/reports/changes.py @@ -0,0 +1,9 @@ +import cc_rustyribs + + +def run_comparison_using_rust(base_report, head_report, diff): + return cc_rustyribs.run_comparison( + base_report.rust_report.get_report(), + head_report.rust_report.get_report(), + cc_rustyribs.rustify_diff(diff), + ) diff --git a/libs/shared/shared/reports/diff.py b/libs/shared/shared/reports/diff.py new file mode 100644 index 0000000000..526d602a6d --- /dev/null +++ b/libs/shared/shared/reports/diff.py @@ -0,0 +1,106 @@ +import dataclasses +from typing import Generator, Literal, Protocol, TypedDict + +from shared.reports.totals import get_line_totals +from shared.reports.types import ReportLine, ReportTotals +from shared.utils.totals import sum_totals + + +class DiffSegment(TypedDict): + lines: list[str] + """The lines within a diff segment, prefixed with "+" or "-" for added/removed lines, or " " for context.""" + header: tuple[int, int, int, int] + """The segment header, which is `old_line`, `old_length`, `new_line`, `new_length`.""" + + +class DiffFile(TypedDict): + type: Literal["new", "modified", "deleted"] + "Whether the file was added, removed or modified in this diff." + segments: list[DiffSegment] + """A list of diff segments, or "hunk"s as they are also called.""" + + +class RawDiff(TypedDict): + files: dict[str, DiffFile] + "This is a dictionary from path to `DiffFile`." + + +class CalculatedDiff(TypedDict): + general: ReportTotals + "The totals across this diff" + files: dict[str, ReportTotals] + "Per-file totals, keyed by path." + + +# TODO: it might make sense to move these abstract interfaces to a different place +class AbstractReportFile(Protocol): + def get(self, line_no: int) -> ReportLine | None: + "Get the line specified by `line_no` (1-indexed), or `None` if the line does not exist." + ... + + def calculate_diff(self, segments: list[DiffSegment]) -> ReportTotals: + "Calculates the totals for the given diff `segments`." + ... + + +class AbstractReport(Protocol): + def get(self, path: str) -> AbstractReportFile | None: + "Get the file using its `path` within the Report, or `None` if no such file exists." + ... + + +def relevant_lines(segment: DiffSegment) -> Generator[int, None, None]: + "Iterates over the relevant line numbers in a diff segment." + return ( + i + for i, line in enumerate( + (ln for ln in segment["lines"] if ln[0] != "-"), + start=int(segment["header"][2]) or 1, + ) + if line[0] == "+" + ) + + +def calculate_file_diff( + file: AbstractReportFile, segments: list[DiffSegment] +) -> ReportTotals: + """ + Calculates the `ReportTotals` across all relevant lines in the diff `segments`. + + Takes a line accessor `get_line` returning an optional `ReportLine` + for a given line number. + """ + + line_numbers = (i for segment in segments for i in relevant_lines(segment)) + lines = (file.get(ln) for ln in line_numbers) + return get_line_totals(line for line in lines if line) + + +def calculate_report_diff(report: AbstractReport, diff: RawDiff) -> CalculatedDiff: + """ + Calculates the `ReportTotals` across a complete Report, as well as per-file, + for all files present in the `diff`. + + Takes a callback function that calculates the per-file totals given a file path. + """ + + files: dict[str, ReportTotals] = {} + # TODO: update `totals` in-place + list_of_file_totals = [] + + for path, data in diff["files"].items(): + if data["type"] in ("modified", "new"): + file = report.get(path) + if file: + file_totals = calculate_file_diff(file, data["segments"]) + files[path] = file_totals + list_of_file_totals.append(file_totals) + + totals = sum_totals(list_of_file_totals) + + if totals.lines == 0: + totals = dataclasses.replace( + totals, coverage=None, complexity=None, complexity_total=None + ) + + return CalculatedDiff(general=totals, files=files) diff --git a/libs/shared/shared/reports/editable.py b/libs/shared/shared/reports/editable.py new file mode 100644 index 0000000000..358c1497df --- /dev/null +++ b/libs/shared/shared/reports/editable.py @@ -0,0 +1,5 @@ +from shared.reports.resources import Report, ReportFile + +# re-export to avoid having to patch the whole world: +EditableReportFile = ReportFile +EditableReport = Report diff --git a/libs/shared/shared/reports/enums.py b/libs/shared/shared/reports/enums.py new file mode 100644 index 0000000000..7dbcb8a6c5 --- /dev/null +++ b/libs/shared/shared/reports/enums.py @@ -0,0 +1,26 @@ +from shared.utils.enums import CodecovDatabaseEnum + + +class UploadState(CodecovDatabaseEnum): + UPLOADED = (1,) + PROCESSED = (2,) + ERROR = (3,) + FULLY_OVERWRITTEN = (4,) + PARTIALLY_OVERWRITTEN = (5,) + + # not used right now, but will be when parallel upload procesing is rolled out. The + # purpose of this is to signify a `UploadProcessor` task has ran for an upload, but + # has not quite fully merged into the final report in `UploadFinisher` task + # PARALLEL_PROCESSED = (6,) + + def __init__(self, db_id): + self.db_id = db_id + + +class UploadType(CodecovDatabaseEnum): + UPLOADED = (1, "uploaded") + CARRIEDFORWARD = (2, "carriedforward") + + def __init__(self, db_id, db_name): + self.db_id = db_id + self.db_name = db_name diff --git a/libs/shared/shared/reports/exceptions.py b/libs/shared/shared/reports/exceptions.py new file mode 100644 index 0000000000..8773fd829a --- /dev/null +++ b/libs/shared/shared/reports/exceptions.py @@ -0,0 +1,6 @@ +class LabelIndexNotFoundError(Exception): + pass + + +class LabelNotFoundError(Exception): + pass diff --git a/libs/shared/shared/reports/filtered.py b/libs/shared/shared/reports/filtered.py new file mode 100644 index 0000000000..eed5dcd248 --- /dev/null +++ b/libs/shared/shared/reports/filtered.py @@ -0,0 +1,252 @@ +import dataclasses +import logging + +import sentry_sdk + +from shared.config import get_config +from shared.reports.diff import ( + CalculatedDiff, + DiffSegment, + RawDiff, + calculate_file_diff, + calculate_report_diff, +) +from shared.reports.totals import get_line_totals +from shared.reports.types import EMPTY, ReportTotals +from shared.utils.make_network_file import make_network_file +from shared.utils.match import Matcher +from shared.utils.merge import get_complexity_from_sessions, merge_all +from shared.utils.totals import agg_totals + +log = logging.getLogger(__name__) + + +def _contain_any_of_the_flags(expected_flags, actual_flags): + if expected_flags is None or actual_flags is None: + return False + return len(set(expected_flags) & set(actual_flags)) > 0 + + +class FilteredReportFile(object): + __slots__ = ["report_file", "session_ids", "_totals", "_cached_lines"] + + def __init__(self, report_file, session_ids): + self.report_file = report_file + self.session_ids = session_ids + self._totals = None + self._cached_lines = None + + def line_modifier(self, line): + new_sessions = [s for s in line.sessions if s.id in self.session_ids] + if len(new_sessions) == 0: + return EMPTY + new_datapoints = ( + [dp for dp in line.datapoints if dp.sessionid in self.session_ids] + if line.datapoints is not None + else None + ) + remaining_coverages = [s.coverage for s in new_sessions] + new_coverage = merge_all(remaining_coverages) + return dataclasses.replace( + line, + complexity=get_complexity_from_sessions(new_sessions), + sessions=new_sessions, + coverage=new_coverage, + datapoints=new_datapoints, + ) + + @property + def name(self): + return self.report_file.name + + @property + def totals(self): + if not self._totals: + self._totals = self._process_totals() + return self._totals + + @property + def eof(self): + return self.report_file.eof + + @property + def lines(self): + """Iter through lines with coverage + returning (ln, line) + + """ + if self._cached_lines: + return self._cached_lines + ret = [] + for ln, line in self.report_file.lines: + line = self.line_modifier(line) # noqa: PLW2901 + if line: + ret.append((ln, line)) + self._cached_lines = ret + return ret + + def calculate_diff(self, segments: list[DiffSegment]) -> ReportTotals: + return calculate_file_diff(self, segments) + + def get(self, ln): + line = self.report_file.get(ln) + if line: + line = self.line_modifier(line) + if not line: + return None + return line + + def _process_totals(self): + """return dict of totals""" + return get_line_totals(line for _ln, line in self.lines) + + +class FilteredReport(object): + def __init__(self, report, path_patterns, flags): + self.report = report + self.path_patterns = path_patterns + self._matcher = Matcher(path_patterns) + self.flags = flags + self._totals = None + self._sessions_to_include = None + self.report_file_cache = {} + + def has_precalculated_totals(self): + return self._totals is not None + + def _calculate_sessionids_to_include(self): + if not self.flags: + return set(self.report.sessions.keys()) + flags_matcher = Matcher(self.flags) + old_style_sessions = set( + sid + for (sid, session) in self.report.sessions.items() + if flags_matcher.match_any(session.flags) + ) + new_style_sessions = set( + sid + for (sid, session) in self.report.sessions.items() + if _contain_any_of_the_flags(self.flags, session.flags) + ) + if old_style_sessions != new_style_sessions: + log.info( + "New result would differ from old result", + extra=dict( + old_result=sorted(old_style_sessions), + new_result=sorted(new_style_sessions), + filter_flags=sorted(self.flags), + report_flags=sorted(self.report.flags.keys()), + ), + ) + if get_config("compatibility", "flag_pattern_matching", default=False): + sentry_sdk.capture_message( + "Mismatch in flag_pattern_matching", + extras={ + "filter_flags": sorted(self.flags), + "report_flags": sorted(self.report.flags.keys()), + }, + ) + return old_style_sessions + return new_style_sessions + + @property + def session_ids_to_include(self): + if self._sessions_to_include is None: + self._sessions_to_include = self._calculate_sessionids_to_include() + return self._sessions_to_include + + def should_include(self, filename): + return self._matcher.match(filename) + + @property + def network(self): + for fname in self.report._files.keys(): + file = self.get(fname) + if file: + yield fname, make_network_file(file.totals) + + def get(self, filename): + if not self.should_include(filename): + return None + if not self.flags: + return self.report.get(filename) + r = self.report.get(filename) + if r is None: + return None + + if filename not in self.report_file_cache: + self.report_file_cache[filename] = FilteredReportFile( + r, self.session_ids_to_include + ) + return self.report_file_cache[filename] + + @property + def files(self): + return [f for f in self.report.files if self.should_include(f)] + + def get_file_totals(self, path): + if self.should_include(path): + return self.report.get_file_totals(path) + + return None + + @property + def totals(self): + if not self._totals: + self._totals = self._process_totals() + return self._totals + + def is_empty(self): + return not any(self.should_include(x) for x in self.report._files.keys()) + + def _iter_totals(self): + for filename in self.report._files.keys(): + if self.should_include(filename): + res = self.get(filename).totals + if res and res.lines > 0: + yield res + + def _process_totals(self): + """Runs through the file network to aggregate totals + returns + """ + totals = agg_totals(self._iter_totals()) + totals.sessions = len(self.session_ids_to_include) + return ReportTotals(*tuple(totals)) + + def calculate_diff(self, diff: RawDiff) -> CalculatedDiff: + """ + Calculates the per-file totals (and total) of the parts + from a `git diff` that are relevant in the report + """ + return calculate_report_diff(self, diff) + + def apply_diff(self, diff, _save=True): + """ + Add coverage details to the diff at ['coverage'] = + returns + """ + if not diff or not diff.get("files"): + return None + totals = self.calculate_diff(diff) + if _save and totals: + self.save_diff_calculation(diff, totals) + return totals.get("general") + + def save_diff_calculation(self, diff, diff_result): + diff["totals"] = diff_result["general"] + self.diff_totals = diff["totals"] + for filename, file_totals in diff_result["files"].items(): + data = diff["files"].get(filename) + data["totals"] = file_totals + + def __iter__(self): + """Iter through all the files + yielding + """ + for file in self.report: + if self.should_include(file.name): + if not self.flags: + yield file + else: + yield FilteredReportFile(file, self.session_ids_to_include) diff --git a/libs/shared/shared/reports/readonly.py b/libs/shared/shared/reports/readonly.py new file mode 100644 index 0000000000..6cea774fbe --- /dev/null +++ b/libs/shared/shared/reports/readonly.py @@ -0,0 +1,164 @@ +import logging +from typing import Any + +import orjson +import sentry_sdk +from cc_rustyribs import FilterAnalyzer, SimpleAnalyzer, parse_report + +from shared.helpers.flag import Flag +from shared.reports.resources import END_OF_HEADER, Report, ReportTotals +from shared.utils.match import Matcher + +log = logging.getLogger(__name__) + + +class LazyRustReport(object): + def __init__(self, filename_mapping, chunks, session_mapping): + # Because Rust can't parse the header. It doesn't need it either, + # So it's simpler to just never sent it. + splits = chunks.split(END_OF_HEADER, maxsplit=1) + if len(splits) > 1: + chunks = splits[1] + self._chunks = chunks + self._filename_mapping = filename_mapping + self._session_mapping = session_mapping + self._actual_report = None + + @sentry_sdk.trace + def _parse_report(self): + parsed = parse_report( + self._filename_mapping, self._chunks, self._session_mapping + ) + self._chunks = None # Free the memory + return parsed + + def get_report(self): + if self._actual_report is None: + self._actual_report = self._parse_report() + return self._actual_report + + +class ReadOnlyReport(object): + def __init__(self, rust_analyzer, rust_report, inner_report, totals=None): + self.rust_analyzer = rust_analyzer + self.rust_report = rust_report + self.inner_report = inner_report + self._totals = totals + self._flags = None + self._uploaded_flags = None + + @classmethod + def from_chunks(cls, files=None, sessions=None, totals=None, chunks=None): + rust_analyzer = SimpleAnalyzer() + inner_report = Report( + files=files, sessions=sessions, totals=totals, chunks=chunks + ) + totals = inner_report._totals + filename_mapping = { + filename: idx for idx, filename in enumerate(inner_report._files.keys()) + } + session_mapping = { + sid: (session.flags or []) for sid, session in inner_report.sessions.items() + } + rust_report = LazyRustReport(filename_mapping, chunks, session_mapping) + return cls(rust_analyzer, rust_report, inner_report, totals=totals) + + @classmethod + def create_from_report(cls, report: Report): + report_json: Any + report_json, chunks, totals = report.serialize() + report_json = orjson.loads(report_json) + + return cls.from_chunks( + chunks=chunks.decode(), + sessions=report.sessions, + files=report_json["files"], + totals=totals, + ) + + def __iter__(self): + return iter(self.inner_report) + + @property + def files(self): + return self.inner_report.files + + @property + def flags(self): + if self._flags is None: + self._flags = {} + for flag_name, flag in self.inner_report.flags.items(): + self._flags[flag_name] = Flag( + self, + flag_name, + carriedforward=flag.carriedforward, + carriedforward_from=flag.carriedforward_from, + ) + return self._flags + + def get_flag_names(self) -> list[str]: + return self.inner_report.get_flag_names() + + @property + def sessions(self): + return self.inner_report.sessions + + def apply_diff(self, *args, **kwargs): + return self.inner_report.apply_diff(*args, **kwargs) + + def calculate_diff(self, *args, **kwargs): + return self.inner_report.calculate_diff(*args, **kwargs) + + def get(self, *args, **kwargs): + return self.inner_report.get(*args, **kwargs) + + @sentry_sdk.trace + def _process_totals(self): + if self.inner_report.has_precalculated_totals(): + return self.inner_report.totals + res = self.rust_analyzer.get_totals(self.rust_report.get_report()) + return ReportTotals( + files=res.files, + lines=res.lines, + hits=res.hits, + misses=res.misses, + partials=res.partials, + coverage=res.coverage, + branches=res.branches, + methods=res.methods, + messages=0, + sessions=res.sessions, + complexity=res.complexity, + complexity_total=res.complexity_total, + diff=0, + ) + + @property + def totals(self): + if self._totals is None: + self._totals = self._process_totals() + return self._totals + + def get_file_totals(self, path): + return self.inner_report.get_file_totals(path) + + def filter(self, paths=None, flags=None): + if paths is None and flags is None: + return self + matcher = Matcher(paths) + matching_files = ( + set(f for f in self.files if matcher.match(f)) if paths else None + ) + rust_analyzer = FilterAnalyzer( + files=matching_files, flags=flags if flags else None + ) + return ReadOnlyReport( + rust_analyzer, + rust_report=self.rust_report, + inner_report=self.inner_report.filter(paths=paths, flags=flags), + ) + + def get_uploaded_flags(self): + if self._uploaded_flags is None: + self._uploaded_flags = self.inner_report.get_uploaded_flags() + return self._uploaded_flags diff --git a/libs/shared/shared/reports/reportfile.py b/libs/shared/shared/reports/reportfile.py new file mode 100644 index 0000000000..7c353d01c4 --- /dev/null +++ b/libs/shared/shared/reports/reportfile.py @@ -0,0 +1,530 @@ +import dataclasses +import logging +from itertools import zip_longest +from typing import Any, cast + +import orjson + +from shared.reports.diff import DiffSegment, calculate_file_diff +from shared.reports.totals import get_line_totals +from shared.reports.types import EMPTY, ReportLine, ReportTotals +from shared.utils.merge import merge_all, merge_line + +log = logging.getLogger(__name__) + + +class ReportFile: + name: str + _totals: ReportTotals | None + diff_totals: ReportTotals | None + _raw_lines: str | None + _parsed_lines: list[None | str | ReportLine] + _details: dict[str, Any] + __present_sessions: set[int] | None + + def __init__( + self, + name: str, + totals: ReportTotals | list | None = None, + lines: list[None | str | ReportLine] | str | None = None, + diff_totals: ReportTotals | list | None = None, + ignore=None, + ): + """ + name = string, filename. "folder/name.py" + totals = [0,1,0,...] (map out to one ReportTotals) + lines = [] or string + if [] then [null, line@1, null, line@3, line@4] + if str then "\nline@1\n\nline@3" + a line is [] that maps to ReportLine:obj + ignore is for report buildling only, it filters out lines that should be not covered + {eof:N, lines:[1,10]} + """ + self.name = name + self._totals = None + self.diff_totals = None + self._raw_lines = None + self._parsed_lines = [] + self._details = {} + self.__present_sessions = None + + if lines: + if isinstance(lines, list): + self._parsed_lines = lines + else: + self._raw_lines = lines + + self._ignore = _ignore_to_func(ignore) if ignore else None + + # The `_totals` and `__present_sessions` fields are cached values for the + # `totals` and `_present_sessions` properties respectively. + # The values are loaded at initialization time, or calculated from line data on-demand. + # All mutating methods (like `append`, `merge`, etc) will either re-calculate these values + # directly, or clear them so the `@property` accessors re-calculate them when needed. + + if isinstance(totals, ReportTotals): + self._totals = totals + elif totals: + self._totals = ReportTotals(*totals) + + if isinstance(diff_totals, ReportTotals): + self.diff_totals = diff_totals + elif diff_totals: + self.diff_totals = ReportTotals(*diff_totals) + + def _invalidate_caches(self): + self._totals = None + self.diff_totals = None + self.__present_sessions = None + + @property + def _lines(self): + if self._raw_lines: + self._parsed_lines = self._raw_lines.splitlines() + detailsline = self._parsed_lines.pop(0) + + self._details = orjson.loads(detailsline or "null") or {} + if present_sessions := self._details.get("present_sessions"): + self.__present_sessions = set(present_sessions) + + self._raw_lines = None + + return self._parsed_lines + + @property + def _present_sessions(self): + _ensure_is_parsed = self._lines + if self.__present_sessions is None: + self.__present_sessions = set() + for _, line in self.lines: + self.__present_sessions.update(int(s.id) for s in line.sessions) + return self.__present_sessions + + @property + def details(self): + _ensure_is_parsed = self._lines + self._details["present_sessions"] = sorted(self._present_sessions) + return self._details + + @property + def totals(self): + if not self._totals: + self._totals = get_line_totals(line for _ln, line in self.lines) + return self._totals + + def __repr__(self): + try: + return "<%s name=%s lines=%s>" % ( + self.__class__.__name__, + self.name, + len(self), + ) + except Exception: + return "<%s name=%s lines=n/a>" % (self.__class__.__name__, self.name) + + def _line(self, line: ReportLine | list | str): + if isinstance(line, ReportLine): + # line is already mapped to obj + return line + if isinstance(line, str): + line = cast(list, orjson.loads(line)) + return ReportLine.create(*line) + + @property + def lines(self): + """Iter through lines with coverage + returning (ln, line) + + """ + for ln, line in enumerate(self._lines, start=1): + if line: + yield ln, self._line(line) + + def calculate_diff(self, segments: list[DiffSegment]) -> ReportTotals: + return calculate_file_diff(self, segments) + + def __iter__(self): + """Iter through lines + returning (line or None) + + """ + for line in self._lines: + if line: + yield self._line(line) + else: + yield None + + def __getitem__(self, ln): + """Return a single line or None""" + if ln == "totals": + return self.totals + if isinstance(ln, slice): + return self._getslice(ln.start, ln.stop) + if not isinstance(ln, int): + raise TypeError("expecting type int got %s" % type(ln)) + elif ln < 1: + raise ValueError("Line number must be greater then 0. Got %s" % ln) + _line = self.get(ln) + if not _line: + raise IndexError("Line #%s not found in report" % ln) + return _line + + def __setitem__(self, ln, line): + """Append line to file, without merging if previously set""" + if not isinstance(ln, int): + raise TypeError("expecting type int got %s" % type(ln)) + elif not isinstance(line, ReportLine): + raise TypeError("expecting type ReportLine got %s" % type(line)) + elif ln < 1: + raise ValueError("Line number must be greater then 0. Got %s" % ln) + elif self._ignore and self._ignore(ln): + return + + length = len(self._lines) + if length <= ln: + self._lines.extend([EMPTY] * (ln - length)) + + self._lines[ln - 1] = line + self._invalidate_caches() + return + + def __delitem__(self, ln: int): + """Delete line from file""" + if not isinstance(ln, int): + raise TypeError("expecting type int got %s" % type(ln)) + elif ln < 1: + raise ValueError("Line number must be greater then 0. Got %s" % ln) + + length = len(self._lines) + if length <= ln: + self._lines.extend([EMPTY] * (ln - length)) + + self._lines[ln - 1] = EMPTY + self._invalidate_caches() + return + + def __len__(self): + """Returns count(number of lines with coverage data)""" + return sum(1 for _f in self._lines if _f) + + @property + def eof(self): + """Returns count(number of lines)""" + return len(self._lines) + 1 + + def _getslice(self, start, stop): + """Returns a stream of lines between two indexes + + slice = report[5:25] + + + for ln, line in report[5:25]: + ... + + slice = report[5:25] + assert slice is gernerator. + list(slice) == [(1, Line), (2, Line)] + + NOTE: not be confused with the builtin function __getslice__ that was deprecated in python 3.x + """ + for ln, line in enumerate(self._lines[start - 1 : stop - 1], start=start): + if line: + yield ln, self._line(line) + + def __contains__(self, ln): + if not isinstance(ln, int): + raise TypeError("expecting type int got %s" % type(ln)) + try: + return self.get(ln) is not None + except IndexError: + return False + + def __bool__(self): + return self.totals.lines > 0 + + def get(self, ln): + if not isinstance(ln, int): + raise TypeError("expecting type int got %s" % type(ln)) + elif ln < 1: + raise ValueError("Line number must be greater then 0. Got %s" % ln) + + try: + line = self._lines[ln - 1] + + except IndexError: + return None + + else: + if line: + return self._line(line) + + def append(self, ln, line): + """Append a line to the report + if the line exists it will merge it + """ + if not isinstance(ln, int): + raise TypeError("expecting type int got %s" % type(ln)) + elif not isinstance(line, ReportLine): + raise TypeError("expecting type ReportLine got %s" % type(line)) + elif ln < 1: + raise ValueError("Line number must be greater then 0. Got %s" % ln) + elif self._ignore and self._ignore(ln): + return False + + length = len(self._lines) + if length <= ln: + self._lines.extend([EMPTY] * (ln - length)) + _line = self.get(ln) + if _line: + self._lines[ln - 1] = merge_line(_line, line) + else: + self._lines[ln - 1] = line + + self._invalidate_caches() + return True + + def merge(self, other_file, joined=True): + """merges another report chunk + returning the + It's quicker to run the totals during processing + """ + if other_file is None: + return + + elif not isinstance(other_file, ReportFile): + raise TypeError("expecting type ReportFile got %s" % type(other_file)) + + if ( + self.name.endswith(".rb") + and self.totals.lines == self.totals.misses + and ( + other_file.totals.lines != self.totals.lines + or other_file.totals.misses != self.totals.misses + ) + ): + # previous file was boil-the-ocean + # OR previous file had END issue + self._parsed_lines = other_file._lines.copy() + self._raw_lines = None + log.warning( + "Doing something weird because of weird .rb logic", + extra=dict(report_filename=self.name), + ) + + elif ( + self.name.endswith(".rb") + and other_file.totals.lines == other_file.totals.misses + and ( + other_file.totals.lines != self.totals.lines + or other_file.totals.misses != self.totals.misses + ) + ): + # skip boil-the-ocean files + # OR skip 0% coverage files because END issue + return False + + else: + # set new lines object + self._parsed_lines = [ + merge_line(before, after, joined) + for before, after in zip_longest(self, other_file) + ] + self._raw_lines = None + + self._invalidate_caches() + return True + + def does_diff_adjust_tracked_lines(self, diff, future_file): + for segment in diff["segments"]: + # loop through each line + pos = int(segment["header"][2]) or 1 + for line in segment["lines"]: + if line[0] == "-": + if pos in self: + # tracked line removed + return True + + elif line[0] == "+": + if pos in future_file: + # tracked line added + return True + pos += 1 + else: + pos += 1 + return False + + def shift_lines_by_diff(self, diff, forward=True) -> None: + """ + Adjusts report _lines IN PLACE to account for the diff given. + !!! This WILL CHANGE the report permanently. + + Given coverage info for commit A (report._lines), and a diff from A to B (diff), + adjust coverage info so that it works AS IF it was uploaded for commit B. + """ + try: + removed = "-" + added = "+" + # loop through each segment in the diff. + for segment in diff["segments"]: + # Header is [pos_in_base, lines_len_base, pos_in_head, lines_len_head] + pos = (int(segment["header"][2]) or 1) - 1 + # loop through each line in segment + for line in segment["lines"]: + if line[0] == removed: + if len(self._lines) > pos: + self._lines.pop(pos) + elif line[0] == added: + self._lines.insert(pos, "") + pos += 1 + else: + pos += 1 + except (ValueError, KeyError, TypeError, IndexError): + log.exception("Failed to shift lines by diff") + pass + self._invalidate_caches() + + @classmethod + def line_without_labels( + cls, line, session_ids_to_delete: set[int], label_ids_to_delete: set[int] + ): + new_datapoints = ( + [ + dp + for dp in line.datapoints + if dp.sessionid not in session_ids_to_delete + or all(lb not in label_ids_to_delete for lb in dp.label_ids) + ] + if line.datapoints is not None + else None + ) + remaining_session_ids = set(dp.sessionid for dp in new_datapoints) + removed_session_ids = session_ids_to_delete - remaining_session_ids + if set(s.id for s in line.sessions) & removed_session_ids: + new_sessions = [s for s in line.sessions if s.id not in removed_session_ids] + else: + new_sessions = line.sessions + if len(new_sessions) == 0: + return EMPTY + remaining_coverages_from_datapoints = [s.coverage for s in new_datapoints] + remaining_coverage_from_sessions_with_no_datapoints = [ + s.coverage for s in new_sessions if s.id not in remaining_session_ids + ] + + new_coverage = merge_all( + remaining_coverages_from_datapoints + + remaining_coverage_from_sessions_with_no_datapoints + ) + return dataclasses.replace( + line, + coverage=new_coverage, + datapoints=new_datapoints, + sessions=new_sessions, + ) + + def delete_labels( + self, + session_ids_to_delete: list[int] | set[int], + label_ids_to_delete: list[int] | set[int], + ): + """ + Given a list of session_ids and label_ids to delete, remove all datapoints + that belong to at least 1 session_ids to delete and include at least 1 of the label_ids to be removed. + """ + session_ids_to_delete = set(session_ids_to_delete) + label_ids_to_delete = set(label_ids_to_delete) + for index, line in self.lines: + if line.datapoints is not None: + if any( + ( + dp.sessionid in session_ids_to_delete + and label_id in label_ids_to_delete + ) + for dp in line.datapoints + for label_id in dp.label_ids + ): + # Line fits change requirements + new_line = self.line_without_labels( + line, session_ids_to_delete, label_ids_to_delete + ) + if new_line == EMPTY: + del self[index] + else: + self[index] = new_line + + self._invalidate_caches() + + @classmethod + def line_without_multiple_sessions( + cls, line: ReportLine, session_ids_to_delete: set[int] + ): + new_sessions = [s for s in line.sessions if s.id not in session_ids_to_delete] + if len(new_sessions) == 0: + return EMPTY + + new_datapoints = ( + [dt for dt in line.datapoints if dt.sessionid not in session_ids_to_delete] + if line.datapoints is not None + else None + ) + remaining_coverages = [s.coverage for s in new_sessions] + new_coverage = merge_all(remaining_coverages) + return dataclasses.replace( + line, + sessions=new_sessions, + coverage=new_coverage, + datapoints=new_datapoints, + ) + + def delete_multiple_sessions(self, session_ids_to_delete: set[int]): + current_sessions = self._present_sessions + new_sessions = current_sessions.difference(session_ids_to_delete) + if current_sessions == new_sessions: + return # nothing to do + + self._invalidate_caches() + + if not new_sessions: + # no remaining sessions means no line data + self._parsed_lines = [] + self._raw_lines = None + return + + for index, line in self.lines: + if any(s.id in session_ids_to_delete for s in line.sessions): + new_line = self.line_without_multiple_sessions( + line, session_ids_to_delete + ) + if new_line == EMPTY: + del self[index] + else: + self[index] = new_line + + self.__present_sessions = new_sessions + + +def _ignore_to_func(ignore): + """Returns a function to determine whether a a line should be saved to the ReportFile + + This function returns a function, that is called with an int parameter: which is the line number + + Args: + ignore: A dict, with a structure similar to + { + 'eof': 41, + 'lines': {40, 33, 37, 38} + } + + Returns: + A function, which takes an int as first parameter and returns a boolean + """ + eof = ignore.get("eof") + lines = ignore.get("lines") or [] + if eof: + if isinstance(eof, str): + # Sometimes eof is 'N', not sure which cases + return lambda ln: str(ln) > eof or ln in lines + # This means the eof as a number: the last line of the file and + # anything after that should be ignored + return lambda ln: ln > eof or ln in lines + else: + return lambda ln: ln in lines diff --git a/libs/shared/shared/reports/resources.py b/libs/shared/shared/reports/resources.py new file mode 100644 index 0000000000..973bf2a248 --- /dev/null +++ b/libs/shared/shared/reports/resources.py @@ -0,0 +1,604 @@ +import dataclasses +import logging +from copy import copy +from itertools import filterfalse +from typing import Any + +import orjson +import sentry_sdk + +from shared.helpers.flag import Flag +from shared.helpers.yaml import walk +from shared.reports.diff import CalculatedDiff, RawDiff, calculate_report_diff +from shared.reports.exceptions import LabelIndexNotFoundError, LabelNotFoundError +from shared.reports.filtered import FilteredReport +from shared.reports.reportfile import ReportFile +from shared.reports.types import ReportHeader, ReportTotals +from shared.utils.flare import report_to_flare +from shared.utils.make_network_file import make_network_file +from shared.utils.migrate import migrate_totals +from shared.utils.sessions import Session, SessionType +from shared.utils.totals import agg_totals + +from .serde import END_OF_CHUNK, END_OF_HEADER, serialize_report + +log = logging.getLogger(__name__) + + +def unique_everseen(iterable): + "List unique elements, preserving order. Remember all elements ever seen." + # unique_everseen('AAAABBBCCDAABBB') --> A B C D + # unique_everseen('ABBCcAD', str.lower) --> A B C D + seen = set() + seen_add = seen.add + for element in filterfalse(seen.__contains__, iterable): + seen_add(element) + yield element + + +class Report: + sessions: dict[int, Session] + _header: ReportHeader + _totals: ReportTotals | None + _files: dict[str, ReportFile] + + def __init__( + self, + files: dict[str, tuple[int, ReportTotals, Any, ReportTotals]] | None = None, + sessions: dict[int | str, Session | dict] | None = None, + totals=None, + chunks=None, + diff_totals=None, + **kwargs, + ): + self.sessions = {} + self._header = ReportHeader() + self._totals = None + self._files = {} + + if sessions: + self.sessions = { + int(sid): copy(session) + if isinstance(session, Session) + else Session.parse_session(**session) + for sid, session in sessions.items() + } + + _chunks: list[str] = [] + if chunks: + if isinstance(chunks, bytes): + chunks = chunks.decode() + if isinstance(chunks, str): + splits = chunks.split(END_OF_HEADER, maxsplit=1) + if len(splits) > 1: + _header = orjson.loads(splits[0] or "{}") + self._header = ReportHeader( + labels_index={ + int(k): v + for k, v in _header.get("labels_index", {}).items() + } + ) + chunks = splits[1] + + _chunks = chunks.split(END_OF_CHUNK) + else: + _chunks = chunks + + if files: + for name, summary in files.items(): + chunks_index = summary[0] + file_totals = summary[1] + try: + # Indices 2 and 3 may not exist. Index 2 used to be `session_totals` + # but is ignored now due to a bug. + file_diff_totals = summary[3] + except IndexError: + file_diff_totals = None + + try: + lines = _chunks[chunks_index] + except IndexError: + lines = "" + + self._files[name] = ReportFile( + name, totals=file_totals, lines=lines, diff_totals=file_diff_totals + ) + + if isinstance(totals, ReportTotals): + self._totals = totals + elif totals: + self._totals = ReportTotals(*migrate_totals(totals)) + + self.diff_totals = diff_totals + + def _invalidate_caches(self): + self._totals = None + + @property + def totals(self): + if not self._totals: + self._totals = self._process_totals() + return self._totals + + def _process_totals(self): + """Runs through the file network to aggregate totals + returns + """ + + totals = agg_totals(file.totals for file in self._files.values()) + totals.sessions = len(self.sessions) + return totals + + @property + def header(self) -> ReportHeader: + return self._header + + @header.setter + def header(self, value: ReportHeader): + self._header = value + + @property + def labels_index(self) -> dict[int, str] | None: + return self._header.get("labels_index") + + @labels_index.setter + def labels_index(self, value: dict[int, str]): + self.header = {**self.header, "labels_index": value} + + def lookup_label_by_id(self, label_id: int) -> str: + if self.labels_index is None: + raise LabelIndexNotFoundError() + if label_id not in self.labels_index: + raise LabelNotFoundError() + return self.labels_index[label_id] + + @classmethod + def from_chunks(cls, *args, **kwargs): + return cls(*args, **kwargs) + + def has_precalculated_totals(self): + return self._totals is not None + + @property + def network(self): + for fname, data in self._files.items(): + yield ( + fname, + make_network_file(data.totals, data.diff_totals), + ) + + def __repr__(self): + try: + return "<%s files=%s>" % ( + self.__class__.__name__, + len(getattr(self, "_files", [])), + ) + except Exception: + return "<%s files=n/a>" % self.__class__.__name__ + + @property + def files(self) -> list[str]: + """returns a list of files in the report""" + return list(self._files.keys()) + + @property + def flags(self): + """returns dict(:name=)""" + flags_dict = {} + for session in self.sessions.values(): + if session.flags: + # If the session was carriedforward, mark its flags as carriedforward + session_carriedforward = ( + session.session_type == SessionType.carriedforward + ) + session_carriedforward_from = getattr( + session, "session_extras", {} + ).get("carriedforward_from") + + for flag in session.flags: + flags_dict[flag] = Flag( + self, + flag, + carriedforward=session_carriedforward, + carriedforward_from=session_carriedforward_from, + ) + return flags_dict + + def get_flag_names(self) -> list[str]: + all_flags = set() + for session in self.sessions.values(): + if session and session.flags: + all_flags.update(session.flags) + return sorted(all_flags) + + def append(self, _file, joined=True): + """adds or merged a file into the report""" + if _file is None: + # skip empty adds + return False + + elif not isinstance(_file, ReportFile): + raise TypeError("expecting ReportFile got %s" % type(_file)) + + elif len(_file) == 0: + # dont append empty files + return False + + assert _file.name, "file must have a name" + + existing_file = self._files.get(_file.name) + if existing_file is not None: + existing_file.merge(_file, joined) + else: + self._files[_file.name] = _file + + self._invalidate_caches() + return True + + def get(self, filename): + return self._files.get(filename) + + def resolve_paths(self, paths: list[tuple[str, str | None]]): + for old, new in paths: + if old in self._files: + self.rename(old, new) + + def rename(self, old: str, new: str | None): + file = self._files.pop(old) + if file is not None: + if new: + file.name = new + self._files[new] = file + + self._invalidate_caches() + return True + + def __getitem__(self, filename): + _file = self.get(filename) + if _file is None: + raise IndexError("File at path %s not found in report" % filename) + return _file + + def __delitem__(self, filename): + self._files.pop(filename) + return True + + def get_file_totals(self, path: str) -> ReportTotals | None: + file = self._files.get(path) + if file is None: + log.warning( + "Fetching file totals for a file that isn't in the report", + extra=dict(path=path), + ) + return None + + return file.totals + + def next_session_number(self): + start_number = len(self.sessions) + while start_number in self.sessions or str(start_number) in self.sessions: + start_number += 1 + return start_number + + def add_session(self, session, use_id_from_session=False): + sessionid = session.id if use_id_from_session else self.next_session_number() + self.sessions[sessionid] = session + if self._totals: + # add session to totals + if use_id_from_session: + self._totals = dataclasses.replace( + self._totals, sessions=self._totals.sessions + 1 + ) + else: + self._totals = dataclasses.replace(self._totals, sessions=sessionid + 1) + + return sessionid, session + + def __iter__(self): + """Iter through all the files + yielding + """ + for file in self._files.values(): + yield file + + def __contains__(self, filename): + return filename in self._files + + @sentry_sdk.trace + def merge(self, new_report, joined=True): + """combine report data from another""" + if new_report is None: + return + + elif not isinstance(new_report, Report): + raise TypeError("expecting type Report got %s" % type(new_report)) + + elif new_report.is_empty(): + return + + # merge files + for _file in new_report: + if _file.name: + self.append(_file, joined) + + def is_empty(self): + """returns boolean if the report has no content""" + return len(self._files) == 0 + + def __bool__(self): + return self.is_empty() is False + + def serialize(self, with_totals=True) -> tuple[bytes, bytes, ReportTotals | None]: + """ + Serializes a report as `(report_json, chunks, totals)`. + + The `totals` is either a `ReportTotals`, or `None`, depending on the `with_totals` flag. + """ + return serialize_report(self, with_totals) + + @sentry_sdk.trace + def flare(self, changes=None, color=None): + if changes is not None: + """ + if changes are provided we produce a new network + only pass totals if they change + """ + # + changed_coverages = dict( + ( + ( + individual_change.path, + individual_change.totals.coverage + if not individual_change.new and individual_change.totals + else None, + ) + for individual_change in changes + ) + ) + # + classes = dict( + ((_Change.path, "s") for _Change in changes if not _Change.in_diff) + ) + + def _network(): + for name, _NetworkFile in self.network: + changed_coverage = changed_coverages.get(name) + if changed_coverage: + # changed file + yield ( + name, + ReportTotals( + lines=_NetworkFile.totals.lines, + coverage=float(changed_coverage), + ), + ) + else: + diff = _NetworkFile.diff_totals + if diff and diff.lines > 0: # lines > 0 + # diff file + yield ( + name, + ReportTotals( + lines=_NetworkFile.totals.lines, + coverage=-1 + if float(diff.coverage) + < float(_NetworkFile.totals.coverage) + else 1, + ), + ) + + else: + # unchanged file + yield name, ReportTotals(lines=_NetworkFile.totals.lines) + + network = _network() + + def color(cov): + return ( + "purple" + if cov is None + else "#e1e1e1" + if cov == 0 + else "green" + if cov > 0 + else "red" + ) + + else: + network = ( + (path, _NetworkFile.totals) for path, _NetworkFile in self.network + ) + classes = {} + # [TODO] [v4.4.0] remove yaml from args, use below + # color = self.yaml.get(('coverage', 'range')) + + return report_to_flare(network, color, classes) + + def filter(self, paths=None, flags=None): + if paths: + if not isinstance(paths, (list, set, tuple)): + raise TypeError( + "expecting list for argument paths got %s" % type(paths) + ) + if paths is None and flags is None: + return self + return FilteredReport(self, path_patterns=paths, flags=flags) + + @sentry_sdk.trace + def does_diff_adjust_tracked_lines(self, diff, future_report, future_diff): + """ + Returns if the diff touches tracked lines + + master . A . C + pull | . . B + + :diff = A...C + :future_report = B + :future_diff = C...B + + future_report is necessary because it is used to determin if + lines added in the diff are tracked by codecov + """ + if diff and diff.get("files"): + for path, data in diff["files"].items(): + future_state = walk(future_diff, ("files", path, "type")) + if data["type"] == "deleted" and path in self: # deleted # and tracked + # found a file that was tracked and deleted + return True + + elif ( + data["type"] == "new" + and future_state != "deleted" # newly tracked + and path # not deleted in future + in future_report # found in future + ): + # newly tracked file + return True + + elif data["type"] == "modified": + in_past = path in self + in_future = future_state != "deleted" and path in future_report + if in_past and in_future: + # get the future version + future_file = future_report.get(path) + # if modified + if future_state == "modified": + # shift the lines to "guess" what C was + future_file.shift_lines_by_diff( + future_diff["files"][path], forward=False + ) + + if self.get(path).does_diff_adjust_tracked_lines( + data, future_file + ): + # lines changed + return True + + elif in_past and not in_future: + # missing in future + return True + + elif not in_past and in_future: + # missing in pats + return True + + return False + + @sentry_sdk.trace + def shift_lines_by_diff(self, diff, forward=True): + """ + [volitile] will permanently adjust repot report + + Takes a and offsets the line based on additions and removals + """ + if diff and diff.get("files"): + for path, data in diff["files"].items(): + if data["type"] == "modified" and path in self: + file = self.get(path) + file.shift_lines_by_diff(data, forward=forward) + + def calculate_diff(self, diff: RawDiff) -> CalculatedDiff: + """ + Calculates the per-file totals (and total) of the parts + from a `git diff` that are relevant in the report + """ + return calculate_report_diff(self, diff) + + def save_diff_calculation(self, diff, diff_result): + diff["totals"] = diff_result["general"] + self.diff_totals = diff["totals"] + for filename, file_totals in diff_result["files"].items(): + data = diff["files"].get(filename) + data["totals"] = file_totals + file = self._files[filename] + if file_totals.lines == 0: + file_totals = dataclasses.replace( # noqa: PLW2901 + file_totals, coverage=None, complexity=None, complexity_total=None + ) + file.diff_totals = file_totals + + @sentry_sdk.trace + def apply_diff(self, diff, _save=True): + """ + Add coverage details to the diff at ['coverage'] = + returns + """ + if not diff or not diff.get("files"): + return None + totals = self.calculate_diff(diff) + if _save and totals: + self.save_diff_calculation(diff, totals) + return totals.get("general") + + def get_uploaded_flags(self): + flags = set() + for sess in self.sessions.values(): + if sess.session_type == SessionType.uploaded and sess.flags is not None: + flags.update(sess.flags) + return flags + + def delete_labels( + self, sessionids: list[int] | set[int], labels_to_delete: list[int] | set[int] + ): + files_to_delete = [] + for file in self: + file.delete_labels(sessionids, labels_to_delete) + if not file: + files_to_delete.append(file.name) + for file in files_to_delete: + del self[file] + + self._invalidate_caches() + return sessionids + + def delete_multiple_sessions(self, session_ids_to_delete: list[int] | set[int]): + session_ids_to_delete = set(session_ids_to_delete) + for sessionid in session_ids_to_delete: + self.sessions.pop(sessionid) + + files_to_delete = [] + for file in self: + file.delete_multiple_sessions(session_ids_to_delete) + if not file: + files_to_delete.append(file.name) + for file in files_to_delete: + del self[file] + + self._invalidate_caches() + + @sentry_sdk.trace + def change_sessionid(self, old_id: int, new_id: int): + """ + This changes the session with `old_id` to have `new_id` instead. + It patches up all the references to that session across all files and line records. + + In particular, it changes the id in all the `LineSession`s and `CoverageDatapoint`s, + and does the equivalent of `calculate_present_sessions`. + """ + session = self.sessions[new_id] = self.sessions.pop(old_id) + session.id = new_id + + for file in self: + all_sessions = set() + + for idx, _line in enumerate(file._lines): + if not _line: + continue + + # this turns the line into an actual `ReportLine` + line = file._lines[idx] = file._line(_line) + + for session in line.sessions: + if session.id == old_id: + session.id = new_id + all_sessions.add(session.id) + + if line.datapoints: + for point in line.datapoints: + if point.sessionid == old_id: + point.sessionid = new_id + + file._invalidate_caches() + file.__present_sessions = all_sessions + + self._invalidate_caches() diff --git a/libs/shared/shared/reports/serde.py b/libs/shared/shared/reports/serde.py new file mode 100644 index 0000000000..91212573c2 --- /dev/null +++ b/libs/shared/shared/reports/serde.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +import dataclasses +from decimal import Decimal +from fractions import Fraction +from types import GeneratorType +from typing import TYPE_CHECKING + +import orjson +import sentry_sdk + +from .reportfile import ReportFile +from .types import ReportLine, ReportTotals + +if TYPE_CHECKING: + from .resources import Report + + +END_OF_CHUNK = "\n<<<<< end_of_chunk >>>>>\n" +END_OF_HEADER = "\n<<<<< end_of_header >>>>>\n" + + +@sentry_sdk.trace +def serialize_report( + report: Report, with_totals=True +) -> tuple[bytes, bytes, ReportTotals | None]: + """ + Serializes a report as `(report_json, chunks, totals)`. + + The `totals` is either a `ReportTotals`, or `None`, depending on the `with_totals` flag. + """ + + indexed_files = list(enumerate(report._files.values())) + + chunks = ( + orjson.dumps(report._header, option=orjson_option).decode() + + END_OF_HEADER + + END_OF_CHUNK.join(_encode_chunk(file) for i, file in indexed_files) + ) + + if with_totals: + totals = report.totals + totals.diff = report.diff_totals + + files = { + file.name: [i, file.totals, None, file.diff_totals] + for i, file in indexed_files + } + else: + totals = None + + files = {file.name: [i, None] for i, file in indexed_files} + + report_json = orjson.dumps( + {"files": files, "sessions": report.sessions, "totals": totals}, + default=report_default, + option=orjson_option, + ) + + return (report_json, chunks.encode(), totals) + + +def report_default(obj): + if dataclasses.is_dataclass(obj): + return obj.astuple() + elif isinstance(obj, Fraction): + return str(obj) + elif isinstance(obj, Decimal): + return str(obj) + elif isinstance(obj, ReportTotals): + # reduce totals + return obj.to_database() + elif hasattr(obj, "_encode"): + return obj._encode() + elif isinstance(obj, GeneratorType): + obj = list(obj) + # let the base class default method raise the typeerror + return obj + + +orjson_option = orjson.OPT_PASSTHROUGH_DATACLASS | orjson.OPT_NON_STR_KEYS + + +def _dumps_not_none(value) -> str: + if isinstance(value, list): + return orjson.dumps( + _rstrip_none(list(value)), default=report_default, option=orjson_option + ).decode() + if isinstance(value, ReportLine): + return orjson.dumps( + _rstrip_none(list(value.astuple())), + default=report_default, + option=orjson_option, + ).decode() + return value if value and value != "null" else "" + + +def _rstrip_none(lst): + while lst[-1] is None: + lst.pop(-1) + return lst + + +def chunk_default(obj): + if dataclasses.is_dataclass(obj): + return obj.astuple() + return obj + + +def _encode_chunk(chunk) -> str: + if chunk is None: + return "null" + elif isinstance(chunk, ReportFile): + if isinstance(chunk._raw_lines, str): + return chunk._raw_lines + else: + return ( + orjson.dumps(chunk.details, option=orjson_option).decode() + + "\n" + + "\n".join(_dumps_not_none(line) for line in chunk._lines) + ) + elif isinstance(chunk, (list, dict)): + return orjson.dumps(chunk, default=chunk_default, option=orjson_option).decode() + else: + return chunk diff --git a/libs/shared/shared/reports/totals.py b/libs/shared/shared/reports/totals.py new file mode 100644 index 0000000000..f1bc8314e0 --- /dev/null +++ b/libs/shared/shared/reports/totals.py @@ -0,0 +1,60 @@ +from typing import Iterator + +from shared.helpers.numeric import ratio +from shared.reports.types import ReportLine, ReportTotals +from shared.utils.merge import LineType as Coverage +from shared.utils.merge import line_type as coverage_type + + +def get_line_totals(lines: Iterator[ReportLine]) -> ReportTotals: + """ + Calculates the totals (`ReportTotals`) across all the given `lines` (`ReportLine`s). + """ + hits = 0 + misses = 0 + partials = 0 + branches = 0 + methods = 0 + messages = 0 + complexity = 0 + complexity_total = 0 + + for line in lines: + match coverage_type(line.coverage): + case Coverage.hit: + hits += 1 + case Coverage.miss: + misses += 1 + case Coverage.partial: + partials += 1 + + if line.type == "b": + branches += 1 + elif line.type == "m": + methods += 1 + + if line.messages: + messages += len(line.messages) + + if isinstance(line.complexity, int): + complexity += line.complexity + elif line.complexity: + complexity += line.complexity[0] + complexity_total += line.complexity[1] + + total_lines = hits + misses + partials + + return ReportTotals( + files=0, + lines=total_lines, + hits=hits, + misses=misses, + partials=partials, + coverage=ratio(hits, total_lines) if total_lines else None, + branches=branches, + methods=methods, + messages=messages, + sessions=0, + complexity=complexity, + complexity_total=complexity_total, + ) diff --git a/libs/shared/shared/reports/types.py b/libs/shared/shared/reports/types.py new file mode 100644 index 0000000000..17275985b3 --- /dev/null +++ b/libs/shared/shared/reports/types.py @@ -0,0 +1,234 @@ +import logging +from dataclasses import asdict, dataclass +from decimal import Decimal +from enum import Enum +from typing import Dict, List, Optional, Sequence, Tuple, TypedDict, Union + +log = logging.getLogger(__name__) + + +@dataclass +class ReportTotals(object): + files: int = 0 + lines: int = 0 + hits: int = 0 + misses: int = 0 + partials: int = 0 + # The coverage is a string of a float that's rounded to 5 decimal places (or "100", "0") + # i.e. "98.76543", "100", "0" are all valid. + coverage: Optional[str] = 0 + branches: int = 0 + methods: int = 0 + messages: int = 0 + sessions: int = 0 + complexity: int = 0 + complexity_total: int = 0 + diff: int = 0 + + def __iter__(self): + return iter(self.astuple()) + + def astuple(self): + return ( + self.files, + self.lines, + self.hits, + self.misses, + self.partials, + self.coverage, + self.branches, + self.methods, + self.messages, + self.sessions, + self.complexity, + self.complexity_total, + self.diff, + ) + + def to_database(self): + obj = list(self) + while obj and obj[-1] in ("0", 0): + obj.pop() + return obj + + def asdict(self): + return asdict(self) + + @classmethod + def default_totals(cls): + return cls( + files=0, + lines=0, + hits=0, + misses=0, + partials=0, + coverage=None, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + + +@dataclass +class LineSession(object): + __slots__ = ("id", "coverage", "branches", "partials", "complexity") + id: int + coverage: Decimal + branches: list[int] | None + partials: Sequence[int] + complexity: int + + def __init__(self, id, coverage, branches=None, partials=None, complexity=None): + self.id = id + self.coverage = coverage + self.branches = branches + self.partials = partials + self.complexity = complexity + + def astuple(self): + if self.branches is None and self.partials is None and self.complexity is None: + return (self.id, self.coverage) + return (self.id, self.coverage, self.branches, self.partials, self.complexity) + + +@dataclass +class CoverageDatapoint(object): + __slots__ = ("sessionid", "coverage", "coverage_type", "label_ids") + sessionid: int + coverage: Decimal + coverage_type: Optional[str] + label_ids: List[int] + + def astuple(self): + return ( + self.sessionid, + self.coverage, + self.coverage_type, + self.label_ids, + ) + + def __post_init__(self): + if self.label_ids is not None: + + def possibly_cast_to_int(el): + return int(el) if isinstance(el, str) and el.isnumeric() else el + + self.label_ids = [possibly_cast_to_int(el) for el in self.label_ids] + + def key_sorting_tuple(self): + return ( + self.sessionid, + str(self.coverage), + self.coverage_type if self.coverage_type is not None else "", + self.label_ids if self.label_ids is not None else [], + ) + + +@dataclass +class ReportLine(object): + __slots__ = ("coverage", "type", "sessions", "messages", "complexity", "datapoints") + coverage: Decimal + type: str + sessions: Sequence[LineSession] + messages: List[str] + complexity: Union[int, Tuple[int, int]] + datapoints: Optional[List[CoverageDatapoint]] + + @classmethod + def create( + cls, + coverage=None, + type=None, + sessions=None, + messages=None, + complexity=None, + datapoints=None, + ): + return cls( + coverage=coverage, + type=type, + sessions=sessions, + messages=messages, + complexity=complexity, + datapoints=datapoints, + ) + + def astuple(self): + return ( + self.coverage, + self.type, + [s.astuple() for s in self.sessions] if self.sessions else None, + self.messages, + self.complexity, + [dt.astuple() for dt in self.datapoints] if self.datapoints else None, + ) + + def __post_init__(self): + if self.sessions is not None: + for i, sess in enumerate(self.sessions): + if not isinstance(sess, LineSession) and sess is not None: + self.sessions[i] = LineSession(*sess) + else: + self.sessions = {} + + if self.datapoints is not None: + for i, cov_dp in enumerate(self.datapoints): + if not isinstance(cov_dp, CoverageDatapoint) and cov_dp is not None: + self.datapoints[i] = CoverageDatapoint(*cov_dp) + + +@dataclass +class Change(object): + path: str = None + new: bool = False + deleted: bool = False + in_diff: bool = None + old_path: str = None + totals: ReportTotals = None + + def __post_init__(self): + if self.totals is not None: + if not isinstance(self.totals, ReportTotals): + self.totals = ReportTotals(*self.totals) + + +EMPTY = "" + +TOTALS_MAP = tuple("fnhmpcbdMsCN") + + +SessionTotals = ReportTotals + + +@dataclass +class NetworkFile(object): + totals: ReportTotals + diff_totals: ReportTotals + + def __init__(self, totals=None, diff_totals=None, *args, **kwargs) -> None: + self.totals = totals + self.diff_totals = diff_totals + + def astuple(self): + return ( + self.totals.astuple(), + # Placeholder for deprecated/broken `session_totals` field. + # Old reports had a map of session ID to per-session totals here, + # but they weren't used and a bug caused them to bloat wildly. + None, + self.diff_totals.astuple() if self.diff_totals else None, + ) + + +class ReportHeader(TypedDict): + labels_index: Dict[int, str] + + +class UploadType(Enum): + COVERAGE = "coverage" + TEST_RESULTS = "test_results" + BUNDLE_ANALYSIS = "bundle_analysis" diff --git a/libs/shared/shared/rollouts/__init__.py b/libs/shared/shared/rollouts/__init__.py new file mode 100644 index 0000000000..3b57973f23 --- /dev/null +++ b/libs/shared/shared/rollouts/__init__.py @@ -0,0 +1,349 @@ +import json +import logging +import os +from functools import cached_property +from typing import Optional + +import mmh3 +from asgiref.sync import sync_to_async +from cachetools.func import lru_cache, ttl_cache + +from shared.config import get_config +from shared.django_apps.rollouts.models import ( + FeatureFlag, + FeatureFlagVariant, +) +from shared.django_apps.utils.rollout_utils import rollout_universe_to_override_string + +log = logging.getLogger("__name__") + + +class Feature: + """ + Represents a feature and its rollout parameters, fetched from the database (see django_apps/rollouts/models.py). + Given an identifier (repo_id, owner_id, etc..), it will decide which variant of a feature should be + used for the request. Each variant will have a `value` that will be returned if that variant is decided to + be used. For example: if you want an ON and OFF variant for your feature, you could have the values + be true and false respectively + + You can modify the parameters of your feature flag via Django Admin. The parameters are fetched and updated roughly + every 5 minutes, meaning it can take up to 5 minutes for changes to show up here. + + If you manage your own deployment, you can disable or override feature checks with environment variables: + - If `CODECOV__FEATURE__{name.upper()}` is set, its value will be deserialized as JSON and returned for the + `Feature` with that name + - If `CODECOV__FEATURE__DISABLE` is set, checks will be skipped and default values will be returned every time + for every feature except those with overrides set via `CODECOV__FEATURE__{name.upper()}` env vars + + If you instantiate a `Feature` instance with a new name, the associated database entry + will be created for you. Otherwise, the existing database entry will be used to populate + the values. Also if you change the name of an existing feature, you will need to update + the name where it is instrumented in the code aswell. + + Examples: + + A simple on/off feature rolled out to 20% of repos: + # By default, features have no variants — you create them via Django Admin. You can create the `on` + # variant there, along with setting the proportion and salt for the flag. + MY_FEATURE_BY_REPO = Feature("my_feature") + + # DB: + # FeatureFlag: + # name: my_feature + # proportion: 0.0 # default value + # salt: ajsdopijaejapvjghiujnarapsjf # default is randomly generated + # + # FeatureFlagVariant: + # name: my_feature_on + # feature_flag: my_feature + # proportion: 1 + # value: true + + A simple A/B test rolled out to 10% of users (5% test, 5% control): + MY_EXPERIMENT_BY_USER = Feature("my_experiment") + + # DB: + # FeatureFlag: + # name: my_experiment + # proportion: 0.1 + # salt: foajdisjfosdjrandomfsfsdfsfsfs + # + # FeatureFlagVariant: + # name: test + # feature_flag: my_experiment + # proportion: 0.5 + # value: true + # + # FeatureFlagVariant: + # name: control + # feature_flag: my_experiment + # proportion: 0.5 + # value: false + + After creating a feature, you can instrument it in code like so: + from shared.rollouts.features import MY_EXPERIMENT_BY_USER + if MY_EXPERIMENT_BY_USER.check_value(user_id, default=False) == True: + new_behavior() + else: + old_behavior() + + Parameters: + - `name`: the unique name of your feature flag you created in django admin + + If you discover a bug and roll back your feature, it's good practice to + change the salt to any other string before restarting the rollout. Changing + the salt basically reassigns every id to a new bucket so the same users + don't feel churned by our rocky rollout. + """ + + HASHSPACE = 2**128 + + def __init__( + self, + name, + feature_flag: Optional[FeatureFlag] = None, + ff_variants: Optional[list[FeatureFlagVariant]] = None, + ): + self.name = name + self.feature_flag = feature_flag + self.ff_variants = ff_variants + + # See if this environment has disabled feature flagging entirely. + # These environments will always get default values. + self.env_disable = os.getenv("CODECOV__FEATURE__DISABLE") is not None + + # See if this environment has provided an override for this feature in + # its environment variables. Since feature variant values are stored in + # the database as JSON, we read these overrides as JSON as well. + # + # We need to be able to tell the difference between the case where an + # override is _undefined_ and the case where the override is defined and + # set to `"null"`/`None`. Two cases: + # - If the override is defined, `hasattr(self, 'env_override') == True` + # - If the override is undefined, `hasattr(self, 'env_override') == False` + env_override = os.getenv(f"CODECOV__FEATURE__{name.upper()}") + if env_override is not None: + self.env_override = json.loads(env_override or '""') + + self.refresh = get_config( + "setup", "skip_feature_cache", default=False + ) # to be used only during development + if self.refresh: + log.warning( + "skip_feature_cache for Feature should only be turned on in development environments, and should not be used in production" + ) + + def check_value(self, identifier, default=False): + """ + Returns the value of the applicable feature variant for an identifier. This is commonly a boolean for feature variants + that represent an ON variant and an OFF variant, but could be other values aswell. You can modify the values in + feature variants via Django Admin. + """ + + if hasattr(self, "env_override"): + return self.env_override + + if self.env_disable: + return default + + if self.refresh: + self._fetch_and_set_from_db.cache_clear() + + # Will only run and refresh values from the database every ~5 minutes due to TTL cache + self._fetch_and_set_from_db() + + return self._check_value(identifier, default) + + @sync_to_async + def check_value_async(self, identifier, default=False): + return self.check_value(identifier, default) + + def check_value_no_fetch(self, identifier, default=False): + """ + Same as `check_value()` except does not make any DB calls, and assumes the flag data has been passed into the class + during object initialization. + """ + if hasattr(self, "env_override"): + return self.env_override + + if self.env_disable: + return default + + return self._check_value_no_lru(identifier, False) + + @cached_property + def _buckets(self): + """ + Calculates the bucket boundaries for feature variants. Simple logic but + the use of floats and int casting may introduce error. + + To check if a feature should be enabled for a specific repo (for instance) + this class will compute a hash including: + - The experiment's name + - The repo's id + - A salt + + The range of possible hash values is divvied up into buckets based on the + `proportion` of the feature flag and its `variants`. The hash for this repo + will fall into one of those buckets and the corresponding variant (or default + value) will be returned. + + Each bucket in the buckets array represents a range: (0, 1000, `enabled_variant`). In + this case, hashes that land in [0, 1000) will be assigned `enabled_variant` + """ + + buckets = [] + quantile = 0 + + for variant in self.ff_variants: + variant_test_population = int(variant.proportion * Feature.HASHSPACE) + + start = int(quantile * Feature.HASHSPACE) + end = int(start + (variant_test_population * self.feature_flag.proportion)) + + quantile += variant.proportion + buckets.append((start, end, variant)) + + return buckets + + def _get_override_variant(self, identifier, identifier_override_field): + """ + Retrieves the feature variant applicable to the given identifer according to + the defined applicable overrides. Returns None if no override is found. + """ + for variant in self.ff_variants: + if identifier in getattr(variant, identifier_override_field): + return variant + return None + + def _is_valid_rollout(self): + """ + Checks if the database entries were given valid values, which is very + possible since these values can be modified via Django Admin. Sum of + variants should equal to 1, and proportions should never be greater than + 1 or less than 0. + """ + variant_proportion_sum = sum(map(lambda x: x.proportion, self.ff_variants)) + variant_proportions_in_range = True not in map( + lambda x: x.proportion < 0 or x.proportion > 1, self.ff_variants + ) + feature_proportion_in_range = ( + self.feature_flag.proportion <= 1 and self.feature_flag.proportion >= 0 + ) + return ( + (variant_proportion_sum == 1.0 or variant_proportion_sum == 0.0) + and variant_proportions_in_range + and feature_proportion_in_range + ) + + @ttl_cache(maxsize=64, ttl=300) # 5 minute time-to-live cache + def _fetch_and_set_from_db(self): + """ + Updates the instance with the newest values from database, and clears other caches so + that their values can be recalculated. + """ + new_feature_flag = FeatureFlag.objects.filter(pk=self.name).first() + new_ff_variants = sorted( + list(FeatureFlagVariant.objects.filter(feature_flag=self.name)), + key=lambda x: x.variant_id, + ) + + if not new_feature_flag: + # create default feature flag + new_feature_flag = FeatureFlag.objects.create(name=self.name) + + clear_cache = False + + # Either completely new or different from what we got last time + if (not self.feature_flag) or self._is_different( + new_feature_flag, self.feature_flag + ): + self.feature_flag = new_feature_flag + clear_cache = True + if (not self.ff_variants) or len(self.ff_variants) != len(new_ff_variants): + self.ff_variants = new_ff_variants + clear_cache = True + else: + for ind in range(len(new_ff_variants)): + if self._is_different(new_ff_variants[ind], self.ff_variants[ind]): + self.ff_variants = new_ff_variants + clear_cache = True + break + + if clear_cache: + self._check_value.cache_clear() + + if hasattr(self, "_buckets"): + del self._buckets # clears @cached_property + + if not self._is_valid_rollout(): + log.warning( + "Feature flag is using invalid values for rollout", + extra=dict(feature_flag_name=self.name), + ) + + # NOTE: `@lru_cache` on instance methods is ordinarily a bad idea: + # - the cache is not per-instance; it's shared by all class instances + # - by holding references to function arguments in the cache, it prolongs + # their lifetimes. Since `self` is a function argument, the cache will + # prevent any instance from getting freed until it is evicted from the + # cache which could be a significant memory leak. + # In this case, we are okay with sharing a cache across instances, and the + # instances are all global constants so they won't be torn down anyway. + @lru_cache(maxsize=64) + def _check_value(self, identifier, default): + """ + This function will have its cache invalidated when `_fetch_and_set_from_db()` pulls new data so that + variant values can be returned using the most up-to-date values from the database. + """ + return self._check_value_impl(identifier, default) + + def _check_value_no_lru(self, identifier, default): + """ + Same as `_check_value()` except no lru cache. This is used by the `/internal/features` endpoint and can't + use lru as it would prevent class instances from getting garbage collected, as the endpoint instantiates + the `Feature` class on every request. + """ + + return self._check_value_impl(identifier, default) + + def _check_value_impl(self, identifier, default): + """ + This is the core logic of how a feature flag variant is assigned to a user based on some identifier, and + returns the variant's value. + """ + # check if an override exists + identifier_override_field = rollout_universe_to_override_string( + self.feature_flag.rollout_universe + ) + override_variant = self._get_override_variant( + identifier, identifier_override_field + ) + + if override_variant: + return override_variant.value + + if self.feature_flag.proportion == 1.0 and len(self.ff_variants) == 1: + # This feature is fully rolled out, since it only has one variant, + # we can skip the hashing and just return its value. + return self.ff_variants[0].value + + key = mmh3.hash128( + self.feature_flag.name + str(identifier) + self.feature_flag.salt + ) + for bucket_start, bucket_end, variant in self._buckets: + if bucket_start <= key and key < bucket_end: + return variant.value + + return default + + def _is_different(self, inst1, inst2): + fields = inst1._meta.get_fields() + + for field in fields: + if isinstance(field, str) and getattr(inst1, field) != getattr( + inst2, field + ): + return False + + return True diff --git a/libs/shared/shared/rollouts/feature_flags_experiments.sql b/libs/shared/shared/rollouts/feature_flags_experiments.sql new file mode 100644 index 0000000000..039744b374 --- /dev/null +++ b/libs/shared/shared/rollouts/feature_flags_experiments.sql @@ -0,0 +1,90 @@ +/* +These are the SQL queries used in Metabase so that we can make the feature flag experiment dashboards like this: +https://metabase.codecov.dev/question/170-experiment-dashboard-by-owner-id?variant_name=list_repos_generator&metric=worker.task.app.tasks.sync_repos.SyncRepos.core_runtime&start_date=2024-04-30 + +These SQL queries are used for the following two dashboards: +https://metabase.codecov.dev/question/175-experiment-dashboard-by-repo-id?variant_name=&metric=&start_date= +https://metabase.codecov.dev/question/170-experiment-dashboard-by-owner-id?variant_name=&metric=&start_date= + +The code that gets executed on Metabase lives within Metabase, but ideally changes should be also checked-in to this file so that we have a version history. + +The relevant tables are: `feature_exposures`, `telemetry_simple`, `feature_flags`, and `feature_flag_variants`. We bucket the timestamps based on the hour which is how we're able to correlate +feature exposures with the telemetry simple metrics. + +The {{___}} notation with {{variant_name}} or {{metric}} are a metabase specific thing that allow us to have variable dropdowns in the dashboard, which is configured through the Metabase UI. Note: +the values populated for {{metric}} are actually hardcoded because querying for all the metrics on-demand was too long of a query. If new metrics/celery tasks are added, those values need to be +populated via the Metabase UI. +*/ + +-- OWNER_ID DASHBOARD +SELECT + "feature_flag_variants__via__feature_flag_variant_id"."name" AS "feature_flag_variants__via__feature_flag_variant_id__name", + DATE_TRUNC('hour', "public"."feature_exposures"."timestamp") AS "timestamp", + COUNT(*) AS "samples", + AVG("Telemetry Simple - Feature Flag"."value") AS "task runtime" +FROM + "public"."feature_exposures" + +LEFT JOIN "public"."telemetry_simple" AS "Telemetry Simple - Feature Flag" ON ( + DATE_TRUNC('hour', "public"."feature_exposures"."timestamp") = DATE_TRUNC( + 'hour', + "Telemetry Simple - Feature Flag"."timestamp" + ) + ) + + AND ( + "public"."feature_exposures"."owner" = "Telemetry Simple - Feature Flag"."owner_id" + ) + LEFT JOIN "public"."feature_flag_variants" AS "feature_flag_variants__via__feature_flag_variant_id" ON "public"."feature_exposures"."feature_flag_variant_id" = "feature_flag_variants__via__feature_flag_variant_id"."variant_id" +WHERE + ( + "public"."feature_exposures"."feature_flag_id" = {{variant_name}} + ) + AND ( + "Telemetry Simple - Feature Flag"."name" = {{metric}} + ) AND ( + "feature_exposures"."timestamp" > {{start_date}} + ) +GROUP BY + "feature_flag_variants__via__feature_flag_variant_id"."name", + DATE_TRUNC('hour', "public"."feature_exposures"."timestamp") +ORDER BY + "feature_flag_variants__via__feature_flag_variant_id"."name" ASC, + DATE_TRUNC('hour', "public"."feature_exposures"."timestamp") ASC + + +-- REPO_ID DASHBOARD +SELECT + "feature_flag_variants__via__feature_flag_variant_id"."name" AS "feature_flag_variants__via__feature_flag_variant_id__name", + DATE_TRUNC('hour', "public"."feature_exposures"."timestamp") AS "timestamp", + COUNT(*) AS "samples", + AVG("Telemetry Simple - Feature Flag"."value") AS "task runtime" +FROM + "public"."feature_exposures" + +LEFT JOIN "public"."telemetry_simple" AS "Telemetry Simple - Feature Flag" ON ( + DATE_TRUNC('hour', "public"."feature_exposures"."timestamp") = DATE_TRUNC( + 'hour', + "Telemetry Simple - Feature Flag"."timestamp" + ) + ) + + AND ( + "public"."feature_exposures"."repo" = "Telemetry Simple - Feature Flag"."repo_id" + ) + LEFT JOIN "public"."feature_flag_variants" AS "feature_flag_variants__via__feature_flag_variant_id" ON "public"."feature_exposures"."feature_flag_variant_id" = "feature_flag_variants__via__feature_flag_variant_id"."variant_id" +WHERE + ( + "public"."feature_exposures"."feature_flag_id" = {{variant_name}} + ) + AND ( + "Telemetry Simple - Feature Flag"."name" = {{metric}} + ) AND ( + "feature_exposures"."timestamp" > {{start_date}} + ) +GROUP BY + "feature_flag_variants__via__feature_flag_variant_id"."name", + DATE_TRUNC('hour', "public"."feature_exposures"."timestamp") +ORDER BY + "feature_flag_variants__via__feature_flag_variant_id"."name" ASC, + DATE_TRUNC('hour', "public"."feature_exposures"."timestamp") ASC \ No newline at end of file diff --git a/libs/shared/shared/rollouts/features.py b/libs/shared/shared/rollouts/features.py new file mode 100644 index 0000000000..d2c1159ed0 --- /dev/null +++ b/libs/shared/shared/rollouts/features.py @@ -0,0 +1,5 @@ +from . import Feature + +BUNDLE_THRESHOLD_FLAG = Feature("bundle_threshold_flag") +INCLUDE_GITHUB_COMMENT_ACTIONS_BY_OWNER = Feature("include_github_comment_actions") +NEW_MINIO = Feature("new_minio") diff --git a/libs/shared/shared/self_hosted/__init__.py b/libs/shared/shared/self_hosted/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/self_hosted/service.py b/libs/shared/shared/self_hosted/service.py new file mode 100644 index 0000000000..05dced9703 --- /dev/null +++ b/libs/shared/shared/self_hosted/service.py @@ -0,0 +1,179 @@ +import operator +from functools import reduce +from typing import Optional + +from django.conf import settings +from django.db import transaction +from django.db.models import F, Func, Q, QuerySet + +from shared.config import get_config +from shared.django_apps.codecov_auth.models import Owner +from shared.license import get_current_license + + +class LicenseException(Exception): + def __init__(self, message: str): + self.message = message + super().__init__(message) + + +def admin_owners() -> QuerySet: + """ + Returns a queryset of admin owners based on the YAML config: + + setup: + admins: + - service: + username: + - ... + """ + admins = get_config("setup", "admins", default=[]) + + filters = [ + Q(service=admin["service"], username=admin["username"]) + for admin in admins + if "service" in admin and "username" in admin + ] + + if len(filters) == 0: + return Owner.objects.none() + else: + return Owner.objects.filter(reduce(operator.or_, filters)) + + +def is_admin_owner(owner: Optional[Owner]) -> bool: + """ + Returns true if the given owner is an admin. + """ + return owner is not None and admin_owners().filter(pk=owner.pk).exists() + + +def activated_owners() -> QuerySet: + """ + Returns all owners that are activated in ANY org's `plan_activated_users` + across the entire instance. + """ + owner_ids = ( + Owner.objects.annotate( + plan_activated_owner_ids=Func( + F("plan_activated_users"), + function="unnest", + ) + ) + .values_list("plan_activated_owner_ids", flat=True) + .distinct() + ) + return Owner.objects.filter(pk__in=owner_ids) + + +def is_activated_owner(owner: Owner) -> bool: + """ + Returns true if the given owner is activated in this instance. + """ + return activated_owners().filter(pk=owner.pk).exists() + + +def license_seats() -> int: + """ + Max number of seats allowed by the current license. + """ + enterprise_license = get_current_license() + if not enterprise_license.is_valid: + return 0 + return enterprise_license.number_allowed_users or 0 + + +def enterprise_has_seats_left() -> bool: + """ + The activated_owner_query is heavy, so check the license first, only proceed if they have a valid license. + """ + license_seat_count = license_seats() + if license_seat_count == 0: + return False + owners = activated_owners() + count = owners.count() + return count < license_seat_count + + +def can_activate_owner(owner: Owner) -> bool: + """ + Returns true if there are available seats left for activation. + """ + if is_activated_owner(owner): + # user is already activated in at least 1 org + return True + else: + return activated_owners().count() < license_seats() + + +@transaction.atomic +def activate_owner(owner: Owner): + """ + Activate the given owner in ALL orgs that the owner is a part of. + """ + if not settings.IS_ENTERPRISE: + raise Exception("activate_owner is only available in self-hosted environments") + + if not can_activate_owner(owner): + raise LicenseException( + "No seats remaining. Please contact Codecov support or deactivate users." + ) + + Owner.objects.filter(pk__in=owner.organizations).update( + plan_activated_users=Func( + owner.pk, + function="array_append_unique", + template="%(function)s(plan_activated_users, %(expressions)s)", + ) + ) + + +def deactivate_owner(owner: Owner): + """ + Deactivate the given owner across ALL orgs. + """ + if not settings.IS_ENTERPRISE: + raise Exception( + "deactivate_owner is only available in self-hosted environments" + ) + + Owner.objects.filter( + plan_activated_users__contains=Func( + owner.pk, + function="array", + template="%(function)s[%(expressions)s]", + ) + ).update( + plan_activated_users=Func( + owner.pk, + function="array_remove", + template="%(function)s(plan_activated_users, %(expressions)s)", + ) + ) + + +def enable_autoactivation(): + """ + Enable auto-activation for the entire instance. + + There's no good place to store this instance-wide so we're just saving this + for all owners. + """ + Owner.objects.all().update(plan_auto_activate=True) + + +def disable_autoactivation(): + """ + Disable auto-activation for the entire instance. + + There's no good place to store this instance-wide so we're just saving this + for all owners. + """ + Owner.objects.all().update(plan_auto_activate=False) + + +def is_autoactivation_enabled(): + """ + Returns true if ANY org has auto-activation enabled. + """ + return Owner.objects.filter(plan_auto_activate=True).exists() diff --git a/libs/shared/shared/staticanalysis/__init__.py b/libs/shared/shared/staticanalysis/__init__.py new file mode 100644 index 0000000000..06a12382bd --- /dev/null +++ b/libs/shared/shared/staticanalysis/__init__.py @@ -0,0 +1,10 @@ +from shared.utils.enums import CodecovDatabaseEnum + + +class StaticAnalysisSingleFileSnapshotState(CodecovDatabaseEnum): + CREATED = (1,) + VALID = (2,) + REJECTED = (3,) + + def __init__(self, db_id): + self.db_id = db_id diff --git a/libs/shared/shared/storage/__init__.py b/libs/shared/shared/storage/__init__.py new file mode 100644 index 0000000000..2180eb62e8 --- /dev/null +++ b/libs/shared/shared/storage/__init__.py @@ -0,0 +1,7 @@ +from shared.config import get_config +from shared.storage.minio import MinioStorageService + + +def get_appropriate_storage_service(*_args, **_kwargs) -> MinioStorageService: + minio_config = get_config("services", "minio", default={}) + return MinioStorageService(minio_config) diff --git a/libs/shared/shared/storage/base.py b/libs/shared/shared/storage/base.py new file mode 100644 index 0000000000..6eb2ef5207 --- /dev/null +++ b/libs/shared/shared/storage/base.py @@ -0,0 +1,110 @@ +from abc import ABC, abstractmethod +from typing import BinaryIO, overload + +CHUNK_SIZE = 1024 * 32 +PART_SIZE = 1024 * 1024 * 20 # 20MiB + + +# Interface class for interfacing with codecov's underlying storage layer +class BaseStorageService(ABC): + @abstractmethod + def create_root_storage(self, bucket_name="archive", region="us-east-1"): + """ + Creates root storage (or bucket, as in some terminologies) + + Args: + bucket_name (str): The name of the bucket to be created (default: {'archive'}) + region (str): The region in which the bucket will be created (default: {'us-east-1'}) + + Raises: + NotImplementedError: If the current instance did not implement this method + BucketAlreadyExistsError: If the bucket already exists + """ + raise NotImplementedError() + + @abstractmethod + def write_file( + self, + bucket_name, + path, + data, + reduced_redundancy=False, + *, + is_already_gzipped: bool = False, + ): + """ + Writes a new file with the contents of `data` + (What happens if the file already exists?) + + + Args: + bucket_name (str): The name of the bucket for the file to be created on + path (str): The desired path of the file + data (str): The data to be written to the file + reduced_redundancy (bool): Whether a reduced redundancy mode should be used (default: {False}) + is_already_gzipped (bool): Whether the file is already gzipped (default: {False}) + + Raises: + NotImplementedError: If the current instance did not implement this method + """ + raise NotImplementedError() + + @abstractmethod + @overload + def read_file(self, bucket_name: str, path: str) -> bytes: ... + + @abstractmethod + @overload + def read_file(self, bucket_name: str, path: str, file_obj: BinaryIO) -> None: ... + + @abstractmethod + def read_file( + self, bucket_name: str, path: str, file_obj: BinaryIO | None = None + ) -> bytes | None: + """Reads the content of a file + + Args: + bucket_name (str): The name of the bucket for the file lives + path (str): The path of the file + file_obj (file like): A file-like object in which to write the contents + + Raises: + NotImplementedError: If the current instance did not implement this method + FileNotInStorageError: If the file does not exist + + Returns: + bytes : The contents of that file, still encoded as bytes (only when file_obj is None) + """ + raise NotImplementedError() + + @abstractmethod + def delete_file(self, bucket_name, path): + """Deletes a single file from the storage + + Note: Not all implementations raise a FileNotInStorageError + if the file is not already there in the first place. + It seems that minio, for example, returns a 204 regardless. + So while you should prepare for a FileNotInStorageError, + know that if it is not raise, it doesn't mean the file + was there beforehand. + + Args: + bucket_name (str): The name of the bucket for the file lives + path (str): The path of the file to be deleted + + Raises: + NotImplementedError: If the current instance did not implement this method + FileNotInStorageError: If the file does not exist + + Returns: + bool: True if the deletion was succesful + """ + raise NotImplementedError() + + +class PresignedURLService(ABC): + @abstractmethod + def create_presigned_put(self, bucket: str, path: str, expires: int) -> str: ... + + @abstractmethod + def create_presigned_get(self, bucket: str, path: str, expires: int) -> str: ... diff --git a/libs/shared/shared/storage/compression.py b/libs/shared/shared/storage/compression.py new file mode 100644 index 0000000000..b9144aec59 --- /dev/null +++ b/libs/shared/shared/storage/compression.py @@ -0,0 +1,40 @@ +import gzip +import importlib.metadata +from typing import IO + + +class GZipStreamReader: + def __init__(self, fileobj: IO[bytes]): + self.data = fileobj + self.bytes_compressed = 0 + + def read(self, size: int = -1, /) -> bytes: + curr_data = self.data.read(size) + + if not curr_data: + return b"" + + compressed = gzip.compress(curr_data) + self.bytes_compressed += len(compressed) + return compressed + + def tell(self) -> int: + return self.bytes_compressed + + +def zstd_decoded_by_default() -> bool: + try: + version = importlib.metadata.version("urllib3") + except importlib.metadata.PackageNotFoundError: + return False + + if version < "2.0.0": + return False + + distribution = importlib.metadata.metadata("urllib3") + if requires_dist := distribution.get_all("Requires-Dist"): + for req in requires_dist: + if "[zstd]" in req: + return True + + return False diff --git a/libs/shared/shared/storage/exceptions.py b/libs/shared/shared/storage/exceptions.py new file mode 100644 index 0000000000..53c99c817f --- /dev/null +++ b/libs/shared/shared/storage/exceptions.py @@ -0,0 +1,10 @@ +class BucketAlreadyExistsError(Exception): + pass + + +class FileNotInStorageError(Exception): + pass + + +class PutRequestRateLimitError(Exception): + pass diff --git a/libs/shared/shared/storage/memory.py b/libs/shared/shared/storage/memory.py new file mode 100644 index 0000000000..e9da720776 --- /dev/null +++ b/libs/shared/shared/storage/memory.py @@ -0,0 +1,124 @@ +from collections import defaultdict + +from shared.storage.base import CHUNK_SIZE, BaseStorageService +from shared.storage.exceptions import BucketAlreadyExistsError, FileNotInStorageError + + +class MemoryStorageService(BaseStorageService): + """ + This Service is not meant to serve as a real storage service. + It provides way to deal with testing and such + + Attributes: + config (dict): The config for this + """ + + def __init__(self, config): + self.config = config + self.root_storage_created = False + self.storage = defaultdict(dict) + + def create_root_storage(self, bucket_name="archive", region="us-east-1"): + """ + Creates root storage (or bucket, as in some terminologies) + + Args: + bucket_name (str): The name of the bucket to be created (default: {'archive'}) + region (str): The region in which the bucket will be created (default: {'us-east-1'}) + + Raises: + NotImplementedError: If the current instance did not implement this method + BucketAlreadyExistsError: If the bucket already exists + """ + if self.root_storage_created: + raise BucketAlreadyExistsError() + self.root_storage_created = True + return {"name": bucket_name} + + def write_file( + self, + bucket_name, + path, + data, + reduced_redundancy=False, + *, + is_already_gzipped: bool = False, + ): + """ + Writes a new file with the contents of `data` + (What happens if the file already exists?) + + + Args: + bucket_name (str): The name of the bucket for the file to be created on + path (str): The desired path of the file + data (str): The data to be written to the file + reduced_redundancy (bool): Whether a reduced redundancy mode should be used (default: {False}) + is_already_gzipped (bool): Whether the file is already gzipped (default: {False}) + + Raises: + NotImplementedError: If the current instance did not implement this method + """ + if isinstance(data, str): + data = data.encode() + if isinstance(data, bytes): + self.storage[bucket_name][path] = data + else: + # data is a file-like object + data.seek(0) + self.storage[bucket_name][path] = data.read() + return True + + def read_file(self, bucket_name, path, file_obj=None): + """Reads the content of a file + + Args: + bucket_name (str): The name of the bucket for the file lives + path (str): The path of the file + + Raises: + NotImplementedError: If the current instance did not implement this method + FileNotInStorageError: If the file does not exist + + Returns: + bytes : The contents of that file, still encoded as bytes + """ + try: + data = self.storage[bucket_name][path] + if file_obj is None: + return data + else: + chunks = [ + data[i : i + CHUNK_SIZE] for i in range(0, len(data), CHUNK_SIZE) + ] + for chunk in chunks: + file_obj.write(chunk) + except KeyError: + raise FileNotInStorageError() + + def delete_file(self, bucket_name, path): + """Deletes a single file from the storage + + Note: Not all implementations raise a FileNotInStorageError + if the file is not already there in the first place. + It seems that minio, for example, returns a 204 regardless. + So while you should prepare for a FileNotInStorageError, + know that if it is not raise, it doesn't mean the file + was there beforehand. + + Args: + bucket_name (str): The name of the bucket for the file lives + path (str): The path of the file to be deleted + + Raises: + NotImplementedError: If the current instance did not implement this method + FileNotInStorageError: If the file does not exist + + Returns: + bool: True if the deletion was succesful + """ + try: + del self.storage[bucket_name][path] + except KeyError: + raise FileNotInStorageError() + return True diff --git a/libs/shared/shared/storage/minio.py b/libs/shared/shared/storage/minio.py new file mode 100644 index 0000000000..24ad28be0a --- /dev/null +++ b/libs/shared/shared/storage/minio.py @@ -0,0 +1,347 @@ +import json +import logging +import os +from datetime import timedelta +from functools import lru_cache +from io import BytesIO +from typing import IO, BinaryIO, Literal, cast, overload + +import certifi +import urllib3 +import zstandard +from minio import Minio +from minio.credentials.providers import ( + ChainedProvider, + EnvAWSProvider, + EnvMinioProvider, + IamAwsProvider, +) +from minio.error import MinioException, S3Error +from minio.helpers import ObjectWriteResult +from urllib3 import HTTPResponse, Retry +from urllib3.util import Timeout + +from shared.storage.base import ( + CHUNK_SIZE, + PART_SIZE, + BaseStorageService, + PresignedURLService, +) +from shared.storage.compression import GZipStreamReader, zstd_decoded_by_default +from shared.storage.exceptions import BucketAlreadyExistsError, FileNotInStorageError + +log = logging.getLogger(__name__) + +CONNECT_TIMEOUT = 10 +READ_TIMEOUT = 60 + + +def init_minio_client( + host: str, + port: str | None, + access_key: str | None, + secret_key: str | None, + verify_ssl: bool, + iam_auth: bool, + iam_endpoint: str | None, + region: str | None, +): + """ + Initialize the minio client + + `iam_auth` adds support for IAM base authentication in a fallback pattern. + The following will be checked in order: + + * EC2 metadata -- a custom endpoint can be provided, default is None. + * AWS env vars, specifically AWS_ACCESS_KEY and AWS_SECRECT_KEY + * Minio env vars, specifically MINIO_ACCESS_KEY and MINIO_SECRET_KEY + + to support backward compatibility, the iam_auth setting should be used in the installation + configuration + + Args: + host (str): The address of the host where minio lives + port (str): The port number (as str or int should be ok) + access_key (str, optional): The access key (optional if IAM is being used) + secret_key (str, optional): The secret key (optional if IAM is being used) + verify_ssl (bool, optional): Whether minio should verify ssl + iam_auth (bool, optional): Whether to use iam_auth + iam_endpoint (str, optional): The endpoint to try to fetch EC2 metadata + region (str, optional): The region of the host where minio lives + """ + if port is not None: + host = "{}:{}".format(host, port) + + http_client = urllib3.PoolManager( + timeout=Timeout(connect=CONNECT_TIMEOUT, read=READ_TIMEOUT), + maxsize=10, + cert_reqs="CERT_REQUIRED", + ca_certs=os.environ.get("SSL_CERT_FILE") or certifi.where(), + retries=Retry( + total=5, + backoff_factor=1, + status_forcelist=[ + 408, + 429, + 500, + 502, + 503, + 504, + ], # https://cloud.google.com/storage/docs/retry-strategy#python + ), + ) + if iam_auth: + return Minio( + host, + secure=verify_ssl, + region=region, + credentials=ChainedProvider( + providers=[ + IamAwsProvider(custom_endpoint=iam_endpoint), + EnvMinioProvider(), + EnvAWSProvider(), + ] + ), + http_client=http_client, + ) + + return Minio( + host, + access_key=access_key, + secret_key=secret_key, + secure=verify_ssl, + region=region, + http_client=http_client, + ) + + +@lru_cache(maxsize=None) +def get_cached_minio_client( + host: str = "", + port: str | None = None, + access_key_id: str | None = None, + secret_access_key: str | None = None, + verify_ssl: bool = False, + iam_auth: bool = False, + iam_endpoint: str | None = None, + region: str | None = None, + **kwargs, +): + return init_minio_client( + host, + port, + access_key_id, + secret_access_key, + verify_ssl, + iam_auth, + iam_endpoint, + region, + ) + + +zstd_default = zstd_decoded_by_default() + + +# Service class for interfacing with codecov's underlying storage layer, minio +class MinioStorageService(BaseStorageService, PresignedURLService): + def __init__(self, minio_config): + self.minio_config = minio_config + + log.debug("Connecting to minio with config %s", self.minio_config) + + self.minio_client = get_cached_minio_client(**self.minio_config) + + log.debug("Done setting up minio client") + + # writes the initial storage bucket to storage via minio. + def create_root_storage(self, bucket_name="archive", region="us-east-1"): + read_only_policy = { + "Statement": [ + { + "Action": ["s3:GetObject"], + "Effect": "Allow", + "Principal": {"AWS": ["*"]}, + "Resource": [f"arn:aws:s3:::{bucket_name}/*"], + } + ], + "Version": "2012-10-17", + } + try: + if not self.minio_client.bucket_exists(bucket_name): + log.debug( + "Making bucket on bucket %s on location %s", bucket_name, region + ) + self.minio_client.make_bucket(bucket_name, location=region) + log.debug("Setting policy") + self.minio_client.set_bucket_policy( + bucket_name, json.dumps(read_only_policy) + ) + log.debug("Done creating root storage") + return {"name": bucket_name} + else: + raise BucketAlreadyExistsError(f"Bucket {bucket_name} already exists") + # todo should only pass or raise + except S3Error as e: + if e.code == "BucketAlreadyOwnedByYou": + raise BucketAlreadyExistsError(f"Bucket {bucket_name} already exists") + elif e.code == "BucketAlreadyExists": + pass + raise + except MinioException: + raise + + # Writes a file to storage will gzip if not compressed already + def write_file( + self, + bucket_name: str, + path: str, + data: IO[bytes] | str | bytes, + reduced_redundancy: bool = False, + *, + is_already_gzipped: bool = False, # deprecated + is_compressed: bool = False, + compression_type: str | None = "zstd", + metadata: dict[str, str] | None = None, + ) -> ObjectWriteResult | Literal[True]: + if isinstance(data, str): + data = BytesIO(data.encode()) + elif isinstance(data, (bytes, bytearray, memoryview)): + data = BytesIO(data) + + if is_already_gzipped: + is_compressed = True + compression_type = "gzip" + + result: IO[bytes] + if is_compressed: + result = data + else: + if compression_type == "zstd": + cctx = zstandard.ZstdCompressor() + result = cctx.stream_reader(data) + + elif compression_type == "gzip": + result = cast(IO[bytes], GZipStreamReader(data)) + + else: + result = data + + headers = {} + if compression_type: + headers["Content-Encoding"] = compression_type + if reduced_redundancy: + headers["x-amz-storage-class"] = "REDUCED_REDUNDANCY" + if metadata: + headers.update( + {f"x-amz-meta-{k}": v for k, v in metadata.items() if v is not None} + ) + + # it's safe to do a BinaryIO cast here because we know that put_object only uses a function of the shape: + # read(self, size: int = -1, /) -> bytes + # GZipStreamReader implements this (we did it ourselves) + # ZstdCompressionReader implements read(): https://github.com/indygreg/python-zstandard/blob/12a80fac558820adf43e6f16206120685b9eb880/zstandard/__init__.pyi#L233C5-L233C49 + # BytesIO implements read(): https://docs.python.org/3/library/io.html#io.BufferedReader.read + # IO[bytes] implements read(): https://github.com/python/cpython/blob/3.13/Lib/typing.py#L3502 + write_result = self.minio_client.put_object( + bucket_name, + path, + cast(BinaryIO, result), + -1, + metadata=headers, + content_type="text/plain", + part_size=PART_SIZE, + ) + + return write_result + + @overload + def read_file( + self, + bucket_name: str, + path: str, + file_obj: None = None, + metadata_container: dict[str, str] | None = None, + ) -> bytes: ... + + @overload + def read_file( + self, + bucket_name: str, + path: str, + file_obj: BinaryIO, + metadata_container: dict[str, str] | None = None, + ) -> None: ... + + def read_file( + self, + bucket_name: str, + path: str, + file_obj: BinaryIO | None = None, + metadata_container: dict[str, str] | None = None, + ) -> bytes | None: + try: + response = cast( + HTTPResponse, + self.minio_client.get_object(bucket_name, path), + ) + except S3Error as e: + if e.code == "NoSuchKey": + raise FileNotInStorageError( + f"File {path} does not exist in {bucket_name}" + ) + raise e + + if metadata_container is not None: + for header, value in response.headers.items(): + if header.startswith("x-amz-meta-"): + metadata_key = header.removeprefix("x-amz-meta-") + metadata_container[metadata_key] = value + + reader = cast(IO[bytes], response) + if ( + response.headers + and not zstd_default + and response.headers.get("Content-Encoding") == "zstd" + ): + # we have to manually decompress zstandard compressed data + cctx = zstandard.ZstdDecompressor() + # if the object passed to this has a read method then that's + # all this object will ever need, since it will just call read + # and get the bytes object resulting from it then compress that + # HTTPResponse + reader = cctx.stream_reader(reader) + + if file_obj: + file_obj.seek(0) + while chunk := reader.read(CHUNK_SIZE): + file_obj.write(chunk) + response.close() + response.release_conn() + return None + else: + res = BytesIO() + while chunk := reader.read(CHUNK_SIZE): + res.write(chunk) + response.close() + response.release_conn() + return res.getvalue() + + def delete_file(self, bucket_name: str, path: str) -> bool: + try: + # delete a file given a bucket name and a path + self.minio_client.remove_object(bucket_name, path) + return True + except S3Error as e: + if e.code == "NoSuchKey": + raise FileNotInStorageError( + f"File {path} does not exist in {bucket_name}" + ) + raise e + + def create_presigned_put(self, bucket: str, path: str, expires: int) -> str: + expires_td = timedelta(seconds=expires) + return self.minio_client.presigned_put_object(bucket, path, expires_td) + + def create_presigned_get(self, bucket: str, path: str, expires: int) -> str: + expires_td = timedelta(seconds=expires) + return self.minio_client.presigned_get_object(bucket, path, expires_td) diff --git a/libs/shared/shared/timeseries/__init__.py b/libs/shared/shared/timeseries/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/timeseries/helpers.py b/libs/shared/shared/timeseries/helpers.py new file mode 100644 index 0000000000..01ce4e983d --- /dev/null +++ b/libs/shared/shared/timeseries/helpers.py @@ -0,0 +1,5 @@ +from shared.config import get_config + + +def is_timeseries_enabled() -> bool: + return get_config("setup", "timeseries", "enabled", default=False) diff --git a/libs/shared/shared/torngit/__init__.py b/libs/shared/shared/torngit/__init__.py new file mode 100644 index 0000000000..8f167a4e69 --- /dev/null +++ b/libs/shared/shared/torngit/__init__.py @@ -0,0 +1,21 @@ +from shared.torngit.bitbucket import Bitbucket +from shared.torngit.bitbucket_server import BitbucketServer +from shared.torngit.github import Github +from shared.torngit.github_enterprise import GithubEnterprise +from shared.torngit.gitlab import Gitlab +from shared.torngit.gitlab_enterprise import GitlabEnterprise + + +def get(git, **data): + if git == "github": + return Github(**data) + elif git == "github_enterprise": + return GithubEnterprise(**data) + elif git == "bitbucket": + return Bitbucket(**data) + elif git == "bitbucket_server": + return BitbucketServer(**data) + elif git == "gitlab": + return Gitlab(**data) + elif git == "gitlab_enterprise": + return GitlabEnterprise(**data) diff --git a/libs/shared/shared/torngit/base.py b/libs/shared/shared/torngit/base.py new file mode 100644 index 0000000000..f8749b2506 --- /dev/null +++ b/libs/shared/shared/torngit/base.py @@ -0,0 +1,435 @@ +import re +from enum import Enum +from typing import Dict, List, Optional, Tuple + +import httpx + +from shared.django_apps.core.models import Repository +from shared.torngit.enums import Endpoints +from shared.torngit.response_types import ProviderPull +from shared.typings.oauth_token_types import ( + OauthConsumerToken, + OnRefreshCallback, + Token, +) +from shared.typings.torngit import TorngitInstanceData + +get_start_of_line = re.compile(r"@@ \-(\d+),?(\d*) \+(\d+),?(\d*).*").match + + +class TokenType(Enum): + read = "read" + admin = "admin" + comment = "comment" + status = "status" + tokenless = "tokenless" + commit = "commit" + pull = "pull" + + +TokenTypeMapping = Dict[TokenType, Token] + + +class TorngitBaseAdapter(object): + _repo_url: str | None = None + _aws_key = None + _oauth: OauthConsumerToken | None = None + _on_token_refresh: OnRefreshCallback = None + _token: Token | None = None + verify_ssl = None + + valid_languages = set(language.value for language in Repository.Languages) + + def __init__( + self, + oauth_consumer_token: OauthConsumerToken | None = None, + timeouts=None, + token: Token | None = None, + token_type_mapping: TokenTypeMapping | None = None, + on_token_refresh: OnRefreshCallback = None, + verify_ssl=None, + **kwargs, + ): + self._timeouts = timeouts or [10, 30] + self._token = token + self._on_token_refresh = on_token_refresh + self._token_type_mapping = token_type_mapping or {} + self._oauth = oauth_consumer_token + self.data: TorngitInstanceData = { + "owner": {}, + "repo": {}, + "fallback_installations": None, + "installation": None, + "additional_data": {}, + } + self.verify_ssl = verify_ssl + self.data.update(kwargs) + + def __repr__(self): + return "<%s slug=%s ownerid=%s repoid=%s>" % ( + self.service, + self.slug, + self.data["owner"].get("ownerid"), + self.data["repo"].get("repoid"), + ) + + def get_client(self, timeouts: List[int] = []) -> httpx.AsyncClient: + if timeouts: + timeout = httpx.Timeout(timeouts[1], connect=timeouts[0]) + else: + timeout = httpx.Timeout(self._timeouts[1], connect=self._timeouts[0]) + return httpx.AsyncClient( + verify=( + self.verify_ssl + if not isinstance(self.verify_ssl, bool) + else self.verify_ssl + ), + timeout=timeout, + ) + + def get_token_by_type(self, token_type: TokenType): + if self._token_type_mapping.get(token_type) is not None: + return self._token_type_mapping.get(token_type) + return self.token + + def get_token_by_type_if_none(self, token: Optional[str], token_type: TokenType): + if token is not None: + return token + return self.get_token_by_type(token_type) + + def _oauth_consumer_token(self) -> OauthConsumerToken: + if not self._oauth: + raise Exception("Oauth consumer token not present") + return self._oauth + + def _validate_language(self, language: str) -> str | None: + if language: + language = language.lower() + if language in self.valid_languages: + return language + return None + + def set_token(self, token: OauthConsumerToken) -> None: + self._token = token + + @property + def token(self) -> Token: + if not self._token: + self._token = self._oauth_consumer_token() + return self._token + + @property + def slug(self) -> str | None: + if self.data.get("owner") and self.data.get("repo"): + if self.data["owner"].get("username") and self.data["repo"].get("name"): + return "%s/%s" % ( + self.data["owner"]["username"], + self.data["repo"]["name"], + ) + return None + + def build_tree_from_commits(self, start, commit_mapping): + parents = [ + self.build_tree_from_commits(p, commit_mapping) + for p in commit_mapping.get(start, []) + ] + return {"commitid": start, "parents": parents} + + def diff_to_json(self, diff): + """ + Processes a full diff (multiple files) into the object pattern below + docs/specs/diff.json + """ + results = {} + diff = ("\n%s" % diff).split("\ndiff --git a/") + segment = None + for _diff in diff[1:]: + _diff = _diff.replace("\r\n", "\n").split("\n") + if _diff[-1] == "": + # if the diff ends in a '\n' character then we'll have an extra + # empty line at the end that we don't want + _diff.pop() + + try: + before, after = _diff.pop(0).split(" b/", 1) + except IndexError: + before, after = None, None + # find the --- a + for source in _diff: + if source.startswith("--- a/"): + before = source[6:] + elif source.startswith("+++ b/"): + after = source[6:] + break + + if after is None: + continue + + # Is the file empty, skipped, etc + # ------------------------------- + _file = dict( + type="new" if before == "/dev/null" else "modified", + before=None if before == after or before == "/dev/null" else before, + segments=[], + ) + + results[after] = _file + + # Get coverage data on each line + # ------------------------------ + # make file, this is ONE file not multiple + for source in _diff: + if re.match(r"\\ No newline at end of file", source): + continue + + sol4 = source[:4] + if sol4 == "dele": + # deleted file mode 100644 + _file["before"] = after + _file["type"] = "deleted" + _file.pop("segments") + break + + elif sol4 == "new " and not source.startswith("new mode "): + _file["type"] = "new" + + elif sol4 == "Bina": + _file["type"] = "binary" + _file.pop("before") + _file.pop("segments") + break + + elif sol4 in ("--- ", "+++ ", "inde", "diff", "old ", "new "): + # diff --git a/app/commit.py b/app/commit.py + # new file mode 100644 + # index 0000000..d5ee3d6 + # --- /dev/null + # +++ b/app/commit.py + continue + + elif sol4 == "@@ -": + # ex: "@@ -31,8 +31,8 @@ blah blah blah" + # ex: "@@ -0,0 +1 @@" + ln = get_start_of_line(source).groups() + segment = dict(header=[ln[0], ln[1], ln[2], ln[3]], lines=[]) + _file["segments"].append(segment) + + elif source == "": + continue + + elif segment: + # actual lines + segment["lines"].append(source) + + # else: + # results.pop(fname) + # break + + if results: + return dict(files=self._add_diff_totals(results)) + + def _add_diff_totals(self, diff): + for data in diff.values(): + rm = 0 + add = 0 + if "segments" in data: + for segment in data["segments"]: + rm += sum([1 for line in segment["lines"] if line[0] == "-"]) + add += sum([1 for line in segment["lines"] if line[0] == "+"]) + data["stats"] = dict(added=add, removed=rm) + return diff + + # COMMENT LOGIC + + async def delete_comment( + self, pullid: str, commentid: str, token: str | None = None + ) -> bool: + """Deletes a comment on a PR from the provider + + Args: + pullid (str): The pull request identifier. If not str, will be stingified on the + formatting of url + commentid (str): The commend identifier + token (str, optional): An optional token that can be used instead of the client default + + Raises: + NotImplementedError: If the adapter does not have this ability implemented + exceptions.ObjectNotFoundException: If this comment could not be found + exceptions.TorngitClientError: If any other HTTP error occurs + """ + raise NotImplementedError() + + async def post_comment(self, pullid: str, body: str, token=None) -> dict: + raise NotImplementedError() + + async def edit_comment( + self, pullid: str, commentid: str, body: str, token=None + ) -> dict: + raise NotImplementedError() + + # PULL REQUEST LOGIC + + async def find_pull_request( + self, commit=None, branch=None, state="open", token=None + ): + raise NotImplementedError() + + async def get_pull_request(self, pullid: str, token=None) -> ProviderPull | None: + raise NotImplementedError() + + async def get_pull_request_commits(self, pullid: str, token=None): + raise NotImplementedError() + + async def get_pull_requests(self, state="open", token=None): + raise NotImplementedError() + + async def get_pull_request_files(self, pullid: str, token=None): + raise NotImplementedError() + + # COMMIT LOGIC + + async def get_commit(self, commit: str, token=None): + raise NotImplementedError() + + async def get_commit_diff(self, commit: str, context=None, token=None): + raise NotImplementedError() + + async def get_commit_statuses(self, commit: str, _merge=None, token=None): + raise NotImplementedError() + + async def set_commit_status( + self, + commit: str, + status, + context, + description, + url, + coverage=None, + merge_commit=None, + token=None, + ): + raise NotImplementedError() + + # WEBHOOK LOGIC + + async def post_webhook(self, name, url, events: dict, secret, token=None) -> dict: + raise NotImplementedError() + + async def delete_webhook(self, hookid: str, token=None): + raise NotImplementedError() + + async def edit_webhook( + self, hookid: str, name, url, events: dict, secret, token=None + ) -> dict: + raise NotImplementedError() + + # OTHERS + + async def get_authenticated(self, token=None) -> Tuple[bool, bool]: + """Finds the user permissions about about whether the user on + `self.data["user"]` can access the repo from `self.data["repo"]` + Returns a `can_view` and a `can_edit` permission tuple + + IMPORTANT NOTE: As it is right now, this function will never return can_view=False + It is either can_view=True, or raise 404 because from the user perspective, that + repo does not exist. + + This kind of makes the first value of the result a bit useless + + Args: + token (None, optional): Description + + Returns:` + Tuple[bool, bool]: A tuple telling: + + can_view, can_edit + + """ + raise NotImplementedError() + + async def get_authenticated_user(self, **kwargs): + raise NotImplementedError() + + async def get_branches(self, token=None): + raise NotImplementedError() + + async def get_branch(self, token=None): + raise NotImplementedError() + + async def get_compare( + self, base, head, context=None, with_commits=True, token=None + ): + raise NotImplementedError() + + async def get_distance_in_commits( + self, base_branch, base, context=None, with_commits=True, token=None + ): + return { + "behind_by": None, + "behind_by_commit": None, + "status": None, + "ahead_by": None, + } + + async def get_is_admin(self, user: dict, token=None) -> bool: + """Tells whether `user` is an admin of the organization described on `self.data` + + Args: + user (dict): Description + token (None, optional): Description + """ + raise NotImplementedError() + + async def get_repository(self, token=None): + raise NotImplementedError() + + async def get_repo_languages(self, token=None): + raise NotImplementedError() + + async def get_source(self, path, ref, token=None): + raise NotImplementedError() + + async def list_repos(self, username=None, token=None): + raise NotImplementedError() + + async def list_teams(self, token=None): + raise NotImplementedError() + + def get_external_endpoint(self, endpoint: Endpoints, **kwargs): + raise NotImplementedError() + + async def list_files(self, ref: str, dir_path: str, token=None): + raise NotImplementedError() + + def get_href(self, endpoint: Endpoints, **kwargs): + path = self.get_external_endpoint(endpoint, **kwargs) + return f"{self.service_url}/{path}" + + async def list_top_level_files(self, ref, token=None): + """List the files on the top level of the repository + + Returns: + list[dict] - A list of dicts, one for each file/directory on the top + level of the repo. While different implementations might + return a different set of values on each dict, the only keys you + can safely expect from each dict are: + + - `path` - The path of the structure + - `type` - The type: can be "folder" or "file" or "other" + """ + raise NotImplementedError() + + async def get_workflow_run(self, run_id, token=None): + raise NotImplementedError() + + async def get_best_effort_branches(self, commit_sha: str, token=None) -> List[str]: + """ + Gets a 'best effort' list of branches this commit is in. + If a branch is returned, this means this commit is in that branch. If not, it could still be + possible that this commit is in that branch + Args: + commit_sha (str): The sha of the commit we want to look at + Returns: + List[str]: A list of branch names + """ + raise NotImplementedError() diff --git a/libs/shared/shared/torngit/bitbucket.py b/libs/shared/shared/torngit/bitbucket.py new file mode 100644 index 0000000000..5e939c06c7 --- /dev/null +++ b/libs/shared/shared/torngit/bitbucket.py @@ -0,0 +1,1084 @@ +import logging +import os +import urllib.parse as urllib_parse +from typing import List + +import httpx +from oauthlib import oauth1 + +from shared.torngit.base import TokenType, TorngitBaseAdapter +from shared.torngit.enums import Endpoints +from shared.torngit.exceptions import ( + TorngitClientError, + TorngitClientGeneralError, + TorngitObjectNotFoundError, + TorngitServer5xxCodeError, + TorngitServerUnreachableError, +) +from shared.torngit.response_types import ProviderPull +from shared.torngit.status import Status +from shared.utils.urls import url_concat + +log = logging.getLogger(__name__) + +METRICS_PREFIX = "services.torngit.bitbucket" + + +class Bitbucket(TorngitBaseAdapter): + _OAUTH_REQUEST_TOKEN_URL = "https://bitbucket.org/api/1.0/oauth/request_token" + _OAUTH_ACCESS_TOKEN_URL = "https://bitbucket.org/api/1.0/oauth/access_token" + _OAUTH_AUTHORIZE_URL = "https://bitbucket.org/api/1.0/oauth/authenticate" + service = "bitbucket" + api_url = "https://bitbucket.org" + service_url = "https://bitbucket.org" + urls = dict( + repo="{username}/{name}", + owner="{username}", + user="{username}", + issues="{username}/{name}/issues/{issueid}", + commit="{username}/{name}/commits/{commitid}", + commits="{username}/{name}/commits", + src="{username}/{name}/src/{commitid}/{path}", + create_file="{username}/{name}/create-file/{commitid}?at={branch}&filename={path}&content={content}", + tree="{username}/{name}/src/{commitid}", + branch="{username}/{name}/branch/{branch}", + pull="{username}/{name}/pull-requests/{pullid}", + compare="{username}/{name}", + ) + + async def api( + self, client, version, method, path, json=False, body=None, token=None, **kwargs + ): + url = "https://bitbucket.org/api/%s.0%s" % (version, path) + headers = { + "Accept": "application/json", + "User-Agent": os.getenv("USER_AGENT", "Default"), + } + + oauth_body = None + url = url_concat(url, kwargs) + + if json: + headers["Content-Type"] = "application/json" + elif body is not None: + headers["Content-Type"] = "application/x-www-form-urlencoded" + oauth_body = body + + token_to_use = token or self.token + oauth_client = oauth1.Client( + self._oauth_consumer_token()["key"], + client_secret=self._oauth_consumer_token()["secret"], + resource_owner_key=token_to_use["key"], + resource_owner_secret=token_to_use["secret"], + signature_type=oauth1.SIGNATURE_TYPE_QUERY, + ) + url, headers, oauth_body = oauth_client.sign( + url, http_method=method, body=oauth_body, headers=headers + ) + + kwargs = dict( + json=body if body is not None and json else None, + data=oauth_body if not json else None, + headers=headers, + ) + log_dict = dict( + event="api", + endpoint=path, + method=method, + bot=token_to_use.get("username"), + repo_slug=self.slug, + ) + try: + res = await client.request(method.upper(), url, **kwargs) + logged_body = None + if res.status_code >= 300 and res.text is not None: + logged_body = res.text + log.log( + logging.WARNING if res.status_code >= 300 else logging.INFO, + "Bitbucket HTTP %s", + res.status_code, + extra=dict(body=logged_body, **log_dict), + ) + except (httpx.NetworkError, httpx.TimeoutException): + raise TorngitServerUnreachableError("Bitbucket was not able to be reached.") + if res.status_code == 599: + raise TorngitServerUnreachableError( + "Bitbucket was not able to be reached, server timed out." + ) + elif res.status_code >= 500: + raise TorngitServer5xxCodeError("Bitbucket is having 5xx issues") + elif res.status_code >= 300: + message = f"Bitbucket API: {res.reason_phrase}" + raise TorngitClientGeneralError( + res.status_code, response_data={"content": res.content}, message=message + ) + if res.status_code == 204: + return None + elif "application/json" in res.headers.get("Content-Type"): + return res.json() + else: + return res.text + + def generate_request_token(self, redirect_url): + client = oauth1.Client( + self._oauth["key"], + client_secret=self._oauth["secret"], + callback_uri=redirect_url, + ) + uri, headers, body = client.sign(self._OAUTH_REQUEST_TOKEN_URL) + r = httpx.get(uri, headers=headers) + oauth_token = urllib_parse.parse_qs(r.text)["oauth_token"][0] + oauth_token_secret = urllib_parse.parse_qs(r.text)["oauth_token_secret"][0] + return dict(oauth_token=oauth_token, oauth_token_secret=oauth_token_secret) + + def generate_access_token( + self, resource_owner_key, resource_owner_secret, verifier + ): + client = oauth1.Client( + self._oauth["key"], + client_secret=self._oauth["secret"], + resource_owner_key=resource_owner_key, + resource_owner_secret=resource_owner_secret, + verifier=verifier, + ) + uri, headers, body = client.sign(self._OAUTH_ACCESS_TOKEN_URL) + r = httpx.get(uri, headers=headers) + resp_args = urllib_parse.parse_qs(r.text) + return { + "key": resp_args["oauth_token"][0], + "secret": resp_args["oauth_token_secret"][0], + } + + async def get_authenticated_user(self): + async with self.get_client() as client: + return await self.api(client, "2", "get", "/user") + + async def post_webhook(self, name, url, events, secret, token=None): + # https://confluence.atlassian.com/bitbucket/webhooks-resource-735642279.html + # https://confluence.atlassian.com/bitbucket/event-payloads-740262817.html + async with self.get_client() as client: + res = await self.api( + client, + "2", + "post", + "/repositories/%s/hooks" % self.slug, + body=dict(description=name, active=True, events=events, url=url), + json=True, + token=token, + ) + res["id"] = res["uuid"][1:-1] + return res + + async def edit_webhook(self, hookid, name, url, events, secret, token=None): + # https://confluence.atlassian.com/bitbucket/webhooks-resource-735642279.html#webhooksResource-PUTawebhookupdate + async with self.get_client() as client: + res = await self.api( + client, + "2", + "put", + "/repositories/%s/hooks/%s" % (self.slug, hookid), + body=dict(description=name, active=True, events=events, url=url), + json=True, + token=token, + ) + res["id"] = res["uuid"][1:-1] + return res + + async def delete_webhook(self, hookid, token=None): + # https://confluence.atlassian.com/bitbucket/webhooks-resource-735642279.html#webhooksResource-DELETEthewebhook + async with self.get_client() as client: + try: + await self.api( + client, + "2", + "delete", + "/repositories/%s/hooks/%s" % (self.slug, hookid), + token=token, + ) + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Webhook with id {hookid} does not exist", + ) + raise + return True + + async def get_is_admin(self, user, token=None): + user_uuid = "{" + user["service_id"] + "}" + workspace_uuid = "{" + self.data["owner"]["service_id"] + "}" + async with self.get_client() as client: + groups = await self.api( + client, "2", "get", "/user/permissions/workspaces", token=token + ) + if groups["values"]: + for group in groups["values"]: + if ( + group["permission"] == "owner" + and group["workspace"]["uuid"] == workspace_uuid + and group["user"]["uuid"] == user_uuid + ): + return True + return False + + async def list_teams(self, token=None): + teams, page = [], None + async with self.get_client() as client: + while True: + if page is not None: + kwargs = dict(page=page, token=token) + else: + kwargs = dict(token=token) + res = await self.api( + client, "2", "get", "/user/permissions/workspaces", **kwargs + ) + for groups in res["values"]: + team = groups["workspace"] + teams.append( + dict( + name=team["name"], + id=team["uuid"][1:-1], + email=None, + username=team["slug"], + ) + ) + + if not res.get("next"): + break + url = res["next"] + parsed = urllib_parse.urlparse(url) + page = urllib_parse.parse_qs(parsed.query)["page"][0] + + return teams + + async def get_pull_request_commits(self, pullid, token=None): + commits, page = [], None + async with self.get_client() as client: + while True: + # https://confluence.atlassian.com/bitbucket/pullrequests-resource-423626332.html#pullrequestsResource-GETthecommitsforapullrequest + if page is not None: + kwargs = dict(page=page, token=token) + else: + kwargs = dict(token=token) + res = await self.api( + client, + "2", + "get", + "/repositories/%s/pullrequests/%s/commits" % (self.slug, pullid), + **kwargs, + ) + commits.extend([c["hash"] for c in res["values"]]) + if not res.get("next"): + break + url = res["next"] + parsed = urllib_parse.urlparse(url) + page = urllib_parse.parse_qs(parsed.query)["page"][0] + return commits + + async def _get_teams_and_username_to_list(self, username=None, token=None): + # if username is not provided, list all repos + repos_to_log = [] + if username is None: + # get all teams a user is member of + teams = await self.list_teams(token) + usernames = set([team["username"] for team in teams]) + # get permission of all repositories a user is member of + permissions = await self.list_permissions(token=token) + # get repo owners + for permission in permissions: + repo = permission["repository"] + repos_to_log.append(repo["full_name"]) + name = repo["full_name"].split("/") + if repo.get("owner") and repo.get("owner").get("username") != name: + log.warning( + "Owner username different from what we think it is", + extra=dict(repo_dict=repo, found_name=name), + ) + usernames.add(name[0]) + # add user's own username + usernames.add(self.data["owner"]["username"]) + else: + usernames = [username] + + return (usernames, repos_to_log) + + async def _fetch_page_of_repos(self, client, username, token, page): + # https://confluence.atlassian.com/display/BITBUCKET/repositories+Endpoint#repositoriesEndpoint-GETalistofrepositoriesforanaccount + res = await self.api( + client, + "2", + "get", + f"/repositories/{username}", + page=page, + token=token, + ) + + repos = [] + for repo in res.get("values", []): + repo_name_arr = repo["full_name"].split("/", 1) + + repos.append( + dict( + owner=dict( + service_id=repo["owner"]["uuid"][1:-1], + username=repo_name_arr[0], + ), + repo=dict( + service_id=repo["uuid"][1:-1], + name=repo_name_arr[1], + language=self._validate_language(repo["language"]), + private=repo["is_private"], + branch="main", + ), + ) + ) + return (repos, res.get("next")) + + async def list_repos(self, username=None, token=None): + """ + Lists all repositories a user is part of. + *Note: + Bitbucket API V2 does not provide a dedicated endpoint which returns all repos a user is part of. + It provides however, an endpoint to get all the repos a user is part of from an specific org or user. + Endpoint to list repos from an specific user: + - /repositories/{username} + In order to get all the repositories a user is part of, we first need to get all the orgs and repo owners + - Orgs/Teams can be obtained using the 'list_teams' method + - Usernames of repo owners is a bit tricky since Bitbucket doesnt provide an endpoint for this + - Solution: + Use the 'list_permissions' method to get all repo permissions and exctract owner's username + from the repository 'full_name' attribute + Once we have all orgs/teams and owner's usernames we should call "/repositories/{username}" endpoint + for each of the orgs/teams and owner's usernames. + """ + data, page = [], 0 + usernames, repos_to_log = await self._get_teams_and_username_to_list( + username, token + ) + # fetch repo information + log.info( + "Bitbucket: fetching repos from teams", + extra=dict(usernames=usernames, repos=repos_to_log), + ) + async with self.get_client() as client: + for team in usernames: + page = 0 + try: + while True: + page += 1 + repos, has_next = await self._fetch_page_of_repos( + client, team, token, page + ) + + data.extend(repos) + + if len(repos) == 0 or not has_next: + break + except TorngitClientError: + log.warning( + "Unable to fetch repos from team on Bitbucket", + extra=dict(team_name=team, repository_names=repos_to_log), + ) + log.info( + "Bitbucket: finished fetching repos", + extra=dict(usernames=usernames, repos=data), + ) + return data + + async def list_repos_generator(self, username=None, token=None): + """ + New version of list_repos() that should replace the old one after safely + rolling out in the worker. + """ + usernames, repos_to_log = await self._get_teams_and_username_to_list( + username, token + ) + + # fetch repo information + log.info( + "Bitbucket: fetching repos from teams", + extra=dict(usernames=usernames, repos=repos_to_log), + ) + async with self.get_client() as client: + for team in usernames: + page = 0 + try: + while True: + page += 1 + repos, has_next = await self._fetch_page_of_repos( + client, team, token, page + ) + + yield repos + + if len(repos) == 0 or not has_next: + break + except TorngitClientError: + log.warning( + "Unable to fetch repos from team on Bitbucket", + extra=dict(team_name=team, repository_names=repos_to_log), + ) + log.info( + "Bitbucket: finished fetching repos", + extra=dict(usernames=usernames), + ) + + async def list_permissions(self, token=None): + data, page = [], 0 + async with self.get_client() as client: + while True: + page += 1 + res = await self.api( + client, + "2", + "get", + "/user/permissions/repositories", + page=page, + token=token, + ) + if not res["values"]: + page = 0 + else: + data.extend(res["values"]) + if not res.get("next"): + page = 0 + break + return data + + async def get_pull_request(self, pullid, token=None) -> ProviderPull | None: + # https://confluence.atlassian.com/display/BITBUCKET/pullrequests+Resource#pullrequestsResource-GETaspecificpullrequest + async with self.get_client() as client: + try: + res = await self.api( + client, + "2", + "get", + "/repositories/{}/pullrequests/{}".format(self.slug, pullid), + token=token, + ) + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"PR with id {pullid} does not exist", + ) + raise + # the commit sha is only {12}. need to get full sha + base = await self.api( + client, + "2", + "get", + "/repositories/{}/commit/{}".format( + self.slug, res["destination"]["commit"]["hash"] + ), + token=token, + ) + head = await self.api( + client, + "2", + "get", + "/repositories/{}/commit/{}".format( + self.slug, res["source"]["commit"]["hash"] + ), + token=token, + ) + return ProviderPull( + author=dict( + id=str(res["author"]["uuid"][1:-1]) if res["author"] else None, + username=( + (res["author"].get("nickname") or res["author"].get("username")) + if res["author"] + else None + ), + ), + base=dict( + branch=res["destination"]["branch"]["name"], commitid=base["hash"] + ), + head=dict(branch=res["source"]["branch"]["name"], commitid=head["hash"]), + state={"OPEN": "open", "MERGED": "merged", "DECLINED": "closed"}.get( + res["state"] + ), + title=res["title"], + id=str(pullid), + number=str(pullid), + merge_commit_sha=( + res.get("merge_commit", dict()).get("hash") + if res["state"] == "MERGED" + else None + ), + ) + + async def post_comment(self, issueid, body, token=None): + # https://confluence.atlassian.com/display/BITBUCKET/issues+Resource#issuesResource-POSTanewcommentontheissue + async with self.get_client() as client: + res = await self.api( + client, + "2", + "post", + "/repositories/%s/pullrequests/%s/comments" % (self.slug, issueid), + body=dict(content=dict(raw=body)), + json=True, + token=token, + ) + return res + + async def edit_comment(self, issueid, commentid, body, token=None): + # https://confluence.atlassian.com/display/BITBUCKET/pullrequests+Resource+1.0#pullrequestsResource1.0-PUTanupdateonacomment + # await self.api('1', 'put', '/repositories/%s/pullrequests/%s/comments/%s' % (self.slug, issueid, commentid), + # body=dict(content=body), token=token) + async with self.get_client() as client: + try: + res = await self.api( + client, + "2", + "put", + f"/repositories/{self.slug}/pullrequests/{issueid}/comments/{commentid}", + body=dict(content=dict(raw=body)), + json=True, + token=token, + ) + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Comment {commentid} from PR {issueid} cannot be found", + ) + raise + return res + + async def delete_comment(self, issueid, commentid, token=None): + # https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Busername%7D/%7Brepo_slug%7D/pullrequests/%7Bpull_request_id%7D/comments/%7Bcomment_id%7D + async with self.get_client() as client: + try: + await self.api( + client, + "2", + "delete", + "/repositories/%s/pullrequests/%s/comments/%s" + % (self.slug, issueid, commentid), + token=token, + ) + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Comment {commentid} from PR {issueid} cannot be found", + ) + raise + return True + + async def get_commit_status(self, commit, token=None): + # https://confluence.atlassian.com/bitbucket/buildstatus-resource-779295267.html + statuses = await self.get_commit_statuses(commit, _in_loop=True, token=token) + return str(statuses) + + async def get_commit_statuses(self, commit, token=None, _in_loop=None): + statuses, page = [], 0 + status_keys = dict(INPROGRESS="pending", SUCCESSFUL="success", FAILED="failure") + async with self.get_client() as client: + while True: + page += 1 + # https://api.bitbucket.org/2.0/repositories/atlassian/aui/commit/d62ae57/statuses + res = await self.api( + client, + "2", + "get", + "/repositories/%s/commit/%s/statuses" % (self.slug, commit), + page=page, + token=token, + ) + _statuses = res["values"] + if len(_statuses) == 0: + break + statuses.extend( + [ + { + "time": s["updated_on"], + "state": status_keys.get(s["state"]), + "description": s["description"], + "url": s["url"], + "context": s["key"], + } + for s in _statuses + ] + ) + if not res.get("next"): + break + return Status(statuses) + + async def set_commit_status( + self, + commit, + status, + context, + description, + url, + merge_commit=None, + token=None, + coverage=None, + ): + token = self.get_token_by_type_if_none(token, TokenType.status) + # https://confluence.atlassian.com/bitbucket/buildstatus-resource-779295267.html + status = dict( + pending="INPROGRESS", success="SUCCESSFUL", error="FAILED", failure="FAILED" + ).get(status) + assert status, "status not valid" + async with self.get_client() as client: + try: + res = await self.api( + client, + "2", + "post", + "/repositories/%s/commit/%s/statuses/build" % (self.slug, commit), + body=dict( + state=status, + key="codecov-" + context, + name=context.replace("/", " ").capitalize() + " Coverage", + url=url, + description=description, + ), + token=token, + ) + except Exception: + res = await self.api( + client, + "2", + "put", + "/repositories/%s/commit/%s/statuses/build/codecov-%s" + % (self.slug, commit, context), + body=dict( + state=status, + name=context.replace("/", " ").capitalize() + " Coverage", + url=url, + description=description, + ), + token=token, + ) + + if merge_commit: + try: + res = await self.api( + client, + "2", + "post", + "/repositories/%s/commit/%s/statuses/build" + % (self.slug, merge_commit[0]), + body=dict( + state=status, + key="codecov-" + merge_commit[1], + name=merge_commit[1].replace("/", " ").capitalize() + + " Coverage", + url=url, + description=description, + ), + token=token, + ) + except Exception: + res = await self.api( + client, + "2", + "put", + "/repositories/%s/commit/%s/statuses/build/codecov-%s" + % (self.slug, merge_commit[0], context), + body=dict( + state=status, + name=merge_commit[1].replace("/", " ").capitalize() + + " Coverage", + url=url, + description=description, + ), + token=token, + ) + # check if the commit is a Merge + return res + + async def get_commit(self, commit, token=None): + # https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Bworkspace%7D/%7Brepo_slug%7D/commit/%7Bnode%7D + async with self.get_client() as client: + try: + data = await self.api( + client, + "2", + "get", + "/repositories/%s/commit/%s" % (self.slug, commit), + token=token, + ) + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Commit {commit} cannot be found", + ) + raise + username = data["author"].get("user", {}).get("nickname") + author_raw = ( + data["author"].get("raw", "")[:-1].rsplit(" <", 1) + if " <" in data["author"].get("raw", "") + else None + ) + + userid = data["author"].get("user", {}).get("uuid", "")[1:-1] or None + + # We used to look up the userid from the username if no uuid provided but + # BitBucket has deprecated the '/users/{username}' endpoint for privacy reasons so we + # have to use '/users/{account_id}' instead + # https://developer.atlassian.com/bitbucket/api/2/reference/resource/users/%7Busername%7D + account_id = data["author"].get("user", {}).get("account_id") + if not userid and account_id: + res = await self.api( + client, "2", "get", "/users/%s" % account_id, token=token + ) + userid = res["uuid"][1:-1] + + return dict( + author=dict( + id=userid, + username=username, + name=author_raw[0] if author_raw else None, + email=author_raw[1] if author_raw else None, + ), + commitid=commit, + parents=[p["hash"] for p in data["parents"]], + message=data["message"], + timestamp=data["date"], + ) + + async def get_branches(self, token=None): + # https://confluence.atlassian.com/display/BITBUCKET/repository+Resource+1.0#repositoryResource1.0-GETlistofbranches + async with self.get_client() as client: + res = await self.api( + client, + "2", + "get", + "/repositories/%s/refs/branches" % self.slug, + token=token, + pagelen="100", + ) + return [(k["name"], k["target"]["hash"]) for k in res["values"]] + + async def get_branch(self, name, token=None): + async with self.get_client() as client: + res = await self.api( + client, + "2", + "get", + "/repositories/%s/refs/branches/%s" % (self.slug, name), + token=token, + ) + return { + "name": res["name"], + "sha": res["target"]["hash"], + } + + async def get_pull_requests(self, state="open", token=None): + state = {"open": "OPEN", "merged": "MERGED", "close": "DECLINED"}.get(state) + pulls, page = [], 0 + async with self.get_client() as client: + while True: + page += 1 + # https://confluence.atlassian.com/display/BITBUCKET/pullrequests+Resource#pullrequestsResource-GETalistofopenpullrequests + res = await self.api( + client, + "2", + "get", + "/repositories/%s/pullrequests" % self.slug, + state=state, + page=page, + token=token, + ) + if len(res["values"]) == 0: + break + pulls.extend([pull["id"] for pull in res["values"]]) + if not res.get("next"): + break + return pulls + + async def find_pull_request( + self, commit=None, branch=None, state="open", token=None + ): + state = {"open": "OPEN", "merged": "MERGED", "close": "DECLINED"}.get(state, "") + page = 0 + async with self.get_client() as client: + if commit or branch: + while True: + page += 1 + # https://confluence.atlassian.com/display/BITBUCKET/pullrequests+Resource#pullrequestsResource-GETalistofopenpullrequests + res = await self.api( + client, + "2", + "get", + "/repositories/%s/pullrequests" % self.slug, + state=state, + page=page, + token=token, + ) + _prs = res["values"] + if len(_prs) == 0: + break + + if commit: + for pull in _prs: + if commit.startswith(pull["source"]["commit"]["hash"]): + return str(pull["id"]) + else: + for pull in _prs: + if pull["source"]["branch"]["name"] == branch: + return str(pull["id"]) + + if not res.get("next"): + break + + async def get_pull_request_files(self, pullid, token=None): + # https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/#api-repositories-workspace-repo-slug-pullrequests-pull-request-id-diffstat-get + async with self.get_client() as client: + try: + res = await self.api( + client, + "2", + "get", + "/repositories/{}/pullrequests/{}/diffstat".format( + self.slug, pullid + ), + token=token, + ) + filenames = [data["new"]["path"] for data in res.get("values")] + return filenames + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"PR with id {pullid} does not exist", + ) + raise + + async def get_repository(self, token=None): + async with self.get_client() as client: + if self.data["repo"].get("service_id") is None: + # https://confluence.atlassian.com/display/BITBUCKET/repository+Resource#repositoryResource-GETarepository + res = await self.api( + client, "2", "get", "/repositories/" + self.slug, token=token + ) + else: + res = await self.api( + client, + "2", + "get", + "/repositories/%%7B%s%%7D/%%7B%s%%7D" + % ( + self.data["owner"]["service_id"], + self.data["repo"]["service_id"], + ), + token=token, + ) + username, repo = tuple(res["full_name"].split("/", 1)) + return dict( + owner=dict(service_id=res["owner"]["uuid"][1:-1], username=username), + repo=dict( + service_id=res["uuid"][1:-1], + private=res["is_private"], + branch="main", + language=self._validate_language(res["language"]), + name=repo, + ), + ) + + async def get_repo_languages( + self, token=None, language: str | None = None + ) -> list[str]: + """ + Gets the languages belonging to this repository. Bitbucket has no way to + track languages, so we'll return a list with the existing language + Param: + language: the language belonging to the repository.language key + Returns: + List[str]: A list of language names + """ + languages = [] + + if language: + languages.append(language.lower()) + + return languages + + async def get_authenticated(self, token=None): + async with self.get_client() as client: + if self.data["repo"].get("private"): + # https://confluence.atlassian.com/bitbucket/repository-resource-423626331.html#repositoryResource-GETarepository + await self.api( + client, "2", "get", "/repositories/" + self.slug, token=token + ) + response = await self.api( + client, + "2", + "get", + "/user/permissions/repositories", + token=token, + q=f'repository.full_name="{self.slug}"', + ) + repo_permissions = response["values"] or [] + can_edit = any( + perm["permission"] in ("admin", "write") + for perm in repo_permissions + ) + if not can_edit: + # Temporary log to track this down more easily + # If you see this, just remove it + log.info("New logic is disallowing customer from editing Bitbucket") + return (True, can_edit) + else: + # https://developer.atlassian.com/bitbucket/api/2/reference/resource/user/permissions/repositories + groups = await self.api( + client, + "2", + "get", + "/user/permissions/repositories", + token=token, + q=f'repository.full_name="{self.slug}" AND (permission="admin" OR permission="write")', + ) + if groups["values"]: + for group in groups["values"]: + assert group["permission"] in ("admin", "write") + return (True, True) + return (True, False) + + async def get_source(self, path, ref, token=None): + # https://confluence.atlassian.com/bitbucket/src-resources-296095214.html + async with self.get_client() as client: + try: + src = await self.api( + client, + "2", + "get", + "/repositories/{0}/src/{1}/{2}".format( + self.slug, ref, path.replace(" ", "%20") + ), + token=token, + ) + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Path {path} not found at {ref}", + ) + raise + return dict(commitid=None, content=src.encode()) + + async def get_compare( + self, base, head, context=None, with_commits=True, token=None + ): + # https://developer.atlassian.com/bitbucket/api/2/reference/resource/snippets/%7Busername%7D/%7Bencoded_id%7D/%7Brevision%7D/diff%C2%A0%E2%80%A6 + # https://api.bitbucket.org/2.0/repositories/markadams-atl/test-repo/diff/1b03803..fcba34b + # IMPORANT it is reversed + async with self.get_client() as client: + diff = await self.api( + client, + "2", + "get", + "/repositories/%s/diff/%s..%s" % (self.slug, head, base), + context=context or 1, + token=token, + ) + + commits = [] + if with_commits: + commits = [{"commitid": head}, {"commitid": base}] + # No endpoint to get commits yet... ugh + + return dict(diff=self.diff_to_json(diff), commits=commits) + + async def get_commit_diff(self, commit, context=None, token=None): + # https://confluence.atlassian.com/bitbucket/diff-resource-425462484.html + async with self.get_client() as client: + diff = await self.api( + client, + "2", + "get", + "/repositories/" + + self.data["owner"]["username"] + + "/" + + self.data["repo"]["name"] + + "/diff/" + + commit, + token=token, + ) + return self.diff_to_json(diff) + + async def list_top_level_files(self, ref, token=None): + return await self.list_files(ref, dir_path="", token=None) + + async def list_files(self, ref, dir_path, token=None): + page = None + has_more = True + files = [] + async with self.get_client() as client: + while has_more: + # https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Busername%7D/%7Brepo_slug%7D/src#get + if page is not None: + kwargs = dict(page=page, token=token) + else: + kwargs = dict(token=token) + results = await self.api( + client, + "2", + "get", + f"/repositories/{self.slug}/src/{ref}/{dir_path}", + **kwargs, + ) + files.extend(results["values"]) + if "next" in results: + url = results["next"] + parsed = urllib_parse.urlparse(url) + page = urllib_parse.parse_qs(parsed.query)["page"][0] + else: + has_more = False + return [ + {"path": f["path"], "type": self._bitbucket_type_to_torngit_type(f["type"])} + for f in files + ] + + def _bitbucket_type_to_torngit_type(self, val): + if val == "commit_file": + return "file" + elif val == "commit_directory": + return "folder" + return "other" + + async def get_ancestors_tree(self, commitid, token=None): + async with self.get_client() as client: + res = await self.api( + client, + "2", + "get", + "/repositories/%s/commits" % self.slug, + token=token, + include=commitid, + ) + start = res["values"][0]["hash"] + commit_mapping = { + val["hash"]: [k["hash"] for k in val["parents"]] for val in res["values"] + } + return self.build_tree_from_commits(start, commit_mapping) + + def get_external_endpoint(self, endpoint: Endpoints, **kwargs): + if endpoint == Endpoints.commit_detail: + return self.urls["commit"].format( + username=self.data["owner"]["username"], + name=self.data["repo"]["name"], + commitid=kwargs["commitid"], + ) + raise NotImplementedError() + + async def get_best_effort_branches(self, commit_sha: str, token=None) -> List[str]: + """ + Gets a 'best effort' list of branches this commit is in. + If a branch is returned, this means this commit is in that branch. If not, it could still be + possible that this commit is in that branch + Args: + commit_sha (str): The sha of the commit we want to look at + Returns: + List[str]: A list of branch names + """ + return [] + + async def is_student(self): + return False diff --git a/libs/shared/shared/torngit/bitbucket_server.py b/libs/shared/shared/torngit/bitbucket_server.py new file mode 100644 index 0000000000..1b3f17d123 --- /dev/null +++ b/libs/shared/shared/torngit/bitbucket_server.py @@ -0,0 +1,772 @@ +import logging +import os +from datetime import datetime + +import httpx +from oauthlib import oauth1 + +from shared.config import get_config +from shared.torngit.base import TorngitBaseAdapter +from shared.torngit.exceptions import ( + TorngitClientError, + TorngitClientGeneralError, + TorngitObjectNotFoundError, + TorngitServer5xxCodeError, + TorngitServerUnreachableError, +) +from shared.torngit.status import Status +from shared.utils.urls import url_concat + +log = logging.getLogger(__name__) + + +class BitbucketServer(TorngitBaseAdapter): + # https://developer.atlassian.com/server/bitbucket/rest/v903/intro/#about + service = "bitbucket_server" + + @classmethod + def get_service_url(cls): + return get_config("bitbucket_server", "url") + + @property + def service_url(self): + return self.get_service_url() + + urls = dict( + user="users/%(username)s", + owner="projects/%(username)s", + repo="projects/%(username)s/repos/%(name)s", + issues="projects/%(username)s/repos/%(name)s/issues/%(issueid)s", + commit="projects/%(username)s/repos/%(name)s/commits/%(commitid)s", + commits="projects/%(username)s/repos/%(name)s/commits", + src="projects/%(username)s/repos/%(name)s/browse/%(path)s?at=%(commitid)s", + tree="projects/%(username)s/repos/%(name)s/browse?at=%(commitid)s", + create_file=None, + branch="projects/%(username)s/repos/%(name)s/browser?at=%(branch)s", + pull="projects/%(username)s/repos/%(name)s/pull-requests/%(pullid)s/overview", + compare="", + ) + + @property + def project(self): + if self.data["owner"].get("service_id", "?")[0] == "U": + return "/projects/~%s" % self.data["owner"]["username"].upper() + else: + return "/projects/%s" % self.data["owner"]["username"].upper() + + def diff_to_json(self, diff_json): + results = {} + for _diff in diff_json: + if not _diff.get("destination"): + results[_diff["source"]["toString"]] = dict(type="deleted") + + else: + fname = _diff["destination"]["toString"] + _before = _diff["source"]["toString"] if _diff.get("source") else None + _file = results.setdefault( + fname, + dict( + before=_before if _before != fname else None, + type="new" if _before is None else "modified", + segments=[], + ), + ) + for hunk in _diff.get("hunks", []): + segment = dict( + header=[ + str(hunk["sourceLine"]), + str(hunk["sourceSpan"]), + str(hunk["destinationLine"]), + str(hunk["destinationSpan"]), + ], + lines=[], + ) + _file["segments"].append(segment) + for seg in hunk["segments"]: + t = seg["type"][0] + for ln in seg["lines"]: + segment["lines"].append( + ("-" if t == "R" else "+" if t == "A" else " ") + + ln["line"] + ) + + if results: + return dict(files=self._add_diff_totals(results)) + else: + return dict(files=[]) + + async def api(self, method, url, body=None, token=None, **kwargs): + # process desired api path + if not url.startswith("http"): + url = "%s/rest/api/1.0%s" % (self.service_url, url) + + # process inline arguments + if kwargs: + url = url_concat(url, kwargs) + + token_to_use = token or self.token + oauth_client = oauth1.Client( + self._oauth_consumer_token()["key"], + client_secret=self._oauth_consumer_token()["secret"], + resource_owner_key=token_to_use["key"], + resource_owner_secret=token_to_use["secret"], + signature_type=oauth1.SIGNATURE_TYPE_QUERY, + ) + + headers = { + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": os.getenv("USER_AGENT", "Default"), + } + url, headers, _oauth_body = oauth_client.sign( + url, http_method=method, headers=headers + ) + + log_dict = dict( + event="api", + endpoint=url, + method=method, + bot=token_to_use.get("username"), + repo_slug=self.slug, + ) + + try: + async with self.get_client() as client: + res = await client.request( + method.upper(), url, json=body, headers=headers + ) + logged_body = None + if res.status_code >= 300 and res.text is not None: + logged_body = res.text + log.log( + logging.WARNING if res.status_code >= 300 else logging.INFO, + "Bitbucket HTTP %s", + res.status_code, + extra=dict(body=logged_body, **log_dict), + ) + except (httpx.NetworkError, httpx.TimeoutException): + raise TorngitServerUnreachableError("Bitbucket was not able to be reached.") + if res.status_code == 599: + raise TorngitServerUnreachableError( + "Bitbucket was not able to be reached, server timed out." + ) + elif res.status_code >= 500: + raise TorngitServer5xxCodeError("Bitbucket is having 5xx issues") + elif res.status_code >= 300: + message = f"Bitbucket API: {res.reason_phrase}" + raise TorngitClientGeneralError( + res.status_code, response_data={"content": res.content}, message=message + ) + if res.status_code == 204: + return None + elif "application/json" in res.headers.get("Content-Type"): + return res.json() + else: + return res.text + + async def get_authenticated(self, token=None): + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp1889424 + if self.data["repo"]["private"]: + await self.api( + "get", + "%s/repos/%s" % (self.project, self.data["repo"]["name"]), + token=token, + ) + return (True, True) + + async def get_is_admin(self, user, token=None): + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp3389568 + res = await self.api( + "get", + "%s/permissions/users" % self.project, + filter=user["username"], + token=token, + ) + userid = str(user["service_id"]).replace("U", "") + # PROJECT_READ, PROJECT_WRITE, PROJECT_ADMIN, ADMIMN + res = any( + filter( + lambda v: str(v["user"]["id"]) == userid and "ADMIN" in v["permission"], + res["values"], + ) + ) + return res + + async def get_repository(self, token=None): + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp1889424 + res = await self.api( + "get", + "%s/repos/%s" % (self.project, self.data["repo"]["name"]), + token=token, + ) + owner_service_id = res["project"]["id"] + if res["project"]["type"] == "PERSONAL": + owner_service_id = "U%d" % res["project"]["owner"]["id"] + + fork = None + if res.get("origin"): + _fork_owner_service_id = res["origin"]["project"]["id"] + if res["origin"]["project"]["type"] == "PERSONAL": + _fork_owner_service_id = "U%d" % res["origin"]["project"]["owner"]["id"] + + fork = dict( + owner=dict( + service_id=_fork_owner_service_id, + username=res["origin"]["project"]["key"], + ), + repo=dict( + service_id=res["origin"]["id"], + language=None, + private=(not res["origin"]["public"]), + branch="main", + fork=fork, + name=res["origin"]["slug"], + ), + ) + + return dict( + owner=dict(service_id=owner_service_id, username=res["project"]["key"]), + repo=dict( + service_id=res["id"], + language=None, + private=(not res.get("public", res.get("origin", {}).get("public"))), + branch="main", + name=res["slug"], + ), + ) + + async def get_repo_languages( + self, token=None, language: str | None = None + ) -> list[str]: + """ + Gets the languages belonging to this repository. Bitbucket has no way to + track languages, so we'll return a list with the existing language + Param: + language: the language belonging to the repository.language key + Returns: + List[str]: A list of language names + """ + languages = [] + + if language: + languages.append(language.lower()) + + return languages + + async def get_source(self, path, ref, token=None): + content, start = [], 0 + while True: + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp2028128 + try: + res = await self.api( + "get", + "{0}/repos/{1}/browse/{2}".format( + self.project, + self.data["repo"]["name"], + path.replace(" ", "%20"), + ), + at=ref, + start=start, + token=token, + ) + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Path {path} not found at {ref}", + ) + raise + + content.extend(res["lines"]) + if res["isLastPage"] or res.get("nextPageStart") is None: + break + else: + start = res["nextPageStart"] + + return dict( + commitid=None, # [FUTURE] unknown atm + content="\n".join(map(lambda a: a.get("text", ""), content)), + ) + + async def get_ancestors_tree(self, commitid, token=None): + res = await self.api( + "get", + "%s/repos/%s/commits/" % (self.project, self.data["repo"]["name"]), + token=token, + until=commitid, + ) + start = res["values"][0]["id"] + commit_mapping = { + val["id"]: [k["id"] for k in val["parents"]] for val in res["values"] + } + return self.build_tree_from_commits(start, commit_mapping) + + async def get_commit(self, commit, token=None): + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp3530560 + res = await self.api( + "get", + "%s/repos/%s/commits/%s" + % (self.project, self.data["repo"]["name"], commit), + token=token, + ) + + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp2598928 + author = await self.api( + "get", "/users", filter=res["author"]["emailAddress"], token=token + ) + if not author["size"]: + author = await self.api("get", "/users", filter=res["author"]["name"]) + author = author["values"][0] if author["size"] else {} + + return dict( + author=dict( + id=("U%s" % author.get("id")) if author.get("id") else None, + username=author.get("name"), + email=res["author"]["emailAddress"], + name=res["author"]["name"], + ), + commitid=commit, + parents=[p["id"] for p in res["parents"]], + message=res["message"], + timestamp=datetime.fromtimestamp( + int(str(res["authorTimestamp"])[:10]) + ).strftime("%Y-%m-%d %H:%M:%S"), + ) + + async def get_pull_request_commits(self, pullid, token=None, _in_loop=None): + commits, start = [], 0 + while True: + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp2519392 + res = await self.api( + "get", + "%s/repos/%s/pull-requests/%s/commits" + % (self.project, self.data["repo"]["name"], pullid), + start=start, + token=token, + ) + if len(res["values"]) == 0: + break + commits.extend([c["id"] for c in res["values"]]) + if res["isLastPage"] or res.get("nextPageStart") is None: + break + else: + start = res["nextPageStart"] + + # order is NEWEST...OLDEST + return commits + + async def get_commit_diff(self, commit, context=None, token=None): + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp3120016 + diff = await self.api( + "get", + "%s/repos/%s/commits/%s/diff" + % (self.project, self.data["repo"]["name"], commit), + withComments=False, + whitespace="ignore-all", + contextLines=context or -1, + token=None, + ) + return self.diff_to_json(diff["diffs"]) + + async def get_compare( + self, base, head, context=None, with_commits=True, token=None + ): + # get diff + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp3370768 + diff = ( + await self.api( + "get", + "%s/repos/%s/commits/%s/diff" + % (self.project, self.data["repo"]["name"], head), + withComments=False, + whitespace="ignore-all", + contextLines=context or -1, + since=base, + token=token, + ) + )["diffs"] + + # get commits + commits, start = [], 0 + while with_commits: + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp3513104 + res = await self.api( + "get", + "%s/repos/%s/commits" % (self.project, self.data["repo"]["name"]), + start=start, + token=token, + since=base, + until=head, + ) + # listed [newest...oldest] + commits.extend( + [ + dict( + commitid=c["id"], + message=c["message"], + timestamp=c["authorTimestamp"], + author=dict( + name=c["author"]["name"], email=c["author"]["emailAddress"] + ), + ) + for c in res["values"] + ] + ) + if res["isLastPage"] or res.get("nextPageStart") is None: + break + else: + start = res["nextPageStart"] + + return dict(diff=self.diff_to_json(diff), commits=commits[::-1]) + + async def post_webhook(self, name, url, events, secret, token=None): + # https://docs.atlassian.com/bitbucket-server/rest/6.0.1/bitbucket-rest.html#idp325 + # https://confluence.atlassian.com/bitbucketserver066/event-payload-978197889.html + res = await self.api( + "post", + "%s/repos/%s/webhooks" % (self.project, self.data["repo"]["name"]), + body=dict(description=name, active=True, events=events, url=url), + json=True, + token=token, + ) + return res + + async def get_pull_request(self, pullid, token=None): + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp2167824 + res = await self.api( + "get", + "%s/repos/%s/pull-requests/%s" + % (self.project, self.data["repo"]["name"], pullid), + token=token, + ) + # need to get all commits, shit. + pull_commitids = await self.get_pull_request_commits( + pullid, token=token, _in_loop=True + ) + first_commit = ( + await self.api( + "get", + "%s/repos/%s/commits/%s" + % (self.project, self.data["repo"]["name"], pull_commitids[-1]), + token=token, + ) + )["parents"][0]["id"] + return dict( + title=res["title"], + state={"OPEN": "open", "DECLINED": "close", "MERGED": "merged"}.get( + res["state"] + ), + id=str(pullid), + number=str(pullid), + base=dict(branch=res["toRef"]["displayId"], commitid=first_commit), + head=dict(branch=res["fromRef"]["displayId"], commitid=pull_commitids[0]), + ) + + async def list_top_level_files(self, ref, token=None): + return await self.list_files(ref, dir_path="", token=None) + + async def list_files(self, ref, dir_path, token=None): + page = None + has_more = True + files = [] + while has_more: + # https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Busername%7D/%7Brepo_slug%7D/src#get + kwargs = {} + + if page is not None: + kwargs["page"] = page + + if ref not in [None, ""]: + kwargs["at"] = ref + + results = await self.api( + "get", + "%s/repos/%s/files/%s" + % (self.project, self.data["repo"]["name"], dir_path), + **kwargs, + ) + files.extend(results["values"]) + page = results["nextPageStart"] + has_more = not results["isLastPage"] + return [{"path": f, "type": "file"} for f in files] + + async def _fetch_page_of_repos(self, start, token): + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp1847760 + res = await self.api("get", "/repos", start=start, token=token) + + repos = [] + for repo in res["values"]: + ownerid = str(repo["project"]["id"]) + if repo["project"]["type"] == "PERSONAL": + ownerid = "U" + str(repo["project"]["owner"]["id"]) + + repos.append( + dict( + owner=dict( + service_id=ownerid, + username=repo["project"]["key"].lower().replace("~", ""), + ), + repo=dict( + service_id=repo["id"], + name=repo["slug"].lower(), + language=None, + private=( + not repo.get("public", repo.get("origin", {}).get("public")) + ), + branch="main", + ), + ) + ) + + next_page_start = res.get("nextPageStart") if not res["isLastPage"] else None + return (repos, next_page_start) + + async def list_repos(self, username=None, token=None): + data, start = [], 0 + while True: + repos, next_page_start = await self._fetch_page_of_repos(start, token) + + if len(repos) == 0 or next_page_start is None: + break + else: + start = next_page_start + + return data + + async def list_repos_generator(self, username=None, token=None): + """ + New version of list_repos() that should replace the old one after safely + rolling out in the worker. + """ + start = 0 + while True: + repos, next_page_start = await self._fetch_page_of_repos(start, token) + + if len(repos) == 0: + break + + yield repos + + if next_page_start is None: + break + else: + start = next_page_start + + async def list_teams(self, token=None): + data, start = [], 0 + while True: + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp2301216 + res = await self.api("get", "/projects", start=start, token=token) + if len(res["values"]) == 0: + break + data.extend( + [ + dict(id=row["id"], username=row["key"], name=row["name"]) + for row in res["values"] + ] + ) + if res["isLastPage"] or res.get("nextPageStart") is None: + break + else: + start = res["nextPageStart"] + + return data + + async def get_commit_statuses(self, commit, _merge=None, token=None): + # https://developer.atlassian.com/stash/docs/latest/how-tos/updating-build-status-for-commits.html + start, data = 0, [] + while True: + res = await self.api( + "get", + "%s/rest/build-status/1.0/commits/%s" % (self.service_url, commit), + start=start, + token=token, + ) + if len(res["values"]) == 0: + break + data.extend( + [ + { + "time": s["dateAdded"], + "state": s["state"], + "url": s["url"], + "description": s["description"], + "context": s["name"], + } + for s in res["values"] + ] + ) + if res["isLastPage"] or res.get("nextPageStart") is None: + break + else: + start = res["nextPageStart"] + + return Status(data) + + async def set_commit_status( + self, + commit, + status, + context, + description, + url=None, + merge_commit=None, + token=None, + coverage=None, + ): + # https://developer.atlassian.com/stash/docs/latest/how-tos/updating-build-status-for-commits.html + assert status in ("pending", "success", "error", "failure"), "status not valid" + res = await self.api( + "post", + "%s/rest/build-status/1.0/commits/%s" % (self.service_url, commit), + body=dict( + state=dict( + pending="INPROGRESS", + success="SUCCESSFUL", + error="FAILED", + failure="FAILED", + ).get(status), + key=context, + name=context, + url=url, + description=description, + ), + token=token, + ) + if merge_commit: + await self.api( + "post", + "%s/rest/build-status/1.0/commits/%s" + % (self.service_url, merge_commit[0]), + body=dict( + state=dict( + pending="INPROGRESS", + success="SUCCESSFUL", + error="FAILED", + failure="FAILED", + ).get(status), + key=merge_commit[1], + name=merge_commit[1], + url=url, + description=description, + ), + token=token, + ) + return {"id": res.get("id", "NO-ID") if res else "NO-ID"} + + async def post_comment(self, pullid, body, token=None): + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp3165808 + res = await self.api( + "post", + "%s/repos/%s/pull-requests/%s/comments" + % (self.project, self.data["repo"]["name"], pullid), + body=dict(text=body), + token=token, + ) + return {"id": "%(id)s:%(version)s" % res} + + async def edit_comment(self, pullid, commentid, body, token=None): + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp3184624 + commentid, version = commentid.split(":", 1) + res = await self.api( + "put", + "%s/repos/%s/pull-requests/%s/comments/%s" + % (self.project, self.data["repo"]["name"], pullid, commentid), + body=dict(text=body, version=version), + token=token, + ) + return {"id": "%(id)s:%(version)s" % res} + + async def delete_comment(self, issueid, commentid, token=None): + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp3189408 + commentid, version = commentid.split(":", 1) + await self.api( + "delete", + "%s/repos/%s/pull-requests/%s/comments/%s" + % (self.project, self.data["repo"]["name"], issueid, commentid), + version=version, + token=token, + ) + return True + + async def get_branches(self, token=None): + branches, start = [], 0 + while True: + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp2243696 + res = await self.api( + "get", + "%s/repos/%s/branches" % (self.project, self.data["repo"]["name"]), + start=start, + token=token, + ) + if len(res["values"]) == 0: + break + branches.extend( + [ + (b["displayId"].encode("utf-8", "replace"), b["latestCommit"]) + for b in res["values"] + ] + ) + if res["isLastPage"] or res.get("nextPageStart") is None: + break + else: + start = res["nextPageStart"] + return branches + + async def find_pull_request( + self, commit=None, branch=None, state="open", token=None + ): + start = 0 + state = {"open": "OPEN", "close": "DECLINED", "merged": "MERGED"}.get( + state, "ALL" + ) + if branch: + while True: + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp2048560 + res = await self.api( + "get", + "%s/repos/%s/pull-requests" + % (self.project, self.data["repo"]["name"]), + state=state, + withAttributes=False, + withProperties=False, + start=start, + token=token, + ) + if len(res["values"]) == 0: + break + + for pull in res["values"]: + if pull["fromRef"]["displayId"] == branch: + return pull["id"] + + if res["isLastPage"] or res.get("nextPageStart") is None: + break + + else: + start = res["nextPageStart"] + + async def get_pull_requests(self, state="open", token=None): + pulls, start = [], 0 + state = {"open": "OPEN", "close": "DECLINED", "merged": "MERGED"}.get( + state, "ALL" + ) + while True: + # https://developer.atlassian.com/static/rest/bitbucket-server/4.0.1/bitbucket-rest.html#idp2048560 + res = await self.api( + "get", + "%s/repos/%s/pull-requests" % (self.project, self.data["repo"]["name"]), + state=state, + withAttributes=False, + withProperties=False, + start=start, + token=token, + ) + if len(res["values"]) == 0: + break + pulls.extend([pull["id"] for pull in res["values"]]) + if res["isLastPage"] or res.get("nextPageStart") is None: + break + else: + start = res["nextPageStart"] + return pulls diff --git a/libs/shared/shared/torngit/enums.py b/libs/shared/shared/torngit/enums.py new file mode 100644 index 0000000000..e2447d6344 --- /dev/null +++ b/libs/shared/shared/torngit/enums.py @@ -0,0 +1,5 @@ +from enum import Enum, auto + + +class Endpoints(Enum): + commit_detail = auto() diff --git a/libs/shared/shared/torngit/exceptions.py b/libs/shared/shared/torngit/exceptions.py new file mode 100644 index 0000000000..679bedc297 --- /dev/null +++ b/libs/shared/shared/torngit/exceptions.py @@ -0,0 +1,87 @@ +class TorngitError(Exception): + pass + + +class TorngitMisconfiguredCredentials(TorngitError): + pass + + +class TorngitClientError(TorngitError): + @property + def code(self): + return self._code + + @property + def response_data(self): + return self._response_data + + +class TorngitClientGeneralError(TorngitClientError): + def __init__(self, status_code, response_data, message): + super().__init__(status_code, response_data, message) + self._code = status_code + self._response_data = response_data + self.message = message + + +class TorngitRepoNotFoundError(TorngitClientError): + def __init__(self, response_data, message): + super().__init__(response_data, message) + self._code = 404 + self._response_data = response_data + self.message = message + + +class TorngitObjectNotFoundError(TorngitClientError): + def __init__(self, response_data, message): + super().__init__(response_data, message) + self._code = 404 + self._response_data = response_data + self.message = message + + +class TorngitRateLimitError(TorngitClientError): + def __init__(self, response_data, message, reset=None, retry_after=None): + super().__init__(response_data, message, reset) + self._code = 403 + self._response_data = response_data + self.message = message + + # timestamp when the rate limit resets + self.reset = reset + # seconds to wait before making another request + self.retry_after = retry_after + + +class TorngitUnauthorizedError(TorngitClientError): + def __init__(self, response_data, message): + super().__init__(response_data, message) + self._code = 401 + self._response_data = response_data + self.message = message + + +class TorngitServerFailureError(TorngitError): + pass + + +class TorngitServerUnreachableError(TorngitServerFailureError): + pass + + +class TorngitServer5xxCodeError(TorngitServerFailureError): + pass + + +class TorngitRefreshTokenFailedError(TorngitError): + def __init__(self, message) -> None: + self._code = 555 + self._response_data = None + self.message = message + + +class TorngitCantRefreshTokenError(TorngitClientError): + def __init__(self, message) -> None: + self._code = 555 + self._response_data = None + self.message = message diff --git a/libs/shared/shared/torngit/github.py b/libs/shared/shared/torngit/github.py new file mode 100644 index 0000000000..cff85f4ab4 --- /dev/null +++ b/libs/shared/shared/torngit/github.py @@ -0,0 +1,2437 @@ +import base64 +import hashlib +import logging +import os +from base64 import b64decode +from datetime import datetime, timezone +from string import Template +from typing import Dict, List, Optional +from urllib.parse import parse_qs, urlencode + +import httpx +import sentry_sdk +from httpx import Response + +from shared.config import get_config +from shared.github import get_github_integration_token, get_github_jwt_token +from shared.helpers.cache import cache +from shared.helpers.redis import get_redis_connection +from shared.metrics import Counter +from shared.rate_limits import set_entity_to_rate_limited +from shared.rollouts.features import INCLUDE_GITHUB_COMMENT_ACTIONS_BY_OWNER +from shared.torngit.base import TokenType, TorngitBaseAdapter +from shared.torngit.enums import Endpoints +from shared.torngit.exceptions import ( + TorngitClientError, + TorngitClientGeneralError, + TorngitMisconfiguredCredentials, + TorngitObjectNotFoundError, + TorngitRateLimitError, + TorngitRefreshTokenFailedError, + TorngitRepoNotFoundError, + TorngitServer5xxCodeError, + TorngitServerUnreachableError, + TorngitUnauthorizedError, +) +from shared.torngit.response_types import ProviderPull +from shared.torngit.status import Status +from shared.typings.oauth_token_types import OauthConsumerToken +from shared.typings.torngit import GithubInstallationInfo, UploadType +from shared.utils.urls import url_concat + +log = logging.getLogger(__name__) + +METRICS_PREFIX = "services.torngit.github" + + +GITHUB_API_CALL_COUNTER = Counter( + "git_provider_api_calls_github", + "Number of times github called this endpoint", + ["endpoint"], +) + +# Github Enterprise uses the same urls as Github, but has a separate Counter +GITHUB_E_API_CALL_COUNTER = Counter( + "git_provider_api_calls_github_enterprise", + "Number of times github enterprise called this endpoint", + ["endpoint"], +) + + +GITHUB_API_ENDPOINTS = { + "request_webhook_redelivery": { + "counter": GITHUB_API_CALL_COUNTER.labels( + endpoint="request_webhook_redelivery" + ), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="request_webhook_redelivery" + ), + "url_template": Template("/app/hook/deliveries/${delivery_id}/attempts"), + }, + "refresh_token": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="refresh_token"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="refresh_token" + ), + "url_template": Template("/login/oauth/access_token"), + }, + "make_http_call_retry": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="make_http_call_retry"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="make_http_call_retry" + ), + "url_template": "", # no url template, just counter + }, + "list_webhook_deliveries": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="list_webhook_deliveries"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="list_webhook_deliveries" + ), + "url_template": Template("/app/hook/deliveries?per_page=50"), + }, + "is_student": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="is_student"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels(endpoint="is_student"), + "url_template": Template("https://education.github.com/api/user"), + }, + "get_best_effort_branches": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_best_effort_branches"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_best_effort_branches" + ), + "url_template": Template( + "/repos/${slug}/commits/${commit_sha}/branches-where-head" + ), + }, + "get_workflow_run": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_workflow_run"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_workflow_run" + ), + "url_template": Template("/repos/${slug}/actions/runs/${run_id}"), + }, + "update_check_run": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="update_check_run"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="update_check_run" + ), + "url_template": Template("/repos/${slug}/check-runs/${check_run_id}"), + }, + "get_repos_with_languages_graphql": { + "counter": GITHUB_API_CALL_COUNTER.labels( + endpoint="get_repos_with_languages_graphql" + ), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_repos_with_languages_graphql" + ), + "url_template": Template("/graphql"), + }, + "get_repo_languages": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_repo_languages"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_repo_languages" + ), + "url_template": Template("/repos/${slug}/languages"), + }, + "get_check_suites": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_check_suites"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_check_suites" + ), + "url_template": Template("/repos/${slug}/commits/${git_sha}/check-suites"), + }, + "create_check_run": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="create_check_run"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="create_check_run" + ), + "url_template": Template("/repos/${slug}/check-runs"), + }, + "get_ancestors_tree": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_ancestors_tree"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_ancestors_tree" + ), + "url_template": Template("/repos/${slug}/commits"), + }, + "list_files": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="list_files"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels(endpoint="list_files"), + "url_template": Template("/repos/${slug}/contents"), + }, + "get_pull_request_files": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_pull_request_files"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_pull_request_files" + ), + "url_template": Template("/repos/${slug}/pulls/${pullid}/files"), + }, + "find_pull_request": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="find_pull_request"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="find_pull_request" + ), + "url_template": Template("/repos/${slug}/commits/${commit}/pulls"), + }, + "get_pull_requests": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_pull_requests"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_pull_requests" + ), + "url_template": Template("/repos/${slug}/pulls"), + }, + "get_pull_request": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_pull_request"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_pull_request" + ), + "url_template": Template("/repos/${slug}/pulls/${pullid}"), + }, + "get_distance_in_commits": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_distance_in_commits"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_distance_in_commits" + ), + "url_template": Template("/repos/${slug}/compare/${base_branch}...${base}"), + }, + "get_compare": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_compare"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels(endpoint="get_compare"), + "url_template": Template("/repos/${slug}/compare/${base}...${head}"), + }, + "get_commit_diff": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_commit_diff"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_commit_diff" + ), + "url_template": Template("/repos/${slug}/commits/${commit}"), + }, + "get_source": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_source"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels(endpoint="get_source"), + "url_template": Template("/repos/${slug}/contents/${path}"), + }, + "get_source_again": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_source_again"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_source_again" + ), + "url_template": "", # no url template, just counter + }, + "get_commit_statuses": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_commit_statuses"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_commit_statuses" + ), + "url_template": Template("/repos/${slug}/commits/${commit}/status"), + }, + "set_commit_status": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="set_commit_status"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="set_commit_status" + ), + "url_template": Template("/repos/${slug}/statuses/${commit}"), + }, + "set_commit_status_merge_commit": { + "counter": GITHUB_API_CALL_COUNTER.labels( + endpoint="set_commit_status_merge_commit" + ), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="set_commit_status_merge_commit" + ), + "url_template": Template("/repos/${slug}/statuses/${merge_commit}"), + }, + "delete_comment": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="delete_comment"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="delete_comment" + ), + "url_template": Template("/repos/${slug}/issues/comments/${commentid}"), + }, + "edit_comment": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="edit_comment"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels(endpoint="edit_comment"), + "url_template": Template("/repos/${slug}/issues/comments/${commentid}"), + }, + "post_comment": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="post_comment"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels(endpoint="post_comment"), + "url_template": Template("/repos/${slug}/issues/${issueid}/comments"), + }, + "delete_webhook": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="delete_webhook"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="delete_webhook" + ), + "url_template": Template("/repos/${slug}/hooks/${hookid}"), + }, + "edit_webhook": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="edit_webhook"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels(endpoint="edit_webhook"), + "url_template": Template("/repos/${slug}/hooks/${hookid}"), + }, + "post_webhook": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="post_webhook"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels(endpoint="post_webhook"), + "url_template": Template("/repos/${slug}/hooks"), + }, + "get_raw_pull_request_commits": { + "counter": GITHUB_API_CALL_COUNTER.labels( + endpoint="get_raw_pull_request_commits" + ), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_raw_pull_request_commits" + ), + "url_template": Template( + "/repos/${slug}/pulls/${pullid}/commits?per_page=${max}&page=${page_n}" + ), + }, + "list_teams": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="list_teams"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels(endpoint="list_teams"), + "url_template": Template("/user/memberships/orgs?state=active"), + }, + "list_teams_org_name": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="list_teams_org_name"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="list_teams_org_name" + ), + "url_template": Template("/users/${login}"), + }, + "get_gh_app_installation": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_gh_app_installation"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_gh_app_installation" + ), + "url_template": Template("/app/installations/${installation_id}"), + }, + "get_repos_from_nodeids_generator_graphql": { + "counter": GITHUB_API_CALL_COUNTER.labels( + endpoint="get_repos_from_nodeids_generator_graphql" + ), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_repos_from_nodeids_generator_graphql" + ), + "url_template": Template("/graphql"), + }, + "get_owner_from_nodeid_graphql": { + "counter": GITHUB_API_CALL_COUNTER.labels( + endpoint="get_owner_from_nodeid_graphql" + ), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_owner_from_nodeid_graphql" + ), + "url_template": Template("/graphql"), + }, + "fetch_number_of_repos_graphql": { + "counter": GITHUB_API_CALL_COUNTER.labels( + endpoint="fetch_number_of_repos_graphql" + ), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="fetch_number_of_repos_graphql" + ), + "url_template": Template("/graphql"), + }, + "fetch_page_of_repos_without_username": { + "counter": GITHUB_API_CALL_COUNTER.labels( + endpoint="fetch_page_of_repos_without_username" + ), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="fetch_page_of_repos_without_username" + ), + "url_template": Template("/user/repos?per_page=${page_size}&page=${page}"), + }, + "fetch_page_of_repos_with_username": { + "counter": GITHUB_API_CALL_COUNTER.labels( + endpoint="fetch_page_of_repos_with_username" + ), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="fetch_page_of_repos_with_username" + ), + "url_template": Template( + "/users/${username}/repos?per_page=${page_size}&page=${page}" + ), + }, + "fetch_page_of_repos_using_installation": { + "counter": GITHUB_API_CALL_COUNTER.labels( + endpoint="fetch_page_of_repos_using_installation" + ), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="fetch_page_of_repos_using_installation" + ), + "url_template": Template( + "/installation/repositories?per_page=${page_size}&page=${page}" + ), + }, + "get_authenticated": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_authenticated"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_authenticated" + ), + "url_template": Template("/repos/${slug}"), + }, + "get_is_admin": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_is_admin"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels(endpoint="get_is_admin"), + "url_template": Template( + "/orgs/${owner_username}/memberships/${user_username}" + ), + }, + "get_user_token": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_user_token"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_user_token" + ), + "url_template": Template("/login/oauth/access_token"), + }, + "get_authenticated_user": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_authenticated_user"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_authenticated_user" + ), + "url_template": Template("/user"), + }, + "get_authenticated_user_email": { + "counter": GITHUB_API_CALL_COUNTER.labels( + endpoint="get_authenticated_user_email" + ), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_authenticated_user_email" + ), + "url_template": Template("/user/emails"), + }, + "get_branch": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_branch"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels(endpoint="get_branch"), + "url_template": Template("/repos/${slug}/branches/${branch_name}"), + }, + "get_branches": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="get_branches"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels(endpoint="get_branches"), + "url_template": Template("/repos/${slug}/branches"), + }, + "get_repository_with_service_id": { + "counter": GITHUB_API_CALL_COUNTER.labels( + endpoint="get_repository_with_service_id" + ), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_repository_with_service_id" + ), + "url_template": Template("/repositories/${service_id}"), + }, + "get_repository_without_service_id": { + "counter": GITHUB_API_CALL_COUNTER.labels( + endpoint="get_repository_without_service_id" + ), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_repository_without_service_id" + ), + "url_template": Template("/repos/${slug}"), + }, + "get_check_runs_with_head_sha": { + "counter": GITHUB_API_CALL_COUNTER.labels( + endpoint="get_check_runs_with_head_sha" + ), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_check_runs_with_head_sha" + ), + "url_template": Template("/repos/${slug}/commits/${head_sha}/check-runs"), + }, + "get_check_runs_with_check_suite_id": { + "counter": GITHUB_API_CALL_COUNTER.labels( + endpoint="get_check_runs_with_check_suite_id" + ), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_check_runs_with_check_suite_id" + ), + "url_template": Template( + "/repos/${slug}/check-suites/${check_suite_id}/check-runs" + ), + }, + "list_files_with_dir_path": { + "counter": GITHUB_API_CALL_COUNTER.labels(endpoint="list_files_with_dir_path"), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="list_files_with_dir_path" + ), + "url_template": Template("/repos/${slug}/contents/${dir_path}"), + }, + "get_github_integration_token": { + "counter": GITHUB_API_CALL_COUNTER.labels( + endpoint="get_github_integration_token" + ), + "enterprise_counter": GITHUB_E_API_CALL_COUNTER.labels( + endpoint="get_github_integration_token" + ), + "url_template": Template( + "${api_endpoint}/app/installations/${integration_id}/access_tokens" + ), + }, +} + + +# uncounted urls +external_endpoint_template = Template("${username}/${name}/commit/${commitid}") + + +class GitHubGraphQLQueries(object): + _queries = dict( + REPOS_FROM_NODEIDS=""" +query GetReposFromNodeIds($node_ids: [ID!]!) { + nodes(ids: $node_ids) { + __typename + ... on Repository { + # databaseId == service_id + databaseId + name + primaryLanguage { + name + } + isPrivate + defaultBranchRef { + name + } + owner { + # This ID is actually the node_id, not the ownerid + id + login + } + } + } +} +""", + OWNER_FROM_NODEID=""" +query GetOwnerFromNodeId($node_id: ID!) { + node(id: $node_id) { + __typename + ... on Organization { + login + databaseId + } + ... on User { + login + databaseId + } + } +} +""", + REPO_LANGUAGES_FROM_OWNER=""" +query Repos($owner: String!, $cursor: String, $first: Int!) { + repositoryOwner(login: $owner) { + repositories( + first: $first + ownerAffiliations: OWNER + isLocked: false + orderBy: {field: NAME, direction: ASC} + after: $cursor + ) { + pageInfo { + hasNextPage + endCursor + } + nodes { + name + languages(first: 100) { + edges { + node { + name + id + } + } + } + } + } + } +} +""", + ) + + def get(self, query_name: str) -> str | None: + return self._queries.get(query_name, None) + + def prepare(self, query_name: str, variables: dict) -> dict | None: + # If Query was an object we could validate the variables + query = self.get(query_name) + if query is not None: + return {"query": query, "variables": variables} + return None + + +class Github(TorngitBaseAdapter): + service = "github" + graphql = GitHubGraphQLQueries() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._redis_connection = get_redis_connection() + + @classmethod + def get_service_url(cls): + return get_config("github", "url", default="https://github.com").strip("/") + + @property + def service_url(self): + return self.get_service_url() + + @classmethod + def get_api_url(cls): + return get_config("github", "api_url", default="https://api.github.com").strip( + "/" + ) + + @property + def api_url(self): + return self.get_api_url() + + @classmethod + def get_api_host_header(cls): + return get_config(cls.service, "api_host_override") + + @property + def api_host_header(self): + return self.get_api_host_header() + + @classmethod + def get_host_header(cls): + return get_config(cls.service, "host_override") + + @property + def host_header(self): + return self.get_host_header() + + @property + def token(self): + return self._token + + @classmethod + def count_and_get_url_template(cls, url_name): + GITHUB_API_ENDPOINTS[url_name]["counter"].inc() + return GITHUB_API_ENDPOINTS[url_name]["url_template"] + + async def build_comment_request_body( + self, body: dict, issueid: int | None = None + ) -> dict: + body = dict(body=body) + upload_type = self.data.get("additional_data", {}).get("upload_type") + if upload_type in [UploadType.BUNDLE_ANALYSIS, UploadType.TEST_RESULTS]: + return body + try: + ownerid = self.data["owner"].get("ownerid") + if ( + issueid is not None + and await INCLUDE_GITHUB_COMMENT_ACTIONS_BY_OWNER.check_value_async( + identifier=ownerid, default=False + ) + ): + bot_name = get_config( + "github", "comment_action_bot_name", default="sentry" + ) + body["actions"] = [ + { + "name": "Generate Unit Tests", + "type": "copilot-chat", + "prompt": f"@{bot_name} generate tests for this PR.", + } + ] + except Exception: + pass + + return body + + async def api(self, *args, token=None, **kwargs): + """ + Makes a single http request to GitHub and returns the parsed response + """ + token_to_use = token or self.token + + log.info( + "Making Github API call", + extra=dict( + has_token=bool(token), + has_self_token=bool(self.token), + is_same_token=(token == self.token), + ), + ) + + if not token_to_use: + raise TorngitMisconfiguredCredentials() + response = await self.make_http_call(*args, token_to_use=token_to_use, **kwargs) + return self._parse_response(response) + + async def paginated_api_generator( + self, client, method, url_name, token=None, **kwargs + ): + """ + Generator that requests pages from GitHub and yields each page as they come. + Continues to request pages while there's a link to the next page. + """ + token_to_use = token or self.token + if not token_to_use: + raise TorngitMisconfiguredCredentials() + url = self.count_and_get_url_template( + url_name=url_name + ).substitute() # counts first call + page = 1 + while url: + args = [client, method, url] + response = await self.make_http_call( + *args, token_to_use=token_to_use, **kwargs + ) + yield self._parse_response(response) + url = response.links.get("next", {}).get("url", "") + if page > 1: + _ = self.count_and_get_url_template( + url_name=url_name + ).substitute() # counts subsequent calls + page += 1 + + def _parse_response(self, res: Response): + if res.status_code == 204: + return None + elif res.headers.get("Content-Type")[:16] == "application/json": + return res.json() + else: + try: + return res.text + except UnicodeDecodeError as uerror: + log.warning( + "Unable to parse Github response", + extra=dict( + first_bytes=res.content[:100], + final_bytes=res.content[-100:], + errored_bytes=res.content[ + (uerror.start - 10) : (uerror.start + 10) + ], + declared_contenttype=res.headers.get("content-type"), + ), + ) + return res.text + + def _possibly_mark_current_entity_as_rate_limited( + self, + *, + reset_timestamp: Optional[str] = None, + retry_in_seconds: Optional[int] = None, + # Couldn't type this to Token cause there's a circular import. Suggestions are welcome + token, + ) -> None: + entity_key_name = token.get("entity_name") + if entity_key_name is None: + log.warning( + "Can't mark entity as rate limited because entity name is missing", + ) + return None + if retry_in_seconds is None and reset_timestamp is None: + log.warning( + "Can't mark entity as rate limited because TTL is missing", + extra=dict(entity_key_name=entity_key_name), + ) + return + ttl_seconds = retry_in_seconds + if ttl_seconds is None: + # https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api?apiVersion=2022-11-28#handle-rate-limit-errors-appropriately + ttl_seconds = max( + 0, + int(reset_timestamp) - int(datetime.now(timezone.utc).timestamp()), + ) + if ttl_seconds > 0: + log.info( + "Marking entity as rate limited", + extra=dict( + entity_key_name=entity_key_name, + rate_limit_duration_seconds=ttl_seconds, + ), + ) + set_entity_to_rate_limited( + redis_connection=self._redis_connection, + key_name=entity_key_name, + ttl_seconds=ttl_seconds, + ) + + def _get_next_fallback_token( + self, + ) -> Optional[str]: + """If additional fallback tokens were passed to this instance of GitHub + select the next token in line to retry the previous request. + + !side effect: Marks the current token as rate limited in redis + !side effect: Updates the self._token value + !side effect: Consumes one of self.data.fallback_installations + """ + fallback_installations: List[GithubInstallationInfo] = self.data.get( + "fallback_installations", None + ) + if fallback_installations is None or fallback_installations == []: + # No tokens to fallback on + return None + # ! side effect: consume one of the fallback tokens (makes it the token of this instance) + installation_info = fallback_installations.pop(0) + # The function arg is 'integration_id' + installation_id = installation_info.pop("installation_id") + obj_id = installation_info.pop("id", None) + token_to_use = get_github_integration_token( + self.service, installation_id, **installation_info + ) + # ! side effect: update the token so subsequent requests won't fail + self.set_token(dict(key=token_to_use)) + self.data["installation"] = { + # Put the installation_id back into the info + "installation_id": installation_id, + "id": obj_id, + **installation_info, + } + return token_to_use + + async def make_http_call( + self, + client, + method, + url, + body=None, + headers=None, + token_to_use=None, + statuses_to_retry=[502, 503, 504], + **args, + ) -> Response: + _headers = { + "Accept": "application/json", + "User-Agent": os.getenv("USER_AGENT", "Default"), + } + if token_to_use: + _headers["Authorization"] = "token %s" % token_to_use["key"] + _headers.update(headers or {}) + log_dict = {} + + method = (method or "GET").upper() + if url[0] == "/": + log_dict = dict( + event="api", + endpoint=url, + method=method, + bot=token_to_use.get("username"), + repo_slug=self.slug, + loggable_token=self.loggable_token(token_to_use), + ) + url = self.api_url + url + + url = url_concat(url, args).replace(" ", "%20") + + if url.startswith(self.api_url) and self.api_host_header is not None: + _headers["Host"] = self.api_host_header + elif url.startswith(self.service_url) and self.host_header is not None: + _headers["Host"] = self.host_header + + kwargs = dict( + json=body if body else None, headers=_headers, follow_redirects=False + ) + max_number_retries = 3 + tried_refresh = False + for current_retry in range(1, max_number_retries + 1): + retry_reason = "retriable_status" + try: + res = await client.request(method, url, **kwargs) + if current_retry > 1: + # count retries without getting a url + self.count_and_get_url_template(url_name="make_http_call_retry") + logged_body = None + if res.status_code >= 300 and res.text is not None: + logged_body = res.text + log.log( + logging.WARNING if res.status_code >= 300 else logging.INFO, + "Github HTTP %s", + res.status_code, + extra=dict( + current_retry=current_retry, + body=logged_body, + rl_remaining=res.headers.get("X-RateLimit-Remaining"), + rl_limit=res.headers.get("X-RateLimit-Limit"), + rl_reset_time=res.headers.get("X-RateLimit-Reset"), + retry_after=res.headers.get("Retry-After"), + gh_request_id=res.headers.get("x-github-request-id"), + **log_dict, + ), + ) + except (httpx.TimeoutException, httpx.NetworkError): + if current_retry < max_number_retries: + log.warning( + "GitHub was not able to be reached, retrying", + extra=dict( + current_retry=current_retry, + **log_dict, + ), + ) + continue + else: + raise TorngitServerUnreachableError( + "GitHub was not able to be reached." + ) + # Github doesn't have any specific message for trying to use an expired token + # on top of that they return 404 for certain endpoints (not 401). + # So this is the little heuristics that we follow to decide on refreshing a token + if ( + # Only try to refresh once + not tried_refresh + # If there's no self._on_token_refresh then the token being used is probably from integration + # and therefore can't be refreshed (i.e. it's not a user-to-server request) + and callable(self._on_token_refresh) + # Exclude the check to see if is_student from refreshes + and url.startswith(self.api_url) + # Requests that can potentially have failed due to token expired + and ( + (res.status_code == 401) + or (res.status_code == 404 and f"/repos/{self.slug}/" in url) + ) + ): + tried_refresh = True + # Refresh token and retry + log.debug("Token is invalid. Refreshing") + token = await self.refresh_token(client, url) + if token is not None: + # Assuming we could retry and the retry was successful + # Update headers and retry + prefix, _ = _headers["Authorization"].split(" ") + _headers["Authorization"] = f"{prefix} {token['key']}" + if not self._on_token_refresh: + sentry_sdk.capture_message( + "Refreshed github token but no on_token_refresh callback defined" + ) + await self._on_token_refresh(token) + # Skip the rest of the validations and try again. + # It does consume one of the retries + retry_reason = "token_was_refreshed" + continue + # Rate limit errors - we might fallback on other available tokens and retry + # If we do fallback the token with rate limit is marked as 'rate limited' in Redis + elif (res.status_code == 403 or res.status_code == 429) and ( + # Primary rate limit + int(res.headers.get("X-RateLimit-Remaining", -1)) == 0 + or + # Secondary rate limit + res.headers.get("Retry-After") is not None + ): + is_primary_rate_limit = ( + int(res.headers.get("X-RateLimit-Remaining", -1)) == 0 + ) + + # ! side effect: mark current token as rate limited + retry_after = res.headers.get("Retry-After") + retry_in_seconds = int(retry_after) if retry_after is not None else None + reset_timestamp = res.headers.get("X-RateLimit-Reset") + self._possibly_mark_current_entity_as_rate_limited( + reset_timestamp=reset_timestamp, + retry_in_seconds=retry_in_seconds, + token=token_to_use, + ) + + fallback_token_key = self._get_next_fallback_token() + if fallback_token_key: + # Update header and try again + # Consumes one of the retries + prefix, _ = _headers["Authorization"].split(" ") + _headers["Authorization"] = f"{prefix} {fallback_token_key}" + retry_reason = "fallback_token_attempt" + continue + else: + message = f"Github API rate limit error: {res.reason_phrase if is_primary_rate_limit else 'secondary rate limit'}" + raise TorngitRateLimitError( + response_data=res.text, + message=message, + reset=res.headers.get("X-RateLimit-Reset"), + retry_after=( + int(retry_after) if retry_after is not None else None + ), + ) + if ( + not statuses_to_retry + or res.status_code not in statuses_to_retry + or current_retry >= max_number_retries # Last retry + ): + if res.status_code == 599: + raise TorngitServerUnreachableError( + "Github was not able to be reached, server timed out." + ) + elif res.status_code >= 500: + raise TorngitServer5xxCodeError("Github is having 5xx issues") + elif res.status_code == 401: + message = f"Github API unauthorized error: {res.reason_phrase}" + raise TorngitUnauthorizedError( + response_data=res.text, message=message + ) + elif res.status_code >= 300: + message = f"Github API: {res.reason_phrase}" + raise TorngitClientGeneralError( + res.status_code, response_data=res.text, message=message + ) + return res + else: + log.info( + "Retrying request to GitHub", + extra=dict( + status=res.status_code, retry_reason=retry_reason, **log_dict + ), + ) + + async def refresh_token( + self, client: httpx.AsyncClient, original_url: str + ) -> Optional[OauthConsumerToken]: + """ + This function requests a refresh token from Github. + The refresh_token value is stored as part of the oauth token dict. + + ! side effect: updates the self._token value + ! raises TorngitCantRefreshTokenError + ! raises TorngitRefreshTokenFailedError + """ + creds_from_token = self._oauth_consumer_token() + creds_to_send = dict( + client_id=creds_from_token["key"], client_secret=creds_from_token["secret"] + ) + + if self.token.get("refresh_token") is None: + log.warning("Trying to refresh Github token with no refresh_token saved") + return None + + # https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens#refreshing-a-user-access-token-with-a-refresh-token + # Returns response as application/x-www-form-urlencoded + params = urlencode( + dict( + refresh_token=self.token["refresh_token"], + grant_type="refresh_token", + **creds_to_send, + ) + ) + url = ( + self.service_url + + self.count_and_get_url_template(url_name="refresh_token").substitute() + ) + res = await client.request( + "POST", + url, + params=params, + ) + if res.status_code >= 300: + raise TorngitRefreshTokenFailedError( + dict( + status_code=res.status_code, + response_text=res.text, + original_url=original_url, + ) + ) + response_text = self._parse_response(res) + session = parse_qs(response_text) + + if session.get("access_token"): + self.set_token( + { + # parse_qs put values in a list for reasons + "key": session["access_token"][0], + "refresh_token": session["refresh_token"][0], + } + ) + return self.token + # https://docs.github.com/apps/managing-oauth-apps/troubleshooting-oauth-app-access-token-request-errors + log.error( + dict( + error="No access_token in response", + gh_error=session.get("error"), + gh_error_description=session.get("error_description"), + ) + ) + # Retunring None will let the code handle the request failure gracefully + # Instead of probably throwing 500 + return None + + # Generic + # ------- + async def get_branches(self, token=None): + async with self.get_client() as client: + token = self.get_token_by_type_if_none(token, TokenType.read) + # https://developer.github.com/v3/repos/#list-branches + page = 0 + branches = [] + while True: + page += 1 + url = self.count_and_get_url_template( + url_name="get_branches" + ).substitute(slug=self.slug) + res = await self.api( + client, + "get", + url, + per_page=100, + page=page, + token=token, + ) + if len(res) == 0: + break + branches.extend([(b["name"], b["commit"]["sha"]) for b in res]) + if len(res) < 100: + break + return branches + + async def get_branch(self, branch_name: str, token=None): + async with self.get_client() as client: + # https://docs.github.com/en/rest/branches/branches?apiVersion=2022-11-28#get-a-branch + url = self.count_and_get_url_template(url_name="get_branch").substitute( + slug=self.slug, branch_name=branch_name + ) + res = await self.api(client, "get", url) + return {"name": res["name"], "sha": res["commit"]["sha"]} + + async def get_authenticated_user(self, code): + creds = self._oauth_consumer_token() + async with self.get_client() as client: + url = ( + self.service_url + + self.count_and_get_url_template( + url_name="get_user_token" + ).substitute() + ) + response = await self.make_http_call( + client, + "get", + url, + code=code, + client_id=creds["key"], + client_secret=creds["secret"], + ) + session = self._parse_response(response) + + if session.get("access_token"): + # set current token + self.set_token( + dict( + key=session["access_token"], + # Refresh token only exists if the app is configured + # to have expiring tokens + refresh_token=session.get("refresh_token", None), + ) + ) + url = self.count_and_get_url_template( + url_name="get_authenticated_user" + ).substitute() + user = await self.api(client, "get", url) + user.update(session or {}) + email = user.get("email") + if not email: + url = self.count_and_get_url_template( + url_name="get_authenticated_user_email" + ).substitute() + emails = await self.api(client, "get", url) + emails = [e["email"] for e in emails if e["visibility"] == "public"] + user["email"] = emails[0] if emails else None + return user + + else: + if "error" in session: + # https://docs.github.com/en/apps/oauth-apps/maintaining-oauth-apps/troubleshooting-oauth-app-access-token-request-errors + log.error( + "Error fetching GitHub access token", + extra=dict( + error=session.get("error"), + error_description=session.get("error_description"), + error_uri=session.get("error_uri"), + ), + ) + return None + + async def get_is_admin(self, user, token=None): + async with self.get_client() as client: + # https://developer.github.com/v3/orgs/members/#get-organization-membership + url = self.count_and_get_url_template(url_name="get_is_admin").substitute( + owner_username=self.data["owner"]["username"], + user_username=user["username"], + ) + res = await self.api(client, "get", url, token=token) + return res["state"] == "active" and res["role"] == "admin" + + async def get_authenticated(self, token=None): + """Returns (can_view, can_edit)""" + # https://developer.github.com/v3/repos/#get + async with self.get_client() as client: + url = self.count_and_get_url_template( + url_name="get_authenticated" + ).substitute(slug=self.slug) + r = await self.api(client, "get", url, token=token) + ok = r["permissions"]["admin"] or r["permissions"]["push"] + return (True, ok) + + async def get_repository(self, token=None): + token = self.get_token_by_type_if_none(token, TokenType.read) + async with self.get_client() as client: + if self.data["repo"].get("service_id") is None: + # https://developer.github.com/v3/repos/#get + url = self.count_and_get_url_template( + url_name="get_repository_without_service_id" + ).substitute(slug=self.slug) + res = await self.api(client, "get", url, token=token) + else: + url = self.count_and_get_url_template( + url_name="get_repository_with_service_id" + ).substitute(service_id=self.data["repo"]["service_id"]) + res = await self.api(client, "get", url, token=token) + + username, repo = tuple(res["full_name"].split("/", 1)) + parent = res.get("parent") + + if parent: + fork = dict( + owner=dict( + service_id=str(parent["owner"]["id"]), + username=parent["owner"]["login"], + ), + repo=dict( + service_id=str(parent["id"]), + name=parent["name"], + language=self._validate_language(parent["language"]), + private=parent["private"], + branch=parent["default_branch"], + ), + ) + else: + fork = None + + return dict( + owner=dict(service_id=str(res["owner"]["id"]), username=username), + repo=dict( + service_id=str(res["id"]), + name=repo, + language=self._validate_language(res["language"]), + private=res["private"], + fork=fork, + branch=res["default_branch"] or "main", + ), + ) + + def _process_repository_page(self, page): + def process(repo): + return dict( + owner=dict( + service_id=str(repo["owner"]["id"]), + username=repo["owner"]["login"], + ), + repo=dict( + service_id=str(repo["id"]), + name=repo["name"], + language=self._validate_language(repo["language"]), + private=repo["private"], + branch=repo["default_branch"] or "main", + ), + ) + + return list(map(process, page)) + + async def _fetch_page_of_repos_using_installation( + self, client, page_size=100, page=1 + ): + # https://docs.github.com/en/rest/apps/installations?apiVersion=2022-11-28 + url = self.count_and_get_url_template( + url_name="fetch_page_of_repos_using_installation" + ).substitute(page_size=page_size, page=page) + res = await self.api( + client, + "get", + url, + headers={"Accept": "application/vnd.github.machine-man-preview+json"}, + ) + + repos = res.get("repositories", []) + + log.info( + "Fetched page of repos using installation", + extra=dict( + page_size=page_size, + page=page, + repo_names=[repo["name"] for repo in repos] if len(repos) > 0 else [], + ), + ) + + return self._process_repository_page(repos) + + async def _fetch_page_of_repos( + self, client, username, token, page_size=100, page=1 + ): + # https://developer.github.com/v3/repos/#list-your-repositories + if username is None: + url = self.count_and_get_url_template( + url_name="fetch_page_of_repos_without_username" + ).substitute(page_size=page_size, page=page) + repos = await self.api(client, "get", url, token=token) + else: + url = self.count_and_get_url_template( + url_name="fetch_page_of_repos_with_username" + ).substitute(username=username, page_size=page_size, page=page) + repos = await self.api(client, "get", url, token=token) + + log.info( + "Fetched page of repos", + extra=dict( + page_size=page_size, + page=page, + repo_names=[repo["name"] for repo in repos] if len(repos) > 0 else [], + username=username, + ), + ) + + return self._process_repository_page(repos) + + async def _get_owner_from_nodeid(self, client, token, owner_node_id: str): + query = self.graphql.prepare( + "OWNER_FROM_NODEID", variables={"node_id": owner_node_id} + ) + url = self.count_and_get_url_template( + url_name="get_owner_from_nodeid_graphql" + ).substitute() + res = await self.api(client, "post", url, body=query, token=token) + owner_data = res["data"]["node"] + return {"username": owner_data["login"], "service_id": owner_data["databaseId"]} + + async def get_repos_from_nodeids_generator( + self, repo_node_ids: List[str], expected_owner_username, *, token=None + ): + """Gets a list of repos from github graphQL API when the node_ids for the repos are known. + Also gets the owner info (also from graphQL API) if the owner is not the expected one. + The expected owner is one we are sure to have the info for available. + + Couldn't find how to use pagination with this endpoint, so we will implement it ourselves + believing that the max number of node_ids we can use is 100. + """ + token = self.get_token_by_type_if_none(token, TokenType.read) + owners_seen = dict() + async with self.get_client() as client: + max_index = len(repo_node_ids) + curr_index = 0 + page_size = 50 + while curr_index < max_index: + chunk = repo_node_ids[curr_index : curr_index + page_size] + curr_index += page_size + query = self.graphql.prepare( + "REPOS_FROM_NODEIDS", variables={"node_ids": chunk} + ) + url = self.count_and_get_url_template( + url_name="get_repos_from_nodeids_generator_graphql" + ).substitute() + res = await self.api(client, "post", url, body=query, token=token) + for raw_repo_data in res["data"]["nodes"]: + if ( + raw_repo_data is None + or raw_repo_data["__typename"] != "Repository" + ): + continue + primary_language = raw_repo_data.get("primaryLanguage") + default_branch = raw_repo_data.get("defaultBranchRef") + repo = { + "service_id": raw_repo_data["databaseId"], + "name": raw_repo_data["name"], + "language": self._validate_language( + primary_language.get("name") if primary_language else None + ), + "private": raw_repo_data["isPrivate"], + "branch": ( + default_branch.get("name") if default_branch else "main" + ), + "owner": { + "node_id": raw_repo_data["owner"]["id"], + "username": raw_repo_data["owner"]["login"], + }, + } + is_expected_owner = ( + repo["owner"]["username"] == expected_owner_username + ) + if not is_expected_owner: + ownerid = repo["owner"]["node_id"] + if ownerid not in owners_seen: + owner_info = await self._get_owner_from_nodeid( + client, token, ownerid + ) + owners_seen[ownerid] = owner_info + repo["owner"] = {**repo["owner"], **owners_seen[ownerid]} + + repo["owner"]["is_expected_owner"] = is_expected_owner + yield repo + + async def list_repos_using_installation(self, username=None): + """ + returns list of repositories included in this integration + """ + data = [] + page = 0 + page_size = 50 + async with self.get_client() as client: + while True: + page += 1 + repos = await self._fetch_page_of_repos_using_installation( + client, page=page, page_size=page_size + ) + + data.extend(repos) + + if len(repos) < page_size: + break + + return data + + async def list_repos_using_installation_generator(self, username=None): + """ + New version of list_repos_using_installation() that should replace the + old one after safely rolling out in the worker. + """ + async for page in self.list_repos_generator( + username=username, using_installation=True + ): + yield page + + async def list_repos(self, username=None, token=None): + """ + GitHub includes all visible repos through + the same endpoint. + """ + token = self.get_token_by_type_if_none(token, TokenType.read) + page = 0 + page_size = 50 + data = [] + async with self.get_client() as client: + while True: + page += 1 + repos = await self._fetch_page_of_repos( + client, username, token, page=page, page_size=page_size + ) + + data.extend(repos) + + if len(repos) < page_size: + break + + return data + + async def list_repos_generator( + self, username=None, token=None, using_installation=False + ): + """ + New version of list_repos() that should replace the old one after safely + rolling out in the worker. + """ + token = self.get_token_by_type_if_none(token, TokenType.read) + page_size = 50 + async with self.get_client() as client: + page = 0 + while True: + page += 1 + + repos = ( + await self._fetch_page_of_repos_using_installation( + client, page=page, page_size=page_size + ) + if using_installation + else await self._fetch_page_of_repos( + client, username, token, page=page, page_size=page_size + ) + ) + + yield repos + + if len(repos) < page_size: + break + + # GH App Installation + async def get_gh_app_installation(self, installation_id: int) -> Dict: + """ + Gets gh app installation from the source. + Reference: + https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#get-an-installation-for-the-authenticated-app + Args: + installation_id (int): Installation id belonging to the github app + Returns: + Dict: a dictionary that adheres to gh's response value in the link above + """ + jwt_token = get_github_jwt_token(service=self.service) + url = self.count_and_get_url_template( + url_name="get_gh_app_installation" + ).substitute(installation_id=installation_id) + headers = { + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {jwt_token}", + "X-GitHub-Api-Version": "2022-11-28", + } + + async with self.get_client() as client: + try: + return await self.api( + client, + "get", + url, + token={"key": jwt_token}, + headers=headers, + ) + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Cannot find gh app with installation_id {installation_id}", + ) + raise + + async def list_teams(self, token=None): + token = self.get_token_by_type_if_none(token, TokenType.admin) + # https://developer.github.com/v3/orgs/#list-your-organizations + page, data = 0, [] + async with self.get_client() as client: + while True: + page += 1 + url = self.count_and_get_url_template( + url_name="list_teams" + ).substitute() + orgs = await self.api(client, "get", url, page=page, token=token) + if len(orgs) == 0: + break + # organization names + for org in orgs: + try: + organization = org["organization"] + url = self.count_and_get_url_template( + url_name="list_teams_org_name" + ).substitute(login=organization["login"]) + org = await self.api(client, "get", url, token=token) # noqa: PLW2901 + data.append( + dict( + name=organization.get("name", org["login"]), + id=str(organization["id"]), + email=organization.get("email"), + username=organization["login"], + ) + ) + except TorngitClientGeneralError: + log.exception( + "Unable to load organization", + extra=dict(url=organization["url"]), + ) + if len(orgs) < 30: + break + + return data + + # Commits + # ------- + async def get_pull_request_commits(self, pullid, token=None): + token = self.get_token_by_type_if_none(token, TokenType.commit) + commits = await self._get_raw_pull_request_commits(pullid, token) + return [commit_info["sha"] for commit_info in commits] + + async def _get_raw_pull_request_commits(self, pullid, token): + # https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request + # NOTE limited to 250 commits + # NOTE page max size is 100 + # Which means we have to fetch at most 3 pages + all_commits = [] + MAX_RESULTS_PER_PAGE = 100 + async with self.get_client() as client: + for page_number in [1, 2, 3]: + url = self.count_and_get_url_template( + url_name="get_raw_pull_request_commits" + ).substitute( + slug=self.slug, + pullid=pullid, + max=MAX_RESULTS_PER_PAGE, + page_n=page_number, + ) + page_results = await self.api(client, "get", url, token=token) + if len(page_results): + all_commits.extend(page_results) + if len(page_results) < MAX_RESULTS_PER_PAGE: + break + return all_commits + + # Webhook + # ------- + async def post_webhook(self, name, url, events, secret, token=None): + token = self.get_token_by_type_if_none(token, TokenType.admin) + # https://developer.github.com/v3/repos/hooks/#create-a-hook + async with self.get_client() as client: + api_url = self.count_and_get_url_template( + url_name="post_webhook" + ).substitute(slug=self.slug) + res = await self.api( + client, + "post", + api_url, + body=dict( + name="web", + active=True, + events=events, + config=dict(url=url, secret=secret, content_type="json"), + ), + token=token, + ) + return res + + async def edit_webhook(self, hookid, name, url, events, secret, token=None): + token = self.get_token_by_type_if_none(token, TokenType.admin) + # https://developer.github.com/v3/repos/hooks/#edit-a-hook + try: + async with self.get_client() as client: + api_url = self.count_and_get_url_template( + url_name="edit_webhook" + ).substitute(slug=self.slug, hookid=hookid) + return await self.api( + client, + "patch", + api_url, + body=dict( + name="web", + active=True, + events=events, + config=dict(url=url, secret=secret, content_type="json"), + ), + token=token, + ) + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Cannot find webhook {hookid}", + ) + raise + + async def delete_webhook(self, hookid, token=None): + token = self.get_token_by_type_if_none(token, TokenType.admin) + # https://developer.github.com/v3/repos/hooks/#delete-a-hook + try: + async with self.get_client() as client: + url = self.count_and_get_url_template( + url_name="delete_webhook" + ).substitute(slug=self.slug, hookid=hookid) + await self.api(client, "delete", url, token=token) + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Cannot find webhook {hookid}", + ) + raise + return True + + # Comments + # -------- + async def post_comment(self, issueid, body, token=None): + token = self.get_token_by_type_if_none(token, TokenType.comment) + body = await self.build_comment_request_body(body, issueid) + # https://developer.github.com/v3/issues/comments/#create-a-comment + async with self.get_client() as client: + url = self.count_and_get_url_template(url_name="post_comment").substitute( + slug=self.slug, issueid=issueid + ) + res = await self.api(client, "post", url, body=body, token=token) + return res + + async def edit_comment(self, issueid, commentid, body, token=None): + token = self.get_token_by_type_if_none(token, TokenType.comment) + body = await self.build_comment_request_body(body, issueid) + # https://developer.github.com/v3/issues/comments/#edit-a-comment + try: + async with self.get_client() as client: + url = self.count_and_get_url_template( + url_name="edit_comment" + ).substitute(slug=self.slug, commentid=commentid) + res = await self.api(client, "patch", url, body=body, token=token) + return res + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Cannot find comment {commentid} from PR {issueid}", + ) + raise + + async def delete_comment(self, issueid, commentid, token=None): + token = self.get_token_by_type_if_none(token, TokenType.comment) + # https://developer.github.com/v3/issues/comments/#delete-a-comment + try: + async with self.get_client() as client: + url = self.count_and_get_url_template( + url_name="delete_comment" + ).substitute(slug=self.slug, commentid=commentid) + await self.api(client, "delete", url, token=token) + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Cannot find comment {commentid} from PR {issueid}", + ) + raise + return True + + # Commit Status + # ------------- + async def set_commit_status( + self, + commit, + status, + context, + description, + url, + merge_commit=None, + token=None, + coverage=None, + ): + # https://developer.github.com/v3/repos/statuses + token = self.get_token_by_type_if_none(token, TokenType.status) + assert status in ("pending", "success", "error", "failure"), "status not valid" + async with self.get_client() as client: + try: + api_url = self.count_and_get_url_template( + url_name="set_commit_status" + ).substitute(slug=self.slug, commit=commit) + res = await self.api( + client, + "post", + api_url, + body=dict( + state=status, + target_url=url, + context=context, + description=description, + ), + token=token, + ) + except TorngitClientError: + raise + if merge_commit: + api_url = self.count_and_get_url_template( + url_name="set_commit_status_merge_commit" + ).substitute(slug=self.slug, merge_commit=merge_commit[0]) + await self.api( + client, + "post", + api_url, + body=dict( + state=status, + target_url=url, + context=merge_commit[1], + description=description, + ), + token=token, + ) + return res + + async def get_commit_statuses(self, commit, token=None): + token = self.get_token_by_type_if_none(token, TokenType.status) + page = 0 + statuses = [] + async with self.get_client() as client: + while True: + page += 1 + # https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref + url = self.count_and_get_url_template( + url_name="get_commit_statuses" + ).substitute(slug=self.slug, commit=commit) + res = await self.api( + client, + "get", + url, + page=page, + per_page=100, + token=token, + ) + provided_statuses = res.get("statuses", []) + statuses.extend( + [ + { + "time": s["updated_at"], + "state": s["state"], + "description": s["description"], + "url": s["target_url"], + "context": s["context"], + } + for s in provided_statuses + ] + ) + if len(provided_statuses) < 100: + break + return Status(statuses) + + # Source + # ------ + @cache.cache_function(ttl=60 * 60) + async def get_source(self, path, ref, token=None): + token = self.get_token_by_type_if_none(token, TokenType.read) + # https://developer.github.com/v3/repos/contents/#get-contents + try: + async with self.get_client() as client: + url = self.count_and_get_url_template(url_name="get_source").substitute( + slug=self.slug, path=path.replace(" ", "%20") + ) + content = await self.api(client, "get", url, ref=ref, token=token) + + # When file size is greater than 1MB, content would not populate, + # instead we have to retrieve it from the download_url + if ( + not content.get("content") + and content.get("download_url") + and content.get("encoding") == "none" + ): + # not a templated url, count separately + self.count_and_get_url_template(url_name="get_source_again") + content["content"] = await self.api( + client=client, method="get", url=content["download_url"] + ) + else: + content["content"] = b64decode(content["content"]) + + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Path {path} not found at {ref}", + ) + raise + + return dict(content=content["content"], commitid=content["sha"]) + + @cache.cache_function(ttl=60 * 60) + async def get_commit_diff(self, commit, context=None, token=None): + token = self.get_token_by_type_if_none(token, TokenType.commit) + # https://developer.github.com/v3/repos/commits/#get-a-single-commit + try: + async with self.get_client() as client: + url = self.count_and_get_url_template( + url_name="get_commit_diff" + ).substitute(slug=self.slug, commit=commit) + res = await self.api( + client, + "get", + url, + headers={"Accept": "application/vnd.github.v3.diff"}, + token=token, + ) + except TorngitClientError as ce: + if ce.code == 422: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Commit with id {commit} does not exist", + ) + raise + return self.diff_to_json(res) + + @cache.cache_function(ttl=60 * 60) + async def get_compare( + self, base, head, context=None, with_commits=True, token=None + ): + token = self.get_token_by_type_if_none(token, TokenType.commit) + # https://developer.github.com/v3/repos/commits/#compare-two-commits + async with self.get_client() as client: + url = self.count_and_get_url_template(url_name="get_compare").substitute( + slug=self.slug, base=base, head=head + ) + res = await self.api(client, "get", url, token=token) + files = {} + for f in res["files"]: + diff = self.diff_to_json( + "diff --git a/%s b/%s%s\n%s\n%s\n%s" + % ( + f.get("previous_filename") or f.get("filename"), + f.get("filename"), + ( + "\ndeleted file mode 100644" + if f["status"] == "removed" + else "\nnew file mode 100644" + if f["status"] == "added" + else "" + ), + "--- " + + ( + "/dev/null" + if f["status"] == "new" + else ("a/" + f.get("previous_filename", f.get("filename"))) + ), + "+++ " + + ( + "/dev/null" + if f["status"] == "removed" + else ("b/" + f["filename"]) + ), + f.get("patch", ""), + ) + ) + files.update(diff["files"]) + + # commits are returned in reverse chronological order. ie [newest...oldest] + return dict( + diff=dict(files=files), + commits=[ + dict( + commitid=c["sha"], + message=c["commit"]["message"], + timestamp=c["commit"]["author"]["date"], + author=dict( + id=(c["author"] or {}).get("id"), + username=(c["author"] or {}).get("login"), + name=c["commit"]["author"]["name"], + email=c["commit"]["author"]["email"], + ), + ) + for c in ([res["base_commit"]] + res["commits"]) + ][::-1], + ) + + async def get_distance_in_commits( + self, base_branch, base, context=None, with_commits=True, token=None + ): + token = self.get_token_by_type_if_none(token, TokenType.commit) + # https://developer.github.com/v3/repos/commits/#compare-two-commits + async with self.get_client() as client: + url = self.count_and_get_url_template( + url_name="get_distance_in_commits" + ).substitute(slug=self.slug, base_branch=base_branch, base=base) + res = await self.api(client, "get", url, token=token) + behind_by = res.get("behind_by") + behind_by_commit = res["base_commit"]["sha"] if "base_commit" in res else None + if behind_by is None or behind_by_commit is None: + behind_by = None + behind_by_commit = None + return dict( + behind_by=behind_by, + behind_by_commit=behind_by_commit, + status=res.get("status"), + ahead_by=res.get("ahead_by"), + ) + + @cache.cache_function(ttl=60 * 60) + async def get_commit(self, commit, token=None): + token = self.get_token_by_type_if_none(token, TokenType.commit) + # https://developer.github.com/v3/repos/commits/#get-a-single-commit + try: + async with self.get_client() as client: + res = await self.api( + client, + "get", + "/repos/%s/commits/%s" % (self.slug, commit), + statuses_to_retry=[401], + token=token, + ) + except TorngitClientError as ce: + if ce.code == 422: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Commit with id {commit} does not exist", + ) + if ce.code == 404: + raise TorngitRepoNotFoundError( + response_data=ce.response_data, + message=f"Repo {self.slug} cannot be found by this user", + ) + raise + return dict( + author=dict( + id=str(res["author"]["id"]) if res["author"] else None, + username=res["author"]["login"] if res["author"] else None, + email=res["commit"]["author"].get("email"), + name=res["commit"]["author"].get("name"), + ), + commitid=commit, + parents=[p["sha"] for p in res["parents"]], + message=res["commit"]["message"], + timestamp=res["commit"]["committer"].get("date"), + ) + + # Pull Requests + # ------------- + def _pull(self, pull) -> ProviderPull: + return { + "author": { + "id": str(pull["user"]["id"]) if pull["user"] else None, + "username": pull["user"]["login"] if pull["user"] else None, + }, + "base": { + "branch": pull["base"]["ref"], + "commitid": pull["base"]["sha"], + "slug": pull["base"]["repo"]["full_name"], + }, + "head": { + "branch": pull["head"]["ref"], + "commitid": pull["head"]["sha"], + # Through empiric test data it seems that the "repo" key in "head" is set to None + # If the PR is from the same repo (e.g. not from a fork) + "slug": ( + pull["head"]["repo"]["full_name"] + if pull["head"]["repo"] + else pull["base"]["repo"]["full_name"] + ), + }, + "state": "merged" if pull["merged"] else pull["state"], + "title": pull["title"], + "id": str(pull["number"]), + "number": str(pull["number"]), + "labels": [label["name"] for label in pull.get("labels", [])], + "merge_commit_sha": pull["merge_commit_sha"] if pull["merged"] else None, + } + + async def get_pull_request(self, pullid, token=None) -> ProviderPull | None: + token = self.get_token_by_type_if_none(token, TokenType.pull) + # https://developer.github.com/v3/pulls/#get-a-single-pull-request + async with self.get_client() as client: + try: + url = self.count_and_get_url_template( + url_name="get_pull_request" + ).substitute(slug=self.slug, pullid=pullid) + res = await self.api(client, "get", url, token=token) + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Pull Request {pullid} not found", + ) + raise + commits = await self._get_raw_pull_request_commits(pullid, token) + commit_mapping = { + val["sha"]: [k["sha"] for k in val["parents"]] for val in commits + } + all_commits_in_pr = set([val["sha"] for val in commits]) + current_level = [res["head"]["sha"]] + while current_level and all(x in all_commits_in_pr for x in current_level): + new_level = [] + for x in current_level: + new_level.extend(commit_mapping[x]) + current_level = new_level + result = self._pull(res) + if current_level == [res["head"]["sha"]]: + log.warning( + "Head not found in PR. PR has probably too many commits to list all of them", + extra=dict(number_commits=len(commits), pullid=pullid), + ) + else: + possible_bases = [ + x for x in current_level if x not in all_commits_in_pr + ] + if possible_bases and result["base"]["commitid"] not in possible_bases: + log.info( + "Github base differs from original base", + extra=dict( + current_level=current_level, + github_base=result["base"]["commitid"], + possible_bases=possible_bases, + pullid=pullid, + ), + ) + result["base"]["commitid"] = possible_bases[0] + return result + + async def get_pull_requests(self, state="open", token=None): + token = self.get_token_by_type_if_none(token, TokenType.pull) + # https://developer.github.com/v3/pulls/#list-pull-requests + page, pulls = 0, [] + async with self.get_client() as client: + while True: + page += 1 + url = self.count_and_get_url_template( + url_name="get_pull_requests" + ).substitute(slug=self.slug) + res = await self.api( + client, + "get", + url, + page=page, + per_page=25, + state=state, + token=token, + ) + if len(res) == 0: + break + + pulls.extend([pull["number"] for pull in res]) + + if len(pulls) < 25: + break + + return pulls + + async def find_pull_request( + self, commit=None, branch=None, state="open", token=None + ): + if not self.slug or not commit: + return None + token = self.get_token_by_type_if_none(token, TokenType.pull) + async with self.get_client() as client: + # https://docs.github.com/en/rest/commits/commits#list-pull-requests-associated-with-a-commit + try: + url = self.count_and_get_url_template( + url_name="find_pull_request" + ).substitute(slug=self.slug, commit=commit) + res = await self.api(client, "get", url, token=token) + prs_with_commit = [ + data["number"] for data in res if data["state"] == state + ] + if prs_with_commit: + if len(prs_with_commit) > 1: + log.warning( + "Commit is referenced in multiple PRs.", + extra=dict( + prs=prs_with_commit, + commit=commit, + slug=self.slug, + state=state, + ), + ) + return prs_with_commit[0] + except TorngitClientGeneralError as exp: + if exp.code == 422: + return None + raise exp + + async def get_pull_request_files(self, pullid, token=None): + if not self.slug: + return None + token = self.get_token_by_type_if_none(token, TokenType.pull) + async with self.get_client() as client: + # https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests-files + try: + url = self.count_and_get_url_template( + url_name="get_pull_request_files" + ).substitute(slug=self.slug, pullid=pullid) + res = await self.api(client, "get", url, token=token) + filenames = [data.get("filename") for data in res] + return filenames + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"PR with id {pullid} does not exist", + ) + raise + + async def list_top_level_files(self, ref, token=None): + return await self.list_files(ref, dir_path="", token=None) + + @cache.cache_function(ttl=60 * 60) + async def list_files(self, ref, dir_path, token=None): + token = self.get_token_by_type_if_none(token, TokenType.read) + # https://developer.github.com/v3/repos/contents/#get-contents + if dir_path: + url = self.count_and_get_url_template( + url_name="list_files_with_dir_path" + ).substitute(slug=self.slug, dir_path=dir_path) + else: + url = self.count_and_get_url_template(url_name="list_files").substitute( + slug=self.slug + ) + async with self.get_client() as client: + content = await self.api(client, "get", url, ref=ref, token=token) + return [ + { + "name": f["name"], + "path": f["path"], + "type": self._github_type_to_torngit_type(f["type"]), + } + for f in content + ] + + def _github_type_to_torngit_type(self, val): + if val == "file": + return "file" + elif val == "dir": + return "folder" + return "other" + + async def get_ancestors_tree(self, commitid, token=None): + token = self.get_token_by_type_if_none(token, TokenType.commit) + async with self.get_client() as client: + url = self.count_and_get_url_template( + url_name="get_ancestors_tree" + ).substitute(slug=self.slug) + res = await self.api(client, "get", url, token=token, sha=commitid) + start = res[0]["sha"] + commit_mapping = {val["sha"]: [k["sha"] for k in val["parents"]] for val in res} + return self.build_tree_from_commits(start, commit_mapping) + + def get_external_endpoint(self, endpoint: Endpoints, **kwargs): + # used in parent obj to get_href + # I think this is for creating a clickable link, + # not a token-using call by us, so not counting these calls. + if endpoint == Endpoints.commit_detail: + return external_endpoint_template.substitute( + username=self.data["owner"]["username"], + name=self.data["repo"]["name"], + commitid=kwargs["commitid"], + ) + raise NotImplementedError() + + # Checks Docs: https://developer.github.com/v3/checks/ + + async def create_check_run( + self, check_name, head_sha, status="in_progress", token=None + ): + async with self.get_client() as client: + url = self.count_and_get_url_template( + url_name="create_check_run" + ).substitute(slug=self.slug) + res = await self.api( + client, + "post", + url, + body=dict(name=check_name, head_sha=head_sha, status=status), + token=token, + ) + return res["id"] + + async def get_check_runs( + self, check_suite_id=None, head_sha=None, name=None, token=None + ): + if check_suite_id is None and head_sha is None: + raise Exception( + "check_suite_id and head_sha parameter should not both be None" + ) + url = "" + if check_suite_id is not None: + url = self.count_and_get_url_template( + url_name="get_check_runs_with_check_suite_id" + ).substitute(slug=self.slug, check_suite_id=check_suite_id) + elif head_sha is not None: + url = self.count_and_get_url_template( + url_name="get_check_runs_with_head_sha" + ).substitute(slug=self.slug, head_sha=head_sha) + if name is not None: + url += f"?check_name={name}" + async with self.get_client() as client: + res = await self.api(client, "get", url, token=token) + return res + + async def get_check_suites(self, git_sha, token=None): + async with self.get_client() as client: + url = self.count_and_get_url_template( + url_name="get_check_suites" + ).substitute(slug=self.slug, git_sha=git_sha) + res = await self.api(client, "get", url, token=token) + return res + + # TODO: deprecated - favour the get_repos_with_languages_graphql() method instead + async def get_repo_languages(self, token=None) -> List[str]: + """ + Gets the languages belonging to this repository. + Reference: + https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#list-repository-languages + Returns: + List[str]: A list of language names + """ + async with self.get_client() as client: + url = self.count_and_get_url_template( + url_name="get_repo_languages" + ).substitute(slug=self.slug) + res = await self.api(client, "get", url, token=token) + return list(k.lower() for k in res.keys()) + + async def get_repos_with_languages_graphql( + self, owner_username: str, token=None, first=100 + ) -> dict[str, List[str]]: + """ + Gets the languages belonging to repositories of a specific owner. + Reference: + https://docs.github.com/en/graphql/reference/objects#repository + Returns: + dict[str, str]: A dictionary with repo_name: [languages] + """ + token = self.get_token_by_type_if_none(token, TokenType.read) + # Initially set to none and true + endCursor = None + hasNextPage = True + all_repositories = {} + + async with self.get_client() as client: + while hasNextPage: + query = self.graphql.prepare( + "REPO_LANGUAGES_FROM_OWNER", + variables={ + "owner": owner_username, + "cursor": endCursor, + "first": first, + }, + ) + url = self.count_and_get_url_template( + url_name="get_repos_with_languages_graphql" + ).substitute() + res = await self.api(client, "post", url, body=query, token=token) + repoOwner = res["data"]["repositoryOwner"] + if not repoOwner: + hasNextPage = False + else: + repositories = repoOwner["repositories"] + hasNextPage = repositories["pageInfo"]["hasNextPage"] + endCursor = repositories["pageInfo"]["endCursor"] + + for repo in repositories["nodes"]: + languages = repo["languages"]["edges"] + res_languages = [ + language["node"]["name"].lower() for language in languages + ] + + all_repositories[repo["name"]] = res_languages + + return all_repositories + + async def update_check_run( + self, + check_run_id, + conclusion, + status="completed", + output=None, + url=None, + token=None, + ): + body = dict(conclusion=conclusion, status=status, output=output) + if url: + body["details_url"] = url + async with self.get_client() as client: + api_url = self.count_and_get_url_template( + url_name="update_check_run" + ).substitute(slug=self.slug, check_run_id=check_run_id) + res = await self.api(client, "patch", api_url, body=body, token=token) + return res + + # Get information for a GitHub Actions build/workflow run + # ------------- + def actions_run_info(self, run): + """ + This method formats the API response from GitHub Actions + for any particular build/workflow run. All fields are relevant to + validating a tokenless response. + """ + public = True + if run["repository"]["private"]: + public = False + return dict( + start_time=run["created_at"], + finish_time=run["updated_at"], + status=run["status"], + public=public, + slug=run["repository"]["full_name"], + commit_sha=run["head_sha"], + ) + + async def get_workflow_run(self, run_id, token=None): + """ + GitHub defines a workflow and a run as the following properties: + Workflow = yaml with build configuration options + Run = one instance when the workflow was triggered + """ + async with self.get_client() as client: + url = self.count_and_get_url_template( + url_name="get_workflow_run" + ).substitute(slug=self.slug, run_id=run_id) + res = await self.api(client, "get", url, token=token) + return self.actions_run_info(res) + + def loggable_token(self, token) -> str: + """Gets a "loggable" version of the current repo token. + + The idea here is to get something in the logs that is enough for us to make comparisons like + "this log line is probably using the same token as this log line" + + But nothing else + + When there is a username, we will just log who owns that token + + For this, on the cases that there are no username, which is the case for integration tokens, + we are taking the token, mixing it with a secret that is present only in the code, + doing a sha256, base64-encoding and only logging the first 5 chars from it + (from the original 44 chars) + + This, added with the fact that each token is valid only for 1 hour, should be enough + for people not to be able to extract any useful information from it + + Returns: + str: A good enough string to tell tokens apart + """ + if token.get("username"): + username = token.get("username") + return f"{username}'s token" + if token is None or token.get("key") is None: + return "notoken" + installation_info = self.data.get("installation", {}) + if installation_info and "installation_id" in installation_info: + return f"GitHub_installation_{installation_info['installation_id']}" + some_secret = "v1CAF4bFYi2+7sN7hgS/flGtooomdTZF0+uGiigV3AY8f4HHNg".encode() + hasher = hashlib.sha256() + hasher.update(some_secret) + hasher.update(self.service.encode()) + if self.slug: + hasher.update(self.slug.encode()) + hasher.update(token.get("key").encode()) + return base64.b64encode(hasher.digest()).decode()[:5] + + async def get_best_effort_branches(self, commit_sha: str, token=None) -> List[str]: + """ + Gets a 'best effort' list of branches this commit is in. + If a branch is returned, this means this commit is in that branch. If not, it could still be + possible that this commit is in that branch + Args: + commit_sha (str): The sha of the commit we want to look at + Returns: + List[str]: A list of branch names + """ + token = self.get_token_by_type_if_none(token, TokenType.read) + url = self.count_and_get_url_template( + url_name="get_best_effort_branches" + ).substitute(slug=self.slug, commit_sha=commit_sha) + async with self.get_client() as client: + res = await self.api( + client, + "get", + url, + token=token, + headers={"Accept": "application/vnd.github.groot-preview+json"}, + ) + return [r["name"] for r in res] + + async def is_student(self): + async with self.get_client([3, 3]) as client: + try: + url = self.count_and_get_url_template( + url_name="is_student" + ).substitute() + res = await self.api(client, "get", url, statuses_to_retry=[]) + return res["student"] + except TorngitServerUnreachableError: + log.warning("Timeout on Github Education API for is_student") + return False + except (TorngitUnauthorizedError, TorngitServer5xxCodeError): + return False + + # GitHub App Webhook management + # ============================= + async def list_webhook_deliveries(self): + """ + Lists the webhook deliveries for the gh app. + docs: https://docs.github.com/en/rest/apps/webhooks?apiVersion=2022-11-28#list-deliveries-for-an-app-webhook + + This is a generator function that yields the pages from webhook deliveries until all have been requested. + Page size is 50. + """ + headers = { + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {self.token['key']}", + } + async with self.get_client() as client: + # self.count_and_get_url_template is called in paginated_api_generator + async for response in self.paginated_api_generator( + client, + "get", + url_name="list_webhook_deliveries", + headers=headers, + ): + yield response + + async def request_webhook_redelivery(self, delivery_id: str) -> bool: + """ + Request redelivery of a webhook from github app. Returns True if request is successful, False otherwise. + docs: https://docs.github.com/en/rest/apps/webhooks?apiVersion=2022-11-28#redeliver-a-delivery-for-an-app-webhook + """ + url = self.count_and_get_url_template( + url_name="request_webhook_redelivery" + ).substitute(delivery_id=delivery_id) + headers = { + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {self.token['key']}", + } + async with self.get_client() as client: + try: + await self.api(client, "post", url, headers=headers) + return True + except (TorngitClientError, TorngitServer5xxCodeError): + return False diff --git a/libs/shared/shared/torngit/github_enterprise.py b/libs/shared/shared/torngit/github_enterprise.py new file mode 100644 index 0000000000..e66f801132 --- /dev/null +++ b/libs/shared/shared/torngit/github_enterprise.py @@ -0,0 +1,29 @@ +import os + +from shared.config import get_config +from shared.torngit.github import GITHUB_API_ENDPOINTS, Github + + +class GithubEnterprise(Github): + # https://developer.github.com/v3/enterprise/#endpoint-urls + + @classmethod + def get_service_url(cls): + return get_config("github_enterprise", "url").strip("/") + + @classmethod + def get_api_url(cls): + if get_config("github_enterprise", "api_url"): + return get_config("github_enterprise", "api_url").strip("/") + return cls.get_service_url() + "/api/v3" + + @classmethod + def count_and_get_url_template(self, url_name): + # Github Enterprise uses the same urls as Github, but has a separate Counter + GITHUB_API_ENDPOINTS[url_name]["enterprise_counter"].inc() + return GITHUB_API_ENDPOINTS[url_name]["url_template"] + + service = "github_enterprise" + verify_ssl = os.getenv("GITHUB_ENTERPRISE_SSL_PEM") or ( + os.getenv("GITHUB_ENTERPRISE_VERIFY_SSL") != "FALSE" + ) diff --git a/libs/shared/shared/torngit/gitlab.py b/libs/shared/shared/torngit/gitlab.py new file mode 100644 index 0000000000..2be9b8c596 --- /dev/null +++ b/libs/shared/shared/torngit/gitlab.py @@ -0,0 +1,1438 @@ +import json +import logging +import os +from base64 import b64decode +from string import Template +from typing import List +from urllib.parse import quote, urlencode + +import httpx + +from shared.config import get_config +from shared.metrics import Counter +from shared.torngit.base import TokenType, TorngitBaseAdapter +from shared.torngit.enums import Endpoints +from shared.torngit.exceptions import ( + TorngitCantRefreshTokenError, + TorngitClientError, + TorngitClientGeneralError, + TorngitObjectNotFoundError, + TorngitRefreshTokenFailedError, + TorngitServer5xxCodeError, + TorngitServerUnreachableError, +) +from shared.torngit.response_types import ProviderPull +from shared.torngit.status import Status +from shared.typings.oauth_token_types import OauthConsumerToken, Token +from shared.utils.urls import url_concat + +log = logging.getLogger(__name__) + +METRICS_PREFIX = "services.torngit.gitlab" + + +GITLAB_API_CALL_COUNTER = Counter( + "git_provider_api_calls_gitlab", + "Number of times gitlab called this endpoint", + ["endpoint"], +) + + +# Gitlab Enterprise uses the same urls as Gitlab, but has a separate Counter +GITLAB_E_API_CALL_COUNTER = Counter( + "git_provider_api_calls_gitlab_enterprise", + "Number of times gitlab enterprise called this endpoint", + ["endpoint"], +) + + +GITLAB_API_ENDPOINTS = { + "fetch_and_handle_errors_retry": { + "counter": GITLAB_API_CALL_COUNTER.labels( + endpoint="fetch_and_handle_errors_retry" + ), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="fetch_and_handle_errors_retry" + ), + "url_template": "", # no url template, just counter + }, + "get_best_effort_branches": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_best_effort_branches"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="get_best_effort_branches" + ), + "url_template": Template( + "/projects/${service_id}/repository/commits/${commit_sha}/refs?type=branch" + ), + }, + "get_ancestors_tree": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_ancestors_tree"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="get_ancestors_tree" + ), + "url_template": Template("/projects/${service_id}/repository/commits"), + }, + "list_files": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="list_files"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels(endpoint="list_files"), + "url_template": Template("/projects/${service_id}/repository/tree"), + }, + "get_compare": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_compare"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels(endpoint="get_compare"), + "url_template": Template( + "/projects/${service_id}/repository/compare/?from=${base}&to=${head}" + ), + }, + "get_source": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_source"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels(endpoint="get_source"), + "url_template": Template("/projects/${service_id}/repository/files/${path}"), + }, + "get_repo_languages": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_repo_languages"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="get_repo_languages" + ), + "url_template": Template("/projects/${service_id}/languages"), + }, + "get_repository_without_service_id": { + "counter": GITLAB_API_CALL_COUNTER.labels( + endpoint="get_repository_without_service_id" + ), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="get_repository_without_service_id" + ), + "url_template": Template("/projects/${slug}"), + }, + "get_repository_with_service_id": { + "counter": GITLAB_API_CALL_COUNTER.labels( + endpoint="get_repository_with_service_id" + ), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="get_repository_with_service_id" + ), + "url_template": Template("/projects/${service_id}"), + }, + "get_commit_diff": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_commit_diff"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="get_commit_diff" + ), + "url_template": Template( + "/projects/${service_id}/repository/commits/${commit}/diff" + ), + }, + "get_is_admin": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_is_admin"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels(endpoint="get_is_admin"), + "url_template": Template("/groups/${service_id}/members/all/${user_id}"), + }, + "get_authenticated": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_authenticated"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="get_authenticated" + ), + "url_template": Template("/projects/${service_id}"), + }, + "find_pull_request_with_commit": { + "counter": GITLAB_API_CALL_COUNTER.labels( + endpoint="find_pull_request_with_commit" + ), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="find_pull_request_with_commit" + ), + "url_template": Template( + "/projects/${service_id}/repository/commits/${commit}/merge_requests" + ), + }, + "find_pull_request": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="find_pull_request"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="find_pull_request" + ), + "url_template": Template( + "/projects/${service_id}/merge_requests?state=${gitlab_state}" + ), + }, + "get_pull_requests": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_pull_requests"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="get_pull_requests" + ), + "url_template": Template( + "/projects/${service_id}/merge_requests?state=${state}" + ), + }, + "get_branch": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_branch"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels(endpoint="get_branch"), + "url_template": Template("/projects/${service_id}/repository/branches/${name}"), + }, + "get_branches": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_branches"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels(endpoint="get_branches"), + "url_template": Template("/projects/${service_id}/repository/branches"), + }, + "get_pull_request_commits": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_pull_request_commits"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="get_pull_request_commits" + ), + "url_template": Template( + "/projects/${service_id}/merge_requests/${pullid}/commits" + ), + }, + "get_commit": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_commit"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels(endpoint="get_commit"), + "url_template": Template( + "/projects/${service_id}/repository/commits/${commit}" + ), + }, + "get_authors": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_authors"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels(endpoint="get_authors"), + "url_template": Template("/users"), + }, + "delete_comment": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="delete_comment"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="delete_comment" + ), + "url_template": Template( + "/projects/${service_id}/merge_requests/${pullid}/notes/${commentid}" + ), + }, + "edit_comment": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="edit_comment"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels(endpoint="edit_comment"), + "url_template": Template( + "/projects/${service_id}/merge_requests/${pullid}/notes/${commentid}" + ), + }, + "post_comment": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="post_comment"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels(endpoint="post_comment"), + "url_template": Template( + "/projects/${service_id}/merge_requests/${pullid}/notes" + ), + }, + "get_commit_statuses": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_commit_statuses"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="get_commit_statuses" + ), + "url_template": Template( + "/projects/${service_id}/repository/commits/${commit}/statuses" + ), + }, + "set_commit_status": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="set_commit_status"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="set_commit_status" + ), + "url_template": Template("/projects/${service_id}/statuses/${commit}"), + }, + "set_commit_status_merge_commit": { + "counter": GITLAB_API_CALL_COUNTER.labels( + endpoint="set_commit_status_merge_commit" + ), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="set_commit_status_merge_commit" + ), + "url_template": Template("/projects/${service_id}/statuses/${merge_commit}"), + }, + "get_pull_request_files": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_pull_request_files"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="get_pull_request_files" + ), + "url_template": Template( + "/projects/${service_id}/merge_requests/${pullid}/diffs" + ), + }, + "get_pull_request": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_pull_request"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="get_pull_request" + ), + "url_template": Template("/projects/${service_id}/merge_requests/${pullid}"), + }, + "get_pull_request_get_commits": { + "counter": GITLAB_API_CALL_COUNTER.labels( + endpoint="get_pull_request_get_commits" + ), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="get_pull_request_get_commits" + ), + "url_template": Template( + "/projects/${service_id}/merge_requests/${pullid}/commits" + ), + }, + "get_pull_request_get_parent": { + "counter": GITLAB_API_CALL_COUNTER.labels( + endpoint="get_pull_request_get_parent" + ), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="get_pull_request_get_parent" + ), + "url_template": Template( + "/projects/${service_id}/repository/commits/${first_commit}" + ), + }, + "get_pipeline_details": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_pipeline_details"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="get_pipeline_details" + ), + "url_template": Template("/projects/${project_id}/jobs/${job_id}"), + }, + "list_repos_get_user": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="list_repos_get_user"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="list_repos_get_user" + ), + "url_template": Template("/user"), + }, + "list_repos_get_groups": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="list_repos_get_groups"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="list_repos_get_groups" + ), + "url_template": Template("/groups/${username}"), + }, + "list_repos_get_user_and_groups": { + "counter": GITLAB_API_CALL_COUNTER.labels( + endpoint="list_repos_get_user_and_groups" + ), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="list_repos_get_user_and_groups" + ), + "url_template": Template("/groups?per_page=100"), + }, + "list_repos_get_owned_projects": { + "counter": GITLAB_API_CALL_COUNTER.labels( + endpoint="list_repos_get_owned_projects" + ), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="list_repos_get_owned_projects" + ), + "url_template": Template("/projects?owned=true&per_page=50&page=${page}"), + }, + "list_repos_get_projects": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="list_repos_get_projects"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="list_repos_get_projects" + ), + "url_template": Template( + "/groups/${group_id}/projects?per_page=50&page=${page}" + ), + }, + "get_owner_info_from_repo": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_owner_info_from_repo"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="get_owner_info_from_repo" + ), + "url_template": Template("/users?username=${username}"), + }, + "delete_webhook": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="delete_webhook"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="delete_webhook" + ), + "url_template": Template("/projects/${service_id}/hooks/${hookid}"), + }, + "edit_webhook": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="edit_webhook"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels(endpoint="edit_webhook"), + "url_template": Template("/projects/${service_id}/hooks/${hookid}"), + }, + "post_webhook": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="post_webhook"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels(endpoint="post_webhook"), + "url_template": Template("/projects/${service_id}/hooks"), + }, + "get_authenticated_user": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="get_authenticated_user"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="get_authenticated_user" + ), + "url_template": Template("/oauth/token"), + }, + "refresh_token": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="refresh_token"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels( + endpoint="refresh_token" + ), + "url_template": Template("/oauth/token"), + }, + "list_teams": { + "counter": GITLAB_API_CALL_COUNTER.labels(endpoint="list_teams"), + "enterprise_counter": GITLAB_E_API_CALL_COUNTER.labels(endpoint="list_teams"), + "url_template": Template("/groups"), + }, +} + +# uncounted urls +external_endpoint_template = Template("${username}/${name}/commit/${commitid}") + + +class OwnerNotFoundError: + pass + + +class Gitlab(TorngitBaseAdapter): + service = "gitlab" + service_url = "https://gitlab.com" + api_url = "https://gitlab.com/api/v{}" + + @property + def redirect_uri(self): + from_config = get_config("gitlab", "redirect_uri", default=None) + if from_config is not None: + return from_config + base = get_config("setup", "codecov_url", default="https://codecov.io") + return base + "/login/gitlab" + + @classmethod + def count_and_get_url_template(cls, url_name): + GITLAB_API_ENDPOINTS[url_name]["counter"].inc() + return GITLAB_API_ENDPOINTS[url_name]["url_template"] + + async def fetch_and_handle_errors( + self, + client, + method, + url_path, + *, + body=None, + token: OauthConsumerToken | None = None, + version=4, + **args, + ): + if url_path.startswith("/"): + _log = dict( + event="api", + endpoint=url_path, + method=method, + bot=(token or self.token).get("username"), + ) + url_path = self.api_url.format(version) + url_path + else: + _log = {} + + headers = { + "Accept": "application/json", + "User-Agent": os.getenv("USER_AGENT", "Default"), + } + if isinstance(body, dict): + headers["Content-Type"] = "application/json" + body = json.dumps(body) + url = url_concat(url_path, args).replace(" ", "%20") + + max_retries = 2 + for current_retry in range(1, max_retries + 1): + if token or self.token: + headers["Authorization"] = "Bearer %s" % (token or self.token)["key"] + + try: + res = await client.request( + method.upper(), url, headers=headers, data=body + ) + if current_retry > 1: + # count retries without getting a url + self.count_and_get_url_template("fetch_and_handle_errors_retry") + logged_body = None + if res.status_code >= 300 and res.text is not None: + logged_body = res.text + log.log( + logging.WARNING if res.status_code >= 300 else logging.INFO, + "GitLab HTTP %s", + res.status_code, + extra=dict(body=logged_body, **_log), + ) + + if res.status_code == 599: + raise TorngitServerUnreachableError( + "Gitlab was not able to be reached, server timed out." + ) + elif res.status_code >= 500: + raise TorngitServer5xxCodeError("Gitlab is having 5xx issues") + elif ( + res.status_code == 401 + and res.json().get("error") == "invalid_token" + ): + # Refresh token and retry + log.debug("Token is invalid. Refreshing") + token = await self.refresh_token(client) + if callable(self._on_token_refresh): + await self._on_token_refresh(token) + elif res.status_code >= 400: + message = f"Gitlab API: {res.status_code}" + raise TorngitClientGeneralError( + res.status_code, response_data=res.json(), message=message + ) + else: + # Success case + return res + except (httpx.TimeoutException, httpx.NetworkError): + raise TorngitServerUnreachableError( + "GitLab was not able to be reached. Gateway 502. Please try again." + ) + + async def refresh_token(self, client: httpx.AsyncClient) -> OauthConsumerToken: + """ + This function requests a refresh token from GitLab. + The refresh_token value is stored as part of the oauth token dict. + + ! side effect: updates the self._token value + ! raises TorngitCantRefreshTokenError + ! raises TorngitRefreshTokenFailedError + """ + creds_from_token = self._oauth_consumer_token() + creds_to_send = dict( + client_id=creds_from_token["key"], client_secret=creds_from_token["secret"] + ) + + if self.token.get("refresh_token") is None: + raise TorngitCantRefreshTokenError( + "Token doesn't have refresh token information" + ) + + # https://docs.gitlab.com/ee/api/oauth2.html#authorization-code-flow + params = urlencode( + dict( + refresh_token=self.token["refresh_token"], + grant_type="refresh_token", + redirect_uri=self.redirect_uri, + **creds_to_send, + ) + ) + url = self.count_and_get_url_template("refresh_token").substitute() + res = await client.request( + "POST", self.service_url + url, data=params, params=params + ) + if res.status_code >= 300: + raise TorngitRefreshTokenFailedError(res) + content = res.json() + self.set_token( + { + "key": content["access_token"], + "refresh_token": content["refresh_token"], + } + ) + return self.token + + async def api(self, method, url_path, *, body=None, token=None, version=4, **args): + async with self.get_client() as client: + res = await self.fetch_and_handle_errors( + client, method, url_path, body=body, token=token, version=4, **args + ) + return None if res.status_code == 204 else res.json() + + async def make_paginated_call( + self, + base_url, + default_kwargs, + max_per_page, + counter_name, + max_number_of_pages=None, + token=None, + ): + current_page = None + has_more = True + count_so_far = 0 + + async with self.get_client() as client: + while has_more and ( + max_number_of_pages is None or count_so_far < max_number_of_pages + ): + current_kwargs = dict(per_page=max_per_page, **default_kwargs) + if current_page is not None: + current_kwargs["page"] = current_page + current_result = await self.fetch_and_handle_errors( + client, "GET", base_url, **current_kwargs + ) + count_so_far += 1 + if count_so_far > 1: + # count calls after initial call + self.count_and_get_url_template(counter_name) + yield ( + None if current_result.status_code == 204 else current_result.json() + ) + if ( + max_number_of_pages is not None + and count_so_far >= max_number_of_pages + ): + has_more = False + elif current_result.headers.get("X-Next-Page"): + current_page, has_more = current_result.headers["X-Next-Page"], True + else: + current_page, has_more = None, False + + async def get_authenticated_user(self, code, redirect_uri=None): + """ + Gets access_token and user's details from gitlab. + Exchanges the code for a proper access_token and refresh_token pair. + Gets user details from /user endpoint from GitLab. + Returns everything. + + Args: + code: the code to be redeemed for an access_token / refresh_token pair + redirect_uri: !deprecated. The uri to redirect to. Needs to match redirect_uri used to get the code. + """ + creds_from_token = self._oauth_consumer_token() + creds_to_send = dict( + client_id=creds_from_token["key"], client_secret=creds_from_token["secret"] + ) + redirect_uri = redirect_uri or self.redirect_uri + url = self.count_and_get_url_template("get_authenticated_user").substitute() + # http://doc.gitlab.com/ce/api/oauth2.html + res = await self.api( + "post", + self.service_url + url, + body=urlencode( + dict( + code=code, + grant_type="authorization_code", + redirect_uri=redirect_uri, + **creds_to_send, + ) + ), + ) + + self.set_token( + { + "key": res["access_token"], + "refresh_token": res["refresh_token"], + } + ) + user = await self.api("get", "/user") + user.update(res) + return user + + async def post_webhook(self, name, url, events, secret, token=None): + token = self.get_token_by_type_if_none(token, TokenType.admin) + # http://doc.gitlab.com/ce/api/projects.html#add-project-hook + api_path = self.count_and_get_url_template("post_webhook").substitute( + service_id=self.data["repo"]["service_id"] + ) + res = await self.api( + "post", + api_path, + body=dict( + url=url, + enable_ssl_verification=( + self.verify_ssl if isinstance(self.verify_ssl, bool) else True + ), + token=secret, + **events, + ), + token=token, + ) + return res + + async def edit_webhook(self, hookid, name, url, events, secret, token=None): + token = self.get_token_by_type_if_none(token, TokenType.admin) + # http://doc.gitlab.com/ce/api/projects.html#edit-project-hook + api_path = self.count_and_get_url_template("edit_webhook").substitute( + service_id=self.data["repo"]["service_id"], hookid=hookid + ) + return await self.api( + "put", + api_path, + body=dict( + url=url, + enable_ssl_verification=( + self.verify_ssl if isinstance(self.verify_ssl, bool) else True + ), + token=secret, + **events, + ), + token=token, + ) + + async def delete_webhook(self, hookid, token=None): + token = self.get_token_by_type_if_none(token, TokenType.admin) + # http://docs.gitlab.com/ce/api/projects.html#delete-project-hook + url = self.count_and_get_url_template("delete_webhook").substitute( + service_id=self.data["repo"]["service_id"], hookid=hookid + ) + try: + await self.api( + "delete", + url, + token=token, + ) + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Webhook with id {hookid} does not exist", + ) + raise + return True + + def diff_to_json(self, diff): + if isinstance(diff, list): + for d in diff: + mode = "" + if d["deleted_file"]: + mode = "deleted file mode\n" + d["diff"] = ( + ("diff --git a/%(old_path)s b/%(new_path)s\n" % d) + + mode + + d["diff"] + ) + return super().diff_to_json("\n".join(map(lambda a: a["diff"], diff))) + else: + return super().diff_to_json(self, diff) + + async def get_owner_info_from_repo(self, repo, token=None): + if repo.get("owner"): + service_id = repo["owner"]["id"] + username = repo["owner"]["username"].replace("/", ":") + elif repo["namespace"]["kind"] == "user": + # we need to get the user id (namespace id != user id) + username = repo["namespace"]["path"] + url = self.count_and_get_url_template( + "get_owner_info_from_repo" + ).substitute(username=username) + user_info = await self.api("get", url, token=token) + service_id = user_info[0].get("id") if user_info[0] else None + elif repo["namespace"]["kind"] == "group": + # we will use the namespace id as its the same as the group/subgroup id + service_id = repo["namespace"]["id"] + username = repo["namespace"]["full_path"].replace( + "/", ":" + ) # full path required for subgroup support + else: + raise OwnerNotFoundError() + + return (service_id, username) + + async def list_repos(self, username=None, token=None): + """ + V4 will return ALL projects, so we need to loop groups first + """ + user_url = self.count_and_get_url_template("list_repos_get_user").substitute() + user = await self.api("get", user_url, token=token) + user["is_user"] = True + if username: + if username.lower() == user["username"].lower(): + # just me + groups = [user] + else: + # a group + groups_url = self.count_and_get_url_template( + "list_repos_get_groups" + ).substitute(username=username) + groups = [(await self.api("get", groups_url, token=token))] + else: + # user and all groups + url = self.count_and_get_url_template( + "list_repos_get_user_and_groups" + ).substitute() + groups = await self.api("get", url, token=token) + groups.append(user) + + data = [] + for group in groups: + page = 0 + while True: + page += 1 + # http://doc.gitlab.com/ce/api/projects.html#projects + if group.get("is_user"): + url = self.count_and_get_url_template( + "list_repos_get_owned_projects" + ).substitute(page=page) + repos = await self.api("get", url, token=token) + else: + try: + url = self.count_and_get_url_template( + "list_repos_get_projects" + ).substitute(group_id=group["id"], page=page) + repos = await self.api("get", url, token=token) + except TorngitClientError as e: + if e.code == 404: + log.warning(f"Group with id {group['id']} does not exist") + repos = [] + for repo in repos: + ( + owner_service_id, + owner_username, + ) = await self.get_owner_info_from_repo(repo, token) + + # Gitlab API will return a repo with one of: no default branch key, default_branch: None, or default_branch: 'some_branch' + branch = "main" + if "default_branch" in repo and repo["default_branch"] is not None: + branch = repo.get("default_branch") + else: + log.warning( + "Repo doesn't have default_branch, using main instead", + extra=dict(repo=repo), + ) + + data.append( + dict( + owner=dict( + service_id=str(owner_service_id), + username=owner_username, + ), + repo=dict( + service_id=str(repo["id"]), + name=repo["path"], + private=(repo["visibility"] != "public"), + language=None, + branch=branch, + ), + ) + ) + if len(repos) < 50: + break + + # deduplicate, since some of them might show up twice + data = [i for n, i in enumerate(data) if i not in data[n + 1 :]] + return data + + async def list_repos_generator(self, username=None, token=None): + """ + Unlike GitHub and Bitbucket, GitLab has to pull a complete list of repos + from multiple endpoints which can return overlapping results. We can + still yield a page at a time through a generator to be consistent with + the other providers, but we have to pre-fetch all of the pages to remove + duplicates and then return slice after slice. + """ + repos = await self.list_repos(username, token) + page_size = 100 + for i in range(0, len(repos), page_size): + yield repos[i : i + page_size] + + async def get_pipeline_details( + self, project_id: int, job_id: int, token: Token | None = None + ) -> str | None: + token_to_use = self.get_token_by_type_if_none(token, TokenType.read) + url = self.count_and_get_url_template("get_pipeline_details").substitute( + project_id=project_id, job_id=job_id + ) + try: + result = await self.api("get", url, token=token_to_use) + return result.get("pipeline", {}).get("sha") + except TorngitClientError as err: + log.warning("Failed to get pipeline details", extra=dict(error=err)) + return None + + async def list_teams(self, token=None): + # https://docs.gitlab.com/ce/api/groups.html#list-groups + all_groups = [] + url = self.count_and_get_url_template("list_teams").substitute() + async_generator = self.make_paginated_call( + url, + max_per_page=100, + default_kwargs={}, + token=token, + counter_name="list_teams", + ) + async for page in async_generator: + groups = page + all_groups.extend( + [ + dict( + name=g["name"], + id=g["id"], + username=(g["full_path"].replace("/", ":")), + avatar_url=g["avatar_url"], + parent_id=g["parent_id"], + ) + for g in groups + ] + ) + return all_groups + + async def get_pull_request(self, pullid, token=None) -> ProviderPull | None: + # https://docs.gitlab.com/ce/api/merge_requests.html#get-single-mr + url = self.count_and_get_url_template("get_pull_request").substitute( + service_id=self.data["repo"]["service_id"], pullid=pullid + ) + try: + pull = await self.api("get", url, token=token) + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"PR with id {pullid} does not exist", + ) + raise + + if pull: + if pull.get("diff_refs", {}) and pull.get("diff_refs", {}).get("base_sha"): + parent = pull.get("diff_refs", {}).get("base_sha") + else: + log.info( + "Could not fetch pull base from diff_refs", + extra=dict(pullid=pullid, pull_information=pull), + ) + # get list of commits and first one out + url = self.count_and_get_url_template( + "get_pull_request_get_commits" + ).substitute(service_id=self.data["repo"]["service_id"], pullid=pullid) + all_commits = await self.api("get", url, token=token) + log.info( + "List of commits is fetched for PR calculation", + extra=dict( + commit_list=[ + {"id": c.get("id"), "parents": c.get("parent_ids")} + for c in all_commits + ] + ), + ) + first_commit = all_commits[-1] + if len(first_commit["parent_ids"]) > 0: + parent = first_commit["parent_ids"][0] + else: + # try querying the parent commit for this parent + url = self.count_and_get_url_template( + "get_pull_request_get_parent" + ).substitute( + service_id=self.data["repo"]["service_id"], + first_commit=first_commit["id"], + ) + parent = (await self.api("get", url, token=token))["parent_ids"][0] + + if pull["state"] == "locked": + pull["state"] = "closed" + + return ProviderPull( + author=dict( + id=str(pull["author"]["id"]) if pull["author"] else None, + username=pull["author"]["username"] if pull["author"] else None, + ), + base=dict(branch=pull["target_branch"] or "", commitid=parent), + head=dict(branch=pull["source_branch"] or "", commitid=pull["sha"]), + state=( + "open" if pull["state"] in ("opened", "reopened") else pull["state"] + ), + title=pull["title"], + id=str(pull["iid"]), + number=str(pull["iid"]), + merge_commit_sha=( + pull["merge_commit_sha"] if pull["state"] == "merged" else None + ), + ) + return None + + async def get_pull_request_files(self, pullid, token=None): + # https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-request-diffs + url = self.count_and_get_url_template("get_pull_request_files").substitute( + service_id=self.data["repo"]["service_id"], pullid=pullid + ) + try: + diffs = await self.api("get", url, token=token) + filenames = [data.get("new_path") for data in diffs] + return filenames + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"PR with id {pullid} does not exist", + ) + raise + + async def set_commit_status( + self, + commit, + status, + context, + description, + url, + coverage=None, + merge_commit=None, + token=None, + ): + token = self.get_token_by_type_if_none(token, TokenType.status) + # https://docs.gitlab.com/ce/api/commits.html#post-the-build-status-to-a-commit + status = dict(error="failed", failure="failed").get(status, status) + api_path = self.count_and_get_url_template("set_commit_status").substitute( + service_id=self.data["repo"]["service_id"], commit=commit + ) + try: + res = await self.api( + "post", + api_path, + body=dict( + state=status, + target_url=url, + coverage=coverage, + name=context, + description=description, + ), + token=token, + ) + except TorngitClientError: + raise + + if merge_commit: + api_path = self.count_and_get_url_template( + "set_commit_status_merge_commit" + ).substitute( + service_id=self.data["repo"]["service_id"], merge_commit=merge_commit[0] + ) + await self.api( + "post", + api_path, + body=dict( + state=status, + target_url=url, + coverage=coverage, + name=merge_commit[1], + description=description, + ), + token=token, + ) + return res + + async def get_commit_statuses(self, commit, _merge=None, token=None): + # http://doc.gitlab.com/ce/api/commits.html#get-the-status-of-a-commit + url = self.count_and_get_url_template("get_commit_statuses").substitute( + service_id=self.data["repo"]["service_id"], commit=commit + ) + statuses_response = await self.api("get", url, token=token) + + _states = dict( + pending="pending", + running="pending", + success="success", + error="failure", + failed="failure", + canceled="failure", + created="pending", + manual="pending", + skipped="success", + waiting_for_resource="pending", + # These aren't on Github documentation but keeping here in case they're used somewhere + # see https://github.com/codecov/shared/pull/30/ for context + cancelled="failure", + failure="failure", + ) + statuses = [ + { + "time": s.get("finished_at", s.get("created_at")), + "state": _states.get(s["status"]), + "description": s["description"], + "url": s.get("target_url"), + "context": s["name"], + } + for s in statuses_response + ] + + for idx, status in enumerate(statuses): + if status["time"] is None: + log.warning( + "Set a None time on Gitlab commit status", + extra=dict( + commit=commit, status=status, gitlab_data=statuses_response[idx] + ), + ) + if status["state"] is None: + log.warning( + "Set a None state on Gitlab commit status", + extra=dict( + commit=commit, status=status, gitlab_data=statuses_response[idx] + ), + ) + + return Status(statuses) + + async def post_comment(self, pullid, body, token=None): + token = self.get_token_by_type_if_none(token, TokenType.comment) + # http://doc.gitlab.com/ce/api/notes.html#create-new-merge-request-note + url = self.count_and_get_url_template("post_comment").substitute( + service_id=self.data["repo"]["service_id"], pullid=pullid + ) + return await self.api("post", url, body=dict(body=body), token=token) + + async def edit_comment(self, pullid, commentid, body, token=None): + token = self.get_token_by_type_if_none(token, TokenType.comment) + # http://doc.gitlab.com/ce/api/notes.html#modify-existing-merge-request-note + url = self.count_and_get_url_template("edit_comment").substitute( + service_id=self.data["repo"]["service_id"], + pullid=pullid, + commentid=commentid, + ) + try: + return await self.api("put", url, body=dict(body=body), token=token) + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Comment {commentid} in PR {pullid} does not exist", + ) + raise + + async def delete_comment(self, pullid, commentid, token=None): + token = self.get_token_by_type_if_none(token, TokenType.comment) + # https://docs.gitlab.com/ce/api/notes.html#delete-a-merge-request-note + url = self.count_and_get_url_template("delete_comment").substitute( + service_id=self.data["repo"]["service_id"], + pullid=pullid, + commentid=commentid, + ) + try: + await self.api("delete", url, token=token) + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Comment {commentid} in PR {pullid} does not exist", + ) + raise + return True + + async def get_commit(self, commit: str, token=None): + # http://doc.gitlab.com/ce/api/commits.html#get-a-single-commit + token = self.get_token_by_type_if_none(token, TokenType.read) + url = self.count_and_get_url_template("get_commit").substitute( + service_id=self.data["repo"]["service_id"], commit=commit + ) + try: + res = await self.api("get", url, token=token) + except TorngitClientError as ce: + if ce.code == 404: + message = f"Commit {commit} not found in repo {self.data['repo']['service_id']}" + raise TorngitObjectNotFoundError( + response_data=ce.response_data, message=message + ) + raise + # http://doc.gitlab.com/ce/api/users.html + email = res["author_email"] + name = res["author_name"] + _id = None + username = None + url = self.count_and_get_url_template("get_authors").substitute() + authors = await self.api("get", url, search=email or name, token=token) + if authors: + for author in authors: + if author["name"] == name or author.get("email") == email: + _id = authors[0]["id"] + username = authors[0]["username"] + name = authors[0]["name"] + break + + return dict( + author=dict(id=_id, username=username, email=email, name=name), + message=res["message"], + parents=res["parent_ids"], + commitid=commit, + timestamp=res["committed_date"], + ) + + async def get_pull_request_commits(self, pullid, token=None): + # http://doc.gitlab.com/ce/api/merge_requests.html#get-single-mr-commits + token = self.get_token_by_type_if_none(token, TokenType.read) + url = self.count_and_get_url_template("get_pull_request_commits").substitute( + service_id=self.data["repo"]["service_id"], pullid=pullid + ) + commits = await self.api("get", url, token=token) + return [c["id"] for c in commits] + + async def get_branches(self, token=None): + # http://doc.gitlab.com/ce/api/projects.html#list-branches + token = self.get_token_by_type_if_none(token, TokenType.read) + url = self.count_and_get_url_template("get_branches").substitute( + service_id=self.data["repo"]["service_id"] + ) + res = await self.api("get", url, token=token) + return [(b["name"], b["commit"]["id"]) for b in res] + + async def get_branch(self, name, token=None): + token = self.get_token_by_type_if_none(token, TokenType.read) + # https://docs.gitlab.com/ee/api/branches.html + url = self.count_and_get_url_template("get_branch").substitute( + service_id=self.data["repo"]["service_id"], name=name + ) + branch = await self.api("get", url, token=token) + return {"name": branch["name"], "sha": branch["commit"]["id"]} + + async def get_pull_requests(self, state="open", token=None): + token = self.get_token_by_type_if_none(token, TokenType.read) + # ONLY searchable by branch. + state = self.get_gitlab_pull_state_from_codecov_state(state=state) + # [TODO] pagination coming soon + # http://doc.gitlab.com/ce/api/merge_requests.html#list-merge-requests + url = self.count_and_get_url_template("get_pull_requests").substitute( + service_id=self.data["repo"]["service_id"], state=state + ) + res = await self.api("get", url, token=token) + # first check if the sha matches + return [pull["iid"] for pull in res] + + def get_gitlab_pull_state_from_codecov_state(self, state): + return {"merged": "merged", "open": "opened", "close": "closed"}.get( + state, "all" + ) + + async def find_pull_request( + self, commit=None, branch=None, state="open", token=None + ): + token = self.get_token_by_type_if_none(token, TokenType.read) + gitlab_state = self.get_gitlab_pull_state_from_codecov_state(state) + if commit is not None: + url = self.count_and_get_url_template( + "find_pull_request_with_commit" + ).substitute(service_id=self.data["repo"]["service_id"], commit=commit) + try: + res = await self.api("get", url, token=token) + if len(res) > 1: + log.info("More than one pull request associated with commit") + for possible_pull in res: + if possible_pull["state"] == gitlab_state or gitlab_state == "all": + return possible_pull["iid"] + except TorngitClientError: + log.warning("Unable to use new merge_requests endpoint") + + # [TODO] pagination coming soon + # http://doc.gitlab.com/ce/api/merge_requests.html#list-merge-requests + try: + url = self.count_and_get_url_template("find_pull_request").substitute( + service_id=self.data["repo"]["service_id"], gitlab_state=gitlab_state + ) + res = await self.api("get", url, token=token) + except TorngitClientError as e: + if e.code == 403: + # will get 403 if merge requests are disabled on gitlab + return None + raise + + # first check if the sha matches + if commit: + for pull in res: + if pull["sha"] == commit: + log.info( + "Unable to find PR from new endpoint, found from old one", + extra=dict(commit=commit), + ) + return pull["iid"] + + elif branch: + for pull in res: + if pull["source_branch"] and pull["source_branch"] == branch: + return pull["iid"] + + else: + return res[0]["iid"] + + async def get_authenticated(self, token=None): + # http://doc.gitlab.com/ce/api/projects.html#get-single-project + # http://doc.gitlab.com/ce/permissions/permissions.html + can_edit = False + url = self.count_and_get_url_template("get_authenticated").substitute( + service_id=self.data["repo"]["service_id"] + ) + try: + res = await self.api("get", url, token=token) + permission = max( + [ + (res["permissions"]["group_access"] or {}).get("access_level") or 0, + (res["permissions"]["project_access"] or {}).get("access_level") + or 0, + ] + ) + can_edit = permission > 20 + except TorngitClientError: + if self.data["repo"]["private"]: + raise + + return (True, can_edit) + + async def get_is_admin(self, user, token=None): + # https://docs.gitlab.com/ce/api/members.html#get-a-member-of-a-group-or-project-including-inherited-members + # 10 = > Guest access + # 20 = > Reporter access + # 30 = > Developer access + # 40 = > Maintainer access + # 50 = > Owner access # Only valid for groups + user_id = int(user["service_id"]) + url = self.count_and_get_url_template("get_is_admin").substitute( + service_id=self.data["owner"]["service_id"], user_id=user_id + ) + res = await self.api("get", url, token=token) + return bool(res["state"] == "active" and res["access_level"] > 39) + + async def get_commit_diff(self, commit, context=None, token=None): + token = self.get_token_by_type_if_none(token, TokenType.read) + # http://doc.gitlab.com/ce/api/commits.html#get-the-diff-of-a-commit + url = self.count_and_get_url_template("get_commit_diff").substitute( + service_id=self.data["repo"]["service_id"], commit=commit + ) + res = await self.api("get", url, token=token) + return self.diff_to_json(res) + + async def get_repository(self, token=None): + token = self.get_token_by_type_if_none(token, TokenType.read) + # https://docs.gitlab.com/ce/api/projects.html#get-single-project + if self.data["repo"].get("service_id") is None: + # convert from codecov ':' separator to gitlab '/' separator for groups/subgroups + slug = self.slug.replace(":", "/") + url = self.count_and_get_url_template( + "get_repository_without_service_id" + ).substitute(slug=slug.replace("/", "%2F")) + res = await self.api("get", url, token=token) + else: + url = self.count_and_get_url_template( + "get_repository_with_service_id" + ).substitute(service_id=self.data["repo"]["service_id"]) + res = await self.api("get", url, token=token) + + owner_service_id, owner_username = await self.get_owner_info_from_repo(res) + repo_name = res["path"] + return dict( + owner=dict(service_id=str(owner_service_id), username=owner_username), + repo=dict( + service_id=str(res["id"]), + private=res["visibility"] != "public", + language=None, + branch=(res["default_branch"] or "main"), + name=repo_name, + ), + ) + + async def get_repo_languages(self, token=None) -> List[str]: + """ + Gets the languages belonging to this repository. + Reference: + https://docs.gitlab.com/ee/api/projects.html#languages + Returns: + List[str]: A list of language names + """ + token = self.get_token_by_type_if_none(token, TokenType.read) + url = self.count_and_get_url_template("get_repo_languages").substitute( + service_id=self.data["repo"]["service_id"] + ) + res = await self.api("get", url, token=token) + return list(k.lower() for k in res.keys()) + + async def get_source(self, path, ref, token=None): + token = self.get_token_by_type_if_none(token, TokenType.read) + # https://docs.gitlab.com/ce/api/repository_files.html#get-file-from-repository + url = self.count_and_get_url_template("get_source").substitute( + service_id=self.data["repo"]["service_id"], + path=urlencode(dict(a=path), quote_via=quote)[2:], + ) + try: + res = await self.api( + "get", + url, + ref=ref, + token=token, + ) + except TorngitClientError as ce: + if ce.code == 404: + raise TorngitObjectNotFoundError( + response_data=ce.response_data, + message=f"Path {path} not found at {ref}", + ) + raise + + return dict(commitid=None, content=b64decode(res["content"])) + + async def get_compare( + self, base, head, context=None, with_commits=True, token=None + ): + token = self.get_token_by_type_if_none(token, TokenType.read) + # https://docs.gitlab.com/ee/api/repositories.html#compare-branches-tags-or-commits + url = self.count_and_get_url_template("get_compare").substitute( + service_id=self.data["repo"]["service_id"], base=base, head=head + ) + compare = await self.api("get", url, token=token) + + return dict( + diff=self.diff_to_json(compare["diffs"]), + commits=[ + dict( + commitid=c["id"], + message=c["title"], + timestamp=c["created_at"], + author=dict(email=c["author_email"], name=c["author_name"]), + ) + for c in compare["commits"] + ][::-1], + ) + + async def list_top_level_files(self, ref, token=None): + return await self.list_files(ref, dir_path="", token=None) + + async def list_files(self, ref, dir_path, token=None): + # https://docs.gitlab.com/ee/api/repositories.html#list-repository-tree + token = self.get_token_by_type_if_none(token, TokenType.read) + url = self.count_and_get_url_template("list_files").substitute( + service_id=self.data["repo"]["service_id"] + ) + async_generator = self.make_paginated_call( + base_url=url, + default_kwargs=dict(ref=ref, path=dir_path), + max_per_page=100, + token=token, + counter_name="list_files", + ) + all_results = [] + async for page in async_generator: + for res in page: + if res["type"] == "blob": + res["type"] = "file" + elif res["type"] == "tree": + res["type"] = "folder" + else: + res["type"] = "other" + all_results.append(res) + return all_results + + async def get_ancestors_tree(self, commitid, token=None): + token = self.get_token_by_type_if_none(token, TokenType.read) + url = self.count_and_get_url_template("get_ancestors_tree").substitute( + service_id=self.data["repo"]["service_id"] + ) + res = await self.api("get", url, token=token, ref_name=commitid) + start = res[0]["id"] + commit_mapping = {val["id"]: val["parent_ids"] for val in res} + return self.build_tree_from_commits(start, commit_mapping) + + def get_external_endpoint(self, endpoint: Endpoints, **kwargs): + # used in parent obj to get_href + # I think this is for creating a clickable link, + # not a token-using call by us, so not counting these calls. + if endpoint == Endpoints.commit_detail: + return external_endpoint_template.substitute( + username=self.data["owner"]["username"].replace(":", "/"), + name=self.data["repo"]["name"], + commitid=kwargs["commitid"], + ) + raise NotImplementedError() + + async def get_best_effort_branches(self, commit_sha: str, token=None) -> List[str]: + """ + Gets a 'best effort' list of branches this commit is in. + If a branch is returned, this means this commit is in that branch. If not, it could still be + possible that this commit is in that branch + Args: + commit_sha (str): The sha of the commit we want to look at + Returns: + List[str]: A list of branch names + """ + token = self.get_token_by_type_if_none(token, TokenType.read) + url = self.count_and_get_url_template("get_best_effort_branches").substitute( + service_id=self.data["repo"]["service_id"], commit_sha=commit_sha + ) + async_generator = self.make_paginated_call( + base_url=url, + default_kwargs=dict(), + max_per_page=100, + token=token, + counter_name="get_best_effort_branches", + ) + + all_results = [res["name"] async for page in async_generator for res in page] + return all_results + + async def is_student(self): + return False diff --git a/libs/shared/shared/torngit/gitlab_enterprise.py b/libs/shared/shared/torngit/gitlab_enterprise.py new file mode 100644 index 0000000000..3a723f1cd6 --- /dev/null +++ b/libs/shared/shared/torngit/gitlab_enterprise.py @@ -0,0 +1,44 @@ +import os + +from shared.config import get_config +from shared.torngit.gitlab import GITLAB_API_ENDPOINTS, Gitlab + + +class GitlabEnterprise(Gitlab): + service = "gitlab_enterprise" + + @classmethod + def count_and_get_url_template(cls, url_name): + # Gitlab Enterprise uses the same urls as Gitlab, but has a separate Counter + GITLAB_API_ENDPOINTS[url_name]["enterprise_counter"].inc() + return GITLAB_API_ENDPOINTS[url_name]["url_template"] + + @property + def redirect_uri(self): + from_config = get_config("gitlab_enterprise", "redirect_uri", default=None) + if from_config is not None: + return from_config + base = get_config("setup", "codecov_url", default="https://codecov.io") + return base + "/login/gle" + + @classmethod + def get_service_url(cls): + return get_config("gitlab_enterprise", "url") + + @property + def service_url(self): + return self.get_service_url() + + @classmethod + def get_api_url(cls): + if get_config("gitlab_enterprise", "api_url"): + return get_config("gitlab_enterprise", "api_url") + return cls.get_service_url() + "/api/v4" + + @property + def api_url(self): + return self.get_api_url() + + verify_ssl = os.getenv("GITLAB_ENTERPRISE_SSL_PEM") or ( + os.getenv("GITLAB_ENTERPRISE_VERIFY_SSL") != "FALSE" + ) diff --git a/libs/shared/shared/torngit/response_types.py b/libs/shared/shared/torngit/response_types.py new file mode 100644 index 0000000000..32795b70a1 --- /dev/null +++ b/libs/shared/shared/torngit/response_types.py @@ -0,0 +1,24 @@ +from typing import Literal, NotRequired, TypedDict + + +class ProviderAuthor(TypedDict): + id: str | None + username: str | None + + +class ProviderCommit(TypedDict): + branch: str + commitid: str + slug: NotRequired[str] # Only GitHub includes slug + + +class ProviderPull(TypedDict): + author: ProviderAuthor + base: ProviderCommit + head: ProviderCommit + state: Literal["open", "closed", "merged"] + title: str + id: str + number: str + labels: NotRequired[list[str]] # Only GitHub includes labels + merge_commit_sha: str | None diff --git a/libs/shared/shared/torngit/status.py b/libs/shared/shared/torngit/status.py new file mode 100644 index 0000000000..035dc80a76 --- /dev/null +++ b/libs/shared/shared/torngit/status.py @@ -0,0 +1,103 @@ +import re +from collections import defaultdict + + +def matches(string, pattern): + if pattern == string: + return True + else: + if "*" in pattern: + return re.match("^%s$" % pattern.replace("*", ".*"), string) is not None + return False + + +class Status(object): + def __init__(self, statuses): + self._statuses = self._fetch_most_relevant_status_per_context(statuses) + states = set(map(lambda s: s["state"], self._statuses)) + self._state = ( + "success" + if all(state == "success" for state in states) + else "failure" + if "failure" in states or "error" in states + else "pending" + if "pending" in states + else "failure" + ) + + @classmethod + def _fetch_most_relevant_status_per_context(cls, statuses): + # reduce based on time + contexts = defaultdict(list) + # group by context(time, !pending, ) + for status in statuses: + contexts[status["context"]].append( + (status["time"], status["state"] != "pending", status) + ) + # extract most recent sorted(time, !pending) + return [ + sorted(context, key=lambda t: (t[0] if t[0] is not None else "", t[1]))[-1][ + 2 + ] + for context in contexts.values() + ] + + def __sub__(self, context): + """Remove ci status from list, return new object""" + return Status( + filter(lambda s: not matches(s["context"], context), self._statuses) + ) + + def __eq__(self, other): + """Returns the current ci status""" + assert other in ("success", "failure", "pending") + return self._state == other + + def __str__(self): + """Returns the current ci status""" + return self._state + + @property + def is_pending(self): + return self._state == "pending" + + @property + def is_success(self): + return self._state == "success" + + @property + def is_failure(self): + return self._state == "failure" + + @property + def state(self): + return self._state + + def as_bool(self): + if self.is_success: + return True + elif self.is_failure: + return False + return None + + def __len__(self): + return len(self._statuses) + + def __contains__(self, context): + for c in self._statuses: + if matches(c["context"], context): + return True + return False + + def filter(self, method): + return Status(filter(method, self._statuses)) + + @property + def pending(self): + # return list of pending statuses + return [status for status in self._statuses if status["state"] == "pending"] + + def get(self, context): + for status in self._statuses: + if status["context"] == context: + return status diff --git a/libs/shared/shared/typings/__init__.py b/libs/shared/shared/typings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/typings/oauth_token_types.py b/libs/shared/shared/typings/oauth_token_types.py new file mode 100644 index 0000000000..8a485b8255 --- /dev/null +++ b/libs/shared/shared/typings/oauth_token_types.py @@ -0,0 +1,21 @@ +from typing import Awaitable, Callable, Optional, TypedDict + + +class Token(TypedDict): + key: str + # This information is used to identify the token owner in the logs, if present + username: str | None + # This represents the entity it belongs to. Entities can have the form of + # _ for Github Apps + # for Owners + # for mapped Tokens if available + # github_bot for Anonymous users + entity_name: str | None + + +class OauthConsumerToken(Token): + secret: Optional[str] + refresh_token: Optional[str] + + +OnRefreshCallback = Optional[Callable[[OauthConsumerToken], Awaitable[None]]] diff --git a/libs/shared/shared/typings/torngit.py b/libs/shared/shared/typings/torngit.py new file mode 100644 index 0000000000..b5c28319fc --- /dev/null +++ b/libs/shared/shared/typings/torngit.py @@ -0,0 +1,45 @@ +from typing import Dict, List, NotRequired, Optional, TypedDict, Union + +from shared.reports.types import UploadType + + +class OwnerInfo(TypedDict): + service_id: str + ownerid: Optional[int] + username: str + + +class RepoInfo(TypedDict): + name: str + using_integration: bool + service_id: str + repoid: int + private: Optional[bool] + + +class GithubInstallationInfo(TypedDict): + """ + Information about a Github installation. + `id` - The id of the GithubAppInstallation object in the database + If using the deprecated owner.integration_id it doesn't exist. + `installation_id` - Required info to get a token from Github for a given installation. + """ + + id: NotRequired[int] + installation_id: int + # The default app (configured via yaml) doesn't need `app_id` and `pem_path`. + # All other apps need `app_id` and `pem_path`. + app_id: NotRequired[int | None] + pem_path: NotRequired[str | None] + + +class AdditionalData(TypedDict): + upload_type: NotRequired[UploadType] + + +class TorngitInstanceData(TypedDict): + owner: Union[OwnerInfo, Dict] + repo: Union[RepoInfo, Dict] + fallback_installations: List[Optional[GithubInstallationInfo]] | None + installation: Optional[GithubInstallationInfo] + additional_data: Optional[AdditionalData] diff --git a/libs/shared/shared/upload/__init__.py b/libs/shared/shared/upload/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/upload/constants.py b/libs/shared/shared/upload/constants.py new file mode 100644 index 0000000000..63a3d75c88 --- /dev/null +++ b/libs/shared/shared/upload/constants.py @@ -0,0 +1,236 @@ +from enum import StrEnum + +ci = { + "travis": { + "title": "Travis-CI", + "icon": "travis", + "require_token_when_public": False, + "instructions": "travis", + "build_url": "https://travis-ci.com/{owner.username}/{repo.name}/jobs/{upload.job_code}", + }, + "azure_pipelines": { + "title": "Azure", + "icon": "azure_pipelines", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "docker": { + "title": "Docker", + "icon": "custom", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "buildbot": { + "title": "Buildbot", + "icon": "buildbot", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "cirrus-ci": { + "title": "Cirrus CI", + "icon": "cirrus-ci", + "require_token_when_public": False, + "instructions": "generic", + "build_url": "https://cirrus-ci.com/build/{upload.build_code}", + }, + "codebuild": { + "title": "AWS Codebuild", + "icon": "codebuild", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "codefresh": { + "title": "Codefresh", + "icon": "custom", + "require_token_when_public": True, + "instructions": "generic", + "build_url": "https://g.codefresh.io/repositories/{owner.username}/{repo.name}/builds/{upload.build_code}", + }, + "bitbucket": { + "title": "Bitbucket Pipelines", + "icon": "bitbucket", + "require_token_when_public": False, + "instructions": "generic", + "build_url": "https://bitbucket.org/{owner.username}/{repo.name}/addon/pipelines/home#!/results/{upload.job_code}", + }, + "circleci": { + "title": "CircleCI", + "icon": "circleci", + "require_token_when_public": False, + "instructions": "circleci", + "build_url": "https://circleci.com/{service_short}/{owner.username}/{repo.name}/{upload.build_code}#tests/containers/{upload.job_code}", + }, + "buddybuild": { + "title": "buddybuild", + "icon": "custom", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "buddy": { + "title": "buddy", + "icon": "custom", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "github-actions": { + "title": "GitHub Actions", + "icon": "github-actions", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "solano": { + "title": "Solano", + "icon": "custom", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "teamcity": { + "title": "TeamCity", + "icon": "teamcity", + "require_token_when_public": True, + "instructions": "teamcity", + "build_url": None, + }, + "appveyor": { + "title": "AppVeyor", + "icon": "appveyor", + "require_token_when_public": False, + "instructions": "appveyor", + "build_url": None, + }, + "wercker": { + "title": "Wercker", + "icon": "wercker", + "require_token_when_public": True, + "instructions": "generic", + "build_url": "https://app.wercker.com/#build/{upload.build_code}", + }, + "shippable": { + "title": "Shippable", + "icon": "shippable", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, # provided in upload, + }, + "codeship": { + "title": "Codeship", + "icon": "codeship", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, # provided in upload, + }, + "drone.io": { + "title": "Drone.io", + "icon": "drone.io", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, # provided in upload, + }, + "jenkins": { + "title": "Jenkins", + "icon": "jenkins", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, # provided in upload, + }, + "semaphore": { + "title": "Semaphore", + "icon": "semaphore", + "require_token_when_public": True, + "instructions": "generic", + "build_url": "https://semaphoreapp.com/{owner.username}/{repo.name}/branches/{commit.branch}/builds/{upload.build_code}", + }, + "gitlab": { + "title": "GitLab CI", + "icon": "gitlab", + "require_token_when_public": True, + "instructions": "generic", + "build_url": "https://gitlab.com/{owner.username}/{repo.name}/builds/{upload.build_code}", + }, + "bamboo": { + "title": "Bamboo", + "icon": "bamboo", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "buildkite": { + "title": "BuildKite", + "icon": "buildkite", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, # provided in upload, + }, + "bitrise": { + "title": "Bitrise", + "icon": "bitrise", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, # provided in upload, + }, + "greenhouse": { + "title": "Greenhouse", + "icon": "greenhouse", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "heroku": { + "title": "Heroku", + "icon": "heroku", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, + }, + "woodpecker": { + "title": "WoodpeckerCI", + "icon": "custom", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, # provided in upload, + }, + "custom": { + "title": "Custom", + "icon": "custom", + "require_token_when_public": True, + "instructions": "generic", + "build_url": None, # provided in upload, + }, +} + +errors = { + "travis": { + "tokenless-general-error": "\nERROR: Tokenless uploads are only supported for public repositories on Travis that can be verified through the Travis API. Please use an upload token if your repository is private and specify it via the -t flag. You can find the token for this repository at the url below on codecov.io (login required):\n\nRepo token: {}\nDocumentation: https://docs.codecov.io/docs/about-the-codecov-bash-uploader#section-upload-token", + "tokenless-stale-build": "\nERROR: The coverage upload was rejected because the build is out of date. Please make sure the build is not stale for uploads to process correctly.", + "tokenless-bad-status": "\nERROR: The build status does not indicate that the current build is in progress. Please make sure the build is in progress or was finished within the past 4 minutes to ensure reports upload properly.", + } +} + +global_upload_token_providers = [ + "github", + "github_enterprise", + "gitlab", + "gitlab_enterprise", + "bitbucket", + "bitbucket_server", +] + + +class UploadErrorCode(StrEnum): + FILE_NOT_IN_STORAGE = "file_not_in_storage" + REPORT_EXPIRED = "report_expired" + REPORT_EMPTY = "report_empty" + PROCESSING_TIMEOUT = "processing_timeout" + UNSUPPORTED_FILE_FORMAT = "unsupported_file_format" + + # We don't want these - try to add error cases when they arise + UNKNOWN_PROCESSING = "unknown_processing" + UNKNOWN_STORAGE = "unknown_storage" diff --git a/libs/shared/shared/upload/utils.py b/libs/shared/shared/upload/utils.py new file mode 100644 index 0000000000..4999fdee10 --- /dev/null +++ b/libs/shared/shared/upload/utils.py @@ -0,0 +1,68 @@ +from datetime import timedelta +from enum import Enum + +from django.db import transaction +from django.db.models import Q +from django.utils import timezone + +from shared.django_apps.codecov_auth.models import TrialStatus +from shared.django_apps.reports.models import ReportType +from shared.django_apps.user_measurements.models import UserMeasurement +from shared.plan.service import PlanService + + +class UploaderType(Enum): + LEGACY = "legacy" + CLI = "cli" + + +def query_monthly_coverage_measurements(plan_service: PlanService) -> int: + owner_id = plan_service.current_org.ownerid + queryset = UserMeasurement.objects.filter( + owner_id=owner_id, + private_repo=True, + created_at__gte=timezone.now() - timedelta(days=30), + report_type=ReportType.COVERAGE.value, + ) + if ( + plan_service.trial_status == TrialStatus.EXPIRED.value + and plan_service.has_trial_dates + ): + queryset = queryset.filter( + Q(created_at__gte=plan_service.trial_end_date) + | Q(created_at__lte=plan_service.trial_start_date) + ) + monthly_limit = plan_service.monthly_uploads_limit + return queryset[:monthly_limit].count() + + +def bulk_insert_coverage_measurements( + measurements: list[UserMeasurement], +) -> list[UserMeasurement]: + """ + This function takes measurements as input and bulk_creates them into the DB. + The atomic transaction ensures either all transactions are inserted or none + if there's an error + """ + with transaction.atomic(): + return UserMeasurement.objects.bulk_create(measurements) + + +def insert_coverage_measurement( + owner_id: int, + repo_id: int, + commit_id: int, + upload_id: int, + uploader_used: str, + private_repo: bool, + report_type: ReportType, +): + return UserMeasurement.objects.create( + repo_id=repo_id, + commit_id=commit_id, + upload_id=upload_id, + owner_id=owner_id, + uploader_used=uploader_used, + private_repo=private_repo, + report_type=report_type, + ) diff --git a/libs/shared/shared/utils/ReportEncoder.py b/libs/shared/shared/utils/ReportEncoder.py new file mode 100644 index 0000000000..988ab02482 --- /dev/null +++ b/libs/shared/shared/utils/ReportEncoder.py @@ -0,0 +1,28 @@ +import dataclasses +from decimal import Decimal +from fractions import Fraction +from json import JSONEncoder +from types import GeneratorType + +from shared.reports.types import ReportTotals + + +class ReportEncoder(JSONEncoder): + separators = (",", ":") + + def default(self, obj): + if dataclasses.is_dataclass(obj): + return obj.astuple() + elif isinstance(obj, Fraction): + return str(obj) + elif isinstance(obj, Decimal): + return str(obj) + elif isinstance(obj, ReportTotals): + # reduce totals + return obj.to_database() + elif hasattr(obj, "_encode"): + return obj._encode() + elif isinstance(obj, GeneratorType): + obj = list(obj) + # let the base class default method raise the typeerror + return JSONEncoder.default(self, obj) diff --git a/libs/shared/shared/utils/__init__.py b/libs/shared/shared/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/utils/enums.py b/libs/shared/shared/utils/enums.py new file mode 100644 index 0000000000..7f5bc0a20f --- /dev/null +++ b/libs/shared/shared/utils/enums.py @@ -0,0 +1,49 @@ +from enum import Enum + + +class CodecovDatabaseEnum(Enum): + @classmethod + def choices(cls): + return tuple((i.db_id, i.name) for i in cls) + + @classmethod + def enum_from_int(cls, value): + for elem in cls: + if elem.db_id == value: + return elem + return None + + +class TaskConfigGroup(Enum): + """ + Configuration Group for tasks. + Marks the config key in the install yaml that affects a given task. + """ + + archive = "archive" + cache_rollup = "cache_rollup" + comment = "comment" + commit_update = "commit_update" + compute_comparison = "compute_comparison" + daily = "daily" + delete_owner = "delete_owner" + flakes = "flakes" + flush_repo = "flush_repo" + healthcheck = "healthcheck" + label_analysis = "label_analysis" + new_user_activated = "new_user_activated" + notify = "notify" + profiling = "profiling" + pulls = "pulls" + send_email = "send_email" + static_analysis = "static_analysis" + status = "status" + sync_account = "sync_account" + sync_plans = "sync_plans" + sync_repos = "sync_repos" + sync_teams = "sync_teams" + sync_repo_languages = "sync_repo_languages" + sync_repo_languages_gql = "sync_repo_languages_gql" + timeseries = "timeseries" + upload = "upload" + test_results = "test_results" diff --git a/libs/shared/shared/utils/flare.py b/libs/shared/shared/utils/flare.py new file mode 100644 index 0000000000..d5191a7899 --- /dev/null +++ b/libs/shared/shared/utils/flare.py @@ -0,0 +1,82 @@ +import collections + +from shared.helpers.color import coverage_to_color + + +class Dict(dict): + def __getitem__(self, index): + if index in ("__l", "__h"): + return super(Dict, self).__getitem__(index) + found = self.get(index) + if not found: + found = self[index] = Dict(__l=0, __h=0) + return found + + def add_child(self, args, value): + i = self + i["__l"] += value[0] + i["__h"] += value[1] + for arg in args[:-1]: + i = i[arg] + i["__l"] += value[0] + i["__h"] += value[1] + i[args[-1]] = value + + +def _dict_to_children(n, d, color, classes): + if type(d) is tuple: + c = color(d[2]) + return dict( + name=n, + _class=classes.get(n), + lines=d[0], + coverage=d[2], + color=getattr(c, "hex", c), + ) + try: + coverage = float(d["__h"]) / float(d["__l"]) * 100.0 + except ZeroDivisionError: + coverage = 100 + + c = color(coverage) + + children = [ + _dict_to_children(key, value, color, classes) + for key, value in d.items() + if key not in ("__l", "__h") + ] + + if len(children) == 1 and children[0].get("children"): + # only one level, join it + children[0]["name"] = "%s/%s" % (n, children[0]["name"]) + return children[0] + + return dict( + coverage=coverage, + lines=d["__l"], + color=getattr(c, "hex", c), + _class=classes.get(n), + name=n, + children=children, + ) + + +def report_to_flare(files, color, classes=None): + flare = Dict(__l=0, __h=0) + fa = flare.add_child + for name, totals in files: + # fa(('path', 'to', 'file'), (lines, hits, coverage)) + fa(name.split("/"), (totals.lines, totals.hits, totals.coverage)) + + return [ + _dict_to_children( + "", + flare, + color + if isinstance(color, collections.abc.Callable) + else coverage_to_color(*color) + if color + else None, + classes or {}, + ) + ] diff --git a/libs/shared/shared/utils/make_network_file.py b/libs/shared/shared/utils/make_network_file.py new file mode 100644 index 0000000000..002d2a11fb --- /dev/null +++ b/libs/shared/shared/utils/make_network_file.py @@ -0,0 +1,8 @@ +from shared.reports.types import NetworkFile, ReportTotals + + +def make_network_file(totals, diff=None): + return NetworkFile( + ReportTotals(*totals) if totals else ReportTotals(), + ReportTotals(*diff) if diff else None, + ) diff --git a/libs/shared/shared/utils/match.py b/libs/shared/shared/utils/match.py new file mode 100644 index 0000000000..cfe2923c2a --- /dev/null +++ b/libs/shared/shared/utils/match.py @@ -0,0 +1,67 @@ +import re +from typing import Sequence + + +class Matcher: + def __init__(self, patterns: Sequence[str] | None): + self._patterns = set(patterns or []) + self._is_initialized = False + # a list of patterns that will result in `True` on a match + self._positives: list[re.Pattern] = [] + # a list of patterns that will result in `False` on a match + self._negatives: list[re.Pattern] = [] + + def _get_matchers(self) -> tuple[list[re.Pattern], list[re.Pattern]]: + if not self._is_initialized: + for pattern in self._patterns: + if not pattern: + continue + if pattern.startswith(("^!", "!")): + self._negatives.append(re.compile(pattern.replace("!", ""))) + else: + self._positives.append(re.compile(pattern)) + self._is_initialized = True + + return self._positives, self._negatives + + def match(self, s: str) -> bool: + if not self._patterns or s in self._patterns: + return True + + positives, negatives = self._get_matchers() + + # must not match + for pattern in negatives: + # matched a negative search + if pattern.match(s): + return False + + if positives: + for pattern in positives: + # match was found + if pattern.match(s): + return True + + # did not match any required paths + return False + + else: + # no positives: everything else is ok + return True + + def match_any(self, strings: Sequence[str] | None) -> bool: + if not strings: + return False + return any(self.match(s) for s in strings) + + +def match(patterns: Sequence[str] | None, string: str): + matcher = Matcher(patterns) + return matcher.match(string) + + +def match_any( + patterns: Sequence[str] | None, match_any_of_these: Sequence[str] | None +) -> bool: + matcher = Matcher(patterns) + return matcher.match_any(match_any_of_these) diff --git a/libs/shared/shared/utils/merge.py b/libs/shared/shared/utils/merge.py new file mode 100644 index 0000000000..7b9aba818d --- /dev/null +++ b/libs/shared/shared/utils/merge.py @@ -0,0 +1,362 @@ +from collections import defaultdict +from enum import IntEnum +from fractions import Fraction +from itertools import groupby +from typing import List, Optional + +from shared.reports.types import CoverageDatapoint, LineSession, ReportLine + + +def merge_all(coverages, missing_branches=None): + if len(coverages) == 1: + return coverages[0] + + cov = coverages[0] + for _ in coverages[1:]: + cov = merge_coverage(cov, _, missing_branches) + return cov + + +def merge_branch(b1, b2): + if b1 == b2: # 1/2 == 1/2 + return b1 + if b1 == -1 or b2 == -1: + return -1 + if isinstance(b1, int) and not isinstance(b1, bool) and b1 > 0: + return b1 + if isinstance(b2, int) and not isinstance(b2, bool) and b2 > 0: + return b2 + if b1 in (0, None, True): + return b2 + if b2 in (0, None, True): + return b1 + if isinstance(b1, list): + return b1 + if isinstance(b2, list): + return b2 + br1, br2 = b1.split("/", 1) + if br1 == br2: # equal 1/1 + return b1 + br3, br4 = b2.split("/", 1) + if br3 == br4: # equal 1/1 + return b2 + # return the greatest found + return "%s/%s" % ( + br1 if int(br1) > int(br3) else br3, + br2 if int(br2) > int(br4) else br4, + ) + + +def merge_partial_line(p1, p2): + if not p1 or not p2: + return p1 or p2 + + np = p1 + p2 + if len(np) == 1: + # one result already + return np + + fl = defaultdict(list) + [ + [fl[x].append(_c) for x in range(_s or 0, _e + 1)] + for _s, _e, _c in np + if _e is not None + ] + ks = list(fl.keys()) + mx = max(ks) + 1 if ks else 0 + # appends coverage on each column when [X, None, C] + [[fl[x].append(_c) for x in range(_s or 0, mx)] for _s, _e, _c in np if _e is None] + ks = list(fl.keys()) + # fl = {1: [1], 2: [1], 4: [0], 3: [1], 5: [0], 7: [0], 8: [0]} + pp = [] + append = pp.append + for cov, group in groupby( + sorted([(cl, max(cv)) for cl, cv in list(fl.items())]), lambda c: c[1] + ): + group = list(group) # noqa: PLW2901 + append(_ifg(group[0][0], group[-1][0], cov)) + + # never ends + if [[max(ks), None, _c] for _s, _e, _c in np if _e is None]: + pp[-1][1] = None + + return pp + + +def merge_coverage(l1, l2, branches_missing=True): + if l1 is None or l2 is None: + return l1 if l1 is not None else l2 + + elif l1 == -1 or l2 == -1: + # ignored line + return -1 + + l1t = cast_ints_float(l1) + l2t = cast_ints_float(l2) + + if isinstance(l1t, (float, Fraction)) and isinstance(l2t, (float, Fraction)): + return l1 if l1 >= l2 else l2 + + elif isinstance(l1t, str) or isinstance(l2t, str): + if isinstance(l1t, float): + # using or here because if l1 is 0 return l2 + # this will trigger 100% if l1 is > 0 + branches_missing = [] if l1 else False + l1 = l2 + + elif isinstance(l2t, float): + branches_missing = [] if l2 else False + + if branches_missing == []: + # all branches were hit, no need to merge them + l1 = l1.split("/")[-1] + return "%s/%s" % (l1, l1) + + elif isinstance(branches_missing, list): + # we know how many are missing + target = int(l1.split("/")[-1]) + bf = target - len(branches_missing) + return "%s/%s" % (bf if bf > 0 else 0, target) + + return merge_branch(l1, l2) + + elif isinstance(l1t, list) and isinstance(l2t, list): + return merge_partial_line(l1, l2) + + elif isinstance(l1t, bool) or isinstance(l2t, bool): + return (l2 or l1) if isinstance(l1t, bool) else (l1 or l2) + + return merge_coverage( + partials_to_line(l1) if isinstance(l1t, list) else l1, + partials_to_line(l2) if isinstance(l2t, list) else l2, + ) + + +def merge_missed_branches(sessions: list[LineSession]) -> list | None: + """ + Returns a list of missed branches, defined as the *intersection* of all + the missed branches of the input sessions. + + Returns `None` if there is no sessions or no missed branches in the sessions. + Returns an empty list (no missed branches) if the line was fully covered, + or the missed branches are disjoint. + """ + if not sessions or not any(s.branches is not None for s in sessions): + return None + + missed_branches: None | set = None + for session in sessions: + if session.branches is None: + if line_type(session.coverage) == LineType.hit: + return [] + continue + if missed_branches is None: + missed_branches = set(session.branches) + else: + missed_branches.intersection_update(session.branches) + if not missed_branches: + return [] + + return list(missed_branches) if missed_branches is not None else None + + +def merge_line(l1, l2, joined=True): + if not l1 or not l2: + return l1 or l2 + + # merge sessions + sessions = _merge_sessions(list(l1.sessions or []), list(l2.sessions or [])) + + return ReportLine.create( + type=l1.type or l2.type, + coverage=get_coverage_from_sessions(sessions) if joined else l1.coverage, + complexity=get_complexity_from_sessions(sessions) if joined else l1.complexity, + sessions=sessions, + messages=merge_messages(l1.messages, l2.messages), + datapoints=merge_datapoints(l1.datapoints, l2.datapoints), + ) + + +def merge_messages(m1, m2): + pass + + +def merge_datapoints( + d1: Optional[List[CoverageDatapoint]], d2: Optional[List[CoverageDatapoint]] +): + if d1 is None and d2 is None: + return None + # Remove duplicates + # str(dp) -> dp + index_of_dps = dict() + both_lists = filter(None, (d1 or []) + (d2 or [])) + for dp in both_lists: + key = str(dp) + index_of_dps[key] = dp + dps_no_duplicates = index_of_dps.values() + # the sorting doesn't really matter how as long as it is a consistent thing + return sorted( + dps_no_duplicates, + key=lambda x: x.key_sorting_tuple(), + ) + + +def merge_line_session(s1, s2): + s1b = s1.branches + s2b = s2.branches + if s1b is None and s2b is None: + # all are None, so return None + mb = None + elif s1b is None: + if line_type(s1.coverage) == 0: + # s1 was a hit, so we have no more branches to get + mb = [] + else: + mb = s2b + elif s2b is None: + if line_type(s2.coverage) == 0: + # s2 was a hit, so we have no more branches to get + mb = [] + else: + mb = s1b + else: + mb = list(set(s1b or []) & set(s2b or [])) + + s1p = s1.partials + s2p = s2.partials + partials = None + if s1p or s2p: + if s1p is None or s2p is None: + partials = s1p or s2p + else: + # list + list + partials = sorted(s1p + s2p, key=lambda p: p[0]) + + return LineSession( + s1.id, merge_coverage(s1.coverage, s2.coverage, mb), mb, partials + ) + + +def _merge_sessions(s1: list[LineSession], s2: list[LineSession]) -> list[LineSession]: + """Merges two lists of different sessions into one""" + if not s1 or not s2: + return s1 or s2 + + session_ids_1 = {s.id for s in s1} + session_ids_2 = {s.id for s in s2} + intersection = session_ids_1.intersection(session_ids_2) + if not intersection: + s1.extend(s2) + return s1 + + sessions_1 = {s.id: s for s in s1} + sessions_2 = {s.id: s for s in s2} + + # merge existing + for session_id in intersection: + sessions_1[session_id] = merge_line_session( + sessions_1[session_id], sessions_2.pop(session_id) + ) + + # add remaining new sessions + return list(sessions_1.values()) + list(sessions_2.values()) + + +def _ifg(s, e, c): + """ + s=start, e=end, c=coverage + Insures the end is larger then the start. + """ + return [s, e if e > s else s + 1, c] + + +def cast_ints_float(value): + """ + When doing a merge we'd like to convert all ints to floats. this method takes in a value and + converts it to flaot if type(value) is int + """ + return value if not isinstance(value, int) else float(value) + + +class LineType(IntEnum): + skipped = -1 + hit = 0 + miss = 1 + partial = 2 + + +def line_type(line) -> LineType | None: + """ + -1 = skipped (had coverage data, but fixed out) + 0 = hit + 1 = miss + 2 = partial + None = ignore (because it has messages or something) + """ + if line is True: + return LineType.partial + if isinstance(line, str): + return branch_type(line) + if line == -1: + return LineType.skipped + if line is False: + return None + if isinstance(line, Fraction): + if line == 0: + return LineType.miss + if line >= 1: + return LineType.hit + return LineType.partial + if line: + return LineType.hit + if line is not None: + return LineType.miss + return None + + +def branch_type(b): + """ + 0 = hit + 1 = miss + 2 = partial + """ + if "/" not in b: + if int(b) == 0: + return LineType.miss + return LineType.hit + b1, b2 = tuple(b.split("/", 1)) + return ( + LineType.hit if b1 == b2 else LineType.miss if b1 == "0" else LineType.partial + ) + + +def partials_to_line(partials): + """ + | . . . . . | + in: 1 1 1 0 0 + out: 1/2 + | . . . . . | + in: 1 0 1 0 0 + out: 2/4 + """ + ln = len(partials) + if ln == 1: + return partials[0][2] + v = sum([1 for (sc, ec, hits) in partials if hits > 0]) + return f"{v}/{ln}" + + +def get_complexity_from_sessions(sessions): + _type = type(sessions[0].complexity) + if _type is int: + return max([(s.complexity or 0) for s in sessions]) + elif _type in (tuple, list): + return ( + max([(s.complexity or (0, 0))[0] for s in sessions]), + max([(s.complexity or (0, 0))[1] for s in sessions]), + ) + + +def get_coverage_from_sessions(sessions): + new_coverages = [s.coverage for s in sessions] + return merge_all(new_coverages, merge_missed_branches(sessions)) diff --git a/libs/shared/shared/utils/migrate.py b/libs/shared/shared/utils/migrate.py new file mode 100644 index 0000000000..5d19b72489 --- /dev/null +++ b/libs/shared/shared/utils/migrate.py @@ -0,0 +1,53 @@ +from json import loads + +from shared.helpers.numeric import ratio + +TOTALS_MAP = ("f", "n", "h", "m", "p", "c", "b", "d", "M", "s", "C", "N", "diff") +TOTALS_MAP_NAMES = ( + "files", + "lines", + "hits", + "misses", + "partials", + "coverage", + "branches", + "methods", + "messages", + "sessions", + "complexity", + "complexity_total", + "diff", +) +TOTALS_MAP_v1 = ( + "files", + "lines", + "hit", + "missed", + "partial", + "coverage", + "branches", + "methods", + "messages", + "sessions", + "complexity", +) + + +def migrate_totals(totals): + if totals: + if isinstance(totals, list): + # v3 + return totals + + elif "hit" in totals: + tg = totals.get + # v1 + data = [tg(k, 0) for k in TOTALS_MAP_v1] + data[5] = ratio(data[2], data[1]) + return data + + else: + tg = totals.get if isinstance(totals, dict) else loads(totals).get + # v2 + return [tg(k, 0) for k in TOTALS_MAP] + return [] diff --git a/libs/shared/shared/utils/sessions.py b/libs/shared/shared/utils/sessions.py new file mode 100644 index 0000000000..aa70955d15 --- /dev/null +++ b/libs/shared/shared/utils/sessions.py @@ -0,0 +1,135 @@ +from enum import Enum + +from shared.reports.types import ReportTotals + + +class SessionType(Enum): + uploaded = "uploaded" + carriedforward = "carriedforward" + + @classmethod + def get_from_string(cls, val): + for member in cls: + if member.value == val: + return member + return None + + +class Session(object): + def __init__( + self, + id=None, + totals=None, + time=None, + archive=None, + flags=None, + provider=None, + build=None, + job=None, + url=None, + state=None, + env=None, + name=None, + session_type=None, + session_extras=None, + **kwargs, + ): + # the kwargs are for old reports + self.id = id + self.totals = totals + self.time = time + self.archive = archive # url where archived + self.flags = flags + self.provider = provider + self.build = build + self.job = job + self.url = url + self.state = state + self.env = env + self.name = name + self.session_type = session_type or SessionType.uploaded + self.session_extras = session_extras or {} + + def __eq__(self, other): + return ( + self.id == other.id + and self.totals == other.totals + and self.time == other.time + and self.archive == other.archive + and self.flags == other.flags + and self.provider == other.provider + and self.build == other.build + and self.job == other.job + and self.url == other.url + and self.state == other.state + and self.env == other.env + and self.name == other.name + and self.session_type == other.session_type + and self.session_extras == other.session_extras + ) + + def __repr__(self): + return f"Session<{self._encode()}>" + + @classmethod + def parse_session( + cls, + id=None, + t=None, + d=None, + a=None, + f=None, + c=None, + n=None, + j=None, + u=None, + p=None, + e=None, + N=None, + st=None, + se=None, + **kwargs, + ): + return cls( + id=id, + totals=parse_totals(t or kwargs.get("totals")), + time=d or kwargs.get("time"), + archive=a or kwargs.get("archive"), + flags=f or kwargs.get("flags"), + provider=c or kwargs.get("provider"), + build=n or kwargs.get("build"), + job=j or kwargs.get("job"), + url=u or kwargs.get("url"), + state=p or kwargs.get("state"), + env=e or kwargs.get("env"), + name=N or kwargs.get("name"), + session_type=SessionType.get_from_string(st), + session_extras=se, + ) + + def _encode(self): + return { + "t": self.totals.astuple() + if isinstance(self.totals, ReportTotals) + else self.totals, + "d": self.time, + "a": self.archive, + "f": self.flags, + "c": self.provider, + "n": self.build, + "N": self.name, + "j": self.job, + "u": self.url, + "p": self.state, + "e": self.env, + "st": self.session_type.value, + "se": self.session_extras, + } + + +def parse_totals(totals): + if isinstance(totals, ReportTotals): + return totals + if totals: + return ReportTotals(*totals) + return None diff --git a/libs/shared/shared/utils/snake_to_camel_case.py b/libs/shared/shared/utils/snake_to_camel_case.py new file mode 100644 index 0000000000..34ece3337b --- /dev/null +++ b/libs/shared/shared/utils/snake_to_camel_case.py @@ -0,0 +1,3 @@ +def snake_to_camel_case(snake: str) -> str: + parts = snake.split("_") + return parts[0] + "".join(part.capitalize() for part in parts[1:]) diff --git a/libs/shared/shared/utils/test_utils/__init__.py b/libs/shared/shared/utils/test_utils/__init__.py new file mode 100644 index 0000000000..499e9de444 --- /dev/null +++ b/libs/shared/shared/utils/test_utils/__init__.py @@ -0,0 +1,5 @@ +from .mock_config_helper import mock_config_helper + +__all__ = [ + "mock_config_helper", +] diff --git a/libs/shared/shared/utils/test_utils/mock_config_helper.py b/libs/shared/shared/utils/test_utils/mock_config_helper.py new file mode 100644 index 0000000000..72c8580c8b --- /dev/null +++ b/libs/shared/shared/utils/test_utils/mock_config_helper.py @@ -0,0 +1,39 @@ +from shared.config import ConfigHelper + + +def mock_config_helper(mocker, configs={}, file_configs={}): + """ + Generic utility for mocking two functions on `ConfigHelper`: + - `get()`, which takes a config key and returns its value + - `load_filename_from_path()`, which takes a config key, treats its value as + a file path, and returns the contents of the file. + + These functions underpin the non-method APIs of the config module. + + Example: + configs = {"github.client_id": "testvalue"} + file_configs = {"github.integration.pem": "--------BEGIN RSA PRIVATE KEY-----..."} + mock_config_helper(mocker, configs, file_configs) + + assert "testvalue" == get_config("github", "client_id") + assert "BEGIN RSA" in load_file_from_path_at_config("github", "integration", "pem") + """ + orig_get = ConfigHelper.get + orig_load_file = ConfigHelper.load_filename_from_path + + def mock_get(obj, *args, **kwargs): + conf_key = ".".join(args) + if conf_key in configs: + return configs.get(conf_key) + else: + return orig_get(obj, *args, **kwargs) + + def mock_load_file(obj, *args, **kwargs): + conf_key = ".".join(args) + if conf_key in file_configs: + return file_configs.get(conf_key) + else: + return orig_load_file(obj, *args, **kwargs) + + mocker.patch.object(ConfigHelper, "get", mock_get) + mocker.patch.object(ConfigHelper, "load_filename_from_path", mock_load_file) diff --git a/libs/shared/shared/utils/totals.py b/libs/shared/shared/utils/totals.py new file mode 100644 index 0000000000..fa17fbeb64 --- /dev/null +++ b/libs/shared/shared/utils/totals.py @@ -0,0 +1,51 @@ +from operator import attrgetter + +from shared.helpers.numeric import ratio +from shared.reports.types import ReportTotals + + +def agg_totals(totals): + totals = [_f for _f in totals if _f] + n_files = len(totals) + totals = list(map(_sum, zip(*totals))) + if not totals: + return ReportTotals.default_totals() + totals = ReportTotals(*totals) + totals.files = n_files + totals.coverage = ratio(totals.hits, totals.lines) if totals.lines else None + return totals + + +def sum_totals(totals): + totals = [_f for _f in totals if _f] + if not totals: + return ReportTotals.default_totals() + + sessions = totals[0].sessions + lines = sum(map(attrgetter("lines"), totals)) + hits = sum(map(attrgetter("hits"), totals)) + return ReportTotals( + files=len(totals), + lines=lines, + hits=hits, + misses=sum(map(attrgetter("misses"), totals)), + partials=sum(map(attrgetter("partials"), totals)), + branches=sum(map(attrgetter("branches"), totals)), + methods=sum(map(attrgetter("methods"), totals)), + messages=sum(map(attrgetter("messages"), totals)), + coverage=ratio(hits, lines) if lines else None, + sessions=sessions, + complexity=sum(map(attrgetter("complexity"), totals)), + complexity_total=sum(map(attrgetter("complexity_total"), totals)), + ) + + +def _sum(array): + if array: + if not isinstance(array[0], (type(None), str)): + try: + return sum(array) + except Exception: + # https://sentry.io/codecov/v4/issues/159966549/ + return sum([a if isinstance(a, int) else 0 for a in array]) + return None diff --git a/libs/shared/shared/utils/urls.py b/libs/shared/shared/utils/urls.py new file mode 100644 index 0000000000..3ceeee5d69 --- /dev/null +++ b/libs/shared/shared/utils/urls.py @@ -0,0 +1,115 @@ +from typing import Dict, List, Tuple, Union +from urllib.parse import parse_qsl, quote_plus, urlencode, urlparse, urlunparse + +from shared.config import get_config + +services_short = dict( + github="gh", + github_enterprise="ghe", + bitbucket="bb", + bitbucket_server="bbs", + gitlab="gl", + gitlab_enterprise="gle", +) + + +def escape(string, escape=False): + if isinstance(string, str): + if escape: + return url_escape(string).replace("%2F", "/") + return string.encode("utf-8", "replace") + elif escape: + return str(string) + else: + return string + + +def make_url(repository, *args, **kwargs): + args = list(map(lambda a: escape(a, True), list(args))) + kwargs = dict([(k, escape(v)) for k, v in kwargs.items() if v is not None]) + if repository: + return url_concat( + "/".join( + [ + get_config("setup", "codecov_url"), + services_short[repository.service], + repository.slug, + ] + + args + ), + kwargs, + ) + else: + return url_concat("/".join([get_config("setup", "codecov_url")] + args), kwargs) + + +def url_escape(value): + """Returns a valid URL-encoded version of the given value.""" + return quote_plus(utf8(value)) + + +def url_concat( + url: str, + args: Union[ + None, Dict[str, str], List[Tuple[str, str]], Tuple[Tuple[str, str], ...] + ], +) -> str: + """Taken from Tornado.httputil + https://github.com/tornadoweb/tornado/blob/f059b41d18909f83610bb48eba4678f7f892f52f/tornado/httputil.py#L609 + + Concatenate url and arguments regardless of whether + url has existing query parameters. + + ``args`` may be either a dictionary or a list of key-value pairs + (the latter allows for multiple values with the same key. + + >>> url_concat("http://example.com/foo", dict(c="d")) + 'http://example.com/foo?c=d' + >>> url_concat("http://example.com/foo?a=b", dict(c="d")) + 'http://example.com/foo?a=b&c=d' + >>> url_concat("http://example.com/foo?a=b", [("c", "d"), ("c", "d2")]) + 'http://example.com/foo?a=b&c=d&c=d2' + """ + if args is None: + return url + parsed_url = urlparse(url) + if isinstance(args, dict): + parsed_query = parse_qsl(parsed_url.query, keep_blank_values=True) + parsed_query.extend(args.items()) + elif isinstance(args, list) or isinstance(args, tuple): + parsed_query = parse_qsl(parsed_url.query, keep_blank_values=True) + parsed_query.extend(args) + else: + err = "'args' parameter should be dict, list or tuple. Not {0}".format( + type(args) + ) + raise TypeError(err) + final_query = urlencode(parsed_query) + url = urlunparse( + ( + parsed_url[0], + parsed_url[1], + parsed_url[2], + parsed_url[3], + final_query, + parsed_url[5], + ) + ) + return url + + +_UTF8_TYPES = (bytes, type(None)) + + +def utf8(value): + """Taken from Tornado.escape + https://github.com/tornadoweb/tornado/blob/1db5b45918da8303d2c6958ee03dbbd5dc2709e9/tornado/escape.py#L188 + Converts a string argument to a byte string. + + If the argument is already a byte string or None, it is returned unchanged. + Otherwise it must be a unicode string and is encoded as utf8. + """ + if isinstance(value, _UTF8_TYPES): + return value + assert isinstance(value, str) + return value.encode("utf-8") diff --git a/libs/shared/shared/validation/__init__.py b/libs/shared/shared/validation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/shared/validation/cli_schema.py b/libs/shared/shared/validation/cli_schema.py new file mode 100644 index 0000000000..aa2c146ec0 --- /dev/null +++ b/libs/shared/shared/validation/cli_schema.py @@ -0,0 +1,11 @@ +schema = { + "cli": { + "type": "dict", + "schema": { + "plugins": { + "type": "dict", + }, + "runners": {"type": "dict"}, + }, + } +} diff --git a/libs/shared/shared/validation/exceptions.py b/libs/shared/shared/validation/exceptions.py new file mode 100644 index 0000000000..2c8866db89 --- /dev/null +++ b/libs/shared/shared/validation/exceptions.py @@ -0,0 +1,11 @@ +class InvalidYamlException(Exception): + def __init__( + self, error_location, error_message, error_dict=None, original_exc=None + ): + self.error_location = error_location + self.error_message = error_message + self.error_dict = error_dict + self.original_exc = original_exc + + def __str__(self) -> str: + return f"InvalidYamlException[error_location={self.error_location}, error_dict={self.error_dict}]" diff --git a/libs/shared/shared/validation/helpers.py b/libs/shared/shared/validation/helpers.py new file mode 100644 index 0000000000..9565eba810 --- /dev/null +++ b/libs/shared/shared/validation/helpers.py @@ -0,0 +1,526 @@ +import functools +import logging +import numbers +import re +from typing import Any, List + +import pyparsing as pp + +from shared.validation.types import ( + BundleThreshold, + CoverageCommentRequiredChanges, + CoverageCommentRequiredChangesANDGroup, + CoverageCommentRequiredChangesORGroup, + ValidRawRequiredChange, +) + +log = logging.getLogger(__name__) + + +class Invalid(Exception): + def __init__(self, error_message): + super().__init__() + self.error_message = error_message + + +class CoverageRangeSchemaField(object): + """ + Pattern for the user to input a range like 60..90 (which means from 60 to 90) + + We accept ".." and "..." as separators + + This value is converted into a two members array + + CoverageRangeSchemaField().validate('30...99') == [30.0, 99.0] + """ + + def validate_bounds(self, lower_bound, upper_bound): + if not 0 <= lower_bound <= 100: + raise Invalid(f"Lower bound {lower_bound} should be between 0 and 100") + if not 0 <= upper_bound <= 100: + raise Invalid(f"Upper bound {upper_bound} should be between 0 and 100") + if lower_bound > upper_bound: + raise Invalid( + f"Upper bound {upper_bound} should be bigger than {lower_bound}" + ) + return [lower_bound, upper_bound] + + def validate(self, data): + if isinstance(data, list): + if len(data) != 2: + raise Invalid(f"{data} should have only two elements") + try: + lower_bound, upper_bound = sorted(float(el) for el in data) + return self.validate_bounds(lower_bound, upper_bound) + except ValueError: + raise Invalid(f"{data} should have numbers as the range limits") + if "...." in data: + raise Invalid(f"{data} should have two or three dots, not four") + elif "..." in data: + splitter = "..." + elif ".." in data: + splitter = ".." + else: + raise Invalid(f"{data} does not have the correct format") + split_value = data.split(splitter) + if len(split_value) != 2: + raise Invalid(f"{data} should have only two numbers") + try: + lower_bound = float(split_value[0]) + upper_bound = float(split_value[1]) + return self.validate_bounds(lower_bound, upper_bound) + except ValueError: + raise Invalid(f"{data} should have numbers as the range limits") + + +class CoverageCommentRequirementSchemaField(object): + """Converts `comment.require_changes` into CoverageCommentRequiredChanges + + Conversion table: + False -> CoverageCommentRequiredChanges.no_requirements + True -> CoverageCommentRequiredChanges.any_change + "any_change" -> CoverageCommentRequiredChanges.any_change + "coverage_drop" -> CoverageCommentRequiredChanges.coverage_drop + "uncovered_patch" -> CoverageCommentRequiredChanges.uncovered_patch + + We also accept AND and OR operators + """ + + def validate(self, data: Any) -> CoverageCommentRequiredChangesANDGroup: + """Validates and coerces values into CoverageCommentRequiredChanges + + raises: Invalid + """ + if isinstance(data, bool): + return self.validate_bool(data) + elif isinstance(data, str): + return self.validate_str(data) + raise Invalid("Only bool and str are accepted values") + + def validate_bool(self, data: bool) -> CoverageCommentRequiredChangesANDGroup: + if data: + return [CoverageCommentRequiredChanges.any_change.value] + return [CoverageCommentRequiredChanges.no_requirements.value] + + def _convert_to_binary_value( + self, valid_requirement: ValidRawRequiredChange + ) -> int: + """Gets the binary value of `valid_requirement` as defined in CoverageCommentRequiredChanges""" + return CoverageCommentRequiredChanges[valid_requirement].value + + def _parse_or_group( + self, acc: int, value: ValidRawRequiredChange + ) -> CoverageCommentRequiredChangesORGroup: + """Combines the individual `valid_requirement` into a single value""" + return acc | self._convert_to_binary_value(value) + + def validate_str(self, data: str) -> CoverageCommentRequiredChangesANDGroup: + """Validates required_changes from a string that represents operations on valid_requirements + and returns the result. + + Result is CoverageCommentRequiredChangesANDGroup, a list of ORGroups. + An ORGroup is 1 or more valid_requirements that are grouped together (using OR operations) + For the overall ANDGroup to be satisfied, ALL the ORGroups that are port of it need to also be satisfied. + """ + if data == "": + raise Invalid("required_changes is empty") + data = data.lower() + + valid_requirements = pp.oneOf( + "coverage_drop uncovered_patch any_change", asKeyword=True + ) + or_groups_parser = pp.delimitedList(valid_requirements, "or").setResultsName( + "or_groups", listAllMatches=True + ) + and_groups_parser = pp.delimitedList(or_groups_parser, "and") + + try: + raw_or_groups: List[pp.ParseResults] = and_groups_parser.parseString( + data, parseAll=True + )["or_groups"] + parsed_or_groups = [ + functools.reduce(self._parse_or_group, raw_group, 0) + for raw_group in raw_or_groups + ] + return parsed_or_groups + + except pp.ParseException: + raise Invalid("Failed to parse required_changes") + + +class ByteSizeSchemaField(object): + """Converts a possible string with byte extension size into integer with number of bytes. + Acceptable extensions are 'mb', 'kb', 'gb', 'b' and 'bytes' (case insensitive). + Also accepts positive integers, returning the value itself as the number of bytes. + + Example: + 100 -> 100 + "100b" -> 100 + "100 mb" -> 100000000 + "12KB" -> 12000 + """ + + def _validate_str(self, data: str) -> int: + data = data.lower() + regex = re.compile(r"^(\d+)\s*(mb|kb|gb|b|bytes)$") + match = regex.match(data) + if match is None: + raise Invalid( + "Value doesn't match expected regex. Acceptable extensions are mb, kb, gb, b or bytes" + ) + size, extension = match.groups() + extension_multiplier = {"b": 1, "bytes": 1, "kb": 1e3, "mb": 1e6, "gb": 1e9} + return int(size) * extension_multiplier[extension] + + def validate(self, data: Any) -> int: + if isinstance(data, int): + if data < 0: + raise Invalid("Only positive values accepted") + return data + if isinstance(data, str): + return self._validate_str(data) + raise Invalid(f"Value should be int or str. Received {type(data).__name__}") + + +class PercentSchemaField(object): + """ + A field for percentages. Accepts both with and without % symbol. + The end result is the percentage number + + PercentSchemaField().validate('60%') == 60.0 + """ + + field_regex = re.compile(r"(\d+)(\.\d+)?%?") + + def validate(self, value, allow_auto=False): + if value == "auto" and allow_auto: + return value + elif value == "auto": + raise Invalid("This field does not accept auto") + if isinstance(value, numbers.Number): + return float(value) + if not self.field_regex.match(value): + raise Invalid(f"{value} should be a number") + if value.endswith("%"): + value = value.rstrip("%") + try: + return float(value) + except ValueError: + raise Invalid(f"{value} should be a number") + + +class BundleSizeThresholdSchemaField(object): + """ + A field for bundle analysis threshold. + It can be either a ByteSizeSchemaField or PercentSchemaField. + ⚠️ integers are considered ByteSize ⚠️ + ⚠️ floats are considered Percent ⚠️ + + Examples: + "10 mb" -> ("absolute", 1000000) + "100%" -> ("percentage", 100.0) + 100 -> ("absolute", 100) + 0.5 -> ("percentage", 50.0) + """ + + def validate(self, value: Any) -> BundleThreshold: + byte_size_schema = ByteSizeSchemaField() + percentage_schema = PercentSchemaField() + if isinstance(value, numbers.Integral): + value: int = byte_size_schema.validate(value) + return BundleThreshold("absolute", value) + if isinstance(value, numbers.Real) or isinstance(value, str) and "%" in value: + value: float = percentage_schema.validate(value, allow_auto=False) + return BundleThreshold("percentage", value) + value: int = byte_size_schema.validate(value) + return BundleThreshold("absolute", value) + + +def determine_path_pattern_type(filepath_pattern): + """ + Tries to determine whether `filepath_pattern` is a: + - 'path_prefix' + - 'glob' + - 'regex' + + As you can see in the documentation for PathPatternSchemaField, + the same pattern can be used as more than one way. + + Args: + filepath_pattern (str): the filepath + + Returns: + str: The probable type of that inputted pattern + """ + reserved_chars = ["*", "$", "]", "["] + if not any(x in filepath_pattern for x in reserved_chars): + return "path_prefix" + if "**" in filepath_pattern or "/*" in filepath_pattern: + return "glob" + expected_regex_star_cases = ["]*", ".*"] + if "*" in filepath_pattern and not any( + x in filepath_pattern for x in expected_regex_star_cases + ): + return "glob" + try: + re.compile(filepath_pattern) + return "regex" + except re.error: + return "glob" + + +def translate_glob_to_regex(pat, end_of_string=True): + """ + Translate a shell PATTERN to a regular expression. + + There is no way to quote meta-characters. + + This is copied from fnmatch.translate_glob_to_regex. If you could be + so kind and see if they changed it since we copied, + that would be very helpful, thanks. + + The only reason we copied (instead of importing and using), + is that we needed to change behavior on ** + """ + + i, n = 0, len(pat) + res = "" + while i < n: + c = pat[i] + i = i + 1 + if c == "*": + if i < n and pat[i] == "*": + res = res + ".*" + i = i + 1 + else: + res = res + r"[^\/]*" + elif c == "?": + res = res + "." + elif c == "[": + j = i + if j < n and pat[j] == "!": + j = j + 1 + if j < n and pat[j] == "]": + j = j + 1 + while j < n and pat[j] != "]": + j = j + 1 + if j >= n: + res = res + "\\[" + else: + stuff = pat[i:j] + if "--" not in stuff: + stuff = stuff.replace("\\", r"\\") + else: + chunks = [] + k = i + 2 if pat[i] == "!" else i + 1 + while True: + k = pat.find("-", k, j) + if k < 0: + break + chunks.append(pat[i:k]) + i = k + 1 + k = k + 3 + chunks.append(pat[i:j]) + # Escape backslashes and hyphens for set difference (--). + # Hyphens that create ranges shouldn't be escaped. + stuff = "-".join( + s.replace("\\", r"\\").replace("-", r"\-") for s in chunks + ) + # Escape set operations (&&, ~~ and ||). + stuff = re.sub(r"([&~|])", r"\\\1", stuff) + i = j + 1 + if stuff[0] == "!": + stuff = "^" + stuff[1:] + elif stuff[0] in ("^", "["): + stuff = "\\" + stuff + res = "%s[%s]" % (res, stuff) + else: + res = res + re.escape(c) + if end_of_string: + return r"(?s:%s)\Z" % res + return r"(?s:%s)" % res + + +class PathPatternSchemaField(object): + """This class holds the logic for validating and processing a user given path pattern + + This is how it works. The intention is to allow the user to give a string as an input, + and in return, that string is used as a pattern to identify which paths to include/exclude + from their report + + For that, we take the user input, and transform it into a regex that python can process + + The user can input three types of patterns: + + - path_prefix - It's when user inputs something like `path/to/folder`. + That means that, every filename for a file that lives inside `path/to/folder` + will match that pattern, regardless of how deep it is. + - regex - The user inputs a regex directly. In this case we simply apply the regex to the + filepath to see if it matches + - glob - The user inputs a glob (as the glob that we use in unix, using `*` and `**`) + + This class tries to determine which type of pattern the user inputted. We say "try", because + some paths can be more than one type, and we try our best to see what the user meant. + + For example, `a.*` could match `a/folder1/path/file.py` as a regex, but not as a glob. + As a glob, `a.*` could match a.yaml, a.py and a.cpp + + After determined the type, the code converts that type of pattern to a regex (in case + the user inputted a regex, it is used as it is) + + One additional processing we do is to account for the usage of `!` by the user. + `!` means negation, and although we support `ignore` fields, sometimes the users + prefer to just use `!` to denote something they want to exclude. + + To see some examples of results from this validator field, take a look at + services/yaml/tests/test_validation.py::TestPathPatternSchemaField + """ + + def input_type(self, value): + return determine_path_pattern_type(value) + + def validate_glob(self, value): + if not value.endswith("$") and not value.endswith("*"): + log.warning( + "Old glob behavior would have interpreted this glob as prefix", + extra=dict(glob=value), + ) + return translate_glob_to_regex(value) + + def validate_path_prefix(self, value): + return f"^{value}.*" + + def validate(self, value): + if value.startswith("!"): + is_negative = True + value = value.lstrip("!") + else: + is_negative = False + + if value.startswith("./"): + value = value[2:] + + input_type = self.input_type(value) + result = self.validate_according_to_type(input_type, value) + if is_negative: + return f"!{result}" + return result + + def validate_according_to_type(self, input_type, value): + if input_type == "regex": + try: + re.compile(value) + return value + except re.error: + raise Invalid(f"{value} does not work as a regex") + elif input_type == "glob": + return self.validate_glob(value) + elif input_type == "path_prefix": + return self.validate_path_prefix(value) + else: + raise Invalid(f"We did not detect what {value} is") + + +class CustomFixPathSchemaField(object): + def input_type(self, value): + return determine_path_pattern_type(value) + + def validate(self, value): + if "::" not in value: + raise Invalid("Pathfix must split before and after with a ::") + before, after = value.split("::", 1) + if before == "" or after == "": + return value + before_input_type = self.input_type(before) + before = self.validate_according_to_type(before_input_type, before) + return f"{before}::{after}" + + def validate_according_to_type(self, input_type, value): + if input_type == "regex": + try: + re.compile(value) + return value + except re.error: + raise Invalid(f"{value} does not work as a regex") + elif input_type == "glob": + return translate_glob_to_regex(value, end_of_string=False) + elif input_type == "path_prefix": + return f"^{value}" + else: + raise Invalid(f"We did not detect what {value} is") + + +class UserGivenBranchRegex(object): + asterisk_to_regexp = re.compile(r"(? apple.* + nv = self.asterisk_to_regexp.sub(".*", value.strip()) + if not nv.startswith((".*", "^")): + nv = "^%s" % nv + if not nv.endswith((".*", "$")): + nv = "%s$" % nv + re.compile(nv) + return nv + + +class LayoutStructure(object): + acceptable_objects = set( + [ + "changes", + "diff", + "file", + "files", + "flag", + "flags", + "footer", + "header", + "reach", + "components", + "suggestions", + "betaprofiling", + "sunburst", + "tree", + "uncovered", + "newheader", # deprecated, keeping it for backward compatibility + "newfooter", # deprecated, keeping it for backward compatibility + "feedback", + "newfiles", # deprecated, keeping it for backward compatibility + "condensed_header", + "condensed_footer", + "condensed_files", + ] + ) + + def validate(self, value): + values = value.split(",") + actual_values = [x.strip().split(":")[0] for x in values if x != ""] + if not set(actual_values) <= self.acceptable_objects: + extra_objects = set(actual_values) - self.acceptable_objects + extra_objects = ",".join(sorted(extra_objects)) + raise Invalid(f"Unexpected values on layout: {extra_objects}") + for val in values: + if ":" in val: + try: + int(val.strip().split(":")[1]) + except ValueError: + raise Invalid( + f"Improper pattern for value on layout: {val.strip()}" + ) + return value + + +class BranchSchemaField(object): + def validate(self, value): + if not isinstance(value, str): + raise Invalid(f"Branch must be {str}, was {type(value)} ({value})") + if value[:7] == "origin/": + return value[7:] + elif value[:11] == "refs/heads/": + return value[11:] + return value diff --git a/libs/shared/shared/validation/install.py b/libs/shared/shared/validation/install.py new file mode 100644 index 0000000000..aa12855f64 --- /dev/null +++ b/libs/shared/shared/validation/install.py @@ -0,0 +1,409 @@ +"""Configuration options that affect an entire instance of Codecov""" + +import logging + +from shared.utils.enums import TaskConfigGroup +from shared.validation.user_schema import schema as user_yaml_schema +from shared.validation.validator import CodecovYamlValidator + +log = logging.getLogger(__name__) + + +def check_task_config_key(field, value, error): + if value == "celery": + return + if value in set(group.value for group in TaskConfigGroup): + return + error(field, "Not a valid TaskConfigGroup") + + +# Bot is a git provider account configured to be used in place of another +bot_details_fields = { + # This is a PAT for some provider account that is going to be used as a bot + "key": {"type": "string", "required": True}, + # This is used only for Bitbucket (uses Oauth1) + "secret": {"type": "string"}, + # Identifies the bot in the logs + "username": {"type": "string"}, +} + +# Credentials used by the OAuth App used when logging into Codecov UI +# note: the OAuth App acts in behalf of the user. The user will need to authorize it +# and enter user's own credentials for logging into the git provider +oauth_credential_fields = { + "client_id": {"type": "string"}, + "client_secret": {"type": "string"}, + # The URI to redirect the user after authorization is granted to the OAuth App + "redirect_uri": {"type": "string"}, +} + +# Default app [GitHub exclusive] - Credentials for a GitHub app used by this installation +# note: these credentials are used to get access_tokens for installations +GitHub_app_fields = { + "expires": {"type": "integer"}, + "id": {"type": "integer", "required": True}, + "pem": {"type": ["string", "dict"], "required": True}, +} + +default_service_fields = { + "verify_ssl": {"type": "boolean"}, + "ssl_pem": {"type": "string"}, + **oauth_credential_fields, + "url": {"type": "string"}, + "api_url": {"type": "string"}, + # bot [enterprise (self-hosted)] - Bot that is used for all repos belonging to a given service. + # bot [cloud] - Used as public bot fallback if bots.tokenless is not provided. + "bot": { + "type": "dict", + "schema": bot_details_fields, + }, + # global_upload_token [enterprise (self-hosted)] - Master upload token. + # Any upload (for any repo) made to the instance using this token will be validated. + "global_upload_token": {"type": "string"}, + "organizations": {"type": "list", "schema": {"type": "string"}}, + "webhook_secret": {"type": "string"}, + # bots - Function-specific bots used as fallbacks for public repos + # Certain functions in the torngit adapter will use one of these tokens if none is present. + # 'bots.tokenless' is the default fallback + "bots": { + "type": "dict", + "schema": { + "read": { + "type": "dict", + "schema": bot_details_fields, + }, + "comment": { + "type": "dict", + "schema": bot_details_fields, + }, + "status": { + "type": "dict", + "schema": bot_details_fields, + }, + "tokenless": { + "type": "dict", + "schema": bot_details_fields, + }, + }, + }, +} + +enterprise_queue_fields = { + "type": "dict", + "schema": { + "soft_timelimit": {"type": "integer"}, + "hard_timelimit": {"type": "integer"}, + }, +} + +default_task_fields = { + "queue": {"type": "string"}, + "timeout": {"type": "integer"}, + "interval_seconds": { + "type": "integer", + "min": 0, + }, + "enterprise": {**enterprise_queue_fields}, +} + +config_schema = { + "setup": { + "type": "dict", + "schema": { + "cache": { + "type": "dict", + "schema": { + "chunks": {"type": "integer"}, + "diff": {"type": "integer"}, + "tree": {"type": "integer"}, + "uploads": {"type": "integer"}, + "yaml": {"type": "integer"}, + }, + }, + "legacy_report_style": {"type": "boolean"}, + "loglvl": {"type": "string", "allowed": ("INFO",)}, + "max_sessions": {"type": "integer"}, + "debug": {"type": "boolean"}, + "codecov_url": {"type": "string"}, + "codecov_api_url": {"type": "string"}, + "webhook_url": {"type": "string"}, + "api_cors_allowed_origins": {"type": "string"}, + "codecov_dashboard_url": {"type": "string"}, + "enterprise_license": {"type": "string"}, + "hide_all_codecov_tokens": {"type": "boolean"}, + "admins": { + "type": "list", + "schema": { + "type": "dict", + "schema": { + "service": {"type": "string"}, + "username": {"type": "string"}, + }, + }, + }, + "api_allowed_hosts": {"type": "list", "schema": {"type": "string"}}, + "secure_cookie": {"type": "boolean"}, + "pubsub": { + "type": "dict", + "schema": { + "project_id": {"type": "string"}, + "topic": {"type": "string"}, + "enabled": {"type": "boolean"}, + }, + }, + "marketo": { + "type": "dict", + "schema": { + "client_id": {"type": "string"}, + "client_secret": {"type": "string"}, + "base_url": {"type": "string"}, + "enabled": {"type": "boolean"}, + }, + }, + "http": { + "type": "dict", + "schema": { + "timeouts": { + "type": "dict", + "schema": { + "external": {"type": "integer"}, + "connect": {"type": "integer"}, + "receive": {"type": "integer"}, + }, + }, + "cookie_secret": {"type": "string"}, + "force_https": {"type": "boolean"}, + }, + }, + "encryption_secret": {"type": "string"}, + "encryption": { + "type": "dict", + "schema": { + "keys": { + "type": "list", + "schema": { + "type": "dict", + "schema": { + "code": {"type": "string"}, + "value": {"type": "string"}, + }, + }, + }, + "yaml_secret": {"type": "string"}, + "write_key": {"type": "string"}, + }, + }, + "media": { + "type": "dict", + "schema": { + "assets": {"type": "string"}, + "dependancies": {"type": "string"}, + }, + }, + "tasks": { + "type": "dict", + "keysrules": {"type": "string", "check_with": check_task_config_key}, + "valuesrules": { + "type": "dict", + "oneof": [ + # Regular tasks config + {"schema": {**default_task_fields}}, + # Special value for 'celery' key + { + "schema": { + "default_queue": {"type": "string"}, + "acks_late": {"type": "boolean"}, + "prefetch": {"type": "integer"}, + "soft_timelimit": {"type": "integer"}, + "hard_timelimit": {"type": "integer"}, + "enterprise": {**enterprise_queue_fields}, + "worker_max_memory_per_child": {"type": "integer"}, + } + }, + ], + }, + }, + "upload_processing_delay": {"type": "integer"}, + "skip_feature_cache": {"type": "boolean"}, + "timeseries": { + "type": "dict", + "schema": {"enabled": {"type": "boolean"}}, + }, + "telemetry": { + "type": "dict", + "schema": { + "enabled": {"type": "boolean"}, + "admin_email": {"type": "string"}, + "anonymous": {"type": "boolean"}, + "endpoint_override": {"type": "string"}, + }, + }, + "health_check": { + "type": "dict", + "schema": { + "enabled": {"type": "boolean"}, + }, + }, + "push_webhook_ignore_repo_names": { + "type": "list", + "schema": {"type": "string"}, + }, + # guest_access [enterprise (self-hosted)] - Wether to allow non-logged in users to access the UI in this Codecov instance + "guest_access": {"type": "boolean"}, + }, + }, + "services": { + "type": "dict", + "schema": { + "external_dependencies_folder": {"type": "string"}, + "google_analytics_key": {"type": "string"}, + "minio": { + "type": "dict", + "schema": { + "host": {"type": "string"}, + "port": {"type": "integer"}, + "hash_key": {"type": "string"}, + "iam_auth": {"type": "boolean"}, + "iam_endpoint": {"type": "string", "nullable": True}, + "access_key_id": {"type": "string"}, + "secret_access_key": {"type": "string"}, + "bucket": {"type": "string"}, + "region": {"type": "string"}, + "expire_raw_after_n_days": {"type": "boolean"}, + "verify_ssl": {"type": "boolean"}, + }, + }, + "gcp": { + "type": "dict", + "schema": {"google_credentials_location": {"type": "string"}}, + }, + "aws": { + "type": "dict", + "schema": { + "region_name": {"type": "string"}, + "resource": {"type": "string"}, + }, + }, + "chosen_storage": {"type": "string"}, + "database_url": {"type": "string"}, + "timeseries_database_url": {"type": "string"}, + "database": { + "type": "dict", + "schema": {"conn_max_age": {"type": "integer"}}, + }, + "redis_url": {"type": "string"}, + "github_marketplace": { + "type": "dict", + "schema": {"use_stubbed": {"type": "boolean"}}, + }, + "stripe": {"type": "dict", "schema": {"api_key": {"type": "string"}}}, + "celery_broker": {"type": "string"}, + "gravatar": {"type": "boolean"}, + "avatars.io": {"type": "boolean"}, + "sentry": {"type": "dict", "schema": {"server_dsn": {"type": "string"}}}, + "ci_providers": {"type": ["string", "list"], "schema": {"type": "string"}}, + "notifications": { + "type": "dict", + "schema": { + "slack": { + "type": ["boolean", "list"], + "schema": {"type": "string"}, + }, + "gitter": { + "type": ["boolean", "list"], + "schema": {"type": "string"}, + }, + "email": { + "type": ["boolean", "list"], + "schema": {"type": "string"}, + }, + "webhook": { + "type": ["boolean", "list"], + "schema": {"type": "string"}, + }, + "irc": {"type": ["boolean", "list"], "schema": {"type": "string"}}, + "hipchat": { + "type": ["boolean", "list"], + "schema": {"type": "string"}, + }, + }, + }, + "vsc_cache": { + "type": "dict", + "schema": { + "enabled": {"type": "boolean"}, + "metrics_app": {"type": "string"}, + "check_duration": {"type": "integer"}, + "compare_duration": {"type": "integer"}, + "status_duration": {"type": "integer"}, + }, + }, + "smtp": { + "type": "dict", + "schema": { + "host": {"type": "string"}, + "port": {"type": "integer"}, + "username": {"type": "string", "required": False}, + "password": {"type": "string", "required": False}, + }, + "required": False, + }, + }, + }, + "site": {"type": "dict", "schema": user_yaml_schema}, + "additional_user_yamls": { + "type": "list", + "schema": { + "type": "dict", + "schema": { + "percentage": {"type": "integer"}, + "name": {"type": "string"}, + "override": {"type": "dict", "schema": user_yaml_schema}, + }, + }, + }, + "github": { + "type": "dict", + "schema": { + **default_service_fields, + # integration - Credentials for the default Codecov App + "integration": { + "type": "dict", + "schema": GitHub_app_fields, + }, + # dedicated_apps - Dedicated apps are used in specific tasks. + # They can have a different set of permissions than the default Codecov App, allowing us to provide different opt-in services + "dedicated_apps": { + "type": "dict", + "valuesrules": {"type": "dict", "schema": GitHub_app_fields}, + }, + }, + }, + "bitbucket": {"type": "dict", "schema": {**default_service_fields}}, + "bitbucket_server": {"type": "dict", "schema": {**default_service_fields}}, + "github_enterprise": {"type": "dict", "schema": {**default_service_fields}}, + "gitlab": {"type": "dict", "schema": {**default_service_fields}}, + "gitlab_enterprise": {"type": "dict", "schema": {**default_service_fields}}, + "compatibility": { + "type": "dict", + "schema": {"flag_pattern_matching": {"type": "boolean"}}, + }, + "migrations": {"type": "dict", "schema": {"skip_risky_steps": {"type": "boolean"}}}, +} + + +def pre_process_config(inputted_dict): + # TODO: Add user yaml preprocess to here + pass + + +def validate_install_configuration(inputted_dict): + pre_process_config(inputted_dict) + validator = CodecovYamlValidator(show_secret=True) + is_valid = validator.validate(inputted_dict, config_schema) + if not is_valid: + log.debug( + "Configuration considered invalid, using dict as it is", + extra=dict(errors=validator.errors), + ) + return validator.document diff --git a/libs/shared/shared/validation/types.py b/libs/shared/shared/validation/types.py new file mode 100644 index 0000000000..072e85ed90 --- /dev/null +++ b/libs/shared/shared/validation/types.py @@ -0,0 +1,43 @@ +from enum import Enum +from typing import List, Literal, NamedTuple + + +class BundleThreshold(NamedTuple): + type: Literal["absolute"] | Literal["percentage"] + threshold: int | float + + +class CoverageCommentRequiredChanges(Enum): + """Concrete choices of requirements to post the coverage PR comment + See shared.validation.user_schema.py for description + """ + + no_requirements = 0b000 + any_change = 0b001 + coverage_drop = 0b010 + uncovered_patch = 0b100 + + +ValidRawRequiredChange = ( + Literal["any_change"] | Literal["coverage_drop"] | Literal["uncovered_patch"] +) +CoverageCommentRequiredChangesORGroup = ( + # This represents a grouping of CoverageCommentRequiredChanges through OR operations (bitwise). + # For the group to be satisfied ANY of the conditions should be satisfied + # To know if a CoverageCommentRequiredChanges you do a bitwise AND: + # Example: + # 0b110 & CoverageCommentRequiredChanges.uncovered_patch == True (so 'uncovered_patch' is a member of this OR group) + Literal[0b000] + | Literal[0b001] + | Literal[0b010] + | Literal[0b011] + | Literal[0b100] + | Literal[0b101] + | Literal[0b110] + | Literal[0b111] +) + +# For the AND group to be satisfied ALL of the individual OR groups need to be satisfied +# Example: +# [0b001, 0b100] - There has to be any change in coverage AND the patch can't be 100% covered +CoverageCommentRequiredChangesANDGroup = List[CoverageCommentRequiredChangesORGroup] diff --git a/libs/shared/shared/validation/user_schema.py b/libs/shared/shared/validation/user_schema.py new file mode 100644 index 0000000000..35e270e96f --- /dev/null +++ b/libs/shared/shared/validation/user_schema.py @@ -0,0 +1,609 @@ +flag_name = { + "type": "string", + "minlength": 1, + "maxlength": 1024, + "regex": r"^[^\'\"]+$", +} + +branches_structure = { + "type": "list", + "schema": {"type": "string", "nullable": True, "coerce": "branch_name"}, + "nullable": True, +} + +layout_structure = {"type": "string", "comma_separated_strings": True, "nullable": True} + +path_list_structure = { + "type": "list", + "nullable": True, + "schema": {"type": "string", "coerce": "regexify_path_pattern"}, +} + +flag_list_structure = { + "type": "list", + "nullable": True, + "schema": {"type": "string", "regex": r"^[^\'\"]{1,1024}$"}, +} + +status_common_config = { + "base": {"type": "string", "allowed": ("parent", "pr", "auto")}, + "branches": branches_structure, + "disable_approx": {"type": "boolean"}, + "enabled": {"type": "boolean"}, + "if_ci_failed": { + "type": "string", + "allowed": ("success", "failure", "error", "ignore"), + }, + "if_no_uploads": { + "type": "string", + "allowed": ("success", "failure", "error", "ignore"), + }, + "if_not_found": { + "type": "string", + "allowed": ("success", "failure", "error", "ignore"), + }, + "informational": {"type": "boolean"}, + "measurement": { + "type": "string", + "nullable": True, + "allowed": ("line", "statement", "branch", "method", "complexity"), + }, + "only_pulls": {"type": "boolean"}, + "skip_if_assumes": {"type": "boolean"}, + "removed_code_behavior": { + "type": ["string", "boolean"], + "allowed": ( + "removals_only", + "adjust_base", + "fully_covered_patch", + "off", + False, + ), + }, +} + +percent_type_or_auto = { + "type": ["string", "number"], + "anyof": [{"allowed": ["auto"]}, {"regex": r"(\d+)(\.\d+)?%?"}], + "nullable": True, + "coerce": "percentage_to_number_or_auto", +} + +percent_type = { + "type": ["string", "number"], + "regex": r"(\d+)(\.\d+)?%?", + "nullable": True, + "coerce": "percentage_to_number", +} + +custom_status_common_config = { + "name_prefix": {"type": "string", "regex": r"^[\w\-\.]+$"}, + "type": {"type": "string", "allowed": ("project", "patch", "changes")}, + "target": percent_type_or_auto, + "threshold": percent_type, +} + +flag_status_base_attributes = { + **status_common_config, + "paths": path_list_structure, + "carryforward_behavior": { + "type": "string", + "allowed": ("include", "exclude", "pass"), + }, + "flag_coverage_not_uploaded_behavior": { + "type": "string", + "allowed": ("include", "exclude", "pass"), + }, +} + +status_standard_attributes = { + "flags": flag_list_structure, + **flag_status_base_attributes, +} + +flag_status_attributes = {**flag_status_base_attributes, **custom_status_common_config} + +component_status_attributes = {**status_common_config, **custom_status_common_config} + +notification_standard_attributes = { + "url": {"type": "string", "coerce": "secret", "nullable": True}, + "branches": branches_structure, + "threshold": percent_type, + "message": {"type": "string"}, + "flags": flag_list_structure, + "base": {"type": "string", "allowed": ("parent", "pr", "auto")}, + "only_pulls": {"type": "boolean"}, + "paths": path_list_structure, +} + + +flags_rule_basic_properties = { + "statuses": { + "type": "list", + "schema": {"type": "dict", "schema": flag_status_attributes}, + }, + "carryforward_mode": { + "type": "string", + "allowed": ("all", "labels"), + }, + "carryforward": {"type": "boolean"}, + "paths": path_list_structure, + "ignore": path_list_structure, + "after_n_builds": {"type": "integer", "min": 0}, +} + +component_rule_basic_properties = { + "statuses": { + "type": "list", + "schema": {"type": "dict", "schema": component_status_attributes}, + }, + "flag_regexes": {"type": "list", "schema": {"type": "string"}}, + "paths": path_list_structure, +} + +coverage_comment_config = { + "layout": { + "type": "string", + "comma_separated_strings": True, + "nullable": True, + }, + "require_changes": { + "coerce": "coverage_comment_required_changes", + "meta": { + "description": "require_changes instructs Codecov to only post a PR Comment if there are coverage changes in the PR.", + "options": { + False: { + "type": bool, + "description": "post comment even if there's no change in coverage", + "default": True, + }, + True: { + "type": bool, + "description": "only post comment if there are changes in coverage (positive or negative)", + }, + "coverage_drop": { + "type": str, + "description": "[project coverage] only post comment if coverage drops more than percent when comparing HEAD to BASE", + }, + "uncovered_patch": { + "type": str, + "description": "[patch coverage] only post comment if the patch has uncovered lines", + }, + }, + }, + }, + "require_base": {"type": "boolean"}, + "require_head": {"type": "boolean"}, + "show_critical_paths": {"type": "boolean"}, + "branches": branches_structure, + "paths": path_list_structure, # DEPRECATED + "flags": flag_list_structure, # DEPRECATED + "behavior": { + "type": "string", + "allowed": ("default", "once", "new", "spammy"), + }, + "after_n_builds": {"type": "integer", "min": 0}, + "show_carryforward_flags": {"type": "boolean"}, + "hide_comment_details": {"type": "boolean"}, + "hide_project_coverage": {"type": "boolean"}, +} + +bundle_analysis_comment_config = { + "require_bundle_changes": { + "type": ["boolean", "string"], + "allowed": ["bundle_increase", False, True], + "meta": { + "description": "require_bundle_changes instructs Codecov to only post a PR Comment if the requirements are met", + "options": { + False: { + "type": bool, + "description": "post comment even if there's no change in the bundle size", + "default": True, + }, + True: { + "type": bool, + "description": "only post comment if there are changes in bundle size (positive or negative)", + }, + "bundle_increase": { + "type": str, + "description": "only post comment if the bundle size increases", + }, + }, + }, + }, + "bundle_change_threshold": { + "coerce": "bundle_analysis_threshold", + "meta": { + "description": "Threshold for 'require_bundle_changes'. Notifications will only be triggered if the change is larger than the threshold." + }, + }, +} + +schema = { + "codecov": { + "type": "dict", + "schema": { + "url": { + "type": "string", + "regex": r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)", + }, + "token": {"type": "string"}, + "slug": {"type": "string"}, + "bot": {"type": "string", "nullable": True}, + "branch": {"type": "string", "coerce": "branch_normalize"}, + "ci": {"type": "list", "schema": {"type": "string"}}, + "assume_all_flags": {"type": "boolean"}, + "strict_yaml_branch": {"type": "string"}, + "max_report_age": {"type": ["string", "integer", "boolean"]}, + "disable_default_path_fixes": {"type": "boolean"}, + "require_ci_to_pass": {"type": "boolean"}, + "allow_coverage_offsets": {"type": "boolean"}, # [DEPRECATED] + "allow_pseudo_compare": {"type": "boolean"}, + "archive": {"type": "dict", "schema": {"uploads": {"type": "boolean"}}}, + "notify": { + "type": "dict", + "schema": { + "after_n_builds": {"type": "integer", "min": 0}, + "countdown": {"type": "integer"}, + "delay": {"type": "integer"}, + "wait_for_ci": {"type": "boolean"}, + "require_ci_to_pass": {"type": "boolean"}, # [DEPRECATED] + "manual_trigger": {"type": "boolean"}, + "notify_error": { + "meta": { + "description": "This option lets the user toggle whether they want to block the regular comment message and replace it with an error message in the comment if any of the upload processing tasks fail." + }, + "type": "boolean", + }, + }, + }, + "ui": { + "type": "dict", + "schema": { + "hide_density": { + "anyof": [ + {"type": "boolean"}, + {"type": "list", "schema": {"type": "string"}}, + ] + }, + "hide_complexity": { + "anyof": [ + {"type": "boolean"}, + {"type": "list", "schema": {"type": "string"}}, + ] + }, + "hide_contexual": {"type": "boolean"}, + "hide_sunburst": {"type": "boolean"}, + "hide_search": {"type": "boolean"}, + }, + }, + }, + }, + "coverage": { + "type": "dict", + "schema": { + "precision": {"type": "integer", "min": 0, "max": 99}, + "round": {"type": "string", "allowed": ("down", "up", "nearest")}, + "range": {"type": "list", "maxlength": 2, "coerce": "string_to_range"}, + "notify": { + "type": "dict", + "schema": { + "irc": { + "type": "dict", + "keysrules": {"type": "string", "regex": r"^[\w\-\.]+$"}, + "valuesrules": { + "type": "dict", + "schema": { + "channel": {"type": "string"}, + "server": {"type": "string"}, + "password": { + "type": "string", + "coerce": "secret", + "nullable": True, + }, + "nickserv_password": { + "type": "string", + "coerce": "secret", + }, + "notice": {"type": "boolean"}, + **notification_standard_attributes, + }, + }, + }, + "slack": { + "type": "dict", + "keysrules": {"type": "string", "regex": r"^[\w\-\.]+$"}, + "valuesrules": { + "type": "dict", + "schema": { + "attachments": layout_structure, + **notification_standard_attributes, + }, + }, + }, + "gitter": { + "type": "dict", + "keysrules": {"type": "string", "regex": r"^[\w\-\.]+$"}, + "valuesrules": { + "type": "dict", + "schema": {**notification_standard_attributes}, + }, + }, + "hipchat": { + "type": "dict", + "keysrules": {"type": "string", "regex": r"^[\w\-\.]+$"}, + "valuesrules": { + "type": "dict", + "schema": { + "card": {"type": "boolean"}, + "notify": {"type": "boolean"}, + **notification_standard_attributes, + }, + }, + }, + "webhook": { + "type": "dict", + "keysrules": {"type": "string", "regex": r"^[\w\-\.]+$"}, + "valuesrules": { + "type": "dict", + "schema": {**notification_standard_attributes}, + }, + }, + "email": { + "type": "dict", + "keysrules": {"type": "string", "regex": r"^[\w\-\.]+$"}, + "valuesrules": { + "type": "dict", + "schema": { + "to": { + "type": "list", + "schema": {"type": "string", "coerce": "secret"}, + }, + "layout": layout_structure, + **notification_standard_attributes, + }, + }, + }, + }, + }, + "status": { + "type": ["boolean", "dict"], + "schema": { + "default_rules": { + "type": "dict", + "schema": { + "flag_coverage_not_uploaded_behavior": { + "type": "string", + "allowed": ("include", "exclude", "pass"), + }, + "carryforward_behavior": { + "type": "string", + "allowed": ("include", "exclude", "pass"), + }, + }, + }, + "project": { + "type": ["dict", "boolean"], + "keysrules": {"type": "string", "regex": r"^[\w\-\.]+$"}, + "valuesrules": { + "nullable": True, + "type": ["dict", "boolean"], + "schema": { + "target": percent_type_or_auto, + "include_changes": percent_type_or_auto, + "threshold": percent_type, + **status_standard_attributes, + }, + }, + }, + "patch": { + "type": ["dict", "boolean"], + "keysrules": {"type": "string", "regex": r"^[\w\-\.]+$"}, + "valuesrules": { + "type": ["dict", "boolean"], + "nullable": True, + "schema": { + "target": percent_type_or_auto, + "include_changes": percent_type_or_auto, + "threshold": percent_type, + **status_standard_attributes, + }, + }, + }, + "changes": { + "type": ["dict", "boolean"], + "keysrules": {"type": "string", "regex": r"^[\w\-\.]+$"}, + "valuesrules": { + "type": ["dict", "boolean"], + "nullable": True, + "schema": status_standard_attributes, + }, + }, + "no_upload_behavior": { + "type": "string", + "allowed": ("pass", "fail"), + }, + }, + }, + }, + }, + "bundle_analysis": { + "type": "dict", + "schema": { + "warning_threshold": { + "coerce": "bundle_analysis_threshold", + "meta": { + "description": "If the change is bigger then the threshold notification includes a warning or fails. See `bundle_analysis.status` for details." + }, + }, + "status": { + "allowed": (True, False, "informational"), + "meta": { + "description": "Configure commit checks for bundle analysis", + "options": { + True: { + "type": bool, + "description": "Enable status. Status will fail if changes exceed `bundle_analysis.warning_threshold`", + }, + False: { + "type": bool, + "description": "Disable status. No status will be sent.", + }, + "informational": { + "type": str, + "description": "Enable status. Status will always pass, but include a warning if changes exceed `bundle_analysis.warning_threshold`", + "default": True, + }, + }, + }, + }, + }, + }, + "parsers": { + "type": "dict", + "schema": { + "go": {"type": "dict", "schema": {"partials_as_hits": {"type": "boolean"}}}, + "javascript": { + "type": "dict", + "schema": {"enable_partials": {"type": "boolean"}}, + }, + "v1": { + "type": "dict", + "schema": {"include_full_missed_files": {"type": "boolean"}}, + }, # [DEPRECATED] + "gcov": { + "type": "dict", + "schema": { + "branch_detection": { + "type": "dict", + "schema": { + "conditional": {"type": "boolean"}, + "loop": {"type": "boolean"}, + "method": {"type": "boolean"}, + "macro": {"type": "boolean"}, + }, + } + }, + }, + "jacoco": { + "type": "dict", + "schema": {"partials_as_hits": {"type": "boolean"}}, + }, + "cobertura": { + "type": "dict", + "schema": { + "handle_missing_conditions": {"type": "boolean"}, + "partials_as_hits": {"type": "boolean"}, + }, + }, + }, + }, + "ignore": path_list_structure, + "fixes": { + "type": "list", + "schema": {"type": "string", "coerce": "regexify_path_fix"}, + }, + "flags": { + "type": "dict", + "keysrules": flag_name, + "valuesrules": { + "type": "dict", + "schema": { + "joined": {"type": "boolean"}, + "carryforward": {"type": "boolean"}, + "carryforward_mode": { + "type": "string", + "allowed": ("all", "labels"), + }, + "required": {"type": "boolean"}, + "ignore": path_list_structure, + "paths": path_list_structure, + "assume": { + "type": ["boolean", "string", "dict"], + "schema": {"branches": branches_structure}, + }, + "after_n_builds": {"type": "integer", "min": 0}, + }, + }, + }, + "flag_management": { + "type": "dict", + "schema": { + "default_rules": {"type": "dict", "schema": flags_rule_basic_properties}, + "individual_flags": { + "type": "list", + "schema": { + "type": "dict", + "schema": { + **flags_rule_basic_properties, + "name": flag_name, + }, + }, + }, + }, + }, + "component_management": { + "type": "dict", + "schema": { + "default_rules": { + "type": "dict", + "schema": component_rule_basic_properties, + }, + "individual_components": { + "type": "list", + "schema": { + "type": "dict", + "schema": { + **component_rule_basic_properties, + "name": {"type": "string"}, + "component_id": {"type": "string", "required": True}, + }, + }, + }, + }, + }, + "comment": { + "type": ["dict", "boolean"], + "schema": {**coverage_comment_config, **bundle_analysis_comment_config}, + }, + "slack_app": { + "type": ["dict", "boolean"], + "schema": {"enabled": {"type": "boolean"}}, + }, + "github_checks": { + "type": ["dict", "boolean"], + "schema": {"annotations": {"type": "boolean"}}, + }, + "profiling": { + "type": "dict", + "schema": { + "fixes": { + "type": "list", + "schema": {"type": "string", "coerce": "regexify_path_fix"}, + }, + "grouping_attributes": {"type": "list", "schema": {"type": "string"}}, + "critical_files_paths": { + "type": "list", + "schema": {"type": "string", "coerce": "regexify_path_pattern"}, + }, + }, + }, + "beta_groups": {"type": "list", "schema": {"type": "string"}}, + "ai_pr_review": { + "type": ["dict"], + "schema": { + "enabled": {"type": "boolean"}, + "method": {"type": "string"}, + "label_name": {"type": "string"}, + }, + }, + "test_analytics": { + "type": ["dict"], + "schema": { + "shorten_paths": { + "type": "boolean", + }, + "flake_detection": {"type": "boolean"}, + }, + }, +} diff --git a/libs/shared/shared/validation/validator.py b/libs/shared/shared/validation/validator.py new file mode 100644 index 0000000000..69c5ccf439 --- /dev/null +++ b/libs/shared/shared/validation/validator.py @@ -0,0 +1,62 @@ +from cerberus import Validator + +from shared.validation.helpers import ( + BranchSchemaField, + BundleSizeThresholdSchemaField, + ByteSizeSchemaField, + CoverageCommentRequirementSchemaField, + CoverageRangeSchemaField, + CustomFixPathSchemaField, + Invalid, + LayoutStructure, + PathPatternSchemaField, + PercentSchemaField, + UserGivenBranchRegex, +) + + +class CodecovYamlValidator(Validator): + def _normalize_coerce_secret(self, value: str) -> str: + return value + + def _normalize_coerce_regexify_path_pattern(self, value): + return PathPatternSchemaField().validate(value) + + def _normalize_coerce_regexify_path_fix(self, value): + return CustomFixPathSchemaField().validate(value) + + def _normalize_coerce_branch_name(self, value): + return UserGivenBranchRegex().validate(value) + + def _normalize_coerce_percentage_to_number(self, value): + return PercentSchemaField().validate(value) + + def _normalize_coerce_percentage_to_number_or_auto(self, value): + return PercentSchemaField().validate(value, allow_auto=True) + + def _normalize_coerce_string_to_range(self, value): + return CoverageRangeSchemaField().validate(value) + + def _normalize_coerce_branch_normalize(self, value): + return BranchSchemaField().validate(value) + + def _normalize_coerce_coverage_comment_required_changes(self, value): + return CoverageCommentRequirementSchemaField().validate(value) + + def _normalize_coerce_byte_size(self, value): + return ByteSizeSchemaField().validate(value) + + def _normalize_coerce_bundle_analysis_threshold(self, value): + return BundleSizeThresholdSchemaField().validate(value) + + def _validate_comma_separated_strings(self, constraint, field, value): + """Test the oddity of a value. + + The rule's arguments are validated against this schema: + {'type': 'boolean'} + """ + if constraint is True: + try: + LayoutStructure().validate(value) + except Invalid as exc: + self._error(field, exc.error_message) diff --git a/libs/shared/shared/yaml/__init__.py b/libs/shared/shared/yaml/__init__.py new file mode 100644 index 0000000000..0631a83734 --- /dev/null +++ b/libs/shared/shared/yaml/__init__.py @@ -0,0 +1,26 @@ +""" +Package for user yaml things + +This package depends on: + +- shared.config +- shared.validation + +And therefore should not be imported by those +""" + +from copy import deepcopy + +from .fetcher import ( + determine_commit_yaml_location, + fetch_current_yaml_from_provider_via_reference, +) +from .user_yaml import UserYaml, merge_yamls + +__all__ = [ + "deepcopy", + "determine_commit_yaml_location", + "fetch_current_yaml_from_provider_via_reference", + "UserYaml", + "merge_yamls", +] diff --git a/libs/shared/shared/yaml/fetcher.py b/libs/shared/shared/yaml/fetcher.py new file mode 100644 index 0000000000..717bb459c3 --- /dev/null +++ b/libs/shared/shared/yaml/fetcher.py @@ -0,0 +1,81 @@ +import logging +from typing import Any, Mapping, Sequence + +from shared.torngit.base import TorngitBaseAdapter +from shared.torngit.exceptions import TorngitObjectNotFoundError + +log = logging.getLogger(__name__) + + +async def fetch_current_yaml_from_provider_via_reference( + ref: str, repository_service: TorngitBaseAdapter +) -> str: + repoid = repository_service.data["repo"]["repoid"] + location = await determine_commit_yaml_location(ref, repository_service) + if not location: + log.info( + "We were not able to find the yaml on the provider API", + extra=dict(commit=ref, repoid=repoid), + ) + return None + log.info( + "Yaml was found on provider API", + extra=dict(commit=ref, repoid=repoid, location=location), + ) + try: + content = await repository_service.get_source(location, ref) + return content["content"] + except TorngitObjectNotFoundError: + log.exception( + "File not in %s for commit", extra=dict(commit=ref, location=location) + ) + + +async def determine_commit_yaml_location( + ref: str, repository_service: TorngitBaseAdapter +) -> str: + """ + Determines where in `ref` the codecov.yaml is, in a given repository + We currently look for the yaml in two different kinds of places + - Root level of the rpeository + - Specific folders that we know some customers use: + - `dev` + - `.github` + Args: + ref (str): The ref. Could be a branch name, tag, commit sha. + repository_service (torngit.base.TorngitBaseAdapter): The torngit handler that can fetch this data. + Indirectly determines the repository + Returns: + str: The path of the codecov.yaml file we found. Or `None,` if not found + """ + possible_locations = [ + "codecov.yml", + ".codecov.yml", + "codecov.yaml", + ".codecov.yaml", + ] + acceptable_folders = set(["dev", ".github"]) + top_level_files = await repository_service.list_top_level_files(ref) + top_level_yaml = _search_among_files(possible_locations, top_level_files) + if top_level_yaml is not None: + return top_level_yaml + all_folders = set(f["path"] for f in top_level_files if f["type"] == "folder") + possible_folders = all_folders & acceptable_folders + for folder in possible_folders: + files_inside_folder = await repository_service.list_files(ref, folder) + yaml_inside_folder = _search_among_files( + possible_locations, files_inside_folder + ) + if yaml_inside_folder: + return yaml_inside_folder + + +def _search_among_files( + desired_filenames: Sequence[str], all_files: Sequence[Mapping[str, Any]] +) -> str: + for file in all_files: + if ( + file.get("name") in desired_filenames + or file.get("path").split("/")[-1] in desired_filenames + ): + return file["path"] diff --git a/libs/shared/shared/yaml/user_yaml.py b/libs/shared/shared/yaml/user_yaml.py new file mode 100644 index 0000000000..43ac42628b --- /dev/null +++ b/libs/shared/shared/yaml/user_yaml.py @@ -0,0 +1,209 @@ +import logging +from copy import deepcopy +from dataclasses import dataclass +from datetime import datetime +from typing import Any, List, Optional + +from shared.components import Component +from shared.config import ( + PATCH_CENTRIC_DEFAULT_CONFIG, + PATCH_CENTRIC_DEFAULT_TIME_START, + get_config, +) + +log = logging.getLogger(__name__) + + +@dataclass +class OwnerContext(object): + ownerid: Optional[int] = None + owner_onboarding_date: Optional[datetime] = None + owner_plan: Optional[str] = None + + +class UserYaml(object): + def __init__(self, inner_dict): + self.inner_dict = inner_dict + + @classmethod + def from_dict(cls, input_dict): + return cls(input_dict) + + def __getitem__(self, key): + return self.inner_dict[key] + + def get(self, key, default=None): + return self.inner_dict.get(key, default) + + def to_dict(self): + return deepcopy(self.inner_dict) + + def read_yaml_field(self, *keys, _else=None) -> Any: + log.debug("Field %s requested", keys) + yaml_dict = self.inner_dict + try: + for key in keys: + yaml_dict = yaml_dict[key] + return yaml_dict + except (AttributeError, KeyError): + return _else + + def flag_has_carryfoward(self, flag_name): + legacy_flag = self.inner_dict.get("flags", {}).get(flag_name) + if legacy_flag: + return legacy_flag.get("carryforward") + flag_management = self.inner_dict.get("flag_management", {}) + for flag_info in flag_management.get("individual_flags", []): + if flag_info["name"] == flag_name: + if flag_info.get("carryforward") is not None: + return flag_info.get("carryforward") + return flag_management.get("default_rules", {}).get("carryforward", False) + + def has_any_carryforward(self): + all_flags = self.inner_dict.get("flags") + if all_flags: + for flag_info in all_flags.values(): + if flag_info.get("carryforward"): + return True + flag_management = self.inner_dict.get("flag_management", {}) + if flag_management.get("default_rules", {}).get("carryforward"): + return True + for flag_info in flag_management.get("individual_flags", []): + if flag_info.get("carryforward"): + return True + return False + + def get_flag_configuration(self, flag_name): + old_dict = self.inner_dict.get("flags", {}) + if flag_name in old_dict: + return old_dict[flag_name] + flag_management_dict = self.inner_dict.get("flag_management") + if flag_management_dict is None: + return None + general_dict = flag_management_dict.get("default_rules", {}) + individual_flags = flag_management_dict.get("individual_flags", []) + for f_dict in individual_flags: + if f_dict["name"] == flag_name: + res = deepcopy(general_dict) + res.update(f_dict) + return res + return deepcopy(general_dict) + + def get_components(self) -> List[Component]: + component_definitions = self.read_yaml_field("component_management") + if not component_definitions: + return [] + # Default set of rules that is overriden by individual components. + # The individual components inherit the values from default_definition if they don't have a particular key defined in the default rules + default_definition = component_definitions.get("default_rules", {}) + + individual_components = list( + map( + lambda component_dict: Component.from_dict( + {**default_definition, **component_dict} + ), + component_definitions.get("individual_components", []), + ) + ) + return individual_components + + def __eq__(self, other): + if not isinstance(other, UserYaml): + return False + return self.to_dict() == other.to_dict() + + def __str__(self): + return f"UserYaml<{self.inner_dict}>" + + @classmethod + def get_final_yaml( + cls, + *, + owner_yaml: Optional[dict[str, Any]] = None, + repo_yaml: Optional[dict[str, Any]] = None, + commit_yaml: Optional[dict[str, Any]] = None, + ownerid: int = None, + owner_context: Optional[OwnerContext] = None, + ): + """Given a owner yaml, repo yaml and user yaml, determines what yaml we need to use + + The answer is usually a "deep merge" between the site-level yaml, the + owner yaml (which is set by them at the UI) and either one of commit_yaml or repo_yaml + + Why does repo_yaml gets overriden by commit_yaml, but owner_yaml doesn't? + The idea is that the commit yaml is something at the repo level, which + at sometime will be used to replace the current repo yaml. + In fact, if that commit gets merged on main, then the old repo_yaml won't have any effect + anymore. So this guarantees that if you set yaml at a certain branch, when you merge + that branch into main the yaml will continue to have the same effect. + It would be a sucky behavior if your commit changes were you trying to get rid of a + repo level yaml config and we were still merging them. + + Args: + owner_yaml (nullable dict): The yaml that is on the owner level (ie at the owner table) + repo_yaml (nullable dict): [description] + commit_yaml (nullable dict): [description] (default: {None}) + ownerid: Optional[int] - [DEPRECATED] The ownerid. Used to get this owner's additional_user_yaml (configured via install yml, so self-hosted) + owner_context: Optional[OwnerContext] - Information about the owner that we are getting the yaml for. + This is used to control defaults and prevent overrides the owner can't do based on their user plan. + + Returns: + dict - The dict we are supposed to use when concerning that user/commit + """ + if ownerid is None: + # Ownerid is kept as an arg to avoid breaking change to the interface of the function + # Passing the info in the owner_context is preferred. + ownerid = owner_context.ownerid if owner_context else None + resulting_yaml = get_config("site", default={}) + if ownerid is not None and get_config("additional_user_yamls", default={}): + additional_yaml = _get_possible_additional_user_yaml(ownerid) + resulting_yaml = merge_yamls(resulting_yaml, additional_yaml) + + if owner_context and owner_context.owner_onboarding_date: + resulting_yaml = _fix_yaml_defaults_based_on_owner_onboarding_date( + resulting_yaml, owner_context.owner_onboarding_date + ) + + if owner_yaml is not None: + resulting_yaml = merge_yamls(resulting_yaml, owner_yaml) + + if commit_yaml is not None: + resulting_yaml = merge_yamls(resulting_yaml, commit_yaml) + elif repo_yaml is not None: + resulting_yaml = merge_yamls(resulting_yaml, repo_yaml) + return cls(resulting_yaml) + + +def _get_possible_additional_user_yaml(ownerid): + additional_user_yamls = get_config("additional_user_yamls", default={}) + key = ownerid % 100 + accumulated_percentage = 0 + for auy in additional_user_yamls: + accumulated_percentage += auy.get("percentage") + if key < accumulated_percentage: + return auy["override"] + return {} + + +def _fix_yaml_defaults_based_on_owner_onboarding_date( + current_yaml: dict, owner_onboarding_date: datetime +) -> dict: + """Changes the site defaults based on the owner_onboarding_date. + Owners onboarded (Owner.created_at) after PATCH_CENTRIC_DEFAULT_TIME_START use the + patch_centric_default_config instead of legacy_default_site_config. + + Owners can still override the defaults through owner_yaml, repo_yaml and commit_yaml. + """ + res = deepcopy(current_yaml) + if owner_onboarding_date > PATCH_CENTRIC_DEFAULT_TIME_START: + adding = deepcopy(PATCH_CENTRIC_DEFAULT_CONFIG) + res.update(adding) + return res + + +def merge_yamls(d1, d2): + if not isinstance(d1, dict) or not isinstance(d2, dict): + return deepcopy(d2) + res = deepcopy(d1) + res.update(dict([(k, merge_yamls(d1.get(k, {}), v)) for k, v in d2.items()])) + return res diff --git a/libs/shared/shared/yaml/validation.py b/libs/shared/shared/yaml/validation.py new file mode 100644 index 0000000000..42631c30c8 --- /dev/null +++ b/libs/shared/shared/yaml/validation.py @@ -0,0 +1,174 @@ +import binascii +import logging +from typing import Dict, List + +from shared.encryption.yaml_secret import yaml_secret_encryptor +from shared.validation.cli_schema import schema as cli_schema +from shared.validation.exceptions import InvalidYamlException +from shared.validation.user_schema import schema as user_schema +from shared.validation.validator import CodecovYamlValidator + +log = logging.getLogger(__name__) + +YAML_TOP_LEVEL_RESERVED_KEYS: List[str] = [ + "to_string" # Used to store the full YAML string preserving the original comments +] + +# *** Reminder: when changes are made to YAML validation, you will need to update the version of shared in both +# worker (to apply the changes) AND codecov-api (so the validate route reflects the changes) *** + + +def validate_yaml(inputted_yaml_dict, show_secrets_for=None): + """ + Receives a dict containing the Codecov yaml provided by the user, validates and normalizes + the fields for usage by other code. + + Args: + inputted_yaml_dict (dict): The Codecov yaml as parsed by a yaml parser and turned into a + dict + show_secrets_for (tuple): indicates a prefix for which we should show the decrypted + versions of any encrypted values. `worker `sets this to the proper tuple since we need + to use these values, codecov-api sets this to false since the validate + route is public and we don't want to unintentionally expose any sensitive user data. + + Returns: + (dict): A deep copy of the dict with the fields normalized + + Raises: + InvalidYamlException: If the yaml inputted by the user is not valid + """ + if not isinstance(inputted_yaml_dict, dict): + raise InvalidYamlException([], "Yaml needs to be a dict") + pre_process_yaml(inputted_yaml_dict) + return do_actual_validation(inputted_yaml_dict, show_secrets_for) + + +def remove_reserved_keys(inputted_yaml_dict: Dict[str, any]) -> None: + """ + This step removes reserved keywords in the base level of the json. + If any YAML is trying to be processed and validated those reserved keys will be ignored + """ + for key in YAML_TOP_LEVEL_RESERVED_KEYS: + if key in inputted_yaml_dict: + inputted_yaml_dict.pop(key) + + +def pre_process_yaml(inputted_yaml_dict): + """ + Changes the inputted_yaml_dict in-place with compatibility changes that need to be done + + Args: + inputted_yaml_dict (dict): The yaml dict inputted by the user + """ + coverage = inputted_yaml_dict.get("coverage", {}) + if "flags" in coverage: + inputted_yaml_dict["flags"] = coverage.pop("flags") + if "parsers" in coverage: + inputted_yaml_dict["parsers"] = coverage.pop("parsers") + if "ignore" in coverage: + inputted_yaml_dict["ignore"] = coverage.pop("ignore") + if "fixes" in coverage: + inputted_yaml_dict["fixes"] = coverage.pop("fixes") + if inputted_yaml_dict.get("codecov") and inputted_yaml_dict["codecov"].get( + "notify" + ): + if "require_ci_to_pass" in inputted_yaml_dict["codecov"]["notify"]: + val = inputted_yaml_dict["codecov"]["notify"].pop("require_ci_to_pass") + inputted_yaml_dict["codecov"]["require_ci_to_pass"] = val + + remove_reserved_keys(inputted_yaml_dict) + + +def _calculate_error_location_and_message_from_error_dict(error_dict): + current_value, location_so_far = error_dict, [] + steps_done = 0 + # max depth to avoid being put in a loop + while steps_done < 20: + if isinstance(current_value, list) and len(current_value) > 0: + current_value = current_value[0] + if isinstance(current_value, dict) and len(current_value) > 0: + first_key, first_value = next(iter((current_value.items()))) + location_so_far.append(first_key) + current_value = first_value + if isinstance(current_value, str): + return location_so_far, current_value + steps_done += 1 + return location_so_far, str(current_value) + + +class CodecovUserYamlValidator(CodecovYamlValidator): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._show_secrets_for = kwargs.get("show_secrets_for", False) + + def _normalize_coerce_secret(self, value: str) -> str: + """ + Coerces secret to normal value + """ + if self._show_secrets_for: + return UserGivenSecret(self._show_secrets_for).validate(value) + return value + + +class UserGivenSecret(object): + class InvalidSecret(Exception): + pass + + encryptor = yaml_secret_encryptor + + def __init__(self, show_secrets_for): + self.required_prefix = ( + "/".join(str(s) for s in show_secrets_for) if show_secrets_for else None + ) + + def validate(self, value): + if not self.required_prefix: + return value + if value is not None and value.startswith("secret:"): + try: + res = self.decode(value, self.required_prefix) + log.info( + "Valid secret was used by customer", + extra=dict(extra_data=self.required_prefix.split("/")), + ) + return res + except UserGivenSecret.InvalidSecret: + return value + return value + + @classmethod + def encode(cls, value): + return "secret:%s" % cls.encryptor.encode(value).decode() + + @classmethod + def decode(cls, value, expected_prefix): + try: + decoded_value = cls.encryptor.decode(value[7:]) + except binascii.Error: + raise UserGivenSecret.InvalidSecret() + except ValueError: + raise UserGivenSecret.InvalidSecret() + if expected_prefix is not None and not decoded_value.startswith( + expected_prefix + ): + raise UserGivenSecret.InvalidSecret() + service, ownerid, repoid, clean_value = decoded_value.split("/", 3) + return clean_value + + +def do_actual_validation(yaml_dict, show_secrets_for): + validator = CodecovUserYamlValidator(show_secrets_for=show_secrets_for) + full_schema = {**user_schema, **cli_schema} + is_valid = validator.validate(yaml_dict, full_schema) + if not is_valid: + error_dict = validator.errors + ( + error_location, + error_message, + ) = _calculate_error_location_and_message_from_error_dict(error_dict) + raise InvalidYamlException( + error_location=error_location, + error_dict=error_dict, + error_message=error_message, + ) + return validator.document diff --git a/libs/shared/tests/__init__.py b/libs/shared/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/tests/base.py b/libs/shared/tests/base.py new file mode 100644 index 0000000000..84ea8443ab --- /dev/null +++ b/libs/shared/tests/base.py @@ -0,0 +1,16 @@ +import os + + +class BaseTestCase(object): + def readfile(self, filename, if_empty_write=None): + with open(os.path.join(os.getcwd(), filename), "r") as r: + contents = r.read() + + # codecov: assert not covered start [FUTURE new concept] + if contents.strip() == "" and if_empty_write: + with open(os.path.join(os.getcwd(), filename), "w+") as r: + r.write(if_empty_write) + return if_empty_write + # codecov: end + + return contents diff --git a/libs/shared/tests/benchmarks/fixtures/worker.diff.zst b/libs/shared/tests/benchmarks/fixtures/worker.diff.zst new file mode 100644 index 0000000000..a5ceb9f295 Binary files /dev/null and b/libs/shared/tests/benchmarks/fixtures/worker.diff.zst differ diff --git a/libs/shared/tests/benchmarks/fixtures/worker_chunks.txt.zst b/libs/shared/tests/benchmarks/fixtures/worker_chunks.txt.zst new file mode 100644 index 0000000000..d5a3f6bb07 Binary files /dev/null and b/libs/shared/tests/benchmarks/fixtures/worker_chunks.txt.zst differ diff --git a/libs/shared/tests/benchmarks/fixtures/worker_report.json.zst b/libs/shared/tests/benchmarks/fixtures/worker_report.json.zst new file mode 100644 index 0000000000..101028261e Binary files /dev/null and b/libs/shared/tests/benchmarks/fixtures/worker_report.json.zst differ diff --git a/libs/shared/tests/benchmarks/test_report.py b/libs/shared/tests/benchmarks/test_report.py new file mode 100644 index 0000000000..8d6d790387 --- /dev/null +++ b/libs/shared/tests/benchmarks/test_report.py @@ -0,0 +1,167 @@ +from pathlib import Path + +import orjson +import pytest +import zstandard as zstd + +from shared.reports.carryforward import generate_carryforward_report +from shared.reports.resources import Report +from shared.torngit.base import TorngitBaseAdapter + + +def read_fixture(name: str) -> bytes: + path = Path(__file__).parent / "fixtures" / name + with open(path, "rb") as f: + data = f.read() + + dctx = zstd.ZstdDecompressor() + return dctx.decompress(data) + + +def load_report() -> tuple[bytes, bytes]: + raw_chunks = read_fixture("worker_chunks.txt.zst") + raw_report_json = read_fixture("worker_report.json.zst") + + return raw_chunks, raw_report_json + + +def load_diff() -> dict: + contents = read_fixture("worker.diff.zst").decode() + + torngit = TorngitBaseAdapter() + return torngit.diff_to_json(contents) + + +def do_parse(raw_report_json, raw_chunks): + report_json = orjson.loads(raw_report_json) + chunks = raw_chunks.decode() + return Report.from_chunks( + chunks=chunks, files=report_json["files"], sessions=report_json["sessions"] + ) + + +def do_full_parse(raw_report_json, raw_chunks): + report = do_parse(raw_report_json, raw_chunks) + # full parsing in this case means iterating over everything in the report + for file in report: + # Which in particular means iterating over every `ReportLine`s, + # which are unfortunately being re-parsed *every damn time* :-( + for _line in file: + pass + + return report + + +def test_parse_shallow(benchmark): + raw_chunks, raw_report_json = load_report() + + def bench_fn(): + do_parse(raw_report_json, raw_chunks) + + benchmark(bench_fn) + + +def test_parse_full(benchmark): + raw_chunks, raw_report_json = load_report() + + def bench_fn(): + do_full_parse(raw_report_json, raw_chunks) + + benchmark(bench_fn) + + +def test_process_totals(benchmark): + raw_chunks, raw_report_json = load_report() + + report = do_full_parse(raw_report_json, raw_chunks) + + def bench_fn(): + # both `ReportFile` and `Report` have a cached `_totals` field, + # and a `totals` accessor calculating the cache on-demand + for file in report: + file._totals = None + _totals = file.totals + + report._totals = None + _totals = report.totals + + benchmark(bench_fn) + + +def test_report_filtering(benchmark): + raw_chunks, raw_report_json = load_report() + + report = do_full_parse(raw_report_json, raw_chunks) + + def bench_fn(): + filtered = report.filter(paths=[".*"], flags=["unit"]) + + for filename in filtered.files: + # contrary to the normal `Report`, `FilteredReport` does not have a `bind` parameter, + # but instead always maintains a cache + file = filtered.get(filename) + + # the `FilteredReportFile` has no `__iter__`, and all the other have no `.lines`. + # what they do have in common is `eof` and `get`: + for ln in range(1, file.eof): + file.get(ln) + + file._totals = None + _totals = file.totals + + filtered._totals = None + _totals = filtered.totals + + benchmark(bench_fn) + + +@pytest.mark.parametrize( + "do_filter", + [pytest.param(False, id="Report"), pytest.param(True, id="FilteredReport")], +) +def test_report_diff_calculation(do_filter, benchmark): + raw_chunks, raw_report_json = load_report() + diff = load_diff() + + report = do_full_parse(raw_report_json, raw_chunks) + if do_filter: + report = report.filter(paths=[".*"], flags=["unit"]) + + def bench_fn(): + report.apply_diff(diff) + + benchmark(bench_fn) + + +def test_report_serialize(benchmark): + raw_chunks, raw_report_json = load_report() + + report = do_parse(raw_report_json, raw_chunks) + + def bench_fn(): + report.serialize() + + benchmark(bench_fn) + + +def test_report_merge(benchmark): + raw_chunks, raw_report_json = load_report() + + report2 = do_full_parse(raw_report_json, raw_chunks) + + def bench_fn(): + report1 = do_parse(raw_report_json, raw_chunks) + report1.merge(report2) + + benchmark(bench_fn) + + +def test_report_carryforward(benchmark): + raw_chunks, raw_report_json = load_report() + + report = do_full_parse(raw_report_json, raw_chunks) + + def bench_fn(): + generate_carryforward_report(report, paths=[".*"], flags=["unit"]) + + benchmark(bench_fn) diff --git a/libs/shared/tests/conftest.py b/libs/shared/tests/conftest.py new file mode 100644 index 0000000000..5019dad3d0 --- /dev/null +++ b/libs/shared/tests/conftest.py @@ -0,0 +1,115 @@ +from pathlib import Path + +import pytest +import vcr + +from shared.config import ConfigHelper +from shared.reports.resources import Report, ReportFile, Session +from shared.reports.types import LineSession, ReportLine + + +@pytest.fixture +def mock_configuration(mocker): + m = mocker.patch("shared.config._get_config_instance") + mock_config = ConfigHelper() + m.return_value = mock_config + our_config = { + "bitbucket": {"bot": {"username": "codecov-io"}}, + "services": { + "minio": { + "access_key_id": "codecov-default-key", + "bucket": "archive", + "hash_key": "88f572f4726e4971827415efa8867978", + "secret_access_key": "codecov-default-secret", + "verify_ssl": False, + "port": "9000", + }, + "redis_url": "redis://redis:@localhost:6379/", + }, + "setup": { + "codecov_url": "https://codecov.io", + "encryption_secret": "zp^P9*i8aR3", + }, + } + mock_config.set_params(our_config) + return mock_config + + +@pytest.fixture +def codecov_vcr(request): + current_path = Path(request.node.fspath) + current_path_name = current_path.name.replace(".py", "") + cls_name = request.node.cls.__name__ + cassette_path = current_path.parent / "cassetes" / current_path_name / cls_name + current_name = request.node.name + cassette_file_path = str(cassette_path / f"{current_name}.yaml") + with vcr.use_cassette( + cassette_file_path, + filter_headers=["authorization"], + filter_query_parameters=["oauth_nonce", "oauth_timestamp", "oauth_signature"], + record_mode="once", + match_on=["method", "scheme", "host", "port", "path", "query"], + ) as cassete_maker: + yield cassete_maker + + +@pytest.fixture +def mock_storage(mocker): + m = mocker.patch("covreports.storage.MinioStorageService") + redis_server = mocker.MagicMock() + m.return_value = redis_server + yield redis_server + + +@pytest.fixture +def sample_report(): + report = Report() + first_file = ReportFile("file_1.go") + second_file = ReportFile("file_2.go") + third_file = ReportFile("location/file_1.py") + first_file.append( + 1, + ReportLine.create( + coverage=1, + sessions=[LineSession(0, 1), LineSession(1, 1), LineSession(2, 1)], + ), + ) + first_file.append( + 2, + ReportLine.create(coverage=1, sessions=[LineSession(0, 0), LineSession(1, 1)]), + ) + first_file.append( + 3, + ReportLine.create(coverage=1, sessions=[LineSession(0, 1), LineSession(1, 0)]), + ) + first_file.append( + 5, + ReportLine.create(coverage=0, sessions=[LineSession(0, 0), LineSession(1, 0)]), + ) + first_file.append( + 6, + ReportLine.create( + coverage="1/2", + sessions=[LineSession(0, "1/2"), LineSession(1, 0), LineSession(2, "1/4")], + ), + ) + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, "1/2"]]) + ) + third_file.append( + 100, ReportLine.create(coverage="1/2", type="b", sessions=[[3, "1/2"]]) + ) + third_file.append( + 101, + ReportLine.create(coverage="1/2", type="b", sessions=[[2, "1/2"], [3, "1/2"]]), + ) + report.append(first_file) + report.append(second_file) + report.append(third_file) + report.add_session(Session(id=0, flags=["simple"])) + report.add_session(Session(id=1, flags=["complex"])) + report.add_session(Session(id=2, flags=["complex", "simple"])) + report.add_session(Session(id=3, flags=[])) + # TODO manually fix Session totals because the defautl logic doesn't + return report diff --git a/libs/shared/tests/helper.py b/libs/shared/tests/helper.py new file mode 100644 index 0000000000..b92739ed73 --- /dev/null +++ b/libs/shared/tests/helper.py @@ -0,0 +1,224 @@ +from json import dumps + +from shared.django_apps.codecov_auth.models import BillingRate +from shared.django_apps.codecov_auth.tests.factories import PlanFactory, TierFactory +from shared.plan.constants import ( + DEFAULT_FREE_PLAN, + PlanName, + PlanPrice, + TierName, +) + + +def v2_to_v3(report): + def _sessions(sessions): + if sessions: + return [ + [int(sid), data.get("p") or data["c"], data.get("b")] + for sid, data in sessions.items() + ] + return None + + files = {} + chunks = [] + for loc, (fname, data) in enumerate(report.get("files", {}).items()): + totals = data.get("t", {}).get + files[fname] = [ + loc, + [totals(k, 0) for k in "fnhmpcbdMs"] if totals("n") else None, + ] + chunk = [""] + if data.get("l"): + lines = data["l"].get + for ln in range(1, max(list(map(int, list(data["l"].keys())))) + 1): + line = lines(str(ln)) + if line: + chunk.append( + dumps( + [ + line.get("c"), + line.get("t"), + _sessions(line.get("s")), + None, + ] + ) + ) + else: + chunk.append("") + chunks.append("\n".join(chunk)) + + return { + "files": files, + "sessions": dict( + [(int(sid), data) for sid, data in report.get("sessions", {}).items()] + ), + "totals": report.get("totals", {}), + "chunks": chunks, + } + + +def mock_all_plans_and_tiers(): + trial_tier = TierFactory(tier_name=TierName.TRIAL.value) + PlanFactory( + tier=trial_tier, + name=PlanName.TRIAL_PLAN_NAME.value, + paid_plan=False, + marketing_name="Developer", + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + ) + + basic_tier = TierFactory(tier_name=TierName.BASIC.value) + PlanFactory( + name=PlanName.FREE_PLAN_NAME.value, + tier=basic_tier, + marketing_name="Developer", + benefits=[ + "Up to 1 user", + "Unlimited public repositories", + "Unlimited private repositories", + ], + ) + + pro_tier = TierFactory(tier_name=TierName.PRO.value) + PlanFactory( + name=PlanName.CODECOV_PRO_MONTHLY.value, + tier=pro_tier, + marketing_name="Pro", + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + billing_rate=BillingRate.MONTHLY.value, + base_unit_price=PlanPrice.MONTHLY.value, + paid_plan=True, + ) + PlanFactory( + name=PlanName.CODECOV_PRO_YEARLY.value, + tier=pro_tier, + marketing_name="Pro", + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + billing_rate=BillingRate.ANNUALLY.value, + base_unit_price=PlanPrice.YEARLY.value, + paid_plan=True, + ) + + team_tier = TierFactory(tier_name=TierName.TEAM.value) + PlanFactory( + name=PlanName.TEAM_MONTHLY.value, + tier=team_tier, + marketing_name="Team", + benefits=[ + "Up to 10 users", + "Unlimited repositories", + "2500 private repo uploads", + "Patch coverage analysis", + ], + billing_rate=BillingRate.MONTHLY.value, + base_unit_price=PlanPrice.TEAM_MONTHLY.value, + monthly_uploads_limit=2500, + paid_plan=True, + ) + PlanFactory( + name=PlanName.TEAM_YEARLY.value, + tier=team_tier, + marketing_name="Team", + benefits=[ + "Up to 10 users", + "Unlimited repositories", + "2500 private repo uploads", + "Patch coverage analysis", + ], + billing_rate=BillingRate.ANNUALLY.value, + base_unit_price=PlanPrice.TEAM_YEARLY.value, + monthly_uploads_limit=2500, + paid_plan=True, + ) + + sentry_tier = TierFactory(tier_name=TierName.SENTRY.value) + PlanFactory( + name=PlanName.SENTRY_MONTHLY.value, + tier=sentry_tier, + marketing_name="Sentry Pro", + billing_rate=BillingRate.MONTHLY.value, + base_unit_price=PlanPrice.MONTHLY.value, + paid_plan=True, + benefits=[ + "Includes 5 seats", + "$12 per additional seat", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + ) + PlanFactory( + name=PlanName.SENTRY_YEARLY.value, + tier=sentry_tier, + marketing_name="Sentry Pro", + billing_rate=BillingRate.ANNUALLY.value, + base_unit_price=PlanPrice.YEARLY.value, + paid_plan=True, + benefits=[ + "Includes 5 seats", + "$10 per additional seat", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + ) + + enterprise_tier = TierFactory(tier_name=TierName.ENTERPRISE.value) + PlanFactory( + name=PlanName.ENTERPRISE_CLOUD_MONTHLY.value, + tier=enterprise_tier, + marketing_name="Enterprise Cloud", + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + billing_rate=BillingRate.MONTHLY.value, + base_unit_price=PlanPrice.MONTHLY.value, + paid_plan=True, + ) + PlanFactory( + name=PlanName.ENTERPRISE_CLOUD_YEARLY.value, + tier=enterprise_tier, + marketing_name="Enterprise Cloud", + billing_rate=BillingRate.ANNUALLY.value, + base_unit_price=PlanPrice.YEARLY.value, + paid_plan=True, + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + ) + + PlanFactory( + name=DEFAULT_FREE_PLAN, + tier=team_tier, + marketing_name="Developer", + billing_rate=None, + base_unit_price=0, + paid_plan=False, + monthly_uploads_limit=250, + benefits=[ + "Up to 1 user", + "Unlimited public repositories", + "Unlimited private repositories", + ], + ) diff --git a/libs/shared/tests/integration/__init__.py b/libs/shared/tests/integration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_comment.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_comment.yaml new file mode 100644 index 0000000000..16f22386f2 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_comment.yaml @@ -0,0 +1,58 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: DELETE + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/107383471?oauth_version=1.0&oauth_token=testss3hxhcfqf1h6g&oauth_nonce=0441f5ae229a4884801e97004003b6a8&oauth_timestamp=1561670312&oauth_signature=bRwiaWHiDGhoJQQuFG9%2BNwbERAg%3D&oauth_consumer_key=arubajamaicaohiwan&oauth_signature_method=HMAC-SHA1 + response: + content: '' + headers: + Connection: + - close + Content-Length: + - '0' + Content-Type: + - text/html; charset=utf-8 + Date: + - Thu, 27 Jun 2019 21:18:34 GMT + Etag: + - '"d41d8cd98f00b204e9800998ecf8427e"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + X-Accepted-Oauth-Scopes: + - pullrequest + X-Cache-Info: + - not cacheable; request wasn't a GET or HEAD + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.180292129517' + X-Request-Count: + - '78' + X-Served-By: + - app-139 + X-Static-Version: + - d664982621c0 + X-Version: + - d664982621c0 + status: + code: 204 + message: No Content + status_code: 204 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/107383471?oauth_version=1.0&oauth_token=testss3hxhcfqf1h6g&oauth_nonce=0441f5ae229a4884801e97004003b6a8&oauth_timestamp=1561670312&oauth_signature=bRwiaWHiDGhoJQQuFG9%2BNwbERAg%3D&oauth_consumer_key=arubajamaicaohiwan&oauth_signature_method=HMAC-SHA1 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_comment_not_found.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_comment_not_found.yaml new file mode 100644 index 0000000000..e8cf77a13e --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_comment_not_found.yaml @@ -0,0 +1,56 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: DELETE + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/113977999?oauth_version=1.0&oauth_token=testss3hxhcfqf1h6g&oauth_nonce=331c6e3852554f5497b04e564dc673c2&oauth_timestamp=1561668838&oauth_signature=Z9fmZ6u%2FG%2Bimv4BHN1PNy7v3WkE%3D&oauth_consumer_key=arubajamaicaohiwan&oauth_signature_method=HMAC-SHA1 + response: + content: '{"type": "error", "error": {"message": "113977999"}}' + headers: + Connection: + - close + Content-Length: + - '52' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 27 Jun 2019 20:53:59 GMT + Etag: + - '"e24710c02940cb5198999dd1d60d6ee4"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + X-Accepted-Oauth-Scopes: + - pullrequest + X-Cache-Info: + - not cacheable; request wasn't a GET or HEAD + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.0523722171783' + X-Request-Count: + - '430' + X-Served-By: + - app-140 + X-Static-Version: + - d664982621c0 + X-Version: + - d664982621c0 + status: + code: 404 + message: Not Found + status_code: 404 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/113977999?oauth_version=1.0&oauth_token=testss3hxhcfqf1h6g&oauth_nonce=331c6e3852554f5497b04e564dc673c2&oauth_timestamp=1561668838&oauth_signature=Z9fmZ6u%2FG%2Bimv4BHN1PNy7v3WkE%3D&oauth_consumer_key=arubajamaicaohiwan&oauth_signature_method=HMAC-SHA1 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_webhook.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_webhook.yaml new file mode 100644 index 0000000000..76df133052 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_webhook.yaml @@ -0,0 +1,58 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: DELETE + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/hooks/4742f092-8397-4677-8876-5e9a06f10f98?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541602115&oauth_nonce=ab985dba77534c24929b8d013d2cb876&oauth_version=1.0&oauth_signature=BCk5R%2Bbn4a3MUcDiyTQ3HF2X%2BW8%3D + response: + content: '' + headers: + Connection: + - close + Content-Length: + - '0' + Content-Type: + - text/plain + Date: + - Wed, 07 Nov 2018 14:48:36 GMT + Etag: + - '"d41d8cd98f00b204e9800998ecf8427e"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + X-Accepted-Oauth-Scopes: + - webhook + X-Cache-Info: + - not cacheable; request wasn't a GET or HEAD + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.100928068161' + X-Request-Count: + - '182' + X-Served-By: + - app-188 + X-Static-Version: + - 1f6d684e3c29 + X-Version: + - 1f6d684e3c29 + status: + code: 204 + message: No Content + status_code: 204 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/hooks/4742f092-8397-4677-8876-5e9a06f10f98?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541602115&oauth_nonce=ab985dba77534c24929b8d013d2cb876&oauth_version=1.0&oauth_signature=BCk5R%2Bbn4a3MUcDiyTQ3HF2X%2BW8%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_webhook_not_found.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_webhook_not_found.yaml new file mode 100644 index 0000000000..3f042b55ea --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_delete_webhook_not_found.yaml @@ -0,0 +1,57 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: DELETE + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/hooks/4742f011-8397-aa77-8876-5e9a06f10f98?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541602198&oauth_nonce=fc8ef877776d441bab406f7a47396073&oauth_version=1.0&oauth_signature=A%2F46Dp0fgbZI9lkjovmcu4LfGiw%3D + response: + content: '{"type": "error", "error": {"message": "example-python is not a valid + hook"}}' + headers: + Connection: + - close + Content-Length: + - '77' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 07 Nov 2018 14:49:59 GMT + Etag: + - '"019115b9f80b7504bb4e084e8c750c15"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + X-Accepted-Oauth-Scopes: + - webhook + X-Cache-Info: + - not cacheable; request wasn't a GET or HEAD + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.0818870067596' + X-Request-Count: + - '308' + X-Served-By: + - app-139 + X-Static-Version: + - 1f6d684e3c29 + X-Version: + - 1f6d684e3c29 + status: + code: 404 + message: Not Found + status_code: 404 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/hooks/4742f011-8397-aa77-8876-5e9a06f10f98?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541602198&oauth_nonce=fc8ef877776d441bab406f7a47396073&oauth_version=1.0&oauth_signature=A%2F46Dp0fgbZI9lkjovmcu4LfGiw%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_comment.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_comment.yaml new file mode 100644 index 0000000000..e8bf6083c9 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_comment.yaml @@ -0,0 +1,77 @@ +interactions: +- request: + body: '{"content": {"raw": "Hello world numbah 2"}}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Default + method: PUT + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/114320127?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1566631440&oauth_nonce=5aa764bebf044e64beb9d9df2912cc08&oauth_version=1.0&oauth_signature=2mI6UpQrKaN8s0sL9N%2F9ItZaF7o%3D + response: + content: '{"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/114320127"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/pull-requests/1/_/diff#comment-114320127"}}, + "deleted": false, "pullrequest": {"type": "pullrequest", "id": 1, "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/pull-requests/1"}}, + "title": "Hahaa That is a PR"}, "content": {"raw": "Hello world numbah 2", "markup": + "markdown", "html": "

    Hello world numbah 2

    ", "type": "rendered"}, "created_on": + "2019-08-24T07:22:19.710114+00:00", "user": {"display_name": "Thiago Ramos", + "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "updated_on": "2019-08-24T07:24:00.990289+00:00", "type": "pullrequest_comment", + "id": 114320127}' + headers: + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Sat, 24 Aug 2019 07:24:01 GMT + Etag: + - '"gz[b66344ea32af191ed90c8f1655af55b3]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - pullrequest + X-Cache-Info: + - not cacheable; request wasn't a GET or HEAD + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Reads-Before-Write-From: + - default + X-Render-Time: + - '0.526782035828' + X-Request-Count: + - '78' + X-Served-By: + - app-144 + X-Static-Version: + - 4973dbe55bef + X-Version: + - 4973dbe55bef + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/114320127?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1566631440&oauth_nonce=5aa764bebf044e64beb9d9df2912cc08&oauth_version=1.0&oauth_signature=2mI6UpQrKaN8s0sL9N%2F9ItZaF7o%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_comment_not_found.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_comment_not_found.yaml new file mode 100644 index 0000000000..168712fed9 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_comment_not_found.yaml @@ -0,0 +1,60 @@ +interactions: +- request: + body: '{"content": {"raw": "Hello world number 2"}}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Default + method: PUT + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/113979999?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1566631150&oauth_nonce=5aeac543acea436d9f0594d750a98dd3&oauth_version=1.0&oauth_signature=16DIGrfTGyeo8k90tViVI%2BcqBPw%3D + response: + content: '{"type": "error", "error": {"message": "113979999"}}' + headers: + Connection: + - close + Content-Length: + - '52' + Content-Type: + - application/json; charset=utf-8 + Date: + - Sat, 24 Aug 2019 07:19:11 GMT + Etag: + - '"f30cdf4645cc1f60db842d167b97cab5"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + X-Accepted-Oauth-Scopes: + - pullrequest + X-Cache-Info: + - not cacheable; request wasn't a GET or HEAD + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Reads-Before-Write-From: + - default + X-Render-Time: + - '0.0458209514618' + X-Request-Count: + - '614' + X-Served-By: + - app-159 + X-Static-Version: + - 4973dbe55bef + X-Version: + - 4973dbe55bef + status: + code: 404 + message: Not Found + status_code: 404 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/113979999?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1566631150&oauth_nonce=5aeac543acea436d9f0594d750a98dd3&oauth_version=1.0&oauth_signature=16DIGrfTGyeo8k90tViVI%2BcqBPw%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_webhook.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_webhook.yaml new file mode 100644 index 0000000000..3c688cf841 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_edit_webhook.yaml @@ -0,0 +1,70 @@ +interactions: +- request: + body: '{"description": "new_name", "active": true, "events": ["issue:updated"], + "url": "http://requestbin.net/r/1ecyaj51"}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Default + method: PUT + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/hooks/4742f092-8397-4677-8876-5e9a06f10f98?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541602041&oauth_nonce=ddb0335319f246d98933af48a59371e5&oauth_version=1.0&oauth_signature=widTrBs%2BlgaUlwKjY2YTvYMpWlc%3D + response: + content: '{"read_only": null, "description": "new_name", "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/hooks/%7B4742f092-8397-4677-8876-5e9a06f10f98%7D"}}, + "url": "http://requestbin.net/r/1ecyaj51", "created_at": "2018-11-07T14:45:47.900077Z", + "skip_cert_verification": false, "source": null, "history_enabled": false, "active": + true, "subject": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "type": "webhook_subscription", + "events": ["issue:updated"], "uuid": "{4742f092-8397-4677-8876-5e9a06f10f98}"}' + headers: + Connection: + - close + Content-Length: + - '931' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 07 Nov 2018 14:47:22 GMT + Etag: + - '"35d4c308470a7423364804ad93cfbe22"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + X-Accepted-Oauth-Scopes: + - webhook + X-Cache-Info: + - not cacheable; request wasn't a GET or HEAD + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.140641927719' + X-Request-Count: + - '236' + X-Served-By: + - app-140 + X-Static-Version: + - 1f6d684e3c29 + X-Version: + - 1f6d684e3c29 + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/hooks/4742f092-8397-4677-8876-5e9a06f10f98?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541602041&oauth_nonce=ddb0335319f246d98933af48a59371e5&oauth_version=1.0&oauth_signature=widTrBs%2BlgaUlwKjY2YTvYMpWlc%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_find_pull_request_nothing_found.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_find_pull_request_nothing_found.yaml new file mode 100644 index 0000000000..1c521f9978 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_find_pull_request_nothing_found.yaml @@ -0,0 +1,60 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests?state=OPEN&page=1&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541516646&oauth_nonce=15af96bfd6e546acaec32b3dac09fe42&oauth_version=1.0&oauth_signature=4E9qt33md6cz0B%2BnnL62C2hs7jA%3D + response: + content: '{"pagelen": 10, "values": [], "page": 1, "size": 0}' + headers: + Cache-Control: + - max-age=900 + Connection: + - close + Content-Length: + - '51' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 06 Nov 2018 15:04:06 GMT + Etag: + - '"e671435b14693e94f294247ac6f96b5f"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + X-Accepted-Oauth-Scopes: + - pullrequest + X-Cache-Info: + - caching + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.292464971542' + X-Request-Count: + - '109' + X-Served-By: + - app-143 + X-Static-Version: + - 4ea3c9800ace + X-Version: + - 4ea3c9800ace + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests?state=OPEN&page=1&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541516646&oauth_nonce=15af96bfd6e546acaec32b3dac09fe42&oauth_version=1.0&oauth_signature=4E9qt33md6cz0B%2BnnL62C2hs7jA%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_ancestors_tree.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_ancestors_tree.yaml new file mode 100644 index 0000000000..9e8c6af26f --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_ancestors_tree.yaml @@ -0,0 +1,831 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/commits?include=6ae5f17&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1568980339&oauth_nonce=1dcb7a2eedd544b395184b18231388a8&oauth_version=1.0&oauth_signature=XdUUtgGHNy2XcsbYlW%2Ftq3Od4qo%3D + response: + content: "{\"pagelen\": 30, \"values\": [{\"rendered\": {\"message\": {\"raw\"\ + : \"Update README.rst\", \"markup\": \"markdown\", \"html\": \"

    Update README.rst

    \"\ + , \"type\": \"rendered\"}}, \"hash\": \"6ae5f1795a441884ed2847bb31154814ac01ef38\"\ + , \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/6ae5f1795a441884ed2847bb31154814ac01ef38\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/6ae5f1795a441884ed2847bb31154814ac01ef38\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38/statuses\"\ + }}, \"author\": {\"raw\": \"Thomas Pedbereznak \", \"type\"\ + : \"author\"}, \"summary\": {\"raw\": \"Update README.rst\", \"markup\": \"\ + markdown\", \"html\": \"

    Update README.rst

    \", \"type\": \"rendered\"},\ + \ \"parents\": [{\"hash\": \"8631ea09b9b689de0a348d5abf70bdd7273d2ae3\", \"\ + type\": \"commit\", \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"\ + }}}], \"date\": \"2018-04-26T08:35:58+00:00\", \"message\": \"Update README.rst\"\ + , \"type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\": \"Merge pull\ + \ request #31 from Gabswim/fix/typo\\n\\nfixing a typo in the README\", \"markup\"\ + : \"markdown\", \"html\": \"

    Merge pull request #31 from Gabswim/fix/typo

    \\\ + n

    fixing a typo in the README

    \", \"type\": \"rendered\"}}, \"hash\": \"\ + 8631ea09b9b689de0a348d5abf70bdd7273d2ae3\", \"repository\": {\"links\": {\"\ + self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3/comments\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Merge pull request #31 from Gabswim/fix/typo\\n\\\ + nfixing a typo in the README\", \"markup\": \"markdown\", \"html\": \"

    Merge\ + \ pull request #31 from Gabswim/fix/typo

    \\n

    fixing a typo in the README

    \"\ + , \"type\": \"rendered\"}, \"parents\": [{\"hash\": \"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + , \"type\": \"commit\", \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + }}}, {\"hash\": \"087ede6771099a66dccb968c8aacfa04e9ba27a8\", \"type\": \"commit\"\ + , \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/087ede6771099a66dccb968c8aacfa04e9ba27a8\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8\"\ + }}}], \"date\": \"2018-02-13T09:13:36+00:00\", \"message\": \"Merge pull request\ + \ #31 from Gabswim/fix/typo\\n\\nfixing a typo in the README\", \"type\": \"\ + commit\"}, {\"rendered\": {\"message\": {\"raw\": \"fixing a typo in the README\\\ + n\\n\", \"markup\": \"markdown\", \"html\": \"

    fixing a typo in the README

    \"\ + , \"type\": \"rendered\"}}, \"hash\": \"087ede6771099a66dccb968c8aacfa04e9ba27a8\"\ + , \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/087ede6771099a66dccb968c8aacfa04e9ba27a8\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/087ede6771099a66dccb968c8aacfa04e9ba27a8/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/087ede6771099a66dccb968c8aacfa04e9ba27a8\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/087ede6771099a66dccb968c8aacfa04e9ba27a8\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/087ede6771099a66dccb968c8aacfa04e9ba27a8/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/087ede6771099a66dccb968c8aacfa04e9ba27a8/statuses\"\ + }}, \"author\": {\"raw\": \"Gabriel Legault \", \"\ + type\": \"author\", \"user\": {\"display_name\": \"Gabriel Legault\", \"uuid\"\ + : \"{8cfe2ef8-b235-4c8b-9eed-26bbd95514e8}\", \"links\": {\"self\": {\"href\"\ + : \"https://bitbucket.org/!api/2.0/users/%7B8cfe2ef8-b235-4c8b-9eed-26bbd95514e8%7D\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/%7B8cfe2ef8-b235-4c8b-9eed-26bbd95514e8%7D/\"\ + }, \"avatar\": {\"href\": \"https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/557058:bb7fe615-4804-442d-b0f1-048767b867bf/edac59b9-d999-4dc3-b7c0-1f7471e753e4/128\"\ + }}, \"nickname\": \"Gabswim\", \"type\": \"user\", \"account_id\": \"557058:bb7fe615-4804-442d-b0f1-048767b867bf\"\ + }}, \"summary\": {\"raw\": \"fixing a typo in the README\\n\\n\", \"markup\"\ + : \"markdown\", \"html\": \"

    fixing a typo in the README

    \", \"type\":\ + \ \"rendered\"}, \"parents\": [{\"hash\": \"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + , \"type\": \"commit\", \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + }}}], \"date\": \"2018-02-13T01:06:57+00:00\", \"message\": \"fixing a typo\ + \ in the README\\n\\n\", \"type\": \"commit\"}, {\"rendered\": {\"message\"\ + : {\"raw\": \"Merge pull request #30 from Jay54520/main\\n\\n#29/Pytest doc\ + \ error\", \"markup\": \"markdown\", \"html\": \"

    Merge pull request #30 from\ + \ Jay54520/main

    \\n

    #29/Pytest doc error

    \", \"type\": \"rendered\"\ + }}, \"hash\": \"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\", \"repository\":\ + \ {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb/comments\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Merge pull request #30 from Jay54520/main\\n\\\ + n#29/Pytest doc error\", \"markup\": \"markdown\", \"html\": \"

    Merge pull\ + \ request #30 from Jay54520/main

    \\n

    #29/Pytest doc error

    \", \"type\"\ + : \"rendered\"}, \"parents\": [{\"hash\": \"76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + , \"type\": \"commit\", \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + }}}, {\"hash\": \"d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\", \"type\": \"commit\"\ + , \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"\ + }}}], \"date\": \"2018-01-30T11:57:49+00:00\", \"message\": \"Merge pull request\ + \ #30 from Jay54520/main\\n\\n#29/Pytest doc error\", \"type\": \"commit\"\ + }, {\"rendered\": {\"message\": {\"raw\": \"#29/Pytest doc error\\n\", \"markup\"\ + : \"markdown\", \"html\": \"

    #29/Pytest doc error

    \", \"type\": \"rendered\"\ + }}, \"hash\": \"d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\", \"repository\":\ + \ {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7/statuses\"\ + }}, \"author\": {\"raw\": \"\u63ED\u601D\u654F \", \"type\"\ + : \"author\"}, \"summary\": {\"raw\": \"#29/Pytest doc error\\n\", \"markup\"\ + : \"markdown\", \"html\": \"

    #29/Pytest doc error

    \", \"type\": \"rendered\"\ + }, \"parents\": [{\"hash\": \"76003ff147414ce80d2a14ab5f1b78d165e9a468\", \"\ + type\": \"commit\", \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + }}}], \"date\": \"2018-01-28T07:12:57+00:00\", \"message\": \"#29/Pytest doc\ + \ error\\n\", \"type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\":\ + \ \"Update README.rst\", \"markup\": \"markdown\", \"html\": \"

    Update README.rst

    \"\ + , \"type\": \"rendered\"}}, \"hash\": \"76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + , \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Update README.rst\", \"markup\": \"markdown\", \"\ + html\": \"

    Update README.rst

    \", \"type\": \"rendered\"}, \"parents\":\ + \ [{\"hash\": \"f9253b0bf56d0e808ab01fdcc70412ac010f5c34\", \"type\": \"commit\"\ + , \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + }}}], \"date\": \"2018-01-22T13:40:28+00:00\", \"message\": \"Update README.rst\"\ + , \"type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\": \"Merge pull\ + \ request #24 from gundalow/README_md_to_rst\\n\\nReadme md to rst\", \"markup\"\ + : \"markdown\", \"html\": \"

    Merge pull request #24 from gundalow/README_md_to_rst

    \\\ + n

    Readme md to rst

    \", \"type\": \"rendered\"}}, \"hash\": \"f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + , \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f9253b0bf56d0e808ab01fdcc70412ac010f5c34/comments\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f9253b0bf56d0e808ab01fdcc70412ac010f5c34/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f9253b0bf56d0e808ab01fdcc70412ac010f5c34/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Merge pull request #24 from gundalow/README_md_to_rst\\\ + n\\nReadme md to rst\", \"markup\": \"markdown\", \"html\": \"

    Merge pull\ + \ request #24 from gundalow/README_md_to_rst

    \\n

    Readme md to rst

    \"\ + , \"type\": \"rendered\"}, \"parents\": [{\"hash\": \"1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + , \"type\": \"commit\", \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + }}}, {\"hash\": \"2903ade6074f09319c1854850ffee2c254c3e17c\", \"type\": \"commit\"\ + , \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2903ade6074f09319c1854850ffee2c254c3e17c\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/2903ade6074f09319c1854850ffee2c254c3e17c\"\ + }}}], \"date\": \"2017-12-11T10:13:41+00:00\", \"message\": \"Merge pull request\ + \ #24 from gundalow/README_md_to_rst\\n\\nReadme md to rst\", \"type\": \"commit\"\ + }, {\"rendered\": {\"message\": {\"raw\": \"typo\\n\", \"markup\": \"markdown\"\ + , \"html\": \"

    typo

    \", \"type\": \"rendered\"}}, \"hash\": \"2903ade6074f09319c1854850ffee2c254c3e17c\"\ + , \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2903ade6074f09319c1854850ffee2c254c3e17c\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2903ade6074f09319c1854850ffee2c254c3e17c/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/2903ade6074f09319c1854850ffee2c254c3e17c\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/2903ade6074f09319c1854850ffee2c254c3e17c\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/2903ade6074f09319c1854850ffee2c254c3e17c\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2903ade6074f09319c1854850ffee2c254c3e17c/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2903ade6074f09319c1854850ffee2c254c3e17c/statuses\"\ + }}, \"author\": {\"raw\": \"John Barker \", \"type\":\ + \ \"author\", \"user\": {\"display_name\": \"John Barker\", \"uuid\": \"{395fd7e0-98bc-4e5e-95aa-0043c9d22301}\"\ + , \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/users/%7B395fd7e0-98bc-4e5e-95aa-0043c9d22301%7D\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/%7B395fd7e0-98bc-4e5e-95aa-0043c9d22301%7D/\"\ + }, \"avatar\": {\"href\": \"https://secure.gravatar.com/avatar/8b7582c16d73324155a1bb826ffa930c?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FJB-4.png\"\ + }}, \"nickname\": \"sloop\", \"type\": \"user\", \"account_id\": \"557058:8c5b4ffb-c7e2-4503-83a2-73648e3f5cdc\"\ + }}, \"summary\": {\"raw\": \"typo\\n\", \"markup\": \"markdown\", \"html\":\ + \ \"

    typo

    \", \"type\": \"rendered\"}, \"parents\": [{\"hash\": \"0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + , \"type\": \"commit\", \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + }}}], \"date\": \"2017-12-09T13:35:07+00:00\", \"message\": \"typo\\n\", \"\ + type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\": \"Valid badge\\\ + n\", \"markup\": \"markdown\", \"html\": \"

    Valid badge

    \", \"type\": \"\ + rendered\"}}, \"hash\": \"0073e9e074081bc2588b9ee311fc01bc9adfa967\", \"repository\"\ + : {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/0073e9e074081bc2588b9ee311fc01bc9adfa967/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/0073e9e074081bc2588b9ee311fc01bc9adfa967/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/0073e9e074081bc2588b9ee311fc01bc9adfa967/statuses\"\ + }}, \"author\": {\"raw\": \"John Barker \", \"type\":\ + \ \"author\", \"user\": {\"display_name\": \"John Barker\", \"uuid\": \"{395fd7e0-98bc-4e5e-95aa-0043c9d22301}\"\ + , \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/users/%7B395fd7e0-98bc-4e5e-95aa-0043c9d22301%7D\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/%7B395fd7e0-98bc-4e5e-95aa-0043c9d22301%7D/\"\ + }, \"avatar\": {\"href\": \"https://secure.gravatar.com/avatar/8b7582c16d73324155a1bb826ffa930c?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FJB-4.png\"\ + }}, \"nickname\": \"sloop\", \"type\": \"user\", \"account_id\": \"557058:8c5b4ffb-c7e2-4503-83a2-73648e3f5cdc\"\ + }}, \"summary\": {\"raw\": \"Valid badge\\n\", \"markup\": \"markdown\", \"\ + html\": \"

    Valid badge

    \", \"type\": \"rendered\"}, \"parents\": [{\"hash\"\ + : \"8d437d531af955c068c03f35a1f6f19667c6d215\", \"type\": \"commit\", \"links\"\ + : {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/8d437d531af955c068c03f35a1f6f19667c6d215\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/8d437d531af955c068c03f35a1f6f19667c6d215\"\ + }}}], \"date\": \"2017-12-09T13:33:54+00:00\", \"message\": \"Valid badge\\\ + n\", \"type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\": \"Convert\ + \ README.md to RST\\n\\n* Update formatting fixes #3\\n* Add example badge (and\ + \ how to use)\\n* Make it cleared that bash uploader should be used\\n\", \"\ + markup\": \"markdown\", \"html\": \"

    Convert README.md to RST

    \\n
      \\\ + n
    • Update formatting fixes #3
    • \\n
    • Add example badge (and how to use)
    • \\\ + n
    • Make it cleared that bash uploader should be used
    • \\n
    \", \"type\"\ + : \"rendered\"}}, \"hash\": \"8d437d531af955c068c03f35a1f6f19667c6d215\", \"\ + repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/8d437d531af955c068c03f35a1f6f19667c6d215\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/8d437d531af955c068c03f35a1f6f19667c6d215/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/8d437d531af955c068c03f35a1f6f19667c6d215\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/8d437d531af955c068c03f35a1f6f19667c6d215\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/8d437d531af955c068c03f35a1f6f19667c6d215\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/8d437d531af955c068c03f35a1f6f19667c6d215/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/8d437d531af955c068c03f35a1f6f19667c6d215/statuses\"\ + }}, \"author\": {\"raw\": \"John Barker \", \"type\":\ + \ \"author\", \"user\": {\"display_name\": \"John Barker\", \"uuid\": \"{395fd7e0-98bc-4e5e-95aa-0043c9d22301}\"\ + , \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/users/%7B395fd7e0-98bc-4e5e-95aa-0043c9d22301%7D\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/%7B395fd7e0-98bc-4e5e-95aa-0043c9d22301%7D/\"\ + }, \"avatar\": {\"href\": \"https://secure.gravatar.com/avatar/8b7582c16d73324155a1bb826ffa930c?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FJB-4.png\"\ + }}, \"nickname\": \"sloop\", \"type\": \"user\", \"account_id\": \"557058:8c5b4ffb-c7e2-4503-83a2-73648e3f5cdc\"\ + }}, \"summary\": {\"raw\": \"Convert README.md to RST\\n\\n* Update formatting\ + \ fixes #3\\n* Add example badge (and how to use)\\n* Make it cleared that bash\ + \ uploader should be used\\n\", \"markup\": \"markdown\", \"html\": \"

    Convert\ + \ README.md to RST

    \\n
      \\n
    • Update formatting fixes #3
    • \\n
    • Add\ + \ example badge (and how to use)
    • \\n
    • Make it cleared that bash uploader\ + \ should be used
    • \\n
    \", \"type\": \"rendered\"}, \"parents\": [{\"\ + hash\": \"1e906160c09128765a75afbcbd60d1cbd3c8d10a\", \"type\": \"commit\",\ + \ \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + }}}], \"date\": \"2017-12-09T13:28:02+00:00\", \"message\": \"Convert README.md\ + \ to RST\\n\\n* Update formatting fixes #3\\n* Add example badge (and how to\ + \ use)\\n* Make it cleared that bash uploader should be used\\n\", \"type\"\ + : \"commit\"}, {\"rendered\": {\"message\": {\"raw\": \"Merge pull request #22\ + \ from IanLee1521/patch-1\\n\\nFixed minor typo\", \"markup\": \"markdown\"\ + , \"html\": \"

    Merge pull request #22 from IanLee1521/patch-1

    \\n

    Fixed\ + \ minor typo

    \", \"type\": \"rendered\"}}, \"hash\": \"1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + , \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a/comments\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Merge pull request #22 from IanLee1521/patch-1\\\ + n\\nFixed minor typo\", \"markup\": \"markdown\", \"html\": \"

    Merge pull\ + \ request #22 from IanLee1521/patch-1

    \\n

    Fixed minor typo

    \", \"type\"\ + : \"rendered\"}, \"parents\": [{\"hash\": \"3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + , \"type\": \"commit\", \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + }}}, {\"hash\": \"b066c93c2676bc957d971d2c4188e77b3e383b77\", \"type\": \"commit\"\ + , \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b066c93c2676bc957d971d2c4188e77b3e383b77\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/b066c93c2676bc957d971d2c4188e77b3e383b77\"\ + }}}], \"date\": \"2017-10-04T09:43:08+00:00\", \"message\": \"Merge pull request\ + \ #22 from IanLee1521/patch-1\\n\\nFixed minor typo\", \"type\": \"commit\"\ + }, {\"rendered\": {\"message\": {\"raw\": \"Fixed minor typo\", \"markup\":\ + \ \"markdown\", \"html\": \"

    Fixed minor typo

    \", \"type\": \"rendered\"\ + }}, \"hash\": \"b066c93c2676bc957d971d2c4188e77b3e383b77\", \"repository\":\ + \ {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b066c93c2676bc957d971d2c4188e77b3e383b77\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b066c93c2676bc957d971d2c4188e77b3e383b77/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/b066c93c2676bc957d971d2c4188e77b3e383b77\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/b066c93c2676bc957d971d2c4188e77b3e383b77\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/b066c93c2676bc957d971d2c4188e77b3e383b77\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b066c93c2676bc957d971d2c4188e77b3e383b77/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b066c93c2676bc957d971d2c4188e77b3e383b77/statuses\"\ + }}, \"author\": {\"raw\": \"Ian Lee \", \"type\": \"author\"\ + , \"user\": {\"display_name\": \"Ian Lee\", \"uuid\": \"{f5f64a51-ab76-49a6-bf39-8cc62cfd9277}\"\ + , \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/users/%7Bf5f64a51-ab76-49a6-bf39-8cc62cfd9277%7D\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/%7Bf5f64a51-ab76-49a6-bf39-8cc62cfd9277%7D/\"\ + }, \"avatar\": {\"href\": \"https://secure.gravatar.com/avatar/c416a04a16b233e65afd993815c167dd?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FIL-5.png\"\ + }}, \"nickname\": \"IanLee1521\", \"type\": \"user\", \"account_id\": \"557058:9efc8d4e-be21-43b6-b1b7-ecdf9e2e7601\"\ + }}, \"summary\": {\"raw\": \"Fixed minor typo\", \"markup\": \"markdown\", \"\ + html\": \"

    Fixed minor typo

    \", \"type\": \"rendered\"}, \"parents\": [{\"\ + hash\": \"3230594a7aa8782fbcf51329b2395118f7cf0d15\", \"type\": \"commit\",\ + \ \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + }}}], \"date\": \"2017-09-29T19:29:42+00:00\", \"message\": \"Fixed minor typo\"\ + , \"type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\": \"Update README.md\"\ + , \"markup\": \"markdown\", \"html\": \"

    Update README.md

    \", \"type\"\ + : \"rendered\"}}, \"hash\": \"3230594a7aa8782fbcf51329b2395118f7cf0d15\", \"\ + repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Update README.md\", \"markup\": \"markdown\", \"\ + html\": \"

    Update README.md

    \", \"type\": \"rendered\"}, \"parents\": [{\"\ + hash\": \"6560cbff33fbff8740f0407f4cac1091bc25e6ae\", \"type\": \"commit\",\ + \ \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + }}}], \"date\": \"2017-08-30T14:02:20+00:00\", \"message\": \"Update README.md\"\ + , \"type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\": \"Update README.md\"\ + , \"markup\": \"markdown\", \"html\": \"

    Update README.md

    \", \"type\"\ + : \"rendered\"}}, \"hash\": \"6560cbff33fbff8740f0407f4cac1091bc25e6ae\", \"\ + repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6560cbff33fbff8740f0407f4cac1091bc25e6ae/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6560cbff33fbff8740f0407f4cac1091bc25e6ae/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6560cbff33fbff8740f0407f4cac1091bc25e6ae/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Update README.md\", \"markup\": \"markdown\", \"\ + html\": \"

    Update README.md

    \", \"type\": \"rendered\"}, \"parents\": [{\"\ + hash\": \"feb5100831541db79eb83a263986df129573f3de\", \"type\": \"commit\",\ + \ \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/feb5100831541db79eb83a263986df129573f3de\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/feb5100831541db79eb83a263986df129573f3de\"\ + }}}], \"date\": \"2017-02-02T19:10:16+00:00\", \"message\": \"Update README.md\"\ + , \"type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\": \"Merge pull\ + \ request #20 from briandant/main\\n\\nAdd further instructions for using\ + \ env vars\", \"markup\": \"markdown\", \"html\": \"

    Merge pull request #20\ + \ from briandant/main

    \\n

    Add further instructions for using env vars

    \"\ + , \"type\": \"rendered\"}}, \"hash\": \"feb5100831541db79eb83a263986df129573f3de\"\ + , \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/feb5100831541db79eb83a263986df129573f3de\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/feb5100831541db79eb83a263986df129573f3de/comments\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/feb5100831541db79eb83a263986df129573f3de\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/feb5100831541db79eb83a263986df129573f3de\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/feb5100831541db79eb83a263986df129573f3de/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/feb5100831541db79eb83a263986df129573f3de/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Merge pull request #20 from briandant/main\\n\\\ + nAdd further instructions for using env vars\", \"markup\": \"markdown\", \"\ + html\": \"

    Merge pull request #20 from briandant/main

    \\n

    Add further\ + \ instructions for using env vars

    \", \"type\": \"rendered\"}, \"parents\"\ + : [{\"hash\": \"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\", \"type\": \"commit\"\ + , \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + }}}, {\"hash\": \"6802411a35f438bf62ee8a1c1928cd36ca50d534\", \"type\": \"commit\"\ + , \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6802411a35f438bf62ee8a1c1928cd36ca50d534\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534\"\ + }}}], \"date\": \"2017-02-02T19:09:38+00:00\", \"message\": \"Merge pull request\ + \ #20 from briandant/main\\n\\nAdd further instructions for using env vars\"\ + , \"type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\": \"Add further\ + \ instructions for using env vars\", \"markup\": \"markdown\", \"html\": \"\ +

    Add further instructions for using env vars

    \", \"type\": \"rendered\"\ + }}, \"hash\": \"6802411a35f438bf62ee8a1c1928cd36ca50d534\", \"repository\":\ + \ {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6802411a35f438bf62ee8a1c1928cd36ca50d534\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6802411a35f438bf62ee8a1c1928cd36ca50d534/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/6802411a35f438bf62ee8a1c1928cd36ca50d534\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/6802411a35f438bf62ee8a1c1928cd36ca50d534\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6802411a35f438bf62ee8a1c1928cd36ca50d534/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/6802411a35f438bf62ee8a1c1928cd36ca50d534/statuses\"\ + }}, \"author\": {\"raw\": \"Brian Dant \"\ + , \"type\": \"author\"}, \"summary\": {\"raw\": \"Add further instructions for\ + \ using env vars\", \"markup\": \"markdown\", \"html\": \"

    Add further instructions\ + \ for using env vars

    \", \"type\": \"rendered\"}, \"parents\": [{\"hash\"\ + : \"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\", \"type\": \"commit\", \"links\"\ + : {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + }}}], \"date\": \"2017-02-02T18:37:42+00:00\", \"message\": \"Add further instructions\ + \ for using env vars\", \"type\": \"commit\"}, {\"rendered\": {\"message\":\ + \ {\"raw\": \"Circle build #355\\nhttps://circleci.com/gh/codecov/testsuite/355\\\ + nbash <(curl -s https://raw.githubusercontent.com/codecov/codecov-bash/main/codecov)\ + \ -v -u https://codecov.io\", \"markup\": \"markdown\", \"html\": \"

    Circle\ + \ build #355
    \\nhttps://circleci.com/gh/codecov/testsuite/355\\nbash <(curl -s https://raw.githubusercontent.com/codecov/codecov-bash/main/codecov)\ + \ -v -u https://codecov.io

    \", \"type\": \"rendered\"}}, \"hash\": \"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + , \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d/statuses\"\ + }}, \"author\": {\"raw\": \"Codecov Test Bot \", \"type\"\ + : \"author\", \"user\": {\"display_name\": \"Codecov Test\", \"uuid\": \"{26f58a7d-734e-434c-b59c-03a224735c63}\"\ + , \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/users/%7B26f58a7d-734e-434c-b59c-03a224735c63%7D\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/%7B26f58a7d-734e-434c-b59c-03a224735c63%7D/\"\ + }, \"avatar\": {\"href\": \"https://secure.gravatar.com/avatar/dcdb35375db567705dd7e74226fae67b?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FCT-1.png\"\ + }}, \"nickname\": \"Codecov Test\", \"type\": \"user\", \"account_id\": \"557058:e83c38f0-a908-4e6e-aeb1-e5cf9aef75a3\"\ + }}, \"summary\": {\"raw\": \"Circle build #355\\nhttps://circleci.com/gh/codecov/testsuite/355\\\ + nbash <(curl -s https://raw.githubusercontent.com/codecov/codecov-bash/main/codecov)\ + \ -v -u https://codecov.io\", \"markup\": \"markdown\", \"html\": \"

    Circle\ + \ build #355
    \\nhttps://circleci.com/gh/codecov/testsuite/355\\nbash <(curl -s https://raw.githubusercontent.com/codecov/codecov-bash/main/codecov)\ + \ -v -u https://codecov.io

    \", \"type\": \"rendered\"}, \"parents\": [{\"\ + hash\": \"63f5740c33f2aac68fd0757f471ab741f9e20a05\", \"type\": \"commit\",\ + \ \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + }}}], \"date\": \"2016-09-29T10:37:07+00:00\", \"message\": \"Circle build #355\\\ + nhttps://circleci.com/gh/codecov/testsuite/355\\nbash <(curl -s https://raw.githubusercontent.com/codecov/codecov-bash/main/codecov)\ + \ -v -u https://codecov.io\", \"type\": \"commit\"}, {\"rendered\": {\"message\"\ + : {\"raw\": \"Update README.md\", \"markup\": \"markdown\", \"html\": \"

    Update\ + \ README.md

    \", \"type\": \"rendered\"}}, \"hash\": \"63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + , \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/63f5740c33f2aac68fd0757f471ab741f9e20a05/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/63f5740c33f2aac68fd0757f471ab741f9e20a05/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/63f5740c33f2aac68fd0757f471ab741f9e20a05/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Update README.md\", \"markup\": \"markdown\", \"\ + html\": \"

    Update README.md

    \", \"type\": \"rendered\"}, \"parents\": [{\"\ + hash\": \"d61bb41b849de7125ca17fbd37292479648e7fa7\", \"type\": \"commit\",\ + \ \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + }}}], \"date\": \"2016-08-26T19:21:23+00:00\", \"message\": \"Update README.md\"\ + , \"type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\": \"Merge pull\ + \ request #18 from yurovant/patch-1\\n\\nUpdate README.md\", \"markup\": \"\ + markdown\", \"html\": \"

    Merge pull request #18 from yurovant/patch-1

    \\\ + n

    Update README.md

    \", \"type\": \"rendered\"}}, \"hash\": \"d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + , \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/d61bb41b849de7125ca17fbd37292479648e7fa7/comments\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/d61bb41b849de7125ca17fbd37292479648e7fa7/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/d61bb41b849de7125ca17fbd37292479648e7fa7/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Merge pull request #18 from yurovant/patch-1\\n\\\ + nUpdate README.md\", \"markup\": \"markdown\", \"html\": \"

    Merge pull request\ + \ #18 from yurovant/patch-1

    \\n

    Update README.md

    \", \"type\": \"rendered\"\ + }, \"parents\": [{\"hash\": \"84ea8b9ba0f8134be0477971e72b233959f5d3b6\", \"\ + type\": \"commit\", \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + }}}, {\"hash\": \"2d1b772e138f05dbbd577ce0dcf3633577629a76\", \"type\": \"commit\"\ + , \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2d1b772e138f05dbbd577ce0dcf3633577629a76\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76\"\ + }}}], \"date\": \"2016-08-16T15:47:11+00:00\", \"message\": \"Merge pull request\ + \ #18 from yurovant/patch-1\\n\\nUpdate README.md\", \"type\": \"commit\"},\ + \ {\"rendered\": {\"message\": {\"raw\": \"Update README.md\\n\\ntypo: priveta\ + \ --> private\", \"markup\": \"markdown\", \"html\": \"

    Update README.md

    \\\ + n

    typo: priveta --> private

    \", \"type\": \"rendered\"}}, \"hash\":\ + \ \"2d1b772e138f05dbbd577ce0dcf3633577629a76\", \"repository\": {\"links\":\ + \ {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2d1b772e138f05dbbd577ce0dcf3633577629a76\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2d1b772e138f05dbbd577ce0dcf3633577629a76/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/2d1b772e138f05dbbd577ce0dcf3633577629a76\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/2d1b772e138f05dbbd577ce0dcf3633577629a76\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2d1b772e138f05dbbd577ce0dcf3633577629a76/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2d1b772e138f05dbbd577ce0dcf3633577629a76/statuses\"\ + }}, \"author\": {\"raw\": \"Anton Yurovskykh \"\ + , \"type\": \"author\", \"user\": {\"display_name\": \"Anton Yurovskykh\", \"\ + uuid\": \"{e9e6c8a1-10fb-4a07-a518-487c13c0ed00}\", \"links\": {\"self\": {\"\ + href\": \"https://bitbucket.org/!api/2.0/users/%7Be9e6c8a1-10fb-4a07-a518-487c13c0ed00%7D\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/%7Be9e6c8a1-10fb-4a07-a518-487c13c0ed00%7D/\"\ + }, \"avatar\": {\"href\": \"https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/557058:79a45bae-ac2b-4c79-acd3-14eb21392cbf/ea6c406c-96c5-4eef-b058-e60e015c7407/128\"\ + }}, \"nickname\": \"yurovant\", \"type\": \"user\", \"account_id\": \"557058:79a45bae-ac2b-4c79-acd3-14eb21392cbf\"\ + }}, \"summary\": {\"raw\": \"Update README.md\\n\\ntypo: priveta --> private\"\ + , \"markup\": \"markdown\", \"html\": \"

    Update README.md

    \\n

    typo: priveta\ + \ --> private

    \", \"type\": \"rendered\"}, \"parents\": [{\"hash\": \"\ + 84ea8b9ba0f8134be0477971e72b233959f5d3b6\", \"type\": \"commit\", \"links\"\ + : {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + }}}], \"date\": \"2016-08-16T07:35:54+00:00\", \"message\": \"Update README.md\\\ + n\\ntypo: priveta --> private\", \"type\": \"commit\"}, {\"rendered\": {\"message\"\ + : {\"raw\": \"Update README.md\", \"markup\": \"markdown\", \"html\": \"

    Update\ + \ README.md

    \", \"type\": \"rendered\"}}, \"hash\": \"84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + , \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Update README.md\", \"markup\": \"markdown\", \"\ + html\": \"

    Update README.md

    \", \"type\": \"rendered\"}, \"parents\": [{\"\ + hash\": \"e051e55647e0ecc27539b2dc40fb9b2839383060\", \"type\": \"commit\",\ + \ \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/e051e55647e0ecc27539b2dc40fb9b2839383060\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/e051e55647e0ecc27539b2dc40fb9b2839383060\"\ + }}}], \"date\": \"2016-08-05T16:46:35+00:00\", \"message\": \"Update README.md\"\ + , \"type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\": \"Update README.md\"\ + , \"markup\": \"markdown\", \"html\": \"

    Update README.md

    \", \"type\"\ + : \"rendered\"}}, \"hash\": \"e051e55647e0ecc27539b2dc40fb9b2839383060\", \"\ + repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/e051e55647e0ecc27539b2dc40fb9b2839383060\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/e051e55647e0ecc27539b2dc40fb9b2839383060/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/e051e55647e0ecc27539b2dc40fb9b2839383060\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/e051e55647e0ecc27539b2dc40fb9b2839383060\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/e051e55647e0ecc27539b2dc40fb9b2839383060\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/e051e55647e0ecc27539b2dc40fb9b2839383060/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/e051e55647e0ecc27539b2dc40fb9b2839383060/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Update README.md\", \"markup\": \"markdown\", \"\ + html\": \"

    Update README.md

    \", \"type\": \"rendered\"}, \"parents\": [{\"\ + hash\": \"248906bd30b16b8bc131d0600ab66545f51a7085\", \"type\": \"commit\",\ + \ \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/248906bd30b16b8bc131d0600ab66545f51a7085\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/248906bd30b16b8bc131d0600ab66545f51a7085\"\ + }}}], \"date\": \"2016-08-05T16:44:34+00:00\", \"message\": \"Update README.md\"\ + , \"type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\": \"Update README.md\"\ + , \"markup\": \"markdown\", \"html\": \"

    Update README.md

    \", \"type\"\ + : \"rendered\"}}, \"hash\": \"248906bd30b16b8bc131d0600ab66545f51a7085\", \"\ + repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/248906bd30b16b8bc131d0600ab66545f51a7085\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/248906bd30b16b8bc131d0600ab66545f51a7085/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/248906bd30b16b8bc131d0600ab66545f51a7085\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/248906bd30b16b8bc131d0600ab66545f51a7085\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/248906bd30b16b8bc131d0600ab66545f51a7085\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/248906bd30b16b8bc131d0600ab66545f51a7085/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/248906bd30b16b8bc131d0600ab66545f51a7085/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Update README.md\", \"markup\": \"markdown\", \"\ + html\": \"

    Update README.md

    \", \"type\": \"rendered\"}, \"parents\": [{\"\ + hash\": \"8b8bd76c787dce785d0d01fd3ea9b0d8741763c4\", \"type\": \"commit\",\ + \ \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/8b8bd76c787dce785d0d01fd3ea9b0d8741763c4\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/8b8bd76c787dce785d0d01fd3ea9b0d8741763c4\"\ + }}}], \"date\": \"2016-08-05T16:44:04+00:00\", \"message\": \"Update README.md\"\ + , \"type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\": \"Update README.md\"\ + , \"markup\": \"markdown\", \"html\": \"

    Update README.md

    \", \"type\"\ + : \"rendered\"}}, \"hash\": \"8b8bd76c787dce785d0d01fd3ea9b0d8741763c4\", \"\ + repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/8b8bd76c787dce785d0d01fd3ea9b0d8741763c4\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/8b8bd76c787dce785d0d01fd3ea9b0d8741763c4/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/8b8bd76c787dce785d0d01fd3ea9b0d8741763c4\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/8b8bd76c787dce785d0d01fd3ea9b0d8741763c4\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/8b8bd76c787dce785d0d01fd3ea9b0d8741763c4\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/8b8bd76c787dce785d0d01fd3ea9b0d8741763c4/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/8b8bd76c787dce785d0d01fd3ea9b0d8741763c4/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Update README.md\", \"markup\": \"markdown\", \"\ + html\": \"

    Update README.md

    \", \"type\": \"rendered\"}, \"parents\": [{\"\ + hash\": \"ca1acdab2996eef3f7e5ddc27243efb0752606e6\", \"type\": \"commit\",\ + \ \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/ca1acdab2996eef3f7e5ddc27243efb0752606e6\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/ca1acdab2996eef3f7e5ddc27243efb0752606e6\"\ + }}}], \"date\": \"2016-08-05T16:43:41+00:00\", \"message\": \"Update README.md\"\ + , \"type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\": \"Update README.md\"\ + , \"markup\": \"markdown\", \"html\": \"

    Update README.md

    \", \"type\"\ + : \"rendered\"}}, \"hash\": \"ca1acdab2996eef3f7e5ddc27243efb0752606e6\", \"\ + repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/ca1acdab2996eef3f7e5ddc27243efb0752606e6\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/ca1acdab2996eef3f7e5ddc27243efb0752606e6/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/ca1acdab2996eef3f7e5ddc27243efb0752606e6\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/ca1acdab2996eef3f7e5ddc27243efb0752606e6\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/ca1acdab2996eef3f7e5ddc27243efb0752606e6\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/ca1acdab2996eef3f7e5ddc27243efb0752606e6/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/ca1acdab2996eef3f7e5ddc27243efb0752606e6/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Update README.md\", \"markup\": \"markdown\", \"\ + html\": \"

    Update README.md

    \", \"type\": \"rendered\"}, \"parents\": [{\"\ + hash\": \"ef8c461aa9f2d7b7a93270328a33da5c7cb366b1\", \"type\": \"commit\",\ + \ \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/ef8c461aa9f2d7b7a93270328a33da5c7cb366b1\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/ef8c461aa9f2d7b7a93270328a33da5c7cb366b1\"\ + }}}], \"date\": \"2016-08-05T16:42:16+00:00\", \"message\": \"Update README.md\"\ + , \"type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\": \"Update README.md\"\ + , \"markup\": \"markdown\", \"html\": \"

    Update README.md

    \", \"type\"\ + : \"rendered\"}}, \"hash\": \"ef8c461aa9f2d7b7a93270328a33da5c7cb366b1\", \"\ + repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/ef8c461aa9f2d7b7a93270328a33da5c7cb366b1\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/ef8c461aa9f2d7b7a93270328a33da5c7cb366b1/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/ef8c461aa9f2d7b7a93270328a33da5c7cb366b1\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/ef8c461aa9f2d7b7a93270328a33da5c7cb366b1\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/ef8c461aa9f2d7b7a93270328a33da5c7cb366b1\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/ef8c461aa9f2d7b7a93270328a33da5c7cb366b1/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/ef8c461aa9f2d7b7a93270328a33da5c7cb366b1/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Update README.md\", \"markup\": \"markdown\", \"\ + html\": \"

    Update README.md

    \", \"type\": \"rendered\"}, \"parents\": [{\"\ + hash\": \"e02bded5e1c9bc48261788c84c34fcbcb89b4568\", \"type\": \"commit\",\ + \ \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/e02bded5e1c9bc48261788c84c34fcbcb89b4568\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/e02bded5e1c9bc48261788c84c34fcbcb89b4568\"\ + }}}], \"date\": \"2016-06-21T15:23:48+00:00\", \"message\": \"Update README.md\"\ + , \"type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\": \"Add more docs\ + \ close #12\", \"markup\": \"markdown\", \"html\": \"

    Add more docs close\ + \ #12

    \", \"type\": \"rendered\"}}, \"hash\": \"e02bded5e1c9bc48261788c84c34fcbcb89b4568\"\ + , \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/e02bded5e1c9bc48261788c84c34fcbcb89b4568\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/e02bded5e1c9bc48261788c84c34fcbcb89b4568/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/e02bded5e1c9bc48261788c84c34fcbcb89b4568\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/e02bded5e1c9bc48261788c84c34fcbcb89b4568\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/e02bded5e1c9bc48261788c84c34fcbcb89b4568\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/e02bded5e1c9bc48261788c84c34fcbcb89b4568/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/e02bded5e1c9bc48261788c84c34fcbcb89b4568/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Add more docs close #12\", \"markup\": \"markdown\"\ + , \"html\": \"

    Add more docs close #12

    \", \"type\": \"rendered\"}, \"\ + parents\": [{\"hash\": \"810e2a539ab476977ec6e6d4b80010431be3cd99\", \"type\"\ + : \"commit\", \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/810e2a539ab476977ec6e6d4b80010431be3cd99\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/810e2a539ab476977ec6e6d4b80010431be3cd99\"\ + }}}], \"date\": \"2016-01-08T00:45:34+00:00\", \"message\": \"Add more docs\ + \ close #12\", \"type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\"\ + : \"Merge pull request #11 from ticosax/faster-travis-build\\n\\nFaster travis\ + \ builds\", \"markup\": \"markdown\", \"html\": \"

    Merge pull request #11\ + \ from ticosax/faster-travis-build

    \\n

    Faster travis builds

    \", \"type\"\ + : \"rendered\"}}, \"hash\": \"810e2a539ab476977ec6e6d4b80010431be3cd99\", \"\ + repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/810e2a539ab476977ec6e6d4b80010431be3cd99\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/810e2a539ab476977ec6e6d4b80010431be3cd99/comments\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/810e2a539ab476977ec6e6d4b80010431be3cd99\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/810e2a539ab476977ec6e6d4b80010431be3cd99\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/810e2a539ab476977ec6e6d4b80010431be3cd99/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/810e2a539ab476977ec6e6d4b80010431be3cd99/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Merge pull request #11 from ticosax/faster-travis-build\\\ + n\\nFaster travis builds\", \"markup\": \"markdown\", \"html\": \"

    Merge pull\ + \ request #11 from ticosax/faster-travis-build

    \\n

    Faster travis builds

    \"\ + , \"type\": \"rendered\"}, \"parents\": [{\"hash\": \"202ddb27233d5007be31e3a4268aeac9a54febce\"\ + , \"type\": \"commit\", \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/202ddb27233d5007be31e3a4268aeac9a54febce\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/202ddb27233d5007be31e3a4268aeac9a54febce\"\ + }}}, {\"hash\": \"2be550c135cc13425cb2c239b9321e78dcfb787b\", \"type\": \"commit\"\ + , \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2be550c135cc13425cb2c239b9321e78dcfb787b\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/2be550c135cc13425cb2c239b9321e78dcfb787b\"\ + }}}], \"date\": \"2015-11-19T21:12:03+00:00\", \"message\": \"Merge pull request\ + \ #11 from ticosax/faster-travis-build\\n\\nFaster travis builds\", \"type\"\ + : \"commit\"}, {\"rendered\": {\"message\": {\"raw\": \"Faster travis builds\\\ + n\", \"markup\": \"markdown\", \"html\": \"

    Faster travis builds

    \", \"\ + type\": \"rendered\"}}, \"hash\": \"2be550c135cc13425cb2c239b9321e78dcfb787b\"\ + , \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2be550c135cc13425cb2c239b9321e78dcfb787b\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2be550c135cc13425cb2c239b9321e78dcfb787b/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/2be550c135cc13425cb2c239b9321e78dcfb787b\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/2be550c135cc13425cb2c239b9321e78dcfb787b\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/2be550c135cc13425cb2c239b9321e78dcfb787b\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2be550c135cc13425cb2c239b9321e78dcfb787b/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2be550c135cc13425cb2c239b9321e78dcfb787b/statuses\"\ + }}, \"author\": {\"raw\": \"Nicolas Delaby \", \"type\"\ + : \"author\"}, \"summary\": {\"raw\": \"Faster travis builds\\n\", \"markup\"\ + : \"markdown\", \"html\": \"

    Faster travis builds

    \", \"type\": \"rendered\"\ + }, \"parents\": [{\"hash\": \"202ddb27233d5007be31e3a4268aeac9a54febce\", \"\ + type\": \"commit\", \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/202ddb27233d5007be31e3a4268aeac9a54febce\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/202ddb27233d5007be31e3a4268aeac9a54febce\"\ + }}}], \"date\": \"2015-11-19T21:09:03+00:00\", \"message\": \"Faster travis\ + \ builds\\n\", \"type\": \"commit\"}, {\"rendered\": {\"message\": {\"raw\"\ + : \"Update README.md\", \"markup\": \"markdown\", \"html\": \"

    Update README.md

    \"\ + , \"type\": \"rendered\"}}, \"hash\": \"202ddb27233d5007be31e3a4268aeac9a54febce\"\ + , \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/202ddb27233d5007be31e3a4268aeac9a54febce\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/202ddb27233d5007be31e3a4268aeac9a54febce/comments\"\ + }, \"patch\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/202ddb27233d5007be31e3a4268aeac9a54febce\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/202ddb27233d5007be31e3a4268aeac9a54febce\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/202ddb27233d5007be31e3a4268aeac9a54febce\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/202ddb27233d5007be31e3a4268aeac9a54febce/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/202ddb27233d5007be31e3a4268aeac9a54febce/statuses\"\ + }}, \"author\": {\"raw\": \"Steve Peak \", \"type\": \"author\"\ + }, \"summary\": {\"raw\": \"Update README.md\", \"markup\": \"markdown\", \"\ + html\": \"

    Update README.md

    \", \"type\": \"rendered\"}, \"parents\": [{\"\ + hash\": \"b1598e3d561f5f21b10150e98ad5c0bab286a6f2\", \"type\": \"commit\",\ + \ \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b1598e3d561f5f21b10150e98ad5c0bab286a6f2\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/b1598e3d561f5f21b10150e98ad5c0bab286a6f2\"\ + }}}], \"date\": \"2015-10-21T07:04:33+00:00\", \"message\": \"Update README.md\"\ + , \"type\": \"commit\"}], \"next\": \"https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/commits?ctx=79edd3eec30e322dee687ae3870c45bb&page=2\"\ + }" + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 20 Sep 2019 11:52:22 GMT + Etag: + - '"gz[41a29bdd83f952edc8bb72352d43aadc]"' + Last-Modified: + - Sat, 24 Aug 2019 07:24:01 GMT + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Reads-Before-Write-From: + - default + X-Render-Time: + - '1.80140686035' + X-Request-Count: + - '3019' + X-Served-By: + - app-139 + X-Static-Version: + - 9ed2231efb65 + X-Version: + - 9ed2231efb65 + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/commits?include=6ae5f17&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1568980339&oauth_nonce=1dcb7a2eedd544b395184b18231388a8&oauth_version=1.0&oauth_signature=XdUUtgGHNy2XcsbYlW%2Ftq3Od4qo%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_authenticated.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_authenticated.yaml new file mode 100644 index 0000000000..af4d54d7d5 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_authenticated.yaml @@ -0,0 +1,82 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - bitbucket.org + user-agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/user/permissions/repositories?q=repository.full_name%3D%22ThiagoCodecov%2Fexample-python%22+AND+%28permission%3D%22admin%22+OR+permission%3D%22write%22%29&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1617377623&oauth_nonce=ac91652521bc4867af3ec36b448a016c&oauth_version=1.0&oauth_signature=BQrCWoBGobnzvY0aRJ51OBJqyPk%3D + response: + content: '{"pagelen": 10, "values": [{"type": "repository_permission", "user": + {"display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "permission": "admin"}], + "page": 1}' + headers: + Cache-Control: + - private + Connection: + - Keep-Alive + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=utf-8 + DC-Location: + - ash2 + Date: + - Fri, 02 Apr 2021 15:33:44 GMT + ETag: + - '"gz[c9c7af06f5a4a68dbe39014c421666a3]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + Vary: + - Authorization, cookie, user-context, Accept-Encoding + X-Accepted-OAuth-Scopes: + - account, repository + X-B3-TraceId: + - 9607fb3be8240ce6 + X-Cache-Info: + - 'not cacheable; response specified "Cache-Control: private"' + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Dc-Location: + - ash2 + X-Frame-Options: + - SAMEORIGIN + X-OAuth-Scopes: + - webhook, snippet, issue, pullrequest:write, repository:delete, repository:admin, + project, team, account + X-Render-Time: + - '0.0605049133301' + X-Request-Count: + - '23' + X-Served-By: + - app-3022 + X-Static-Version: + - 937e1df6c57f + X-Version: + - 937e1df6c57f + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_authenticated_no_edit_permission.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_authenticated_no_edit_permission.yaml new file mode 100644 index 0000000000..dd54f98f3d --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_authenticated_no_edit_permission.yaml @@ -0,0 +1,69 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - bitbucket.org + user-agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/user/permissions/repositories?q=repository.full_name%3D%22atlassian%2Fstash-example-plugin%22+AND+%28permission%3D%22admin%22+OR+permission%3D%22write%22%29&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1617377940&oauth_nonce=54b366ce70c843e38c6f5e2d08b7d25b&oauth_version=1.0&oauth_signature=lihQwQ9cT1CJGybvYG1NAYvWuYw%3D + response: + content: '{"pagelen": 10, "values": [], "page": 1}' + headers: + Cache-Control: + - private + Connection: + - Keep-Alive + Content-Length: + - '40' + Content-Type: + - application/json; charset=utf-8 + DC-Location: + - ash2 + Date: + - Fri, 02 Apr 2021 15:39:00 GMT + ETag: + - '"c03c6bf133039c14a77d00c2b9d7e1c3"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, cookie, user-context + X-Accepted-OAuth-Scopes: + - account, repository + X-B3-TraceId: + - c38a09d6a8b0bc85 + X-Cache-Info: + - 'not cacheable; response specified "Cache-Control: private"' + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Dc-Location: + - ash2 + X-Frame-Options: + - SAMEORIGIN + X-OAuth-Scopes: + - webhook, snippet, issue, pullrequest:write, repository:delete, repository:admin, + project, team, account + X-Render-Time: + - '0.0483520030975' + X-Request-Count: + - '4458' + X-Served-By: + - app-3016 + X-Static-Version: + - 937e1df6c57f + X-Version: + - 937e1df6c57f + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_branches.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_branches.yaml new file mode 100644 index 0000000000..27f3096555 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_branches.yaml @@ -0,0 +1,194 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/refs/branches?oauth_nonce=68196bfc35d5473cba07ec92748644c1&oauth_timestamp=1561674319&oauth_consumer_key=arubajamaicaohiwan&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&oauth_token=testss3hxhcfqf1h6g&oauth_signature=7%2BIvkm9uQV%2FozMt2tOdrE4Hf5dY%3D&pagelen=100 + response: + content: '{"pagelen": 100, "values": [{"name": "f/new-branch", "links": {"commits": + {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commits/f/new-branch"}, + "self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/refs/branches/f/new-branch"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/branch/f/new-branch"}}, + "default_merge_strategy": "merge_commit", "merge_strategies": ["merge_commit", + "squash", "fast_forward"], "type": "branch", "target": {"hash": "806c7e9f67dd1a84218f74d5c45d046d99ecf440", + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/806c7e9f67dd1a84218f74d5c45d046d99ecf440"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/806c7e9f67dd1a84218f74d5c45d046d99ecf440/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/806c7e9f67dd1a84218f74d5c45d046d99ecf440"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/806c7e9f67dd1a84218f74d5c45d046d99ecf440"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/806c7e9f67dd1a84218f74d5c45d046d99ecf440"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/806c7e9f67dd1a84218f74d5c45d046d99ecf440/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/806c7e9f67dd1a84218f74d5c45d046d99ecf440/statuses"}}, + "author": {"raw": "Thiago Ramos ", "type": "author", "user": + {"display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/5bce04c759d0e84f8c7555e9/83eddf7b-6307-4b37-a193-a855cc683bc5/128"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}}, + "parents": [{"hash": "44542c3afa4c75963f60e9424e87fc40ee503864", "type": "commit", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/44542c3afa4c75963f60e9424e87fc40ee503864"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/44542c3afa4c75963f60e9424e87fc40ee503864"}}}], + "date": "2019-03-13T14:09:07+00:00", "message": "New branch\n", "type": "commit"}}, + {"name": "main", "links": {"commits": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commits/main"}, + "self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/refs/branches/main"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/branch/main"}}, + "default_merge_strategy": "merge_commit", "merge_strategies": ["merge_commit", + "squash", "fast_forward"], "type": "branch", "target": {"hash": "44542c3afa4c75963f60e9424e87fc40ee503864", + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/44542c3afa4c75963f60e9424e87fc40ee503864"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/44542c3afa4c75963f60e9424e87fc40ee503864/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/44542c3afa4c75963f60e9424e87fc40ee503864"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/44542c3afa4c75963f60e9424e87fc40ee503864"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/44542c3afa4c75963f60e9424e87fc40ee503864"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/44542c3afa4c75963f60e9424e87fc40ee503864/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/44542c3afa4c75963f60e9424e87fc40ee503864/statuses"}}, + "author": {"raw": "Thiago Ramos ", "type": "author", "user": + {"display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/5bce04c759d0e84f8c7555e9/83eddf7b-6307-4b37-a193-a855cc683bc5/128"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}}, + "parents": [{"hash": "09dbf4b54aa0ee15cc22ca0068c206d4afe71a4e", "type": "commit", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/09dbf4b54aa0ee15cc22ca0068c206d4afe71a4e"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/09dbf4b54aa0ee15cc22ca0068c206d4afe71a4e"}}}], + "date": "2019-03-13T14:02:11+00:00", "message": "Testing new bash\n", "type": + "commit"}}, {"name": "second-branch", "links": {"commits": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commits/second-branch"}, + "self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/refs/branches/second-branch"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/branch/second-branch"}}, + "default_merge_strategy": "merge_commit", "merge_strategies": ["merge_commit", + "squash", "fast_forward"], "type": "branch", "target": {"hash": "f8a26b4c1bf8eef0bc3aeeb0b23f74f4b96a7d04", + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f8a26b4c1bf8eef0bc3aeeb0b23f74f4b96a7d04"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f8a26b4c1bf8eef0bc3aeeb0b23f74f4b96a7d04/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/f8a26b4c1bf8eef0bc3aeeb0b23f74f4b96a7d04"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/f8a26b4c1bf8eef0bc3aeeb0b23f74f4b96a7d04"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/f8a26b4c1bf8eef0bc3aeeb0b23f74f4b96a7d04"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f8a26b4c1bf8eef0bc3aeeb0b23f74f4b96a7d04/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f8a26b4c1bf8eef0bc3aeeb0b23f74f4b96a7d04/statuses"}}, + "author": {"raw": "Thiago Ramos ", "type": "author", "user": + {"display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/5bce04c759d0e84f8c7555e9/83eddf7b-6307-4b37-a193-a855cc683bc5/128"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}}, + "parents": [{"hash": "d3bedda462a79fafe4f5dfdb0ecf710f558e6aab", "type": "commit", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/d3bedda462a79fafe4f5dfdb0ecf710f558e6aab"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/d3bedda462a79fafe4f5dfdb0ecf710f558e6aab"}}}], + "date": "2019-05-08T15:35:56+00:00", "message": "Message 11\n", "type": "commit"}}, + {"name": "example", "links": {"commits": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commits/example"}, + "self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/refs/branches/example"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/branch/example"}}, + "default_merge_strategy": "merge_commit", "merge_strategies": ["merge_commit", + "squash", "fast_forward"], "type": "branch", "target": {"hash": "204d5bdef94d5aa07db499096d7f5ae93ed769a4", + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/204d5bdef94d5aa07db499096d7f5ae93ed769a4"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/204d5bdef94d5aa07db499096d7f5ae93ed769a4/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/204d5bdef94d5aa07db499096d7f5ae93ed769a4"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/204d5bdef94d5aa07db499096d7f5ae93ed769a4"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/204d5bdef94d5aa07db499096d7f5ae93ed769a4"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/204d5bdef94d5aa07db499096d7f5ae93ed769a4/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/204d5bdef94d5aa07db499096d7f5ae93ed769a4/statuses"}}, + "author": {"raw": "Steve Peak ", "type": "author"}, "parents": + [{"hash": "ca27c05556fa077ec06606435e658615f91d8512", "type": "commit", "links": + {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/ca27c05556fa077ec06606435e658615f91d8512"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/ca27c05556fa077ec06606435e658615f91d8512"}}}], + "date": "2016-08-17T16:02:13+00:00", "message": "Update tests.py", "type": "commit"}}, + {"name": "future", "links": {"commits": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commits/future"}, + "self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/refs/branches/future"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/branch/future"}}, + "default_merge_strategy": "merge_commit", "merge_strategies": ["merge_commit", + "squash", "fast_forward"], "type": "branch", "target": {"hash": "e451bd2ad777a858a665c05a2bfe484a2250cd15", + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/e451bd2ad777a858a665c05a2bfe484a2250cd15"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/e451bd2ad777a858a665c05a2bfe484a2250cd15/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/e451bd2ad777a858a665c05a2bfe484a2250cd15"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/e451bd2ad777a858a665c05a2bfe484a2250cd15"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/e451bd2ad777a858a665c05a2bfe484a2250cd15"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/e451bd2ad777a858a665c05a2bfe484a2250cd15/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/e451bd2ad777a858a665c05a2bfe484a2250cd15/statuses"}}, + "author": {"raw": "Codecov Test Bot ", "type": "author", "user": + {"display_name": "Codecov Test", "uuid": "{26f58a7d-734e-434c-b59c-03a224735c63}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/%7B26f58a7d-734e-434c-b59c-03a224735c63%7D"}, + "html": {"href": "https://bitbucket.org/%7B26f58a7d-734e-434c-b59c-03a224735c63%7D/"}, + "avatar": {"href": "https://avatar-cdn.atlassian.com/557058%3Ae83c38f0-a908-4e6e-aeb1-e5cf9aef75a3?by=id&sg=MZrPRCc40RiG%2BiNt6Pfe%2BBpxyFg%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FCT-1.png"}}, + "nickname": "Codecov Test", "type": "user", "account_id": "557058:e83c38f0-a908-4e6e-aeb1-e5cf9aef75a3"}}, + "parents": [{"hash": "3adc4957bc501aa3aa31ff4e18fa64c4d51197b7", "type": "commit", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3adc4957bc501aa3aa31ff4e18fa64c4d51197b7"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/3adc4957bc501aa3aa31ff4e18fa64c4d51197b7"}}}], + "date": "2017-04-07T18:39:44+00:00", "message": "Circle build #437\nhttps://circleci.com/gh/codecov/testsuite/437\nbash + <(curl -s https://raw.githubusercontent.com/codecov/codecov-bash/main/codecov) + -v -u https://codecov.io -Z", "type": "commit"}}], "page": 1, "size": 5}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 27 Jun 2019 22:25:19 GMT + Etag: + - '"gz[4862bdd679c11845025f968f2d6596ed]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.126863956451' + X-Request-Count: + - '206' + X-Served-By: + - app-144 + X-Static-Version: + - d664982621c0 + X-Version: + - d664982621c0 + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/refs/branches?oauth_nonce=68196bfc35d5473cba07ec92748644c1&oauth_timestamp=1561674319&oauth_consumer_key=arubajamaicaohiwan&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&oauth_token=testss3hxhcfqf1h6g&oauth_signature=7%2BIvkm9uQV%2FozMt2tOdrE4Hf5dY%3D&pagelen=100 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit.yaml new file mode 100644 index 0000000000..ee19a7df0e --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit.yaml @@ -0,0 +1,157 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/codecov/private/commit/6a45b83?oauth_consumer_key=arubajamaicaohiwan&oauth_token=waydowntokokomo&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1575576386&oauth_nonce=7487fd483bfd4daaa38904b8d9feca99&oauth_version=1.0&oauth_signature=QBngQxqDm8r4CVe5UnZK6KUEF60%3D + response: + content: '{"rendered": {"message": {"raw": "wip\n", "markup": "markdown", "html": + "

    wip

    ", "type": "rendered"}}, "hash": "6a45b838ae4fe22953c93aa17cc41b4b4216eb93", + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private"}, + "html": {"href": "https://bitbucket.org/codecov/private"}, "avatar": {"href": + "https://bytebucket.org/ravatar/%7B3edf54ab-cfe4-4049-aa70-5eb9f69f60d4%7D?ts=python"}}, + "type": "repository", "name": "private", "full_name": "codecov/private", "uuid": + "{3edf54ab-cfe4-4049-aa70-5eb9f69f60d4}"}, "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/commit/6a45b838ae4fe22953c93aa17cc41b4b4216eb93"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/commit/6a45b838ae4fe22953c93aa17cc41b4b4216eb93/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/patch/6a45b838ae4fe22953c93aa17cc41b4b4216eb93"}, + "html": {"href": "https://bitbucket.org/codecov/private/commits/6a45b838ae4fe22953c93aa17cc41b4b4216eb93"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/diff/6a45b838ae4fe22953c93aa17cc41b4b4216eb93"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/commit/6a45b838ae4fe22953c93aa17cc41b4b4216eb93/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/commit/6a45b838ae4fe22953c93aa17cc41b4b4216eb93/statuses"}}, + "author": {"raw": "stevepeak ", "type": "author", "user": + {"display_name": "Steve Peak", "uuid": "{test6y9pl15lzivhmkgsk67k10x53n04i85o}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/%7Btest6y9pl15lzivhmkgsk67k10x53n04i85o%7D"}, + "html": {"href": "https://bitbucket.org/%7Btest6y9pl15lzivhmkgsk67k10x53n04i85o%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/SP-6.png"}}, + "nickname": "stevepeak", "type": "user", "account_id": "557058:f66972a5-1c36-43ab-9543-cd030a0dae21"}}, + "summary": {"raw": "wip\n", "markup": "markdown", "html": "

    wip

    ", "type": + "rendered"}, "participants": [], "parents": [{"hash": "0028015f7fa260f5fd68f78c0deffc15183d955e", + "type": "commit", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/commit/0028015f7fa260f5fd68f78c0deffc15183d955e"}, + "html": {"href": "https://bitbucket.org/codecov/private/commits/0028015f7fa260f5fd68f78c0deffc15183d955e"}}}], + "date": "2015-02-27T03:44:32+00:00", "message": "wip\n", "type": "commit"}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 05 Dec 2019 20:06:27 GMT + Etag: + - '"gz[0b612cb1ba1520ccb7ae95109e8bb9a8]"' + Last-Modified: + - Fri, 27 Feb 2015 03:44:45 GMT + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Reads-Before-Write-From: + - default + X-Render-Time: + - '0.741528987885' + X-Request-Count: + - '2098' + X-Served-By: + - app-1118 + X-Static-Version: + - abd48467d96e + X-Version: + - abd48467d96e + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/codecov/private/commit/6a45b83?oauth_consumer_key=arubajamaicaohiwan&oauth_token=waydowntokokomo&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1575576386&oauth_nonce=7487fd483bfd4daaa38904b8d9feca99&oauth_version=1.0&oauth_signature=QBngQxqDm8r4CVe5UnZK6KUEF60%3D +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/users/stevepeak?oauth_consumer_key=arubajamaicaohiwan&oauth_token=waydowntokokomo&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1575576388&oauth_nonce=9b743f0720d74655a3b083aa8d756bdf&oauth_version=1.0&oauth_signature=o69UGzzNxIMf6CZ%2B1BdnMIclt5I%3D + response: + content: '{"display_name": "Steve Peak", "uuid": "{test6y9pl15lzivhmkgsk67k10x53n04i85o}", + "links": {"hooks": {"href": "https://bitbucket.org/!api/2.0/users/%7Btest6y9pl15lzivhmkgsk67k10x53n04i85o%7D/hooks"}, + "self": {"href": "https://bitbucket.org/!api/2.0/users/%7Btest6y9pl15lzivhmkgsk67k10x53n04i85o%7D"}, + "repositories": {"href": "https://bitbucket.org/!api/2.0/repositories/%7Btest6y9pl15lzivhmkgsk67k10x53n04i85o%7D"}, + "html": {"href": "https://bitbucket.org/%7Btest6y9pl15lzivhmkgsk67k10x53n04i85o%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/SP-6.png"}, + "snippets": {"href": "https://bitbucket.org/!api/2.0/snippets/%7Btest6y9pl15lzivhmkgsk67k10x53n04i85o%7D"}}, + "nickname": "stevepeak", "created_on": "2012-11-25T21:15:09.146957+00:00", "is_staff": + false, "account_status": "active", "type": "user", "account_id": "557058:f66972a5-1c36-43ab-9543-cd030a0dae21"}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Length: + - '921' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 05 Dec 2019 20:06:28 GMT + Etag: + - '"501ff97fa50378e6f7f21129d531ca14"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + X-Cache-Info: + - caching + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Reads-Before-Write-From: + - default + X-Render-Time: + - '0.060840845108' + X-Request-Count: + - '4932' + X-Served-By: + - app-1118 + X-Static-Version: + - abd48467d96e + X-Version: + - abd48467d96e + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/users/stevepeak?oauth_consumer_key=arubajamaicaohiwan&oauth_token=waydowntokokomo&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1575576388&oauth_nonce=9b743f0720d74655a3b083aa8d756bdf&oauth_version=1.0&oauth_signature=o69UGzzNxIMf6CZ%2B1BdnMIclt5I%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit_diff.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit_diff.yaml new file mode 100644 index 0000000000..978f71879f --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit_diff.yaml @@ -0,0 +1,80 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/diff/3017d53?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541599673&oauth_nonce=7f08d97c0f8f4826b4e9c79f9969f788&oauth_version=1.0&oauth_signature=NQpb%2B4E%2BtSRDp9XufM9d6ha1QVk%3D + response: + content: 'diff --git a/awesome/code_fib.py b/awesome/code_fib.py + + new file mode 100644 + + index 0000000..eafa811 + + --- /dev/null + + +++ b/awesome/code_fib.py + + @@ -0,0 +1,4 @@ + + +def fib(n): + + + if n <= 1: + + + return 0 + + + return fib(n - 1) + fib(n - 2) + + ' + headers: + Cache-Control: + - max-age=900 + Connection: + - close + Content-Length: + - '238' + Content-Type: + - text/plain + Date: + - Wed, 07 Nov 2018 14:07:53 GMT + Etag: + - '"87fe259700a5ff6d84edf0ff81827089"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.160104990005' + X-Request-Count: + - '441' + X-Served-By: + - app-144 + X-Static-Version: + - 1f6d684e3c29 + X-Version: + - 1f6d684e3c29 + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/diff/3017d53?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541599673&oauth_nonce=7f08d97c0f8f4826b4e9c79f9969f788&oauth_version=1.0&oauth_signature=NQpb%2B4E%2BtSRDp9XufM9d6ha1QVk%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit_no_uuid.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit_no_uuid.yaml new file mode 100644 index 0000000000..82640454de --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit_no_uuid.yaml @@ -0,0 +1,152 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/codecov/private/commit/6a45b83?oauth_consumer_key=arubajamaicaohiwan&oauth_token=waydowntokokomo&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1589295213&oauth_nonce=122b27a07a7b4e2881665d1e5e6f1d69&oauth_version=1.0&oauth_signature=%2FrQVb4nY1pR9uKtpoajmyJy9pz8%3D + response: + content: '{"rendered": {"message": {"raw": "wip\n", "markup": "markdown", "html": + "

    wip

    ", "type": "rendered"}}, "hash": "6a45b838ae4fe22953c93aa17cc41b4b4216eb93", + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private"}, + "html": {"href": "https://bitbucket.org/codecov/private"}, "avatar": {"href": + "https://bytebucket.org/ravatar/%7B3edf54ab-cfe4-4049-aa70-5eb9f69f60d4%7D?ts=python"}}, + "type": "repository", "name": "private", "full_name": "codecov/private", "uuid": + "{3edf54ab-cfe4-4049-aa70-5eb9f69f60d4}"}, "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/commit/6a45b838ae4fe22953c93aa17cc41b4b4216eb93"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/commit/6a45b838ae4fe22953c93aa17cc41b4b4216eb93/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/patch/6a45b838ae4fe22953c93aa17cc41b4b4216eb93"}, + "html": {"href": "https://bitbucket.org/codecov/private/commits/6a45b838ae4fe22953c93aa17cc41b4b4216eb93"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/diff/6a45b838ae4fe22953c93aa17cc41b4b4216eb93"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/commit/6a45b838ae4fe22953c93aa17cc41b4b4216eb93/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/commit/6a45b838ae4fe22953c93aa17cc41b4b4216eb93/statuses"}}, + "author": {"raw": "stevepeak ", "type": "author", "user": + {"display_name": "Steve Peak", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/%7Btest6y9pl15lzivhmkgsk67k10x53n04i85o%7D"}, + "html": {"href": "https://bitbucket.org/%7Btest6y9pl15lzivhmkgsk67k10x53n04i85o%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/SP-6.png"}}, + "nickname": "stevepeak", "type": "user", "account_id": "557058:f66972a5-1c36-43ab-9543-cd030a0dae21"}}, + "summary": {"raw": "wip\n", "markup": "markdown", "html": "

    wip

    ", "type": + "rendered"}, "participants": [], "parents": [{"hash": "0028015f7fa260f5fd68f78c0deffc15183d955e", + "type": "commit", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/commit/0028015f7fa260f5fd68f78c0deffc15183d955e"}, + "html": {"href": "https://bitbucket.org/codecov/private/commits/0028015f7fa260f5fd68f78c0deffc15183d955e"}}}], + "date": "2015-02-27T03:44:32+00:00", "message": "wip\n", "type": "commit"}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 12 May 2020 14:53:33 GMT + Dc-Location: + - ash1 + Etag: + - '"gz[0b612cb1ba1520ccb7ae95109e8bb9a8]"' + Last-Modified: + - Fri, 27 Feb 2015 03:44:45 GMT + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.290028810501' + X-Request-Count: + - '2728' + X-Served-By: + - app-1119 + X-Static-Version: + - afcf0ac51a78 + X-Version: + - afcf0ac51a78 + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/codecov/private/commit/6a45b83?oauth_consumer_key=arubajamaicaohiwan&oauth_token=waydowntokokomo&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1589295213&oauth_nonce=122b27a07a7b4e2881665d1e5e6f1d69&oauth_version=1.0&oauth_signature=%2FrQVb4nY1pR9uKtpoajmyJy9pz8%3D +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/users/557058:f66972a5-1c36-43ab-9543-cd030a0dae21?oauth_consumer_key=arubajamaicaohiwan&oauth_token=waydowntokokomo&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1589296016&oauth_nonce=15e3029593534a5f82936b1316705196&oauth_version=1.0&oauth_signature=S2CiFxmssCuPjAW56O0hKqOlbtY%3D + response: + content: '{"display_name": "Steve Peak", "uuid": "{test6y9pl15lzivhmkgsk67k10x53n04i85o}", + "links": {"hooks": {"href": "https://bitbucket.org/!api/2.0/users/%7Btest6y9pl15lzivhmkgsk67k10x53n04i85o%7D/hooks"}, + "self": {"href": "https://bitbucket.org/!api/2.0/users/%7Btest6y9pl15lzivhmkgsk67k10x53n04i85o%7D"}, + "repositories": {"href": "https://bitbucket.org/!api/2.0/repositories/%7Btest6y9pl15lzivhmkgsk67k10x53n04i85o%7D"}, + "html": {"href": "https://bitbucket.org/%7Btest6y9pl15lzivhmkgsk67k10x53n04i85o%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/SP-6.png"}, + "snippets": {"href": "https://bitbucket.org/!api/2.0/snippets/%7Btest6y9pl15lzivhmkgsk67k10x53n04i85o%7D"}}, + "nickname": "stevepeak", "created_on": "2012-11-25T21:15:09.146957+00:00", "is_staff": + false, "account_status": "active", "type": "user", "account_id": "557058:f66972a5-1c36-43ab-9543-cd030a0dae21"}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Content-Length: + - '921' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 12 May 2020 15:06:57 GMT + Dc-Location: + - ash1 + Etag: + - '"501ff97fa50378e6f7f21129d531ca14"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + X-Cache-Info: + - caching + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.262531995773' + X-Request-Count: + - '3619' + X-Served-By: + - app-1129 + X-Static-Version: + - afcf0ac51a78 + X-Version: + - afcf0ac51a78 + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/users/557058:f66972a5-1c36-43ab-9543-cd030a0dae21?oauth_consumer_key=arubajamaicaohiwan&oauth_token=waydowntokokomo&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1589296016&oauth_nonce=15e3029593534a5f82936b1316705196&oauth_version=1.0&oauth_signature=S2CiFxmssCuPjAW56O0hKqOlbtY%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit_not_found.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit_not_found.yaml new file mode 100644 index 0000000000..e1a82d921a --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit_not_found.yaml @@ -0,0 +1,60 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/commit/none?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541516650&oauth_nonce=c540a957a26c4efa8c99d95b3559ec24&oauth_version=1.0&oauth_signature=1lAqnWwUkGv4ivSZ3G%2BGByd7Alo%3D + response: + content: '{"type": "error", "error": {"message": "Changeset not found."}}' + headers: + Cache-Control: + - max-age=900 + Connection: + - close + Content-Length: + - '63' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 06 Nov 2018 15:04:11 GMT + Etag: + - '"ee9474a321558b5791a6292d0f19fd94"' + Last-Modified: + - Tue, 06 Nov 2018 12:53:06 GMT + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.0715439319611' + X-Request-Count: + - '200' + X-Served-By: + - app-188 + X-Static-Version: + - 4ea3c9800ace + X-Version: + - 4ea3c9800ace + status: + code: 404 + message: Not Found + status_code: 404 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/commit/none?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541516650&oauth_nonce=c540a957a26c4efa8c99d95b3559ec24&oauth_version=1.0&oauth_signature=1lAqnWwUkGv4ivSZ3G%2BGByd7Alo%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit_statuses.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit_statuses.yaml new file mode 100644 index 0000000000..49af3f1d89 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_commit_statuses.yaml @@ -0,0 +1,60 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d53/statuses?page=1&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541600748&oauth_nonce=72e42ab0da7143a0ab3a7a6a9dad659f&oauth_version=1.0&oauth_signature=x9HN6uts0FTPXJp%2BwnFIRb99nPU%3D + response: + content: '{"pagelen": 10, "values": [], "page": 1, "size": 0}' + headers: + Cache-Control: + - max-age=900 + Connection: + - close + Content-Length: + - '51' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 07 Nov 2018 14:25:49 GMT + Etag: + - '"e671435b14693e94f294247ac6f96b5f"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.128561019897' + X-Request-Count: + - '267' + X-Served-By: + - app-141 + X-Static-Version: + - 1f6d684e3c29 + X-Version: + - 1f6d684e3c29 + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d53/statuses?page=1&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541600748&oauth_nonce=72e42ab0da7143a0ab3a7a6a9dad659f&oauth_version=1.0&oauth_signature=x9HN6uts0FTPXJp%2BwnFIRb99nPU%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_compare.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_compare.yaml new file mode 100644 index 0000000000..f669f47770 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_compare.yaml @@ -0,0 +1,92 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - bitbucket.org + user-agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/diff/b92edba..6ae5f17?context=1&oauth_consumer_key=arubajamaicaohiwan&oauth_signature_method=HMAC-SHA1&oauth_token=testss3hxhcfqf1h6g&oauth_version=1.0 + response: + content: "diff --git a/README.rst b/README.rst\nindex 09c90a8..405d834 100644\n--- + a/README.rst\n+++ b/README.rst\n@@ -11,3 +11,4 @@ Overview\n \n-Main website: + `Codecov `_.\n+\n+website: `Codecov `_.\n + \n@@ -48,3 +49,3 @@ You may need to configure a ``.coveragerc`` file. Learn + more `here `_.\n" + headers: + Cache-Control: + - must-revalidate, max-age=0 + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - text/plain + Date: + - Fri, 29 Oct 2021 14:32:16 GMT + ETag: + - '"gz[b6174f21649dc111a92136a8f9ab1a4b80464f41bca0fec4989119d2fa7d1a81]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + Vary: + - Authorization, Origin, cookie, user-context, Accept-Encoding + X-Accepted-OAuth-Scopes: + - repository + X-B3-TraceId: + - 27fa68fa2b7e2251 + X-Cache-Info: + - not cacheable; response specified max-age <= 0 + X-Consumer-Client-Id: + - arubajamaicaohiwan + X-Credential-Type: + - oauth1 + X-Dc-Location: + - Micros + X-Frame-Options: + - SAMEORIGIN + X-OAuth-Scopes: + - pullrequest, account + X-Render-Time: + - '0.174722909927' + X-Request-Count: + - '3471' + X-Served-By: + - 5c41b9fa51b5 + X-Static-Version: + - 667d31475744 + X-Usage-Input-Ops: + - '176' + X-Usage-Output-Ops: + - '0' + X-Usage-System-Time: + - '0.005862' + X-Usage-User-Time: + - '0.046338' + X-Version: + - 667d31475744 + X-View-Name: + - bitbucket.apps.repo2.api.v20.diff.DiffHandler + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_compare_same_commit.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_compare_same_commit.yaml new file mode 100644 index 0000000000..394c18dcb3 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_compare_same_commit.yaml @@ -0,0 +1,76 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - bitbucket.org + user-agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/diff/6ae5f17..6ae5f17?context=1&oauth_consumer_key=arubajamaicaohiwan&oauth_signature_method=HMAC-SHA1&oauth_token=testss3hxhcfqf1h6g&oauth_version=1.0 + response: + content: '' + headers: + Cache-Control: + - must-revalidate, max-age=0 + Connection: + - keep-alive + Content-Length: + - '0' + Content-Type: + - text/plain + Date: + - Fri, 29 Oct 2021 14:34:13 GMT + ETag: + - '"12259356ef026337a111aa75503a1c1a1197ba2d451b6e10e7cd1c7577a6e028"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Origin, cookie, user-context + X-Accepted-OAuth-Scopes: + - repository + X-B3-TraceId: + - e05f9710536c8a66 + X-Cache-Info: + - not cacheable; response specified max-age <= 0 + X-Consumer-Client-Id: + - arubajamaicaohiwan + X-Credential-Type: + - oauth1 + X-Dc-Location: + - Micros + X-Frame-Options: + - SAMEORIGIN + X-OAuth-Scopes: + - pullrequest, account + X-Render-Time: + - '0.128483057022' + X-Request-Count: + - '461' + X-Served-By: + - 07909d56b1cb + X-Static-Version: + - 667d31475744 + X-Usage-Input-Ops: + - '176' + X-Usage-Output-Ops: + - '0' + X-Usage-System-Time: + - '0.006206' + X-Usage-User-Time: + - '0.042409' + X-Version: + - 667d31475744 + X-View-Name: + - bitbucket.apps.repo2.api.v20.diff.DiffHandler + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_is_admin.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_is_admin.yaml new file mode 100644 index 0000000000..59d85dd658 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_is_admin.yaml @@ -0,0 +1,83 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - bitbucket.org + user-agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/user/permissions/workspaces?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1617378487&oauth_nonce=fe94e338b0294054a6973e26fba8b158&oauth_version=1.0&oauth_signature=fuzvRwiZHcRKdYQdU2yvGg9jhnw%3D + response: + content: '{"pagelen": 50, "values": [{"links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramosworkspace/members/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}}, + "permission": "owner", "last_accessed": null, "user": {"display_name": "Thiago + Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "workspace": {"slug": "thiagorramosworkspace", "type": "workspace", "name": + "ThiagoRRamosworkspace", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramosworkspace"}, + "html": {"href": "https://bitbucket.org/thiagorramosworkspace/"}, "avatar": + {"href": "https://bitbucket.org/workspaces/thiagorramosworkspace/avatar/?ts=1617373144"}}, + "uuid": "{727d78e8-7431-4532-9519-1e5fe2b61d4b}"}, "type": "workspace_membership", + "added_on": "2021-04-02T14:20:48.179"}], "page": 1, "size": 1}' + headers: + Cache-Control: + - private + Connection: + - Keep-Alive + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=utf-8 + DC-Location: + - ash2 + Date: + - Fri, 02 Apr 2021 15:48:07 GMT + ETag: + - '"gz[b1d58debfe044b8d21db575b63d35843]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + Vary: + - Authorization, cookie, user-context, Accept-Encoding + X-Accepted-OAuth-Scopes: + - account + X-B3-TraceId: + - d27e9456550b8e83 + X-Cache-Info: + - 'not cacheable; response specified "Cache-Control: private"' + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Dc-Location: + - ash2 + X-Frame-Options: + - SAMEORIGIN + X-OAuth-Scopes: + - webhook, snippet, issue, pullrequest:write, repository:delete, repository:admin, + project, team, account + X-Render-Time: + - '0.0563180446625' + X-Request-Count: + - '4490' + X-Served-By: + - app-3020 + X-Static-Version: + - 937e1df6c57f + X-Version: + - 937e1df6c57f + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_is_admin_not_admin.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_is_admin_not_admin.yaml new file mode 100644 index 0000000000..e0b33739f8 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_is_admin_not_admin.yaml @@ -0,0 +1,69 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - bitbucket.org + user-agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/user/permissions/workspaces?oauth_consumer_key=arubajamaicaohiwan&oauth_signature_method=HMAC-SHA1&oauth_token=testss3hxhcfqf1h6g&oauth_version=1.0 + response: + content: '{"pagelen": 50, "values": [], "page": 1, "size": 0}' + headers: + Cache-Control: + - private + Connection: + - Keep-Alive + Content-Length: + - '51' + Content-Type: + - application/json; charset=utf-8 + DC-Location: + - ash2 + Date: + - Fri, 02 Apr 2021 14:57:58 GMT + ETag: + - '"d42f8757ef1abf01e68f5601c2c0eb4b"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, cookie, user-context + X-Accepted-OAuth-Scopes: + - account + X-B3-TraceId: + - 21325741ffc43f02 + X-Cache-Info: + - 'not cacheable; response specified "Cache-Control: private"' + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Dc-Location: + - ash2 + X-Frame-Options: + - SAMEORIGIN + X-OAuth-Scopes: + - webhook, snippet, issue, pullrequest:write, repository:delete, repository:admin, + project, team, account + X-Render-Time: + - '0.0363488197327' + X-Request-Count: + - '2234' + X-Served-By: + - app-3016 + X-Static-Version: + - 937e1df6c57f + X-Version: + - 937e1df6c57f + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_pull_request[1-b0].yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_pull_request[1-b0].yaml new file mode 100644 index 0000000000..bfdd096034 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_pull_request[1-b0].yaml @@ -0,0 +1,270 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541519086&oauth_nonce=da983fada1d9419fa5de96e66bffbc3f&oauth_version=1.0&oauth_signature=XbL1fI4LzN%2Fngrmk%2BztzEVTrwMM%3D + response: + content: "{\"rendered\": {\"description\": {\"raw\": \"Maybeeeeee\\r\\n\\r\\nYou\u2019\ + re gonna be the one that saves meeee\", \"markup\": \"markdown\", \"html\":\ + \ \"

    Maybeeeeee

    \\n

    You\u2019re gonna be the one that saves meeee

    \"\ + , \"type\": \"rendered\"}, \"title\": {\"raw\": \"Hahaa That is a PR\", \"markup\"\ + : \"markdown\", \"html\": \"

    Hahaa That is a PR

    \", \"type\": \"rendered\"\ + }}, \"type\": \"pullrequest\", \"description\": \"Maybeeeeee\\r\\n\\r\\nYou\u2019\ + re gonna be the one that saves meeee\", \"links\": {\"decline\": {\"href\":\ + \ \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/decline\"\ + }, \"commits\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/commits\"\ + }, \"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments\"\ + }, \"merge\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/merge\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/pull-requests/1\"\ + }, \"activity\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/activity\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/diff\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/statuses\"\ + }}, \"title\": \"Hahaa That is a PR\", \"close_source_branch\": true, \"reviewers\"\ + : [], \"id\": 1, \"destination\": {\"commit\": {\"hash\": \"b92edba44fdd\",\ + \ \"type\": \"commit\", \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b92edba44fdd\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/b92edba44fdd\"\ + }}}, \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"branch\": {\"name\": \"main\"}}, \"created_on\": \"2018-11-06T15:43:55.296636+00:00\"\ + , \"summary\": {\"raw\": \"Maybeeeeee\\r\\n\\r\\nYou\u2019re gonna be the one\ + \ that saves meeee\", \"markup\": \"markdown\", \"html\": \"

    Maybeeeeee

    \\\ + n

    You\u2019re gonna be the one that saves meeee

    \", \"type\": \"rendered\"\ + }, \"source\": {\"commit\": {\"hash\": \"3017d534ab41\", \"type\": \"commit\"\ + , \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/3017d534ab41\"\ + }}}, \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"branch\": {\"name\": \"second-branch\"}}, \"comment_count\": 0, \"state\"\ + : \"MERGED\", \"task_count\": 0, \"participants\": [], \"reason\": \"\", \"updated_on\"\ + : \"2018-11-06T15:43:55.494768+00:00\", \"author\": {\"username\": \"ThiagoCodecov\"\ + , \"display_name\": \"Thiago Ramos\", \"account_id\": \"5bce04c759d0e84f8c7555e9\"\ + , \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/users/ThiagoCodecov\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/\"}, \"avatar\"\ + : {\"href\": \"https://bitbucket.org/account/ThiagoCodecov/avatar/\"}}, \"nickname\"\ + : \"ThiagoCodecov\", \"type\": \"user\", \"uuid\": \"{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}\"\ + }, \"merge_commit\": {\"hash\": \"b92edba44fdd\"}, \"closed_by\": null}" + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 06 Nov 2018 15:44:46 GMT + Etag: + - '"gz[ebc1db0919c1607d962a7196fb91b235]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - pullrequest + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.134595870972' + X-Request-Count: + - '131' + X-Served-By: + - app-160 + X-Static-Version: + - 4ea3c9800ace + X-Version: + - 4ea3c9800ace + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541519086&oauth_nonce=da983fada1d9419fa5de96e66bffbc3f&oauth_version=1.0&oauth_signature=XbL1fI4LzN%2Fngrmk%2BztzEVTrwMM%3D +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/commit/b92edba44fdd?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541519086&oauth_nonce=ccaeb71e52f54375b959a11234f8924b&oauth_version=1.0&oauth_signature=lS3Teg7gVOF0FHoSXr9Dj%2BRsrlo%3D + response: + content: '{"hash": "b92edba44fdd29fcc506317cc3ddeae1a723dd08", "repository": {"links": + {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b92edba44fdd29fcc506317cc3ddeae1a723dd08"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b92edba44fdd29fcc506317cc3ddeae1a723dd08/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/b92edba44fdd29fcc506317cc3ddeae1a723dd08"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/b92edba44fdd29fcc506317cc3ddeae1a723dd08"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b92edba44fdd29fcc506317cc3ddeae1a723dd08/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b92edba44fdd29fcc506317cc3ddeae1a723dd08/statuses"}}, + "author": {"raw": "Jerrod "}, "summary": {"raw": "Update + README.rst", "markup": "markdown", "html": "

    Update README.rst

    ", "type": + "rendered"}, "participants": [], "parents": [{"hash": "c7f608036a3d2e89f8c59989ee213900c1ef39d1", + "type": "commit", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/c7f608036a3d2e89f8c59989ee213900c1ef39d1"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1"}}}], + "date": "2018-07-09T23:51:16+00:00", "message": "Update README.rst", "type": + "commit"}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 06 Nov 2018 15:44:47 GMT + Etag: + - '"gz[6034116c6d1152b4896d2d76522be687]"' + Last-Modified: + - Tue, 06 Nov 2018 15:42:58 GMT + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.670464038849' + X-Request-Count: + - '575' + X-Served-By: + - app-144 + X-Static-Version: + - 4ea3c9800ace + X-Version: + - 4ea3c9800ace + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/commit/b92edba44fdd?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541519086&oauth_nonce=ccaeb71e52f54375b959a11234f8924b&oauth_version=1.0&oauth_signature=lS3Teg7gVOF0FHoSXr9Dj%2BRsrlo%3D +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541519088&oauth_nonce=b3f1a11c3244492d961496da1249b514&oauth_version=1.0&oauth_signature=1b7GWr37zHQx6V0moLV0%2BWiXX6Y%3D + response: + content: '{"hash": "3017d534ab41e217bdf34d4c615fb355b0081f4b", "repository": {"links": + {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/3017d534ab41e217bdf34d4c615fb355b0081f4b"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/3017d534ab41e217bdf34d4c615fb355b0081f4b"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/3017d534ab41e217bdf34d4c615fb355b0081f4b"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b/statuses"}}, + "author": {"raw": "Thiago Ribeiro Ramos "}, "summary": + {"raw": "Creating second branch :happy:\n", "markup": "markdown", "html": "

    Creating + second branch :happy:

    ", "type": "rendered"}, "participants": [], "parents": + [{"hash": "b92edba44fdd29fcc506317cc3ddeae1a723dd08", "type": "commit", "links": + {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b92edba44fdd29fcc506317cc3ddeae1a723dd08"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08"}}}], + "date": "2018-11-06T15:42:45+00:00", "message": "Creating second branch :happy:\n", + "type": "commit"}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 06 Nov 2018 15:44:48 GMT + Etag: + - '"gz[6a6a1c5c416b82641a089d388c37d191]"' + Last-Modified: + - Tue, 06 Nov 2018 15:42:58 GMT + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.313195943832' + X-Request-Count: + - '140' + X-Served-By: + - app-141 + X-Static-Version: + - 4ea3c9800ace + X-Version: + - 4ea3c9800ace + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541519088&oauth_nonce=b3f1a11c3244492d961496da1249b514&oauth_version=1.0&oauth_signature=1b7GWr37zHQx6V0moLV0%2BWiXX6Y%3D diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_pull_request_commits.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_pull_request_commits.yaml new file mode 100644 index 0000000000..881dbe2b6c --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_pull_request_commits.yaml @@ -0,0 +1,82 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/commits?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1557329523&oauth_nonce=8a1e01a50d7a4652b0fb66154494e464&oauth_version=1.0&oauth_signature=8Owtyk%2B%2BfFJ9ZcOdPBnT1OCgN%2Bo%3D + response: + content: '{"pagelen": 10, "values": [{"hash": "3017d534ab41e217bdf34d4c615fb355b0081f4b", + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/3017d534ab41e217bdf34d4c615fb355b0081f4b"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/3017d534ab41e217bdf34d4c615fb355b0081f4b"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/3017d534ab41e217bdf34d4c615fb355b0081f4b"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b/statuses"}}, + "author": {"raw": "Thiago Ribeiro Ramos ", "type": + "author"}, "summary": {"raw": "Creating second branch :happy:\n", "markup": + "markdown", "html": "

    Creating second branch :happy:

    ", "type": "rendered"}, + "parents": [{"hash": "b92edba44fdd29fcc506317cc3ddeae1a723dd08", "type": "commit", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b92edba44fdd29fcc506317cc3ddeae1a723dd08"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08"}}}], + "date": "2018-11-06T15:42:45+00:00", "message": "Creating second branch :happy:\n", + "type": "commit"}], "page": 1}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 08 May 2019 15:32:04 GMT + Etag: + - '"gz[e5d9f6d88a26a126574ec9c4d56ed69b]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - pullrequest + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.547208070755' + X-Request-Count: + - '89' + X-Served-By: + - app-141 + X-Static-Version: + - 86a24bb2f896 + X-Version: + - 86a24bb2f896 + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/commits?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1557329523&oauth_nonce=8a1e01a50d7a4652b0fb66154494e464&oauth_version=1.0&oauth_signature=8Owtyk%2B%2BfFJ9ZcOdPBnT1OCgN%2Bo%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_pull_request_commits_multiple_pages.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_pull_request_commits_multiple_pages.yaml new file mode 100644 index 0000000000..644289f59d --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_pull_request_commits_multiple_pages.yaml @@ -0,0 +1,406 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/commits?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1557330116&oauth_nonce=3a21bf1970bd4e85830de3519e931ea1&oauth_version=1.0&oauth_signature=7kTJV1LCOK1AVDDahsDfcEgPdNs%3D + response: + content: '{"pagelen": 10, "values": [{"hash": "f8a26b4c1bf8eef0bc3aeeb0b23f74f4b96a7d04", + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f8a26b4c1bf8eef0bc3aeeb0b23f74f4b96a7d04"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f8a26b4c1bf8eef0bc3aeeb0b23f74f4b96a7d04/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/f8a26b4c1bf8eef0bc3aeeb0b23f74f4b96a7d04"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/f8a26b4c1bf8eef0bc3aeeb0b23f74f4b96a7d04"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/f8a26b4c1bf8eef0bc3aeeb0b23f74f4b96a7d04"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f8a26b4c1bf8eef0bc3aeeb0b23f74f4b96a7d04/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f8a26b4c1bf8eef0bc3aeeb0b23f74f4b96a7d04/statuses"}}, + "author": {"raw": "Thiago Ramos ", "type": "author", "user": + {"username": "ThiagoCodecov", "display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/ThiagoCodecov"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/"}, "avatar": {"href": + "https://avatar-cdn.atlassian.com/5bce04c759d0e84f8c7555e9?by=id&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FT-6.svg"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}}, + "summary": {"raw": "Message 11\n", "markup": "markdown", "html": "

    Message + 11

    ", "type": "rendered"}, "parents": [{"hash": "d3bedda462a79fafe4f5dfdb0ecf710f558e6aab", + "type": "commit", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/d3bedda462a79fafe4f5dfdb0ecf710f558e6aab"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/d3bedda462a79fafe4f5dfdb0ecf710f558e6aab"}}}], + "date": "2019-05-08T15:35:56+00:00", "message": "Message 11\n", "type": "commit"}, + {"hash": "d3bedda462a79fafe4f5dfdb0ecf710f558e6aab", "repository": {"links": + {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/d3bedda462a79fafe4f5dfdb0ecf710f558e6aab"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/d3bedda462a79fafe4f5dfdb0ecf710f558e6aab/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/d3bedda462a79fafe4f5dfdb0ecf710f558e6aab"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/d3bedda462a79fafe4f5dfdb0ecf710f558e6aab"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/d3bedda462a79fafe4f5dfdb0ecf710f558e6aab"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/d3bedda462a79fafe4f5dfdb0ecf710f558e6aab/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/d3bedda462a79fafe4f5dfdb0ecf710f558e6aab/statuses"}}, + "author": {"raw": "Thiago Ramos ", "type": "author", "user": + {"username": "ThiagoCodecov", "display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/ThiagoCodecov"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/"}, "avatar": {"href": + "https://avatar-cdn.atlassian.com/5bce04c759d0e84f8c7555e9?by=id&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FT-6.svg"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}}, + "summary": {"raw": "Message 10\n", "markup": "markdown", "html": "

    Message + 10

    ", "type": "rendered"}, "parents": [{"hash": "bd666be433ce4123ab0674fc8eb86708d340c31b", + "type": "commit", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/bd666be433ce4123ab0674fc8eb86708d340c31b"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/bd666be433ce4123ab0674fc8eb86708d340c31b"}}}], + "date": "2019-05-08T15:35:49+00:00", "message": "Message 10\n", "type": "commit"}, + {"hash": "bd666be433ce4123ab0674fc8eb86708d340c31b", "repository": {"links": + {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/bd666be433ce4123ab0674fc8eb86708d340c31b"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/bd666be433ce4123ab0674fc8eb86708d340c31b/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/bd666be433ce4123ab0674fc8eb86708d340c31b"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/bd666be433ce4123ab0674fc8eb86708d340c31b"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/bd666be433ce4123ab0674fc8eb86708d340c31b"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/bd666be433ce4123ab0674fc8eb86708d340c31b/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/bd666be433ce4123ab0674fc8eb86708d340c31b/statuses"}}, + "author": {"raw": "Thiago Ramos ", "type": "author", "user": + {"username": "ThiagoCodecov", "display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/ThiagoCodecov"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/"}, "avatar": {"href": + "https://avatar-cdn.atlassian.com/5bce04c759d0e84f8c7555e9?by=id&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FT-6.svg"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}}, + "summary": {"raw": "Message 9\n", "markup": "markdown", "html": "

    Message + 9

    ", "type": "rendered"}, "parents": [{"hash": "c80b02c4b65d141f0274ebb13e2a88f22a31820c", + "type": "commit", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/c80b02c4b65d141f0274ebb13e2a88f22a31820c"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/c80b02c4b65d141f0274ebb13e2a88f22a31820c"}}}], + "date": "2019-05-08T15:35:44+00:00", "message": "Message 9\n", "type": "commit"}, + {"hash": "c80b02c4b65d141f0274ebb13e2a88f22a31820c", "repository": {"links": + {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/c80b02c4b65d141f0274ebb13e2a88f22a31820c"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/c80b02c4b65d141f0274ebb13e2a88f22a31820c/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/c80b02c4b65d141f0274ebb13e2a88f22a31820c"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/c80b02c4b65d141f0274ebb13e2a88f22a31820c"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/c80b02c4b65d141f0274ebb13e2a88f22a31820c"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/c80b02c4b65d141f0274ebb13e2a88f22a31820c/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/c80b02c4b65d141f0274ebb13e2a88f22a31820c/statuses"}}, + "author": {"raw": "Thiago Ramos ", "type": "author", "user": + {"username": "ThiagoCodecov", "display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/ThiagoCodecov"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/"}, "avatar": {"href": + "https://avatar-cdn.atlassian.com/5bce04c759d0e84f8c7555e9?by=id&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FT-6.svg"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}}, + "summary": {"raw": "Message 8\n", "markup": "markdown", "html": "

    Message + 8

    ", "type": "rendered"}, "parents": [{"hash": "b3fe71aeb1a405219f4bf58d44ba9a0057072d06", + "type": "commit", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b3fe71aeb1a405219f4bf58d44ba9a0057072d06"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/b3fe71aeb1a405219f4bf58d44ba9a0057072d06"}}}], + "date": "2019-05-08T15:35:37+00:00", "message": "Message 8\n", "type": "commit"}, + {"hash": "b3fe71aeb1a405219f4bf58d44ba9a0057072d06", "repository": {"links": + {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b3fe71aeb1a405219f4bf58d44ba9a0057072d06"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b3fe71aeb1a405219f4bf58d44ba9a0057072d06/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/b3fe71aeb1a405219f4bf58d44ba9a0057072d06"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/b3fe71aeb1a405219f4bf58d44ba9a0057072d06"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/b3fe71aeb1a405219f4bf58d44ba9a0057072d06"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b3fe71aeb1a405219f4bf58d44ba9a0057072d06/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b3fe71aeb1a405219f4bf58d44ba9a0057072d06/statuses"}}, + "author": {"raw": "Thiago Ramos ", "type": "author", "user": + {"username": "ThiagoCodecov", "display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/ThiagoCodecov"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/"}, "avatar": {"href": + "https://avatar-cdn.atlassian.com/5bce04c759d0e84f8c7555e9?by=id&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FT-6.svg"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}}, + "summary": {"raw": "Message 7\n", "markup": "markdown", "html": "

    Message + 7

    ", "type": "rendered"}, "parents": [{"hash": "2909d0fae30c1d3e628cab1f549e29e1da7b385d", + "type": "commit", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2909d0fae30c1d3e628cab1f549e29e1da7b385d"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/2909d0fae30c1d3e628cab1f549e29e1da7b385d"}}}], + "date": "2019-05-08T15:35:33+00:00", "message": "Message 7\n", "type": "commit"}, + {"hash": "2909d0fae30c1d3e628cab1f549e29e1da7b385d", "repository": {"links": + {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2909d0fae30c1d3e628cab1f549e29e1da7b385d"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2909d0fae30c1d3e628cab1f549e29e1da7b385d/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/2909d0fae30c1d3e628cab1f549e29e1da7b385d"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/2909d0fae30c1d3e628cab1f549e29e1da7b385d"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/2909d0fae30c1d3e628cab1f549e29e1da7b385d"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2909d0fae30c1d3e628cab1f549e29e1da7b385d/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/2909d0fae30c1d3e628cab1f549e29e1da7b385d/statuses"}}, + "author": {"raw": "Thiago Ramos ", "type": "author", "user": + {"username": "ThiagoCodecov", "display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/ThiagoCodecov"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/"}, "avatar": {"href": + "https://avatar-cdn.atlassian.com/5bce04c759d0e84f8c7555e9?by=id&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FT-6.svg"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}}, + "summary": {"raw": "Message 6\n", "markup": "markdown", "html": "

    Message + 6

    ", "type": "rendered"}, "parents": [{"hash": "3fe51078bb5f6000617d71e32cfde4ebed6f2052", + "type": "commit", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3fe51078bb5f6000617d71e32cfde4ebed6f2052"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/3fe51078bb5f6000617d71e32cfde4ebed6f2052"}}}], + "date": "2019-05-08T15:35:28+00:00", "message": "Message 6\n", "type": "commit"}, + {"hash": "3fe51078bb5f6000617d71e32cfde4ebed6f2052", "repository": {"links": + {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3fe51078bb5f6000617d71e32cfde4ebed6f2052"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3fe51078bb5f6000617d71e32cfde4ebed6f2052/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/3fe51078bb5f6000617d71e32cfde4ebed6f2052"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/3fe51078bb5f6000617d71e32cfde4ebed6f2052"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/3fe51078bb5f6000617d71e32cfde4ebed6f2052"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3fe51078bb5f6000617d71e32cfde4ebed6f2052/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3fe51078bb5f6000617d71e32cfde4ebed6f2052/statuses"}}, + "author": {"raw": "Thiago Ramos ", "type": "author", "user": + {"username": "ThiagoCodecov", "display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/ThiagoCodecov"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/"}, "avatar": {"href": + "https://avatar-cdn.atlassian.com/5bce04c759d0e84f8c7555e9?by=id&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FT-6.svg"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}}, + "summary": {"raw": "Message 5\n", "markup": "markdown", "html": "

    Message + 5

    ", "type": "rendered"}, "parents": [{"hash": "974bce36e097868d6eb087656f929dd698d0507e", + "type": "commit", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/974bce36e097868d6eb087656f929dd698d0507e"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/974bce36e097868d6eb087656f929dd698d0507e"}}}], + "date": "2019-05-08T15:35:23+00:00", "message": "Message 5\n", "type": "commit"}, + {"hash": "974bce36e097868d6eb087656f929dd698d0507e", "repository": {"links": + {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/974bce36e097868d6eb087656f929dd698d0507e"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/974bce36e097868d6eb087656f929dd698d0507e/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/974bce36e097868d6eb087656f929dd698d0507e"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/974bce36e097868d6eb087656f929dd698d0507e"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/974bce36e097868d6eb087656f929dd698d0507e"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/974bce36e097868d6eb087656f929dd698d0507e/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/974bce36e097868d6eb087656f929dd698d0507e/statuses"}}, + "author": {"raw": "Thiago Ramos ", "type": "author", "user": + {"username": "ThiagoCodecov", "display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/ThiagoCodecov"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/"}, "avatar": {"href": + "https://avatar-cdn.atlassian.com/5bce04c759d0e84f8c7555e9?by=id&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FT-6.svg"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}}, + "summary": {"raw": "Message 4\n", "markup": "markdown", "html": "

    Message + 4

    ", "type": "rendered"}, "parents": [{"hash": "3b2aa7b423369c766173121e8a8bfa2d225ee235", + "type": "commit", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3b2aa7b423369c766173121e8a8bfa2d225ee235"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/3b2aa7b423369c766173121e8a8bfa2d225ee235"}}}], + "date": "2019-05-08T15:35:19+00:00", "message": "Message 4\n", "type": "commit"}, + {"hash": "3b2aa7b423369c766173121e8a8bfa2d225ee235", "repository": {"links": + {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3b2aa7b423369c766173121e8a8bfa2d225ee235"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3b2aa7b423369c766173121e8a8bfa2d225ee235/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/3b2aa7b423369c766173121e8a8bfa2d225ee235"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/3b2aa7b423369c766173121e8a8bfa2d225ee235"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/3b2aa7b423369c766173121e8a8bfa2d225ee235"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3b2aa7b423369c766173121e8a8bfa2d225ee235/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3b2aa7b423369c766173121e8a8bfa2d225ee235/statuses"}}, + "author": {"raw": "Thiago Ramos ", "type": "author", "user": + {"username": "ThiagoCodecov", "display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/ThiagoCodecov"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/"}, "avatar": {"href": + "https://avatar-cdn.atlassian.com/5bce04c759d0e84f8c7555e9?by=id&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FT-6.svg"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}}, + "summary": {"raw": "Message 3\n", "markup": "markdown", "html": "

    Message + 3

    ", "type": "rendered"}, "parents": [{"hash": "f1b9dc07dcd5301c215824d1884816435cf269ea", + "type": "commit", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f1b9dc07dcd5301c215824d1884816435cf269ea"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/f1b9dc07dcd5301c215824d1884816435cf269ea"}}}], + "date": "2019-05-08T15:35:15+00:00", "message": "Message 3\n", "type": "commit"}, + {"hash": "f1b9dc07dcd5301c215824d1884816435cf269ea", "repository": {"links": + {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f1b9dc07dcd5301c215824d1884816435cf269ea"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f1b9dc07dcd5301c215824d1884816435cf269ea/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/f1b9dc07dcd5301c215824d1884816435cf269ea"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/f1b9dc07dcd5301c215824d1884816435cf269ea"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/f1b9dc07dcd5301c215824d1884816435cf269ea"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f1b9dc07dcd5301c215824d1884816435cf269ea/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/f1b9dc07dcd5301c215824d1884816435cf269ea/statuses"}}, + "author": {"raw": "Thiago Ramos ", "type": "author", "user": + {"username": "ThiagoCodecov", "display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/ThiagoCodecov"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/"}, "avatar": {"href": + "https://avatar-cdn.atlassian.com/5bce04c759d0e84f8c7555e9?by=id&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FT-6.svg"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}}, + "summary": {"raw": "Message 2\n", "markup": "markdown", "html": "

    Message + 2

    ", "type": "rendered"}, "parents": [{"hash": "266e6b98f88847c8c4b6e8cf38cf5397266211d3", + "type": "commit", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/266e6b98f88847c8c4b6e8cf38cf5397266211d3"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/266e6b98f88847c8c4b6e8cf38cf5397266211d3"}}}], + "date": "2019-05-08T15:35:10+00:00", "message": "Message 2\n", "type": "commit"}], + "page": 1, "next": "https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/commits?page=8xhd"}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 08 May 2019 15:41:56 GMT + Etag: + - '"gz[bbc9e92a11bd159aa2170ed8eb65d817]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - pullrequest + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.273545980453' + X-Request-Count: + - '403' + X-Served-By: + - app-143 + X-Static-Version: + - 86a24bb2f896 + X-Version: + - 86a24bb2f896 + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/commits?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1557330116&oauth_nonce=3a21bf1970bd4e85830de3519e931ea1&oauth_version=1.0&oauth_signature=7kTJV1LCOK1AVDDahsDfcEgPdNs%3D +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/commits?page=8xhd&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1557330116&oauth_nonce=09f75b95d13247bfb56fb579bcc6deab&oauth_version=1.0&oauth_signature=%2Bawwc%2F%2FfMR9Vr6YRo9rV5zZUxME%3D + response: + content: '{"pagelen": 10, "values": [{"hash": "266e6b98f88847c8c4b6e8cf38cf5397266211d3", + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/266e6b98f88847c8c4b6e8cf38cf5397266211d3"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/266e6b98f88847c8c4b6e8cf38cf5397266211d3/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/266e6b98f88847c8c4b6e8cf38cf5397266211d3"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/266e6b98f88847c8c4b6e8cf38cf5397266211d3"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/266e6b98f88847c8c4b6e8cf38cf5397266211d3"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/266e6b98f88847c8c4b6e8cf38cf5397266211d3/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/266e6b98f88847c8c4b6e8cf38cf5397266211d3/statuses"}}, + "author": {"raw": "Thiago Ramos ", "type": "author", "user": + {"username": "ThiagoCodecov", "display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/ThiagoCodecov"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/"}, "avatar": {"href": + "https://avatar-cdn.atlassian.com/5bce04c759d0e84f8c7555e9?by=id&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FT-6.svg"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}}, + "summary": {"raw": "Message 1\n", "markup": "markdown", "html": "

    Message + 1

    ", "type": "rendered"}, "parents": [{"hash": "3017d534ab41e217bdf34d4c615fb355b0081f4b", + "type": "commit", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/3017d534ab41e217bdf34d4c615fb355b0081f4b"}}}], + "date": "2019-05-08T15:35:04+00:00", "message": "Message 1\n", "type": "commit"}, + {"hash": "3017d534ab41e217bdf34d4c615fb355b0081f4b", "repository": {"links": + {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b"}, + "comments": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b/comments"}, + "patch": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/patch/3017d534ab41e217bdf34d4c615fb355b0081f4b"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/3017d534ab41e217bdf34d4c615fb355b0081f4b"}, + "diff": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/diff/3017d534ab41e217bdf34d4c615fb355b0081f4b"}, + "approve": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b/approve"}, + "statuses": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b/statuses"}}, + "author": {"raw": "Thiago Ribeiro Ramos ", "type": + "author"}, "summary": {"raw": "Creating second branch :happy:\n", "markup": + "markdown", "html": "

    Creating second branch :happy:

    ", "type": "rendered"}, + "parents": [{"hash": "b92edba44fdd29fcc506317cc3ddeae1a723dd08", "type": "commit", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b92edba44fdd29fcc506317cc3ddeae1a723dd08"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08"}}}], + "date": "2018-11-06T15:42:45+00:00", "message": "Creating second branch :happy:\n", + "type": "commit"}], "page": 2}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 08 May 2019 15:41:57 GMT + Etag: + - '"gz[8678167429e5e3076eabd5ff26b80b46]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - pullrequest + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.18968296051' + X-Request-Count: + - '421' + X-Served-By: + - app-146 + X-Static-Version: + - 86a24bb2f896 + X-Version: + - 86a24bb2f896 + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/commits?page=8xhd&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1557330116&oauth_nonce=09f75b95d13247bfb56fb579bcc6deab&oauth_version=1.0&oauth_signature=%2Bawwc%2F%2FfMR9Vr6YRo9rV5zZUxME%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_pull_request_fail.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_pull_request_fail.yaml new file mode 100644 index 0000000000..e1846b8b8b --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_pull_request_fail.yaml @@ -0,0 +1,58 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/100?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541516647&oauth_nonce=49da357ab8e640c4b33e8c255d21dc26&oauth_version=1.0&oauth_signature=BfuFRNVYccJ6bMxHMf0QiIfrqaw%3D + response: + content: Not Found + headers: + Cache-Control: + - max-age=900 + Connection: + - close + Content-Length: + - '9' + Content-Type: + - text/plain + Date: + - Tue, 06 Nov 2018 15:04:07 GMT + Etag: + - '"9d1ead73e678fa2f51a70a933b0bf017"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + X-Accepted-Oauth-Scopes: + - pullrequest + X-Cache-Info: + - caching + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.143668889999' + X-Request-Count: + - '39' + X-Served-By: + - app-139 + X-Static-Version: + - 4ea3c9800ace + X-Version: + - 4ea3c9800ace + status: + code: 404 + message: Not Found + status_code: 404 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/100?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541516647&oauth_nonce=49da357ab8e640c4b33e8c255d21dc26&oauth_version=1.0&oauth_signature=BfuFRNVYccJ6bMxHMf0QiIfrqaw%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_pull_requests.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_pull_requests.yaml new file mode 100644 index 0000000000..279722e196 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_pull_requests.yaml @@ -0,0 +1,102 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests?state=OPEN&page=1&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541519414&oauth_nonce=081105162122405c8c0492a0e5a9b8ad&oauth_version=1.0&oauth_signature=ftblvicG7qWmUdyWojB17tbwaWI%3D + response: + content: "{\"pagelen\": 10, \"values\": [{\"description\": \"Maybeeeeee\\r\\n\\\ + r\\nYou\u2019re gonna be the one that saves meeee\", \"links\": {\"decline\"\ + : {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/decline\"\ + }, \"commits\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/commits\"\ + }, \"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1\"\ + }, \"comments\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments\"\ + }, \"merge\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/merge\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/pull-requests/1\"\ + }, \"activity\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/activity\"\ + }, \"diff\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/diff\"\ + }, \"approve\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/approve\"\ + }, \"statuses\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/statuses\"\ + }}, \"title\": \"Hahaa That is a PR\", \"close_source_branch\": true, \"type\"\ + : \"pullrequest\", \"id\": 1, \"destination\": {\"commit\": {\"hash\": \"b92edba44fdd\"\ + , \"type\": \"commit\", \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/b92edba44fdd\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/b92edba44fdd\"\ + }}}, \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"branch\": {\"name\": \"main\"}}, \"created_on\": \"2018-11-06T15:43:55.296636+00:00\"\ + , \"summary\": {\"raw\": \"Maybeeeeee\\r\\n\\r\\nYou\u2019re gonna be the one\ + \ that saves meeee\", \"markup\": \"markdown\", \"html\": \"

    Maybeeeeee

    \\\ + n

    You\u2019re gonna be the one that saves meeee

    \", \"type\": \"rendered\"\ + }, \"source\": {\"commit\": {\"hash\": \"3017d534ab41\", \"type\": \"commit\"\ + , \"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python/commits/3017d534ab41\"\ + }}}, \"repository\": {\"links\": {\"self\": {\"href\": \"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python\"\ + }, \"html\": {\"href\": \"https://bitbucket.org/ThiagoCodecov/example-python\"\ + }, \"avatar\": {\"href\": \"https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default\"\ + }}, \"type\": \"repository\", \"name\": \"example-python\", \"full_name\": \"\ + ThiagoCodecov/example-python\", \"uuid\": \"{a8c50527-2c3a-480e-afe1-7700e2b00074}\"\ + }, \"branch\": {\"name\": \"second-branch\"}}, \"comment_count\": 0, \"state\"\ + : \"OPEN\", \"task_count\": 0, \"reason\": \"\", \"updated_on\": \"2018-11-06T15:43:55.494768+00:00\"\ + , \"author\": {\"username\": \"ThiagoCodecov\", \"display_name\": \"Thiago Ramos\"\ + , \"account_id\": \"5bce04c759d0e84f8c7555e9\", \"links\": {\"self\": {\"href\"\ + : \"https://bitbucket.org/!api/2.0/users/ThiagoCodecov\"}, \"html\": {\"href\"\ + : \"https://bitbucket.org/ThiagoCodecov/\"}, \"avatar\": {\"href\": \"https://bitbucket.org/account/ThiagoCodecov/avatar/\"\ + }}, \"nickname\": \"ThiagoCodecov\", \"type\": \"user\", \"uuid\": \"{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}\"\ + }, \"merge_commit\": null, \"closed_by\": null}], \"page\": 1, \"size\": 1}" + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 06 Nov 2018 15:50:15 GMT + Etag: + - '"gz[aa7d456d1d2dd9f8c83721240d0abf73]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - pullrequest + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.107109069824' + X-Request-Count: + - '238' + X-Served-By: + - app-141 + X-Static-Version: + - 4ea3c9800ace + X-Version: + - 4ea3c9800ace + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests?state=OPEN&page=1&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541519414&oauth_nonce=081105162122405c8c0492a0e5a9b8ad&oauth_version=1.0&oauth_signature=ftblvicG7qWmUdyWojB17tbwaWI%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_repository.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_repository.yaml new file mode 100644 index 0000000000..fa5451f5eb --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_repository.yaml @@ -0,0 +1,86 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541516658&oauth_nonce=6cd9584e29ab4fb7943788f0f4863ba0&oauth_version=1.0&oauth_signature=ITTWRg%2FnBwMS4lmOIdvDMVtKQKQ%3D + response: + content: '{"scm": "git", "website": "", "has_wiki": false, "name": "example-python", + "links": {"watchers": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/watchers"}, + "branches": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/refs/branches"}, + "tags": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/refs/tags"}, + "commits": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commits"}, + "clone": [{"href": "https://ThiagoCodecov@bitbucket.org/ThiagoCodecov/example-python.git", + "name": "https"}, {"href": "git@bitbucket.org:ThiagoCodecov/example-python.git", + "name": "ssh"}], "self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "source": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}, + "hooks": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/hooks"}, + "forks": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/forks"}, + "downloads": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/downloads"}, + "pullrequests": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests"}}, + "fork_policy": "no_public_forks", "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}", + "language": "", "created_on": "2018-11-06T12:53:06.587605+00:00", "mainbranch": + {"type": "branch", "name": "main"}, "full_name": "ThiagoCodecov/example-python", + "has_issues": false, "owner": {"username": "ThiagoCodecov", "display_name": + "Thiago Ramos", "account_id": "5bce04c759d0e84f8c7555e9", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/users/ThiagoCodecov"}, "html": {"href": + "https://bitbucket.org/ThiagoCodecov/"}, "avatar": {"href": "https://bitbucket.org/account/ThiagoCodecov/avatar/"}}, + "nickname": "ThiagoCodecov", "type": "user", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}"}, + "updated_on": "2018-11-06T12:53:06.821726+00:00", "size": 111786, "type": "repository", + "slug": "example-python", "is_private": true, "description": ""}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 06 Nov 2018 15:04:19 GMT + Etag: + - '"gz[ccc2bf429b8eeff900ae72a454290701]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.0547559261322' + X-Request-Count: + - '34' + X-Served-By: + - app-141 + X-Static-Version: + - 4ea3c9800ace + X-Version: + - 4ea3c9800ace + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541516658&oauth_nonce=6cd9584e29ab4fb7943788f0f4863ba0&oauth_version=1.0&oauth_signature=ITTWRg%2FnBwMS4lmOIdvDMVtKQKQ%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_source_master.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_source_master.yaml new file mode 100644 index 0000000000..7b829d76e1 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_source_master.yaml @@ -0,0 +1,64 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/src/master/tests/test_k.py?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1565278205&oauth_nonce=a01af83f79054905931ea31ddb0d3728&oauth_version=1.0&oauth_signature=SzP1qSAxizO%2Bn2sZFm5MYd%2FY%2Bu8%3D + response: + content: "from kaploft import smile, fib\n\n\ndef test_something():\n assert\ + \ smile() == ':)'\n\n\ndef test_fib():\n assert fib(1) == 1\n\n\ndef test_fib_second():\n\ + \ assert fib(3) == 3\n" + headers: + Cache-Control: + - max-age=900 + Connection: + - close + Content-Length: + - '171' + Content-Type: + - text/plain + Date: + - Thu, 08 Aug 2019 15:30:05 GMT + Etag: + - '-4866249612541806531' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Reads-Before-Write-From: + - default + X-Render-Time: + - '0.142271995544' + X-Request-Count: + - '369' + X-Served-By: + - app-142 + X-Static-Version: + - d7eafd731394 + X-Version: + - d7eafd731394 + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/src/master/tests/test_k.py?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1565278205&oauth_nonce=a01af83f79054905931ea31ddb0d3728&oauth_version=1.0&oauth_signature=SzP1qSAxizO%2Bn2sZFm5MYd%2FY%2Bu8%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_source_random_commit.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_source_random_commit.yaml new file mode 100644 index 0000000000..8881eba838 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_source_random_commit.yaml @@ -0,0 +1,62 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/src/96492d409fc86aa7ae31b214dfe6b08ae860458a/awesome/__init__.py?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1565277950&oauth_nonce=a1ab9517ae334e78b4aa9d4a3396b1b9&oauth_version=1.0&oauth_signature=bW3y7DCif0TmawI%2FTv2NHRr0gDA%3D + response: + content: "def smile():\n return \":)\"\n\ndef frown():\n return \":(\"\n" + headers: + Cache-Control: + - max-age=900 + Connection: + - close + Content-Length: + - '59' + Content-Type: + - text/plain + Date: + - Thu, 08 Aug 2019 15:25:51 GMT + Etag: + - '-8639567690277796124' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Reads-Before-Write-From: + - default + X-Render-Time: + - '0.0693199634552' + X-Request-Count: + - '94' + X-Served-By: + - app-146 + X-Static-Version: + - d7eafd731394 + X-Version: + - d7eafd731394 + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/src/96492d409fc86aa7ae31b214dfe6b08ae860458a/awesome/__init__.py?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1565277950&oauth_nonce=a1ab9517ae334e78b4aa9d4a3396b1b9&oauth_version=1.0&oauth_signature=bW3y7DCif0TmawI%2FTv2NHRr0gDA%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_source_random_commit_not_found.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_source_random_commit_not_found.yaml new file mode 100644 index 0000000000..5f7e7a0cf4 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_get_source_random_commit_not_found.yaml @@ -0,0 +1,60 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/src/96492d409fc86aa7ae31b214dfe6b08ae860458a/awesome/non_exising_file.py?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1565277951&oauth_nonce=6167bfcc119d47eab6b73f34b312ada9&oauth_version=1.0&oauth_signature=6dfFD9yUD7a%2BB%2BjJ33OKZ2nOgqw%3D + response: + content: '{"type":"error","error":{"message":"No such file or directory: awesome/non_exising_file.py"}}' + headers: + Cache-Control: + - max-age=900 + Connection: + - close + Content-Length: + - '93' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 08 Aug 2019 15:25:52 GMT + Etag: + - '"cset:1:e91bf881850e779a3f7bcce71f838c8ec0a6aa88"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Reads-Before-Write-From: + - default + X-Render-Time: + - '0.055340051651' + X-Request-Count: + - '242' + X-Served-By: + - app-149 + X-Static-Version: + - d7eafd731394 + X-Version: + - d7eafd731394 + status: + code: 404 + message: NOT FOUND + status_code: 404 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/src/96492d409fc86aa7ae31b214dfe6b08ae860458a/awesome/non_exising_file.py?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1565277951&oauth_nonce=6167bfcc119d47eab6b73f34b312ada9&oauth_version=1.0&oauth_signature=6dfFD9yUD7a%2BB%2BjJ33OKZ2nOgqw%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_files.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_files.yaml new file mode 100644 index 0000000000..b43c403133 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_files.yaml @@ -0,0 +1,62 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/src/second-branch/tests?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1583532942&oauth_nonce=5279ec8a149e421db80cf4e931fa7a50&oauth_version=1.0&oauth_signature=Di5RSb8vWBrh7VoUGF7%2FMOq3e%2FE%3D + response: + content: '{"pagelen":10,"values":[{"path":"tests/__pycache__","type":"commit_directory","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/tests/__pycache__/"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/tests/__pycache__/?format=meta"}},"commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}}},{"mimetype":"text/x-python","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/tests/__init__.py"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/tests/__init__.py?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/tests/__init__.py"}},"path":"tests/__init__.py","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":0},{"mimetype":"text/x-python","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/tests/test_k.py"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/tests/test_k.py?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/tests/test_k.py"}},"path":"tests/test_k.py","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":171}],"page":1}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 06 Mar 2020 22:15:42 GMT + Etag: + - '"gz[645f773bd04ccebe2c770a5eee37cb13]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.0936629772186' + X-Request-Count: + - '4860' + X-Served-By: + - app-1131 + X-Static-Version: + - a856747ae2e5 + X-Version: + - a856747ae2e5 + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/src/second-branch/tests?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1583532942&oauth_nonce=5279ec8a149e421db80cf4e931fa7a50&oauth_version=1.0&oauth_signature=Di5RSb8vWBrh7VoUGF7%2FMOq3e%2FE%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_permissions.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_permissions.yaml new file mode 100644 index 0000000000..708ab4477d --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_permissions.yaml @@ -0,0 +1,135 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/user/permissions/repositories?page=1&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1574530963&oauth_nonce=61c6c9bd8ae74a28b32148ddd1297e16&oauth_version=1.0&oauth_signature=dyDNhaDGwSRnymGpVGtBJ9dl6NA%3D + response: + content: '{"pagelen": 10, "values": [{"type": "repository_permission", "user": + {"display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/ci-repo"}, + "html": {"href": "https://bitbucket.org/codecov/ci-repo"}, "avatar": {"href": + "https://bytebucket.org/ravatar/%7Ba980e378-088f-48a8-9850-98923f497546%7D?ts=default"}}, + "type": "repository", "name": "ci-repo", "full_name": "codecov/ci-repo", "uuid": + "{a980e378-088f-48a8-9850-98923f497546}"}, "permission": "admin"}, {"type": + "repository_permission", "user": {"display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private"}, + "html": {"href": "https://bitbucket.org/codecov/private"}, "avatar": {"href": + "https://bytebucket.org/ravatar/%7B3edf54ab-cfe4-4049-aa70-5eb9f69f60d4%7D?ts=python"}}, + "type": "repository", "name": "private", "full_name": "codecov/private", "uuid": + "{3edf54ab-cfe4-4049-aa70-5eb9f69f60d4}"}, "permission": "admin"}, {"type": + "repository_permission", "user": {"display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/coverage.py"}, + "html": {"href": "https://bitbucket.org/codecov/coverage.py"}, "avatar": {"href": + "https://bytebucket.org/ravatar/%7Bd08f4587-489f-4b55-abad-3d4f396d9862%7D?ts=python"}}, + "type": "repository", "name": "coverage.py", "full_name": "codecov/coverage.py", + "uuid": "{d08f4587-489f-4b55-abad-3d4f396d9862}"}, "permission": "admin"}, {"type": + "repository_permission", "user": {"display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "permission": "admin"}, {"type": + "repository_permission", "user": {"display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/integration-test-repo"}, + "html": {"href": "https://bitbucket.org/codecov/integration-test-repo"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7B4fab7a33-92dd-450b-8d12-ea1ab7816300%7D?ts=python"}}, + "type": "repository", "name": "integration-test-repo", "full_name": "codecov/integration-test-repo", + "uuid": "{4fab7a33-92dd-450b-8d12-ea1ab7816300}"}, "permission": "admin"}, {"type": + "repository_permission", "user": {"display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/test-bb-integration-public"}, + "html": {"href": "https://bitbucket.org/codecov/test-bb-integration-public"}, + "avatar": {"href": "https://bytebucket.org/ravatar/%7B2e219352-777c-4e2b-9a16-71211fbd4d93%7D?ts=markdown"}}, + "type": "repository", "name": "test-bb-integration-public", "full_name": "codecov/test-bb-integration-public", + "uuid": "{2e219352-777c-4e2b-9a16-71211fbd4d93}"}, "permission": "admin"}, {"type": + "repository_permission", "user": {"display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/test-private-repo-2"}, + "html": {"href": "https://bitbucket.org/codecov/test-private-repo-2"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Bd215b8f1-b862-4fae-9bc8-c2c8ea2e1a70%7D?ts=python"}}, + "type": "repository", "name": "test-private-repo-2", "full_name": "codecov/test-private-repo-2", + "uuid": "{d215b8f1-b862-4fae-9bc8-c2c8ea2e1a70}"}, "permission": "admin"}], + "page": 1, "size": 7}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Sat, 23 Nov 2019 17:42:43 GMT + Etag: + - '"gz[b93c8d5932e602a75da0405ad7093579]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - account + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Reads-Before-Write-From: + - default + X-Render-Time: + - '0.171509981155' + X-Request-Count: + - '1788' + X-Served-By: + - app-1131 + X-Static-Version: + - f93a4a92e0ef + X-Version: + - f93a4a92e0ef + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/user/permissions/repositories?page=1&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1574530963&oauth_nonce=61c6c9bd8ae74a28b32148ddd1297e16&oauth_version=1.0&oauth_signature=dyDNhaDGwSRnymGpVGtBJ9dl6NA%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_repos.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_repos.yaml new file mode 100644 index 0000000000..e87abfc910 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_repos.yaml @@ -0,0 +1,218 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/codecov?oauth_nonce=4f5ef91295974e429927bc84aec888e8&oauth_timestamp=1574056637&oauth_consumer_key=arubajamaicaohiwan&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&oauth_token=testss3hxhcfqf1h6g&oauth_signature=wJlFcH5fZCsKE9iBZi0ELNQpHAQ%3D&page=1 + response: + content: '{"pagelen": 10, "values": [{"scm": "git", "website": "", "has_wiki": + false, "uuid": "{a980e378-088f-48a8-9850-98923f497546}", "links": {"watchers": + {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/ci-repo/watchers"}, + "branches": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/ci-repo/refs/branches"}, + "tags": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/ci-repo/refs/tags"}, + "commits": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/ci-repo/commits"}, + "clone": [{"href": "https://ThiagoCodecov@bitbucket.org/codecov/ci-repo.git", + "name": "https"}, {"href": "git@bitbucket.org:codecov/ci-repo.git", "name": + "ssh"}], "self": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/ci-repo"}, + "source": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/ci-repo/src"}, + "html": {"href": "https://bitbucket.org/codecov/ci-repo"}, "avatar": {"href": + "https://bytebucket.org/ravatar/%7Ba980e378-088f-48a8-9850-98923f497546%7D?ts=default"}, + "hooks": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/ci-repo/hooks"}, + "forks": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/ci-repo/forks"}, + "downloads": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/ci-repo/downloads"}, + "issues": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/ci-repo/issues"}, + "pullrequests": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/ci-repo/pullrequests"}}, + "fork_policy": "allow_forks", "name": "ci-repo", "project": {"key": "PROJ", + "type": "project", "uuid": "{5bdda146-54b5-4afc-95cf-17d09df45d16}", "links": + {"self": {"href": "https://bitbucket.org/!api/2.0/teams/codecov/projects/PROJ"}, + "html": {"href": "https://bitbucket.org/account/user/codecov/projects/PROJ"}, + "avatar": {"href": "https://bitbucket.org/account/user/codecov/projects/PROJ/avatar/32"}}, + "name": "Untitled project"}, "language": "", "created_on": "2014-09-22T00:31:02.553308+00:00", + "mainbranch": {"type": "branch", "name": "main"}, "full_name": "codecov/ci-repo", + "has_issues": true, "owner": {"username": "codecov", "display_name": "Codecov", + "type": "team", "uuid": "{6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/teams/%7B6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49%7D"}, + "html": {"href": "https://bitbucket.org/%7B6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49%7D/"}, + "avatar": {"href": "https://bitbucket.org/account/codecov/avatar/"}}}, "updated_on": + "2016-08-22T13:01:12.956929+00:00", "size": 161832, "type": "repository", "slug": + "ci-repo", "is_private": false, "description": "for ci testing"}, {"scm": "git", + "website": "", "has_wiki": false, "uuid": "{3edf54ab-cfe4-4049-aa70-5eb9f69f60d4}", + "links": {"watchers": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/watchers"}, + "branches": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/refs/branches"}, + "tags": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/refs/tags"}, + "commits": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/commits"}, + "clone": [{"href": "https://ThiagoCodecov@bitbucket.org/codecov/private.git", + "name": "https"}, {"href": "git@bitbucket.org:codecov/private.git", "name": + "ssh"}], "self": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private"}, + "source": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/src"}, + "html": {"href": "https://bitbucket.org/codecov/private"}, "avatar": {"href": + "https://bytebucket.org/ravatar/%7B3edf54ab-cfe4-4049-aa70-5eb9f69f60d4%7D?ts=python"}, + "hooks": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/hooks"}, + "forks": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/forks"}, + "downloads": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/downloads"}, + "pullrequests": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/private/pullrequests"}}, + "fork_policy": "no_public_forks", "name": "private", "project": {"key": "PROJ", + "type": "project", "uuid": "{5bdda146-54b5-4afc-95cf-17d09df45d16}", "links": + {"self": {"href": "https://bitbucket.org/!api/2.0/teams/codecov/projects/PROJ"}, + "html": {"href": "https://bitbucket.org/account/user/codecov/projects/PROJ"}, + "avatar": {"href": "https://bitbucket.org/account/user/codecov/projects/PROJ/avatar/32"}}, + "name": "Untitled project"}, "language": "python", "created_on": "2015-02-27T03:35:28.555210+00:00", + "mainbranch": {"type": "branch", "name": "main"}, "full_name": "codecov/private", + "has_issues": false, "owner": {"username": "codecov", "display_name": "Codecov", + "type": "team", "uuid": "{6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/teams/%7B6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49%7D"}, + "html": {"href": "https://bitbucket.org/%7B6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49%7D/"}, + "avatar": {"href": "https://bitbucket.org/account/codecov/avatar/"}}}, "updated_on": + "2015-02-27T03:44:45.906783+00:00", "size": 144782, "type": "repository", "slug": + "private", "is_private": true, "description": ""}, {"scm": "hg", "website": + null, "has_wiki": false, "uuid": "{d08f4587-489f-4b55-abad-3d4f396d9862}", "links": + {"watchers": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/coverage.py/watchers"}, + "branches": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/coverage.py/refs/branches"}, + "tags": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/coverage.py/refs/tags"}, + "commits": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/coverage.py/commits"}, + "clone": [{"href": "https://ThiagoCodecov@bitbucket.org/codecov/coverage.py", + "name": "https"}, {"href": "ssh://hg@bitbucket.org/codecov/coverage.py", "name": + "ssh"}], "self": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/coverage.py"}, + "source": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/coverage.py/src"}, + "html": {"href": "https://bitbucket.org/codecov/coverage.py"}, "avatar": {"href": + "https://bytebucket.org/ravatar/%7Bd08f4587-489f-4b55-abad-3d4f396d9862%7D?ts=python"}, + "hooks": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/coverage.py/hooks"}, + "forks": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/coverage.py/forks"}, + "downloads": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/coverage.py/downloads"}, + "issues": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/coverage.py/issues"}, + "pullrequests": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/coverage.py/pullrequests"}}, + "fork_policy": "allow_forks", "name": "coverage.py", "project": {"key": "PROJ", + "type": "project", "uuid": "{5bdda146-54b5-4afc-95cf-17d09df45d16}", "links": + {"self": {"href": "https://bitbucket.org/!api/2.0/teams/codecov/projects/PROJ"}, + "html": {"href": "https://bitbucket.org/account/user/codecov/projects/PROJ"}, + "avatar": {"href": "https://bitbucket.org/account/user/codecov/projects/PROJ/avatar/32"}}, + "name": "Untitled project"}, "language": "python", "created_on": "2015-05-17T22:59:41.300232+00:00", + "parent": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ned/coveragepy"}, + "html": {"href": "https://bitbucket.org/ned/coveragepy"}, "avatar": {"href": + "https://bytebucket.org/ravatar/%7B480ce285-de3f-4eff-9e9c-8eccf8f07e99%7D?ts=python"}}, + "type": "repository", "name": "coveragepy", "full_name": "ned/coveragepy", "uuid": + "{480ce285-de3f-4eff-9e9c-8eccf8f07e99}"}, "mainbranch": {"type": "named_branch", + "name": "default"}, "full_name": "codecov/coverage.py", "has_issues": true, + "owner": {"username": "codecov", "display_name": "Codecov", "type": "team", + "uuid": "{6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49}", "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/teams/%7B6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49%7D"}, + "html": {"href": "https://bitbucket.org/%7B6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49%7D/"}, + "avatar": {"href": "https://bitbucket.org/account/codecov/avatar/"}}}, "updated_on": + "2015-05-18T14:03:10.877986+00:00", "size": 4182791, "type": "repository", "slug": + "coverage.py", "is_private": false, "description": ""}, {"scm": "git", "website": + null, "has_wiki": false, "uuid": "{4fab7a33-92dd-450b-8d12-ea1ab7816300}", "links": + {"watchers": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/integration-test-repo/watchers"}, + "branches": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/integration-test-repo/refs/branches"}, + "tags": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/integration-test-repo/refs/tags"}, + "commits": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/integration-test-repo/commits"}, + "clone": [{"href": "https://ThiagoCodecov@bitbucket.org/codecov/integration-test-repo.git", + "name": "https"}, {"href": "git@bitbucket.org:codecov/integration-test-repo.git", + "name": "ssh"}], "self": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/integration-test-repo"}, + "source": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/integration-test-repo/src"}, + "html": {"href": "https://bitbucket.org/codecov/integration-test-repo"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7B4fab7a33-92dd-450b-8d12-ea1ab7816300%7D?ts=python"}, + "hooks": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/integration-test-repo/hooks"}, + "forks": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/integration-test-repo/forks"}, + "downloads": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/integration-test-repo/downloads"}, + "pullrequests": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/integration-test-repo/pullrequests"}}, + "fork_policy": "no_public_forks", "name": "integration-test-repo", "project": + {"key": "PROJ", "type": "project", "uuid": "{5bdda146-54b5-4afc-95cf-17d09df45d16}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/teams/codecov/projects/PROJ"}, + "html": {"href": "https://bitbucket.org/account/user/codecov/projects/PROJ"}, + "avatar": {"href": "https://bitbucket.org/account/user/codecov/projects/PROJ/avatar/32"}}, + "name": "Untitled project"}, "language": "python", "created_on": "2019-10-02T02:24:47.121351+00:00", + "mainbranch": {"type": "branch", "name": "main"}, "full_name": "codecov/integration-test-repo", + "has_issues": false, "owner": {"username": "codecov", "display_name": "Codecov", + "type": "team", "uuid": "{6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/teams/%7B6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49%7D"}, + "html": {"href": "https://bitbucket.org/%7B6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49%7D/"}, + "avatar": {"href": "https://bitbucket.org/account/codecov/avatar/"}}}, "updated_on": + "2019-10-02T02:24:47.827791+00:00", "size": 59667, "type": "repository", "slug": + "integration-test-repo", "is_private": true, "description": "A repo for testing + codecov''s bitbucket integration"}, {"scm": "git", "website": null, "has_wiki": + false, "uuid": "{2e219352-777c-4e2b-9a16-71211fbd4d93}", "links": {"watchers": + {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/test-bb-integration-public/watchers"}, + "branches": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/test-bb-integration-public/refs/branches"}, + "tags": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/test-bb-integration-public/refs/tags"}, + "commits": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/test-bb-integration-public/commits"}, + "clone": [{"href": "https://ThiagoCodecov@bitbucket.org/codecov/test-bb-integration-public.git", + "name": "https"}, {"href": "git@bitbucket.org:codecov/test-bb-integration-public.git", + "name": "ssh"}], "self": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/test-bb-integration-public"}, + "source": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/test-bb-integration-public/src"}, + "html": {"href": "https://bitbucket.org/codecov/test-bb-integration-public"}, + "avatar": {"href": "https://bytebucket.org/ravatar/%7B2e219352-777c-4e2b-9a16-71211fbd4d93%7D?ts=markdown"}, + "hooks": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/test-bb-integration-public/hooks"}, + "forks": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/test-bb-integration-public/forks"}, + "downloads": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/test-bb-integration-public/downloads"}, + "pullrequests": {"href": "https://bitbucket.org/!api/2.0/repositories/codecov/test-bb-integration-public/pullrequests"}}, + "fork_policy": "allow_forks", "name": "test-bb-integration-public", "project": + {"key": "PROJ", "type": "project", "uuid": "{5bdda146-54b5-4afc-95cf-17d09df45d16}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/teams/codecov/projects/PROJ"}, + "html": {"href": "https://bitbucket.org/account/user/codecov/projects/PROJ"}, + "avatar": {"href": "https://bitbucket.org/account/user/codecov/projects/PROJ/avatar/32"}}, + "name": "Untitled project"}, "language": "markdown", "created_on": "2019-10-04T14:47:42.067409+00:00", + "mainbranch": {"type": "branch", "name": "main"}, "full_name": "codecov/test-bb-integration-public", + "has_issues": false, "owner": {"username": "codecov", "display_name": "Codecov", + "type": "team", "uuid": "{6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/teams/%7B6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49%7D"}, + "html": {"href": "https://bitbucket.org/%7B6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49%7D/"}, + "avatar": {"href": "https://bitbucket.org/account/codecov/avatar/"}}}, "updated_on": + "2019-10-04T14:47:43.490833+00:00", "size": 59625, "type": "repository", "slug": + "test-bb-integration-public", "is_private": false, "description": ""}], "page": + 1, "size": 5}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 18 Nov 2019 05:57:17 GMT + Etag: + - '"gz[ee2bc1394df18a25d8dedeec8e7a16ca]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Reads-Before-Write-From: + - default + X-Render-Time: + - '0.0558829307556' + X-Request-Count: + - '941' + X-Served-By: + - app-1129 + X-Static-Version: + - c991a84b6162 + X-Version: + - c991a84b6162 + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/codecov?oauth_nonce=4f5ef91295974e429927bc84aec888e8&oauth_timestamp=1574056637&oauth_consumer_key=arubajamaicaohiwan&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&oauth_token=testss3hxhcfqf1h6g&oauth_signature=wJlFcH5fZCsKE9iBZi0ELNQpHAQ%3D&page=1 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_repos_no_username.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_repos_no_username.yaml new file mode 100644 index 0000000000..986e4594e8 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_repos_no_username.yaml @@ -0,0 +1,661 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - bitbucket.org + user-agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/user/permissions/workspaces?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1617378448&oauth_nonce=2cef59817b2d4705a17bedc4487b96b9&oauth_version=1.0&oauth_signature=G2N3hnpWZCOuMvtlb6qLWTLuFKE%3D + response: + content: '{"pagelen": 50, "values": [{"links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramostestnumbar3/members/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}}, + "permission": "member", "last_accessed": null, "user": {"display_name": "Thiago + Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "workspace": {"slug": "thiagorramostestnumbar3", "type": "workspace", "name": + "thiagorramostestnumbar3", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramostestnumbar3"}, + "html": {"href": "https://bitbucket.org/thiagorramostestnumbar3/"}, "avatar": + {"href": "https://bitbucket.org/workspaces/thiagorramostestnumbar3/avatar/?ts=1617373497"}}, + "uuid": "{d7c73e87-90ab-450f-bb5f-39e6a5870456}"}, "type": "workspace_membership", + "added_on": "2021-04-02T14:25:58.600"}, {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramostest2/members/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}}, + "permission": "collaborator", "last_accessed": null, "user": {"display_name": + "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "workspace": {"slug": "thiagorramostest2", "type": "workspace", "name": "ThiagoRRamostest2", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramostest2"}, + "html": {"href": "https://bitbucket.org/thiagorramostest2/"}, "avatar": {"href": + "https://bitbucket.org/workspaces/thiagorramostest2/avatar/?ts=1617373390"}}, + "uuid": "{33b5f87a-bda0-40c2-ba1b-9eb892492290}"}, "type": "workspace_membership", + "added_on": "2021-04-02T14:24:01.106"}, {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramosanotherw/members/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}}, + "permission": "owner", "last_accessed": null, "user": {"display_name": "Thiago + Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "workspace": {"slug": "thiagorramosanotherw", "type": "workspace", "name": "ThiagoRRamosanotherw", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramosanotherw"}, + "html": {"href": "https://bitbucket.org/thiagorramosanotherw/"}, "avatar": {"href": + "https://bitbucket.org/workspaces/thiagorramosanotherw/avatar/?ts=1617373374"}}, + "uuid": "{11e04628-2c7b-4d89-9319-e7eed8818e56}"}, "type": "workspace_membership", + "added_on": "2021-04-02T14:22:54.290"}, {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramosworkspace/members/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}}, + "permission": "owner", "last_accessed": null, "user": {"display_name": "Thiago + Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "workspace": {"slug": "thiagorramosworkspace", "type": "workspace", "name": + "ThiagoRRamosworkspace", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramosworkspace"}, + "html": {"href": "https://bitbucket.org/thiagorramosworkspace/"}, "avatar": + {"href": "https://bitbucket.org/workspaces/thiagorramosworkspace/avatar/?ts=1617373144"}}, + "uuid": "{727d78e8-7431-4532-9519-1e5fe2b61d4b}"}, "type": "workspace_membership", + "added_on": "2021-04-02T14:20:48.179"}, {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagocodecovbanana/members/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}}, + "permission": "owner", "last_accessed": null, "user": {"display_name": "Thiago + Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "workspace": {"slug": "thiagocodecovbanana", "type": "workspace", "name": "ThiagoCodecovbanana", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagocodecovbanana"}, + "html": {"href": "https://bitbucket.org/thiagocodecovbanana/"}, "avatar": {"href": + "https://bitbucket.org/workspaces/thiagocodecovbanana/avatar/?ts=1617370980"}}, + "uuid": "{68f2da06-b2f8-4f00-92fa-32bd60df9d27}"}, "type": "workspace_membership", + "added_on": "2021-04-02T13:43:00.117"}, {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/ThiagoCodecov/members/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}}, + "permission": "owner", "last_accessed": null, "user": {"display_name": "Thiago + Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "workspace": {"slug": "ThiagoCodecov", "type": "workspace", "name": "Thiago + Ramos", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/ThiagoCodecov"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/"}, "avatar": {"href": + "https://bitbucket.org/workspaces/ThiagoCodecov/avatar/?ts=1543758107"}}, "uuid": + "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}"}, "type": "workspace_membership", "added_on": + null}], "page": 1, "size": 6}' + headers: + Cache-Control: + - private + Connection: + - Keep-Alive + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=utf-8 + DC-Location: + - ash2 + Date: + - Fri, 02 Apr 2021 15:47:29 GMT + ETag: + - '"gz[f9d666b4417eebda21d94a1e0f28b335]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + Vary: + - Authorization, cookie, user-context, Accept-Encoding + X-Accepted-OAuth-Scopes: + - account + X-B3-TraceId: + - e53d48681d5beb6c + X-Cache-Info: + - 'not cacheable; response specified "Cache-Control: private"' + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Dc-Location: + - ash2 + X-Frame-Options: + - SAMEORIGIN + X-OAuth-Scopes: + - webhook, snippet, issue, pullrequest:write, repository:delete, repository:admin, + project, team, account + X-Render-Time: + - '0.0693681240082' + X-Request-Count: + - '929' + X-Served-By: + - app-3006 + X-Static-Version: + - 937e1df6c57f + X-Version: + - 937e1df6c57f + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - bitbucket.org + user-agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/user/permissions/repositories?page=1&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1617378449&oauth_nonce=eb97637399e84b20a7264e406819042a&oauth_version=1.0&oauth_signature=iZhZfWro9xk5WBZ3BOFEuKTo1UQ%3D + response: + content: '{"pagelen": 10, "values": [{"type": "repository_permission", "user": + {"display_name": "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "repository": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "permission": "admin"}], + "page": 1, "size": 1}' + headers: + Cache-Control: + - private + Connection: + - Keep-Alive + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=utf-8 + DC-Location: + - ash2 + Date: + - Fri, 02 Apr 2021 15:47:30 GMT + ETag: + - '"gz[27c83aa41b14fe5489b5e008a5e89c39]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + Vary: + - Authorization, cookie, user-context, Accept-Encoding + X-Accepted-OAuth-Scopes: + - account, repository + X-B3-TraceId: + - 26763b49ad2ff550 + X-Cache-Info: + - 'not cacheable; response specified "Cache-Control: private"' + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Dc-Location: + - ash2 + X-Frame-Options: + - SAMEORIGIN + X-OAuth-Scopes: + - webhook, snippet, issue, pullrequest:write, repository:delete, repository:admin, + project, team, account + X-Render-Time: + - '0.0464129447937' + X-Request-Count: + - '618' + X-Served-By: + - app-3031 + X-Static-Version: + - 937e1df6c57f + X-Version: + - 937e1df6c57f + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - bitbucket.org + user-agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/thiagocodecovbanana?page=1&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1617378450&oauth_nonce=f1d27bd9905a4dd99aca4506db3f4075&oauth_version=1.0&oauth_signature=8t9863CqUO2M1MMz5XWVlIYo%2BTI%3D + response: + content: '{"pagelen": 10, "values": [], "page": 1, "size": 0}' + headers: + Cache-Control: + - private + Connection: + - Keep-Alive + Content-Length: + - '51' + Content-Type: + - application/json; charset=utf-8 + DC-Location: + - ash2 + Date: + - Fri, 02 Apr 2021 15:47:32 GMT + ETag: + - '"e671435b14693e94f294247ac6f96b5f"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, cookie, user-context + X-Accepted-OAuth-Scopes: + - repository + X-B3-TraceId: + - 3578dd49871be871 + X-Cache-Info: + - 'not cacheable; response specified "Cache-Control: private"' + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Dc-Location: + - ash2 + X-Frame-Options: + - SAMEORIGIN + X-OAuth-Scopes: + - webhook, snippet, issue, pullrequest:write, repository:delete, repository:admin, + project, team, account + X-Render-Time: + - '0.0550200939178' + X-Request-Count: + - '1340' + X-Served-By: + - app-3008 + X-Static-Version: + - 937e1df6c57f + X-Version: + - 937e1df6c57f + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - bitbucket.org + user-agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov?page=1&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1617378452&oauth_nonce=fa0d9bc2cae04dbcacdc760cc5d5e690&oauth_version=1.0&oauth_signature=dMe8TMVDfX7dOtf5UAIcl9Cr63s%3D + response: + content: '{"pagelen": 10, "values": [{"scm": "git", "website": "", "has_wiki": + false, "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}", "links": {"watchers": + {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/watchers"}, + "branches": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/refs/branches"}, + "tags": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/refs/tags"}, + "commits": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commits"}, + "clone": [{"href": "https://ThiagoCodecov@bitbucket.org/ThiagoCodecov/example-python.git", + "name": "https"}, {"href": "git@bitbucket.org:ThiagoCodecov/example-python.git", + "name": "ssh"}], "self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "source": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}, + "hooks": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/hooks"}, + "forks": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/forks"}, + "downloads": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/downloads"}, + "pullrequests": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests"}}, + "fork_policy": "no_public_forks", "full_name": "ThiagoCodecov/example-python", + "name": "example-python", "project": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/ThiagoCodecov/projects/PROJ"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/workspace/projects/PROJ"}, + "avatar": {"href": "https://bitbucket.org/account/user/ThiagoCodecov/projects/PROJ/avatar/32?ts=1543758107"}}, + "type": "project", "name": "Untitled project", "key": "PROJ", "uuid": "{ceb7caa9-0c9c-4391-8423-ed9dc36a2575}"}, + "language": "", "created_on": "2018-11-06T12:53:06.587605+00:00", "mainbranch": + {"type": "branch", "name": "main"}, "workspace": {"slug": "ThiagoCodecov", + "type": "workspace", "name": "Thiago Ramos", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/ThiagoCodecov"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/"}, "avatar": {"href": + "https://bitbucket.org/workspaces/ThiagoCodecov/avatar/?ts=1543758107"}}, "uuid": + "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}"}, "has_issues": false, "owner": {"display_name": + "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "updated_on": "2020-12-02T12:33:17.882179+00:00", "size": 550781, "type": "repository", + "slug": "example-python", "is_private": true, "description": ""}], "page": 1, + "size": 1}' + headers: + Cache-Control: + - private + Connection: + - Keep-Alive + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=utf-8 + DC-Location: + - ash2 + Date: + - Fri, 02 Apr 2021 15:47:33 GMT + ETag: + - '"gz[85f91cb12cf323880c2a4fb16c92015e]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + Vary: + - Authorization, cookie, user-context, Accept-Encoding + X-Accepted-OAuth-Scopes: + - repository + X-B3-TraceId: + - 67791b58634c6f3f + X-Cache-Info: + - 'not cacheable; response specified "Cache-Control: private"' + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Dc-Location: + - ash2 + X-Frame-Options: + - SAMEORIGIN + X-OAuth-Scopes: + - webhook, snippet, issue, pullrequest:write, repository:delete, repository:admin, + project, team, account + X-Render-Time: + - '0.0602920055389' + X-Request-Count: + - '874' + X-Served-By: + - app-3013 + X-Static-Version: + - 937e1df6c57f + X-Version: + - 937e1df6c57f + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - bitbucket.org + user-agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/thiagorramosworkspace?page=1&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1617378453&oauth_nonce=067cb97417664c3dafbf917b3ba3745d&oauth_version=1.0&oauth_signature=WsTLv5PDCxwVfhLJB4W53He0%2FaA%3D + response: + content: '{"pagelen": 10, "values": [], "page": 1, "size": 0}' + headers: + Cache-Control: + - private + Connection: + - Keep-Alive + Content-Length: + - '51' + Content-Type: + - application/json; charset=utf-8 + DC-Location: + - ash2 + Date: + - Fri, 02 Apr 2021 15:47:33 GMT + ETag: + - '"e671435b14693e94f294247ac6f96b5f"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, cookie, user-context + X-Accepted-OAuth-Scopes: + - repository + X-B3-TraceId: + - a627da97ccbda708 + X-Cache-Info: + - 'not cacheable; response specified "Cache-Control: private"' + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Dc-Location: + - ash2 + X-Frame-Options: + - SAMEORIGIN + X-OAuth-Scopes: + - webhook, snippet, issue, pullrequest:write, repository:delete, repository:admin, + project, team, account + X-Render-Time: + - '0.0422210693359' + X-Request-Count: + - '1481' + X-Served-By: + - app-3024 + X-Static-Version: + - 937e1df6c57f + X-Version: + - 937e1df6c57f + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - bitbucket.org + user-agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/thiagorramostestnumbar3?page=1&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1617378453&oauth_nonce=f8ebea66b7e049ff936758f26a3a0ad8&oauth_version=1.0&oauth_signature=aGF%2B4V%2BFx2xJwREQSGU1Kv5PWzc%3D + response: + content: '{"pagelen": 10, "values": [], "page": 1, "size": 0}' + headers: + Cache-Control: + - private + Connection: + - Keep-Alive + Content-Length: + - '51' + Content-Type: + - application/json; charset=utf-8 + DC-Location: + - ash2 + Date: + - Fri, 02 Apr 2021 15:47:33 GMT + ETag: + - '"e671435b14693e94f294247ac6f96b5f"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, cookie, user-context + X-Accepted-OAuth-Scopes: + - repository + X-B3-TraceId: + - 62611d8f3141204b + X-Cache-Info: + - 'not cacheable; response specified "Cache-Control: private"' + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Dc-Location: + - ash2 + X-Frame-Options: + - SAMEORIGIN + X-OAuth-Scopes: + - webhook, snippet, issue, pullrequest:write, repository:delete, repository:admin, + project, team, account + X-Render-Time: + - '0.0395669937134' + X-Request-Count: + - '1074' + X-Served-By: + - app-3002 + X-Static-Version: + - 937e1df6c57f + X-Version: + - 937e1df6c57f + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - bitbucket.org + user-agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/thiagorramostest2?page=1&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1617378453&oauth_nonce=84ccc86d7ea74e02af5dd0b21f5ea801&oauth_version=1.0&oauth_signature=amtp59yTXrSlESDa4BuGcP026HU%3D + response: + content: '{"pagelen": 10, "values": [], "page": 1, "size": 0}' + headers: + Cache-Control: + - private + Connection: + - Keep-Alive + Content-Length: + - '51' + Content-Type: + - application/json; charset=utf-8 + DC-Location: + - ash2 + Date: + - Fri, 02 Apr 2021 15:47:33 GMT + ETag: + - '"e671435b14693e94f294247ac6f96b5f"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, cookie, user-context + X-Accepted-OAuth-Scopes: + - repository + X-B3-TraceId: + - 320b9ce68cc4e9a6 + X-Cache-Info: + - 'not cacheable; response specified "Cache-Control: private"' + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Dc-Location: + - ash2 + X-Frame-Options: + - SAMEORIGIN + X-OAuth-Scopes: + - webhook, snippet, issue, pullrequest:write, repository:delete, repository:admin, + project, team, account + X-Render-Time: + - '0.0545430183411' + X-Request-Count: + - '1688' + X-Served-By: + - app-3013 + X-Static-Version: + - 937e1df6c57f + X-Version: + - 937e1df6c57f + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - bitbucket.org + user-agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/thiagorramosanotherw?page=1&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1617378454&oauth_nonce=c002378141da4fca846c268291c16291&oauth_version=1.0&oauth_signature=0eCiphpxhul4G%2BB0dfwka0tjf8c%3D + response: + content: '{"pagelen": 10, "values": [], "page": 1, "size": 0}' + headers: + Cache-Control: + - private + Connection: + - Keep-Alive + Content-Length: + - '51' + Content-Type: + - application/json; charset=utf-8 + DC-Location: + - ash2 + Date: + - Fri, 02 Apr 2021 15:47:34 GMT + ETag: + - '"e671435b14693e94f294247ac6f96b5f"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, cookie, user-context + X-Accepted-OAuth-Scopes: + - repository + X-B3-TraceId: + - e684e493ea211994 + X-Cache-Info: + - 'not cacheable; response specified "Cache-Control: private"' + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Dc-Location: + - ash2 + X-Frame-Options: + - SAMEORIGIN + X-OAuth-Scopes: + - webhook, snippet, issue, pullrequest:write, repository:delete, repository:admin, + project, team, account + X-Render-Time: + - '0.0421149730682' + X-Request-Count: + - '2539' + X-Served-By: + - app-3018 + X-Static-Version: + - 937e1df6c57f + X-Version: + - 937e1df6c57f + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_teams.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_teams.yaml new file mode 100644 index 0000000000..98101aa7e9 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_teams.yaml @@ -0,0 +1,143 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - bitbucket.org + user-agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/user/permissions/workspaces?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1617378454&oauth_nonce=d5ecf457da8d41be901fbc20c12fa5ff&oauth_version=1.0&oauth_signature=e%2BxL5oewcKsMyDq%2Fxf8v3F6LyC8%3D + response: + content: '{"pagelen": 50, "values": [{"links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramostestnumbar3/members/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}}, + "permission": "member", "last_accessed": null, "user": {"display_name": "Thiago + Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "workspace": {"slug": "thiagorramostestnumbar3", "type": "workspace", "name": + "thiagorramostestnumbar3", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramostestnumbar3"}, + "html": {"href": "https://bitbucket.org/thiagorramostestnumbar3/"}, "avatar": + {"href": "https://bitbucket.org/workspaces/thiagorramostestnumbar3/avatar/?ts=1617373497"}}, + "uuid": "{d7c73e87-90ab-450f-bb5f-39e6a5870456}"}, "type": "workspace_membership", + "added_on": "2021-04-02T14:25:58.600"}, {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramostest2/members/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}}, + "permission": "collaborator", "last_accessed": null, "user": {"display_name": + "Thiago Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "workspace": {"slug": "thiagorramostest2", "type": "workspace", "name": "ThiagoRRamostest2", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramostest2"}, + "html": {"href": "https://bitbucket.org/thiagorramostest2/"}, "avatar": {"href": + "https://bitbucket.org/workspaces/thiagorramostest2/avatar/?ts=1617373390"}}, + "uuid": "{33b5f87a-bda0-40c2-ba1b-9eb892492290}"}, "type": "workspace_membership", + "added_on": "2021-04-02T14:24:01.106"}, {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramosanotherw/members/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}}, + "permission": "owner", "last_accessed": null, "user": {"display_name": "Thiago + Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "workspace": {"slug": "thiagorramosanotherw", "type": "workspace", "name": "ThiagoRRamosanotherw", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramosanotherw"}, + "html": {"href": "https://bitbucket.org/thiagorramosanotherw/"}, "avatar": {"href": + "https://bitbucket.org/workspaces/thiagorramosanotherw/avatar/?ts=1617373374"}}, + "uuid": "{11e04628-2c7b-4d89-9319-e7eed8818e56}"}, "type": "workspace_membership", + "added_on": "2021-04-02T14:22:54.290"}, {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramosworkspace/members/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}}, + "permission": "owner", "last_accessed": null, "user": {"display_name": "Thiago + Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "workspace": {"slug": "thiagorramosworkspace", "type": "workspace", "name": + "ThiagoRRamosworkspace", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagorramosworkspace"}, + "html": {"href": "https://bitbucket.org/thiagorramosworkspace/"}, "avatar": + {"href": "https://bitbucket.org/workspaces/thiagorramosworkspace/avatar/?ts=1617373144"}}, + "uuid": "{727d78e8-7431-4532-9519-1e5fe2b61d4b}"}, "type": "workspace_membership", + "added_on": "2021-04-02T14:20:48.179"}, {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagocodecovbanana/members/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}}, + "permission": "owner", "last_accessed": null, "user": {"display_name": "Thiago + Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "workspace": {"slug": "thiagocodecovbanana", "type": "workspace", "name": "ThiagoCodecovbanana", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/thiagocodecovbanana"}, + "html": {"href": "https://bitbucket.org/thiagocodecovbanana/"}, "avatar": {"href": + "https://bitbucket.org/workspaces/thiagocodecovbanana/avatar/?ts=1617370980"}}, + "uuid": "{68f2da06-b2f8-4f00-92fa-32bd60df9d27}"}, "type": "workspace_membership", + "added_on": "2021-04-02T13:43:00.117"}, {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/ThiagoCodecov/members/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}}, + "permission": "owner", "last_accessed": null, "user": {"display_name": "Thiago + Ramos", "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "workspace": {"slug": "ThiagoCodecov", "type": "workspace", "name": "Thiago + Ramos", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/workspaces/ThiagoCodecov"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/"}, "avatar": {"href": + "https://bitbucket.org/workspaces/ThiagoCodecov/avatar/?ts=1543758107"}}, "uuid": + "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}"}, "type": "workspace_membership", "added_on": + null}], "page": 1, "size": 6}' + headers: + Cache-Control: + - private + Connection: + - Keep-Alive + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=utf-8 + DC-Location: + - ash2 + Date: + - Fri, 02 Apr 2021 15:47:35 GMT + ETag: + - '"gz[f9d666b4417eebda21d94a1e0f28b335]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + Vary: + - Authorization, cookie, user-context, Accept-Encoding + X-Accepted-OAuth-Scopes: + - account + X-B3-TraceId: + - c8373df1d238408f + X-Cache-Info: + - 'not cacheable; response specified "Cache-Control: private"' + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Dc-Location: + - ash2 + X-Frame-Options: + - SAMEORIGIN + X-OAuth-Scopes: + - webhook, snippet, issue, pullrequest:write, repository:delete, repository:admin, + project, team, account + X-Render-Time: + - '0.0590329170227' + X-Request-Count: + - '237' + X-Served-By: + - app-3013 + X-Static-Version: + - 937e1df6c57f + X-Version: + - 937e1df6c57f + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_top_level_files_multiple_pages.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_top_level_files_multiple_pages.yaml new file mode 100644 index 0000000000..d003453b38 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_top_level_files_multiple_pages.yaml @@ -0,0 +1,188 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/src/second-branch/?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1567082223&oauth_nonce=a3cdc47a1ac6423e910ba2e2aeb203a4&oauth_version=1.0&oauth_signature=Rxci9EnohgZDdmquYNRnUZaQkPg%3D + response: + content: '{"pagelen":10,"values":[{"path":"awesome","type":"commit_directory","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/awesome/"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/awesome/?format=meta"}},"commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}}},{"path":"kaploft","type":"commit_directory","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/kaploft/"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/kaploft/?format=meta"}},"commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}}},{"path":"tests","type":"commit_directory","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/tests/"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/tests/?format=meta"}},"commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}}},{"mimetype":null,"links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/.coverage"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/.coverage?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/.coverage"}},"path":".coverage","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":475},{"mimetype":null,"links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/.gitignore"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/.gitignore?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/.gitignore"}},"path":".gitignore","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":1346},{"mimetype":"text/prs.fallenstein.rst","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/README.rst"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/README.rst?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/README.rst"}},"path":"README.rst","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":6377},{"mimetype":"text/x-python","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/__init__.py"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/__init__.py?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/__init__.py"}},"path":"__init__.py","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":0},{"mimetype":"text/plain","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a1.txt"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a1.txt?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a1.txt"}},"path":"a1.txt","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":5},{"mimetype":"text/plain","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a10.txt"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a10.txt?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a10.txt"}},"path":"a10.txt","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":5},{"mimetype":"text/plain","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a11.txt"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a11.txt?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a11.txt"}},"path":"a11.txt","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":5}],"page":1,"next":"https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/src/second-branch/?page=8xhd"}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 29 Aug 2019 12:37:04 GMT + Etag: + - '"gz[6f4112203f82108c9d3c956ea134613e]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Reads-Before-Write-From: + - default + X-Render-Time: + - '0.266921043396' + X-Request-Count: + - '80' + X-Served-By: + - app-144 + X-Static-Version: + - 2cc5a40f1b3e + X-Version: + - 2cc5a40f1b3e + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/src/second-branch/?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1567082223&oauth_nonce=a3cdc47a1ac6423e910ba2e2aeb203a4&oauth_version=1.0&oauth_signature=Rxci9EnohgZDdmquYNRnUZaQkPg%3D +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/src/second-branch/?page=8xhd&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1567082224&oauth_nonce=0abdb51a148a469187c215b8a045e4ba&oauth_version=1.0&oauth_signature=uwnKZsJsOYZniuIyxwY0NcJrbs8%3D + response: + content: '{"pagelen":10,"values":[{"mimetype":"text/plain","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a2.txt"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a2.txt?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a2.txt"}},"path":"a2.txt","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":5},{"mimetype":"text/plain","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a3.txt"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a3.txt?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a3.txt"}},"path":"a3.txt","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":5},{"mimetype":"text/plain","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a4.txt"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a4.txt?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a4.txt"}},"path":"a4.txt","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":5},{"mimetype":"text/plain","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a5.txt"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a5.txt?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a5.txt"}},"path":"a5.txt","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":5},{"mimetype":"text/plain","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a6.txt"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a6.txt?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a6.txt"}},"path":"a6.txt","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":5},{"mimetype":"text/plain","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a7.txt"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a7.txt?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a7.txt"}},"path":"a7.txt","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":5},{"mimetype":"text/plain","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a8.txt"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a8.txt?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a8.txt"}},"path":"a8.txt","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":5},{"mimetype":"text/plain","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a9.txt"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a9.txt?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/a9.txt"}},"path":"a9.txt","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":5},{"mimetype":null,"links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/bitbucket-pipelines.yml"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/bitbucket-pipelines.yml?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/bitbucket-pipelines.yml"}},"path":"bitbucket-pipelines.yml","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":536},{"mimetype":"text/xml","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/coverage.xml"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/coverage.xml?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/coverage.xml"}},"path":"coverage.xml","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":2047}],"page":2,"next":"https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/src/second-branch/?page=dbtR"}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 29 Aug 2019 12:37:05 GMT + Etag: + - '"gz[b85ea2c4f60742a229902a9abf9e6177]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Reads-Before-Write-From: + - default + X-Render-Time: + - '0.277836084366' + X-Request-Count: + - '220' + X-Served-By: + - app-142 + X-Static-Version: + - 2cc5a40f1b3e + X-Version: + - 2cc5a40f1b3e + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/src/second-branch/?page=8xhd&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1567082224&oauth_nonce=0abdb51a148a469187c215b8a045e4ba&oauth_version=1.0&oauth_signature=uwnKZsJsOYZniuIyxwY0NcJrbs8%3D +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/src/second-branch/?page=dbtR&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1567082225&oauth_nonce=d1eab422bfb640db9fb0f7f3b01bb895&oauth_version=1.0&oauth_signature=yEmhbfkzsV8EIcmqx8kDhp51zb0%3D + response: + content: '{"pagelen":10,"values":[{"mimetype":"text/x-python","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/filet2.py"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/filet2.py?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/filet2.py"}},"path":"filet2.py","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":14},{"mimetype":"text/plain","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/requirements.txt"},"meta":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/src/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/requirements.txt?format=meta"},"history":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/filehistory/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4/requirements.txt"}},"path":"requirements.txt","commit":{"type":"commit","hash":"76e9bb63b8196a5477cd5f8af52fc28f455b3ec4","links":{"self":{"href":"https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"},"html":{"href":"https://bitbucket.org/ThiagoCodecov/example-python/commits/76e9bb63b8196a5477cd5f8af52fc28f455b3ec4"}}},"attributes":[],"type":"commit_file","size":32}],"page":3}' + headers: + Accept-Ranges: + - bytes + Cache-Control: + - max-age=900 + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 29 Aug 2019 12:37:06 GMT + Etag: + - '"gz[690fa1f60f31b8227be600c3ad0f0e78]"' + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization, Accept-Encoding + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - caching + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Reads-Before-Write-From: + - default + X-Render-Time: + - '0.185860157013' + X-Request-Count: + - '332' + X-Served-By: + - app-162 + X-Static-Version: + - 2cc5a40f1b3e + X-Version: + - 2cc5a40f1b3e + status: + code: 200 + message: OK + status_code: 200 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/src/second-branch/?page=dbtR&oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1567082225&oauth_nonce=d1eab422bfb640db9fb0f7f3b01bb895&oauth_version=1.0&oauth_signature=yEmhbfkzsV8EIcmqx8kDhp51zb0%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_post_comment.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_post_comment.yaml new file mode 100644 index 0000000000..9ca9901d7d --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_post_comment.yaml @@ -0,0 +1,79 @@ +interactions: +- request: + body: '{"content": {"raw": "Hello world"}}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Default + method: POST + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1566631339&oauth_nonce=df6ee48d165a45758389a71ee8e71073&oauth_version=1.0&oauth_signature=%2F7%2BobvGwMOAp3HiYcYrxkRXIx8M%3D + response: + content: '{"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/114320127"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/pull-requests/1/_/diff#comment-114320127"}}, + "deleted": false, "pullrequest": {"type": "pullrequest", "id": 1, "links": {"self": + {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/pull-requests/1"}}, + "title": "Hahaa That is a PR"}, "content": {"raw": "Hello world", "markup": + "markdown", "html": "

    Hello world

    ", "type": "rendered"}, "created_on": + "2019-08-24T07:22:19.710114+00:00", "user": {"display_name": "Thiago Ramos", + "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", "links": {"self": {"href": + "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D"}, + "html": {"href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/"}, + "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png"}}, + "nickname": "thiago", "type": "user", "account_id": "5bce04c759d0e84f8c7555e9"}, + "updated_on": "2019-08-24T07:22:19.719805+00:00", "type": "pullrequest_comment", + "id": 114320127}' + headers: + Connection: + - close + Content-Type: + - application/json; charset=utf-8 + Date: + - Sat, 24 Aug 2019 07:22:19 GMT + Etag: + - '"gz[18e862913e8cea383d83bfaadb5d8482]"' + Location: + - https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/114320127 + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - pullrequest + X-Cache-Info: + - not cacheable; request wasn't a GET or HEAD + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Reads-Before-Write-From: + - default + X-Render-Time: + - '0.294704914093' + X-Request-Count: + - '59' + X-Served-By: + - app-146 + X-Static-Version: + - 4973dbe55bef + X-Version: + - 4973dbe55bef + status: + code: 201 + message: CREATED + status_code: 201 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1566631339&oauth_nonce=df6ee48d165a45758389a71ee8e71073&oauth_version=1.0&oauth_signature=%2F7%2BobvGwMOAp3HiYcYrxkRXIx8M%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_post_webhook.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_post_webhook.yaml new file mode 100644 index 0000000000..34be0512a6 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_post_webhook.yaml @@ -0,0 +1,71 @@ +interactions: +- request: + body: '{"description": "a", "active": true, "events": ["repo:push", "issue:created"], + "url": "http://requestbin.net/r/1ecyaj51"}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Default + method: POST + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/hooks?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541601947&oauth_nonce=98405099cd3342b78c37278e1fed4597&oauth_version=1.0&oauth_signature=HpuNydAXNpJ0uUjTpX%2FDsUqiEUI%3D + response: + content: '{"read_only": null, "description": "a", "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/hooks/%7B4742f092-8397-4677-8876-5e9a06f10f98%7D"}}, + "url": "http://requestbin.net/r/1ecyaj51", "created_at": "2018-11-07T14:45:47.900077Z", + "skip_cert_verification": false, "source": null, "history_enabled": false, "active": + true, "subject": {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "type": "webhook_subscription", + "events": ["issue:created", "repo:push"], "uuid": "{4742f092-8397-4677-8876-5e9a06f10f98}"}' + headers: + Connection: + - close + Content-Length: + - '937' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 07 Nov 2018 14:45:47 GMT + Etag: + - '"fba5aa7d407b28aa12eff544ee2c38fe"' + Location: + - https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/hooks/%7B4742f092-8397-4677-8876-5e9a06f10f98%7D + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + X-Accepted-Oauth-Scopes: + - webhook + X-Cache-Info: + - not cacheable; request wasn't a GET or HEAD + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.184465885162' + X-Request-Count: + - '295' + X-Served-By: + - app-143 + X-Static-Version: + - 1f6d684e3c29 + X-Version: + - 1f6d684e3c29 + status: + code: 201 + message: CREATED + status_code: 201 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/hooks?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541601947&oauth_nonce=98405099cd3342b78c37278e1fed4597&oauth_version=1.0&oauth_signature=HpuNydAXNpJ0uUjTpX%2FDsUqiEUI%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_set_commit_status.yaml b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_set_commit_status.yaml new file mode 100644 index 0000000000..b728f98102 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_set_commit_status.yaml @@ -0,0 +1,74 @@ +interactions: +- request: + body: state=SUCCESSFUL&key=codecov-context&name=Context+Coverage&url=https%3A%2F%2Flocalhost%3A50036%2Fgitlab%2Fcodecov%2Fci-repo%3Fref%3Dad798926730aad14aadf72281204bdb85734fe67&description=aaaaaaaaaa + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d53/statuses/build?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541600749&oauth_nonce=35d6156376c04962a22bd881dcb34677&oauth_version=1.0&oauth_signature=2xxB%2FTlEr319N%2Fr4HfezaS%2FZqfM%3D + response: + content: '{"key": "codecov-context", "description": "aaaaaaaaaa", "repository": + {"links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python"}, "avatar": + {"href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default"}}, + "type": "repository", "name": "example-python", "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}"}, "url": "https://localhost:50036/gitlab/codecov/ci-repo?ref=ad798926730aad14aadf72281204bdb85734fe67", + "links": {"commit": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b"}, + "self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b/statuses/build/codecov-context"}}, + "refname": null, "state": "SUCCESSFUL", "created_on": "2018-11-07T14:25:50.103547+00:00", + "commit": {"hash": "3017d534ab41e217bdf34d4c615fb355b0081f4b", "type": "commit", + "links": {"self": {"href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b"}, + "html": {"href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/3017d534ab41e217bdf34d4c615fb355b0081f4b"}}}, + "updated_on": "2018-11-07T14:25:50.103583+00:00", "type": "build", "name": "Context + Coverage"}' + headers: + Connection: + - close + Content-Type: + - application/json + Date: + - Wed, 07 Nov 2018 14:25:50 GMT + Etag: + - '"gz[14cfdd5f86c1dac3891c5ebf9a1e370e]"' + Location: + - /!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b/statuses/build/codecov-context + Server: + - nginx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Vary: + - Authorization + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - repository + X-Cache-Info: + - not cacheable; request wasn't a GET or HEAD + X-Consumed-Content-Encoding: + - gzip + X-Content-Type-Options: + - nosniff + X-Credential-Type: + - oauth1 + X-Frame-Options: + - SAMEORIGIN + X-Oauth-Scopes: + - pipeline:variable, webhook, snippet:write, wiki, issue:write, pullrequest:write, + repository:delete, repository:admin, project:write, team:write, account:write + X-Render-Time: + - '0.0698249340057' + X-Request-Count: + - '517' + X-Served-By: + - app-141 + X-Static-Version: + - 1f6d684e3c29 + X-Version: + - 1f6d684e3c29 + status: + code: 201 + message: Created + status_code: 201 + url: https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d53/statuses/build?oauth_consumer_key=arubajamaicaohiwan&oauth_token=testss3hxhcfqf1h6g&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1541600749&oauth_nonce=35d6156376c04962a22bd881dcb34677&oauth_version=1.0&oauth_signature=2xxB%2FTlEr319N%2Fr4HfezaS%2FZqfM%3D +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_bots/TestRepositoryServiceIntegration/test_get_token_type_mapping_bad_data.yaml b/libs/shared/tests/integration/cassetes/test_bots/TestRepositoryServiceIntegration/test_get_token_type_mapping_bad_data.yaml new file mode 100644 index 0000000000..1fca56452e --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_bots/TestRepositoryServiceIntegration/test_get_token_type_mapping_bad_data.yaml @@ -0,0 +1,59 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/vnd.github.machine-man-preview+json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - Codecov + method: POST + uri: https://api.github.com/app/installations/5944641/access_tokens + response: + body: + string: '{"message":"Integration must generate a public key","documentation_url":"https://docs.github.com/rest","status":"401"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Length: + - '118' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 04 Jul 2024 22:38:39 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - istio-envoy + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=machine-man-preview; format=json + X-GitHub-Request-Id: + - FC5A:F2894:10E2DBB:1E80523:6687246F + X-XSS-Protection: + - '0' + x-envoy-upstream-service-time: + - '23' + status: + code: 401 + message: Unauthorized +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_create_github_check.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_create_github_check.yaml new file mode 100644 index 0000000000..9c6b35463a --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_create_github_check.yaml @@ -0,0 +1,131 @@ +interactions: +- request: + body: '{"name": "Test check", "head_sha": "75f355d8d14ba3d7761c728b4d2607cde0eef065", + "status": "in_progress"}' + headers: + accept: + - application/vnd.github.antiope-preview+json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '103' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/check-runs + response: + content: "{\"id\":1256232357,\"node_id\":\"MDg6Q2hlY2tSdW4xMjU2MjMyMzU3\",\"head_sha\"\ + :\"75f355d8d14ba3d7761c728b4d2607cde0eef065\",\"external_id\":\"\",\"url\":\"\ + https://api.github.com/repos/ThiagoCodecov/example-python/check-runs/1256232357\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/runs/1256232357\"\ + ,\"details_url\":\"https://app.codecov.io/gh/codecov/example-python/compare/1?src=pr\",\"status\":\"in_progress\",\"conclusion\"\ + :null,\"started_at\":\"2020-10-14T23:00:59Z\",\"completed_at\":null,\"output\"\ + :{\"title\":null,\"summary\":null,\"text\":null,\"annotations_count\":0,\"annotations_url\"\ + :\"https://api.github.com/repos/ThiagoCodecov/example-python/check-runs/1256232357/annotations\"\ + },\"name\":\"Test check\",\"check_suite\":{\"id\":1341719124},\"app\":{\"id\"\ + :254,\"slug\":\"codecov\",\"node_id\":\"MDM6QXBwMjU0\",\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"name\":\"Codecov\",\"description\"\ + :\"Codecov provides highly integrated tools to group, merge, archive and compare\ + \ coverage reports. Whether your team is comparing changes in a pull request\ + \ or reviewing a single commit, Codecov will improve the code review workflow\ + \ and quality.\\r\\n\\r\\n## Code coverage done right.\xAE\\r\\n\\r\\n1. Upload\ + \ coverage reports from your CI builds.\\r\\n2. Codecov merges all builds and\ + \ languages into one beautiful coherent report.\\r\\n3. Get commit statuses,\ + \ pull request comments and coverage overlay via our browser extension.\\r\\\ + n\\r\\nWhen Codecov merges your uploads it keeps track of the CI provider (inc.\ + \ build details) and user specified context, e.g. `#unittest` ~ `#smoketest`\ + \ or `#oldcode` ~ `#newcode`. You can track the `#unittest` coverage independently\ + \ of other groups. [Learn more here](\\r\\nhttp://docs.codecov.io/docs/flags)\\\ + r\\n\\r\\nThrough **Codecov's Browser Extension** reports overlay directly in\ + \ GitHub UI to assist in code review. [Watch here](https://docs.codecov.io/docs/browser-extension)\\\ + r\\n\\r\\n*Highly detailed* **pull request comments** and *customizable* **commit\ + \ statuses** will improve your team's workflow and code coverage incrementally.\\\ + r\\n\\r\\n**File backed configuration** all through the `codecov.yml`. \\r\\\ + n\\r\\n## FAQ\\r\\n- Do you **merge multiple uploads** to the same commit? **Yes**\\\ + r\\n- Do you **support multiple languages** in the same project? **Yes**\\r\\\ + n- Can you **group coverage reports** by project and/or test type? **Yes**\\\ + r\\n- How does **pricing** work? Only paid users can view reports and post statuses/comments.\ + \ \",\"external_url\":\"https://codecov.io\",\"html_url\":\"https://github.com/apps/codecov\"\ + ,\"created_at\":\"2016-09-25T14:18:27Z\",\"updated_at\":\"2020-08-27T18:10:18Z\"\ + ,\"permissions\":{\"administration\":\"read\",\"checks\":\"write\",\"contents\"\ + :\"read\",\"issues\":\"read\",\"members\":\"read\",\"metadata\":\"read\",\"\ + pull_requests\":\"write\",\"statuses\":\"write\"},\"events\":[\"check_run\"\ + ,\"check_suite\",\"create\",\"delete\",\"fork\",\"membership\",\"public\",\"\ + pull_request\",\"push\",\"release\",\"repository\",\"status\",\"team_add\"]},\"\ + pull_requests\":[{\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18\"\ + ,\"id\":383348775,\"number\":18,\"head\":{\"ref\":\"thiago/base-no-base\",\"\ + sha\":\"75f355d8d14ba3d7761c728b4d2607cde0eef065\",\"repo\":{\"id\":156617777,\"\ + url\":\"https://api.github.com/repos/ThiagoCodecov/example-python\",\"name\"\ + :\"example-python\"}},\"base\":{\"ref\":\"main\",\"sha\":\"f0895290dc26668faeeb20ee5ccd4cc995925775\"\ + ,\"repo\":{\"id\":156617777,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python\"\ + ,\"name\":\"example-python\"}}}]}" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '4204' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 23:00:59 GMT + ETag: + - '"c835d3f1159c8047d7523a5b70e9e54a647efa5ba4ed29a08694bf1fde879ad1"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/check-runs/1256232357 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=antiope-preview; format=json + X-GitHub-Request-Id: + - DA59:5067:3B796:AF2FF:5F87832A + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4995' + X-RateLimit-Reset: + - '1602719974' + X-RateLimit-Used: + - '5' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 201 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_delete_comment.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_delete_comment.yaml new file mode 100644 index 0000000000..ddb29667c0 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_delete_comment.yaml @@ -0,0 +1,64 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: DELETE + uri: https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments/708545249 + response: + content: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Content-Security-Policy: + - default-src 'none' + Date: + - Wed, 14 Oct 2020 21:28:41 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 204 No Content + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D7B9:3A6C:7C51B:D82D8:5F876D88 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 204 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_delete_comment_not_found.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_delete_comment_not_found.yaml new file mode 100644 index 0000000000..b6c0371603 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_delete_comment_not_found.yaml @@ -0,0 +1,75 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: DELETE + uri: https://api.github.com/repos/codecove2e/example-python/issues/comments/113977999 + response: + content: '{"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#delete-an-issue-comment"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 18 Aug 2023 20:21:33 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E311:1DAF:190B798:1A7AF87:64DFD2CD + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4996' + X-RateLimit-Reset: + - '1692393273' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '4' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-08-25 20:13:04 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_delete_webhook.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_delete_webhook.yaml new file mode 100644 index 0000000000..3e14888c3a --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_delete_webhook.yaml @@ -0,0 +1,64 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: DELETE + uri: https://api.github.com/repos/ThiagoCodecov/example-python/hooks/255680134 + response: + content: '' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Content-Security-Policy: + - default-src 'none' + Date: + - Wed, 14 Oct 2020 21:52:34 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 204 No Content + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - admin:repo_hook, public_repo, repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D8B3:69B4:31F89FD:56ACFDC:5F877322 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4980' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '20' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 204 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_delete_webhook_not_found.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_delete_webhook_not_found.yaml new file mode 100644 index 0000000000..7e9666e997 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_delete_webhook_not_found.yaml @@ -0,0 +1,70 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: DELETE + uri: https://api.github.com/repos/ThiagoCodecov/example-python/hooks/4742f011-8397-aa77-8876-5e9a06f10f98 + response: + content: '{"message":"Not Found","documentation_url":"https://docs.github.com/rest/reference/repos#delete-a-repository-webhook"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:31 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 404 Not Found + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - admin:repo_hook, public_repo, repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFF4:5092:29EC73:7BF8C3:5F87362F + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4935' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '65' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_edit_comment.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_edit_comment.yaml new file mode 100644 index 0000000000..8be0aff6d9 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_edit_comment.yaml @@ -0,0 +1,80 @@ +interactions: +- request: + body: '{"body": "Hello world numbah 2 my friendo"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '43' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: PATCH + uri: https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments/436811257 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments/436811257","html_url":"https://github.com/ThiagoCodecov/example-python/pull/1#issuecomment-436811257","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1","id":436811257,"node_id":"MDEyOklzc3VlQ29tbWVudDQzNjgxMTI1Nw==","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"created_at":"2018-11-07T23:08:03Z","updated_at":"2020-10-14T17:32:03Z","author_association":"OWNER","body":"Hello + world numbah 2 my friendo","performed_via_github_app":null}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:03 GMT + ETag: + - W/"0dea4e1a5cc6ac3455e652b593f4e9dd99e85734fdaa14702e761ff12f4425e9" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFD1:394E:1E454E:34690B:5F873613 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4998' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '2' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_edit_comment_not_found.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_edit_comment_not_found.yaml new file mode 100644 index 0000000000..328d5c1f0e --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_edit_comment_not_found.yaml @@ -0,0 +1,79 @@ +interactions: +- request: + body: '{"body": "Hello world number 2"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '32' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: PATCH + uri: https://api.github.com/repos/codecove2e/example-python/issues/comments/113979999 + response: + content: '{"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#update-an-issue-comment"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 18 Aug 2023 20:21:33 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E310:1725:17B63EA:1925BB4:64DFD2CD + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4997' + X-RateLimit-Reset: + - '1692393273' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '3' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-08-25 20:13:04 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_edit_webhook.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_edit_webhook.yaml new file mode 100644 index 0000000000..18aaa66a36 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_edit_webhook.yaml @@ -0,0 +1,82 @@ +interactions: +- request: + body: '{"name": "web", "active": true, "events": ["project", "pull_request", "release"], + "config": {"url": "https://enfehm3qrtj5u.x.pipedream.net", "secret": "new_secret", + "content_type": "json"}}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '189' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: PATCH + uri: https://api.github.com/repos/ThiagoCodecov/example-python/hooks/255680134 + response: + content: '{"type":"Repository","id":255680134,"name":"web","active":true,"events":["project","pull_request","release"],"config":{"content_type":"json","secret":"********","url":"https://enfehm3qrtj5u.x.pipedream.net","insecure_ssl":"0"},"updated_at":"2020-10-14T21:51:05Z","created_at":"2020-10-14T17:32:29Z","url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks/255680134","test_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks/255680134/test","ping_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks/255680134/pings","last_response":{"code":404,"status":"missing","message":"Invalid + HTTP Response: 404"}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 21:51:05 GMT + ETag: + - W/"fff258ee5aba7d9654ce6c6849fbcad1ab78b08c07016d94102feeef64d0afd6" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - admin:repo_hook, public_repo, repo, write:repo_hook + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D8AB:3A6B:5FCC4:DA21F:5F8772C9 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4981' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '19' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_find_pull_request_nothing_found.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_find_pull_request_nothing_found.yaml new file mode 100644 index 0000000000..cfa9c66408 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_find_pull_request_nothing_found.yaml @@ -0,0 +1,145 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecove2e/example-python/commits/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pulls + response: + content: '{"message":"No commit found for SHA: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","documentation_url":"https://docs.github.com/rest/reference/repos#list-pull-requests-associated-with-a-commit"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Length: + - '190' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 18 Jul 2022 18:49:04 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FA23:3A7F:808CF7:9752AB:62D5AB1F + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4996' + X-RateLimit-Reset: + - '1658172388' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '4' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-07-25 18:16:08 UTC + http_version: HTTP/1.1 + status_code: 422 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/search/issues?q=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+repo%3Acodecove2e%2Fexample-python+type%3Apr+state%3Aopen + response: + content: '{"total_count":0,"incomplete_results":false,"items":[]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - no-cache + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 18 Jul 2022 18:49:04 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FA24:0CD6:11294B:16B7BA:62D5AB20 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '30' + X-RateLimit-Remaining: + - '29' + X-RateLimit-Reset: + - '1658170204' + X-RateLimit-Resource: + - search + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-07-25 18:16:08 UTC + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_find_pull_request_one_found.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_find_pull_request_one_found.yaml new file mode 100644 index 0000000000..ca90318c09 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_find_pull_request_one_found.yaml @@ -0,0 +1,81 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecove2e/example-python/commits/bec77909e0a9b04603d2cdddcba62f99592d6579/pulls + response: + content: '[{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1","id":999745749,"node_id":"PR_kwDOHrbKcs47lujV","html_url":"https://github.com/codecove2e/example-python/pull/1","diff_url":"https://github.com/codecove2e/example-python/pull/1.diff","patch_url":"https://github.com/codecove2e/example-python/pull/1.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1","number":1,"state":"open","locked":false,"title":"Create + README.md","user":{"login":"codecove2e","id":93560619,"node_id":"U_kgDOBZOfKw","avatar_url":"https://avatars.githubusercontent.com/u/93560619?v=4","gravatar_id":"","url":"https://api.github.com/users/codecove2e","html_url":"https://github.com/codecove2e","followers_url":"https://api.github.com/users/codecove2e/followers","following_url":"https://api.github.com/users/codecove2e/following{/other_user}","gists_url":"https://api.github.com/users/codecove2e/gists{/gist_id}","starred_url":"https://api.github.com/users/codecove2e/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecove2e/subscriptions","organizations_url":"https://api.github.com/users/codecove2e/orgs","repos_url":"https://api.github.com/users/codecove2e/repos","events_url":"https://api.github.com/users/codecove2e/events{/privacy}","received_events_url":"https://api.github.com/users/codecove2e/received_events","type":"User","site_admin":false},"body":null,"created_at":"2022-07-18T18:24:01Z","updated_at":"2022-07-18T18:24:01Z","closed_at":null,"merged_at":null,"merge_commit_sha":"f5111b5bbd8afa7911075009d3d6385e175d3494","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/bec77909e0a9b04603d2cdddcba62f99592d6579","head":{"label":"codecove2e:test-branch","ref":"test-branch","sha":"bec77909e0a9b04603d2cdddcba62f99592d6579","user":{"login":"codecove2e","id":93560619,"node_id":"U_kgDOBZOfKw","avatar_url":"https://avatars.githubusercontent.com/u/93560619?v=4","gravatar_id":"","url":"https://api.github.com/users/codecove2e","html_url":"https://github.com/codecove2e","followers_url":"https://api.github.com/users/codecove2e/followers","following_url":"https://api.github.com/users/codecove2e/following{/other_user}","gists_url":"https://api.github.com/users/codecove2e/gists{/gist_id}","starred_url":"https://api.github.com/users/codecove2e/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecove2e/subscriptions","organizations_url":"https://api.github.com/users/codecove2e/orgs","repos_url":"https://api.github.com/users/codecove2e/repos","events_url":"https://api.github.com/users/codecove2e/events{/privacy}","received_events_url":"https://api.github.com/users/codecove2e/received_events","type":"User","site_admin":false},"repo":{"id":515295858,"node_id":"R_kgDOHrbKcg","name":"example-python","full_name":"codecove2e/example-python","private":false,"owner":{"login":"codecove2e","id":93560619,"node_id":"U_kgDOBZOfKw","avatar_url":"https://avatars.githubusercontent.com/u/93560619?v=4","gravatar_id":"","url":"https://api.github.com/users/codecove2e","html_url":"https://github.com/codecove2e","followers_url":"https://api.github.com/users/codecove2e/followers","following_url":"https://api.github.com/users/codecove2e/following{/other_user}","gists_url":"https://api.github.com/users/codecove2e/gists{/gist_id}","starred_url":"https://api.github.com/users/codecove2e/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecove2e/subscriptions","organizations_url":"https://api.github.com/users/codecove2e/orgs","repos_url":"https://api.github.com/users/codecove2e/repos","events_url":"https://api.github.com/users/codecove2e/events{/privacy}","received_events_url":"https://api.github.com/users/codecove2e/received_events","type":"User","site_admin":false},"html_url":"https://github.com/codecove2e/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/codecove2e/example-python","forks_url":"https://api.github.com/repos/codecove2e/example-python/forks","keys_url":"https://api.github.com/repos/codecove2e/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecove2e/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecove2e/example-python/teams","hooks_url":"https://api.github.com/repos/codecove2e/example-python/hooks","issue_events_url":"https://api.github.com/repos/codecove2e/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/codecove2e/example-python/events","assignees_url":"https://api.github.com/repos/codecove2e/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/codecove2e/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/codecove2e/example-python/tags","blobs_url":"https://api.github.com/repos/codecove2e/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecove2e/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecove2e/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecove2e/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecove2e/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/codecove2e/example-python/languages","stargazers_url":"https://api.github.com/repos/codecove2e/example-python/stargazers","contributors_url":"https://api.github.com/repos/codecove2e/example-python/contributors","subscribers_url":"https://api.github.com/repos/codecove2e/example-python/subscribers","subscription_url":"https://api.github.com/repos/codecove2e/example-python/subscription","commits_url":"https://api.github.com/repos/codecove2e/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecove2e/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecove2e/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecove2e/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecove2e/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/codecove2e/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecove2e/example-python/merges","archive_url":"https://api.github.com/repos/codecove2e/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecove2e/example-python/downloads","issues_url":"https://api.github.com/repos/codecove2e/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/codecove2e/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/codecove2e/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/codecove2e/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecove2e/example-python/labels{/name}","releases_url":"https://api.github.com/repos/codecove2e/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/codecove2e/example-python/deployments","created_at":"2022-07-18T18:21:28Z","updated_at":"2020-12-04T04:21:29Z","pushed_at":"2022-07-18T18:24:02Z","git_url":"git://github.com/codecove2e/example-python.git","ssh_url":"git@github.com:codecove2e/example-python.git","clone_url":"https://github.com/codecove2e/example-python.git","svn_url":"https://github.com/codecove2e/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main"}},"base":{"label":"codecove2e:main","ref":"main","sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","user":{"login":"codecove2e","id":93560619,"node_id":"U_kgDOBZOfKw","avatar_url":"https://avatars.githubusercontent.com/u/93560619?v=4","gravatar_id":"","url":"https://api.github.com/users/codecove2e","html_url":"https://github.com/codecove2e","followers_url":"https://api.github.com/users/codecove2e/followers","following_url":"https://api.github.com/users/codecove2e/following{/other_user}","gists_url":"https://api.github.com/users/codecove2e/gists{/gist_id}","starred_url":"https://api.github.com/users/codecove2e/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecove2e/subscriptions","organizations_url":"https://api.github.com/users/codecove2e/orgs","repos_url":"https://api.github.com/users/codecove2e/repos","events_url":"https://api.github.com/users/codecove2e/events{/privacy}","received_events_url":"https://api.github.com/users/codecove2e/received_events","type":"User","site_admin":false},"repo":{"id":515295858,"node_id":"R_kgDOHrbKcg","name":"example-python","full_name":"codecove2e/example-python","private":false,"owner":{"login":"codecove2e","id":93560619,"node_id":"U_kgDOBZOfKw","avatar_url":"https://avatars.githubusercontent.com/u/93560619?v=4","gravatar_id":"","url":"https://api.github.com/users/codecove2e","html_url":"https://github.com/codecove2e","followers_url":"https://api.github.com/users/codecove2e/followers","following_url":"https://api.github.com/users/codecove2e/following{/other_user}","gists_url":"https://api.github.com/users/codecove2e/gists{/gist_id}","starred_url":"https://api.github.com/users/codecove2e/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecove2e/subscriptions","organizations_url":"https://api.github.com/users/codecove2e/orgs","repos_url":"https://api.github.com/users/codecove2e/repos","events_url":"https://api.github.com/users/codecove2e/events{/privacy}","received_events_url":"https://api.github.com/users/codecove2e/received_events","type":"User","site_admin":false},"html_url":"https://github.com/codecove2e/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/codecove2e/example-python","forks_url":"https://api.github.com/repos/codecove2e/example-python/forks","keys_url":"https://api.github.com/repos/codecove2e/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecove2e/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecove2e/example-python/teams","hooks_url":"https://api.github.com/repos/codecove2e/example-python/hooks","issue_events_url":"https://api.github.com/repos/codecove2e/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/codecove2e/example-python/events","assignees_url":"https://api.github.com/repos/codecove2e/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/codecove2e/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/codecove2e/example-python/tags","blobs_url":"https://api.github.com/repos/codecove2e/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecove2e/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecove2e/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecove2e/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecove2e/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/codecove2e/example-python/languages","stargazers_url":"https://api.github.com/repos/codecove2e/example-python/stargazers","contributors_url":"https://api.github.com/repos/codecove2e/example-python/contributors","subscribers_url":"https://api.github.com/repos/codecove2e/example-python/subscribers","subscription_url":"https://api.github.com/repos/codecove2e/example-python/subscription","commits_url":"https://api.github.com/repos/codecove2e/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecove2e/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecove2e/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecove2e/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecove2e/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/codecove2e/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecove2e/example-python/merges","archive_url":"https://api.github.com/repos/codecove2e/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecove2e/example-python/downloads","issues_url":"https://api.github.com/repos/codecove2e/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/codecove2e/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/codecove2e/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/codecove2e/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecove2e/example-python/labels{/name}","releases_url":"https://api.github.com/repos/codecove2e/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/codecove2e/example-python/deployments","created_at":"2022-07-18T18:21:28Z","updated_at":"2020-12-04T04:21:29Z","pushed_at":"2022-07-18T18:24:02Z","git_url":"git://github.com/codecove2e/example-python.git","ssh_url":"git@github.com:codecove2e/example-python.git","clone_url":"https://github.com/codecove2e/example-python.git","svn_url":"https://github.com/codecove2e/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1"},"html":{"href":"https://github.com/codecove2e/example-python/pull/1"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/bec77909e0a9b04603d2cdddcba62f99592d6579"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 18 Jul 2022 18:31:44 GMT + ETag: + - W/"159c07e1e5d140391ea2b9f214adf022777ac29a1aec6c821eea0bdde3892f46" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - F91B:68B6:4681CD:53DB0A:62D5A710 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4997' + X-RateLimit-Reset: + - '1658172388' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '3' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2022-07-25 18:16:08 UTC + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_ancestors_tree.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_ancestors_tree.yaml new file mode 100644 index 0000000000..d3e9cd325b --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_ancestors_tree.yaml @@ -0,0 +1,979 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits?sha=6ae5f17 + response: + content: "[{\"sha\":\"6ae5f1795a441884ed2847bb31154814ac01ef38\",\"node_id\":\"\ + MDY6Q29tbWl0MTU2NjE3Nzc3OjZhZTVmMTc5NWE0NDE4ODRlZDI4NDdiYjMxMTU0ODE0YWMwMWVmMzg=\"\ + ,\"commit\":{\"author\":{\"name\":\"Thomas Pedbereznak\",\"email\":\"tom@tomped.com\"\ + ,\"date\":\"2018-04-26T08:35:58Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2018-04-26T08:35:58Z\"},\"message\":\"Update\ + \ README.rst\",\"tree\":{\"sha\":\"b5592410a15d7a596a8eaea6399766fbbbe0366c\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b5592410a15d7a596a8eaea6399766fbbbe0366c\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6ae5f1795a441884ed2847bb31154814ac01ef38\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\"\ + ,\"signature\":\"-----BEGIN PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJa4Y9uCRBK7hj4Ov3rIwAAdHIIAAyJAC1mOPnKmkDSzraV47Wq\\\ + nXma/2QidKpXnRqKgY6XFBXAH0RIpHpZ3NwGR/L2GH1l7xLjXtOMTvXOCjFBZUwRE\\nLlM9IdoUFyPU2E9P0z0vfGR/nk5QC8PY9lzDwe/N8ZhR0j4M2rTM2ue97om9nJ4e\\\ + nmD+HR2ZwjKA9Z9zFeALgBjokKs44F6oN6lLuPYn06oiCnYB3ytlWJy+vpmEGLhoM\\nL+a/ct2e6O5MmlpbRlKVME4FL0O4wDBMrAaFeeZgQTCl2LKfdsfYScJnypkB7X06\\\ + n6cDtC/TJ436n4PCTBRHVMDNGxzmgMgMFYbCPkJ27BeWlTuKVDcJ2msOV7ZJKqqs=\\n=oGiR\\\ + n-----END PGP SIGNATURE-----\\n\",\"payload\":\"tree b5592410a15d7a596a8eaea6399766fbbbe0366c\\\ + nparent 8631ea09b9b689de0a348d5abf70bdd7273d2ae3\\nauthor Thomas Pedbereznak\ + \ 1524731758 +0200\\ncommitter GitHub \ + \ 1524731758 +0200\\n\\nUpdate README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38/comments\"\ + ,\"author\":{\"login\":\"TomPed\",\"id\":11602092,\"node_id\":\"MDQ6VXNlcjExNjAyMDky\"\ + ,\"avatar_url\":\"https://avatars1.githubusercontent.com/u/11602092?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/TomPed\",\"html_url\"\ + :\"https://github.com/TomPed\",\"followers_url\":\"https://api.github.com/users/TomPed/followers\"\ + ,\"following_url\":\"https://api.github.com/users/TomPed/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/TomPed/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/TomPed/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/TomPed/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/TomPed/orgs\",\"repos_url\":\"https://api.github.com/users/TomPed/repos\"\ + ,\"events_url\":\"https://api.github.com/users/TomPed/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/TomPed/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\"\ + ,\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"\ + }]},{\"sha\":\"8631ea09b9b689de0a348d5abf70bdd7273d2ae3\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg2MzFlYTA5YjliNjg5ZGUwYTM0OGQ1YWJmNzBiZGQ3MjczZDJhZTM=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2018-02-13T09:13:36Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2018-02-13T09:13:36Z\"},\"message\":\"Merge\ + \ pull request #31 from Gabswim/fix/typo\\n\\nfixing a typo in the README\"\ + ,\"tree\":{\"sha\":\"e08452bb815b0a8039d5c326e126798aeaf8898f\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e08452bb815b0a8039d5c326e126798aeaf8898f\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\"\ + ,\"signature\":\"-----BEGIN PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJagqxACRBK7hj4Ov3rIwAAdHIIAHiUfENfdOLuNZ/2IvhBe6oJ\\\ + nkkRIlb0iVOaYEtRm7zaEdj8Jt08IFL2C/jztCnE0Osx1r0K/qGXd2gVmBX6mBlda\\n+NSIdLBOdtTmdJQv/zN9ddht4uGakPOHsHTrodFaMN/nWRn9pDUBu1kQNK664zWo\\\ + nSOBwmDU/zMPDUOpYoC2dmorA1Xze0aaKaA11zDO42jqfyHWmlqYoa6Eaf+TYC4Or\\nT8vjYyIJ6TC27XWWvqW1Rk/lZYGbx6QneIT5XLBmyHBCYt+RAYVB09mrrTYBqwVI\\\ + nY7ZqJubRfkzWwAif6vS1jb1U7iisP1oQep+9j6p8RsViVx6s8qGIKea4HGjxm/8=\\n=7ECB\\\ + n-----END PGP SIGNATURE-----\\n\",\"payload\":\"tree e08452bb815b0a8039d5c326e126798aeaf8898f\\\ + nparent 48f47f9d1b58ba418fdcd50117fc9781c10a27fb\\nparent 087ede6771099a66dccb968c8aacfa04e9ba27a8\\\ + nauthor Steve Peak 1518513216 +0100\\ncommitter GitHub \ + \ 1518513216 +0100\\n\\nMerge pull request #31 from Gabswim/fix/typo\\n\\nfixing\ + \ a typo in the README\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + },{\"sha\":\"087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/087ede6771099a66dccb968c8aacfa04e9ba27a8\"\ + }]},{\"sha\":\"087ede6771099a66dccb968c8aacfa04e9ba27a8\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjA4N2VkZTY3NzEwOTlhNjZkY2NiOTY4YzhhYWNmYTA0ZTliYTI3YTg=\"\ + ,\"commit\":{\"author\":{\"name\":\"Gabriel Legault\",\"email\":\"gablegault1@hotmail.com\"\ + ,\"date\":\"2018-02-13T01:06:57Z\"},\"committer\":{\"name\":\"Gabriel Legault\"\ + ,\"email\":\"gablegault1@hotmail.com\",\"date\":\"2018-02-13T01:06:57Z\"},\"\ + message\":\"fixing a typo in the README\",\"tree\":{\"sha\":\"e08452bb815b0a8039d5c326e126798aeaf8898f\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e08452bb815b0a8039d5c326e126798aeaf8898f\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/087ede6771099a66dccb968c8aacfa04e9ba27a8\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/087ede6771099a66dccb968c8aacfa04e9ba27a8/comments\"\ + ,\"author\":{\"login\":\"Gabswim\",\"id\":2859712,\"node_id\":\"MDQ6VXNlcjI4NTk3MTI=\"\ + ,\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2859712?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/Gabswim\",\"html_url\"\ + :\"https://github.com/Gabswim\",\"followers_url\":\"https://api.github.com/users/Gabswim/followers\"\ + ,\"following_url\":\"https://api.github.com/users/Gabswim/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/Gabswim/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/Gabswim/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/Gabswim/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/Gabswim/orgs\",\"repos_url\":\"https://api.github.com/users/Gabswim/repos\"\ + ,\"events_url\":\"https://api.github.com/users/Gabswim/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/Gabswim/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"Gabswim\"\ + ,\"id\":2859712,\"node_id\":\"MDQ6VXNlcjI4NTk3MTI=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/2859712?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/Gabswim\",\"html_url\"\ + :\"https://github.com/Gabswim\",\"followers_url\":\"https://api.github.com/users/Gabswim/followers\"\ + ,\"following_url\":\"https://api.github.com/users/Gabswim/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/Gabswim/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/Gabswim/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/Gabswim/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/Gabswim/orgs\",\"repos_url\":\"https://api.github.com/users/Gabswim/repos\"\ + ,\"events_url\":\"https://api.github.com/users/Gabswim/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/Gabswim/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + }]},{\"sha\":\"48f47f9d1b58ba418fdcd50117fc9781c10a27fb\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjQ4ZjQ3ZjlkMWI1OGJhNDE4ZmRjZDUwMTE3ZmM5NzgxYzEwYTI3ZmI=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2018-01-30T11:57:49Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2018-01-30T11:57:49Z\"},\"message\":\"Merge\ + \ pull request #30 from Jay54520/main\\n\\n#29/Pytest doc error\",\"tree\"\ + :{\"sha\":\"3cdd37198ec70196f94aaae638599c10b95be8a0\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3cdd37198ec70196f94aaae638599c10b95be8a0\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\"\ + ,\"signature\":\"-----BEGIN PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJacF29CRBK7hj4Ov3rIwAAdHIIABlpiW1RCdcVH3F1mUGSPHBa\\\ + n6htdwCoISAHOoiJpYXHv6C3ad0eBX6NCsyhcKw0IFGotqvoMvJo6/vS8cJnvdrkj\\nKhOr478QT2M50XYx+izaey55ckSMG2VFU/0rlnoGgnLsqQ5+tLt8xKU5PjCnYleF\\\ + nSK7l5D8pb2vvMesGDHHrESaHup//flHCLvYCJsCVslhVU4+iAE7xx/s0ln8gVg9K\\n0r+2IKjsleoHxjiHWibWOqaDH6z/WUIE7RrO7JsitFg7/4aX4/JXIzDp+EI8wU8a\\\ + npqvjtl+2Qj5hgr5qa6Wj5Qi4vh+wwhNR/Ujv6eK0hFZwMv5wMQ656eSRTeQH//U=\\n=V6j3\\\ + n-----END PGP SIGNATURE-----\\n\",\"payload\":\"tree 3cdd37198ec70196f94aaae638599c10b95be8a0\\\ + nparent 76003ff147414ce80d2a14ab5f1b78d165e9a468\\nparent d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\\\ + nauthor Steve Peak 1517313469 +0100\\ncommitter GitHub \ + \ 1517313469 +0100\\n\\nMerge pull request #30 from Jay54520/main\\n\\n#29/Pytest\ + \ doc error\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/48f47f9d1b58ba418fdcd50117fc9781c10a27fb\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/48f47f9d1b58ba418fdcd50117fc9781c10a27fb/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + },{\"sha\":\"d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"\ + }]},{\"sha\":\"d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmQyY2QyOGQ1M2I3MmQ5OTllOWExOGQ2NjMxOWRmZTRkOWI3MTU1Yzc=\"\ + ,\"commit\":{\"author\":{\"name\":\"\u63ED\u601D\u654F\",\"email\":\"jsm0834@175game.com\"\ + ,\"date\":\"2018-01-28T07:12:57Z\"},\"committer\":{\"name\":\"\u63ED\u601D\u654F\ + \",\"email\":\"jsm0834@175game.com\",\"date\":\"2018-01-28T07:12:57Z\"},\"message\"\ + :\"#29/Pytest doc error\",\"tree\":{\"sha\":\"3cdd37198ec70196f94aaae638599c10b95be8a0\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3cdd37198ec70196f94aaae638599c10b95be8a0\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2cd28d53b72d999e9a18d66319dfe4d9b7155c7/comments\"\ + ,\"author\":{\"login\":\"Jay54520\",\"id\":13315364,\"node_id\":\"MDQ6VXNlcjEzMzE1MzY0\"\ + ,\"avatar_url\":\"https://avatars2.githubusercontent.com/u/13315364?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/Jay54520\",\"html_url\"\ + :\"https://github.com/Jay54520\",\"followers_url\":\"https://api.github.com/users/Jay54520/followers\"\ + ,\"following_url\":\"https://api.github.com/users/Jay54520/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/Jay54520/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/Jay54520/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/Jay54520/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/Jay54520/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/Jay54520/repos\",\"events_url\":\"https://api.github.com/users/Jay54520/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/Jay54520/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"Jay54520\"\ + ,\"id\":13315364,\"node_id\":\"MDQ6VXNlcjEzMzE1MzY0\",\"avatar_url\":\"https://avatars2.githubusercontent.com/u/13315364?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/Jay54520\",\"html_url\"\ + :\"https://github.com/Jay54520\",\"followers_url\":\"https://api.github.com/users/Jay54520/followers\"\ + ,\"following_url\":\"https://api.github.com/users/Jay54520/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/Jay54520/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/Jay54520/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/Jay54520/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/Jay54520/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/Jay54520/repos\",\"events_url\":\"https://api.github.com/users/Jay54520/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/Jay54520/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + }]},{\"sha\":\"76003ff147414ce80d2a14ab5f1b78d165e9a468\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojc2MDAzZmYxNDc0MTRjZTgwZDJhMTRhYjVmMWI3OGQxNjVlOWE0Njg=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2018-01-22T13:40:28Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2018-01-22T13:40:28Z\"},\"message\":\"Update\ + \ README.rst\",\"tree\":{\"sha\":\"eac434ca240c7045eaf41e16b70a56e256467621\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/eac434ca240c7045eaf41e16b70a56e256467621\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\"\ + ,\"signature\":\"-----BEGIN PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJaZenMCRBK7hj4Ov3rIwAAdHIIAEKcjkLuqsx9HOj8kr7APqrJ\\\ + nOm2KpXC0WCvn4QaqDzA2hNDoE7rwYvS+MEV308/39AKAg9GBv9T1GwzyeU6krnBn\\nVWfo6Ee3r+/H61GOEZmqHWoQ140LtvC0Z6wCG5xNTfmrr3eUizqPPi9ePnyTeoHQ\\\ + nBvNEvI3FCqmudfBAWPfCWp4zDKp25siRLJ+jCEV3FZ8OmZ8kE5EvPspZDUFgCTzs\\nNaGYfpPOZoxSPrC3Th9ujHCEEontzTzTDHCBsTJLoZeoWrM25R4kcaBoHzggHHBn\\\ + nvMLwN+kwoq4bdbIhTxoeFqc9eVPtVtG9D7jv0+oheBiu5nKkx44SinVBHpXgQ5w=\\n=yptQ\\\ + n-----END PGP SIGNATURE-----\\n\",\"payload\":\"tree eac434ca240c7045eaf41e16b70a56e256467621\\\ + nparent f9253b0bf56d0e808ab01fdcc70412ac010f5c34\\nauthor Steve Peak \ + \ 1516628428 +0100\\ncommitter GitHub 1516628428 +0100\\\ + n\\nUpdate README.rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/76003ff147414ce80d2a14ab5f1b78d165e9a468\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76003ff147414ce80d2a14ab5f1b78d165e9a468/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + }]},{\"sha\":\"f9253b0bf56d0e808ab01fdcc70412ac010f5c34\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmY5MjUzYjBiZjU2ZDBlODA4YWIwMWZkY2M3MDQxMmFjMDEwZjVjMzQ=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2017-12-11T10:13:41Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2017-12-11T10:13:41Z\"},\"message\":\"Merge\ + \ pull request #24 from gundalow/README_md_to_rst\\n\\nReadme md to rst\",\"\ + tree\":{\"sha\":\"f80cce2672a08f03dbcd902468341e4c71727a2c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f80cce2672a08f03dbcd902468341e4c71727a2c\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":true,\"reason\":\"valid\"\ + ,\"signature\":\"-----BEGIN PGP SIGNATURE-----\\n\\nwsBcBAABCAAQBQJaLlpVCRBK7hj4Ov3rIwAAdHIIAKPJ0bOZe6GVHVPSbpxQctA9\\\ + nCX3gxTFNxaWR9UW0ikeIYLPQBrvzyTYx0HDIErnSfktBFl02cGKm020SvU2qBWFs\\nwtOWnvxVILMH1xV30Q8n/pqLUomaQkSCu9LO/0w1QDF0BgBEP1F5dJb6lzU5uL7t\\\ + nA4FllQ+UKpebhnI3PBCLK6pEUUN/2S6x76pXRvK6j56c+mHfeuOm66R663ZuTnDa\\neYis+Y4R4AYVMrZ12FCkGRly1BVZGgNtrmYQw0pG3DVplp8k9vjw7WaUbxaPsYFj\\\ + nQ33FRzKIqOdjiXH5AdSX1NIsuJo1Fq459i/utW4rfT5EEDuK8V2ZHe4qUzggjmA=\\n=F9Iq\\\ + n-----END PGP SIGNATURE-----\\n\",\"payload\":\"tree f80cce2672a08f03dbcd902468341e4c71727a2c\\\ + nparent 1e906160c09128765a75afbcbd60d1cbd3c8d10a\\nparent 2903ade6074f09319c1854850ffee2c254c3e17c\\\ + nauthor Steve Peak 1512987221 +0100\\ncommitter GitHub \ + \ 1512987221 +0100\\n\\nMerge pull request #24 from gundalow/README_md_to_rst\\\ + n\\nReadme md to rst\"}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/f9253b0bf56d0e808ab01fdcc70412ac010f5c34\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9253b0bf56d0e808ab01fdcc70412ac010f5c34/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + },{\"sha\":\"2903ade6074f09319c1854850ffee2c254c3e17c\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2903ade6074f09319c1854850ffee2c254c3e17c\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2903ade6074f09319c1854850ffee2c254c3e17c\"\ + }]},{\"sha\":\"2903ade6074f09319c1854850ffee2c254c3e17c\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjI5MDNhZGU2MDc0ZjA5MzE5YzE4NTQ4NTBmZmVlMmMyNTRjM2UxN2M=\"\ + ,\"commit\":{\"author\":{\"name\":\"John Barker\",\"email\":\"john@johnrbarker.com\"\ + ,\"date\":\"2017-12-09T13:35:07Z\"},\"committer\":{\"name\":\"John Barker\"\ + ,\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:35:07Z\"},\"message\"\ + :\"typo\",\"tree\":{\"sha\":\"f80cce2672a08f03dbcd902468341e4c71727a2c\",\"\ + url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f80cce2672a08f03dbcd902468341e4c71727a2c\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2903ade6074f09319c1854850ffee2c254c3e17c\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2903ade6074f09319c1854850ffee2c254c3e17c\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2903ade6074f09319c1854850ffee2c254c3e17c\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2903ade6074f09319c1854850ffee2c254c3e17c/comments\"\ + ,\"author\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\"\ + ,\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\"\ + :\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\"\ + ,\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\"\ + :\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\"\ + ,\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/gundalow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"gundalow\"\ + ,\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\"\ + :\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + }]},{\"sha\":\"0073e9e074081bc2588b9ee311fc01bc9adfa967\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjAwNzNlOWUwNzQwODFiYzI1ODhiOWVlMzExZmMwMWJjOWFkZmE5Njc=\"\ + ,\"commit\":{\"author\":{\"name\":\"John Barker\",\"email\":\"john@johnrbarker.com\"\ + ,\"date\":\"2017-12-09T13:33:54Z\"},\"committer\":{\"name\":\"John Barker\"\ + ,\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:33:54Z\"},\"message\"\ + :\"Valid badge\",\"tree\":{\"sha\":\"c6dad62f00e288976a31e2c6c189f18fc23881e4\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c6dad62f00e288976a31e2c6c189f18fc23881e4\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/0073e9e074081bc2588b9ee311fc01bc9adfa967\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0073e9e074081bc2588b9ee311fc01bc9adfa967/comments\"\ + ,\"author\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\"\ + ,\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\"\ + :\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\"\ + ,\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\"\ + :\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\"\ + ,\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/gundalow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"gundalow\"\ + ,\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\"\ + :\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"8d437d531af955c068c03f35a1f6f19667c6d215\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d437d531af955c068c03f35a1f6f19667c6d215\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8d437d531af955c068c03f35a1f6f19667c6d215\"\ + }]},{\"sha\":\"8d437d531af955c068c03f35a1f6f19667c6d215\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjhkNDM3ZDUzMWFmOTU1YzA2OGMwM2YzNWExZjZmMTk2NjdjNmQyMTU=\"\ + ,\"commit\":{\"author\":{\"name\":\"John Barker\",\"email\":\"john@johnrbarker.com\"\ + ,\"date\":\"2017-12-09T13:28:02Z\"},\"committer\":{\"name\":\"John Barker\"\ + ,\"email\":\"john@johnrbarker.com\",\"date\":\"2017-12-09T13:28:02Z\"},\"message\"\ + :\"Convert README.md to RST\\n\\n* Update formatting fixes #3\\n* Add example\ + \ badge (and how to use)\\n* Make it cleared that bash uploader should be used\"\ + ,\"tree\":{\"sha\":\"3bdc420145fb2cc4232a0ba454675b84311c4bc4\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3bdc420145fb2cc4232a0ba454675b84311c4bc4\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8d437d531af955c068c03f35a1f6f19667c6d215\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d437d531af955c068c03f35a1f6f19667c6d215\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8d437d531af955c068c03f35a1f6f19667c6d215\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d437d531af955c068c03f35a1f6f19667c6d215/comments\"\ + ,\"author\":{\"login\":\"gundalow\",\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\"\ + ,\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\",\"gravatar_id\"\ + :\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\":\"https://github.com/gundalow\"\ + ,\"followers_url\":\"https://api.github.com/users/gundalow/followers\",\"following_url\"\ + :\"https://api.github.com/users/gundalow/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/gundalow/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/gundalow/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/gundalow/orgs\",\"repos_url\":\"https://api.github.com/users/gundalow/repos\"\ + ,\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/gundalow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"gundalow\"\ + ,\"id\":940557,\"node_id\":\"MDQ6VXNlcjk0MDU1Nw==\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/940557?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/gundalow\",\"html_url\"\ + :\"https://github.com/gundalow\",\"followers_url\":\"https://api.github.com/users/gundalow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/gundalow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/gundalow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/gundalow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/gundalow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/gundalow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/gundalow/repos\",\"events_url\":\"https://api.github.com/users/gundalow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/gundalow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + }]},{\"sha\":\"1e906160c09128765a75afbcbd60d1cbd3c8d10a\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjFlOTA2MTYwYzA5MTI4NzY1YTc1YWZiY2JkNjBkMWNiZDNjOGQxMGE=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2017-10-04T09:43:08Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2017-10-04T09:43:08Z\"},\"message\":\"Merge\ + \ pull request #22 from IanLee1521/patch-1\\n\\nFixed minor typo\",\"tree\"\ + :{\"sha\":\"4b1780a3cac3fcfe3356b1da70346f19f63106b7\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4b1780a3cac3fcfe3356b1da70346f19f63106b7\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/1e906160c09128765a75afbcbd60d1cbd3c8d10a\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e906160c09128765a75afbcbd60d1cbd3c8d10a/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + },{\"sha\":\"b066c93c2676bc957d971d2c4188e77b3e383b77\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b066c93c2676bc957d971d2c4188e77b3e383b77\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b066c93c2676bc957d971d2c4188e77b3e383b77\"\ + }]},{\"sha\":\"b066c93c2676bc957d971d2c4188e77b3e383b77\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmIwNjZjOTNjMjY3NmJjOTU3ZDk3MWQyYzQxODhlNzdiM2UzODNiNzc=\"\ + ,\"commit\":{\"author\":{\"name\":\"Ian Lee\",\"email\":\"IanLee1521@gmail.com\"\ + ,\"date\":\"2017-09-29T19:29:42Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2017-09-29T19:29:42Z\"},\"message\":\"Fixed\ + \ minor typo\",\"tree\":{\"sha\":\"4b1780a3cac3fcfe3356b1da70346f19f63106b7\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4b1780a3cac3fcfe3356b1da70346f19f63106b7\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b066c93c2676bc957d971d2c4188e77b3e383b77\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b066c93c2676bc957d971d2c4188e77b3e383b77\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b066c93c2676bc957d971d2c4188e77b3e383b77\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b066c93c2676bc957d971d2c4188e77b3e383b77/comments\"\ + ,\"author\":{\"login\":\"IanLee1521\",\"id\":828452,\"node_id\":\"MDQ6VXNlcjgyODQ1Mg==\"\ + ,\"avatar_url\":\"https://avatars0.githubusercontent.com/u/828452?v=4\",\"gravatar_id\"\ + :\"\",\"url\":\"https://api.github.com/users/IanLee1521\",\"html_url\":\"https://github.com/IanLee1521\"\ + ,\"followers_url\":\"https://api.github.com/users/IanLee1521/followers\",\"\ + following_url\":\"https://api.github.com/users/IanLee1521/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/IanLee1521/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/IanLee1521/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/IanLee1521/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/IanLee1521/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/IanLee1521/repos\",\"events_url\":\"https://api.github.com/users/IanLee1521/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/IanLee1521/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\"\ + ,\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + }]},{\"sha\":\"3230594a7aa8782fbcf51329b2395118f7cf0d15\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjMyMzA1OTRhN2FhODc4MmZiY2Y1MTMyOWIyMzk1MTE4ZjdjZjBkMTU=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2017-08-30T14:02:20Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2017-08-30T14:02:20Z\"},\"message\":\"Update\ + \ README.md\",\"tree\":{\"sha\":\"2ba18d3d080f553b23501cd8485eae3c50589e6a\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2ba18d3d080f553b23501cd8485eae3c50589e6a\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3230594a7aa8782fbcf51329b2395118f7cf0d15\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3230594a7aa8782fbcf51329b2395118f7cf0d15/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + }]},{\"sha\":\"6560cbff33fbff8740f0407f4cac1091bc25e6ae\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjY1NjBjYmZmMzNmYmZmODc0MGYwNDA3ZjRjYWMxMDkxYmMyNWU2YWU=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2017-02-02T19:10:16Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2017-02-02T19:10:16Z\"},\"message\":\"Update\ + \ README.md\",\"tree\":{\"sha\":\"39e2c45b98a62d06d6d84222d5a8c244150ec3c4\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/39e2c45b98a62d06d6d84222d5a8c244150ec3c4\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6560cbff33fbff8740f0407f4cac1091bc25e6ae\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6560cbff33fbff8740f0407f4cac1091bc25e6ae/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"feb5100831541db79eb83a263986df129573f3de\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/feb5100831541db79eb83a263986df129573f3de\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/feb5100831541db79eb83a263986df129573f3de\"\ + }]},{\"sha\":\"feb5100831541db79eb83a263986df129573f3de\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmZlYjUxMDA4MzE1NDFkYjc5ZWI4M2EyNjM5ODZkZjEyOTU3M2YzZGU=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2017-02-02T19:09:38Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2017-02-02T19:09:38Z\"},\"message\":\"Merge\ + \ pull request #20 from briandant/main\\n\\nAdd further instructions for using\ + \ env vars\",\"tree\":{\"sha\":\"be98fa09eb1a8343b44071cdae6aac15e61d0cc9\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/be98fa09eb1a8343b44071cdae6aac15e61d0cc9\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/feb5100831541db79eb83a263986df129573f3de\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/feb5100831541db79eb83a263986df129573f3de\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/feb5100831541db79eb83a263986df129573f3de\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/feb5100831541db79eb83a263986df129573f3de/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + },{\"sha\":\"6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6802411a35f438bf62ee8a1c1928cd36ca50d534\"\ + }]},{\"sha\":\"6802411a35f438bf62ee8a1c1928cd36ca50d534\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjY4MDI0MTFhMzVmNDM4YmY2MmVlOGExYzE5MjhjZDM2Y2E1MGQ1MzQ=\"\ + ,\"commit\":{\"author\":{\"name\":\"Brian Dant\",\"email\":\"briandant@users.noreply.github.com\"\ + ,\"date\":\"2017-02-02T18:37:42Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2017-02-02T18:37:42Z\"},\"message\":\"Add\ + \ further instructions for using env vars\",\"tree\":{\"sha\":\"be98fa09eb1a8343b44071cdae6aac15e61d0cc9\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/be98fa09eb1a8343b44071cdae6aac15e61d0cc9\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/6802411a35f438bf62ee8a1c1928cd36ca50d534\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6802411a35f438bf62ee8a1c1928cd36ca50d534/comments\"\ + ,\"author\":{\"login\":\"briandant\",\"id\":1884902,\"node_id\":\"MDQ6VXNlcjE4ODQ5MDI=\"\ + ,\"avatar_url\":\"https://avatars2.githubusercontent.com/u/1884902?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/briandant\",\"html_url\"\ + :\"https://github.com/briandant\",\"followers_url\":\"https://api.github.com/users/briandant/followers\"\ + ,\"following_url\":\"https://api.github.com/users/briandant/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/briandant/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/briandant/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/briandant/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/briandant/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/briandant/repos\",\"events_url\":\"https://api.github.com/users/briandant/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/briandant/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\"\ + ,\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + }]},{\"sha\":\"3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjNmMjhlOTNjYmMxZDI2NmMzMGE1YmRmNDMxMjZlZDQ0ZmJlYjAwOWQ=\"\ + ,\"commit\":{\"author\":{\"name\":\"Codecov Test Bot\",\"email\":\"hello@codecov.io\"\ + ,\"date\":\"2016-09-29T10:37:07Z\"},\"committer\":{\"name\":\"Codecov Test Bot\"\ + ,\"email\":\"hello@codecov.io\",\"date\":\"2016-09-29T10:37:07Z\"},\"message\"\ + :\"Circle build #355\\nhttps://circleci.com/gh/codecov/testsuite/355\\nbash\ + \ <(curl -s https://raw.githubusercontent.com/codecov/codecov-bash/main/codecov)\ + \ -v -u https://codecov.io\",\"tree\":{\"sha\":\"80c741dbd6916ef10fadd2357efdcac15402af49\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/80c741dbd6916ef10fadd2357efdcac15402af49\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f28e93cbc1d266c30a5bdf43126ed44fbeb009d/comments\"\ + ,\"author\":{\"login\":\"codecov-test\",\"id\":8485477,\"node_id\":\"MDQ6VXNlcjg0ODU0Nzc=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8485477?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov-test\",\"\ + html_url\":\"https://github.com/codecov-test\",\"followers_url\":\"https://api.github.com/users/codecov-test/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov-test/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov-test/gists{/gist_id}\"\ + ,\"starred_url\":\"https://api.github.com/users/codecov-test/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/codecov-test/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/codecov-test/orgs\",\"\ + repos_url\":\"https://api.github.com/users/codecov-test/repos\",\"events_url\"\ + :\"https://api.github.com/users/codecov-test/events{/privacy}\",\"received_events_url\"\ + :\"https://api.github.com/users/codecov-test/received_events\",\"type\":\"User\"\ + ,\"site_admin\":false},\"committer\":{\"login\":\"codecov-test\",\"id\":8485477,\"\ + node_id\":\"MDQ6VXNlcjg0ODU0Nzc=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8485477?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov-test\"\ + ,\"html_url\":\"https://github.com/codecov-test\",\"followers_url\":\"https://api.github.com/users/codecov-test/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov-test/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov-test/gists{/gist_id}\"\ + ,\"starred_url\":\"https://api.github.com/users/codecov-test/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/codecov-test/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/codecov-test/orgs\",\"\ + repos_url\":\"https://api.github.com/users/codecov-test/repos\",\"events_url\"\ + :\"https://api.github.com/users/codecov-test/events{/privacy}\",\"received_events_url\"\ + :\"https://api.github.com/users/codecov-test/received_events\",\"type\":\"User\"\ + ,\"site_admin\":false},\"parents\":[{\"sha\":\"63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + }]},{\"sha\":\"63f5740c33f2aac68fd0757f471ab741f9e20a05\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjYzZjU3NDBjMzNmMmFhYzY4ZmQwNzU3ZjQ3MWFiNzQxZjllMjBhMDU=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2016-08-26T19:21:23Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2016-08-26T19:21:23Z\"},\"message\":\"Update\ + \ README.md\",\"tree\":{\"sha\":\"80c741dbd6916ef10fadd2357efdcac15402af49\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/80c741dbd6916ef10fadd2357efdcac15402af49\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/63f5740c33f2aac68fd0757f471ab741f9e20a05\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/63f5740c33f2aac68fd0757f471ab741f9e20a05/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + }]},{\"sha\":\"d61bb41b849de7125ca17fbd37292479648e7fa7\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmQ2MWJiNDFiODQ5ZGU3MTI1Y2ExN2ZiZDM3MjkyNDc5NjQ4ZTdmYTc=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2016-08-16T15:47:11Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2016-08-16T15:47:11Z\"},\"message\":\"Merge\ + \ pull request #18 from yurovant/patch-1\\n\\nUpdate README.md\",\"tree\":{\"\ + sha\":\"6f202265ab6e9bbe035e9567729cef9d042faa2d\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6f202265ab6e9bbe035e9567729cef9d042faa2d\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/d61bb41b849de7125ca17fbd37292479648e7fa7\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d61bb41b849de7125ca17fbd37292479648e7fa7/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + },{\"sha\":\"2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2d1b772e138f05dbbd577ce0dcf3633577629a76\"\ + }]},{\"sha\":\"2d1b772e138f05dbbd577ce0dcf3633577629a76\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjJkMWI3NzJlMTM4ZjA1ZGJiZDU3N2NlMGRjZjM2MzM1Nzc2MjlhNzY=\"\ + ,\"commit\":{\"author\":{\"name\":\"Anton Yurovskykh\",\"email\":\"anton.yurovskykh@gmail.com\"\ + ,\"date\":\"2016-08-16T07:35:54Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2016-08-16T07:35:54Z\"},\"message\":\"Update\ + \ README.md\\n\\ntypo: priveta --> private\",\"tree\":{\"sha\":\"6f202265ab6e9bbe035e9567729cef9d042faa2d\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6f202265ab6e9bbe035e9567729cef9d042faa2d\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2d1b772e138f05dbbd577ce0dcf3633577629a76\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d1b772e138f05dbbd577ce0dcf3633577629a76/comments\"\ + ,\"author\":{\"login\":\"yurovant\",\"id\":11337124,\"node_id\":\"MDQ6VXNlcjExMzM3MTI0\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/11337124?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/yurovant\",\"html_url\"\ + :\"https://github.com/yurovant\",\"followers_url\":\"https://api.github.com/users/yurovant/followers\"\ + ,\"following_url\":\"https://api.github.com/users/yurovant/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/yurovant/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/yurovant/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/yurovant/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/yurovant/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/yurovant/repos\",\"events_url\":\"https://api.github.com/users/yurovant/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/yurovant/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"web-flow\"\ + ,\"id\":19864447,\"node_id\":\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + }]},{\"sha\":\"84ea8b9ba0f8134be0477971e72b233959f5d3b6\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg0ZWE4YjliYTBmODEzNGJlMDQ3Nzk3MWU3MmIyMzM5NTlmNWQzYjY=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2016-08-05T16:46:35Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2016-08-05T16:46:35Z\"},\"message\":\"Update\ + \ README.md\",\"tree\":{\"sha\":\"9b948b519e86c5089a2c9c00d0a8f326282bc5cd\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9b948b519e86c5089a2c9c00d0a8f326282bc5cd\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/84ea8b9ba0f8134be0477971e72b233959f5d3b6\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/84ea8b9ba0f8134be0477971e72b233959f5d3b6/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"e051e55647e0ecc27539b2dc40fb9b2839383060\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e051e55647e0ecc27539b2dc40fb9b2839383060\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/e051e55647e0ecc27539b2dc40fb9b2839383060\"\ + }]},{\"sha\":\"e051e55647e0ecc27539b2dc40fb9b2839383060\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmUwNTFlNTU2NDdlMGVjYzI3NTM5YjJkYzQwZmI5YjI4MzkzODMwNjA=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2016-08-05T16:44:34Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2016-08-05T16:44:34Z\"},\"message\":\"Update\ + \ README.md\",\"tree\":{\"sha\":\"1f6a16b0fa552bf95a5494b9ee8bb8f9743619d5\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/1f6a16b0fa552bf95a5494b9ee8bb8f9743619d5\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e051e55647e0ecc27539b2dc40fb9b2839383060\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e051e55647e0ecc27539b2dc40fb9b2839383060\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/e051e55647e0ecc27539b2dc40fb9b2839383060\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e051e55647e0ecc27539b2dc40fb9b2839383060/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"248906bd30b16b8bc131d0600ab66545f51a7085\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/248906bd30b16b8bc131d0600ab66545f51a7085\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/248906bd30b16b8bc131d0600ab66545f51a7085\"\ + }]},{\"sha\":\"248906bd30b16b8bc131d0600ab66545f51a7085\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjI0ODkwNmJkMzBiMTZiOGJjMTMxZDA2MDBhYjY2NTQ1ZjUxYTcwODU=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2016-08-05T16:44:04Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2016-08-05T16:44:04Z\"},\"message\":\"Update\ + \ README.md\",\"tree\":{\"sha\":\"28998def90133cfa0dbbe834eb345fdc174093f9\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/28998def90133cfa0dbbe834eb345fdc174093f9\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/248906bd30b16b8bc131d0600ab66545f51a7085\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/248906bd30b16b8bc131d0600ab66545f51a7085\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/248906bd30b16b8bc131d0600ab66545f51a7085\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/248906bd30b16b8bc131d0600ab66545f51a7085/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"8b8bd76c787dce785d0d01fd3ea9b0d8741763c4\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8b8bd76c787dce785d0d01fd3ea9b0d8741763c4\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8b8bd76c787dce785d0d01fd3ea9b0d8741763c4\"\ + }]},{\"sha\":\"8b8bd76c787dce785d0d01fd3ea9b0d8741763c4\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjhiOGJkNzZjNzg3ZGNlNzg1ZDBkMDFmZDNlYTliMGQ4NzQxNzYzYzQ=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2016-08-05T16:43:41Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2016-08-05T16:43:41Z\"},\"message\":\"Update\ + \ README.md\",\"tree\":{\"sha\":\"8c70a0880d65826016322215446e7bb9e1c4f4b9\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/8c70a0880d65826016322215446e7bb9e1c4f4b9\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8b8bd76c787dce785d0d01fd3ea9b0d8741763c4\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8b8bd76c787dce785d0d01fd3ea9b0d8741763c4\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/8b8bd76c787dce785d0d01fd3ea9b0d8741763c4\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8b8bd76c787dce785d0d01fd3ea9b0d8741763c4/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"ca1acdab2996eef3f7e5ddc27243efb0752606e6\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ca1acdab2996eef3f7e5ddc27243efb0752606e6\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/ca1acdab2996eef3f7e5ddc27243efb0752606e6\"\ + }]},{\"sha\":\"ca1acdab2996eef3f7e5ddc27243efb0752606e6\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmNhMWFjZGFiMjk5NmVlZjNmN2U1ZGRjMjcyNDNlZmIwNzUyNjA2ZTY=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2016-08-05T16:42:16Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2016-08-05T16:42:16Z\"},\"message\":\"Update\ + \ README.md\",\"tree\":{\"sha\":\"dd291f7d2f90eefb3f8cd34ee2445832dd0de5f7\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/dd291f7d2f90eefb3f8cd34ee2445832dd0de5f7\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ca1acdab2996eef3f7e5ddc27243efb0752606e6\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ca1acdab2996eef3f7e5ddc27243efb0752606e6\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/ca1acdab2996eef3f7e5ddc27243efb0752606e6\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ca1acdab2996eef3f7e5ddc27243efb0752606e6/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"ef8c461aa9f2d7b7a93270328a33da5c7cb366b1\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ef8c461aa9f2d7b7a93270328a33da5c7cb366b1\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/ef8c461aa9f2d7b7a93270328a33da5c7cb366b1\"\ + }]},{\"sha\":\"ef8c461aa9f2d7b7a93270328a33da5c7cb366b1\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmVmOGM0NjFhYTlmMmQ3YjdhOTMyNzAzMjhhMzNkYTVjN2NiMzY2YjE=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2016-06-21T15:23:48Z\"},\"committer\":{\"name\":\"GitHub\",\"email\"\ + :\"noreply@github.com\",\"date\":\"2016-06-21T15:23:48Z\"},\"message\":\"Update\ + \ README.md\",\"tree\":{\"sha\":\"9e3b853075b2b62d027d5d2acb3c4898bbc3182d\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9e3b853075b2b62d027d5d2acb3c4898bbc3182d\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ef8c461aa9f2d7b7a93270328a33da5c7cb366b1\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ef8c461aa9f2d7b7a93270328a33da5c7cb366b1\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/ef8c461aa9f2d7b7a93270328a33da5c7cb366b1\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ef8c461aa9f2d7b7a93270328a33da5c7cb366b1/comments\"\ + ,\"author\":null,\"committer\":{\"login\":\"web-flow\",\"id\":19864447,\"node_id\"\ + :\"MDQ6VXNlcjE5ODY0NDQ3\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/19864447?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/web-flow\",\"html_url\"\ + :\"https://github.com/web-flow\",\"followers_url\":\"https://api.github.com/users/web-flow/followers\"\ + ,\"following_url\":\"https://api.github.com/users/web-flow/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/web-flow/gists{/gist_id}\",\"\ + starred_url\":\"https://api.github.com/users/web-flow/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/web-flow/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/web-flow/orgs\",\"repos_url\"\ + :\"https://api.github.com/users/web-flow/repos\",\"events_url\":\"https://api.github.com/users/web-flow/events{/privacy}\"\ + ,\"received_events_url\":\"https://api.github.com/users/web-flow/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"e02bded5e1c9bc48261788c84c34fcbcb89b4568\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e02bded5e1c9bc48261788c84c34fcbcb89b4568\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/e02bded5e1c9bc48261788c84c34fcbcb89b4568\"\ + }]},{\"sha\":\"e02bded5e1c9bc48261788c84c34fcbcb89b4568\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OmUwMmJkZWQ1ZTFjOWJjNDgyNjE3ODhjODRjMzRmY2JjYjg5YjQ1Njg=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2016-01-08T00:45:34Z\"},\"committer\":{\"name\":\"Steve Peak\",\"\ + email\":\"steve@codecov.io\",\"date\":\"2016-01-08T00:45:34Z\"},\"message\"\ + :\"Add more docs close #12\",\"tree\":{\"sha\":\"49430b410f06ab891abefd6d181eaa25a2e2350d\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/49430b410f06ab891abefd6d181eaa25a2e2350d\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e02bded5e1c9bc48261788c84c34fcbcb89b4568\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e02bded5e1c9bc48261788c84c34fcbcb89b4568\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/e02bded5e1c9bc48261788c84c34fcbcb89b4568\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e02bded5e1c9bc48261788c84c34fcbcb89b4568/comments\"\ + ,\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"810e2a539ab476977ec6e6d4b80010431be3cd99\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/810e2a539ab476977ec6e6d4b80010431be3cd99\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/810e2a539ab476977ec6e6d4b80010431be3cd99\"\ + }]},{\"sha\":\"810e2a539ab476977ec6e6d4b80010431be3cd99\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjgxMGUyYTUzOWFiNDc2OTc3ZWM2ZTZkNGI4MDAxMDQzMWJlM2NkOTk=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2015-11-19T21:12:03Z\"},\"committer\":{\"name\":\"Steve Peak\",\"\ + email\":\"steve@codecov.io\",\"date\":\"2015-11-19T21:12:03Z\"},\"message\"\ + :\"Merge pull request #11 from ticosax/faster-travis-build\\n\\nFaster travis\ + \ builds\",\"tree\":{\"sha\":\"923c160d9c1665340094d064643021f85026928c\",\"\ + url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/923c160d9c1665340094d064643021f85026928c\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/810e2a539ab476977ec6e6d4b80010431be3cd99\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/810e2a539ab476977ec6e6d4b80010431be3cd99\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/810e2a539ab476977ec6e6d4b80010431be3cd99\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/810e2a539ab476977ec6e6d4b80010431be3cd99/comments\"\ + ,\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"202ddb27233d5007be31e3a4268aeac9a54febce\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/202ddb27233d5007be31e3a4268aeac9a54febce\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/202ddb27233d5007be31e3a4268aeac9a54febce\"\ + },{\"sha\":\"2be550c135cc13425cb2c239b9321e78dcfb787b\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2be550c135cc13425cb2c239b9321e78dcfb787b\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2be550c135cc13425cb2c239b9321e78dcfb787b\"\ + }]},{\"sha\":\"2be550c135cc13425cb2c239b9321e78dcfb787b\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjJiZTU1MGMxMzVjYzEzNDI1Y2IyYzIzOWI5MzIxZTc4ZGNmYjc4N2I=\"\ + ,\"commit\":{\"author\":{\"name\":\"Nicolas Delaby\",\"email\":\"nicolas.delaby@lock8.me\"\ + ,\"date\":\"2015-11-19T21:09:03Z\"},\"committer\":{\"name\":\"Nicolas Delaby\"\ + ,\"email\":\"nicolas.delaby@lock8.me\",\"date\":\"2015-11-19T21:09:03Z\"},\"\ + message\":\"Faster travis builds\",\"tree\":{\"sha\":\"923c160d9c1665340094d064643021f85026928c\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/923c160d9c1665340094d064643021f85026928c\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2be550c135cc13425cb2c239b9321e78dcfb787b\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2be550c135cc13425cb2c239b9321e78dcfb787b\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/2be550c135cc13425cb2c239b9321e78dcfb787b\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2be550c135cc13425cb2c239b9321e78dcfb787b/comments\"\ + ,\"author\":{\"login\":\"ticosax\",\"id\":1174343,\"node_id\":\"MDQ6VXNlcjExNzQzNDM=\"\ + ,\"avatar_url\":\"https://avatars1.githubusercontent.com/u/1174343?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/ticosax\",\"html_url\"\ + :\"https://github.com/ticosax\",\"followers_url\":\"https://api.github.com/users/ticosax/followers\"\ + ,\"following_url\":\"https://api.github.com/users/ticosax/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/ticosax/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/ticosax/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/ticosax/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/ticosax/orgs\",\"repos_url\":\"https://api.github.com/users/ticosax/repos\"\ + ,\"events_url\":\"https://api.github.com/users/ticosax/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/ticosax/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"committer\":{\"login\":\"ticosax\"\ + ,\"id\":1174343,\"node_id\":\"MDQ6VXNlcjExNzQzNDM=\",\"avatar_url\":\"https://avatars1.githubusercontent.com/u/1174343?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/ticosax\",\"html_url\"\ + :\"https://github.com/ticosax\",\"followers_url\":\"https://api.github.com/users/ticosax/followers\"\ + ,\"following_url\":\"https://api.github.com/users/ticosax/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/ticosax/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/ticosax/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/ticosax/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/ticosax/orgs\",\"repos_url\":\"https://api.github.com/users/ticosax/repos\"\ + ,\"events_url\":\"https://api.github.com/users/ticosax/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/ticosax/received_events\"\ + ,\"type\":\"User\",\"site_admin\":false},\"parents\":[{\"sha\":\"202ddb27233d5007be31e3a4268aeac9a54febce\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/202ddb27233d5007be31e3a4268aeac9a54febce\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/202ddb27233d5007be31e3a4268aeac9a54febce\"\ + }]},{\"sha\":\"202ddb27233d5007be31e3a4268aeac9a54febce\",\"node_id\":\"MDY6Q29tbWl0MTU2NjE3Nzc3OjIwMmRkYjI3MjMzZDUwMDdiZTMxZTNhNDI2OGFlYWM5YTU0ZmViY2U=\"\ + ,\"commit\":{\"author\":{\"name\":\"Steve Peak\",\"email\":\"steve@codecov.io\"\ + ,\"date\":\"2015-10-21T07:04:33Z\"},\"committer\":{\"name\":\"Steve Peak\",\"\ + email\":\"steve@codecov.io\",\"date\":\"2015-10-21T07:04:33Z\"},\"message\"\ + :\"Update README.md\",\"tree\":{\"sha\":\"4b56572177ecf98807947e380cfb0030d12c6e72\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4b56572177ecf98807947e380cfb0030d12c6e72\"\ + },\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/202ddb27233d5007be31e3a4268aeac9a54febce\"\ + ,\"comment_count\":0,\"verification\":{\"verified\":false,\"reason\":\"unsigned\"\ + ,\"signature\":null,\"payload\":null}},\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/202ddb27233d5007be31e3a4268aeac9a54febce\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/202ddb27233d5007be31e3a4268aeac9a54febce\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/202ddb27233d5007be31e3a4268aeac9a54febce/comments\"\ + ,\"author\":null,\"committer\":null,\"parents\":[{\"sha\":\"b1598e3d561f5f21b10150e98ad5c0bab286a6f2\"\ + ,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b1598e3d561f5f21b10150e98ad5c0bab286a6f2\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/commit/b1598e3d561f5f21b10150e98ad5c0bab286a6f2\"\ + }]}]" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:44 GMT + ETag: + - W/"1a82438d748546277d3c30c4a9f96807b098a62547a0190af29f5a613c3eb2fc" + Last-Modified: + - Thu, 26 Apr 2018 08:35:58 GMT + Link: + - ; + rel="next", ; + rel="last" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFE:6A20:1F6AD05:46BDA9A:5F87363C + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4909' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '91' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_authenticated.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_authenticated.yaml new file mode 100644 index 0000000000..1a69d0d4d8 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_authenticated.yaml @@ -0,0 +1,80 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python + response: + content: '{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-03-24T22:01:40Z","pushed_at":"2020-10-13T15:15:47Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":174,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":null,"forks":0,"open_issues":2,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true},"temp_clone_token":"","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"delete_branch_on_merge":false,"parent":{"id":24344106,"node_id":"MDEwOlJlcG9zaXRvcnkyNDM0NDEwNg==","name":"example-python","full_name":"codecov/example-python","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-python","description":"Python + coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-python","forks_url":"https://api.github.com/repos/codecov/example-python/forks","keys_url":"https://api.github.com/repos/codecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-python/teams","hooks_url":"https://api.github.com/repos/codecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-python/events","assignees_url":"https://api.github.com/repos/codecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-python/tags","blobs_url":"https://api.github.com/repos/codecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-python/languages","stargazers_url":"https://api.github.com/repos/codecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-python/subscription","commits_url":"https://api.github.com/repos/codecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-python/merges","archive_url":"https://api.github.com/repos/codecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-python/downloads","issues_url":"https://api.github.com/repos/codecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-python/deployments","created_at":"2014-09-22T20:20:06Z","updated_at":"2020-09-27T16:45:48Z","pushed_at":"2020-10-13T23:33:12Z","git_url":"git://github.com/codecov/example-python.git","ssh_url":"git@github.com:codecov/example-python.git","clone_url":"https://github.com/codecov/example-python.git","svn_url":"https://github.com/codecov/example-python","homepage":"https://codecov.io","size":83,"stargazers_count":226,"watchers_count":226,"language":"Python","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":189,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":5,"license":null,"forks":189,"open_issues":5,"watchers":226,"default_branch":"main"},"source":{"id":24344106,"node_id":"MDEwOlJlcG9zaXRvcnkyNDM0NDEwNg==","name":"example-python","full_name":"codecov/example-python","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-python","description":"Python + coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-python","forks_url":"https://api.github.com/repos/codecov/example-python/forks","keys_url":"https://api.github.com/repos/codecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-python/teams","hooks_url":"https://api.github.com/repos/codecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-python/events","assignees_url":"https://api.github.com/repos/codecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-python/tags","blobs_url":"https://api.github.com/repos/codecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-python/languages","stargazers_url":"https://api.github.com/repos/codecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-python/subscription","commits_url":"https://api.github.com/repos/codecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-python/merges","archive_url":"https://api.github.com/repos/codecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-python/downloads","issues_url":"https://api.github.com/repos/codecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-python/deployments","created_at":"2014-09-22T20:20:06Z","updated_at":"2020-09-27T16:45:48Z","pushed_at":"2020-10-13T23:33:12Z","git_url":"git://github.com/codecov/example-python.git","ssh_url":"git@github.com:codecov/example-python.git","clone_url":"https://github.com/codecov/example-python.git","svn_url":"https://github.com/codecov/example-python","homepage":"https://codecov.io","size":83,"stargazers_count":226,"watchers_count":226,"language":"Python","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":189,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":5,"license":null,"forks":189,"open_issues":5,"watchers":226,"default_branch":"main"},"network_count":189,"subscribers_count":0}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:31 GMT + ETag: + - W/"10336721274b17a77895a1bc924223c0cea0145d13cc9eaf2595f17c2de170a8" + Last-Modified: + - Tue, 24 Mar 2020 22:01:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFF5:3E7F:1F43804:480BE2F:5F87362F + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4934' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '66' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_authenticated_user.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_authenticated_user.yaml new file mode 100644 index 0000000000..8e2304639b --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_authenticated_user.yaml @@ -0,0 +1,218 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - github.com + user-agent: + - Default + method: GET + uri: https://github.com/login/oauth/access_token?code=dc38acf492b071cc4dce&client_id=999247146557c3ba045c&client_secret=testo8lnq6ihj7zsf896r15yxujnl06og9o0fqiu + response: + content: '{"refresh_token": "testblahblahblahblahsfas", "access_token":"testw5efy5qccduniyucsk5tesu08s4640xtoymv","token_type":"bearer","scope":"read:org,repo:status,user:email,write:repo_hook"}' + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - 'default-src ''none''; base-uri ''self''; block-all-mixed-content; connect-src + ''self'' uploads.github.com www.githubstatus.com collector.githubapp.com api.github.com + www.google-analytics.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com + github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com + cdn.optimizely.com logx.optimizely.com/v1/events wss://alive.github.com; font-src + github.githubassets.com; form-action ''self'' github.com gist.github.com; + frame-ancestors ''none''; frame-src render.githubusercontent.com; img-src + ''self'' data: github.githubassets.com identicons.github.com collector.githubapp.com + github-cloud.s3.amazonaws.com *.githubusercontent.com; manifest-src ''self''; + media-src ''none''; script-src github.githubassets.com; style-src ''unsafe-inline'' + github.githubassets.com; worker-src github.com/socket-worker.js gist.github.com/socket-worker.js' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 19:03:09 GMT + ETag: + - W/"f7010df29ae7de00c6f093b748234cb0" + Expect-CT: + - max-age=2592000, report-uri="https://api.github.com/_private/browser/errors" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - X-PJAX + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Request-Id: + - D324:2F20:1EA2E95:32133F8:5F874B6C + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/user + response: + content: '{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false,"name":"Thiago","company":"@codecov + ","blog":"","location":null,"email":null,"hireable":null,"bio":null,"twitter_username":null,"public_repos":3,"public_gists":0,"followers":0,"following":0,"created_at":"2018-10-22T17:51:44Z","updated_at":"2020-10-14T17:58:13Z"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 19:03:09 GMT + ETag: + - W/"3b03bdc732212d313b16662cf09edbbcf4fa403246b6b1da4419b4967f65dad6" + Last-Modified: + - Wed, 14 Oct 2020 17:58:13 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D325:2B25:2B2F90:47EA57:5F874B6D + X-OAuth-Client-Id: + - 999247146557c3ba045c + X-OAuth-Scopes: + - read:org, repo:status, user:email, write:repo_hook + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4998' + X-RateLimit-Reset: + - '1602705580' + X-RateLimit-Used: + - '2' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/user/emails + response: + content: '[{"email":"thiago@codecov.io","primary":true,"verified":true,"visibility":"private"},{"email":"44376991+ThiagoCodecov@users.noreply.github.com","primary":false,"verified":true,"visibility":null},{"email":"abcdefg@domain.qwertyuiopas.com","primary":false,"verified":true,"visibility":null}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 18 Mar 2021 23:58:18 GMT + ETag: + - W/"1ede8842545047586cb0de28048ca55b6f90555a35fdd69e0237830f407fb408" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - user, user:email + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C29D:7BB6:5422A0B:898EB76:6053E91A + X-OAuth-Scopes: + - read:org, repo:status, user:email, write:repo_hook + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1616115498' + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + x-oauth-client-id: + - 999247146557c3ba045c + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_authenticated_user_no_refresh_token.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_authenticated_user_no_refresh_token.yaml new file mode 100644 index 0000000000..73664574f0 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_authenticated_user_no_refresh_token.yaml @@ -0,0 +1,218 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - github.com + user-agent: + - Default + method: GET + uri: https://github.com/login/oauth/access_token?code=dc38acf492b071cc4dce&client_id=999247146557c3ba045c&client_secret=testo8lnq6ihj7zsf896r15yxujnl06og9o0fqiu + response: + content: '{"access_token":"testw5efy5qccduniyucsk5tesu08s4640xtoymv","token_type":"bearer","scope":"read:org,repo:status,user:email,write:repo_hook"}' + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - 'default-src ''none''; base-uri ''self''; block-all-mixed-content; connect-src + ''self'' uploads.github.com www.githubstatus.com collector.githubapp.com api.github.com + www.google-analytics.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com + github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com + cdn.optimizely.com logx.optimizely.com/v1/events wss://alive.github.com; font-src + github.githubassets.com; form-action ''self'' github.com gist.github.com; + frame-ancestors ''none''; frame-src render.githubusercontent.com; img-src + ''self'' data: github.githubassets.com identicons.github.com collector.githubapp.com + github-cloud.s3.amazonaws.com *.githubusercontent.com; manifest-src ''self''; + media-src ''none''; script-src github.githubassets.com; style-src ''unsafe-inline'' + github.githubassets.com; worker-src github.com/socket-worker.js gist.github.com/socket-worker.js' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 19:03:09 GMT + ETag: + - W/"f7010df29ae7de00c6f093b748234cb0" + Expect-CT: + - max-age=2592000, report-uri="https://api.github.com/_private/browser/errors" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - X-PJAX + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Request-Id: + - D324:2F20:1EA2E95:32133F8:5F874B6C + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/user + response: + content: '{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false,"name":"Thiago","company":"@codecov + ","blog":"","location":null,"email":null,"hireable":null,"bio":null,"twitter_username":null,"public_repos":3,"public_gists":0,"followers":0,"following":0,"created_at":"2018-10-22T17:51:44Z","updated_at":"2020-10-14T17:58:13Z"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 19:03:09 GMT + ETag: + - W/"3b03bdc732212d313b16662cf09edbbcf4fa403246b6b1da4419b4967f65dad6" + Last-Modified: + - Wed, 14 Oct 2020 17:58:13 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D325:2B25:2B2F90:47EA57:5F874B6D + X-OAuth-Client-Id: + - 999247146557c3ba045c + X-OAuth-Scopes: + - read:org, repo:status, user:email, write:repo_hook + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4998' + X-RateLimit-Reset: + - '1602705580' + X-RateLimit-Used: + - '2' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/user/emails + response: + content: '[{"email":"thiago@codecov.io","primary":true,"verified":true,"visibility":"private"},{"email":"44376991+ThiagoCodecov@users.noreply.github.com","primary":false,"verified":true,"visibility":null},{"email":"abcdefg@domain.qwertyuiopas.com","primary":false,"verified":true,"visibility":null}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 18 Mar 2021 23:58:18 GMT + ETag: + - W/"1ede8842545047586cb0de28048ca55b6f90555a35fdd69e0237830f407fb408" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - user, user:email + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C29D:7BB6:5422A0B:898EB76:6053E91A + X-OAuth-Scopes: + - read:org, repo:status, user:email, write:repo_hook + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1616115498' + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + x-oauth-client-id: + - 999247146557c3ba045c + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_authenticated_user_with_public_emails.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_authenticated_user_with_public_emails.yaml new file mode 100644 index 0000000000..74774afdb7 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_authenticated_user_with_public_emails.yaml @@ -0,0 +1,172 @@ +interactions: + - request: + body: "" + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - github.com + user-agent: + - Default + method: GET + uri: https://github.com/login/oauth/access_token?client_id=Iv23liSqj8DAO20A3KLA&client_secret=a6a6397fffea369e54495c88ca469d988ea4ccd2&code=71367b9f258ca9a60c44 + response: + content: '{"access_token":"testtoken","expires_in":28800,"refresh_token":"testtoken","refresh_token_expires_in":15897600,"token_type":"bearer","scope":""}' + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - "default-src 'none'; base-uri 'self'; child-src github.com/assets-cdn/worker/ + github.com/webpack/ github.com/assets/ gist.github.com/assets-cdn/worker/; + connect-src 'self' uploads.github.com www.githubstatus.com collector.github.com + raw.githubusercontent.com api.github.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com + github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com + *.rel.tunnels.api.visualstudio.com wss://*.rel.tunnels.api.visualstudio.com + api.githubcopilot.com objects-origin.githubusercontent.com copilot-proxy.githubusercontent.com/v1/engines/github-completion/completions + proxy.enterprise.githubcopilot.com/v1/engines/github-completion/completions + *.actions.githubusercontent.com wss://*.actions.githubusercontent.com productionresultssa0.blob.core.windows.net/ + productionresultssa1.blob.core.windows.net/ productionresultssa2.blob.core.windows.net/ + productionresultssa3.blob.core.windows.net/ productionresultssa4.blob.core.windows.net/ + productionresultssa5.blob.core.windows.net/ productionresultssa6.blob.core.windows.net/ + productionresultssa7.blob.core.windows.net/ productionresultssa8.blob.core.windows.net/ + productionresultssa9.blob.core.windows.net/ productionresultssa10.blob.core.windows.net/ + productionresultssa11.blob.core.windows.net/ productionresultssa12.blob.core.windows.net/ + productionresultssa13.blob.core.windows.net/ productionresultssa14.blob.core.windows.net/ + productionresultssa15.blob.core.windows.net/ productionresultssa16.blob.core.windows.net/ + productionresultssa17.blob.core.windows.net/ productionresultssa18.blob.core.windows.net/ + productionresultssa19.blob.core.windows.net/ github-production-repository-image-32fea6.s3.amazonaws.com + github-production-release-asset-2e65be.s3.amazonaws.com insights.github.com + wss://alive.github.com; font-src github.githubassets.com; form-action 'self' + github.com gist.github.com copilot-workspace.githubnext.com objects-origin.githubusercontent.com; + frame-ancestors 'none'; frame-src viewscreen.githubusercontent.com notebooks.githubusercontent.com; + img-src 'self' data: blob: github.githubassets.com media.githubusercontent.com + camo.githubusercontent.com identicons.github.com avatars.githubusercontent.com + github-cloud.s3.amazonaws.com objects.githubusercontent.com secured-user-images.githubusercontent.com/ + user-images.githubusercontent.com/ private-user-images.githubusercontent.com + opengraph.githubassets.com github-production-user-asset-6210df.s3.amazonaws.com + customer-stories-feed.github.com spotlights-feed.github.com objects-origin.githubusercontent.com + *.githubusercontent.com; manifest-src 'self'; media-src github.com user-images.githubusercontent.com/ + secured-user-images.githubusercontent.com/ private-user-images.githubusercontent.com + github-production-user-asset-6210df.s3.amazonaws.com gist.github.com; script-src + github.githubassets.com; style-src 'unsafe-inline' github.githubassets.com; + upgrade-insecure-requests; worker-src github.com/assets-cdn/worker/ github.com/webpack/ + github.com/assets/ gist.github.com/assets-cdn/worker/" + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 12 Aug 2024 11:42:46 GMT + ETag: + - W/"aa51c72a4b5b31363cfcd944b4128eda" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - X-PJAX, X-PJAX-Container, Turbo-Visit, Turbo-Frame + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Request-Id: + - 54D7:1A73BB:EDCCC87:F3586CD:66B9F536 + X-XSS-Protection: + - "0" + http_version: HTTP/1.1 + status_code: 200 + - request: + body: "" + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/user + response: + content: + '{"login":"RulaKhaled","id":91732700,"node_id":"U_kgDOBXe63A","avatar_url":"https://avatars.githubusercontent.com/u/91732700?v=4","gravatar_id":"","url":"https://api.github.com/users/RulaKhaled","html_url":"https://github.com/RulaKhaled","followers_url":"https://api.github.com/users/RulaKhaled/followers","following_url":"https://api.github.com/users/RulaKhaled/following{/other_user}","gists_url":"https://api.github.com/users/RulaKhaled/gists{/gist_id}","starred_url":"https://api.github.com/users/RulaKhaled/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/RulaKhaled/subscriptions","organizations_url":"https://api.github.com/users/RulaKhaled/orgs","repos_url":"https://api.github.com/users/RulaKhaled/repos","events_url":"https://api.github.com/users/RulaKhaled/events{/privacy}","received_events_url":"https://api.github.com/users/RulaKhaled/received_events","type":"User","site_admin":false,"name":"Rola + Abuhasna","company":null,"blog":"","location":"Austria","email":"rola.abuhasna@sentry.io","hireable":null,"bio":null,"twitter_username":null,"notification_email":"rola.abuhasna@sentry.io","public_repos":2,"public_gists":0,"followers":1,"following":0,"created_at":"2021-10-01T11:03:07Z","updated_at":"2024-08-01T13:21:20Z"}' + headers: + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 12 Aug 2024 11:42:46 GMT + ETag: + - W/"a76d381d9e60d0f6dd8b2ef9bd41c7f4fcba15d1905c777026f693e7266f4644" + Last-Modified: + - Thu, 01 Aug 2024 13:21:20 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - github.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - "" + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D7C2:A952:101AD801:104D7485:66B9F536 + X-OAuth-Scopes: + - "" + X-RateLimit-Limit: + - "5000" + X-RateLimit-Remaining: + - "4999" + X-RateLimit-Reset: + - "1723466566" + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - "1" + X-XSS-Protection: + - "0" + github-authentication-token-expiration: + - 2024-08-12 19:42:46 UTC + x-accepted-github-permissions: + - allows_permissionless_access=true + x-github-api-version-selected: + - "2022-11-28" + x-oauth-client-id: + - Iv23liSqj8DAO20A3KLA + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_behind_by.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_behind_by.yaml new file mode 100644 index 0000000000..eee1b16220 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_behind_by.yaml @@ -0,0 +1,102 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecove2e/example-python/compare/main...0206296b1424912cc05069a9bf4025cbb95f5ecc + response: + content: '{"url":"https://api.github.com/repos/codecove2e/example-python/compare/main...0206296b1424912cc05069a9bf4025cbb95f5ecc","html_url":"https://github.com/codecove2e/example-python/compare/main...0206296b1424912cc05069a9bf4025cbb95f5ecc","permalink_url":"https://github.com/codecove2e/example-python/compare/codecove2e:93189ce...codecove2e:0206296","diff_url":"https://github.com/codecove2e/example-python/compare/main...0206296b1424912cc05069a9bf4025cbb95f5ecc.diff","patch_url":"https://github.com/codecove2e/example-python/compare/main...0206296b1424912cc05069a9bf4025cbb95f5ecc.patch","base_commit":{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","node_id":"MDY6Q29tbWl0NTE1Mjk1ODU4OjkzMTg5Y2U1MGYyMjQyOTZkNjQxMmUyODg0YjkzZGNjM2M3Yzg2NTQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"message":"Adding + some encoding","tree":{"sha":"966e4dc888b0828c5dea75f21e82eb2daa9a4485","url":"https://api.github.com/repos/codecove2e/example-python/git/trees/966e4dc888b0828c5dea75f21e82eb2daa9a4485"},"url":"https://api.github.com/repos/codecove2e/example-python/git/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","html_url":"https://github.com/codecove2e/example-python/commit/93189ce50f224296d6412e2884b93dcc3c7c8654","comments_url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","url":"https://api.github.com/repos/codecove2e/example-python/commits/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","html_url":"https://github.com/codecove2e/example-python/commit/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67"}]},"merge_base_commit":{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","node_id":"MDY6Q29tbWl0NTE1Mjk1ODU4OjkzMTg5Y2U1MGYyMjQyOTZkNjQxMmUyODg0YjkzZGNjM2M3Yzg2NTQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"message":"Adding + some encoding","tree":{"sha":"966e4dc888b0828c5dea75f21e82eb2daa9a4485","url":"https://api.github.com/repos/codecove2e/example-python/git/trees/966e4dc888b0828c5dea75f21e82eb2daa9a4485"},"url":"https://api.github.com/repos/codecove2e/example-python/git/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","html_url":"https://github.com/codecove2e/example-python/commit/93189ce50f224296d6412e2884b93dcc3c7c8654","comments_url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","url":"https://api.github.com/repos/codecove2e/example-python/commits/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","html_url":"https://github.com/codecove2e/example-python/commit/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67"}]},"status":"ahead","ahead_by":2,"behind_by":0,"total_commits":2,"commits":[{"sha":"8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","node_id":"C_kwDOHrbKctoAKDg1ODljMTljZTk1YTJiMTNjZjdiMzI3MmNiZjI3NWNhOTY1MWFlOWM","commit":{"author":{"name":"codecove2e","email":"93560619+codecove2e@users.noreply.github.com","date":"2022-08-16T19:38:32Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-08-16T19:38:32Z"},"message":"Update + __init__.py","tree":{"sha":"7c5d795e95bf1dcf12ebe45e3f2abf071e8bcaf3","url":"https://api.github.com/repos/codecove2e/example-python/git/trees/7c5d795e95bf1dcf12ebe45e3f2abf071e8bcaf3"},"url":"https://api.github.com/repos/codecove2e/example-python/git/commits/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJi+/I4CRBK7hj4Ov3rIwAApVwIACMRcC8711GZMH7nXKZH1tFK\ny7tOt6/WfCZsqEMtH5aQ5Px45MKT03wp+uPNBDnTJcdPLwZLfIdf7u9OpB9F4aVP\nsoLxW7IkUo9hT+E5i4YdldQOlmBF6pRwXA1AYYUAy1EgMchjf7kRkVCi3owJRXOV\nTP7cnM7ocP/RV46TzOgkhIh4wa0xbR8+LpFU7/+pRTbK+0u++UOB02Saq1RLvGyb\n8g8bHAtSNtTIRPfqI0EzE3piYjp6EFtoVmjDoTWn3z+fNw/6jB9SO9J9lyWFr62r\nbAoWgl8TuKuGogunswjsoWzs+13oFikh+4x2+V/ze0nk983D1ykXnMHQO2fIZV4=\n=i4b6\n-----END + PGP SIGNATURE-----\n","payload":"tree 7c5d795e95bf1dcf12ebe45e3f2abf071e8bcaf3\nparent + 93189ce50f224296d6412e2884b93dcc3c7c8654\nauthor codecove2e <93560619+codecove2e@users.noreply.github.com> + 1660678712 -0300\ncommitter GitHub 1660678712 -0300\n\nUpdate + __init__.py"}},"url":"https://api.github.com/repos/codecove2e/example-python/commits/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","html_url":"https://github.com/codecove2e/example-python/commit/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","comments_url":"https://api.github.com/repos/codecove2e/example-python/commits/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c/comments","author":{"login":"codecove2e","id":93560619,"node_id":"U_kgDOBZOfKw","avatar_url":"https://avatars.githubusercontent.com/u/93560619?v=4","gravatar_id":"","url":"https://api.github.com/users/codecove2e","html_url":"https://github.com/codecove2e","followers_url":"https://api.github.com/users/codecove2e/followers","following_url":"https://api.github.com/users/codecove2e/following{/other_user}","gists_url":"https://api.github.com/users/codecove2e/gists{/gist_id}","starred_url":"https://api.github.com/users/codecove2e/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecove2e/subscriptions","organizations_url":"https://api.github.com/users/codecove2e/orgs","repos_url":"https://api.github.com/users/codecove2e/repos","events_url":"https://api.github.com/users/codecove2e/events{/privacy}","received_events_url":"https://api.github.com/users/codecove2e/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","html_url":"https://github.com/codecove2e/example-python/commit/93189ce50f224296d6412e2884b93dcc3c7c8654"}]},{"sha":"0206296b1424912cc05069a9bf4025cbb95f5ecc","node_id":"C_kwDOHrbKctoAKDAyMDYyOTZiMTQyNDkxMmNjMDUwNjlhOWJmNDAyNWNiYjk1ZjVlY2M","commit":{"author":{"name":"codecove2e","email":"93560619+codecove2e@users.noreply.github.com","date":"2022-08-16T19:40:46Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-08-16T19:40:46Z"},"message":"Update + __init__.py","tree":{"sha":"fd5012113a1251fbc771a2a51f23cd2bcf5494db","url":"https://api.github.com/repos/codecove2e/example-python/git/trees/fd5012113a1251fbc771a2a51f23cd2bcf5494db"},"url":"https://api.github.com/repos/codecove2e/example-python/git/commits/0206296b1424912cc05069a9bf4025cbb95f5ecc","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJi+/K+CRBK7hj4Ov3rIwAAju4IAHamBTf5EPjFJ53s5yGCkRRj\ni70PeAVHUyAbeMdfB9MNm3jwqbKqLuhhSvKxhyoX7+7qZUDJnnsDJIlF5iT2hCPT\nr3qQ3llv3q7hGhV04UZLTVcd5XAcOJgPySw6Cu8UznNQOZWdwaOW8NO2m8OEESwv\njw7Z0KNo50WeKf+PTeMxz066S9vuVHtXtsbxm+doe4U0cJvhPrXyXuqPRqylhxy8\ncLkHTQrWHvbfaRskfiwxmA4kQrYtPP74lruOj6q/kOSYVPbeZFJznJoQem5GQ0zO\noYWdmQfYK4NthDSxqgw7IEjGTRkgrNSj2I61HC67oEOR5sbgQ/BgFTja27++ne4=\n=K83G\n-----END + PGP SIGNATURE-----\n","payload":"tree fd5012113a1251fbc771a2a51f23cd2bcf5494db\nparent + 8589c19ce95a2b13cf7b3272cbf275ca9651ae9c\nauthor codecove2e <93560619+codecove2e@users.noreply.github.com> + 1660678846 -0300\ncommitter GitHub 1660678846 -0300\n\nUpdate + __init__.py"}},"url":"https://api.github.com/repos/codecove2e/example-python/commits/0206296b1424912cc05069a9bf4025cbb95f5ecc","html_url":"https://github.com/codecove2e/example-python/commit/0206296b1424912cc05069a9bf4025cbb95f5ecc","comments_url":"https://api.github.com/repos/codecove2e/example-python/commits/0206296b1424912cc05069a9bf4025cbb95f5ecc/comments","author":{"login":"codecove2e","id":93560619,"node_id":"U_kgDOBZOfKw","avatar_url":"https://avatars.githubusercontent.com/u/93560619?v=4","gravatar_id":"","url":"https://api.github.com/users/codecove2e","html_url":"https://github.com/codecove2e","followers_url":"https://api.github.com/users/codecove2e/followers","following_url":"https://api.github.com/users/codecove2e/following{/other_user}","gists_url":"https://api.github.com/users/codecove2e/gists{/gist_id}","starred_url":"https://api.github.com/users/codecove2e/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecove2e/subscriptions","organizations_url":"https://api.github.com/users/codecove2e/orgs","repos_url":"https://api.github.com/users/codecove2e/repos","events_url":"https://api.github.com/users/codecove2e/events{/privacy}","received_events_url":"https://api.github.com/users/codecove2e/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","url":"https://api.github.com/repos/codecove2e/example-python/commits/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","html_url":"https://github.com/codecove2e/example-python/commit/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c"}]}],"files":[{"sha":"898991ad883e00916ed4ced91b534734b211c7ba","filename":"awesome/__init__.py","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/codecove2e/example-python/blob/0206296b1424912cc05069a9bf4025cbb95f5ecc/awesome%2F__init__.py","raw_url":"https://github.com/codecove2e/example-python/raw/0206296b1424912cc05069a9bf4025cbb95f5ecc/awesome%2F__init__.py","contents_url":"https://api.github.com/repos/codecove2e/example-python/contents/awesome%2F__init__.py?ref=0206296b1424912cc05069a9bf4025cbb95f5ecc","patch":"@@ + -3,7 +3,7 @@ def smile():\n \n \n def frown():\n- return \":(\"\n+ return + \"==(\"\n \n \n def shieeee(g):"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 06 Apr 2023 11:50:20 GMT + ETag: + - W/"0f624ee560abad668df8da2a607f6b7add5830226c1eab831e1ebda1280c7bc9" + Last-Modified: + - Tue, 16 Aug 2022 19:40:46 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E852:2F56:5D53E5F:5E16006:642EB1FB + X-OAuth-Scopes: + - project, read:org, read:repo_hook, repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4996' + X-RateLimit-Reset: + - '1680784935' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '4' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-05-04 19:16:14 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_best_effort_branches.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_best_effort_branches.yaml new file mode 100644 index 0000000000..e9c50d8860 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_best_effort_branches.yaml @@ -0,0 +1,75 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/vnd.github.groot-preview+json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/75f355d8d14ba3d7761c728b4d2607cde0eef065/branches-where-head + response: + content: '[{"name":"thiago/base-no-base","commit":{"sha":"75f355d8d14ba3d7761c728b4d2607cde0eef065","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/75f355d8d14ba3d7761c728b4d2607cde0eef065"},"protected":false}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 22:41:09 GMT + ETag: + - W/"cba07981f12c7f723ba1ee3adb1cc867eb06b174da9165f37dbefc2dafe7cff6" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.groot-preview; format=json + X-GitHub-Request-Id: + - D990:592E:411E4FB:6BCC43E:5F877E85 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4996' + X-RateLimit-Reset: + - '1602718640' + X-RateLimit-Used: + - '4' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_branch.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_branch.yaml new file mode 100644 index 0000000000..b38aae4833 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_branch.yaml @@ -0,0 +1,85 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/branches/main + response: + content: '{"name":"main","commit":{"sha":"38c2d0214f2a48c9212a140f5311977059a15b35","node_id":"C_kwDOJu7MkNoAKDM4YzJkMDIxNGYyYTQ4YzkyMTJhMTQwZjUzMTE5NzcwNTlhMTViMzU","commit":{"author":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"committer":{"name":"joseph-sentry","email":"joseph.sawaya@sentry.io","date":"2023-08-18T16:48:10Z"},"message":"make + change to app\n\nSigned-off-by: joseph-sentry ","tree":{"sha":"9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/trees/9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043"},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/git/commits/38c2d0214f2a48c9212a140f5311977059a15b35","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + SSH SIGNATURE-----\nU1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZAgzn4ZBqwutNsBP2EqViyU9PP\n/bJINxsDu+12+Wr9EAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5\nAAAAQJe3OYXkY7fA1+RZ130Zrab06lH36l2XC9HMqjHDAW8iCW5VUbvNhOxNpKSokDItdd\nroqDRnazyFj3+oWVCvxgM=\n-----END + SSH SIGNATURE-----","payload":"tree 9ba2c9dd228af0bbaf34cc8dcac85a1bdeed6043\nparent + 1713006782821e36912c23baa98901c7361ee83c\nauthor joseph-sentry + 1692377290 -0400\ncommitter joseph-sentry 1692377290 + -0400\n\nmake change to app\n\nSigned-off-by: joseph-sentry \n"}},"url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/38c2d0214f2a48c9212a140f5311977059a15b35","comments_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/38c2d0214f2a48c9212a140f5311977059a15b35/comments","author":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"committer":{"login":"joseph-sentry","id":136376984,"node_id":"U_kgDOCCDymA","avatar_url":"https://avatars.githubusercontent.com/u/136376984?v=4","gravatar_id":"","url":"https://api.github.com/users/joseph-sentry","html_url":"https://github.com/joseph-sentry","followers_url":"https://api.github.com/users/joseph-sentry/followers","following_url":"https://api.github.com/users/joseph-sentry/following{/other_user}","gists_url":"https://api.github.com/users/joseph-sentry/gists{/gist_id}","starred_url":"https://api.github.com/users/joseph-sentry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joseph-sentry/subscriptions","organizations_url":"https://api.github.com/users/joseph-sentry/orgs","repos_url":"https://api.github.com/users/joseph-sentry/repos","events_url":"https://api.github.com/users/joseph-sentry/events{/privacy}","received_events_url":"https://api.github.com/users/joseph-sentry/received_events","type":"User","site_admin":false},"parents":[{"sha":"1713006782821e36912c23baa98901c7361ee83c","url":"https://api.github.com/repos/joseph-sentry/codecov-demo/commits/1713006782821e36912c23baa98901c7361ee83c","html_url":"https://github.com/joseph-sentry/codecov-demo/commit/1713006782821e36912c23baa98901c7361ee83c"}]},"_links":{"self":"https://api.github.com/repos/joseph-sentry/codecov-demo/branches/main","html":"https://github.com/joseph-sentry/codecov-demo/tree/main"},"protected":false,"protection":{"enabled":false,"required_status_checks":{"enforcement_level":"off","contexts":[],"checks":[]}},"protection_url":"https://api.github.com/repos/joseph-sentry/codecov-demo/branches/main/protection"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 13:48:13 GMT + ETag: + - W/"a4b0d9bc04a6bc5e48bb347055a6db9e6bb3f10a38bccb0c21abb3b9d69b6ba5" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - F674:18BC:1B8CB12:37E2EC3:64EF489C + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1693406893' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-06 13:39:44 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_branch_not_existent.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_branch_not_existent.yaml new file mode 100644 index 0000000000..08e7c5ed5d --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_branch_not_existent.yaml @@ -0,0 +1,148 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/branches/none + response: + content: '{"message":"Branch not found","documentation_url":"https://docs.github.com/rest/branches/branches#get-a-branch"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 14:16:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FAF3:54E5:1518A8A:2AFCA6E:64EF4F48 + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4938' + X-RateLimit-Reset: + - '1693406893' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '62' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 14:15:09 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 404 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/joseph-sentry/codecov-demo/branches/none + response: + content: '{"message":"Branch not found","documentation_url":"https://docs.github.com/rest/branches/branches#get-a-branch"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 30 Aug 2023 14:41:53 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FEA3:62D0:150937E:2AE2E7A:64EF5531 + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4836' + X-RateLimit-Reset: + - '1693406893' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '164' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-29 14:40:07 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_branches.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_branches.yaml new file mode 100644 index 0000000000..a0b6ca6565 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_branches.yaml @@ -0,0 +1,75 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/branches?per_page=100&page=1 + response: + content: '[{"name":"main","commit":{"sha":"f0895290dc26668faeeb20ee5ccd4cc995925775","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775"},"protected":false,"protection":{"enabled":false,"required_status_checks":{"enforcement_level":"off","contexts":[]}},"protection_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches/main/protection"},{"name":"random-branch","commit":{"sha":"b12b8ba6046c1738c5c89325b8a63a6f51bc4ff6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b12b8ba6046c1738c5c89325b8a63a6f51bc4ff6"},"protected":false,"protection":{"enabled":false,"required_status_checks":{"enforcement_level":"off","contexts":[]}},"protection_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches/random-branch/protection"},{"name":"thiago/base-no-base","commit":{"sha":"75f355d8d14ba3d7761c728b4d2607cde0eef065","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/75f355d8d14ba3d7761c728b4d2607cde0eef065"},"protected":false,"protection":{"enabled":false,"required_status_checks":{"enforcement_level":"off","contexts":[]}},"protection_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches/thiago/base-no-base/protection"},{"name":"thiago/f/big-pt","commit":{"sha":"d55dc4ef748fd11537e50c9abed4ab1864fa1d94","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d55dc4ef748fd11537e50c9abed4ab1864fa1d94"},"protected":false,"protection":{"enabled":false,"required_status_checks":{"enforcement_level":"off","contexts":[]}},"protection_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches/thiago/f/big-pt/protection"},{"name":"thiago/f/something","commit":{"sha":"c9fb9262268f11b53c6b0682d4f9acac89bdeee5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c9fb9262268f11b53c6b0682d4f9acac89bdeee5"},"protected":false,"protection":{"enabled":false,"required_status_checks":{"enforcement_level":"off","contexts":[]}},"protection_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches/thiago/f/something/protection"},{"name":"thiago/test-1","commit":{"sha":"2e2600aa09525e2e1e1d98b09de61454d29c94bb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb"},"protected":false,"protection":{"enabled":false,"required_status_checks":{"enforcement_level":"off","contexts":[]}},"protection_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches/thiago/test-1/protection"}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:29 GMT + ETag: + - W/"494058c9831f575809ff109182a4ecc060d86ec266223d9b321459acf26ac31e" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFF0:48FC:1036FF1:1D06283:5F87362C + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4939' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '61' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit.yaml new file mode 100644 index 0000000000..a730d14ae6 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit.yaml @@ -0,0 +1,96 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b64 + response: + content: '{"sha":"6895b6479dbe12b5cb3baa02416c6343ddb888b4","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjY4OTViNjQ3OWRiZTEyYjVjYjNiYWEwMjQxNmM2MzQzZGRiODg4YjQ=","commit":{"author":{"name":"Jerrod","email":"jerrod@fundersclub.com","date":"2018-07-09T23:39:20Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2018-07-09T23:39:20Z"},"message":"Adding + ''include'' term if multiple sources\n\nbased on a support ticket around multiple + sources\r\n\r\nhttps://codecov.freshdesk.com/a/tickets/87","tree":{"sha":"3c47e2b9d9791503b56f0e4f78e76b9d061ad529","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3c47e2b9d9791503b56f0e4f78e76b9d061ad529"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJbQ/IoCRBK7hj4Ov3rIwAAdHIIAGm5AdlM8E0E7TyFKWgwPpjO\nsxiQswFXWosTZnJAn2NN/JF5aNqxUFLa9mo7Z+jztQuxrWsAFQsNFHf/t90iZi4w\ne0CkIHJdI8ukcae5/3eP+9h8GyqEq/RcvxYtvW6zYkWAK3Pyqwrs+qwH1MuLsl6E\n02fgD6T99Pq2V+3S1+dfgU6ot4IrMwT7aR+u9fCM8G4tF4y/5znIzuke6amVt52S\nUfjnHOHbDxdD4Mkxn8107zX1XmQ4BEzhh1kjTVd3Mean6ye7xsFxFGYHA5Zd1iyM\nCsmW5waqonRf03m1bQ9pYleufcwpr72iARLiBFhTOcAF6vpdoshO1qmTtsweFno=\n=vKnQ\n-----END + PGP SIGNATURE-----\n","payload":"tree 3c47e2b9d9791503b56f0e4f78e76b9d061ad529\nparent + adb252173d2107fad86bcdcbc149884c2dd4c609\nauthor Jerrod + 1531179560 -0700\ncommitter GitHub 1531179560 -0700\n\nAdding + ''include'' term if multiple sources\n\nbased on a support ticket around multiple + sources\r\n\r\nhttps://codecov.freshdesk.com/a/tickets/87"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6895b6479dbe12b5cb3baa02416c6343ddb888b4","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4/comments","author":null,"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars3.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"adb252173d2107fad86bcdcbc149884c2dd4c609","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609","html_url":"https://github.com/ThiagoCodecov/example-python/commit/adb252173d2107fad86bcdcbc149884c2dd4c609"}],"stats":{"total":9,"additions":8,"deletions":1},"files":[{"sha":"1fbfc366bd98e0c8df4fd297061a420b674857f4","filename":"README.rst","status":"modified","additions":8,"deletions":1,"changes":9,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/6895b6479dbe12b5cb3baa02416c6343ddb888b4/README.rst","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/6895b6479dbe12b5cb3baa02416c6343ddb888b4/README.rst","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.rst?ref=6895b6479dbe12b5cb3baa02416c6343ddb888b4","patch":"@@ + -47,12 +47,19 @@ Below are some examples on how to include coverage tracking + during your tests. C\n \n You may need to configure a ``.coveragerc`` file. + Learn more `here `_. + Start with this `generic .coveragerc `_ + for example.\n \n-We highly suggest adding `source` to your ``.coveragerc`` + which solves a number of issues collecting coverage.\n+We highly suggest adding + `source` to your ``.coveragerc``, which solves a number of issues collecting + coverage.\n \n .. code-block:: ini\n \n [run]\n source=your_package_name\n+ \n+If + there are multiple sources, you instead should add ''include'' to your ``.coveragerc``\n+\n+.. + code-block:: ini\n+\n+ [run]\n+ include=your_package_name/*\n \n unittests\n + ---------"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 21:34:52 GMT + ETag: + - W/"152c76ba58a92fc047b2ce3dedfb74c42b939c33844864e0d30bd97b4c7f54dd" + Last-Modified: + - Mon, 09 Jul 2018 23:39:20 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D7EE:13B2:46AEF:9F712:5F876EFC + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4997' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '3' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_diff.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_diff.yaml new file mode 100644 index 0000000000..67bee89d6e --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_diff.yaml @@ -0,0 +1,77 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/vnd.github.v3.diff + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/2be550c135cc13425cb2c239b9321e78dcfb787b + response: + content: "diff --git a/.travis.yml b/.travis.yml\nindex 0ee9617..11d295c 100644\n\ + --- a/.travis.yml\n+++ b/.travis.yml\n@@ -1,3 +1,5 @@\n+sudo: false\n+\n language:\ + \ python\n \n python:\n" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '165' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/vnd.github.v3.diff; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:16 GMT + ETag: + - '"2ddaeec7d4c849aea4ebcf3755de366021876ed7dba09b24ac9f7f945b5682e0"' + Last-Modified: + - Thu, 19 Nov 2015 21:09:03 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=diff + X-GitHub-Request-Id: + - CFE0:4251:3B0E5C4:61A8F64:5F873620 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4984' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '16' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_diff_not_found.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_diff_not_found.yaml new file mode 100644 index 0000000000..0ee58dbda0 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_diff_not_found.yaml @@ -0,0 +1,68 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/vnd.github.v3.diff + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/3be850c135ccaa425cb2c239b9321e78dcfb78ff + response: + content: '{"message":"No commit found for SHA: 3be850c135ccaa425cb2c239b9321e78dcfb78ff","documentation_url":"https://docs.github.com/rest/reference/repos#get-a-commit"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Content-Length: + - '159' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:17 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 422 Unprocessable Entity + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=diff + X-GitHub-Request-Id: + - CFE1:6D05:39E0383:614F23A:5F873621 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4983' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '17' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 422 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_diff_unicode_newline.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_diff_unicode_newline.yaml new file mode 100644 index 0000000000..78d217323c --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_diff_unicode_newline.yaml @@ -0,0 +1,77 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/vnd.github.v3.diff + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/2be550c135cc13425cb2c239b9321e78dcfb787b + response: + content: "diff --git a/.travis.yml b/.travis.yml\nindex 0ee9617..11d295c 100644\n\ + --- a/.travis.yml\n+++ b/.travis.yml\n@@ -1,3 +1,5 @@\n+sudo: false\n+\n language:\ + \ python\n \n python\u2028:\n" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '165' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/vnd.github.v3.diff; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:16 GMT + ETag: + - '"2ddaeec7d4c849aea4ebcf3755de366021876ed7dba09b24ac9f7f945b5682e0"' + Last-Modified: + - Thu, 19 Nov 2015 21:09:03 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=diff + X-GitHub-Request-Id: + - CFE0:4251:3B0E5C4:61A8F64:5F873620 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4984' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '16' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_no_permissions.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_no_permissions.yaml new file mode 100644 index 0000000000..6c9801c758 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_no_permissions.yaml @@ -0,0 +1,70 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/worker/commits/bbe3e94949d11471cc4e054f822d222254a4a4f8 + response: + content: '{"message":"Not Found","documentation_url":"https://docs.github.com/rest/reference/repos#get-a-commit"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:52:49 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 404 Not Found + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D0A0:35B8:226935:3B2836:5F873AF1 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1602701569' + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_not_found.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_not_found.yaml new file mode 100644 index 0000000000..d395c1bbae --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_not_found.yaml @@ -0,0 +1,68 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/abe3e94949d11471cc4e054f822d222254a4a4f8 + response: + content: '{"message":"No commit found for SHA: abe3e94949d11471cc4e054f822d222254a4a4f8","documentation_url":"https://docs.github.com/rest/reference/repos#get-a-commit"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Content-Length: + - '159' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:14 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 422 Unprocessable Entity + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFDD:10E6:3EDE705:6750D5F:5F87361E + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4985' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '15' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 422 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_repo_doesnt_exist.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_repo_doesnt_exist.yaml new file mode 100644 index 0000000000..3811c29c89 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_repo_doesnt_exist.yaml @@ -0,0 +1,70 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/badrepo/commits/bbe3e94949d11471cc4e054f822d222254a4a4f8 + response: + content: '{"message":"Not Found","documentation_url":"https://docs.github.com/rest/reference/repos#get-a-commit"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:54:19 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 404 Not Found + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D0A5:646D:7A43A8:15252D7:5F873B4B + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4903' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '97' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_statuses.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_statuses.yaml new file mode 100644 index 0000000000..203e07562a --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_statuses.yaml @@ -0,0 +1,82 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/worker/commits/3fb5f4700da7818e561054ec26f5657de720717f/status?page=1&per_page=100 + response: + content: '{"state":"success","statuses":[{"url":"https://api.github.com/repos/codecov/worker/statuses/3fb5f4700da7818e561054ec26f5657de720717f","avatar_url":"https://avatars2.githubusercontent.com/in/254?v=4","id":9315478770,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzE1NDc4Nzcw","state":"success","description":"94.21% + (+0.18%) compared to 48775c6","target_url":"https://codecov.io/gh/codecov/worker/compare/48775c672437630c9c6f582ecfae5854a3617be2...3fb5f4700da7818e561054ec26f5657de720717f","context":"codecov/project","created_at":"2020-04-08T05:44:02Z","updated_at":"2020-04-08T05:44:02Z"},{"url":"https://api.github.com/repos/codecov/worker/statuses/3fb5f4700da7818e561054ec26f5657de720717f","avatar_url":"https://avatars2.githubusercontent.com/in/254?v=4","id":9315478783,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzE1NDc4Nzgz","state":"success","description":"100.00% + of diff hit (target 94.02%)","target_url":"https://codecov.io/gh/codecov/worker/compare/48775c672437630c9c6f582ecfae5854a3617be2...3fb5f4700da7818e561054ec26f5657de720717f","context":"codecov/patch","created_at":"2020-04-08T05:44:02Z","updated_at":"2020-04-08T05:44:02Z"},{"url":"https://api.github.com/repos/codecov/worker/statuses/3fb5f4700da7818e561054ec26f5657de720717f","avatar_url":"https://avatars2.githubusercontent.com/oa/4808?v=4","id":9324134563,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MTM0NTYz","state":"success","description":"Your + tests passed on CircleCI!","target_url":"https://circleci.com/gh/codecov/worker/2619?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link","context":"ci/circleci: + build","created_at":"2020-04-08T20:39:33Z","updated_at":"2020-04-08T20:39:33Z"},{"url":"https://api.github.com/repos/codecov/worker/statuses/3fb5f4700da7818e561054ec26f5657de720717f","avatar_url":"https://avatars2.githubusercontent.com/oa/4808?v=4","id":9324144770,"node_id":"MDEzOlN0YXR1c0NvbnRleHQ5MzI0MTQ0Nzcw","state":"success","description":"Your + tests passed on CircleCI!","target_url":"https://circleci.com/gh/codecov/worker/2620?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link","context":"ci/circleci: + test","created_at":"2020-04-08T20:40:32Z","updated_at":"2020-04-08T20:40:32Z"}],"sha":"3fb5f4700da7818e561054ec26f5657de720717f","total_count":4,"repository":{"id":157271496,"node_id":"MDEwOlJlcG9zaXRvcnkxNTcyNzE0OTY=","name":"worker","full_name":"codecov/worker","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/worker","description":"Code + for Background Workers of Codecov","fork":false,"url":"https://api.github.com/repos/codecov/worker","forks_url":"https://api.github.com/repos/codecov/worker/forks","keys_url":"https://api.github.com/repos/codecov/worker/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/worker/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/worker/teams","hooks_url":"https://api.github.com/repos/codecov/worker/hooks","issue_events_url":"https://api.github.com/repos/codecov/worker/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/worker/events","assignees_url":"https://api.github.com/repos/codecov/worker/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/worker/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/worker/tags","blobs_url":"https://api.github.com/repos/codecov/worker/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/worker/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/worker/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/worker/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/worker/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/worker/languages","stargazers_url":"https://api.github.com/repos/codecov/worker/stargazers","contributors_url":"https://api.github.com/repos/codecov/worker/contributors","subscribers_url":"https://api.github.com/repos/codecov/worker/subscribers","subscription_url":"https://api.github.com/repos/codecov/worker/subscription","commits_url":"https://api.github.com/repos/codecov/worker/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/worker/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/worker/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/worker/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/worker/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/worker/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/worker/merges","archive_url":"https://api.github.com/repos/codecov/worker/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/worker/downloads","issues_url":"https://api.github.com/repos/codecov/worker/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/worker/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/worker/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/worker/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/worker/labels{/name}","releases_url":"https://api.github.com/repos/codecov/worker/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/worker/deployments"},"commit_url":"https://api.github.com/repos/codecov/worker/commits/3fb5f4700da7818e561054ec26f5657de720717f","url":"https://api.github.com/repos/codecov/worker/commits/3fb5f4700da7818e561054ec26f5657de720717f/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 21:39:42 GMT + ETag: + - W/"7499cd329dea0e9538995b592bb18d02540c95888f7bd50aea4ad48671bc6bdd" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo, repo:status + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D805:660E:42E10D:70C79A:5F87701E + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4995' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '5' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_with_proper_author.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_with_proper_author.yaml new file mode 100644 index 0000000000..6850f7c4ab --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_commit_with_proper_author.yaml @@ -0,0 +1,1277 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/75f355d8d14ba3d7761c728b4d2607cde0eef065 + response: + content: '{"sha":"75f355d8d14ba3d7761c728b4d2607cde0eef065","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojc1ZjM1NWQ4ZDE0YmEzZDc3NjFjNzI4YjRkMjYwN2NkZTBlZWYwNjU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-05-18T03:16:22Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-10-13T15:15:31Z"},"message":"Adding + README\n\nsurpriseaAKDS\n\nddkokgfnskfds\n\nBanana\n\nYallow\n\nABG","tree":{"sha":"b737740a931a34f5be73f553ea87a1161c917be0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b737740a931a34f5be73f553ea87a1161c917be0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/75f355d8d14ba3d7761c728b4d2607cde0eef065","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/75f355d8d14ba3d7761c728b4d2607cde0eef065","html_url":"https://github.com/ThiagoCodecov/example-python/commit/75f355d8d14ba3d7761c728b4d2607cde0eef065","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/75f355d8d14ba3d7761c728b4d2607cde0eef065/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f0895290dc26668faeeb20ee5ccd4cc995925775","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0895290dc26668faeeb20ee5ccd4cc995925775","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f0895290dc26668faeeb20ee5ccd4cc995925775"}],"stats":{"total":3593,"additions":38,"deletions":3555},"files":[{"sha":"1585e843d6b3bb9351592db62eddb2f0a0c4a484","filename":".gitignore","status":"modified","additions":5,"deletions":0,"changes":5,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/75f355d8d14ba3d7761c728b4d2607cde0eef065/.gitignore","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/75f355d8d14ba3d7761c728b4d2607cde0eef065/.gitignore","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.gitignore?ref=75f355d8d14ba3d7761c728b4d2607cde0eef065","patch":"@@ + -8,6 +8,11 @@ __pycache__/\n \n .coverage\n coverage.xml\n+flagone.coverage.xml\n+flagtwo.coverage.xml\n+unit.coverage.xml\n+dev.sh\n+direct.sh\n + \n # Distribution / packaging\n .Python"},{"sha":"0f9bd797f9e0a2be4f4e707d38c70553d2bb5ec8","filename":"Makefile","status":"modified","additions":30,"deletions":4,"changes":34,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/75f355d8d14ba3d7761c728b4d2607cde0eef065/Makefile","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/75f355d8d14ba3d7761c728b4d2607cde0eef065/Makefile","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/Makefile?ref=75f355d8d14ba3d7761c728b4d2607cde0eef065","patch":"@@ + -1,5 +1,6 @@\n dev_token = ${LOCAL_TOKEN}\n production_token = ${PRODUCTION_TOKEN}\n+staging_token + = ${STAGING_TOKEN}\n \n test:\n \trm coverage.xml || true\n@@ -12,16 +13,25 + @@ test.flagone:\n \tpython -m pytest --cov=./ tests/test_number_two.py --cov-report=xml:flagone.coverage.xml\n + \n dev.report:\n-\t./dev.sh -t ${dev_token} -F flagtwo -f flagtwo.coverage.xml\n+\t./dev.sh + -t ${dev_token} -F pt -f flagtwo.coverage.xml\n+\n+staging.report:\n+\t./production.sh + -t ${staging_token} -F pt -f flagtwo.coverage.xml -u https://stage-web.codecov.dev\n+\n+direct.report:\n+\t./direct.sh + -t ${dev_token} -F pt -f flagtwo.coverage.xml\n \n dev.report.flagone:\n-\t./dev.sh + -t ${dev_token} -F flagone -f flagone.coverage.xml\n+\t./dev.sh -t ${dev_token} + -F psdb -f flagone.coverage.xml\n \n production.report:\n-\t./production.sh + -t ${production_token} -F flagtwo -f flagtwo.coverage.xml\n+\t./production.sh + -t ${production_token} -F pt -f flagtwo.coverage.xml -b 123\n \n production.report.flagone:\n-\t./production.sh + -t ${production_token} -F flagone -f flagone.coverage.xml\n+\t./production.sh + -t ${production_token} -F psdb -f flagone.coverage.xml -b 654\n+\n+staging.report.flagone:\n+\t./production.sh + -t ${staging_token} -F psdb -f flagone.coverage.xml -u https://codecov.io\n + \n dev.full:\n \t${MAKE} dev.download\n@@ -35,6 +45,11 @@ dev.partial:\n \t${MAKE} + test\n \t${MAKE} dev.report\n \n+dev.flagone:\n+\t${MAKE} dev.download\n+\t${MAKE} + test.flagone\n+\t${MAKE} dev.report.flagone\n+\n production.full:\n \t${MAKE} + production.download\n \t${MAKE} test.flagone\n@@ -47,10 +62,21 @@ production.partial:\n + \t${MAKE} test\n \t${MAKE} production.report\n \n+staging.full:\n+\t${MAKE} + production.download\n+\t${MAKE} test.flagone\n+\t${MAKE} staging.report.flagone\n+\t${MAKE} + test\n+\t${MAKE} staging.report\n+\n dev.download:\n \tcurl -s http://localhost/bash + > dev.sh\n \tchmod +x ./dev.sh\n \n production.download:\n \tcurl -s https://codecov.io/bash + > production.sh\n \tchmod +x ./production.sh\n+\n+staging.download:\n+\tcurl + -s https://codecov.io/bash > staging.sh\n+\tchmod +x ./staging.sh"},{"sha":"f2a9f3e932bb13cb506de777ef2c0940feb9ee1c","filename":"README.md","status":"removed","additions":0,"deletions":74,"changes":74,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/f0895290dc26668faeeb20ee5ccd4cc995925775/README.md","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/f0895290dc26668faeeb20ee5ccd4cc995925775/README.md","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.md?ref=f0895290dc26668faeeb20ee5ccd4cc995925775","patch":"@@ + -1,74 +0,0 @@\n-Now this\n-704CE324-640A-4464-B8C3-E88AD3167C40\n-60C40C8C-0CC8-42EE-93C3-FFCDA777E328\n-83D8CF33-58C9-48A8-B013-D8D2A1B99FBA\n-094A8584-F366-47E6-8E86-4098F6450D8F\n-2FAFBE5F-E5D1-4D1E-AB4B-96700303585D\n-C8292636-FBBC-4F57-B94B-A74B779C99CA\n-663EB689-A841-40FC-B5C5-7153692D5CC4\n-053DE902-60F7-486F-B3B0-12FD053FF137\n-521407A7-B324-4D2D-89EE-6370D605B917\n-C2B1DDBC-DCBD-4D21-B44D-7E01277DA64F\n-20EE4D09-F49E-474B-B7AF-4E4916EF82FA\n-E0637A75-CED2-48C1-AE34-A1FAB6AAEF8A\n-888C5E8A-793B-4BF7-8E40-AC15CDCE314F\n-A6D5604D-6AB4-4E3F-9636-CDA27CD5B0E9\n-8C93D40F-9BD0-4C70-A474-3DE83B491330\n-5609B3F3-74E2-42AE-B298-51B8280371C6\n-C1B0D231-650E-4BA2-A5F1-A6EDB515B073\n-1ED35DC7-4E13-4F33-AD4C-658EB45CD85B\n-286EEB83-BD2B-40FE-9AF2-4EBA81297A48\n-1AACE5C5-E03B-40D9-AA32-375B0CB63865\n-E9CF0F03-6B73-4643-BD9F-10B7A037BEF3\n-A39C2BB3-44ED-4B62-A177-0AA29595BC99\n-95763FF2-95F9-45AB-B390-283A1975E8F0\n-A0192C74-D04F-49F0-8DB7-5AD6A575914F\n-41670326-FBE3-4E67-AAD1-29CA48E18659\n-1A61D4F1-C79F-4D29-BD9B-559B7FB17063\n-BBB63289-5667-4FAE-A403-A0CA61F1DC7C\n-51A26161-0F2C-4A10-A0DF-34F9600C8E8B\n-909720D6-CC5A-4F70-ACB1-3B13C22756F5\n-CE6BF3DF-D0D4-4899-A951-29F7EFAAF4D7\n-BA5677D4-EE49-4C2F-817E-BE26542063F5\n-02BD4F67-4902-4AB7-966D-F24C69A29303\n-42E5299B-6270-4259-BDBF-8E73A9784B4D\n-3E2B9185-DDB0-4683-8B75-A684C27EC113\n-8406E591-E8CE-40E8-A37C-D0A12114B88B\n-4E2CEA68-D2C0-4AE9-B91E-0F2EC4542918\n-B644C629-1C4D-4627-9DF0-DE9385CEAD71\n-27C6788F-0CAE-4EB1-B8F3-29857EC3FB2A\n-D5218308-1E5C-411E-9BB4-2A3D053D2EF9\n-7828F0A1-DF19-41D0-855C-F704C9F19ECC\n-C1F02001-117D-4EFB-B8C2-B6C6C474BD79\n-67CD977A-D6CB-4248-8528-3E7A340154C6\n-6663A943-C39A-4F60-AC84-D917A39EC74A\n-9A742E42-741A-4C46-87B6-EF8FD0FA8DE7\n-768A0A2C-4E79-4BB5-9815-C45316D88265\n-F0F82276-44D0-4A89-92D0-32BB652160EB\n-36BED83E-FC17-46A2-825D-BCCB5783ECE9\n-8AAB2B79-2543-4769-B4E5-C7F8DF996128\n-BC8B58F8-D84F-41DA-8411-D145DFBBCDF7\n-90E63F6E-2D82-4C95-B711-55F48F203062\n-6F2AA94B-B8B7-4E5D-89F2-4B781B58FE20\n-49C38017-A85B-4A35-9891-A9A8C929EBD1\n-75FF7739-474E-4F40-A1F5-DA644247777C\n-D91343F0-6020-451C-AE34-AE13C54768B7\n-86238B48-1C1A-404D-84F6-540BA12026D0\n-96F6A299-FA1B-40E7-97BD-BB78805B17E4\n-76E3FB43-76B6-4ED3-9315-818C4BB073D2\n-7574F3EA-731E-4605-A7E7-31DC93690FF5\n-7BBD2F88-35FB-4788-80DA-1A253AD2570B\n-EAF7489D-C2DD-4CB4-B53D-DD7781CE329E\n-F904B847-3311-42AC-A0A0-36A40EA7D488\n-E97F66AF-BBF5-47E8-8576-9148110A1E57\n-63C8D1C3-703F-40A1-8F7B-580A4D85D632\n-C989EE78-765E-479B-B8D5-8EDF99EAB6D0\n-F205752F-E9E5-4B41-8334-8366A8F44EA6\n-23F7D7DC-F29C-47A5-BEA9-D2C600255F32\n-FCFA1979-26B8-4048-AF74-FE6DA53C96B6\n-5B7972E5-0E8A-4134-B6F4-6AD44FE97085\n-44521915-9E24-4E78-A553-8696280B1CE2\n-EC5B1F9F-2293-4947-8E01-62F6F828E139\n-A8FC2A63-27CD-4C2F-868C-28F728EB8EEF\n-8E855C85-E884-4187-A673-0D77F3F379A1\n-182B7277-7D2C-420B-B005-92418CBD6F09"},{"sha":"622f906eac3a67c6f77f3a96149c555651ba6ac3","filename":"changed_production.sh","status":"removed","additions":0,"deletions":1660,"changes":1660,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/f0895290dc26668faeeb20ee5ccd4cc995925775/changed_production.sh","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/f0895290dc26668faeeb20ee5ccd4cc995925775/changed_production.sh","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/changed_production.sh?ref=f0895290dc26668faeeb20ee5ccd4cc995925775","patch":"@@ + -1,1660 +0,0 @@\n-#!/usr/bin/env bash\n-\n-# Apache License Version 2.0, January + 2004\n-# https://github.com/codecov/codecov-bash/blob/main/LICENSE\n-\n-\n-set + -e +o pipefail\n-\n-VERSION=\"20191211-b8db533\"\n-\n-url=\"https://codecov.io\"\n-env=\"$CODECOV_ENV\"\n-service=\"\"\n-token=\"\"\n-search_in=\"\"\n-flags=\"\"\n-exit_with=0\n-curlargs=\"\"\n-curlawsargs=\"\"\n-dump=\"0\"\n-clean=\"0\"\n-curl_s=\"-s\"\n-name=\"$CODECOV_NAME\"\n-include_cov=\"\"\n-exclude_cov=\"\"\n-ddp=\"$(echo + ~)/Library/Developer/Xcode/DerivedData\"\n-xp=\"\"\n-files=\"\"\n-cacert=\"$CODECOV_CA_BUNDLE\"\n-gcov_ignore=\"-not + -path ''./bower_components/**'' -not -path ''./node_modules/**'' -not -path + ''./vendor/**''\"\n-gcov_include=\"\"\n-\n-ft_gcov=\"1\"\n-ft_coveragepy=\"1\"\n-ft_fix=\"1\"\n-ft_search=\"1\"\n-ft_s3=\"1\"\n-ft_network=\"1\"\n-ft_xcodellvm=\"1\"\n-ft_xcodeplist=\"0\"\n-ft_gcovout=\"1\"\n-\n-_git_root=$(git + rev-parse --show-toplevel 2>/dev/null || hg root 2>/dev/null || echo $PWD)\n-git_root=\"$_git_root\"\n-codecov_yml=\"\"\n-remote_addr=\"\"\n-if + [ \"$git_root\" = \"$PWD\" ];\n-then\n- git_root=\".\"\n-fi\n-\n-url_o=\"\"\n-pr_o=\"\"\n-build_o=\"\"\n-commit_o=\"\"\n-search_in_o=\"\"\n-tag_o=\"\"\n-branch_o=\"\"\n-slug_o=\"\"\n-prefix_o=\"\"\n-\n-commit=\"$VCS_COMMIT_ID\"\n-branch=\"$VCS_BRANCH_NAME\"\n-pr=\"$VCS_PULL_REQUEST\"\n-slug=\"$VCS_SLUG\"\n-tag=\"$VCS_TAG\"\n-build_url=\"$CI_BUILD_URL\"\n-build=\"$CI_BUILD_ID\"\n-job=\"$CI_JOB_ID\"\n-\n-beta_xcode_partials=\"\"\n-\n-proj_root=\"$git_root\"\n-gcov_exe=\"gcov\"\n-gcov_arg=\"\"\n-\n-b=\"\\033[0;36m\"\n-g=\"\\033[0;32m\"\n-r=\"\\033[0;31m\"\n-e=\"\\033[0;90m\"\n-x=\"\\033[0m\"\n-\n-show_help() + {\n-cat << EOF\n-\n- Codecov Bash $VERSION\n-\n- Global + report uploading tool for Codecov\n- Documentation at https://docs.codecov.io/docs\n- Contribute + at https://github.com/codecov/codecov-bash\n-\n-\n- -h Display this + help and exit\n- -f FILE Target file(s) to upload\n-\n- -f + \"path/to/file\" only upload this file\n- skips + searching unless provided patterns below\n-\n- -f ''!*.bar'' ignore + all files at pattern *.bar\n- -f ''*.foo'' include + all files at pattern *.foo\n- Must use single quotes.\n- This + is non-exclusive, use -s \"*.foo\" to match specific paths.\n-\n- -s DIR Directory + to search for coverage reports.\n- Already searches project + root and artifact folders.\n- -t TOKEN Set the private repository token\n- (option) + set environment variable CODECOV_TOKEN=:uuid\n-\n- -t @/path/to/token_file\n- -t + uuid\n-\n- -n NAME Custom defined name of the upload. Visible in Codecov + UI\n-\n- -e ENV Specify environment variables to be included with this + build\n- Also accepting environment variables: CODECOV_ENV=VAR,VAR2\n-\n- -e + VAR,VAR2\n-\n- -X feature Toggle functionalities\n-\n- -X + gcov Disable gcov\n- -X coveragepy Disable python + coverage\n- -X fix Disable report fixing\n- -X + search Disable searching for reports\n- -X xcode Disable + xcode processing\n- -X network Disable uploading the file + network\n- -X gcovout Disable gcov output\n-\n- -N The + commit SHA of the parent for which you are uploading coverage. If not present,\n- the + parent will be determined using the API of your repository provider.\n- When + using the repository provider''s API, the parent is determined via finding\n- the + closest ancestor to the commit.\n-\n- -R root dir Used when not in git/hg + project to identify project root directory\n- -y conf file Used to specify + the location of the .codecov.yml config file\n- -F flag Flag the upload + to group coverage metrics\n-\n- -F unittests This upload + is only unittests\n- -F integration This upload is only + integration tests\n- -F ui,chrome This upload is Chrome + - UI tests\n-\n- -c Move discovered coverage reports to the trash\n- -Z Exit + with 1 if not successful. Default will Exit with 0\n-\n- -- xcode --\n- -D Custom + Derived Data Path for Coverage.profdata and gcov processing\n- Default + ''~/Library/Developer/Xcode/DerivedData''\n- -J Specify packages + to build coverage.\n- This can significantly reduces time to + build coverage reports.\n-\n- -J ''MyAppName'' Will match + \"MyAppName\" and \"MyAppNameTests\"\n- -J ''^ExampleApp$'' Will + match only \"ExampleApp\" not \"ExampleAppTests\"\n-\n- -- gcov --\n- -g + GLOB Paths to ignore during gcov gathering\n- -G GLOB Paths to + include during gcov gathering\n- -p dir Project root directory\n- Also + used when preparing gcov\n- -k prefix Prefix filepaths to help resolve + path fixing: https://github.com/codecov/support/issues/472\n- -x gcovexe gcov + executable to run. Defaults to ''gcov''\n- -a gcovargs extra arguments to + pass to gcov\n-\n- -- Override CI Environment Variables --\n- These + variables are automatically detected by popular CI providers\n-\n- -B branch Specify + the branch name\n- -C sha Specify the commit sha\n- -P pr Specify + the pull request number\n- -b build Specify the build number\n- -T + tag Specify the git tag\n-\n- -- Enterprise --\n- -u URL Set + the target url for Enterprise customers\n- Not required when + retrieving the bash uploader from your CCE\n- (option) Set environment + variable CODECOV_URL=https://my-hosted-codecov.com\n- -r SLUG owner/repo + slug used instead of the private repo token in Enterprise\n- (option) + set environment variable CODECOV_SLUG=:owner/:repo\n- (option) + set in your codecov.yml \"codecov.slug\"\n- -S PATH File path to your + cacert.pem file used to verify ssl with Codecov Enterprise (optional)\n- (option) + Set environment variable: CODECOV_CA_BUNDLE=\"/path/to/ca.pem\"\n- -U curlargs Extra + curl arguments to communicate with Codecov. e.g., -U \"--proxy http://http-proxy\"\n- -A + curlargs Extra curl arguments to communicate with AWS.\n-\n- -- Debugging + --\n- -d Don''t upload, but dump upload file to stdout\n- -K Remove + color from the output\n- -v Verbose mode\n-\n-EOF\n-}\n-\n-\n-say() + {\n- echo -e \"$1\"\n-}\n-\n-\n-urlencode() {\n- echo \"$1\" | curl -Gso /dev/null + -w %{url_effective} --data-urlencode @- \"\" | cut -c 3- | sed -e ''s/%0A//''\n-}\n-\n-\n-swiftcov() + {\n- _dir=$(dirname \"$1\" | sed ''s/\\(Build\\).*/\\1/g'')\n- for _type in + app framework xctest\n- do\n- find \"$_dir\" -name \"*.$_type\" | while + read f\n- do\n- _proj=${f##*/}\n- _proj=${_proj%.\"$_type\"}\n- if + [ \"$2\" = \"\" ] || [ \"$(echo \"$_proj\" | grep -i \"$2\")\" != \"\" ];\n- then\n- say + \" $g+$x Building reports for $_proj $_type\"\n- dest=$([ -f \"$f/$_proj\" + ] && echo \"$f/$_proj\" || echo \"$f/Contents/MacOS/$_proj\")\n- _proj_name=$(echo + \"$_proj\" | sed -e ''s/[[:space:]]//g'')\n- xcrun llvm-cov show $beta_xcode_partials + -instr-profile \"$1\" \"$dest\" > \"$_proj_name.$_type.coverage.txt\" \\\n- || + say \" ${r}x>${x} llvm-cov failed to produce results for $dest\"\n- fi\n- done\n- done\n-}\n-\n-\n-# + Credits to: https://gist.github.com/pkuczynski/8665367\n-parse_yaml() {\n- local + prefix=$2\n- local s=''[[:space:]]*'' w=''[a-zA-Z0-9_]*'' fs=$(echo @|tr @ + ''\\034'')\n- sed -ne \"s|^\\($s\\)\\($w\\)$s:$s\\\"\\(.*\\)\\\"$s\\$|\\1$fs\\2$fs\\3|p\" + \\\n- -e \"s|^\\($s\\)\\($w\\)$s:$s\\(.*\\)$s\\$|\\1$fs\\2$fs\\3|p\" + $1 |\n- awk -F$fs ''{\n- indent = length($1)/2;\n- vname[indent] + = $2;\n- for (i in vname) {if (i > indent) {delete vname[i]}}\n- if + (length($3) > 0) {\n- vn=\"\"; if (indent > 0) {vn=(vn)(vname[0])(\"_\")}\n- printf(\"%s%s%s=\\\"%s\\\"\\n\", + \"''$prefix''\",vn, $2, $3);\n- }\n- }''\n-}\n-\n-\n-if [ $# != 0 ];\n-then\n- while + getopts \"a:A:b:B:cC:dD:e:f:F:g:G:hJ:k:Kn:p:P:r:R:y:s:S:t:T:u:U:vx:X:ZN:\" o\n- do\n- case + \"$o\" in\n- \"N\")\n- parent=$OPTARG\n- ;;\n- \"a\")\n- gcov_arg=$OPTARG\n- ;;\n- \"A\")\n- curlawsargs=\"$OPTARG\"\n- ;;\n- \"b\")\n- build_o=\"$OPTARG\"\n- ;;\n- \"B\")\n- branch_o=\"$OPTARG\"\n- ;;\n- \"c\")\n- clean=\"1\"\n- ;;\n- \"C\")\n- commit_o=\"$OPTARG\"\n- ;;\n- \"d\")\n- dump=\"1\"\n- ;;\n- \"D\")\n- ddp=\"$OPTARG\"\n- ;;\n- \"e\")\n- env=\"$env,$OPTARG\"\n- ;;\n- \"f\")\n- if + [ \"${OPTARG::1}\" = \"!\" ];\n- then\n- exclude_cov=\"$exclude_cov + -not -path ''${OPTARG:1}''\"\n-\n- elif [[ \"$OPTARG\" = *\"*\"* ]];\n- then\n- include_cov=\"$include_cov + -or -name ''$OPTARG''\"\n-\n- else\n- ft_search=0\n- if + [ \"$files\" = \"\" ];\n- then\n- files=\"$OPTARG\"\n- else\n- files=\"$files\n-$OPTARG\"\n- fi\n- fi\n- ;;\n- \"F\")\n- if + [ \"$flags\" = \"\" ];\n- then\n- flags=\"$OPTARG\"\n- else\n- flags=\"$flags,$OPTARG\"\n- fi\n- ;;\n- \"g\")\n- gcov_ignore=\"$gcov_ignore + -not -path ''$OPTARG''\"\n- ;;\n- \"G\")\n- gcov_include=\"$gcov_include + -path ''$OPTARG''\"\n- ;;\n- \"h\")\n- show_help\n- exit + 0;\n- ;;\n- \"J\")\n- ft_xcodellvm=\"1\"\n- ft_xcodeplist=\"0\"\n- if + [ \"$xp\" = \"\" ];\n- then\n- xp=\"$OPTARG\"\n- else\n- xp=\"$xp\\|$OPTARG\"\n- fi\n- ;;\n- \"k\")\n- prefix_o=$(echo + \"$OPTARG\" | sed -e ''s:^/*::'' -e ''s:/*$::'')\n- ;;\n- \"K\")\n- b=\"\"\n- g=\"\"\n- r=\"\"\n- e=\"\"\n- x=\"\"\n- ;;\n- \"n\")\n- name=\"$OPTARG\"\n- ;;\n- \"p\")\n- proj_root=\"$OPTARG\"\n- ;;\n- \"P\")\n- pr_o=\"$OPTARG\"\n- ;;\n- \"r\")\n- slug_o=\"$OPTARG\"\n- ;;\n- \"R\")\n- git_root=\"$OPTARG\"\n- ;;\n- \"s\")\n- if + [ \"$search_in_o\" = \"\" ];\n- then\n- search_in_o=\"$OPTARG\"\n- else\n- search_in_o=\"$search_in_o + $OPTARG\"\n- fi\n- ;;\n- \"S\")\n- cacert=\"--cacert + \\\"$OPTARG\\\"\"\n- ;;\n- \"t\")\n- if [ \"${OPTARG::1}\" + = \"@\" ];\n- then\n- token=$(cat \"${OPTARG:1}\" | tr -d '' + \\n'')\n- else\n- token=\"$OPTARG\"\n- fi\n- ;;\n- \"T\")\n- tag_o=\"$OPTARG\"\n- ;;\n- \"u\")\n- url_o=$(echo + \"$OPTARG\" | sed -e ''s/\\/$//'')\n- ;;\n- \"U\")\n- curlargs=\"$OPTARG\"\n- ;;\n- \"v\")\n- set + -x\n- curl_s=\"\"\n- ;;\n- \"x\")\n- gcov_exe=$OPTARG\n- ;;\n- \"X\")\n- if + [ \"$OPTARG\" = \"gcov\" ];\n- then\n- ft_gcov=\"0\"\n- elif + [ \"$OPTARG\" = \"coveragepy\" ] || [ \"$OPTARG\" = \"py\" ];\n- then\n- ft_coveragepy=\"0\"\n- elif + [ \"$OPTARG\" = \"gcovout\" ];\n- then\n- ft_gcovout=\"0\"\n- elif + [ \"$OPTARG\" = \"xcodellvm\" ];\n- then\n- ft_xcodellvm=\"1\"\n- ft_xcodeplist=\"0\"\n- elif + [ \"$OPTARG\" = \"fix\" ] || [ \"$OPTARG\" = \"fixes\" ];\n- then\n- ft_fix=\"0\"\n- elif + [ \"$OPTARG\" = \"xcode\" ];\n- then\n- ft_xcodellvm=\"0\"\n- ft_xcodeplist=\"0\"\n- elif + [ \"$OPTARG\" = \"search\" ];\n- then\n- ft_search=\"0\"\n- elif + [ \"$OPTARG\" = \"xcodepartials\" ];\n- then\n- beta_xcode_partials=\"-use-color\"\n- elif + [ \"$OPTARG\" = \"network\" ];\n- then\n- ft_network=\"0\"\n- elif + [ \"$OPTARG\" = \"s3\" ];\n- then\n- ft_s3=\"0\"\n- fi\n- ;;\n- \"y\")\n- codecov_yml=\"$OPTARG\"\n- ;;\n- \"Z\")\n- exit_with=1\n- ;;\n- esac\n- done\n-fi\n-\n-say + \"\n- _____ _\n- / ____| | |\n-| | ___ __| | ___ ___ + _____ __\n-| | / _ \\\\ / _\\` |/ _ \\\\/ __/ _ \\\\ \\\\ / /\n-| |___| + (_) | (_| | __/ (_| (_) \\\\ V /\n- \\\\_____\\\\___/ \\\\__,_|\\\\___|\\\\___\\\\___/ + \\\\_/\n- Bash-$VERSION\n-\n-\"\n-\n-search_in=\"$proj_root\"\n-\n-if + [ \"$JENKINS_URL\" != \"\" ];\n-then\n- say \"$e==>$x Jenkins CI detected.\"\n- # + https://wiki.jenkins-ci.org/display/JENKINS/Building+a+software+project\n- # + https://wiki.jenkins-ci.org/display/JENKINS/GitHub+pull+request+builder+plugin#GitHubpullrequestbuilderplugin-EnvironmentVariables\n- service=\"jenkins\"\n-\n- if + [ \"$ghprbSourceBranch\" != \"\" ];\n- then\n- branch=\"$ghprbSourceBranch\"\n- elif + [ \"$GIT_BRANCH\" != \"\" ];\n- then\n- branch=\"$GIT_BRANCH\"\n- elif + [ \"$BRANCH_NAME\" != \"\" ];\n- then\n- branch=\"$BRANCH_NAME\"\n- fi\n-\n- if + [ \"$ghprbActualCommit\" != \"\" ];\n- then\n- commit=\"$ghprbActualCommit\"\n- elif + [ \"$GIT_COMMIT\" != \"\" ];\n- then\n- commit=\"$GIT_COMMIT\"\n- fi\n-\n- if + [ \"$ghprbPullId\" != \"\" ];\n- then\n- pr=\"$ghprbPullId\"\n- elif [ + \"$CHANGE_ID\" != \"\" ];\n- then\n- pr=\"$CHANGE_ID\"\n- fi\n-\n- build=\"$BUILD_NUMBER\"\n- build_url=$(urlencode + \"$BUILD_URL\")\n-\n-elif [ \"$CI\" = \"true\" ] && [ \"$TRAVIS\" = \"true\" + ] && [ \"$SHIPPABLE\" != \"true\" ];\n-then\n- say \"$e==>$x Travis CI detected.\"\n- # + https://docs.travis-ci.com/user/environment-variables/\n- service=\"travis\"\n- commit=\"${TRAVIS_PULL_REQUEST_SHA:-$TRAVIS_COMMIT}\"\n- build=\"$TRAVIS_JOB_NUMBER\"\n- pr=\"$TRAVIS_PULL_REQUEST\"\n- job=\"$TRAVIS_JOB_ID\"\n- slug=\"$TRAVIS_REPO_SLUG\"\n- env=\"$env,TRAVIS_OS_NAME\"\n- tag=\"$TRAVIS_TAG\"\n- if + [ \"$TRAVIS_BRANCH\" != \"$TRAVIS_TAG\" ];\n- then\n- branch=\"$TRAVIS_BRANCH\"\n- fi\n-\n- language=$(compgen + -A variable | grep \"^TRAVIS_.*_VERSION$\" | head -1)\n- if [ \"$language\" + != \"\" ];\n- then\n- env=\"$env,${!language}\"\n- fi\n-\n-elif [ \"$CODEBUILD_BUILD_ARN\" + != \"\" ];\n-then\n- say \"$e==>$x AWS Codebuild detected.\"\n- # https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html\n- service=\"codebuild\"\n- commit=\"$CODEBUILD_RESOLVED_SOURCE_VERSION\"\n- build=\"$CODEBUILD_BUILD_ID\"\n- branch=\"$(echo + $CODEBUILD_WEBHOOK_HEAD_REF | sed ''s/^refs\\/heads\\///'')\"\n- if [ \"${CODEBUILD_SOURCE_VERSION/pr}\" + = \"$CODEBUILD_SOURCE_VERSION\" ] ; then\n- pr=\"false\"\n- else\n- pr=\"$(echo + $CODEBUILD_SOURCE_VERSION | sed ''s/^pr\\///'')\"\n- fi\n- job=\"$CODEBUILD_BUILD_ID\"\n- slug=\"$(echo + $CODEBUILD_SOURCE_REPO_URL | sed ''s/^.*github.com\\///'' | sed ''s/\\.git$//'')\"\n-\n-elif + [ \"$DOCKER_REPO\" != \"\" ];\n-then\n- say \"$e==>$x Docker detected.\"\n- # + https://docs.docker.com/docker-cloud/builds/advanced/\n- service=\"docker\"\n- branch=\"$SOURCE_BRANCH\"\n- commit=\"$SOURCE_COMMIT\"\n- slug=\"$DOCKER_REPO\"\n- tag=\"$CACHE_TAG\"\n- env=\"$env,IMAGE_NAME\"\n-\n-elif + [ \"$CI\" = \"true\" ] && [ \"$CI_NAME\" = \"codeship\" ];\n-then\n- say \"$e==>$x + Codeship CI detected.\"\n- # https://www.codeship.io/documentation/continuous-integration/set-environment-variables/\n- service=\"codeship\"\n- branch=\"$CI_BRANCH\"\n- build=\"$CI_BUILD_NUMBER\"\n- build_url=$(urlencode + \"$CI_BUILD_URL\")\n- commit=\"$CI_COMMIT_ID\"\n-\n-elif [ ! -z \"$CF_BUILD_URL\" + ] && [ ! -z \"$CF_BUILD_ID\" ];\n-then\n- say \"$e==>$x Codefresh CI detected.\"\n- # + https://docs.codefresh.io/v1.0/docs/variables\n- service=\"codefresh\"\n- branch=\"$CF_BRANCH\"\n- build=\"$CF_BUILD_ID\"\n- build_url=$(urlencode + \"$CF_BUILD_URL\")\n- commit=\"$CF_REVISION\"\n-\n-elif [ \"$TEAMCITY_VERSION\" + != \"\" ];\n-then\n- say \"$e==>$x TeamCity CI detected.\"\n- # https://confluence.jetbrains.com/display/TCD8/Predefined+Build+Parameters\n- # + https://confluence.jetbrains.com/plugins/servlet/mobile#content/view/74847298\n- if + [ \"$TEAMCITY_BUILD_BRANCH\" = '''' ];\n- then\n- echo \" Teamcity does + not automatically make build parameters available as environment variables.\"\n- echo + \" Add the following environment parameters to the build configuration\"\n- echo + \" env.TEAMCITY_BUILD_BRANCH = %teamcity.build.branch%\"\n- echo \" env.TEAMCITY_BUILD_ID + = %teamcity.build.id%\"\n- echo \" env.TEAMCITY_BUILD_URL = %teamcity.serverUrl%/viewLog.html?buildId=%teamcity.build.id%\"\n- echo + \" env.TEAMCITY_BUILD_COMMIT = %system.build.vcs.number%\"\n- echo \" env.TEAMCITY_BUILD_REPOSITORY + = %vcsroot..url%\"\n- fi\n- service=\"teamcity\"\n- branch=\"$TEAMCITY_BUILD_BRANCH\"\n- build=\"$TEAMCITY_BUILD_ID\"\n- build_url=$(urlencode + \"$TEAMCITY_BUILD_URL\")\n- if [ \"$TEAMCITY_BUILD_COMMIT\" != \"\" ];\n- then\n- commit=\"$TEAMCITY_BUILD_COMMIT\"\n- else\n- commit=\"$BUILD_VCS_NUMBER\"\n- fi\n- remote_addr=\"$TEAMCITY_BUILD_REPOSITORY\"\n-\n-elif + [ \"$CI\" = \"true\" ] && [ \"$CIRCLECI\" = \"true\" ];\n-then\n- say \"$e==>$x + Circle CI detected.\"\n- # https://circleci.com/docs/environment-variables\n- service=\"circleci\"\n- branch=\"$CIRCLE_BRANCH\"\n- build=\"$CIRCLE_BUILD_NUM\"\n- job=\"$CIRCLE_NODE_INDEX\"\n- if + [ \"$CIRCLE_PROJECT_REPONAME\" != \"\" ];\n- then\n- slug=\"$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME\"\n- else\n- # + git@github.com:owner/repo.git\n- slug=\"${CIRCLE_REPOSITORY_URL##*:}\"\n- # + owner/repo.git\n- slug=\"${slug%%.git}\"\n- fi\n- pr=\"$CIRCLE_PR_NUMBER\"\n- commit=\"$CIRCLE_SHA1\"\n- search_in=\"$search_in + $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS\"\n-\n-elif [ \"$BUDDYBUILD_BRANCH\" + != \"\" ];\n-then\n- say \"$e==>$x buddybuild detected\"\n- # http://docs.buddybuild.com/v6/docs/custom-prebuild-and-postbuild-steps\n- service=\"buddybuild\"\n- branch=\"$BUDDYBUILD_BRANCH\"\n- build=\"$BUDDYBUILD_BUILD_NUMBER\"\n- build_url=\"https://dashboard.buddybuild.com/public/apps/$BUDDYBUILD_APP_ID/build/$BUDDYBUILD_BUILD_ID\"\n- # + BUDDYBUILD_TRIGGERED_BY\n- if [ \"$ddp\" = \"$(echo ~)/Library/Developer/Xcode/DerivedData\" + ];\n- then\n- ddp=\"/private/tmp/sandbox/${BUDDYBUILD_APP_ID}/bbtest\"\n- fi\n-\n-elif + [ \"${bamboo_planRepository_revision}\" != \"\" ];\n-then\n- say \"$e==>$x + Bamboo detected\"\n- # https://confluence.atlassian.com/bamboo/bamboo-variables-289277087.html#Bamboovariables-Build-specificvariables\n- service=\"bamboo\"\n- commit=\"${bamboo_planRepository_revision}\"\n- branch=\"${bamboo_planRepository_branch}\"\n- build=\"${bamboo_buildNumber}\"\n- build_url=\"${bamboo_buildResultsUrl}\"\n- remote_addr=\"${bamboo_planRepository_repositoryUrl}\"\n-\n-elif + [ \"$CI\" = \"true\" ] && [ \"$BITRISE_IO\" = \"true\" ];\n-then\n- # http://devcenter.bitrise.io/faq/available-environment-variables/\n- say + \"$e==>$x Bitrise CI detected.\"\n- service=\"bitrise\"\n- branch=\"$BITRISE_GIT_BRANCH\"\n- build=\"$BITRISE_BUILD_NUMBER\"\n- build_url=$(urlencode + \"$BITRISE_BUILD_URL\")\n- pr=\"$BITRISE_PULL_REQUEST\"\n- if [ \"$GIT_CLONE_COMMIT_HASH\" + != \"\" ];\n- then\n- commit=\"$GIT_CLONE_COMMIT_HASH\"\n- fi\n-\n-elif + [ \"$CI\" = \"true\" ] && [ \"$SEMAPHORE\" = \"true\" ];\n-then\n- say \"$e==>$x + Semaphore CI detected.\"\n- # https://semaphoreapp.com/docs/available-environment-variables.html\n- service=\"semaphore\"\n- branch=\"$BRANCH_NAME\"\n- build=\"$SEMAPHORE_BUILD_NUMBER\"\n- job=\"$SEMAPHORE_CURRENT_THREAD\"\n- pr=\"$PULL_REQUEST_NUMBER\"\n- slug=\"$SEMAPHORE_REPO_SLUG\"\n- commit=\"$REVISION\"\n- env=\"$env,SEMAPHORE_TRIGGER_SOURCE\"\n-\n-elif + [ \"$CI\" = \"true\" ] && [ \"$BUILDKITE\" = \"true\" ];\n-then\n- say \"$e==>$x + Buildkite CI detected.\"\n- # https://buildkite.com/docs/guides/environment-variables\n- service=\"buildkite\"\n- branch=\"$BUILDKITE_BRANCH\"\n- build=\"$BUILDKITE_BUILD_NUMBER\"\n- job=\"$BUILDKITE_JOB_ID\"\n- build_url=$(urlencode + \"$BUILDKITE_BUILD_URL\")\n- slug=\"$BUILDKITE_PROJECT_SLUG\"\n- commit=\"$BUILDKITE_COMMIT\"\n- if + [[ \"$BUILDKITE_PULL_REQUEST\" != \"false\" ]]; then\n- pr=\"$BUILDKITE_PULL_REQUEST\"\n- fi\n- tag=\"$BUILDKITE_TAG\"\n-\n-elif + [ \"$CI\" = \"drone\" ] || [ \"$DRONE\" = \"true\" ];\n-then\n- say \"$e==>$x + Drone CI detected.\"\n- # http://docs.drone.io/env.html\n- # drone commits + are not full shas\n- service=\"drone.io\"\n- branch=\"$DRONE_BRANCH\"\n- build=\"$DRONE_BUILD_NUMBER\"\n- build_url=$(urlencode + \"${DRONE_BUILD_LINK}\")\n- pr=\"$DRONE_PULL_REQUEST\"\n- job=\"$DRONE_JOB_NUMBER\"\n- tag=\"$DRONE_TAG\"\n-\n-elif + [ \"$HEROKU_TEST_RUN_BRANCH\" != \"\" ];\n-then\n- say \"$e==>$x Heroku CI + detected.\"\n- # https://devcenter.heroku.com/articles/heroku-ci#environment-variables\n- service=\"heroku\"\n- branch=\"$HEROKU_TEST_RUN_BRANCH\"\n- build=\"$HEROKU_TEST_RUN_ID\"\n-\n-elif + [ \"$CI\" = \"True\" ] && [ \"$APPVEYOR\" = \"True\" ];\n-then\n- say \"$e==>$x + Appveyor CI detected.\"\n- # http://www.appveyor.com/docs/environment-variables\n- service=\"appveyor\"\n- branch=\"$APPVEYOR_REPO_BRANCH\"\n- build=$(urlencode + \"$APPVEYOR_JOB_ID\")\n- pr=\"$APPVEYOR_PULL_REQUEST_NUMBER\"\n- job=\"$APPVEYOR_ACCOUNT_NAME%2F$APPVEYOR_PROJECT_SLUG%2F$APPVEYOR_BUILD_VERSION\"\n- slug=\"$APPVEYOR_REPO_NAME\"\n- commit=\"$APPVEYOR_REPO_COMMIT\"\n- build_url=$(urlencode + \"${APPVEYOR_URL}/project/${APPVEYOR_REPO_NAME}/builds/$APPVEYOR_BUILD_ID/job/${APPVEYOR_JOB_ID}\")\n-elif + [ \"$CI\" = \"true\" ] && [ \"$WERCKER_GIT_BRANCH\" != \"\" ];\n-then\n- say + \"$e==>$x Wercker CI detected.\"\n- # http://devcenter.wercker.com/articles/steps/variables.html\n- service=\"wercker\"\n- branch=\"$WERCKER_GIT_BRANCH\"\n- build=\"$WERCKER_MAIN_PIPELINE_STARTED\"\n- slug=\"$WERCKER_GIT_OWNER/$WERCKER_GIT_REPOSITORY\"\n- commit=\"$WERCKER_GIT_COMMIT\"\n-\n-elif + [ \"$CI\" = \"true\" ] && [ \"$MAGNUM\" = \"true\" ];\n-then\n- say \"$e==>$x + Magnum CI detected.\"\n- # https://magnum-ci.com/docs/environment\n- service=\"magnum\"\n- branch=\"$CI_BRANCH\"\n- build=\"$CI_BUILD_NUMBER\"\n- commit=\"$CI_COMMIT\"\n-\n-elif + [ \"$SHIPPABLE\" = \"true\" ];\n-then\n- say \"$e==>$x Shippable CI detected.\"\n- # + http://docs.shippable.com/ci_configure/\n- service=\"shippable\"\n- branch=$([ + \"$HEAD_BRANCH\" != \"\" ] && echo \"$HEAD_BRANCH\" || echo \"$BRANCH\")\n- build=\"$BUILD_NUMBER\"\n- build_url=$(urlencode + \"$BUILD_URL\")\n- pr=\"$PULL_REQUEST\"\n- slug=\"$REPO_FULL_NAME\"\n- commit=\"$COMMIT\"\n-\n-elif + [ \"$TDDIUM\" = \"true\" ];\n-then\n- say \"Solano CI detected.\"\n- # http://docs.solanolabs.com/Setup/tddium-set-environment-variables/\n- service=\"solano\"\n- commit=\"$TDDIUM_CURRENT_COMMIT\"\n- branch=\"$TDDIUM_CURRENT_BRANCH\"\n- build=\"$TDDIUM_TID\"\n- pr=\"$TDDIUM_PR_ID\"\n-\n-elif + [ \"$GREENHOUSE\" = \"true\" ];\n-then\n- say \"$e==>$x Greenhouse CI detected.\"\n- # + http://docs.greenhouseci.com/docs/environment-variables-files\n- service=\"greenhouse\"\n- branch=\"$GREENHOUSE_BRANCH\"\n- build=\"$GREENHOUSE_BUILD_NUMBER\"\n- build_url=$(urlencode + \"$GREENHOUSE_BUILD_URL\")\n- pr=\"$GREENHOUSE_PULL_REQUEST\"\n- commit=\"$GREENHOUSE_COMMIT\"\n- search_in=\"$search_in + $GREENHOUSE_EXPORT_DIR\"\n-\n-elif [ \"$GITLAB_CI\" != \"\" ];\n-then\n- say + \"$e==>$x GitLab CI detected.\"\n- # http://doc.gitlab.com/ce/ci/variables/README.html\n- service=\"gitlab\"\n- branch=\"${CI_BUILD_REF_NAME:-$CI_COMMIT_REF_NAME}\"\n- build=\"${CI_BUILD_ID:-$CI_JOB_ID}\"\n- remote_addr=\"${CI_BUILD_REPO:-$CI_REPOSITORY_URL}\"\n- commit=\"${CI_BUILD_REF:-$CI_COMMIT_SHA}\"\n- slug=\"${CI_PROJECT_PATH}\"\n-\n-elif + [ \"$GITHUB_ACTION\" != \"\" ];\n-then\n- say \"$e==>$x GitHub Actions detected.\"\n-\n- # + https://github.com/features/actions\n- service=\"github-actions\"\n-\n- # + https://help.github.com/en/articles/virtual-environments-for-github-actions#environment-variables\n- branch=\"${GITHUB_REF#refs/heads/}\"\n- commit=\"${GITHUB_SHA}\"\n- slug=\"${GITHUB_REPOSITORY}\"\n-\n-elif + [ \"$SYSTEM_TEAMFOUNDATIONSERVERURI\" != \"\" ];\n-then\n- say \"$e==>$x Azure + Pipelines detected.\"\n- # https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=vsts\n- service=\"azure_pipelines\"\n- commit=\"$BUILD_SOURCEVERSION\"\n- build=\"$BUILD_BUILDNUMBER\"\n- if + [ -z \"$PULL_REQUEST_NUMBER\" ];\n- then\n- pr=\"$PULL_REQUEST_ID\"\n- else\n- pr=\"$PULL_REQUEST_NUMBER\"\n- fi\n- project=\"${SYSTEM_TEAMPROJECT}\"\n- server_uri=\"${SYSTEM_TEAMFOUNDATIONSERVERURI}\"\n- job=\"${BUILD_BUILDID}\"\n- branch=\"$BUILD_SOURCEBRANCHNAME\"\n- build_url=$(urlencode + \"${SYSTEM_TEAMFOUNDATIONSERVERURI}${SYSTEM_TEAMPROJECT}/_build/results?buildId=${BUILD_BUILDID}\")\n-elif + [ \"$CI\" = \"true\" ] && [ \"$BITBUCKET_BUILD_NUMBER\" != \"\" ];\n-then\n- say + \"$e==>$x Bitbucket detected.\"\n- # https://confluence.atlassian.com/bitbucket/variables-in-pipelines-794502608.html\n- service=\"bitbucket\"\n- branch=\"$BITBUCKET_BRANCH\"\n- build=\"$BITBUCKET_BUILD_NUMBER\"\n- slug=\"$BITBUCKET_REPO_OWNER/$BITBUCKET_REPO_SLUG\"\n- job=\"$BITBUCKET_BUILD_NUMBER\"\n- pr=\"$BITBUCKET_PR_ID\"\n- commit=\"$BITBUCKET_COMMIT\"\n- # + See https://jira.atlassian.com/browse/BCLOUD-19393\n- if [ \"${#commit}\" = + 12 ];\n- then\n- commit=$(git rev-parse \"$BITBUCKET_COMMIT\")\n- fi\n-elif + [ \"$CIRRUS_CI\" != \"\" ];\n-then\n- say \"$e==>$x Cirrus CI detected.\"\n- # + https://cirrus-ci.org/guide/writing-tasks/#environment-variables\n- service=\"cirrus-ci\"\n- slug=\"$CIRRUS_REPO_FULL_NAME\"\n- branch=\"$CIRRUS_BRANCH\"\n- pr=\"$CIRRUS_PR\"\n- commit=\"$CIRRUS_CHANGE_IN_REPO\"\n- build=\"$CIRRUS_TASK_ID\"\n- job=\"$CIRRUS_TASK_NAME\"\n-else\n- say + \"${r}x>${x} No CI provider detected.\"\n- say \" Testing inside Docker? + ${b}http://docs.codecov.io/docs/testing-with-docker${x}\"\n- say \" Testing + with Tox? ${b}https://docs.codecov.io/docs/python#section-testing-with-tox${x}\"\n-\n-fi\n-\n-say + \" ${e}project root:${x} $git_root\"\n-\n-# find branch, commit, repo from + git command\n-if [ \"$GIT_BRANCH\" != \"\" ];\n-then\n- branch=\"$GIT_BRANCH\"\n-\n-elif + [ \"$branch\" = \"\" ];\n-then\n- branch=$(git rev-parse --abbrev-ref HEAD + 2>/dev/null || hg branch 2>/dev/null || echo \"\")\n- if [ \"$branch\" = \"HEAD\" + ];\n- then\n- branch=\"\"\n- fi\n-fi\n-\n-if [ \"$commit_o\" = \"\" ];\n-then\n- # + merge commit -> actual commit\n- mc=\n- if [ -n \"$pr\" ] && [ \"$pr\" != + false ];\n- then\n- mc=$(git show --no-patch --format=\"%P\" 2>/dev/null + || echo \"\")\n- fi\n- if [[ \"$mc\" =~ ^[a-z0-9]{40}[[:space:]][a-z0-9]{40}$ + ]];\n- then\n- say \" Fixing merge commit SHA\"\n- commit=$(echo \"$mc\" + | cut -d'' '' -f2)\n- elif [ \"$GIT_COMMIT\" != \"\" ];\n- then\n- commit=\"$GIT_COMMIT\"\n- elif + [ \"$commit\" = \"\" ];\n- then\n- commit=$(git log -1 --format=\"%H\" 2>/dev/null + || hg id -i --debug 2>/dev/null | tr -d ''+'' || echo \"\")\n- fi\n-else\n- commit=\"$commit_o\"\n-fi\n-\n-if + [ \"$CODECOV_TOKEN\" != \"\" ] && [ \"$token\" = \"\" ];\n-then\n- say \"${e}-->${x} + token set from env\"\n- token=\"$CODECOV_TOKEN\"\n-fi\n-\n-if [ \"$CODECOV_URL\" + != \"\" ] && [ \"$url_o\" = \"\" ];\n-then\n- say \"${e}-->${x} url set from + env\"\n- url_o=$(echo \"$CODECOV_URL\" | sed -e ''s/\\/$//'')\n-fi\n-\n-if + [ \"$CODECOV_SLUG\" != \"\" ];\n-then\n- say \"${e}-->${x} slug set from env\"\n- slug_o=\"$CODECOV_SLUG\"\n-\n-elif + [ \"$slug\" = \"\" ];\n-then\n- if [ \"$remote_addr\" = \"\" ];\n- then\n- remote_addr=$(git + config --get remote.origin.url || hg paths default || echo '''')\n- fi\n- if + [ \"$remote_addr\" != \"\" ];\n- then\n- if echo \"$remote_addr\" | grep + -q \"//\"; then\n- # https\n- slug=$(echo \"$remote_addr\" | cut -d + / -f 4,5 | sed -e ''s/\\.git$//'')\n- else\n- # ssh\n- slug=$(echo + \"$remote_addr\" | cut -d : -f 2 | sed -e ''s/\\.git$//'')\n- fi\n- fi\n- if + [ \"$slug\" = \"/\" ];\n- then\n- slug=\"\"\n- fi\n-fi\n-\n-yaml=$(test + -n \"$codecov_yml\" && echo \"$codecov_yml\" \\\n- || cd \"$git_root\" + && \\\n- git ls-files \"*codecov.yml\" \"*codecov.yaml\" 2>/dev/null + \\\n- || hg locate \"*codecov.yml\" \"*codecov.yaml\" 2>/dev/null \\\n- || + cd $proj_root && find . -type f -name ''*codecov.y*ml'' -depth 1 2>/dev/null + \\\n- || echo '''')\n-yaml=$(echo \"$yaml\" | head -1)\n-\n-if [ \"$yaml\" + != \"\" ];\n-then\n- say \" ${e}Yaml found at:${x} $yaml\"\n- config=$(parse_yaml + \"$git_root/$yaml\" || echo '''')\n-\n- # TODO validate the yaml here\n-\n- if + [ \"$(echo \"$config\" | grep ''codecov_token=\"'')\" != \"\" ] && [ \"$token\" + = \"\" ];\n- then\n- say \"${e}-->${x} token set from yaml\"\n- token=\"$(echo + \"$config\" | grep ''codecov_token=\"'' | sed -e ''s/codecov_token=\"//'' | + sed -e ''s/\"\\.*//'')\"\n- fi\n-\n- if [ \"$(echo \"$config\" | grep ''codecov_url=\"'')\" + != \"\" ] && [ \"$url_o\" = \"\" ];\n- then\n- say \"${e}-->${x} url set + from yaml\"\n- url_o=\"$(echo \"$config\" | grep ''codecov_url=\"'' | sed + -e ''s/codecov_url=\"//'' | sed -e ''s/\"\\.*//'')\"\n- fi\n-\n- if [ \"$(echo + \"$config\" | grep ''codecov_slug=\"'')\" != \"\" ] && [ \"$slug_o\" = \"\" + ];\n- then\n- say \"${e}-->${x} slug set from yaml\"\n- slug_o=\"$(echo + \"$config\" | grep ''codecov_slug=\"'' | sed -e ''s/codecov_slug=\"//'' | sed + -e ''s/\"\\.*//'')\"\n- fi\n-else\n- say \" ${g}Yaml not found, that''s + ok! Learn more at${x} ${b}http://docs.codecov.io/docs/codecov-yaml${x}\"\n-\n-fi\n-\n-if + [ \"$branch_o\" != \"\" ];\n-then\n- branch=$(urlencode \"$branch_o\")\n-else\n- branch=$(urlencode + \"$branch\")\n-fi\n-\n-query=\"branch=$branch\\\n- &commit=$commit\\\n- &build=$([ + \"$build_o\" = \"\" ] && echo \"$build\" || echo \"$build_o\")\\\n- &build_url=$build_url\\\n- &name=$(urlencode + \"$name\")\\\n- &tag=$([ \"$tag_o\" = \"\" ] && echo \"$tag\" || echo + \"$tag_o\")\\\n- &slug=$([ \"$slug_o\" = \"\" ] && urlencode \"$slug\" + || urlencode \"$slug_o\")\\\n- &service=$service\\\n- &flags=$flags\\\n- &pr=$([ + \"$pr_o\" = \"\" ] && echo \"${pr##\\#}\" || echo \"${pr_o##\\#}\")\\\n- &job=$job\"\n-\n-if + [ ! -z \"$project\" ] && [ ! -z \"$server_uri\" ];\n-then\n- query=$(echo \"$query&project=$project&server_uri=$server_uri\" + | tr -d '' '')\n-fi\n-\n-if [ \"$parent\" != \"\" ];\n-then\n- query=$(echo + \"parent=$parent&$query\" | tr -d '' '')\n-fi\n-\n-if [ \"$ft_search\" = \"1\" + ];\n-then\n- # detect bower comoponents location\n- bower_components=\"bower_components\"\n- bower_rc=$(cd + \"$git_root\" && cat .bowerrc 2>/dev/null || echo \"\")\n- if [ \"$bower_rc\" + != \"\" ];\n- then\n- bower_components=$(echo \"$bower_rc\" | tr -d ''\\n'' + | grep ''\"directory\"'' | cut -d''\"'' -f4 | sed -e ''s/\\/$//'')\n- if + [ \"$bower_components\" = \"\" ];\n- then\n- bower_components=\"bower_components\"\n- fi\n- fi\n-\n- # + Swift Coverage\n- if [ \"$ft_xcodellvm\" = \"1\" ] && [ -d \"$ddp\" ];\n- then\n- say + \"${e}==>${x} Processing Xcode reports via llvm-cov\"\n- say \" DerivedData + folder: $ddp\"\n- profdata_files=$(find \"$ddp\" -name ''*.profdata'' 2>/dev/null + || echo '''')\n- if [ \"$profdata_files\" != \"\" ];\n- then\n- # + xcode via profdata\n- if [ \"$xp\" = \"\" ];\n- then\n- # xp=$(xcodebuild + -showBuildSettings 2>/dev/null | grep -i \"^\\s*PRODUCT_NAME\" | sed -e ''s/.*= + \\(.*\\)/\\1/'')\n- # say \" ${e}->${x} Speed up Xcode processing by + adding ${e}-J ''$xp''${x}\"\n- say \" ${g}hint${x} Speed up Swift + processing by using use ${g}-J ''AppName''${x} (regexp accepted)\"\n- say + \" ${g}hint${x} This will remove Pods/ from your report. Also ${b}https://docs.codecov.io/docs/ignoring-paths${x}\"\n- fi\n- while + read -r profdata;\n- do\n- if [ \"$profdata\" != \"\" ];\n- then\n- swiftcov + \"$profdata\" \"$xp\"\n- fi\n- done <<< \"$profdata_files\"\n- else\n- say + \" ${e}->${x} No Swift coverage found\"\n- fi\n-\n- # Obj-C Gcov Coverage\n- if + [ \"$ft_gcov\" = \"1\" ];\n- then\n- say \" ${e}->${x} Running $gcov_exe + for Obj-C\"\n- if [ \"$ft_gcovout\" = \"1\" ];\n- then\n- # + suppress gcov output\n- bash -c \"find $ddp -type f -name ''*.gcda'' + $gcov_include $gcov_ignore -exec $gcov_exe -p $gcov_arg {} +\" || true 2>/dev/null\n- else\n- bash + -c \"find $ddp -type f -name ''*.gcda'' $gcov_include $gcov_ignore -exec $gcov_exe + -p $gcov_arg {} +\" || true\n- fi\n- fi\n- fi\n-\n- if [ \"$ft_xcodeplist\" + = \"1\" ] && [ -d \"$ddp\" ];\n- then\n- say \"${e}==>${x} Processing Xcode + plists\"\n- plists_files=$(find \"$ddp\" -name ''*.xccoverage'' 2>/dev/null + || echo '''')\n- if [ \"$plists_files\" != \"\" ];\n- then\n- while + read -r plist;\n- do\n- if [ \"$plist\" != \"\" ];\n- then\n- say + \" ${g}Found${x} plist file at $plist\"\n- plutil -convert xml1 + -o \"$(basename \"$plist\").plist\" -- $plist\n- fi\n- done <<< + \"$plists_files\"\n- fi\n- fi\n-\n- # Gcov Coverage\n- if [ \"$ft_gcov\" + = \"1\" ];\n- then\n- say \"${e}==>${x} Running gcov in $proj_root ${e}(disable + via -X gcov)${x}\"\n- bash -c \"find $proj_root -type f -name ''*.gcno'' + $gcov_include $gcov_ignore -execdir $gcov_exe -pb $gcov_arg {} +\" || true\n- else\n- say + \"${e}==>${x} gcov disabled\"\n- fi\n-\n- # Python Coverage\n- if [ \"$ft_coveragepy\" + = \"1\" ];\n- then\n- if [ ! -f coverage.xml ];\n- then\n- if which + coverage >/dev/null 2>&1;\n- then\n- say \"${e}==>${x} Python coveragepy + exists ${e}disable via -X coveragepy${x}\"\n-\n- dotcoverage=$(find \"$git_root\" + -name ''.coverage'' -or -name ''.coverage.*'' | head -1 || echo '''')\n- if + [ \"$dotcoverage\" != \"\" ];\n- then\n- cd \"$(dirname \"$dotcoverage\")\"\n- if + [ ! -f .coverage ];\n- then\n- say \" ${e}->${x} Running + coverage combine\"\n- coverage combine -a\n- fi\n- say + \" ${e}->${x} Running coverage xml\"\n- if [ \"$(coverage xml -i)\" + != \"No data to report.\" ];\n- then\n- files=\"$files\n-$PWD/coverage.xml\"\n- else\n- say + \" ${r}No data to report.${x}\"\n- fi\n- cd \"$proj_root\"\n- else\n- say + \" ${r}No .coverage file found.${x}\"\n- fi\n- else\n- say + \"${e}==>${x} Python coveragepy not found\"\n- fi\n- fi\n- else\n- say + \"${e}==>${x} Python coveragepy disabled\"\n- fi\n-\n- if [ \"$search_in_o\" + != \"\" ];\n- then\n- # location override\n- search_in=\"$search_in_o\"\n- fi\n-\n- say + \"$e==>$x Searching for coverage reports in:\"\n- for _path in $search_in\n- do\n- say + \" ${g}+${x} $_path\"\n- done\n-\n- patterns=\"find $search_in \\( \\\n- -name + vendor \\\n- -or -name htmlcov \\\n- -or + -name virtualenv \\\n- -or -name js/generated/coverage + \\\n- -or -name .virtualenv \\\n- -or + -name virtualenvs \\\n- -or -name .virtualenvs \\\n- -or + -name .env \\\n- -or -name .envs \\\n- -or + -name env \\\n- -or -name .yarn-cache \\\n- -or + -name envs \\\n- -or -name .venv \\\n- -or + -name .venvs \\\n- -or -name venv \\\n- -or + -name venvs \\\n- -or -name .git \\\n- -or + -name .hg \\\n- -or -name .tox \\\n- -or + -name __pycache__ \\\n- -or -name ''.egg-info*'' \\\n- -or + -name ''$bower_components'' \\\n- -or -name node_modules + \\\n- -or -name ''conftest_*.c.gcov'' \\\n- \\) + -prune -or \\\n- -type f \\( -name ''*coverage*.*'' \\\n- -or + -name ''nosetests.xml'' \\\n- -or -name ''jacoco*.xml'' + \\\n- -or -name ''clover.xml'' \\\n- -or + -name ''report.xml'' \\\n- -or -name ''*.codecov.*'' \\\n- -or + -name ''codecov.*'' \\\n- -or -name ''cobertura.xml'' \\\n- -or + -name ''excoveralls.json'' \\\n- -or -name ''luacov.report.out'' + \\\n- -or -name ''coverage-final.json'' \\\n- -or + -name ''naxsi.info'' \\\n- -or -name ''lcov.info'' \\\n- -or + -name ''lcov.dat'' \\\n- -or -name ''*.lcov'' \\\n- -or + -name ''*.clover'' \\\n- -or -name ''cover.out'' \\\n- -or + -name ''gcov.info'' \\\n- -or -name ''*.gcov'' \\\n- -or + -name ''*.lst'' \\\n- $include_cov \\) \\\n- $exclude_cov + \\\n- -not -name ''*.profdata'' \\\n- -not + -name ''coverage-summary.json'' \\\n- -not -name ''phpunit-code-coverage.xml'' + \\\n- -not -name ''*/classycle/report.xml'' \\\n- -not + -name ''remapInstanbul.coverage*.json'' \\\n- -not -name + ''phpunit-coverage.xml'' \\\n- -not -name ''*codecov.yml'' + \\\n- -not -name ''*.serialized'' \\\n- -not + -name ''.coverage*'' \\\n- -not -name ''.*coveragerc'' \\\n- -not + -name ''*.sh'' \\\n- -not -name ''*.bat'' \\\n- -not + -name ''*.ps1'' \\\n- -not -name ''*.env'' \\\n- -not + -name ''*.cmake'' \\\n- -not -name ''*.dox'' \\\n- -not + -name ''*.ec'' \\\n- -not -name ''*.rst'' \\\n- -not + -name ''*.h'' \\\n- -not -name ''*.scss'' \\\n- -not + -name ''*.o'' \\\n- -not -name ''*.proto'' \\\n- -not + -name ''*.sbt'' \\\n- -not -name ''*.xcoverage.*'' \\\n- -not + -name ''*.gz'' \\\n- -not -name ''*.conf'' \\\n- -not + -name ''*.p12'' \\\n- -not -name ''*.csv'' \\\n- -not + -name ''*.rsp'' \\\n- -not -name ''*.m4'' \\\n- -not + -name ''*.pem'' \\\n- -not -name ''*~'' \\\n- -not + -name ''*.exe'' \\\n- -not -name ''*.am'' \\\n- -not + -name ''*.template'' \\\n- -not -name ''*.cp'' \\\n- -not + -name ''*.bw'' \\\n- -not -name ''*.crt'' \\\n- -not + -name ''*.log'' \\\n- -not -name ''*.cmake'' \\\n- -not + -name ''*.pth'' \\\n- -not -name ''*.in'' \\\n- -not + -name ''*.jar*'' \\\n- -not -name ''*.pom*'' \\\n- -not + -name ''*.png'' \\\n- -not -name ''*.jpg'' \\\n- -not + -name ''*.sql'' \\\n- -not -name ''*.jpeg'' \\\n- -not + -name ''*.svg'' \\\n- -not -name ''*.gif'' \\\n- -not + -name ''*.csv'' \\\n- -not -name ''*.snapshot'' \\\n- -not + -name ''*.mak*'' \\\n- -not -name ''*.bash'' \\\n- -not + -name ''*.data'' \\\n- -not -name ''*.py'' \\\n- -not + -name ''*.class'' \\\n- -not -name ''*.xcconfig'' \\\n- -not + -name ''*.ec'' \\\n- -not -name ''*.coverage'' \\\n- -not + -name ''*.pyc'' \\\n- -not -name ''*.cfg'' \\\n- -not + -name ''*.egg'' \\\n- -not -name ''*.ru'' \\\n- -not + -name ''*.css'' \\\n- -not -name ''*.less'' \\\n- -not + -name ''*.pyo'' \\\n- -not -name ''*.whl'' \\\n- -not + -name ''*.html'' \\\n- -not -name ''*.ftl'' \\\n- -not + -name ''*.erb'' \\\n- -not -name ''*.rb'' \\\n- -not + -name ''*.js'' \\\n- -not -name ''*.jade'' \\\n- -not + -name ''*.db'' \\\n- -not -name ''*.md'' \\\n- -not + -name ''*.cpp'' \\\n- -not -name ''*.gradle'' \\\n- -not + -name ''*.tar.tz'' \\\n- -not -name ''*.scss'' \\\n- -not + -name ''include.lst'' \\\n- -not -name ''fullLocaleNames.lst'' + \\\n- -not -name ''inputFiles.lst'' \\\n- -not + -name ''createdFiles.lst'' \\\n- -not -name ''scoverage.measurements.*'' + \\\n- -not -name ''test_*_coverage.txt'' \\\n- -not + -name ''testrunner-coverage*'' \\\n- -print 2>/dev/null\"\n- files=$(eval + \"$patterns\" || echo '''')\n-\n-elif [ \"$include_cov\" != \"\" ];\n-then\n- files=$(eval + \"find $search_in -type f \\( ${include_cov:5} \\)$exclude_cov 2>/dev/null\" + || echo '''')\n-fi\n-\n-num_of_files=$(echo \"$files\" | wc -l | tr -d '' '')\n-if + [ \"$num_of_files\" != '''' ] && [ \"$files\" != '''' ];\n-then\n- say \" ${e}->${x} + Found $num_of_files reports\"\n-fi\n-\n-# no files found\n-if [ \"$files\" = + \"\" ];\n-then\n- say \"${r}-->${x} No coverage report found.\"\n- say \" Please + visit ${b}http://docs.codecov.io/docs/supported-languages${x}\"\n- exit ${exit_with};\n-fi\n-\n-if + [ \"$ft_network\" == \"1\" ];\n-then\n- say \"${e}==>${x} Detecting git/mercurial + file structure\"\n- network=$(cd \"$git_root\" && git ls-files 2>/dev/null + || hg locate 2>/dev/null || echo \"\")\n- if [ \"$network\" = \"\" ];\n- then\n- network=$(find + \"$git_root\" \\( \\\n- -name virtualenv \\\n- -name + .virtualenv \\\n- -name virtualenvs \\\n- -name + .virtualenvs \\\n- -name ''*.png'' \\\n- -name + ''*.gif'' \\\n- -name ''*.jpg'' \\\n- -name + ''*.jpeg'' \\\n- -name ''*.md'' \\\n- -name + .env \\\n- -name .envs \\\n- -name env \\\n- -name + envs \\\n- -name .venv \\\n- -name .venvs + \\\n- -name venv \\\n- -name venvs \\\n- -name + .git \\\n- -name .egg-info \\\n- -name shunit2-2.1.6 + \\\n- -name vendor \\\n- -name __pycache__ + \\\n- -name node_modules \\\n- -path ''*/$bower_components/*'' + \\\n- -path ''*/target/delombok/*'' \\\n- -path + ''*/build/lib/*'' \\\n- -path ''*/js/generated/coverage/*'' + \\\n- \\) -prune -or \\\n- -type f -print + 2>/dev/null || echo '''')\n- fi\n-\n- if [ \"$prefix_o\" != \"\" ];\n- then\n- network=$(echo + \"$network\" | awk \"{print \\\"$prefix_o/\\\"\\$0}\")\n- fi\n-fi\n-\n-upload_file=`mktemp + /tmp/codecov.XXXXXX`\n-adjustments_file=`mktemp /tmp/codecov.adjustments.XXXXXX`\n-\n-cleanup() + {\n- rm -f $upload_file $adjustments_file $upload_file.gz\n-}\n-\n-trap cleanup + INT ABRT TERM\n-\n-if [ \"$env\" != \"\" ];\n-then\n- inc_env=\"\"\n- say + \"${e}==>${x} Appending build variables\"\n- for varname in $(echo \"$env\" + | tr '','' '' '')\n- do\n- if [ \"$varname\" != \"\" ];\n- then\n- say + \" ${g}+${x} $varname\"\n- inc_env=\"${inc_env}${varname}=$(eval echo + \"\\$${varname}\")\n-\"\n- fi\n- done\n-\n-echo \"$inc_env<<<<<< ENV\" >> + $upload_file\n-fi\n-\n-# Append git file list\n-# write discovered yaml location\n-echo + \"$yaml\" >> $upload_file\n-if [ \"$ft_network\" == \"1\" ];\n-then\n- i=\"woff|eot|otf\" # + fonts\n- i=\"$i|gif|png|jpg|jpeg|psd\" # images\n- i=\"$i|ptt|pptx|numbers|pages|md|txt|xlsx|docx|doc|pdf|html|csv\" # + docs\n- i=\"$i|yml|yaml|.gitignore\" # supporting docs\n- echo \"$network\" + | grep -vwE \"($i)$\" >> $upload_file\n-fi\n-echo \"<<<<<< network\" >> $upload_file\n-\n-fr=0\n-say + \"${e}==>${x} Reading reports\"\n-while IFS='''' read -r file;\n-do\n- # read + the coverage file\n- if [ \"$(echo \"$file\" | tr -d '' '')\" != '''' ];\n- then\n- if + [ -f \"$file\" ];\n- then\n- report_len=$(wc -c < \"$file\")\n- if + [ \"$report_len\" -ne 0 ];\n- then\n- say \" ${g}+${x} $file + ${e}bytes=$(echo \"$report_len\" | tr -d '' '')${x}\"\n- # append to + to upload\n- _filename=$(basename \"$file\")\n- if [ \"${_filename##*.}\" + = ''gcov'' ];\n- then\n- echo \"# path=$(echo \"$file.reduced\" + | sed \"s|^$git_root/||\")\" >> $upload_file\n- # get file name\n- head + -1 $file >> $upload_file\n- # 1. remove source code\n- # 2. + remove ending bracket lines\n- # 3. remove whitespace\n- # + 4. remove contextual lines\n- # 5. remove function names\n- awk + -F'': *'' ''{print $1\":\"$2\":\"}'' $file \\\n- | sed ''\\/: *} + *$/d'' \\\n- | sed ''s/^ *//'' \\\n- | sed ''/^-/d'' \\\n- | + sed ''s/^function.*/func/'' >> $upload_file\n- else\n- echo + \"# path=$(echo \"$file\" | sed \"s|^$git_root/||\")\" >> $upload_file\n- cat + \"$file\" >> $upload_file\n- fi\n- echo \"<<<<<< EOF\" >> $upload_file\n- fr=1\n- if + [ \"$clean\" = \"1\" ];\n- then\n- rm \"$file\"\n- fi\n- else\n- say + \" ${r}-${x} Skipping empty file $file\"\n- fi\n- else\n- say + \" ${r}-${x} file not found at $file\"\n- fi\n- fi\n-done <<< \"$(echo + -e \"$files\")\"\n-\n-if [ \"$fr\" = \"0\" ];\n-then\n- say \"${r}-->${x} No + coverage data found.\"\n- say \" Please visit ${b}http://docs.codecov.io/docs/supported-languages${x}\"\n- say + \" search for your projects language to learn how to collect reports.\"\n- exit + ${exit_with};\n-fi\n-\n-if [ \"$ft_fix\" = \"1\" ];\n-then\n- say \"${e}==>${x} + Appending adjustments\"\n- say \" ${b}http://docs.codecov.io/docs/fixing-reports${x}\"\n-\n- empty_line=''^[[:space:]]*$''\n- # + //\n- syntax_comment=''^[[:space:]]*//.*''\n- # /* or */\n- syntax_comment_block=''^[[:space:]]*(\\/\\*|\\*\\/)[[:space:]]*$''\n- # + { or }\n- syntax_bracket=''^[[:space:]]*[\\{\\}][[:space:]]*(//.*)?$''\n- # + [ or ]\n- syntax_list=''^[[:space:]]*[][][[:space:]]*(//.*)?$''\n-\n- skip_dirs=\"-not + -path ''*/$bower_components/*'' \\\n- -not -path ''*/node_modules/*''\"\n-\n- cut_and_join() + {\n- awk ''BEGIN { FS=\":\" }\n- $3 ~ /\\/\\*/ || $3 ~ /\\*\\// { + print $0 ; next }\n- $1!=key { if (key!=\"\") print out ; key=$1 ; out=$1\":\"$2 + ; next }\n- { out=out\",\"$2 }\n- END { print out }'' 2>/dev/null\n- }\n-\n- if + echo \"$network\" | grep -m1 ''.kt$'' 1>/dev/null;\n- then\n- # skip brackets + and comments\n- find \"$git_root\" -type f \\\n- -name + ''*.kt'' \\\n- -exec \\\n- grep -nIHE -e $syntax_bracket + \\\n- -e $syntax_comment_block {} \\; \\\n- | cut_and_join + \\\n- >> $adjustments_file \\\n- || echo ''''\n-\n- # last line + in file\n- find \"$git_root\" -type f \\\n- -name ''*.kt'' + -exec \\\n- wc -l {} \\; \\\n- | while read l; do echo \"EOF: $l\"; + done \\\n- 2>/dev/null \\\n- >> $adjustments_file \\\n- || echo + ''''\n-\n- fi\n-\n- if echo \"$network\" | grep -m1 ''.go$'' 1>/dev/null;\n- then\n- # + skip empty lines, comments, and brackets\n- find \"$git_root\" -not -path + ''*/vendor/*'' \\\n- -type f \\\n- -name + ''*.go'' \\\n- -exec \\\n- grep -nIHE \\\n- -e + $empty_line \\\n- -e $syntax_comment \\\n- -e $syntax_comment_block + \\\n- -e $syntax_bracket \\\n- {} \\; \\\n- | cut_and_join + \\\n- >> $adjustments_file \\\n- || echo ''''\n- fi\n-\n- if echo + \"$network\" | grep -m1 ''.dart$'' 1>/dev/null;\n- then\n- # skip brackets\n- find + \"$git_root\" -type f \\\n- -name ''*.dart'' \\\n- -exec + \\\n- grep -nIHE \\\n- -e $syntax_bracket \\\n- {} + \\; \\\n- | cut_and_join \\\n- >> $adjustments_file \\\n- || + echo ''''\n- fi\n-\n- if echo \"$network\" | grep -m1 ''.php$'' 1>/dev/null;\n- then\n- # + skip empty lines, comments, and brackets\n- find \"$git_root\" -not -path + \"*/vendor/*\" \\\n- -type f \\\n- -name + ''*.php'' \\\n- -exec \\\n- grep -nIHE \\\n- -e + $syntax_list \\\n- -e $syntax_bracket \\\n- -e ''^[[:space:]]*\\);[[:space:]]*(//.*)?$'' + \\\n- {} \\; \\\n- | cut_and_join \\\n- >> $adjustments_file + \\\n- || echo ''''\n- fi\n-\n- if echo \"$network\" | grep -m1 ''\\(.cpp\\|.h\\|.cxx\\|.c\\|.hpp\\|.m\\)$'' + 1>/dev/null;\n- then\n- # skip brackets\n- find \"$git_root\" -type f + \\\n- $skip_dirs \\\n- \\( \\\n- -name + ''*.h'' \\\n- -or -name ''*.cpp'' \\\n- -or -name ''*.cxx'' + \\\n- -or -name ''*.m'' \\\n- -or -name ''*.c'' \\\n- -or + -name ''*.hpp'' \\\n- \\) -exec \\\n- grep -nIHE \\\n- -e + $empty_line \\\n- -e $syntax_bracket \\\n- -e ''// LCOV_EXCL'' + \\\n- {} \\; \\\n- | cut_and_join \\\n- >> $adjustments_file + \\\n- || echo ''''\n-\n- # skip brackets\n- find \"$git_root\" -type + f \\\n- $skip_dirs \\\n- \\( \\\n- -name + ''*.h'' \\\n- -or -name ''*.cpp'' \\\n- -or -name ''*.cxx'' + \\\n- -or -name ''*.m'' \\\n- -or -name ''*.c'' \\\n- -or + -name ''*.hpp'' \\\n- \\) -exec \\\n- grep -nIH ''// LCOV_EXCL'' + \\\n- {} \\; \\\n- >> $adjustments_file \\\n- || echo ''''\n-\n- fi\n-\n- found=$(cat + $adjustments_file | tr -d '' '')\n-\n- if [ \"$found\" != \"\" ];\n- then\n- say + \" ${g}+${x} Found adjustments\"\n- echo \"# path=fixes\" >> $upload_file\n- cat + $adjustments_file >> $upload_file\n- echo \"<<<<<< EOF\" >> $upload_file\n- rm + -rf $adjustments_file\n- else\n- say \" ${e}->${x} No adjustments found\"\n- fi\n-fi\n-\n-if + [ \"$url_o\" != \"\" ];\n-then\n- url=\"$url_o\"\n-fi\n-\n-if [ \"$dump\" != + \"0\" ];\n-then\n- # trim whitespace from query\n- say \" ${e}->${x} Dumping + upload file (no upload)\"\n- echo \"$url/upload/v4?$(echo \"package=bash-$VERSION&token=$token&$query\" + | tr -d '' '')\"\n- cat $upload_file\n-else\n-\n- say \"${e}==>${x} Gzipping + contents\"\n- gzip -nf9 $upload_file\n-\n- query=$(echo \"${query}\" | tr + -d '' '')\n- say \"${e}==>${x} Uploading reports\"\n- say \" ${e}url:${x} + $url\"\n- say \" ${e}query:${x} $query\"\n-\n- # Full query without token + (to display on terminal output)\n- queryNoToken=$(echo \"package=bash-$VERSION&token=secret&$query\" + | tr -d '' '')\n- # now add token to query\n- query=$(echo \"package=bash-$VERSION&token=$token&$query\" + | tr -d '' '')\n-\n- if [ \"$ft_s3\" = \"1\" ];\n- then\n- i=\"0\"\n- while + [ $i -lt 4 ]\n- do\n- i=$[$i+1]\n- say \" ${e}->${x} Pinging + Codecov\"\n- say \"$url/upload/v4?$queryNoToken\"\n- res=$(curl $curl_s + -X POST $curlargs $cacert \\\n- -H ''X-Reduced-Redundancy: false'' + \\\n- -H ''X-Content-Type: text/plain'' \\\n- \"$url/upload/v4?$query\" + || true)\n- # a good replay is \"https://codecov.io\" + \"\\n\" + \"https://codecov.s3.amazonaws.com/...\"\n- status=$(echo + \"$res\" | head -1 | grep ''HTTP '' | cut -d'' '' -f2)\n- if [ \"$status\" + = \"\" ];\n- then\n- s3target=$(echo \"$res\" | sed -n 2p)\n- say + \" ${e}->${x} Uploading\"\n-\n-\n- s3=$(curl $curl_s -fiX PUT $curlawsargs + \\\n- --data-binary @$upload_file.gz \\\n- -H ''Content-Type: + text/plain'' \\\n- -H ''Content-Encoding: gzip'' \\\n- -H + ''x-amz-acl: public-read'' \\\n- \"$s3target\" || true)\n-\n-\n- if + [ \"$s3\" != \"\" ];\n- then\n- say \" ${g}->${x} View reports + at ${b}$(echo \"$res\" | sed -n 1p)${x}\"\n- exit 0\n- else\n- say + \" ${r}X>${x} Failed to upload\"\n- fi\n- elif [ \"$status\" + = \"400\" ];\n- then\n- # 400 Error\n- say \"${g}${res}${x}\"\n- exit + ${exit_with}\n- fi\n- say \" ${e}->${x} Sleeping for 30s and trying + again...\"\n- sleep 30\n- done\n- fi\n-\n- say \" ${e}->${x} Uploading + to Codecov\"\n- i=\"0\"\n- while [ $i -lt 4 ]\n- do\n- i=$[$i+1]\n-\n- res=$(curl + $curl_s -X POST $curlargs $cacert \\\n- --data-binary @$upload_file.gz + \\\n- -H ''Content-Type: text/plain'' \\\n- -H ''Content-Encoding: + gzip'' \\\n- -H ''X-Content-Encoding: gzip'' \\\n- -H ''Accept: + text/plain'' \\\n- \"$url/upload/v2?$query\" || echo ''HTTP 500'')\n- # + HTTP 200\n- # http://....\n- status=$(echo \"$res\" | head -1 | cut -d'' + '' -f2)\n- if [ \"$status\" = \"\" ];\n- then\n- say \" View reports + at ${b}$(echo \"$res\" | head -2 | tail -1)${x}\"\n- exit 0\n-\n- elif + [ \"${status:0:1}\" = \"5\" ];\n- then\n- say \" ${e}->${x} Sleeping + for 30s and trying again...\"\n- sleep 30\n-\n- else\n- say \" ${g}${res}${x}\"\n- exit + 0\n- exit ${exit_with}\n- fi\n-\n- done\n-\n- say \" ${r}X> Failed + to upload coverage reports${x}\"\n-fi\n-\n-exit ${exit_with}"},{"sha":"e14ecab9403dfb32d16ac5db3eff4b5dac940e2b","filename":"codecov.yaml","status":"modified","additions":3,"deletions":1,"changes":4,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/75f355d8d14ba3d7761c728b4d2607cde0eef065/codecov.yaml","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/75f355d8d14ba3d7761c728b4d2607cde0eef065/codecov.yaml","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=75f355d8d14ba3d7761c728b4d2607cde0eef065","patch":"@@ + -22,7 +22,9 @@ parsers:\n macro: no\n \n flags:\n- flagone:\n+ flag:\n+ carryforward: + true\n+ flagonebaby:\n carryforward: true\n \n comment:"},{"sha":"e87e3022e9306b6b38192509918377b9cf8f6310","filename":"dev.sh","status":"removed","additions":0,"deletions":1644,"changes":1644,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/f0895290dc26668faeeb20ee5ccd4cc995925775/dev.sh","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/f0895290dc26668faeeb20ee5ccd4cc995925775/dev.sh","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/dev.sh?ref=f0895290dc26668faeeb20ee5ccd4cc995925775","patch":"@@ + -1,1644 +0,0 @@\n-#!/usr/bin/env bash\n-\n-# Apache License Version 2.0, January + 2004\n-# https://github.com/codecov/codecov-bash/blob/main/LICENSE\n-\n-\n-set + -e +o pipefail\n-\n-VERSION=\"tbd\"\n-\n-url=\"http://localhost\"\n-env=\"$CODECOV_ENV\"\n-service=\"\"\n-token=\"\"\n-search_in=\"\"\n-flags=\"\"\n-exit_with=0\n-curlargs=\"\"\n-curlawsargs=\"\"\n-dump=\"0\"\n-clean=\"0\"\n-curl_s=\"-s\"\n-name=\"$CODECOV_NAME\"\n-include_cov=\"\"\n-exclude_cov=\"\"\n-ddp=\"$(echo + ~)/Library/Developer/Xcode/DerivedData\"\n-xp=\"\"\n-files=\"\"\n-cacert=\"$CODECOV_CA_BUNDLE\"\n-gcov_ignore=\"-not + -path ''./bower_components/**'' -not -path ''./node_modules/**'' -not -path + ''./vendor/**''\"\n-gcov_include=\"\"\n-\n-ft_gcov=\"1\"\n-ft_coveragepy=\"1\"\n-ft_fix=\"1\"\n-ft_search=\"1\"\n-ft_s3=\"1\"\n-ft_network=\"1\"\n-ft_xcodellvm=\"1\"\n-ft_xcodeplist=\"0\"\n-ft_gcovout=\"1\"\n-\n-_git_root=$(git + rev-parse --show-toplevel 2>/dev/null || hg root 2>/dev/null || echo $PWD)\n-git_root=\"$_git_root\"\n-codecov_yml=\"\"\n-remote_addr=\"\"\n-if + [ \"$git_root\" = \"$PWD\" ];\n-then\n- git_root=\".\"\n-fi\n-\n-url_o=\"\"\n-pr_o=\"\"\n-build_o=\"\"\n-commit_o=\"\"\n-search_in_o=\"\"\n-tag_o=\"\"\n-branch_o=\"\"\n-slug_o=\"\"\n-prefix_o=\"\"\n-\n-commit=\"$VCS_COMMIT_ID\"\n-branch=\"$VCS_BRANCH_NAME\"\n-pr=\"$VCS_PULL_REQUEST\"\n-slug=\"$VCS_SLUG\"\n-tag=\"$VCS_TAG\"\n-build_url=\"$CI_BUILD_URL\"\n-build=\"$CI_BUILD_ID\"\n-job=\"$CI_JOB_ID\"\n-\n-beta_xcode_partials=\"\"\n-\n-proj_root=\"$git_root\"\n-gcov_exe=\"gcov\"\n-gcov_arg=\"\"\n-\n-b=\"\\033[0;36m\"\n-g=\"\\033[0;32m\"\n-r=\"\\033[0;31m\"\n-e=\"\\033[0;90m\"\n-x=\"\\033[0m\"\n-\n-show_help() + {\n-cat << EOF\n-\n- Codecov Bash $VERSION\n-\n- Global + report uploading tool for Codecov\n- Documentation at https://docs.codecov.io/docs\n- Contribute + at https://github.com/codecov/codecov-bash\n-\n-\n- -h Display this + help and exit\n- -f FILE Target file(s) to upload\n-\n- -f + \"path/to/file\" only upload this file\n- skips + searching unless provided patterns below\n-\n- -f ''!*.bar'' ignore + all files at pattern *.bar\n- -f ''*.foo'' include + all files at pattern *.foo\n- Must use single quotes.\n- This + is non-exclusive, use -s \"*.foo\" to match specific paths.\n-\n- -s DIR Directory + to search for coverage reports.\n- Already searches project + root and artifact folders.\n- -t TOKEN Set the private repository token\n- (option) + set environment variable CODECOV_TOKEN=:uuid\n-\n- -t @/path/to/token_file\n- -t + uuid\n-\n- -n NAME Custom defined name of the upload. Visible in Codecov + UI\n-\n- -e ENV Specify environment variables to be included with this + build\n- Also accepting environment variables: CODECOV_ENV=VAR,VAR2\n-\n- -e + VAR,VAR2\n-\n- -X feature Toggle functionalities\n-\n- -X + gcov Disable gcov\n- -X coveragepy Disable python + coverage\n- -X fix Disable report fixing\n- -X + search Disable searching for reports\n- -X xcode Disable + xcode processing\n- -X network Disable uploading the file + network\n- -X gcovout Disable gcov output\n-\n- -N The + commit SHA of the parent for which you are uploading coverage. If not present,\n- the + parent will be determined using the API of your repository provider.\n- When + using the repository provider''s API, the parent is determined via finding\n- the + closest ancestor to the commit.\n-\n- -R root dir Used when not in git/hg + project to identify project root directory\n- -y conf file Used to specify + the location of the .codecov.yml config file\n- -F flag Flag the upload + to group coverage metrics\n-\n- -F unittests This upload + is only unittests\n- -F integration This upload is only + integration tests\n- -F ui,chrome This upload is Chrome + - UI tests\n-\n- -c Move discovered coverage reports to the trash\n- -Z Exit + with 1 if not successful. Default will Exit with 0\n-\n- -- xcode --\n- -D Custom + Derived Data Path for Coverage.profdata and gcov processing\n- Default + ''~/Library/Developer/Xcode/DerivedData''\n- -J Specify packages + to build coverage.\n- This can significantly reduces time to + build coverage reports.\n-\n- -J ''MyAppName'' Will match + \"MyAppName\" and \"MyAppNameTests\"\n- -J ''^ExampleApp$'' Will + match only \"ExampleApp\" not \"ExampleAppTests\"\n-\n- -- gcov --\n- -g + GLOB Paths to ignore during gcov gathering\n- -G GLOB Paths to + include during gcov gathering\n- -p dir Project root directory\n- Also + used when preparing gcov\n- -k prefix Prefix filepaths to help resolve + path fixing: https://github.com/codecov/support/issues/472\n- -x gcovexe gcov + executable to run. Defaults to ''gcov''\n- -a gcovargs extra arguments to + pass to gcov\n-\n- -- Override CI Environment Variables --\n- These + variables are automatically detected by popular CI providers\n-\n- -B branch Specify + the branch name\n- -C sha Specify the commit sha\n- -P pr Specify + the pull request number\n- -b build Specify the build number\n- -T + tag Specify the git tag\n-\n- -- Enterprise --\n- -u URL Set + the target url for Enterprise customers\n- Not required when + retrieving the bash uploader from your CCE\n- (option) Set environment + variable CODECOV_URL=https://my-hosted-codecov.com\n- -r SLUG owner/repo + slug used instead of the private repo token in Enterprise\n- (option) + set environment variable CODECOV_SLUG=:owner/:repo\n- (option) + set in your codecov.yml \"codecov.slug\"\n- -S PATH File path to your + cacert.pem file used to verify ssl with Codecov Enterprise (optional)\n- (option) + Set environment variable: CODECOV_CA_BUNDLE=\"/path/to/ca.pem\"\n- -U curlargs Extra + curl arguments to communicate with Codecov. e.g., -U \"--proxy http://http-proxy\"\n- -A + curlargs Extra curl arguments to communicate with AWS.\n-\n- -- Debugging + --\n- -d Don''t upload, but dump upload file to stdout\n- -K Remove + color from the output\n- -v Verbose mode\n-\n-EOF\n-}\n-\n-\n-say() + {\n- echo -e \"$1\"\n-}\n-\n-\n-urlencode() {\n- echo \"$1\" | curl -Gso /dev/null + -w %{url_effective} --data-urlencode @- \"\" | cut -c 3- | sed -e ''s/%0A//''\n-}\n-\n-\n-swiftcov() + {\n- _dir=$(dirname \"$1\" | sed ''s/\\(Build\\).*/\\1/g'')\n- for _type in + app framework xctest\n- do\n- find \"$_dir\" -name \"*.$_type\" | while + read f\n- do\n- _proj=${f##*/}\n- _proj=${_proj%.\"$_type\"}\n- if + [ \"$2\" = \"\" ] || [ \"$(echo \"$_proj\" | grep -i \"$2\")\" != \"\" ];\n- then\n- say + \" $g+$x Building reports for $_proj $_type\"\n- dest=$([ -f \"$f/$_proj\" + ] && echo \"$f/$_proj\" || echo \"$f/Contents/MacOS/$_proj\")\n- _proj_name=$(echo + \"$_proj\" | sed -e ''s/[[:space:]]//g'')\n- xcrun llvm-cov show $beta_xcode_partials + -instr-profile \"$1\" \"$dest\" > \"$_proj_name.$_type.coverage.txt\" \\\n- || + say \" ${r}x>${x} llvm-cov failed to produce results for $dest\"\n- fi\n- done\n- done\n-}\n-\n-\n-# + Credits to: https://gist.github.com/pkuczynski/8665367\n-parse_yaml() {\n- local + prefix=$2\n- local s=''[[:space:]]*'' w=''[a-zA-Z0-9_]*'' fs=$(echo @|tr @ + ''\\034'')\n- sed -ne \"s|^\\($s\\)\\($w\\)$s:$s\\\"\\(.*\\)\\\"$s\\$|\\1$fs\\2$fs\\3|p\" + \\\n- -e \"s|^\\($s\\)\\($w\\)$s:$s\\(.*\\)$s\\$|\\1$fs\\2$fs\\3|p\" + $1 |\n- awk -F$fs ''{\n- indent = length($1)/2;\n- vname[indent] + = $2;\n- for (i in vname) {if (i > indent) {delete vname[i]}}\n- if + (length($3) > 0) {\n- vn=\"\"; if (indent > 0) {vn=(vn)(vname[0])(\"_\")}\n- printf(\"%s%s%s=\\\"%s\\\"\\n\", + \"''$prefix''\",vn, $2, $3);\n- }\n- }''\n-}\n-\n-\n-if [ $# != 0 ];\n-then\n- while + getopts \"a:A:b:B:cC:dD:e:f:F:g:G:hJ:k:Kn:p:P:r:R:y:s:S:t:T:u:U:vx:X:ZN:\" o\n- do\n- case + \"$o\" in\n- \"N\")\n- parent=$OPTARG\n- ;;\n- \"a\")\n- gcov_arg=$OPTARG\n- ;;\n- \"A\")\n- curlawsargs=\"$OPTARG\"\n- ;;\n- \"b\")\n- build_o=\"$OPTARG\"\n- ;;\n- \"B\")\n- branch_o=\"$OPTARG\"\n- ;;\n- \"c\")\n- clean=\"1\"\n- ;;\n- \"C\")\n- commit_o=\"$OPTARG\"\n- ;;\n- \"d\")\n- dump=\"1\"\n- ;;\n- \"D\")\n- ddp=\"$OPTARG\"\n- ;;\n- \"e\")\n- env=\"$env,$OPTARG\"\n- ;;\n- \"f\")\n- if + [ \"${OPTARG::1}\" = \"!\" ];\n- then\n- exclude_cov=\"$exclude_cov + -not -path ''${OPTARG:1}''\"\n-\n- elif [[ \"$OPTARG\" = *\"*\"* ]];\n- then\n- include_cov=\"$include_cov + -or -name ''$OPTARG''\"\n-\n- else\n- ft_search=0\n- if + [ \"$files\" = \"\" ];\n- then\n- files=\"$OPTARG\"\n- else\n- files=\"$files\n-$OPTARG\"\n- fi\n- fi\n- ;;\n- \"F\")\n- if + [ \"$flags\" = \"\" ];\n- then\n- flags=\"$OPTARG\"\n- else\n- flags=\"$flags,$OPTARG\"\n- fi\n- ;;\n- \"g\")\n- gcov_ignore=\"$gcov_ignore + -not -path ''$OPTARG''\"\n- ;;\n- \"G\")\n- gcov_include=\"$gcov_include + -path ''$OPTARG''\"\n- ;;\n- \"h\")\n- show_help\n- exit + 0;\n- ;;\n- \"J\")\n- ft_xcodellvm=\"1\"\n- ft_xcodeplist=\"0\"\n- if + [ \"$xp\" = \"\" ];\n- then\n- xp=\"$OPTARG\"\n- else\n- xp=\"$xp\\|$OPTARG\"\n- fi\n- ;;\n- \"k\")\n- prefix_o=$(echo + \"$OPTARG\" | sed -e ''s:^/*::'' -e ''s:/*$::'')\n- ;;\n- \"K\")\n- b=\"\"\n- g=\"\"\n- r=\"\"\n- e=\"\"\n- x=\"\"\n- ;;\n- \"n\")\n- name=\"$OPTARG\"\n- ;;\n- \"p\")\n- proj_root=\"$OPTARG\"\n- ;;\n- \"P\")\n- pr_o=\"$OPTARG\"\n- ;;\n- \"r\")\n- slug_o=\"$OPTARG\"\n- ;;\n- \"R\")\n- git_root=\"$OPTARG\"\n- ;;\n- \"s\")\n- if + [ \"$search_in_o\" = \"\" ];\n- then\n- search_in_o=\"$OPTARG\"\n- else\n- search_in_o=\"$search_in_o + $OPTARG\"\n- fi\n- ;;\n- \"S\")\n- cacert=\"--cacert + \\\"$OPTARG\\\"\"\n- ;;\n- \"t\")\n- if [ \"${OPTARG::1}\" + = \"@\" ];\n- then\n- token=$(cat \"${OPTARG:1}\" | tr -d '' + \\n'')\n- else\n- token=\"$OPTARG\"\n- fi\n- ;;\n- \"T\")\n- tag_o=\"$OPTARG\"\n- ;;\n- \"u\")\n- url_o=$(echo + \"$OPTARG\" | sed -e ''s/\\/$//'')\n- ;;\n- \"U\")\n- curlargs=\"$OPTARG\"\n- ;;\n- \"v\")\n- set + -x\n- curl_s=\"\"\n- ;;\n- \"x\")\n- gcov_exe=$OPTARG\n- ;;\n- \"X\")\n- if + [ \"$OPTARG\" = \"gcov\" ];\n- then\n- ft_gcov=\"0\"\n- elif + [ \"$OPTARG\" = \"coveragepy\" ] || [ \"$OPTARG\" = \"py\" ];\n- then\n- ft_coveragepy=\"0\"\n- elif + [ \"$OPTARG\" = \"gcovout\" ];\n- then\n- ft_gcovout=\"0\"\n- elif + [ \"$OPTARG\" = \"xcodellvm\" ];\n- then\n- ft_xcodellvm=\"1\"\n- ft_xcodeplist=\"0\"\n- elif + [ \"$OPTARG\" = \"fix\" ] || [ \"$OPTARG\" = \"fixes\" ];\n- then\n- ft_fix=\"0\"\n- elif + [ \"$OPTARG\" = \"xcode\" ];\n- then\n- ft_xcodellvm=\"0\"\n- ft_xcodeplist=\"0\"\n- elif + [ \"$OPTARG\" = \"search\" ];\n- then\n- ft_search=\"0\"\n- elif + [ \"$OPTARG\" = \"xcodepartials\" ];\n- then\n- beta_xcode_partials=\"-use-color\"\n- elif + [ \"$OPTARG\" = \"network\" ];\n- then\n- ft_network=\"0\"\n- elif + [ \"$OPTARG\" = \"s3\" ];\n- then\n- ft_s3=\"0\"\n- fi\n- ;;\n- \"y\")\n- codecov_yml=\"$OPTARG\"\n- ;;\n- \"Z\")\n- exit_with=1\n- ;;\n- esac\n- done\n-fi\n-\n-say + \"\n- _____ _\n- / ____| | |\n-| | ___ __| | ___ ___ + _____ __\n-| | / _ \\\\ / _\\` |/ _ \\\\/ __/ _ \\\\ \\\\ / /\n-| |___| + (_) | (_| | __/ (_| (_) \\\\ V /\n- \\\\_____\\\\___/ \\\\__,_|\\\\___|\\\\___\\\\___/ + \\\\_/\n- Bash-$VERSION\n-\n-\"\n-\n-search_in=\"$proj_root\"\n-\n-if + [ \"$JENKINS_URL\" != \"\" ];\n-then\n- say \"$e==>$x Jenkins CI detected.\"\n- # + https://wiki.jenkins-ci.org/display/JENKINS/Building+a+software+project\n- # + https://wiki.jenkins-ci.org/display/JENKINS/GitHub+pull+request+builder+plugin#GitHubpullrequestbuilderplugin-EnvironmentVariables\n- service=\"jenkins\"\n-\n- if + [ \"$ghprbSourceBranch\" != \"\" ];\n- then\n- branch=\"$ghprbSourceBranch\"\n- elif + [ \"$GIT_BRANCH\" != \"\" ];\n- then\n- branch=\"$GIT_BRANCH\"\n- elif + [ \"$BRANCH_NAME\" != \"\" ];\n- then\n- branch=\"$BRANCH_NAME\"\n- fi\n-\n- if + [ \"$ghprbActualCommit\" != \"\" ];\n- then\n- commit=\"$ghprbActualCommit\"\n- elif + [ \"$GIT_COMMIT\" != \"\" ];\n- then\n- commit=\"$GIT_COMMIT\"\n- fi\n-\n- if + [ \"$ghprbPullId\" != \"\" ];\n- then\n- pr=\"$ghprbPullId\"\n- elif [ + \"$CHANGE_ID\" != \"\" ];\n- then\n- pr=\"$CHANGE_ID\"\n- fi\n-\n- build=\"$BUILD_NUMBER\"\n- build_url=$(urlencode + \"$BUILD_URL\")\n-\n-elif [ \"$CI\" = \"true\" ] && [ \"$TRAVIS\" = \"true\" + ] && [ \"$SHIPPABLE\" != \"true\" ];\n-then\n- say \"$e==>$x Travis CI detected.\"\n- # + https://docs.travis-ci.com/user/environment-variables/\n- service=\"travis\"\n- commit=\"${TRAVIS_PULL_REQUEST_SHA:-$TRAVIS_COMMIT}\"\n- build=\"$TRAVIS_JOB_NUMBER\"\n- pr=\"$TRAVIS_PULL_REQUEST\"\n- job=\"$TRAVIS_JOB_ID\"\n- slug=\"$TRAVIS_REPO_SLUG\"\n- env=\"$env,TRAVIS_OS_NAME\"\n- tag=\"$TRAVIS_TAG\"\n- if + [ \"$TRAVIS_BRANCH\" != \"$TRAVIS_TAG\" ];\n- then\n- branch=\"$TRAVIS_BRANCH\"\n- fi\n-\n- language=$(compgen + -A variable | grep \"^TRAVIS_.*_VERSION$\" | head -1)\n- if [ \"$language\" + != \"\" ];\n- then\n- env=\"$env,${!language}\"\n- fi\n-\n-elif [ \"$CODEBUILD_BUILD_ARN\" + != \"\" ];\n-then\n- say \"$e==>$x AWS Codebuild detected.\"\n- # https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html\n- service=\"codebuild\"\n- commit=\"$CODEBUILD_RESOLVED_SOURCE_VERSION\"\n- build=\"$CODEBUILD_BUILD_ID\"\n- branch=\"$(echo + $CODEBUILD_WEBHOOK_HEAD_REF | sed ''s/^refs\\/heads\\///'')\"\n- if [ \"${CODEBUILD_SOURCE_VERSION/pr}\" + = \"$CODEBUILD_SOURCE_VERSION\" ] ; then\n- pr=\"false\"\n- else\n- pr=\"$(echo + $CODEBUILD_SOURCE_VERSION | sed ''s/^pr\\///'')\"\n- fi\n- job=\"$CODEBUILD_BUILD_ID\"\n- slug=\"$(echo + $CODEBUILD_SOURCE_REPO_URL | sed ''s/^.*github.com\\///'' | sed ''s/\\.git$//'')\"\n-\n-elif + [ \"$DOCKER_REPO\" != \"\" ];\n-then\n- say \"$e==>$x Docker detected.\"\n- # + https://docs.docker.com/docker-cloud/builds/advanced/\n- service=\"docker\"\n- branch=\"$SOURCE_BRANCH\"\n- commit=\"$SOURCE_COMMIT\"\n- slug=\"$DOCKER_REPO\"\n- tag=\"$CACHE_TAG\"\n- env=\"$env,IMAGE_NAME\"\n-\n-elif + [ \"$CI\" = \"true\" ] && [ \"$CI_NAME\" = \"codeship\" ];\n-then\n- say \"$e==>$x + Codeship CI detected.\"\n- # https://www.codeship.io/documentation/continuous-integration/set-environment-variables/\n- service=\"codeship\"\n- branch=\"$CI_BRANCH\"\n- build=\"$CI_BUILD_NUMBER\"\n- build_url=$(urlencode + \"$CI_BUILD_URL\")\n- commit=\"$CI_COMMIT_ID\"\n-\n-elif [ ! -z \"$CF_BUILD_URL\" + ] && [ ! -z \"$CF_BUILD_ID\" ];\n-then\n- say \"$e==>$x Codefresh CI detected.\"\n- # + https://docs.codefresh.io/v1.0/docs/variables\n- service=\"codefresh\"\n- branch=\"$CF_BRANCH\"\n- build=\"$CF_BUILD_ID\"\n- build_url=$(urlencode + \"$CF_BUILD_URL\")\n- commit=\"$CF_REVISION\"\n-\n-elif [ \"$TEAMCITY_VERSION\" + != \"\" ];\n-then\n- say \"$e==>$x TeamCity CI detected.\"\n- # https://confluence.jetbrains.com/display/TCD8/Predefined+Build+Parameters\n- # + https://confluence.jetbrains.com/plugins/servlet/mobile#content/view/74847298\n- if + [ \"$TEAMCITY_BUILD_BRANCH\" = '''' ];\n- then\n- echo \" Teamcity does + not automatically make build parameters available as environment variables.\"\n- echo + \" Add the following environment parameters to the build configuration\"\n- echo + \" env.TEAMCITY_BUILD_BRANCH = %teamcity.build.branch%\"\n- echo \" env.TEAMCITY_BUILD_ID + = %teamcity.build.id%\"\n- echo \" env.TEAMCITY_BUILD_URL = %teamcity.serverUrl%/viewLog.html?buildId=%teamcity.build.id%\"\n- echo + \" env.TEAMCITY_BUILD_COMMIT = %system.build.vcs.number%\"\n- echo \" env.TEAMCITY_BUILD_REPOSITORY + = %vcsroot..url%\"\n- fi\n- service=\"teamcity\"\n- branch=\"$TEAMCITY_BUILD_BRANCH\"\n- build=\"$TEAMCITY_BUILD_ID\"\n- build_url=$(urlencode + \"$TEAMCITY_BUILD_URL\")\n- if [ \"$TEAMCITY_BUILD_COMMIT\" != \"\" ];\n- then\n- commit=\"$TEAMCITY_BUILD_COMMIT\"\n- else\n- commit=\"$BUILD_VCS_NUMBER\"\n- fi\n- remote_addr=\"$TEAMCITY_BUILD_REPOSITORY\"\n-\n-elif + [ \"$CI\" = \"true\" ] && [ \"$CIRCLECI\" = \"true\" ];\n-then\n- say \"$e==>$x + Circle CI detected.\"\n- # https://circleci.com/docs/environment-variables\n- service=\"circleci\"\n- branch=\"$CIRCLE_BRANCH\"\n- build=\"$CIRCLE_BUILD_NUM\"\n- job=\"$CIRCLE_NODE_INDEX\"\n- if + [ \"$CIRCLE_PROJECT_REPONAME\" != \"\" ];\n- then\n- slug=\"$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME\"\n- else\n- # + git@github.com:owner/repo.git\n- slug=\"${CIRCLE_REPOSITORY_URL##*:}\"\n- # + owner/repo.git\n- slug=\"${slug%%.git}\"\n- fi\n- pr=\"$CIRCLE_PR_NUMBER\"\n- commit=\"$CIRCLE_SHA1\"\n- search_in=\"$search_in + $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS\"\n-\n-elif [ \"$BUDDYBUILD_BRANCH\" + != \"\" ];\n-then\n- say \"$e==>$x buddybuild detected\"\n- # http://docs.buddybuild.com/v6/docs/custom-prebuild-and-postbuild-steps\n- service=\"buddybuild\"\n- branch=\"$BUDDYBUILD_BRANCH\"\n- build=\"$BUDDYBUILD_BUILD_NUMBER\"\n- build_url=\"https://dashboard.buddybuild.com/public/apps/$BUDDYBUILD_APP_ID/build/$BUDDYBUILD_BUILD_ID\"\n- # + BUDDYBUILD_TRIGGERED_BY\n- if [ \"$ddp\" = \"$(echo ~)/Library/Developer/Xcode/DerivedData\" + ];\n- then\n- ddp=\"/private/tmp/sandbox/${BUDDYBUILD_APP_ID}/bbtest\"\n- fi\n-\n-elif + [ \"${bamboo_planRepository_revision}\" != \"\" ];\n-then\n- say \"$e==>$x + Bamboo detected\"\n- # https://confluence.atlassian.com/bamboo/bamboo-variables-289277087.html#Bamboovariables-Build-specificvariables\n- service=\"bamboo\"\n- commit=\"${bamboo_planRepository_revision}\"\n- branch=\"${bamboo_planRepository_branch}\"\n- build=\"${bamboo_buildNumber}\"\n- build_url=\"${bamboo_buildResultsUrl}\"\n- remote_addr=\"${bamboo_planRepository_repositoryUrl}\"\n-\n-elif + [ \"$CI\" = \"true\" ] && [ \"$BITRISE_IO\" = \"true\" ];\n-then\n- # http://devcenter.bitrise.io/faq/available-environment-variables/\n- say + \"$e==>$x Bitrise CI detected.\"\n- service=\"bitrise\"\n- branch=\"$BITRISE_GIT_BRANCH\"\n- build=\"$BITRISE_BUILD_NUMBER\"\n- build_url=$(urlencode + \"$BITRISE_BUILD_URL\")\n- pr=\"$BITRISE_PULL_REQUEST\"\n- if [ \"$GIT_CLONE_COMMIT_HASH\" + != \"\" ];\n- then\n- commit=\"$GIT_CLONE_COMMIT_HASH\"\n- fi\n-\n-elif + [ \"$CI\" = \"true\" ] && [ \"$SEMAPHORE\" = \"true\" ];\n-then\n- say \"$e==>$x + Semaphore CI detected.\"\n- # https://semaphoreapp.com/docs/available-environment-variables.html\n- service=\"semaphore\"\n- branch=\"$BRANCH_NAME\"\n- build=\"$SEMAPHORE_BUILD_NUMBER\"\n- job=\"$SEMAPHORE_CURRENT_THREAD\"\n- pr=\"$PULL_REQUEST_NUMBER\"\n- slug=\"$SEMAPHORE_REPO_SLUG\"\n- commit=\"$REVISION\"\n- env=\"$env,SEMAPHORE_TRIGGER_SOURCE\"\n-\n-elif + [ \"$CI\" = \"true\" ] && [ \"$BUILDKITE\" = \"true\" ];\n-then\n- say \"$e==>$x + Buildkite CI detected.\"\n- # https://buildkite.com/docs/guides/environment-variables\n- service=\"buildkite\"\n- branch=\"$BUILDKITE_BRANCH\"\n- build=\"$BUILDKITE_BUILD_NUMBER\"\n- job=\"$BUILDKITE_JOB_ID\"\n- build_url=$(urlencode + \"$BUILDKITE_BUILD_URL\")\n- slug=\"$BUILDKITE_PROJECT_SLUG\"\n- commit=\"$BUILDKITE_COMMIT\"\n- if + [[ \"$BUILDKITE_PULL_REQUEST\" != \"false\" ]]; then\n- pr=\"$BUILDKITE_PULL_REQUEST\"\n- fi\n- tag=\"$BUILDKITE_TAG\"\n-\n-elif + [ \"$CI\" = \"drone\" ] || [ \"$DRONE\" = \"true\" ];\n-then\n- say \"$e==>$x + Drone CI detected.\"\n- # http://docs.drone.io/env.html\n- # drone commits + are not full shas\n- service=\"drone.io\"\n- branch=\"$DRONE_BRANCH\"\n- build=\"$DRONE_BUILD_NUMBER\"\n- build_url=$(urlencode + \"${DRONE_BUILD_LINK}\")\n- pr=\"$DRONE_PULL_REQUEST\"\n- job=\"$DRONE_JOB_NUMBER\"\n- tag=\"$DRONE_TAG\"\n-\n-elif + [ \"$HEROKU_TEST_RUN_BRANCH\" != \"\" ];\n-then\n- say \"$e==>$x Heroku CI + detected.\"\n- # https://devcenter.heroku.com/articles/heroku-ci#environment-variables\n- service=\"heroku\"\n- branch=\"$HEROKU_TEST_RUN_BRANCH\"\n- build=\"$HEROKU_TEST_RUN_ID\"\n-\n-elif + [ \"$CI\" = \"True\" ] && [ \"$APPVEYOR\" = \"True\" ];\n-then\n- say \"$e==>$x + Appveyor CI detected.\"\n- # http://www.appveyor.com/docs/environment-variables\n- service=\"appveyor\"\n- branch=\"$APPVEYOR_REPO_BRANCH\"\n- build=$(urlencode + \"$APPVEYOR_JOB_ID\")\n- pr=\"$APPVEYOR_PULL_REQUEST_NUMBER\"\n- job=\"$APPVEYOR_ACCOUNT_NAME%2F$APPVEYOR_PROJECT_SLUG%2F$APPVEYOR_BUILD_VERSION\"\n- slug=\"$APPVEYOR_REPO_NAME\"\n- commit=\"$APPVEYOR_REPO_COMMIT\"\n- build_url=$(urlencode + \"${APPVEYOR_URL}/project/${APPVEYOR_REPO_NAME}/builds/$APPVEYOR_BUILD_ID/job/${APPVEYOR_JOB_ID}\")\n-elif + [ \"$CI\" = \"true\" ] && [ \"$WERCKER_GIT_BRANCH\" != \"\" ];\n-then\n- say + \"$e==>$x Wercker CI detected.\"\n- # http://devcenter.wercker.com/articles/steps/variables.html\n- service=\"wercker\"\n- branch=\"$WERCKER_GIT_BRANCH\"\n- build=\"$WERCKER_MAIN_PIPELINE_STARTED\"\n- slug=\"$WERCKER_GIT_OWNER/$WERCKER_GIT_REPOSITORY\"\n- commit=\"$WERCKER_GIT_COMMIT\"\n-\n-elif + [ \"$CI\" = \"true\" ] && [ \"$MAGNUM\" = \"true\" ];\n-then\n- say \"$e==>$x + Magnum CI detected.\"\n- # https://magnum-ci.com/docs/environment\n- service=\"magnum\"\n- branch=\"$CI_BRANCH\"\n- build=\"$CI_BUILD_NUMBER\"\n- commit=\"$CI_COMMIT\"\n-\n-elif + [ \"$SHIPPABLE\" = \"true\" ];\n-then\n- say \"$e==>$x Shippable CI detected.\"\n- # + http://docs.shippable.com/ci_configure/\n- service=\"shippable\"\n- branch=$([ + \"$HEAD_BRANCH\" != \"\" ] && echo \"$HEAD_BRANCH\" || echo \"$BRANCH\")\n- build=\"$BUILD_NUMBER\"\n- build_url=$(urlencode + \"$BUILD_URL\")\n- pr=\"$PULL_REQUEST\"\n- slug=\"$REPO_FULL_NAME\"\n- commit=\"$COMMIT\"\n-\n-elif + [ \"$TDDIUM\" = \"true\" ];\n-then\n- say \"Solano CI detected.\"\n- # http://docs.solanolabs.com/Setup/tddium-set-environment-variables/\n- service=\"solano\"\n- commit=\"$TDDIUM_CURRENT_COMMIT\"\n- branch=\"$TDDIUM_CURRENT_BRANCH\"\n- build=\"$TDDIUM_TID\"\n- pr=\"$TDDIUM_PR_ID\"\n-\n-elif + [ \"$GREENHOUSE\" = \"true\" ];\n-then\n- say \"$e==>$x Greenhouse CI detected.\"\n- # + http://docs.greenhouseci.com/docs/environment-variables-files\n- service=\"greenhouse\"\n- branch=\"$GREENHOUSE_BRANCH\"\n- build=\"$GREENHOUSE_BUILD_NUMBER\"\n- build_url=$(urlencode + \"$GREENHOUSE_BUILD_URL\")\n- pr=\"$GREENHOUSE_PULL_REQUEST\"\n- commit=\"$GREENHOUSE_COMMIT\"\n- search_in=\"$search_in + $GREENHOUSE_EXPORT_DIR\"\n-\n-elif [ \"$GITLAB_CI\" != \"\" ];\n-then\n- say + \"$e==>$x GitLab CI detected.\"\n- # http://doc.gitlab.com/ce/ci/variables/README.html\n- service=\"gitlab\"\n- branch=\"${CI_BUILD_REF_NAME:-$CI_COMMIT_REF_NAME}\"\n- build=\"${CI_BUILD_ID:-$CI_JOB_ID}\"\n- remote_addr=\"${CI_BUILD_REPO:-$CI_REPOSITORY_URL}\"\n- commit=\"${CI_BUILD_REF:-$CI_COMMIT_SHA}\"\n- slug=\"${CI_PROJECT_PATH}\"\n-\n-elif + [ \"$GITHUB_ACTION\" != \"\" ];\n-then\n- say \"$e==>$x GitHub Actions detected.\"\n-\n- # + https://github.com/features/actions\n- service=\"github-actions\"\n-\n- # + https://help.github.com/en/articles/virtual-environments-for-github-actions#environment-variables\n- branch=\"${GITHUB_REF#refs/heads/}\"\n- commit=\"${GITHUB_SHA}\"\n- slug=\"${GITHUB_REPOSITORY}\"\n- job=\"${GITHUB_ACTION}\"\n-\n-elif + [ \"$SYSTEM_TEAMFOUNDATIONSERVERURI\" != \"\" ];\n-then\n- say \"$e==>$x Azure + Pipelines detected.\"\n- # https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=vsts\n- service=\"azure_pipelines\"\n- commit=\"$BUILD_SOURCEVERSION\"\n- build=\"$BUILD_BUILDNUMBER\"\n- if + [ -z \"$PULL_REQUEST_NUMBER\" ];\n- then\n- pr=\"$PULL_REQUEST_ID\"\n- else\n- pr=\"$PULL_REQUEST_NUMBER\"\n- fi\n- project=\"${SYSTEM_TEAMPROJECT}\"\n- server_uri=\"${SYSTEM_TEAMFOUNDATIONSERVERURI}\"\n- job=\"${BUILD_BUILDID}\"\n- branch=\"$BUILD_SOURCEBRANCHNAME\"\n- build_url=$(urlencode + \"${SYSTEM_TEAMFOUNDATIONSERVERURI}${SYSTEM_TEAMPROJECT}/_build/results?buildId=${BUILD_BUILDID}\")\n-elif + [ \"$CI\" = \"true\" ] && [ \"$BITBUCKET_BUILD_NUMBER\" != \"\" ];\n-then\n- say + \"$e==>$x Bitbucket detected.\"\n- # https://confluence.atlassian.com/bitbucket/variables-in-pipelines-794502608.html\n- service=\"bitbucket\"\n- branch=\"$BITBUCKET_BRANCH\"\n- build=\"$BITBUCKET_BUILD_NUMBER\"\n- slug=\"$BITBUCKET_REPO_OWNER/$BITBUCKET_REPO_SLUG\"\n- job=\"$BITBUCKET_BUILD_NUMBER\"\n- pr=\"$BITBUCKET_PR_ID\"\n-else\n- say + \"${r}x>${x} No CI provider detected.\"\n- say \" Testing inside Docker? + ${b}http://docs.codecov.io/docs/testing-with-docker${x}\"\n- say \" Testing + with Tox? ${b}https://docs.codecov.io/docs/python#section-testing-with-tox${x}\"\n-\n-fi\n-\n-say + \" ${e}project root:${x} $git_root\"\n-\n-# find branch, commit, repo from + git command\n-if [ \"$GIT_BRANCH\" != \"\" ];\n-then\n- branch=\"$GIT_BRANCH\"\n-\n-elif + [ \"$branch\" = \"\" ];\n-then\n- branch=$(git rev-parse --abbrev-ref HEAD + 2>/dev/null || hg branch 2>/dev/null || echo \"\")\n- if [ \"$branch\" = \"HEAD\" + ];\n- then\n- branch=\"\"\n- fi\n-fi\n-\n-if [ \"$commit_o\" = \"\" ];\n-then\n- # + merge commit -> actual commit\n- mc=\n- if [ -n \"$pr\" ] && [ \"$pr\" != + false ];\n- then\n- mc=$(git show --no-patch --format=\"%P\" 2>/dev/null + || echo \"\")\n- fi\n- if [[ \"$mc\" =~ ^[a-z0-9]{40}[[:space:]][a-z0-9]{40}$ + ]];\n- then\n- say \" Fixing merge commit SHA\"\n- commit=$(echo \"$mc\" + | cut -d'' '' -f2)\n- elif [ \"$GIT_COMMIT\" != \"\" ];\n- then\n- commit=\"$GIT_COMMIT\"\n- elif + [ \"$commit\" = \"\" ];\n- then\n- commit=$(git log -1 --format=\"%H\" 2>/dev/null + || hg id -i --debug 2>/dev/null | tr -d ''+'' || echo \"\")\n- fi\n-else\n- commit=\"$commit_o\"\n-fi\n-\n-if + [ \"$CODECOV_TOKEN\" != \"\" ] && [ \"$token\" = \"\" ];\n-then\n- say \"${e}-->${x} + token set from env\"\n- token=\"$CODECOV_TOKEN\"\n-fi\n-\n-if [ \"$CODECOV_URL\" + != \"\" ] && [ \"$url_o\" = \"\" ];\n-then\n- say \"${e}-->${x} url set from + env\"\n- url_o=$(echo \"$CODECOV_URL\" | sed -e ''s/\\/$//'')\n-fi\n-\n-if + [ \"$CODECOV_SLUG\" != \"\" ];\n-then\n- say \"${e}-->${x} slug set from env\"\n- slug_o=\"$CODECOV_SLUG\"\n-\n-elif + [ \"$slug\" = \"\" ];\n-then\n- if [ \"$remote_addr\" = \"\" ];\n- then\n- remote_addr=$(git + config --get remote.origin.url || hg paths default || echo '''')\n- fi\n- if + [ \"$remote_addr\" != \"\" ];\n- then\n- if echo \"$remote_addr\" | grep + -q \"//\"; then\n- # https\n- slug=$(echo \"$remote_addr\" | cut -d + / -f 4,5 | sed -e ''s/\\.git$//'')\n- else\n- # ssh\n- slug=$(echo + \"$remote_addr\" | cut -d : -f 2 | sed -e ''s/\\.git$//'')\n- fi\n- fi\n- if + [ \"$slug\" = \"/\" ];\n- then\n- slug=\"\"\n- fi\n-fi\n-\n-yaml=$(test + -n \"$codecov_yml\" && echo \"$codecov_yml\" \\\n- || cd \"$git_root\" + && \\\n- git ls-files \"*codecov.yml\" \"*codecov.yaml\" 2>/dev/null + \\\n- || hg locate \"*codecov.yml\" \"*codecov.yaml\" 2>/dev/null \\\n- || + cd $proj_root && find . -type f -name ''*codecov.y*ml'' -depth 1 2>/dev/null + \\\n- || echo '''')\n-yaml=$(echo \"$yaml\" | head -1)\n-\n-if [ \"$yaml\" + != \"\" ];\n-then\n- say \" ${e}Yaml found at:${x} $yaml\"\n- config=$(parse_yaml + \"$git_root/$yaml\" || echo '''')\n-\n- # TODO validate the yaml here\n-\n- if + [ \"$(echo \"$config\" | grep ''codecov_token=\"'')\" != \"\" ] && [ \"$token\" + = \"\" ];\n- then\n- say \"${e}-->${x} token set from yaml\"\n- token=\"$(echo + \"$config\" | grep ''codecov_token=\"'' | sed -e ''s/codecov_token=\"//'' | + sed -e ''s/\"\\.*//'')\"\n- fi\n-\n- if [ \"$(echo \"$config\" | grep ''codecov_url=\"'')\" + != \"\" ] && [ \"$url_o\" = \"\" ];\n- then\n- say \"${e}-->${x} url set + from yaml\"\n- url_o=\"$(echo \"$config\" | grep ''codecov_url=\"'' | sed + -e ''s/codecov_url=\"//'' | sed -e ''s/\"\\.*//'')\"\n- fi\n-\n- if [ \"$(echo + \"$config\" | grep ''codecov_slug=\"'')\" != \"\" ] && [ \"$slug_o\" = \"\" + ];\n- then\n- say \"${e}-->${x} slug set from yaml\"\n- slug_o=\"$(echo + \"$config\" | grep ''codecov_slug=\"'' | sed -e ''s/codecov_slug=\"//'' | sed + -e ''s/\"\\.*//'')\"\n- fi\n-else\n- say \" ${g}Yaml not found, that''s + ok! Learn more at${x} ${b}http://docs.codecov.io/docs/codecov-yaml${x}\"\n-\n-fi\n-\n-if + [ \"$branch_o\" != \"\" ];\n-then\n- branch=$(urlencode \"$branch_o\")\n-else\n- branch=$(urlencode + \"$branch\")\n-fi\n-\n-query=\"branch=$branch\\\n- &commit=$commit\\\n- &build=$([ + \"$build_o\" = \"\" ] && echo \"$build\" || echo \"$build_o\")\\\n- &build_url=$build_url\\\n- &name=$(urlencode + \"$name\")\\\n- &tag=$([ \"$tag_o\" = \"\" ] && echo \"$tag\" || echo + \"$tag_o\")\\\n- &slug=$([ \"$slug_o\" = \"\" ] && urlencode \"$slug\" + || urlencode \"$slug_o\")\\\n- &service=$service\\\n- &flags=$flags\\\n- &pr=$([ + \"$pr_o\" = \"\" ] && echo \"${pr##\\#}\" || echo \"${pr_o##\\#}\")\\\n- &job=$job\"\n-\n-if + [ ! -z \"$project\" ] && [ ! -z \"$server_uri\" ];\n-then\n- query=$(echo \"$query&project=$project&server_uri=$server_uri\" + | tr -d '' '')\n-fi\n-\n-if [ \"$parent\" != \"\" ];\n-then\n- query=$(echo + \"parent=$parent&$query\" | tr -d '' '')\n-fi\n-\n-if [ \"$ft_search\" = \"1\" + ];\n-then\n- # detect bower comoponents location\n- bower_components=\"bower_components\"\n- bower_rc=$(cd + \"$git_root\" && cat .bowerrc 2>/dev/null || echo \"\")\n- if [ \"$bower_rc\" + != \"\" ];\n- then\n- bower_components=$(echo \"$bower_rc\" | tr -d ''\\n'' + | grep ''\"directory\"'' | cut -d''\"'' -f4 | sed -e ''s/\\/$//'')\n- if + [ \"$bower_components\" = \"\" ];\n- then\n- bower_components=\"bower_components\"\n- fi\n- fi\n-\n- # + Swift Coverage\n- if [ \"$ft_xcodellvm\" = \"1\" ] && [ -d \"$ddp\" ];\n- then\n- say + \"${e}==>${x} Processing Xcode reports via llvm-cov\"\n- say \" DerivedData + folder: $ddp\"\n- profdata_files=$(find \"$ddp\" -name ''*.profdata'' 2>/dev/null + || echo '''')\n- if [ \"$profdata_files\" != \"\" ];\n- then\n- # + xcode via profdata\n- if [ \"$xp\" = \"\" ];\n- then\n- # xp=$(xcodebuild + -showBuildSettings 2>/dev/null | grep -i \"^\\s*PRODUCT_NAME\" | sed -e ''s/.*= + \\(.*\\)/\\1/'')\n- # say \" ${e}->${x} Speed up Xcode processing by + adding ${e}-J ''$xp''${x}\"\n- say \" ${g}hint${x} Speed up Swift + processing by using use ${g}-J ''AppName''${x} (regexp accepted)\"\n- say + \" ${g}hint${x} This will remove Pods/ from your report. Also ${b}https://docs.codecov.io/docs/ignoring-paths${x}\"\n- fi\n- while + read -r profdata;\n- do\n- if [ \"$profdata\" != \"\" ];\n- then\n- swiftcov + \"$profdata\" \"$xp\"\n- fi\n- done <<< \"$profdata_files\"\n- else\n- say + \" ${e}->${x} No Swift coverage found\"\n- fi\n-\n- # Obj-C Gcov Coverage\n- if + [ \"$ft_gcov\" = \"1\" ];\n- then\n- say \" ${e}->${x} Running $gcov_exe + for Obj-C\"\n- if [ \"$ft_gcovout\" = \"1\" ];\n- then\n- # + suppress gcov output\n- bash -c \"find $ddp -type f -name ''*.gcda'' + $gcov_include $gcov_ignore -exec $gcov_exe -p $gcov_arg {} +\" || true 2>/dev/null\n- else\n- bash + -c \"find $ddp -type f -name ''*.gcda'' $gcov_include $gcov_ignore -exec $gcov_exe + -p $gcov_arg {} +\" || true\n- fi\n- fi\n- fi\n-\n- if [ \"$ft_xcodeplist\" + = \"1\" ] && [ -d \"$ddp\" ];\n- then\n- say \"${e}==>${x} Processing Xcode + plists\"\n- plists_files=$(find \"$ddp\" -name ''*.xccoverage'' 2>/dev/null + || echo '''')\n- if [ \"$plists_files\" != \"\" ];\n- then\n- while + read -r plist;\n- do\n- if [ \"$plist\" != \"\" ];\n- then\n- say + \" ${g}Found${x} plist file at $plist\"\n- plutil -convert xml1 + -o \"$(basename \"$plist\").plist\" -- $plist\n- fi\n- done <<< + \"$plists_files\"\n- fi\n- fi\n-\n- # Gcov Coverage\n- if [ \"$ft_gcov\" + = \"1\" ];\n- then\n- say \"${e}==>${x} Running gcov in $proj_root ${e}(disable + via -X gcov)${x}\"\n- bash -c \"find $proj_root -type f -name ''*.gcno'' + $gcov_include $gcov_ignore -execdir $gcov_exe -pb $gcov_arg {} +\" || true\n- else\n- say + \"${e}==>${x} gcov disabled\"\n- fi\n-\n- # Python Coverage\n- if [ \"$ft_coveragepy\" + = \"1\" ];\n- then\n- if [ ! -f coverage.xml ];\n- then\n- if which + coverage >/dev/null 2>&1;\n- then\n- say \"${e}==>${x} Python coveragepy + exists ${e}disable via -X coveragepy${x}\"\n-\n- dotcoverage=$(find \"$git_root\" + -name ''.coverage'' -or -name ''.coverage.*'' | head -1 || echo '''')\n- if + [ \"$dotcoverage\" != \"\" ];\n- then\n- cd \"$(dirname \"$dotcoverage\")\"\n- if + [ ! -f .coverage ];\n- then\n- say \" ${e}->${x} Running + coverage combine\"\n- coverage combine -a\n- fi\n- say + \" ${e}->${x} Running coverage xml\"\n- if [ \"$(coverage xml -i)\" + != \"No data to report.\" ];\n- then\n- files=\"$files\n-$PWD/coverage.xml\"\n- else\n- say + \" ${r}No data to report.${x}\"\n- fi\n- cd \"$proj_root\"\n- else\n- say + \" ${r}No .coverage file found.${x}\"\n- fi\n- else\n- say + \"${e}==>${x} Python coveragepy not found\"\n- fi\n- fi\n- else\n- say + \"${e}==>${x} Python coveragepy disabled\"\n- fi\n-\n- if [ \"$search_in_o\" + != \"\" ];\n- then\n- # location override\n- search_in=\"$search_in_o\"\n- fi\n-\n- say + \"$e==>$x Searching for coverage reports in:\"\n- for _path in $search_in\n- do\n- say + \" ${g}+${x} $_path\"\n- done\n-\n- patterns=\"find $search_in \\( \\\n- -name + vendor \\\n- -or -name htmlcov \\\n- -or + -name virtualenv \\\n- -or -name js/generated/coverage + \\\n- -or -name .virtualenv \\\n- -or + -name virtualenvs \\\n- -or -name .virtualenvs \\\n- -or + -name .env \\\n- -or -name .envs \\\n- -or + -name env \\\n- -or -name .yarn-cache \\\n- -or + -name envs \\\n- -or -name .venv \\\n- -or + -name .venvs \\\n- -or -name venv \\\n- -or + -name venvs \\\n- -or -name .git \\\n- -or + -name .hg \\\n- -or -name .tox \\\n- -or + -name __pycache__ \\\n- -or -name ''.egg-info*'' \\\n- -or + -name ''$bower_components'' \\\n- -or -name node_modules + \\\n- -or -name ''conftest_*.c.gcov'' \\\n- \\) + -prune -or \\\n- -type f \\( -name ''*coverage*.*'' \\\n- -or + -name ''nosetests.xml'' \\\n- -or -name ''jacoco*.xml'' + \\\n- -or -name ''clover.xml'' \\\n- -or + -name ''report.xml'' \\\n- -or -name ''*.codecov.*'' \\\n- -or + -name ''codecov.*'' \\\n- -or -name ''cobertura.xml'' \\\n- -or + -name ''excoveralls.json'' \\\n- -or -name ''luacov.report.out'' + \\\n- -or -name ''coverage-final.json'' \\\n- -or + -name ''naxsi.info'' \\\n- -or -name ''lcov.info'' \\\n- -or + -name ''lcov.dat'' \\\n- -or -name ''*.lcov'' \\\n- -or + -name ''*.clover'' \\\n- -or -name ''cover.out'' \\\n- -or + -name ''gcov.info'' \\\n- -or -name ''*.gcov'' \\\n- -or + -name ''*.lst'' \\\n- $include_cov \\) \\\n- $exclude_cov + \\\n- -not -name ''*.profdata'' \\\n- -not + -name ''coverage-summary.json'' \\\n- -not -name ''phpunit-code-coverage.xml'' + \\\n- -not -name ''*/classycle/report.xml'' \\\n- -not + -name ''remapInstanbul.coverage*.json'' \\\n- -not -name + ''phpunit-coverage.xml'' \\\n- -not -name ''*codecov.yml'' + \\\n- -not -name ''*.serialized'' \\\n- -not + -name ''.coverage*'' \\\n- -not -name ''.*coveragerc'' \\\n- -not + -name ''*.sh'' \\\n- -not -name ''*.bat'' \\\n- -not + -name ''*.ps1'' \\\n- -not -name ''*.env'' \\\n- -not + -name ''*.cmake'' \\\n- -not -name ''*.dox'' \\\n- -not + -name ''*.ec'' \\\n- -not -name ''*.rst'' \\\n- -not + -name ''*.h'' \\\n- -not -name ''*.scss'' \\\n- -not + -name ''*.o'' \\\n- -not -name ''*.proto'' \\\n- -not + -name ''*.sbt'' \\\n- -not -name ''*.xcoverage.*'' \\\n- -not + -name ''*.gz'' \\\n- -not -name ''*.conf'' \\\n- -not + -name ''*.p12'' \\\n- -not -name ''*.csv'' \\\n- -not + -name ''*.rsp'' \\\n- -not -name ''*.m4'' \\\n- -not + -name ''*.pem'' \\\n- -not -name ''*~'' \\\n- -not + -name ''*.exe'' \\\n- -not -name ''*.am'' \\\n- -not + -name ''*.template'' \\\n- -not -name ''*.cp'' \\\n- -not + -name ''*.bw'' \\\n- -not -name ''*.crt'' \\\n- -not + -name ''*.log'' \\\n- -not -name ''*.cmake'' \\\n- -not + -name ''*.pth'' \\\n- -not -name ''*.in'' \\\n- -not + -name ''*.jar*'' \\\n- -not -name ''*.pom*'' \\\n- -not + -name ''*.png'' \\\n- -not -name ''*.jpg'' \\\n- -not + -name ''*.sql'' \\\n- -not -name ''*.jpeg'' \\\n- -not + -name ''*.svg'' \\\n- -not -name ''*.gif'' \\\n- -not + -name ''*.csv'' \\\n- -not -name ''*.snapshot'' \\\n- -not + -name ''*.mak*'' \\\n- -not -name ''*.bash'' \\\n- -not + -name ''*.data'' \\\n- -not -name ''*.py'' \\\n- -not + -name ''*.class'' \\\n- -not -name ''*.xcconfig'' \\\n- -not + -name ''*.ec'' \\\n- -not -name ''*.coverage'' \\\n- -not + -name ''*.pyc'' \\\n- -not -name ''*.cfg'' \\\n- -not + -name ''*.egg'' \\\n- -not -name ''*.ru'' \\\n- -not + -name ''*.css'' \\\n- -not -name ''*.less'' \\\n- -not + -name ''*.pyo'' \\\n- -not -name ''*.whl'' \\\n- -not + -name ''*.html'' \\\n- -not -name ''*.ftl'' \\\n- -not + -name ''*.erb'' \\\n- -not -name ''*.rb'' \\\n- -not + -name ''*.js'' \\\n- -not -name ''*.jade'' \\\n- -not + -name ''*.db'' \\\n- -not -name ''*.md'' \\\n- -not + -name ''*.cpp'' \\\n- -not -name ''*.gradle'' \\\n- -not + -name ''*.tar.tz'' \\\n- -not -name ''*.scss'' \\\n- -not + -name ''include.lst'' \\\n- -not -name ''fullLocaleNames.lst'' + \\\n- -not -name ''inputFiles.lst'' \\\n- -not + -name ''createdFiles.lst'' \\\n- -not -name ''scoverage.measurements.*'' + \\\n- -not -name ''test_*_coverage.txt'' \\\n- -not + -name ''testrunner-coverage*'' \\\n- -print 2>/dev/null\"\n- files=$(eval + \"$patterns\" || echo '''')\n-\n-elif [ \"$include_cov\" != \"\" ];\n-then\n- files=$(eval + \"find $search_in -type f \\( ${include_cov:5} \\)$exclude_cov 2>/dev/null\" + || echo '''')\n-fi\n-\n-num_of_files=$(echo \"$files\" | wc -l | tr -d '' '')\n-if + [ \"$num_of_files\" != '''' ] && [ \"$files\" != '''' ];\n-then\n- say \" ${e}->${x} + Found $num_of_files reports\"\n-fi\n-\n-# no files found\n-if [ \"$files\" = + \"\" ];\n-then\n- say \"${r}-->${x} No coverage report found.\"\n- say \" Please + visit ${b}http://docs.codecov.io/docs/supported-languages${x}\"\n- exit ${exit_with};\n-fi\n-\n-if + [ \"$ft_network\" == \"1\" ];\n-then\n- say \"${e}==>${x} Detecting git/mercurial + file structure\"\n- network=$(cd \"$git_root\" && git ls-files 2>/dev/null + || hg locate 2>/dev/null || echo \"\")\n- if [ \"$network\" = \"\" ];\n- then\n- network=$(find + \"$git_root\" \\( \\\n- -name virtualenv \\\n- -name + .virtualenv \\\n- -name virtualenvs \\\n- -name + .virtualenvs \\\n- -name ''*.png'' \\\n- -name + ''*.gif'' \\\n- -name ''*.jpg'' \\\n- -name + ''*.jpeg'' \\\n- -name ''*.md'' \\\n- -name + .env \\\n- -name .envs \\\n- -name env \\\n- -name + envs \\\n- -name .venv \\\n- -name .venvs + \\\n- -name venv \\\n- -name venvs \\\n- -name + .git \\\n- -name .egg-info \\\n- -name shunit2-2.1.6 + \\\n- -name vendor \\\n- -name __pycache__ + \\\n- -name node_modules \\\n- -path ''*/$bower_components/*'' + \\\n- -path ''*/target/delombok/*'' \\\n- -path + ''*/build/lib/*'' \\\n- -path ''*/js/generated/coverage/*'' + \\\n- \\) -prune -or \\\n- -type f -print + 2>/dev/null || echo '''')\n- fi\n-\n- if [ \"$prefix_o\" != \"\" ];\n- then\n- network=$(echo + \"$network\" | awk \"{print \\\"$prefix_o/\\\"\\$0}\")\n- fi\n-fi\n-\n-upload_file=`mktemp + /tmp/codecov.XXXXXX`\n-adjustments_file=`mktemp /tmp/codecov.adjustments.XXXXXX`\n-\n-cleanup() + {\n- rm -f $upload_file $adjustments_file $upload_file.gz\n-}\n-\n-trap cleanup + INT ABRT TERM\n-\n-if [ \"$env\" != \"\" ];\n-then\n- inc_env=\"\"\n- say + \"${e}==>${x} Appending build variables\"\n- for varname in $(echo \"$env\" + | tr '','' '' '')\n- do\n- if [ \"$varname\" != \"\" ];\n- then\n- say + \" ${g}+${x} $varname\"\n- inc_env=\"${inc_env}${varname}=$(eval echo + \"\\$${varname}\")\n-\"\n- fi\n- done\n-\n-echo \"$inc_env<<<<<< ENV\" >> + $upload_file\n-fi\n-\n-# Append git file list\n-# write discovered yaml location\n-echo + \"$yaml\" >> $upload_file\n-if [ \"$ft_network\" == \"1\" ];\n-then\n- i=\"woff|eot|otf\" # + fonts\n- i=\"$i|gif|png|jpg|jpeg|psd\" # images\n- i=\"$i|ptt|pptx|numbers|pages|md|txt|xlsx|docx|doc|pdf|html|csv\" # + docs\n- i=\"$i|yml|yaml|.gitignore\" # supporting docs\n- echo \"$network\" + | grep -vwE \"($i)$\" >> $upload_file\n-fi\n-echo \"<<<<<< network\" >> $upload_file\n-\n-fr=0\n-say + \"${e}==>${x} Reading reports\"\n-while IFS='''' read -r file;\n-do\n- # read + the coverage file\n- if [ \"$(echo \"$file\" | tr -d '' '')\" != '''' ];\n- then\n- if + [ -f \"$file\" ];\n- then\n- report_len=$(wc -c < \"$file\")\n- if + [ \"$report_len\" -ne 0 ];\n- then\n- say \" ${g}+${x} $file + ${e}bytes=$(echo \"$report_len\" | tr -d '' '')${x}\"\n- # append to + to upload\n- _filename=$(basename \"$file\")\n- if [ \"${_filename##*.}\" + = ''gcov'' ];\n- then\n- echo \"# path=$(echo \"$file.reduced\" + | sed \"s|^$git_root/||\")\" >> $upload_file\n- # get file name\n- head + -1 $file >> $upload_file\n- # 1. remove source code\n- # 2. + remove ending bracket lines\n- # 3. remove whitespace\n- # + 4. remove contextual lines\n- # 5. remove function names\n- awk + -F'': *'' ''{print $1\":\"$2\":\"}'' $file \\\n- | sed ''\\/: *} + *$/d'' \\\n- | sed ''s/^ *//'' \\\n- | sed ''/^-/d'' \\\n- | + sed ''s/^function.*/func/'' >> $upload_file\n- else\n- echo + \"# path=$(echo \"$file\" | sed \"s|^$git_root/||\")\" >> $upload_file\n- cat + \"$file\" >> $upload_file\n- fi\n- echo \"<<<<<< EOF\" >> $upload_file\n- fr=1\n- if + [ \"$clean\" = \"1\" ];\n- then\n- rm \"$file\"\n- fi\n- else\n- say + \" ${r}-${x} Skipping empty file $file\"\n- fi\n- else\n- say + \" ${r}-${x} file not found at $file\"\n- fi\n- fi\n-done <<< \"$(echo + -e \"$files\")\"\n-\n-if [ \"$fr\" = \"0\" ];\n-then\n- say \"${r}-->${x} No + coverage data found.\"\n- say \" Please visit ${b}http://docs.codecov.io/docs/supported-languages${x}\"\n- say + \" search for your projects language to learn how to collect reports.\"\n- exit + ${exit_with};\n-fi\n-\n-if [ \"$ft_fix\" = \"1\" ];\n-then\n- say \"${e}==>${x} + Appending adjustments\"\n- say \" ${b}http://docs.codecov.io/docs/fixing-reports${x}\"\n-\n- empty_line=''^[[:space:]]*$''\n- # + //\n- syntax_comment=''^[[:space:]]*//.*''\n- # /* or */\n- syntax_comment_block=''^[[:space:]]*(\\/\\*|\\*\\/)[[:space:]]*$''\n- # + { or }\n- syntax_bracket=''^[[:space:]]*[\\{\\}][[:space:]]*(//.*)?$''\n- # + [ or ]\n- syntax_list=''^[[:space:]]*[][][[:space:]]*(//.*)?$''\n-\n- skip_dirs=\"-not + -path ''*/$bower_components/*'' \\\n- -not -path ''*/node_modules/*''\"\n-\n- cut_and_join() + {\n- awk ''BEGIN { FS=\":\" }\n- $3 ~ /\\/\\*/ || $3 ~ /\\*\\// { + print $0 ; next }\n- $1!=key { if (key!=\"\") print out ; key=$1 ; out=$1\":\"$2 + ; next }\n- { out=out\",\"$2 }\n- END { print out }'' 2>/dev/null\n- }\n-\n- if + echo \"$network\" | grep -m1 ''.kt$'' 1>/dev/null;\n- then\n- # skip brackets + and comments\n- find \"$git_root\" -type f \\\n- -name + ''*.kt'' \\\n- -exec \\\n- grep -nIHE -e $syntax_bracket + \\\n- -e $syntax_comment_block {} \\; \\\n- | cut_and_join + \\\n- >> $adjustments_file \\\n- || echo ''''\n-\n- # last line + in file\n- find \"$git_root\" -type f \\\n- -name ''*.kt'' + -exec \\\n- wc -l {} \\; \\\n- | while read l; do echo \"EOF: $l\"; + done \\\n- 2>/dev/null \\\n- >> $adjustments_file \\\n- || echo + ''''\n-\n- fi\n-\n- if echo \"$network\" | grep -m1 ''.go$'' 1>/dev/null;\n- then\n- # + skip empty lines, comments, and brackets\n- find \"$git_root\" -not -path + ''*/vendor/*'' \\\n- -type f \\\n- -name + ''*.go'' \\\n- -exec \\\n- grep -nIHE \\\n- -e + $empty_line \\\n- -e $syntax_comment \\\n- -e $syntax_comment_block + \\\n- -e $syntax_bracket \\\n- {} \\; \\\n- | cut_and_join + \\\n- >> $adjustments_file \\\n- || echo ''''\n- fi\n-\n- if echo + \"$network\" | grep -m1 ''.dart$'' 1>/dev/null;\n- then\n- # skip brackets\n- find + \"$git_root\" -type f \\\n- -name ''*.dart'' \\\n- -exec + \\\n- grep -nIHE \\\n- -e $syntax_bracket \\\n- {} + \\; \\\n- | cut_and_join \\\n- >> $adjustments_file \\\n- || + echo ''''\n- fi\n-\n- if echo \"$network\" | grep -m1 ''.php$'' 1>/dev/null;\n- then\n- # + skip empty lines, comments, and brackets\n- find \"$git_root\" -not -path + \"*/vendor/*\" \\\n- -type f \\\n- -name + ''*.php'' \\\n- -exec \\\n- grep -nIHE \\\n- -e + $syntax_list \\\n- -e $syntax_bracket \\\n- -e ''^[[:space:]]*\\);[[:space:]]*(//.*)?$'' + \\\n- {} \\; \\\n- | cut_and_join \\\n- >> $adjustments_file + \\\n- || echo ''''\n- fi\n-\n- if echo \"$network\" | grep -m1 ''\\(.cpp\\|.h\\|.cxx\\|.c\\|.hpp\\|.m\\)$'' + 1>/dev/null;\n- then\n- # skip brackets\n- find \"$git_root\" -type f + \\\n- $skip_dirs \\\n- \\( \\\n- -name + ''*.h'' \\\n- -or -name ''*.cpp'' \\\n- -or -name ''*.cxx'' + \\\n- -or -name ''*.m'' \\\n- -or -name ''*.c'' \\\n- -or + -name ''*.hpp'' \\\n- \\) -exec \\\n- grep -nIHE \\\n- -e + $empty_line \\\n- -e $syntax_bracket \\\n- -e ''// LCOV_EXCL'' + \\\n- {} \\; \\\n- | cut_and_join \\\n- >> $adjustments_file + \\\n- || echo ''''\n-\n- # skip brackets\n- find \"$git_root\" -type + f \\\n- $skip_dirs \\\n- \\( \\\n- -name + ''*.h'' \\\n- -or -name ''*.cpp'' \\\n- -or -name ''*.cxx'' + \\\n- -or -name ''*.m'' \\\n- -or -name ''*.c'' \\\n- -or + -name ''*.hpp'' \\\n- \\) -exec \\\n- grep -nIH ''// LCOV_EXCL'' + \\\n- {} \\; \\\n- >> $adjustments_file \\\n- || echo ''''\n-\n- fi\n-\n- found=$(cat + $adjustments_file | tr -d '' '')\n-\n- if [ \"$found\" != \"\" ];\n- then\n- say + \" ${g}+${x} Found adjustments\"\n- echo \"# path=fixes\" >> $upload_file\n- cat + $adjustments_file >> $upload_file\n- echo \"<<<<<< EOF\" >> $upload_file\n- rm + -rf $adjustments_file\n- else\n- say \" ${e}->${x} No adjustments found\"\n- fi\n-fi\n-\n-if + [ \"$url_o\" != \"\" ];\n-then\n- url=\"$url_o\"\n-fi\n-\n-if [ \"$dump\" != + \"0\" ];\n-then\n- # trim whitespace from query\n- say \" ${e}->${x} Dumping + upload file (no upload)\"\n- echo \"$url/upload/v4?$(echo \"package=bash-$VERSION&token=$token&$query\" + | tr -d '' '')\"\n- cat $upload_file\n-else\n-\n- say \"${e}==>${x} Gzipping + contents\"\n- gzip -nf9 $upload_file\n-\n- query=$(echo \"${query}\" | tr + -d '' '')\n- say \"${e}==>${x} Uploading reports\"\n- say \" ${e}url:${x} + $url\"\n- say \" ${e}query:${x} $query\"\n-\n- # Full query without token + (to display on terminal output)\n- queryNoToken=$(echo \"package=bash-$VERSION&token=secret&$query\" + | tr -d '' '')\n- # now add token to query\n- query=$(echo \"package=bash-$VERSION&token=$token&$query\" + | tr -d '' '')\n-\n- if [ \"$ft_s3\" = \"1\" ];\n- then\n- i=\"0\"\n- while + [ $i -lt 4 ]\n- do\n- i=$[$i+1]\n- say \" ${e}->${x} Pinging + Codecov\"\n- say \"$url/upload/v4?$queryNoToken\"\n- res=$(curl $curl_s + -X POST $curlargs $cacert \\\n- -H ''X-Reduced-Redundancy: false'' + \\\n- -H ''X-Content-Type: application/x-gzip'' \\\n- \"$url/upload/v4?$query\" + || true)\n- # a good replay is \"https://codecov.io\" + \"\\n\" + \"https://codecov.s3.amazonaws.com/...\"\n- status=$(echo + \"$res\" | head -1 | grep ''HTTP '' | cut -d'' '' -f2)\n- if [ \"$status\" + = \"\" ];\n- then\n- s3target=$(echo \"$res\" | sed -n 2p)\n- say + \" ${e}->${x} Uploading\"\n- \n- \n- s3=$(curl $curl_s + -fiX PUT $curlawsargs \\\n- --data-binary @$upload_file.gz \\\n- -H + ''Content-Type: application/x-gzip'' \\\n- -H ''Content-Encoding: + gzip'' \\\n- -H ''x-amz-acl: public-read'' \\\n- \"$s3target\" + || true)\n- \n-\n- if [ \"$s3\" != \"\" ];\n- then\n- say + \" ${g}->${x} View reports at ${b}$(echo \"$res\" | sed -n 1p)${x}\"\n- exit + 0\n- else\n- say \" ${r}X>${x} Failed to upload\"\n- fi\n- elif + [ \"$status\" = \"400\" ];\n- then\n- # 400 Error\n- say + \"${g}${res}${x}\"\n- exit ${exit_with}\n- fi\n- say \" ${e}->${x} + Sleeping for 30s and trying again...\"\n- sleep 30\n- done\n- fi\n-\n- say + \" ${e}->${x} Uploading to Codecov\"\n- i=\"0\"\n- while [ $i -lt 4 ]\n- do\n- i=$[$i+1]\n-\n- res=$(curl + $curl_s -X POST $curlargs $cacert \\\n- --data-binary @$upload_file.gz + \\\n- -H ''Content-Type: text/plain'' \\\n- -H ''Content-Encoding: + gzip'' \\\n- -H ''X-Content-Encoding: gzip'' \\\n- -H ''Accept: + text/plain'' \\\n- \"$url/upload/v2?$query\" || echo ''HTTP 500'')\n- # + HTTP 200\n- # http://....\n- status=$(echo \"$res\" | head -1 | cut -d'' + '' -f2)\n- if [ \"$status\" = \"\" ];\n- then\n- say \" View reports + at ${b}$(echo \"$res\" | head -2 | tail -1)${x}\"\n- exit 0\n-\n- elif + [ \"${status:0:1}\" = \"5\" ];\n- then\n- say \" ${e}->${x} Sleeping + for 30s and trying again...\"\n- sleep 30\n-\n- else\n- say \" ${g}${res}${x}\"\n- exit + 0\n- exit ${exit_with}\n- fi\n-\n- done\n-\n- say \" ${r}X> Failed + to upload coverage reports${x}\"\n-fi\n-\n-exit ${exit_with}"},{"sha":"49532268d6565b973ef58adcf63971f5a9e47898","filename":"flagone.coverage.xml","status":"removed","additions":0,"deletions":86,"changes":86,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/f0895290dc26668faeeb20ee5ccd4cc995925775/flagone.coverage.xml","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/f0895290dc26668faeeb20ee5ccd4cc995925775/flagone.coverage.xml","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/flagone.coverage.xml?ref=f0895290dc26668faeeb20ee5ccd4cc995925775","patch":"@@ + -1,86 +0,0 @@\n-\n-\n-\t\n-\t\n-\t\n-\t\t/Users/thiagorramos/Projects/clientenv/example-python\n-\t\n-\t\n-\t\t\n-\t\t\t\n-\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\n-\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\n-\t\t\t\n-\t\t\n-\t\t\n-\t\t\t\n-\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\n-\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\n-\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\n-\t\t\t\n-\t\t\n-\t\n-"},{"sha":"ae0db9038664db16b44e63c772de96bc0a8092d0","filename":"flagtwo.coverage.xml","status":"removed","additions":0,"deletions":86,"changes":86,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/f0895290dc26668faeeb20ee5ccd4cc995925775/flagtwo.coverage.xml","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/f0895290dc26668faeeb20ee5ccd4cc995925775/flagtwo.coverage.xml","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/flagtwo.coverage.xml?ref=f0895290dc26668faeeb20ee5ccd4cc995925775","patch":"@@ + -1,86 +0,0 @@\n-\n-\n-\t\n-\t\n-\t\n-\t\t/Users/thiagorramos/Projects/clientenv/example-python\n-\t\n-\t\n-\t\t\n-\t\t\t\n-\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\n-\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\n-\t\t\t\n-\t\t\n-\t\t\n-\t\t\t\n-\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\n-\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\n-\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\t\n-\t\t\t\t\t\n-\t\t\t\t\n-\t\t\t\n-\t\t\n-\t\n-"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 21:35:04 GMT + ETag: + - W/"b742e173229ed17156e3dfac7a65d1cab37c8648ad893cad9823a2044e61c746" + Last-Modified: + - Tue, 13 Oct 2020 15:15:31 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D7F0:4EA7:4D0CC:A4BF0:5F876F08 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4996' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '4' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_compare.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_compare.yaml new file mode 100644 index 0000000000..f41dd74e66 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_compare.yaml @@ -0,0 +1,135 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/compare/6ae5f17...b92edba + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/6ae5f17...b92edba","html_url":"https://github.com/ThiagoCodecov/example-python/compare/6ae5f17...b92edba","permalink_url":"https://github.com/ThiagoCodecov/example-python/compare/ThiagoCodecov:6ae5f17...ThiagoCodecov:b92edba","diff_url":"https://github.com/ThiagoCodecov/example-python/compare/6ae5f17...b92edba.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/compare/6ae5f17...b92edba.patch","base_commit":{"sha":"6ae5f1795a441884ed2847bb31154814ac01ef38","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjZhZTVmMTc5NWE0NDE4ODRlZDI4NDdiYjMxMTU0ODE0YWMwMWVmMzg=","commit":{"author":{"name":"Thomas + Pedbereznak","email":"tom@tomped.com","date":"2018-04-26T08:35:58Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2018-04-26T08:35:58Z"},"message":"Update + README.rst","tree":{"sha":"b5592410a15d7a596a8eaea6399766fbbbe0366c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b5592410a15d7a596a8eaea6399766fbbbe0366c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6ae5f1795a441884ed2847bb31154814ac01ef38","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJa4Y9uCRBK7hj4Ov3rIwAAdHIIAAyJAC1mOPnKmkDSzraV47Wq\nXma/2QidKpXnRqKgY6XFBXAH0RIpHpZ3NwGR/L2GH1l7xLjXtOMTvXOCjFBZUwRE\nLlM9IdoUFyPU2E9P0z0vfGR/nk5QC8PY9lzDwe/N8ZhR0j4M2rTM2ue97om9nJ4e\nmD+HR2ZwjKA9Z9zFeALgBjokKs44F6oN6lLuPYn06oiCnYB3ytlWJy+vpmEGLhoM\nL+a/ct2e6O5MmlpbRlKVME4FL0O4wDBMrAaFeeZgQTCl2LKfdsfYScJnypkB7X06\n6cDtC/TJ436n4PCTBRHVMDNGxzmgMgMFYbCPkJ27BeWlTuKVDcJ2msOV7ZJKqqs=\n=oGiR\n-----END + PGP SIGNATURE-----\n","payload":"tree b5592410a15d7a596a8eaea6399766fbbbe0366c\nparent + 8631ea09b9b689de0a348d5abf70bdd7273d2ae3\nauthor Thomas Pedbereznak + 1524731758 +0200\ncommitter GitHub 1524731758 +0200\n\nUpdate + README.rst"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38/comments","author":{"login":"TomPed","id":11602092,"node_id":"MDQ6VXNlcjExNjAyMDky","avatar_url":"https://avatars1.githubusercontent.com/u/11602092?v=4","gravatar_id":"","url":"https://api.github.com/users/TomPed","html_url":"https://github.com/TomPed","followers_url":"https://api.github.com/users/TomPed/followers","following_url":"https://api.github.com/users/TomPed/following{/other_user}","gists_url":"https://api.github.com/users/TomPed/gists{/gist_id}","starred_url":"https://api.github.com/users/TomPed/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/TomPed/subscriptions","organizations_url":"https://api.github.com/users/TomPed/orgs","repos_url":"https://api.github.com/users/TomPed/repos","events_url":"https://api.github.com/users/TomPed/events{/privacy}","received_events_url":"https://api.github.com/users/TomPed/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars3.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"8631ea09b9b689de0a348d5abf70bdd7273d2ae3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3"}]},"merge_base_commit":{"sha":"6ae5f1795a441884ed2847bb31154814ac01ef38","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjZhZTVmMTc5NWE0NDE4ODRlZDI4NDdiYjMxMTU0ODE0YWMwMWVmMzg=","commit":{"author":{"name":"Thomas + Pedbereznak","email":"tom@tomped.com","date":"2018-04-26T08:35:58Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2018-04-26T08:35:58Z"},"message":"Update + README.rst","tree":{"sha":"b5592410a15d7a596a8eaea6399766fbbbe0366c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b5592410a15d7a596a8eaea6399766fbbbe0366c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6ae5f1795a441884ed2847bb31154814ac01ef38","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJa4Y9uCRBK7hj4Ov3rIwAAdHIIAAyJAC1mOPnKmkDSzraV47Wq\nXma/2QidKpXnRqKgY6XFBXAH0RIpHpZ3NwGR/L2GH1l7xLjXtOMTvXOCjFBZUwRE\nLlM9IdoUFyPU2E9P0z0vfGR/nk5QC8PY9lzDwe/N8ZhR0j4M2rTM2ue97om9nJ4e\nmD+HR2ZwjKA9Z9zFeALgBjokKs44F6oN6lLuPYn06oiCnYB3ytlWJy+vpmEGLhoM\nL+a/ct2e6O5MmlpbRlKVME4FL0O4wDBMrAaFeeZgQTCl2LKfdsfYScJnypkB7X06\n6cDtC/TJ436n4PCTBRHVMDNGxzmgMgMFYbCPkJ27BeWlTuKVDcJ2msOV7ZJKqqs=\n=oGiR\n-----END + PGP SIGNATURE-----\n","payload":"tree b5592410a15d7a596a8eaea6399766fbbbe0366c\nparent + 8631ea09b9b689de0a348d5abf70bdd7273d2ae3\nauthor Thomas Pedbereznak + 1524731758 +0200\ncommitter GitHub 1524731758 +0200\n\nUpdate + README.rst"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38/comments","author":{"login":"TomPed","id":11602092,"node_id":"MDQ6VXNlcjExNjAyMDky","avatar_url":"https://avatars1.githubusercontent.com/u/11602092?v=4","gravatar_id":"","url":"https://api.github.com/users/TomPed","html_url":"https://github.com/TomPed","followers_url":"https://api.github.com/users/TomPed/followers","following_url":"https://api.github.com/users/TomPed/following{/other_user}","gists_url":"https://api.github.com/users/TomPed/gists{/gist_id}","starred_url":"https://api.github.com/users/TomPed/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/TomPed/subscriptions","organizations_url":"https://api.github.com/users/TomPed/orgs","repos_url":"https://api.github.com/users/TomPed/repos","events_url":"https://api.github.com/users/TomPed/events{/privacy}","received_events_url":"https://api.github.com/users/TomPed/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars3.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"8631ea09b9b689de0a348d5abf70bdd7273d2ae3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3"}]},"status":"ahead","ahead_by":4,"behind_by":0,"total_commits":4,"commits":[{"sha":"adb252173d2107fad86bcdcbc149884c2dd4c609","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmFkYjI1MjE3M2QyMTA3ZmFkODZiY2RjYmMxNDk4ODRjMmRkNGM2MDk=","commit":{"author":{"name":"Thomas + Pedbereznak","email":"tom@tomped.com","date":"2018-04-26T08:39:32Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2018-04-26T08:39:32Z"},"message":"Update + README.rst","tree":{"sha":"26b90aa0aa92d1d873342d7b95df4ad79f7db8b9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/26b90aa0aa92d1d873342d7b95df4ad79f7db8b9"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/adb252173d2107fad86bcdcbc149884c2dd4c609","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJa4ZBECRBK7hj4Ov3rIwAAdHIIAEoo6hDo1yVW2e9pe5R6cesa\nzQrd0cjAMvcjwvdDRAbAHkiNuJtJElO41xjyC4sAthl9zM1Wx1Jo4lc8+4CeJ2Vs\n3b3PDbwp6MLBJcwfhC/mox0PYPzTFO56r61HJI7T2CkBh9GXHAifXMHhkmYP0y5A\nGzeOE7FlP7Mz3N7NaXzlSPJbIZPD4X9swR0cqDZCFuD1R48QXi3+IbREUzO4KneM\nS4KwJQNPWRefH8pEkZBLZ8KFPL0ftXr6YuCKE7ySwoer7uQ0AXVHY5HcLSZD/js/\n9R7G+7CWkyBivTJAUFzql3j+A/ZiDTnXbEO6lty5K4LpvGD8kbkXTZB6QsIPC+g=\n=fcYU\n-----END + PGP SIGNATURE-----\n","payload":"tree 26b90aa0aa92d1d873342d7b95df4ad79f7db8b9\nparent + 6ae5f1795a441884ed2847bb31154814ac01ef38\nauthor Thomas Pedbereznak + 1524731972 +0200\ncommitter GitHub 1524731972 +0200\n\nUpdate + README.rst"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609","html_url":"https://github.com/ThiagoCodecov/example-python/commit/adb252173d2107fad86bcdcbc149884c2dd4c609","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609/comments","author":{"login":"TomPed","id":11602092,"node_id":"MDQ6VXNlcjExNjAyMDky","avatar_url":"https://avatars1.githubusercontent.com/u/11602092?v=4","gravatar_id":"","url":"https://api.github.com/users/TomPed","html_url":"https://github.com/TomPed","followers_url":"https://api.github.com/users/TomPed/followers","following_url":"https://api.github.com/users/TomPed/following{/other_user}","gists_url":"https://api.github.com/users/TomPed/gists{/gist_id}","starred_url":"https://api.github.com/users/TomPed/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/TomPed/subscriptions","organizations_url":"https://api.github.com/users/TomPed/orgs","repos_url":"https://api.github.com/users/TomPed/repos","events_url":"https://api.github.com/users/TomPed/events{/privacy}","received_events_url":"https://api.github.com/users/TomPed/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars3.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"6ae5f1795a441884ed2847bb31154814ac01ef38","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38"}]},{"sha":"6895b6479dbe12b5cb3baa02416c6343ddb888b4","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjY4OTViNjQ3OWRiZTEyYjVjYjNiYWEwMjQxNmM2MzQzZGRiODg4YjQ=","commit":{"author":{"name":"Jerrod","email":"jerrod@fundersclub.com","date":"2018-07-09T23:39:20Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2018-07-09T23:39:20Z"},"message":"Adding + ''include'' term if multiple sources\n\nbased on a support ticket around multiple + sources\r\n\r\nhttps://codecov.freshdesk.com/a/tickets/87","tree":{"sha":"3c47e2b9d9791503b56f0e4f78e76b9d061ad529","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3c47e2b9d9791503b56f0e4f78e76b9d061ad529"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJbQ/IoCRBK7hj4Ov3rIwAAdHIIAGm5AdlM8E0E7TyFKWgwPpjO\nsxiQswFXWosTZnJAn2NN/JF5aNqxUFLa9mo7Z+jztQuxrWsAFQsNFHf/t90iZi4w\ne0CkIHJdI8ukcae5/3eP+9h8GyqEq/RcvxYtvW6zYkWAK3Pyqwrs+qwH1MuLsl6E\n02fgD6T99Pq2V+3S1+dfgU6ot4IrMwT7aR+u9fCM8G4tF4y/5znIzuke6amVt52S\nUfjnHOHbDxdD4Mkxn8107zX1XmQ4BEzhh1kjTVd3Mean6ye7xsFxFGYHA5Zd1iyM\nCsmW5waqonRf03m1bQ9pYleufcwpr72iARLiBFhTOcAF6vpdoshO1qmTtsweFno=\n=vKnQ\n-----END + PGP SIGNATURE-----\n","payload":"tree 3c47e2b9d9791503b56f0e4f78e76b9d061ad529\nparent + adb252173d2107fad86bcdcbc149884c2dd4c609\nauthor Jerrod + 1531179560 -0700\ncommitter GitHub 1531179560 -0700\n\nAdding + ''include'' term if multiple sources\n\nbased on a support ticket around multiple + sources\r\n\r\nhttps://codecov.freshdesk.com/a/tickets/87"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6895b6479dbe12b5cb3baa02416c6343ddb888b4","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4/comments","author":null,"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars3.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"adb252173d2107fad86bcdcbc149884c2dd4c609","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/adb252173d2107fad86bcdcbc149884c2dd4c609","html_url":"https://github.com/ThiagoCodecov/example-python/commit/adb252173d2107fad86bcdcbc149884c2dd4c609"}]},{"sha":"c7f608036a3d2e89f8c59989ee213900c1ef39d1","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmM3ZjYwODAzNmEzZDJlODlmOGM1OTk4OWVlMjEzOTAwYzFlZjM5ZDE=","commit":{"author":{"name":"Jerrod","email":"jerrod@fundersclub.com","date":"2018-07-09T23:48:34Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2018-07-09T23:48:34Z"},"message":"Update + README.rst","tree":{"sha":"67a425bb5cdf5dba974649a92b9bd1b332ccbada","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/67a425bb5cdf5dba974649a92b9bd1b332ccbada"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJbQ/RSCRBK7hj4Ov3rIwAAdHIIADaMP9S0JFvlxV7y32ytgpMy\nHHzFrThiO4KivcY0JNiTLPTD9zdKYKeczLw2fV2GLZU/3Ho/msh9gk+GB07yxJiK\nSxQxW78XRBNeXMNtN1gQHTB/1XpDMk//uZRFD4CAY3Rf9n8MxKhtLV66vmvmInsu\n/pErsBSOyZH1plHejRJFloQCbHjwzVB8/OrtoV1P/woVsX6nmX59NHWsMo5rY80W\n/AEr58FzjXV0b0mQ05q9VjHVhFZqOwh981YWHrgv0Ujxu0z9qpbTAhZx5+JOjAuX\nR9zILOWUgZ6w7YUXhAgXfqYztYfCZyPiaDPOxgl1RWMPtAh9KvYZzriKuEZgTdk=\n=utzN\n-----END + PGP SIGNATURE-----\n","payload":"tree 67a425bb5cdf5dba974649a92b9bd1b332ccbada\nparent + 6895b6479dbe12b5cb3baa02416c6343ddb888b4\nauthor Jerrod + 1531180114 -0700\ncommitter GitHub 1531180114 -0700\n\nUpdate + README.rst"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c7f608036a3d2e89f8c59989ee213900c1ef39d1","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1/comments","author":null,"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars3.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"6895b6479dbe12b5cb3baa02416c6343ddb888b4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6895b6479dbe12b5cb3baa02416c6343ddb888b4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6895b6479dbe12b5cb3baa02416c6343ddb888b4"}]},{"sha":"b92edba44fdd29fcc506317cc3ddeae1a723dd08","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmI5MmVkYmE0NGZkZDI5ZmNjNTA2MzE3Y2MzZGRlYWUxYTcyM2RkMDg=","commit":{"author":{"name":"Jerrod","email":"jerrod@fundersclub.com","date":"2018-07-09T23:51:16Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2018-07-09T23:51:16Z"},"message":"Update + README.rst","tree":{"sha":"0cbac4b22e6b7a239338e6550a59553c9bc76eb0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/0cbac4b22e6b7a239338e6550a59553c9bc76eb0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJbQ/T0CRBK7hj4Ov3rIwAAdHIIAGISA3RET3zrQdUjtrsxVc8K\nGjR/6NYt0xJxRA+tJ5JuRGplJJuVECOADr52eXaRMw+3jvfsqZOt7oKAnU/Q490u\nwb8V8Y7vOo9doxqrJY6vQKCddjbiRZKD/clwAlBFFO0UJJtRWWANqeD0PHnDyzIG\nIasWMQyRb1RSMBAg7tIGsBwxzXKBaMr9Y6IVuh2HSLS/mOg124vy9hHKx5L60IyJ\nvOlcFiEQpWYtFDn9hc+BvEgdaIcKP6mkOo+AGz6uYJ8149ukTwpGZQr8NJgxl4Yx\nY+gBGy7CurVoFZ4N3JOY94H9RffoYKXJwJmZS01n0y9ar8CG2YjSniFY7x7hJNQ=\n=SnzP\n-----END + PGP SIGNATURE-----\n","payload":"tree 0cbac4b22e6b7a239338e6550a59553c9bc76eb0\nparent + c7f608036a3d2e89f8c59989ee213900c1ef39d1\nauthor Jerrod + 1531180276 -0700\ncommitter GitHub 1531180276 -0700\n\nUpdate + README.rst"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b92edba44fdd29fcc506317cc3ddeae1a723dd08","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b92edba44fdd29fcc506317cc3ddeae1a723dd08/comments","author":null,"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars3.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"c7f608036a3d2e89f8c59989ee213900c1ef39d1","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c7f608036a3d2e89f8c59989ee213900c1ef39d1","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c7f608036a3d2e89f8c59989ee213900c1ef39d1"}]}],"files":[{"sha":"405d834472636bc2c83dd8bd6818e4b2c2871e86","filename":"README.rst","status":"modified","additions":11,"deletions":4,"changes":15,"blob_url":"https://github.com/ThiagoCodecov/example-python/blob/b92edba44fdd29fcc506317cc3ddeae1a723dd08/README.rst","raw_url":"https://github.com/ThiagoCodecov/example-python/raw/b92edba44fdd29fcc506317cc3ddeae1a723dd08/README.rst","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.rst?ref=b92edba44fdd29fcc506317cc3ddeae1a723dd08","patch":"@@ + -9,7 +9,8 @@\n Overview\n --------\n \n-Main website: `Codecov `_.\n+\n+website: + `Codecov `_.\n \n .. code-block:: shell-session\n \n@@ + -46,12 +47,19 @@ Below are some examples on how to include coverage tracking + during your tests. C\n \n You may need to configure a ``.coveragerc`` file. + Learn more `here `_. + Start with this `generic .coveragerc `_ + for example.\n \n-We highly suggest adding `source` to your ``.coveragerc`` + which solves a number of issues collecting coverage.\n+We highly suggest adding + ``source`` to your ``.coveragerc``, which solves a number of issues collecting + coverage.\n \n .. code-block:: ini\n \n [run]\n source=your_package_name\n+ \n+If + there are multiple sources, you instead should add ``include`` to your ``.coveragerc``\n+\n+.. + code-block:: ini\n+\n+ [run]\n+ include=your_package_name/*\n \n unittests\n + ---------\n@@ -150,5 +158,4 @@ Links\n * Twitter: `@codecov `_.\n + * Email: `hello@codecov.io `_.\n \n-We are happy to help if + you have any questions. Please contact email our Support at [support@codecov.io](mailto:support@codecov.io)\n-\n+We + are happy to help if you have any questions. Please contact email our Support + at `support@codecov.io `_."}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:32 GMT + ETag: + - W/"54a8a69ae63ce6ba5740dd3da8960e622de6a5d574a17ae56128f32270e90ab5" + Last-Modified: + - Mon, 09 Jul 2018 23:51:16 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFF6:0B47:40BACE5:695F631:5F873630 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4933' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '67' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_compare_same_commit.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_compare_same_commit.yaml new file mode 100644 index 0000000000..732c8b0207 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_compare_same_commit.yaml @@ -0,0 +1,89 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/compare/6ae5f17...6ae5f17 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/6ae5f17...6ae5f17","html_url":"https://github.com/ThiagoCodecov/example-python/compare/6ae5f17...6ae5f17","permalink_url":"https://github.com/ThiagoCodecov/example-python/compare/ThiagoCodecov:6ae5f17...ThiagoCodecov:6ae5f17","diff_url":"https://github.com/ThiagoCodecov/example-python/compare/6ae5f17...6ae5f17.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/compare/6ae5f17...6ae5f17.patch","base_commit":{"sha":"6ae5f1795a441884ed2847bb31154814ac01ef38","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjZhZTVmMTc5NWE0NDE4ODRlZDI4NDdiYjMxMTU0ODE0YWMwMWVmMzg=","commit":{"author":{"name":"Thomas + Pedbereznak","email":"tom@tomped.com","date":"2018-04-26T08:35:58Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2018-04-26T08:35:58Z"},"message":"Update + README.rst","tree":{"sha":"b5592410a15d7a596a8eaea6399766fbbbe0366c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b5592410a15d7a596a8eaea6399766fbbbe0366c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6ae5f1795a441884ed2847bb31154814ac01ef38","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJa4Y9uCRBK7hj4Ov3rIwAAdHIIAAyJAC1mOPnKmkDSzraV47Wq\nXma/2QidKpXnRqKgY6XFBXAH0RIpHpZ3NwGR/L2GH1l7xLjXtOMTvXOCjFBZUwRE\nLlM9IdoUFyPU2E9P0z0vfGR/nk5QC8PY9lzDwe/N8ZhR0j4M2rTM2ue97om9nJ4e\nmD+HR2ZwjKA9Z9zFeALgBjokKs44F6oN6lLuPYn06oiCnYB3ytlWJy+vpmEGLhoM\nL+a/ct2e6O5MmlpbRlKVME4FL0O4wDBMrAaFeeZgQTCl2LKfdsfYScJnypkB7X06\n6cDtC/TJ436n4PCTBRHVMDNGxzmgMgMFYbCPkJ27BeWlTuKVDcJ2msOV7ZJKqqs=\n=oGiR\n-----END + PGP SIGNATURE-----\n","payload":"tree b5592410a15d7a596a8eaea6399766fbbbe0366c\nparent + 8631ea09b9b689de0a348d5abf70bdd7273d2ae3\nauthor Thomas Pedbereznak + 1524731758 +0200\ncommitter GitHub 1524731758 +0200\n\nUpdate + README.rst"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38/comments","author":{"login":"TomPed","id":11602092,"node_id":"MDQ6VXNlcjExNjAyMDky","avatar_url":"https://avatars1.githubusercontent.com/u/11602092?v=4","gravatar_id":"","url":"https://api.github.com/users/TomPed","html_url":"https://github.com/TomPed","followers_url":"https://api.github.com/users/TomPed/followers","following_url":"https://api.github.com/users/TomPed/following{/other_user}","gists_url":"https://api.github.com/users/TomPed/gists{/gist_id}","starred_url":"https://api.github.com/users/TomPed/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/TomPed/subscriptions","organizations_url":"https://api.github.com/users/TomPed/orgs","repos_url":"https://api.github.com/users/TomPed/repos","events_url":"https://api.github.com/users/TomPed/events{/privacy}","received_events_url":"https://api.github.com/users/TomPed/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars3.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"8631ea09b9b689de0a348d5abf70bdd7273d2ae3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3"}]},"merge_base_commit":{"sha":"6ae5f1795a441884ed2847bb31154814ac01ef38","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjZhZTVmMTc5NWE0NDE4ODRlZDI4NDdiYjMxMTU0ODE0YWMwMWVmMzg=","commit":{"author":{"name":"Thomas + Pedbereznak","email":"tom@tomped.com","date":"2018-04-26T08:35:58Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2018-04-26T08:35:58Z"},"message":"Update + README.rst","tree":{"sha":"b5592410a15d7a596a8eaea6399766fbbbe0366c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b5592410a15d7a596a8eaea6399766fbbbe0366c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6ae5f1795a441884ed2847bb31154814ac01ef38","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJa4Y9uCRBK7hj4Ov3rIwAAdHIIAAyJAC1mOPnKmkDSzraV47Wq\nXma/2QidKpXnRqKgY6XFBXAH0RIpHpZ3NwGR/L2GH1l7xLjXtOMTvXOCjFBZUwRE\nLlM9IdoUFyPU2E9P0z0vfGR/nk5QC8PY9lzDwe/N8ZhR0j4M2rTM2ue97om9nJ4e\nmD+HR2ZwjKA9Z9zFeALgBjokKs44F6oN6lLuPYn06oiCnYB3ytlWJy+vpmEGLhoM\nL+a/ct2e6O5MmlpbRlKVME4FL0O4wDBMrAaFeeZgQTCl2LKfdsfYScJnypkB7X06\n6cDtC/TJ436n4PCTBRHVMDNGxzmgMgMFYbCPkJ27BeWlTuKVDcJ2msOV7ZJKqqs=\n=oGiR\n-----END + PGP SIGNATURE-----\n","payload":"tree b5592410a15d7a596a8eaea6399766fbbbe0366c\nparent + 8631ea09b9b689de0a348d5abf70bdd7273d2ae3\nauthor Thomas Pedbereznak + 1524731758 +0200\ncommitter GitHub 1524731758 +0200\n\nUpdate + README.rst"}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6ae5f1795a441884ed2847bb31154814ac01ef38","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ae5f1795a441884ed2847bb31154814ac01ef38/comments","author":{"login":"TomPed","id":11602092,"node_id":"MDQ6VXNlcjExNjAyMDky","avatar_url":"https://avatars1.githubusercontent.com/u/11602092?v=4","gravatar_id":"","url":"https://api.github.com/users/TomPed","html_url":"https://github.com/TomPed","followers_url":"https://api.github.com/users/TomPed/followers","following_url":"https://api.github.com/users/TomPed/following{/other_user}","gists_url":"https://api.github.com/users/TomPed/gists{/gist_id}","starred_url":"https://api.github.com/users/TomPed/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/TomPed/subscriptions","organizations_url":"https://api.github.com/users/TomPed/orgs","repos_url":"https://api.github.com/users/TomPed/repos","events_url":"https://api.github.com/users/TomPed/events{/privacy}","received_events_url":"https://api.github.com/users/TomPed/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars3.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"8631ea09b9b689de0a348d5abf70bdd7273d2ae3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3"}]},"status":"identical","ahead_by":0,"behind_by":0,"total_commits":0,"commits":[],"files":[]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:33 GMT + ETag: + - W/"a26373917ffb81fbd7f2f84b161c5e9179f92c2e7d1d33ae430d176f825f6b69" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFF7:5098:2F3599E:52CA347:5F873631 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4932' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '68' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_distance_in_commits.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_distance_in_commits.yaml new file mode 100644 index 0000000000..e274217620 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_distance_in_commits.yaml @@ -0,0 +1,102 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecove2e/example-python/compare/main...0206296b1424912cc05069a9bf4025cbb95f5ecc + response: + content: '{"url":"https://api.github.com/repos/codecove2e/example-python/compare/main...0206296b1424912cc05069a9bf4025cbb95f5ecc","html_url":"https://github.com/codecove2e/example-python/compare/main...0206296b1424912cc05069a9bf4025cbb95f5ecc","permalink_url":"https://github.com/codecove2e/example-python/compare/codecove2e:93189ce...codecove2e:0206296","diff_url":"https://github.com/codecove2e/example-python/compare/main...0206296b1424912cc05069a9bf4025cbb95f5ecc.diff","patch_url":"https://github.com/codecove2e/example-python/compare/main...0206296b1424912cc05069a9bf4025cbb95f5ecc.patch","base_commit":{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","node_id":"MDY6Q29tbWl0NTE1Mjk1ODU4OjkzMTg5Y2U1MGYyMjQyOTZkNjQxMmUyODg0YjkzZGNjM2M3Yzg2NTQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"message":"Adding + some encoding","tree":{"sha":"966e4dc888b0828c5dea75f21e82eb2daa9a4485","url":"https://api.github.com/repos/codecove2e/example-python/git/trees/966e4dc888b0828c5dea75f21e82eb2daa9a4485"},"url":"https://api.github.com/repos/codecove2e/example-python/git/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","html_url":"https://github.com/codecove2e/example-python/commit/93189ce50f224296d6412e2884b93dcc3c7c8654","comments_url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","url":"https://api.github.com/repos/codecove2e/example-python/commits/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","html_url":"https://github.com/codecove2e/example-python/commit/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67"}]},"merge_base_commit":{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","node_id":"MDY6Q29tbWl0NTE1Mjk1ODU4OjkzMTg5Y2U1MGYyMjQyOTZkNjQxMmUyODg0YjkzZGNjM2M3Yzg2NTQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2020-12-04T04:21:18Z"},"message":"Adding + some encoding","tree":{"sha":"966e4dc888b0828c5dea75f21e82eb2daa9a4485","url":"https://api.github.com/repos/codecove2e/example-python/git/trees/966e4dc888b0828c5dea75f21e82eb2daa9a4485"},"url":"https://api.github.com/repos/codecove2e/example-python/git/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","html_url":"https://github.com/codecove2e/example-python/commit/93189ce50f224296d6412e2884b93dcc3c7c8654","comments_url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","url":"https://api.github.com/repos/codecove2e/example-python/commits/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67","html_url":"https://github.com/codecove2e/example-python/commit/6f87a6bba8b0151bb78064a0dba9fa4f13b76a67"}]},"status":"ahead","ahead_by":2,"behind_by":0,"total_commits":2,"commits":[{"sha":"8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","node_id":"C_kwDOHrbKctoAKDg1ODljMTljZTk1YTJiMTNjZjdiMzI3MmNiZjI3NWNhOTY1MWFlOWM","commit":{"author":{"name":"codecove2e","email":"93560619+codecove2e@users.noreply.github.com","date":"2022-08-16T19:38:32Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-08-16T19:38:32Z"},"message":"Update + __init__.py","tree":{"sha":"7c5d795e95bf1dcf12ebe45e3f2abf071e8bcaf3","url":"https://api.github.com/repos/codecove2e/example-python/git/trees/7c5d795e95bf1dcf12ebe45e3f2abf071e8bcaf3"},"url":"https://api.github.com/repos/codecove2e/example-python/git/commits/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJi+/I4CRBK7hj4Ov3rIwAApVwIACMRcC8711GZMH7nXKZH1tFK\ny7tOt6/WfCZsqEMtH5aQ5Px45MKT03wp+uPNBDnTJcdPLwZLfIdf7u9OpB9F4aVP\nsoLxW7IkUo9hT+E5i4YdldQOlmBF6pRwXA1AYYUAy1EgMchjf7kRkVCi3owJRXOV\nTP7cnM7ocP/RV46TzOgkhIh4wa0xbR8+LpFU7/+pRTbK+0u++UOB02Saq1RLvGyb\n8g8bHAtSNtTIRPfqI0EzE3piYjp6EFtoVmjDoTWn3z+fNw/6jB9SO9J9lyWFr62r\nbAoWgl8TuKuGogunswjsoWzs+13oFikh+4x2+V/ze0nk983D1ykXnMHQO2fIZV4=\n=i4b6\n-----END + PGP SIGNATURE-----\n","payload":"tree 7c5d795e95bf1dcf12ebe45e3f2abf071e8bcaf3\nparent + 93189ce50f224296d6412e2884b93dcc3c7c8654\nauthor codecove2e <93560619+codecove2e@users.noreply.github.com> + 1660678712 -0300\ncommitter GitHub 1660678712 -0300\n\nUpdate + __init__.py"}},"url":"https://api.github.com/repos/codecove2e/example-python/commits/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","html_url":"https://github.com/codecove2e/example-python/commit/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","comments_url":"https://api.github.com/repos/codecove2e/example-python/commits/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c/comments","author":{"login":"codecove2e","id":93560619,"node_id":"U_kgDOBZOfKw","avatar_url":"https://avatars.githubusercontent.com/u/93560619?v=4","gravatar_id":"","url":"https://api.github.com/users/codecove2e","html_url":"https://github.com/codecove2e","followers_url":"https://api.github.com/users/codecove2e/followers","following_url":"https://api.github.com/users/codecove2e/following{/other_user}","gists_url":"https://api.github.com/users/codecove2e/gists{/gist_id}","starred_url":"https://api.github.com/users/codecove2e/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecove2e/subscriptions","organizations_url":"https://api.github.com/users/codecove2e/orgs","repos_url":"https://api.github.com/users/codecove2e/repos","events_url":"https://api.github.com/users/codecove2e/events{/privacy}","received_events_url":"https://api.github.com/users/codecove2e/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"93189ce50f224296d6412e2884b93dcc3c7c8654","url":"https://api.github.com/repos/codecove2e/example-python/commits/93189ce50f224296d6412e2884b93dcc3c7c8654","html_url":"https://github.com/codecove2e/example-python/commit/93189ce50f224296d6412e2884b93dcc3c7c8654"}]},{"sha":"0206296b1424912cc05069a9bf4025cbb95f5ecc","node_id":"C_kwDOHrbKctoAKDAyMDYyOTZiMTQyNDkxMmNjMDUwNjlhOWJmNDAyNWNiYjk1ZjVlY2M","commit":{"author":{"name":"codecove2e","email":"93560619+codecove2e@users.noreply.github.com","date":"2022-08-16T19:40:46Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2022-08-16T19:40:46Z"},"message":"Update + __init__.py","tree":{"sha":"fd5012113a1251fbc771a2a51f23cd2bcf5494db","url":"https://api.github.com/repos/codecove2e/example-python/git/trees/fd5012113a1251fbc771a2a51f23cd2bcf5494db"},"url":"https://api.github.com/repos/codecove2e/example-python/git/commits/0206296b1424912cc05069a9bf4025cbb95f5ecc","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJi+/K+CRBK7hj4Ov3rIwAAju4IAHamBTf5EPjFJ53s5yGCkRRj\ni70PeAVHUyAbeMdfB9MNm3jwqbKqLuhhSvKxhyoX7+7qZUDJnnsDJIlF5iT2hCPT\nr3qQ3llv3q7hGhV04UZLTVcd5XAcOJgPySw6Cu8UznNQOZWdwaOW8NO2m8OEESwv\njw7Z0KNo50WeKf+PTeMxz066S9vuVHtXtsbxm+doe4U0cJvhPrXyXuqPRqylhxy8\ncLkHTQrWHvbfaRskfiwxmA4kQrYtPP74lruOj6q/kOSYVPbeZFJznJoQem5GQ0zO\noYWdmQfYK4NthDSxqgw7IEjGTRkgrNSj2I61HC67oEOR5sbgQ/BgFTja27++ne4=\n=K83G\n-----END + PGP SIGNATURE-----\n","payload":"tree fd5012113a1251fbc771a2a51f23cd2bcf5494db\nparent + 8589c19ce95a2b13cf7b3272cbf275ca9651ae9c\nauthor codecove2e <93560619+codecove2e@users.noreply.github.com> + 1660678846 -0300\ncommitter GitHub 1660678846 -0300\n\nUpdate + __init__.py"}},"url":"https://api.github.com/repos/codecove2e/example-python/commits/0206296b1424912cc05069a9bf4025cbb95f5ecc","html_url":"https://github.com/codecove2e/example-python/commit/0206296b1424912cc05069a9bf4025cbb95f5ecc","comments_url":"https://api.github.com/repos/codecove2e/example-python/commits/0206296b1424912cc05069a9bf4025cbb95f5ecc/comments","author":{"login":"codecove2e","id":93560619,"node_id":"U_kgDOBZOfKw","avatar_url":"https://avatars.githubusercontent.com/u/93560619?v=4","gravatar_id":"","url":"https://api.github.com/users/codecove2e","html_url":"https://github.com/codecove2e","followers_url":"https://api.github.com/users/codecove2e/followers","following_url":"https://api.github.com/users/codecove2e/following{/other_user}","gists_url":"https://api.github.com/users/codecove2e/gists{/gist_id}","starred_url":"https://api.github.com/users/codecove2e/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecove2e/subscriptions","organizations_url":"https://api.github.com/users/codecove2e/orgs","repos_url":"https://api.github.com/users/codecove2e/repos","events_url":"https://api.github.com/users/codecove2e/events{/privacy}","received_events_url":"https://api.github.com/users/codecove2e/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","url":"https://api.github.com/repos/codecove2e/example-python/commits/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c","html_url":"https://github.com/codecove2e/example-python/commit/8589c19ce95a2b13cf7b3272cbf275ca9651ae9c"}]}],"files":[{"sha":"898991ad883e00916ed4ced91b534734b211c7ba","filename":"awesome/__init__.py","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/codecove2e/example-python/blob/0206296b1424912cc05069a9bf4025cbb95f5ecc/awesome%2F__init__.py","raw_url":"https://github.com/codecove2e/example-python/raw/0206296b1424912cc05069a9bf4025cbb95f5ecc/awesome%2F__init__.py","contents_url":"https://api.github.com/repos/codecove2e/example-python/contents/awesome%2F__init__.py?ref=0206296b1424912cc05069a9bf4025cbb95f5ecc","patch":"@@ + -3,7 +3,7 @@ def smile():\n \n \n def frown():\n- return \":(\"\n+ return + \"==(\"\n \n \n def shieeee(g):"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 11 Apr 2023 09:28:34 GMT + ETag: + - W/"05d204d784dc1f63276b4876c0126c4f936cc6a18e52c182f1d6b389505e2e84" + Last-Modified: + - Tue, 16 Aug 2022 19:40:46 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D94A:1533:11E50145:12182034:64352842 + X-OAuth-Scopes: + - project, read:org, read:repo_hook, repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1681208914' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-05-11 09:27:53 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_gh_app_installation.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_gh_app_installation.yaml new file mode 100644 index 0000000000..0b47592b88 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_gh_app_installation.yaml @@ -0,0 +1,248 @@ +interactions: + - request: + body: "" + headers: + accept: + - application/vnd.github+json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + x-github-api-version: + - "2022-11-28" + method: GET + uri: https://api.github.com/app/installations/345678 + response: + content: '{"message":"Not Found","documentation_url":"https://docs.github.com/rest/apps/apps#get-an-installation-for-the-authenticated-app"}' + headers: + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 12 Feb 2024 22:15:31 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; format=json + X-GitHub-Request-Id: + - EA6A:2E1B:12FFB1:25FFF5:65CA9882 + X-XSS-Protection: + - "0" + x-github-api-version-selected: + - "2022-11-28" + http_version: HTTP/1.1 + status_code: 404 + - request: + body: "" + headers: + accept: + - application/vnd.github+json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + x-github-api-version: + - "2022-11-28" + method: GET + uri: https://api.github.com/app/installations/345678 + response: + content: '{"message":"Not Found","documentation_url":"https://docs.github.com/rest/apps/apps#get-an-installation-for-the-authenticated-app"}' + headers: + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 12 Feb 2024 22:31:13 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; format=json + X-GitHub-Request-Id: + - EDD4:73F2:6391BF:C82AD0:65CA9C31 + X-XSS-Protection: + - "0" + x-github-api-version-selected: + - "2022-11-28" + http_version: HTTP/1.1 + status_code: 404 + - request: + body: "" + headers: + accept: + - application/vnd.github+json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + x-github-api-version: + - "2022-11-28" + method: GET + uri: https://api.github.com/app/installations/12345678 + response: + content: '{"id":12345678,"account":{"login":"fake-test-user","id":72693746,"node_id":"AMSDFU7234Msdf7N#","avatar_url":"https://avatars.githubusercontent.com/u/72693746?v=4","gravatar_id":"","url":"https://api.github.com/users/fake-test-user","html_url":"https://github.com/fake-test-user","followers_url":"https://api.github.com/users/fake-test-user/followers","following_url":"https://api.github.com/users/fake-test-user/following{/other_user}","gists_url":"https://api.github.com/users/fake-test-user/gists{/gist_id}","starred_url":"https://api.github.com/users/fake-test-user/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fake-test-user/subscriptions","organizations_url":"https://api.github.com/users/fake-test-user/orgs","repos_url":"https://api.github.com/users/fake-test-user/repos","events_url":"https://api.github.com/users/fake-test-user/events{/privacy}","received_events_url":"https://api.github.com/users/fake-test-user/received_events","type":"User","site_admin":false},"repository_selection":"all","access_tokens_url":"https://api.github.com/app/installations/12345678/access_tokens","repositories_url":"https://api.github.com/installation/repositories","html_url":"https://github.com/settings/installations/12345678","app_id":345678,"app_slug":"local-github-app-adrian","target_id":72693746,"target_type":"User","permissions":{"checks":"write","contents":"write","metadata":"read","statuses":"write","pull_requests":"write","administration":"read"},"events":[],"created_at":"2024-02-11T20:39:08.000Z","updated_at":"2024-02-11T20:39:09.000Z","single_file_name":null,"has_multiple_single_files":false,"single_file_paths":[],"suspended_by":null,"suspended_at":null}' + headers: + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - public, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 12 Feb 2024 22:33:03 GMT + ETag: + - W/"42ebd0b4f200aff55e8eec91270cbac17b66016d0a9cef1b3778f18354849016" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; format=json + X-GitHub-Request-Id: + - EE45:8088:685AF7:D17290:65CA9C9F + X-XSS-Protection: + - "0" + x-github-api-version-selected: + - "2022-11-28" + http_version: HTTP/1.1 + status_code: 200 + - request: + body: "" + headers: + accept: + - application/vnd.github+json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + x-github-api-version: + - "2022-11-28" + method: GET + uri: https://api.github.com/app/installations/12345678 + response: + content: '{"id":12345678,"account":{"login":"fake-test-user","id":72693746,"node_id":"AMSDFU7234Msdf7N#","avatar_url":"https://avatars.githubusercontent.com/u/72693746?v=4","gravatar_id":"","url":"https://api.github.com/users/fake-test-user","html_url":"https://github.com/fake-test-user","followers_url":"https://api.github.com/users/fake-test-user/followers","following_url":"https://api.github.com/users/fake-test-user/following{/other_user}","gists_url":"https://api.github.com/users/fake-test-user/gists{/gist_id}","starred_url":"https://api.github.com/users/fake-test-user/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fake-test-user/subscriptions","organizations_url":"https://api.github.com/users/fake-test-user/orgs","repos_url":"https://api.github.com/users/fake-test-user/repos","events_url":"https://api.github.com/users/fake-test-user/events{/privacy}","received_events_url":"https://api.github.com/users/fake-test-user/received_events","type":"User","site_admin":false},"repository_selection":"all","access_tokens_url":"https://api.github.com/app/installations/12345678/access_tokens","repositories_url":"https://api.github.com/installation/repositories","html_url":"https://github.com/settings/installations/12345678","app_id":345678,"app_slug":"local-github-app-adrian","target_id":72693746,"target_type":"User","permissions":{"checks":"write","contents":"write","metadata":"read","statuses":"write","pull_requests":"write","administration":"read"},"events":[],"created_at":"2024-02-11T20:39:08.000Z","updated_at":"2024-02-11T20:39:09.000Z","single_file_name":null,"has_multiple_single_files":false,"single_file_paths":[],"suspended_by":null,"suspended_at":null}' + headers: + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - public, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 12 Feb 2024 22:37:31 GMT + ETag: + - W/"42ebd0b4f200aff55e8eec91270cbac17b66016d0a9cef1b3778f18354849016" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; format=json + X-GitHub-Request-Id: + - EF47:66DF:6C10B5:D9D86F:65CA9DAA + X-XSS-Protection: + - "0" + x-github-api-version-selected: + - "2022-11-28" + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_gh_app_installation_non_existent.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_gh_app_installation_non_existent.yaml new file mode 100644 index 0000000000..f45daa9df9 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_gh_app_installation_non_existent.yaml @@ -0,0 +1,61 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/vnd.github+json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + x-github-api-version: + - '2022-11-28' + method: GET + uri: https://api.github.com/app/installations/12345678 + response: + content: '{"message":"Not Found","documentation_url":"https://docs.github.com/rest/apps/apps#get-an-installation-for-the-authenticated-app"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 12 Feb 2024 22:55:03 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; format=json + X-GitHub-Request-Id: + - F43D:0B10:25980C:4B7E14:65CAA1C7 + X-XSS-Protection: + - '0' + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_github_check_runs.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_github_check_runs.yaml new file mode 100644 index 0000000000..8dfa1a0ea6 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_github_check_runs.yaml @@ -0,0 +1,126 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/vnd.github.antiope-preview+json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/75f355d8d14ba3d7761c728b4d2607cde0eef065/check-runs?check_name=Test+check + response: + content: "{\"total_count\":1,\"check_runs\":[{\"id\":1256232357,\"node_id\":\"\ + MDg6Q2hlY2tSdW4xMjU2MjMyMzU3\",\"head_sha\":\"75f355d8d14ba3d7761c728b4d2607cde0eef065\"\ + ,\"external_id\":\"\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/check-runs/1256232357\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/runs/1256232357\"\ + ,\"details_url\":\"https://app.codecov.io/gh/codecov/example-python/compare/1?src=pr\",\"status\":\"completed\",\"conclusion\"\ + :\"success\",\"started_at\":\"2020-10-14T23:00:59Z\",\"completed_at\":\"2020-10-14T23:01:14Z\"\ + ,\"output\":{\"title\":null,\"summary\":null,\"text\":null,\"annotations_count\"\ + :0,\"annotations_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/check-runs/1256232357/annotations\"\ + },\"name\":\"Test check\",\"check_suite\":{\"id\":1341719124},\"app\":{\"id\"\ + :254,\"slug\":\"codecov\",\"node_id\":\"MDM6QXBwMjU0\",\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"name\":\"Codecov\",\"description\"\ + :\"Codecov provides highly integrated tools to group, merge, archive and compare\ + \ coverage reports. Whether your team is comparing changes in a pull request\ + \ or reviewing a single commit, Codecov will improve the code review workflow\ + \ and quality.\\r\\n\\r\\n## Code coverage done right.\xAE\\r\\n\\r\\n1. Upload\ + \ coverage reports from your CI builds.\\r\\n2. Codecov merges all builds and\ + \ languages into one beautiful coherent report.\\r\\n3. Get commit statuses,\ + \ pull request comments and coverage overlay via our browser extension.\\r\\\ + n\\r\\nWhen Codecov merges your uploads it keeps track of the CI provider (inc.\ + \ build details) and user specified context, e.g. `#unittest` ~ `#smoketest`\ + \ or `#oldcode` ~ `#newcode`. You can track the `#unittest` coverage independently\ + \ of other groups. [Learn more here](\\r\\nhttp://docs.codecov.io/docs/flags)\\\ + r\\n\\r\\nThrough **Codecov's Browser Extension** reports overlay directly in\ + \ GitHub UI to assist in code review. [Watch here](https://docs.codecov.io/docs/browser-extension)\\\ + r\\n\\r\\n*Highly detailed* **pull request comments** and *customizable* **commit\ + \ statuses** will improve your team's workflow and code coverage incrementally.\\\ + r\\n\\r\\n**File backed configuration** all through the `codecov.yml`. \\r\\\ + n\\r\\n## FAQ\\r\\n- Do you **merge multiple uploads** to the same commit? **Yes**\\\ + r\\n- Do you **support multiple languages** in the same project? **Yes**\\r\\\ + n- Can you **group coverage reports** by project and/or test type? **Yes**\\\ + r\\n- How does **pricing** work? Only paid users can view reports and post statuses/comments.\ + \ \",\"external_url\":\"https://codecov.io\",\"html_url\":\"https://github.com/apps/codecov\"\ + ,\"created_at\":\"2016-09-25T14:18:27Z\",\"updated_at\":\"2020-08-27T18:10:18Z\"\ + ,\"permissions\":{\"administration\":\"read\",\"checks\":\"write\",\"contents\"\ + :\"read\",\"issues\":\"read\",\"members\":\"read\",\"metadata\":\"read\",\"\ + pull_requests\":\"write\",\"statuses\":\"write\"},\"events\":[\"check_run\"\ + ,\"check_suite\",\"create\",\"delete\",\"fork\",\"membership\",\"public\",\"\ + pull_request\",\"push\",\"release\",\"repository\",\"status\",\"team_add\"]},\"\ + pull_requests\":[{\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18\"\ + ,\"id\":383348775,\"number\":18,\"head\":{\"ref\":\"thiago/base-no-base\",\"\ + sha\":\"75f355d8d14ba3d7761c728b4d2607cde0eef065\",\"repo\":{\"id\":156617777,\"\ + url\":\"https://api.github.com/repos/ThiagoCodecov/example-python\",\"name\"\ + :\"example-python\"}},\"base\":{\"ref\":\"main\",\"sha\":\"f0895290dc26668faeeb20ee5ccd4cc995925775\"\ + ,\"repo\":{\"id\":156617777,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python\"\ + ,\"name\":\"example-python\"}}}]}]}" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 23:06:16 GMT + ETag: + - W/"4b47d3bb727ee10a7766d413fdd981ad1041775d16ef38391dc72ff3284425fd" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=antiope-preview; format=json + X-GitHub-Request-Id: + - DA82:422B:5371D2:88CB86:5F878467 + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4991' + X-RateLimit-Reset: + - '1602719974' + X-RateLimit-Used: + - '9' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_github_check_suite.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_github_check_suite.yaml new file mode 100644 index 0000000000..048c9aa210 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_github_check_suite.yaml @@ -0,0 +1,181 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/vnd.github.antiope-preview+json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/75f355d8d14ba3d7761c728b4d2607cde0eef065/check-suites + response: + content: "{\"total_count\":1,\"check_suites\":[{\"id\":1341719124,\"node_id\"\ + :\"MDEwOkNoZWNrU3VpdGUxMzQxNzE5MTI0\",\"head_branch\":\"thiago/base-no-base\"\ + ,\"head_sha\":\"75f355d8d14ba3d7761c728b4d2607cde0eef065\",\"status\":\"completed\"\ + ,\"conclusion\":\"success\",\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/check-suites/1341719124\"\ + ,\"before\":\"f0fe310b54d2b944a1d16b79958d9d3add7c902c\",\"after\":\"75f355d8d14ba3d7761c728b4d2607cde0eef065\"\ + ,\"pull_requests\":[{\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18\"\ + ,\"id\":383348775,\"number\":18,\"head\":{\"ref\":\"thiago/base-no-base\",\"\ + sha\":\"75f355d8d14ba3d7761c728b4d2607cde0eef065\",\"repo\":{\"id\":156617777,\"\ + url\":\"https://api.github.com/repos/ThiagoCodecov/example-python\",\"name\"\ + :\"example-python\"}},\"base\":{\"ref\":\"main\",\"sha\":\"f0895290dc26668faeeb20ee5ccd4cc995925775\"\ + ,\"repo\":{\"id\":156617777,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python\"\ + ,\"name\":\"example-python\"}}}],\"app\":{\"id\":254,\"slug\":\"codecov\",\"\ + node_id\":\"MDM6QXBwMjU0\",\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"\ + node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"name\":\"Codecov\",\"description\"\ + :\"Codecov provides highly integrated tools to group, merge, archive and compare\ + \ coverage reports. Whether your team is comparing changes in a pull request\ + \ or reviewing a single commit, Codecov will improve the code review workflow\ + \ and quality.\\r\\n\\r\\n## Code coverage done right.\xAE\\r\\n\\r\\n1. Upload\ + \ coverage reports from your CI builds.\\r\\n2. Codecov merges all builds and\ + \ languages into one beautiful coherent report.\\r\\n3. Get commit statuses,\ + \ pull request comments and coverage overlay via our browser extension.\\r\\\ + n\\r\\nWhen Codecov merges your uploads it keeps track of the CI provider (inc.\ + \ build details) and user specified context, e.g. `#unittest` ~ `#smoketest`\ + \ or `#oldcode` ~ `#newcode`. You can track the `#unittest` coverage independently\ + \ of other groups. [Learn more here](\\r\\nhttp://docs.codecov.io/docs/flags)\\\ + r\\n\\r\\nThrough **Codecov's Browser Extension** reports overlay directly in\ + \ GitHub UI to assist in code review. [Watch here](https://docs.codecov.io/docs/browser-extension)\\\ + r\\n\\r\\n*Highly detailed* **pull request comments** and *customizable* **commit\ + \ statuses** will improve your team's workflow and code coverage incrementally.\\\ + r\\n\\r\\n**File backed configuration** all through the `codecov.yml`. \\r\\\ + n\\r\\n## FAQ\\r\\n- Do you **merge multiple uploads** to the same commit? **Yes**\\\ + r\\n- Do you **support multiple languages** in the same project? **Yes**\\r\\\ + n- Can you **group coverage reports** by project and/or test type? **Yes**\\\ + r\\n- How does **pricing** work? Only paid users can view reports and post statuses/comments.\ + \ \",\"external_url\":\"https://codecov.io\",\"html_url\":\"https://github.com/apps/codecov\"\ + ,\"created_at\":\"2016-09-25T14:18:27Z\",\"updated_at\":\"2020-08-27T18:10:18Z\"\ + ,\"permissions\":{\"administration\":\"read\",\"checks\":\"write\",\"contents\"\ + :\"read\",\"issues\":\"read\",\"members\":\"read\",\"metadata\":\"read\",\"\ + pull_requests\":\"write\",\"statuses\":\"write\"},\"events\":[\"check_run\"\ + ,\"check_suite\",\"create\",\"delete\",\"fork\",\"membership\",\"public\",\"\ + pull_request\",\"push\",\"release\",\"repository\",\"status\",\"team_add\"]},\"\ + created_at\":\"2020-10-14T23:00:59Z\",\"updated_at\":\"2020-10-14T23:01:14Z\"\ + ,\"latest_check_runs_count\":1,\"check_runs_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/check-suites/1341719124/check-runs\"\ + ,\"head_commit\":{\"id\":\"75f355d8d14ba3d7761c728b4d2607cde0eef065\",\"tree_id\"\ + :\"b737740a931a34f5be73f553ea87a1161c917be0\",\"message\":\"Adding README\\\ + n\\nsurpriseaAKDS\\n\\nddkokgfnskfds\\n\\nBanana\\n\\nYallow\\n\\nABG\",\"timestamp\"\ + :\"2020-10-13T15:15:31Z\",\"author\":{\"name\":\"Thiago Ramos\",\"email\":\"\ + thiago@codecov.io\"},\"committer\":{\"name\":\"Thiago Ramos\",\"email\":\"thiago@codecov.io\"\ + }},\"repository\":{\"id\":156617777,\"node_id\":\"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=\"\ + ,\"name\":\"example-python\",\"full_name\":\"ThiagoCodecov/example-python\"\ + ,\"private\":false,\"owner\":{\"login\":\"ThiagoCodecov\",\"id\":44376991,\"\ + node_id\":\"MDQ6VXNlcjQ0Mzc2OTkx\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/44376991?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/ThiagoCodecov\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov\",\"followers_url\":\"https://api.github.com/users/ThiagoCodecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/ThiagoCodecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}\"\ + ,\"starred_url\":\"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}\"\ + ,\"subscriptions_url\":\"https://api.github.com/users/ThiagoCodecov/subscriptions\"\ + ,\"organizations_url\":\"https://api.github.com/users/ThiagoCodecov/orgs\",\"\ + repos_url\":\"https://api.github.com/users/ThiagoCodecov/repos\",\"events_url\"\ + :\"https://api.github.com/users/ThiagoCodecov/events{/privacy}\",\"received_events_url\"\ + :\"https://api.github.com/users/ThiagoCodecov/received_events\",\"type\":\"\ + User\",\"site_admin\":false},\"html_url\":\"https://github.com/ThiagoCodecov/example-python\"\ + ,\"description\":\"Python coverage example\",\"fork\":true,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python\"\ + ,\"forks_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/deployments\"\ + }}]}" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 23:02:14 GMT + ETag: + - W/"1a3627e27a71ac6abb5040a8c8efc6d3438da4f2848cd970e55ae2afe351c1c9" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=antiope-preview; format=json + X-GitHub-Request-Id: + - DA61:2DBD:5527AE:8C5F16:5F878375 + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4993' + X-RateLimit-Reset: + - '1602719974' + X-RateLimit-Used: + - '7' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request[1-b0].yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request[1-b0].yaml new file mode 100644 index 0000000000..5d19824ede --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request[1-b0].yaml @@ -0,0 +1,351 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1","id":229193531,"node_id":"MDExOlB1bGxSZXF1ZXN0MjI5MTkzNTMx","html_url":"https://github.com/ThiagoCodecov/example-python/pull/1","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/1.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/1.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1","number":1,"state":"closed","locked":true,"title":"Creating + new code for reasons no one knows","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"Why + you ask?\r\n\r\nI dont know","created_at":"2018-11-07T22:44:49Z","updated_at":"2020-10-14T21:28:41Z","closed_at":"2019-09-09T22:23:11Z","merged_at":"2019-09-09T22:23:11Z","merge_commit_sha":"038ac8ac2127baa19a927c67f0d5168d9928abf3","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/119c1907fb266f374b8440bbd70dccbea54daf8f","head":{"label":"ThiagoCodecov:reason/some-testing","ref":"reason/some-testing","sha":"119c1907fb266f374b8440bbd70dccbea54daf8f","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2023-07-04T20:51:23Z","pushed_at":"2022-08-17T20:09:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":3,"open_issues":1,"watchers":0,"default_branch":"main"}},"base":{"label":"ThiagoCodecov:main","ref":"main","sha":"68946ef98daec68c7798459150982fc799c87d85","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2023-07-04T20:51:23Z","pushed_at":"2022-08-17T20:09:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":3,"open_issues":1,"watchers":0,"default_branch":"main"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/1"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/119c1907fb266f374b8440bbd70dccbea54daf8f"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null,"merged":true,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"comments":4,"review_comments":0,"maintainer_can_modify":false,"commits":10,"additions":48,"deletions":6,"changed_files":5}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:16:18 GMT + ETag: + - W/"98ebeab4535d824b2ba44dda80ed5ac0308432b48bcb2c4071c7ac28551ade7b" + Last-Modified: + - Fri, 01 Sep 2023 11:48:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF7E:1A94:121B59:12ED72:64F610E1 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4995' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '5' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits?page=2&per_page=100 + response: + content: '[]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '2' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:16:18 GMT + ETag: + - '"a20ff71ece9649384768ca03ee284519e9552357700b0ef7c43cdee37866531e"' + Last-Modified: + - Fri, 01 Sep 2023 11:48:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF80:6DFA:122706:12F917:64F610E2 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4994' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '6' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits?page=3&per_page=100 + response: + content: '[]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '2' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:16:18 GMT + ETag: + - '"a20ff71ece9649384768ca03ee284519e9552357700b0ef7c43cdee37866531e"' + Last-Modified: + - Fri, 01 Sep 2023 11:48:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF81:42F4:11FA5C:12CC75:64F610E2 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4993' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '7' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits?page=1&per_page=100 + response: + content: '[{"sha":"587662b6e5403ae0d126e0c7839a8d98382c4760","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjU4NzY2MmI2ZTU0MDNhZTBkMTI2ZTBjNzgzOWE4ZDk4MzgyYzQ3NjA=","commit":{"author":{"name":"Thiago + Ribeiro Ramos","email":"thiago@ribeiroramos.com","date":"2018-11-07T22:43:54Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:34:45Z"},"message":"Creating + new code for reasons no one knows","tree":{"sha":"ec56802a37b981f13bdc3c9a56ae68ef82ab424a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ec56802a37b981f13bdc3c9a56ae68ef82ab424a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","html_url":"https://github.com/ThiagoCodecov/example-python/commit/587662b6e5403ae0d126e0c7839a8d98382c4760","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760/comments","author":null,"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"68946ef98daec68c7798459150982fc799c87d85","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/68946ef98daec68c7798459150982fc799c87d85","html_url":"https://github.com/ThiagoCodecov/example-python/commit/68946ef98daec68c7798459150982fc799c87d85"}]},{"sha":"03a8b737cb9d8585076ebdbac7b7235c8da0620d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjAzYThiNzM3Y2I5ZDg1ODUwNzZlYmRiYWM3YjcyMzVjOGRhMDYyMGQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-03-12T02:37:19Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:36:02Z"},"message":"Now + what","tree":{"sha":"51a385e1f575447b0b70fd597596c32c4f5bd172","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/51a385e1f575447b0b70fd597596c32c4f5bd172"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/03a8b737cb9d8585076ebdbac7b7235c8da0620d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"587662b6e5403ae0d126e0c7839a8d98382c4760","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","html_url":"https://github.com/ThiagoCodecov/example-python/commit/587662b6e5403ae0d126e0c7839a8d98382c4760"}]},{"sha":"bf9b57cf7b169806ae2d18d7671aba3825b99203","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmJmOWI1N2NmN2IxNjk4MDZhZTJkMThkNzY3MWFiYTM4MjViOTkyMDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-03-12T02:42:33Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:36:02Z"},"message":"Adding + untested code","tree":{"sha":"ce5383a6feb3e0bf20a4df46ae6c67ec3955723e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ce5383a6feb3e0bf20a4df46ae6c67ec3955723e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bf9b57cf7b169806ae2d18d7671aba3825b99203","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"03a8b737cb9d8585076ebdbac7b7235c8da0620d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/03a8b737cb9d8585076ebdbac7b7235c8da0620d"}]},{"sha":"cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmNlZGUxOWNiMzEwY2Q0Y2RkZmI1ZDg5MjFjYjhkMGNjN2M3YzE1MDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-16T22:02:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-16T22:11:24Z"},"message":"asdadafdsfdsfds","tree":{"sha":"e614247adf8a0705575e9c2170fad7c2848870a0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e614247adf8a0705575e9c2170fad7c2848870a0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"bf9b57cf7b169806ae2d18d7671aba3825b99203","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bf9b57cf7b169806ae2d18d7671aba3825b99203"}]},{"sha":"ea3ada938db123368d62b0133e7c5bb54b5292b9","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmVhM2FkYTkzOGRiMTIzMzY4ZDYyYjAxMzNlN2M1YmI1NGI1MjkyYjk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-19T18:48:19Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-19T18:48:19Z"},"message":"Adding + file t2 haha","tree":{"sha":"9ac6564d515ed2630026080e7cbdad4edfa9eca6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9ac6564d515ed2630026080e7cbdad4edfa9eca6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ea3ada938db123368d62b0133e7c5bb54b5292b9","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503"}]},{"sha":"2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjIwNDhiMjc3ZGQ2NTQyZjE4NGM2YTMwYzNlMmIwZjNlZTVlZWFmNGI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:43:42Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:43:42Z"},"message":"Adding + file t2 haha oooggg","tree":{"sha":"8b8d478591c3125af92ac395e87ddfb37fec5086","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/8b8d478591c3125af92ac395e87ddfb37fec5086"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ea3ada938db123368d62b0133e7c5bb54b5292b9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ea3ada938db123368d62b0133e7c5bb54b5292b9"}]},{"sha":"119de54e3cfdf8227a8556b9f5730c328a1390cd","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjExOWRlNTRlM2NmZGY4MjI3YTg1NTZiOWY1NzMwYzMyOGExMzkwY2Q=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:46:16Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:46:16Z"},"message":"Adding + file t2 haha oooggdsadsdsag","tree":{"sha":"d3868402c41afd8dcafb50e5bfa0e023f35c307e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/d3868402c41afd8dcafb50e5bfa0e023f35c307e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119de54e3cfdf8227a8556b9f5730c328a1390cd","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b"}]},{"sha":"2d55e8501b058b6f25382c4e287f022e8938461f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJkNTVlODUwMWIwNThiNmYyNTM4MmM0ZTI4N2YwMjJlODkzODQ2MWY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-24T21:32:08Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-24T21:32:08Z"},"message":"Adding + file t4 unpredictable","tree":{"sha":"a87f6d6ddd74d6df712bad79cc65d040c408efe8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/a87f6d6ddd74d6df712bad79cc65d040c408efe8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2d55e8501b058b6f25382c4e287f022e8938461f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2d55e8501b058b6f25382c4e287f022e8938461f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"119de54e3cfdf8227a8556b9f5730c328a1390cd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119de54e3cfdf8227a8556b9f5730c328a1390cd"}]},{"sha":"364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjM2NGJkZmJjNzJkNWUwNWI1MjBmMDMyMGIwZDhiMzlmZDllYTY5MmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-28T22:50:25Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-28T22:50:25Z"},"message":"Adding + Makefile","tree":{"sha":"452c48e858913bacb4be63a8e2351c98719406dd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/452c48e858913bacb4be63a8e2351c98719406dd"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2d55e8501b058b6f25382c4e287f022e8938461f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2d55e8501b058b6f25382c4e287f022e8938461f"}]},{"sha":"119c1907fb266f374b8440bbd70dccbea54daf8f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjExOWMxOTA3ZmIyNjZmMzc0Yjg0NDBiYmQ3MGRjY2JlYTU0ZGFmOGY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-09-02T23:07:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-09-02T23:07:56Z"},"message":"Cleaning + some stuff","tree":{"sha":"4995d75a388061164491217b50ee296137150f89","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4995d75a388061164491217b50ee296137150f89"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/119c1907fb266f374b8440bbd70dccbea54daf8f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119c1907fb266f374b8440bbd70dccbea54daf8f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119c1907fb266f374b8440bbd70dccbea54daf8f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119c1907fb266f374b8440bbd70dccbea54daf8f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:16:18 GMT + ETag: + - W/"b91cf76b3ef50bf6937de9f89888efb09fe44940be57d540bab83c68a1ee68a0" + Last-Modified: + - Fri, 01 Sep 2023 11:48:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF7F:04E5:1207E3:12D9F4:64F610E2 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4992' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '8' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_base_doesnt_match.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_base_doesnt_match.yaml new file mode 100644 index 0000000000..ddb3f2db11 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_base_doesnt_match.yaml @@ -0,0 +1,327 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15","id":350683791,"node_id":"MDExOlB1bGxSZXF1ZXN0MzUwNjgzNzkx","html_url":"https://github.com/ThiagoCodecov/example-python/pull/15","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/15.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/15.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/15","number":15,"state":"closed","locked":true,"title":"Thiago/test + 1","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"","created_at":"2019-12-09T12:19:01Z","updated_at":"2023-07-04T15:10:22Z","closed_at":"2020-03-24T22:04:05Z","merged_at":null,"merge_commit_sha":null,"assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/15/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/2e2600aa09525e2e1e1d98b09de61454d29c94bb","head":{"label":"ThiagoCodecov:thiago/test-1","ref":"thiago/test-1","sha":"2e2600aa09525e2e1e1d98b09de61454d29c94bb","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2023-07-04T20:51:23Z","pushed_at":"2022-08-17T20:09:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":3,"open_issues":1,"watchers":0,"default_branch":"main"}},"base":{"label":"ThiagoCodecov:main","ref":"main","sha":"d723f5cb5c9c9f48c47f2df97c47de20457d3fdc","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2023-07-04T20:51:23Z","pushed_at":"2022-08-17T20:09:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":3,"open_issues":1,"watchers":0,"default_branch":"main"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/15"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/15"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/15/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/2e2600aa09525e2e1e1d98b09de61454d29c94bb"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null,"merged":false,"mergeable":false,"rebaseable":false,"mergeable_state":"dirty","merged_by":null,"comments":7,"review_comments":0,"maintainer_can_modify":false,"commits":3,"additions":8,"deletions":0,"changed_files":1}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:17:58 GMT + ETag: + - W/"e1f031a4d28a7df8c827e8ee40291ad4a1dda93f6d05741a961444d761bb079a" + Last-Modified: + - Fri, 01 Sep 2023 11:48:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF97:20F2:1227BB:12FB65:64F61146 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4988' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '12' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/commits?page=3&per_page=100 + response: + content: '[]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '2' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:17:59 GMT + ETag: + - '"a20ff71ece9649384768ca03ee284519e9552357700b0ef7c43cdee37866531e"' + Last-Modified: + - Fri, 01 Sep 2023 11:48:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF9A:1972:129062:13640A:64F61146 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4987' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '13' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/commits?page=1&per_page=100 + response: + content: '[{"sha":"e022f588e40269f8ab1c568080dc4a7662ff2335","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmUwMjJmNTg4ZTQwMjY5ZjhhYjFjNTY4MDgwZGM0YTc2NjJmZjIzMzU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:44Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:44Z"},"message":"69E76A1D-FD86-4A90-A6CE-6A66BEEE75CA","tree":{"sha":"6188b6c086ebf6c63626abca649d2596b145c84c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6188b6c086ebf6c63626abca649d2596b145c84c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e022f588e40269f8ab1c568080dc4a7662ff2335","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd"}]},{"sha":"c2a05aa15ecad5bec37e29b9fe51ef30120f8642","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmMyYTA1YWExNWVjYWQ1YmVjMzdlMjliOWZlNTFlZjMwMTIwZjg2NDI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:47Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:47Z"},"message":"81905186-E567-4E4E-A5CB-3E5BFB820E57","tree":{"sha":"dd944d7739d5d429f264709345fe481c1e602be8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/dd944d7739d5d429f264709345fe481c1e602be8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e022f588e40269f8ab1c568080dc4a7662ff2335","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e022f588e40269f8ab1c568080dc4a7662ff2335","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e022f588e40269f8ab1c568080dc4a7662ff2335"}]},{"sha":"2e2600aa09525e2e1e1d98b09de61454d29c94bb","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJlMjYwMGFhMDk1MjVlMmUxZTFkOThiMDlkZTYxNDU0ZDI5Yzk0YmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:49Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-09T12:17:49Z"},"message":"AE0A9904-3B05-405A-B8F7-A5DD3B38C92D","tree":{"sha":"f64d89541a0899881da6764081991e70ed43bfea","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f64d89541a0899881da6764081991e70ed43bfea"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2e2600aa09525e2e1e1d98b09de61454d29c94bb","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e2600aa09525e2e1e1d98b09de61454d29c94bb/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"c2a05aa15ecad5bec37e29b9fe51ef30120f8642","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c2a05aa15ecad5bec37e29b9fe51ef30120f8642","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c2a05aa15ecad5bec37e29b9fe51ef30120f8642"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:17:59 GMT + ETag: + - W/"aa87682167f47df445596f22862d2d8c81b1287fecb7daaafe5b8d20ecb961a6" + Last-Modified: + - Fri, 01 Sep 2023 11:48:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF98:709A:125015:1323D4:64F61146 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4986' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '14' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/15/commits?page=2&per_page=100 + response: + content: '[]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '2' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:17:59 GMT + ETag: + - '"a20ff71ece9649384768ca03ee284519e9552357700b0ef7c43cdee37866531e"' + Last-Modified: + - Fri, 01 Sep 2023 11:48:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF99:7B0C:126FF9:1343A5:64F61146 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4985' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '15' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_base_partially_differs.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_base_partially_differs.yaml new file mode 100644 index 0000000000..ae4d0c7115 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_base_partially_differs.yaml @@ -0,0 +1,362 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/codecov-api-archive/pulls/110 + response: + content: '{"url":"https://api.github.com/repos/codecov/codecov-api-archive/pulls/110","id":376868677,"node_id":"MDExOlB1bGxSZXF1ZXN0Mzc2ODY4Njc3","html_url":"https://github.com/codecov/codecov-api-archive/pull/110","diff_url":"https://github.com/codecov/codecov-api-archive/pull/110.diff","patch_url":"https://github.com/codecov/codecov-api-archive/pull/110.patch","issue_url":"https://api.github.com/repos/codecov/codecov-api-archive/issues/110","number":110,"state":"closed","locked":false,"title":"CE-1314 + GitHub Status Event Handler","user":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"body":"### + Purpose/Motivation\r\nHandle `status` events from github.\r\n\r\n### Links to + relevant tickets\r\nhttps://codecovio.atlassian.net/browse/CE-1314\r\n\r\n### + What does this PR do?\r\nImplements the above ticket, also moves all the service + classes into the `services` folder we created for `task.py`.","created_at":"2020-02-18T22:29:45Z","updated_at":"2021-11-03T18:40:11Z","closed_at":"2020-02-19T18:23:56Z","merged_at":"2020-02-19T18:23:55Z","merge_commit_sha":"e1d42c058e7169cc430f387591c1fc7cac35d2ae","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/codecov/codecov-api-archive/pulls/110/commits","review_comments_url":"https://api.github.com/repos/codecov/codecov-api-archive/pulls/110/comments","review_comment_url":"https://api.github.com/repos/codecov/codecov-api-archive/pulls/comments{/number}","comments_url":"https://api.github.com/repos/codecov/codecov-api-archive/issues/110/comments","statuses_url":"https://api.github.com/repos/codecov/codecov-api-archive/statuses/a178a13c65f44d5b81c807f3c0fa2cb4922f020f","head":{"label":"codecov:ce-1314/gh-status-handler","ref":"ce-1314/gh-status-handler","sha":"a178a13c65f44d5b81c807f3c0fa2cb4922f020f","user":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"repo":{"id":160537716,"node_id":"MDEwOlJlcG9zaXRvcnkxNjA1Mzc3MTY=","name":"codecov-api-archive","full_name":"codecov/codecov-api-archive","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-api-archive","description":"Code + for new API of Codecov","fork":false,"url":"https://api.github.com/repos/codecov/codecov-api-archive","forks_url":"https://api.github.com/repos/codecov/codecov-api-archive/forks","keys_url":"https://api.github.com/repos/codecov/codecov-api-archive/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-api-archive/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-api-archive/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-api-archive/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-api-archive/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-api-archive/events","assignees_url":"https://api.github.com/repos/codecov/codecov-api-archive/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-api-archive/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-api-archive/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-api-archive/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-api-archive/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-api-archive/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-api-archive/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-api-archive/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-api-archive/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-api-archive/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-api-archive/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-api-archive/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-api-archive/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-api-archive/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-api-archive/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-api-archive/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-api-archive/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-api-archive/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-api-archive/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-api-archive/merges","archive_url":"https://api.github.com/repos/codecov/codecov-api-archive/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-api-archive/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-api-archive/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-api-archive/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-api-archive/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-api-archive/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-api-archive/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-api-archive/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-api-archive/deployments","created_at":"2018-12-05T15:21:09Z","updated_at":"2023-07-17T19:06:23Z","pushed_at":"2023-07-17T20:00:53Z","git_url":"git://github.com/codecov/codecov-api-archive.git","ssh_url":"git@github.com:codecov/codecov-api-archive.git","clone_url":"https://github.com/codecov/codecov-api-archive.git","svn_url":"https://github.com/codecov/codecov-api-archive","homepage":null,"size":30781,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":26,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":26,"watchers":0,"default_branch":"main"}},"base":{"label":"codecov:main","ref":"main","sha":"77141afbd13a1273f87cf02f7f32265ea19a3b77","user":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"repo":{"id":160537716,"node_id":"MDEwOlJlcG9zaXRvcnkxNjA1Mzc3MTY=","name":"codecov-api-archive","full_name":"codecov/codecov-api-archive","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-api-archive","description":"Code + for new API of Codecov","fork":false,"url":"https://api.github.com/repos/codecov/codecov-api-archive","forks_url":"https://api.github.com/repos/codecov/codecov-api-archive/forks","keys_url":"https://api.github.com/repos/codecov/codecov-api-archive/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-api-archive/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-api-archive/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-api-archive/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-api-archive/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-api-archive/events","assignees_url":"https://api.github.com/repos/codecov/codecov-api-archive/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-api-archive/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-api-archive/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-api-archive/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-api-archive/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-api-archive/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-api-archive/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-api-archive/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-api-archive/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-api-archive/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-api-archive/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-api-archive/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-api-archive/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-api-archive/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-api-archive/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-api-archive/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-api-archive/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-api-archive/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-api-archive/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-api-archive/merges","archive_url":"https://api.github.com/repos/codecov/codecov-api-archive/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-api-archive/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-api-archive/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-api-archive/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-api-archive/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-api-archive/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-api-archive/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-api-archive/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-api-archive/deployments","created_at":"2018-12-05T15:21:09Z","updated_at":"2023-07-17T19:06:23Z","pushed_at":"2023-07-17T20:00:53Z","git_url":"git://github.com/codecov/codecov-api-archive.git","ssh_url":"git@github.com:codecov/codecov-api-archive.git","clone_url":"https://github.com/codecov/codecov-api-archive.git","svn_url":"https://github.com/codecov/codecov-api-archive","homepage":null,"size":30781,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":26,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":26,"watchers":0,"default_branch":"main"}},"_links":{"self":{"href":"https://api.github.com/repos/codecov/codecov-api-archive/pulls/110"},"html":{"href":"https://github.com/codecov/codecov-api-archive/pull/110"},"issue":{"href":"https://api.github.com/repos/codecov/codecov-api-archive/issues/110"},"comments":{"href":"https://api.github.com/repos/codecov/codecov-api-archive/issues/110/comments"},"review_comments":{"href":"https://api.github.com/repos/codecov/codecov-api-archive/pulls/110/comments"},"review_comment":{"href":"https://api.github.com/repos/codecov/codecov-api-archive/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/codecov/codecov-api-archive/pulls/110/commits"},"statuses":{"href":"https://api.github.com/repos/codecov/codecov-api-archive/statuses/a178a13c65f44d5b81c807f3c0fa2cb4922f020f"}},"author_association":"CONTRIBUTOR","auto_merge":null,"active_lock_reason":null,"merged":true,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"comments":1,"review_comments":0,"maintainer_can_modify":false,"commits":11,"additions":180,"deletions":83,"changed_files":37}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:35:36 GMT + ETag: + - W/"87eb6e8f42ba6a2ca498fdc414110285344e1038a6609c4788b5bf662c2891ce" + Last-Modified: + - Tue, 25 Jul 2023 19:07:11 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C0CB:48C3:144969:152E2C:64F61568 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4980' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '20' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/codecov-api-archive/pulls/110/commits?page=3&per_page=100 + response: + content: '[]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '2' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:35:36 GMT + ETag: + - '"a20ff71ece9649384768ca03ee284519e9552357700b0ef7c43cdee37866531e"' + Last-Modified: + - Tue, 25 Jul 2023 19:07:11 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C0CC:52C9:13F854:14DD13:64F61568 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4979' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '21' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/codecov-api-archive/pulls/110/commits?page=2&per_page=100 + response: + content: '[]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '2' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:35:36 GMT + ETag: + - '"a20ff71ece9649384768ca03ee284519e9552357700b0ef7c43cdee37866531e"' + Last-Modified: + - Tue, 25 Jul 2023 19:07:11 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C0CE:1A20:13CED3:14B389:64F61568 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4978' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '22' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/codecov-api-archive/pulls/110/commits?page=1&per_page=100 + response: + content: '[{"sha":"67a58a280ad5a082edb9843f75a1a0f5424c84ee","node_id":"MDY6Q29tbWl0MTYwNTM3NzE2OjY3YTU4YTI4MGFkNWEwODJlZGI5ODQzZjc1YTFhMGY1NDI0Yzg0ZWU=","commit":{"author":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-14T22:15:05Z"},"committer":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-14T22:15:05Z"},"message":"Add + errors; status handler early exit.","tree":{"sha":"7c2d5e4d5bdc5477c49a45ab88fe324e1b5ae716","url":"https://api.github.com/repos/codecov/codecov-api-archive/git/trees/7c2d5e4d5bdc5477c49a45ab88fe324e1b5ae716"},"url":"https://api.github.com/repos/codecov/codecov-api-archive/git/commits/67a58a280ad5a082edb9843f75a1a0f5424c84ee","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/67a58a280ad5a082edb9843f75a1a0f5424c84ee","html_url":"https://github.com/codecov/codecov-api-archive/commit/67a58a280ad5a082edb9843f75a1a0f5424c84ee","comments_url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/67a58a280ad5a082edb9843f75a1a0f5424c84ee/comments","author":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"committer":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"parents":[{"sha":"a619c7b3aab3301f70f113c77f1af893ef591b5c","url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/a619c7b3aab3301f70f113c77f1af893ef591b5c","html_url":"https://github.com/codecov/codecov-api-archive/commit/a619c7b3aab3301f70f113c77f1af893ef591b5c"}]},{"sha":"47dad27e31e8802eacf6b7b67fa8c2456ccd472b","node_id":"MDY6Q29tbWl0MTYwNTM3NzE2OjQ3ZGFkMjdlMzFlODgwMmVhY2Y2YjdiNjdmYThjMjQ1NmNjZDQ3MmI=","commit":{"author":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-18T20:09:00Z"},"committer":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-18T20:09:00Z"},"message":"Add + status event handler.","tree":{"sha":"c0f7f65e8a46557a2d4a8c5c93763dac450092cb","url":"https://api.github.com/repos/codecov/codecov-api-archive/git/trees/c0f7f65e8a46557a2d4a8c5c93763dac450092cb"},"url":"https://api.github.com/repos/codecov/codecov-api-archive/git/commits/47dad27e31e8802eacf6b7b67fa8c2456ccd472b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/47dad27e31e8802eacf6b7b67fa8c2456ccd472b","html_url":"https://github.com/codecov/codecov-api-archive/commit/47dad27e31e8802eacf6b7b67fa8c2456ccd472b","comments_url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/47dad27e31e8802eacf6b7b67fa8c2456ccd472b/comments","author":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"committer":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"parents":[{"sha":"67a58a280ad5a082edb9843f75a1a0f5424c84ee","url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/67a58a280ad5a082edb9843f75a1a0f5424c84ee","html_url":"https://github.com/codecov/codecov-api-archive/commit/67a58a280ad5a082edb9843f75a1a0f5424c84ee"}]},{"sha":"ed26e75e78bbb97b4e3e115e530831a67543eb45","node_id":"MDY6Q29tbWl0MTYwNTM3NzE2OmVkMjZlNzVlNzhiYmI5N2I0ZTNlMTE1ZTUzMDgzMWE2NzU0M2ViNDU=","commit":{"author":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-18T22:12:50Z"},"committer":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-18T22:16:55Z"},"message":"Move + services to common folder.","tree":{"sha":"b2ebb45d85ffe881844e2840538e4a0fd2c7f30f","url":"https://api.github.com/repos/codecov/codecov-api-archive/git/trees/b2ebb45d85ffe881844e2840538e4a0fd2c7f30f"},"url":"https://api.github.com/repos/codecov/codecov-api-archive/git/commits/ed26e75e78bbb97b4e3e115e530831a67543eb45","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/ed26e75e78bbb97b4e3e115e530831a67543eb45","html_url":"https://github.com/codecov/codecov-api-archive/commit/ed26e75e78bbb97b4e3e115e530831a67543eb45","comments_url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/ed26e75e78bbb97b4e3e115e530831a67543eb45/comments","author":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"committer":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"parents":[{"sha":"47dad27e31e8802eacf6b7b67fa8c2456ccd472b","url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/47dad27e31e8802eacf6b7b67fa8c2456ccd472b","html_url":"https://github.com/codecov/codecov-api-archive/commit/47dad27e31e8802eacf6b7b67fa8c2456ccd472b"}]},{"sha":"acee97c0eb7efe2309f5ef36176037883c54a22d","node_id":"MDY6Q29tbWl0MTYwNTM3NzE2OmFjZWU5N2MwZWI3ZWZlMjMwOWY1ZWYzNjE3NjAzNzg4M2M1NGEyMmQ=","commit":{"author":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-18T22:14:38Z"},"committer":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-18T22:16:55Z"},"message":"Remove + old service directories.","tree":{"sha":"735f1ac6082ce5cd5443cf3c59c2214d8cba6763","url":"https://api.github.com/repos/codecov/codecov-api-archive/git/trees/735f1ac6082ce5cd5443cf3c59c2214d8cba6763"},"url":"https://api.github.com/repos/codecov/codecov-api-archive/git/commits/acee97c0eb7efe2309f5ef36176037883c54a22d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/acee97c0eb7efe2309f5ef36176037883c54a22d","html_url":"https://github.com/codecov/codecov-api-archive/commit/acee97c0eb7efe2309f5ef36176037883c54a22d","comments_url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/acee97c0eb7efe2309f5ef36176037883c54a22d/comments","author":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"committer":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"parents":[{"sha":"ed26e75e78bbb97b4e3e115e530831a67543eb45","url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/ed26e75e78bbb97b4e3e115e530831a67543eb45","html_url":"https://github.com/codecov/codecov-api-archive/commit/ed26e75e78bbb97b4e3e115e530831a67543eb45"}]},{"sha":"e2ca2002bcb34defae14450092e54961e9871a1f","node_id":"MDY6Q29tbWl0MTYwNTM3NzE2OmUyY2EyMDAyYmNiMzRkZWZhZTE0NDUwMDkyZTU0OTYxZTk4NzFhMWY=","commit":{"author":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-18T22:26:59Z"},"committer":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-18T22:26:59Z"},"message":"Actually + move files.","tree":{"sha":"84a8a259125b512320c5408ff0311b3e327d903e","url":"https://api.github.com/repos/codecov/codecov-api-archive/git/trees/84a8a259125b512320c5408ff0311b3e327d903e"},"url":"https://api.github.com/repos/codecov/codecov-api-archive/git/commits/e2ca2002bcb34defae14450092e54961e9871a1f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/e2ca2002bcb34defae14450092e54961e9871a1f","html_url":"https://github.com/codecov/codecov-api-archive/commit/e2ca2002bcb34defae14450092e54961e9871a1f","comments_url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/e2ca2002bcb34defae14450092e54961e9871a1f/comments","author":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"committer":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"parents":[{"sha":"acee97c0eb7efe2309f5ef36176037883c54a22d","url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/acee97c0eb7efe2309f5ef36176037883c54a22d","html_url":"https://github.com/codecov/codecov-api-archive/commit/acee97c0eb7efe2309f5ef36176037883c54a22d"}]},{"sha":"72a0a454c08485b8f1b9b1f63374b75c09e3691c","node_id":"MDY6Q29tbWl0MTYwNTM3NzE2OjcyYTBhNDU0YzA4NDg1YjhmMWI5YjFmNjMzNzRiNzVjMDllMzY5MWM=","commit":{"author":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-18T22:33:24Z"},"committer":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-18T22:33:24Z"},"message":"Add + __init__.py to services.","tree":{"sha":"243fc1bef1ed84b6416495f9b9d5f1ce7c9442bb","url":"https://api.github.com/repos/codecov/codecov-api-archive/git/trees/243fc1bef1ed84b6416495f9b9d5f1ce7c9442bb"},"url":"https://api.github.com/repos/codecov/codecov-api-archive/git/commits/72a0a454c08485b8f1b9b1f63374b75c09e3691c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/72a0a454c08485b8f1b9b1f63374b75c09e3691c","html_url":"https://github.com/codecov/codecov-api-archive/commit/72a0a454c08485b8f1b9b1f63374b75c09e3691c","comments_url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/72a0a454c08485b8f1b9b1f63374b75c09e3691c/comments","author":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"committer":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"parents":[{"sha":"e2ca2002bcb34defae14450092e54961e9871a1f","url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/e2ca2002bcb34defae14450092e54961e9871a1f","html_url":"https://github.com/codecov/codecov-api-archive/commit/e2ca2002bcb34defae14450092e54961e9871a1f"}]},{"sha":"4a793c8ddc01173d8c6bc5ded082d3858f45d36f","node_id":"MDY6Q29tbWl0MTYwNTM3NzE2OjRhNzkzYzhkZGMwMTE3M2Q4YzZiYzVkZWQwODJkMzg1OGY0NWQzNmY=","commit":{"author":{"name":"Thomas + Buida","email":"TJBIII@users.noreply.github.com","date":"2020-02-18T22:45:18Z"},"committer":{"name":"GitHub","email":"noreply@github.com","date":"2020-02-18T22:45:18Z"},"message":"Merge + branch ''main'' into ce-1314/gh-status-handler","tree":{"sha":"3df81f42dccb5187daf267f655656c00de68020d","url":"https://api.github.com/repos/codecov/codecov-api-archive/git/trees/3df81f42dccb5187daf267f655656c00de68020d"},"url":"https://api.github.com/repos/codecov/codecov-api-archive/git/commits/4a793c8ddc01173d8c6bc5ded082d3858f45d36f","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJeTGj+CRBK7hj4Ov3rIwAAdHIIABjx6sPiJx6Snw/MakmJ9/A1\nfPWUskWKZDUJ6mTh5EQ3XNrtvldw+84CdDiG4xelqgYzVJ41L+dQlQLxg6iIA4h2\nDGjkPVXbPqoyfrb6Zh+e4GaPEM2UO/IaV3OfGq9YgOtgI8iYSskmykK+0ZszIajV\nhT3Tro3iDjx0nuU1Va+aQEsghN95iPqeFSe4KHD0cujb5LLyNGRb8BdWH5Wur0Dn\nTJkMHVWGB5z3tOC+11m3E/snHPV/75IqhgwGgDTdSWIwnKcoM7VZQZEN0SUUKaDc\nQLGtkSxvCQ4+9f2VT3uXARIlwjrKIpD+4kTRgoFP1+ZM3bw5xHmVjxS0R/kaiDU=\n=Tth7\n-----END + PGP SIGNATURE-----\n","payload":"tree 3df81f42dccb5187daf267f655656c00de68020d\nparent + 72a0a454c08485b8f1b9b1f63374b75c09e3691c\nparent 77141afbd13a1273f87cf02f7f32265ea19a3b77\nauthor + Thomas Buida 1582065918 -0600\ncommitter GitHub + 1582065918 -0600\n\nMerge branch ''main'' into ce-1314/gh-status-handler"}},"url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/4a793c8ddc01173d8c6bc5ded082d3858f45d36f","html_url":"https://github.com/codecov/codecov-api-archive/commit/4a793c8ddc01173d8c6bc5ded082d3858f45d36f","comments_url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/4a793c8ddc01173d8c6bc5ded082d3858f45d36f/comments","author":{"login":"TJBIII","id":13630281,"node_id":"MDQ6VXNlcjEzNjMwMjgx","avatar_url":"https://avatars.githubusercontent.com/u/13630281?v=4","gravatar_id":"","url":"https://api.github.com/users/TJBIII","html_url":"https://github.com/TJBIII","followers_url":"https://api.github.com/users/TJBIII/followers","following_url":"https://api.github.com/users/TJBIII/following{/other_user}","gists_url":"https://api.github.com/users/TJBIII/gists{/gist_id}","starred_url":"https://api.github.com/users/TJBIII/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/TJBIII/subscriptions","organizations_url":"https://api.github.com/users/TJBIII/orgs","repos_url":"https://api.github.com/users/TJBIII/repos","events_url":"https://api.github.com/users/TJBIII/events{/privacy}","received_events_url":"https://api.github.com/users/TJBIII/received_events","type":"User","site_admin":false},"committer":{"login":"web-flow","id":19864447,"node_id":"MDQ6VXNlcjE5ODY0NDQ3","avatar_url":"https://avatars.githubusercontent.com/u/19864447?v=4","gravatar_id":"","url":"https://api.github.com/users/web-flow","html_url":"https://github.com/web-flow","followers_url":"https://api.github.com/users/web-flow/followers","following_url":"https://api.github.com/users/web-flow/following{/other_user}","gists_url":"https://api.github.com/users/web-flow/gists{/gist_id}","starred_url":"https://api.github.com/users/web-flow/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/web-flow/subscriptions","organizations_url":"https://api.github.com/users/web-flow/orgs","repos_url":"https://api.github.com/users/web-flow/repos","events_url":"https://api.github.com/users/web-flow/events{/privacy}","received_events_url":"https://api.github.com/users/web-flow/received_events","type":"User","site_admin":false},"parents":[{"sha":"72a0a454c08485b8f1b9b1f63374b75c09e3691c","url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/72a0a454c08485b8f1b9b1f63374b75c09e3691c","html_url":"https://github.com/codecov/codecov-api-archive/commit/72a0a454c08485b8f1b9b1f63374b75c09e3691c"},{"sha":"77141afbd13a1273f87cf02f7f32265ea19a3b77","url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/77141afbd13a1273f87cf02f7f32265ea19a3b77","html_url":"https://github.com/codecov/codecov-api-archive/commit/77141afbd13a1273f87cf02f7f32265ea19a3b77"}]},{"sha":"f5f235daff84200fb828869f80d389fca81880af","node_id":"MDY6Q29tbWl0MTYwNTM3NzE2OmY1ZjIzNWRhZmY4NDIwMGZiODI4ODY5ZjgwZDM4OWZjYTgxODgwYWY=","commit":{"author":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-18T23:16:28Z"},"committer":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-18T23:16:28Z"},"message":"Fix + spelling issue.","tree":{"sha":"6f97b2fc81424323d4757aef55edbc60eabb26c3","url":"https://api.github.com/repos/codecov/codecov-api-archive/git/trees/6f97b2fc81424323d4757aef55edbc60eabb26c3"},"url":"https://api.github.com/repos/codecov/codecov-api-archive/git/commits/f5f235daff84200fb828869f80d389fca81880af","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/f5f235daff84200fb828869f80d389fca81880af","html_url":"https://github.com/codecov/codecov-api-archive/commit/f5f235daff84200fb828869f80d389fca81880af","comments_url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/f5f235daff84200fb828869f80d389fca81880af/comments","author":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"committer":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"parents":[{"sha":"4a793c8ddc01173d8c6bc5ded082d3858f45d36f","url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/4a793c8ddc01173d8c6bc5ded082d3858f45d36f","html_url":"https://github.com/codecov/codecov-api-archive/commit/4a793c8ddc01173d8c6bc5ded082d3858f45d36f"}]},{"sha":"b68cdcbf6cc1b270a16d8a82b67027bdbc087452","node_id":"MDY6Q29tbWl0MTYwNTM3NzE2OmI2OGNkY2JmNmNjMWIyNzBhMTZkOGE4MmI2NzAyN2JkYmMwODc0NTI=","commit":{"author":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-19T01:43:54Z"},"committer":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-19T01:43:54Z"},"message":"rename + file to be pytest compatible????","tree":{"sha":"f2c6e03213fe34512851e3734775272875d97606","url":"https://api.github.com/repos/codecov/codecov-api-archive/git/trees/f2c6e03213fe34512851e3734775272875d97606"},"url":"https://api.github.com/repos/codecov/codecov-api-archive/git/commits/b68cdcbf6cc1b270a16d8a82b67027bdbc087452","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/b68cdcbf6cc1b270a16d8a82b67027bdbc087452","html_url":"https://github.com/codecov/codecov-api-archive/commit/b68cdcbf6cc1b270a16d8a82b67027bdbc087452","comments_url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/b68cdcbf6cc1b270a16d8a82b67027bdbc087452/comments","author":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"committer":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"parents":[{"sha":"f5f235daff84200fb828869f80d389fca81880af","url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/f5f235daff84200fb828869f80d389fca81880af","html_url":"https://github.com/codecov/codecov-api-archive/commit/f5f235daff84200fb828869f80d389fca81880af"}]},{"sha":"ee76f3b455600fc0ed39f52084f3d46a739504cf","node_id":"MDY6Q29tbWl0MTYwNTM3NzE2OmVlNzZmM2I0NTU2MDBmYzBlZDM5ZjUyMDg0ZjNkNDZhNzM5NTA0Y2Y=","commit":{"author":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-19T18:02:56Z"},"committer":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-19T18:02:56Z"},"message":"Patch + redis as a hack until we can make it part of circle build.","tree":{"sha":"f9c8e519f90824764b528f6a05f474bdfd7907bc","url":"https://api.github.com/repos/codecov/codecov-api-archive/git/trees/f9c8e519f90824764b528f6a05f474bdfd7907bc"},"url":"https://api.github.com/repos/codecov/codecov-api-archive/git/commits/ee76f3b455600fc0ed39f52084f3d46a739504cf","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/ee76f3b455600fc0ed39f52084f3d46a739504cf","html_url":"https://github.com/codecov/codecov-api-archive/commit/ee76f3b455600fc0ed39f52084f3d46a739504cf","comments_url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/ee76f3b455600fc0ed39f52084f3d46a739504cf/comments","author":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"committer":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"parents":[{"sha":"b68cdcbf6cc1b270a16d8a82b67027bdbc087452","url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/b68cdcbf6cc1b270a16d8a82b67027bdbc087452","html_url":"https://github.com/codecov/codecov-api-archive/commit/b68cdcbf6cc1b270a16d8a82b67027bdbc087452"}]},{"sha":"a178a13c65f44d5b81c807f3c0fa2cb4922f020f","node_id":"MDY6Q29tbWl0MTYwNTM3NzE2OmExNzhhMTNjNjVmNDRkNWI4MWM4MDdmM2MwZmEyY2I0OTIyZjAyMGY=","commit":{"author":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-19T18:19:22Z"},"committer":{"name":"Pierce + McEntagart","email":"pierce@codecov.io","date":"2020-02-19T18:19:22Z"},"message":"Move + github view and tests into folders.","tree":{"sha":"a71c40d1e4c5055a133887c9ae6334084b8e3889","url":"https://api.github.com/repos/codecov/codecov-api-archive/git/trees/a71c40d1e4c5055a133887c9ae6334084b8e3889"},"url":"https://api.github.com/repos/codecov/codecov-api-archive/git/commits/a178a13c65f44d5b81c807f3c0fa2cb4922f020f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/a178a13c65f44d5b81c807f3c0fa2cb4922f020f","html_url":"https://github.com/codecov/codecov-api-archive/commit/a178a13c65f44d5b81c807f3c0fa2cb4922f020f","comments_url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/a178a13c65f44d5b81c807f3c0fa2cb4922f020f/comments","author":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"committer":{"login":"pierce-m","id":5767537,"node_id":"MDQ6VXNlcjU3Njc1Mzc=","avatar_url":"https://avatars.githubusercontent.com/u/5767537?v=4","gravatar_id":"","url":"https://api.github.com/users/pierce-m","html_url":"https://github.com/pierce-m","followers_url":"https://api.github.com/users/pierce-m/followers","following_url":"https://api.github.com/users/pierce-m/following{/other_user}","gists_url":"https://api.github.com/users/pierce-m/gists{/gist_id}","starred_url":"https://api.github.com/users/pierce-m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pierce-m/subscriptions","organizations_url":"https://api.github.com/users/pierce-m/orgs","repos_url":"https://api.github.com/users/pierce-m/repos","events_url":"https://api.github.com/users/pierce-m/events{/privacy}","received_events_url":"https://api.github.com/users/pierce-m/received_events","type":"User","site_admin":false},"parents":[{"sha":"ee76f3b455600fc0ed39f52084f3d46a739504cf","url":"https://api.github.com/repos/codecov/codecov-api-archive/commits/ee76f3b455600fc0ed39f52084f3d46a739504cf","html_url":"https://github.com/codecov/codecov-api-archive/commit/ee76f3b455600fc0ed39f52084f3d46a739504cf"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:35:36 GMT + ETag: + - W/"f1a29b59181e785ac1e5d1a45594f6a4d19d321742d46b7a3a1769e4c7804de2" + Last-Modified: + - Tue, 25 Jul 2023 19:07:11 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - C0CD:0DA1:141BA9:15007C:64F61568 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4977' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '23' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_commits.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_commits.yaml new file mode 100644 index 0000000000..0538777c4a --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_commits.yaml @@ -0,0 +1,267 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits?page=3&per_page=100 + response: + content: '[]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '2' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:17:14 GMT + ETag: + - '"a20ff71ece9649384768ca03ee284519e9552357700b0ef7c43cdee37866531e"' + Last-Modified: + - Fri, 01 Sep 2023 11:48:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF8C:48C3:129149:136439:64F61119 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4991' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '9' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits?page=2&per_page=100 + response: + content: '[]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '2' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:17:14 GMT + ETag: + - '"a20ff71ece9649384768ca03ee284519e9552357700b0ef7c43cdee37866531e"' + Last-Modified: + - Fri, 01 Sep 2023 11:48:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF8E:0DA1:1253C3:1326C0:64F61119 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4990' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '10' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/1/commits?page=1&per_page=100 + response: + content: '[{"sha":"587662b6e5403ae0d126e0c7839a8d98382c4760","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjU4NzY2MmI2ZTU0MDNhZTBkMTI2ZTBjNzgzOWE4ZDk4MzgyYzQ3NjA=","commit":{"author":{"name":"Thiago + Ribeiro Ramos","email":"thiago@ribeiroramos.com","date":"2018-11-07T22:43:54Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:34:45Z"},"message":"Creating + new code for reasons no one knows","tree":{"sha":"ec56802a37b981f13bdc3c9a56ae68ef82ab424a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ec56802a37b981f13bdc3c9a56ae68ef82ab424a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","html_url":"https://github.com/ThiagoCodecov/example-python/commit/587662b6e5403ae0d126e0c7839a8d98382c4760","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760/comments","author":null,"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"68946ef98daec68c7798459150982fc799c87d85","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/68946ef98daec68c7798459150982fc799c87d85","html_url":"https://github.com/ThiagoCodecov/example-python/commit/68946ef98daec68c7798459150982fc799c87d85"}]},{"sha":"03a8b737cb9d8585076ebdbac7b7235c8da0620d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjAzYThiNzM3Y2I5ZDg1ODUwNzZlYmRiYWM3YjcyMzVjOGRhMDYyMGQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-03-12T02:37:19Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:36:02Z"},"message":"Now + what","tree":{"sha":"51a385e1f575447b0b70fd597596c32c4f5bd172","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/51a385e1f575447b0b70fd597596c32c4f5bd172"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/03a8b737cb9d8585076ebdbac7b7235c8da0620d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"587662b6e5403ae0d126e0c7839a8d98382c4760","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/587662b6e5403ae0d126e0c7839a8d98382c4760","html_url":"https://github.com/ThiagoCodecov/example-python/commit/587662b6e5403ae0d126e0c7839a8d98382c4760"}]},{"sha":"bf9b57cf7b169806ae2d18d7671aba3825b99203","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmJmOWI1N2NmN2IxNjk4MDZhZTJkMThkNzY3MWFiYTM4MjViOTkyMDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-03-12T02:42:33Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-10T20:36:02Z"},"message":"Adding + untested code","tree":{"sha":"ce5383a6feb3e0bf20a4df46ae6c67ec3955723e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ce5383a6feb3e0bf20a4df46ae6c67ec3955723e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bf9b57cf7b169806ae2d18d7671aba3825b99203","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"03a8b737cb9d8585076ebdbac7b7235c8da0620d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03a8b737cb9d8585076ebdbac7b7235c8da0620d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/03a8b737cb9d8585076ebdbac7b7235c8da0620d"}]},{"sha":"cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmNlZGUxOWNiMzEwY2Q0Y2RkZmI1ZDg5MjFjYjhkMGNjN2M3YzE1MDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-16T22:02:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-16T22:11:24Z"},"message":"asdadafdsfdsfds","tree":{"sha":"e614247adf8a0705575e9c2170fad7c2848870a0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e614247adf8a0705575e9c2170fad7c2848870a0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"bf9b57cf7b169806ae2d18d7671aba3825b99203","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bf9b57cf7b169806ae2d18d7671aba3825b99203","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bf9b57cf7b169806ae2d18d7671aba3825b99203"}]},{"sha":"ea3ada938db123368d62b0133e7c5bb54b5292b9","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmVhM2FkYTkzOGRiMTIzMzY4ZDYyYjAxMzNlN2M1YmI1NGI1MjkyYjk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-19T18:48:19Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-07-19T18:48:19Z"},"message":"Adding + file t2 haha","tree":{"sha":"9ac6564d515ed2630026080e7cbdad4edfa9eca6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9ac6564d515ed2630026080e7cbdad4edfa9eca6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ea3ada938db123368d62b0133e7c5bb54b5292b9","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503"}]},{"sha":"2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjIwNDhiMjc3ZGQ2NTQyZjE4NGM2YTMwYzNlMmIwZjNlZTVlZWFmNGI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:43:42Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:43:42Z"},"message":"Adding + file t2 haha oooggg","tree":{"sha":"8b8d478591c3125af92ac395e87ddfb37fec5086","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/8b8d478591c3125af92ac395e87ddfb37fec5086"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ea3ada938db123368d62b0133e7c5bb54b5292b9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea3ada938db123368d62b0133e7c5bb54b5292b9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ea3ada938db123368d62b0133e7c5bb54b5292b9"}]},{"sha":"119de54e3cfdf8227a8556b9f5730c328a1390cd","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjExOWRlNTRlM2NmZGY4MjI3YTg1NTZiOWY1NzMwYzMyOGExMzkwY2Q=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:46:16Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-08T07:46:16Z"},"message":"Adding + file t2 haha oooggdsadsdsag","tree":{"sha":"d3868402c41afd8dcafb50e5bfa0e023f35c307e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/d3868402c41afd8dcafb50e5bfa0e023f35c307e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119de54e3cfdf8227a8556b9f5730c328a1390cd","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b"}]},{"sha":"2d55e8501b058b6f25382c4e287f022e8938461f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJkNTVlODUwMWIwNThiNmYyNTM4MmM0ZTI4N2YwMjJlODkzODQ2MWY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-24T21:32:08Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-24T21:32:08Z"},"message":"Adding + file t4 unpredictable","tree":{"sha":"a87f6d6ddd74d6df712bad79cc65d040c408efe8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/a87f6d6ddd74d6df712bad79cc65d040c408efe8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2d55e8501b058b6f25382c4e287f022e8938461f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2d55e8501b058b6f25382c4e287f022e8938461f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"119de54e3cfdf8227a8556b9f5730c328a1390cd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119de54e3cfdf8227a8556b9f5730c328a1390cd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119de54e3cfdf8227a8556b9f5730c328a1390cd"}]},{"sha":"364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjM2NGJkZmJjNzJkNWUwNWI1MjBmMDMyMGIwZDhiMzlmZDllYTY5MmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-28T22:50:25Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-08-28T22:50:25Z"},"message":"Adding + Makefile","tree":{"sha":"452c48e858913bacb4be63a8e2351c98719406dd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/452c48e858913bacb4be63a8e2351c98719406dd"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2d55e8501b058b6f25382c4e287f022e8938461f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2d55e8501b058b6f25382c4e287f022e8938461f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2d55e8501b058b6f25382c4e287f022e8938461f"}]},{"sha":"119c1907fb266f374b8440bbd70dccbea54daf8f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjExOWMxOTA3ZmIyNjZmMzc0Yjg0NDBiYmQ3MGRjY2JlYTU0ZGFmOGY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-09-02T23:07:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-09-02T23:07:56Z"},"message":"Cleaning + some stuff","tree":{"sha":"4995d75a388061164491217b50ee296137150f89","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4995d75a388061164491217b50ee296137150f89"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/119c1907fb266f374b8440bbd70dccbea54daf8f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119c1907fb266f374b8440bbd70dccbea54daf8f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/119c1907fb266f374b8440bbd70dccbea54daf8f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/119c1907fb266f374b8440bbd70dccbea54daf8f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/364bdfbc72d5e05b520f0320b0d8b39fd9ea692b"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:17:14 GMT + ETag: + - W/"b91cf76b3ef50bf6937de9f89888efb09fe44940be57d540bab83c68a1ee68a0" + Last-Modified: + - Fri, 01 Sep 2023 11:48:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF8D:52C9:1242C8:1315BA:64F61119 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4989' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '11' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_commits_paginated.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_commits_paginated.yaml new file mode 100644 index 0000000000..9b34b62e0d --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_commits_paginated.yaml @@ -0,0 +1,504 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/y0-causal-inference/y0/pulls/149/commits?page=3&per_page=100 + response: + content: '[]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '2' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:19:00 GMT + ETag: + - '"a20ff71ece9649384768ca03ee284519e9552357700b0ef7c43cdee37866531e"' + Last-Modified: + - Mon, 04 Sep 2023 16:56:11 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FFB4:1A94:125C91:133156:64F61184 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4983' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '17' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/y0-causal-inference/y0/pulls/149/commits?page=2&per_page=100 + response: + content: '[{"sha":"d44a99623f25654118660d6a4456c9cf96a6a2ee","node_id":"C_kwDOE5hB_NoAKGQ0NGE5OTYyM2YyNTY1NDExODY2MGQ2YTQ0NTZjOWNmOTZhNmEyZWU","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T10:39:25Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T10:39:25Z"},"message":"Keep + working towards improving DSL output","tree":{"sha":"5a72d1485b104a26eedfcab50fb846667da428bd","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/5a72d1485b104a26eedfcab50fb846667da428bd"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/d44a99623f25654118660d6a4456c9cf96a6a2ee","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/d44a99623f25654118660d6a4456c9cf96a6a2ee","html_url":"https://github.com/y0-causal-inference/y0/commit/d44a99623f25654118660d6a4456c9cf96a6a2ee","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/d44a99623f25654118660d6a4456c9cf96a6a2ee/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"07620c296d085a2a508a2aecf0bfe7969a5a2260","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/07620c296d085a2a508a2aecf0bfe7969a5a2260","html_url":"https://github.com/y0-causal-inference/y0/commit/07620c296d085a2a508a2aecf0bfe7969a5a2260"}]},{"sha":"360e47c4e31023762e0874deb77da17cf0eb1a01","node_id":"C_kwDOE5hB_NoAKDM2MGU0N2M0ZTMxMDIzNzYyZTA4NzRkZWI3N2RhMTdjZjBlYjFhMDE","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T15:35:26Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T15:35:26Z"},"message":"Update + conditional to work with CF variables","tree":{"sha":"8dfce5cca031e778320af6ad0fd884fe5cf42850","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/8dfce5cca031e778320af6ad0fd884fe5cf42850"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/360e47c4e31023762e0874deb77da17cf0eb1a01","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/360e47c4e31023762e0874deb77da17cf0eb1a01","html_url":"https://github.com/y0-causal-inference/y0/commit/360e47c4e31023762e0874deb77da17cf0eb1a01","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/360e47c4e31023762e0874deb77da17cf0eb1a01/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"d44a99623f25654118660d6a4456c9cf96a6a2ee","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/d44a99623f25654118660d6a4456c9cf96a6a2ee","html_url":"https://github.com/y0-causal-inference/y0/commit/d44a99623f25654118660d6a4456c9cf96a6a2ee"}]},{"sha":"ce8e2b71c5561e5a349dfaf1ed25ccff3dbed0b9","node_id":"C_kwDOE5hB_NoAKGNlOGUyYjcxYzU1NjFlNWEzNDlkZmFmMWVkMjVjY2ZmM2RiZWQwYjk","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-30T15:43:24Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-30T15:43:24Z"},"message":"added + tests to trso","tree":{"sha":"3a9e3c6538f3ad0aa9f2467cff96a0713822772c","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/3a9e3c6538f3ad0aa9f2467cff96a0713822772c"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/ce8e2b71c5561e5a349dfaf1ed25ccff3dbed0b9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ce8e2b71c5561e5a349dfaf1ed25ccff3dbed0b9","html_url":"https://github.com/y0-causal-inference/y0/commit/ce8e2b71c5561e5a349dfaf1ed25ccff3dbed0b9","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ce8e2b71c5561e5a349dfaf1ed25ccff3dbed0b9/comments","author":null,"committer":null,"parents":[{"sha":"360e47c4e31023762e0874deb77da17cf0eb1a01","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/360e47c4e31023762e0874deb77da17cf0eb1a01","html_url":"https://github.com/y0-causal-inference/y0/commit/360e47c4e31023762e0874deb77da17cf0eb1a01"}]},{"sha":"9b8e6aa8c39f3dd89fdfde598a263b1e51b83d9f","node_id":"C_kwDOE5hB_NoAKDliOGU2YWE4YzM5ZjNkZDg5ZmRmZGU1OThhMjYzYjFlNTFiODNkOWY","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-30T16:10:07Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-30T16:10:07Z"},"message":"updates + to trso_line10","tree":{"sha":"526278db546afa1bca53163371260f7f2b663f46","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/526278db546afa1bca53163371260f7f2b663f46"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/9b8e6aa8c39f3dd89fdfde598a263b1e51b83d9f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/9b8e6aa8c39f3dd89fdfde598a263b1e51b83d9f","html_url":"https://github.com/y0-causal-inference/y0/commit/9b8e6aa8c39f3dd89fdfde598a263b1e51b83d9f","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/9b8e6aa8c39f3dd89fdfde598a263b1e51b83d9f/comments","author":null,"committer":null,"parents":[{"sha":"ce8e2b71c5561e5a349dfaf1ed25ccff3dbed0b9","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ce8e2b71c5561e5a349dfaf1ed25ccff3dbed0b9","html_url":"https://github.com/y0-causal-inference/y0/commit/ce8e2b71c5561e5a349dfaf1ed25ccff3dbed0b9"}]},{"sha":"667efb38e419db55b2967555bef331b3a5991ea6","node_id":"C_kwDOE5hB_NoAKDY2N2VmYjM4ZTQxOWRiNTViMjk2NzU1NWJlZjMzMWIzYTU5OTFlYTY","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T22:18:24Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T22:18:24Z"},"message":"Merge + branch ''main'' into transport","tree":{"sha":"f448b0944f49e537d2a2a273c1483be6439e79cf","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/f448b0944f49e537d2a2a273c1483be6439e79cf"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/667efb38e419db55b2967555bef331b3a5991ea6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/667efb38e419db55b2967555bef331b3a5991ea6","html_url":"https://github.com/y0-causal-inference/y0/commit/667efb38e419db55b2967555bef331b3a5991ea6","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/667efb38e419db55b2967555bef331b3a5991ea6/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"9b8e6aa8c39f3dd89fdfde598a263b1e51b83d9f","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/9b8e6aa8c39f3dd89fdfde598a263b1e51b83d9f","html_url":"https://github.com/y0-causal-inference/y0/commit/9b8e6aa8c39f3dd89fdfde598a263b1e51b83d9f"},{"sha":"9b88d0e89efba2887518825bcdf3af7bc8df9a7f","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/9b88d0e89efba2887518825bcdf3af7bc8df9a7f","html_url":"https://github.com/y0-causal-inference/y0/commit/9b88d0e89efba2887518825bcdf3af7bc8df9a7f"}]},{"sha":"302a3143024120d0644b62bab698b6ee39149266","node_id":"C_kwDOE5hB_NoAKDMwMmEzMTQzMDI0MTIwZDA2NDRiNjJiYWI2OThiNmVlMzkxNDkyNjY","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-31T14:10:56Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-31T14:10:56Z"},"message":"Mypy + cleanup","tree":{"sha":"42ef5fe361224f1a1e5921208c73d63e1546ac0c","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/42ef5fe361224f1a1e5921208c73d63e1546ac0c"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/302a3143024120d0644b62bab698b6ee39149266","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/302a3143024120d0644b62bab698b6ee39149266","html_url":"https://github.com/y0-causal-inference/y0/commit/302a3143024120d0644b62bab698b6ee39149266","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/302a3143024120d0644b62bab698b6ee39149266/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"667efb38e419db55b2967555bef331b3a5991ea6","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/667efb38e419db55b2967555bef331b3a5991ea6","html_url":"https://github.com/y0-causal-inference/y0/commit/667efb38e419db55b2967555bef331b3a5991ea6"}]},{"sha":"cfebc9b875b034a8291121147184d5eeab455193","node_id":"C_kwDOE5hB_NoAKGNmZWJjOWI4NzViMDM0YTgyOTExMjExNDcxODRkNWVlYWI0NTUxOTM","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-31T16:30:59Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-31T16:30:59Z"},"message":"Update + transport.py","tree":{"sha":"169c7cf082aceee8f456a9d1f0f3d7035e5e428c","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/169c7cf082aceee8f456a9d1f0f3d7035e5e428c"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/cfebc9b875b034a8291121147184d5eeab455193","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/cfebc9b875b034a8291121147184d5eeab455193","html_url":"https://github.com/y0-causal-inference/y0/commit/cfebc9b875b034a8291121147184d5eeab455193","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/cfebc9b875b034a8291121147184d5eeab455193/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"302a3143024120d0644b62bab698b6ee39149266","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/302a3143024120d0644b62bab698b6ee39149266","html_url":"https://github.com/y0-causal-inference/y0/commit/302a3143024120d0644b62bab698b6ee39149266"}]},{"sha":"b832bcac8ecbd90c489d83ec1969b9fed5fcfbba","node_id":"C_kwDOE5hB_NoAKGI4MzJiY2FjOGVjYmQ5MGM0ODlkODNlYzE5NjliOWZlZDVmY2ZiYmE","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-31T16:58:26Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-31T16:58:26Z"},"message":"Update + transport.py","tree":{"sha":"7c34983fe6f352fa70d0cbbe594349b7d0170203","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/7c34983fe6f352fa70d0cbbe594349b7d0170203"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/b832bcac8ecbd90c489d83ec1969b9fed5fcfbba","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/b832bcac8ecbd90c489d83ec1969b9fed5fcfbba","html_url":"https://github.com/y0-causal-inference/y0/commit/b832bcac8ecbd90c489d83ec1969b9fed5fcfbba","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/b832bcac8ecbd90c489d83ec1969b9fed5fcfbba/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"cfebc9b875b034a8291121147184d5eeab455193","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/cfebc9b875b034a8291121147184d5eeab455193","html_url":"https://github.com/y0-causal-inference/y0/commit/cfebc9b875b034a8291121147184d5eeab455193"}]},{"sha":"7aa120be97c0101b36e2770eae10581b0cc63b75","node_id":"C_kwDOE5hB_NoAKDdhYTEyMGJlOTdjMDEwMWIzNmUyNzcwZWFlMTA1ODFiMGNjNjNiNzU","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-31T16:59:22Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-31T16:59:22Z"},"message":"test + for trso_line10","tree":{"sha":"e06ede19c4c7e2aa8136c24943a33da80fe2198c","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/e06ede19c4c7e2aa8136c24943a33da80fe2198c"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/7aa120be97c0101b36e2770eae10581b0cc63b75","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7aa120be97c0101b36e2770eae10581b0cc63b75","html_url":"https://github.com/y0-causal-inference/y0/commit/7aa120be97c0101b36e2770eae10581b0cc63b75","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7aa120be97c0101b36e2770eae10581b0cc63b75/comments","author":null,"committer":null,"parents":[{"sha":"cfebc9b875b034a8291121147184d5eeab455193","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/cfebc9b875b034a8291121147184d5eeab455193","html_url":"https://github.com/y0-causal-inference/y0/commit/cfebc9b875b034a8291121147184d5eeab455193"}]},{"sha":"684e04fc7c8840c12184c7dfb2b6ea2279cb12b9","node_id":"C_kwDOE5hB_NoAKDY4NGUwNGZjN2M4ODQwYzEyMTg0YzdkZmIyYjZlYTIyNzljYjEyYjk","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-31T16:59:35Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-31T16:59:35Z"},"message":"Merge + branch ''transport'' of https://github.com/y0-causal-inference/y0 into transport","tree":{"sha":"c3b14ce122f7b52ce17df413d4ec6bcef086620f","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/c3b14ce122f7b52ce17df413d4ec6bcef086620f"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/684e04fc7c8840c12184c7dfb2b6ea2279cb12b9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/684e04fc7c8840c12184c7dfb2b6ea2279cb12b9","html_url":"https://github.com/y0-causal-inference/y0/commit/684e04fc7c8840c12184c7dfb2b6ea2279cb12b9","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/684e04fc7c8840c12184c7dfb2b6ea2279cb12b9/comments","author":null,"committer":null,"parents":[{"sha":"7aa120be97c0101b36e2770eae10581b0cc63b75","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7aa120be97c0101b36e2770eae10581b0cc63b75","html_url":"https://github.com/y0-causal-inference/y0/commit/7aa120be97c0101b36e2770eae10581b0cc63b75"},{"sha":"b832bcac8ecbd90c489d83ec1969b9fed5fcfbba","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/b832bcac8ecbd90c489d83ec1969b9fed5fcfbba","html_url":"https://github.com/y0-causal-inference/y0/commit/b832bcac8ecbd90c489d83ec1969b9fed5fcfbba"}]},{"sha":"486fbf6d7f36a01a3ff232eb10574feaa830f648","node_id":"C_kwDOE5hB_NoAKDQ4NmZiZjZkN2YzNmEwMWEzZmYyMzJlYjEwNTc0ZmVhYTgzMGY2NDg","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-31T17:20:31Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-31T17:20:31Z"},"message":"Update + canonicalize_expr.py","tree":{"sha":"ed57781d2cc1c6670947c7e8f8c5f6159ac48026","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/ed57781d2cc1c6670947c7e8f8c5f6159ac48026"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/486fbf6d7f36a01a3ff232eb10574feaa830f648","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/486fbf6d7f36a01a3ff232eb10574feaa830f648","html_url":"https://github.com/y0-causal-inference/y0/commit/486fbf6d7f36a01a3ff232eb10574feaa830f648","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/486fbf6d7f36a01a3ff232eb10574feaa830f648/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"684e04fc7c8840c12184c7dfb2b6ea2279cb12b9","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/684e04fc7c8840c12184c7dfb2b6ea2279cb12b9","html_url":"https://github.com/y0-causal-inference/y0/commit/684e04fc7c8840c12184c7dfb2b6ea2279cb12b9"}]},{"sha":"7bac61f566db72e4d6c6b5b8e01022a67c69401b","node_id":"C_kwDOE5hB_NoAKDdiYWM2MWY1NjZkYjcyZTRkNmM2YjViOGUwMTAyMmE2N2M2OTQwMWI","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-31T17:52:47Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-31T17:52:47Z"},"message":"fixed + trso tests, all passing","tree":{"sha":"54cdea80d3c4b1399e3c1302e6695319b6fe62d7","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/54cdea80d3c4b1399e3c1302e6695319b6fe62d7"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/7bac61f566db72e4d6c6b5b8e01022a67c69401b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7bac61f566db72e4d6c6b5b8e01022a67c69401b","html_url":"https://github.com/y0-causal-inference/y0/commit/7bac61f566db72e4d6c6b5b8e01022a67c69401b","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7bac61f566db72e4d6c6b5b8e01022a67c69401b/comments","author":null,"committer":null,"parents":[{"sha":"486fbf6d7f36a01a3ff232eb10574feaa830f648","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/486fbf6d7f36a01a3ff232eb10574feaa830f648","html_url":"https://github.com/y0-causal-inference/y0/commit/486fbf6d7f36a01a3ff232eb10574feaa830f648"}]},{"sha":"a441ea3b0d1f392044a31bf8112d64663b4219ac","node_id":"C_kwDOE5hB_NoAKGE0NDFlYTNiMGQxZjM5MjA0NGEzMWJmODExMmQ2NDY2M2I0MjE5YWM","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-31T18:33:36Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-31T18:33:36Z"},"message":"Added + transport function and test","tree":{"sha":"5decc745aa1ba310eb2db45f06afcab553a67608","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/5decc745aa1ba310eb2db45f06afcab553a67608"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/a441ea3b0d1f392044a31bf8112d64663b4219ac","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/a441ea3b0d1f392044a31bf8112d64663b4219ac","html_url":"https://github.com/y0-causal-inference/y0/commit/a441ea3b0d1f392044a31bf8112d64663b4219ac","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/a441ea3b0d1f392044a31bf8112d64663b4219ac/comments","author":null,"committer":null,"parents":[{"sha":"7bac61f566db72e4d6c6b5b8e01022a67c69401b","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7bac61f566db72e4d6c6b5b8e01022a67c69401b","html_url":"https://github.com/y0-causal-inference/y0/commit/7bac61f566db72e4d6c6b5b8e01022a67c69401b"}]},{"sha":"4c015cbc129e39aa20287fb454c1e721a205f84b","node_id":"C_kwDOE5hB_NoAKDRjMDE1Y2JjMTI5ZTM5YWEyMDI4N2ZiNDU0YzFlNzIxYTIwNWY4NGI","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-31T19:07:06Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-31T19:07:06Z"},"message":"repaced + bayes_expand with fraction_expand","tree":{"sha":"22ae5b43eb83329eca7880a527ef59cacbffa9ab","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/22ae5b43eb83329eca7880a527ef59cacbffa9ab"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/4c015cbc129e39aa20287fb454c1e721a205f84b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/4c015cbc129e39aa20287fb454c1e721a205f84b","html_url":"https://github.com/y0-causal-inference/y0/commit/4c015cbc129e39aa20287fb454c1e721a205f84b","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/4c015cbc129e39aa20287fb454c1e721a205f84b/comments","author":null,"committer":null,"parents":[{"sha":"a441ea3b0d1f392044a31bf8112d64663b4219ac","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/a441ea3b0d1f392044a31bf8112d64663b4219ac","html_url":"https://github.com/y0-causal-inference/y0/commit/a441ea3b0d1f392044a31bf8112d64663b4219ac"}]},{"sha":"67699e8eb951bee831aace922fc17cc7daeba211","node_id":"C_kwDOE5hB_NoAKDY3Njk5ZThlYjk1MWJlZTgzMWFhY2U5MjJmYzE3Y2M3ZGFlYmEyMTE","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-31T21:21:48Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-31T21:21:48Z"},"message":"Created + additional tests, coverage at 87%","tree":{"sha":"e947c97c59474902cf9b1b08d1c15bba763499f7","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/e947c97c59474902cf9b1b08d1c15bba763499f7"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/67699e8eb951bee831aace922fc17cc7daeba211","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/67699e8eb951bee831aace922fc17cc7daeba211","html_url":"https://github.com/y0-causal-inference/y0/commit/67699e8eb951bee831aace922fc17cc7daeba211","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/67699e8eb951bee831aace922fc17cc7daeba211/comments","author":null,"committer":null,"parents":[{"sha":"4c015cbc129e39aa20287fb454c1e721a205f84b","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/4c015cbc129e39aa20287fb454c1e721a205f84b","html_url":"https://github.com/y0-causal-inference/y0/commit/4c015cbc129e39aa20287fb454c1e721a205f84b"}]},{"sha":"7e5d44946a21746066c7ff186b90227e126f2b8e","node_id":"C_kwDOE5hB_NoAKDdlNWQ0NDk0NmEyMTc0NjA2NmM3ZmYxODZiOTAyMjdlMTI2ZjJiOGU","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-31T21:24:36Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-31T21:24:36Z"},"message":"Fix + sort based on pp","tree":{"sha":"da64e24ccd09730191e8a91fbe99b88cf7d23e26","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/da64e24ccd09730191e8a91fbe99b88cf7d23e26"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/7e5d44946a21746066c7ff186b90227e126f2b8e","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7e5d44946a21746066c7ff186b90227e126f2b8e","html_url":"https://github.com/y0-causal-inference/y0/commit/7e5d44946a21746066c7ff186b90227e126f2b8e","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7e5d44946a21746066c7ff186b90227e126f2b8e/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"67699e8eb951bee831aace922fc17cc7daeba211","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/67699e8eb951bee831aace922fc17cc7daeba211","html_url":"https://github.com/y0-causal-inference/y0/commit/67699e8eb951bee831aace922fc17cc7daeba211"}]},{"sha":"a930c6b294b2db597c46af02bf98b51c817da29d","node_id":"C_kwDOE5hB_NoAKGE5MzBjNmIyOTRiMmRiNTk3YzQ2YWYwMmJmOThiNTFjODE3ZGEyOWQ","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-31T21:24:44Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-31T21:24:44Z"},"message":"Pass + mypy","tree":{"sha":"3f4b3308817924539dd783657986ce7536cffead","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/3f4b3308817924539dd783657986ce7536cffead"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/a930c6b294b2db597c46af02bf98b51c817da29d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/a930c6b294b2db597c46af02bf98b51c817da29d","html_url":"https://github.com/y0-causal-inference/y0/commit/a930c6b294b2db597c46af02bf98b51c817da29d","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/a930c6b294b2db597c46af02bf98b51c817da29d/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"7e5d44946a21746066c7ff186b90227e126f2b8e","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7e5d44946a21746066c7ff186b90227e126f2b8e","html_url":"https://github.com/y0-causal-inference/y0/commit/7e5d44946a21746066c7ff186b90227e126f2b8e"}]},{"sha":"2ddf102660f08e486683209ffb53b006eeba2eb6","node_id":"C_kwDOE5hB_NoAKDJkZGYxMDI2NjBmMDhlNDg2NjgzMjA5ZmZiNTNiMDA2ZWViYTJlYjY","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-31T22:45:17Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-31T22:45:17Z"},"message":"Added + test to trso_line2","tree":{"sha":"1d6ad666c7f0e08e7f5a43d092dd593587107ef9","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/1d6ad666c7f0e08e7f5a43d092dd593587107ef9"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/2ddf102660f08e486683209ffb53b006eeba2eb6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/2ddf102660f08e486683209ffb53b006eeba2eb6","html_url":"https://github.com/y0-causal-inference/y0/commit/2ddf102660f08e486683209ffb53b006eeba2eb6","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/2ddf102660f08e486683209ffb53b006eeba2eb6/comments","author":null,"committer":null,"parents":[{"sha":"a930c6b294b2db597c46af02bf98b51c817da29d","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/a930c6b294b2db597c46af02bf98b51c817da29d","html_url":"https://github.com/y0-causal-inference/y0/commit/a930c6b294b2db597c46af02bf98b51c817da29d"}]},{"sha":"d8a5f07674ac64c08c20e7549558dfc0e7603a4e","node_id":"C_kwDOE5hB_NoAKGQ4YTVmMDc2NzRhYzY0YzA4YzIwZTc1NDk1NThkZmMwZTc2MDNhNGU","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-01T00:19:15Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-01T00:19:15Z"},"message":"test + that triggers part of line 11","tree":{"sha":"06eb2220a6fc4656b68fa013639160692c9c9da3","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/06eb2220a6fc4656b68fa013639160692c9c9da3"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/d8a5f07674ac64c08c20e7549558dfc0e7603a4e","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/d8a5f07674ac64c08c20e7549558dfc0e7603a4e","html_url":"https://github.com/y0-causal-inference/y0/commit/d8a5f07674ac64c08c20e7549558dfc0e7603a4e","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/d8a5f07674ac64c08c20e7549558dfc0e7603a4e/comments","author":null,"committer":null,"parents":[{"sha":"2ddf102660f08e486683209ffb53b006eeba2eb6","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/2ddf102660f08e486683209ffb53b006eeba2eb6","html_url":"https://github.com/y0-causal-inference/y0/commit/2ddf102660f08e486683209ffb53b006eeba2eb6"}]},{"sha":"3b4c535b4a289b17227ff2301727637ac01f6e68","node_id":"C_kwDOE5hB_NoAKDNiNGM1MzViNGEyODliMTcyMjdmZjIzMDE3Mjc2MzdhYzAxZjZlNjg","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-01T00:49:57Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-01T00:49:57Z"},"message":"wrote + test that triggers NotImplementedError on line 9","tree":{"sha":"d3a8adf57f758591379ccee5cefc78b9ff67f856","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/d3a8adf57f758591379ccee5cefc78b9ff67f856"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/3b4c535b4a289b17227ff2301727637ac01f6e68","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3b4c535b4a289b17227ff2301727637ac01f6e68","html_url":"https://github.com/y0-causal-inference/y0/commit/3b4c535b4a289b17227ff2301727637ac01f6e68","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3b4c535b4a289b17227ff2301727637ac01f6e68/comments","author":null,"committer":null,"parents":[{"sha":"d8a5f07674ac64c08c20e7549558dfc0e7603a4e","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/d8a5f07674ac64c08c20e7549558dfc0e7603a4e","html_url":"https://github.com/y0-causal-inference/y0/commit/d8a5f07674ac64c08c20e7549558dfc0e7603a4e"}]},{"sha":"a3f7a4c2f06fe6017643e5a8b909fd5b7b7c2e8a","node_id":"C_kwDOE5hB_NoAKGEzZjdhNGMyZjA2ZmU2MDE3NjQzZTVhOGI5MDlmZDViN2I3YzJlOGE","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-01T01:37:41Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-01T01:37:41Z"},"message":"wrote + test that hits line 10","tree":{"sha":"51c3a5a39e52069bf71dc09c72bae674985a46f9","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/51c3a5a39e52069bf71dc09c72bae674985a46f9"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/a3f7a4c2f06fe6017643e5a8b909fd5b7b7c2e8a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/a3f7a4c2f06fe6017643e5a8b909fd5b7b7c2e8a","html_url":"https://github.com/y0-causal-inference/y0/commit/a3f7a4c2f06fe6017643e5a8b909fd5b7b7c2e8a","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/a3f7a4c2f06fe6017643e5a8b909fd5b7b7c2e8a/comments","author":null,"committer":null,"parents":[{"sha":"3b4c535b4a289b17227ff2301727637ac01f6e68","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3b4c535b4a289b17227ff2301727637ac01f6e68","html_url":"https://github.com/y0-causal-inference/y0/commit/3b4c535b4a289b17227ff2301727637ac01f6e68"}]},{"sha":"d978b74308c2462facfec76d3e6d826bd33aaaa2","node_id":"C_kwDOE5hB_NoAKGQ5NzhiNzQzMDhjMjQ2MmZhY2ZlYzc2ZDNlNmQ4MjZiZDMzYWFhYTI","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-09-01T08:01:05Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-09-01T08:01:05Z"},"message":"Clean + up for mypy and small renames","tree":{"sha":"fab42c41b1cc701eb94803488807ff587ed7deb6","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/fab42c41b1cc701eb94803488807ff587ed7deb6"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/d978b74308c2462facfec76d3e6d826bd33aaaa2","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/d978b74308c2462facfec76d3e6d826bd33aaaa2","html_url":"https://github.com/y0-causal-inference/y0/commit/d978b74308c2462facfec76d3e6d826bd33aaaa2","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/d978b74308c2462facfec76d3e6d826bd33aaaa2/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"a3f7a4c2f06fe6017643e5a8b909fd5b7b7c2e8a","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/a3f7a4c2f06fe6017643e5a8b909fd5b7b7c2e8a","html_url":"https://github.com/y0-causal-inference/y0/commit/a3f7a4c2f06fe6017643e5a8b909fd5b7b7c2e8a"}]},{"sha":"2b45066ce73463bf217569a84b8e0a93bf0db886","node_id":"C_kwDOE5hB_NoAKDJiNDUwNjZjZTczNDYzYmYyMTc1NjlhODRiOGUwYTkzYmYwZGI4ODY","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-01T16:25:10Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-01T16:25:10Z"},"message":"small + fixes to tests","tree":{"sha":"0e01efac65ca54d6cf9acc3ecc33969e16eea775","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/0e01efac65ca54d6cf9acc3ecc33969e16eea775"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/2b45066ce73463bf217569a84b8e0a93bf0db886","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/2b45066ce73463bf217569a84b8e0a93bf0db886","html_url":"https://github.com/y0-causal-inference/y0/commit/2b45066ce73463bf217569a84b8e0a93bf0db886","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/2b45066ce73463bf217569a84b8e0a93bf0db886/comments","author":null,"committer":null,"parents":[{"sha":"d978b74308c2462facfec76d3e6d826bd33aaaa2","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/d978b74308c2462facfec76d3e6d826bd33aaaa2","html_url":"https://github.com/y0-causal-inference/y0/commit/d978b74308c2462facfec76d3e6d826bd33aaaa2"}]},{"sha":"727f1334489d326565f5864e5083d5fe7c9e7f12","node_id":"C_kwDOE5hB_NoAKDcyN2YxMzM0NDg5ZDMyNjU2NWY1ODY0ZTUwODNkNWZlN2M5ZTdmMTI","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-01T17:34:55Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-01T17:34:55Z"},"message":"added + checks to input of transport","tree":{"sha":"06b8d58aefce285d3b69a4237366957c7c02ce1c","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/06b8d58aefce285d3b69a4237366957c7c02ce1c"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/727f1334489d326565f5864e5083d5fe7c9e7f12","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/727f1334489d326565f5864e5083d5fe7c9e7f12","html_url":"https://github.com/y0-causal-inference/y0/commit/727f1334489d326565f5864e5083d5fe7c9e7f12","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/727f1334489d326565f5864e5083d5fe7c9e7f12/comments","author":null,"committer":null,"parents":[{"sha":"2b45066ce73463bf217569a84b8e0a93bf0db886","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/2b45066ce73463bf217569a84b8e0a93bf0db886","html_url":"https://github.com/y0-causal-inference/y0/commit/2b45066ce73463bf217569a84b8e0a93bf0db886"}]},{"sha":"450297d3e36ec3c927450f1ba0ba6e6157f1046f","node_id":"C_kwDOE5hB_NoAKDQ1MDI5N2QzZTM2ZWMzYzkyNzQ1MGYxYmEwYmE2ZTYxNTdmMTA0NmY","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-01T18:17:40Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-01T18:17:40Z"},"message":"modified + several trso tests to be transport tests","tree":{"sha":"cdc49904844d9d7d1b8e2c5aa605f7afde67aaec","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/cdc49904844d9d7d1b8e2c5aa605f7afde67aaec"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/450297d3e36ec3c927450f1ba0ba6e6157f1046f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/450297d3e36ec3c927450f1ba0ba6e6157f1046f","html_url":"https://github.com/y0-causal-inference/y0/commit/450297d3e36ec3c927450f1ba0ba6e6157f1046f","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/450297d3e36ec3c927450f1ba0ba6e6157f1046f/comments","author":null,"committer":null,"parents":[{"sha":"727f1334489d326565f5864e5083d5fe7c9e7f12","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/727f1334489d326565f5864e5083d5fe7c9e7f12","html_url":"https://github.com/y0-causal-inference/y0/commit/727f1334489d326565f5864e5083d5fe7c9e7f12"}]},{"sha":"709122c75db756f16fa18db425a6ffa280c15ee7","node_id":"C_kwDOE5hB_NoAKDcwOTEyMmM3NWRiNzU2ZjE2ZmExOGRiNDI1YTZmZmEyODBjMTVlZTc","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-01T18:37:17Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-01T18:37:17Z"},"message":"added + comments","tree":{"sha":"6343810fc9a1fff1f770fad0dbc0e884e9398602","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/6343810fc9a1fff1f770fad0dbc0e884e9398602"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/709122c75db756f16fa18db425a6ffa280c15ee7","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/709122c75db756f16fa18db425a6ffa280c15ee7","html_url":"https://github.com/y0-causal-inference/y0/commit/709122c75db756f16fa18db425a6ffa280c15ee7","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/709122c75db756f16fa18db425a6ffa280c15ee7/comments","author":null,"committer":null,"parents":[{"sha":"450297d3e36ec3c927450f1ba0ba6e6157f1046f","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/450297d3e36ec3c927450f1ba0ba6e6157f1046f","html_url":"https://github.com/y0-causal-inference/y0/commit/450297d3e36ec3c927450f1ba0ba6e6157f1046f"}]},{"sha":"f35c7cc756b88031612a33fe036a5c30753845f7","node_id":"C_kwDOE5hB_NoAKGYzNWM3Y2M3NTZiODgwMzE2MTJhMzNmZTAzNmE1YzMwNzUzODQ1Zjc","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-01T18:59:42Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-01T18:59:42Z"},"message":"Added + condition for surrogate_interventions to be empty, converted a trso test to + transport test, 94% coverage","tree":{"sha":"fd21b40053b32922eaab2f6bdcfee958b90ecba0","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/fd21b40053b32922eaab2f6bdcfee958b90ecba0"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/f35c7cc756b88031612a33fe036a5c30753845f7","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/f35c7cc756b88031612a33fe036a5c30753845f7","html_url":"https://github.com/y0-causal-inference/y0/commit/f35c7cc756b88031612a33fe036a5c30753845f7","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/f35c7cc756b88031612a33fe036a5c30753845f7/comments","author":null,"committer":null,"parents":[{"sha":"709122c75db756f16fa18db425a6ffa280c15ee7","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/709122c75db756f16fa18db425a6ffa280c15ee7","html_url":"https://github.com/y0-causal-inference/y0/commit/709122c75db756f16fa18db425a6ffa280c15ee7"}]},{"sha":"b3ba608b32af2cdee87faffbcd5240791fa141c0","node_id":"C_kwDOE5hB_NoAKGIzYmE2MDhiMzJhZjJjZGVlODdmYWZmYmNkNTI0MDc5MWZhMTQxYzA","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-02T00:13:17Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-09-02T00:13:17Z"},"message":"Added + more tests for coverage, cleanup needed","tree":{"sha":"4b4fc768c5f353a134eb16455ebb3d2fed5b4e0f","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/4b4fc768c5f353a134eb16455ebb3d2fed5b4e0f"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/b3ba608b32af2cdee87faffbcd5240791fa141c0","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/b3ba608b32af2cdee87faffbcd5240791fa141c0","html_url":"https://github.com/y0-causal-inference/y0/commit/b3ba608b32af2cdee87faffbcd5240791fa141c0","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/b3ba608b32af2cdee87faffbcd5240791fa141c0/comments","author":null,"committer":null,"parents":[{"sha":"f35c7cc756b88031612a33fe036a5c30753845f7","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/f35c7cc756b88031612a33fe036a5c30753845f7","html_url":"https://github.com/y0-causal-inference/y0/commit/f35c7cc756b88031612a33fe036a5c30753845f7"}]},{"sha":"28aea396a803bf13c8fb4d190475d98bbe32c871","node_id":"C_kwDOE5hB_NoAKDI4YWVhMzk2YTgwM2JmMTNjOGZiNGQxOTA0NzVkOThiYmUzMmM4NzE","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-09-02T17:57:36Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-09-02T17:57:36Z"},"message":"Cleanup","tree":{"sha":"1ff056262d8aa00953f5e6e3a5aa4d86eae4f71b","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/1ff056262d8aa00953f5e6e3a5aa4d86eae4f71b"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/28aea396a803bf13c8fb4d190475d98bbe32c871","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/28aea396a803bf13c8fb4d190475d98bbe32c871","html_url":"https://github.com/y0-causal-inference/y0/commit/28aea396a803bf13c8fb4d190475d98bbe32c871","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/28aea396a803bf13c8fb4d190475d98bbe32c871/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"b3ba608b32af2cdee87faffbcd5240791fa141c0","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/b3ba608b32af2cdee87faffbcd5240791fa141c0","html_url":"https://github.com/y0-causal-inference/y0/commit/b3ba608b32af2cdee87faffbcd5240791fa141c0"}]},{"sha":"e33dfe9fc422586b9d05fcd90251069377d14377","node_id":"C_kwDOE5hB_NoAKGUzM2RmZTlmYzQyMjU4NmI5ZDA1ZmNkOTAyNTEwNjkzNzdkMTQzNzc","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-09-02T18:13:17Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-09-02T18:13:17Z"},"message":"Clean + up conditionals","tree":{"sha":"49b6f8fc28e1c9aa6a6d17cae05f5ae2eb5e48c9","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/49b6f8fc28e1c9aa6a6d17cae05f5ae2eb5e48c9"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/e33dfe9fc422586b9d05fcd90251069377d14377","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/e33dfe9fc422586b9d05fcd90251069377d14377","html_url":"https://github.com/y0-causal-inference/y0/commit/e33dfe9fc422586b9d05fcd90251069377d14377","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/e33dfe9fc422586b9d05fcd90251069377d14377/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"28aea396a803bf13c8fb4d190475d98bbe32c871","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/28aea396a803bf13c8fb4d190475d98bbe32c871","html_url":"https://github.com/y0-causal-inference/y0/commit/28aea396a803bf13c8fb4d190475d98bbe32c871"}]},{"sha":"813f65459d5e168a693c40cd84b0c94ca38c239b","node_id":"C_kwDOE5hB_NoAKDgxM2Y2NTQ1OWQ1ZTE2OGE2OTNjNDBjZDg0YjBjOTRjYTM4YzIzOWI","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-09-04T13:58:26Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-09-04T13:58:26Z"},"message":"Merge + branch ''main'' into transport","tree":{"sha":"deda7b2ce81bf178960007dbaa0030558f282bea","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/deda7b2ce81bf178960007dbaa0030558f282bea"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/813f65459d5e168a693c40cd84b0c94ca38c239b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/813f65459d5e168a693c40cd84b0c94ca38c239b","html_url":"https://github.com/y0-causal-inference/y0/commit/813f65459d5e168a693c40cd84b0c94ca38c239b","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/813f65459d5e168a693c40cd84b0c94ca38c239b/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"e33dfe9fc422586b9d05fcd90251069377d14377","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/e33dfe9fc422586b9d05fcd90251069377d14377","html_url":"https://github.com/y0-causal-inference/y0/commit/e33dfe9fc422586b9d05fcd90251069377d14377"},{"sha":"32d2cfc93b0ae431e01ee355235321e977493e65","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/32d2cfc93b0ae431e01ee355235321e977493e65","html_url":"https://github.com/y0-causal-inference/y0/commit/32d2cfc93b0ae431e01ee355235321e977493e65"}]},{"sha":"fb0a8e4fbdcd1c805ad4d3bac68aa1df5d69b290","node_id":"C_kwDOE5hB_NoAKGZiMGE4ZTRmYmRjZDFjODA1YWQ0ZDNiYWM2OGFhMWRmNWQ2OWIyOTA","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-09-04T13:58:58Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-09-04T13:58:58Z"},"message":"Update + canonicalize_expr.py","tree":{"sha":"0535f4727a225f151c1e51b0e7c183f3130fc628","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/0535f4727a225f151c1e51b0e7c183f3130fc628"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/fb0a8e4fbdcd1c805ad4d3bac68aa1df5d69b290","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/fb0a8e4fbdcd1c805ad4d3bac68aa1df5d69b290","html_url":"https://github.com/y0-causal-inference/y0/commit/fb0a8e4fbdcd1c805ad4d3bac68aa1df5d69b290","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/fb0a8e4fbdcd1c805ad4d3bac68aa1df5d69b290/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"813f65459d5e168a693c40cd84b0c94ca38c239b","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/813f65459d5e168a693c40cd84b0c94ca38c239b","html_url":"https://github.com/y0-causal-inference/y0/commit/813f65459d5e168a693c40cd84b0c94ca38c239b"}]},{"sha":"8549bb69c05c2138b7f9fe1a6b57b78621e6dab7","node_id":"C_kwDOE5hB_NoAKDg1NDliYjY5YzA1YzIxMzhiN2Y5ZmUxYTZiNTdiNzg2MjFlNmRhYjc","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-09-04T16:44:22Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-09-04T16:44:22Z"},"message":"Merge + branch ''main'' into transport","tree":{"sha":"bdb3938b11440c3b3d85ef4411acac4ebd12dd7b","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/bdb3938b11440c3b3d85ef4411acac4ebd12dd7b"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/8549bb69c05c2138b7f9fe1a6b57b78621e6dab7","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/8549bb69c05c2138b7f9fe1a6b57b78621e6dab7","html_url":"https://github.com/y0-causal-inference/y0/commit/8549bb69c05c2138b7f9fe1a6b57b78621e6dab7","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/8549bb69c05c2138b7f9fe1a6b57b78621e6dab7/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"fb0a8e4fbdcd1c805ad4d3bac68aa1df5d69b290","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/fb0a8e4fbdcd1c805ad4d3bac68aa1df5d69b290","html_url":"https://github.com/y0-causal-inference/y0/commit/fb0a8e4fbdcd1c805ad4d3bac68aa1df5d69b290"},{"sha":"ba48ae42284bd01e8fc5b2d4e8733070e4858b13","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ba48ae42284bd01e8fc5b2d4e8733070e4858b13","html_url":"https://github.com/y0-causal-inference/y0/commit/ba48ae42284bd01e8fc5b2d4e8733070e4858b13"}]},{"sha":"7121f9699f0012cbba18b62f755adbb6df6e0206","node_id":"C_kwDOE5hB_NoAKDcxMjFmOTY5OWYwMDEyY2JiYTE4YjYyZjc1NWFkYmI2ZGY2ZTAyMDY","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-09-04T16:51:11Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-09-04T16:51:11Z"},"message":"Merge + branch ''main'' into transport","tree":{"sha":"eae5f613b8b5f8d65c69e809ef415cea4b295bca","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/eae5f613b8b5f8d65c69e809ef415cea4b295bca"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/7121f9699f0012cbba18b62f755adbb6df6e0206","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7121f9699f0012cbba18b62f755adbb6df6e0206","html_url":"https://github.com/y0-causal-inference/y0/commit/7121f9699f0012cbba18b62f755adbb6df6e0206","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7121f9699f0012cbba18b62f755adbb6df6e0206/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"8549bb69c05c2138b7f9fe1a6b57b78621e6dab7","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/8549bb69c05c2138b7f9fe1a6b57b78621e6dab7","html_url":"https://github.com/y0-causal-inference/y0/commit/8549bb69c05c2138b7f9fe1a6b57b78621e6dab7"},{"sha":"73b98d42a8eb3c5d4631889d2e7e5ce092254518","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/73b98d42a8eb3c5d4631889d2e7e5ce092254518","html_url":"https://github.com/y0-causal-inference/y0/commit/73b98d42a8eb3c5d4631889d2e7e5ce092254518"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:19:00 GMT + ETag: + - W/"3e7b848e6f564d53c01e41b6a3bb653fa94b4e574ca78af747b11346f80c4e47" + Last-Modified: + - Mon, 04 Sep 2023 16:56:11 GMT + Link: + - ; + rel="prev", ; + rel="first" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FFB6:6DFA:126559:133A09:64F61184 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4982' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '18' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/y0-causal-inference/y0/pulls/149/commits?page=1&per_page=100 + response: + content: '[{"sha":"ae67d2cfc120e09dcca1bb9dfd6c785198ae77d8","node_id":"C_kwDOE5hB_NoAKGFlNjdkMmNmYzEyMGUwOWRjY2ExYmI5ZGZkNmM3ODUxOThhZTc3ZDg","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-07-20T16:11:38Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-07-20T16:11:38Z"},"message":"Extend + DSL for surrogate endpoints and transportability","tree":{"sha":"21ce23fd81f4527a62d1bce6f86be2bff71dc70f","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/21ce23fd81f4527a62d1bce6f86be2bff71dc70f"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/ae67d2cfc120e09dcca1bb9dfd6c785198ae77d8","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ae67d2cfc120e09dcca1bb9dfd6c785198ae77d8","html_url":"https://github.com/y0-causal-inference/y0/commit/ae67d2cfc120e09dcca1bb9dfd6c785198ae77d8","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ae67d2cfc120e09dcca1bb9dfd6c785198ae77d8/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"4569c99152d0a218f67144c5a76c3269b3207fd7","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/4569c99152d0a218f67144c5a76c3269b3207fd7","html_url":"https://github.com/y0-causal-inference/y0/commit/4569c99152d0a218f67144c5a76c3269b3207fd7"}]},{"sha":"83df5803f311a168cb2fe04c2b0e3d4b9ca36f5d","node_id":"C_kwDOE5hB_NoAKDgzZGY1ODAzZjMxMWExNjhjYjJmZTA0YzJiMGUzZDRiOWNhMzZmNWQ","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-07-20T18:54:50Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-07-20T18:54:50Z"},"message":"Add + transport","tree":{"sha":"61df1d258fdf6588204f4348e8b0cc6496196590","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/61df1d258fdf6588204f4348e8b0cc6496196590"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/83df5803f311a168cb2fe04c2b0e3d4b9ca36f5d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/83df5803f311a168cb2fe04c2b0e3d4b9ca36f5d","html_url":"https://github.com/y0-causal-inference/y0/commit/83df5803f311a168cb2fe04c2b0e3d4b9ca36f5d","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/83df5803f311a168cb2fe04c2b0e3d4b9ca36f5d/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"ae67d2cfc120e09dcca1bb9dfd6c785198ae77d8","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ae67d2cfc120e09dcca1bb9dfd6c785198ae77d8","html_url":"https://github.com/y0-causal-inference/y0/commit/ae67d2cfc120e09dcca1bb9dfd6c785198ae77d8"}]},{"sha":"27a380ac239ab55d12dd8d7de6318bf7523e93bf","node_id":"C_kwDOE5hB_NoAKDI3YTM4MGFjMjM5YWI1NWQxMmRkOGQ3ZGU2MzE4YmY3NTIzZTkzYmY","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-07-26T15:58:50Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-07-26T15:58:50Z"},"message":"Add + tests","tree":{"sha":"c910f510962d14a1594010103e5923f566052007","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/c910f510962d14a1594010103e5923f566052007"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/27a380ac239ab55d12dd8d7de6318bf7523e93bf","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/27a380ac239ab55d12dd8d7de6318bf7523e93bf","html_url":"https://github.com/y0-causal-inference/y0/commit/27a380ac239ab55d12dd8d7de6318bf7523e93bf","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/27a380ac239ab55d12dd8d7de6318bf7523e93bf/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"83df5803f311a168cb2fe04c2b0e3d4b9ca36f5d","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/83df5803f311a168cb2fe04c2b0e3d4b9ca36f5d","html_url":"https://github.com/y0-causal-inference/y0/commit/83df5803f311a168cb2fe04c2b0e3d4b9ca36f5d"}]},{"sha":"579b83fe8909e2511f00801936ccfc74ed734bc9","node_id":"C_kwDOE5hB_NoAKDU3OWI4M2ZlODkwOWUyNTExZjAwODAxOTM2Y2NmYzc0ZWQ3MzRiYzk","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-07-26T16:56:25Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-07-26T16:56:25Z"},"message":"Update + test_transport.py","tree":{"sha":"da8e5e0edfaff106cfd52d0bd0d8073e288eb6e8","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/da8e5e0edfaff106cfd52d0bd0d8073e288eb6e8"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/579b83fe8909e2511f00801936ccfc74ed734bc9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/579b83fe8909e2511f00801936ccfc74ed734bc9","html_url":"https://github.com/y0-causal-inference/y0/commit/579b83fe8909e2511f00801936ccfc74ed734bc9","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/579b83fe8909e2511f00801936ccfc74ed734bc9/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"27a380ac239ab55d12dd8d7de6318bf7523e93bf","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/27a380ac239ab55d12dd8d7de6318bf7523e93bf","html_url":"https://github.com/y0-causal-inference/y0/commit/27a380ac239ab55d12dd8d7de6318bf7523e93bf"}]},{"sha":"9e70a8fff783d5203c128503ab61da73ee7085d1","node_id":"C_kwDOE5hB_NoAKDllNzBhOGZmZjc4M2Q1MjAzYzEyODUwM2FiNjFkYTczZWU3MDg1ZDE","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-07-28T20:36:57Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-07-28T20:36:57Z"},"message":"Added + find_transport_vertices to transport.py and test_transport.py","tree":{"sha":"bd5f49819003745dab7a974f3515c2330fabd45f","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/bd5f49819003745dab7a974f3515c2330fabd45f"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/9e70a8fff783d5203c128503ab61da73ee7085d1","comment_count":4,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/9e70a8fff783d5203c128503ab61da73ee7085d1","html_url":"https://github.com/y0-causal-inference/y0/commit/9e70a8fff783d5203c128503ab61da73ee7085d1","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/9e70a8fff783d5203c128503ab61da73ee7085d1/comments","author":null,"committer":null,"parents":[{"sha":"579b83fe8909e2511f00801936ccfc74ed734bc9","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/579b83fe8909e2511f00801936ccfc74ed734bc9","html_url":"https://github.com/y0-causal-inference/y0/commit/579b83fe8909e2511f00801936ccfc74ed734bc9"}]},{"sha":"ca6aa305d71279cfb244ac4695d84a24606382dd","node_id":"C_kwDOE5hB_NoAKGNhNmFhMzA1ZDcxMjc5Y2ZiMjQ0YWM0Njk1ZDg0YTI0NjA2MzgyZGQ","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-07-28T21:03:37Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-07-28T21:03:37Z"},"message":"Edited + transport.py functions to allow lists instead of single variables","tree":{"sha":"70326918ee8449daebe7de73943e9877bf3fe204","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/70326918ee8449daebe7de73943e9877bf3fe204"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/ca6aa305d71279cfb244ac4695d84a24606382dd","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ca6aa305d71279cfb244ac4695d84a24606382dd","html_url":"https://github.com/y0-causal-inference/y0/commit/ca6aa305d71279cfb244ac4695d84a24606382dd","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ca6aa305d71279cfb244ac4695d84a24606382dd/comments","author":null,"committer":null,"parents":[{"sha":"9e70a8fff783d5203c128503ab61da73ee7085d1","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/9e70a8fff783d5203c128503ab61da73ee7085d1","html_url":"https://github.com/y0-causal-inference/y0/commit/9e70a8fff783d5203c128503ab61da73ee7085d1"}]},{"sha":"8fde61299b545ecbfa80a13db46f1707a29e1b54","node_id":"C_kwDOE5hB_NoAKDhmZGU2MTI5OWI1NDVlY2JmYTgwYTEzZGI0NmYxNzA3YTI5ZTFiNTQ","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-07-31T18:06:36Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-07-31T18:06:36Z"},"message":"Started + writting out trso, untested and still messy","tree":{"sha":"8c687e08a1d29a7755d56c715a7633e63b33add9","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/8c687e08a1d29a7755d56c715a7633e63b33add9"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/8fde61299b545ecbfa80a13db46f1707a29e1b54","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/8fde61299b545ecbfa80a13db46f1707a29e1b54","html_url":"https://github.com/y0-causal-inference/y0/commit/8fde61299b545ecbfa80a13db46f1707a29e1b54","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/8fde61299b545ecbfa80a13db46f1707a29e1b54/comments","author":null,"committer":null,"parents":[{"sha":"ca6aa305d71279cfb244ac4695d84a24606382dd","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ca6aa305d71279cfb244ac4695d84a24606382dd","html_url":"https://github.com/y0-causal-inference/y0/commit/ca6aa305d71279cfb244ac4695d84a24606382dd"}]},{"sha":"c4d38b39ead09ae46739536421b2f59756b46397","node_id":"C_kwDOE5hB_NoAKGM0ZDM4YjM5ZWFkMDlhZTQ2NzM5NTM2NDIxYjJmNTk3NTZiNDYzOTc","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-07-31T18:27:33Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-07-31T18:27:33Z"},"message":"Updated + variable names","tree":{"sha":"baaeb9756827179852ee96548f397ed87d40a261","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/baaeb9756827179852ee96548f397ed87d40a261"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/c4d38b39ead09ae46739536421b2f59756b46397","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/c4d38b39ead09ae46739536421b2f59756b46397","html_url":"https://github.com/y0-causal-inference/y0/commit/c4d38b39ead09ae46739536421b2f59756b46397","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/c4d38b39ead09ae46739536421b2f59756b46397/comments","author":null,"committer":null,"parents":[{"sha":"8fde61299b545ecbfa80a13db46f1707a29e1b54","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/8fde61299b545ecbfa80a13db46f1707a29e1b54","html_url":"https://github.com/y0-causal-inference/y0/commit/8fde61299b545ecbfa80a13db46f1707a29e1b54"}]},{"sha":"01a723aaa844d076f92a24ea9b56efb395fc2dc3","node_id":"C_kwDOE5hB_NoAKDAxYTcyM2FhYTg0NGQwNzZmOTJhMjRlYTliNTZlZmIzOTVmYzJkYzM","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-07-31T18:32:36Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-07-31T18:32:36Z"},"message":"Ran + tox lint","tree":{"sha":"9b4a8705107a2134747effe0f553b439992803b1","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/9b4a8705107a2134747effe0f553b439992803b1"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/01a723aaa844d076f92a24ea9b56efb395fc2dc3","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/01a723aaa844d076f92a24ea9b56efb395fc2dc3","html_url":"https://github.com/y0-causal-inference/y0/commit/01a723aaa844d076f92a24ea9b56efb395fc2dc3","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/01a723aaa844d076f92a24ea9b56efb395fc2dc3/comments","author":null,"committer":null,"parents":[{"sha":"c4d38b39ead09ae46739536421b2f59756b46397","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/c4d38b39ead09ae46739536421b2f59756b46397","html_url":"https://github.com/y0-causal-inference/y0/commit/c4d38b39ead09ae46739536421b2f59756b46397"}]},{"sha":"ce1648ca11243e771a1039bd2620898a525621aa","node_id":"C_kwDOE5hB_NoAKGNlMTY0OGNhMTEyNDNlNzcxYTEwMzliZDI2MjA4OThhNTI1NjIxYWE","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-07-31T19:13:30Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-07-31T19:13:30Z"},"message":"Added + tests for simple trso cases, they still fail","tree":{"sha":"d7337e008400ed09737de95362f5a2f8c5508092","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/d7337e008400ed09737de95362f5a2f8c5508092"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/ce1648ca11243e771a1039bd2620898a525621aa","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ce1648ca11243e771a1039bd2620898a525621aa","html_url":"https://github.com/y0-causal-inference/y0/commit/ce1648ca11243e771a1039bd2620898a525621aa","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ce1648ca11243e771a1039bd2620898a525621aa/comments","author":null,"committer":null,"parents":[{"sha":"01a723aaa844d076f92a24ea9b56efb395fc2dc3","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/01a723aaa844d076f92a24ea9b56efb395fc2dc3","html_url":"https://github.com/y0-causal-inference/y0/commit/01a723aaa844d076f92a24ea9b56efb395fc2dc3"}]},{"sha":"de73438a1cfeaa3e6ca2b49321255873587161c6","node_id":"C_kwDOE5hB_NoAKGRlNzM0MzhhMWNmZWFhM2U2Y2EyYjQ5MzIxMjU1ODczNTg3MTYxYzY","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-07-31T19:13:53Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-07-31T19:13:53Z"},"message":"Added + tests for simple trso cases, they still fail","tree":{"sha":"1c311a6126d930c503f2d8d3d12bd341dfdc79f5","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/1c311a6126d930c503f2d8d3d12bd341dfdc79f5"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/de73438a1cfeaa3e6ca2b49321255873587161c6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/de73438a1cfeaa3e6ca2b49321255873587161c6","html_url":"https://github.com/y0-causal-inference/y0/commit/de73438a1cfeaa3e6ca2b49321255873587161c6","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/de73438a1cfeaa3e6ca2b49321255873587161c6/comments","author":null,"committer":null,"parents":[{"sha":"ce1648ca11243e771a1039bd2620898a525621aa","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ce1648ca11243e771a1039bd2620898a525621aa","html_url":"https://github.com/y0-causal-inference/y0/commit/ce1648ca11243e771a1039bd2620898a525621aa"}]},{"sha":"11d561e9d5beba3df7a77f06afbe6ce67d64e777","node_id":"C_kwDOE5hB_NoAKDExZDU2MWU5ZDViZWJhM2RmN2E3N2YwNmFmYmU2Y2U2N2Q2NGU3Nzc","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-01T22:56:34Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-01T22:56:34Z"},"message":"Added + line 8, split into functions","tree":{"sha":"9e8bbdfa31231e300aad6a4ecee7697fbbd667fe","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/9e8bbdfa31231e300aad6a4ecee7697fbbd667fe"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/11d561e9d5beba3df7a77f06afbe6ce67d64e777","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/11d561e9d5beba3df7a77f06afbe6ce67d64e777","html_url":"https://github.com/y0-causal-inference/y0/commit/11d561e9d5beba3df7a77f06afbe6ce67d64e777","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/11d561e9d5beba3df7a77f06afbe6ce67d64e777/comments","author":null,"committer":null,"parents":[{"sha":"de73438a1cfeaa3e6ca2b49321255873587161c6","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/de73438a1cfeaa3e6ca2b49321255873587161c6","html_url":"https://github.com/y0-causal-inference/y0/commit/de73438a1cfeaa3e6ca2b49321255873587161c6"}]},{"sha":"faa76d534cc9c4c48d063d5e250689afcc9db888","node_id":"C_kwDOE5hB_NoAKGZhYTc2ZDUzNGNjOWM0YzQ4ZDA2M2Q1ZTI1MDY4OWFmY2M5ZGI4ODg","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-01T23:53:16Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-01T23:53:16Z"},"message":"Added + unit tests, 4 passing tests","tree":{"sha":"03ae7f66eb63e7571386754404b0aebb3393c391","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/03ae7f66eb63e7571386754404b0aebb3393c391"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/faa76d534cc9c4c48d063d5e250689afcc9db888","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/faa76d534cc9c4c48d063d5e250689afcc9db888","html_url":"https://github.com/y0-causal-inference/y0/commit/faa76d534cc9c4c48d063d5e250689afcc9db888","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/faa76d534cc9c4c48d063d5e250689afcc9db888/comments","author":null,"committer":null,"parents":[{"sha":"11d561e9d5beba3df7a77f06afbe6ce67d64e777","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/11d561e9d5beba3df7a77f06afbe6ce67d64e777","html_url":"https://github.com/y0-causal-inference/y0/commit/11d561e9d5beba3df7a77f06afbe6ce67d64e777"}]},{"sha":"5011d4d1b67f3479d46c3016a276846a313f134d","node_id":"C_kwDOE5hB_NoAKDUwMTFkNGQxYjY3ZjM0NzlkNDZjMzAxNmEyNzY4NDZhMzEzZjEzNGQ","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-02T15:44:13Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-02T15:44:13Z"},"message":"Test + case for trso_line4 passes","tree":{"sha":"899d0574eca1a89c5090cabbb8e8eec267d5c5ee","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/899d0574eca1a89c5090cabbb8e8eec267d5c5ee"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/5011d4d1b67f3479d46c3016a276846a313f134d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/5011d4d1b67f3479d46c3016a276846a313f134d","html_url":"https://github.com/y0-causal-inference/y0/commit/5011d4d1b67f3479d46c3016a276846a313f134d","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/5011d4d1b67f3479d46c3016a276846a313f134d/comments","author":null,"committer":null,"parents":[{"sha":"faa76d534cc9c4c48d063d5e250689afcc9db888","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/faa76d534cc9c4c48d063d5e250689afcc9db888","html_url":"https://github.com/y0-causal-inference/y0/commit/faa76d534cc9c4c48d063d5e250689afcc9db888"}]},{"sha":"46e1ce8750b33f04bd466fd5fdedb1dda846f2e8","node_id":"C_kwDOE5hB_NoAKDQ2ZTFjZTg3NTBiMzNmMDRiZDQ2NmZkNWZkZWRiMWRkYTg0NmYyZTg","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-02T16:16:19Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-02T16:16:19Z"},"message":"Added + test for surrogate_to_transport, doesn''t work due to Transport Creation","tree":{"sha":"af5136fd941e079bdb8073649c5cffffff43c614","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/af5136fd941e079bdb8073649c5cffffff43c614"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/46e1ce8750b33f04bd466fd5fdedb1dda846f2e8","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/46e1ce8750b33f04bd466fd5fdedb1dda846f2e8","html_url":"https://github.com/y0-causal-inference/y0/commit/46e1ce8750b33f04bd466fd5fdedb1dda846f2e8","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/46e1ce8750b33f04bd466fd5fdedb1dda846f2e8/comments","author":null,"committer":null,"parents":[{"sha":"5011d4d1b67f3479d46c3016a276846a313f134d","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/5011d4d1b67f3479d46c3016a276846a313f134d","html_url":"https://github.com/y0-causal-inference/y0/commit/5011d4d1b67f3479d46c3016a276846a313f134d"}]},{"sha":"7c4205891e684b9124939200a7875ea7bb6107a9","node_id":"C_kwDOE5hB_NoAKDdjNDIwNTg5MWU2ODRiOTEyNDkzOTIwMGE3ODc1ZWE3YmI2MTA3YTk","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-02T20:55:40Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-02T20:55:40Z"},"message":"Test + for surrogate_to_transport now passing, using Variable instead of Transport + for transport nodes","tree":{"sha":"2cfa8bd7859c1e6bccd5858b8616819220e64236","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/2cfa8bd7859c1e6bccd5858b8616819220e64236"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/7c4205891e684b9124939200a7875ea7bb6107a9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7c4205891e684b9124939200a7875ea7bb6107a9","html_url":"https://github.com/y0-causal-inference/y0/commit/7c4205891e684b9124939200a7875ea7bb6107a9","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7c4205891e684b9124939200a7875ea7bb6107a9/comments","author":null,"committer":null,"parents":[{"sha":"46e1ce8750b33f04bd466fd5fdedb1dda846f2e8","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/46e1ce8750b33f04bd466fd5fdedb1dda846f2e8","html_url":"https://github.com/y0-causal-inference/y0/commit/46e1ce8750b33f04bd466fd5fdedb1dda846f2e8"}]},{"sha":"8464533686f3a3e33709a6a701715ec2ac56217e","node_id":"C_kwDOE5hB_NoAKDg0NjQ1MzM2ODZmM2EzZTMzNzA5YTZhNzAxNzE1ZWMyYWM1NjIxN2U","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-02T21:26:19Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-02T21:26:19Z"},"message":"Added + docstrings for functions, noted inconsistencies with TODO","tree":{"sha":"7c889d57c5f05f12bb5537fca88c2f97e486805b","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/7c889d57c5f05f12bb5537fca88c2f97e486805b"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/8464533686f3a3e33709a6a701715ec2ac56217e","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/8464533686f3a3e33709a6a701715ec2ac56217e","html_url":"https://github.com/y0-causal-inference/y0/commit/8464533686f3a3e33709a6a701715ec2ac56217e","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/8464533686f3a3e33709a6a701715ec2ac56217e/comments","author":null,"committer":null,"parents":[{"sha":"7c4205891e684b9124939200a7875ea7bb6107a9","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7c4205891e684b9124939200a7875ea7bb6107a9","html_url":"https://github.com/y0-causal-inference/y0/commit/7c4205891e684b9124939200a7875ea7bb6107a9"}]},{"sha":"ddf57d4dbe4261d05d8a853109be99ca21c6f6ff","node_id":"C_kwDOE5hB_NoAKGRkZjU3ZDRkYmU0MjYxZDA1ZDhhODUzMTA5YmU5OWNhMjFjNmY2ZmY","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-05T16:24:35Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-05T16:24:35Z"},"message":"Small + cleanup\n\nThere''s a lot of stuff that doesn''t make sense - need to run tox + -e flake8 and tox -e mypy a lot more often to see issues coming up\n\nAlso fixed + incorrect usage of PP","tree":{"sha":"d39d3dedf7a05ccdd95a1215d4d7de2e3ed22356","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/d39d3dedf7a05ccdd95a1215d4d7de2e3ed22356"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/ddf57d4dbe4261d05d8a853109be99ca21c6f6ff","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ddf57d4dbe4261d05d8a853109be99ca21c6f6ff","html_url":"https://github.com/y0-causal-inference/y0/commit/ddf57d4dbe4261d05d8a853109be99ca21c6f6ff","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ddf57d4dbe4261d05d8a853109be99ca21c6f6ff/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"8464533686f3a3e33709a6a701715ec2ac56217e","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/8464533686f3a3e33709a6a701715ec2ac56217e","html_url":"https://github.com/y0-causal-inference/y0/commit/8464533686f3a3e33709a6a701715ec2ac56217e"}]},{"sha":"2df5f3cdaca4fe28412e0d3157773c2c2c5c7d96","node_id":"C_kwDOE5hB_NoAKDJkZjVmM2NkYWNhNGZlMjg0MTJlMGQzMTU3NzczYzJjMmM1YzdkOTY","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-05T16:30:02Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-05T16:30:02Z"},"message":"Add + additional fixmes","tree":{"sha":"cb0f4094fd25ce6f13cccee3b57ca91680d184c5","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/cb0f4094fd25ce6f13cccee3b57ca91680d184c5"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/2df5f3cdaca4fe28412e0d3157773c2c2c5c7d96","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/2df5f3cdaca4fe28412e0d3157773c2c2c5c7d96","html_url":"https://github.com/y0-causal-inference/y0/commit/2df5f3cdaca4fe28412e0d3157773c2c2c5c7d96","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/2df5f3cdaca4fe28412e0d3157773c2c2c5c7d96/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"ddf57d4dbe4261d05d8a853109be99ca21c6f6ff","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ddf57d4dbe4261d05d8a853109be99ca21c6f6ff","html_url":"https://github.com/y0-causal-inference/y0/commit/ddf57d4dbe4261d05d8a853109be99ca21c6f6ff"}]},{"sha":"4b456b5455a74a4823a8fcb5366f34c549e318c1","node_id":"C_kwDOE5hB_NoAKDRiNDU2YjU0NTVhNzRhNDgyM2E4ZmNiNTM2NmYzNGM1NDllMzE4YzE","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-08T16:00:17Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-08T16:00:17Z"},"message":"Edits + on surrogate_to_transport. Changing surrogate_interventions to dict","tree":{"sha":"eefedc2dc4e9abc80fbdb308c9dc916d6401beec","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/eefedc2dc4e9abc80fbdb308c9dc916d6401beec"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/4b456b5455a74a4823a8fcb5366f34c549e318c1","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/4b456b5455a74a4823a8fcb5366f34c549e318c1","html_url":"https://github.com/y0-causal-inference/y0/commit/4b456b5455a74a4823a8fcb5366f34c549e318c1","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/4b456b5455a74a4823a8fcb5366f34c549e318c1/comments","author":null,"committer":null,"parents":[{"sha":"2df5f3cdaca4fe28412e0d3157773c2c2c5c7d96","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/2df5f3cdaca4fe28412e0d3157773c2c2c5c7d96","html_url":"https://github.com/y0-causal-inference/y0/commit/2df5f3cdaca4fe28412e0d3157773c2c2c5c7d96"}]},{"sha":"c00907f40a1a0e378fae70fbb377644fb3a049f9","node_id":"C_kwDOE5hB_NoAKGMwMDkwN2Y0MGExYTBlMzc4ZmFlNzBmYmIzNzc2NDRmYjNhMDQ5Zjk","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-08T16:31:44Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-08T16:31:44Z"},"message":"transportability_diagrams + and available_interventions are now dictionaries keyed by domain","tree":{"sha":"243b46496b5b214e1375c0b50b48aaf5573c2fde","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/243b46496b5b214e1375c0b50b48aaf5573c2fde"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/c00907f40a1a0e378fae70fbb377644fb3a049f9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/c00907f40a1a0e378fae70fbb377644fb3a049f9","html_url":"https://github.com/y0-causal-inference/y0/commit/c00907f40a1a0e378fae70fbb377644fb3a049f9","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/c00907f40a1a0e378fae70fbb377644fb3a049f9/comments","author":null,"committer":null,"parents":[{"sha":"4b456b5455a74a4823a8fcb5366f34c549e318c1","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/4b456b5455a74a4823a8fcb5366f34c549e318c1","html_url":"https://github.com/y0-causal-inference/y0/commit/4b456b5455a74a4823a8fcb5366f34c549e318c1"}]},{"sha":"59029e85626f632eccfa237e13f1ffa16061dbd3","node_id":"C_kwDOE5hB_NoAKDU5MDI5ZTg1NjI2ZjYzMmVjY2ZhMjM3ZTEzZjFmZmExNjA2MWRiZDM","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-08T16:35:19Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-08T16:35:19Z"},"message":"modified + trso_line6 to prevent deep indentations","tree":{"sha":"79e167adabb9df8f9a171f42978f84743b62a56e","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/79e167adabb9df8f9a171f42978f84743b62a56e"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/59029e85626f632eccfa237e13f1ffa16061dbd3","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/59029e85626f632eccfa237e13f1ffa16061dbd3","html_url":"https://github.com/y0-causal-inference/y0/commit/59029e85626f632eccfa237e13f1ffa16061dbd3","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/59029e85626f632eccfa237e13f1ffa16061dbd3/comments","author":null,"committer":null,"parents":[{"sha":"c00907f40a1a0e378fae70fbb377644fb3a049f9","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/c00907f40a1a0e378fae70fbb377644fb3a049f9","html_url":"https://github.com/y0-causal-inference/y0/commit/c00907f40a1a0e378fae70fbb377644fb3a049f9"}]},{"sha":"79b8234c04005c52868aa03a1b1b86db307d7171","node_id":"C_kwDOE5hB_NoAKDc5YjgyMzRjMDQwMDVjNTI4NjhhYTAzYTFiMWI4NmRiMzA3ZDcxNzE","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-08T16:55:43Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-08T16:55:43Z"},"message":"some + docstring fixes and cleanup.","tree":{"sha":"df59fdcfd6366104ce05a2f01c905146627c5f42","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/df59fdcfd6366104ce05a2f01c905146627c5f42"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/79b8234c04005c52868aa03a1b1b86db307d7171","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/79b8234c04005c52868aa03a1b1b86db307d7171","html_url":"https://github.com/y0-causal-inference/y0/commit/79b8234c04005c52868aa03a1b1b86db307d7171","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/79b8234c04005c52868aa03a1b1b86db307d7171/comments","author":null,"committer":null,"parents":[{"sha":"59029e85626f632eccfa237e13f1ffa16061dbd3","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/59029e85626f632eccfa237e13f1ffa16061dbd3","html_url":"https://github.com/y0-causal-inference/y0/commit/59029e85626f632eccfa237e13f1ffa16061dbd3"}]},{"sha":"9d3e7de850f675fc9e9ec6b17aeff0dd2cf81daa","node_id":"C_kwDOE5hB_NoAKDlkM2U3ZGU4NTBmNjc1ZmM5ZTllYzZiMTdhZWZmMGRkMmNmODFkYWE","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-09T17:01:52Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-09T17:01:52Z"},"message":"added + class for TransportQuery, simplified code","tree":{"sha":"445c1b49eb064f2b0ba6008f51c3bc259c664e4c","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/445c1b49eb064f2b0ba6008f51c3bc259c664e4c"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/9d3e7de850f675fc9e9ec6b17aeff0dd2cf81daa","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/9d3e7de850f675fc9e9ec6b17aeff0dd2cf81daa","html_url":"https://github.com/y0-causal-inference/y0/commit/9d3e7de850f675fc9e9ec6b17aeff0dd2cf81daa","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/9d3e7de850f675fc9e9ec6b17aeff0dd2cf81daa/comments","author":null,"committer":null,"parents":[{"sha":"79b8234c04005c52868aa03a1b1b86db307d7171","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/79b8234c04005c52868aa03a1b1b86db307d7171","html_url":"https://github.com/y0-causal-inference/y0/commit/79b8234c04005c52868aa03a1b1b86db307d7171"}]},{"sha":"a1e11af8db65976a3631f8e0b6f76e28f3a74763","node_id":"C_kwDOE5hB_NoAKGExZTExYWY4ZGI2NTk3NmEzNjMxZjhlMGI2Zjc2ZTI4ZjNhNzQ3NjM","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-09T17:34:44Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-09T17:34:44Z"},"message":"A + few small fixes, started refactoring tests 2/6 pass","tree":{"sha":"8509b7f95af13285f7c6d615ce9e8310b49d0639","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/8509b7f95af13285f7c6d615ce9e8310b49d0639"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/a1e11af8db65976a3631f8e0b6f76e28f3a74763","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/a1e11af8db65976a3631f8e0b6f76e28f3a74763","html_url":"https://github.com/y0-causal-inference/y0/commit/a1e11af8db65976a3631f8e0b6f76e28f3a74763","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/a1e11af8db65976a3631f8e0b6f76e28f3a74763/comments","author":null,"committer":null,"parents":[{"sha":"9d3e7de850f675fc9e9ec6b17aeff0dd2cf81daa","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/9d3e7de850f675fc9e9ec6b17aeff0dd2cf81daa","html_url":"https://github.com/y0-causal-inference/y0/commit/9d3e7de850f675fc9e9ec6b17aeff0dd2cf81daa"}]},{"sha":"bb90fc5ea995217fb33aa2693f939463130bf8b0","node_id":"C_kwDOE5hB_NoAKGJiOTBmYzVlYTk5NTIxN2ZiMzNhYTI2OTNmOTM5NDYzMTMwYmY4YjA","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-09T21:19:32Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-09T21:19:32Z"},"message":"Cleanup","tree":{"sha":"193c5b37d80b763bb5fd8addd5643fa8873b568d","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/193c5b37d80b763bb5fd8addd5643fa8873b568d"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/bb90fc5ea995217fb33aa2693f939463130bf8b0","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/bb90fc5ea995217fb33aa2693f939463130bf8b0","html_url":"https://github.com/y0-causal-inference/y0/commit/bb90fc5ea995217fb33aa2693f939463130bf8b0","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/bb90fc5ea995217fb33aa2693f939463130bf8b0/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"a1e11af8db65976a3631f8e0b6f76e28f3a74763","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/a1e11af8db65976a3631f8e0b6f76e28f3a74763","html_url":"https://github.com/y0-causal-inference/y0/commit/a1e11af8db65976a3631f8e0b6f76e28f3a74763"}]},{"sha":"fbe09ce7380d97c75145250a8cef4bdc5fcb5e86","node_id":"C_kwDOE5hB_NoAKGZiZTA5Y2U3MzgwZDk3Yzc1MTQ1MjUwYThjZWY0YmRjNWZjYjVlODY","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-09T21:26:35Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-09T21:26:35Z"},"message":"More + cleanup","tree":{"sha":"e2018f9f3b280804fe8489aae215cd567346966e","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/e2018f9f3b280804fe8489aae215cd567346966e"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/fbe09ce7380d97c75145250a8cef4bdc5fcb5e86","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/fbe09ce7380d97c75145250a8cef4bdc5fcb5e86","html_url":"https://github.com/y0-causal-inference/y0/commit/fbe09ce7380d97c75145250a8cef4bdc5fcb5e86","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/fbe09ce7380d97c75145250a8cef4bdc5fcb5e86/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"bb90fc5ea995217fb33aa2693f939463130bf8b0","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/bb90fc5ea995217fb33aa2693f939463130bf8b0","html_url":"https://github.com/y0-causal-inference/y0/commit/bb90fc5ea995217fb33aa2693f939463130bf8b0"}]},{"sha":"a22a1ce055926d3ccb3be48e7e0d80d355d00cfd","node_id":"C_kwDOE5hB_NoAKGEyMmExY2UwNTU5MjZkM2NjYjNiZTQ4ZTdlMGQ4MGQzNTVkMDBjZmQ","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-09T21:28:38Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-09T21:28:38Z"},"message":"Cleanup","tree":{"sha":"5242c27e2158ac5cdd65263c1a82792d6edd875d","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/5242c27e2158ac5cdd65263c1a82792d6edd875d"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/a22a1ce055926d3ccb3be48e7e0d80d355d00cfd","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/a22a1ce055926d3ccb3be48e7e0d80d355d00cfd","html_url":"https://github.com/y0-causal-inference/y0/commit/a22a1ce055926d3ccb3be48e7e0d80d355d00cfd","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/a22a1ce055926d3ccb3be48e7e0d80d355d00cfd/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"fbe09ce7380d97c75145250a8cef4bdc5fcb5e86","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/fbe09ce7380d97c75145250a8cef4bdc5fcb5e86","html_url":"https://github.com/y0-causal-inference/y0/commit/fbe09ce7380d97c75145250a8cef4bdc5fcb5e86"}]},{"sha":"1baad5a1289aab10dee555f696ae5c7843c0a1c1","node_id":"C_kwDOE5hB_NoAKDFiYWFkNWExMjg5YWFiMTBkZWU1NTVmNjk2YWU1Yzc4NDNjMGExYzE","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-09T21:32:21Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-09T21:32:21Z"},"message":"More + renames","tree":{"sha":"12c14687de70efc81ab9bf275bec03d1138da76a","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/12c14687de70efc81ab9bf275bec03d1138da76a"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/1baad5a1289aab10dee555f696ae5c7843c0a1c1","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/1baad5a1289aab10dee555f696ae5c7843c0a1c1","html_url":"https://github.com/y0-causal-inference/y0/commit/1baad5a1289aab10dee555f696ae5c7843c0a1c1","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/1baad5a1289aab10dee555f696ae5c7843c0a1c1/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"a22a1ce055926d3ccb3be48e7e0d80d355d00cfd","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/a22a1ce055926d3ccb3be48e7e0d80d355d00cfd","html_url":"https://github.com/y0-causal-inference/y0/commit/a22a1ce055926d3ccb3be48e7e0d80d355d00cfd"}]},{"sha":"d866d6bf43f844155e4508c3500d3bc41fae7ab7","node_id":"C_kwDOE5hB_NoAKGQ4NjZkNmJmNDNmODQ0MTU1ZTQ1MDhjMzUwMGQzYmM0MWZhZTdhYjc","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-09T23:26:25Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-09T23:26:25Z"},"message":"Created + TRSOQuery, refactored trso_line2 and test passes","tree":{"sha":"f2998db3e31db522a5885e0d158c799c6540f5be","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/f2998db3e31db522a5885e0d158c799c6540f5be"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/d866d6bf43f844155e4508c3500d3bc41fae7ab7","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/d866d6bf43f844155e4508c3500d3bc41fae7ab7","html_url":"https://github.com/y0-causal-inference/y0/commit/d866d6bf43f844155e4508c3500d3bc41fae7ab7","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/d866d6bf43f844155e4508c3500d3bc41fae7ab7/comments","author":null,"committer":null,"parents":[{"sha":"1baad5a1289aab10dee555f696ae5c7843c0a1c1","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/1baad5a1289aab10dee555f696ae5c7843c0a1c1","html_url":"https://github.com/y0-causal-inference/y0/commit/1baad5a1289aab10dee555f696ae5c7843c0a1c1"}]},{"sha":"064da8e68a10c30fd6b043dbfc20dce77647fb90","node_id":"C_kwDOE5hB_NoAKDA2NGRhOGU2OGExMGMzMGZkNmIwNDNkYmZjMjBkY2U3NzY0N2ZiOTA","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-09T23:32:21Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-09T23:32:21Z"},"message":"get_nodes_to_transport + test now passes","tree":{"sha":"7415cee5a10f349a06c0b6d538bec393515e1194","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/7415cee5a10f349a06c0b6d538bec393515e1194"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/064da8e68a10c30fd6b043dbfc20dce77647fb90","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/064da8e68a10c30fd6b043dbfc20dce77647fb90","html_url":"https://github.com/y0-causal-inference/y0/commit/064da8e68a10c30fd6b043dbfc20dce77647fb90","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/064da8e68a10c30fd6b043dbfc20dce77647fb90/comments","author":null,"committer":null,"parents":[{"sha":"d866d6bf43f844155e4508c3500d3bc41fae7ab7","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/d866d6bf43f844155e4508c3500d3bc41fae7ab7","html_url":"https://github.com/y0-causal-inference/y0/commit/d866d6bf43f844155e4508c3500d3bc41fae7ab7"}]},{"sha":"66819f35d071660b8e2691cb2fb545b64852a662","node_id":"C_kwDOE5hB_NoAKDY2ODE5ZjM1ZDA3MTY2MGI4ZTI2OTFjYjJmYjU0NWI2NDg1MmE2NjI","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-09T23:37:11Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-09T23:37:11Z"},"message":"get_nodes_to_transport + test now passes","tree":{"sha":"492e8e71e965bea977724ecc4a116419109c3a02","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/492e8e71e965bea977724ecc4a116419109c3a02"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/66819f35d071660b8e2691cb2fb545b64852a662","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/66819f35d071660b8e2691cb2fb545b64852a662","html_url":"https://github.com/y0-causal-inference/y0/commit/66819f35d071660b8e2691cb2fb545b64852a662","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/66819f35d071660b8e2691cb2fb545b64852a662/comments","author":null,"committer":null,"parents":[{"sha":"064da8e68a10c30fd6b043dbfc20dce77647fb90","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/064da8e68a10c30fd6b043dbfc20dce77647fb90","html_url":"https://github.com/y0-causal-inference/y0/commit/064da8e68a10c30fd6b043dbfc20dce77647fb90"}]},{"sha":"7d6b2da10657b1ac1cd7370c024815bc257f46cc","node_id":"C_kwDOE5hB_NoAKDdkNmIyZGExMDY1N2IxYWMxY2Q3MzcwYzAyNDgxNWJjMjU3ZjQ2Y2M","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-09T23:46:07Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-09T23:46:07Z"},"message":"refactored + surrogate_to_transport, test passes","tree":{"sha":"3a07e94ffa31c20fc87068d107fdced7b882b8ff","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/3a07e94ffa31c20fc87068d107fdced7b882b8ff"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/7d6b2da10657b1ac1cd7370c024815bc257f46cc","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7d6b2da10657b1ac1cd7370c024815bc257f46cc","html_url":"https://github.com/y0-causal-inference/y0/commit/7d6b2da10657b1ac1cd7370c024815bc257f46cc","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7d6b2da10657b1ac1cd7370c024815bc257f46cc/comments","author":null,"committer":null,"parents":[{"sha":"66819f35d071660b8e2691cb2fb545b64852a662","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/66819f35d071660b8e2691cb2fb545b64852a662","html_url":"https://github.com/y0-causal-inference/y0/commit/66819f35d071660b8e2691cb2fb545b64852a662"}]},{"sha":"59b02a797c20ec7a0a84547f1eabf6c20bad9add","node_id":"C_kwDOE5hB_NoAKDU5YjAyYTc5N2MyMGVjN2EwYTg0NTQ3ZjFlYWJmNmMyMGJhZDlhZGQ","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T15:27:22Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T15:27:22Z"},"message":"Refactored + trso_line3, test passes","tree":{"sha":"15dd0c779e2fac1f88ea424f268a59607503a78f","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/15dd0c779e2fac1f88ea424f268a59607503a78f"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/59b02a797c20ec7a0a84547f1eabf6c20bad9add","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/59b02a797c20ec7a0a84547f1eabf6c20bad9add","html_url":"https://github.com/y0-causal-inference/y0/commit/59b02a797c20ec7a0a84547f1eabf6c20bad9add","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/59b02a797c20ec7a0a84547f1eabf6c20bad9add/comments","author":null,"committer":null,"parents":[{"sha":"7d6b2da10657b1ac1cd7370c024815bc257f46cc","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7d6b2da10657b1ac1cd7370c024815bc257f46cc","html_url":"https://github.com/y0-causal-inference/y0/commit/7d6b2da10657b1ac1cd7370c024815bc257f46cc"}]},{"sha":"84f5a0a396ffa6e19ea1d06c67744a2ff290ff0b","node_id":"C_kwDOE5hB_NoAKDg0ZjVhMGEzOTZmZmE2ZTE5ZWExZDA2YzY3NzQ0YTJmZjI5MGZmMGI","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T15:35:27Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T15:35:27Z"},"message":"Refactored + trso_line4, test passes","tree":{"sha":"ac1f2af4e9d0906c7d4f723f772c60ecd5453589","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/ac1f2af4e9d0906c7d4f723f772c60ecd5453589"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/84f5a0a396ffa6e19ea1d06c67744a2ff290ff0b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/84f5a0a396ffa6e19ea1d06c67744a2ff290ff0b","html_url":"https://github.com/y0-causal-inference/y0/commit/84f5a0a396ffa6e19ea1d06c67744a2ff290ff0b","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/84f5a0a396ffa6e19ea1d06c67744a2ff290ff0b/comments","author":null,"committer":null,"parents":[{"sha":"59b02a797c20ec7a0a84547f1eabf6c20bad9add","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/59b02a797c20ec7a0a84547f1eabf6c20bad9add","html_url":"https://github.com/y0-causal-inference/y0/commit/59b02a797c20ec7a0a84547f1eabf6c20bad9add"}]},{"sha":"7e311498913088711a63c745b8a7c0b9b0a2a657","node_id":"C_kwDOE5hB_NoAKDdlMzExNDk4OTEzMDg4NzExYTYzYzc0NWI4YTdjMGI5YjBhMmE2NTc","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T16:55:23Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T16:55:23Z"},"message":"Refactored + trso_line6","tree":{"sha":"d61b058cbe9ed33d1fd2a494f71584a7cc867787","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/d61b058cbe9ed33d1fd2a494f71584a7cc867787"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/7e311498913088711a63c745b8a7c0b9b0a2a657","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7e311498913088711a63c745b8a7c0b9b0a2a657","html_url":"https://github.com/y0-causal-inference/y0/commit/7e311498913088711a63c745b8a7c0b9b0a2a657","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7e311498913088711a63c745b8a7c0b9b0a2a657/comments","author":null,"committer":null,"parents":[{"sha":"84f5a0a396ffa6e19ea1d06c67744a2ff290ff0b","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/84f5a0a396ffa6e19ea1d06c67744a2ff290ff0b","html_url":"https://github.com/y0-causal-inference/y0/commit/84f5a0a396ffa6e19ea1d06c67744a2ff290ff0b"}]},{"sha":"9734c3bfbb8b9fa7f01c5482ad4c39b043493464","node_id":"C_kwDOE5hB_NoAKDk3MzRjM2JmYmI4YjlmYTdmMDFjNTQ4MmFkNGMzOWIwNDM0OTM0NjQ","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T16:55:48Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T16:55:48Z"},"message":"Refactored + trso_line6, ran black","tree":{"sha":"f183aec9a8dfbbe98436d612dadfef13ab00cc4d","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/f183aec9a8dfbbe98436d612dadfef13ab00cc4d"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/9734c3bfbb8b9fa7f01c5482ad4c39b043493464","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/9734c3bfbb8b9fa7f01c5482ad4c39b043493464","html_url":"https://github.com/y0-causal-inference/y0/commit/9734c3bfbb8b9fa7f01c5482ad4c39b043493464","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/9734c3bfbb8b9fa7f01c5482ad4c39b043493464/comments","author":null,"committer":null,"parents":[{"sha":"7e311498913088711a63c745b8a7c0b9b0a2a657","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7e311498913088711a63c745b8a7c0b9b0a2a657","html_url":"https://github.com/y0-causal-inference/y0/commit/7e311498913088711a63c745b8a7c0b9b0a2a657"}]},{"sha":"237fd265bc750b6ee6e4e253e88d8365340d6e0d","node_id":"C_kwDOE5hB_NoAKDIzN2ZkMjY1YmM3NTBiNmVlNmU0ZTI1M2U4OGQ4MzY1MzQwZDZlMGQ","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T17:25:13Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T17:25:13Z"},"message":"test + for trso_line6 passes","tree":{"sha":"c29ac26e5fd2f626987a533ab7990918e8a678d8","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/c29ac26e5fd2f626987a533ab7990918e8a678d8"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/237fd265bc750b6ee6e4e253e88d8365340d6e0d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/237fd265bc750b6ee6e4e253e88d8365340d6e0d","html_url":"https://github.com/y0-causal-inference/y0/commit/237fd265bc750b6ee6e4e253e88d8365340d6e0d","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/237fd265bc750b6ee6e4e253e88d8365340d6e0d/comments","author":null,"committer":null,"parents":[{"sha":"9734c3bfbb8b9fa7f01c5482ad4c39b043493464","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/9734c3bfbb8b9fa7f01c5482ad4c39b043493464","html_url":"https://github.com/y0-causal-inference/y0/commit/9734c3bfbb8b9fa7f01c5482ad4c39b043493464"}]},{"sha":"899feffdad97d39059b5c9d55bc0ee3b526d4add","node_id":"C_kwDOE5hB_NoAKDg5OWZlZmZkYWQ5N2QzOTA1OWI1YzlkNTViYzBlZTNiNTI2ZDRhZGQ","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-10T19:04:41Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-10T19:04:41Z"},"message":"Refactor + names","tree":{"sha":"8f2296527ed4d0bcc1c7c21e290137174c978ca6","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/8f2296527ed4d0bcc1c7c21e290137174c978ca6"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/899feffdad97d39059b5c9d55bc0ee3b526d4add","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/899feffdad97d39059b5c9d55bc0ee3b526d4add","html_url":"https://github.com/y0-causal-inference/y0/commit/899feffdad97d39059b5c9d55bc0ee3b526d4add","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/899feffdad97d39059b5c9d55bc0ee3b526d4add/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"237fd265bc750b6ee6e4e253e88d8365340d6e0d","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/237fd265bc750b6ee6e4e253e88d8365340d6e0d","html_url":"https://github.com/y0-causal-inference/y0/commit/237fd265bc750b6ee6e4e253e88d8365340d6e0d"}]},{"sha":"149d30b2598133e3b540effaaee75e7ecc2114fd","node_id":"C_kwDOE5hB_NoAKDE0OWQzMGIyNTk4MTMzZTNiNTQwZWZmYWFlZTc1ZTdlY2MyMTE0ZmQ","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-10T19:12:33Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-10T19:12:33Z"},"message":"Update + typing","tree":{"sha":"f9f38d6f7fc0c6ac22763c0eaea94986ab3f8526","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/f9f38d6f7fc0c6ac22763c0eaea94986ab3f8526"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/149d30b2598133e3b540effaaee75e7ecc2114fd","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/149d30b2598133e3b540effaaee75e7ecc2114fd","html_url":"https://github.com/y0-causal-inference/y0/commit/149d30b2598133e3b540effaaee75e7ecc2114fd","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/149d30b2598133e3b540effaaee75e7ecc2114fd/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"899feffdad97d39059b5c9d55bc0ee3b526d4add","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/899feffdad97d39059b5c9d55bc0ee3b526d4add","html_url":"https://github.com/y0-causal-inference/y0/commit/899feffdad97d39059b5c9d55bc0ee3b526d4add"}]},{"sha":"5418a75b01f2dab3e9732490b1dc7d51e0286522","node_id":"C_kwDOE5hB_NoAKDU0MThhNzViMDFmMmRhYjNlOTczMjQ5MGIxZGM3ZDUxZTAyODY1MjI","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-10T19:26:31Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-10T19:26:31Z"},"message":"Cleanup","tree":{"sha":"48eb473f60d6d0881daae0761a860c6af0d7de37","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/48eb473f60d6d0881daae0761a860c6af0d7de37"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/5418a75b01f2dab3e9732490b1dc7d51e0286522","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/5418a75b01f2dab3e9732490b1dc7d51e0286522","html_url":"https://github.com/y0-causal-inference/y0/commit/5418a75b01f2dab3e9732490b1dc7d51e0286522","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/5418a75b01f2dab3e9732490b1dc7d51e0286522/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"149d30b2598133e3b540effaaee75e7ecc2114fd","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/149d30b2598133e3b540effaaee75e7ecc2114fd","html_url":"https://github.com/y0-causal-inference/y0/commit/149d30b2598133e3b540effaaee75e7ecc2114fd"}]},{"sha":"c23fbf0ed7141e1a338447d2b0faa213667e53dc","node_id":"C_kwDOE5hB_NoAKGMyM2ZiZjBlZDcxNDFlMWEzMzg0NDdkMmIwZmFhMjEzNjY3ZTUzZGM","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T19:49:19Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T19:49:19Z"},"message":"Added + lines 9 and 10, not sure if expression are handled correctly","tree":{"sha":"66e7ac76c77defae855c83dadae055725b31bddd","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/66e7ac76c77defae855c83dadae055725b31bddd"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/c23fbf0ed7141e1a338447d2b0faa213667e53dc","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/c23fbf0ed7141e1a338447d2b0faa213667e53dc","html_url":"https://github.com/y0-causal-inference/y0/commit/c23fbf0ed7141e1a338447d2b0faa213667e53dc","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/c23fbf0ed7141e1a338447d2b0faa213667e53dc/comments","author":null,"committer":null,"parents":[{"sha":"149d30b2598133e3b540effaaee75e7ecc2114fd","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/149d30b2598133e3b540effaaee75e7ecc2114fd","html_url":"https://github.com/y0-causal-inference/y0/commit/149d30b2598133e3b540effaaee75e7ecc2114fd"}]},{"sha":"7b7900b7e59ecc98d5d3991c3703cd09bd6abc0e","node_id":"C_kwDOE5hB_NoAKDdiNzkwMGI3ZTU5ZWNjOThkNWQzOTkxYzM3MDNjZDA5YmQ2YWJjMGU","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T19:49:40Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T19:49:40Z"},"message":"Merge + branch ''transport'' of https://github.com/y0-causal-inference/y0 into transport","tree":{"sha":"6d0b3d6954d9baa9d650861db62981c042086421","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/6d0b3d6954d9baa9d650861db62981c042086421"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/7b7900b7e59ecc98d5d3991c3703cd09bd6abc0e","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7b7900b7e59ecc98d5d3991c3703cd09bd6abc0e","html_url":"https://github.com/y0-causal-inference/y0/commit/7b7900b7e59ecc98d5d3991c3703cd09bd6abc0e","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7b7900b7e59ecc98d5d3991c3703cd09bd6abc0e/comments","author":null,"committer":null,"parents":[{"sha":"c23fbf0ed7141e1a338447d2b0faa213667e53dc","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/c23fbf0ed7141e1a338447d2b0faa213667e53dc","html_url":"https://github.com/y0-causal-inference/y0/commit/c23fbf0ed7141e1a338447d2b0faa213667e53dc"},{"sha":"5418a75b01f2dab3e9732490b1dc7d51e0286522","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/5418a75b01f2dab3e9732490b1dc7d51e0286522","html_url":"https://github.com/y0-causal-inference/y0/commit/5418a75b01f2dab3e9732490b1dc7d51e0286522"}]},{"sha":"062bd0a24c5ed0e0f103ac5918ee4ef276077f40","node_id":"C_kwDOE5hB_NoAKDA2MmJkMGEyNGM1ZWQwZTBmMTAzYWM1OTE4ZWU0ZWYyNzYwNzdmNDA","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T20:19:49Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T20:19:49Z"},"message":"Small + edits to line 9, not completely fixed yet","tree":{"sha":"71361c351b089bebdf840077e9359a48c241820e","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/71361c351b089bebdf840077e9359a48c241820e"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/062bd0a24c5ed0e0f103ac5918ee4ef276077f40","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/062bd0a24c5ed0e0f103ac5918ee4ef276077f40","html_url":"https://github.com/y0-causal-inference/y0/commit/062bd0a24c5ed0e0f103ac5918ee4ef276077f40","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/062bd0a24c5ed0e0f103ac5918ee4ef276077f40/comments","author":null,"committer":null,"parents":[{"sha":"7b7900b7e59ecc98d5d3991c3703cd09bd6abc0e","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7b7900b7e59ecc98d5d3991c3703cd09bd6abc0e","html_url":"https://github.com/y0-causal-inference/y0/commit/7b7900b7e59ecc98d5d3991c3703cd09bd6abc0e"}]},{"sha":"490d95b6145cf0fe22e72a37697f3c3aa09d12b8","node_id":"C_kwDOE5hB_NoAKDQ5MGQ5NWI2MTQ1Y2YwZmUyMmU3MmEzNzY5N2YzYzNhYTA5ZDEyYjg","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-10T22:45:01Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-10T22:45:01Z"},"message":"Add + more explicit conditionals for consideration","tree":{"sha":"bb6b4ff26862c1ff6f0a08c10317e1c3219f31f6","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/bb6b4ff26862c1ff6f0a08c10317e1c3219f31f6"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/490d95b6145cf0fe22e72a37697f3c3aa09d12b8","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/490d95b6145cf0fe22e72a37697f3c3aa09d12b8","html_url":"https://github.com/y0-causal-inference/y0/commit/490d95b6145cf0fe22e72a37697f3c3aa09d12b8","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/490d95b6145cf0fe22e72a37697f3c3aa09d12b8/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"062bd0a24c5ed0e0f103ac5918ee4ef276077f40","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/062bd0a24c5ed0e0f103ac5918ee4ef276077f40","html_url":"https://github.com/y0-causal-inference/y0/commit/062bd0a24c5ed0e0f103ac5918ee4ef276077f40"}]},{"sha":"04047704458c9588584beb4f42b489da23fe2dfe","node_id":"C_kwDOE5hB_NoAKDA0MDQ3NzA0NDU4Yzk1ODg1ODRiZWI0ZjQyYjQ4OWRhMjNmZTJkZmU","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T23:05:50Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T23:05:50Z"},"message":"added + case to are_d_separated to account for node not in graph","tree":{"sha":"8321eb80b60c9bd4ed43c7995bc849c30ea524d9","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/8321eb80b60c9bd4ed43c7995bc849c30ea524d9"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/04047704458c9588584beb4f42b489da23fe2dfe","comment_count":1,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/04047704458c9588584beb4f42b489da23fe2dfe","html_url":"https://github.com/y0-causal-inference/y0/commit/04047704458c9588584beb4f42b489da23fe2dfe","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/04047704458c9588584beb4f42b489da23fe2dfe/comments","author":null,"committer":null,"parents":[{"sha":"490d95b6145cf0fe22e72a37697f3c3aa09d12b8","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/490d95b6145cf0fe22e72a37697f3c3aa09d12b8","html_url":"https://github.com/y0-causal-inference/y0/commit/490d95b6145cf0fe22e72a37697f3c3aa09d12b8"}]},{"sha":"5f82db48722ecf5b9fcf19143c9ef7e8743005db","node_id":"C_kwDOE5hB_NoAKDVmODJkYjQ4NzIyZWNmNWI5ZmNmMTkxNDNjOWVmN2U4NzQzMDA1ZGI","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T23:21:02Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T23:21:02Z"},"message":"small + fixes to trso_line10","tree":{"sha":"90f62af458c0332532d428151de5b8ea867a6539","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/90f62af458c0332532d428151de5b8ea867a6539"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/5f82db48722ecf5b9fcf19143c9ef7e8743005db","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/5f82db48722ecf5b9fcf19143c9ef7e8743005db","html_url":"https://github.com/y0-causal-inference/y0/commit/5f82db48722ecf5b9fcf19143c9ef7e8743005db","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/5f82db48722ecf5b9fcf19143c9ef7e8743005db/comments","author":null,"committer":null,"parents":[{"sha":"04047704458c9588584beb4f42b489da23fe2dfe","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/04047704458c9588584beb4f42b489da23fe2dfe","html_url":"https://github.com/y0-causal-inference/y0/commit/04047704458c9588584beb4f42b489da23fe2dfe"}]},{"sha":"d0c71e1fecbe065526243ca0c3cbe3917ff7b060","node_id":"C_kwDOE5hB_NoAKGQwYzcxZTFmZWNiZTA2NTUyNjI0M2NhMGMzY2JlMzkxN2ZmN2IwNjA","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T23:21:35Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-10T23:21:35Z"},"message":"small + fixes to trso_line10,again","tree":{"sha":"55f5192a8d996103e6dfe4dacc776949625f4f45","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/55f5192a8d996103e6dfe4dacc776949625f4f45"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/d0c71e1fecbe065526243ca0c3cbe3917ff7b060","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/d0c71e1fecbe065526243ca0c3cbe3917ff7b060","html_url":"https://github.com/y0-causal-inference/y0/commit/d0c71e1fecbe065526243ca0c3cbe3917ff7b060","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/d0c71e1fecbe065526243ca0c3cbe3917ff7b060/comments","author":null,"committer":null,"parents":[{"sha":"5f82db48722ecf5b9fcf19143c9ef7e8743005db","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/5f82db48722ecf5b9fcf19143c9ef7e8743005db","html_url":"https://github.com/y0-causal-inference/y0/commit/5f82db48722ecf5b9fcf19143c9ef7e8743005db"}]},{"sha":"34cdb3a1a87386637a49ed8f6459e6171c2bc280","node_id":"C_kwDOE5hB_NoAKDM0Y2RiM2ExYTg3Mzg2NjM3YTQ5ZWQ4ZjY0NTllNjE3MWMyYmMyODA","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-11T10:39:52Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-11T10:39:52Z"},"message":"Update + transport.py","tree":{"sha":"075e9511cabc77cd14c0e5fb12d27c622a8f0842","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/075e9511cabc77cd14c0e5fb12d27c622a8f0842"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/34cdb3a1a87386637a49ed8f6459e6171c2bc280","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/34cdb3a1a87386637a49ed8f6459e6171c2bc280","html_url":"https://github.com/y0-causal-inference/y0/commit/34cdb3a1a87386637a49ed8f6459e6171c2bc280","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/34cdb3a1a87386637a49ed8f6459e6171c2bc280/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"d0c71e1fecbe065526243ca0c3cbe3917ff7b060","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/d0c71e1fecbe065526243ca0c3cbe3917ff7b060","html_url":"https://github.com/y0-causal-inference/y0/commit/d0c71e1fecbe065526243ca0c3cbe3917ff7b060"}]},{"sha":"329d7468e4112b377842aad0c0bb4deaa89f7a93","node_id":"C_kwDOE5hB_NoAKDMyOWQ3NDY4ZTQxMTJiMzc3ODQyYWFkMGMwYmI0ZGVhYTg5ZjdhOTM","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-11T11:29:02Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-11T11:29:02Z"},"message":"Merge + branch ''main'' into transport","tree":{"sha":"9d35b99af5ea1c3bcb39b73cf33ca6d1d7374ece","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/9d35b99af5ea1c3bcb39b73cf33ca6d1d7374ece"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/329d7468e4112b377842aad0c0bb4deaa89f7a93","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/329d7468e4112b377842aad0c0bb4deaa89f7a93","html_url":"https://github.com/y0-causal-inference/y0/commit/329d7468e4112b377842aad0c0bb4deaa89f7a93","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/329d7468e4112b377842aad0c0bb4deaa89f7a93/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"34cdb3a1a87386637a49ed8f6459e6171c2bc280","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/34cdb3a1a87386637a49ed8f6459e6171c2bc280","html_url":"https://github.com/y0-causal-inference/y0/commit/34cdb3a1a87386637a49ed8f6459e6171c2bc280"},{"sha":"34e3ac01c86b9744425526f0eb0a5928b1c5ede7","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/34e3ac01c86b9744425526f0eb0a5928b1c5ede7","html_url":"https://github.com/y0-causal-inference/y0/commit/34e3ac01c86b9744425526f0eb0a5928b1c5ede7"}]},{"sha":"6320cd8fad7988aad1f0b8a58339058794a16ade","node_id":"C_kwDOE5hB_NoAKDYzMjBjZDhmYWQ3OTg4YWFkMWYwYjhhNTgzMzkwNTg3OTRhMTZhZGU","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-11T11:35:58Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-11T11:35:58Z"},"message":"Update + transport.py","tree":{"sha":"fef244efa062c18da73d598eaf0e870e9f2eb0fc","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/fef244efa062c18da73d598eaf0e870e9f2eb0fc"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/6320cd8fad7988aad1f0b8a58339058794a16ade","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/6320cd8fad7988aad1f0b8a58339058794a16ade","html_url":"https://github.com/y0-causal-inference/y0/commit/6320cd8fad7988aad1f0b8a58339058794a16ade","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/6320cd8fad7988aad1f0b8a58339058794a16ade/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"329d7468e4112b377842aad0c0bb4deaa89f7a93","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/329d7468e4112b377842aad0c0bb4deaa89f7a93","html_url":"https://github.com/y0-causal-inference/y0/commit/329d7468e4112b377842aad0c0bb4deaa89f7a93"}]},{"sha":"7c1168639ba0ddebb873d97a96fa720b3cdb4471","node_id":"C_kwDOE5hB_NoAKDdjMTE2ODYzOWJhMGRkZWJiODczZDk3YTk2ZmE3MjBiM2NkYjQ0NzE","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-11T11:36:34Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-11T11:36:34Z"},"message":"Update + setup.cfg","tree":{"sha":"8e9c508eaa881601bf7b37a52649bb3061d8f9c4","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/8e9c508eaa881601bf7b37a52649bb3061d8f9c4"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/7c1168639ba0ddebb873d97a96fa720b3cdb4471","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7c1168639ba0ddebb873d97a96fa720b3cdb4471","html_url":"https://github.com/y0-causal-inference/y0/commit/7c1168639ba0ddebb873d97a96fa720b3cdb4471","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7c1168639ba0ddebb873d97a96fa720b3cdb4471/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"6320cd8fad7988aad1f0b8a58339058794a16ade","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/6320cd8fad7988aad1f0b8a58339058794a16ade","html_url":"https://github.com/y0-causal-inference/y0/commit/6320cd8fad7988aad1f0b8a58339058794a16ade"}]},{"sha":"866b818f5346793b76f516d3eee0aa50466c099d","node_id":"C_kwDOE5hB_NoAKDg2NmI4MThmNTM0Njc5M2I3NmY1MTZkM2VlZTBhYTUwNDY2YzA5OWQ","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-11T13:20:19Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-11T13:20:19Z"},"message":"Merge + branch ''main'' into transport","tree":{"sha":"877d2a622c0bf03043e2a6ea21365c615e316944","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/877d2a622c0bf03043e2a6ea21365c615e316944"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/866b818f5346793b76f516d3eee0aa50466c099d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/866b818f5346793b76f516d3eee0aa50466c099d","html_url":"https://github.com/y0-causal-inference/y0/commit/866b818f5346793b76f516d3eee0aa50466c099d","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/866b818f5346793b76f516d3eee0aa50466c099d/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"7c1168639ba0ddebb873d97a96fa720b3cdb4471","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/7c1168639ba0ddebb873d97a96fa720b3cdb4471","html_url":"https://github.com/y0-causal-inference/y0/commit/7c1168639ba0ddebb873d97a96fa720b3cdb4471"},{"sha":"af29e64a5cce390e3e2beff5e80f7f96015714fd","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/af29e64a5cce390e3e2beff5e80f7f96015714fd","html_url":"https://github.com/y0-causal-inference/y0/commit/af29e64a5cce390e3e2beff5e80f7f96015714fd"}]},{"sha":"faedd0af4a0b5bc882f0986316bb5536374c3622","node_id":"C_kwDOE5hB_NoAKGZhZWRkMGFmNGEwYjViYzg4MmYwOTg2MzE2YmI1NTM2Mzc0YzM2MjI","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-11T13:21:10Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-11T13:21:10Z"},"message":"Fix + method name","tree":{"sha":"bd4cbcab1e99b5ca82eff8497a962d937af54a33","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/bd4cbcab1e99b5ca82eff8497a962d937af54a33"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/faedd0af4a0b5bc882f0986316bb5536374c3622","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/faedd0af4a0b5bc882f0986316bb5536374c3622","html_url":"https://github.com/y0-causal-inference/y0/commit/faedd0af4a0b5bc882f0986316bb5536374c3622","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/faedd0af4a0b5bc882f0986316bb5536374c3622/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"866b818f5346793b76f516d3eee0aa50466c099d","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/866b818f5346793b76f516d3eee0aa50466c099d","html_url":"https://github.com/y0-causal-inference/y0/commit/866b818f5346793b76f516d3eee0aa50466c099d"}]},{"sha":"e4a9afff0681a40ba58e349026a9662c5d275897","node_id":"C_kwDOE5hB_NoAKGU0YTlhZmZmMDY4MWE0MGJhNThlMzQ5MDI2YTk2NjJjNWQyNzU4OTc","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-11T18:39:10Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-11T18:39:10Z"},"message":"Small + fixes in trso_line6 and trso_line10, algorithm example runs to completion","tree":{"sha":"5a33873a02cc34e20c94d007765032075e02570d","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/5a33873a02cc34e20c94d007765032075e02570d"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/e4a9afff0681a40ba58e349026a9662c5d275897","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/e4a9afff0681a40ba58e349026a9662c5d275897","html_url":"https://github.com/y0-causal-inference/y0/commit/e4a9afff0681a40ba58e349026a9662c5d275897","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/e4a9afff0681a40ba58e349026a9662c5d275897/comments","author":null,"committer":null,"parents":[{"sha":"faedd0af4a0b5bc882f0986316bb5536374c3622","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/faedd0af4a0b5bc882f0986316bb5536374c3622","html_url":"https://github.com/y0-causal-inference/y0/commit/faedd0af4a0b5bc882f0986316bb5536374c3622"}]},{"sha":"dbad7a4dbf76685771ddc16a66e89085d62ef599","node_id":"C_kwDOE5hB_NoAKGRiYWQ3YTRkYmY3NjY4NTc3MWRkYzE2YTY2ZTg5MDg1ZDYyZWY1OTk","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-11T20:28:47Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-11T20:28:47Z"},"message":"docstring + edits and updates","tree":{"sha":"fd51fd0fbb3d838188bca08165663c84868bd4fa","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/fd51fd0fbb3d838188bca08165663c84868bd4fa"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/dbad7a4dbf76685771ddc16a66e89085d62ef599","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/dbad7a4dbf76685771ddc16a66e89085d62ef599","html_url":"https://github.com/y0-causal-inference/y0/commit/dbad7a4dbf76685771ddc16a66e89085d62ef599","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/dbad7a4dbf76685771ddc16a66e89085d62ef599/comments","author":null,"committer":null,"parents":[{"sha":"e4a9afff0681a40ba58e349026a9662c5d275897","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/e4a9afff0681a40ba58e349026a9662c5d275897","html_url":"https://github.com/y0-causal-inference/y0/commit/e4a9afff0681a40ba58e349026a9662c5d275897"}]},{"sha":"08cf2609b19cd9541f340d5cfdefd5bd157764fb","node_id":"C_kwDOE5hB_NoAKDA4Y2YyNjA5YjE5Y2Q5NTQxZjM0MGQ1Y2ZkZWZkNWJkMTU3NzY0ZmI","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-15T18:42:40Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-15T18:42:40Z"},"message":"line + 9 adjustments, moving closer to correctness","tree":{"sha":"4de79a4ad3dc175e520af8e1a7364580ea1c17fc","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/4de79a4ad3dc175e520af8e1a7364580ea1c17fc"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/08cf2609b19cd9541f340d5cfdefd5bd157764fb","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/08cf2609b19cd9541f340d5cfdefd5bd157764fb","html_url":"https://github.com/y0-causal-inference/y0/commit/08cf2609b19cd9541f340d5cfdefd5bd157764fb","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/08cf2609b19cd9541f340d5cfdefd5bd157764fb/comments","author":null,"committer":null,"parents":[{"sha":"dbad7a4dbf76685771ddc16a66e89085d62ef599","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/dbad7a4dbf76685771ddc16a66e89085d62ef599","html_url":"https://github.com/y0-causal-inference/y0/commit/dbad7a4dbf76685771ddc16a66e89085d62ef599"}]},{"sha":"de53cf4f471d17623d8dadb37a18affe74af5f46","node_id":"C_kwDOE5hB_NoAKGRlNTNjZjRmNDcxZDE3NjIzZDhkYWRiMzdhMThhZmZlNzRhZjVmNDY","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-15T20:00:13Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-15T20:00:13Z"},"message":"Update + product tabulation","tree":{"sha":"fe63d4edac1bdd2158d4524ce6a78988c7796d1b","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/fe63d4edac1bdd2158d4524ce6a78988c7796d1b"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/de53cf4f471d17623d8dadb37a18affe74af5f46","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/de53cf4f471d17623d8dadb37a18affe74af5f46","html_url":"https://github.com/y0-causal-inference/y0/commit/de53cf4f471d17623d8dadb37a18affe74af5f46","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/de53cf4f471d17623d8dadb37a18affe74af5f46/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"08cf2609b19cd9541f340d5cfdefd5bd157764fb","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/08cf2609b19cd9541f340d5cfdefd5bd157764fb","html_url":"https://github.com/y0-causal-inference/y0/commit/08cf2609b19cd9541f340d5cfdefd5bd157764fb"}]},{"sha":"5040a837bb2a9f37012f25db7f654fe1e1ded140","node_id":"C_kwDOE5hB_NoAKDUwNDBhODM3YmIyYTlmMzcwMTJmMjVkYjdmNjU0ZmUxZTFkZWQxNDA","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-16T11:43:36Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-16T11:43:36Z"},"message":"Update + transport.py","tree":{"sha":"f01fdd71197a50a3dabbac06f39d5984054332d0","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/f01fdd71197a50a3dabbac06f39d5984054332d0"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/5040a837bb2a9f37012f25db7f654fe1e1ded140","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/5040a837bb2a9f37012f25db7f654fe1e1ded140","html_url":"https://github.com/y0-causal-inference/y0/commit/5040a837bb2a9f37012f25db7f654fe1e1ded140","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/5040a837bb2a9f37012f25db7f654fe1e1ded140/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"de53cf4f471d17623d8dadb37a18affe74af5f46","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/de53cf4f471d17623d8dadb37a18affe74af5f46","html_url":"https://github.com/y0-causal-inference/y0/commit/de53cf4f471d17623d8dadb37a18affe74af5f46"}]},{"sha":"804fafe42587eb6c924d0e73a2a626b7a3c5ce9a","node_id":"C_kwDOE5hB_NoAKDgwNGZhZmU0MjU4N2ViNmM5MjRkMGU3M2EyYTYyNmI3YTNjNWNlOWE","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-16T16:15:23Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-16T16:15:23Z"},"message":"added + TODO and fraction simplify","tree":{"sha":"a6eb292539becd2ec8fe923cbc0e461220bee771","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/a6eb292539becd2ec8fe923cbc0e461220bee771"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/804fafe42587eb6c924d0e73a2a626b7a3c5ce9a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/804fafe42587eb6c924d0e73a2a626b7a3c5ce9a","html_url":"https://github.com/y0-causal-inference/y0/commit/804fafe42587eb6c924d0e73a2a626b7a3c5ce9a","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/804fafe42587eb6c924d0e73a2a626b7a3c5ce9a/comments","author":null,"committer":null,"parents":[{"sha":"5040a837bb2a9f37012f25db7f654fe1e1ded140","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/5040a837bb2a9f37012f25db7f654fe1e1ded140","html_url":"https://github.com/y0-causal-inference/y0/commit/5040a837bb2a9f37012f25db7f654fe1e1ded140"}]},{"sha":"74d2e0baa38951c2451d5f5f6052f693a24d0288","node_id":"C_kwDOE5hB_NoAKDc0ZDJlMGJhYTM4OTUxYzI0NTFkNWY1ZjYwNTJmNjkzYTI0ZDAyODg","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-16T16:43:10Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-16T16:43:10Z"},"message":"Merge + branch ''main'' into transport","tree":{"sha":"71d5d3bc02557e5f3bfd0269ff295ca8f3da6560","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/71d5d3bc02557e5f3bfd0269ff295ca8f3da6560"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/74d2e0baa38951c2451d5f5f6052f693a24d0288","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/74d2e0baa38951c2451d5f5f6052f693a24d0288","html_url":"https://github.com/y0-causal-inference/y0/commit/74d2e0baa38951c2451d5f5f6052f693a24d0288","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/74d2e0baa38951c2451d5f5f6052f693a24d0288/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"804fafe42587eb6c924d0e73a2a626b7a3c5ce9a","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/804fafe42587eb6c924d0e73a2a626b7a3c5ce9a","html_url":"https://github.com/y0-causal-inference/y0/commit/804fafe42587eb6c924d0e73a2a626b7a3c5ce9a"},{"sha":"a887cbc2341b2c0374c3e99176846430a98ed358","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/a887cbc2341b2c0374c3e99176846430a98ed358","html_url":"https://github.com/y0-causal-inference/y0/commit/a887cbc2341b2c0374c3e99176846430a98ed358"}]},{"sha":"000c79f5f9570952c56fa6f9c375b2f74cc30e49","node_id":"C_kwDOE5hB_NoAKDAwMGM3OWY1Zjk1NzA5NTJjNTZmYTZmOWMzNzViMmY3NGNjMzBlNDk","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-16T16:44:47Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-16T16:44:47Z"},"message":"Update + based on new sum.safe","tree":{"sha":"8e83c532a4c41c1ce4b9a555c3334828e97fa604","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/8e83c532a4c41c1ce4b9a555c3334828e97fa604"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/000c79f5f9570952c56fa6f9c375b2f74cc30e49","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/000c79f5f9570952c56fa6f9c375b2f74cc30e49","html_url":"https://github.com/y0-causal-inference/y0/commit/000c79f5f9570952c56fa6f9c375b2f74cc30e49","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/000c79f5f9570952c56fa6f9c375b2f74cc30e49/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"74d2e0baa38951c2451d5f5f6052f693a24d0288","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/74d2e0baa38951c2451d5f5f6052f693a24d0288","html_url":"https://github.com/y0-causal-inference/y0/commit/74d2e0baa38951c2451d5f5f6052f693a24d0288"}]},{"sha":"97cb000b3283351a657041e3e06e9e1c8fcfd3b3","node_id":"C_kwDOE5hB_NoAKDk3Y2IwMDBiMzI4MzM1MWE2NTcwNDFlM2UwNmU5ZTFjOGZjZmQzYjM","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-16T16:44:56Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-16T16:44:56Z"},"message":"Add + notimplementederror","tree":{"sha":"4323884d871d55b5aff839e26e430af0ecfd0f98","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/4323884d871d55b5aff839e26e430af0ecfd0f98"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/97cb000b3283351a657041e3e06e9e1c8fcfd3b3","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/97cb000b3283351a657041e3e06e9e1c8fcfd3b3","html_url":"https://github.com/y0-causal-inference/y0/commit/97cb000b3283351a657041e3e06e9e1c8fcfd3b3","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/97cb000b3283351a657041e3e06e9e1c8fcfd3b3/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"000c79f5f9570952c56fa6f9c375b2f74cc30e49","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/000c79f5f9570952c56fa6f9c375b2f74cc30e49","html_url":"https://github.com/y0-causal-inference/y0/commit/000c79f5f9570952c56fa6f9c375b2f74cc30e49"}]},{"sha":"75d36edc9773f4c1ce412e77254b5bbee75a9f8f","node_id":"C_kwDOE5hB_NoAKDc1ZDM2ZWRjOTc3M2Y0YzFjZTQxMmU3NzI1NGI1YmJlZTc1YTlmOGY","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-16T20:06:46Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-16T20:06:46Z"},"message":"get_regular_nodes + added","tree":{"sha":"a1a3c789d8b6870c350e515c8fc56511ffe423e1","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/a1a3c789d8b6870c350e515c8fc56511ffe423e1"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/75d36edc9773f4c1ce412e77254b5bbee75a9f8f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/75d36edc9773f4c1ce412e77254b5bbee75a9f8f","html_url":"https://github.com/y0-causal-inference/y0/commit/75d36edc9773f4c1ce412e77254b5bbee75a9f8f","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/75d36edc9773f4c1ce412e77254b5bbee75a9f8f/comments","author":null,"committer":null,"parents":[{"sha":"97cb000b3283351a657041e3e06e9e1c8fcfd3b3","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/97cb000b3283351a657041e3e06e9e1c8fcfd3b3","html_url":"https://github.com/y0-causal-inference/y0/commit/97cb000b3283351a657041e3e06e9e1c8fcfd3b3"}]},{"sha":"1973bf4f9c566b5a121f517cd92f9a33bb6805b6","node_id":"C_kwDOE5hB_NoAKDE5NzNiZjRmOWM1NjZiNWExMjFmNTE3Y2Q5MmY5YTMzYmI2ODA1YjY","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-23T22:28:10Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-23T22:28:10Z"},"message":"Aux + function","tree":{"sha":"2e91b5d4e7a60e324a979515c0406bcbf7ea1086","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/2e91b5d4e7a60e324a979515c0406bcbf7ea1086"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/1973bf4f9c566b5a121f517cd92f9a33bb6805b6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/1973bf4f9c566b5a121f517cd92f9a33bb6805b6","html_url":"https://github.com/y0-causal-inference/y0/commit/1973bf4f9c566b5a121f517cd92f9a33bb6805b6","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/1973bf4f9c566b5a121f517cd92f9a33bb6805b6/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"97cb000b3283351a657041e3e06e9e1c8fcfd3b3","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/97cb000b3283351a657041e3e06e9e1c8fcfd3b3","html_url":"https://github.com/y0-causal-inference/y0/commit/97cb000b3283351a657041e3e06e9e1c8fcfd3b3"}]},{"sha":"3189196e8196afc806c21899cc644ddd4edf3b98","node_id":"C_kwDOE5hB_NoAKDMxODkxOTZlODE5NmFmYzgwNmMyMTg5OWNjNjQ0ZGRkNGVkZjNiOTg","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-23T22:28:21Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-23T22:28:21Z"},"message":"Improve + zero check","tree":{"sha":"fdf2eb42d7541b524d33e41d17414b32c57260c6","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/fdf2eb42d7541b524d33e41d17414b32c57260c6"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/3189196e8196afc806c21899cc644ddd4edf3b98","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3189196e8196afc806c21899cc644ddd4edf3b98","html_url":"https://github.com/y0-causal-inference/y0/commit/3189196e8196afc806c21899cc644ddd4edf3b98","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3189196e8196afc806c21899cc644ddd4edf3b98/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"1973bf4f9c566b5a121f517cd92f9a33bb6805b6","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/1973bf4f9c566b5a121f517cd92f9a33bb6805b6","html_url":"https://github.com/y0-causal-inference/y0/commit/1973bf4f9c566b5a121f517cd92f9a33bb6805b6"}]},{"sha":"3dc12950b62ed5cee65fb194de071139c5c3ceb0","node_id":"C_kwDOE5hB_NoAKDNkYzEyOTUwYjYyZWQ1Y2VlNjVmYjE5NGRlMDcxMTM5YzVjM2NlYjA","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-23T22:28:43Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-23T22:28:43Z"},"message":"Add + expression equivalence check","tree":{"sha":"fd62af7c9a86558c4ef280e167990dfa6553363a","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/fd62af7c9a86558c4ef280e167990dfa6553363a"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/3dc12950b62ed5cee65fb194de071139c5c3ceb0","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3dc12950b62ed5cee65fb194de071139c5c3ceb0","html_url":"https://github.com/y0-causal-inference/y0/commit/3dc12950b62ed5cee65fb194de071139c5c3ceb0","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3dc12950b62ed5cee65fb194de071139c5c3ceb0/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"3189196e8196afc806c21899cc644ddd4edf3b98","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3189196e8196afc806c21899cc644ddd4edf3b98","html_url":"https://github.com/y0-causal-inference/y0/commit/3189196e8196afc806c21899cc644ddd4edf3b98"}]},{"sha":"0defa8c97ba6fd02c9cae18bb83933e5ee7dce1a","node_id":"C_kwDOE5hB_NoAKDBkZWZhOGM5N2JhNmZkMDJjOWNhZTE4YmI4MzkzM2U1ZWU3ZGNlMWE","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-23T23:01:36Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-23T23:01:36Z"},"message":"Resolved + conflicts","tree":{"sha":"9fe97344801c728cc5f7b3e45be84b4c89fe6666","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/9fe97344801c728cc5f7b3e45be84b4c89fe6666"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/0defa8c97ba6fd02c9cae18bb83933e5ee7dce1a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/0defa8c97ba6fd02c9cae18bb83933e5ee7dce1a","html_url":"https://github.com/y0-causal-inference/y0/commit/0defa8c97ba6fd02c9cae18bb83933e5ee7dce1a","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/0defa8c97ba6fd02c9cae18bb83933e5ee7dce1a/comments","author":null,"committer":null,"parents":[{"sha":"75d36edc9773f4c1ce412e77254b5bbee75a9f8f","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/75d36edc9773f4c1ce412e77254b5bbee75a9f8f","html_url":"https://github.com/y0-causal-inference/y0/commit/75d36edc9773f4c1ce412e77254b5bbee75a9f8f"},{"sha":"3dc12950b62ed5cee65fb194de071139c5c3ceb0","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3dc12950b62ed5cee65fb194de071139c5c3ceb0","html_url":"https://github.com/y0-causal-inference/y0/commit/3dc12950b62ed5cee65fb194de071139c5c3ceb0"}]},{"sha":"4f2fd73f77b6d5e49bc13f2606c976502baa264d","node_id":"C_kwDOE5hB_NoAKDRmMmZkNzNmNzdiNmQ1ZTQ5YmMxM2YyNjA2Yzk3NjUwMmJhYTI2NGQ","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T01:20:31Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T01:20:31Z"},"message":"Added + logger output","tree":{"sha":"c1e9154059c9bc99fc3c1d51dbde70f46f49a983","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/c1e9154059c9bc99fc3c1d51dbde70f46f49a983"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/4f2fd73f77b6d5e49bc13f2606c976502baa264d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/4f2fd73f77b6d5e49bc13f2606c976502baa264d","html_url":"https://github.com/y0-causal-inference/y0/commit/4f2fd73f77b6d5e49bc13f2606c976502baa264d","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/4f2fd73f77b6d5e49bc13f2606c976502baa264d/comments","author":null,"committer":null,"parents":[{"sha":"0defa8c97ba6fd02c9cae18bb83933e5ee7dce1a","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/0defa8c97ba6fd02c9cae18bb83933e5ee7dce1a","html_url":"https://github.com/y0-causal-inference/y0/commit/0defa8c97ba6fd02c9cae18bb83933e5ee7dce1a"}]},{"sha":"fdad5875f14332258f684f2e9a813927eb3682cd","node_id":"C_kwDOE5hB_NoAKGZkYWQ1ODc1ZjE0MzMyMjU4ZjY4NGYyZTlhODEzOTI3ZWIzNjgyY2Q","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T01:20:50Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T01:20:50Z"},"message":"Ran + black","tree":{"sha":"9f1ce8ea7b8d9ba561bebdff59b4172b7665d6ba","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/9f1ce8ea7b8d9ba561bebdff59b4172b7665d6ba"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/fdad5875f14332258f684f2e9a813927eb3682cd","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/fdad5875f14332258f684f2e9a813927eb3682cd","html_url":"https://github.com/y0-causal-inference/y0/commit/fdad5875f14332258f684f2e9a813927eb3682cd","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/fdad5875f14332258f684f2e9a813927eb3682cd/comments","author":null,"committer":null,"parents":[{"sha":"4f2fd73f77b6d5e49bc13f2606c976502baa264d","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/4f2fd73f77b6d5e49bc13f2606c976502baa264d","html_url":"https://github.com/y0-causal-inference/y0/commit/4f2fd73f77b6d5e49bc13f2606c976502baa264d"}]},{"sha":"8362c7e16ed4018296c5bd8e123f1cfbb84c36c0","node_id":"C_kwDOE5hB_NoAKDgzNjJjN2UxNmVkNDAxODI5NmM1YmQ4ZTEyM2YxY2ZiYjg0YzM2YzA","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T15:22:37Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T15:22:37Z"},"message":"modified + line 2, test for full trso passes, (still missing do statements)","tree":{"sha":"4df496b812c32d2bb9582d7f9c6cb6496ad5a2cd","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/4df496b812c32d2bb9582d7f9c6cb6496ad5a2cd"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/8362c7e16ed4018296c5bd8e123f1cfbb84c36c0","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/8362c7e16ed4018296c5bd8e123f1cfbb84c36c0","html_url":"https://github.com/y0-causal-inference/y0/commit/8362c7e16ed4018296c5bd8e123f1cfbb84c36c0","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/8362c7e16ed4018296c5bd8e123f1cfbb84c36c0/comments","author":null,"committer":null,"parents":[{"sha":"fdad5875f14332258f684f2e9a813927eb3682cd","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/fdad5875f14332258f684f2e9a813927eb3682cd","html_url":"https://github.com/y0-causal-inference/y0/commit/fdad5875f14332258f684f2e9a813927eb3682cd"}]},{"sha":"1cf5357477d956a755b3aedf2b2a83d1c442521c","node_id":"C_kwDOE5hB_NoAKDFjZjUzNTc0NzdkOTU2YTc1NWIzYWVkZjJiMmE4M2QxYzQ0MjUyMWM","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T15:43:56Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T15:43:56Z"},"message":"added + canonicalize for trso returns","tree":{"sha":"b35133245de66b263f31754a798c66e965e02010","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/b35133245de66b263f31754a798c66e965e02010"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/1cf5357477d956a755b3aedf2b2a83d1c442521c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/1cf5357477d956a755b3aedf2b2a83d1c442521c","html_url":"https://github.com/y0-causal-inference/y0/commit/1cf5357477d956a755b3aedf2b2a83d1c442521c","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/1cf5357477d956a755b3aedf2b2a83d1c442521c/comments","author":null,"committer":null,"parents":[{"sha":"8362c7e16ed4018296c5bd8e123f1cfbb84c36c0","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/8362c7e16ed4018296c5bd8e123f1cfbb84c36c0","html_url":"https://github.com/y0-causal-inference/y0/commit/8362c7e16ed4018296c5bd8e123f1cfbb84c36c0"}]},{"sha":"9b88918b57554122c8a4254c05aa50a86429a6b3","node_id":"C_kwDOE5hB_NoAKDliODg5MThiNTc1NTQxMjJjOGE0MjU0YzA1YWE1MGE4NjQyOWE2YjM","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T16:34:13Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T16:34:13Z"},"message":"added + canonicalize for trso returns","tree":{"sha":"7acab93e8177418899a1eee8be9245ab9b9063be","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/7acab93e8177418899a1eee8be9245ab9b9063be"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/9b88918b57554122c8a4254c05aa50a86429a6b3","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/9b88918b57554122c8a4254c05aa50a86429a6b3","html_url":"https://github.com/y0-causal-inference/y0/commit/9b88918b57554122c8a4254c05aa50a86429a6b3","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/9b88918b57554122c8a4254c05aa50a86429a6b3/comments","author":null,"committer":null,"parents":[{"sha":"1cf5357477d956a755b3aedf2b2a83d1c442521c","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/1cf5357477d956a755b3aedf2b2a83d1c442521c","html_url":"https://github.com/y0-causal-inference/y0/commit/1cf5357477d956a755b3aedf2b2a83d1c442521c"}]},{"sha":"93f1998b93a01c0a141d0efe569e8f2edbeaf32a","node_id":"C_kwDOE5hB_NoAKDkzZjE5OThiOTNhMDFjMGExNDFkMGVmZTU2OWU4ZjJlZGJlYWYzMmE","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T16:34:54Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T16:34:54Z"},"message":"added + activate interventions to the trso algorithm","tree":{"sha":"5d204c9c2ed95129bca0d54ff0884103bd6264fc","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/5d204c9c2ed95129bca0d54ff0884103bd6264fc"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/93f1998b93a01c0a141d0efe569e8f2edbeaf32a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/93f1998b93a01c0a141d0efe569e8f2edbeaf32a","html_url":"https://github.com/y0-causal-inference/y0/commit/93f1998b93a01c0a141d0efe569e8f2edbeaf32a","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/93f1998b93a01c0a141d0efe569e8f2edbeaf32a/comments","author":null,"committer":null,"parents":[{"sha":"9b88918b57554122c8a4254c05aa50a86429a6b3","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/9b88918b57554122c8a4254c05aa50a86429a6b3","html_url":"https://github.com/y0-causal-inference/y0/commit/9b88918b57554122c8a4254c05aa50a86429a6b3"}]},{"sha":"1b518ef21631b971c092129614a709652ab32323","node_id":"C_kwDOE5hB_NoAKDFiNTE4ZWYyMTYzMWI5NzFjMDkyMTI5NjE0YTcwOTY1MmFiMzIzMjM","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T17:39:34Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T17:39:34Z"},"message":"Added + intervene_on_target to dsl","tree":{"sha":"550a1bb2765b0d7e2251423097c78ebfedbfab70","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/550a1bb2765b0d7e2251423097c78ebfedbfab70"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/1b518ef21631b971c092129614a709652ab32323","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/1b518ef21631b971c092129614a709652ab32323","html_url":"https://github.com/y0-causal-inference/y0/commit/1b518ef21631b971c092129614a709652ab32323","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/1b518ef21631b971c092129614a709652ab32323/comments","author":null,"committer":null,"parents":[{"sha":"93f1998b93a01c0a141d0efe569e8f2edbeaf32a","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/93f1998b93a01c0a141d0efe569e8f2edbeaf32a","html_url":"https://github.com/y0-causal-inference/y0/commit/93f1998b93a01c0a141d0efe569e8f2edbeaf32a"}]},{"sha":"3360527eb461de14d0e23b1287fdc2bb5cdc07c6","node_id":"C_kwDOE5hB_NoAKDMzNjA1MjdlYjQ2MWRlMTRkMGUyM2IxMjg3ZmRjMmJiNWNkYzA3YzY","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T20:42:22Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T20:42:22Z"},"message":"Added + line 9 tests","tree":{"sha":"4c14070188bbd113059484a59fd34a2e775d3af4","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/4c14070188bbd113059484a59fd34a2e775d3af4"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/3360527eb461de14d0e23b1287fdc2bb5cdc07c6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3360527eb461de14d0e23b1287fdc2bb5cdc07c6","html_url":"https://github.com/y0-causal-inference/y0/commit/3360527eb461de14d0e23b1287fdc2bb5cdc07c6","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3360527eb461de14d0e23b1287fdc2bb5cdc07c6/comments","author":null,"committer":null,"parents":[{"sha":"1b518ef21631b971c092129614a709652ab32323","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/1b518ef21631b971c092129614a709652ab32323","html_url":"https://github.com/y0-causal-inference/y0/commit/1b518ef21631b971c092129614a709652ab32323"}]},{"sha":"2b8f89d0ac13bda79969a315cc29088056c24b63","node_id":"C_kwDOE5hB_NoAKDJiOGY4OWQwYWMxM2JkYTc5OTY5YTMxNWNjMjkwODgwNTZjMjRiNjM","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T20:45:26Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T20:45:26Z"},"message":"Additional + line 9 tests","tree":{"sha":"4487ee71138e7516f16f8dcba42a60c756365389","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/4487ee71138e7516f16f8dcba42a60c756365389"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/2b8f89d0ac13bda79969a315cc29088056c24b63","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/2b8f89d0ac13bda79969a315cc29088056c24b63","html_url":"https://github.com/y0-causal-inference/y0/commit/2b8f89d0ac13bda79969a315cc29088056c24b63","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/2b8f89d0ac13bda79969a315cc29088056c24b63/comments","author":null,"committer":null,"parents":[{"sha":"3360527eb461de14d0e23b1287fdc2bb5cdc07c6","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3360527eb461de14d0e23b1287fdc2bb5cdc07c6","html_url":"https://github.com/y0-causal-inference/y0/commit/3360527eb461de14d0e23b1287fdc2bb5cdc07c6"}]},{"sha":"aee132aa0c8b27c4b77ff9be446569df02b70eee","node_id":"C_kwDOE5hB_NoAKGFlZTEzMmFhMGM4YjI3YzRiNzdmZjliZTQ0NjU2OWRmMDJiNzBlZWU","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T21:15:05Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T21:15:05Z"},"message":"Edits + to line 10","tree":{"sha":"f6a7e99687473f4d5079960d652347fb55a02c33","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/f6a7e99687473f4d5079960d652347fb55a02c33"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/aee132aa0c8b27c4b77ff9be446569df02b70eee","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/aee132aa0c8b27c4b77ff9be446569df02b70eee","html_url":"https://github.com/y0-causal-inference/y0/commit/aee132aa0c8b27c4b77ff9be446569df02b70eee","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/aee132aa0c8b27c4b77ff9be446569df02b70eee/comments","author":null,"committer":null,"parents":[{"sha":"2b8f89d0ac13bda79969a315cc29088056c24b63","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/2b8f89d0ac13bda79969a315cc29088056c24b63","html_url":"https://github.com/y0-causal-inference/y0/commit/2b8f89d0ac13bda79969a315cc29088056c24b63"}]},{"sha":"4a35f0fabc5dc13feadd532354f989da99c4f706","node_id":"C_kwDOE5hB_NoAKDRhMzVmMGZhYmM1ZGMxM2ZlYWRkNTMyMzU0Zjk4OWRhOTljNGY3MDY","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T21:25:25Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-24T21:25:25Z"},"message":"some + flake8 fixes","tree":{"sha":"e6c9a47c07339eb195c74861fa25c7a0fdde2f27","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/e6c9a47c07339eb195c74861fa25c7a0fdde2f27"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/4a35f0fabc5dc13feadd532354f989da99c4f706","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/4a35f0fabc5dc13feadd532354f989da99c4f706","html_url":"https://github.com/y0-causal-inference/y0/commit/4a35f0fabc5dc13feadd532354f989da99c4f706","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/4a35f0fabc5dc13feadd532354f989da99c4f706/comments","author":null,"committer":null,"parents":[{"sha":"aee132aa0c8b27c4b77ff9be446569df02b70eee","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/aee132aa0c8b27c4b77ff9be446569df02b70eee","html_url":"https://github.com/y0-causal-inference/y0/commit/aee132aa0c8b27c4b77ff9be446569df02b70eee"}]},{"sha":"926aa6e663bb2b10bdd6164d7b6f93641d0961b8","node_id":"C_kwDOE5hB_NoAKDkyNmFhNmU2NjNiYjJiMTBiZGQ2MTY0ZDdiNmY5MzY0MWQwOTYxYjg","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-25T13:41:08Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-25T13:41:08Z"},"message":"Docstring + fixes","tree":{"sha":"16c2b4a2db790bc2aa361bbe8f00e1082741db46","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/16c2b4a2db790bc2aa361bbe8f00e1082741db46"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/926aa6e663bb2b10bdd6164d7b6f93641d0961b8","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/926aa6e663bb2b10bdd6164d7b6f93641d0961b8","html_url":"https://github.com/y0-causal-inference/y0/commit/926aa6e663bb2b10bdd6164d7b6f93641d0961b8","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/926aa6e663bb2b10bdd6164d7b6f93641d0961b8/comments","author":null,"committer":null,"parents":[{"sha":"4a35f0fabc5dc13feadd532354f989da99c4f706","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/4a35f0fabc5dc13feadd532354f989da99c4f706","html_url":"https://github.com/y0-causal-inference/y0/commit/4a35f0fabc5dc13feadd532354f989da99c4f706"}]},{"sha":"27e747ab289488c298faf5dd728e37aeed36d7db","node_id":"C_kwDOE5hB_NoAKDI3ZTc0N2FiMjg5NDg4YzI5OGZhZjVkZDcyOGUzN2FlZWQzNmQ3ZGI","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-25T13:53:50Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-25T13:53:50Z"},"message":"More + Docstring fixes","tree":{"sha":"e5dd0d3d7afacdcceeb7635a10feff6fc72d132b","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/e5dd0d3d7afacdcceeb7635a10feff6fc72d132b"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/27e747ab289488c298faf5dd728e37aeed36d7db","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/27e747ab289488c298faf5dd728e37aeed36d7db","html_url":"https://github.com/y0-causal-inference/y0/commit/27e747ab289488c298faf5dd728e37aeed36d7db","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/27e747ab289488c298faf5dd728e37aeed36d7db/comments","author":null,"committer":null,"parents":[{"sha":"926aa6e663bb2b10bdd6164d7b6f93641d0961b8","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/926aa6e663bb2b10bdd6164d7b6f93641d0961b8","html_url":"https://github.com/y0-causal-inference/y0/commit/926aa6e663bb2b10bdd6164d7b6f93641d0961b8"}]},{"sha":"29a047cde7b335eed898d49a104a45c9109f0d5e","node_id":"C_kwDOE5hB_NoAKDI5YTA0N2NkZTdiMzM1ZWVkODk4ZDQ5YTEwNGE0NWM5MTA5ZjBkNWU","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-25T14:00:58Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-25T14:00:58Z"},"message":"Docstring + fixes in graph.py","tree":{"sha":"2370dffa001440ac39658e03ca9b10adb2ae89f4","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/2370dffa001440ac39658e03ca9b10adb2ae89f4"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/29a047cde7b335eed898d49a104a45c9109f0d5e","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/29a047cde7b335eed898d49a104a45c9109f0d5e","html_url":"https://github.com/y0-causal-inference/y0/commit/29a047cde7b335eed898d49a104a45c9109f0d5e","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/29a047cde7b335eed898d49a104a45c9109f0d5e/comments","author":null,"committer":null,"parents":[{"sha":"27e747ab289488c298faf5dd728e37aeed36d7db","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/27e747ab289488c298faf5dd728e37aeed36d7db","html_url":"https://github.com/y0-causal-inference/y0/commit/27e747ab289488c298faf5dd728e37aeed36d7db"}]},{"sha":"44716e72f49dcd961366cf05c7a042ba005efe7b","node_id":"C_kwDOE5hB_NoAKDQ0NzE2ZTcyZjQ5ZGNkOTYxMzY2Y2YwNWM3YTA0MmJhMDA1ZWZlN2I","commit":{"author":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-29T19:37:00Z"},"committer":{"name":"njmmerrill","email":"nathaniel.merrill@pnnl.gov","date":"2023-08-29T19:37:00Z"},"message":"Working + tests for TRSO","tree":{"sha":"a249c5881439dfd45789edf848c0f69cac0f758f","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/a249c5881439dfd45789edf848c0f69cac0f758f"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/44716e72f49dcd961366cf05c7a042ba005efe7b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/44716e72f49dcd961366cf05c7a042ba005efe7b","html_url":"https://github.com/y0-causal-inference/y0/commit/44716e72f49dcd961366cf05c7a042ba005efe7b","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/44716e72f49dcd961366cf05c7a042ba005efe7b/comments","author":null,"committer":null,"parents":[{"sha":"29a047cde7b335eed898d49a104a45c9109f0d5e","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/29a047cde7b335eed898d49a104a45c9109f0d5e","html_url":"https://github.com/y0-causal-inference/y0/commit/29a047cde7b335eed898d49a104a45c9109f0d5e"}]},{"sha":"2724658a77df923150a26775568bb1cb18cfc848","node_id":"C_kwDOE5hB_NoAKDI3MjQ2NThhNzdkZjkyMzE1MGEyNjc3NTU2OGJiMWNiMThjZmM4NDg","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-29T20:03:18Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-29T20:03:18Z"},"message":"Better + handling of subfuncs","tree":{"sha":"b5f2e56996d48039267240287a2fda3b880cfced","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/b5f2e56996d48039267240287a2fda3b880cfced"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/2724658a77df923150a26775568bb1cb18cfc848","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/2724658a77df923150a26775568bb1cb18cfc848","html_url":"https://github.com/y0-causal-inference/y0/commit/2724658a77df923150a26775568bb1cb18cfc848","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/2724658a77df923150a26775568bb1cb18cfc848/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"44716e72f49dcd961366cf05c7a042ba005efe7b","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/44716e72f49dcd961366cf05c7a042ba005efe7b","html_url":"https://github.com/y0-causal-inference/y0/commit/44716e72f49dcd961366cf05c7a042ba005efe7b"}]},{"sha":"1e6c2dfc722db0da7743b9ff99d0ca3a82f202ab","node_id":"C_kwDOE5hB_NoAKDFlNmMyZGZjNzIyZGIwZGE3NzQzYjlmZjk5ZDBjYTNhODJmMjAyYWI","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-29T20:15:13Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-29T20:15:13Z"},"message":"Update + bug in bayes_expand","tree":{"sha":"01014ae9f34feac562b5234692a0c9e611b56cce","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/01014ae9f34feac562b5234692a0c9e611b56cce"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/1e6c2dfc722db0da7743b9ff99d0ca3a82f202ab","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/1e6c2dfc722db0da7743b9ff99d0ca3a82f202ab","html_url":"https://github.com/y0-causal-inference/y0/commit/1e6c2dfc722db0da7743b9ff99d0ca3a82f202ab","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/1e6c2dfc722db0da7743b9ff99d0ca3a82f202ab/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"2724658a77df923150a26775568bb1cb18cfc848","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/2724658a77df923150a26775568bb1cb18cfc848","html_url":"https://github.com/y0-causal-inference/y0/commit/2724658a77df923150a26775568bb1cb18cfc848"}]},{"sha":"dbf015e742ff35f5c32077ce6623c982ce491014","node_id":"C_kwDOE5hB_NoAKGRiZjAxNWU3NDJmZjM1ZjVjMzIwNzdjZTY2MjNjOTgyY2U0OTEwMTQ","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-29T20:24:21Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-29T20:24:21Z"},"message":"Fix + summing","tree":{"sha":"f7eefb00a7d27c54c7c776d0bd56e217b65ba77a","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/f7eefb00a7d27c54c7c776d0bd56e217b65ba77a"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/dbf015e742ff35f5c32077ce6623c982ce491014","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/dbf015e742ff35f5c32077ce6623c982ce491014","html_url":"https://github.com/y0-causal-inference/y0/commit/dbf015e742ff35f5c32077ce6623c982ce491014","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/dbf015e742ff35f5c32077ce6623c982ce491014/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"1e6c2dfc722db0da7743b9ff99d0ca3a82f202ab","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/1e6c2dfc722db0da7743b9ff99d0ca3a82f202ab","html_url":"https://github.com/y0-causal-inference/y0/commit/1e6c2dfc722db0da7743b9ff99d0ca3a82f202ab"}]},{"sha":"0cfe5536fd073ef42e3c7e8b3d92342beb0a300a","node_id":"C_kwDOE5hB_NoAKDBjZmU1NTM2ZmQwNzNlZjQyZTNjN2U4YjNkOTIzNDJiZWIwYTMwMGE","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-29T20:42:00Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-29T20:42:00Z"},"message":"Update + dsl.py","tree":{"sha":"423e84971c60896750ebd809d82b138bf8a98816","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/423e84971c60896750ebd809d82b138bf8a98816"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/0cfe5536fd073ef42e3c7e8b3d92342beb0a300a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/0cfe5536fd073ef42e3c7e8b3d92342beb0a300a","html_url":"https://github.com/y0-causal-inference/y0/commit/0cfe5536fd073ef42e3c7e8b3d92342beb0a300a","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/0cfe5536fd073ef42e3c7e8b3d92342beb0a300a/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"dbf015e742ff35f5c32077ce6623c982ce491014","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/dbf015e742ff35f5c32077ce6623c982ce491014","html_url":"https://github.com/y0-causal-inference/y0/commit/dbf015e742ff35f5c32077ce6623c982ce491014"}]},{"sha":"19aa0cba5cf788536c6a726e80029712cbe8f002","node_id":"C_kwDOE5hB_NoAKDE5YWEwY2JhNWNmNzg4NTM2YzZhNzI2ZTgwMDI5NzEyY2JlOGYwMDI","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-29T20:43:40Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-29T20:43:40Z"},"message":"Update + dsl.py","tree":{"sha":"7f6e84ca7f80ab7479b1fab23fc1277f6b4bb46b","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/7f6e84ca7f80ab7479b1fab23fc1277f6b4bb46b"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/19aa0cba5cf788536c6a726e80029712cbe8f002","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/19aa0cba5cf788536c6a726e80029712cbe8f002","html_url":"https://github.com/y0-causal-inference/y0/commit/19aa0cba5cf788536c6a726e80029712cbe8f002","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/19aa0cba5cf788536c6a726e80029712cbe8f002/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"0cfe5536fd073ef42e3c7e8b3d92342beb0a300a","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/0cfe5536fd073ef42e3c7e8b3d92342beb0a300a","html_url":"https://github.com/y0-causal-inference/y0/commit/0cfe5536fd073ef42e3c7e8b3d92342beb0a300a"}]},{"sha":"f29a7c56ca475e2ba6da9f9dc3e5ecdde2b188da","node_id":"C_kwDOE5hB_NoAKGYyOWE3YzU2Y2E0NzVlMmJhNmRhOWY5ZGMzZTVlY2RkZTJiMTg4ZGE","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-29T20:53:39Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-29T20:53:39Z"},"message":"Update + dsl.py","tree":{"sha":"39f0be6cc82b8d550d26ac13ecfd76e115a5202c","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/39f0be6cc82b8d550d26ac13ecfd76e115a5202c"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/f29a7c56ca475e2ba6da9f9dc3e5ecdde2b188da","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/f29a7c56ca475e2ba6da9f9dc3e5ecdde2b188da","html_url":"https://github.com/y0-causal-inference/y0/commit/f29a7c56ca475e2ba6da9f9dc3e5ecdde2b188da","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/f29a7c56ca475e2ba6da9f9dc3e5ecdde2b188da/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"19aa0cba5cf788536c6a726e80029712cbe8f002","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/19aa0cba5cf788536c6a726e80029712cbe8f002","html_url":"https://github.com/y0-causal-inference/y0/commit/19aa0cba5cf788536c6a726e80029712cbe8f002"}]},{"sha":"3933cd3a4c245343fb29343f69a0da8f6182f806","node_id":"C_kwDOE5hB_NoAKDM5MzNjZDNhNGMyNDUzNDNmYjI5MzQzZjY5YTBkYThmNjE4MmY4MDY","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-29T21:30:55Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-29T21:30:55Z"},"message":"Merge + branch ''main'' into transport","tree":{"sha":"ba76bb5ea709854ee79413bfeef01e9a68783ba4","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/ba76bb5ea709854ee79413bfeef01e9a68783ba4"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/3933cd3a4c245343fb29343f69a0da8f6182f806","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3933cd3a4c245343fb29343f69a0da8f6182f806","html_url":"https://github.com/y0-causal-inference/y0/commit/3933cd3a4c245343fb29343f69a0da8f6182f806","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3933cd3a4c245343fb29343f69a0da8f6182f806/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"f29a7c56ca475e2ba6da9f9dc3e5ecdde2b188da","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/f29a7c56ca475e2ba6da9f9dc3e5ecdde2b188da","html_url":"https://github.com/y0-causal-inference/y0/commit/f29a7c56ca475e2ba6da9f9dc3e5ecdde2b188da"},{"sha":"856c290ab9e8f682ac552eb25be8c71aa376db79","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/856c290ab9e8f682ac552eb25be8c71aa376db79","html_url":"https://github.com/y0-causal-inference/y0/commit/856c290ab9e8f682ac552eb25be8c71aa376db79"}]},{"sha":"582f7a5e3600304403088fa11365769ea12db578","node_id":"C_kwDOE5hB_NoAKDU4MmY3YTVlMzYwMDMwNDQwMzA4OGZhMTEzNjU3NjllYTEyZGI1Nzg","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T07:24:00Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T07:24:00Z"},"message":"Merge + branch ''main'' into transport","tree":{"sha":"e08271fb3a7864d8bf1d7407f1d4900fc7c6397a","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/e08271fb3a7864d8bf1d7407f1d4900fc7c6397a"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/582f7a5e3600304403088fa11365769ea12db578","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/582f7a5e3600304403088fa11365769ea12db578","html_url":"https://github.com/y0-causal-inference/y0/commit/582f7a5e3600304403088fa11365769ea12db578","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/582f7a5e3600304403088fa11365769ea12db578/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"3933cd3a4c245343fb29343f69a0da8f6182f806","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3933cd3a4c245343fb29343f69a0da8f6182f806","html_url":"https://github.com/y0-causal-inference/y0/commit/3933cd3a4c245343fb29343f69a0da8f6182f806"},{"sha":"2024f29f5dc000d9b1854b8e3cd2c55ce4b55fad","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/2024f29f5dc000d9b1854b8e3cd2c55ce4b55fad","html_url":"https://github.com/y0-causal-inference/y0/commit/2024f29f5dc000d9b1854b8e3cd2c55ce4b55fad"}]},{"sha":"ecde32344a5ff0dd85112bbbe3ddcf3cf296a5f9","node_id":"C_kwDOE5hB_NoAKGVjZGUzMjM0NGE1ZmYwZGQ4NTExMmJiYmUzZGRjZjNjZjI5NmE1Zjk","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T07:46:59Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T07:46:59Z"},"message":"Merge + branch ''main'' into transport","tree":{"sha":"b21183eb2f4b7802db90413b2388c7e8c315647f","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/b21183eb2f4b7802db90413b2388c7e8c315647f"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/ecde32344a5ff0dd85112bbbe3ddcf3cf296a5f9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ecde32344a5ff0dd85112bbbe3ddcf3cf296a5f9","html_url":"https://github.com/y0-causal-inference/y0/commit/ecde32344a5ff0dd85112bbbe3ddcf3cf296a5f9","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ecde32344a5ff0dd85112bbbe3ddcf3cf296a5f9/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"582f7a5e3600304403088fa11365769ea12db578","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/582f7a5e3600304403088fa11365769ea12db578","html_url":"https://github.com/y0-causal-inference/y0/commit/582f7a5e3600304403088fa11365769ea12db578"},{"sha":"fc8a1d1b49d8851c4b2a2820f405c0bedcc1e9d7","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/fc8a1d1b49d8851c4b2a2820f405c0bedcc1e9d7","html_url":"https://github.com/y0-causal-inference/y0/commit/fc8a1d1b49d8851c4b2a2820f405c0bedcc1e9d7"}]},{"sha":"3d44bc6f338a0207a0e078fe593242e8c78f9a20","node_id":"C_kwDOE5hB_NoAKDNkNDRiYzZmMzM4YTAyMDdhMGUwNzhmZTU5MzI0MmU4Yzc4ZjlhMjA","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T08:23:41Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T08:23:41Z"},"message":"Clean + up canonicalization of sums","tree":{"sha":"2de6b7858106107151cbec115ff8f1537ffffacb","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/2de6b7858106107151cbec115ff8f1537ffffacb"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/3d44bc6f338a0207a0e078fe593242e8c78f9a20","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3d44bc6f338a0207a0e078fe593242e8c78f9a20","html_url":"https://github.com/y0-causal-inference/y0/commit/3d44bc6f338a0207a0e078fe593242e8c78f9a20","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3d44bc6f338a0207a0e078fe593242e8c78f9a20/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"ecde32344a5ff0dd85112bbbe3ddcf3cf296a5f9","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/ecde32344a5ff0dd85112bbbe3ddcf3cf296a5f9","html_url":"https://github.com/y0-causal-inference/y0/commit/ecde32344a5ff0dd85112bbbe3ddcf3cf296a5f9"}]},{"sha":"339964c43af8e08fa3687d667ec297441f0ff0ad","node_id":"C_kwDOE5hB_NoAKDMzOTk2NGM0M2FmOGUwOGZhMzY4N2Q2NjdlYzI5NzQ0MWYwZmYwYWQ","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T08:30:03Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T08:30:03Z"},"message":"Update + dsl.py","tree":{"sha":"956884aee247ea5a7d17a0b176f69ed749a96e9e","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/956884aee247ea5a7d17a0b176f69ed749a96e9e"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/339964c43af8e08fa3687d667ec297441f0ff0ad","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/339964c43af8e08fa3687d667ec297441f0ff0ad","html_url":"https://github.com/y0-causal-inference/y0/commit/339964c43af8e08fa3687d667ec297441f0ff0ad","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/339964c43af8e08fa3687d667ec297441f0ff0ad/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"3d44bc6f338a0207a0e078fe593242e8c78f9a20","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/3d44bc6f338a0207a0e078fe593242e8c78f9a20","html_url":"https://github.com/y0-causal-inference/y0/commit/3d44bc6f338a0207a0e078fe593242e8c78f9a20"}]},{"sha":"0bc68ad7275ad24a50a485c82e2ce2f30941e5de","node_id":"C_kwDOE5hB_NoAKDBiYzY4YWQ3Mjc1YWQyNGE1MGE0ODVjODJlMmNlMmYzMDk0MWU1ZGU","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T08:50:29Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T08:50:29Z"},"message":"Handle + simplifying sums with counterfactuals","tree":{"sha":"8d88e1f7ba071adafe5220363d0caf3553febe3f","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/8d88e1f7ba071adafe5220363d0caf3553febe3f"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/0bc68ad7275ad24a50a485c82e2ce2f30941e5de","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/0bc68ad7275ad24a50a485c82e2ce2f30941e5de","html_url":"https://github.com/y0-causal-inference/y0/commit/0bc68ad7275ad24a50a485c82e2ce2f30941e5de","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/0bc68ad7275ad24a50a485c82e2ce2f30941e5de/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"339964c43af8e08fa3687d667ec297441f0ff0ad","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/339964c43af8e08fa3687d667ec297441f0ff0ad","html_url":"https://github.com/y0-causal-inference/y0/commit/339964c43af8e08fa3687d667ec297441f0ff0ad"}]},{"sha":"01ac98e84512d46b2bbeb8868244c9d78688be82","node_id":"C_kwDOE5hB_NoAKDAxYWM5OGU4NDUxMmQ0NmIyYmJlYjg4NjgyNDRjOWQ3ODY4OGJlODI","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T09:02:59Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T09:02:59Z"},"message":"Improve + text output of probabilities\n\nThis makes debugging easier","tree":{"sha":"2255772d93f52b41bb11ab7d33cc37f62851ed0d","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/2255772d93f52b41bb11ab7d33cc37f62851ed0d"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/01ac98e84512d46b2bbeb8868244c9d78688be82","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/01ac98e84512d46b2bbeb8868244c9d78688be82","html_url":"https://github.com/y0-causal-inference/y0/commit/01ac98e84512d46b2bbeb8868244c9d78688be82","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/01ac98e84512d46b2bbeb8868244c9d78688be82/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"0bc68ad7275ad24a50a485c82e2ce2f30941e5de","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/0bc68ad7275ad24a50a485c82e2ce2f30941e5de","html_url":"https://github.com/y0-causal-inference/y0/commit/0bc68ad7275ad24a50a485c82e2ce2f30941e5de"}]},{"sha":"177f2e5643a45b170fb10565b41a2ffa482a0e73","node_id":"C_kwDOE5hB_NoAKDE3N2YyZTU2NDNhNDViMTcwZmIxMDU2NWI0MWEyZmZhNDgyYTBlNzM","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T09:07:29Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T09:07:29Z"},"message":"Fix + ordering inside products","tree":{"sha":"fd5e68d7cb55df901afb9d8170746c9cf3d37035","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/fd5e68d7cb55df901afb9d8170746c9cf3d37035"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/177f2e5643a45b170fb10565b41a2ffa482a0e73","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/177f2e5643a45b170fb10565b41a2ffa482a0e73","html_url":"https://github.com/y0-causal-inference/y0/commit/177f2e5643a45b170fb10565b41a2ffa482a0e73","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/177f2e5643a45b170fb10565b41a2ffa482a0e73/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"01ac98e84512d46b2bbeb8868244c9d78688be82","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/01ac98e84512d46b2bbeb8868244c9d78688be82","html_url":"https://github.com/y0-causal-inference/y0/commit/01ac98e84512d46b2bbeb8868244c9d78688be82"}]},{"sha":"003907a164260d1a97264529f53540a21bc422d4","node_id":"C_kwDOE5hB_NoAKDAwMzkwN2ExNjQyNjBkMWE5NzI2NDUyOWY1MzU0MGEyMWJjNDIyZDQ","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T09:07:37Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T09:07:37Z"},"message":"Update + type notes","tree":{"sha":"b6aec05e0a5a59a42a385c9666378a8729de6f57","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/b6aec05e0a5a59a42a385c9666378a8729de6f57"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/003907a164260d1a97264529f53540a21bc422d4","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/003907a164260d1a97264529f53540a21bc422d4","html_url":"https://github.com/y0-causal-inference/y0/commit/003907a164260d1a97264529f53540a21bc422d4","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/003907a164260d1a97264529f53540a21bc422d4/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"177f2e5643a45b170fb10565b41a2ffa482a0e73","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/177f2e5643a45b170fb10565b41a2ffa482a0e73","html_url":"https://github.com/y0-causal-inference/y0/commit/177f2e5643a45b170fb10565b41a2ffa482a0e73"}]},{"sha":"84295600d136e32f82ec37a4e5d2d5f3c9bcdf4b","node_id":"C_kwDOE5hB_NoAKDg0Mjk1NjAwZDEzNmUzMmY4MmVjMzdhNGU1ZDJkNWYzYzliY2RmNGI","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T09:12:25Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T09:12:25Z"},"message":"Update + transport.py","tree":{"sha":"fefd00ccd2fd66dce0c3655e4d020986b01d7247","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/fefd00ccd2fd66dce0c3655e4d020986b01d7247"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/84295600d136e32f82ec37a4e5d2d5f3c9bcdf4b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/84295600d136e32f82ec37a4e5d2d5f3c9bcdf4b","html_url":"https://github.com/y0-causal-inference/y0/commit/84295600d136e32f82ec37a4e5d2d5f3c9bcdf4b","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/84295600d136e32f82ec37a4e5d2d5f3c9bcdf4b/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"003907a164260d1a97264529f53540a21bc422d4","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/003907a164260d1a97264529f53540a21bc422d4","html_url":"https://github.com/y0-causal-inference/y0/commit/003907a164260d1a97264529f53540a21bc422d4"}]},{"sha":"07620c296d085a2a508a2aecf0bfe7969a5a2260","node_id":"C_kwDOE5hB_NoAKDA3NjIwYzI5NmQwODVhMmE1MDhhMmFlY2YwYmZlNzk2OWE1YTIyNjA","commit":{"author":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T09:18:53Z"},"committer":{"name":"Charles + Tapley Hoyt","email":"cthoyt@gmail.com","date":"2023-08-30T09:18:53Z"},"message":"Flake8 + and mypy cleanup","tree":{"sha":"baa3fd48e255d5f0b04a36445175f0a2a4b4fe67","url":"https://api.github.com/repos/y0-causal-inference/y0/git/trees/baa3fd48e255d5f0b04a36445175f0a2a4b4fe67"},"url":"https://api.github.com/repos/y0-causal-inference/y0/git/commits/07620c296d085a2a508a2aecf0bfe7969a5a2260","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/y0-causal-inference/y0/commits/07620c296d085a2a508a2aecf0bfe7969a5a2260","html_url":"https://github.com/y0-causal-inference/y0/commit/07620c296d085a2a508a2aecf0bfe7969a5a2260","comments_url":"https://api.github.com/repos/y0-causal-inference/y0/commits/07620c296d085a2a508a2aecf0bfe7969a5a2260/comments","author":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"committer":{"login":"cthoyt","id":5069736,"node_id":"MDQ6VXNlcjUwNjk3MzY=","avatar_url":"https://avatars.githubusercontent.com/u/5069736?v=4","gravatar_id":"","url":"https://api.github.com/users/cthoyt","html_url":"https://github.com/cthoyt","followers_url":"https://api.github.com/users/cthoyt/followers","following_url":"https://api.github.com/users/cthoyt/following{/other_user}","gists_url":"https://api.github.com/users/cthoyt/gists{/gist_id}","starred_url":"https://api.github.com/users/cthoyt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cthoyt/subscriptions","organizations_url":"https://api.github.com/users/cthoyt/orgs","repos_url":"https://api.github.com/users/cthoyt/repos","events_url":"https://api.github.com/users/cthoyt/events{/privacy}","received_events_url":"https://api.github.com/users/cthoyt/received_events","type":"User","site_admin":false},"parents":[{"sha":"84295600d136e32f82ec37a4e5d2d5f3c9bcdf4b","url":"https://api.github.com/repos/y0-causal-inference/y0/commits/84295600d136e32f82ec37a4e5d2d5f3c9bcdf4b","html_url":"https://github.com/y0-causal-inference/y0/commit/84295600d136e32f82ec37a4e5d2d5f3c9bcdf4b"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:19:00 GMT + ETag: + - W/"78a35085295eda4028984de008fb5f42f17831fe7bab6fd7814199b848296bc5" + Last-Modified: + - Mon, 04 Sep 2023 16:56:11 GMT + Link: + - ; + rel="next", ; + rel="last" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FFB5:04E5:124719:131BD2:64F61184 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4981' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '19' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_fail.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_fail.yaml new file mode 100644 index 0000000000..80b226bbeb --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_fail.yaml @@ -0,0 +1,75 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecove2e/example-python/pulls/100 + response: + content: '{"message":"Not Found","documentation_url":"https://docs.github.com/rest/pulls/pulls#get-a-pull-request"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 18 Aug 2023 20:27:44 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - E3BB:25EE:12C1DB4:13EE268:64DFD440 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4995' + X-RateLimit-Reset: + - '1692393273' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '5' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-08-25 20:13:04 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_from_fork.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_from_fork.yaml new file mode 100644 index 0000000000..3981378993 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_from_fork.yaml @@ -0,0 +1,190 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/codecov-api/pulls/285 + response: + content: '{"url":"https://api.github.com/repos/codecov/codecov-api/pulls/285","id":1626444498,"node_id":"PR_kwDOJ8oEEM5g8ZLS","html_url":"https://github.com/codecov/codecov-api/pull/285","diff_url":"https://github.com/codecov/codecov-api/pull/285.diff","patch_url":"https://github.com/codecov/codecov-api/pull/285.patch","issue_url":"https://api.github.com/repos/codecov/codecov-api/issues/285","number":285,"state":"open","locked":false,"title":"chore: + Switch to Python 3.12","user":{"login":"FraBle","id":1584268,"node_id":"MDQ6VXNlcjE1ODQyNjg=","avatar_url":"https://avatars.githubusercontent.com/u/1584268?v=4","gravatar_id":"","url":"https://api.github.com/users/FraBle","html_url":"https://github.com/FraBle","followers_url":"https://api.github.com/users/FraBle/followers","following_url":"https://api.github.com/users/FraBle/following{/other_user}","gists_url":"https://api.github.com/users/FraBle/gists{/gist_id}","starred_url":"https://api.github.com/users/FraBle/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/FraBle/subscriptions","organizations_url":"https://api.github.com/users/FraBle/orgs","repos_url":"https://api.github.com/users/FraBle/repos","events_url":"https://api.github.com/users/FraBle/events{/privacy}","received_events_url":"https://api.github.com/users/FraBle/received_events","type":"User","site_admin":false},"body":"### + Purpose/Motivation\r\nSwitches the API to Python 3.12.\r\n\r\n### Links to relevant + tickets\r\n\r\nhttps://github.com/codecov/engineering-team/issues/875\r\n\r\n### + Legal Boilerplate\r\n\r\nLook, I get it. The entity doing business as \"Sentry\" + was incorporated in the State of Delaware in 2015 as Functional Software, Inc. + In 2022 this entity acquired Codecov and as result Sentry is going to need some + rights from me in order to utilize my contributions in this PR. So here''s the + deal: I retain all rights, title and interest in and to my contributions, and + by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, + and redistribute my contributions, under Sentry''s choice of terms.\r\n","created_at":"2023-12-01T23:20:48Z","updated_at":"2023-12-01T23:21:51Z","closed_at":null,"merged_at":null,"merge_commit_sha":"7458689acf385c2cbe5fe09bd73536a50ef545c6","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/codecov/codecov-api/pulls/285/commits","review_comments_url":"https://api.github.com/repos/codecov/codecov-api/pulls/285/comments","review_comment_url":"https://api.github.com/repos/codecov/codecov-api/pulls/comments{/number}","comments_url":"https://api.github.com/repos/codecov/codecov-api/issues/285/comments","statuses_url":"https://api.github.com/repos/codecov/codecov-api/statuses/67a44e176ffd419f066c1cc34cff391e2a1304e2","head":{"label":"FraBle:python-3-12","ref":"python-3-12","sha":"67a44e176ffd419f066c1cc34cff391e2a1304e2","user":{"login":"FraBle","id":1584268,"node_id":"MDQ6VXNlcjE1ODQyNjg=","avatar_url":"https://avatars.githubusercontent.com/u/1584268?v=4","gravatar_id":"","url":"https://api.github.com/users/FraBle","html_url":"https://github.com/FraBle","followers_url":"https://api.github.com/users/FraBle/followers","following_url":"https://api.github.com/users/FraBle/following{/other_user}","gists_url":"https://api.github.com/users/FraBle/gists{/gist_id}","starred_url":"https://api.github.com/users/FraBle/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/FraBle/subscriptions","organizations_url":"https://api.github.com/users/FraBle/orgs","repos_url":"https://api.github.com/users/FraBle/repos","events_url":"https://api.github.com/users/FraBle/events{/privacy}","received_events_url":"https://api.github.com/users/FraBle/received_events","type":"User","site_admin":false},"repo":{"id":724832882,"node_id":"R_kgDOKzQScg","name":"codecov-api","full_name":"FraBle/codecov-api","private":false,"owner":{"login":"FraBle","id":1584268,"node_id":"MDQ6VXNlcjE1ODQyNjg=","avatar_url":"https://avatars.githubusercontent.com/u/1584268?v=4","gravatar_id":"","url":"https://api.github.com/users/FraBle","html_url":"https://github.com/FraBle","followers_url":"https://api.github.com/users/FraBle/followers","following_url":"https://api.github.com/users/FraBle/following{/other_user}","gists_url":"https://api.github.com/users/FraBle/gists{/gist_id}","starred_url":"https://api.github.com/users/FraBle/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/FraBle/subscriptions","organizations_url":"https://api.github.com/users/FraBle/orgs","repos_url":"https://api.github.com/users/FraBle/repos","events_url":"https://api.github.com/users/FraBle/events{/privacy}","received_events_url":"https://api.github.com/users/FraBle/received_events","type":"User","site_admin":false},"html_url":"https://github.com/FraBle/codecov-api","description":"Code + for the API of Codecov","fork":true,"url":"https://api.github.com/repos/FraBle/codecov-api","forks_url":"https://api.github.com/repos/FraBle/codecov-api/forks","keys_url":"https://api.github.com/repos/FraBle/codecov-api/keys{/key_id}","collaborators_url":"https://api.github.com/repos/FraBle/codecov-api/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/FraBle/codecov-api/teams","hooks_url":"https://api.github.com/repos/FraBle/codecov-api/hooks","issue_events_url":"https://api.github.com/repos/FraBle/codecov-api/issues/events{/number}","events_url":"https://api.github.com/repos/FraBle/codecov-api/events","assignees_url":"https://api.github.com/repos/FraBle/codecov-api/assignees{/user}","branches_url":"https://api.github.com/repos/FraBle/codecov-api/branches{/branch}","tags_url":"https://api.github.com/repos/FraBle/codecov-api/tags","blobs_url":"https://api.github.com/repos/FraBle/codecov-api/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/FraBle/codecov-api/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/FraBle/codecov-api/git/refs{/sha}","trees_url":"https://api.github.com/repos/FraBle/codecov-api/git/trees{/sha}","statuses_url":"https://api.github.com/repos/FraBle/codecov-api/statuses/{sha}","languages_url":"https://api.github.com/repos/FraBle/codecov-api/languages","stargazers_url":"https://api.github.com/repos/FraBle/codecov-api/stargazers","contributors_url":"https://api.github.com/repos/FraBle/codecov-api/contributors","subscribers_url":"https://api.github.com/repos/FraBle/codecov-api/subscribers","subscription_url":"https://api.github.com/repos/FraBle/codecov-api/subscription","commits_url":"https://api.github.com/repos/FraBle/codecov-api/commits{/sha}","git_commits_url":"https://api.github.com/repos/FraBle/codecov-api/git/commits{/sha}","comments_url":"https://api.github.com/repos/FraBle/codecov-api/comments{/number}","issue_comment_url":"https://api.github.com/repos/FraBle/codecov-api/issues/comments{/number}","contents_url":"https://api.github.com/repos/FraBle/codecov-api/contents/{+path}","compare_url":"https://api.github.com/repos/FraBle/codecov-api/compare/{base}...{head}","merges_url":"https://api.github.com/repos/FraBle/codecov-api/merges","archive_url":"https://api.github.com/repos/FraBle/codecov-api/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/FraBle/codecov-api/downloads","issues_url":"https://api.github.com/repos/FraBle/codecov-api/issues{/number}","pulls_url":"https://api.github.com/repos/FraBle/codecov-api/pulls{/number}","milestones_url":"https://api.github.com/repos/FraBle/codecov-api/milestones{/number}","notifications_url":"https://api.github.com/repos/FraBle/codecov-api/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/FraBle/codecov-api/labels{/name}","releases_url":"https://api.github.com/repos/FraBle/codecov-api/releases{/id}","deployments_url":"https://api.github.com/repos/FraBle/codecov-api/deployments","created_at":"2023-11-28T22:10:46Z","updated_at":"2023-11-28T22:10:46Z","pushed_at":"2023-12-01T23:21:49Z","git_url":"git://github.com/FraBle/codecov-api.git","ssh_url":"git@github.com:FraBle/codecov-api.git","clone_url":"https://github.com/FraBle/codecov-api.git","svn_url":"https://github.com/FraBle/codecov-api","homepage":null,"size":27481,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":0,"watchers":0,"default_branch":"main"}},"base":{"label":"codecov:main","ref":"main","sha":"109eea9a085f5856a20ae5f1714b8c4786c3327b","user":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"repo":{"id":667550736,"node_id":"R_kgDOJ8oEEA","name":"codecov-api","full_name":"codecov/codecov-api","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-api","description":"Code + for the API of Codecov","fork":false,"url":"https://api.github.com/repos/codecov/codecov-api","forks_url":"https://api.github.com/repos/codecov/codecov-api/forks","keys_url":"https://api.github.com/repos/codecov/codecov-api/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-api/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-api/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-api/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-api/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-api/events","assignees_url":"https://api.github.com/repos/codecov/codecov-api/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-api/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-api/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-api/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-api/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-api/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-api/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-api/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-api/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-api/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-api/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-api/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-api/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-api/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-api/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-api/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-api/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-api/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-api/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-api/merges","archive_url":"https://api.github.com/repos/codecov/codecov-api/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-api/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-api/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-api/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-api/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-api/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-api/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-api/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-api/deployments","created_at":"2023-07-17T19:06:58Z","updated_at":"2023-12-08T12:41:15Z","pushed_at":"2023-12-12T15:56:05Z","git_url":"git://github.com/codecov/codecov-api.git","ssh_url":"git@github.com:codecov/codecov-api.git","clone_url":"https://github.com/codecov/codecov-api.git","svn_url":"https://github.com/codecov/codecov-api","homepage":null,"size":27915,"stargazers_count":197,"watchers_count":197,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":21,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":14,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":21,"open_issues":14,"watchers":197,"default_branch":"main"}},"_links":{"self":{"href":"https://api.github.com/repos/codecov/codecov-api/pulls/285"},"html":{"href":"https://github.com/codecov/codecov-api/pull/285"},"issue":{"href":"https://api.github.com/repos/codecov/codecov-api/issues/285"},"comments":{"href":"https://api.github.com/repos/codecov/codecov-api/issues/285/comments"},"review_comments":{"href":"https://api.github.com/repos/codecov/codecov-api/pulls/285/comments"},"review_comment":{"href":"https://api.github.com/repos/codecov/codecov-api/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/codecov/codecov-api/pulls/285/commits"},"statuses":{"href":"https://api.github.com/repos/codecov/codecov-api/statuses/67a44e176ffd419f066c1cc34cff391e2a1304e2"}},"author_association":"CONTRIBUTOR","auto_merge":null,"active_lock_reason":null,"merged":false,"mergeable":true,"rebaseable":true,"mergeable_state":"behind","merged_by":null,"comments":0,"review_comments":0,"maintainer_can_modify":true,"commits":2,"additions":4,"deletions":6,"changed_files":3}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 12 Dec 2023 17:17:52 GMT + ETag: + - W/"75031a844ae3475a0255b2014cb4d7f96973216079c0123c4415f75234d5b626" + Last-Modified: + - Fri, 01 Dec 2023 23:21:51 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CF27:BC0A:16CF49:18099C:657895BF + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4987' + X-RateLimit-Reset: + - '1702401614' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '13' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-12-19 17:14:49 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/codecov-api/pulls/285/commits?page=1&per_page=100 + response: + content: '[{"sha":"24732c7c43912738228b2509fe73fa0b94255440","node_id":"C_kwDOKzQSctoAKDI0NzMyYzdjNDM5MTI3MzgyMjhiMjUwOWZlNzNmYTBiOTQyNTU0NDA","commit":{"author":{"name":"Frank + Blechschmidt","email":"frank.blechschmidt@lattice.com","date":"2023-12-01T23:19:54Z"},"committer":{"name":"Frank + Blechschmidt","email":"frank.blechschmidt@lattice.com","date":"2023-12-01T23:19:54Z"},"message":"chore: + Switch to Python 3.12","tree":{"sha":"0c5669987c62f010dfbded5d7e691b3a6fbb0e3b","url":"https://api.github.com/repos/codecov/codecov-api/git/trees/0c5669987c62f010dfbded5d7e691b3a6fbb0e3b"},"url":"https://api.github.com/repos/codecov/codecov-api/git/commits/24732c7c43912738228b2509fe73fa0b94255440","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\niHUEABYIAB0WIQSVjuhupFplVZZQvEi9MqB215bO9AUCZWpqGgAKCRC9MqB215bO\n9B9dAPsEc1epSl3Txv+A0/768ZmtbW+IoCKNIngtD99zJQuHMAEAykNO+OoEeWWC\nJGSAA3psVhbfTq0mZg1hbwUjI0J2tQE=\n=zVJX\n-----END + PGP SIGNATURE-----","payload":"tree 0c5669987c62f010dfbded5d7e691b3a6fbb0e3b\nparent + 109eea9a085f5856a20ae5f1714b8c4786c3327b\nauthor Frank Blechschmidt + 1701472794 -0800\ncommitter Frank Blechschmidt + 1701472794 -0800\n\nchore: Switch to Python 3.12\n"}},"url":"https://api.github.com/repos/codecov/codecov-api/commits/24732c7c43912738228b2509fe73fa0b94255440","html_url":"https://github.com/codecov/codecov-api/commit/24732c7c43912738228b2509fe73fa0b94255440","comments_url":"https://api.github.com/repos/codecov/codecov-api/commits/24732c7c43912738228b2509fe73fa0b94255440/comments","author":{"login":"FraBle","id":1584268,"node_id":"MDQ6VXNlcjE1ODQyNjg=","avatar_url":"https://avatars.githubusercontent.com/u/1584268?v=4","gravatar_id":"","url":"https://api.github.com/users/FraBle","html_url":"https://github.com/FraBle","followers_url":"https://api.github.com/users/FraBle/followers","following_url":"https://api.github.com/users/FraBle/following{/other_user}","gists_url":"https://api.github.com/users/FraBle/gists{/gist_id}","starred_url":"https://api.github.com/users/FraBle/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/FraBle/subscriptions","organizations_url":"https://api.github.com/users/FraBle/orgs","repos_url":"https://api.github.com/users/FraBle/repos","events_url":"https://api.github.com/users/FraBle/events{/privacy}","received_events_url":"https://api.github.com/users/FraBle/received_events","type":"User","site_admin":false},"committer":{"login":"FraBle","id":1584268,"node_id":"MDQ6VXNlcjE1ODQyNjg=","avatar_url":"https://avatars.githubusercontent.com/u/1584268?v=4","gravatar_id":"","url":"https://api.github.com/users/FraBle","html_url":"https://github.com/FraBle","followers_url":"https://api.github.com/users/FraBle/followers","following_url":"https://api.github.com/users/FraBle/following{/other_user}","gists_url":"https://api.github.com/users/FraBle/gists{/gist_id}","starred_url":"https://api.github.com/users/FraBle/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/FraBle/subscriptions","organizations_url":"https://api.github.com/users/FraBle/orgs","repos_url":"https://api.github.com/users/FraBle/repos","events_url":"https://api.github.com/users/FraBle/events{/privacy}","received_events_url":"https://api.github.com/users/FraBle/received_events","type":"User","site_admin":false},"parents":[{"sha":"109eea9a085f5856a20ae5f1714b8c4786c3327b","url":"https://api.github.com/repos/codecov/codecov-api/commits/109eea9a085f5856a20ae5f1714b8c4786c3327b","html_url":"https://github.com/codecov/codecov-api/commit/109eea9a085f5856a20ae5f1714b8c4786c3327b"}]},{"sha":"67a44e176ffd419f066c1cc34cff391e2a1304e2","node_id":"C_kwDOKzQSctoAKDY3YTQ0ZTE3NmZmZDQxOWYwNjZjMWNjMzRjZmYzOTFlMmExMzA0ZTI","commit":{"author":{"name":"Frank + Blechschmidt","email":"frank.blechschmidt@lattice.com","date":"2023-12-01T23:21:47Z"},"committer":{"name":"Frank + Blechschmidt","email":"frank.blechschmidt@lattice.com","date":"2023-12-01T23:21:47Z"},"message":"Add + note to readme","tree":{"sha":"61276344bbf52c071bbc03f105911755cc1a6263","url":"https://api.github.com/repos/codecov/codecov-api/git/trees/61276344bbf52c071bbc03f105911755cc1a6263"},"url":"https://api.github.com/repos/codecov/codecov-api/git/commits/67a44e176ffd419f066c1cc34cff391e2a1304e2","comment_count":0,"verification":{"verified":true,"reason":"valid","signature":"-----BEGIN + PGP SIGNATURE-----\n\niHUEABYIAB0WIQSVjuhupFplVZZQvEi9MqB215bO9AUCZWpqiwAKCRC9MqB215bO\n9IgdAP48j41v/TOEBuJ6Q3Jte8qBnNfyfWKpbK7Ah+O6a5JUsAEA07u4N0TdZoOT\nnNcDRYX7+VgV404rmdn4VsSN630d9Ag=\n=dqsE\n-----END + PGP SIGNATURE-----","payload":"tree 61276344bbf52c071bbc03f105911755cc1a6263\nparent + 24732c7c43912738228b2509fe73fa0b94255440\nauthor Frank Blechschmidt + 1701472907 -0800\ncommitter Frank Blechschmidt + 1701472907 -0800\n\nAdd note to readme\n"}},"url":"https://api.github.com/repos/codecov/codecov-api/commits/67a44e176ffd419f066c1cc34cff391e2a1304e2","html_url":"https://github.com/codecov/codecov-api/commit/67a44e176ffd419f066c1cc34cff391e2a1304e2","comments_url":"https://api.github.com/repos/codecov/codecov-api/commits/67a44e176ffd419f066c1cc34cff391e2a1304e2/comments","author":{"login":"FraBle","id":1584268,"node_id":"MDQ6VXNlcjE1ODQyNjg=","avatar_url":"https://avatars.githubusercontent.com/u/1584268?v=4","gravatar_id":"","url":"https://api.github.com/users/FraBle","html_url":"https://github.com/FraBle","followers_url":"https://api.github.com/users/FraBle/followers","following_url":"https://api.github.com/users/FraBle/following{/other_user}","gists_url":"https://api.github.com/users/FraBle/gists{/gist_id}","starred_url":"https://api.github.com/users/FraBle/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/FraBle/subscriptions","organizations_url":"https://api.github.com/users/FraBle/orgs","repos_url":"https://api.github.com/users/FraBle/repos","events_url":"https://api.github.com/users/FraBle/events{/privacy}","received_events_url":"https://api.github.com/users/FraBle/received_events","type":"User","site_admin":false},"committer":{"login":"FraBle","id":1584268,"node_id":"MDQ6VXNlcjE1ODQyNjg=","avatar_url":"https://avatars.githubusercontent.com/u/1584268?v=4","gravatar_id":"","url":"https://api.github.com/users/FraBle","html_url":"https://github.com/FraBle","followers_url":"https://api.github.com/users/FraBle/followers","following_url":"https://api.github.com/users/FraBle/following{/other_user}","gists_url":"https://api.github.com/users/FraBle/gists{/gist_id}","starred_url":"https://api.github.com/users/FraBle/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/FraBle/subscriptions","organizations_url":"https://api.github.com/users/FraBle/orgs","repos_url":"https://api.github.com/users/FraBle/repos","events_url":"https://api.github.com/users/FraBle/events{/privacy}","received_events_url":"https://api.github.com/users/FraBle/received_events","type":"User","site_admin":false},"parents":[{"sha":"24732c7c43912738228b2509fe73fa0b94255440","url":"https://api.github.com/repos/codecov/codecov-api/commits/24732c7c43912738228b2509fe73fa0b94255440","html_url":"https://github.com/codecov/codecov-api/commit/24732c7c43912738228b2509fe73fa0b94255440"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 12 Dec 2023 17:17:52 GMT + ETag: + - W/"0ed03bab2f3600d91a5c852473bf50d36aacef2f38fbf69665673e2859dbef55" + Last-Modified: + - Fri, 01 Dec 2023 23:21:51 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CF28:10533:157338:16AD8C:657895C0 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4986' + X-RateLimit-Reset: + - '1702401614' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '14' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-12-19 17:14:49 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_way_more_than_250_results.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_way_more_than_250_results.yaml new file mode 100644 index 0000000000..ff8a37f646 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_request_way_more_than_250_results.yaml @@ -0,0 +1,839 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/16 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/16","id":352177461,"node_id":"MDExOlB1bGxSZXF1ZXN0MzUyMTc3NDYx","html_url":"https://github.com/ThiagoCodecov/example-python/pull/16","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/16.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/16.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/16","number":16,"state":"open","locked":true,"title":"PR + with more than 250 results","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"","created_at":"2019-12-12T00:31:54Z","updated_at":"2019-12-12T00:33:00Z","closed_at":null,"merged_at":null,"merge_commit_sha":"da802f783cf54cd0682e0b10341679130759f842","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/16/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/16/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/16/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/d55dc4ef748fd11537e50c9abed4ab1864fa1d94","head":{"label":"ThiagoCodecov:thiago/f/big-pt","ref":"thiago/f/big-pt","sha":"d55dc4ef748fd11537e50c9abed4ab1864fa1d94","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2023-07-04T20:51:23Z","pushed_at":"2022-08-17T20:09:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":3,"open_issues":1,"watchers":0,"default_branch":"main"}},"base":{"label":"ThiagoCodecov:main","ref":"main","sha":"d723f5cb5c9c9f48c47f2df97c47de20457d3fdc","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2023-07-04T20:51:23Z","pushed_at":"2022-08-17T20:09:02Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":178,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":3,"open_issues":1,"watchers":0,"default_branch":"main"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/16"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/16"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/16"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/16/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/16/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/16/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/d55dc4ef748fd11537e50c9abed4ab1864fa1d94"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null,"merged":false,"mergeable":false,"rebaseable":false,"mergeable_state":"dirty","merged_by":null,"comments":0,"review_comments":0,"maintainer_can_modify":false,"commits":270,"additions":270,"deletions":0,"changed_files":4}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:16:16 GMT + ETag: + - W/"0dfe9b925d50723b3282a1ffc4da3aedc7020350a48fb0d19854ed51f2b2c2cb" + Last-Modified: + - Fri, 01 Sep 2023 11:48:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF7A:48C3:127A79:134C78:64F610DF + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/16/commits?page=3&per_page=100 + response: + content: '[{"sha":"6ec725d57868bcd10fed6a05ec3ba69dbd607b22","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjZlYzcyNWQ1Nzg2OGJjZDEwZmVkNmEwNWVjM2JhNjlkYmQ2MDdiMjI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"358D2863-F325-476A-8AF7-EF51A6A7A511","tree":{"sha":"9a1027ce36d4b2a4441042ae8f8c2a350a9c49b9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9a1027ce36d4b2a4441042ae8f8c2a350a9c49b9"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6ec725d57868bcd10fed6a05ec3ba69dbd607b22","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ec725d57868bcd10fed6a05ec3ba69dbd607b22","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6ec725d57868bcd10fed6a05ec3ba69dbd607b22","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ec725d57868bcd10fed6a05ec3ba69dbd607b22/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"5ca84ce84d2ae634e07bcdf4a62e5268c2f6f36e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/5ca84ce84d2ae634e07bcdf4a62e5268c2f6f36e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/5ca84ce84d2ae634e07bcdf4a62e5268c2f6f36e"}]},{"sha":"a132221ca123f2f1ffedf707b4af816e701aaeb6","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmExMzIyMjFjYTEyM2YyZjFmZmVkZjcwN2I0YWY4MTZlNzAxYWFlYjY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"7A4B0618-D124-4679-AB95-DB9BC5CB1615","tree":{"sha":"72b98f19e9caeb030e0deb8dde3fa2f8ab00fa47","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/72b98f19e9caeb030e0deb8dde3fa2f8ab00fa47"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/a132221ca123f2f1ffedf707b4af816e701aaeb6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a132221ca123f2f1ffedf707b4af816e701aaeb6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a132221ca123f2f1ffedf707b4af816e701aaeb6","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a132221ca123f2f1ffedf707b4af816e701aaeb6/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6ec725d57868bcd10fed6a05ec3ba69dbd607b22","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ec725d57868bcd10fed6a05ec3ba69dbd607b22","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6ec725d57868bcd10fed6a05ec3ba69dbd607b22"}]},{"sha":"f5454f39b512905dae28bd0e5c1bccbfe0d66001","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmY1NDU0ZjM5YjUxMjkwNWRhZTI4YmQwZTVjMWJjY2JmZTBkNjYwMDE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"2E386F22-1DEE-49CD-9846-30DE30BD27D5","tree":{"sha":"c5626370215e09840f0c2d86fcd55b61403e4dab","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c5626370215e09840f0c2d86fcd55b61403e4dab"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f5454f39b512905dae28bd0e5c1bccbfe0d66001","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f5454f39b512905dae28bd0e5c1bccbfe0d66001","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f5454f39b512905dae28bd0e5c1bccbfe0d66001","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f5454f39b512905dae28bd0e5c1bccbfe0d66001/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"a132221ca123f2f1ffedf707b4af816e701aaeb6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a132221ca123f2f1ffedf707b4af816e701aaeb6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a132221ca123f2f1ffedf707b4af816e701aaeb6"}]},{"sha":"726f548ec87c1b9ff0aafd83da0efc09a573ca9f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjcyNmY1NDhlYzg3YzFiOWZmMGFhZmQ4M2RhMGVmYzA5YTU3M2NhOWY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"ACFFA633-87FF-4F15-9DCF-6858E2A37F9A","tree":{"sha":"69e532ad8c42194d48c4344686b3fb9be074ba68","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/69e532ad8c42194d48c4344686b3fb9be074ba68"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/726f548ec87c1b9ff0aafd83da0efc09a573ca9f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/726f548ec87c1b9ff0aafd83da0efc09a573ca9f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/726f548ec87c1b9ff0aafd83da0efc09a573ca9f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/726f548ec87c1b9ff0aafd83da0efc09a573ca9f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f5454f39b512905dae28bd0e5c1bccbfe0d66001","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f5454f39b512905dae28bd0e5c1bccbfe0d66001","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f5454f39b512905dae28bd0e5c1bccbfe0d66001"}]},{"sha":"0f358e9aed3667af52370d6b1d5638a946d43aeb","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjBmMzU4ZTlhZWQzNjY3YWY1MjM3MGQ2YjFkNTYzOGE5NDZkNDNhZWI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"37F41054-3A2F-499B-B650-B4D169D6CE48","tree":{"sha":"0e5bf4b923b0aa5c1e6924b8b4116d325c2ff831","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/0e5bf4b923b0aa5c1e6924b8b4116d325c2ff831"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/0f358e9aed3667af52370d6b1d5638a946d43aeb","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0f358e9aed3667af52370d6b1d5638a946d43aeb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0f358e9aed3667af52370d6b1d5638a946d43aeb","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0f358e9aed3667af52370d6b1d5638a946d43aeb/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"726f548ec87c1b9ff0aafd83da0efc09a573ca9f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/726f548ec87c1b9ff0aafd83da0efc09a573ca9f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/726f548ec87c1b9ff0aafd83da0efc09a573ca9f"}]},{"sha":"25ba11a5b6bc11fa1b56ce237960b49f7e28ccdf","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjI1YmExMWE1YjZiYzExZmExYjU2Y2UyMzc5NjBiNDlmN2UyOGNjZGY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"6FECDB71-A6EC-454F-82E3-F208EA98230C","tree":{"sha":"27813ff0d6d3a8b510474db5c3c5a15a9fc724e0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/27813ff0d6d3a8b510474db5c3c5a15a9fc724e0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/25ba11a5b6bc11fa1b56ce237960b49f7e28ccdf","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/25ba11a5b6bc11fa1b56ce237960b49f7e28ccdf","html_url":"https://github.com/ThiagoCodecov/example-python/commit/25ba11a5b6bc11fa1b56ce237960b49f7e28ccdf","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/25ba11a5b6bc11fa1b56ce237960b49f7e28ccdf/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"0f358e9aed3667af52370d6b1d5638a946d43aeb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0f358e9aed3667af52370d6b1d5638a946d43aeb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0f358e9aed3667af52370d6b1d5638a946d43aeb"}]},{"sha":"a87fd914d25b66b9aa26a1983242f2b3f9bc5759","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmE4N2ZkOTE0ZDI1YjY2YjlhYTI2YTE5ODMyNDJmMmIzZjliYzU3NTk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"B480C908-B658-4D23-893E-07756D197EBA","tree":{"sha":"ab0aa3b26cf533e6b2b5f24fc9f60b7b2ee6230b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ab0aa3b26cf533e6b2b5f24fc9f60b7b2ee6230b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/a87fd914d25b66b9aa26a1983242f2b3f9bc5759","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a87fd914d25b66b9aa26a1983242f2b3f9bc5759","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a87fd914d25b66b9aa26a1983242f2b3f9bc5759","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a87fd914d25b66b9aa26a1983242f2b3f9bc5759/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"25ba11a5b6bc11fa1b56ce237960b49f7e28ccdf","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/25ba11a5b6bc11fa1b56ce237960b49f7e28ccdf","html_url":"https://github.com/ThiagoCodecov/example-python/commit/25ba11a5b6bc11fa1b56ce237960b49f7e28ccdf"}]},{"sha":"3e409f1b55011148558aeb99550a8408dd6084b9","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjNlNDA5ZjFiNTUwMTExNDg1NThhZWI5OTU1MGE4NDA4ZGQ2MDg0Yjk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"723CE071-21EC-4166-8A85-665C97DFED5F","tree":{"sha":"f80f9f8e7c7d0338a1424f777a3c38ca76f4b04b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f80f9f8e7c7d0338a1424f777a3c38ca76f4b04b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3e409f1b55011148558aeb99550a8408dd6084b9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3e409f1b55011148558aeb99550a8408dd6084b9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3e409f1b55011148558aeb99550a8408dd6084b9","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3e409f1b55011148558aeb99550a8408dd6084b9/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"a87fd914d25b66b9aa26a1983242f2b3f9bc5759","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a87fd914d25b66b9aa26a1983242f2b3f9bc5759","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a87fd914d25b66b9aa26a1983242f2b3f9bc5759"}]},{"sha":"ce8a58b6027e87b139a83daf3160714f0619d418","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmNlOGE1OGI2MDI3ZTg3YjEzOWE4M2RhZjMxNjA3MTRmMDYxOWQ0MTg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"D07D498A-D3BD-4177-993A-84DCFC19B84C","tree":{"sha":"f75d025a52298f59cf844e789b2f8873f61385e9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f75d025a52298f59cf844e789b2f8873f61385e9"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ce8a58b6027e87b139a83daf3160714f0619d418","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ce8a58b6027e87b139a83daf3160714f0619d418","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ce8a58b6027e87b139a83daf3160714f0619d418","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ce8a58b6027e87b139a83daf3160714f0619d418/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"3e409f1b55011148558aeb99550a8408dd6084b9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3e409f1b55011148558aeb99550a8408dd6084b9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3e409f1b55011148558aeb99550a8408dd6084b9"}]},{"sha":"28e05fba84d5730865c9fb1caa89e274a76e56d8","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjI4ZTA1ZmJhODRkNTczMDg2NWM5ZmIxY2FhODllMjc0YTc2ZTU2ZDg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"090937C7-A482-4C0D-B8F5-B27CDC167038","tree":{"sha":"fb41b0f0c6159b7a9d9d26904015c4caf4cdfb3b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/fb41b0f0c6159b7a9d9d26904015c4caf4cdfb3b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/28e05fba84d5730865c9fb1caa89e274a76e56d8","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/28e05fba84d5730865c9fb1caa89e274a76e56d8","html_url":"https://github.com/ThiagoCodecov/example-python/commit/28e05fba84d5730865c9fb1caa89e274a76e56d8","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/28e05fba84d5730865c9fb1caa89e274a76e56d8/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ce8a58b6027e87b139a83daf3160714f0619d418","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ce8a58b6027e87b139a83daf3160714f0619d418","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ce8a58b6027e87b139a83daf3160714f0619d418"}]},{"sha":"205cfba9bca504ef9410a24300b2913f917e96ac","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjIwNWNmYmE5YmNhNTA0ZWY5NDEwYTI0MzAwYjI5MTNmOTE3ZTk2YWM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"9897876C-FE73-452C-80F0-6A7CDD23D0D1","tree":{"sha":"cea0c675d2fc22f1b440010306489e7d746ee674","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/cea0c675d2fc22f1b440010306489e7d746ee674"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/205cfba9bca504ef9410a24300b2913f917e96ac","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/205cfba9bca504ef9410a24300b2913f917e96ac","html_url":"https://github.com/ThiagoCodecov/example-python/commit/205cfba9bca504ef9410a24300b2913f917e96ac","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/205cfba9bca504ef9410a24300b2913f917e96ac/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"28e05fba84d5730865c9fb1caa89e274a76e56d8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/28e05fba84d5730865c9fb1caa89e274a76e56d8","html_url":"https://github.com/ThiagoCodecov/example-python/commit/28e05fba84d5730865c9fb1caa89e274a76e56d8"}]},{"sha":"b2593feff18344adffa5f9451866dbbbe7f38867","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmIyNTkzZmVmZjE4MzQ0YWRmZmE1Zjk0NTE4NjZkYmJiZTdmMzg4Njc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"5704AC10-DF7A-4387-BF5A-3F1BC3FC1DBF","tree":{"sha":"c0fd38e05e89bf646bd5b0f293d54b02aaa7de7a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c0fd38e05e89bf646bd5b0f293d54b02aaa7de7a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b2593feff18344adffa5f9451866dbbbe7f38867","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b2593feff18344adffa5f9451866dbbbe7f38867","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b2593feff18344adffa5f9451866dbbbe7f38867","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b2593feff18344adffa5f9451866dbbbe7f38867/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"205cfba9bca504ef9410a24300b2913f917e96ac","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/205cfba9bca504ef9410a24300b2913f917e96ac","html_url":"https://github.com/ThiagoCodecov/example-python/commit/205cfba9bca504ef9410a24300b2913f917e96ac"}]},{"sha":"88e7779c607cf8626288ebee8e70fb6aa4c0903f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg4ZTc3NzljNjA3Y2Y4NjI2Mjg4ZWJlZThlNzBmYjZhYTRjMDkwM2Y=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"94A737B7-A2B3-4B0A-9AF6-051234719AAD","tree":{"sha":"6229a34e0a518d4999f7a1cc09a6c669b7632773","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6229a34e0a518d4999f7a1cc09a6c669b7632773"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/88e7779c607cf8626288ebee8e70fb6aa4c0903f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/88e7779c607cf8626288ebee8e70fb6aa4c0903f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/88e7779c607cf8626288ebee8e70fb6aa4c0903f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/88e7779c607cf8626288ebee8e70fb6aa4c0903f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"b2593feff18344adffa5f9451866dbbbe7f38867","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b2593feff18344adffa5f9451866dbbbe7f38867","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b2593feff18344adffa5f9451866dbbbe7f38867"}]},{"sha":"b21be451f31cbbed6425f12930fb9e97a2476d18","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmIyMWJlNDUxZjMxY2JiZWQ2NDI1ZjEyOTMwZmI5ZTk3YTI0NzZkMTg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"AA0BA364-0B19-4B43-A441-E5F2BEF4D46D","tree":{"sha":"759a39cec73996d4afa660ad78b7a12165d56b67","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/759a39cec73996d4afa660ad78b7a12165d56b67"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b21be451f31cbbed6425f12930fb9e97a2476d18","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b21be451f31cbbed6425f12930fb9e97a2476d18","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b21be451f31cbbed6425f12930fb9e97a2476d18","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b21be451f31cbbed6425f12930fb9e97a2476d18/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"88e7779c607cf8626288ebee8e70fb6aa4c0903f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/88e7779c607cf8626288ebee8e70fb6aa4c0903f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/88e7779c607cf8626288ebee8e70fb6aa4c0903f"}]},{"sha":"80ddb5045f47da0ec9fb3b767ae076a1ec307172","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjgwZGRiNTA0NWY0N2RhMGVjOWZiM2I3NjdhZTA3NmExZWMzMDcxNzI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"28CDCF38-7654-4F8A-B4B6-65273D316D47","tree":{"sha":"9bf7bed9da00fb6ea552a98a2c01a283d9b52362","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9bf7bed9da00fb6ea552a98a2c01a283d9b52362"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/80ddb5045f47da0ec9fb3b767ae076a1ec307172","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/80ddb5045f47da0ec9fb3b767ae076a1ec307172","html_url":"https://github.com/ThiagoCodecov/example-python/commit/80ddb5045f47da0ec9fb3b767ae076a1ec307172","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/80ddb5045f47da0ec9fb3b767ae076a1ec307172/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"b21be451f31cbbed6425f12930fb9e97a2476d18","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b21be451f31cbbed6425f12930fb9e97a2476d18","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b21be451f31cbbed6425f12930fb9e97a2476d18"}]},{"sha":"54277b6fbb0b48ae42cb507da814000d55c875e6","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjU0Mjc3YjZmYmIwYjQ4YWU0MmNiNTA3ZGE4MTQwMDBkNTVjODc1ZTY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"0CD433BD-51E1-46E7-9123-EEAF04412C94","tree":{"sha":"83d0667c58d4ba862e01d2b75a7d4efe491e965b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/83d0667c58d4ba862e01d2b75a7d4efe491e965b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/54277b6fbb0b48ae42cb507da814000d55c875e6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/54277b6fbb0b48ae42cb507da814000d55c875e6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/54277b6fbb0b48ae42cb507da814000d55c875e6","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/54277b6fbb0b48ae42cb507da814000d55c875e6/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"80ddb5045f47da0ec9fb3b767ae076a1ec307172","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/80ddb5045f47da0ec9fb3b767ae076a1ec307172","html_url":"https://github.com/ThiagoCodecov/example-python/commit/80ddb5045f47da0ec9fb3b767ae076a1ec307172"}]},{"sha":"aba2b8c31222761d64112e4888f1e72e6c6297b0","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmFiYTJiOGMzMTIyMjc2MWQ2NDExMmU0ODg4ZjFlNzJlNmM2Mjk3YjA=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"9C0ACEDB-B9B1-41A7-BE48-82247AAF9C06","tree":{"sha":"ecacd63ff97604326ff11f0215831a11d36ccca1","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ecacd63ff97604326ff11f0215831a11d36ccca1"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/aba2b8c31222761d64112e4888f1e72e6c6297b0","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/aba2b8c31222761d64112e4888f1e72e6c6297b0","html_url":"https://github.com/ThiagoCodecov/example-python/commit/aba2b8c31222761d64112e4888f1e72e6c6297b0","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/aba2b8c31222761d64112e4888f1e72e6c6297b0/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"54277b6fbb0b48ae42cb507da814000d55c875e6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/54277b6fbb0b48ae42cb507da814000d55c875e6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/54277b6fbb0b48ae42cb507da814000d55c875e6"}]},{"sha":"e05de3362facb5ff9af4cc8e47d6e2daec3ff389","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmUwNWRlMzM2MmZhY2I1ZmY5YWY0Y2M4ZTQ3ZDZlMmRhZWMzZmYzODk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"9A21810D-620B-4CF8-A6FE-B054C9C09C67","tree":{"sha":"fe0091db36b60c128597bc85cbce68658900a813","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/fe0091db36b60c128597bc85cbce68658900a813"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e05de3362facb5ff9af4cc8e47d6e2daec3ff389","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e05de3362facb5ff9af4cc8e47d6e2daec3ff389","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e05de3362facb5ff9af4cc8e47d6e2daec3ff389","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e05de3362facb5ff9af4cc8e47d6e2daec3ff389/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"aba2b8c31222761d64112e4888f1e72e6c6297b0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/aba2b8c31222761d64112e4888f1e72e6c6297b0","html_url":"https://github.com/ThiagoCodecov/example-python/commit/aba2b8c31222761d64112e4888f1e72e6c6297b0"}]},{"sha":"dde8ffc6a16ad143f62f478e273e8cc92a8eab7d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmRkZThmZmM2YTE2YWQxNDNmNjJmNDc4ZTI3M2U4Y2M5MmE4ZWFiN2Q=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"CC011F16-14E0-41D0-AE45-0BD3B6E53F65","tree":{"sha":"f6f53e90fc9a96a754e1bcc33fb176134c70a56e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f6f53e90fc9a96a754e1bcc33fb176134c70a56e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/dde8ffc6a16ad143f62f478e273e8cc92a8eab7d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/dde8ffc6a16ad143f62f478e273e8cc92a8eab7d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/dde8ffc6a16ad143f62f478e273e8cc92a8eab7d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/dde8ffc6a16ad143f62f478e273e8cc92a8eab7d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e05de3362facb5ff9af4cc8e47d6e2daec3ff389","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e05de3362facb5ff9af4cc8e47d6e2daec3ff389","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e05de3362facb5ff9af4cc8e47d6e2daec3ff389"}]},{"sha":"2396efab593d23031345dfebf283900deaef4531","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjIzOTZlZmFiNTkzZDIzMDMxMzQ1ZGZlYmYyODM5MDBkZWFlZjQ1MzE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"65D6929A-6B5F-4BE4-91DB-73AC1896CE6C","tree":{"sha":"1557a9c00989efc83bd45cc961ef126661e7d9a7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/1557a9c00989efc83bd45cc961ef126661e7d9a7"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2396efab593d23031345dfebf283900deaef4531","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2396efab593d23031345dfebf283900deaef4531","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2396efab593d23031345dfebf283900deaef4531","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2396efab593d23031345dfebf283900deaef4531/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"dde8ffc6a16ad143f62f478e273e8cc92a8eab7d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/dde8ffc6a16ad143f62f478e273e8cc92a8eab7d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/dde8ffc6a16ad143f62f478e273e8cc92a8eab7d"}]},{"sha":"14e29bd4574da2ac51720109b8e13c95345d2e50","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjE0ZTI5YmQ0NTc0ZGEyYWM1MTcyMDEwOWI4ZTEzYzk1MzQ1ZDJlNTA=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"2DEFCE62-D435-4E82-B5B4-F211105141E7","tree":{"sha":"b9946b526a6c891a1956ebe9a032ce2be23c811a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b9946b526a6c891a1956ebe9a032ce2be23c811a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/14e29bd4574da2ac51720109b8e13c95345d2e50","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/14e29bd4574da2ac51720109b8e13c95345d2e50","html_url":"https://github.com/ThiagoCodecov/example-python/commit/14e29bd4574da2ac51720109b8e13c95345d2e50","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/14e29bd4574da2ac51720109b8e13c95345d2e50/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2396efab593d23031345dfebf283900deaef4531","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2396efab593d23031345dfebf283900deaef4531","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2396efab593d23031345dfebf283900deaef4531"}]},{"sha":"37da7859513e8558268b93feed8f04086c4ba7d0","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjM3ZGE3ODU5NTEzZTg1NTgyNjhiOTNmZWVkOGYwNDA4NmM0YmE3ZDA=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"97A7A698-2621-4A90-82FF-7C02220AD203","tree":{"sha":"5b0e3782fd0f5663c985f04b58db6a53f0cfc9fc","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/5b0e3782fd0f5663c985f04b58db6a53f0cfc9fc"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/37da7859513e8558268b93feed8f04086c4ba7d0","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/37da7859513e8558268b93feed8f04086c4ba7d0","html_url":"https://github.com/ThiagoCodecov/example-python/commit/37da7859513e8558268b93feed8f04086c4ba7d0","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/37da7859513e8558268b93feed8f04086c4ba7d0/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"14e29bd4574da2ac51720109b8e13c95345d2e50","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/14e29bd4574da2ac51720109b8e13c95345d2e50","html_url":"https://github.com/ThiagoCodecov/example-python/commit/14e29bd4574da2ac51720109b8e13c95345d2e50"}]},{"sha":"3f6c572414b9287101300acedf9819492e8a48d5","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjNmNmM1NzI0MTRiOTI4NzEwMTMwMGFjZWRmOTgxOTQ5MmU4YTQ4ZDU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"FCE74910-8E9A-4D01-9576-953A634A1780","tree":{"sha":"524ec21d5d887c4fa1a459f9760a554225e42efb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/524ec21d5d887c4fa1a459f9760a554225e42efb"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3f6c572414b9287101300acedf9819492e8a48d5","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f6c572414b9287101300acedf9819492e8a48d5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3f6c572414b9287101300acedf9819492e8a48d5","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f6c572414b9287101300acedf9819492e8a48d5/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"37da7859513e8558268b93feed8f04086c4ba7d0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/37da7859513e8558268b93feed8f04086c4ba7d0","html_url":"https://github.com/ThiagoCodecov/example-python/commit/37da7859513e8558268b93feed8f04086c4ba7d0"}]},{"sha":"ce45e698af269ef92ad5ffd4f5b93d7f1673c27d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmNlNDVlNjk4YWYyNjllZjkyYWQ1ZmZkNGY1YjkzZDdmMTY3M2MyN2Q=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"449FA9F7-F485-4281-B52B-34C93DC8AD06","tree":{"sha":"9e364a3d0a5572bc82fe4edbdbc77430305564e7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9e364a3d0a5572bc82fe4edbdbc77430305564e7"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ce45e698af269ef92ad5ffd4f5b93d7f1673c27d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ce45e698af269ef92ad5ffd4f5b93d7f1673c27d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ce45e698af269ef92ad5ffd4f5b93d7f1673c27d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ce45e698af269ef92ad5ffd4f5b93d7f1673c27d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"3f6c572414b9287101300acedf9819492e8a48d5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f6c572414b9287101300acedf9819492e8a48d5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3f6c572414b9287101300acedf9819492e8a48d5"}]},{"sha":"b264ebeb7298845cf0d3596e74558310d116733d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmIyNjRlYmViNzI5ODg0NWNmMGQzNTk2ZTc0NTU4MzEwZDExNjczM2Q=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"FF6DD7E1-D03A-4D26-AEDA-371D32AFAE93","tree":{"sha":"ad143d98ded7729674232ebec57153d248f01814","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ad143d98ded7729674232ebec57153d248f01814"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b264ebeb7298845cf0d3596e74558310d116733d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b264ebeb7298845cf0d3596e74558310d116733d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b264ebeb7298845cf0d3596e74558310d116733d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b264ebeb7298845cf0d3596e74558310d116733d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ce45e698af269ef92ad5ffd4f5b93d7f1673c27d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ce45e698af269ef92ad5ffd4f5b93d7f1673c27d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ce45e698af269ef92ad5ffd4f5b93d7f1673c27d"}]},{"sha":"0b83bb24cfdb2d70c05a2a0b3da1bff53352bdc2","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjBiODNiYjI0Y2ZkYjJkNzBjMDVhMmEwYjNkYTFiZmY1MzM1MmJkYzI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"A63934A4-E73A-4E03-AAD5-38F308789A58","tree":{"sha":"b0460d63801cc575798b27624af18251468e3d0b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b0460d63801cc575798b27624af18251468e3d0b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/0b83bb24cfdb2d70c05a2a0b3da1bff53352bdc2","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0b83bb24cfdb2d70c05a2a0b3da1bff53352bdc2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0b83bb24cfdb2d70c05a2a0b3da1bff53352bdc2","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0b83bb24cfdb2d70c05a2a0b3da1bff53352bdc2/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"b264ebeb7298845cf0d3596e74558310d116733d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b264ebeb7298845cf0d3596e74558310d116733d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b264ebeb7298845cf0d3596e74558310d116733d"}]},{"sha":"f60231ceb1bc4a28c03ccba42be6fd50cf0984eb","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmY2MDIzMWNlYjFiYzRhMjhjMDNjY2JhNDJiZTZmZDUwY2YwOTg0ZWI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"61A3986C-EA79-4DD3-8CB8-B83CF08A3081","tree":{"sha":"b0f080927bfa449b883e925b6c18a9d44f026c39","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b0f080927bfa449b883e925b6c18a9d44f026c39"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f60231ceb1bc4a28c03ccba42be6fd50cf0984eb","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f60231ceb1bc4a28c03ccba42be6fd50cf0984eb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f60231ceb1bc4a28c03ccba42be6fd50cf0984eb","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f60231ceb1bc4a28c03ccba42be6fd50cf0984eb/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"0b83bb24cfdb2d70c05a2a0b3da1bff53352bdc2","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0b83bb24cfdb2d70c05a2a0b3da1bff53352bdc2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0b83bb24cfdb2d70c05a2a0b3da1bff53352bdc2"}]},{"sha":"500eb11dca0a829370f38d67a3a18d142cba66a5","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjUwMGViMTFkY2EwYTgyOTM3MGYzOGQ2N2EzYTE4ZDE0MmNiYTY2YTU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"FE50DCDA-515D-49B6-863F-EE273D588598","tree":{"sha":"aa4f59f8dab4e1b7da87ec0344ac97e54a6573de","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/aa4f59f8dab4e1b7da87ec0344ac97e54a6573de"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/500eb11dca0a829370f38d67a3a18d142cba66a5","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/500eb11dca0a829370f38d67a3a18d142cba66a5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/500eb11dca0a829370f38d67a3a18d142cba66a5","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/500eb11dca0a829370f38d67a3a18d142cba66a5/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f60231ceb1bc4a28c03ccba42be6fd50cf0984eb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f60231ceb1bc4a28c03ccba42be6fd50cf0984eb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f60231ceb1bc4a28c03ccba42be6fd50cf0984eb"}]},{"sha":"0a577342a7ef35da9130001840e84809a57a385c","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjBhNTc3MzQyYTdlZjM1ZGE5MTMwMDAxODQwZTg0ODA5YTU3YTM4NWM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"F2E6DDE7-2390-4C00-8536-D6067C276AC6","tree":{"sha":"f038d6284b0a466d071899b91cb65af3b5e1900e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f038d6284b0a466d071899b91cb65af3b5e1900e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/0a577342a7ef35da9130001840e84809a57a385c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0a577342a7ef35da9130001840e84809a57a385c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0a577342a7ef35da9130001840e84809a57a385c","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0a577342a7ef35da9130001840e84809a57a385c/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"500eb11dca0a829370f38d67a3a18d142cba66a5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/500eb11dca0a829370f38d67a3a18d142cba66a5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/500eb11dca0a829370f38d67a3a18d142cba66a5"}]},{"sha":"67c4e2de8b1c49565daf20289208095917e5bc55","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjY3YzRlMmRlOGIxYzQ5NTY1ZGFmMjAyODkyMDgwOTU5MTdlNWJjNTU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"5EC8E798-ED64-4D75-863F-585A63B1A53D","tree":{"sha":"6ce8c18e8ce32e5aed10b0968fc825f1eb07aa46","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6ce8c18e8ce32e5aed10b0968fc825f1eb07aa46"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/67c4e2de8b1c49565daf20289208095917e5bc55","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/67c4e2de8b1c49565daf20289208095917e5bc55","html_url":"https://github.com/ThiagoCodecov/example-python/commit/67c4e2de8b1c49565daf20289208095917e5bc55","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/67c4e2de8b1c49565daf20289208095917e5bc55/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"0a577342a7ef35da9130001840e84809a57a385c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0a577342a7ef35da9130001840e84809a57a385c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0a577342a7ef35da9130001840e84809a57a385c"}]},{"sha":"cf110403825f32672396261efa71002a975c3c8a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmNmMTEwNDAzODI1ZjMyNjcyMzk2MjYxZWZhNzEwMDJhOTc1YzNjOGE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"C8C18386-91B1-400E-9E76-06B4F5817F8F","tree":{"sha":"d1eca7f3880688d863d8724dd26fe86d4e306af0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/d1eca7f3880688d863d8724dd26fe86d4e306af0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/cf110403825f32672396261efa71002a975c3c8a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cf110403825f32672396261efa71002a975c3c8a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cf110403825f32672396261efa71002a975c3c8a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cf110403825f32672396261efa71002a975c3c8a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"67c4e2de8b1c49565daf20289208095917e5bc55","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/67c4e2de8b1c49565daf20289208095917e5bc55","html_url":"https://github.com/ThiagoCodecov/example-python/commit/67c4e2de8b1c49565daf20289208095917e5bc55"}]},{"sha":"9d1f1dcb0037c14719d8540586dd3094e304bd41","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjlkMWYxZGNiMDAzN2MxNDcxOWQ4NTQwNTg2ZGQzMDk0ZTMwNGJkNDE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"67F783E6-137D-4FBE-BF44-0F70CFF2CDE3","tree":{"sha":"a9c11531736db79479c1d4a3f20182a8be88ac4d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/a9c11531736db79479c1d4a3f20182a8be88ac4d"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/9d1f1dcb0037c14719d8540586dd3094e304bd41","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9d1f1dcb0037c14719d8540586dd3094e304bd41","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9d1f1dcb0037c14719d8540586dd3094e304bd41","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9d1f1dcb0037c14719d8540586dd3094e304bd41/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"cf110403825f32672396261efa71002a975c3c8a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cf110403825f32672396261efa71002a975c3c8a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cf110403825f32672396261efa71002a975c3c8a"}]},{"sha":"920ed683ccb0f2245b13f33ff7bccc4912417113","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjkyMGVkNjgzY2NiMGYyMjQ1YjEzZjMzZmY3YmNjYzQ5MTI0MTcxMTM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"F81F1976-D59C-486C-A625-83DCB0719A25","tree":{"sha":"96ee1b3f86675d9920ed728aca6f83cf73383bf6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/96ee1b3f86675d9920ed728aca6f83cf73383bf6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/920ed683ccb0f2245b13f33ff7bccc4912417113","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/920ed683ccb0f2245b13f33ff7bccc4912417113","html_url":"https://github.com/ThiagoCodecov/example-python/commit/920ed683ccb0f2245b13f33ff7bccc4912417113","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/920ed683ccb0f2245b13f33ff7bccc4912417113/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"9d1f1dcb0037c14719d8540586dd3094e304bd41","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9d1f1dcb0037c14719d8540586dd3094e304bd41","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9d1f1dcb0037c14719d8540586dd3094e304bd41"}]},{"sha":"57d21b1eb6b2e6d029c606a5590072518e073139","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjU3ZDIxYjFlYjZiMmU2ZDAyOWM2MDZhNTU5MDA3MjUxOGUwNzMxMzk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"8ACED5EA-3481-4DF4-9DF1-31C5D99BC5E4","tree":{"sha":"f273a9c6d33bd172b639808865cd6df57b9e7d5a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f273a9c6d33bd172b639808865cd6df57b9e7d5a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/57d21b1eb6b2e6d029c606a5590072518e073139","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/57d21b1eb6b2e6d029c606a5590072518e073139","html_url":"https://github.com/ThiagoCodecov/example-python/commit/57d21b1eb6b2e6d029c606a5590072518e073139","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/57d21b1eb6b2e6d029c606a5590072518e073139/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"920ed683ccb0f2245b13f33ff7bccc4912417113","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/920ed683ccb0f2245b13f33ff7bccc4912417113","html_url":"https://github.com/ThiagoCodecov/example-python/commit/920ed683ccb0f2245b13f33ff7bccc4912417113"}]},{"sha":"dbc8aec0b9f9ec10cf5e06c5f90b92708dc40ede","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmRiYzhhZWMwYjlmOWVjMTBjZjVlMDZjNWY5MGI5MjcwOGRjNDBlZGU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"BA42BE2D-2547-4B42-BAA6-9701AC7F93E5","tree":{"sha":"d40a4d6428dcb99ff1e8cf56344c55bfd6bf9451","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/d40a4d6428dcb99ff1e8cf56344c55bfd6bf9451"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/dbc8aec0b9f9ec10cf5e06c5f90b92708dc40ede","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/dbc8aec0b9f9ec10cf5e06c5f90b92708dc40ede","html_url":"https://github.com/ThiagoCodecov/example-python/commit/dbc8aec0b9f9ec10cf5e06c5f90b92708dc40ede","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/dbc8aec0b9f9ec10cf5e06c5f90b92708dc40ede/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"57d21b1eb6b2e6d029c606a5590072518e073139","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/57d21b1eb6b2e6d029c606a5590072518e073139","html_url":"https://github.com/ThiagoCodecov/example-python/commit/57d21b1eb6b2e6d029c606a5590072518e073139"}]},{"sha":"54d5f91c03d481444ed072c7596ca2948e478a62","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjU0ZDVmOTFjMDNkNDgxNDQ0ZWQwNzJjNzU5NmNhMjk0OGU0NzhhNjI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"A4104045-E7F8-4663-BB76-CDF8DB19D901","tree":{"sha":"6252f82bd8c2ef795673b40692db1ff7688c10bb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6252f82bd8c2ef795673b40692db1ff7688c10bb"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/54d5f91c03d481444ed072c7596ca2948e478a62","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/54d5f91c03d481444ed072c7596ca2948e478a62","html_url":"https://github.com/ThiagoCodecov/example-python/commit/54d5f91c03d481444ed072c7596ca2948e478a62","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/54d5f91c03d481444ed072c7596ca2948e478a62/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"dbc8aec0b9f9ec10cf5e06c5f90b92708dc40ede","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/dbc8aec0b9f9ec10cf5e06c5f90b92708dc40ede","html_url":"https://github.com/ThiagoCodecov/example-python/commit/dbc8aec0b9f9ec10cf5e06c5f90b92708dc40ede"}]},{"sha":"01e7f2eb72ca79d53f576c052919f65ca257d13b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjAxZTdmMmViNzJjYTc5ZDUzZjU3NmMwNTI5MTlmNjVjYTI1N2QxM2I=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"40EF93A2-37A5-4B3D-BB18-CEDFC8D4F66C","tree":{"sha":"768310d8f2dfe33650cccb71b498587a048e55d0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/768310d8f2dfe33650cccb71b498587a048e55d0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/01e7f2eb72ca79d53f576c052919f65ca257d13b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/01e7f2eb72ca79d53f576c052919f65ca257d13b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/01e7f2eb72ca79d53f576c052919f65ca257d13b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/01e7f2eb72ca79d53f576c052919f65ca257d13b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"54d5f91c03d481444ed072c7596ca2948e478a62","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/54d5f91c03d481444ed072c7596ca2948e478a62","html_url":"https://github.com/ThiagoCodecov/example-python/commit/54d5f91c03d481444ed072c7596ca2948e478a62"}]},{"sha":"d64e78af13fa9838a7e410235336889af01b5e9a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmQ2NGU3OGFmMTNmYTk4MzhhN2U0MTAyMzUzMzY4ODlhZjAxYjVlOWE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:02Z"},"message":"69FFA30E-8E73-4575-AD66-468E5DE7FFAE","tree":{"sha":"9c9c3c04586538d36627093be7afa47280c23d39","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9c9c3c04586538d36627093be7afa47280c23d39"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d64e78af13fa9838a7e410235336889af01b5e9a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d64e78af13fa9838a7e410235336889af01b5e9a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d64e78af13fa9838a7e410235336889af01b5e9a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d64e78af13fa9838a7e410235336889af01b5e9a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"01e7f2eb72ca79d53f576c052919f65ca257d13b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/01e7f2eb72ca79d53f576c052919f65ca257d13b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/01e7f2eb72ca79d53f576c052919f65ca257d13b"}]},{"sha":"b76d3b007f71249c55b330da54ed26e61659524b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmI3NmQzYjAwN2Y3MTI0OWM1NWIzMzBkYTU0ZWQyNmU2MTY1OTUyNGI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"message":"1F66E867-41C2-4DDD-9FDC-8D189EA74675","tree":{"sha":"9432e4159f5db0d3105c00432ca15aaf929e79e7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9432e4159f5db0d3105c00432ca15aaf929e79e7"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b76d3b007f71249c55b330da54ed26e61659524b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b76d3b007f71249c55b330da54ed26e61659524b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b76d3b007f71249c55b330da54ed26e61659524b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b76d3b007f71249c55b330da54ed26e61659524b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"d64e78af13fa9838a7e410235336889af01b5e9a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d64e78af13fa9838a7e410235336889af01b5e9a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d64e78af13fa9838a7e410235336889af01b5e9a"}]},{"sha":"ce4b4f64a47047059de1c466115c97c74ab8a3d6","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmNlNGI0ZjY0YTQ3MDQ3MDU5ZGUxYzQ2NjExNWM5N2M3NGFiOGEzZDY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"message":"5DF89BE6-689E-4256-A82F-D4FFE7B83B46","tree":{"sha":"50bd366895b86ed6c2aa6ff15b24a067008a2b0f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/50bd366895b86ed6c2aa6ff15b24a067008a2b0f"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ce4b4f64a47047059de1c466115c97c74ab8a3d6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ce4b4f64a47047059de1c466115c97c74ab8a3d6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ce4b4f64a47047059de1c466115c97c74ab8a3d6","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ce4b4f64a47047059de1c466115c97c74ab8a3d6/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"b76d3b007f71249c55b330da54ed26e61659524b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b76d3b007f71249c55b330da54ed26e61659524b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b76d3b007f71249c55b330da54ed26e61659524b"}]},{"sha":"871cf7c596b4a5396670cb3572f0c67353a00b76","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg3MWNmN2M1OTZiNGE1Mzk2NjcwY2IzNTcyZjBjNjczNTNhMDBiNzY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"message":"5F964512-92EE-4367-8DAC-BAFE17A94DD6","tree":{"sha":"f218b9df117fb0d51d42c15d22792f38c57f1517","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f218b9df117fb0d51d42c15d22792f38c57f1517"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/871cf7c596b4a5396670cb3572f0c67353a00b76","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/871cf7c596b4a5396670cb3572f0c67353a00b76","html_url":"https://github.com/ThiagoCodecov/example-python/commit/871cf7c596b4a5396670cb3572f0c67353a00b76","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/871cf7c596b4a5396670cb3572f0c67353a00b76/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ce4b4f64a47047059de1c466115c97c74ab8a3d6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ce4b4f64a47047059de1c466115c97c74ab8a3d6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ce4b4f64a47047059de1c466115c97c74ab8a3d6"}]},{"sha":"aec5dea78d86a588fe83a3b257a525e5aa65d7c9","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmFlYzVkZWE3OGQ4NmE1ODhmZTgzYTNiMjU3YTUyNWU1YWE2NWQ3Yzk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"message":"C4BC9FB0-67A9-4B6B-8300-B863F7ED56ED","tree":{"sha":"02c7e4a92f1b5ec0fc1589597e0ff34361a1505a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/02c7e4a92f1b5ec0fc1589597e0ff34361a1505a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/aec5dea78d86a588fe83a3b257a525e5aa65d7c9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/aec5dea78d86a588fe83a3b257a525e5aa65d7c9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/aec5dea78d86a588fe83a3b257a525e5aa65d7c9","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/aec5dea78d86a588fe83a3b257a525e5aa65d7c9/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"871cf7c596b4a5396670cb3572f0c67353a00b76","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/871cf7c596b4a5396670cb3572f0c67353a00b76","html_url":"https://github.com/ThiagoCodecov/example-python/commit/871cf7c596b4a5396670cb3572f0c67353a00b76"}]},{"sha":"864dfe9b162c86c2ec375b1efafa7130ca9a38de","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg2NGRmZTliMTYyYzg2YzJlYzM3NWIxZWZhZmE3MTMwY2E5YTM4ZGU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"message":"668B6D65-543E-4D2E-BC10-228EB0BE71CC","tree":{"sha":"fc0ec1985e047c8da7bb546cd82587c0871f4b2b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/fc0ec1985e047c8da7bb546cd82587c0871f4b2b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/864dfe9b162c86c2ec375b1efafa7130ca9a38de","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/864dfe9b162c86c2ec375b1efafa7130ca9a38de","html_url":"https://github.com/ThiagoCodecov/example-python/commit/864dfe9b162c86c2ec375b1efafa7130ca9a38de","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/864dfe9b162c86c2ec375b1efafa7130ca9a38de/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"aec5dea78d86a588fe83a3b257a525e5aa65d7c9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/aec5dea78d86a588fe83a3b257a525e5aa65d7c9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/aec5dea78d86a588fe83a3b257a525e5aa65d7c9"}]},{"sha":"3d934b41b676f8b855248a8dc60008c518b58ae2","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjNkOTM0YjQxYjY3NmY4Yjg1NTI0OGE4ZGM2MDAwOGM1MThiNThhZTI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"message":"658A26A9-E7A6-4EFA-A538-D5A258750826","tree":{"sha":"977d922608e78556998e810c21143972311a7633","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/977d922608e78556998e810c21143972311a7633"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3d934b41b676f8b855248a8dc60008c518b58ae2","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3d934b41b676f8b855248a8dc60008c518b58ae2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3d934b41b676f8b855248a8dc60008c518b58ae2","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3d934b41b676f8b855248a8dc60008c518b58ae2/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"864dfe9b162c86c2ec375b1efafa7130ca9a38de","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/864dfe9b162c86c2ec375b1efafa7130ca9a38de","html_url":"https://github.com/ThiagoCodecov/example-python/commit/864dfe9b162c86c2ec375b1efafa7130ca9a38de"}]},{"sha":"659e343f3e4e23f24f4c5201f49a49192fde902f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjY1OWUzNDNmM2U0ZTIzZjI0ZjRjNTIwMWY0OWE0OTE5MmZkZTkwMmY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"message":"8BA4408E-AEF6-4771-BC8D-E9375AE03C55","tree":{"sha":"e0aeb611631642c17483b259bff5a2f6e7564703","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e0aeb611631642c17483b259bff5a2f6e7564703"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/659e343f3e4e23f24f4c5201f49a49192fde902f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/659e343f3e4e23f24f4c5201f49a49192fde902f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/659e343f3e4e23f24f4c5201f49a49192fde902f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/659e343f3e4e23f24f4c5201f49a49192fde902f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"3d934b41b676f8b855248a8dc60008c518b58ae2","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3d934b41b676f8b855248a8dc60008c518b58ae2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3d934b41b676f8b855248a8dc60008c518b58ae2"}]},{"sha":"690d7ba34c224d4217f42ea28f0341fa2d9564e2","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjY5MGQ3YmEzNGMyMjRkNDIxN2Y0MmVhMjhmMDM0MWZhMmQ5NTY0ZTI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"message":"8697F50B-940A-4146-AFB4-ADCA92AD37EB","tree":{"sha":"ac135cc12774eb0796c65bb87e8f6a57e3ed4045","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ac135cc12774eb0796c65bb87e8f6a57e3ed4045"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/690d7ba34c224d4217f42ea28f0341fa2d9564e2","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/690d7ba34c224d4217f42ea28f0341fa2d9564e2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/690d7ba34c224d4217f42ea28f0341fa2d9564e2","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/690d7ba34c224d4217f42ea28f0341fa2d9564e2/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"659e343f3e4e23f24f4c5201f49a49192fde902f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/659e343f3e4e23f24f4c5201f49a49192fde902f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/659e343f3e4e23f24f4c5201f49a49192fde902f"}]},{"sha":"759cac6d5fe1f48584bebb74de8f48ae30a6e683","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojc1OWNhYzZkNWZlMWY0ODU4NGJlYmI3NGRlOGY0OGFlMzBhNmU2ODM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"message":"99C307D2-29CC-4F20-9EF8-192DC154AE5D","tree":{"sha":"5dab0c89898086aee83083d8ff1caa379439726a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/5dab0c89898086aee83083d8ff1caa379439726a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/759cac6d5fe1f48584bebb74de8f48ae30a6e683","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/759cac6d5fe1f48584bebb74de8f48ae30a6e683","html_url":"https://github.com/ThiagoCodecov/example-python/commit/759cac6d5fe1f48584bebb74de8f48ae30a6e683","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/759cac6d5fe1f48584bebb74de8f48ae30a6e683/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"690d7ba34c224d4217f42ea28f0341fa2d9564e2","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/690d7ba34c224d4217f42ea28f0341fa2d9564e2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/690d7ba34c224d4217f42ea28f0341fa2d9564e2"}]},{"sha":"fb396b0bcb352cbecff5e3a37b94b63973935d55","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmZiMzk2YjBiY2IzNTJjYmVjZmY1ZTNhMzdiOTRiNjM5NzM5MzVkNTU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"message":"FBB1969B-4C0A-4649-9373-457D64A94034","tree":{"sha":"0de62ded5a2e2096d9fcc20d1a73e5beafc38ac5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/0de62ded5a2e2096d9fcc20d1a73e5beafc38ac5"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/fb396b0bcb352cbecff5e3a37b94b63973935d55","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fb396b0bcb352cbecff5e3a37b94b63973935d55","html_url":"https://github.com/ThiagoCodecov/example-python/commit/fb396b0bcb352cbecff5e3a37b94b63973935d55","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fb396b0bcb352cbecff5e3a37b94b63973935d55/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"759cac6d5fe1f48584bebb74de8f48ae30a6e683","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/759cac6d5fe1f48584bebb74de8f48ae30a6e683","html_url":"https://github.com/ThiagoCodecov/example-python/commit/759cac6d5fe1f48584bebb74de8f48ae30a6e683"}]},{"sha":"e3499f7a869e728f5ab5060f85c24e6d8ecf6242","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmUzNDk5ZjdhODY5ZTcyOGY1YWI1MDYwZjg1YzI0ZTZkOGVjZjYyNDI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"message":"7F270B7F-AEE4-44D0-AEFB-97A546403DB9","tree":{"sha":"e21a27f2c4f0ee3ac514fd67c59a8df3397758d0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e21a27f2c4f0ee3ac514fd67c59a8df3397758d0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e3499f7a869e728f5ab5060f85c24e6d8ecf6242","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e3499f7a869e728f5ab5060f85c24e6d8ecf6242","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e3499f7a869e728f5ab5060f85c24e6d8ecf6242","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e3499f7a869e728f5ab5060f85c24e6d8ecf6242/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"fb396b0bcb352cbecff5e3a37b94b63973935d55","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fb396b0bcb352cbecff5e3a37b94b63973935d55","html_url":"https://github.com/ThiagoCodecov/example-python/commit/fb396b0bcb352cbecff5e3a37b94b63973935d55"}]},{"sha":"d55dc4ef748fd11537e50c9abed4ab1864fa1d94","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmQ1NWRjNGVmNzQ4ZmQxMTUzN2U1MGM5YWJlZDRhYjE4NjRmYTFkOTQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:03Z"},"message":"BC74EA88-33AA-4231-98E9-FE7CA60EE4D0","tree":{"sha":"8b6a7159d7b2177f66f731d9ed60050c7ac4313c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/8b6a7159d7b2177f66f731d9ed60050c7ac4313c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d55dc4ef748fd11537e50c9abed4ab1864fa1d94","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d55dc4ef748fd11537e50c9abed4ab1864fa1d94","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d55dc4ef748fd11537e50c9abed4ab1864fa1d94","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d55dc4ef748fd11537e50c9abed4ab1864fa1d94/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e3499f7a869e728f5ab5060f85c24e6d8ecf6242","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e3499f7a869e728f5ab5060f85c24e6d8ecf6242","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e3499f7a869e728f5ab5060f85c24e6d8ecf6242"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:16:16 GMT + ETag: + - W/"471f4af818ae364d96a287c46b365b13db0865d51eb3add83c5503dd53313c3c" + Last-Modified: + - Fri, 01 Sep 2023 11:48:40 GMT + Link: + - ; + rel="prev", ; + rel="first" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF7D:1A20:120E4B:12E04D:64F610E0 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4997' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '3' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/16/commits?page=1&per_page=100 + response: + content: '[{"sha":"e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmUzYjZjOTc2ZWZlODhiMmEzNzgxZGM4MTU3NDg1ZTQ2YmYyYWM3YWI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"message":"DCE960AA-ACE5-47BD-8851-11B85F1060CB","tree":{"sha":"f6f559b56da125de3c7cad84e9055c8ae5a29d22","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f6f559b56da125de3c7cad84e9055c8ae5a29d22"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"335ec9958daf0242bc8945659bb120c05800eacf","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/335ec9958daf0242bc8945659bb120c05800eacf","html_url":"https://github.com/ThiagoCodecov/example-python/commit/335ec9958daf0242bc8945659bb120c05800eacf"}]},{"sha":"723a156d174c0152af04297f47a7b6b0cb88a9ba","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjcyM2ExNTZkMTc0YzAxNTJhZjA0Mjk3ZjQ3YTdiNmIwY2I4OGE5YmE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"message":"96C49EEA-7E27-4598-A780-4A6F8E011611","tree":{"sha":"e80cde4950a53fa946e44e606eea9c375b3be159","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e80cde4950a53fa946e44e606eea9c375b3be159"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/723a156d174c0152af04297f47a7b6b0cb88a9ba","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/723a156d174c0152af04297f47a7b6b0cb88a9ba","html_url":"https://github.com/ThiagoCodecov/example-python/commit/723a156d174c0152af04297f47a7b6b0cb88a9ba","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/723a156d174c0152af04297f47a7b6b0cb88a9ba/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e3b6c976efe88b2a3781dc8157485e46bf2ac7ab"}]},{"sha":"88e1bc54c07a336f2a9b7a990839e37d352bff18","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg4ZTFiYzU0YzA3YTMzNmYyYTliN2E5OTA4MzllMzdkMzUyYmZmMTg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"message":"5981A8B9-EB4E-415D-AAFA-A54FAE638FFD","tree":{"sha":"0bdc3ce9aa640204b331b3b2d1d93641da0e5e80","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/0bdc3ce9aa640204b331b3b2d1d93641da0e5e80"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/88e1bc54c07a336f2a9b7a990839e37d352bff18","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/88e1bc54c07a336f2a9b7a990839e37d352bff18","html_url":"https://github.com/ThiagoCodecov/example-python/commit/88e1bc54c07a336f2a9b7a990839e37d352bff18","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/88e1bc54c07a336f2a9b7a990839e37d352bff18/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"723a156d174c0152af04297f47a7b6b0cb88a9ba","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/723a156d174c0152af04297f47a7b6b0cb88a9ba","html_url":"https://github.com/ThiagoCodecov/example-python/commit/723a156d174c0152af04297f47a7b6b0cb88a9ba"}]},{"sha":"be51c95560c018bde45093d889e602b93cfdd076","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmJlNTFjOTU1NjBjMDE4YmRlNDUwOTNkODg5ZTYwMmI5M2NmZGQwNzY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"message":"EA93A367-505F-4CFA-A37C-0E2ED29FB4A0","tree":{"sha":"7e2b17afc4c30d84fdf7bfee4579a6136d05f2a9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7e2b17afc4c30d84fdf7bfee4579a6136d05f2a9"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/be51c95560c018bde45093d889e602b93cfdd076","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/be51c95560c018bde45093d889e602b93cfdd076","html_url":"https://github.com/ThiagoCodecov/example-python/commit/be51c95560c018bde45093d889e602b93cfdd076","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/be51c95560c018bde45093d889e602b93cfdd076/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"88e1bc54c07a336f2a9b7a990839e37d352bff18","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/88e1bc54c07a336f2a9b7a990839e37d352bff18","html_url":"https://github.com/ThiagoCodecov/example-python/commit/88e1bc54c07a336f2a9b7a990839e37d352bff18"}]},{"sha":"7133da4f0d21109088906d5284fb3bcc9c5466c2","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjcxMzNkYTRmMGQyMTEwOTA4ODkwNmQ1Mjg0ZmIzYmNjOWM1NDY2YzI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"message":"AEB9FD0B-4D20-41D1-8CC3-60CD29E083E0","tree":{"sha":"74a9e2c2bbf42f6e26537519d330c3e196b8181c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/74a9e2c2bbf42f6e26537519d330c3e196b8181c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/7133da4f0d21109088906d5284fb3bcc9c5466c2","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7133da4f0d21109088906d5284fb3bcc9c5466c2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7133da4f0d21109088906d5284fb3bcc9c5466c2","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7133da4f0d21109088906d5284fb3bcc9c5466c2/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"be51c95560c018bde45093d889e602b93cfdd076","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/be51c95560c018bde45093d889e602b93cfdd076","html_url":"https://github.com/ThiagoCodecov/example-python/commit/be51c95560c018bde45093d889e602b93cfdd076"}]},{"sha":"a7e6358d71d894ee3034a0b002d8ca86b488097d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmE3ZTYzNThkNzFkODk0ZWUzMDM0YTBiMDAyZDhjYTg2YjQ4ODA5N2Q=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"message":"E78997A4-015B-40B8-AC9C-A999F25A1F9A","tree":{"sha":"9e4d9f1d40a91e1d38b5decb2fa3da39e5ac1dda","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9e4d9f1d40a91e1d38b5decb2fa3da39e5ac1dda"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/a7e6358d71d894ee3034a0b002d8ca86b488097d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a7e6358d71d894ee3034a0b002d8ca86b488097d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a7e6358d71d894ee3034a0b002d8ca86b488097d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a7e6358d71d894ee3034a0b002d8ca86b488097d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"7133da4f0d21109088906d5284fb3bcc9c5466c2","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7133da4f0d21109088906d5284fb3bcc9c5466c2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7133da4f0d21109088906d5284fb3bcc9c5466c2"}]},{"sha":"55d9cce249200ea9dbe4f446955f343d6503c101","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjU1ZDljY2UyNDkyMDBlYTlkYmU0ZjQ0Njk1NWYzNDNkNjUwM2MxMDE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"message":"6A19233E-1A03-4A9E-BCC4-078A7DE3547E","tree":{"sha":"dd80b65bc6615021548f18577c0af212c00c4e27","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/dd80b65bc6615021548f18577c0af212c00c4e27"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/55d9cce249200ea9dbe4f446955f343d6503c101","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/55d9cce249200ea9dbe4f446955f343d6503c101","html_url":"https://github.com/ThiagoCodecov/example-python/commit/55d9cce249200ea9dbe4f446955f343d6503c101","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/55d9cce249200ea9dbe4f446955f343d6503c101/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"a7e6358d71d894ee3034a0b002d8ca86b488097d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a7e6358d71d894ee3034a0b002d8ca86b488097d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a7e6358d71d894ee3034a0b002d8ca86b488097d"}]},{"sha":"7a3f81c3eed7d3f589a0f03bb5750d5a55306fc2","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjdhM2Y4MWMzZWVkN2QzZjU4OWEwZjAzYmI1NzUwZDVhNTUzMDZmYzI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"message":"2458AB14-990A-4407-A6E9-885C8AF71C0E","tree":{"sha":"f2d19d27926f70c1ca7380f06f1e0908fb7b9e49","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f2d19d27926f70c1ca7380f06f1e0908fb7b9e49"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/7a3f81c3eed7d3f589a0f03bb5750d5a55306fc2","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7a3f81c3eed7d3f589a0f03bb5750d5a55306fc2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7a3f81c3eed7d3f589a0f03bb5750d5a55306fc2","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7a3f81c3eed7d3f589a0f03bb5750d5a55306fc2/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"55d9cce249200ea9dbe4f446955f343d6503c101","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/55d9cce249200ea9dbe4f446955f343d6503c101","html_url":"https://github.com/ThiagoCodecov/example-python/commit/55d9cce249200ea9dbe4f446955f343d6503c101"}]},{"sha":"8fc7f0b608df3a687050ee475d78147614085468","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjhmYzdmMGI2MDhkZjNhNjg3MDUwZWU0NzVkNzgxNDc2MTQwODU0Njg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"message":"0AB50AA5-84B8-4413-94D4-F02ABBDCC738","tree":{"sha":"2d11b1f07a6184ca5ef9b31f2922b5b7982a199f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2d11b1f07a6184ca5ef9b31f2922b5b7982a199f"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8fc7f0b608df3a687050ee475d78147614085468","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8fc7f0b608df3a687050ee475d78147614085468","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8fc7f0b608df3a687050ee475d78147614085468","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8fc7f0b608df3a687050ee475d78147614085468/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"7a3f81c3eed7d3f589a0f03bb5750d5a55306fc2","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7a3f81c3eed7d3f589a0f03bb5750d5a55306fc2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7a3f81c3eed7d3f589a0f03bb5750d5a55306fc2"}]},{"sha":"95b20c61857b1c6396fc404be4a90aa056e68085","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojk1YjIwYzYxODU3YjFjNjM5NmZjNDA0YmU0YTkwYWEwNTZlNjgwODU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"message":"28C563A3-A0E7-4B4A-A249-D92EA9BDC377","tree":{"sha":"b84fb699416ee4d744c92578a2097170a8d52af1","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b84fb699416ee4d744c92578a2097170a8d52af1"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/95b20c61857b1c6396fc404be4a90aa056e68085","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/95b20c61857b1c6396fc404be4a90aa056e68085","html_url":"https://github.com/ThiagoCodecov/example-python/commit/95b20c61857b1c6396fc404be4a90aa056e68085","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/95b20c61857b1c6396fc404be4a90aa056e68085/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"8fc7f0b608df3a687050ee475d78147614085468","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8fc7f0b608df3a687050ee475d78147614085468","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8fc7f0b608df3a687050ee475d78147614085468"}]},{"sha":"683e8eb4c2dcc18ca75d5a42d031c465e6a1c071","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjY4M2U4ZWI0YzJkY2MxOGNhNzVkNWE0MmQwMzFjNDY1ZTZhMWMwNzE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"message":"5B8ADA8B-BAF7-4A41-A810-D18CE9DF07DA","tree":{"sha":"dc1ac99f713d2f924111fda24460543282098f81","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/dc1ac99f713d2f924111fda24460543282098f81"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/683e8eb4c2dcc18ca75d5a42d031c465e6a1c071","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/683e8eb4c2dcc18ca75d5a42d031c465e6a1c071","html_url":"https://github.com/ThiagoCodecov/example-python/commit/683e8eb4c2dcc18ca75d5a42d031c465e6a1c071","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/683e8eb4c2dcc18ca75d5a42d031c465e6a1c071/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"95b20c61857b1c6396fc404be4a90aa056e68085","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/95b20c61857b1c6396fc404be4a90aa056e68085","html_url":"https://github.com/ThiagoCodecov/example-python/commit/95b20c61857b1c6396fc404be4a90aa056e68085"}]},{"sha":"25beb8cb4a88252e7fdd04dd9f40ea613ff54776","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjI1YmViOGNiNGE4ODI1MmU3ZmRkMDRkZDlmNDBlYTYxM2ZmNTQ3NzY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:55Z"},"message":"4149193B-D74A-454C-80B9-B62B4F470D1A","tree":{"sha":"4aeff083f58185aafe819845694fa432ef416a71","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4aeff083f58185aafe819845694fa432ef416a71"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/25beb8cb4a88252e7fdd04dd9f40ea613ff54776","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/25beb8cb4a88252e7fdd04dd9f40ea613ff54776","html_url":"https://github.com/ThiagoCodecov/example-python/commit/25beb8cb4a88252e7fdd04dd9f40ea613ff54776","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/25beb8cb4a88252e7fdd04dd9f40ea613ff54776/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"683e8eb4c2dcc18ca75d5a42d031c465e6a1c071","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/683e8eb4c2dcc18ca75d5a42d031c465e6a1c071","html_url":"https://github.com/ThiagoCodecov/example-python/commit/683e8eb4c2dcc18ca75d5a42d031c465e6a1c071"}]},{"sha":"e8e69ad93f82856174cf233cf367d315bb21fd0a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmU4ZTY5YWQ5M2Y4Mjg1NjE3NGNmMjMzY2YzNjdkMzE1YmIyMWZkMGE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"686EFCF7-80E9-4701-9CF3-865A2F99E21C","tree":{"sha":"619aec3032818cc02128798a6e048496f21421e6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/619aec3032818cc02128798a6e048496f21421e6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e8e69ad93f82856174cf233cf367d315bb21fd0a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e8e69ad93f82856174cf233cf367d315bb21fd0a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e8e69ad93f82856174cf233cf367d315bb21fd0a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e8e69ad93f82856174cf233cf367d315bb21fd0a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"25beb8cb4a88252e7fdd04dd9f40ea613ff54776","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/25beb8cb4a88252e7fdd04dd9f40ea613ff54776","html_url":"https://github.com/ThiagoCodecov/example-python/commit/25beb8cb4a88252e7fdd04dd9f40ea613ff54776"}]},{"sha":"f1bc03f6ca69001407f99a8516ba275e4640c48a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmYxYmMwM2Y2Y2E2OTAwMTQwN2Y5OWE4NTE2YmEyNzVlNDY0MGM0OGE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"CAE89BA7-5BCF-4371-8C15-309B898E50CA","tree":{"sha":"cf1863db20e5d068641cf420de667bdf5c31023e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/cf1863db20e5d068641cf420de667bdf5c31023e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f1bc03f6ca69001407f99a8516ba275e4640c48a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f1bc03f6ca69001407f99a8516ba275e4640c48a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f1bc03f6ca69001407f99a8516ba275e4640c48a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f1bc03f6ca69001407f99a8516ba275e4640c48a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e8e69ad93f82856174cf233cf367d315bb21fd0a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e8e69ad93f82856174cf233cf367d315bb21fd0a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e8e69ad93f82856174cf233cf367d315bb21fd0a"}]},{"sha":"49e8ad35a308b76d9c0f58fb7485ace459d1e6f0","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjQ5ZThhZDM1YTMwOGI3NmQ5YzBmNThmYjc0ODVhY2U0NTlkMWU2ZjA=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"610BC118-AECE-4A21-A278-60CCB3C80117","tree":{"sha":"74c858a49db7a976590e5ca2467925a6688ecd8f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/74c858a49db7a976590e5ca2467925a6688ecd8f"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/49e8ad35a308b76d9c0f58fb7485ace459d1e6f0","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/49e8ad35a308b76d9c0f58fb7485ace459d1e6f0","html_url":"https://github.com/ThiagoCodecov/example-python/commit/49e8ad35a308b76d9c0f58fb7485ace459d1e6f0","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/49e8ad35a308b76d9c0f58fb7485ace459d1e6f0/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f1bc03f6ca69001407f99a8516ba275e4640c48a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f1bc03f6ca69001407f99a8516ba275e4640c48a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f1bc03f6ca69001407f99a8516ba275e4640c48a"}]},{"sha":"2a8518d0c6c6ccb0591e17347ab24cc790fd665f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJhODUxOGQwYzZjNmNjYjA1OTFlMTczNDdhYjI0Y2M3OTBmZDY2NWY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"0BD05646-28EE-47A5-982D-472B961F2132","tree":{"sha":"ec5eef02a2654161663cce1bfee6af76c46f224a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ec5eef02a2654161663cce1bfee6af76c46f224a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2a8518d0c6c6ccb0591e17347ab24cc790fd665f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2a8518d0c6c6ccb0591e17347ab24cc790fd665f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2a8518d0c6c6ccb0591e17347ab24cc790fd665f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2a8518d0c6c6ccb0591e17347ab24cc790fd665f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"49e8ad35a308b76d9c0f58fb7485ace459d1e6f0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/49e8ad35a308b76d9c0f58fb7485ace459d1e6f0","html_url":"https://github.com/ThiagoCodecov/example-python/commit/49e8ad35a308b76d9c0f58fb7485ace459d1e6f0"}]},{"sha":"0cffc2b329397083ef146bfa637874614127ccd3","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjBjZmZjMmIzMjkzOTcwODNlZjE0NmJmYTYzNzg3NDYxNDEyN2NjZDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"78F1C3E9-45FF-4DED-B22F-7B6FABD0EED6","tree":{"sha":"59b62ecd5d8535bc5d4ae16cec4acdf246c3f9a0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/59b62ecd5d8535bc5d4ae16cec4acdf246c3f9a0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/0cffc2b329397083ef146bfa637874614127ccd3","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0cffc2b329397083ef146bfa637874614127ccd3","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0cffc2b329397083ef146bfa637874614127ccd3","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0cffc2b329397083ef146bfa637874614127ccd3/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2a8518d0c6c6ccb0591e17347ab24cc790fd665f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2a8518d0c6c6ccb0591e17347ab24cc790fd665f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2a8518d0c6c6ccb0591e17347ab24cc790fd665f"}]},{"sha":"fb7cab6107a1466e72964e4b06f48109d8accb7c","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmZiN2NhYjYxMDdhMTQ2NmU3Mjk2NGU0YjA2ZjQ4MTA5ZDhhY2NiN2M=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"46362C13-22C5-439D-A63D-00571730EABA","tree":{"sha":"eae502019e893e35880276bc0d63192d63571f86","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/eae502019e893e35880276bc0d63192d63571f86"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/fb7cab6107a1466e72964e4b06f48109d8accb7c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fb7cab6107a1466e72964e4b06f48109d8accb7c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/fb7cab6107a1466e72964e4b06f48109d8accb7c","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fb7cab6107a1466e72964e4b06f48109d8accb7c/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"0cffc2b329397083ef146bfa637874614127ccd3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0cffc2b329397083ef146bfa637874614127ccd3","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0cffc2b329397083ef146bfa637874614127ccd3"}]},{"sha":"b2279f48203fa8dac6f250f08d33b8be04cf5a8f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmIyMjc5ZjQ4MjAzZmE4ZGFjNmYyNTBmMDhkMzNiOGJlMDRjZjVhOGY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"3AF72174-D4FD-4077-A76A-FEA5B293E21E","tree":{"sha":"3a372351de741f5c2f50509dc3ffed31fe35e552","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3a372351de741f5c2f50509dc3ffed31fe35e552"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b2279f48203fa8dac6f250f08d33b8be04cf5a8f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b2279f48203fa8dac6f250f08d33b8be04cf5a8f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b2279f48203fa8dac6f250f08d33b8be04cf5a8f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b2279f48203fa8dac6f250f08d33b8be04cf5a8f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"fb7cab6107a1466e72964e4b06f48109d8accb7c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fb7cab6107a1466e72964e4b06f48109d8accb7c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/fb7cab6107a1466e72964e4b06f48109d8accb7c"}]},{"sha":"1bbfdab5519529f3ad398a23412debb90e181ee9","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjFiYmZkYWI1NTE5NTI5ZjNhZDM5OGEyMzQxMmRlYmI5MGUxODFlZTk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"3C8CC07D-6E86-49FE-8347-CCA049250D54","tree":{"sha":"de99f331fbcd7e919a62ee400b5e591c189123ab","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/de99f331fbcd7e919a62ee400b5e591c189123ab"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/1bbfdab5519529f3ad398a23412debb90e181ee9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1bbfdab5519529f3ad398a23412debb90e181ee9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1bbfdab5519529f3ad398a23412debb90e181ee9","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1bbfdab5519529f3ad398a23412debb90e181ee9/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"b2279f48203fa8dac6f250f08d33b8be04cf5a8f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b2279f48203fa8dac6f250f08d33b8be04cf5a8f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b2279f48203fa8dac6f250f08d33b8be04cf5a8f"}]},{"sha":"7b9ed866bf926b61414cca391c5684d0b96f4a43","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjdiOWVkODY2YmY5MjZiNjE0MTRjY2EzOTFjNTY4NGQwYjk2ZjRhNDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"170296CA-64A7-43CC-8A3D-B893C5AB95C0","tree":{"sha":"c6d157b4e8223c5959d3cccaab3b0e8163af3d5a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c6d157b4e8223c5959d3cccaab3b0e8163af3d5a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/7b9ed866bf926b61414cca391c5684d0b96f4a43","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7b9ed866bf926b61414cca391c5684d0b96f4a43","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7b9ed866bf926b61414cca391c5684d0b96f4a43","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7b9ed866bf926b61414cca391c5684d0b96f4a43/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"1bbfdab5519529f3ad398a23412debb90e181ee9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1bbfdab5519529f3ad398a23412debb90e181ee9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1bbfdab5519529f3ad398a23412debb90e181ee9"}]},{"sha":"e36d8be07d9e0122fd3f2ab5f6ef06bb9cb684cb","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmUzNmQ4YmUwN2Q5ZTAxMjJmZDNmMmFiNWY2ZWYwNmJiOWNiNjg0Y2I=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"F1D75BA7-9256-499A-A128-B237995E04FE","tree":{"sha":"31550441c71408e54014108c5067a983f2ab0b95","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/31550441c71408e54014108c5067a983f2ab0b95"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e36d8be07d9e0122fd3f2ab5f6ef06bb9cb684cb","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e36d8be07d9e0122fd3f2ab5f6ef06bb9cb684cb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e36d8be07d9e0122fd3f2ab5f6ef06bb9cb684cb","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e36d8be07d9e0122fd3f2ab5f6ef06bb9cb684cb/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"7b9ed866bf926b61414cca391c5684d0b96f4a43","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7b9ed866bf926b61414cca391c5684d0b96f4a43","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7b9ed866bf926b61414cca391c5684d0b96f4a43"}]},{"sha":"d419a0cf72f40e6adef91a815a3c8a85f912937b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmQ0MTlhMGNmNzJmNDBlNmFkZWY5MWE4MTVhM2M4YTg1ZjkxMjkzN2I=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"CB8131B8-FE52-4553-8A1A-214AC23AE83B","tree":{"sha":"443058d954af47c2c890ed8c396e98ab55ffa03b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/443058d954af47c2c890ed8c396e98ab55ffa03b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d419a0cf72f40e6adef91a815a3c8a85f912937b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d419a0cf72f40e6adef91a815a3c8a85f912937b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d419a0cf72f40e6adef91a815a3c8a85f912937b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d419a0cf72f40e6adef91a815a3c8a85f912937b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e36d8be07d9e0122fd3f2ab5f6ef06bb9cb684cb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e36d8be07d9e0122fd3f2ab5f6ef06bb9cb684cb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e36d8be07d9e0122fd3f2ab5f6ef06bb9cb684cb"}]},{"sha":"a4193d3f13c157199873e3a62ca7b94233615589","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmE0MTkzZDNmMTNjMTU3MTk5ODczZTNhNjJjYTdiOTQyMzM2MTU1ODk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"312AA733-B863-4040-96E0-5BEEFB1D3881","tree":{"sha":"9022dc10fc8e88aa539a4f0c1e53e9a40b92eff4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9022dc10fc8e88aa539a4f0c1e53e9a40b92eff4"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/a4193d3f13c157199873e3a62ca7b94233615589","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a4193d3f13c157199873e3a62ca7b94233615589","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a4193d3f13c157199873e3a62ca7b94233615589","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a4193d3f13c157199873e3a62ca7b94233615589/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"d419a0cf72f40e6adef91a815a3c8a85f912937b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d419a0cf72f40e6adef91a815a3c8a85f912937b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d419a0cf72f40e6adef91a815a3c8a85f912937b"}]},{"sha":"438b1152a52c3e46174aa0d3513236ad23dcfa2d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjQzOGIxMTUyYTUyYzNlNDYxNzRhYTBkMzUxMzIzNmFkMjNkY2ZhMmQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"C603DD08-D6D9-4B36-947F-9C5A531AF227","tree":{"sha":"1f7317cd260f530bb1e8bae320d98969fd41bd3d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/1f7317cd260f530bb1e8bae320d98969fd41bd3d"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/438b1152a52c3e46174aa0d3513236ad23dcfa2d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/438b1152a52c3e46174aa0d3513236ad23dcfa2d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/438b1152a52c3e46174aa0d3513236ad23dcfa2d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/438b1152a52c3e46174aa0d3513236ad23dcfa2d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"a4193d3f13c157199873e3a62ca7b94233615589","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a4193d3f13c157199873e3a62ca7b94233615589","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a4193d3f13c157199873e3a62ca7b94233615589"}]},{"sha":"cd6cacdd49d14663cabf9dfeda772619f6a3c89d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmNkNmNhY2RkNDlkMTQ2NjNjYWJmOWRmZWRhNzcyNjE5ZjZhM2M4OWQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"D9249A0E-7943-4D31-A945-E293876C1648","tree":{"sha":"f266a02798cee4d56712ab86c5887b9830af3085","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f266a02798cee4d56712ab86c5887b9830af3085"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/cd6cacdd49d14663cabf9dfeda772619f6a3c89d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cd6cacdd49d14663cabf9dfeda772619f6a3c89d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cd6cacdd49d14663cabf9dfeda772619f6a3c89d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cd6cacdd49d14663cabf9dfeda772619f6a3c89d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"438b1152a52c3e46174aa0d3513236ad23dcfa2d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/438b1152a52c3e46174aa0d3513236ad23dcfa2d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/438b1152a52c3e46174aa0d3513236ad23dcfa2d"}]},{"sha":"aa0d0e4e2186ec9c08a6a49551f9e40221705e31","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmFhMGQwZTRlMjE4NmVjOWMwOGE2YTQ5NTUxZjllNDAyMjE3MDVlMzE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"D4FB8D81-B4C9-4867-BE9B-C4D7842014D5","tree":{"sha":"0ba0edbf58e0916a07660e9d2eb28e6216085949","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/0ba0edbf58e0916a07660e9d2eb28e6216085949"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/aa0d0e4e2186ec9c08a6a49551f9e40221705e31","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/aa0d0e4e2186ec9c08a6a49551f9e40221705e31","html_url":"https://github.com/ThiagoCodecov/example-python/commit/aa0d0e4e2186ec9c08a6a49551f9e40221705e31","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/aa0d0e4e2186ec9c08a6a49551f9e40221705e31/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"cd6cacdd49d14663cabf9dfeda772619f6a3c89d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cd6cacdd49d14663cabf9dfeda772619f6a3c89d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cd6cacdd49d14663cabf9dfeda772619f6a3c89d"}]},{"sha":"fa96ddf11386a8cd61ab007fb0e844d076e79917","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmZhOTZkZGYxMTM4NmE4Y2Q2MWFiMDA3ZmIwZTg0NGQwNzZlNzk5MTc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"C9564863-9809-44AD-B36F-C4615E5FFF68","tree":{"sha":"67319b62753a3785756981abb437e460d8b798d7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/67319b62753a3785756981abb437e460d8b798d7"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/fa96ddf11386a8cd61ab007fb0e844d076e79917","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fa96ddf11386a8cd61ab007fb0e844d076e79917","html_url":"https://github.com/ThiagoCodecov/example-python/commit/fa96ddf11386a8cd61ab007fb0e844d076e79917","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fa96ddf11386a8cd61ab007fb0e844d076e79917/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"aa0d0e4e2186ec9c08a6a49551f9e40221705e31","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/aa0d0e4e2186ec9c08a6a49551f9e40221705e31","html_url":"https://github.com/ThiagoCodecov/example-python/commit/aa0d0e4e2186ec9c08a6a49551f9e40221705e31"}]},{"sha":"e5f7ebdbe2d963dde909652ba4d0fe06e2696f38","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmU1ZjdlYmRiZTJkOTYzZGRlOTA5NjUyYmE0ZDBmZTA2ZTI2OTZmMzg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"0553FA8B-8BA5-4949-A9BE-2560C615A675","tree":{"sha":"4a8cb3ffecbc99d9e47d94cc0220ac01d976b974","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4a8cb3ffecbc99d9e47d94cc0220ac01d976b974"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e5f7ebdbe2d963dde909652ba4d0fe06e2696f38","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e5f7ebdbe2d963dde909652ba4d0fe06e2696f38","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e5f7ebdbe2d963dde909652ba4d0fe06e2696f38","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e5f7ebdbe2d963dde909652ba4d0fe06e2696f38/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"fa96ddf11386a8cd61ab007fb0e844d076e79917","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fa96ddf11386a8cd61ab007fb0e844d076e79917","html_url":"https://github.com/ThiagoCodecov/example-python/commit/fa96ddf11386a8cd61ab007fb0e844d076e79917"}]},{"sha":"bfe0a44aaa6ba75d68c9b14a553dc29148b12071","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmJmZTBhNDRhYWE2YmE3NWQ2OGM5YjE0YTU1M2RjMjkxNDhiMTIwNzE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"EC904C72-119F-4A83-82A2-ECDE3DFDC809","tree":{"sha":"0b2cd8d987531393fa5cb9abc40f43ee60fc759d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/0b2cd8d987531393fa5cb9abc40f43ee60fc759d"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/bfe0a44aaa6ba75d68c9b14a553dc29148b12071","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bfe0a44aaa6ba75d68c9b14a553dc29148b12071","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bfe0a44aaa6ba75d68c9b14a553dc29148b12071","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bfe0a44aaa6ba75d68c9b14a553dc29148b12071/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e5f7ebdbe2d963dde909652ba4d0fe06e2696f38","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e5f7ebdbe2d963dde909652ba4d0fe06e2696f38","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e5f7ebdbe2d963dde909652ba4d0fe06e2696f38"}]},{"sha":"2ac57ac6c8f4bc264b07e7696a51c2ef432cdc2f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJhYzU3YWM2YzhmNGJjMjY0YjA3ZTc2OTZhNTFjMmVmNDMyY2RjMmY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"CCB65816-01D1-4F9C-A8E0-AEBE87B25F43","tree":{"sha":"ce8a9d84537092f704989e88cc4202365733ef0a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ce8a9d84537092f704989e88cc4202365733ef0a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2ac57ac6c8f4bc264b07e7696a51c2ef432cdc2f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2ac57ac6c8f4bc264b07e7696a51c2ef432cdc2f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2ac57ac6c8f4bc264b07e7696a51c2ef432cdc2f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2ac57ac6c8f4bc264b07e7696a51c2ef432cdc2f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"bfe0a44aaa6ba75d68c9b14a553dc29148b12071","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bfe0a44aaa6ba75d68c9b14a553dc29148b12071","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bfe0a44aaa6ba75d68c9b14a553dc29148b12071"}]},{"sha":"630b673de4141ae2a4a4be711604112c51ce3b7d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjYzMGI2NzNkZTQxNDFhZTJhNGE0YmU3MTE2MDQxMTJjNTFjZTNiN2Q=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"B3C79E73-2FF1-45D5-A57B-ECF5F73A0728","tree":{"sha":"f5d9e7a30e7fbd1e76581397d89c60bd27042204","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f5d9e7a30e7fbd1e76581397d89c60bd27042204"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/630b673de4141ae2a4a4be711604112c51ce3b7d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/630b673de4141ae2a4a4be711604112c51ce3b7d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/630b673de4141ae2a4a4be711604112c51ce3b7d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/630b673de4141ae2a4a4be711604112c51ce3b7d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2ac57ac6c8f4bc264b07e7696a51c2ef432cdc2f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2ac57ac6c8f4bc264b07e7696a51c2ef432cdc2f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2ac57ac6c8f4bc264b07e7696a51c2ef432cdc2f"}]},{"sha":"19ed6023b31ab5643855951c8aaaa023ac48accd","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjE5ZWQ2MDIzYjMxYWI1NjQzODU1OTUxYzhhYWFhMDIzYWM0OGFjY2Q=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"CF23D4EB-E977-413A-944D-D264A72A15E9","tree":{"sha":"f24a1275978329c93a649dc68e35c42eaf1d463d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f24a1275978329c93a649dc68e35c42eaf1d463d"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/19ed6023b31ab5643855951c8aaaa023ac48accd","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/19ed6023b31ab5643855951c8aaaa023ac48accd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/19ed6023b31ab5643855951c8aaaa023ac48accd","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/19ed6023b31ab5643855951c8aaaa023ac48accd/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"630b673de4141ae2a4a4be711604112c51ce3b7d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/630b673de4141ae2a4a4be711604112c51ce3b7d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/630b673de4141ae2a4a4be711604112c51ce3b7d"}]},{"sha":"3dc2616c02d19eea8dbe0843126c8b06289d12f3","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjNkYzI2MTZjMDJkMTllZWE4ZGJlMDg0MzEyNmM4YjA2Mjg5ZDEyZjM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"1B88A437-C1DD-460B-BA59-E8DEC451D27A","tree":{"sha":"4c6961428cb1c43e5a3cdfa1541033c2d1a1a6f3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4c6961428cb1c43e5a3cdfa1541033c2d1a1a6f3"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3dc2616c02d19eea8dbe0843126c8b06289d12f3","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3dc2616c02d19eea8dbe0843126c8b06289d12f3","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3dc2616c02d19eea8dbe0843126c8b06289d12f3","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3dc2616c02d19eea8dbe0843126c8b06289d12f3/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"19ed6023b31ab5643855951c8aaaa023ac48accd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/19ed6023b31ab5643855951c8aaaa023ac48accd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/19ed6023b31ab5643855951c8aaaa023ac48accd"}]},{"sha":"9a9699358b62474f69903d3df45e697d4174795f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjlhOTY5OTM1OGI2MjQ3NGY2OTkwM2QzZGY0NWU2OTdkNDE3NDc5NWY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"12247952-A37B-45C5-8625-E1517318ACC6","tree":{"sha":"1452b05d019d4b63d0ef6892249e9d87378cbfce","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/1452b05d019d4b63d0ef6892249e9d87378cbfce"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/9a9699358b62474f69903d3df45e697d4174795f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9a9699358b62474f69903d3df45e697d4174795f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9a9699358b62474f69903d3df45e697d4174795f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9a9699358b62474f69903d3df45e697d4174795f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"3dc2616c02d19eea8dbe0843126c8b06289d12f3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3dc2616c02d19eea8dbe0843126c8b06289d12f3","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3dc2616c02d19eea8dbe0843126c8b06289d12f3"}]},{"sha":"32ce123360fd63212e1cc0ca21ef9057d64fa40d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjMyY2UxMjMzNjBmZDYzMjEyZTFjYzBjYTIxZWY5MDU3ZDY0ZmE0MGQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"A7484750-D47E-4305-91F8-D38C0A90BAB7","tree":{"sha":"82d97214e5459736de5aed1d608b7f48e40cf336","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/82d97214e5459736de5aed1d608b7f48e40cf336"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/32ce123360fd63212e1cc0ca21ef9057d64fa40d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/32ce123360fd63212e1cc0ca21ef9057d64fa40d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/32ce123360fd63212e1cc0ca21ef9057d64fa40d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/32ce123360fd63212e1cc0ca21ef9057d64fa40d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"9a9699358b62474f69903d3df45e697d4174795f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9a9699358b62474f69903d3df45e697d4174795f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9a9699358b62474f69903d3df45e697d4174795f"}]},{"sha":"636a24301b149a59bbc52d93a0a583b41eeb8925","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjYzNmEyNDMwMWIxNDlhNTliYmM1MmQ5M2EwYTU4M2I0MWVlYjg5MjU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"994E84DF-B24D-4937-A883-6792CB4AD33F","tree":{"sha":"7faf727bff952cb6531fdfdb557d2b7ecace30e0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7faf727bff952cb6531fdfdb557d2b7ecace30e0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/636a24301b149a59bbc52d93a0a583b41eeb8925","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/636a24301b149a59bbc52d93a0a583b41eeb8925","html_url":"https://github.com/ThiagoCodecov/example-python/commit/636a24301b149a59bbc52d93a0a583b41eeb8925","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/636a24301b149a59bbc52d93a0a583b41eeb8925/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"32ce123360fd63212e1cc0ca21ef9057d64fa40d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/32ce123360fd63212e1cc0ca21ef9057d64fa40d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/32ce123360fd63212e1cc0ca21ef9057d64fa40d"}]},{"sha":"944144d7ad3d58a58b7f600c16c67f07464e92c2","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojk0NDE0NGQ3YWQzZDU4YTU4YjdmNjAwYzE2YzY3ZjA3NDY0ZTkyYzI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"D6BD9DFA-ED95-46CB-B6B9-75B809425B46","tree":{"sha":"e6aeebc297105afc5116f4287671bd2d4c51b678","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e6aeebc297105afc5116f4287671bd2d4c51b678"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/944144d7ad3d58a58b7f600c16c67f07464e92c2","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/944144d7ad3d58a58b7f600c16c67f07464e92c2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/944144d7ad3d58a58b7f600c16c67f07464e92c2","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/944144d7ad3d58a58b7f600c16c67f07464e92c2/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"636a24301b149a59bbc52d93a0a583b41eeb8925","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/636a24301b149a59bbc52d93a0a583b41eeb8925","html_url":"https://github.com/ThiagoCodecov/example-python/commit/636a24301b149a59bbc52d93a0a583b41eeb8925"}]},{"sha":"1fd98c03b1b74fc9dc259aa80f59c94602807171","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjFmZDk4YzAzYjFiNzRmYzlkYzI1OWFhODBmNTljOTQ2MDI4MDcxNzE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"82B1F75B-8D9F-40A1-A698-7C1A5D09834D","tree":{"sha":"1853a1f0f8754f13f69a6294b458edccc52c7101","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/1853a1f0f8754f13f69a6294b458edccc52c7101"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/1fd98c03b1b74fc9dc259aa80f59c94602807171","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1fd98c03b1b74fc9dc259aa80f59c94602807171","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1fd98c03b1b74fc9dc259aa80f59c94602807171","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1fd98c03b1b74fc9dc259aa80f59c94602807171/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"944144d7ad3d58a58b7f600c16c67f07464e92c2","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/944144d7ad3d58a58b7f600c16c67f07464e92c2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/944144d7ad3d58a58b7f600c16c67f07464e92c2"}]},{"sha":"87ffc9fb00fb84c557bc832f42dae152a398edd1","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg3ZmZjOWZiMDBmYjg0YzU1N2JjODMyZjQyZGFlMTUyYTM5OGVkZDE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"7E4A06FA-C23F-4B5A-A2DB-E5922CDC638B","tree":{"sha":"6c32914e234aa1cc37a5c195f6b335725949164d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6c32914e234aa1cc37a5c195f6b335725949164d"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/87ffc9fb00fb84c557bc832f42dae152a398edd1","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/87ffc9fb00fb84c557bc832f42dae152a398edd1","html_url":"https://github.com/ThiagoCodecov/example-python/commit/87ffc9fb00fb84c557bc832f42dae152a398edd1","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/87ffc9fb00fb84c557bc832f42dae152a398edd1/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"1fd98c03b1b74fc9dc259aa80f59c94602807171","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1fd98c03b1b74fc9dc259aa80f59c94602807171","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1fd98c03b1b74fc9dc259aa80f59c94602807171"}]},{"sha":"bbac1c497a966c873b71efbee7089b8407bb5d2d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmJiYWMxYzQ5N2E5NjZjODczYjcxZWZiZWU3MDg5Yjg0MDdiYjVkMmQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"4B3403C8-06A2-4F5F-ABFC-B750CE8E14CA","tree":{"sha":"39cdc80b5d1c1e98ff47a2e26aaf1101d41076ec","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/39cdc80b5d1c1e98ff47a2e26aaf1101d41076ec"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/bbac1c497a966c873b71efbee7089b8407bb5d2d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bbac1c497a966c873b71efbee7089b8407bb5d2d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bbac1c497a966c873b71efbee7089b8407bb5d2d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bbac1c497a966c873b71efbee7089b8407bb5d2d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"87ffc9fb00fb84c557bc832f42dae152a398edd1","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/87ffc9fb00fb84c557bc832f42dae152a398edd1","html_url":"https://github.com/ThiagoCodecov/example-python/commit/87ffc9fb00fb84c557bc832f42dae152a398edd1"}]},{"sha":"2ff88dd6e6fd8e06bca84b73f0e610343dfa6e07","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJmZjg4ZGQ2ZTZmZDhlMDZiY2E4NGI3M2YwZTYxMDM0M2RmYTZlMDc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"133C8F79-44DB-4C7C-A32E-97114A82E7CD","tree":{"sha":"53de816b5b7d98d20a5080685a3b463af61a31b3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/53de816b5b7d98d20a5080685a3b463af61a31b3"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2ff88dd6e6fd8e06bca84b73f0e610343dfa6e07","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2ff88dd6e6fd8e06bca84b73f0e610343dfa6e07","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2ff88dd6e6fd8e06bca84b73f0e610343dfa6e07","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2ff88dd6e6fd8e06bca84b73f0e610343dfa6e07/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"bbac1c497a966c873b71efbee7089b8407bb5d2d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bbac1c497a966c873b71efbee7089b8407bb5d2d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bbac1c497a966c873b71efbee7089b8407bb5d2d"}]},{"sha":"e4bce53dde3201d739a1033c57136b9e5569ca1e","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmU0YmNlNTNkZGUzMjAxZDczOWExMDMzYzU3MTM2YjllNTU2OWNhMWU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"BAA620F4-FC23-4B46-B6C9-D858077990CC","tree":{"sha":"7170eb5763744f4d343828f3c7c45fa6fabe4e98","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7170eb5763744f4d343828f3c7c45fa6fabe4e98"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e4bce53dde3201d739a1033c57136b9e5569ca1e","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e4bce53dde3201d739a1033c57136b9e5569ca1e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e4bce53dde3201d739a1033c57136b9e5569ca1e","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e4bce53dde3201d739a1033c57136b9e5569ca1e/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2ff88dd6e6fd8e06bca84b73f0e610343dfa6e07","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2ff88dd6e6fd8e06bca84b73f0e610343dfa6e07","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2ff88dd6e6fd8e06bca84b73f0e610343dfa6e07"}]},{"sha":"2e9f4bdcaedda83ded16ab26c8654516b69bf746","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJlOWY0YmRjYWVkZGE4M2RlZDE2YWIyNmM4NjU0NTE2YjY5YmY3NDY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"881E4D90-1DE5-455D-B316-22E1B7481CCF","tree":{"sha":"433885f2d494e492cec2d27cd46b2c877557150e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/433885f2d494e492cec2d27cd46b2c877557150e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2e9f4bdcaedda83ded16ab26c8654516b69bf746","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e9f4bdcaedda83ded16ab26c8654516b69bf746","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2e9f4bdcaedda83ded16ab26c8654516b69bf746","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e9f4bdcaedda83ded16ab26c8654516b69bf746/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e4bce53dde3201d739a1033c57136b9e5569ca1e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e4bce53dde3201d739a1033c57136b9e5569ca1e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e4bce53dde3201d739a1033c57136b9e5569ca1e"}]},{"sha":"5da001cade892e5a6924018f42b494aaf57780ed","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjVkYTAwMWNhZGU4OTJlNWE2OTI0MDE4ZjQyYjQ5NGFhZjU3NzgwZWQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:56Z"},"message":"09C12A65-4EEE-450E-8EA1-D68F893DBB7B","tree":{"sha":"8b91e8d8319945f12919fc31ac1d9cbe20d0eac7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/8b91e8d8319945f12919fc31ac1d9cbe20d0eac7"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/5da001cade892e5a6924018f42b494aaf57780ed","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/5da001cade892e5a6924018f42b494aaf57780ed","html_url":"https://github.com/ThiagoCodecov/example-python/commit/5da001cade892e5a6924018f42b494aaf57780ed","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/5da001cade892e5a6924018f42b494aaf57780ed/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2e9f4bdcaedda83ded16ab26c8654516b69bf746","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2e9f4bdcaedda83ded16ab26c8654516b69bf746","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2e9f4bdcaedda83ded16ab26c8654516b69bf746"}]},{"sha":"dfcc46d08541c7d3fbe600f29654aa55cc47aff5","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmRmY2M0NmQwODU0MWM3ZDNmYmU2MDBmMjk2NTRhYTU1Y2M0N2FmZjU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"925AB37C-91FA-46A1-9FB2-213EDBB2666A","tree":{"sha":"fc9b7b013bc45671a666b41e39b43260a25302bf","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/fc9b7b013bc45671a666b41e39b43260a25302bf"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/dfcc46d08541c7d3fbe600f29654aa55cc47aff5","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/dfcc46d08541c7d3fbe600f29654aa55cc47aff5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/dfcc46d08541c7d3fbe600f29654aa55cc47aff5","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/dfcc46d08541c7d3fbe600f29654aa55cc47aff5/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"5da001cade892e5a6924018f42b494aaf57780ed","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/5da001cade892e5a6924018f42b494aaf57780ed","html_url":"https://github.com/ThiagoCodecov/example-python/commit/5da001cade892e5a6924018f42b494aaf57780ed"}]},{"sha":"9e7485674efd723dfa348b2bdb7df5510a5fd74d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjllNzQ4NTY3NGVmZDcyM2RmYTM0OGIyYmRiN2RmNTUxMGE1ZmQ3NGQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"8EE54A60-7B62-40A8-9B76-AF391B5965A2","tree":{"sha":"736f203700b3f07d58c7624157d91526e756ae43","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/736f203700b3f07d58c7624157d91526e756ae43"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/9e7485674efd723dfa348b2bdb7df5510a5fd74d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9e7485674efd723dfa348b2bdb7df5510a5fd74d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9e7485674efd723dfa348b2bdb7df5510a5fd74d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9e7485674efd723dfa348b2bdb7df5510a5fd74d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"dfcc46d08541c7d3fbe600f29654aa55cc47aff5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/dfcc46d08541c7d3fbe600f29654aa55cc47aff5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/dfcc46d08541c7d3fbe600f29654aa55cc47aff5"}]},{"sha":"6ca33709cb8a64689f6133009a53d8d2112c8632","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjZjYTMzNzA5Y2I4YTY0Njg5ZjYxMzMwMDlhNTNkOGQyMTEyYzg2MzI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"76BE3ACC-FD3A-4D4E-B813-1D44B3199EEB","tree":{"sha":"e3266f4f924fdd3f0015b0375f3c0bfa6314a56d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e3266f4f924fdd3f0015b0375f3c0bfa6314a56d"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6ca33709cb8a64689f6133009a53d8d2112c8632","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ca33709cb8a64689f6133009a53d8d2112c8632","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6ca33709cb8a64689f6133009a53d8d2112c8632","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ca33709cb8a64689f6133009a53d8d2112c8632/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"9e7485674efd723dfa348b2bdb7df5510a5fd74d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9e7485674efd723dfa348b2bdb7df5510a5fd74d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9e7485674efd723dfa348b2bdb7df5510a5fd74d"}]},{"sha":"25c791d1e6cba5937d53bc82ea7b61a48e34fe5e","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjI1Yzc5MWQxZTZjYmE1OTM3ZDUzYmM4MmVhN2I2MWE0OGUzNGZlNWU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"D90E7807-ED27-4166-B3E6-6F6C67409FC2","tree":{"sha":"fa500f45b8dbbfb0bc74624958623a33007bc1cf","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/fa500f45b8dbbfb0bc74624958623a33007bc1cf"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/25c791d1e6cba5937d53bc82ea7b61a48e34fe5e","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/25c791d1e6cba5937d53bc82ea7b61a48e34fe5e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/25c791d1e6cba5937d53bc82ea7b61a48e34fe5e","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/25c791d1e6cba5937d53bc82ea7b61a48e34fe5e/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6ca33709cb8a64689f6133009a53d8d2112c8632","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ca33709cb8a64689f6133009a53d8d2112c8632","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6ca33709cb8a64689f6133009a53d8d2112c8632"}]},{"sha":"8c7d3ea008c75befa86cc13bd7e0dfda4eaf05f5","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjhjN2QzZWEwMDhjNzViZWZhODZjYzEzYmQ3ZTBkZmRhNGVhZjA1ZjU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"0AA79CA5-C259-4A2B-864C-B63789FCCF90","tree":{"sha":"198422d14e70f23b135ba6b3ac19e773c1346af5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/198422d14e70f23b135ba6b3ac19e773c1346af5"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8c7d3ea008c75befa86cc13bd7e0dfda4eaf05f5","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8c7d3ea008c75befa86cc13bd7e0dfda4eaf05f5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8c7d3ea008c75befa86cc13bd7e0dfda4eaf05f5","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8c7d3ea008c75befa86cc13bd7e0dfda4eaf05f5/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"25c791d1e6cba5937d53bc82ea7b61a48e34fe5e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/25c791d1e6cba5937d53bc82ea7b61a48e34fe5e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/25c791d1e6cba5937d53bc82ea7b61a48e34fe5e"}]},{"sha":"285cd47f93dcc29bf25357c5ed63a99971665d0c","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjI4NWNkNDdmOTNkY2MyOWJmMjUzNTdjNWVkNjNhOTk5NzE2NjVkMGM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"9A727A02-79DB-4CD7-BECA-CBF4F00F7438","tree":{"sha":"8cb26858088c08b0391e2a664b57578d915d38a5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/8cb26858088c08b0391e2a664b57578d915d38a5"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/285cd47f93dcc29bf25357c5ed63a99971665d0c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/285cd47f93dcc29bf25357c5ed63a99971665d0c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/285cd47f93dcc29bf25357c5ed63a99971665d0c","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/285cd47f93dcc29bf25357c5ed63a99971665d0c/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"8c7d3ea008c75befa86cc13bd7e0dfda4eaf05f5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8c7d3ea008c75befa86cc13bd7e0dfda4eaf05f5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8c7d3ea008c75befa86cc13bd7e0dfda4eaf05f5"}]},{"sha":"1221158a67e153a86cb01e0ffe9c59e589f7b46f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjEyMjExNThhNjdlMTUzYTg2Y2IwMWUwZmZlOWM1OWU1ODlmN2I0NmY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"38565A9C-F5C3-4C62-BB23-9AC672D8B807","tree":{"sha":"73b321034dcacbb5ee8e57c535f80903ccf1973a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/73b321034dcacbb5ee8e57c535f80903ccf1973a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/1221158a67e153a86cb01e0ffe9c59e589f7b46f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1221158a67e153a86cb01e0ffe9c59e589f7b46f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1221158a67e153a86cb01e0ffe9c59e589f7b46f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1221158a67e153a86cb01e0ffe9c59e589f7b46f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"285cd47f93dcc29bf25357c5ed63a99971665d0c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/285cd47f93dcc29bf25357c5ed63a99971665d0c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/285cd47f93dcc29bf25357c5ed63a99971665d0c"}]},{"sha":"b419247085e2627ed4aef6cee2b78c36363caa60","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmI0MTkyNDcwODVlMjYyN2VkNGFlZjZjZWUyYjc4YzM2MzYzY2FhNjA=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"3EC65182-8173-4180-A6B6-EEDBA63BC5E5","tree":{"sha":"1c0cb03364d01430278cf91da08be1539ddef23a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/1c0cb03364d01430278cf91da08be1539ddef23a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b419247085e2627ed4aef6cee2b78c36363caa60","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b419247085e2627ed4aef6cee2b78c36363caa60","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b419247085e2627ed4aef6cee2b78c36363caa60","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b419247085e2627ed4aef6cee2b78c36363caa60/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"1221158a67e153a86cb01e0ffe9c59e589f7b46f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1221158a67e153a86cb01e0ffe9c59e589f7b46f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1221158a67e153a86cb01e0ffe9c59e589f7b46f"}]},{"sha":"320f64c05e875501f79e846a6ae5249572692168","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjMyMGY2NGMwNWU4NzU1MDFmNzllODQ2YTZhZTUyNDk1NzI2OTIxNjg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"1FD764C4-0492-4CB6-B505-AAE7AAA8D317","tree":{"sha":"951e0d732caefce8ac5c861f4a5d253643822f7c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/951e0d732caefce8ac5c861f4a5d253643822f7c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/320f64c05e875501f79e846a6ae5249572692168","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/320f64c05e875501f79e846a6ae5249572692168","html_url":"https://github.com/ThiagoCodecov/example-python/commit/320f64c05e875501f79e846a6ae5249572692168","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/320f64c05e875501f79e846a6ae5249572692168/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"b419247085e2627ed4aef6cee2b78c36363caa60","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b419247085e2627ed4aef6cee2b78c36363caa60","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b419247085e2627ed4aef6cee2b78c36363caa60"}]},{"sha":"0721d998272b742f8d3b3d455bd3a29a17fda0a2","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA3MjFkOTk4MjcyYjc0MmY4ZDNiM2Q0NTViZDNhMjlhMTdmZGEwYTI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"210B2BB1-EA28-4CE7-8386-E731AB2A78CE","tree":{"sha":"c4090e2fa45a8d6da012819b744c72687e85cae3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c4090e2fa45a8d6da012819b744c72687e85cae3"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/0721d998272b742f8d3b3d455bd3a29a17fda0a2","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0721d998272b742f8d3b3d455bd3a29a17fda0a2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0721d998272b742f8d3b3d455bd3a29a17fda0a2","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0721d998272b742f8d3b3d455bd3a29a17fda0a2/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"320f64c05e875501f79e846a6ae5249572692168","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/320f64c05e875501f79e846a6ae5249572692168","html_url":"https://github.com/ThiagoCodecov/example-python/commit/320f64c05e875501f79e846a6ae5249572692168"}]},{"sha":"1e811079e7ea5e76eaa8b06ab12e257ef4794457","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjFlODExMDc5ZTdlYTVlNzZlYWE4YjA2YWIxMmUyNTdlZjQ3OTQ0NTc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"DB5D5357-5A1D-48C1-9710-1AC7649B3192","tree":{"sha":"c8a50e23eb8ec6c33dc426ac7cc40aef998a25ab","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c8a50e23eb8ec6c33dc426ac7cc40aef998a25ab"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/1e811079e7ea5e76eaa8b06ab12e257ef4794457","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e811079e7ea5e76eaa8b06ab12e257ef4794457","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1e811079e7ea5e76eaa8b06ab12e257ef4794457","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e811079e7ea5e76eaa8b06ab12e257ef4794457/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"0721d998272b742f8d3b3d455bd3a29a17fda0a2","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0721d998272b742f8d3b3d455bd3a29a17fda0a2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0721d998272b742f8d3b3d455bd3a29a17fda0a2"}]},{"sha":"f051ad1353e199f5e40640250aeb666c05d666c1","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmYwNTFhZDEzNTNlMTk5ZjVlNDA2NDAyNTBhZWI2NjZjMDVkNjY2YzE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"F2C9FC15-DC29-4E9E-99A9-F6F65DD9C735","tree":{"sha":"e74d016fe49fd945e8dfa86a83d263aa5a560515","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e74d016fe49fd945e8dfa86a83d263aa5a560515"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f051ad1353e199f5e40640250aeb666c05d666c1","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f051ad1353e199f5e40640250aeb666c05d666c1","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f051ad1353e199f5e40640250aeb666c05d666c1","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f051ad1353e199f5e40640250aeb666c05d666c1/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"1e811079e7ea5e76eaa8b06ab12e257ef4794457","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e811079e7ea5e76eaa8b06ab12e257ef4794457","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1e811079e7ea5e76eaa8b06ab12e257ef4794457"}]},{"sha":"3c6031bb33b0faef62c8ac0fc8c7d917eae68937","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjNjNjAzMWJiMzNiMGZhZWY2MmM4YWMwZmM4YzdkOTE3ZWFlNjg5Mzc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"EAF53ACD-8B14-4A03-82DD-813FB4A1350D","tree":{"sha":"652c481a5e89976ec9be5e17a66ead4c3fcf014f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/652c481a5e89976ec9be5e17a66ead4c3fcf014f"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3c6031bb33b0faef62c8ac0fc8c7d917eae68937","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3c6031bb33b0faef62c8ac0fc8c7d917eae68937","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3c6031bb33b0faef62c8ac0fc8c7d917eae68937","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3c6031bb33b0faef62c8ac0fc8c7d917eae68937/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f051ad1353e199f5e40640250aeb666c05d666c1","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f051ad1353e199f5e40640250aeb666c05d666c1","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f051ad1353e199f5e40640250aeb666c05d666c1"}]},{"sha":"f0f7b7c5c4d0560e8357a3daa58d30c92c89bb87","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmYwZjdiN2M1YzRkMDU2MGU4MzU3YTNkYWE1OGQzMGM5MmM4OWJiODc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"57573920-D53E-4730-9EF0-16C0E4B7FAF1","tree":{"sha":"8513020011a2b346f9076dc3579a922bb5a3c303","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/8513020011a2b346f9076dc3579a922bb5a3c303"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f0f7b7c5c4d0560e8357a3daa58d30c92c89bb87","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0f7b7c5c4d0560e8357a3daa58d30c92c89bb87","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f0f7b7c5c4d0560e8357a3daa58d30c92c89bb87","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0f7b7c5c4d0560e8357a3daa58d30c92c89bb87/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"3c6031bb33b0faef62c8ac0fc8c7d917eae68937","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3c6031bb33b0faef62c8ac0fc8c7d917eae68937","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3c6031bb33b0faef62c8ac0fc8c7d917eae68937"}]},{"sha":"51100bd22f7abaa21fc615d86e95f6d850a22b39","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjUxMTAwYmQyMmY3YWJhYTIxZmM2MTVkODZlOTVmNmQ4NTBhMjJiMzk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"772B5C66-C74F-4E8C-A02E-E8345CEE9440","tree":{"sha":"459cf1df7bb75be10d5c5c2964d45fe3bd80774c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/459cf1df7bb75be10d5c5c2964d45fe3bd80774c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/51100bd22f7abaa21fc615d86e95f6d850a22b39","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/51100bd22f7abaa21fc615d86e95f6d850a22b39","html_url":"https://github.com/ThiagoCodecov/example-python/commit/51100bd22f7abaa21fc615d86e95f6d850a22b39","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/51100bd22f7abaa21fc615d86e95f6d850a22b39/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f0f7b7c5c4d0560e8357a3daa58d30c92c89bb87","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f0f7b7c5c4d0560e8357a3daa58d30c92c89bb87","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f0f7b7c5c4d0560e8357a3daa58d30c92c89bb87"}]},{"sha":"a6a596025c30bcdd03e184dd610d65b39f5cd21d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmE2YTU5NjAyNWMzMGJjZGQwM2UxODRkZDYxMGQ2NWIzOWY1Y2QyMWQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"BA79BEFD-BECD-4F4D-9B69-979384FE3AC9","tree":{"sha":"6748f4b724c7f3c868b27b5a1754558cc79959e2","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6748f4b724c7f3c868b27b5a1754558cc79959e2"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/a6a596025c30bcdd03e184dd610d65b39f5cd21d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a6a596025c30bcdd03e184dd610d65b39f5cd21d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a6a596025c30bcdd03e184dd610d65b39f5cd21d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a6a596025c30bcdd03e184dd610d65b39f5cd21d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"51100bd22f7abaa21fc615d86e95f6d850a22b39","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/51100bd22f7abaa21fc615d86e95f6d850a22b39","html_url":"https://github.com/ThiagoCodecov/example-python/commit/51100bd22f7abaa21fc615d86e95f6d850a22b39"}]},{"sha":"c214450741d8d13b5aaab7cae07579cbd97d2144","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmMyMTQ0NTA3NDFkOGQxM2I1YWFhYjdjYWUwNzU3OWNiZDk3ZDIxNDQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"41F31BE2-6B32-4D53-A76E-8F8FC2B56D42","tree":{"sha":"aabad5d33ba2314bec1b7985bcdbe531f0c225b4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/aabad5d33ba2314bec1b7985bcdbe531f0c225b4"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c214450741d8d13b5aaab7cae07579cbd97d2144","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c214450741d8d13b5aaab7cae07579cbd97d2144","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c214450741d8d13b5aaab7cae07579cbd97d2144","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c214450741d8d13b5aaab7cae07579cbd97d2144/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"a6a596025c30bcdd03e184dd610d65b39f5cd21d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a6a596025c30bcdd03e184dd610d65b39f5cd21d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a6a596025c30bcdd03e184dd610d65b39f5cd21d"}]},{"sha":"7932e0a229768fc5ee7ddf4979fa233e58be6f9a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojc5MzJlMGEyMjk3NjhmYzVlZTdkZGY0OTc5ZmEyMzNlNThiZTZmOWE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"6AE17FC4-8545-45C3-A3C1-37D9D1C7D30A","tree":{"sha":"dca0b65d83de67fc37192a33555f3ef09fd3ccd9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/dca0b65d83de67fc37192a33555f3ef09fd3ccd9"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/7932e0a229768fc5ee7ddf4979fa233e58be6f9a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7932e0a229768fc5ee7ddf4979fa233e58be6f9a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7932e0a229768fc5ee7ddf4979fa233e58be6f9a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7932e0a229768fc5ee7ddf4979fa233e58be6f9a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"c214450741d8d13b5aaab7cae07579cbd97d2144","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c214450741d8d13b5aaab7cae07579cbd97d2144","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c214450741d8d13b5aaab7cae07579cbd97d2144"}]},{"sha":"4c27905283faf4ed46cabcce8c3d7de45884d74c","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjRjMjc5MDUyODNmYWY0ZWQ0NmNhYmNjZThjM2Q3ZGU0NTg4NGQ3NGM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"51AA6157-79BC-4CA6-B977-F0838E4C5E20","tree":{"sha":"352e6a4f88ee760009a041a50a2233ceb9a23d03","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/352e6a4f88ee760009a041a50a2233ceb9a23d03"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/4c27905283faf4ed46cabcce8c3d7de45884d74c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4c27905283faf4ed46cabcce8c3d7de45884d74c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/4c27905283faf4ed46cabcce8c3d7de45884d74c","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4c27905283faf4ed46cabcce8c3d7de45884d74c/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"7932e0a229768fc5ee7ddf4979fa233e58be6f9a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7932e0a229768fc5ee7ddf4979fa233e58be6f9a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7932e0a229768fc5ee7ddf4979fa233e58be6f9a"}]},{"sha":"d2822c9103449a1199c390263fde45826731d5dc","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmQyODIyYzkxMDM0NDlhMTE5OWMzOTAyNjNmZGU0NTgyNjczMWQ1ZGM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"550978BD-7D4D-4FC9-8E4F-CD8132710C63","tree":{"sha":"76329bc8c2b5e797c2b8fd00da6320cac26b376a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/76329bc8c2b5e797c2b8fd00da6320cac26b376a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d2822c9103449a1199c390263fde45826731d5dc","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2822c9103449a1199c390263fde45826731d5dc","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d2822c9103449a1199c390263fde45826731d5dc","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2822c9103449a1199c390263fde45826731d5dc/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"4c27905283faf4ed46cabcce8c3d7de45884d74c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4c27905283faf4ed46cabcce8c3d7de45884d74c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/4c27905283faf4ed46cabcce8c3d7de45884d74c"}]},{"sha":"d80068217262779f767eb336229efb6ac1f830db","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmQ4MDA2ODIxNzI2Mjc3OWY3NjdlYjMzNjIyOWVmYjZhYzFmODMwZGI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"4FBA3653-F944-4771-A72C-97AAA48A255D","tree":{"sha":"84248ff9b70ac6adf72abe96af95d7c3bf455715","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/84248ff9b70ac6adf72abe96af95d7c3bf455715"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d80068217262779f767eb336229efb6ac1f830db","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d80068217262779f767eb336229efb6ac1f830db","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d80068217262779f767eb336229efb6ac1f830db","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d80068217262779f767eb336229efb6ac1f830db/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"d2822c9103449a1199c390263fde45826731d5dc","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d2822c9103449a1199c390263fde45826731d5dc","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d2822c9103449a1199c390263fde45826731d5dc"}]},{"sha":"bd2132443a5ad5dd979ab5744e6beb4f66fb6a65","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmJkMjEzMjQ0M2E1YWQ1ZGQ5NzlhYjU3NDRlNmJlYjRmNjZmYjZhNjU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"C38BFA76-331E-4556-8A65-96448CB61B21","tree":{"sha":"69010dee29d2f8124c0875aa5d85922953cd179f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/69010dee29d2f8124c0875aa5d85922953cd179f"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/bd2132443a5ad5dd979ab5744e6beb4f66fb6a65","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bd2132443a5ad5dd979ab5744e6beb4f66fb6a65","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bd2132443a5ad5dd979ab5744e6beb4f66fb6a65","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bd2132443a5ad5dd979ab5744e6beb4f66fb6a65/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"d80068217262779f767eb336229efb6ac1f830db","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d80068217262779f767eb336229efb6ac1f830db","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d80068217262779f767eb336229efb6ac1f830db"}]},{"sha":"a8152a3f456e115ba9d2afae28aa8d9faf2b407b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmE4MTUyYTNmNDU2ZTExNWJhOWQyYWZhZTI4YWE4ZDlmYWYyYjQwN2I=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"3862DB85-3D14-455D-99B3-E805BA7F6132","tree":{"sha":"40be7d8cda48bcd9185007313648789412d6a8cc","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/40be7d8cda48bcd9185007313648789412d6a8cc"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/a8152a3f456e115ba9d2afae28aa8d9faf2b407b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a8152a3f456e115ba9d2afae28aa8d9faf2b407b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a8152a3f456e115ba9d2afae28aa8d9faf2b407b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a8152a3f456e115ba9d2afae28aa8d9faf2b407b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"bd2132443a5ad5dd979ab5744e6beb4f66fb6a65","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bd2132443a5ad5dd979ab5744e6beb4f66fb6a65","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bd2132443a5ad5dd979ab5744e6beb4f66fb6a65"}]},{"sha":"8f007b0c2d68532d682e112cd550f2038dcdd60f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjhmMDA3YjBjMmQ2ODUzMmQ2ODJlMTEyY2Q1NTBmMjAzOGRjZGQ2MGY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"959BA2E3-668A-4CB5-9406-F9BB69154BF4","tree":{"sha":"46f143ebff58ca10f36685aa35cfa4979aed23f3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/46f143ebff58ca10f36685aa35cfa4979aed23f3"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8f007b0c2d68532d682e112cd550f2038dcdd60f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8f007b0c2d68532d682e112cd550f2038dcdd60f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8f007b0c2d68532d682e112cd550f2038dcdd60f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8f007b0c2d68532d682e112cd550f2038dcdd60f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"a8152a3f456e115ba9d2afae28aa8d9faf2b407b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a8152a3f456e115ba9d2afae28aa8d9faf2b407b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a8152a3f456e115ba9d2afae28aa8d9faf2b407b"}]},{"sha":"e4f468cd17e4d99d928742b379797947ba6e6ff2","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmU0ZjQ2OGNkMTdlNGQ5OWQ5Mjg3NDJiMzc5Nzk3OTQ3YmE2ZTZmZjI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"8DD798F6-75C7-4D0F-A7CF-3BF9C7524DF2","tree":{"sha":"4ebedcecaf4fe36404de4aa72d9474fb7fd6410e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4ebedcecaf4fe36404de4aa72d9474fb7fd6410e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e4f468cd17e4d99d928742b379797947ba6e6ff2","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e4f468cd17e4d99d928742b379797947ba6e6ff2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e4f468cd17e4d99d928742b379797947ba6e6ff2","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e4f468cd17e4d99d928742b379797947ba6e6ff2/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"8f007b0c2d68532d682e112cd550f2038dcdd60f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8f007b0c2d68532d682e112cd550f2038dcdd60f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8f007b0c2d68532d682e112cd550f2038dcdd60f"}]},{"sha":"1533f4cb337b3d0ed0ee3edf3c6f23559a50819b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjE1MzNmNGNiMzM3YjNkMGVkMGVlM2VkZjNjNmYyMzU1OWE1MDgxOWI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"B3022B4C-9D1D-40AE-8DDE-BCB4C635AD0B","tree":{"sha":"cdbdfddc3a2554d782378d2a1a9aa71267932478","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/cdbdfddc3a2554d782378d2a1a9aa71267932478"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/1533f4cb337b3d0ed0ee3edf3c6f23559a50819b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1533f4cb337b3d0ed0ee3edf3c6f23559a50819b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1533f4cb337b3d0ed0ee3edf3c6f23559a50819b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1533f4cb337b3d0ed0ee3edf3c6f23559a50819b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e4f468cd17e4d99d928742b379797947ba6e6ff2","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e4f468cd17e4d99d928742b379797947ba6e6ff2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e4f468cd17e4d99d928742b379797947ba6e6ff2"}]},{"sha":"5bee08662128404ff06b4de4f918ae85eca40c04","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjViZWUwODY2MjEyODQwNGZmMDZiNGRlNGY5MThhZTg1ZWNhNDBjMDQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"030B42AA-EB2C-456F-8B55-EC355BA03188","tree":{"sha":"b7647598568ace029cee9a2f21cf38cf9fc4a841","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b7647598568ace029cee9a2f21cf38cf9fc4a841"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/5bee08662128404ff06b4de4f918ae85eca40c04","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/5bee08662128404ff06b4de4f918ae85eca40c04","html_url":"https://github.com/ThiagoCodecov/example-python/commit/5bee08662128404ff06b4de4f918ae85eca40c04","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/5bee08662128404ff06b4de4f918ae85eca40c04/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"1533f4cb337b3d0ed0ee3edf3c6f23559a50819b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1533f4cb337b3d0ed0ee3edf3c6f23559a50819b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1533f4cb337b3d0ed0ee3edf3c6f23559a50819b"}]},{"sha":"ec615ecaa1d92c9aa7008fa7d7a1031e0f783838","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmVjNjE1ZWNhYTFkOTJjOWFhNzAwOGZhN2Q3YTEwMzFlMGY3ODM4Mzg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"7317C91E-A371-44AC-8D10-F46C036DCAB7","tree":{"sha":"03329da7b7e792ff5f4529f4be2e819c3aa2617f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/03329da7b7e792ff5f4529f4be2e819c3aa2617f"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ec615ecaa1d92c9aa7008fa7d7a1031e0f783838","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ec615ecaa1d92c9aa7008fa7d7a1031e0f783838","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ec615ecaa1d92c9aa7008fa7d7a1031e0f783838","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ec615ecaa1d92c9aa7008fa7d7a1031e0f783838/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"5bee08662128404ff06b4de4f918ae85eca40c04","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/5bee08662128404ff06b4de4f918ae85eca40c04","html_url":"https://github.com/ThiagoCodecov/example-python/commit/5bee08662128404ff06b4de4f918ae85eca40c04"}]},{"sha":"6dd614be3041e377f30620cf091c96639da45bc9","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjZkZDYxNGJlMzA0MWUzNzdmMzA2MjBjZjA5MWM5NjYzOWRhNDViYzk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"70C5D376-DFAF-4C86-9480-C97E955FFDF9","tree":{"sha":"418ee233398b129a5bd621f813a574c60b0f33b8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/418ee233398b129a5bd621f813a574c60b0f33b8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6dd614be3041e377f30620cf091c96639da45bc9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6dd614be3041e377f30620cf091c96639da45bc9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6dd614be3041e377f30620cf091c96639da45bc9","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6dd614be3041e377f30620cf091c96639da45bc9/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ec615ecaa1d92c9aa7008fa7d7a1031e0f783838","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ec615ecaa1d92c9aa7008fa7d7a1031e0f783838","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ec615ecaa1d92c9aa7008fa7d7a1031e0f783838"}]},{"sha":"8aadd17a7c0166405dd4b2359765714be14670da","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjhhYWRkMTdhN2MwMTY2NDA1ZGQ0YjIzNTk3NjU3MTRiZTE0NjcwZGE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"78232D7E-447A-4B28-ACD3-7B5FFF3B51B2","tree":{"sha":"eb8ac115c3d56c64b550ccb4c6f5b46d0f59f553","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/eb8ac115c3d56c64b550ccb4c6f5b46d0f59f553"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8aadd17a7c0166405dd4b2359765714be14670da","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8aadd17a7c0166405dd4b2359765714be14670da","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8aadd17a7c0166405dd4b2359765714be14670da","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8aadd17a7c0166405dd4b2359765714be14670da/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6dd614be3041e377f30620cf091c96639da45bc9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6dd614be3041e377f30620cf091c96639da45bc9","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6dd614be3041e377f30620cf091c96639da45bc9"}]},{"sha":"14e7ffb76f739afcd04689bd17a4cec803798359","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjE0ZTdmZmI3NmY3MzlhZmNkMDQ2ODliZDE3YTRjZWM4MDM3OTgzNTk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"EE429FD5-2729-4704-829E-2BEF042930DA","tree":{"sha":"81aa997024a6a90d5c70328223de1b790845a454","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/81aa997024a6a90d5c70328223de1b790845a454"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/14e7ffb76f739afcd04689bd17a4cec803798359","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/14e7ffb76f739afcd04689bd17a4cec803798359","html_url":"https://github.com/ThiagoCodecov/example-python/commit/14e7ffb76f739afcd04689bd17a4cec803798359","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/14e7ffb76f739afcd04689bd17a4cec803798359/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"8aadd17a7c0166405dd4b2359765714be14670da","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8aadd17a7c0166405dd4b2359765714be14670da","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8aadd17a7c0166405dd4b2359765714be14670da"}]},{"sha":"2c1eb1598156d945c0d98fed7475aa34f908d48e","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJjMWViMTU5ODE1NmQ5NDVjMGQ5OGZlZDc0NzVhYTM0ZjkwOGQ0OGU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"AE177E4B-813E-480E-9F9A-FEA5F91D2F40","tree":{"sha":"a23c82737a921ec48fa7812584c92e91b8b3ec94","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/a23c82737a921ec48fa7812584c92e91b8b3ec94"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2c1eb1598156d945c0d98fed7475aa34f908d48e","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2c1eb1598156d945c0d98fed7475aa34f908d48e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2c1eb1598156d945c0d98fed7475aa34f908d48e","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2c1eb1598156d945c0d98fed7475aa34f908d48e/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"14e7ffb76f739afcd04689bd17a4cec803798359","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/14e7ffb76f739afcd04689bd17a4cec803798359","html_url":"https://github.com/ThiagoCodecov/example-python/commit/14e7ffb76f739afcd04689bd17a4cec803798359"}]},{"sha":"06897ce8789598ae5dfec918ddd9bc55a2eceb23","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA2ODk3Y2U4Nzg5NTk4YWU1ZGZlYzkxOGRkZDliYzU1YTJlY2ViMjM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:57Z"},"message":"2FDED016-F563-4772-B4F4-84F4D6ADD3E5","tree":{"sha":"caaad41237c0a27d5351562205509b600326918b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/caaad41237c0a27d5351562205509b600326918b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/06897ce8789598ae5dfec918ddd9bc55a2eceb23","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/06897ce8789598ae5dfec918ddd9bc55a2eceb23","html_url":"https://github.com/ThiagoCodecov/example-python/commit/06897ce8789598ae5dfec918ddd9bc55a2eceb23","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/06897ce8789598ae5dfec918ddd9bc55a2eceb23/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2c1eb1598156d945c0d98fed7475aa34f908d48e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2c1eb1598156d945c0d98fed7475aa34f908d48e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2c1eb1598156d945c0d98fed7475aa34f908d48e"}]},{"sha":"96ca08e30233c6c3941d25e187ea3174f092bdd5","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojk2Y2EwOGUzMDIzM2M2YzM5NDFkMjVlMTg3ZWEzMTc0ZjA5MmJkZDU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"0B65977E-2204-4CB7-8327-5127533B2D73","tree":{"sha":"e712195ccb02a864fea4868f2301b294eeddc429","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e712195ccb02a864fea4868f2301b294eeddc429"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/96ca08e30233c6c3941d25e187ea3174f092bdd5","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/96ca08e30233c6c3941d25e187ea3174f092bdd5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/96ca08e30233c6c3941d25e187ea3174f092bdd5","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/96ca08e30233c6c3941d25e187ea3174f092bdd5/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"06897ce8789598ae5dfec918ddd9bc55a2eceb23","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/06897ce8789598ae5dfec918ddd9bc55a2eceb23","html_url":"https://github.com/ThiagoCodecov/example-python/commit/06897ce8789598ae5dfec918ddd9bc55a2eceb23"}]},{"sha":"189c5d027658a859959c9f655e6fa4a065b87075","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjE4OWM1ZDAyNzY1OGE4NTk5NTljOWY2NTVlNmZhNGEwNjViODcwNzU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"DA5AE212-19E8-4686-BFFF-4739DBE6F6C0","tree":{"sha":"ab22abe3d423117bdbfa4ee72d8d9125c2bca43e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ab22abe3d423117bdbfa4ee72d8d9125c2bca43e"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/189c5d027658a859959c9f655e6fa4a065b87075","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/189c5d027658a859959c9f655e6fa4a065b87075","html_url":"https://github.com/ThiagoCodecov/example-python/commit/189c5d027658a859959c9f655e6fa4a065b87075","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/189c5d027658a859959c9f655e6fa4a065b87075/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"96ca08e30233c6c3941d25e187ea3174f092bdd5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/96ca08e30233c6c3941d25e187ea3174f092bdd5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/96ca08e30233c6c3941d25e187ea3174f092bdd5"}]},{"sha":"23553e6759458084f786872aa3cb75a4104c9af1","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjIzNTUzZTY3NTk0NTgwODRmNzg2ODcyYWEzY2I3NWE0MTA0YzlhZjE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"7F1A5A4D-1F9B-4F8C-8D4F-707B1AA1B306","tree":{"sha":"f5e073e84a81ed8cad8537e605fdfc00679bf7af","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f5e073e84a81ed8cad8537e605fdfc00679bf7af"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/23553e6759458084f786872aa3cb75a4104c9af1","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/23553e6759458084f786872aa3cb75a4104c9af1","html_url":"https://github.com/ThiagoCodecov/example-python/commit/23553e6759458084f786872aa3cb75a4104c9af1","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/23553e6759458084f786872aa3cb75a4104c9af1/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"189c5d027658a859959c9f655e6fa4a065b87075","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/189c5d027658a859959c9f655e6fa4a065b87075","html_url":"https://github.com/ThiagoCodecov/example-python/commit/189c5d027658a859959c9f655e6fa4a065b87075"}]},{"sha":"057cb2a6b660b7cb72b486c7cfa732ee524b2159","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA1N2NiMmE2YjY2MGI3Y2I3MmI0ODZjN2NmYTczMmVlNTI0YjIxNTk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"41E574A5-EC85-4400-93A5-9D2355742840","tree":{"sha":"f9a01c6004533a4f825c81d1146635d6e215dec6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f9a01c6004533a4f825c81d1146635d6e215dec6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/057cb2a6b660b7cb72b486c7cfa732ee524b2159","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/057cb2a6b660b7cb72b486c7cfa732ee524b2159","html_url":"https://github.com/ThiagoCodecov/example-python/commit/057cb2a6b660b7cb72b486c7cfa732ee524b2159","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/057cb2a6b660b7cb72b486c7cfa732ee524b2159/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"23553e6759458084f786872aa3cb75a4104c9af1","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/23553e6759458084f786872aa3cb75a4104c9af1","html_url":"https://github.com/ThiagoCodecov/example-python/commit/23553e6759458084f786872aa3cb75a4104c9af1"}]},{"sha":"e8a1baa295e6712b5e7b89fced258e2ef87ead80","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmU4YTFiYWEyOTVlNjcxMmI1ZTdiODlmY2VkMjU4ZTJlZjg3ZWFkODA=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"D97041F4-BF94-4D8C-9721-7099E227AB85","tree":{"sha":"6faeaa9507037e78d2fb8de0ed7707a700490447","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6faeaa9507037e78d2fb8de0ed7707a700490447"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e8a1baa295e6712b5e7b89fced258e2ef87ead80","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e8a1baa295e6712b5e7b89fced258e2ef87ead80","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e8a1baa295e6712b5e7b89fced258e2ef87ead80","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e8a1baa295e6712b5e7b89fced258e2ef87ead80/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"057cb2a6b660b7cb72b486c7cfa732ee524b2159","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/057cb2a6b660b7cb72b486c7cfa732ee524b2159","html_url":"https://github.com/ThiagoCodecov/example-python/commit/057cb2a6b660b7cb72b486c7cfa732ee524b2159"}]},{"sha":"2390924ab012cbe70880405a5459d27ea92f6f7a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjIzOTA5MjRhYjAxMmNiZTcwODgwNDA1YTU0NTlkMjdlYTkyZjZmN2E=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"109A382E-8BE6-4578-954D-4131DE06064C","tree":{"sha":"5c95f29223218fe134cc2db0e6e2c8254726108a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/5c95f29223218fe134cc2db0e6e2c8254726108a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2390924ab012cbe70880405a5459d27ea92f6f7a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2390924ab012cbe70880405a5459d27ea92f6f7a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2390924ab012cbe70880405a5459d27ea92f6f7a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2390924ab012cbe70880405a5459d27ea92f6f7a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e8a1baa295e6712b5e7b89fced258e2ef87ead80","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e8a1baa295e6712b5e7b89fced258e2ef87ead80","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e8a1baa295e6712b5e7b89fced258e2ef87ead80"}]},{"sha":"d76b42be3b07e4faeb50b285135b582be5f41e5d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmQ3NmI0MmJlM2IwN2U0ZmFlYjUwYjI4NTEzNWI1ODJiZTVmNDFlNWQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"6F1928E6-2D10-4679-B55E-D1A36F149858","tree":{"sha":"2c6e0da8162707fbf913366c1c7a9fb468df245b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2c6e0da8162707fbf913366c1c7a9fb468df245b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d76b42be3b07e4faeb50b285135b582be5f41e5d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d76b42be3b07e4faeb50b285135b582be5f41e5d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d76b42be3b07e4faeb50b285135b582be5f41e5d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d76b42be3b07e4faeb50b285135b582be5f41e5d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2390924ab012cbe70880405a5459d27ea92f6f7a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2390924ab012cbe70880405a5459d27ea92f6f7a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2390924ab012cbe70880405a5459d27ea92f6f7a"}]},{"sha":"e45417219db212a1d9c03708c292e3e2d9107884","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmU0NTQxNzIxOWRiMjEyYTFkOWMwMzcwOGMyOTJlM2UyZDkxMDc4ODQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"58D5AFAF-A1C7-485E-908F-8A3C61328DA2","tree":{"sha":"587656357d7887d9d724a1d55dec3e80b63b53bd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/587656357d7887d9d724a1d55dec3e80b63b53bd"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e45417219db212a1d9c03708c292e3e2d9107884","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e45417219db212a1d9c03708c292e3e2d9107884","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e45417219db212a1d9c03708c292e3e2d9107884","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e45417219db212a1d9c03708c292e3e2d9107884/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"d76b42be3b07e4faeb50b285135b582be5f41e5d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d76b42be3b07e4faeb50b285135b582be5f41e5d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d76b42be3b07e4faeb50b285135b582be5f41e5d"}]},{"sha":"5f23debdfdcfb8ef6ab077483b32763c1693364a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjVmMjNkZWJkZmRjZmI4ZWY2YWIwNzc0ODNiMzI3NjNjMTY5MzM2NGE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"C6CDE269-A257-4956-8CB1-165B25D9A3B7","tree":{"sha":"c310901f0b5e0e16cc3fc2da38e1532887caa834","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c310901f0b5e0e16cc3fc2da38e1532887caa834"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/5f23debdfdcfb8ef6ab077483b32763c1693364a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/5f23debdfdcfb8ef6ab077483b32763c1693364a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/5f23debdfdcfb8ef6ab077483b32763c1693364a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/5f23debdfdcfb8ef6ab077483b32763c1693364a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e45417219db212a1d9c03708c292e3e2d9107884","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e45417219db212a1d9c03708c292e3e2d9107884","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e45417219db212a1d9c03708c292e3e2d9107884"}]},{"sha":"0ac08ebe32a18fe51bef77b724f5cd2a42ae2a75","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjBhYzA4ZWJlMzJhMThmZTUxYmVmNzdiNzI0ZjVjZDJhNDJhZTJhNzU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"99AECBAA-97F7-4A65-B79A-C114D925D9F7","tree":{"sha":"c4049ebe4f3cb6ef38623adaa0a0c05bf122e2a5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c4049ebe4f3cb6ef38623adaa0a0c05bf122e2a5"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/0ac08ebe32a18fe51bef77b724f5cd2a42ae2a75","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0ac08ebe32a18fe51bef77b724f5cd2a42ae2a75","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0ac08ebe32a18fe51bef77b724f5cd2a42ae2a75","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0ac08ebe32a18fe51bef77b724f5cd2a42ae2a75/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"5f23debdfdcfb8ef6ab077483b32763c1693364a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/5f23debdfdcfb8ef6ab077483b32763c1693364a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/5f23debdfdcfb8ef6ab077483b32763c1693364a"}]},{"sha":"22fb105345b290a363999301cd1d9b636bb71ec3","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjIyZmIxMDUzNDViMjkwYTM2Mzk5OTMwMWNkMWQ5YjYzNmJiNzFlYzM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"04492768-BC9E-43B1-8A8A-79B4683C63AF","tree":{"sha":"91b35e05a9e0c032df342a849aca7232afbb1833","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/91b35e05a9e0c032df342a849aca7232afbb1833"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/22fb105345b290a363999301cd1d9b636bb71ec3","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/22fb105345b290a363999301cd1d9b636bb71ec3","html_url":"https://github.com/ThiagoCodecov/example-python/commit/22fb105345b290a363999301cd1d9b636bb71ec3","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/22fb105345b290a363999301cd1d9b636bb71ec3/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"0ac08ebe32a18fe51bef77b724f5cd2a42ae2a75","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0ac08ebe32a18fe51bef77b724f5cd2a42ae2a75","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0ac08ebe32a18fe51bef77b724f5cd2a42ae2a75"}]},{"sha":"d5a31a3ed97a8ec36ed850f76c003cfcb96d4b72","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmQ1YTMxYTNlZDk3YThlYzM2ZWQ4NTBmNzZjMDAzY2ZjYjk2ZDRiNzI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"B3926553-07D7-4C93-8C4C-57CB052930C0","tree":{"sha":"4b54ea68d21cc4235b2968e96256be3719cd6798","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4b54ea68d21cc4235b2968e96256be3719cd6798"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d5a31a3ed97a8ec36ed850f76c003cfcb96d4b72","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d5a31a3ed97a8ec36ed850f76c003cfcb96d4b72","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d5a31a3ed97a8ec36ed850f76c003cfcb96d4b72","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d5a31a3ed97a8ec36ed850f76c003cfcb96d4b72/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"22fb105345b290a363999301cd1d9b636bb71ec3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/22fb105345b290a363999301cd1d9b636bb71ec3","html_url":"https://github.com/ThiagoCodecov/example-python/commit/22fb105345b290a363999301cd1d9b636bb71ec3"}]},{"sha":"8d7ba29d364a1ec37ce43b0230ab40c195fedd10","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjhkN2JhMjlkMzY0YTFlYzM3Y2U0M2IwMjMwYWI0MGMxOTVmZWRkMTA=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"0DC3369A-759C-4C71-99FD-7914C1890F39","tree":{"sha":"b7a07832d5a8529c366b6b04370b44062fc12173","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b7a07832d5a8529c366b6b04370b44062fc12173"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8d7ba29d364a1ec37ce43b0230ab40c195fedd10","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d7ba29d364a1ec37ce43b0230ab40c195fedd10","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8d7ba29d364a1ec37ce43b0230ab40c195fedd10","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d7ba29d364a1ec37ce43b0230ab40c195fedd10/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"d5a31a3ed97a8ec36ed850f76c003cfcb96d4b72","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d5a31a3ed97a8ec36ed850f76c003cfcb96d4b72","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d5a31a3ed97a8ec36ed850f76c003cfcb96d4b72"}]},{"sha":"fd3f19bf33676258eb00fedd7db2c35922f994b6","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmZkM2YxOWJmMzM2NzYyNThlYjAwZmVkZDdkYjJjMzU5MjJmOTk0YjY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"48A6EEE2-C68D-4F30-A457-CE4E09D3D1D5","tree":{"sha":"4bf1e55afc0da98f59e0d59d8b6fc5bdc1175236","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4bf1e55afc0da98f59e0d59d8b6fc5bdc1175236"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/fd3f19bf33676258eb00fedd7db2c35922f994b6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fd3f19bf33676258eb00fedd7db2c35922f994b6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/fd3f19bf33676258eb00fedd7db2c35922f994b6","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fd3f19bf33676258eb00fedd7db2c35922f994b6/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"8d7ba29d364a1ec37ce43b0230ab40c195fedd10","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d7ba29d364a1ec37ce43b0230ab40c195fedd10","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8d7ba29d364a1ec37ce43b0230ab40c195fedd10"}]},{"sha":"4d0cdd064055274d9f0f5428a3e92463fddece26","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjRkMGNkZDA2NDA1NTI3NGQ5ZjBmNTQyOGEzZTkyNDYzZmRkZWNlMjY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"F53B01C9-AF9C-4126-9339-4B19E16D23B7","tree":{"sha":"24f0e3a17706e1ec9607861199861ddf7b14e910","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/24f0e3a17706e1ec9607861199861ddf7b14e910"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/4d0cdd064055274d9f0f5428a3e92463fddece26","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4d0cdd064055274d9f0f5428a3e92463fddece26","html_url":"https://github.com/ThiagoCodecov/example-python/commit/4d0cdd064055274d9f0f5428a3e92463fddece26","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4d0cdd064055274d9f0f5428a3e92463fddece26/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"fd3f19bf33676258eb00fedd7db2c35922f994b6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fd3f19bf33676258eb00fedd7db2c35922f994b6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/fd3f19bf33676258eb00fedd7db2c35922f994b6"}]},{"sha":"668733180a7c76b75d82d882e9df677fa187ed9f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjY2ODczMzE4MGE3Yzc2Yjc1ZDgyZDg4MmU5ZGY2NzdmYTE4N2VkOWY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"6B360EF4-06E2-4F48-87E1-013AF3A4F8CD","tree":{"sha":"54016f4c8af72037bc4f360e5381ff919f3b4177","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/54016f4c8af72037bc4f360e5381ff919f3b4177"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/668733180a7c76b75d82d882e9df677fa187ed9f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/668733180a7c76b75d82d882e9df677fa187ed9f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/668733180a7c76b75d82d882e9df677fa187ed9f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/668733180a7c76b75d82d882e9df677fa187ed9f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"4d0cdd064055274d9f0f5428a3e92463fddece26","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4d0cdd064055274d9f0f5428a3e92463fddece26","html_url":"https://github.com/ThiagoCodecov/example-python/commit/4d0cdd064055274d9f0f5428a3e92463fddece26"}]},{"sha":"80b360a8b70634b52c228b5086da9aa152e7d7f4","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjgwYjM2MGE4YjcwNjM0YjUyYzIyOGI1MDg2ZGE5YWExNTJlN2Q3ZjQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"5508F8C6-4452-45DD-9C43-CC1AD3CA2B1C","tree":{"sha":"1d77ddcca8f31079334df2d9f045d7886965d564","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/1d77ddcca8f31079334df2d9f045d7886965d564"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/80b360a8b70634b52c228b5086da9aa152e7d7f4","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/80b360a8b70634b52c228b5086da9aa152e7d7f4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/80b360a8b70634b52c228b5086da9aa152e7d7f4","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/80b360a8b70634b52c228b5086da9aa152e7d7f4/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"668733180a7c76b75d82d882e9df677fa187ed9f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/668733180a7c76b75d82d882e9df677fa187ed9f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/668733180a7c76b75d82d882e9df677fa187ed9f"}]},{"sha":"136bb4a16c1c0d176eafa1cb5b6e069d12c41b2b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjEzNmJiNGExNmMxYzBkMTc2ZWFmYTFjYjViNmUwNjlkMTJjNDFiMmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"6F18FDC8-8595-4570-A92F-732842EBF2C0","tree":{"sha":"d32639354003fc8b996d46c119fd03232d4441d9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/d32639354003fc8b996d46c119fd03232d4441d9"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/136bb4a16c1c0d176eafa1cb5b6e069d12c41b2b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/136bb4a16c1c0d176eafa1cb5b6e069d12c41b2b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/136bb4a16c1c0d176eafa1cb5b6e069d12c41b2b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/136bb4a16c1c0d176eafa1cb5b6e069d12c41b2b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"80b360a8b70634b52c228b5086da9aa152e7d7f4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/80b360a8b70634b52c228b5086da9aa152e7d7f4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/80b360a8b70634b52c228b5086da9aa152e7d7f4"}]},{"sha":"016fc5a4543905e4b435c6a885b3931a48439e6d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjAxNmZjNWE0NTQzOTA1ZTRiNDM1YzZhODg1YjM5MzFhNDg0MzllNmQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"5A7F8E75-46E4-4397-B281-19FDE96501AD","tree":{"sha":"791cdf9911c0279e9c1d5d1789e05dfe403dbfec","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/791cdf9911c0279e9c1d5d1789e05dfe403dbfec"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/016fc5a4543905e4b435c6a885b3931a48439e6d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/016fc5a4543905e4b435c6a885b3931a48439e6d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/016fc5a4543905e4b435c6a885b3931a48439e6d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/016fc5a4543905e4b435c6a885b3931a48439e6d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"136bb4a16c1c0d176eafa1cb5b6e069d12c41b2b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/136bb4a16c1c0d176eafa1cb5b6e069d12c41b2b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/136bb4a16c1c0d176eafa1cb5b6e069d12c41b2b"}]},{"sha":"b1e665b9b77b834f0ba90eb76ab77bf5c8a4095a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmIxZTY2NWI5Yjc3YjgzNGYwYmE5MGViNzZhYjc3YmY1YzhhNDA5NWE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"DB49E132-1772-4E5A-906A-64F436DF4C96","tree":{"sha":"04764a194dc36e8b11fc540c5bf8381726f581e9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/04764a194dc36e8b11fc540c5bf8381726f581e9"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b1e665b9b77b834f0ba90eb76ab77bf5c8a4095a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b1e665b9b77b834f0ba90eb76ab77bf5c8a4095a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b1e665b9b77b834f0ba90eb76ab77bf5c8a4095a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b1e665b9b77b834f0ba90eb76ab77bf5c8a4095a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"016fc5a4543905e4b435c6a885b3931a48439e6d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/016fc5a4543905e4b435c6a885b3931a48439e6d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/016fc5a4543905e4b435c6a885b3931a48439e6d"}]},{"sha":"96eecdae49231272a5d70b77d8d483139196b234","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojk2ZWVjZGFlNDkyMzEyNzJhNWQ3MGI3N2Q4ZDQ4MzEzOTE5NmIyMzQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"AC4AAD6F-D72B-4F2B-82E6-36DE917774F6","tree":{"sha":"8827af9854d802d2deada9d44a61df60480b1df0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/8827af9854d802d2deada9d44a61df60480b1df0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/96eecdae49231272a5d70b77d8d483139196b234","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/96eecdae49231272a5d70b77d8d483139196b234","html_url":"https://github.com/ThiagoCodecov/example-python/commit/96eecdae49231272a5d70b77d8d483139196b234","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/96eecdae49231272a5d70b77d8d483139196b234/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"b1e665b9b77b834f0ba90eb76ab77bf5c8a4095a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b1e665b9b77b834f0ba90eb76ab77bf5c8a4095a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b1e665b9b77b834f0ba90eb76ab77bf5c8a4095a"}]},{"sha":"6771df80ed445674cd82bb8964d4a76dd9cb079f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjY3NzFkZjgwZWQ0NDU2NzRjZDgyYmI4OTY0ZDRhNzZkZDljYjA3OWY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"9AB989D9-47B7-4632-92B6-DDF67B33AE84","tree":{"sha":"525c8c7afc9739250051a0cbf1c02a71875ed4f5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/525c8c7afc9739250051a0cbf1c02a71875ed4f5"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6771df80ed445674cd82bb8964d4a76dd9cb079f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6771df80ed445674cd82bb8964d4a76dd9cb079f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6771df80ed445674cd82bb8964d4a76dd9cb079f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6771df80ed445674cd82bb8964d4a76dd9cb079f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"96eecdae49231272a5d70b77d8d483139196b234","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/96eecdae49231272a5d70b77d8d483139196b234","html_url":"https://github.com/ThiagoCodecov/example-python/commit/96eecdae49231272a5d70b77d8d483139196b234"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:16:16 GMT + ETag: + - W/"741f6de5ae0f11d0d061e7b7fca6985a892f1faf8f7e48924d8d162e002c68bc" + Last-Modified: + - Fri, 01 Sep 2023 11:48:40 GMT + Link: + - ; + rel="next", ; + rel="last" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF7C:0DA1:123C6C:130E81:64F610E0 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4998' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '2' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls/16/commits?page=2&per_page=100 + response: + content: '[{"sha":"20a046587d7ae216b364ee67130937ae4603b090","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjIwYTA0NjU4N2Q3YWUyMTZiMzY0ZWU2NzEzMDkzN2FlNDYwM2IwOTA=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"D26702A0-F137-4C1D-A91C-B2D866B9325C","tree":{"sha":"0a2bed8926e200d14b0ac8449059ea4c07b023ea","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/0a2bed8926e200d14b0ac8449059ea4c07b023ea"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/20a046587d7ae216b364ee67130937ae4603b090","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/20a046587d7ae216b364ee67130937ae4603b090","html_url":"https://github.com/ThiagoCodecov/example-python/commit/20a046587d7ae216b364ee67130937ae4603b090","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/20a046587d7ae216b364ee67130937ae4603b090/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6771df80ed445674cd82bb8964d4a76dd9cb079f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6771df80ed445674cd82bb8964d4a76dd9cb079f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6771df80ed445674cd82bb8964d4a76dd9cb079f"}]},{"sha":"07886c36de7b5e8b22876c43d42039c435cc4cf4","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA3ODg2YzM2ZGU3YjVlOGIyMjg3NmM0M2Q0MjAzOWM0MzVjYzRjZjQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"5D395B1D-30E4-4E74-AD58-21C97F52069D","tree":{"sha":"b9aeb97475dab077da56fc9b3b7b6ffe06b4742a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b9aeb97475dab077da56fc9b3b7b6ffe06b4742a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/07886c36de7b5e8b22876c43d42039c435cc4cf4","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/07886c36de7b5e8b22876c43d42039c435cc4cf4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/07886c36de7b5e8b22876c43d42039c435cc4cf4","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/07886c36de7b5e8b22876c43d42039c435cc4cf4/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"20a046587d7ae216b364ee67130937ae4603b090","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/20a046587d7ae216b364ee67130937ae4603b090","html_url":"https://github.com/ThiagoCodecov/example-python/commit/20a046587d7ae216b364ee67130937ae4603b090"}]},{"sha":"eceb16180f4774cd4cb50ffc93c3a2057d0fef38","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmVjZWIxNjE4MGY0Nzc0Y2Q0Y2I1MGZmYzkzYzNhMjA1N2QwZmVmMzg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"FF707719-7996-4DFA-B594-46AD1C7A2BEA","tree":{"sha":"22df14c2c9f28600b08c9ee2c03dab1e35b29512","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/22df14c2c9f28600b08c9ee2c03dab1e35b29512"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/eceb16180f4774cd4cb50ffc93c3a2057d0fef38","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/eceb16180f4774cd4cb50ffc93c3a2057d0fef38","html_url":"https://github.com/ThiagoCodecov/example-python/commit/eceb16180f4774cd4cb50ffc93c3a2057d0fef38","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/eceb16180f4774cd4cb50ffc93c3a2057d0fef38/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"07886c36de7b5e8b22876c43d42039c435cc4cf4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/07886c36de7b5e8b22876c43d42039c435cc4cf4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/07886c36de7b5e8b22876c43d42039c435cc4cf4"}]},{"sha":"c44ef474ccd9364b5c1295cfb14dcb149ca65031","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmM0NGVmNDc0Y2NkOTM2NGI1YzEyOTVjZmIxNGRjYjE0OWNhNjUwMzE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"EDD294D5-E51E-494B-89C6-C30892A858CD","tree":{"sha":"1c08ffe505962f46dabd61a42c2b094ccb8fdb07","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/1c08ffe505962f46dabd61a42c2b094ccb8fdb07"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c44ef474ccd9364b5c1295cfb14dcb149ca65031","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c44ef474ccd9364b5c1295cfb14dcb149ca65031","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c44ef474ccd9364b5c1295cfb14dcb149ca65031","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c44ef474ccd9364b5c1295cfb14dcb149ca65031/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"eceb16180f4774cd4cb50ffc93c3a2057d0fef38","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/eceb16180f4774cd4cb50ffc93c3a2057d0fef38","html_url":"https://github.com/ThiagoCodecov/example-python/commit/eceb16180f4774cd4cb50ffc93c3a2057d0fef38"}]},{"sha":"fa7b1547534c6377b720b839fc7a56af9251ddc2","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmZhN2IxNTQ3NTM0YzYzNzdiNzIwYjgzOWZjN2E1NmFmOTI1MWRkYzI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"3C377550-9091-4546-BA58-9F3733350B3E","tree":{"sha":"d2032f28c0c396db450c230052043754e62c7435","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/d2032f28c0c396db450c230052043754e62c7435"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/fa7b1547534c6377b720b839fc7a56af9251ddc2","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fa7b1547534c6377b720b839fc7a56af9251ddc2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/fa7b1547534c6377b720b839fc7a56af9251ddc2","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fa7b1547534c6377b720b839fc7a56af9251ddc2/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"c44ef474ccd9364b5c1295cfb14dcb149ca65031","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c44ef474ccd9364b5c1295cfb14dcb149ca65031","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c44ef474ccd9364b5c1295cfb14dcb149ca65031"}]},{"sha":"d748c76d0deac775aac3ba83088519cb64e79e5f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmQ3NDhjNzZkMGRlYWM3NzVhYWMzYmE4MzA4ODUxOWNiNjRlNzllNWY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"C2A99525-C89C-4CC7-9B05-DA1D0EEA6859","tree":{"sha":"3228865c03a9e136fbf800b320a57a5beedf4892","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3228865c03a9e136fbf800b320a57a5beedf4892"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d748c76d0deac775aac3ba83088519cb64e79e5f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d748c76d0deac775aac3ba83088519cb64e79e5f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d748c76d0deac775aac3ba83088519cb64e79e5f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d748c76d0deac775aac3ba83088519cb64e79e5f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"fa7b1547534c6377b720b839fc7a56af9251ddc2","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/fa7b1547534c6377b720b839fc7a56af9251ddc2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/fa7b1547534c6377b720b839fc7a56af9251ddc2"}]},{"sha":"b27d4784de9cf15db616d8f1cb25bdd9631ae468","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmIyN2Q0Nzg0ZGU5Y2YxNWRiNjE2ZDhmMWNiMjViZGQ5NjMxYWU0Njg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"17F62215-0BAA-4998-99F8-28A388A0F210","tree":{"sha":"7c1699c3ad164494feb27ab4e7073cbe6af3ea6c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7c1699c3ad164494feb27ab4e7073cbe6af3ea6c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b27d4784de9cf15db616d8f1cb25bdd9631ae468","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b27d4784de9cf15db616d8f1cb25bdd9631ae468","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b27d4784de9cf15db616d8f1cb25bdd9631ae468","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b27d4784de9cf15db616d8f1cb25bdd9631ae468/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"d748c76d0deac775aac3ba83088519cb64e79e5f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d748c76d0deac775aac3ba83088519cb64e79e5f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d748c76d0deac775aac3ba83088519cb64e79e5f"}]},{"sha":"b389c058621f4d467bd1c410a9453c6ad0eb4d4d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmIzODljMDU4NjIxZjRkNDY3YmQxYzQxMGE5NDUzYzZhZDBlYjRkNGQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"B0A67885-588E-4D8E-A47E-25D125BA1581","tree":{"sha":"179aa0e7abb9e70690ec45b4912425a990b09ef6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/179aa0e7abb9e70690ec45b4912425a990b09ef6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b389c058621f4d467bd1c410a9453c6ad0eb4d4d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b389c058621f4d467bd1c410a9453c6ad0eb4d4d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b389c058621f4d467bd1c410a9453c6ad0eb4d4d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b389c058621f4d467bd1c410a9453c6ad0eb4d4d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"b27d4784de9cf15db616d8f1cb25bdd9631ae468","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b27d4784de9cf15db616d8f1cb25bdd9631ae468","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b27d4784de9cf15db616d8f1cb25bdd9631ae468"}]},{"sha":"6f14f0d678b1e77e2b7232396d99c165bf1ace30","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjZmMTRmMGQ2NzhiMWU3N2UyYjcyMzIzOTZkOTljMTY1YmYxYWNlMzA=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"8D62F76A-9C8A-4EB3-81E1-0BAB2211AD8B","tree":{"sha":"28efc49b0f521030f0f40a00ce58c1c4c32dde13","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/28efc49b0f521030f0f40a00ce58c1c4c32dde13"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6f14f0d678b1e77e2b7232396d99c165bf1ace30","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6f14f0d678b1e77e2b7232396d99c165bf1ace30","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6f14f0d678b1e77e2b7232396d99c165bf1ace30","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6f14f0d678b1e77e2b7232396d99c165bf1ace30/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"b389c058621f4d467bd1c410a9453c6ad0eb4d4d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b389c058621f4d467bd1c410a9453c6ad0eb4d4d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b389c058621f4d467bd1c410a9453c6ad0eb4d4d"}]},{"sha":"14801950abd8bac1b1afe959feffca73bdf19695","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjE0ODAxOTUwYWJkOGJhYzFiMWFmZTk1OWZlZmZjYTczYmRmMTk2OTU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:58Z"},"message":"AC319456-E909-4790-B365-9026182DCBD3","tree":{"sha":"eb1f49ed47f4d8b490e062c13537d7e8df51b12a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/eb1f49ed47f4d8b490e062c13537d7e8df51b12a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/14801950abd8bac1b1afe959feffca73bdf19695","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/14801950abd8bac1b1afe959feffca73bdf19695","html_url":"https://github.com/ThiagoCodecov/example-python/commit/14801950abd8bac1b1afe959feffca73bdf19695","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/14801950abd8bac1b1afe959feffca73bdf19695/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6f14f0d678b1e77e2b7232396d99c165bf1ace30","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6f14f0d678b1e77e2b7232396d99c165bf1ace30","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6f14f0d678b1e77e2b7232396d99c165bf1ace30"}]},{"sha":"021727c33d7288c352d80691bf7e3fd3a7c11943","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjAyMTcyN2MzM2Q3Mjg4YzM1MmQ4MDY5MWJmN2UzZmQzYTdjMTE5NDM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"730B560D-9BB7-4317-B957-41217176CE7C","tree":{"sha":"bcaa07bea76f494c987c341e7b72f06fc683ecf0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/bcaa07bea76f494c987c341e7b72f06fc683ecf0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/021727c33d7288c352d80691bf7e3fd3a7c11943","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/021727c33d7288c352d80691bf7e3fd3a7c11943","html_url":"https://github.com/ThiagoCodecov/example-python/commit/021727c33d7288c352d80691bf7e3fd3a7c11943","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/021727c33d7288c352d80691bf7e3fd3a7c11943/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"14801950abd8bac1b1afe959feffca73bdf19695","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/14801950abd8bac1b1afe959feffca73bdf19695","html_url":"https://github.com/ThiagoCodecov/example-python/commit/14801950abd8bac1b1afe959feffca73bdf19695"}]},{"sha":"93669e4a1b0adbd2060887699151ca450c4279c1","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjkzNjY5ZTRhMWIwYWRiZDIwNjA4ODc2OTkxNTFjYTQ1MGM0Mjc5YzE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"CE0AF793-4715-4780-80D8-E7C4882FDAE9","tree":{"sha":"c28ca5cd235a4849454c8fec6f1c885b91c7003a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c28ca5cd235a4849454c8fec6f1c885b91c7003a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/93669e4a1b0adbd2060887699151ca450c4279c1","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/93669e4a1b0adbd2060887699151ca450c4279c1","html_url":"https://github.com/ThiagoCodecov/example-python/commit/93669e4a1b0adbd2060887699151ca450c4279c1","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/93669e4a1b0adbd2060887699151ca450c4279c1/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"021727c33d7288c352d80691bf7e3fd3a7c11943","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/021727c33d7288c352d80691bf7e3fd3a7c11943","html_url":"https://github.com/ThiagoCodecov/example-python/commit/021727c33d7288c352d80691bf7e3fd3a7c11943"}]},{"sha":"59177f8de42bf080d2b5692b0b6b628638f1416c","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjU5MTc3ZjhkZTQyYmYwODBkMmI1NjkyYjBiNmI2Mjg2MzhmMTQxNmM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"6934523F-8CC7-469C-B32F-F2009B9AEA39","tree":{"sha":"7d97610e511c7247d0238efa5dcdeacb8ef8339c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7d97610e511c7247d0238efa5dcdeacb8ef8339c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/59177f8de42bf080d2b5692b0b6b628638f1416c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/59177f8de42bf080d2b5692b0b6b628638f1416c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/59177f8de42bf080d2b5692b0b6b628638f1416c","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/59177f8de42bf080d2b5692b0b6b628638f1416c/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"93669e4a1b0adbd2060887699151ca450c4279c1","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/93669e4a1b0adbd2060887699151ca450c4279c1","html_url":"https://github.com/ThiagoCodecov/example-python/commit/93669e4a1b0adbd2060887699151ca450c4279c1"}]},{"sha":"cd061d4460c55c39eb8df7b0f54657c831e01ee1","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmNkMDYxZDQ0NjBjNTVjMzllYjhkZjdiMGY1NDY1N2M4MzFlMDFlZTE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"24676F2E-AF3E-470C-98A0-A7A30D9E2A04","tree":{"sha":"ae9e5df5b5d815d24119a0c21c60154861a512e6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ae9e5df5b5d815d24119a0c21c60154861a512e6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/cd061d4460c55c39eb8df7b0f54657c831e01ee1","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cd061d4460c55c39eb8df7b0f54657c831e01ee1","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cd061d4460c55c39eb8df7b0f54657c831e01ee1","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cd061d4460c55c39eb8df7b0f54657c831e01ee1/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"59177f8de42bf080d2b5692b0b6b628638f1416c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/59177f8de42bf080d2b5692b0b6b628638f1416c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/59177f8de42bf080d2b5692b0b6b628638f1416c"}]},{"sha":"1b83e035cf2e4e9ad9062bc87846c7febc99d806","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjFiODNlMDM1Y2YyZTRlOWFkOTA2MmJjODc4NDZjN2ZlYmM5OWQ4MDY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"3A5724A3-A5F8-46D3-A5E6-8D7605C95F30","tree":{"sha":"2ab2b55065669ad64afd7d0339852e7d73619dd1","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2ab2b55065669ad64afd7d0339852e7d73619dd1"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/1b83e035cf2e4e9ad9062bc87846c7febc99d806","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1b83e035cf2e4e9ad9062bc87846c7febc99d806","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1b83e035cf2e4e9ad9062bc87846c7febc99d806","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1b83e035cf2e4e9ad9062bc87846c7febc99d806/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"cd061d4460c55c39eb8df7b0f54657c831e01ee1","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cd061d4460c55c39eb8df7b0f54657c831e01ee1","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cd061d4460c55c39eb8df7b0f54657c831e01ee1"}]},{"sha":"158a680bfc02a28516ec732a0d8521eafb3c69ed","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjE1OGE2ODBiZmMwMmEyODUxNmVjNzMyYTBkODUyMWVhZmIzYzY5ZWQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"A52EFCB3-376A-4785-A642-024964508826","tree":{"sha":"4f2aa604716a3ce0eba227581fcade2317795a47","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4f2aa604716a3ce0eba227581fcade2317795a47"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/158a680bfc02a28516ec732a0d8521eafb3c69ed","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/158a680bfc02a28516ec732a0d8521eafb3c69ed","html_url":"https://github.com/ThiagoCodecov/example-python/commit/158a680bfc02a28516ec732a0d8521eafb3c69ed","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/158a680bfc02a28516ec732a0d8521eafb3c69ed/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"1b83e035cf2e4e9ad9062bc87846c7febc99d806","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1b83e035cf2e4e9ad9062bc87846c7febc99d806","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1b83e035cf2e4e9ad9062bc87846c7febc99d806"}]},{"sha":"cb86221894914df7013359a8ffe4385d6328c59f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmNiODYyMjE4OTQ5MTRkZjcwMTMzNTlhOGZmZTQzODVkNjMyOGM1OWY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"BD1B64EB-642D-48B4-9AF1-F00BBF6B78F1","tree":{"sha":"656499872cf33236014a0727ebad99bc63095f87","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/656499872cf33236014a0727ebad99bc63095f87"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/cb86221894914df7013359a8ffe4385d6328c59f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cb86221894914df7013359a8ffe4385d6328c59f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cb86221894914df7013359a8ffe4385d6328c59f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cb86221894914df7013359a8ffe4385d6328c59f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"158a680bfc02a28516ec732a0d8521eafb3c69ed","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/158a680bfc02a28516ec732a0d8521eafb3c69ed","html_url":"https://github.com/ThiagoCodecov/example-python/commit/158a680bfc02a28516ec732a0d8521eafb3c69ed"}]},{"sha":"87571d23b35dbede41ed62c7e1239e8e599176f2","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg3NTcxZDIzYjM1ZGJlZGU0MWVkNjJjN2UxMjM5ZThlNTk5MTc2ZjI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"16240060-9D4B-4E72-A86D-DB50C17664CD","tree":{"sha":"b8f75f92ce9815a0e5cfe954b2e838541620ccb0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b8f75f92ce9815a0e5cfe954b2e838541620ccb0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/87571d23b35dbede41ed62c7e1239e8e599176f2","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/87571d23b35dbede41ed62c7e1239e8e599176f2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/87571d23b35dbede41ed62c7e1239e8e599176f2","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/87571d23b35dbede41ed62c7e1239e8e599176f2/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"cb86221894914df7013359a8ffe4385d6328c59f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/cb86221894914df7013359a8ffe4385d6328c59f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/cb86221894914df7013359a8ffe4385d6328c59f"}]},{"sha":"063f07b9dd97051a677afbf30ccef526b88cbf95","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA2M2YwN2I5ZGQ5NzA1MWE2NzdhZmJmMzBjY2VmNTI2Yjg4Y2JmOTU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"04E51DEF-602B-4FA4-916F-98A1472F99A1","tree":{"sha":"cc27de6df671965a8ee3d41ff4b06e15b575a6f3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/cc27de6df671965a8ee3d41ff4b06e15b575a6f3"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/063f07b9dd97051a677afbf30ccef526b88cbf95","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/063f07b9dd97051a677afbf30ccef526b88cbf95","html_url":"https://github.com/ThiagoCodecov/example-python/commit/063f07b9dd97051a677afbf30ccef526b88cbf95","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/063f07b9dd97051a677afbf30ccef526b88cbf95/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"87571d23b35dbede41ed62c7e1239e8e599176f2","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/87571d23b35dbede41ed62c7e1239e8e599176f2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/87571d23b35dbede41ed62c7e1239e8e599176f2"}]},{"sha":"a02952da978b95ab41363fda07846c9800f4c3a8","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmEwMjk1MmRhOTc4Yjk1YWI0MTM2M2ZkYTA3ODQ2Yzk4MDBmNGMzYTg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"CBC391DD-7E09-40C5-B6ED-29E328B5B08D","tree":{"sha":"136dada7ef46ef9fb331d82ff9f216a2139ff707","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/136dada7ef46ef9fb331d82ff9f216a2139ff707"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/a02952da978b95ab41363fda07846c9800f4c3a8","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a02952da978b95ab41363fda07846c9800f4c3a8","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a02952da978b95ab41363fda07846c9800f4c3a8","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a02952da978b95ab41363fda07846c9800f4c3a8/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"063f07b9dd97051a677afbf30ccef526b88cbf95","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/063f07b9dd97051a677afbf30ccef526b88cbf95","html_url":"https://github.com/ThiagoCodecov/example-python/commit/063f07b9dd97051a677afbf30ccef526b88cbf95"}]},{"sha":"39d4575b9804d45956579582a793b2d2aedf5a5c","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjM5ZDQ1NzViOTgwNGQ0NTk1NjU3OTU4MmE3OTNiMmQyYWVkZjVhNWM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"E51387F2-377E-4EF2-8DF1-2F95633ADEDB","tree":{"sha":"b1c3089c850ea8aa301ce1f8ddbe28eb97856671","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b1c3089c850ea8aa301ce1f8ddbe28eb97856671"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/39d4575b9804d45956579582a793b2d2aedf5a5c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/39d4575b9804d45956579582a793b2d2aedf5a5c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/39d4575b9804d45956579582a793b2d2aedf5a5c","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/39d4575b9804d45956579582a793b2d2aedf5a5c/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"a02952da978b95ab41363fda07846c9800f4c3a8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a02952da978b95ab41363fda07846c9800f4c3a8","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a02952da978b95ab41363fda07846c9800f4c3a8"}]},{"sha":"3faae75078754deaf91a55c6c225be06f23e5ff6","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjNmYWFlNzUwNzg3NTRkZWFmOTFhNTVjNmMyMjViZTA2ZjIzZTVmZjY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"5DC2B814-E792-4851-8D98-BD72247F4A1C","tree":{"sha":"6e05480053c394945994c15061df64823c6db9bc","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6e05480053c394945994c15061df64823c6db9bc"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3faae75078754deaf91a55c6c225be06f23e5ff6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3faae75078754deaf91a55c6c225be06f23e5ff6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3faae75078754deaf91a55c6c225be06f23e5ff6","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3faae75078754deaf91a55c6c225be06f23e5ff6/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"39d4575b9804d45956579582a793b2d2aedf5a5c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/39d4575b9804d45956579582a793b2d2aedf5a5c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/39d4575b9804d45956579582a793b2d2aedf5a5c"}]},{"sha":"8a484482ef9a80a3886565a578ef36c374813429","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjhhNDg0NDgyZWY5YTgwYTM4ODY1NjVhNTc4ZWYzNmMzNzQ4MTM0Mjk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"61060E92-E7B7-4ABD-AE5D-A6BA2D5B59A7","tree":{"sha":"74f34ec0772654240f01158b6e3ebe07113138f6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/74f34ec0772654240f01158b6e3ebe07113138f6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8a484482ef9a80a3886565a578ef36c374813429","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8a484482ef9a80a3886565a578ef36c374813429","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8a484482ef9a80a3886565a578ef36c374813429","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8a484482ef9a80a3886565a578ef36c374813429/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"3faae75078754deaf91a55c6c225be06f23e5ff6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3faae75078754deaf91a55c6c225be06f23e5ff6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3faae75078754deaf91a55c6c225be06f23e5ff6"}]},{"sha":"727997adcc8a38828928fb315432a05e0460fe8f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjcyNzk5N2FkY2M4YTM4ODI4OTI4ZmIzMTU0MzJhMDVlMDQ2MGZlOGY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"B456E55B-5D8D-47F6-8960-0D54BD80AABE","tree":{"sha":"b49d713dfc3ef865cf51ac2efc30229840d5f105","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b49d713dfc3ef865cf51ac2efc30229840d5f105"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/727997adcc8a38828928fb315432a05e0460fe8f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/727997adcc8a38828928fb315432a05e0460fe8f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/727997adcc8a38828928fb315432a05e0460fe8f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/727997adcc8a38828928fb315432a05e0460fe8f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"8a484482ef9a80a3886565a578ef36c374813429","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8a484482ef9a80a3886565a578ef36c374813429","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8a484482ef9a80a3886565a578ef36c374813429"}]},{"sha":"f81f47661e28c303eb2da56685b711561d7eea9a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmY4MWY0NzY2MWUyOGMzMDNlYjJkYTU2Njg1YjcxMTU2MWQ3ZWVhOWE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"9EFA1541-F347-4FA4-B549-E9078B7DDBBF","tree":{"sha":"e267fb8a0c6033997f020b6156e018cf68656da9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e267fb8a0c6033997f020b6156e018cf68656da9"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f81f47661e28c303eb2da56685b711561d7eea9a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f81f47661e28c303eb2da56685b711561d7eea9a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f81f47661e28c303eb2da56685b711561d7eea9a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f81f47661e28c303eb2da56685b711561d7eea9a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"727997adcc8a38828928fb315432a05e0460fe8f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/727997adcc8a38828928fb315432a05e0460fe8f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/727997adcc8a38828928fb315432a05e0460fe8f"}]},{"sha":"f5104e791dcbedc1a023c512a90d58bdfb481f45","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmY1MTA0ZTc5MWRjYmVkYzFhMDIzYzUxMmE5MGQ1OGJkZmI0ODFmNDU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"ADBFD9C0-CC48-4683-9E97-70B595A2D498","tree":{"sha":"4cc17c10ae9e4abc03a3c35832633b1c208a2441","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4cc17c10ae9e4abc03a3c35832633b1c208a2441"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f5104e791dcbedc1a023c512a90d58bdfb481f45","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f5104e791dcbedc1a023c512a90d58bdfb481f45","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f5104e791dcbedc1a023c512a90d58bdfb481f45","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f5104e791dcbedc1a023c512a90d58bdfb481f45/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f81f47661e28c303eb2da56685b711561d7eea9a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f81f47661e28c303eb2da56685b711561d7eea9a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f81f47661e28c303eb2da56685b711561d7eea9a"}]},{"sha":"bdd043fe9c546b2bce0e8bbd610c6c89de66867c","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmJkZDA0M2ZlOWM1NDZiMmJjZTBlOGJiZDYxMGM2Yzg5ZGU2Njg2N2M=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"6062F6F3-1813-494B-B4BA-4E26875A3C7C","tree":{"sha":"e508020b96dfd97e3d304e135284a4442bac9643","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e508020b96dfd97e3d304e135284a4442bac9643"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/bdd043fe9c546b2bce0e8bbd610c6c89de66867c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bdd043fe9c546b2bce0e8bbd610c6c89de66867c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bdd043fe9c546b2bce0e8bbd610c6c89de66867c","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bdd043fe9c546b2bce0e8bbd610c6c89de66867c/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f5104e791dcbedc1a023c512a90d58bdfb481f45","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f5104e791dcbedc1a023c512a90d58bdfb481f45","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f5104e791dcbedc1a023c512a90d58bdfb481f45"}]},{"sha":"97d3254823a9f3c6813269c5ab711d63a1e1b4f4","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojk3ZDMyNTQ4MjNhOWYzYzY4MTMyNjljNWFiNzExZDYzYTFlMWI0ZjQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"DC6AF819-BEFD-4B58-A289-49299BF42530","tree":{"sha":"9f98b149e1cc237d4c654cd284180223483ed115","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9f98b149e1cc237d4c654cd284180223483ed115"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/97d3254823a9f3c6813269c5ab711d63a1e1b4f4","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/97d3254823a9f3c6813269c5ab711d63a1e1b4f4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/97d3254823a9f3c6813269c5ab711d63a1e1b4f4","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/97d3254823a9f3c6813269c5ab711d63a1e1b4f4/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"bdd043fe9c546b2bce0e8bbd610c6c89de66867c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bdd043fe9c546b2bce0e8bbd610c6c89de66867c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bdd043fe9c546b2bce0e8bbd610c6c89de66867c"}]},{"sha":"e8a28ffaf79b926bbb3cc674c5187c79505d165c","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmU4YTI4ZmZhZjc5YjkyNmJiYjNjYzY3NGM1MTg3Yzc5NTA1ZDE2NWM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"60D13934-BB22-4FF0-BF85-1D2CEDFC4C56","tree":{"sha":"9685bca5e919311764126ccc7b4e1a482e7faf67","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9685bca5e919311764126ccc7b4e1a482e7faf67"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e8a28ffaf79b926bbb3cc674c5187c79505d165c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e8a28ffaf79b926bbb3cc674c5187c79505d165c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e8a28ffaf79b926bbb3cc674c5187c79505d165c","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e8a28ffaf79b926bbb3cc674c5187c79505d165c/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"97d3254823a9f3c6813269c5ab711d63a1e1b4f4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/97d3254823a9f3c6813269c5ab711d63a1e1b4f4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/97d3254823a9f3c6813269c5ab711d63a1e1b4f4"}]},{"sha":"b1b83c31442854547e212f9624e1acc6e2f3eff6","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmIxYjgzYzMxNDQyODU0NTQ3ZTIxMmY5NjI0ZTFhY2M2ZTJmM2VmZjY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"50A3CF26-77C2-433A-8379-412685C80036","tree":{"sha":"2b0d4bb8c73a5fb1bbd0a33603a10923d9b54bdc","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2b0d4bb8c73a5fb1bbd0a33603a10923d9b54bdc"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b1b83c31442854547e212f9624e1acc6e2f3eff6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b1b83c31442854547e212f9624e1acc6e2f3eff6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b1b83c31442854547e212f9624e1acc6e2f3eff6","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b1b83c31442854547e212f9624e1acc6e2f3eff6/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e8a28ffaf79b926bbb3cc674c5187c79505d165c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e8a28ffaf79b926bbb3cc674c5187c79505d165c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e8a28ffaf79b926bbb3cc674c5187c79505d165c"}]},{"sha":"09c91c4314e5869a5680fbb67cac0e0d9cf6911d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA5YzkxYzQzMTRlNTg2OWE1NjgwZmJiNjdjYWMwZTBkOWNmNjkxMWQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"40D27A9A-B011-4E07-8870-50CE0E9BAFC6","tree":{"sha":"156b48877e3607b7af17bf918131e8670abdedf9","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/156b48877e3607b7af17bf918131e8670abdedf9"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/09c91c4314e5869a5680fbb67cac0e0d9cf6911d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/09c91c4314e5869a5680fbb67cac0e0d9cf6911d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/09c91c4314e5869a5680fbb67cac0e0d9cf6911d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/09c91c4314e5869a5680fbb67cac0e0d9cf6911d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"b1b83c31442854547e212f9624e1acc6e2f3eff6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b1b83c31442854547e212f9624e1acc6e2f3eff6","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b1b83c31442854547e212f9624e1acc6e2f3eff6"}]},{"sha":"85963b9174accc9d1da0666187e14957a85c4c69","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg1OTYzYjkxNzRhY2NjOWQxZGEwNjY2MTg3ZTE0OTU3YTg1YzRjNjk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"B654D0B6-0588-463F-A881-1D889C545E4C","tree":{"sha":"961ba9b64b781b373d579fb40f3a7278a26307d1","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/961ba9b64b781b373d579fb40f3a7278a26307d1"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/85963b9174accc9d1da0666187e14957a85c4c69","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/85963b9174accc9d1da0666187e14957a85c4c69","html_url":"https://github.com/ThiagoCodecov/example-python/commit/85963b9174accc9d1da0666187e14957a85c4c69","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/85963b9174accc9d1da0666187e14957a85c4c69/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"09c91c4314e5869a5680fbb67cac0e0d9cf6911d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/09c91c4314e5869a5680fbb67cac0e0d9cf6911d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/09c91c4314e5869a5680fbb67cac0e0d9cf6911d"}]},{"sha":"4150ff6f39ff97220a2919a4a972cd3243d1ca18","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjQxNTBmZjZmMzlmZjk3MjIwYTI5MTlhNGE5NzJjZDMyNDNkMWNhMTg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"0C4CA878-DA3B-4C72-980B-43BA64397F4B","tree":{"sha":"206f589ac76d9295eed538b872c05d8d9cf4851d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/206f589ac76d9295eed538b872c05d8d9cf4851d"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/4150ff6f39ff97220a2919a4a972cd3243d1ca18","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4150ff6f39ff97220a2919a4a972cd3243d1ca18","html_url":"https://github.com/ThiagoCodecov/example-python/commit/4150ff6f39ff97220a2919a4a972cd3243d1ca18","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4150ff6f39ff97220a2919a4a972cd3243d1ca18/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"85963b9174accc9d1da0666187e14957a85c4c69","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/85963b9174accc9d1da0666187e14957a85c4c69","html_url":"https://github.com/ThiagoCodecov/example-python/commit/85963b9174accc9d1da0666187e14957a85c4c69"}]},{"sha":"1ebcef9ada87072e5d553a3ff08361b8ebd90d31","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjFlYmNlZjlhZGE4NzA3MmU1ZDU1M2EzZmYwODM2MWI4ZWJkOTBkMzE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"4A5A3E71-5E05-4DA0-B9E5-5C8DA53CFFCC","tree":{"sha":"c995615d0a326cd8094b87b184e78624661fe579","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c995615d0a326cd8094b87b184e78624661fe579"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/1ebcef9ada87072e5d553a3ff08361b8ebd90d31","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1ebcef9ada87072e5d553a3ff08361b8ebd90d31","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1ebcef9ada87072e5d553a3ff08361b8ebd90d31","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1ebcef9ada87072e5d553a3ff08361b8ebd90d31/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"4150ff6f39ff97220a2919a4a972cd3243d1ca18","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4150ff6f39ff97220a2919a4a972cd3243d1ca18","html_url":"https://github.com/ThiagoCodecov/example-python/commit/4150ff6f39ff97220a2919a4a972cd3243d1ca18"}]},{"sha":"8766a530fd4cb7b32550bb299b5d3510542f75bb","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojg3NjZhNTMwZmQ0Y2I3YjMyNTUwYmIyOTliNWQzNTEwNTQyZjc1YmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"A60630FA-504F-4B92-8B38-F035B8EB5DAB","tree":{"sha":"d60f697b021b42b321d5896fe5984d0f053dd6f2","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/d60f697b021b42b321d5896fe5984d0f053dd6f2"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8766a530fd4cb7b32550bb299b5d3510542f75bb","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8766a530fd4cb7b32550bb299b5d3510542f75bb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8766a530fd4cb7b32550bb299b5d3510542f75bb","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8766a530fd4cb7b32550bb299b5d3510542f75bb/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"1ebcef9ada87072e5d553a3ff08361b8ebd90d31","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1ebcef9ada87072e5d553a3ff08361b8ebd90d31","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1ebcef9ada87072e5d553a3ff08361b8ebd90d31"}]},{"sha":"a3ddc54762360c65e5974614fb10eae70da9f1ac","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmEzZGRjNTQ3NjIzNjBjNjVlNTk3NDYxNGZiMTBlYWU3MGRhOWYxYWM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"028AE730-C15D-41C2-B292-2822EC729531","tree":{"sha":"ffb431a06c9ea7d8a5b8072bdd88f03b1c2488e6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ffb431a06c9ea7d8a5b8072bdd88f03b1c2488e6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/a3ddc54762360c65e5974614fb10eae70da9f1ac","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a3ddc54762360c65e5974614fb10eae70da9f1ac","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a3ddc54762360c65e5974614fb10eae70da9f1ac","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a3ddc54762360c65e5974614fb10eae70da9f1ac/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"8766a530fd4cb7b32550bb299b5d3510542f75bb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8766a530fd4cb7b32550bb299b5d3510542f75bb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8766a530fd4cb7b32550bb299b5d3510542f75bb"}]},{"sha":"35e960e3765de362d747f6294ebca5a95f98f27c","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjM1ZTk2MGUzNzY1ZGUzNjJkNzQ3ZjYyOTRlYmNhNWE5NWY5OGYyN2M=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"66911A75-3871-4333-846D-DE03163241AD","tree":{"sha":"954c5296be5138090b05ea39bfcce54d17dd4ce0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/954c5296be5138090b05ea39bfcce54d17dd4ce0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/35e960e3765de362d747f6294ebca5a95f98f27c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/35e960e3765de362d747f6294ebca5a95f98f27c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/35e960e3765de362d747f6294ebca5a95f98f27c","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/35e960e3765de362d747f6294ebca5a95f98f27c/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"a3ddc54762360c65e5974614fb10eae70da9f1ac","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a3ddc54762360c65e5974614fb10eae70da9f1ac","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a3ddc54762360c65e5974614fb10eae70da9f1ac"}]},{"sha":"18b71e92481f44d2553e9ea1852ac7086ceac173","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjE4YjcxZTkyNDgxZjQ0ZDI1NTNlOWVhMTg1MmFjNzA4NmNlYWMxNzM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"D23AD010-BF62-46C1-8C9E-02C0074220AB","tree":{"sha":"201e6d5a7f181dc326a7731ff18606763f82f563","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/201e6d5a7f181dc326a7731ff18606763f82f563"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/18b71e92481f44d2553e9ea1852ac7086ceac173","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/18b71e92481f44d2553e9ea1852ac7086ceac173","html_url":"https://github.com/ThiagoCodecov/example-python/commit/18b71e92481f44d2553e9ea1852ac7086ceac173","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/18b71e92481f44d2553e9ea1852ac7086ceac173/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"35e960e3765de362d747f6294ebca5a95f98f27c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/35e960e3765de362d747f6294ebca5a95f98f27c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/35e960e3765de362d747f6294ebca5a95f98f27c"}]},{"sha":"d064e04618f10421b03822b12a939c891bbd0151","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmQwNjRlMDQ2MThmMTA0MjFiMDM4MjJiMTJhOTM5Yzg5MWJiZDAxNTE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"3E0C54E6-EFD8-40A1-94C7-B6D0608BF526","tree":{"sha":"d303e254a7c6afb3f192e423cd7993695ec68e7b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/d303e254a7c6afb3f192e423cd7993695ec68e7b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d064e04618f10421b03822b12a939c891bbd0151","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d064e04618f10421b03822b12a939c891bbd0151","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d064e04618f10421b03822b12a939c891bbd0151","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d064e04618f10421b03822b12a939c891bbd0151/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"18b71e92481f44d2553e9ea1852ac7086ceac173","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/18b71e92481f44d2553e9ea1852ac7086ceac173","html_url":"https://github.com/ThiagoCodecov/example-python/commit/18b71e92481f44d2553e9ea1852ac7086ceac173"}]},{"sha":"41b90bf52b1cb94f02aae4a775875bd57f6c1cdb","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjQxYjkwYmY1MmIxY2I5NGYwMmFhZTRhNzc1ODc1YmQ1N2Y2YzFjZGI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"0B0BD4D3-71EB-4CFD-957C-98EE5F5EE898","tree":{"sha":"0c5d46ec06196f364156ec638fce699cf5e6bb8b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/0c5d46ec06196f364156ec638fce699cf5e6bb8b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/41b90bf52b1cb94f02aae4a775875bd57f6c1cdb","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/41b90bf52b1cb94f02aae4a775875bd57f6c1cdb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/41b90bf52b1cb94f02aae4a775875bd57f6c1cdb","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/41b90bf52b1cb94f02aae4a775875bd57f6c1cdb/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"d064e04618f10421b03822b12a939c891bbd0151","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d064e04618f10421b03822b12a939c891bbd0151","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d064e04618f10421b03822b12a939c891bbd0151"}]},{"sha":"0c59e9415ab1f62042702b892355c78b1bb7a0bb","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjBjNTllOTQxNWFiMWY2MjA0MjcwMmI4OTIzNTVjNzhiMWJiN2EwYmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"D459DBA2-188F-46FE-9A7B-5FBC74C98BD9","tree":{"sha":"450cd2aa4827685e5f11e3bbe9d0342ba3f8b01d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/450cd2aa4827685e5f11e3bbe9d0342ba3f8b01d"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/0c59e9415ab1f62042702b892355c78b1bb7a0bb","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0c59e9415ab1f62042702b892355c78b1bb7a0bb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0c59e9415ab1f62042702b892355c78b1bb7a0bb","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0c59e9415ab1f62042702b892355c78b1bb7a0bb/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"41b90bf52b1cb94f02aae4a775875bd57f6c1cdb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/41b90bf52b1cb94f02aae4a775875bd57f6c1cdb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/41b90bf52b1cb94f02aae4a775875bd57f6c1cdb"}]},{"sha":"e4be18db28c335f1de52e99e106267ce4da09405","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmU0YmUxOGRiMjhjMzM1ZjFkZTUyZTk5ZTEwNjI2N2NlNGRhMDk0MDU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:29:59Z"},"message":"10B9D0E4-4E81-4B08-89C9-DFF91C1B9C8E","tree":{"sha":"9a53f4e63aef76de3f64d4138029fa95935df3fa","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9a53f4e63aef76de3f64d4138029fa95935df3fa"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e4be18db28c335f1de52e99e106267ce4da09405","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e4be18db28c335f1de52e99e106267ce4da09405","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e4be18db28c335f1de52e99e106267ce4da09405","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e4be18db28c335f1de52e99e106267ce4da09405/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"0c59e9415ab1f62042702b892355c78b1bb7a0bb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0c59e9415ab1f62042702b892355c78b1bb7a0bb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0c59e9415ab1f62042702b892355c78b1bb7a0bb"}]},{"sha":"1d19ee9e4a2bf428e3ea17c16b5e78fe76a651f7","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjFkMTllZTllNGEyYmY0MjhlM2VhMTdjMTZiNWU3OGZlNzZhNjUxZjc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"209C7E2F-A408-40EB-A507-2AEED2FF9A82","tree":{"sha":"bea38b0ba475ceed674b289c78799efcf1f1882c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/bea38b0ba475ceed674b289c78799efcf1f1882c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/1d19ee9e4a2bf428e3ea17c16b5e78fe76a651f7","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1d19ee9e4a2bf428e3ea17c16b5e78fe76a651f7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1d19ee9e4a2bf428e3ea17c16b5e78fe76a651f7","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1d19ee9e4a2bf428e3ea17c16b5e78fe76a651f7/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e4be18db28c335f1de52e99e106267ce4da09405","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e4be18db28c335f1de52e99e106267ce4da09405","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e4be18db28c335f1de52e99e106267ce4da09405"}]},{"sha":"c5bebcb95bf76b8cc34ebf88b2ba1be3ffd8e9ca","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmM1YmViY2I5NWJmNzZiOGNjMzRlYmY4OGIyYmExYmUzZmZkOGU5Y2E=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"D5CCBC97-4AEE-4220-ACBC-29DA95877E2C","tree":{"sha":"91391401dddc27848dbd739863c083b383075e10","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/91391401dddc27848dbd739863c083b383075e10"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/c5bebcb95bf76b8cc34ebf88b2ba1be3ffd8e9ca","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5bebcb95bf76b8cc34ebf88b2ba1be3ffd8e9ca","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c5bebcb95bf76b8cc34ebf88b2ba1be3ffd8e9ca","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5bebcb95bf76b8cc34ebf88b2ba1be3ffd8e9ca/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"1d19ee9e4a2bf428e3ea17c16b5e78fe76a651f7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1d19ee9e4a2bf428e3ea17c16b5e78fe76a651f7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1d19ee9e4a2bf428e3ea17c16b5e78fe76a651f7"}]},{"sha":"6950cbe2424727244f8af33afbeea3fac98dd101","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjY5NTBjYmUyNDI0NzI3MjQ0ZjhhZjMzYWZiZWVhM2ZhYzk4ZGQxMDE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"3608A398-04FA-41D1-B460-6397C1FB98C7","tree":{"sha":"9978a575a2feb0990bd5df0550c85a1507a48250","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/9978a575a2feb0990bd5df0550c85a1507a48250"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6950cbe2424727244f8af33afbeea3fac98dd101","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6950cbe2424727244f8af33afbeea3fac98dd101","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6950cbe2424727244f8af33afbeea3fac98dd101","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6950cbe2424727244f8af33afbeea3fac98dd101/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"c5bebcb95bf76b8cc34ebf88b2ba1be3ffd8e9ca","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/c5bebcb95bf76b8cc34ebf88b2ba1be3ffd8e9ca","html_url":"https://github.com/ThiagoCodecov/example-python/commit/c5bebcb95bf76b8cc34ebf88b2ba1be3ffd8e9ca"}]},{"sha":"f95ad1ac3ac31fe3dc66e7b433d243230a7d4b50","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmY5NWFkMWFjM2FjMzFmZTNkYzY2ZTdiNDMzZDI0MzIzMGE3ZDRiNTA=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"1AB7FD55-2338-4207-9B10-2CD3E16D46E3","tree":{"sha":"93ce57f58e7e9023e6221cd62cb9060f02809904","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/93ce57f58e7e9023e6221cd62cb9060f02809904"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f95ad1ac3ac31fe3dc66e7b433d243230a7d4b50","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f95ad1ac3ac31fe3dc66e7b433d243230a7d4b50","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f95ad1ac3ac31fe3dc66e7b433d243230a7d4b50","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f95ad1ac3ac31fe3dc66e7b433d243230a7d4b50/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6950cbe2424727244f8af33afbeea3fac98dd101","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6950cbe2424727244f8af33afbeea3fac98dd101","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6950cbe2424727244f8af33afbeea3fac98dd101"}]},{"sha":"b0d005a73630368862eef002cc6b72f837d883d7","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmIwZDAwNWE3MzYzMDM2ODg2MmVlZjAwMmNjNmI3MmY4MzdkODgzZDc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"A3FE2EC3-EB24-4743-BBEB-0F580A7F0497","tree":{"sha":"2459e08b4dcf63fef5f94eb2f85747884842a027","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2459e08b4dcf63fef5f94eb2f85747884842a027"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b0d005a73630368862eef002cc6b72f837d883d7","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b0d005a73630368862eef002cc6b72f837d883d7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b0d005a73630368862eef002cc6b72f837d883d7","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b0d005a73630368862eef002cc6b72f837d883d7/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f95ad1ac3ac31fe3dc66e7b433d243230a7d4b50","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f95ad1ac3ac31fe3dc66e7b433d243230a7d4b50","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f95ad1ac3ac31fe3dc66e7b433d243230a7d4b50"}]},{"sha":"f9e633b2dccdb654c059efa3e093eeca87fac260","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmY5ZTYzM2IyZGNjZGI2NTRjMDU5ZWZhM2UwOTNlZWNhODdmYWMyNjA=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"1AE518FB-D28F-4D9D-8EAF-0E2C4430B178","tree":{"sha":"e442d084ca1a0c1ea43c6a221244b2f0c4b1e0b4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e442d084ca1a0c1ea43c6a221244b2f0c4b1e0b4"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f9e633b2dccdb654c059efa3e093eeca87fac260","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9e633b2dccdb654c059efa3e093eeca87fac260","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f9e633b2dccdb654c059efa3e093eeca87fac260","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9e633b2dccdb654c059efa3e093eeca87fac260/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"b0d005a73630368862eef002cc6b72f837d883d7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b0d005a73630368862eef002cc6b72f837d883d7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b0d005a73630368862eef002cc6b72f837d883d7"}]},{"sha":"30b1e130ec9c5eea839350c0a0fa96b2f27e1124","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjMwYjFlMTMwZWM5YzVlZWE4MzkzNTBjMGEwZmE5NmIyZjI3ZTExMjQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"94C37590-0B98-4BE4-8746-F078A52827DE","tree":{"sha":"3ef046433f33a28bb6ac6c441a699c410511166a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3ef046433f33a28bb6ac6c441a699c410511166a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/30b1e130ec9c5eea839350c0a0fa96b2f27e1124","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30b1e130ec9c5eea839350c0a0fa96b2f27e1124","html_url":"https://github.com/ThiagoCodecov/example-python/commit/30b1e130ec9c5eea839350c0a0fa96b2f27e1124","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30b1e130ec9c5eea839350c0a0fa96b2f27e1124/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f9e633b2dccdb654c059efa3e093eeca87fac260","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f9e633b2dccdb654c059efa3e093eeca87fac260","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f9e633b2dccdb654c059efa3e093eeca87fac260"}]},{"sha":"9d32d13a765f44af714deefdc592e6ca600acc64","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjlkMzJkMTNhNzY1ZjQ0YWY3MTRkZWVmZGM1OTJlNmNhNjAwYWNjNjQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"B2A3F22E-6AAA-4069-8D58-9F911EF331D6","tree":{"sha":"47f5f92254c4705e46daacdc35d18cee237490c3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/47f5f92254c4705e46daacdc35d18cee237490c3"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/9d32d13a765f44af714deefdc592e6ca600acc64","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9d32d13a765f44af714deefdc592e6ca600acc64","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9d32d13a765f44af714deefdc592e6ca600acc64","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9d32d13a765f44af714deefdc592e6ca600acc64/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"30b1e130ec9c5eea839350c0a0fa96b2f27e1124","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/30b1e130ec9c5eea839350c0a0fa96b2f27e1124","html_url":"https://github.com/ThiagoCodecov/example-python/commit/30b1e130ec9c5eea839350c0a0fa96b2f27e1124"}]},{"sha":"9450d20f9e5748b556ea728ff567e14e6bc87f33","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojk0NTBkMjBmOWU1NzQ4YjU1NmVhNzI4ZmY1NjdlMTRlNmJjODdmMzM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"3CB048B7-BBE2-427A-8BA3-2A5F175C1DF6","tree":{"sha":"cc07cdf8efe1685e655d88639b9715b41a4896bd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/cc07cdf8efe1685e655d88639b9715b41a4896bd"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/9450d20f9e5748b556ea728ff567e14e6bc87f33","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9450d20f9e5748b556ea728ff567e14e6bc87f33","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9450d20f9e5748b556ea728ff567e14e6bc87f33","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9450d20f9e5748b556ea728ff567e14e6bc87f33/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"9d32d13a765f44af714deefdc592e6ca600acc64","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9d32d13a765f44af714deefdc592e6ca600acc64","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9d32d13a765f44af714deefdc592e6ca600acc64"}]},{"sha":"7378e45f48911af1866ca1012d548598c2239256","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjczNzhlNDVmNDg5MTFhZjE4NjZjYTEwMTJkNTQ4NTk4YzIyMzkyNTY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"E41D5691-ED5E-4636-AD59-780AD9800A5E","tree":{"sha":"97a363add14c8d4b81f9bbedf6f31357b73975d6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/97a363add14c8d4b81f9bbedf6f31357b73975d6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/7378e45f48911af1866ca1012d548598c2239256","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7378e45f48911af1866ca1012d548598c2239256","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7378e45f48911af1866ca1012d548598c2239256","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7378e45f48911af1866ca1012d548598c2239256/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"9450d20f9e5748b556ea728ff567e14e6bc87f33","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9450d20f9e5748b556ea728ff567e14e6bc87f33","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9450d20f9e5748b556ea728ff567e14e6bc87f33"}]},{"sha":"1a78965b57c27cd42eb77a63b3f361385dfcf5a4","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjFhNzg5NjViNTdjMjdjZDQyZWI3N2E2M2IzZjM2MTM4NWRmY2Y1YTQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"F33823E7-58B7-41CA-8B03-6CB6C1208396","tree":{"sha":"67bf2a7b22e27e419228cdb0f2a2e988dca7cbc6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/67bf2a7b22e27e419228cdb0f2a2e988dca7cbc6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/1a78965b57c27cd42eb77a63b3f361385dfcf5a4","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1a78965b57c27cd42eb77a63b3f361385dfcf5a4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1a78965b57c27cd42eb77a63b3f361385dfcf5a4","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1a78965b57c27cd42eb77a63b3f361385dfcf5a4/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"7378e45f48911af1866ca1012d548598c2239256","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7378e45f48911af1866ca1012d548598c2239256","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7378e45f48911af1866ca1012d548598c2239256"}]},{"sha":"58d84cede473a9a177e865649a53d03dd09db33f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjU4ZDg0Y2VkZTQ3M2E5YTE3N2U4NjU2NDlhNTNkMDNkZDA5ZGIzM2Y=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"E7F63987-2A76-4F9F-BAB7-830D6CA32E5F","tree":{"sha":"4349504d8fc937cff6e3d6596ba2549ca86527f7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4349504d8fc937cff6e3d6596ba2549ca86527f7"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/58d84cede473a9a177e865649a53d03dd09db33f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/58d84cede473a9a177e865649a53d03dd09db33f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/58d84cede473a9a177e865649a53d03dd09db33f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/58d84cede473a9a177e865649a53d03dd09db33f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"1a78965b57c27cd42eb77a63b3f361385dfcf5a4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1a78965b57c27cd42eb77a63b3f361385dfcf5a4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1a78965b57c27cd42eb77a63b3f361385dfcf5a4"}]},{"sha":"2cd79ebc72fb1c829a86088ab8b10faded7ddc3e","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjJjZDc5ZWJjNzJmYjFjODI5YTg2MDg4YWI4YjEwZmFkZWQ3ZGRjM2U=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"A3F8E982-7AB3-4171-A72D-517DC3C45ABD","tree":{"sha":"56c631d754cf7066da5cf72787b7249ed913885b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/56c631d754cf7066da5cf72787b7249ed913885b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/2cd79ebc72fb1c829a86088ab8b10faded7ddc3e","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2cd79ebc72fb1c829a86088ab8b10faded7ddc3e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2cd79ebc72fb1c829a86088ab8b10faded7ddc3e","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2cd79ebc72fb1c829a86088ab8b10faded7ddc3e/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"58d84cede473a9a177e865649a53d03dd09db33f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/58d84cede473a9a177e865649a53d03dd09db33f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/58d84cede473a9a177e865649a53d03dd09db33f"}]},{"sha":"8b6014b78e0d24feafc50f1fe7db6f2809015908","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjhiNjAxNGI3OGUwZDI0ZmVhZmM1MGYxZmU3ZGI2ZjI4MDkwMTU5MDg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"4041EF86-DA44-4D0D-8DC6-F13F3C94969E","tree":{"sha":"576979f67b66df12b388ca92affe8002a31ff96c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/576979f67b66df12b388ca92affe8002a31ff96c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8b6014b78e0d24feafc50f1fe7db6f2809015908","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8b6014b78e0d24feafc50f1fe7db6f2809015908","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8b6014b78e0d24feafc50f1fe7db6f2809015908","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8b6014b78e0d24feafc50f1fe7db6f2809015908/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"2cd79ebc72fb1c829a86088ab8b10faded7ddc3e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/2cd79ebc72fb1c829a86088ab8b10faded7ddc3e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/2cd79ebc72fb1c829a86088ab8b10faded7ddc3e"}]},{"sha":"0faf425961a1f220c9c3cfe545d8f33721a059fc","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjBmYWY0MjU5NjFhMWYyMjBjOWMzY2ZlNTQ1ZDhmMzM3MjFhMDU5ZmM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"9CFEC12B-5CBF-4782-8A44-E2E8DE960DDC","tree":{"sha":"41dbba236bea0580ee460290d373ca1e8485bcf5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/41dbba236bea0580ee460290d373ca1e8485bcf5"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/0faf425961a1f220c9c3cfe545d8f33721a059fc","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0faf425961a1f220c9c3cfe545d8f33721a059fc","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0faf425961a1f220c9c3cfe545d8f33721a059fc","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0faf425961a1f220c9c3cfe545d8f33721a059fc/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"8b6014b78e0d24feafc50f1fe7db6f2809015908","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8b6014b78e0d24feafc50f1fe7db6f2809015908","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8b6014b78e0d24feafc50f1fe7db6f2809015908"}]},{"sha":"96ddcdd8f4ab56952d5e8bcd84114ab76f856088","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojk2ZGRjZGQ4ZjRhYjU2OTUyZDVlOGJjZDg0MTE0YWI3NmY4NTYwODg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"94B5B96E-D172-4445-98A3-142FF51E5068","tree":{"sha":"4f69ef5859afb1857287c19c7ae66ca743f2339f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4f69ef5859afb1857287c19c7ae66ca743f2339f"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/96ddcdd8f4ab56952d5e8bcd84114ab76f856088","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/96ddcdd8f4ab56952d5e8bcd84114ab76f856088","html_url":"https://github.com/ThiagoCodecov/example-python/commit/96ddcdd8f4ab56952d5e8bcd84114ab76f856088","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/96ddcdd8f4ab56952d5e8bcd84114ab76f856088/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"0faf425961a1f220c9c3cfe545d8f33721a059fc","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/0faf425961a1f220c9c3cfe545d8f33721a059fc","html_url":"https://github.com/ThiagoCodecov/example-python/commit/0faf425961a1f220c9c3cfe545d8f33721a059fc"}]},{"sha":"5d84876d9a5a894a7b78a73c24c3e522dba53f11","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjVkODQ4NzZkOWE1YTg5NGE3Yjc4YTczYzI0YzNlNTIyZGJhNTNmMTE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"CCF3A901-CBE3-4995-B839-1B3A285B587E","tree":{"sha":"3d40c8dd7a866f9e8a21a707e93efaeb9fb2c82c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/3d40c8dd7a866f9e8a21a707e93efaeb9fb2c82c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/5d84876d9a5a894a7b78a73c24c3e522dba53f11","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/5d84876d9a5a894a7b78a73c24c3e522dba53f11","html_url":"https://github.com/ThiagoCodecov/example-python/commit/5d84876d9a5a894a7b78a73c24c3e522dba53f11","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/5d84876d9a5a894a7b78a73c24c3e522dba53f11/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"96ddcdd8f4ab56952d5e8bcd84114ab76f856088","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/96ddcdd8f4ab56952d5e8bcd84114ab76f856088","html_url":"https://github.com/ThiagoCodecov/example-python/commit/96ddcdd8f4ab56952d5e8bcd84114ab76f856088"}]},{"sha":"15920b41eae483576709d4dddce7173a7a585894","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjE1OTIwYjQxZWFlNDgzNTc2NzA5ZDRkZGRjZTcxNzNhN2E1ODU4OTQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"7C8799BD-DE84-459E-9758-F11F98678202","tree":{"sha":"c2e04ea8cc56f0cf4aa4df6bf14f11d5c77601ba","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c2e04ea8cc56f0cf4aa4df6bf14f11d5c77601ba"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/15920b41eae483576709d4dddce7173a7a585894","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/15920b41eae483576709d4dddce7173a7a585894","html_url":"https://github.com/ThiagoCodecov/example-python/commit/15920b41eae483576709d4dddce7173a7a585894","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/15920b41eae483576709d4dddce7173a7a585894/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"5d84876d9a5a894a7b78a73c24c3e522dba53f11","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/5d84876d9a5a894a7b78a73c24c3e522dba53f11","html_url":"https://github.com/ThiagoCodecov/example-python/commit/5d84876d9a5a894a7b78a73c24c3e522dba53f11"}]},{"sha":"8a4379f39d9f03df8d90be613ff7c71478642627","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjhhNDM3OWYzOWQ5ZjAzZGY4ZDkwYmU2MTNmZjdjNzE0Nzg2NDI2Mjc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"062B4A48-FE54-46F5-BCE4-0CBEFAA64B60","tree":{"sha":"41579ef10d21efabcdf75b932b57ba7173eece92","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/41579ef10d21efabcdf75b932b57ba7173eece92"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8a4379f39d9f03df8d90be613ff7c71478642627","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8a4379f39d9f03df8d90be613ff7c71478642627","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8a4379f39d9f03df8d90be613ff7c71478642627","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8a4379f39d9f03df8d90be613ff7c71478642627/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"15920b41eae483576709d4dddce7173a7a585894","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/15920b41eae483576709d4dddce7173a7a585894","html_url":"https://github.com/ThiagoCodecov/example-python/commit/15920b41eae483576709d4dddce7173a7a585894"}]},{"sha":"96a4bce44d7e8234d0f9d082620b22f8eb67d875","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojk2YTRiY2U0NGQ3ZTgyMzRkMGY5ZDA4MjYyMGIyMmY4ZWI2N2Q4NzU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"1B662FA3-4BF3-4EE5-9A74-FA8B5154979E","tree":{"sha":"c5a9a97a87d9651478c3cdccdf40c1d71e74f10c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c5a9a97a87d9651478c3cdccdf40c1d71e74f10c"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/96a4bce44d7e8234d0f9d082620b22f8eb67d875","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/96a4bce44d7e8234d0f9d082620b22f8eb67d875","html_url":"https://github.com/ThiagoCodecov/example-python/commit/96a4bce44d7e8234d0f9d082620b22f8eb67d875","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/96a4bce44d7e8234d0f9d082620b22f8eb67d875/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"8a4379f39d9f03df8d90be613ff7c71478642627","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8a4379f39d9f03df8d90be613ff7c71478642627","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8a4379f39d9f03df8d90be613ff7c71478642627"}]},{"sha":"e598b9b1f2c12e363088d1c80243c59a8cd4909f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmU1OThiOWIxZjJjMTJlMzYzMDg4ZDFjODAyNDNjNTlhOGNkNDkwOWY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"C0B8D0B6-FA1C-4412-AA07-4CC962CF6152","tree":{"sha":"8e9937fedc3700e355e4cd63440035e7d25434e0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/8e9937fedc3700e355e4cd63440035e7d25434e0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e598b9b1f2c12e363088d1c80243c59a8cd4909f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e598b9b1f2c12e363088d1c80243c59a8cd4909f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e598b9b1f2c12e363088d1c80243c59a8cd4909f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e598b9b1f2c12e363088d1c80243c59a8cd4909f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"96a4bce44d7e8234d0f9d082620b22f8eb67d875","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/96a4bce44d7e8234d0f9d082620b22f8eb67d875","html_url":"https://github.com/ThiagoCodecov/example-python/commit/96a4bce44d7e8234d0f9d082620b22f8eb67d875"}]},{"sha":"7ab2c1d6db37de9d21e68df3632abd1f35c4579c","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjdhYjJjMWQ2ZGIzN2RlOWQyMWU2OGRmMzYzMmFiZDFmMzVjNDU3OWM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"8AAF8339-8580-4138-9676-52E3503A2A45","tree":{"sha":"dd1d20446f80f5c8bcb36fc6562f872d8d09d17a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/dd1d20446f80f5c8bcb36fc6562f872d8d09d17a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/7ab2c1d6db37de9d21e68df3632abd1f35c4579c","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7ab2c1d6db37de9d21e68df3632abd1f35c4579c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7ab2c1d6db37de9d21e68df3632abd1f35c4579c","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7ab2c1d6db37de9d21e68df3632abd1f35c4579c/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e598b9b1f2c12e363088d1c80243c59a8cd4909f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e598b9b1f2c12e363088d1c80243c59a8cd4909f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e598b9b1f2c12e363088d1c80243c59a8cd4909f"}]},{"sha":"526ec4fe6ff49ad1b24013a2adc49c93b4422b4d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjUyNmVjNGZlNmZmNDlhZDFiMjQwMTNhMmFkYzQ5YzkzYjQ0MjJiNGQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"6B8CA164-8E4E-423F-B637-346F6225D666","tree":{"sha":"75884d38eaa31189a5cf4514834464837f75a2e8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/75884d38eaa31189a5cf4514834464837f75a2e8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/526ec4fe6ff49ad1b24013a2adc49c93b4422b4d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/526ec4fe6ff49ad1b24013a2adc49c93b4422b4d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/526ec4fe6ff49ad1b24013a2adc49c93b4422b4d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/526ec4fe6ff49ad1b24013a2adc49c93b4422b4d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"7ab2c1d6db37de9d21e68df3632abd1f35c4579c","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/7ab2c1d6db37de9d21e68df3632abd1f35c4579c","html_url":"https://github.com/ThiagoCodecov/example-python/commit/7ab2c1d6db37de9d21e68df3632abd1f35c4579c"}]},{"sha":"9376f6e2e60c31718e45011f4dbeb3a3580ba0e5","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjkzNzZmNmUyZTYwYzMxNzE4ZTQ1MDExZjRkYmViM2EzNTgwYmEwZTU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"B25AF40F-BAF9-4C7B-8C95-EC0E362E637A","tree":{"sha":"34a1b082f6a15523d68b5d5a7f91a03818f824f8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/34a1b082f6a15523d68b5d5a7f91a03818f824f8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/9376f6e2e60c31718e45011f4dbeb3a3580ba0e5","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9376f6e2e60c31718e45011f4dbeb3a3580ba0e5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9376f6e2e60c31718e45011f4dbeb3a3580ba0e5","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9376f6e2e60c31718e45011f4dbeb3a3580ba0e5/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"526ec4fe6ff49ad1b24013a2adc49c93b4422b4d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/526ec4fe6ff49ad1b24013a2adc49c93b4422b4d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/526ec4fe6ff49ad1b24013a2adc49c93b4422b4d"}]},{"sha":"add5912fe7c41181d2380e387b1d72175608d72d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmFkZDU5MTJmZTdjNDExODFkMjM4MGUzODdiMWQ3MjE3NTYwOGQ3MmQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"7914A47A-7B99-47D6-AAC5-76EFDC6C2DDE","tree":{"sha":"ceb09f271cb82c7add3bdf97029d2813f649292a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ceb09f271cb82c7add3bdf97029d2813f649292a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/add5912fe7c41181d2380e387b1d72175608d72d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/add5912fe7c41181d2380e387b1d72175608d72d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/add5912fe7c41181d2380e387b1d72175608d72d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/add5912fe7c41181d2380e387b1d72175608d72d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"9376f6e2e60c31718e45011f4dbeb3a3580ba0e5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9376f6e2e60c31718e45011f4dbeb3a3580ba0e5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9376f6e2e60c31718e45011f4dbeb3a3580ba0e5"}]},{"sha":"bddf7db2abdb2f641cba33bfefdb0b7b07725af4","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmJkZGY3ZGIyYWJkYjJmNjQxY2JhMzNiZmVmZGIwYjdiMDc3MjVhZjQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"95BCCC68-CE92-4722-90D6-74950C0A25B7","tree":{"sha":"0bd60d0cc1fe3e3b2a002fc9e894a574ceec97fe","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/0bd60d0cc1fe3e3b2a002fc9e894a574ceec97fe"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/bddf7db2abdb2f641cba33bfefdb0b7b07725af4","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bddf7db2abdb2f641cba33bfefdb0b7b07725af4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bddf7db2abdb2f641cba33bfefdb0b7b07725af4","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bddf7db2abdb2f641cba33bfefdb0b7b07725af4/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"add5912fe7c41181d2380e387b1d72175608d72d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/add5912fe7c41181d2380e387b1d72175608d72d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/add5912fe7c41181d2380e387b1d72175608d72d"}]},{"sha":"23c54dc9eca35944924b102ec65d530197b094f2","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjIzYzU0ZGM5ZWNhMzU5NDQ5MjRiMTAyZWM2NWQ1MzAxOTdiMDk0ZjI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"38215319-2557-4E2E-A416-C81301295654","tree":{"sha":"c579752465d157f287ba302a9bd23150d27c16d8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c579752465d157f287ba302a9bd23150d27c16d8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/23c54dc9eca35944924b102ec65d530197b094f2","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/23c54dc9eca35944924b102ec65d530197b094f2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/23c54dc9eca35944924b102ec65d530197b094f2","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/23c54dc9eca35944924b102ec65d530197b094f2/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"bddf7db2abdb2f641cba33bfefdb0b7b07725af4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bddf7db2abdb2f641cba33bfefdb0b7b07725af4","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bddf7db2abdb2f641cba33bfefdb0b7b07725af4"}]},{"sha":"deb5b65d42e3d82b1910e32bb9ae2e58acf40fed","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmRlYjViNjVkNDJlM2Q4MmIxOTEwZTMyYmI5YWUyZTU4YWNmNDBmZWQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"89F210DE-E749-4855-B138-51F07DF3D6F8","tree":{"sha":"31118e430635551b3ef5bc09a7f33c5da2408a0a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/31118e430635551b3ef5bc09a7f33c5da2408a0a"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/deb5b65d42e3d82b1910e32bb9ae2e58acf40fed","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/deb5b65d42e3d82b1910e32bb9ae2e58acf40fed","html_url":"https://github.com/ThiagoCodecov/example-python/commit/deb5b65d42e3d82b1910e32bb9ae2e58acf40fed","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/deb5b65d42e3d82b1910e32bb9ae2e58acf40fed/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"23c54dc9eca35944924b102ec65d530197b094f2","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/23c54dc9eca35944924b102ec65d530197b094f2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/23c54dc9eca35944924b102ec65d530197b094f2"}]},{"sha":"3b84eeb8d1b44b834263bf668f2f9cce3430c908","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjNiODRlZWI4ZDFiNDRiODM0MjYzYmY2NjhmMmY5Y2NlMzQzMGM5MDg=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"3E3D0DD1-9510-4292-9EAA-9E80F1D24DD6","tree":{"sha":"74ad8cdd8b5ab77ed84e4ca13fa16f8c248c9279","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/74ad8cdd8b5ab77ed84e4ca13fa16f8c248c9279"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3b84eeb8d1b44b834263bf668f2f9cce3430c908","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3b84eeb8d1b44b834263bf668f2f9cce3430c908","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3b84eeb8d1b44b834263bf668f2f9cce3430c908","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3b84eeb8d1b44b834263bf668f2f9cce3430c908/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"deb5b65d42e3d82b1910e32bb9ae2e58acf40fed","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/deb5b65d42e3d82b1910e32bb9ae2e58acf40fed","html_url":"https://github.com/ThiagoCodecov/example-python/commit/deb5b65d42e3d82b1910e32bb9ae2e58acf40fed"}]},{"sha":"ea87561036c9588836e8844aa1b400b0f904e4b3","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmVhODc1NjEwMzZjOTU4ODgzNmU4ODQ0YWExYjQwMGIwZjkwNGU0YjM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"50A3EC78-90BE-45F6-AA90-084BA34E8A6B","tree":{"sha":"2eb1d8a1b491dc44335960468879bcd3dc47fda7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2eb1d8a1b491dc44335960468879bcd3dc47fda7"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ea87561036c9588836e8844aa1b400b0f904e4b3","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea87561036c9588836e8844aa1b400b0f904e4b3","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ea87561036c9588836e8844aa1b400b0f904e4b3","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea87561036c9588836e8844aa1b400b0f904e4b3/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"3b84eeb8d1b44b834263bf668f2f9cce3430c908","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3b84eeb8d1b44b834263bf668f2f9cce3430c908","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3b84eeb8d1b44b834263bf668f2f9cce3430c908"}]},{"sha":"b88fa604e687068de20e7e8e173c5b1037b28605","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmI4OGZhNjA0ZTY4NzA2OGRlMjBlN2U4ZTE3M2M1YjEwMzdiMjg2MDU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"F2EE0C5F-D438-48E2-B199-623CAAA4FC54","tree":{"sha":"95badee8c73d82f8398fb60d91c3c611159aaf4b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/95badee8c73d82f8398fb60d91c3c611159aaf4b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b88fa604e687068de20e7e8e173c5b1037b28605","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b88fa604e687068de20e7e8e173c5b1037b28605","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b88fa604e687068de20e7e8e173c5b1037b28605","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b88fa604e687068de20e7e8e173c5b1037b28605/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ea87561036c9588836e8844aa1b400b0f904e4b3","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ea87561036c9588836e8844aa1b400b0f904e4b3","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ea87561036c9588836e8844aa1b400b0f904e4b3"}]},{"sha":"ceec5687a908804c937b4e2771558a3aa50f15e2","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmNlZWM1Njg3YTkwODgwNGM5MzdiNGUyNzcxNTU4YTNhYTUwZjE1ZTI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:00Z"},"message":"2AC42615-2E43-4264-937A-19E6681CE3BB","tree":{"sha":"7840f137837c8f5824e2931e0685998532c80aa7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7840f137837c8f5824e2931e0685998532c80aa7"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ceec5687a908804c937b4e2771558a3aa50f15e2","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ceec5687a908804c937b4e2771558a3aa50f15e2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ceec5687a908804c937b4e2771558a3aa50f15e2","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ceec5687a908804c937b4e2771558a3aa50f15e2/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"b88fa604e687068de20e7e8e173c5b1037b28605","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b88fa604e687068de20e7e8e173c5b1037b28605","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b88fa604e687068de20e7e8e173c5b1037b28605"}]},{"sha":"bfadc5b4294cb4ad98e8b146d9ed939b64e58bc0","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmJmYWRjNWI0Mjk0Y2I0YWQ5OGU4YjE0NmQ5ZWQ5MzliNjRlNThiYzA=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"3D2B18B9-078A-4666-AE47-DC822F01A43E","tree":{"sha":"6717e0252dbaf04856699a205e3c87aaa03cde6d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6717e0252dbaf04856699a205e3c87aaa03cde6d"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/bfadc5b4294cb4ad98e8b146d9ed939b64e58bc0","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bfadc5b4294cb4ad98e8b146d9ed939b64e58bc0","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bfadc5b4294cb4ad98e8b146d9ed939b64e58bc0","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bfadc5b4294cb4ad98e8b146d9ed939b64e58bc0/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ceec5687a908804c937b4e2771558a3aa50f15e2","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ceec5687a908804c937b4e2771558a3aa50f15e2","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ceec5687a908804c937b4e2771558a3aa50f15e2"}]},{"sha":"3f24ab34158d9dd5bc45e3d1c1b19dbe98b7470a","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjNmMjRhYjM0MTU4ZDlkZDViYzQ1ZTNkMWMxYjE5ZGJlOThiNzQ3MGE=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"5F8E53A8-E4D7-4104-91B5-5DF7EBD8550B","tree":{"sha":"a37f4568bb47bfce9704ce9f514b4cb7252b9fb7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/a37f4568bb47bfce9704ce9f514b4cb7252b9fb7"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/3f24ab34158d9dd5bc45e3d1c1b19dbe98b7470a","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f24ab34158d9dd5bc45e3d1c1b19dbe98b7470a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3f24ab34158d9dd5bc45e3d1c1b19dbe98b7470a","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f24ab34158d9dd5bc45e3d1c1b19dbe98b7470a/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"bfadc5b4294cb4ad98e8b146d9ed939b64e58bc0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/bfadc5b4294cb4ad98e8b146d9ed939b64e58bc0","html_url":"https://github.com/ThiagoCodecov/example-python/commit/bfadc5b4294cb4ad98e8b146d9ed939b64e58bc0"}]},{"sha":"aab4d70d9bfb2333d087f785fb6fb7c3ed27b536","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmFhYjRkNzBkOWJmYjIzMzNkMDg3Zjc4NWZiNmZiN2MzZWQyN2I1MzY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"278FF3DE-B1F5-4172-AA35-580D2BF5B3D9","tree":{"sha":"2d23900e60a6bb4de8bcac70c2036481dc8d2886","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2d23900e60a6bb4de8bcac70c2036481dc8d2886"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/aab4d70d9bfb2333d087f785fb6fb7c3ed27b536","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/aab4d70d9bfb2333d087f785fb6fb7c3ed27b536","html_url":"https://github.com/ThiagoCodecov/example-python/commit/aab4d70d9bfb2333d087f785fb6fb7c3ed27b536","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/aab4d70d9bfb2333d087f785fb6fb7c3ed27b536/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"3f24ab34158d9dd5bc45e3d1c1b19dbe98b7470a","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/3f24ab34158d9dd5bc45e3d1c1b19dbe98b7470a","html_url":"https://github.com/ThiagoCodecov/example-python/commit/3f24ab34158d9dd5bc45e3d1c1b19dbe98b7470a"}]},{"sha":"4db44f2501a45116f9af3398460b022324c1468f","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjRkYjQ0ZjI1MDFhNDUxMTZmOWFmMzM5ODQ2MGIwMjIzMjRjMTQ2OGY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"725BD029-FEFE-4048-98B4-EF19C7D69E0E","tree":{"sha":"4d0f9fe208eaef325836e3ff2f164348f27a5c47","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4d0f9fe208eaef325836e3ff2f164348f27a5c47"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/4db44f2501a45116f9af3398460b022324c1468f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4db44f2501a45116f9af3398460b022324c1468f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/4db44f2501a45116f9af3398460b022324c1468f","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4db44f2501a45116f9af3398460b022324c1468f/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"aab4d70d9bfb2333d087f785fb6fb7c3ed27b536","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/aab4d70d9bfb2333d087f785fb6fb7c3ed27b536","html_url":"https://github.com/ThiagoCodecov/example-python/commit/aab4d70d9bfb2333d087f785fb6fb7c3ed27b536"}]},{"sha":"76d67a9f9a3fc04f2d484c5bf5f7874f8f4e0857","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3Ojc2ZDY3YTlmOWEzZmMwNGYyZDQ4NGM1YmY1Zjc4NzRmOGY0ZTA4NTc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"66798461-FDF9-4BA0-9C2F-1DAE7842BF9B","tree":{"sha":"b6e4ca79d2c460e4e6c68202075f06ae9495d0b5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/b6e4ca79d2c460e4e6c68202075f06ae9495d0b5"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/76d67a9f9a3fc04f2d484c5bf5f7874f8f4e0857","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76d67a9f9a3fc04f2d484c5bf5f7874f8f4e0857","html_url":"https://github.com/ThiagoCodecov/example-python/commit/76d67a9f9a3fc04f2d484c5bf5f7874f8f4e0857","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76d67a9f9a3fc04f2d484c5bf5f7874f8f4e0857/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"4db44f2501a45116f9af3398460b022324c1468f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/4db44f2501a45116f9af3398460b022324c1468f","html_url":"https://github.com/ThiagoCodecov/example-python/commit/4db44f2501a45116f9af3398460b022324c1468f"}]},{"sha":"1fe4fa1325c7e5bbd705225611d17104fcfb7113","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjFmZTRmYTEzMjVjN2U1YmJkNzA1MjI1NjExZDE3MTA0ZmNmYjcxMTM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"527AE5C2-1CFA-42DD-9832-92E963E37B8D","tree":{"sha":"f4c2ad49a1be39c72a1898e4f7e88ad5b3b7db0b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/f4c2ad49a1be39c72a1898e4f7e88ad5b3b7db0b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/1fe4fa1325c7e5bbd705225611d17104fcfb7113","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1fe4fa1325c7e5bbd705225611d17104fcfb7113","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1fe4fa1325c7e5bbd705225611d17104fcfb7113","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1fe4fa1325c7e5bbd705225611d17104fcfb7113/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"76d67a9f9a3fc04f2d484c5bf5f7874f8f4e0857","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/76d67a9f9a3fc04f2d484c5bf5f7874f8f4e0857","html_url":"https://github.com/ThiagoCodecov/example-python/commit/76d67a9f9a3fc04f2d484c5bf5f7874f8f4e0857"}]},{"sha":"d04db584d8653e0d3ea79744b8964659fe874cfb","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmQwNGRiNTg0ZDg2NTNlMGQzZWE3OTc0NGI4OTY0NjU5ZmU4NzRjZmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"4E264585-351C-49D0-B2F2-0C2334E5717B","tree":{"sha":"2e679d4277d9e20daee95c5aec3c6b176c4494c5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2e679d4277d9e20daee95c5aec3c6b176c4494c5"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/d04db584d8653e0d3ea79744b8964659fe874cfb","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d04db584d8653e0d3ea79744b8964659fe874cfb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d04db584d8653e0d3ea79744b8964659fe874cfb","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d04db584d8653e0d3ea79744b8964659fe874cfb/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"1fe4fa1325c7e5bbd705225611d17104fcfb7113","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1fe4fa1325c7e5bbd705225611d17104fcfb7113","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1fe4fa1325c7e5bbd705225611d17104fcfb7113"}]},{"sha":"45949ca86e4615ee120d03d836d59970e8370f50","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjQ1OTQ5Y2E4NmU0NjE1ZWUxMjBkMDNkODM2ZDU5OTcwZTgzNzBmNTA=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"E1AC39C8-444D-48E0-9B98-DA0DCE416432","tree":{"sha":"ebdbbcfa75ad3b9c1f85a0407ffd00c1b612055f","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ebdbbcfa75ad3b9c1f85a0407ffd00c1b612055f"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/45949ca86e4615ee120d03d836d59970e8370f50","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/45949ca86e4615ee120d03d836d59970e8370f50","html_url":"https://github.com/ThiagoCodecov/example-python/commit/45949ca86e4615ee120d03d836d59970e8370f50","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/45949ca86e4615ee120d03d836d59970e8370f50/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"d04db584d8653e0d3ea79744b8964659fe874cfb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/d04db584d8653e0d3ea79744b8964659fe874cfb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/d04db584d8653e0d3ea79744b8964659fe874cfb"}]},{"sha":"06e108add03229846039578980b9b5d12533ba1b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA2ZTEwOGFkZDAzMjI5ODQ2MDM5NTc4OTgwYjliNWQxMjUzM2JhMWI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"33E0398D-5D60-4472-950C-52284CF2D125","tree":{"sha":"c740c648ad7ca1a42fe55501ca58d033242b3bbd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c740c648ad7ca1a42fe55501ca58d033242b3bbd"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/06e108add03229846039578980b9b5d12533ba1b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/06e108add03229846039578980b9b5d12533ba1b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/06e108add03229846039578980b9b5d12533ba1b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/06e108add03229846039578980b9b5d12533ba1b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"45949ca86e4615ee120d03d836d59970e8370f50","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/45949ca86e4615ee120d03d836d59970e8370f50","html_url":"https://github.com/ThiagoCodecov/example-python/commit/45949ca86e4615ee120d03d836d59970e8370f50"}]},{"sha":"61472a8c4f24a364e77908795961823400c4406d","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjYxNDcyYThjNGYyNGEzNjRlNzc5MDg3OTU5NjE4MjM0MDBjNDQwNmQ=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"01A8EA6A-C530-4EE3-9257-C3693DF01C7B","tree":{"sha":"4757fe26bc3ce06fcb3d1f753897872d05a88238","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/4757fe26bc3ce06fcb3d1f753897872d05a88238"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/61472a8c4f24a364e77908795961823400c4406d","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/61472a8c4f24a364e77908795961823400c4406d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/61472a8c4f24a364e77908795961823400c4406d","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/61472a8c4f24a364e77908795961823400c4406d/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"06e108add03229846039578980b9b5d12533ba1b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/06e108add03229846039578980b9b5d12533ba1b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/06e108add03229846039578980b9b5d12533ba1b"}]},{"sha":"03b7470b88ebe8526a46767633d4cbd8fff62956","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjAzYjc0NzBiODhlYmU4NTI2YTQ2NzY3NjMzZDRjYmQ4ZmZmNjI5NTY=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"60D79295-54BA-48E9-9EDE-74F8063BE1DD","tree":{"sha":"a9c6add0b79860ac328a3afa0526724bd1c86104","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/a9c6add0b79860ac328a3afa0526724bd1c86104"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/03b7470b88ebe8526a46767633d4cbd8fff62956","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03b7470b88ebe8526a46767633d4cbd8fff62956","html_url":"https://github.com/ThiagoCodecov/example-python/commit/03b7470b88ebe8526a46767633d4cbd8fff62956","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03b7470b88ebe8526a46767633d4cbd8fff62956/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"61472a8c4f24a364e77908795961823400c4406d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/61472a8c4f24a364e77908795961823400c4406d","html_url":"https://github.com/ThiagoCodecov/example-python/commit/61472a8c4f24a364e77908795961823400c4406d"}]},{"sha":"e408d64ba2157a8bc22574cb383548037bde92bb","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmU0MDhkNjRiYTIxNTdhOGJjMjI1NzRjYjM4MzU0ODAzN2JkZTkyYmI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"AA939F61-0EF7-4588-96E3-2AD141CF6FF7","tree":{"sha":"baf62e007c87baef1efa6efa17018ea80c266667","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/baf62e007c87baef1efa6efa17018ea80c266667"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/e408d64ba2157a8bc22574cb383548037bde92bb","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e408d64ba2157a8bc22574cb383548037bde92bb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e408d64ba2157a8bc22574cb383548037bde92bb","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e408d64ba2157a8bc22574cb383548037bde92bb/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"03b7470b88ebe8526a46767633d4cbd8fff62956","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/03b7470b88ebe8526a46767633d4cbd8fff62956","html_url":"https://github.com/ThiagoCodecov/example-python/commit/03b7470b88ebe8526a46767633d4cbd8fff62956"}]},{"sha":"f1355e71e13acb00aa85c18cf74f3918394cdd29","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmYxMzU1ZTcxZTEzYWNiMDBhYTg1YzE4Y2Y3NGYzOTE4Mzk0Y2RkMjk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"0844CB64-C616-4CCD-8D9A-E280CC4A3E5C","tree":{"sha":"7b5637d64732f4a8677362c5bdfa81c8152d8ab0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/7b5637d64732f4a8677362c5bdfa81c8152d8ab0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f1355e71e13acb00aa85c18cf74f3918394cdd29","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f1355e71e13acb00aa85c18cf74f3918394cdd29","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f1355e71e13acb00aa85c18cf74f3918394cdd29","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f1355e71e13acb00aa85c18cf74f3918394cdd29/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"e408d64ba2157a8bc22574cb383548037bde92bb","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/e408d64ba2157a8bc22574cb383548037bde92bb","html_url":"https://github.com/ThiagoCodecov/example-python/commit/e408d64ba2157a8bc22574cb383548037bde92bb"}]},{"sha":"614c27e59b710cee284485d0eb19fe06085fb819","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjYxNGMyN2U1OWI3MTBjZWUyODQ0ODVkMGViMTlmZTA2MDg1ZmI4MTk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"E31D672D-926A-43E3-8084-59DB1414B97B","tree":{"sha":"ef22ccd68a011487831ca961cf1d06643a94e4a0","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/ef22ccd68a011487831ca961cf1d06643a94e4a0"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/614c27e59b710cee284485d0eb19fe06085fb819","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/614c27e59b710cee284485d0eb19fe06085fb819","html_url":"https://github.com/ThiagoCodecov/example-python/commit/614c27e59b710cee284485d0eb19fe06085fb819","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/614c27e59b710cee284485d0eb19fe06085fb819/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f1355e71e13acb00aa85c18cf74f3918394cdd29","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f1355e71e13acb00aa85c18cf74f3918394cdd29","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f1355e71e13acb00aa85c18cf74f3918394cdd29"}]},{"sha":"ef52cd1cc91bfc79503c1a0811e7b3612a8a33b5","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmVmNTJjZDFjYzkxYmZjNzk1MDNjMWEwODExZTdiMzYxMmE4YTMzYjU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"9006F548-CD76-4429-B91B-941679361BB7","tree":{"sha":"715bd8117c7d91ceb4585301b6668506db105396","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/715bd8117c7d91ceb4585301b6668506db105396"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/ef52cd1cc91bfc79503c1a0811e7b3612a8a33b5","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ef52cd1cc91bfc79503c1a0811e7b3612a8a33b5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ef52cd1cc91bfc79503c1a0811e7b3612a8a33b5","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ef52cd1cc91bfc79503c1a0811e7b3612a8a33b5/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"614c27e59b710cee284485d0eb19fe06085fb819","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/614c27e59b710cee284485d0eb19fe06085fb819","html_url":"https://github.com/ThiagoCodecov/example-python/commit/614c27e59b710cee284485d0eb19fe06085fb819"}]},{"sha":"08fa3725f276c6baffb47406e14f3471281084ec","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjA4ZmEzNzI1ZjI3NmM2YmFmZmI0NzQwNmUxNGYzNDcxMjgxMDg0ZWM=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"CC1B18C9-AE10-4579-8A14-AC533A7D4537","tree":{"sha":"c4785d960e280a993da10f12712f2232f36091d7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c4785d960e280a993da10f12712f2232f36091d7"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/08fa3725f276c6baffb47406e14f3471281084ec","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/08fa3725f276c6baffb47406e14f3471281084ec","html_url":"https://github.com/ThiagoCodecov/example-python/commit/08fa3725f276c6baffb47406e14f3471281084ec","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/08fa3725f276c6baffb47406e14f3471281084ec/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"ef52cd1cc91bfc79503c1a0811e7b3612a8a33b5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/ef52cd1cc91bfc79503c1a0811e7b3612a8a33b5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/ef52cd1cc91bfc79503c1a0811e7b3612a8a33b5"}]},{"sha":"b105efa2e8ad12955a0e90e13f085116848124c7","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmIxMDVlZmEyZThhZDEyOTU1YTBlOTBlMTNmMDg1MTE2ODQ4MTI0Yzc=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"8A7E0145-42A8-4020-85A4-43CEEE96D671","tree":{"sha":"785c94ed34da3bf78247f4415dba106ae5016dff","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/785c94ed34da3bf78247f4415dba106ae5016dff"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/b105efa2e8ad12955a0e90e13f085116848124c7","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b105efa2e8ad12955a0e90e13f085116848124c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b105efa2e8ad12955a0e90e13f085116848124c7","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b105efa2e8ad12955a0e90e13f085116848124c7/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"08fa3725f276c6baffb47406e14f3471281084ec","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/08fa3725f276c6baffb47406e14f3471281084ec","html_url":"https://github.com/ThiagoCodecov/example-python/commit/08fa3725f276c6baffb47406e14f3471281084ec"}]},{"sha":"9ab2fb038f64a6f55d9dfb7165faf52889bfc69e","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjlhYjJmYjAzOGY2NGE2ZjU1ZDlkZmI3MTY1ZmFmNTI4ODliZmM2OWU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"07DAE847-260A-44A2-9E5C-FA626C466176","tree":{"sha":"895eb4c4e88243bf35e6ba31f1504cd45ade60e6","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/895eb4c4e88243bf35e6ba31f1504cd45ade60e6"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/9ab2fb038f64a6f55d9dfb7165faf52889bfc69e","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9ab2fb038f64a6f55d9dfb7165faf52889bfc69e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9ab2fb038f64a6f55d9dfb7165faf52889bfc69e","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9ab2fb038f64a6f55d9dfb7165faf52889bfc69e/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"b105efa2e8ad12955a0e90e13f085116848124c7","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/b105efa2e8ad12955a0e90e13f085116848124c7","html_url":"https://github.com/ThiagoCodecov/example-python/commit/b105efa2e8ad12955a0e90e13f085116848124c7"}]},{"sha":"1e06fde8f0acc33395ce19db8b6af979645ab945","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjFlMDZmZGU4ZjBhY2MzMzM5NWNlMTlkYjhiNmFmOTc5NjQ1YWI5NDU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"4539B304-39F3-4CCD-A1BE-67A580ECF163","tree":{"sha":"6a8ef93a2e424d4287fc7163cb4df5ff4826b1d4","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/6a8ef93a2e424d4287fc7163cb4df5ff4826b1d4"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/1e06fde8f0acc33395ce19db8b6af979645ab945","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e06fde8f0acc33395ce19db8b6af979645ab945","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1e06fde8f0acc33395ce19db8b6af979645ab945","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e06fde8f0acc33395ce19db8b6af979645ab945/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"9ab2fb038f64a6f55d9dfb7165faf52889bfc69e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/9ab2fb038f64a6f55d9dfb7165faf52889bfc69e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/9ab2fb038f64a6f55d9dfb7165faf52889bfc69e"}]},{"sha":"a10761e41d8bb3b7c7aa8a22ccf69224bd74b029","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmExMDc2MWU0MWQ4YmIzYjdjN2FhOGEyMmNjZjY5MjI0YmQ3NGIwMjk=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"6A806881-179F-47A1-858A-1EEB6057FADC","tree":{"sha":"1071fd3ef2d1502ec80e9a72a802c722163ee975","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/1071fd3ef2d1502ec80e9a72a802c722163ee975"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/a10761e41d8bb3b7c7aa8a22ccf69224bd74b029","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a10761e41d8bb3b7c7aa8a22ccf69224bd74b029","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a10761e41d8bb3b7c7aa8a22ccf69224bd74b029","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a10761e41d8bb3b7c7aa8a22ccf69224bd74b029/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"1e06fde8f0acc33395ce19db8b6af979645ab945","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/1e06fde8f0acc33395ce19db8b6af979645ab945","html_url":"https://github.com/ThiagoCodecov/example-python/commit/1e06fde8f0acc33395ce19db8b6af979645ab945"}]},{"sha":"6ed4e02aa5323cf0528de4d9598e226e6296d26e","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjZlZDRlMDJhYTUzMjNjZjA1MjhkZTRkOTU5OGUyMjZlNjI5NmQyNmU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"7C19581F-AF78-41B0-A3CB-872BB3983C1D","tree":{"sha":"748328c2f900ac130d699619190df6751a9795f8","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/748328c2f900ac130d699619190df6751a9795f8"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/6ed4e02aa5323cf0528de4d9598e226e6296d26e","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ed4e02aa5323cf0528de4d9598e226e6296d26e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6ed4e02aa5323cf0528de4d9598e226e6296d26e","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ed4e02aa5323cf0528de4d9598e226e6296d26e/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"a10761e41d8bb3b7c7aa8a22ccf69224bd74b029","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/a10761e41d8bb3b7c7aa8a22ccf69224bd74b029","html_url":"https://github.com/ThiagoCodecov/example-python/commit/a10761e41d8bb3b7c7aa8a22ccf69224bd74b029"}]},{"sha":"65f436b617c274987fe2d42b199979c2836e5e4b","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjY1ZjQzNmI2MTdjMjc0OTg3ZmUyZDQyYjE5OTk3OWMyODM2ZTVlNGI=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"DC4F5C04-0E44-456C-8393-317B58503D4C","tree":{"sha":"a4409ae954f4ac650f0b2ca6122078b1051e1066","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/a4409ae954f4ac650f0b2ca6122078b1051e1066"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/65f436b617c274987fe2d42b199979c2836e5e4b","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/65f436b617c274987fe2d42b199979c2836e5e4b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/65f436b617c274987fe2d42b199979c2836e5e4b","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/65f436b617c274987fe2d42b199979c2836e5e4b/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"6ed4e02aa5323cf0528de4d9598e226e6296d26e","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/6ed4e02aa5323cf0528de4d9598e226e6296d26e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/6ed4e02aa5323cf0528de4d9598e226e6296d26e"}]},{"sha":"8d1ae011ed38c838a48cec6f931574e53d3303b5","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjhkMWFlMDExZWQzOGM4MzhhNDhjZWM2ZjkzMTU3NGU1M2QzMzAzYjU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"85A29350-3370-4C82-9DB2-735BDBE8180A","tree":{"sha":"53fd039717c56c9e09c1ad700241a889d90e94cf","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/53fd039717c56c9e09c1ad700241a889d90e94cf"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/8d1ae011ed38c838a48cec6f931574e53d3303b5","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d1ae011ed38c838a48cec6f931574e53d3303b5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8d1ae011ed38c838a48cec6f931574e53d3303b5","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d1ae011ed38c838a48cec6f931574e53d3303b5/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"65f436b617c274987fe2d42b199979c2836e5e4b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/65f436b617c274987fe2d42b199979c2836e5e4b","html_url":"https://github.com/ThiagoCodecov/example-python/commit/65f436b617c274987fe2d42b199979c2836e5e4b"}]},{"sha":"80b1afa36abe0b56ffe54a9d586eededfc5ee0cd","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjgwYjFhZmEzNmFiZTBiNTZmZmU1NGE5ZDU4NmVlZGVkZmM1ZWUwY2Q=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"A017DC71-C241-46C1-9FC3-D15CB664BCF8","tree":{"sha":"055400055cfcc7c4cf7439f3169897d7bb5e3a1b","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/055400055cfcc7c4cf7439f3169897d7bb5e3a1b"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/80b1afa36abe0b56ffe54a9d586eededfc5ee0cd","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/80b1afa36abe0b56ffe54a9d586eededfc5ee0cd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/80b1afa36abe0b56ffe54a9d586eededfc5ee0cd","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/80b1afa36abe0b56ffe54a9d586eededfc5ee0cd/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"8d1ae011ed38c838a48cec6f931574e53d3303b5","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/8d1ae011ed38c838a48cec6f931574e53d3303b5","html_url":"https://github.com/ThiagoCodecov/example-python/commit/8d1ae011ed38c838a48cec6f931574e53d3303b5"}]},{"sha":"f747ffb1dc704ac3e198f77b6be7e3c06330fc80","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OmY3NDdmZmIxZGM3MDRhYzNlMTk4Zjc3YjZiZTdlM2MwNjMzMGZjODA=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"CB2936ED-2074-483A-B62C-A1CA8DBC9B46","tree":{"sha":"c01004c85a97ff093077119f329bcb907524d990","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/c01004c85a97ff093077119f329bcb907524d990"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/f747ffb1dc704ac3e198f77b6be7e3c06330fc80","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f747ffb1dc704ac3e198f77b6be7e3c06330fc80","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f747ffb1dc704ac3e198f77b6be7e3c06330fc80","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f747ffb1dc704ac3e198f77b6be7e3c06330fc80/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"80b1afa36abe0b56ffe54a9d586eededfc5ee0cd","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/80b1afa36abe0b56ffe54a9d586eededfc5ee0cd","html_url":"https://github.com/ThiagoCodecov/example-python/commit/80b1afa36abe0b56ffe54a9d586eededfc5ee0cd"}]},{"sha":"5ca84ce84d2ae634e07bcdf4a62e5268c2f6f36e","node_id":"MDY6Q29tbWl0MTU2NjE3Nzc3OjVjYTg0Y2U4NGQyYWU2MzRlMDdiY2RmNGE2MmU1MjY4YzJmNmYzNmU=","commit":{"author":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"committer":{"name":"Thiago + Ramos","email":"thiago@codecov.io","date":"2019-12-12T00:30:01Z"},"message":"62D27150-DEF7-4A23-9C58-293FC5AE8E40","tree":{"sha":"e218d7b5c99b143be0d29aa25ea764804ece033d","url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/e218d7b5c99b143be0d29aa25ea764804ece033d"},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits/5ca84ce84d2ae634e07bcdf4a62e5268c2f6f36e","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/5ca84ce84d2ae634e07bcdf4a62e5268c2f6f36e","html_url":"https://github.com/ThiagoCodecov/example-python/commit/5ca84ce84d2ae634e07bcdf4a62e5268c2f6f36e","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/5ca84ce84d2ae634e07bcdf4a62e5268c2f6f36e/comments","author":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"committer":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"parents":[{"sha":"f747ffb1dc704ac3e198f77b6be7e3c06330fc80","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/f747ffb1dc704ac3e198f77b6be7e3c06330fc80","html_url":"https://github.com/ThiagoCodecov/example-python/commit/f747ffb1dc704ac3e198f77b6be7e3c06330fc80"}]}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 04 Sep 2023 17:16:17 GMT + ETag: + - W/"43fc1cf08b318a40f0045f9f74c722db34cffc19a3595c2acafaa47f487bf286" + Last-Modified: + - Fri, 01 Sep 2023 11:48:40 GMT + Link: + - ; + rel="prev", ; + rel="next", ; + rel="last", ; + rel="first" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - FF7B:52C9:122D00:12FF01:64F610E0 + X-OAuth-Scopes: + - repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4996' + X-RateLimit-Reset: + - '1693851376' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '4' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-11 17:05:55 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_requests.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_requests.yaml new file mode 100644 index 0000000000..8b3d2b58af --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_requests.yaml @@ -0,0 +1,81 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/pulls?page=1&per_page=25&state=open + response: + content: '[{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18","id":383348775,"node_id":"MDExOlB1bGxSZXF1ZXN0MzgzMzQ4Nzc1","html_url":"https://github.com/ThiagoCodecov/example-python/pull/18","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/18.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/18.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/18","number":18,"state":"open","locked":false,"title":"Thiago/base + no base","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"","created_at":"2020-03-04T05:32:53Z","updated_at":"2020-10-14T13:14:46Z","closed_at":null,"merged_at":null,"merge_commit_sha":"9137d3a246fb2f0fa0f5801d1625cb60a2cf6363","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/18/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/75f355d8d14ba3d7761c728b4d2607cde0eef065","head":{"label":"ThiagoCodecov:thiago/base-no-base","ref":"thiago/base-no-base","sha":"75f355d8d14ba3d7761c728b4d2607cde0eef065","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-03-24T22:01:40Z","pushed_at":"2020-10-13T15:15:47Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":174,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":null,"forks":0,"open_issues":2,"watchers":0,"default_branch":"main"}},"base":{"label":"ThiagoCodecov:main","ref":"main","sha":"f0895290dc26668faeeb20ee5ccd4cc995925775","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-03-24T22:01:40Z","pushed_at":"2020-10-13T15:15:47Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":174,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":null,"forks":0,"open_issues":2,"watchers":0,"default_branch":"main"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/18"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/18"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/18/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/75f355d8d14ba3d7761c728b4d2607cde0eef065"}},"author_association":"OWNER","active_lock_reason":null},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/16","id":352177461,"node_id":"MDExOlB1bGxSZXF1ZXN0MzUyMTc3NDYx","html_url":"https://github.com/ThiagoCodecov/example-python/pull/16","diff_url":"https://github.com/ThiagoCodecov/example-python/pull/16.diff","patch_url":"https://github.com/ThiagoCodecov/example-python/pull/16.patch","issue_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/16","number":16,"state":"open","locked":false,"title":"PR + with more than 250 results","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"body":"","created_at":"2019-12-12T00:31:54Z","updated_at":"2019-12-12T00:33:00Z","closed_at":null,"merged_at":null,"merge_commit_sha":"da802f783cf54cd0682e0b10341679130759f842","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/16/commits","review_comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/16/comments","review_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/16/comments","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/d55dc4ef748fd11537e50c9abed4ab1864fa1d94","head":{"label":"ThiagoCodecov:thiago/f/big-pt","ref":"thiago/f/big-pt","sha":"d55dc4ef748fd11537e50c9abed4ab1864fa1d94","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-03-24T22:01:40Z","pushed_at":"2020-10-13T15:15:47Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":174,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":null,"forks":0,"open_issues":2,"watchers":0,"default_branch":"main"}},"base":{"label":"ThiagoCodecov:main","ref":"main","sha":"d723f5cb5c9c9f48c47f2df97c47de20457d3fdc","user":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"repo":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-03-24T22:01:40Z","pushed_at":"2020-10-13T15:15:47Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":174,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":null,"forks":0,"open_issues":2,"watchers":0,"default_branch":"main"}},"_links":{"self":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/16"},"html":{"href":"https://github.com/ThiagoCodecov/example-python/pull/16"},"issue":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/16"},"comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/16/comments"},"review_comments":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/16/comments"},"review_comment":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/16/commits"},"statuses":{"href":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/d55dc4ef748fd11537e50c9abed4ab1864fa1d94"}},"author_association":"OWNER","active_lock_reason":null}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:12 GMT + ETag: + - W/"1bb56d59c6151bc00871b8fe08880aebc6b80f31a5eeccd8fae4fdb7da7d8d7d" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFDB:3B1F:3D739AE:6464680:5F87361C + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4987' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '13' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_requests_files.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_requests_files.yaml new file mode 100644 index 0000000000..35a640800b --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_pull_requests_files.yaml @@ -0,0 +1,84 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecove2e/example-python/pulls/4/files + response: + content: '[{"sha":"898991ad883e00916ed4ced91b534734b211c7ba","filename":"awesome/__init__.py","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/codecove2e/example-python/blob/0206296b1424912cc05069a9bf4025cbb95f5ecc/awesome%2F__init__.py","raw_url":"https://github.com/codecove2e/example-python/raw/0206296b1424912cc05069a9bf4025cbb95f5ecc/awesome%2F__init__.py","contents_url":"https://api.github.com/repos/codecove2e/example-python/contents/awesome%2F__init__.py?ref=0206296b1424912cc05069a9bf4025cbb95f5ecc","patch":"@@ + -3,7 +3,7 @@ def smile():\n \n \n def frown():\n- return \":(\"\n+ return + \"==(\"\n \n \n def shieeee(g):"}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 12 Apr 2023 12:09:47 GMT + ETag: + - W/"2622ac23d8520a43925683b13198cd0cf1087dd1a6c4374d14965f485fa07194" + Last-Modified: + - Tue, 04 Apr 2023 19:15:16 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - F236:DD94:1438888:14648C2:64369F8B + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1681304987' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-05-12 12:07:21 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_repo_languages.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_repo_languages.yaml new file mode 100644 index 0000000000..b662b94ba1 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_repo_languages.yaml @@ -0,0 +1,80 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecove2e/example-python/languages + response: + content: '{"Shell":98477,"Makefile":1308,"Python":1129}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 12 Jan 2024 16:33:48 GMT + ETag: + - W/"c755ddad2b5748d9a039112451e484e0b2e86901ee2a4a8d1ec610b12d60d8bc" + Last-Modified: + - Fri, 04 Dec 2020 04:21:29 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - 1F31:8AB9:407962F:855D740:65A169EB + X-OAuth-Scopes: + - admin:enterprise, admin:gpg_key, admin:org + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4997' + X-RateLimit-Reset: + - '1705079671' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '3' + X-XSS-Protection: + - '0' + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_repo_no_languages.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_repo_no_languages.yaml new file mode 100644 index 0000000000..1c03374f6b --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_repo_no_languages.yaml @@ -0,0 +1,78 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecove2e/test-no-languages/languages + response: + content: '{}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '2' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 12 Jan 2024 18:28:10 GMT + ETag: + - '"c3d1c6c45a5681d35f5176c6ff5bbeba97ef873b5d36abfac80fa181a4acfba3"' + Last-Modified: + - Fri, 12 Jan 2024 18:24:02 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - 1FD4:441A:6684DD0:D45FF80:65A184B9 + X-OAuth-Scopes: + - admin:enterprise, admin:gpg_key, admin:org, admin:org_hook + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1705087690' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_repo_with_languages_graphql.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_repo_with_languages_graphql.yaml new file mode 100644 index 0000000000..f961a7f417 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_repo_with_languages_graphql.yaml @@ -0,0 +1,81 @@ +interactions: +- request: + body: '{"query": "\nquery Repos($owner: String!, $cursor: String, $first: Int!) + {\n repositoryOwner(login: $owner) {\n repositories(\n first: $first\n ownerAffiliations: + OWNER\n isFork: false\n isLocked: false\n orderBy: {field: NAME, + direction: ASC}\n after: $cursor\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes + {\n name\n languages(first: 100) {\n edges {\n node + {\n name\n id\n }\n }\n }\n }\n }\n }\n}\n", + "variables": {"owner": "adrian-codecov", "cursor": null, "first": 100}}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '648' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/graphql + response: + content: '{"data":{"repositoryOwner":{"repositories":{"pageInfo":{"hasNextPage":false,"endCursor":"Y3Vyc29yOnYyOpKxdGVzdC1uby1sYW5ndWFnZXPOLEIluA=="},"nodes":[{"name":"another-test","languages":{"edges":[{"node":{"name":"JavaScript","id":"MDg6TGFuZ3VhZ2UxNDA="}},{"node":{"name":"HTML","id":"MDg6TGFuZ3VhZ2U0MTc="}},{"node":{"name":"CSS","id":"MDg6TGFuZ3VhZ2UzMDg="}}]}},{"name":"new-test-repo","languages":{"edges":[{"node":{"name":"HTML","id":"MDg6TGFuZ3VhZ2U0MTc="}},{"node":{"name":"CSS","id":"MDg6TGFuZ3VhZ2UzMDg="}},{"node":{"name":"JavaScript","id":"MDg6TGFuZ3VhZ2UxNDA="}}]}},{"name":"test-no-languages","languages":{"edges":[]}}]}}}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 08 Mar 2024 00:59:29 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v4 + X-GitHub-Request-Id: + - DC4E:4ECA:91EE42:115CF24:65EA62F1 + X-OAuth-Scopes: + - admin:enterprise, admin:gpg_key, admin:org + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4472' + X-RateLimit-Reset: + - '1709860075' + X-RateLimit-Resource: + - graphql + X-RateLimit-Used: + - '528' + X-XSS-Protection: + - '0' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_repos_from_nodeids_generator.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_repos_from_nodeids_generator.yaml new file mode 100644 index 0000000000..5789306e7a --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_repos_from_nodeids_generator.yaml @@ -0,0 +1,81 @@ +interactions: +- request: + body: '{"query": "\nquery GetReposFromNodeIds($node_ids: [ID!]!) {\n nodes(ids: + $node_ids) {\n __typename \n ... on Repository {\n # + databaseId == service_id\n databaseId\n name\n primaryLanguage + {\n name\n }\n isPrivate\n defaultBranchRef + {\n name\n }\n owner {\n # + This ID is actually the node_id, not the ownerid\n id\n login\n }\n }\n }\n}\n", + "variables": {"node_ids": ["R_kgDOHrbKcg", "R_kgDOLEJx2g"]}}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '613' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/graphql + response: + content: '{"data":{"nodes":[{"__typename":"Repository","databaseId":515295858,"name":"example-python","primaryLanguage":{"name":"Shell"},"isPrivate":false,"defaultBranchRef":{"name":"main"},"owner":{"id":"U_kgDOBZOfKw","login":"codecove2e"}},{"__typename":"Repository","databaseId":742552026,"name":"test-no-languages","primaryLanguage":null,"isPrivate":false,"defaultBranchRef":null,"owner":{"id":"U_kgDOBZOfKw","login":"codecove2e"}}]}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 06 Feb 2024 13:21:07 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v4 + X-GitHub-Request-Id: + - C11E:116D76:8B8D4:94D71:65C23242 + X-OAuth-Scopes: + - repo + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4997' + X-RateLimit-Reset: + - '1707227531' + X-RateLimit-Resource: + - graphql + X-RateLimit-Used: + - '3' + X-XSS-Protection: + - '0' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_repository.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_repository.yaml new file mode 100644 index 0000000000..20af1f7c6b --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_repository.yaml @@ -0,0 +1,80 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python + response: + content: '{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-03-24T22:01:40Z","pushed_at":"2020-10-13T15:15:47Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":174,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":null,"forks":0,"open_issues":2,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true},"temp_clone_token":"","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"delete_branch_on_merge":false,"parent":{"id":24344106,"node_id":"MDEwOlJlcG9zaXRvcnkyNDM0NDEwNg==","name":"example-python","full_name":"codecov/example-python","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-python","description":"Python + coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-python","forks_url":"https://api.github.com/repos/codecov/example-python/forks","keys_url":"https://api.github.com/repos/codecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-python/teams","hooks_url":"https://api.github.com/repos/codecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-python/events","assignees_url":"https://api.github.com/repos/codecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-python/tags","blobs_url":"https://api.github.com/repos/codecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-python/languages","stargazers_url":"https://api.github.com/repos/codecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-python/subscription","commits_url":"https://api.github.com/repos/codecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-python/merges","archive_url":"https://api.github.com/repos/codecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-python/downloads","issues_url":"https://api.github.com/repos/codecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-python/deployments","created_at":"2014-09-22T20:20:06Z","updated_at":"2020-09-27T16:45:48Z","pushed_at":"2020-10-13T23:33:12Z","git_url":"git://github.com/codecov/example-python.git","ssh_url":"git@github.com:codecov/example-python.git","clone_url":"https://github.com/codecov/example-python.git","svn_url":"https://github.com/codecov/example-python","homepage":"https://codecov.io","size":83,"stargazers_count":226,"watchers_count":226,"language":"Python","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":189,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":5,"license":null,"forks":189,"open_issues":5,"watchers":226,"default_branch":"main"},"source":{"id":24344106,"node_id":"MDEwOlJlcG9zaXRvcnkyNDM0NDEwNg==","name":"example-python","full_name":"codecov/example-python","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-python","description":"Python + coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-python","forks_url":"https://api.github.com/repos/codecov/example-python/forks","keys_url":"https://api.github.com/repos/codecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-python/teams","hooks_url":"https://api.github.com/repos/codecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-python/events","assignees_url":"https://api.github.com/repos/codecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-python/tags","blobs_url":"https://api.github.com/repos/codecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-python/languages","stargazers_url":"https://api.github.com/repos/codecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-python/subscription","commits_url":"https://api.github.com/repos/codecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-python/merges","archive_url":"https://api.github.com/repos/codecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-python/downloads","issues_url":"https://api.github.com/repos/codecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-python/deployments","created_at":"2014-09-22T20:20:06Z","updated_at":"2020-09-27T16:45:48Z","pushed_at":"2020-10-13T23:33:12Z","git_url":"git://github.com/codecov/example-python.git","ssh_url":"git@github.com:codecov/example-python.git","clone_url":"https://github.com/codecov/example-python.git","svn_url":"https://github.com/codecov/example-python","homepage":"https://codecov.io","size":83,"stargazers_count":226,"watchers_count":226,"language":"Python","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":189,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":5,"license":null,"forks":189,"open_issues":5,"watchers":226,"default_branch":"main"},"network_count":189,"subscribers_count":0}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:33 GMT + ETag: + - W/"10336721274b17a77895a1bc924223c0cea0145d13cc9eaf2595f17c2de170a8" + Last-Modified: + - Tue, 24 Mar 2020 22:01:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFF8:68C5:845A49:16C30B8:5F873631 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4931' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '69' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_source_master.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_source_master.yaml new file mode 100644 index 0000000000..4f0159b276 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_source_master.yaml @@ -0,0 +1,77 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/code_fib.py?ref=master + response: + content: '{"name":"code_fib.py","path":"awesome/code_fib.py","sha":"7fb3c3fbd71a6d3f4b98964c0130f7e083505fcd","size":156,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/code_fib.py?ref=master","html_url":"https://github.com/ThiagoCodecov/example-python/blob/main/awesome/code_fib.py","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/7fb3c3fbd71a6d3f4b98964c0130f7e083505fcd","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/main/awesome/code_fib.py","type":"file","content":"ZGVmIGZpYihuKToKICAgIGlmIG4gPCAwOgogICAgICAgIHJldHVybiAwCiAg\nICBpZiBuIDw9IDE6CiAgICAgICAgcmV0dXJuIDEKICAgIHJldHVybiBmaWIo\nbiAtIDEpICsgZmliKG4gLSAyKQoKCmRlZiB1bnRlc3RlZF9jb2RlKGEpOgog\nICAgcmFpc2UgRXhjZXB0aW9uKCkK\n","encoding":"base64","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/code_fib.py?ref=master","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/7fb3c3fbd71a6d3f4b98964c0130f7e083505fcd","html":"https://github.com/ThiagoCodecov/example-python/blob/main/awesome/code_fib.py"}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 21:56:45 GMT + ETag: + - W/"7fb3c3fbd71a6d3f4b98964c0130f7e083505fcd" + Last-Modified: + - Tue, 24 Mar 2020 22:01:33 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D8D7:7BF2:5EAA4:C98A8:5F87741D + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4979' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '21' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_source_random_commit.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_source_random_commit.yaml new file mode 100644 index 0000000000..a5af925aef --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_source_random_commit.yaml @@ -0,0 +1,77 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/__init__.py?ref=96492d409fc86aa7ae31b214dfe6b08ae860458a + response: + content: '{"name":"__init__.py","path":"awesome/__init__.py","sha":"4d34acc61e7abe5536c84fec4fe9fd9b26311cc7","size":59,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/__init__.py?ref=96492d409fc86aa7ae31b214dfe6b08ae860458a","html_url":"https://github.com/ThiagoCodecov/example-python/blob/96492d409fc86aa7ae31b214dfe6b08ae860458a/awesome/__init__.py","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/4d34acc61e7abe5536c84fec4fe9fd9b26311cc7","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/96492d409fc86aa7ae31b214dfe6b08ae860458a/awesome/__init__.py","type":"file","content":"ZGVmIHNtaWxlKCk6CiAgICByZXR1cm4gIjopIgoKZGVmIGZyb3duKCk6CiAg\nICByZXR1cm4gIjooIgo=\n","encoding":"base64","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/__init__.py?ref=96492d409fc86aa7ae31b214dfe6b08ae860458a","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/4d34acc61e7abe5536c84fec4fe9fd9b26311cc7","html":"https://github.com/ThiagoCodecov/example-python/blob/96492d409fc86aa7ae31b214dfe6b08ae860458a/awesome/__init__.py"}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:35 GMT + ETag: + - W/"4d34acc61e7abe5536c84fec4fe9fd9b26311cc7" + Last-Modified: + - Mon, 22 Sep 2014 20:24:04 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFA:68FA:6DEAA4:122F1B3:5F873632 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4929' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '71' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_source_random_commit_not_found.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_source_random_commit_not_found.yaml new file mode 100644 index 0000000000..f3ca6d682f --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_source_random_commit_not_found.yaml @@ -0,0 +1,70 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/non_exising_file.py?ref=96492d409fc86aa7ae31b214dfe6b08ae860458a + response: + content: '{"message":"Not Found","documentation_url":"https://docs.github.com/rest/reference/repos#get-repository-content"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:35 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 404 Not Found + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFB:4112:10B3774:28F9ED9:5F873633 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4928' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '72' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_workflow_run.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_workflow_run.yaml new file mode 100644 index 0000000000..98b9edb818 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_get_workflow_run.yaml @@ -0,0 +1,84 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/django/django/actions/runs/3265999402 + response: + content: '{"id":3265999402,"name":"Docs","node_id":"WFR_kwLOAD-Lgs7CqzIq","head_branch":"refs_34099_ticket","head_sha":"384dba7ce472c0f22c33f2bcede8f8d04b9c2b0f","path":".github/workflows/docs.yml","display_title":"Refs + #34099 -- Documented a recommendation to add any fields set in the save method + to the update_fields kwarg.","run_number":3981,"event":"pull_request","status":"completed","conclusion":"success","workflow_id":6146682,"check_suite_id":8812361904,"check_suite_node_id":"CS_kwDOAD-Lgs8AAAACDUH4sA","url":"https://api.github.com/repos/django/django/actions/runs/3265999402","html_url":"https://github.com/django/django/actions/runs/3265999402","pull_requests":[],"created_at":"2022-10-17T14:29:14Z","updated_at":"2022-10-17T14:31:13Z","actor":{"login":"sarahboyce","id":42296566,"node_id":"MDQ6VXNlcjQyMjk2NTY2","avatar_url":"https://avatars.githubusercontent.com/u/42296566?v=4","gravatar_id":"","url":"https://api.github.com/users/sarahboyce","html_url":"https://github.com/sarahboyce","followers_url":"https://api.github.com/users/sarahboyce/followers","following_url":"https://api.github.com/users/sarahboyce/following{/other_user}","gists_url":"https://api.github.com/users/sarahboyce/gists{/gist_id}","starred_url":"https://api.github.com/users/sarahboyce/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sarahboyce/subscriptions","organizations_url":"https://api.github.com/users/sarahboyce/orgs","repos_url":"https://api.github.com/users/sarahboyce/repos","events_url":"https://api.github.com/users/sarahboyce/events{/privacy}","received_events_url":"https://api.github.com/users/sarahboyce/received_events","type":"User","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2022-10-17T14:29:14Z","triggering_actor":{"login":"sarahboyce","id":42296566,"node_id":"MDQ6VXNlcjQyMjk2NTY2","avatar_url":"https://avatars.githubusercontent.com/u/42296566?v=4","gravatar_id":"","url":"https://api.github.com/users/sarahboyce","html_url":"https://github.com/sarahboyce","followers_url":"https://api.github.com/users/sarahboyce/followers","following_url":"https://api.github.com/users/sarahboyce/following{/other_user}","gists_url":"https://api.github.com/users/sarahboyce/gists{/gist_id}","starred_url":"https://api.github.com/users/sarahboyce/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sarahboyce/subscriptions","organizations_url":"https://api.github.com/users/sarahboyce/orgs","repos_url":"https://api.github.com/users/sarahboyce/repos","events_url":"https://api.github.com/users/sarahboyce/events{/privacy}","received_events_url":"https://api.github.com/users/sarahboyce/received_events","type":"User","site_admin":false},"jobs_url":"https://api.github.com/repos/django/django/actions/runs/3265999402/jobs","logs_url":"https://api.github.com/repos/django/django/actions/runs/3265999402/logs","check_suite_url":"https://api.github.com/repos/django/django/check-suites/8812361904","artifacts_url":"https://api.github.com/repos/django/django/actions/runs/3265999402/artifacts","cancel_url":"https://api.github.com/repos/django/django/actions/runs/3265999402/cancel","rerun_url":"https://api.github.com/repos/django/django/actions/runs/3265999402/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/django/django/actions/workflows/6146682","head_commit":{"id":"384dba7ce472c0f22c33f2bcede8f8d04b9c2b0f","tree_id":"de5dcf3076ab1c1f7d7aed399d63a6f08be4e1ff","message":"Refs + #34099 -- Documented a recommendation to add any fields set in the save method + to the update_fields kwarg.","timestamp":"2022-10-17T14:28:55Z","author":{"name":"sarahboyce","email":"sarahvboyce95@gmail.com"},"committer":{"name":"sarahboyce","email":"sarahvboyce95@gmail.com"}},"repository":{"id":4164482,"node_id":"MDEwOlJlcG9zaXRvcnk0MTY0NDgy","name":"django","full_name":"django/django","private":false,"owner":{"login":"django","id":27804,"node_id":"MDEyOk9yZ2FuaXphdGlvbjI3ODA0","avatar_url":"https://avatars.githubusercontent.com/u/27804?v=4","gravatar_id":"","url":"https://api.github.com/users/django","html_url":"https://github.com/django","followers_url":"https://api.github.com/users/django/followers","following_url":"https://api.github.com/users/django/following{/other_user}","gists_url":"https://api.github.com/users/django/gists{/gist_id}","starred_url":"https://api.github.com/users/django/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/django/subscriptions","organizations_url":"https://api.github.com/users/django/orgs","repos_url":"https://api.github.com/users/django/repos","events_url":"https://api.github.com/users/django/events{/privacy}","received_events_url":"https://api.github.com/users/django/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/django/django","description":"The + Web framework for perfectionists with deadlines.","fork":false,"url":"https://api.github.com/repos/django/django","forks_url":"https://api.github.com/repos/django/django/forks","keys_url":"https://api.github.com/repos/django/django/keys{/key_id}","collaborators_url":"https://api.github.com/repos/django/django/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/django/django/teams","hooks_url":"https://api.github.com/repos/django/django/hooks","issue_events_url":"https://api.github.com/repos/django/django/issues/events{/number}","events_url":"https://api.github.com/repos/django/django/events","assignees_url":"https://api.github.com/repos/django/django/assignees{/user}","branches_url":"https://api.github.com/repos/django/django/branches{/branch}","tags_url":"https://api.github.com/repos/django/django/tags","blobs_url":"https://api.github.com/repos/django/django/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/django/django/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/django/django/git/refs{/sha}","trees_url":"https://api.github.com/repos/django/django/git/trees{/sha}","statuses_url":"https://api.github.com/repos/django/django/statuses/{sha}","languages_url":"https://api.github.com/repos/django/django/languages","stargazers_url":"https://api.github.com/repos/django/django/stargazers","contributors_url":"https://api.github.com/repos/django/django/contributors","subscribers_url":"https://api.github.com/repos/django/django/subscribers","subscription_url":"https://api.github.com/repos/django/django/subscription","commits_url":"https://api.github.com/repos/django/django/commits{/sha}","git_commits_url":"https://api.github.com/repos/django/django/git/commits{/sha}","comments_url":"https://api.github.com/repos/django/django/comments{/number}","issue_comment_url":"https://api.github.com/repos/django/django/issues/comments{/number}","contents_url":"https://api.github.com/repos/django/django/contents/{+path}","compare_url":"https://api.github.com/repos/django/django/compare/{base}...{head}","merges_url":"https://api.github.com/repos/django/django/merges","archive_url":"https://api.github.com/repos/django/django/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/django/django/downloads","issues_url":"https://api.github.com/repos/django/django/issues{/number}","pulls_url":"https://api.github.com/repos/django/django/pulls{/number}","milestones_url":"https://api.github.com/repos/django/django/milestones{/number}","notifications_url":"https://api.github.com/repos/django/django/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/django/django/labels{/name}","releases_url":"https://api.github.com/repos/django/django/releases{/id}","deployments_url":"https://api.github.com/repos/django/django/deployments"},"head_repository":{"id":380729936,"node_id":"MDEwOlJlcG9zaXRvcnkzODA3Mjk5MzY=","name":"django","full_name":"sarahboyce/django","private":false,"owner":{"login":"sarahboyce","id":42296566,"node_id":"MDQ6VXNlcjQyMjk2NTY2","avatar_url":"https://avatars.githubusercontent.com/u/42296566?v=4","gravatar_id":"","url":"https://api.github.com/users/sarahboyce","html_url":"https://github.com/sarahboyce","followers_url":"https://api.github.com/users/sarahboyce/followers","following_url":"https://api.github.com/users/sarahboyce/following{/other_user}","gists_url":"https://api.github.com/users/sarahboyce/gists{/gist_id}","starred_url":"https://api.github.com/users/sarahboyce/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sarahboyce/subscriptions","organizations_url":"https://api.github.com/users/sarahboyce/orgs","repos_url":"https://api.github.com/users/sarahboyce/repos","events_url":"https://api.github.com/users/sarahboyce/events{/privacy}","received_events_url":"https://api.github.com/users/sarahboyce/received_events","type":"User","site_admin":false},"html_url":"https://github.com/sarahboyce/django","description":"The + Web framework for perfectionists with deadlines.","fork":true,"url":"https://api.github.com/repos/sarahboyce/django","forks_url":"https://api.github.com/repos/sarahboyce/django/forks","keys_url":"https://api.github.com/repos/sarahboyce/django/keys{/key_id}","collaborators_url":"https://api.github.com/repos/sarahboyce/django/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/sarahboyce/django/teams","hooks_url":"https://api.github.com/repos/sarahboyce/django/hooks","issue_events_url":"https://api.github.com/repos/sarahboyce/django/issues/events{/number}","events_url":"https://api.github.com/repos/sarahboyce/django/events","assignees_url":"https://api.github.com/repos/sarahboyce/django/assignees{/user}","branches_url":"https://api.github.com/repos/sarahboyce/django/branches{/branch}","tags_url":"https://api.github.com/repos/sarahboyce/django/tags","blobs_url":"https://api.github.com/repos/sarahboyce/django/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/sarahboyce/django/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/sarahboyce/django/git/refs{/sha}","trees_url":"https://api.github.com/repos/sarahboyce/django/git/trees{/sha}","statuses_url":"https://api.github.com/repos/sarahboyce/django/statuses/{sha}","languages_url":"https://api.github.com/repos/sarahboyce/django/languages","stargazers_url":"https://api.github.com/repos/sarahboyce/django/stargazers","contributors_url":"https://api.github.com/repos/sarahboyce/django/contributors","subscribers_url":"https://api.github.com/repos/sarahboyce/django/subscribers","subscription_url":"https://api.github.com/repos/sarahboyce/django/subscription","commits_url":"https://api.github.com/repos/sarahboyce/django/commits{/sha}","git_commits_url":"https://api.github.com/repos/sarahboyce/django/git/commits{/sha}","comments_url":"https://api.github.com/repos/sarahboyce/django/comments{/number}","issue_comment_url":"https://api.github.com/repos/sarahboyce/django/issues/comments{/number}","contents_url":"https://api.github.com/repos/sarahboyce/django/contents/{+path}","compare_url":"https://api.github.com/repos/sarahboyce/django/compare/{base}...{head}","merges_url":"https://api.github.com/repos/sarahboyce/django/merges","archive_url":"https://api.github.com/repos/sarahboyce/django/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/sarahboyce/django/downloads","issues_url":"https://api.github.com/repos/sarahboyce/django/issues{/number}","pulls_url":"https://api.github.com/repos/sarahboyce/django/pulls{/number}","milestones_url":"https://api.github.com/repos/sarahboyce/django/milestones{/number}","notifications_url":"https://api.github.com/repos/sarahboyce/django/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/sarahboyce/django/labels{/name}","releases_url":"https://api.github.com/repos/sarahboyce/django/releases{/id}","deployments_url":"https://api.github.com/repos/sarahboyce/django/deployments"}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 19 Oct 2022 02:15:51 GMT + ETag: + - W/"55a9ec2a17199f50f5ca2bee9c50b17d66539c2849ebc7c6e847ceb4ff87ff02" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D44B:5C2A:1289BD2:13F41A4:634F5DD6 + X-OAuth-Scopes: + - admin:enterprise, admin:gpg_key, admin:org, admin:org_hook, admin:public_key, + admin:repo_hook, delete:packages, delete_repo, gist, notifications, repo, + user, workflow, write:discussion, write:packages + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4995' + X-RateLimit-Reset: + - '1666148190' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '5' + X-XSS-Protection: + - '0' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_is_student_github_education_503.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_is_student_github_education_503.yaml new file mode 100644 index 0000000000..c45e07a71f --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_is_student_github_education_503.yaml @@ -0,0 +1,77 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - education.github.com + user-agent: + - Default + method: GET + uri: https://education.github.com/api/user + response: + content: "\n\n \n \n \n \nGitHub Enterprise is currently + down for maintenance\n\n\n\n + \
    \n brb\n
    \n

    Down for maintenance

    \n

    \n GitHub + Education is performing a scheduled maintenance.\n If you have any questions, + please reach out to GitHub Support\n + \

    \n\n\n" + headers: + Content-Length: + - '702032' + Content-Type: + - text/html + Date: + - Mon, 13 Dec 2021 22:58:03 GMT + ETag: + - '"61b7c389-ab650"' + Server: + - nginx/1.15.12 + X-GitHub-Request-Id: + - D95E:62C6:31943:1873F0:61B7CFFB + x-github-backend: + - Kubernetes + http_version: HTTP/1.1 + status_code: 503 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_is_student_not_capable_app.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_is_student_not_capable_app.yaml new file mode 100644 index 0000000000..0584045b66 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_is_student_not_capable_app.yaml @@ -0,0 +1,143 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - education.github.com + user-agent: + - Default + method: GET + uri: https://education.github.com/api/user + response: + content: '' + headers: + Cache-Control: + - no-cache + Content-Security-Policy: + - 'default-src ''self'' dwa5x7aod66zk.cloudfront.net; object-src ''self''; frame-src + *.vimeo.com platform.twitter.com connect.facebook.net cdn.usefathom.com *.facebook.com + speakerdeck.com *.youtube.com *.youtube-nocookie.com embed.twitch.tv player.twitch.tv; + connect-src ''self'' github-education-web.s3.amazonaws.com *.clearbit.com + www.google.com *.githubusercontent.com geoip-js.com *.virtualearth.net *.bing.com + *.virtualearth.net api.github.com atlas.microsoft.com; img-src ''self'' blob: + dwa5x7aod66zk.cloudfront.net data: github.com blobaccountproduction.blob.core.windows.net + cdn.usefathom.com *.clearbit.com avatars.githubusercontent.com user-images.githubusercontent.com + github-education-web.s3.amazonaws.com analytics.twitter.com t.co t.com *.facebook.com + *.twitter.com *.youtube.com raw.githubusercontent.com static-cdn.jtvnw.net + dl.airtable.com *.bing.com *.virtualearth.net atlas.microsoft.com; font-src + ''self'' dwa5x7aod66zk.cloudfront.net atlas.microsoft.com; script-src ''self'' + dwa5x7aod66zk.cloudfront.net www.google.com cdn.usefathom.com api.demandbase.com + speakerdeck.com platform.twitter.com connect.facebook.net *.facebook.com s3-eu-west-1.amazonaws.com/share.typeform.com/* + *.github.com *.githubassets.com js.maxmind.com static.ads-twitter.com snap.licdn.com + geoip-js.com embed.twitch.tv analytics.twitter.com *.bing.com *.virtualearth.net + *.jquery.com *.cloudflare.com *.bootstrapcdn.com unpkg.com ''nonce-9d92aIH0h3eHkrivYZlcWg==''; + style-src ''self'' dwa5x7aod66zk.cloudfront.net ''unsafe-inline'' www.google.com + ajax.googleapis.com *.github.com *.bing.com *.virtualearth.net unpkg.com' + Content-Type: + - application/json + Date: + - Wed, 16 Aug 2023 17:44:54 GMT + Referrer-Policy: + - strict-origin-when-cross-origin + Server: + - nginx/1.25.1 + Set-Cookie: + - _octo=GH1.1.373487652.1692207893; domain=.github.com; path=/; expires=Fri, + 16 Aug 2024 17:44:53 GMT + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-GitHub-Request-Id: + - E02C:4EEA:7A50D:105E79:64DD0B15 + X-Runtime: + - '0.167302' + X-XSS-Protection: + - 1; mode=block + x-download-options: + - noopen + x-github-backend: + - Kubernetes + x-permitted-cross-domain-policies: + - none + http_version: HTTP/1.1 + status_code: 401 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + cookie: + - _octo=GH1.1.373487652.1692207893 + host: + - education.github.com + user-agent: + - Default + method: GET + uri: https://education.github.com/api/user + response: + content: '' + headers: + Cache-Control: + - no-cache + Content-Security-Policy: + - 'default-src ''self'' dwa5x7aod66zk.cloudfront.net; object-src ''self''; frame-src + *.vimeo.com platform.twitter.com connect.facebook.net cdn.usefathom.com *.facebook.com + speakerdeck.com *.youtube.com *.youtube-nocookie.com embed.twitch.tv player.twitch.tv; + connect-src ''self'' github-education-web.s3.amazonaws.com *.clearbit.com + www.google.com *.githubusercontent.com geoip-js.com *.virtualearth.net *.bing.com + *.virtualearth.net api.github.com atlas.microsoft.com; img-src ''self'' blob: + dwa5x7aod66zk.cloudfront.net data: github.com blobaccountproduction.blob.core.windows.net + cdn.usefathom.com *.clearbit.com avatars.githubusercontent.com user-images.githubusercontent.com + github-education-web.s3.amazonaws.com analytics.twitter.com t.co t.com *.facebook.com + *.twitter.com *.youtube.com raw.githubusercontent.com static-cdn.jtvnw.net + dl.airtable.com *.bing.com *.virtualearth.net atlas.microsoft.com; font-src + ''self'' dwa5x7aod66zk.cloudfront.net atlas.microsoft.com; script-src ''self'' + dwa5x7aod66zk.cloudfront.net www.google.com cdn.usefathom.com api.demandbase.com + speakerdeck.com platform.twitter.com connect.facebook.net *.facebook.com s3-eu-west-1.amazonaws.com/share.typeform.com/* + *.github.com *.githubassets.com js.maxmind.com static.ads-twitter.com snap.licdn.com + geoip-js.com embed.twitch.tv analytics.twitter.com *.bing.com *.virtualearth.net + *.jquery.com *.cloudflare.com *.bootstrapcdn.com unpkg.com ''nonce-ntysTM3E0mxWir4S9dFfuw==''; + style-src ''self'' dwa5x7aod66zk.cloudfront.net ''unsafe-inline'' www.google.com + ajax.googleapis.com *.github.com *.bing.com *.virtualearth.net unpkg.com' + Content-Type: + - application/json + Date: + - Wed, 16 Aug 2023 17:44:54 GMT + Referrer-Policy: + - strict-origin-when-cross-origin + Server: + - nginx/1.25.1 + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-GitHub-Request-Id: + - E02C:4EEA:7A526:105EAC:64DD0B16 + X-Runtime: + - '0.089019' + X-XSS-Protection: + - 1; mode=block + x-download-options: + - noopen + x-github-backend: + - Kubernetes + x-permitted-cross-domain-policies: + - none + http_version: HTTP/1.1 + status_code: 401 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_is_student_not_student.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_is_student_not_student.yaml new file mode 100644 index 0000000000..4585204f0a --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_is_student_not_student.yaml @@ -0,0 +1,71 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - education.github.com + user-agent: + - Default + method: GET + uri: https://education.github.com/api/user + response: + content: '{"student":false}' + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Content-Security-Policy: + - 'default-src ''self'' dwa5x7aod66zk.cloudfront.net; object-src ''self''; frame-src + *.vimeo.com platform.twitter.com connect.facebook.net *.facebook.com speakerdeck.com + *.youtube.com; connect-src ''self'' github-education-web.s3.amazonaws.com + *.mapbox.com *.clearbit.com www.google.com *.githubusercontent.com; img-src + ''self'' blob: dwa5x7aod66zk.cloudfront.net data: github.com *.mapbox.com + *.clearbit.com avatars.githubusercontent.com user-images.githubusercontent.com + github-education-web.s3.amazonaws.com www.google-analytics.com analytics.twitter.com + t.com *.facebook.com *.twitter.com *.youtube.com raw.githubusercontent.com; + font-src ''self'' dwa5x7aod66zk.cloudfront.net; script-src ''self'' dwa5x7aod66zk.cloudfront.net + ''unsafe-eval'' ''unsafe-inline'' www.google.com www.google-analytics.com + api.demandbase.com speakerdeck.com platform.twitter.com connect.facebook.net + *.facebook.com s3-eu-west-1.amazonaws.com/share.typeform.com/* *.github.com + *.githubassets.com; style-src ''self'' dwa5x7aod66zk.cloudfront.net ''unsafe-inline'' + www.google.com ajax.googleapis.com *.github.com' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 23:10:25 GMT + ETag: + - W/"4a8a28ca5b0f0ebcef62c74c7acd0536" + Referrer-Policy: + - strict-origin-when-cross-origin + Server: + - nginx/1.15.12 + Strict-Transport-Security: + - max-age=31536000 + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - DENY + X-GitHub-Backend: + - Kubernetes + X-GitHub-Request-Id: + - DA9A:19D3:BE2A6:1B442D:5F878561 + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - 23b2592c-f29f-4cd7-9a24-e17b863607f6 + X-Runtime: + - '0.153347' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_is_student_yes_student.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_is_student_yes_student.yaml new file mode 100644 index 0000000000..08be1ef624 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_is_student_yes_student.yaml @@ -0,0 +1,71 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - education.github.com + user-agent: + - Default + method: GET + uri: https://education.github.com/api/user + response: + content: '{"student":true}' + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Content-Security-Policy: + - 'default-src ''self'' dwa5x7aod66zk.cloudfront.net; object-src ''self''; frame-src + *.vimeo.com platform.twitter.com connect.facebook.net *.facebook.com speakerdeck.com + *.youtube.com; connect-src ''self'' github-education-web.s3.amazonaws.com + *.mapbox.com *.clearbit.com www.google.com *.githubusercontent.com; img-src + ''self'' blob: dwa5x7aod66zk.cloudfront.net data: github.com *.mapbox.com + *.clearbit.com avatars.githubusercontent.com user-images.githubusercontent.com + github-education-web.s3.amazonaws.com www.google-analytics.com analytics.twitter.com + t.com *.facebook.com *.twitter.com *.youtube.com raw.githubusercontent.com; + font-src ''self'' dwa5x7aod66zk.cloudfront.net; script-src ''self'' dwa5x7aod66zk.cloudfront.net + ''unsafe-eval'' ''unsafe-inline'' www.google.com www.google-analytics.com + api.demandbase.com speakerdeck.com platform.twitter.com connect.facebook.net + *.facebook.com s3-eu-west-1.amazonaws.com/share.typeform.com/* *.github.com + *.githubassets.com; style-src ''self'' dwa5x7aod66zk.cloudfront.net ''unsafe-inline'' + www.google.com ajax.googleapis.com *.github.com' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 23:11:11 GMT + ETag: + - W/"31a958d4bc41803067179ef7230b0b12" + Referrer-Policy: + - strict-origin-when-cross-origin + Server: + - nginx/1.15.12 + Strict-Transport-Security: + - max-age=31536000 + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - DENY + X-GitHub-Backend: + - Kubernetes + X-GitHub-Request-Id: + - DAA3:758D:25388:6FEED:5F87858E + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - 1f8d3831-02b4-4900-b73c-26ef1fda9d25 + X-Runtime: + - '0.178595' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_files.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_files.yaml new file mode 100644 index 0000000000..90022ff5ec --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_files.yaml @@ -0,0 +1,77 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=main + response: + content: '[{"name":"__init__.py","path":"awesome/__init__.py","sha":"326dc8b55279ac0c4796cb803520b1486ff7a778","size":347,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/__init__.py?ref=main","html_url":"https://github.com/ThiagoCodecov/example-python/blob/main/awesome/__init__.py","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/326dc8b55279ac0c4796cb803520b1486ff7a778","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/main/awesome/__init__.py","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/__init__.py?ref=main","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/326dc8b55279ac0c4796cb803520b1486ff7a778","html":"https://github.com/ThiagoCodecov/example-python/blob/main/awesome/__init__.py"}},{"name":"code_fib.py","path":"awesome/code_fib.py","sha":"7fb3c3fbd71a6d3f4b98964c0130f7e083505fcd","size":156,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/code_fib.py?ref=main","html_url":"https://github.com/ThiagoCodecov/example-python/blob/main/awesome/code_fib.py","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/7fb3c3fbd71a6d3f4b98964c0130f7e083505fcd","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/main/awesome/code_fib.py","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome/code_fib.py?ref=main","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/7fb3c3fbd71a6d3f4b98964c0130f7e083505fcd","html":"https://github.com/ThiagoCodecov/example-python/blob/main/awesome/code_fib.py"}}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:46:38 GMT + ETag: + - W/"cda64d688c17ae3fcf03b22ee869238002e410f0" + Last-Modified: + - Tue, 24 Mar 2020 22:01:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D055:4250:1D73C19:431363A:5F87397D + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4904' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '96' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_github_app_webhook_deliveries.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_github_app_webhook_deliveries.yaml new file mode 100644 index 0000000000..43735473f1 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_github_app_webhook_deliveries.yaml @@ -0,0 +1,77 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/vnd.github+json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/app/hook/deliveries?per_page=50 + response: + content: '[{"id":17324040107,"guid":"53c93580-7a6e-11ed-96c9-5e1ce3e5574e","delivered_at":"2022-12-12T22:42:59Z","redelivery":false,"duration":0.37,"status":"OK","status_code":200,"event":"installation_repositories","action":"added","installation_id":null,"repository_id":null,"url":""},{"id":17324018336,"guid":"40d7f830-7a6e-11ed-8b90-0777e88b1858","delivered_at":"2022-12-12T22:42:30Z","redelivery":false,"duration":2.31,"status":"OK","status_code":200,"event":"installation_repositories","action":"removed","installation_id":null,"repository_id":null,"url":""},{"id":17324018291,"guid":"40ca1490-7a6e-11ed-967d-29986a5a51d7","delivered_at":"2022-12-12T22:42:29Z","redelivery":false,"duration":2.31,"status":"OK","status_code":200,"event":"installation_repositories","action":"added","installation_id":null,"repository_id":null,"url":""},{"id":17323914960,"guid":"ec63f0b0-7a6d-11ed-8520-02855a090a3f","delivered_at":"2022-12-12T22:40:09Z","redelivery":false,"duration":3.07,"status":"Invalid + HTTP Response: 403","status_code":403,"event":"installation_repositories","action":"added","installation_id":null,"repository_id":null,"url":""},{"id":17323444221,"guid":"7457a07c-7a6c-11ed-9053-6e7a5afa9f92","delivered_at":"2022-12-12T22:29:39Z","redelivery":false,"duration":4.28,"status":"Invalid + HTTP Response: 403","status_code":403,"event":"installation_repositories","action":"removed","installation_id":null,"repository_id":null,"url":""},{"id":17323444217,"guid":"743ffa80-7a6c-11ed-9fde-491e59e80b4c","delivered_at":"2022-12-12T22:29:39Z","redelivery":false,"duration":4.03,"status":"Invalid + HTTP Response: 403","status_code":403,"event":"installation_repositories","action":"added","installation_id":null,"repository_id":null,"url":""},{"id":17323292984,"guid":"0498e8e0-7a6c-11ed-8834-c5eb5a4b102a","delivered_at":"2022-12-12T22:26:28Z","redelivery":false,"duration":0.69,"status":"Invalid + HTTP Response: 400","status_code":400,"event":"installation","action":"created","installation_id":null,"repository_id":null,"url":""},{"id":17323228732,"guid":"d41fa780-7a6b-11ed-8890-0619085a3f97","delivered_at":"2022-12-12T22:25:07Z","redelivery":false,"duration":0.74,"status":"Invalid + HTTP Response: 400","status_code":400,"event":"installation","action":"deleted","installation_id":null,"repository_id":null,"url":""},{"id":17323148882,"guid":"971e2d20-7a6b-11ed-8d64-8069e97df19b","delivered_at":"2022-12-12T22:23:25Z","redelivery":false,"duration":0.93,"status":"Invalid + HTTP Response: 400","status_code":400,"event":"installation","action":"created","installation_id":null,"repository_id":null,"url":""},{"id":17323034945,"guid":"44e51d48-7a6b-11ed-896a-952fdec99fb1","delivered_at":"2022-12-12T22:21:06Z","redelivery":false,"duration":0.7,"status":"Invalid + HTTP Response: 400","status_code":400,"event":"installation","action":"deleted","installation_id":null,"repository_id":null,"url":""},{"id":17322838109,"guid":"bde24c80-7a6a-11ed-82f8-3ab3fc2087e2","delivered_at":"2022-12-12T22:17:20Z","redelivery":false,"duration":0.61,"status":"Invalid + HTTP Response: 400","status_code":400,"event":"installation","action":"created","installation_id":null,"repository_id":null,"url":""},{"id":17322818598,"guid":"b0ad9bfa-7a6a-11ed-8cd1-5991761b218b","delivered_at":"2022-12-12T22:16:58Z","redelivery":false,"duration":0.79,"status":"Invalid + HTTP Response: 400","status_code":400,"event":"installation","action":"deleted","installation_id":null,"repository_id":null,"url":""},{"id":17322611772,"guid":"1fadf690-7a6a-11ed-8f0c-93835183bf91","delivered_at":"2022-12-12T22:12:54Z","redelivery":false,"duration":0.59,"status":"Invalid + HTTP Response: 400","status_code":400,"event":"installation","action":"created","installation_id":null,"repository_id":null,"url":""},{"id":17322592837,"guid":"11f168e8-7a6a-11ed-800f-f78cd489c77d","delivered_at":"2022-12-12T22:12:31Z","redelivery":false,"duration":0.93,"status":"Invalid + HTTP Response: 400","status_code":400,"event":"installation","action":"deleted","installation_id":null,"repository_id":null,"url":""},{"id":17322555251,"guid":"f80fc050-7a69-11ed-9523-490fcf66f6ce","delivered_at":"2022-12-12T22:11:48Z","redelivery":false,"duration":0.59,"status":"Invalid + HTTP Response: 400","status_code":400,"event":"installation","action":"new_permissions_accepted","installation_id":null,"repository_id":null,"url":""},{"id":17322476105,"guid":"c03336d0-7a69-11ed-83e4-5b94a522ce04","delivered_at":"2022-12-12T22:10:14Z","redelivery":false,"duration":0.7,"status":"Invalid + HTTP Response: 400","status_code":400,"event":"ping","action":null,"installation_id":null,"repository_id":null,"url":""}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - public, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 02 Jan 2023 17:40:03 GMT + ETag: + - W/"dde9b182b3b4e7c4c6dea22b0af116672743b6c8ad0da6122104ac0d94f5ee3f" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; format=json + X-GitHub-Request-Id: + - E7E5:5F8D:71A03C:8677D4:63B316F2 + X-XSS-Protection: + - '0' + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_github_app_webhook_redelivery.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_github_app_webhook_redelivery.yaml new file mode 100644 index 0000000000..69381e1bf1 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_github_app_webhook_redelivery.yaml @@ -0,0 +1,59 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/vnd.github+json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '0' + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/app/hook/deliveries/17322555251/attempts + response: + content: '{}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Length: + - '2' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 02 Jan 2023 18:34:21 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; format=json + X-GitHub-Request-Id: + - E9D5:6DF4:70F9C3:8603A7:63B323AC + X-XSS-Protection: + - '0' + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 202 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_repos.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_repos.yaml new file mode 100644 index 0000000000..a0b4086094 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_repos.yaml @@ -0,0 +1,7329 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/user/repos?per_page=50&page=1 + response: + content: "[{\"id\":24730274,\"node_id\":\"MDEwOlJlcG9zaXRvcnkyNDczMDI3NA==\",\"\ + name\":\"admin\",\"full_name\":\"codecov/admin\",\"private\":true,\"owner\"\ + :{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/admin\"\ + ,\"description\":\"Automatically adding Codecov\",\"fork\":false,\"url\":\"\ + https://api.github.com/repos/codecov/admin\",\"forks_url\":\"https://api.github.com/repos/codecov/admin/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/admin/keys{/key_id}\",\"\ + collaborators_url\":\"https://api.github.com/repos/codecov/admin/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/admin/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/admin/hooks\",\"issue_events_url\":\"\ + https://api.github.com/repos/codecov/admin/issues/events{/number}\",\"events_url\"\ + :\"https://api.github.com/repos/codecov/admin/events\",\"assignees_url\":\"\ + https://api.github.com/repos/codecov/admin/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/admin/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/admin/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/admin/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/admin/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/admin/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/admin/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/admin/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/admin/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/admin/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/admin/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/admin/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/admin/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/admin/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/admin/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/admin/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/admin/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/admin/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/admin/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/admin/merges\",\"archive_url\"\ + :\"https://api.github.com/repos/codecov/admin/{archive_format}{/ref}\",\"downloads_url\"\ + :\"https://api.github.com/repos/codecov/admin/downloads\",\"issues_url\":\"\ + https://api.github.com/repos/codecov/admin/issues{/number}\",\"pulls_url\":\"\ + https://api.github.com/repos/codecov/admin/pulls{/number}\",\"milestones_url\"\ + :\"https://api.github.com/repos/codecov/admin/milestones{/number}\",\"notifications_url\"\ + :\"https://api.github.com/repos/codecov/admin/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/admin/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/admin/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/admin/deployments\"\ + ,\"created_at\":\"2014-10-02T18:05:04Z\",\"updated_at\":\"2015-05-03T17:20:10Z\"\ + ,\"pushed_at\":\"2015-06-30T17:44:14Z\",\"git_url\":\"git://github.com/codecov/admin.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/admin.git\",\"clone_url\":\"https://github.com/codecov/admin.git\"\ + ,\"svn_url\":\"https://github.com/codecov/admin\",\"homepage\":\"\",\"size\"\ + :338,\"stargazers_count\":0,\"watchers_count\":0,\"language\":\"Python\",\"\ + has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\"\ + :true,\"has_pages\":false,\"forks_count\":0,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"forks\"\ + :0,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":239903643,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkyMzk5MDM2NDM=\",\"name\":\"analytics\",\"full_name\":\"\ + codecov/analytics\",\"private\":true,\"owner\":{\"login\":\"codecov\",\"id\"\ + :8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"\ + https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\"\ + ,\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/analytics\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/analytics\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/analytics/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/analytics/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/analytics/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/analytics/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/analytics/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/analytics/issues/events{/number}\",\"\ + events_url\":\"https://api.github.com/repos/codecov/analytics/events\",\"assignees_url\"\ + :\"https://api.github.com/repos/codecov/analytics/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/analytics/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/analytics/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/analytics/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/analytics/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/analytics/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/analytics/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/analytics/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/analytics/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/analytics/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/analytics/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/analytics/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/analytics/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/analytics/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/analytics/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/analytics/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/analytics/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/analytics/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/analytics/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/analytics/merges\",\"\ + archive_url\":\"https://api.github.com/repos/codecov/analytics/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/analytics/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/analytics/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/analytics/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/analytics/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/analytics/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/analytics/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/analytics/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/analytics/deployments\"\ + ,\"created_at\":\"2020-02-12T01:43:15Z\",\"updated_at\":\"2020-09-29T15:06:58Z\"\ + ,\"pushed_at\":\"2020-09-29T15:06:56Z\",\"git_url\":\"git://github.com/codecov/analytics.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/analytics.git\",\"clone_url\":\"https://github.com/codecov/analytics.git\"\ + ,\"svn_url\":\"https://github.com/codecov/analytics\",\"homepage\":null,\"size\"\ + :7,\"stargazers_count\":0,\"watchers_count\":0,\"language\":\"Shell\",\"has_issues\"\ + :true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\":true,\"has_pages\"\ + :false,\"forks_count\":0,\"mirror_url\":null,\"archived\":false,\"disabled\"\ + :false,\"open_issues_count\":0,\"license\":null,\"forks\":0,\"open_issues\"\ + :0,\"watchers\":0,\"default_branch\":\"main\",\"permissions\":{\"admin\":true,\"\ + push\":true,\"pull\":true}},{\"id\":286876237,\"node_id\":\"MDEwOlJlcG9zaXRvcnkyODY4NzYyMzc=\"\ + ,\"name\":\"autotest\",\"full_name\":\"codecov/autotest\",\"private\":true,\"\ + owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/autotest\"\ + ,\"description\":\"A project for automated testing.\",\"fork\":false,\"url\"\ + :\"https://api.github.com/repos/codecov/autotest\",\"forks_url\":\"https://api.github.com/repos/codecov/autotest/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/autotest/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/autotest/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/autotest/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/autotest/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/autotest/issues/events{/number}\",\"\ + events_url\":\"https://api.github.com/repos/codecov/autotest/events\",\"assignees_url\"\ + :\"https://api.github.com/repos/codecov/autotest/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/autotest/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/autotest/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/autotest/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/autotest/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/autotest/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/autotest/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/autotest/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/autotest/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/autotest/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/autotest/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/autotest/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/autotest/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/autotest/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/autotest/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/autotest/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/autotest/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/autotest/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/autotest/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/autotest/merges\",\"\ + archive_url\":\"https://api.github.com/repos/codecov/autotest/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/autotest/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/autotest/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/autotest/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/autotest/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/autotest/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/autotest/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/autotest/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/autotest/deployments\"\ + ,\"created_at\":\"2020-08-12T00:21:36Z\",\"updated_at\":\"2020-09-10T02:50:35Z\"\ + ,\"pushed_at\":\"2020-09-10T02:50:32Z\",\"git_url\":\"git://github.com/codecov/autotest.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/autotest.git\",\"clone_url\":\"https://github.com/codecov/autotest.git\"\ + ,\"svn_url\":\"https://github.com/codecov/autotest\",\"homepage\":null,\"size\"\ + :85,\"stargazers_count\":0,\"watchers_count\":0,\"language\":\"JavaScript\"\ + ,\"has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\"\ + :true,\"has_pages\":false,\"forks_count\":0,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"forks\"\ + :0,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":155724957,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkxNTU3MjQ5NTc=\",\"name\":\"backend\",\"full_name\":\"\ + codecov/backend\",\"private\":true,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"\ + node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/backend\"\ + ,\"description\":\"A repository to organize and track codecov.io's planned backend\ + \ refactor\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/backend\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/backend/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/backend/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/backend/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/backend/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/backend/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/backend/issues/events{/number}\",\"\ + events_url\":\"https://api.github.com/repos/codecov/backend/events\",\"assignees_url\"\ + :\"https://api.github.com/repos/codecov/backend/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/backend/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/backend/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/backend/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/backend/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/backend/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/backend/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/backend/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/backend/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/backend/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/backend/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/backend/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/backend/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/backend/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/backend/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/backend/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/backend/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/backend/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/backend/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/backend/merges\",\"archive_url\"\ + :\"https://api.github.com/repos/codecov/backend/{archive_format}{/ref}\",\"\ + downloads_url\":\"https://api.github.com/repos/codecov/backend/downloads\",\"\ + issues_url\":\"https://api.github.com/repos/codecov/backend/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/backend/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/backend/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/backend/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/backend/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/backend/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/backend/deployments\"\ + ,\"created_at\":\"2018-11-01T14:09:13Z\",\"updated_at\":\"2019-09-19T15:59:40Z\"\ + ,\"pushed_at\":\"2018-11-01T14:36:47Z\",\"git_url\":\"git://github.com/codecov/backend.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/backend.git\",\"clone_url\":\"https://github.com/codecov/backend.git\"\ + ,\"svn_url\":\"https://github.com/codecov/backend\",\"homepage\":null,\"size\"\ + :8,\"stargazers_count\":0,\"watchers_count\":0,\"language\":null,\"has_issues\"\ + :true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\":true,\"has_pages\"\ + :false,\"forks_count\":0,\"mirror_url\":null,\"archived\":true,\"disabled\"\ + :false,\"open_issues_count\":5,\"license\":null,\"forks\":0,\"open_issues\"\ + :5,\"watchers\":0,\"default_branch\":\"main\",\"permissions\":{\"admin\":false,\"\ + push\":true,\"pull\":true}},{\"id\":29562917,\"node_id\":\"MDEwOlJlcG9zaXRvcnkyOTU2MjkxNw==\"\ + ,\"name\":\"browser-extension\",\"full_name\":\"codecov/browser-extension\"\ + ,\"private\":false,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\"\ + :\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/browser-extension\"\ + ,\"description\":\"Codecov Browser Extension\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/browser-extension\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/browser-extension/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/browser-extension/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/browser-extension/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/browser-extension/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/browser-extension/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/browser-extension/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/browser-extension/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/browser-extension/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/browser-extension/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/browser-extension/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/browser-extension/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/browser-extension/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/browser-extension/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/browser-extension/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/browser-extension/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/browser-extension/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/browser-extension/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/browser-extension/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/browser-extension/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/browser-extension/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/browser-extension/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/browser-extension/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/browser-extension/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/browser-extension/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/browser-extension/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/browser-extension/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/browser-extension/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/browser-extension/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/browser-extension/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/browser-extension/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/browser-extension/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/browser-extension/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/browser-extension/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/browser-extension/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/browser-extension/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/browser-extension/deployments\"\ + ,\"created_at\":\"2015-01-21T00:32:42Z\",\"updated_at\":\"2020-08-06T21:11:56Z\"\ + ,\"pushed_at\":\"2018-06-28T15:53:29Z\",\"git_url\":\"git://github.com/codecov/browser-extension.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/browser-extension.git\",\"clone_url\"\ + :\"https://github.com/codecov/browser-extension.git\",\"svn_url\":\"https://github.com/codecov/browser-extension\"\ + ,\"homepage\":\"http://codecov.io\",\"size\":2388,\"stargazers_count\":212,\"\ + watchers_count\":212,\"language\":\"JavaScript\",\"has_issues\":true,\"has_projects\"\ + :true,\"has_downloads\":true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\"\ + :80,\"mirror_url\":null,\"archived\":true,\"disabled\":false,\"open_issues_count\"\ + :26,\"license\":{\"key\":\"apache-2.0\",\"name\":\"Apache License 2.0\",\"spdx_id\"\ + :\"Apache-2.0\",\"url\":\"https://api.github.com/licenses/apache-2.0\",\"node_id\"\ + :\"MDc6TGljZW5zZTI=\"},\"forks\":80,\"open_issues\":26,\"watchers\":212,\"default_branch\"\ + :\"main\",\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"\ + id\":186653620,\"node_id\":\"MDEwOlJlcG9zaXRvcnkxODY2NTM2MjA=\",\"name\":\"\ + candidate-exercises\",\"full_name\":\"codecov/candidate-exercises\",\"private\"\ + :true,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/candidate-exercises\"\ + ,\"description\":\"A repository of all exercises, code tests, etc used to vet\ + \ the applicability of job candidates\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/candidate-exercises\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/candidate-exercises/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/candidate-exercises/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/candidate-exercises/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/candidate-exercises/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/candidate-exercises/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/candidate-exercises/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/candidate-exercises/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/candidate-exercises/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/candidate-exercises/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/candidate-exercises/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/candidate-exercises/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/candidate-exercises/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/candidate-exercises/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/candidate-exercises/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/candidate-exercises/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/candidate-exercises/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/candidate-exercises/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/candidate-exercises/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/candidate-exercises/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/candidate-exercises/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/candidate-exercises/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/candidate-exercises/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/candidate-exercises/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/candidate-exercises/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/candidate-exercises/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/candidate-exercises/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/candidate-exercises/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/candidate-exercises/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/candidate-exercises/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/candidate-exercises/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/candidate-exercises/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/candidate-exercises/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/candidate-exercises/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/candidate-exercises/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/candidate-exercises/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/candidate-exercises/deployments\"\ + ,\"created_at\":\"2019-05-14T15:47:13Z\",\"updated_at\":\"2019-06-18T21:55:59Z\"\ + ,\"pushed_at\":\"2019-06-18T21:55:57Z\",\"git_url\":\"git://github.com/codecov/candidate-exercises.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/candidate-exercises.git\",\"clone_url\"\ + :\"https://github.com/codecov/candidate-exercises.git\",\"svn_url\":\"https://github.com/codecov/candidate-exercises\"\ + ,\"homepage\":null,\"size\":10,\"stargazers_count\":0,\"watchers_count\":0,\"\ + language\":null,\"has_issues\":true,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":true,\"has_pages\":false,\"forks_count\":0,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\"\ + :null,\"forks\":0,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":false,\"push\":false,\"pull\":true}},{\"id\":82936397,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnk4MjkzNjM5Nw==\",\"name\":\"cc-process-coverage\"\ + ,\"full_name\":\"codecov/cc-process-coverage\",\"private\":true,\"owner\":{\"\ + login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/cc-process-coverage\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/cc-process-coverage\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/cc-process-coverage/deployments\"\ + ,\"created_at\":\"2017-02-23T14:41:39Z\",\"updated_at\":\"2017-06-08T19:55:38Z\"\ + ,\"pushed_at\":\"2018-09-01T23:22:04Z\",\"git_url\":\"git://github.com/codecov/cc-process-coverage.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/cc-process-coverage.git\",\"clone_url\"\ + :\"https://github.com/codecov/cc-process-coverage.git\",\"svn_url\":\"https://github.com/codecov/cc-process-coverage\"\ + ,\"homepage\":null,\"size\":33314,\"stargazers_count\":0,\"watchers_count\"\ + :0,\"language\":\"Python\",\"has_issues\":true,\"has_projects\":false,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":0,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":3,\"license\"\ + :null,\"forks\":0,\"open_issues\":3,\"watchers\":0,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":206573760,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkyMDY1NzM3NjA=\",\"name\":\"changes-tab-tests\"\ + ,\"full_name\":\"codecov/changes-tab-tests\",\"private\":true,\"owner\":{\"\ + login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/changes-tab-tests\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/changes-tab-tests\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/changes-tab-tests/deployments\"\ + ,\"created_at\":\"2019-09-05T13:42:53Z\",\"updated_at\":\"2019-09-05T14:30:18Z\"\ + ,\"pushed_at\":\"2019-09-05T16:07:18Z\",\"git_url\":\"git://github.com/codecov/changes-tab-tests.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/changes-tab-tests.git\",\"clone_url\"\ + :\"https://github.com/codecov/changes-tab-tests.git\",\"svn_url\":\"https://github.com/codecov/changes-tab-tests\"\ + ,\"homepage\":null,\"size\":5,\"stargazers_count\":0,\"watchers_count\":0,\"\ + language\":\"Python\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":true,\"has_pages\":false,\"forks_count\":0,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":2,\"license\"\ + :{\"key\":\"mit\",\"name\":\"MIT License\",\"spdx_id\":\"MIT\",\"url\":\"https://api.github.com/licenses/mit\"\ + ,\"node_id\":\"MDc6TGljZW5zZTEz\"},\"forks\":0,\"open_issues\":2,\"watchers\"\ + :0,\"default_branch\":\"main\",\"permissions\":{\"admin\":true,\"push\":true,\"\ + pull\":true}},{\"id\":30621011,\"node_id\":\"MDEwOlJlcG9zaXRvcnkzMDYyMTAxMQ==\"\ + ,\"name\":\"ci-private\",\"full_name\":\"codecov/ci-private\",\"private\":true,\"\ + owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/ci-private\"\ + ,\"description\":\"Used in CI tests\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/ci-private\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/ci-private/forks\",\"\ + keys_url\":\"https://api.github.com/repos/codecov/ci-private/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/ci-private/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/ci-private/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/ci-private/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/ci-private/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/ci-private/events\",\"\ + assignees_url\":\"https://api.github.com/repos/codecov/ci-private/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/ci-private/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/ci-private/tags\",\"blobs_url\"\ + :\"https://api.github.com/repos/codecov/ci-private/git/blobs{/sha}\",\"git_tags_url\"\ + :\"https://api.github.com/repos/codecov/ci-private/git/tags{/sha}\",\"git_refs_url\"\ + :\"https://api.github.com/repos/codecov/ci-private/git/refs{/sha}\",\"trees_url\"\ + :\"https://api.github.com/repos/codecov/ci-private/git/trees{/sha}\",\"statuses_url\"\ + :\"https://api.github.com/repos/codecov/ci-private/statuses/{sha}\",\"languages_url\"\ + :\"https://api.github.com/repos/codecov/ci-private/languages\",\"stargazers_url\"\ + :\"https://api.github.com/repos/codecov/ci-private/stargazers\",\"contributors_url\"\ + :\"https://api.github.com/repos/codecov/ci-private/contributors\",\"subscribers_url\"\ + :\"https://api.github.com/repos/codecov/ci-private/subscribers\",\"subscription_url\"\ + :\"https://api.github.com/repos/codecov/ci-private/subscription\",\"commits_url\"\ + :\"https://api.github.com/repos/codecov/ci-private/commits{/sha}\",\"git_commits_url\"\ + :\"https://api.github.com/repos/codecov/ci-private/git/commits{/sha}\",\"comments_url\"\ + :\"https://api.github.com/repos/codecov/ci-private/comments{/number}\",\"issue_comment_url\"\ + :\"https://api.github.com/repos/codecov/ci-private/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/ci-private/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/ci-private/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/ci-private/merges\",\"\ + archive_url\":\"https://api.github.com/repos/codecov/ci-private/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/ci-private/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/ci-private/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/ci-private/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/ci-private/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/ci-private/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/ci-private/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/ci-private/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/ci-private/deployments\"\ + ,\"created_at\":\"2015-02-10T23:51:25Z\",\"updated_at\":\"2015-02-10T23:54:07Z\"\ + ,\"pushed_at\":\"2015-02-10T23:51:25Z\",\"git_url\":\"git://github.com/codecov/ci-private.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/ci-private.git\",\"clone_url\":\"https://github.com/codecov/ci-private.git\"\ + ,\"svn_url\":\"https://github.com/codecov/ci-private\",\"homepage\":\"\",\"\ + size\":0,\"stargazers_count\":0,\"watchers_count\":0,\"language\":null,\"has_issues\"\ + :true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\":true,\"has_pages\"\ + :false,\"forks_count\":0,\"mirror_url\":null,\"archived\":false,\"disabled\"\ + :false,\"open_issues_count\":0,\"license\":null,\"forks\":0,\"open_issues\"\ + :0,\"watchers\":0,\"default_branch\":\"main\",\"permissions\":{\"admin\":true,\"\ + push\":true,\"pull\":true}},{\"id\":23162118,\"node_id\":\"MDEwOlJlcG9zaXRvcnkyMzE2MjExOA==\"\ + ,\"name\":\"ci-repo\",\"full_name\":\"codecov/ci-repo\",\"private\":false,\"\ + owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/ci-repo\"\ + ,\"description\":\"debug and ci only\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/ci-repo\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/ci-repo/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/ci-repo/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/ci-repo/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/ci-repo/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/ci-repo/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/ci-repo/issues/events{/number}\",\"\ + events_url\":\"https://api.github.com/repos/codecov/ci-repo/events\",\"assignees_url\"\ + :\"https://api.github.com/repos/codecov/ci-repo/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/ci-repo/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/ci-repo/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/ci-repo/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/ci-repo/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/ci-repo/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/ci-repo/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/ci-repo/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/ci-repo/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/ci-repo/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/ci-repo/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/ci-repo/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/ci-repo/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/ci-repo/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/ci-repo/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/ci-repo/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/ci-repo/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/ci-repo/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/ci-repo/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/ci-repo/merges\",\"archive_url\"\ + :\"https://api.github.com/repos/codecov/ci-repo/{archive_format}{/ref}\",\"\ + downloads_url\":\"https://api.github.com/repos/codecov/ci-repo/downloads\",\"\ + issues_url\":\"https://api.github.com/repos/codecov/ci-repo/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/ci-repo/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/ci-repo/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/ci-repo/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/ci-repo/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/ci-repo/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/ci-repo/deployments\"\ + ,\"created_at\":\"2014-08-20T19:56:09Z\",\"updated_at\":\"2020-08-27T03:04:19Z\"\ + ,\"pushed_at\":\"2020-08-27T03:04:16Z\",\"git_url\":\"git://github.com/codecov/ci-repo.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/ci-repo.git\",\"clone_url\":\"https://github.com/codecov/ci-repo.git\"\ + ,\"svn_url\":\"https://github.com/codecov/ci-repo\",\"homepage\":\"https://codecov.io\"\ + ,\"size\":18,\"stargazers_count\":0,\"watchers_count\":0,\"language\":\"Python\"\ + ,\"has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":5,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"forks\"\ + :5,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":35577774,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkzNTU3Nzc3NA==\",\"name\":\"cli\",\"full_name\":\"codecov/cli\"\ + ,\"private\":true,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\"\ + :\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/cli\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/cli\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/cli/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/cli/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/cli/collaborators{/collaborator}\",\"\ + teams_url\":\"https://api.github.com/repos/codecov/cli/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/cli/hooks\",\"issue_events_url\":\"\ + https://api.github.com/repos/codecov/cli/issues/events{/number}\",\"events_url\"\ + :\"https://api.github.com/repos/codecov/cli/events\",\"assignees_url\":\"https://api.github.com/repos/codecov/cli/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/cli/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/cli/tags\",\"blobs_url\"\ + :\"https://api.github.com/repos/codecov/cli/git/blobs{/sha}\",\"git_tags_url\"\ + :\"https://api.github.com/repos/codecov/cli/git/tags{/sha}\",\"git_refs_url\"\ + :\"https://api.github.com/repos/codecov/cli/git/refs{/sha}\",\"trees_url\":\"\ + https://api.github.com/repos/codecov/cli/git/trees{/sha}\",\"statuses_url\"\ + :\"https://api.github.com/repos/codecov/cli/statuses/{sha}\",\"languages_url\"\ + :\"https://api.github.com/repos/codecov/cli/languages\",\"stargazers_url\":\"\ + https://api.github.com/repos/codecov/cli/stargazers\",\"contributors_url\":\"\ + https://api.github.com/repos/codecov/cli/contributors\",\"subscribers_url\"\ + :\"https://api.github.com/repos/codecov/cli/subscribers\",\"subscription_url\"\ + :\"https://api.github.com/repos/codecov/cli/subscription\",\"commits_url\":\"\ + https://api.github.com/repos/codecov/cli/commits{/sha}\",\"git_commits_url\"\ + :\"https://api.github.com/repos/codecov/cli/git/commits{/sha}\",\"comments_url\"\ + :\"https://api.github.com/repos/codecov/cli/comments{/number}\",\"issue_comment_url\"\ + :\"https://api.github.com/repos/codecov/cli/issues/comments{/number}\",\"contents_url\"\ + :\"https://api.github.com/repos/codecov/cli/contents/{+path}\",\"compare_url\"\ + :\"https://api.github.com/repos/codecov/cli/compare/{base}...{head}\",\"merges_url\"\ + :\"https://api.github.com/repos/codecov/cli/merges\",\"archive_url\":\"https://api.github.com/repos/codecov/cli/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/cli/downloads\",\"\ + issues_url\":\"https://api.github.com/repos/codecov/cli/issues{/number}\",\"\ + pulls_url\":\"https://api.github.com/repos/codecov/cli/pulls{/number}\",\"milestones_url\"\ + :\"https://api.github.com/repos/codecov/cli/milestones{/number}\",\"notifications_url\"\ + :\"https://api.github.com/repos/codecov/cli/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/cli/labels{/name}\",\"\ + releases_url\":\"https://api.github.com/repos/codecov/cli/releases{/id}\",\"\ + deployments_url\":\"https://api.github.com/repos/codecov/cli/deployments\",\"\ + created_at\":\"2015-05-13T22:42:02Z\",\"updated_at\":\"2016-07-26T14:19:14Z\"\ + ,\"pushed_at\":\"2017-04-10T15:24:07Z\",\"git_url\":\"git://github.com/codecov/cli.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/cli.git\",\"clone_url\":\"https://github.com/codecov/cli.git\"\ + ,\"svn_url\":\"https://github.com/codecov/cli\",\"homepage\":null,\"size\":15,\"\ + stargazers_count\":0,\"watchers_count\":0,\"language\":\"Shell\",\"has_issues\"\ + :true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\":true,\"has_pages\"\ + :false,\"forks_count\":0,\"mirror_url\":null,\"archived\":false,\"disabled\"\ + :false,\"open_issues_count\":0,\"license\":null,\"forks\":0,\"open_issues\"\ + :0,\"watchers\":0,\"default_branch\":\"main\",\"permissions\":{\"admin\":true,\"\ + push\":true,\"pull\":true}},{\"id\":160537716,\"node_id\":\"MDEwOlJlcG9zaXRvcnkxNjA1Mzc3MTY=\"\ + ,\"name\":\"codecov-api\",\"full_name\":\"codecov/codecov-api\",\"private\"\ + :true,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/codecov-api\"\ + ,\"description\":\"Code for new API of Codecov\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/codecov-api\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/codecov-api/forks\",\"\ + keys_url\":\"https://api.github.com/repos/codecov/codecov-api/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/codecov-api/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/codecov-api/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/codecov-api/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/codecov-api/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/codecov-api/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/codecov-api/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/codecov-api/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/codecov-api/tags\",\"blobs_url\"\ + :\"https://api.github.com/repos/codecov/codecov-api/git/blobs{/sha}\",\"git_tags_url\"\ + :\"https://api.github.com/repos/codecov/codecov-api/git/tags{/sha}\",\"git_refs_url\"\ + :\"https://api.github.com/repos/codecov/codecov-api/git/refs{/sha}\",\"trees_url\"\ + :\"https://api.github.com/repos/codecov/codecov-api/git/trees{/sha}\",\"statuses_url\"\ + :\"https://api.github.com/repos/codecov/codecov-api/statuses/{sha}\",\"languages_url\"\ + :\"https://api.github.com/repos/codecov/codecov-api/languages\",\"stargazers_url\"\ + :\"https://api.github.com/repos/codecov/codecov-api/stargazers\",\"contributors_url\"\ + :\"https://api.github.com/repos/codecov/codecov-api/contributors\",\"subscribers_url\"\ + :\"https://api.github.com/repos/codecov/codecov-api/subscribers\",\"subscription_url\"\ + :\"https://api.github.com/repos/codecov/codecov-api/subscription\",\"commits_url\"\ + :\"https://api.github.com/repos/codecov/codecov-api/commits{/sha}\",\"git_commits_url\"\ + :\"https://api.github.com/repos/codecov/codecov-api/git/commits{/sha}\",\"comments_url\"\ + :\"https://api.github.com/repos/codecov/codecov-api/comments{/number}\",\"issue_comment_url\"\ + :\"https://api.github.com/repos/codecov/codecov-api/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/codecov-api/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/codecov-api/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/codecov-api/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/codecov-api/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/codecov-api/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/codecov-api/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/codecov-api/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/codecov-api/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/codecov-api/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/codecov-api/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/codecov-api/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/codecov-api/deployments\"\ + ,\"created_at\":\"2018-12-05T15:21:09Z\",\"updated_at\":\"2020-10-07T23:30:50Z\"\ + ,\"pushed_at\":\"2020-10-12T22:57:09Z\",\"git_url\":\"git://github.com/codecov/codecov-api.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/codecov-api.git\",\"clone_url\":\"https://github.com/codecov/codecov-api.git\"\ + ,\"svn_url\":\"https://github.com/codecov/codecov-api\",\"homepage\":null,\"\ + size\":20065,\"stargazers_count\":0,\"watchers_count\":0,\"language\":\"Python\"\ + ,\"has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\"\ + :true,\"has_pages\":false,\"forks_count\":1,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":12,\"license\":null,\"forks\"\ + :1,\"open_issues\":12,\"watchers\":0,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":192778030,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkxOTI3NzgwMzA=\",\"name\":\"codecov-app\",\"full_name\"\ + :\"codecov/codecov-app\",\"private\":true,\"owner\":{\"login\":\"codecov\",\"\ + id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/codecov-app\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/codecov-app\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/codecov-app/forks\",\"\ + keys_url\":\"https://api.github.com/repos/codecov/codecov-app/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/codecov-app/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/codecov-app/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/codecov-app/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/codecov-app/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/codecov-app/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/codecov-app/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/codecov-app/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/codecov-app/tags\",\"blobs_url\"\ + :\"https://api.github.com/repos/codecov/codecov-app/git/blobs{/sha}\",\"git_tags_url\"\ + :\"https://api.github.com/repos/codecov/codecov-app/git/tags{/sha}\",\"git_refs_url\"\ + :\"https://api.github.com/repos/codecov/codecov-app/git/refs{/sha}\",\"trees_url\"\ + :\"https://api.github.com/repos/codecov/codecov-app/git/trees{/sha}\",\"statuses_url\"\ + :\"https://api.github.com/repos/codecov/codecov-app/statuses/{sha}\",\"languages_url\"\ + :\"https://api.github.com/repos/codecov/codecov-app/languages\",\"stargazers_url\"\ + :\"https://api.github.com/repos/codecov/codecov-app/stargazers\",\"contributors_url\"\ + :\"https://api.github.com/repos/codecov/codecov-app/contributors\",\"subscribers_url\"\ + :\"https://api.github.com/repos/codecov/codecov-app/subscribers\",\"subscription_url\"\ + :\"https://api.github.com/repos/codecov/codecov-app/subscription\",\"commits_url\"\ + :\"https://api.github.com/repos/codecov/codecov-app/commits{/sha}\",\"git_commits_url\"\ + :\"https://api.github.com/repos/codecov/codecov-app/git/commits{/sha}\",\"comments_url\"\ + :\"https://api.github.com/repos/codecov/codecov-app/comments{/number}\",\"issue_comment_url\"\ + :\"https://api.github.com/repos/codecov/codecov-app/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/codecov-app/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/codecov-app/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/codecov-app/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/codecov-app/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/codecov-app/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/codecov-app/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/codecov-app/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/codecov-app/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/codecov-app/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/codecov-app/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/codecov-app/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/codecov-app/deployments\"\ + ,\"created_at\":\"2019-06-19T17:38:22Z\",\"updated_at\":\"2019-11-14T17:52:17Z\"\ + ,\"pushed_at\":\"2019-11-26T15:38:58Z\",\"git_url\":\"git://github.com/codecov/codecov-app.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/codecov-app.git\",\"clone_url\":\"https://github.com/codecov/codecov-app.git\"\ + ,\"svn_url\":\"https://github.com/codecov/codecov-app\",\"homepage\":null,\"\ + size\":217,\"stargazers_count\":0,\"watchers_count\":0,\"language\":\"Dockerfile\"\ + ,\"has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\"\ + :true,\"has_pages\":false,\"forks_count\":0,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":1,\"license\":null,\"forks\"\ + :0,\"open_issues\":1,\"watchers\":0,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":false,\"push\":false,\"pull\":true}},{\"id\":160685350,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkxNjA2ODUzNTA=\",\"name\":\"codecov-assume-flag-test\"\ + ,\"full_name\":\"codecov/codecov-assume-flag-test\",\"private\":true,\"owner\"\ + :{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/codecov-assume-flag-test\"\ + ,\"description\":\"A small test repo for codecov assume flags\",\"fork\":false,\"\ + url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test\",\"forks_url\"\ + :\"https://api.github.com/repos/codecov/codecov-assume-flag-test/forks\",\"\ + keys_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/codecov-assume-flag-test/deployments\"\ + ,\"created_at\":\"2018-12-06T14:21:07Z\",\"updated_at\":\"2020-09-24T15:35:01Z\"\ + ,\"pushed_at\":\"2020-10-07T14:59:34Z\",\"git_url\":\"git://github.com/codecov/codecov-assume-flag-test.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/codecov-assume-flag-test.git\",\"clone_url\"\ + :\"https://github.com/codecov/codecov-assume-flag-test.git\",\"svn_url\":\"\ + https://github.com/codecov/codecov-assume-flag-test\",\"homepage\":null,\"size\"\ + :83,\"stargazers_count\":0,\"watchers_count\":0,\"language\":\"Python\",\"has_issues\"\ + :true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\":true,\"has_pages\"\ + :false,\"forks_count\":2,\"mirror_url\":null,\"archived\":false,\"disabled\"\ + :false,\"open_issues_count\":9,\"license\":{\"key\":\"mit\",\"name\":\"MIT License\"\ + ,\"spdx_id\":\"MIT\",\"url\":\"https://api.github.com/licenses/mit\",\"node_id\"\ + :\"MDc6TGljZW5zZTEz\"},\"forks\":2,\"open_issues\":9,\"watchers\":0,\"default_branch\"\ + :\"main\",\"permissions\":{\"admin\":false,\"push\":false,\"pull\":true}},{\"\ + id\":34096540,\"node_id\":\"MDEwOlJlcG9zaXRvcnkzNDA5NjU0MA==\",\"name\":\"codecov-bash\"\ + ,\"full_name\":\"codecov/codecov-bash\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/codecov-bash\"\ + ,\"description\":\"Global coverage report uploader for Codecov\",\"fork\":false,\"\ + url\":\"https://api.github.com/repos/codecov/codecov-bash\",\"forks_url\":\"\ + https://api.github.com/repos/codecov/codecov-bash/forks\",\"keys_url\":\"https://api.github.com/repos/codecov/codecov-bash/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/codecov-bash/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/codecov-bash/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/codecov-bash/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/codecov-bash/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/codecov-bash/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/codecov-bash/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/codecov-bash/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/codecov-bash/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/codecov-bash/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/codecov-bash/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/codecov-bash/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/codecov-bash/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/codecov-bash/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/codecov-bash/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/codecov-bash/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/codecov-bash/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/codecov-bash/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/codecov-bash/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/codecov-bash/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/codecov-bash/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/codecov-bash/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/codecov-bash/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/codecov-bash/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/codecov-bash/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/codecov-bash/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/codecov-bash/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/codecov-bash/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/codecov-bash/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/codecov-bash/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/codecov-bash/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/codecov-bash/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/codecov-bash/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/codecov-bash/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/codecov-bash/deployments\"\ + ,\"created_at\":\"2015-04-17T04:31:13Z\",\"updated_at\":\"2020-10-13T13:07:08Z\"\ + ,\"pushed_at\":\"2020-10-14T06:32:25Z\",\"git_url\":\"git://github.com/codecov/codecov-bash.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/codecov-bash.git\",\"clone_url\":\"https://github.com/codecov/codecov-bash.git\"\ + ,\"svn_url\":\"https://github.com/codecov/codecov-bash\",\"homepage\":\"https://codecov.io\"\ + ,\"size\":938,\"stargazers_count\":210,\"watchers_count\":210,\"language\":\"\ + Shell\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"\ + has_wiki\":false,\"has_pages\":false,\"forks_count\":162,\"mirror_url\":null,\"\ + archived\":false,\"disabled\":false,\"open_issues_count\":57,\"license\":{\"\ + key\":\"apache-2.0\",\"name\":\"Apache License 2.0\",\"spdx_id\":\"Apache-2.0\"\ + ,\"url\":\"https://api.github.com/licenses/apache-2.0\",\"node_id\":\"MDc6TGljZW5zZTI=\"\ + },\"forks\":162,\"open_issues\":57,\"watchers\":210,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":157147600,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkxNTcxNDc2MDA=\",\"name\":\"codecov-circleci-orb\"\ + ,\"full_name\":\"codecov/codecov-circleci-orb\",\"private\":false,\"owner\"\ + :{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/codecov-circleci-orb\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/codecov-circleci-orb/deployments\"\ + ,\"created_at\":\"2018-11-12T02:50:24Z\",\"updated_at\":\"2020-10-09T12:43:30Z\"\ + ,\"pushed_at\":\"2020-10-09T12:47:04Z\",\"git_url\":\"git://github.com/codecov/codecov-circleci-orb.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/codecov-circleci-orb.git\",\"clone_url\"\ + :\"https://github.com/codecov/codecov-circleci-orb.git\",\"svn_url\":\"https://github.com/codecov/codecov-circleci-orb\"\ + ,\"homepage\":null,\"size\":64,\"stargazers_count\":11,\"watchers_count\":11,\"\ + language\":\"Python\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":19,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":3,\"license\"\ + :null,\"forks\":19,\"open_issues\":3,\"watchers\":11,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":169578228,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkxNjk1NzgyMjg=\",\"name\":\"codecov-client\",\"\ + full_name\":\"codecov/codecov-client\",\"private\":true,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/codecov-client\"\ + ,\"description\":\"New Codecov Client\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/codecov-client\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/codecov-client/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/codecov-client/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/codecov-client/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/codecov-client/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/codecov-client/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/codecov-client/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/codecov-client/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/codecov-client/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/codecov-client/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/codecov-client/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/codecov-client/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/codecov-client/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/codecov-client/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/codecov-client/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/codecov-client/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/codecov-client/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/codecov-client/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/codecov-client/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/codecov-client/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/codecov-client/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/codecov-client/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/codecov-client/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/codecov-client/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/codecov-client/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/codecov-client/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/codecov-client/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/codecov-client/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/codecov-client/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/codecov-client/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/codecov-client/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/codecov-client/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/codecov-client/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/codecov-client/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/codecov-client/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/codecov-client/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/codecov-client/deployments\"\ + ,\"created_at\":\"2019-02-07T13:44:46Z\",\"updated_at\":\"2020-10-14T17:09:29Z\"\ + ,\"pushed_at\":\"2020-10-14T17:32:02Z\",\"git_url\":\"git://github.com/codecov/codecov-client.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/codecov-client.git\",\"clone_url\":\"\ + https://github.com/codecov/codecov-client.git\",\"svn_url\":\"https://github.com/codecov/codecov-client\"\ + ,\"homepage\":\"\",\"size\":6656,\"stargazers_count\":0,\"watchers_count\":0,\"\ + language\":\"TypeScript\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":true,\"has_pages\":false,\"forks_count\":0,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":10,\"license\"\ + :null,\"forks\":0,\"open_issues\":10,\"watchers\":0,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":79283223,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnk3OTI4MzIyMw==\",\"name\":\"codecov-exe\",\"full_name\"\ + :\"codecov/codecov-exe\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/codecov-exe\"\ + ,\"description\":\".exe report uploader for Codecov https://codecov.io\",\"\ + fork\":false,\"url\":\"https://api.github.com/repos/codecov/codecov-exe\",\"\ + forks_url\":\"https://api.github.com/repos/codecov/codecov-exe/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/codecov-exe/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/codecov-exe/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/codecov-exe/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/codecov-exe/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/codecov-exe/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/codecov-exe/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/codecov-exe/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/codecov-exe/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/codecov-exe/tags\",\"blobs_url\"\ + :\"https://api.github.com/repos/codecov/codecov-exe/git/blobs{/sha}\",\"git_tags_url\"\ + :\"https://api.github.com/repos/codecov/codecov-exe/git/tags{/sha}\",\"git_refs_url\"\ + :\"https://api.github.com/repos/codecov/codecov-exe/git/refs{/sha}\",\"trees_url\"\ + :\"https://api.github.com/repos/codecov/codecov-exe/git/trees{/sha}\",\"statuses_url\"\ + :\"https://api.github.com/repos/codecov/codecov-exe/statuses/{sha}\",\"languages_url\"\ + :\"https://api.github.com/repos/codecov/codecov-exe/languages\",\"stargazers_url\"\ + :\"https://api.github.com/repos/codecov/codecov-exe/stargazers\",\"contributors_url\"\ + :\"https://api.github.com/repos/codecov/codecov-exe/contributors\",\"subscribers_url\"\ + :\"https://api.github.com/repos/codecov/codecov-exe/subscribers\",\"subscription_url\"\ + :\"https://api.github.com/repos/codecov/codecov-exe/subscription\",\"commits_url\"\ + :\"https://api.github.com/repos/codecov/codecov-exe/commits{/sha}\",\"git_commits_url\"\ + :\"https://api.github.com/repos/codecov/codecov-exe/git/commits{/sha}\",\"comments_url\"\ + :\"https://api.github.com/repos/codecov/codecov-exe/comments{/number}\",\"issue_comment_url\"\ + :\"https://api.github.com/repos/codecov/codecov-exe/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/codecov-exe/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/codecov-exe/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/codecov-exe/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/codecov-exe/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/codecov-exe/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/codecov-exe/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/codecov-exe/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/codecov-exe/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/codecov-exe/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/codecov-exe/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/codecov-exe/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/codecov-exe/deployments\"\ + ,\"created_at\":\"2017-01-17T23:27:49Z\",\"updated_at\":\"2020-10-12T14:15:48Z\"\ + ,\"pushed_at\":\"2020-10-08T06:16:07Z\",\"git_url\":\"git://github.com/codecov/codecov-exe.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/codecov-exe.git\",\"clone_url\":\"https://github.com/codecov/codecov-exe.git\"\ + ,\"svn_url\":\"https://github.com/codecov/codecov-exe\",\"homepage\":\"\",\"\ + size\":747,\"stargazers_count\":19,\"watchers_count\":19,\"language\":\"C#\"\ + ,\"has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":23,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":11,\"license\":{\"key\":\"mit\"\ + ,\"name\":\"MIT License\",\"spdx_id\":\"MIT\",\"url\":\"https://api.github.com/licenses/mit\"\ + ,\"node_id\":\"MDc6TGljZW5zZTEz\"},\"forks\":23,\"open_issues\":11,\"watchers\"\ + :19,\"default_branch\":\"main\",\"permissions\":{\"admin\":false,\"push\"\ + :true,\"pull\":true}},{\"id\":143443068,\"node_id\":\"MDEwOlJlcG9zaXRvcnkxNDM0NDMwNjg=\"\ + ,\"name\":\"codecov-marketing\",\"full_name\":\"codecov/codecov-marketing\"\ + ,\"private\":true,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\"\ + :\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/codecov-marketing\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/codecov-marketing\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/codecov-marketing/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/codecov-marketing/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/codecov-marketing/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/codecov-marketing/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/codecov-marketing/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/codecov-marketing/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/codecov-marketing/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/codecov-marketing/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/codecov-marketing/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/codecov-marketing/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/codecov-marketing/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/codecov-marketing/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/codecov-marketing/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/codecov-marketing/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/codecov-marketing/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/codecov-marketing/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/codecov-marketing/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/codecov-marketing/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/codecov-marketing/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/codecov-marketing/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/codecov-marketing/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/codecov-marketing/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/codecov-marketing/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/codecov-marketing/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/codecov-marketing/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/codecov-marketing/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/codecov-marketing/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/codecov-marketing/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/codecov-marketing/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/codecov-marketing/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/codecov-marketing/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/codecov-marketing/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/codecov-marketing/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/codecov-marketing/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/codecov-marketing/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/codecov-marketing/deployments\"\ + ,\"created_at\":\"2018-08-03T15:19:49Z\",\"updated_at\":\"2020-10-14T04:03:50Z\"\ + ,\"pushed_at\":\"2020-10-14T16:22:03Z\",\"git_url\":\"git://github.com/codecov/codecov-marketing.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/codecov-marketing.git\",\"clone_url\"\ + :\"https://github.com/codecov/codecov-marketing.git\",\"svn_url\":\"https://github.com/codecov/codecov-marketing\"\ + ,\"homepage\":null,\"size\":2858,\"stargazers_count\":0,\"watchers_count\":0,\"\ + language\":\"Vue\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":0,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":10,\"license\"\ + :null,\"forks\":0,\"open_issues\":10,\"watchers\":0,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":40017868,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnk0MDAxNzg2OA==\",\"name\":\"codecov-perl\",\"\ + full_name\":\"codecov/codecov-perl\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/codecov-perl\"\ + ,\"description\":\"Backend for Codecov reporting of coverage\",\"fork\":false,\"\ + url\":\"https://api.github.com/repos/codecov/codecov-perl\",\"forks_url\":\"\ + https://api.github.com/repos/codecov/codecov-perl/forks\",\"keys_url\":\"https://api.github.com/repos/codecov/codecov-perl/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/codecov-perl/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/codecov-perl/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/codecov-perl/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/codecov-perl/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/codecov-perl/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/codecov-perl/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/codecov-perl/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/codecov-perl/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/codecov-perl/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/codecov-perl/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/codecov-perl/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/codecov-perl/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/codecov-perl/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/codecov-perl/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/codecov-perl/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/codecov-perl/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/codecov-perl/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/codecov-perl/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/codecov-perl/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/codecov-perl/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/codecov-perl/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/codecov-perl/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/codecov-perl/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/codecov-perl/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/codecov-perl/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/codecov-perl/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/codecov-perl/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/codecov-perl/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/codecov-perl/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/codecov-perl/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/codecov-perl/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/codecov-perl/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/codecov-perl/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/codecov-perl/deployments\"\ + ,\"created_at\":\"2015-07-31T17:57:48Z\",\"updated_at\":\"2020-09-09T17:09:05Z\"\ + ,\"pushed_at\":\"2020-09-09T17:09:01Z\",\"git_url\":\"git://github.com/codecov/codecov-perl.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/codecov-perl.git\",\"clone_url\":\"https://github.com/codecov/codecov-perl.git\"\ + ,\"svn_url\":\"https://github.com/codecov/codecov-perl\",\"homepage\":\"https://metacpan.org/pod/Devel::Cover::Report::Codecov\"\ + ,\"size\":156,\"stargazers_count\":12,\"watchers_count\":12,\"language\":\"\ + Perl\",\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"\ + has_wiki\":false,\"has_pages\":false,\"forks_count\":11,\"mirror_url\":null,\"\ + archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\":{\"\ + key\":\"mit\",\"name\":\"MIT License\",\"spdx_id\":\"MIT\",\"url\":\"https://api.github.com/licenses/mit\"\ + ,\"node_id\":\"MDc6TGljZW5zZTEz\"},\"forks\":11,\"open_issues\":0,\"watchers\"\ + :12,\"default_branch\":\"main\",\"permissions\":{\"admin\":true,\"push\":true,\"\ + pull\":true}},{\"id\":22685735,\"node_id\":\"MDEwOlJlcG9zaXRvcnkyMjY4NTczNQ==\"\ + ,\"name\":\"codecov-python\",\"full_name\":\"codecov/codecov-python\",\"private\"\ + :false,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/codecov-python\"\ + ,\"description\":\"Python report uploader for Codecov\",\"fork\":false,\"url\"\ + :\"https://api.github.com/repos/codecov/codecov-python\",\"forks_url\":\"https://api.github.com/repos/codecov/codecov-python/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/codecov-python/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/codecov-python/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/codecov-python/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/codecov-python/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/codecov-python/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/codecov-python/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/codecov-python/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/codecov-python/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/codecov-python/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/codecov-python/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/codecov-python/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/codecov-python/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/codecov-python/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/codecov-python/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/codecov-python/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/codecov-python/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/codecov-python/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/codecov-python/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/codecov-python/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/codecov-python/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/codecov-python/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/codecov-python/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/codecov-python/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/codecov-python/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/codecov-python/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/codecov-python/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/codecov-python/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/codecov-python/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/codecov-python/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/codecov-python/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/codecov-python/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/codecov-python/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/codecov-python/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/codecov-python/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/codecov-python/deployments\"\ + ,\"created_at\":\"2014-08-06T14:33:18Z\",\"updated_at\":\"2020-10-13T02:45:32Z\"\ + ,\"pushed_at\":\"2020-10-13T14:01:24Z\",\"git_url\":\"git://github.com/codecov/codecov-python.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/codecov-python.git\",\"clone_url\":\"\ + https://github.com/codecov/codecov-python.git\",\"svn_url\":\"https://github.com/codecov/codecov-python\"\ + ,\"homepage\":\"https://codecov.io\",\"size\":654,\"stargazers_count\":155,\"\ + watchers_count\":155,\"language\":\"Python\",\"has_issues\":true,\"has_projects\"\ + :true,\"has_downloads\":true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\"\ + :117,\"mirror_url\":null,\"archived\":false,\"disabled\":false,\"open_issues_count\"\ + :55,\"license\":{\"key\":\"apache-2.0\",\"name\":\"Apache License 2.0\",\"spdx_id\"\ + :\"Apache-2.0\",\"url\":\"https://api.github.com/licenses/apache-2.0\",\"node_id\"\ + :\"MDc6TGljZW5zZTI=\"},\"forks\":117,\"open_issues\":55,\"watchers\":155,\"\ + default_branch\":\"main\",\"permissions\":{\"admin\":true,\"push\":true,\"\ + pull\":true}},{\"id\":23790467,\"node_id\":\"MDEwOlJlcG9zaXRvcnkyMzc5MDQ2Nw==\"\ + ,\"name\":\"codecov-ruby\",\"full_name\":\"codecov/codecov-ruby\",\"private\"\ + :false,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/codecov-ruby\"\ + ,\"description\":\"Ruby uploader for Codecov\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/codecov-ruby\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/codecov-ruby/forks\",\"\ + keys_url\":\"https://api.github.com/repos/codecov/codecov-ruby/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/codecov-ruby/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/codecov-ruby/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/codecov-ruby/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/codecov-ruby/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/codecov-ruby/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/codecov-ruby/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/codecov-ruby/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/codecov-ruby/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/codecov-ruby/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/codecov-ruby/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/codecov-ruby/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/codecov-ruby/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/codecov-ruby/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/codecov-ruby/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/codecov-ruby/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/codecov-ruby/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/codecov-ruby/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/codecov-ruby/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/codecov-ruby/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/codecov-ruby/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/codecov-ruby/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/codecov-ruby/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/codecov-ruby/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/codecov-ruby/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/codecov-ruby/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/codecov-ruby/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/codecov-ruby/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/codecov-ruby/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/codecov-ruby/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/codecov-ruby/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/codecov-ruby/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/codecov-ruby/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/codecov-ruby/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/codecov-ruby/deployments\"\ + ,\"created_at\":\"2014-09-08T12:49:42Z\",\"updated_at\":\"2020-10-13T14:04:21Z\"\ + ,\"pushed_at\":\"2020-10-13T14:04:18Z\",\"git_url\":\"git://github.com/codecov/codecov-ruby.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/codecov-ruby.git\",\"clone_url\":\"https://github.com/codecov/codecov-ruby.git\"\ + ,\"svn_url\":\"https://github.com/codecov/codecov-ruby\",\"homepage\":\"https://codecov.io\"\ + ,\"size\":229,\"stargazers_count\":57,\"watchers_count\":57,\"language\":\"\ + Ruby\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":65,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":2,\"license\":{\"key\":\"mit\"\ + ,\"name\":\"MIT License\",\"spdx_id\":\"MIT\",\"url\":\"https://api.github.com/licenses/mit\"\ + ,\"node_id\":\"MDc6TGljZW5zZTEz\"},\"forks\":65,\"open_issues\":2,\"watchers\"\ + :57,\"default_branch\":\"main\",\"permissions\":{\"admin\":true,\"push\":true,\"\ + pull\":true}},{\"id\":263925491,\"node_id\":\"MDEwOlJlcG9zaXRvcnkyNjM5MjU0OTE=\"\ + ,\"name\":\"codecov-typescript-client\",\"full_name\":\"codecov/codecov-typescript-client\"\ + ,\"private\":true,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\"\ + :\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/codecov-typescript-client\"\ + ,\"description\":\"TypeScript client for accessing Codecov's internal REST API.\ + \ Auto-generated with OpenAPI schemas.\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/codecov-typescript-client\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/codecov-typescript-client/deployments\"\ + ,\"created_at\":\"2020-05-14T13:39:05Z\",\"updated_at\":\"2020-05-15T19:52:23Z\"\ + ,\"pushed_at\":\"2020-05-15T19:52:20Z\",\"git_url\":\"git://github.com/codecov/codecov-typescript-client.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/codecov-typescript-client.git\",\"clone_url\"\ + :\"https://github.com/codecov/codecov-typescript-client.git\",\"svn_url\":\"\ + https://github.com/codecov/codecov-typescript-client\",\"homepage\":null,\"\ + size\":27,\"stargazers_count\":0,\"watchers_count\":0,\"language\":\"TypeScript\"\ + ,\"has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\"\ + :true,\"has_pages\":false,\"forks_count\":0,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"forks\"\ + :0,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":265929273,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkyNjU5MjkyNzM=\",\"name\":\"codecov-ui\",\"full_name\"\ + :\"codecov/codecov-ui\",\"private\":true,\"owner\":{\"login\":\"codecov\",\"\ + id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/codecov-ui\"\ + ,\"description\":\"Repository of reusable Vue components\",\"fork\":false,\"\ + url\":\"https://api.github.com/repos/codecov/codecov-ui\",\"forks_url\":\"https://api.github.com/repos/codecov/codecov-ui/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/codecov-ui/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/codecov-ui/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/codecov-ui/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/codecov-ui/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/codecov-ui/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/codecov-ui/events\",\"\ + assignees_url\":\"https://api.github.com/repos/codecov/codecov-ui/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/codecov-ui/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/codecov-ui/tags\",\"blobs_url\"\ + :\"https://api.github.com/repos/codecov/codecov-ui/git/blobs{/sha}\",\"git_tags_url\"\ + :\"https://api.github.com/repos/codecov/codecov-ui/git/tags{/sha}\",\"git_refs_url\"\ + :\"https://api.github.com/repos/codecov/codecov-ui/git/refs{/sha}\",\"trees_url\"\ + :\"https://api.github.com/repos/codecov/codecov-ui/git/trees{/sha}\",\"statuses_url\"\ + :\"https://api.github.com/repos/codecov/codecov-ui/statuses/{sha}\",\"languages_url\"\ + :\"https://api.github.com/repos/codecov/codecov-ui/languages\",\"stargazers_url\"\ + :\"https://api.github.com/repos/codecov/codecov-ui/stargazers\",\"contributors_url\"\ + :\"https://api.github.com/repos/codecov/codecov-ui/contributors\",\"subscribers_url\"\ + :\"https://api.github.com/repos/codecov/codecov-ui/subscribers\",\"subscription_url\"\ + :\"https://api.github.com/repos/codecov/codecov-ui/subscription\",\"commits_url\"\ + :\"https://api.github.com/repos/codecov/codecov-ui/commits{/sha}\",\"git_commits_url\"\ + :\"https://api.github.com/repos/codecov/codecov-ui/git/commits{/sha}\",\"comments_url\"\ + :\"https://api.github.com/repos/codecov/codecov-ui/comments{/number}\",\"issue_comment_url\"\ + :\"https://api.github.com/repos/codecov/codecov-ui/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/codecov-ui/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/codecov-ui/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/codecov-ui/merges\",\"\ + archive_url\":\"https://api.github.com/repos/codecov/codecov-ui/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/codecov-ui/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/codecov-ui/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/codecov-ui/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/codecov-ui/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/codecov-ui/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/codecov-ui/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/codecov-ui/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/codecov-ui/deployments\"\ + ,\"created_at\":\"2020-05-21T19:01:14Z\",\"updated_at\":\"2020-10-02T13:51:27Z\"\ + ,\"pushed_at\":\"2020-10-14T04:14:40Z\",\"git_url\":\"git://github.com/codecov/codecov-ui.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/codecov-ui.git\",\"clone_url\":\"https://github.com/codecov/codecov-ui.git\"\ + ,\"svn_url\":\"https://github.com/codecov/codecov-ui\",\"homepage\":null,\"\ + size\":4152,\"stargazers_count\":1,\"watchers_count\":1,\"language\":\"TypeScript\"\ + ,\"has_issues\":true,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":0,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":10,\"license\":null,\"forks\"\ + :0,\"open_issues\":10,\"watchers\":1,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":22071731,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkyMjA3MTczMQ==\",\"name\":\"codecov.io\",\"full_name\"\ + :\"codecov/codecov.io\",\"private\":true,\"owner\":{\"login\":\"codecov\",\"\ + id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/codecov.io\"\ + ,\"description\":\"Project Core\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/codecov.io\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/codecov.io/forks\",\"\ + keys_url\":\"https://api.github.com/repos/codecov/codecov.io/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/codecov.io/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/codecov.io/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/codecov.io/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/codecov.io/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/codecov.io/events\",\"\ + assignees_url\":\"https://api.github.com/repos/codecov/codecov.io/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/codecov.io/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/codecov.io/tags\",\"blobs_url\"\ + :\"https://api.github.com/repos/codecov/codecov.io/git/blobs{/sha}\",\"git_tags_url\"\ + :\"https://api.github.com/repos/codecov/codecov.io/git/tags{/sha}\",\"git_refs_url\"\ + :\"https://api.github.com/repos/codecov/codecov.io/git/refs{/sha}\",\"trees_url\"\ + :\"https://api.github.com/repos/codecov/codecov.io/git/trees{/sha}\",\"statuses_url\"\ + :\"https://api.github.com/repos/codecov/codecov.io/statuses/{sha}\",\"languages_url\"\ + :\"https://api.github.com/repos/codecov/codecov.io/languages\",\"stargazers_url\"\ + :\"https://api.github.com/repos/codecov/codecov.io/stargazers\",\"contributors_url\"\ + :\"https://api.github.com/repos/codecov/codecov.io/contributors\",\"subscribers_url\"\ + :\"https://api.github.com/repos/codecov/codecov.io/subscribers\",\"subscription_url\"\ + :\"https://api.github.com/repos/codecov/codecov.io/subscription\",\"commits_url\"\ + :\"https://api.github.com/repos/codecov/codecov.io/commits{/sha}\",\"git_commits_url\"\ + :\"https://api.github.com/repos/codecov/codecov.io/git/commits{/sha}\",\"comments_url\"\ + :\"https://api.github.com/repos/codecov/codecov.io/comments{/number}\",\"issue_comment_url\"\ + :\"https://api.github.com/repos/codecov/codecov.io/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/codecov.io/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/codecov.io/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/codecov.io/merges\",\"\ + archive_url\":\"https://api.github.com/repos/codecov/codecov.io/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/codecov.io/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/codecov.io/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/codecov.io/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/codecov.io/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/codecov.io/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/codecov.io/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/codecov.io/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/codecov.io/deployments\"\ + ,\"created_at\":\"2014-07-21T16:30:43Z\",\"updated_at\":\"2020-10-06T15:47:13Z\"\ + ,\"pushed_at\":\"2020-10-09T17:17:22Z\",\"git_url\":\"git://github.com/codecov/codecov.io.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/codecov.io.git\",\"clone_url\":\"https://github.com/codecov/codecov.io.git\"\ + ,\"svn_url\":\"https://github.com/codecov/codecov.io\",\"homepage\":\"https://codecov.io\"\ + ,\"size\":39739,\"stargazers_count\":1,\"watchers_count\":1,\"language\":\"\ + Python\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"\ + has_wiki\":false,\"has_pages\":false,\"forks_count\":0,\"mirror_url\":null,\"\ + archived\":false,\"disabled\":false,\"open_issues_count\":65,\"license\":null,\"\ + forks\":0,\"open_issues\":65,\"watchers\":1,\"default_branch\":\"main\",\"\ + permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":204493189,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkyMDQ0OTMxODk=\",\"name\":\"cpp-11-standard\"\ + ,\"full_name\":\"codecov/cpp-11-standard\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/cpp-11-standard\"\ + ,\"description\":\"Codecov coverage standard for c++ 11\",\"fork\":false,\"\ + url\":\"https://api.github.com/repos/codecov/cpp-11-standard\",\"forks_url\"\ + :\"https://api.github.com/repos/codecov/cpp-11-standard/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/cpp-11-standard/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/cpp-11-standard/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/cpp-11-standard/deployments\"\ + ,\"created_at\":\"2019-08-26T14:26:30Z\",\"updated_at\":\"2020-10-13T20:05:56Z\"\ + ,\"pushed_at\":\"2020-10-13T20:05:54Z\",\"git_url\":\"git://github.com/codecov/cpp-11-standard.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/cpp-11-standard.git\",\"clone_url\":\"\ + https://github.com/codecov/cpp-11-standard.git\",\"svn_url\":\"https://github.com/codecov/cpp-11-standard\"\ + ,\"homepage\":null,\"size\":225,\"stargazers_count\":1,\"watchers_count\":1,\"\ + language\":\"C++\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":true,\"has_pages\":false,\"forks_count\":3,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\"\ + :null,\"forks\":3,\"open_issues\":0,\"watchers\":1,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":265252321,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkyNjUyNTIzMjE=\",\"name\":\"critical-path-research\"\ + ,\"full_name\":\"codecov/critical-path-research\",\"private\":true,\"owner\"\ + :{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/critical-path-research\"\ + ,\"description\":\"A place to collect research and experiments on critical path\ + \ coverage\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/critical-path-research\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/critical-path-research/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/critical-path-research/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/critical-path-research/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/critical-path-research/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/critical-path-research/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/critical-path-research/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/critical-path-research/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/critical-path-research/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/critical-path-research/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/critical-path-research/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/critical-path-research/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/critical-path-research/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/critical-path-research/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/critical-path-research/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/critical-path-research/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/critical-path-research/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/critical-path-research/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/critical-path-research/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/critical-path-research/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/critical-path-research/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/critical-path-research/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/critical-path-research/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/critical-path-research/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/critical-path-research/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/critical-path-research/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/critical-path-research/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/critical-path-research/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/critical-path-research/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/critical-path-research/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/critical-path-research/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/critical-path-research/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/critical-path-research/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/critical-path-research/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/critical-path-research/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/critical-path-research/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/critical-path-research/deployments\"\ + ,\"created_at\":\"2020-05-19T13:17:55Z\",\"updated_at\":\"2020-08-23T17:30:28Z\"\ + ,\"pushed_at\":\"2020-10-06T02:09:25Z\",\"git_url\":\"git://github.com/codecov/critical-path-research.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/critical-path-research.git\",\"clone_url\"\ + :\"https://github.com/codecov/critical-path-research.git\",\"svn_url\":\"https://github.com/codecov/critical-path-research\"\ + ,\"homepage\":null,\"size\":13321,\"stargazers_count\":0,\"watchers_count\"\ + :0,\"language\":\"JavaScript\",\"has_issues\":true,\"has_projects\":true,\"\ + has_downloads\":true,\"has_wiki\":true,\"has_pages\":false,\"forks_count\":0,\"\ + mirror_url\":null,\"archived\":false,\"disabled\":false,\"open_issues_count\"\ + :5,\"license\":null,\"forks\":0,\"open_issues\":5,\"watchers\":0,\"default_branch\"\ + :\"main\",\"permissions\":{\"admin\":false,\"push\":true,\"pull\":true}},{\"\ + id\":36365816,\"node_id\":\"MDEwOlJlcG9zaXRvcnkzNjM2NTgxNg==\",\"name\":\"dart\"\ + ,\"full_name\":\"codecov/dart\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/dart\"\ + ,\"description\":\"Codecov Dart coverage report uploader\",\"fork\":false,\"\ + url\":\"https://api.github.com/repos/codecov/dart\",\"forks_url\":\"https://api.github.com/repos/codecov/dart/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/dart/keys{/key_id}\",\"\ + collaborators_url\":\"https://api.github.com/repos/codecov/dart/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/dart/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/dart/hooks\",\"issue_events_url\":\"\ + https://api.github.com/repos/codecov/dart/issues/events{/number}\",\"events_url\"\ + :\"https://api.github.com/repos/codecov/dart/events\",\"assignees_url\":\"https://api.github.com/repos/codecov/dart/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/dart/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/dart/tags\",\"blobs_url\"\ + :\"https://api.github.com/repos/codecov/dart/git/blobs{/sha}\",\"git_tags_url\"\ + :\"https://api.github.com/repos/codecov/dart/git/tags{/sha}\",\"git_refs_url\"\ + :\"https://api.github.com/repos/codecov/dart/git/refs{/sha}\",\"trees_url\"\ + :\"https://api.github.com/repos/codecov/dart/git/trees{/sha}\",\"statuses_url\"\ + :\"https://api.github.com/repos/codecov/dart/statuses/{sha}\",\"languages_url\"\ + :\"https://api.github.com/repos/codecov/dart/languages\",\"stargazers_url\"\ + :\"https://api.github.com/repos/codecov/dart/stargazers\",\"contributors_url\"\ + :\"https://api.github.com/repos/codecov/dart/contributors\",\"subscribers_url\"\ + :\"https://api.github.com/repos/codecov/dart/subscribers\",\"subscription_url\"\ + :\"https://api.github.com/repos/codecov/dart/subscription\",\"commits_url\"\ + :\"https://api.github.com/repos/codecov/dart/commits{/sha}\",\"git_commits_url\"\ + :\"https://api.github.com/repos/codecov/dart/git/commits{/sha}\",\"comments_url\"\ + :\"https://api.github.com/repos/codecov/dart/comments{/number}\",\"issue_comment_url\"\ + :\"https://api.github.com/repos/codecov/dart/issues/comments{/number}\",\"contents_url\"\ + :\"https://api.github.com/repos/codecov/dart/contents/{+path}\",\"compare_url\"\ + :\"https://api.github.com/repos/codecov/dart/compare/{base}...{head}\",\"merges_url\"\ + :\"https://api.github.com/repos/codecov/dart/merges\",\"archive_url\":\"https://api.github.com/repos/codecov/dart/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/dart/downloads\",\"\ + issues_url\":\"https://api.github.com/repos/codecov/dart/issues{/number}\",\"\ + pulls_url\":\"https://api.github.com/repos/codecov/dart/pulls{/number}\",\"\ + milestones_url\":\"https://api.github.com/repos/codecov/dart/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/dart/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/dart/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/dart/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/dart/deployments\"\ + ,\"created_at\":\"2015-05-27T12:37:41Z\",\"updated_at\":\"2020-09-21T14:02:05Z\"\ + ,\"pushed_at\":\"2020-09-21T14:02:01Z\",\"git_url\":\"git://github.com/codecov/dart.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/dart.git\",\"clone_url\":\"https://github.com/codecov/dart.git\"\ + ,\"svn_url\":\"https://github.com/codecov/dart\",\"homepage\":\"https://codecov.io\"\ + ,\"size\":77,\"stargazers_count\":18,\"watchers_count\":18,\"language\":\"Dart\"\ + ,\"has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":23,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":5,\"license\":{\"key\":\"other\"\ + ,\"name\":\"Other\",\"spdx_id\":\"NOASSERTION\",\"url\":null,\"node_id\":\"\ + MDc6TGljZW5zZTA=\"},\"forks\":23,\"open_issues\":5,\"watchers\":18,\"default_branch\"\ + :\"main\",\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"\ + id\":198240301,\"node_id\":\"MDEwOlJlcG9zaXRvcnkxOTgyNDAzMDE=\",\"name\":\"\ + django-vue-dockerized-celery\",\"full_name\":\"codecov/django-vue-dockerized-celery\"\ + ,\"private\":true,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\"\ + :\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/django-vue-dockerized-celery\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/django-vue-dockerized-celery/deployments\"\ + ,\"created_at\":\"2019-07-22T14:29:54Z\",\"updated_at\":\"2019-07-22T17:32:58Z\"\ + ,\"pushed_at\":\"2020-07-08T01:10:54Z\",\"git_url\":\"git://github.com/codecov/django-vue-dockerized-celery.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/django-vue-dockerized-celery.git\",\"\ + clone_url\":\"https://github.com/codecov/django-vue-dockerized-celery.git\"\ + ,\"svn_url\":\"https://github.com/codecov/django-vue-dockerized-celery\",\"\ + homepage\":null,\"size\":219,\"stargazers_count\":0,\"watchers_count\":0,\"\ + language\":\"Python\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":true,\"has_pages\":false,\"forks_count\":1,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":1,\"license\"\ + :null,\"forks\":1,\"open_issues\":1,\"watchers\":0,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":39341336,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkzOTM0MTMzNg==\",\"name\":\"enterprise\",\"full_name\"\ + :\"codecov/enterprise\",\"private\":false,\"owner\":{\"login\":\"codecov\",\"\ + id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/enterprise\"\ + ,\"description\":\"Code coverage done right.\xAE On-premise enterprise version.\"\ + ,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/enterprise\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/enterprise/forks\",\"\ + keys_url\":\"https://api.github.com/repos/codecov/enterprise/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/enterprise/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/enterprise/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/enterprise/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/enterprise/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/enterprise/events\",\"\ + assignees_url\":\"https://api.github.com/repos/codecov/enterprise/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/enterprise/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/enterprise/tags\",\"blobs_url\"\ + :\"https://api.github.com/repos/codecov/enterprise/git/blobs{/sha}\",\"git_tags_url\"\ + :\"https://api.github.com/repos/codecov/enterprise/git/tags{/sha}\",\"git_refs_url\"\ + :\"https://api.github.com/repos/codecov/enterprise/git/refs{/sha}\",\"trees_url\"\ + :\"https://api.github.com/repos/codecov/enterprise/git/trees{/sha}\",\"statuses_url\"\ + :\"https://api.github.com/repos/codecov/enterprise/statuses/{sha}\",\"languages_url\"\ + :\"https://api.github.com/repos/codecov/enterprise/languages\",\"stargazers_url\"\ + :\"https://api.github.com/repos/codecov/enterprise/stargazers\",\"contributors_url\"\ + :\"https://api.github.com/repos/codecov/enterprise/contributors\",\"subscribers_url\"\ + :\"https://api.github.com/repos/codecov/enterprise/subscribers\",\"subscription_url\"\ + :\"https://api.github.com/repos/codecov/enterprise/subscription\",\"commits_url\"\ + :\"https://api.github.com/repos/codecov/enterprise/commits{/sha}\",\"git_commits_url\"\ + :\"https://api.github.com/repos/codecov/enterprise/git/commits{/sha}\",\"comments_url\"\ + :\"https://api.github.com/repos/codecov/enterprise/comments{/number}\",\"issue_comment_url\"\ + :\"https://api.github.com/repos/codecov/enterprise/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/enterprise/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/enterprise/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/enterprise/merges\",\"\ + archive_url\":\"https://api.github.com/repos/codecov/enterprise/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/enterprise/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/enterprise/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/enterprise/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/enterprise/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/enterprise/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/enterprise/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/enterprise/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/enterprise/deployments\"\ + ,\"created_at\":\"2015-07-19T16:57:00Z\",\"updated_at\":\"2020-10-13T02:50:46Z\"\ + ,\"pushed_at\":\"2020-10-05T18:12:20Z\",\"git_url\":\"git://github.com/codecov/enterprise.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/enterprise.git\",\"clone_url\":\"https://github.com/codecov/enterprise.git\"\ + ,\"svn_url\":\"https://github.com/codecov/enterprise\",\"homepage\":\"https://codecov.io/enterprise\"\ + ,\"size\":1393894,\"stargazers_count\":54,\"watchers_count\":54,\"language\"\ + :\"Shell\",\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"\ + has_wiki\":false,\"has_pages\":false,\"forks_count\":45,\"mirror_url\":null,\"\ + archived\":false,\"disabled\":false,\"open_issues_count\":2,\"license\":{\"\ + key\":\"other\",\"name\":\"Other\",\"spdx_id\":\"NOASSERTION\",\"url\":null,\"\ + node_id\":\"MDc6TGljZW5zZTA=\"},\"forks\":45,\"open_issues\":2,\"watchers\"\ + :54,\"default_branch\":\"v4.5\",\"permissions\":{\"admin\":true,\"push\":true,\"\ + pull\":true}},{\"id\":45991588,\"node_id\":\"MDEwOlJlcG9zaXRvcnk0NTk5MTU4OA==\"\ + ,\"name\":\"example-android\",\"full_name\":\"codecov/example-android\",\"private\"\ + :false,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-android\"\ + ,\"description\":\"Android code coverage example with https://codecov.io\",\"\ + fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-android\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-android/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-android/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-android/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-android/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-android/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-android/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-android/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-android/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-android/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-android/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/example-android/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-android/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-android/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-android/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-android/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-android/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-android/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-android/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-android/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-android/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-android/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-android/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-android/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-android/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-android/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-android/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-android/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-android/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-android/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-android/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-android/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-android/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-android/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-android/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-android/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-android/deployments\"\ + ,\"created_at\":\"2015-11-11T15:55:13Z\",\"updated_at\":\"2020-09-26T07:21:49Z\"\ + ,\"pushed_at\":\"2020-09-04T02:46:54Z\",\"git_url\":\"git://github.com/codecov/example-android.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-android.git\",\"clone_url\":\"\ + https://github.com/codecov/example-android.git\",\"svn_url\":\"https://github.com/codecov/example-android\"\ + ,\"homepage\":null,\"size\":138,\"stargazers_count\":131,\"watchers_count\"\ + :131,\"language\":\"Java\",\"has_issues\":false,\"has_projects\":false,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":108,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":2,\"license\"\ + :{\"key\":\"apache-2.0\",\"name\":\"Apache License 2.0\",\"spdx_id\":\"Apache-2.0\"\ + ,\"url\":\"https://api.github.com/licenses/apache-2.0\",\"node_id\":\"MDc6TGljZW5zZTI=\"\ + },\"forks\":108,\"open_issues\":2,\"watchers\":131,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":44101505,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnk0NDEwMTUwNQ==\",\"name\":\"example-bash\",\"\ + full_name\":\"codecov/example-bash\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-bash\"\ + ,\"description\":\"Codecov: Bash/Shell coverage example\",\"fork\":false,\"\ + url\":\"https://api.github.com/repos/codecov/example-bash\",\"forks_url\":\"\ + https://api.github.com/repos/codecov/example-bash/forks\",\"keys_url\":\"https://api.github.com/repos/codecov/example-bash/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-bash/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-bash/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/example-bash/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/example-bash/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-bash/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-bash/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-bash/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-bash/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-bash/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-bash/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-bash/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-bash/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-bash/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-bash/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-bash/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-bash/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-bash/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-bash/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-bash/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-bash/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-bash/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-bash/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-bash/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-bash/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-bash/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-bash/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-bash/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-bash/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-bash/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-bash/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-bash/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-bash/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-bash/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-bash/deployments\"\ + ,\"created_at\":\"2015-10-12T10:50:49Z\",\"updated_at\":\"2020-09-29T16:35:57Z\"\ + ,\"pushed_at\":\"2020-08-27T03:11:48Z\",\"git_url\":\"git://github.com/codecov/example-bash.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-bash.git\",\"clone_url\":\"https://github.com/codecov/example-bash.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-bash\",\"homepage\":\"https://codecov.io\"\ + ,\"size\":7,\"stargazers_count\":36,\"watchers_count\":36,\"language\":\"Shell\"\ + ,\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":34,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":2,\"license\":null,\"forks\"\ + :34,\"open_issues\":2,\"watchers\":36,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":35053332,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkzNTA1MzMzMg==\",\"name\":\"example-c\",\"full_name\":\"\ + codecov/example-c\",\"private\":false,\"owner\":{\"login\":\"codecov\",\"id\"\ + :8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"\ + https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\"\ + ,\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-c\"\ + ,\"description\":\"Upload reports to Codecov using C/C++\",\"fork\":false,\"\ + url\":\"https://api.github.com/repos/codecov/example-c\",\"forks_url\":\"https://api.github.com/repos/codecov/example-c/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-c/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-c/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-c/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/example-c/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/example-c/issues/events{/number}\",\"\ + events_url\":\"https://api.github.com/repos/codecov/example-c/events\",\"assignees_url\"\ + :\"https://api.github.com/repos/codecov/example-c/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/example-c/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/example-c/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/example-c/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-c/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-c/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-c/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-c/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-c/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-c/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-c/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-c/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-c/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-c/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-c/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-c/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-c/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-c/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-c/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-c/merges\",\"\ + archive_url\":\"https://api.github.com/repos/codecov/example-c/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-c/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-c/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-c/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-c/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-c/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-c/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-c/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-c/deployments\"\ + ,\"created_at\":\"2015-05-04T18:58:23Z\",\"updated_at\":\"2020-09-24T07:35:36Z\"\ + ,\"pushed_at\":\"2020-08-28T19:36:17Z\",\"git_url\":\"git://github.com/codecov/example-c.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-c.git\",\"clone_url\":\"https://github.com/codecov/example-c.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-c\",\"homepage\":null,\"size\"\ + :21,\"stargazers_count\":32,\"watchers_count\":32,\"language\":\"C\",\"has_issues\"\ + :false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\":false,\"has_pages\"\ + :false,\"forks_count\":34,\"mirror_url\":null,\"archived\":false,\"disabled\"\ + :false,\"open_issues_count\":4,\"license\":null,\"forks\":34,\"open_issues\"\ + :4,\"watchers\":32,\"default_branch\":\"main\",\"permissions\":{\"admin\"\ + :true,\"push\":true,\"pull\":true}},{\"id\":36365442,\"node_id\":\"MDEwOlJlcG9zaXRvcnkzNjM2NTQ0Mg==\"\ + ,\"name\":\"example-clojure\",\"full_name\":\"codecov/example-clojure\",\"private\"\ + :false,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-clojure\"\ + ,\"description\":\"Example Clojure integration with Codecov\",\"fork\":false,\"\ + url\":\"https://api.github.com/repos/codecov/example-clojure\",\"forks_url\"\ + :\"https://api.github.com/repos/codecov/example-clojure/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/example-clojure/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/example-clojure/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-clojure/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-clojure/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-clojure/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-clojure/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-clojure/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-clojure/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-clojure/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/example-clojure/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-clojure/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-clojure/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-clojure/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-clojure/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-clojure/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-clojure/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-clojure/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-clojure/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-clojure/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-clojure/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-clojure/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-clojure/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-clojure/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-clojure/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-clojure/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-clojure/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-clojure/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-clojure/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-clojure/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-clojure/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-clojure/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-clojure/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-clojure/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-clojure/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-clojure/deployments\"\ + ,\"created_at\":\"2015-05-27T12:30:22Z\",\"updated_at\":\"2020-09-17T18:06:30Z\"\ + ,\"pushed_at\":\"2020-09-17T18:06:28Z\",\"git_url\":\"git://github.com/codecov/example-clojure.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-clojure.git\",\"clone_url\":\"\ + https://github.com/codecov/example-clojure.git\",\"svn_url\":\"https://github.com/codecov/example-clojure\"\ + ,\"homepage\":\"https://codecov.io\",\"size\":16,\"stargazers_count\":10,\"\ + watchers_count\":10,\"language\":\"Clojure\",\"has_issues\":false,\"has_projects\"\ + :false,\"has_downloads\":true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\"\ + :9,\"mirror_url\":null,\"archived\":false,\"disabled\":false,\"open_issues_count\"\ + :0,\"license\":{\"key\":\"mit\",\"name\":\"MIT License\",\"spdx_id\":\"MIT\"\ + ,\"url\":\"https://api.github.com/licenses/mit\",\"node_id\":\"MDc6TGljZW5zZTEz\"\ + },\"forks\":9,\"open_issues\":0,\"watchers\":10,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":48370345,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnk0ODM3MDM0NQ==\",\"name\":\"example-cpp\",\"full_name\"\ + :\"codecov/example-cpp\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-cpp\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-cpp\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-cpp/forks\",\"\ + keys_url\":\"https://api.github.com/repos/codecov/example-cpp/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-cpp/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-cpp/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/example-cpp/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/example-cpp/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-cpp/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-cpp/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-cpp/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-cpp/tags\",\"blobs_url\"\ + :\"https://api.github.com/repos/codecov/example-cpp/git/blobs{/sha}\",\"git_tags_url\"\ + :\"https://api.github.com/repos/codecov/example-cpp/git/tags{/sha}\",\"git_refs_url\"\ + :\"https://api.github.com/repos/codecov/example-cpp/git/refs{/sha}\",\"trees_url\"\ + :\"https://api.github.com/repos/codecov/example-cpp/git/trees{/sha}\",\"statuses_url\"\ + :\"https://api.github.com/repos/codecov/example-cpp/statuses/{sha}\",\"languages_url\"\ + :\"https://api.github.com/repos/codecov/example-cpp/languages\",\"stargazers_url\"\ + :\"https://api.github.com/repos/codecov/example-cpp/stargazers\",\"contributors_url\"\ + :\"https://api.github.com/repos/codecov/example-cpp/contributors\",\"subscribers_url\"\ + :\"https://api.github.com/repos/codecov/example-cpp/subscribers\",\"subscription_url\"\ + :\"https://api.github.com/repos/codecov/example-cpp/subscription\",\"commits_url\"\ + :\"https://api.github.com/repos/codecov/example-cpp/commits{/sha}\",\"git_commits_url\"\ + :\"https://api.github.com/repos/codecov/example-cpp/git/commits{/sha}\",\"comments_url\"\ + :\"https://api.github.com/repos/codecov/example-cpp/comments{/number}\",\"issue_comment_url\"\ + :\"https://api.github.com/repos/codecov/example-cpp/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-cpp/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-cpp/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-cpp/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-cpp/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-cpp/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-cpp/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-cpp/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-cpp/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-cpp/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-cpp/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-cpp/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-cpp/deployments\"\ + ,\"created_at\":\"2015-12-21T12:22:29Z\",\"updated_at\":\"2018-04-26T08:30:56Z\"\ + ,\"pushed_at\":\"2016-08-25T15:11:26Z\",\"git_url\":\"git://github.com/codecov/example-cpp.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-cpp.git\",\"clone_url\":\"https://github.com/codecov/example-cpp.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-cpp\",\"homepage\":null,\"\ + size\":0,\"stargazers_count\":2,\"watchers_count\":2,\"language\":null,\"has_issues\"\ + :false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\":false,\"has_pages\"\ + :false,\"forks_count\":2,\"mirror_url\":null,\"archived\":false,\"disabled\"\ + :false,\"open_issues_count\":0,\"license\":null,\"forks\":2,\"open_issues\"\ + :0,\"watchers\":2,\"default_branch\":\"main\",\"permissions\":{\"admin\":false,\"\ + push\":true,\"pull\":true}},{\"id\":65390076,\"node_id\":\"MDEwOlJlcG9zaXRvcnk2NTM5MDA3Ng==\"\ + ,\"name\":\"example-cpp11\",\"full_name\":\"codecov/example-cpp11\",\"private\"\ + :false,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-cpp11\"\ + ,\"description\":\"Minimal project that uses qmake, GCC, C++11, gcov and is\ + \ tested by Travis CI\",\"fork\":true,\"url\":\"https://api.github.com/repos/codecov/example-cpp11\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-cpp11/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-cpp11/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-cpp11/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-cpp11/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-cpp11/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-cpp11/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-cpp11/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-cpp11/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-cpp11/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-cpp11/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-cpp11/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-cpp11/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-cpp11/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-cpp11/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-cpp11/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-cpp11/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-cpp11/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-cpp11/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-cpp11/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-cpp11/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-cpp11/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-cpp11/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-cpp11/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-cpp11/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-cpp11/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-cpp11/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-cpp11/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-cpp11/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-cpp11/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-cpp11/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-cpp11/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-cpp11/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-cpp11/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-cpp11/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-cpp11/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-cpp11/deployments\"\ + ,\"created_at\":\"2016-08-10T14:38:50Z\",\"updated_at\":\"2020-08-28T13:00:35Z\"\ + ,\"pushed_at\":\"2020-08-27T03:14:27Z\",\"git_url\":\"git://github.com/codecov/example-cpp11.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-cpp11.git\",\"clone_url\":\"https://github.com/codecov/example-cpp11.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-cpp11\",\"homepage\":null,\"\ + size\":31,\"stargazers_count\":25,\"watchers_count\":25,\"language\":\"Shell\"\ + ,\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":24,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":{\"key\":\"gpl-3.0\"\ + ,\"name\":\"GNU General Public License v3.0\",\"spdx_id\":\"GPL-3.0\",\"url\"\ + :\"https://api.github.com/licenses/gpl-3.0\",\"node_id\":\"MDc6TGljZW5zZTk=\"\ + },\"forks\":24,\"open_issues\":0,\"watchers\":25,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":78873888,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnk3ODg3Mzg4OA==\",\"name\":\"example-cpp11-cmake\"\ + ,\"full_name\":\"codecov/example-cpp11-cmake\",\"private\":false,\"owner\":{\"\ + login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-cpp11-cmake\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-cpp11-cmake/deployments\"\ + ,\"created_at\":\"2017-01-13T18:12:49Z\",\"updated_at\":\"2020-10-02T16:26:30Z\"\ + ,\"pushed_at\":\"2020-09-08T20:07:28Z\",\"git_url\":\"git://github.com/codecov/example-cpp11-cmake.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-cpp11-cmake.git\",\"clone_url\"\ + :\"https://github.com/codecov/example-cpp11-cmake.git\",\"svn_url\":\"https://github.com/codecov/example-cpp11-cmake\"\ + ,\"homepage\":null,\"size\":47,\"stargazers_count\":122,\"watchers_count\":122,\"\ + language\":\"CMake\",\"has_issues\":false,\"has_projects\":false,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":51,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\"\ + :{\"key\":\"mit\",\"name\":\"MIT License\",\"spdx_id\":\"MIT\",\"url\":\"https://api.github.com/licenses/mit\"\ + ,\"node_id\":\"MDc6TGljZW5zZTEz\"},\"forks\":51,\"open_issues\":0,\"watchers\"\ + :122,\"default_branch\":\"main\",\"permissions\":{\"admin\":false,\"push\"\ + :true,\"pull\":true}},{\"id\":65390037,\"node_id\":\"MDEwOlJlcG9zaXRvcnk2NTM5MDAzNw==\"\ + ,\"name\":\"example-cpp11_boost\",\"full_name\":\"codecov/example-cpp11_boost\"\ + ,\"private\":false,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\"\ + :\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-cpp11_boost\"\ + ,\"description\":\"Minimal project that uses qmake, GCC, C++11, Boost, Boost.Test,\ + \ gcov and is tested by Travis CI\",\"fork\":true,\"url\":\"https://api.github.com/repos/codecov/example-cpp11_boost\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-cpp11_boost/deployments\"\ + ,\"created_at\":\"2016-08-10T14:38:20Z\",\"updated_at\":\"2020-09-04T02:47:19Z\"\ + ,\"pushed_at\":\"2020-09-04T02:47:17Z\",\"git_url\":\"git://github.com/codecov/example-cpp11_boost.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-cpp11_boost.git\",\"clone_url\"\ + :\"https://github.com/codecov/example-cpp11_boost.git\",\"svn_url\":\"https://github.com/codecov/example-cpp11_boost\"\ + ,\"homepage\":null,\"size\":28,\"stargazers_count\":3,\"watchers_count\":3,\"\ + language\":\"Shell\",\"has_issues\":false,\"has_projects\":false,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":2,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\"\ + :{\"key\":\"gpl-3.0\",\"name\":\"GNU General Public License v3.0\",\"spdx_id\"\ + :\"GPL-3.0\",\"url\":\"https://api.github.com/licenses/gpl-3.0\",\"node_id\"\ + :\"MDc6TGljZW5zZTk=\"},\"forks\":2,\"open_issues\":0,\"watchers\":3,\"default_branch\"\ + :\"main\",\"permissions\":{\"admin\":false,\"push\":true,\"pull\":true}},{\"\ + id\":65389969,\"node_id\":\"MDEwOlJlcG9zaXRvcnk2NTM4OTk2OQ==\",\"name\":\"example-cpp98\"\ + ,\"full_name\":\"codecov/example-cpp98\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-cpp98\"\ + ,\"description\":\"Test how to use qmake and gcov\",\"fork\":true,\"url\":\"\ + https://api.github.com/repos/codecov/example-cpp98\",\"forks_url\":\"https://api.github.com/repos/codecov/example-cpp98/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-cpp98/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-cpp98/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-cpp98/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-cpp98/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-cpp98/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-cpp98/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-cpp98/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-cpp98/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-cpp98/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-cpp98/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-cpp98/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-cpp98/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-cpp98/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-cpp98/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-cpp98/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-cpp98/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-cpp98/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-cpp98/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-cpp98/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-cpp98/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-cpp98/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-cpp98/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-cpp98/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-cpp98/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-cpp98/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-cpp98/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-cpp98/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-cpp98/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-cpp98/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-cpp98/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-cpp98/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-cpp98/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-cpp98/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-cpp98/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-cpp98/deployments\"\ + ,\"created_at\":\"2016-08-10T14:37:38Z\",\"updated_at\":\"2019-03-30T07:53:46Z\"\ + ,\"pushed_at\":\"2018-04-26T08:51:17Z\",\"git_url\":\"git://github.com/codecov/example-cpp98.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-cpp98.git\",\"clone_url\":\"https://github.com/codecov/example-cpp98.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-cpp98\",\"homepage\":null,\"\ + size\":156,\"stargazers_count\":2,\"watchers_count\":2,\"language\":\"Prolog\"\ + ,\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":16,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":{\"key\":\"gpl-3.0\"\ + ,\"name\":\"GNU General Public License v3.0\",\"spdx_id\":\"GPL-3.0\",\"url\"\ + :\"https://api.github.com/licenses/gpl-3.0\",\"node_id\":\"MDc6TGljZW5zZTk=\"\ + },\"forks\":16,\"open_issues\":0,\"watchers\":2,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":34411629,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkzNDQxMTYyOQ==\",\"name\":\"example-csharp\",\"\ + full_name\":\"codecov/example-csharp\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-csharp\"\ + ,\"description\":\"Codecov: C# example repository\",\"fork\":false,\"url\":\"\ + https://api.github.com/repos/codecov/example-csharp\",\"forks_url\":\"https://api.github.com/repos/codecov/example-csharp/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-csharp/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-csharp/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-csharp/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-csharp/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-csharp/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-csharp/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-csharp/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-csharp/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-csharp/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-csharp/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-csharp/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-csharp/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-csharp/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-csharp/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-csharp/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-csharp/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-csharp/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-csharp/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-csharp/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-csharp/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-csharp/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-csharp/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-csharp/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-csharp/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-csharp/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-csharp/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-csharp/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-csharp/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-csharp/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-csharp/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-csharp/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-csharp/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-csharp/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-csharp/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-csharp/deployments\"\ + ,\"created_at\":\"2015-04-22T19:38:00Z\",\"updated_at\":\"2020-10-06T01:51:34Z\"\ + ,\"pushed_at\":\"2020-08-28T19:30:38Z\",\"git_url\":\"git://github.com/codecov/example-csharp.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-csharp.git\",\"clone_url\":\"\ + https://github.com/codecov/example-csharp.git\",\"svn_url\":\"https://github.com/codecov/example-csharp\"\ + ,\"homepage\":\"https://codecov.io\",\"size\":42,\"stargazers_count\":96,\"\ + watchers_count\":96,\"language\":\"C#\",\"has_issues\":false,\"has_projects\"\ + :false,\"has_downloads\":true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\"\ + :70,\"mirror_url\":null,\"archived\":false,\"disabled\":false,\"open_issues_count\"\ + :4,\"license\":{\"key\":\"mit\",\"name\":\"MIT License\",\"spdx_id\":\"MIT\"\ + ,\"url\":\"https://api.github.com/licenses/mit\",\"node_id\":\"MDc6TGljZW5zZTEz\"\ + },\"forks\":70,\"open_issues\":4,\"watchers\":96,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":53706437,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnk1MzcwNjQzNw==\",\"name\":\"example-csharp-sharpcover\"\ + ,\"full_name\":\"codecov/example-csharp-sharpcover\",\"private\":false,\"owner\"\ + :{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-csharp-sharpcover\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-csharp-sharpcover/deployments\"\ + ,\"created_at\":\"2016-03-12T01:10:08Z\",\"updated_at\":\"2020-08-27T03:15:09Z\"\ + ,\"pushed_at\":\"2020-08-28T22:18:50Z\",\"git_url\":\"git://github.com/codecov/example-csharp-sharpcover.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-csharp-sharpcover.git\",\"clone_url\"\ + :\"https://github.com/codecov/example-csharp-sharpcover.git\",\"svn_url\":\"\ + https://github.com/codecov/example-csharp-sharpcover\",\"homepage\":null,\"\ + size\":372,\"stargazers_count\":0,\"watchers_count\":0,\"language\":\"C#\",\"\ + has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":5,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":1,\"license\":null,\"forks\"\ + :5,\"open_issues\":1,\"watchers\":0,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":32197069,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkzMjE5NzA2OQ==\",\"name\":\"example-d\",\"full_name\":\"\ + codecov/example-d\",\"private\":false,\"owner\":{\"login\":\"codecov\",\"id\"\ + :8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"\ + https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\"\ + ,\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-d\"\ + ,\"description\":\"Example repository for D and Codecov\",\"fork\":false,\"\ + url\":\"https://api.github.com/repos/codecov/example-d\",\"forks_url\":\"https://api.github.com/repos/codecov/example-d/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-d/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-d/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-d/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/example-d/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/example-d/issues/events{/number}\",\"\ + events_url\":\"https://api.github.com/repos/codecov/example-d/events\",\"assignees_url\"\ + :\"https://api.github.com/repos/codecov/example-d/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/example-d/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/example-d/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/example-d/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-d/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-d/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-d/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-d/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-d/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-d/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-d/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-d/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-d/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-d/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-d/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-d/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-d/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-d/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-d/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-d/merges\",\"\ + archive_url\":\"https://api.github.com/repos/codecov/example-d/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-d/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-d/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-d/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-d/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-d/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-d/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-d/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-d/deployments\"\ + ,\"created_at\":\"2015-03-14T05:16:03Z\",\"updated_at\":\"2020-04-27T15:22:41Z\"\ + ,\"pushed_at\":\"2018-06-14T12:11:48Z\",\"git_url\":\"git://github.com/codecov/example-d.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-d.git\",\"clone_url\":\"https://github.com/codecov/example-d.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-d\",\"homepage\":\"https://codecov.io\"\ + ,\"size\":45,\"stargazers_count\":8,\"watchers_count\":8,\"language\":\"D\"\ + ,\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":6,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"forks\"\ + :6,\"open_issues\":0,\"watchers\":8,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":45387112,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnk0NTM4NzExMg==\",\"name\":\"example-delphi\",\"full_name\"\ + :\"codecov/example-delphi\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-delphi\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-delphi\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-delphi/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-delphi/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-delphi/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-delphi/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-delphi/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-delphi/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-delphi/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-delphi/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-delphi/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-delphi/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-delphi/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-delphi/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-delphi/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-delphi/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-delphi/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-delphi/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-delphi/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-delphi/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-delphi/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-delphi/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-delphi/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-delphi/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-delphi/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-delphi/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-delphi/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-delphi/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-delphi/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-delphi/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-delphi/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-delphi/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-delphi/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-delphi/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-delphi/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-delphi/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-delphi/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-delphi/deployments\"\ + ,\"created_at\":\"2015-11-02T10:16:22Z\",\"updated_at\":\"2018-04-26T08:31:08Z\"\ + ,\"pushed_at\":\"2015-11-02T10:16:22Z\",\"git_url\":\"git://github.com/codecov/example-delphi.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-delphi.git\",\"clone_url\":\"\ + https://github.com/codecov/example-delphi.git\",\"svn_url\":\"https://github.com/codecov/example-delphi\"\ + ,\"homepage\":null,\"size\":120,\"stargazers_count\":0,\"watchers_count\":0,\"\ + language\":null,\"has_issues\":false,\"has_projects\":false,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":1,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\"\ + :null,\"forks\":1,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":70835757,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnk3MDgzNTc1Nw==\",\"name\":\"example-elixir\",\"\ + full_name\":\"codecov/example-elixir\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-elixir\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-elixir\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-elixir/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-elixir/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-elixir/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-elixir/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-elixir/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-elixir/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-elixir/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-elixir/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-elixir/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-elixir/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-elixir/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-elixir/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-elixir/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-elixir/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-elixir/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-elixir/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-elixir/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-elixir/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-elixir/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-elixir/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-elixir/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-elixir/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-elixir/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-elixir/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-elixir/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-elixir/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-elixir/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-elixir/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-elixir/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-elixir/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-elixir/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-elixir/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-elixir/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-elixir/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-elixir/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-elixir/deployments\"\ + ,\"created_at\":\"2016-10-13T18:24:48Z\",\"updated_at\":\"2020-10-07T05:49:42Z\"\ + ,\"pushed_at\":\"2018-04-26T08:45:47Z\",\"git_url\":\"git://github.com/codecov/example-elixir.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-elixir.git\",\"clone_url\":\"\ + https://github.com/codecov/example-elixir.git\",\"svn_url\":\"https://github.com/codecov/example-elixir\"\ + ,\"homepage\":null,\"size\":6,\"stargazers_count\":13,\"watchers_count\":13,\"\ + language\":\"Elixir\",\"has_issues\":false,\"has_projects\":false,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":7,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\"\ + :null,\"forks\":7,\"open_issues\":0,\"watchers\":13,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":37341433,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkzNzM0MTQzMw==\",\"name\":\"example-erlang\",\"\ + full_name\":\"codecov/example-erlang\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-erlang\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-erlang\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-erlang/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-erlang/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-erlang/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-erlang/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-erlang/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-erlang/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-erlang/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-erlang/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-erlang/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-erlang/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-erlang/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-erlang/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-erlang/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-erlang/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-erlang/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-erlang/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-erlang/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-erlang/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-erlang/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-erlang/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-erlang/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-erlang/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-erlang/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-erlang/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-erlang/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-erlang/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-erlang/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-erlang/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-erlang/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-erlang/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-erlang/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-erlang/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-erlang/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-erlang/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-erlang/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-erlang/deployments\"\ + ,\"created_at\":\"2015-06-12T19:49:05Z\",\"updated_at\":\"2020-08-27T03:06:36Z\"\ + ,\"pushed_at\":\"2020-08-27T03:06:34Z\",\"git_url\":\"git://github.com/codecov/example-erlang.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-erlang.git\",\"clone_url\":\"\ + https://github.com/codecov/example-erlang.git\",\"svn_url\":\"https://github.com/codecov/example-erlang\"\ + ,\"homepage\":null,\"size\":9,\"stargazers_count\":2,\"watchers_count\":2,\"\ + language\":\"Erlang\",\"has_issues\":false,\"has_projects\":false,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":5,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\"\ + :null,\"forks\":5,\"open_issues\":0,\"watchers\":2,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":37135271,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkzNzEzNTI3MQ==\",\"name\":\"example-fortran\"\ + ,\"full_name\":\"codecov/example-fortran\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-fortran\"\ + ,\"description\":\"Example repo for uploading reports to Codecov\",\"fork\"\ + :false,\"url\":\"https://api.github.com/repos/codecov/example-fortran\",\"forks_url\"\ + :\"https://api.github.com/repos/codecov/example-fortran/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/example-fortran/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/example-fortran/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-fortran/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-fortran/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-fortran/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-fortran/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-fortran/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-fortran/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-fortran/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/example-fortran/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-fortran/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-fortran/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-fortran/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-fortran/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-fortran/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-fortran/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-fortran/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-fortran/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-fortran/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-fortran/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-fortran/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-fortran/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-fortran/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-fortran/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-fortran/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-fortran/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-fortran/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-fortran/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-fortran/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-fortran/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-fortran/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-fortran/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-fortran/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-fortran/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-fortran/deployments\"\ + ,\"created_at\":\"2015-06-09T13:56:31Z\",\"updated_at\":\"2020-07-10T14:58:42Z\"\ + ,\"pushed_at\":\"2018-04-26T08:44:09Z\",\"git_url\":\"git://github.com/codecov/example-fortran.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-fortran.git\",\"clone_url\":\"\ + https://github.com/codecov/example-fortran.git\",\"svn_url\":\"https://github.com/codecov/example-fortran\"\ + ,\"homepage\":\"https://codecov.io\",\"size\":45,\"stargazers_count\":16,\"\ + watchers_count\":16,\"language\":\"Fortran\",\"has_issues\":false,\"has_projects\"\ + :false,\"has_downloads\":true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\"\ + :7,\"mirror_url\":null,\"archived\":false,\"disabled\":false,\"open_issues_count\"\ + :0,\"license\":null,\"forks\":7,\"open_issues\":0,\"watchers\":16,\"default_branch\"\ + :\"main\",\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"\ + id\":116717242,\"node_id\":\"MDEwOlJlcG9zaXRvcnkxMTY3MTcyNDI=\",\"name\":\"\ + example-fsharp\",\"full_name\":\"codecov/example-fsharp\",\"private\":false,\"\ + owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-fsharp\"\ + ,\"description\":\"Example of codecov in fsharp\",\"fork\":false,\"url\":\"\ + https://api.github.com/repos/codecov/example-fsharp\",\"forks_url\":\"https://api.github.com/repos/codecov/example-fsharp/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-fsharp/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-fsharp/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-fsharp/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-fsharp/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-fsharp/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-fsharp/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-fsharp/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-fsharp/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-fsharp/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-fsharp/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-fsharp/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-fsharp/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-fsharp/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-fsharp/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-fsharp/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-fsharp/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-fsharp/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-fsharp/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-fsharp/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-fsharp/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-fsharp/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-fsharp/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-fsharp/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-fsharp/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-fsharp/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-fsharp/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-fsharp/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-fsharp/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-fsharp/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-fsharp/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-fsharp/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-fsharp/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-fsharp/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-fsharp/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-fsharp/deployments\"\ + ,\"created_at\":\"2018-01-08T19:14:02Z\",\"updated_at\":\"2020-09-04T02:46:42Z\"\ + ,\"pushed_at\":\"2020-09-04T02:46:39Z\",\"git_url\":\"git://github.com/codecov/example-fsharp.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-fsharp.git\",\"clone_url\":\"\ + https://github.com/codecov/example-fsharp.git\",\"svn_url\":\"https://github.com/codecov/example-fsharp\"\ + ,\"homepage\":null,\"size\":18,\"stargazers_count\":4,\"watchers_count\":4,\"\ + language\":\"F#\",\"has_issues\":false,\"has_projects\":false,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":3,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\"\ + :{\"key\":\"mit\",\"name\":\"MIT License\",\"spdx_id\":\"MIT\",\"url\":\"https://api.github.com/licenses/mit\"\ + ,\"node_id\":\"MDc6TGljZW5zZTEz\"},\"forks\":3,\"open_issues\":0,\"watchers\"\ + :4,\"default_branch\":\"main\",\"permissions\":{\"admin\":false,\"push\":true,\"\ + pull\":true}},{\"id\":24332073,\"node_id\":\"MDEwOlJlcG9zaXRvcnkyNDMzMjA3Mw==\"\ + ,\"name\":\"example-go\",\"full_name\":\"codecov/example-go\",\"private\":false,\"\ + owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-go\"\ + ,\"description\":\"Go coverage example\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-go\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-go/forks\",\"\ + keys_url\":\"https://api.github.com/repos/codecov/example-go/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-go/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-go/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/example-go/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/example-go/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-go/events\",\"\ + assignees_url\":\"https://api.github.com/repos/codecov/example-go/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-go/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-go/tags\",\"blobs_url\"\ + :\"https://api.github.com/repos/codecov/example-go/git/blobs{/sha}\",\"git_tags_url\"\ + :\"https://api.github.com/repos/codecov/example-go/git/tags{/sha}\",\"git_refs_url\"\ + :\"https://api.github.com/repos/codecov/example-go/git/refs{/sha}\",\"trees_url\"\ + :\"https://api.github.com/repos/codecov/example-go/git/trees{/sha}\",\"statuses_url\"\ + :\"https://api.github.com/repos/codecov/example-go/statuses/{sha}\",\"languages_url\"\ + :\"https://api.github.com/repos/codecov/example-go/languages\",\"stargazers_url\"\ + :\"https://api.github.com/repos/codecov/example-go/stargazers\",\"contributors_url\"\ + :\"https://api.github.com/repos/codecov/example-go/contributors\",\"subscribers_url\"\ + :\"https://api.github.com/repos/codecov/example-go/subscribers\",\"subscription_url\"\ + :\"https://api.github.com/repos/codecov/example-go/subscription\",\"commits_url\"\ + :\"https://api.github.com/repos/codecov/example-go/commits{/sha}\",\"git_commits_url\"\ + :\"https://api.github.com/repos/codecov/example-go/git/commits{/sha}\",\"comments_url\"\ + :\"https://api.github.com/repos/codecov/example-go/comments{/number}\",\"issue_comment_url\"\ + :\"https://api.github.com/repos/codecov/example-go/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-go/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-go/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-go/merges\",\"\ + archive_url\":\"https://api.github.com/repos/codecov/example-go/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-go/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-go/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-go/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-go/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-go/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-go/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-go/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-go/deployments\"\ + ,\"created_at\":\"2014-09-22T14:33:53Z\",\"updated_at\":\"2020-10-07T12:23:55Z\"\ + ,\"pushed_at\":\"2020-09-17T17:33:20Z\",\"git_url\":\"git://github.com/codecov/example-go.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-go.git\",\"clone_url\":\"https://github.com/codecov/example-go.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-go\",\"homepage\":\"https://codecov.io\"\ + ,\"size\":56,\"stargazers_count\":155,\"watchers_count\":155,\"language\":\"\ + Go\",\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":36,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":1,\"license\":null,\"forks\"\ + :36,\"open_issues\":1,\"watchers\":155,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":53363022,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnk1MzM2MzAyMg==\",\"name\":\"example-gradle\",\"full_name\"\ + :\"codecov/example-gradle\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-gradle\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-gradle\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-gradle/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-gradle/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-gradle/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-gradle/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-gradle/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-gradle/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-gradle/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-gradle/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-gradle/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-gradle/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-gradle/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-gradle/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-gradle/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-gradle/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-gradle/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-gradle/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-gradle/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-gradle/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-gradle/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-gradle/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-gradle/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-gradle/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-gradle/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-gradle/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-gradle/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-gradle/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-gradle/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-gradle/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-gradle/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-gradle/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-gradle/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-gradle/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-gradle/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-gradle/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-gradle/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-gradle/deployments\"\ + ,\"created_at\":\"2016-03-07T22:13:27Z\",\"updated_at\":\"2020-10-13T04:45:13Z\"\ + ,\"pushed_at\":\"2020-09-09T21:07:11Z\",\"git_url\":\"git://github.com/codecov/example-gradle.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-gradle.git\",\"clone_url\":\"\ + https://github.com/codecov/example-gradle.git\",\"svn_url\":\"https://github.com/codecov/example-gradle\"\ + ,\"homepage\":null,\"size\":80,\"stargazers_count\":67,\"watchers_count\":67,\"\ + language\":\"Java\",\"has_issues\":false,\"has_projects\":false,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":54,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":2,\"license\"\ + :null,\"forks\":54,\"open_issues\":2,\"watchers\":67,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":24567550,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkyNDU2NzU1MA==\",\"name\":\"example-groovy\",\"\ + full_name\":\"codecov/example-groovy\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-groovy\"\ + ,\"description\":\"Groovy coverage example\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-groovy\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-groovy/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-groovy/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-groovy/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-groovy/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-groovy/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-groovy/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-groovy/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-groovy/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-groovy/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-groovy/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-groovy/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-groovy/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-groovy/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-groovy/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-groovy/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-groovy/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-groovy/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-groovy/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-groovy/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-groovy/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-groovy/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-groovy/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-groovy/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-groovy/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-groovy/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-groovy/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-groovy/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-groovy/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-groovy/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-groovy/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-groovy/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-groovy/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-groovy/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-groovy/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-groovy/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-groovy/deployments\"\ + ,\"created_at\":\"2014-09-28T18:53:21Z\",\"updated_at\":\"2019-02-25T22:15:49Z\"\ + ,\"pushed_at\":\"2018-04-26T08:47:32Z\",\"git_url\":\"git://github.com/codecov/example-groovy.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-groovy.git\",\"clone_url\":\"\ + https://github.com/codecov/example-groovy.git\",\"svn_url\":\"https://github.com/codecov/example-groovy\"\ + ,\"homepage\":\"https://codecov.io\",\"size\":9,\"stargazers_count\":6,\"watchers_count\"\ + :6,\"language\":\"Groovy\",\"has_issues\":false,\"has_projects\":false,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":10,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":1,\"license\"\ + :null,\"forks\":10,\"open_issues\":1,\"watchers\":6,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":36934337,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkzNjkzNDMzNw==\",\"name\":\"example-haskell\"\ + ,\"full_name\":\"codecov/example-haskell\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-haskell\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-haskell\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-haskell/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-haskell/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-haskell/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-haskell/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-haskell/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-haskell/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-haskell/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-haskell/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-haskell/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-haskell/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/example-haskell/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-haskell/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-haskell/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-haskell/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-haskell/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-haskell/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-haskell/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-haskell/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-haskell/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-haskell/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-haskell/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-haskell/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-haskell/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-haskell/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-haskell/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-haskell/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-haskell/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-haskell/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-haskell/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-haskell/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-haskell/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-haskell/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-haskell/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-haskell/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-haskell/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-haskell/deployments\"\ + ,\"created_at\":\"2015-06-05T13:32:50Z\",\"updated_at\":\"2020-08-27T02:44:15Z\"\ + ,\"pushed_at\":\"2020-08-27T02:44:12Z\",\"git_url\":\"git://github.com/codecov/example-haskell.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-haskell.git\",\"clone_url\":\"\ + https://github.com/codecov/example-haskell.git\",\"svn_url\":\"https://github.com/codecov/example-haskell\"\ + ,\"homepage\":null,\"size\":1,\"stargazers_count\":1,\"watchers_count\":1,\"\ + language\":null,\"has_issues\":false,\"has_projects\":false,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":3,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":1,\"license\"\ + :null,\"forks\":3,\"open_issues\":1,\"watchers\":1,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":24567516,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkyNDU2NzUxNg==\",\"name\":\"example-java\",\"\ + full_name\":\"codecov/example-java\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-java\"\ + ,\"description\":\"Java Example\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-java\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-java/forks\",\"\ + keys_url\":\"https://api.github.com/repos/codecov/example-java/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-java/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-java/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/example-java/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/example-java/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-java/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-java/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-java/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-java/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-java/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-java/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-java/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-java/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-java/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-java/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-java/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-java/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-java/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-java/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-java/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-java/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-java/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-java/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-java/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-java/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-java/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-java/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-java/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-java/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-java/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-java/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-java/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-java/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-java/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-java/deployments\"\ + ,\"created_at\":\"2014-09-28T18:52:03Z\",\"updated_at\":\"2020-10-13T04:33:42Z\"\ + ,\"pushed_at\":\"2020-10-12T06:57:17Z\",\"git_url\":\"git://github.com/codecov/example-java.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-java.git\",\"clone_url\":\"https://github.com/codecov/example-java.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-java\",\"homepage\":\"https://codecov.io\"\ + ,\"size\":94,\"stargazers_count\":57,\"watchers_count\":57,\"language\":\"Java\"\ + ,\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":134,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":2,\"license\":null,\"forks\"\ + :134,\"open_issues\":2,\"watchers\":57,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":66935478,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnk2NjkzNTQ3OA==\",\"name\":\"example-java-maven\",\"full_name\"\ + :\"codecov/example-java-maven\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-java-maven\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-java-maven\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-java-maven/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-java-maven/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-java-maven/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-java-maven/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-java-maven/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-java-maven/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-java-maven/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-java-maven/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-java-maven/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-java-maven/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/example-java-maven/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-java-maven/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-java-maven/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-java-maven/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-java-maven/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-java-maven/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-java-maven/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-java-maven/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-java-maven/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-java-maven/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-java-maven/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-java-maven/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-java-maven/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-java-maven/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-java-maven/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-java-maven/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-java-maven/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-java-maven/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-java-maven/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-java-maven/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-java-maven/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-java-maven/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-java-maven/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-java-maven/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-java-maven/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-java-maven/deployments\"\ + ,\"created_at\":\"2016-08-30T11:41:58Z\",\"updated_at\":\"2020-10-07T16:10:08Z\"\ + ,\"pushed_at\":\"2020-08-27T03:06:23Z\",\"git_url\":\"git://github.com/codecov/example-java-maven.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-java-maven.git\",\"clone_url\"\ + :\"https://github.com/codecov/example-java-maven.git\",\"svn_url\":\"https://github.com/codecov/example-java-maven\"\ + ,\"homepage\":null,\"size\":30,\"stargazers_count\":46,\"watchers_count\":46,\"\ + language\":\"Java\",\"has_issues\":false,\"has_projects\":false,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":81,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":2,\"license\"\ + :null,\"forks\":81,\"open_issues\":2,\"watchers\":46,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":56502136,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnk1NjUwMjEzNg==\",\"name\":\"example-javascript-react\"\ + ,\"full_name\":\"codecov/example-javascript-react\",\"private\":false,\"owner\"\ + :{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-javascript-react\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-javascript-react\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-javascript-react/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-javascript-react/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-javascript-react/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-javascript-react/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-javascript-react/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-javascript-react/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-javascript-react/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-javascript-react/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-javascript-react/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-javascript-react/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/example-javascript-react/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-javascript-react/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-javascript-react/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-javascript-react/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-javascript-react/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-javascript-react/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-javascript-react/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-javascript-react/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-javascript-react/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-javascript-react/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-javascript-react/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-javascript-react/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-javascript-react/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-javascript-react/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-javascript-react/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-javascript-react/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-javascript-react/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-javascript-react/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-javascript-react/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-javascript-react/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-javascript-react/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-javascript-react/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-javascript-react/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-javascript-react/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-javascript-react/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-javascript-react/deployments\"\ + ,\"created_at\":\"2016-04-18T11:27:33Z\",\"updated_at\":\"2018-04-26T08:31:03Z\"\ + ,\"pushed_at\":\"2016-04-18T11:27:33Z\",\"git_url\":\"git://github.com/codecov/example-javascript-react.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-javascript-react.git\",\"clone_url\"\ + :\"https://github.com/codecov/example-javascript-react.git\",\"svn_url\":\"\ + https://github.com/codecov/example-javascript-react\",\"homepage\":null,\"size\"\ + :0,\"stargazers_count\":0,\"watchers_count\":0,\"language\":null,\"has_issues\"\ + :false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\":false,\"has_pages\"\ + :false,\"forks_count\":0,\"mirror_url\":null,\"archived\":false,\"disabled\"\ + :false,\"open_issues_count\":0,\"license\":null,\"forks\":0,\"open_issues\"\ + :0,\"watchers\":0,\"default_branch\":\"main\",\"permissions\":{\"admin\":false,\"\ + push\":true,\"pull\":true}},{\"id\":37738341,\"node_id\":\"MDEwOlJlcG9zaXRvcnkzNzczODM0MQ==\"\ + ,\"name\":\"example-julia\",\"full_name\":\"codecov/example-julia\",\"private\"\ + :false,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-julia\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-julia\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-julia/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-julia/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-julia/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-julia/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-julia/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-julia/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-julia/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-julia/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-julia/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-julia/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-julia/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-julia/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-julia/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-julia/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-julia/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-julia/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-julia/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-julia/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-julia/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-julia/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-julia/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-julia/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-julia/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-julia/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-julia/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-julia/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-julia/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-julia/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-julia/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-julia/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-julia/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-julia/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-julia/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-julia/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-julia/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-julia/deployments\"\ + ,\"created_at\":\"2015-06-19T18:15:13Z\",\"updated_at\":\"2020-09-09T20:54:27Z\"\ + ,\"pushed_at\":\"2020-09-09T20:54:25Z\",\"git_url\":\"git://github.com/codecov/example-julia.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-julia.git\",\"clone_url\":\"https://github.com/codecov/example-julia.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-julia\",\"homepage\":null,\"\ + size\":4,\"stargazers_count\":2,\"watchers_count\":2,\"language\":null,\"has_issues\"\ + :false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\":false,\"has_pages\"\ + :false,\"forks_count\":6,\"mirror_url\":null,\"archived\":false,\"disabled\"\ + :false,\"open_issues_count\":0,\"license\":null,\"forks\":6,\"open_issues\"\ + :0,\"watchers\":2,\"default_branch\":\"main\",\"permissions\":{\"admin\":true,\"\ + push\":true,\"pull\":true}},{\"id\":24567577,\"node_id\":\"MDEwOlJlcG9zaXRvcnkyNDU2NzU3Nw==\"\ + ,\"name\":\"example-kotlin\",\"full_name\":\"codecov/example-kotlin\",\"private\"\ + :false,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-kotlin\"\ + ,\"description\":\"Kotlin coverage example\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-kotlin\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-kotlin/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-kotlin/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-kotlin/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-kotlin/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-kotlin/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-kotlin/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-kotlin/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-kotlin/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-kotlin/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-kotlin/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-kotlin/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-kotlin/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-kotlin/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-kotlin/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-kotlin/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-kotlin/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-kotlin/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-kotlin/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-kotlin/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-kotlin/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-kotlin/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-kotlin/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-kotlin/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-kotlin/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-kotlin/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-kotlin/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-kotlin/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-kotlin/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-kotlin/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-kotlin/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-kotlin/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-kotlin/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-kotlin/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-kotlin/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-kotlin/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-kotlin/deployments\"\ + ,\"created_at\":\"2014-09-28T18:54:08Z\",\"updated_at\":\"2020-09-04T02:47:29Z\"\ + ,\"pushed_at\":\"2020-09-04T02:47:27Z\",\"git_url\":\"git://github.com/codecov/example-kotlin.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-kotlin.git\",\"clone_url\":\"\ + https://github.com/codecov/example-kotlin.git\",\"svn_url\":\"https://github.com/codecov/example-kotlin\"\ + ,\"homepage\":\"https://codecov.io\",\"size\":19,\"stargazers_count\":15,\"\ + watchers_count\":15,\"language\":\"Kotlin\",\"has_issues\":false,\"has_projects\"\ + :false,\"has_downloads\":true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\"\ + :17,\"mirror_url\":null,\"archived\":false,\"disabled\":false,\"open_issues_count\"\ + :1,\"license\":null,\"forks\":17,\"open_issues\":1,\"watchers\":15,\"default_branch\"\ + :\"main\",\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"\ + id\":39953512,\"node_id\":\"MDEwOlJlcG9zaXRvcnkzOTk1MzUxMg==\",\"name\":\"example-lua\"\ + ,\"full_name\":\"codecov/example-lua\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-lua\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-lua\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-lua/forks\",\"\ + keys_url\":\"https://api.github.com/repos/codecov/example-lua/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-lua/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-lua/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/example-lua/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/example-lua/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-lua/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-lua/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-lua/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-lua/tags\",\"blobs_url\"\ + :\"https://api.github.com/repos/codecov/example-lua/git/blobs{/sha}\",\"git_tags_url\"\ + :\"https://api.github.com/repos/codecov/example-lua/git/tags{/sha}\",\"git_refs_url\"\ + :\"https://api.github.com/repos/codecov/example-lua/git/refs{/sha}\",\"trees_url\"\ + :\"https://api.github.com/repos/codecov/example-lua/git/trees{/sha}\",\"statuses_url\"\ + :\"https://api.github.com/repos/codecov/example-lua/statuses/{sha}\",\"languages_url\"\ + :\"https://api.github.com/repos/codecov/example-lua/languages\",\"stargazers_url\"\ + :\"https://api.github.com/repos/codecov/example-lua/stargazers\",\"contributors_url\"\ + :\"https://api.github.com/repos/codecov/example-lua/contributors\",\"subscribers_url\"\ + :\"https://api.github.com/repos/codecov/example-lua/subscribers\",\"subscription_url\"\ + :\"https://api.github.com/repos/codecov/example-lua/subscription\",\"commits_url\"\ + :\"https://api.github.com/repos/codecov/example-lua/commits{/sha}\",\"git_commits_url\"\ + :\"https://api.github.com/repos/codecov/example-lua/git/commits{/sha}\",\"comments_url\"\ + :\"https://api.github.com/repos/codecov/example-lua/comments{/number}\",\"issue_comment_url\"\ + :\"https://api.github.com/repos/codecov/example-lua/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-lua/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-lua/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-lua/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-lua/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-lua/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-lua/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-lua/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-lua/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-lua/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-lua/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-lua/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-lua/deployments\"\ + ,\"created_at\":\"2015-07-30T14:01:24Z\",\"updated_at\":\"2020-06-22T13:51:46Z\"\ + ,\"pushed_at\":\"2018-04-26T08:47:51Z\",\"git_url\":\"git://github.com/codecov/example-lua.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-lua.git\",\"clone_url\":\"https://github.com/codecov/example-lua.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-lua\",\"homepage\":null,\"\ + size\":32,\"stargazers_count\":6,\"watchers_count\":6,\"language\":\"Shell\"\ + ,\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":8,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"forks\"\ + :8,\"open_issues\":0,\"watchers\":6,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":32312776,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkzMjMxMjc3Ng==\",\"name\":\"example-node\",\"full_name\"\ + :\"codecov/example-node\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-node\"\ + ,\"description\":\"Example repo for uploading reports to Codecov\",\"fork\"\ + :false,\"url\":\"https://api.github.com/repos/codecov/example-node\",\"forks_url\"\ + :\"https://api.github.com/repos/codecov/example-node/forks\",\"keys_url\":\"\ + https://api.github.com/repos/codecov/example-node/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/example-node/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-node/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/example-node/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/example-node/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-node/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-node/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-node/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-node/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-node/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-node/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-node/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-node/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-node/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-node/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-node/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-node/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-node/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-node/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-node/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-node/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-node/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-node/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-node/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-node/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-node/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-node/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-node/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-node/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-node/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-node/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-node/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-node/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-node/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-node/deployments\"\ + ,\"created_at\":\"2015-03-16T09:01:54Z\",\"updated_at\":\"2020-09-21T11:37:17Z\"\ + ,\"pushed_at\":\"2020-08-28T19:35:45Z\",\"git_url\":\"git://github.com/codecov/example-node.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-node.git\",\"clone_url\":\"https://github.com/codecov/example-node.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-node\",\"homepage\":\"https://codecov.io\"\ + ,\"size\":61,\"stargazers_count\":175,\"watchers_count\":175,\"language\":\"\ + JavaScript\",\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"\ + has_wiki\":false,\"has_pages\":false,\"forks_count\":70,\"mirror_url\":null,\"\ + archived\":false,\"disabled\":false,\"open_issues_count\":4,\"license\":null,\"\ + forks\":70,\"open_issues\":4,\"watchers\":175,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":35696045,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkzNTY5NjA0NQ==\",\"name\":\"example-objc\",\"\ + full_name\":\"codecov/example-objc\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-objc\"\ + ,\"description\":\"Codecov example for Xcode\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-objc\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-objc/forks\",\"\ + keys_url\":\"https://api.github.com/repos/codecov/example-objc/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-objc/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-objc/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/example-objc/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/example-objc/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-objc/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-objc/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-objc/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-objc/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-objc/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-objc/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-objc/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-objc/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-objc/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-objc/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-objc/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-objc/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-objc/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-objc/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-objc/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-objc/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-objc/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-objc/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-objc/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-objc/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-objc/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-objc/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-objc/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-objc/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-objc/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-objc/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-objc/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-objc/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-objc/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-objc/deployments\"\ + ,\"created_at\":\"2015-05-15T20:49:04Z\",\"updated_at\":\"2020-08-27T03:11:41Z\"\ + ,\"pushed_at\":\"2020-08-27T03:11:39Z\",\"git_url\":\"git://github.com/codecov/example-objc.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-objc.git\",\"clone_url\":\"https://github.com/codecov/example-objc.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-objc\",\"homepage\":\"https://codecov.io\"\ + ,\"size\":57,\"stargazers_count\":22,\"watchers_count\":22,\"language\":\"Objective-C\"\ + ,\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":14,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":3,\"license\":null,\"forks\"\ + :14,\"open_issues\":3,\"watchers\":22,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":40403363,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnk0MDQwMzM2Mw==\",\"name\":\"example-perl\",\"full_name\"\ + :\"codecov/example-perl\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-perl\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-perl\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-perl/forks\",\"\ + keys_url\":\"https://api.github.com/repos/codecov/example-perl/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-perl/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-perl/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/example-perl/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/example-perl/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-perl/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-perl/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-perl/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-perl/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-perl/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-perl/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-perl/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-perl/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-perl/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-perl/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-perl/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-perl/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-perl/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-perl/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-perl/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-perl/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-perl/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-perl/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-perl/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-perl/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-perl/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-perl/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-perl/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-perl/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-perl/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-perl/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-perl/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-perl/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-perl/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-perl/deployments\"\ + ,\"created_at\":\"2015-08-08T13:16:02Z\",\"updated_at\":\"2020-08-31T02:17:27Z\"\ + ,\"pushed_at\":\"2020-08-27T03:14:17Z\",\"git_url\":\"git://github.com/codecov/example-perl.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-perl.git\",\"clone_url\":\"https://github.com/codecov/example-perl.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-perl\",\"homepage\":null,\"\ + size\":20,\"stargazers_count\":20,\"watchers_count\":20,\"language\":\"Perl\"\ + ,\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":8,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":1,\"license\":null,\"forks\"\ + :8,\"open_issues\":1,\"watchers\":20,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":23512977,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkyMzUxMjk3Nw==\",\"name\":\"example-php\",\"full_name\"\ + :\"codecov/example-php\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-php\"\ + ,\"description\":\"PHP coverage example\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-php\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-php/forks\",\"\ + keys_url\":\"https://api.github.com/repos/codecov/example-php/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-php/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-php/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/example-php/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/example-php/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-php/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-php/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-php/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-php/tags\",\"blobs_url\"\ + :\"https://api.github.com/repos/codecov/example-php/git/blobs{/sha}\",\"git_tags_url\"\ + :\"https://api.github.com/repos/codecov/example-php/git/tags{/sha}\",\"git_refs_url\"\ + :\"https://api.github.com/repos/codecov/example-php/git/refs{/sha}\",\"trees_url\"\ + :\"https://api.github.com/repos/codecov/example-php/git/trees{/sha}\",\"statuses_url\"\ + :\"https://api.github.com/repos/codecov/example-php/statuses/{sha}\",\"languages_url\"\ + :\"https://api.github.com/repos/codecov/example-php/languages\",\"stargazers_url\"\ + :\"https://api.github.com/repos/codecov/example-php/stargazers\",\"contributors_url\"\ + :\"https://api.github.com/repos/codecov/example-php/contributors\",\"subscribers_url\"\ + :\"https://api.github.com/repos/codecov/example-php/subscribers\",\"subscription_url\"\ + :\"https://api.github.com/repos/codecov/example-php/subscription\",\"commits_url\"\ + :\"https://api.github.com/repos/codecov/example-php/commits{/sha}\",\"git_commits_url\"\ + :\"https://api.github.com/repos/codecov/example-php/git/commits{/sha}\",\"comments_url\"\ + :\"https://api.github.com/repos/codecov/example-php/comments{/number}\",\"issue_comment_url\"\ + :\"https://api.github.com/repos/codecov/example-php/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-php/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-php/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-php/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-php/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-php/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-php/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-php/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-php/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-php/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-php/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-php/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-php/deployments\"\ + ,\"created_at\":\"2014-08-31T12:23:18Z\",\"updated_at\":\"2020-10-03T19:58:17Z\"\ + ,\"pushed_at\":\"2020-08-27T03:13:13Z\",\"git_url\":\"git://github.com/codecov/example-php.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-php.git\",\"clone_url\":\"https://github.com/codecov/example-php.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-php\",\"homepage\":\"https://codecov.io\"\ + ,\"size\":74,\"stargazers_count\":73,\"watchers_count\":73,\"language\":\"PHP\"\ + ,\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":58,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":1,\"license\":null,\"forks\"\ + :58,\"open_issues\":1,\"watchers\":73,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":24344106,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkyNDM0NDEwNg==\",\"name\":\"example-python\",\"full_name\"\ + :\"codecov/example-python\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-python\"\ + ,\"description\":\"Python coverage example\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-python\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-python/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-python/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-python/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-python/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-python/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-python/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-python/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-python/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-python/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-python/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-python/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-python/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-python/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-python/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-python/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-python/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-python/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-python/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-python/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-python/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-python/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-python/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-python/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-python/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-python/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-python/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-python/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-python/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-python/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-python/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-python/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-python/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-python/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-python/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-python/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-python/deployments\"\ + ,\"created_at\":\"2014-09-22T20:20:06Z\",\"updated_at\":\"2020-09-27T16:45:48Z\"\ + ,\"pushed_at\":\"2020-10-13T23:33:12Z\",\"git_url\":\"git://github.com/codecov/example-python.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-python.git\",\"clone_url\":\"\ + https://github.com/codecov/example-python.git\",\"svn_url\":\"https://github.com/codecov/example-python\"\ + ,\"homepage\":\"https://codecov.io\",\"size\":83,\"stargazers_count\":226,\"\ + watchers_count\":226,\"language\":\"Python\",\"has_issues\":false,\"has_projects\"\ + :false,\"has_downloads\":true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\"\ + :189,\"mirror_url\":null,\"archived\":false,\"disabled\":false,\"open_issues_count\"\ + :5,\"license\":null,\"forks\":189,\"open_issues\":5,\"watchers\":226,\"default_branch\"\ + :\"main\",\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"\ + id\":32196875,\"node_id\":\"MDEwOlJlcG9zaXRvcnkzMjE5Njg3NQ==\",\"name\":\"example-r\"\ + ,\"full_name\":\"codecov/example-r\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-r\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-r\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-r/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/example-r/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/example-r/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-r/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/example-r/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/example-r/issues/events{/number}\",\"\ + events_url\":\"https://api.github.com/repos/codecov/example-r/events\",\"assignees_url\"\ + :\"https://api.github.com/repos/codecov/example-r/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/example-r/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/example-r/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/example-r/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-r/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-r/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-r/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-r/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-r/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-r/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-r/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-r/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-r/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-r/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-r/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-r/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-r/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-r/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-r/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-r/merges\",\"\ + archive_url\":\"https://api.github.com/repos/codecov/example-r/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-r/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-r/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-r/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-r/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-r/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-r/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-r/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-r/deployments\"\ + ,\"created_at\":\"2015-03-14T05:08:48Z\",\"updated_at\":\"2020-09-04T02:47:07Z\"\ + ,\"pushed_at\":\"2020-09-04T02:47:04Z\",\"git_url\":\"git://github.com/codecov/example-r.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-r.git\",\"clone_url\":\"https://github.com/codecov/example-r.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-r\",\"homepage\":null,\"size\"\ + :67,\"stargazers_count\":14,\"watchers_count\":14,\"language\":\"R\",\"has_issues\"\ + :false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\":false,\"has_pages\"\ + :false,\"forks_count\":34,\"mirror_url\":null,\"archived\":false,\"disabled\"\ + :false,\"open_issues_count\":0,\"license\":null,\"forks\":34,\"open_issues\"\ + :0,\"watchers\":14,\"default_branch\":\"main\",\"permissions\":{\"admin\"\ + :true,\"push\":true,\"pull\":true}},{\"id\":24344263,\"node_id\":\"MDEwOlJlcG9zaXRvcnkyNDM0NDI2Mw==\"\ + ,\"name\":\"example-ruby\",\"full_name\":\"codecov/example-ruby\",\"private\"\ + :false,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-ruby\"\ + ,\"description\":\"Ruby coverage example\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-ruby\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-ruby/forks\",\"\ + keys_url\":\"https://api.github.com/repos/codecov/example-ruby/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-ruby/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-ruby/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/example-ruby/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/example-ruby/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-ruby/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-ruby/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-ruby/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-ruby/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-ruby/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-ruby/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-ruby/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-ruby/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-ruby/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-ruby/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-ruby/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-ruby/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-ruby/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-ruby/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-ruby/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-ruby/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-ruby/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-ruby/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-ruby/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-ruby/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-ruby/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-ruby/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-ruby/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-ruby/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-ruby/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-ruby/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-ruby/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-ruby/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-ruby/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-ruby/deployments\"\ + ,\"created_at\":\"2014-09-22T20:25:12Z\",\"updated_at\":\"2020-08-27T03:13:50Z\"\ + ,\"pushed_at\":\"2020-08-27T03:13:48Z\",\"git_url\":\"git://github.com/codecov/example-ruby.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-ruby.git\",\"clone_url\":\"https://github.com/codecov/example-ruby.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-ruby\",\"homepage\":\"https://codecov.io\"\ + ,\"size\":17,\"stargazers_count\":17,\"watchers_count\":17,\"language\":\"Ruby\"\ + ,\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":22,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"forks\"\ + :22,\"open_issues\":0,\"watchers\":17,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":47836521,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnk0NzgzNjUyMQ==\",\"name\":\"example-rust\",\"full_name\"\ + :\"codecov/example-rust\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-rust\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-rust\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-rust/forks\",\"\ + keys_url\":\"https://api.github.com/repos/codecov/example-rust/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-rust/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-rust/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/example-rust/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/example-rust/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-rust/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-rust/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-rust/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-rust/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-rust/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-rust/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-rust/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-rust/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-rust/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-rust/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-rust/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-rust/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-rust/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-rust/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-rust/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-rust/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-rust/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-rust/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-rust/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-rust/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-rust/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-rust/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-rust/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-rust/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-rust/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-rust/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-rust/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-rust/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-rust/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-rust/deployments\"\ + ,\"created_at\":\"2015-12-11T16:07:49Z\",\"updated_at\":\"2020-09-15T13:07:59Z\"\ + ,\"pushed_at\":\"2020-08-27T02:44:32Z\",\"git_url\":\"git://github.com/codecov/example-rust.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-rust.git\",\"clone_url\":\"https://github.com/codecov/example-rust.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-rust\",\"homepage\":null,\"\ + size\":22,\"stargazers_count\":58,\"watchers_count\":58,\"language\":\"Rust\"\ + ,\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":9,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"forks\"\ + :9,\"open_issues\":0,\"watchers\":58,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":24330864,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkyNDMzMDg2NA==\",\"name\":\"example-scala\",\"full_name\"\ + :\"codecov/example-scala\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-scala\"\ + ,\"description\":\"Scala coverage example\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-scala\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-scala/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-scala/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-scala/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-scala/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-scala/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-scala/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-scala/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-scala/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-scala/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-scala/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-scala/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-scala/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-scala/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-scala/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-scala/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-scala/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-scala/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-scala/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-scala/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-scala/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-scala/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-scala/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-scala/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-scala/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-scala/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-scala/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-scala/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-scala/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-scala/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-scala/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-scala/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-scala/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-scala/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-scala/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-scala/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-scala/deployments\"\ + ,\"created_at\":\"2014-09-22T14:00:30Z\",\"updated_at\":\"2020-09-21T22:56:00Z\"\ + ,\"pushed_at\":\"2020-08-28T19:36:06Z\",\"git_url\":\"git://github.com/codecov/example-scala.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-scala.git\",\"clone_url\":\"https://github.com/codecov/example-scala.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-scala\",\"homepage\":\"https://codecov.io/\"\ + ,\"size\":58,\"stargazers_count\":26,\"watchers_count\":26,\"language\":\"Scala\"\ + ,\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":23,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":1,\"license\":null,\"forks\"\ + :23,\"open_issues\":1,\"watchers\":26,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":24569996,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkyNDU2OTk5Ng==\",\"name\":\"example-scala-maven\",\"full_name\"\ + :\"codecov/example-scala-maven\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-scala-maven\"\ + ,\"description\":\"Scala via Maven coverage example\",\"fork\":false,\"url\"\ + :\"https://api.github.com/repos/codecov/example-scala-maven\",\"forks_url\"\ + :\"https://api.github.com/repos/codecov/example-scala-maven/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/example-scala-maven/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-scala-maven/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-scala-maven/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-scala-maven/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-scala-maven/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-scala-maven/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-scala-maven/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-scala-maven/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-scala-maven/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/example-scala-maven/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-scala-maven/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-scala-maven/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-scala-maven/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-scala-maven/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-scala-maven/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-scala-maven/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-scala-maven/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-scala-maven/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-scala-maven/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-scala-maven/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-scala-maven/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-scala-maven/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-scala-maven/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-scala-maven/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-scala-maven/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-scala-maven/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-scala-maven/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-scala-maven/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-scala-maven/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-scala-maven/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-scala-maven/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-scala-maven/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-scala-maven/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-scala-maven/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-scala-maven/deployments\"\ + ,\"created_at\":\"2014-09-28T20:42:41Z\",\"updated_at\":\"2020-10-02T16:17:03Z\"\ + ,\"pushed_at\":\"2020-08-27T03:13:59Z\",\"git_url\":\"git://github.com/codecov/example-scala-maven.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-scala-maven.git\",\"clone_url\"\ + :\"https://github.com/codecov/example-scala-maven.git\",\"svn_url\":\"https://github.com/codecov/example-scala-maven\"\ + ,\"homepage\":\"https://codecov.io\",\"size\":7,\"stargazers_count\":1,\"watchers_count\"\ + :1,\"language\":\"Scala\",\"has_issues\":false,\"has_projects\":false,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":9,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\"\ + :null,\"forks\":9,\"open_issues\":0,\"watchers\":1,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":42443308,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnk0MjQ0MzMwOA==\",\"name\":\"example-swift\",\"\ + full_name\":\"codecov/example-swift\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-swift\"\ + ,\"description\":\"Codecov: Swift coverage example\",\"fork\":false,\"url\"\ + :\"https://api.github.com/repos/codecov/example-swift\",\"forks_url\":\"https://api.github.com/repos/codecov/example-swift/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-swift/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-swift/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-swift/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-swift/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-swift/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-swift/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-swift/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-swift/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-swift/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-swift/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-swift/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-swift/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-swift/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-swift/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-swift/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-swift/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-swift/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-swift/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-swift/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-swift/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-swift/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-swift/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-swift/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-swift/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-swift/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-swift/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-swift/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-swift/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-swift/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-swift/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-swift/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-swift/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-swift/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-swift/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-swift/deployments\"\ + ,\"created_at\":\"2015-09-14T10:54:57Z\",\"updated_at\":\"2020-10-13T09:54:09Z\"\ + ,\"pushed_at\":\"2020-08-27T03:12:47Z\",\"git_url\":\"git://github.com/codecov/example-swift.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-swift.git\",\"clone_url\":\"https://github.com/codecov/example-swift.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-swift\",\"homepage\":\"https://codecov.io\"\ + ,\"size\":121,\"stargazers_count\":109,\"watchers_count\":109,\"language\":\"\ + Swift\",\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"\ + has_wiki\":false,\"has_pages\":false,\"forks_count\":78,\"mirror_url\":null,\"\ + archived\":false,\"disabled\":false,\"open_issues_count\":3,\"license\":null,\"\ + forks\":78,\"open_issues\":3,\"watchers\":109,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":59709356,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnk1OTcwOTM1Ng==\",\"name\":\"example-typescript\"\ + ,\"full_name\":\"codecov/example-typescript\",\"private\":false,\"owner\":{\"\ + login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-typescript\"\ + ,\"description\":\"Example repo for uploading reports to Codecov https://codecov.io\"\ + ,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-typescript\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-typescript/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-typescript/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-typescript/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-typescript/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-typescript/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-typescript/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-typescript/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-typescript/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-typescript/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-typescript/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/example-typescript/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-typescript/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-typescript/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-typescript/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-typescript/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-typescript/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-typescript/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-typescript/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-typescript/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-typescript/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-typescript/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-typescript/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-typescript/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-typescript/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-typescript/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-typescript/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-typescript/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-typescript/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-typescript/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-typescript/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-typescript/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-typescript/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-typescript/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-typescript/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-typescript/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-typescript/deployments\"\ + ,\"created_at\":\"2016-05-26T01:19:38Z\",\"updated_at\":\"2020-09-10T06:20:16Z\"\ + ,\"pushed_at\":\"2020-08-27T03:13:36Z\",\"git_url\":\"git://github.com/codecov/example-typescript.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-typescript.git\",\"clone_url\"\ + :\"https://github.com/codecov/example-typescript.git\",\"svn_url\":\"https://github.com/codecov/example-typescript\"\ + ,\"homepage\":null,\"size\":49,\"stargazers_count\":32,\"watchers_count\":32,\"\ + language\":\"TypeScript\",\"has_issues\":false,\"has_projects\":false,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":49,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":2,\"license\"\ + :{\"key\":\"mit\",\"name\":\"MIT License\",\"spdx_id\":\"MIT\",\"url\":\"https://api.github.com/licenses/mit\"\ + ,\"node_id\":\"MDc6TGljZW5zZTEz\"},\"forks\":49,\"open_issues\":2,\"watchers\"\ + :32,\"default_branch\":\"main\",\"permissions\":{\"admin\":false,\"push\"\ + :true,\"pull\":true}},{\"id\":78723389,\"node_id\":\"MDEwOlJlcG9zaXRvcnk3ODcyMzM4OQ==\"\ + ,\"name\":\"example-typescript-vscode-extension\",\"full_name\":\"codecov/example-typescript-vscode-extension\"\ + ,\"private\":false,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\"\ + :\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-typescript-vscode-extension\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-typescript-vscode-extension/deployments\"\ + ,\"created_at\":\"2017-01-12T08:25:12Z\",\"updated_at\":\"2020-09-10T15:40:05Z\"\ + ,\"pushed_at\":\"2020-09-04T02:48:03Z\",\"git_url\":\"git://github.com/codecov/example-typescript-vscode-extension.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-typescript-vscode-extension.git\"\ + ,\"clone_url\":\"https://github.com/codecov/example-typescript-vscode-extension.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-typescript-vscode-extension\"\ + ,\"homepage\":null,\"size\":23,\"stargazers_count\":22,\"watchers_count\":22,\"\ + language\":\"TypeScript\",\"has_issues\":false,\"has_projects\":false,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":12,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":2,\"license\"\ + :{\"key\":\"mit\",\"name\":\"MIT License\",\"spdx_id\":\"MIT\",\"url\":\"https://api.github.com/licenses/mit\"\ + ,\"node_id\":\"MDc6TGljZW5zZTEz\"},\"forks\":12,\"open_issues\":2,\"watchers\"\ + :22,\"default_branch\":\"main\",\"permissions\":{\"admin\":false,\"push\"\ + :true,\"pull\":true}},{\"id\":61906398,\"node_id\":\"MDEwOlJlcG9zaXRvcnk2MTkwNjM5OA==\"\ + ,\"name\":\"example-vala\",\"full_name\":\"codecov/example-vala\",\"private\"\ + :false,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-vala\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-vala\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-vala/forks\",\"\ + keys_url\":\"https://api.github.com/repos/codecov/example-vala/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-vala/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-vala/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/example-vala/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/example-vala/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-vala/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-vala/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-vala/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-vala/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-vala/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-vala/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-vala/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-vala/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-vala/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-vala/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-vala/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-vala/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-vala/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-vala/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-vala/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-vala/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-vala/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-vala/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-vala/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-vala/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-vala/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-vala/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-vala/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-vala/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-vala/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-vala/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-vala/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-vala/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-vala/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-vala/deployments\"\ + ,\"created_at\":\"2016-06-24T19:02:52Z\",\"updated_at\":\"2020-08-30T00:30:04Z\"\ + ,\"pushed_at\":\"2018-04-26T08:48:21Z\",\"git_url\":\"git://github.com/codecov/example-vala.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-vala.git\",\"clone_url\":\"https://github.com/codecov/example-vala.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-vala\",\"homepage\":null,\"\ + size\":8,\"stargazers_count\":7,\"watchers_count\":7,\"language\":\"Vala\",\"\ + has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":3,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"forks\"\ + :3,\"open_issues\":0,\"watchers\":7,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":24567595,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkyNDU2NzU5NQ==\",\"name\":\"example-xtend\",\"full_name\"\ + :\"codecov/example-xtend\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/example-xtend\"\ + ,\"description\":\"Xtend coverage example\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/example-xtend\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/example-xtend/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/example-xtend/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/example-xtend/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/example-xtend/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/example-xtend/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/example-xtend/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/example-xtend/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/example-xtend/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/example-xtend/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/example-xtend/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/example-xtend/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/example-xtend/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/example-xtend/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/example-xtend/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/example-xtend/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/example-xtend/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/example-xtend/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/example-xtend/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/example-xtend/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/example-xtend/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/example-xtend/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/example-xtend/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/example-xtend/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/example-xtend/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/example-xtend/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/example-xtend/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/example-xtend/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/example-xtend/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/example-xtend/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/example-xtend/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/example-xtend/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/example-xtend/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/example-xtend/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/example-xtend/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/example-xtend/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/example-xtend/deployments\"\ + ,\"created_at\":\"2014-09-28T18:54:44Z\",\"updated_at\":\"2018-04-26T08:48:48Z\"\ + ,\"pushed_at\":\"2018-04-26T08:48:47Z\",\"git_url\":\"git://github.com/codecov/example-xtend.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/example-xtend.git\",\"clone_url\":\"https://github.com/codecov/example-xtend.git\"\ + ,\"svn_url\":\"https://github.com/codecov/example-xtend\",\"homepage\":\"https://codecov.io\"\ + ,\"size\":8,\"stargazers_count\":2,\"watchers_count\":2,\"language\":\"Xtend\"\ + ,\"has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":3,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"forks\"\ + :3,\"open_issues\":0,\"watchers\":2,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":163929956,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkxNjM5Mjk5NTY=\",\"name\":\"freshdesk-app\",\"full_name\"\ + :\"codecov/freshdesk-app\",\"private\":true,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/freshdesk-app\"\ + ,\"description\":\"Freshdesk App for Codecov\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/freshdesk-app\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/freshdesk-app/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/freshdesk-app/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/freshdesk-app/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/freshdesk-app/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/freshdesk-app/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/freshdesk-app/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/freshdesk-app/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/freshdesk-app/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/freshdesk-app/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/freshdesk-app/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/freshdesk-app/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/freshdesk-app/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/freshdesk-app/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/freshdesk-app/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/freshdesk-app/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/freshdesk-app/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/freshdesk-app/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/freshdesk-app/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/freshdesk-app/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/freshdesk-app/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/freshdesk-app/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/freshdesk-app/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/freshdesk-app/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/freshdesk-app/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/freshdesk-app/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/freshdesk-app/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/freshdesk-app/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/freshdesk-app/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/freshdesk-app/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/freshdesk-app/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/freshdesk-app/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/freshdesk-app/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/freshdesk-app/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/freshdesk-app/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/freshdesk-app/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/freshdesk-app/deployments\"\ + ,\"created_at\":\"2019-01-03T06:03:21Z\",\"updated_at\":\"2019-01-30T17:20:19Z\"\ + ,\"pushed_at\":\"2019-01-30T17:20:17Z\",\"git_url\":\"git://github.com/codecov/freshdesk-app.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/freshdesk-app.git\",\"clone_url\":\"https://github.com/codecov/freshdesk-app.git\"\ + ,\"svn_url\":\"https://github.com/codecov/freshdesk-app\",\"homepage\":null,\"\ + size\":63,\"stargazers_count\":0,\"watchers_count\":0,\"language\":\"HTML\"\ + ,\"has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\"\ + :true,\"has_pages\":false,\"forks_count\":0,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"forks\"\ + :0,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":204369089,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkyMDQzNjkwODk=\",\"name\":\"go-standard\",\"full_name\"\ + :\"codecov/go-standard\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/go-standard\"\ + ,\"description\":\"Codecov coverage standard for go\",\"fork\":false,\"url\"\ + :\"https://api.github.com/repos/codecov/go-standard\",\"forks_url\":\"https://api.github.com/repos/codecov/go-standard/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/go-standard/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/go-standard/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/go-standard/teams\",\"\ + hooks_url\":\"https://api.github.com/repos/codecov/go-standard/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/go-standard/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/go-standard/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/go-standard/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/go-standard/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/go-standard/tags\",\"blobs_url\"\ + :\"https://api.github.com/repos/codecov/go-standard/git/blobs{/sha}\",\"git_tags_url\"\ + :\"https://api.github.com/repos/codecov/go-standard/git/tags{/sha}\",\"git_refs_url\"\ + :\"https://api.github.com/repos/codecov/go-standard/git/refs{/sha}\",\"trees_url\"\ + :\"https://api.github.com/repos/codecov/go-standard/git/trees{/sha}\",\"statuses_url\"\ + :\"https://api.github.com/repos/codecov/go-standard/statuses/{sha}\",\"languages_url\"\ + :\"https://api.github.com/repos/codecov/go-standard/languages\",\"stargazers_url\"\ + :\"https://api.github.com/repos/codecov/go-standard/stargazers\",\"contributors_url\"\ + :\"https://api.github.com/repos/codecov/go-standard/contributors\",\"subscribers_url\"\ + :\"https://api.github.com/repos/codecov/go-standard/subscribers\",\"subscription_url\"\ + :\"https://api.github.com/repos/codecov/go-standard/subscription\",\"commits_url\"\ + :\"https://api.github.com/repos/codecov/go-standard/commits{/sha}\",\"git_commits_url\"\ + :\"https://api.github.com/repos/codecov/go-standard/git/commits{/sha}\",\"comments_url\"\ + :\"https://api.github.com/repos/codecov/go-standard/comments{/number}\",\"issue_comment_url\"\ + :\"https://api.github.com/repos/codecov/go-standard/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/go-standard/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/go-standard/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/go-standard/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/go-standard/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/go-standard/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/go-standard/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/go-standard/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/go-standard/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/go-standard/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/go-standard/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/go-standard/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/go-standard/deployments\"\ + ,\"created_at\":\"2019-08-26T00:59:07Z\",\"updated_at\":\"2020-10-13T20:05:53Z\"\ + ,\"pushed_at\":\"2020-10-13T20:05:50Z\",\"git_url\":\"git://github.com/codecov/go-standard.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/go-standard.git\",\"clone_url\":\"https://github.com/codecov/go-standard.git\"\ + ,\"svn_url\":\"https://github.com/codecov/go-standard\",\"homepage\":null,\"\ + size\":77,\"stargazers_count\":3,\"watchers_count\":3,\"language\":\"Python\"\ + ,\"has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\"\ + :true,\"has_pages\":false,\"forks_count\":12,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"forks\"\ + :12,\"open_issues\":0,\"watchers\":3,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":206460969,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkyMDY0NjA5Njk=\",\"name\":\"java-standard\",\"full_name\"\ + :\"codecov/java-standard\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/java-standard\"\ + ,\"description\":\"Codecov coverage standard for Java\",\"fork\":false,\"url\"\ + :\"https://api.github.com/repos/codecov/java-standard\",\"forks_url\":\"https://api.github.com/repos/codecov/java-standard/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/java-standard/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/java-standard/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/java-standard/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/java-standard/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/java-standard/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/java-standard/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/java-standard/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/java-standard/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/java-standard/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/java-standard/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/java-standard/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/java-standard/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/java-standard/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/java-standard/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/java-standard/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/java-standard/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/java-standard/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/java-standard/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/java-standard/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/java-standard/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/java-standard/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/java-standard/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/java-standard/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/java-standard/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/java-standard/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/java-standard/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/java-standard/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/java-standard/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/java-standard/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/java-standard/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/java-standard/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/java-standard/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/java-standard/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/java-standard/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/java-standard/deployments\"\ + ,\"created_at\":\"2019-09-05T02:49:55Z\",\"updated_at\":\"2020-10-13T20:06:00Z\"\ + ,\"pushed_at\":\"2020-10-13T20:05:58Z\",\"git_url\":\"git://github.com/codecov/java-standard.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/java-standard.git\",\"clone_url\":\"https://github.com/codecov/java-standard.git\"\ + ,\"svn_url\":\"https://github.com/codecov/java-standard\",\"homepage\":null,\"\ + size\":139,\"stargazers_count\":5,\"watchers_count\":5,\"language\":\"Python\"\ + ,\"has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\"\ + :true,\"has_pages\":false,\"forks_count\":8,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"forks\"\ + :8,\"open_issues\":0,\"watchers\":5,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":144112628,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkxNDQxMTI2Mjg=\",\"name\":\"k8s\",\"full_name\":\"codecov/k8s\"\ + ,\"private\":true,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\"\ + :\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/k8s\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/k8s\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/k8s/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/k8s/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/k8s/collaborators{/collaborator}\",\"\ + teams_url\":\"https://api.github.com/repos/codecov/k8s/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/k8s/hooks\",\"issue_events_url\":\"\ + https://api.github.com/repos/codecov/k8s/issues/events{/number}\",\"events_url\"\ + :\"https://api.github.com/repos/codecov/k8s/events\",\"assignees_url\":\"https://api.github.com/repos/codecov/k8s/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/k8s/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/k8s/tags\",\"blobs_url\"\ + :\"https://api.github.com/repos/codecov/k8s/git/blobs{/sha}\",\"git_tags_url\"\ + :\"https://api.github.com/repos/codecov/k8s/git/tags{/sha}\",\"git_refs_url\"\ + :\"https://api.github.com/repos/codecov/k8s/git/refs{/sha}\",\"trees_url\":\"\ + https://api.github.com/repos/codecov/k8s/git/trees{/sha}\",\"statuses_url\"\ + :\"https://api.github.com/repos/codecov/k8s/statuses/{sha}\",\"languages_url\"\ + :\"https://api.github.com/repos/codecov/k8s/languages\",\"stargazers_url\":\"\ + https://api.github.com/repos/codecov/k8s/stargazers\",\"contributors_url\":\"\ + https://api.github.com/repos/codecov/k8s/contributors\",\"subscribers_url\"\ + :\"https://api.github.com/repos/codecov/k8s/subscribers\",\"subscription_url\"\ + :\"https://api.github.com/repos/codecov/k8s/subscription\",\"commits_url\":\"\ + https://api.github.com/repos/codecov/k8s/commits{/sha}\",\"git_commits_url\"\ + :\"https://api.github.com/repos/codecov/k8s/git/commits{/sha}\",\"comments_url\"\ + :\"https://api.github.com/repos/codecov/k8s/comments{/number}\",\"issue_comment_url\"\ + :\"https://api.github.com/repos/codecov/k8s/issues/comments{/number}\",\"contents_url\"\ + :\"https://api.github.com/repos/codecov/k8s/contents/{+path}\",\"compare_url\"\ + :\"https://api.github.com/repos/codecov/k8s/compare/{base}...{head}\",\"merges_url\"\ + :\"https://api.github.com/repos/codecov/k8s/merges\",\"archive_url\":\"https://api.github.com/repos/codecov/k8s/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/k8s/downloads\",\"\ + issues_url\":\"https://api.github.com/repos/codecov/k8s/issues{/number}\",\"\ + pulls_url\":\"https://api.github.com/repos/codecov/k8s/pulls{/number}\",\"milestones_url\"\ + :\"https://api.github.com/repos/codecov/k8s/milestones{/number}\",\"notifications_url\"\ + :\"https://api.github.com/repos/codecov/k8s/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/k8s/labels{/name}\",\"\ + releases_url\":\"https://api.github.com/repos/codecov/k8s/releases{/id}\",\"\ + deployments_url\":\"https://api.github.com/repos/codecov/k8s/deployments\",\"\ + created_at\":\"2018-08-09T06:53:39Z\",\"updated_at\":\"2020-09-18T19:26:44Z\"\ + ,\"pushed_at\":\"2020-09-18T19:26:41Z\",\"git_url\":\"git://github.com/codecov/k8s.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/k8s.git\",\"clone_url\":\"https://github.com/codecov/k8s.git\"\ + ,\"svn_url\":\"https://github.com/codecov/k8s\",\"homepage\":\"\",\"size\":148,\"\ + stargazers_count\":0,\"watchers_count\":0,\"language\":\"Shell\",\"has_issues\"\ + :true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\":true,\"has_pages\"\ + :false,\"forks_count\":0,\"mirror_url\":null,\"archived\":false,\"disabled\"\ + :false,\"open_issues_count\":15,\"license\":null,\"forks\":0,\"open_issues\"\ + :15,\"watchers\":0,\"default_branch\":\"main\",\"permissions\":{\"admin\"\ + :true,\"push\":true,\"pull\":true}},{\"id\":225711958,\"node_id\":\"MDEwOlJlcG9zaXRvcnkyMjU3MTE5NTg=\"\ + ,\"name\":\"k8s-v2\",\"full_name\":\"codecov/k8s-v2\",\"private\":true,\"owner\"\ + :{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/k8s-v2\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/k8s-v2\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/k8s-v2/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/k8s-v2/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/k8s-v2/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/k8s-v2/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/k8s-v2/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/k8s-v2/issues/events{/number}\",\"events_url\"\ + :\"https://api.github.com/repos/codecov/k8s-v2/events\",\"assignees_url\":\"\ + https://api.github.com/repos/codecov/k8s-v2/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/k8s-v2/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/k8s-v2/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/k8s-v2/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/k8s-v2/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/k8s-v2/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/k8s-v2/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/k8s-v2/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/k8s-v2/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/k8s-v2/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/k8s-v2/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/k8s-v2/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/k8s-v2/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/k8s-v2/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/k8s-v2/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/k8s-v2/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/k8s-v2/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/k8s-v2/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/k8s-v2/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/k8s-v2/merges\",\"archive_url\"\ + :\"https://api.github.com/repos/codecov/k8s-v2/{archive_format}{/ref}\",\"downloads_url\"\ + :\"https://api.github.com/repos/codecov/k8s-v2/downloads\",\"issues_url\":\"\ + https://api.github.com/repos/codecov/k8s-v2/issues{/number}\",\"pulls_url\"\ + :\"https://api.github.com/repos/codecov/k8s-v2/pulls{/number}\",\"milestones_url\"\ + :\"https://api.github.com/repos/codecov/k8s-v2/milestones{/number}\",\"notifications_url\"\ + :\"https://api.github.com/repos/codecov/k8s-v2/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/k8s-v2/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/k8s-v2/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/k8s-v2/deployments\"\ + ,\"created_at\":\"2019-12-03T20:43:31Z\",\"updated_at\":\"2020-10-13T20:28:39Z\"\ + ,\"pushed_at\":\"2020-10-13T20:28:37Z\",\"git_url\":\"git://github.com/codecov/k8s-v2.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/k8s-v2.git\",\"clone_url\":\"https://github.com/codecov/k8s-v2.git\"\ + ,\"svn_url\":\"https://github.com/codecov/k8s-v2\",\"homepage\":null,\"size\"\ + :331,\"stargazers_count\":0,\"watchers_count\":0,\"language\":null,\"has_issues\"\ + :true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\":true,\"has_pages\"\ + :false,\"forks_count\":0,\"mirror_url\":null,\"archived\":false,\"disabled\"\ + :false,\"open_issues_count\":4,\"license\":null,\"forks\":0,\"open_issues\"\ + :4,\"watchers\":0,\"default_branch\":\"main\",\"permissions\":{\"admin\":true,\"\ + push\":true,\"pull\":true}},{\"id\":204188879,\"node_id\":\"MDEwOlJlcG9zaXRvcnkyMDQxODg4Nzk=\"\ + ,\"name\":\"kotlin-standard\",\"full_name\":\"codecov/kotlin-standard\",\"private\"\ + :false,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/kotlin-standard\"\ + ,\"description\":\"Codecov coverage standard for Kotlin\",\"fork\":false,\"\ + url\":\"https://api.github.com/repos/codecov/kotlin-standard\",\"forks_url\"\ + :\"https://api.github.com/repos/codecov/kotlin-standard/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/kotlin-standard/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/kotlin-standard/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/kotlin-standard/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/kotlin-standard/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/kotlin-standard/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/kotlin-standard/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/kotlin-standard/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/kotlin-standard/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/kotlin-standard/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/kotlin-standard/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/kotlin-standard/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/kotlin-standard/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/kotlin-standard/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/kotlin-standard/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/kotlin-standard/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/kotlin-standard/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/kotlin-standard/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/kotlin-standard/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/kotlin-standard/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/kotlin-standard/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/kotlin-standard/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/kotlin-standard/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/kotlin-standard/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/kotlin-standard/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/kotlin-standard/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/kotlin-standard/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/kotlin-standard/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/kotlin-standard/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/kotlin-standard/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/kotlin-standard/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/kotlin-standard/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/kotlin-standard/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/kotlin-standard/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/kotlin-standard/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/kotlin-standard/deployments\"\ + ,\"created_at\":\"2019-08-24T17:17:09Z\",\"updated_at\":\"2020-10-13T20:05:49Z\"\ + ,\"pushed_at\":\"2020-10-13T20:05:46Z\",\"git_url\":\"git://github.com/codecov/kotlin-standard.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/kotlin-standard.git\",\"clone_url\":\"\ + https://github.com/codecov/kotlin-standard.git\",\"svn_url\":\"https://github.com/codecov/kotlin-standard\"\ + ,\"homepage\":null,\"size\":163,\"stargazers_count\":3,\"watchers_count\":3,\"\ + language\":\"Kotlin\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":true,\"has_pages\":false,\"forks_count\":3,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\"\ + :null,\"forks\":3,\"open_issues\":0,\"watchers\":3,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":22689010,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkyMjY4OTAxMA==\",\"name\":\"media\",\"full_name\"\ + :\"codecov/media\",\"private\":false,\"owner\":{\"login\":\"codecov\",\"id\"\ + :8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"\ + https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\"\ + ,\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/media\"\ + ,\"description\":\"Press and marketing resources\",\"fork\":false,\"url\":\"\ + https://api.github.com/repos/codecov/media\",\"forks_url\":\"https://api.github.com/repos/codecov/media/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/media/keys{/key_id}\",\"\ + collaborators_url\":\"https://api.github.com/repos/codecov/media/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/media/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/media/hooks\",\"issue_events_url\":\"\ + https://api.github.com/repos/codecov/media/issues/events{/number}\",\"events_url\"\ + :\"https://api.github.com/repos/codecov/media/events\",\"assignees_url\":\"\ + https://api.github.com/repos/codecov/media/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/media/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/media/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/media/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/media/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/media/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/media/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/media/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/media/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/media/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/media/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/media/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/media/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/media/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/media/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/media/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/media/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/media/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/media/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/media/merges\",\"archive_url\"\ + :\"https://api.github.com/repos/codecov/media/{archive_format}{/ref}\",\"downloads_url\"\ + :\"https://api.github.com/repos/codecov/media/downloads\",\"issues_url\":\"\ + https://api.github.com/repos/codecov/media/issues{/number}\",\"pulls_url\":\"\ + https://api.github.com/repos/codecov/media/pulls{/number}\",\"milestones_url\"\ + :\"https://api.github.com/repos/codecov/media/milestones{/number}\",\"notifications_url\"\ + :\"https://api.github.com/repos/codecov/media/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/media/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/media/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/media/deployments\"\ + ,\"created_at\":\"2014-08-06T16:02:47Z\",\"updated_at\":\"2020-09-10T15:30:02Z\"\ + ,\"pushed_at\":\"2020-09-10T15:30:02Z\",\"git_url\":\"git://github.com/codecov/media.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/media.git\",\"clone_url\":\"https://github.com/codecov/media.git\"\ + ,\"svn_url\":\"https://github.com/codecov/media\",\"homepage\":\"https://codecov.io\"\ + ,\"size\":12364,\"stargazers_count\":2,\"watchers_count\":2,\"language\":null,\"\ + has_issues\":false,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":6,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":3,\"license\":null,\"forks\"\ + :6,\"open_issues\":3,\"watchers\":2,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":150576343,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkxNTA1NzYzNDM=\",\"name\":\"migrate\",\"full_name\":\"\ + codecov/migrate\",\"private\":false,\"owner\":{\"login\":\"codecov\",\"id\"\ + :8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"\ + https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\"\ + ,\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/migrate\"\ + ,\"description\":\"A collection of docker images and scripts to provide an upgrade\ + \ path for some users of Codecov Enterprise 4.3.9 to 4.4.x. Please review these\ + \ docs carefully if you plan to upgrade your Codecov Enterprise installation.\"\ + ,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/migrate\",\"\ + forks_url\":\"https://api.github.com/repos/codecov/migrate/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/migrate/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/migrate/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/migrate/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/migrate/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/migrate/issues/events{/number}\",\"\ + events_url\":\"https://api.github.com/repos/codecov/migrate/events\",\"assignees_url\"\ + :\"https://api.github.com/repos/codecov/migrate/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/migrate/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/migrate/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/migrate/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/migrate/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/migrate/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/migrate/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/migrate/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/migrate/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/migrate/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/migrate/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/migrate/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/migrate/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/migrate/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/migrate/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/migrate/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/migrate/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/migrate/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/migrate/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/migrate/merges\",\"archive_url\"\ + :\"https://api.github.com/repos/codecov/migrate/{archive_format}{/ref}\",\"\ + downloads_url\":\"https://api.github.com/repos/codecov/migrate/downloads\",\"\ + issues_url\":\"https://api.github.com/repos/codecov/migrate/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/migrate/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/migrate/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/migrate/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/migrate/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/migrate/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/migrate/deployments\"\ + ,\"created_at\":\"2018-09-27T11:30:18Z\",\"updated_at\":\"2020-09-09T16:59:15Z\"\ + ,\"pushed_at\":\"2020-09-09T16:59:13Z\",\"git_url\":\"git://github.com/codecov/migrate.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/migrate.git\",\"clone_url\":\"https://github.com/codecov/migrate.git\"\ + ,\"svn_url\":\"https://github.com/codecov/migrate\",\"homepage\":\"https://codecov.io\"\ + ,\"size\":84,\"stargazers_count\":1,\"watchers_count\":1,\"language\":\"PLpgSQL\"\ + ,\"has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":1,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":1,\"license\":null,\"forks\"\ + :1,\"open_issues\":1,\"watchers\":1,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":156281130,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkxNTYyODExMzA=\",\"name\":\"migration-tests\",\"full_name\"\ + :\"codecov/migration-tests\",\"private\":true,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/migration-tests\"\ + ,\"description\":\"Tests for migrating from Codecov Enterprise 4.3.9 to 4.4.0\"\ + ,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/migration-tests\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/migration-tests/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/migration-tests/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/migration-tests/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/migration-tests/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/migration-tests/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/migration-tests/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/migration-tests/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/migration-tests/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/migration-tests/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/migration-tests/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/migration-tests/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/migration-tests/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/migration-tests/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/migration-tests/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/migration-tests/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/migration-tests/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/migration-tests/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/migration-tests/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/migration-tests/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/migration-tests/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/migration-tests/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/migration-tests/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/migration-tests/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/migration-tests/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/migration-tests/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/migration-tests/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/migration-tests/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/migration-tests/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/migration-tests/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/migration-tests/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/migration-tests/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/migration-tests/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/migration-tests/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/migration-tests/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/migration-tests/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/migration-tests/deployments\"\ + ,\"created_at\":\"2018-11-05T20:49:31Z\",\"updated_at\":\"2018-11-16T18:59:13Z\"\ + ,\"pushed_at\":\"2018-11-16T18:59:12Z\",\"git_url\":\"git://github.com/codecov/migration-tests.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/migration-tests.git\",\"clone_url\":\"\ + https://github.com/codecov/migration-tests.git\",\"svn_url\":\"https://github.com/codecov/migration-tests\"\ + ,\"homepage\":null,\"size\":383,\"stargazers_count\":0,\"watchers_count\":0,\"\ + language\":\"Python\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":true,\"has_pages\":false,\"forks_count\":0,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\"\ + :null,\"forks\":0,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":39343510,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkzOTM0MzUxMA==\",\"name\":\"nginx-buildpack\"\ + ,\"full_name\":\"codecov/nginx-buildpack\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/nginx-buildpack\"\ + ,\"description\":\"Run NGINX in front of your app server on Heroku\",\"fork\"\ + :true,\"url\":\"https://api.github.com/repos/codecov/nginx-buildpack\",\"forks_url\"\ + :\"https://api.github.com/repos/codecov/nginx-buildpack/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/nginx-buildpack/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/nginx-buildpack/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/nginx-buildpack/deployments\"\ + ,\"created_at\":\"2015-07-19T18:04:49Z\",\"updated_at\":\"2020-09-13T14:52:37Z\"\ + ,\"pushed_at\":\"2018-07-24T12:53:33Z\",\"git_url\":\"git://github.com/codecov/nginx-buildpack.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/nginx-buildpack.git\",\"clone_url\":\"\ + https://github.com/codecov/nginx-buildpack.git\",\"svn_url\":\"https://github.com/codecov/nginx-buildpack\"\ + ,\"homepage\":\"\",\"size\":7811,\"stargazers_count\":0,\"watchers_count\":0,\"\ + language\":\"Shell\",\"has_issues\":false,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":2,\"mirror_url\"\ + :null,\"archived\":true,\"disabled\":false,\"open_issues_count\":0,\"license\"\ + :null,\"forks\":2,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":209791850,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkyMDk3OTE4NTA=\",\"name\":\"old-codecov-ui\",\"\ + full_name\":\"codecov/old-codecov-ui\",\"private\":true,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/old-codecov-ui\"\ + ,\"description\":\"Component Library & Design System\",\"fork\":false,\"url\"\ + :\"https://api.github.com/repos/codecov/old-codecov-ui\",\"forks_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/old-codecov-ui/deployments\"\ + ,\"created_at\":\"2019-09-20T13:01:08Z\",\"updated_at\":\"2020-05-21T18:50:58Z\"\ + ,\"pushed_at\":\"2019-12-08T23:06:53Z\",\"git_url\":\"git://github.com/codecov/old-codecov-ui.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/old-codecov-ui.git\",\"clone_url\":\"\ + https://github.com/codecov/old-codecov-ui.git\",\"svn_url\":\"https://github.com/codecov/old-codecov-ui\"\ + ,\"homepage\":\"https://codecov-ui.netlify.com\",\"size\":576,\"stargazers_count\"\ + :0,\"watchers_count\":0,\"language\":\"Vue\",\"has_issues\":true,\"has_projects\"\ + :true,\"has_downloads\":true,\"has_wiki\":true,\"has_pages\":false,\"forks_count\"\ + :0,\"mirror_url\":null,\"archived\":true,\"disabled\":false,\"open_issues_count\"\ + :0,\"license\":null,\"forks\":0,\"open_issues\":0,\"watchers\":0,\"default_branch\"\ + :\"main\",\"permissions\":{\"admin\":false,\"push\":false,\"pull\":true}},{\"\ + id\":167416484,\"node_id\":\"MDEwOlJlcG9zaXRvcnkxNjc0MTY0ODQ=\",\"name\":\"\ + python-sample-repo\",\"full_name\":\"codecov/python-sample-repo\",\"private\"\ + :true,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/python-sample-repo\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/python-sample-repo\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/python-sample-repo/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/python-sample-repo/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/python-sample-repo/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/python-sample-repo/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/python-sample-repo/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/python-sample-repo/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/python-sample-repo/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/python-sample-repo/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/python-sample-repo/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/python-sample-repo/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/python-sample-repo/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/python-sample-repo/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/python-sample-repo/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/python-sample-repo/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/python-sample-repo/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/python-sample-repo/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/python-sample-repo/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/python-sample-repo/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/python-sample-repo/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/python-sample-repo/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/python-sample-repo/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/python-sample-repo/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/python-sample-repo/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/python-sample-repo/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/python-sample-repo/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/python-sample-repo/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/python-sample-repo/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/python-sample-repo/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/python-sample-repo/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/python-sample-repo/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/python-sample-repo/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/python-sample-repo/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/python-sample-repo/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/python-sample-repo/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/python-sample-repo/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/python-sample-repo/deployments\"\ + ,\"created_at\":\"2019-01-24T18:31:15Z\",\"updated_at\":\"2020-04-23T06:34:53Z\"\ + ,\"pushed_at\":\"2020-07-21T05:47:16Z\",\"git_url\":\"git://github.com/codecov/python-sample-repo.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/python-sample-repo.git\",\"clone_url\"\ + :\"https://github.com/codecov/python-sample-repo.git\",\"svn_url\":\"https://github.com/codecov/python-sample-repo\"\ + ,\"homepage\":null,\"size\":67,\"stargazers_count\":0,\"watchers_count\":0,\"\ + language\":\"Shell\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":true,\"has_pages\":false,\"forks_count\":2,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":8,\"license\"\ + :null,\"forks\":2,\"open_issues\":8,\"watchers\":0,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":193794389,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkxOTM3OTQzODk=\",\"name\":\"python-standard\"\ + ,\"full_name\":\"codecov/python-standard\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/python-standard\"\ + ,\"description\":\"Codecov coverage standard for Python\",\"fork\":false,\"\ + url\":\"https://api.github.com/repos/codecov/python-standard\",\"forks_url\"\ + :\"https://api.github.com/repos/codecov/python-standard/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/python-standard/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/python-standard/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/python-standard/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/python-standard/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/python-standard/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/python-standard/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/python-standard/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/python-standard/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/python-standard/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/python-standard/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/python-standard/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/python-standard/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/python-standard/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/python-standard/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/python-standard/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/python-standard/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/python-standard/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/python-standard/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/python-standard/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/python-standard/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/python-standard/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/python-standard/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/python-standard/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/python-standard/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/python-standard/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/python-standard/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/python-standard/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/python-standard/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/python-standard/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/python-standard/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/python-standard/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/python-standard/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/python-standard/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/python-standard/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/python-standard/deployments\"\ + ,\"created_at\":\"2019-06-25T23:01:03Z\",\"updated_at\":\"2020-10-13T20:05:29Z\"\ + ,\"pushed_at\":\"2020-10-13T20:05:26Z\",\"git_url\":\"git://github.com/codecov/python-standard.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/python-standard.git\",\"clone_url\":\"\ + https://github.com/codecov/python-standard.git\",\"svn_url\":\"https://github.com/codecov/python-standard\"\ + ,\"homepage\":\"\",\"size\":214,\"stargazers_count\":2,\"watchers_count\":2,\"\ + language\":\"Python\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":true,\"has_pages\":false,\"forks_count\":8,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\"\ + :null,\"forks\":8,\"open_issues\":0,\"watchers\":2,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":272458865,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkyNzI0NTg4NjU=\",\"name\":\"raw-report-viewer\"\ + ,\"full_name\":\"codecov/raw-report-viewer\",\"private\":true,\"owner\":{\"\ + login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/raw-report-viewer\"\ + ,\"description\":\"Provides presigned GETs to download raw reports from codecov\ + \ storage\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/raw-report-viewer\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/raw-report-viewer/deployments\"\ + ,\"created_at\":\"2020-06-15T14:21:14Z\",\"updated_at\":\"2020-09-16T20:14:02Z\"\ + ,\"pushed_at\":\"2020-06-17T14:27:06Z\",\"git_url\":\"git://github.com/codecov/raw-report-viewer.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/raw-report-viewer.git\",\"clone_url\"\ + :\"https://github.com/codecov/raw-report-viewer.git\",\"svn_url\":\"https://github.com/codecov/raw-report-viewer\"\ + ,\"homepage\":null,\"size\":785,\"stargazers_count\":0,\"watchers_count\":0,\"\ + language\":\"Python\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":true,\"has_pages\":false,\"forks_count\":0,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":1,\"license\"\ + :null,\"forks\":0,\"open_issues\":1,\"watchers\":0,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":120340827,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkxMjAzNDA4Mjc=\",\"name\":\"report\",\"full_name\"\ + :\"codecov/report\",\"private\":true,\"owner\":{\"login\":\"codecov\",\"id\"\ + :8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"\ + https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\"\ + ,\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/report\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/report\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/report/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/report/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/report/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/report/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/report/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/report/issues/events{/number}\",\"events_url\"\ + :\"https://api.github.com/repos/codecov/report/events\",\"assignees_url\":\"\ + https://api.github.com/repos/codecov/report/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/report/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/report/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/report/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/report/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/report/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/report/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/report/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/report/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/report/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/report/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/report/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/report/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/report/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/report/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/report/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/report/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/report/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/report/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/report/merges\",\"archive_url\"\ + :\"https://api.github.com/repos/codecov/report/{archive_format}{/ref}\",\"downloads_url\"\ + :\"https://api.github.com/repos/codecov/report/downloads\",\"issues_url\":\"\ + https://api.github.com/repos/codecov/report/issues{/number}\",\"pulls_url\"\ + :\"https://api.github.com/repos/codecov/report/pulls{/number}\",\"milestones_url\"\ + :\"https://api.github.com/repos/codecov/report/milestones{/number}\",\"notifications_url\"\ + :\"https://api.github.com/repos/codecov/report/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/report/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/report/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/report/deployments\"\ + ,\"created_at\":\"2018-02-05T17:56:27Z\",\"updated_at\":\"2020-04-09T03:16:35Z\"\ + ,\"pushed_at\":\"2020-04-09T03:21:16Z\",\"git_url\":\"git://github.com/codecov/report.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/report.git\",\"clone_url\":\"https://github.com/codecov/report.git\"\ + ,\"svn_url\":\"https://github.com/codecov/report\",\"homepage\":null,\"size\"\ + :443,\"stargazers_count\":0,\"watchers_count\":0,\"language\":\"Python\",\"\ + has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\"\ + :true,\"has_pages\":false,\"forks_count\":1,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"forks\"\ + :1,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":false,\"push\":true,\"pull\":true}},{\"id\":197329672,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkxOTczMjk2NzI=\",\"name\":\"ruby-standard-1\",\"full_name\"\ + :\"codecov/ruby-standard-1\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/ruby-standard-1\"\ + ,\"description\":\"Codecov coverage standard for Ruby using the Codecov gem\"\ + ,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/ruby-standard-1\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/ruby-standard-1/deployments\"\ + ,\"created_at\":\"2019-07-17T06:34:44Z\",\"updated_at\":\"2020-10-13T20:05:36Z\"\ + ,\"pushed_at\":\"2020-10-13T20:05:33Z\",\"git_url\":\"git://github.com/codecov/ruby-standard-1.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/ruby-standard-1.git\",\"clone_url\":\"\ + https://github.com/codecov/ruby-standard-1.git\",\"svn_url\":\"https://github.com/codecov/ruby-standard-1\"\ + ,\"homepage\":\"\",\"size\":191,\"stargazers_count\":0,\"watchers_count\":0,\"\ + language\":\"Ruby\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":true,\"has_pages\":false,\"forks_count\":2,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\"\ + :null,\"forks\":2,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":198187206,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkxOTgxODcyMDY=\",\"name\":\"ruby-standard-2\"\ + ,\"full_name\":\"codecov/ruby-standard-2\",\"private\":false,\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/ruby-standard-2\"\ + ,\"description\":\"Codecov coverage standard for Ruby using Codecov's Bash uploader\"\ + ,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/ruby-standard-2\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/ruby-standard-2/deployments\"\ + ,\"created_at\":\"2019-07-22T09:06:36Z\",\"updated_at\":\"2020-10-13T20:05:40Z\"\ + ,\"pushed_at\":\"2020-10-13T20:05:38Z\",\"git_url\":\"git://github.com/codecov/ruby-standard-2.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/ruby-standard-2.git\",\"clone_url\":\"\ + https://github.com/codecov/ruby-standard-2.git\",\"svn_url\":\"https://github.com/codecov/ruby-standard-2\"\ + ,\"homepage\":\"\",\"size\":167,\"stargazers_count\":0,\"watchers_count\":0,\"\ + language\":\"Ruby\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":true,\"has_pages\":false,\"forks_count\":2,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\"\ + :null,\"forks\":2,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":24387241,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkyNDM4NzI0MQ==\",\"name\":\"scraper\",\"full_name\"\ + :\"codecov/scraper\",\"private\":true,\"owner\":{\"login\":\"codecov\",\"id\"\ + :8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"\ + https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\"\ + ,\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/scraper\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/scraper\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/scraper/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/scraper/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/scraper/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/scraper/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/scraper/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/scraper/issues/events{/number}\",\"\ + events_url\":\"https://api.github.com/repos/codecov/scraper/events\",\"assignees_url\"\ + :\"https://api.github.com/repos/codecov/scraper/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/scraper/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/scraper/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/scraper/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/scraper/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/scraper/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/scraper/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/scraper/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/scraper/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/scraper/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/scraper/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/scraper/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/scraper/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/scraper/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/scraper/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/scraper/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/scraper/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/scraper/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/scraper/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/scraper/merges\",\"archive_url\"\ + :\"https://api.github.com/repos/codecov/scraper/{archive_format}{/ref}\",\"\ + downloads_url\":\"https://api.github.com/repos/codecov/scraper/downloads\",\"\ + issues_url\":\"https://api.github.com/repos/codecov/scraper/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/scraper/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/scraper/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/scraper/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/scraper/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/scraper/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/scraper/deployments\"\ + ,\"created_at\":\"2014-09-23T20:01:50Z\",\"updated_at\":\"2014-09-23T20:12:55Z\"\ + ,\"pushed_at\":\"2014-09-26T16:52:17Z\",\"git_url\":\"git://github.com/codecov/scraper.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/scraper.git\",\"clone_url\":\"https://github.com/codecov/scraper.git\"\ + ,\"svn_url\":\"https://github.com/codecov/scraper\",\"homepage\":null,\"size\"\ + :140,\"stargazers_count\":0,\"watchers_count\":0,\"language\":\"Python\",\"\ + has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\"\ + :true,\"has_pages\":false,\"forks_count\":0,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"forks\"\ + :0,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":35773997,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkzNTc3Mzk5Nw==\",\"name\":\"services\",\"full_name\":\"\ + codecov/services\",\"private\":false,\"owner\":{\"login\":\"codecov\",\"id\"\ + :8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"\ + https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\"\ + ,\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/services\"\ + ,\"description\":\"Gitter integrations\",\"fork\":true,\"url\":\"https://api.github.com/repos/codecov/services\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/services/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/services/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/services/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/services/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/services/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/services/issues/events{/number}\",\"\ + events_url\":\"https://api.github.com/repos/codecov/services/events\",\"assignees_url\"\ + :\"https://api.github.com/repos/codecov/services/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/services/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/services/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/services/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/services/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/services/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/services/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/services/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/services/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/services/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/services/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/services/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/services/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/services/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/services/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/services/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/services/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/services/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/services/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/services/merges\",\"\ + archive_url\":\"https://api.github.com/repos/codecov/services/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/services/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/services/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/services/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/services/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/services/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/services/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/services/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/services/deployments\"\ + ,\"created_at\":\"2015-05-17T16:43:35Z\",\"updated_at\":\"2020-09-10T20:32:40Z\"\ + ,\"pushed_at\":\"2020-09-10T20:32:37Z\",\"git_url\":\"git://github.com/codecov/services.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/services.git\",\"clone_url\":\"https://github.com/codecov/services.git\"\ + ,\"svn_url\":\"https://github.com/codecov/services\",\"homepage\":\"\",\"size\"\ + :565,\"stargazers_count\":0,\"watchers_count\":0,\"language\":\"JavaScript\"\ + ,\"has_issues\":false,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\"\ + :false,\"has_pages\":false,\"forks_count\":2,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"forks\"\ + :2,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":254750340,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkyNTQ3NTAzNDA=\",\"name\":\"shared\",\"full_name\":\"codecov/shared\"\ + ,\"private\":true,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\"\ + :\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\"\ + ,\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/shared\"\ + ,\"description\":\"Shared code between worker and api\",\"fork\":false,\"url\"\ + :\"https://api.github.com/repos/codecov/shared\",\"forks_url\":\"https://api.github.com/repos/codecov/shared/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/shared/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/shared/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/shared/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/shared/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/shared/issues/events{/number}\",\"events_url\"\ + :\"https://api.github.com/repos/codecov/shared/events\",\"assignees_url\":\"\ + https://api.github.com/repos/codecov/shared/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/shared/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/shared/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/shared/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/shared/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/shared/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/shared/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/shared/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/shared/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/shared/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/shared/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/shared/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/shared/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/shared/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/shared/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/shared/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/shared/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/shared/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/shared/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/shared/merges\",\"archive_url\"\ + :\"https://api.github.com/repos/codecov/shared/{archive_format}{/ref}\",\"downloads_url\"\ + :\"https://api.github.com/repos/codecov/shared/downloads\",\"issues_url\":\"\ + https://api.github.com/repos/codecov/shared/issues{/number}\",\"pulls_url\"\ + :\"https://api.github.com/repos/codecov/shared/pulls{/number}\",\"milestones_url\"\ + :\"https://api.github.com/repos/codecov/shared/milestones{/number}\",\"notifications_url\"\ + :\"https://api.github.com/repos/codecov/shared/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/shared/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/shared/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/shared/deployments\"\ + ,\"created_at\":\"2020-04-10T22:42:46Z\",\"updated_at\":\"2020-09-21T20:05:39Z\"\ + ,\"pushed_at\":\"2020-09-21T20:22:06Z\",\"git_url\":\"git://github.com/codecov/shared.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/shared.git\",\"clone_url\":\"https://github.com/codecov/shared.git\"\ + ,\"svn_url\":\"https://github.com/codecov/shared\",\"homepage\":null,\"size\"\ + :1499,\"stargazers_count\":0,\"watchers_count\":0,\"language\":\"Python\",\"\ + has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\"\ + :true,\"has_pages\":false,\"forks_count\":0,\"mirror_url\":null,\"archived\"\ + :false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"forks\"\ + :0,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"main\",\"permissions\"\ + :{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":140897583,\"node_id\"\ + :\"MDEwOlJlcG9zaXRvcnkxNDA4OTc1ODM=\",\"name\":\"sourcegraph-codecov\",\"full_name\"\ + :\"codecov/sourcegraph-codecov\",\"private\":false,\"owner\":{\"login\":\"codecov\"\ + ,\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/sourcegraph-codecov\"\ + ,\"description\":\"See code coverage information from Codecov on GitHub, Sourcegraph,\ + \ and other tools.\",\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/tags\"\ + ,\"blobs_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/sourcegraph-codecov/deployments\"\ + ,\"created_at\":\"2018-07-13T22:21:05Z\",\"updated_at\":\"2020-09-09T19:01:11Z\"\ + ,\"pushed_at\":\"2020-09-23T20:30:56Z\",\"git_url\":\"git://github.com/codecov/sourcegraph-codecov.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/sourcegraph-codecov.git\",\"clone_url\"\ + :\"https://github.com/codecov/sourcegraph-codecov.git\",\"svn_url\":\"https://github.com/codecov/sourcegraph-codecov\"\ + ,\"homepage\":\"\",\"size\":871,\"stargazers_count\":50,\"watchers_count\":50,\"\ + language\":\"TypeScript\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":44,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":10,\"license\"\ + :{\"key\":\"mit\",\"name\":\"MIT License\",\"spdx_id\":\"MIT\",\"url\":\"https://api.github.com/licenses/mit\"\ + ,\"node_id\":\"MDc6TGljZW5zZTEz\"},\"forks\":44,\"open_issues\":10,\"watchers\"\ + :50,\"default_branch\":\"main\",\"permissions\":{\"admin\":false,\"push\"\ + :false,\"pull\":true}},{\"id\":194928953,\"node_id\":\"MDEwOlJlcG9zaXRvcnkxOTQ5Mjg5NTM=\"\ + ,\"name\":\"standards\",\"full_name\":\"codecov/standards\",\"private\":false,\"\ + owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/standards\"\ + ,\"description\":\"List of Codecov language standards \",\"fork\":false,\"url\"\ + :\"https://api.github.com/repos/codecov/standards\",\"forks_url\":\"https://api.github.com/repos/codecov/standards/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/standards/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/standards/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/standards/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/standards/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/standards/issues/events{/number}\",\"\ + events_url\":\"https://api.github.com/repos/codecov/standards/events\",\"assignees_url\"\ + :\"https://api.github.com/repos/codecov/standards/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/standards/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/standards/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/standards/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/standards/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/standards/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/standards/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/standards/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/standards/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/standards/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/standards/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/standards/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/standards/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/standards/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/standards/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/standards/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/standards/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/standards/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/standards/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/standards/merges\",\"\ + archive_url\":\"https://api.github.com/repos/codecov/standards/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/standards/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/standards/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/standards/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/standards/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/standards/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/standards/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/standards/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/standards/deployments\"\ + ,\"created_at\":\"2019-07-02T20:16:31Z\",\"updated_at\":\"2020-08-27T03:01:59Z\"\ + ,\"pushed_at\":\"2020-08-27T03:01:57Z\",\"git_url\":\"git://github.com/codecov/standards.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/standards.git\",\"clone_url\":\"https://github.com/codecov/standards.git\"\ + ,\"svn_url\":\"https://github.com/codecov/standards\",\"homepage\":\"\",\"size\"\ + :32,\"stargazers_count\":1,\"watchers_count\":1,\"language\":\"Shell\",\"has_issues\"\ + :true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\":true,\"has_pages\"\ + :false,\"forks_count\":2,\"mirror_url\":null,\"archived\":false,\"disabled\"\ + :false,\"open_issues_count\":0,\"license\":null,\"forks\":2,\"open_issues\"\ + :0,\"watchers\":1,\"default_branch\":\"main\",\"permissions\":{\"admin\":true,\"\ + push\":true,\"pull\":true}},{\"id\":36328489,\"node_id\":\"MDEwOlJlcG9zaXRvcnkzNjMyODQ4OQ==\"\ + ,\"name\":\"sublime-plugin\",\"full_name\":\"codecov/sublime-plugin\",\"private\"\ + :true,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/sublime-plugin\"\ + ,\"description\":\"Overlay coverage reports in Sublime\",\"fork\":false,\"url\"\ + :\"https://api.github.com/repos/codecov/sublime-plugin\",\"forks_url\":\"https://api.github.com/repos/codecov/sublime-plugin/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/sublime-plugin/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/sublime-plugin/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/sublime-plugin/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/sublime-plugin/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/sublime-plugin/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/sublime-plugin/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/sublime-plugin/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/sublime-plugin/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/sublime-plugin/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/sublime-plugin/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/sublime-plugin/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/sublime-plugin/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/sublime-plugin/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/sublime-plugin/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/sublime-plugin/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/sublime-plugin/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/sublime-plugin/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/sublime-plugin/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/sublime-plugin/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/sublime-plugin/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/sublime-plugin/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/sublime-plugin/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/sublime-plugin/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/sublime-plugin/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/sublime-plugin/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/sublime-plugin/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/sublime-plugin/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/sublime-plugin/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/sublime-plugin/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/sublime-plugin/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/sublime-plugin/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/sublime-plugin/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/sublime-plugin/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/sublime-plugin/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/sublime-plugin/deployments\"\ + ,\"created_at\":\"2015-05-26T22:51:55Z\",\"updated_at\":\"2019-09-19T15:38:13Z\"\ + ,\"pushed_at\":\"2015-05-31T00:30:49Z\",\"git_url\":\"git://github.com/codecov/sublime-plugin.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/sublime-plugin.git\",\"clone_url\":\"\ + https://github.com/codecov/sublime-plugin.git\",\"svn_url\":\"https://github.com/codecov/sublime-plugin\"\ + ,\"homepage\":null,\"size\":132,\"stargazers_count\":0,\"watchers_count\":0,\"\ + language\":\"JavaScript\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":false,\"has_pages\":false,\"forks_count\":0,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\"\ + :null,\"forks\":0,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":35619273,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkzNTYxOTI3Mw==\",\"name\":\"support\",\"full_name\"\ + :\"codecov/support\",\"private\":true,\"owner\":{\"login\":\"codecov\",\"id\"\ + :8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\":\"\ + https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\"\ + ,\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/support\"\ + ,\"description\":\"Customer feedback: Support, feature requests, QA and FAQ\"\ + ,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/support\",\"\ + forks_url\":\"https://api.github.com/repos/codecov/support/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/support/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/support/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/support/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/support/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/support/issues/events{/number}\",\"\ + events_url\":\"https://api.github.com/repos/codecov/support/events\",\"assignees_url\"\ + :\"https://api.github.com/repos/codecov/support/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/support/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/support/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/support/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/support/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/support/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/support/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/support/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/support/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/support/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/support/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/support/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/support/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/support/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/support/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/support/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/support/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/support/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/support/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/support/merges\",\"archive_url\"\ + :\"https://api.github.com/repos/codecov/support/{archive_format}{/ref}\",\"\ + downloads_url\":\"https://api.github.com/repos/codecov/support/downloads\",\"\ + issues_url\":\"https://api.github.com/repos/codecov/support/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/support/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/support/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/support/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/support/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/support/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/support/deployments\"\ + ,\"created_at\":\"2015-05-14T15:29:48Z\",\"updated_at\":\"2019-09-19T16:30:26Z\"\ + ,\"pushed_at\":\"2017-08-21T07:38:46Z\",\"git_url\":\"git://github.com/codecov/support.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/support.git\",\"clone_url\":\"https://github.com/codecov/support.git\"\ + ,\"svn_url\":\"https://github.com/codecov/support\",\"homepage\":null,\"size\"\ + :246,\"stargazers_count\":0,\"watchers_count\":0,\"language\":\"HTML\",\"has_issues\"\ + :true,\"has_projects\":false,\"has_downloads\":true,\"has_wiki\":false,\"has_pages\"\ + :false,\"forks_count\":0,\"mirror_url\":null,\"archived\":true,\"disabled\"\ + :false,\"open_issues_count\":138,\"license\":null,\"forks\":0,\"open_issues\"\ + :138,\"watchers\":0,\"default_branch\":\"main\",\"permissions\":{\"admin\"\ + :true,\"push\":true,\"pull\":true}},{\"id\":196071888,\"node_id\":\"MDEwOlJlcG9zaXRvcnkxOTYwNzE4ODg=\"\ + ,\"name\":\"swift-standard\",\"full_name\":\"codecov/swift-standard\",\"private\"\ + :false,\"owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/swift-standard\"\ + ,\"description\":\"Codecov coverage standard for Swift\",\"fork\":false,\"url\"\ + :\"https://api.github.com/repos/codecov/swift-standard\",\"forks_url\":\"https://api.github.com/repos/codecov/swift-standard/forks\"\ + ,\"keys_url\":\"https://api.github.com/repos/codecov/swift-standard/keys{/key_id}\"\ + ,\"collaborators_url\":\"https://api.github.com/repos/codecov/swift-standard/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/swift-standard/teams\"\ + ,\"hooks_url\":\"https://api.github.com/repos/codecov/swift-standard/hooks\"\ + ,\"issue_events_url\":\"https://api.github.com/repos/codecov/swift-standard/issues/events{/number}\"\ + ,\"events_url\":\"https://api.github.com/repos/codecov/swift-standard/events\"\ + ,\"assignees_url\":\"https://api.github.com/repos/codecov/swift-standard/assignees{/user}\"\ + ,\"branches_url\":\"https://api.github.com/repos/codecov/swift-standard/branches{/branch}\"\ + ,\"tags_url\":\"https://api.github.com/repos/codecov/swift-standard/tags\",\"\ + blobs_url\":\"https://api.github.com/repos/codecov/swift-standard/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/swift-standard/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/swift-standard/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/swift-standard/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/swift-standard/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/swift-standard/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/swift-standard/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/swift-standard/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/swift-standard/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/swift-standard/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/swift-standard/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/swift-standard/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/swift-standard/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/swift-standard/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/swift-standard/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/swift-standard/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/swift-standard/merges\"\ + ,\"archive_url\":\"https://api.github.com/repos/codecov/swift-standard/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/swift-standard/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/swift-standard/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/swift-standard/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/swift-standard/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/swift-standard/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/swift-standard/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/swift-standard/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/swift-standard/deployments\"\ + ,\"created_at\":\"2019-07-09T19:35:18Z\",\"updated_at\":\"2020-10-13T20:05:36Z\"\ + ,\"pushed_at\":\"2020-10-13T20:05:29Z\",\"git_url\":\"git://github.com/codecov/swift-standard.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/swift-standard.git\",\"clone_url\":\"\ + https://github.com/codecov/swift-standard.git\",\"svn_url\":\"https://github.com/codecov/swift-standard\"\ + ,\"homepage\":\"\",\"size\":242,\"stargazers_count\":3,\"watchers_count\":3,\"\ + language\":\"Swift\",\"has_issues\":true,\"has_projects\":true,\"has_downloads\"\ + :true,\"has_wiki\":true,\"has_pages\":false,\"forks_count\":7,\"mirror_url\"\ + :null,\"archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\"\ + :null,\"forks\":7,\"open_issues\":0,\"watchers\":3,\"default_branch\":\"main\"\ + ,\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"id\":37015668,\"\ + node_id\":\"MDEwOlJlcG9zaXRvcnkzNzAxNTY2OA==\",\"name\":\"SwiftCov\",\"full_name\"\ + :\"codecov/SwiftCov\",\"private\":false,\"owner\":{\"login\":\"codecov\",\"\ + id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\",\"avatar_url\"\ + :\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"gravatar_id\":\"\ + \",\"url\":\"https://api.github.com/users/codecov\",\"html_url\":\"https://github.com/codecov\"\ + ,\"followers_url\":\"https://api.github.com/users/codecov/followers\",\"following_url\"\ + :\"https://api.github.com/users/codecov/following{/other_user}\",\"gists_url\"\ + :\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\":\"\ + https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/SwiftCov\"\ + ,\"description\":\"A tool to generate test code coverage information for Swift.\"\ + ,\"fork\":true,\"url\":\"https://api.github.com/repos/codecov/SwiftCov\",\"\ + forks_url\":\"https://api.github.com/repos/codecov/SwiftCov/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/SwiftCov/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/SwiftCov/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/SwiftCov/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/SwiftCov/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/SwiftCov/issues/events{/number}\",\"\ + events_url\":\"https://api.github.com/repos/codecov/SwiftCov/events\",\"assignees_url\"\ + :\"https://api.github.com/repos/codecov/SwiftCov/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/SwiftCov/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/SwiftCov/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/SwiftCov/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/SwiftCov/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/SwiftCov/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/SwiftCov/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/SwiftCov/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/SwiftCov/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/SwiftCov/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/SwiftCov/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/SwiftCov/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/SwiftCov/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/SwiftCov/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/SwiftCov/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/SwiftCov/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/SwiftCov/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/SwiftCov/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/SwiftCov/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/SwiftCov/merges\",\"\ + archive_url\":\"https://api.github.com/repos/codecov/SwiftCov/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/SwiftCov/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/SwiftCov/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/SwiftCov/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/SwiftCov/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/SwiftCov/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/SwiftCov/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/SwiftCov/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/SwiftCov/deployments\"\ + ,\"created_at\":\"2015-06-07T12:24:42Z\",\"updated_at\":\"2019-09-19T15:42:02Z\"\ + ,\"pushed_at\":\"2015-06-07T12:27:00Z\",\"git_url\":\"git://github.com/codecov/SwiftCov.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/SwiftCov.git\",\"clone_url\":\"https://github.com/codecov/SwiftCov.git\"\ + ,\"svn_url\":\"https://github.com/codecov/SwiftCov\",\"homepage\":\"\",\"size\"\ + :208,\"stargazers_count\":2,\"watchers_count\":2,\"language\":\"Swift\",\"has_issues\"\ + :false,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\":false,\"has_pages\"\ + :false,\"forks_count\":1,\"mirror_url\":null,\"archived\":true,\"disabled\"\ + :false,\"open_issues_count\":0,\"license\":{\"key\":\"mit\",\"name\":\"MIT License\"\ + ,\"spdx_id\":\"MIT\",\"url\":\"https://api.github.com/licenses/mit\",\"node_id\"\ + :\"MDc6TGljZW5zZTEz\"},\"forks\":1,\"open_issues\":0,\"watchers\":2,\"default_branch\"\ + :\"main\",\"permissions\":{\"admin\":true,\"push\":true,\"pull\":true}},{\"\ + id\":225705568,\"node_id\":\"MDEwOlJlcG9zaXRvcnkyMjU3MDU1Njg=\",\"name\":\"\ + terraform\",\"full_name\":\"codecov/terraform\",\"private\":true,\"owner\":{\"\ + login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/terraform\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/terraform\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/terraform/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/terraform/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/terraform/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/terraform/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/terraform/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/terraform/issues/events{/number}\",\"\ + events_url\":\"https://api.github.com/repos/codecov/terraform/events\",\"assignees_url\"\ + :\"https://api.github.com/repos/codecov/terraform/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/terraform/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/terraform/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/terraform/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/terraform/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/terraform/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/terraform/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/terraform/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/terraform/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/terraform/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/terraform/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/terraform/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/terraform/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/terraform/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/terraform/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/terraform/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/terraform/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/terraform/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/terraform/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/terraform/merges\",\"\ + archive_url\":\"https://api.github.com/repos/codecov/terraform/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/terraform/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/terraform/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/terraform/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/terraform/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/terraform/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/terraform/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/terraform/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/terraform/deployments\"\ + ,\"created_at\":\"2019-12-03T20:06:41Z\",\"updated_at\":\"2020-10-12T15:52:47Z\"\ + ,\"pushed_at\":\"2020-10-14T16:16:49Z\",\"git_url\":\"git://github.com/codecov/terraform.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/terraform.git\",\"clone_url\":\"https://github.com/codecov/terraform.git\"\ + ,\"svn_url\":\"https://github.com/codecov/terraform\",\"homepage\":null,\"size\"\ + :102,\"stargazers_count\":0,\"watchers_count\":0,\"language\":\"HCL\",\"has_issues\"\ + :true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\":true,\"has_pages\"\ + :false,\"forks_count\":0,\"mirror_url\":null,\"archived\":false,\"disabled\"\ + :false,\"open_issues_count\":2,\"license\":null,\"forks\":0,\"open_issues\"\ + :2,\"watchers\":0,\"default_branch\":\"main\",\"permissions\":{\"admin\":true,\"\ + push\":true,\"pull\":true}},{\"id\":41119705,\"node_id\":\"MDEwOlJlcG9zaXRvcnk0MTExOTcwNQ==\"\ + ,\"name\":\"testsuite\",\"full_name\":\"codecov/testsuite\",\"private\":false,\"\ + owner\":{\"login\":\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"html_url\":\"https://github.com/codecov/testsuite\"\ + ,\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/codecov/testsuite\"\ + ,\"forks_url\":\"https://api.github.com/repos/codecov/testsuite/forks\",\"keys_url\"\ + :\"https://api.github.com/repos/codecov/testsuite/keys{/key_id}\",\"collaborators_url\"\ + :\"https://api.github.com/repos/codecov/testsuite/collaborators{/collaborator}\"\ + ,\"teams_url\":\"https://api.github.com/repos/codecov/testsuite/teams\",\"hooks_url\"\ + :\"https://api.github.com/repos/codecov/testsuite/hooks\",\"issue_events_url\"\ + :\"https://api.github.com/repos/codecov/testsuite/issues/events{/number}\",\"\ + events_url\":\"https://api.github.com/repos/codecov/testsuite/events\",\"assignees_url\"\ + :\"https://api.github.com/repos/codecov/testsuite/assignees{/user}\",\"branches_url\"\ + :\"https://api.github.com/repos/codecov/testsuite/branches{/branch}\",\"tags_url\"\ + :\"https://api.github.com/repos/codecov/testsuite/tags\",\"blobs_url\":\"https://api.github.com/repos/codecov/testsuite/git/blobs{/sha}\"\ + ,\"git_tags_url\":\"https://api.github.com/repos/codecov/testsuite/git/tags{/sha}\"\ + ,\"git_refs_url\":\"https://api.github.com/repos/codecov/testsuite/git/refs{/sha}\"\ + ,\"trees_url\":\"https://api.github.com/repos/codecov/testsuite/git/trees{/sha}\"\ + ,\"statuses_url\":\"https://api.github.com/repos/codecov/testsuite/statuses/{sha}\"\ + ,\"languages_url\":\"https://api.github.com/repos/codecov/testsuite/languages\"\ + ,\"stargazers_url\":\"https://api.github.com/repos/codecov/testsuite/stargazers\"\ + ,\"contributors_url\":\"https://api.github.com/repos/codecov/testsuite/contributors\"\ + ,\"subscribers_url\":\"https://api.github.com/repos/codecov/testsuite/subscribers\"\ + ,\"subscription_url\":\"https://api.github.com/repos/codecov/testsuite/subscription\"\ + ,\"commits_url\":\"https://api.github.com/repos/codecov/testsuite/commits{/sha}\"\ + ,\"git_commits_url\":\"https://api.github.com/repos/codecov/testsuite/git/commits{/sha}\"\ + ,\"comments_url\":\"https://api.github.com/repos/codecov/testsuite/comments{/number}\"\ + ,\"issue_comment_url\":\"https://api.github.com/repos/codecov/testsuite/issues/comments{/number}\"\ + ,\"contents_url\":\"https://api.github.com/repos/codecov/testsuite/contents/{+path}\"\ + ,\"compare_url\":\"https://api.github.com/repos/codecov/testsuite/compare/{base}...{head}\"\ + ,\"merges_url\":\"https://api.github.com/repos/codecov/testsuite/merges\",\"\ + archive_url\":\"https://api.github.com/repos/codecov/testsuite/{archive_format}{/ref}\"\ + ,\"downloads_url\":\"https://api.github.com/repos/codecov/testsuite/downloads\"\ + ,\"issues_url\":\"https://api.github.com/repos/codecov/testsuite/issues{/number}\"\ + ,\"pulls_url\":\"https://api.github.com/repos/codecov/testsuite/pulls{/number}\"\ + ,\"milestones_url\":\"https://api.github.com/repos/codecov/testsuite/milestones{/number}\"\ + ,\"notifications_url\":\"https://api.github.com/repos/codecov/testsuite/notifications{?since,all,participating}\"\ + ,\"labels_url\":\"https://api.github.com/repos/codecov/testsuite/labels{/name}\"\ + ,\"releases_url\":\"https://api.github.com/repos/codecov/testsuite/releases{/id}\"\ + ,\"deployments_url\":\"https://api.github.com/repos/codecov/testsuite/deployments\"\ + ,\"created_at\":\"2015-08-20T21:28:02Z\",\"updated_at\":\"2019-09-19T15:47:56Z\"\ + ,\"pushed_at\":\"2017-04-10T19:53:56Z\",\"git_url\":\"git://github.com/codecov/testsuite.git\"\ + ,\"ssh_url\":\"git@github.com:codecov/testsuite.git\",\"clone_url\":\"https://github.com/codecov/testsuite.git\"\ + ,\"svn_url\":\"https://github.com/codecov/testsuite\",\"homepage\":null,\"size\"\ + :42,\"stargazers_count\":0,\"watchers_count\":0,\"language\":\"Python\",\"has_issues\"\ + :true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\":false,\"has_pages\"\ + :false,\"forks_count\":5,\"mirror_url\":null,\"archived\":true,\"disabled\"\ + :false,\"open_issues_count\":0,\"license\":null,\"forks\":5,\"open_issues\"\ + :0,\"watchers\":0,\"default_branch\":\"main\",\"permissions\":{\"admin\":true,\"\ + push\":true,\"pull\":true}}]" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:37 GMT + ETag: + - W/"76104902f7fd570621facacfe5dcaa58cc7726dea3402cddab79f7a3fa254589" + Link: + - ; rel="next", ; + rel="last" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFC:1EA2:358A5B1:5DDA29D:5F873633 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4927' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '73' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/example-cpp11 + response: + content: '{"id":65390076,"node_id":"MDEwOlJlcG9zaXRvcnk2NTM5MDA3Ng==","name":"example-cpp11","full_name":"codecov/example-cpp11","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-cpp11","description":"Minimal + project that uses qmake, GCC, C++11, gcov and is tested by Travis CI","fork":true,"url":"https://api.github.com/repos/codecov/example-cpp11","forks_url":"https://api.github.com/repos/codecov/example-cpp11/forks","keys_url":"https://api.github.com/repos/codecov/example-cpp11/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-cpp11/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-cpp11/teams","hooks_url":"https://api.github.com/repos/codecov/example-cpp11/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-cpp11/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-cpp11/events","assignees_url":"https://api.github.com/repos/codecov/example-cpp11/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-cpp11/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-cpp11/tags","blobs_url":"https://api.github.com/repos/codecov/example-cpp11/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-cpp11/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-cpp11/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-cpp11/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-cpp11/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-cpp11/languages","stargazers_url":"https://api.github.com/repos/codecov/example-cpp11/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-cpp11/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-cpp11/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-cpp11/subscription","commits_url":"https://api.github.com/repos/codecov/example-cpp11/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-cpp11/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-cpp11/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-cpp11/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-cpp11/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-cpp11/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-cpp11/merges","archive_url":"https://api.github.com/repos/codecov/example-cpp11/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-cpp11/downloads","issues_url":"https://api.github.com/repos/codecov/example-cpp11/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-cpp11/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-cpp11/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-cpp11/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-cpp11/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-cpp11/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-cpp11/deployments","created_at":"2016-08-10T14:38:50Z","updated_at":"2020-08-28T13:00:35Z","pushed_at":"2020-08-27T03:14:27Z","git_url":"git://github.com/codecov/example-cpp11.git","ssh_url":"git@github.com:codecov/example-cpp11.git","clone_url":"https://github.com/codecov/example-cpp11.git","svn_url":"https://github.com/codecov/example-cpp11","homepage":null,"size":31,"stargazers_count":25,"watchers_count":25,"language":"Shell","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":24,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"gpl-3.0","name":"GNU + General Public License v3.0","spdx_id":"GPL-3.0","url":"https://api.github.com/licenses/gpl-3.0","node_id":"MDc6TGljZW5zZTk="},"forks":24,"open_issues":0,"watchers":25,"default_branch":"main","permissions":{"admin":false,"push":true,"pull":true},"temp_clone_token":"","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"delete_branch_on_merge":false,"organization":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"parent":{"id":54310570,"node_id":"MDEwOlJlcG9zaXRvcnk1NDMxMDU3MA==","name":"travis_qmake_gcc_cpp11_gcov","full_name":"richelbilderbeek/travis_qmake_gcc_cpp11_gcov","private":false,"owner":{"login":"richelbilderbeek","id":2098230,"node_id":"MDQ6VXNlcjIwOTgyMzA=","avatar_url":"https://avatars3.githubusercontent.com/u/2098230?v=4","gravatar_id":"","url":"https://api.github.com/users/richelbilderbeek","html_url":"https://github.com/richelbilderbeek","followers_url":"https://api.github.com/users/richelbilderbeek/followers","following_url":"https://api.github.com/users/richelbilderbeek/following{/other_user}","gists_url":"https://api.github.com/users/richelbilderbeek/gists{/gist_id}","starred_url":"https://api.github.com/users/richelbilderbeek/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/richelbilderbeek/subscriptions","organizations_url":"https://api.github.com/users/richelbilderbeek/orgs","repos_url":"https://api.github.com/users/richelbilderbeek/repos","events_url":"https://api.github.com/users/richelbilderbeek/events{/privacy}","received_events_url":"https://api.github.com/users/richelbilderbeek/received_events","type":"User","site_admin":false},"html_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp11_gcov","description":"Minimal + project that uses qmake, GCC, C++11, gcov and is tested by Travis CI","fork":false,"url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov","forks_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/forks","keys_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/keys{/key_id}","collaborators_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/teams","hooks_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/hooks","issue_events_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/issues/events{/number}","events_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/events","assignees_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/assignees{/user}","branches_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/branches{/branch}","tags_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/tags","blobs_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/git/refs{/sha}","trees_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/git/trees{/sha}","statuses_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/statuses/{sha}","languages_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/languages","stargazers_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/stargazers","contributors_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/contributors","subscribers_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/subscribers","subscription_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/subscription","commits_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/commits{/sha}","git_commits_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/git/commits{/sha}","comments_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/comments{/number}","issue_comment_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/issues/comments{/number}","contents_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/contents/{+path}","compare_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/compare/{base}...{head}","merges_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/merges","archive_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/downloads","issues_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/issues{/number}","pulls_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/pulls{/number}","milestones_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/milestones{/number}","notifications_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/labels{/name}","releases_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/releases{/id}","deployments_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/deployments","created_at":"2016-03-20T09:51:48Z","updated_at":"2020-04-23T02:32:06Z","pushed_at":"2019-08-15T12:39:43Z","git_url":"git://github.com/richelbilderbeek/travis_qmake_gcc_cpp11_gcov.git","ssh_url":"git@github.com:richelbilderbeek/travis_qmake_gcc_cpp11_gcov.git","clone_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp11_gcov.git","svn_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp11_gcov","homepage":null,"size":31,"stargazers_count":4,"watchers_count":4,"language":"Shell","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":27,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"gpl-3.0","name":"GNU + General Public License v3.0","spdx_id":"GPL-3.0","url":"https://api.github.com/licenses/gpl-3.0","node_id":"MDc6TGljZW5zZTk="},"forks":27,"open_issues":0,"watchers":4,"default_branch":"main"},"source":{"id":54310570,"node_id":"MDEwOlJlcG9zaXRvcnk1NDMxMDU3MA==","name":"travis_qmake_gcc_cpp11_gcov","full_name":"richelbilderbeek/travis_qmake_gcc_cpp11_gcov","private":false,"owner":{"login":"richelbilderbeek","id":2098230,"node_id":"MDQ6VXNlcjIwOTgyMzA=","avatar_url":"https://avatars3.githubusercontent.com/u/2098230?v=4","gravatar_id":"","url":"https://api.github.com/users/richelbilderbeek","html_url":"https://github.com/richelbilderbeek","followers_url":"https://api.github.com/users/richelbilderbeek/followers","following_url":"https://api.github.com/users/richelbilderbeek/following{/other_user}","gists_url":"https://api.github.com/users/richelbilderbeek/gists{/gist_id}","starred_url":"https://api.github.com/users/richelbilderbeek/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/richelbilderbeek/subscriptions","organizations_url":"https://api.github.com/users/richelbilderbeek/orgs","repos_url":"https://api.github.com/users/richelbilderbeek/repos","events_url":"https://api.github.com/users/richelbilderbeek/events{/privacy}","received_events_url":"https://api.github.com/users/richelbilderbeek/received_events","type":"User","site_admin":false},"html_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp11_gcov","description":"Minimal + project that uses qmake, GCC, C++11, gcov and is tested by Travis CI","fork":false,"url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov","forks_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/forks","keys_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/keys{/key_id}","collaborators_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/teams","hooks_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/hooks","issue_events_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/issues/events{/number}","events_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/events","assignees_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/assignees{/user}","branches_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/branches{/branch}","tags_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/tags","blobs_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/git/refs{/sha}","trees_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/git/trees{/sha}","statuses_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/statuses/{sha}","languages_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/languages","stargazers_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/stargazers","contributors_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/contributors","subscribers_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/subscribers","subscription_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/subscription","commits_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/commits{/sha}","git_commits_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/git/commits{/sha}","comments_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/comments{/number}","issue_comment_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/issues/comments{/number}","contents_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/contents/{+path}","compare_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/compare/{base}...{head}","merges_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/merges","archive_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/downloads","issues_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/issues{/number}","pulls_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/pulls{/number}","milestones_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/milestones{/number}","notifications_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/labels{/name}","releases_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/releases{/id}","deployments_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_gcov/deployments","created_at":"2016-03-20T09:51:48Z","updated_at":"2020-04-23T02:32:06Z","pushed_at":"2019-08-15T12:39:43Z","git_url":"git://github.com/richelbilderbeek/travis_qmake_gcc_cpp11_gcov.git","ssh_url":"git@github.com:richelbilderbeek/travis_qmake_gcc_cpp11_gcov.git","clone_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp11_gcov.git","svn_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp11_gcov","homepage":null,"size":31,"stargazers_count":4,"watchers_count":4,"language":"Shell","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":27,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"gpl-3.0","name":"GNU + General Public License v3.0","spdx_id":"GPL-3.0","url":"https://api.github.com/licenses/gpl-3.0","node_id":"MDc6TGljZW5zZTk="},"forks":27,"open_issues":0,"watchers":4,"default_branch":"main"},"network_count":27,"subscribers_count":6}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:37 GMT + ETag: + - W/"ed019105403cae9a42d9e4c4117049a88d52864dcbd033fe2fdabb9c4d6dc247" + Last-Modified: + - Fri, 28 Aug 2020 13:00:35 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFC:1EA2:358A6A3:5DDA3CD:5F873635 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4926' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '74' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/example-cpp11_boost + response: + content: '{"id":65390037,"node_id":"MDEwOlJlcG9zaXRvcnk2NTM5MDAzNw==","name":"example-cpp11_boost","full_name":"codecov/example-cpp11_boost","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-cpp11_boost","description":"Minimal + project that uses qmake, GCC, C++11, Boost, Boost.Test, gcov and is tested by + Travis CI","fork":true,"url":"https://api.github.com/repos/codecov/example-cpp11_boost","forks_url":"https://api.github.com/repos/codecov/example-cpp11_boost/forks","keys_url":"https://api.github.com/repos/codecov/example-cpp11_boost/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-cpp11_boost/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-cpp11_boost/teams","hooks_url":"https://api.github.com/repos/codecov/example-cpp11_boost/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-cpp11_boost/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-cpp11_boost/events","assignees_url":"https://api.github.com/repos/codecov/example-cpp11_boost/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-cpp11_boost/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-cpp11_boost/tags","blobs_url":"https://api.github.com/repos/codecov/example-cpp11_boost/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-cpp11_boost/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-cpp11_boost/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-cpp11_boost/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-cpp11_boost/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-cpp11_boost/languages","stargazers_url":"https://api.github.com/repos/codecov/example-cpp11_boost/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-cpp11_boost/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-cpp11_boost/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-cpp11_boost/subscription","commits_url":"https://api.github.com/repos/codecov/example-cpp11_boost/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-cpp11_boost/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-cpp11_boost/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-cpp11_boost/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-cpp11_boost/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-cpp11_boost/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-cpp11_boost/merges","archive_url":"https://api.github.com/repos/codecov/example-cpp11_boost/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-cpp11_boost/downloads","issues_url":"https://api.github.com/repos/codecov/example-cpp11_boost/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-cpp11_boost/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-cpp11_boost/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-cpp11_boost/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-cpp11_boost/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-cpp11_boost/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-cpp11_boost/deployments","created_at":"2016-08-10T14:38:20Z","updated_at":"2020-09-04T02:47:19Z","pushed_at":"2020-09-04T02:47:17Z","git_url":"git://github.com/codecov/example-cpp11_boost.git","ssh_url":"git@github.com:codecov/example-cpp11_boost.git","clone_url":"https://github.com/codecov/example-cpp11_boost.git","svn_url":"https://github.com/codecov/example-cpp11_boost","homepage":null,"size":28,"stargazers_count":3,"watchers_count":3,"language":"Shell","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":2,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"gpl-3.0","name":"GNU + General Public License v3.0","spdx_id":"GPL-3.0","url":"https://api.github.com/licenses/gpl-3.0","node_id":"MDc6TGljZW5zZTk="},"forks":2,"open_issues":0,"watchers":3,"default_branch":"main","permissions":{"admin":false,"push":true,"pull":true},"temp_clone_token":"","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"delete_branch_on_merge":false,"organization":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"parent":{"id":54314576,"node_id":"MDEwOlJlcG9zaXRvcnk1NDMxNDU3Ng==","name":"travis_qmake_gcc_cpp11_boost_test_gcov","full_name":"richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov","private":false,"owner":{"login":"richelbilderbeek","id":2098230,"node_id":"MDQ6VXNlcjIwOTgyMzA=","avatar_url":"https://avatars3.githubusercontent.com/u/2098230?v=4","gravatar_id":"","url":"https://api.github.com/users/richelbilderbeek","html_url":"https://github.com/richelbilderbeek","followers_url":"https://api.github.com/users/richelbilderbeek/followers","following_url":"https://api.github.com/users/richelbilderbeek/following{/other_user}","gists_url":"https://api.github.com/users/richelbilderbeek/gists{/gist_id}","starred_url":"https://api.github.com/users/richelbilderbeek/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/richelbilderbeek/subscriptions","organizations_url":"https://api.github.com/users/richelbilderbeek/orgs","repos_url":"https://api.github.com/users/richelbilderbeek/repos","events_url":"https://api.github.com/users/richelbilderbeek/events{/privacy}","received_events_url":"https://api.github.com/users/richelbilderbeek/received_events","type":"User","site_admin":false},"html_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov","description":"Minimal + project that uses qmake, GCC, C++11, Boost, Boost.Test, gcov and is tested by + Travis CI","fork":false,"url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov","forks_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/forks","keys_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/keys{/key_id}","collaborators_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/teams","hooks_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/hooks","issue_events_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/issues/events{/number}","events_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/events","assignees_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/assignees{/user}","branches_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/branches{/branch}","tags_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/tags","blobs_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/git/refs{/sha}","trees_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/git/trees{/sha}","statuses_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/statuses/{sha}","languages_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/languages","stargazers_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/stargazers","contributors_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/contributors","subscribers_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/subscribers","subscription_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/subscription","commits_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/commits{/sha}","git_commits_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/git/commits{/sha}","comments_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/comments{/number}","issue_comment_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/issues/comments{/number}","contents_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/contents/{+path}","compare_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/compare/{base}...{head}","merges_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/merges","archive_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/downloads","issues_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/issues{/number}","pulls_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/pulls{/number}","milestones_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/milestones{/number}","notifications_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/labels{/name}","releases_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/releases{/id}","deployments_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/deployments","created_at":"2016-03-20T11:43:59Z","updated_at":"2019-05-04T13:21:05Z","pushed_at":"2019-10-07T07:33:19Z","git_url":"git://github.com/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov.git","ssh_url":"git@github.com:richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov.git","clone_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov.git","svn_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov","homepage":null,"size":27,"stargazers_count":0,"watchers_count":0,"language":"QMake","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":4,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"gpl-3.0","name":"GNU + General Public License v3.0","spdx_id":"GPL-3.0","url":"https://api.github.com/licenses/gpl-3.0","node_id":"MDc6TGljZW5zZTk="},"forks":4,"open_issues":0,"watchers":0,"default_branch":"main"},"source":{"id":54314576,"node_id":"MDEwOlJlcG9zaXRvcnk1NDMxNDU3Ng==","name":"travis_qmake_gcc_cpp11_boost_test_gcov","full_name":"richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov","private":false,"owner":{"login":"richelbilderbeek","id":2098230,"node_id":"MDQ6VXNlcjIwOTgyMzA=","avatar_url":"https://avatars3.githubusercontent.com/u/2098230?v=4","gravatar_id":"","url":"https://api.github.com/users/richelbilderbeek","html_url":"https://github.com/richelbilderbeek","followers_url":"https://api.github.com/users/richelbilderbeek/followers","following_url":"https://api.github.com/users/richelbilderbeek/following{/other_user}","gists_url":"https://api.github.com/users/richelbilderbeek/gists{/gist_id}","starred_url":"https://api.github.com/users/richelbilderbeek/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/richelbilderbeek/subscriptions","organizations_url":"https://api.github.com/users/richelbilderbeek/orgs","repos_url":"https://api.github.com/users/richelbilderbeek/repos","events_url":"https://api.github.com/users/richelbilderbeek/events{/privacy}","received_events_url":"https://api.github.com/users/richelbilderbeek/received_events","type":"User","site_admin":false},"html_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov","description":"Minimal + project that uses qmake, GCC, C++11, Boost, Boost.Test, gcov and is tested by + Travis CI","fork":false,"url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov","forks_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/forks","keys_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/keys{/key_id}","collaborators_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/teams","hooks_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/hooks","issue_events_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/issues/events{/number}","events_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/events","assignees_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/assignees{/user}","branches_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/branches{/branch}","tags_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/tags","blobs_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/git/refs{/sha}","trees_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/git/trees{/sha}","statuses_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/statuses/{sha}","languages_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/languages","stargazers_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/stargazers","contributors_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/contributors","subscribers_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/subscribers","subscription_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/subscription","commits_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/commits{/sha}","git_commits_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/git/commits{/sha}","comments_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/comments{/number}","issue_comment_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/issues/comments{/number}","contents_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/contents/{+path}","compare_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/compare/{base}...{head}","merges_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/merges","archive_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/downloads","issues_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/issues{/number}","pulls_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/pulls{/number}","milestones_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/milestones{/number}","notifications_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/labels{/name}","releases_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/releases{/id}","deployments_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov/deployments","created_at":"2016-03-20T11:43:59Z","updated_at":"2019-05-04T13:21:05Z","pushed_at":"2019-10-07T07:33:19Z","git_url":"git://github.com/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov.git","ssh_url":"git@github.com:richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov.git","clone_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov.git","svn_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp11_boost_test_gcov","homepage":null,"size":27,"stargazers_count":0,"watchers_count":0,"language":"QMake","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":4,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"gpl-3.0","name":"GNU + General Public License v3.0","spdx_id":"GPL-3.0","url":"https://api.github.com/licenses/gpl-3.0","node_id":"MDc6TGljZW5zZTk="},"forks":4,"open_issues":0,"watchers":0,"default_branch":"main"},"network_count":4,"subscribers_count":5}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:38 GMT + ETag: + - W/"8c18cf30fd2507d62753a1df64869e514eafb391065963b59f6e9ef6cb75db93" + Last-Modified: + - Fri, 04 Sep 2020 02:47:19 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFC:1EA2:358A6C8:5DDA441:5F873635 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4925' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '75' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/example-cpp98 + response: + content: '{"id":65389969,"node_id":"MDEwOlJlcG9zaXRvcnk2NTM4OTk2OQ==","name":"example-cpp98","full_name":"codecov/example-cpp98","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-cpp98","description":"Test + how to use qmake and gcov","fork":true,"url":"https://api.github.com/repos/codecov/example-cpp98","forks_url":"https://api.github.com/repos/codecov/example-cpp98/forks","keys_url":"https://api.github.com/repos/codecov/example-cpp98/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-cpp98/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-cpp98/teams","hooks_url":"https://api.github.com/repos/codecov/example-cpp98/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-cpp98/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-cpp98/events","assignees_url":"https://api.github.com/repos/codecov/example-cpp98/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-cpp98/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-cpp98/tags","blobs_url":"https://api.github.com/repos/codecov/example-cpp98/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-cpp98/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-cpp98/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-cpp98/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-cpp98/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-cpp98/languages","stargazers_url":"https://api.github.com/repos/codecov/example-cpp98/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-cpp98/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-cpp98/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-cpp98/subscription","commits_url":"https://api.github.com/repos/codecov/example-cpp98/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-cpp98/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-cpp98/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-cpp98/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-cpp98/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-cpp98/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-cpp98/merges","archive_url":"https://api.github.com/repos/codecov/example-cpp98/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-cpp98/downloads","issues_url":"https://api.github.com/repos/codecov/example-cpp98/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-cpp98/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-cpp98/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-cpp98/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-cpp98/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-cpp98/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-cpp98/deployments","created_at":"2016-08-10T14:37:38Z","updated_at":"2019-03-30T07:53:46Z","pushed_at":"2018-04-26T08:51:17Z","git_url":"git://github.com/codecov/example-cpp98.git","ssh_url":"git@github.com:codecov/example-cpp98.git","clone_url":"https://github.com/codecov/example-cpp98.git","svn_url":"https://github.com/codecov/example-cpp98","homepage":null,"size":156,"stargazers_count":2,"watchers_count":2,"language":"Prolog","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":16,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"gpl-3.0","name":"GNU + General Public License v3.0","spdx_id":"GPL-3.0","url":"https://api.github.com/licenses/gpl-3.0","node_id":"MDc6TGljZW5zZTk="},"forks":16,"open_issues":0,"watchers":2,"default_branch":"main","permissions":{"admin":false,"push":true,"pull":true},"temp_clone_token":"","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"delete_branch_on_merge":false,"organization":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"parent":{"id":53744232,"node_id":"MDEwOlJlcG9zaXRvcnk1Mzc0NDIzMg==","name":"travis_qmake_gcc_cpp98_gcov","full_name":"richelbilderbeek/travis_qmake_gcc_cpp98_gcov","private":false,"owner":{"login":"richelbilderbeek","id":2098230,"node_id":"MDQ6VXNlcjIwOTgyMzA=","avatar_url":"https://avatars3.githubusercontent.com/u/2098230?v=4","gravatar_id":"","url":"https://api.github.com/users/richelbilderbeek","html_url":"https://github.com/richelbilderbeek","followers_url":"https://api.github.com/users/richelbilderbeek/followers","following_url":"https://api.github.com/users/richelbilderbeek/following{/other_user}","gists_url":"https://api.github.com/users/richelbilderbeek/gists{/gist_id}","starred_url":"https://api.github.com/users/richelbilderbeek/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/richelbilderbeek/subscriptions","organizations_url":"https://api.github.com/users/richelbilderbeek/orgs","repos_url":"https://api.github.com/users/richelbilderbeek/repos","events_url":"https://api.github.com/users/richelbilderbeek/events{/privacy}","received_events_url":"https://api.github.com/users/richelbilderbeek/received_events","type":"User","site_admin":false},"html_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp98_gcov","description":"Minimal + project that uses qmake, GCC, C++98, gcov and is tested by Travis CI","fork":false,"url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov","forks_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/forks","keys_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/keys{/key_id}","collaborators_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/teams","hooks_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/hooks","issue_events_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/issues/events{/number}","events_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/events","assignees_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/assignees{/user}","branches_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/branches{/branch}","tags_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/tags","blobs_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/git/refs{/sha}","trees_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/git/trees{/sha}","statuses_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/statuses/{sha}","languages_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/languages","stargazers_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/stargazers","contributors_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/contributors","subscribers_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/subscribers","subscription_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/subscription","commits_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/commits{/sha}","git_commits_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/git/commits{/sha}","comments_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/comments{/number}","issue_comment_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/issues/comments{/number}","contents_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/contents/{+path}","compare_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/compare/{base}...{head}","merges_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/merges","archive_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/downloads","issues_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/issues{/number}","pulls_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/pulls{/number}","milestones_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/milestones{/number}","notifications_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/labels{/name}","releases_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/releases{/id}","deployments_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/deployments","created_at":"2016-03-12T17:30:12Z","updated_at":"2019-08-15T10:05:54Z","pushed_at":"2019-08-15T10:05:52Z","git_url":"git://github.com/richelbilderbeek/travis_qmake_gcc_cpp98_gcov.git","ssh_url":"git@github.com:richelbilderbeek/travis_qmake_gcc_cpp98_gcov.git","clone_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp98_gcov.git","svn_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp98_gcov","homepage":"","size":278,"stargazers_count":1,"watchers_count":1,"language":"Shell","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":21,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"gpl-3.0","name":"GNU + General Public License v3.0","spdx_id":"GPL-3.0","url":"https://api.github.com/licenses/gpl-3.0","node_id":"MDc6TGljZW5zZTk="},"forks":21,"open_issues":0,"watchers":1,"default_branch":"main"},"source":{"id":53744232,"node_id":"MDEwOlJlcG9zaXRvcnk1Mzc0NDIzMg==","name":"travis_qmake_gcc_cpp98_gcov","full_name":"richelbilderbeek/travis_qmake_gcc_cpp98_gcov","private":false,"owner":{"login":"richelbilderbeek","id":2098230,"node_id":"MDQ6VXNlcjIwOTgyMzA=","avatar_url":"https://avatars3.githubusercontent.com/u/2098230?v=4","gravatar_id":"","url":"https://api.github.com/users/richelbilderbeek","html_url":"https://github.com/richelbilderbeek","followers_url":"https://api.github.com/users/richelbilderbeek/followers","following_url":"https://api.github.com/users/richelbilderbeek/following{/other_user}","gists_url":"https://api.github.com/users/richelbilderbeek/gists{/gist_id}","starred_url":"https://api.github.com/users/richelbilderbeek/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/richelbilderbeek/subscriptions","organizations_url":"https://api.github.com/users/richelbilderbeek/orgs","repos_url":"https://api.github.com/users/richelbilderbeek/repos","events_url":"https://api.github.com/users/richelbilderbeek/events{/privacy}","received_events_url":"https://api.github.com/users/richelbilderbeek/received_events","type":"User","site_admin":false},"html_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp98_gcov","description":"Minimal + project that uses qmake, GCC, C++98, gcov and is tested by Travis CI","fork":false,"url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov","forks_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/forks","keys_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/keys{/key_id}","collaborators_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/teams","hooks_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/hooks","issue_events_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/issues/events{/number}","events_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/events","assignees_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/assignees{/user}","branches_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/branches{/branch}","tags_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/tags","blobs_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/git/refs{/sha}","trees_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/git/trees{/sha}","statuses_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/statuses/{sha}","languages_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/languages","stargazers_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/stargazers","contributors_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/contributors","subscribers_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/subscribers","subscription_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/subscription","commits_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/commits{/sha}","git_commits_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/git/commits{/sha}","comments_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/comments{/number}","issue_comment_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/issues/comments{/number}","contents_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/contents/{+path}","compare_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/compare/{base}...{head}","merges_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/merges","archive_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/downloads","issues_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/issues{/number}","pulls_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/pulls{/number}","milestones_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/milestones{/number}","notifications_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/labels{/name}","releases_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/releases{/id}","deployments_url":"https://api.github.com/repos/richelbilderbeek/travis_qmake_gcc_cpp98_gcov/deployments","created_at":"2016-03-12T17:30:12Z","updated_at":"2019-08-15T10:05:54Z","pushed_at":"2019-08-15T10:05:52Z","git_url":"git://github.com/richelbilderbeek/travis_qmake_gcc_cpp98_gcov.git","ssh_url":"git@github.com:richelbilderbeek/travis_qmake_gcc_cpp98_gcov.git","clone_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp98_gcov.git","svn_url":"https://github.com/richelbilderbeek/travis_qmake_gcc_cpp98_gcov","homepage":"","size":278,"stargazers_count":1,"watchers_count":1,"language":"Shell","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":21,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"gpl-3.0","name":"GNU + General Public License v3.0","spdx_id":"GPL-3.0","url":"https://api.github.com/licenses/gpl-3.0","node_id":"MDc6TGljZW5zZTk="},"forks":21,"open_issues":0,"watchers":1,"default_branch":"main"},"network_count":21,"subscribers_count":7}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:38 GMT + ETag: + - W/"c35c18e1013041cc22e82c9a3e00d6af2a15a13cea43e368c5c0ddcac262845a" + Last-Modified: + - Sat, 30 Mar 2019 07:53:46 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFC:1EA2:358A6F1:5DDA481:5F873636 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4924' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '76' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/nginx-buildpack + response: + content: '{"id":39343510,"node_id":"MDEwOlJlcG9zaXRvcnkzOTM0MzUxMA==","name":"nginx-buildpack","full_name":"codecov/nginx-buildpack","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/nginx-buildpack","description":"Run + NGINX in front of your app server on Heroku","fork":true,"url":"https://api.github.com/repos/codecov/nginx-buildpack","forks_url":"https://api.github.com/repos/codecov/nginx-buildpack/forks","keys_url":"https://api.github.com/repos/codecov/nginx-buildpack/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/nginx-buildpack/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/nginx-buildpack/teams","hooks_url":"https://api.github.com/repos/codecov/nginx-buildpack/hooks","issue_events_url":"https://api.github.com/repos/codecov/nginx-buildpack/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/nginx-buildpack/events","assignees_url":"https://api.github.com/repos/codecov/nginx-buildpack/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/nginx-buildpack/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/nginx-buildpack/tags","blobs_url":"https://api.github.com/repos/codecov/nginx-buildpack/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/nginx-buildpack/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/nginx-buildpack/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/nginx-buildpack/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/nginx-buildpack/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/nginx-buildpack/languages","stargazers_url":"https://api.github.com/repos/codecov/nginx-buildpack/stargazers","contributors_url":"https://api.github.com/repos/codecov/nginx-buildpack/contributors","subscribers_url":"https://api.github.com/repos/codecov/nginx-buildpack/subscribers","subscription_url":"https://api.github.com/repos/codecov/nginx-buildpack/subscription","commits_url":"https://api.github.com/repos/codecov/nginx-buildpack/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/nginx-buildpack/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/nginx-buildpack/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/nginx-buildpack/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/nginx-buildpack/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/nginx-buildpack/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/nginx-buildpack/merges","archive_url":"https://api.github.com/repos/codecov/nginx-buildpack/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/nginx-buildpack/downloads","issues_url":"https://api.github.com/repos/codecov/nginx-buildpack/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/nginx-buildpack/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/nginx-buildpack/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/nginx-buildpack/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/nginx-buildpack/labels{/name}","releases_url":"https://api.github.com/repos/codecov/nginx-buildpack/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/nginx-buildpack/deployments","created_at":"2015-07-19T18:04:49Z","updated_at":"2020-09-13T14:52:37Z","pushed_at":"2018-07-24T12:53:33Z","git_url":"git://github.com/codecov/nginx-buildpack.git","ssh_url":"git@github.com:codecov/nginx-buildpack.git","clone_url":"https://github.com/codecov/nginx-buildpack.git","svn_url":"https://github.com/codecov/nginx-buildpack","homepage":"","size":7811,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":2,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":0,"license":null,"forks":2,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true},"temp_clone_token":"","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"delete_branch_on_merge":false,"organization":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"parent":{"id":26317174,"node_id":"MDEwOlJlcG9zaXRvcnkyNjMxNzE3NA==","name":"nginx-buildpack","full_name":"stevepeak/nginx-buildpack","private":false,"owner":{"login":"stevepeak","id":2041757,"node_id":"MDQ6VXNlcjIwNDE3NTc=","avatar_url":"https://avatars1.githubusercontent.com/u/2041757?v=4","gravatar_id":"","url":"https://api.github.com/users/stevepeak","html_url":"https://github.com/stevepeak","followers_url":"https://api.github.com/users/stevepeak/followers","following_url":"https://api.github.com/users/stevepeak/following{/other_user}","gists_url":"https://api.github.com/users/stevepeak/gists{/gist_id}","starred_url":"https://api.github.com/users/stevepeak/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stevepeak/subscriptions","organizations_url":"https://api.github.com/users/stevepeak/orgs","repos_url":"https://api.github.com/users/stevepeak/repos","events_url":"https://api.github.com/users/stevepeak/events{/privacy}","received_events_url":"https://api.github.com/users/stevepeak/received_events","type":"User","site_admin":false},"html_url":"https://github.com/stevepeak/nginx-buildpack","description":"Run + NGINX in front of your app server on Heroku","fork":true,"url":"https://api.github.com/repos/stevepeak/nginx-buildpack","forks_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/forks","keys_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/keys{/key_id}","collaborators_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/teams","hooks_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/hooks","issue_events_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/issues/events{/number}","events_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/events","assignees_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/assignees{/user}","branches_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/branches{/branch}","tags_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/tags","blobs_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/git/refs{/sha}","trees_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/git/trees{/sha}","statuses_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/statuses/{sha}","languages_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/languages","stargazers_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/stargazers","contributors_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/contributors","subscribers_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/subscribers","subscription_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/subscription","commits_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/commits{/sha}","git_commits_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/git/commits{/sha}","comments_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/comments{/number}","issue_comment_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/issues/comments{/number}","contents_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/contents/{+path}","compare_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/compare/{base}...{head}","merges_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/merges","archive_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/downloads","issues_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/issues{/number}","pulls_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/pulls{/number}","milestones_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/milestones{/number}","notifications_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/labels{/name}","releases_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/releases{/id}","deployments_url":"https://api.github.com/repos/stevepeak/nginx-buildpack/deployments","created_at":"2014-11-07T11:35:43Z","updated_at":"2020-09-13T14:52:37Z","pushed_at":"2016-04-11T18:04:10Z","git_url":"git://github.com/stevepeak/nginx-buildpack.git","ssh_url":"git@github.com:stevepeak/nginx-buildpack.git","clone_url":"https://github.com/stevepeak/nginx-buildpack.git","svn_url":"https://github.com/stevepeak/nginx-buildpack","homepage":"","size":6793,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"forks":1,"open_issues":0,"watchers":0,"default_branch":"main"},"source":{"id":44764319,"node_id":"MDEwOlJlcG9zaXRvcnk0NDc2NDMxOQ==","name":"heroku-buildpack-nginx","full_name":"heroku/heroku-buildpack-nginx","private":false,"owner":{"login":"heroku","id":23211,"node_id":"MDEyOk9yZ2FuaXphdGlvbjIzMjEx","avatar_url":"https://avatars3.githubusercontent.com/u/23211?v=4","gravatar_id":"","url":"https://api.github.com/users/heroku","html_url":"https://github.com/heroku","followers_url":"https://api.github.com/users/heroku/followers","following_url":"https://api.github.com/users/heroku/following{/other_user}","gists_url":"https://api.github.com/users/heroku/gists{/gist_id}","starred_url":"https://api.github.com/users/heroku/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/heroku/subscriptions","organizations_url":"https://api.github.com/users/heroku/orgs","repos_url":"https://api.github.com/users/heroku/repos","events_url":"https://api.github.com/users/heroku/events{/privacy}","received_events_url":"https://api.github.com/users/heroku/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/heroku/heroku-buildpack-nginx","description":"Run + NGINX in front of your app server on Heroku","fork":false,"url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx","forks_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/forks","keys_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/keys{/key_id}","collaborators_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/teams","hooks_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/hooks","issue_events_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/issues/events{/number}","events_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/events","assignees_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/assignees{/user}","branches_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/branches{/branch}","tags_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/tags","blobs_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/git/refs{/sha}","trees_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/git/trees{/sha}","statuses_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/statuses/{sha}","languages_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/languages","stargazers_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/stargazers","contributors_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/contributors","subscribers_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/subscribers","subscription_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/subscription","commits_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/commits{/sha}","git_commits_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/git/commits{/sha}","comments_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/comments{/number}","issue_comment_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/issues/comments{/number}","contents_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/contents/{+path}","compare_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/compare/{base}...{head}","merges_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/merges","archive_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/downloads","issues_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/issues{/number}","pulls_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/pulls{/number}","milestones_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/milestones{/number}","notifications_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/labels{/name}","releases_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/releases{/id}","deployments_url":"https://api.github.com/repos/heroku/heroku-buildpack-nginx/deployments","created_at":"2015-10-22T18:14:02Z","updated_at":"2020-10-04T15:53:38Z","pushed_at":"2020-10-07T22:03:24Z","git_url":"git://github.com/heroku/heroku-buildpack-nginx.git","ssh_url":"git@github.com:heroku/heroku-buildpack-nginx.git","clone_url":"https://github.com/heroku/heroku-buildpack-nginx.git","svn_url":"https://github.com/heroku/heroku-buildpack-nginx","homepage":"","size":72022,"stargazers_count":160,"watchers_count":160,"language":"Shell","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":661,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":10,"license":null,"forks":661,"open_issues":10,"watchers":160,"default_branch":"main"},"network_count":661,"subscribers_count":2}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:38 GMT + ETag: + - W/"5b6547e051d906faab2c28834410c0c5a9f8604c3558942b85e41ca3e32469de" + Last-Modified: + - Sun, 13 Sep 2020 14:52:37 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFC:1EA2:358A71D:5DDA4C2:5F873636 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4923' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '77' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/services + response: + content: '{"id":35773997,"node_id":"MDEwOlJlcG9zaXRvcnkzNTc3Mzk5Nw==","name":"services","full_name":"codecov/services","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/services","description":"Gitter + integrations","fork":true,"url":"https://api.github.com/repos/codecov/services","forks_url":"https://api.github.com/repos/codecov/services/forks","keys_url":"https://api.github.com/repos/codecov/services/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/services/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/services/teams","hooks_url":"https://api.github.com/repos/codecov/services/hooks","issue_events_url":"https://api.github.com/repos/codecov/services/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/services/events","assignees_url":"https://api.github.com/repos/codecov/services/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/services/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/services/tags","blobs_url":"https://api.github.com/repos/codecov/services/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/services/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/services/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/services/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/services/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/services/languages","stargazers_url":"https://api.github.com/repos/codecov/services/stargazers","contributors_url":"https://api.github.com/repos/codecov/services/contributors","subscribers_url":"https://api.github.com/repos/codecov/services/subscribers","subscription_url":"https://api.github.com/repos/codecov/services/subscription","commits_url":"https://api.github.com/repos/codecov/services/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/services/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/services/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/services/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/services/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/services/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/services/merges","archive_url":"https://api.github.com/repos/codecov/services/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/services/downloads","issues_url":"https://api.github.com/repos/codecov/services/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/services/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/services/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/services/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/services/labels{/name}","releases_url":"https://api.github.com/repos/codecov/services/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/services/deployments","created_at":"2015-05-17T16:43:35Z","updated_at":"2020-09-10T20:32:40Z","pushed_at":"2020-09-10T20:32:37Z","git_url":"git://github.com/codecov/services.git","ssh_url":"git@github.com:codecov/services.git","clone_url":"https://github.com/codecov/services.git","svn_url":"https://github.com/codecov/services","homepage":"","size":565,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":2,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"forks":2,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true},"temp_clone_token":"","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"delete_branch_on_merge":false,"organization":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"parent":{"id":17982418,"node_id":"MDEwOlJlcG9zaXRvcnkxNzk4MjQxOA==","name":"services","full_name":"gitterHQ/services","private":false,"owner":{"login":"gitterHQ","id":5990364,"node_id":"MDEyOk9yZ2FuaXphdGlvbjU5OTAzNjQ=","avatar_url":"https://avatars0.githubusercontent.com/u/5990364?v=4","gravatar_id":"","url":"https://api.github.com/users/gitterHQ","html_url":"https://github.com/gitterHQ","followers_url":"https://api.github.com/users/gitterHQ/followers","following_url":"https://api.github.com/users/gitterHQ/following{/other_user}","gists_url":"https://api.github.com/users/gitterHQ/gists{/gist_id}","starred_url":"https://api.github.com/users/gitterHQ/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gitterHQ/subscriptions","organizations_url":"https://api.github.com/users/gitterHQ/orgs","repos_url":"https://api.github.com/users/gitterHQ/repos","events_url":"https://api.github.com/users/gitterHQ/events{/privacy}","received_events_url":"https://api.github.com/users/gitterHQ/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/gitterHQ/services","description":"Project + moved to https://gitlab.com/gitlab-org/gitter/services","fork":false,"url":"https://api.github.com/repos/gitterHQ/services","forks_url":"https://api.github.com/repos/gitterHQ/services/forks","keys_url":"https://api.github.com/repos/gitterHQ/services/keys{/key_id}","collaborators_url":"https://api.github.com/repos/gitterHQ/services/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/gitterHQ/services/teams","hooks_url":"https://api.github.com/repos/gitterHQ/services/hooks","issue_events_url":"https://api.github.com/repos/gitterHQ/services/issues/events{/number}","events_url":"https://api.github.com/repos/gitterHQ/services/events","assignees_url":"https://api.github.com/repos/gitterHQ/services/assignees{/user}","branches_url":"https://api.github.com/repos/gitterHQ/services/branches{/branch}","tags_url":"https://api.github.com/repos/gitterHQ/services/tags","blobs_url":"https://api.github.com/repos/gitterHQ/services/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/gitterHQ/services/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/gitterHQ/services/git/refs{/sha}","trees_url":"https://api.github.com/repos/gitterHQ/services/git/trees{/sha}","statuses_url":"https://api.github.com/repos/gitterHQ/services/statuses/{sha}","languages_url":"https://api.github.com/repos/gitterHQ/services/languages","stargazers_url":"https://api.github.com/repos/gitterHQ/services/stargazers","contributors_url":"https://api.github.com/repos/gitterHQ/services/contributors","subscribers_url":"https://api.github.com/repos/gitterHQ/services/subscribers","subscription_url":"https://api.github.com/repos/gitterHQ/services/subscription","commits_url":"https://api.github.com/repos/gitterHQ/services/commits{/sha}","git_commits_url":"https://api.github.com/repos/gitterHQ/services/git/commits{/sha}","comments_url":"https://api.github.com/repos/gitterHQ/services/comments{/number}","issue_comment_url":"https://api.github.com/repos/gitterHQ/services/issues/comments{/number}","contents_url":"https://api.github.com/repos/gitterHQ/services/contents/{+path}","compare_url":"https://api.github.com/repos/gitterHQ/services/compare/{base}...{head}","merges_url":"https://api.github.com/repos/gitterHQ/services/merges","archive_url":"https://api.github.com/repos/gitterHQ/services/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/gitterHQ/services/downloads","issues_url":"https://api.github.com/repos/gitterHQ/services/issues{/number}","pulls_url":"https://api.github.com/repos/gitterHQ/services/pulls{/number}","milestones_url":"https://api.github.com/repos/gitterHQ/services/milestones{/number}","notifications_url":"https://api.github.com/repos/gitterHQ/services/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/gitterHQ/services/labels{/name}","releases_url":"https://api.github.com/repos/gitterHQ/services/releases{/id}","deployments_url":"https://api.github.com/repos/gitterHQ/services/deployments","created_at":"2014-03-21T14:35:21Z","updated_at":"2019-11-06T02:56:15Z","pushed_at":"2018-07-18T20:12:51Z","git_url":"git://github.com/gitterHQ/services.git","ssh_url":"git@github.com:gitterHQ/services.git","clone_url":"https://github.com/gitterHQ/services.git","svn_url":"https://github.com/gitterHQ/services","homepage":"https://gitlab.com/gitlab-org/gitter/services","size":686,"stargazers_count":144,"watchers_count":144,"language":"JavaScript","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":87,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":35,"license":null,"forks":87,"open_issues":35,"watchers":144,"default_branch":"main"},"source":{"id":17982418,"node_id":"MDEwOlJlcG9zaXRvcnkxNzk4MjQxOA==","name":"services","full_name":"gitterHQ/services","private":false,"owner":{"login":"gitterHQ","id":5990364,"node_id":"MDEyOk9yZ2FuaXphdGlvbjU5OTAzNjQ=","avatar_url":"https://avatars0.githubusercontent.com/u/5990364?v=4","gravatar_id":"","url":"https://api.github.com/users/gitterHQ","html_url":"https://github.com/gitterHQ","followers_url":"https://api.github.com/users/gitterHQ/followers","following_url":"https://api.github.com/users/gitterHQ/following{/other_user}","gists_url":"https://api.github.com/users/gitterHQ/gists{/gist_id}","starred_url":"https://api.github.com/users/gitterHQ/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gitterHQ/subscriptions","organizations_url":"https://api.github.com/users/gitterHQ/orgs","repos_url":"https://api.github.com/users/gitterHQ/repos","events_url":"https://api.github.com/users/gitterHQ/events{/privacy}","received_events_url":"https://api.github.com/users/gitterHQ/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/gitterHQ/services","description":"Project + moved to https://gitlab.com/gitlab-org/gitter/services","fork":false,"url":"https://api.github.com/repos/gitterHQ/services","forks_url":"https://api.github.com/repos/gitterHQ/services/forks","keys_url":"https://api.github.com/repos/gitterHQ/services/keys{/key_id}","collaborators_url":"https://api.github.com/repos/gitterHQ/services/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/gitterHQ/services/teams","hooks_url":"https://api.github.com/repos/gitterHQ/services/hooks","issue_events_url":"https://api.github.com/repos/gitterHQ/services/issues/events{/number}","events_url":"https://api.github.com/repos/gitterHQ/services/events","assignees_url":"https://api.github.com/repos/gitterHQ/services/assignees{/user}","branches_url":"https://api.github.com/repos/gitterHQ/services/branches{/branch}","tags_url":"https://api.github.com/repos/gitterHQ/services/tags","blobs_url":"https://api.github.com/repos/gitterHQ/services/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/gitterHQ/services/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/gitterHQ/services/git/refs{/sha}","trees_url":"https://api.github.com/repos/gitterHQ/services/git/trees{/sha}","statuses_url":"https://api.github.com/repos/gitterHQ/services/statuses/{sha}","languages_url":"https://api.github.com/repos/gitterHQ/services/languages","stargazers_url":"https://api.github.com/repos/gitterHQ/services/stargazers","contributors_url":"https://api.github.com/repos/gitterHQ/services/contributors","subscribers_url":"https://api.github.com/repos/gitterHQ/services/subscribers","subscription_url":"https://api.github.com/repos/gitterHQ/services/subscription","commits_url":"https://api.github.com/repos/gitterHQ/services/commits{/sha}","git_commits_url":"https://api.github.com/repos/gitterHQ/services/git/commits{/sha}","comments_url":"https://api.github.com/repos/gitterHQ/services/comments{/number}","issue_comment_url":"https://api.github.com/repos/gitterHQ/services/issues/comments{/number}","contents_url":"https://api.github.com/repos/gitterHQ/services/contents/{+path}","compare_url":"https://api.github.com/repos/gitterHQ/services/compare/{base}...{head}","merges_url":"https://api.github.com/repos/gitterHQ/services/merges","archive_url":"https://api.github.com/repos/gitterHQ/services/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/gitterHQ/services/downloads","issues_url":"https://api.github.com/repos/gitterHQ/services/issues{/number}","pulls_url":"https://api.github.com/repos/gitterHQ/services/pulls{/number}","milestones_url":"https://api.github.com/repos/gitterHQ/services/milestones{/number}","notifications_url":"https://api.github.com/repos/gitterHQ/services/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/gitterHQ/services/labels{/name}","releases_url":"https://api.github.com/repos/gitterHQ/services/releases{/id}","deployments_url":"https://api.github.com/repos/gitterHQ/services/deployments","created_at":"2014-03-21T14:35:21Z","updated_at":"2019-11-06T02:56:15Z","pushed_at":"2018-07-18T20:12:51Z","git_url":"git://github.com/gitterHQ/services.git","ssh_url":"git@github.com:gitterHQ/services.git","clone_url":"https://github.com/gitterHQ/services.git","svn_url":"https://github.com/gitterHQ/services","homepage":"https://gitlab.com/gitlab-org/gitter/services","size":686,"stargazers_count":144,"watchers_count":144,"language":"JavaScript","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":87,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":35,"license":null,"forks":87,"open_issues":35,"watchers":144,"default_branch":"main"},"network_count":87,"subscribers_count":2}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:39 GMT + ETag: + - W/"40612f5d66197904997a9d581212583c33335b48d03ea3ffbda8037976edfc88" + Last-Modified: + - Thu, 10 Sep 2020 20:32:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFC:1EA2:358A751:5DDA520:5F873636 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4922' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '78' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/SwiftCov + response: + content: '{"id":37015668,"node_id":"MDEwOlJlcG9zaXRvcnkzNzAxNTY2OA==","name":"SwiftCov","full_name":"codecov/SwiftCov","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/SwiftCov","description":"A + tool to generate test code coverage information for Swift.","fork":true,"url":"https://api.github.com/repos/codecov/SwiftCov","forks_url":"https://api.github.com/repos/codecov/SwiftCov/forks","keys_url":"https://api.github.com/repos/codecov/SwiftCov/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/SwiftCov/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/SwiftCov/teams","hooks_url":"https://api.github.com/repos/codecov/SwiftCov/hooks","issue_events_url":"https://api.github.com/repos/codecov/SwiftCov/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/SwiftCov/events","assignees_url":"https://api.github.com/repos/codecov/SwiftCov/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/SwiftCov/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/SwiftCov/tags","blobs_url":"https://api.github.com/repos/codecov/SwiftCov/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/SwiftCov/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/SwiftCov/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/SwiftCov/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/SwiftCov/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/SwiftCov/languages","stargazers_url":"https://api.github.com/repos/codecov/SwiftCov/stargazers","contributors_url":"https://api.github.com/repos/codecov/SwiftCov/contributors","subscribers_url":"https://api.github.com/repos/codecov/SwiftCov/subscribers","subscription_url":"https://api.github.com/repos/codecov/SwiftCov/subscription","commits_url":"https://api.github.com/repos/codecov/SwiftCov/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/SwiftCov/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/SwiftCov/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/SwiftCov/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/SwiftCov/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/SwiftCov/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/SwiftCov/merges","archive_url":"https://api.github.com/repos/codecov/SwiftCov/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/SwiftCov/downloads","issues_url":"https://api.github.com/repos/codecov/SwiftCov/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/SwiftCov/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/SwiftCov/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/SwiftCov/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/SwiftCov/labels{/name}","releases_url":"https://api.github.com/repos/codecov/SwiftCov/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/SwiftCov/deployments","created_at":"2015-06-07T12:24:42Z","updated_at":"2019-09-19T15:42:02Z","pushed_at":"2015-06-07T12:27:00Z","git_url":"git://github.com/codecov/SwiftCov.git","ssh_url":"git@github.com:codecov/SwiftCov.git","clone_url":"https://github.com/codecov/SwiftCov.git","svn_url":"https://github.com/codecov/SwiftCov","homepage":"","size":208,"stargazers_count":2,"watchers_count":2,"language":"Swift","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":1,"open_issues":0,"watchers":2,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true},"temp_clone_token":"","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"delete_branch_on_merge":false,"organization":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"parent":{"id":35943055,"node_id":"MDEwOlJlcG9zaXRvcnkzNTk0MzA1NQ==","name":"SwiftCov","full_name":"realm/SwiftCov","private":false,"owner":{"login":"realm","id":7575099,"node_id":"MDEyOk9yZ2FuaXphdGlvbjc1NzUwOTk=","avatar_url":"https://avatars0.githubusercontent.com/u/7575099?v=4","gravatar_id":"","url":"https://api.github.com/users/realm","html_url":"https://github.com/realm","followers_url":"https://api.github.com/users/realm/followers","following_url":"https://api.github.com/users/realm/following{/other_user}","gists_url":"https://api.github.com/users/realm/gists{/gist_id}","starred_url":"https://api.github.com/users/realm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/realm/subscriptions","organizations_url":"https://api.github.com/users/realm/orgs","repos_url":"https://api.github.com/users/realm/repos","events_url":"https://api.github.com/users/realm/events{/privacy}","received_events_url":"https://api.github.com/users/realm/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/realm/SwiftCov","description":"A + tool to generate test code coverage information for Swift.","fork":false,"url":"https://api.github.com/repos/realm/SwiftCov","forks_url":"https://api.github.com/repos/realm/SwiftCov/forks","keys_url":"https://api.github.com/repos/realm/SwiftCov/keys{/key_id}","collaborators_url":"https://api.github.com/repos/realm/SwiftCov/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/realm/SwiftCov/teams","hooks_url":"https://api.github.com/repos/realm/SwiftCov/hooks","issue_events_url":"https://api.github.com/repos/realm/SwiftCov/issues/events{/number}","events_url":"https://api.github.com/repos/realm/SwiftCov/events","assignees_url":"https://api.github.com/repos/realm/SwiftCov/assignees{/user}","branches_url":"https://api.github.com/repos/realm/SwiftCov/branches{/branch}","tags_url":"https://api.github.com/repos/realm/SwiftCov/tags","blobs_url":"https://api.github.com/repos/realm/SwiftCov/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/realm/SwiftCov/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/realm/SwiftCov/git/refs{/sha}","trees_url":"https://api.github.com/repos/realm/SwiftCov/git/trees{/sha}","statuses_url":"https://api.github.com/repos/realm/SwiftCov/statuses/{sha}","languages_url":"https://api.github.com/repos/realm/SwiftCov/languages","stargazers_url":"https://api.github.com/repos/realm/SwiftCov/stargazers","contributors_url":"https://api.github.com/repos/realm/SwiftCov/contributors","subscribers_url":"https://api.github.com/repos/realm/SwiftCov/subscribers","subscription_url":"https://api.github.com/repos/realm/SwiftCov/subscription","commits_url":"https://api.github.com/repos/realm/SwiftCov/commits{/sha}","git_commits_url":"https://api.github.com/repos/realm/SwiftCov/git/commits{/sha}","comments_url":"https://api.github.com/repos/realm/SwiftCov/comments{/number}","issue_comment_url":"https://api.github.com/repos/realm/SwiftCov/issues/comments{/number}","contents_url":"https://api.github.com/repos/realm/SwiftCov/contents/{+path}","compare_url":"https://api.github.com/repos/realm/SwiftCov/compare/{base}...{head}","merges_url":"https://api.github.com/repos/realm/SwiftCov/merges","archive_url":"https://api.github.com/repos/realm/SwiftCov/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/realm/SwiftCov/downloads","issues_url":"https://api.github.com/repos/realm/SwiftCov/issues{/number}","pulls_url":"https://api.github.com/repos/realm/SwiftCov/pulls{/number}","milestones_url":"https://api.github.com/repos/realm/SwiftCov/milestones{/number}","notifications_url":"https://api.github.com/repos/realm/SwiftCov/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/realm/SwiftCov/labels{/name}","releases_url":"https://api.github.com/repos/realm/SwiftCov/releases{/id}","deployments_url":"https://api.github.com/repos/realm/SwiftCov/deployments","created_at":"2015-05-20T11:07:52Z","updated_at":"2020-10-13T13:49:23Z","pushed_at":"2017-08-10T20:42:13Z","git_url":"git://github.com/realm/SwiftCov.git","ssh_url":"git@github.com:realm/SwiftCov.git","clone_url":"https://github.com/realm/SwiftCov.git","svn_url":"https://github.com/realm/SwiftCov","homepage":"","size":119,"stargazers_count":558,"watchers_count":558,"language":"Swift","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":37,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":14,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":37,"open_issues":14,"watchers":558,"default_branch":"main"},"source":{"id":35943055,"node_id":"MDEwOlJlcG9zaXRvcnkzNTk0MzA1NQ==","name":"SwiftCov","full_name":"realm/SwiftCov","private":false,"owner":{"login":"realm","id":7575099,"node_id":"MDEyOk9yZ2FuaXphdGlvbjc1NzUwOTk=","avatar_url":"https://avatars0.githubusercontent.com/u/7575099?v=4","gravatar_id":"","url":"https://api.github.com/users/realm","html_url":"https://github.com/realm","followers_url":"https://api.github.com/users/realm/followers","following_url":"https://api.github.com/users/realm/following{/other_user}","gists_url":"https://api.github.com/users/realm/gists{/gist_id}","starred_url":"https://api.github.com/users/realm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/realm/subscriptions","organizations_url":"https://api.github.com/users/realm/orgs","repos_url":"https://api.github.com/users/realm/repos","events_url":"https://api.github.com/users/realm/events{/privacy}","received_events_url":"https://api.github.com/users/realm/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/realm/SwiftCov","description":"A + tool to generate test code coverage information for Swift.","fork":false,"url":"https://api.github.com/repos/realm/SwiftCov","forks_url":"https://api.github.com/repos/realm/SwiftCov/forks","keys_url":"https://api.github.com/repos/realm/SwiftCov/keys{/key_id}","collaborators_url":"https://api.github.com/repos/realm/SwiftCov/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/realm/SwiftCov/teams","hooks_url":"https://api.github.com/repos/realm/SwiftCov/hooks","issue_events_url":"https://api.github.com/repos/realm/SwiftCov/issues/events{/number}","events_url":"https://api.github.com/repos/realm/SwiftCov/events","assignees_url":"https://api.github.com/repos/realm/SwiftCov/assignees{/user}","branches_url":"https://api.github.com/repos/realm/SwiftCov/branches{/branch}","tags_url":"https://api.github.com/repos/realm/SwiftCov/tags","blobs_url":"https://api.github.com/repos/realm/SwiftCov/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/realm/SwiftCov/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/realm/SwiftCov/git/refs{/sha}","trees_url":"https://api.github.com/repos/realm/SwiftCov/git/trees{/sha}","statuses_url":"https://api.github.com/repos/realm/SwiftCov/statuses/{sha}","languages_url":"https://api.github.com/repos/realm/SwiftCov/languages","stargazers_url":"https://api.github.com/repos/realm/SwiftCov/stargazers","contributors_url":"https://api.github.com/repos/realm/SwiftCov/contributors","subscribers_url":"https://api.github.com/repos/realm/SwiftCov/subscribers","subscription_url":"https://api.github.com/repos/realm/SwiftCov/subscription","commits_url":"https://api.github.com/repos/realm/SwiftCov/commits{/sha}","git_commits_url":"https://api.github.com/repos/realm/SwiftCov/git/commits{/sha}","comments_url":"https://api.github.com/repos/realm/SwiftCov/comments{/number}","issue_comment_url":"https://api.github.com/repos/realm/SwiftCov/issues/comments{/number}","contents_url":"https://api.github.com/repos/realm/SwiftCov/contents/{+path}","compare_url":"https://api.github.com/repos/realm/SwiftCov/compare/{base}...{head}","merges_url":"https://api.github.com/repos/realm/SwiftCov/merges","archive_url":"https://api.github.com/repos/realm/SwiftCov/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/realm/SwiftCov/downloads","issues_url":"https://api.github.com/repos/realm/SwiftCov/issues{/number}","pulls_url":"https://api.github.com/repos/realm/SwiftCov/pulls{/number}","milestones_url":"https://api.github.com/repos/realm/SwiftCov/milestones{/number}","notifications_url":"https://api.github.com/repos/realm/SwiftCov/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/realm/SwiftCov/labels{/name}","releases_url":"https://api.github.com/repos/realm/SwiftCov/releases{/id}","deployments_url":"https://api.github.com/repos/realm/SwiftCov/deployments","created_at":"2015-05-20T11:07:52Z","updated_at":"2020-10-13T13:49:23Z","pushed_at":"2017-08-10T20:42:13Z","git_url":"git://github.com/realm/SwiftCov.git","ssh_url":"git@github.com:realm/SwiftCov.git","clone_url":"https://github.com/realm/SwiftCov.git","svn_url":"https://github.com/realm/SwiftCov","homepage":"","size":119,"stargazers_count":558,"watchers_count":558,"language":"Swift","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":37,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":14,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":37,"open_issues":14,"watchers":558,"default_branch":"main"},"network_count":37,"subscribers_count":2}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:39 GMT + ETag: + - W/"d904063e8eb4f1610c54d91384378adbdc34ff2272caf907542b7b0ba93065cd" + Last-Modified: + - Thu, 19 Sep 2019 15:42:02 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFC:1EA2:358A780:5DDA562:5F873637 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4921' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '79' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/user/repos?per_page=50&page=2 + response: + content: '[{"id":7615485,"node_id":"MDEwOlJlcG9zaXRvcnk3NjE1NDg1","name":"timestring","full_name":"codecov/timestring","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/timestring","description":"Making + time easier since \"Jan 17th, 2013 at 3:59pm\"","fork":false,"url":"https://api.github.com/repos/codecov/timestring","forks_url":"https://api.github.com/repos/codecov/timestring/forks","keys_url":"https://api.github.com/repos/codecov/timestring/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/timestring/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/timestring/teams","hooks_url":"https://api.github.com/repos/codecov/timestring/hooks","issue_events_url":"https://api.github.com/repos/codecov/timestring/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/timestring/events","assignees_url":"https://api.github.com/repos/codecov/timestring/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/timestring/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/timestring/tags","blobs_url":"https://api.github.com/repos/codecov/timestring/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/timestring/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/timestring/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/timestring/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/timestring/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/timestring/languages","stargazers_url":"https://api.github.com/repos/codecov/timestring/stargazers","contributors_url":"https://api.github.com/repos/codecov/timestring/contributors","subscribers_url":"https://api.github.com/repos/codecov/timestring/subscribers","subscription_url":"https://api.github.com/repos/codecov/timestring/subscription","commits_url":"https://api.github.com/repos/codecov/timestring/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/timestring/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/timestring/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/timestring/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/timestring/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/timestring/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/timestring/merges","archive_url":"https://api.github.com/repos/codecov/timestring/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/timestring/downloads","issues_url":"https://api.github.com/repos/codecov/timestring/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/timestring/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/timestring/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/timestring/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/timestring/labels{/name}","releases_url":"https://api.github.com/repos/codecov/timestring/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/timestring/deployments","created_at":"2013-01-15T00:13:44Z","updated_at":"2020-10-04T13:51:15Z","pushed_at":"2020-09-21T17:14:52Z","git_url":"git://github.com/codecov/timestring.git","ssh_url":"git@github.com:codecov/timestring.git","clone_url":"https://github.com/codecov/timestring.git","svn_url":"https://github.com/codecov/timestring","homepage":"","size":147,"stargazers_count":99,"watchers_count":99,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":25,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":27,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":25,"open_issues":27,"watchers":99,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true}},{"id":40784552,"node_id":"MDEwOlJlcG9zaXRvcnk0MDc4NDU1Mg==","name":"tornado","full_name":"codecov/tornado","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/tornado","description":"Tornado + is a Python web framework and asynchronous networking library, originally developed + at FriendFeed.","fork":true,"url":"https://api.github.com/repos/codecov/tornado","forks_url":"https://api.github.com/repos/codecov/tornado/forks","keys_url":"https://api.github.com/repos/codecov/tornado/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/tornado/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/tornado/teams","hooks_url":"https://api.github.com/repos/codecov/tornado/hooks","issue_events_url":"https://api.github.com/repos/codecov/tornado/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/tornado/events","assignees_url":"https://api.github.com/repos/codecov/tornado/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/tornado/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/tornado/tags","blobs_url":"https://api.github.com/repos/codecov/tornado/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/tornado/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/tornado/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/tornado/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/tornado/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/tornado/languages","stargazers_url":"https://api.github.com/repos/codecov/tornado/stargazers","contributors_url":"https://api.github.com/repos/codecov/tornado/contributors","subscribers_url":"https://api.github.com/repos/codecov/tornado/subscribers","subscription_url":"https://api.github.com/repos/codecov/tornado/subscription","commits_url":"https://api.github.com/repos/codecov/tornado/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/tornado/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/tornado/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/tornado/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/tornado/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/tornado/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/tornado/merges","archive_url":"https://api.github.com/repos/codecov/tornado/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/tornado/downloads","issues_url":"https://api.github.com/repos/codecov/tornado/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/tornado/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/tornado/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/tornado/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/tornado/labels{/name}","releases_url":"https://api.github.com/repos/codecov/tornado/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/tornado/deployments","created_at":"2015-08-15T20:55:12Z","updated_at":"2018-12-19T21:13:24Z","pushed_at":"2015-08-18T15:32:18Z","git_url":"git://github.com/codecov/tornado.git","ssh_url":"git@github.com:codecov/tornado.git","clone_url":"https://github.com/codecov/tornado.git","svn_url":"https://github.com/codecov/tornado","homepage":"http://www.tornadoweb.org/","size":9068,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":1,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true}},{"id":46487935,"node_id":"MDEwOlJlcG9zaXRvcnk0NjQ4NzkzNQ==","name":"torngit","full_name":"codecov/torngit","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/torngit","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/torngit","forks_url":"https://api.github.com/repos/codecov/torngit/forks","keys_url":"https://api.github.com/repos/codecov/torngit/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/torngit/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/torngit/teams","hooks_url":"https://api.github.com/repos/codecov/torngit/hooks","issue_events_url":"https://api.github.com/repos/codecov/torngit/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/torngit/events","assignees_url":"https://api.github.com/repos/codecov/torngit/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/torngit/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/torngit/tags","blobs_url":"https://api.github.com/repos/codecov/torngit/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/torngit/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/torngit/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/torngit/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/torngit/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/torngit/languages","stargazers_url":"https://api.github.com/repos/codecov/torngit/stargazers","contributors_url":"https://api.github.com/repos/codecov/torngit/contributors","subscribers_url":"https://api.github.com/repos/codecov/torngit/subscribers","subscription_url":"https://api.github.com/repos/codecov/torngit/subscription","commits_url":"https://api.github.com/repos/codecov/torngit/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/torngit/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/torngit/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/torngit/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/torngit/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/torngit/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/torngit/merges","archive_url":"https://api.github.com/repos/codecov/torngit/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/torngit/downloads","issues_url":"https://api.github.com/repos/codecov/torngit/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/torngit/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/torngit/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/torngit/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/torngit/labels{/name}","releases_url":"https://api.github.com/repos/codecov/torngit/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/torngit/deployments","created_at":"2015-11-19T11:26:42Z","updated_at":"2020-05-15T13:21:19Z","pushed_at":"2020-05-15T13:21:17Z","git_url":"git://github.com/codecov/torngit.git","ssh_url":"git@github.com:codecov/torngit.git","clone_url":"https://github.com/codecov/torngit.git","svn_url":"https://github.com/codecov/torngit","homepage":null,"size":907,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"forks":0,"open_issues":1,"watchers":0,"default_branch":"main","permissions":{"admin":false,"push":true,"pull":true}},{"id":210706185,"node_id":"MDEwOlJlcG9zaXRvcnkyMTA3MDYxODU=","name":"tornpsql","full_name":"codecov/tornpsql","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/tornpsql","description":"Simple + PostgreSQL wrapper","fork":true,"url":"https://api.github.com/repos/codecov/tornpsql","forks_url":"https://api.github.com/repos/codecov/tornpsql/forks","keys_url":"https://api.github.com/repos/codecov/tornpsql/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/tornpsql/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/tornpsql/teams","hooks_url":"https://api.github.com/repos/codecov/tornpsql/hooks","issue_events_url":"https://api.github.com/repos/codecov/tornpsql/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/tornpsql/events","assignees_url":"https://api.github.com/repos/codecov/tornpsql/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/tornpsql/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/tornpsql/tags","blobs_url":"https://api.github.com/repos/codecov/tornpsql/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/tornpsql/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/tornpsql/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/tornpsql/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/tornpsql/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/tornpsql/languages","stargazers_url":"https://api.github.com/repos/codecov/tornpsql/stargazers","contributors_url":"https://api.github.com/repos/codecov/tornpsql/contributors","subscribers_url":"https://api.github.com/repos/codecov/tornpsql/subscribers","subscription_url":"https://api.github.com/repos/codecov/tornpsql/subscription","commits_url":"https://api.github.com/repos/codecov/tornpsql/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/tornpsql/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/tornpsql/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/tornpsql/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/tornpsql/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/tornpsql/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/tornpsql/merges","archive_url":"https://api.github.com/repos/codecov/tornpsql/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/tornpsql/downloads","issues_url":"https://api.github.com/repos/codecov/tornpsql/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/tornpsql/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/tornpsql/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/tornpsql/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/tornpsql/labels{/name}","releases_url":"https://api.github.com/repos/codecov/tornpsql/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/tornpsql/deployments","created_at":"2019-09-24T22:10:47Z","updated_at":"2019-10-03T03:55:57Z","pushed_at":"2020-04-16T15:01:42Z","git_url":"git://github.com/codecov/tornpsql.git","ssh_url":"git@github.com:codecov/tornpsql.git","clone_url":"https://github.com/codecov/tornpsql.git","svn_url":"https://github.com/codecov/tornpsql","homepage":"","size":177,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":1,"open_issues":1,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true}},{"id":194693245,"node_id":"MDEwOlJlcG9zaXRvcnkxOTQ2OTMyNDU=","name":"tornwrap","full_name":"codecov/tornwrap","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/tornwrap","description":"awesome + tornado plugins and decorators","fork":true,"url":"https://api.github.com/repos/codecov/tornwrap","forks_url":"https://api.github.com/repos/codecov/tornwrap/forks","keys_url":"https://api.github.com/repos/codecov/tornwrap/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/tornwrap/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/tornwrap/teams","hooks_url":"https://api.github.com/repos/codecov/tornwrap/hooks","issue_events_url":"https://api.github.com/repos/codecov/tornwrap/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/tornwrap/events","assignees_url":"https://api.github.com/repos/codecov/tornwrap/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/tornwrap/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/tornwrap/tags","blobs_url":"https://api.github.com/repos/codecov/tornwrap/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/tornwrap/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/tornwrap/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/tornwrap/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/tornwrap/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/tornwrap/languages","stargazers_url":"https://api.github.com/repos/codecov/tornwrap/stargazers","contributors_url":"https://api.github.com/repos/codecov/tornwrap/contributors","subscribers_url":"https://api.github.com/repos/codecov/tornwrap/subscribers","subscription_url":"https://api.github.com/repos/codecov/tornwrap/subscription","commits_url":"https://api.github.com/repos/codecov/tornwrap/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/tornwrap/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/tornwrap/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/tornwrap/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/tornwrap/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/tornwrap/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/tornwrap/merges","archive_url":"https://api.github.com/repos/codecov/tornwrap/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/tornwrap/downloads","issues_url":"https://api.github.com/repos/codecov/tornwrap/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/tornwrap/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/tornwrap/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/tornwrap/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/tornwrap/labels{/name}","releases_url":"https://api.github.com/repos/codecov/tornwrap/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/tornwrap/deployments","created_at":"2019-07-01T14:53:10Z","updated_at":"2020-09-03T15:57:22Z","pushed_at":"2020-09-03T15:57:20Z","git_url":"git://github.com/codecov/tornwrap.git","ssh_url":"git@github.com:codecov/tornwrap.git","clone_url":"https://github.com/codecov/tornwrap.git","svn_url":"https://github.com/codecov/tornwrap","homepage":"","size":140,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":1,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"push":true,"pull":true}},{"id":198353745,"node_id":"MDEwOlJlcG9zaXRvcnkxOTgzNTM3NDU=","name":"typescript-standard","full_name":"codecov/typescript-standard","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/typescript-standard","description":"Codecov + coverage standard for TypeScript","fork":false,"url":"https://api.github.com/repos/codecov/typescript-standard","forks_url":"https://api.github.com/repos/codecov/typescript-standard/forks","keys_url":"https://api.github.com/repos/codecov/typescript-standard/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/typescript-standard/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/typescript-standard/teams","hooks_url":"https://api.github.com/repos/codecov/typescript-standard/hooks","issue_events_url":"https://api.github.com/repos/codecov/typescript-standard/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/typescript-standard/events","assignees_url":"https://api.github.com/repos/codecov/typescript-standard/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/typescript-standard/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/typescript-standard/tags","blobs_url":"https://api.github.com/repos/codecov/typescript-standard/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/typescript-standard/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/typescript-standard/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/typescript-standard/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/typescript-standard/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/typescript-standard/languages","stargazers_url":"https://api.github.com/repos/codecov/typescript-standard/stargazers","contributors_url":"https://api.github.com/repos/codecov/typescript-standard/contributors","subscribers_url":"https://api.github.com/repos/codecov/typescript-standard/subscribers","subscription_url":"https://api.github.com/repos/codecov/typescript-standard/subscription","commits_url":"https://api.github.com/repos/codecov/typescript-standard/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/typescript-standard/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/typescript-standard/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/typescript-standard/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/typescript-standard/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/typescript-standard/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/typescript-standard/merges","archive_url":"https://api.github.com/repos/codecov/typescript-standard/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/typescript-standard/downloads","issues_url":"https://api.github.com/repos/codecov/typescript-standard/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/typescript-standard/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/typescript-standard/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/typescript-standard/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/typescript-standard/labels{/name}","releases_url":"https://api.github.com/repos/codecov/typescript-standard/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/typescript-standard/deployments","created_at":"2019-07-23T04:47:07Z","updated_at":"2020-10-13T20:05:45Z","pushed_at":"2020-10-13T20:05:42Z","git_url":"git://github.com/codecov/typescript-standard.git","ssh_url":"git@github.com:codecov/typescript-standard.git","clone_url":"https://github.com/codecov/typescript-standard.git","svn_url":"https://github.com/codecov/typescript-standard","homepage":"","size":9056,"stargazers_count":3,"watchers_count":3,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":6,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"forks":6,"open_issues":0,"watchers":3,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true}},{"id":223286392,"node_id":"MDEwOlJlcG9zaXRvcnkyMjMyODYzOTI=","name":"uploader","full_name":"codecov/uploader","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/uploader","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/uploader","forks_url":"https://api.github.com/repos/codecov/uploader/forks","keys_url":"https://api.github.com/repos/codecov/uploader/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/uploader/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/uploader/teams","hooks_url":"https://api.github.com/repos/codecov/uploader/hooks","issue_events_url":"https://api.github.com/repos/codecov/uploader/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/uploader/events","assignees_url":"https://api.github.com/repos/codecov/uploader/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/uploader/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/uploader/tags","blobs_url":"https://api.github.com/repos/codecov/uploader/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/uploader/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/uploader/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/uploader/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/uploader/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/uploader/languages","stargazers_url":"https://api.github.com/repos/codecov/uploader/stargazers","contributors_url":"https://api.github.com/repos/codecov/uploader/contributors","subscribers_url":"https://api.github.com/repos/codecov/uploader/subscribers","subscription_url":"https://api.github.com/repos/codecov/uploader/subscription","commits_url":"https://api.github.com/repos/codecov/uploader/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/uploader/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/uploader/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/uploader/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/uploader/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/uploader/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/uploader/merges","archive_url":"https://api.github.com/repos/codecov/uploader/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/uploader/downloads","issues_url":"https://api.github.com/repos/codecov/uploader/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/uploader/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/uploader/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/uploader/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/uploader/labels{/name}","releases_url":"https://api.github.com/repos/codecov/uploader/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/uploader/deployments","created_at":"2019-11-21T23:51:13Z","updated_at":"2020-09-14T22:33:58Z","pushed_at":"2020-10-08T12:17:50Z","git_url":"git://github.com/codecov/uploader.git","ssh_url":"git@github.com:codecov/uploader.git","clone_url":"https://github.com/codecov/uploader.git","svn_url":"https://github.com/codecov/uploader","homepage":null,"size":9961,"stargazers_count":2,"watchers_count":2,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":4,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":8,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":4,"open_issues":8,"watchers":2,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true}},{"id":157271496,"node_id":"MDEwOlJlcG9zaXRvcnkxNTcyNzE0OTY=","name":"worker","full_name":"codecov/worker","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/worker","description":"Code + for Background Workers of Codecov","fork":false,"url":"https://api.github.com/repos/codecov/worker","forks_url":"https://api.github.com/repos/codecov/worker/forks","keys_url":"https://api.github.com/repos/codecov/worker/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/worker/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/worker/teams","hooks_url":"https://api.github.com/repos/codecov/worker/hooks","issue_events_url":"https://api.github.com/repos/codecov/worker/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/worker/events","assignees_url":"https://api.github.com/repos/codecov/worker/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/worker/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/worker/tags","blobs_url":"https://api.github.com/repos/codecov/worker/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/worker/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/worker/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/worker/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/worker/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/worker/languages","stargazers_url":"https://api.github.com/repos/codecov/worker/stargazers","contributors_url":"https://api.github.com/repos/codecov/worker/contributors","subscribers_url":"https://api.github.com/repos/codecov/worker/subscribers","subscription_url":"https://api.github.com/repos/codecov/worker/subscription","commits_url":"https://api.github.com/repos/codecov/worker/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/worker/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/worker/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/worker/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/worker/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/worker/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/worker/merges","archive_url":"https://api.github.com/repos/codecov/worker/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/worker/downloads","issues_url":"https://api.github.com/repos/codecov/worker/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/worker/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/worker/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/worker/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/worker/labels{/name}","releases_url":"https://api.github.com/repos/codecov/worker/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/worker/deployments","created_at":"2018-11-12T20:20:58Z","updated_at":"2020-10-14T16:23:50Z","pushed_at":"2020-10-14T16:23:47Z","git_url":"git://github.com/codecov/worker.git","ssh_url":"git@github.com:codecov/worker.git","clone_url":"https://github.com/codecov/worker.git","svn_url":"https://github.com/codecov/worker","homepage":null,"size":2524,"stargazers_count":1,"watchers_count":1,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":6,"license":null,"forks":1,"open_issues":6,"watchers":1,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true}},{"id":178031824,"node_id":"MDEwOlJlcG9zaXRvcnkxNzgwMzE4MjQ=","name":"bitbucket-testing","full_name":"ThiagoCodecov/bitbucket-testing","private":true,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/bitbucket-testing","description":null,"fork":false,"url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing","forks_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/bitbucket-testing/deployments","created_at":"2019-03-27T16:17:34Z","updated_at":"2019-03-27T16:17:34Z","pushed_at":"2019-03-27T16:17:35Z","git_url":"git://github.com/ThiagoCodecov/bitbucket-testing.git","ssh_url":"git@github.com:ThiagoCodecov/bitbucket-testing.git","clone_url":"https://github.com/ThiagoCodecov/bitbucket-testing.git","svn_url":"https://github.com/ThiagoCodecov/bitbucket-testing","homepage":null,"size":0,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true}},{"id":175435584,"node_id":"MDEwOlJlcG9zaXRvcnkxNzU0MzU1ODQ=","name":"codecov-bash","full_name":"ThiagoCodecov/codecov-bash","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/codecov-bash","description":"Global + coverage report uploader for Codecov","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash","forks_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/deployments","created_at":"2019-03-13T14:19:46Z","updated_at":"2019-03-13T14:19:48Z","pushed_at":"2019-03-03T10:08:00Z","git_url":"git://github.com/ThiagoCodecov/codecov-bash.git","ssh_url":"git@github.com:ThiagoCodecov/codecov-bash.git","clone_url":"https://github.com/ThiagoCodecov/codecov-bash.git","svn_url":"https://github.com/ThiagoCodecov/codecov-bash","homepage":"https://codecov.io","size":605,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true}},{"id":198066359,"node_id":"MDEwOlJlcG9zaXRvcnkxOTgwNjYzNTk=","name":"django-vue-dockerized","full_name":"ThiagoCodecov/django-vue-dockerized","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/django-vue-dockerized","description":null,"fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized","forks_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/deployments","created_at":"2019-07-21T14:26:52Z","updated_at":"2019-07-22T12:56:20Z","pushed_at":"2019-07-21T16:50:23Z","git_url":"git://github.com/ThiagoCodecov/django-vue-dockerized.git","ssh_url":"git@github.com:ThiagoCodecov/django-vue-dockerized.git","clone_url":"https://github.com/ThiagoCodecov/django-vue-dockerized.git","svn_url":"https://github.com/ThiagoCodecov/django-vue-dockerized","homepage":null,"size":226,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true}},{"id":204042091,"node_id":"MDEwOlJlcG9zaXRvcnkyMDQwNDIwOTE=","name":"django-vue-dockerized-celery","full_name":"ThiagoCodecov/django-vue-dockerized-celery","private":true,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/django-vue-dockerized-celery","description":null,"fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery","forks_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/deployments","created_at":"2019-08-23T17:27:15Z","updated_at":"2019-08-23T17:27:17Z","pushed_at":"2019-07-22T17:32:56Z","git_url":"git://github.com/ThiagoCodecov/django-vue-dockerized-celery.git","ssh_url":"git@github.com:ThiagoCodecov/django-vue-dockerized-celery.git","clone_url":"https://github.com/ThiagoCodecov/django-vue-dockerized-celery.git","svn_url":"https://github.com/ThiagoCodecov/django-vue-dockerized-celery","homepage":null,"size":219,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true}},{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-03-24T22:01:40Z","pushed_at":"2020-10-13T15:15:47Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":174,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":null,"forks":0,"open_issues":2,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true}},{"id":254717319,"node_id":"MDEwOlJlcG9zaXRvcnkyNTQ3MTczMTk=","name":"shared","full_name":"ThiagoCodecov/shared","private":true,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/shared","description":null,"fork":false,"url":"https://api.github.com/repos/ThiagoCodecov/shared","forks_url":"https://api.github.com/repos/ThiagoCodecov/shared/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/shared/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/shared/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/shared/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/shared/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/shared/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/shared/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/shared/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/shared/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/shared/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/shared/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/shared/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/shared/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/shared/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/shared/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/shared/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/shared/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/shared/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/shared/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/shared/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/shared/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/shared/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/shared/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/shared/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/shared/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/shared/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/shared/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/shared/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/shared/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/shared/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/shared/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/shared/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/shared/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/shared/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/shared/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/shared/deployments","created_at":"2020-04-10T19:20:45Z","updated_at":"2020-04-10T19:21:49Z","pushed_at":"2020-04-15T14:06:42Z","git_url":"git://github.com/ThiagoCodecov/shared.git","ssh_url":"git@github.com:ThiagoCodecov/shared.git","clone_url":"https://github.com/ThiagoCodecov/shared.git","svn_url":"https://github.com/ThiagoCodecov/shared","homepage":null,"size":1111,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true}},{"id":204233902,"node_id":"MDEwOlJlcG9zaXRvcnkyMDQyMzM5MDI=","name":"umbrella","full_name":"ThiagoCodecov/umbrella","private":true,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/umbrella","description":"My + small test on getting a different setup","fork":false,"url":"https://api.github.com/repos/ThiagoCodecov/umbrella","forks_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/umbrella/deployments","created_at":"2019-08-25T02:00:17Z","updated_at":"2019-09-07T02:58:45Z","pushed_at":"2019-09-07T02:58:43Z","git_url":"git://github.com/ThiagoCodecov/umbrella.git","ssh_url":"git@github.com:ThiagoCodecov/umbrella.git","clone_url":"https://github.com/ThiagoCodecov/umbrella.git","svn_url":"https://github.com/ThiagoCodecov/umbrella","homepage":null,"size":6,"stargazers_count":0,"watchers_count":0,"language":"Makefile","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true}}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:39 GMT + ETag: + - W/"bebab2b75c77d4e354b0ed17884754cc551b6fa2778b8c6308391d89acf36871" + Link: + - ; rel="prev", ; + rel="first" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFC:1EA2:358A7A5:5DDA5AB:5F873637 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4920' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '80' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/tornado + response: + content: '{"id":40784552,"node_id":"MDEwOlJlcG9zaXRvcnk0MDc4NDU1Mg==","name":"tornado","full_name":"codecov/tornado","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/tornado","description":"Tornado + is a Python web framework and asynchronous networking library, originally developed + at FriendFeed.","fork":true,"url":"https://api.github.com/repos/codecov/tornado","forks_url":"https://api.github.com/repos/codecov/tornado/forks","keys_url":"https://api.github.com/repos/codecov/tornado/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/tornado/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/tornado/teams","hooks_url":"https://api.github.com/repos/codecov/tornado/hooks","issue_events_url":"https://api.github.com/repos/codecov/tornado/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/tornado/events","assignees_url":"https://api.github.com/repos/codecov/tornado/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/tornado/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/tornado/tags","blobs_url":"https://api.github.com/repos/codecov/tornado/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/tornado/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/tornado/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/tornado/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/tornado/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/tornado/languages","stargazers_url":"https://api.github.com/repos/codecov/tornado/stargazers","contributors_url":"https://api.github.com/repos/codecov/tornado/contributors","subscribers_url":"https://api.github.com/repos/codecov/tornado/subscribers","subscription_url":"https://api.github.com/repos/codecov/tornado/subscription","commits_url":"https://api.github.com/repos/codecov/tornado/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/tornado/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/tornado/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/tornado/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/tornado/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/tornado/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/tornado/merges","archive_url":"https://api.github.com/repos/codecov/tornado/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/tornado/downloads","issues_url":"https://api.github.com/repos/codecov/tornado/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/tornado/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/tornado/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/tornado/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/tornado/labels{/name}","releases_url":"https://api.github.com/repos/codecov/tornado/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/tornado/deployments","created_at":"2015-08-15T20:55:12Z","updated_at":"2018-12-19T21:13:24Z","pushed_at":"2015-08-18T15:32:18Z","git_url":"git://github.com/codecov/tornado.git","ssh_url":"git@github.com:codecov/tornado.git","clone_url":"https://github.com/codecov/tornado.git","svn_url":"https://github.com/codecov/tornado","homepage":"http://www.tornadoweb.org/","size":9068,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":1,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true},"temp_clone_token":"","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"delete_branch_on_merge":false,"organization":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"parent":{"id":301742,"node_id":"MDEwOlJlcG9zaXRvcnkzMDE3NDI=","name":"tornado","full_name":"tornadoweb/tornado","private":false,"owner":{"login":"tornadoweb","id":7468980,"node_id":"MDEyOk9yZ2FuaXphdGlvbjc0Njg5ODA=","avatar_url":"https://avatars3.githubusercontent.com/u/7468980?v=4","gravatar_id":"","url":"https://api.github.com/users/tornadoweb","html_url":"https://github.com/tornadoweb","followers_url":"https://api.github.com/users/tornadoweb/followers","following_url":"https://api.github.com/users/tornadoweb/following{/other_user}","gists_url":"https://api.github.com/users/tornadoweb/gists{/gist_id}","starred_url":"https://api.github.com/users/tornadoweb/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tornadoweb/subscriptions","organizations_url":"https://api.github.com/users/tornadoweb/orgs","repos_url":"https://api.github.com/users/tornadoweb/repos","events_url":"https://api.github.com/users/tornadoweb/events{/privacy}","received_events_url":"https://api.github.com/users/tornadoweb/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/tornadoweb/tornado","description":"Tornado + is a Python web framework and asynchronous networking library, originally developed + at FriendFeed.","fork":false,"url":"https://api.github.com/repos/tornadoweb/tornado","forks_url":"https://api.github.com/repos/tornadoweb/tornado/forks","keys_url":"https://api.github.com/repos/tornadoweb/tornado/keys{/key_id}","collaborators_url":"https://api.github.com/repos/tornadoweb/tornado/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/tornadoweb/tornado/teams","hooks_url":"https://api.github.com/repos/tornadoweb/tornado/hooks","issue_events_url":"https://api.github.com/repos/tornadoweb/tornado/issues/events{/number}","events_url":"https://api.github.com/repos/tornadoweb/tornado/events","assignees_url":"https://api.github.com/repos/tornadoweb/tornado/assignees{/user}","branches_url":"https://api.github.com/repos/tornadoweb/tornado/branches{/branch}","tags_url":"https://api.github.com/repos/tornadoweb/tornado/tags","blobs_url":"https://api.github.com/repos/tornadoweb/tornado/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/tornadoweb/tornado/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/tornadoweb/tornado/git/refs{/sha}","trees_url":"https://api.github.com/repos/tornadoweb/tornado/git/trees{/sha}","statuses_url":"https://api.github.com/repos/tornadoweb/tornado/statuses/{sha}","languages_url":"https://api.github.com/repos/tornadoweb/tornado/languages","stargazers_url":"https://api.github.com/repos/tornadoweb/tornado/stargazers","contributors_url":"https://api.github.com/repos/tornadoweb/tornado/contributors","subscribers_url":"https://api.github.com/repos/tornadoweb/tornado/subscribers","subscription_url":"https://api.github.com/repos/tornadoweb/tornado/subscription","commits_url":"https://api.github.com/repos/tornadoweb/tornado/commits{/sha}","git_commits_url":"https://api.github.com/repos/tornadoweb/tornado/git/commits{/sha}","comments_url":"https://api.github.com/repos/tornadoweb/tornado/comments{/number}","issue_comment_url":"https://api.github.com/repos/tornadoweb/tornado/issues/comments{/number}","contents_url":"https://api.github.com/repos/tornadoweb/tornado/contents/{+path}","compare_url":"https://api.github.com/repos/tornadoweb/tornado/compare/{base}...{head}","merges_url":"https://api.github.com/repos/tornadoweb/tornado/merges","archive_url":"https://api.github.com/repos/tornadoweb/tornado/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/tornadoweb/tornado/downloads","issues_url":"https://api.github.com/repos/tornadoweb/tornado/issues{/number}","pulls_url":"https://api.github.com/repos/tornadoweb/tornado/pulls{/number}","milestones_url":"https://api.github.com/repos/tornadoweb/tornado/milestones{/number}","notifications_url":"https://api.github.com/repos/tornadoweb/tornado/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/tornadoweb/tornado/labels{/name}","releases_url":"https://api.github.com/repos/tornadoweb/tornado/releases{/id}","deployments_url":"https://api.github.com/repos/tornadoweb/tornado/deployments","created_at":"2009-09-09T04:55:16Z","updated_at":"2020-10-14T12:14:08Z","pushed_at":"2020-10-13T23:48:33Z","git_url":"git://github.com/tornadoweb/tornado.git","ssh_url":"git@github.com:tornadoweb/tornado.git","clone_url":"https://github.com/tornadoweb/tornado.git","svn_url":"https://github.com/tornadoweb/tornado","homepage":"http://www.tornadoweb.org/","size":8875,"stargazers_count":19481,"watchers_count":19481,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":5253,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":204,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":5253,"open_issues":204,"watchers":19481,"default_branch":"main"},"source":{"id":301742,"node_id":"MDEwOlJlcG9zaXRvcnkzMDE3NDI=","name":"tornado","full_name":"tornadoweb/tornado","private":false,"owner":{"login":"tornadoweb","id":7468980,"node_id":"MDEyOk9yZ2FuaXphdGlvbjc0Njg5ODA=","avatar_url":"https://avatars3.githubusercontent.com/u/7468980?v=4","gravatar_id":"","url":"https://api.github.com/users/tornadoweb","html_url":"https://github.com/tornadoweb","followers_url":"https://api.github.com/users/tornadoweb/followers","following_url":"https://api.github.com/users/tornadoweb/following{/other_user}","gists_url":"https://api.github.com/users/tornadoweb/gists{/gist_id}","starred_url":"https://api.github.com/users/tornadoweb/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tornadoweb/subscriptions","organizations_url":"https://api.github.com/users/tornadoweb/orgs","repos_url":"https://api.github.com/users/tornadoweb/repos","events_url":"https://api.github.com/users/tornadoweb/events{/privacy}","received_events_url":"https://api.github.com/users/tornadoweb/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/tornadoweb/tornado","description":"Tornado + is a Python web framework and asynchronous networking library, originally developed + at FriendFeed.","fork":false,"url":"https://api.github.com/repos/tornadoweb/tornado","forks_url":"https://api.github.com/repos/tornadoweb/tornado/forks","keys_url":"https://api.github.com/repos/tornadoweb/tornado/keys{/key_id}","collaborators_url":"https://api.github.com/repos/tornadoweb/tornado/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/tornadoweb/tornado/teams","hooks_url":"https://api.github.com/repos/tornadoweb/tornado/hooks","issue_events_url":"https://api.github.com/repos/tornadoweb/tornado/issues/events{/number}","events_url":"https://api.github.com/repos/tornadoweb/tornado/events","assignees_url":"https://api.github.com/repos/tornadoweb/tornado/assignees{/user}","branches_url":"https://api.github.com/repos/tornadoweb/tornado/branches{/branch}","tags_url":"https://api.github.com/repos/tornadoweb/tornado/tags","blobs_url":"https://api.github.com/repos/tornadoweb/tornado/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/tornadoweb/tornado/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/tornadoweb/tornado/git/refs{/sha}","trees_url":"https://api.github.com/repos/tornadoweb/tornado/git/trees{/sha}","statuses_url":"https://api.github.com/repos/tornadoweb/tornado/statuses/{sha}","languages_url":"https://api.github.com/repos/tornadoweb/tornado/languages","stargazers_url":"https://api.github.com/repos/tornadoweb/tornado/stargazers","contributors_url":"https://api.github.com/repos/tornadoweb/tornado/contributors","subscribers_url":"https://api.github.com/repos/tornadoweb/tornado/subscribers","subscription_url":"https://api.github.com/repos/tornadoweb/tornado/subscription","commits_url":"https://api.github.com/repos/tornadoweb/tornado/commits{/sha}","git_commits_url":"https://api.github.com/repos/tornadoweb/tornado/git/commits{/sha}","comments_url":"https://api.github.com/repos/tornadoweb/tornado/comments{/number}","issue_comment_url":"https://api.github.com/repos/tornadoweb/tornado/issues/comments{/number}","contents_url":"https://api.github.com/repos/tornadoweb/tornado/contents/{+path}","compare_url":"https://api.github.com/repos/tornadoweb/tornado/compare/{base}...{head}","merges_url":"https://api.github.com/repos/tornadoweb/tornado/merges","archive_url":"https://api.github.com/repos/tornadoweb/tornado/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/tornadoweb/tornado/downloads","issues_url":"https://api.github.com/repos/tornadoweb/tornado/issues{/number}","pulls_url":"https://api.github.com/repos/tornadoweb/tornado/pulls{/number}","milestones_url":"https://api.github.com/repos/tornadoweb/tornado/milestones{/number}","notifications_url":"https://api.github.com/repos/tornadoweb/tornado/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/tornadoweb/tornado/labels{/name}","releases_url":"https://api.github.com/repos/tornadoweb/tornado/releases{/id}","deployments_url":"https://api.github.com/repos/tornadoweb/tornado/deployments","created_at":"2009-09-09T04:55:16Z","updated_at":"2020-10-14T12:14:08Z","pushed_at":"2020-10-13T23:48:33Z","git_url":"git://github.com/tornadoweb/tornado.git","ssh_url":"git@github.com:tornadoweb/tornado.git","clone_url":"https://github.com/tornadoweb/tornado.git","svn_url":"https://github.com/tornadoweb/tornado","homepage":"http://www.tornadoweb.org/","size":8875,"stargazers_count":19481,"watchers_count":19481,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":5253,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":204,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":5253,"open_issues":204,"watchers":19481,"default_branch":"main"},"network_count":5253,"subscribers_count":2}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:40 GMT + ETag: + - W/"de95e5ba33a0db6b3150b177ff1256f065d6b500bf502c8963c8de6664e6e493" + Last-Modified: + - Wed, 19 Dec 2018 21:13:24 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFC:1EA2:358A7EB:5DDA60E:5F873637 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4919' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '81' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/tornpsql + response: + content: '{"id":210706185,"node_id":"MDEwOlJlcG9zaXRvcnkyMTA3MDYxODU=","name":"tornpsql","full_name":"codecov/tornpsql","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/tornpsql","description":"Simple + PostgreSQL wrapper","fork":true,"url":"https://api.github.com/repos/codecov/tornpsql","forks_url":"https://api.github.com/repos/codecov/tornpsql/forks","keys_url":"https://api.github.com/repos/codecov/tornpsql/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/tornpsql/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/tornpsql/teams","hooks_url":"https://api.github.com/repos/codecov/tornpsql/hooks","issue_events_url":"https://api.github.com/repos/codecov/tornpsql/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/tornpsql/events","assignees_url":"https://api.github.com/repos/codecov/tornpsql/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/tornpsql/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/tornpsql/tags","blobs_url":"https://api.github.com/repos/codecov/tornpsql/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/tornpsql/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/tornpsql/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/tornpsql/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/tornpsql/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/tornpsql/languages","stargazers_url":"https://api.github.com/repos/codecov/tornpsql/stargazers","contributors_url":"https://api.github.com/repos/codecov/tornpsql/contributors","subscribers_url":"https://api.github.com/repos/codecov/tornpsql/subscribers","subscription_url":"https://api.github.com/repos/codecov/tornpsql/subscription","commits_url":"https://api.github.com/repos/codecov/tornpsql/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/tornpsql/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/tornpsql/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/tornpsql/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/tornpsql/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/tornpsql/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/tornpsql/merges","archive_url":"https://api.github.com/repos/codecov/tornpsql/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/tornpsql/downloads","issues_url":"https://api.github.com/repos/codecov/tornpsql/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/tornpsql/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/tornpsql/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/tornpsql/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/tornpsql/labels{/name}","releases_url":"https://api.github.com/repos/codecov/tornpsql/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/tornpsql/deployments","created_at":"2019-09-24T22:10:47Z","updated_at":"2019-10-03T03:55:57Z","pushed_at":"2020-04-16T15:01:42Z","git_url":"git://github.com/codecov/tornpsql.git","ssh_url":"git@github.com:codecov/tornpsql.git","clone_url":"https://github.com/codecov/tornpsql.git","svn_url":"https://github.com/codecov/tornpsql","homepage":"","size":177,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":1,"open_issues":1,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true},"temp_clone_token":"","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"delete_branch_on_merge":false,"organization":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"parent":{"id":10322600,"node_id":"MDEwOlJlcG9zaXRvcnkxMDMyMjYwMA==","name":"tornpsql","full_name":"stevepeak/tornpsql","private":false,"owner":{"login":"stevepeak","id":2041757,"node_id":"MDQ6VXNlcjIwNDE3NTc=","avatar_url":"https://avatars1.githubusercontent.com/u/2041757?v=4","gravatar_id":"","url":"https://api.github.com/users/stevepeak","html_url":"https://github.com/stevepeak","followers_url":"https://api.github.com/users/stevepeak/followers","following_url":"https://api.github.com/users/stevepeak/following{/other_user}","gists_url":"https://api.github.com/users/stevepeak/gists{/gist_id}","starred_url":"https://api.github.com/users/stevepeak/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stevepeak/subscriptions","organizations_url":"https://api.github.com/users/stevepeak/orgs","repos_url":"https://api.github.com/users/stevepeak/repos","events_url":"https://api.github.com/users/stevepeak/events{/privacy}","received_events_url":"https://api.github.com/users/stevepeak/received_events","type":"User","site_admin":false},"html_url":"https://github.com/stevepeak/tornpsql","description":"Simple + PostgreSQL wrapper","fork":false,"url":"https://api.github.com/repos/stevepeak/tornpsql","forks_url":"https://api.github.com/repos/stevepeak/tornpsql/forks","keys_url":"https://api.github.com/repos/stevepeak/tornpsql/keys{/key_id}","collaborators_url":"https://api.github.com/repos/stevepeak/tornpsql/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/stevepeak/tornpsql/teams","hooks_url":"https://api.github.com/repos/stevepeak/tornpsql/hooks","issue_events_url":"https://api.github.com/repos/stevepeak/tornpsql/issues/events{/number}","events_url":"https://api.github.com/repos/stevepeak/tornpsql/events","assignees_url":"https://api.github.com/repos/stevepeak/tornpsql/assignees{/user}","branches_url":"https://api.github.com/repos/stevepeak/tornpsql/branches{/branch}","tags_url":"https://api.github.com/repos/stevepeak/tornpsql/tags","blobs_url":"https://api.github.com/repos/stevepeak/tornpsql/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/stevepeak/tornpsql/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/stevepeak/tornpsql/git/refs{/sha}","trees_url":"https://api.github.com/repos/stevepeak/tornpsql/git/trees{/sha}","statuses_url":"https://api.github.com/repos/stevepeak/tornpsql/statuses/{sha}","languages_url":"https://api.github.com/repos/stevepeak/tornpsql/languages","stargazers_url":"https://api.github.com/repos/stevepeak/tornpsql/stargazers","contributors_url":"https://api.github.com/repos/stevepeak/tornpsql/contributors","subscribers_url":"https://api.github.com/repos/stevepeak/tornpsql/subscribers","subscription_url":"https://api.github.com/repos/stevepeak/tornpsql/subscription","commits_url":"https://api.github.com/repos/stevepeak/tornpsql/commits{/sha}","git_commits_url":"https://api.github.com/repos/stevepeak/tornpsql/git/commits{/sha}","comments_url":"https://api.github.com/repos/stevepeak/tornpsql/comments{/number}","issue_comment_url":"https://api.github.com/repos/stevepeak/tornpsql/issues/comments{/number}","contents_url":"https://api.github.com/repos/stevepeak/tornpsql/contents/{+path}","compare_url":"https://api.github.com/repos/stevepeak/tornpsql/compare/{base}...{head}","merges_url":"https://api.github.com/repos/stevepeak/tornpsql/merges","archive_url":"https://api.github.com/repos/stevepeak/tornpsql/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/stevepeak/tornpsql/downloads","issues_url":"https://api.github.com/repos/stevepeak/tornpsql/issues{/number}","pulls_url":"https://api.github.com/repos/stevepeak/tornpsql/pulls{/number}","milestones_url":"https://api.github.com/repos/stevepeak/tornpsql/milestones{/number}","notifications_url":"https://api.github.com/repos/stevepeak/tornpsql/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/stevepeak/tornpsql/labels{/name}","releases_url":"https://api.github.com/repos/stevepeak/tornpsql/releases{/id}","deployments_url":"https://api.github.com/repos/stevepeak/tornpsql/deployments","created_at":"2013-05-27T21:04:47Z","updated_at":"2016-03-23T18:37:34Z","pushed_at":"2020-03-31T20:50:40Z","git_url":"git://github.com/stevepeak/tornpsql.git","ssh_url":"git@github.com:stevepeak/tornpsql.git","clone_url":"https://github.com/stevepeak/tornpsql.git","svn_url":"https://github.com/stevepeak/tornpsql","homepage":"","size":160,"stargazers_count":4,"watchers_count":4,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":7,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":7,"open_issues":1,"watchers":4,"default_branch":"main"},"source":{"id":10322600,"node_id":"MDEwOlJlcG9zaXRvcnkxMDMyMjYwMA==","name":"tornpsql","full_name":"stevepeak/tornpsql","private":false,"owner":{"login":"stevepeak","id":2041757,"node_id":"MDQ6VXNlcjIwNDE3NTc=","avatar_url":"https://avatars1.githubusercontent.com/u/2041757?v=4","gravatar_id":"","url":"https://api.github.com/users/stevepeak","html_url":"https://github.com/stevepeak","followers_url":"https://api.github.com/users/stevepeak/followers","following_url":"https://api.github.com/users/stevepeak/following{/other_user}","gists_url":"https://api.github.com/users/stevepeak/gists{/gist_id}","starred_url":"https://api.github.com/users/stevepeak/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stevepeak/subscriptions","organizations_url":"https://api.github.com/users/stevepeak/orgs","repos_url":"https://api.github.com/users/stevepeak/repos","events_url":"https://api.github.com/users/stevepeak/events{/privacy}","received_events_url":"https://api.github.com/users/stevepeak/received_events","type":"User","site_admin":false},"html_url":"https://github.com/stevepeak/tornpsql","description":"Simple + PostgreSQL wrapper","fork":false,"url":"https://api.github.com/repos/stevepeak/tornpsql","forks_url":"https://api.github.com/repos/stevepeak/tornpsql/forks","keys_url":"https://api.github.com/repos/stevepeak/tornpsql/keys{/key_id}","collaborators_url":"https://api.github.com/repos/stevepeak/tornpsql/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/stevepeak/tornpsql/teams","hooks_url":"https://api.github.com/repos/stevepeak/tornpsql/hooks","issue_events_url":"https://api.github.com/repos/stevepeak/tornpsql/issues/events{/number}","events_url":"https://api.github.com/repos/stevepeak/tornpsql/events","assignees_url":"https://api.github.com/repos/stevepeak/tornpsql/assignees{/user}","branches_url":"https://api.github.com/repos/stevepeak/tornpsql/branches{/branch}","tags_url":"https://api.github.com/repos/stevepeak/tornpsql/tags","blobs_url":"https://api.github.com/repos/stevepeak/tornpsql/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/stevepeak/tornpsql/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/stevepeak/tornpsql/git/refs{/sha}","trees_url":"https://api.github.com/repos/stevepeak/tornpsql/git/trees{/sha}","statuses_url":"https://api.github.com/repos/stevepeak/tornpsql/statuses/{sha}","languages_url":"https://api.github.com/repos/stevepeak/tornpsql/languages","stargazers_url":"https://api.github.com/repos/stevepeak/tornpsql/stargazers","contributors_url":"https://api.github.com/repos/stevepeak/tornpsql/contributors","subscribers_url":"https://api.github.com/repos/stevepeak/tornpsql/subscribers","subscription_url":"https://api.github.com/repos/stevepeak/tornpsql/subscription","commits_url":"https://api.github.com/repos/stevepeak/tornpsql/commits{/sha}","git_commits_url":"https://api.github.com/repos/stevepeak/tornpsql/git/commits{/sha}","comments_url":"https://api.github.com/repos/stevepeak/tornpsql/comments{/number}","issue_comment_url":"https://api.github.com/repos/stevepeak/tornpsql/issues/comments{/number}","contents_url":"https://api.github.com/repos/stevepeak/tornpsql/contents/{+path}","compare_url":"https://api.github.com/repos/stevepeak/tornpsql/compare/{base}...{head}","merges_url":"https://api.github.com/repos/stevepeak/tornpsql/merges","archive_url":"https://api.github.com/repos/stevepeak/tornpsql/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/stevepeak/tornpsql/downloads","issues_url":"https://api.github.com/repos/stevepeak/tornpsql/issues{/number}","pulls_url":"https://api.github.com/repos/stevepeak/tornpsql/pulls{/number}","milestones_url":"https://api.github.com/repos/stevepeak/tornpsql/milestones{/number}","notifications_url":"https://api.github.com/repos/stevepeak/tornpsql/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/stevepeak/tornpsql/labels{/name}","releases_url":"https://api.github.com/repos/stevepeak/tornpsql/releases{/id}","deployments_url":"https://api.github.com/repos/stevepeak/tornpsql/deployments","created_at":"2013-05-27T21:04:47Z","updated_at":"2016-03-23T18:37:34Z","pushed_at":"2020-03-31T20:50:40Z","git_url":"git://github.com/stevepeak/tornpsql.git","ssh_url":"git@github.com:stevepeak/tornpsql.git","clone_url":"https://github.com/stevepeak/tornpsql.git","svn_url":"https://github.com/stevepeak/tornpsql","homepage":"","size":160,"stargazers_count":4,"watchers_count":4,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":7,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":7,"open_issues":1,"watchers":4,"default_branch":"main"},"network_count":7,"subscribers_count":0}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:40 GMT + ETag: + - W/"25ea40aabe4aa594e0fdff2451fc659c01b73227a9292c46082f54aec6abd4f8" + Last-Modified: + - Thu, 03 Oct 2019 03:55:57 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFC:1EA2:358A819:5DDA64F:5F873638 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4918' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '82' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/codecov/tornwrap + response: + content: '{"id":194693245,"node_id":"MDEwOlJlcG9zaXRvcnkxOTQ2OTMyNDU=","name":"tornwrap","full_name":"codecov/tornwrap","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/tornwrap","description":"awesome + tornado plugins and decorators","fork":true,"url":"https://api.github.com/repos/codecov/tornwrap","forks_url":"https://api.github.com/repos/codecov/tornwrap/forks","keys_url":"https://api.github.com/repos/codecov/tornwrap/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/tornwrap/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/tornwrap/teams","hooks_url":"https://api.github.com/repos/codecov/tornwrap/hooks","issue_events_url":"https://api.github.com/repos/codecov/tornwrap/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/tornwrap/events","assignees_url":"https://api.github.com/repos/codecov/tornwrap/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/tornwrap/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/tornwrap/tags","blobs_url":"https://api.github.com/repos/codecov/tornwrap/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/tornwrap/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/tornwrap/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/tornwrap/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/tornwrap/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/tornwrap/languages","stargazers_url":"https://api.github.com/repos/codecov/tornwrap/stargazers","contributors_url":"https://api.github.com/repos/codecov/tornwrap/contributors","subscribers_url":"https://api.github.com/repos/codecov/tornwrap/subscribers","subscription_url":"https://api.github.com/repos/codecov/tornwrap/subscription","commits_url":"https://api.github.com/repos/codecov/tornwrap/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/tornwrap/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/tornwrap/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/tornwrap/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/tornwrap/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/tornwrap/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/tornwrap/merges","archive_url":"https://api.github.com/repos/codecov/tornwrap/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/tornwrap/downloads","issues_url":"https://api.github.com/repos/codecov/tornwrap/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/tornwrap/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/tornwrap/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/tornwrap/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/tornwrap/labels{/name}","releases_url":"https://api.github.com/repos/codecov/tornwrap/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/tornwrap/deployments","created_at":"2019-07-01T14:53:10Z","updated_at":"2020-09-03T15:57:22Z","pushed_at":"2020-09-03T15:57:20Z","git_url":"git://github.com/codecov/tornwrap.git","ssh_url":"git@github.com:codecov/tornwrap.git","clone_url":"https://github.com/codecov/tornwrap.git","svn_url":"https://github.com/codecov/tornwrap","homepage":"","size":140,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":1,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"push":true,"pull":true},"temp_clone_token":"","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"delete_branch_on_merge":false,"organization":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"parent":{"id":23321986,"node_id":"MDEwOlJlcG9zaXRvcnkyMzMyMTk4Ng==","name":"tornwrap","full_name":"stevepeak/tornwrap","private":false,"owner":{"login":"stevepeak","id":2041757,"node_id":"MDQ6VXNlcjIwNDE3NTc=","avatar_url":"https://avatars1.githubusercontent.com/u/2041757?v=4","gravatar_id":"","url":"https://api.github.com/users/stevepeak","html_url":"https://github.com/stevepeak","followers_url":"https://api.github.com/users/stevepeak/followers","following_url":"https://api.github.com/users/stevepeak/following{/other_user}","gists_url":"https://api.github.com/users/stevepeak/gists{/gist_id}","starred_url":"https://api.github.com/users/stevepeak/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stevepeak/subscriptions","organizations_url":"https://api.github.com/users/stevepeak/orgs","repos_url":"https://api.github.com/users/stevepeak/repos","events_url":"https://api.github.com/users/stevepeak/events{/privacy}","received_events_url":"https://api.github.com/users/stevepeak/received_events","type":"User","site_admin":false},"html_url":"https://github.com/stevepeak/tornwrap","description":"awesome + tornado plugins and decorators","fork":false,"url":"https://api.github.com/repos/stevepeak/tornwrap","forks_url":"https://api.github.com/repos/stevepeak/tornwrap/forks","keys_url":"https://api.github.com/repos/stevepeak/tornwrap/keys{/key_id}","collaborators_url":"https://api.github.com/repos/stevepeak/tornwrap/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/stevepeak/tornwrap/teams","hooks_url":"https://api.github.com/repos/stevepeak/tornwrap/hooks","issue_events_url":"https://api.github.com/repos/stevepeak/tornwrap/issues/events{/number}","events_url":"https://api.github.com/repos/stevepeak/tornwrap/events","assignees_url":"https://api.github.com/repos/stevepeak/tornwrap/assignees{/user}","branches_url":"https://api.github.com/repos/stevepeak/tornwrap/branches{/branch}","tags_url":"https://api.github.com/repos/stevepeak/tornwrap/tags","blobs_url":"https://api.github.com/repos/stevepeak/tornwrap/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/stevepeak/tornwrap/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/stevepeak/tornwrap/git/refs{/sha}","trees_url":"https://api.github.com/repos/stevepeak/tornwrap/git/trees{/sha}","statuses_url":"https://api.github.com/repos/stevepeak/tornwrap/statuses/{sha}","languages_url":"https://api.github.com/repos/stevepeak/tornwrap/languages","stargazers_url":"https://api.github.com/repos/stevepeak/tornwrap/stargazers","contributors_url":"https://api.github.com/repos/stevepeak/tornwrap/contributors","subscribers_url":"https://api.github.com/repos/stevepeak/tornwrap/subscribers","subscription_url":"https://api.github.com/repos/stevepeak/tornwrap/subscription","commits_url":"https://api.github.com/repos/stevepeak/tornwrap/commits{/sha}","git_commits_url":"https://api.github.com/repos/stevepeak/tornwrap/git/commits{/sha}","comments_url":"https://api.github.com/repos/stevepeak/tornwrap/comments{/number}","issue_comment_url":"https://api.github.com/repos/stevepeak/tornwrap/issues/comments{/number}","contents_url":"https://api.github.com/repos/stevepeak/tornwrap/contents/{+path}","compare_url":"https://api.github.com/repos/stevepeak/tornwrap/compare/{base}...{head}","merges_url":"https://api.github.com/repos/stevepeak/tornwrap/merges","archive_url":"https://api.github.com/repos/stevepeak/tornwrap/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/stevepeak/tornwrap/downloads","issues_url":"https://api.github.com/repos/stevepeak/tornwrap/issues{/number}","pulls_url":"https://api.github.com/repos/stevepeak/tornwrap/pulls{/number}","milestones_url":"https://api.github.com/repos/stevepeak/tornwrap/milestones{/number}","notifications_url":"https://api.github.com/repos/stevepeak/tornwrap/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/stevepeak/tornwrap/labels{/name}","releases_url":"https://api.github.com/repos/stevepeak/tornwrap/releases{/id}","deployments_url":"https://api.github.com/repos/stevepeak/tornwrap/deployments","created_at":"2014-08-25T17:15:30Z","updated_at":"2019-01-11T11:09:44Z","pushed_at":"2019-07-05T15:51:43Z","git_url":"git://github.com/stevepeak/tornwrap.git","ssh_url":"git@github.com:stevepeak/tornwrap.git","clone_url":"https://github.com/stevepeak/tornwrap.git","svn_url":"https://github.com/stevepeak/tornwrap","homepage":"","size":128,"stargazers_count":3,"watchers_count":3,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":3,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":3,"open_issues":1,"watchers":3,"default_branch":"main"},"source":{"id":23321986,"node_id":"MDEwOlJlcG9zaXRvcnkyMzMyMTk4Ng==","name":"tornwrap","full_name":"stevepeak/tornwrap","private":false,"owner":{"login":"stevepeak","id":2041757,"node_id":"MDQ6VXNlcjIwNDE3NTc=","avatar_url":"https://avatars1.githubusercontent.com/u/2041757?v=4","gravatar_id":"","url":"https://api.github.com/users/stevepeak","html_url":"https://github.com/stevepeak","followers_url":"https://api.github.com/users/stevepeak/followers","following_url":"https://api.github.com/users/stevepeak/following{/other_user}","gists_url":"https://api.github.com/users/stevepeak/gists{/gist_id}","starred_url":"https://api.github.com/users/stevepeak/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stevepeak/subscriptions","organizations_url":"https://api.github.com/users/stevepeak/orgs","repos_url":"https://api.github.com/users/stevepeak/repos","events_url":"https://api.github.com/users/stevepeak/events{/privacy}","received_events_url":"https://api.github.com/users/stevepeak/received_events","type":"User","site_admin":false},"html_url":"https://github.com/stevepeak/tornwrap","description":"awesome + tornado plugins and decorators","fork":false,"url":"https://api.github.com/repos/stevepeak/tornwrap","forks_url":"https://api.github.com/repos/stevepeak/tornwrap/forks","keys_url":"https://api.github.com/repos/stevepeak/tornwrap/keys{/key_id}","collaborators_url":"https://api.github.com/repos/stevepeak/tornwrap/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/stevepeak/tornwrap/teams","hooks_url":"https://api.github.com/repos/stevepeak/tornwrap/hooks","issue_events_url":"https://api.github.com/repos/stevepeak/tornwrap/issues/events{/number}","events_url":"https://api.github.com/repos/stevepeak/tornwrap/events","assignees_url":"https://api.github.com/repos/stevepeak/tornwrap/assignees{/user}","branches_url":"https://api.github.com/repos/stevepeak/tornwrap/branches{/branch}","tags_url":"https://api.github.com/repos/stevepeak/tornwrap/tags","blobs_url":"https://api.github.com/repos/stevepeak/tornwrap/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/stevepeak/tornwrap/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/stevepeak/tornwrap/git/refs{/sha}","trees_url":"https://api.github.com/repos/stevepeak/tornwrap/git/trees{/sha}","statuses_url":"https://api.github.com/repos/stevepeak/tornwrap/statuses/{sha}","languages_url":"https://api.github.com/repos/stevepeak/tornwrap/languages","stargazers_url":"https://api.github.com/repos/stevepeak/tornwrap/stargazers","contributors_url":"https://api.github.com/repos/stevepeak/tornwrap/contributors","subscribers_url":"https://api.github.com/repos/stevepeak/tornwrap/subscribers","subscription_url":"https://api.github.com/repos/stevepeak/tornwrap/subscription","commits_url":"https://api.github.com/repos/stevepeak/tornwrap/commits{/sha}","git_commits_url":"https://api.github.com/repos/stevepeak/tornwrap/git/commits{/sha}","comments_url":"https://api.github.com/repos/stevepeak/tornwrap/comments{/number}","issue_comment_url":"https://api.github.com/repos/stevepeak/tornwrap/issues/comments{/number}","contents_url":"https://api.github.com/repos/stevepeak/tornwrap/contents/{+path}","compare_url":"https://api.github.com/repos/stevepeak/tornwrap/compare/{base}...{head}","merges_url":"https://api.github.com/repos/stevepeak/tornwrap/merges","archive_url":"https://api.github.com/repos/stevepeak/tornwrap/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/stevepeak/tornwrap/downloads","issues_url":"https://api.github.com/repos/stevepeak/tornwrap/issues{/number}","pulls_url":"https://api.github.com/repos/stevepeak/tornwrap/pulls{/number}","milestones_url":"https://api.github.com/repos/stevepeak/tornwrap/milestones{/number}","notifications_url":"https://api.github.com/repos/stevepeak/tornwrap/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/stevepeak/tornwrap/labels{/name}","releases_url":"https://api.github.com/repos/stevepeak/tornwrap/releases{/id}","deployments_url":"https://api.github.com/repos/stevepeak/tornwrap/deployments","created_at":"2014-08-25T17:15:30Z","updated_at":"2019-01-11T11:09:44Z","pushed_at":"2019-07-05T15:51:43Z","git_url":"git://github.com/stevepeak/tornwrap.git","ssh_url":"git@github.com:stevepeak/tornwrap.git","clone_url":"https://github.com/stevepeak/tornwrap.git","svn_url":"https://github.com/stevepeak/tornwrap","homepage":"","size":128,"stargazers_count":3,"watchers_count":3,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":3,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":3,"open_issues":1,"watchers":3,"default_branch":"main"},"network_count":3,"subscribers_count":1}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:40 GMT + ETag: + - W/"1df33fbf9849bfa7adf9f886083b7c799e8b4d4bfb838c76f4eddea1378a6496" + Last-Modified: + - Thu, 03 Sep 2020 15:57:22 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFC:1EA2:358A843:5DDA69B:5F873638 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4917' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '83' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/codecov-bash + response: + content: '{"id":175435584,"node_id":"MDEwOlJlcG9zaXRvcnkxNzU0MzU1ODQ=","name":"codecov-bash","full_name":"ThiagoCodecov/codecov-bash","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/codecov-bash","description":"Global + coverage report uploader for Codecov","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash","forks_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/codecov-bash/deployments","created_at":"2019-03-13T14:19:46Z","updated_at":"2019-03-13T14:19:48Z","pushed_at":"2019-03-03T10:08:00Z","git_url":"git://github.com/ThiagoCodecov/codecov-bash.git","ssh_url":"git@github.com:ThiagoCodecov/codecov-bash.git","clone_url":"https://github.com/ThiagoCodecov/codecov-bash.git","svn_url":"https://github.com/ThiagoCodecov/codecov-bash","homepage":"https://codecov.io","size":605,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true},"temp_clone_token":"","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"delete_branch_on_merge":false,"parent":{"id":34096540,"node_id":"MDEwOlJlcG9zaXRvcnkzNDA5NjU0MA==","name":"codecov-bash","full_name":"codecov/codecov-bash","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-bash","description":"Global + coverage report uploader for Codecov","fork":false,"url":"https://api.github.com/repos/codecov/codecov-bash","forks_url":"https://api.github.com/repos/codecov/codecov-bash/forks","keys_url":"https://api.github.com/repos/codecov/codecov-bash/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-bash/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-bash/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-bash/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-bash/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-bash/events","assignees_url":"https://api.github.com/repos/codecov/codecov-bash/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-bash/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-bash/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-bash/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-bash/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-bash/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-bash/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-bash/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-bash/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-bash/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-bash/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-bash/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-bash/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-bash/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-bash/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-bash/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-bash/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-bash/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-bash/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-bash/merges","archive_url":"https://api.github.com/repos/codecov/codecov-bash/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-bash/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-bash/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-bash/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-bash/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-bash/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-bash/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-bash/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-bash/deployments","created_at":"2015-04-17T04:31:13Z","updated_at":"2020-10-13T13:07:08Z","pushed_at":"2020-10-14T06:32:25Z","git_url":"git://github.com/codecov/codecov-bash.git","ssh_url":"git@github.com:codecov/codecov-bash.git","clone_url":"https://github.com/codecov/codecov-bash.git","svn_url":"https://github.com/codecov/codecov-bash","homepage":"https://codecov.io","size":938,"stargazers_count":210,"watchers_count":210,"language":"Shell","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":162,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":57,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":162,"open_issues":57,"watchers":210,"default_branch":"main"},"source":{"id":34096540,"node_id":"MDEwOlJlcG9zaXRvcnkzNDA5NjU0MA==","name":"codecov-bash","full_name":"codecov/codecov-bash","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-bash","description":"Global + coverage report uploader for Codecov","fork":false,"url":"https://api.github.com/repos/codecov/codecov-bash","forks_url":"https://api.github.com/repos/codecov/codecov-bash/forks","keys_url":"https://api.github.com/repos/codecov/codecov-bash/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-bash/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-bash/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-bash/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-bash/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-bash/events","assignees_url":"https://api.github.com/repos/codecov/codecov-bash/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-bash/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-bash/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-bash/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-bash/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-bash/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-bash/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-bash/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-bash/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-bash/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-bash/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-bash/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-bash/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-bash/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-bash/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-bash/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-bash/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-bash/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-bash/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-bash/merges","archive_url":"https://api.github.com/repos/codecov/codecov-bash/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-bash/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-bash/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-bash/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-bash/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-bash/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-bash/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-bash/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-bash/deployments","created_at":"2015-04-17T04:31:13Z","updated_at":"2020-10-13T13:07:08Z","pushed_at":"2020-10-14T06:32:25Z","git_url":"git://github.com/codecov/codecov-bash.git","ssh_url":"git@github.com:codecov/codecov-bash.git","clone_url":"https://github.com/codecov/codecov-bash.git","svn_url":"https://github.com/codecov/codecov-bash","homepage":"https://codecov.io","size":938,"stargazers_count":210,"watchers_count":210,"language":"Shell","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":162,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":57,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":162,"open_issues":57,"watchers":210,"default_branch":"main"},"network_count":162,"subscribers_count":0}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:41 GMT + ETag: + - W/"ea51c94503f4ee6c60fef553504746242386a3106963db9588f662eb4491dce6" + Last-Modified: + - Wed, 13 Mar 2019 14:19:48 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFC:1EA2:358A866:5DDA6D7:5F873638 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4916' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '84' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized + response: + content: '{"id":198066359,"node_id":"MDEwOlJlcG9zaXRvcnkxOTgwNjYzNTk=","name":"django-vue-dockerized","full_name":"ThiagoCodecov/django-vue-dockerized","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/django-vue-dockerized","description":null,"fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized","forks_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized/deployments","created_at":"2019-07-21T14:26:52Z","updated_at":"2019-07-22T12:56:20Z","pushed_at":"2019-07-21T16:50:23Z","git_url":"git://github.com/ThiagoCodecov/django-vue-dockerized.git","ssh_url":"git@github.com:ThiagoCodecov/django-vue-dockerized.git","clone_url":"https://github.com/ThiagoCodecov/django-vue-dockerized.git","svn_url":"https://github.com/ThiagoCodecov/django-vue-dockerized","homepage":null,"size":226,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true},"temp_clone_token":"","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"delete_branch_on_merge":false,"parent":{"id":192785793,"node_id":"MDEwOlJlcG9zaXRvcnkxOTI3ODU3OTM=","name":"django-vue-dockerized","full_name":"dlucasgordon/django-vue-dockerized","private":false,"owner":{"login":"dlucasgordon","id":50807898,"node_id":"MDQ6VXNlcjUwODA3ODk4","avatar_url":"https://avatars3.githubusercontent.com/u/50807898?v=4","gravatar_id":"","url":"https://api.github.com/users/dlucasgordon","html_url":"https://github.com/dlucasgordon","followers_url":"https://api.github.com/users/dlucasgordon/followers","following_url":"https://api.github.com/users/dlucasgordon/following{/other_user}","gists_url":"https://api.github.com/users/dlucasgordon/gists{/gist_id}","starred_url":"https://api.github.com/users/dlucasgordon/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dlucasgordon/subscriptions","organizations_url":"https://api.github.com/users/dlucasgordon/orgs","repos_url":"https://api.github.com/users/dlucasgordon/repos","events_url":"https://api.github.com/users/dlucasgordon/events{/privacy}","received_events_url":"https://api.github.com/users/dlucasgordon/received_events","type":"User","site_admin":false},"html_url":"https://github.com/dlucasgordon/django-vue-dockerized","description":null,"fork":false,"url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized","forks_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/forks","keys_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/teams","hooks_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/hooks","issue_events_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/issues/events{/number}","events_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/events","assignees_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/assignees{/user}","branches_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/branches{/branch}","tags_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/tags","blobs_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/git/refs{/sha}","trees_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/statuses/{sha}","languages_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/languages","stargazers_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/stargazers","contributors_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/contributors","subscribers_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/subscribers","subscription_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/subscription","commits_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/commits{/sha}","git_commits_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/git/commits{/sha}","comments_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/comments{/number}","issue_comment_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/issues/comments{/number}","contents_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/contents/{+path}","compare_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/merges","archive_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/downloads","issues_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/issues{/number}","pulls_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/pulls{/number}","milestones_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/milestones{/number}","notifications_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/labels{/name}","releases_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/releases{/id}","deployments_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/deployments","created_at":"2019-06-19T18:38:40Z","updated_at":"2019-07-22T12:56:20Z","pushed_at":"2019-06-19T22:30:55Z","git_url":"git://github.com/dlucasgordon/django-vue-dockerized.git","ssh_url":"git@github.com:dlucasgordon/django-vue-dockerized.git","clone_url":"https://github.com/dlucasgordon/django-vue-dockerized.git","svn_url":"https://github.com/dlucasgordon/django-vue-dockerized","homepage":null,"size":220,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"forks":1,"open_issues":0,"watchers":0,"default_branch":"main"},"source":{"id":192785793,"node_id":"MDEwOlJlcG9zaXRvcnkxOTI3ODU3OTM=","name":"django-vue-dockerized","full_name":"dlucasgordon/django-vue-dockerized","private":false,"owner":{"login":"dlucasgordon","id":50807898,"node_id":"MDQ6VXNlcjUwODA3ODk4","avatar_url":"https://avatars3.githubusercontent.com/u/50807898?v=4","gravatar_id":"","url":"https://api.github.com/users/dlucasgordon","html_url":"https://github.com/dlucasgordon","followers_url":"https://api.github.com/users/dlucasgordon/followers","following_url":"https://api.github.com/users/dlucasgordon/following{/other_user}","gists_url":"https://api.github.com/users/dlucasgordon/gists{/gist_id}","starred_url":"https://api.github.com/users/dlucasgordon/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dlucasgordon/subscriptions","organizations_url":"https://api.github.com/users/dlucasgordon/orgs","repos_url":"https://api.github.com/users/dlucasgordon/repos","events_url":"https://api.github.com/users/dlucasgordon/events{/privacy}","received_events_url":"https://api.github.com/users/dlucasgordon/received_events","type":"User","site_admin":false},"html_url":"https://github.com/dlucasgordon/django-vue-dockerized","description":null,"fork":false,"url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized","forks_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/forks","keys_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/teams","hooks_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/hooks","issue_events_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/issues/events{/number}","events_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/events","assignees_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/assignees{/user}","branches_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/branches{/branch}","tags_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/tags","blobs_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/git/refs{/sha}","trees_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/statuses/{sha}","languages_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/languages","stargazers_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/stargazers","contributors_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/contributors","subscribers_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/subscribers","subscription_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/subscription","commits_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/commits{/sha}","git_commits_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/git/commits{/sha}","comments_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/comments{/number}","issue_comment_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/issues/comments{/number}","contents_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/contents/{+path}","compare_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/merges","archive_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/downloads","issues_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/issues{/number}","pulls_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/pulls{/number}","milestones_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/milestones{/number}","notifications_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/labels{/name}","releases_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/releases{/id}","deployments_url":"https://api.github.com/repos/dlucasgordon/django-vue-dockerized/deployments","created_at":"2019-06-19T18:38:40Z","updated_at":"2019-07-22T12:56:20Z","pushed_at":"2019-06-19T22:30:55Z","git_url":"git://github.com/dlucasgordon/django-vue-dockerized.git","ssh_url":"git@github.com:dlucasgordon/django-vue-dockerized.git","clone_url":"https://github.com/dlucasgordon/django-vue-dockerized.git","svn_url":"https://github.com/dlucasgordon/django-vue-dockerized","homepage":null,"size":220,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"forks":1,"open_issues":0,"watchers":0,"default_branch":"main"},"network_count":1,"subscribers_count":0}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:41 GMT + ETag: + - W/"9f5708de96d005e6632eebd4ea7b9f2feee7b5a9998852ffe7081a0c094c4019" + Last-Modified: + - Mon, 22 Jul 2019 12:56:20 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFC:1EA2:358A896:5DDA719:5F873639 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4915' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '85' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery + response: + content: '{"id":204042091,"node_id":"MDEwOlJlcG9zaXRvcnkyMDQwNDIwOTE=","name":"django-vue-dockerized-celery","full_name":"ThiagoCodecov/django-vue-dockerized-celery","private":true,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/django-vue-dockerized-celery","description":null,"fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery","forks_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/django-vue-dockerized-celery/deployments","created_at":"2019-08-23T17:27:15Z","updated_at":"2019-08-23T17:27:17Z","pushed_at":"2019-07-22T17:32:56Z","git_url":"git://github.com/ThiagoCodecov/django-vue-dockerized-celery.git","ssh_url":"git@github.com:ThiagoCodecov/django-vue-dockerized-celery.git","clone_url":"https://github.com/ThiagoCodecov/django-vue-dockerized-celery.git","svn_url":"https://github.com/ThiagoCodecov/django-vue-dockerized-celery","homepage":null,"size":219,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true},"temp_clone_token":"testljr2ywowpawgvlaj1zo39duk6","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"delete_branch_on_merge":false,"organization":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"parent":{"id":198240301,"node_id":"MDEwOlJlcG9zaXRvcnkxOTgyNDAzMDE=","name":"django-vue-dockerized-celery","full_name":"codecov/django-vue-dockerized-celery","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/django-vue-dockerized-celery","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery","forks_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/forks","keys_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/teams","hooks_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/hooks","issue_events_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/events","assignees_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/tags","blobs_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/languages","stargazers_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/stargazers","contributors_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/contributors","subscribers_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/subscribers","subscription_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/subscription","commits_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/merges","archive_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/downloads","issues_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/labels{/name}","releases_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/deployments","created_at":"2019-07-22T14:29:54Z","updated_at":"2019-07-22T17:32:58Z","pushed_at":"2020-07-08T01:10:54Z","git_url":"git://github.com/codecov/django-vue-dockerized-celery.git","ssh_url":"git@github.com:codecov/django-vue-dockerized-celery.git","clone_url":"https://github.com/codecov/django-vue-dockerized-celery.git","svn_url":"https://github.com/codecov/django-vue-dockerized-celery","homepage":null,"size":219,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"forks":1,"open_issues":1,"watchers":0,"default_branch":"main"},"source":{"id":198240301,"node_id":"MDEwOlJlcG9zaXRvcnkxOTgyNDAzMDE=","name":"django-vue-dockerized-celery","full_name":"codecov/django-vue-dockerized-celery","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/django-vue-dockerized-celery","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery","forks_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/forks","keys_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/teams","hooks_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/hooks","issue_events_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/events","assignees_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/tags","blobs_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/languages","stargazers_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/stargazers","contributors_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/contributors","subscribers_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/subscribers","subscription_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/subscription","commits_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/merges","archive_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/downloads","issues_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/labels{/name}","releases_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/deployments","created_at":"2019-07-22T14:29:54Z","updated_at":"2019-07-22T17:32:58Z","pushed_at":"2020-07-08T01:10:54Z","git_url":"git://github.com/codecov/django-vue-dockerized-celery.git","ssh_url":"git@github.com:codecov/django-vue-dockerized-celery.git","clone_url":"https://github.com/codecov/django-vue-dockerized-celery.git","svn_url":"https://github.com/codecov/django-vue-dockerized-celery","homepage":null,"size":219,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"forks":1,"open_issues":1,"watchers":0,"default_branch":"main"},"network_count":1,"subscribers_count":0}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:41 GMT + ETag: + - W/"41b56bfa31313c75cba33b4cd3009a9814ab56ec085abb94d8921873a02c5d36" + Last-Modified: + - Fri, 23 Aug 2019 17:27:17 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFC:1EA2:358A8C0:5DDA759:5F873639 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4914' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '86' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python + response: + content: '{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments","created_at":"2018-11-07T22:40:20Z","updated_at":"2020-03-24T22:01:40Z","pushed_at":"2020-10-13T15:15:47Z","git_url":"git://github.com/ThiagoCodecov/example-python.git","ssh_url":"git@github.com:ThiagoCodecov/example-python.git","clone_url":"https://github.com/ThiagoCodecov/example-python.git","svn_url":"https://github.com/ThiagoCodecov/example-python","homepage":"https://codecov.io","size":174,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":null,"forks":0,"open_issues":2,"watchers":0,"default_branch":"main","permissions":{"admin":true,"push":true,"pull":true},"temp_clone_token":"","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"delete_branch_on_merge":false,"parent":{"id":24344106,"node_id":"MDEwOlJlcG9zaXRvcnkyNDM0NDEwNg==","name":"example-python","full_name":"codecov/example-python","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-python","description":"Python + coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-python","forks_url":"https://api.github.com/repos/codecov/example-python/forks","keys_url":"https://api.github.com/repos/codecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-python/teams","hooks_url":"https://api.github.com/repos/codecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-python/events","assignees_url":"https://api.github.com/repos/codecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-python/tags","blobs_url":"https://api.github.com/repos/codecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-python/languages","stargazers_url":"https://api.github.com/repos/codecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-python/subscription","commits_url":"https://api.github.com/repos/codecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-python/merges","archive_url":"https://api.github.com/repos/codecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-python/downloads","issues_url":"https://api.github.com/repos/codecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-python/deployments","created_at":"2014-09-22T20:20:06Z","updated_at":"2020-09-27T16:45:48Z","pushed_at":"2020-10-13T23:33:12Z","git_url":"git://github.com/codecov/example-python.git","ssh_url":"git@github.com:codecov/example-python.git","clone_url":"https://github.com/codecov/example-python.git","svn_url":"https://github.com/codecov/example-python","homepage":"https://codecov.io","size":83,"stargazers_count":226,"watchers_count":226,"language":"Python","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":189,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":5,"license":null,"forks":189,"open_issues":5,"watchers":226,"default_branch":"main"},"source":{"id":24344106,"node_id":"MDEwOlJlcG9zaXRvcnkyNDM0NDEwNg==","name":"example-python","full_name":"codecov/example-python","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars3.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-python","description":"Python + coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-python","forks_url":"https://api.github.com/repos/codecov/example-python/forks","keys_url":"https://api.github.com/repos/codecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-python/teams","hooks_url":"https://api.github.com/repos/codecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-python/events","assignees_url":"https://api.github.com/repos/codecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-python/tags","blobs_url":"https://api.github.com/repos/codecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-python/languages","stargazers_url":"https://api.github.com/repos/codecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-python/subscription","commits_url":"https://api.github.com/repos/codecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-python/merges","archive_url":"https://api.github.com/repos/codecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-python/downloads","issues_url":"https://api.github.com/repos/codecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-python/deployments","created_at":"2014-09-22T20:20:06Z","updated_at":"2020-09-27T16:45:48Z","pushed_at":"2020-10-13T23:33:12Z","git_url":"git://github.com/codecov/example-python.git","ssh_url":"git@github.com:codecov/example-python.git","clone_url":"https://github.com/codecov/example-python.git","svn_url":"https://github.com/codecov/example-python","homepage":"https://codecov.io","size":83,"stargazers_count":226,"watchers_count":226,"language":"Python","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":189,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":5,"license":null,"forks":189,"open_issues":5,"watchers":226,"default_branch":"main"},"network_count":189,"subscribers_count":0}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:41 GMT + ETag: + - W/"10336721274b17a77895a1bc924223c0cea0145d13cc9eaf2595f17c2de170a8" + Last-Modified: + - Tue, 24 Mar 2020 22:01:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFFC:1EA2:358A8F7:5DDA7A2:5F873639 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4913' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '87' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_repos_generator.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_repos_generator.yaml new file mode 100644 index 0000000000..32a5a3249f --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_repos_generator.yaml @@ -0,0 +1,424 @@ +interactions: +- request: + body: '{"query": "\nquery {\n viewer {\n repositories(\n ownerAffiliations: + [OWNER, COLLABORATOR, ORGANIZATION_MEMBER]\n affiliations: [OWNER, + COLLABORATOR, ORGANIZATION_MEMBER]\n ) {\n totalCount\n }\n }\n}\n"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '264' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/graphql + response: + content: '{"data":{"viewer":{"repositories":{"totalCount":146}}}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 22 Sep 2023 18:36:10 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v4 + X-GitHub-Request-Id: + - F04E:4CD7:66D873:D0CC18:650DDE9A + X-OAuth-Scopes: + - admin:org, repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1695411370' + X-RateLimit-Resource: + - graphql + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-23 22:46:17 UTC + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/user/repos?page=2&per_page=50 + response: + content: '[{"id":139983834,"node_id":"MDEwOlJlcG9zaXRvcnkxMzk5ODM4MzQ=","name":"license","full_name":"codecov/license","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/license","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/license","forks_url":"https://api.github.com/repos/codecov/license/forks","keys_url":"https://api.github.com/repos/codecov/license/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/license/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/license/teams","hooks_url":"https://api.github.com/repos/codecov/license/hooks","issue_events_url":"https://api.github.com/repos/codecov/license/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/license/events","assignees_url":"https://api.github.com/repos/codecov/license/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/license/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/license/tags","blobs_url":"https://api.github.com/repos/codecov/license/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/license/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/license/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/license/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/license/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/license/languages","stargazers_url":"https://api.github.com/repos/codecov/license/stargazers","contributors_url":"https://api.github.com/repos/codecov/license/contributors","subscribers_url":"https://api.github.com/repos/codecov/license/subscribers","subscription_url":"https://api.github.com/repos/codecov/license/subscription","commits_url":"https://api.github.com/repos/codecov/license/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/license/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/license/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/license/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/license/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/license/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/license/merges","archive_url":"https://api.github.com/repos/codecov/license/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/license/downloads","issues_url":"https://api.github.com/repos/codecov/license/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/license/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/license/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/license/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/license/labels{/name}","releases_url":"https://api.github.com/repos/codecov/license/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/license/deployments","created_at":"2018-07-06T12:45:08Z","updated_at":"2021-03-10T20:07:30Z","pushed_at":"2021-03-10T20:24:42Z","git_url":"git://github.com/codecov/license.git","ssh_url":"git@github.com:codecov/license.git","clone_url":"https://github.com/codecov/license.git","svn_url":"https://github.com/codecov/license","homepage":"","size":18,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":347827402,"node_id":"MDEwOlJlcG9zaXRvcnkzNDc4Mjc0MDI=","name":"licenseapp","full_name":"codecov/licenseapp","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/licenseapp","description":"A + Laravel web application to help with Codecov Self-Hosted License Management","fork":false,"url":"https://api.github.com/repos/codecov/licenseapp","forks_url":"https://api.github.com/repos/codecov/licenseapp/forks","keys_url":"https://api.github.com/repos/codecov/licenseapp/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/licenseapp/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/licenseapp/teams","hooks_url":"https://api.github.com/repos/codecov/licenseapp/hooks","issue_events_url":"https://api.github.com/repos/codecov/licenseapp/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/licenseapp/events","assignees_url":"https://api.github.com/repos/codecov/licenseapp/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/licenseapp/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/licenseapp/tags","blobs_url":"https://api.github.com/repos/codecov/licenseapp/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/licenseapp/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/licenseapp/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/licenseapp/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/licenseapp/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/licenseapp/languages","stargazers_url":"https://api.github.com/repos/codecov/licenseapp/stargazers","contributors_url":"https://api.github.com/repos/codecov/licenseapp/contributors","subscribers_url":"https://api.github.com/repos/codecov/licenseapp/subscribers","subscription_url":"https://api.github.com/repos/codecov/licenseapp/subscription","commits_url":"https://api.github.com/repos/codecov/licenseapp/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/licenseapp/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/licenseapp/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/licenseapp/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/licenseapp/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/licenseapp/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/licenseapp/merges","archive_url":"https://api.github.com/repos/codecov/licenseapp/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/licenseapp/downloads","issues_url":"https://api.github.com/repos/codecov/licenseapp/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/licenseapp/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/licenseapp/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/licenseapp/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/licenseapp/labels{/name}","releases_url":"https://api.github.com/repos/codecov/licenseapp/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/licenseapp/deployments","created_at":"2021-03-15T03:42:55Z","updated_at":"2021-11-08T20:52:47Z","pushed_at":"2023-09-11T23:02:01Z","git_url":"git://github.com/codecov/licenseapp.git","ssh_url":"git@github.com:codecov/licenseapp.git","clone_url":"https://github.com/codecov/licenseapp.git","svn_url":"https://github.com/codecov/licenseapp","homepage":null,"size":1640,"stargazers_count":0,"watchers_count":0,"language":"PHP","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":150576343,"node_id":"MDEwOlJlcG9zaXRvcnkxNTA1NzYzNDM=","name":"migrate","full_name":"codecov/migrate","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/migrate","description":"A + collection of docker images and scripts to provide an upgrade path for some + users of Codecov Enterprise 4.3.9 to 4.4.x. Please review these docs carefully + if you plan to upgrade your Codecov Enterprise installation.","fork":false,"url":"https://api.github.com/repos/codecov/migrate","forks_url":"https://api.github.com/repos/codecov/migrate/forks","keys_url":"https://api.github.com/repos/codecov/migrate/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/migrate/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/migrate/teams","hooks_url":"https://api.github.com/repos/codecov/migrate/hooks","issue_events_url":"https://api.github.com/repos/codecov/migrate/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/migrate/events","assignees_url":"https://api.github.com/repos/codecov/migrate/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/migrate/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/migrate/tags","blobs_url":"https://api.github.com/repos/codecov/migrate/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/migrate/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/migrate/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/migrate/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/migrate/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/migrate/languages","stargazers_url":"https://api.github.com/repos/codecov/migrate/stargazers","contributors_url":"https://api.github.com/repos/codecov/migrate/contributors","subscribers_url":"https://api.github.com/repos/codecov/migrate/subscribers","subscription_url":"https://api.github.com/repos/codecov/migrate/subscription","commits_url":"https://api.github.com/repos/codecov/migrate/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/migrate/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/migrate/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/migrate/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/migrate/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/migrate/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/migrate/merges","archive_url":"https://api.github.com/repos/codecov/migrate/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/migrate/downloads","issues_url":"https://api.github.com/repos/codecov/migrate/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/migrate/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/migrate/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/migrate/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/migrate/labels{/name}","releases_url":"https://api.github.com/repos/codecov/migrate/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/migrate/deployments","created_at":"2018-09-27T11:30:18Z","updated_at":"2023-09-13T16:36:36Z","pushed_at":"2022-10-31T20:39:20Z","git_url":"git://github.com/codecov/migrate.git","ssh_url":"git@github.com:codecov/migrate.git","clone_url":"https://github.com/codecov/migrate.git","svn_url":"https://github.com/codecov/migrate","homepage":"https://codecov.io","size":85,"stargazers_count":1,"watchers_count":1,"language":"PLpgSQL","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":2,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":2,"open_issues":1,"watchers":1,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":156281130,"node_id":"MDEwOlJlcG9zaXRvcnkxNTYyODExMzA=","name":"migration-tests","full_name":"codecov/migration-tests","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/migration-tests","description":"Tests + for migrating from Codecov Enterprise 4.3.9 to 4.4.0","fork":false,"url":"https://api.github.com/repos/codecov/migration-tests","forks_url":"https://api.github.com/repos/codecov/migration-tests/forks","keys_url":"https://api.github.com/repos/codecov/migration-tests/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/migration-tests/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/migration-tests/teams","hooks_url":"https://api.github.com/repos/codecov/migration-tests/hooks","issue_events_url":"https://api.github.com/repos/codecov/migration-tests/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/migration-tests/events","assignees_url":"https://api.github.com/repos/codecov/migration-tests/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/migration-tests/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/migration-tests/tags","blobs_url":"https://api.github.com/repos/codecov/migration-tests/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/migration-tests/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/migration-tests/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/migration-tests/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/migration-tests/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/migration-tests/languages","stargazers_url":"https://api.github.com/repos/codecov/migration-tests/stargazers","contributors_url":"https://api.github.com/repos/codecov/migration-tests/contributors","subscribers_url":"https://api.github.com/repos/codecov/migration-tests/subscribers","subscription_url":"https://api.github.com/repos/codecov/migration-tests/subscription","commits_url":"https://api.github.com/repos/codecov/migration-tests/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/migration-tests/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/migration-tests/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/migration-tests/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/migration-tests/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/migration-tests/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/migration-tests/merges","archive_url":"https://api.github.com/repos/codecov/migration-tests/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/migration-tests/downloads","issues_url":"https://api.github.com/repos/codecov/migration-tests/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/migration-tests/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/migration-tests/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/migration-tests/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/migration-tests/labels{/name}","releases_url":"https://api.github.com/repos/codecov/migration-tests/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/migration-tests/deployments","created_at":"2018-11-05T20:49:31Z","updated_at":"2018-11-16T18:59:13Z","pushed_at":"2018-11-16T18:59:12Z","git_url":"git://github.com/codecov/migration-tests.git","ssh_url":"git@github.com:codecov/migration-tests.git","clone_url":"https://github.com/codecov/migration-tests.git","svn_url":"https://github.com/codecov/migration-tests","homepage":null,"size":383,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":209791850,"node_id":"MDEwOlJlcG9zaXRvcnkyMDk3OTE4NTA=","name":"old-codecov-ui","full_name":"codecov/old-codecov-ui","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/old-codecov-ui","description":"Component + Library & Design System","fork":false,"url":"https://api.github.com/repos/codecov/old-codecov-ui","forks_url":"https://api.github.com/repos/codecov/old-codecov-ui/forks","keys_url":"https://api.github.com/repos/codecov/old-codecov-ui/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/old-codecov-ui/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/old-codecov-ui/teams","hooks_url":"https://api.github.com/repos/codecov/old-codecov-ui/hooks","issue_events_url":"https://api.github.com/repos/codecov/old-codecov-ui/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/old-codecov-ui/events","assignees_url":"https://api.github.com/repos/codecov/old-codecov-ui/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/old-codecov-ui/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/old-codecov-ui/tags","blobs_url":"https://api.github.com/repos/codecov/old-codecov-ui/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/old-codecov-ui/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/old-codecov-ui/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/old-codecov-ui/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/old-codecov-ui/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/old-codecov-ui/languages","stargazers_url":"https://api.github.com/repos/codecov/old-codecov-ui/stargazers","contributors_url":"https://api.github.com/repos/codecov/old-codecov-ui/contributors","subscribers_url":"https://api.github.com/repos/codecov/old-codecov-ui/subscribers","subscription_url":"https://api.github.com/repos/codecov/old-codecov-ui/subscription","commits_url":"https://api.github.com/repos/codecov/old-codecov-ui/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/old-codecov-ui/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/old-codecov-ui/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/old-codecov-ui/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/old-codecov-ui/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/old-codecov-ui/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/old-codecov-ui/merges","archive_url":"https://api.github.com/repos/codecov/old-codecov-ui/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/old-codecov-ui/downloads","issues_url":"https://api.github.com/repos/codecov/old-codecov-ui/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/old-codecov-ui/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/old-codecov-ui/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/old-codecov-ui/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/old-codecov-ui/labels{/name}","releases_url":"https://api.github.com/repos/codecov/old-codecov-ui/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/old-codecov-ui/deployments","created_at":"2019-09-20T13:01:08Z","updated_at":"2023-01-28T14:28:02Z","pushed_at":"2019-12-08T23:06:53Z","git_url":"git://github.com/codecov/old-codecov-ui.git","ssh_url":"git@github.com:codecov/old-codecov-ui.git","clone_url":"https://github.com/codecov/old-codecov-ui.git","svn_url":"https://github.com/codecov/old-codecov-ui","homepage":"https://codecov-ui.netlify.com","size":576,"stargazers_count":0,"watchers_count":0,"language":"Vue","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":false,"triage":false,"pull":true}},{"id":473693142,"node_id":"R_kgDOHDv71g","name":"opentelem-node","full_name":"codecov/opentelem-node","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/opentelem-node","description":"A + WIP OpenTelemetry Exporter for Runtime Insights","fork":false,"url":"https://api.github.com/repos/codecov/opentelem-node","forks_url":"https://api.github.com/repos/codecov/opentelem-node/forks","keys_url":"https://api.github.com/repos/codecov/opentelem-node/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/opentelem-node/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/opentelem-node/teams","hooks_url":"https://api.github.com/repos/codecov/opentelem-node/hooks","issue_events_url":"https://api.github.com/repos/codecov/opentelem-node/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/opentelem-node/events","assignees_url":"https://api.github.com/repos/codecov/opentelem-node/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/opentelem-node/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/opentelem-node/tags","blobs_url":"https://api.github.com/repos/codecov/opentelem-node/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/opentelem-node/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/opentelem-node/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/opentelem-node/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/opentelem-node/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/opentelem-node/languages","stargazers_url":"https://api.github.com/repos/codecov/opentelem-node/stargazers","contributors_url":"https://api.github.com/repos/codecov/opentelem-node/contributors","subscribers_url":"https://api.github.com/repos/codecov/opentelem-node/subscribers","subscription_url":"https://api.github.com/repos/codecov/opentelem-node/subscription","commits_url":"https://api.github.com/repos/codecov/opentelem-node/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/opentelem-node/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/opentelem-node/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/opentelem-node/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/opentelem-node/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/opentelem-node/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/opentelem-node/merges","archive_url":"https://api.github.com/repos/codecov/opentelem-node/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/opentelem-node/downloads","issues_url":"https://api.github.com/repos/codecov/opentelem-node/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/opentelem-node/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/opentelem-node/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/opentelem-node/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/opentelem-node/labels{/name}","releases_url":"https://api.github.com/repos/codecov/opentelem-node/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/opentelem-node/deployments","created_at":"2022-03-24T16:46:43Z","updated_at":"2023-07-25T14:55:46Z","pushed_at":"2023-09-16T04:58:41Z","git_url":"git://github.com/codecov/opentelem-node.git","ssh_url":"git@github.com:codecov/opentelem-node.git","clone_url":"https://github.com/codecov/opentelem-node.git","svn_url":"https://github.com/codecov/opentelem-node","homepage":null,"size":541,"stargazers_count":4,"watchers_count":4,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":1,"open_issues":1,"watchers":4,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":399619197,"node_id":"MDEwOlJlcG9zaXRvcnkzOTk2MTkxOTc=","name":"opentelem-python","full_name":"codecov/opentelem-python","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/opentelem-python","description":"Open + Telemetry Python Prototype","fork":false,"url":"https://api.github.com/repos/codecov/opentelem-python","forks_url":"https://api.github.com/repos/codecov/opentelem-python/forks","keys_url":"https://api.github.com/repos/codecov/opentelem-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/opentelem-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/opentelem-python/teams","hooks_url":"https://api.github.com/repos/codecov/opentelem-python/hooks","issue_events_url":"https://api.github.com/repos/codecov/opentelem-python/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/opentelem-python/events","assignees_url":"https://api.github.com/repos/codecov/opentelem-python/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/opentelem-python/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/opentelem-python/tags","blobs_url":"https://api.github.com/repos/codecov/opentelem-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/opentelem-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/opentelem-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/opentelem-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/opentelem-python/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/opentelem-python/languages","stargazers_url":"https://api.github.com/repos/codecov/opentelem-python/stargazers","contributors_url":"https://api.github.com/repos/codecov/opentelem-python/contributors","subscribers_url":"https://api.github.com/repos/codecov/opentelem-python/subscribers","subscription_url":"https://api.github.com/repos/codecov/opentelem-python/subscription","commits_url":"https://api.github.com/repos/codecov/opentelem-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/opentelem-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/opentelem-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/opentelem-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/opentelem-python/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/opentelem-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/opentelem-python/merges","archive_url":"https://api.github.com/repos/codecov/opentelem-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/opentelem-python/downloads","issues_url":"https://api.github.com/repos/codecov/opentelem-python/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/opentelem-python/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/opentelem-python/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/opentelem-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/opentelem-python/labels{/name}","releases_url":"https://api.github.com/repos/codecov/opentelem-python/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/opentelem-python/deployments","created_at":"2021-08-24T22:19:55Z","updated_at":"2023-07-25T14:49:02Z","pushed_at":"2023-02-28T14:46:22Z","git_url":"git://github.com/codecov/opentelem-python.git","ssh_url":"git@github.com:codecov/opentelem-python.git","clone_url":"https://github.com/codecov/opentelem-python.git","svn_url":"https://github.com/codecov/opentelem-python","homepage":null,"size":45,"stargazers_count":2,"watchers_count":2,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":1,"open_issues":1,"watchers":2,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":430976555,"node_id":"R_kgDOGbAuKw","name":"opentelem-ruby","full_name":"codecov/opentelem-ruby","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/opentelem-ruby","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/opentelem-ruby","forks_url":"https://api.github.com/repos/codecov/opentelem-ruby/forks","keys_url":"https://api.github.com/repos/codecov/opentelem-ruby/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/opentelem-ruby/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/opentelem-ruby/teams","hooks_url":"https://api.github.com/repos/codecov/opentelem-ruby/hooks","issue_events_url":"https://api.github.com/repos/codecov/opentelem-ruby/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/opentelem-ruby/events","assignees_url":"https://api.github.com/repos/codecov/opentelem-ruby/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/opentelem-ruby/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/opentelem-ruby/tags","blobs_url":"https://api.github.com/repos/codecov/opentelem-ruby/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/opentelem-ruby/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/opentelem-ruby/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/opentelem-ruby/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/opentelem-ruby/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/opentelem-ruby/languages","stargazers_url":"https://api.github.com/repos/codecov/opentelem-ruby/stargazers","contributors_url":"https://api.github.com/repos/codecov/opentelem-ruby/contributors","subscribers_url":"https://api.github.com/repos/codecov/opentelem-ruby/subscribers","subscription_url":"https://api.github.com/repos/codecov/opentelem-ruby/subscription","commits_url":"https://api.github.com/repos/codecov/opentelem-ruby/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/opentelem-ruby/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/opentelem-ruby/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/opentelem-ruby/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/opentelem-ruby/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/opentelem-ruby/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/opentelem-ruby/merges","archive_url":"https://api.github.com/repos/codecov/opentelem-ruby/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/opentelem-ruby/downloads","issues_url":"https://api.github.com/repos/codecov/opentelem-ruby/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/opentelem-ruby/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/opentelem-ruby/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/opentelem-ruby/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/opentelem-ruby/labels{/name}","releases_url":"https://api.github.com/repos/codecov/opentelem-ruby/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/opentelem-ruby/deployments","created_at":"2021-11-23T05:49:33Z","updated_at":"2022-05-12T20:07:18Z","pushed_at":"2022-06-22T15:49:05Z","git_url":"git://github.com/codecov/opentelem-ruby.git","ssh_url":"git@github.com:codecov/opentelem-ruby.git","clone_url":"https://github.com/codecov/opentelem-ruby.git","svn_url":"https://github.com/codecov/opentelem-ruby","homepage":null,"size":43,"stargazers_count":0,"watchers_count":0,"language":"Ruby","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":643745888,"node_id":"R_kgDOJl7IYA","name":"parasol","full_name":"codecov/parasol","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/parasol","description":"Monitoring","fork":false,"url":"https://api.github.com/repos/codecov/parasol","forks_url":"https://api.github.com/repos/codecov/parasol/forks","keys_url":"https://api.github.com/repos/codecov/parasol/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/parasol/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/parasol/teams","hooks_url":"https://api.github.com/repos/codecov/parasol/hooks","issue_events_url":"https://api.github.com/repos/codecov/parasol/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/parasol/events","assignees_url":"https://api.github.com/repos/codecov/parasol/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/parasol/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/parasol/tags","blobs_url":"https://api.github.com/repos/codecov/parasol/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/parasol/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/parasol/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/parasol/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/parasol/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/parasol/languages","stargazers_url":"https://api.github.com/repos/codecov/parasol/stargazers","contributors_url":"https://api.github.com/repos/codecov/parasol/contributors","subscribers_url":"https://api.github.com/repos/codecov/parasol/subscribers","subscription_url":"https://api.github.com/repos/codecov/parasol/subscription","commits_url":"https://api.github.com/repos/codecov/parasol/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/parasol/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/parasol/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/parasol/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/parasol/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/parasol/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/parasol/merges","archive_url":"https://api.github.com/repos/codecov/parasol/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/parasol/downloads","issues_url":"https://api.github.com/repos/codecov/parasol/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/parasol/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/parasol/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/parasol/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/parasol/labels{/name}","releases_url":"https://api.github.com/repos/codecov/parasol/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/parasol/deployments","created_at":"2023-05-22T04:23:46Z","updated_at":"2023-05-22T04:25:54Z","pushed_at":"2023-09-12T14:43:10Z","git_url":"git://github.com/codecov/parasol.git","ssh_url":"git@github.com:codecov/parasol.git","clone_url":"https://github.com/codecov/parasol.git","svn_url":"https://github.com/codecov/parasol","homepage":null,"size":37,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":574675406,"node_id":"R_kgDOIkDZzg","name":"pavilion","full_name":"codecov/pavilion","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/pavilion","description":"React + component library built with the Pavilion design system.","fork":false,"url":"https://api.github.com/repos/codecov/pavilion","forks_url":"https://api.github.com/repos/codecov/pavilion/forks","keys_url":"https://api.github.com/repos/codecov/pavilion/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/pavilion/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/pavilion/teams","hooks_url":"https://api.github.com/repos/codecov/pavilion/hooks","issue_events_url":"https://api.github.com/repos/codecov/pavilion/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/pavilion/events","assignees_url":"https://api.github.com/repos/codecov/pavilion/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/pavilion/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/pavilion/tags","blobs_url":"https://api.github.com/repos/codecov/pavilion/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/pavilion/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/pavilion/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/pavilion/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/pavilion/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/pavilion/languages","stargazers_url":"https://api.github.com/repos/codecov/pavilion/stargazers","contributors_url":"https://api.github.com/repos/codecov/pavilion/contributors","subscribers_url":"https://api.github.com/repos/codecov/pavilion/subscribers","subscription_url":"https://api.github.com/repos/codecov/pavilion/subscription","commits_url":"https://api.github.com/repos/codecov/pavilion/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/pavilion/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/pavilion/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/pavilion/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/pavilion/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/pavilion/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/pavilion/merges","archive_url":"https://api.github.com/repos/codecov/pavilion/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/pavilion/downloads","issues_url":"https://api.github.com/repos/codecov/pavilion/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/pavilion/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/pavilion/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/pavilion/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/pavilion/labels{/name}","releases_url":"https://api.github.com/repos/codecov/pavilion/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/pavilion/deployments","created_at":"2022-12-05T20:47:18Z","updated_at":"2022-12-09T15:50:31Z","pushed_at":"2023-06-20T17:55:52Z","git_url":"git://github.com/codecov/pavilion.git","ssh_url":"git@github.com:codecov/pavilion.git","clone_url":"https://github.com/codecov/pavilion.git","svn_url":"https://github.com/codecov/pavilion","homepage":"","size":2933,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":2,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":277806084,"node_id":"MDEwOlJlcG9zaXRvcnkyNzc4MDYwODQ=","name":"private-cf-flags-demo","full_name":"codecov/private-cf-flags-demo","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/private-cf-flags-demo","description":"A + small cf flags demo, meant for private use only since it works against staging.","fork":false,"url":"https://api.github.com/repos/codecov/private-cf-flags-demo","forks_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/forks","keys_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/teams","hooks_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/hooks","issue_events_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/events","assignees_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/tags","blobs_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/languages","stargazers_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/stargazers","contributors_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/contributors","subscribers_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/subscribers","subscription_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/subscription","commits_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/merges","archive_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/downloads","issues_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/labels{/name}","releases_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/private-cf-flags-demo/deployments","created_at":"2020-07-07T12:09:06Z","updated_at":"2020-07-07T12:24:30Z","pushed_at":"2020-07-07T12:41:23Z","git_url":"git://github.com/codecov/private-cf-flags-demo.git","ssh_url":"git@github.com:codecov/private-cf-flags-demo.git","clone_url":"https://github.com/codecov/private-cf-flags-demo.git","svn_url":"https://github.com/codecov/private-cf-flags-demo","homepage":null,"size":4,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":375756660,"node_id":"MDEwOlJlcG9zaXRvcnkzNzU3NTY2NjA=","name":"python-codecov-opentelemetry","full_name":"codecov/python-codecov-opentelemetry","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/python-codecov-opentelemetry","description":"A + WIP exporter for Python to capture and export opentelem data.","fork":false,"url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry","forks_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/forks","keys_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/teams","hooks_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/hooks","issue_events_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/events","assignees_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/tags","blobs_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/languages","stargazers_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/stargazers","contributors_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/contributors","subscribers_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/subscribers","subscription_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/subscription","commits_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/merges","archive_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/downloads","issues_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/labels{/name}","releases_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/python-codecov-opentelemetry/deployments","created_at":"2021-06-10T16:05:38Z","updated_at":"2021-08-06T15:54:03Z","pushed_at":"2021-08-06T15:54:00Z","git_url":"git://github.com/codecov/python-codecov-opentelemetry.git","ssh_url":"git@github.com:codecov/python-codecov-opentelemetry.git","clone_url":"https://github.com/codecov/python-codecov-opentelemetry.git","svn_url":"https://github.com/codecov/python-codecov-opentelemetry","homepage":null,"size":14,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":167416484,"node_id":"MDEwOlJlcG9zaXRvcnkxNjc0MTY0ODQ=","name":"python-sample-repo","full_name":"codecov/python-sample-repo","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/python-sample-repo","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/python-sample-repo","forks_url":"https://api.github.com/repos/codecov/python-sample-repo/forks","keys_url":"https://api.github.com/repos/codecov/python-sample-repo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/python-sample-repo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/python-sample-repo/teams","hooks_url":"https://api.github.com/repos/codecov/python-sample-repo/hooks","issue_events_url":"https://api.github.com/repos/codecov/python-sample-repo/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/python-sample-repo/events","assignees_url":"https://api.github.com/repos/codecov/python-sample-repo/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/python-sample-repo/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/python-sample-repo/tags","blobs_url":"https://api.github.com/repos/codecov/python-sample-repo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/python-sample-repo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/python-sample-repo/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/python-sample-repo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/python-sample-repo/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/python-sample-repo/languages","stargazers_url":"https://api.github.com/repos/codecov/python-sample-repo/stargazers","contributors_url":"https://api.github.com/repos/codecov/python-sample-repo/contributors","subscribers_url":"https://api.github.com/repos/codecov/python-sample-repo/subscribers","subscription_url":"https://api.github.com/repos/codecov/python-sample-repo/subscription","commits_url":"https://api.github.com/repos/codecov/python-sample-repo/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/python-sample-repo/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/python-sample-repo/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/python-sample-repo/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/python-sample-repo/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/python-sample-repo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/python-sample-repo/merges","archive_url":"https://api.github.com/repos/codecov/python-sample-repo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/python-sample-repo/downloads","issues_url":"https://api.github.com/repos/codecov/python-sample-repo/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/python-sample-repo/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/python-sample-repo/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/python-sample-repo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/python-sample-repo/labels{/name}","releases_url":"https://api.github.com/repos/codecov/python-sample-repo/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/python-sample-repo/deployments","created_at":"2019-01-24T18:31:15Z","updated_at":"2020-04-23T06:34:53Z","pushed_at":"2020-07-21T05:47:16Z","git_url":"git://github.com/codecov/python-sample-repo.git","ssh_url":"git@github.com:codecov/python-sample-repo.git","clone_url":"https://github.com/codecov/python-sample-repo.git","svn_url":"https://github.com/codecov/python-sample-repo","homepage":null,"size":67,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":8,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":8,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":193794389,"node_id":"MDEwOlJlcG9zaXRvcnkxOTM3OTQzODk=","name":"python-standard","full_name":"codecov/python-standard","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/python-standard","description":"Codecov + coverage standard for Python","fork":false,"url":"https://api.github.com/repos/codecov/python-standard","forks_url":"https://api.github.com/repos/codecov/python-standard/forks","keys_url":"https://api.github.com/repos/codecov/python-standard/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/python-standard/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/python-standard/teams","hooks_url":"https://api.github.com/repos/codecov/python-standard/hooks","issue_events_url":"https://api.github.com/repos/codecov/python-standard/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/python-standard/events","assignees_url":"https://api.github.com/repos/codecov/python-standard/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/python-standard/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/python-standard/tags","blobs_url":"https://api.github.com/repos/codecov/python-standard/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/python-standard/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/python-standard/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/python-standard/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/python-standard/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/python-standard/languages","stargazers_url":"https://api.github.com/repos/codecov/python-standard/stargazers","contributors_url":"https://api.github.com/repos/codecov/python-standard/contributors","subscribers_url":"https://api.github.com/repos/codecov/python-standard/subscribers","subscription_url":"https://api.github.com/repos/codecov/python-standard/subscription","commits_url":"https://api.github.com/repos/codecov/python-standard/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/python-standard/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/python-standard/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/python-standard/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/python-standard/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/python-standard/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/python-standard/merges","archive_url":"https://api.github.com/repos/codecov/python-standard/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/python-standard/downloads","issues_url":"https://api.github.com/repos/codecov/python-standard/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/python-standard/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/python-standard/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/python-standard/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/python-standard/labels{/name}","releases_url":"https://api.github.com/repos/codecov/python-standard/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/python-standard/deployments","created_at":"2019-06-25T23:01:03Z","updated_at":"2023-08-03T07:05:02Z","pushed_at":"2023-09-16T05:05:33Z","git_url":"git://github.com/codecov/python-standard.git","ssh_url":"git@github.com:codecov/python-standard.git","clone_url":"https://github.com/codecov/python-standard.git","svn_url":"https://github.com/codecov/python-standard","homepage":"","size":475,"stargazers_count":12,"watchers_count":12,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":22,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["codecov","coverage-reports","python"],"visibility":"public","forks":22,"open_issues":0,"watchers":12,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":272458865,"node_id":"MDEwOlJlcG9zaXRvcnkyNzI0NTg4NjU=","name":"raw-report-viewer","full_name":"codecov/raw-report-viewer","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/raw-report-viewer","description":"Provides + presigned GETs to download raw reports from codecov storage","fork":false,"url":"https://api.github.com/repos/codecov/raw-report-viewer","forks_url":"https://api.github.com/repos/codecov/raw-report-viewer/forks","keys_url":"https://api.github.com/repos/codecov/raw-report-viewer/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/raw-report-viewer/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/raw-report-viewer/teams","hooks_url":"https://api.github.com/repos/codecov/raw-report-viewer/hooks","issue_events_url":"https://api.github.com/repos/codecov/raw-report-viewer/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/raw-report-viewer/events","assignees_url":"https://api.github.com/repos/codecov/raw-report-viewer/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/raw-report-viewer/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/raw-report-viewer/tags","blobs_url":"https://api.github.com/repos/codecov/raw-report-viewer/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/raw-report-viewer/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/raw-report-viewer/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/raw-report-viewer/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/raw-report-viewer/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/raw-report-viewer/languages","stargazers_url":"https://api.github.com/repos/codecov/raw-report-viewer/stargazers","contributors_url":"https://api.github.com/repos/codecov/raw-report-viewer/contributors","subscribers_url":"https://api.github.com/repos/codecov/raw-report-viewer/subscribers","subscription_url":"https://api.github.com/repos/codecov/raw-report-viewer/subscription","commits_url":"https://api.github.com/repos/codecov/raw-report-viewer/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/raw-report-viewer/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/raw-report-viewer/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/raw-report-viewer/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/raw-report-viewer/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/raw-report-viewer/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/raw-report-viewer/merges","archive_url":"https://api.github.com/repos/codecov/raw-report-viewer/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/raw-report-viewer/downloads","issues_url":"https://api.github.com/repos/codecov/raw-report-viewer/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/raw-report-viewer/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/raw-report-viewer/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/raw-report-viewer/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/raw-report-viewer/labels{/name}","releases_url":"https://api.github.com/repos/codecov/raw-report-viewer/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/raw-report-viewer/deployments","created_at":"2020-06-15T14:21:14Z","updated_at":"2020-09-16T20:14:02Z","pushed_at":"2020-06-17T14:27:06Z","git_url":"git://github.com/codecov/raw-report-viewer.git","ssh_url":"git@github.com:codecov/raw-report-viewer.git","clone_url":"https://github.com/codecov/raw-report-viewer.git","svn_url":"https://github.com/codecov/raw-report-viewer","homepage":null,"size":785,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":120340827,"node_id":"MDEwOlJlcG9zaXRvcnkxMjAzNDA4Mjc=","name":"report","full_name":"codecov/report","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/report","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/report","forks_url":"https://api.github.com/repos/codecov/report/forks","keys_url":"https://api.github.com/repos/codecov/report/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/report/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/report/teams","hooks_url":"https://api.github.com/repos/codecov/report/hooks","issue_events_url":"https://api.github.com/repos/codecov/report/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/report/events","assignees_url":"https://api.github.com/repos/codecov/report/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/report/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/report/tags","blobs_url":"https://api.github.com/repos/codecov/report/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/report/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/report/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/report/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/report/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/report/languages","stargazers_url":"https://api.github.com/repos/codecov/report/stargazers","contributors_url":"https://api.github.com/repos/codecov/report/contributors","subscribers_url":"https://api.github.com/repos/codecov/report/subscribers","subscription_url":"https://api.github.com/repos/codecov/report/subscription","commits_url":"https://api.github.com/repos/codecov/report/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/report/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/report/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/report/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/report/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/report/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/report/merges","archive_url":"https://api.github.com/repos/codecov/report/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/report/downloads","issues_url":"https://api.github.com/repos/codecov/report/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/report/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/report/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/report/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/report/labels{/name}","releases_url":"https://api.github.com/repos/codecov/report/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/report/deployments","created_at":"2018-02-05T17:56:27Z","updated_at":"2022-01-10T19:31:54Z","pushed_at":"2022-01-10T19:31:51Z","git_url":"git://github.com/codecov/report.git","ssh_url":"git@github.com:codecov/report.git","clone_url":"https://github.com/codecov/report.git","svn_url":"https://github.com/codecov/report","homepage":null,"size":446,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":307780093,"node_id":"MDEwOlJlcG9zaXRvcnkzMDc3ODAwOTM=","name":"ribs","full_name":"codecov/ribs","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/ribs","description":"Rust + Service to be called from inside python","fork":false,"url":"https://api.github.com/repos/codecov/ribs","forks_url":"https://api.github.com/repos/codecov/ribs/forks","keys_url":"https://api.github.com/repos/codecov/ribs/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/ribs/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/ribs/teams","hooks_url":"https://api.github.com/repos/codecov/ribs/hooks","issue_events_url":"https://api.github.com/repos/codecov/ribs/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/ribs/events","assignees_url":"https://api.github.com/repos/codecov/ribs/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/ribs/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/ribs/tags","blobs_url":"https://api.github.com/repos/codecov/ribs/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/ribs/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/ribs/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/ribs/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/ribs/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/ribs/languages","stargazers_url":"https://api.github.com/repos/codecov/ribs/stargazers","contributors_url":"https://api.github.com/repos/codecov/ribs/contributors","subscribers_url":"https://api.github.com/repos/codecov/ribs/subscribers","subscription_url":"https://api.github.com/repos/codecov/ribs/subscription","commits_url":"https://api.github.com/repos/codecov/ribs/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/ribs/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/ribs/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/ribs/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/ribs/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/ribs/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/ribs/merges","archive_url":"https://api.github.com/repos/codecov/ribs/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/ribs/downloads","issues_url":"https://api.github.com/repos/codecov/ribs/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/ribs/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/ribs/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/ribs/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/ribs/labels{/name}","releases_url":"https://api.github.com/repos/codecov/ribs/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/ribs/deployments","created_at":"2020-10-27T17:37:18Z","updated_at":"2021-08-27T16:35:49Z","pushed_at":"2021-09-03T04:20:55Z","git_url":"git://github.com/codecov/ribs.git","ssh_url":"git@github.com:codecov/ribs.git","clone_url":"https://github.com/codecov/ribs.git","svn_url":"https://github.com/codecov/ribs","homepage":null,"size":175,"stargazers_count":0,"watchers_count":0,"language":"Rust","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":613581539,"node_id":"R_kgDOJJKC4w","name":"roadmap","full_name":"codecov/roadmap","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/roadmap","description":"The + public roadmap for Codecov","fork":false,"url":"https://api.github.com/repos/codecov/roadmap","forks_url":"https://api.github.com/repos/codecov/roadmap/forks","keys_url":"https://api.github.com/repos/codecov/roadmap/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/roadmap/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/roadmap/teams","hooks_url":"https://api.github.com/repos/codecov/roadmap/hooks","issue_events_url":"https://api.github.com/repos/codecov/roadmap/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/roadmap/events","assignees_url":"https://api.github.com/repos/codecov/roadmap/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/roadmap/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/roadmap/tags","blobs_url":"https://api.github.com/repos/codecov/roadmap/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/roadmap/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/roadmap/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/roadmap/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/roadmap/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/roadmap/languages","stargazers_url":"https://api.github.com/repos/codecov/roadmap/stargazers","contributors_url":"https://api.github.com/repos/codecov/roadmap/contributors","subscribers_url":"https://api.github.com/repos/codecov/roadmap/subscribers","subscription_url":"https://api.github.com/repos/codecov/roadmap/subscription","commits_url":"https://api.github.com/repos/codecov/roadmap/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/roadmap/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/roadmap/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/roadmap/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/roadmap/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/roadmap/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/roadmap/merges","archive_url":"https://api.github.com/repos/codecov/roadmap/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/roadmap/downloads","issues_url":"https://api.github.com/repos/codecov/roadmap/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/roadmap/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/roadmap/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/roadmap/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/roadmap/labels{/name}","releases_url":"https://api.github.com/repos/codecov/roadmap/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/roadmap/deployments","created_at":"2023-03-13T21:17:53Z","updated_at":"2023-03-13T21:17:53Z","pushed_at":"2023-09-07T22:21:26Z","git_url":"git://github.com/codecov/roadmap.git","ssh_url":"git@github.com:codecov/roadmap.git","clone_url":"https://github.com/codecov/roadmap.git","svn_url":"https://github.com/codecov/roadmap","homepage":null,"size":3,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":17,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":17,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":false,"triage":false,"pull":true}},{"id":197329672,"node_id":"MDEwOlJlcG9zaXRvcnkxOTczMjk2NzI=","name":"ruby-standard-1","full_name":"codecov/ruby-standard-1","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/ruby-standard-1","description":"Codecov + coverage standard for Ruby using the Codecov gem","fork":false,"url":"https://api.github.com/repos/codecov/ruby-standard-1","forks_url":"https://api.github.com/repos/codecov/ruby-standard-1/forks","keys_url":"https://api.github.com/repos/codecov/ruby-standard-1/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/ruby-standard-1/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/ruby-standard-1/teams","hooks_url":"https://api.github.com/repos/codecov/ruby-standard-1/hooks","issue_events_url":"https://api.github.com/repos/codecov/ruby-standard-1/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/ruby-standard-1/events","assignees_url":"https://api.github.com/repos/codecov/ruby-standard-1/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/ruby-standard-1/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/ruby-standard-1/tags","blobs_url":"https://api.github.com/repos/codecov/ruby-standard-1/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/ruby-standard-1/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/ruby-standard-1/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/ruby-standard-1/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/ruby-standard-1/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/ruby-standard-1/languages","stargazers_url":"https://api.github.com/repos/codecov/ruby-standard-1/stargazers","contributors_url":"https://api.github.com/repos/codecov/ruby-standard-1/contributors","subscribers_url":"https://api.github.com/repos/codecov/ruby-standard-1/subscribers","subscription_url":"https://api.github.com/repos/codecov/ruby-standard-1/subscription","commits_url":"https://api.github.com/repos/codecov/ruby-standard-1/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/ruby-standard-1/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/ruby-standard-1/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/ruby-standard-1/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/ruby-standard-1/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/ruby-standard-1/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/ruby-standard-1/merges","archive_url":"https://api.github.com/repos/codecov/ruby-standard-1/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/ruby-standard-1/downloads","issues_url":"https://api.github.com/repos/codecov/ruby-standard-1/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/ruby-standard-1/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/ruby-standard-1/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/ruby-standard-1/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/ruby-standard-1/labels{/name}","releases_url":"https://api.github.com/repos/codecov/ruby-standard-1/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/ruby-standard-1/deployments","created_at":"2019-07-17T06:34:44Z","updated_at":"2023-01-31T17:25:39Z","pushed_at":"2023-08-24T18:42:09Z","git_url":"git://github.com/codecov/ruby-standard-1.git","ssh_url":"git@github.com:codecov/ruby-standard-1.git","clone_url":"https://github.com/codecov/ruby-standard-1.git","svn_url":"https://github.com/codecov/ruby-standard-1","homepage":"","size":231,"stargazers_count":1,"watchers_count":1,"language":"Ruby","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["codecov","coverage-reports","rails","ruby"],"visibility":"public","forks":3,"open_issues":0,"watchers":1,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":198187206,"node_id":"MDEwOlJlcG9zaXRvcnkxOTgxODcyMDY=","name":"ruby-standard-2","full_name":"codecov/ruby-standard-2","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/ruby-standard-2","description":"Codecov + coverage standard for Ruby using Codecov''s Bash uploader","fork":false,"url":"https://api.github.com/repos/codecov/ruby-standard-2","forks_url":"https://api.github.com/repos/codecov/ruby-standard-2/forks","keys_url":"https://api.github.com/repos/codecov/ruby-standard-2/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/ruby-standard-2/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/ruby-standard-2/teams","hooks_url":"https://api.github.com/repos/codecov/ruby-standard-2/hooks","issue_events_url":"https://api.github.com/repos/codecov/ruby-standard-2/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/ruby-standard-2/events","assignees_url":"https://api.github.com/repos/codecov/ruby-standard-2/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/ruby-standard-2/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/ruby-standard-2/tags","blobs_url":"https://api.github.com/repos/codecov/ruby-standard-2/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/ruby-standard-2/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/ruby-standard-2/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/ruby-standard-2/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/ruby-standard-2/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/ruby-standard-2/languages","stargazers_url":"https://api.github.com/repos/codecov/ruby-standard-2/stargazers","contributors_url":"https://api.github.com/repos/codecov/ruby-standard-2/contributors","subscribers_url":"https://api.github.com/repos/codecov/ruby-standard-2/subscribers","subscription_url":"https://api.github.com/repos/codecov/ruby-standard-2/subscription","commits_url":"https://api.github.com/repos/codecov/ruby-standard-2/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/ruby-standard-2/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/ruby-standard-2/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/ruby-standard-2/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/ruby-standard-2/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/ruby-standard-2/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/ruby-standard-2/merges","archive_url":"https://api.github.com/repos/codecov/ruby-standard-2/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/ruby-standard-2/downloads","issues_url":"https://api.github.com/repos/codecov/ruby-standard-2/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/ruby-standard-2/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/ruby-standard-2/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/ruby-standard-2/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/ruby-standard-2/labels{/name}","releases_url":"https://api.github.com/repos/codecov/ruby-standard-2/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/ruby-standard-2/deployments","created_at":"2019-07-22T09:06:36Z","updated_at":"2023-07-25T14:27:39Z","pushed_at":"2023-09-16T05:07:20Z","git_url":"git://github.com/codecov/ruby-standard-2.git","ssh_url":"git@github.com:codecov/ruby-standard-2.git","clone_url":"https://github.com/codecov/ruby-standard-2.git","svn_url":"https://github.com/codecov/ruby-standard-2","homepage":"","size":446,"stargazers_count":3,"watchers_count":3,"language":"Ruby","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":5,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["codecov","coverage-reports","rails","ruby"],"visibility":"public","forks":5,"open_issues":1,"watchers":3,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":659790884,"node_id":"R_kgDOJ1OcJA","name":"self-hosted","full_name":"codecov/self-hosted","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/self-hosted","description":"Example + of how to setup Codecov with docker compose","fork":false,"url":"https://api.github.com/repos/codecov/self-hosted","forks_url":"https://api.github.com/repos/codecov/self-hosted/forks","keys_url":"https://api.github.com/repos/codecov/self-hosted/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/self-hosted/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/self-hosted/teams","hooks_url":"https://api.github.com/repos/codecov/self-hosted/hooks","issue_events_url":"https://api.github.com/repos/codecov/self-hosted/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/self-hosted/events","assignees_url":"https://api.github.com/repos/codecov/self-hosted/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/self-hosted/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/self-hosted/tags","blobs_url":"https://api.github.com/repos/codecov/self-hosted/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/self-hosted/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/self-hosted/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/self-hosted/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/self-hosted/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/self-hosted/languages","stargazers_url":"https://api.github.com/repos/codecov/self-hosted/stargazers","contributors_url":"https://api.github.com/repos/codecov/self-hosted/contributors","subscribers_url":"https://api.github.com/repos/codecov/self-hosted/subscribers","subscription_url":"https://api.github.com/repos/codecov/self-hosted/subscription","commits_url":"https://api.github.com/repos/codecov/self-hosted/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/self-hosted/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/self-hosted/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/self-hosted/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/self-hosted/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/self-hosted/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/self-hosted/merges","archive_url":"https://api.github.com/repos/codecov/self-hosted/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/self-hosted/downloads","issues_url":"https://api.github.com/repos/codecov/self-hosted/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/self-hosted/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/self-hosted/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/self-hosted/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/self-hosted/labels{/name}","releases_url":"https://api.github.com/repos/codecov/self-hosted/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/self-hosted/deployments","created_at":"2023-06-28T15:06:37Z","updated_at":"2023-09-21T20:16:20Z","pushed_at":"2023-09-08T20:35:43Z","git_url":"git://github.com/codecov/self-hosted.git","ssh_url":"git@github.com:codecov/self-hosted.git","clone_url":"https://github.com/codecov/self-hosted.git","svn_url":"https://github.com/codecov/self-hosted","homepage":null,"size":29,"stargazers_count":334,"watchers_count":334,"language":"Shell","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":14,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":5,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":14,"open_issues":5,"watchers":334,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":667551874,"node_id":"R_kgDOJ8oIgg","name":"shared","full_name":"codecov/shared","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/shared","description":"Shared + code between worker and api","fork":false,"url":"https://api.github.com/repos/codecov/shared","forks_url":"https://api.github.com/repos/codecov/shared/forks","keys_url":"https://api.github.com/repos/codecov/shared/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/shared/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/shared/teams","hooks_url":"https://api.github.com/repos/codecov/shared/hooks","issue_events_url":"https://api.github.com/repos/codecov/shared/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/shared/events","assignees_url":"https://api.github.com/repos/codecov/shared/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/shared/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/shared/tags","blobs_url":"https://api.github.com/repos/codecov/shared/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/shared/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/shared/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/shared/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/shared/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/shared/languages","stargazers_url":"https://api.github.com/repos/codecov/shared/stargazers","contributors_url":"https://api.github.com/repos/codecov/shared/contributors","subscribers_url":"https://api.github.com/repos/codecov/shared/subscribers","subscription_url":"https://api.github.com/repos/codecov/shared/subscription","commits_url":"https://api.github.com/repos/codecov/shared/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/shared/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/shared/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/shared/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/shared/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/shared/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/shared/merges","archive_url":"https://api.github.com/repos/codecov/shared/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/shared/downloads","issues_url":"https://api.github.com/repos/codecov/shared/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/shared/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/shared/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/shared/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/shared/labels{/name}","releases_url":"https://api.github.com/repos/codecov/shared/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/shared/deployments","created_at":"2023-07-17T19:10:49Z","updated_at":"2023-08-03T16:30:50Z","pushed_at":"2023-09-22T00:30:44Z","git_url":"git://github.com/codecov/shared.git","ssh_url":"git@github.com:codecov/shared.git","clone_url":"https://github.com/codecov/shared.git","svn_url":"https://github.com/codecov/shared","homepage":null,"size":3064,"stargazers_count":11,"watchers_count":11,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":2,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":9,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":2,"open_issues":9,"watchers":11,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":254750340,"node_id":"MDEwOlJlcG9zaXRvcnkyNTQ3NTAzNDA=","name":"shared-archive","full_name":"codecov/shared-archive","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/shared-archive","description":"Shared + code between worker and api","fork":false,"url":"https://api.github.com/repos/codecov/shared-archive","forks_url":"https://api.github.com/repos/codecov/shared-archive/forks","keys_url":"https://api.github.com/repos/codecov/shared-archive/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/shared-archive/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/shared-archive/teams","hooks_url":"https://api.github.com/repos/codecov/shared-archive/hooks","issue_events_url":"https://api.github.com/repos/codecov/shared-archive/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/shared-archive/events","assignees_url":"https://api.github.com/repos/codecov/shared-archive/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/shared-archive/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/shared-archive/tags","blobs_url":"https://api.github.com/repos/codecov/shared-archive/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/shared-archive/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/shared-archive/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/shared-archive/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/shared-archive/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/shared-archive/languages","stargazers_url":"https://api.github.com/repos/codecov/shared-archive/stargazers","contributors_url":"https://api.github.com/repos/codecov/shared-archive/contributors","subscribers_url":"https://api.github.com/repos/codecov/shared-archive/subscribers","subscription_url":"https://api.github.com/repos/codecov/shared-archive/subscription","commits_url":"https://api.github.com/repos/codecov/shared-archive/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/shared-archive/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/shared-archive/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/shared-archive/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/shared-archive/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/shared-archive/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/shared-archive/merges","archive_url":"https://api.github.com/repos/codecov/shared-archive/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/shared-archive/downloads","issues_url":"https://api.github.com/repos/codecov/shared-archive/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/shared-archive/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/shared-archive/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/shared-archive/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/shared-archive/labels{/name}","releases_url":"https://api.github.com/repos/codecov/shared-archive/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/shared-archive/deployments","created_at":"2020-04-10T22:42:46Z","updated_at":"2023-07-17T19:10:09Z","pushed_at":"2023-07-17T18:46:27Z","git_url":"git://github.com/codecov/shared-archive.git","ssh_url":"git@github.com:codecov/shared-archive.git","clone_url":"https://github.com/codecov/shared-archive.git","svn_url":"https://github.com/codecov/shared-archive","homepage":null,"size":3300,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":true,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":6,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":6,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":683479413,"node_id":"R_kgDOKL0RdQ","name":"shelter","full_name":"codecov/shelter","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/shelter","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/shelter","forks_url":"https://api.github.com/repos/codecov/shelter/forks","keys_url":"https://api.github.com/repos/codecov/shelter/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/shelter/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/shelter/teams","hooks_url":"https://api.github.com/repos/codecov/shelter/hooks","issue_events_url":"https://api.github.com/repos/codecov/shelter/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/shelter/events","assignees_url":"https://api.github.com/repos/codecov/shelter/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/shelter/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/shelter/tags","blobs_url":"https://api.github.com/repos/codecov/shelter/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/shelter/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/shelter/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/shelter/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/shelter/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/shelter/languages","stargazers_url":"https://api.github.com/repos/codecov/shelter/stargazers","contributors_url":"https://api.github.com/repos/codecov/shelter/contributors","subscribers_url":"https://api.github.com/repos/codecov/shelter/subscribers","subscription_url":"https://api.github.com/repos/codecov/shelter/subscription","commits_url":"https://api.github.com/repos/codecov/shelter/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/shelter/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/shelter/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/shelter/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/shelter/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/shelter/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/shelter/merges","archive_url":"https://api.github.com/repos/codecov/shelter/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/shelter/downloads","issues_url":"https://api.github.com/repos/codecov/shelter/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/shelter/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/shelter/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/shelter/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/shelter/labels{/name}","releases_url":"https://api.github.com/repos/codecov/shelter/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/shelter/deployments","created_at":"2023-08-26T17:49:04Z","updated_at":"2023-09-05T14:15:34Z","pushed_at":"2023-09-13T19:07:43Z","git_url":"git://github.com/codecov/shelter.git","ssh_url":"git@github.com:codecov/shelter.git","clone_url":"https://github.com/codecov/shelter.git","svn_url":"https://github.com/codecov/shelter","homepage":null,"size":288,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":32,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":32,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":140897583,"node_id":"MDEwOlJlcG9zaXRvcnkxNDA4OTc1ODM=","name":"sourcegraph-codecov","full_name":"codecov/sourcegraph-codecov","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/sourcegraph-codecov","description":"See + code coverage information from Codecov on GitHub, Sourcegraph, and other tools.","fork":false,"url":"https://api.github.com/repos/codecov/sourcegraph-codecov","forks_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/forks","keys_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/teams","hooks_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/hooks","issue_events_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/events","assignees_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/tags","blobs_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/languages","stargazers_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/stargazers","contributors_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/contributors","subscribers_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/subscribers","subscription_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/subscription","commits_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/merges","archive_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/downloads","issues_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/labels{/name}","releases_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/sourcegraph-codecov/deployments","created_at":"2018-07-13T22:21:05Z","updated_at":"2023-05-28T01:46:33Z","pushed_at":"2023-08-01T15:34:28Z","git_url":"git://github.com/codecov/sourcegraph-codecov.git","ssh_url":"git@github.com:codecov/sourcegraph-codecov.git","clone_url":"https://github.com/codecov/sourcegraph-codecov.git","svn_url":"https://github.com/codecov/sourcegraph-codecov","homepage":"","size":1489,"stargazers_count":74,"watchers_count":74,"language":"TypeScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":69,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":69,"open_issues":0,"watchers":74,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":false,"triage":false,"pull":true}},{"id":194928953,"node_id":"MDEwOlJlcG9zaXRvcnkxOTQ5Mjg5NTM=","name":"standards","full_name":"codecov/standards","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/standards","description":"List + of Codecov language standards ","fork":false,"url":"https://api.github.com/repos/codecov/standards","forks_url":"https://api.github.com/repos/codecov/standards/forks","keys_url":"https://api.github.com/repos/codecov/standards/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/standards/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/standards/teams","hooks_url":"https://api.github.com/repos/codecov/standards/hooks","issue_events_url":"https://api.github.com/repos/codecov/standards/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/standards/events","assignees_url":"https://api.github.com/repos/codecov/standards/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/standards/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/standards/tags","blobs_url":"https://api.github.com/repos/codecov/standards/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/standards/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/standards/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/standards/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/standards/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/standards/languages","stargazers_url":"https://api.github.com/repos/codecov/standards/stargazers","contributors_url":"https://api.github.com/repos/codecov/standards/contributors","subscribers_url":"https://api.github.com/repos/codecov/standards/subscribers","subscription_url":"https://api.github.com/repos/codecov/standards/subscription","commits_url":"https://api.github.com/repos/codecov/standards/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/standards/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/standards/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/standards/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/standards/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/standards/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/standards/merges","archive_url":"https://api.github.com/repos/codecov/standards/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/standards/downloads","issues_url":"https://api.github.com/repos/codecov/standards/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/standards/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/standards/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/standards/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/standards/labels{/name}","releases_url":"https://api.github.com/repos/codecov/standards/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/standards/deployments","created_at":"2019-07-02T20:16:31Z","updated_at":"2023-08-13T18:53:38Z","pushed_at":"2023-09-16T05:14:17Z","git_url":"git://github.com/codecov/standards.git","ssh_url":"git@github.com:codecov/standards.git","clone_url":"https://github.com/codecov/standards.git","svn_url":"https://github.com/codecov/standards","homepage":"","size":6931,"stargazers_count":8,"watchers_count":8,"language":"C++","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":9,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":9,"open_issues":2,"watchers":8,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":196071888,"node_id":"MDEwOlJlcG9zaXRvcnkxOTYwNzE4ODg=","name":"swift-standard","full_name":"codecov/swift-standard","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/swift-standard","description":"Codecov + coverage standard for Swift","fork":false,"url":"https://api.github.com/repos/codecov/swift-standard","forks_url":"https://api.github.com/repos/codecov/swift-standard/forks","keys_url":"https://api.github.com/repos/codecov/swift-standard/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/swift-standard/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/swift-standard/teams","hooks_url":"https://api.github.com/repos/codecov/swift-standard/hooks","issue_events_url":"https://api.github.com/repos/codecov/swift-standard/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/swift-standard/events","assignees_url":"https://api.github.com/repos/codecov/swift-standard/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/swift-standard/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/swift-standard/tags","blobs_url":"https://api.github.com/repos/codecov/swift-standard/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/swift-standard/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/swift-standard/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/swift-standard/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/swift-standard/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/swift-standard/languages","stargazers_url":"https://api.github.com/repos/codecov/swift-standard/stargazers","contributors_url":"https://api.github.com/repos/codecov/swift-standard/contributors","subscribers_url":"https://api.github.com/repos/codecov/swift-standard/subscribers","subscription_url":"https://api.github.com/repos/codecov/swift-standard/subscription","commits_url":"https://api.github.com/repos/codecov/swift-standard/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/swift-standard/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/swift-standard/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/swift-standard/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/swift-standard/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/swift-standard/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/swift-standard/merges","archive_url":"https://api.github.com/repos/codecov/swift-standard/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/swift-standard/downloads","issues_url":"https://api.github.com/repos/codecov/swift-standard/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/swift-standard/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/swift-standard/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/swift-standard/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/swift-standard/labels{/name}","releases_url":"https://api.github.com/repos/codecov/swift-standard/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/swift-standard/deployments","created_at":"2019-07-09T19:35:18Z","updated_at":"2023-07-25T14:27:19Z","pushed_at":"2023-09-16T05:09:01Z","git_url":"git://github.com/codecov/swift-standard.git","ssh_url":"git@github.com:codecov/swift-standard.git","clone_url":"https://github.com/codecov/swift-standard.git","svn_url":"https://github.com/codecov/swift-standard","homepage":"","size":475,"stargazers_count":17,"watchers_count":17,"language":"Swift","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":11,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["codecov","coverage-reports","swift","xcode11"],"visibility":"public","forks":11,"open_issues":1,"watchers":17,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":225705568,"node_id":"MDEwOlJlcG9zaXRvcnkyMjU3MDU1Njg=","name":"terraform","full_name":"codecov/terraform","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/terraform","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/terraform","forks_url":"https://api.github.com/repos/codecov/terraform/forks","keys_url":"https://api.github.com/repos/codecov/terraform/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/terraform/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/terraform/teams","hooks_url":"https://api.github.com/repos/codecov/terraform/hooks","issue_events_url":"https://api.github.com/repos/codecov/terraform/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/terraform/events","assignees_url":"https://api.github.com/repos/codecov/terraform/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/terraform/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/terraform/tags","blobs_url":"https://api.github.com/repos/codecov/terraform/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/terraform/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/terraform/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/terraform/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/terraform/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/terraform/languages","stargazers_url":"https://api.github.com/repos/codecov/terraform/stargazers","contributors_url":"https://api.github.com/repos/codecov/terraform/contributors","subscribers_url":"https://api.github.com/repos/codecov/terraform/subscribers","subscription_url":"https://api.github.com/repos/codecov/terraform/subscription","commits_url":"https://api.github.com/repos/codecov/terraform/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/terraform/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/terraform/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/terraform/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/terraform/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/terraform/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/terraform/merges","archive_url":"https://api.github.com/repos/codecov/terraform/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/terraform/downloads","issues_url":"https://api.github.com/repos/codecov/terraform/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/terraform/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/terraform/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/terraform/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/terraform/labels{/name}","releases_url":"https://api.github.com/repos/codecov/terraform/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/terraform/deployments","created_at":"2019-12-03T20:06:41Z","updated_at":"2023-04-24T18:15:49Z","pushed_at":"2023-09-21T19:39:29Z","git_url":"git://github.com/codecov/terraform.git","ssh_url":"git@github.com:codecov/terraform.git","clone_url":"https://github.com/codecov/terraform.git","svn_url":"https://github.com/codecov/terraform","homepage":null,"size":4686,"stargazers_count":1,"watchers_count":1,"language":"HCL","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":12,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":12,"watchers":1,"default_branch":"develop","permissions":{"admin":false,"maintain":false,"push":false,"triage":false,"pull":true}},{"id":7615485,"node_id":"MDEwOlJlcG9zaXRvcnk3NjE1NDg1","name":"timestring","full_name":"codecov/timestring","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/timestring","description":"Making + time easier since \"Jan 17th, 2013 at 3:59pm\"","fork":false,"url":"https://api.github.com/repos/codecov/timestring","forks_url":"https://api.github.com/repos/codecov/timestring/forks","keys_url":"https://api.github.com/repos/codecov/timestring/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/timestring/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/timestring/teams","hooks_url":"https://api.github.com/repos/codecov/timestring/hooks","issue_events_url":"https://api.github.com/repos/codecov/timestring/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/timestring/events","assignees_url":"https://api.github.com/repos/codecov/timestring/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/timestring/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/timestring/tags","blobs_url":"https://api.github.com/repos/codecov/timestring/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/timestring/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/timestring/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/timestring/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/timestring/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/timestring/languages","stargazers_url":"https://api.github.com/repos/codecov/timestring/stargazers","contributors_url":"https://api.github.com/repos/codecov/timestring/contributors","subscribers_url":"https://api.github.com/repos/codecov/timestring/subscribers","subscription_url":"https://api.github.com/repos/codecov/timestring/subscription","commits_url":"https://api.github.com/repos/codecov/timestring/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/timestring/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/timestring/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/timestring/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/timestring/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/timestring/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/timestring/merges","archive_url":"https://api.github.com/repos/codecov/timestring/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/timestring/downloads","issues_url":"https://api.github.com/repos/codecov/timestring/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/timestring/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/timestring/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/timestring/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/timestring/labels{/name}","releases_url":"https://api.github.com/repos/codecov/timestring/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/timestring/deployments","created_at":"2013-01-15T00:13:44Z","updated_at":"2023-09-13T16:37:00Z","pushed_at":"2020-09-21T17:14:52Z","git_url":"git://github.com/codecov/timestring.git","ssh_url":"git@github.com:codecov/timestring.git","clone_url":"https://github.com/codecov/timestring.git","svn_url":"https://github.com/codecov/timestring","homepage":"","size":147,"stargazers_count":102,"watchers_count":102,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":26,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":1,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":26,"open_issues":1,"watchers":102,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":46487935,"node_id":"MDEwOlJlcG9zaXRvcnk0NjQ4NzkzNQ==","name":"torngit","full_name":"codecov/torngit","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/torngit","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/torngit","forks_url":"https://api.github.com/repos/codecov/torngit/forks","keys_url":"https://api.github.com/repos/codecov/torngit/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/torngit/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/torngit/teams","hooks_url":"https://api.github.com/repos/codecov/torngit/hooks","issue_events_url":"https://api.github.com/repos/codecov/torngit/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/torngit/events","assignees_url":"https://api.github.com/repos/codecov/torngit/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/torngit/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/torngit/tags","blobs_url":"https://api.github.com/repos/codecov/torngit/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/torngit/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/torngit/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/torngit/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/torngit/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/torngit/languages","stargazers_url":"https://api.github.com/repos/codecov/torngit/stargazers","contributors_url":"https://api.github.com/repos/codecov/torngit/contributors","subscribers_url":"https://api.github.com/repos/codecov/torngit/subscribers","subscription_url":"https://api.github.com/repos/codecov/torngit/subscription","commits_url":"https://api.github.com/repos/codecov/torngit/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/torngit/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/torngit/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/torngit/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/torngit/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/torngit/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/torngit/merges","archive_url":"https://api.github.com/repos/codecov/torngit/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/torngit/downloads","issues_url":"https://api.github.com/repos/codecov/torngit/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/torngit/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/torngit/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/torngit/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/torngit/labels{/name}","releases_url":"https://api.github.com/repos/codecov/torngit/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/torngit/deployments","created_at":"2015-11-19T11:26:42Z","updated_at":"2022-01-10T21:22:07Z","pushed_at":"2022-01-10T21:22:03Z","git_url":"git://github.com/codecov/torngit.git","ssh_url":"git@github.com:codecov/torngit.git","clone_url":"https://github.com/codecov/torngit.git","svn_url":"https://github.com/codecov/torngit","homepage":null,"size":929,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":148645401,"node_id":"MDEwOlJlcG9zaXRvcnkxNDg2NDU0MDE=","name":"torngit-sync","full_name":"codecov/torngit-sync","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/torngit-sync","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/torngit-sync","forks_url":"https://api.github.com/repos/codecov/torngit-sync/forks","keys_url":"https://api.github.com/repos/codecov/torngit-sync/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/torngit-sync/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/torngit-sync/teams","hooks_url":"https://api.github.com/repos/codecov/torngit-sync/hooks","issue_events_url":"https://api.github.com/repos/codecov/torngit-sync/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/torngit-sync/events","assignees_url":"https://api.github.com/repos/codecov/torngit-sync/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/torngit-sync/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/torngit-sync/tags","blobs_url":"https://api.github.com/repos/codecov/torngit-sync/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/torngit-sync/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/torngit-sync/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/torngit-sync/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/torngit-sync/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/torngit-sync/languages","stargazers_url":"https://api.github.com/repos/codecov/torngit-sync/stargazers","contributors_url":"https://api.github.com/repos/codecov/torngit-sync/contributors","subscribers_url":"https://api.github.com/repos/codecov/torngit-sync/subscribers","subscription_url":"https://api.github.com/repos/codecov/torngit-sync/subscription","commits_url":"https://api.github.com/repos/codecov/torngit-sync/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/torngit-sync/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/torngit-sync/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/torngit-sync/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/torngit-sync/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/torngit-sync/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/torngit-sync/merges","archive_url":"https://api.github.com/repos/codecov/torngit-sync/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/torngit-sync/downloads","issues_url":"https://api.github.com/repos/codecov/torngit-sync/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/torngit-sync/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/torngit-sync/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/torngit-sync/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/torngit-sync/labels{/name}","releases_url":"https://api.github.com/repos/codecov/torngit-sync/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/torngit-sync/deployments","created_at":"2018-09-13T13:54:58Z","updated_at":"2018-09-13T13:56:46Z","pushed_at":"2018-09-13T13:56:34Z","git_url":"git://github.com/codecov/torngit-sync.git","ssh_url":"git@github.com:codecov/torngit-sync.git","clone_url":"https://github.com/codecov/torngit-sync.git","svn_url":"https://github.com/codecov/torngit-sync","homepage":null,"size":333,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":210706185,"node_id":"MDEwOlJlcG9zaXRvcnkyMTA3MDYxODU=","name":"tornpsql","full_name":"codecov/tornpsql","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/tornpsql","description":"Simple + PostgreSQL wrapper","fork":true,"url":"https://api.github.com/repos/codecov/tornpsql","forks_url":"https://api.github.com/repos/codecov/tornpsql/forks","keys_url":"https://api.github.com/repos/codecov/tornpsql/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/tornpsql/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/tornpsql/teams","hooks_url":"https://api.github.com/repos/codecov/tornpsql/hooks","issue_events_url":"https://api.github.com/repos/codecov/tornpsql/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/tornpsql/events","assignees_url":"https://api.github.com/repos/codecov/tornpsql/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/tornpsql/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/tornpsql/tags","blobs_url":"https://api.github.com/repos/codecov/tornpsql/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/tornpsql/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/tornpsql/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/tornpsql/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/tornpsql/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/tornpsql/languages","stargazers_url":"https://api.github.com/repos/codecov/tornpsql/stargazers","contributors_url":"https://api.github.com/repos/codecov/tornpsql/contributors","subscribers_url":"https://api.github.com/repos/codecov/tornpsql/subscribers","subscription_url":"https://api.github.com/repos/codecov/tornpsql/subscription","commits_url":"https://api.github.com/repos/codecov/tornpsql/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/tornpsql/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/tornpsql/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/tornpsql/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/tornpsql/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/tornpsql/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/tornpsql/merges","archive_url":"https://api.github.com/repos/codecov/tornpsql/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/tornpsql/downloads","issues_url":"https://api.github.com/repos/codecov/tornpsql/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/tornpsql/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/tornpsql/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/tornpsql/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/tornpsql/labels{/name}","releases_url":"https://api.github.com/repos/codecov/tornpsql/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/tornpsql/deployments","created_at":"2019-09-24T22:10:47Z","updated_at":"2023-07-25T14:29:16Z","pushed_at":"2020-04-16T15:01:42Z","git_url":"git://github.com/codecov/tornpsql.git","ssh_url":"git@github.com:codecov/tornpsql.git","clone_url":"https://github.com/codecov/tornpsql.git","svn_url":"https://github.com/codecov/tornpsql","homepage":"","size":177,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":3,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":194693245,"node_id":"MDEwOlJlcG9zaXRvcnkxOTQ2OTMyNDU=","name":"tornwrap","full_name":"codecov/tornwrap","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/tornwrap","description":"awesome + tornado plugins and decorators","fork":true,"url":"https://api.github.com/repos/codecov/tornwrap","forks_url":"https://api.github.com/repos/codecov/tornwrap/forks","keys_url":"https://api.github.com/repos/codecov/tornwrap/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/tornwrap/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/tornwrap/teams","hooks_url":"https://api.github.com/repos/codecov/tornwrap/hooks","issue_events_url":"https://api.github.com/repos/codecov/tornwrap/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/tornwrap/events","assignees_url":"https://api.github.com/repos/codecov/tornwrap/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/tornwrap/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/tornwrap/tags","blobs_url":"https://api.github.com/repos/codecov/tornwrap/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/tornwrap/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/tornwrap/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/tornwrap/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/tornwrap/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/tornwrap/languages","stargazers_url":"https://api.github.com/repos/codecov/tornwrap/stargazers","contributors_url":"https://api.github.com/repos/codecov/tornwrap/contributors","subscribers_url":"https://api.github.com/repos/codecov/tornwrap/subscribers","subscription_url":"https://api.github.com/repos/codecov/tornwrap/subscription","commits_url":"https://api.github.com/repos/codecov/tornwrap/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/tornwrap/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/tornwrap/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/tornwrap/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/tornwrap/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/tornwrap/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/tornwrap/merges","archive_url":"https://api.github.com/repos/codecov/tornwrap/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/tornwrap/downloads","issues_url":"https://api.github.com/repos/codecov/tornwrap/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/tornwrap/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/tornwrap/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/tornwrap/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/tornwrap/labels{/name}","releases_url":"https://api.github.com/repos/codecov/tornwrap/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/tornwrap/deployments","created_at":"2019-07-01T14:53:10Z","updated_at":"2023-07-25T14:27:08Z","pushed_at":"2020-09-03T15:57:20Z","git_url":"git://github.com/codecov/tornwrap.git","ssh_url":"git@github.com:codecov/tornwrap.git","clone_url":"https://github.com/codecov/tornwrap.git","svn_url":"https://github.com/codecov/tornwrap","homepage":"","size":140,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":3,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":354077097,"node_id":"MDEwOlJlcG9zaXRvcnkzNTQwNzcwOTc=","name":"typeahead-component","full_name":"codecov/typeahead-component","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/typeahead-component","description":"Typeahead + search component for Codecov header","fork":false,"url":"https://api.github.com/repos/codecov/typeahead-component","forks_url":"https://api.github.com/repos/codecov/typeahead-component/forks","keys_url":"https://api.github.com/repos/codecov/typeahead-component/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/typeahead-component/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/typeahead-component/teams","hooks_url":"https://api.github.com/repos/codecov/typeahead-component/hooks","issue_events_url":"https://api.github.com/repos/codecov/typeahead-component/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/typeahead-component/events","assignees_url":"https://api.github.com/repos/codecov/typeahead-component/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/typeahead-component/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/typeahead-component/tags","blobs_url":"https://api.github.com/repos/codecov/typeahead-component/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/typeahead-component/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/typeahead-component/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/typeahead-component/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/typeahead-component/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/typeahead-component/languages","stargazers_url":"https://api.github.com/repos/codecov/typeahead-component/stargazers","contributors_url":"https://api.github.com/repos/codecov/typeahead-component/contributors","subscribers_url":"https://api.github.com/repos/codecov/typeahead-component/subscribers","subscription_url":"https://api.github.com/repos/codecov/typeahead-component/subscription","commits_url":"https://api.github.com/repos/codecov/typeahead-component/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/typeahead-component/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/typeahead-component/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/typeahead-component/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/typeahead-component/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/typeahead-component/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/typeahead-component/merges","archive_url":"https://api.github.com/repos/codecov/typeahead-component/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/typeahead-component/downloads","issues_url":"https://api.github.com/repos/codecov/typeahead-component/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/typeahead-component/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/typeahead-component/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/typeahead-component/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/typeahead-component/labels{/name}","releases_url":"https://api.github.com/repos/codecov/typeahead-component/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/typeahead-component/deployments","created_at":"2021-04-02T16:43:24Z","updated_at":"2021-04-02T16:43:27Z","pushed_at":"2021-04-12T19:37:31Z","git_url":"git://github.com/codecov/typeahead-component.git","ssh_url":"git@github.com:codecov/typeahead-component.git","clone_url":"https://github.com/codecov/typeahead-component.git","svn_url":"https://github.com/codecov/typeahead-component","homepage":null,"size":1442,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":3,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":3,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":198353745,"node_id":"MDEwOlJlcG9zaXRvcnkxOTgzNTM3NDU=","name":"typescript-standard","full_name":"codecov/typescript-standard","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/typescript-standard","description":"Codecov + coverage standard for TypeScript","fork":false,"url":"https://api.github.com/repos/codecov/typescript-standard","forks_url":"https://api.github.com/repos/codecov/typescript-standard/forks","keys_url":"https://api.github.com/repos/codecov/typescript-standard/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/typescript-standard/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/typescript-standard/teams","hooks_url":"https://api.github.com/repos/codecov/typescript-standard/hooks","issue_events_url":"https://api.github.com/repos/codecov/typescript-standard/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/typescript-standard/events","assignees_url":"https://api.github.com/repos/codecov/typescript-standard/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/typescript-standard/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/typescript-standard/tags","blobs_url":"https://api.github.com/repos/codecov/typescript-standard/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/typescript-standard/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/typescript-standard/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/typescript-standard/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/typescript-standard/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/typescript-standard/languages","stargazers_url":"https://api.github.com/repos/codecov/typescript-standard/stargazers","contributors_url":"https://api.github.com/repos/codecov/typescript-standard/contributors","subscribers_url":"https://api.github.com/repos/codecov/typescript-standard/subscribers","subscription_url":"https://api.github.com/repos/codecov/typescript-standard/subscription","commits_url":"https://api.github.com/repos/codecov/typescript-standard/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/typescript-standard/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/typescript-standard/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/typescript-standard/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/typescript-standard/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/typescript-standard/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/typescript-standard/merges","archive_url":"https://api.github.com/repos/codecov/typescript-standard/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/typescript-standard/downloads","issues_url":"https://api.github.com/repos/codecov/typescript-standard/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/typescript-standard/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/typescript-standard/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/typescript-standard/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/typescript-standard/labels{/name}","releases_url":"https://api.github.com/repos/codecov/typescript-standard/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/typescript-standard/deployments","created_at":"2019-07-23T04:47:07Z","updated_at":"2023-08-05T04:45:58Z","pushed_at":"2023-09-16T05:05:43Z","git_url":"git://github.com/codecov/typescript-standard.git","ssh_url":"git@github.com:codecov/typescript-standard.git","clone_url":"https://github.com/codecov/typescript-standard.git","svn_url":"https://github.com/codecov/typescript-standard","homepage":"","size":9445,"stargazers_count":9,"watchers_count":9,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":20,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["codecov","coverage-reports","typescript"],"visibility":"public","forks":20,"open_issues":0,"watchers":9,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":204233902,"node_id":"MDEwOlJlcG9zaXRvcnkyMDQyMzM5MDI=","name":"umbrella","full_name":"codecov/umbrella","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/umbrella","description":"My + small test on getting a different setup","fork":false,"url":"https://api.github.com/repos/codecov/umbrella","forks_url":"https://api.github.com/repos/codecov/umbrella/forks","keys_url":"https://api.github.com/repos/codecov/umbrella/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/umbrella/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/umbrella/teams","hooks_url":"https://api.github.com/repos/codecov/umbrella/hooks","issue_events_url":"https://api.github.com/repos/codecov/umbrella/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/umbrella/events","assignees_url":"https://api.github.com/repos/codecov/umbrella/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/umbrella/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/umbrella/tags","blobs_url":"https://api.github.com/repos/codecov/umbrella/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/umbrella/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/umbrella/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/umbrella/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/umbrella/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/umbrella/languages","stargazers_url":"https://api.github.com/repos/codecov/umbrella/stargazers","contributors_url":"https://api.github.com/repos/codecov/umbrella/contributors","subscribers_url":"https://api.github.com/repos/codecov/umbrella/subscribers","subscription_url":"https://api.github.com/repos/codecov/umbrella/subscription","commits_url":"https://api.github.com/repos/codecov/umbrella/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/umbrella/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/umbrella/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/umbrella/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/umbrella/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/umbrella/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/umbrella/merges","archive_url":"https://api.github.com/repos/codecov/umbrella/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/umbrella/downloads","issues_url":"https://api.github.com/repos/codecov/umbrella/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/umbrella/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/umbrella/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/umbrella/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/umbrella/labels{/name}","releases_url":"https://api.github.com/repos/codecov/umbrella/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/umbrella/deployments","created_at":"2019-08-25T02:00:17Z","updated_at":"2022-04-05T14:29:11Z","pushed_at":"2022-10-31T13:38:37Z","git_url":"git://github.com/codecov/umbrella.git","ssh_url":"git@github.com:codecov/umbrella.git","clone_url":"https://github.com/codecov/umbrella.git","svn_url":"https://github.com/codecov/umbrella","homepage":null,"size":10,"stargazers_count":1,"watchers_count":1,"language":"Makefile","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":4,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":4,"watchers":1,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":673886250,"node_id":"R_kgDOKCqwKg","name":"umbrella-hat","full_name":"codecov/umbrella-hat","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/umbrella-hat","description":"Developer + environment resources like git hooks, `docker-compose.yml`s, and helper scripts","fork":false,"url":"https://api.github.com/repos/codecov/umbrella-hat","forks_url":"https://api.github.com/repos/codecov/umbrella-hat/forks","keys_url":"https://api.github.com/repos/codecov/umbrella-hat/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/umbrella-hat/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/umbrella-hat/teams","hooks_url":"https://api.github.com/repos/codecov/umbrella-hat/hooks","issue_events_url":"https://api.github.com/repos/codecov/umbrella-hat/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/umbrella-hat/events","assignees_url":"https://api.github.com/repos/codecov/umbrella-hat/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/umbrella-hat/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/umbrella-hat/tags","blobs_url":"https://api.github.com/repos/codecov/umbrella-hat/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/umbrella-hat/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/umbrella-hat/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/umbrella-hat/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/umbrella-hat/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/umbrella-hat/languages","stargazers_url":"https://api.github.com/repos/codecov/umbrella-hat/stargazers","contributors_url":"https://api.github.com/repos/codecov/umbrella-hat/contributors","subscribers_url":"https://api.github.com/repos/codecov/umbrella-hat/subscribers","subscription_url":"https://api.github.com/repos/codecov/umbrella-hat/subscription","commits_url":"https://api.github.com/repos/codecov/umbrella-hat/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/umbrella-hat/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/umbrella-hat/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/umbrella-hat/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/umbrella-hat/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/umbrella-hat/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/umbrella-hat/merges","archive_url":"https://api.github.com/repos/codecov/umbrella-hat/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/umbrella-hat/downloads","issues_url":"https://api.github.com/repos/codecov/umbrella-hat/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/umbrella-hat/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/umbrella-hat/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/umbrella-hat/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/umbrella-hat/labels{/name}","releases_url":"https://api.github.com/repos/codecov/umbrella-hat/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/umbrella-hat/deployments","created_at":"2023-08-02T16:40:21Z","updated_at":"2023-08-22T21:22:00Z","pushed_at":"2023-09-20T16:01:12Z","git_url":"git://github.com/codecov/umbrella-hat.git","ssh_url":"git@github.com:codecov/umbrella-hat.git","clone_url":"https://github.com/codecov/umbrella-hat.git","svn_url":"https://github.com/codecov/umbrella-hat","homepage":null,"size":19,"stargazers_count":1,"watchers_count":1,"language":"Shell","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":9,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":9,"watchers":1,"default_branch":"main","permissions":{"admin":true,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":223286392,"node_id":"MDEwOlJlcG9zaXRvcnkyMjMyODYzOTI=","name":"uploader","full_name":"codecov/uploader","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/uploader","description":"Codecov''s + universal binary uploader.","fork":false,"url":"https://api.github.com/repos/codecov/uploader","forks_url":"https://api.github.com/repos/codecov/uploader/forks","keys_url":"https://api.github.com/repos/codecov/uploader/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/uploader/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/uploader/teams","hooks_url":"https://api.github.com/repos/codecov/uploader/hooks","issue_events_url":"https://api.github.com/repos/codecov/uploader/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/uploader/events","assignees_url":"https://api.github.com/repos/codecov/uploader/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/uploader/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/uploader/tags","blobs_url":"https://api.github.com/repos/codecov/uploader/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/uploader/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/uploader/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/uploader/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/uploader/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/uploader/languages","stargazers_url":"https://api.github.com/repos/codecov/uploader/stargazers","contributors_url":"https://api.github.com/repos/codecov/uploader/contributors","subscribers_url":"https://api.github.com/repos/codecov/uploader/subscribers","subscription_url":"https://api.github.com/repos/codecov/uploader/subscription","commits_url":"https://api.github.com/repos/codecov/uploader/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/uploader/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/uploader/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/uploader/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/uploader/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/uploader/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/uploader/merges","archive_url":"https://api.github.com/repos/codecov/uploader/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/uploader/downloads","issues_url":"https://api.github.com/repos/codecov/uploader/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/uploader/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/uploader/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/uploader/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/uploader/labels{/name}","releases_url":"https://api.github.com/repos/codecov/uploader/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/uploader/deployments","created_at":"2019-11-21T23:51:13Z","updated_at":"2023-08-18T17:02:01Z","pushed_at":"2023-09-22T18:32:40Z","git_url":"git://github.com/codecov/uploader.git","ssh_url":"git@github.com:codecov/uploader.git","clone_url":"https://github.com/codecov/uploader.git","svn_url":"https://github.com/codecov/uploader","homepage":"https://docs.codecov.com/docs/codecov-uploader","size":5792,"stargazers_count":96,"watchers_count":96,"language":"TypeScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":82,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":36,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["hacktoberfest"],"visibility":"public","forks":82,"open_issues":36,"watchers":96,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":644427712,"node_id":"R_kgDOJmkvwA","name":"vscode","full_name":"codecov/vscode","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/vscode","description":"A + Codecov vscode extention which helps validate and configure new repositories.","fork":false,"url":"https://api.github.com/repos/codecov/vscode","forks_url":"https://api.github.com/repos/codecov/vscode/forks","keys_url":"https://api.github.com/repos/codecov/vscode/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/vscode/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/vscode/teams","hooks_url":"https://api.github.com/repos/codecov/vscode/hooks","issue_events_url":"https://api.github.com/repos/codecov/vscode/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/vscode/events","assignees_url":"https://api.github.com/repos/codecov/vscode/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/vscode/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/vscode/tags","blobs_url":"https://api.github.com/repos/codecov/vscode/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/vscode/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/vscode/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/vscode/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/vscode/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/vscode/languages","stargazers_url":"https://api.github.com/repos/codecov/vscode/stargazers","contributors_url":"https://api.github.com/repos/codecov/vscode/contributors","subscribers_url":"https://api.github.com/repos/codecov/vscode/subscribers","subscription_url":"https://api.github.com/repos/codecov/vscode/subscription","commits_url":"https://api.github.com/repos/codecov/vscode/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/vscode/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/vscode/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/vscode/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/vscode/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/vscode/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/vscode/merges","archive_url":"https://api.github.com/repos/codecov/vscode/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/vscode/downloads","issues_url":"https://api.github.com/repos/codecov/vscode/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/vscode/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/vscode/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/vscode/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/vscode/labels{/name}","releases_url":"https://api.github.com/repos/codecov/vscode/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/vscode/deployments","created_at":"2023-05-23T13:49:29Z","updated_at":"2023-06-23T22:26:55Z","pushed_at":"2023-08-28T19:27:09Z","git_url":"git://github.com/codecov/vscode.git","ssh_url":"git@github.com:codecov/vscode.git","clone_url":"https://github.com/codecov/vscode.git","svn_url":"https://github.com/codecov/vscode","homepage":null,"size":3865,"stargazers_count":3,"watchers_count":3,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":2,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":7,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":2,"open_issues":7,"watchers":3,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":665728948,"node_id":"R_kgDOJ643tA","name":"worker","full_name":"codecov/worker","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/worker","description":"Code + for Background Workers of Codecov","fork":false,"url":"https://api.github.com/repos/codecov/worker","forks_url":"https://api.github.com/repos/codecov/worker/forks","keys_url":"https://api.github.com/repos/codecov/worker/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/worker/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/worker/teams","hooks_url":"https://api.github.com/repos/codecov/worker/hooks","issue_events_url":"https://api.github.com/repos/codecov/worker/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/worker/events","assignees_url":"https://api.github.com/repos/codecov/worker/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/worker/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/worker/tags","blobs_url":"https://api.github.com/repos/codecov/worker/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/worker/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/worker/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/worker/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/worker/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/worker/languages","stargazers_url":"https://api.github.com/repos/codecov/worker/stargazers","contributors_url":"https://api.github.com/repos/codecov/worker/contributors","subscribers_url":"https://api.github.com/repos/codecov/worker/subscribers","subscription_url":"https://api.github.com/repos/codecov/worker/subscription","commits_url":"https://api.github.com/repos/codecov/worker/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/worker/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/worker/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/worker/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/worker/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/worker/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/worker/merges","archive_url":"https://api.github.com/repos/codecov/worker/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/worker/downloads","issues_url":"https://api.github.com/repos/codecov/worker/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/worker/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/worker/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/worker/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/worker/labels{/name}","releases_url":"https://api.github.com/repos/codecov/worker/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/worker/deployments","created_at":"2023-07-12T21:38:48Z","updated_at":"2023-09-02T12:15:24Z","pushed_at":"2023-09-22T11:49:53Z","git_url":"git://github.com/codecov/worker.git","ssh_url":"git@github.com:codecov/worker.git","clone_url":"https://github.com/codecov/worker.git","svn_url":"https://github.com/codecov/worker","homepage":"","size":4228,"stargazers_count":42,"watchers_count":42,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":13,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":3,"open_issues":13,"watchers":42,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":157271496,"node_id":"MDEwOlJlcG9zaXRvcnkxNTcyNzE0OTY=","name":"worker-archive","full_name":"codecov/worker-archive","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/worker-archive","description":"Code + for Background Workers of Codecov","fork":false,"url":"https://api.github.com/repos/codecov/worker-archive","forks_url":"https://api.github.com/repos/codecov/worker-archive/forks","keys_url":"https://api.github.com/repos/codecov/worker-archive/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/worker-archive/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/worker-archive/teams","hooks_url":"https://api.github.com/repos/codecov/worker-archive/hooks","issue_events_url":"https://api.github.com/repos/codecov/worker-archive/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/worker-archive/events","assignees_url":"https://api.github.com/repos/codecov/worker-archive/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/worker-archive/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/worker-archive/tags","blobs_url":"https://api.github.com/repos/codecov/worker-archive/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/worker-archive/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/worker-archive/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/worker-archive/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/worker-archive/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/worker-archive/languages","stargazers_url":"https://api.github.com/repos/codecov/worker-archive/stargazers","contributors_url":"https://api.github.com/repos/codecov/worker-archive/contributors","subscribers_url":"https://api.github.com/repos/codecov/worker-archive/subscribers","subscription_url":"https://api.github.com/repos/codecov/worker-archive/subscription","commits_url":"https://api.github.com/repos/codecov/worker-archive/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/worker-archive/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/worker-archive/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/worker-archive/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/worker-archive/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/worker-archive/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/worker-archive/merges","archive_url":"https://api.github.com/repos/codecov/worker-archive/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/worker-archive/downloads","issues_url":"https://api.github.com/repos/codecov/worker-archive/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/worker-archive/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/worker-archive/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/worker-archive/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/worker-archive/labels{/name}","releases_url":"https://api.github.com/repos/codecov/worker-archive/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/worker-archive/deployments","created_at":"2018-11-12T20:20:58Z","updated_at":"2023-07-21T17:01:35Z","pushed_at":"2023-07-12T20:41:55Z","git_url":"git://github.com/codecov/worker-archive.git","ssh_url":"git@github.com:codecov/worker-archive.git","clone_url":"https://github.com/codecov/worker-archive.git","svn_url":"https://github.com/codecov/worker-archive","homepage":null,"size":5991,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":true,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":12,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":12,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":670770179,"node_id":"R_kgDOJ_skAw","name":"rust","full_name":"matt-codecov/rust","private":false,"owner":{"login":"matt-codecov","id":137832199,"node_id":"U_kgDOCDcnBw","avatar_url":"https://avatars.githubusercontent.com/u/137832199?v=4","gravatar_id":"","url":"https://api.github.com/users/matt-codecov","html_url":"https://github.com/matt-codecov","followers_url":"https://api.github.com/users/matt-codecov/followers","following_url":"https://api.github.com/users/matt-codecov/following{/other_user}","gists_url":"https://api.github.com/users/matt-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/matt-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/matt-codecov/subscriptions","organizations_url":"https://api.github.com/users/matt-codecov/orgs","repos_url":"https://api.github.com/users/matt-codecov/repos","events_url":"https://api.github.com/users/matt-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/matt-codecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/matt-codecov/rust","description":"Empowering + everyone to build reliable and efficient software.","fork":true,"url":"https://api.github.com/repos/matt-codecov/rust","forks_url":"https://api.github.com/repos/matt-codecov/rust/forks","keys_url":"https://api.github.com/repos/matt-codecov/rust/keys{/key_id}","collaborators_url":"https://api.github.com/repos/matt-codecov/rust/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/matt-codecov/rust/teams","hooks_url":"https://api.github.com/repos/matt-codecov/rust/hooks","issue_events_url":"https://api.github.com/repos/matt-codecov/rust/issues/events{/number}","events_url":"https://api.github.com/repos/matt-codecov/rust/events","assignees_url":"https://api.github.com/repos/matt-codecov/rust/assignees{/user}","branches_url":"https://api.github.com/repos/matt-codecov/rust/branches{/branch}","tags_url":"https://api.github.com/repos/matt-codecov/rust/tags","blobs_url":"https://api.github.com/repos/matt-codecov/rust/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/matt-codecov/rust/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/matt-codecov/rust/git/refs{/sha}","trees_url":"https://api.github.com/repos/matt-codecov/rust/git/trees{/sha}","statuses_url":"https://api.github.com/repos/matt-codecov/rust/statuses/{sha}","languages_url":"https://api.github.com/repos/matt-codecov/rust/languages","stargazers_url":"https://api.github.com/repos/matt-codecov/rust/stargazers","contributors_url":"https://api.github.com/repos/matt-codecov/rust/contributors","subscribers_url":"https://api.github.com/repos/matt-codecov/rust/subscribers","subscription_url":"https://api.github.com/repos/matt-codecov/rust/subscription","commits_url":"https://api.github.com/repos/matt-codecov/rust/commits{/sha}","git_commits_url":"https://api.github.com/repos/matt-codecov/rust/git/commits{/sha}","comments_url":"https://api.github.com/repos/matt-codecov/rust/comments{/number}","issue_comment_url":"https://api.github.com/repos/matt-codecov/rust/issues/comments{/number}","contents_url":"https://api.github.com/repos/matt-codecov/rust/contents/{+path}","compare_url":"https://api.github.com/repos/matt-codecov/rust/compare/{base}...{head}","merges_url":"https://api.github.com/repos/matt-codecov/rust/merges","archive_url":"https://api.github.com/repos/matt-codecov/rust/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/matt-codecov/rust/downloads","issues_url":"https://api.github.com/repos/matt-codecov/rust/issues{/number}","pulls_url":"https://api.github.com/repos/matt-codecov/rust/pulls{/number}","milestones_url":"https://api.github.com/repos/matt-codecov/rust/milestones{/number}","notifications_url":"https://api.github.com/repos/matt-codecov/rust/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/matt-codecov/rust/labels{/name}","releases_url":"https://api.github.com/repos/matt-codecov/rust/releases{/id}","deployments_url":"https://api.github.com/repos/matt-codecov/rust/deployments","created_at":"2023-07-25T19:51:26Z","updated_at":"2023-07-25T19:51:26Z","pushed_at":"2023-05-06T20:26:50Z","git_url":"git://github.com/matt-codecov/rust.git","ssh_url":"git@github.com:matt-codecov/rust.git","clone_url":"https://github.com/matt-codecov/rust.git","svn_url":"https://github.com/matt-codecov/rust","homepage":"https://www.rust-lang.org","size":914880,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":true,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":673480981,"node_id":"R_kgDOKCSBFQ","name":"sentry-docs","full_name":"matt-codecov/sentry-docs","private":false,"owner":{"login":"matt-codecov","id":137832199,"node_id":"U_kgDOCDcnBw","avatar_url":"https://avatars.githubusercontent.com/u/137832199?v=4","gravatar_id":"","url":"https://api.github.com/users/matt-codecov","html_url":"https://github.com/matt-codecov","followers_url":"https://api.github.com/users/matt-codecov/followers","following_url":"https://api.github.com/users/matt-codecov/following{/other_user}","gists_url":"https://api.github.com/users/matt-codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/matt-codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/matt-codecov/subscriptions","organizations_url":"https://api.github.com/users/matt-codecov/orgs","repos_url":"https://api.github.com/users/matt-codecov/repos","events_url":"https://api.github.com/users/matt-codecov/events{/privacy}","received_events_url":"https://api.github.com/users/matt-codecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/matt-codecov/sentry-docs","description":"Sentry''s + documentation (and tools to build it)","fork":true,"url":"https://api.github.com/repos/matt-codecov/sentry-docs","forks_url":"https://api.github.com/repos/matt-codecov/sentry-docs/forks","keys_url":"https://api.github.com/repos/matt-codecov/sentry-docs/keys{/key_id}","collaborators_url":"https://api.github.com/repos/matt-codecov/sentry-docs/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/matt-codecov/sentry-docs/teams","hooks_url":"https://api.github.com/repos/matt-codecov/sentry-docs/hooks","issue_events_url":"https://api.github.com/repos/matt-codecov/sentry-docs/issues/events{/number}","events_url":"https://api.github.com/repos/matt-codecov/sentry-docs/events","assignees_url":"https://api.github.com/repos/matt-codecov/sentry-docs/assignees{/user}","branches_url":"https://api.github.com/repos/matt-codecov/sentry-docs/branches{/branch}","tags_url":"https://api.github.com/repos/matt-codecov/sentry-docs/tags","blobs_url":"https://api.github.com/repos/matt-codecov/sentry-docs/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/matt-codecov/sentry-docs/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/matt-codecov/sentry-docs/git/refs{/sha}","trees_url":"https://api.github.com/repos/matt-codecov/sentry-docs/git/trees{/sha}","statuses_url":"https://api.github.com/repos/matt-codecov/sentry-docs/statuses/{sha}","languages_url":"https://api.github.com/repos/matt-codecov/sentry-docs/languages","stargazers_url":"https://api.github.com/repos/matt-codecov/sentry-docs/stargazers","contributors_url":"https://api.github.com/repos/matt-codecov/sentry-docs/contributors","subscribers_url":"https://api.github.com/repos/matt-codecov/sentry-docs/subscribers","subscription_url":"https://api.github.com/repos/matt-codecov/sentry-docs/subscription","commits_url":"https://api.github.com/repos/matt-codecov/sentry-docs/commits{/sha}","git_commits_url":"https://api.github.com/repos/matt-codecov/sentry-docs/git/commits{/sha}","comments_url":"https://api.github.com/repos/matt-codecov/sentry-docs/comments{/number}","issue_comment_url":"https://api.github.com/repos/matt-codecov/sentry-docs/issues/comments{/number}","contents_url":"https://api.github.com/repos/matt-codecov/sentry-docs/contents/{+path}","compare_url":"https://api.github.com/repos/matt-codecov/sentry-docs/compare/{base}...{head}","merges_url":"https://api.github.com/repos/matt-codecov/sentry-docs/merges","archive_url":"https://api.github.com/repos/matt-codecov/sentry-docs/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/matt-codecov/sentry-docs/downloads","issues_url":"https://api.github.com/repos/matt-codecov/sentry-docs/issues{/number}","pulls_url":"https://api.github.com/repos/matt-codecov/sentry-docs/pulls{/number}","milestones_url":"https://api.github.com/repos/matt-codecov/sentry-docs/milestones{/number}","notifications_url":"https://api.github.com/repos/matt-codecov/sentry-docs/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/matt-codecov/sentry-docs/labels{/name}","releases_url":"https://api.github.com/repos/matt-codecov/sentry-docs/releases{/id}","deployments_url":"https://api.github.com/repos/matt-codecov/sentry-docs/deployments","created_at":"2023-08-01T18:11:11Z","updated_at":"2023-08-01T18:11:11Z","pushed_at":"2023-08-01T18:12:47Z","git_url":"git://github.com/matt-codecov/sentry-docs.git","ssh_url":"git@github.com:matt-codecov/sentry-docs.git","clone_url":"https://github.com/matt-codecov/sentry-docs.git","svn_url":"https://github.com/matt-codecov/sentry-docs","homepage":"https://docs.sentry.io","size":334777,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":true,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":665219266,"node_id":"R_kgDOJ6Zwwg","name":"ike","full_name":"matt-codecov-club/ike","private":true,"owner":{"login":"matt-codecov-club","id":139263855,"node_id":"O_kgDOCEz_bw","avatar_url":"https://avatars.githubusercontent.com/u/139263855?v=4","gravatar_id":"","url":"https://api.github.com/users/matt-codecov-club","html_url":"https://github.com/matt-codecov-club","followers_url":"https://api.github.com/users/matt-codecov-club/followers","following_url":"https://api.github.com/users/matt-codecov-club/following{/other_user}","gists_url":"https://api.github.com/users/matt-codecov-club/gists{/gist_id}","starred_url":"https://api.github.com/users/matt-codecov-club/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/matt-codecov-club/subscriptions","organizations_url":"https://api.github.com/users/matt-codecov-club/orgs","repos_url":"https://api.github.com/users/matt-codecov-club/repos","events_url":"https://api.github.com/users/matt-codecov-club/events{/privacy}","received_events_url":"https://api.github.com/users/matt-codecov-club/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/matt-codecov-club/ike","description":null,"fork":false,"url":"https://api.github.com/repos/matt-codecov-club/ike","forks_url":"https://api.github.com/repos/matt-codecov-club/ike/forks","keys_url":"https://api.github.com/repos/matt-codecov-club/ike/keys{/key_id}","collaborators_url":"https://api.github.com/repos/matt-codecov-club/ike/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/matt-codecov-club/ike/teams","hooks_url":"https://api.github.com/repos/matt-codecov-club/ike/hooks","issue_events_url":"https://api.github.com/repos/matt-codecov-club/ike/issues/events{/number}","events_url":"https://api.github.com/repos/matt-codecov-club/ike/events","assignees_url":"https://api.github.com/repos/matt-codecov-club/ike/assignees{/user}","branches_url":"https://api.github.com/repos/matt-codecov-club/ike/branches{/branch}","tags_url":"https://api.github.com/repos/matt-codecov-club/ike/tags","blobs_url":"https://api.github.com/repos/matt-codecov-club/ike/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/matt-codecov-club/ike/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/matt-codecov-club/ike/git/refs{/sha}","trees_url":"https://api.github.com/repos/matt-codecov-club/ike/git/trees{/sha}","statuses_url":"https://api.github.com/repos/matt-codecov-club/ike/statuses/{sha}","languages_url":"https://api.github.com/repos/matt-codecov-club/ike/languages","stargazers_url":"https://api.github.com/repos/matt-codecov-club/ike/stargazers","contributors_url":"https://api.github.com/repos/matt-codecov-club/ike/contributors","subscribers_url":"https://api.github.com/repos/matt-codecov-club/ike/subscribers","subscription_url":"https://api.github.com/repos/matt-codecov-club/ike/subscription","commits_url":"https://api.github.com/repos/matt-codecov-club/ike/commits{/sha}","git_commits_url":"https://api.github.com/repos/matt-codecov-club/ike/git/commits{/sha}","comments_url":"https://api.github.com/repos/matt-codecov-club/ike/comments{/number}","issue_comment_url":"https://api.github.com/repos/matt-codecov-club/ike/issues/comments{/number}","contents_url":"https://api.github.com/repos/matt-codecov-club/ike/contents/{+path}","compare_url":"https://api.github.com/repos/matt-codecov-club/ike/compare/{base}...{head}","merges_url":"https://api.github.com/repos/matt-codecov-club/ike/merges","archive_url":"https://api.github.com/repos/matt-codecov-club/ike/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/matt-codecov-club/ike/downloads","issues_url":"https://api.github.com/repos/matt-codecov-club/ike/issues{/number}","pulls_url":"https://api.github.com/repos/matt-codecov-club/ike/pulls{/number}","milestones_url":"https://api.github.com/repos/matt-codecov-club/ike/milestones{/number}","notifications_url":"https://api.github.com/repos/matt-codecov-club/ike/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/matt-codecov-club/ike/labels{/name}","releases_url":"https://api.github.com/repos/matt-codecov-club/ike/releases{/id}","deployments_url":"https://api.github.com/repos/matt-codecov-club/ike/deployments","created_at":"2023-07-11T17:52:35Z","updated_at":"2023-07-11T17:52:36Z","pushed_at":"2023-07-11T17:52:36Z","git_url":"git://github.com/matt-codecov-club/ike.git","ssh_url":"git@github.com:matt-codecov-club/ike.git","clone_url":"https://github.com/matt-codecov-club/ike.git","svn_url":"https://github.com/matt-codecov-club/ike","homepage":null,"size":0,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":false,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":true,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":665219192,"node_id":"R_kgDOJ6ZweA","name":"mike","full_name":"matt-codecov-club/mike","private":true,"owner":{"login":"matt-codecov-club","id":139263855,"node_id":"O_kgDOCEz_bw","avatar_url":"https://avatars.githubusercontent.com/u/139263855?v=4","gravatar_id":"","url":"https://api.github.com/users/matt-codecov-club","html_url":"https://github.com/matt-codecov-club","followers_url":"https://api.github.com/users/matt-codecov-club/followers","following_url":"https://api.github.com/users/matt-codecov-club/following{/other_user}","gists_url":"https://api.github.com/users/matt-codecov-club/gists{/gist_id}","starred_url":"https://api.github.com/users/matt-codecov-club/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/matt-codecov-club/subscriptions","organizations_url":"https://api.github.com/users/matt-codecov-club/orgs","repos_url":"https://api.github.com/users/matt-codecov-club/repos","events_url":"https://api.github.com/users/matt-codecov-club/events{/privacy}","received_events_url":"https://api.github.com/users/matt-codecov-club/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/matt-codecov-club/mike","description":null,"fork":false,"url":"https://api.github.com/repos/matt-codecov-club/mike","forks_url":"https://api.github.com/repos/matt-codecov-club/mike/forks","keys_url":"https://api.github.com/repos/matt-codecov-club/mike/keys{/key_id}","collaborators_url":"https://api.github.com/repos/matt-codecov-club/mike/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/matt-codecov-club/mike/teams","hooks_url":"https://api.github.com/repos/matt-codecov-club/mike/hooks","issue_events_url":"https://api.github.com/repos/matt-codecov-club/mike/issues/events{/number}","events_url":"https://api.github.com/repos/matt-codecov-club/mike/events","assignees_url":"https://api.github.com/repos/matt-codecov-club/mike/assignees{/user}","branches_url":"https://api.github.com/repos/matt-codecov-club/mike/branches{/branch}","tags_url":"https://api.github.com/repos/matt-codecov-club/mike/tags","blobs_url":"https://api.github.com/repos/matt-codecov-club/mike/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/matt-codecov-club/mike/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/matt-codecov-club/mike/git/refs{/sha}","trees_url":"https://api.github.com/repos/matt-codecov-club/mike/git/trees{/sha}","statuses_url":"https://api.github.com/repos/matt-codecov-club/mike/statuses/{sha}","languages_url":"https://api.github.com/repos/matt-codecov-club/mike/languages","stargazers_url":"https://api.github.com/repos/matt-codecov-club/mike/stargazers","contributors_url":"https://api.github.com/repos/matt-codecov-club/mike/contributors","subscribers_url":"https://api.github.com/repos/matt-codecov-club/mike/subscribers","subscription_url":"https://api.github.com/repos/matt-codecov-club/mike/subscription","commits_url":"https://api.github.com/repos/matt-codecov-club/mike/commits{/sha}","git_commits_url":"https://api.github.com/repos/matt-codecov-club/mike/git/commits{/sha}","comments_url":"https://api.github.com/repos/matt-codecov-club/mike/comments{/number}","issue_comment_url":"https://api.github.com/repos/matt-codecov-club/mike/issues/comments{/number}","contents_url":"https://api.github.com/repos/matt-codecov-club/mike/contents/{+path}","compare_url":"https://api.github.com/repos/matt-codecov-club/mike/compare/{base}...{head}","merges_url":"https://api.github.com/repos/matt-codecov-club/mike/merges","archive_url":"https://api.github.com/repos/matt-codecov-club/mike/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/matt-codecov-club/mike/downloads","issues_url":"https://api.github.com/repos/matt-codecov-club/mike/issues{/number}","pulls_url":"https://api.github.com/repos/matt-codecov-club/mike/pulls{/number}","milestones_url":"https://api.github.com/repos/matt-codecov-club/mike/milestones{/number}","notifications_url":"https://api.github.com/repos/matt-codecov-club/mike/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/matt-codecov-club/mike/labels{/name}","releases_url":"https://api.github.com/repos/matt-codecov-club/mike/releases{/id}","deployments_url":"https://api.github.com/repos/matt-codecov-club/mike/deployments","created_at":"2023-07-11T17:52:21Z","updated_at":"2023-07-11T17:52:22Z","pushed_at":"2023-07-11T17:52:21Z","git_url":"git://github.com/matt-codecov-club/mike.git","ssh_url":"git@github.com:matt-codecov-club/mike.git","clone_url":"https://github.com/matt-codecov-club/mike.git","svn_url":"https://github.com/matt-codecov-club/mike","homepage":null,"size":0,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":false,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":true,"maintain":true,"push":true,"triage":true,"pull":true}}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 22 Sep 2023 18:36:11 GMT + ETag: + - W/"64069367d0c222b38ed3ddc4f70eb5d2854fc587a39d0f78ed04970e9239e1fa" + Link: + - ; rel="prev", ; + rel="first" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - F050:31FD:36D428E:6F9F1CC:650DDE9A + X-OAuth-Scopes: + - admin:org, repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1695411371' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-23 22:46:17 UTC + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/user/repos?page=1&per_page=50 + response: + content: '[{"id":239903643,"node_id":"MDEwOlJlcG9zaXRvcnkyMzk5MDM2NDM=","name":"analytics","full_name":"codecov/analytics","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/analytics","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/analytics","forks_url":"https://api.github.com/repos/codecov/analytics/forks","keys_url":"https://api.github.com/repos/codecov/analytics/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/analytics/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/analytics/teams","hooks_url":"https://api.github.com/repos/codecov/analytics/hooks","issue_events_url":"https://api.github.com/repos/codecov/analytics/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/analytics/events","assignees_url":"https://api.github.com/repos/codecov/analytics/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/analytics/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/analytics/tags","blobs_url":"https://api.github.com/repos/codecov/analytics/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/analytics/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/analytics/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/analytics/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/analytics/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/analytics/languages","stargazers_url":"https://api.github.com/repos/codecov/analytics/stargazers","contributors_url":"https://api.github.com/repos/codecov/analytics/contributors","subscribers_url":"https://api.github.com/repos/codecov/analytics/subscribers","subscription_url":"https://api.github.com/repos/codecov/analytics/subscription","commits_url":"https://api.github.com/repos/codecov/analytics/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/analytics/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/analytics/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/analytics/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/analytics/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/analytics/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/analytics/merges","archive_url":"https://api.github.com/repos/codecov/analytics/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/analytics/downloads","issues_url":"https://api.github.com/repos/codecov/analytics/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/analytics/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/analytics/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/analytics/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/analytics/labels{/name}","releases_url":"https://api.github.com/repos/codecov/analytics/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/analytics/deployments","created_at":"2020-02-12T01:43:15Z","updated_at":"2021-02-04T17:37:04Z","pushed_at":"2021-02-04T17:39:39Z","git_url":"git://github.com/codecov/analytics.git","ssh_url":"git@github.com:codecov/analytics.git","clone_url":"https://github.com/codecov/analytics.git","svn_url":"https://github.com/codecov/analytics","homepage":null,"size":10,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":666096231,"node_id":"R_kgDOJ7PSZw","name":"applications-team","full_name":"codecov/applications-team","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/applications-team","description":"Applications + team repo for project management ","fork":false,"url":"https://api.github.com/repos/codecov/applications-team","forks_url":"https://api.github.com/repos/codecov/applications-team/forks","keys_url":"https://api.github.com/repos/codecov/applications-team/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/applications-team/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/applications-team/teams","hooks_url":"https://api.github.com/repos/codecov/applications-team/hooks","issue_events_url":"https://api.github.com/repos/codecov/applications-team/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/applications-team/events","assignees_url":"https://api.github.com/repos/codecov/applications-team/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/applications-team/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/applications-team/tags","blobs_url":"https://api.github.com/repos/codecov/applications-team/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/applications-team/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/applications-team/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/applications-team/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/applications-team/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/applications-team/languages","stargazers_url":"https://api.github.com/repos/codecov/applications-team/stargazers","contributors_url":"https://api.github.com/repos/codecov/applications-team/contributors","subscribers_url":"https://api.github.com/repos/codecov/applications-team/subscribers","subscription_url":"https://api.github.com/repos/codecov/applications-team/subscription","commits_url":"https://api.github.com/repos/codecov/applications-team/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/applications-team/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/applications-team/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/applications-team/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/applications-team/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/applications-team/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/applications-team/merges","archive_url":"https://api.github.com/repos/codecov/applications-team/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/applications-team/downloads","issues_url":"https://api.github.com/repos/codecov/applications-team/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/applications-team/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/applications-team/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/applications-team/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/applications-team/labels{/name}","releases_url":"https://api.github.com/repos/codecov/applications-team/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/applications-team/deployments","created_at":"2023-07-13T17:45:48Z","updated_at":"2023-08-09T14:56:50Z","pushed_at":"2023-07-13T17:45:48Z","git_url":"git://github.com/codecov/applications-team.git","ssh_url":"git@github.com:codecov/applications-team.git","clone_url":"https://github.com/codecov/applications-team.git","svn_url":"https://github.com/codecov/applications-team","homepage":null,"size":0,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":286876237,"node_id":"MDEwOlJlcG9zaXRvcnkyODY4NzYyMzc=","name":"autotest","full_name":"codecov/autotest","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/autotest","description":"A + project for automated testing.","fork":false,"url":"https://api.github.com/repos/codecov/autotest","forks_url":"https://api.github.com/repos/codecov/autotest/forks","keys_url":"https://api.github.com/repos/codecov/autotest/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/autotest/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/autotest/teams","hooks_url":"https://api.github.com/repos/codecov/autotest/hooks","issue_events_url":"https://api.github.com/repos/codecov/autotest/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/autotest/events","assignees_url":"https://api.github.com/repos/codecov/autotest/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/autotest/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/autotest/tags","blobs_url":"https://api.github.com/repos/codecov/autotest/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/autotest/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/autotest/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/autotest/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/autotest/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/autotest/languages","stargazers_url":"https://api.github.com/repos/codecov/autotest/stargazers","contributors_url":"https://api.github.com/repos/codecov/autotest/contributors","subscribers_url":"https://api.github.com/repos/codecov/autotest/subscribers","subscription_url":"https://api.github.com/repos/codecov/autotest/subscription","commits_url":"https://api.github.com/repos/codecov/autotest/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/autotest/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/autotest/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/autotest/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/autotest/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/autotest/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/autotest/merges","archive_url":"https://api.github.com/repos/codecov/autotest/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/autotest/downloads","issues_url":"https://api.github.com/repos/codecov/autotest/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/autotest/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/autotest/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/autotest/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/autotest/labels{/name}","releases_url":"https://api.github.com/repos/codecov/autotest/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/autotest/deployments","created_at":"2020-08-12T00:21:36Z","updated_at":"2022-01-03T14:28:11Z","pushed_at":"2022-08-02T18:00:18Z","git_url":"git://github.com/codecov/autotest.git","ssh_url":"git@github.com:codecov/autotest.git","clone_url":"https://github.com/codecov/autotest.git","svn_url":"https://github.com/codecov/autotest","homepage":null,"size":313,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":5,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":5,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":666484623,"node_id":"R_kgDOJ7m_jw","name":"brolly","full_name":"codecov/brolly","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/brolly","description":"Beacon + API Service","fork":false,"url":"https://api.github.com/repos/codecov/brolly","forks_url":"https://api.github.com/repos/codecov/brolly/forks","keys_url":"https://api.github.com/repos/codecov/brolly/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/brolly/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/brolly/teams","hooks_url":"https://api.github.com/repos/codecov/brolly/hooks","issue_events_url":"https://api.github.com/repos/codecov/brolly/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/brolly/events","assignees_url":"https://api.github.com/repos/codecov/brolly/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/brolly/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/brolly/tags","blobs_url":"https://api.github.com/repos/codecov/brolly/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/brolly/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/brolly/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/brolly/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/brolly/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/brolly/languages","stargazers_url":"https://api.github.com/repos/codecov/brolly/stargazers","contributors_url":"https://api.github.com/repos/codecov/brolly/contributors","subscribers_url":"https://api.github.com/repos/codecov/brolly/subscribers","subscription_url":"https://api.github.com/repos/codecov/brolly/subscription","commits_url":"https://api.github.com/repos/codecov/brolly/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/brolly/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/brolly/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/brolly/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/brolly/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/brolly/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/brolly/merges","archive_url":"https://api.github.com/repos/codecov/brolly/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/brolly/downloads","issues_url":"https://api.github.com/repos/codecov/brolly/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/brolly/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/brolly/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/brolly/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/brolly/labels{/name}","releases_url":"https://api.github.com/repos/codecov/brolly/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/brolly/deployments","created_at":"2023-07-14T16:29:31Z","updated_at":"2023-07-14T16:52:19Z","pushed_at":"2023-09-11T23:02:03Z","git_url":"git://github.com/codecov/brolly.git","ssh_url":"git@github.com:codecov/brolly.git","clone_url":"https://github.com/codecov/brolly.git","svn_url":"https://github.com/codecov/brolly","homepage":null,"size":361,"stargazers_count":0,"watchers_count":0,"language":"PHP","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":186653620,"node_id":"MDEwOlJlcG9zaXRvcnkxODY2NTM2MjA=","name":"candidate-exercises","full_name":"codecov/candidate-exercises","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/candidate-exercises","description":"A + repository of all exercises, code tests, etc used to vet the applicability of + job candidates","fork":false,"url":"https://api.github.com/repos/codecov/candidate-exercises","forks_url":"https://api.github.com/repos/codecov/candidate-exercises/forks","keys_url":"https://api.github.com/repos/codecov/candidate-exercises/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/candidate-exercises/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/candidate-exercises/teams","hooks_url":"https://api.github.com/repos/codecov/candidate-exercises/hooks","issue_events_url":"https://api.github.com/repos/codecov/candidate-exercises/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/candidate-exercises/events","assignees_url":"https://api.github.com/repos/codecov/candidate-exercises/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/candidate-exercises/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/candidate-exercises/tags","blobs_url":"https://api.github.com/repos/codecov/candidate-exercises/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/candidate-exercises/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/candidate-exercises/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/candidate-exercises/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/candidate-exercises/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/candidate-exercises/languages","stargazers_url":"https://api.github.com/repos/codecov/candidate-exercises/stargazers","contributors_url":"https://api.github.com/repos/codecov/candidate-exercises/contributors","subscribers_url":"https://api.github.com/repos/codecov/candidate-exercises/subscribers","subscription_url":"https://api.github.com/repos/codecov/candidate-exercises/subscription","commits_url":"https://api.github.com/repos/codecov/candidate-exercises/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/candidate-exercises/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/candidate-exercises/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/candidate-exercises/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/candidate-exercises/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/candidate-exercises/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/candidate-exercises/merges","archive_url":"https://api.github.com/repos/codecov/candidate-exercises/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/candidate-exercises/downloads","issues_url":"https://api.github.com/repos/codecov/candidate-exercises/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/candidate-exercises/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/candidate-exercises/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/candidate-exercises/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/candidate-exercises/labels{/name}","releases_url":"https://api.github.com/repos/codecov/candidate-exercises/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/candidate-exercises/deployments","created_at":"2019-05-14T15:47:13Z","updated_at":"2019-06-18T21:55:59Z","pushed_at":"2019-06-18T21:55:57Z","git_url":"git://github.com/codecov/candidate-exercises.git","ssh_url":"git@github.com:codecov/candidate-exercises.git","clone_url":"https://github.com/codecov/candidate-exercises.git","svn_url":"https://github.com/codecov/candidate-exercises","homepage":null,"size":10,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":82936397,"node_id":"MDEwOlJlcG9zaXRvcnk4MjkzNjM5Nw==","name":"cc-process-coverage","full_name":"codecov/cc-process-coverage","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/cc-process-coverage","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/cc-process-coverage","forks_url":"https://api.github.com/repos/codecov/cc-process-coverage/forks","keys_url":"https://api.github.com/repos/codecov/cc-process-coverage/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/cc-process-coverage/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/cc-process-coverage/teams","hooks_url":"https://api.github.com/repos/codecov/cc-process-coverage/hooks","issue_events_url":"https://api.github.com/repos/codecov/cc-process-coverage/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/cc-process-coverage/events","assignees_url":"https://api.github.com/repos/codecov/cc-process-coverage/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/cc-process-coverage/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/cc-process-coverage/tags","blobs_url":"https://api.github.com/repos/codecov/cc-process-coverage/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/cc-process-coverage/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/cc-process-coverage/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/cc-process-coverage/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/cc-process-coverage/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/cc-process-coverage/languages","stargazers_url":"https://api.github.com/repos/codecov/cc-process-coverage/stargazers","contributors_url":"https://api.github.com/repos/codecov/cc-process-coverage/contributors","subscribers_url":"https://api.github.com/repos/codecov/cc-process-coverage/subscribers","subscription_url":"https://api.github.com/repos/codecov/cc-process-coverage/subscription","commits_url":"https://api.github.com/repos/codecov/cc-process-coverage/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/cc-process-coverage/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/cc-process-coverage/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/cc-process-coverage/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/cc-process-coverage/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/cc-process-coverage/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/cc-process-coverage/merges","archive_url":"https://api.github.com/repos/codecov/cc-process-coverage/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/cc-process-coverage/downloads","issues_url":"https://api.github.com/repos/codecov/cc-process-coverage/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/cc-process-coverage/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/cc-process-coverage/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/cc-process-coverage/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/cc-process-coverage/labels{/name}","releases_url":"https://api.github.com/repos/codecov/cc-process-coverage/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/cc-process-coverage/deployments","created_at":"2017-02-23T14:41:39Z","updated_at":"2023-01-10T22:43:34Z","pushed_at":"2018-09-01T23:22:04Z","git_url":"git://github.com/codecov/cc-process-coverage.git","ssh_url":"git@github.com:codecov/cc-process-coverage.git","clone_url":"https://github.com/codecov/cc-process-coverage.git","svn_url":"https://github.com/codecov/cc-process-coverage","homepage":null,"size":33314,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":3,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":3,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":667550736,"node_id":"R_kgDOJ8oEEA","name":"codecov-api","full_name":"codecov/codecov-api","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-api","description":"Code + for the API of Codecov","fork":false,"url":"https://api.github.com/repos/codecov/codecov-api","forks_url":"https://api.github.com/repos/codecov/codecov-api/forks","keys_url":"https://api.github.com/repos/codecov/codecov-api/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-api/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-api/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-api/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-api/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-api/events","assignees_url":"https://api.github.com/repos/codecov/codecov-api/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-api/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-api/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-api/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-api/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-api/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-api/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-api/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-api/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-api/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-api/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-api/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-api/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-api/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-api/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-api/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-api/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-api/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-api/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-api/merges","archive_url":"https://api.github.com/repos/codecov/codecov-api/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-api/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-api/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-api/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-api/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-api/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-api/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-api/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-api/deployments","created_at":"2023-07-17T19:06:58Z","updated_at":"2023-09-16T20:24:43Z","pushed_at":"2023-09-22T17:39:16Z","git_url":"git://github.com/codecov/codecov-api.git","ssh_url":"git@github.com:codecov/codecov-api.git","clone_url":"https://github.com/codecov/codecov-api.git","svn_url":"https://github.com/codecov/codecov-api","homepage":null,"size":27667,"stargazers_count":192,"watchers_count":192,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":16,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":12,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":16,"open_issues":12,"watchers":192,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":160537716,"node_id":"MDEwOlJlcG9zaXRvcnkxNjA1Mzc3MTY=","name":"codecov-api-archive","full_name":"codecov/codecov-api-archive","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-api-archive","description":"Code + for new API of Codecov","fork":false,"url":"https://api.github.com/repos/codecov/codecov-api-archive","forks_url":"https://api.github.com/repos/codecov/codecov-api-archive/forks","keys_url":"https://api.github.com/repos/codecov/codecov-api-archive/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-api-archive/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-api-archive/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-api-archive/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-api-archive/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-api-archive/events","assignees_url":"https://api.github.com/repos/codecov/codecov-api-archive/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-api-archive/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-api-archive/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-api-archive/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-api-archive/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-api-archive/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-api-archive/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-api-archive/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-api-archive/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-api-archive/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-api-archive/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-api-archive/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-api-archive/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-api-archive/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-api-archive/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-api-archive/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-api-archive/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-api-archive/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-api-archive/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-api-archive/merges","archive_url":"https://api.github.com/repos/codecov/codecov-api-archive/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-api-archive/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-api-archive/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-api-archive/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-api-archive/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-api-archive/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-api-archive/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-api-archive/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-api-archive/deployments","created_at":"2018-12-05T15:21:09Z","updated_at":"2023-07-17T19:06:23Z","pushed_at":"2023-07-17T20:00:53Z","git_url":"git://github.com/codecov/codecov-api-archive.git","ssh_url":"git@github.com:codecov/codecov-api-archive.git","clone_url":"https://github.com/codecov/codecov-api-archive.git","svn_url":"https://github.com/codecov/codecov-api-archive","homepage":null,"size":30781,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":26,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":26,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":192778030,"node_id":"MDEwOlJlcG9zaXRvcnkxOTI3NzgwMzA=","name":"codecov-app","full_name":"codecov/codecov-app","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-app","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/codecov-app","forks_url":"https://api.github.com/repos/codecov/codecov-app/forks","keys_url":"https://api.github.com/repos/codecov/codecov-app/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-app/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-app/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-app/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-app/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-app/events","assignees_url":"https://api.github.com/repos/codecov/codecov-app/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-app/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-app/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-app/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-app/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-app/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-app/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-app/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-app/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-app/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-app/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-app/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-app/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-app/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-app/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-app/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-app/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-app/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-app/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-app/merges","archive_url":"https://api.github.com/repos/codecov/codecov-app/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-app/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-app/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-app/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-app/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-app/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-app/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-app/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-app/deployments","created_at":"2019-06-19T17:38:22Z","updated_at":"2023-01-10T22:27:51Z","pushed_at":"2021-10-27T15:01:27Z","git_url":"git://github.com/codecov/codecov-app.git","ssh_url":"git@github.com:codecov/codecov-app.git","clone_url":"https://github.com/codecov/codecov-app.git","svn_url":"https://github.com/codecov/codecov-app","homepage":null,"size":218,"stargazers_count":0,"watchers_count":0,"language":"Dockerfile","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":160685350,"node_id":"MDEwOlJlcG9zaXRvcnkxNjA2ODUzNTA=","name":"codecov-assume-flag-test","full_name":"codecov/codecov-assume-flag-test","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-assume-flag-test","description":"A + small test repo for codecov assume flags","fork":false,"url":"https://api.github.com/repos/codecov/codecov-assume-flag-test","forks_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/forks","keys_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/events","assignees_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/merges","archive_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-assume-flag-test/deployments","created_at":"2018-12-06T14:21:07Z","updated_at":"2021-12-08T17:23:32Z","pushed_at":"2022-01-26T12:32:33Z","git_url":"git://github.com/codecov/codecov-assume-flag-test.git","ssh_url":"git@github.com:codecov/codecov-assume-flag-test.git","clone_url":"https://github.com/codecov/codecov-assume-flag-test.git","svn_url":"https://github.com/codecov/codecov-assume-flag-test","homepage":null,"size":97,"stargazers_count":1,"watchers_count":1,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":15,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":1,"open_issues":15,"watchers":1,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":34096540,"node_id":"MDEwOlJlcG9zaXRvcnkzNDA5NjU0MA==","name":"codecov-bash","full_name":"codecov/codecov-bash","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-bash","description":"Global + coverage report uploader for Codecov","fork":false,"url":"https://api.github.com/repos/codecov/codecov-bash","forks_url":"https://api.github.com/repos/codecov/codecov-bash/forks","keys_url":"https://api.github.com/repos/codecov/codecov-bash/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-bash/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-bash/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-bash/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-bash/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-bash/events","assignees_url":"https://api.github.com/repos/codecov/codecov-bash/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-bash/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-bash/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-bash/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-bash/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-bash/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-bash/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-bash/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-bash/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-bash/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-bash/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-bash/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-bash/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-bash/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-bash/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-bash/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-bash/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-bash/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-bash/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-bash/merges","archive_url":"https://api.github.com/repos/codecov/codecov-bash/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-bash/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-bash/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-bash/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-bash/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-bash/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-bash/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-bash/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-bash/deployments","created_at":"2015-04-17T04:31:13Z","updated_at":"2023-07-14T06:37:11Z","pushed_at":"2022-01-04T00:08:47Z","git_url":"git://github.com/codecov/codecov-bash.git","ssh_url":"git@github.com:codecov/codecov-bash.git","clone_url":"https://github.com/codecov/codecov-bash.git","svn_url":"https://github.com/codecov/codecov-bash","homepage":"https://codecov.io","size":1183,"stargazers_count":233,"watchers_count":233,"language":"Shell","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":187,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":22,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["codecov","coverage","shell"],"visibility":"public","forks":187,"open_issues":22,"watchers":233,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":293862691,"node_id":"MDEwOlJlcG9zaXRvcnkyOTM4NjI2OTE=","name":"codecov-bitrise","full_name":"codecov/codecov-bitrise","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-bitrise","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/codecov-bitrise","forks_url":"https://api.github.com/repos/codecov/codecov-bitrise/forks","keys_url":"https://api.github.com/repos/codecov/codecov-bitrise/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-bitrise/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-bitrise/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-bitrise/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-bitrise/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-bitrise/events","assignees_url":"https://api.github.com/repos/codecov/codecov-bitrise/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-bitrise/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-bitrise/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-bitrise/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-bitrise/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-bitrise/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-bitrise/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-bitrise/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-bitrise/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-bitrise/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-bitrise/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-bitrise/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-bitrise/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-bitrise/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-bitrise/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-bitrise/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-bitrise/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-bitrise/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-bitrise/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-bitrise/merges","archive_url":"https://api.github.com/repos/codecov/codecov-bitrise/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-bitrise/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-bitrise/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-bitrise/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-bitrise/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-bitrise/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-bitrise/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-bitrise/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-bitrise/deployments","created_at":"2020-09-08T16:11:22Z","updated_at":"2023-07-25T14:39:05Z","pushed_at":"2023-09-16T16:28:55Z","git_url":"git://github.com/codecov/codecov-bitrise.git","ssh_url":"git@github.com:codecov/codecov-bitrise.git","clone_url":"https://github.com/codecov/codecov-bitrise.git","svn_url":"https://github.com/codecov/codecov-bitrise","homepage":null,"size":86,"stargazers_count":1,"watchers_count":1,"language":"Shell","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":2,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":2,"open_issues":2,"watchers":1,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":157147600,"node_id":"MDEwOlJlcG9zaXRvcnkxNTcxNDc2MDA=","name":"codecov-circleci-orb","full_name":"codecov/codecov-circleci-orb","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-circleci-orb","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/codecov-circleci-orb","forks_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/forks","keys_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/events","assignees_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/merges","archive_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-circleci-orb/deployments","created_at":"2018-11-12T02:50:24Z","updated_at":"2023-09-16T04:45:05Z","pushed_at":"2023-09-21T09:48:24Z","git_url":"git://github.com/codecov/codecov-circleci-orb.git","ssh_url":"git@github.com:codecov/codecov-circleci-orb.git","clone_url":"https://github.com/codecov/codecov-circleci-orb.git","svn_url":"https://github.com/codecov/codecov-circleci-orb","homepage":null,"size":406,"stargazers_count":17,"watchers_count":17,"language":"Shell","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":40,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":6,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":40,"open_issues":6,"watchers":17,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":460565350,"node_id":"R_kgDOG3OrZg","name":"codecov-cli","full_name":"codecov/codecov-cli","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-cli","description":"Codecov''s + Command Line Interface. Used for uploading to Codecov in your CI, Test Labelling, + Local Upload, and more","fork":false,"url":"https://api.github.com/repos/codecov/codecov-cli","forks_url":"https://api.github.com/repos/codecov/codecov-cli/forks","keys_url":"https://api.github.com/repos/codecov/codecov-cli/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-cli/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-cli/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-cli/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-cli/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-cli/events","assignees_url":"https://api.github.com/repos/codecov/codecov-cli/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-cli/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-cli/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-cli/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-cli/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-cli/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-cli/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-cli/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-cli/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-cli/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-cli/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-cli/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-cli/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-cli/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-cli/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-cli/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-cli/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-cli/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-cli/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-cli/merges","archive_url":"https://api.github.com/repos/codecov/codecov-cli/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-cli/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-cli/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-cli/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-cli/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-cli/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-cli/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-cli/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-cli/deployments","created_at":"2022-02-17T18:49:00Z","updated_at":"2023-09-12T23:31:35Z","pushed_at":"2023-09-21T13:14:08Z","git_url":"git://github.com/codecov/codecov-cli.git","ssh_url":"git@github.com:codecov/codecov-cli.git","clone_url":"https://github.com/codecov/codecov-cli.git","svn_url":"https://github.com/codecov/codecov-cli","homepage":"","size":1101,"stargazers_count":28,"watchers_count":28,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":9,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":11,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":9,"open_issues":11,"watchers":28,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":169578228,"node_id":"MDEwOlJlcG9zaXRvcnkxNjk1NzgyMjg=","name":"codecov-client","full_name":"codecov/codecov-client","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-client","description":"New + Codecov Client","fork":false,"url":"https://api.github.com/repos/codecov/codecov-client","forks_url":"https://api.github.com/repos/codecov/codecov-client/forks","keys_url":"https://api.github.com/repos/codecov/codecov-client/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-client/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-client/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-client/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-client/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-client/events","assignees_url":"https://api.github.com/repos/codecov/codecov-client/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-client/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-client/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-client/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-client/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-client/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-client/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-client/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-client/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-client/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-client/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-client/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-client/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-client/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-client/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-client/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-client/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-client/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-client/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-client/merges","archive_url":"https://api.github.com/repos/codecov/codecov-client/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-client/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-client/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-client/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-client/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-client/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-client/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-client/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-client/deployments","created_at":"2019-02-07T13:44:46Z","updated_at":"2021-11-30T16:41:28Z","pushed_at":"2023-03-01T07:21:38Z","git_url":"git://github.com/codecov/codecov-client.git","ssh_url":"git@github.com:codecov/codecov-client.git","clone_url":"https://github.com/codecov/codecov-client.git","svn_url":"https://github.com/codecov/codecov-client","homepage":"","size":8487,"stargazers_count":0,"watchers_count":0,"language":"TypeScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":16,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":1,"open_issues":16,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":322453795,"node_id":"MDEwOlJlcG9zaXRvcnkzMjI0NTM3OTU=","name":"codecov-critical-path-loader","full_name":"codecov/codecov-critical-path-loader","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-critical-path-loader","description":"instruments + javascript projects to transmit critical paths","fork":false,"url":"https://api.github.com/repos/codecov/codecov-critical-path-loader","forks_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/forks","keys_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/events","assignees_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/merges","archive_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-critical-path-loader/deployments","created_at":"2020-12-18T01:14:29Z","updated_at":"2023-01-10T22:28:29Z","pushed_at":"2021-03-15T19:27:20Z","git_url":"git://github.com/codecov/codecov-critical-path-loader.git","ssh_url":"git@github.com:codecov/codecov-critical-path-loader.git","clone_url":"https://github.com/codecov/codecov-critical-path-loader.git","svn_url":"https://github.com/codecov/codecov-critical-path-loader","homepage":null,"size":45,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":489084635,"node_id":"R_kgDOHSbW2w","name":"codecov-enterprise-local","full_name":"codecov/codecov-enterprise-local","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-enterprise-local","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/codecov-enterprise-local","forks_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/forks","keys_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/events","assignees_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/merges","archive_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-enterprise-local/deployments","created_at":"2022-05-05T18:27:49Z","updated_at":"2022-05-05T18:31:45Z","pushed_at":"2022-11-16T18:05:53Z","git_url":"git://github.com/codecov/codecov-enterprise-local.git","ssh_url":"git@github.com:codecov/codecov-enterprise-local.git","clone_url":"https://github.com/codecov/codecov-enterprise-local.git","svn_url":"https://github.com/codecov/codecov-enterprise-local","homepage":null,"size":12,"stargazers_count":0,"watchers_count":0,"language":"Makefile","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":79283223,"node_id":"MDEwOlJlcG9zaXRvcnk3OTI4MzIyMw==","name":"codecov-exe","full_name":"codecov/codecov-exe","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-exe","description":".exe + report uploader for Codecov https://codecov.io","fork":false,"url":"https://api.github.com/repos/codecov/codecov-exe","forks_url":"https://api.github.com/repos/codecov/codecov-exe/forks","keys_url":"https://api.github.com/repos/codecov/codecov-exe/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-exe/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-exe/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-exe/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-exe/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-exe/events","assignees_url":"https://api.github.com/repos/codecov/codecov-exe/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-exe/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-exe/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-exe/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-exe/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-exe/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-exe/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-exe/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-exe/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-exe/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-exe/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-exe/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-exe/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-exe/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-exe/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-exe/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-exe/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-exe/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-exe/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-exe/merges","archive_url":"https://api.github.com/repos/codecov/codecov-exe/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-exe/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-exe/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-exe/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-exe/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-exe/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-exe/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-exe/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-exe/deployments","created_at":"2017-01-17T23:27:49Z","updated_at":"2023-07-25T14:06:37Z","pushed_at":"2023-01-06T12:00:46Z","git_url":"git://github.com/codecov/codecov-exe.git","ssh_url":"git@github.com:codecov/codecov-exe.git","clone_url":"https://github.com/codecov/codecov-exe.git","svn_url":"https://github.com/codecov/codecov-exe","homepage":"","size":824,"stargazers_count":25,"watchers_count":25,"language":"C#","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":24,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":13,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["codecov","coverage","powershell"],"visibility":"public","forks":24,"open_issues":13,"watchers":25,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":413604943,"node_id":"R_kgDOGKccTw","name":"codecov-frontend-docker","full_name":"codecov/codecov-frontend-docker","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-frontend-docker","description":"Docker + build for our frontends","fork":false,"url":"https://api.github.com/repos/codecov/codecov-frontend-docker","forks_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/forks","keys_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/events","assignees_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/merges","archive_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-frontend-docker/deployments","created_at":"2021-10-04T22:48:15Z","updated_at":"2022-01-03T19:18:21Z","pushed_at":"2022-12-08T16:17:20Z","git_url":"git://github.com/codecov/codecov-frontend-docker.git","ssh_url":"git@github.com:codecov/codecov-frontend-docker.git","clone_url":"https://github.com/codecov/codecov-frontend-docker.git","svn_url":"https://github.com/codecov/codecov-frontend-docker","homepage":null,"size":99,"stargazers_count":0,"watchers_count":0,"language":"Makefile","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":false,"triage":false,"pull":true}},{"id":469877366,"node_id":"R_kgDOHAHCdg","name":"codecov-gateway","full_name":"codecov/codecov-gateway","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-gateway","description":"Gateway + for self hosted. The brains!","fork":false,"url":"https://api.github.com/repos/codecov/codecov-gateway","forks_url":"https://api.github.com/repos/codecov/codecov-gateway/forks","keys_url":"https://api.github.com/repos/codecov/codecov-gateway/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-gateway/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-gateway/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-gateway/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-gateway/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-gateway/events","assignees_url":"https://api.github.com/repos/codecov/codecov-gateway/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-gateway/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-gateway/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-gateway/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-gateway/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-gateway/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-gateway/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-gateway/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-gateway/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-gateway/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-gateway/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-gateway/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-gateway/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-gateway/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-gateway/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-gateway/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-gateway/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-gateway/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-gateway/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-gateway/merges","archive_url":"https://api.github.com/repos/codecov/codecov-gateway/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-gateway/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-gateway/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-gateway/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-gateway/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-gateway/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-gateway/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-gateway/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-gateway/deployments","created_at":"2022-03-14T19:31:08Z","updated_at":"2023-08-04T09:23:11Z","pushed_at":"2023-09-12T08:34:52Z","git_url":"git://github.com/codecov/codecov-gateway.git","ssh_url":"git@github.com:codecov/codecov-gateway.git","clone_url":"https://github.com/codecov/codecov-gateway.git","svn_url":"https://github.com/codecov/codecov-gateway","homepage":null,"size":154,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":3,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":3,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":224493125,"node_id":"MDEwOlJlcG9zaXRvcnkyMjQ0OTMxMjU=","name":"codecov-interview-api","full_name":"codecov/codecov-interview-api","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-interview-api","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/codecov-interview-api","forks_url":"https://api.github.com/repos/codecov/codecov-interview-api/forks","keys_url":"https://api.github.com/repos/codecov/codecov-interview-api/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-interview-api/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-interview-api/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-interview-api/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-interview-api/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-interview-api/events","assignees_url":"https://api.github.com/repos/codecov/codecov-interview-api/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-interview-api/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-interview-api/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-interview-api/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-interview-api/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-interview-api/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-interview-api/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-interview-api/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-interview-api/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-interview-api/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-interview-api/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-interview-api/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-interview-api/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-interview-api/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-interview-api/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-interview-api/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-interview-api/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-interview-api/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-interview-api/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-interview-api/merges","archive_url":"https://api.github.com/repos/codecov/codecov-interview-api/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-interview-api/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-interview-api/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-interview-api/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-interview-api/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-interview-api/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-interview-api/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-interview-api/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-interview-api/deployments","created_at":"2019-11-27T18:28:29Z","updated_at":"2023-03-09T11:03:54Z","pushed_at":"2023-08-01T15:35:33Z","git_url":"git://github.com/codecov/codecov-interview-api.git","ssh_url":"git@github.com:codecov/codecov-interview-api.git","clone_url":"https://github.com/codecov/codecov-interview-api.git","svn_url":"https://github.com/codecov/codecov-interview-api","homepage":null,"size":1030,"stargazers_count":1,"watchers_count":1,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":2,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":2,"open_issues":0,"watchers":1,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":143443068,"node_id":"MDEwOlJlcG9zaXRvcnkxNDM0NDMwNjg=","name":"codecov-marketing","full_name":"codecov/codecov-marketing","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-marketing","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/codecov-marketing","forks_url":"https://api.github.com/repos/codecov/codecov-marketing/forks","keys_url":"https://api.github.com/repos/codecov/codecov-marketing/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-marketing/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-marketing/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-marketing/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-marketing/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-marketing/events","assignees_url":"https://api.github.com/repos/codecov/codecov-marketing/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-marketing/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-marketing/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-marketing/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-marketing/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-marketing/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-marketing/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-marketing/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-marketing/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-marketing/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-marketing/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-marketing/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-marketing/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-marketing/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-marketing/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-marketing/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-marketing/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-marketing/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-marketing/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-marketing/merges","archive_url":"https://api.github.com/repos/codecov/codecov-marketing/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-marketing/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-marketing/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-marketing/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-marketing/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-marketing/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-marketing/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-marketing/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-marketing/deployments","created_at":"2018-08-03T15:19:49Z","updated_at":"2023-01-10T22:29:09Z","pushed_at":"2020-11-11T17:55:18Z","git_url":"git://github.com/codecov/codecov-marketing.git","ssh_url":"git@github.com:codecov/codecov-marketing.git","clone_url":"https://github.com/codecov/codecov-marketing.git","svn_url":"https://github.com/codecov/codecov-marketing","homepage":null,"size":2880,"stargazers_count":0,"watchers_count":0,"language":"Vue","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":10,"license":null,"allow_forking":false,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":10,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":false,"triage":false,"pull":true}},{"id":514766138,"node_id":"R_kgDOHq61Og","name":"codecov-monitor","full_name":"codecov/codecov-monitor","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-monitor","description":"Monitoring + Suite for Codecov","fork":false,"url":"https://api.github.com/repos/codecov/codecov-monitor","forks_url":"https://api.github.com/repos/codecov/codecov-monitor/forks","keys_url":"https://api.github.com/repos/codecov/codecov-monitor/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-monitor/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-monitor/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-monitor/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-monitor/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-monitor/events","assignees_url":"https://api.github.com/repos/codecov/codecov-monitor/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-monitor/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-monitor/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-monitor/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-monitor/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-monitor/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-monitor/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-monitor/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-monitor/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-monitor/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-monitor/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-monitor/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-monitor/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-monitor/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-monitor/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-monitor/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-monitor/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-monitor/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-monitor/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-monitor/merges","archive_url":"https://api.github.com/repos/codecov/codecov-monitor/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-monitor/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-monitor/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-monitor/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-monitor/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-monitor/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-monitor/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-monitor/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-monitor/deployments","created_at":"2022-07-17T06:35:05Z","updated_at":"2022-07-17T07:09:04Z","pushed_at":"2023-02-25T10:55:23Z","git_url":"git://github.com/codecov/codecov-monitor.git","ssh_url":"git@github.com:codecov/codecov-monitor.git","clone_url":"https://github.com/codecov/codecov-monitor.git","svn_url":"https://github.com/codecov/codecov-monitor","homepage":null,"size":50,"stargazers_count":0,"watchers_count":0,"language":"Go","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":402187031,"node_id":"MDEwOlJlcG9zaXRvcnk0MDIxODcwMzE=","name":"codecov-onsite-backend","full_name":"codecov/codecov-onsite-backend","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-onsite-backend","description":"Skeleton + App for Backend Onsite","fork":false,"url":"https://api.github.com/repos/codecov/codecov-onsite-backend","forks_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/forks","keys_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/events","assignees_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/merges","archive_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-onsite-backend/deployments","created_at":"2021-09-01T19:58:38Z","updated_at":"2022-08-09T21:25:07Z","pushed_at":"2023-02-26T21:51:36Z","git_url":"git://github.com/codecov/codecov-onsite-backend.git","ssh_url":"git@github.com:codecov/codecov-onsite-backend.git","clone_url":"https://github.com/codecov/codecov-onsite-backend.git","svn_url":"https://github.com/codecov/codecov-onsite-backend","homepage":null,"size":27,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":22685735,"node_id":"MDEwOlJlcG9zaXRvcnkyMjY4NTczNQ==","name":"codecov-python","full_name":"codecov/codecov-python","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-python","description":"Python + report uploader for Codecov","fork":false,"url":"https://api.github.com/repos/codecov/codecov-python","forks_url":"https://api.github.com/repos/codecov/codecov-python/forks","keys_url":"https://api.github.com/repos/codecov/codecov-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-python/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-python/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-python/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-python/events","assignees_url":"https://api.github.com/repos/codecov/codecov-python/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-python/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-python/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-python/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-python/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-python/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-python/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-python/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-python/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-python/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-python/merges","archive_url":"https://api.github.com/repos/codecov/codecov-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-python/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-python/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-python/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-python/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-python/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-python/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-python/deployments","created_at":"2014-08-06T14:33:18Z","updated_at":"2023-07-25T13:52:45Z","pushed_at":"2023-04-18T21:38:12Z","git_url":"git://github.com/codecov/codecov-python.git","ssh_url":"git@github.com:codecov/codecov-python.git","clone_url":"https://github.com/codecov/codecov-python.git","svn_url":"https://github.com/codecov/codecov-python","homepage":"https://codecov.io","size":683,"stargazers_count":183,"watchers_count":183,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":176,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":23,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["ci","codecov","coverage","coverage-report","coveragepy"],"visibility":"public","forks":176,"open_issues":23,"watchers":183,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":371168220,"node_id":"MDEwOlJlcG9zaXRvcnkzNzExNjgyMjA=","name":"codecov-release","full_name":"codecov/codecov-release","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-release","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/codecov-release","forks_url":"https://api.github.com/repos/codecov/codecov-release/forks","keys_url":"https://api.github.com/repos/codecov/codecov-release/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-release/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-release/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-release/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-release/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-release/events","assignees_url":"https://api.github.com/repos/codecov/codecov-release/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-release/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-release/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-release/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-release/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-release/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-release/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-release/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-release/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-release/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-release/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-release/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-release/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-release/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-release/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-release/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-release/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-release/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-release/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-release/merges","archive_url":"https://api.github.com/repos/codecov/codecov-release/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-release/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-release/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-release/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-release/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-release/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-release/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-release/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-release/deployments","created_at":"2021-05-26T21:04:05Z","updated_at":"2023-06-20T16:28:35Z","pushed_at":"2023-08-28T13:19:59Z","git_url":"git://github.com/codecov/codecov-release.git","ssh_url":"git@github.com:codecov/codecov-release.git","clone_url":"https://github.com/codecov/codecov-release.git","svn_url":"https://github.com/codecov/codecov-release","homepage":null,"size":419,"stargazers_count":1,"watchers_count":1,"language":"PHP","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":1,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":607842426,"node_id":"R_kgDOJDrweg","name":"codecov-slack-app","full_name":"codecov/codecov-slack-app","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-slack-app","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/codecov-slack-app","forks_url":"https://api.github.com/repos/codecov/codecov-slack-app/forks","keys_url":"https://api.github.com/repos/codecov/codecov-slack-app/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-slack-app/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-slack-app/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-slack-app/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-slack-app/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-slack-app/events","assignees_url":"https://api.github.com/repos/codecov/codecov-slack-app/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-slack-app/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-slack-app/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-slack-app/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-slack-app/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-slack-app/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-slack-app/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-slack-app/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-slack-app/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-slack-app/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-slack-app/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-slack-app/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-slack-app/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-slack-app/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-slack-app/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-slack-app/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-slack-app/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-slack-app/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-slack-app/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-slack-app/merges","archive_url":"https://api.github.com/repos/codecov/codecov-slack-app/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-slack-app/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-slack-app/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-slack-app/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-slack-app/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-slack-app/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-slack-app/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-slack-app/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-slack-app/deployments","created_at":"2023-02-28T19:37:54Z","updated_at":"2023-02-28T19:40:41Z","pushed_at":"2023-09-21T15:54:51Z","git_url":"git://github.com/codecov/codecov-slack-app.git","ssh_url":"git@github.com:codecov/codecov-slack-app.git","clone_url":"https://github.com/codecov/codecov-slack-app.git","svn_url":"https://github.com/codecov/codecov-slack-app","homepage":null,"size":495,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":263925491,"node_id":"MDEwOlJlcG9zaXRvcnkyNjM5MjU0OTE=","name":"codecov-typescript-client","full_name":"codecov/codecov-typescript-client","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-typescript-client","description":"TypeScript + client for accessing Codecov''s internal REST API. Auto-generated with OpenAPI + schemas.","fork":false,"url":"https://api.github.com/repos/codecov/codecov-typescript-client","forks_url":"https://api.github.com/repos/codecov/codecov-typescript-client/forks","keys_url":"https://api.github.com/repos/codecov/codecov-typescript-client/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-typescript-client/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-typescript-client/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-typescript-client/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-typescript-client/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-typescript-client/events","assignees_url":"https://api.github.com/repos/codecov/codecov-typescript-client/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-typescript-client/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-typescript-client/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-typescript-client/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-typescript-client/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-typescript-client/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-typescript-client/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-typescript-client/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-typescript-client/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-typescript-client/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-typescript-client/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-typescript-client/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-typescript-client/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-typescript-client/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-typescript-client/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-typescript-client/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-typescript-client/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-typescript-client/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-typescript-client/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-typescript-client/merges","archive_url":"https://api.github.com/repos/codecov/codecov-typescript-client/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-typescript-client/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-typescript-client/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-typescript-client/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-typescript-client/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-typescript-client/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-typescript-client/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-typescript-client/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-typescript-client/deployments","created_at":"2020-05-14T13:39:05Z","updated_at":"2020-05-15T19:52:23Z","pushed_at":"2020-05-15T19:52:20Z","git_url":"git://github.com/codecov/codecov-typescript-client.git","ssh_url":"git@github.com:codecov/codecov-typescript-client.git","clone_url":"https://github.com/codecov/codecov-typescript-client.git","svn_url":"https://github.com/codecov/codecov-typescript-client","homepage":null,"size":27,"stargazers_count":0,"watchers_count":0,"language":"TypeScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":265929273,"node_id":"MDEwOlJlcG9zaXRvcnkyNjU5MjkyNzM=","name":"codecov-ui","full_name":"codecov/codecov-ui","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov-ui","description":"Repository + of reusable Vue components","fork":false,"url":"https://api.github.com/repos/codecov/codecov-ui","forks_url":"https://api.github.com/repos/codecov/codecov-ui/forks","keys_url":"https://api.github.com/repos/codecov/codecov-ui/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov-ui/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov-ui/teams","hooks_url":"https://api.github.com/repos/codecov/codecov-ui/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov-ui/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov-ui/events","assignees_url":"https://api.github.com/repos/codecov/codecov-ui/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov-ui/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov-ui/tags","blobs_url":"https://api.github.com/repos/codecov/codecov-ui/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov-ui/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov-ui/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov-ui/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov-ui/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov-ui/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov-ui/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov-ui/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov-ui/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov-ui/subscription","commits_url":"https://api.github.com/repos/codecov/codecov-ui/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov-ui/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov-ui/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov-ui/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov-ui/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov-ui/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov-ui/merges","archive_url":"https://api.github.com/repos/codecov/codecov-ui/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov-ui/downloads","issues_url":"https://api.github.com/repos/codecov/codecov-ui/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov-ui/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov-ui/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov-ui/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov-ui/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov-ui/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov-ui/deployments","created_at":"2020-05-21T19:01:14Z","updated_at":"2022-02-03T22:37:55Z","pushed_at":"2023-01-27T05:18:45Z","git_url":"git://github.com/codecov/codecov-ui.git","ssh_url":"git@github.com:codecov/codecov-ui.git","clone_url":"https://github.com/codecov/codecov-ui.git","svn_url":"https://github.com/codecov/codecov-ui","homepage":null,"size":3349,"stargazers_count":0,"watchers_count":0,"language":"TypeScript","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":14,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":14,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":22071731,"node_id":"MDEwOlJlcG9zaXRvcnkyMjA3MTczMQ==","name":"codecov.io","full_name":"codecov/codecov.io","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/codecov.io","description":"Project + Core","fork":false,"url":"https://api.github.com/repos/codecov/codecov.io","forks_url":"https://api.github.com/repos/codecov/codecov.io/forks","keys_url":"https://api.github.com/repos/codecov/codecov.io/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/codecov.io/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/codecov.io/teams","hooks_url":"https://api.github.com/repos/codecov/codecov.io/hooks","issue_events_url":"https://api.github.com/repos/codecov/codecov.io/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/codecov.io/events","assignees_url":"https://api.github.com/repos/codecov/codecov.io/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/codecov.io/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/codecov.io/tags","blobs_url":"https://api.github.com/repos/codecov/codecov.io/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/codecov.io/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/codecov.io/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/codecov.io/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/codecov.io/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/codecov.io/languages","stargazers_url":"https://api.github.com/repos/codecov/codecov.io/stargazers","contributors_url":"https://api.github.com/repos/codecov/codecov.io/contributors","subscribers_url":"https://api.github.com/repos/codecov/codecov.io/subscribers","subscription_url":"https://api.github.com/repos/codecov/codecov.io/subscription","commits_url":"https://api.github.com/repos/codecov/codecov.io/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/codecov.io/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/codecov.io/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/codecov.io/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/codecov.io/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/codecov.io/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/codecov.io/merges","archive_url":"https://api.github.com/repos/codecov/codecov.io/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/codecov.io/downloads","issues_url":"https://api.github.com/repos/codecov/codecov.io/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/codecov.io/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/codecov.io/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/codecov.io/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/codecov.io/labels{/name}","releases_url":"https://api.github.com/repos/codecov/codecov.io/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/codecov.io/deployments","created_at":"2014-07-21T16:30:43Z","updated_at":"2022-01-10T19:44:17Z","pushed_at":"2023-09-11T22:36:50Z","git_url":"git://github.com/codecov/codecov.io.git","ssh_url":"git@github.com:codecov/codecov.io.git","clone_url":"https://github.com/codecov/codecov.io.git","svn_url":"https://github.com/codecov/codecov.io","homepage":"https://codecov.io","size":48726,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":62,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":62,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":663129962,"node_id":"R_kgDOJ4aPag","name":"contributing","full_name":"codecov/contributing","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/contributing","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/contributing","forks_url":"https://api.github.com/repos/codecov/contributing/forks","keys_url":"https://api.github.com/repos/codecov/contributing/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/contributing/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/contributing/teams","hooks_url":"https://api.github.com/repos/codecov/contributing/hooks","issue_events_url":"https://api.github.com/repos/codecov/contributing/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/contributing/events","assignees_url":"https://api.github.com/repos/codecov/contributing/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/contributing/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/contributing/tags","blobs_url":"https://api.github.com/repos/codecov/contributing/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/contributing/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/contributing/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/contributing/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/contributing/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/contributing/languages","stargazers_url":"https://api.github.com/repos/codecov/contributing/stargazers","contributors_url":"https://api.github.com/repos/codecov/contributing/contributors","subscribers_url":"https://api.github.com/repos/codecov/contributing/subscribers","subscription_url":"https://api.github.com/repos/codecov/contributing/subscription","commits_url":"https://api.github.com/repos/codecov/contributing/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/contributing/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/contributing/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/contributing/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/contributing/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/contributing/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/contributing/merges","archive_url":"https://api.github.com/repos/codecov/contributing/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/contributing/downloads","issues_url":"https://api.github.com/repos/codecov/contributing/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/contributing/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/contributing/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/contributing/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/contributing/labels{/name}","releases_url":"https://api.github.com/repos/codecov/contributing/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/contributing/deployments","created_at":"2023-07-06T16:04:39Z","updated_at":"2023-08-26T08:10:02Z","pushed_at":"2023-08-04T11:00:01Z","git_url":"git://github.com/codecov/contributing.git","ssh_url":"git@github.com:codecov/contributing.git","clone_url":"https://github.com/codecov/contributing.git","svn_url":"https://github.com/codecov/contributing","homepage":null,"size":28,"stargazers_count":14,"watchers_count":14,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":1,"open_issues":1,"watchers":14,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":204493189,"node_id":"MDEwOlJlcG9zaXRvcnkyMDQ0OTMxODk=","name":"cpp-11-standard","full_name":"codecov/cpp-11-standard","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/cpp-11-standard","description":"Codecov + coverage standard for c++ 11","fork":false,"url":"https://api.github.com/repos/codecov/cpp-11-standard","forks_url":"https://api.github.com/repos/codecov/cpp-11-standard/forks","keys_url":"https://api.github.com/repos/codecov/cpp-11-standard/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/cpp-11-standard/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/cpp-11-standard/teams","hooks_url":"https://api.github.com/repos/codecov/cpp-11-standard/hooks","issue_events_url":"https://api.github.com/repos/codecov/cpp-11-standard/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/cpp-11-standard/events","assignees_url":"https://api.github.com/repos/codecov/cpp-11-standard/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/cpp-11-standard/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/cpp-11-standard/tags","blobs_url":"https://api.github.com/repos/codecov/cpp-11-standard/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/cpp-11-standard/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/cpp-11-standard/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/cpp-11-standard/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/cpp-11-standard/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/cpp-11-standard/languages","stargazers_url":"https://api.github.com/repos/codecov/cpp-11-standard/stargazers","contributors_url":"https://api.github.com/repos/codecov/cpp-11-standard/contributors","subscribers_url":"https://api.github.com/repos/codecov/cpp-11-standard/subscribers","subscription_url":"https://api.github.com/repos/codecov/cpp-11-standard/subscription","commits_url":"https://api.github.com/repos/codecov/cpp-11-standard/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/cpp-11-standard/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/cpp-11-standard/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/cpp-11-standard/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/cpp-11-standard/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/cpp-11-standard/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/cpp-11-standard/merges","archive_url":"https://api.github.com/repos/codecov/cpp-11-standard/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/cpp-11-standard/downloads","issues_url":"https://api.github.com/repos/codecov/cpp-11-standard/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/cpp-11-standard/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/cpp-11-standard/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/cpp-11-standard/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/cpp-11-standard/labels{/name}","releases_url":"https://api.github.com/repos/codecov/cpp-11-standard/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/cpp-11-standard/deployments","created_at":"2019-08-26T14:26:30Z","updated_at":"2023-02-25T07:02:09Z","pushed_at":"2023-09-16T04:51:56Z","git_url":"git://github.com/codecov/cpp-11-standard.git","ssh_url":"git@github.com:codecov/cpp-11-standard.git","clone_url":"https://github.com/codecov/cpp-11-standard.git","svn_url":"https://github.com/codecov/cpp-11-standard","homepage":null,"size":381,"stargazers_count":7,"watchers_count":7,"language":"C++","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":12,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":12,"open_issues":1,"watchers":7,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":265252321,"node_id":"MDEwOlJlcG9zaXRvcnkyNjUyNTIzMjE=","name":"critical-path-research","full_name":"codecov/critical-path-research","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/critical-path-research","description":"A + place to collect research and experiments on critical path coverage","fork":false,"url":"https://api.github.com/repos/codecov/critical-path-research","forks_url":"https://api.github.com/repos/codecov/critical-path-research/forks","keys_url":"https://api.github.com/repos/codecov/critical-path-research/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/critical-path-research/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/critical-path-research/teams","hooks_url":"https://api.github.com/repos/codecov/critical-path-research/hooks","issue_events_url":"https://api.github.com/repos/codecov/critical-path-research/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/critical-path-research/events","assignees_url":"https://api.github.com/repos/codecov/critical-path-research/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/critical-path-research/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/critical-path-research/tags","blobs_url":"https://api.github.com/repos/codecov/critical-path-research/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/critical-path-research/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/critical-path-research/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/critical-path-research/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/critical-path-research/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/critical-path-research/languages","stargazers_url":"https://api.github.com/repos/codecov/critical-path-research/stargazers","contributors_url":"https://api.github.com/repos/codecov/critical-path-research/contributors","subscribers_url":"https://api.github.com/repos/codecov/critical-path-research/subscribers","subscription_url":"https://api.github.com/repos/codecov/critical-path-research/subscription","commits_url":"https://api.github.com/repos/codecov/critical-path-research/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/critical-path-research/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/critical-path-research/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/critical-path-research/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/critical-path-research/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/critical-path-research/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/critical-path-research/merges","archive_url":"https://api.github.com/repos/codecov/critical-path-research/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/critical-path-research/downloads","issues_url":"https://api.github.com/repos/codecov/critical-path-research/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/critical-path-research/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/critical-path-research/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/critical-path-research/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/critical-path-research/labels{/name}","releases_url":"https://api.github.com/repos/codecov/critical-path-research/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/critical-path-research/deployments","created_at":"2020-05-19T13:17:55Z","updated_at":"2021-10-12T15:44:25Z","pushed_at":"2021-10-12T15:44:22Z","git_url":"git://github.com/codecov/critical-path-research.git","ssh_url":"git@github.com:codecov/critical-path-research.git","clone_url":"https://github.com/codecov/critical-path-research.git","svn_url":"https://github.com/codecov/critical-path-research","homepage":null,"size":14030,"stargazers_count":1,"watchers_count":1,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":5,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":5,"watchers":1,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":349821826,"node_id":"MDEwOlJlcG9zaXRvcnkzNDk4MjE4MjY=","name":"data-transform","full_name":"codecov/data-transform","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/data-transform","description":"Codecov + data warehouse transformation layer","fork":false,"url":"https://api.github.com/repos/codecov/data-transform","forks_url":"https://api.github.com/repos/codecov/data-transform/forks","keys_url":"https://api.github.com/repos/codecov/data-transform/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/data-transform/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/data-transform/teams","hooks_url":"https://api.github.com/repos/codecov/data-transform/hooks","issue_events_url":"https://api.github.com/repos/codecov/data-transform/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/data-transform/events","assignees_url":"https://api.github.com/repos/codecov/data-transform/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/data-transform/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/data-transform/tags","blobs_url":"https://api.github.com/repos/codecov/data-transform/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/data-transform/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/data-transform/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/data-transform/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/data-transform/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/data-transform/languages","stargazers_url":"https://api.github.com/repos/codecov/data-transform/stargazers","contributors_url":"https://api.github.com/repos/codecov/data-transform/contributors","subscribers_url":"https://api.github.com/repos/codecov/data-transform/subscribers","subscription_url":"https://api.github.com/repos/codecov/data-transform/subscription","commits_url":"https://api.github.com/repos/codecov/data-transform/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/data-transform/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/data-transform/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/data-transform/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/data-transform/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/data-transform/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/data-transform/merges","archive_url":"https://api.github.com/repos/codecov/data-transform/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/data-transform/downloads","issues_url":"https://api.github.com/repos/codecov/data-transform/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/data-transform/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/data-transform/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/data-transform/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/data-transform/labels{/name}","releases_url":"https://api.github.com/repos/codecov/data-transform/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/data-transform/deployments","created_at":"2021-03-20T19:51:53Z","updated_at":"2021-12-09T19:09:30Z","pushed_at":"2023-08-04T22:04:15Z","git_url":"git://github.com/codecov/data-transform.git","ssh_url":"git@github.com:codecov/data-transform.git","clone_url":"https://github.com/codecov/data-transform.git","svn_url":"https://github.com/codecov/data-transform","homepage":"","size":391,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":2,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":356628560,"node_id":"MDEwOlJlcG9zaXRvcnkzNTY2Mjg1NjA=","name":"dataflow-export-logs","full_name":"codecov/dataflow-export-logs","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/dataflow-export-logs","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/dataflow-export-logs","forks_url":"https://api.github.com/repos/codecov/dataflow-export-logs/forks","keys_url":"https://api.github.com/repos/codecov/dataflow-export-logs/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/dataflow-export-logs/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/dataflow-export-logs/teams","hooks_url":"https://api.github.com/repos/codecov/dataflow-export-logs/hooks","issue_events_url":"https://api.github.com/repos/codecov/dataflow-export-logs/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/dataflow-export-logs/events","assignees_url":"https://api.github.com/repos/codecov/dataflow-export-logs/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/dataflow-export-logs/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/dataflow-export-logs/tags","blobs_url":"https://api.github.com/repos/codecov/dataflow-export-logs/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/dataflow-export-logs/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/dataflow-export-logs/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/dataflow-export-logs/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/dataflow-export-logs/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/dataflow-export-logs/languages","stargazers_url":"https://api.github.com/repos/codecov/dataflow-export-logs/stargazers","contributors_url":"https://api.github.com/repos/codecov/dataflow-export-logs/contributors","subscribers_url":"https://api.github.com/repos/codecov/dataflow-export-logs/subscribers","subscription_url":"https://api.github.com/repos/codecov/dataflow-export-logs/subscription","commits_url":"https://api.github.com/repos/codecov/dataflow-export-logs/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/dataflow-export-logs/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/dataflow-export-logs/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/dataflow-export-logs/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/dataflow-export-logs/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/dataflow-export-logs/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/dataflow-export-logs/merges","archive_url":"https://api.github.com/repos/codecov/dataflow-export-logs/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/dataflow-export-logs/downloads","issues_url":"https://api.github.com/repos/codecov/dataflow-export-logs/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/dataflow-export-logs/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/dataflow-export-logs/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/dataflow-export-logs/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/dataflow-export-logs/labels{/name}","releases_url":"https://api.github.com/repos/codecov/dataflow-export-logs/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/dataflow-export-logs/deployments","created_at":"2021-04-10T15:54:41Z","updated_at":"2023-04-20T18:13:22Z","pushed_at":"2022-09-23T23:17:18Z","git_url":"git://github.com/codecov/dataflow-export-logs.git","ssh_url":"git@github.com:codecov/dataflow-export-logs.git","clone_url":"https://github.com/codecov/dataflow-export-logs.git","svn_url":"https://github.com/codecov/dataflow-export-logs","homepage":null,"size":10,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":388926719,"node_id":"MDEwOlJlcG9zaXRvcnkzODg5MjY3MTk=","name":"deep-dive","full_name":"codecov/deep-dive","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/deep-dive","description":"Dive + docker layers for potential file exposure","fork":false,"url":"https://api.github.com/repos/codecov/deep-dive","forks_url":"https://api.github.com/repos/codecov/deep-dive/forks","keys_url":"https://api.github.com/repos/codecov/deep-dive/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/deep-dive/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/deep-dive/teams","hooks_url":"https://api.github.com/repos/codecov/deep-dive/hooks","issue_events_url":"https://api.github.com/repos/codecov/deep-dive/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/deep-dive/events","assignees_url":"https://api.github.com/repos/codecov/deep-dive/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/deep-dive/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/deep-dive/tags","blobs_url":"https://api.github.com/repos/codecov/deep-dive/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/deep-dive/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/deep-dive/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/deep-dive/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/deep-dive/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/deep-dive/languages","stargazers_url":"https://api.github.com/repos/codecov/deep-dive/stargazers","contributors_url":"https://api.github.com/repos/codecov/deep-dive/contributors","subscribers_url":"https://api.github.com/repos/codecov/deep-dive/subscribers","subscription_url":"https://api.github.com/repos/codecov/deep-dive/subscription","commits_url":"https://api.github.com/repos/codecov/deep-dive/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/deep-dive/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/deep-dive/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/deep-dive/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/deep-dive/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/deep-dive/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/deep-dive/merges","archive_url":"https://api.github.com/repos/codecov/deep-dive/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/deep-dive/downloads","issues_url":"https://api.github.com/repos/codecov/deep-dive/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/deep-dive/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/deep-dive/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/deep-dive/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/deep-dive/labels{/name}","releases_url":"https://api.github.com/repos/codecov/deep-dive/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/deep-dive/deployments","created_at":"2021-07-23T21:06:36Z","updated_at":"2022-08-29T13:56:21Z","pushed_at":"2023-05-11T20:46:52Z","git_url":"git://github.com/codecov/deep-dive.git","ssh_url":"git@github.com:codecov/deep-dive.git","clone_url":"https://github.com/codecov/deep-dive.git","svn_url":"https://github.com/codecov/deep-dive","homepage":null,"size":215,"stargazers_count":0,"watchers_count":0,"language":"Go","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":4,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":4,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":558498691,"node_id":"R_kgDOIUoDgw","name":"devops-workflows","full_name":"codecov/devops-workflows","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/devops-workflows","description":"Collection + of workflows for devops of other repos","fork":false,"url":"https://api.github.com/repos/codecov/devops-workflows","forks_url":"https://api.github.com/repos/codecov/devops-workflows/forks","keys_url":"https://api.github.com/repos/codecov/devops-workflows/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/devops-workflows/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/devops-workflows/teams","hooks_url":"https://api.github.com/repos/codecov/devops-workflows/hooks","issue_events_url":"https://api.github.com/repos/codecov/devops-workflows/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/devops-workflows/events","assignees_url":"https://api.github.com/repos/codecov/devops-workflows/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/devops-workflows/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/devops-workflows/tags","blobs_url":"https://api.github.com/repos/codecov/devops-workflows/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/devops-workflows/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/devops-workflows/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/devops-workflows/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/devops-workflows/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/devops-workflows/languages","stargazers_url":"https://api.github.com/repos/codecov/devops-workflows/stargazers","contributors_url":"https://api.github.com/repos/codecov/devops-workflows/contributors","subscribers_url":"https://api.github.com/repos/codecov/devops-workflows/subscribers","subscription_url":"https://api.github.com/repos/codecov/devops-workflows/subscription","commits_url":"https://api.github.com/repos/codecov/devops-workflows/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/devops-workflows/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/devops-workflows/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/devops-workflows/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/devops-workflows/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/devops-workflows/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/devops-workflows/merges","archive_url":"https://api.github.com/repos/codecov/devops-workflows/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/devops-workflows/downloads","issues_url":"https://api.github.com/repos/codecov/devops-workflows/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/devops-workflows/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/devops-workflows/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/devops-workflows/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/devops-workflows/labels{/name}","releases_url":"https://api.github.com/repos/codecov/devops-workflows/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/devops-workflows/deployments","created_at":"2022-10-27T17:06:29Z","updated_at":"2022-10-27T17:06:29Z","pushed_at":"2023-09-05T18:37:39Z","git_url":"git://github.com/codecov/devops-workflows.git","ssh_url":"git@github.com:codecov/devops-workflows.git","clone_url":"https://github.com/codecov/devops-workflows.git","svn_url":"https://github.com/codecov/devops-workflows","homepage":null,"size":21,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":false,"triage":false,"pull":true}},{"id":198240301,"node_id":"MDEwOlJlcG9zaXRvcnkxOTgyNDAzMDE=","name":"django-vue-dockerized-celery","full_name":"codecov/django-vue-dockerized-celery","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/django-vue-dockerized-celery","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery","forks_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/forks","keys_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/teams","hooks_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/hooks","issue_events_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/events","assignees_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/tags","blobs_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/languages","stargazers_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/stargazers","contributors_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/contributors","subscribers_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/subscribers","subscription_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/subscription","commits_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/merges","archive_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/downloads","issues_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/labels{/name}","releases_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/django-vue-dockerized-celery/deployments","created_at":"2019-07-22T14:29:54Z","updated_at":"2020-12-08T16:00:07Z","pushed_at":"2020-07-08T01:10:54Z","git_url":"git://github.com/codecov/django-vue-dockerized-celery.git","ssh_url":"git@github.com:codecov/django-vue-dockerized-celery.git","clone_url":"https://github.com/codecov/django-vue-dockerized-celery.git","svn_url":"https://github.com/codecov/django-vue-dockerized-celery","homepage":null,"size":219,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":646981237,"node_id":"R_kgDOJpAmdQ","name":"engineering-team","full_name":"codecov/engineering-team","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/engineering-team","description":"This + is a general repo to use with GH Projects","fork":false,"url":"https://api.github.com/repos/codecov/engineering-team","forks_url":"https://api.github.com/repos/codecov/engineering-team/forks","keys_url":"https://api.github.com/repos/codecov/engineering-team/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/engineering-team/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/engineering-team/teams","hooks_url":"https://api.github.com/repos/codecov/engineering-team/hooks","issue_events_url":"https://api.github.com/repos/codecov/engineering-team/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/engineering-team/events","assignees_url":"https://api.github.com/repos/codecov/engineering-team/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/engineering-team/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/engineering-team/tags","blobs_url":"https://api.github.com/repos/codecov/engineering-team/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/engineering-team/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/engineering-team/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/engineering-team/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/engineering-team/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/engineering-team/languages","stargazers_url":"https://api.github.com/repos/codecov/engineering-team/stargazers","contributors_url":"https://api.github.com/repos/codecov/engineering-team/contributors","subscribers_url":"https://api.github.com/repos/codecov/engineering-team/subscribers","subscription_url":"https://api.github.com/repos/codecov/engineering-team/subscription","commits_url":"https://api.github.com/repos/codecov/engineering-team/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/engineering-team/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/engineering-team/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/engineering-team/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/engineering-team/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/engineering-team/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/engineering-team/merges","archive_url":"https://api.github.com/repos/codecov/engineering-team/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/engineering-team/downloads","issues_url":"https://api.github.com/repos/codecov/engineering-team/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/engineering-team/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/engineering-team/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/engineering-team/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/engineering-team/labels{/name}","releases_url":"https://api.github.com/repos/codecov/engineering-team/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/engineering-team/deployments","created_at":"2023-05-29T19:44:59Z","updated_at":"2023-09-15T16:02:11Z","pushed_at":"2023-08-04T17:07:24Z","git_url":"git://github.com/codecov/engineering-team.git","ssh_url":"git@github.com:codecov/engineering-team.git","clone_url":"https://github.com/codecov/engineering-team.git","svn_url":"https://github.com/codecov/engineering-team","homepage":null,"size":3,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":292,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":292,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":377615393,"node_id":"MDEwOlJlcG9zaXRvcnkzNzc2MTUzOTM=","name":"enterprise-qa-helm","full_name":"codecov/enterprise-qa-helm","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/enterprise-qa-helm","description":"Helm + chart for deploying Codecov into QA","fork":false,"url":"https://api.github.com/repos/codecov/enterprise-qa-helm","forks_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/forks","keys_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/teams","hooks_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/hooks","issue_events_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/events","assignees_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/tags","blobs_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/languages","stargazers_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/stargazers","contributors_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/contributors","subscribers_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/subscribers","subscription_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/subscription","commits_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/merges","archive_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/downloads","issues_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/labels{/name}","releases_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/enterprise-qa-helm/deployments","created_at":"2021-06-16T20:09:58Z","updated_at":"2021-12-09T18:11:18Z","pushed_at":"2022-09-19T21:25:38Z","git_url":"git://github.com/codecov/enterprise-qa-helm.git","ssh_url":"git@github.com:codecov/enterprise-qa-helm.git","clone_url":"https://github.com/codecov/enterprise-qa-helm.git","svn_url":"https://github.com/codecov/enterprise-qa-helm","homepage":null,"size":94,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":45991588,"node_id":"MDEwOlJlcG9zaXRvcnk0NTk5MTU4OA==","name":"example-android","full_name":"codecov/example-android","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-android","description":"Android + code coverage example with https://codecov.io","fork":false,"url":"https://api.github.com/repos/codecov/example-android","forks_url":"https://api.github.com/repos/codecov/example-android/forks","keys_url":"https://api.github.com/repos/codecov/example-android/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-android/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-android/teams","hooks_url":"https://api.github.com/repos/codecov/example-android/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-android/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-android/events","assignees_url":"https://api.github.com/repos/codecov/example-android/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-android/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-android/tags","blobs_url":"https://api.github.com/repos/codecov/example-android/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-android/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-android/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-android/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-android/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-android/languages","stargazers_url":"https://api.github.com/repos/codecov/example-android/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-android/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-android/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-android/subscription","commits_url":"https://api.github.com/repos/codecov/example-android/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-android/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-android/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-android/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-android/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-android/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-android/merges","archive_url":"https://api.github.com/repos/codecov/example-android/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-android/downloads","issues_url":"https://api.github.com/repos/codecov/example-android/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-android/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-android/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-android/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-android/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-android/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-android/deployments","created_at":"2015-11-11T15:55:13Z","updated_at":"2023-09-13T13:41:36Z","pushed_at":"2021-07-20T19:21:08Z","git_url":"git://github.com/codecov/example-android.git","ssh_url":"git@github.com:codecov/example-android.git","clone_url":"https://github.com/codecov/example-android.git","svn_url":"https://github.com/codecov/example-android","homepage":null,"size":159,"stargazers_count":143,"watchers_count":143,"language":"Java","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":134,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":1,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":134,"open_issues":1,"watchers":143,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":164959283,"node_id":"MDEwOlJlcG9zaXRvcnkxNjQ5NTkyODM=","name":"example-azure-pipelines","full_name":"codecov/example-azure-pipelines","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-azure-pipelines","description":"Azure + Pipelines coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-azure-pipelines","forks_url":"https://api.github.com/repos/codecov/example-azure-pipelines/forks","keys_url":"https://api.github.com/repos/codecov/example-azure-pipelines/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-azure-pipelines/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-azure-pipelines/teams","hooks_url":"https://api.github.com/repos/codecov/example-azure-pipelines/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-azure-pipelines/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-azure-pipelines/events","assignees_url":"https://api.github.com/repos/codecov/example-azure-pipelines/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-azure-pipelines/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-azure-pipelines/tags","blobs_url":"https://api.github.com/repos/codecov/example-azure-pipelines/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-azure-pipelines/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-azure-pipelines/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-azure-pipelines/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-azure-pipelines/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-azure-pipelines/languages","stargazers_url":"https://api.github.com/repos/codecov/example-azure-pipelines/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-azure-pipelines/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-azure-pipelines/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-azure-pipelines/subscription","commits_url":"https://api.github.com/repos/codecov/example-azure-pipelines/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-azure-pipelines/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-azure-pipelines/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-azure-pipelines/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-azure-pipelines/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-azure-pipelines/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-azure-pipelines/merges","archive_url":"https://api.github.com/repos/codecov/example-azure-pipelines/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-azure-pipelines/downloads","issues_url":"https://api.github.com/repos/codecov/example-azure-pipelines/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-azure-pipelines/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-azure-pipelines/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-azure-pipelines/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-azure-pipelines/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-azure-pipelines/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-azure-pipelines/deployments","created_at":"2019-01-10T00:20:56Z","updated_at":"2023-02-16T19:29:47Z","pushed_at":"2021-07-20T19:38:19Z","git_url":"git://github.com/codecov/example-azure-pipelines.git","ssh_url":"git@github.com:codecov/example-azure-pipelines.git","clone_url":"https://github.com/codecov/example-azure-pipelines.git","svn_url":"https://github.com/codecov/example-azure-pipelines","homepage":null,"size":8,"stargazers_count":2,"watchers_count":2,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":3,"open_issues":0,"watchers":2,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":44101505,"node_id":"MDEwOlJlcG9zaXRvcnk0NDEwMTUwNQ==","name":"example-bash","full_name":"codecov/example-bash","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-bash","description":"Codecov: + Bash/Shell coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-bash","forks_url":"https://api.github.com/repos/codecov/example-bash/forks","keys_url":"https://api.github.com/repos/codecov/example-bash/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-bash/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-bash/teams","hooks_url":"https://api.github.com/repos/codecov/example-bash/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-bash/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-bash/events","assignees_url":"https://api.github.com/repos/codecov/example-bash/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-bash/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-bash/tags","blobs_url":"https://api.github.com/repos/codecov/example-bash/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-bash/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-bash/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-bash/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-bash/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-bash/languages","stargazers_url":"https://api.github.com/repos/codecov/example-bash/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-bash/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-bash/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-bash/subscription","commits_url":"https://api.github.com/repos/codecov/example-bash/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-bash/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-bash/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-bash/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-bash/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-bash/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-bash/merges","archive_url":"https://api.github.com/repos/codecov/example-bash/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-bash/downloads","issues_url":"https://api.github.com/repos/codecov/example-bash/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-bash/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-bash/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-bash/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-bash/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-bash/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-bash/deployments","created_at":"2015-10-12T10:50:49Z","updated_at":"2023-09-14T16:29:24Z","pushed_at":"2023-09-16T05:06:16Z","git_url":"git://github.com/codecov/example-bash.git","ssh_url":"git@github.com:codecov/example-bash.git","clone_url":"https://github.com/codecov/example-bash.git","svn_url":"https://github.com/codecov/example-bash","homepage":"https://codecov.io","size":23,"stargazers_count":48,"watchers_count":48,"language":"Ruby","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":54,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":54,"open_issues":2,"watchers":48,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":35053332,"node_id":"MDEwOlJlcG9zaXRvcnkzNTA1MzMzMg==","name":"example-c","full_name":"codecov/example-c","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-c","description":"Upload + reports to Codecov using C/C++","fork":false,"url":"https://api.github.com/repos/codecov/example-c","forks_url":"https://api.github.com/repos/codecov/example-c/forks","keys_url":"https://api.github.com/repos/codecov/example-c/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-c/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-c/teams","hooks_url":"https://api.github.com/repos/codecov/example-c/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-c/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-c/events","assignees_url":"https://api.github.com/repos/codecov/example-c/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-c/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-c/tags","blobs_url":"https://api.github.com/repos/codecov/example-c/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-c/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-c/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-c/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-c/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-c/languages","stargazers_url":"https://api.github.com/repos/codecov/example-c/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-c/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-c/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-c/subscription","commits_url":"https://api.github.com/repos/codecov/example-c/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-c/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-c/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-c/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-c/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-c/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-c/merges","archive_url":"https://api.github.com/repos/codecov/example-c/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-c/downloads","issues_url":"https://api.github.com/repos/codecov/example-c/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-c/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-c/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-c/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-c/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-c/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-c/deployments","created_at":"2015-05-04T18:58:23Z","updated_at":"2023-07-27T05:10:45Z","pushed_at":"2023-09-16T05:06:00Z","git_url":"git://github.com/codecov/example-c.git","ssh_url":"git@github.com:codecov/example-c.git","clone_url":"https://github.com/codecov/example-c.git","svn_url":"https://github.com/codecov/example-c","homepage":null,"size":54,"stargazers_count":33,"watchers_count":33,"language":"C","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":45,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":4,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["c","codecov","coverage"],"visibility":"public","forks":45,"open_issues":4,"watchers":33,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":36365442,"node_id":"MDEwOlJlcG9zaXRvcnkzNjM2NTQ0Mg==","name":"example-clojure","full_name":"codecov/example-clojure","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-clojure","description":"Example + Clojure integration with Codecov","fork":false,"url":"https://api.github.com/repos/codecov/example-clojure","forks_url":"https://api.github.com/repos/codecov/example-clojure/forks","keys_url":"https://api.github.com/repos/codecov/example-clojure/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-clojure/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-clojure/teams","hooks_url":"https://api.github.com/repos/codecov/example-clojure/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-clojure/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-clojure/events","assignees_url":"https://api.github.com/repos/codecov/example-clojure/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-clojure/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-clojure/tags","blobs_url":"https://api.github.com/repos/codecov/example-clojure/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-clojure/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-clojure/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-clojure/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-clojure/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-clojure/languages","stargazers_url":"https://api.github.com/repos/codecov/example-clojure/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-clojure/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-clojure/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-clojure/subscription","commits_url":"https://api.github.com/repos/codecov/example-clojure/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-clojure/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-clojure/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-clojure/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-clojure/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-clojure/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-clojure/merges","archive_url":"https://api.github.com/repos/codecov/example-clojure/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-clojure/downloads","issues_url":"https://api.github.com/repos/codecov/example-clojure/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-clojure/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-clojure/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-clojure/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-clojure/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-clojure/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-clojure/deployments","created_at":"2015-05-27T12:30:22Z","updated_at":"2023-07-25T13:56:26Z","pushed_at":"2023-09-16T04:54:31Z","git_url":"git://github.com/codecov/example-clojure.git","ssh_url":"git@github.com:codecov/example-clojure.git","clone_url":"https://github.com/codecov/example-clojure.git","svn_url":"https://github.com/codecov/example-clojure","homepage":"https://codecov.io","size":16,"stargazers_count":14,"watchers_count":14,"language":"Clojure","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":10,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["clojure","coverage"],"visibility":"public","forks":10,"open_issues":0,"watchers":14,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":48370345,"node_id":"MDEwOlJlcG9zaXRvcnk0ODM3MDM0NQ==","name":"example-cpp","full_name":"codecov/example-cpp","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-cpp","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/example-cpp","forks_url":"https://api.github.com/repos/codecov/example-cpp/forks","keys_url":"https://api.github.com/repos/codecov/example-cpp/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-cpp/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-cpp/teams","hooks_url":"https://api.github.com/repos/codecov/example-cpp/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-cpp/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-cpp/events","assignees_url":"https://api.github.com/repos/codecov/example-cpp/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-cpp/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-cpp/tags","blobs_url":"https://api.github.com/repos/codecov/example-cpp/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-cpp/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-cpp/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-cpp/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-cpp/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-cpp/languages","stargazers_url":"https://api.github.com/repos/codecov/example-cpp/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-cpp/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-cpp/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-cpp/subscription","commits_url":"https://api.github.com/repos/codecov/example-cpp/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-cpp/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-cpp/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-cpp/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-cpp/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-cpp/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-cpp/merges","archive_url":"https://api.github.com/repos/codecov/example-cpp/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-cpp/downloads","issues_url":"https://api.github.com/repos/codecov/example-cpp/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-cpp/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-cpp/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-cpp/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-cpp/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-cpp/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-cpp/deployments","created_at":"2015-12-21T12:22:29Z","updated_at":"2022-10-31T20:33:03Z","pushed_at":"2022-10-31T20:29:49Z","git_url":"git://github.com/codecov/example-cpp.git","ssh_url":"git@github.com:codecov/example-cpp.git","clone_url":"https://github.com/codecov/example-cpp.git","svn_url":"https://github.com/codecov/example-cpp","homepage":null,"size":2,"stargazers_count":4,"watchers_count":4,"language":null,"has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":3,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":3,"open_issues":0,"watchers":4,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":65390076,"node_id":"MDEwOlJlcG9zaXRvcnk2NTM5MDA3Ng==","name":"example-cpp11","full_name":"codecov/example-cpp11","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-cpp11","description":"Minimal + project that uses qmake, GCC, C++11, gcov and is tested by Travis CI","fork":true,"url":"https://api.github.com/repos/codecov/example-cpp11","forks_url":"https://api.github.com/repos/codecov/example-cpp11/forks","keys_url":"https://api.github.com/repos/codecov/example-cpp11/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-cpp11/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-cpp11/teams","hooks_url":"https://api.github.com/repos/codecov/example-cpp11/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-cpp11/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-cpp11/events","assignees_url":"https://api.github.com/repos/codecov/example-cpp11/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-cpp11/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-cpp11/tags","blobs_url":"https://api.github.com/repos/codecov/example-cpp11/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-cpp11/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-cpp11/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-cpp11/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-cpp11/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-cpp11/languages","stargazers_url":"https://api.github.com/repos/codecov/example-cpp11/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-cpp11/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-cpp11/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-cpp11/subscription","commits_url":"https://api.github.com/repos/codecov/example-cpp11/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-cpp11/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-cpp11/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-cpp11/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-cpp11/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-cpp11/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-cpp11/merges","archive_url":"https://api.github.com/repos/codecov/example-cpp11/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-cpp11/downloads","issues_url":"https://api.github.com/repos/codecov/example-cpp11/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-cpp11/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-cpp11/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-cpp11/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-cpp11/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-cpp11/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-cpp11/deployments","created_at":"2016-08-10T14:38:50Z","updated_at":"2023-07-28T06:56:58Z","pushed_at":"2021-07-20T20:27:48Z","git_url":"git://github.com/codecov/example-cpp11.git","ssh_url":"git@github.com:codecov/example-cpp11.git","clone_url":"https://github.com/codecov/example-cpp11.git","svn_url":"https://github.com/codecov/example-cpp11","homepage":null,"size":49,"stargazers_count":26,"watchers_count":26,"language":"Shell","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":29,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"gpl-3.0","name":"GNU + General Public License v3.0","spdx_id":"GPL-3.0","url":"https://api.github.com/licenses/gpl-3.0","node_id":"MDc6TGljZW5zZTk="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":29,"open_issues":0,"watchers":26,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":78873888,"node_id":"MDEwOlJlcG9zaXRvcnk3ODg3Mzg4OA==","name":"example-cpp11-cmake","full_name":"codecov/example-cpp11-cmake","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-cpp11-cmake","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/example-cpp11-cmake","forks_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/forks","keys_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/teams","hooks_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/events","assignees_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/tags","blobs_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/languages","stargazers_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/subscription","commits_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/merges","archive_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/downloads","issues_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-cpp11-cmake/deployments","created_at":"2017-01-13T18:12:49Z","updated_at":"2023-09-08T17:19:23Z","pushed_at":"2022-05-25T19:22:24Z","git_url":"git://github.com/codecov/example-cpp11-cmake.git","ssh_url":"git@github.com:codecov/example-cpp11-cmake.git","clone_url":"https://github.com/codecov/example-cpp11-cmake.git","svn_url":"https://github.com/codecov/example-cpp11-cmake","homepage":null,"size":47,"stargazers_count":143,"watchers_count":143,"language":"CMake","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":63,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["cmake","codecov","coverage","cpp","cpp11"],"visibility":"public","forks":63,"open_issues":0,"watchers":143,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":65390037,"node_id":"MDEwOlJlcG9zaXRvcnk2NTM5MDAzNw==","name":"example-cpp11_boost","full_name":"codecov/example-cpp11_boost","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-cpp11_boost","description":"Minimal + project that uses qmake, GCC, C++11, Boost, Boost.Test, gcov and is tested by + Travis CI","fork":true,"url":"https://api.github.com/repos/codecov/example-cpp11_boost","forks_url":"https://api.github.com/repos/codecov/example-cpp11_boost/forks","keys_url":"https://api.github.com/repos/codecov/example-cpp11_boost/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-cpp11_boost/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-cpp11_boost/teams","hooks_url":"https://api.github.com/repos/codecov/example-cpp11_boost/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-cpp11_boost/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-cpp11_boost/events","assignees_url":"https://api.github.com/repos/codecov/example-cpp11_boost/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-cpp11_boost/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-cpp11_boost/tags","blobs_url":"https://api.github.com/repos/codecov/example-cpp11_boost/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-cpp11_boost/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-cpp11_boost/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-cpp11_boost/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-cpp11_boost/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-cpp11_boost/languages","stargazers_url":"https://api.github.com/repos/codecov/example-cpp11_boost/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-cpp11_boost/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-cpp11_boost/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-cpp11_boost/subscription","commits_url":"https://api.github.com/repos/codecov/example-cpp11_boost/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-cpp11_boost/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-cpp11_boost/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-cpp11_boost/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-cpp11_boost/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-cpp11_boost/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-cpp11_boost/merges","archive_url":"https://api.github.com/repos/codecov/example-cpp11_boost/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-cpp11_boost/downloads","issues_url":"https://api.github.com/repos/codecov/example-cpp11_boost/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-cpp11_boost/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-cpp11_boost/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-cpp11_boost/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-cpp11_boost/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-cpp11_boost/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-cpp11_boost/deployments","created_at":"2016-08-10T14:38:20Z","updated_at":"2021-01-06T20:25:50Z","pushed_at":"2020-09-04T02:47:17Z","git_url":"git://github.com/codecov/example-cpp11_boost.git","ssh_url":"git@github.com:codecov/example-cpp11_boost.git","clone_url":"https://github.com/codecov/example-cpp11_boost.git","svn_url":"https://github.com/codecov/example-cpp11_boost","homepage":null,"size":28,"stargazers_count":2,"watchers_count":2,"language":"Shell","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":5,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"gpl-3.0","name":"GNU + General Public License v3.0","spdx_id":"GPL-3.0","url":"https://api.github.com/licenses/gpl-3.0","node_id":"MDc6TGljZW5zZTk="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":5,"open_issues":0,"watchers":2,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":65389969,"node_id":"MDEwOlJlcG9zaXRvcnk2NTM4OTk2OQ==","name":"example-cpp98","full_name":"codecov/example-cpp98","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-cpp98","description":"Test + how to use qmake and gcov","fork":true,"url":"https://api.github.com/repos/codecov/example-cpp98","forks_url":"https://api.github.com/repos/codecov/example-cpp98/forks","keys_url":"https://api.github.com/repos/codecov/example-cpp98/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-cpp98/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-cpp98/teams","hooks_url":"https://api.github.com/repos/codecov/example-cpp98/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-cpp98/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-cpp98/events","assignees_url":"https://api.github.com/repos/codecov/example-cpp98/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-cpp98/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-cpp98/tags","blobs_url":"https://api.github.com/repos/codecov/example-cpp98/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-cpp98/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-cpp98/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-cpp98/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-cpp98/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-cpp98/languages","stargazers_url":"https://api.github.com/repos/codecov/example-cpp98/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-cpp98/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-cpp98/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-cpp98/subscription","commits_url":"https://api.github.com/repos/codecov/example-cpp98/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-cpp98/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-cpp98/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-cpp98/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-cpp98/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-cpp98/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-cpp98/merges","archive_url":"https://api.github.com/repos/codecov/example-cpp98/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-cpp98/downloads","issues_url":"https://api.github.com/repos/codecov/example-cpp98/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-cpp98/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-cpp98/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-cpp98/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-cpp98/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-cpp98/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-cpp98/deployments","created_at":"2016-08-10T14:37:38Z","updated_at":"2023-07-25T14:03:18Z","pushed_at":"2018-04-26T08:51:17Z","git_url":"git://github.com/codecov/example-cpp98.git","ssh_url":"git@github.com:codecov/example-cpp98.git","clone_url":"https://github.com/codecov/example-cpp98.git","svn_url":"https://github.com/codecov/example-cpp98","homepage":null,"size":156,"stargazers_count":3,"watchers_count":3,"language":"Prolog","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":24,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"gpl-3.0","name":"GNU + General Public License v3.0","spdx_id":"GPL-3.0","url":"https://api.github.com/licenses/gpl-3.0","node_id":"MDc6TGljZW5zZTk="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":24,"open_issues":0,"watchers":3,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":34411629,"node_id":"MDEwOlJlcG9zaXRvcnkzNDQxMTYyOQ==","name":"example-csharp","full_name":"codecov/example-csharp","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-csharp","description":"Codecov: + C# example repository","fork":false,"url":"https://api.github.com/repos/codecov/example-csharp","forks_url":"https://api.github.com/repos/codecov/example-csharp/forks","keys_url":"https://api.github.com/repos/codecov/example-csharp/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-csharp/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-csharp/teams","hooks_url":"https://api.github.com/repos/codecov/example-csharp/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-csharp/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-csharp/events","assignees_url":"https://api.github.com/repos/codecov/example-csharp/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-csharp/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-csharp/tags","blobs_url":"https://api.github.com/repos/codecov/example-csharp/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-csharp/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-csharp/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-csharp/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-csharp/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-csharp/languages","stargazers_url":"https://api.github.com/repos/codecov/example-csharp/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-csharp/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-csharp/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-csharp/subscription","commits_url":"https://api.github.com/repos/codecov/example-csharp/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-csharp/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-csharp/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-csharp/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-csharp/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-csharp/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-csharp/merges","archive_url":"https://api.github.com/repos/codecov/example-csharp/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-csharp/downloads","issues_url":"https://api.github.com/repos/codecov/example-csharp/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-csharp/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-csharp/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-csharp/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-csharp/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-csharp/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-csharp/deployments","created_at":"2015-04-22T19:38:00Z","updated_at":"2023-08-06T01:39:10Z","pushed_at":"2021-05-16T16:08:07Z","git_url":"git://github.com/codecov/example-csharp.git","ssh_url":"git@github.com:codecov/example-csharp.git","clone_url":"https://github.com/codecov/example-csharp.git","svn_url":"https://github.com/codecov/example-csharp","homepage":"https://codecov.io","size":42,"stargazers_count":113,"watchers_count":113,"language":"C#","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":103,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":4,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":103,"open_issues":4,"watchers":113,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":53706437,"node_id":"MDEwOlJlcG9zaXRvcnk1MzcwNjQzNw==","name":"example-csharp-sharpcover","full_name":"codecov/example-csharp-sharpcover","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-csharp-sharpcover","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/example-csharp-sharpcover","forks_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/forks","keys_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/teams","hooks_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/events","assignees_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/tags","blobs_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/languages","stargazers_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/subscription","commits_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/merges","archive_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/downloads","issues_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-csharp-sharpcover/deployments","created_at":"2016-03-12T01:10:08Z","updated_at":"2022-10-31T20:33:01Z","pushed_at":"2023-08-23T02:58:06Z","git_url":"git://github.com/codecov/example-csharp-sharpcover.git","ssh_url":"git@github.com:codecov/example-csharp-sharpcover.git","clone_url":"https://github.com/codecov/example-csharp-sharpcover.git","svn_url":"https://github.com/codecov/example-csharp-sharpcover","homepage":null,"size":375,"stargazers_count":2,"watchers_count":2,"language":"C#","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":7,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":7,"open_issues":1,"watchers":2,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":32197069,"node_id":"MDEwOlJlcG9zaXRvcnkzMjE5NzA2OQ==","name":"example-d","full_name":"codecov/example-d","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-d","description":"Example + repository for D and Codecov","fork":false,"url":"https://api.github.com/repos/codecov/example-d","forks_url":"https://api.github.com/repos/codecov/example-d/forks","keys_url":"https://api.github.com/repos/codecov/example-d/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-d/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-d/teams","hooks_url":"https://api.github.com/repos/codecov/example-d/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-d/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-d/events","assignees_url":"https://api.github.com/repos/codecov/example-d/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-d/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-d/tags","blobs_url":"https://api.github.com/repos/codecov/example-d/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-d/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-d/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-d/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-d/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-d/languages","stargazers_url":"https://api.github.com/repos/codecov/example-d/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-d/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-d/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-d/subscription","commits_url":"https://api.github.com/repos/codecov/example-d/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-d/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-d/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-d/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-d/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-d/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-d/merges","archive_url":"https://api.github.com/repos/codecov/example-d/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-d/downloads","issues_url":"https://api.github.com/repos/codecov/example-d/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-d/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-d/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-d/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-d/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-d/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-d/deployments","created_at":"2015-03-14T05:16:03Z","updated_at":"2023-07-25T13:55:20Z","pushed_at":"2022-10-31T20:30:22Z","git_url":"git://github.com/codecov/example-d.git","ssh_url":"git@github.com:codecov/example-d.git","clone_url":"https://github.com/codecov/example-d.git","svn_url":"https://github.com/codecov/example-d","homepage":"https://codecov.io","size":46,"stargazers_count":10,"watchers_count":10,"language":"D","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":7,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["codecov","coverage","d"],"visibility":"public","forks":7,"open_issues":0,"watchers":10,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":45387112,"node_id":"MDEwOlJlcG9zaXRvcnk0NTM4NzExMg==","name":"example-delphi","full_name":"codecov/example-delphi","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-delphi","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/example-delphi","forks_url":"https://api.github.com/repos/codecov/example-delphi/forks","keys_url":"https://api.github.com/repos/codecov/example-delphi/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-delphi/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-delphi/teams","hooks_url":"https://api.github.com/repos/codecov/example-delphi/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-delphi/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-delphi/events","assignees_url":"https://api.github.com/repos/codecov/example-delphi/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-delphi/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-delphi/tags","blobs_url":"https://api.github.com/repos/codecov/example-delphi/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-delphi/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-delphi/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-delphi/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-delphi/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-delphi/languages","stargazers_url":"https://api.github.com/repos/codecov/example-delphi/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-delphi/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-delphi/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-delphi/subscription","commits_url":"https://api.github.com/repos/codecov/example-delphi/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-delphi/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-delphi/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-delphi/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-delphi/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-delphi/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-delphi/merges","archive_url":"https://api.github.com/repos/codecov/example-delphi/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-delphi/downloads","issues_url":"https://api.github.com/repos/codecov/example-delphi/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-delphi/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-delphi/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-delphi/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-delphi/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-delphi/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-delphi/deployments","created_at":"2015-11-02T10:16:22Z","updated_at":"2022-10-31T20:32:14Z","pushed_at":"2022-10-31T20:30:34Z","git_url":"git://github.com/codecov/example-delphi.git","ssh_url":"git@github.com:codecov/example-delphi.git","clone_url":"https://github.com/codecov/example-delphi.git","svn_url":"https://github.com/codecov/example-delphi","homepage":null,"size":1,"stargazers_count":2,"watchers_count":2,"language":null,"has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":2,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":2,"open_issues":0,"watchers":2,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":70835757,"node_id":"MDEwOlJlcG9zaXRvcnk3MDgzNTc1Nw==","name":"example-elixir","full_name":"codecov/example-elixir","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-elixir","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/example-elixir","forks_url":"https://api.github.com/repos/codecov/example-elixir/forks","keys_url":"https://api.github.com/repos/codecov/example-elixir/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-elixir/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-elixir/teams","hooks_url":"https://api.github.com/repos/codecov/example-elixir/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-elixir/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-elixir/events","assignees_url":"https://api.github.com/repos/codecov/example-elixir/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-elixir/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-elixir/tags","blobs_url":"https://api.github.com/repos/codecov/example-elixir/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-elixir/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-elixir/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-elixir/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-elixir/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-elixir/languages","stargazers_url":"https://api.github.com/repos/codecov/example-elixir/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-elixir/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-elixir/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-elixir/subscription","commits_url":"https://api.github.com/repos/codecov/example-elixir/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-elixir/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-elixir/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-elixir/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-elixir/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-elixir/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-elixir/merges","archive_url":"https://api.github.com/repos/codecov/example-elixir/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-elixir/downloads","issues_url":"https://api.github.com/repos/codecov/example-elixir/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-elixir/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-elixir/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-elixir/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-elixir/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-elixir/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-elixir/deployments","created_at":"2016-10-13T18:24:48Z","updated_at":"2023-07-25T14:04:50Z","pushed_at":"2022-10-31T20:30:43Z","git_url":"git://github.com/codecov/example-elixir.git","ssh_url":"git@github.com:codecov/example-elixir.git","clone_url":"https://github.com/codecov/example-elixir.git","svn_url":"https://github.com/codecov/example-elixir","homepage":null,"size":7,"stargazers_count":16,"watchers_count":16,"language":"Elixir","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":12,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["codecov","coverage","elixir","mix"],"visibility":"public","forks":12,"open_issues":0,"watchers":16,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":37341433,"node_id":"MDEwOlJlcG9zaXRvcnkzNzM0MTQzMw==","name":"example-erlang","full_name":"codecov/example-erlang","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-erlang","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/example-erlang","forks_url":"https://api.github.com/repos/codecov/example-erlang/forks","keys_url":"https://api.github.com/repos/codecov/example-erlang/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-erlang/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-erlang/teams","hooks_url":"https://api.github.com/repos/codecov/example-erlang/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-erlang/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-erlang/events","assignees_url":"https://api.github.com/repos/codecov/example-erlang/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-erlang/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-erlang/tags","blobs_url":"https://api.github.com/repos/codecov/example-erlang/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-erlang/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-erlang/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-erlang/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-erlang/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-erlang/languages","stargazers_url":"https://api.github.com/repos/codecov/example-erlang/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-erlang/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-erlang/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-erlang/subscription","commits_url":"https://api.github.com/repos/codecov/example-erlang/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-erlang/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-erlang/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-erlang/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-erlang/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-erlang/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-erlang/merges","archive_url":"https://api.github.com/repos/codecov/example-erlang/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-erlang/downloads","issues_url":"https://api.github.com/repos/codecov/example-erlang/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-erlang/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-erlang/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-erlang/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-erlang/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-erlang/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-erlang/deployments","created_at":"2015-06-12T19:49:05Z","updated_at":"2023-07-25T13:56:42Z","pushed_at":"2022-10-31T20:30:55Z","git_url":"git://github.com/codecov/example-erlang.git","ssh_url":"git@github.com:codecov/example-erlang.git","clone_url":"https://github.com/codecov/example-erlang.git","svn_url":"https://github.com/codecov/example-erlang","homepage":null,"size":10,"stargazers_count":4,"watchers_count":4,"language":"Erlang","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":6,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":6,"open_issues":1,"watchers":4,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":37135271,"node_id":"MDEwOlJlcG9zaXRvcnkzNzEzNTI3MQ==","name":"example-fortran","full_name":"codecov/example-fortran","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-fortran","description":"Example + repo for uploading reports to Codecov","fork":false,"url":"https://api.github.com/repos/codecov/example-fortran","forks_url":"https://api.github.com/repos/codecov/example-fortran/forks","keys_url":"https://api.github.com/repos/codecov/example-fortran/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-fortran/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-fortran/teams","hooks_url":"https://api.github.com/repos/codecov/example-fortran/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-fortran/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-fortran/events","assignees_url":"https://api.github.com/repos/codecov/example-fortran/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-fortran/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-fortran/tags","blobs_url":"https://api.github.com/repos/codecov/example-fortran/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-fortran/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-fortran/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-fortran/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-fortran/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-fortran/languages","stargazers_url":"https://api.github.com/repos/codecov/example-fortran/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-fortran/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-fortran/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-fortran/subscription","commits_url":"https://api.github.com/repos/codecov/example-fortran/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-fortran/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-fortran/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-fortran/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-fortran/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-fortran/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-fortran/merges","archive_url":"https://api.github.com/repos/codecov/example-fortran/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-fortran/downloads","issues_url":"https://api.github.com/repos/codecov/example-fortran/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-fortran/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-fortran/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-fortran/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-fortran/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-fortran/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-fortran/deployments","created_at":"2015-06-09T13:56:31Z","updated_at":"2022-10-31T20:32:08Z","pushed_at":"2022-10-31T20:31:08Z","git_url":"git://github.com/codecov/example-fortran.git","ssh_url":"git@github.com:codecov/example-fortran.git","clone_url":"https://github.com/codecov/example-fortran.git","svn_url":"https://github.com/codecov/example-fortran","homepage":"https://codecov.io","size":46,"stargazers_count":22,"watchers_count":22,"language":"Fortran","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":8,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":8,"open_issues":0,"watchers":22,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":116717242,"node_id":"MDEwOlJlcG9zaXRvcnkxMTY3MTcyNDI=","name":"example-fsharp","full_name":"codecov/example-fsharp","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-fsharp","description":"Example + of codecov in fsharp","fork":false,"url":"https://api.github.com/repos/codecov/example-fsharp","forks_url":"https://api.github.com/repos/codecov/example-fsharp/forks","keys_url":"https://api.github.com/repos/codecov/example-fsharp/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-fsharp/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-fsharp/teams","hooks_url":"https://api.github.com/repos/codecov/example-fsharp/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-fsharp/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-fsharp/events","assignees_url":"https://api.github.com/repos/codecov/example-fsharp/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-fsharp/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-fsharp/tags","blobs_url":"https://api.github.com/repos/codecov/example-fsharp/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-fsharp/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-fsharp/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-fsharp/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-fsharp/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-fsharp/languages","stargazers_url":"https://api.github.com/repos/codecov/example-fsharp/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-fsharp/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-fsharp/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-fsharp/subscription","commits_url":"https://api.github.com/repos/codecov/example-fsharp/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-fsharp/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-fsharp/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-fsharp/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-fsharp/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-fsharp/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-fsharp/merges","archive_url":"https://api.github.com/repos/codecov/example-fsharp/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-fsharp/downloads","issues_url":"https://api.github.com/repos/codecov/example-fsharp/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-fsharp/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-fsharp/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-fsharp/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-fsharp/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-fsharp/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-fsharp/deployments","created_at":"2018-01-08T19:14:02Z","updated_at":"2023-07-25T14:14:05Z","pushed_at":"2020-09-04T02:46:39Z","git_url":"git://github.com/codecov/example-fsharp.git","ssh_url":"git@github.com:codecov/example-fsharp.git","clone_url":"https://github.com/codecov/example-fsharp.git","svn_url":"https://github.com/codecov/example-fsharp","homepage":null,"size":18,"stargazers_count":6,"watchers_count":6,"language":"F#","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":6,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":6,"open_issues":0,"watchers":6,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":24332073,"node_id":"MDEwOlJlcG9zaXRvcnkyNDMzMjA3Mw==","name":"example-go","full_name":"codecov/example-go","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-go","description":"Go + coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-go","forks_url":"https://api.github.com/repos/codecov/example-go/forks","keys_url":"https://api.github.com/repos/codecov/example-go/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-go/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-go/teams","hooks_url":"https://api.github.com/repos/codecov/example-go/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-go/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-go/events","assignees_url":"https://api.github.com/repos/codecov/example-go/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-go/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-go/tags","blobs_url":"https://api.github.com/repos/codecov/example-go/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-go/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-go/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-go/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-go/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-go/languages","stargazers_url":"https://api.github.com/repos/codecov/example-go/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-go/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-go/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-go/subscription","commits_url":"https://api.github.com/repos/codecov/example-go/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-go/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-go/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-go/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-go/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-go/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-go/merges","archive_url":"https://api.github.com/repos/codecov/example-go/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-go/downloads","issues_url":"https://api.github.com/repos/codecov/example-go/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-go/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-go/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-go/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-go/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-go/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-go/deployments","created_at":"2014-09-22T14:33:53Z","updated_at":"2023-09-19T21:36:43Z","pushed_at":"2023-09-16T05:06:37Z","git_url":"git://github.com/codecov/example-go.git","ssh_url":"git@github.com:codecov/example-go.git","clone_url":"https://github.com/codecov/example-go.git","svn_url":"https://github.com/codecov/example-go","homepage":"https://codecov.io","size":47,"stargazers_count":207,"watchers_count":207,"language":"Go","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":102,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["coverage","go","gocov"],"visibility":"public","forks":102,"open_issues":1,"watchers":207,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":279392693,"node_id":"MDEwOlJlcG9zaXRvcnkyNzkzOTI2OTM=","name":"example-gradle-multiproject","full_name":"codecov/example-gradle-multiproject","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-gradle-multiproject","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/example-gradle-multiproject","forks_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/forks","keys_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/teams","hooks_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/events","assignees_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/tags","blobs_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/languages","stargazers_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/subscription","commits_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/merges","archive_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/downloads","issues_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-gradle-multiproject/deployments","created_at":"2020-07-13T19:23:30Z","updated_at":"2023-07-25T14:37:25Z","pushed_at":"2022-10-31T20:44:06Z","git_url":"git://github.com/codecov/example-gradle-multiproject.git","ssh_url":"git@github.com:codecov/example-gradle-multiproject.git","clone_url":"https://github.com/codecov/example-gradle-multiproject.git","svn_url":"https://github.com/codecov/example-gradle-multiproject","homepage":"","size":62,"stargazers_count":1,"watchers_count":1,"language":"Java","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":4,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["codecov","gradle","jacoco","java"],"visibility":"public","forks":4,"open_issues":0,"watchers":1,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":24567550,"node_id":"MDEwOlJlcG9zaXRvcnkyNDU2NzU1MA==","name":"example-groovy","full_name":"codecov/example-groovy","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-groovy","description":"Groovy + coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-groovy","forks_url":"https://api.github.com/repos/codecov/example-groovy/forks","keys_url":"https://api.github.com/repos/codecov/example-groovy/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-groovy/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-groovy/teams","hooks_url":"https://api.github.com/repos/codecov/example-groovy/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-groovy/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-groovy/events","assignees_url":"https://api.github.com/repos/codecov/example-groovy/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-groovy/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-groovy/tags","blobs_url":"https://api.github.com/repos/codecov/example-groovy/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-groovy/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-groovy/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-groovy/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-groovy/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-groovy/languages","stargazers_url":"https://api.github.com/repos/codecov/example-groovy/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-groovy/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-groovy/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-groovy/subscription","commits_url":"https://api.github.com/repos/codecov/example-groovy/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-groovy/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-groovy/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-groovy/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-groovy/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-groovy/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-groovy/merges","archive_url":"https://api.github.com/repos/codecov/example-groovy/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-groovy/downloads","issues_url":"https://api.github.com/repos/codecov/example-groovy/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-groovy/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-groovy/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-groovy/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-groovy/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-groovy/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-groovy/deployments","created_at":"2014-09-28T18:53:21Z","updated_at":"2023-07-25T13:53:21Z","pushed_at":"2022-10-31T20:31:41Z","git_url":"git://github.com/codecov/example-groovy.git","ssh_url":"git@github.com:codecov/example-groovy.git","clone_url":"https://github.com/codecov/example-groovy.git","svn_url":"https://github.com/codecov/example-groovy","homepage":"https://codecov.io","size":10,"stargazers_count":7,"watchers_count":7,"language":"Groovy","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":13,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["coverage","groovy","jacoco"],"visibility":"public","forks":13,"open_issues":1,"watchers":7,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":36934337,"node_id":"MDEwOlJlcG9zaXRvcnkzNjkzNDMzNw==","name":"example-haskell","full_name":"codecov/example-haskell","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-haskell","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/example-haskell","forks_url":"https://api.github.com/repos/codecov/example-haskell/forks","keys_url":"https://api.github.com/repos/codecov/example-haskell/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-haskell/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-haskell/teams","hooks_url":"https://api.github.com/repos/codecov/example-haskell/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-haskell/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-haskell/events","assignees_url":"https://api.github.com/repos/codecov/example-haskell/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-haskell/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-haskell/tags","blobs_url":"https://api.github.com/repos/codecov/example-haskell/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-haskell/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-haskell/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-haskell/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-haskell/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-haskell/languages","stargazers_url":"https://api.github.com/repos/codecov/example-haskell/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-haskell/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-haskell/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-haskell/subscription","commits_url":"https://api.github.com/repos/codecov/example-haskell/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-haskell/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-haskell/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-haskell/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-haskell/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-haskell/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-haskell/merges","archive_url":"https://api.github.com/repos/codecov/example-haskell/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-haskell/downloads","issues_url":"https://api.github.com/repos/codecov/example-haskell/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-haskell/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-haskell/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-haskell/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-haskell/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-haskell/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-haskell/deployments","created_at":"2015-06-05T13:32:50Z","updated_at":"2022-10-31T20:34:31Z","pushed_at":"2022-10-31T20:31:51Z","git_url":"git://github.com/codecov/example-haskell.git","ssh_url":"git@github.com:codecov/example-haskell.git","clone_url":"https://github.com/codecov/example-haskell.git","svn_url":"https://github.com/codecov/example-haskell","homepage":null,"size":2,"stargazers_count":3,"watchers_count":3,"language":null,"has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":4,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":4,"open_issues":1,"watchers":3,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":24567516,"node_id":"MDEwOlJlcG9zaXRvcnkyNDU2NzUxNg==","name":"example-java","full_name":"codecov/example-java","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-java","description":"Java + Example","fork":false,"url":"https://api.github.com/repos/codecov/example-java","forks_url":"https://api.github.com/repos/codecov/example-java/forks","keys_url":"https://api.github.com/repos/codecov/example-java/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-java/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-java/teams","hooks_url":"https://api.github.com/repos/codecov/example-java/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-java/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-java/events","assignees_url":"https://api.github.com/repos/codecov/example-java/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-java/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-java/tags","blobs_url":"https://api.github.com/repos/codecov/example-java/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-java/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-java/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-java/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-java/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-java/languages","stargazers_url":"https://api.github.com/repos/codecov/example-java/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-java/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-java/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-java/subscription","commits_url":"https://api.github.com/repos/codecov/example-java/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-java/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-java/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-java/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-java/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-java/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-java/merges","archive_url":"https://api.github.com/repos/codecov/example-java/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-java/downloads","issues_url":"https://api.github.com/repos/codecov/example-java/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-java/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-java/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-java/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-java/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-java/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-java/deployments","created_at":"2014-09-28T18:52:03Z","updated_at":"2023-08-02T14:52:36Z","pushed_at":"2023-02-08T21:31:29Z","git_url":"git://github.com/codecov/example-java.git","ssh_url":"git@github.com:codecov/example-java.git","clone_url":"https://github.com/codecov/example-java.git","svn_url":"https://github.com/codecov/example-java","homepage":"https://codecov.io","size":103,"stargazers_count":70,"watchers_count":70,"language":null,"has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":278,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["coverage","jacoco","java"],"visibility":"public","forks":278,"open_issues":1,"watchers":70,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":53363022,"node_id":"MDEwOlJlcG9zaXRvcnk1MzM2MzAyMg==","name":"example-java-gradle","full_name":"codecov/example-java-gradle","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-java-gradle","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/example-java-gradle","forks_url":"https://api.github.com/repos/codecov/example-java-gradle/forks","keys_url":"https://api.github.com/repos/codecov/example-java-gradle/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-java-gradle/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-java-gradle/teams","hooks_url":"https://api.github.com/repos/codecov/example-java-gradle/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-java-gradle/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-java-gradle/events","assignees_url":"https://api.github.com/repos/codecov/example-java-gradle/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-java-gradle/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-java-gradle/tags","blobs_url":"https://api.github.com/repos/codecov/example-java-gradle/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-java-gradle/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-java-gradle/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-java-gradle/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-java-gradle/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-java-gradle/languages","stargazers_url":"https://api.github.com/repos/codecov/example-java-gradle/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-java-gradle/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-java-gradle/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-java-gradle/subscription","commits_url":"https://api.github.com/repos/codecov/example-java-gradle/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-java-gradle/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-java-gradle/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-java-gradle/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-java-gradle/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-java-gradle/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-java-gradle/merges","archive_url":"https://api.github.com/repos/codecov/example-java-gradle/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-java-gradle/downloads","issues_url":"https://api.github.com/repos/codecov/example-java-gradle/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-java-gradle/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-java-gradle/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-java-gradle/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-java-gradle/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-java-gradle/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-java-gradle/deployments","created_at":"2016-03-07T22:13:27Z","updated_at":"2023-07-25T14:00:44Z","pushed_at":"2023-09-16T05:07:38Z","git_url":"git://github.com/codecov/example-java-gradle.git","ssh_url":"git@github.com:codecov/example-java-gradle.git","clone_url":"https://github.com/codecov/example-java-gradle.git","svn_url":"https://github.com/codecov/example-java-gradle","homepage":null,"size":148,"stargazers_count":82,"watchers_count":82,"language":"Java","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":79,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["codecov","coverage","jacoco","java"],"visibility":"public","forks":79,"open_issues":2,"watchers":82,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":66935478,"node_id":"MDEwOlJlcG9zaXRvcnk2NjkzNTQ3OA==","name":"example-java-maven","full_name":"codecov/example-java-maven","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-java-maven","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/example-java-maven","forks_url":"https://api.github.com/repos/codecov/example-java-maven/forks","keys_url":"https://api.github.com/repos/codecov/example-java-maven/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-java-maven/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-java-maven/teams","hooks_url":"https://api.github.com/repos/codecov/example-java-maven/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-java-maven/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-java-maven/events","assignees_url":"https://api.github.com/repos/codecov/example-java-maven/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-java-maven/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-java-maven/tags","blobs_url":"https://api.github.com/repos/codecov/example-java-maven/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-java-maven/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-java-maven/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-java-maven/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-java-maven/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-java-maven/languages","stargazers_url":"https://api.github.com/repos/codecov/example-java-maven/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-java-maven/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-java-maven/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-java-maven/subscription","commits_url":"https://api.github.com/repos/codecov/example-java-maven/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-java-maven/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-java-maven/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-java-maven/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-java-maven/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-java-maven/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-java-maven/merges","archive_url":"https://api.github.com/repos/codecov/example-java-maven/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-java-maven/downloads","issues_url":"https://api.github.com/repos/codecov/example-java-maven/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-java-maven/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-java-maven/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-java-maven/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-java-maven/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-java-maven/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-java-maven/deployments","created_at":"2016-08-30T11:41:58Z","updated_at":"2023-07-25T14:03:42Z","pushed_at":"2023-09-16T05:05:23Z","git_url":"git://github.com/codecov/example-java-maven.git","ssh_url":"git@github.com:codecov/example-java-maven.git","clone_url":"https://github.com/codecov/example-java-maven.git","svn_url":"https://github.com/codecov/example-java-maven","homepage":null,"size":41,"stargazers_count":53,"watchers_count":53,"language":"Java","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":109,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["coverage","java","maven"],"visibility":"public","forks":109,"open_issues":2,"watchers":53,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":37738341,"node_id":"MDEwOlJlcG9zaXRvcnkzNzczODM0MQ==","name":"example-julia","full_name":"codecov/example-julia","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-julia","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/example-julia","forks_url":"https://api.github.com/repos/codecov/example-julia/forks","keys_url":"https://api.github.com/repos/codecov/example-julia/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-julia/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-julia/teams","hooks_url":"https://api.github.com/repos/codecov/example-julia/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-julia/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-julia/events","assignees_url":"https://api.github.com/repos/codecov/example-julia/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-julia/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-julia/tags","blobs_url":"https://api.github.com/repos/codecov/example-julia/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-julia/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-julia/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-julia/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-julia/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-julia/languages","stargazers_url":"https://api.github.com/repos/codecov/example-julia/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-julia/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-julia/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-julia/subscription","commits_url":"https://api.github.com/repos/codecov/example-julia/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-julia/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-julia/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-julia/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-julia/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-julia/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-julia/merges","archive_url":"https://api.github.com/repos/codecov/example-julia/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-julia/downloads","issues_url":"https://api.github.com/repos/codecov/example-julia/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-julia/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-julia/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-julia/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-julia/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-julia/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-julia/deployments","created_at":"2015-06-19T18:15:13Z","updated_at":"2023-07-25T13:56:48Z","pushed_at":"2023-09-16T05:06:43Z","git_url":"git://github.com/codecov/example-julia.git","ssh_url":"git@github.com:codecov/example-julia.git","clone_url":"https://github.com/codecov/example-julia.git","svn_url":"https://github.com/codecov/example-julia","homepage":null,"size":26,"stargazers_count":11,"watchers_count":11,"language":"Julia","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":9,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":9,"open_issues":0,"watchers":11,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":24567577,"node_id":"MDEwOlJlcG9zaXRvcnkyNDU2NzU3Nw==","name":"example-kotlin","full_name":"codecov/example-kotlin","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-kotlin","description":"Kotlin + coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-kotlin","forks_url":"https://api.github.com/repos/codecov/example-kotlin/forks","keys_url":"https://api.github.com/repos/codecov/example-kotlin/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-kotlin/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-kotlin/teams","hooks_url":"https://api.github.com/repos/codecov/example-kotlin/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-kotlin/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-kotlin/events","assignees_url":"https://api.github.com/repos/codecov/example-kotlin/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-kotlin/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-kotlin/tags","blobs_url":"https://api.github.com/repos/codecov/example-kotlin/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-kotlin/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-kotlin/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-kotlin/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-kotlin/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-kotlin/languages","stargazers_url":"https://api.github.com/repos/codecov/example-kotlin/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-kotlin/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-kotlin/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-kotlin/subscription","commits_url":"https://api.github.com/repos/codecov/example-kotlin/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-kotlin/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-kotlin/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-kotlin/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-kotlin/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-kotlin/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-kotlin/merges","archive_url":"https://api.github.com/repos/codecov/example-kotlin/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-kotlin/downloads","issues_url":"https://api.github.com/repos/codecov/example-kotlin/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-kotlin/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-kotlin/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-kotlin/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-kotlin/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-kotlin/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-kotlin/deployments","created_at":"2014-09-28T18:54:08Z","updated_at":"2023-09-06T23:44:35Z","pushed_at":"2023-01-23T18:28:48Z","git_url":"git://github.com/codecov/example-kotlin.git","ssh_url":"git@github.com:codecov/example-kotlin.git","clone_url":"https://github.com/codecov/example-kotlin.git","svn_url":"https://github.com/codecov/example-kotlin","homepage":"https://codecov.io","size":21,"stargazers_count":20,"watchers_count":20,"language":"Kotlin","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":21,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":21,"open_issues":1,"watchers":20,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":290263618,"node_id":"MDEwOlJlcG9zaXRvcnkyOTAyNjM2MTg=","name":"example-kotlin-flat","full_name":"codecov/example-kotlin-flat","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-kotlin-flat","description":"Kotlin + codecov example with standard file structure","fork":false,"url":"https://api.github.com/repos/codecov/example-kotlin-flat","forks_url":"https://api.github.com/repos/codecov/example-kotlin-flat/forks","keys_url":"https://api.github.com/repos/codecov/example-kotlin-flat/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-kotlin-flat/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-kotlin-flat/teams","hooks_url":"https://api.github.com/repos/codecov/example-kotlin-flat/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-kotlin-flat/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-kotlin-flat/events","assignees_url":"https://api.github.com/repos/codecov/example-kotlin-flat/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-kotlin-flat/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-kotlin-flat/tags","blobs_url":"https://api.github.com/repos/codecov/example-kotlin-flat/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-kotlin-flat/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-kotlin-flat/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-kotlin-flat/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-kotlin-flat/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-kotlin-flat/languages","stargazers_url":"https://api.github.com/repos/codecov/example-kotlin-flat/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-kotlin-flat/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-kotlin-flat/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-kotlin-flat/subscription","commits_url":"https://api.github.com/repos/codecov/example-kotlin-flat/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-kotlin-flat/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-kotlin-flat/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-kotlin-flat/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-kotlin-flat/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-kotlin-flat/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-kotlin-flat/merges","archive_url":"https://api.github.com/repos/codecov/example-kotlin-flat/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-kotlin-flat/downloads","issues_url":"https://api.github.com/repos/codecov/example-kotlin-flat/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-kotlin-flat/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-kotlin-flat/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-kotlin-flat/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-kotlin-flat/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-kotlin-flat/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-kotlin-flat/deployments","created_at":"2020-08-25T16:16:12Z","updated_at":"2022-11-18T12:37:33Z","pushed_at":"2022-10-31T20:43:13Z","git_url":"git://github.com/codecov/example-kotlin-flat.git","ssh_url":"git@github.com:codecov/example-kotlin-flat.git","clone_url":"https://github.com/codecov/example-kotlin-flat.git","svn_url":"https://github.com/codecov/example-kotlin-flat","homepage":null,"size":65,"stargazers_count":2,"watchers_count":2,"language":"Kotlin","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":2,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":2,"open_issues":0,"watchers":2,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":39953512,"node_id":"MDEwOlJlcG9zaXRvcnkzOTk1MzUxMg==","name":"example-lua","full_name":"codecov/example-lua","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-lua","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/example-lua","forks_url":"https://api.github.com/repos/codecov/example-lua/forks","keys_url":"https://api.github.com/repos/codecov/example-lua/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-lua/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-lua/teams","hooks_url":"https://api.github.com/repos/codecov/example-lua/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-lua/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-lua/events","assignees_url":"https://api.github.com/repos/codecov/example-lua/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-lua/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-lua/tags","blobs_url":"https://api.github.com/repos/codecov/example-lua/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-lua/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-lua/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-lua/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-lua/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-lua/languages","stargazers_url":"https://api.github.com/repos/codecov/example-lua/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-lua/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-lua/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-lua/subscription","commits_url":"https://api.github.com/repos/codecov/example-lua/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-lua/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-lua/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-lua/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-lua/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-lua/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-lua/merges","archive_url":"https://api.github.com/repos/codecov/example-lua/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-lua/downloads","issues_url":"https://api.github.com/repos/codecov/example-lua/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-lua/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-lua/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-lua/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-lua/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-lua/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-lua/deployments","created_at":"2015-07-30T14:01:24Z","updated_at":"2023-07-25T13:57:21Z","pushed_at":"2022-10-31T20:34:01Z","git_url":"git://github.com/codecov/example-lua.git","ssh_url":"git@github.com:codecov/example-lua.git","clone_url":"https://github.com/codecov/example-lua.git","svn_url":"https://github.com/codecov/example-lua","homepage":null,"size":33,"stargazers_count":7,"watchers_count":7,"language":"Shell","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":8,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["codecov","coverage","lua","luacov"],"visibility":"public","forks":8,"open_issues":0,"watchers":7,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":32312776,"node_id":"MDEwOlJlcG9zaXRvcnkzMjMxMjc3Ng==","name":"example-node","full_name":"codecov/example-node","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-node","description":"Example + repo for uploading reports to Codecov","fork":false,"url":"https://api.github.com/repos/codecov/example-node","forks_url":"https://api.github.com/repos/codecov/example-node/forks","keys_url":"https://api.github.com/repos/codecov/example-node/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-node/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-node/teams","hooks_url":"https://api.github.com/repos/codecov/example-node/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-node/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-node/events","assignees_url":"https://api.github.com/repos/codecov/example-node/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-node/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-node/tags","blobs_url":"https://api.github.com/repos/codecov/example-node/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-node/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-node/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-node/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-node/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-node/languages","stargazers_url":"https://api.github.com/repos/codecov/example-node/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-node/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-node/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-node/subscription","commits_url":"https://api.github.com/repos/codecov/example-node/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-node/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-node/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-node/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-node/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-node/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-node/merges","archive_url":"https://api.github.com/repos/codecov/example-node/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-node/downloads","issues_url":"https://api.github.com/repos/codecov/example-node/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-node/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-node/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-node/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-node/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-node/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-node/deployments","created_at":"2015-03-16T09:01:54Z","updated_at":"2023-06-26T03:55:29Z","pushed_at":"2023-01-19T22:22:30Z","git_url":"git://github.com/codecov/example-node.git","ssh_url":"git@github.com:codecov/example-node.git","clone_url":"https://github.com/codecov/example-node.git","svn_url":"https://github.com/codecov/example-node","homepage":"https://codecov.io","size":58,"stargazers_count":190,"watchers_count":190,"language":"JavaScript","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":84,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":4,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["istanbul","javascript","node"],"visibility":"public","forks":84,"open_issues":4,"watchers":190,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":35696045,"node_id":"MDEwOlJlcG9zaXRvcnkzNTY5NjA0NQ==","name":"example-objc","full_name":"codecov/example-objc","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-objc","description":"Codecov + example for Xcode","fork":false,"url":"https://api.github.com/repos/codecov/example-objc","forks_url":"https://api.github.com/repos/codecov/example-objc/forks","keys_url":"https://api.github.com/repos/codecov/example-objc/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-objc/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-objc/teams","hooks_url":"https://api.github.com/repos/codecov/example-objc/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-objc/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-objc/events","assignees_url":"https://api.github.com/repos/codecov/example-objc/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-objc/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-objc/tags","blobs_url":"https://api.github.com/repos/codecov/example-objc/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-objc/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-objc/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-objc/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-objc/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-objc/languages","stargazers_url":"https://api.github.com/repos/codecov/example-objc/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-objc/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-objc/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-objc/subscription","commits_url":"https://api.github.com/repos/codecov/example-objc/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-objc/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-objc/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-objc/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-objc/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-objc/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-objc/merges","archive_url":"https://api.github.com/repos/codecov/example-objc/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-objc/downloads","issues_url":"https://api.github.com/repos/codecov/example-objc/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-objc/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-objc/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-objc/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-objc/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-objc/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-objc/deployments","created_at":"2015-05-15T20:49:04Z","updated_at":"2023-07-25T13:56:15Z","pushed_at":"2022-10-31T20:34:23Z","git_url":"git://github.com/codecov/example-objc.git","ssh_url":"git@github.com:codecov/example-objc.git","clone_url":"https://github.com/codecov/example-objc.git","svn_url":"https://github.com/codecov/example-objc","homepage":"https://codecov.io","size":59,"stargazers_count":26,"watchers_count":26,"language":"Objective-C","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":15,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":3,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["codecov","coverage","xcode"],"visibility":"public","forks":15,"open_issues":3,"watchers":26,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":40403363,"node_id":"MDEwOlJlcG9zaXRvcnk0MDQwMzM2Mw==","name":"example-perl","full_name":"codecov/example-perl","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-perl","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/example-perl","forks_url":"https://api.github.com/repos/codecov/example-perl/forks","keys_url":"https://api.github.com/repos/codecov/example-perl/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-perl/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-perl/teams","hooks_url":"https://api.github.com/repos/codecov/example-perl/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-perl/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-perl/events","assignees_url":"https://api.github.com/repos/codecov/example-perl/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-perl/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-perl/tags","blobs_url":"https://api.github.com/repos/codecov/example-perl/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-perl/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-perl/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-perl/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-perl/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-perl/languages","stargazers_url":"https://api.github.com/repos/codecov/example-perl/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-perl/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-perl/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-perl/subscription","commits_url":"https://api.github.com/repos/codecov/example-perl/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-perl/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-perl/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-perl/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-perl/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-perl/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-perl/merges","archive_url":"https://api.github.com/repos/codecov/example-perl/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-perl/downloads","issues_url":"https://api.github.com/repos/codecov/example-perl/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-perl/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-perl/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-perl/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-perl/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-perl/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-perl/deployments","created_at":"2015-08-08T13:16:02Z","updated_at":"2023-08-04T02:47:43Z","pushed_at":"2023-02-08T20:36:40Z","git_url":"git://github.com/codecov/example-perl.git","ssh_url":"git@github.com:codecov/example-perl.git","clone_url":"https://github.com/codecov/example-perl.git","svn_url":"https://github.com/codecov/example-perl","homepage":null,"size":26,"stargazers_count":22,"watchers_count":22,"language":"Perl","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":12,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":12,"open_issues":1,"watchers":22,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":23512977,"node_id":"MDEwOlJlcG9zaXRvcnkyMzUxMjk3Nw==","name":"example-php","full_name":"codecov/example-php","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-php","description":"PHP + coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-php","forks_url":"https://api.github.com/repos/codecov/example-php/forks","keys_url":"https://api.github.com/repos/codecov/example-php/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-php/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-php/teams","hooks_url":"https://api.github.com/repos/codecov/example-php/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-php/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-php/events","assignees_url":"https://api.github.com/repos/codecov/example-php/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-php/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-php/tags","blobs_url":"https://api.github.com/repos/codecov/example-php/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-php/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-php/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-php/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-php/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-php/languages","stargazers_url":"https://api.github.com/repos/codecov/example-php/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-php/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-php/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-php/subscription","commits_url":"https://api.github.com/repos/codecov/example-php/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-php/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-php/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-php/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-php/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-php/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-php/merges","archive_url":"https://api.github.com/repos/codecov/example-php/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-php/downloads","issues_url":"https://api.github.com/repos/codecov/example-php/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-php/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-php/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-php/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-php/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-php/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-php/deployments","created_at":"2014-08-31T12:23:18Z","updated_at":"2023-09-03T15:53:17Z","pushed_at":"2023-09-22T11:14:17Z","git_url":"git://github.com/codecov/example-php.git","ssh_url":"git@github.com:codecov/example-php.git","clone_url":"https://github.com/codecov/example-php.git","svn_url":"https://github.com/codecov/example-php","homepage":"https://codecov.io","size":102,"stargazers_count":86,"watchers_count":86,"language":"PHP","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":72,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["clover","coverage"],"visibility":"public","forks":72,"open_issues":2,"watchers":86,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":24344106,"node_id":"MDEwOlJlcG9zaXRvcnkyNDM0NDEwNg==","name":"example-python","full_name":"codecov/example-python","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-python","description":"Python + coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-python","forks_url":"https://api.github.com/repos/codecov/example-python/forks","keys_url":"https://api.github.com/repos/codecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-python/teams","hooks_url":"https://api.github.com/repos/codecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-python/events","assignees_url":"https://api.github.com/repos/codecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-python/tags","blobs_url":"https://api.github.com/repos/codecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-python/languages","stargazers_url":"https://api.github.com/repos/codecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-python/subscription","commits_url":"https://api.github.com/repos/codecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-python/merges","archive_url":"https://api.github.com/repos/codecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-python/downloads","issues_url":"https://api.github.com/repos/codecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-python/deployments","created_at":"2014-09-22T20:20:06Z","updated_at":"2023-07-04T13:31:40Z","pushed_at":"2023-09-22T06:25:35Z","git_url":"git://github.com/codecov/example-python.git","ssh_url":"git@github.com:codecov/example-python.git","clone_url":"https://github.com/codecov/example-python.git","svn_url":"https://github.com/codecov/example-python","homepage":"https://codecov.io","size":331,"stargazers_count":291,"watchers_count":291,"language":"Python","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":279,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":4,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["coverage","coveragepy"],"visibility":"public","forks":279,"open_issues":4,"watchers":291,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":32196875,"node_id":"MDEwOlJlcG9zaXRvcnkzMjE5Njg3NQ==","name":"example-r","full_name":"codecov/example-r","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-r","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/example-r","forks_url":"https://api.github.com/repos/codecov/example-r/forks","keys_url":"https://api.github.com/repos/codecov/example-r/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-r/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-r/teams","hooks_url":"https://api.github.com/repos/codecov/example-r/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-r/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-r/events","assignees_url":"https://api.github.com/repos/codecov/example-r/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-r/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-r/tags","blobs_url":"https://api.github.com/repos/codecov/example-r/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-r/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-r/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-r/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-r/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-r/languages","stargazers_url":"https://api.github.com/repos/codecov/example-r/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-r/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-r/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-r/subscription","commits_url":"https://api.github.com/repos/codecov/example-r/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-r/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-r/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-r/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-r/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-r/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-r/merges","archive_url":"https://api.github.com/repos/codecov/example-r/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-r/downloads","issues_url":"https://api.github.com/repos/codecov/example-r/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-r/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-r/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-r/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-r/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-r/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-r/deployments","created_at":"2015-03-14T05:08:48Z","updated_at":"2023-07-25T13:55:20Z","pushed_at":"2022-10-31T20:36:32Z","git_url":"git://github.com/codecov/example-r.git","ssh_url":"git@github.com:codecov/example-r.git","clone_url":"https://github.com/codecov/example-r.git","svn_url":"https://github.com/codecov/example-r","homepage":null,"size":69,"stargazers_count":17,"watchers_count":17,"language":"R","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":39,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":39,"open_issues":0,"watchers":17,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":24344263,"node_id":"MDEwOlJlcG9zaXRvcnkyNDM0NDI2Mw==","name":"example-ruby","full_name":"codecov/example-ruby","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-ruby","description":"Ruby + coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-ruby","forks_url":"https://api.github.com/repos/codecov/example-ruby/forks","keys_url":"https://api.github.com/repos/codecov/example-ruby/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-ruby/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-ruby/teams","hooks_url":"https://api.github.com/repos/codecov/example-ruby/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-ruby/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-ruby/events","assignees_url":"https://api.github.com/repos/codecov/example-ruby/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-ruby/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-ruby/tags","blobs_url":"https://api.github.com/repos/codecov/example-ruby/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-ruby/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-ruby/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-ruby/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-ruby/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-ruby/languages","stargazers_url":"https://api.github.com/repos/codecov/example-ruby/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-ruby/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-ruby/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-ruby/subscription","commits_url":"https://api.github.com/repos/codecov/example-ruby/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-ruby/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-ruby/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-ruby/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-ruby/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-ruby/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-ruby/merges","archive_url":"https://api.github.com/repos/codecov/example-ruby/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-ruby/downloads","issues_url":"https://api.github.com/repos/codecov/example-ruby/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-ruby/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-ruby/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-ruby/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-ruby/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-ruby/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-ruby/deployments","created_at":"2014-09-22T20:25:12Z","updated_at":"2022-07-27T06:08:40Z","pushed_at":"2023-09-16T05:07:08Z","git_url":"git://github.com/codecov/example-ruby.git","ssh_url":"git@github.com:codecov/example-ruby.git","clone_url":"https://github.com/codecov/example-ruby.git","svn_url":"https://github.com/codecov/example-ruby","homepage":"https://codecov.io","size":33,"stargazers_count":20,"watchers_count":20,"language":"Ruby","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":24,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["coverage","ruby","simpelcov"],"visibility":"public","forks":24,"open_issues":0,"watchers":20,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":47836521,"node_id":"MDEwOlJlcG9zaXRvcnk0NzgzNjUyMQ==","name":"example-rust","full_name":"codecov/example-rust","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-rust","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/example-rust","forks_url":"https://api.github.com/repos/codecov/example-rust/forks","keys_url":"https://api.github.com/repos/codecov/example-rust/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-rust/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-rust/teams","hooks_url":"https://api.github.com/repos/codecov/example-rust/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-rust/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-rust/events","assignees_url":"https://api.github.com/repos/codecov/example-rust/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-rust/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-rust/tags","blobs_url":"https://api.github.com/repos/codecov/example-rust/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-rust/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-rust/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-rust/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-rust/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-rust/languages","stargazers_url":"https://api.github.com/repos/codecov/example-rust/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-rust/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-rust/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-rust/subscription","commits_url":"https://api.github.com/repos/codecov/example-rust/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-rust/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-rust/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-rust/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-rust/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-rust/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-rust/merges","archive_url":"https://api.github.com/repos/codecov/example-rust/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-rust/downloads","issues_url":"https://api.github.com/repos/codecov/example-rust/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-rust/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-rust/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-rust/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-rust/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-rust/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-rust/deployments","created_at":"2015-12-11T16:07:49Z","updated_at":"2023-08-15T12:58:26Z","pushed_at":"2023-09-16T05:07:27Z","git_url":"git://github.com/codecov/example-rust.git","ssh_url":"git@github.com:codecov/example-rust.git","clone_url":"https://github.com/codecov/example-rust.git","svn_url":"https://github.com/codecov/example-rust","homepage":null,"size":40,"stargazers_count":86,"watchers_count":86,"language":"Rust","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":18,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":18,"open_issues":0,"watchers":86,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":24330864,"node_id":"MDEwOlJlcG9zaXRvcnkyNDMzMDg2NA==","name":"example-scala","full_name":"codecov/example-scala","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-scala","description":"Scala + coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-scala","forks_url":"https://api.github.com/repos/codecov/example-scala/forks","keys_url":"https://api.github.com/repos/codecov/example-scala/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-scala/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-scala/teams","hooks_url":"https://api.github.com/repos/codecov/example-scala/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-scala/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-scala/events","assignees_url":"https://api.github.com/repos/codecov/example-scala/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-scala/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-scala/tags","blobs_url":"https://api.github.com/repos/codecov/example-scala/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-scala/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-scala/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-scala/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-scala/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-scala/languages","stargazers_url":"https://api.github.com/repos/codecov/example-scala/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-scala/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-scala/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-scala/subscription","commits_url":"https://api.github.com/repos/codecov/example-scala/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-scala/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-scala/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-scala/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-scala/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-scala/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-scala/merges","archive_url":"https://api.github.com/repos/codecov/example-scala/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-scala/downloads","issues_url":"https://api.github.com/repos/codecov/example-scala/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-scala/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-scala/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-scala/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-scala/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-scala/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-scala/deployments","created_at":"2014-09-22T14:00:30Z","updated_at":"2023-08-19T19:18:50Z","pushed_at":"2022-10-31T20:36:56Z","git_url":"git://github.com/codecov/example-scala.git","ssh_url":"git@github.com:codecov/example-scala.git","clone_url":"https://github.com/codecov/example-scala.git","svn_url":"https://github.com/codecov/example-scala","homepage":"https://codecov.io/","size":59,"stargazers_count":35,"watchers_count":35,"language":"Scala","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":27,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["coverage","scala","scoverage"],"visibility":"public","forks":27,"open_issues":1,"watchers":35,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":24569996,"node_id":"MDEwOlJlcG9zaXRvcnkyNDU2OTk5Ng==","name":"example-scala-maven","full_name":"codecov/example-scala-maven","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-scala-maven","description":"Scala + via Maven coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-scala-maven","forks_url":"https://api.github.com/repos/codecov/example-scala-maven/forks","keys_url":"https://api.github.com/repos/codecov/example-scala-maven/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-scala-maven/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-scala-maven/teams","hooks_url":"https://api.github.com/repos/codecov/example-scala-maven/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-scala-maven/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-scala-maven/events","assignees_url":"https://api.github.com/repos/codecov/example-scala-maven/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-scala-maven/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-scala-maven/tags","blobs_url":"https://api.github.com/repos/codecov/example-scala-maven/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-scala-maven/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-scala-maven/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-scala-maven/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-scala-maven/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-scala-maven/languages","stargazers_url":"https://api.github.com/repos/codecov/example-scala-maven/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-scala-maven/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-scala-maven/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-scala-maven/subscription","commits_url":"https://api.github.com/repos/codecov/example-scala-maven/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-scala-maven/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-scala-maven/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-scala-maven/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-scala-maven/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-scala-maven/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-scala-maven/merges","archive_url":"https://api.github.com/repos/codecov/example-scala-maven/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-scala-maven/downloads","issues_url":"https://api.github.com/repos/codecov/example-scala-maven/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-scala-maven/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-scala-maven/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-scala-maven/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-scala-maven/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-scala-maven/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-scala-maven/deployments","created_at":"2014-09-28T20:42:41Z","updated_at":"2023-03-25T09:24:01Z","pushed_at":"2022-10-31T20:37:04Z","git_url":"git://github.com/codecov/example-scala-maven.git","ssh_url":"git@github.com:codecov/example-scala-maven.git","clone_url":"https://github.com/codecov/example-scala-maven.git","svn_url":"https://github.com/codecov/example-scala-maven","homepage":"https://codecov.io","size":9,"stargazers_count":3,"watchers_count":3,"language":"Scala","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":12,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":12,"open_issues":0,"watchers":3,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":42443308,"node_id":"MDEwOlJlcG9zaXRvcnk0MjQ0MzMwOA==","name":"example-swift","full_name":"codecov/example-swift","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-swift","description":"Codecov: + Swift coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-swift","forks_url":"https://api.github.com/repos/codecov/example-swift/forks","keys_url":"https://api.github.com/repos/codecov/example-swift/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-swift/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-swift/teams","hooks_url":"https://api.github.com/repos/codecov/example-swift/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-swift/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-swift/events","assignees_url":"https://api.github.com/repos/codecov/example-swift/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-swift/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-swift/tags","blobs_url":"https://api.github.com/repos/codecov/example-swift/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-swift/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-swift/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-swift/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-swift/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-swift/languages","stargazers_url":"https://api.github.com/repos/codecov/example-swift/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-swift/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-swift/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-swift/subscription","commits_url":"https://api.github.com/repos/codecov/example-swift/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-swift/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-swift/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-swift/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-swift/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-swift/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-swift/merges","archive_url":"https://api.github.com/repos/codecov/example-swift/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-swift/downloads","issues_url":"https://api.github.com/repos/codecov/example-swift/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-swift/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-swift/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-swift/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-swift/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-swift/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-swift/deployments","created_at":"2015-09-14T10:54:57Z","updated_at":"2023-07-25T13:57:59Z","pushed_at":"2022-10-31T20:37:11Z","git_url":"git://github.com/codecov/example-swift.git","ssh_url":"git@github.com:codecov/example-swift.git","clone_url":"https://github.com/codecov/example-swift.git","svn_url":"https://github.com/codecov/example-swift","homepage":"https://codecov.io","size":117,"stargazers_count":127,"watchers_count":127,"language":"Swift","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":84,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":3,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["codecov","coverage","gather-coverage-data","swift","xcode8"],"visibility":"public","forks":84,"open_issues":3,"watchers":127,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":276692401,"node_id":"MDEwOlJlcG9zaXRvcnkyNzY2OTI0MDE=","name":"example-systemverilog","full_name":"codecov/example-systemverilog","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-systemverilog","description":null,"fork":true,"url":"https://api.github.com/repos/codecov/example-systemverilog","forks_url":"https://api.github.com/repos/codecov/example-systemverilog/forks","keys_url":"https://api.github.com/repos/codecov/example-systemverilog/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-systemverilog/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-systemverilog/teams","hooks_url":"https://api.github.com/repos/codecov/example-systemverilog/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-systemverilog/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-systemverilog/events","assignees_url":"https://api.github.com/repos/codecov/example-systemverilog/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-systemverilog/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-systemverilog/tags","blobs_url":"https://api.github.com/repos/codecov/example-systemverilog/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-systemverilog/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-systemverilog/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-systemverilog/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-systemverilog/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-systemverilog/languages","stargazers_url":"https://api.github.com/repos/codecov/example-systemverilog/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-systemverilog/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-systemverilog/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-systemverilog/subscription","commits_url":"https://api.github.com/repos/codecov/example-systemverilog/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-systemverilog/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-systemverilog/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-systemverilog/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-systemverilog/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-systemverilog/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-systemverilog/merges","archive_url":"https://api.github.com/repos/codecov/example-systemverilog/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-systemverilog/downloads","issues_url":"https://api.github.com/repos/codecov/example-systemverilog/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-systemverilog/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-systemverilog/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-systemverilog/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-systemverilog/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-systemverilog/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-systemverilog/deployments","created_at":"2020-07-02T16:13:06Z","updated_at":"2023-07-25T14:37:06Z","pushed_at":"2023-09-16T05:06:32Z","git_url":"git://github.com/codecov/example-systemverilog.git","ssh_url":"git@github.com:codecov/example-systemverilog.git","clone_url":"https://github.com/codecov/example-systemverilog.git","svn_url":"https://github.com/codecov/example-systemverilog","homepage":null,"size":30,"stargazers_count":0,"watchers_count":0,"language":"Makefile","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":2,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"gpl-3.0","name":"GNU + General Public License v3.0","spdx_id":"GPL-3.0","url":"https://api.github.com/licenses/gpl-3.0","node_id":"MDc6TGljZW5zZTk="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":2,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":59709356,"node_id":"MDEwOlJlcG9zaXRvcnk1OTcwOTM1Ng==","name":"example-typescript","full_name":"codecov/example-typescript","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-typescript","description":"Example + repo for uploading reports to Codecov https://codecov.io","fork":false,"url":"https://api.github.com/repos/codecov/example-typescript","forks_url":"https://api.github.com/repos/codecov/example-typescript/forks","keys_url":"https://api.github.com/repos/codecov/example-typescript/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-typescript/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-typescript/teams","hooks_url":"https://api.github.com/repos/codecov/example-typescript/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-typescript/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-typescript/events","assignees_url":"https://api.github.com/repos/codecov/example-typescript/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-typescript/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-typescript/tags","blobs_url":"https://api.github.com/repos/codecov/example-typescript/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-typescript/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-typescript/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-typescript/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-typescript/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-typescript/languages","stargazers_url":"https://api.github.com/repos/codecov/example-typescript/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-typescript/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-typescript/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-typescript/subscription","commits_url":"https://api.github.com/repos/codecov/example-typescript/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-typescript/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-typescript/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-typescript/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-typescript/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-typescript/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-typescript/merges","archive_url":"https://api.github.com/repos/codecov/example-typescript/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-typescript/downloads","issues_url":"https://api.github.com/repos/codecov/example-typescript/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-typescript/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-typescript/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-typescript/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-typescript/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-typescript/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-typescript/deployments","created_at":"2016-05-26T01:19:38Z","updated_at":"2023-08-24T17:09:38Z","pushed_at":"2023-09-18T18:40:52Z","git_url":"git://github.com/codecov/example-typescript.git","ssh_url":"git@github.com:codecov/example-typescript.git","clone_url":"https://github.com/codecov/example-typescript.git","svn_url":"https://github.com/codecov/example-typescript","homepage":null,"size":1334,"stargazers_count":46,"watchers_count":46,"language":"TypeScript","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":78,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["codecov","coverage","istanbul","typescript"],"visibility":"public","forks":78,"open_issues":2,"watchers":46,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":61906398,"node_id":"MDEwOlJlcG9zaXRvcnk2MTkwNjM5OA==","name":"example-vala","full_name":"codecov/example-vala","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-vala","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/example-vala","forks_url":"https://api.github.com/repos/codecov/example-vala/forks","keys_url":"https://api.github.com/repos/codecov/example-vala/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-vala/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-vala/teams","hooks_url":"https://api.github.com/repos/codecov/example-vala/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-vala/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-vala/events","assignees_url":"https://api.github.com/repos/codecov/example-vala/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-vala/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-vala/tags","blobs_url":"https://api.github.com/repos/codecov/example-vala/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-vala/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-vala/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-vala/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-vala/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-vala/languages","stargazers_url":"https://api.github.com/repos/codecov/example-vala/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-vala/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-vala/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-vala/subscription","commits_url":"https://api.github.com/repos/codecov/example-vala/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-vala/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-vala/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-vala/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-vala/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-vala/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-vala/merges","archive_url":"https://api.github.com/repos/codecov/example-vala/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-vala/downloads","issues_url":"https://api.github.com/repos/codecov/example-vala/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-vala/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-vala/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-vala/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-vala/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-vala/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-vala/deployments","created_at":"2016-06-24T19:02:52Z","updated_at":"2023-07-25T14:02:32Z","pushed_at":"2022-10-31T20:37:22Z","git_url":"git://github.com/codecov/example-vala.git","ssh_url":"git@github.com:codecov/example-vala.git","clone_url":"https://github.com/codecov/example-vala.git","svn_url":"https://github.com/codecov/example-vala","homepage":null,"size":9,"stargazers_count":12,"watchers_count":12,"language":"Vala","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":5,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":5,"open_issues":0,"watchers":12,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":24567595,"node_id":"MDEwOlJlcG9zaXRvcnkyNDU2NzU5NQ==","name":"example-xtend","full_name":"codecov/example-xtend","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/example-xtend","description":"Xtend + coverage example","fork":false,"url":"https://api.github.com/repos/codecov/example-xtend","forks_url":"https://api.github.com/repos/codecov/example-xtend/forks","keys_url":"https://api.github.com/repos/codecov/example-xtend/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/example-xtend/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/example-xtend/teams","hooks_url":"https://api.github.com/repos/codecov/example-xtend/hooks","issue_events_url":"https://api.github.com/repos/codecov/example-xtend/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/example-xtend/events","assignees_url":"https://api.github.com/repos/codecov/example-xtend/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/example-xtend/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/example-xtend/tags","blobs_url":"https://api.github.com/repos/codecov/example-xtend/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/example-xtend/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/example-xtend/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/example-xtend/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/example-xtend/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/example-xtend/languages","stargazers_url":"https://api.github.com/repos/codecov/example-xtend/stargazers","contributors_url":"https://api.github.com/repos/codecov/example-xtend/contributors","subscribers_url":"https://api.github.com/repos/codecov/example-xtend/subscribers","subscription_url":"https://api.github.com/repos/codecov/example-xtend/subscription","commits_url":"https://api.github.com/repos/codecov/example-xtend/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/example-xtend/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/example-xtend/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/example-xtend/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/example-xtend/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/example-xtend/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/example-xtend/merges","archive_url":"https://api.github.com/repos/codecov/example-xtend/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/example-xtend/downloads","issues_url":"https://api.github.com/repos/codecov/example-xtend/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/example-xtend/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/example-xtend/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/example-xtend/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/example-xtend/labels{/name}","releases_url":"https://api.github.com/repos/codecov/example-xtend/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/example-xtend/deployments","created_at":"2014-09-28T18:54:44Z","updated_at":"2023-07-25T13:53:21Z","pushed_at":"2022-10-31T20:37:29Z","git_url":"git://github.com/codecov/example-xtend.git","ssh_url":"git@github.com:codecov/example-xtend.git","clone_url":"https://github.com/codecov/example-xtend.git","svn_url":"https://github.com/codecov/example-xtend","homepage":"https://codecov.io","size":9,"stargazers_count":3,"watchers_count":3,"language":"Xtend","has_issues":false,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":4,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":4,"open_issues":0,"watchers":3,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":377426552,"node_id":"MDEwOlJlcG9zaXRvcnkzNzc0MjY1NTI=","name":"feedback","full_name":"codecov/feedback","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/feedback","description":"A + place to discuss feedback about the pull request and web product experience.","fork":false,"url":"https://api.github.com/repos/codecov/feedback","forks_url":"https://api.github.com/repos/codecov/feedback/forks","keys_url":"https://api.github.com/repos/codecov/feedback/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/feedback/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/feedback/teams","hooks_url":"https://api.github.com/repos/codecov/feedback/hooks","issue_events_url":"https://api.github.com/repos/codecov/feedback/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/feedback/events","assignees_url":"https://api.github.com/repos/codecov/feedback/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/feedback/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/feedback/tags","blobs_url":"https://api.github.com/repos/codecov/feedback/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/feedback/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/feedback/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/feedback/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/feedback/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/feedback/languages","stargazers_url":"https://api.github.com/repos/codecov/feedback/stargazers","contributors_url":"https://api.github.com/repos/codecov/feedback/contributors","subscribers_url":"https://api.github.com/repos/codecov/feedback/subscribers","subscription_url":"https://api.github.com/repos/codecov/feedback/subscription","commits_url":"https://api.github.com/repos/codecov/feedback/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/feedback/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/feedback/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/feedback/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/feedback/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/feedback/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/feedback/merges","archive_url":"https://api.github.com/repos/codecov/feedback/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/feedback/downloads","issues_url":"https://api.github.com/repos/codecov/feedback/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/feedback/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/feedback/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/feedback/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/feedback/labels{/name}","releases_url":"https://api.github.com/repos/codecov/feedback/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/feedback/deployments","created_at":"2021-06-16T08:34:14Z","updated_at":"2023-08-18T17:02:02Z","pushed_at":"2023-08-04T14:41:26Z","git_url":"git://github.com/codecov/feedback.git","ssh_url":"git@github.com:codecov/feedback.git","clone_url":"https://github.com/codecov/feedback.git","svn_url":"https://github.com/codecov/feedback","homepage":null,"size":4,"stargazers_count":27,"watchers_count":27,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":true,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":19,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":1,"open_issues":19,"watchers":27,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":163929956,"node_id":"MDEwOlJlcG9zaXRvcnkxNjM5Mjk5NTY=","name":"freshdesk-app","full_name":"codecov/freshdesk-app","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/freshdesk-app","description":"Freshdesk + App for Codecov","fork":false,"url":"https://api.github.com/repos/codecov/freshdesk-app","forks_url":"https://api.github.com/repos/codecov/freshdesk-app/forks","keys_url":"https://api.github.com/repos/codecov/freshdesk-app/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/freshdesk-app/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/freshdesk-app/teams","hooks_url":"https://api.github.com/repos/codecov/freshdesk-app/hooks","issue_events_url":"https://api.github.com/repos/codecov/freshdesk-app/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/freshdesk-app/events","assignees_url":"https://api.github.com/repos/codecov/freshdesk-app/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/freshdesk-app/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/freshdesk-app/tags","blobs_url":"https://api.github.com/repos/codecov/freshdesk-app/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/freshdesk-app/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/freshdesk-app/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/freshdesk-app/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/freshdesk-app/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/freshdesk-app/languages","stargazers_url":"https://api.github.com/repos/codecov/freshdesk-app/stargazers","contributors_url":"https://api.github.com/repos/codecov/freshdesk-app/contributors","subscribers_url":"https://api.github.com/repos/codecov/freshdesk-app/subscribers","subscription_url":"https://api.github.com/repos/codecov/freshdesk-app/subscription","commits_url":"https://api.github.com/repos/codecov/freshdesk-app/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/freshdesk-app/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/freshdesk-app/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/freshdesk-app/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/freshdesk-app/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/freshdesk-app/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/freshdesk-app/merges","archive_url":"https://api.github.com/repos/codecov/freshdesk-app/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/freshdesk-app/downloads","issues_url":"https://api.github.com/repos/codecov/freshdesk-app/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/freshdesk-app/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/freshdesk-app/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/freshdesk-app/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/freshdesk-app/labels{/name}","releases_url":"https://api.github.com/repos/codecov/freshdesk-app/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/freshdesk-app/deployments","created_at":"2019-01-03T06:03:21Z","updated_at":"2019-01-30T17:20:19Z","pushed_at":"2019-01-30T17:20:17Z","git_url":"git://github.com/codecov/freshdesk-app.git","ssh_url":"git@github.com:codecov/freshdesk-app.git","clone_url":"https://github.com/codecov/freshdesk-app.git","svn_url":"https://github.com/codecov/freshdesk-app","homepage":null,"size":63,"stargazers_count":0,"watchers_count":0,"language":"HTML","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":310336565,"node_id":"MDEwOlJlcG9zaXRvcnkzMTAzMzY1NjU=","name":"gazebo","full_name":"codecov/gazebo","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/gazebo","description":"React + SPA","fork":false,"url":"https://api.github.com/repos/codecov/gazebo","forks_url":"https://api.github.com/repos/codecov/gazebo/forks","keys_url":"https://api.github.com/repos/codecov/gazebo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/gazebo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/gazebo/teams","hooks_url":"https://api.github.com/repos/codecov/gazebo/hooks","issue_events_url":"https://api.github.com/repos/codecov/gazebo/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/gazebo/events","assignees_url":"https://api.github.com/repos/codecov/gazebo/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/gazebo/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/gazebo/tags","blobs_url":"https://api.github.com/repos/codecov/gazebo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/gazebo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/gazebo/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/gazebo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/gazebo/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/gazebo/languages","stargazers_url":"https://api.github.com/repos/codecov/gazebo/stargazers","contributors_url":"https://api.github.com/repos/codecov/gazebo/contributors","subscribers_url":"https://api.github.com/repos/codecov/gazebo/subscribers","subscription_url":"https://api.github.com/repos/codecov/gazebo/subscription","commits_url":"https://api.github.com/repos/codecov/gazebo/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/gazebo/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/gazebo/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/gazebo/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/gazebo/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/gazebo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/gazebo/merges","archive_url":"https://api.github.com/repos/codecov/gazebo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/gazebo/downloads","issues_url":"https://api.github.com/repos/codecov/gazebo/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/gazebo/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/gazebo/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/gazebo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/gazebo/labels{/name}","releases_url":"https://api.github.com/repos/codecov/gazebo/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/gazebo/deployments","created_at":"2020-11-05T15:12:50Z","updated_at":"2023-09-12T23:32:43Z","pushed_at":"2023-09-22T18:15:51Z","git_url":"git://github.com/codecov/gazebo.git","ssh_url":"git@github.com:codecov/gazebo.git","clone_url":"https://github.com/codecov/gazebo.git","svn_url":"https://github.com/codecov/gazebo","homepage":null,"size":50155,"stargazers_count":39,"watchers_count":39,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":4,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":13,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":4,"open_issues":13,"watchers":39,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":204369089,"node_id":"MDEwOlJlcG9zaXRvcnkyMDQzNjkwODk=","name":"go-standard","full_name":"codecov/go-standard","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/go-standard","description":"Codecov + coverage standard for go","fork":false,"url":"https://api.github.com/repos/codecov/go-standard","forks_url":"https://api.github.com/repos/codecov/go-standard/forks","keys_url":"https://api.github.com/repos/codecov/go-standard/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/go-standard/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/go-standard/teams","hooks_url":"https://api.github.com/repos/codecov/go-standard/hooks","issue_events_url":"https://api.github.com/repos/codecov/go-standard/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/go-standard/events","assignees_url":"https://api.github.com/repos/codecov/go-standard/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/go-standard/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/go-standard/tags","blobs_url":"https://api.github.com/repos/codecov/go-standard/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/go-standard/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/go-standard/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/go-standard/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/go-standard/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/go-standard/languages","stargazers_url":"https://api.github.com/repos/codecov/go-standard/stargazers","contributors_url":"https://api.github.com/repos/codecov/go-standard/contributors","subscribers_url":"https://api.github.com/repos/codecov/go-standard/subscribers","subscription_url":"https://api.github.com/repos/codecov/go-standard/subscription","commits_url":"https://api.github.com/repos/codecov/go-standard/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/go-standard/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/go-standard/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/go-standard/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/go-standard/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/go-standard/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/go-standard/merges","archive_url":"https://api.github.com/repos/codecov/go-standard/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/go-standard/downloads","issues_url":"https://api.github.com/repos/codecov/go-standard/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/go-standard/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/go-standard/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/go-standard/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/go-standard/labels{/name}","releases_url":"https://api.github.com/repos/codecov/go-standard/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/go-standard/deployments","created_at":"2019-08-26T00:59:07Z","updated_at":"2023-09-18T18:08:08Z","pushed_at":"2023-09-16T05:07:31Z","git_url":"git://github.com/codecov/go-standard.git","ssh_url":"git@github.com:codecov/go-standard.git","clone_url":"https://github.com/codecov/go-standard.git","svn_url":"https://github.com/codecov/go-standard","homepage":null,"size":336,"stargazers_count":12,"watchers_count":12,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":19,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":19,"open_issues":0,"watchers":12,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":307667917,"node_id":"MDEwOlJlcG9zaXRvcnkzMDc2Njc5MTc=","name":"greenfield","full_name":"codecov/greenfield","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/greenfield","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/greenfield","forks_url":"https://api.github.com/repos/codecov/greenfield/forks","keys_url":"https://api.github.com/repos/codecov/greenfield/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/greenfield/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/greenfield/teams","hooks_url":"https://api.github.com/repos/codecov/greenfield/hooks","issue_events_url":"https://api.github.com/repos/codecov/greenfield/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/greenfield/events","assignees_url":"https://api.github.com/repos/codecov/greenfield/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/greenfield/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/greenfield/tags","blobs_url":"https://api.github.com/repos/codecov/greenfield/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/greenfield/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/greenfield/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/greenfield/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/greenfield/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/greenfield/languages","stargazers_url":"https://api.github.com/repos/codecov/greenfield/stargazers","contributors_url":"https://api.github.com/repos/codecov/greenfield/contributors","subscribers_url":"https://api.github.com/repos/codecov/greenfield/subscribers","subscription_url":"https://api.github.com/repos/codecov/greenfield/subscription","commits_url":"https://api.github.com/repos/codecov/greenfield/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/greenfield/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/greenfield/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/greenfield/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/greenfield/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/greenfield/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/greenfield/merges","archive_url":"https://api.github.com/repos/codecov/greenfield/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/greenfield/downloads","issues_url":"https://api.github.com/repos/codecov/greenfield/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/greenfield/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/greenfield/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/greenfield/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/greenfield/labels{/name}","releases_url":"https://api.github.com/repos/codecov/greenfield/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/greenfield/deployments","created_at":"2020-10-27T10:47:25Z","updated_at":"2023-01-28T10:57:48Z","pushed_at":"2020-11-04T14:43:27Z","git_url":"git://github.com/codecov/greenfield.git","ssh_url":"git@github.com:codecov/greenfield.git","clone_url":"https://github.com/codecov/greenfield.git","svn_url":"https://github.com/codecov/greenfield","homepage":null,"size":918,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":true,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":474124611,"node_id":"R_kgDOHEKRQw","name":"helm-charts","full_name":"codecov/helm-charts","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/helm-charts","description":"Public + Helm Charts","fork":false,"url":"https://api.github.com/repos/codecov/helm-charts","forks_url":"https://api.github.com/repos/codecov/helm-charts/forks","keys_url":"https://api.github.com/repos/codecov/helm-charts/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/helm-charts/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/helm-charts/teams","hooks_url":"https://api.github.com/repos/codecov/helm-charts/hooks","issue_events_url":"https://api.github.com/repos/codecov/helm-charts/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/helm-charts/events","assignees_url":"https://api.github.com/repos/codecov/helm-charts/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/helm-charts/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/helm-charts/tags","blobs_url":"https://api.github.com/repos/codecov/helm-charts/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/helm-charts/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/helm-charts/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/helm-charts/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/helm-charts/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/helm-charts/languages","stargazers_url":"https://api.github.com/repos/codecov/helm-charts/stargazers","contributors_url":"https://api.github.com/repos/codecov/helm-charts/contributors","subscribers_url":"https://api.github.com/repos/codecov/helm-charts/subscribers","subscription_url":"https://api.github.com/repos/codecov/helm-charts/subscription","commits_url":"https://api.github.com/repos/codecov/helm-charts/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/helm-charts/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/helm-charts/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/helm-charts/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/helm-charts/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/helm-charts/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/helm-charts/merges","archive_url":"https://api.github.com/repos/codecov/helm-charts/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/helm-charts/downloads","issues_url":"https://api.github.com/repos/codecov/helm-charts/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/helm-charts/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/helm-charts/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/helm-charts/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/helm-charts/labels{/name}","releases_url":"https://api.github.com/repos/codecov/helm-charts/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/helm-charts/deployments","created_at":"2022-03-25T18:33:17Z","updated_at":"2023-07-26T19:36:27Z","pushed_at":"2023-02-16T20:37:47Z","git_url":"git://github.com/codecov/helm-charts.git","ssh_url":"git@github.com:codecov/helm-charts.git","clone_url":"https://github.com/codecov/helm-charts.git","svn_url":"https://github.com/codecov/helm-charts","homepage":null,"size":68,"stargazers_count":1,"watchers_count":1,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"apache-2.0","name":"Apache + License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":1,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":678640381,"node_id":"R_kgDOKHM6_Q","name":"hooky","full_name":"codecov/hooky","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/hooky","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/hooky","forks_url":"https://api.github.com/repos/codecov/hooky/forks","keys_url":"https://api.github.com/repos/codecov/hooky/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/hooky/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/hooky/teams","hooks_url":"https://api.github.com/repos/codecov/hooky/hooks","issue_events_url":"https://api.github.com/repos/codecov/hooky/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/hooky/events","assignees_url":"https://api.github.com/repos/codecov/hooky/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/hooky/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/hooky/tags","blobs_url":"https://api.github.com/repos/codecov/hooky/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/hooky/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/hooky/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/hooky/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/hooky/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/hooky/languages","stargazers_url":"https://api.github.com/repos/codecov/hooky/stargazers","contributors_url":"https://api.github.com/repos/codecov/hooky/contributors","subscribers_url":"https://api.github.com/repos/codecov/hooky/subscribers","subscription_url":"https://api.github.com/repos/codecov/hooky/subscription","commits_url":"https://api.github.com/repos/codecov/hooky/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/hooky/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/hooky/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/hooky/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/hooky/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/hooky/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/hooky/merges","archive_url":"https://api.github.com/repos/codecov/hooky/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/hooky/downloads","issues_url":"https://api.github.com/repos/codecov/hooky/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/hooky/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/hooky/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/hooky/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/hooky/labels{/name}","releases_url":"https://api.github.com/repos/codecov/hooky/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/hooky/deployments","created_at":"2023-08-15T02:40:42Z","updated_at":"2023-09-20T19:43:26Z","pushed_at":"2023-09-14T06:14:15Z","git_url":"git://github.com/codecov/hooky.git","ssh_url":"git@github.com:codecov/hooky.git","clone_url":"https://github.com/codecov/hooky.git","svn_url":"https://github.com/codecov/hooky","homepage":null,"size":832,"stargazers_count":0,"watchers_count":0,"language":"PHP","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":7,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":7,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":483728443,"node_id":"R_kgDOHNUcOw","name":"impact-analysis","full_name":"codecov/impact-analysis","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/impact-analysis","description":"A + working POC that runs only impacted tests","fork":false,"url":"https://api.github.com/repos/codecov/impact-analysis","forks_url":"https://api.github.com/repos/codecov/impact-analysis/forks","keys_url":"https://api.github.com/repos/codecov/impact-analysis/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/impact-analysis/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/impact-analysis/teams","hooks_url":"https://api.github.com/repos/codecov/impact-analysis/hooks","issue_events_url":"https://api.github.com/repos/codecov/impact-analysis/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/impact-analysis/events","assignees_url":"https://api.github.com/repos/codecov/impact-analysis/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/impact-analysis/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/impact-analysis/tags","blobs_url":"https://api.github.com/repos/codecov/impact-analysis/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/impact-analysis/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/impact-analysis/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/impact-analysis/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/impact-analysis/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/impact-analysis/languages","stargazers_url":"https://api.github.com/repos/codecov/impact-analysis/stargazers","contributors_url":"https://api.github.com/repos/codecov/impact-analysis/contributors","subscribers_url":"https://api.github.com/repos/codecov/impact-analysis/subscribers","subscription_url":"https://api.github.com/repos/codecov/impact-analysis/subscription","commits_url":"https://api.github.com/repos/codecov/impact-analysis/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/impact-analysis/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/impact-analysis/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/impact-analysis/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/impact-analysis/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/impact-analysis/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/impact-analysis/merges","archive_url":"https://api.github.com/repos/codecov/impact-analysis/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/impact-analysis/downloads","issues_url":"https://api.github.com/repos/codecov/impact-analysis/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/impact-analysis/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/impact-analysis/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/impact-analysis/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/impact-analysis/labels{/name}","releases_url":"https://api.github.com/repos/codecov/impact-analysis/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/impact-analysis/deployments","created_at":"2022-04-20T16:21:00Z","updated_at":"2022-04-20T16:23:02Z","pushed_at":"2022-05-18T14:39:29Z","git_url":"git://github.com/codecov/impact-analysis.git","ssh_url":"git@github.com:codecov/impact-analysis.git","clone_url":"https://github.com/codecov/impact-analysis.git","svn_url":"https://github.com/codecov/impact-analysis","homepage":null,"size":4635,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":10,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":10,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":506038153,"node_id":"R_kgDOHimHiQ","name":"impact-analysis-example-node","full_name":"codecov/impact-analysis-example-node","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/impact-analysis-example-node","description":"An + Example Repository for Impact Analysis Using NodeJS","fork":false,"url":"https://api.github.com/repos/codecov/impact-analysis-example-node","forks_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/forks","keys_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/teams","hooks_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/hooks","issue_events_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/events","assignees_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/tags","blobs_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/languages","stargazers_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/stargazers","contributors_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/contributors","subscribers_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/subscribers","subscription_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/subscription","commits_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/merges","archive_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/downloads","issues_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/labels{/name}","releases_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/impact-analysis-example-node/deployments","created_at":"2022-06-21T23:50:06Z","updated_at":"2022-12-05T19:03:30Z","pushed_at":"2023-02-13T20:11:33Z","git_url":"git://github.com/codecov/impact-analysis-example-node.git","ssh_url":"git@github.com:codecov/impact-analysis-example-node.git","clone_url":"https://github.com/codecov/impact-analysis-example-node.git","svn_url":"https://github.com/codecov/impact-analysis-example-node","homepage":null,"size":10,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":638534899,"node_id":"R_kgDOJg9E8w","name":"infrastructure-team","full_name":"codecov/infrastructure-team","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/infrastructure-team","description":"Placeholder + for GH issues from GH projects","fork":false,"url":"https://api.github.com/repos/codecov/infrastructure-team","forks_url":"https://api.github.com/repos/codecov/infrastructure-team/forks","keys_url":"https://api.github.com/repos/codecov/infrastructure-team/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/infrastructure-team/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/infrastructure-team/teams","hooks_url":"https://api.github.com/repos/codecov/infrastructure-team/hooks","issue_events_url":"https://api.github.com/repos/codecov/infrastructure-team/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/infrastructure-team/events","assignees_url":"https://api.github.com/repos/codecov/infrastructure-team/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/infrastructure-team/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/infrastructure-team/tags","blobs_url":"https://api.github.com/repos/codecov/infrastructure-team/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/infrastructure-team/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/infrastructure-team/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/infrastructure-team/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/infrastructure-team/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/infrastructure-team/languages","stargazers_url":"https://api.github.com/repos/codecov/infrastructure-team/stargazers","contributors_url":"https://api.github.com/repos/codecov/infrastructure-team/contributors","subscribers_url":"https://api.github.com/repos/codecov/infrastructure-team/subscribers","subscription_url":"https://api.github.com/repos/codecov/infrastructure-team/subscription","commits_url":"https://api.github.com/repos/codecov/infrastructure-team/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/infrastructure-team/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/infrastructure-team/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/infrastructure-team/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/infrastructure-team/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/infrastructure-team/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/infrastructure-team/merges","archive_url":"https://api.github.com/repos/codecov/infrastructure-team/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/infrastructure-team/downloads","issues_url":"https://api.github.com/repos/codecov/infrastructure-team/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/infrastructure-team/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/infrastructure-team/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/infrastructure-team/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/infrastructure-team/labels{/name}","releases_url":"https://api.github.com/repos/codecov/infrastructure-team/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/infrastructure-team/deployments","created_at":"2023-05-09T14:58:38Z","updated_at":"2023-07-06T16:07:34Z","pushed_at":"2023-08-30T14:37:02Z","git_url":"git://github.com/codecov/infrastructure-team.git","ssh_url":"git@github.com:codecov/infrastructure-team.git","clone_url":"https://github.com/codecov/infrastructure-team.git","svn_url":"https://github.com/codecov/infrastructure-team","homepage":null,"size":3,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":67,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":67,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":false,"triage":true,"pull":true}},{"id":688560984,"node_id":"R_kgDOKQqbWA","name":"internal-issues","full_name":"codecov/internal-issues","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/internal-issues","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/internal-issues","forks_url":"https://api.github.com/repos/codecov/internal-issues/forks","keys_url":"https://api.github.com/repos/codecov/internal-issues/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/internal-issues/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/internal-issues/teams","hooks_url":"https://api.github.com/repos/codecov/internal-issues/hooks","issue_events_url":"https://api.github.com/repos/codecov/internal-issues/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/internal-issues/events","assignees_url":"https://api.github.com/repos/codecov/internal-issues/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/internal-issues/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/internal-issues/tags","blobs_url":"https://api.github.com/repos/codecov/internal-issues/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/internal-issues/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/internal-issues/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/internal-issues/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/internal-issues/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/internal-issues/languages","stargazers_url":"https://api.github.com/repos/codecov/internal-issues/stargazers","contributors_url":"https://api.github.com/repos/codecov/internal-issues/contributors","subscribers_url":"https://api.github.com/repos/codecov/internal-issues/subscribers","subscription_url":"https://api.github.com/repos/codecov/internal-issues/subscription","commits_url":"https://api.github.com/repos/codecov/internal-issues/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/internal-issues/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/internal-issues/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/internal-issues/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/internal-issues/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/internal-issues/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/internal-issues/merges","archive_url":"https://api.github.com/repos/codecov/internal-issues/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/internal-issues/downloads","issues_url":"https://api.github.com/repos/codecov/internal-issues/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/internal-issues/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/internal-issues/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/internal-issues/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/internal-issues/labels{/name}","releases_url":"https://api.github.com/repos/codecov/internal-issues/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/internal-issues/deployments","created_at":"2023-09-07T15:48:53Z","updated_at":"2023-09-20T13:50:01Z","pushed_at":"2023-09-07T15:48:54Z","git_url":"git://github.com/codecov/internal-issues.git","ssh_url":"git@github.com:codecov/internal-issues.git","clone_url":"https://github.com/codecov/internal-issues.git","svn_url":"https://github.com/codecov/internal-issues","homepage":null,"size":0,"stargazers_count":1,"watchers_count":1,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":31,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":31,"watchers":1,"default_branch":"main","permissions":{"admin":false,"maintain":true,"push":true,"triage":true,"pull":true}},{"id":206460969,"node_id":"MDEwOlJlcG9zaXRvcnkyMDY0NjA5Njk=","name":"java-standard","full_name":"codecov/java-standard","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/java-standard","description":"Codecov + coverage standard for Java","fork":false,"url":"https://api.github.com/repos/codecov/java-standard","forks_url":"https://api.github.com/repos/codecov/java-standard/forks","keys_url":"https://api.github.com/repos/codecov/java-standard/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/java-standard/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/java-standard/teams","hooks_url":"https://api.github.com/repos/codecov/java-standard/hooks","issue_events_url":"https://api.github.com/repos/codecov/java-standard/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/java-standard/events","assignees_url":"https://api.github.com/repos/codecov/java-standard/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/java-standard/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/java-standard/tags","blobs_url":"https://api.github.com/repos/codecov/java-standard/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/java-standard/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/java-standard/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/java-standard/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/java-standard/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/java-standard/languages","stargazers_url":"https://api.github.com/repos/codecov/java-standard/stargazers","contributors_url":"https://api.github.com/repos/codecov/java-standard/contributors","subscribers_url":"https://api.github.com/repos/codecov/java-standard/subscribers","subscription_url":"https://api.github.com/repos/codecov/java-standard/subscription","commits_url":"https://api.github.com/repos/codecov/java-standard/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/java-standard/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/java-standard/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/java-standard/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/java-standard/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/java-standard/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/java-standard/merges","archive_url":"https://api.github.com/repos/codecov/java-standard/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/java-standard/downloads","issues_url":"https://api.github.com/repos/codecov/java-standard/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/java-standard/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/java-standard/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/java-standard/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/java-standard/labels{/name}","releases_url":"https://api.github.com/repos/codecov/java-standard/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/java-standard/deployments","created_at":"2019-09-05T02:49:55Z","updated_at":"2023-08-06T06:49:06Z","pushed_at":"2023-09-16T05:05:54Z","git_url":"git://github.com/codecov/java-standard.git","ssh_url":"git@github.com:codecov/java-standard.git","clone_url":"https://github.com/codecov/java-standard.git","svn_url":"https://github.com/codecov/java-standard","homepage":null,"size":291,"stargazers_count":9,"watchers_count":9,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":15,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":15,"open_issues":0,"watchers":9,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":144112628,"node_id":"MDEwOlJlcG9zaXRvcnkxNDQxMTI2Mjg=","name":"k8s","full_name":"codecov/k8s","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/k8s","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/k8s","forks_url":"https://api.github.com/repos/codecov/k8s/forks","keys_url":"https://api.github.com/repos/codecov/k8s/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/k8s/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/k8s/teams","hooks_url":"https://api.github.com/repos/codecov/k8s/hooks","issue_events_url":"https://api.github.com/repos/codecov/k8s/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/k8s/events","assignees_url":"https://api.github.com/repos/codecov/k8s/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/k8s/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/k8s/tags","blobs_url":"https://api.github.com/repos/codecov/k8s/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/k8s/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/k8s/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/k8s/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/k8s/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/k8s/languages","stargazers_url":"https://api.github.com/repos/codecov/k8s/stargazers","contributors_url":"https://api.github.com/repos/codecov/k8s/contributors","subscribers_url":"https://api.github.com/repos/codecov/k8s/subscribers","subscription_url":"https://api.github.com/repos/codecov/k8s/subscription","commits_url":"https://api.github.com/repos/codecov/k8s/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/k8s/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/k8s/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/k8s/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/k8s/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/k8s/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/k8s/merges","archive_url":"https://api.github.com/repos/codecov/k8s/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/k8s/downloads","issues_url":"https://api.github.com/repos/codecov/k8s/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/k8s/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/k8s/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/k8s/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/k8s/labels{/name}","releases_url":"https://api.github.com/repos/codecov/k8s/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/k8s/deployments","created_at":"2018-08-09T06:53:39Z","updated_at":"2020-09-18T19:26:44Z","pushed_at":"2020-09-18T19:26:41Z","git_url":"git://github.com/codecov/k8s.git","ssh_url":"git@github.com:codecov/k8s.git","clone_url":"https://github.com/codecov/k8s.git","svn_url":"https://github.com/codecov/k8s","homepage":"","size":148,"stargazers_count":0,"watchers_count":0,"language":"Shell","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":15,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":15,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":225711958,"node_id":"MDEwOlJlcG9zaXRvcnkyMjU3MTE5NTg=","name":"k8s-v2","full_name":"codecov/k8s-v2","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/k8s-v2","description":null,"fork":false,"url":"https://api.github.com/repos/codecov/k8s-v2","forks_url":"https://api.github.com/repos/codecov/k8s-v2/forks","keys_url":"https://api.github.com/repos/codecov/k8s-v2/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/k8s-v2/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/k8s-v2/teams","hooks_url":"https://api.github.com/repos/codecov/k8s-v2/hooks","issue_events_url":"https://api.github.com/repos/codecov/k8s-v2/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/k8s-v2/events","assignees_url":"https://api.github.com/repos/codecov/k8s-v2/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/k8s-v2/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/k8s-v2/tags","blobs_url":"https://api.github.com/repos/codecov/k8s-v2/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/k8s-v2/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/k8s-v2/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/k8s-v2/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/k8s-v2/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/k8s-v2/languages","stargazers_url":"https://api.github.com/repos/codecov/k8s-v2/stargazers","contributors_url":"https://api.github.com/repos/codecov/k8s-v2/contributors","subscribers_url":"https://api.github.com/repos/codecov/k8s-v2/subscribers","subscription_url":"https://api.github.com/repos/codecov/k8s-v2/subscription","commits_url":"https://api.github.com/repos/codecov/k8s-v2/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/k8s-v2/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/k8s-v2/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/k8s-v2/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/k8s-v2/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/k8s-v2/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/k8s-v2/merges","archive_url":"https://api.github.com/repos/codecov/k8s-v2/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/k8s-v2/downloads","issues_url":"https://api.github.com/repos/codecov/k8s-v2/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/k8s-v2/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/k8s-v2/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/k8s-v2/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/k8s-v2/labels{/name}","releases_url":"https://api.github.com/repos/codecov/k8s-v2/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/k8s-v2/deployments","created_at":"2019-12-03T20:43:31Z","updated_at":"2022-07-26T22:18:56Z","pushed_at":"2023-09-22T18:13:28Z","git_url":"git://github.com/codecov/k8s-v2.git","ssh_url":"git@github.com:codecov/k8s-v2.git","clone_url":"https://github.com/codecov/k8s-v2.git","svn_url":"https://github.com/codecov/k8s-v2","homepage":null,"size":2171,"stargazers_count":1,"watchers_count":1,"language":"Smarty","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":8,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":8,"watchers":1,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":361234281,"node_id":"MDEwOlJlcG9zaXRvcnkzNjEyMzQyODE=","name":"keylogapp","full_name":"codecov/keylogapp","private":true,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/keylogapp","description":"A + web application meant to provide paper trail auditing for secrets and other + credentials","fork":false,"url":"https://api.github.com/repos/codecov/keylogapp","forks_url":"https://api.github.com/repos/codecov/keylogapp/forks","keys_url":"https://api.github.com/repos/codecov/keylogapp/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/keylogapp/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/keylogapp/teams","hooks_url":"https://api.github.com/repos/codecov/keylogapp/hooks","issue_events_url":"https://api.github.com/repos/codecov/keylogapp/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/keylogapp/events","assignees_url":"https://api.github.com/repos/codecov/keylogapp/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/keylogapp/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/keylogapp/tags","blobs_url":"https://api.github.com/repos/codecov/keylogapp/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/keylogapp/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/keylogapp/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/keylogapp/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/keylogapp/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/keylogapp/languages","stargazers_url":"https://api.github.com/repos/codecov/keylogapp/stargazers","contributors_url":"https://api.github.com/repos/codecov/keylogapp/contributors","subscribers_url":"https://api.github.com/repos/codecov/keylogapp/subscribers","subscription_url":"https://api.github.com/repos/codecov/keylogapp/subscription","commits_url":"https://api.github.com/repos/codecov/keylogapp/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/keylogapp/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/keylogapp/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/keylogapp/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/keylogapp/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/keylogapp/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/keylogapp/merges","archive_url":"https://api.github.com/repos/codecov/keylogapp/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/keylogapp/downloads","issues_url":"https://api.github.com/repos/codecov/keylogapp/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/keylogapp/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/keylogapp/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/keylogapp/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/keylogapp/labels{/name}","releases_url":"https://api.github.com/repos/codecov/keylogapp/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/keylogapp/deployments","created_at":"2021-04-24T18:14:29Z","updated_at":"2022-01-10T17:22:14Z","pushed_at":"2023-09-19T19:06:56Z","git_url":"git://github.com/codecov/keylogapp.git","ssh_url":"git@github.com:codecov/keylogapp.git","clone_url":"https://github.com/codecov/keylogapp.git","svn_url":"https://github.com/codecov/keylogapp","homepage":null,"size":1943,"stargazers_count":0,"watchers_count":0,"language":"PHP","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":21,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":21,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}},{"id":204188879,"node_id":"MDEwOlJlcG9zaXRvcnkyMDQxODg4Nzk=","name":"kotlin-standard","full_name":"codecov/kotlin-standard","private":false,"owner":{"login":"codecov","id":8226205,"node_id":"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=","avatar_url":"https://avatars.githubusercontent.com/u/8226205?v=4","gravatar_id":"","url":"https://api.github.com/users/codecov","html_url":"https://github.com/codecov","followers_url":"https://api.github.com/users/codecov/followers","following_url":"https://api.github.com/users/codecov/following{/other_user}","gists_url":"https://api.github.com/users/codecov/gists{/gist_id}","starred_url":"https://api.github.com/users/codecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codecov/subscriptions","organizations_url":"https://api.github.com/users/codecov/orgs","repos_url":"https://api.github.com/users/codecov/repos","events_url":"https://api.github.com/users/codecov/events{/privacy}","received_events_url":"https://api.github.com/users/codecov/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/codecov/kotlin-standard","description":"Codecov + coverage standard for Kotlin","fork":false,"url":"https://api.github.com/repos/codecov/kotlin-standard","forks_url":"https://api.github.com/repos/codecov/kotlin-standard/forks","keys_url":"https://api.github.com/repos/codecov/kotlin-standard/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codecov/kotlin-standard/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codecov/kotlin-standard/teams","hooks_url":"https://api.github.com/repos/codecov/kotlin-standard/hooks","issue_events_url":"https://api.github.com/repos/codecov/kotlin-standard/issues/events{/number}","events_url":"https://api.github.com/repos/codecov/kotlin-standard/events","assignees_url":"https://api.github.com/repos/codecov/kotlin-standard/assignees{/user}","branches_url":"https://api.github.com/repos/codecov/kotlin-standard/branches{/branch}","tags_url":"https://api.github.com/repos/codecov/kotlin-standard/tags","blobs_url":"https://api.github.com/repos/codecov/kotlin-standard/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codecov/kotlin-standard/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codecov/kotlin-standard/git/refs{/sha}","trees_url":"https://api.github.com/repos/codecov/kotlin-standard/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codecov/kotlin-standard/statuses/{sha}","languages_url":"https://api.github.com/repos/codecov/kotlin-standard/languages","stargazers_url":"https://api.github.com/repos/codecov/kotlin-standard/stargazers","contributors_url":"https://api.github.com/repos/codecov/kotlin-standard/contributors","subscribers_url":"https://api.github.com/repos/codecov/kotlin-standard/subscribers","subscription_url":"https://api.github.com/repos/codecov/kotlin-standard/subscription","commits_url":"https://api.github.com/repos/codecov/kotlin-standard/commits{/sha}","git_commits_url":"https://api.github.com/repos/codecov/kotlin-standard/git/commits{/sha}","comments_url":"https://api.github.com/repos/codecov/kotlin-standard/comments{/number}","issue_comment_url":"https://api.github.com/repos/codecov/kotlin-standard/issues/comments{/number}","contents_url":"https://api.github.com/repos/codecov/kotlin-standard/contents/{+path}","compare_url":"https://api.github.com/repos/codecov/kotlin-standard/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codecov/kotlin-standard/merges","archive_url":"https://api.github.com/repos/codecov/kotlin-standard/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codecov/kotlin-standard/downloads","issues_url":"https://api.github.com/repos/codecov/kotlin-standard/issues{/number}","pulls_url":"https://api.github.com/repos/codecov/kotlin-standard/pulls{/number}","milestones_url":"https://api.github.com/repos/codecov/kotlin-standard/milestones{/number}","notifications_url":"https://api.github.com/repos/codecov/kotlin-standard/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codecov/kotlin-standard/labels{/name}","releases_url":"https://api.github.com/repos/codecov/kotlin-standard/releases{/id}","deployments_url":"https://api.github.com/repos/codecov/kotlin-standard/deployments","created_at":"2019-08-24T17:17:09Z","updated_at":"2023-09-10T14:30:27Z","pushed_at":"2023-09-16T05:05:16Z","git_url":"git://github.com/codecov/kotlin-standard.git","ssh_url":"git@github.com:codecov/kotlin-standard.git","clone_url":"https://github.com/codecov/kotlin-standard.git","svn_url":"https://github.com/codecov/kotlin-standard","homepage":null,"size":399,"stargazers_count":6,"watchers_count":6,"language":"Kotlin","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":4,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT + License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":4,"open_issues":1,"watchers":6,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true}}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 22 Sep 2023 18:36:11 GMT + ETag: + - W/"18fdc5d25dd02e8634cce00f6ae9689a33efc0b2f7541f00c7d41e080660d591" + Link: + - ; rel="next", ; + rel="last" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - F04E:4CD7:66D8BC:D0CCC1:650DDE9A + X-OAuth-Scopes: + - admin:org, repo, user + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4998' + X-RateLimit-Reset: + - '1695411371' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '2' + X-XSS-Protection: + - '0' + github-authentication-token-expiration: + - 2023-09-23 22:46:17 UTC + x-github-api-version-selected: + - '2022-11-28' + x-github-sso: + - partial-results; organizations=1396951 + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_repos_using_installation.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_repos_using_installation.yaml new file mode 100644 index 0000000000..ad5c0ef7fa --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_repos_using_installation.yaml @@ -0,0 +1,74 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/vnd.github.machine-man-preview+json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/installation/repositories?page=1&per_page=50 + response: + content: '{"total_count":1,"repository_selection":"selected","repositories":[{"id":610348935,"node_id":"R_kgDOJGEvhw","name":"codecov-test","full_name":"scott-codecov-org/codecov-test","private":true,"owner":{"login":"scott-codecov-org","id":111885151,"node_id":"O_kgDOBqs7Xw","avatar_url":"https://avatars.githubusercontent.com/u/111885151?v=4","gravatar_id":"","url":"https://api.github.com/users/scott-codecov-org","html_url":"https://github.com/scott-codecov-org","followers_url":"https://api.github.com/users/scott-codecov-org/followers","following_url":"https://api.github.com/users/scott-codecov-org/following{/other_user}","gists_url":"https://api.github.com/users/scott-codecov-org/gists{/gist_id}","starred_url":"https://api.github.com/users/scott-codecov-org/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/scott-codecov-org/subscriptions","organizations_url":"https://api.github.com/users/scott-codecov-org/orgs","repos_url":"https://api.github.com/users/scott-codecov-org/repos","events_url":"https://api.github.com/users/scott-codecov-org/events{/privacy}","received_events_url":"https://api.github.com/users/scott-codecov-org/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/scott-codecov-org/codecov-test","description":null,"fork":false,"url":"https://api.github.com/repos/scott-codecov-org/codecov-test","forks_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/forks","keys_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/keys{/key_id}","collaborators_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/teams","hooks_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/hooks","issue_events_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/issues/events{/number}","events_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/events","assignees_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/assignees{/user}","branches_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/branches{/branch}","tags_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/tags","blobs_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/git/refs{/sha}","trees_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/git/trees{/sha}","statuses_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/statuses/{sha}","languages_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/languages","stargazers_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/stargazers","contributors_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/contributors","subscribers_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/subscribers","subscription_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/subscription","commits_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/commits{/sha}","git_commits_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/git/commits{/sha}","comments_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/comments{/number}","issue_comment_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/issues/comments{/number}","contents_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/contents/{+path}","compare_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/compare/{base}...{head}","merges_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/merges","archive_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/downloads","issues_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/issues{/number}","pulls_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/pulls{/number}","milestones_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/milestones{/number}","notifications_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/labels{/name}","releases_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/releases{/id}","deployments_url":"https://api.github.com/repos/scott-codecov-org/codecov-test/deployments","created_at":"2023-03-06T15:40:27Z","updated_at":"2023-03-06T15:41:05Z","pushed_at":"2023-05-15T16:35:09Z","git_url":"git://github.com/scott-codecov-org/codecov-test.git","ssh_url":"git@github.com:scott-codecov-org/codecov-test.git","clone_url":"https://github.com/scott-codecov-org/codecov-test.git","svn_url":"https://github.com/scott-codecov-org/codecov-test","homepage":null,"size":39,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":false,"triage":false,"pull":false}}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 28 Jul 2023 16:39:39 GMT + ETag: + - W/"1d303135692e53dd6692c4e4218f984034396e7b70791e9e65ace6e603473075" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=machine-man-preview; format=json + X-GitHub-Request-Id: + - E28A:2DDC:509D08:A2E450:64C3EF4B + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4998' + X-RateLimit-Reset: + - '1690565939' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '2' + X-XSS-Protection: + - '0' + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_repos_using_installation_generator.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_repos_using_installation_generator.yaml new file mode 100644 index 0000000000..a70c599322 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_repos_using_installation_generator.yaml @@ -0,0 +1,145 @@ +interactions: +- request: + body: '{"query": "\nquery {\n viewer {\n repositories(\n ownerAffiliations: + [OWNER, COLLABORATOR, ORGANIZATION_MEMBER]\n affiliations: [OWNER, + COLLABORATOR, ORGANIZATION_MEMBER]\n ) {\n totalCount\n }\n }\n}\n"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '264' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/graphql + response: + content: '{"data":{"viewer":{"repositories":{"totalCount":1}}}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 22 Sep 2023 18:36:12 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v4 + X-GitHub-Request-Id: + - F053:6826:66F4DB:D1347F:650DDE9B + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1695411372' + X-RateLimit-Resource: + - graphql + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/vnd.github.machine-man-preview+json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/installation/repositories?page=1&per_page=50 + response: + content: '{"total_count":1,"repository_selection":"selected","repositories":[{"id":665219192,"node_id":"R_kgDOJ6ZweA","name":"mike","full_name":"matt-codecov-club/mike","private":true,"owner":{"login":"matt-codecov-club","id":139263855,"node_id":"O_kgDOCEz_bw","avatar_url":"https://avatars.githubusercontent.com/u/139263855?v=4","gravatar_id":"","url":"https://api.github.com/users/matt-codecov-club","html_url":"https://github.com/matt-codecov-club","followers_url":"https://api.github.com/users/matt-codecov-club/followers","following_url":"https://api.github.com/users/matt-codecov-club/following{/other_user}","gists_url":"https://api.github.com/users/matt-codecov-club/gists{/gist_id}","starred_url":"https://api.github.com/users/matt-codecov-club/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/matt-codecov-club/subscriptions","organizations_url":"https://api.github.com/users/matt-codecov-club/orgs","repos_url":"https://api.github.com/users/matt-codecov-club/repos","events_url":"https://api.github.com/users/matt-codecov-club/events{/privacy}","received_events_url":"https://api.github.com/users/matt-codecov-club/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/matt-codecov-club/mike","description":null,"fork":false,"url":"https://api.github.com/repos/matt-codecov-club/mike","forks_url":"https://api.github.com/repos/matt-codecov-club/mike/forks","keys_url":"https://api.github.com/repos/matt-codecov-club/mike/keys{/key_id}","collaborators_url":"https://api.github.com/repos/matt-codecov-club/mike/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/matt-codecov-club/mike/teams","hooks_url":"https://api.github.com/repos/matt-codecov-club/mike/hooks","issue_events_url":"https://api.github.com/repos/matt-codecov-club/mike/issues/events{/number}","events_url":"https://api.github.com/repos/matt-codecov-club/mike/events","assignees_url":"https://api.github.com/repos/matt-codecov-club/mike/assignees{/user}","branches_url":"https://api.github.com/repos/matt-codecov-club/mike/branches{/branch}","tags_url":"https://api.github.com/repos/matt-codecov-club/mike/tags","blobs_url":"https://api.github.com/repos/matt-codecov-club/mike/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/matt-codecov-club/mike/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/matt-codecov-club/mike/git/refs{/sha}","trees_url":"https://api.github.com/repos/matt-codecov-club/mike/git/trees{/sha}","statuses_url":"https://api.github.com/repos/matt-codecov-club/mike/statuses/{sha}","languages_url":"https://api.github.com/repos/matt-codecov-club/mike/languages","stargazers_url":"https://api.github.com/repos/matt-codecov-club/mike/stargazers","contributors_url":"https://api.github.com/repos/matt-codecov-club/mike/contributors","subscribers_url":"https://api.github.com/repos/matt-codecov-club/mike/subscribers","subscription_url":"https://api.github.com/repos/matt-codecov-club/mike/subscription","commits_url":"https://api.github.com/repos/matt-codecov-club/mike/commits{/sha}","git_commits_url":"https://api.github.com/repos/matt-codecov-club/mike/git/commits{/sha}","comments_url":"https://api.github.com/repos/matt-codecov-club/mike/comments{/number}","issue_comment_url":"https://api.github.com/repos/matt-codecov-club/mike/issues/comments{/number}","contents_url":"https://api.github.com/repos/matt-codecov-club/mike/contents/{+path}","compare_url":"https://api.github.com/repos/matt-codecov-club/mike/compare/{base}...{head}","merges_url":"https://api.github.com/repos/matt-codecov-club/mike/merges","archive_url":"https://api.github.com/repos/matt-codecov-club/mike/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/matt-codecov-club/mike/downloads","issues_url":"https://api.github.com/repos/matt-codecov-club/mike/issues{/number}","pulls_url":"https://api.github.com/repos/matt-codecov-club/mike/pulls{/number}","milestones_url":"https://api.github.com/repos/matt-codecov-club/mike/milestones{/number}","notifications_url":"https://api.github.com/repos/matt-codecov-club/mike/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/matt-codecov-club/mike/labels{/name}","releases_url":"https://api.github.com/repos/matt-codecov-club/mike/releases{/id}","deployments_url":"https://api.github.com/repos/matt-codecov-club/mike/deployments","created_at":"2023-07-11T17:52:21Z","updated_at":"2023-07-11T17:52:22Z","pushed_at":"2023-07-11T17:52:21Z","git_url":"git://github.com/matt-codecov-club/mike.git","ssh_url":"git@github.com:matt-codecov-club/mike.git","clone_url":"https://github.com/matt-codecov-club/mike.git","svn_url":"https://github.com/matt-codecov-club/mike","homepage":null,"size":0,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":null,"allow_forking":false,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"private","forks":0,"open_issues":0,"watchers":0,"default_branch":"main","permissions":{"admin":false,"maintain":false,"push":false,"triage":false,"pull":false}}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 22 Sep 2023 18:36:12 GMT + ETag: + - W/"8d2dcc0cfab97e3c4a10b2eddfa1d1ebdb043c125df33ba1ba4421ebf8de78a9" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=machine-man-preview; format=json + X-GitHub-Request-Id: + - F053:6826:66F50C:D13509:650DDE9C + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4999' + X-RateLimit-Reset: + - '1695411372' + X-RateLimit-Resource: + - core + X-RateLimit-Used: + - '1' + X-XSS-Protection: + - '0' + x-github-api-version-selected: + - '2022-11-28' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_top_level_files.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_top_level_files.yaml new file mode 100644 index 0000000000..5bb8a78cf8 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_list_top_level_files.yaml @@ -0,0 +1,77 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/contents?ref=main + response: + content: '[{"name":".gitignore","path":".gitignore","sha":"e9428a03c7fc4ce77bba5997760efb3bb6ec42ee","size":1765,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.gitignore?ref=main","html_url":"https://github.com/ThiagoCodecov/example-python/blob/main/.gitignore","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e9428a03c7fc4ce77bba5997760efb3bb6ec42ee","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/main/.gitignore","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/.gitignore?ref=main","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e9428a03c7fc4ce77bba5997760efb3bb6ec42ee","html":"https://github.com/ThiagoCodecov/example-python/blob/main/.gitignore"}},{"name":"Makefile","path":"Makefile","sha":"fb9193e8ee425d900aca761839c1429a94049933","size":1308,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/Makefile?ref=main","html_url":"https://github.com/ThiagoCodecov/example-python/blob/main/Makefile","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/fb9193e8ee425d900aca761839c1429a94049933","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/main/Makefile","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/Makefile?ref=main","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/fb9193e8ee425d900aca761839c1429a94049933","html":"https://github.com/ThiagoCodecov/example-python/blob/main/Makefile"}},{"name":"README.md","path":"README.md","sha":"f2a9f3e932bb13cb506de777ef2c0940feb9ee1c","size":2710,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.md?ref=main","html_url":"https://github.com/ThiagoCodecov/example-python/blob/main/README.md","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/f2a9f3e932bb13cb506de777ef2c0940feb9ee1c","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/main/README.md","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/README.md?ref=main","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/f2a9f3e932bb13cb506de777ef2c0940feb9ee1c","html":"https://github.com/ThiagoCodecov/example-python/blob/main/README.md"}},{"name":"awesome","path":"awesome","sha":"2bbe48d1e574bd891e1a6d9f5e5bb7618ac8770f","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=main","html_url":"https://github.com/ThiagoCodecov/example-python/tree/main/awesome","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2bbe48d1e574bd891e1a6d9f5e5bb7618ac8770f","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/awesome?ref=main","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/2bbe48d1e574bd891e1a6d9f5e5bb7618ac8770f","html":"https://github.com/ThiagoCodecov/example-python/tree/main/awesome"}},{"name":"changed_production.sh","path":"changed_production.sh","sha":"622f906eac3a67c6f77f3a96149c555651ba6ac3","size":49464,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/changed_production.sh?ref=main","html_url":"https://github.com/ThiagoCodecov/example-python/blob/main/changed_production.sh","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/622f906eac3a67c6f77f3a96149c555651ba6ac3","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/main/changed_production.sh","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/changed_production.sh?ref=main","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/622f906eac3a67c6f77f3a96149c555651ba6ac3","html":"https://github.com/ThiagoCodecov/example-python/blob/main/changed_production.sh"}},{"name":"codecov.yaml","path":"codecov.yaml","sha":"5d7afc5bea96ab5a4c48869023041ce43ecfcb32","size":438,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=main","html_url":"https://github.com/ThiagoCodecov/example-python/blob/main/codecov.yaml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/5d7afc5bea96ab5a4c48869023041ce43ecfcb32","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/main/codecov.yaml","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/codecov.yaml?ref=main","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/5d7afc5bea96ab5a4c48869023041ce43ecfcb32","html":"https://github.com/ThiagoCodecov/example-python/blob/main/codecov.yaml"}},{"name":"dev.sh","path":"dev.sh","sha":"e87e3022e9306b6b38192509918377b9cf8f6310","size":49013,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/dev.sh?ref=main","html_url":"https://github.com/ThiagoCodecov/example-python/blob/main/dev.sh","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e87e3022e9306b6b38192509918377b9cf8f6310","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/main/dev.sh","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/dev.sh?ref=main","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/e87e3022e9306b6b38192509918377b9cf8f6310","html":"https://github.com/ThiagoCodecov/example-python/blob/main/dev.sh"}},{"name":"flagone.coverage.xml","path":"flagone.coverage.xml","sha":"49532268d6565b973ef58adcf63971f5a9e47898","size":3072,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/flagone.coverage.xml?ref=main","html_url":"https://github.com/ThiagoCodecov/example-python/blob/main/flagone.coverage.xml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/49532268d6565b973ef58adcf63971f5a9e47898","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/main/flagone.coverage.xml","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/flagone.coverage.xml?ref=main","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/49532268d6565b973ef58adcf63971f5a9e47898","html":"https://github.com/ThiagoCodecov/example-python/blob/main/flagone.coverage.xml"}},{"name":"flagtwo.coverage.xml","path":"flagtwo.coverage.xml","sha":"ae0db9038664db16b44e63c772de96bc0a8092d0","size":3073,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/flagtwo.coverage.xml?ref=main","html_url":"https://github.com/ThiagoCodecov/example-python/blob/main/flagtwo.coverage.xml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/ae0db9038664db16b44e63c772de96bc0a8092d0","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/main/flagtwo.coverage.xml","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/flagtwo.coverage.xml?ref=main","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/ae0db9038664db16b44e63c772de96bc0a8092d0","html":"https://github.com/ThiagoCodecov/example-python/blob/main/flagtwo.coverage.xml"}},{"name":"requirements.txt","path":"requirements.txt","sha":"ab0a8735f8dfaef9044d2092e6de622052069b1d","size":319,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/requirements.txt?ref=main","html_url":"https://github.com/ThiagoCodecov/example-python/blob/main/requirements.txt","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/ab0a8735f8dfaef9044d2092e6de622052069b1d","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/main/requirements.txt","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/requirements.txt?ref=main","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/ab0a8735f8dfaef9044d2092e6de622052069b1d","html":"https://github.com/ThiagoCodecov/example-python/blob/main/requirements.txt"}},{"name":"tests","path":"tests","sha":"568848eaea0bf53ad1637d86e979f29cd5b61765","size":0,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=main","html_url":"https://github.com/ThiagoCodecov/example-python/tree/main/tests","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/568848eaea0bf53ad1637d86e979f29cd5b61765","download_url":null,"type":"dir","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/tests?ref=main","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees/568848eaea0bf53ad1637d86e979f29cd5b61765","html":"https://github.com/ThiagoCodecov/example-python/tree/main/tests"}},{"name":"unit.coverage.xml","path":"unit.coverage.xml","sha":"bd7429c8f385fe21427a6ade21a0acedf0e0460a","size":1103,"url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/unit.coverage.xml?ref=main","html_url":"https://github.com/ThiagoCodecov/example-python/blob/main/unit.coverage.xml","git_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/bd7429c8f385fe21427a6ade21a0acedf0e0460a","download_url":"https://raw.githubusercontent.com/ThiagoCodecov/example-python/main/unit.coverage.xml","type":"file","_links":{"self":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/unit.coverage.xml?ref=main","git":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs/bd7429c8f385fe21427a6ade21a0acedf0e0460a","html":"https://github.com/ThiagoCodecov/example-python/blob/main/unit.coverage.xml"}}]' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:46:37 GMT + ETag: + - W/"cda64d688c17ae3fcf03b22ee869238002e410f0" + Last-Modified: + - Tue, 24 Mar 2020 22:01:40 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D054:6075:41910BA:6A33F26:5F87397D + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4905' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '95' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_post_webhook.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_post_webhook.yaml new file mode 100644 index 0000000000..2fe68e666c --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_post_webhook.yaml @@ -0,0 +1,80 @@ +interactions: +- request: + body: '{"name": "web", "active": true, "events": ["push", "pull_request"], "config": + {"url": "http://requestbin.net/r/1ecyaj51", "secret": "d", "content_type": "json"}}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '161' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/hooks + response: + content: '{"type":"Repository","id":255680134,"name":"web","active":true,"events":["pull_request","push"],"config":{"content_type":"json","secret":"********","url":"http://requestbin.net/r/1ecyaj51","insecure_ssl":"0"},"updated_at":"2020-10-14T17:32:29Z","created_at":"2020-10-14T17:32:29Z","url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks/255680134","test_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks/255680134/test","ping_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks/255680134/pings","last_response":{"code":null,"status":"unused","message":null}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '611' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:29 GMT + ETag: + - '"89f829395d88062b2b88761648bbe80c074e16932b157176fd23eed79d26a81c"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/hooks/255680134 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - admin:repo_hook, public_repo, repo, write:repo_hook + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFF1:356C:8D45E9:17589FF:5F87362D + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4938' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '62' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 201 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_set_commit_status.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_set_commit_status.yaml new file mode 100644 index 0000000000..c26431b9ef --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_set_commit_status.yaml @@ -0,0 +1,80 @@ +interactions: +- request: + body: '{"state": "success", "target_url": "https://localhost:50036/gitlab/codecov/ci-repo?ref=ad798926730aad14aadf72281204bdb85734fe67", + "context": "context", "description": "aaaaaaaaaa"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '180' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/a06aef4356ca35b34c5486269585288489e578db + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/a06aef4356ca35b34c5486269585288489e578db","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":11050927805,"node_id":"MDEzOlN0YXR1c0NvbnRleHQxMTA1MDkyNzgwNQ==","state":"success","description":"aaaaaaaaaa","target_url":"https://localhost:50036/gitlab/codecov/ci-repo?ref=ad798926730aad14aadf72281204bdb85734fe67","context":"context","created_at":"2020-10-14T17:32:28Z","updated_at":"2020-10-14T17:32:28Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '1479' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 17:32:28 GMT + ETag: + - '"612a8d4b488f16f982fa4581c78bf9bcf192e7f0b609cf070542e92fba277dd1"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/a06aef4356ca35b34c5486269585288489e578db + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - CFEF:6070:E8E21C:26D8D77:5F87362C + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4940' + X-RateLimit-Reset: + - '1602700322' + X-RateLimit-Used: + - '60' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 201 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_set_commit_statuses_then_get.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_set_commit_statuses_then_get.yaml new file mode 100644 index 0000000000..e280b56882 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_set_commit_statuses_then_get.yaml @@ -0,0 +1,950 @@ +interactions: +- request: + body: '{"state": "success", "target_url": "https://localhost:50036/github/codecov", + "context": "turtle", "description": "success - 0 - turtle"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '136' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":11053792448,"node_id":"MDEzOlN0YXR1c0NvbnRleHQxMTA1Mzc5MjQ0OA==","state":"success","description":"success + - 0 - turtle","target_url":"https://localhost:50036/github/codecov","context":"turtle","created_at":"2020-10-14T21:43:27Z","updated_at":"2020-10-14T21:43:27Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '1435' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 21:43:27 GMT + ETag: + - '"b3ba0402be60c3957933e0856f97ad8a4462a8cd0c60eeee42368820b2141cff"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D846:7BF0:209BD:4F8D6:5F8770FF + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4994' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '6' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 201 +- request: + body: '{"state": "pending", "target_url": "https://localhost:50036/github/codecov", + "context": "bird", "description": "pending - 1 - bird"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '132' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":11053792622,"node_id":"MDEzOlN0YXR1c0NvbnRleHQxMTA1Mzc5MjYyMg==","state":"pending","description":"pending + - 1 - bird","target_url":"https://localhost:50036/github/codecov","context":"bird","created_at":"2020-10-14T21:43:28Z","updated_at":"2020-10-14T21:43:28Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '1431' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 21:43:28 GMT + ETag: + - '"e72deef747cca424ad994992d0b7b5f6a6d38fe8cce61490ced9cfd47368a599"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D847:1A2D:2A710:6D0ED:5F877100 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4993' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '7' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 201 +- request: + body: '{"state": "failure", "target_url": "https://localhost:50036/github/codecov", + "context": "pig", "description": "failure - 2 - pig"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '130' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":11053792732,"node_id":"MDEzOlN0YXR1c0NvbnRleHQxMTA1Mzc5MjczMg==","state":"failure","description":"failure + - 2 - pig","target_url":"https://localhost:50036/github/codecov","context":"pig","created_at":"2020-10-14T21:43:29Z","updated_at":"2020-10-14T21:43:29Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '1429' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 21:43:29 GMT + ETag: + - '"4d11660fed32c1d2d7f1b8d4ddba0dc0dc24833358e7d5ed0804b7d1422e676c"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D848:3A6A:2A778:6F561:5F877100 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4992' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '8' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 201 +- request: + body: '{"state": "error", "target_url": "https://localhost:50036/github/codecov", + "context": "giant", "description": "error - 3 - giant"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '130' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":11053792860,"node_id":"MDEzOlN0YXR1c0NvbnRleHQxMTA1Mzc5Mjg2MA==","state":"error","description":"error + - 3 - giant","target_url":"https://localhost:50036/github/codecov","context":"giant","created_at":"2020-10-14T21:43:29Z","updated_at":"2020-10-14T21:43:29Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '1429' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 21:43:29 GMT + ETag: + - '"f0d5990017b6a5497bde1800cdd41bb96cf93457cc8b01bb901a42b5e16315e4"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D849:7D45:93F55:1020A1:5F877101 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4991' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '9' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 201 +- request: + body: '{"state": "pending", "target_url": "https://localhost:50036/github/codecov", + "context": "turtle", "description": "pending - 4 - turtle"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '136' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":11053792996,"node_id":"MDEzOlN0YXR1c0NvbnRleHQxMTA1Mzc5Mjk5Ng==","state":"pending","description":"pending + - 4 - turtle","target_url":"https://localhost:50036/github/codecov","context":"turtle","created_at":"2020-10-14T21:43:30Z","updated_at":"2020-10-14T21:43:30Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '1435' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 21:43:30 GMT + ETag: + - '"5f7ca9608923fbfa088c6060936ad0f8e40b6d4b037158df7189bfb1e3973fbc"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D84A:1208:1A205:483DB:5F877102 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4990' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '10' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 201 +- request: + body: '{"state": "failure", "target_url": "https://localhost:50036/github/codecov", + "context": "bird", "description": "failure - 5 - bird"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '132' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":11053793107,"node_id":"MDEzOlN0YXR1c0NvbnRleHQxMTA1Mzc5MzEwNw==","state":"failure","description":"failure + - 5 - bird","target_url":"https://localhost:50036/github/codecov","context":"bird","created_at":"2020-10-14T21:43:31Z","updated_at":"2020-10-14T21:43:31Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '1431' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 21:43:31 GMT + ETag: + - '"04dfc5423e38c1303feb2f079fcb6025d3cbcd3f60729a93dc0ec8d1b53d3598"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D84C:1FE9:9D603:10CEAD:5F877102 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4989' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '11' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 201 +- request: + body: '{"state": "error", "target_url": "https://localhost:50036/github/codecov", + "context": "pig", "description": "error - 6 - pig"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '126' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":11053793212,"node_id":"MDEzOlN0YXR1c0NvbnRleHQxMTA1Mzc5MzIxMg==","state":"error","description":"error + - 6 - pig","target_url":"https://localhost:50036/github/codecov","context":"pig","created_at":"2020-10-14T21:43:31Z","updated_at":"2020-10-14T21:43:31Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '1425' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 21:43:31 GMT + ETag: + - '"238d4fa903d15c828a3683b08adfffcbcae90c9a61ad1473c3e526baf13b5ec1"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D84D:687F:2D20C:6D959:5F877103 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4988' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '12' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 201 +- request: + body: '{"state": "success", "target_url": "https://localhost:50036/github/codecov", + "context": "giant", "description": "success - 7 - giant"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '134' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":11053793333,"node_id":"MDEzOlN0YXR1c0NvbnRleHQxMTA1Mzc5MzMzMw==","state":"success","description":"success + - 7 - giant","target_url":"https://localhost:50036/github/codecov","context":"giant","created_at":"2020-10-14T21:43:32Z","updated_at":"2020-10-14T21:43:32Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '1433' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 21:43:32 GMT + ETag: + - '"52cf59066e1ffe22c9cb27c8209799e393edcd9ab77625e06a9aa678b4aa1c5e"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D84E:5FB2:2397A:6569E:5F877104 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4987' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '13' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 201 +- request: + body: '{"state": "error", "target_url": "https://localhost:50036/github/codecov", + "context": "giant", "description": "error - 8 - giant"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '130' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":11053793474,"node_id":"MDEzOlN0YXR1c0NvbnRleHQxMTA1Mzc5MzQ3NA==","state":"error","description":"error + - 8 - giant","target_url":"https://localhost:50036/github/codecov","context":"giant","created_at":"2020-10-14T21:43:32Z","updated_at":"2020-10-14T21:43:32Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '1429' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 21:43:33 GMT + ETag: + - '"161512240fdd40ae1ba6e6379ec9b8917209580a6f8d2d1d3485ed298a1f924f"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D84F:686E:2BED8:74E84:5F877104 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4986' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '14' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 201 +- request: + body: '{"state": "success", "target_url": "https://localhost:50036/github/codecov", + "context": "giant", "description": "success - 9 - giant"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '134' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":11053793637,"node_id":"MDEzOlN0YXR1c0NvbnRleHQxMTA1Mzc5MzYzNw==","state":"success","description":"success + - 9 - giant","target_url":"https://localhost:50036/github/codecov","context":"giant","created_at":"2020-10-14T21:43:33Z","updated_at":"2020-10-14T21:43:33Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '1433' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 21:43:33 GMT + ETag: + - '"be12bf96ff07ab3d4e9cc4dc9c1b9a03ab495a54c6406b44973bc75ae8737aab"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D850:19B0:4F5C6:ADDDD:5F877105 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4985' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '15' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 201 +- request: + body: '{"state": "success", "target_url": "https://localhost:50036/github/codecov", + "context": "capybara", "description": "success - 10 - capybara"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '141' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: POST + uri: https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + response: + content: '{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":11053793749,"node_id":"MDEzOlN0YXR1c0NvbnRleHQxMTA1Mzc5Mzc0OQ==","state":"success","description":"success + - 10 - capybara","target_url":"https://localhost:50036/github/codecov","context":"capybara","created_at":"2020-10-14T21:43:34Z","updated_at":"2020-10-14T21:43:34Z","creator":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Length: + - '1440' + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 21:43:34 GMT + ETag: + - '"ad64ac8aeeb1d3d9c8ffbadbb4e57b16441ba08dbe583459ba685b0e9b3b41b4"' + Location: + - https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382 + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 201 Created + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - '' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D851:5FB4:A9612:1170DE:5F877106 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4984' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '16' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 201 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.github.com + user-agent: + - Default + method: GET + uri: https://api.github.com/repos/ThiagoCodecov/example-python/commits/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382/status?page=1&per_page=100 + response: + content: '{"state":"failure","statuses":[{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":11053792996,"node_id":"MDEzOlN0YXR1c0NvbnRleHQxMTA1Mzc5Mjk5Ng==","state":"pending","description":"pending + - 4 - turtle","target_url":"https://localhost:50036/github/codecov","context":"turtle","created_at":"2020-10-14T21:43:30Z","updated_at":"2020-10-14T21:43:30Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":11053793107,"node_id":"MDEzOlN0YXR1c0NvbnRleHQxMTA1Mzc5MzEwNw==","state":"failure","description":"failure + - 5 - bird","target_url":"https://localhost:50036/github/codecov","context":"bird","created_at":"2020-10-14T21:43:31Z","updated_at":"2020-10-14T21:43:31Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":11053793212,"node_id":"MDEzOlN0YXR1c0NvbnRleHQxMTA1Mzc5MzIxMg==","state":"error","description":"error + - 6 - pig","target_url":"https://localhost:50036/github/codecov","context":"pig","created_at":"2020-10-14T21:43:31Z","updated_at":"2020-10-14T21:43:31Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":11053793637,"node_id":"MDEzOlN0YXR1c0NvbnRleHQxMTA1Mzc5MzYzNw==","state":"success","description":"success + - 9 - giant","target_url":"https://localhost:50036/github/codecov","context":"giant","created_at":"2020-10-14T21:43:33Z","updated_at":"2020-10-14T21:43:33Z"},{"url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","id":11053793749,"node_id":"MDEzOlN0YXR1c0NvbnRleHQxMTA1Mzc5Mzc0OQ==","state":"success","description":"success + - 10 - capybara","target_url":"https://localhost:50036/github/codecov","context":"capybara","created_at":"2020-10-14T21:43:34Z","updated_at":"2020-10-14T21:43:34Z"}],"sha":"702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","total_count":5,"repository":{"id":156617777,"node_id":"MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=","name":"example-python","full_name":"ThiagoCodecov/example-python","private":false,"owner":{"login":"ThiagoCodecov","id":44376991,"node_id":"MDQ6VXNlcjQ0Mzc2OTkx","avatar_url":"https://avatars3.githubusercontent.com/u/44376991?v=4","gravatar_id":"","url":"https://api.github.com/users/ThiagoCodecov","html_url":"https://github.com/ThiagoCodecov","followers_url":"https://api.github.com/users/ThiagoCodecov/followers","following_url":"https://api.github.com/users/ThiagoCodecov/following{/other_user}","gists_url":"https://api.github.com/users/ThiagoCodecov/gists{/gist_id}","starred_url":"https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ThiagoCodecov/subscriptions","organizations_url":"https://api.github.com/users/ThiagoCodecov/orgs","repos_url":"https://api.github.com/users/ThiagoCodecov/repos","events_url":"https://api.github.com/users/ThiagoCodecov/events{/privacy}","received_events_url":"https://api.github.com/users/ThiagoCodecov/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ThiagoCodecov/example-python","description":"Python + coverage example","fork":true,"url":"https://api.github.com/repos/ThiagoCodecov/example-python","forks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/forks","keys_url":"https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ThiagoCodecov/example-python/teams","hooks_url":"https://api.github.com/repos/ThiagoCodecov/example-python/hooks","issue_events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}","events_url":"https://api.github.com/repos/ThiagoCodecov/example-python/events","assignees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}","branches_url":"https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}","tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/tags","blobs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}","trees_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}","languages_url":"https://api.github.com/repos/ThiagoCodecov/example-python/languages","stargazers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/stargazers","contributors_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contributors","subscribers_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscribers","subscription_url":"https://api.github.com/repos/ThiagoCodecov/example-python/subscription","commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}","git_commits_url":"https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}","comments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}","issue_comment_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}","contents_url":"https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}","compare_url":"https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ThiagoCodecov/example-python/merges","archive_url":"https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ThiagoCodecov/example-python/downloads","issues_url":"https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}","pulls_url":"https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}","milestones_url":"https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}","notifications_url":"https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}","releases_url":"https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}","deployments_url":"https://api.github.com/repos/ThiagoCodecov/example-python/deployments"},"commit_url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382","url":"https://api.github.com/repos/ThiagoCodecov/example-python/commits/702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382/status"}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 21:43:35 GMT + ETag: + - W/"0c89436facbf0942e132cea622b6934620d44714079df7a4f86d9f793da3b472" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Accepted-OAuth-Scopes: + - repo, repo:status + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3 + X-GitHub-Request-Id: + - D852:3397:12AEA:2D30B:5F877106 + X-OAuth-Scopes: + - admin:org, admin:public_key, admin:repo_hook, repo, user, write:discussion + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4983' + X-RateLimit-Reset: + - '1602714521' + X-RateLimit-Used: + - '17' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_update_github_check.yaml b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_update_github_check.yaml new file mode 100644 index 0000000000..dc11963f6a --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_github/TestGithubTestCase/test_update_github_check.yaml @@ -0,0 +1,130 @@ +interactions: +- request: + body: '{"conclusion": "success", "status": "completed", "output": null, "details_url":"https://app.codecov.io/gh/codecov/example-python/compare/1?src=pr"}' + headers: + accept: + - application/vnd.github.antiope-preview+json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '64' + content-type: + - application/json + host: + - api.github.com + user-agent: + - Default + method: PATCH + uri: https://api.github.com/repos/ThiagoCodecov/example-python/check-runs/1256232357 + response: + content: "{\"id\":1256232357,\"node_id\":\"MDg6Q2hlY2tSdW4xMjU2MjMyMzU3\",\"head_sha\"\ + :\"75f355d8d14ba3d7761c728b4d2607cde0eef065\",\"external_id\":\"\",\"url\":\"\ + https://api.github.com/repos/ThiagoCodecov/example-python/check-runs/1256232357\"\ + ,\"html_url\":\"https://github.com/ThiagoCodecov/example-python/runs/1256232357\"\ + ,\"details_url\":\"https://app.codecov.io/gh/codecov/example-python/compare/1?src=pr\",\"status\":\"completed\",\"conclusion\"\ + :\"success\",\"started_at\":\"2020-10-14T23:00:59Z\",\"completed_at\":\"2020-10-14T23:01:14Z\"\ + ,\"output\":{\"title\":null,\"summary\":null,\"text\":null,\"annotations_count\"\ + :0,\"annotations_url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/check-runs/1256232357/annotations\"\ + },\"name\":\"Test check\",\"check_suite\":{\"id\":1341719124},\"app\":{\"id\"\ + :254,\"slug\":\"codecov\",\"node_id\":\"MDM6QXBwMjU0\",\"owner\":{\"login\"\ + :\"codecov\",\"id\":8226205,\"node_id\":\"MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=\"\ + ,\"avatar_url\":\"https://avatars3.githubusercontent.com/u/8226205?v=4\",\"\ + gravatar_id\":\"\",\"url\":\"https://api.github.com/users/codecov\",\"html_url\"\ + :\"https://github.com/codecov\",\"followers_url\":\"https://api.github.com/users/codecov/followers\"\ + ,\"following_url\":\"https://api.github.com/users/codecov/following{/other_user}\"\ + ,\"gists_url\":\"https://api.github.com/users/codecov/gists{/gist_id}\",\"starred_url\"\ + :\"https://api.github.com/users/codecov/starred{/owner}{/repo}\",\"subscriptions_url\"\ + :\"https://api.github.com/users/codecov/subscriptions\",\"organizations_url\"\ + :\"https://api.github.com/users/codecov/orgs\",\"repos_url\":\"https://api.github.com/users/codecov/repos\"\ + ,\"events_url\":\"https://api.github.com/users/codecov/events{/privacy}\",\"\ + received_events_url\":\"https://api.github.com/users/codecov/received_events\"\ + ,\"type\":\"Organization\",\"site_admin\":false},\"name\":\"Codecov\",\"description\"\ + :\"Codecov provides highly integrated tools to group, merge, archive and compare\ + \ coverage reports. Whether your team is comparing changes in a pull request\ + \ or reviewing a single commit, Codecov will improve the code review workflow\ + \ and quality.\\r\\n\\r\\n## Code coverage done right.\xAE\\r\\n\\r\\n1. Upload\ + \ coverage reports from your CI builds.\\r\\n2. Codecov merges all builds and\ + \ languages into one beautiful coherent report.\\r\\n3. Get commit statuses,\ + \ pull request comments and coverage overlay via our browser extension.\\r\\\ + n\\r\\nWhen Codecov merges your uploads it keeps track of the CI provider (inc.\ + \ build details) and user specified context, e.g. `#unittest` ~ `#smoketest`\ + \ or `#oldcode` ~ `#newcode`. You can track the `#unittest` coverage independently\ + \ of other groups. [Learn more here](\\r\\nhttp://docs.codecov.io/docs/flags)\\\ + r\\n\\r\\nThrough **Codecov's Browser Extension** reports overlay directly in\ + \ GitHub UI to assist in code review. [Watch here](https://docs.codecov.io/docs/browser-extension)\\\ + r\\n\\r\\n*Highly detailed* **pull request comments** and *customizable* **commit\ + \ statuses** will improve your team's workflow and code coverage incrementally.\\\ + r\\n\\r\\n**File backed configuration** all through the `codecov.yml`. \\r\\\ + n\\r\\n## FAQ\\r\\n- Do you **merge multiple uploads** to the same commit? **Yes**\\\ + r\\n- Do you **support multiple languages** in the same project? **Yes**\\r\\\ + n- Can you **group coverage reports** by project and/or test type? **Yes**\\\ + r\\n- How does **pricing** work? Only paid users can view reports and post statuses/comments.\ + \ \",\"external_url\":\"https://codecov.io\",\"html_url\":\"https://github.com/apps/codecov\"\ + ,\"created_at\":\"2016-09-25T14:18:27Z\",\"updated_at\":\"2020-08-27T18:10:18Z\"\ + ,\"permissions\":{\"administration\":\"read\",\"checks\":\"write\",\"contents\"\ + :\"read\",\"issues\":\"read\",\"members\":\"read\",\"metadata\":\"read\",\"\ + pull_requests\":\"write\",\"statuses\":\"write\"},\"events\":[\"check_run\"\ + ,\"check_suite\",\"create\",\"delete\",\"fork\",\"membership\",\"public\",\"\ + pull_request\",\"push\",\"release\",\"repository\",\"status\",\"team_add\"]},\"\ + pull_requests\":[{\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18\"\ + ,\"id\":383348775,\"number\":18,\"head\":{\"ref\":\"thiago/base-no-base\",\"\ + sha\":\"75f355d8d14ba3d7761c728b4d2607cde0eef065\",\"repo\":{\"id\":156617777,\"\ + url\":\"https://api.github.com/repos/ThiagoCodecov/example-python\",\"name\"\ + :\"example-python\"}},\"base\":{\"ref\":\"main\",\"sha\":\"f0895290dc26668faeeb20ee5ccd4cc995925775\"\ + ,\"repo\":{\"id\":156617777,\"url\":\"https://api.github.com/repos/ThiagoCodecov/example-python\"\ + ,\"name\":\"example-python\"}}}]}" + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset + Cache-Control: + - private, max-age=60, s-maxage=60 + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Oct 2020 23:01:14 GMT + ETag: + - W/"360545ef5181d34fad4d9d1862db9b5ebfce43b3fc0d4b376c5dd896fbb533b6" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + Vary: + - Accept, Authorization, Cookie, X-GitHub-OTP + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v3; param=antiope-preview; format=json + X-GitHub-Request-Id: + - DA5C:7DCA:467310:7C17C9:5F878339 + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4994' + X-RateLimit-Reset: + - '1602719974' + X-RateLimit-Used: + - '6' + X-XSS-Protection: + - 1; mode=block + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_delete_comment.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_delete_comment.yaml new file mode 100644 index 0000000000..4583be6883 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_delete_comment.yaml @@ -0,0 +1,31 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: DELETE + uri: https://gitlab.com/api/v4/projects/187725/merge_requests/1/notes/113977323 + response: + url: https://gitlab.com/api/v4/projects/187725/merge_requests/1/notes/113977323 + content: '' + headers: + Server: nginx + Date: Mon, 05 Nov 2018 18:56:26 GMT + Connection: close + Cache-Control: no-cache + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: b50eb5b8-764c-4515-8add-edb771242065 + X-Runtime: '0.109366' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '2' + Ratelimit-Remaining: '598' + Ratelimit-Reset: '1541444246' + Ratelimit-Resettime: Tue, 05 Nov 2018 18:57:26 GMT + status_code: 204 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_delete_comment_not_found.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_delete_comment_not_found.yaml new file mode 100644 index 0000000000..e0db58c035 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_delete_comment_not_found.yaml @@ -0,0 +1,30 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: DELETE + uri: https://gitlab.com/api/v4/projects/187725/merge_requests/1/notes/113977999 + response: + url: https://gitlab.com/api/v4/projects/187725/merge_requests/1/notes/113977999 + content: '{"message":"404 Not found"}' + headers: + Server: nginx + Date: Mon, 05 Nov 2018 18:58:23 GMT + Content-Type: application/json + Content-Length: '27' + Connection: close + Cache-Control: no-cache + Vary: Origin + X-Request-Id: 66034a28-c90b-477a-92c8-75cf7811f20d + X-Runtime: '0.055006' + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1541444363' + Ratelimit-Resettime: Tue, 05 Nov 2018 18:59:23 GMT + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_delete_webhook.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_delete_webhook.yaml new file mode 100644 index 0000000000..f39faee304 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_delete_webhook.yaml @@ -0,0 +1,31 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: DELETE + uri: https://gitlab.com/api/v4/projects/187725/hooks/422507 + response: + url: https://gitlab.com/api/v4/projects/187725/hooks/422507 + content: '' + headers: + Server: nginx + Date: Tue, 06 Nov 2018 05:02:13 GMT + Connection: close + Cache-Control: no-cache + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: f2d875ed-8876-4b72-931c-d659574d6264 + X-Runtime: '0.049754' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1541480593' + Ratelimit-Resettime: Wed, 06 Nov 2018 05:03:13 GMT + status_code: 204 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_delete_webhook_not_found.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_delete_webhook_not_found.yaml new file mode 100644 index 0000000000..2deef5ee51 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_delete_webhook_not_found.yaml @@ -0,0 +1,30 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: DELETE + uri: https://gitlab.com/api/v4/projects/187725/hooks/422507987 + response: + url: https://gitlab.com/api/v4/projects/187725/hooks/422507987 + content: '{"message":"404 Not found"}' + headers: + Server: nginx + Date: Tue, 06 Nov 2018 05:02:30 GMT + Content-Type: application/json + Content-Length: '27' + Connection: close + Cache-Control: no-cache + Vary: Origin + X-Request-Id: 48077c9c-ee79-4e03-bc54-adf26d45685c + X-Runtime: '0.052297' + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1541480610' + Ratelimit-Resettime: Wed, 06 Nov 2018 05:03:30 GMT + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_edit_comment.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_edit_comment.yaml new file mode 100644 index 0000000000..4fbc6e102f --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_edit_comment.yaml @@ -0,0 +1,36 @@ +interactions: + - request: + body: '{"body": "Hello world number 2"}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Default + method: PUT + uri: https://gitlab.com/api/v4/projects/187725/merge_requests/1/notes/113977323 + response: + url: https://gitlab.com/api/v4/projects/187725/merge_requests/1/notes/113977323 + content: '{"id":113977323,"type":null,"body":"Hello world number 2","attachment":null,"author":{"id":109640,"name":"Codecov","username":"codecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/dcdb35375db567705dd7e74226fae67b?s=80\u0026d=identicon","web_url":"https://gitlab.com/codecov"},"created_at":"2018-11-02T05:25:09.363Z","updated_at":"2018-11-02T05:25:53.612Z","system":false,"noteable_id":59639,"noteable_type":"MergeRequest","resolvable":false,"noteable_iid":1}' + headers: + Server: nginx + Date: Fri, 02 Nov 2018 05:25:53 GMT + Content-Type: application/json + Content-Length: '480' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"30094dfa7e43a90a8f05a73e82200a64" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: 99818e41-33b5-414c-83cd-81026caa367c + X-Runtime: '0.084227' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '2' + Ratelimit-Remaining: '598' + Ratelimit-Reset: '1541136413' + Ratelimit-Resettime: Sat, 02 Nov 2018 05:26:53 GMT + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_edit_comment_not_found.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_edit_comment_not_found.yaml new file mode 100644 index 0000000000..0dd497f407 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_edit_comment_not_found.yaml @@ -0,0 +1,32 @@ +interactions: + - request: + body: '{"body": "Hello world number 2"}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Default + method: PUT + uri: https://gitlab.com/api/v4/projects/187725/merge_requests/1/notes/113979999 + response: + url: https://gitlab.com/api/v4/projects/187725/merge_requests/1/notes/113979999 + content: '{"message":"404 Not found"}' + headers: + Server: nginx + Date: Mon, 05 Nov 2018 18:59:21 GMT + Content-Type: application/json + Content-Length: '27' + Connection: close + Cache-Control: no-cache + Vary: Origin + X-Request-Id: dd5009dc-95d5-4ecd-9fb6-32f563119d58 + X-Runtime: '0.044932' + Ratelimit-Limit: '600' + Ratelimit-Observed: '11' + Ratelimit-Remaining: '589' + Ratelimit-Reset: '1541444421' + Ratelimit-Resettime: Tue, 05 Nov 2018 19:00:21 GMT + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_edit_webhook.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_edit_webhook.yaml new file mode 100644 index 0000000000..4250e807a2 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_edit_webhook.yaml @@ -0,0 +1,37 @@ +interactions: + - request: + body: '{"url": "http://requestbin.net/r/1ecyaj51", "enable_ssl_verification": + true, "tag_push_events": true, "note_events": true}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Default + method: PUT + uri: https://gitlab.com/api/v4/projects/187725/hooks/422507 + response: + url: https://gitlab.com/api/v4/projects/187725/hooks/422507 + content: '{"id":422507,"url":"http://requestbin.net/r/1ecyaj51","created_at":"2018-11-06T04:51:57.164Z","push_events":true,"tag_push_events":true,"merge_requests_events":false,"repository_update_events":false,"enable_ssl_verification":true,"project_id":187725,"issues_events":false,"confidential_issues_events":false,"note_events":true,"confidential_note_events":null,"pipeline_events":false,"wiki_page_events":false,"job_events":true,"push_events_branch_filter":null}' + headers: + Server: nginx + Date: Tue, 06 Nov 2018 05:00:26 GMT + Content-Type: application/json + Content-Length: '458' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"9def5c5ade40d26af196eeecaf5fee6a" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: 73c95306-3e10-49f7-8f1e-6a92e7633d06 + X-Runtime: '0.138632' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1541480486' + Ratelimit-Resettime: Wed, 06 Nov 2018 05:01:26 GMT + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_find_pull_request_merge_requests_disabled.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_find_pull_request_merge_requests_disabled.yaml new file mode 100644 index 0000000000..9e3546bafd --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_find_pull_request_merge_requests_disabled.yaml @@ -0,0 +1,83 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/merge_requests?state=opened + response: + url: https://gitlab.com/api/v4/projects/187725/merge_requests?state=opened + content: '{"message":"403 Forbidden"}' + headers: + Server: nginx + Date: Mon, 05 Nov 2018 19:13:26 GMT + Content-Type: application/json + Content-Length: '1084' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"89a964567a1549306cc2fe9aa5ca23ae" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: f9101899-1557-4c8e-a461-0db681ffe5fd + X-Runtime: '0.060825' + X-Total: '1' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '5' + Ratelimit-Remaining: '595' + Ratelimit-Reset: '1541445266' + Ratelimit-Resettime: Tue, 05 Nov 2018 19:14:26 GMT + status_code: 403 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/commits/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/merge_requests + response: + url: https://gitlab.com/api/v4/projects/187725/repository/commits/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/merge_requests + content: '{"message":"404 Commit Not Found"}' + headers: + X-Http-Reason: Not Found + Date: Wed, 17 Jun 2020 18:28:58 GMT + Content-Type: application/json + Content-Length: '34' + Connection: keep-alive + Set-Cookie: __cfduid=d7bcd1d15a03f936b3984e0e7fdc22ded1592418538; expires=Fri, + 17-Jul-20 18:28:58 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; + Secure + Cache-Control: no-cache + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: ATKnGryIvT9 + X-Runtime: '0.034947' + Ratelimit-Limit: '600' + Ratelimit-Observed: '4' + Ratelimit-Remaining: '596' + Ratelimit-Reset: '1592418598' + Ratelimit-Resettime: Wed, 17 Jun 2020 18:29:58 GMT + Gitlab-Lb: fe-03-lb-gprd + Gitlab-Sv: localhost + Cf-Cache-Status: DYNAMIC + Cf-Request-Id: 036523aa670000f64b1a0b1200000001 + Expect-Ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Server: cloudflare + Cf-Ray: 5a4ed5570999f64b-GRU + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_find_pull_request_nothing_found.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_find_pull_request_nothing_found.yaml new file mode 100644 index 0000000000..62024fe1d6 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_find_pull_request_nothing_found.yaml @@ -0,0 +1,83 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/merge_requests?state=opened + response: + url: https://gitlab.com/api/v4/projects/187725/merge_requests?state=opened + content: '[{"id":59639,"iid":1,"project_id":187725,"title":"Other branch","description":"","state":"opened","created_at":"2015-03-12T04:40:38.680Z","updated_at":"2018-11-02T05:25:53.623Z","target_branch":"main","source_branch":"other-branch","upvotes":0,"downvotes":0,"author":{"id":109640,"name":"Codecov","username":"codecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/dcdb35375db567705dd7e74226fae67b?s=80\u0026d=identicon","web_url":"https://gitlab.com/codecov"},"assignee":null,"source_project_id":187725,"target_project_id":187725,"labels":[],"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"can_be_merged","sha":"dd798926730aad14aadf72281204bdb85734fe67","merge_commit_sha":null,"user_notes_count":89,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":null,"web_url":"https://gitlab.com/codecov/ci-repo/merge_requests/1","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"approvals_before_merge":null}]' + headers: + Server: nginx + Date: Mon, 05 Nov 2018 19:13:26 GMT + Content-Type: application/json + Content-Length: '1084' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"89a964567a1549306cc2fe9aa5ca23ae" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: f9101899-1557-4c8e-a461-0db681ffe5fd + X-Runtime: '0.060825' + X-Total: '1' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '5' + Ratelimit-Remaining: '595' + Ratelimit-Reset: '1541445266' + Ratelimit-Resettime: Tue, 05 Nov 2018 19:14:26 GMT + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/commits/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/merge_requests + response: + url: https://gitlab.com/api/v4/projects/187725/repository/commits/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/merge_requests + content: '{"message":"404 Commit Not Found"}' + headers: + X-Http-Reason: Not Found + Date: Wed, 17 Jun 2020 18:28:57 GMT + Content-Type: application/json + Content-Length: '34' + Connection: keep-alive + Set-Cookie: __cfduid=d22f88fdbc24209bc7f0a882b092b403d1592418537; expires=Fri, + 17-Jul-20 18:28:57 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; + Secure + Cache-Control: no-cache + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: PkcL6Ke2nf3 + X-Runtime: '0.049885' + Ratelimit-Limit: '600' + Ratelimit-Observed: '3' + Ratelimit-Remaining: '597' + Ratelimit-Reset: '1592418597' + Ratelimit-Resettime: Wed, 17 Jun 2020 18:29:57 GMT + Gitlab-Lb: fe-04-lb-gprd + Gitlab-Sv: localhost + Cf-Cache-Status: DYNAMIC + Cf-Request-Id: 036523a8bd0000f6e3a23d9200000001 + Expect-Ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Server: cloudflare + Cf-Ray: 5a4ed5546847f6e3-GRU + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_find_pull_request_pr_found.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_find_pull_request_pr_found.yaml new file mode 100644 index 0000000000..3c657efb39 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_find_pull_request_pr_found.yaml @@ -0,0 +1,112 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/commits/dd798926730aad14aadf72281204bdb85734fe67/merge_requests + response: + url: https://gitlab.com/api/v4/projects/187725/repository/commits/dd798926730aad14aadf72281204bdb85734fe67/merge_requests + content: '[{"id":181852,"iid":2,"project_id":187725,"title":"Other branch","description":"sadfsadf","state":"closed","created_at":"2015-10-20T10:07:15.430Z","updated_at":"2015-10-20T10:47:25.003Z","merged_by":null,"merged_at":null,"closed_by":null,"closed_at":null,"target_branch":"main","source_branch":"other-branch","user_notes_count":1,"upvotes":0,"downvotes":0,"assignee":null,"author":{"id":256075,"name":"Mahmut + Bayri","username":"mahmutbayri","state":"active","avatar_url":"https://secure.gravatar.com/avatar/d28ee7e15f316b46fd6904449c22188f?s=80\u0026d=identicon","web_url":"https://gitlab.com/mahmutbayri"},"assignees":[],"source_project_id":482893,"target_project_id":187725,"labels":[],"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"can_be_merged","sha":"02f639e9edeec1ddaf8029ebc047707a53bc5c18","merge_commit_sha":null,"squash_commit_sha":null,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":null,"allow_collaboration":null,"allow_maintainer_to_push":null,"reference":"!2","references":{"short":"!2","relative":"!2","full":"codecov/ci-repo!2"},"web_url":"https://gitlab.com/codecov/ci-repo/-/merge_requests/2","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"approvals_before_merge":null},{"id":59639,"iid":1,"project_id":187725,"title":"Other + branch","description":"","state":"opened","created_at":"2015-03-12T04:40:38.680Z","updated_at":"2020-04-24T02:53:32.995Z","merged_by":null,"merged_at":null,"closed_by":null,"closed_at":null,"target_branch":"main","source_branch":"other-branch","user_notes_count":91,"upvotes":0,"downvotes":0,"assignee":null,"author":{"id":109640,"name":"Codecov","username":"codecov","state":"active","avatar_url":"https://assets.gitlab-static.net/uploads/-/system/user/avatar/109640/avatar.png","web_url":"https://gitlab.com/codecov"},"assignees":[],"source_project_id":187725,"target_project_id":187725,"labels":[],"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"can_be_merged","sha":"dd798926730aad14aadf72281204bdb85734fe67","merge_commit_sha":null,"squash_commit_sha":null,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":null,"reference":"!1","references":{"short":"!1","relative":"!1","full":"codecov/ci-repo!1"},"web_url":"https://gitlab.com/codecov/ci-repo/-/merge_requests/1","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"approvals_before_merge":null}]' + headers: + X-Http-Reason: OK + Date: Wed, 17 Jun 2020 22:01:01 GMT + Content-Type: application/json + Transfer-Encoding: chunked + Connection: keep-alive + Set-Cookie: __cfduid=de186c9c664d94f434e40e187a808687d1592431260; expires=Fri, + 17-Jul-20 22:01:00 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; + Secure + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"40b35909665a08d41248ea15d10c8a40" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: v2CC9PlfLE9 + X-Runtime: '0.181644' + X-Total: '2' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Referrer-Policy: strict-origin-when-cross-origin + Ratelimit-Limit: '600' + Ratelimit-Observed: '9' + Ratelimit-Remaining: '591' + Ratelimit-Reset: '1592431321' + Ratelimit-Resettime: Wed, 17 Jun 2020 22:02:01 GMT + Gitlab-Lb: fe-07-lb-gprd + Gitlab-Sv: localhost + Cf-Cache-Status: DYNAMIC + Cf-Request-Id: 0365e5cbf30000f3bbfc39d200000001 + Expect-Ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Server: cloudflare + Cf-Ray: 5a500bf31b91f3bb-GRU + Content-Encoding: gzip + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/commits/dd798926730aad14aadf72281204bdb85734fe67/merge_requests + response: + url: https://gitlab.com/api/v4/projects/187725/repository/commits/dd798926730aad14aadf72281204bdb85734fe67/merge_requests + content: '[{"id":181852,"iid":2,"project_id":187725,"title":"Other branch","description":"sadfsadf","state":"closed","created_at":"2015-10-20T10:07:15.430Z","updated_at":"2015-10-20T10:47:25.003Z","merged_by":null,"merged_at":null,"closed_by":null,"closed_at":null,"target_branch":"main","source_branch":"other-branch","user_notes_count":1,"upvotes":0,"downvotes":0,"assignee":null,"author":{"id":256075,"name":"Mahmut + Bayri","username":"mahmutbayri","state":"active","avatar_url":"https://secure.gravatar.com/avatar/d28ee7e15f316b46fd6904449c22188f?s=80\u0026d=identicon","web_url":"https://gitlab.com/mahmutbayri"},"assignees":[],"source_project_id":482893,"target_project_id":187725,"labels":[],"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"can_be_merged","sha":"02f639e9edeec1ddaf8029ebc047707a53bc5c18","merge_commit_sha":null,"squash_commit_sha":null,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":null,"allow_collaboration":null,"allow_maintainer_to_push":null,"reference":"!2","references":{"short":"!2","relative":"!2","full":"codecov/ci-repo!2"},"web_url":"https://gitlab.com/codecov/ci-repo/-/merge_requests/2","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"approvals_before_merge":null},{"id":59639,"iid":1,"project_id":187725,"title":"Other + branch","description":"","state":"opened","created_at":"2015-03-12T04:40:38.680Z","updated_at":"2020-04-24T02:53:32.995Z","merged_by":null,"merged_at":null,"closed_by":null,"closed_at":null,"target_branch":"main","source_branch":"other-branch","user_notes_count":91,"upvotes":0,"downvotes":0,"assignee":null,"author":{"id":109640,"name":"Codecov","username":"codecov","state":"active","avatar_url":"https://assets.gitlab-static.net/uploads/-/system/user/avatar/109640/avatar.png","web_url":"https://gitlab.com/codecov"},"assignees":[],"source_project_id":187725,"target_project_id":187725,"labels":[],"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"can_be_merged","sha":"dd798926730aad14aadf72281204bdb85734fe67","merge_commit_sha":null,"squash_commit_sha":null,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":null,"reference":"!1","references":{"short":"!1","relative":"!1","full":"codecov/ci-repo!1"},"web_url":"https://gitlab.com/codecov/ci-repo/-/merge_requests/1","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"approvals_before_merge":null}]' + headers: + X-Http-Reason: OK + Date: Wed, 17 Jun 2020 22:01:01 GMT + Content-Type: application/json + Transfer-Encoding: chunked + Connection: keep-alive + Set-Cookie: __cfduid=d72f7e96d2f081758647c32c29cd947c11592431261; expires=Fri, + 17-Jul-20 22:01:01 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; + Secure + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"40b35909665a08d41248ea15d10c8a40" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: oxHUpFW7W5 + X-Runtime: '0.175102' + X-Total: '2' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Referrer-Policy: strict-origin-when-cross-origin + Ratelimit-Limit: '600' + Ratelimit-Observed: '10' + Ratelimit-Remaining: '590' + Ratelimit-Reset: '1592431321' + Ratelimit-Resettime: Wed, 17 Jun 2020 22:02:01 GMT + Gitlab-Lb: fe-03-lb-gprd + Gitlab-Sv: localhost + Cf-Cache-Status: DYNAMIC + Cf-Request-Id: 0365e5ce610000f3bbfc3cc200000001 + Expect-Ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Server: cloudflare + Cf-Ray: 5a500bf70a40f3bb-GRU + Content-Encoding: gzip + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_find_pull_request_pr_found_branch.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_find_pull_request_pr_found_branch.yaml new file mode 100644 index 0000000000..c7654a9a62 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_find_pull_request_pr_found_branch.yaml @@ -0,0 +1,109 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/merge_requests?state=closed + response: + url: https://gitlab.com/api/v4/projects/187725/merge_requests?state=closed + content: '[{"id":181852,"iid":2,"project_id":187725,"title":"Other branch","description":"sadfsadf","state":"closed","created_at":"2015-10-20T10:07:15.430Z","updated_at":"2015-10-20T10:47:25.003Z","merged_by":null,"merged_at":null,"closed_by":null,"closed_at":null,"target_branch":"main","source_branch":"other-branch","user_notes_count":1,"upvotes":0,"downvotes":0,"assignee":null,"author":{"id":256075,"name":"Mahmut + Bayri","username":"mahmutbayri","state":"active","avatar_url":"https://secure.gravatar.com/avatar/d28ee7e15f316b46fd6904449c22188f?s=80\u0026d=identicon","web_url":"https://gitlab.com/mahmutbayri"},"assignees":[],"source_project_id":482893,"target_project_id":187725,"labels":[],"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"can_be_merged","sha":"02f639e9edeec1ddaf8029ebc047707a53bc5c18","merge_commit_sha":null,"squash_commit_sha":null,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":null,"allow_collaboration":null,"allow_maintainer_to_push":null,"reference":"!2","references":{"short":"!2","relative":"!2","full":"codecov/ci-repo!2"},"web_url":"https://gitlab.com/codecov/ci-repo/-/merge_requests/2","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"approvals_before_merge":null}]' + headers: + X-Http-Reason: OK + Date: Wed, 17 Jun 2020 22:58:09 GMT + Content-Type: application/json + Transfer-Encoding: chunked + Connection: keep-alive + Set-Cookie: __cfduid=d008249b67a70a8bafae10ae4d5822b981592434689; expires=Fri, + 17-Jul-20 22:58:09 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; + Secure + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"c4b9048e8f53b6ebd3896cf4e5944a77" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: rZDjDPw54C7 + X-Runtime: '0.077213' + X-Total: '1' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Referrer-Policy: strict-origin-when-cross-origin + Ratelimit-Limit: '600' + Ratelimit-Observed: '4' + Ratelimit-Remaining: '596' + Ratelimit-Reset: '1592434749' + Ratelimit-Resettime: Wed, 17 Jun 2020 22:59:09 GMT + Gitlab-Lb: fe-09-lb-gprd + Gitlab-Sv: localhost + Cf-Cache-Status: DYNAMIC + Cf-Request-Id: 03661a1d000000f6e82eb18200000001 + Expect-Ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Server: cloudflare + Cf-Ray: 5a505fa80938f6e8-GRU + Content-Encoding: gzip + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/merge_requests?state=opened + response: + url: https://gitlab.com/api/v4/projects/187725/merge_requests?state=opened + content: '[{"id":59639,"iid":1,"project_id":187725,"title":"Other branch","description":"","state":"opened","created_at":"2015-03-12T04:40:38.680Z","updated_at":"2020-04-24T02:53:32.995Z","merged_by":null,"merged_at":null,"closed_by":null,"closed_at":null,"target_branch":"main","source_branch":"other-branch","user_notes_count":91,"upvotes":0,"downvotes":0,"assignee":null,"author":{"id":109640,"name":"Codecov","username":"codecov","state":"active","avatar_url":"https://assets.gitlab-static.net/uploads/-/system/user/avatar/109640/avatar.png","web_url":"https://gitlab.com/codecov"},"assignees":[],"source_project_id":187725,"target_project_id":187725,"labels":[],"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"can_be_merged","sha":"dd798926730aad14aadf72281204bdb85734fe67","merge_commit_sha":null,"squash_commit_sha":null,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":null,"reference":"!1","references":{"short":"!1","relative":"!1","full":"codecov/ci-repo!1"},"web_url":"https://gitlab.com/codecov/ci-repo/-/merge_requests/1","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"approvals_before_merge":null}]' + headers: + X-Http-Reason: OK + Date: Wed, 17 Jun 2020 22:58:09 GMT + Content-Type: application/json + Transfer-Encoding: chunked + Connection: keep-alive + Set-Cookie: __cfduid=d008249b67a70a8bafae10ae4d5822b981592434689; expires=Fri, + 17-Jul-20 22:58:09 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; + Secure + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"a73070a70ef694691570e4dae7aa39e0" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: RAvSJfd7wN6 + X-Runtime: '0.085442' + X-Total: '1' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Referrer-Policy: strict-origin-when-cross-origin + Ratelimit-Limit: '600' + Ratelimit-Observed: '5' + Ratelimit-Remaining: '595' + Ratelimit-Reset: '1592434749' + Ratelimit-Resettime: Wed, 17 Jun 2020 22:59:09 GMT + Gitlab-Lb: fe-05-lb-gprd + Gitlab-Sv: localhost + Cf-Cache-Status: DYNAMIC + Cf-Request-Id: 03661a1e2a0000f6e82eb33200000001 + Expect-Ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Server: cloudflare + Cf-Ray: 5a505fa9dcaaf6e8-GRU + Content-Encoding: gzip + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_find_pull_request_project_not_found.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_find_pull_request_project_not_found.yaml new file mode 100644 index 0000000000..e365ded125 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_find_pull_request_project_not_found.yaml @@ -0,0 +1,83 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/merge_requests?state=opened + response: + url: https://gitlab.com/api/v4/projects/187725/merge_requests?state=opened + content: '{"message":"404 Not found"}' + headers: + Server: nginx + Date: Mon, 05 Nov 2018 19:13:26 GMT + Content-Type: application/json + Content-Length: '1084' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"89a964567a1549306cc2fe9aa5ca23ae" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: f9101899-1557-4c8e-a461-0db681ffe5fd + X-Runtime: '0.060825' + X-Total: '1' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '5' + Ratelimit-Remaining: '595' + Ratelimit-Reset: '1541445266' + Ratelimit-Resettime: Tue, 05 Nov 2018 19:14:26 GMT + status_code: 404 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/commits/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/merge_requests + response: + url: https://gitlab.com/api/v4/projects/187725/repository/commits/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/merge_requests + content: '{"message":"404 Commit Not Found"}' + headers: + X-Http-Reason: Not Found + Date: Wed, 17 Jun 2020 18:28:50 GMT + Content-Type: application/json + Content-Length: '34' + Connection: keep-alive + Set-Cookie: __cfduid=dbfc65142d3a405159327b5a39321e61e1592418529; expires=Fri, + 17-Jul-20 18:28:49 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; + Secure + Cache-Control: no-cache + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: 4cKJfBOUbw6 + X-Runtime: '0.166783' + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1592418590' + Ratelimit-Resettime: Wed, 17 Jun 2020 18:29:50 GMT + Gitlab-Lb: fe-12-lb-gprd + Gitlab-Sv: localhost + Cf-Cache-Status: DYNAMIC + Cf-Request-Id: 03652389db0000f6371b2fd200000001 + Expect-Ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Server: cloudflare + Cf-Ray: 5a4ed522f864f637-GRU + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_ancestors_tree.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_ancestors_tree.yaml new file mode 100644 index 0000000000..d96f7462d2 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_ancestors_tree.yaml @@ -0,0 +1,49 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/commits?ref_name=c739768fcac68144a3a6d82305b9c4106934d31a + response: + url: https://gitlab.com/api/v4/projects/187725/repository/commits?ref_name=c739768fcac68144a3a6d82305b9c4106934d31a + content: '[{"id":"c739768fcac68144a3a6d82305b9c4106934d31a","short_id":"c739768f","created_at":"2014-08-20T21:52:44.000Z","parent_ids":["b33e12816cc3f386dae8add4968cedeff5155021"],"title":"shhhh + i''m batman!","message":"shhhh i''m batman!\n","author_name":"stevepeak","author_email":"steve@stevepeak.net","authored_date":"2014-08-20T21:52:44.000Z","committer_name":"stevepeak","committer_email":"steve@stevepeak.net","committed_date":"2014-08-20T21:52:44.000Z"},{"id":"b33e12816cc3f386dae8add4968cedeff5155021","short_id":"b33e1281","created_at":"2014-08-20T21:50:56.000Z","parent_ids":["743b04806ea677403aa2ff26c6bdeb85005de658"],"title":"added + nothing amazing","message":"added nothing amazing\n","author_name":"stevepeak","author_email":"steve@stevepeak.net","authored_date":"2014-08-20T21:50:56.000Z","committer_name":"stevepeak","committer_email":"steve@stevepeak.net","committed_date":"2014-08-20T21:50:56.000Z"},{"id":"743b04806ea677403aa2ff26c6bdeb85005de658","short_id":"743b0480","created_at":"2014-08-20T19:56:54.000Z","parent_ids":[],"title":"initial + commit","message":"initial commit\n","author_name":"stevepeak","author_email":"steve@stevepeak.net","authored_date":"2014-08-20T19:56:54.000Z","committer_name":"stevepeak","committer_email":"steve@stevepeak.net","committed_date":"2014-08-20T19:56:54.000Z"}]' + headers: + Server: nginx + Date: Fri, 20 Sep 2019 11:52:24 GMT + Content-Type: application/json + Content-Length: '1308' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"e460be25b22ed16c2939a1d590df3f3e" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: iDerNhAbD03 + X-Runtime: '0.046963' + X-Total: '3' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Referrer-Policy: strict-origin-when-cross-origin + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1568980404' + Ratelimit-Resettime: Fri, 20 Sep 2019 11:53:24 GMT + Gitlab-Lb: fe-09-lb-gprd + Gitlab-Sv: localhost + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_authenticated.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_authenticated.yaml new file mode 100644 index 0000000000..919c775bac --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_authenticated.yaml @@ -0,0 +1,35 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725 + response: + url: https://gitlab.com/api/v4/projects/187725 + content: '{"id":187725,"description":"","name":"ci-repo","name_with_namespace":"Codecov + / ci-repo","path":"ci-repo","path_with_namespace":"codecov/ci-repo","created_at":"2015-03-10T10:25:37.701Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:codecov/ci-repo.git","http_url_to_repo":"https://gitlab.com/codecov/ci-repo.git","web_url":"https://gitlab.com/codecov/ci-repo","readme_url":"https://gitlab.com/codecov/ci-repo/blob/main/README.md","avatar_url":null,"star_count":2,"forks_count":2,"last_activity_at":"2018-11-02T04:52:20.582Z","namespace":{"id":126816,"name":"codecov","path":"codecov","kind":"user","full_path":"codecov","parent_id":null},"_links":{"self":"https://gitlab.com/api/v4/projects/187725","issues":"https://gitlab.com/api/v4/projects/187725/issues","merge_requests":"https://gitlab.com/api/v4/projects/187725/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/187725/repository/branches","labels":"https://gitlab.com/api/v4/projects/187725/labels","events":"https://gitlab.com/api/v4/projects/187725/events","members":"https://gitlab.com/api/v4/projects/187725/members"},"archived":false,"visibility":"public","owner":{"id":109640,"name":"Codecov","username":"codecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/dcdb35375db567705dd7e74226fae67b?s=80\u0026d=identicon","web_url":"https://gitlab.com/codecov"},"resolve_outdated_diff_discussions":null,"container_registry_enabled":null,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":false,"shared_runners_enabled":false,"lfs_enabled":true,"creator_id":109640,"import_status":"finished","import_error":null,"open_issues_count":11,"runners_token":"77a755cac8b3463203b771118334c1","public_jobs":true,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":true,"only_allow_merge_if_all_discussions_are_resolved":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","permissions":{"project_access":{"access_level":40,"notification_level":3},"group_access":null},"approvals_before_merge":0,"mirror":false}' + headers: + Server: nginx + Date: Tue, 06 Nov 2018 05:04:40 GMT + Content-Type: application/json + Content-Length: '2170' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"d9b8864573cc404e2b24b2b84acd1fb3" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: c112c1f7-13d4-4057-b63d-412e6d0aefb0 + X-Runtime: '0.104756' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1541480740' + Ratelimit-Resettime: Wed, 06 Nov 2018 05:05:40 GMT + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_authenticated_user.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_authenticated_user.yaml new file mode 100644 index 0000000000..0b014c9f50 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_authenticated_user.yaml @@ -0,0 +1,83 @@ +interactions: + - request: + body: code=7c005cbb342626fffe4f24e5eedac28d9e8fa1c8592808dd294bfe0d39ea084d&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Flogin%2Fgl&client_id=testsb4ooxhwdveqlkv2gpoqq1n2tf54kzwv3q9xvsqfq45c33lx0jxp4j8a0och&client_secret=testux56mx9le9kn3cf0p8j8s1qpuurxk6wqebni1vm3slvbpuhmblgefp6yyoam + headers: + Accept: + - application/json + User-Agent: + - Default + method: POST + uri: https://gitlab.com/oauth/token + response: + url: https://gitlab.com/oauth/token + content: '{"access_token":"testhi9nk25akajgzhudabirpz3vjau7qe8i9mavz2d9i1i0cfwjp8ggkcqavglx","token_type":"Bearer","refresh_token":"testwnoeg1a4bjhoa65vzxdn8grh4asp0b6l4idtdazw7ps5h8xx77m8gbyty7gi","scope":"api","created_at":1599149427}' + headers: + X-Http-Reason: OK + Date: Thu, 03 Sep 2020 16:10:27 GMT + Content-Type: application/json; charset=utf-8 + Transfer-Encoding: chunked + Connection: keep-alive + Set-Cookie: __cfduid=d7a5cae25d6fcc8a717eb8ad280f9c6cf1599149427; expires=Sat, + 03-Oct-20 16:10:27 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; + Secure + Cache-Control: private, no-store + Etag: W/"e20e24aba72dd0c24b3aab58a8bbaf2d" + Pragma: no-cache + Referrer-Policy: strict-origin-when-cross-origin + X-Content-Type-Options: nosniff + X-Download-Options: noopen + X-Frame-Options: SAMEORIGIN + X-Permitted-Cross-Domain-Policies: none + X-Request-Id: jMcYhcbMPH9 + X-Runtime: '0.102941' + X-Xss-Protection: 1; mode=block + Strict-Transport-Security: max-age=31536000 + Gitlab-Lb: fe-12-lb-gprd + Gitlab-Sv: web-06-sv-gprd + Cf-Cache-Status: DYNAMIC + Cf-Request-Id: 04f654e1d90000db90f898b200000001 + Expect-Ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Server: cloudflare + Cf-Ray: 5cd0bdafc840db90-GIG + Content-Encoding: gzip + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/user + response: + url: https://gitlab.com/api/v4/user + content: '{"id":3124507,"name":"Thiago Ramos","username":"ThiagoCodecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/337420e188ca8138d4b8599d3a20ad47?s=80\u0026d=identicon","web_url":"https://gitlab.com/ThiagoCodecov","created_at":"2018-11-12T19:05:01.723Z","bio":"","bio_html":"","location":null,"public_email":"","skype":"","linkedin":"","twitter":"","website_url":"","organization":null,"job_title":"","work_information":null,"last_sign_in_at":"2020-05-28T21:29:21.563Z","confirmed_at":"2018-11-12T19:05:01.249Z","last_activity_on":"2020-09-03","email":"thiago@codecov.io","theme_id":1,"color_scheme_id":1,"projects_limit":100000,"current_sign_in_at":"2020-09-03T15:46:43.559Z","identities":[{"provider":"google_oauth2","extern_uid":"114705562456763720684","saml_provider_id":null}],"can_create_group":true,"can_create_project":true,"two_factor_enabled":false,"external":false,"private_profile":false,"shared_runners_minutes_limit":null,"extra_shared_runners_minutes_limit":null}' + headers: + X-Http-Reason: OK + Date: Thu, 03 Sep 2020 16:10:27 GMT + Content-Type: application/json + Transfer-Encoding: chunked + Connection: keep-alive + Set-Cookie: __cfduid=d7a5cae25d6fcc8a717eb8ad280f9c6cf1599149427; expires=Sat, + 03-Oct-20 16:10:27 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; + Secure + Vary: Accept-Encoding + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"17b57d5f5a096afd1f7222c273a0f640" + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: lRxq5XnVDz3 + X-Runtime: '0.024227' + Strict-Transport-Security: max-age=31536000 + Referrer-Policy: strict-origin-when-cross-origin + Content-Encoding: gzip + Gitlab-Lb: fe-04-lb-gprd + Gitlab-Sv: api-01-sv-gprd + Cf-Cache-Status: DYNAMIC + Cf-Request-Id: 04f654e31f0000db90f89aa200000001 + Expect-Ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Server: cloudflare + Cf-Ray: 5cd0bdb1cd1bdb90-GIG + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_best_effort_branches.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_best_effort_branches.yaml new file mode 100644 index 0000000000..d2e708d794 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_best_effort_branches.yaml @@ -0,0 +1,55 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/commits/c739768fcac68144a3a6d82305b9c4106934d31a/refs?type=branch&per_page=100 + response: + url: https://gitlab.com/api/v4/projects/187725/repository/commits/c739768fcac68144a3a6d82305b9c4106934d31a/refs?type=branch&per_page=100 + content: '[{"type":"branch","name":"main"},{"type":"branch","name":"other-branch"}]' + headers: + X-Http-Reason: OK + Date: Fri, 18 Sep 2020 05:49:38 GMT + Content-Type: application/json + Transfer-Encoding: chunked + Connection: keep-alive + Set-Cookie: __cfduid=d0bcead20ac32e40f612652c91451575e1600408177; expires=Sun, + 18-Oct-20 05:49:37 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; + Secure + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"67a38a4ca73fa48247cd38cf22c91eb7" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '100' + X-Prev-Page: '' + X-Request-Id: bDHArNkwCy4 + X-Runtime: '0.042247' + X-Total: '2' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Referrer-Policy: strict-origin-when-cross-origin + Ratelimit-Limit: '600' + Ratelimit-Observed: '2' + Ratelimit-Remaining: '598' + Ratelimit-Reset: '1600408238' + Ratelimit-Resettime: Fri, 18 Sep 2020 05:50:38 GMT + Gitlab-Lb: fe-01-lb-gprd + Gitlab-Sv: localhost + Cf-Cache-Status: DYNAMIC + Cf-Request-Id: 05415be4140000f3cb6ea2f200000001 + Expect-Ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Server: cloudflare + Cf-Ray: 5d48c8e68e02f3cb-GRU + Content-Encoding: gzip + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_branch.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_branch.yaml new file mode 100644 index 0000000000..57f7d7dae9 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_branch.yaml @@ -0,0 +1,71 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - gitlab.com + user-agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/branches/main + response: + content: '{"name":"main","commit":{"id":"0fc784af11c401449e56b24a174bae7b9af86c98","short_id":"0fc784af","created_at":"2023-06-27T11:13:34.000-04:00","parent_ids":["b64b9b4c25c6c088781ead5eae424f156ee98d8c"],"title":"3","message":"3\n","author_name":"joseph-sentry","author_email":"joseph.sawaya@sentry.io","authored_date":"2023-06-27T11:13:34.000-04:00","committer_name":"joseph-sentry","committer_email":"joseph.sawaya@sentry.io","committed_date":"2023-06-27T11:13:34.000-04:00","trailers":{},"web_url":"https://gitlab.com/joseph-sentry/example-python/-/commit/0fc784af11c401449e56b24a174bae7b9af86c98"},"merged":false,"protected":true,"developers_can_push":false,"developers_can_merge":false,"can_push":true,"default":true,"web_url":"https://gitlab.com/joseph-sentry/example-python/-/tree/main"}' + headers: + CF-Cache-Status: + - MISS + CF-RAY: + - 7fed9431ed625794-IAD + Cache-Control: + - max-age=0, private, must-revalidate + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json + Date: + - Wed, 30 Aug 2023 14:03:44 GMT + Etag: + - W/"dc6c2ab0a46ec8ec3206a880d101b54d" + GitLab-LB: + - fe-14-lb-gprd + GitLab-SV: + - api-gke-us-east1-d + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Referrer-Policy: + - strict-origin-when-cross-origin + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=XEJJs%2F7hPMLtjBnLXP45vRBG8HNG5%2Bx19W1IBYQyKvjEKErE0sLd5ElmZEa2shWFskj%2BeBHhodJclRSzODnCaOVaLXBkjzsKHi2p4lW%2FzcHNJHdp7ofTDgy9p2HvIz3jxa9amNJXm5Q%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - _cfuvid=6AVW_X5yf1WZ42EMYu64yCL7C5mLWpGVD9Zkbwqb4oI-1693404224628-0-604800000; + path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000 + Transfer-Encoding: + - chunked + Vary: + - Origin, Accept-Encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Gitlab-Meta: + - '{"correlation_id":"35ed075323ab9c477e7353e72d454624","version":"1"}' + X-Request-Id: + - 35ed075323ab9c477e7353e72d454624 + X-Runtime: + - '0.183047' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_branches.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_branches.yaml new file mode 100644 index 0000000000..b450e54821 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_branches.yaml @@ -0,0 +1,47 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/branches + response: + url: https://gitlab.com/api/v4/projects/187725/repository/branches + content: '[{"name":"main","commit":{"id":"0028015f7fa260f5fd68f78c0deffc15183d955e","short_id":"0028015f","title":"added + large file","created_at":"2014-10-19T14:32:33.000+00:00","parent_ids":null,"message":"added + large file","author_name":"stevepeak","author_email":"steve@stevepeak.net","authored_date":"2014-10-19T14:32:33.000+00:00","committer_name":"stevepeak","committer_email":"steve@stevepeak.net","committed_date":"2014-10-19T14:32:33.000+00:00"},"merged":false,"protected":false,"developers_can_push":false,"developers_can_merge":false,"can_push":true,"default":true},{"name":"other-branch","commit":{"id":"dd798926730aad14aadf72281204bdb85734fe67","short_id":"dd798926","title":"left + blank","created_at":"2014-09-04T16:13:44.000+00:00","parent_ids":null,"message":"left + blank","author_name":"stevepeak","author_email":"steve@stevepeak.net","authored_date":"2014-09-04T16:13:44.000+00:00","committer_name":"stevepeak","committer_email":"steve@stevepeak.net","committed_date":"2014-09-04T16:13:44.000+00:00"},"merged":false,"protected":false,"developers_can_push":false,"developers_can_merge":false,"can_push":true,"default":false}]' + headers: + Server: nginx + Date: Mon, 05 Nov 2018 19:13:10 GMT + Content-Type: application/json + Content-Length: '1132' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"1a04e16fb74995f2f09ab731f1b1b420" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: f0666f2f-7db1-410f-a8c1-2685abe5742c + X-Runtime: '0.066216' + X-Total: '2' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '3' + Ratelimit-Remaining: '597' + Ratelimit-Reset: '1541445250' + Ratelimit-Resettime: Tue, 05 Nov 2018 19:14:10 GMT + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_commit.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_commit.yaml new file mode 100644 index 0000000000..9e63cbb7b1 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_commit.yaml @@ -0,0 +1,77 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/commits/0028015f7fa260f5fd68f78c0deffc15183d955e + response: + url: https://gitlab.com/api/v4/projects/187725/repository/commits/0028015f7fa260f5fd68f78c0deffc15183d955e + content: '{"id":"0028015f7fa260f5fd68f78c0deffc15183d955e","short_id":"0028015f","title":"added + large file","created_at":"2014-10-19T14:32:33.000Z","parent_ids":["5716de23b27020419d1a40dd93b469c041a1eeef"],"message":"added + large file\n","author_name":"stevepeak","author_email":"steve@stevepeak.net","authored_date":"2014-10-19T14:32:33.000Z","committer_name":"stevepeak","committer_email":"steve@stevepeak.net","committed_date":"2014-10-19T14:32:33.000Z","stats":{"additions":816,"deletions":0,"total":816},"status":"success","last_pipeline":{"id":558130,"sha":"0028015f7fa260f5fd68f78c0deffc15183d955e","ref":null,"status":"success","web_url":"https://gitlab.com/codecov/ci-repo/pipelines/558130"},"project_id":187725}' + headers: + Server: nginx + Date: Fri, 02 Nov 2018 05:28:33 GMT + Content-Type: application/json + Content-Length: '710' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"deec56396689c6620f247cd619830d3e" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: b506b0d6-6d0f-4a23-af81-5e720dad39b7 + X-Runtime: '0.056148' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '3' + Ratelimit-Remaining: '597' + Ratelimit-Reset: '1541136573' + Ratelimit-Resettime: Sat, 02 Nov 2018 05:29:33 GMT + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/users?search=steve%40stevepeak.net + response: + url: https://gitlab.com/api/v4/users?search=steve%40stevepeak.net + content: '[{"id":109479,"name":"Steve Peak","username":"stevepeak","state":"active","avatar_url":"https://secure.gravatar.com/avatar/3712e9b9aee2ce5090aae58c2495cdee?s=80\u0026d=identicon","web_url":"https://gitlab.com/stevepeak"}]' + headers: + Server: nginx + Date: Fri, 02 Nov 2018 05:28:34 GMT + Content-Type: application/json + Content-Length: '221' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"5585a35955afb6a8cf2874f045e5569f" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: 5ddf1c33-df01-4cdb-ad4a-789e0d49f30e + X-Runtime: '0.068397' + X-Total: '1' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '4' + Ratelimit-Remaining: '596' + Ratelimit-Reset: '1541136574' + Ratelimit-Resettime: Sat, 02 Nov 2018 05:29:34 GMT + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_commit_diff.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_commit_diff.yaml new file mode 100644 index 0000000000..503b14c1c7 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_commit_diff.yaml @@ -0,0 +1,49 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/commits/c739768fcac68144a3a6d82305b9c4106934d31a/diff + response: + url: https://gitlab.com/api/v4/projects/187725/repository/commits/c739768fcac68144a3a6d82305b9c4106934d31a/diff + content: '[{"old_path":"README.md","new_path":"README.md","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"diff":"@@ + -1,5 +1,15 @@\n-### Example\n+### CI Testing\n \n-\u003e This repo is used + for CI Testing. Enjoy this gif as a reward!\n+\u003e This repo is used for + CI Testing\n+\n+\n+| [https://codecov.io/][1] | [@codecov][2] | [hello@codecov.io][3] + |\n+| ------------------------ | ------------- | --------------------- |\n+\n+-----\n+\n+\n+[1]: + https://codecov.io/\n+[2]: https://twitter.com/codecov\n+[3]: mailto:hello@codecov.io\n + \n-![i can do that](http://gph.is/17cvPc4)\n"}]' + headers: + Server: nginx + Date: Mon, 05 Nov 2018 20:01:29 GMT + Content-Type: application/json + Content-Length: '622' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"bec75dff6f5bc03146f5806e4ec76721" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: a1f9adc1-5e19-4670-b1c0-46db03fcb5e3 + X-Runtime: '0.036736' + X-Total: '1' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '2' + Ratelimit-Remaining: '598' + Ratelimit-Reset: '1541448149' + Ratelimit-Resettime: Tue, 05 Nov 2018 20:02:29 GMT + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_commit_diff_file_change.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_commit_diff_file_change.yaml new file mode 100644 index 0000000000..a0be0bdd57 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_commit_diff_file_change.yaml @@ -0,0 +1,423 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/commits/0028015f7fa260f5fd68f78c0deffc15183d955e/diff + response: + url: https://gitlab.com/api/v4/projects/187725/repository/commits/0028015f7fa260f5fd68f78c0deffc15183d955e/diff + content: '[{"old_path":"large.md","new_path":"large.md","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"diff":"@@ + -0,0 +1,816 @@\n+xxxxxx xx\n+xxxxxx xx\n+xxxxxx xxxx\n+xxxxxx xxxx\n+xxxxxx + xxxxxx\n+xxxxxx xxxxxxx\n+xxxxxx xxxxxxxx\n+xxxxxx xxxxxxxx\n+xxxxxx xxxxx + xx xxxxx\n+xxxx xxxx xxxxxx xxxxx\n+xxxx xxxxxxxx xxxxxx xxxxxx\n+xxxx xxxxxxxxxxxxxxx + xxxxxx xxxx\n+xxxx xxxxxxxxxxxxxxxx xxxxxx xxxxxxxxxx\n+\n+xxxx xxxxxxxxxxxxxxxxx + xxxxxx xxxxxxxxxxxxx\n+\n+\n+xxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+\n+x xxxxxx\n+xxxxxxxxxxxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxx x xxxxxxxxxxxxxxxxxxxxxxxxx\n+\n+x + xxxxxxxxx xxxxxx\n+xxxxxxxxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+\n+x + xxxxxxxxx xxxx\n+xxxxx xxxxx\n+ xxxxxxxxxxxxxxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ x + xxxxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxx x + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxx + x xxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxx x xxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxx + x xxxxxxxxxxxxxxxx xxxxxxxxx \n+ xxxxxxxxxxxxx xxxxxxxxx + \n+ xxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+\n+x + xxxxxxxxx xxxx\n+xxxxxxxxxxxxx x xxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+xxxxxxxxxxxxxxxxxxxxxxx\n+\n+xxxxx + xxxxxxxxxxxxxxxxxx\n+ xxxx\n+\n+xxx xxxxxxxx\n+ xxxxxx xxxxxxxxx xx + x xxxx xxxx\n+\n+xxx xxxxxxxxxxxxxxxx xxxxxxxxx xxxxxxxxxxxx xxx xxxxxxx\n+ xxxxxxxxxx + xxxxxx xx xxxxxxx xxx xxx xxxxx xxx xxxxxxxx xxx xxxx xx xxxxxx\n+ xxx\n+ xxxx + x x\n+ xxxxx xxxxx\n+ xxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + x xxxxxxxxxx xxxxxxxxxxxx xxxxxxxxxxxxx\n+ xx xxx xxxxxx \n+ xxxxx\n+ xxxx + x xxxx x x\n+ xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxx xxxx xx xxxxxxxx\n+ xxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxx xxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxx\n+\n+xxx xxxxxxxxxxxxxx xxxxxxxx xxxxxxxxxxxx + xxxxxxxxxxxxx\n+ xxxxxxxx xxx xxxxx xxx xxxxxxxxxxxx xxxxxxx\n+ xxx\n+ xxxxxx + xxxx xx xxxxxxxxxxxx xxxxxxxxxx\n+ xx xxxxxxxxxxxxxxx xx xxxxxx xxx xxxxxxxxxxxxxxxxxxxxx + x xxxxxxx xx xxxxx\n+ xx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxx + xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxx\n+ xxxxx\n+ xx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxx x xxxxxxxxxxxxxx + xxxxxxxxxxxx xxxxxxxxx xxxxxxxxxxxxx xxxx xxxxxx xxxxx xxxxxxxxxx xxx xxxxxxxxxxxxxxxx + xxxxx xxxx xxxxxxxx\n+ xxxxxx xxxxx xxxxx xxx xxxxxx\n+\n+ x xxxxxxxxxxxxxx\n+ x + xxxx xxx xxxxx\n+ x xxxxxxxxxxxxxx\n+ xx xxxx xx xxxxxxxxxxx\n+ x + xxxx xxxxx\n+ xxxxxxxxxxxxxxxx xxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxx xxx + xxxxxx\n+ xx xxxxxxxxxxxxxxxxxxx\n+ x xxx xxxxx\n+ xxx + xxx xx xxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxx + xxxx xxxxxxxxxx xxxxxxxxxxxxxxxxxxxx xxx xxxxxx\n+\n+ x xxxxxxxxxxxxx\n+ x + xxx xxx xxxxx\n+ x xxxxxxxxxxxxx\n+ xx xxxxxxx\n+ xxxxx x xxxxxxxx\n+ xxxxx\n+ xxxxx + x xxxxxxxxxxxxxxxxxx xxxxxxxx\n+ xxxx xxxxx x\n+ xxxxx + xxxxxxxxxxx xxx xx xxxxxxxxxx xxxxxxxxxxx xxxxxxxxx xx xxxxxxxxxx xx xxxxx\n+ xxx + xxxxxxxxx xx xxxxxxx xxxxxxxxx \n+ xxxx + xxxxxx x\n+ xxxxx xxxxxxxxxx + xx xx \n+ xxx xxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxx x xxxxxxxxxxxxxxxxxxxxxxx xx xxxxx\n+\n+ x xxxxxxxxx\n+ x + xxxx xxxx\n+ x xxxxxxxxx\n+ xxxxxx x xxxxxxx xx xxxxxxxxxxxxxxx xxxx + xxxxxxxx\n+ xxxx x xxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxx + xxx xxxx xx xxxxxx\n+ xx xxxxxx xxx xxxxxxxxx x xx x xxxxxxx xx xxxxx\n+ x + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxx\n+ xxxxxxxxxxxxx xxxxx\n+ xxxxxxxxx\n+ xxxxxxxx\n+ xxxxx\n+ xxxxxxxxxxx + xxxxx\n+\n+ x xxxxxxxxxxxxx\n+ x xxxx xxxxxxxx\n+ x xxxxxxxxxxxxx\n+ xxxxxxx + x xxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxx + x xxxxxxxxxxx xx xxxxxxxxxxxxxxxx xxxx xxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxx xxxxx\n+ xxxx x xxxxxxxxxxxxxxx\n+ xx xxxx xx xxxxxxxxx\n+ xxxx\n+ xxxxxxxxxxxx + x xxxxxxxxxxxxxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxx x xx xxx xxxx\n+ xxxxxxx\n+ xxxx\n+ xxxx + xxxx xx xxxxxxxxxxx xxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxx x xx\n+ xxxxxxxxxxxxxxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxx\n+\n+ x + xxxxxxxxxxxx\n+ x xxxxxxx xxxx\n+ x xxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxx\n+\n+\n+xxx xxxxxxxxxxxxxxxx\n+ xxxx\n+ xxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxx xxxxxxxxx\n+ xxxxxx xxxxxxx\n+ xxxx\n+ xxxxxxx\n+ xx + xxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+\n+xxx + xxxxxxxxxxxxxxxxx\n+ xxxx\n+ xxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxx + xxxxxxxxx\n+ xxxxxx xxxxxxx\n+ xxxx\n+ xxxxxxx\n+ xx xxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+\n+\n+xxxxx + xxxxxxxxxxxxxxxx\n+ xxx xxxxxxxxxxxxxx xxxxxxxx xxxxxxx xxxxxx xxxxxxxxxxxxx + xxxxxxxxxxxxxxx xxxxxxxx xxxxxxxxxxxx\n+ xxx\n+ xxxxxxxx xxxxx + xxxx xxxx xx xxxx xxxxxx xxxx xx xxxx xxx xxxxxxxxx\n+ xxxxxxx xxxxx + xxx xxxx xx xxxxxxx\n+ xxxxxx xxxxxxx xxxxxx xxxxxxxxxxx\n+ xxxxxxx + xxxxxxx xxxxxxxx xxxxx xxxxxx xx xxxx xxxxx xxxxxxx xxx xxxxxxxxxxxxx\n+ xxxx + xxxxxxxx xxx xxxxxxxxxx xxxx xxx xxx xxxxxxx\n+ xxx\n+ xxx x + xxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxx x xx\n+ xxxxxxxxxxx x xxxxx\n+ xxxxxxx + x xxxxx\n+ xxxxxxxxxxxxxxx x x\n+ xxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxx + xxxxxx xxxxxxxxx\n+ x\n+ xxxxxxxxxx x xx\n+\n+ x xxxxxxxx\n+ x + xxxxxxxx\n+ x xxxxxxxx\n+ xxxxxxxxx x xxxxxxxxxxxxxxxxxxxxx + xxxxxxxxx xxxxxxxxxx xx xxxxxx xxxxxxx xxxxxxxxx xxxxxxxxxx xxxxxxxxxxxxxx + xxxxxxxxxxx xxxxxxxxxxxxxxx xxxxxxxxxxx xxxx\n+ xxxx + xxxxx x\n+ xxxxx xxxx xxxxxx x xxxxx xxxxxxxxx\n+ xxxxx + xxxxxxxxxxx\n+ xxxxx xxxxxx xxxxxxx\n+ xxxxxx + xxxxxxxxxx xxxxx xxx xxxxxx\n+\n+ x xxxxxx\n+ x xxxxxx\n+ x + xxxxxx\n+ xx xxxxxxxxxxxxxxxxxx xx xxxxxxxxx\n+ x xxxxxxx + xxxxxxxxx xxx xxxxxx\n+ xxxx\n+\n+ xxxx xxxxxxxxxxxxxxxxxx + xx xxxxxxxxxxxx\n+ x xxxxx xx xxx xxxxx\n+ xxxx x xxxxxxxxxxxxxxx + xxxxxxxx xxxxxxxxxxxxxxxxxx\n+ xx xxxxxxxxxxxxx xx xxxxxxxxx\n+ xx + xxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx + xx\n+ xxxxxxxxxxxxxxxxxxxxxxx xxxxx xxx xxxxxxxxxxxxxxxxx + xxxxx xxxxxxxxxxx xxxxxxxxxxxxxxxxx\n+ xxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxx xx\n+ xxxxxxxxxxxxxxxxxxxxxxx xxxxx + xxx xxxxxxxxxxxxxxxxx xxxxx xxxxxxxxxxx xxxxxxxxxxxxxxxxx\n+\n+ xxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xx xxxxxxxxxxxxxxxxxxxx xxxx xx\n+ xxxxxxxxxxxxxxxxxxxxxxx + x xxxxx\n+ xxxxxxxxxxxxxxxx x xxxxxxxxxxxxxxxxxxx\n+\n+ x + xxxxxx\n+ x xxxxxx\n+ x xxxxxx\n+ xx xxxxxxxx + xx xxxxxxxxxxxxxxxxxx xx xxxxx\n+ x xxx xxxx\n+ xxxx + x xxxxxxxxxxxxxxx xxxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxxx\n+ xxxxx + x xxxxx\n+ xxxxxxxx x xxxxx\n+ xxx xx xx xxxxxxxxxxxxx\n+ xx + xxxxxxxxxx xx xxxxxxxxxx\n+ xxxxxxxxxxxxxxxx\n+ xxxx + xxxxxxxxxx xx xxxxxxxxxxxxxxx xxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxx\n+ xxxxxxxxx + xxxx x xxxxxxxxxxxxxxxxxxxx\n+ xxxxx x xxxxxxxxxxxxxxx\n+ xxxx\n+ xxxxxxxx + x xxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxx\n+ xxxxxxxx + x xx\n+ xx xxxxxxxxx xx xxxxx\n+ x + xxxxxxx xx xxxxxxxxxx xx xxxx xx xxx\n+ xxxxxxxxxxxxxxxxxxxxxxx + xxxxx xxx xxxxxxxxxxxxxxxxxxx xxxxx xxxxxxxxxxxx xxxxxxxxxxxxxxxxx\n+ xxxxxx\n+ xx + xxxxxxxxxxxxxxxxxxxx xx xxxxx\n+ xxxxxxxx x xxxx\n+ xxxxx\n+ xxxx + xxxxxxxxxx xx xxxxxxxxxxxxx xxx xxxxxxxx xx xxxxxxxxx\n+ xxxxxxxx + x xxxx\n+ xxxxx\n+ xxxx + xxxxxxxxxx xx xxxxxxxxxxxx xxx xxxxxxxxx xx xxxxx\n+ xxxxxxxx + x xxxx\n+ xxxxx\n+ xxxx + x xx xxxxx xx xxxx xxx xxxxxxxxx xx xxxxxx xxxxxxxxx xx xxxxxxxxx xx xxxxxx\n+ xxxxxxxx + x xxxx\n+ xxxxx\n+ xxxx xxxxxxxxxx + xx xxxxxxxxxxx xxxxxxxxxxxxxxxx\n+ xxxxxxxxx xxxx x + xxxxxxxxxxxxxxxxxxxx\n+ xx xxxxxxxxx xx xxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxx + xxxxx xxx xxxxxxxxxxxxxxxxxxx xxxxx xxxxxxxxxxxx xxxxxxxxxxxxxxxxx\n+ xxxxxx\n+ xxxx + xxxxxxxxxxx xx xxxxx\n+ xxxxxxxx x xxxx\n+ xxxxx\n+\n+ xx + xxxxx xx xxxxx xxx xxxxx xx xxxxxx\n+ xxxxxxxx x xxxx\n+\n+ xxxxxxxxxxxxxxxxxxxxxxx + xxxxx xxx xxxxxxxxxxx xxxxx xxxxxxxxxxxx xxxxxxxxxxx xx xxxxxxxx xxxx xxxxxxxxxxxx + xxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxx + x xxxxxxxxxx xx xxxxxxxx xxxx xxxxxxxxxx\n+ xx xxxxxxxxx\n+ xxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxx xxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxx\n+\n+ x + xxxxxxx\n+ x xxxxxxx\n+ x xxxxxxx\n+ xx xxxxxxx + xxx xxxxxxxxxxxxxxxxxxxxx xx xxxxxxxxxxx\n+ xxxx\n+ xxxxxxxxxxxx\n+ xxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxx xx\n+ xxxxxxxxxxxxxxxxxxxxxxx xxxxx xxx + xxxxxxxxxxxxxxxx xxxxx xxxxxxxxxxxx xxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxx xxxxxxxxxxxxx\n+\n+ xxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxx + xxxxx xxx xxxxxxxxxxxxxxxxxxxxx xxxxx xxxxx xxxxxxxxxxxx xxxxxxxxxxxxxxxxxx + xxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxx xx\n+ xxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxx xxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxx\n+\n+ xxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxx x xxxx\n+\n+ xxxxxxxxx\n+ xxx + xxxxxxxxx\n+ xx xxxxxxxxx\n+ xxxxxx xxxxxxxx\n+\n+ xx + xxxxxxxxxxxxxxx xx xxxxxx xxx xxxxxxxxxxxxxxxxxxxxx x xxxxxxx xx xxxxx\n+ xxxxxxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxx + xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxx\n+ xxxxx\n+ xxxxxxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxx xxxxxxxx\n+\n+ xxxxxxxxx\n+ xxx + xxxxxxxxxxxx\n+ xx xxx xxxxxxxxxxxx\n+ xxxxxxxxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxx + xxxxxxxxxxx\n+\n+ xxx xxxxxxxxx xxxxxxx xxxxxx xxxxxxxxxx\n+ xxxxxxxxxx + xxx xxxxxxx xxxxxx xxx xxxxxxxx\n+ xxx\n+ xxxx x xxxxxxxxxxxxxxxxxxxxxxxxx + xx xxxxxx xx xxxxxx xxxx xxxx\n+ xxxx x x\n+ xxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + x xxxxxxxxxxxxxxxxxxxxxxx xxxxxxx\n+ xxxxx xxxx x xx\n+ xxxxxx + x xxxxxxxxxxxxxxxxx xxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxx\n+ xx + xxxxxxxxxxxxxxxxxx xx xxx xxx xxxxxxxxxxxxxxxxxxxxxxxx xx xxxx xxxxxxxxxx + xx xxxxxxxx\n+ xxxxx xxxxxx\n+\n+ xxxx xxxxxxxxxxxxxxxxxx + x xxxx\n+ xxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxx xxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxx + x xxxx x x\n+ xxxxxxxxxxxxx x xxxx xx xxxxxx xxxxx xxxxx xx\n+ xxxxx\n+ xxxxx \n+\n+ xxxxxxxx + x xxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxx\n+ xx xxxxxxxxxxxxxxxxxx + xx xxxx\n+ xxxxx xxxxxxxxxxxxxxxxxx xxx xxx xxxxxxxx xxx xxxxx + xxx xxxxxxxx xxxxxxxxxxxxxx\n+ xxxxxx xxxxxxxx\n+\n+ xxx xxxxxxxxxxxx\n+ x + xxx xxxxxx xxxxxx xx xxxxxxx xxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxx + x xxxxxxxxxxxxxxx xxxxxx xxxxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxxx\n+\n+ x + xxx xxx\n+ xxxxxxxxxxxxxxxx x xxxxxxxxxxxxxxxxxxxxxxxxx\n+\n+ x + xxx xxxx\n+ xxxxxxxxx x xxxxxxxxxxxxxxx xxxxxx xxxxxxxx xxxxxxxxxxxxxxxxx\n+\n+ x + xxxx xxxxxxx xxxx xxx xxxx xxxxxxx xx xxxxx\n+ xxxxxxxx xxxxxxxxx x + xxxxxx xxxxx\n+ xxx xx xx xxxxxxxxxxxxxxxxxx\n+ xxxx x xxxxxxxxxxxxxxxxxx\n+ xx + xxxx xx xxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxx + xxxx xx xxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxx + xxxx xx xxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxx + xxxx xx xxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxx x xxxx\n+ xxxx + xxxx xx xxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxx\n+ xxxx + xxxx xx xxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxx\n+ xxxx xxxx xx xxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxx\n+ xxxx xxxx xx xxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxx + x xxxx\n+ xxxx xxxx xx xxxxxxxxxxxxx\n+ xxxxxxxxx + x xxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxx xxxx + xx xxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxx\n+ xxxx + xxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxx\n+\n+\n+ xx + xxxxxxx xxx xxx xxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxx\n+\n+ x + xxxxxxxxxxx\n+ x xxxxxx xxxx\n+ x xxxxxxxxxxx\n+ xxxx + x xxxxxxxxxxxxxxxx xxxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxx\n+\n+ x + xxxxxxxxxxxxx\n+ x xxxxxx xxxxxx\n+ x xxxxxxxxxxxxx\n+ xxxxxx + x xxxxxxxxxxxxxxxx xxxxxx xxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxx xxxxxxx xxxxxxxx + xxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+\n+ x + xxxxxxxxxxxxx\n+ x xxxxxx xxxxxx\n+ x xxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxx + xxxxxx xxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxx\n+\n+ x + xxxxxxxxxxxxxxxxxxx\n+ x xxxxxx xxxx xxxxxxx\n+ x xxxxxxxxxxxxxxxxxxx\n+ xxxxxxx + x xxxxxxxxxxxxxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxxxxx xxxxxxx xxxxxxxx xxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxx\n+ xxxxxxxxxxx + xxxxx xxxxxxxxx xx xxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxx xxxx xxxx xx xxxxx xx xxxxxxxxxx xxx xxx xxxx xxxxxxx xx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + x xxxxxxxxxxx\n+\n+ xxxxxxx x xxxxxxxxxxxxxxxxxxxxxxxx xx xxx xxxxx\n+ xxxxxxxxxxxxxxxxxxxxx + x xxxxxxxxxxx\n+\n+ xxx xxxxxxxxx xxxxxxxxxxx xxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxx\n+ xx xxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxx\n+\n+ xxx xxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxx xxxxxxxxx\n+ x + xxx xxxxxxx xxxxxxxx\n+ xxxxxxxx x xxxxxxxxxxxxxxx xxxxxxxxxxx xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxx\n+ x xxxxxx\n+ xxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxx + x xxxxxxxxxxxxxxxx\n+ x xxxxxx xxxxx\n+ xx xxxxxxx xx xxxxx\n+ xxxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + x xxxxxxxxx\n+ xxxxxx x xxxxxxxxxxxxxx\n+\n+ xxxx xxxxxxx + xx xxxxxxxxxx\n+ xxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + x xxxxxxxxx\n+ xxxxxx x xxxxxxxxxxxxx\n+\n+ xxxx xxxxxxx + xx xxxxxx\n+ xxxxx x xxx xxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx x + xxxxxxxxx\n+ xxxxxx x xxxxxxxxx\n+ \n+ x xxxx xxxxx + xxxxxxx xxxxxx xxxxx\n+ xxxxx x xxxxx\n+ xxx xx xxxx xx xxxxxxxxxxxxxxxxx\n+ xx + xxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxx xxxxxx\n+ xxxxx + x xxxx\n+ xxxxx\n+\n+ x xxx xx xxxxx xx xxxxxxxx\n+ xx + xxx xxxxxx\n+ xxxxxxxx x xxx xxx x xxxxxxxxxx xxxxxx\n+\n+ x + xxxxxx xxxx\n+ xxxx x xxxxxxxxxxxxxxxx xxxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxx + xxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxx\n+\n+ xxx xxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxx\n+ xxxxxxxxx xxxx x xxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxx + x xxxxxxxxxxxxxxxx\n+\n+ x xxxxxxxx xxxxxx\n+ xx x x x x xxxxxxxx + xxxxx\n+ x x x x x xxxxx\n+ xx x xxxx x xxxxxxxxxx xx\n+ xxxxx + x xxxxx\n+\n+ xxx xx xxxx xx xxxxxxxxxxxxxxxxx\n+ xx xxx + xxxxx xxx xxxxxxxxxxxx xx xxxxx\n+ xx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xx + x x\n+ xxxx xxx xxxxx xxx xxxxxxxxxxx xx xxxxx\n+ x + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxx xxxxxxxxxx xx xxxxx\n+ xxxxx\n+ xxxx + xxxxxxxxxxxxxxxxxxxx xx xxxxx\n+ xxxxx x xxxx\n+ xxxx + xxxxxxxxxxxxxxxxxxxxx xx xxxxx\n+ xxxxx x xxxxx\n+\n+ x + xxxxxxx xxxxxxxx xxxxx\n+ x x xxxxxxxxxxxxx xxx xx\n+\n+ xxxxxx + x xxxxxxxxxxxxx x xxxxxxxxxxx xxxx\n+ xxxxxxxxxxxxxxxx xxxxxxx\n+\n+ x + xxxxxx xxxx\n+ xxxx x xxxxxxxxxxxxxxxx xxxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxx + xxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxx\n+\n+ xxx xxxxxxxxxxxxx xxxxxxxx + xxxx xxxxxxxx\n+ xxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ x xxxxxxxx + xxxxx\n+ xxxxxx xxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+\n+ xxx + xx xxxx xx xxxxxxxxxxxxxxxxx\n+ xx xxxxxxxxxxxxxxxxxxxxx\n+ x + xxxxxxx xxxxx\n+ xxxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xx + xxxxxxx\n+ x xxxx xxxxx\n+ xxxxxxxx + x xxxxx x xxx\n+ xxxxxxxxxxxxxxxxx xxxx xxx x xxxxxxx xxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxx + xxxx xxx x xxxxxxx xxxxxxxx\n+ xxxxxx xxxxxxxxxxxxxxxx + x xxxx\n+ xxxxx\n+ x xxxx\n+ xxxxxxxxxxxxxxxxx + xxxx xxx x xxxxxxx xxxxxxxx\n+ xxxxxx xxxxxxxxxxxxxxxx + x xxxx\n+\n+ x xx xxx xxxxxx xxxxxx xxx xx xx xxxxxx\n+ xxxxxxxxxxxxxxxxxxx + xxx x xxxxx xx xxx xxxx xxx xxxx xxxxxxxx\n+ xxxxxx xxxxxxxxxxxxxxxx + x xxxx\n+\n+ xxx xxxxxxxxxxxxx xxxxxxxx xxxxxxxx xxxx xxxxxxxx\n+ xxxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ x xxxxxxxx xxxxx\n+ xxxxxx + xxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ \n+ xxxxxxx x xxxxx\n+ xxx + xx xxxx xx xxxxxxxxxxxxxxxxx\n+ xx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxx + x xxxx\n+ xxxx xxxxxxx xxx xxx xxxxxxxxxxxxxxxxxxxxxxx\n+ x + xx xxx xxxxx xx xxxxx xxxx xxxxxxx\n+ x xxx xxxx xxx xxxxx + xxx xxxxxxx\n+ xxxxxxxxxxxxxxx xxxxxxx x xxxxxxx xxxxx\n+ xxxxxxxxxxxxxxxxx + xxxxxx xxx x xxxxxxx xxxxxx xxxxxxxx\n+ xxxxxx xxxxxxxxxxxxxxxx + x xxxx\n+\n+ xxxx xxxxxxx xxx xxxxxxxxxxx xx xxxxx\n+ x + xxxxxxx xxxxx\n+ xxxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xx + xxxxxxx\n+ x xxxx xxxxx\n+ xxxxxxxx + x xxxxxxx x xxxxxxx xxxx\n+ xxxxxxxxxxxxxxxxx xxxxxx xxx + x xxxxxxx xxxxxx xxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxx + xxxxxx xxx x xxxxxxx xxxxxx xxxxxxxx\n+ xxxxxx xxxxxxxxxxxxxxxx + x xxxx\n+ xxxxx\n+ x xxxx\n+ xxxxxxxxxxxxxxxxx + xxxxxx xxx x xxxxxxx xxxxxx xxxxxxxx\n+ xxxxxx xxxxxxxxxxxxxxxx + x xxxx\n+\n+ xx xxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxx x xxxxxxx + xxxxx\n+ xxxxxxxxxxxxxxxxxxx xxx x xxxxxxx xxxxxx xxxxxxxx\n+ xxxxx\n+ x + xx xxx xxxxxx xxxxxx xxx xx xx xxxxxx\n+ xxxxxxxxxxxxxxxxxx x xxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxx + x xxxxxxx xxxxx\n+ xxxxxxxxxxxxxxxxxxx xxx x xxxxxxx xxxxxx xxxxxxxx\n+ xxxxxx + xxxxxxxxxxxxxxxx x xxxx\n+ \n+ xxx xxxxxxxxxxxxxxxxxx xxxxxxxx xxxxxx\n+ xxx\n+ xx + xxxxxxxxx xxx xxx xxxxxxxxxxx\n+ xxxxxxxxx xxxxxxxxxxxxxxx\n+ xx + xxxx xxxxxx xxxxxxxx\n+ xxxxxxxx xxx xxxxxx x xxxxxxx xxxxxx xxxxxx\n+ xxx\n+ xxx + xxxxxx xx xxxxxxxx\n+ xx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ x + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xx + xxxxxxxxxxxx xx xxxxxx xx xxxxxxxxxxxxxxxxx xx xxxxxxx\n+ x + xxxxxxxxxxxxx xxxx xxxxx\n+ xxxxx\n+ xxxx + xxxxxxxxxx xx xxxxxxx\n+ xxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxx + x xxxxxxxxxxxxxxx xxxxxxxxxxx xxxxx xxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xx xxxxxxxxxxxxxxxxx + xx xxxx xx xxxxxxxxxxxx xx xxxxxxxxxxxx\n+ xxxxx\n+ x + xxxxxx xxxx\n+ xxxxxxxxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxx x xxxxxxxxxxxxxxxx + xxxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxx xxxxxxxxxxxxxx xxxxxxxxxxx\n+\n+ xxxxx\n+ x + xxx xxxxxxxxxxxxxxx\n+ xxxx x xxxxxxxxxxxxxxxxxxxx xxx + xxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxx\n+ xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ x + xxxxxxx xxxxxx xx xxxxxxxx xxx\n+ xxxx x xxxxxxxxxxxxxxxxxxxx + xxxxxxxxx xxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxx\n+\n+ xxxxxx + xxxx\n+\n+ xxx xxxxxxxxxxxxxxx xxxxxx\n+ xx xxxxxxxxxxxxxxxxxxx + xx xxxxx\n+ xxxxxx xxxx\n+ xxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+\n+ xxx xxxxxxxxxxxxxxxxx + xxxxxxxxx\n+ x xxx xxxxxxx xxxxxxxx\n+ xxxxxxxx x xxxxxxxxxxxxxxx + xxxxxxxxxxx xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxx\n+ x xxxxxx\n+ xxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxx xxxxxxxxx xxxx\n+\n+ xxx + xxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxx\n+ xxxxxxxxx xxxx x xxxxxxxxxxxxxxxxxxxxxxxxx\n+ x + xxx xxxxxxxx\n+ xxxxx x xxxxxxxxxxxxxxx\n+ xxxxxxxx x xxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxx\n+\n+ xx xxxxxxxx xx xxxxxxx\n+ xxxx + x xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxx xxxxxxxxxxx x xxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xx xxxxxxxxxxxxxxxxxxxx xxxx xxxx\n+\n+ xx xxxxxxxx xx xxxxxxxxx\n+ xxxx + x xxxxxxxxxxxxxxxxxx xxxxxxxxxx xxxx xxxxxxx xxxxxxxxx\n+ xxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxx\n+ \n+ xxxx + xxxxxxxx xx xxxxx\n+ xxxx x xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxx + xxxxx xxx xxxxxxx xxxxxxxxx\n+ xx xxxxxxxxxxxxxxx xxx xx xxxxx\n+ xx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxx xx xxxxx\n+ xxxx + x xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxx xxx xxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxx + x xxxxxxxxxxxxxxxx xxxxxx xxx xxxx xxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx\n+\n+ xxxx + xxxxxxxx xx xxxxxxx\n+ x xxxx xxxx xxxxxxx\n+ xxxx\n+ \n+ xxxx + xxxxxxxx xx xxxxxx\n+ xxxx x xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxx + xxxxx xxx xxxxxxx xxxxxxxxx\n+ xxxx x xxxxxxxxxxxxxxxxxxxx\n+\n+ x + xxxxxxxxx xxxx xxxxxxxx\n+ xxxxx\n+ xxxx x xxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxx xxxxx xxx xxxxxxx xxxxxxxxx\n+\n+ x xxxxxx xxxx\n+ xxxx + x xxxxxxxxxxxxxxxx xxxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxx + xxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxx\n+\n+ xxx xxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxx\n+ xxxxxxxxx xxxx x xxxxxxxxxxxxxxxxxxxxxxxxx\n+ x + xxx xxxxxxxx\n+ xxxx x xxxxxxxxxxxxxxxxxx xxxxxxx xxxxxxx xxxxxxxxxxx + x xxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xx xxxxxxxxxxxxxxxxxxxx xxxx xxxx\n+ xxxxx + x xxxxxxxxxxxxxxx\n+ xx xxxxxxxx xx xxxxxxxxxxxxxxxxx\n+ xxxx + x xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxx xxxxxxxxxxx xxxx xxxxxxx xxxxxxxxx\n+ xxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxx\n+ xxxxx\n+ xx + xxxxx xx xxxxxxxxxxxxxxxxx\n+ xxxx x xxxxxxxxxxxxxxxxxxxx\n+ xxxx + x xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxx xxxxxxxxxxx xxxxx xxx xxxxxxx xxxxxxxxx\n+ x + xxxxxx xxxx\n+ xxxx x xxxxxxxxxxxxxxxx xxxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxx + xxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxx\n+\n+ xxx xxxxxxxxxxxxxxxxxxxxxxxx\n+ x + xxxxx xx xx xxxxxx\n+ xx xxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxx\n+\n+ xxxxxxx + x xx\n+ xxx xx xx xxxxxxxxxxxxxxxxxx\n+ xx xxxxxxxxxx xx + xxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxx x xxxx\n+ xxxxxx\n+ xxxx + xxxxxxxxxx xx xxxxxx xxx xxxxxx xxx xx xxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+\n+ xx + xxx xxxxxxxx\n+ x xxxxx xxx xxxxxxxx xxx xxxxxxx\n+ xxxxxx\n+\n+ x + xxxxxx xx\n+ xxxxxxxxxx x xxxxxxxxxxxxxxxxxxxx xxxxxxx x xxxxxx xxxxxxx + xx x xxx xxxxxxxxxxxxxxxxxxxxx xx xxxxxxxxxxxxxxx xxxx xxxxx\n+ x + xxxxxxxx xx xxxxxxxxxxxxxx xxxx xxxx\n+ x xxxx + xxxxxxxxxxx xxxxxxxxxxxxxx x xxxxxx xxxxxxxxxxxxxx xxx xxxxxxxxxx xx xxxxxxx + xxxxxx xxxxxxxxxx xxxxx xxxxxxxxxxxxx xxxxxxxx xxxxxxxxxxxxxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxxxx + xxxxx xxxx xxxxx xxxxxxxx xx xxxxxxxxxxxxx xxxxx\n+\n+ x xxx xx xxxx + xx\n+ xxxxxxxxxxxxxxxxxxxxxxx x xxxx\n+ x xxxxxx xxxx\n+ xxxx + x xxxxxxxxxxxxxxxx xxxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxx + xxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxx\n+\n+ xxx xxxxxxxxxxxxxxxxxxxxxxx\n+ xxxx + x xxxxxxxxxxxxxxxx xxxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxx xxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxx\n+\n+ xxx xxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxx\n+ xxxxxxxxx xxxx x xxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxx + x xxxxxxxxxxxxxxxx xxxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxx xxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxx\n+\n+ xxx xxxxxxxxxxxxxxxxxxxx + xxxxxxxxx\n+ xxxxxxxxx xxxx x xxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxx + x xxxxxxxxxxxxxxxx\n+ xxx xx xxxx xx xxxxxxxxxxxxxxxx\n+ xxx + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xx xxxx\n+ xxxxxx + xxxxx x xxxxxxxxxxxxxxxxxxx\n+ xxxxxxx x xxxxxxx xxxxxxxxxx + x xxxxxx\n+ xx xxxxxxxxx xxxxxx xx xxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxx\n+ xx xxxxxxxxx xx xxxxxx xx xxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxx + xx xxxxxxx\n+ xx xxxxxxx xx xxxxxx xx xxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxx + xx xxxxxxx\n+ xxxxxxxxxxxxxxxx xx xxxxxxxxxxxxxxxx\n+ xxxxx\n+\n+ xxxx + x xxxxxxxxxxxxxxxx xxxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxx + xxxxxxxxxxxxxx xxxxxxxxxxxxxxxx\n+\n+ xxx xxxxxxxxxxxxxxxxxxxx xxxxxxxxx\n+ xxxxxxxxx + xxxx x xxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxx x xxxxxxxxxxxxxxxx\n+\n+ xxx + xx xxxx xx xxxxxxxxxxxxxxxx\n+ xxxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xx + xxxxxxx\n+ xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxx\n+\n+ x xxxxxxxxx\n+ x xxxx xxxx\n+ x xxxxxxxxx\n+ xxxx + x xxxxxxxxxxxxxxxx xxxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxx + xxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxx\n+\n+ xxx xxxxxxxxxxxxxxxxxxxx xxxxxxxxx\n+ xxxxxxxxx + xxxx x xxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxx x xxxxxxxxxxxxxxxx\n+ xxxxx + xxxxxxxxxxx xxxxx xxxx x xxx xxxxx xxxxx xxxx\n+ xxxxx xxxx x xxx xxxx\n+\n+ x + xxxxxxxxxxxxx\n+ x xxxxx xxxxxxx\n+ x xxxxxxxxxxxxx\n+ xxxxx + x xxxxx\n+ xxx xx xxxx xx xxxxxxxxxxxxxxxx\n+ xx xxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxx + x xxxx\n+ xxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx x x\n+\n+ xxxx + xxxxx xxx xxxxxxxxxxxxxxxxxxxxxx\n+ xx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xx xxxxx\n+ xxxxxxxxxx x xxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxx + x x\n+ xxxx xxxxxxxxxxx\n+ xxxxxxxxxxxxxxxx + x x\n+ xxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+\n+ xxxx + xxxxx xxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxx\n+\n+ x + xxxxxxxxxxxxxxxxxxxxxxxxx\n+ x xxxxx xxxxxxxxxxxx xx xxx\n+ x + xxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxx xxxxx xxx xxx xx + xxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxx x xxxxxxxxxxx\n+ xxxxxxxxxxx\n+ xxxxxxxxxxxxxx\n+ xxx + xxxx xxxx xx xxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xx xxxxxxxxx xx xxxxx\n+ xxxxx + x xxxxxxxxxxxxxxx\n+ xxx x xxxxxxxxxxxxxxxxxxxxxx xx xxxxx + xx x xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxx xxxx xx xxxxxxxx\n+\n+ x xxxxxxxxxxxxxxxxxxx\n+ x + xxx xx xxxxxxxxxxxx\n+ x xxxxxxxxxxxxxxxxxxx\n+ xxxxx x xxxxx\n+ xxx + xx xxxx xx xxxxxxxxxxxxxxxx\n+ xx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxx + x xxxx\n+ xxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+\n+ xxxx + xxxxx xxx xxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxx x x\n+\n+ xxxx + xxxxx xxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxx\n+\n+ xx + xxxxxxxxx xxx xx xxxxx\n+ xxxx x xxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxx\n+ xxx + x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxx\n+\n+ x + xxxxxxxxx\n+ x xxxx xxxx\n+ x xxxxxxxxx\n+ xxxx x xxxxxxxxxxxxxxxx + xxxxxx xxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxx\n+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxx + xxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxx\n+\n+ xxx xxxxxxxxxxxxxxxxxxxxxx xxxxxxx\n+ xxxxxx + xxx x x xx xxxxx x xxxx xxxx xxx xxxxx\n+ xxx xxxx xx xxxxxx\n+ xx + xxxx xx xxx\n+ xxx x xxxx\n+ xx xxxxxxxxxxxxxxxxx + xx xx xxxxxxxxxxxxxxxxxxxxxx\n+ xxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ xxxxx\n+ xxxxxx + xxxxxx xxx\n"}]' + headers: + Server: nginx + Date: Mon, 05 Nov 2018 20:01:28 GMT + Content-Type: application/json + Transfer-Encoding: chunked + Connection: close + Vary: Accept-Encoding + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"1730e6a0083504ed942ece1cd627f6e2" + Link: ; + rel="first", ; + rel="last" + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: ae95c8f8-b183-4fa5-b392-358112004304 + X-Runtime: '0.045400' + X-Total: '1' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1541448148' + Ratelimit-Resettime: Tue, 05 Nov 2018 20:02:28 GMT + X-Consumed-Content-Encoding: gzip + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_commit_not_found.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_commit_not_found.yaml new file mode 100644 index 0000000000..0d71cff0a0 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_commit_not_found.yaml @@ -0,0 +1,32 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/commits/none + response: + url: https://gitlab.com/api/v4/projects/187725/repository/commits/none + content: '{"message":"404 Commit Not Found"}' + headers: + Server: nginx + Date: Fri, 02 Nov 2018 05:32:21 GMT + Content-Type: application/json + Content-Length: '34' + Connection: close + Cache-Control: no-cache + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: 117a9399-d538-4d56-bb76-823b5319ab69 + X-Runtime: '0.030542' + Ratelimit-Limit: '600' + Ratelimit-Observed: '6' + Ratelimit-Remaining: '594' + Ratelimit-Reset: '1541136801' + Ratelimit-Resettime: Sat, 02 Nov 2018 05:33:21 GMT + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_commit_statuses.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_commit_statuses.yaml new file mode 100644 index 0000000000..50bebdea6d --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_commit_statuses.yaml @@ -0,0 +1,43 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/commits/c739768fcac68144a3a6d82305b9c4106934d31a/statuses + response: + url: https://gitlab.com/api/v4/projects/187725/repository/commits/c739768fcac68144a3a6d82305b9c4106934d31a/statuses + content: '[]' + headers: + Server: nginx + Date: Mon, 05 Nov 2018 20:02:49 GMT + Content-Type: application/json + Content-Length: '2' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"d751713988987e9331980363e24189ce" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: 060be9e7-929c-4c24-aa93-df4459474b89 + X-Runtime: '0.039247' + X-Total: '0' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1541448229' + Ratelimit-Resettime: Tue, 05 Nov 2018 20:03:49 GMT + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_compare.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_compare.yaml new file mode 100644 index 0000000000..96fab8f222 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_compare.yaml @@ -0,0 +1,47 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/compare/?from=b33e1281&to=5716de23 + response: + url: https://gitlab.com/api/v4/projects/187725/repository/compare/?from=b33e1281&to=5716de23 + content: '{"commit":{"id":"5716de23b27020419d1a40dd93b469c041a1eeef","short_id":"5716de23","title":"addd + folder","created_at":"2014-08-21T18:36:38.000Z","parent_ids":["c739768fcac68144a3a6d82305b9c4106934d31a"],"message":"addd + folder\n","author_name":"stevepeak","author_email":"steve@stevepeak.net","authored_date":"2014-08-21T18:36:38.000Z","committer_name":"stevepeak","committer_email":"steve@stevepeak.net","committed_date":"2014-08-21T18:36:38.000Z"},"commits":[{"id":"c739768fcac68144a3a6d82305b9c4106934d31a","short_id":"c739768f","title":"shhhh + i''m batman!","created_at":"2014-08-20T21:52:44.000Z","parent_ids":["b33e12816cc3f386dae8add4968cedeff5155021"],"message":"shhhh + i''m batman!\n","author_name":"stevepeak","author_email":"steve@stevepeak.net","authored_date":"2014-08-20T21:52:44.000Z","committer_name":"stevepeak","committer_email":"steve@stevepeak.net","committed_date":"2014-08-20T21:52:44.000Z"},{"id":"5716de23b27020419d1a40dd93b469c041a1eeef","short_id":"5716de23","title":"addd + folder","created_at":"2014-08-21T18:36:38.000Z","parent_ids":["c739768fcac68144a3a6d82305b9c4106934d31a"],"message":"addd + folder\n","author_name":"stevepeak","author_email":"steve@stevepeak.net","authored_date":"2014-08-21T18:36:38.000Z","committer_name":"stevepeak","committer_email":"steve@stevepeak.net","committed_date":"2014-08-21T18:36:38.000Z"}],"diffs":[{"old_path":"README.md","new_path":"README.md","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"diff":"@@ + -1,5 +1,15 @@\n-### Example\n+### CI Testing\n \n-\u003e This repo is used + for CI Testing. Enjoy this gif as a reward!\n+\u003e This repo is used for + CI Testing\n+\n+\n+| [https://codecov.io/][1] | [@codecov][2] | [hello@codecov.io][3] + |\n+| ------------------------ | ------------- | --------------------- |\n+\n+-----\n+\n+\n+[1]: + https://codecov.io/\n+[2]: https://twitter.com/codecov\n+[3]: mailto:hello@codecov.io\n + \n-![i can do that](http://gph.is/17cvPc4)\n"},{"old_path":"folder/hello-world.txt","new_path":"folder/hello-world.txt","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"diff":"@@ + -0,0 +1 @@\n+hello world\n"}],"compare_timeout":false,"compare_same_ref":false}' + headers: + Server: nginx + Date: Tue, 06 Nov 2018 05:43:10 GMT + Content-Type: application/json + Content-Length: '2227' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"336a3d6ff8510f0fd417c13b61113264" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: 06a19213-6145-4f5d-825c-4bc7b0f87776 + X-Runtime: '0.068229' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1541483050' + Ratelimit-Resettime: Wed, 06 Nov 2018 05:44:10 GMT + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_is_admin.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_is_admin.yaml new file mode 100644 index 0000000000..523bd52c0e --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_is_admin.yaml @@ -0,0 +1,37 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups/4037482/members/all/3108129 + response: + url: https://gitlab.com/api/v4/groups/4037482/members/all/3108129 + content: '{"id":3108129,"name":"Eli Hooten","username":"hootener","state":"active","avatar_url":"https://secure.gravatar.com/avatar/ab022b5adf4e3edc9ea77fe7fd561837?s=80\u0026d=identicon","web_url":"https://gitlab.com/hootener","access_level":50,"expires_at":null}' + headers: + Content-Length: '254' + Referrer-Policy: strict-origin-when-cross-origin + X-Content-Type-Options: nosniff + Ratelimit-Remaining: '599' + Strict-Transport-Security: max-age=31536000 + Vary: Origin + X-Request-Id: WMZOHYN11Q1 + Ratelimit-Reset: '1574271035' + Server: nginx + Ratelimit-Limit: '600' + Gitlab-Sv: localhost + Connection: close + Etag: W/"ce7fa4ae0ad9b560bd4bb61da4f8d66f" + Gitlab-Lb: fe-10-lb-gprd + Cache-Control: max-age=0, private, must-revalidate + Date: Wed, 20 Nov 2019 17:29:35 GMT + X-Frame-Options: SAMEORIGIN + X-Runtime: '0.087430' + Content-Type: application/json + Ratelimit-Resettime: Wed, 20 Nov 2019 17:30:35 GMT + Ratelimit-Observed: '1' + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pipeline_details.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pipeline_details.yaml new file mode 100644 index 0000000000..e0bf28d0dc --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pipeline_details.yaml @@ -0,0 +1,74 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - gitlab.com + user-agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/61173290/jobs/7676952799 + response: + content: '{"id":7676952799,"status":"success","stage":"test","name":"tests","ref":"main","tag":false,"coverage":null,"allow_failure":false,"created_at":"2024-08-27T12:56:33.109Z","started_at":"2024-08-27T12:56:33.855Z","finished_at":"2024-08-27T12:57:21.007Z","erased_at":null,"duration":47.152076,"queued_duration":0.240875,"user":{"id":11111386,"username":"giovanni-guidini","name":"Giovanni + M Guidini","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/11111386/avatar.png","web_url":"https://gitlab.com/giovanni-guidini","created_at":"2022-03-14T16:58:59.064Z","bio":"","location":"","public_email":"","skype":"","linkedin":"","twitter":"","discord":"","website_url":"","organization":"","job_title":"","pronouns":"","bot":false,"work_information":null,"followers":0,"following":0,"local_time":null},"commit":{"id":"508c25daba5bbc77d8e7cf3c1917d5859153cfd3","short_id":"508c25da","created_at":"2024-08-27T14:56:19.000+02:00","parent_ids":["5c6b671b6610a57342fcb8d821f2058c0089bd0b"],"title":"yaml + was not valid","message":"yaml was not valid\n","author_name":"Giovanni Guidini","author_email":"giovanni.guidini@sentry.io","authored_date":"2024-08-27T14:56:19.000+02:00","committer_name":"Giovanni + Guidini","committer_email":"giovanni.guidini@sentry.io","committed_date":"2024-08-27T14:56:19.000+02:00","trailers":{},"extended_trailers":{},"web_url":"https://gitlab.com/giovanni-guidini/unexpected-changes/-/commit/508c25daba5bbc77d8e7cf3c1917d5859153cfd3"},"pipeline":{"id":1428929768,"iid":8,"project_id":61173290,"sha":"508c25daba5bbc77d8e7cf3c1917d5859153cfd3","ref":"main","status":"success","source":"push","created_at":"2024-08-27T12:56:33.097Z","updated_at":"2024-08-27T12:57:54.393Z","web_url":"https://gitlab.com/giovanni-guidini/unexpected-changes/-/pipelines/1428929768"},"web_url":"https://gitlab.com/giovanni-guidini/unexpected-changes/-/jobs/7676952799","project":{"ci_job_token_scope_enabled":false},"artifacts":[{"file_type":"trace","size":15937,"filename":"job.log","file_format":null}],"runner":{"id":12270852,"description":"3-green.saas-linux-small-amd64.runners-manager.gitlab.com/default","ip_address":null,"active":true,"paused":false,"is_shared":true,"runner_type":"instance_type","name":"gitlab-runner","online":false,"status":"offline"},"runner_manager":{"id":24719420,"system_id":"s_0e6850b2bce1","version":"17.0.0~pre.88.g761ae5dd","revision":"761ae5dd","platform":"linux","architecture":"amd64","created_at":"2024-05-08T09:53:44.828Z","contacted_at":"2024-09-09T15:01:03.267Z","ip_address":"10.1.5.251","status":"offline"},"artifacts_expire_at":null,"archived":false,"tag_list":[]}' + headers: + CF-Cache-Status: + - MISS + CF-RAY: + - 8c0fcd713ccc6239-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 10 Sep 2024 13:35:40 GMT + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=wrjFWAI0f7IQ8bwVZ5ONkT6teyLwmH1YLOL9cF3LQG%2BPyNTrpqD%2BOfvANCjDKGSSntPN%2BDfrePSeTWNSHvbnfK81Ld7DW%2B0f0A8tJwHhaRg54rguPF1lacR4obP%2BDb0s5D7f8s%2FosDY%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - _cfuvid=RBtgo_2FCmHgtMP_.de3SVtJpfb5YmvExkxHey7anPw-1725975340037-0.0.1.1-604800000; + path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + cache-control: + - max-age=0, private, must-revalidate + content-security-policy: + - default-src 'none' + etag: + - W/"a75cec584a181c65d4477f97f57e7603" + gitlab-lb: + - haproxy-main-40-lb-gprd + gitlab-sv: + - gke-cny-api + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=31536000 + vary: + - Origin, Accept-Encoding + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-gitlab-meta: + - '{"correlation_id":"382fafa62d3f904ba4431f49d518a9ea","version":"1"}' + x-request-id: + - 382fafa62d3f904ba4431f49d518a9ea + x-runtime: + - '0.147015' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pipeline_details_fail.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pipeline_details_fail.yaml new file mode 100644 index 0000000000..bfa8bd5ff8 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pipeline_details_fail.yaml @@ -0,0 +1,67 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - gitlab.com + user-agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/61173000/jobs/7676952000 + response: + content: '{"message":"404 Project Not Found"}' + headers: + CF-Cache-Status: + - MISS + CF-RAY: + - 8c0fcd8bed1c6220-GRU + Connection: + - keep-alive + Content-Length: + - '35' + Content-Type: + - application/json + Date: + - Tue, 10 Sep 2024 13:35:44 GMT + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=FWlunF0HE040y9BVAzzagBOzwSvdJDNzkcN%2BBZjpaTDl2eNiai5FCZfkkpsEJP2VKJlvNtnWdRc%2FHcQNYtnVF7hNYewYGEbWnJfXpyGi8kgGwYCgkmGdJReIE2Zt5qMEfd%2BcUseyxV8%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - _cfuvid=LkC1x_pyjXsmSkFz_UU4ryODz_7JfZh9Hw0qeABWYw0-1725975344194-0.0.1.1-604800000; + path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None + cache-control: + - no-cache + content-security-policy: + - default-src 'none' + gitlab-lb: + - haproxy-main-38-lb-gprd + gitlab-sv: + - api-gke-us-east1-d + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=31536000 + vary: + - Origin, Accept-Encoding + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-gitlab-meta: + - '{"correlation_id":"9722527d775c70b8b91d95cda8551124","version":"1"}' + x-request-id: + - 9722527d775c70b8b91d95cda8551124 + x-runtime: + - '0.047577' + http_version: HTTP/1.1 + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_request[1-b0].yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_request[1-b0].yaml new file mode 100644 index 0000000000..fda4398b8c --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_request[1-b0].yaml @@ -0,0 +1,111 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/merge_requests/1 + response: + url: https://gitlab.com/api/v4/projects/187725/merge_requests/1 + content: '{"id":59639,"iid":1,"project_id":187725,"title":"Other branch","description":"","state":"merged","created_at":"2015-03-12T04:40:38.680Z","updated_at":"2018-11-02T05:25:53.623Z","target_branch":"main","source_branch":"other-branch","upvotes":0,"downvotes":0,"author":{"id":109640,"name":"Codecov","username":"codecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/dcdb35375db567705dd7e74226fae67b?s=80\u0026d=identicon","web_url":"https://gitlab.com/codecov"},"assignee":null,"source_project_id":187725,"target_project_id":187725,"labels":[],"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"can_be_merged","sha":"dd798926730aad14aadf72281204bdb85734fe67","merge_commit_sha":"dd798926730aad14aadf72281204bdb85734fe67","user_notes_count":89,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":null,"web_url":"https://gitlab.com/codecov/ci-repo/merge_requests/1","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"subscribed":true,"changes_count":null,"merged_by":null,"merged_at":null,"closed_by":null,"closed_at":null,"latest_build_started_at":null,"latest_build_finished_at":null,"first_deployed_to_production_at":null,"pipeline":null,"diff_refs":null,"approvals_before_merge":null}' + headers: + Server: nginx + Date: Mon, 05 Nov 2018 19:12:30 GMT + Content-Type: application/json + Content-Length: '1324' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"d1b5dae30cb71053b0d2d4ff868d79c2" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: 95fb67bd-dc8f-4ff2-a91d-e5fc69121ff4 + X-Runtime: '0.134720' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '2' + Ratelimit-Remaining: '598' + Ratelimit-Reset: '1541445210' + Ratelimit-Resettime: Tue, 05 Nov 2018 19:13:30 GMT + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/merge_requests/1/commits + response: + url: https://gitlab.com/api/v4/projects/187725/merge_requests/1/commits + content: '[{"id":"dd798926730aad14aadf72281204bdb85734fe67","short_id":"dd798926","title":"left + blank","created_at":"2014-09-04T16:13:44.000Z","parent_ids":[],"message":"left + blank\n","author_name":"stevepeak","author_email":"steve@stevepeak.net","authored_date":"2014-09-04T16:13:44.000Z","committer_name":"stevepeak","committer_email":"steve@stevepeak.net","committed_date":"2014-09-04T16:13:44.000Z"}]' + headers: + Server: nginx + Date: Mon, 05 Nov 2018 19:12:31 GMT + Content-Type: application/json + Content-Length: '394' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"774a263b0b93e422d9976f357f1ce079" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: 51b58ee7-d084-4024-8139-34ad70d7827e + X-Runtime: '0.053195' + X-Total: '1' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '3' + Ratelimit-Remaining: '597' + Ratelimit-Reset: '1541445211' + Ratelimit-Resettime: Tue, 05 Nov 2018 19:13:31 GMT + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/commits/dd798926730aad14aadf72281204bdb85734fe67 + response: + url: https://gitlab.com/api/v4/projects/187725/repository/commits/dd798926730aad14aadf72281204bdb85734fe67 + content: '{"id":"dd798926730aad14aadf72281204bdb85734fe67","short_id":"dd798926","title":"left + blank","created_at":"2014-09-04T16:13:44.000Z","parent_ids":["5716de23b27020419d1a40dd93b469c041a1eeef"],"message":"left + blank\n","author_name":"stevepeak","author_email":"steve@stevepeak.net","authored_date":"2014-09-04T16:13:44.000Z","committer_name":"stevepeak","committer_email":"steve@stevepeak.net","committed_date":"2014-09-04T16:13:44.000Z","stats":{"additions":1,"deletions":0,"total":1},"status":null,"last_pipeline":null,"project_id":187725}' + headers: + Server: nginx + Date: Mon, 05 Nov 2018 19:12:31 GMT + Content-Type: application/json + Content-Length: '537' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"90b7515c6a40ac4fa89b2b0105bc3a81" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: dfece622-b55e-4cf4-ae38-8d0ef9630612 + X-Runtime: '0.051297' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '4' + Ratelimit-Remaining: '596' + Ratelimit-Reset: '1541445211' + Ratelimit-Resettime: Tue, 05 Nov 2018 19:13:31 GMT + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_request_commits.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_request_commits.yaml new file mode 100644 index 0000000000..33d353cb19 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_request_commits.yaml @@ -0,0 +1,45 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/merge_requests/1/commits + response: + url: https://gitlab.com/api/v4/projects/187725/merge_requests/1/commits + content: '[{"id":"dd798926730aad14aadf72281204bdb85734fe67","short_id":"dd798926","title":"left + blank","created_at":"2014-09-04T16:13:44.000Z","parent_ids":[],"message":"left + blank\n","author_name":"stevepeak","author_email":"steve@stevepeak.net","authored_date":"2014-09-04T16:13:44.000Z","committer_name":"stevepeak","committer_email":"steve@stevepeak.net","committed_date":"2014-09-04T16:13:44.000Z"}]' + headers: + Server: nginx + Date: Mon, 05 Nov 2018 19:45:48 GMT + Content-Type: application/json + Content-Length: '394' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"774a263b0b93e422d9976f357f1ce079" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: b2df358a-eead-4f12-9d78-e80c57ba7db0 + X-Runtime: '0.070521' + X-Total: '1' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '2' + Ratelimit-Remaining: '598' + Ratelimit-Reset: '1541447208' + Ratelimit-Resettime: Tue, 05 Nov 2018 19:46:48 GMT + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_request_fail.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_request_fail.yaml new file mode 100644 index 0000000000..2b182e0258 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_request_fail.yaml @@ -0,0 +1,30 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/merge_requests/100 + response: + url: https://gitlab.com/api/v4/projects/187725/merge_requests/100 + content: '{"message":"404 Not found"}' + headers: + Server: nginx + Date: Mon, 05 Nov 2018 19:12:29 GMT + Content-Type: application/json + Content-Length: '27' + Connection: close + Cache-Control: no-cache + Vary: Origin + X-Request-Id: 2e65b43f-43c1-414c-b037-91762ba9b520 + X-Runtime: '0.024981' + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1541445209' + Ratelimit-Resettime: Tue, 05 Nov 2018 19:13:29 GMT + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_request_files.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_request_files.yaml new file mode 100644 index 0000000000..e54d3ffec1 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_request_files.yaml @@ -0,0 +1,105 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - gitlab.com + user-agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/30951850/merge_requests/1/diffs + response: + content: "[{\"diff\":\"@@ -1,11 +0,0 @@\\n-# Learn Gitlab\\n-\\n-We prepared tutorials + to help you set up GitLab in a way to support your complete software development + life cycle. Each tutorial is contained in an issue, and features a task to complete.\\n-\\n-View + the tutorials by clicking on [Issues \\u003e Boards](../../boards) in the left + sidebar. Complete each tutorial in turn by working through the Open list. When + you've completed a tutorial, drag its issue card to the Closed list and move + on to the next.\\n-\\n-\\u003ca href=\\\"../../boards\\\"\\u003e\\u003cimg src=\\\"/issue-board.png\\\" + width=\\\"700\\\" /\\u003e\\u003c/a\\u003e\\n-\\n-# Don\u2019t need this project + anymore?\\n-\\n-You can delete it by going to [Settings \\u003e General](../../../edit), + then expand the \u201CAdvanced\u201D section at the bottom of the page and either + archive or remove the project.\\n\",\"new_path\":\"README.md\",\"old_path\":\"README.md\",\"a_mode\":\"100644\",\"b_mode\":\"0\",\"new_file\":false,\"renamed_file\":false,\"deleted_file\":true}]" + headers: + CF-Cache-Status: + - MISS + CF-RAY: + - 7c204f491b171e14-FRA + Cache-Control: + - max-age=0, private, must-revalidate + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json + Date: + - Thu, 04 May 2023 11:12:37 GMT + Etag: + - W/"359fd2183d5fa775e07032b0519a43d0" + GitLab-LB: + - fe-15-lb-gprd + GitLab-SV: + - localhost + Link: + - ; + rel="first", ; + rel="last" + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + RateLimit-Limit: + - '2000' + RateLimit-Observed: + - '1' + RateLimit-Remaining: + - '1999' + RateLimit-Reset: + - '1683198817' + RateLimit-ResetTime: + - Thu, 04 May 2023 11:13:37 GMT + Referrer-Policy: + - strict-origin-when-cross-origin + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=hvdK%2BRsc44AILDUEh3X7toqKx0%2FHRzMSiZIrwAdmlukwa1ub1uZvN8JJFI0pENyB9FVnwcBHDrMnxw8R5IMzPCHK8tDH9GjxE6DrfAnPWHyKfnB6PU%2BbEmYfx7Y%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - _cfuvid=wLGRAKTJPY8.TWZIfL.NtQZuDigPRbHFoVEJNwk5DLI-1683198757663-0-604800000; + path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000 + Transfer-Encoding: + - chunked + Vary: + - Origin, Accept-Encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Next-Page: + - '' + X-Page: + - '1' + X-Per-Page: + - '20' + X-Prev-Page: + - '' + X-Request-Id: + - 9aca4da5f52a9b70d5b0d264199a6083 + X-Runtime: + - '0.206571' + X-Total: + - '1' + X-Total-Pages: + - '1' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_request_with_diff_refs.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_request_with_diff_refs.yaml new file mode 100644 index 0000000000..0a3f75eb7d --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_request_with_diff_refs.yaml @@ -0,0 +1,51 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/18347774/merge_requests/1 + response: + url: https://gitlab.com/api/v4/projects/18347774/merge_requests/1 + content: '{"id":57043208,"iid":1,"project_id":18347774,"title":"Thiago/base + no base","description":"Some test","state":"merged","created_at":"2020-04-27T14:38:42.926Z","updated_at":"2020-04-28T16:11:38.429Z","merged_by":null,"merged_at":null,"closed_by":null,"closed_at":null,"target_branch":"main","source_branch":"thiago/base-no-base","user_notes_count":1,"upvotes":0,"downvotes":0,"assignee":{"id":3124507,"name":"Thiago + Ramos","username":"ThiagoCodecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/337420e188ca8138d4b8599d3a20ad47?s=80\u0026d=identicon","web_url":"https://gitlab.com/ThiagoCodecov"},"author":{"id":3124507,"name":"Thiago + Ramos","username":"ThiagoCodecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/337420e188ca8138d4b8599d3a20ad47?s=80\u0026d=identicon","web_url":"https://gitlab.com/ThiagoCodecov"},"assignees":[{"id":3124507,"name":"Thiago + Ramos","username":"ThiagoCodecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/337420e188ca8138d4b8599d3a20ad47?s=80\u0026d=identicon","web_url":"https://gitlab.com/ThiagoCodecov"}],"source_project_id":18347774,"target_project_id":18347774,"labels":[],"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"can_be_merged","sha":"b34b00d0872d129943b634693fd8f19f5f37acf9","merge_commit_sha":"b34b00d0872d129943b634693fd8f19f5f37acf9","squash_commit_sha":null,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":true,"reference":"!1","references":{"short":"!1","relative":"!1","full":"ThiagoCodecov/example-python!1"},"web_url":"https://gitlab.com/ThiagoCodecov/example-python/-/merge_requests/1","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"approvals_before_merge":null,"subscribed":false,"changes_count":"8","head_pipeline":{"id":140727691,"sha":"b34b00d0872d129943b634693fd8f19f5f37acf9","ref":"thiago/base-no-base","status":"success","created_at":"2020-04-28T16:11:56.392Z","updated_at":"2020-04-28T16:11:56.535Z","web_url":"https://gitlab.com/ThiagoCodecov/example-python/pipelines/140727691","before_sha":"0000000000000000000000000000000000000000","tag":false,"yaml_errors":null,"user":{"id":3124507,"name":"Thiago + Ramos","username":"ThiagoCodecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/337420e188ca8138d4b8599d3a20ad47?s=80\u0026d=identicon","web_url":"https://gitlab.com/ThiagoCodecov"},"started_at":null,"finished_at":"2020-04-28T16:11:56.534Z","committed_at":null,"duration":null,"coverage":"86.36","detailed_status":{"icon":"status_success","text":"passed","label":"passed","group":"success","tooltip":"passed","has_details":true,"details_path":"/ThiagoCodecov/example-python/pipelines/140727691","illustration":null,"favicon":"https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"}},"diff_refs":{"base_sha":"081d91921f05a8a39d39aef667eddb88e96300c7","head_sha":"b34b00d0872d129943b634693fd8f19f5f37acf9","start_sha":"f0895290dc26668faeeb20ee5ccd4cc995925775"},"merge_error":null,"first_contribution":false,"user":{"can_merge":false}}' + headers: + X-Http-Reason: OK + Date: Tue, 28 Apr 2020 23:57:07 GMT + Content-Type: application/json + Transfer-Encoding: chunked + Connection: keep-alive + Set-Cookie: __cfduid=d7a146a02bed4121701bdf130aab2de861588118227; expires=Thu, + 28-May-20 23:57:07 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; + Secure + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"b99d4b518f82db547ffe63c3a10f6821" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: aEEWmHD8F92 + X-Runtime: '0.092363' + Strict-Transport-Security: max-age=31536000 + Referrer-Policy: strict-origin-when-cross-origin + Ratelimit-Limit: '600' + Ratelimit-Observed: '2' + Ratelimit-Remaining: '598' + Ratelimit-Reset: '1588118287' + Ratelimit-Resettime: Tue, 28 Apr 2020 23:58:07 GMT + Gitlab-Lb: fe-09-lb-gprd + Gitlab-Sv: localhost + Cf-Cache-Status: DYNAMIC + Expect-Ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Server: cloudflare + Cf-Ray: 58b4b947ea32f663-GRU + Content-Encoding: gzip + Cf-Request-Id: 0264d220ee0000f66365ba1200000001 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_requests.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_requests.yaml new file mode 100644 index 0000000000..d44ac2962b --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_pull_requests.yaml @@ -0,0 +1,43 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/merge_requests?state=opened + response: + url: https://gitlab.com/api/v4/projects/187725/merge_requests?state=opened + content: '[{"id":59639,"iid":1,"project_id":187725,"title":"Other branch","description":"","state":"opened","created_at":"2015-03-12T04:40:38.680Z","updated_at":"2018-11-02T05:25:53.623Z","target_branch":"main","source_branch":"other-branch","upvotes":0,"downvotes":0,"author":{"id":109640,"name":"Codecov","username":"codecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/dcdb35375db567705dd7e74226fae67b?s=80\u0026d=identicon","web_url":"https://gitlab.com/codecov"},"assignee":null,"source_project_id":187725,"target_project_id":187725,"labels":[],"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"can_be_merged","sha":"dd798926730aad14aadf72281204bdb85734fe67","merge_commit_sha":null,"user_notes_count":89,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":null,"web_url":"https://gitlab.com/codecov/ci-repo/merge_requests/1","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"approvals_before_merge":null}]' + headers: + Server: nginx + Date: Mon, 05 Nov 2018 20:04:01 GMT + Content-Type: application/json + Content-Length: '1084' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"89a964567a1549306cc2fe9aa5ca23ae" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: ae448bda-7185-4f01-9713-23252fb2dc56 + X-Runtime: '0.061063' + X-Total: '1' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '2' + Ratelimit-Remaining: '598' + Ratelimit-Reset: '1541448301' + Ratelimit-Resettime: Tue, 05 Nov 2018 20:05:01 GMT + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_repo_languages.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_repo_languages.yaml new file mode 100644 index 0000000000..29f690c5a7 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_repo_languages.yaml @@ -0,0 +1,81 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - gitlab.com + user-agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/languages + response: + content: '{"Python":100.0}' + headers: + Accept-Ranges: + - bytes + CF-Cache-Status: + - MISS + CF-RAY: + - 84471cbfba0167cc-MIA + Connection: + - keep-alive + Content-Length: + - '16' + Content-Type: + - application/json + Date: + - Fri, 12 Jan 2024 17:27:48 GMT + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=wV1SedSVFW7JkdWKOkASpH36wHw3Hr31jBP7JPqFe3NsyLOvas4xtVT%2FWFR8ZaNGP%2BT9f5tadCS8zgTnp3FtaoyFDSVrPUmBrGSahUxErPxXRhlFNz1BEoLr81k%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - _cfuvid=z5Nay76qSKw_zi99u6P66tX4XaUIfLXGPAC6zhTmU8c-1705080468609-0-604800000; + path=/; domain=.gitlab.com; HttpOnly; Secure; SameSite=None + cache-control: + - max-age=0, private, must-revalidate + content-security-policy: + - default-src 'none' + etag: + - W/"a865996219ecdc57d7499d3bb4309309" + gitlab-lb: + - haproxy-main-16-lb-gprd + gitlab-sv: + - localhost + ratelimit-limit: + - '2000' + ratelimit-observed: + - '1' + ratelimit-remaining: + - '1999' + ratelimit-reset: + - '1705080528' + ratelimit-resettime: + - Fri, 12 Jan 2024 17:28:48 GMT + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=31536000 + vary: + - Origin, Accept-Encoding + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-gitlab-meta: + - '{"correlation_id":"9e405f2c77754b1b37ba1b30b2a15b74","version":"1"}' + x-request-id: + - 9e405f2c77754b1b37ba1b30b2a15b74 + x-runtime: + - '0.078590' + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_repository.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_repository.yaml new file mode 100644 index 0000000000..fecfc04772 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_repository.yaml @@ -0,0 +1,35 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725 + response: + url: https://gitlab.com/api/v4/projects/187725 + content: '{"id":187725,"description":"","name":"ci-repo","name_with_namespace":"Codecov + / ci-repo","path":"ci-repo","path_with_namespace":"codecov/ci-repo","created_at":"2015-03-10T10:25:37.701Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:codecov/ci-repo.git","http_url_to_repo":"https://gitlab.com/codecov/ci-repo.git","web_url":"https://gitlab.com/codecov/ci-repo","readme_url":"https://gitlab.com/codecov/ci-repo/blob/main/README.md","avatar_url":null,"star_count":2,"forks_count":2,"last_activity_at":"2018-11-02T04:52:20.582Z","namespace":{"id":126816,"name":"codecov","path":"codecov","kind":"user","full_path":"codecov","parent_id":null},"_links":{"self":"https://gitlab.com/api/v4/projects/187725","issues":"https://gitlab.com/api/v4/projects/187725/issues","merge_requests":"https://gitlab.com/api/v4/projects/187725/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/187725/repository/branches","labels":"https://gitlab.com/api/v4/projects/187725/labels","events":"https://gitlab.com/api/v4/projects/187725/events","members":"https://gitlab.com/api/v4/projects/187725/members"},"archived":false,"visibility":"public","owner":{"id":109640,"name":"Codecov","username":"codecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/dcdb35375db567705dd7e74226fae67b?s=80\u0026d=identicon","web_url":"https://gitlab.com/codecov"},"resolve_outdated_diff_discussions":null,"container_registry_enabled":null,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":false,"shared_runners_enabled":false,"lfs_enabled":true,"creator_id":109640,"import_status":"finished","import_error":null,"open_issues_count":11,"runners_token":"77a755cac8b3463203b771118334c1","public_jobs":true,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":true,"only_allow_merge_if_all_discussions_are_resolved":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","permissions":{"project_access":{"access_level":40,"notification_level":3},"group_access":null},"approvals_before_merge":0,"mirror":false}' + headers: + Server: nginx + Date: Tue, 06 Nov 2018 05:17:03 GMT + Content-Type: application/json + Content-Length: '2170' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"d9b8864573cc404e2b24b2b84acd1fb3" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: 63f148f5-b925-415a-bea9-3153e936a012 + X-Runtime: '0.097648' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '5' + Ratelimit-Remaining: '595' + Ratelimit-Reset: '1541481483' + Ratelimit-Resettime: Wed, 06 Nov 2018 05:18:03 GMT + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_repository_subgroup.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_repository_subgroup.yaml new file mode 100644 index 0000000000..0386a637a5 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_repository_subgroup.yaml @@ -0,0 +1,35 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/9715852 + response: + url: https://gitlab.com/api/v4/projects/9715852 + content: '{"id":9715852,"description":"","name":"proj-A","name_with_namespace":"My + Awesome Group / subgroup1 / proj-A","path":"proj-a","path_with_namespace":"l00p_group_1/subgroup1/proj-a","created_at":"2018-12-01T19:47:18.634Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/subgroup1/proj-a.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/subgroup1/proj-a.git","web_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-a","readme_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-a/blob/main/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2018-12-01T19:47:18.634Z","namespace":{"id":4165905,"name":"subgroup1","path":"subgroup1","kind":"group","full_path":"l00p_group_1/subgroup1","parent_id":4165904},"_links":{"self":"https://gitlab.com/api/v4/projects/9715852","issues":"https://gitlab.com/api/v4/projects/9715852/issues","merge_requests":"https://gitlab.com/api/v4/projects/9715852/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9715852/repository/branches","labels":"https://gitlab.com/api/v4/projects/9715852/labels","events":"https://gitlab.com/api/v4/projects/9715852/events","members":"https://gitlab.com/api/v4/projects/9715852/members"},"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","import_error":null,"open_issues_count":0,"runners_token":"test748yaxmvwt3v5d9t","public_jobs":true,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"printing_merge_request_link_enabled":true,"merge_method":"merge","permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}},"mirror":false,"external_authorization_classification_label":""}' + headers: + Content-Length: '2079' + Ratelimit-Remaining: '599' + X-Content-Type-Options: nosniff + Strict-Transport-Security: max-age=31536000 + Vary: Origin + X-Request-Id: LvFqxnSjbj6 + Ratelimit-Reset: '1543874973' + Server: nginx + Ratelimit-Limit: '600' + Connection: close + Etag: W/"075b98ba3d72c978722eb17c4630f3d7" + Cache-Control: max-age=0, private, must-revalidate + Date: Mon, 03 Dec 2018 22:08:33 GMT + X-Frame-Options: SAMEORIGIN + X-Runtime: '0.113571' + Content-Type: application/json + Ratelimit-Resettime: Tue, 03 Dec 2018 22:09:33 GMT + Ratelimit-Observed: '1' + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_repository_subgroup_no_repo_service_id.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_repository_subgroup_no_repo_service_id.yaml new file mode 100644 index 0000000000..705519cf5c --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_repository_subgroup_no_repo_service_id.yaml @@ -0,0 +1,39 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/l00p_group_1%2Fsubgroup1%2Fproj-a + response: + url: https://gitlab.com/api/v4/projects/l00p_group_1%2Fsubgroup1%2Fproj-a + content: '{"id":9715852,"description":"","name":"proj-A","name_with_namespace":"My + Awesome Group / subgroup1 / proj-A","path":"proj-a","path_with_namespace":"l00p_group_1/subgroup1/proj-a","created_at":"2018-12-01T19:47:18.634Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/subgroup1/proj-a.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/subgroup1/proj-a.git","web_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-a","readme_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-a/-/blob/main/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-03-01T01:16:00.572Z","namespace":{"id":4165905,"name":"subgroup1","path":"subgroup1","kind":"group","full_path":"l00p_group_1/subgroup1","parent_id":4165904,"avatar_url":null,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup1"},"_links":{"self":"https://gitlab.com/api/v4/projects/9715852","issues":"https://gitlab.com/api/v4/projects/9715852/issues","merge_requests":"https://gitlab.com/api/v4/projects/9715852/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9715852/repository/branches","labels":"https://gitlab.com/api/v4/projects/9715852/labels","events":"https://gitlab.com/api/v4/projects/9715852/events","members":"https://gitlab.com/api/v4/projects/9715852/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"can_create_merge_request_in":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","forking_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","pages_access_level":"private","emails_disabled":null,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","import_error":null,"open_issues_count":0,"runners_token":"testird3adm5pik6n3jy","ci_default_git_depth":null,"public_jobs":true,"build_git_strategy":"fetch","build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"remove_source_branch_after_merge":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","suggestion_commit_message":null,"auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","autoclose_referenced_issues":true,"external_authorization_classification_label":"","permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}}' + headers: + Date: Wed, 13 May 2020 14:44:52 GMT + Content-Type: application/json + Set-Cookie: __cfduid=d76641b7725a4d68b8ed8ca6ce79a29341589381092; expires=Fri, + 12-Jun-20 14:44:52 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; + Secure + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"a8c9ad15b5244c982f01dc10dc598436" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: AURQFXWRRn4 + X-Runtime: '0.078028' + Strict-Transport-Security: max-age=31536000 + Referrer-Policy: strict-origin-when-cross-origin + Gitlab-Lb: fe-07-lb-gprd + Gitlab-Sv: api-19-sv-gprd + Cf-Cache-Status: DYNAMIC + Expect-Ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Server: cloudflare + Cf-Ray: 592d28f3cbd007de-ATL + Content-Encoding: gzip + Cf-Request-Id: 02b017ec58000007de02326200000001 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_source_master.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_source_master.yaml new file mode 100644 index 0000000000..d0ca9aba69 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_source_master.yaml @@ -0,0 +1,43 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/files/tests.py?ref=master + response: + url: https://gitlab.com/api/v4/projects/187725/repository/files/tests.py?ref=master + content: '{"file_name":"tests.py","file_path":"tests.py","size":195,"encoding":"base64","content_sha256":"311b6ae18bee76f263ddbaa75daf776a3d16a40e613e3476170b98e24f5ae46e","ref":"main","blob_id":"20642e5c79ebd16b1c87ca300ff8b1afd478be5e","commit_id":"0028015f7fa260f5fd68f78c0deffc15183d955e","last_commit_id":"b33e12816cc3f386dae8add4968cedeff5155021","content":"aW1wb3J0IHVuaXR0ZXN0CmltcG9ydCBteV9wYWNrYWdlCgoKY2xhc3MgVGVzdE1ldGhvZHModW5pdHRlc3QuVGVzdENhc2UpOgogICAgZGVmIHRlc3RfYWRkKHNlbGYpOgogICAgICAgIHNlbGYuYXNzZXJ0RXF1YWwobXlfcGFja2FnZS5hZGQoMTApLCAyMCkKCmlmIF9fbmFtZV9fID09ICdfX21haW5fXyc6CiAgICB1bml0dGVzdC5tYWluKCkK"}' + headers: + Server: nginx + Date: Tue, 06 Nov 2018 05:33:12 GMT + Content-Type: application/json + Content-Length: '618' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"646eb491d3af4b6508678d3abeeeb6e5" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Gitlab-Blob-Id: 20642e5c79ebd16b1c87ca300ff8b1afd478be5e + X-Gitlab-Commit-Id: 0028015f7fa260f5fd68f78c0deffc15183d955e + X-Gitlab-Content-Sha256: 311b6ae18bee76f263ddbaa75daf776a3d16a40e613e3476170b98e24f5ae46e + X-Gitlab-Encoding: base64 + X-Gitlab-File-Name: tests.py + X-Gitlab-File-Path: tests.py + X-Gitlab-Last-Commit-Id: b33e12816cc3f386dae8add4968cedeff5155021 + X-Gitlab-Ref: main + X-Gitlab-Size: '195' + X-Request-Id: d5392c7f-1396-4fef-94ec-8f2d0dd36070 + X-Runtime: '0.056949' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1541482452' + Ratelimit-Resettime: Wed, 06 Nov 2018 05:34:12 GMT + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_source_random_commit.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_source_random_commit.yaml new file mode 100644 index 0000000000..4e272104fd --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_source_random_commit.yaml @@ -0,0 +1,43 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/files/folder%2Fhello-world.txt?ref=5716de23 + response: + url: https://gitlab.com/api/v4/projects/187725/repository/files/folder%2Fhello-world.txt?ref=5716de23 + content: '{"file_name":"hello-world.txt","file_path":"folder/hello-world.txt","size":12,"encoding":"base64","content_sha256":"a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447","ref":"5716de23","blob_id":"3b18e512dba79e4c8300dd08aeb37f8e728b8dad","commit_id":"5716de23b27020419d1a40dd93b469c041a1eeef","last_commit_id":"5716de23b27020419d1a40dd93b469c041a1eeef","content":"aGVsbG8gd29ybGQK"}' + headers: + Server: nginx + Date: Tue, 06 Nov 2018 05:37:40 GMT + Content-Type: application/json + Content-Length: '396' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"759767a41423c19cd8f76131b2ada4f8" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Gitlab-Blob-Id: 3b18e512dba79e4c8300dd08aeb37f8e728b8dad + X-Gitlab-Commit-Id: 5716de23b27020419d1a40dd93b469c041a1eeef + X-Gitlab-Content-Sha256: a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447 + X-Gitlab-Encoding: base64 + X-Gitlab-File-Name: hello-world.txt + X-Gitlab-File-Path: folder/hello-world.txt + X-Gitlab-Last-Commit-Id: 5716de23b27020419d1a40dd93b469c041a1eeef + X-Gitlab-Ref: 5716de23 + X-Gitlab-Size: '12' + X-Request-Id: ec0fd6ff-5bb2-4fba-b185-90604d3853da + X-Runtime: '0.038130' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1541482720' + Ratelimit-Resettime: Wed, 06 Nov 2018 05:38:40 GMT + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_source_random_commit_not_found.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_source_random_commit_not_found.yaml new file mode 100644 index 0000000000..3c8d3e3e94 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_get_source_random_commit_not_found.yaml @@ -0,0 +1,32 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/files/awesome%2Fnon_exising_file.py?ref=5716de23 + response: + url: https://gitlab.com/api/v4/projects/187725/repository/files/awesome%2Fnon_exising_file.py?ref=5716de23 + content: '{"message":"404 File Not Found"}' + headers: + Server: nginx + Date: Thu, 08 Aug 2019 15:31:54 GMT + Content-Type: application/json + Content-Length: '32' + Connection: close + Cache-Control: no-cache + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: GVjsH4M4AW3 + X-Runtime: '0.052322' + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1565278374' + Ratelimit-Resettime: Thu, 08 Aug 2019 15:32:54 GMT + status_code: 404 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_files.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_files.yaml new file mode 100644 index 0000000000..9ebcea6b37 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_files.yaml @@ -0,0 +1,46 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/tree?ref=main&path=folder&per_page=100 + response: + url: https://gitlab.com/api/v4/projects/187725/repository/tree?ref=main&path=folder&per_page=100 + content: '[{"id":"3b18e512dba79e4c8300dd08aeb37f8e728b8dad","name":"hello-world.txt","type":"blob","path":"folder/hello-world.txt","mode":"100644"}]' + headers: + Server: nginx + Date: Fri, 06 Mar 2020 22:10:36 GMT + Content-Type: application/json + Content-Length: '138' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"46598bb942924146ec4836f29516d6f1" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: FzMwgxOhEd7 + X-Runtime: '0.042623' + X-Total: '1' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Referrer-Policy: strict-origin-when-cross-origin + Ratelimit-Limit: '600' + Ratelimit-Observed: '2' + Ratelimit-Remaining: '598' + Ratelimit-Reset: '1583532696' + Ratelimit-Resettime: Fri, 06 Mar 2020 22:11:36 GMT + Gitlab-Lb: fe-04-lb-gprd + Gitlab-Sv: localhost + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_repos.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_repos.yaml new file mode 100644 index 0000000000..867da20b99 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_repos.yaml @@ -0,0 +1,165 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/user + response: + url: https://gitlab.com/api/v4/user + content: '{"id":109640,"name":"Codecov","username":"codecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/dcdb35375db567705dd7e74226fae67b?s=80\u0026d=identicon","web_url":"https://gitlab.com/codecov","created_at":"2015-03-10T10:23:04.323Z","bio":"Code + coverage done right.®","location":"","public_email":"hello@codecov.io","skype":"","linkedin":"","twitter":"@codecov","website_url":"https://codecov.io","organization":"","last_sign_in_at":"2017-12-11T13:27:17.950Z","confirmed_at":"2015-03-10T10:23:36.102Z","last_activity_on":"2018-08-30","email":"hello@codecov.io","theme_id":null,"color_scheme_id":1,"projects_limit":100000,"current_sign_in_at":"2018-08-30T18:40:31.616Z","identities":[],"can_create_group":true,"can_create_project":true,"two_factor_enabled":false,"external":false,"private_profile":null,"shared_runners_minutes_limit":2000}' + headers: + Server: nginx + Date: Tue, 06 Nov 2018 05:17:04 GMT + Content-Type: application/json + Content-Length: '858' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"3ba0c085997617a490e9a7bd5238d8f8" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: 1d790873-d6ea-49cf-831d-bb6e5d82eb08 + X-Runtime: '0.031368' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '2' + Ratelimit-Remaining: '598' + Ratelimit-Reset: '1541481484' + Ratelimit-Resettime: Wed, 06 Nov 2018 05:18:04 GMT + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups?per_page=100 + response: + url: https://gitlab.com/api/v4/groups?per_page=100 + content: '[{"id":726800,"web_url":"https://gitlab.com/groups/delectamentum-mud","name":"delectamentum-mud","path":"delectamentum-mud","description":"Server + and client for the MUD built by delectamentum","visibility":"public","lfs_enabled":true,"avatar_url":null,"request_access_enabled":true,"full_name":"delectamentum-mud","full_path":"delectamentum-mud","parent_id":null,"ldap_cn":null,"ldap_access":null}]' + headers: + Server: nginx + Date: Tue, 06 Nov 2018 05:17:05 GMT + Content-Type: application/json + Content-Length: '398' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"48ef1f363add5151788ef2efc8bae6ae" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '100' + X-Prev-Page: '' + X-Request-Id: e645c283-818a-4dfa-ba41-58964439d86a + X-Runtime: '0.041557' + X-Total: '1' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1541481485' + Ratelimit-Resettime: Wed, 06 Nov 2018 05:18:05 GMT + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups/726800/projects?per_page=50&page=1 + response: + url: https://gitlab.com/api/v4/groups/726800/projects?per_page=50&page=1 + content: '[{"id":1384844,"description":"The engine and server for the delectamentum + MUD. Connect to it with the delectamentum mud client","name":"delectamentum-mud-server","name_with_namespace":"Aaron + Echols / delectamentum-mud-server","path":"delectamentum-mud-server","path_with_namespace":"morerunes/delectamentum-mud-server","created_at":"2016-07-09T18:16:53.481Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:morerunes/delectamentum-mud-server.git","http_url_to_repo":"https://gitlab.com/morerunes/delectamentum-mud-server.git","web_url":"https://gitlab.com/morerunes/delectamentum-mud-server","readme_url":"https://gitlab.com/morerunes/delectamentum-mud-server/blob/main/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2018-01-24T03:11:05.389Z","namespace":{"id":223023,"name":"morerunes","path":"morerunes","kind":"user","full_path":"morerunes","parent_id":null},"_links":{"self":"https://gitlab.com/api/v4/projects/1384844","issues":"https://gitlab.com/api/v4/projects/1384844/issues","merge_requests":"https://gitlab.com/api/v4/projects/1384844/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/1384844/repository/branches","labels":"https://gitlab.com/api/v4/projects/1384844/labels","events":"https://gitlab.com/api/v4/projects/1384844/events","members":"https://gitlab.com/api/v4/projects/1384844/members"},"archived":false,"visibility":"public","owner":{"id":189208,"name":"Aaron + Echols","username":"morerunes","state":"active","avatar_url":"https://secure.gravatar.com/avatar/c6486a95668969d682a7551ccfae54a7?s=80\u0026d=identicon","web_url":"https://gitlab.com/morerunes"},"resolve_outdated_diff_discussions":null,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":false,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":189208,"import_status":"none","open_issues_count":0,"public_jobs":true,"ci_config_path":null,"shared_with_groups":[{"group_id":726800,"group_name":"delectamentum-mud","group_access_level":30,"expires_at":null}],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":true,"only_allow_merge_if_all_discussions_are_resolved":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","approvals_before_merge":0,"mirror":false}]' + headers: + Server: nginx + Date: Tue, 06 Nov 2018 05:17:06 GMT + Content-Type: application/json + Content-Length: '2365' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"312d1de5a73d77e2d41892fe48d867c7" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '50' + X-Prev-Page: '' + X-Request-Id: c5bbe13b-09a3-4bbc-a1a4-417e4a4bf633 + X-Runtime: '0.089954' + X-Total: '1' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '2' + Ratelimit-Remaining: '598' + Ratelimit-Reset: '1541481486' + Ratelimit-Resettime: Wed, 06 Nov 2018 05:18:06 GMT + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects?owned=true&per_page=50&page=1 + response: + url: https://gitlab.com/api/v4/projects?owned=true&per_page=50&page=1 + content: '[{"id":580838,"description":"Python coverage example","name":"example-python","name_with_namespace":"Codecov + / example-python","path":"example-python","path_with_namespace":"codecov/example-python","created_at":"2015-11-07T13:26:29.369Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:codecov/example-python.git","http_url_to_repo":"https://gitlab.com/codecov/example-python.git","web_url":"https://gitlab.com/codecov/example-python","readme_url":"https://gitlab.com/codecov/example-python/blob/main/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2016-08-07T17:34:58.153Z","namespace":{"id":126816,"name":"codecov","path":"codecov","kind":"user","full_path":"codecov","parent_id":null},"_links":{"self":"https://gitlab.com/api/v4/projects/580838","issues":"https://gitlab.com/api/v4/projects/580838/issues","merge_requests":"https://gitlab.com/api/v4/projects/580838/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/580838/repository/branches","labels":"https://gitlab.com/api/v4/projects/580838/labels","events":"https://gitlab.com/api/v4/projects/580838/events","members":"https://gitlab.com/api/v4/projects/580838/members"},"archived":false,"visibility":"public","owner":{"id":109640,"name":"Codecov","username":"codecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/dcdb35375db567705dd7e74226fae67b?s=80\u0026d=identicon","web_url":"https://gitlab.com/codecov"},"resolve_outdated_diff_discussions":null,"container_registry_enabled":null,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":false,"snippets_enabled":false,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":109640,"import_status":"finished","open_issues_count":3,"public_jobs":true,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":true,"only_allow_merge_if_all_discussions_are_resolved":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","permissions":{"project_access":{"access_level":40,"notification_level":3},"group_access":null},"approvals_before_merge":0,"mirror":false},{"id":190307,"description":"","name":"ci-private","name_with_namespace":"Codecov + / ci-private","path":"ci-private","path_with_namespace":"codecov/ci-private","created_at":"2015-03-12T10:14:49.641Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:codecov/ci-private.git","http_url_to_repo":"https://gitlab.com/codecov/ci-private.git","web_url":"https://gitlab.com/codecov/ci-private","readme_url":"https://gitlab.com/codecov/ci-private/blob/main/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2015-03-12T10:14:49.641Z","namespace":{"id":126816,"name":"codecov","path":"codecov","kind":"user","full_path":"codecov","parent_id":null},"_links":{"self":"https://gitlab.com/api/v4/projects/190307","issues":"https://gitlab.com/api/v4/projects/190307/issues","merge_requests":"https://gitlab.com/api/v4/projects/190307/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/190307/repository/branches","labels":"https://gitlab.com/api/v4/projects/190307/labels","events":"https://gitlab.com/api/v4/projects/190307/events","members":"https://gitlab.com/api/v4/projects/190307/members"},"archived":false,"visibility":"private","owner":{"id":109640,"name":"Codecov","username":"codecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/dcdb35375db567705dd7e74226fae67b?s=80\u0026d=identicon","web_url":"https://gitlab.com/codecov"},"resolve_outdated_diff_discussions":null,"container_registry_enabled":null,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":false,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":109640,"import_status":"finished","open_issues_count":0,"public_jobs":true,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":true,"only_allow_merge_if_all_discussions_are_resolved":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","permissions":{"project_access":{"access_level":40,"notification_level":3},"group_access":null},"mirror":false},{"id":187725,"description":"","name":"ci-repo","name_with_namespace":"Codecov + / ci-repo","path":"ci-repo","path_with_namespace":"codecov/ci-repo","created_at":"2015-03-10T10:25:37.701Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:codecov/ci-repo.git","http_url_to_repo":"https://gitlab.com/codecov/ci-repo.git","web_url":"https://gitlab.com/codecov/ci-repo","readme_url":"https://gitlab.com/codecov/ci-repo/blob/main/README.md","avatar_url":null,"star_count":2,"forks_count":2,"last_activity_at":"2018-11-02T04:52:20.582Z","namespace":{"id":126816,"name":"codecov","path":"codecov","kind":"user","full_path":"codecov","parent_id":null},"_links":{"self":"https://gitlab.com/api/v4/projects/187725","issues":"https://gitlab.com/api/v4/projects/187725/issues","merge_requests":"https://gitlab.com/api/v4/projects/187725/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/187725/repository/branches","labels":"https://gitlab.com/api/v4/projects/187725/labels","events":"https://gitlab.com/api/v4/projects/187725/events","members":"https://gitlab.com/api/v4/projects/187725/members"},"archived":false,"visibility":"public","owner":{"id":109640,"name":"Codecov","username":"codecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/dcdb35375db567705dd7e74226fae67b?s=80\u0026d=identicon","web_url":"https://gitlab.com/codecov"},"resolve_outdated_diff_discussions":null,"container_registry_enabled":null,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":false,"shared_runners_enabled":false,"lfs_enabled":true,"creator_id":109640,"import_status":"finished","open_issues_count":11,"public_jobs":true,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":true,"only_allow_merge_if_all_discussions_are_resolved":null,"printing_merge_request_link_enabled":true,"merge_method":"merge","permissions":{"project_access":{"access_level":40,"notification_level":3},"group_access":null},"approvals_before_merge":0,"mirror":false}]' + headers: + Server: nginx + Date: Tue, 06 Nov 2018 05:17:06 GMT + Content-Type: application/json + Content-Length: '6381' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"a9a00a288435db966f534bc5fe8ac887" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '50' + X-Prev-Page: '' + X-Request-Id: 50a44793-be1d-4b01-a3d3-8dba0ae7bb95 + X-Runtime: '0.162823' + X-Total: '3' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '3' + Ratelimit-Remaining: '597' + Ratelimit-Reset: '1541481486' + Ratelimit-Resettime: Wed, 06 Nov 2018 05:18:06 GMT + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_repos_subgroups.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_repos_subgroups.yaml new file mode 100644 index 0000000000..5da5ca909a --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_repos_subgroups.yaml @@ -0,0 +1,331 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/user + response: + url: https://gitlab.com/api/v4/user + content: '{"id":3215137,"name":"Infinite Loop","username":"1nf1n1t3l00p","state":"active","avatar_url":"https://secure.gravatar.com/avatar/45037396c3928e53057931410b918291?s=80\u0026d=identicon","web_url":"https://gitlab.com/1nf1n1t3l00p","created_at":"2018-12-01T19:43:16.121Z","bio":null,"location":null,"public_email":"","skype":"","linkedin":"","twitter":"","website_url":"","organization":null,"last_sign_in_at":"2018-12-01T19:44:00.908Z","confirmed_at":"2018-12-01T19:43:33.397Z","last_activity_on":"2018-12-01","email":"tjbiii.photo@gmail.com","theme_id":1,"color_scheme_id":1,"projects_limit":100000,"current_sign_in_at":"2018-12-01T19:44:00.908Z","identities":[],"can_create_group":true,"can_create_project":true,"two_factor_enabled":false,"external":false,"private_profile":null,"shared_runners_minutes_limit":null}' + headers: + Content-Length: '813' + Ratelimit-Remaining: '599' + X-Content-Type-Options: nosniff + Strict-Transport-Security: max-age=31536000 + Vary: Origin + X-Request-Id: d8uC2McQpm4 + Ratelimit-Reset: '1543694484' + Server: nginx + Ratelimit-Limit: '600' + Connection: close + Etag: W/"3e22b9679d0652f77389486f025a01dc" + Cache-Control: max-age=0, private, must-revalidate + Date: Sat, 01 Dec 2018 20:00:24 GMT + X-Frame-Options: SAMEORIGIN + X-Runtime: '0.021925' + Content-Type: application/json + Ratelimit-Resettime: Sun, 01 Dec 2018 20:01:24 GMT + Ratelimit-Observed: '1' + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups?per_page=100 + response: + url: https://gitlab.com/api/v4/groups?per_page=100 + content: '[{"id":4165904,"web_url":"https://gitlab.com/groups/l00p_group_1","name":"My + Awesome Group","path":"l00p_group_1","description":"","visibility":"private","lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group","full_path":"l00p_group_1","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":4165905,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup1","name":"subgroup1","path":"subgroup1","description":"","visibility":"private","lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup1","full_path":"l00p_group_1/subgroup1","parent_id":4165904,"ldap_cn":null,"ldap_access":null},{"id":4165907,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup2","name":"subgroup2","path":"subgroup2","description":"","visibility":"private","lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup2","full_path":"l00p_group_1/subgroup2","parent_id":4165904,"ldap_cn":null,"ldap_access":null}]' + headers: + Content-Length: '1044' + X-Request-Id: ashLilhc1n5 + Ratelimit-Reset: '1543694484' + Etag: W/"5f4cb15d470d686b54f5cf0b6e3acf56" + X-Frame-Options: SAMEORIGIN + Ratelimit-Remaining: '598' + X-Total: '3' + X-Runtime: '0.068277' + Ratelimit-Limit: '600' + Link: ; + rel="first", ; + rel="last" + X-Next-Page: '' + Date: Sat, 01 Dec 2018 20:00:24 GMT + X-Prev-Page: '' + Strict-Transport-Security: max-age=31536000 + Server: nginx + Connection: close + Ratelimit-Observed: '2' + X-Per-Page: '100' + X-Content-Type-Options: nosniff + Vary: Origin + Ratelimit-Resettime: Sun, 01 Dec 2018 20:01:24 GMT + Cache-Control: max-age=0, private, must-revalidate + X-Page: '1' + X-Total-Pages: '1' + Content-Type: application/json + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups/4165904/projects?per_page=50&page=1 + response: + url: https://gitlab.com/api/v4/groups/4165904/projects?per_page=50&page=1 + content: '[{"id":9715859,"description":"","name":"loop proj","name_with_namespace":"My + Awesome Group / loop proj","path":"loop-proj","path_with_namespace":"l00p_group_1/loop-proj","created_at":"2018-12-01T19:48:07.749Z","default_branch":null,"tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/loop-proj.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/loop-proj.git","web_url":"https://gitlab.com/l00p_group_1/loop-proj","readme_url":null,"avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2018-12-01T19:48:07.749Z","namespace":{"id":4165904,"name":"My + Awesome Group","path":"l00p_group_1","kind":"group","full_path":"l00p_group_1","parent_id":null},"_links":{"self":"https://gitlab.com/api/v4/projects/9715859","issues":"https://gitlab.com/api/v4/projects/9715859/issues","merge_requests":"https://gitlab.com/api/v4/projects/9715859/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9715859/repository/branches","labels":"https://gitlab.com/api/v4/projects/9715859/labels","events":"https://gitlab.com/api/v4/projects/9715859/events","members":"https://gitlab.com/api/v4/projects/9715859/members"},"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"public_jobs":true,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"printing_merge_request_link_enabled":true,"merge_method":"merge","mirror":false,"external_authorization_classification_label":""}]' + headers: + Content-Length: '1820' + X-Request-Id: sahQFoVEyr3 + Ratelimit-Reset: '1543694484' + Etag: W/"b98d36443ddf2b24c29a6320bbac5876" + X-Frame-Options: SAMEORIGIN + Ratelimit-Remaining: '597' + X-Total: '1' + X-Runtime: '0.075727' + Ratelimit-Limit: '600' + Link: ; + rel="first", ; + rel="last" + X-Next-Page: '' + Date: Sat, 01 Dec 2018 20:00:24 GMT + X-Prev-Page: '' + Strict-Transport-Security: max-age=31536000 + Server: nginx + Connection: close + Ratelimit-Observed: '3' + X-Per-Page: '50' + X-Content-Type-Options: nosniff + Vary: Origin + Ratelimit-Resettime: Sun, 01 Dec 2018 20:01:24 GMT + Cache-Control: max-age=0, private, must-revalidate + X-Page: '1' + X-Total-Pages: '1' + Content-Type: application/json + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups/4165905/projects?per_page=50&page=1 + response: + url: https://gitlab.com/api/v4/groups/4165905/projects?per_page=50&page=1 + content: '[{"id":9715852,"description":"","name":"proj-A","name_with_namespace":"My + Awesome Group / subgroup1 / proj-A","path":"proj-a","path_with_namespace":"l00p_group_1/subgroup1/proj-a","created_at":"2018-12-01T19:47:18.634Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/subgroup1/proj-a.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/subgroup1/proj-a.git","web_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-a","readme_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-a/blob/main/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2018-12-01T19:47:18.634Z","namespace":{"id":4165905,"name":"subgroup1","path":"subgroup1","kind":"group","full_path":"l00p_group_1/subgroup1","parent_id":4165904},"_links":{"self":"https://gitlab.com/api/v4/projects/9715852","issues":"https://gitlab.com/api/v4/projects/9715852/issues","merge_requests":"https://gitlab.com/api/v4/projects/9715852/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9715852/repository/branches","labels":"https://gitlab.com/api/v4/projects/9715852/labels","events":"https://gitlab.com/api/v4/projects/9715852/events","members":"https://gitlab.com/api/v4/projects/9715852/members"},"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"public_jobs":true,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"printing_merge_request_link_enabled":true,"merge_method":"merge","mirror":false,"external_authorization_classification_label":""}]' + headers: + Content-Length: '1926' + X-Request-Id: Gd2fXcZ51S9 + Ratelimit-Reset: '1543694484' + Etag: W/"f00c61e95b12249c70a00c5f4fc3af86" + X-Frame-Options: SAMEORIGIN + Ratelimit-Remaining: '596' + X-Total: '1' + X-Runtime: '0.104565' + Ratelimit-Limit: '600' + Link: ; + rel="first", ; + rel="last" + X-Next-Page: '' + Date: Sat, 01 Dec 2018 20:00:24 GMT + X-Prev-Page: '' + Strict-Transport-Security: max-age=31536000 + Server: nginx + Connection: close + Ratelimit-Observed: '4' + X-Per-Page: '50' + X-Content-Type-Options: nosniff + Vary: Origin + Ratelimit-Resettime: Sun, 01 Dec 2018 20:01:24 GMT + Cache-Control: max-age=0, private, must-revalidate + X-Page: '1' + X-Total-Pages: '1' + Content-Type: application/json + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups/4165907/projects?per_page=50&page=1 + response: + url: https://gitlab.com/api/v4/groups/4165907/projects?per_page=50&page=1 + content: '[{"id":9715886,"description":"flake8 is a python tool that glues together + pep8, pyflakes, mccabe, and third-party plugins to check the style and quality + of some python code.","name":"flake8","name_with_namespace":"My Awesome Group + / subgroup2 / flake8","path":"flake8","path_with_namespace":"l00p_group_1/subgroup2/flake8","created_at":"2018-12-01T19:52:43.980Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/subgroup2/flake8.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/subgroup2/flake8.git","web_url":"https://gitlab.com/l00p_group_1/subgroup2/flake8","readme_url":"https://gitlab.com/l00p_group_1/subgroup2/flake8/blob/main/README.rst","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2018-12-01T19:52:43.980Z","namespace":{"id":4165907,"name":"subgroup2","path":"subgroup2","kind":"group","full_path":"l00p_group_1/subgroup2","parent_id":4165904},"_links":{"self":"https://gitlab.com/api/v4/projects/9715886","issues":"https://gitlab.com/api/v4/projects/9715886/issues","merge_requests":"https://gitlab.com/api/v4/projects/9715886/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9715886/repository/branches","labels":"https://gitlab.com/api/v4/projects/9715886/labels","events":"https://gitlab.com/api/v4/projects/9715886/events","members":"https://gitlab.com/api/v4/projects/9715886/members"},"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"forked_from_project":{"id":88891,"description":"flake8 + is a python tool that glues together pep8, pyflakes, mccabe, and third-party + plugins to check the style and quality of some python code.","name":"flake8","name_with_namespace":"PyCQA + / flake8","path":"flake8","path_with_namespace":"pycqa/flake8","created_at":"2014-09-13T13:30:20.102Z","default_branch":"main","tag_list":["analysis","mccabe","pep8","plugins","pycodestyle","pyflakes","python","quality","style"],"ssh_url_to_repo":"git@gitlab.com:pycqa/flake8.git","http_url_to_repo":"https://gitlab.com/pycqa/flake8.git","web_url":"https://gitlab.com/pycqa/flake8","readme_url":"https://gitlab.com/pycqa/flake8/blob/main/README.rst","avatar_url":null,"star_count":218,"forks_count":133,"last_activity_at":"2018-12-01T03:24:09.875Z","namespace":{"id":61704,"name":"PyCQA","path":"pycqa","kind":"group","full_path":"pycqa","parent_id":null}},"import_status":"finished","open_issues_count":0,"public_jobs":true,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"printing_merge_request_link_enabled":true,"merge_method":"merge","mirror":false,"external_authorization_classification_label":""}]' + headers: + Content-Length: '2974' + X-Request-Id: P8z7p3Ajij9 + Ratelimit-Reset: '1543694485' + Etag: W/"1a7515b1004bca40f148c84e3b92cdc4" + X-Frame-Options: SAMEORIGIN + Ratelimit-Remaining: '595' + X-Total: '1' + X-Runtime: '0.197040' + Ratelimit-Limit: '600' + Link: ; + rel="first", ; + rel="last" + X-Next-Page: '' + Date: Sat, 01 Dec 2018 20:00:25 GMT + X-Prev-Page: '' + Strict-Transport-Security: max-age=31536000 + Server: nginx + Connection: close + Ratelimit-Observed: '5' + X-Per-Page: '50' + X-Content-Type-Options: nosniff + Vary: Origin + Ratelimit-Resettime: Sun, 01 Dec 2018 20:01:25 GMT + Cache-Control: max-age=0, private, must-revalidate + X-Page: '1' + X-Total-Pages: '1' + Content-Type: application/json + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/88891 + response: + url: https://gitlab.com/api/v4/projects/88891 + content: '{"id":88891,"description":"flake8 is a python tool that glues together + pep8, pyflakes, mccabe, and third-party plugins to check the style and quality + of some python code.","name":"flake8","name_with_namespace":"PyCQA / flake8","path":"flake8","path_with_namespace":"pycqa/flake8","created_at":"2014-09-13T13:30:20.102Z","default_branch":"main","tag_list":["analysis","mccabe","pep8","plugins","pycodestyle","pyflakes","python","quality","style"],"ssh_url_to_repo":"git@gitlab.com:pycqa/flake8.git","http_url_to_repo":"https://gitlab.com/pycqa/flake8.git","web_url":"https://gitlab.com/pycqa/flake8","readme_url":"https://gitlab.com/pycqa/flake8/blob/main/README.rst","avatar_url":null,"star_count":218,"forks_count":133,"last_activity_at":"2018-12-01T03:24:09.875Z","namespace":{"id":61704,"name":"PyCQA","path":"pycqa","kind":"group","full_path":"pycqa","parent_id":null},"_links":{"self":"https://gitlab.com/api/v4/projects/88891","issues":"https://gitlab.com/api/v4/projects/88891/issues","merge_requests":"https://gitlab.com/api/v4/projects/88891/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/88891/repository/branches","labels":"https://gitlab.com/api/v4/projects/88891/labels","events":"https://gitlab.com/api/v4/projects/88891/events","members":"https://gitlab.com/api/v4/projects/88891/members"},"archived":false,"visibility":"public","resolve_outdated_diff_discussions":false,"container_registry_enabled":false,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":false,"jobs_enabled":true,"snippets_enabled":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":6325,"import_status":"none","open_issues_count":50,"public_jobs":true,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":true,"printing_merge_request_link_enabled":true,"merge_method":"merge","permissions":{"project_access":null,"group_access":null},"approvals_before_merge":0,"mirror":false,"external_authorization_classification_label":""}' + headers: + Content-Length: '2089' + Ratelimit-Remaining: '594' + X-Content-Type-Options: nosniff + Strict-Transport-Security: max-age=31536000 + Vary: Origin + X-Request-Id: uU8QWEOHl38 + Ratelimit-Reset: '1543694485' + Server: nginx + Ratelimit-Limit: '600' + Connection: close + Etag: W/"392248e461e996375f71eda12eae7de7" + Cache-Control: max-age=0, private, must-revalidate + Date: Sat, 01 Dec 2018 20:00:25 GMT + X-Frame-Options: SAMEORIGIN + X-Runtime: '0.110194' + Content-Type: application/json + Ratelimit-Resettime: Sun, 01 Dec 2018 20:01:25 GMT + Ratelimit-Observed: '6' + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects?owned=true&per_page=50&page=1 + response: + url: https://gitlab.com/api/v4/projects?owned=true&per_page=50&page=1 + content: '[{"id":9715886,"description":"flake8 is a python tool that glues together + pep8, pyflakes, mccabe, and third-party plugins to check the style and quality + of some python code.","name":"flake8","name_with_namespace":"My Awesome Group + / subgroup2 / flake8","path":"flake8","path_with_namespace":"l00p_group_1/subgroup2/flake8","created_at":"2018-12-01T19:52:43.980Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/subgroup2/flake8.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/subgroup2/flake8.git","web_url":"https://gitlab.com/l00p_group_1/subgroup2/flake8","readme_url":"https://gitlab.com/l00p_group_1/subgroup2/flake8/blob/main/README.rst","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2018-12-01T19:52:43.980Z","namespace":{"id":4165907,"name":"subgroup2","path":"subgroup2","kind":"group","full_path":"l00p_group_1/subgroup2","parent_id":4165904},"_links":{"self":"https://gitlab.com/api/v4/projects/9715886","issues":"https://gitlab.com/api/v4/projects/9715886/issues","merge_requests":"https://gitlab.com/api/v4/projects/9715886/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9715886/repository/branches","labels":"https://gitlab.com/api/v4/projects/9715886/labels","events":"https://gitlab.com/api/v4/projects/9715886/events","members":"https://gitlab.com/api/v4/projects/9715886/members"},"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"forked_from_project":{"id":88891,"description":"flake8 + is a python tool that glues together pep8, pyflakes, mccabe, and third-party + plugins to check the style and quality of some python code.","name":"flake8","name_with_namespace":"PyCQA + / flake8","path":"flake8","path_with_namespace":"pycqa/flake8","created_at":"2014-09-13T13:30:20.102Z","default_branch":"main","tag_list":["analysis","mccabe","pep8","plugins","pycodestyle","pyflakes","python","quality","style"],"ssh_url_to_repo":"git@gitlab.com:pycqa/flake8.git","http_url_to_repo":"https://gitlab.com/pycqa/flake8.git","web_url":"https://gitlab.com/pycqa/flake8","readme_url":"https://gitlab.com/pycqa/flake8/blob/main/README.rst","avatar_url":null,"star_count":218,"forks_count":133,"last_activity_at":"2018-12-01T03:24:09.875Z","namespace":{"id":61704,"name":"PyCQA","path":"pycqa","kind":"group","full_path":"pycqa","parent_id":null}},"import_status":"finished","open_issues_count":0,"public_jobs":true,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"printing_merge_request_link_enabled":true,"merge_method":"merge","permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}},"mirror":false,"external_authorization_classification_label":""},{"id":9715862,"description":"","name":"inf + proj","name_with_namespace":"Infinite Loop / inf proj","path":"inf-proj","path_with_namespace":"1nf1n1t3l00p/inf-proj","created_at":"2018-12-01T19:48:26.216Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:1nf1n1t3l00p/inf-proj.git","http_url_to_repo":"https://gitlab.com/1nf1n1t3l00p/inf-proj.git","web_url":"https://gitlab.com/1nf1n1t3l00p/inf-proj","readme_url":"https://gitlab.com/1nf1n1t3l00p/inf-proj/blob/main/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2018-12-01T19:48:26.216Z","namespace":{"id":4165899,"name":"1nf1n1t3l00p","path":"1nf1n1t3l00p","kind":"user","full_path":"1nf1n1t3l00p","parent_id":null},"_links":{"self":"https://gitlab.com/api/v4/projects/9715862","issues":"https://gitlab.com/api/v4/projects/9715862/issues","merge_requests":"https://gitlab.com/api/v4/projects/9715862/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9715862/repository/branches","labels":"https://gitlab.com/api/v4/projects/9715862/labels","events":"https://gitlab.com/api/v4/projects/9715862/events","members":"https://gitlab.com/api/v4/projects/9715862/members"},"archived":false,"visibility":"private","owner":{"id":3215137,"name":"Infinite + Loop","username":"1nf1n1t3l00p","state":"active","avatar_url":"https://secure.gravatar.com/avatar/45037396c3928e53057931410b918291?s=80\u0026d=identicon","web_url":"https://gitlab.com/1nf1n1t3l00p"},"resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"public_jobs":true,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"printing_merge_request_link_enabled":true,"merge_method":"merge","permissions":{"project_access":{"access_level":40,"notification_level":3},"group_access":null},"mirror":false,"external_authorization_classification_label":""},{"id":9715859,"description":"","name":"loop + proj","name_with_namespace":"My Awesome Group / loop proj","path":"loop-proj","path_with_namespace":"l00p_group_1/loop-proj","created_at":"2018-12-01T19:48:07.749Z","default_branch":null,"tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/loop-proj.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/loop-proj.git","web_url":"https://gitlab.com/l00p_group_1/loop-proj","readme_url":null,"avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2018-12-01T19:48:07.749Z","namespace":{"id":4165904,"name":"My + Awesome Group","path":"l00p_group_1","kind":"group","full_path":"l00p_group_1","parent_id":null},"_links":{"self":"https://gitlab.com/api/v4/projects/9715859","issues":"https://gitlab.com/api/v4/projects/9715859/issues","merge_requests":"https://gitlab.com/api/v4/projects/9715859/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9715859/repository/branches","labels":"https://gitlab.com/api/v4/projects/9715859/labels","events":"https://gitlab.com/api/v4/projects/9715859/events","members":"https://gitlab.com/api/v4/projects/9715859/members"},"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"public_jobs":true,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"printing_merge_request_link_enabled":true,"merge_method":"merge","permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}},"mirror":false,"external_authorization_classification_label":""},{"id":9715852,"description":"","name":"proj-A","name_with_namespace":"My + Awesome Group / subgroup1 / proj-A","path":"proj-a","path_with_namespace":"l00p_group_1/subgroup1/proj-a","created_at":"2018-12-01T19:47:18.634Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:l00p_group_1/subgroup1/proj-a.git","http_url_to_repo":"https://gitlab.com/l00p_group_1/subgroup1/proj-a.git","web_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-a","readme_url":"https://gitlab.com/l00p_group_1/subgroup1/proj-a/blob/main/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2018-12-01T19:47:18.634Z","namespace":{"id":4165905,"name":"subgroup1","path":"subgroup1","kind":"group","full_path":"l00p_group_1/subgroup1","parent_id":4165904},"_links":{"self":"https://gitlab.com/api/v4/projects/9715852","issues":"https://gitlab.com/api/v4/projects/9715852/issues","merge_requests":"https://gitlab.com/api/v4/projects/9715852/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9715852/repository/branches","labels":"https://gitlab.com/api/v4/projects/9715852/labels","events":"https://gitlab.com/api/v4/projects/9715852/events","members":"https://gitlab.com/api/v4/projects/9715852/members"},"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3215137,"import_status":"none","open_issues_count":0,"public_jobs":true,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"printing_merge_request_link_enabled":true,"merge_method":"merge","permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}},"mirror":false,"external_authorization_classification_label":""}]' + headers: + Content-Length: '9198' + X-Request-Id: SvwjEHyRaa + Ratelimit-Reset: '1543694485' + Etag: W/"f00d17b10de7a7ea9875c8332d0ba7ea" + X-Frame-Options: SAMEORIGIN + Ratelimit-Remaining: '599' + X-Total: '4' + X-Runtime: '0.299171' + Ratelimit-Limit: '600' + Link: ; + rel="first", ; + rel="last" + X-Next-Page: '' + Date: Sat, 01 Dec 2018 20:00:25 GMT + X-Prev-Page: '' + Strict-Transport-Security: max-age=31536000 + Server: nginx + Connection: close + Ratelimit-Observed: '1' + X-Per-Page: '50' + X-Content-Type-Options: nosniff + Vary: Origin + Ratelimit-Resettime: Sun, 01 Dec 2018 20:01:25 GMT + Cache-Control: max-age=0, private, must-revalidate + X-Page: '1' + X-Total-Pages: '1' + Content-Type: application/json + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/88891 + response: + url: https://gitlab.com/api/v4/projects/88891 + content: '{"id":88891,"description":"flake8 is a python tool that glues together + pep8, pyflakes, mccabe, and third-party plugins to check the style and quality + of some python code.","name":"flake8","name_with_namespace":"PyCQA / flake8","path":"flake8","path_with_namespace":"pycqa/flake8","created_at":"2014-09-13T13:30:20.102Z","default_branch":"main","tag_list":["analysis","mccabe","pep8","plugins","pycodestyle","pyflakes","python","quality","style"],"ssh_url_to_repo":"git@gitlab.com:pycqa/flake8.git","http_url_to_repo":"https://gitlab.com/pycqa/flake8.git","web_url":"https://gitlab.com/pycqa/flake8","readme_url":"https://gitlab.com/pycqa/flake8/blob/main/README.rst","avatar_url":null,"star_count":218,"forks_count":133,"last_activity_at":"2018-12-01T03:24:09.875Z","namespace":{"id":61704,"name":"PyCQA","path":"pycqa","kind":"group","full_path":"pycqa","parent_id":null},"_links":{"self":"https://gitlab.com/api/v4/projects/88891","issues":"https://gitlab.com/api/v4/projects/88891/issues","merge_requests":"https://gitlab.com/api/v4/projects/88891/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/88891/repository/branches","labels":"https://gitlab.com/api/v4/projects/88891/labels","events":"https://gitlab.com/api/v4/projects/88891/events","members":"https://gitlab.com/api/v4/projects/88891/members"},"archived":false,"visibility":"public","resolve_outdated_diff_discussions":false,"container_registry_enabled":false,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":false,"jobs_enabled":true,"snippets_enabled":true,"shared_runners_enabled":true,"lfs_enabled":true,"creator_id":6325,"import_status":"none","open_issues_count":50,"public_jobs":true,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":true,"printing_merge_request_link_enabled":true,"merge_method":"merge","permissions":{"project_access":null,"group_access":null},"approvals_before_merge":0,"mirror":false,"external_authorization_classification_label":""}' + headers: + Content-Length: '2089' + Ratelimit-Remaining: '598' + X-Content-Type-Options: nosniff + Strict-Transport-Security: max-age=31536000 + Vary: Origin + X-Request-Id: OUY7zMBTBya + Ratelimit-Reset: '1543694486' + Server: nginx + Ratelimit-Limit: '600' + Connection: close + Etag: W/"392248e461e996375f71eda12eae7de7" + Cache-Control: max-age=0, private, must-revalidate + Date: Sat, 01 Dec 2018 20:00:26 GMT + X-Frame-Options: SAMEORIGIN + X-Runtime: '0.094113' + Content-Type: application/json + Ratelimit-Resettime: Sun, 01 Dec 2018 20:01:26 GMT + Ratelimit-Observed: '2' + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_repos_subgroups_from_subgroups_username.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_repos_subgroups_from_subgroups_username.yaml new file mode 100644 index 0000000000..c94c8d778f --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_repos_subgroups_from_subgroups_username.yaml @@ -0,0 +1,340 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/user + response: + url: https://gitlab.com/api/v4/user + content: '{"id":3124507,"name":"Thiago Ramos","username":"ThiagoCodecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/337420e188ca8138d4b8599d3a20ad47?s=80\u0026d=identicon","web_url":"https://gitlab.com/ThiagoCodecov","created_at":"2018-11-12T19:05:01.723Z","bio":null,"location":null,"public_email":"","skype":"","linkedin":"","twitter":"","website_url":"","organization":null,"last_sign_in_at":"2019-07-16T21:18:29.703Z","confirmed_at":"2018-11-12T19:05:01.249Z","last_activity_on":"2019-08-28","email":"thiago@codecov.io","theme_id":1,"color_scheme_id":1,"projects_limit":100000,"current_sign_in_at":"2019-08-28T10:01:43.462Z","identities":[{"provider":"google_oauth2","extern_uid":"114705562456763720684","saml_provider_id":null}],"can_create_group":true,"can_create_project":true,"two_factor_enabled":false,"external":false,"private_profile":false,"shared_runners_minutes_limit":null,"extra_shared_runners_minutes_limit":null}' + headers: + Content-Length: '943' + Referrer-Policy: strict-origin-when-cross-origin + X-Content-Type-Options: nosniff + Ratelimit-Remaining: '599' + Strict-Transport-Security: max-age=31536000 + Vary: Origin + X-Request-Id: kaHmyPgvSV2 + Ratelimit-Reset: '1566990971' + Server: nginx + Ratelimit-Limit: '600' + Connection: close + Etag: W/"8dffae120254dc912037c596c7c61bcf" + Cache-Control: max-age=0, private, must-revalidate + Date: Wed, 28 Aug 2019 11:15:11 GMT + X-Frame-Options: SAMEORIGIN + X-Runtime: '0.030201' + Content-Type: application/json + Ratelimit-Resettime: Wed, 28 Aug 2019 11:16:11 GMT + Ratelimit-Observed: '1' + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups?per_page=100 + response: + url: https://gitlab.com/api/v4/groups?per_page=100 + content: '[{"id":4037482,"web_url":"https://gitlab.com/groups/codecov-organization","name":"Codecov + Organization","path":"codecov-organization","description":"Codecov organizational + and private repos group. ","visibility":"private","lfs_enabled":true,"avatar_url":"https://gitlab.com/uploads/-/system/group/avatar/4037482/codecov_avatar.png","request_access_enabled":false,"full_name":"Codecov + Organization","full_path":"codecov-organization","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":5938762,"web_url":"https://gitlab.com/groups/thiagocodecovtestgroup","name":"ThiagoCodecovTestGroup","path":"thiagocodecovtestgroup","description":"","visibility":"private","lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"ThiagoCodecovTestGroup","full_path":"thiagocodecovtestgroup","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":5938764,"web_url":"https://gitlab.com/groups/thiagocodecovtestgroup/test-subgroup","name":"test-subgroup","path":"test-subgroup","description":"","visibility":"private","lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"ThiagoCodecovTestGroup + / test-subgroup","full_path":"thiagocodecovtestgroup/test-subgroup","parent_id":5938762,"ldap_cn":null,"ldap_access":null}]' + headers: + Content-Length: '1260' + X-Request-Id: iFoQMaOTqu7 + Ratelimit-Reset: '1566990972' + Etag: W/"51a52180f552780ac9834c841a2092f6" + X-Frame-Options: SAMEORIGIN + Ratelimit-Remaining: '598' + X-Total: '3' + X-Runtime: '0.058890' + Ratelimit-Limit: '600' + Link: ; + rel="first", ; + rel="last" + X-Next-Page: '' + Date: Wed, 28 Aug 2019 11:15:12 GMT + X-Prev-Page: '' + Strict-Transport-Security: max-age=31536000 + Server: nginx + Connection: close + Ratelimit-Observed: '2' + Referrer-Policy: strict-origin-when-cross-origin + X-Per-Page: '100' + X-Content-Type-Options: nosniff + Vary: Origin + Ratelimit-Resettime: Wed, 28 Aug 2019 11:16:12 GMT + Cache-Control: max-age=0, private, must-revalidate + X-Page: '1' + X-Total-Pages: '1' + Content-Type: application/json + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups/4037482/projects?per_page=50&page=1 + response: + url: https://gitlab.com/api/v4/groups/4037482/projects?per_page=50&page=1 + content: '[{"id":12060694,"description":" A GitLab demo for codecov, showing + CI, flags, etc.","name":"demo-gitlab","name_with_namespace":"Codecov Organization + / demo-gitlab","path":"demo-gitlab","path_with_namespace":"codecov-organization/demo-gitlab","created_at":"2019-04-27T15:56:21.542Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:codecov-organization/demo-gitlab.git","http_url_to_repo":"https://gitlab.com/codecov-organization/demo-gitlab.git","web_url":"https://gitlab.com/codecov-organization/demo-gitlab","readme_url":"https://gitlab.com/codecov-organization/demo-gitlab/blob/main/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-04-29T10:57:12.799Z","namespace":{"id":4037482,"name":"Codecov + Organization","path":"codecov-organization","kind":"group","full_path":"codecov-organization","parent_id":null,"avatar_url":"https://gitlab.com/uploads/-/system/group/avatar/4037482/codecov_avatar.png","web_url":"https://gitlab.com/groups/codecov-organization"},"_links":{"self":"https://gitlab.com/api/v4/projects/12060694","issues":"https://gitlab.com/api/v4/projects/12060694/issues","merge_requests":"https://gitlab.com/api/v4/projects/12060694/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/12060694/repository/branches","labels":"https://gitlab.com/api/v4/projects/12060694/labels","events":"https://gitlab.com/api/v4/projects/12060694/events","members":"https://gitlab.com/api/v4/projects/12060694/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3108129,"import_status":"none","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","mirror":false,"external_authorization_classification_label":""},{"id":10575601,"description":"Tests + for codecov assume flags","name":"codecov-assume-flag-test","name_with_namespace":"Codecov + Organization / codecov-assume-flag-test","path":"codecov-assume-flag-test","path_with_namespace":"codecov-organization/codecov-assume-flag-test","created_at":"2019-01-28T15:12:11.654Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:codecov-organization/codecov-assume-flag-test.git","http_url_to_repo":"https://gitlab.com/codecov-organization/codecov-assume-flag-test.git","web_url":"https://gitlab.com/codecov-organization/codecov-assume-flag-test","readme_url":"https://gitlab.com/codecov-organization/codecov-assume-flag-test/blob/main/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-03-15T01:15:24.777Z","namespace":{"id":4037482,"name":"Codecov + Organization","path":"codecov-organization","kind":"group","full_path":"codecov-organization","parent_id":null,"avatar_url":"https://gitlab.com/uploads/-/system/group/avatar/4037482/codecov_avatar.png","web_url":"https://gitlab.com/groups/codecov-organization"},"_links":{"self":"https://gitlab.com/api/v4/projects/10575601","issues":"https://gitlab.com/api/v4/projects/10575601/issues","merge_requests":"https://gitlab.com/api/v4/projects/10575601/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/10575601/repository/branches","labels":"https://gitlab.com/api/v4/projects/10575601/labels","events":"https://gitlab.com/api/v4/projects/10575601/events","members":"https://gitlab.com/api/v4/projects/10575601/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3108129,"import_status":"none","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","mirror":false,"external_authorization_classification_label":""},{"id":9422435,"description":"Tests + for migrating from Codecov Enterprise 4.3.9 to 4.4.0","name":"migration-tests","name_with_namespace":"Codecov + Organization / migration-tests","path":"migration-tests","path_with_namespace":"codecov-organization/migration-tests","created_at":"2018-11-15T16:15:30.347Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:codecov-organization/migration-tests.git","http_url_to_repo":"https://gitlab.com/codecov-organization/migration-tests.git","web_url":"https://gitlab.com/codecov-organization/migration-tests","readme_url":"https://gitlab.com/codecov-organization/migration-tests/blob/main/README.md","avatar_url":null,"star_count":0,"forks_count":1,"last_activity_at":"2018-11-15T16:15:30.347Z","namespace":{"id":4037482,"name":"Codecov + Organization","path":"codecov-organization","kind":"group","full_path":"codecov-organization","parent_id":null,"avatar_url":"https://gitlab.com/uploads/-/system/group/avatar/4037482/codecov_avatar.png","web_url":"https://gitlab.com/groups/codecov-organization"},"_links":{"self":"https://gitlab.com/api/v4/projects/9422435","issues":"https://gitlab.com/api/v4/projects/9422435/issues","merge_requests":"https://gitlab.com/api/v4/projects/9422435/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/9422435/repository/branches","labels":"https://gitlab.com/api/v4/projects/9422435/labels","events":"https://gitlab.com/api/v4/projects/9422435/events","members":"https://gitlab.com/api/v4/projects/9422435/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3108129,"import_status":"finished","open_issues_count":0,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","mirror":false,"external_authorization_classification_label":""}]' + headers: + Content-Length: '7877' + X-Request-Id: lRYxFxyY5X7 + Ratelimit-Reset: '1566990973' + Etag: W/"b3bb989e2735df4bb70650f38d81c7f8" + X-Frame-Options: SAMEORIGIN + Ratelimit-Remaining: '597' + X-Total: '3' + X-Runtime: '0.150274' + Ratelimit-Limit: '600' + Link: ; + rel="first", ; + rel="last" + X-Next-Page: '' + Date: Wed, 28 Aug 2019 11:15:13 GMT + X-Prev-Page: '' + Strict-Transport-Security: max-age=31536000 + Server: nginx + Connection: close + Ratelimit-Observed: '3' + Referrer-Policy: strict-origin-when-cross-origin + X-Per-Page: '50' + X-Content-Type-Options: nosniff + Vary: Origin + Ratelimit-Resettime: Wed, 28 Aug 2019 11:16:13 GMT + Cache-Control: max-age=0, private, must-revalidate + X-Page: '1' + X-Total-Pages: '1' + Content-Type: application/json + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups/5938762/projects?per_page=50&page=1 + response: + url: https://gitlab.com/api/v4/groups/5938762/projects?per_page=50&page=1 + content: '[]' + headers: + Content-Length: '2' + X-Request-Id: vryAo9niej3 + Ratelimit-Reset: '1566990973' + Etag: W/"4f53cda18c2baa0c0354bb5f9a3ecbe5" + X-Frame-Options: SAMEORIGIN + Ratelimit-Remaining: '597' + X-Total: '0' + X-Runtime: '0.044881' + Ratelimit-Limit: '600' + Link: ; + rel="first", ; + rel="last" + X-Next-Page: '' + Date: Wed, 28 Aug 2019 11:15:13 GMT + X-Prev-Page: '' + Strict-Transport-Security: max-age=31536000 + Server: nginx + Connection: close + Ratelimit-Observed: '3' + Referrer-Policy: strict-origin-when-cross-origin + X-Per-Page: '50' + X-Content-Type-Options: nosniff + Vary: Origin + Ratelimit-Resettime: Wed, 28 Aug 2019 11:16:13 GMT + Cache-Control: max-age=0, private, must-revalidate + X-Page: '1' + X-Total-Pages: '1' + Content-Type: application/json + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups/5938764/projects?per_page=50&page=1 + response: + url: https://gitlab.com/api/v4/groups/5938764/projects?per_page=50&page=1 + content: '[{"id":14027433,"description":"GitLab release tasks project, release + managers issue tracker","name":"tasks","name_with_namespace":"ThiagoCodecovTestGroup + / test-subgroup / tasks","path":"tasks","path_with_namespace":"thiagocodecovtestgroup/test-subgroup/tasks","created_at":"2019-08-28T11:14:45.905Z","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:thiagocodecovtestgroup/test-subgroup/tasks.git","http_url_to_repo":"https://gitlab.com/thiagocodecovtestgroup/test-subgroup/tasks.git","web_url":"https://gitlab.com/thiagocodecovtestgroup/test-subgroup/tasks","readme_url":"https://gitlab.com/thiagocodecovtestgroup/test-subgroup/tasks/blob/main/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-08-28T11:14:45.905Z","namespace":{"id":5938764,"name":"test-subgroup","path":"test-subgroup","kind":"group","full_path":"thiagocodecovtestgroup/test-subgroup","parent_id":5938762,"avatar_url":null,"web_url":"https://gitlab.com/groups/thiagocodecovtestgroup/test-subgroup"},"_links":{"self":"https://gitlab.com/api/v4/projects/14027433","issues":"https://gitlab.com/api/v4/projects/14027433/issues","merge_requests":"https://gitlab.com/api/v4/projects/14027433/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/14027433/repository/branches","labels":"https://gitlab.com/api/v4/projects/14027433/labels","events":"https://gitlab.com/api/v4/projects/14027433/events","members":"https://gitlab.com/api/v4/projects/14027433/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":false,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"disabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3124507,"forked_from_project":{"id":5064907,"description":"GitLab + release tasks project, release managers issue tracker","name":"tasks","name_with_namespace":"GitLab.org + / release / tasks","path":"tasks","path_with_namespace":"gitlab-org/release/tasks","created_at":"2018-01-05T11:51:22.955Z","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:gitlab-org/release/tasks.git","http_url_to_repo":"https://gitlab.com/gitlab-org/release/tasks.git","web_url":"https://gitlab.com/gitlab-org/release/tasks","readme_url":"https://gitlab.com/gitlab-org/release/tasks/blob/main/README.md","avatar_url":null,"star_count":68,"forks_count":101,"last_activity_at":"2019-08-28T10:20:29.513Z","namespace":{"id":2351283,"name":"release","path":"release","kind":"group","full_path":"gitlab-org/release","parent_id":9970,"avatar_url":null,"web_url":"https://gitlab.com/groups/gitlab-org/release"}},"import_status":"finished","open_issues_count":0,"ci_default_git_depth":0,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","mirror":false,"external_authorization_classification_label":""},{"id":14026543,"description":"Testing + this project","name":"GroupTestProjectTRR","name_with_namespace":"ThiagoCodecovTestGroup + / test-subgroup / GroupTestProjectTRR","path":"grouptestprojecttrr","path_with_namespace":"thiagocodecovtestgroup/test-subgroup/grouptestprojecttrr","created_at":"2019-08-28T10:05:20.838Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:thiagocodecovtestgroup/test-subgroup/grouptestprojecttrr.git","http_url_to_repo":"https://gitlab.com/thiagocodecovtestgroup/test-subgroup/grouptestprojecttrr.git","web_url":"https://gitlab.com/thiagocodecovtestgroup/test-subgroup/grouptestprojecttrr","readme_url":"https://gitlab.com/thiagocodecovtestgroup/test-subgroup/grouptestprojecttrr/blob/main/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-08-28T10:05:20.838Z","namespace":{"id":5938764,"name":"test-subgroup","path":"test-subgroup","kind":"group","full_path":"thiagocodecovtestgroup/test-subgroup","parent_id":5938762,"avatar_url":null,"web_url":"https://gitlab.com/groups/thiagocodecovtestgroup/test-subgroup"},"_links":{"self":"https://gitlab.com/api/v4/projects/14026543","issues":"https://gitlab.com/api/v4/projects/14026543/issues","merge_requests":"https://gitlab.com/api/v4/projects/14026543/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/14026543/repository/branches","labels":"https://gitlab.com/api/v4/projects/14026543/labels","events":"https://gitlab.com/api/v4/projects/14026543/events","members":"https://gitlab.com/api/v4/projects/14026543/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3124507,"import_status":"finished","open_issues_count":0,"ci_default_git_depth":50,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","mirror":false,"external_authorization_classification_label":""}]' + headers: + Content-Length: '6159' + X-Request-Id: ZcUs2dRMfq4 + Ratelimit-Reset: '1566990974' + Etag: W/"24259f8c01b8a9ffb9f48bcf2649b717" + X-Frame-Options: SAMEORIGIN + Ratelimit-Remaining: '596' + X-Total: '2' + X-Runtime: '0.188782' + Ratelimit-Limit: '600' + Link: ; + rel="first", ; + rel="last" + X-Next-Page: '' + Date: Wed, 28 Aug 2019 11:15:14 GMT + X-Prev-Page: '' + Strict-Transport-Security: max-age=31536000 + Server: nginx + Connection: close + Ratelimit-Observed: '4' + Referrer-Policy: strict-origin-when-cross-origin + X-Per-Page: '50' + X-Content-Type-Options: nosniff + Vary: Origin + Ratelimit-Resettime: Wed, 28 Aug 2019 11:16:14 GMT + Cache-Control: max-age=0, private, must-revalidate + X-Page: '1' + X-Total-Pages: '1' + Content-Type: application/json + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/5064907 + response: + url: https://gitlab.com/api/v4/projects/5064907 + content: '{"id":5064907,"description":"GitLab release tasks project, release + managers issue tracker","name":"tasks","name_with_namespace":"GitLab.org / + release / tasks","path":"tasks","path_with_namespace":"gitlab-org/release/tasks","created_at":"2018-01-05T11:51:22.955Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:gitlab-org/release/tasks.git","http_url_to_repo":"https://gitlab.com/gitlab-org/release/tasks.git","web_url":"https://gitlab.com/gitlab-org/release/tasks","readme_url":"https://gitlab.com/gitlab-org/release/tasks/blob/main/README.md","avatar_url":null,"star_count":68,"forks_count":101,"last_activity_at":"2019-08-28T10:20:29.513Z","namespace":{"id":2351283,"name":"release","path":"release","kind":"group","full_path":"gitlab-org/release","parent_id":9970,"avatar_url":null,"web_url":"https://gitlab.com/groups/gitlab-org/release"},"_links":{"self":"https://gitlab.com/api/v4/projects/5064907","issues":"https://gitlab.com/api/v4/projects/5064907/issues","merge_requests":"https://gitlab.com/api/v4/projects/5064907/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/5064907/repository/branches","labels":"https://gitlab.com/api/v4/projects/5064907/labels","events":"https://gitlab.com/api/v4/projects/5064907/events","members":"https://gitlab.com/api/v4/projects/5064907/members"},"empty_repo":false,"archived":false,"visibility":"public","resolve_outdated_diff_discussions":false,"container_registry_enabled":false,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":false,"jobs_enabled":false,"snippets_enabled":false,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"disabled","builds_access_level":"disabled","snippets_access_level":"disabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":116,"import_status":"none","open_issues_count":1,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","permissions":{"project_access":null,"group_access":null},"approvals_before_merge":0,"mirror":false,"external_authorization_classification_label":"","packages_enabled":null}' + headers: + Content-Length: '2539' + Referrer-Policy: strict-origin-when-cross-origin + X-Content-Type-Options: nosniff + Ratelimit-Remaining: '595' + Strict-Transport-Security: max-age=31536000 + Vary: Origin + X-Request-Id: IxcfLM2HEf1 + Ratelimit-Reset: '1566990975' + Server: nginx + Ratelimit-Limit: '600' + Connection: close + Etag: W/"6cad7f9483dcc4076ac485f90fe39068" + Cache-Control: max-age=0, private, must-revalidate + Date: Wed, 28 Aug 2019 11:15:15 GMT + X-Frame-Options: SAMEORIGIN + X-Runtime: '0.091134' + Content-Type: application/json + Ratelimit-Resettime: Wed, 28 Aug 2019 11:16:15 GMT + Ratelimit-Observed: '5' + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects?owned=true&per_page=50&page=1 + response: + url: https://gitlab.com/api/v4/projects?owned=true&per_page=50&page=1 + content: '[{"id":14027433,"description":"GitLab release tasks project, release + managers issue tracker","name":"tasks","name_with_namespace":"ThiagoCodecovTestGroup + / test-subgroup / tasks","path":"tasks","path_with_namespace":"thiagocodecovtestgroup/test-subgroup/tasks","created_at":"2019-08-28T11:14:45.905Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:thiagocodecovtestgroup/test-subgroup/tasks.git","http_url_to_repo":"https://gitlab.com/thiagocodecovtestgroup/test-subgroup/tasks.git","web_url":"https://gitlab.com/thiagocodecovtestgroup/test-subgroup/tasks","readme_url":"https://gitlab.com/thiagocodecovtestgroup/test-subgroup/tasks/blob/main/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-08-28T11:14:45.905Z","namespace":{"id":5938764,"name":"test-subgroup","path":"test-subgroup","kind":"group","full_path":"thiagocodecovtestgroup/test-subgroup","parent_id":5938762,"avatar_url":null,"web_url":"https://gitlab.com/groups/thiagocodecovtestgroup/test-subgroup"},"_links":{"self":"https://gitlab.com/api/v4/projects/14027433","issues":"https://gitlab.com/api/v4/projects/14027433/issues","merge_requests":"https://gitlab.com/api/v4/projects/14027433/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/14027433/repository/branches","labels":"https://gitlab.com/api/v4/projects/14027433/labels","events":"https://gitlab.com/api/v4/projects/14027433/events","members":"https://gitlab.com/api/v4/projects/14027433/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":false,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"disabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3124507,"forked_from_project":{"id":5064907,"description":"GitLab + release tasks project, release managers issue tracker","name":"tasks","name_with_namespace":"GitLab.org + / release / tasks","path":"tasks","path_with_namespace":"gitlab-org/release/tasks","created_at":"2018-01-05T11:51:22.955Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:gitlab-org/release/tasks.git","http_url_to_repo":"https://gitlab.com/gitlab-org/release/tasks.git","web_url":"https://gitlab.com/gitlab-org/release/tasks","readme_url":"https://gitlab.com/gitlab-org/release/tasks/blob/main/README.md","avatar_url":null,"star_count":68,"forks_count":101,"last_activity_at":"2019-08-28T10:20:29.513Z","namespace":{"id":2351283,"name":"release","path":"release","kind":"group","full_path":"gitlab-org/release","parent_id":9970,"avatar_url":null,"web_url":"https://gitlab.com/groups/gitlab-org/release"}},"import_status":"finished","open_issues_count":0,"ci_default_git_depth":0,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}},"mirror":false,"external_authorization_classification_label":""},{"id":14026543,"description":"Testing + this project","name":"GroupTestProjectTRR","name_with_namespace":"ThiagoCodecovTestGroup + / test-subgroup / GroupTestProjectTRR","path":"grouptestprojecttrr","path_with_namespace":"thiagocodecovtestgroup/test-subgroup/grouptestprojecttrr","created_at":"2019-08-28T10:05:20.838Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:thiagocodecovtestgroup/test-subgroup/grouptestprojecttrr.git","http_url_to_repo":"https://gitlab.com/thiagocodecovtestgroup/test-subgroup/grouptestprojecttrr.git","web_url":"https://gitlab.com/thiagocodecovtestgroup/test-subgroup/grouptestprojecttrr","readme_url":"https://gitlab.com/thiagocodecovtestgroup/test-subgroup/grouptestprojecttrr/blob/main/README.md","avatar_url":null,"star_count":0,"forks_count":0,"last_activity_at":"2019-08-28T10:05:20.838Z","namespace":{"id":5938764,"name":"test-subgroup","path":"test-subgroup","kind":"group","full_path":"thiagocodecovtestgroup/test-subgroup","parent_id":5938762,"avatar_url":null,"web_url":"https://gitlab.com/groups/thiagocodecovtestgroup/test-subgroup"},"_links":{"self":"https://gitlab.com/api/v4/projects/14026543","issues":"https://gitlab.com/api/v4/projects/14026543/issues","merge_requests":"https://gitlab.com/api/v4/projects/14026543/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/14026543/repository/branches","labels":"https://gitlab.com/api/v4/projects/14026543/labels","events":"https://gitlab.com/api/v4/projects/14026543/events","members":"https://gitlab.com/api/v4/projects/14026543/members"},"empty_repo":false,"archived":false,"visibility":"private","resolve_outdated_diff_discussions":false,"container_registry_enabled":true,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"jobs_enabled":true,"snippets_enabled":true,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"enabled","builds_access_level":"enabled","snippets_access_level":"enabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":3124507,"import_status":"finished","open_issues_count":0,"ci_default_git_depth":50,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","permissions":{"project_access":{"access_level":40,"notification_level":null},"group_access":{"access_level":50,"notification_level":3}},"mirror":false,"external_authorization_classification_label":""}]' + headers: + Content-Length: '6392' + X-Request-Id: zpRjTs9axsa + Ratelimit-Reset: '1566990975' + Etag: W/"adf241023ec462e9083ae4e31e37e3ac" + X-Frame-Options: SAMEORIGIN + Ratelimit-Remaining: '595' + X-Total: '2' + X-Runtime: '0.168679' + Ratelimit-Limit: '600' + Link: ; + rel="first", ; + rel="last" + X-Next-Page: '' + Date: Wed, 28 Aug 2019 11:15:15 GMT + X-Prev-Page: '' + Strict-Transport-Security: max-age=31536000 + Server: nginx + Connection: close + Ratelimit-Observed: '5' + Referrer-Policy: strict-origin-when-cross-origin + X-Per-Page: '50' + X-Content-Type-Options: nosniff + Vary: Origin + Ratelimit-Resettime: Wed, 28 Aug 2019 11:16:15 GMT + Cache-Control: max-age=0, private, must-revalidate + X-Page: '1' + X-Total-Pages: '1' + Content-Type: application/json + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/5064907 + response: + url: https://gitlab.com/api/v4/projects/5064907 + content: '{"id":5064907,"description":"GitLab release tasks project, release + managers issue tracker","name":"tasks","name_with_namespace":"GitLab.org / + release / tasks","path":"tasks","path_with_namespace":"gitlab-org/release/tasks","created_at":"2018-01-05T11:51:22.955Z","default_branch":"main","tag_list":[],"ssh_url_to_repo":"git@gitlab.com:gitlab-org/release/tasks.git","http_url_to_repo":"https://gitlab.com/gitlab-org/release/tasks.git","web_url":"https://gitlab.com/gitlab-org/release/tasks","readme_url":"https://gitlab.com/gitlab-org/release/tasks/blob/main/README.md","avatar_url":null,"star_count":68,"forks_count":101,"last_activity_at":"2019-08-28T10:20:29.513Z","namespace":{"id":2351283,"name":"release","path":"release","kind":"group","full_path":"gitlab-org/release","parent_id":9970,"avatar_url":null,"web_url":"https://gitlab.com/groups/gitlab-org/release"},"_links":{"self":"https://gitlab.com/api/v4/projects/5064907","issues":"https://gitlab.com/api/v4/projects/5064907/issues","merge_requests":"https://gitlab.com/api/v4/projects/5064907/merge_requests","repo_branches":"https://gitlab.com/api/v4/projects/5064907/repository/branches","labels":"https://gitlab.com/api/v4/projects/5064907/labels","events":"https://gitlab.com/api/v4/projects/5064907/events","members":"https://gitlab.com/api/v4/projects/5064907/members"},"empty_repo":false,"archived":false,"visibility":"public","resolve_outdated_diff_discussions":false,"container_registry_enabled":false,"issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":false,"jobs_enabled":false,"snippets_enabled":false,"issues_access_level":"enabled","repository_access_level":"enabled","merge_requests_access_level":"enabled","wiki_access_level":"disabled","builds_access_level":"disabled","snippets_access_level":"disabled","shared_runners_enabled":true,"lfs_enabled":true,"creator_id":116,"import_status":"none","open_issues_count":1,"ci_default_git_depth":null,"public_jobs":true,"build_timeout":3600,"auto_cancel_pending_pipelines":"enabled","build_coverage_regex":null,"ci_config_path":null,"shared_with_groups":[],"only_allow_merge_if_pipeline_succeeds":false,"request_access_enabled":false,"only_allow_merge_if_all_discussions_are_resolved":false,"printing_merge_request_link_enabled":true,"merge_method":"merge","auto_devops_enabled":false,"auto_devops_deploy_strategy":"continuous","permissions":{"project_access":null,"group_access":null},"approvals_before_merge":0,"mirror":false,"external_authorization_classification_label":"","packages_enabled":null}' + headers: + Content-Length: '2539' + Referrer-Policy: strict-origin-when-cross-origin + X-Content-Type-Options: nosniff + Ratelimit-Remaining: '594' + Strict-Transport-Security: max-age=31536000 + Vary: Origin + X-Request-Id: CE1n0735eH7 + Ratelimit-Reset: '1566990976' + Server: nginx + Ratelimit-Limit: '600' + Connection: close + Etag: W/"6cad7f9483dcc4076ac485f90fe39068" + Cache-Control: max-age=0, private, must-revalidate + Date: Wed, 28 Aug 2019 11:15:16 GMT + X-Frame-Options: SAMEORIGIN + X-Runtime: '0.101784' + Content-Type: application/json + Ratelimit-Resettime: Wed, 28 Aug 2019 11:16:16 GMT + Ratelimit-Observed: '6' + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_teams.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_teams.yaml new file mode 100644 index 0000000000..1107d3ba39 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_teams.yaml @@ -0,0 +1,44 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups?per_page=100 + response: + url: https://gitlab.com/api/v4/groups?per_page=100 + content: '[{"id":726800,"web_url":"https://gitlab.com/groups/delectamentum-mud","name":"delectamentum-mud","path":"delectamentum-mud","description":"Server + and client for the MUD built by delectamentum","visibility":"public","lfs_enabled":true,"avatar_url":null,"request_access_enabled":true,"full_name":"delectamentum-mud","full_path":"delectamentum-mud","parent_id":null,"ldap_cn":null,"ldap_access":null}]' + headers: + Server: nginx + Date: Tue, 06 Nov 2018 05:17:07 GMT + Content-Type: application/json + Content-Length: '398' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"48ef1f363add5151788ef2efc8bae6ae" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '100' + X-Prev-Page: '' + X-Request-Id: 9d423da9-bb28-49a8-8483-74d774e65b18 + X-Runtime: '0.042034' + X-Total: '1' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1541481487' + Ratelimit-Resettime: Wed, 06 Nov 2018 05:18:07 GMT + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_teams_subgroups.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_teams_subgroups.yaml new file mode 100644 index 0000000000..01573ff5ad --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_teams_subgroups.yaml @@ -0,0 +1,47 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups?per_page=100 + response: + url: https://gitlab.com/api/v4/groups?per_page=100 + content: '[{"id":4165904,"web_url":"https://gitlab.com/groups/l00p_group_1","name":"My + Awesome Group","path":"l00p_group_1","description":"","visibility":"private","lfs_enabled":true,"avatar_url":"https://assets.gitlab-static.net/uploads/-/system/user/avatar/4165904/avatar.png","request_access_enabled":false,"full_name":"My + Awesome Group","full_path":"l00p_group_1","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":4165905,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup1","name":"subgroup1","path":"subgroup1","description":"","visibility":"private","lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup1","full_path":"l00p_group_1/subgroup1","parent_id":4165904,"ldap_cn":null,"ldap_access":null},{"id":4165907,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup2","name":"subgroup2","path":"subgroup2","description":"","visibility":"private","lfs_enabled":true,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup2","full_path":"l00p_group_1/subgroup2","parent_id":4165904,"ldap_cn":null,"ldap_access":null}]' + headers: + Content-Length: '1044' + X-Request-Id: Nz6MzfYGM56 + Ratelimit-Reset: '1543694666' + Etag: W/"5f4cb15d470d686b54f5cf0b6e3acf56" + X-Frame-Options: SAMEORIGIN + Ratelimit-Remaining: '599' + X-Total: '3' + X-Runtime: '0.042099' + Ratelimit-Limit: '600' + Link: ; + rel="first", ; + rel="last" + X-Next-Page: '' + Date: Sat, 01 Dec 2018 20:03:26 GMT + X-Prev-Page: '' + Strict-Transport-Security: max-age=31536000 + Server: nginx + Connection: close + Ratelimit-Observed: '1' + X-Per-Page: '100' + X-Content-Type-Options: nosniff + Vary: Origin + Ratelimit-Resettime: Sun, 01 Dec 2018 20:04:26 GMT + Cache-Control: max-age=0, private, must-revalidate + X-Page: '1' + X-Total-Pages: '1' + Content-Type: application/json + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_top_level_files.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_top_level_files.yaml new file mode 100644 index 0000000000..2f3a34c5d4 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_top_level_files.yaml @@ -0,0 +1,44 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/projects/187725/repository/tree?ref=main&per_page=100 + response: + url: https://gitlab.com/api/v4/projects/187725/repository/tree?ref=main&per_page=100 + content: '[{"id":"1da1ddbfe1ed846f7493964bf489754e464eef64","name":"folder","type":"tree","path":"folder","mode":"040000"},{"id":"c77cff04774d6debf9f8f645323fbe1cea368692","name":".gitignore","type":"blob","path":".gitignore","mode":"100644"},{"id":"321cc67810818865affe8f6bac28f50d3c0a761c","name":".travis.yml","type":"blob","path":".travis.yml","mode":"100644"},{"id":"7974a2260a70aab9ce8ae581fba307c6d448c468","name":"README.md","type":"blob","path":"README.md","mode":"100644"},{"id":"c6b04a8c4a6bd3f8c12e65c7ad3ac759166298dd","name":"large.md","type":"blob","path":"large.md","mode":"100644"},{"id":"478e1519b72ffd69712d77c5f50dd45b203846c4","name":"my_package.py","type":"blob","path":"my_package.py","mode":"100644"},{"id":"20642e5c79ebd16b1c87ca300ff8b1afd478be5e","name":"tests.py","type":"blob","path":"tests.py","mode":"100644"}]' + headers: + Server: nginx + Date: Thu, 29 Aug 2019 13:15:00 GMT + Content-Type: application/json + Content-Length: '831' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"feac3762d9937f4d8568f69c70f52d51" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '20' + X-Prev-Page: '' + X-Request-Id: Yn0Y11Cgoe3 + X-Runtime: '0.042339' + X-Total: '7' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Referrer-Policy: strict-origin-when-cross-origin + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1567084560' + Ratelimit-Resettime: Thu, 29 Aug 2019 13:16:00 GMT + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_make_paginated_call.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_make_paginated_call.yaml new file mode 100644 index 0000000000..158ca114c6 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_make_paginated_call.yaml @@ -0,0 +1,301 @@ +interactions: +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - gitlab.com + user-agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups?per_page=4 + response: + content: '[{"id":5608536,"web_url":"https://gitlab.com/groups/bevera","name":"Bevera","path":"bevera","description":"Bevera + Code and Issues.","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"Bevera","full_path":"bevera","created_at":"2019-07-10T15:34:43.654Z","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":4037482,"web_url":"https://gitlab.com/groups/codecov-organization","name":"Codecov + Organization","path":"codecov-organization","description":"Codecov organizational + and private repos group. ","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":"https://gitlab.com/uploads/-/system/group/avatar/4037482/codecov_avatar.png","request_access_enabled":false,"full_name":"Codecov + Organization","full_path":"codecov-organization","created_at":"2018-11-15T15:58:42.306Z","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":4165904,"web_url":"https://gitlab.com/groups/l00p_group_1","name":"My + Awesome Group","path":"l00p_group_1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group","full_path":"l00p_group_1","created_at":"2018-12-01T19:44:36.005Z","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":5542118,"web_url":"https://gitlab.com/groups/sm-package-zen","name":"Package + Zen","path":"sm-package-zen","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"Package + Zen","full_path":"sm-package-zen","created_at":"2019-07-01T19:57:39.988Z","parent_id":null,"ldap_cn":null,"ldap_access":null}]' + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 61958ae70de4f75c-GRU + Cache-Control: + - max-age=0, private, must-revalidate + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Fri, 29 Jan 2021 20:01:00 GMT + Etag: + - W/"331beacc73ab9f449a0d421eb2edaefc" + Expect-CT: + - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + GitLab-LB: + - fe-02-lb-gprd + GitLab-SV: + - localhost + Link: + - ; + rel="next", ; + rel="first", ; + rel="last" + RateLimit-Limit: + - '2000' + RateLimit-Observed: + - '1' + RateLimit-Remaining: + - '1999' + RateLimit-Reset: + - '1611950520' + RateLimit-ResetTime: + - Fri, 29 Jan 2021 20:02:00 GMT + Referrer-Policy: + - strict-origin-when-cross-origin + Server: + - cloudflare + Set-Cookie: + - __cfduid=df4b980f9298adb9ddd8cb1e51d8b44e81611950460; expires=Sun, 28-Feb-21 + 20:01:00 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; Secure + Strict-Transport-Security: + - max-age=31536000 + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + - Origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Gitlab-Feature-Category: + - subgroups + X-Next-Page: + - '2' + X-Page: + - '1' + X-Per-Page: + - '4' + X-Prev-Page: + - '' + X-Request-Id: + - 01EX7VR36ENXK99FVA95353PEX + X-Runtime: + - '0.098164' + X-Total: + - '9' + X-Total-Pages: + - '3' + cf-request-id: + - 07f155246a0000f75c7e147000000001 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + cookie: + - __cfduid=df4b980f9298adb9ddd8cb1e51d8b44e81611950460 + host: + - gitlab.com + user-agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups?per_page=4&page=2 + response: + content: '[{"id":4165905,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup1","name":"subgroup1","path":"subgroup1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup1","full_path":"l00p_group_1/subgroup1","created_at":"2018-12-01T19:44:50.074Z","parent_id":4165904,"ldap_cn":null,"ldap_access":null},{"id":4165907,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup2","name":"subgroup2","path":"subgroup2","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup2","full_path":"l00p_group_1/subgroup2","created_at":"2018-12-01T19:44:59.453Z","parent_id":4165904,"ldap_cn":null,"ldap_access":null},{"id":4255344,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup2/subsub","name":"subsub","path":"subsub","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup2 / subsub","full_path":"l00p_group_1/subgroup2/subsub","created_at":"2018-12-16T17:02:35.644Z","parent_id":4165907,"ldap_cn":null,"ldap_access":null},{"id":6364610,"web_url":"https://gitlab.com/groups/codecov-organization/test-subgroup","name":"test-subgroup","path":"test-subgroup","description":"A + private subgroup for testing api calls etc in codecov.","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"maintainer","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":true,"full_name":"Codecov + Organization / test-subgroup","full_path":"codecov-organization/test-subgroup","created_at":"2019-10-23T15:05:59.708Z","parent_id":4037482,"ldap_cn":null,"ldap_access":null}]' + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 61958ae90948f75c-GRU + Cache-Control: + - max-age=0, private, must-revalidate + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Fri, 29 Jan 2021 20:01:00 GMT + Etag: + - W/"846b5c5a9e6744c9e3ed9bdac061a00b" + Expect-CT: + - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + GitLab-LB: + - fe-08-lb-gprd + GitLab-SV: + - localhost + Link: + - ; + rel="prev", ; + rel="next", ; + rel="first", ; + rel="last" + RateLimit-Limit: + - '2000' + RateLimit-Observed: + - '2' + RateLimit-Remaining: + - '1998' + RateLimit-Reset: + - '1611950520' + RateLimit-ResetTime: + - Fri, 29 Jan 2021 20:02:00 GMT + Referrer-Policy: + - strict-origin-when-cross-origin + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000 + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + - Origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Gitlab-Feature-Category: + - subgroups + X-Next-Page: + - '3' + X-Page: + - '2' + X-Per-Page: + - '4' + X-Prev-Page: + - '1' + X-Request-Id: + - 01EX7VR3GBQ1X1MS263XVE2K8C + X-Runtime: + - '0.109981' + X-Total: + - '9' + X-Total-Pages: + - '3' + cf-request-id: + - 07f15525a80000f75c90018000000001 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + cookie: + - __cfduid=df4b980f9298adb9ddd8cb1e51d8b44e81611950460 + host: + - gitlab.com + user-agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups?per_page=4&page=3 + response: + content: '[{"id":6571432,"web_url":"https://gitlab.com/groups/codecov-organization/test-subgroup-2","name":"test-subgroup-2","path":"test-subgroup-2","description":"Another + test subgroup to ensure codecov''s API integration works properly. ","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"maintainer","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":true,"full_name":"Codecov + Organization / test-subgroup-2","full_path":"codecov-organization/test-subgroup-2","created_at":"2019-11-20T18:13:04.288Z","parent_id":4037482,"ldap_cn":null,"ldap_access":null}]' + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 61958aeafcd2f75c-GRU + Cache-Control: + - max-age=0, private, must-revalidate + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Fri, 29 Jan 2021 20:01:00 GMT + Etag: + - W/"d51978c34d33a958de3361d265b3132e" + Expect-CT: + - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + GitLab-LB: + - fe-11-lb-gprd + GitLab-SV: + - localhost + Link: + - ; + rel="prev", ; + rel="first", ; + rel="last" + RateLimit-Limit: + - '2000' + RateLimit-Observed: + - '3' + RateLimit-Remaining: + - '1997' + RateLimit-Reset: + - '1611950520' + RateLimit-ResetTime: + - Fri, 29 Jan 2021 20:02:00 GMT + Referrer-Policy: + - strict-origin-when-cross-origin + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000 + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + - Origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Gitlab-Feature-Category: + - subgroups + X-Next-Page: + - '' + X-Page: + - '3' + X-Per-Page: + - '4' + X-Prev-Page: + - '2' + X-Request-Id: + - 01EX7VR3SZAF4PVEJCPYWKGZCZ + X-Runtime: + - '0.101678' + X-Total: + - '9' + X-Total-Pages: + - '3' + cf-request-id: + - 07f15526dc0000f75c7e177000000001 + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_make_paginated_call_max_number_of_pages.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_make_paginated_call_max_number_of_pages.yaml new file mode 100644 index 0000000000..e5d2787c68 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_make_paginated_call_max_number_of_pages.yaml @@ -0,0 +1,168 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups?per_page=3 + response: + url: https://gitlab.com/api/v4/groups?per_page=3 + content: '[{"id":5608536,"web_url":"https://gitlab.com/groups/bevera","name":"Bevera","path":"bevera","description":"Bevera + Code and Issues.","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"Bevera","full_path":"bevera","created_at":"2019-07-10T15:34:43.654Z","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":4165904,"web_url":"https://gitlab.com/groups/l00p_group_1","name":"My + Awesome Group","path":"l00p_group_1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group","full_path":"l00p_group_1","created_at":"2018-12-01T19:44:36.005Z","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":5542118,"web_url":"https://gitlab.com/groups/sm-package-zen","name":"Package + Zen","path":"sm-package-zen","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"Package + Zen","full_path":"sm-package-zen","created_at":"2019-07-01T19:57:39.988Z","parent_id":null,"ldap_cn":null,"ldap_access":null}]' + headers: + Date: Wed, 15 Apr 2020 00:59:36 GMT + Content-Type: application/json + Transfer-Encoding: chunked + Connection: close + Set-Cookie: __cfduid=d62d83ae8b019276982ec9a2b5c9269b61586912376; expires=Fri, + 15-May-20 00:59:36 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; + Secure + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"508ec68e5c00b0266ebbc7533b124456" + Link: ; + rel="next", ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '2' + X-Page: '1' + X-Per-Page: '3' + X-Prev-Page: '' + X-Request-Id: 8CttgzM6BJ5 + X-Runtime: '0.050419' + X-Total: '8' + X-Total-Pages: '3' + Strict-Transport-Security: max-age=31536000 + Referrer-Policy: strict-origin-when-cross-origin + Ratelimit-Limit: '600' + Ratelimit-Observed: '7' + Ratelimit-Remaining: '593' + Ratelimit-Reset: '1586912436' + Ratelimit-Resettime: Wed, 15 Apr 2020 01:00:36 GMT + Gitlab-Lb: fe-02-lb-gprd + Gitlab-Sv: localhost + Cf-Cache-Status: DYNAMIC + Expect-Ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Server: cloudflare + Cf-Ray: 5841b98eab03d024-GRU + X-Consumed-Content-Encoding: gzip + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups?per_page=3&page=2 + response: + url: https://gitlab.com/api/v4/groups?per_page=3&page=2 + content: '[{"id":4570068,"web_url":"https://gitlab.com/groups/falco-group-1","name":"falco-group-1","path":"falco-group-1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"falco-group-1","full_path":"falco-group-1","created_at":"2019-02-08T15:20:51.245Z","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":4570071,"web_url":"https://gitlab.com/groups/falco-group-1/falco-subgroup-1","name":"falco-subgroup-1","path":"falco-subgroup-1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"falco-group-1 + / falco-subgroup-1","full_path":"falco-group-1/falco-subgroup-1","created_at":"2019-02-08T15:21:07.365Z","parent_id":4570068,"ldap_cn":null,"ldap_access":null},{"id":4165905,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup1","name":"subgroup1","path":"subgroup1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup1","full_path":"l00p_group_1/subgroup1","created_at":"2018-12-01T19:44:50.074Z","parent_id":4165904,"ldap_cn":null,"ldap_access":null}]' + headers: + Date: Wed, 15 Apr 2020 00:59:38 GMT + Content-Type: application/json + Transfer-Encoding: chunked + Connection: close + Set-Cookie: __cfduid=d392ba9aedd1fa2010cce847fba4f0a4e1586912377; expires=Fri, + 15-May-20 00:59:37 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; + Secure + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"03d5a579da0c132063121b7b8968766a" + Link: ; + rel="prev", ; + rel="next", ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '3' + X-Page: '2' + X-Per-Page: '3' + X-Prev-Page: '1' + X-Request-Id: Wa6ppHGp7f2 + X-Runtime: '0.048282' + X-Total: '8' + X-Total-Pages: '3' + Strict-Transport-Security: max-age=31536000 + Referrer-Policy: strict-origin-when-cross-origin + Ratelimit-Limit: '600' + Ratelimit-Observed: '8' + Ratelimit-Remaining: '592' + Ratelimit-Reset: '1586912437' + Ratelimit-Resettime: Wed, 15 Apr 2020 01:00:37 GMT + Gitlab-Lb: fe-22-lb-gprd + Gitlab-Sv: localhost + Cf-Cache-Status: DYNAMIC + Expect-Ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Server: cloudflare + Cf-Ray: 5841b9994e98f3a3-GRU + X-Consumed-Content-Encoding: gzip + status_code: 200 + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups?per_page=3&page=3 + response: + url: https://gitlab.com/api/v4/groups?per_page=3&page=3 + content: '[{"id":4165907,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup2","name":"subgroup2","path":"subgroup2","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup2","full_path":"l00p_group_1/subgroup2","created_at":"2018-12-01T19:44:59.453Z","parent_id":4165904,"ldap_cn":null,"ldap_access":null},{"id":4255344,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup2/subsub","name":"subsub","path":"subsub","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup2 / subsub","full_path":"l00p_group_1/subgroup2/subsub","created_at":"2018-12-16T17:02:35.644Z","parent_id":4165907,"ldap_cn":null,"ldap_access":null}]' + headers: + Date: Wed, 15 Apr 2020 00:59:38 GMT + Content-Type: application/json + Transfer-Encoding: chunked + Connection: close + Set-Cookie: __cfduid=d02a7b0dca569af7ddb9a2cdbe5018fa41586912378; expires=Fri, + 15-May-20 00:59:38 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; + Secure + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"8638b662237b71cbe2c42437cc1aeeb7" + Link: ; + rel="prev", ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '3' + X-Per-Page: '3' + X-Prev-Page: '2' + X-Request-Id: gGtdu2Zutw8 + X-Runtime: '0.045066' + X-Total: '8' + X-Total-Pages: '3' + Strict-Transport-Security: max-age=31536000 + Referrer-Policy: strict-origin-when-cross-origin + Ratelimit-Limit: '600' + Ratelimit-Observed: '3' + Ratelimit-Remaining: '597' + Ratelimit-Reset: '1586912438' + Ratelimit-Resettime: Wed, 15 Apr 2020 01:00:38 GMT + Gitlab-Lb: fe-11-lb-gprd + Gitlab-Sv: localhost + Cf-Cache-Status: DYNAMIC + Expect-Ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Server: cloudflare + Cf-Ray: 5841b99dd90cd03c-GRU + X-Consumed-Content-Encoding: gzip + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_make_paginated_call_no_limit.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_make_paginated_call_no_limit.yaml new file mode 100644 index 0000000000..faff9527db --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_make_paginated_call_no_limit.yaml @@ -0,0 +1,62 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + User-Agent: + - Default + method: GET + uri: https://gitlab.com/api/v4/groups?per_page=100 + response: + url: https://gitlab.com/api/v4/groups?per_page=100 + content: '[{"id":5608536,"web_url":"https://gitlab.com/groups/bevera","name":"Bevera","path":"bevera","description":"Bevera + Code and Issues.","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"Bevera","full_path":"bevera","created_at":"2019-07-10T15:34:43.654Z","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":4165904,"web_url":"https://gitlab.com/groups/l00p_group_1","name":"My + Awesome Group","path":"l00p_group_1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group","full_path":"l00p_group_1","created_at":"2018-12-01T19:44:36.005Z","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":5542118,"web_url":"https://gitlab.com/groups/sm-package-zen","name":"Package + Zen","path":"sm-package-zen","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"Package + Zen","full_path":"sm-package-zen","created_at":"2019-07-01T19:57:39.988Z","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":4570068,"web_url":"https://gitlab.com/groups/falco-group-1","name":"falco-group-1","path":"falco-group-1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"falco-group-1","full_path":"falco-group-1","created_at":"2019-02-08T15:20:51.245Z","parent_id":null,"ldap_cn":null,"ldap_access":null},{"id":4570071,"web_url":"https://gitlab.com/groups/falco-group-1/falco-subgroup-1","name":"falco-subgroup-1","path":"falco-subgroup-1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"falco-group-1 + / falco-subgroup-1","full_path":"falco-group-1/falco-subgroup-1","created_at":"2019-02-08T15:21:07.365Z","parent_id":4570068,"ldap_cn":null,"ldap_access":null},{"id":4165905,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup1","name":"subgroup1","path":"subgroup1","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup1","full_path":"l00p_group_1/subgroup1","created_at":"2018-12-01T19:44:50.074Z","parent_id":4165904,"ldap_cn":null,"ldap_access":null},{"id":4165907,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup2","name":"subgroup2","path":"subgroup2","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup2","full_path":"l00p_group_1/subgroup2","created_at":"2018-12-01T19:44:59.453Z","parent_id":4165904,"ldap_cn":null,"ldap_access":null},{"id":4255344,"web_url":"https://gitlab.com/groups/l00p_group_1/subgroup2/subsub","name":"subsub","path":"subsub","description":"","visibility":"private","share_with_group_lock":false,"require_two_factor_authentication":false,"two_factor_grace_period":48,"project_creation_level":"developer","auto_devops_enabled":null,"subgroup_creation_level":"owner","emails_disabled":null,"mentions_disabled":null,"lfs_enabled":true,"default_branch_protection":2,"avatar_url":null,"request_access_enabled":false,"full_name":"My + Awesome Group / subgroup2 / subsub","full_path":"l00p_group_1/subgroup2/subsub","created_at":"2018-12-16T17:02:35.644Z","parent_id":4165907,"ldap_cn":null,"ldap_access":null}]' + headers: + Date: Wed, 15 Apr 2020 00:59:33 GMT + Content-Type: application/json + Transfer-Encoding: chunked + Connection: close + Set-Cookie: __cfduid=d78fd070365e452861f1d41c8fae260951586912372; expires=Fri, + 15-May-20 00:59:32 GMT; path=/; domain=.gitlab.com; HttpOnly; SameSite=Lax; + Secure + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"cda0ff5418608f5812235d5c9220d548" + Link: ; + rel="first", ; + rel="last" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Next-Page: '' + X-Page: '1' + X-Per-Page: '100' + X-Prev-Page: '' + X-Request-Id: 5go6O2plpl3 + X-Runtime: '0.087248' + X-Total: '8' + X-Total-Pages: '1' + Strict-Transport-Security: max-age=31536000 + Referrer-Policy: strict-origin-when-cross-origin + Ratelimit-Limit: '600' + Ratelimit-Observed: '3' + Ratelimit-Remaining: '597' + Ratelimit-Reset: '1586912433' + Ratelimit-Resettime: Wed, 15 Apr 2020 01:00:33 GMT + Gitlab-Lb: fe-19-lb-gprd + Gitlab-Sv: localhost + Cf-Cache-Status: DYNAMIC + Expect-Ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + Server: cloudflare + Cf-Ray: 5841b97b3c94d010-GRU + X-Consumed-Content-Encoding: gzip + status_code: 200 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_post_comment.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_post_comment.yaml new file mode 100644 index 0000000000..0aeac361fe --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_post_comment.yaml @@ -0,0 +1,36 @@ +interactions: + - request: + body: '{"body": "Hello world"}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Default + method: POST + uri: https://gitlab.com/api/v4/projects/187725/merge_requests/1/notes + response: + url: https://gitlab.com/api/v4/projects/187725/merge_requests/1/notes + content: '{"id":113977323,"type":null,"body":"Hello world","attachment":null,"author":{"id":109640,"name":"Codecov","username":"codecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/dcdb35375db567705dd7e74226fae67b?s=80\u0026d=identicon","web_url":"https://gitlab.com/codecov"},"created_at":"2018-11-02T05:25:09.363Z","updated_at":"2018-11-02T05:25:09.363Z","system":false,"noteable_id":59639,"noteable_type":"MergeRequest","resolvable":false,"noteable_iid":1}' + headers: + Server: nginx + Date: Fri, 02 Nov 2018 05:25:09 GMT + Content-Type: application/json + Content-Length: '471' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"4ccd7068de297e4b5bdfe42cd5b92d44" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: 954546f5-a0dd-45c8-824a-6b2da06773f0 + X-Runtime: '0.103485' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '2' + Ratelimit-Remaining: '598' + Ratelimit-Reset: '1541136369' + Ratelimit-Resettime: Sat, 02 Nov 2018 05:26:09 GMT + status_code: 201 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_post_webhook.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_post_webhook.yaml new file mode 100644 index 0000000000..49ab22deb6 --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_post_webhook.yaml @@ -0,0 +1,37 @@ +interactions: + - request: + body: '{"url": "http://requestbin.net/r/1ecyaj51", "enable_ssl_verification": + true, "job_events": true}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Default + method: POST + uri: https://gitlab.com/api/v4/projects/187725/hooks + response: + url: https://gitlab.com/api/v4/projects/187725/hooks + content: '{"id":422507,"url":"http://requestbin.net/r/1ecyaj51","created_at":"2018-11-06T04:51:57.164Z","push_events":true,"tag_push_events":false,"merge_requests_events":false,"repository_update_events":false,"enable_ssl_verification":true,"project_id":187725,"issues_events":false,"confidential_issues_events":false,"note_events":false,"confidential_note_events":null,"pipeline_events":false,"wiki_page_events":false,"job_events":true,"push_events_branch_filter":null}' + headers: + Server: nginx + Date: Tue, 06 Nov 2018 04:51:57 GMT + Content-Type: application/json + Content-Length: '460' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"9f33e8b2ea308e14bcd19be87f43d094" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: eed21ba3-1044-4fdf-9696-2cf8de8b2756 + X-Runtime: '0.066501' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1541479977' + Ratelimit-Resettime: Wed, 06 Nov 2018 04:52:57 GMT + status_code: 201 +version: 1 diff --git a/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_set_commit_status.yaml b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_set_commit_status.yaml new file mode 100644 index 0000000000..5b5846552d --- /dev/null +++ b/libs/shared/tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_set_commit_status.yaml @@ -0,0 +1,37 @@ +interactions: + - request: + body: '{"state": "success", "target_url": "https://localhost:50036/gitlab/codecov/ci-repo?ref=ad798926730aad14aadf72281204bdb85734fe67", + "coverage": null, "name": "context", "description": "aaaaaaaaaa"}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Default + method: POST + uri: https://gitlab.com/api/v4/projects/187725/statuses/c739768fcac68144a3a6d82305b9c4106934d31a + response: + url: https://gitlab.com/api/v4/projects/187725/statuses/c739768fcac68144a3a6d82305b9c4106934d31a + content: '{"id":116703167,"sha":"c739768fcac68144a3a6d82305b9c4106934d31a","ref":"main","status":"success","name":"context","target_url":"https://localhost:50036/gitlab/codecov/ci-repo?ref=ad798926730aad14aadf72281204bdb85734fe67","description":"aaaaaaaaaa","created_at":"2018-11-05T20:11:18.104Z","started_at":null,"finished_at":"2018-11-05T20:11:18.137Z","allow_failure":false,"coverage":null,"author":{"id":109640,"name":"Codecov","username":"codecov","state":"active","avatar_url":"https://secure.gravatar.com/avatar/dcdb35375db567705dd7e74226fae67b?s=80\u0026d=identicon","web_url":"https://gitlab.com/codecov"}}' + headers: + Server: nginx + Date: Mon, 05 Nov 2018 20:11:18 GMT + Content-Type: application/json + Content-Length: '609' + Connection: close + Cache-Control: max-age=0, private, must-revalidate + Etag: W/"07c4fcbc3da0157b7ba51a6a7a05a47c" + Vary: Origin + X-Content-Type-Options: nosniff + X-Frame-Options: SAMEORIGIN + X-Request-Id: 9932c7ed-5695-4484-89c0-f2caababf860 + X-Runtime: '0.119850' + Strict-Transport-Security: max-age=31536000 + Ratelimit-Limit: '600' + Ratelimit-Observed: '1' + Ratelimit-Remaining: '599' + Ratelimit-Reset: '1541448738' + Ratelimit-Resettime: Tue, 05 Nov 2018 20:12:18 GMT + status_code: 201 +version: 1 diff --git a/libs/shared/tests/integration/test_bitbucket.py b/libs/shared/tests/integration/test_bitbucket.py new file mode 100644 index 0000000000..66286b152a --- /dev/null +++ b/libs/shared/tests/integration/test_bitbucket.py @@ -0,0 +1,1157 @@ +import pytest +import vcr + +from shared.torngit.bitbucket import Bitbucket +from shared.torngit.enums import Endpoints +from shared.torngit.exceptions import TorngitObjectNotFoundError + + +@pytest.fixture +def valid_handler(): + return Bitbucket( + repo=dict(name="example-python"), + owner=dict( + username="ThiagoCodecov", service_id="9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645" + ), + oauth_consumer_token=dict( + key="arubajamaicaohiwan", secret="natakeyoubermudabahamacomeonpret" + ), + token=dict(secret="testpnilpfmyehw45pa7rvtkvtm7bhcx", key="testss3hxhcfqf1h6g"), + ) + + +@pytest.fixture +def valid_codecov_handler(): + return Bitbucket( + repo=dict(name="private"), + owner=dict(username="codecov"), + oauth_consumer_token=dict( + key="arubajamaicaohiwan", secret="natakeyoubermudabahamacomeonpret" + ), + token=dict(secret="KeyLargoMontegobabywhydontwego", key="waydowntokokomo"), + ) + + +class TestBitbucketTestCase(object): + @pytest.mark.asyncio + async def test_get_best_effort_branches(self, valid_handler, codecov_vcr): + branches = await valid_handler.get_best_effort_branches("6a45b83") + assert branches == [] + + @pytest.mark.asyncio + async def test_post_comment(self, valid_handler, codecov_vcr): + expected_result = { + "deleted": False, + "id": 114320127, + "content": { + "html": "

    Hello world

    ", + "markup": "markdown", + "raw": "Hello world", + "type": "rendered", + }, + "created_on": "2019-08-24T07:22:19.710114+00:00", + "links": { + "html": { + "href": "https://bitbucket.org/ThiagoCodecov/example-python/pull-requests/1/_/diff#comment-114320127" + }, + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1/comments/114320127" + }, + }, + "pullrequest": { + "id": 1, + "links": { + "html": { + "href": "https://bitbucket.org/ThiagoCodecov/example-python/pull-requests/1" + }, + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/pullrequests/1" + }, + }, + "title": "Hahaa That is a PR", + "type": "pullrequest", + }, + "type": "pullrequest_comment", + "updated_on": "2019-08-24T07:22:19.719805+00:00", + "user": { + "account_id": "5bce04c759d0e84f8c7555e9", + "display_name": "Thiago Ramos", + "links": { + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png" + }, + "html": { + "href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/" + }, + "self": { + "href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D" + }, + }, + "nickname": "thiago", + "type": "user", + "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + }, + } + res = await valid_handler.post_comment("1", "Hello world") + assert res == expected_result + + @pytest.mark.asyncio + async def test_edit_comment(self, valid_handler, codecov_vcr): + res = await valid_handler.edit_comment("1", "114320127", "Hello world numbah 2") + assert res is not None + assert res["id"] == 114320127 + assert res["content"]["raw"] == "Hello world numbah 2" + + @pytest.mark.asyncio + async def test_edit_comment_not_found(self, valid_handler, codecov_vcr): + with pytest.raises(TorngitObjectNotFoundError): + await valid_handler.edit_comment("1", 113979999, "Hello world number 2") + + @pytest.mark.asyncio + async def test_delete_comment(self, valid_handler, codecov_vcr): + assert await valid_handler.delete_comment("1", "107383471") is True + + @pytest.mark.asyncio + async def test_delete_comment_not_found(self, valid_handler, codecov_vcr): + with pytest.raises(TorngitObjectNotFoundError): + await valid_handler.delete_comment("1", 113977999) + + @pytest.mark.asyncio + async def test_find_pull_request_nothing_found(self, valid_handler, codecov_vcr): + assert await valid_handler.find_pull_request("a" * 40, "no-branch") is None + + @pytest.mark.asyncio + async def test_get_pull_request_fail(self, valid_handler, codecov_vcr): + with pytest.raises(TorngitObjectNotFoundError): + await valid_handler.get_pull_request("100") + + get_pull_request_test_data = [ + ( + "1", + { + "base": { + "branch": "main", + "commitid": "b92edba44fdd29fcc506317cc3ddeae1a723dd08", + }, + "head": { + "branch": "second-branch", + "commitid": "3017d534ab41e217bdf34d4c615fb355b0081f4b", + }, + "number": "1", + "id": "1", + "state": "merged", + "title": "Hahaa That is a PR", + "author": { + "id": "9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645", + "username": "ThiagoCodecov", + }, + "merge_commit_sha": "b92edba44fdd", + }, + ) + ] + + @pytest.mark.asyncio + @pytest.mark.parametrize("a,b", get_pull_request_test_data) + async def test_get_pull_request(self, valid_handler, a, b, codecov_vcr): + res = await valid_handler.get_pull_request(a) + assert res["base"] == b["base"] + assert res == b + + @pytest.mark.asyncio + async def test_get_pull_request_commits(self, valid_handler, codecov_vcr): + expected_result = ["3017d534ab41e217bdf34d4c615fb355b0081f4b"] + res = await valid_handler.get_pull_request_commits("1") + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_pull_request_commits_multiple_pages( + self, valid_handler, codecov_vcr + ): + expected_result = [ + "f8a26b4c1bf8eef0bc3aeeb0b23f74f4b96a7d04", + "d3bedda462a79fafe4f5dfdb0ecf710f558e6aab", + "bd666be433ce4123ab0674fc8eb86708d340c31b", + "c80b02c4b65d141f0274ebb13e2a88f22a31820c", + "b3fe71aeb1a405219f4bf58d44ba9a0057072d06", + "2909d0fae30c1d3e628cab1f549e29e1da7b385d", + "3fe51078bb5f6000617d71e32cfde4ebed6f2052", + "974bce36e097868d6eb087656f929dd698d0507e", + "3b2aa7b423369c766173121e8a8bfa2d225ee235", + "f1b9dc07dcd5301c215824d1884816435cf269ea", + "266e6b98f88847c8c4b6e8cf38cf5397266211d3", + "3017d534ab41e217bdf34d4c615fb355b0081f4b", + ] + res = await valid_handler.get_pull_request_commits("1") + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_pull_requests(self, valid_handler, codecov_vcr): + expected_result = [1] + res = await valid_handler.get_pull_requests() + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_commit(self, valid_codecov_handler, codecov_vcr): + commit = await valid_codecov_handler.get_commit("6a45b83") + assert commit == { + "commitid": "6a45b83", + "timestamp": "2015-02-27T03:44:32+00:00", + "message": """wip\n""", + "parents": ["0028015f7fa260f5fd68f78c0deffc15183d955e"], + "author": { + "username": "stevepeak", + "id": "test6y9pl15lzivhmkgsk67k10x53n04i85o", + "name": "stevepeak", + "email": "steve@stevepeak.net", + }, + } + + @pytest.mark.asyncio + async def test_get_commit_no_uuid(self, valid_codecov_handler, codecov_vcr): + commit = await valid_codecov_handler.get_commit("6a45b83") + assert commit == { + "commitid": "6a45b83", + "timestamp": "2015-02-27T03:44:32+00:00", + "message": """wip\n""", + "parents": ["0028015f7fa260f5fd68f78c0deffc15183d955e"], + "author": { + "username": "stevepeak", + "id": "test6y9pl15lzivhmkgsk67k10x53n04i85o", + "name": "stevepeak", + "email": "steve@stevepeak.net", + }, + } + + @pytest.mark.asyncio + async def test_get_commit_not_found(self, valid_handler, codecov_vcr): + with pytest.raises(TorngitObjectNotFoundError): + await valid_handler.get_commit("none") + + @pytest.mark.asyncio + async def test_get_commit_diff(self, valid_handler, codecov_vcr): + expected_result = { + "files": { + "awesome/code_fib.py": { + "type": "new", + "before": None, + "segments": [ + { + "header": ["0", "0", "1", "4"], + "lines": [ + "+def fib(n):", + "+ if n <= 1:", + "+ return 0", + "+ return fib(n - 1) + fib(n - 2)", + ], + } + ], + "stats": {"added": 4, "removed": 0}, + } + } + } + res = await valid_handler.get_commit_diff("3017d53") + assert res["files"] == expected_result["files"] + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_commit_statuses(self, valid_handler, codecov_vcr): + res = await valid_handler.get_commit_statuses("3017d53") + assert res == "success" + + @pytest.mark.asyncio + async def test_get_is_admin(self, valid_handler, codecov_vcr): + valid_handler.data = dict( + owner=dict( + username="ThiagoRRamosworkspace", + service_id="727d78e8-7431-4532-9519-1e5fe2b61d4b", + ) + ) + user = dict(service_id="9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645") + res = await valid_handler.get_is_admin(user) + assert res is True + + @pytest.mark.asyncio + async def test_get_is_admin_not_admin(self, valid_handler, codecov_vcr): + valid_handler.data = dict( + owner=dict( + username="thiagorramostestnumbar3", + service_id="d7c73e87-90ab-450f-bb5f-39e6a5870456", + ) + ) + user = dict(service_id="9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645") + res = await valid_handler.get_is_admin(user) + assert res is False + + @pytest.mark.asyncio + async def test_set_commit_status(self, valid_handler, codecov_vcr): + target_url = "https://localhost:50036/gitlab/codecov/ci-repo?ref=ad798926730aad14aadf72281204bdb85734fe67" + expected_result = { + "key": "codecov-context", + "description": "aaaaaaaaaa", + "repository": { + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python" + }, + "html": { + "href": "https://bitbucket.org/ThiagoCodecov/example-python" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default" + }, + }, + "type": "repository", + "name": "example-python", + "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}", + }, + "url": "https://localhost:50036/gitlab/codecov/ci-repo?ref=ad798926730aad14aadf72281204bdb85734fe67", + "links": { + "commit": { + "href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b" + }, + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b/statuses/build/codecov-context" + }, + }, + "refname": None, + "state": "SUCCESSFUL", + "created_on": "2018-11-07T14:25:50.103547+00:00", + "commit": { + "hash": "3017d534ab41e217bdf34d4c615fb355b0081f4b", + "type": "commit", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/commit/3017d534ab41e217bdf34d4c615fb355b0081f4b" + }, + "html": { + "href": "https://bitbucket.org/ThiagoCodecov/example-python/commits/3017d534ab41e217bdf34d4c615fb355b0081f4b" + }, + }, + }, + "updated_on": "2018-11-07T14:25:50.103583+00:00", + "type": "build", + "name": "Context Coverage", + } + res = await valid_handler.set_commit_status( + "3017d53", "success", "context", "aaaaaaaaaa", target_url + ) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_branches(self, valid_handler, codecov_vcr): + branches = sorted(await valid_handler.get_branches()) + assert list(map(lambda a: a[0], branches)) == [ + "example", + "f/new-branch", + "future", + "main", + "second-branch", + ] + + @pytest.mark.asyncio + async def test_post_webhook(self, valid_handler, codecov_vcr): + url = "http://requestbin.net/r/1ecyaj51" + events = ["repo:push", "issue:created"] + name, secret = "a", "d" + expected_result = { + "read_only": None, + "description": "a", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/hooks/%7B4742f092-8397-4677-8876-5e9a06f10f98%7D" + } + }, + "url": "http://requestbin.net/r/1ecyaj51", + "created_at": "2018-11-07T14:45:47.900077Z", + "skip_cert_verification": False, + "source": None, + "history_enabled": False, + "active": True, + "subject": { + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python" + }, + "html": { + "href": "https://bitbucket.org/ThiagoCodecov/example-python" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default" + }, + }, + "type": "repository", + "name": "example-python", + "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}", + }, + "type": "webhook_subscription", + "events": ["issue:created", "repo:push"], + "uuid": "{4742f092-8397-4677-8876-5e9a06f10f98}", + "id": "4742f092-8397-4677-8876-5e9a06f10f98", + } + res = await valid_handler.post_webhook(name, url, events, secret) + assert res == expected_result + + @pytest.mark.asyncio + async def test_edit_webhook(self, valid_handler, codecov_vcr): + url = "http://requestbin.net/r/1ecyaj51" + events = ["issue:updated"] + new_name, secret = "new_name", "new_secret" + expected_result = { + "read_only": None, + "description": "new_name", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python/hooks/%7B4742f092-8397-4677-8876-5e9a06f10f98%7D" + } + }, + "url": "http://requestbin.net/r/1ecyaj51", + "created_at": "2018-11-07T14:45:47.900077Z", + "skip_cert_verification": False, + "source": None, + "history_enabled": False, + "active": True, + "subject": { + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python" + }, + "html": { + "href": "https://bitbucket.org/ThiagoCodecov/example-python" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default" + }, + }, + "type": "repository", + "name": "example-python", + "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}", + }, + "type": "webhook_subscription", + "events": ["issue:updated"], + "uuid": "{4742f092-8397-4677-8876-5e9a06f10f98}", + "id": "4742f092-8397-4677-8876-5e9a06f10f98", + } + res = await valid_handler.edit_webhook( + "4742f092-8397-4677-8876-5e9a06f10f98", new_name, url, events, secret + ) + assert res == expected_result + + @pytest.mark.asyncio + async def test_delete_webhook(self, valid_handler, codecov_vcr): + res = await valid_handler.delete_webhook("4742f092-8397-4677-8876-5e9a06f10f98") + assert res is True + + @pytest.mark.asyncio + async def test_delete_webhook_not_found(self, valid_handler, codecov_vcr): + with pytest.raises(TorngitObjectNotFoundError): + await valid_handler.delete_webhook("4742f011-8397-aa77-8876-5e9a06f10f98") + + @pytest.mark.asyncio + async def test_get_authenticated(self, valid_handler, codecov_vcr): + res = await valid_handler.get_authenticated() + # This needs to be True/True because ThiagoCodecov owns the repo ThiagoCodecov/example-python + assert res == (True, True) + + @pytest.mark.asyncio + async def test_get_authenticated_no_edit_permission( + self, valid_handler, codecov_vcr + ): + valid_handler.data["repo"] = {"name": "stash-example-plugin"} + valid_handler.data["owner"]["username"] = "atlassian" + res = await valid_handler.get_authenticated() + # This needs to be True/False because ThiagoCodecov has nothing to do with this repo + assert res == (True, False) + + @pytest.mark.asyncio + async def test_get_compare(self, valid_handler, codecov_vcr): + base, head = "6ae5f17", "b92edba" + expected_result = { + "diff": { + "files": { + "README.rst": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["11", "3", "11", "4"], + "lines": [ + " ", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + " ", + ], + }, + { + "header": ["48", "3", "49", "3"], + "lines": [ + " ", + "-We highly suggest adding `source` to your ``.coveragerc`` which solves a number of issues collecting coverage.", + "+We highly suggest adding ``source`` to your ``.coveragerc``, which solves a number of issues collecting coverage.", + " ", + ], + }, + { + "header": ["54", "2", "55", "9"], + "lines": [ + " source=your_package_name", + "+ ", + "+If there are multiple sources, you instead should add ``include`` to your ``.coveragerc``", + "+", + "+.. code-block:: ini", + "+", + "+ [run]", + "+ include=your_package_name/*", + " ", + ], + }, + { + "header": ["152", "3", "160", "2"], + "lines": [ + " ", + "-We are happy to help if you have any questions. Please contact email our Support at [support@codecov.io](mailto:support@codecov.io)", + "-", + "+We are happy to help if you have any questions. Please contact email our Support at `support@codecov.io `_.", + ], + }, + ], + "stats": {"added": 11, "removed": 4}, + } + } + }, + "commits": [{"commitid": "b92edba"}, {"commitid": "6ae5f17"}], + } + res = await valid_handler.get_compare(base, head) + assert sorted(list(res.keys())) == sorted(list(expected_result.keys())) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_compare_same_commit(self, valid_handler, codecov_vcr): + base, head = "6ae5f17", "6ae5f17" + expected_result = { + "diff": None, + "commits": [{"commitid": "6ae5f17"}, {"commitid": "6ae5f17"}], + } + res = await valid_handler.get_compare(base, head) + assert sorted(list(res.keys())) == sorted(list(expected_result.keys())) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_repository(self, valid_handler, codecov_vcr): + expected_result = { + "owner": { + "service_id": "9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645", + "username": "ThiagoCodecov", + }, + "repo": { + "branch": "main", + "language": None, + "name": "example-python", + "private": True, + "service_id": "a8c50527-2c3a-480e-afe1-7700e2b00074", + }, + } + res = await valid_handler.get_repository() + assert res["repo"] == expected_result["repo"] + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_source_master(self, valid_handler, codecov_vcr): + expected_result = { + "commitid": None, + "content": b"from kaploft import smile, fib\n\n\ndef test_something():\n assert smile() == ':)'\n\n\ndef test_fib():\n assert fib(1) == 1\n\n\ndef test_fib_second():\n assert fib(3) == 3\n", + } + path, ref = "tests/test_k.py", "master" + res = await valid_handler.get_source(path, ref) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_source_random_commit(self, valid_handler, codecov_vcr): + expected_result = { + "commitid": None, + "content": b'def smile():\n return ":)"\n\ndef frown():\n return ":("\n', + } + path, ref = "awesome/__init__.py", "96492d409fc86aa7ae31b214dfe6b08ae860458a" + res = await valid_handler.get_source(path, ref) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_source_random_commit_not_found(self, valid_handler, codecov_vcr): + path, ref = ( + "awesome/non_exising_file.py", + "96492d409fc86aa7ae31b214dfe6b08ae860458a", + ) + with pytest.raises(TorngitObjectNotFoundError): + await valid_handler.get_source(path, ref) + + @pytest.mark.asyncio + async def test_list_repos(self, valid_handler, codecov_vcr): + expected_result = [ + { + "repo": { + "name": "ci-repo", + "language": None, + "branch": "main", + "service_id": "a980e378-088f-48a8-9850-98923f497546", + "private": False, + }, + "owner": { + "username": "codecov", + "service_id": "6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49", + }, + }, + { + "repo": { + "name": "private", + "language": "python", + "branch": "main", + "service_id": "3edf54ab-cfe4-4049-aa70-5eb9f69f60d4", + "private": True, + }, + "owner": { + "username": "codecov", + "service_id": "6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49", + }, + }, + { + "repo": { + "name": "coverage.py", + "language": "python", + "branch": "main", + "service_id": "d08f4587-489f-4b55-abad-3d4f396d9862", + "private": False, + }, + "owner": { + "username": "codecov", + "service_id": "6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49", + }, + }, + { + "repo": { + "name": "integration-test-repo", + "language": "python", + "branch": "main", + "service_id": "4fab7a33-92dd-450b-8d12-ea1ab7816300", + "private": True, + }, + "owner": { + "username": "codecov", + "service_id": "6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49", + }, + }, + { + "repo": { + "name": "test-bb-integration-public", + "language": None, + "branch": "main", + "service_id": "2e219352-777c-4e2b-9a16-71211fbd4d93", + "private": False, + }, + "owner": { + "username": "codecov", + "service_id": "6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49", + }, + }, + ] + + res = await valid_handler.list_repos("codecov") + assert sorted(res, key=lambda x: x["repo"]["service_id"]) == sorted( + expected_result, key=lambda x: x["repo"]["service_id"] + ) + + @pytest.mark.asyncio + @vcr.use_cassette( + "tests/integration/cassetes/test_bitbucket/TestBitbucketTestCase/test_list_repos.yaml", + record_mode="none", + filter_headers=["authorization"], + match_on=["method", "scheme", "host", "port", "path", "query"], + filter_query_parameters=["oauth_nonce", "oauth_timestamp", "oauth_signature"], + ) + async def test_list_repos_generator(self, valid_handler, codecov_vcr): + expected_result = [ + { + "repo": { + "name": "ci-repo", + "language": None, + "branch": "main", + "service_id": "a980e378-088f-48a8-9850-98923f497546", + "private": False, + }, + "owner": { + "username": "codecov", + "service_id": "6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49", + }, + }, + { + "repo": { + "name": "private", + "language": "python", + "branch": "main", + "service_id": "3edf54ab-cfe4-4049-aa70-5eb9f69f60d4", + "private": True, + }, + "owner": { + "username": "codecov", + "service_id": "6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49", + }, + }, + { + "repo": { + "name": "coverage.py", + "language": "python", + "branch": "main", + "service_id": "d08f4587-489f-4b55-abad-3d4f396d9862", + "private": False, + }, + "owner": { + "username": "codecov", + "service_id": "6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49", + }, + }, + { + "repo": { + "name": "integration-test-repo", + "language": "python", + "branch": "main", + "service_id": "4fab7a33-92dd-450b-8d12-ea1ab7816300", + "private": True, + }, + "owner": { + "username": "codecov", + "service_id": "6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49", + }, + }, + { + "repo": { + "name": "test-bb-integration-public", + "language": None, + "branch": "main", + "service_id": "2e219352-777c-4e2b-9a16-71211fbd4d93", + "private": False, + }, + "owner": { + "username": "codecov", + "service_id": "6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49", + }, + }, + ] + + repos = [] + page_count = 0 + async for page in valid_handler.list_repos_generator("codecov"): + repos.extend(page) + page_count += 1 + + assert page_count == 1 + assert sorted(repos, key=lambda x: x["repo"]["service_id"]) == sorted( + expected_result, key=lambda x: x["repo"]["service_id"] + ) + + @pytest.mark.asyncio + async def test_list_permissions(self, valid_handler, codecov_vcr): + expected_result = [ + { + "type": "repository_permission", + "user": { + "display_name": "Thiago Ramos", + "account_id": "5bce04c759d0e84f8c7555e9", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D" + }, + "html": { + "href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/" + }, + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png" + }, + }, + "type": "user", + "nickname": "thiago", + "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + }, + "repository": { + "full_name": "codecov/ci-repo", + "type": "repository", + "name": "ci-repo", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/codecov/ci-repo" + }, + "html": {"href": "https://bitbucket.org/codecov/ci-repo"}, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Ba980e378-088f-48a8-9850-98923f497546%7D?ts=default" + }, + }, + "uuid": "{a980e378-088f-48a8-9850-98923f497546}", + }, + "permission": "admin", + }, + { + "type": "repository_permission", + "user": { + "display_name": "Thiago Ramos", + "account_id": "5bce04c759d0e84f8c7555e9", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D" + }, + "html": { + "href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/" + }, + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png" + }, + }, + "type": "user", + "nickname": "thiago", + "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + }, + "repository": { + "full_name": "codecov/private", + "type": "repository", + "name": "private", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/codecov/private" + }, + "html": {"href": "https://bitbucket.org/codecov/private"}, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B3edf54ab-cfe4-4049-aa70-5eb9f69f60d4%7D?ts=python" + }, + }, + "uuid": "{3edf54ab-cfe4-4049-aa70-5eb9f69f60d4}", + }, + "permission": "admin", + }, + { + "type": "repository_permission", + "user": { + "display_name": "Thiago Ramos", + "account_id": "5bce04c759d0e84f8c7555e9", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D" + }, + "html": { + "href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/" + }, + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png" + }, + }, + "type": "user", + "nickname": "thiago", + "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + }, + "repository": { + "full_name": "codecov/coverage.py", + "type": "repository", + "name": "coverage.py", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/codecov/coverage.py" + }, + "html": {"href": "https://bitbucket.org/codecov/coverage.py"}, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bd08f4587-489f-4b55-abad-3d4f396d9862%7D?ts=python" + }, + }, + "uuid": "{d08f4587-489f-4b55-abad-3d4f396d9862}", + }, + "permission": "admin", + }, + { + "type": "repository_permission", + "user": { + "display_name": "Thiago Ramos", + "account_id": "5bce04c759d0e84f8c7555e9", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D" + }, + "html": { + "href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/" + }, + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png" + }, + }, + "type": "user", + "nickname": "thiago", + "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + }, + "repository": { + "full_name": "ThiagoCodecov/example-python", + "type": "repository", + "name": "example-python", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/ThiagoCodecov/example-python" + }, + "html": { + "href": "https://bitbucket.org/ThiagoCodecov/example-python" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Ba8c50527-2c3a-480e-afe1-7700e2b00074%7D?ts=default" + }, + }, + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}", + }, + "permission": "admin", + }, + { + "type": "repository_permission", + "user": { + "display_name": "Thiago Ramos", + "account_id": "5bce04c759d0e84f8c7555e9", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D" + }, + "html": { + "href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/" + }, + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png" + }, + }, + "type": "user", + "nickname": "thiago", + "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + }, + "repository": { + "full_name": "codecov/integration-test-repo", + "type": "repository", + "name": "integration-test-repo", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/codecov/integration-test-repo" + }, + "html": { + "href": "https://bitbucket.org/codecov/integration-test-repo" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B4fab7a33-92dd-450b-8d12-ea1ab7816300%7D?ts=python" + }, + }, + "uuid": "{4fab7a33-92dd-450b-8d12-ea1ab7816300}", + }, + "permission": "admin", + }, + { + "type": "repository_permission", + "user": { + "display_name": "Thiago Ramos", + "account_id": "5bce04c759d0e84f8c7555e9", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D" + }, + "html": { + "href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/" + }, + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png" + }, + }, + "type": "user", + "nickname": "thiago", + "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + }, + "repository": { + "full_name": "codecov/test-bb-integration-public", + "type": "repository", + "name": "test-bb-integration-public", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/codecov/test-bb-integration-public" + }, + "html": { + "href": "https://bitbucket.org/codecov/test-bb-integration-public" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B2e219352-777c-4e2b-9a16-71211fbd4d93%7D?ts=markdown" + }, + }, + "uuid": "{2e219352-777c-4e2b-9a16-71211fbd4d93}", + }, + "permission": "admin", + }, + { + "type": "repository_permission", + "user": { + "display_name": "Thiago Ramos", + "account_id": "5bce04c759d0e84f8c7555e9", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/users/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D" + }, + "html": { + "href": "https://bitbucket.org/%7B9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645%7D/" + }, + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/TR-6.png" + }, + }, + "type": "user", + "nickname": "thiago", + "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + }, + "repository": { + "full_name": "codecov/test-private-repo-2", + "type": "repository", + "name": "test-private-repo-2", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/codecov/test-private-repo-2" + }, + "html": { + "href": "https://bitbucket.org/codecov/test-private-repo-2" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bd215b8f1-b862-4fae-9bc8-c2c8ea2e1a70%7D?ts=python" + }, + }, + "uuid": "{d215b8f1-b862-4fae-9bc8-c2c8ea2e1a70}", + }, + "permission": "admin", + }, + ] + res = await valid_handler.list_permissions() + assert res == expected_result + + @pytest.mark.asyncio + async def test_list_repos_no_username(self, valid_handler, codecov_vcr): + expected_result = [ + { + "repo": { + "name": "example-python", + "language": None, + "branch": "main", + "service_id": "a8c50527-2c3a-480e-afe1-7700e2b00074", + "private": True, + }, + "owner": { + "username": "ThiagoCodecov", + "service_id": "9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645", + }, + } + ] + res = await valid_handler.list_repos() + assert sorted(res, key=lambda x: x["repo"]["service_id"]) == sorted( + expected_result, key=lambda x: x["repo"]["service_id"] + ) + + @pytest.mark.asyncio + async def test_list_teams(self, valid_handler, codecov_vcr): + expected_result = [ + { + "name": "thiagorramostestnumbar3", + "id": "d7c73e87-90ab-450f-bb5f-39e6a5870456", + "email": None, + "username": "thiagorramostestnumbar3", + }, + { + "name": "ThiagoRRamostest2", + "id": "33b5f87a-bda0-40c2-ba1b-9eb892492290", + "email": None, + "username": "thiagorramostest2", + }, + { + "name": "ThiagoRRamosanotherw", + "id": "11e04628-2c7b-4d89-9319-e7eed8818e56", + "email": None, + "username": "thiagorramosanotherw", + }, + { + "name": "ThiagoRRamosworkspace", + "id": "727d78e8-7431-4532-9519-1e5fe2b61d4b", + "email": None, + "username": "thiagorramosworkspace", + }, + { + "name": "ThiagoCodecovbanana", + "id": "68f2da06-b2f8-4f00-92fa-32bd60df9d27", + "email": None, + "username": "thiagocodecovbanana", + }, + { + "name": "Thiago Ramos", + "id": "9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645", + "email": None, + "username": "ThiagoCodecov", + }, + ] + res = await valid_handler.list_teams() + assert res == expected_result + + @pytest.mark.asyncio + async def test_list_top_level_files_multiple_pages( + self, valid_handler, codecov_vcr + ): + expected_result = [ + {"path": "awesome", "type": "folder"}, + {"path": "kaploft", "type": "folder"}, + {"path": "tests", "type": "folder"}, + {"path": ".coverage", "type": "file"}, + {"path": ".gitignore", "type": "file"}, + {"path": "README.rst", "type": "file"}, + {"path": "__init__.py", "type": "file"}, + {"path": "a1.txt", "type": "file"}, + {"path": "a10.txt", "type": "file"}, + {"path": "a11.txt", "type": "file"}, + {"path": "a2.txt", "type": "file"}, + {"path": "a3.txt", "type": "file"}, + {"path": "a4.txt", "type": "file"}, + {"path": "a5.txt", "type": "file"}, + {"path": "a6.txt", "type": "file"}, + {"path": "a7.txt", "type": "file"}, + {"path": "a8.txt", "type": "file"}, + {"path": "a9.txt", "type": "file"}, + {"path": "bitbucket-pipelines.yml", "type": "file"}, + {"path": "coverage.xml", "type": "file"}, + {"path": "filet2.py", "type": "file"}, + {"path": "requirements.txt", "type": "file"}, + ] + + res = await valid_handler.list_top_level_files("second-branch") + assert sorted(res, key=lambda x: x["path"]) == sorted( + expected_result, key=lambda x: x["path"] + ) + + @pytest.mark.asyncio + async def test_list_files(self, valid_handler, codecov_vcr): + expected_result = [ + {"path": "tests/__pycache__", "type": "folder"}, + {"path": "tests/__init__.py", "type": "file"}, + {"path": "tests/test_k.py", "type": "file"}, + ] + res = await valid_handler.list_files("second-branch", "tests") + assert sorted(res, key=lambda x: x["path"]) == sorted( + expected_result, key=lambda x: x["path"] + ) + + @pytest.mark.asyncio + async def test_get_ancestors_tree(self, valid_handler, codecov_vcr): + commitid = "6ae5f17" + res = await valid_handler.get_ancestors_tree(commitid) + assert res["commitid"] == "6ae5f1795a441884ed2847bb31154814ac01ef38" + assert sorted([x["commitid"] for x in res["parents"]]) == [ + "8631ea09b9b689de0a348d5abf70bdd7273d2ae3" + ] + + def test_get_href(self, valid_handler): + expected_result = "https://bitbucket.org/ThiagoCodecov/example-python/commits/8631ea09b9b689de0a348d5abf70bdd7273d2ae3" + res = valid_handler.get_href( + Endpoints.commit_detail, commitid="8631ea09b9b689de0a348d5abf70bdd7273d2ae3" + ) + assert res == expected_result + + @pytest.mark.asyncio + async def test_is_student(self, valid_handler, codecov_vcr): + res = await valid_handler.is_student() + assert not res diff --git a/libs/shared/tests/integration/test_bitbucket_server.py b/libs/shared/tests/integration/test_bitbucket_server.py new file mode 100644 index 0000000000..52a003ec2d --- /dev/null +++ b/libs/shared/tests/integration/test_bitbucket_server.py @@ -0,0 +1,330 @@ +import pytest + +from shared.torngit.bitbucket_server import BitbucketServer + + +def valid_handler(): + return BitbucketServer( + repo=dict(name="python-standard"), + owner=dict(username="TEST"), + oauth_consumer_token=dict(key=""), + token=dict(secret="", key=""), + ) + + +class TestBitbucketTestCase(object): + @pytest.mark.asyncio + async def test_find_pull_request_found(self, mocker): + api_result = { + "size": 1, + "limit": 25, + "isLastPage": True, + "values": [ + { + "id": 3, + "version": 9, + "title": "brand-new-branch-1591913005", + "state": "OPEN", + "open": True, + "closed": False, + "createdDate": 1591913044441, + "updatedDate": 1591913541017, + "fromRef": { + "id": "refs/heads/brand-new-branch", + "displayId": "brand-new-branch", + "latestCommit": "86be80adfc64355e523c38ef9b9bab7408c173e3", + "repository": { + "slug": "python-standard", + "id": 1, + "name": "python-standard", + "hierarchyId": "0199083afd9cd1cffafe", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": False, + "project": { + "key": "TEST", + "id": 1, + "name": "Test", + "public": False, + "type": "NORMAL", + "links": { + "self": [ + { + "href": "https://bitbucket-server.codecov.dev:8443/projects/TEST" + } + ] + }, + }, + "public": False, + "links": { + "clone": [ + { + "href": "ssh://git@bitbucket-server.codecov.dev:7999/test/python-standard.git", + "name": "ssh", + }, + { + "href": "https://bitbucket-server.codecov.dev:8443/scm/test/python-standard.git", + "name": "http", + }, + ], + "self": [ + { + "href": "https://bitbucket-server.codecov.dev:8443/projects/TEST/repos/python-standard/browse" + } + ], + }, + }, + }, + "toRef": { + "id": "refs/heads/main", + "displayId": "main", + "latestCommit": "f3d4a16b651356d9599bd634f6be868508f81f99", + "repository": { + "slug": "python-standard", + "id": 1, + "name": "python-standard", + "hierarchyId": "0199083afd9cd1cffafe", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": False, + "project": { + "key": "TEST", + "id": 1, + "name": "Test", + "public": False, + "type": "NORMAL", + "links": { + "self": [ + { + "href": "https://bitbucket-server.codecov.dev:8443/projects/TEST" + } + ] + }, + }, + "public": False, + "links": { + "clone": [ + { + "href": "ssh://git@bitbucket-server.codecov.dev:7999/test/python-standard.git", + "name": "ssh", + }, + { + "href": "https://bitbucket-server.codecov.dev:8443/scm/test/python-standard.git", + "name": "http", + }, + ], + "self": [ + { + "href": "https://bitbucket-server.codecov.dev:8443/projects/TEST/repos/python-standard/browse" + } + ], + }, + }, + }, + "locked": False, + "author": { + "user": { + "name": "bbsadmin", + "emailAddress": "edward@codecov.io", + "id": 1, + "displayName": "BBS Admin", + "active": True, + "slug": "bbsadmin", + "type": "NORMAL", + "links": { + "self": [ + { + "href": "https://bitbucket-server.codecov.dev:8443/users/bbsadmin" + } + ] + }, + }, + "role": "AUTHOR", + "approved": False, + "status": "UNAPPROVED", + }, + "reviewers": [], + "participants": [], + "links": { + "self": [ + { + "href": "https://bitbucket-server.codecov.dev:8443/projects/TEST/repos/python-standard/pull-requests/3" + } + ] + }, + } + ], + "start": 0, + } + mocker.patch.object(BitbucketServer, "api", return_value=api_result) + res = await valid_handler().find_pull_request( + "86be80adfc64355e523c38ef9b9bab7408c173e3", "brand-new-branch" + ) + assert res == 3 + + @pytest.mark.asyncio + async def test_find_pull_request_nothing_found(self, mocker): + api_result = { + "size": 0, + "limit": 25, + "isLastPage": True, + "values": [], + "start": 0, + } + mocker.patch.object(BitbucketServer, "api", return_value=api_result) + assert await valid_handler().find_pull_request("a" * 40, "no-branch") is None + + @pytest.mark.asyncio + async def test_list_top_level_files(self, mocker): + api_result = { + "values": [ + ".gitignore", + ".travis.yml", + "Dockerfile", + "README.md", + "codecov.yml", + "coverage.xml", + "docker-compose.yml", + "entrypoint.sh", + "src/index.py", + "src/request.py", + "src/test_index.py", + ], + "size": 11, + "isLastPage": True, + "start": 0, + "limit": 25, + "nextPageStart": None, + } + mocker.patch.object(BitbucketServer, "api", return_value=api_result) + files = await valid_handler().list_top_level_files("ref", "") + assert len(files) == 11 + + @pytest.mark.asyncio + async def test_diff_to_json(self, mocker): + diff = [ + { + "source": None, + "destination": { + "components": [ + "src", + "__pycache__", + "test_index.cpython-36-PYTEST.pyc", + ], + "parent": "src/__pycache__", + "name": "test_index.cpython-36-PYTEST.pyc", + "extension": "pyc", + "toString": "src/__pycache__/test_index.cpython-36-PYTEST.pyc", + }, + "binary": True, + }, + { + "source": { + "components": ["src", "index.py"], + "parent": "src", + "name": "index.py", + "extension": "py", + "toString": "src/index.py", + }, + "destination": { + "components": ["src", "index.py"], + "parent": "src", + "name": "index.py", + "extension": "py", + "toString": "src/index.py", + }, + "hunks": [ + { + "sourceLine": 1, + "sourceSpan": 10, + "destinationLine": 1, + "destinationSpan": 11, + "segments": [ + { + "type": "ADDED", + "lines": [ + { + "source": 1, + "destination": 1, + "line": "import asyncio", + "truncated": False, + } + ], + "truncated": False, + }, + { + "type": "CONTEXT", + "lines": [ + { + "source": 1, + "destination": 2, + "line": "def uncovered_if(var=True):", + "truncated": False, + }, + { + "source": 2, + "destination": 3, + "line": " if var:", + "truncated": False, + }, + { + "source": 3, + "destination": 4, + "line": " return False", + "truncated": False, + }, + { + "source": 4, + "destination": 5, + "line": " else:", + "truncated": False, + }, + { + "source": 5, + "destination": 6, + "line": " return True", + "truncated": False, + }, + { + "source": 6, + "destination": 7, + "line": "", + "truncated": False, + }, + { + "source": 7, + "destination": 8, + "line": "def fully_covered():", + "truncated": False, + }, + { + "source": 8, + "destination": 9, + "line": " return True;", + "truncated": False, + }, + { + "source": 9, + "destination": 10, + "line": "", + "truncated": False, + }, + { + "source": 10, + "destination": 11, + "line": "def uncovered():", + "truncated": False, + }, + ], + "truncated": False, + }, + ], + "truncated": False, + } + ], + "truncated": False, + }, + ] + assert valid_handler().diff_to_json(diff) is not None diff --git a/libs/shared/tests/integration/test_bots.py b/libs/shared/tests/integration/test_bots.py new file mode 100644 index 0000000000..8d36b2b559 --- /dev/null +++ b/libs/shared/tests/integration/test_bots.py @@ -0,0 +1,76 @@ +import pytest + +from shared.bots.exceptions import RepositoryWithoutValidBotError +from shared.bots.github_apps import get_github_app_info_for_owner +from shared.bots.repo_bots import get_repo_appropriate_bot_token +from shared.django_apps.codecov_auth.models import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, +) +from shared.django_apps.core.tests.factories import RepositoryFactory +from shared.github import InvalidInstallationError + +fake_private_key = """-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDCFqq2ygFh9UQU/6PoDJ6L9e4ovLPCHtlBt7vzDwyfwr3XGxln +0VbfycVLc6unJDVEGZ/PsFEuS9j1QmBTTEgvCLR6RGpfzmVuMO8wGVEO52pH73h9 +rviojaheX/u3ZqaA0di9RKy8e3L+T0ka3QYgDx5wiOIUu1wGXCs6PhrtEwICBAEC +gYBu9jsi0eVROozSz5dmcZxUAzv7USiUcYrxX007SUpm0zzUY+kPpWLeWWEPaddF +VONCp//0XU8hNhoh0gedw7ZgUTG6jYVOdGlaV95LhgY6yXaQGoKSQNNTY+ZZVT61 +zvHOlPynt3GZcaRJOlgf+3hBF5MCRoWKf+lDA5KiWkqOYQJBAMQp0HNVeTqz+E0O +6E0neqQDQb95thFmmCI7Kgg4PvkS5mz7iAbZa5pab3VuyfmvnVvYLWejOwuYSp0U +9N8QvUsCQQD9StWHaVNM4Lf5zJnB1+lJPTXQsmsuzWvF3HmBkMHYWdy84N/TdCZX +Cxve1LR37lM/Vijer0K77wAx2RAN/ppZAkB8+GwSh5+mxZKydyPaPN29p6nC6aLx +3DV2dpzmhD0ZDwmuk8GN+qc0YRNOzzJ/2UbHH9L/lvGqui8I6WLOi8nDAkEA9CYq +ewfdZ9LcytGz7QwPEeWVhvpm0HQV9moetFWVolYecqBP4QzNyokVnpeUOqhIQAwe +Z0FJEQ9VWsG+Df0noQJBALFjUUZEtv4x31gMlV24oiSWHxIRX4fEND/6LpjleDZ5 +C/tY+lZIEO1Gg/FxSMB+hwwhwfSuE3WohZfEcSy+R48= +-----END RSA PRIVATE KEY-----""" + + +class TestRepositoryServiceIntegration(object): + @pytest.mark.django_db(databases={"default"}) + def test_get_token_type_mapping_non_existing_integration( + self, codecov_vcr, mock_configuration, mocker + ): + # this test was done with valid integration_id, pem and then the data was scrubbed + mocker.patch("shared.github.get_pem", return_value=fake_private_key) + mock_configuration._params = {"github": {"integration": {"id": 123}}} + repo = RepositoryFactory( + author__username="ThiagoCodecov", + author__service="github", + author__integration_id=5944641, + name="example-python", + using_integration=True, + private=True, + author__oauth_token=None, + ) + repo.save() + with pytest.raises(RepositoryWithoutValidBotError): + get_repo_appropriate_bot_token(repo) + + @pytest.mark.django_db(databases={"default"}) + def test_get_token_type_mapping_bad_data( + self, codecov_vcr, mock_configuration, mocker + ): + mocker.patch("shared.github.get_pem", return_value=fake_private_key) + mock_configuration._params = {"github": {"integration": {"id": 999}}} + repo = RepositoryFactory( + author__username="ThiagoCodecov", + author__service="github", + author__integration_id=None, + name="example-python", + using_integration=False, + ) + repo.save() + app = GithubAppInstallation( + repository_service_ids=None, + installation_id=5944641, + app_id=999, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + owner=repo.author, + ) + app.save() + assert list(repo.author.github_app_installations.all()) == [app] + with pytest.raises(InvalidInstallationError): + info = get_github_app_info_for_owner(repo.author) + get_repo_appropriate_bot_token(repo, info[0]) diff --git a/libs/shared/tests/integration/test_github.py b/libs/shared/tests/integration/test_github.py new file mode 100644 index 0000000000..d4f662daaf --- /dev/null +++ b/libs/shared/tests/integration/test_github.py @@ -0,0 +1,1963 @@ +import pytest + +from shared.torngit.enums import Endpoints +from shared.torngit.exceptions import ( + TorngitClientGeneralError, + TorngitObjectNotFoundError, + TorngitRepoNotFoundError, +) +from shared.torngit.github import Github + +# This is a fake key +fake_private_key = """-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDCFqq2ygFh9UQU/6PoDJ6L9e4ovLPCHtlBt7vzDwyfwr3XGxln +0VbfycVLc6unJDVEGZ/PsFEuS9j1QmBTTEgvCLR6RGpfzmVuMO8wGVEO52pH73h9 +rviojaheX/u3ZqaA0di9RKy8e3L+T0ka3QYgDx5wiOIUu1wGXCs6PhrtEwICBAEC +gYBu9jsi0eVROozSz5dmcZxUAzv7USiUcYrxX007SUpm0zzUY+kPpWLeWWEPaddF +VONCp//0XU8hNhoh0gedw7ZgUTG6jYVOdGlaV95LhgY6yXaQGoKSQNNTY+ZZVT61 +zvHOlPynt3GZcaRJOlgf+3hBF5MCRoWKf+lDA5KiWkqOYQJBAMQp0HNVeTqz+E0O +6E0neqQDQb95thFmmCI7Kgg4PvkS5mz7iAbZa5pab3VuyfmvnVvYLWejOwuYSp0U +9N8QvUsCQQD9StWHaVNM4Lf5zJnB1+lJPTXQsmsuzWvF3HmBkMHYWdy84N/TdCZX +Cxve1LR37lM/Vijer0K77wAx2RAN/ppZAkB8+GwSh5+mxZKydyPaPN29p6nC6aLx +3DV2dpzmhD0ZDwmuk8GN+qc0YRNOzzJ/2UbHH9L/lvGqui8I6WLOi8nDAkEA9CYq +ewfdZ9LcytGz7QwPEeWVhvpm0HQV9moetFWVolYecqBP4QzNyokVnpeUOqhIQAwe +Z0FJEQ9VWsG+Df0noQJBALFjUUZEtv4x31gMlV24oiSWHxIRX4fEND/6LpjleDZ5 +C/tY+lZIEO1Gg/FxSMB+hwwhwfSuE3WohZfEcSy+R48= +-----END RSA PRIVATE KEY-----""" + + +@pytest.fixture +def valid_handler(): + return Github( + repo=dict(name="example-python"), + owner=dict(username="ThiagoCodecov"), + token=dict(key=10 * "a280"), + oauth_consumer_token=dict(key=None, secret=None, refresh_token=None), + on_token_refresh=lambda token: token, + ) + + +@pytest.fixture +def generic_valid_handler(): + # TODO replace all occurences of valid_handler (above) by this handler. + return Github( + repo=dict(name="example-python"), + owner=dict(username="codecove2e"), + token=dict(key=10 * "a280", refresh_token=10 * "a180"), + on_token_refresh=lambda token: token, + ) + + +@pytest.fixture +def valid_repo_no_languages(): + return Github( + repo=dict(name="test-no-languages"), + owner=dict(username="codecove2e"), + token=dict(key=10 * "a280", refresh_token=10 * "a180"), + on_token_refresh=lambda token: token, + ) + + +@pytest.fixture +def valid_but_no_permissions_handler(): + return Github( + repo=dict(name="worker"), + owner=dict(username="codecov"), + token=dict(key=10 * "a280"), # ThiagoCodecovTester + ) + + +@pytest.fixture +def repo_doesnt_exist_handler(): + return Github( + repo=dict(name="badrepo"), + owner=dict(username="codecov"), + token=dict(key=10 * "8a85"), + ) + + +@pytest.fixture +def more_complex_handler(): + return Github( + repo=dict(name="worker"), + owner=dict(username="codecov"), + token=dict(key=10 * "8a85"), + ) + + +@pytest.fixture +def student_app_capable_not_student_handler(): + # we need a token generated by an app that is whitelisted by github + # ie, our production app. This below is fake + return Github( + repo=dict(name="worker"), + owner=dict(username="codecov"), + token=dict(key="a" * 40), + ) + + +@pytest.fixture +def student_app_capable_yes_student_handler(): + # we need a token generated by an app that is whitelisted by github + # ie, our production app. This below is fake + return Github( + repo=dict(name="worker"), + owner=dict(username="codecov"), + token=dict(key="b" * 40), + ) + + +@pytest.fixture +def integration_installed_handler(): + return Github( + repo=dict(name="example-python"), + owner=dict(username="ThiagoCodecov"), + token=dict(key="v1.testfa2yxpxi79hyhqctum1vscqtaiy2dtv3d23e"), + ) + + +class TestGithubTestCase(object): + @pytest.mark.asyncio + async def test_get_authenticated_user(self, codecov_vcr): + # To regenerate this test, go to + # https://github.com/login/oauth/authorize?response_type=code&scope=user%3Aemail%2Cread%3Aorg%2Crepo%3Astatus%2Cwrite%3Arepo_hook&client_id=999247146557c3ba045c + # get the code and paste it here below + code = "dc38acf492b071cc4dce" + handler = Github( + oauth_consumer_token=dict( + key="999247146557c3ba045c", + secret="testo8lnq6ihj7zsf896r15yxujnl06og9o0fqiu", + ) + ) + res = await handler.get_authenticated_user(code) + assert res == { + "login": "ThiagoCodecov", + "id": 44376991, + "node_id": "MDQ6VXNlcjQ0Mzc2OTkx", + "avatar_url": "https://avatars3.githubusercontent.com/u/44376991?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ThiagoCodecov", + "html_url": "https://github.com/ThiagoCodecov", + "followers_url": "https://api.github.com/users/ThiagoCodecov/followers", + "following_url": "https://api.github.com/users/ThiagoCodecov/following{/other_user}", + "gists_url": "https://api.github.com/users/ThiagoCodecov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ThiagoCodecov/subscriptions", + "organizations_url": "https://api.github.com/users/ThiagoCodecov/orgs", + "repos_url": "https://api.github.com/users/ThiagoCodecov/repos", + "events_url": "https://api.github.com/users/ThiagoCodecov/events{/privacy}", + "received_events_url": "https://api.github.com/users/ThiagoCodecov/received_events", + "type": "User", + "site_admin": False, + "name": "Thiago", + "company": "@codecov ", + "blog": "", + "location": None, + "email": None, + "hireable": None, + "bio": None, + "twitter_username": None, + "public_repos": 3, + "public_gists": 0, + "followers": 0, + "following": 0, + "created_at": "2018-10-22T17:51:44Z", + "updated_at": "2020-10-14T17:58:13Z", + "access_token": "testw5efy5qccduniyucsk5tesu08s4640xtoymv", + "refresh_token": "testblahblahblahblahsfas", + "token_type": "bearer", + "scope": "read:org,repo:status,user:email,write:repo_hook", + } + + @pytest.mark.asyncio + async def test_get_authenticated_user_with_public_emails(self, codecov_vcr): + code = "71367b9f258ca9a60c44" + + handler = Github( + oauth_consumer_token=dict( + key="Iv23liSqj8DAO20A3KLA", + secret="a6a6397fffea369e54495c88ca469d988ea4ccd2", + ) + ) + res = await handler.get_authenticated_user(code) + assert res["email"] == "rola.abuhasna@sentry.io" + + @pytest.mark.asyncio + async def test_get_authenticated_user_no_refresh_token(self, codecov_vcr): + # To regenerate this test, go to + # https://github.com/login/oauth/authorize?response_type=code&scope=user%3Aemail%2Cread%3Aorg%2Crepo%3Astatus%2Cwrite%3Arepo_hook&client_id=999247146557c3ba045c + # get the code and paste it here below + code = "dc38acf492b071cc4dce" + handler = Github( + oauth_consumer_token=dict( + key="999247146557c3ba045c", + secret="testo8lnq6ihj7zsf896r15yxujnl06og9o0fqiu", + ) + ) + res = await handler.get_authenticated_user(code) + assert res == { + "login": "ThiagoCodecov", + "id": 44376991, + "node_id": "MDQ6VXNlcjQ0Mzc2OTkx", + "avatar_url": "https://avatars3.githubusercontent.com/u/44376991?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ThiagoCodecov", + "html_url": "https://github.com/ThiagoCodecov", + "followers_url": "https://api.github.com/users/ThiagoCodecov/followers", + "following_url": "https://api.github.com/users/ThiagoCodecov/following{/other_user}", + "gists_url": "https://api.github.com/users/ThiagoCodecov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ThiagoCodecov/subscriptions", + "organizations_url": "https://api.github.com/users/ThiagoCodecov/orgs", + "repos_url": "https://api.github.com/users/ThiagoCodecov/repos", + "events_url": "https://api.github.com/users/ThiagoCodecov/events{/privacy}", + "received_events_url": "https://api.github.com/users/ThiagoCodecov/received_events", + "type": "User", + "site_admin": False, + "name": "Thiago", + "company": "@codecov ", + "blog": "", + "location": None, + "email": None, + "hireable": None, + "bio": None, + "twitter_username": None, + "public_repos": 3, + "public_gists": 0, + "followers": 0, + "following": 0, + "created_at": "2018-10-22T17:51:44Z", + "updated_at": "2020-10-14T17:58:13Z", + "access_token": "testw5efy5qccduniyucsk5tesu08s4640xtoymv", + "token_type": "bearer", + "scope": "read:org,repo:status,user:email,write:repo_hook", + } + + @pytest.mark.asyncio + async def test_edit_comment(self, valid_handler, codecov_vcr): + res = await valid_handler.edit_comment( + "1", "436811257", "Hello world numbah 2 my friendo" + ) + assert res is not None + assert res["id"] == 436811257 + assert res["body"] == "Hello world numbah 2 my friendo" + + @pytest.mark.asyncio + async def test_edit_comment_not_found( + self, generic_valid_handler, codecov_vcr, mocker + ): + mock_refresh = mocker.patch.object(Github, "refresh_token", return_value=None) + with pytest.raises(TorngitObjectNotFoundError): + await generic_valid_handler.edit_comment( + "1", "113979999", "Hello world number 2" + ) + mock_refresh.assert_called_once() + + @pytest.mark.asyncio + async def test_delete_comment(self, valid_handler, codecov_vcr): + assert await valid_handler.delete_comment("1", "708545249") is True + + @pytest.mark.asyncio + async def test_delete_comment_not_found( + self, generic_valid_handler, codecov_vcr, mocker + ): + mock_refresh = mocker.patch.object(Github, "refresh_token", return_value=None) + with pytest.raises(TorngitObjectNotFoundError): + await generic_valid_handler.delete_comment("1", 113977999) + mock_refresh.assert_called_once() + + @pytest.mark.asyncio + async def test_find_pull_request_nothing_found( + self, generic_valid_handler, codecov_vcr + ): + assert ( + await generic_valid_handler.find_pull_request("a" * 40, "no-branch") is None + ) + + @pytest.mark.asyncio + async def test_find_pull_request_one_found( + self, generic_valid_handler, codecov_vcr + ): + # PR is https://github.com/codecove2e/example-python/pull/1 + commitid = "bec77909e0a9b04603d2cdddcba62f99592d6579" + assert await generic_valid_handler.find_pull_request(commitid) == 1 + + @pytest.mark.asyncio + async def test_get_pull_request_fail( + self, generic_valid_handler, codecov_vcr, mocker + ): + mock_refresh = mocker.patch.object(Github, "refresh_token", return_value=None) + with pytest.raises(TorngitObjectNotFoundError): + await generic_valid_handler.get_pull_request("100") + mock_refresh.assert_called_once() + + get_pull_request_test_data = [ + ( + "1", + { + "base": { + "branch": "main", + "commitid": "68946ef98daec68c7798459150982fc799c87d85", + "slug": "ThiagoCodecov/example-python", + }, + "head": { + "branch": "reason/some-testing", + "commitid": "119c1907fb266f374b8440bbd70dccbea54daf8f", + "slug": "ThiagoCodecov/example-python", + }, + "number": "1", + "id": "1", + "state": "merged", + "title": "Creating new code for reasons no one knows", + "author": {"id": "44376991", "username": "ThiagoCodecov"}, + "labels": [], + "merge_commit_sha": "038ac8ac2127baa19a927c67f0d5168d9928abf3", + }, + ) + ] + + @pytest.mark.asyncio + async def test_get_pull_request_way_more_than_250_results( + self, valid_handler, codecov_vcr + ): + pull_id = "16" + expected_result = { + "base": { + "branch": "main", + "commitid": "335ec9958daf0242bc8945659bb120c05800eacf", + "slug": "ThiagoCodecov/example-python", + }, + "head": { + "branch": "thiago/f/big-pt", + "commitid": "d55dc4ef748fd11537e50c9abed4ab1864fa1d94", + "slug": "ThiagoCodecov/example-python", + }, + "number": pull_id, + "id": pull_id, + "state": "open", + "title": "PR with more than 250 results", + "author": {"id": "44376991", "username": "ThiagoCodecov"}, + "labels": [], + "merge_commit_sha": None, + } + res = await valid_handler.get_pull_request(pull_id) + assert res == expected_result + + @pytest.mark.asyncio + @pytest.mark.parametrize("a,b", get_pull_request_test_data) + async def test_get_pull_request(self, valid_handler, a, b, codecov_vcr): + res = await valid_handler.get_pull_request(a) + assert res == b + + @pytest.mark.asyncio + async def test_get_pull_request_commits(self, valid_handler, codecov_vcr): + expected_result = [ + "587662b6e5403ae0d126e0c7839a8d98382c4760", + "03a8b737cb9d8585076ebdbac7b7235c8da0620d", + "bf9b57cf7b169806ae2d18d7671aba3825b99203", + "cede19cb310cd4cddfb5d8921cb8d0cc7c7c1503", + "ea3ada938db123368d62b0133e7c5bb54b5292b9", + "2048b277dd6542f184c6a30c3e2b0f3ee5eeaf4b", + "119de54e3cfdf8227a8556b9f5730c328a1390cd", + "2d55e8501b058b6f25382c4e287f022e8938461f", + "364bdfbc72d5e05b520f0320b0d8b39fd9ea692b", + "119c1907fb266f374b8440bbd70dccbea54daf8f", + ] + res = await valid_handler.get_pull_request_commits("1") + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_pull_request_commits_paginated(self, codecov_vcr): + handler = Github( + repo=dict(name="y0"), + owner=dict(username="y0-causal-inference"), + token=dict(key=10 * "a128"), + oauth_consumer_token=dict(key=None, secret=None, refresh_token=None), + ) + res = await handler.get_pull_request_commits("149") + + # From https://stackoverflow.com/a/32234251 + def is_sha1(maybe_sha): + if len(maybe_sha) != 40: + return False + try: + int(maybe_sha, 16) + return True + except ValueError: + return False + + assert len(res) == 134 + assert all(map(is_sha1, res)) + + @pytest.mark.asyncio + async def test_get_pull_requests(self, valid_handler, codecov_vcr): + expected_result = [18, 16] + res = await valid_handler.get_pull_requests() + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_commit(self, valid_handler, codecov_vcr): + # Looks like that jerrod@fundersclub.com doesn't seem to have a username anymore + # Maybe this account was removed from the org? + expected_result = { + "author": { + "id": None, + "username": None, + "email": "jerrod@fundersclub.com", + "name": "Jerrod", + }, + "message": "Adding 'include' term if multiple sources\n\nbased on a support ticket around multiple sources\r\n\r\nhttps://codecov.freshdesk.com/a/tickets/87", + "parents": ["adb252173d2107fad86bcdcbc149884c2dd4c609"], + "commitid": "6895b64", + "timestamp": "2018-07-09T23:39:20Z", + } + + commit = await valid_handler.get_commit("6895b64") + assert commit["author"] == expected_result["author"] + assert commit == expected_result + + @pytest.mark.asyncio + async def test_get_pull_requests_files(self, generic_valid_handler, codecov_vcr): + expected_result = [ + "awesome/__init__.py", + ] + res = await generic_valid_handler.get_pull_request_files(4) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_commit_with_proper_author(self, valid_handler, codecov_vcr): + expected_result = { + "author": { + "id": "44376991", + "username": "ThiagoCodecov", + "email": "thiago@codecov.io", + "name": "Thiago Ramos", + }, + "commitid": "75f355d8d14ba3d7761c728b4d2607cde0eef065", + "parents": ["f0895290dc26668faeeb20ee5ccd4cc995925775"], + "message": "Adding README\n\nsurpriseaAKDS\n\nddkokgfnskfds\n\nBanana\n\nYallow\n\nABG", + "timestamp": "2020-10-13T15:15:31Z", + } + + commit = await valid_handler.get_commit( + "75f355d8d14ba3d7761c728b4d2607cde0eef065" + ) + assert commit["author"] == expected_result["author"] + assert commit == expected_result + + @pytest.mark.asyncio + async def test_get_commit_not_found(self, valid_handler, codecov_vcr): + commitid = "abe3e94949d11471cc4e054f822d222254a4a4f8" + with pytest.raises(TorngitObjectNotFoundError): + await valid_handler.get_commit(commitid) + + @pytest.mark.asyncio + async def test_get_commit_no_permissions( + self, valid_but_no_permissions_handler, codecov_vcr, mocker + ): + commitid = "bbe3e94949d11471cc4e054f822d222254a4a4f8" + with pytest.raises(TorngitRepoNotFoundError): + await valid_but_no_permissions_handler.get_commit(commitid) + + @pytest.mark.asyncio + async def test_get_commit_repo_doesnt_exist( + self, repo_doesnt_exist_handler, codecov_vcr, mocker + ): + commitid = "bbe3e94949d11471cc4e054f822d222254a4a4f8" + with pytest.raises(TorngitRepoNotFoundError) as ex: + await repo_doesnt_exist_handler.get_commit(commitid) + expected_response = '{"message":"Not Found","documentation_url":"https://docs.github.com/rest/reference/repos#get-a-commit"}' + exc = ex.value + assert exc.response_data == expected_response + + @pytest.mark.asyncio + async def test_get_commit_diff(self, valid_handler, codecov_vcr): + expected_result = { + "files": { + ".travis.yml": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["1", "3", "1", "5"], + "lines": [ + "+sudo: false", + "+", + " language: python", + " ", + " python:", + ], + } + ], + "stats": {"added": 2, "removed": 0}, + } + } + } + + res = await valid_handler.get_commit_diff( + "2be550c135cc13425cb2c239b9321e78dcfb787b" + ) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_commit_diff_unicode_newline(self, valid_handler, codecov_vcr): + expected_result = { + "files": { + ".travis.yml": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["1", "3", "1", "5"], + "lines": [ + "+sudo: false", + "+", + " language: python", + " ", + " python\u2028:", # should not split on the unicode line separator + ], + } + ], + "stats": {"added": 2, "removed": 0}, + } + } + } + + res = await valid_handler.get_commit_diff( + "2be550c135cc13425cb2c239b9321e78dcfb787b" + ) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_commit_diff_not_found(self, valid_handler, codecov_vcr): + with pytest.raises(TorngitObjectNotFoundError): + await valid_handler.get_commit_diff( + "3be850c135ccaa425cb2c239b9321e78dcfb78ff" + ) + + @pytest.mark.asyncio + async def test_get_commit_statuses(self, more_complex_handler, codecov_vcr): + res = await more_complex_handler.get_commit_statuses( + "3fb5f4700da7818e561054ec26f5657de720717f" + ) + assert res._statuses == [ + { + "time": "2020-04-08T05:44:02Z", + "state": "success", + "description": "94.21% (+0.18%) compared to 48775c6", + "url": "https://codecov.io/gh/codecov/worker/compare/48775c672437630c9c6f582ecfae5854a3617be2...3fb5f4700da7818e561054ec26f5657de720717f", + "context": "codecov/project", + }, + { + "time": "2020-04-08T05:44:02Z", + "state": "success", + "description": "100.00% of diff hit (target 94.02%)", + "url": "https://codecov.io/gh/codecov/worker/compare/48775c672437630c9c6f582ecfae5854a3617be2...3fb5f4700da7818e561054ec26f5657de720717f", + "context": "codecov/patch", + }, + { + "time": "2020-04-08T20:39:33Z", + "state": "success", + "description": "Your tests passed on CircleCI!", + "url": "https://circleci.com/gh/codecov/worker/2619?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link", + "context": "ci/circleci: build", + }, + { + "time": "2020-04-08T20:40:32Z", + "state": "success", + "description": "Your tests passed on CircleCI!", + "url": "https://circleci.com/gh/codecov/worker/2620?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link", + "context": "ci/circleci: test", + }, + ] + assert res == "success" + + @pytest.mark.asyncio + async def test_set_commit_statuses_then_get(self, valid_handler, codecov_vcr): + commit_sha = "702d05fd3e57a1d7d1e4a5e3e3a0017fe2571382" + target_url = "https://localhost:50036/github/codecov" + statuses_to_set = [ + ("turtle", "success"), + ("bird", "pending"), + ("pig", "failure"), + ("giant", "error"), + ("turtle", "pending"), + ("bird", "failure"), + ("pig", "error"), + ("giant", "success"), + ("giant", "error"), + ("giant", "success"), + ("capybara", "success"), + ] + for i, val in enumerate(statuses_to_set): + context, status = val + res = await valid_handler.set_commit_status( + commit_sha, status, context, f"{status} - {i} - {context}", target_url + ) + res = await valid_handler.get_commit_statuses(commit_sha) + assert res._statuses == [ + { + "time": "2020-10-14T21:43:30Z", + "state": "pending", + "description": "pending - 4 - turtle", + "url": "https://localhost:50036/github/codecov", + "context": "turtle", + }, + { + "time": "2020-10-14T21:43:31Z", + "state": "failure", + "description": "failure - 5 - bird", + "url": "https://localhost:50036/github/codecov", + "context": "bird", + }, + { + "time": "2020-10-14T21:43:31Z", + "state": "error", + "description": "error - 6 - pig", + "url": "https://localhost:50036/github/codecov", + "context": "pig", + }, + { + "time": "2020-10-14T21:43:33Z", + "state": "success", + "description": "success - 9 - giant", + "url": "https://localhost:50036/github/codecov", + "context": "giant", + }, + { + "time": "2020-10-14T21:43:34Z", + "state": "success", + "description": "success - 10 - capybara", + "url": "https://localhost:50036/github/codecov", + "context": "capybara", + }, + ] + assert res == "failure" + + @pytest.mark.asyncio + async def test_set_commit_status(self, valid_handler, codecov_vcr): + target_url = "https://localhost:50036/gitlab/codecov/ci-repo?ref=ad798926730aad14aadf72281204bdb85734fe67" + expected_result = { + "url": "https://api.github.com/repos/ThiagoCodecov/example-python/statuses/a06aef4356ca35b34c5486269585288489e578db", + "avatar_url": "https://avatars3.githubusercontent.com/u/44376991?v=4", + "id": 11050927805, + "node_id": "MDEzOlN0YXR1c0NvbnRleHQxMTA1MDkyNzgwNQ==", + "state": "success", + "description": "aaaaaaaaaa", + "target_url": "https://localhost:50036/gitlab/codecov/ci-repo?ref=ad798926730aad14aadf72281204bdb85734fe67", + "context": "context", + "created_at": "2020-10-14T17:32:28Z", + "updated_at": "2020-10-14T17:32:28Z", + "creator": { + "login": "ThiagoCodecov", + "id": 44376991, + "node_id": "MDQ6VXNlcjQ0Mzc2OTkx", + "avatar_url": "https://avatars3.githubusercontent.com/u/44376991?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ThiagoCodecov", + "html_url": "https://github.com/ThiagoCodecov", + "followers_url": "https://api.github.com/users/ThiagoCodecov/followers", + "following_url": "https://api.github.com/users/ThiagoCodecov/following{/other_user}", + "gists_url": "https://api.github.com/users/ThiagoCodecov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ThiagoCodecov/subscriptions", + "organizations_url": "https://api.github.com/users/ThiagoCodecov/orgs", + "repos_url": "https://api.github.com/users/ThiagoCodecov/repos", + "events_url": "https://api.github.com/users/ThiagoCodecov/events{/privacy}", + "received_events_url": "https://api.github.com/users/ThiagoCodecov/received_events", + "type": "User", + "site_admin": False, + }, + } + res = await valid_handler.set_commit_status( + "a06aef4356ca35b34c5486269585288489e578db", + "success", + "context", + "aaaaaaaaaa", + target_url, + ) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_branches(self, valid_handler, codecov_vcr): + expected_result = [ + "main", + "random-branch", + "thiago/base-no-base", + "thiago/f/big-pt", + "thiago/f/something", + "thiago/test-1", + ] + branches = sorted(await valid_handler.get_branches()) + assert sorted(map(lambda a: a[0], branches)) == expected_result + + @pytest.mark.asyncio + async def test_get_branch(self, valid_handler, codecov_vcr): + expected_result = { + "name": "main", + "sha": "38c2d0214f2a48c9212a140f5311977059a15b35", + } + branch = await valid_handler.get_branch("main") + assert branch == expected_result + + @pytest.mark.asyncio + async def test_get_branch_not_existent(self, valid_handler, codecov_vcr): + with pytest.raises(TorngitClientGeneralError) as e: + await valid_handler.get_branch("none") + assert e[0] == 404 + assert e[1]["message"] == "Branch not found" + + @pytest.mark.asyncio + async def test_post_webhook(self, valid_handler, codecov_vcr): + url = "http://requestbin.net/r/1ecyaj51" + events = ["push", "pull_request"] + name, secret = "a", "d" + expected_result = { + "type": "Repository", + "id": 255680134, + "name": "web", + "active": True, + "events": ["pull_request", "push"], + "config": { + "content_type": "json", + "secret": "********", + "url": "http://requestbin.net/r/1ecyaj51", + "insecure_ssl": "0", + }, + "updated_at": "2020-10-14T17:32:29Z", + "created_at": "2020-10-14T17:32:29Z", + "url": "https://api.github.com/repos/ThiagoCodecov/example-python/hooks/255680134", + "test_url": "https://api.github.com/repos/ThiagoCodecov/example-python/hooks/255680134/test", + "ping_url": "https://api.github.com/repos/ThiagoCodecov/example-python/hooks/255680134/pings", + "last_response": {"code": None, "status": "unused", "message": None}, + } + res = await valid_handler.post_webhook(name, url, events, secret) + assert res == expected_result + + @pytest.mark.asyncio + async def test_edit_webhook(self, valid_handler, codecov_vcr): + url = "https://enfehm3qrtj5u.x.pipedream.net" + events = ["project", "pull_request", "release"] + new_name, secret = "new_name", "new_secret" + expected_result = { + "type": "Repository", + "id": 255680134, + "name": "web", + "active": True, + "events": ["project", "pull_request", "release"], + "config": { + "content_type": "json", + "secret": "********", + "url": "https://enfehm3qrtj5u.x.pipedream.net", + "insecure_ssl": "0", + }, + "updated_at": "2020-10-14T21:51:05Z", + "created_at": "2020-10-14T17:32:29Z", + "url": "https://api.github.com/repos/ThiagoCodecov/example-python/hooks/255680134", + "test_url": "https://api.github.com/repos/ThiagoCodecov/example-python/hooks/255680134/test", + "ping_url": "https://api.github.com/repos/ThiagoCodecov/example-python/hooks/255680134/pings", + "last_response": { + "code": 404, + "status": "missing", + "message": "Invalid HTTP Response: 404", + }, + } + res = await valid_handler.edit_webhook( + "255680134", new_name, url, events, secret + ) + assert res == expected_result + + @pytest.mark.asyncio + async def test_delete_webhook(self, valid_handler, codecov_vcr): + res = await valid_handler.delete_webhook("255680134") + assert res is True + + @pytest.mark.asyncio + async def test_delete_webhook_not_found(self, valid_handler, codecov_vcr, mocker): + mock_refresh = mocker.patch.object(Github, "refresh_token", return_value=None) + with pytest.raises(TorngitObjectNotFoundError): + await valid_handler.delete_webhook("4742f011-8397-aa77-8876-5e9a06f10f98") + mock_refresh.assert_called_once() + + @pytest.mark.asyncio + async def test_get_authenticated(self, valid_handler, codecov_vcr): + res = await valid_handler.get_authenticated() + assert res == (True, True) + + @pytest.mark.asyncio + async def test_get_compare(self, valid_handler, codecov_vcr): + base, head = "6ae5f17", "b92edba" + expected_result = { + "diff": { + "files": { + "README.rst": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["9", "7", "9", "8"], + "lines": [ + " Overview", + " --------", + " ", + "-Main website: `Codecov `_.", + "+", + "+website: `Codecov `_.", + " ", + " .. code-block:: shell-session", + " ", + ], + }, + { + "header": ["46", "12", "47", "19"], + "lines": [ + " ", + " You may need to configure a ``.coveragerc`` file. Learn more `here `_. Start with this `generic .coveragerc `_ for example.", + " ", + "-We highly suggest adding `source` to your ``.coveragerc`` which solves a number of issues collecting coverage.", + "+We highly suggest adding ``source`` to your ``.coveragerc``, which solves a number of issues collecting coverage.", + " ", + " .. code-block:: ini", + " ", + " [run]", + " source=your_package_name", + "+ ", + "+If there are multiple sources, you instead should add ``include`` to your ``.coveragerc``", + "+", + "+.. code-block:: ini", + "+", + "+ [run]", + "+ include=your_package_name/*", + " ", + " unittests", + " ---------", + ], + }, + { + "header": ["150", "5", "158", "4"], + "lines": [ + " * Twitter: `@codecov `_.", + " * Email: `hello@codecov.io `_.", + " ", + "-We are happy to help if you have any questions. Please contact email our Support at [support@codecov.io](mailto:support@codecov.io)", + "-", + "+We are happy to help if you have any questions. Please contact email our Support at `support@codecov.io `_.", + ], + }, + ], + "stats": {"added": 11, "removed": 4}, + } + } + }, + "commits": [ + { + "commitid": "b92edba44fdd29fcc506317cc3ddeae1a723dd08", + "message": "Update README.rst", + "timestamp": "2018-07-09T23:51:16Z", + "author": { + "id": None, + "username": None, + "name": "Jerrod", + "email": "jerrod@fundersclub.com", + }, + }, + { + "commitid": "c7f608036a3d2e89f8c59989ee213900c1ef39d1", + "message": "Update README.rst", + "timestamp": "2018-07-09T23:48:34Z", + "author": { + "id": None, + "username": None, + "name": "Jerrod", + "email": "jerrod@fundersclub.com", + }, + }, + { + "commitid": "6895b6479dbe12b5cb3baa02416c6343ddb888b4", + "message": "Adding 'include' term if multiple sources\n\nbased on a support ticket around multiple sources\r\n\r\nhttps://codecov.freshdesk.com/a/tickets/87", + "timestamp": "2018-07-09T23:39:20Z", + "author": { + "id": None, + "username": None, + "name": "Jerrod", + "email": "jerrod@fundersclub.com", + }, + }, + { + "commitid": "adb252173d2107fad86bcdcbc149884c2dd4c609", + "message": "Update README.rst", + "timestamp": "2018-04-26T08:39:32Z", + "author": { + "id": 11602092, + "username": "TomPed", + "name": "Thomas Pedbereznak", + "email": "tom@tomped.com", + }, + }, + { + "commitid": "6ae5f1795a441884ed2847bb31154814ac01ef38", + "message": "Update README.rst", + "timestamp": "2018-04-26T08:35:58Z", + "author": { + "id": 11602092, + "username": "TomPed", + "name": "Thomas Pedbereznak", + "email": "tom@tomped.com", + }, + }, + ], + } + + res = await valid_handler.get_compare(base, head) + assert sorted(list(res.keys())) == sorted(list(expected_result.keys())) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_distance_in_commits(self, generic_valid_handler, codecov_vcr): + base_branch, head = "main", "0206296b1424912cc05069a9bf4025cbb95f5ecc" + expected_result = { + "behind_by": 0, + "behind_by_commit": "93189ce50f224296d6412e2884b93dcc3c7c8654", + "status": "ahead", + "ahead_by": 2, + } + res = await generic_valid_handler.get_distance_in_commits(base_branch, head) + assert sorted(list(res.keys())) == sorted(list(expected_result.keys())) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_compare_same_commit(self, valid_handler, codecov_vcr): + base, head = "6ae5f17", "6ae5f17" + expected_result = { + "diff": {"files": {}}, + "commits": [ + { + "commitid": "6ae5f1795a441884ed2847bb31154814ac01ef38", + "author": { + "email": "tom@tomped.com", + "id": 11602092, + "name": "Thomas Pedbereznak", + "username": "TomPed", + }, + "message": "Update README.rst", + "timestamp": "2018-04-26T08:35:58Z", + } + ], + } + res = await valid_handler.get_compare(base, head) + assert sorted(list(res.keys())) == sorted(list(expected_result.keys())) + assert len(res["commits"]) == len(expected_result["commits"]) + assert res["commits"][0] == expected_result["commits"][0] + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_repository(self, valid_handler, codecov_vcr): + expected_result = { + "owner": {"service_id": "44376991", "username": "ThiagoCodecov"}, + "repo": { + "service_id": "156617777", + "name": "example-python", + "language": "shell", + "private": False, + "fork": { + "owner": {"service_id": "8226205", "username": "codecov"}, + "repo": { + "service_id": "24344106", + "name": "example-python", + "language": "python", + "private": False, + "branch": "main", + }, + }, + "branch": "main", + }, + } + res = await valid_handler.get_repository() + assert res["owner"] == expected_result["owner"] + assert res["repo"] == expected_result["repo"] + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_repo_with_languages_graphql(self, valid_handler, codecov_vcr): + expected_result = { + "another-test": ["javascript", "html", "css"], + "new-test-repo": ["html", "css", "javascript"], + "test-no-languages": [], + } + owner_username = "adrian-codecov" + + res = await valid_handler.get_repos_with_languages_graphql( + owner_username=owner_username + ) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_repo_no_languages(self, valid_repo_no_languages, codecov_vcr): + expected_result = [] + res = await valid_repo_no_languages.get_repo_languages() + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_gh_app_installation( + self, mocker, generic_valid_handler, codecov_vcr + ): + expected_response = { + "id": 12345678, + "account": { + "login": "fake-test-user", + "id": 72693746, + "node_id": "AMSDFU7234Msdf7N#", + "avatar_url": "https://avatars.githubusercontent.com/u/72693746?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/fake-test-user", + "html_url": "https://github.com/fake-test-user", + "followers_url": "https://api.github.com/users/fake-test-user/followers", + "following_url": "https://api.github.com/users/fake-test-user/following{/other_user}", + "gists_url": "https://api.github.com/users/fake-test-user/gists{/gist_id}", + "starred_url": "https://api.github.com/users/fake-test-user/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/fake-test-user/subscriptions", + "organizations_url": "https://api.github.com/users/fake-test-user/orgs", + "repos_url": "https://api.github.com/users/fake-test-user/repos", + "events_url": "https://api.github.com/users/fake-test-user/events{/privacy}", + "received_events_url": "https://api.github.com/users/fake-test-user/received_events", + "type": "User", + "site_admin": False, + }, + "repository_selection": "all", + "access_tokens_url": "https://api.github.com/app/installations/12345678/access_tokens", + "repositories_url": "https://api.github.com/installation/repositories", + "html_url": "https://github.com/settings/installations/12345678", + "app_id": 345678, + "app_slug": "local-github-app-adrian", + "target_id": 72693746, + "target_type": "User", + "permissions": { + "checks": "write", + "contents": "write", + "metadata": "read", + "statuses": "write", + "pull_requests": "write", + "administration": "read", + }, + "events": [], + "created_at": "2024-02-11T20:39:08.000Z", + "updated_at": "2024-02-11T20:39:09.000Z", + "single_file_name": None, + "has_multiple_single_files": False, + "single_file_paths": [], + "suspended_by": None, + "suspended_at": None, + } + + mocker.patch("shared.github.get_pem", return_value=fake_private_key) + + res = await generic_valid_handler.get_gh_app_installation( + installation_id=12345678 + ) + assert res == expected_response + + @pytest.mark.asyncio + async def test_get_gh_app_installation_non_existent( + self, mocker, generic_valid_handler, codecov_vcr + ): + mocker.patch("shared.github.get_pem", return_value=fake_private_key) + + with pytest.raises(TorngitObjectNotFoundError) as e: + await generic_valid_handler.get_gh_app_installation( + installation_id=12345678 + ) + assert e[0] == 404 + assert e[1]["message"] == "Cannot find gh app with installation_id 12345678" + + @pytest.mark.asyncio + async def test_get_source_master(self, valid_handler, codecov_vcr): + expected_result = { + "content": b"\n".join( + [ + b"def fib(n):", + b" if n < 0:", + b" return 0", + b" if n <= 1:", + b" return 1", + b" return fib(n - 1) + fib(n - 2)", + b"", + b"", + b"def untested_code(a):", + b" raise Exception()", + b"", + ] + ), + "commitid": "7fb3c3fbd71a6d3f4b98964c0130f7e083505fcd", + } + + path, ref = "awesome/code_fib.py", "master" + res = await valid_handler.get_source(path, ref) + assert res["content"].split(b"\n") == expected_result["content"].split(b"\n") + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_source_random_commit(self, valid_handler, codecov_vcr): + expected_result = { + "content": b'def smile():\n return ":)"\n\ndef frown():\n return ":("\n', + "commitid": "4d34acc61e7abe5536c84fec4fe9fd9b26311cc7", + } + path, ref = "awesome/__init__.py", "96492d409fc86aa7ae31b214dfe6b08ae860458a" + res = await valid_handler.get_source(path, ref) + assert res["content"].split(b"\n") == expected_result["content"].split(b"\n") + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_source_random_commit_not_found( + self, valid_handler, codecov_vcr, mocker + ): + mock_refresh = mocker.patch.object(Github, "refresh_token", return_value=None) + path, ref = ( + "awesome/non_exising_file.py", + "96492d409fc86aa7ae31b214dfe6b08ae860458a", + ) + with pytest.raises(TorngitObjectNotFoundError): + await valid_handler.get_source(path, ref) + mock_refresh.assert_called_once() + + @pytest.mark.asyncio + async def test_get_source_large_file(self, valid_handler, codecov_vcr, mocker): + ret = {"content": "", "download_url": "url", "encoding": "none", "sha": "sha"} + mock_api = mocker.patch.object(Github, "api", return_value=ret) + path, ref = "awesome/__init__.py", "96492d409fc86aa7ae31b214dfe6b08ae860458a" + await valid_handler.get_source(path, ref) + assert mock_api.call_count == 2 + + @pytest.mark.asyncio + @pytest.mark.django_db(databases={"default"}) + async def test_list_repos(self, valid_handler, codecov_vcr): + res = await valid_handler.list_repos() + assert len(res) == 115 + assert all(x["owner"]["service_id"] in ["8226205", "44376991"] for x in res) + one_expected_result = { + "owner": {"service_id": "44376991", "username": "ThiagoCodecov"}, + "repo": { + "service_id": "156617777", + "name": "example-python", + "language": "shell", + "private": False, + "branch": "main", + }, + } + + assert one_expected_result in res + + @pytest.mark.asyncio + @pytest.mark.django_db(databases={"default"}) + async def test_list_repos_generator(self, valid_handler, codecov_vcr): + repos = [] + page_count = 0 + async for page in valid_handler.list_repos_generator(): + repos.extend(page) + page_count += 1 + + assert page_count == 2 + assert len(repos) == 145 + + expected_owners = [ + "137832199", # matt-codecov + "139263855", # matt-codecov-club + "8226205", # codecov + ] + assert all(x["owner"]["service_id"] in expected_owners for x in repos) + + some_expected_results = [ + { + "owner": {"service_id": "137832199", "username": "matt-codecov"}, + "repo": { + "service_id": "670770179", + "name": "rust", + "language": None, + "private": False, + "branch": "main", + }, + }, + { + "owner": {"service_id": "139263855", "username": "matt-codecov-club"}, + "repo": { + "service_id": "665219192", + "name": "mike", + "language": None, + "private": True, + "branch": "main", + }, + }, + { + "owner": {"service_id": "8226205", "username": "codecov"}, + "repo": { + "service_id": "665728948", + "name": "worker", + "language": "python", + "private": False, + "branch": "main", + }, + }, + ] + assert all(x in repos for x in some_expected_results) + + @pytest.mark.asyncio + @pytest.mark.django_db(databases={"default"}) + async def test_list_repos_using_installation(self, valid_handler, codecov_vcr): + res = await valid_handler.list_repos_using_installation() + assert res == [ + { + "owner": {"service_id": "111885151", "username": "scott-codecov-org"}, + "repo": { + "service_id": "610348935", + "name": "codecov-test", + "language": "python", + "private": True, + "branch": "main", + }, + } + ] + + @pytest.mark.asyncio + @pytest.mark.django_db(databases={"default"}) + async def test_list_repos_using_installation_generator( + self, valid_handler, codecov_vcr + ): + repos = [] + page_count = 0 + async for page in valid_handler.list_repos_using_installation_generator(): + repos.extend(page) + page_count += 1 + + assert page_count == 1 + assert repos == [ + { + "owner": {"service_id": "139263855", "username": "matt-codecov-club"}, + "repo": { + "service_id": "665219192", + "name": "mike", + "language": None, + "private": True, + "branch": "main", + }, + }, + ] + + @pytest.mark.asyncio + async def test_list_top_level_files(self, valid_handler, codecov_vcr): + expected_result = [ + {"name": ".gitignore", "path": ".gitignore", "type": "file"}, + {"name": "Makefile", "path": "Makefile", "type": "file"}, + {"name": "README.md", "path": "README.md", "type": "file"}, + {"name": "awesome", "path": "awesome", "type": "folder"}, + { + "name": "changed_production.sh", + "path": "changed_production.sh", + "type": "file", + }, + {"name": "codecov.yaml", "path": "codecov.yaml", "type": "file"}, + {"name": "dev.sh", "path": "dev.sh", "type": "file"}, + { + "name": "flagone.coverage.xml", + "path": "flagone.coverage.xml", + "type": "file", + }, + { + "name": "flagtwo.coverage.xml", + "path": "flagtwo.coverage.xml", + "type": "file", + }, + {"name": "requirements.txt", "path": "requirements.txt", "type": "file"}, + {"name": "tests", "path": "tests", "type": "folder"}, + {"name": "unit.coverage.xml", "path": "unit.coverage.xml", "type": "file"}, + ] + res = await valid_handler.list_top_level_files("main") + assert sorted(res, key=lambda x: x["path"]) == sorted( + expected_result, key=lambda x: x["path"] + ) + + @pytest.mark.asyncio + async def test_list_files(self, valid_handler, codecov_vcr): + expected_result = [ + {"name": "__init__.py", "path": "awesome/__init__.py", "type": "file"}, + {"name": "code_fib.py", "path": "awesome/code_fib.py", "type": "file"}, + ] + res = await valid_handler.list_files("main", "awesome") + assert sorted(res, key=lambda x: x["path"]) == sorted( + expected_result, key=lambda x: x["path"] + ) + + @pytest.mark.asyncio + async def test_get_ancestors_tree(self, valid_handler, codecov_vcr): + commitid = "6ae5f17" + res = await valid_handler.get_ancestors_tree(commitid) + assert res["commitid"] == "6ae5f1795a441884ed2847bb31154814ac01ef38" + assert sorted([x["commitid"] for x in res["parents"]]) == [ + "8631ea09b9b689de0a348d5abf70bdd7273d2ae3" + ] + + def test_get_href(self, valid_handler): + expected_result = "https://github.com/ThiagoCodecov/example-python/commit/8631ea09b9b689de0a348d5abf70bdd7273d2ae3" + res = valid_handler.get_href( + Endpoints.commit_detail, commitid="8631ea09b9b689de0a348d5abf70bdd7273d2ae3" + ) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_pull_request_base_doesnt_match(self, valid_handler, codecov_vcr): + pull_id = "15" + expected_result = { + "base": { + "branch": "main", + "commitid": "30cc1ed751a59fa9e7ad8e79fff41a6fe11ef5dd", + "slug": "ThiagoCodecov/example-python", + }, + "head": { + "branch": "thiago/test-1", + "commitid": "2e2600aa09525e2e1e1d98b09de61454d29c94bb", + "slug": "ThiagoCodecov/example-python", + }, + "number": "15", + "id": "15", + "state": "closed", + "title": "Thiago/test 1", + "author": {"id": "44376991", "username": "ThiagoCodecov"}, + "labels": [], + "merge_commit_sha": None, + } + res = await valid_handler.get_pull_request(pull_id) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_pull_request_from_fork(self, valid_handler, codecov_vcr): + handler = Github( + repo=dict(name="codecov-api"), + owner=dict(username="codecov"), + token=valid_handler.token, + ) + pull_id = "285" + expected_result = { + "base": { + "branch": "main", + "commitid": "109eea9a085f5856a20ae5f1714b8c4786c3327b", + "slug": "codecov/codecov-api", + }, + "head": { + "branch": "python-3-12", + "commitid": "67a44e176ffd419f066c1cc34cff391e2a1304e2", + "slug": "FraBle/codecov-api", + }, + "number": "285", + "id": "285", + "state": "open", + "title": "chore: Switch to Python 3.12", + "author": {"id": "1584268", "username": "FraBle"}, + "labels": [], + "merge_commit_sha": None, + } + res = await handler.get_pull_request(pull_id) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_pull_request_base_partially_differs( + self, valid_handler, codecov_vcr + ): + handler = Github( + repo=dict(name="codecov-api-archive"), + owner=dict(username="codecov"), + token=valid_handler.token, + ) + pull_id = "110" + expected_result = { + "base": { + "branch": "main", + "commitid": "77141afbd13a1273f87cf02f7f32265ea19a3b77", + "slug": "codecov/codecov-api-archive", + }, + "head": { + "branch": "ce-1314/gh-status-handler", + "commitid": "a178a13c65f44d5b81c807f3c0fa2cb4922f020f", + "slug": "codecov/codecov-api-archive", + }, + "number": "110", + "id": "110", + "state": "merged", + "title": "CE-1314 GitHub Status Event Handler", + "author": {"id": "5767537", "username": "pierce-m"}, + "labels": [], + "merge_commit_sha": "e1d42c058e7169cc430f387591c1fc7cac35d2ae", + } + res = await handler.get_pull_request(pull_id) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_workflow_run(self, codecov_vcr): + handler = Github( + repo=dict(name="django"), + owner=dict(username="django"), + token=dict(key="test9zwlbanm8k3m3394ihpwyqk08okirro3l3n0"), + ) + expected_result = { + "start_time": "2022-10-17T14:29:14Z", + "finish_time": "2022-10-17T14:31:13Z", + "status": "completed", + "public": True, + "slug": "django/django", + "commit_sha": "384dba7ce472c0f22c33f2bcede8f8d04b9c2b0f", + } + run_id = "3265999402" + res = await handler.get_workflow_run(run_id) + assert res == expected_result + + @pytest.mark.asyncio + async def test_create_github_check( + self, integration_installed_handler, codecov_vcr + ): + res = await integration_installed_handler.create_check_run( + "Test check", + "75f355d8d14ba3d7761c728b4d2607cde0eef065", + status="in_progress", + ) + assert res == 1256232357 + + @pytest.mark.asyncio + async def test_update_github_check( + self, integration_installed_handler, codecov_vcr + ): + res = await integration_installed_handler.update_check_run( + 1256232357, + "success", + url="https://app.codecov.io/gh/codecov/example-python/compare/1?src=pr", + ) + expected_result = { + "id": 1256232357, + "node_id": "MDg6Q2hlY2tSdW4xMjU2MjMyMzU3", + "head_sha": "75f355d8d14ba3d7761c728b4d2607cde0eef065", + "external_id": "", + "url": "https://api.github.com/repos/ThiagoCodecov/example-python/check-runs/1256232357", + "html_url": "https://github.com/ThiagoCodecov/example-python/runs/1256232357", + "details_url": "https://app.codecov.io/gh/codecov/example-python/compare/1?src=pr", + "status": "completed", + "conclusion": "success", + "started_at": "2020-10-14T23:00:59Z", + "completed_at": "2020-10-14T23:01:14Z", + "output": { + "title": None, + "summary": None, + "text": None, + "annotations_count": 0, + "annotations_url": "https://api.github.com/repos/ThiagoCodecov/example-python/check-runs/1256232357/annotations", + }, + "name": "Test check", + "check_suite": {"id": 1341719124}, + "app": { + "id": 254, + "slug": "codecov", + "node_id": "MDM6QXBwMjU0", + "owner": { + "login": "codecov", + "id": 8226205, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=", + "avatar_url": "https://avatars3.githubusercontent.com/u/8226205?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/codecov", + "html_url": "https://github.com/codecov", + "followers_url": "https://api.github.com/users/codecov/followers", + "following_url": "https://api.github.com/users/codecov/following{/other_user}", + "gists_url": "https://api.github.com/users/codecov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/codecov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/codecov/subscriptions", + "organizations_url": "https://api.github.com/users/codecov/orgs", + "repos_url": "https://api.github.com/users/codecov/repos", + "events_url": "https://api.github.com/users/codecov/events{/privacy}", + "received_events_url": "https://api.github.com/users/codecov/received_events", + "type": "Organization", + "site_admin": False, + }, + "name": "Codecov", + "description": "Codecov provides highly integrated tools to group, merge, archive and compare coverage reports. Whether your team is comparing changes in a pull request or reviewing a single commit, Codecov will improve the code review workflow and quality.\r\n\r\n## Code coverage done right.®\r\n\r\n1. Upload coverage reports from your CI builds.\r\n2. Codecov merges all builds and languages into one beautiful coherent report.\r\n3. Get commit statuses, pull request comments and coverage overlay via our browser extension.\r\n\r\nWhen Codecov merges your uploads it keeps track of the CI provider (inc. build details) and user specified context, e.g. `#unittest` ~ `#smoketest` or `#oldcode` ~ `#newcode`. You can track the `#unittest` coverage independently of other groups. [Learn more here](\r\nhttp://docs.codecov.io/docs/flags)\r\n\r\nThrough **Codecov's Browser Extension** reports overlay directly in GitHub UI to assist in code review. [Watch here](https://docs.codecov.io/docs/browser-extension)\r\n\r\n*Highly detailed* **pull request comments** and *customizable* **commit statuses** will improve your team's workflow and code coverage incrementally.\r\n\r\n**File backed configuration** all through the `codecov.yml`. \r\n\r\n## FAQ\r\n- Do you **merge multiple uploads** to the same commit? **Yes**\r\n- Do you **support multiple languages** in the same project? **Yes**\r\n- Can you **group coverage reports** by project and/or test type? **Yes**\r\n- How does **pricing** work? Only paid users can view reports and post statuses/comments. ", + "external_url": "https://codecov.io", + "html_url": "https://github.com/apps/codecov", + "created_at": "2016-09-25T14:18:27Z", + "updated_at": "2020-08-27T18:10:18Z", + "permissions": { + "administration": "read", + "checks": "write", + "contents": "read", + "issues": "read", + "members": "read", + "metadata": "read", + "pull_requests": "write", + "statuses": "write", + }, + "events": [ + "check_run", + "check_suite", + "create", + "delete", + "fork", + "membership", + "public", + "pull_request", + "push", + "release", + "repository", + "status", + "team_add", + ], + }, + "pull_requests": [ + { + "url": "https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18", + "id": 383348775, + "number": 18, + "head": { + "ref": "thiago/base-no-base", + "sha": "75f355d8d14ba3d7761c728b4d2607cde0eef065", + "repo": { + "id": 156617777, + "url": "https://api.github.com/repos/ThiagoCodecov/example-python", + "name": "example-python", + }, + }, + "base": { + "ref": "main", + "sha": "f0895290dc26668faeeb20ee5ccd4cc995925775", + "repo": { + "id": 156617777, + "url": "https://api.github.com/repos/ThiagoCodecov/example-python", + "name": "example-python", + }, + }, + } + ], + } + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_github_check_runs_no_params( + self, more_complex_handler, codecov_vcr + ): + with pytest.raises(Exception): + await more_complex_handler.get_check_runs( + name="Test check", + token={"key": "v1.test7plgcp94kp45aqvz1zr1crhganpdm9t6u52i"}, + ) + + @pytest.mark.asyncio + async def test_get_github_check_runs( + self, integration_installed_handler, codecov_vcr + ): + res = await integration_installed_handler.get_check_runs( + name="Test check", head_sha="75f355d8d14ba3d7761c728b4d2607cde0eef065" + ) + expected_result = { + "total_count": 1, + "check_runs": [ + { + "id": 1256232357, + "node_id": "MDg6Q2hlY2tSdW4xMjU2MjMyMzU3", + "head_sha": "75f355d8d14ba3d7761c728b4d2607cde0eef065", + "external_id": "", + "url": "https://api.github.com/repos/ThiagoCodecov/example-python/check-runs/1256232357", + "html_url": "https://github.com/ThiagoCodecov/example-python/runs/1256232357", + "details_url": "https://app.codecov.io/gh/codecov/example-python/compare/1?src=pr", + "status": "completed", + "conclusion": "success", + "started_at": "2020-10-14T23:00:59Z", + "completed_at": "2020-10-14T23:01:14Z", + "output": { + "title": None, + "summary": None, + "text": None, + "annotations_count": 0, + "annotations_url": "https://api.github.com/repos/ThiagoCodecov/example-python/check-runs/1256232357/annotations", + }, + "name": "Test check", + "check_suite": {"id": 1341719124}, + "app": { + "id": 254, + "slug": "codecov", + "node_id": "MDM6QXBwMjU0", + "owner": { + "login": "codecov", + "id": 8226205, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=", + "avatar_url": "https://avatars3.githubusercontent.com/u/8226205?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/codecov", + "html_url": "https://github.com/codecov", + "followers_url": "https://api.github.com/users/codecov/followers", + "following_url": "https://api.github.com/users/codecov/following{/other_user}", + "gists_url": "https://api.github.com/users/codecov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/codecov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/codecov/subscriptions", + "organizations_url": "https://api.github.com/users/codecov/orgs", + "repos_url": "https://api.github.com/users/codecov/repos", + "events_url": "https://api.github.com/users/codecov/events{/privacy}", + "received_events_url": "https://api.github.com/users/codecov/received_events", + "type": "Organization", + "site_admin": False, + }, + "name": "Codecov", + "description": "Codecov provides highly integrated tools to group, merge, archive and compare coverage reports. Whether your team is comparing changes in a pull request or reviewing a single commit, Codecov will improve the code review workflow and quality.\r\n\r\n## Code coverage done right.®\r\n\r\n1. Upload coverage reports from your CI builds.\r\n2. Codecov merges all builds and languages into one beautiful coherent report.\r\n3. Get commit statuses, pull request comments and coverage overlay via our browser extension.\r\n\r\nWhen Codecov merges your uploads it keeps track of the CI provider (inc. build details) and user specified context, e.g. `#unittest` ~ `#smoketest` or `#oldcode` ~ `#newcode`. You can track the `#unittest` coverage independently of other groups. [Learn more here](\r\nhttp://docs.codecov.io/docs/flags)\r\n\r\nThrough **Codecov's Browser Extension** reports overlay directly in GitHub UI to assist in code review. [Watch here](https://docs.codecov.io/docs/browser-extension)\r\n\r\n*Highly detailed* **pull request comments** and *customizable* **commit statuses** will improve your team's workflow and code coverage incrementally.\r\n\r\n**File backed configuration** all through the `codecov.yml`. \r\n\r\n## FAQ\r\n- Do you **merge multiple uploads** to the same commit? **Yes**\r\n- Do you **support multiple languages** in the same project? **Yes**\r\n- Can you **group coverage reports** by project and/or test type? **Yes**\r\n- How does **pricing** work? Only paid users can view reports and post statuses/comments. ", + "external_url": "https://codecov.io", + "html_url": "https://github.com/apps/codecov", + "created_at": "2016-09-25T14:18:27Z", + "updated_at": "2020-08-27T18:10:18Z", + "permissions": { + "administration": "read", + "checks": "write", + "contents": "read", + "issues": "read", + "members": "read", + "metadata": "read", + "pull_requests": "write", + "statuses": "write", + }, + "events": [ + "check_run", + "check_suite", + "create", + "delete", + "fork", + "membership", + "public", + "pull_request", + "push", + "release", + "repository", + "status", + "team_add", + ], + }, + "pull_requests": [ + { + "url": "https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18", + "id": 383348775, + "number": 18, + "head": { + "ref": "thiago/base-no-base", + "sha": "75f355d8d14ba3d7761c728b4d2607cde0eef065", + "repo": { + "id": 156617777, + "url": "https://api.github.com/repos/ThiagoCodecov/example-python", + "name": "example-python", + }, + }, + "base": { + "ref": "main", + "sha": "f0895290dc26668faeeb20ee5ccd4cc995925775", + "repo": { + "id": 156617777, + "url": "https://api.github.com/repos/ThiagoCodecov/example-python", + "name": "example-python", + }, + }, + } + ], + } + ], + } + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_best_effort_branches(self, valid_handler, codecov_vcr): + commit_sha = "75f355d8d14ba3d7761c728b4d2607cde0eef065" + res = await valid_handler.get_best_effort_branches(commit_sha) + assert res == ["thiago/base-no-base"] + + @pytest.mark.asyncio + async def test_is_student_not_student( + self, student_app_capable_not_student_handler, codecov_vcr + ): + res = await student_app_capable_not_student_handler.is_student() + assert not res + + @pytest.mark.asyncio + async def test_is_student_not_capable_app( + self, generic_valid_handler, codecov_vcr, mocker + ): + mock_refresh = mocker.patch.object(Github, "refresh_token") + res = await generic_valid_handler.is_student() + assert not res + assert mock_refresh.call_count == 0 + + @pytest.mark.asyncio + async def test_is_student_github_education_503(self, valid_handler, codecov_vcr): + res = await valid_handler.is_student() + assert not res + + @pytest.mark.asyncio + async def test_is_student_yes_student( + self, student_app_capable_yes_student_handler, codecov_vcr + ): + result = await student_app_capable_yes_student_handler.is_student() + assert result + + @pytest.mark.asyncio + async def test_get_github_check_suite( + self, integration_installed_handler, codecov_vcr + ): + res = await integration_installed_handler.get_check_suites( + "75f355d8d14ba3d7761c728b4d2607cde0eef065" + ) + expected_result = { + "total_count": 1, + "check_suites": [ + { + "id": 1341719124, + "node_id": "MDEwOkNoZWNrU3VpdGUxMzQxNzE5MTI0", + "head_branch": "thiago/base-no-base", + "head_sha": "75f355d8d14ba3d7761c728b4d2607cde0eef065", + "status": "completed", + "conclusion": "success", + "url": "https://api.github.com/repos/ThiagoCodecov/example-python/check-suites/1341719124", + "before": "f0fe310b54d2b944a1d16b79958d9d3add7c902c", + "after": "75f355d8d14ba3d7761c728b4d2607cde0eef065", + "pull_requests": [ + { + "url": "https://api.github.com/repos/ThiagoCodecov/example-python/pulls/18", + "id": 383348775, + "number": 18, + "head": { + "ref": "thiago/base-no-base", + "sha": "75f355d8d14ba3d7761c728b4d2607cde0eef065", + "repo": { + "id": 156617777, + "url": "https://api.github.com/repos/ThiagoCodecov/example-python", + "name": "example-python", + }, + }, + "base": { + "ref": "main", + "sha": "f0895290dc26668faeeb20ee5ccd4cc995925775", + "repo": { + "id": 156617777, + "url": "https://api.github.com/repos/ThiagoCodecov/example-python", + "name": "example-python", + }, + }, + } + ], + "app": { + "id": 254, + "slug": "codecov", + "node_id": "MDM6QXBwMjU0", + "owner": { + "login": "codecov", + "id": 8226205, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=", + "avatar_url": "https://avatars3.githubusercontent.com/u/8226205?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/codecov", + "html_url": "https://github.com/codecov", + "followers_url": "https://api.github.com/users/codecov/followers", + "following_url": "https://api.github.com/users/codecov/following{/other_user}", + "gists_url": "https://api.github.com/users/codecov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/codecov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/codecov/subscriptions", + "organizations_url": "https://api.github.com/users/codecov/orgs", + "repos_url": "https://api.github.com/users/codecov/repos", + "events_url": "https://api.github.com/users/codecov/events{/privacy}", + "received_events_url": "https://api.github.com/users/codecov/received_events", + "type": "Organization", + "site_admin": False, + }, + "name": "Codecov", + "description": "Codecov provides highly integrated tools to group, merge, archive and compare coverage reports. Whether your team is comparing changes in a pull request or reviewing a single commit, Codecov will improve the code review workflow and quality.\r\n\r\n## Code coverage done right.®\r\n\r\n1. Upload coverage reports from your CI builds.\r\n2. Codecov merges all builds and languages into one beautiful coherent report.\r\n3. Get commit statuses, pull request comments and coverage overlay via our browser extension.\r\n\r\nWhen Codecov merges your uploads it keeps track of the CI provider (inc. build details) and user specified context, e.g. `#unittest` ~ `#smoketest` or `#oldcode` ~ `#newcode`. You can track the `#unittest` coverage independently of other groups. [Learn more here](\r\nhttp://docs.codecov.io/docs/flags)\r\n\r\nThrough **Codecov's Browser Extension** reports overlay directly in GitHub UI to assist in code review. [Watch here](https://docs.codecov.io/docs/browser-extension)\r\n\r\n*Highly detailed* **pull request comments** and *customizable* **commit statuses** will improve your team's workflow and code coverage incrementally.\r\n\r\n**File backed configuration** all through the `codecov.yml`. \r\n\r\n## FAQ\r\n- Do you **merge multiple uploads** to the same commit? **Yes**\r\n- Do you **support multiple languages** in the same project? **Yes**\r\n- Can you **group coverage reports** by project and/or test type? **Yes**\r\n- How does **pricing** work? Only paid users can view reports and post statuses/comments. ", + "external_url": "https://codecov.io", + "html_url": "https://github.com/apps/codecov", + "created_at": "2016-09-25T14:18:27Z", + "updated_at": "2020-08-27T18:10:18Z", + "permissions": { + "administration": "read", + "checks": "write", + "contents": "read", + "issues": "read", + "members": "read", + "metadata": "read", + "pull_requests": "write", + "statuses": "write", + }, + "events": [ + "check_run", + "check_suite", + "create", + "delete", + "fork", + "membership", + "public", + "pull_request", + "push", + "release", + "repository", + "status", + "team_add", + ], + }, + "created_at": "2020-10-14T23:00:59Z", + "updated_at": "2020-10-14T23:01:14Z", + "latest_check_runs_count": 1, + "check_runs_url": "https://api.github.com/repos/ThiagoCodecov/example-python/check-suites/1341719124/check-runs", + "head_commit": { + "id": "75f355d8d14ba3d7761c728b4d2607cde0eef065", + "tree_id": "b737740a931a34f5be73f553ea87a1161c917be0", + "message": "Adding README\n\nsurpriseaAKDS\n\nddkokgfnskfds\n\nBanana\n\nYallow\n\nABG", + "timestamp": "2020-10-13T15:15:31Z", + "author": { + "name": "Thiago Ramos", + "email": "thiago@codecov.io", + }, + "committer": { + "name": "Thiago Ramos", + "email": "thiago@codecov.io", + }, + }, + "repository": { + "id": 156617777, + "node_id": "MDEwOlJlcG9zaXRvcnkxNTY2MTc3Nzc=", + "name": "example-python", + "full_name": "ThiagoCodecov/example-python", + "private": False, + "owner": { + "login": "ThiagoCodecov", + "id": 44376991, + "node_id": "MDQ6VXNlcjQ0Mzc2OTkx", + "avatar_url": "https://avatars3.githubusercontent.com/u/44376991?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ThiagoCodecov", + "html_url": "https://github.com/ThiagoCodecov", + "followers_url": "https://api.github.com/users/ThiagoCodecov/followers", + "following_url": "https://api.github.com/users/ThiagoCodecov/following{/other_user}", + "gists_url": "https://api.github.com/users/ThiagoCodecov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ThiagoCodecov/subscriptions", + "organizations_url": "https://api.github.com/users/ThiagoCodecov/orgs", + "repos_url": "https://api.github.com/users/ThiagoCodecov/repos", + "events_url": "https://api.github.com/users/ThiagoCodecov/events{/privacy}", + "received_events_url": "https://api.github.com/users/ThiagoCodecov/received_events", + "type": "User", + "site_admin": False, + }, + "html_url": "https://github.com/ThiagoCodecov/example-python", + "description": "Python coverage example", + "fork": True, + "url": "https://api.github.com/repos/ThiagoCodecov/example-python", + "forks_url": "https://api.github.com/repos/ThiagoCodecov/example-python/forks", + "keys_url": "https://api.github.com/repos/ThiagoCodecov/example-python/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/ThiagoCodecov/example-python/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/ThiagoCodecov/example-python/teams", + "hooks_url": "https://api.github.com/repos/ThiagoCodecov/example-python/hooks", + "issue_events_url": "https://api.github.com/repos/ThiagoCodecov/example-python/issues/events{/number}", + "events_url": "https://api.github.com/repos/ThiagoCodecov/example-python/events", + "assignees_url": "https://api.github.com/repos/ThiagoCodecov/example-python/assignees{/user}", + "branches_url": "https://api.github.com/repos/ThiagoCodecov/example-python/branches{/branch}", + "tags_url": "https://api.github.com/repos/ThiagoCodecov/example-python/tags", + "blobs_url": "https://api.github.com/repos/ThiagoCodecov/example-python/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/ThiagoCodecov/example-python/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/ThiagoCodecov/example-python/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/ThiagoCodecov/example-python/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/ThiagoCodecov/example-python/statuses/{sha}", + "languages_url": "https://api.github.com/repos/ThiagoCodecov/example-python/languages", + "stargazers_url": "https://api.github.com/repos/ThiagoCodecov/example-python/stargazers", + "contributors_url": "https://api.github.com/repos/ThiagoCodecov/example-python/contributors", + "subscribers_url": "https://api.github.com/repos/ThiagoCodecov/example-python/subscribers", + "subscription_url": "https://api.github.com/repos/ThiagoCodecov/example-python/subscription", + "commits_url": "https://api.github.com/repos/ThiagoCodecov/example-python/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/ThiagoCodecov/example-python/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/ThiagoCodecov/example-python/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/ThiagoCodecov/example-python/contents/{+path}", + "compare_url": "https://api.github.com/repos/ThiagoCodecov/example-python/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/ThiagoCodecov/example-python/merges", + "archive_url": "https://api.github.com/repos/ThiagoCodecov/example-python/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/ThiagoCodecov/example-python/downloads", + "issues_url": "https://api.github.com/repos/ThiagoCodecov/example-python/issues{/number}", + "pulls_url": "https://api.github.com/repos/ThiagoCodecov/example-python/pulls{/number}", + "milestones_url": "https://api.github.com/repos/ThiagoCodecov/example-python/milestones{/number}", + "notifications_url": "https://api.github.com/repos/ThiagoCodecov/example-python/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/ThiagoCodecov/example-python/labels{/name}", + "releases_url": "https://api.github.com/repos/ThiagoCodecov/example-python/releases{/id}", + "deployments_url": "https://api.github.com/repos/ThiagoCodecov/example-python/deployments", + }, + } + ], + } + assert res == expected_result + + @pytest.mark.asyncio + async def test_list_github_app_webhook_deliveries(self, codecov_vcr): + ghapp_handler = Github( + token=dict(key="not the real token"), + oauth_consumer_token=dict( + key="client_id", + secret="client_secret", + ), + ) + deliveries = [] + async for res in ghapp_handler.list_webhook_deliveries(): + deliveries += res + assert len(deliveries) == 16 + assert deliveries[0] == { + "id": 17324040107, + "guid": "53c93580-7a6e-11ed-96c9-5e1ce3e5574e", + "delivered_at": "2022-12-12T22:42:59Z", + "redelivery": False, + "duration": 0.37, + "status": "OK", + "status_code": 200, + "event": "installation_repositories", + "action": "added", + "installation_id": None, + "repository_id": None, + "url": "", + } + + @pytest.mark.asyncio + async def test_list_github_app_webhook_redelivery(self, codecov_vcr): + ghapp_handler = Github( + token=dict(key="not real token"), + oauth_consumer_token=dict( + key="client_id", + secret="client_secret", + ), + ) + res = await ghapp_handler.request_webhook_redelivery(17322555251) + assert res is True + + @pytest.mark.asyncio + @pytest.mark.django_db(databases={"default"}) + async def test_get_repos_from_nodeids_generator(self, valid_handler, codecov_vcr): + repo_node_ids = ["R_kgDOHrbKcg", "R_kgDOLEJx2g"] + expected = [ + { + "service_id": 515295858, + "name": "example-python", + "language": "shell", + "private": False, + "branch": "main", + "owner": { + "node_id": "U_kgDOBZOfKw", + "username": "codecove2e", + "is_expected_owner": True, + }, + }, + { + "service_id": 742552026, + "name": "test-no-languages", + "language": None, + "private": False, + "branch": "main", + "owner": { + "node_id": "U_kgDOBZOfKw", + "username": "codecove2e", + "is_expected_owner": True, + }, + }, + ] + received = [ + repo + async for repo in valid_handler.get_repos_from_nodeids_generator( + repo_node_ids, "codecove2e" + ) + ] + assert received == expected diff --git a/libs/shared/tests/integration/test_gitlab.py b/libs/shared/tests/integration/test_gitlab.py new file mode 100644 index 0000000000..36c2277ede --- /dev/null +++ b/libs/shared/tests/integration/test_gitlab.py @@ -0,0 +1,1119 @@ +import pytest +import vcr +from prometheus_client import REGISTRY + +from shared.torngit.enums import Endpoints +from shared.torngit.exceptions import TorngitClientError, TorngitObjectNotFoundError +from shared.torngit.gitlab import Gitlab + + +@pytest.fixture +def valid_handler(): + return Gitlab( + repo=dict(service_id="187725", name="ci-repo"), + owner=dict(username="codecov", service_id="109479"), + token=dict(key=16 * "f882"), + ) + + +@pytest.fixture +def subgroup_handler(): + return Gitlab( + repo=dict(service_id="187725", name="codecov-test"), + owner=dict(username="group:subgroup1:subgroup2", service_id="7983213"), + token=dict(key=16 * "f882"), + ) + + +@pytest.fixture +def admin_handler(): + return Gitlab( + repo=dict(service_id="12060694"), + owner=dict(username="codecov-organization", service_id="4037482"), + token=dict(key=16 * "f882"), + ) + + +class TestGitlabTestCase(object): + @pytest.mark.asyncio + async def test_get_is_admin(self, admin_handler, codecov_vcr): + user = dict(service_id="3108129") + is_admin = await admin_handler.get_is_admin( + user=user, token=dict(key=16 * "f882", username="hootener") + ) + assert is_admin + + @pytest.mark.asyncio + async def test_get_best_effort_branches(self, valid_handler, codecov_vcr): + branches = await valid_handler.get_best_effort_branches( + "c739768fcac68144a3a6d82305b9c4106934d31a" + ) + assert branches == ["main", "other-branch"] + + @pytest.mark.asyncio + async def test_post_comment(self, valid_handler, codecov_vcr): + expected_result = { + "id": 113977323, + "noteable_id": 59639, + "noteable_iid": 1, + "noteable_type": "MergeRequest", + "resolvable": False, + "system": False, + "type": None, + "updated_at": "2018-11-02T05:25:09.363Z", + "attachment": None, + "author": { + "avatar_url": "https://secure.gravatar.com/avatar/dcdb35375db567705dd7e74226fae67b?s=80&d=identicon", + "name": "Codecov", + "state": "active", + "id": 109640, + "username": "codecov", + "web_url": "https://gitlab.com/codecov", + }, + "body": "Hello world", + "created_at": "2018-11-02T05:25:09.363Z", + } + res = await valid_handler.post_comment("1", "Hello world") + assert res["author"] == expected_result["author"] + assert res == expected_result + + @pytest.mark.asyncio + async def test_edit_comment(self, valid_handler, codecov_vcr): + res = await valid_handler.edit_comment("1", "113977323", "Hello world number 2") + assert res is not None + assert res["id"] == 113977323 + + @pytest.mark.asyncio + async def test_edit_comment_not_found(self, valid_handler, codecov_vcr): + with pytest.raises(TorngitObjectNotFoundError): + await valid_handler.edit_comment("1", 113979999, "Hello world number 2") + + @pytest.mark.asyncio + async def test_delete_comment(self, valid_handler, codecov_vcr): + assert await valid_handler.delete_comment("1", "113977323") is True + + @pytest.mark.asyncio + async def test_delete_comment_not_found(self, valid_handler, codecov_vcr): + with pytest.raises(TorngitObjectNotFoundError): + await valid_handler.delete_comment("1", 113977999) + + @pytest.mark.asyncio + async def test_find_pull_request_nothing_found(self, valid_handler, codecov_vcr): + # nothing matches commit or branch + assert await valid_handler.find_pull_request("a" * 40, "no-branch") is None + + @pytest.mark.asyncio + async def test_find_pull_request_pr_found(self, valid_handler, codecov_vcr): + commitid = "dd798926730aad14aadf72281204bdb85734fe67" + assert ( + await valid_handler.find_pull_request(commit=commitid, state="close") == 2 + ) + assert await valid_handler.find_pull_request(commit=commitid, state="open") == 1 + + @pytest.mark.asyncio + async def test_find_pull_request_pr_found_branch(self, valid_handler, codecov_vcr): + branch = "other-branch" + assert await valid_handler.find_pull_request(branch=branch, state="close") == 2 + assert await valid_handler.find_pull_request(branch=branch, state="open") == 1 + + @pytest.mark.asyncio + async def test_find_pull_request_merge_requests_disabled( + self, valid_handler, codecov_vcr + ): + # merge requests turned off on Gitlab settings + res = await valid_handler.find_pull_request("a" * 40) + assert res is None + + @pytest.mark.asyncio + async def test_find_pull_request_project_not_found( + self, valid_handler, codecov_vcr + ): + with pytest.raises(TorngitClientError) as excinfo: + await valid_handler.find_pull_request("a" * 40) + assert excinfo.value.code == 404 + + @pytest.mark.asyncio + async def test_get_pull_request_fail(self, valid_handler, codecov_vcr): + with pytest.raises(TorngitObjectNotFoundError): + await valid_handler.get_pull_request("100") + + get_pull_request_test_data = [ + ( + "1", + { + "base": { + "branch": "main", + "commitid": "5716de23b27020419d1a40dd93b469c041a1eeef", + }, + "head": { + "branch": "other-branch", + "commitid": "dd798926730aad14aadf72281204bdb85734fe67", + }, + "number": "1", + "id": "1", + "state": "merged", + "title": "Other branch", + "author": {"id": "109640", "username": "codecov"}, + "merge_commit_sha": "dd798926730aad14aadf72281204bdb85734fe67", + }, + ) + ] + + @pytest.mark.asyncio + @pytest.mark.parametrize("a,b", get_pull_request_test_data) + async def test_get_pull_request(self, valid_handler, a, b, codecov_vcr): + res = await valid_handler.get_pull_request(a) + assert res == b + + @pytest.mark.asyncio + async def test_get_pull_request_with_diff_refs(self, codecov_vcr): + recent_handler = Gitlab( + repo=dict(service_id="18347774", name="codecov-example"), + owner=dict(username="ThiagoCodecov", service_id="_meaningless_"), + token=dict(key=16 * "f882"), + ) + res = await recent_handler.get_pull_request("1") + assert res == { + "author": {"id": "3124507", "username": "ThiagoCodecov"}, + "base": { + "branch": "main", + "commitid": "081d91921f05a8a39d39aef667eddb88e96300c7", + }, + "head": { + "branch": "thiago/base-no-base", + "commitid": "b34b00d0872d129943b634693fd8f19f5f37acf9", + }, + "state": "merged", + "title": "Thiago/base no base", + "id": "1", + "number": "1", + "merge_commit_sha": "b34b00d0872d129943b634693fd8f19f5f37acf9", + } + + @pytest.mark.asyncio + async def test_get_pipeline_details(self, valid_handler: Gitlab, codecov_vcr): + res = await valid_handler.get_pipeline_details(61173290, 7676952799) + assert res == "508c25daba5bbc77d8e7cf3c1917d5859153cfd3" + + @pytest.mark.asyncio + async def test_get_pipeline_details_fail(self, valid_handler: Gitlab, codecov_vcr): + res = await valid_handler.get_pipeline_details(61173000, 7676952000) + assert res is None + + @pytest.mark.asyncio + async def test_get_pull_request_files(self, codecov_vcr): + recent_handler = Gitlab( + repo=dict(service_id="30951850", name="learn-gitlab"), + owner=dict(username="codecove2e", service_id="10119799"), + token=dict(key=16 * "f882"), + ) + res = await recent_handler.get_pull_request_files("1") + + assert res == [ + "README.md", + ] + + @pytest.mark.asyncio + async def test_get_pull_request_commits(self, valid_handler, codecov_vcr): + expected_result = ["dd798926730aad14aadf72281204bdb85734fe67"] + res = await valid_handler.get_pull_request_commits("1") + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_pull_requests(self, valid_handler, codecov_vcr): + expected_result = [1] + res = await valid_handler.get_pull_requests() + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_commit(self, valid_handler, codecov_vcr): + commit = await valid_handler.get_commit( + "0028015f7fa260f5fd68f78c0deffc15183d955e" + ) + assert commit == { + "author": { + "id": None, + "username": None, + "email": "steve@stevepeak.net", + "name": "stevepeak", + }, + "message": "added large file\n", + "parents": ["5716de23b27020419d1a40dd93b469c041a1eeef"], + "commitid": "0028015f7fa260f5fd68f78c0deffc15183d955e", + "timestamp": "2014-10-19T14:32:33.000Z", + } + + @pytest.mark.asyncio + async def test_get_commit_not_found(self, valid_handler, codecov_vcr): + with pytest.raises(TorngitObjectNotFoundError): + await valid_handler.get_commit("none") + + @pytest.mark.asyncio + async def test_get_commit_diff_file_change(self, valid_handler, codecov_vcr): + expected_result = { + "files": { + "large.md": { + "before": None, + "segments": [{"header": ["0", "0", "1", "816"]}], + "stats": {"added": 816, "removed": 0}, + "type": "modified", + } + } + } + res = await valid_handler.get_commit_diff( + "0028015f7fa260f5fd68f78c0deffc15183d955e" + ) + assert "files" in res + assert "large.md" in res["files"] + assert "segments" in res["files"]["large.md"] + assert len(res["files"]["large.md"]["segments"]) == 1 + assert "lines" in res["files"]["large.md"]["segments"][0] + assert len(res["files"]["large.md"]["segments"][0].pop("lines")) == 816 + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_commit_diff(self, valid_handler, codecov_vcr): + expected_result = { + "files": { + "README.md": { + "before": None, + "segments": [ + { + "header": ["1", "5", "1", "15"], + "lines": [ + "-### Example", + "+### CI Testing", + " ", + "-> This repo is used for CI " + "Testing. Enjoy this gif as a " + "reward!", + "+> This repo is used for CI Testing", + "+", + "+", + "+| [https://codecov.io/][1] " + "| [@codecov][2] | " + "[hello@codecov.io][3] |", + "+| ------------------------ " + "| ------------- | " + "--------------------- |", + "+", + "+-----", + "+", + "+", + "+[1]: https://codecov.io/", + "+[2]: https://twitter.com/codecov", + "+[3]: mailto:hello@codecov.io", + " ", + "-![i can do that](http://gph.is/17cvPc4)", + ], + } + ], + "stats": {"added": 13, "removed": 3}, + "type": "modified", + } + } + } + res = await valid_handler.get_commit_diff( + "c739768fcac68144a3a6d82305b9c4106934d31a" + ) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_commit_statuses(self, valid_handler, codecov_vcr): + res = await valid_handler.get_commit_statuses( + "c739768fcac68144a3a6d82305b9c4106934d31a" + ) + assert res == "success" + + @pytest.mark.asyncio + async def test_set_commit_status(self, valid_handler, codecov_vcr): + target_url = "https://localhost:50036/gitlab/codecov/ci-repo?ref=ad798926730aad14aadf72281204bdb85734fe67" + expected_result = { + "allow_failure": False, + "author": { + "avatar_url": "https://secure.gravatar.com/avatar/dcdb35375db567705dd7e74226fae67b?s=80&d=identicon", + "id": 109640, + "name": "Codecov", + "state": "active", + "username": "codecov", + "web_url": "https://gitlab.com/codecov", + }, + "coverage": None, + "description": "aaaaaaaaaa", + "finished_at": "2018-11-05T20:11:18.137Z", + "id": 116703167, + "name": "context", + "ref": "main", + "sha": "c739768fcac68144a3a6d82305b9c4106934d31a", + "started_at": None, + "status": "success", + "target_url": target_url, + "created_at": "2018-11-05T20:11:18.104Z", + } + res = await valid_handler.set_commit_status( + "c739768fcac68144a3a6d82305b9c4106934d31a", + "success", + "context", + "aaaaaaaaaa", + target_url, + ) + assert res["author"] == expected_result["author"] + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_branches(self, valid_handler, codecov_vcr): + branches = sorted(await valid_handler.get_branches()) + assert list(map(lambda a: a[0], branches)) == ["main", "other-branch"] + + @pytest.mark.asyncio + async def test_get_branch(self, valid_handler, codecov_vcr): + expected_result = { + "name": "main", + "sha": "0fc784af11c401449e56b24a174bae7b9af86c98", + } + branch = await valid_handler.get_branch("main") + assert branch == expected_result + + @pytest.mark.asyncio + async def test_post_webhook(self, valid_handler, codecov_vcr): + url = "http://requestbin.net/r/1ecyaj51" + name, events, secret = "a", {"job_events": True}, "d" + expected_result = { + "confidential_issues_events": False, + "confidential_note_events": None, + "created_at": "2018-11-06T04:51:57.164Z", + "enable_ssl_verification": True, + "id": 422507, + "issues_events": False, + "job_events": True, + "merge_requests_events": False, + "note_events": False, + "pipeline_events": False, + "project_id": 187725, + "push_events": True, + "push_events_branch_filter": None, + "repository_update_events": False, + "tag_push_events": False, + "url": "http://requestbin.net/r/1ecyaj51", + "wiki_page_events": False, + } + res = await valid_handler.post_webhook(name, url, events, secret) + assert res == expected_result + + @pytest.mark.asyncio + async def test_edit_webhook(self, valid_handler, codecov_vcr): + url = "http://requestbin.net/r/1ecyaj51" + events = {"tag_push_events": True, "note_events": True} + new_name, secret = "new_name", "new_secret" + expected_result = { + "confidential_issues_events": False, + "confidential_note_events": None, + "created_at": "2018-11-06T04:51:57.164Z", + "enable_ssl_verification": True, + "id": 422507, + "issues_events": False, + "job_events": True, + "merge_requests_events": False, + "note_events": True, # Notice this changed + "pipeline_events": False, + "project_id": 187725, + "push_events": True, + "push_events_branch_filter": None, + "repository_update_events": False, + "tag_push_events": True, # Notice this changeds + "url": "http://requestbin.net/r/1ecyaj51", + "wiki_page_events": False, + } + res = await valid_handler.edit_webhook("422507", new_name, url, events, secret) + assert res == expected_result + + @pytest.mark.asyncio + async def test_delete_webhook(self, valid_handler, codecov_vcr): + res = await valid_handler.delete_webhook("422507") + assert res is True + + @pytest.mark.asyncio + async def test_delete_webhook_not_found(self, valid_handler, codecov_vcr): + with pytest.raises(TorngitObjectNotFoundError): + await valid_handler.delete_webhook("422507987") + + @pytest.mark.asyncio + async def test_get_authenticated(self, valid_handler, codecov_vcr): + res = await valid_handler.get_authenticated() + assert res == (True, True) + + @pytest.mark.asyncio + async def test_get_compare(self, valid_handler, codecov_vcr): + base, head = "b33e1281", "5716de23" + expected_result = { + "diff": { + "files": { + "README.md": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["1", "5", "1", "15"], + "lines": [ + "-### Example", + "+### CI Testing", + " ", + "-> This repo is used for CI Testing. Enjoy this gif as a reward!", + "+> This repo is used for CI Testing", + "+", + "+", + "+| [https://codecov.io/][1] | [@codecov][2] | [hello@codecov.io][3] |", + "+| ------------------------ | ------------- | --------------------- |", + "+", + "+-----", + "+", + "+", + "+[1]: https://codecov.io/", + "+[2]: https://twitter.com/codecov", + "+[3]: mailto:hello@codecov.io", + " ", + "-![i can do that](http://gph.is/17cvPc4)", + ], + } + ], + "stats": {"added": 13, "removed": 3}, + }, + "folder/hello-world.txt": { + "type": "modified", + "before": None, + "segments": [ + {"header": ["0", "0", "1", ""], "lines": ["+hello world"]} + ], + "stats": {"added": 1, "removed": 0}, + }, + } + }, + "commits": [ + { + "commitid": "5716de23b27020419d1a40dd93b469c041a1eeef", + "message": "addd folder", + "timestamp": "2014-08-21T18:36:38.000Z", + "author": {"email": "steve@stevepeak.net", "name": "stevepeak"}, + }, + { + "commitid": "c739768fcac68144a3a6d82305b9c4106934d31a", + "message": "shhhh i'm batman!", + "timestamp": "2014-08-20T21:52:44.000Z", + "author": {"email": "steve@stevepeak.net", "name": "stevepeak"}, + }, + ], + } + res = await valid_handler.get_compare(base, head) + assert sorted(list(res.keys())) == sorted(list(expected_result.keys())) + for key in res: + assert res[key] == expected_result[key] + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_repository(self, valid_handler, codecov_vcr): + expected_result = { + "owner": {"service_id": "109640", "username": "codecov"}, + "repo": { + "branch": "main", + "language": None, + "name": "ci-repo", + "private": False, + "service_id": "187725", + }, + } + res = await valid_handler.get_repository() + assert res["repo"] == expected_result["repo"] + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_repo_languages(self, valid_handler, codecov_vcr): + expected_result = ["python"] + res = await valid_handler.get_repo_languages() + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_repository_subgroup(self, valid_handler, codecov_vcr): + # test get_repository for repo in a subgroup + expected_result = { + "owner": {"service_id": "4165905", "username": "l00p_group_1:subgroup1"}, + "repo": { + "branch": "main", + "language": None, + "name": "proj-a", + "private": True, + "service_id": "9715852", + }, + } + res = await Gitlab( + repo=dict(service_id="9715852"), + owner=dict(username="1nf1n1t3l00p"), + token=dict(key=16 * "f882"), + ).get_repository() + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_repository_subgroup_no_repo_service_id(self, codecov_vcr): + # test get repo in a subgroup when no repo service_id which happens when a user + # tries to view a repo on legacy codecov.io and the repo is not in the database yet + expected_result = { + "owner": {"service_id": "4165905", "username": "l00p_group_1:subgroup1"}, + "repo": { + "branch": "main", + "language": None, + "name": "proj-a", + "private": True, + "service_id": "9715852", + }, + } + res = await Gitlab( + repo=dict(name="proj-a"), + owner=dict(username="l00p_group_1:subgroup1"), + token=dict(key=16 * "f882"), + ).get_repository() + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_source_master(self, valid_handler, codecov_vcr): + expected_result = { + "commitid": None, + "content": b"import unittest\nimport my_package\n\n\nclass TestMethods(unittest.TestCase):\n def test_add(self):\n self.assertEqual(my_package.add(10), 20)\n\nif __name__ == '__main__':\n unittest.main()\n", + } + path, ref = "tests.py", "master" + res = await valid_handler.get_source(path, ref) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_source_random_commit(self, valid_handler, codecov_vcr): + expected_result = {"commitid": None, "content": b"hello world\n"} + path, ref = "folder/hello-world.txt", "5716de23" + res = await valid_handler.get_source(path, ref) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_source_random_commit_not_found(self, valid_handler, codecov_vcr): + path, ref = "awesome/non_exising_file.py", "5716de23" + with pytest.raises(TorngitObjectNotFoundError): + await valid_handler.get_source(path, ref) + + @pytest.mark.asyncio + async def test_list_repos(self, valid_handler, codecov_vcr): + expected_result = [ + { + "owner": {"service_id": "189208", "username": "morerunes"}, + "repo": { + "branch": "main", + "language": None, + "name": "delectamentum-mud-server", + "private": False, + "service_id": "1384844", + }, + }, + { + "owner": {"service_id": "109640", "username": "codecov"}, + "repo": { + "branch": "main", + "language": None, + "name": "example-python", + "private": False, + "service_id": "580838", + }, + }, + { + "owner": {"service_id": "109640", "username": "codecov"}, + "repo": { + "branch": "main", + "language": None, + "name": "ci-private", + "private": True, + "service_id": "190307", + }, + }, + { + "owner": {"service_id": "109640", "username": "codecov"}, + "repo": { + "branch": "main", + "language": None, + "name": "ci-repo", + "private": False, + "service_id": "187725", + }, + }, + ] + res = await valid_handler.list_repos() + assert res == expected_result + + @pytest.mark.asyncio + @vcr.use_cassette( + "tests/integration/cassetes/test_gitlab/TestGitlabTestCase/test_list_repos.yaml", + record_mode="once", + filter_headers=["authorization"], + match_on=["method", "scheme", "host", "port", "path", "query"], + filter_query_parameters=["oauth_nonce", "oauth_timestamp", "oauth_signature"], + ) + async def test_list_repos_generator(self, valid_handler, codecov_vcr): + expected_result = [ + { + "owner": {"service_id": "189208", "username": "morerunes"}, + "repo": { + "branch": "main", + "language": None, + "name": "delectamentum-mud-server", + "private": False, + "service_id": "1384844", + }, + }, + { + "owner": {"service_id": "109640", "username": "codecov"}, + "repo": { + "branch": "main", + "language": None, + "name": "example-python", + "private": False, + "service_id": "580838", + }, + }, + { + "owner": {"service_id": "109640", "username": "codecov"}, + "repo": { + "branch": "main", + "language": None, + "name": "ci-private", + "private": True, + "service_id": "190307", + }, + }, + { + "owner": {"service_id": "109640", "username": "codecov"}, + "repo": { + "branch": "main", + "language": None, + "name": "ci-repo", + "private": False, + "service_id": "187725", + }, + }, + ] + repos = [] + page_count = 0 + async for page in valid_handler.list_repos_generator(): + repos.extend(page) + page_count += 1 + assert page_count == 1 + assert repos == expected_result + + @pytest.mark.asyncio + async def test_list_repos_subgroups(self, valid_handler, codecov_vcr): + expected_result = [ + { + "owner": { + "service_id": "4165907", + "username": "l00p_group_1:subgroup2", + }, + "repo": { + "service_id": "9715886", + "name": "flake8", + "private": True, + "language": None, + "branch": "main", + }, + }, + { + "owner": {"service_id": "3215137", "username": "1nf1n1t3l00p"}, + "repo": { + "service_id": "9715862", + "name": "inf-proj", + "private": True, + "language": None, + "branch": "main", + }, + }, + { + "owner": {"service_id": "4165904", "username": "l00p_group_1"}, + "repo": { + "service_id": "9715859", + "name": "loop-proj", + "private": True, + "language": None, + "branch": "main", + }, + }, + { + "owner": { + "service_id": "4165905", + "username": "l00p_group_1:subgroup1", + }, + "repo": { + "service_id": "9715852", + "name": "proj-a", + "private": True, + "language": None, + "branch": "main", + }, + }, + ] + res = await Gitlab( + repo=dict(service_id="9715852"), + owner=dict(username="1nf1n1t3l00p"), + token=dict(key=16 * "f882"), + ).list_repos() + assert res == expected_result + + @pytest.mark.asyncio + async def test_list_repos_subgroups_from_subgroups_username( + self, valid_handler, codecov_vcr + ): + expected_result = [ + { + "owner": {"service_id": "4037482", "username": "codecov-organization"}, + "repo": { + "branch": "main", + "language": None, + "name": "demo-gitlab", + "private": True, + "service_id": "12060694", + }, + }, + { + "owner": {"service_id": "4037482", "username": "codecov-organization"}, + "repo": { + "branch": "main", + "language": None, + "name": "codecov-assume-flag-test", + "private": True, + "service_id": "10575601", + }, + }, + { + "owner": {"service_id": "4037482", "username": "codecov-organization"}, + "repo": { + "branch": "main", + "language": None, + "name": "migration-tests", + "private": True, + "service_id": "9422435", + }, + }, + { + "owner": { + "service_id": "5938764", + "username": "thiagocodecovtestgroup:test-subgroup", + }, + "repo": { + "branch": "main", + "language": None, + "name": "tasks", + "private": True, + "service_id": "14027433", + }, + }, + { + "owner": { + "service_id": "5938764", + "username": "thiagocodecovtestgroup:test-subgroup", + }, + "repo": { + "branch": "main", + "language": None, + "name": "grouptestprojecttrr", + "private": True, + "service_id": "14026543", + }, + }, + ] + res = await Gitlab( + repo=dict(service_id="5938764"), + owner=dict(username="thiagocodecovtestgroup:test-subgroup"), + token=dict(key=16 * "f882"), + ).list_repos() + assert res == expected_result + + @pytest.mark.asyncio + async def test_list_teams(self, valid_handler, codecov_vcr): + expected_result = [ + { + "id": 726800, + "name": "delectamentum-mud", + "username": "delectamentum-mud", + "avatar_url": None, + "parent_id": None, + } + ] + res = await valid_handler.list_teams() + assert res == expected_result + + @pytest.mark.asyncio + async def test_list_teams_subgroups(self, valid_handler, codecov_vcr): + expected_result = [ + { + "username": "l00p_group_1", + "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/4165904/avatar.png", + "id": 4165904, + "name": "My Awesome Group", + "parent_id": None, + }, + { + "username": "l00p_group_1:subgroup1", + "avatar_url": None, + "id": 4165905, + "name": "subgroup1", + "parent_id": 4165904, + }, + { + "username": "l00p_group_1:subgroup2", + "avatar_url": None, + "id": 4165907, + "name": "subgroup2", + "parent_id": 4165904, + }, + ] + res = await Gitlab( + repo=dict(service_id="9715852"), + owner=dict(username="1nf1n1t3l00p"), + token=dict(key=16 * "f882"), + ).list_teams() + assert res == expected_result + + @pytest.mark.asyncio + async def test_list_top_level_files(self, valid_handler, codecov_vcr): + expected_result = [ + { + "id": "1da1ddbfe1ed846f7493964bf489754e464eef64", + "mode": "040000", + "name": "folder", + "path": "folder", + "type": "folder", + }, + { + "id": "c77cff04774d6debf9f8f645323fbe1cea368692", + "mode": "100644", + "name": ".gitignore", + "path": ".gitignore", + "type": "file", + }, + { + "id": "321cc67810818865affe8f6bac28f50d3c0a761c", + "mode": "100644", + "name": ".travis.yml", + "path": ".travis.yml", + "type": "file", + }, + { + "id": "7974a2260a70aab9ce8ae581fba307c6d448c468", + "mode": "100644", + "name": "README.md", + "path": "README.md", + "type": "file", + }, + { + "id": "c6b04a8c4a6bd3f8c12e65c7ad3ac759166298dd", + "mode": "100644", + "name": "large.md", + "path": "large.md", + "type": "file", + }, + { + "id": "478e1519b72ffd69712d77c5f50dd45b203846c4", + "mode": "100644", + "name": "my_package.py", + "path": "my_package.py", + "type": "file", + }, + { + "id": "20642e5c79ebd16b1c87ca300ff8b1afd478be5e", + "mode": "100644", + "name": "tests.py", + "path": "tests.py", + "type": "file", + }, + ] + + res = await valid_handler.list_top_level_files("main") + assert sorted(res, key=lambda x: x["path"]) == sorted( + expected_result, key=lambda x: x["path"] + ) + + @pytest.mark.asyncio + async def test_list_files(self, valid_handler, codecov_vcr): + expected_result = [ + { + "id": "3b18e512dba79e4c8300dd08aeb37f8e728b8dad", + "name": "hello-world.txt", + "type": "file", + "path": "folder/hello-world.txt", + "mode": "100644", + } + ] + + res = await valid_handler.list_files("main", "folder") + assert sorted(res, key=lambda x: x["path"]) == sorted( + expected_result, key=lambda x: x["path"] + ) + + @pytest.mark.asyncio + async def test_get_ancestors_tree(self, valid_handler, codecov_vcr): + commitid = "c739768fcac68144a3a6d82305b9c4106934d31a" + res = await valid_handler.get_ancestors_tree(commitid) + expected_result = { + "commitid": "c739768fcac68144a3a6d82305b9c4106934d31a", + "parents": [ + { + "commitid": "b33e12816cc3f386dae8add4968cedeff5155021", + "parents": [ + { + "commitid": "743b04806ea677403aa2ff26c6bdeb85005de658", + "parents": [], + } + ], + } + ], + } + assert res == expected_result + + def test_get_href(self, valid_handler): + expected_result = "https://gitlab.com/codecov/ci-repo/commit/743b04806ea677403aa2ff26c6bdeb85005de658" + res = valid_handler.get_href( + Endpoints.commit_detail, commitid="743b04806ea677403aa2ff26c6bdeb85005de658" + ) + assert res == expected_result + + def test_get_href_subgroup(self, subgroup_handler): + commitid = "743b04806ea677403aa2ff26c6bdeb85005de658" + expected_result = f"https://gitlab.com/group/subgroup1/subgroup2/codecov-test/commit/{commitid}" + res = subgroup_handler.get_href(Endpoints.commit_detail, commitid=commitid) + assert res == expected_result + + @pytest.mark.asyncio + async def test_make_paginated_call_no_limit(self, codecov_vcr): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "list_teams"}, + ) + handler = Gitlab( + repo=dict(service_id="9715852"), + owner=dict(username="1nf1n1t3l00p"), + token=dict(key=16 * "f882"), + ) + url = handler.count_and_get_url_template("list_teams").substitute() + res = handler.make_paginated_call( + url, max_per_page=100, default_kwargs={}, counter_name="list_teams" + ) + res = [i async for i in res] + assert len(res) == 1 + assert list(len(p) for p in res) == [8] + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "list_teams"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_make_paginated_call(self, codecov_vcr): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "list_teams"}, + ) + handler = Gitlab( + repo=dict(service_id="9715852"), + owner=dict(username="1nf1n1t3l00p"), + token=dict(key=16 * "f882"), + ) + url = handler.count_and_get_url_template("list_teams").substitute() + res = handler.make_paginated_call( + url, max_per_page=4, default_kwargs={}, counter_name="list_teams" + ) + res = [i async for i in res] + assert len(res) == 3 + assert list(len(p) for p in res) == [4, 4, 1] + assert codecov_vcr.play_count == 3 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "list_teams"}, + ) + assert after - before == 3 + + @pytest.mark.asyncio + async def test_make_paginated_call_max_number_of_pages(self, codecov_vcr): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "list_teams"}, + ) + handler = Gitlab( + repo=dict(service_id="9715852"), + owner=dict(username="1nf1n1t3l00p"), + token=dict(key=16 * "f882"), + ) + url = handler.count_and_get_url_template("list_teams").substitute() + res = handler.make_paginated_call( + url, + max_per_page=3, + max_number_of_pages=2, + default_kwargs={}, + counter_name="list_teams", + ) + res = [i async for i in res] + assert len(res) == 2 + assert list(len(p) for p in res) == [3, 3] + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "list_teams"}, + ) + assert after - before == 2 + + @pytest.mark.asyncio + async def test_get_authenticated_user(self, codecov_vcr): + code = "7c005cbb342626fffe4f24e5eedac28d9e8fa1c8592808dd294bfe0d39ea084d" + handler = Gitlab(oauth_consumer_token=dict(key=16 * "f882", secret=16 * "f882")) + res = await handler.get_authenticated_user( + code, "http://localhost:8000/login/gl" + ) + assert res == { + "id": 3124507, + "name": "Thiago Ramos", + "username": "ThiagoCodecov", + "state": "active", + "avatar_url": "https://secure.gravatar.com/avatar/337420e188ca8138d4b8599d3a20ad47?s=80&d=identicon", + "web_url": "https://gitlab.com/ThiagoCodecov", + "created_at": 1599149427, + "bio": "", + "bio_html": "", + "location": None, + "public_email": "", + "skype": "", + "linkedin": "", + "twitter": "", + "website_url": "", + "organization": None, + "job_title": "", + "work_information": None, + "last_sign_in_at": "2020-05-28T21:29:21.563Z", + "confirmed_at": "2018-11-12T19:05:01.249Z", + "last_activity_on": "2020-09-03", + "email": "thiago@codecov.io", + "theme_id": 1, + "color_scheme_id": 1, + "projects_limit": 100000, + "current_sign_in_at": "2020-09-03T15:46:43.559Z", + "identities": [ + { + "provider": "google_oauth2", + "extern_uid": "114705562456763720684", + "saml_provider_id": None, + } + ], + "can_create_group": True, + "can_create_project": True, + "two_factor_enabled": False, + "external": False, + "private_profile": False, + "shared_runners_minutes_limit": None, + "extra_shared_runners_minutes_limit": None, + "access_token": "testhi9nk25akajgzhudabirpz3vjau7qe8i9mavz2d9i1i0cfwjp8ggkcqavglx", + "token_type": "Bearer", + "refresh_token": "testwnoeg1a4bjhoa65vzxdn8grh4asp0b6l4idtdazw7ps5h8xx77m8gbyty7gi", + "scope": "api", + } + + @pytest.mark.asyncio + async def test_is_student(self, valid_handler, codecov_vcr): + res = await valid_handler.is_student() + assert not res diff --git a/libs/shared/tests/integration/test_report.py b/libs/shared/tests/integration/test_report.py new file mode 100644 index 0000000000..c2d96d6e6f --- /dev/null +++ b/libs/shared/tests/integration/test_report.py @@ -0,0 +1,752 @@ +import pytest + +from shared.reports.resources import Report, ReportFile +from shared.reports.types import ( + Change, + LineSession, + NetworkFile, + ReportLine, + ReportTotals, +) +from shared.utils.sessions import Session +from tests.helper import v2_to_v3 + + +@pytest.mark.integration +def test_report_repr(): + assert repr(Report()) == "" + + +@pytest.mark.integration +@pytest.mark.parametrize( + "r, network", + [ + ( + Report(files={"py.py": [0, ReportTotals(1)]}), + [ + ( + "py.py", + NetworkFile(totals=ReportTotals(1), diff_totals=None), + ) + ], + ), + ( + Report( + files={"py.py": [0, ReportTotals(1, 1, 1, 1, 1, 1)]}, + chunks="null\n[1]\n[1]\n[1]\n<<<<< end_of_chunk >>>>>\nnull\n[1]\n[1]\n[1]", + ).filter(paths=["py.py"]), + [ + ( + "py.py", + NetworkFile( + totals=ReportTotals(1, 1, 1, 1, 1, 1), + diff_totals=None, + ), + ) + ], + ), + ], +) +def test_network(r, network): + assert list(r.network) == network + + +@pytest.mark.integration +@pytest.mark.parametrize( + "r", + [ + (Report(files={"py.py": [0, ReportTotals(1)]})), + (Report(files={"py.py": [0, ReportTotals(1)]}).filter(paths=["py.py"])), + ], +) +def test_files(r): + assert r.files == ["py.py"] + + +@pytest.mark.integration +def test_resolve_paths(): + r = Report( + files={"py.py": [0, ReportTotals(1)]}, + chunks="null\n[1]\n[1]\n[1]\n<<<<< end_of_chunk >>>>>\nnull\n[1]\n[1]\n[1]", + ) + assert r.files == ["py.py"] + r.resolve_paths([("py.py", "file.py")]) + assert r.files == ["file.py"] + + +@pytest.mark.integration +def test_resolve_paths_duplicate_paths(): + r = Report( + files={"py.py": [0, ReportTotals(1)]}, + chunks="null\n[1]\n[1]\n[1]\n<<<<< end_of_chunk >>>>>\nnull\n[1]\n[1]\n[1]", + ) + assert r.files == ["py.py"] + r.resolve_paths([("py.py", "py.py"), ("py.py", "py.py")]) + assert r.files == ["py.py"] + + +@pytest.mark.integration +@pytest.mark.parametrize( + "r, _file, joined, boolean, lines, hits", + [ + ( + Report(), + ReportFile( + "a", totals=ReportTotals(1, 50, 10), lines=[ReportLine.create(1)] + ), + False, + True, + 50, + 10, + ), + (Report(), None, True, False, 0, 0), + (Report(), ReportFile("name.py"), True, False, 0, 0), + ], +) +def test_append(r, _file, joined, boolean, lines, hits): + assert r.append(_file, joined) is boolean + assert r.totals.lines == lines + assert r.totals.hits == hits + + +def test_append_already_exists(): + report = Report() + first_file = ReportFile("path.py") + second_file = ReportFile("path.py") + first_file.append(1, ReportLine.create(1, sessions=[LineSession(1, 1)])) + first_file.append(2, ReportLine.create(1, sessions=[LineSession(1, 1)])) + first_file.append(3, ReportLine.create(0, sessions=[LineSession(1, 0)])) + first_file.append(4, ReportLine.create(0, sessions=[LineSession(1, 0)])) + first_file.append(5, ReportLine.create("1/2", sessions=[LineSession(1, "1/2")])) + first_file.append(6, ReportLine.create("1/2", sessions=[LineSession(1, "1/2")])) + second_file.append(2, ReportLine.create(0, sessions=[LineSession(1, 0)])) + second_file.append(3, ReportLine.create("1/2", sessions=[LineSession(1, "1/2")])) + second_file.append(4, ReportLine.create(1, sessions=[LineSession(1, 1)])) + second_file.append(5, ReportLine.create(0, sessions=[LineSession(1, 0)])) + second_file.append(6, ReportLine.create("3/4", sessions=[LineSession(1, "3/4")])) + second_file.append(7, ReportLine.create(1, sessions=[LineSession(1, 1)])) + report.append(first_file) + assert report.totals == ReportTotals( + files=1, + lines=6, + hits=2, + misses=2, + partials=2, + coverage="33.33333", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + assert report.get("path.py").totals == ReportTotals( + files=0, + lines=6, + hits=2, + misses=2, + partials=2, + coverage="33.33333", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + report.append(second_file) + assert report.totals == ReportTotals( + files=1, + lines=7, + hits=4, + misses=0, + partials=3, + coverage="57.14286", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + assert report.get("path.py").totals == ReportTotals( + files=0, + lines=7, + hits=4, + misses=0, + partials=3, + coverage="57.14286", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + + +@pytest.mark.integration +def test_append_error(): + r = Report() + with pytest.raises(Exception) as e_info: + r.append("str") + assert str(e_info.value) == "expecting ReportFile got " + + +@pytest.mark.integration +@pytest.mark.parametrize( + "r, file_repr, lines", + [ + ( + Report(files={"file.py": [0, ReportTotals(1)]}), + "", + [], + ), + ( + Report(files={"file.py": [0, ReportTotals(1)]}).filter(paths=["py.py"]), + "None", + None, + ), + ( + Report( + files={"file.py": [0, ReportTotals(1)]}, + chunks="null\n[1]\n[1]\n[1]\n<<<<< end_of_chunk >>>>>\nnull\n[1]\n[1]\n[1]", + ), + "", + [ + (1, ReportLine.create(1)), + (2, ReportLine.create(1)), + (3, ReportLine.create(1)), + ], + ), + ( + Report( + files={"file.py": [0, ReportTotals(1)]}, + chunks=[ReportFile(name="file.py")], + ), + "", + [], + ), + ( + Report( + files={"file.py": [1, ReportTotals(1)]}, + chunks=[ReportFile(name="other-file.py")], + ), + "", + [], + ), + ], +) +def test_get(r, file_repr, lines): + assert repr(r.get("file.py")) == file_repr + if lines: + assert list(r.get("file.py").lines) == lines + + +@pytest.mark.integration +def test_rename(): + r = Report( + files={"file.py": [0, ReportTotals(1)]}, + chunks="null\n[1]\n[1]\n[1]\n<<<<< end_of_chunk >>>>>\nnull\n[1]\n[1]\n[1]", + ) + assert r.get("name.py") is None + assert repr(r.get("file.py")) == "" + assert r.rename("file.py", "name.py") is True + assert r.get("file.py") is None + assert repr(r.get("name.py")) == "" + + +@pytest.mark.integration +def test_get_item(): + r = Report(files={"file.py": [0, ReportTotals(1)]}) + assert repr(r["file.py"]) == "" + + +@pytest.mark.integration +def test_get_item_exception(): + r = Report(files={"file.py": [0, ReportTotals(1)]}) + with pytest.raises(Exception) as e_info: + r["name.py"] + assert str(e_info.value) == "File at path name.py not found in report" + + +@pytest.mark.integration +def test_del_item(): + r = Report( + files={"file.py": [0, ReportTotals(1)]}, + chunks="null\n[1]\n[1]\n[1]\n<<<<< end_of_chunk >>>>>\nnull\n[1]\n[1]\n[1]", + ) + assert repr(r.get("file.py")) == "" + del r["file.py"] + assert r.get("file.py") is None + + +@pytest.mark.integration +@pytest.mark.parametrize( + "r, manifest", + [ + ( + Report( + files={ + "file1.py": [0, ReportTotals(1)], + "file2.py": [1, ReportTotals(1)], + } + ), + ["file1.py", "file2.py"], + ), + ( + Report( + files={ + "file1.py": [0, ReportTotals(1)], + "file2.py": [1, ReportTotals(1)], + } + ).filter(paths=["file2.py"]), + ["file2.py"], + ), + ], +) +def test_manifest(r, manifest): + assert r.files == manifest + + +@pytest.mark.integration +def test_flags(): + sessions = { + 1: {"f": ["a", 1, "test"]}, + 2: { + "f": ["c"], + "st": "carriedforward", + "se": {"carriedforward_from": "commit_SHA"}, + }, + } + r = Report(files={"py.py": [0, ReportTotals(1)]}, sessions=sessions) + + assert list(r.flags.keys()) == ["a", 1, "test", "c"] + for name, flag in r.flags.items(): + assert flag.carriedforward is (True if name == "c" else False) + assert flag.carriedforward_from is ("commit_SHA" if name == "c" else None) + + +@pytest.mark.integration +def test_iter(): + r = Report( + files={"file1.py": [0, ReportTotals(1)], "file2.py": [1, ReportTotals(1)]} + ) + files = [_file for _file in r] + assert ( + repr(files) + == "[, ]" + ) + + +@pytest.mark.integration +def test_contains(): + r = Report(files={"file1.py": [0, ReportTotals(1)]}) + assert ("file1.py" in r) is True + assert ("file2.py" in r) is False + + +@pytest.mark.integration +@pytest.mark.parametrize( + "r, new_report, manifest", + [ + (Report(files={"file.py": [0, ReportTotals(1)]}), None, ["file.py"]), + (Report(files={"file.py": [0, ReportTotals(1)]}), Report(), ["file.py"]), + ( + Report( + files={"file.py": [0, ReportTotals(1)]}, + chunks="null\n[1]\n[1]\n[1]\n<<<<< end_of_chunk >>>>>", + ), + Report( + files={"other-file.py": [1, ReportTotals(2)]}, + chunks="null\n[1]\n[1]\n[1]\n<<<<< end_of_chunk >>>>>\nnull\n[1]\n[1]\n[1]", + ), + ["file.py", "other-file.py"], + ), + ], +) +def test_merge(r, new_report, manifest): + assert r.files == ["file.py"] + r.merge(new_report) + assert r.files == manifest + + +@pytest.mark.integration +@pytest.mark.parametrize( + "r, boolean", + [ + (Report(), True), + ( + Report(files={"file.py": [0, ReportTotals(1)]}).filter( + paths=["this-file.py"] + ), + True, + ), + (Report(files={"file.py": [0, ReportTotals(1)]}), False), + ], +) +def test_is_empty(r, boolean): + assert r.is_empty() is boolean + + +@pytest.mark.integration +@pytest.mark.parametrize( + "r, boolean", + [(Report(), False), (Report(files={"file.py": [0, ReportTotals(1)]}), True)], +) +def test_non_zero(r, boolean): + assert bool(r) is boolean + + +def test_serialize(mocker): + report = Report( + files={"file.py": [0, ReportTotals()]}, + chunks="null\n[1]\n[1]\n[1]\n<<<<< end_of_chunk >>>>>\nnull\n[1]\n[1]\n[1]", + ) + report_json1, chunks1, totals1 = report.serialize() + + assert ( + report_json1 + == b'{"files":{"file.py":[0,[0,0,0,0,0,0,0,0,0,0,0,0,0],null,null]},"sessions":{},"totals":[1,0,0,0,0,null,0,0,0,0,0,0,null]}' + ) + assert chunks1 == b"""{}\n<<<<< end_of_header >>>>>\nnull\n[1]\n[1]\n[1]""" + assert totals1 == ReportTotals(files=1, coverage=None, diff=None) + + report = Report( + files={"file.py": [0, ReportTotals()]}, + chunks="null\n[1]\n[1]\n[1]\n<<<<< end_of_chunk >>>>>\nnull\n[1]\n[1]\n[1]", + ) + mocker.patch.object(report, "_process_totals") + + report_json2, chunks2, totals2 = report.serialize(with_totals=False) + assert report_json2 == b'{"files":{"file.py":[0,null]},"sessions":{},"totals":null}' + assert chunks2 == chunks1 + assert totals2 is None + + +@pytest.mark.integration +@pytest.mark.parametrize( + "diff, future, future_diff, res", + [ + ({}, None, None, False), # empty + (None, None, None, False), # empty + ({"files": {}}, None, None, False), # empty + ({"files": {"b": {"type": "new"}}}, None, None, False), # new file not tracked + ( + {"files": {"b": {"type": "new"}}}, + {"files": {"b": {"l": {"1": {"c": 1}}}}}, + None, + True, + ), # new file is tracked + ( + {"files": {"b": {"type": "modified"}}}, + None, + None, + False, + ), # file not tracked in base or head + ( + {"files": {"a": {"type": "deleted"}}}, + None, + None, + True, + ), # tracked file deleted + ( + {"files": {"b": {"type": "deleted"}}}, + None, + None, + False, + ), # not-tracked file deleted + ( + {"files": {"z": {"type": "modified"}}}, + None, + None, + True, + ), # modified file missing in base + ( + {"files": {"a": {"type": "modified"}}}, + None, + None, + True, + ), # modified file missing in head + ( + { + "files": { + "a": { + "type": "modified", + "segments": [{"header": [0, 1, 1, 2], "lines": ["- a", "+ a"]}], + } + } + }, + {"files": {"a": {"l": {"1": {"c": 1}}}}}, + None, + True, + ), # tracked line deleted + ( + { + "files": { + "a": { + "type": "modified", + "segments": [{"header": [0, 1, 1, 2], "lines": ["- a", "+ a"]}], + } + } + }, + {"files": {"a": {"l": {"1": {"c": 1}}}}}, + {"files": {"a": {"type": "modified"}}}, + True, + ), # tracked line deleted + ( + { + "files": { + "a": { + "type": "modified", + "segments": [ + {"header": [10, 1, 10, 2], "lines": ["- a", "+ a"]} + ], + } + } + }, + {"files": {"a": {"l": {"1": {"c": 1}}}}}, + None, + False, + ), # lines not tracked` + ], +) +def test_does_diff_adjust_tracked_lines(diff, future, future_diff, res): + report = Report(**v2_to_v3({"files": {"a": {"l": {"1": {"c": 1}, "2": {"c": 1}}}}})) + if future: + future = Report(**v2_to_v3(future)) + else: + future = Report(**v2_to_v3({"files": {"z": {}}})) + + assert report.does_diff_adjust_tracked_lines(diff, future, future_diff) == res + + +@pytest.mark.integration +def test_apply_diff(): + report = Report( + files={"a": [0, None], "d": [1, None]}, + chunks=[ + "\n[1, null, null, null]\n[0, null, null, null]", + "\n[1, null, null, null]\n[0, null, null, null]", + ], + ) + diff = { + "files": { + "a": { + "type": "new", + "segments": [{"header": list("1313"), "lines": list("---+++")}], + }, + "b": {"type": "deleted"}, + "c": {"type": "modified"}, + "d": { + "type": "modified", + "segments": [ + {"header": ["10", "3", "10", "3"], "lines": list("---+++")} + ], + }, + } + } + assert report.apply_diff(None) is None + assert report.apply_diff({}) is None + res = report.apply_diff(diff) + assert res == diff["totals"] + assert diff["totals"].coverage == "50.00000" + + +@pytest.mark.integration +def test_apply_diff_no_append(): + report = Report(**v2_to_v3({"files": {"a": {"l": {"1": {"c": 1}, "2": {"c": 0}}}}})) + diff = { + "files": { + "a": { + "type": "new", + "segments": [{"header": list("1313"), "lines": list("---+++")}], + }, + "b": {"type": "deleted"}, + "c": {"type": "modified"}, + } + } + res = report.apply_diff(diff, _save=False) + assert "totals" not in diff + assert "totals" not in diff["files"]["a"] + assert "totals" not in diff["files"]["c"] + assert res.coverage == "50.00000" + + +@pytest.mark.integration +def test_add_session(): + s = Session(5) + r = Report(files={"file.py": [0, ReportTotals(0)]}, totals=ReportTotals(0)) + assert r.totals.sessions == 0 + assert r.sessions == {} + assert r.add_session(s) == (0, s) + assert r.totals.sessions == 1 + assert r.sessions == {0: s} + + +@pytest.mark.integration +@pytest.mark.parametrize( + "r, params, flare", + [ + ( + Report( + files={"py.py": [0, ReportTotals(1)]}, + chunks="null\n[1]\n[1]\n[1]\n<<<<< end_of_chunk >>>>>\nnull\n[1]\n[1]\n[1]", + ), + { + "color": lambda cov: "purple" + if cov is None + else "#e1e1e1" + if cov == 0 + else "green" + if cov > 0 + else "red" + }, + [ + { + "name": "", + "coverage": 100, + "color": "green", + "_class": None, + "lines": 0, + "children": [ + { + "color": "#e1e1e1", + "_class": None, + "lines": 0, + "name": "py.py", + "coverage": 0, + } + ], + } + ], + ), + ( + Report(files={"py.py": [0, ReportTotals(1)]}), + {"changes": {}}, + [ + { + "name": "", + "coverage": 100, + "color": "green", + "_class": None, + "lines": 0, + "children": [ + { + "color": "#e1e1e1", + "_class": None, + "lines": 0, + "name": "py.py", + "coverage": 0, + } + ], + } + ], + ), + ], +) +def test_flare(r, params, flare): + assert r.flare(**params) == flare + + +def test_flare_with_changes(): + report = Report(files={"py.py": [0, ReportTotals(1), [], ReportTotals(lines=5)]}) + flare = [ + { + "name": "", + "coverage": 100, + "color": "green", + "_class": None, + "lines": 0, + "children": [ + { + "color": "green", + "_class": None, + "lines": 0, + "name": "py.py", + "coverage": 1, + } + ], + } + ] + modified_change = Change( + path="modified.py", + in_diff=True, + totals=ReportTotals( + files=0, + lines=0, + hits=-2, + misses=1, + partials=0, + coverage=-23.333330000000004, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ) + renamed_with_changes_change = Change( + path="renamed_with_changes.py", + in_diff=True, + old_path="old_renamed_with_changes.py", + totals=ReportTotals( + files=0, + lines=0, + hits=-1, + misses=1, + partials=0, + coverage=-20.0, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ) + unrelated_change = Change( + path="unrelated.py", + in_diff=False, + totals=ReportTotals( + files=0, + lines=5, + hits=-3, + misses=2, + partials=0, + coverage=-43.333330000000004, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ) + added_change = Change( + path="added.py", new=True, in_diff=None, old_path=None, totals=None + ) + deleted_change = Change(path="deleted.py", deleted=True) + changes = [ + modified_change, + renamed_with_changes_change, + unrelated_change, + added_change, + deleted_change, + ] + assert report.flare(changes=changes) == flare + + +@pytest.mark.integration +def test_filter_exception(): + with pytest.raises(Exception) as e_info: + Report().filter(paths="str") + assert str(e_info.value) == "expecting list for argument paths got " diff --git a/libs/shared/tests/integration/test_report_file.py b/libs/shared/tests/integration/test_report_file.py new file mode 100644 index 0000000000..04cfc40e56 --- /dev/null +++ b/libs/shared/tests/integration/test_report_file.py @@ -0,0 +1,346 @@ +import pytest + +from shared.reports.resources import ReportFile +from shared.reports.types import ReportLine, ReportTotals + + +@pytest.mark.integration +def test_report_file_constructor(): + r1 = ReportFile("folder/file.py", [0, 1, 1, 1]) + assert r1.name == "folder/file.py" + r2 = ReportFile(name="file.py", lines="\n[1,2]\n[1,1]") + assert list(r2.lines) == [ + (1, ReportLine.create(1, 2)), + (2, ReportLine.create(1, 1)), + ] + assert r2.name == "file.py" + + +@pytest.mark.integration +def test_repr(): + r = ReportFile( + "folder/file.py", + [0, 1, 1, 1], + lines=[ReportLine.create(1), ReportLine.create(2), None], + ) + assert repr(r) == "" + + +def test_get_item(): + lines = [ + ReportLine.create(1), + ReportLine.create(2), + ReportLine.create(3), + ReportLine.create(4), + ] + r = ReportFile( + "filename", ReportTotals(0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0), lines=lines + ) + assert r[1] == ReportLine.create(1) + assert r["totals"] == ReportTotals(0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0) + + +@pytest.mark.integration +@pytest.mark.parametrize( + "get_val, error_message", + [ + ([], "expecting type int got "), + (-1, "Line number must be greater then 0. Got -1"), + (1, "Line #1 not found in report"), + ], +) +def test_get_item_exception(get_val, error_message): + r = ReportFile("folder/file.py", [0, 1, 1, 1]) + with pytest.raises(Exception) as e_info: + r[get_val] + assert str(e_info.value) == error_message + + +@pytest.mark.integration +@pytest.mark.parametrize( + "r, get_val", + [ + ( + ReportFile( + name="folder/file.py", + lines=[ReportLine.create(1)], + ignore={"eof": "N", "lines": [1, 10]}, + ), + ReportLine.create(1), + ), + ( + ReportFile(name="folder/file.py", lines=[ReportLine.create(1)]), + ReportLine.create(0), + ), + ], +) +def test_set_item(r, get_val): + assert r[1] == ReportLine.create(1) + r[1] = ReportLine.create(0) + assert r[1] == get_val + + +def test_set_item_with_ignore_lines(): + ignore_lines = {"eof": 41, "lines": {40, 33, 37, 38}} + filename = "folder/file.java" + r = ReportFile(filename, ignore=ignore_lines) + r[1] = ReportLine.create(0) + r[2] = ReportLine.create(1) + r[33] = ReportLine.create(0) + r[37] = ReportLine.create("1/2") + r[38] = ReportLine.create(1) + r[39] = ReportLine.create("2/3") + r[40] = ReportLine.create(0) + r[41] = ReportLine.create(1) + r[42] = ReportLine.create("1/8") + assert list(r.lines) == [ + (1, ReportLine.create(0)), + (2, ReportLine.create(1)), + (39, ReportLine.create("2/3")), + (41, ReportLine.create(1)), + ] + + +@pytest.mark.integration +@pytest.mark.parametrize( + "index, set_val, error_message", + [ + ("str", 1, "expecting type int got "), + (1, "str", "expecting type ReportLine got "), + (-1, ReportLine.create(), "Line number must be greater then 0. Got -1"), + ], +) +def test_set_item_exception(index, set_val, error_message): + r = ReportFile("folder/file.py") + with pytest.raises(Exception) as e_info: + r[index] = set_val + assert str(e_info.value) == error_message + + +@pytest.mark.integration +def test_len(): + r = ReportFile( + name="folder/file.py", lines=[ReportLine.create(1), ReportLine.create(), None] + ) + assert len(r) == 2 + + +@pytest.mark.integration +def test_eof(): + r = ReportFile( + name="folder/file.py", lines=[ReportLine.create(1), ReportLine.create(), None] + ) + assert r.eof == 4 + + +@pytest.mark.integration +def test_get_slice(): + r = ReportFile( + name="folder/file.py", + lines=[ + ReportLine.create(1), + ReportLine.create(2), + ReportLine.create(3), + ReportLine.create(4), + ], + ) + assert list(r[2:4]) == [ + (2, ReportLine.create(2)), + (3, ReportLine.create(3)), + ] + + +@pytest.mark.integration +def test_non_zero(): + assert bool(ReportFile(name="name.py")) is False + assert bool(ReportFile(name="name.py", lines=[ReportLine.create(1)])) is True + + +@pytest.mark.integration +def test_contains(): + r = ReportFile("file.py", lines=[ReportLine.create(1), ReportLine.create(2)]) + assert (2 in r) is True + assert (7 in r) is False + + +@pytest.mark.integration +def test_contains_exception(): + r = ReportFile("folder/file.py") + with pytest.raises(Exception) as e_info: + "str" in r + assert str(e_info.value) == "expecting type int got " + + +@pytest.mark.integration +def test_report_file_get(): + r = ReportFile( + name="folder/file.py", lines=[ReportLine.create(), ReportLine.create()] + ) + assert r.get(1) == ReportLine.create() + + +@pytest.mark.integration +@pytest.mark.parametrize( + "get_val, error_message", + [ + ("str", "expecting type int got "), + (-1, "Line number must be greater then 0. Got -1"), + ], +) +def test_report_file_get_exception(get_val, error_message): + r = ReportFile("folder/file.py") + with pytest.raises(Exception) as e_info: + r.get(get_val) + assert str(e_info.value) == error_message + + +# TODO branch where this calls merge_line +@pytest.mark.integration +@pytest.mark.parametrize( + "r, boolean, lines", + [ + ( + ReportFile(name="folder/file.py", ignore={"eof": "N", "lines": [1, 10]}), + False, + [], + ), + (ReportFile(name="file.py"), True, [(1, ReportLine.create(1))]), + ], +) +def test_append(r, boolean, lines): + assert r.append(1, ReportLine.create(1)) is boolean + assert list(r.lines) == lines + + +@pytest.mark.integration +@pytest.mark.parametrize( + "key, val, error_message", + [ + ("str", ReportLine.create(), "expecting type int got "), + (1, "str", "expecting type ReportLine got "), + (-1, ReportLine.create(), "Line number must be greater then 0. Got -1"), + ], +) +def test_report_file_append_exception(key, val, error_message): + r = ReportFile("folder/file.py") + with pytest.raises(Exception) as e_info: + r.append(key, val) + assert str(e_info.value) == error_message + + +@pytest.mark.integration +@pytest.mark.parametrize( + "r, list_before, merge_val, merge_return, list_after", + [ + (ReportFile(name="file.py"), [], None, None, []), + ( + ReportFile(name="file.rb"), + [], + ReportFile(name="file.rb", lines=[ReportLine.create(2)]), + True, + [(1, ReportLine.create(2))], + ), + ( + ReportFile(name="file.rb", totals=[1, 2, 3]), + [], + ReportFile(name="file.rb"), + False, + [], + ), + ], +) +def test_merge(r, list_before, merge_val, merge_return, list_after): + assert list(r.lines) == list_before + assert r.merge(merge_val) == merge_return + assert list(r.lines) == list_after + + +@pytest.mark.integration +def test_merge_exception(): + r = ReportFile(name="name.py") + with pytest.raises(Exception) as e_info: + r.merge("str") + assert str(e_info.value) == "expecting type ReportFile got " + + +@pytest.mark.integration +def test_totals(): + r = ReportFile(name="file.py", totals=[0]) + assert r.totals == ReportTotals(0) + + +@pytest.mark.integration +def test_lines(): + r = ReportFile(name="asd", lines=[[1], ReportLine.create(2), None]) + assert list(r.lines) == [(1, ReportLine.create(1)), (2, ReportLine.create(2))] + + +@pytest.mark.integration +def test_iter(): + r = ReportFile( + name="folder/file.py", + lines=[ReportLine.create(1), ReportLine.create(0), None], + ) + lines = [ln for ln in r] + assert lines == [ReportLine.create(1), ReportLine.create(0), None] + + +@pytest.mark.integration +@pytest.mark.parametrize( + "r, diff, new_file, boolean", + [ + (ReportFile("file.py"), {"segments": []}, ReportFile("new.py"), False), + ( + ReportFile("file.py"), + { + "segments": [ + {"header": [1, 1, 1, 1], "lines": ["- afefe", "+ fefe", "="]} + ] + }, + ReportFile("new.py"), + False, + ), + ( + ReportFile("file.py", lines=[ReportLine.create(1), ReportLine.create(2)]), + { + "segments": [ + {"header": [1, 1, 1, 1], "lines": ["- afefe", "+ fefe", "="]} + ] + }, + ReportFile("new.py"), + True, + ), + ( + ReportFile("file.py"), + { + "segments": [ + {"header": [1, 1, 1, 1], "lines": ["- afefe", "+ fefe", "="]} + ] + }, + ReportFile("new.py", lines=[ReportLine.create(1), ReportLine.create(2)]), + True, + ), + ], +) +def test_does_diff_adjust_tracked_lines(r, diff, new_file, boolean): + assert r.does_diff_adjust_tracked_lines(diff, new_file) is boolean + + +@pytest.mark.integration +def test_shift_lines_by_diff(): + r = ReportFile( + name="folder/file.py", lines=[ReportLine.create(), ReportLine.create()] + ) + assert len(list(r.lines)) == 2 + r.shift_lines_by_diff( + { + "segments": [ + { + # [-, -, POS_to_start, new_lines_added] + "header": [1, 1, 1, 1], + "lines": ["- afefe", "+ fefe", "="], + } + ] + } + ) + assert len(list(r.lines)) == 1 diff --git a/libs/shared/tests/samples/asset_link_curr_a.json b/libs/shared/tests/samples/asset_link_curr_a.json new file mode 100644 index 0000000000..438343add7 --- /dev/null +++ b/libs/shared/tests/samples/asset_link_curr_a.json @@ -0,0 +1,113 @@ +{ + "version": "3", + "builtAt": 1716480551148, + "duration": 294, + "bundleName": "BundleA", + "outputPath": "/vite/dist", + "bundler": { "name": "rollup", "version": "4.16.2" }, + "plugin": { "name": "@codecov/vite-plugin", "version": "0.0.1-beta.8" }, + "assets": [ + { + "name": "asset-same-name-diff-modules.js", + "size": 4126, + "normalized": "asset-*.js" + }, + { + "name": "asset-diff-name-same-modules-TWO.js", + "size": 1421, + "normalized": "asset-*.js" + }, + { + "name": "asset-diff-name-diff-modules-TWO.js", + "size": 161, + "normalized": "asset-*.js" + }, + { + "name": "asset-css-A-TWO.css", + "size": 4, + "normalized": "assets/index-*.css" + }, + { + "name": "asset-css-B-TWO.css", + "size": 5, + "normalized": "assets/index-*.css" + }, + { + "name": "asset-font-A-TWO.woff", + "size": 40, + "normalized": "assets/index-*.woff" + }, + { + "name": "asset-font-B-TWO.ttf", + "size": 50, + "normalized": "assets/index-*.ttf" + }, + { + "name": "asset-image-A-TWO.jpg", + "size": 400, + "normalized": "assets/index-*.jpg" + }, + { + "name": "asset-image-B-TWO.svg", + "size": 500, + "normalized": "assets/index-*.svg" + } + ], + "chunks": [ + { + "id": "asset", + "uniqueId": "0-asset", + "entry": false, + "initial": true, + "files": ["asset-same-name-diff-modules.js"], + "names": ["index"] + }, + { + "id": "asset", + "uniqueId": "1-asset", + "entry": false, + "initial": true, + "files": ["asset-diff-name-same-modules-TWO.js"], + "names": ["asset"] + }, + { + "id": "asset", + "uniqueId": "2-asset", + "entry": true, + "initial": false, + "files": ["asset-diff-name-diff-modules-TWO.js"], + "names": ["asset"] + } + ], + "modules": [ + { + "name": "./src/file-aTwo.js", + "size": 189, + "chunkUniqueIds": ["0-asset"] + }, + { + "name": "./src/file-bTwo.js", + "size": 353, + "chunkUniqueIds": ["0-asset"] + }, + { + "name": "./src/file-cTwo.js", + "size": 497, + "chunkUniqueIds": ["0-asset"] + }, + { "name": "./src/file-d.js", "size": 154, "chunkUniqueIds": ["1-asset"] }, + { "name": "./src/file-e.js", "size": 140, "chunkUniqueIds": ["1-asset"] }, + { "name": "./src/file-f.js", "size": 315, "chunkUniqueIds": ["1-asset"] }, + { + "name": "./src/file-sTWO.js", + "size": 406, + "chunkUniqueIds": ["2-asset"] + }, + { + "name": "./src/file-tTWO.js", + "size": 262, + "chunkUniqueIds": ["2-asset"] + }, + { "name": "./src/file-uTWO.js", "size": 308, "chunkUniqueIds": ["2-asset"] } + ] +} diff --git a/libs/shared/tests/samples/asset_link_curr_b.json b/libs/shared/tests/samples/asset_link_curr_b.json new file mode 100644 index 0000000000..a1112ea471 --- /dev/null +++ b/libs/shared/tests/samples/asset_link_curr_b.json @@ -0,0 +1,113 @@ +{ + "version": "3", + "builtAt": 1716480551148, + "duration": 294, + "bundleName": "BundleB", + "outputPath": "/vite/dist", + "bundler": { "name": "rollup", "version": "4.16.2" }, + "plugin": { "name": "@codecov/vite-plugin", "version": "0.0.1-beta.8" }, + "assets": [ + { + "name": "asset-same-name-diff-modules.js", + "size": 4126, + "normalized": "asset-*.js" + }, + { + "name": "asset-diff-name-same-modules-TWO.js", + "size": 1421, + "normalized": "asset-*.js" + }, + { + "name": "asset-diff-name-diff-modules-TWO.js", + "size": 161, + "normalized": "asset-*.js" + }, + { + "name": "asset-css-A-TWO.css", + "size": 4, + "normalized": "assets/index-*.css" + }, + { + "name": "asset-css-B-TWO.css", + "size": 5, + "normalized": "assets/index-*.css" + }, + { + "name": "asset-font-A-TWO.woff", + "size": 40, + "normalized": "assets/index-*.woff" + }, + { + "name": "asset-font-B-TWO.ttf", + "size": 50, + "normalized": "assets/index-*.ttf" + }, + { + "name": "asset-image-A-TWO.jpg", + "size": 400, + "normalized": "assets/index-*.jpg" + }, + { + "name": "asset-image-B-TWO.svg", + "size": 500, + "normalized": "assets/index-*.svg" + } + ], + "chunks": [ + { + "id": "asset", + "uniqueId": "0-asset", + "entry": false, + "initial": true, + "files": ["asset-same-name-diff-modules.js"], + "names": ["index"] + }, + { + "id": "asset", + "uniqueId": "1-asset", + "entry": false, + "initial": true, + "files": ["asset-diff-name-same-modules-TWO.js"], + "names": ["asset"] + }, + { + "id": "asset", + "uniqueId": "2-asset", + "entry": true, + "initial": false, + "files": ["asset-diff-name-diff-modules-TWO.js"], + "names": ["asset"] + } + ], + "modules": [ + { + "name": "./src/file-aTwo.js", + "size": 189, + "chunkUniqueIds": ["0-asset"] + }, + { + "name": "./src/file-bTwo.js", + "size": 353, + "chunkUniqueIds": ["0-asset"] + }, + { + "name": "./src/file-cTwo.js", + "size": 497, + "chunkUniqueIds": ["0-asset"] + }, + { "name": "./src/file-d.js", "size": 154, "chunkUniqueIds": ["1-asset"] }, + { "name": "./src/file-e.js", "size": 140, "chunkUniqueIds": ["1-asset"] }, + { "name": "./src/file-f.js", "size": 315, "chunkUniqueIds": ["1-asset"] }, + { + "name": "./src/file-sTWO.js", + "size": 406, + "chunkUniqueIds": ["2-asset"] + }, + { + "name": "./src/file-tTWO.js", + "size": 262, + "chunkUniqueIds": ["2-asset"] + }, + { "name": "./src/file-uTWO.js", "size": 308, "chunkUniqueIds": ["2-asset"] } + ] +} diff --git a/libs/shared/tests/samples/asset_link_prev_a.json b/libs/shared/tests/samples/asset_link_prev_a.json new file mode 100644 index 0000000000..751860c2f5 --- /dev/null +++ b/libs/shared/tests/samples/asset_link_prev_a.json @@ -0,0 +1,113 @@ +{ + "version": "3", + "builtAt": 1716480551136, + "duration": 294, + "bundleName": "BundleA", + "outputPath": "/vite/dist", + "bundler": { "name": "rollup", "version": "4.16.2" }, + "plugin": { "name": "@codecov/vite-plugin", "version": "0.0.1-beta.8" }, + "assets": [ + { + "name": "asset-same-name-diff-modules.js", + "size": 4126, + "normalized": "asset-*.js" + }, + { + "name": "asset-diff-name-same-modules-ONE.js", + "size": 1421, + "normalized": "asset-*.js" + }, + { + "name": "asset-diff-name-diff-modules-ONE.js", + "size": 161, + "normalized": "asset-*.js" + }, + { + "name": "asset-css-A-ONE.css", + "size": 2, + "normalized": "assets/index-*.css" + }, + { + "name": "asset-css-B-ONE.css", + "size": 3, + "normalized": "assets/index-*.css" + }, + { + "name": "asset-font-A-ONE.woff", + "size": 20, + "normalized": "assets/index-*.woff" + }, + { + "name": "asset-font-B-ONE.ttf", + "size": 30, + "normalized": "assets/index-*.ttf" + }, + { + "name": "asset-image-A-ONE.jpg", + "size": 200, + "normalized": "assets/index-*.jpg" + }, + { + "name": "asset-image-B-ONE.svg", + "size": 300, + "normalized": "assets/index-*.svg" + } + ], + "chunks": [ + { + "id": "asset", + "uniqueId": "0-asset", + "entry": false, + "initial": true, + "files": ["asset-same-name-diff-modules.js"], + "names": ["index"] + }, + { + "id": "asset", + "uniqueId": "1-asset", + "entry": false, + "initial": true, + "files": ["asset-diff-name-same-modules-ONE.js"], + "names": ["asset"] + }, + { + "id": "asset", + "uniqueId": "2-asset", + "entry": true, + "initial": false, + "files": ["asset-diff-name-diff-modules-ONE.js"], + "names": ["asset"] + } + ], + "modules": [ + { + "name": "./src/file-aOne.js", + "size": 189, + "chunkUniqueIds": ["0-asset"] + }, + { + "name": "./src/file-bOne.js", + "size": 353, + "chunkUniqueIds": ["0-asset"] + }, + { + "name": "./src/file-cOne.js", + "size": 497, + "chunkUniqueIds": ["0-asset"] + }, + { "name": "./src/file-d.js", "size": 154, "chunkUniqueIds": ["1-asset"] }, + { "name": "./src/file-e.js", "size": 140, "chunkUniqueIds": ["1-asset"] }, + { "name": "./src/file-f.js", "size": 315, "chunkUniqueIds": ["1-asset"] }, + { + "name": "./src/file-gONE.js", + "size": 406, + "chunkUniqueIds": ["2-asset"] + }, + { + "name": "./src/file-hONE.js", + "size": 262, + "chunkUniqueIds": ["2-asset"] + }, + { "name": "./src/file-iONE.js", "size": 308, "chunkUniqueIds": ["2-asset"] } + ] +} diff --git a/libs/shared/tests/samples/asset_link_prev_b.json b/libs/shared/tests/samples/asset_link_prev_b.json new file mode 100644 index 0000000000..5b6c313929 --- /dev/null +++ b/libs/shared/tests/samples/asset_link_prev_b.json @@ -0,0 +1,113 @@ +{ + "version": "3", + "builtAt": 1716480551136, + "duration": 294, + "bundleName": "BundleB", + "outputPath": "/vite/dist", + "bundler": { "name": "rollup", "version": "4.16.2" }, + "plugin": { "name": "@codecov/vite-plugin", "version": "0.0.1-beta.8" }, + "assets": [ + { + "name": "asset-same-name-diff-modules.js", + "size": 4126, + "normalized": "asset-*.js" + }, + { + "name": "asset-diff-name-same-modules-ONE.js", + "size": 1421, + "normalized": "asset-*.js" + }, + { + "name": "asset-diff-name-diff-modules-ONE.js", + "size": 161, + "normalized": "asset-*.js" + }, + { + "name": "asset-css-A-ONE.css", + "size": 2, + "normalized": "assets/index-*.css" + }, + { + "name": "asset-css-B-ONE.css", + "size": 3, + "normalized": "assets/index-*.css" + }, + { + "name": "asset-font-A-ONE.woff", + "size": 20, + "normalized": "assets/index-*.woff" + }, + { + "name": "asset-font-B-ONE.ttf", + "size": 30, + "normalized": "assets/index-*.ttf" + }, + { + "name": "asset-image-A-ONE.jpg", + "size": 200, + "normalized": "assets/index-*.jpg" + }, + { + "name": "asset-image-B-ONE.svg", + "size": 300, + "normalized": "assets/index-*.svg" + } + ], + "chunks": [ + { + "id": "asset", + "uniqueId": "0-asset", + "entry": false, + "initial": true, + "files": ["asset-same-name-diff-modules.js"], + "names": ["index"] + }, + { + "id": "asset", + "uniqueId": "1-asset", + "entry": false, + "initial": true, + "files": ["asset-diff-name-same-modules-ONE.js"], + "names": ["asset"] + }, + { + "id": "asset", + "uniqueId": "2-asset", + "entry": true, + "initial": false, + "files": ["asset-diff-name-diff-modules-ONE.js"], + "names": ["asset"] + } + ], + "modules": [ + { + "name": "./src/file-aOne.js", + "size": 189, + "chunkUniqueIds": ["0-asset"] + }, + { + "name": "./src/file-bOne.js", + "size": 353, + "chunkUniqueIds": ["0-asset"] + }, + { + "name": "./src/file-cOne.js", + "size": 497, + "chunkUniqueIds": ["0-asset"] + }, + { "name": "./src/file-d.js", "size": 154, "chunkUniqueIds": ["1-asset"] }, + { "name": "./src/file-e.js", "size": 140, "chunkUniqueIds": ["1-asset"] }, + { "name": "./src/file-f.js", "size": 315, "chunkUniqueIds": ["1-asset"] }, + { + "name": "./src/file-gONE.js", + "size": 406, + "chunkUniqueIds": ["2-asset"] + }, + { + "name": "./src/file-hONE.js", + "size": 262, + "chunkUniqueIds": ["2-asset"] + }, + { "name": "./src/file-iONE.js", "size": 308, "chunkUniqueIds": ["2-asset"] } + ] +} diff --git a/libs/shared/tests/samples/sample_bundle_stats.json b/libs/shared/tests/samples/sample_bundle_stats.json new file mode 100644 index 0000000000..bb0ac8d2d0 --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats.json @@ -0,0 +1,206 @@ +{ + "version": "2", + "plugin": { + "name": "codecov-vite-bundle-analysis-plugin", + "version": "1.0.0" + }, + "builtAt": 1701451048604, + "duration": 331, + "bundler": { "name": "rollup", "version": "3.29.4" }, + "bundleName": "sample", + "assets": [ + { + "name": "assets/react-35ef61ed.svg", + "size": 4126, + "gzipSize": 4125, + "normalized": "assets/react-*.svg" + }, + { + "name": "assets/index-d526a0c5.css", + "size": 1421, + "gzipSize": 1420, + "normalized": "assets/index-*.css" + }, + { + "name": "assets/LazyComponent-fcbb0922.js", + "size": 294, + "gzipSize": 293, + "normalized": "assets/LazyComponent-*.js" + }, + { + "name": "assets/index-c8676264.js", + "size": 154, + "gzipSize": 153, + "normalized": "assets/index-*.js" + }, + { + "name": "assets/index-666d2e09.js", + "size": 144577, + "gzipSize": 144576, + "normalized": "assets/index-*.js" + } + ], + "chunks": [ + { + "id": "LazyComponent", + "uniqueId": "0-LazyComponent", + "entry": false, + "initial": true, + "files": ["assets/LazyComponent-fcbb0922.js"], + "names": ["LazyComponent"] + }, + { + "id": "index", + "uniqueId": "1-index", + "entry": false, + "initial": true, + "files": ["assets/index-c8676264.js"], + "names": ["index"] + }, + { + "id": "index", + "uniqueId": "2-index", + "entry": true, + "initial": true, + "files": ["assets/index-666d2e09.js"], + "names": ["index"] + } + ], + "modules": [ + { + "name": "./src/LazyComponent/LazyComponent.tsx", + "size": 497, + "chunkUniqueIds": ["0-LazyComponent"] + }, + { + "name": "./src/IndexedLazyComponent/IndexedLazyComponent.tsx", + "size": 189, + "chunkUniqueIds": ["1-index"] + }, + { + "name": "./src/IndexedLazyComponent/index.ts", + "size": 0, + "chunkUniqueIds": ["1-index"] + }, + { + "name": "./vite/modulepreload-polyfill", + "size": 1548, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./commonjsHelpers.js", + "size": 140, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js?commonjs-module", + "size": 31, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js?commonjs-exports", + "size": 40, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js?commonjs-module", + "size": 26, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js?commonjs-exports", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js", + "size": 7591, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js", + "size": 144, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js", + "size": 919, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js", + "size": 103, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js?commonjs-exports", + "size": 16, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js?commonjs-module", + "size": 29, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js?commonjs-exports", + "size": 33, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js?commonjs-module", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js?commonjs-exports", + "size": 34, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js", + "size": 4315, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js", + "size": 94, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js", + "size": 132340, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js", + "size": 755, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js", + "size": 102, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./vite/preload-helper", + "size": 2488, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./src/assets/react.svg", + "size": 45, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../../../../../vite.svg", + "size": 51, + "chunkUniqueIds": ["2-index"] + }, + { "name": "./src/App.css", "size": 17, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/App.tsx", "size": 1977, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/index.css", "size": 17, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/main.tsx", "size": 181, "chunkUniqueIds": ["2-index"] }, + { "name": "./index.html", "size": 0, "chunkUniqueIds": ["2-index"] } + ] +} \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_another_bundle.json b/libs/shared/tests/samples/sample_bundle_stats_another_bundle.json new file mode 100644 index 0000000000..94e20aa295 --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_another_bundle.json @@ -0,0 +1,206 @@ +{ + "version": "2", + "plugin": { + "name": "codecov-vite-bundle-analysis-plugin", + "version": "1.0.0" + }, + "builtAt": 1701451048604, + "duration": 331, + "bundler": { "name": "rollup", "version": "3.29.4" }, + "bundleName": "sample2", + "assets": [ + { + "name": "assets/react-35ef61ed.svg", + "size": 41260, + "gzipSize": 41250, + "normalized": "assets/react-*.svg" + }, + { + "name": "assets/index-d526a0c5.css", + "size": 14210, + "gzipSize": 14200, + "normalized": "assets/index-*.css" + }, + { + "name": "assets/LazyComponent-fcbb0922.js", + "size": 2940, + "gzipSize": 2930, + "normalized": "assets/LazyComponent-*.js" + }, + { + "name": "assets/index-c8676264.js", + "size": 1540, + "gzipSize": 1530, + "normalized": "assets/index-*.js" + }, + { + "name": "assets/index-666d2e09.js", + "size": 1445770, + "gzipSize": 1445760, + "normalized": "assets/index-*.js" + } + ], + "chunks": [ + { + "id": "LazyComponent", + "uniqueId": "0-LazyComponent", + "entry": false, + "initial": true, + "files": ["assets/LazyComponent-fcbb0922.js"], + "names": ["LazyComponent"] + }, + { + "id": "index", + "uniqueId": "1-index", + "entry": false, + "initial": true, + "files": ["assets/index-c8676264.js"], + "names": ["index"] + }, + { + "id": "index", + "uniqueId": "2-index", + "entry": true, + "initial": true, + "files": ["assets/index-666d2e09.js"], + "names": ["index"] + } + ], + "modules": [ + { + "name": "./src/LazyComponent/LazyComponent.tsx", + "size": 497, + "chunkUniqueIds": ["0-LazyComponent"] + }, + { + "name": "./src/IndexedLazyComponent/IndexedLazyComponent.tsx", + "size": 189, + "chunkUniqueIds": ["1-index"] + }, + { + "name": "./src/IndexedLazyComponent/index.ts", + "size": 0, + "chunkUniqueIds": ["1-index"] + }, + { + "name": "./vite/modulepreload-polyfill", + "size": 1548, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./commonjsHelpers.js", + "size": 140, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js?commonjs-module", + "size": 31, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js?commonjs-exports", + "size": 40, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js?commonjs-module", + "size": 26, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js?commonjs-exports", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js", + "size": 7591, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js", + "size": 144, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js", + "size": 919, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js", + "size": 103, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js?commonjs-exports", + "size": 16, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js?commonjs-module", + "size": 29, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js?commonjs-exports", + "size": 33, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js?commonjs-module", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js?commonjs-exports", + "size": 34, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js", + "size": 4315, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js", + "size": 94, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js", + "size": 132340, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js", + "size": 755, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js", + "size": 102, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./vite/preload-helper", + "size": 2488, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./src/assets/react.svg", + "size": 45, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../../../../../vite.svg", + "size": 51, + "chunkUniqueIds": ["2-index"] + }, + { "name": "./src/App.css", "size": 17, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/App.tsx", "size": 1977, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/index.css", "size": 17, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/main.tsx", "size": 181, "chunkUniqueIds": ["2-index"] }, + { "name": "./index.html", "size": 0, "chunkUniqueIds": ["2-index"] } + ] + } \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_asset_routes.json b/libs/shared/tests/samples/sample_bundle_stats_asset_routes.json new file mode 100644 index 0000000000..6fc2c97085 --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_asset_routes.json @@ -0,0 +1,661 @@ +{ + "version": "2", + "builtAt": 1721755530651, + "duration": 289, + "bundleName": "sample", + "outputPath": "/Users/nicholas/Projects/codecov-javascript-bundler-plugins/examples/sveltekit/.svelte-kit/output/client", + "bundler": { "name": "rollup", "version": "4.16.2" }, + "plugin": { "name": "@codecov/sveltekit-plugin", "version": "0.0.1-beta.11" }, + "assets": [ + { + "name": "_app/immutable/assets/svelte-welcome.0pIiHnVF.webp", + "size": 115470, + "gzipSize": null, + "normalized": "_app/immutable/assets/svelte-welcome.*.webp" + }, + { + "name": "_app/immutable/assets/svelte-welcome.VNiyy3gC.png", + "size": 360807, + "gzipSize": null, + "normalized": "_app/immutable/assets/svelte-welcome.*.png" + }, + { + "name": "_app/immutable/assets/fira-mono-greek-400-normal.C3zng6O6.woff2", + "size": 10520, + "gzipSize": null, + "normalized": "_app/immutable/assets/fira-mono-greek-400-normal.*.woff2" + }, + { + "name": "_app/immutable/assets/fira-mono-cyrillic-ext-400-normal.B04YIrm4.woff2", + "size": 15772, + "gzipSize": null, + "normalized": "_app/immutable/assets/fira-mono-cyrillic-ext-400-normal.*.woff2" + }, + { + "name": "_app/immutable/assets/fira-mono-cyrillic-400-normal.36-45Uyg.woff2", + "size": 9104, + "gzipSize": null, + "normalized": "_app/immutable/assets/fira-mono-cyrillic-400-normal.*.woff2" + }, + { + "name": "_app/immutable/assets/fira-mono-latin-ext-400-normal.D6XfiR-_.woff2", + "size": 11364, + "gzipSize": null, + "normalized": "_app/immutable/assets/fira-mono-latin-ext-400-normal.D6XfiR-_.woff2" + }, + { + "name": "_app/immutable/assets/fira-mono-latin-400-normal.DKjLVgQi.woff2", + "size": 16284, + "gzipSize": null, + "normalized": "_app/immutable/assets/fira-mono-latin-400-normal.*.woff2" + }, + { + "name": "_app/immutable/assets/fira-mono-greek-ext-400-normal.CsqI23CO.woff2", + "size": 7508, + "gzipSize": null, + "normalized": "_app/immutable/assets/fira-mono-greek-ext-400-normal.*.woff2" + }, + { + "name": "_app/immutable/assets/fira-mono-all-400-normal.B2mvLtSD.woff", + "size": 77364, + "gzipSize": null, + "normalized": "_app/immutable/assets/fira-mono-all-400-normal.*.woff" + }, + { + "name": "_app/immutable/assets/5.CU6psp88.css", + "size": 797, + "gzipSize": 347, + "normalized": "_app/immutable/assets/5.*.css" + }, + { + "name": "_app/immutable/assets/0.CT0x_Q5c.css", + "size": 5146, + "gzipSize": 1660, + "normalized": "_app/immutable/assets/0.CT0x_Q5c.css" + }, + { + "name": "_app/immutable/assets/4.DOkkq0IA.css", + "size": 3779, + "gzipSize": 1062, + "normalized": "_app/immutable/assets/4.*.css" + }, + { + "name": "_app/immutable/chunks/index.Ice1EKvx.js", + "size": 509, + "gzipSize": 335, + "normalized": "_app/immutable/chunks/index.*.js" + }, + { + "name": "_app/immutable/assets/2.Cs8KR-Bb.css", + "size": 1470, + "gzipSize": 533, + "normalized": "_app/immutable/assets/2.*.css" + }, + { + "name": "_app/immutable/nodes/3.BqQOub2U.js", + "size": 1572, + "gzipSize": 957, + "normalized": "_app/immutable/nodes/3.*.js" + }, + { + "name": "_app/immutable/nodes/1.stWWSe4n.js", + "size": 836, + "gzipSize": 518, + "normalized": "_app/immutable/nodes/1.*.js" + }, + { + "name": "_app/immutable/entry/start.B1Q1eB84.js", + "size": 68, + "gzipSize": 83, + "normalized": "_app/immutable/entry/start.*.js" + }, + { + "name": "_app/immutable/chunks/index.R8ovVqwX.js", + "size": 27, + "gzipSize": 47, + "normalized": "_app/immutable/chunks/index.*.js" + }, + { + "name": "_app/immutable/chunks/stores.BrqGIpx3.js", + "size": 233, + "gzipSize": 167, + "normalized": "_app/immutable/chunks/stores.*.js" + }, + { + "name": "_app/immutable/chunks/scheduler.Dk-snqIU.js", + "size": 2250, + "gzipSize": 1051, + "normalized": "_app/immutable/chunks/scheduler.*.js" + }, + { + "name": "_app/immutable/entry/app.Dd9ByE1Q.js", + "size": 6044, + "gzipSize": 2440, + "normalized": "_app/immutable/entry/app.*.js" + }, + { + "name": "_app/immutable/nodes/5.CwxmUzn6.js", + "size": 2350, + "gzipSize": 1112, + "normalized": "_app/immutable/nodes/5.*.js" + }, + { + "name": "_app/version.json", + "size": 27, + "gzipSize": 47, + "normalized": "_app/version.json" + }, + { + "name": "_app/immutable/chunks/index.DDRweiI9.js", + "size": 6087, + "gzipSize": 2573, + "normalized": "_app/immutable/chunks/index.*.js" + }, + { + "name": "_app/immutable/nodes/2.BMQFqo-e.js", + "size": 5477, + "gzipSize": 2595, + "normalized": "_app/immutable/nodes/2.*.js" + }, + { + "name": "_app/immutable/nodes/0.CL_S-12h.js", + "size": 8774, + "gzipSize": 3572, + "normalized": "_app/immutable/nodes/0.CL_S-12h.js" + }, + { + "name": "_app/immutable/nodes/4.CcjRtXvw.js", + "size": 17081, + "gzipSize": 7066, + "normalized": "_app/immutable/nodes/4.*.js" + }, + { + "name": "_app/immutable/chunks/entry.BaWB2kHj.js", + "size": 27879, + "gzipSize": 10936, + "normalized": "_app/immutable/chunks/entry.*.js" + } + ], + "chunks": [ + { + "id": "index", + "uniqueId": "0-index", + "entry": false, + "initial": false, + "files": ["_app/immutable/chunks/index.Ice1EKvx.js"], + "names": ["index"] + }, + { + "id": "nodes/3", + "uniqueId": "1-nodes/3", + "entry": true, + "initial": true, + "files": ["_app/immutable/nodes/3.BqQOub2U.js"], + "names": ["nodes/3"] + }, + { + "id": "nodes/1", + "uniqueId": "2-nodes/1", + "entry": true, + "initial": true, + "files": ["_app/immutable/nodes/1.stWWSe4n.js"], + "names": ["nodes/1"] + }, + { + "id": "entry/start", + "uniqueId": "3-entry/start", + "entry": true, + "initial": false, + "files": ["_app/immutable/entry/start.B1Q1eB84.js"], + "names": ["entry/start"] + }, + { + "id": "index", + "uniqueId": "4-index", + "entry": false, + "initial": false, + "files": ["_app/immutable/chunks/index.R8ovVqwX.js"], + "names": ["index"] + }, + { + "id": "stores", + "uniqueId": "5-stores", + "entry": false, + "initial": false, + "files": ["_app/immutable/chunks/stores.BrqGIpx3.js"], + "names": ["stores"] + }, + { + "id": "scheduler", + "uniqueId": "6-scheduler", + "entry": false, + "initial": false, + "files": ["_app/immutable/chunks/scheduler.Dk-snqIU.js"], + "names": ["scheduler"] + }, + { + "id": "entry/app", + "uniqueId": "7-entry/app", + "entry": true, + "initial": false, + "files": ["_app/immutable/entry/app.Dd9ByE1Q.js"], + "names": ["entry/app"] + }, + { + "id": "nodes/5", + "uniqueId": "8-nodes/5", + "entry": true, + "initial": true, + "files": ["_app/immutable/nodes/5.CwxmUzn6.js"], + "names": ["nodes/5"] + }, + { + "id": "index", + "uniqueId": "9-index", + "entry": false, + "initial": false, + "files": ["_app/immutable/chunks/index.DDRweiI9.js"], + "names": ["index"] + }, + { + "id": "nodes/2", + "uniqueId": "10-nodes/2", + "entry": true, + "initial": true, + "files": ["_app/immutable/nodes/2.BMQFqo-e.js"], + "names": ["nodes/2"] + }, + { + "id": "nodes/0", + "uniqueId": "11-nodes/0", + "entry": true, + "initial": true, + "files": ["_app/immutable/nodes/0.CL_S-12h.js"], + "names": ["nodes/0"] + }, + { + "id": "nodes/4", + "uniqueId": "12-nodes/4", + "entry": true, + "initial": true, + "files": ["_app/immutable/nodes/4.CcjRtXvw.js"], + "names": ["nodes/4"] + }, + { + "id": "entry", + "uniqueId": "13-entry", + "entry": false, + "initial": false, + "files": ["_app/immutable/chunks/entry.BaWB2kHj.js"], + "names": ["entry"] + } + ], + "modules": [ + { + "name": "../../node_modules/.pnpm/svelte@4.2.15/node_modules/svelte/src/runtime/store/index.js", + "size": 2217, + "chunkUniqueIds": ["0-index"] + }, + { + "name": "./src/routes/about/+page.ts", + "size": 40, + "chunkUniqueIds": ["1-nodes/3"] + }, + { + "name": "./src/routes/about/+page.svelte", + "size": 1907, + "chunkUniqueIds": ["1-nodes/3"] + }, + { + "name": "./.svelte-kit/generated/client-optimized/nodes/3.js", + "size": 0, + "chunkUniqueIds": ["1-nodes/3"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/runtime/components/error.svelte", + "size": 1820, + "chunkUniqueIds": ["2-nodes/1"] + }, + { + "name": "./.svelte-kit/generated/client-optimized/nodes/1.js", + "size": 0, + "chunkUniqueIds": ["2-nodes/1"] + }, + { + "name": "../../node_modules/.pnpm/esm-env@1.0.0/node_modules/esm-env/prod-browser.js", + "size": 18, + "chunkUniqueIds": ["4-index"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/runtime/app/environment/index.js", + "size": 16, + "chunkUniqueIds": ["4-index"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/runtime/app/stores.js", + "size": 444, + "chunkUniqueIds": ["5-stores"] + }, + { + "name": "../../node_modules/.pnpm/svelte@4.2.15/node_modules/svelte/src/runtime/internal/utils.js", + "size": 2801, + "chunkUniqueIds": ["6-scheduler"] + }, + { + "name": "../../node_modules/.pnpm/svelte@4.2.15/node_modules/svelte/src/runtime/internal/lifecycle.js", + "size": 1404, + "chunkUniqueIds": ["6-scheduler"] + }, + { + "name": "../../node_modules/.pnpm/svelte@4.2.15/node_modules/svelte/src/runtime/internal/scheduler.js", + "size": 3992, + "chunkUniqueIds": ["6-scheduler"] + }, + { + "name": "./vite/preload-helper.js", + "size": 3313, + "chunkUniqueIds": ["7-entry/app"] + }, + { + "name": "./.svelte-kit/generated/client-optimized/matchers.js", + "size": 20, + "chunkUniqueIds": ["7-entry/app"] + }, + { + "name": "./.svelte-kit/generated/root.svelte", + "size": 14236, + "chunkUniqueIds": ["7-entry/app"] + }, + { + "name": "./.svelte-kit/generated/client-optimized/app.js", + "size": 986, + "chunkUniqueIds": ["7-entry/app"] + }, + { + "name": "./src/routes/sverdle/how-to-play/+page.ts", + "size": 40, + "chunkUniqueIds": ["8-nodes/5"] + }, + { + "name": "./src/routes/sverdle/how-to-play/+page.svelte?svelte&type=style&lang.css", + "size": 0, + "chunkUniqueIds": ["8-nodes/5"] + }, + { + "name": "./src/routes/sverdle/how-to-play/+page.svelte", + "size": 2713, + "chunkUniqueIds": ["8-nodes/5"] + }, + { + "name": "./.svelte-kit/generated/client-optimized/nodes/5.js", + "size": 0, + "chunkUniqueIds": ["8-nodes/5"] + }, + { + "name": "../../node_modules/.pnpm/svelte@4.2.15/node_modules/svelte/src/runtime/internal/dom.js", + "size": 13255, + "chunkUniqueIds": ["9-index"] + }, + { + "name": "../../node_modules/.pnpm/svelte@4.2.15/node_modules/svelte/src/runtime/internal/transitions.js", + "size": 1743, + "chunkUniqueIds": ["9-index"] + }, + { + "name": "../../node_modules/.pnpm/svelte@4.2.15/node_modules/svelte/src/runtime/internal/Component.js", + "size": 5380, + "chunkUniqueIds": ["9-index"] + }, + { + "name": "../../node_modules/.pnpm/svelte@4.2.15/node_modules/svelte/src/shared/version.js", + "size": 71, + "chunkUniqueIds": ["9-index"] + }, + { + "name": "../../node_modules/.pnpm/svelte@4.2.15/node_modules/svelte/src/runtime/internal/disclose-version/index.js", + "size": 131, + "chunkUniqueIds": ["9-index"] + }, + { + "name": "../../node_modules/.pnpm/svelte@4.2.15/node_modules/svelte/src/runtime/internal/environment.js", + "size": 215, + "chunkUniqueIds": ["10-nodes/2"] + }, + { + "name": "../../node_modules/.pnpm/svelte@4.2.15/node_modules/svelte/src/runtime/internal/loop.js", + "size": 718, + "chunkUniqueIds": ["10-nodes/2"] + }, + { + "name": "./src/routes/+page.ts", + "size": 23, + "chunkUniqueIds": ["10-nodes/2"] + }, + { + "name": "../../node_modules/.pnpm/svelte@4.2.15/node_modules/svelte/src/runtime/motion/utils.js", + "size": 140, + "chunkUniqueIds": ["10-nodes/2"] + }, + { + "name": "../../node_modules/.pnpm/svelte@4.2.15/node_modules/svelte/src/runtime/motion/spring.js", + "size": 4122, + "chunkUniqueIds": ["10-nodes/2"] + }, + { + "name": "./src/routes/Counter.svelte?svelte&type=style&lang.css", + "size": 0, + "chunkUniqueIds": ["10-nodes/2"] + }, + { + "name": "./src/routes/Counter.svelte", + "size": 5148, + "chunkUniqueIds": ["10-nodes/2"] + }, + { + "name": "./src/lib/images/svelte-welcome.webp", + "size": 43, + "chunkUniqueIds": ["10-nodes/2"] + }, + { + "name": "./src/lib/images/svelte-welcome.png", + "size": 52, + "chunkUniqueIds": ["10-nodes/2"] + }, + { + "name": "./src/routes/+page.svelte?svelte&type=style&lang.css", + "size": 0, + "chunkUniqueIds": ["10-nodes/2"] + }, + { + "name": "./src/routes/+page.svelte", + "size": 2703, + "chunkUniqueIds": ["10-nodes/2"] + }, + { + "name": "./.svelte-kit/generated/client-optimized/nodes/2.js", + "size": 0, + "chunkUniqueIds": ["10-nodes/2"] + }, + { + "name": "./src/lib/images/svelte-logo.svg", + "size": 1977, + "chunkUniqueIds": ["11-nodes/0"] + }, + { + "name": "./src/lib/images/github.svg", + "size": 2159, + "chunkUniqueIds": ["11-nodes/0"] + }, + { + "name": "./src/routes/Header.svelte?svelte&type=style&lang.css", + "size": 0, + "chunkUniqueIds": ["11-nodes/0"] + }, + { + "name": "./src/routes/Header.svelte", + "size": 7226, + "chunkUniqueIds": ["11-nodes/0"] + }, + { + "name": "./src/routes/styles.css", + "size": 0, + "chunkUniqueIds": ["11-nodes/0"] + }, + { + "name": "./src/routes/+layout.svelte?svelte&type=style&lang.css", + "size": 0, + "chunkUniqueIds": ["11-nodes/0"] + }, + { + "name": "./src/routes/+layout.svelte", + "size": 3021, + "chunkUniqueIds": ["11-nodes/0"] + }, + { + "name": "./.svelte-kit/generated/client-optimized/nodes/0.js", + "size": 0, + "chunkUniqueIds": ["11-nodes/0"] + }, + { + "name": "../../node_modules/.pnpm/svelte@4.2.15/node_modules/svelte/src/runtime/internal/globals.js", + "size": 196, + "chunkUniqueIds": ["12-nodes/4"] + }, + { + "name": "../../node_modules/.pnpm/svelte@4.2.15/node_modules/svelte/src/runtime/internal/each.js", + "size": 2277, + "chunkUniqueIds": ["12-nodes/4"] + }, + { + "name": "../../node_modules/.pnpm/@neoconfetti+svelte@1.0.0/node_modules/@neoconfetti/svelte/dist/index.js", + "size": 3863, + "chunkUniqueIds": ["12-nodes/4"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/runtime/app/forms.js", + "size": 5639, + "chunkUniqueIds": ["12-nodes/4"] + }, + { + "name": "./src/routes/sverdle/reduced-motion.ts", + "size": 603, + "chunkUniqueIds": ["12-nodes/4"] + }, + { + "name": "./src/routes/sverdle/+page.svelte?svelte&type=style&lang.css", + "size": 0, + "chunkUniqueIds": ["12-nodes/4"] + }, + { + "name": "./src/routes/sverdle/+page.svelte", + "size": 28009, + "chunkUniqueIds": ["12-nodes/4"] + }, + { + "name": "./.svelte-kit/generated/client-optimized/nodes/4.js", + "size": 0, + "chunkUniqueIds": ["12-nodes/4"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/utils/url.js", + "size": 2673, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/runtime/hash.js", + "size": 587, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/runtime/utils.js", + "size": 242, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/runtime/client/fetcher.js", + "size": 2586, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/utils/routing.js", + "size": 5778, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/runtime/client/parse.js", + "size": 1698, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/runtime/client/session-storage.js", + "size": 517, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "./virtual:__sveltekit/paths", + "size": 117, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "./virtual:__sveltekit/environment", + "size": 32, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/runtime/client/constants.js", + "size": 377, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/runtime/client/utils.js", + "size": 4247, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "../../node_modules/.pnpm/devalue@5.0.0/node_modules/devalue/src/constants.js", + "size": 140, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "../../node_modules/.pnpm/devalue@5.0.0/node_modules/devalue/src/parse.js", + "size": 2923, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/utils/exports.js", + "size": 335, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/utils/array.js", + "size": 199, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/runtime/control.js", + "size": 1090, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/runtime/shared.js", + "size": 172, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/utils/error.js", + "size": 296, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/runtime/client/client.js", + "size": 42521, + "chunkUniqueIds": ["13-entry"] + }, + { + "name": "../../node_modules/.pnpm/@sveltejs+kit@2.5.7_@sveltejs+vite-plugin-svelte@3.1.0_svelte@4.2.15_vite@5.2.10_@types+node@_hxiu4y2ji5vqh2sunewgtt73zq/node_modules/@sveltejs/kit/src/runtime/client/entry.js", + "size": 0, + "chunkUniqueIds": ["13-entry"] + } + ] + } + \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_asset_type_javascript_v1.json b/libs/shared/tests/samples/sample_bundle_stats_asset_type_javascript_v1.json new file mode 100644 index 0000000000..09083c933d --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_asset_type_javascript_v1.json @@ -0,0 +1,39 @@ +{ + "version": "1", + "plugin": { + "name": "test_only", + "version": "test" + }, + "builtAt": 1701451048604, + "duration": 331, + "bundler": { "name": "test_only", "version": "test" }, + "bundleName": "sample", + "assets": [ + { + "name": "assets/LazyComponent-123.js", + "size": 10, + "gzipSize": 5, + "normalized": "assets/LazyComponent-*.js" + }, + { + "name": "assets/index-abc.mjs", + "size": 20, + "gzipSize": 10, + "normalized": "assets/index-*.mjs" + }, + { + "name": "assets/index-xyz.cjs", + "size": 30, + "gzipSize": 15, + "normalized": "assets/index-*.cjs" + }, + { + "name": "assets/test.notjs", + "size": 30, + "gzipSize": 15, + "normalized": "assets/test.notjs" + } + ], + "chunks": [], + "modules": [] +} diff --git a/libs/shared/tests/samples/sample_bundle_stats_asset_type_javascript_v2.json b/libs/shared/tests/samples/sample_bundle_stats_asset_type_javascript_v2.json new file mode 100644 index 0000000000..2a027dc596 --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_asset_type_javascript_v2.json @@ -0,0 +1,39 @@ +{ + "version": "2", + "plugin": { + "name": "test_only", + "version": "test" + }, + "builtAt": 1701451048604, + "duration": 331, + "bundler": { "name": "test_only", "version": "test" }, + "bundleName": "sample", + "assets": [ + { + "name": "assets/LazyComponent-123.js", + "size": 10, + "gzipSize": 5, + "normalized": "assets/LazyComponent-*.js" + }, + { + "name": "assets/index-abc.mjs", + "size": 20, + "gzipSize": 10, + "normalized": "assets/index-*.mjs" + }, + { + "name": "assets/index-xyz.cjs", + "size": 30, + "gzipSize": 15, + "normalized": "assets/index-*.cjs" + }, + { + "name": "assets/test.notjs", + "size": 30, + "gzipSize": 15, + "normalized": "assets/test.notjs" + } + ], + "chunks": [], + "modules": [] +} diff --git a/libs/shared/tests/samples/sample_bundle_stats_decimal_size.json b/libs/shared/tests/samples/sample_bundle_stats_decimal_size.json new file mode 100644 index 0000000000..379b6c5ced --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_decimal_size.json @@ -0,0 +1,201 @@ +{ + "version": "1", + "plugin": { + "name": "codecov-vite-bundle-analysis-plugin", + "version": "1.0.0" + }, + "builtAt": 1701451048604, + "duration": 331, + "bundler": { "name": "rollup", "version": "3.29.4" }, + "bundleName": "sample", + "assets": [ + { + "name": "assets/react-35ef61ed.svg", + "size": 4126.0, + "normalized": "assets/react-*.svg" + }, + { + "name": "assets/index-d526a0c5.css", + "size": 1421.1, + "normalized": "assets/index-*.css" + }, + { + "name": "assets/LazyComponent-fcbb0922.js", + "size": 294.2, + "normalized": "assets/LazyComponent-*.js" + }, + { + "name": "assets/index-c8676264.js", + "size": 154.3, + "normalized": "assets/index-*.js" + }, + { + "name": "assets/index-666d2e09.js", + "size": 144577.4, + "normalized": "assets/index-*.js" + } + ], + "chunks": [ + { + "id": "LazyComponent", + "uniqueId": "0-LazyComponent", + "entry": false, + "initial": true, + "files": ["assets/LazyComponent-fcbb0922.js"], + "names": ["LazyComponent"] + }, + { + "id": "index", + "uniqueId": "1-index", + "entry": false, + "initial": true, + "files": ["assets/index-c8676264.js"], + "names": ["index"] + }, + { + "id": "index", + "uniqueId": "2-index", + "entry": true, + "initial": false, + "files": ["assets/index-666d2e09.js"], + "names": ["index"] + } + ], + "modules": [ + { + "name": "./src/LazyComponent/LazyComponent.tsx", + "size": 497.5, + "chunkUniqueIds": ["0-LazyComponent"] + }, + { + "name": "./src/IndexedLazyComponent/IndexedLazyComponent.tsx", + "size": 189.6, + "chunkUniqueIds": ["1-index"] + }, + { + "name": "./src/IndexedLazyComponent/index.ts", + "size": 0.7, + "chunkUniqueIds": ["1-index"] + }, + { + "name": "./vite/modulepreload-polyfill", + "size": 1548.8, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./commonjsHelpers.js", + "size": 140.9, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js?commonjs-module", + "size": 31.11, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js?commonjs-exports", + "size": 40.12, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js?commonjs-module", + "size": 26.13, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js?commonjs-exports", + "size": 30.14, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js", + "size": 7591.15, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js", + "size": 144.16, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js", + "size": 919.17, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js", + "size": 103.18, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js?commonjs-exports", + "size": 16, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js?commonjs-module", + "size": 29, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js?commonjs-exports", + "size": 33, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js?commonjs-module", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js?commonjs-exports", + "size": 34, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js", + "size": 4315, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js", + "size": 94, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js", + "size": 132340, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js", + "size": 755, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js", + "size": 102, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./vite/preload-helper", + "size": 2488, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./src/assets/react.svg", + "size": 45, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../../../../../vite.svg", + "size": 51, + "chunkUniqueIds": ["2-index"] + }, + { "name": "./src/App.css", "size": 17, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/App.tsx", "size": 1977, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/index.css", "size": 17, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/main.tsx", "size": 181, "chunkUniqueIds": ["2-index"] }, + { "name": "./index.html", "size": 0, "chunkUniqueIds": ["2-index"] } + ] + } \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_dynamic_import_routing_1.json b/libs/shared/tests/samples/sample_bundle_stats_dynamic_import_routing_1.json new file mode 100644 index 0000000000..6bafae92c2 --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_dynamic_import_routing_1.json @@ -0,0 +1,106 @@ +{ + "version": "3", + "builtAt": 1732907862271, + "duration": 252, + "bundleName": "dynamic_imports", + "outputPath": "/dist", + "bundler": { "name": "rollup", "version": "4.22.4" }, + "plugin": { "name": "@codecov/sveltekit-plugin", "version": "0.0.1-beta.11" }, + "assets": [ + { + "name": "A1.js", + "size": 1, + "gzipSize": 1, + "normalized": "index1-*.js" + }, + { + "name": "A2.js", + "size": 10, + "gzipSize": 10, + "normalized": "index2-*.js" + }, + { + "name": "A3.js", + "size": 100, + "gzipSize": 100, + "normalized": "index3-*.js" + }, + { + "name": "A4.js", + "size": 1000, + "gzipSize": 1000, + "normalized": "index4-*.js" + }, + { + "name": "A5.js", + "size": 1000, + "gzipSize": 1000, + "normalized": "index5-*.js" + } + ], + "chunks": [ + { + "id": "C1", + "uniqueId": "C1", + "entry": false, + "initial": true, + "files": ["A1.js"], + "names": ["index"], + "dynamicImports": ["A2.js"] + }, + { + "id": "C2", + "uniqueId": "C2", + "entry": false, + "initial": true, + "files": ["A2.js"], + "names": ["index"], + "dynamicImports": ["A3.js"] + }, + { + "id": "C3", + "uniqueId": "C3", + "entry": false, + "initial": true, + "files": ["A3.js"], + "names": ["index"], + "dynamicImports": ["A4.js"] + }, + { + "id": "C4", + "uniqueId": "C4", + "entry": false, + "initial": true, + "files": ["A3.js"], + "names": ["index"], + "dynamicImports": ["A5.js"] + } + ], + "modules": [ + { + "name": "./src/routes/sverdle/about/+page.ts", + "size": 189, + "chunkUniqueIds": ["C1"] + }, + { + "name": "./src/routes/sverdle/users/+page.ts", + "size": 189, + "chunkUniqueIds": ["C1"] + }, + { + "name": "./src/routes/sverdle/faq/+page.ts", + "size": 189, + "chunkUniqueIds": ["C2"] + }, + { + "name": "./.svelte-kit/generated/client-optimized/nodes/5-no-route.js", + "size": 189, + "chunkUniqueIds": ["C3"] + }, + { + "name": "./src/routes/sverdle/careers/+page.ts", + "size": 189, + "chunkUniqueIds": ["C3"] + } + ] + } \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_dynamic_imports_1.json b/libs/shared/tests/samples/sample_bundle_stats_dynamic_imports_1.json new file mode 100644 index 0000000000..892f129544 --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_dynamic_imports_1.json @@ -0,0 +1,207 @@ +{ + "version": "3", + "builtAt": 1732907862271, + "duration": 252, + "bundleName": "dynamic_imports", + "outputPath": "/dist", + "bundler": { "name": "rollup", "version": "4.22.4" }, + "plugin": { "name": "@codecov/vite-plugin", "version": "1.4.0" }, + "assets": [ + { + "name": "react.CHdo91hT.js", + "size": 4126, + "gzipSize": 2053, + "normalized": "react.*.js" + }, + { + "name": "index.CRhFRBHw.js", + "size": 1433, + "gzipSize": 743, + "normalized": "index.*.js" + }, + { + "name": "index-C-Z8zsvD.js", + "size": 161, + "gzipSize": 152, + "normalized": "index-*.js" + }, + { + "name": "LazyComponent-BBSC53Nv.js", + "size": 299, + "gzipSize": 246, + "normalized": "LazyComponent-*.js" + }, + { + "name": "assets/index-oTNkmlIs.js", + "size": 144686, + "gzipSize": 46757, + "normalized": "assets/index-*.js" + } + ], + "chunks": [ + { + "id": "index", + "uniqueId": "0-index", + "entry": false, + "initial": true, + "files": ["index-C-Z8zsvD.js"], + "names": ["index"], + "dynamicImports": [] + }, + { + "id": "LazyComponent", + "uniqueId": "1-LazyComponent", + "entry": false, + "initial": true, + "files": ["LazyComponent-BBSC53Nv.js"], + "names": ["LazyComponent"], + "dynamicImports": ["index-C-Z8zsvD.js"] + }, + { + "id": "index", + "uniqueId": "2-index", + "entry": true, + "initial": false, + "files": ["assets/index-oTNkmlIs.js"], + "names": ["index"], + "dynamicImports": ["index-C-Z8zsvD.js", "LazyComponent-BBSC53Nv.js"] + } + ], + "modules": [ + { + "name": "./src/IndexedLazyComponent/IndexedLazyComponent.tsx", + "size": 189, + "chunkUniqueIds": ["0-index"] + }, + { + "name": "./src/IndexedLazyComponent/index.ts", + "size": 0, + "chunkUniqueIds": ["0-index"] + }, + { + "name": "./src/LazyComponent/LazyComponent.tsx", + "size": 495, + "chunkUniqueIds": ["1-LazyComponent"] + }, + { + "name": "./vite/modulepreload-polyfill.js", + "size": 1280, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./commonjsHelpers.js", + "size": 140, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js?commonjs-module", + "size": 31, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js?commonjs-exports", + "size": 40, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js?commonjs-module", + "size": 26, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js?commonjs-exports", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js", + "size": 7591, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js", + "size": 144, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js", + "size": 919, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js", + "size": 103, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js?commonjs-exports", + "size": 16, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js?commonjs-module", + "size": 29, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js?commonjs-exports", + "size": 33, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js?commonjs-module", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js?commonjs-exports", + "size": 34, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js", + "size": 4315, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js", + "size": 94, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js", + "size": 132340, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js", + "size": 381, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js", + "size": 102, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./vite/preload-helper.js", + "size": 1928, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./src/assets/react.svg", + "size": 45, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../../../../../vite.svg", + "size": 51, + "chunkUniqueIds": ["2-index"] + }, + { "name": "./src/App.css", "size": 0, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/App.tsx", "size": 1975, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/index.css", "size": 0, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/main.tsx", "size": 181, "chunkUniqueIds": ["2-index"] }, + { "name": "./index.html", "size": 0, "chunkUniqueIds": ["2-index"] } + ] + } \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_dynamic_imports_2.json b/libs/shared/tests/samples/sample_bundle_stats_dynamic_imports_2.json new file mode 100644 index 0000000000..19e0306be7 --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_dynamic_imports_2.json @@ -0,0 +1,207 @@ +{ + "version": "3", + "builtAt": 1732907862271, + "duration": 252, + "bundleName": "dynamic_imports", + "outputPath": "/dist", + "bundler": { "name": "rollup", "version": "4.22.4" }, + "plugin": { "name": "@codecov/vite-plugin", "version": "1.4.0" }, + "assets": [ + { + "name": "react.CHdo91hT.js", + "size": 4126, + "gzipSize": 2053, + "normalized": "react.*.js" + }, + { + "name": "index.CRhFRBHw.js", + "size": 1433, + "gzipSize": 743, + "normalized": "index.*.js" + }, + { + "name": "index-C-Z8zsvD.js", + "size": 161, + "gzipSize": 152, + "normalized": "index-*.js" + }, + { + "name": "LazyComponent-BBSC53Nv.js", + "size": 299, + "gzipSize": 246, + "normalized": "LazyComponent-*.js" + }, + { + "name": "assets/index-oTNkmlIs.js", + "size": 144686, + "gzipSize": 46757, + "normalized": "assets/index-*.js" + } + ], + "chunks": [ + { + "id": "index", + "uniqueId": "0-index", + "entry": false, + "initial": true, + "files": ["index-C-Z8zsvD.js"], + "names": ["index"], + "dynamicImports": ["LazyComponent-BBSC53Nv.js"] + }, + { + "id": "LazyComponent", + "uniqueId": "1-LazyComponent", + "entry": false, + "initial": true, + "files": ["LazyComponent-BBSC53Nv.js"], + "names": ["LazyComponent"], + "dynamicImports": ["index-C-Z8zsvD.js"] + }, + { + "id": "index", + "uniqueId": "2-index", + "entry": true, + "initial": false, + "files": ["assets/index-oTNkmlIs.js"], + "names": ["index"], + "dynamicImports": ["index-C-Z8zsvD.js"] + } + ], + "modules": [ + { + "name": "./src/IndexedLazyComponent/IndexedLazyComponent.tsx", + "size": 189, + "chunkUniqueIds": ["0-index"] + }, + { + "name": "./src/IndexedLazyComponent/index.ts", + "size": 0, + "chunkUniqueIds": ["0-index"] + }, + { + "name": "./src/LazyComponent/LazyComponent.tsx", + "size": 495, + "chunkUniqueIds": ["1-LazyComponent"] + }, + { + "name": "./vite/modulepreload-polyfill.js", + "size": 1280, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./commonjsHelpers.js", + "size": 140, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js?commonjs-module", + "size": 31, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js?commonjs-exports", + "size": 40, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js?commonjs-module", + "size": 26, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js?commonjs-exports", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js", + "size": 7591, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js", + "size": 144, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js", + "size": 919, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js", + "size": 103, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js?commonjs-exports", + "size": 16, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js?commonjs-module", + "size": 29, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js?commonjs-exports", + "size": 33, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js?commonjs-module", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js?commonjs-exports", + "size": 34, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js", + "size": 4315, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js", + "size": 94, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js", + "size": 132340, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js", + "size": 381, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js", + "size": 102, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./vite/preload-helper.js", + "size": 1928, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./src/assets/react.svg", + "size": 45, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../../../../../vite.svg", + "size": 51, + "chunkUniqueIds": ["2-index"] + }, + { "name": "./src/App.css", "size": 0, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/App.tsx", "size": 1975, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/index.css", "size": 0, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/main.tsx", "size": 181, "chunkUniqueIds": ["2-index"] }, + { "name": "./index.html", "size": 0, "chunkUniqueIds": ["2-index"] } + ] + } \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_dynamic_imports_3.json b/libs/shared/tests/samples/sample_bundle_stats_dynamic_imports_3.json new file mode 100644 index 0000000000..d049bfed13 --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_dynamic_imports_3.json @@ -0,0 +1,209 @@ +{ + "version": "3", + "builtAt": 1732907862271, + "duration": 252, + "bundleName": "dynamic_imports", + "outputPath": "/dist", + "bundler": { "name": "rollup", "version": "4.22.4" }, + "plugin": { "name": "@codecov/vite-plugin", "version": "1.4.0" }, + "assets": [ + { + "name": "react.CHdo91hT.js", + "size": 4126, + "gzipSize": 2053, + "normalized": "react.*.js" + }, + { + "name": "index.CRhFRBHw.js", + "size": 1433, + "gzipSize": 743, + "normalized": "index.*.js" + }, + { + "name": "index-C-Z8zsvD.js", + "size": 161, + "gzipSize": 152, + "normalized": "index-*.js" + }, + { + "name": "LazyComponent-BBSC53Nv.js", + "size": 299, + "gzipSize": 246, + "normalized": "LazyComponent-*.js" + }, + { + "name": "assets/index-oTNkmlIs.js", + "size": 144686, + "gzipSize": 46757, + "normalized": "assets/index-*.js" + } + ], + "chunks": [ + { + "id": "index", + "uniqueId": "0-index", + "entry": false, + "initial": true, + "files": ["index-C-Z8zsvD.js"], + "names": ["index"], + "dynamicImports":[ + "this-is-a-picture-that-does-not-exist-in-assets.svg" + ] + }, + { + "id": "LazyComponent", + "uniqueId": "1-LazyComponent", + "entry": false, + "initial": true, + "files": ["LazyComponent-BBSC53Nv.js"], + "names": ["LazyComponent"], + "dynamicImports": ["index-C-Z8zsvD.js"] + }, + { + "id": "index", + "uniqueId": "2-index", + "entry": true, + "initial": false, + "files": ["assets/index-oTNkmlIs.js"], + "names": ["index"], + "dynamicImports": ["index-C-Z8zsvD.js", "LazyComponent-BBSC53Nv.js"] + } + ], + "modules": [ + { + "name": "./src/IndexedLazyComponent/IndexedLazyComponent.tsx", + "size": 189, + "chunkUniqueIds": ["0-index"] + }, + { + "name": "./src/IndexedLazyComponent/index.ts", + "size": 0, + "chunkUniqueIds": ["0-index"] + }, + { + "name": "./src/LazyComponent/LazyComponent.tsx", + "size": 495, + "chunkUniqueIds": ["1-LazyComponent"] + }, + { + "name": "./vite/modulepreload-polyfill.js", + "size": 1280, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./commonjsHelpers.js", + "size": 140, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js?commonjs-module", + "size": 31, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js?commonjs-exports", + "size": 40, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js?commonjs-module", + "size": 26, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js?commonjs-exports", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js", + "size": 7591, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js", + "size": 144, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js", + "size": 919, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js", + "size": 103, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js?commonjs-exports", + "size": 16, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js?commonjs-module", + "size": 29, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js?commonjs-exports", + "size": 33, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js?commonjs-module", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js?commonjs-exports", + "size": 34, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js", + "size": 4315, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js", + "size": 94, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js", + "size": 132340, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js", + "size": 381, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js", + "size": 102, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./vite/preload-helper.js", + "size": 1928, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./src/assets/react.svg", + "size": 45, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../../../../../vite.svg", + "size": 51, + "chunkUniqueIds": ["2-index"] + }, + { "name": "./src/App.css", "size": 0, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/App.tsx", "size": 1975, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/index.css", "size": 0, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/main.tsx", "size": 181, "chunkUniqueIds": ["2-index"] }, + { "name": "./index.html", "size": 0, "chunkUniqueIds": ["2-index"] } + ] + } \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_dynamic_imports_4.json b/libs/shared/tests/samples/sample_bundle_stats_dynamic_imports_4.json new file mode 100644 index 0000000000..c3594a121a --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_dynamic_imports_4.json @@ -0,0 +1,221 @@ +{ + "version": "3", + "builtAt": 1732907862271, + "duration": 252, + "bundleName": "bongo", + "outputPath": "/dist", + "bundler": { "name": "rollup", "version": "4.22.4" }, + "plugin": { "name": "@codecov/vite-plugin", "version": "1.4.0" }, + "assets": [ + { + "name": "react.CHdo91hT.js", + "size": 4126, + "gzipSize": 2053, + "normalized": "react.*.js" + }, + { + "name": "index.CRhFRBHw.js", + "size": 1433, + "gzipSize": 743, + "normalized": "index.*.js" + }, + { + "name": "index-C-Z8zsvD.js", + "size": 161, + "gzipSize": 152, + "normalized": "index-*.js" + }, + { + "name": "LazyComponent-BBSC53Nv.js", + "size": 299, + "gzipSize": 246, + "normalized": "LazyComponent-*.js" + }, + { + "name": "assets/index-oTNkmlIs.js", + "size": 144686, + "gzipSize": 46757, + "normalized": "assets/index-*.js" + }, + { + "name": "there-is-two-of-these-assets.js", + "size": 999999, + "gzipSize": 999999, + "normalized": "there-is-two-of-these-assets.js" + }, + { + "name": "there-is-two-of-these-assets.js", + "size": 888888, + "gzipSize": 888888, + "normalized": "there-is-two-of-these-assets.js" + } + ], + "chunks": [ + { + "id": "index", + "uniqueId": "0-index", + "entry": false, + "initial": true, + "files": ["index-C-Z8zsvD.js"], + "names": ["index"], + "dynamicImports":[ + "there-is-two-of-these-assets.js" + ] + }, + { + "id": "LazyComponent", + "uniqueId": "1-LazyComponent", + "entry": false, + "initial": true, + "files": ["LazyComponent-BBSC53Nv.js"], + "names": ["LazyComponent"], + "dynamicImports": ["index-C-Z8zsvD.js"] + }, + { + "id": "index", + "uniqueId": "2-index", + "entry": true, + "initial": false, + "files": ["assets/index-oTNkmlIs.js"], + "names": ["index"], + "dynamicImports": ["index-C-Z8zsvD.js", "LazyComponent-BBSC53Nv.js"] + } + ], + "modules": [ + { + "name": "./src/IndexedLazyComponent/IndexedLazyComponent.tsx", + "size": 189, + "chunkUniqueIds": ["0-index"] + }, + { + "name": "./src/IndexedLazyComponent/index.ts", + "size": 0, + "chunkUniqueIds": ["0-index"] + }, + { + "name": "./src/LazyComponent/LazyComponent.tsx", + "size": 495, + "chunkUniqueIds": ["1-LazyComponent"] + }, + { + "name": "./vite/modulepreload-polyfill.js", + "size": 1280, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./commonjsHelpers.js", + "size": 140, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js?commonjs-module", + "size": 31, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js?commonjs-exports", + "size": 40, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js?commonjs-module", + "size": 26, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js?commonjs-exports", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js", + "size": 7591, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js", + "size": 144, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js", + "size": 919, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js", + "size": 103, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js?commonjs-exports", + "size": 16, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js?commonjs-module", + "size": 29, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js?commonjs-exports", + "size": 33, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js?commonjs-module", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js?commonjs-exports", + "size": 34, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js", + "size": 4315, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js", + "size": 94, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js", + "size": 132340, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js", + "size": 381, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js", + "size": 102, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./vite/preload-helper.js", + "size": 1928, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./src/assets/react.svg", + "size": 45, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../../../../../vite.svg", + "size": 51, + "chunkUniqueIds": ["2-index"] + }, + { "name": "./src/App.css", "size": 0, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/App.tsx", "size": 1975, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/index.css", "size": 0, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/main.tsx", "size": 181, "chunkUniqueIds": ["2-index"] }, + { "name": "./index.html", "size": 0, "chunkUniqueIds": ["2-index"] } + ] + } \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_invalid_name.json b/libs/shared/tests/samples/sample_bundle_stats_invalid_name.json new file mode 100644 index 0000000000..552c342e33 --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_invalid_name.json @@ -0,0 +1,201 @@ +{ + "version": "1", + "plugin": { + "name": "codecov-vite-bundle-analysis-plugin", + "version": "1.0.0" + }, + "builtAt": 1701451048604, + "duration": 331, + "bundler": { "name": "rollup", "version": "3.29.4" }, + "bundleName": "sample!", + "assets": [ + { + "name": "assets/react-35ef61ed.svg", + "size": 4126, + "normalized": "assets/react-*.svg" + }, + { + "name": "assets/index-d526a0c5.css", + "size": 1421, + "normalized": "assets/index-*.css" + }, + { + "name": "assets/LazyComponent-fcbb0922.js", + "size": 294, + "normalized": "assets/LazyComponent-*.js" + }, + { + "name": "assets/index-c8676264.js", + "size": 154, + "normalized": "assets/index-*.js" + }, + { + "name": "assets/index-666d2e09.js", + "size": 144577, + "normalized": "assets/index-*.js" + } + ], + "chunks": [ + { + "id": "LazyComponent", + "uniqueId": "0-LazyComponent", + "entry": false, + "initial": true, + "files": ["assets/LazyComponent-fcbb0922.js"], + "names": ["LazyComponent"] + }, + { + "id": "index", + "uniqueId": "1-index", + "entry": false, + "initial": true, + "files": ["assets/index-c8676264.js"], + "names": ["index"] + }, + { + "id": "index", + "uniqueId": "2-index", + "entry": true, + "initial": false, + "files": ["assets/index-666d2e09.js"], + "names": ["index"] + } + ], + "modules": [ + { + "name": "./src/LazyComponent/LazyComponent.tsx", + "size": 497, + "chunkUniqueIds": ["0-LazyComponent"] + }, + { + "name": "./src/IndexedLazyComponent/IndexedLazyComponent.tsx", + "size": 189, + "chunkUniqueIds": ["1-index"] + }, + { + "name": "./src/IndexedLazyComponent/index.ts", + "size": 0, + "chunkUniqueIds": ["1-index"] + }, + { + "name": "./vite/modulepreload-polyfill", + "size": 1548, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./commonjsHelpers.js", + "size": 140, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js?commonjs-module", + "size": 31, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js?commonjs-exports", + "size": 40, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js?commonjs-module", + "size": 26, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js?commonjs-exports", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js", + "size": 7591, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js", + "size": 144, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js", + "size": 919, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js", + "size": 103, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js?commonjs-exports", + "size": 16, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js?commonjs-module", + "size": 29, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js?commonjs-exports", + "size": 33, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js?commonjs-module", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js?commonjs-exports", + "size": 34, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js", + "size": 4315, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js", + "size": 94, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js", + "size": 132340, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js", + "size": 755, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js", + "size": 102, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./vite/preload-helper", + "size": 2488, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./src/assets/react.svg", + "size": 45, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../../../../../vite.svg", + "size": 51, + "chunkUniqueIds": ["2-index"] + }, + { "name": "./src/App.css", "size": 17, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/App.tsx", "size": 1977, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/index.css", "size": 17, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/main.tsx", "size": 181, "chunkUniqueIds": ["2-index"] }, + { "name": "./index.html", "size": 0, "chunkUniqueIds": ["2-index"] } + ] + } \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_no_assets.json b/libs/shared/tests/samples/sample_bundle_stats_no_assets.json new file mode 100644 index 0000000000..ff8b5b5ae7 --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_no_assets.json @@ -0,0 +1,44 @@ +{ + "version": "1", + "plugin": { + "name": "codecov-vite-bundle-analysis-plugin", + "version": "1.0.0" + }, + "builtAt": 1701451048604, + "duration": 331, + "bundler": { "name": "rollup", "version": "3.29.4" }, + "bundleName": "b5", + "assets": [ + + ], + "chunks": [ + { + "id": "index", + "uniqueId": "b5-1-index", + "entry": false, + "initial": true, + "files": [], + "names": ["index"] + }, + { + "id": "index", + "uniqueId": "b5-2-index", + "entry": true, + "initial": false, + "files": [], + "names": ["index"] + } + ], + "modules": [ + { + "name": "./src/IndexedLazyComponent/IndexedLazyComponent.tsx", + "size": 1890, + "chunkUniqueIds": ["b5-1-index", "b5-2-index"] + }, + { + "name": "./src/IndexedLazyComponent/index.ts", + "size": 0, + "chunkUniqueIds": ["b5-1-index", "b5-2-index"] + } + ] + } \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_no_chunks.json b/libs/shared/tests/samples/sample_bundle_stats_no_chunks.json new file mode 100644 index 0000000000..fde744ddf1 --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_no_chunks.json @@ -0,0 +1,38 @@ +{ + "version": "1", + "plugin": { + "name": "codecov-vite-bundle-analysis-plugin", + "version": "1.0.0" + }, + "builtAt": 1701451048604, + "duration": 331, + "bundler": { "name": "rollup", "version": "3.29.4" }, + "bundleName": "sample", + "assets": [ + { + "name": "assets/index-c8676264.js", + "size": 154, + "normalized": "assets/index-*.js" + }, + { + "name": "assets/index-666d2e09.js", + "size": 144577, + "normalized": "assets/index-*.js" + } + ], + "chunks": [ + + ], + "modules": [ + { + "name": "./src/LazyComponent/LazyComponent.tsx", + "size": 497, + "chunkUniqueIds": [] + }, + { + "name": "./src/IndexedLazyComponent/IndexedLazyComponent.tsx", + "size": 189, + "chunkUniqueIds": [] + } + ] + } \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_no_modules.json b/libs/shared/tests/samples/sample_bundle_stats_no_modules.json new file mode 100644 index 0000000000..8de2c52303 --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_no_modules.json @@ -0,0 +1,44 @@ +{ + "version": "1", + "plugin": { + "name": "codecov-vite-bundle-analysis-plugin", + "version": "1.0.0" + }, + "builtAt": 1701451048604, + "duration": 331, + "bundler": { "name": "rollup", "version": "3.29.4" }, + "bundleName": "sample", + "assets": [ + { + "name": "assets/index-c8676264.js", + "size": 154, + "normalized": "assets/index-*.js" + }, + { + "name": "assets/index-666d2e09.js", + "size": 144577, + "normalized": "assets/index-*.js" + } + ], + "chunks": [ + { + "id": "index", + "uniqueId": "1-index", + "entry": false, + "initial": true, + "files": ["assets/index-c8676264.js"], + "names": ["index"] + }, + { + "id": "index", + "uniqueId": "2-index", + "entry": true, + "initial": false, + "files": ["assets/index-666d2e09.js"], + "names": ["index"] + } + ], + "modules": [ + + ] + } \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_other.json b/libs/shared/tests/samples/sample_bundle_stats_other.json new file mode 100644 index 0000000000..2a44d7ec5c --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_other.json @@ -0,0 +1,207 @@ +{ + "version": "2", + "plugin": { + "name": "codecov-vite-bundle-analysis-plugin", + "version": "1.0.0" + }, + "builtAt": 1701451048604, + "duration": 331, + "bundler": { "name": "rollup", "version": "3.29.4" }, + "bundleName": "sample", + "assets": [ + { + "name": "assets/other-35ef61ed.svg", + "size": 5126, + "gzipSize": 5125, + "normalized": "assets/other-*.svg" + }, + { + "name": "assets/index-d526a0c5.css", + "size": 1421, + "gzipSize": 1420, + "normalized": "assets/index-*.css" + }, + { + "name": "assets/LazyComponent-fcbb0922.js", + "size": 294, + "gzipSize": 293, + "normalized": "assets/LazyComponent-*.js" + }, + { + "name": "assets/index-c8676264.js", + "size": 254, + "gzipSize": 253, + "normalized": "assets/index-*.js" + }, + { + "name": "assets/index-666d2e09.js", + "size": 144577, + "gzipSize": 144576, + "normalized": "assets/index-*.js" + } + ], + "chunks": [ + { + "id": "LazyComponent", + "uniqueId": "0-LazyComponent", + "entry": false, + "initial": true, + "files": ["assets/LazyComponent-fcbb0922.js"], + "names": ["LazyComponent"] + }, + { + "id": "index", + "uniqueId": "1-index", + "entry": false, + "initial": true, + "files": ["assets/index-c8676264.js"], + "names": ["index"] + }, + { + "id": "index", + "uniqueId": "2-index", + "entry": true, + "initial": false, + "files": ["assets/index-666d2e09.js"], + "names": ["index"] + } + ], + "modules": [ + { + "name": "./src/LazyComponent/LazyComponent.tsx", + "size": 497, + "chunkUniqueIds": ["0-LazyComponent"] + }, + { + "name": "./src/IndexedLazyComponent/IndexedLazyComponent.tsx", + "size": 189, + "chunkUniqueIds": ["1-index"] + }, + { + "name": "./src/IndexedLazyComponent/index.ts", + "size": 0, + "chunkUniqueIds": ["1-index"] + }, + { + "name": "./vite/modulepreload-polyfill", + "size": 1548, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./commonjsHelpers.js", + "size": 140, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js?commonjs-module", + "size": 31, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js?commonjs-exports", + "size": 40, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js?commonjs-module", + "size": 26, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js?commonjs-exports", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js", + "size": 7591, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js", + "size": 144, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js", + "size": 919, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js", + "size": 103, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js?commonjs-exports", + "size": 16, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js?commonjs-module", + "size": 29, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js?commonjs-exports", + "size": 33, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js?commonjs-module", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js?commonjs-exports", + "size": 34, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js", + "size": 4315, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js", + "size": 94, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js", + "size": 132340, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js", + "size": 755, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js", + "size": 102, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./vite/preload-helper", + "size": 2488, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./src/assets/react.svg", + "size": 45, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../../../../../vite.svg", + "size": 51, + "chunkUniqueIds": ["2-index"] + }, + { "name": "./src/App.css", "size": 17, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/App.tsx", "size": 1977, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/index.css", "size": 17, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/main.tsx", "size": 181, "chunkUniqueIds": ["2-index"] }, + { "name": "./index.html", "size": 0, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/Other.tsx", "size": 100, "chunkUniqueIds": ["1-index"] } + ] +} \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_v1.json b/libs/shared/tests/samples/sample_bundle_stats_v1.json new file mode 100644 index 0000000000..86b16b108f --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_v1.json @@ -0,0 +1,201 @@ +{ + "version": "1", + "plugin": { + "name": "codecov-vite-bundle-analysis-plugin", + "version": "1.0.0" + }, + "builtAt": 1701451048604, + "duration": 331, + "bundler": { "name": "rollup", "version": "3.29.4" }, + "bundleName": "sample", + "assets": [ + { + "name": "assets/react-35ef61ed.svg", + "size": 4126, + "normalized": "assets/react-*.svg" + }, + { + "name": "assets/index-d526a0c5.css", + "size": 1421, + "normalized": "assets/index-*.css" + }, + { + "name": "assets/LazyComponent-fcbb0922.js", + "size": 294, + "normalized": "assets/LazyComponent-*.js" + }, + { + "name": "assets/index-c8676264.js", + "size": 154, + "normalized": "assets/index-*.js" + }, + { + "name": "assets/index-666d2e09.js", + "size": 144577, + "normalized": "assets/index-*.js" + } + ], + "chunks": [ + { + "id": "LazyComponent", + "uniqueId": "0-LazyComponent", + "entry": false, + "initial": true, + "files": ["assets/LazyComponent-fcbb0922.js"], + "names": ["LazyComponent"] + }, + { + "id": "index", + "uniqueId": "1-index", + "entry": false, + "initial": true, + "files": ["assets/index-c8676264.js"], + "names": ["index"] + }, + { + "id": "index", + "uniqueId": "2-index", + "entry": true, + "initial": true, + "files": ["assets/index-666d2e09.js"], + "names": ["index"] + } + ], + "modules": [ + { + "name": "./src/LazyComponent/LazyComponent.tsx", + "size": 497, + "chunkUniqueIds": ["0-LazyComponent"] + }, + { + "name": "./src/IndexedLazyComponent/IndexedLazyComponent.tsx", + "size": 189, + "chunkUniqueIds": ["1-index"] + }, + { + "name": "./src/IndexedLazyComponent/index.ts", + "size": 0, + "chunkUniqueIds": ["1-index"] + }, + { + "name": "./vite/modulepreload-polyfill", + "size": 1548, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./commonjsHelpers.js", + "size": 140, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js?commonjs-module", + "size": 31, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js?commonjs-exports", + "size": 40, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js?commonjs-module", + "size": 26, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js?commonjs-exports", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js", + "size": 7591, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js", + "size": 144, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js", + "size": 919, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js", + "size": 103, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js?commonjs-exports", + "size": 16, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js?commonjs-module", + "size": 29, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js?commonjs-exports", + "size": 33, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js?commonjs-module", + "size": 30, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js?commonjs-exports", + "size": 34, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js", + "size": 4315, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js", + "size": 94, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js", + "size": 132340, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js", + "size": 755, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js", + "size": 102, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./vite/preload-helper", + "size": 2488, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "./src/assets/react.svg", + "size": 45, + "chunkUniqueIds": ["2-index"] + }, + { + "name": "../../../../../../vite.svg", + "size": 51, + "chunkUniqueIds": ["2-index"] + }, + { "name": "./src/App.css", "size": 17, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/App.tsx", "size": 1977, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/index.css", "size": 17, "chunkUniqueIds": ["2-index"] }, + { "name": "./src/main.tsx", "size": 181, "chunkUniqueIds": ["2-index"] }, + { "name": "./index.html", "size": 0, "chunkUniqueIds": ["2-index"] } + ] + } \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_v3_comparison_base_1.json b/libs/shared/tests/samples/sample_bundle_stats_v3_comparison_base_1.json new file mode 100644 index 0000000000..195632b4c3 --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_v3_comparison_base_1.json @@ -0,0 +1,66 @@ +{ + "version": "3", + "builtAt": 1732907862271, + "duration": 252, + "bundleName": "bundle1", + "outputPath": "/dist", + "bundler": { "name": "rollup", "version": "4.22.4" }, + "plugin": { "name": "@codecov/sveltekit-plugin", "version": "0.0.1-beta.11" }, + "assets": [ + { + "name": "A1.js", + "size": 1, + "gzipSize": 1, + "normalized": "index1-*.js" + }, + { + "name": "A2.js", + "size": 10, + "gzipSize": 10, + "normalized": "index2-*.js" + }, + { + "name": "A3.js", + "size": 100, + "gzipSize": 100, + "normalized": "index3-*.js" + } + ], + "chunks": [ + { + "id": "C1", + "uniqueId": "C1", + "entry": false, + "initial": true, + "files": ["A1.js"], + "names": ["index"], + "dynamicImports": ["A2.js"] + }, + { + "id": "C2", + "uniqueId": "C2", + "entry": false, + "initial": true, + "files": ["A2.js"], + "names": ["index"], + "dynamicImports": ["A3.js"] + } + ], + "modules": [ + { + "name": "./src/routes/sverdle/about/+page.ts", + "size": 189, + "chunkUniqueIds": ["C1"] + }, + { + "name": "./src/routes/sverdle/users/+page.ts", + "size": 189, + "chunkUniqueIds": ["C1"] + }, + { + "name": "./src/routes/sverdle/faq/+page.ts", + "size": 189, + "chunkUniqueIds": ["C2"] + } + ] + } \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_v3_comparison_base_2.json b/libs/shared/tests/samples/sample_bundle_stats_v3_comparison_base_2.json new file mode 100644 index 0000000000..8cd6ee67af --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_v3_comparison_base_2.json @@ -0,0 +1,66 @@ +{ + "version": "3", + "builtAt": 1732907862271, + "duration": 252, + "bundleName": "bundle2", + "outputPath": "/dist", + "bundler": { "name": "rollup", "version": "4.22.4" }, + "plugin": { "name": "@codecov/sveltekit-plugin", "version": "0.0.1-beta.11" }, + "assets": [ + { + "name": "A1.js", + "size": 1, + "gzipSize": 1, + "normalized": "index1-*.js" + }, + { + "name": "A2.js", + "size": 10, + "gzipSize": 10, + "normalized": "index2-*.js" + }, + { + "name": "A3.js", + "size": 100, + "gzipSize": 100, + "normalized": "index3-*.js" + } + ], + "chunks": [ + { + "id": "C1", + "uniqueId": "C1", + "entry": false, + "initial": true, + "files": ["A1.js"], + "names": ["index"], + "dynamicImports": ["A2.js"] + }, + { + "id": "C2", + "uniqueId": "C2", + "entry": false, + "initial": true, + "files": ["A2.js"], + "names": ["index"], + "dynamicImports": ["A3.js"] + } + ], + "modules": [ + { + "name": "./src/routes/sverdle/about/+page.ts", + "size": 189, + "chunkUniqueIds": ["C1"] + }, + { + "name": "./src/routes/sverdle/users/+page.ts", + "size": 189, + "chunkUniqueIds": ["C1"] + }, + { + "name": "./src/routes/sverdle/faq/+page.ts", + "size": 189, + "chunkUniqueIds": ["C2"] + } + ] + } \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_v3_comparison_head_1.json b/libs/shared/tests/samples/sample_bundle_stats_v3_comparison_head_1.json new file mode 100644 index 0000000000..b2e668de0e --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_v3_comparison_head_1.json @@ -0,0 +1,66 @@ +{ + "version": "3", + "builtAt": 1732907862271, + "duration": 252, + "bundleName": "bundle1", + "outputPath": "/dist", + "bundler": { "name": "rollup", "version": "4.22.4" }, + "plugin": { "name": "@codecov/sveltekit-plugin", "version": "0.0.1-beta.11" }, + "assets": [ + { + "name": "A1.js", + "size": 1, + "gzipSize": 1, + "normalized": "index1-*.js" + }, + { + "name": "A2.js", + "size": 10, + "gzipSize": 10, + "normalized": "index2-*.js" + }, + { + "name": "A3-prime.js", + "size": 1000, + "gzipSize": 1000, + "normalized": "index3-*.js" + } + ], + "chunks": [ + { + "id": "C1", + "uniqueId": "C1", + "entry": false, + "initial": true, + "files": ["A1.js"], + "names": ["index"], + "dynamicImports": ["A2.js"] + }, + { + "id": "C2", + "uniqueId": "C2", + "entry": false, + "initial": true, + "files": ["A2.js"], + "names": ["index"], + "dynamicImports": ["A3-prime.js"] + } + ], + "modules": [ + { + "name": "./src/routes/sverdle/about/+page.ts", + "size": 189, + "chunkUniqueIds": ["C1"] + }, + { + "name": "./src/routes/sverdle/users/+page.ts", + "size": 189, + "chunkUniqueIds": ["C1"] + }, + { + "name": "./src/routes/sverdle/faq-prime/+page.ts", + "size": 189, + "chunkUniqueIds": ["C2"] + } + ] + } \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_v3_comparison_head_2.json b/libs/shared/tests/samples/sample_bundle_stats_v3_comparison_head_2.json new file mode 100644 index 0000000000..47702ecca0 --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_v3_comparison_head_2.json @@ -0,0 +1,66 @@ +{ + "version": "3", + "builtAt": 1732907862271, + "duration": 252, + "bundleName": "bundle2", + "outputPath": "/dist", + "bundler": { "name": "rollup", "version": "4.22.4" }, + "plugin": { "name": "@codecov/sveltekit-plugin", "version": "0.0.1-beta.11" }, + "assets": [ + { + "name": "A1.js", + "size": 10, + "gzipSize": 10, + "normalized": "index1-*.js" + }, + { + "name": "A2.js", + "size": 100, + "gzipSize": 100, + "normalized": "index2-*.js" + }, + { + "name": "A3-prime.js", + "size": 10000, + "gzipSize": 10000, + "normalized": "index3-*.js" + } + ], + "chunks": [ + { + "id": "C1", + "uniqueId": "C1", + "entry": false, + "initial": true, + "files": ["A1.js"], + "names": ["index"], + "dynamicImports": ["A2.js"] + }, + { + "id": "C2", + "uniqueId": "C2", + "entry": false, + "initial": true, + "files": ["A2.js"], + "names": ["index"], + "dynamicImports": ["A3-prime.js"] + } + ], + "modules": [ + { + "name": "./src/routes/sverdle/about/+page.ts", + "size": 189, + "chunkUniqueIds": ["C1"] + }, + { + "name": "./src/routes/sverdle/users/+page.ts", + "size": 189, + "chunkUniqueIds": ["C1"] + }, + { + "name": "./src/routes/sverdle/faq-prime/+page.ts", + "size": 189, + "chunkUniqueIds": ["C2"] + } + ] + } \ No newline at end of file diff --git a/libs/shared/tests/samples/sample_bundle_stats_w_bad_chunk.json b/libs/shared/tests/samples/sample_bundle_stats_w_bad_chunk.json new file mode 100644 index 0000000000..2c38c43219 --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_w_bad_chunk.json @@ -0,0 +1,49 @@ +{ + "version": "__VERSION__", + "plugin": { + "name": "codecov-vite-bundle-analysis-plugin", + "version": "1.0.0" + }, + "builtAt": 1701451048604, + "duration": 331, + "bundler": { "name": "rollup", "version": "3.29.4" }, + "bundleName": "sample", + "assets": [ + { + "name": "assets/LazyComponent-fcbb0922.js", + "size": 294, + "gzipSize": 293, + "normalized": "assets/LazyComponent-*.js" + } + ], + "chunks": [ + { + "id": "LazyComponent", + "uniqueId": "0-LazyComponent", + "entry": false, + "initial": true, + "files": ["assets/LazyComponent-fcbb0922.js"], + "names": ["LazyComponent"] + }, + { + "id": "this_claims_to_have_an_asset_that_does_not_exist", + "uniqueId": "invalid_chunk", + "entry": false, + "initial": true, + "files": ["assets/asset_that_does_not_exist.js"], + "names": ["Dodo"] + } + ], + "modules": [ + { + "name": "./src/LazyComponent/LazyComponent.tsx", + "size": 497, + "chunkUniqueIds": ["0-LazyComponent"] + }, + { + "name": "./xxx.jpg", + "size": 0, + "chunkUniqueIds": ["invalid_chunk"] + } + ] +} diff --git a/libs/shared/tests/samples/sample_bundle_stats_zero_size_asset.json b/libs/shared/tests/samples/sample_bundle_stats_zero_size_asset.json new file mode 100644 index 0000000000..68ac95822c --- /dev/null +++ b/libs/shared/tests/samples/sample_bundle_stats_zero_size_asset.json @@ -0,0 +1,36 @@ +{ + "version": "2", + "plugin": { + "name": "codecov-vite-bundle-analysis-plugin", + "version": "1.0.0" + }, + "builtAt": 1701451048604, + "duration": 331, + "bundler": { "name": "rollup", "version": "3.29.4" }, + "bundleName": "sample", + "assets": [ + { + "name": "assets/LazyComponent-fcbb0922.js", + "size": 0, + "gzipSize": 0, + "normalized": "assets/LazyComponent-*.js" + } + ], + "chunks": [ + { + "id": "LazyComponent", + "uniqueId": "0-LazyComponent", + "entry": false, + "initial": true, + "files": ["assets/LazyComponent-fcbb0922.js"], + "names": ["LazyComponent"] + } + ], + "modules": [ + { + "name": "./src/LazyComponent/LazyComponent.tsx", + "size": 0, + "chunkUniqueIds": ["0-LazyComponent"] + } + ] + } \ No newline at end of file diff --git a/libs/shared/tests/unit/__init__.py b/libs/shared/tests/unit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/tests/unit/analytics_tracking/test_analytics_tracking.py b/libs/shared/tests/unit/analytics_tracking/test_analytics_tracking.py new file mode 100644 index 0000000000..dc821ac1c8 --- /dev/null +++ b/libs/shared/tests/unit/analytics_tracking/test_analytics_tracking.py @@ -0,0 +1,214 @@ +import json +from datetime import datetime, timezone + +import pytest +from mock import patch + +from shared.analytics_tracking import get_list_of_analytic_tools, get_tools_manager +from shared.analytics_tracking.events import Event, Events +from shared.analytics_tracking.manager import AnalyticsToolManager +from shared.analytics_tracking.marketo import Marketo +from shared.analytics_tracking.pubsub import CustomJSONEncoder, PubSub +from shared.config import ConfigHelper + + +@pytest.fixture +def mock_pubsub(): + with patch("shared.analytics_tracking.PubSub.is_enabled", return_value=True): + yield + + +@pytest.fixture +def mock_marketo(): + with patch("shared.analytics_tracking.Marketo.is_enabled", return_value=True): + yield + + +@pytest.fixture +def mock_pubsub_publisher(): + with patch( + "shared.analytics_tracking.pubsub.pubsub_v1.PublisherClient" + ) as mock_publisher_client: + mock_publisher = mock_publisher_client.return_value + mock_publisher.topic_path.return_value = "projects/1234/topics/codecov" + yield mock_publisher + + +def test_get_list_of_analytic_tools(): + tools = get_list_of_analytic_tools() + assert isinstance(tools, list) + + +def test_get_tools_manager(mock_pubsub): + tool = get_tools_manager() + assert tool is not None + assert isinstance(tool, AnalyticsToolManager) + + +def test_track_event(mock_pubsub, mock_marketo, mock_pubsub_publisher, mocker): + analytics_tool = get_tools_manager() + mock_marketo_request = mocker.patch( + "shared.analytics_tracking.Marketo.make_rest_request" + ) + event = Event( + Events.USER_SIGNED_IN.value, + test=True, + ) + mocker.patch("shared.analytics_tracking.manager.Event", return_value=event) + analytics_tool.track_event( + Events.USER_SIGNED_IN.value, + is_enterprise=False, + event_data={"test": True}, + ) + + mock_pubsub_publisher.publish.assert_called_with( + "projects/1234/topics/codecov", + data=json.dumps(event.serialize()).encode("utf-8"), + ) + mock_marketo_request.assert_called_with( + Marketo.LEAD_URL, + method="POST", + json={ + "input": [event.serialize()], + }, + ) + + +def test_track_event_tool_not_enabled(mocker, mock_pubsub_publisher): + analytics_tool = get_tools_manager() + + analytics_tool.track_event( + Events.USER_SIGNED_UP.value, + is_enterprise=False, + event_data={"test": True}, + ) + + mock_pubsub_publisher.publish.assert_not_called() + + +def test_track_event_invalid_name(mocker, mock_pubsub_publisher): + analytics_tool = get_tools_manager() + with pytest.raises(ValueError): + analytics_tool.track_event( + "Invalid Name", is_enterprise=False, event_data={"test": True} + ) + mock_pubsub_publisher.publish.assert_not_called() + + +def test_track_event_is_enterprise(mock_pubsub_publisher, mocker): + analytics_tool = get_tools_manager() + analytics_tool.track_event( + "codecov.account.uploaded_coverage_report", + is_enterprise=True, + event_data={"test": True}, + ) + mock_pubsub_publisher.publish.assert_not_called() + + +class TestPubSub(object): + def test_pubsub_enabled(self, mocker): + yaml_content = "\n".join( + [ + "setup:", + " pubsub:", + " enabled: true", + ] + ) + mocker.patch.object(ConfigHelper, "load_yaml_file", return_value=yaml_content) + this_config = ConfigHelper() + mocker.patch("shared.config._get_config_instance", return_value=this_config) + assert PubSub.is_enabled() + + def test_pubsub_not_enabled(self, mocker): + yaml_content = "\n".join( + [ + "setup:", + " pubsub:", + " enabled: false", + ] + ) + mocker.patch.object(ConfigHelper, "load_yaml_file", return_value=yaml_content) + this_config = ConfigHelper() + mocker.patch("shared.config._get_config_instance", return_value=this_config) + assert not PubSub.is_enabled() + + def test_pubsub_initialized(self, mocker, mock_pubsub_publisher): + yaml_content = "\n".join( + [ + "setup:", + " pubsub:", + " enabled: true", + " project_id: '1234'", + " topic: codecov", + ] + ) + mocker.patch.object(ConfigHelper, "load_yaml_file", return_value=yaml_content) + this_config = ConfigHelper() + mocker.patch("shared.config._get_config_instance", return_value=this_config) + pubsub = PubSub() + assert pubsub.topic == "projects/1234/topics/codecov" + assert pubsub.project == "1234" + + def test_pubsub_track_event(self, mocker, mock_pubsub_publisher, mock_pubsub): + event = Event( + Events.ACCOUNT_ACTIVATED_REPOSITORY.value, + user_id="1234", + repo_id="1234", + branch="test_branch", + ) + pubsub = PubSub() + pubsub.track_event(event) + mock_pubsub_publisher.publish.assert_called_with( + pubsub.topic, data=json.dumps(event.serialize()).encode("utf-8") + ) + + def test_pubsub_track_event_with_datetime( + self, mocker, mock_pubsub_publisher, mock_pubsub + ): + other_timestamp = datetime(2023, 9, 12, tzinfo=timezone.utc) + event = Event( + event_name=Events.ACCOUNT_ACTIVATED_REPOSITORY.value, + other_timestamp=other_timestamp, + user_id="1234", + repo_id="1234", + branch="test_branch", + ) + pubsub = PubSub() + pubsub.track_event(event) + serialized_event = json.dumps(event.serialize(), cls=CustomJSONEncoder).encode( + "utf-8" + ) + mock_pubsub_publisher.publish.assert_called_with( + pubsub.topic, data=serialized_event + ) + + +class TestEvent(object): + def test_event(self, mocker): + class uuid(object): + bytes = b"\x00\x01\x02" + + mocker.patch("shared.analytics_tracking.events.uuid1", return_value=uuid) + event = Event( + Events.ACCOUNT_ACTIVATED_REPOSITORY.value, + dt=datetime(2023, 9, 12, tzinfo=timezone.utc), + user_id="1234", + repo_id="1234", + branch="test_branch", + ) + assert event.serialize() == { + "uuid": "AAEC", + "timestamp": 1694476800.0, + "type": "codecov.account.activated_repository", + "data": {"user_id": "1234", "repo_id": "1234", "branch": "test_branch"}, + } + + def test_invalid_event(self, mocker): + with pytest.raises(ValueError, match="Invalid event name: Invalid name"): + Event( + "Invalid name", + dt=datetime(2023, 9, 12, tzinfo=timezone.utc), + user_id="1234", + repo_id="1234", + branch="test_branch", + ) diff --git a/libs/shared/tests/unit/analytics_tracking/test_marketo.py b/libs/shared/tests/unit/analytics_tracking/test_marketo.py new file mode 100644 index 0000000000..8e9da970f7 --- /dev/null +++ b/libs/shared/tests/unit/analytics_tracking/test_marketo.py @@ -0,0 +1,231 @@ +import httpx +import pytest +import respx + +from shared.analytics_tracking.events import Event, Events +from shared.analytics_tracking.marketo import Marketo, MarketoError +from shared.config import ConfigHelper + + +class TestMarketo(object): + @pytest.fixture + def mock_setup(self, mocker): + yaml_content = "\n".join( + [ + "setup:", + " marketo:", + " enabled: true", + " client_id: 1234", + " client_secret: secret", + " base_url: https://marketo/test", + ] + ) + mocker.patch.object(ConfigHelper, "load_yaml_file", return_value=yaml_content) + this_config = ConfigHelper() + mocker.patch("shared.config._get_config_instance", return_value=this_config) + + @pytest.mark.asyncio + def test_make_rest_request(self, mocker, mock_setup): + with respx.mock: + respx.get( + "https://marketo/test/identity/oauth/token?grant_type=client_credentials&client_id=1234&client_secret=secret" + ).mock( + return_value=httpx.Response( + status_code=200, + json={ + "access_token": "test1657-1111-2222-3333-444444444444:int", + "token_type": "bearer", + "expires_in": 3599, + "scope": "apis@acmeinc.com", + }, + ) + ) + respx.get("https://marketo/test/path/url").mock( + return_value=httpx.Response( + status_code=200, + headers={ + "X-RateLimit-Remaining": "0", + "X-RateLimit-Reset": "1350085394", + }, + json={ + "requestId": "e42b#14272d07d78", + "success": True, + "result": [ + { + "id": 318581, + "updatedAt": "2015-06-11T23:15:23Z", + "lastName": "Doe", + "email": "jdoe@marketo.com", + "createdAt": "2015-03-17T00:18:40Z", + "firstName": "John", + } + ], + }, + ) + ) + marketo = Marketo() + url = "/path/url" + response = marketo.make_rest_request(url) + assert response == { + "requestId": "e42b#14272d07d78", + "success": True, + "result": [ + { + "id": 318581, + "updatedAt": "2015-06-11T23:15:23Z", + "lastName": "Doe", + "email": "jdoe@marketo.com", + "createdAt": "2015-03-17T00:18:40Z", + "firstName": "John", + } + ], + } + + @pytest.mark.asyncio + def test_track_event(self, mocker, mock_setup): + class uuid(object): + bytes = b"\x00\x01\x02" + + with respx.mock: + respx.get( + "https://marketo/test/identity/oauth/token?grant_type=client_credentials&client_id=1234&client_secret=secret" + ).mock( + return_value=httpx.Response( + status_code=200, + json={ + "access_token": "test1657-1111-2222-3333-444444444444:int", + "token_type": "bearer", + "expires_in": 3599, + "scope": "apis@acmeinc.com", + }, + ) + ) + respx.post("https://marketo/test/rest/v1/leads.json").mock( + return_value=httpx.Response( + status_code=200, + headers={ + "X-RateLimit-Remaining": "0", + "X-RateLimit-Reset": "1350085394", + }, + json={ + "requestId": "e42b#14272d07d78", + "success": True, + "result": [ + { + "id": 318581, + "updatedAt": "2015-06-11T23:15:23Z", + "lastName": "Doe", + "email": "jdoe@marketo.com", + "createdAt": "2015-03-17T00:18:40Z", + "firstName": "John", + } + ], + }, + ) + ) + mocker.patch("shared.analytics_tracking.events.uuid1", return_value=uuid) + event = Event( + Events.ACCOUNT_UPLOADED_COVERAGE_REPORT.value, + user_id="1234", + repo_id="1234", + branch="test_branch", + ) + marketo = Marketo() + response = marketo.track_event(event, is_enterprise=False, context=None) + assert response == { + "requestId": "e42b#14272d07d78", + "success": True, + "result": [ + { + "id": 318581, + "updatedAt": "2015-06-11T23:15:23Z", + "lastName": "Doe", + "email": "jdoe@marketo.com", + "createdAt": "2015-03-17T00:18:40Z", + "firstName": "John", + } + ], + } + + @pytest.mark.asyncio + def test_make_failed_rest_request(self, mocker, mock_setup): + with respx.mock: + respx.get( + "https://marketo/test/identity/oauth/token?grant_type=client_credentials&client_id=1234&client_secret=secret" + ).mock( + return_value=httpx.Response( + status_code=200, + json={ + "access_token": "test1657-1111-2222-3333-444444444444:int", + "token_type": "bearer", + "expires_in": 3599, + "scope": "apis@acmeinc.com", + }, + ) + ) + respx.get("https://marketo/test/path/url").mock( + return_value=httpx.Response( + status_code=200, + headers={ + "X-RateLimit-Remaining": "0", + "X-RateLimit-Reset": "1350085394", + }, + json={ + "requestId": "e42b#14272d07d78", + "success": False, + "errors": [{"code": "601", "message": "Unauthorized"}], + }, + ) + ) + marketo = Marketo() + url = "/path/url" + with pytest.raises(MarketoError) as exc: + marketo.make_rest_request(url) + assert exc.value.code == "601" + assert exc.value.message == "Unauthorized" + + @pytest.mark.asyncio + def test_make_rest_request_failed_record_level(self, mocker, mock_setup): + with respx.mock: + respx.get( + "https://marketo/test/identity/oauth/token?grant_type=client_credentials&client_id=1234&client_secret=secret" + ).mock( + return_value=httpx.Response( + status_code=200, + json={ + "access_token": "test1657-1111-2222-3333-444444444444:int", + "token_type": "bearer", + "expires_in": 3599, + "scope": "apis@acmeinc.com", + }, + ) + ) + respx.get("https://marketo/test/path/url").mock( + return_value=httpx.Response( + status_code=200, + headers={ + "X-RateLimit-Remaining": "0", + "X-RateLimit-Reset": "1350085394", + }, + json={ + "requestId": "e42b#14272d07d78", + "success": True, + "result": [ + {"id": 50, "status": "created"}, + {"id": 51, "status": "created"}, + { + "status": "skipped", + "reasons": [ + {"code": "1005", "message": "Lead already exists"} + ], + }, + ], + }, + ) + ) + marketo = Marketo() + url = "/path/url" + with pytest.raises(MarketoError) as exc: + marketo.make_rest_request(url) + assert exc.value.code == "1005" + assert exc.value.message == "Lead already exists" diff --git a/libs/shared/tests/unit/bots/test_bots.py b/libs/shared/tests/unit/bots/test_bots.py new file mode 100644 index 0000000000..a883f0fa94 --- /dev/null +++ b/libs/shared/tests/unit/bots/test_bots.py @@ -0,0 +1,670 @@ +import datetime +from typing import List, Optional +from unittest.mock import patch + +import pytest + +from shared.bots import get_adapter_auth_information +from shared.bots.types import AdapterAuthInformation +from shared.django_apps.codecov_auth.models import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, +) +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory +from shared.django_apps.core.tests.factories import RepositoryFactory +from shared.torngit.base import TokenType +from shared.typings.oauth_token_types import Token +from shared.typings.torngit import GithubInstallationInfo +from shared.utils.test_utils import mock_config_helper + + +def get_github_integration_token_side_effect( + service: str, + installation_id: int = None, + app_id: Optional[str] = None, + pem_path: Optional[str] = None, +): + return f"installation_token_{installation_id}_{app_id}" + + +class TestGettingAdapterAuthInformation(object): + class TestGitHubOwnerNoRepoInfo(object): + def _generate_test_owner( + self, + *, + with_bot: bool, + integration_id: int | None = None, + ghapp_installations: List[GithubAppInstallation] = None, + ): + if ghapp_installations is None: + ghapp_installations = [] + owner = OwnerFactory( + service="github", + bot=None, + unencrypted_oauth_token="owner_token: :refresh_token", + integration_id=integration_id, + ) + if with_bot: + owner.bot = OwnerFactory( + service="github", + unencrypted_oauth_token="bot_token: :bot_refresh_token", + ) + owner.save() + + if ghapp_installations: + for app in ghapp_installations: + app.owner = owner + app.save() + + assert bool(owner.bot) == with_bot + assert list(owner.github_app_installations.all()) == ghapp_installations + + return owner + + @pytest.mark.django_db(databases={"default"}) + def test_select_owner_info(self): + owner = self._generate_test_owner(with_bot=False) + expected = AdapterAuthInformation( + token=Token( + key="owner_token", + refresh_token="refresh_token", + secret=None, + entity_name=str(owner.ownerid), + ), + token_owner=owner, + selected_installation_info=None, + fallback_installations=None, + token_type_mapping=None, + ) + assert get_adapter_auth_information(owner) == expected + + @pytest.mark.django_db(databases={"default"}) + def test_select_owner_bot_info(self): + owner = self._generate_test_owner(with_bot=True) + expected = AdapterAuthInformation( + token=Token( + key="bot_token", + refresh_token="bot_refresh_token", + secret=None, + entity_name=str(owner.bot.ownerid), + ), + token_owner=owner.bot, + selected_installation_info=None, + fallback_installations=None, + token_type_mapping=None, + ) + assert get_adapter_auth_information(owner) == expected + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + @pytest.mark.django_db(databases={"default"}) + def test_select_owner_single_installation( + self, mock_get_github_integration_token + ): + installations = [ + GithubAppInstallation( + repository_service_ids=None, + installation_id=1200, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=200, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ) + ] + owner = self._generate_test_owner( + with_bot=False, ghapp_installations=installations + ) + expected = AdapterAuthInformation( + token=Token( + key="installation_token_1200_200", + entity_name="200_1200", + username="installation_1200", + ), + token_owner=None, + selected_installation_info=GithubInstallationInfo( + id=installations[0].id, + installation_id=1200, + app_id=200, + pem_path="pem_path", + ), + fallback_installations=[], + token_type_mapping=None, + ) + assert get_adapter_auth_information(owner) == expected + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + @pytest.mark.django_db(databases={"default"}) + def test_select_owner_single_installation_ignoring_installations( + self, mock_get_github_integration_token + ): + installations = [ + GithubAppInstallation( + repository_service_ids=None, + installation_id=1200, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=200, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ) + ] + owner = self._generate_test_owner( + with_bot=False, ghapp_installations=installations + ) + expected = AdapterAuthInformation( + token=Token( + key="owner_token", + refresh_token="refresh_token", + secret=None, + entity_name=str(owner.ownerid), + ), + token_owner=owner, + selected_installation_info=None, + fallback_installations=None, + token_type_mapping=None, + ) + assert ( + get_adapter_auth_information(owner, ignore_installations=True) + == expected + ) + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + @pytest.mark.django_db(databases={"default"}) + def test_select_owner_deprecated_using_integration( + self, mock_get_github_integration_token + ): + owner = self._generate_test_owner(with_bot=False, integration_id=1500) + owner.oauth_token = None + # Owner has no GithubApp, no token, and no bot configured + # The integration_id is selected + expected = AdapterAuthInformation( + token=Token( + key="installation_token_1500_None", + entity_name="default_app_1500", + username="installation_1500", + ), + token_owner=None, + selected_installation_info=GithubInstallationInfo(installation_id=1500), + fallback_installations=[], + token_type_mapping=None, + ) + assert get_adapter_auth_information(owner) == expected + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + @pytest.mark.django_db(databases={"default"}) + def test_select_owner_multiple_installations_default_name( + self, mock_get_github_integration_token + ): + installations = [ + GithubAppInstallation( + repository_service_ids=None, + installation_id=1200, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=200, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ), + # This should be ignored in the selection because of the name + GithubAppInstallation( + repository_service_ids=None, + installation_id=1300, + name="my_dedicated_app", + app_id=300, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ), + ] + owner = self._generate_test_owner( + with_bot=False, ghapp_installations=installations + ) + expected = AdapterAuthInformation( + token=Token( + key="installation_token_1200_200", + entity_name="200_1200", + username="installation_1200", + ), + token_owner=None, + selected_installation_info=GithubInstallationInfo( + id=installations[0].id, + installation_id=1200, + app_id=200, + pem_path="pem_path", + ), + fallback_installations=[], + token_type_mapping=None, + ) + assert get_adapter_auth_information(owner) == expected + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + @pytest.mark.django_db(databases={"default"}) + def test_select_owner_multiple_installations_custom_name( + self, mock_get_github_integration_token + ): + installations = [ + GithubAppInstallation( + repository_service_ids=None, + installation_id=1200, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=200, + pem_path="pem_path", + created_at=datetime.datetime.now(), + ), + # This should be selected first + GithubAppInstallation( + repository_service_ids=None, + installation_id=1300, + name="my_dedicated_app", + app_id=300, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ), + ] + owner = self._generate_test_owner( + with_bot=False, ghapp_installations=installations + ) + expected = AdapterAuthInformation( + token=Token( + key="installation_token_1300_300", + entity_name="300_1300", + username="installation_1300", + ), + token_owner=None, + selected_installation_info=GithubInstallationInfo( + id=installations[1].id, + installation_id=1300, + app_id=300, + pem_path="pem_path", + ), + fallback_installations=[ + GithubInstallationInfo( + id=installations[0].id, + installation_id=1200, + app_id=200, + pem_path="pem_path", + ) + ], + token_type_mapping=None, + ) + assert ( + get_adapter_auth_information( + owner, installation_name_to_use="my_dedicated_app" + ) + == expected + ) + + class TestGitHubOwnerWithRepoInfo(object): + def _generate_test_repo( + self, + *, + with_bot: bool, + with_owner_bot: bool, + integration_id: int | None = None, + private: bool = True, + ghapp_installations: List[GithubAppInstallation] = None, + ): + if ghapp_installations is None: + ghapp_installations = [] + owner = OwnerFactory( + service="github", + bot=None, + unencrypted_oauth_token="owner_token: :refresh_token", + integration_id=integration_id, + ) + if with_owner_bot: + owner.bot = OwnerFactory( + service="github", + unencrypted_oauth_token="bot_token: :bot_refresh_token", + ) + owner.save() + + if ghapp_installations: + for app in ghapp_installations: + app.owner = owner + app.save() + + repo = RepositoryFactory( + author=owner, + using_integration=(integration_id is not None), + private=private, + ) + if with_bot: + repo.bot = OwnerFactory( + service="github", + unencrypted_oauth_token="repo_bot_token: :repo_bot_refresh_token", + ) + + repo.save() + + assert bool(owner.bot) == with_owner_bot + assert bool(repo.bot) == with_bot + assert list(owner.github_app_installations.all()) == ghapp_installations + + return repo + + @pytest.mark.django_db(databases={"default"}) + def test_select_repo_info_fallback_to_owner(self): + repo = self._generate_test_repo(with_bot=False, with_owner_bot=False) + expected = AdapterAuthInformation( + token=Token( + key="owner_token", + refresh_token="refresh_token", + secret=None, + username=repo.author.username, + entity_name=str(repo.author.ownerid), + ), + token_owner=repo.author, + selected_installation_info=None, + fallback_installations=None, + token_type_mapping=None, + ) + assert get_adapter_auth_information(repo.author, repo) == expected + + @pytest.mark.django_db(databases={"default"}) + def test_select_owner_bot_info(self): + repo = self._generate_test_repo(with_owner_bot=True, with_bot=False) + expected = AdapterAuthInformation( + token=Token( + key="bot_token", + refresh_token="bot_refresh_token", + secret=None, + username=repo.author.bot.username, + entity_name=str(repo.author.bot.ownerid), + ), + token_owner=repo.author.bot, + selected_installation_info=None, + fallback_installations=None, + token_type_mapping=None, + ) + assert get_adapter_auth_information(repo.author, repo) == expected + + @pytest.mark.django_db(databases={"default"}) + def test_select_repo_bot_info(self): + repo = self._generate_test_repo(with_owner_bot=True, with_bot=True) + expected = AdapterAuthInformation( + token=Token( + key="repo_bot_token", + refresh_token="repo_bot_refresh_token", + secret=None, + username=repo.bot.username, + entity_name=str(repo.bot.ownerid), + ), + token_owner=repo.bot, + selected_installation_info=None, + fallback_installations=None, + token_type_mapping=None, + ) + assert get_adapter_auth_information(repo.author, repo) == expected + + @pytest.mark.django_db(databases={"default"}) + def test_select_repo_bot_info_public_repo(self, mock_configuration): + repo = self._generate_test_repo( + with_owner_bot=True, with_bot=True, private=False + ) + mock_configuration.set_params( + { + "github": { + "bot": {"key": "some_key"}, + "bots": { + "read": {"key": "read_bot_key"}, + "status": {"key": "status_bot_key"}, + "comment": {"key": "commenter_bot_key"}, + }, + } + } + ) + + repo_bot_token = Token( + key="repo_bot_token", + refresh_token="repo_bot_refresh_token", + secret=None, + username=repo.bot.username, + entity_name=str(repo.bot.ownerid), + ) + expected = AdapterAuthInformation( + token=repo_bot_token, + token_owner=repo.bot, + selected_installation_info=None, + fallback_installations=None, + token_type_mapping={ + TokenType.comment: Token(key="commenter_bot_key"), + TokenType.read: repo_bot_token, + TokenType.admin: repo_bot_token, + TokenType.status: repo_bot_token, + TokenType.tokenless: repo_bot_token, + TokenType.pull: repo_bot_token, + TokenType.commit: repo_bot_token, + }, + ) + assert get_adapter_auth_information(repo.author, repo) == expected + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + @pytest.mark.django_db(databases={"default"}) + def test_select_repo_single_installation( + self, mock_get_github_integration_token + ): + installations = [ + GithubAppInstallation( + repository_service_ids=None, + installation_id=1200, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=200, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ) + ] + repo = self._generate_test_repo( + with_bot=False, + with_owner_bot=False, + ghapp_installations=installations, + ) + expected = AdapterAuthInformation( + token=Token( + key="installation_token_1200_200", + entity_name="200_1200", + username="installation_1200", + ), + token_owner=None, + selected_installation_info=GithubInstallationInfo( + id=installations[0].id, + installation_id=1200, + app_id=200, + pem_path="pem_path", + ), + fallback_installations=[], + token_type_mapping=None, + ) + assert get_adapter_auth_information(repo.author, repo) == expected + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + @pytest.mark.django_db(databases={"default"}) + def test_select_repo_deprecated_using_integration( + self, mock_get_github_integration_token + ): + repo = self._generate_test_repo( + with_bot=False, integration_id=1500, with_owner_bot=False + ) + repo.author.oauth_token = None + # Repo's owner has no GithubApp, no token, and no bot configured + # The repo has not a bot configured + # The integration_id is no longer verified + # So we fail with exception + expected = AdapterAuthInformation( + token=Token( + key="installation_token_1500_None", + entity_name="default_app_1500", + username="installation_1500", + ), + token_owner=None, + selected_installation_info=GithubInstallationInfo(installation_id=1500), + fallback_installations=[], + token_type_mapping=None, + ) + assert get_adapter_auth_information(repo.author, repo) == expected + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + @pytest.mark.django_db(databases={"default"}) + def test_select_repo_multiple_installations_default_name( + self, mock_get_github_integration_token + ): + installations = [ + GithubAppInstallation( + repository_service_ids=None, + installation_id=1200, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=200, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ), + # This should be ignored in the selection because of the name + GithubAppInstallation( + repository_service_ids=None, + installation_id=1300, + name="my_dedicated_app", + app_id=300, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ), + ] + repo = self._generate_test_repo( + with_bot=False, + with_owner_bot=False, + ghapp_installations=installations, + ) + expected = AdapterAuthInformation( + token=Token( + key="installation_token_1200_200", + entity_name="200_1200", + username="installation_1200", + ), + token_owner=None, + selected_installation_info=GithubInstallationInfo( + id=installations[0].id, + installation_id=1200, + app_id=200, + pem_path="pem_path", + ), + fallback_installations=[], + token_type_mapping=None, + ) + assert get_adapter_auth_information(repo.author, repo) == expected + + @patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, + ) + @pytest.mark.django_db(databases={"default"}) + def test_select_repo_multiple_installations_custom_name( + self, mock_get_github_integration_token + ): + installations = [ + GithubAppInstallation( + repository_service_ids=None, + installation_id=1200, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=200, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ), + # This should be selected first + GithubAppInstallation( + repository_service_ids=None, + installation_id=1300, + name="my_dedicated_app", + app_id=300, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + ), + ] + repo = self._generate_test_repo( + with_bot=False, + with_owner_bot=False, + ghapp_installations=installations, + ) + expected = AdapterAuthInformation( + token=Token( + key="installation_token_1300_300", + entity_name="300_1300", + username="installation_1300", + ), + token_owner=None, + selected_installation_info=GithubInstallationInfo( + id=installations[1].id, + installation_id=1300, + app_id=300, + pem_path="pem_path", + ), + fallback_installations=[ + GithubInstallationInfo( + id=installations[0].id, + installation_id=1200, + app_id=200, + pem_path="pem_path", + ) + ], + token_type_mapping=None, + ) + assert ( + get_adapter_auth_information( + repo.author, repo, installation_name_to_use="my_dedicated_app" + ) + == expected + ) + + @pytest.mark.parametrize("service", ["github", "gitlab"]) + @pytest.mark.django_db(databases={"default"}) + def test_select_repo_public_with_no_token_no_admin_token_configured( + self, service, mocker + ): + repo = RepositoryFactory( + author__service=service, private=False, bot=None, author__oauth_token=None + ) + repo.save() + mock_config_helper( + mocker, + configs={ + f"{service}.bots.tokenless": {"key": "tokenless_bot_token"}, + f"{service}.bots.comment": {"key": "commenter_bot_token"}, + f"{service}.bots.read": {"key": "reader_bot_token"}, + f"{service}.bots.status": {"key": "status_bot_token"}, + }, + ) + expected = AdapterAuthInformation( + token=Token(key="tokenless_bot_token", entity_name="tokenless"), + token_owner=None, + selected_installation_info=None, + fallback_installations=None, + token_type_mapping={ + TokenType.comment: Token(key="commenter_bot_token"), + TokenType.read: Token(key="reader_bot_token", entity_name="read"), + TokenType.admin: None, + TokenType.status: Token(key="status_bot_token"), + TokenType.tokenless: Token( + key="tokenless_bot_token", entity_name="tokenless" + ), + TokenType.pull: None, + TokenType.commit: None, + }, + ) + assert get_adapter_auth_information(repo.author, repo) == expected diff --git a/libs/shared/tests/unit/bots/test_bots_helpers.py b/libs/shared/tests/unit/bots/test_bots_helpers.py new file mode 100644 index 0000000000..cb548b5bab --- /dev/null +++ b/libs/shared/tests/unit/bots/test_bots_helpers.py @@ -0,0 +1,131 @@ +from unittest.mock import patch + +import pytest + +from shared.bots.helpers import ( + get_dedicated_app_token_from_config, + get_token_type_from_config, +) +from shared.torngit.base import TokenType +from shared.typings.oauth_token_types import Token + + +@pytest.fixture +def mock_configuration(mock_configuration): + custom_params = { + "gitlab": { + "bots": { + "read": {"key": "bot_token", "username": "read_bot"}, + }, + }, + "github": { + "bots": { + "tokenless": {"key": "bot_token", "username": "tokenless_bot"}, + "read": {"key": "bot_token", "username": "read_bot"}, + }, + "dedicated_apps": { + "read": {"id": 1234, "installation_id": 1000, "pem": "some_path"}, + "commit": { + "id": 2345, + "installation_id": 1000, + "pem": "another_path", + }, + "tokenless": {"id": 1111}, # not configured (missing pem) + }, + }, + } + mock_configuration.set_params(custom_params) + return custom_params + + +@pytest.mark.parametrize( + "token_type", + [ + # Has a dedicated app that is configured + pytest.param(TokenType.read, id="TokenType.read-app_configured"), + # Has a _different_ dedicated app that is configured + pytest.param(TokenType.commit, id="TokenType.commit-app_configured"), + ], +) +@patch("shared.bots.helpers.get_github_integration_token") +def test_get_dedicated_app_token_from_config( + mock_get_integration_token, token_type: TokenType, mock_configuration +): + mock_get_integration_token.return_value = "installation_access_token" + dedicated_app_details = mock_configuration["github"]["dedicated_apps"][ + token_type.value + ] + + assert get_dedicated_app_token_from_config("github", token_type) == Token( + key="installation_access_token", + username=f"{token_type.value}_dedicated_app", + entity_name=token_type.value, + ) + mock_get_integration_token.assert_called_with( + "github", + app_id=str(dedicated_app_details["id"]), + installation_id=dedicated_app_details["installation_id"], + pem_path=f"yaml+file://github.dedicated_apps.{token_type.value}.pem", + ) + + +@pytest.mark.parametrize( + "token_type", + [ + # No configuration present + pytest.param(TokenType.pull, id="TokenType.pull-app_NOT_configured"), + # Some configuration exist, but it's not properly configured, so we can't use + pytest.param( + TokenType.tokenless, + id="TokenType.tokenless-app_NOT_properly_configured", + ), + ], +) +@patch("shared.bots.helpers.get_github_integration_token") +def test_get_dedicated_app_token_from_config_not_configured( + mock_get_integration_token, token_type, mock_configuration +): + assert get_dedicated_app_token_from_config("github", token_type) is None + mock_get_integration_token.assert_not_called() + + +@pytest.mark.parametrize( + "token_type, expected", + [ + pytest.param( + TokenType.read, + Token( + key="installation_access_token", + username="read_dedicated_app", + entity_name="read", + ), + id="dedicated_app_AND_bot_configured", + ), + pytest.param(TokenType.pull, None, id="no_dedicated_app_no_bot"), + pytest.param( + TokenType.tokenless, + Token( + key="bot_token", + username="tokenless_bot", + entity_name="tokenless", + ), + id="no_dedicated_app_yes_bot", + ), + ], +) +@patch("shared.bots.helpers.get_github_integration_token") +def test_get_token_type_from_config( + mock_get_integration_token, token_type, expected, mock_configuration +): + mock_get_integration_token.return_value = "installation_access_token" + assert get_token_type_from_config("github", token_type) == expected + + +@patch("shared.bots.helpers.get_github_integration_token") +def test_get_token_type_from_config_not_github_skip_dedicated_apps( + mock_get_integration_token, mock_configuration +): + assert get_token_type_from_config("gitlab", TokenType.read) == Token( + key="bot_token", username="read_bot", entity_name="read" + ) + mock_get_integration_token.assert_not_called() diff --git a/libs/shared/tests/unit/bots/test_github_apps.py b/libs/shared/tests/unit/bots/test_github_apps.py new file mode 100644 index 0000000000..58694b21c7 --- /dev/null +++ b/libs/shared/tests/unit/bots/test_github_apps.py @@ -0,0 +1,236 @@ +import datetime + +import pytest + +from shared.bots.exceptions import NoConfiguredAppsAvailable, RequestedGithubAppNotFound +from shared.bots.github_apps import ( + get_github_app_info_for_owner, + get_github_app_token, + get_specific_github_app_details, +) +from shared.django_apps.codecov_auth.models import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, + Owner, + Service, +) +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory +from shared.github import InvalidInstallationError +from shared.typings.torngit import GithubInstallationInfo + + +def _get_owner_with_apps() -> Owner: + owner = OwnerFactory(service="github", integration_id=1111) + # Just so there are other owners in the database + _ = OwnerFactory(service="github", integration_id=1234) + app_1 = GithubAppInstallation( + owner=owner, + installation_id=1200, + app_id=12, + ) + app_2 = GithubAppInstallation( + owner=owner, + installation_id=1500, + app_id=15, + pem_path="some_path", + ) + GithubAppInstallation.objects.bulk_create([app_1, app_2]) + assert list(owner.github_app_installations.all()) == [app_1, app_2] + return owner + + +def _to_installation_info( + installation: GithubAppInstallation, +) -> GithubInstallationInfo: + return GithubInstallationInfo( + id=installation.id, + installation_id=installation.installation_id, + pem_path=installation.pem_path, + app_id=installation.app_id, + ) + + +class TestGetSpecificGithubAppDetails(object): + @pytest.mark.django_db(databases={"default"}) + def test_get_specific_github_app_details(self): + owner = _get_owner_with_apps() + assert get_specific_github_app_details( + owner, owner.github_app_installations.all()[0].id, "commit_id_for_logs" + ) == GithubInstallationInfo( + id=owner.github_app_installations.all()[0].id, + installation_id=1200, + app_id=12, + pem_path=None, + ) + assert get_specific_github_app_details( + owner, owner.github_app_installations.all()[1].id, "commit_id_for_logs" + ) == GithubInstallationInfo( + id=owner.github_app_installations.all()[1].id, + installation_id=1500, + app_id=15, + pem_path="some_path", + ) + + @pytest.mark.django_db(databases={"default"}) + def test_get_specific_github_app_not_found(self): + owner = _get_owner_with_apps() + with pytest.raises(RequestedGithubAppNotFound): + get_specific_github_app_details(owner, 123456, "commit_id_for_logs") + + @pytest.mark.parametrize( + "app, is_rate_limited", + [ + pytest.param( + GithubAppInstallation( + repository_service_ids=None, + installation_id=1400, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=400, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + is_suspended=True, + ), + False, + id="suspended_app", + ), + pytest.param( + GithubAppInstallation( + repository_service_ids=None, + installation_id=1400, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=400, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + is_suspended=False, + ), + True, + id="rate_limited_app", + ), + ], + ) + @pytest.mark.django_db(databases={"default"}) + def test_raise_NoAppsConfiguredAvailable_if_suspended_or_rate_limited( + self, app, is_rate_limited, mocker + ): + owner = OwnerFactory( + service="github", + bot=None, + unencrypted_oauth_token="owner_token: :refresh_token", + ) + owner.save() + + app.owner = owner + app.save() + + mock_is_rate_limited = mocker.patch( + "shared.bots.github_apps.determine_if_entity_is_rate_limited", + return_value=is_rate_limited, + ) + with pytest.raises(NoConfiguredAppsAvailable) as exp: + get_github_app_info_for_owner(owner) + mock_is_rate_limited.assert_called() + assert exp.value.apps_count == 1 + assert exp.value.suspended_count == int(app.is_suspended) + assert exp.value.rate_limited_count == int(is_rate_limited) + + +class TestGettingGitHubAppTokenSideEffect(object): + @pytest.mark.django_db(databases={"default"}) + def test_mark_installation_suspended_side_effect(self, mocker): + owner = _get_owner_with_apps() + installations: list[GithubAppInstallation] = ( + owner.github_app_installations.all() + ) + installation_info = _to_installation_info(installations[0]) + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=InvalidInstallationError("installation_suspended"), + ) + + assert all( + [installation.is_suspended == False for installation in installations] + ) + + with pytest.raises(InvalidInstallationError): + get_github_app_token(Service(owner.service), installation_info) + + installations[0].refresh_from_db() + assert installations[0].is_suspended is True + installations[1].refresh_from_db() + assert installations[1].is_suspended is False + assert not Owner.objects.filter(integration_id=None).exists() + + @pytest.mark.django_db(databases={"default"}) + def test_mark_installation_not_found_side_effect(self, mocker): + owner = _get_owner_with_apps() + installations: list[GithubAppInstallation] = ( + owner.github_app_installations.all() + ) + installation_info = _to_installation_info(installations[0]) + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=InvalidInstallationError("installation_not_found"), + ) + + assert all( + [installation.is_suspended == False for installation in installations] + ) + + with pytest.raises(InvalidInstallationError): + get_github_app_token(Service(owner.service), installation_info) + + installations[1].refresh_from_db() + assert installations[1].is_suspended is False + assert not Owner.objects.filter(integration_id=None).exists() + owner.refresh_from_db() + assert list(owner.github_app_installations.all()) == [installations[1]] + + @pytest.mark.django_db(databases={"default"}) + def test_mark_installation_suspended_legacy_path_side_effect(self, mocker): + owner = _get_owner_with_apps() + installations: list[GithubAppInstallation] = ( + owner.github_app_installations.all() + ) + installation_info = {"installation_id": 1111} + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=InvalidInstallationError("installation_not_found"), + ) + + assert all( + [installation.is_suspended == False for installation in installations] + ) + + with pytest.raises(InvalidInstallationError): + get_github_app_token(Service(owner.service), installation_info) + + for installation in installations: + installation.refresh_from_db() + assert installation.is_suspended == False + owner.refresh_from_db() + assert list(Owner.objects.filter(integration_id=None).all()) == [owner] + + @pytest.mark.django_db(databases={"default"}) + def test_mark_installation_suspended_side_effect_not_called(self, mocker): + owner = _get_owner_with_apps() + installations: list[GithubAppInstallation] = ( + owner.github_app_installations.all() + ) + installation_info = _to_installation_info(installations[0]) + mocker.patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=InvalidInstallationError("permission_error"), + ) + + assert all( + [installation.is_suspended == False for installation in installations] + ) + + with pytest.raises(InvalidInstallationError): + get_github_app_token(Service(owner.service), installation_info) + + installations[0].refresh_from_db() + installations[1].refresh_from_db() + assert all( + [installation.is_suspended == False for installation in installations] + ) diff --git a/libs/shared/tests/unit/bundle_analysis/test_asset_association.py b/libs/shared/tests/unit/bundle_analysis/test_asset_association.py new file mode 100644 index 0000000000..7a7f85a7a6 --- /dev/null +++ b/libs/shared/tests/unit/bundle_analysis/test_asset_association.py @@ -0,0 +1,98 @@ +from pathlib import Path +from typing import Dict + +from shared.bundle_analysis import BundleAnalysisReport +from shared.bundle_analysis.models import Asset, AssetType + +bundle_stats_prev_a_path = ( + Path(__file__).parent.parent.parent / "samples" / "asset_link_prev_a.json" +) + +bundle_stats_prev_b_path = ( + Path(__file__).parent.parent.parent / "samples" / "asset_link_prev_b.json" +) + +bundle_stats_curr_a_path = ( + Path(__file__).parent.parent.parent / "samples" / "asset_link_curr_a.json" +) + +bundle_stats_curr_b_path = ( + Path(__file__).parent.parent.parent / "samples" / "asset_link_curr_b.json" +) + + +def _get_asset_mapping( + bundle_analysis_report: BundleAnalysisReport, bundle_name: Asset +) -> Dict[str, str]: + bundle_report = bundle_analysis_report.bundle_report(bundle_name) + asset_report = list(bundle_report.asset_reports()) + return {asset.hashed_name: asset for asset in asset_report} + + +def test_asset_association(): + try: + prev_bar = BundleAnalysisReport() + prev_bar.ingest(bundle_stats_prev_a_path) + prev_bar.ingest(bundle_stats_prev_b_path) + + prev_a_asset_mapping = _get_asset_mapping(prev_bar, "BundleA") + prev_b_asset_mapping = _get_asset_mapping(prev_bar, "BundleB") + + curr_bar = BundleAnalysisReport() + curr_bar.ingest(bundle_stats_curr_a_path) + curr_bar.ingest(bundle_stats_curr_b_path) + + curr_a_asset_mapping_before = _get_asset_mapping(curr_bar, "BundleA") + curr_b_asset_mapping_before = _get_asset_mapping(curr_bar, "BundleB") + + curr_bar.associate_previous_assets(prev_bar) + + curr_a_asset_mapping_after = _get_asset_mapping(curr_bar, "BundleA") + curr_b_asset_mapping_after = _get_asset_mapping(curr_bar, "BundleB") + + # Check that non javascript asset types didn't have their UUIDs updated + for hashed_name, asset in curr_a_asset_mapping_before.items(): + if asset.asset_type != AssetType.JAVASCRIPT: + assert asset.uuid == curr_a_asset_mapping_after[hashed_name].uuid + for hashed_name, asset in curr_b_asset_mapping_before.items(): + if asset.asset_type != AssetType.JAVASCRIPT: + assert asset.uuid == curr_b_asset_mapping_after[hashed_name].uuid + + # Same name -> asset associated + asset_a = prev_a_asset_mapping["asset-same-name-diff-modules.js"] + assert ( + curr_a_asset_mapping_after["asset-same-name-diff-modules.js"].uuid + == asset_a.uuid + ) + asset_b = prev_b_asset_mapping["asset-same-name-diff-modules.js"] + assert ( + curr_b_asset_mapping_after["asset-same-name-diff-modules.js"].uuid + == asset_b.uuid + ) + + # Diff name, same modules -> asset associated + asset_a = prev_a_asset_mapping["asset-diff-name-same-modules-ONE.js"] + assert ( + asset_a.uuid + == curr_a_asset_mapping_after["asset-diff-name-same-modules-TWO.js"].uuid + ) + asset_b = prev_b_asset_mapping["asset-diff-name-same-modules-ONE.js"] + assert ( + asset_b.uuid + == curr_b_asset_mapping_after["asset-diff-name-same-modules-TWO.js"].uuid + ) + # Diff name, diff modules -> asset not associated + asset_a = prev_a_asset_mapping["asset-diff-name-diff-modules-ONE.js"] + assert ( + asset_a.uuid + != curr_a_asset_mapping_after["asset-diff-name-diff-modules-TWO.js"].uuid + ) + asset_b = prev_b_asset_mapping["asset-diff-name-diff-modules-ONE.js"] + assert ( + asset_b.uuid + != curr_b_asset_mapping_after["asset-diff-name-diff-modules-TWO.js"].uuid + ) + + finally: + prev_bar.cleanup() + curr_bar.cleanup() diff --git a/libs/shared/tests/unit/bundle_analysis/test_bundle_analysis.py b/libs/shared/tests/unit/bundle_analysis/test_bundle_analysis.py new file mode 100644 index 0000000000..9cc59fa976 --- /dev/null +++ b/libs/shared/tests/unit/bundle_analysis/test_bundle_analysis.py @@ -0,0 +1,1156 @@ +from pathlib import Path +from typing import Tuple +from unittest import TestCase +from unittest.mock import patch + +import pytest +from sqlalchemy import select +from sqlalchemy.orm import Session as DbSession + +from shared.bundle_analysis import BundleAnalysisReport, BundleAnalysisReportLoader +from shared.bundle_analysis.models import ( + SCHEMA_VERSION, + Asset, + AssetType, + Bundle, + Chunk, + DynamicImport, + Metadata, + MetadataKey, + Module, + Session, + get_db_session, +) +from shared.storage.exceptions import PutRequestRateLimitError +from shared.storage.memory import MemoryStorageService + +sample_bundle_stats_path = ( + Path(__file__).parent.parent.parent / "samples" / "sample_bundle_stats.json" +) + +sample_bundle_stats_path_2 = ( + Path(__file__).parent.parent.parent / "samples" / "sample_bundle_stats_other.json" +) + +sample_bundle_stats_path_3 = ( + Path(__file__).parent.parent.parent + / "samples" + / "sample_bundle_stats_invalid_name.json" +) + +sample_bundle_stats_path_4 = ( + Path(__file__).parent.parent.parent / "samples" / "sample_bundle_stats_v1.json" +) + +sample_bundle_stats_path_5 = ( + Path(__file__).parent.parent.parent + / "samples" + / "sample_bundle_stats_another_bundle.json" +) + +sample_bundle_stats_path_6 = ( + Path(__file__).parent.parent.parent + / "samples" + / "sample_bundle_stats_asset_routes.json" +) + +sample_bundle_stats_path_7 = ( + Path(__file__).parent.parent.parent + / "samples" + / "sample_bundle_stats_dynamic_imports_1.json" +) + +sample_bundle_stats_path_8 = ( + Path(__file__).parent.parent.parent + / "samples" + / "sample_bundle_stats_dynamic_imports_2.json" +) + +sample_bundle_stats_path_9 = ( + Path(__file__).parent.parent.parent + / "samples" + / "sample_bundle_stats_dynamic_import_routing_1.json" +) + +sample_bundle_stats_path_10 = ( + Path(__file__).parent.parent.parent + / "samples" + / "sample_bundle_stats_w_bad_chunk.json" +) + +sample_bundle_stats_path_11 = ( + Path(__file__).parent.parent.parent + / "samples" + / "sample_bundle_stats_dynamic_imports_3.json" +) + +sample_bundle_stats_path_12 = ( + Path(__file__).parent.parent.parent + / "samples" + / "sample_bundle_stats_dynamic_imports_4.json" +) + + +def _table_rows_count(db_session: DbSession) -> Tuple[int]: + return ( + db_session.query(Bundle).count(), + db_session.query(Session).count(), + db_session.query(Asset).count(), + db_session.query(Chunk).count(), + db_session.query(Module).count(), + ) + + +def test_create_bundle_report(): + try: + report = BundleAnalysisReport() + session_id, bundle_name = report.ingest(sample_bundle_stats_path) + assert session_id == 1 + assert bundle_name == "sample" + + assert report.metadata() == { + MetadataKey.SCHEMA_VERSION: SCHEMA_VERSION, + } + + bundle_reports = list(report.bundle_reports()) + assert len(bundle_reports) == 1 + + bundle_report = report.bundle_report("invalid") + assert bundle_report is None + bundle_report = report.bundle_report("sample") + + # Find an asset by its name + asset_report_by_name = bundle_report.asset_report_by_name( + "assets/index-666d2e09.js" + ) + assert asset_report_by_name.name == "assets/index-*.js" + assert asset_report_by_name.hashed_name == "assets/index-666d2e09.js" + assert asset_report_by_name.size == 144577 + assert asset_report_by_name.gzip_size == 144576 + assert len(asset_report_by_name.modules()) == 28 + assert asset_report_by_name.asset_type == AssetType.JAVASCRIPT + + # Find a non existent asset by its name + asset_report_by_name = bundle_report.asset_report_by_name( + "assets/doesnotexist.js" + ) + assert asset_report_by_name is None + + asset_reports = list(bundle_report.asset_reports()) + + assert [ + ( + ar.name, + ar.hashed_name, + ar.size, + ar.gzip_size, + len(ar.modules()), + ar.asset_type, + ) + for ar in asset_reports + ] == [ + # FIXME: this is wrong since it's capturing the SVG and CSS modules as well. + # Made a similar note in the parser code where the associations are made + ( + "assets/index-*.js", + "assets/index-666d2e09.js", + 144577, + 144576, + 28, + AssetType.JAVASCRIPT, + ), + ( + "assets/react-*.svg", + "assets/react-35ef61ed.svg", + 4126, + 4125, + 0, + AssetType.IMAGE, + ), + ( + "assets/index-*.css", + "assets/index-d526a0c5.css", + 1421, + 1420, + 0, + AssetType.STYLESHEET, + ), + ( + "assets/LazyComponent-*.js", + "assets/LazyComponent-fcbb0922.js", + 294, + 293, + 1, + AssetType.JAVASCRIPT, + ), + ( + "assets/index-*.js", + "assets/index-c8676264.js", + 154, + 153, + 2, + AssetType.JAVASCRIPT, + ), + ] + + for ar in asset_reports: + for module in ar.modules(): + assert isinstance(module.name, str) + assert isinstance(module.size, int) + + assert bundle_report.total_size() == 150572 + assert report.session_count() == 1 + finally: + report.cleanup() + + +def test_bundle_report_asset_filtering(): + try: + report = BundleAnalysisReport() + session_id, bundle_name = report.ingest(sample_bundle_stats_path) + assert session_id == 1 + assert bundle_name == "sample" + + assert report.metadata() == { + MetadataKey.SCHEMA_VERSION: SCHEMA_VERSION, + } + + bundle_reports = list(report.bundle_reports()) + assert len(bundle_reports) == 1 + + bundle_report = report.bundle_report("invalid") + assert bundle_report is None + bundle_report = report.bundle_report("sample") + + all_asset_reports = list(bundle_report.asset_reports()) + assert len(all_asset_reports) == 5 + + filtered_asset_reports = list( + bundle_report.asset_reports( + asset_types=[AssetType.JAVASCRIPT], + chunk_entry=True, + chunk_initial=True, + ) + ) + + for ar in filtered_asset_reports: + for module in ar.modules(): + assert isinstance(module.name, str) + assert isinstance(module.size, int) + + assert len(filtered_asset_reports) == 1 + assert ( + bundle_report.total_size( + asset_types=[AssetType.JAVASCRIPT], + chunk_entry=True, + chunk_initial=True, + ) + == 144577 + ) + + finally: + report.cleanup() + + +def test_bundle_report_asset_ordering(): + try: + report = BundleAnalysisReport() + session_id, bundle_name = report.ingest(sample_bundle_stats_path) + assert session_id == 1 + assert bundle_name == "sample" + + assert report.metadata() == { + MetadataKey.SCHEMA_VERSION: SCHEMA_VERSION, + } + + bundle_reports = list(report.bundle_reports()) + assert len(bundle_reports) == 1 + + bundle_report = report.bundle_report("invalid") + assert bundle_report is None + bundle_report = report.bundle_report("sample") + + all_asset_reports = list(bundle_report.asset_reports()) + assert len(all_asset_reports) == 5 + + # Sort by size in descending + ordered_asset_reports = list( + (ar.name, ar.size) + for ar in bundle_report.asset_reports( + ordering_column="size", + ordering_desc=True, + ) + ) + assert ordered_asset_reports == [ + ("assets/index-*.js", 144577), + ("assets/react-*.svg", 4126), + ("assets/index-*.css", 1421), + ("assets/LazyComponent-*.js", 294), + ("assets/index-*.js", 154), + ] + # Sort by size in ascending + ordered_asset_reports = list( + (ar.name, ar.size) + for ar in bundle_report.asset_reports( + ordering_column="size", + ordering_desc=False, + ) + ) + assert ordered_asset_reports == [ + ("assets/index-*.js", 154), + ("assets/LazyComponent-*.js", 294), + ("assets/index-*.css", 1421), + ("assets/react-*.svg", 4126), + ("assets/index-*.js", 144577), + ] + # Sort by name in descending + ordered_asset_reports = list( + (ar.name, ar.size) + for ar in bundle_report.asset_reports( + ordering_column="name", + ordering_desc=True, + ) + ) + assert ordered_asset_reports == [ + ("assets/react-*.svg", 4126), + ("assets/index-*.css", 1421), + ("assets/index-*.js", 154), + ("assets/index-*.js", 144577), + ("assets/LazyComponent-*.js", 294), + ] + # Sort by name in ascending + ordered_asset_reports = list( + (ar.name, ar.size) + for ar in bundle_report.asset_reports( + ordering_column="name", + ordering_desc=False, + ) + ) + assert ordered_asset_reports == [ + ("assets/LazyComponent-*.js", 294), + ("assets/index-*.js", 144577), + ("assets/index-*.js", 154), + ("assets/index-*.css", 1421), + ("assets/react-*.svg", 4126), + ] + finally: + report.cleanup() + + +def test_bundle_report_asset_routes_supported_plugin(): + try: + report = BundleAnalysisReport() + report.ingest(sample_bundle_stats_path_6) + + bundle_report = report.bundle_report("sample") + all_asset_reports = list(bundle_report.asset_reports()) + + EXPECTED_ASSET_ROUTES_MAPPING = { + "_app/immutable/assets/0.CT0x_Q5c.css": [], + "_app/immutable/assets/2.Cs8KR-Bb.css": [], + "_app/immutable/assets/4.DOkkq0IA.css": [], + "_app/immutable/assets/5.CU6psp88.css": [], + "_app/immutable/assets/fira-mono-all-400-normal.B2mvLtSD.woff": [], + "_app/immutable/assets/fira-mono-cyrillic-400-normal.36-45Uyg.woff2": [], + "_app/immutable/assets/fira-mono-cyrillic-ext-400-normal.B04YIrm4.woff2": [], + "_app/immutable/assets/fira-mono-greek-400-normal.C3zng6O6.woff2": [], + "_app/immutable/assets/fira-mono-greek-ext-400-normal.CsqI23CO.woff2": [], + "_app/immutable/assets/fira-mono-latin-400-normal.DKjLVgQi.woff2": [], + "_app/immutable/assets/fira-mono-latin-ext-400-normal.D6XfiR-_.woff2": [], + "_app/immutable/assets/svelte-welcome.0pIiHnVF.webp": [], + "_app/immutable/assets/svelte-welcome.VNiyy3gC.png": [], + "_app/immutable/chunks/entry.BaWB2kHj.js": [], + "_app/immutable/chunks/index.DDRweiI9.js": [], + "_app/immutable/chunks/index.Ice1EKvx.js": [], + "_app/immutable/chunks/index.R8ovVqwX.js": [], + "_app/immutable/chunks/scheduler.Dk-snqIU.js": [], + "_app/immutable/chunks/stores.BrqGIpx3.js": [], + "_app/immutable/entry/app.Dd9ByE1Q.js": [], + "_app/immutable/entry/start.B1Q1eB84.js": [], + "_app/immutable/nodes/0.CL_S-12h.js": ["/"], + "_app/immutable/nodes/1.stWWSe4n.js": [], + "_app/immutable/nodes/2.BMQFqo-e.js": ["/"], + "_app/immutable/nodes/3.BqQOub2U.js": ["/about"], + "_app/immutable/nodes/4.CcjRtXvw.js": ["/sverdle"], + "_app/immutable/nodes/5.CwxmUzn6.js": ["/sverdle/how-to-play"], + "_app/version.json": [], + } + + asset_routes_mapping = {} + for asset_report in all_asset_reports: + asset_routes_mapping[asset_report.hashed_name] = asset_report.routes() + tc = TestCase() + tc.maxDiff = None + tc.assertDictEqual(EXPECTED_ASSET_ROUTES_MAPPING, asset_routes_mapping) + + finally: + report.cleanup() + + +def test_bundle_report_asset_routes_unsupported_plugin(): + try: + report = BundleAnalysisReport() + report.ingest(sample_bundle_stats_path) + + bundle_report = report.bundle_report("sample") + all_asset_reports = list(bundle_report.asset_reports()) + + EXPECTED_ASSET_ROUTES_MAPPING = { + "assets/LazyComponent-fcbb0922.js": None, + "assets/index-666d2e09.js": None, + "assets/index-c8676264.js": None, + "assets/index-d526a0c5.css": None, + "assets/react-35ef61ed.svg": None, + } + + asset_routes_mapping = {} + for asset_report in all_asset_reports: + asset_routes_mapping[asset_report.hashed_name] = asset_report.routes() + tc = TestCase() + tc.maxDiff = None + tc.assertDictEqual(EXPECTED_ASSET_ROUTES_MAPPING, asset_routes_mapping) + + finally: + report.cleanup() + + +def test_save_load_bundle_report(): + try: + created_report = BundleAnalysisReport() + created_report.ingest(sample_bundle_stats_path) + + loader = BundleAnalysisReportLoader( + storage_service=MemoryStorageService({}), + repo_key="testing", + ) + test_key = "8d1099f1-ba73-472f-957f-6908eced3f42" + loader.save(created_report, test_key) + + report = loader.load(test_key) + + initial_data = open(created_report.db_path, "rb").read() + loaded_data = open(report.db_path, "rb").read() + + assert created_report.db_path != report.db_path + assert len(initial_data) > 0 + assert initial_data == loaded_data + finally: + created_report.cleanup() + report.cleanup() + + +def test_reupload_bundle_report(): + try: + report = BundleAnalysisReport() + + # Upload the first stats file + report.ingest(sample_bundle_stats_path) + + assert report.metadata() == { + MetadataKey.SCHEMA_VERSION: SCHEMA_VERSION, + } + + bundle_reports = list(report.bundle_reports()) + assert len(bundle_reports) == 1 + + bundle_report = report.bundle_report("sample") + + assert bundle_report.total_size() == 150572 + assert report.session_count() == 1 + + # Re-upload another file of the same name, it should fully replace the previous + report.ingest(sample_bundle_stats_path_2) + + assert report.metadata() == { + MetadataKey.SCHEMA_VERSION: SCHEMA_VERSION, + } + + bundle_reports = list(report.bundle_reports()) + assert len(bundle_reports) == 1 + + bundle_report = report.bundle_report("sample") + + assert bundle_report.total_size() == 151672 + assert report.session_count() == 1 + finally: + report.cleanup() + + +def test_bundle_report_no_assets(): + try: + report_path = ( + Path(__file__).parent.parent.parent + / "samples" + / "sample_bundle_stats_no_assets.json" + ) + report = BundleAnalysisReport() + report.ingest(report_path) + bundle_report = report.bundle_report("b5") + asset_reports = list(bundle_report.asset_reports()) + + assert asset_reports == [] + assert bundle_report.total_size() == 0 + finally: + report.cleanup() + + +def test_bundle_report_no_chunks(): + try: + report_path = ( + Path(__file__).parent.parent.parent + / "samples" + / "sample_bundle_stats_no_chunks.json" + ) + report = BundleAnalysisReport() + report.ingest(report_path) + bundle_report = report.bundle_report("sample") + asset_reports = list(bundle_report.asset_reports()) + + assert len(asset_reports) == 2 + assert bundle_report.total_size() == 144731 + finally: + report.cleanup() + + +def test_bundle_report_no_modules(): + try: + report_path = ( + Path(__file__).parent.parent.parent + / "samples" + / "sample_bundle_stats_no_modules.json" + ) + report = BundleAnalysisReport() + report.ingest(report_path) + bundle_report = report.bundle_report("sample") + asset_reports = list(bundle_report.asset_reports()) + + assert len(asset_reports) == 2 + assert bundle_report.total_size() == 144731 + finally: + report.cleanup() + + +def test_bundle_report_info(): + try: + report = BundleAnalysisReport() + report.ingest(sample_bundle_stats_path) + bundle_report = report.bundle_report("sample") + bundle_report_info = bundle_report.info() + + assert bundle_report_info["version"] == "2" + assert bundle_report_info["bundler_name"] == "rollup" + assert bundle_report_info["bundler_version"] == "3.29.4" + assert bundle_report_info["built_at"] == 1701451048604 + assert ( + bundle_report_info["plugin_name"] == "codecov-vite-bundle-analysis-plugin" + ) + assert bundle_report_info["plugin_version"] == "1.0.0" + assert bundle_report_info["duration"] == 331 + finally: + report.cleanup() + + +def test_bundle_report_size_integer(): + try: + report_path = ( + Path(__file__).parent.parent.parent + / "samples" + / "sample_bundle_stats_decimal_size.json" + ) + report = BundleAnalysisReport() + report.ingest(report_path) + bundle_report = report.bundle_report("sample") + + assert bundle_report.total_size() == 150572 + finally: + report.cleanup() + + +def test_bundle_parser_error(): + with patch( + "shared.bundle_analysis.parsers.ParserV1._parse_assets_event", + side_effect=Exception("MockError"), + ): + try: + report = BundleAnalysisReport() + with pytest.raises(Exception) as excinfo: + report.ingest(sample_bundle_stats_path) + assert ( + excinfo.bundle_analysis_plugin_name + == "codecov-vite-bundle-analysis-plugin" + ) + finally: + report.cleanup() + + +def test_bundle_name_not_valid(): + try: + report = BundleAnalysisReport() + with pytest.raises(Exception) as excinfo: + report.ingest(sample_bundle_stats_path_3) + assert ( + excinfo.bundle_analysis_plugin_name + == "codecov-vite-bundle-analysis-plugin" + ) + finally: + report.cleanup() + + +def test_bundle_file_save_rate_limit_error(): + with patch( + "shared.storage.memory.MemoryStorageService.write_file", + side_effect=Exception("TooManyRequests"), + ): + with pytest.raises(Exception) as excinfo: + try: + report = BundleAnalysisReport() + report.ingest(sample_bundle_stats_path) + + loader = BundleAnalysisReportLoader( + storage_service=MemoryStorageService({}), + repo_key="testing", + ) + test_key = "8d1099f1-ba73-472f-957f-6908eced3f42" + loader.save(report, test_key) + + assert str(excinfo) == "TooManyRequests" + assert isinstance(excinfo, PutRequestRateLimitError) + finally: + report.cleanup() + + +def test_bundle_file_save_unknown_error(): + with patch( + "shared.storage.memory.MemoryStorageService.write_file", + side_effect=Exception("UnknownError"), + ): + with pytest.raises(Exception) as excinfo: + try: + report = BundleAnalysisReport() + report.ingest(sample_bundle_stats_path) + + loader = BundleAnalysisReportLoader( + storage_service=MemoryStorageService({}), + repo_key="testing", + ) + test_key = "8d1099f1-ba73-472f-957f-6908eced3f42" + loader.save(report, test_key) + + assert str(excinfo) == "UnknownError" + assert type(excinfo) is Exception + finally: + report.cleanup() + + +def test_create_bundle_report_v1(): + try: + report = BundleAnalysisReport() + session_id, bundle_name = report.ingest(sample_bundle_stats_path_4) + assert session_id == 1 + assert bundle_name == "sample" + + assert report.metadata() == { + MetadataKey.SCHEMA_VERSION: SCHEMA_VERSION, + } + + bundle_reports = list(report.bundle_reports()) + assert len(bundle_reports) == 1 + + bundle_report = report.bundle_report("invalid") + assert bundle_report is None + bundle_report = report.bundle_report("sample") + + bundle_report_info = bundle_report.info() + assert bundle_report_info["version"] == "1" + + asset_reports = list(bundle_report.asset_reports()) + + assert [ + ( + ar.name, + ar.hashed_name, + ar.size, + ar.gzip_size, + len(ar.modules()), + ar.asset_type, + ) + for ar in asset_reports + ] == [ + # FIXME: this is wrong since it's capturing the SVG and CSS modules as well. + # Made a similar note in the parser code where the associations are made + ( + "assets/index-*.js", + "assets/index-666d2e09.js", + 144577, + 144, + 28, + AssetType.JAVASCRIPT, + ), + ( + "assets/react-*.svg", + "assets/react-35ef61ed.svg", + 4126, + 4, + 0, + AssetType.IMAGE, + ), + ( + "assets/index-*.css", + "assets/index-d526a0c5.css", + 1421, + 1, + 0, + AssetType.STYLESHEET, + ), + ( + "assets/LazyComponent-*.js", + "assets/LazyComponent-fcbb0922.js", + 294, + 0, + 1, + AssetType.JAVASCRIPT, + ), + ( + "assets/index-*.js", + "assets/index-c8676264.js", + 154, + 0, + 2, + AssetType.JAVASCRIPT, + ), + ] + + for ar in asset_reports: + for module in ar.modules(): + assert isinstance(module.name, str) + assert isinstance(module.size, int) + + assert bundle_report.total_size() == 150572 + assert report.session_count() == 1 + finally: + report.cleanup() + + +def test_bundle_is_cached(): + try: + bundle_analysis_report = BundleAnalysisReport() + session_id, bundle_name = bundle_analysis_report.ingest( + sample_bundle_stats_path + ) + assert session_id == 1 + assert bundle_name == "sample" + + session_id, bundle_name = bundle_analysis_report.ingest( + sample_bundle_stats_path_5 + ) + assert session_id == 2 + assert bundle_name == "sample2" + + assert bundle_analysis_report.metadata() == { + MetadataKey.SCHEMA_VERSION: SCHEMA_VERSION, + } + + # When doing ingest (ie when handling a upload), its never cached + bundle_reports = list(bundle_analysis_report.bundle_reports()) + assert len(bundle_reports) == 2 + for bundle in bundle_reports: + assert bundle.is_cached() == False + assert bundle_analysis_report.is_cached() == False + + # Test setting 'sample' bundle to True + bundle_analysis_report.update_is_cached(data={"sample": True}) + assert bundle_analysis_report.bundle_report("sample").is_cached() == True + assert bundle_analysis_report.bundle_report("sample2").is_cached() == False + assert bundle_analysis_report.is_cached() == True + + # Test setting 'sample2' bundle to True and 'sample' back to False + bundle_analysis_report.update_is_cached(data={"sample2": True, "sample": False}) + assert bundle_analysis_report.bundle_report("sample").is_cached() == False + assert bundle_analysis_report.bundle_report("sample2").is_cached() == True + assert bundle_analysis_report.is_cached() == True + + finally: + bundle_analysis_report.cleanup() + + +def test_bundle_deletion(): + try: + bundle_analysis_report = BundleAnalysisReport() + with get_db_session(bundle_analysis_report.db_path) as db_session: + session_id, bundle_name = bundle_analysis_report.ingest( + sample_bundle_stats_path + ) + assert session_id == 1 + assert bundle_name == "sample" + + session_id, bundle_name = bundle_analysis_report.ingest( + sample_bundle_stats_path_5 + ) + assert session_id == 2 + assert bundle_name == "sample2" + + assert _table_rows_count(db_session) == (2, 2, 10, 6, 62) + + # Delete non-existent bundle + bundle_analysis_report.delete_bundle_by_name("fake") + assert _table_rows_count(db_session) == (2, 2, 10, 6, 62) + + # Delete bundle 'sample' + bundle_analysis_report.delete_bundle_by_name("sample") + assert _table_rows_count(db_session) == (1, 1, 5, 3, 31) + res = list(db_session.query(Bundle).all()) + assert len(res) == 1 + assert res[0].name == "sample2" + + # Delete bundle 'sample' again + bundle_analysis_report.delete_bundle_by_name("sample") + assert _table_rows_count(db_session) == (1, 1, 5, 3, 31) + res = list(db_session.query(Bundle).all()) + assert len(res) == 1 + assert res[0].name == "sample2" + + # Delete bundle 'sample2' + bundle_analysis_report.delete_bundle_by_name("sample2") + assert _table_rows_count(db_session) == (0, 0, 0, 0, 0) + res = list(db_session.query(Bundle).all()) + assert len(res) == 0 + finally: + bundle_analysis_report.cleanup() + + +def test_create_bundle_report_without_and_with_compare_sha(): + try: + report = BundleAnalysisReport() + with get_db_session(report.db_path) as db_session: + session_id, bundle_name = report.ingest(sample_bundle_stats_path) + assert session_id == 1 + assert bundle_name == "sample" + + res = db_session.query(Metadata).filter_by(key="compare_sha").all() + assert len(res) == 0 + + session_id, bundle_name = report.ingest( + sample_bundle_stats_path, "compare_sha_123" + ) + assert session_id == 2 + assert bundle_name == "sample" + + res = db_session.query(Metadata).filter_by(key="compare_sha").all() + assert len(res) == 1 + assert res[0].value == "compare_sha_123" + finally: + report.cleanup() + + +def test_bundle_report_asset_type_javascript(): + test_cases = [ + { + "version": "v1", + "path": "sample_bundle_stats_asset_type_javascript_v1.json", + "expected_total_size": 90, + "expected_js_size": 60, + }, + { + "version": "v2", + "path": "sample_bundle_stats_asset_type_javascript_v2.json", + "expected_total_size": 90, + "expected_js_size": 60, + }, + ] + + def load_and_test_report( + version, report_path, expected_total_size, expected_js_size + ): + report = BundleAnalysisReport() + try: + report.ingest(report_path) + bundle_report = report.bundle_report("sample") + asset_reports = list(bundle_report.asset_reports()) + assert bundle_report.total_size() == expected_total_size, ( + f"Version {version}: Total size mismatch" + ) + + total_js_size = sum( + asset.size + for asset in asset_reports + if asset.asset_type == AssetType.JAVASCRIPT + ) + assert total_js_size == expected_js_size, ( + f"Version {version}: JS size mismatch" + ) + finally: + report.cleanup() + + for case in test_cases: + report_path = Path(__file__).parent.parent.parent / "samples" / case["path"] + load_and_test_report( + case["version"], + report_path, + case["expected_total_size"], + case["expected_js_size"], + ) + + +def test_bundle_report_total_gzip_size(): + try: + report = BundleAnalysisReport() + session_id, bundle_name = report.ingest(sample_bundle_stats_path) + assert session_id == 1 + assert bundle_name == "sample" + + assert report.metadata() == { + MetadataKey.SCHEMA_VERSION: SCHEMA_VERSION, + } + + bundle_reports = list(report.bundle_reports()) + assert len(bundle_reports) == 1 + + bundle_report = report.bundle_report("invalid") + assert bundle_report is None + bundle_report = report.bundle_report("sample") + + assert bundle_report.total_size() == 150572 + assert bundle_report.total_gzip_size() == 150567 + finally: + report.cleanup() + + +def test_bundle_report_dynamic_imports_object_model(): + try: + report = BundleAnalysisReport() + with get_db_session(report.db_path) as db_session: + # Check that the DB is set up correctly + # 1 chunk has 2, another chunk has 1 + report.ingest(sample_bundle_stats_path_7) + + query = ( + select(Chunk.unique_external_id, Asset.name) + .join(DynamicImport, Chunk.id == DynamicImport.chunk_id) + .join(Asset, Asset.id == DynamicImport.asset_id) + .order_by(Chunk.unique_external_id) + ) + + results = db_session.execute(query).all() + + result_dicts = [ + {"unique_external_id": unique_external_id, "asset_name": asset_name} + for unique_external_id, asset_name in results + ] + + assert result_dicts == [ + { + "asset_name": "index-C-Z8zsvD.js", + "unique_external_id": "1-LazyComponent", + }, + { + "asset_name": "index-C-Z8zsvD.js", + "unique_external_id": "2-index", + }, + { + "asset_name": "LazyComponent-BBSC53Nv.js", + "unique_external_id": "2-index", + }, + ] + + # Check that the DB is set up correctly after a rewrite of a new file + # 1 chunk has 1, another chunk has 1, another chunk has 1 + report.ingest(sample_bundle_stats_path_8) + + query = ( + select(Chunk.unique_external_id, Asset.name) + .join(DynamicImport, Chunk.id == DynamicImport.chunk_id) + .join(Asset, Asset.id == DynamicImport.asset_id) + .order_by(Chunk.unique_external_id) + ) + + results = db_session.execute(query).all() + + result_dicts = [ + {"unique_external_id": unique_external_id, "asset_name": asset_name} + for unique_external_id, asset_name in results + ] + + assert result_dicts == [ + { + "asset_name": "LazyComponent-BBSC53Nv.js", + "unique_external_id": "0-index", + }, + { + "asset_name": "index-C-Z8zsvD.js", + "unique_external_id": "1-LazyComponent", + }, + { + "asset_name": "index-C-Z8zsvD.js", + "unique_external_id": "2-index", + }, + ] + + finally: + report.cleanup() + + +def test_bundle_report_dynamic_imports_fetching(): + try: + report = BundleAnalysisReport() + report.ingest(sample_bundle_stats_path_7) + bundle_report = report.bundle_report("dynamic_imports") + + # There should only be 3 dynamic imports total + dynamic_imports = [] + for asset in list(bundle_report.asset_reports()): + dynamic_imports.extend(asset.dynamically_imported_assets()) + assert len(dynamic_imports) == 3 + + # 1 of them should be from the asset called: LazyComponent-BBSC53Nv.js + asset = bundle_report.asset_report_by_name("LazyComponent-BBSC53Nv.js") + imports = [item for item in asset.dynamically_imported_assets()] + assert len(imports) == 1 + assert imports[0].hashed_name == "index-C-Z8zsvD.js" + + # 2 of them should be from the asset called: assets/index-oTNkmlIs.js + asset = bundle_report.asset_report_by_name("assets/index-oTNkmlIs.js") + imports = [item.hashed_name for item in asset.dynamically_imported_assets()] + assert len(imports) == 2 + assert "index-C-Z8zsvD.js" in imports + assert "LazyComponent-BBSC53Nv.js" in imports + finally: + report.cleanup() + + +def test_bundle_report_dynamic_imports_with_missing_assets(): + with patch("shared.bundle_analysis.parsers.v3.log.warn") as mock_warn: + try: + report = BundleAnalysisReport() + report.ingest(sample_bundle_stats_path_11) + bundle_report = report.bundle_report("dynamic_imports") + + # There should only be 3 dynamic imports total + dynamic_imports = [] + for asset in list(bundle_report.asset_reports()): + dynamic_imports.extend(asset.dynamically_imported_assets()) + assert len(dynamic_imports) == 3 + + # 1 of them should be from the asset called: LazyComponent-BBSC53Nv.js + asset = bundle_report.asset_report_by_name("LazyComponent-BBSC53Nv.js") + imports = [item for item in asset.dynamically_imported_assets()] + assert len(imports) == 1 + assert imports[0].hashed_name == "index-C-Z8zsvD.js" + + # 2 of them should be from the asset called: assets/index-oTNkmlIs.js + asset = bundle_report.asset_report_by_name("assets/index-oTNkmlIs.js") + imports = [item.hashed_name for item in asset.dynamically_imported_assets()] + assert len(imports) == 2 + assert "index-C-Z8zsvD.js" in imports + assert "LazyComponent-BBSC53Nv.js" in imports + + finally: + report.cleanup() + + # Check if the warning log for missing assets was triggered + mock_warn.assert_called_with( + 'Asset not found for dynamic import: "this-is-a-picture-that-does-not-exist-in-assets.svg". Skipping...', + ) + + +def test_bundle_report_dynamic_imports_with_multiple_assets(): + with patch("shared.bundle_analysis.parsers.v3.log.error") as mock_error: + try: + report = BundleAnalysisReport() + report.ingest(sample_bundle_stats_path_12) + except Exception: + pass + finally: + report.cleanup() + + # Check if the error log for multiple assets found was triggered + mock_error.assert_called_with( + 'Multiple assets found for dynamic import: "there-is-two-of-these-assets.js"', + exc_info=True, + ) + + +def test_bundle_report_routes(): + try: + report = BundleAnalysisReport() + report.ingest(sample_bundle_stats_path_6) + bundle_report = report.bundle_report("sample") + route_map = bundle_report.routes() + + # Total of 4 routes in this bundle + assert len(route_map) == 4 + + # "/" route has two Assets + assets = sorted([item.hashed_name for item in route_map["/"]]) + assert assets == [ + "_app/immutable/nodes/0.CL_S-12h.js", + "_app/immutable/nodes/2.BMQFqo-e.js", + ] + + # "/about" has one asset + assets = sorted([item.hashed_name for item in route_map["/about"]]) + assert assets == ["_app/immutable/nodes/3.BqQOub2U.js"] + + # "/sverdle" has one asset + assets = sorted([item.hashed_name for item in route_map["/sverdle"]]) + assert assets == ["_app/immutable/nodes/4.CcjRtXvw.js"] + + # "/sverdle/how-to-play" has one asset + assets = sorted( + [item.hashed_name for item in route_map["/sverdle/how-to-play"]] + ) + assert assets == ["_app/immutable/nodes/5.CwxmUzn6.js"] + finally: + report.cleanup() + + +def test_bundle_report_route_report_with_dynamic_imports(): + try: + report = BundleAnalysisReport() + report.ingest(sample_bundle_stats_path_9) + bundle_report = report.bundle_report("dynamic_imports") + + route_report = bundle_report.full_route_report() + + assert route_report.get_sizes() == { + "/sverdle/about": 2111, + "/sverdle/careers": 2100, + "/sverdle/faq": 2110, + "/sverdle/users": 2111, + } + assert route_report.get_size("/sverdle/fake") is None + assert route_report.get_size("/sverdle/about") == 2111 + assert route_report.get_size("/sverdle/careers") == 2100 + assert route_report.get_size("/sverdle/faq") == 2110 + assert route_report.get_size("/sverdle/users") == 2111 + finally: + report.cleanup() + + +@pytest.mark.parametrize("version", ["1", "2", "3"]) +def test_bundle_report_cleans_bad_chunks(version): + try: + report = BundleAnalysisReport() + + # Read and modify the JSON content + with open(sample_bundle_stats_path_10, "r") as f: + content = f.read().replace("__VERSION__", version) + + # Create a temporary file with the modified content + import tempfile + + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) as temp: + temp.write(content) + temp_path = Path(temp.name) + + report.ingest(temp_path) + bundle_report = report.bundle_report("sample") + + assets = sorted([item.hashed_name for item in bundle_report.asset_reports()]) + assert assets == [ + "assets/LazyComponent-fcbb0922.js", + ] + temp_path.unlink() + finally: + report.cleanup() diff --git a/libs/shared/tests/unit/bundle_analysis/test_bundle_comparison.py b/libs/shared/tests/unit/bundle_analysis/test_bundle_comparison.py new file mode 100644 index 0000000000..a0eb42c9d2 --- /dev/null +++ b/libs/shared/tests/unit/bundle_analysis/test_bundle_comparison.py @@ -0,0 +1,1002 @@ +from pathlib import Path + +import pytest + +from shared.bundle_analysis import ( + AssetChange, + BundleAnalysisComparison, + BundleAnalysisReport, + BundleAnalysisReportLoader, + BundleChange, + MissingBaseReportError, + MissingBundleError, + MissingHeadReportError, + RouteChange, +) +from shared.bundle_analysis.models import Bundle, get_db_session +from shared.storage.memory import MemoryStorageService + +here = Path(__file__) +base_report_bundle_stats_path = ( + here.parent.parent.parent / "samples" / "sample_bundle_stats.json" +) +head_report_bundle_stats_path = ( + here.parent.parent.parent / "samples" / "sample_bundle_stats_other.json" +) +head_report_bundle_stats_path_route_base_1 = ( + here.parent.parent.parent + / "samples" + / "sample_bundle_stats_v3_comparison_base_1.json" +) +head_report_bundle_stats_path_route_base_2 = ( + here.parent.parent.parent + / "samples" + / "sample_bundle_stats_v3_comparison_base_2.json" +) +head_report_bundle_stats_path_route_head_1 = ( + here.parent.parent.parent + / "samples" + / "sample_bundle_stats_v3_comparison_head_1.json" +) +head_report_bundle_stats_path_route_head_2 = ( + here.parent.parent.parent + / "samples" + / "sample_bundle_stats_v3_comparison_head_2.json" +) +base_report_bundle_stats_zero_size_asset = ( + here.parent.parent.parent / "samples" / "sample_bundle_stats_zero_size_asset.json" +) + + +def test_bundle_analysis_comparison(): + loader = BundleAnalysisReportLoader( + storage_service=MemoryStorageService({}), + repo_key="testing", + ) + + comparison = BundleAnalysisComparison( + loader=loader, + base_report_key="base-report", + head_report_key="head-report", + ) + + # raises errors when either report doesn't exist in storage + with pytest.raises(MissingBaseReportError): + comparison.base_report + with pytest.raises(MissingHeadReportError): + comparison.head_report + + try: + base_report = BundleAnalysisReport() + base_report.ingest(base_report_bundle_stats_path) + + old_bundle = Bundle(name="old") + with get_db_session(base_report.db_path) as db_session: + db_session.add(old_bundle) + db_session.commit() + + head_report = BundleAnalysisReport() + head_report.ingest(head_report_bundle_stats_path) + + new_bundle = Bundle(name="new") + with get_db_session(head_report.db_path) as db_session: + db_session.add(new_bundle) + db_session.commit() + + loader.save(base_report, "base-report") + loader.save(head_report, "head-report") + finally: + base_report.cleanup() + head_report.cleanup() + + bundle_changes = comparison.bundle_changes() + assert set(bundle_changes) == set( + [ + BundleChange( + bundle_name="sample", + change_type=BundleChange.ChangeType.CHANGED, + size_delta=1100, + percentage_delta=0.73, + ), + BundleChange( + bundle_name="new", + change_type=BundleChange.ChangeType.ADDED, + size_delta=0, + percentage_delta=100, + ), + BundleChange( + bundle_name="old", + change_type=BundleChange.ChangeType.REMOVED, + size_delta=0, + percentage_delta=-100, + ), + ] + ) + + bundle_comparison = comparison.bundle_comparison("sample") + total_size_delta = bundle_comparison.total_size_delta() + assert total_size_delta == 1100 + assert comparison.percentage_delta == 0.73 + + with pytest.raises(MissingBundleError): + comparison.bundle_comparison("new") + + +def test_bundle_asset_comparison_using_closest_size_delta(): + loader = BundleAnalysisReportLoader( + storage_service=MemoryStorageService({}), + repo_key="testing", + ) + + comparison = BundleAnalysisComparison( + loader=loader, + base_report_key="base-report", + head_report_key="head-report", + ) + + # raises errors when either report doesn't exist in storage + with pytest.raises(MissingBaseReportError): + comparison.base_report + with pytest.raises(MissingHeadReportError): + comparison.head_report + + try: + base_report = BundleAnalysisReport() + base_report.ingest(base_report_bundle_stats_path) + + old_bundle = Bundle(name="old") + with get_db_session(base_report.db_path) as db_session: + db_session.add(old_bundle) + db_session.commit() + + head_report = BundleAnalysisReport() + head_report.ingest(head_report_bundle_stats_path) + + new_bundle = Bundle(name="new") + with get_db_session(head_report.db_path) as db_session: + db_session.add(new_bundle) + db_session.commit() + + loader.save(base_report, "base-report") + loader.save(head_report, "head-report") + finally: + base_report.cleanup() + head_report.cleanup() + + bundle_changes = comparison.bundle_changes() + assert set(bundle_changes) == set( + [ + BundleChange( + bundle_name="sample", + change_type=BundleChange.ChangeType.CHANGED, + size_delta=1100, + percentage_delta=0.73, + ), + BundleChange( + bundle_name="new", + change_type=BundleChange.ChangeType.ADDED, + size_delta=0, + percentage_delta=100, + ), + BundleChange( + bundle_name="old", + change_type=BundleChange.ChangeType.REMOVED, + size_delta=0, + percentage_delta=-100, + ), + ] + ) + + bundle_comparison = comparison.bundle_comparison("sample") + asset_comparisons = bundle_comparison.asset_comparisons() + assert len(asset_comparisons) == 6 + + asset_comparison_d = {} + for asset_comparison in asset_comparisons: + key = ( + asset_comparison.base_asset_report.hashed_name + if asset_comparison.base_asset_report + else None, + asset_comparison.head_asset_report.hashed_name + if asset_comparison.head_asset_report + else None, + ) + assert key not in asset_comparison_d + asset_comparison_d[key] = asset_comparison + + # Check asset change is correct + assert asset_comparison_d[ + ("assets/index-666d2e09.js", "assets/index-666d2e09.js") + ].asset_change() == AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_delta=0, + asset_name="assets/index-*.js", + percentage_delta=0, + size_base=144577, + size_head=144577, + ) + assert asset_comparison_d[ + ("assets/index-c8676264.js", "assets/index-c8676264.js") + ].asset_change() == AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_delta=100, + asset_name="assets/index-*.js", + percentage_delta=64.94, + size_base=154, + size_head=254, + ) + assert asset_comparison_d[ + (None, "assets/other-35ef61ed.svg") + ].asset_change() == AssetChange( + change_type=AssetChange.ChangeType.ADDED, + size_delta=5126, + asset_name="assets/other-*.svg", + percentage_delta=100, + size_base=0, + size_head=5126, + ) + assert asset_comparison_d[ + ("assets/index-d526a0c5.css", "assets/index-d526a0c5.css") + ].asset_change() == AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_delta=0, + asset_name="assets/index-*.css", + percentage_delta=0, + size_base=1421, + size_head=1421, + ) + assert asset_comparison_d[ + ("assets/LazyComponent-fcbb0922.js", "assets/LazyComponent-fcbb0922.js") + ].asset_change() == AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_delta=0, + asset_name="assets/LazyComponent-*.js", + percentage_delta=0, + size_base=294, + size_head=294, + ) + assert asset_comparison_d[ + ("assets/react-35ef61ed.svg", None) + ].asset_change() == AssetChange( + change_type=AssetChange.ChangeType.REMOVED, + size_delta=-4126, + asset_name="assets/react-*.svg", + percentage_delta=-100, + size_base=4126, + size_head=0, + ) + + # Check asset contributing modules is correct + module_reports = asset_comparison_d[ + ("assets/index-666d2e09.js", "assets/index-666d2e09.js") + ].contributing_modules() + assert [module.name for module in module_reports] == [ + "./vite/modulepreload-polyfill", + "./commonjsHelpers.js", + "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js?commonjs-module", + "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js?commonjs-exports", + "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js?commonjs-module", + "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js?commonjs-exports", + "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react.production.min.js", + "../../node_modules/.pnpm/react@18.2.0/node_modules/react/index.js", + "../../node_modules/.pnpm/react@18.2.0/node_modules/react/cjs/react-jsx-runtime.production.min.js", + "../../node_modules/.pnpm/react@18.2.0/node_modules/react/jsx-runtime.js", + "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js?commonjs-exports", + "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js?commonjs-module", + "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js?commonjs-exports", + "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js?commonjs-module", + "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js?commonjs-exports", + "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/cjs/scheduler.production.min.js", + "../../node_modules/.pnpm/scheduler@0.23.0/node_modules/scheduler/index.js", + "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/cjs/react-dom.production.min.js", + "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/index.js", + "../../node_modules/.pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom/client.js", + "./vite/preload-helper", + "./src/assets/react.svg", + "../../../../../../vite.svg", + "./src/App.css", + "./src/App.tsx", + "./src/index.css", + "./src/main.tsx", + "./index.html", + ] + module_reports = asset_comparison_d[ + ("assets/index-c8676264.js", "assets/index-c8676264.js") + ].contributing_modules() + assert [module.name for module in module_reports] == [ + "./src/IndexedLazyComponent/IndexedLazyComponent.tsx", + "./src/IndexedLazyComponent/index.ts", + "./src/Other.tsx", + ] + module_reports = asset_comparison_d[ + (None, "assets/other-35ef61ed.svg") + ].contributing_modules() + assert [module.name for module in module_reports] == [] + module_reports = asset_comparison_d[ + ("assets/index-d526a0c5.css", "assets/index-d526a0c5.css") + ].contributing_modules() + assert [module.name for module in module_reports] == [] + module_reports = asset_comparison_d[ + ("assets/LazyComponent-fcbb0922.js", "assets/LazyComponent-fcbb0922.js") + ].contributing_modules() + assert [module.name for module in module_reports] == [ + "./src/LazyComponent/LazyComponent.tsx", + ] + module_reports = asset_comparison_d[ + ("assets/react-35ef61ed.svg", None) + ].contributing_modules() + assert [module.name for module in module_reports] == [] + + # Check no contributing modules filter + module_reports = asset_comparison_d[ + ("assets/index-666d2e09.js", "assets/index-666d2e09.js") + ].contributing_modules() + assert len(module_reports) == 28 + + # Check no PR changed files + module_reports = asset_comparison_d[ + ("assets/index-666d2e09.js", "assets/index-666d2e09.js") + ].contributing_modules([]) + assert len(module_reports) == 0 + + # Check with proper filtered files + module_reports = asset_comparison_d[ + ("assets/index-666d2e09.js", "assets/index-666d2e09.js") + ].contributing_modules( + [ + "app1/index.html", + "app2/index.html", # <- don't match because dupe + "./app1/src/main.tsx", + "/example/svelte/app1/src/App.css", + "abc/def/ghi.ts", # <- don't match + ] + ) + assert set([module.name for module in module_reports]) == set( + ["./index.html", "./src/App.css", "./src/main.tsx"] + ) + + +def test_bundle_asset_comparison_using_uuid(): + """ + In the default setup we have: + (base:index-666d2e09.js, head:index-666d2e09.js): 144577 -> 144577 + (base:index-c8676264.js, head:index-c8676264.js): 154 -> 254 + this matches based on closes size delta, now we will update to the following UUIDs + base:index-666d2e09.js -> UUID=123 + base:index-c8676264.js -> UUID=456 + head:index-666d2e09.js -> UUID=456 + head:index-c8676264.js -> UUID=123 + this will yield the following comparisons + (base:index-666d2e09.js, head:index-c8676264.js): 144577 -> 254 + (base:index-c8676264.js, head:index-666d2e09.js): 154 -> 144577 + """ + loader = BundleAnalysisReportLoader( + storage_service=MemoryStorageService({}), + repo_key="testing", + ) + + comparison = BundleAnalysisComparison( + loader=loader, + base_report_key="base-report", + head_report_key="head-report", + ) + + # raises errors when either report doesn't exist in storage + with pytest.raises(MissingBaseReportError): + comparison.base_report + with pytest.raises(MissingHeadReportError): + comparison.head_report + + try: + base_report = BundleAnalysisReport() + base_report.ingest(base_report_bundle_stats_path) + + old_bundle = Bundle(name="old") + with get_db_session(base_report.db_path) as db_session: + db_session.add(old_bundle) + db_session.commit() + + head_report = BundleAnalysisReport() + head_report.ingest(head_report_bundle_stats_path) + + new_bundle = Bundle(name="new") + with get_db_session(head_report.db_path) as db_session: + db_session.add(new_bundle) + db_session.commit() + + loader.save(base_report, "base-report") + loader.save(head_report, "head-report") + finally: + base_report.cleanup() + head_report.cleanup() + + # Update the UUIDs + with get_db_session(comparison.base_report.db_path) as db_session: + from shared.bundle_analysis.models import Asset + + db_session.query(Asset).filter(Asset.name == "assets/index-666d2e09.js").update( + {Asset.uuid: "123"}, synchronize_session="fetch" + ) + db_session.query(Asset).filter(Asset.name == "assets/index-c8676264.js").update( + {Asset.uuid: "456"}, synchronize_session="fetch" + ) + db_session.commit() + + with get_db_session(comparison.head_report.db_path) as db_session: + from shared.bundle_analysis.models import Asset + + db_session.query(Asset).filter(Asset.name == "assets/index-666d2e09.js").update( + {Asset.uuid: "456"}, synchronize_session="fetch" + ) + db_session.query(Asset).filter(Asset.name == "assets/index-c8676264.js").update( + {Asset.uuid: "123"}, synchronize_session="fetch" + ) + db_session.commit() + + bundle_changes = comparison.bundle_changes() + assert set(bundle_changes) == set( + [ + BundleChange( + bundle_name="sample", + change_type=BundleChange.ChangeType.CHANGED, + size_delta=1100, + percentage_delta=0.73, + ), + BundleChange( + bundle_name="new", + change_type=BundleChange.ChangeType.ADDED, + size_delta=0, + percentage_delta=100, + ), + BundleChange( + bundle_name="old", + change_type=BundleChange.ChangeType.REMOVED, + size_delta=0, + percentage_delta=-100, + ), + ] + ) + + bundle_comparison = comparison.bundle_comparison("sample") + asset_comparisons = bundle_comparison.asset_comparisons() + assert len(asset_comparisons) == 6 + + asset_comparison_d = {} + for asset_comparison in asset_comparisons: + key = ( + asset_comparison.base_asset_report.hashed_name + if asset_comparison.base_asset_report + else None, + asset_comparison.head_asset_report.hashed_name + if asset_comparison.head_asset_report + else None, + ) + assert key not in asset_comparison_d + asset_comparison_d[key] = asset_comparison + + # Check asset change is correct + assert asset_comparison_d[ + ("assets/index-666d2e09.js", "assets/index-c8676264.js") + ].asset_change() == AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_delta=-144323, + asset_name="assets/index-*.js", + percentage_delta=-99.82, + size_base=144577, + size_head=254, + ) + assert asset_comparison_d[ + ("assets/index-c8676264.js", "assets/index-666d2e09.js") + ].asset_change() == AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_delta=144423, + asset_name="assets/index-*.js", + percentage_delta=93781.17, + size_base=154, + size_head=144577, + ) + assert asset_comparison_d[ + (None, "assets/other-35ef61ed.svg") + ].asset_change() == AssetChange( + change_type=AssetChange.ChangeType.ADDED, + size_delta=5126, + asset_name="assets/other-*.svg", + percentage_delta=100, + size_base=0, + size_head=5126, + ) + assert asset_comparison_d[ + ("assets/index-d526a0c5.css", "assets/index-d526a0c5.css") + ].asset_change() == AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_delta=0, + asset_name="assets/index-*.css", + percentage_delta=0, + size_base=1421, + size_head=1421, + ) + assert asset_comparison_d[ + ("assets/LazyComponent-fcbb0922.js", "assets/LazyComponent-fcbb0922.js") + ].asset_change() == AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_delta=0, + asset_name="assets/LazyComponent-*.js", + percentage_delta=0, + size_base=294, + size_head=294, + ) + assert asset_comparison_d[ + ("assets/react-35ef61ed.svg", None) + ].asset_change() == AssetChange( + change_type=AssetChange.ChangeType.REMOVED, + size_delta=-4126, + asset_name="assets/react-*.svg", + percentage_delta=-100, + size_base=4126, + size_head=0, + ) + + +def test_bundle_analysis_total_size_delta(): + try: + loader = BundleAnalysisReportLoader( + storage_service=MemoryStorageService({}), + repo_key="testing", + ) + + comparison = BundleAnalysisComparison( + loader=loader, + base_report_key="base-report", + head_report_key="head-report", + ) + + base_report = BundleAnalysisReport() + base_report.ingest(base_report_bundle_stats_path) + + old_bundle = Bundle(name="old") + with get_db_session(base_report.db_path) as db_session: + db_session.add(old_bundle) + db_session.commit() + + head_report = BundleAnalysisReport() + head_report.ingest(head_report_bundle_stats_path) + + new_bundle = Bundle(name="new") + with get_db_session(head_report.db_path) as db_session: + db_session.add(new_bundle) + db_session.commit() + + loader.save(base_report, "base-report") + loader.save(head_report, "head-report") + + assert comparison.total_size_delta == 1100 + assert comparison.percentage_delta == 0.73 + + finally: + base_report.cleanup() + head_report.cleanup() + + +def test_bundle_analysis_route_comparison_by_bundle_name(): + loader = BundleAnalysisReportLoader( + storage_service=MemoryStorageService({}), + repo_key="testing", + ) + + comparison = BundleAnalysisComparison( + loader=loader, + base_report_key="base-report", + head_report_key="head-report", + ) + + # raises errors when either report doesn't exist in storage + with pytest.raises(MissingBaseReportError): + comparison.base_report + with pytest.raises(MissingHeadReportError): + comparison.head_report + + try: + base_report = BundleAnalysisReport() + base_report.ingest(head_report_bundle_stats_path_route_base_1) + base_report.ingest(head_report_bundle_stats_path_route_base_2) + + head_report = BundleAnalysisReport() + head_report.ingest(head_report_bundle_stats_path_route_head_1) + head_report.ingest(head_report_bundle_stats_path_route_head_2) + + loader.save(base_report, "base-report") + loader.save(head_report, "head-report") + finally: + base_report.cleanup() + head_report.cleanup() + + route_changes = comparison.bundle_routes_changes_by_bundle("bundle1") + sorted_route_changes = sorted(route_changes, key=lambda x: x.route_name) + expected_changes = [ + RouteChange( + route_name="/sverdle/about", + change_type=AssetChange.ChangeType.CHANGED, + size_delta=900, + percentage_delta=810.81, + size_base=111, + size_head=1011, + ), + RouteChange( + route_name="/sverdle/faq", + change_type=AssetChange.ChangeType.REMOVED, + size_delta=-110, + percentage_delta=-100.0, + size_base=110, + size_head=0, + ), + RouteChange( + route_name="/sverdle/faq-prime", + change_type=AssetChange.ChangeType.ADDED, + size_delta=1010, + percentage_delta=100, + size_base=0, + size_head=1010, + ), + RouteChange( + route_name="/sverdle/users", + change_type=AssetChange.ChangeType.CHANGED, + size_delta=900, + percentage_delta=810.81, + size_base=111, + size_head=1011, + ), + ] + + assert sorted_route_changes == expected_changes + + +def test_bundle_analysis_route_comparison_all_bundles(): + loader = BundleAnalysisReportLoader( + storage_service=MemoryStorageService({}), + repo_key="testing", + ) + + comparison = BundleAnalysisComparison( + loader=loader, + base_report_key="base-report", + head_report_key="head-report", + ) + + # raises errors when either report doesn't exist in storage + with pytest.raises(MissingBaseReportError): + comparison.base_report + with pytest.raises(MissingHeadReportError): + comparison.head_report + + try: + base_report = BundleAnalysisReport() + base_report.ingest(head_report_bundle_stats_path_route_base_1) + base_report.ingest(head_report_bundle_stats_path_route_base_2) + + head_report = BundleAnalysisReport() + head_report.ingest(head_report_bundle_stats_path_route_head_1) + head_report.ingest(head_report_bundle_stats_path_route_head_2) + + loader.save(base_report, "base-report") + loader.save(head_report, "head-report") + finally: + base_report.cleanup() + head_report.cleanup() + + route_changes = comparison.bundle_routes_changes() + + assert len(route_changes) == 2 + assert "bundle1" in route_changes and "bundle2" in route_changes + + sorted_route_changes = sorted(route_changes["bundle1"], key=lambda x: x.route_name) + expected_bundle1_changes = [ + RouteChange( + route_name="/sverdle/about", + change_type=AssetChange.ChangeType.CHANGED, + size_delta=900, + percentage_delta=810.81, + size_base=111, + size_head=1011, + ), + RouteChange( + route_name="/sverdle/faq", + change_type=AssetChange.ChangeType.REMOVED, + size_delta=-110, + percentage_delta=-100.0, + size_base=110, + size_head=0, + ), + RouteChange( + route_name="/sverdle/faq-prime", + change_type=AssetChange.ChangeType.ADDED, + size_delta=1010, + percentage_delta=100, + size_base=0, + size_head=1010, + ), + RouteChange( + route_name="/sverdle/users", + change_type=AssetChange.ChangeType.CHANGED, + size_delta=900, + percentage_delta=810.81, + size_base=111, + size_head=1011, + ), + ] + assert sorted_route_changes == expected_bundle1_changes + + sorted_route_changes = sorted(route_changes["bundle2"], key=lambda x: x.route_name) + expected_bundle2_changes = [ + RouteChange( + route_name="/sverdle/about", + change_type=AssetChange.ChangeType.CHANGED, + size_delta=9999, + percentage_delta=9008.11, + size_base=111, + size_head=10110, + ), + RouteChange( + route_name="/sverdle/faq", + change_type=AssetChange.ChangeType.REMOVED, + size_delta=-110, + percentage_delta=-100.0, + size_base=110, + size_head=0, + ), + RouteChange( + route_name="/sverdle/faq-prime", + change_type=AssetChange.ChangeType.ADDED, + size_delta=10100, + percentage_delta=100, + size_base=0, + size_head=10100, + ), + RouteChange( + route_name="/sverdle/users", + change_type=AssetChange.ChangeType.CHANGED, + size_delta=9999, + percentage_delta=9008.11, + size_base=111, + size_head=10110, + ), + ] + assert sorted_route_changes == expected_bundle2_changes + + +def test_bundle_analysis_route_comparison_by_bundle_name_not_exist(): + loader = BundleAnalysisReportLoader( + storage_service=MemoryStorageService({}), + repo_key="testing", + ) + + comparison = BundleAnalysisComparison( + loader=loader, + base_report_key="base-report", + head_report_key="head-report", + ) + + # raises errors when either report doesn't exist in storage + with pytest.raises(MissingBaseReportError): + comparison.base_report + with pytest.raises(MissingHeadReportError): + comparison.head_report + + try: + base_report = BundleAnalysisReport() + base_report.ingest(head_report_bundle_stats_path_route_base_1) + + head_report = BundleAnalysisReport() + head_report.ingest(head_report_bundle_stats_path_route_head_1) + + loader.save(base_report, "base-report") + loader.save(head_report, "head-report") + finally: + base_report.cleanup() + head_report.cleanup() + + with pytest.raises(MissingBundleError): + comparison.bundle_routes_changes_by_bundle("bundle2") + + +def test_bundle_analysis_route_comparison_different_bundle_names(): + loader = BundleAnalysisReportLoader( + storage_service=MemoryStorageService({}), + repo_key="testing", + ) + + comparison = BundleAnalysisComparison( + loader=loader, + base_report_key="base-report", + head_report_key="head-report", + ) + + # raises errors when either report doesn't exist in storage + with pytest.raises(MissingBaseReportError): + comparison.base_report + with pytest.raises(MissingHeadReportError): + comparison.head_report + + try: + base_report = BundleAnalysisReport() + base_report.ingest(head_report_bundle_stats_path_route_base_1) + + head_report = BundleAnalysisReport() + head_report.ingest(head_report_bundle_stats_path_route_head_2) + + loader.save(base_report, "base-report") + loader.save(head_report, "head-report") + finally: + base_report.cleanup() + head_report.cleanup() + + route_changes = comparison.bundle_routes_changes() + + EXPECTED_CHANGES = { + "bundle1": [ + RouteChange( + change_type=RouteChange.ChangeType.REMOVED, + size_delta=-111, + route_name="/sverdle/about", + percentage_delta=-100.0, + size_base=111, + size_head=0, + ), + RouteChange( + change_type=RouteChange.ChangeType.REMOVED, + size_delta=-110, + route_name="/sverdle/faq", + percentage_delta=-100.0, + size_base=110, + size_head=0, + ), + RouteChange( + change_type=RouteChange.ChangeType.REMOVED, + size_delta=-111, + route_name="/sverdle/users", + percentage_delta=-100.0, + size_base=111, + size_head=0, + ), + ], + "bundle2": [ + RouteChange( + change_type=RouteChange.ChangeType.ADDED, + size_delta=10110, + route_name="/sverdle/about", + percentage_delta=100, + size_base=0, + size_head=10110, + ), + RouteChange( + change_type=RouteChange.ChangeType.ADDED, + size_delta=10100, + route_name="/sverdle/faq-prime", + percentage_delta=100, + size_base=0, + size_head=10100, + ), + RouteChange( + change_type=RouteChange.ChangeType.ADDED, + size_delta=10110, + route_name="/sverdle/users", + percentage_delta=100, + size_base=0, + size_head=10110, + ), + ], + } + + assert len(route_changes) == 2 + assert ( + sorted(route_changes["bundle1"], key=lambda x: x.route_name) + == EXPECTED_CHANGES["bundle1"] + ) + assert ( + sorted(route_changes["bundle2"], key=lambda x: x.route_name) + == EXPECTED_CHANGES["bundle2"] + ) + + +def test_bundle_analysis_zero_asset_size_base_and_head(): + loader = BundleAnalysisReportLoader( + storage_service=MemoryStorageService({}), + repo_key="testing", + ) + + comparison = BundleAnalysisComparison( + loader=loader, + base_report_key="base-report", + head_report_key="head-report", + ) + + # raises errors when either report doesn't exist in storage + with pytest.raises(MissingBaseReportError): + comparison.base_report + with pytest.raises(MissingHeadReportError): + comparison.head_report + + try: + base_report = BundleAnalysisReport() + base_report.ingest(base_report_bundle_stats_zero_size_asset) + + head_report = BundleAnalysisReport() + head_report.ingest(base_report_bundle_stats_zero_size_asset) + + loader.save(base_report, "base-report") + loader.save(head_report, "head-report") + finally: + base_report.cleanup() + head_report.cleanup() + + bundle_comparison = comparison.bundle_comparison("sample") + asset_comparisons = bundle_comparison.asset_comparisons() + assert len(asset_comparisons) == 1 + + asset_comparisons[0].asset_change() == AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_delta=0, + asset_name="assets/index-*.js", + percentage_delta=0, + size_base=144577, + size_head=144577, + ) + + +def test_bundle_analysis_zero_asset_size_base_sized_head(): + loader = BundleAnalysisReportLoader( + storage_service=MemoryStorageService({}), + repo_key="testing", + ) + + comparison = BundleAnalysisComparison( + loader=loader, + base_report_key="base-report", + head_report_key="head-report", + ) + + # raises errors when either report doesn't exist in storage + with pytest.raises(MissingBaseReportError): + comparison.base_report + with pytest.raises(MissingHeadReportError): + comparison.head_report + + try: + base_report = BundleAnalysisReport() + base_report.ingest(base_report_bundle_stats_zero_size_asset) + + head_report = BundleAnalysisReport() + head_report.ingest(head_report_bundle_stats_path) + + loader.save(base_report, "base-report") + loader.save(head_report, "head-report") + finally: + base_report.cleanup() + head_report.cleanup() + + bundle_comparison = comparison.bundle_comparison("sample") + asset_comparisons = bundle_comparison.asset_comparisons() + assert len(asset_comparisons) == 5 + + asset_comparison_d = {} + for asset_comparison in asset_comparisons: + key = ( + asset_comparison.base_asset_report.hashed_name + if asset_comparison.base_asset_report + else None, + asset_comparison.head_asset_report.hashed_name + if asset_comparison.head_asset_report + else None, + ) + assert key not in asset_comparison_d + asset_comparison_d[key] = asset_comparison + + assert asset_comparison_d[ + ("assets/LazyComponent-fcbb0922.js", "assets/LazyComponent-fcbb0922.js") + ].asset_change() == AssetChange( + change_type=AssetChange.ChangeType.CHANGED, + size_delta=294, + asset_name="assets/LazyComponent-*.js", + percentage_delta=100.0, + size_base=0, + size_head=294, + ) diff --git a/libs/shared/tests/unit/bundle_analysis/test_bundle_utils.py b/libs/shared/tests/unit/bundle_analysis/test_bundle_utils.py new file mode 100644 index 0000000000..015f5ffc7f --- /dev/null +++ b/libs/shared/tests/unit/bundle_analysis/test_bundle_utils.py @@ -0,0 +1,410 @@ +from typing import List, Optional +from unittest.mock import MagicMock, patch + +import pytest + +from shared.bundle_analysis.utils import ( + AssetRoute, + AssetRoutePluginName, + split_by_delimiter, +) + + +@pytest.fixture +def sample_filenames(): + """ + A fixture providing sample filenames for testing different plugins. + """ + return { + AssetRoutePluginName.REMIX_VITE: "routes/index.tsx", + AssetRoutePluginName.NEXTJS_WEBPACK: "pages/api/hello.js", + AssetRoutePluginName.NUXT: "pages/index.vue", + AssetRoutePluginName.SOLIDSTART: "routes/dashboard.ts", + AssetRoutePluginName.SVELTEKIT: "src/routes/about/+page.svelte", + } + + +def test_bundle_asset_route_asset_route_remix_vite_get_from_filename(): + """ + Test the get_from_filename method for the Remive Vite plugin. + """ + plugin = AssetRoutePluginName.REMIX_VITE + + # Valid cases + valid_cases = [ + ("./app/routes/_index.tsx", "/"), # Root + ( + "app/routes/_index.tsx?__remix-build-client-route", + "/", + ), # With parameter after extension + ("app/routes/about.tsx", "/about"), # Base case + ("app/routes/concerts.new-york.jsx", "/concerts/new-york"), # Dot delimiters + ("app/routes/concerts.$city.ts", "/concerts/$city"), # Dynamic segments + ("app/routes/concerts._index.js", "/concerts"), # Nested routes + ( + "app/routes/concerts_.mine.tsx", + "/concerts/mine", + ), # Nested URLs without Layout Nesting + ( + "app/routes/_auth.register.tsx", + "/register", + ), # Nested Layouts without Nested URLs + ( + "app/routes/($lang).$productId.jsx", + "/($lang)/$productId", + ), # Optional segments + ("app/routes/files.$.tsx", "/files/$"), # Splat routes + # Escaping Special Characters + ("app/routes/sitemap[.]xml.tsx", "/sitemap.xml"), + ("app/routes/[sitemap.xml].tsx", "/sitemap.xml"), + ("app/routes/weird-url.[_index].tsx", "/weird-url"), + ("app/routes/dolla-bills-[$].tsx", "/dolla-bills-$"), + ("app/routes/[[so-weird]].tsx", "/[so-weird]"), + ] + + # Invalid cases + invalid_cases = [ + ("hello.js", None), # Contain at lease 3 parts + ("app/bout/hello.js", None), # Prefix is present + ("gap/routest/hello.js", None), # Prefix is present + ("app/routes/.hiddenfile", None), # Hidden file (no valid name) + ("app/routes/invalidfile.", None), # Ends with a dot + ("app/routes/invalidfile", None), # No dot (not a file) + ("app/routes/badextension.py", None), # Not valid extension + ] + + # Test valid cases + for filename, expected in valid_cases: + asset_route = AssetRoute(plugin=plugin) + result = asset_route.get_from_filename(filename=filename) + assert result == expected, f"Failed for valid case: {filename}" + + # Test invalid cases + for filename, expected in invalid_cases: + asset_route = AssetRoute(plugin=plugin) + result = asset_route.get_from_filename(filename=filename) + assert result == expected, f"Failed for invalid case: {filename}" + + +def test_bundle_asset_route_nextjs_webpack_get_from_filename(): + """ + Test the get_from_filename method for the Next.js Webpack plugin. + """ + plugin = AssetRoutePluginName.NEXTJS_WEBPACK + + # Valid cases + valid_cases = [ + ("app/pages/api/hello.js", "/pages/api"), + ("app/pages/index.jsx", "/pages"), + ("app/components/page.module.css", "/components"), + ("app/pages/subdir/index.tsx", "/pages/subdir"), + ] + + # Invalid cases + invalid_cases = [ + ("pages/api/hello.js", None), # Missing `app` prefix + ("app/pages/api/", None), # Last part is not a file + ("app/pages", None), # Not enough parts + ("app/pages/.hiddenfile", None), # Hidden file (no valid name) + ("app/pages/invalidfile.", None), # Ends with a dot + ("app/pages/invalidfile", None), # No dot (not a file) + ] + + # Test valid cases + for filename, expected in valid_cases: + asset_route = AssetRoute(plugin=plugin) + result = asset_route.get_from_filename(filename=filename) + assert result == expected, f"Failed for valid case: {filename}" + + # Test invalid cases + for filename, expected in invalid_cases: + asset_route = AssetRoute(plugin=plugin) + result = asset_route.get_from_filename(filename=filename) + assert result == expected, f"Failed for invalid case: {filename}" + + +def test_bundle_asset_route_nuxt_get_from_filename(): + """ + Test the get_from_filename method for the Nuxt plugin. + """ + plugin = AssetRoutePluginName.NUXT + + # Valid cases + valid_cases = [ + ("pages/api/hello.vue", "/api/hello"), + ("pages/index.vue", "/"), + ("pages/[components]/page.module.vue", "/[components]/page.module"), + ("pages/pages/subdir/index.vue", "/pages/subdir"), + ("pages/pages/subdir/[id].vue", "/pages/subdir/[id]"), + ] + + # Invalid cases + invalid_cases = [ + ("app/api/hello.vue", None), # Missing `pages` prefix + ("pages/pages/api/", None), # Last part is not a file + ("pages/pages/.js", None), # Not vue file (no valid name) + ("pages/pages/invalidfile.", None), # Ends with a dot + ("pages/pages/invalidfile", None), # No dot (not a file) + ] + + # Test valid cases + for filename, expected in valid_cases: + asset_route = AssetRoute(plugin=plugin) + result = asset_route.get_from_filename(filename=filename) + assert result == expected, f"Failed for valid case: {filename}" + + # Test invalid cases + for filename, expected in invalid_cases: + asset_route = AssetRoute(plugin=plugin) + result = asset_route.get_from_filename(filename=filename) + assert result == expected, f"Failed for invalid case: {filename}" + + +def test_bundle_asset_route_solidstart_get_from_filename(): + """ + Test the get_from_filename method for the SolidStart plugin. + """ + plugin = AssetRoutePluginName.SOLIDSTART + + # Valid cases + valid_cases = [ + ("./src/routes/blog.tsx", "/blog"), + ("src/routes/contact.jsx", "/contact"), + ("./src/routes/directions.ts", "/directions"), + ("./src/routes/blog/article-1.js", "/blog/article-1"), + ("./src/routes/work/job-1.tsx", "/work/job-1"), + ("./src/routes/index.tsx", "/"), + ("./src/routes/socials/index.tsx", "/socials"), + ("./src/routes/users(details)/[id].tsx/", "/users/[id]"), + ("./src/routes/users/[id]/[name].tsx", "/users/[id]/[name]"), + ("./src/routes/[...missing].tsx", "/[...missing]"), + ("./src/routes/[[id]].tsx", "/[[id]]"), + ("./src/routes/users/(static)/about-us/index.tsx", "/users/about-us"), + ] + + # Invalid cases + invalid_cases = [ + ("./src/pages/api/hello.js", None), # Missing `src/routes` prefix + ("./src/routes/api/", None), # Last part is not a file + ("./src/routes", None), # Not enough parts + ("./src/routes/.hiddenfile", None), # Hidden file (no valid name) + ("./src/routes/invalidfile.", None), # Ends with a dot + ("./src/routes/invalidfile", None), # No dot (not a file) + ] + + # Test valid cases + for filename, expected in valid_cases: + asset_route = AssetRoute(plugin=plugin) + result = asset_route.get_from_filename(filename=filename) + assert result == expected, f"Failed for valid case: {filename}" + + # Test invalid cases + for filename, expected in invalid_cases: + asset_route = AssetRoute(plugin=plugin) + result = asset_route.get_from_filename(filename=filename) + assert result == expected, f"Failed for invalid case: {filename}" + + +def test_bundle_asset_route_sveltekit_get_from_filename(): + """ + Test the get_from_filename method for the SvelteKit plugin. + """ + plugin = AssetRoutePluginName.SVELTEKIT + + # Valid cases + valid_cases = [ + ("src/routes/about/+page.svelte", "/about"), + ("src/routes/blog/post/+page.svelte", "/blog/post"), + ("src/routes/+layout.svelte", "/"), + ("src/routes/subdir/+layout.svelte", "/subdir"), + ("src/routes/deep/nested/+page.svelte", "/deep/nested"), + ] + + # Invalid cases + invalid_cases = [ + ("src/routes/notafile.txt", None), # Missing "+" + ("src/routes/notafile", None), # No file extension + ("src/routes/notafile.", None), # Ends with a dot + ("src/routes/+folder/", None), # Suffix is not a file + ("src/routes/+foldername", None), # Missing file extension + ("src/+folder/+page.svelte", None), # Prefix missing "routes" + ("routes/+page.svelte", None), # Missing "src" in prefix + ("src/not_routes/+page.svelte", None), # Prefix mismatch + ("src/routes/", None), # Missing file suffix + ("src/routes/not_a_file.svelte", None), # Missing "+" prefix in file + ] + + # Test valid cases + for filename, expected in valid_cases: + asset_route = AssetRoute(plugin=plugin) + result = asset_route.get_from_filename(filename=filename) + assert result == expected, f"Failed for valid case: {filename}" + + # Test invalid cases + for filename, expected in invalid_cases: + asset_route = AssetRoute(plugin=plugin) + result = asset_route.get_from_filename(filename=filename) + assert result == expected, f"Failed for invalid case: {filename}" + + +def test_bundle_asset_route_asset_route_astro_get_from_filename(): + """ + Test the get_from_filename method for the Astro plugin. + """ + plugin = AssetRoutePluginName.ASTRO + + # Valid cases + valid_cases = [ + ("./src/pages/index.astro", "/"), # Root + ( + "src/pages/index.ts?__client-route", + "/", + ), # With parameter after extension + ("src/pages/about.md", "/about"), # Base case + ("src/pages/concerts/new-york.mdx", "/concerts/new-york"), # Multiple + ("src/pages/concerts/index.ts", "/concerts"), # Dynamic segments + ( + "src/pages/[concerts].html", + "/[concerts]", + ), # HTML + ( + "src/pages/[city]/[..concerts]/[band].astro", + "/[city]/[..concerts]/[band]", + ), # Dot dot expansion + # Ignoring underscore prefix + ("src/pages/_city/index.ts", None), + ("src/pages/city/_bank/index.ts", None), + ("src/pages/city/bank/_index.ts", None), + ("src/pages/city/bank/_post.ts", None), + ] + + # Invalid cases + invalid_cases = [ + ("hello.js", None), # Contain at lease 3 parts + ("app/bout/hello.js", None), # Prefix is not present + ("gap/routest/hello.js", None), # Prefix is not present + ("src/pages/.hiddenfile", None), # Hidden file (no valid name) + ("src/pages/invalidfile.", None), # Ends with a dot + ("src/pages/invalidfile", None), # No dot (not a file) + ("src/pages/badextension.py", None), # Not valid extension + ] + + # Test valid cases + for filename, expected in valid_cases: + asset_route = AssetRoute(plugin=plugin) + result = asset_route.get_from_filename(filename=filename) + assert result == expected, f"Failed for valid case: {filename}" + + # Test invalid cases + for filename, expected in invalid_cases: + asset_route = AssetRoute(plugin=plugin) + result = asset_route.get_from_filename(filename=filename) + assert result == expected, f"Failed for invalid case: {filename}" + + +def test_bundle_asset_route_exception_handling(): + """ + Test that get_from_filename correctly handles exceptions and logs them. + """ + plugin = AssetRoutePluginName.REMIX_VITE + filename = "invalid_filename" + + # Mock the `_compute_from_filename` method to raise an exception + with patch("shared.bundle_analysis.utils.log") as mock_log: + asset_route = AssetRoute(plugin=plugin) + asset_route._compute_from_filename = MagicMock( + side_effect=ValueError("Test exception") + ) + + # Call the method and check the return value + result = asset_route.get_from_filename(filename=filename) + + # Verify that None is returned + assert result is None + + # Verify that the error was logged + mock_log.error.assert_called_once() + assert ( + "Uncaught error during AssetRoute path compute" + in mock_log.error.call_args[0][0] + ) + + +@pytest.mark.parametrize( + "input_str, extensions, expected", + [ + # Generic file validation (no extension specified) + ("file.txt", None, True), + ("page.module.css", None, True), + ("file", None, False), + ("dir/file.js", None, False), # Invalid: contains directory separator + # Specific extension validation + ("file.txt", ["txt"], True), + ("page.module.css", ["css"], True), + ("file.txt", ["css"], False), + ("file", ["txt"], False), + ("dir/file.txt", ["txt"], False), # Invalid: contains directory separator + ("file.css.js", ["js"], True), # Multiple dots, ends with js + ("file.css.js", ["css"], False), # Ends with js, not css + ("file.", ["txt"], False), # Invalid: no characters after the dot + ], +) +def test_bundle_asset_route_is_file(input_str, extensions, expected): + asset_route = AssetRoute(plugin=AssetRoutePluginName.NEXTJS_WEBPACK) + result = asset_route._is_file(input_str, extensions) + assert result == expected, f"Failed for input: {input_str}, extension: {extensions}" + + +@pytest.mark.parametrize( + "s, splitter, escape_open, escape_close, expected", + [ + # Basic splitting without escapes + ("a.b.c", ".", None, None, ["a", "b", "c"]), + ("a,b,c", ",", None, None, ["a", "b", "c"]), + ("a;b;c", ";", None, None, ["a", "b", "c"]), + # Splitting with escapes + ("a[.]b.c", ".", "[", "]", ["a.b", "c"]), + ("[a.]b.c", ".", "[", "]", ["a.b", "c"]), + ("[a.].b.c", ".", "[", "]", ["a.", "b", "c"]), + ("[a[.]b].c", ".", "[", "]", ["a[.]b", "c"]), + ("ab", "<", ">", []), + # No splitting for unmatched escapes + ("[a.b.c", ".", "[", "]", []), + ("a.b.c]", ".", "[", "]", []), + # Invalid input handling (invalid splitter, escape_open, or escape_close) + ("a.b.c", "dot", None, None, []), # Splitter not 1 char + ("a.b.c", ".", "[", None, []), # escape_close is None + ("a.b.c", ".", None, "]", []), # escape_open is None + ("a.b.c", ".", "[[", "]", []), # escape_open not 1 char + ("a.b.c", ".", "[", "]]", []), # escape_close not 1 char + # Empty string + ("", ".", None, None, []), + ("", ".", "[", "]", []), + # String with no splitters + ("abc", ".", None, None, ["abc"]), + ("abc", ",", None, None, ["abc"]), + # String with only splitters + ("...", ".", None, None, ["", "", "", ""]), + (",,,", ",", None, None, ["", "", "", ""]), + # Edge cases with nested escapes + ("[a[b.c]].d", ".", "[", "]", ["a[b.c]", "d"]), + ("[a[b[c.d]]].e", ".", "[", "]", ["a[b[c.d]]", "e"]), + ("[a.b.c]", ".", "[", "]", ["a.b.c"]), + ("[a[.]b.c]", ".", "[", "]", ["a[.]b.c"]), + ("[[a]].b", ".", "[", "]", ["[a]", "b"]), + # Validation: Splitter identical to escape characters (should return []) + ("ab", []), # Splitter == escape_open + ("ab", "<", ">", []), # Splitter == escape_close + # Validation: Unmatched escapes (should return []) + ("ab", []), # Unmatched escape + ("ab>c>", ".", "<", ">", []), # Extra closing escape + ], +) +def test_bundle_asset_route_split_by_delimiter( + s: str, + splitter: str, + escape_open: Optional[str], + escape_close: Optional[str], + expected: List[str], +): + assert split_by_delimiter(s, splitter, escape_open, escape_close) == expected diff --git a/libs/shared/tests/unit/components/__init__.py b/libs/shared/tests/unit/components/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/tests/unit/components/test_components.py b/libs/shared/tests/unit/components/test_components.py new file mode 100644 index 0000000000..2e7dec3ad6 --- /dev/null +++ b/libs/shared/tests/unit/components/test_components.py @@ -0,0 +1,26 @@ +from shared.components import Component + + +def test_from_dict(): + default_component: Component = Component.from_dict({}) + assert default_component.name == "" + assert default_component.component_id == "" + assert default_component.statuses == [] + assert default_component.paths == [] + assert default_component.flag_regexes == [] + assert default_component.get_display_name() == "default_component" + + +def test_get_display_name(): + component: Component = Component.from_dict({"component_id": "myID"}) + assert component.get_display_name() == "myID" + component.name = "myName" + assert component.get_display_name() == "myName" + + +def test_get_matching_flags(): + component: Component = Component.from_dict({"flag_regexes": ["teamA.*", "batata"]}) + matched_flags = component.get_matching_flags( + ["teamA/unit", "teamB/unit", "teamA/core", "batata", "random"] + ) + assert sorted(matched_flags) == ["batata", "teamA/core", "teamA/unit"] diff --git a/libs/shared/tests/unit/conftest.py b/libs/shared/tests/unit/conftest.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/tests/unit/django_apps/__init__.py b/libs/shared/tests/unit/django_apps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/tests/unit/django_apps/codecov_auth/test_codecov_auth_models.py b/libs/shared/tests/unit/django_apps/codecov_auth/test_codecov_auth_models.py new file mode 100644 index 0000000000..96e31f7f7a --- /dev/null +++ b/libs/shared/tests/unit/django_apps/codecov_auth/test_codecov_auth_models.py @@ -0,0 +1,1231 @@ +import logging +from unittest.mock import patch + +import pytest +from django.db import IntegrityError +from django.forms import ValidationError +from django.test import TestCase, TransactionTestCase +from pytest import LogCaptureFixture + +from shared.django_apps.codecov_auth.models import ( + DEFAULT_AVATAR_SIZE, + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + INFINITY, + SERVICE_BITBUCKET, + SERVICE_BITBUCKET_SERVER, + SERVICE_CODECOV_ENTERPRISE, + SERVICE_GITHUB, + SERVICE_GITHUB_ENTERPRISE, + Account, + AccountsUsers, + GithubAppInstallation, + OrganizationLevelToken, + Owner, + Service, + User, +) +from shared.django_apps.codecov_auth.tests.factories import ( + AccountFactory, + InvoiceBillingFactory, + OktaSettingsFactory, + OrganizationLevelTokenFactory, + OwnerFactory, + StripeBillingFactory, + UserFactory, +) +from shared.django_apps.core.tests.factories import RepositoryFactory +from shared.plan.constants import ( + DEFAULT_FREE_PLAN, + PlanName, +) +from shared.utils.test_utils import mock_config_helper +from tests.helper import mock_all_plans_and_tiers + + +class TestOwnerModel(TestCase): + def setUp(self): + self.owner = OwnerFactory(username="codecov_name", email="name@codecov.io") + + def test_repo_total_credits_returns_correct_repos_for_legacy_plan(self): + self.owner.plan = "5m" + assert self.owner.repo_total_credits == 5 + + def test_repo_total_credits_returns_correct_repos_for_v4_plan(self): + self.owner.plan = "v4-100m" + assert self.owner.repo_total_credits == 100 + + def test_repo_total_credits_returns_infinity_for_user_plans(self): + users_plans = ("users", "users-inappm", "users-inappy", "users-free") + for plan in users_plans: + self.owner.plan = plan + assert self.owner.repo_total_credits == INFINITY + + def test_repo_credits_accounts_for_currently_active_private_repos(self): + self.owner.plan = "5m" + RepositoryFactory(author=self.owner, active=True, private=True) + + assert self.owner.repo_credits == 4 + + def test_repo_credits_ignores_active_public_repos(self): + self.owner.plan = "5m" + RepositoryFactory(author=self.owner, active=True, private=True) + RepositoryFactory(author=self.owner, active=True, private=False) + + assert self.owner.repo_credits == 4 + + def test_repo_credits_returns_infinity_for_user_plans(self): + users_plans = ("users", "users-inappm", "users-inappy", "users-free") + for plan in users_plans: + self.owner.plan = plan + assert self.owner.repo_credits == INFINITY + + def test_repo_credits_treats_null_plan_as_free_plan(self): + self.owner.plan = None + self.owner.save() + assert self.owner.repo_credits == 1 + self.owner.free or 0 + + def test_nb_active_private_repos(self): + owner = OwnerFactory() + RepositoryFactory(author=owner, active=True, private=True) + RepositoryFactory(author=owner, active=True, private=False) + RepositoryFactory(author=owner, active=False, private=True) + RepositoryFactory(author=owner, active=False, private=False) + + assert owner.nb_active_private_repos == 1 + + def test_has_public_repos(self): + owner1 = OwnerFactory() + RepositoryFactory(author=owner1, active=True, private=True) + RepositoryFactory(author=owner1, active=True, private=False) + RepositoryFactory(author=owner1, active=False, private=True) + RepositoryFactory(author=owner1, active=False, private=False) + assert owner1.has_public_repos is True + + owner2 = OwnerFactory() + RepositoryFactory(author=owner2, active=True, private=True) + RepositoryFactory(author=owner2, active=False, private=True) + RepositoryFactory(author=owner2, active=False, private=False) + assert owner2.has_public_repos is True + + owner3 = OwnerFactory() + RepositoryFactory(author=owner3, active=True, private=True) + RepositoryFactory(author=owner3, active=False, private=True) + assert owner3.has_public_repos is False + + owner4 = OwnerFactory() + assert owner4.has_public_repos is False + + def test_has_active_repos(self): + owner1 = OwnerFactory() + RepositoryFactory(author=owner1, active=True, private=True) + RepositoryFactory(author=owner1, active=False, private=True) + RepositoryFactory(author=owner1, active=False, private=False) + assert owner1.has_active_repos is True + + owner2 = OwnerFactory() + RepositoryFactory(author=owner2, active=False, private=True) + RepositoryFactory(author=owner2, active=True, private=False) + assert owner2.has_active_repos is True + + owner3 = OwnerFactory() + RepositoryFactory(author=owner3, active=False, private=False) + RepositoryFactory(author=owner3, active=False, private=True) + assert owner3.has_active_repos is False + + owner4 = OwnerFactory() + assert owner4.has_active_repos is False + + def test_plan_is_null_when_validating_form(self): + owner = OwnerFactory() + owner.plan = "" + owner.stripe_customer_id = "" + owner.stripe_subscription_id = "" + owner.clean() + assert owner.plan is None + assert owner.stripe_customer_id is None + assert owner.stripe_subscription_id is None + + def test_setting_staff_on_for_not_a_codecov_member(self): + user_not_part_of_codecov = OwnerFactory(email="user@notcodecov.io", staff=True) + with self.assertRaises(ValidationError): + user_not_part_of_codecov.clean() + + def test_setting_staff_on_with_email_null(self): + user_with_null_email = OwnerFactory(email=None, staff=True) + with self.assertRaises(ValidationError): + user_with_null_email.clean() + + @patch("shared.django_apps.codecov_auth.models.get_config") + def test_main_avatar_url_services(self, mock_get_config): + test_cases = [ + { + "service": SERVICE_GITHUB, + "get_config": None, + "expected": f"https://avatars0.githubusercontent.com/u/{self.owner.service_id}?v=3&s={DEFAULT_AVATAR_SIZE}", + }, + { + "service": SERVICE_GITHUB_ENTERPRISE, + "get_config": "github_enterprise", + "expected": f"github_enterprise/avatars/u/{self.owner.service_id}?v=3&s={DEFAULT_AVATAR_SIZE}", + }, + { + "service": SERVICE_BITBUCKET, + "get_config": None, + "expected": f"https://bitbucket.org/account/codecov_name/avatar/{DEFAULT_AVATAR_SIZE}", + }, + ] + for i in range(0, len(test_cases)): + with self.subTest(i=i): + mock_get_config.return_value = test_cases[i]["get_config"] + self.owner.service = test_cases[i]["service"] + self.assertEqual(self.owner.avatar_url, test_cases[i]["expected"]) + + @patch("shared.django_apps.codecov_auth.models.get_config") + def test_bitbucket_without_u_url(self, mock_get_config): + def side_effect(*args): + if ( + len(args) == 2 + and args[0] == SERVICE_BITBUCKET_SERVER + and args[1] == "url" + ): + return SERVICE_BITBUCKET_SERVER + + mock_get_config.side_effect = side_effect + self.owner.service = SERVICE_BITBUCKET_SERVER + self.assertEqual( + self.owner.avatar_url, + f"bitbucket_server/projects/codecov_name/avatar.png?s={DEFAULT_AVATAR_SIZE}", + ) + + @patch("shared.django_apps.codecov_auth.models.get_config") + def test_bitbucket_with_u_url(self, mock_get_config): + def side_effect(*args): + if ( + len(args) == 2 + and args[0] == SERVICE_BITBUCKET_SERVER + and args[1] == "url" + ): + return SERVICE_BITBUCKET_SERVER + + mock_get_config.side_effect = side_effect + self.owner.service = SERVICE_BITBUCKET_SERVER + self.owner.service_id = "U1234" + self.assertEqual( + self.owner.avatar_url, + f"bitbucket_server/users/codecov_name/avatar.png?s={DEFAULT_AVATAR_SIZE}", + ) + + @patch("shared.django_apps.codecov_auth.models.get_gitlab_url") + def test_gitlab_service(self, mock_gitlab_url): + mock_gitlab_url.return_value = "gitlab_url" + self.owner.service = "gitlab" + self.assertEqual(self.owner.avatar_url, "gitlab_url") + mock_gitlab_url.assert_called_once() + + @patch("shared.django_apps.codecov_auth.models.get_config") + def test_gravatar_url(self, mock_get_config): + def side_effect(*args): + if len(args) == 2 and args[0] == "services" and args[1] == "gravatar": + return "gravatar" + + mock_get_config.side_effect = side_effect + self.owner.service = None + self.assertEqual( + self.owner.avatar_url, + f"https://www.gravatar.com/avatar/9a74a018e6162103a2845e22ec5d88ef?s={DEFAULT_AVATAR_SIZE}", + ) + + @patch("shared.django_apps.codecov_auth.models.get_config") + def test_avatario_url(self, mock_get_config): + def side_effect(*args): + if len(args) == 2 and args[0] == "services" and args[1] == "avatars.io": + return "avatars.io" + + mock_get_config.side_effect = side_effect + self.owner.service = None + self.assertEqual( + self.owner.avatar_url, + f"https://avatars.io/avatar/9a74a018e6162103a2845e22ec5d88ef/{DEFAULT_AVATAR_SIZE}", + ) + + @patch("shared.django_apps.codecov_auth.models.get_config") + def test_ownerid_url(self, mock_get_config): + def side_effect(*args): + if len(args) == 2 and args[0] == "setup" and args[1] == "codecov_url": + return "codecov_url" + + mock_get_config.side_effect = side_effect + self.owner.service = None + self.assertEqual( + self.owner.avatar_url, + f"codecov_url/users/{self.owner.ownerid}.png?size={DEFAULT_AVATAR_SIZE}", + ) + + @patch("shared.django_apps.codecov_auth.models.get_config") + @patch("shared.django_apps.codecov_auth.models.os.getenv") + def test_service_codecov_enterprise_url(self, mock_getenv, mock_get_config): + def side_effect(*args): + if len(args) == 2 and args[0] == "setup" and args[1] == "codecov_url": + return "codecov_url" + + mock_get_config.side_effect = side_effect + mock_getenv.return_value = SERVICE_CODECOV_ENTERPRISE + self.owner.service = None + self.owner.ownerid = None + self.assertEqual( + self.owner.avatar_url, "codecov_url/media/images/gafsi/avatar.svg" + ) + + @patch("shared.django_apps.codecov_auth.models.get_config") + def test_service_codecov_media_url(self, mock_get_config): + def side_effect(*args): + if ( + len(args) == 3 + and args[0] == "setup" + and args[1] == "media" + and args[2] == "assets" + ): + return "codecov_url_media" + + mock_get_config.side_effect = side_effect + self.owner.service = None + self.owner.ownerid = None + self.assertEqual( + self.owner.avatar_url, "codecov_url_media/media/images/gafsi/avatar.svg" + ) + + def test_is_admin_returns_false_if_admin_array_is_null(self): + assert self.owner.is_admin(OwnerFactory()) is False + + def test_is_admin_returns_true_when_comparing_with_self(self): + assert self.owner.is_admin(self.owner) is True + + def test_is_admin_returns_true_if_ownerid_in_admin_array(self): + owner = OwnerFactory() + self.owner.admins = [owner.ownerid] + assert self.owner.is_admin(owner) is True + + def test_is_admin_returns_false_if_ownerid_not_in_admin_array(self): + owner = OwnerFactory() + self.owner.admins = [] + assert self.owner.is_admin(owner) is False + + def test_activated_user_count_returns_num_activated_users(self): + owner = OwnerFactory( + plan_activated_users=[OwnerFactory().ownerid, OwnerFactory().ownerid] + ) + assert owner.activated_user_count == 2 + + def test_activated_user_count_returns_0_if_plan_activated_users_is_null(self): + owner = OwnerFactory(plan_activated_users=None) + assert owner.plan_activated_users is None + assert owner.activated_user_count == 0 + + def test_activated_user_count_ignores_students(self): + student = OwnerFactory(student=True) + self.owner.plan_activated_users = [student.ownerid] + self.owner.save() + assert self.owner.activated_user_count == 0 + + def test_activate_user_adds_ownerid_to_plan_activated_users(self): + to_activate = OwnerFactory() + self.owner.activate_user(to_activate) + self.owner.refresh_from_db() + assert to_activate.ownerid in self.owner.plan_activated_users + + def test_activate_user_does_nothing_if_user_is_activated(self): + to_activate = OwnerFactory() + self.owner.plan_activated_users = [to_activate.ownerid] + self.owner.save() + self.owner.activate_user(to_activate) + self.owner.refresh_from_db() + assert self.owner.plan_activated_users == [to_activate.ownerid] + + def test_activate_user_updates_account_user(self): + to_activate = OwnerFactory() + account = AccountFactory() + self.owner.account = account + self.owner.save() + + self.owner.activate_user(to_activate) + self.owner.refresh_from_db() + + assert to_activate.ownerid in self.owner.plan_activated_users + user = to_activate.user + assert AccountsUsers.objects.filter(user=user, account=account).first() + + def test_deactivate_removes_ownerid_from_plan_activated_users(self): + to_deactivate = OwnerFactory() + self.owner.plan_activated_users = [3, 4, to_deactivate.ownerid] + self.owner.save() + self.owner.deactivate_user(to_deactivate) + self.owner.refresh_from_db() + assert to_deactivate.ownerid not in self.owner.plan_activated_users + + def test_deactivate_non_activated_user_doesnt_crash(self): + to_deactivate = OwnerFactory() + self.owner.plan_activated_users = [] + self.owner.save() + self.owner.deactivate_user(to_deactivate) + + def test_deactivate_user_updates_account_user(self): + owner_org = self.owner + to_deactivate = OwnerFactory() + owner_org.account = AccountFactory() + to_deactivate.user = UserFactory() + owner_org.save() + AccountsUsers(user=to_deactivate.user, account=self.owner.account).save() + + owner_org.deactivate_user(to_deactivate) + owner_org.refresh_from_db() + + assert ( + AccountsUsers.objects.filter( + user=to_deactivate.user, account=self.owner.account + ).first() + is None + ) + + def test_can_activate_user_returns_true_if_user_is_student(self): + student = OwnerFactory(student=True) + assert self.owner.can_activate_user(student) is True + + def test_can_activate_user_returns_true_if_activated_user_count_not_maxed(self): + to_activate = OwnerFactory() + existing_user = OwnerFactory(ownerid=1000, student=False) + self.owner.plan_activated_users = [existing_user.ownerid] + self.owner.plan_user_count = 2 + self.owner.save() + assert self.owner.can_activate_user(to_activate) is True + + def test_can_activate_user_factors_free_seats_into_total_allowed(self): + to_activate = OwnerFactory() + self.owner.free = 1 + self.owner.plan_user_count = 0 + self.owner.save() + assert self.owner.can_activate_user(to_activate) is True + + def test_can_activate_user_can_activate_account(self): + self.owner.account = AccountFactory(plan_seat_count=1) + self.owner.plan_user_count = 1 + self.owner.save() + assert self.owner.can_activate_user(self.owner) + + def test_can_activate_user_cannot_activate_account(self): + self.owner.account = AccountFactory(plan_seat_count=0) + self.owner.plan_user_count = 1 + self.owner.save() + assert not self.owner.can_activate_user(self.owner) + + def test_default_owner_plan_is_developer(self): + owner = OwnerFactory() + assert owner.plan == DEFAULT_FREE_PLAN + + def test_fields_that_account_overrides(self): + mock_all_plans_and_tiers() + to_activate = OwnerFactory() + self.owner.plan = DEFAULT_FREE_PLAN + self.owner.plan_user_count = 1 + self.owner.save() + self.assertTrue(self.owner.can_activate_user(to_activate)) + # Pretty plan stuff that we care about + self.assertEqual(self.owner.pretty_plan["quantity"], 1) + self.assertEqual(self.owner.pretty_plan["value"], self.owner.plan) + + self.owner.account = AccountFactory( + plan_seat_count=0, plan=PlanName.ENTERPRISE_CLOUD_YEARLY.value + ) + self.owner.save() + self.owner.refresh_from_db() + self.assertFalse(self.owner.can_activate_user(to_activate)) + # Pretty plan stuff that we care about + self.assertEqual(self.owner.pretty_plan["quantity"], 0) + self.assertEqual(self.owner.pretty_plan["value"], self.owner.account.plan) + + def test_add_admin_adds_ownerid_to_admin_array(self): + self.owner.admins = [] + self.owner.save() + admin = OwnerFactory() + self.owner.add_admin(admin) + + self.owner.refresh_from_db() + assert admin.ownerid in self.owner.admins + + def test_add_admin_creates_array_if_null(self): + self.owner.admins = None + self.owner.save() + admin = OwnerFactory() + self.owner.add_admin(admin) + + self.owner.refresh_from_db() + assert self.owner.admins == [admin.ownerid] + + def test_add_admin_doesnt_add_if_ownerid_already_in_admins(self): + admin = OwnerFactory() + self.owner.admins = [admin.ownerid] + self.owner.save() + + self.owner.add_admin(admin) + + self.owner.refresh_from_db() + assert self.owner.admins == [admin.ownerid] + + def test_remove_admin_removes_ownerid_from_admins(self): + admin1 = OwnerFactory() + admin2 = OwnerFactory() + self.owner.admins = [admin1.ownerid, admin2.ownerid] + self.owner.save() + + self.owner.remove_admin(admin1) + + self.owner.refresh_from_db() + assert self.owner.admins == [admin2.ownerid] + + def test_remove_admin_does_nothing_if_user_not_admin(self): + admin1 = OwnerFactory() + admin2 = OwnerFactory() + self.owner.admins = [admin1.ownerid] + self.owner.save() + + self.owner.remove_admin(admin2) + + self.owner.refresh_from_db() + assert self.owner.admins == [admin1.ownerid] + + def test_access_no_root_organization(self): + assert self.owner.root_organization is None + + def test_access_root_organization(self): + root = OwnerFactory(service="gitlab") + parent = OwnerFactory(parent_service_id=root.service_id, service="gitlab") + self.owner.parent_service_id = parent.service_id + self.owner.service = "gitlab" + self.owner.save() + + # In some cases, there will be a 4th query from OrganizationLevelToken. There's a hook that rnus after Owner is saved + # To see if a org-wide token should be deleted. For cases when it should be deleted, the number of queries becomes 4 + with self.assertNumQueries(3): + assert self.owner.root_organization == root + + # cache the root organization id + assert self.owner.root_parent_service_id == root.service_id + + with self.assertNumQueries(1): + self.owner.root_organization + + def test_inactive_users_count(self): + org = OwnerFactory() + + activated_user = OwnerFactory() + activated_user_in_org = OwnerFactory(organizations=[org.ownerid]) + activated_student = OwnerFactory(student=True) + activated_student_in_org = OwnerFactory( + organizations=[org.ownerid], student=True + ) + + OwnerFactory(organizations=[org.ownerid], student=True) + OwnerFactory(organizations=[org.ownerid]) + + org.plan_activated_users = [ + activated_user.ownerid, + activated_user_in_org.ownerid, + activated_student.ownerid, + activated_student_in_org.ownerid, + ] + org.save() + + self.assertEqual(org.inactive_user_count, 1) + + def test_student_count(self): + org = OwnerFactory(service=Service.GITHUB.value, service_id="1") + + activated_user = OwnerFactory() + activated_user_in_org = OwnerFactory(organizations=[org.ownerid]) + activated_student = OwnerFactory(student=True) + activated_student_in_org = OwnerFactory( + organizations=[org.ownerid], student=True + ) + + OwnerFactory(organizations=[org.ownerid], student=True) + OwnerFactory(organizations=[org.ownerid]) + + org.plan_activated_users = [ + activated_user.ownerid, + activated_user_in_org.ownerid, + activated_student.ownerid, + activated_student_in_org.ownerid, + ] + org.save() + + self.assertEqual(org.student_count, 3) + + def test_has_yaml(self): + org = OwnerFactory(yaml=None) + assert org.has_yaml is False + org.yaml = {"require_ci_to_pass": True} + org.save() + assert org.has_yaml is True + + +class TestOrganizationLevelTokenModel(TestCase): + def test_can_save_org_token_for_org_basic_plan(self): + owner = OwnerFactory(plan=DEFAULT_FREE_PLAN) + owner.save() + token = OrganizationLevelToken(owner=owner) + token.save() + assert OrganizationLevelToken.objects.filter(owner=owner).count() == 1 + + @patch( + "shared.django_apps.codecov_auth.services.org_level_token_service.OrgLevelTokenService.org_can_have_upload_token" + ) + def test_token_is_deleted_when_changing_user_plan( + self, mocked_org_can_have_upload_token + ): + mocked_org_can_have_upload_token.return_value = False + owner = OwnerFactory(plan="users-enterprisem") + org_token = OrganizationLevelTokenFactory(owner=owner) + owner.save() + org_token.save() + assert OrganizationLevelToken.objects.filter(owner=owner).count() == 1 + owner.plan = DEFAULT_FREE_PLAN + owner.save() + assert OrganizationLevelToken.objects.filter(owner=owner).count() == 0 + + +class TestGithubAppInstallationModel(TestCase): + DEFAULT_APP_ID = 12345 + + @pytest.fixture(autouse=True) + def mock_default_app_id(self, mocker): + mock_config_helper( + mocker, configs={"github.integration.id": self.DEFAULT_APP_ID} + ) + + def test_covers_all_repos(self): + owner = OwnerFactory() + repo1 = RepositoryFactory(author=owner) + repo2 = RepositoryFactory(author=owner) + repo3 = RepositoryFactory(author=owner) + other_repo_different_owner = RepositoryFactory() + installation_obj = GithubAppInstallation( + owner=owner, + repository_service_ids=None, + installation_id=100, + ) + installation_obj.save() + assert installation_obj.name == "codecov_app_installation" + assert installation_obj.covers_all_repos() == True + assert installation_obj.is_repo_covered_by_integration(repo1) == True + assert ( + installation_obj.is_repo_covered_by_integration(other_repo_different_owner) + == False + ) + assert list(owner.github_app_installations.all()) == [installation_obj] + assert installation_obj.repository_queryset().exists() + assert set(installation_obj.repository_queryset().all()) == set( + [repo1, repo2, repo3] + ) + + def test_covers_some_repos(self): + owner = OwnerFactory() + repo = RepositoryFactory(author=owner) + same_owner_other_repo = RepositoryFactory(author=owner) + other_repo_different_owner = RepositoryFactory() + installation_obj = GithubAppInstallation( + owner=owner, + repository_service_ids=[repo.service_id], + installation_id=100, + ) + installation_obj.save() + assert installation_obj.covers_all_repos() == False + assert installation_obj.is_repo_covered_by_integration(repo) == True + assert ( + installation_obj.is_repo_covered_by_integration(other_repo_different_owner) + == False + ) + assert ( + installation_obj.is_repo_covered_by_integration(same_owner_other_repo) + == False + ) + assert list(owner.github_app_installations.all()) == [installation_obj] + assert installation_obj.repository_queryset().exists() + assert list(installation_obj.repository_queryset().all()) == [repo] + + def test_is_configured(self): + owner = OwnerFactory() + installation_default = GithubAppInstallation( + owner=owner, + repository_service_ids=None, + installation_id=123, + app_id=self.DEFAULT_APP_ID, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + ) + installation_configured = GithubAppInstallation( + owner=owner, + repository_service_ids=None, + name="my_installation", + installation_id=100, + app_id=123, + pem_path="some_path", + ) + installation_not_configured = GithubAppInstallation( + owner=owner, + repository_service_ids=None, + installation_id=100, + name="my_other_installation", + app_id=1234, + ) + installation_default_name_not_configured = GithubAppInstallation( + owner=owner, + repository_service_ids=None, + installation_id=100, + app_id=121212, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + ) + installation_default_name_not_default_id_configured = GithubAppInstallation( + owner=owner, + repository_service_ids=None, + installation_id=100, + app_id=121212, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + pem_path="some_path", + ) + installation_default.save() + + installation_configured.save() + installation_not_configured.save() + installation_default_name_not_configured.save() + installation_default_name_not_default_id_configured.save() + + assert installation_default.is_configured() == True + installation_default.app_id = str(self.DEFAULT_APP_ID) + assert installation_default.is_configured() == True + # Unconfigured apps are not configured + installation_default.name = "unconfigured_app" + assert installation_default.is_configured() == False + + assert installation_configured.is_configured() == True + assert installation_not_configured.is_configured() == False + assert installation_default_name_not_configured.app_id != self.DEFAULT_APP_ID + assert installation_default_name_not_configured.is_configured() == False + assert ( + installation_default_name_not_default_id_configured.app_id + != self.DEFAULT_APP_ID + ) + assert ( + installation_default_name_not_default_id_configured.is_configured() == True + ) + + +class TestGitHubAppInstallationNoDefaultAppIdConfig(TestCase): + @pytest.fixture(autouse=True) + def mock_no_default_app_id(self, mocker): + mock_config_helper(mocker, configs={"github.integration.id": None}) + + def test_is_configured_no_default(self): + owner = OwnerFactory() + installation_default = GithubAppInstallation( + owner=owner, + repository_service_ids=None, + installation_id=123, + app_id=1200, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + ) + installation_default.save() + assert installation_default.is_configured() == True + + +class TestAccountModel(TransactionTestCase): + @pytest.fixture(autouse=True) + def inject_fixtures(self, caplog: LogCaptureFixture): + self.caplog = caplog + + def test_account_with_billing_details(self): + account = AccountFactory() + OktaSettingsFactory(account=account) + # set up stripe + stripe = StripeBillingFactory(account=account) + self.assertTrue(stripe.is_active) + # switch to invoice + invoice = InvoiceBillingFactory(account=account) + stripe.refresh_from_db() + invoice.refresh_from_db() + self.assertTrue(invoice.is_active) + self.assertFalse(stripe.is_active) + # back to stripe + stripe.is_active = True + stripe.save() + stripe.refresh_from_db() + invoice.refresh_from_db() + self.assertFalse(invoice.is_active) + self.assertTrue(stripe.is_active) + + def test_account_with_users(self): + mock_all_plans_and_tiers() + user_1 = UserFactory() + OwnerFactory(user=user_1) + user_2 = UserFactory() + OwnerFactory(user=user_2) + account = AccountFactory() + + account.users.add(user_1) + account.save() + + user_2.accounts.add(account) + user_2.save() + + self.assertEqual(account.users.count(), 2) + self.assertEqual(user_1.accounts.count(), 1) + self.assertEqual(user_2.accounts.count(), 1) + self.assertEqual(AccountsUsers.objects.all().count(), 2) + + # handles duplicates gracefully + account.users.add(user_2) + account.save() + self.assertEqual(account.users.count(), 2) + user_2.accounts.add(account) + user_2.save() + self.assertEqual(user_2.accounts.count(), 1) + self.assertEqual(AccountsUsers.objects.all().count(), 2) + + # does not handle duplicates gracefully + with self.assertRaises(IntegrityError): + AccountsUsers.objects.create(user=user_1, account=account) + self.assertEqual(account.users.count(), 2) + self.assertEqual(user_1.accounts.count(), 1) + self.assertEqual(AccountsUsers.objects.all().count(), 2) + + self.assertEqual(account.all_user_count, 2) + self.assertEqual(account.organizations_count, 0) + + self.assertEqual(account.activated_student_count, 0) + self.assertEqual(account.total_seat_count, 1) + self.assertEqual(account.available_seat_count, 0) + # Pretty plan stuff that we care about + self.assertEqual(account.pretty_plan["quantity"], 1) + self.assertEqual(account.pretty_plan["value"], DEFAULT_FREE_PLAN) + + def test_create_account_for_enterprise_experience(self): + mock_all_plans_and_tiers() + # 2 separate OwnerOrgs that wish to Enterprise + stripe_customer_id = "abc123" + stripe_subscription_id = "defg456" + + user_for_owner_1 = UserFactory(email="hello@email.com", name="Luigi") + owner_1 = OwnerFactory( + username="codecov-1", + plan=DEFAULT_FREE_PLAN, + plan_user_count=1, + organizations=[], + user_id=user_for_owner_1.id, # has user + ) + owner_2 = OwnerFactory( + username="codecov-sentry", + plan=DEFAULT_FREE_PLAN, + plan_user_count=1, + organizations=[], + user_id=None, # no user + ) + owner_3 = OwnerFactory( + username="sentry-1", + plan=DEFAULT_FREE_PLAN, + plan_user_count=1, + organizations=[], + user_id=None, # no user + ) + + unrelated_owner = OwnerFactory( + user_id=UserFactory().id, + organizations=[], + ) + unrelated_org = OwnerFactory( + stripe_customer_id=stripe_customer_id, + stripe_subscription_id=stripe_subscription_id, + plan=PlanName.CODECOV_PRO_YEARLY.value, + plan_user_count=50, + plan_activated_users=[unrelated_owner.ownerid], + ) + unrelated_owner.organizations.append(unrelated_org.ownerid) + unrelated_owner.save() + + org_1 = OwnerFactory( + username="codecov-org", + stripe_customer_id=stripe_customer_id, + stripe_subscription_id=stripe_subscription_id, + plan=DEFAULT_FREE_PLAN, + plan_user_count=50, + plan_activated_users=[owner_1.ownerid, owner_2.ownerid], + free=10, + ) + org_2 = OwnerFactory( + username="sentry-org", + stripe_customer_id=stripe_customer_id, + stripe_subscription_id=stripe_subscription_id, + plan=PlanName.CODECOV_PRO_YEARLY.value, + plan_user_count=50, + plan_activated_users=[owner_2.ownerid, owner_3.ownerid], + free=10, + ) + owner_1.organizations.append(org_1.ownerid) + owner_1.save() + owner_2.organizations.extend([org_2.ownerid, org_1.ownerid]) + owner_2.save() + owner_3.organizations.append(org_2.ownerid) + owner_3.save() + + # How to Enterprise + enterprise_account = AccountFactory( + name="getsentry", + plan=org_1.plan, + plan_seat_count=org_1.plan_user_count, + free_seat_count=org_1.free, + ) + # create inactive Stripe billing + StripeBillingFactory( + account=enterprise_account, + customer_id=stripe_customer_id, + subscription_id=None, + is_active=False, + ) + # create active Invoice billing + InvoiceBillingFactory( + account=enterprise_account, + account_manager="Mario", + ) + + org_1.account = enterprise_account + org_1.save() + org_2.account = enterprise_account + org_2.save() + enterprise_account.refresh_from_db() + + # connect Users to Account + for org in [org_1, org_2]: + for owner_user_id in org.plan_activated_users: + owner_user = Owner.objects.get(ownerid=owner_user_id) + if not owner_user.user_id: + # if the OwnerUser doesn't have a User obj, make one for them and attach to Owner object + user = UserFactory( + email=owner_user.email, + name=owner_user.name, + ) + owner_user.user_id = user.id + owner_user.save() + enterprise_account.users.add(user) + enterprise_account.save() + else: + enterprise_account.users.add(owner_user.user_id) + + enterprise_account.refresh_from_db() + org_1.refresh_from_db() + org_2.refresh_from_db() + unrelated_org.refresh_from_db() + owner_1.refresh_from_db() + owner_2.refresh_from_db() + owner_3.refresh_from_db() + unrelated_owner.refresh_from_db() + + # for users + for owner in [owner_1, owner_2, owner_3]: + user = User.objects.get(id=owner.user_id) + self.assertEqual(user.accounts.count(), 1) + self.assertEqual(user.accounts.first(), enterprise_account) + self.assertEqual( + AccountsUsers.objects.get(user=user).account, enterprise_account + ) + unrelated_user = User.objects.get(id=unrelated_owner.user_id) + self.assertEqual(unrelated_user.accounts.count(), 0) + self.assertIsNone(unrelated_user.accounts.first()) + self.assertFalse(AccountsUsers.objects.filter(user=unrelated_user).exists()) + + # for orgs + self.assertTrue(org_1.account) + self.assertTrue(org_2.account) + self.assertFalse(unrelated_org.account) + self.assertEqual(org_1.account, enterprise_account) + self.assertEqual(org_2.account, enterprise_account) + self.assertEqual( + set( + enterprise_account.organizations.all().values_list("ownerid", flat=True) + ), + {org_1.ownerid, org_2.ownerid}, + ) + + # for the enterprise account + self.assertEqual( + set(enterprise_account.users.all().values_list("id", flat=True)), + {owner_1.user_id, owner_2.user_id, owner_3.user_id}, + ) + self.assertEqual( + set( + enterprise_account.organizations.all().values_list("ownerid", flat=True) + ), + {org_1.ownerid, org_2.ownerid}, + ) + self.assertTrue( + AccountsUsers.objects.filter(account=enterprise_account).count(), 3 + ) + self.assertFalse(enterprise_account.stripe_billing.first().is_active) + self.assertTrue(enterprise_account.invoice_billing.first().is_active) + self.assertEqual(enterprise_account.all_user_count, 3) + self.assertEqual(enterprise_account.organizations_count, 2) + self.assertEqual(enterprise_account.activated_student_count, 0) + self.assertEqual(enterprise_account.total_seat_count, 60) + self.assertEqual(enterprise_account.available_seat_count, 57) + + # Pretty plan stuff that we care about + self.assertEqual(enterprise_account.pretty_plan["quantity"], 50) + self.assertEqual(enterprise_account.pretty_plan["value"], DEFAULT_FREE_PLAN) + + def test_activate_user_onto_account(self): + user = UserFactory() + user.save() + account = AccountFactory() + account.save() + + assert AccountsUsers.objects.filter(user=user, account=account).first() is None + account.activate_user_onto_account(user) + account.refresh_from_db() + + assert AccountsUsers.objects.filter(user=user, account=account).first() + + def test_activate_owner_user_onto_account_create_user(self): + owner = OwnerFactory(user=None) + account = AccountFactory() + self.assertEqual(User.objects.all().count(), 0) + self.assertEqual(AccountsUsers.objects.all().count(), 0) + self.assertIsNone(owner.user) + + account.activate_owner_user_onto_account(owner) + account.refresh_from_db() + owner.refresh_from_db() + + self.assertEqual(User.objects.all().count(), 1) + self.assertEqual(AccountsUsers.objects.all().count(), 1) + self.assertIsNotNone(owner.user) + + new_user = User.objects.get(id=owner.user_id) + assert AccountsUsers.objects.filter(user=new_user, account=account).first() + + def test_activate_owner_user_onto_account_with_user(self): + owner = OwnerFactory() + user = UserFactory() + owner.user = user + account = AccountFactory() + account.save() + + account.activate_owner_user_onto_account(owner) + account.refresh_from_db() + + assert AccountsUsers.objects.filter(user=user, account=account).first() + + def test_activate_owner_user_onto_account_existing_account_user(self): + owner = OwnerFactory() + user = UserFactory() + owner.user = user + account = AccountFactory() + account.save() + + account.activate_owner_user_onto_account(owner) + account.refresh_from_db() + + assert AccountsUsers.objects.filter(user=user, account=account).first() + + account.activate_owner_user_onto_account(owner) + account.refresh_from_db() + + assert AccountsUsers.objects.filter(user=user, account=account).first() + + def test_deactivate_owner_user_from_account_remove_user(self): + # Set up User to be associated with an Org under an account + owner = OwnerFactory() + user = UserFactory() + owner.user = user + org = OwnerFactory( + plan=PlanName.CODECOV_PRO_YEARLY.value, + plan_activated_users=[owner.ownerid], + ) + account = AccountFactory() + org.account = account + org.save() + account.users.add(user) + account.save() + + # ensure that there exists an account user relationship before deactivating + assert AccountsUsers.objects.filter(user=user, account=account).first() + + org.plan_activated_users = [] + org.save() + account.deactivate_owner_user_from_account(owner) + + assert AccountsUsers.objects.filter(user=user, account=account).first() is None + + def test_deactivate_owner_user_from_account_do_not_remove_user(self): + # Set up User to be associated with an Org under an account + owner = OwnerFactory() + user = UserFactory() + owner.user = user + org1 = OwnerFactory( + plan=PlanName.CODECOV_PRO_YEARLY.value, + plan_activated_users=[owner.ownerid], + ) + org2 = OwnerFactory( + plan=PlanName.CODECOV_PRO_YEARLY.value, + plan_activated_users=[owner.ownerid], + ) + account = AccountFactory() + org1.account = account + org1.save() + org2.account = account + org2.save() + account.users.add(user) + account.save() + + # ensure that there exists an account user relationship before deactivating + assert AccountsUsers.objects.filter(user=user, account=account).first() + + # Only deactivate user for org1, org2 still has a reference to the user + org1.plan_activated_users = [] + org1.save() + account.deactivate_owner_user_from_account(owner) + + assert AccountsUsers.objects.filter(user=user, account=account).first() + + def test_deactivate_owner_user_no_user_do_nothing(self): + owner_user = OwnerFactory(user=None) + account = AccountFactory() + account.save() + with self.caplog.at_level(logging.WARNING): + assert account.deactivate_owner_user_from_account(owner_user) is None + assert ( + self.caplog.records[0].message + == "Attempting to deactivate an owner without associated user. Skipping deactivation." + ) + + def test_activated_user_count(self): + # This shouldn't show up on the account + unrelated_owner: Owner = OwnerFactory(service="github") + unrelated_user: User = UserFactory() + unrelated_owner.user = unrelated_user + unrelated_user.save() + + # This shouldn't show up because it's a student, and students don't + # count towards activated users. + student_owner: Owner = OwnerFactory(service="github", student=True) + student_user: User = UserFactory() + student_owner.user = student_user + student_user.save() + student_owner.save() + + # User1 also has multiple owners. Should be counted as 1 user + owner1: Owner = OwnerFactory(service="github", student=False) + owner1_gitlab: Owner = OwnerFactory(service="gitlab", student=False) + owner1_bitbucket: Owner = OwnerFactory(service="bitbucket", student=False) + user1: User = UserFactory() + owner1.user = user1 + owner1_gitlab.user = user1 + owner1_bitbucket.user = user1 + owner1.save() + owner1_gitlab.save() + owner1_bitbucket.save() + + owner2: Owner = OwnerFactory(service="bitbucket", student=False) + user2: User = UserFactory() + owner2.user = user2 + owner2.save() + + org: Owner = OwnerFactory() + account: Account = AccountFactory() + org.account = account + org.save() + + account.users.add(student_user) + account.users.add(user1) + account.users.add(user2) + + assert 2 == account.activated_user_count + + def test_can_activate_user_already_exist(self): + owner: Owner = OwnerFactory(service="github", student=False) + user: User = UserFactory() + owner.user = user + owner.save() + user.save() + + org: Owner = OwnerFactory() + account: Account = AccountFactory() + org.account = account + org.save() + + account.users.add(user) + assert account.can_activate_user(user) + + def test_can_activate_user_student(self): + owner: Owner = OwnerFactory(service="github", student=True) + user: User = UserFactory() + owner.user = user + owner.save() + + org: Owner = OwnerFactory() + account: Account = AccountFactory(free_seat_count=0, plan_seat_count=0) + org.account = account + org.save() + + assert account.can_activate_user(user) + + def test_can_activate_user_not_enough_seats_left(self): + owner: Owner = OwnerFactory(service="github", student=False) + user: User = UserFactory() + owner.user = user + owner.save() + + org: Owner = OwnerFactory() + account: Account = AccountFactory(free_seat_count=0, plan_seat_count=0) + org.account = account + org.save() + + assert not account.can_activate_user(user) + + def test_can_activate_user_enough_seats_left(self): + owner: Owner = OwnerFactory(service="github", student=False) + user: User = UserFactory() + owner.user = user + owner.save() + + org: Owner = OwnerFactory() + account: Account = AccountFactory(free_seat_count=0, plan_seat_count=1) + org.account = account + org.save() + + assert account.can_activate_user(user) + + def test_can_activate_user_no_user(self): + org: Owner = OwnerFactory() + account: Account = AccountFactory(free_seat_count=0, plan_seat_count=1) + org.account = account + org.save() + + assert account.can_activate_user() + + +class TestUserModels(TransactionTestCase): + def test_is_github_student(self): + github_user: Owner = OwnerFactory(service="github", student=True) + user = UserFactory() + github_user.user = user + github_user.save() + + assert user.is_github_student is True + + def test_is_not_github_student(self): + github_user: Owner = OwnerFactory(service="github", student=False) + user = UserFactory() + github_user.user = user + github_user.save() + + assert user.is_github_student is False + + def test_is_not_github_student_no_owners(self): + user = UserFactory() + + assert user.is_github_student is False diff --git a/libs/shared/tests/unit/django_apps/core/__init__.py b/libs/shared/tests/unit/django_apps/core/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/tests/unit/django_apps/core/test_core_models.py b/libs/shared/tests/unit/django_apps/core/test_core_models.py new file mode 100644 index 0000000000..67d69c8897 --- /dev/null +++ b/libs/shared/tests/unit/django_apps/core/test_core_models.py @@ -0,0 +1,121 @@ +import json +from unittest.mock import MagicMock, patch + +from django.forms import ValidationError +from django.test import TestCase + +from shared.django_apps.core.models import Commit +from shared.django_apps.core.tests.factories import ( + CommitFactory, + RepositoryFactory, +) +from shared.django_apps.reports.tests.factories import CommitReportFactory +from shared.storage.exceptions import FileNotInStorageError + + +class RepoTests(TestCase): + def test_clean_repo(self): + repo = RepositoryFactory(using_integration=None) + with self.assertRaises(ValidationError): + repo.clean() + + +class CommitTests(TestCase): + def test_commitreport_no_code(self): + commit = CommitFactory() + CommitReportFactory( + commit=commit, code="testing" + ) # this is a report for a "local upload" + report2 = CommitReportFactory(commit=commit, code=None) + assert commit.commitreport == report2 + + sample_report = { + "files": { + "different/test_file.py": [ + 2, + [0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0], + [[0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0]], + [0, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + ], + }, + "sessions": { + "0": { + "N": None, + "a": "v4/raw/2019-01-10/4434BC2A2EC4FCA57F77B473D83F928C/abf6d4df662c47e32460020ab14abf9303581429/9ccc55a1-8b41-4bb1-a946-ee7a33a7fb56.txt", + "c": None, + "d": 1547084427, + "e": None, + "f": ["unittests"], + "j": None, + "n": None, + "p": None, + "t": [3, 20, 17, 3, 0, "85.00000", 0, 0, 0, 0, 0, 0, 0], + "": None, + } + }, + } + + @patch("shared.django_apps.utils.model_utils.ArchiveService") + def test_get_report_from_db(self, mock_archive): + commit = CommitFactory() + mock_read_file = MagicMock() + mock_archive.return_value.read_file = mock_read_file + commit._report = self.sample_report + commit.save() + + fetched = Commit.objects.get(id=commit.id) + assert fetched.report == self.sample_report + mock_archive.assert_not_called() + mock_read_file.assert_not_called() + + @patch("shared.django_apps.utils.model_utils.ArchiveService") + def test_get_report_from_storage(self, mock_archive): + commit = CommitFactory() + storage_path = "https://storage/path/report.json" + mock_read_file = MagicMock(return_value=json.dumps(self.sample_report)) + mock_archive.return_value.read_file = mock_read_file + commit._report = None + commit._report_storage_path = storage_path + commit.save() + + fetched = Commit.objects.get(id=commit.id) + assert fetched.report == self.sample_report + mock_archive.assert_called() + mock_read_file.assert_called_with(storage_path) + # Calls it again to test caching + assert fetched.report == self.sample_report + assert mock_archive.call_count == 1 + assert mock_read_file.call_count == 1 + # This one to help us understand caching across different instances + assert commit.report == self.sample_report + assert mock_archive.call_count == 2 + assert mock_read_file.call_count == 2 + # Let's see for objects with different IDs + diff_commit = CommitFactory() + storage_path = "https://storage/path/files_array.json" + diff_commit._report = None + diff_commit._report_storage_path = storage_path + diff_commit.save() + assert diff_commit.report == self.sample_report + assert mock_archive.call_count == 3 + assert mock_read_file.call_count == 3 + + @patch("shared.django_apps.utils.model_utils.ArchiveService") + def test_get_report_from_storage_file_not_found(self, mock_archive): + commit = CommitFactory() + storage_path = "https://storage/path/files_array.json" + + def side_effect(*args, **kwargs): + raise FileNotInStorageError() + + mock_read_file = MagicMock(side_effect=side_effect) + mock_archive.return_value.read_file = mock_read_file + commit._report = None + commit._report_storage_path = storage_path + commit.save() + + fetched = Commit.objects.get(id=commit.id) + assert fetched._report_storage_path == storage_path + assert fetched.report == {} + mock_archive.assert_called() + mock_read_file.assert_called_with(storage_path) diff --git a/libs/shared/tests/unit/django_apps/core/test_manager.py b/libs/shared/tests/unit/django_apps/core/test_manager.py new file mode 100644 index 0000000000..fca196f175 --- /dev/null +++ b/libs/shared/tests/unit/django_apps/core/test_manager.py @@ -0,0 +1,118 @@ +import pytest + +from shared.django_apps.codecov_auth.models import Owner +from shared.django_apps.codecov_auth.tests.factories import ( + AccountFactory, + OktaSettingsFactory, + OwnerFactory, +) +from shared.django_apps.core.models import Repository +from shared.django_apps.core.tests.factories import RepositoryFactory + + +@pytest.fixture +def user_owner(viewable_repo_by_permission: Repository) -> Owner: + owner = OwnerFactory(permission=[viewable_repo_by_permission.repoid]) + owner.save() + return owner + + +@pytest.fixture +def viewable_repo_by_authorship(user_owner: Owner) -> Repository: + repo = RepositoryFactory(author=user_owner, private=True) + repo.save() + return repo + + +@pytest.fixture +def viewable_repo_by_permission() -> Repository: + repo = RepositoryFactory(private=True) + repo.save() + return repo + + +@pytest.fixture +def unviewable_repo() -> Repository: + repo = RepositoryFactory() + repo.save() + return repo + + +@pytest.mark.django_db +def test_repository_queryset_viewable_repos( + user_owner: Owner, + unviewable_repo: Repository, + viewable_repo_by_authorship: Repository, + viewable_repo_by_permission: Repository, +): + all_queryset = Repository.objects.all() + assert all_queryset.count() == 3 + + # filters out the unviewable repo, which is the repo that has neither permissions nor + # authorship related to the user. + viewable_repos = all_queryset.viewable_repos(user_owner) + assert viewable_repos.count() == 2 + + +@pytest.mark.django_db +def test_repository_queryset_exclude_accounts_enforced_okta(user_owner: Owner): + account = AccountFactory() + org_owner = OwnerFactory(account=account) + org_owner.save() + okta_settings = OktaSettingsFactory(account=account, enforced=True) + okta_settings.save() + + repo = RepositoryFactory(author=org_owner, private=True) + repo.save() + + all_queryset = Repository.objects.filter(author=org_owner) + assert all_queryset.count() == 1 + + filtered_queryset = all_queryset.exclude_accounts_enforced_okta([]) + assert filtered_queryset.count() == 0 + + +@pytest.mark.parametrize( + "is_private,has_account,has_okta,enforced_okta,is_authenticated", + [ + pytest.param(False, False, False, False, False, id="not private repo"), + pytest.param(True, False, False, False, False, id="no account"), + pytest.param(True, True, False, False, False, id="no okta settings"), + pytest.param(True, True, True, False, False, id="not enforced okta"), + pytest.param(True, True, True, True, True, id="is authenticated"), + ], +) +@pytest.mark.django_db +def test_repository_queryset_exclude_accounts_enforced_okta_do_not_exclude( + user_owner: Owner, + is_private: bool, + has_account: bool, + has_okta: bool, + enforced_okta: bool, + is_authenticated: bool, +): + org_owner = OwnerFactory() + repo = RepositoryFactory(author=org_owner, private=is_private) + repo.save() + + authenticated_accounts = [] + + if has_account: + account = AccountFactory() + org_owner.account = account + org_owner.save() + + if has_okta: + okta_settings = OktaSettingsFactory(account=account, enforced=enforced_okta) + okta_settings.save() + + if is_authenticated: + authenticated_accounts.append(account.id) + + all_queryset = Repository.objects.filter(author=org_owner) + assert all_queryset.count() == 1 + + filtered_queryset = all_queryset.exclude_accounts_enforced_okta( + authenticated_accounts + ) + assert filtered_queryset.count() == 1 diff --git a/libs/shared/tests/unit/django_apps/pg_telemetry/test_pg_models.py b/libs/shared/tests/unit/django_apps/pg_telemetry/test_pg_models.py new file mode 100644 index 0000000000..7bcb2e68fa --- /dev/null +++ b/libs/shared/tests/unit/django_apps/pg_telemetry/test_pg_models.py @@ -0,0 +1,35 @@ +from datetime import datetime, timezone + +from django.test import TestCase + +from shared.django_apps.pg_telemetry.models import SimpleMetric as PgSimpleMetric + + +class TestPgSimpleMetricModel(TestCase): + """ + Test that we can create `PgSimpleMetric` records and that they are routed + to a separate database from `TsSimpleMetric` records. + """ + + databases = {"default", "timeseries"} + + def test_create_simple_metric(self): + timestamp = datetime.now().replace(tzinfo=timezone.utc) + + PgSimpleMetric.objects.create( + name="foo", + value=3.0, + timestamp=timestamp, + repo_id=1, + owner_id=2, + commit_id=3, + ) + fetched = PgSimpleMetric.objects.get(timestamp=timestamp) + + # Assert that we got back the record we saved + assert fetched.name == "foo" + assert fetched.value == 3.0 + assert fetched.timestamp == timestamp + assert fetched.repo_id == 1 + assert fetched.owner_id == 2 + assert fetched.commit_id == 3 diff --git a/libs/shared/tests/unit/django_apps/reports/test_reports_models.py b/libs/shared/tests/unit/django_apps/reports/test_reports_models.py new file mode 100644 index 0000000000..4c514064d6 --- /dev/null +++ b/libs/shared/tests/unit/django_apps/reports/test_reports_models.py @@ -0,0 +1,41 @@ +from django.test import TestCase + +from shared.django_apps.reports.tests.factories import ( + RepositoryFlagFactory, + UploadFactory, + UploadFlagMembershipFactory, +) + + +class UploadTests(TestCase): + def test_ci_url_when_no_provider(self): + session = UploadFactory(provider=None) + assert session.ci_url is None + + def test_ci_url_when_provider_do_not_have_build_url(self): + session = UploadFactory(provider="azure_pipelines") + assert session.ci_url is None + + def test_ci_url_when_provider_has_build_url(self): + session = UploadFactory(provider="travis", job_code="123") + repo = session.report.commit.repository + assert ( + session.ci_url + == f"https://travis-ci.com/{repo.author.username}/{repo.name}/jobs/{session.job_code}" + ) + + def test_ci_url_when_db_has_build_url(self): + session = UploadFactory(build_url="http://example.com") + assert session.ci_url == "http://example.com" + + def test_flags(self): + session = UploadFactory() + flag_one = RepositoryFlagFactory() + flag_two = RepositoryFlagFactory() + # connect the flag and membership + UploadFlagMembershipFactory(flag=flag_one, report_session=session) + UploadFlagMembershipFactory(flag=flag_two, report_session=session) + + assert ( + session.flag_names.sort() == [flag_one.flag_name, flag_two.flag_name].sort() + ) diff --git a/libs/shared/tests/unit/django_apps/test_db_routers.py b/libs/shared/tests/unit/django_apps/test_db_routers.py new file mode 100644 index 0000000000..bd10f6f884 --- /dev/null +++ b/libs/shared/tests/unit/django_apps/test_db_routers.py @@ -0,0 +1,77 @@ +from django.test import override_settings + +from shared.django_apps.codecov_auth.models import Owner +from shared.django_apps.db_routers import MultiDatabaseRouter +from shared.django_apps.pg_telemetry.models import SimpleMetric as PgSimpleMetric + + +class TestMultiDatabaseRouter: + @override_settings( + TIMESERIES_DATABASE_READ_REPLICA_ENABLED=True, + DATABASE_READ_REPLICA_ENABLED=True, + ) + def test_db_for_read_read_replica(self, mocker): + # At time of writing, the Django timeseries models don't live in this + # repo so we're pretending a different model is from the timeseries app + mocker.patch.object(Owner._meta, "app_label", "timeseries") + + router = MultiDatabaseRouter() + assert router.db_for_read(Owner) == "timeseries_read" + assert router.db_for_read(PgSimpleMetric) == "default_read" + + @override_settings( + TIMESERIES_DATABASE_READ_REPLICA_ENABLED=False, + DATABASE_READ_REPLICA_ENABLED=False, + ) + def test_db_for_read_no_read_replica(self, mocker): + # At time of writing, the Django timeseries models don't live in this + # repo so we're pretending a different model is from the timeseries app + mocker.patch.object(Owner._meta, "app_label", "timeseries") + + router = MultiDatabaseRouter() + assert router.db_for_read(Owner) == "timeseries" + assert router.db_for_read(PgSimpleMetric) == "default" + + def test_db_for_write(self, mocker): + # At time of writing, the Django timeseries models don't live in this + # repo so we're pretending a different model is from the timeseries app + mocker.patch.object(Owner._meta, "app_label", "timeseries") + + router = MultiDatabaseRouter() + assert router.db_for_write(Owner) == "timeseries" + assert router.db_for_write(PgSimpleMetric) == "default" + + @override_settings(TIMESERIES_ENABLED=True) + def test_allow_migrate_timeseries_enabled(self): + router = MultiDatabaseRouter() + assert router.allow_migrate("timeseries", "timeseries") == True + assert router.allow_migrate("timeseries_read", "timeseries") == False + assert router.allow_migrate("timeseries", "default") == False + assert router.allow_migrate("timeseries_read", "default") == False + assert router.allow_migrate("default", "default") == True + assert router.allow_migrate("default_read", "default") == False + assert router.allow_migrate("default", "timeseries") == False + assert router.allow_migrate("default_read", "timeseries") == False + + @override_settings(TIMESERIES_ENABLED=False) + def test_allow_migrate_timeseries_disabled(self): + router = MultiDatabaseRouter() + assert router.allow_migrate("timeseries", "timeseries") == False + assert router.allow_migrate("timeseries_read", "timeseries") == False + assert router.allow_migrate("timeseries", "default") == False + assert router.allow_migrate("timeseries_read", "default") == False + assert router.allow_migrate("default", "default") == True + assert router.allow_migrate("default_read", "default") == False + assert router.allow_migrate("default", "timeseries") == False + assert router.allow_migrate("default_read", "timeseries") == False + + def test_allow_relation(self, mocker): + # At time of writing, the Django timeseries models don't live in this + # repo so we're pretending a different model is from the timeseries app + mocker.patch.object(Owner._meta, "app_label", "timeseries") + + router = MultiDatabaseRouter() + assert router.allow_relation(Owner, Owner) == True + assert router.allow_relation(PgSimpleMetric, Owner) == False + assert router.allow_relation(Owner, PgSimpleMetric) == False + assert router.allow_relation(PgSimpleMetric, PgSimpleMetric) == True diff --git a/libs/shared/tests/unit/django_apps/test_migration_utils.py b/libs/shared/tests/unit/django_apps/test_migration_utils.py new file mode 100644 index 0000000000..98be00206a --- /dev/null +++ b/libs/shared/tests/unit/django_apps/test_migration_utils.py @@ -0,0 +1,237 @@ +from django.db import models +from django.test import override_settings + +from shared.django_apps.migration_utils import * + + +class TestMigrationUtils: + def test_risky_add_field(self, mocker): + mock_forward = mocker.patch( + "shared.django_apps.migration_utils.migrations.AddField.database_forwards" + ) + mock_backward = mocker.patch( + "shared.django_apps.migration_utils.migrations.AddField.database_backwards" + ) + with override_settings(SKIP_RISKY_MIGRATION_STEPS=True): + migration = RiskyAddField("foo", "bar", None) + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert not mock_forward.called + assert not mock_backward.called + + with override_settings(SKIP_RISKY_MIGRATION_STEPS=False): + migration = RiskyAddField("foo", "bar", None) + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert mock_forward.called + assert mock_backward.called + + def test_risky_alter_field(self, mocker): + mock_forward = mocker.patch( + "shared.django_apps.migration_utils.migrations.AlterField.database_forwards" + ) + mock_backward = mocker.patch( + "shared.django_apps.migration_utils.migrations.AlterField.database_backwards" + ) + with override_settings(SKIP_RISKY_MIGRATION_STEPS=True): + migration = RiskyAlterField("foo", "bar", None) + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert not mock_forward.called + assert not mock_backward.called + + with override_settings(SKIP_RISKY_MIGRATION_STEPS=False): + migration = RiskyAlterField("foo", "bar", None) + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert mock_forward.called + assert mock_backward.called + + def test_risky_remove_field(self, mocker): + mock_forward = mocker.patch( + "shared.django_apps.migration_utils.migrations.RemoveField.database_forwards" + ) + mock_backward = mocker.patch( + "shared.django_apps.migration_utils.migrations.RemoveField.database_backwards" + ) + with override_settings(SKIP_RISKY_MIGRATION_STEPS=True): + migration = RiskyRemoveField("foo", "bar", None) + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert not mock_forward.called + assert not mock_backward.called + + with override_settings(SKIP_RISKY_MIGRATION_STEPS=False): + migration = RiskyRemoveField("foo", "bar", None) + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert mock_forward.called + assert mock_backward.called + + def test_risky_alter_unique_together(self, mocker): + mock_forward = mocker.patch( + "shared.django_apps.migration_utils.migrations.AlterUniqueTogether.database_forwards" + ) + mock_backward = mocker.patch( + "shared.django_apps.migration_utils.migrations.AlterUniqueTogether.database_backwards" + ) + with override_settings(SKIP_RISKY_MIGRATION_STEPS=True): + migration = RiskyAlterUniqueTogether("foo", "bar") + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert not mock_forward.called + assert not mock_backward.called + + with override_settings(SKIP_RISKY_MIGRATION_STEPS=False): + migration = RiskyAlterUniqueTogether("foo", "bar") + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert mock_forward.called + assert mock_backward.called + + def test_risky_alter_index_together(self, mocker): + mock_forward = mocker.patch( + "shared.django_apps.migration_utils.migrations.AlterIndexTogether.database_forwards" + ) + mock_backward = mocker.patch( + "shared.django_apps.migration_utils.migrations.AlterIndexTogether.database_backwards" + ) + with override_settings(SKIP_RISKY_MIGRATION_STEPS=True): + migration = RiskyAlterIndexTogether("foo", "bar") + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert not mock_forward.called + assert not mock_backward.called + + with override_settings(SKIP_RISKY_MIGRATION_STEPS=False): + migration = RiskyAlterIndexTogether("foo", "bar") + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert mock_forward.called + assert mock_backward.called + + def test_risky_add_index(self, mocker): + mock_forward = mocker.patch( + "shared.django_apps.migration_utils.migrations.AddIndex.database_forwards" + ) + mock_backward = mocker.patch( + "shared.django_apps.migration_utils.migrations.AddIndex.database_backwards" + ) + with override_settings(SKIP_RISKY_MIGRATION_STEPS=True): + migration = RiskyAddIndex("foo", models.Index(name="bar", fields=["id"])) + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert not mock_forward.called + assert not mock_backward.called + + with override_settings(SKIP_RISKY_MIGRATION_STEPS=False): + migration = RiskyAddIndex("foo", models.Index(name="bar", fields=["id"])) + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert mock_forward.called + assert mock_backward.called + + def test_risky_remove_index(self, mocker): + mock_forward = mocker.patch( + "shared.django_apps.migration_utils.migrations.RemoveIndex.database_forwards" + ) + mock_backward = mocker.patch( + "shared.django_apps.migration_utils.migrations.RemoveIndex.database_backwards" + ) + with override_settings(SKIP_RISKY_MIGRATION_STEPS=True): + migration = RiskyRemoveIndex("foo", "bar") + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert not mock_forward.called + assert not mock_backward.called + + with override_settings(SKIP_RISKY_MIGRATION_STEPS=False): + migration = RiskyRemoveIndex("foo", "bar") + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert mock_forward.called + assert mock_backward.called + + def test_risky_add_constraint(self, mocker): + mock_forward = mocker.patch( + "shared.django_apps.migration_utils.migrations.AddConstraint.database_forwards" + ) + mock_backward = mocker.patch( + "shared.django_apps.migration_utils.migrations.AddConstraint.database_backwards" + ) + with override_settings(SKIP_RISKY_MIGRATION_STEPS=True): + migration = RiskyAddConstraint("foo", "bar") + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert not mock_forward.called + assert not mock_backward.called + + with override_settings(SKIP_RISKY_MIGRATION_STEPS=False): + migration = RiskyAddConstraint("foo", "bar") + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert mock_forward.called + assert mock_backward.called + + def test_risky_remove_constraint(self, mocker): + mock_forward = mocker.patch( + "shared.django_apps.migration_utils.migrations.RemoveConstraint.database_forwards" + ) + mock_backward = mocker.patch( + "shared.django_apps.migration_utils.migrations.RemoveConstraint.database_backwards" + ) + with override_settings(SKIP_RISKY_MIGRATION_STEPS=True): + migration = RiskyRemoveConstraint("foo", "bar") + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert not mock_forward.called + assert not mock_backward.called + + with override_settings(SKIP_RISKY_MIGRATION_STEPS=False): + migration = RiskyRemoveConstraint("foo", "bar") + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert mock_forward.called + assert mock_backward.called + + def test_risky_run_sql(self, mocker): + mock_forward = mocker.patch( + "shared.django_apps.migration_utils.migrations.RunSQL.database_forwards" + ) + mock_backward = mocker.patch( + "shared.django_apps.migration_utils.migrations.RunSQL.database_backwards" + ) + with override_settings(SKIP_RISKY_MIGRATION_STEPS=True): + migration = RiskyRunSQL("foo", "bar", None) + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert not mock_forward.called + assert not mock_backward.called + + with override_settings(SKIP_RISKY_MIGRATION_STEPS=False): + migration = RiskyRunSQL("foo", "bar", None) + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert mock_forward.called + assert mock_backward.called + + def test_risky_run_python(self, mocker): + mock_forward = mocker.patch( + "shared.django_apps.migration_utils.migrations.RunPython.database_forwards" + ) + mock_backward = mocker.patch( + "shared.django_apps.migration_utils.migrations.RunPython.database_backwards" + ) + with override_settings(SKIP_RISKY_MIGRATION_STEPS=True): + migration = RiskyRunPython(lambda *args, **kwargs: "") + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert not mock_forward.called + assert not mock_backward.called + + with override_settings(SKIP_RISKY_MIGRATION_STEPS=False): + migration = RiskyRunPython(lambda *args, **kwargs: "") + migration.database_forwards("foo", "bar", "baz", "qux") + migration.database_backwards("foo", "bar", "baz", "qux") + assert mock_forward.called + assert mock_backward.called diff --git a/libs/shared/tests/unit/django_apps/test_model_utils.py b/libs/shared/tests/unit/django_apps/test_model_utils.py new file mode 100644 index 0000000000..04d06012a6 --- /dev/null +++ b/libs/shared/tests/unit/django_apps/test_model_utils.py @@ -0,0 +1,31 @@ +import pytest + +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory +from shared.django_apps.utils.model_utils import get_ownerid_if_member + + +class TestMigrationUtils: + @pytest.mark.django_db(databases={"default"}) + def test_get_ownerid_if_member(self): + test_owner_id = 123 + valid_owner_id = 456 + invalid_owner_id = 62139 + service = "github" + username = "test-username" + OwnerFactory( + ownerid=test_owner_id, + service=service, + private_access=True, + organizations=[valid_owner_id], + username=username, + ) + + owner_id = get_ownerid_if_member( + service=service, owner_username=username, owner_id=valid_owner_id + ) + assert owner_id == test_owner_id + + null_owner_id = get_ownerid_if_member( + service=service, owner_username=username, owner_id=invalid_owner_id + ) + assert null_owner_id is None diff --git a/libs/shared/tests/unit/encryption/__init__.py b/libs/shared/tests/unit/encryption/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/tests/unit/encryption/test_configuration.py b/libs/shared/tests/unit/encryption/test_configuration.py new file mode 100644 index 0000000000..65c701620c --- /dev/null +++ b/libs/shared/tests/unit/encryption/test_configuration.py @@ -0,0 +1,43 @@ +from shared.encryption.oauth import get_encryptor_from_configuration +from shared.encryption.selector import EncryptorDivider +from shared.encryption.standard import StandardEncryptor + + +def test_get_encryptor_from_configuration_nothing_new(mock_configuration): + res = get_encryptor_from_configuration() + assert isinstance(res, EncryptorDivider) + assert sorted(res._encryptor_mapping.keys()) == ["default_enc"] + assert all( + isinstance(x, StandardEncryptor) for x in res._encryptor_mapping.values() + ) + string_to_encode = "supercaliforgettherest" + encoded = res.encode(string_to_encode) + assert b"::" not in encoded + assert res.decode(encoded) == string_to_encode + + +def test_get_encryptor_from_configuration_full_thing(mock_configuration): + mock_configuration._params["setup"]["encryption"] = { + "keys": [ + {"name": "abc", "value": "84wvnuiho2%^Q(DIHD"}, + {"name": "def", "value": "somerandomstring_(+"}, + {"name": "ghi", "value": "i3uc8s3u%^Qdsda(ads"}, + ], + "write_key": "abc", + } + res = get_encryptor_from_configuration() + assert isinstance(res, EncryptorDivider) + assert sorted(res._encryptor_mapping.keys()) == [ + "default_enc", + "v1_abc", + "v1_def", + "v1_ghi", + ] + assert all( + isinstance(x, StandardEncryptor) for x in res._encryptor_mapping.values() + ) + string_to_encode = "supercaliforgettherest" + encoded = res.encode(string_to_encode) + assert b"::" in encoded + assert encoded.startswith(b"v1_abc") + assert res.decode(encoded) == string_to_encode diff --git a/libs/shared/tests/unit/encryption/test_selector.py b/libs/shared/tests/unit/encryption/test_selector.py new file mode 100644 index 0000000000..70a24efcec --- /dev/null +++ b/libs/shared/tests/unit/encryption/test_selector.py @@ -0,0 +1,123 @@ +import pytest + +from shared.encryption.selector import DEFAULT_ENCRYPTOR_CONSTANT, EncryptorDivider +from shared.encryption.standard import StandardEncryptor + + +def test_encrypt_decrypt(): + enc_dict = { + "abc": StandardEncryptor("part1"), + "abd": StandardEncryptor("part1", "abd", "banana"), + "plq": StandardEncryptor("r", "qwerty", "apple"), + DEFAULT_ENCRYPTOR_CONSTANT: StandardEncryptor("legacy", "things"), + } + enc = EncryptorDivider(enc_dict, "abc") + different_enc = EncryptorDivider(enc_dict, "abd") + res_encode = enc.encode("mykey123456") + assert res_encode.startswith(b"abc::") + assert different_enc.decode(res_encode) == "mykey123456" + + +def test_decode_legacy_value(): + legacy_encryptor = StandardEncryptor("legacy", "things") + enc_dict = { + "abc": StandardEncryptor("part1"), + "abd": StandardEncryptor("part1", "abd", "banana"), + "plq": StandardEncryptor("r", "qwerty", "apple"), + DEFAULT_ENCRYPTOR_CONSTANT: legacy_encryptor, + } + different_enc = EncryptorDivider(enc_dict, "abd") + res_encode = legacy_encryptor.encode("mykey123456") + assert b"::" not in res_encode + assert different_enc.decode(res_encode) == "mykey123456" + assert different_enc.decode(res_encode.decode()) == "mykey123456" + + +def test_encode_decode_all_from_divider(): + enc_dict = { + "abc": StandardEncryptor("part1"), + "abd": StandardEncryptor("part1", "abd", "banana"), + "plq": StandardEncryptor("r", "qwerty", "apple"), + DEFAULT_ENCRYPTOR_CONSTANT: StandardEncryptor("legacy", "things"), + } + enc = EncryptorDivider(enc_dict, DEFAULT_ENCRYPTOR_CONSTANT) + different_enc = EncryptorDivider(enc_dict, "abd") + res_encode = enc.encode("mykey123456") + assert b"::" not in res_encode + assert different_enc.decode(res_encode) == "mykey123456" + + +def test_init_bad_write_code(): + legacy_encryptor = StandardEncryptor("legacy", "things") + enc_dict = { + "abc": StandardEncryptor("part1"), + "abd": StandardEncryptor("part1", "abd", "banana"), + "plq": StandardEncryptor("r", "qwerty", "apple"), + DEFAULT_ENCRYPTOR_CONSTANT: legacy_encryptor, + } + with pytest.raises(Exception, match="Encryption misconfigured on write code"): + EncryptorDivider(enc_dict, "zzz") + + +def test_decrypt_token_legacy_generated(): + value = "jd3ewr8cndsbc-0wr$" + legacy_encryptor = StandardEncryptor("aruba", "jamaica") + enc_dict = { + "abc": StandardEncryptor("part1"), + "abd": StandardEncryptor("part1", "abd", "banana"), + "plq": StandardEncryptor("r", "qwerty", "apple"), + DEFAULT_ENCRYPTOR_CONSTANT: legacy_encryptor, + } + different_enc = EncryptorDivider(enc_dict, "abd") + encoded = legacy_encryptor.encode(value) + res = different_enc.decrypt_token(encoded) + assert res == {"key": "jd3ewr8cndsbc-0wr$", "secret": None} + + +def test_decrypt_token_key_normal_generated(): + value = "jd3dsfsasq$^ewr8cndsbc-0wr$" + legacy_encryptor = StandardEncryptor("aruba", "jamaica") + enc_dict = { + "abc": StandardEncryptor("part1"), + "abd": StandardEncryptor("part1", "abd", "banana"), + "plq": StandardEncryptor("r", "qwerty", "apple"), + DEFAULT_ENCRYPTOR_CONSTANT: legacy_encryptor, + } + different_enc = EncryptorDivider(enc_dict, "abd") + encoded = different_enc.encode(value) + res = different_enc.decrypt_token(encoded) + assert res == {"key": value, "secret": None} + + +def test_decrypt_token_key_normal_generated_with_secret_pair(): + value = "jd3dsfsasq$^ew:r8cndsbc-0wr$" + legacy_encryptor = StandardEncryptor("aruba", "jamaica") + enc_dict = { + "abc": StandardEncryptor("part1"), + "abd": StandardEncryptor("part1", "abd", "banana"), + "plq": StandardEncryptor("r", "qwerty", "apple"), + DEFAULT_ENCRYPTOR_CONSTANT: legacy_encryptor, + } + different_enc = EncryptorDivider(enc_dict, "abd") + encoded = different_enc.encode(value) + res = different_enc.decrypt_token(encoded) + assert res == {"key": value.split(":")[0], "secret": value.split(":")[1]} + + +def test_decrypt_token_key_normal_generated_with_secret_pair_refresh(): + value = "jd3dsfsasq$^ew: :r8cndsbc-0wr$" + legacy_encryptor = StandardEncryptor("aruba", "jamaica") + enc_dict = { + "abc": StandardEncryptor("part1"), + "abd": StandardEncryptor("part1", "abd", "banana"), + "plq": StandardEncryptor("r", "qwerty", "apple"), + DEFAULT_ENCRYPTOR_CONSTANT: legacy_encryptor, + } + different_enc = EncryptorDivider(enc_dict, "abd") + encoded = different_enc.encode(value) + res = different_enc.decrypt_token(encoded) + assert res == { + "key": "jd3dsfsasq$^ew", + "refresh_token": "r8cndsbc-0wr$", + "secret": None, + } diff --git a/libs/shared/tests/unit/encryption/test_standard.py b/libs/shared/tests/unit/encryption/test_standard.py new file mode 100644 index 0000000000..94bc457a7f --- /dev/null +++ b/libs/shared/tests/unit/encryption/test_standard.py @@ -0,0 +1,67 @@ +from shared.encryption.standard import StandardEncryptor + + +def test_standard_encryptor(): + se = StandardEncryptor( + "part1", "part2", iv=b"\xe7\xdf\x12i&`\x9f:\xce\x97\x99\xdf\xd5\xe3\xcd\x8c" + ) + word_to_encode = "mykey123456" + res = se.encode(word_to_encode) + assert res == b"598SaSZgnzrOl5nf1ePNjP9mdoj6hma717YLPiIxbCs=" + assert se.decode(res) == word_to_encode + + +def test_standard_encryptor_one_part(): + se = StandardEncryptor( + "part1", iv=b"\xb5\xfa\xc7!\xd2\x8b\x1c\x06\xf0\x1c\xa2\\\xfe\x9e\xa8\x1d" + ) + oe = StandardEncryptor( + "part1", iv=b"\xb5\xfa\xc7!\xd2\x8b\x1c\x06\xf0\x1c\xa2\\\xfe\x9e\xa8\x1d" + ) + word_to_encode = "somekey9865321" + res = se.encode(word_to_encode) + assert res == oe.encode(word_to_encode) + assert se.decode(res) == word_to_encode + assert se.decode(res) == oe.decode(res) + + +def test_standard_encryptor_three_parts(): + se = StandardEncryptor( + "part1", + "fnudbashbdsahbdahbcdcsc cxcx", + "dadsadbiyygweereeier", + iv=b"\x14\x8c\x9e\xd8\xa1\xc5\xff\x1d\x9e[\xd7\x05K\t\xf4\x95", + ) + word_to_encode = "mydsdbsdbebehbewbew123456" + res = se.encode(word_to_encode) + assert res == b"FIye2KHF/x2eW9cFSwn0lYV5MKCIr8o/Gr2/nBgat1Kb0D256PFNxphi5PfFXrCF" + assert se.decode(res) == word_to_encode + + +def test_standard_encryptor_no_iv(): + se = StandardEncryptor("part1") + assert se.decode(se.encode("mykey123456")) == "mykey123456" + + +def test_decrypt_token(): + value = "jd3ewr8cndsbc-0wr$" + se = StandardEncryptor("aruba", "jamaica") + encoded = se.encode(value) + res = se.decrypt_token(encoded) + assert res == {"key": "jd3ewr8cndsbc-0wr$", "secret": None} + + +def test_decrypt_token_key_secret_pair(): + value = "jd3ewr8cnd:sbc-0wr$" + se = StandardEncryptor("aruba", "jamaica") + encoded = se.encode(value) + res = se.decrypt_token(encoded) + assert res == {"key": "jd3ewr8cnd", "secret": "sbc-0wr$"} + + +def test_decrypt_token_key_secret_pair_refresh(): + value = "jd3ewr8cnd: :sbc-0wr$" + se = StandardEncryptor("aruba", "jamaica") + encoded = se.encode(value) + res = se.decrypt_token(encoded) + assert res == {"key": "jd3ewr8cnd", "refresh_token": "sbc-0wr$", "secret": None} diff --git a/libs/shared/tests/unit/encryption/test_token.py b/libs/shared/tests/unit/encryption/test_token.py new file mode 100644 index 0000000000..0851a3c18c --- /dev/null +++ b/libs/shared/tests/unit/encryption/test_token.py @@ -0,0 +1,54 @@ +import pytest + +from shared.encryption.token import decode_token, encode_token + + +@pytest.mark.parametrize( + "input, expected", + [ + ({"key": "some_key"}, "some_key"), + ({"key": "some_key", "secret": "some_secret"}, "some_key:some_secret"), + ( + { + "key": "some_key", + "secret": "some_secret", + "refresh_token": "refresh", + }, + "some_key:some_secret:refresh", + ), + ( + {"key": "some_key", "refresh_token": "refresh"}, + "some_key: :refresh", + ), + ], +) +def test_encode_access_token(input, expected): + assert encode_token(input) == expected + + +@pytest.mark.parametrize( + "input, expected", + [ + ("some_key", {"key": "some_key", "secret": None}), + ("some_key:some_secret", {"key": "some_key", "secret": "some_secret"}), + ( + "some_key:some_secret:refresh", + {"key": "some_key", "secret": "some_secret", "refresh_token": "refresh"}, + ), + ( + "some_key: :refresh", + {"key": "some_key", "secret": None, "refresh_token": "refresh"}, + ), + ], +) +def test_decode_access_token(input, expected): + assert decode_token(input) == expected + + +def test_decode_encode_access_token(): + token = { + "key": "some_key", + "secret": "some_secret", + "refresh_token": "refresh", + } + assert decode_token(encode_token(token)) == token diff --git a/libs/shared/tests/unit/encryption/test_yaml_secret.py b/libs/shared/tests/unit/encryption/test_yaml_secret.py new file mode 100644 index 0000000000..d8b0bea5ca --- /dev/null +++ b/libs/shared/tests/unit/encryption/test_yaml_secret.py @@ -0,0 +1,48 @@ +import pytest + +from shared.encryption.yaml_secret import get_yaml_secret_encryptor + + +@pytest.mark.parametrize( + "user_input, expected_output", + [ + ( + "v1::uwP3wUeU5tTSjHM14F1C4X372EQo8FlLIhYMVn6E5FlUBKs2CAgJCNLXx9WYxIk8VAk6b2yiffladvs0GatPdXCaauPruGbOC7Pbp2xlLl6QkWFL0cG0K5l+4o5UpwGz", + "github/44376991/308757874/Will no one rid me of this turbulent priest", + ), + ("wnb1smVbBvjHNFgEgK2RV9rfVhGygj3IAPOkWp3rO8o=", "Im a banana"), + ("default_enc::wnb1smVbBvjHNFgEgK2RV9rfVhGygj3IAPOkWp3rO8o=", "Im a banana"), + ( + "v2::FpVtGkoVY7ACYicwfD1QP3oAPOmJj+eM24l2IkWfiNMtVInE8JVNJnJZJqIKmVLy7LW479oJuIfFRWI2COIJrA==", + "gitlab/1234/876/Talkthetalkwalkthewalk", + ), + ], +) +def test_yaml_secret_cases(user_input, expected_output, mock_configuration): + assert get_yaml_secret_encryptor().decode(user_input) == expected_output + + +@pytest.mark.parametrize( + "user_config_input, user_input, expected_output", + [ + ( + "messymessysecretstuff", + "v2::ZypitOrairOs1O11UpaYuD3rLldHcu5zYjKLEAGK54QmYl2IMrP4uQ/ZOoOLaBdJO333ythhPRVHTMy7xqOIxnLc+c32oVlkt5zpjD224my4RJX5bjh0JMZ3Rgbi/4db", + "github/44376991/308757874/Will no one rid me of this turbulent priest", + ), + ( + "19JOdicq0fCF47Pjv9RsG20xj6MIz4DXkZA5aFK4uOY", + "v2::+QA+DDAeWcTLqrSPmYcV6HqIUVK+U6SzBQznO7IXjdynj4UPezqcDQYOg8dd/LEy81GpeR05VmrLWScA0N8HdQndBZH9tjb2W9h+d8MpBuIJCEVuf+q7otlzwJz6L9Yo", + "github/44376991/308757874/Will no one rid me of this turbulent priest", + ), + ("whatever", "wnb1smVbBvjHNFgEgK2RV9rfVhGygj3IAPOkWp3rO8o=", "Im a banana"), + ("whenever", "wnb1smVbBvjHNFgEgK2RV9rfVhGygj3IAPOkWp3rO8o=", "Im a banana"), + ], +) +def test_yaml_secret_cases_with_different_config( + user_config_input, user_input, expected_output, mock_configuration +): + mock_configuration._params["setup"]["encryption"] = { + "yaml_secret": user_config_input + } + assert get_yaml_secret_encryptor().decode(user_input) == expected_output diff --git a/libs/shared/tests/unit/events/test_amplitude.py b/libs/shared/tests/unit/events/test_amplitude.py new file mode 100644 index 0000000000..5380094b5c --- /dev/null +++ b/libs/shared/tests/unit/events/test_amplitude.py @@ -0,0 +1,257 @@ +from unittest.mock import Mock, patch + +from django.test import override_settings + +from shared.events.amplitude import UNKNOWN_USER_OWNERID, AmplitudeEventPublisher +from shared.events.amplitude.metrics import ( + AMPLITUDE_PUBLISH_COUNTER, + AMPLITUDE_PUBLISH_FAILURE_COUNTER, +) +from shared.events.amplitude.publisher import StubbedAmplitudeClient + + +@override_settings(AMPLITUDE_API_KEY="asdf1234") +@patch("shared.events.amplitude.publisher.EventOptions") +@patch("shared.events.amplitude.publisher.Amplitude") +def test_set_orgs(amplitude_mock, event_options_mock): + amplitude = AmplitudeEventPublisher(override_env=True) + + amplitude.client.set_group = Mock() + event_options_mock.return_value = "mock_event_options" + + amplitude.publish("set_orgs", {"user_ownerid": 123, "org_ids": [1, 32]}) + + amplitude_mock.assert_called_once() + amplitude.client.set_group.assert_called_once_with( + group_type="org", group_name=["1", "32"], event_options="mock_event_options" + ) + event_options_mock.assert_called_once_with(user_id="123") + + +@override_settings(AMPLITUDE_API_KEY="asdf1234") +@patch("shared.events.amplitude.publisher.EventOptions") +@patch("shared.events.amplitude.publisher.Amplitude") +def test_set_orgs_returns_early_when_anonymous_user(amplitude_mock, event_options_mock): + amplitude = AmplitudeEventPublisher(override_env=True) + + amplitude.client.set_group = Mock() + event_options_mock.return_value = "mock_event_options" + + amplitude.publish( + "set_orgs", {"user_ownerid": UNKNOWN_USER_OWNERID, "org_ids": [1, 32]} + ) + + amplitude_mock.assert_called_once() + amplitude.client.set_group.assert_not_called() + event_options_mock.assert_not_called() + + +@override_settings(AMPLITUDE_API_KEY="asdf1234") +@patch("shared.events.amplitude.publisher.Amplitude") +@patch("shared.events.amplitude.publisher.inc_counter") +def test_set_orgs_throws_when_missing_org_ids(mock_inc_counter, _): + amplitude = AmplitudeEventPublisher(override_env=True) + + amplitude.publish("set_orgs", {"user_ownerid": 123}) + + mock_inc_counter.assert_called_with( + AMPLITUDE_PUBLISH_FAILURE_COUNTER, + labels={"event_type": "set_orgs", "error": "MissingEventPropertyException"}, + ) + + +@override_settings(AMPLITUDE_API_KEY="asdf1234") +@patch("shared.events.amplitude.publisher.BaseEvent") +@patch("shared.events.amplitude.publisher.Amplitude") +def test_publish(amplitude_mock, base_event_mock): + amplitude = AmplitudeEventPublisher(override_env=True) + + amplitude.client.track = Mock() + + amplitude.publish("App Installed", {"user_ownerid": 123, "ownerid": 321}) + + amplitude_mock.assert_called_once() + amplitude.client.track.assert_called_once() + base_event_mock.assert_called_once_with( + "App Installed", + user_id="123", + event_properties={"ownerid": 321}, + groups={"org": 321}, + ) + + +@override_settings(AMPLITUDE_API_KEY="asdf1234") +@patch("shared.events.amplitude.publisher.BaseEvent") +@patch("shared.events.amplitude.publisher.Amplitude") +@patch("shared.events.amplitude.publisher.inc_counter") +def test_publish_increments_counter(mock_inc_counter, amplitude_mock, base_event_mock): + amplitude = AmplitudeEventPublisher(override_env=True) + + amplitude.client.track = Mock() + + amplitude.publish("App Installed", {"user_ownerid": 123, "ownerid": 321}) + + amplitude_mock.assert_called_once() + amplitude.client.track.assert_called_once() + base_event_mock.assert_called_once_with( + "App Installed", + user_id="123", + event_properties={"ownerid": 321}, + groups={"org": 321}, + ) + mock_inc_counter.assert_called_once_with( + AMPLITUDE_PUBLISH_COUNTER, labels={"event_type": "App Installed"} + ) + + +@override_settings(AMPLITUDE_API_KEY="asdf1234") +@patch("shared.events.amplitude.publisher.BaseEvent") +@patch("shared.events.amplitude.publisher.Amplitude") +def test_publish_removes_extra_properties(amplitude_mock, base_event_mock): + amplitude = AmplitudeEventPublisher(override_env=True) + + amplitude.client.track = Mock() + + amplitude.publish( + "App Installed", {"user_ownerid": 123, "ownerid": 321, "repoid": 9} + ) + + amplitude_mock.assert_called_once() + amplitude.client.track.assert_called_once() + base_event_mock.assert_called_once_with( + "App Installed", + user_id="123", + event_properties={"ownerid": 321}, + groups={"org": 321}, + ) + + +@override_settings(AMPLITUDE_API_KEY="asdf1234") +@patch("shared.events.amplitude.publisher.BaseEvent") +@patch("shared.events.amplitude.publisher.Amplitude") +def test_publish_converts_to_camel_case(amplitude_mock, base_event_mock): + amplitude = AmplitudeEventPublisher(override_env=True) + + amplitude.client.track = Mock() + + amplitude.publish( + "Upload Received", + { + "user_ownerid": 123, + "ownerid": 321, + "repoid": 132, + "commitid": 12, + "pullid": None, + "upload_type": "Coverage report", + }, + ) + + amplitude_mock.assert_called_once() + amplitude.client.track.assert_called_once() + base_event_mock.assert_called_once_with( + "Upload Received", + user_id="123", + event_properties={ + "ownerid": 321, + "repoid": 132, + "commitid": 12, + "pullid": None, + "uploadType": "Coverage report", + }, + groups={ + "org": 321, + }, + ) + + +@override_settings(AMPLITUDE_API_KEY="asdf1234") +@patch("shared.events.amplitude.publisher.BaseEvent") +@patch("shared.events.amplitude.publisher.Amplitude") +def test_publish_converts_anonymous_owner_id_to_user_id( + amplitude_mock, base_event_mock +): + amplitude = AmplitudeEventPublisher(override_env=True) + + amplitude.client.track = Mock() + + amplitude.publish( + "Upload Received", + { + "user_ownerid": UNKNOWN_USER_OWNERID, + "ownerid": 321, + "repoid": 132, + "commitid": 12, + "pullid": None, + "upload_type": "Coverage report", + }, + ) + + amplitude_mock.assert_called_once() + amplitude.client.track.assert_called_once() + base_event_mock.assert_called_once_with( + "Upload Received", + user_id="anon", + event_properties={ + "ownerid": 321, + "repoid": 132, + "commitid": 12, + "pullid": None, + "uploadType": "Coverage report", + }, + groups={ + "org": 321, + }, + ) + + +@override_settings(AMPLITUDE_API_KEY="asdf1234") +@patch("shared.events.amplitude.publisher.Amplitude") +def test_publish_fails_gracefully(amplitude_mock): + amplitude = AmplitudeEventPublisher(override_env=True) + + amplitude.client.track = Mock() + + try: + amplitude.publish("App Installed", {"user_ownerid": 123}) + except Exception: + assert False + + amplitude_mock.assert_called_once() + amplitude.client.track.assert_not_called() + + +@override_settings(AMPLITUDE_API_KEY="asdf1234") +@patch("shared.events.amplitude.publisher.Amplitude") +@patch("shared.events.amplitude.publisher.inc_counter") +def test_publish_missing_required_property(mock_inc_counter, _): + amplitude = AmplitudeEventPublisher(override_env=True) + + amplitude.client.track = Mock() + + amplitude.publish("App Installed", {"user_ownerid": 123}) + + mock_inc_counter.assert_called_with( + AMPLITUDE_PUBLISH_FAILURE_COUNTER, + labels={ + "event_type": "App Installed", + "error": "MissingEventPropertyException", + }, + ) + + +@override_settings(AMPLITUDE_API_KEY=None) +def test_uses_stubbed_amplitude_when_None_api_key(): + amplitude = AmplitudeEventPublisher(override_env=True) + + assert isinstance(amplitude.client, StubbedAmplitudeClient) + + +@override_settings(AMPLITUDE_API_KEY=None) +@patch("shared.events.amplitude.publisher.Amplitude") +def test_stubbed_amplitude_does_not_call_amplitude(amplitude_mock): + amplitude = AmplitudeEventPublisher(override_env=True) + + amplitude.publish("User Created", {"user_ownerid": 123}) + amplitude.publish("set_orgs", {"user_ownerid": 123, "org_ids": [1, 32]}) + + amplitude_mock.assert_not_called() diff --git a/libs/shared/tests/unit/github/test_github_installation_helpers.py b/libs/shared/tests/unit/github/test_github_installation_helpers.py new file mode 100644 index 0000000000..dd429224c5 --- /dev/null +++ b/libs/shared/tests/unit/github/test_github_installation_helpers.py @@ -0,0 +1,68 @@ +import pickle +from time import time +from unittest.mock import patch + +import pytest +from freezegun import freeze_time + +# This import here avoids a circular import issue +from shared.github import InvalidInstallationError, get_github_jwt_token, get_pem +from shared.utils.test_utils import mock_config_helper + + +def test_can_unpickle_invalid_installation_error(): + exception = InvalidInstallationError("permission_error") + pickled = pickle.dumps(exception) + unpickled = pickle.loads(pickled) + assert str(unpickled) == str(exception) + + +@patch("shared.github.load_pem_from_path") +def test_get_pem_from_name(mock_load_pem, mocker): + configs = {} + file_configs = {"github.integration.pem": "--------BEGIN RSA PRIVATE KEY-----..."} + mock_config_helper(mocker, configs, file_configs) + assert get_pem(pem_name="github") == "--------BEGIN RSA PRIVATE KEY-----..." + mock_load_pem.assert_not_called() + + +def test_get_pem_from_path(mocker): + configs = {} + file_configs = {"yaml.path.to.pem": "--------BEGIN RSA PRIVATE KEY-----..."} + mock_config_helper(mocker, configs, file_configs) + assert ( + get_pem(pem_path="yaml+file://yaml.path.to.pem") + == "--------BEGIN RSA PRIVATE KEY-----..." + ) + + +def test_get_pem_from_nowhere(): + with pytest.raises(Exception) as exp: + get_pem() + assert exp.exconly() == "Exception: No PEM provided to get installation token" + + +def test_get_pem_from_path_unknown_schema(): + with pytest.raises(Exception) as exp: + get_pem(pem_path="unknown_schema://some_path") + assert exp.exconly() == "Exception: Unknown schema to load PEM" + + +@freeze_time("2024-02-21T00:00:00") +@patch("shared.github.jwt") +def test_get_github_jwt_token(mock_jwt, mocker): + mock_jwt.encode.return_value = "encoded_jwt" + configs = {"github.integration.id": 15000, "github.integration.expires": 300} + file_configs = {"github.integration.pem": "--------BEGIN RSA PRIVATE KEY-----..."} + mock_config_helper(mocker, configs, file_configs) + token = get_github_jwt_token("github") + assert token == "encoded_jwt" + mock_jwt.encode.assert_called_with( + { + "iat": int(time()), + "exp": int(time()) + 300, + "iss": 15000, + }, + "--------BEGIN RSA PRIVATE KEY-----...", + algorithm="RS256", + ) diff --git a/libs/shared/tests/unit/helpers/__init__.py b/libs/shared/tests/unit/helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/tests/unit/helpers/test_cache.py b/libs/shared/tests/unit/helpers/test_cache.py new file mode 100644 index 0000000000..090a14249a --- /dev/null +++ b/libs/shared/tests/unit/helpers/test_cache.py @@ -0,0 +1,192 @@ +import unittest + +import pytest +from redis.exceptions import TimeoutError + +from shared.helpers.cache import ( + NO_VALUE, + BaseBackend, + OurOwnCache, + RedisBackend, + make_hash_sha256, +) + + +class RandomCounter(object): + def __init__(self): + self.value = 0 + + def call_function(self): + self.value += 1 + return self.value + + def call_function_args(self, base, head, something=None, danger=True): + return base + head + + async def async_call_function(self): + self.value += 2 + self.value *= 4 + return self.value + + async def async_call_function_args(self, base, head, something=None, danger=True): + return base + head + + +class FakeBackend(BaseBackend): + def __init__(self): + self.all_keys = {} + + def get(self, key): + possible_values = self.all_keys.get(key, {}) + for val in possible_values.values(): + return val + return NO_VALUE + + def set(self, key, ttl, value): + if key not in self.all_keys: + self.all_keys[key] = {} + self.all_keys[key][ttl] = value + + +class FakeRedis(object): + def __init__(self): + self.all_keys = {} + + def get(self, key): + return self.all_keys.get(key) + + def setex(self, key, expire, value): + self.all_keys[key] = value + + +class FakeRedisWithIssues(object): + def get(self, key): + raise TimeoutError() + + def setex(self, key, expire, value): + raise TimeoutError() + + +class TestRedisBackend(unittest.TestCase): + def test_simple_redis_call(self): + redis_backend = RedisBackend(FakeRedis()) + assert redis_backend.get("normal_key") == NO_VALUE + value_1 = list(set("ascdefgh")) + redis_backend.set("normal_key", 120, {"value_1": value_1, "1": [1, 3]}) + assert redis_backend.get("normal_key") == { + "value_1": value_1, + "1": [1, 3], + } + + def test_simple_redis_call_exception(self): + redis_backend = RedisBackend(FakeRedisWithIssues()) + assert redis_backend.get("normal_key") == NO_VALUE + redis_backend.set( + "normal_key", 120, {"value_1": list(set("ascdefgh")), "1": [1, 3]} + ) + assert redis_backend.get("normal_key") == NO_VALUE + + def test_simple_redis_call_not_json_serializable(self): + redis_backend = RedisBackend(FakeRedis()) + + unserializable = set("abcdefg") + redis_backend.set("normal_key", 120, unserializable) + assert redis_backend.get("normal_key") == NO_VALUE + + def test_simple_redis_call_dict_with_int_keys(self): + redis_backend = RedisBackend(FakeRedis()) + + d = {"abcde": {1: [1, 2, 3], 2: [4, 5, 6]}} + redis_backend.set("normal_key", 120, d) + assert redis_backend.get("normal_key") == NO_VALUE + + +class TestCache(object): + def test_simple_caching_no_backend_no_params(self): + cache = OurOwnCache() + sample_function = RandomCounter().call_function + cached_function = cache.cache_function()(sample_function) + assert cached_function() == 1 + assert cached_function() == 2 + assert cached_function() == 3 + + def test_simple_caching_no_backend_no_params_with_ttl(self): + cache = OurOwnCache() + sample_function = RandomCounter().call_function + cached_function = cache.cache_function(ttl=300)(sample_function) + assert cached_function() == 1 + assert cached_function() == 2 + assert cached_function() == 3 + + @pytest.mark.asyncio + async def test_simple_caching_no_backend_async_no_params(self): + cache = OurOwnCache() + sample_function = RandomCounter().async_call_function + cached_function = cache.cache_function()(sample_function) + assert (await cached_function()) == 8 + assert (await cached_function()) == 40 + assert (await cached_function()) == 168 + + def test_simple_caching_fake_backend_no_params(self): + cache = OurOwnCache() + cache.configure(FakeBackend()) + sample_function = RandomCounter().call_function + cached_function = cache.cache_function()(sample_function) + assert cached_function() == 1 + assert cached_function() == 1 + assert cached_function() == 1 + + def test_simple_caching_fake_backend_with_params(self): + cache = OurOwnCache() + cache.configure(FakeBackend()) + sample_function = RandomCounter().call_function_args + cached_function = cache.cache_function()(sample_function) + assert cached_function("base", "head", something="batata") == "basehead" + assert cached_function("base", "head", something="else") == "basehead" + assert cached_function("base", "head", something="else") == "basehead" + # Changing the way we call the function + assert cached_function("base", head="head", something="else") == "basehead" + assert cached_function("base", head="head", something="else") == "basehead" + + @pytest.mark.asyncio + async def test_simple_caching_fake_backend_async_no_params(self): + cache = OurOwnCache() + cache.configure(FakeBackend()) + sample_function = RandomCounter().async_call_function + cached_function = cache.cache_function()(sample_function) + assert (await cached_function()) == 8 + assert (await cached_function()) == 8 + assert (await cached_function()) == 8 + + @pytest.mark.asyncio + async def test_simple_caching_fake_backend_async_with_params(self): + cache = OurOwnCache() + cache.configure(FakeBackend()) + sample_function = RandomCounter().async_call_function_args + cached_function = cache.cache_function()(sample_function) + assert await cached_function("base", "head", something="batata") == "basehead" + assert await cached_function("base", "head", something="else") == "basehead" + assert await cached_function("base", "head", something="else") == "basehead" + # Changing the way we call the function + assert ( + await cached_function("base", head="head", something="else") == "basehead" + ) + assert ( + await cached_function("base", head="head", something="else") == "basehead" + ) + + @pytest.mark.asyncio + async def test_make_hash_sha256(self): + assert make_hash_sha256(1) == "a4ayc/80/OGda4BO/1o/V0etpOqiLx1JwB5S3beHW0s=" + assert ( + make_hash_sha256("somestring") + == "l5nfZJ7iQAll9QGKjGm4wPuSgUoikOMrdpOw/36GLyw=" + ) + this_set = set(["1", "something", "True", "another_string_of_values"]) + assert ( + make_hash_sha256(this_set) == "siFp5vd4+aI5SxlURDMV3Z5Yfn5qnpSbCctIewE6m44=" + ) + this_set.add("ooops") + assert ( + make_hash_sha256(this_set) == "aoU2Of3YNk0/iW1hqfSkXPbhIAzGMHCSCoxsiLI2b8U=" + ) diff --git a/libs/shared/tests/unit/helpers/test_color.py b/libs/shared/tests/unit/helpers/test_color.py new file mode 100644 index 0000000000..4abe3cba72 --- /dev/null +++ b/libs/shared/tests/unit/helpers/test_color.py @@ -0,0 +1,19 @@ +import pytest + +from shared.helpers.color import coverage_to_color + + +@pytest.mark.parametrize( + "range_low, range_high, cov, hex_val", + [ + (70, 100, 60.0, "#e05d44"), + (70, 100, 70.0, "#e05d44"), + (70, 100, 80.0, "#efa41b"), + (70, 100, 90.0, "#a3b114"), + (70, 100, 100.0, "#4c1"), + (70, 100, 99.99999, "#48cc10"), + (50, 90, 95.0, "#4c1"), + ], +) +def test_coverage_to_color(range_low, range_high, cov, hex_val): + assert coverage_to_color(range_low, range_high)(cov).hex == hex_val diff --git a/libs/shared/tests/unit/helpers/test_flag.py b/libs/shared/tests/unit/helpers/test_flag.py new file mode 100644 index 0000000000..c3e12a3a88 --- /dev/null +++ b/libs/shared/tests/unit/helpers/test_flag.py @@ -0,0 +1,48 @@ +from mock import Mock + +from shared.helpers.flag import Flag + + +def test_report(): + paths = ["a", "b"] + report = Mock( + filter=Mock(return_value=Mock(__enter__=lambda self: self, __exit__=Mock())), + yaml={"flags": {"a": {"paths": paths}}}, + ) + with Flag(report, "a").report: + report.filter.assert_called_with(paths=paths, flags=["a"]) + + +def test_totals_cached(): + assert Flag(None, "_", totals="").totals == "" + + +def test_totals(): + report = Mock( + filter=Mock( + return_value=Mock( + __enter__=lambda self: self, __exit__=Mock(), totals="" + ) + ), + yaml={}, + ) + assert Flag(report, "_").totals == "" + + +def test_apply_diff(): + apply_diff = Mock(return_value="") + report = Mock(filter=Mock(return_value=Mock(apply_diff=apply_diff)), yaml={}) + assert Flag(report, "_").apply_diff("") == "" + apply_diff.assert_called_with("", _save=False) + + +def test_carryforward_info(): + flag_no_cf_info = Flag(None, "Flag") + assert flag_no_cf_info.carriedforward is False + assert flag_no_cf_info.carriedforward_from is None + + flag_with_cf_info = Flag( + None, "Another flag", carriedforward=True, carriedforward_from="12345" + ) + assert flag_with_cf_info.carriedforward is True + assert flag_with_cf_info.carriedforward_from == "12345" diff --git a/libs/shared/tests/unit/helpers/test_max_int.py b/libs/shared/tests/unit/helpers/test_max_int.py new file mode 100644 index 0000000000..5793157aac --- /dev/null +++ b/libs/shared/tests/unit/helpers/test_max_int.py @@ -0,0 +1,8 @@ +import pytest + +from shared.helpers.numeric import maxint + + +@pytest.mark.parametrize("string, number", [("7", 7), ("1231412412", 99999)]) +def test_max_int(string, number): + assert maxint(string) == number diff --git a/libs/shared/tests/unit/helpers/test_ratio.py b/libs/shared/tests/unit/helpers/test_ratio.py new file mode 100644 index 0000000000..da95bbb7aa --- /dev/null +++ b/libs/shared/tests/unit/helpers/test_ratio.py @@ -0,0 +1,10 @@ +import pytest + +from shared.helpers.numeric import ratio + + +@pytest.mark.parametrize( + "x, y, percent", [(2, 2, "100"), (0, 2, "0"), (2, 0, "0"), (1, 2, "50.00000")] +) +def test_ratio(x, y, percent): + assert ratio(x, y) == percent diff --git a/libs/shared/tests/unit/helpers/test_redis.py b/libs/shared/tests/unit/helpers/test_redis.py new file mode 100644 index 0000000000..a006845b7a --- /dev/null +++ b/libs/shared/tests/unit/helpers/test_redis.py @@ -0,0 +1,12 @@ +from shared.helpers.redis import get_redis_url + + +def test_get_redis_default(): + assert get_redis_url() == "redis://redis:6379" + + +def test_get_redis_from_url(mock_configuration): + mock_configuration.set_params( + {"services": {"redis_url": "https://my-redis-instance:6378"}} + ) + assert get_redis_url() == "https://my-redis-instance:6378" diff --git a/libs/shared/tests/unit/helpers/test_yaml.py b/libs/shared/tests/unit/helpers/test_yaml.py new file mode 100644 index 0000000000..bb53209ce4 --- /dev/null +++ b/libs/shared/tests/unit/helpers/test_yaml.py @@ -0,0 +1,32 @@ +from types import GeneratorType + +import pytest + +from shared.helpers.yaml import default_if_true, walk + + +def test_yaml_walk(mocker): + assert walk({"a": "b"}, ("a",), "c") == "b" + assert walk({"a": {"b": "c"}}, ("a", "b"), "d") == "c" + assert walk({"a": mocker.Mock(b="banana")}, ("a", "b"), "d") == "banana" + assert walk({"a": {"_": "c"}}, ("a", "b"), "d") == "d" + assert walk({"a": {"_": "c"}}, ("a", "b"), None) is None + + +@pytest.mark.parametrize( + "a, b", + [ + (True, {"default": {}}), + (None, {}), + (False, {}), + ({"a": False, "b": True}, {"b": {}}), + ({"custom": {"enabled": False}}, {}), + ({"custom": {"enabled": True}}, {"custom": {"enabled": True}}), + ], +) +def test_default_if_true(a, b): + res = default_if_true(a) + if isinstance(res, GeneratorType): + assert dict(res) == b + else: + assert res == b diff --git a/libs/shared/tests/unit/helpers/test_zfill.py b/libs/shared/tests/unit/helpers/test_zfill.py new file mode 100644 index 0000000000..4843bd4966 --- /dev/null +++ b/libs/shared/tests/unit/helpers/test_zfill.py @@ -0,0 +1,14 @@ +import pytest + +from shared.helpers.zfill import zfill + + +@pytest.mark.parametrize( + "lst, index, value, res", + [ + ([1, 2, 3, 4, 5], 2, 10, [1, 2, 10, 4, 5]), + ([1, 2, 3], 10, 5, [1, 2, 3, None, None, None, None, None, None, None, 5]), + ], +) +def test_zfill(lst, index, value, res): + assert zfill(lst, index, value) == res diff --git a/libs/shared/tests/unit/label_analysis/__init__.py b/libs/shared/tests/unit/label_analysis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/tests/unit/label_analysis/test_enums.py b/libs/shared/tests/unit/label_analysis/test_enums.py new file mode 100644 index 0000000000..25dd7b90b8 --- /dev/null +++ b/libs/shared/tests/unit/label_analysis/test_enums.py @@ -0,0 +1,20 @@ +from shared.labelanalysis import LabelAnalysisRequestState + + +def test_enum_choices(): + assert LabelAnalysisRequestState.choices() == ( + (1, "CREATED"), + (2, "FINISHED"), + (3, "ERROR"), + ) + + +def test_enum_from_int(): + assert ( + LabelAnalysisRequestState.enum_from_int(1) == LabelAnalysisRequestState.CREATED + ) + assert ( + LabelAnalysisRequestState.enum_from_int(2) == LabelAnalysisRequestState.FINISHED + ) + assert LabelAnalysisRequestState.enum_from_int(3) == LabelAnalysisRequestState.ERROR + assert LabelAnalysisRequestState.enum_from_int(4) is None diff --git a/libs/shared/tests/unit/plan/test_plan.py b/libs/shared/tests/unit/plan/test_plan.py new file mode 100644 index 0000000000..8413e086ea --- /dev/null +++ b/libs/shared/tests/unit/plan/test_plan.py @@ -0,0 +1,1208 @@ +from datetime import datetime, timedelta +from unittest.mock import patch + +from django.test import TestCase, override_settings +from freezegun import freeze_time + +from shared.django_apps.codecov.commands.exceptions import ValidationError +from shared.django_apps.codecov_auth.models import Plan, Service +from shared.django_apps.codecov_auth.tests.factories import ( + OwnerFactory, + PlanFactory, + TierFactory, +) +from shared.plan.constants import ( + DEFAULT_FREE_PLAN, + TRIAL_PLAN_SEATS, + PlanName, + TierName, + TrialDaysAmount, + TrialStatus, +) +from shared.plan.service import PlanService +from tests.helper import mock_all_plans_and_tiers + + +@freeze_time("2023-06-19") +class PlanServiceTests(TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + + def test_plan_service_trial_status_not_started(self): + current_org = OwnerFactory(plan=DEFAULT_FREE_PLAN) + plan_service = PlanService(current_org=current_org) + + assert plan_service.trial_status == TrialStatus.NOT_STARTED.value + + def test_plan_service_trial_status_expired(self): + trial_start_date = datetime.utcnow() + trial_end_date_expired = trial_start_date - timedelta(days=1) + current_org = OwnerFactory( + plan=DEFAULT_FREE_PLAN, + trial_start_date=trial_start_date, + trial_end_date=trial_end_date_expired, + trial_status=TrialStatus.EXPIRED.value, + ) + plan_service = PlanService(current_org=current_org) + + assert plan_service.trial_status == TrialStatus.EXPIRED.value + + def test_plan_service_trial_status_ongoing(self): + trial_start_date = datetime.utcnow() + trial_end_date_ongoing = trial_start_date + timedelta(days=5) + current_org = OwnerFactory( + plan=PlanName.TRIAL_PLAN_NAME.value, + trial_start_date=trial_start_date, + trial_end_date=trial_end_date_ongoing, + trial_status=TrialStatus.ONGOING.value, + ) + plan_service = PlanService(current_org=current_org) + + assert plan_service.trial_status == TrialStatus.ONGOING.value + assert plan_service.is_org_trialing == True + + def test_plan_service_expire_trial_when_upgrading_successful_if_trial_is_not_started( + self, + ): + current_org_with_ongoing_trial = OwnerFactory( + plan=DEFAULT_FREE_PLAN, + trial_start_date=None, + trial_end_date=None, + trial_status=TrialStatus.NOT_STARTED.value, + ) + plan_service = PlanService(current_org=current_org_with_ongoing_trial) + plan_service.expire_trial_when_upgrading() + assert current_org_with_ongoing_trial.trial_status == TrialStatus.EXPIRED.value + assert current_org_with_ongoing_trial.plan_activated_users is None + assert current_org_with_ongoing_trial.plan_user_count == 1 + assert current_org_with_ongoing_trial.trial_end_date == datetime.utcnow() + + def test_plan_service_expire_trial_when_upgrading_successful_if_trial_is_ongoing( + self, + ): + trial_start_date = datetime.utcnow() + trial_end_date_ongoing = trial_start_date + timedelta(days=5) + current_org_with_ongoing_trial = OwnerFactory( + plan=DEFAULT_FREE_PLAN, + trial_start_date=trial_start_date, + trial_end_date=trial_end_date_ongoing, + trial_status=TrialStatus.ONGOING.value, + ) + plan_service = PlanService(current_org=current_org_with_ongoing_trial) + plan_service.expire_trial_when_upgrading() + assert current_org_with_ongoing_trial.trial_status == TrialStatus.EXPIRED.value + assert current_org_with_ongoing_trial.plan_activated_users is None + assert current_org_with_ongoing_trial.plan_user_count == 1 + assert current_org_with_ongoing_trial.trial_end_date == datetime.utcnow() + + def test_plan_service_expire_trial_users_pretrial_users_count_if_existing( + self, + ): + trial_start_date = datetime.utcnow() + trial_end_date_ongoing = trial_start_date + timedelta(days=5) + pretrial_users_count = 5 + current_org_with_ongoing_trial = OwnerFactory( + plan=DEFAULT_FREE_PLAN, + trial_start_date=trial_start_date, + trial_end_date=trial_end_date_ongoing, + trial_status=TrialStatus.ONGOING.value, + pretrial_users_count=pretrial_users_count, + ) + plan_service = PlanService(current_org=current_org_with_ongoing_trial) + plan_service.expire_trial_when_upgrading() + assert current_org_with_ongoing_trial.trial_status == TrialStatus.EXPIRED.value + assert current_org_with_ongoing_trial.plan_activated_users is None + assert current_org_with_ongoing_trial.plan_user_count == pretrial_users_count + assert current_org_with_ongoing_trial.trial_end_date == datetime.utcnow() + + def test_plan_service_start_trial_errors_if_status_is_ongoing(self): + trial_start_date = datetime.utcnow() + trial_end_date = trial_start_date + timedelta( + days=TrialDaysAmount.CODECOV_SENTRY.value + ) + current_org = OwnerFactory( + plan=DEFAULT_FREE_PLAN, + trial_start_date=trial_start_date, + trial_end_date=trial_end_date, + trial_status=TrialStatus.ONGOING.value, + ) + plan_service = PlanService(current_org=current_org) + current_owner = OwnerFactory() + + with self.assertRaises(ValidationError): + plan_service.start_trial(current_owner=current_owner) + + def test_plan_service_start_trial_errors_if_status_is_expired(self): + trial_start_date = datetime.utcnow() + trial_end_date = trial_start_date + timedelta(days=-1) + current_org = OwnerFactory( + plan=DEFAULT_FREE_PLAN, + trial_start_date=trial_start_date, + trial_end_date=trial_end_date, + trial_status=TrialStatus.EXPIRED.value, + ) + plan_service = PlanService(current_org=current_org) + current_owner = OwnerFactory() + + with self.assertRaises(ValidationError): + plan_service.start_trial(current_owner=current_owner) + + def test_plan_service_start_trial_errors_if_status_is_cannot_trial(self): + current_org = OwnerFactory( + plan=DEFAULT_FREE_PLAN, + trial_start_date=None, + trial_end_date=None, + trial_status=TrialStatus.CANNOT_TRIAL.value, + ) + plan_service = PlanService(current_org=current_org) + current_owner = OwnerFactory() + + with self.assertRaises(ValidationError): + plan_service.start_trial(current_owner=current_owner) + + def test_plan_service_start_trial_errors_owners_plan_is_not_a_free_plan(self): + current_org = OwnerFactory( + plan=PlanName.CODECOV_PRO_MONTHLY.value, + trial_start_date=None, + trial_end_date=None, + trial_status=TrialStatus.CANNOT_TRIAL.value, + ) + plan_service = PlanService(current_org=current_org) + current_owner = OwnerFactory() + + with self.assertRaises(ValidationError): + plan_service.start_trial(current_owner=current_owner) + + def test_plan_service_start_trial_succeeds_if_trial_has_not_started(self): + trial_start_date = None + trial_end_date = None + plan_user_count = 5 + current_org = OwnerFactory( + plan=DEFAULT_FREE_PLAN, + trial_start_date=trial_start_date, + trial_end_date=trial_end_date, + trial_status=TrialStatus.NOT_STARTED.value, + plan_user_count=plan_user_count, + ) + plan_service = PlanService(current_org=current_org) + current_owner = OwnerFactory() + + plan_service.start_trial(current_owner=current_owner) + assert current_org.trial_start_date == datetime.utcnow() + assert current_org.trial_end_date == datetime.utcnow() + timedelta( + days=TrialDaysAmount.CODECOV_SENTRY.value + ) + assert current_org.trial_status == TrialStatus.ONGOING.value + assert current_org.plan == PlanName.TRIAL_PLAN_NAME.value + assert current_org.pretrial_users_count == plan_user_count + assert current_org.plan_user_count == TRIAL_PLAN_SEATS + assert current_org.plan_auto_activate == True + assert current_org.trial_fired_by == current_owner.ownerid + + def test_plan_service_start_trial_manually(self): + trial_start_date = None + trial_end_date = None + plan_user_count = 5 + current_org = OwnerFactory( + plan=DEFAULT_FREE_PLAN, + trial_start_date=trial_start_date, + trial_end_date=trial_end_date, + trial_status=TrialStatus.NOT_STARTED.value, + plan_user_count=plan_user_count, + ) + plan_service = PlanService(current_org=current_org) + current_owner = OwnerFactory() + + plan_service.start_trial_manually( + current_owner=current_owner, end_date="2024-01-01 00:00:00" + ) + assert current_org.trial_start_date == datetime.utcnow() + assert current_org.trial_end_date == "2024-01-01 00:00:00" + assert current_org.trial_status == TrialStatus.ONGOING.value + assert current_org.plan == PlanName.TRIAL_PLAN_NAME.value + assert current_org.pretrial_users_count == plan_user_count + assert current_org.plan_user_count == TRIAL_PLAN_SEATS + assert current_org.plan_auto_activate == True + assert current_org.trial_fired_by == current_owner.ownerid + + def test_plan_service_start_trial_manually_already_on_paid_plan(self): + current_org = OwnerFactory( + plan=PlanName.CODECOV_PRO_MONTHLY.value, + trial_start_date=None, + trial_end_date=None, + trial_status=TrialStatus.NOT_STARTED.value, + ) + plan_service = PlanService(current_org=current_org) + current_owner = OwnerFactory() + + with self.assertRaises(ValidationError): + plan_service.start_trial_manually( + current_owner=current_owner, end_date="2024-01-01 00:00:00" + ) + + def test_plan_service_returns_plan_data_for_non_trial_developer_plan(self): + trial_start_date = None + trial_end_date = None + current_org = OwnerFactory( + plan=DEFAULT_FREE_PLAN, + trial_start_date=trial_start_date, + trial_end_date=trial_end_date, + ) + plan_service = PlanService(current_org=current_org) + developer_plan = Plan.objects.get(name=DEFAULT_FREE_PLAN) + assert plan_service.current_org == current_org + assert plan_service.trial_status == TrialStatus.NOT_STARTED.value + assert plan_service.marketing_name == developer_plan.marketing_name + assert plan_service.plan_name == developer_plan.name + assert plan_service.tier_name == developer_plan.tier.tier_name + assert plan_service.billing_rate == developer_plan.billing_rate + assert plan_service.base_unit_price == developer_plan.base_unit_price + assert plan_service.benefits == developer_plan.benefits + assert ( + plan_service.monthly_uploads_limit == developer_plan.monthly_uploads_limit + ) # should be 250 + assert plan_service.monthly_uploads_limit == 250 + + def test_plan_service_returns_plan_data_for_trialing_user_trial_plan(self): + trial_start_date = datetime.utcnow() + trial_end_date = datetime.utcnow() + timedelta( + days=TrialDaysAmount.CODECOV_SENTRY.value + ) + current_org = OwnerFactory( + plan=PlanName.TRIAL_PLAN_NAME.value, + trial_start_date=trial_start_date, + trial_end_date=trial_end_date, + trial_status=TrialStatus.ONGOING.value, + ) + plan_service = PlanService(current_org=current_org) + + trial_plan = Plan.objects.get(name=PlanName.TRIAL_PLAN_NAME.value) + assert plan_service.trial_status == TrialStatus.ONGOING.value + assert plan_service.marketing_name == trial_plan.marketing_name + assert plan_service.plan_name == trial_plan.name + assert plan_service.tier_name == trial_plan.tier.tier_name + assert plan_service.billing_rate == trial_plan.billing_rate + assert plan_service.base_unit_price == trial_plan.base_unit_price + assert plan_service.benefits == trial_plan.benefits + assert plan_service.monthly_uploads_limit is None # Not 250 since it's trialing + + def test_plan_service_sets_default_plan_data_values_correctly(self): + current_org = OwnerFactory( + plan=PlanName.CODECOV_PRO_MONTHLY.value, + stripe_subscription_id="test-sub-123", + plan_user_count=20, + plan_activated_users=[44], + plan_auto_activate=False, + ) + current_org.save() + + plan_service = PlanService(current_org=current_org) + plan_service.set_default_plan_data() + + assert current_org.plan == DEFAULT_FREE_PLAN + assert current_org.plan_user_count == 1 + assert current_org.plan_activated_users is None + assert current_org.stripe_subscription_id is None + + def test_plan_service_returns_if_owner_has_trial_dates(self): + current_org = OwnerFactory( + plan=PlanName.CODECOV_PRO_MONTHLY.value, + trial_start_date=datetime.utcnow(), + trial_end_date=datetime.utcnow() + timedelta(days=14), + ) + current_org.save() + + plan_service = PlanService(current_org=current_org) + + assert plan_service.has_trial_dates == True + + def test_plan_service_gitlab_with_root_org(self): + root_owner_org = OwnerFactory( + service=Service.GITLAB.value, + plan=PlanName.FREE_PLAN_NAME.value, + plan_user_count=1, + service_id="1234", + ) + middle_org = OwnerFactory( + service=Service.GITLAB.value, + service_id="5678", + parent_service_id=root_owner_org.service_id, + ) + child_owner_org = OwnerFactory( + service=Service.GITLAB.value, + plan=PlanName.CODECOV_PRO_MONTHLY.value, + plan_user_count=20, + parent_service_id=middle_org.service_id, + ) + # root_plan and child_plan should be the same + root_plan = PlanService(current_org=root_owner_org) + child_plan = PlanService(current_org=child_owner_org) + + assert root_plan.is_pro_plan == child_plan.is_pro_plan == False + assert root_plan.plan_user_count == child_plan.plan_user_count == 1 + assert ( + root_plan.plan_name == child_plan.plan_name == PlanName.FREE_PLAN_NAME.value + ) + + def test_plan_service_activated_user_count_includes_student_users(self): + student_user = OwnerFactory(student=True) + other_user = OwnerFactory() + current_org = OwnerFactory( + plan=PlanName.CODECOV_PRO_MONTHLY.value, + plan_activated_users=[student_user.ownerid, other_user.ownerid], + plan_auto_activate=False, + plan_user_count=2, + ) + current_org.save() + + plan = PlanService(current_org=current_org) + + assert plan.has_seats_left == True + + def test_plan_service_activated_user_count_includes_student_users_and_has_no_seats_left( + self, + ): + student_user = OwnerFactory(student=True) + other_user = OwnerFactory() + other_user_2 = OwnerFactory() + current_org = OwnerFactory( + plan=PlanName.CODECOV_PRO_MONTHLY.value, + plan_activated_users=[ + student_user.ownerid, + other_user.ownerid, + other_user_2.ownerid, + ], + plan_auto_activate=False, + plan_user_count=2, + ) + current_org.save() + + plan = PlanService(current_org=current_org) + + assert plan.has_seats_left == False + + +class AvailablePlansBeforeTrial(TestCase): + """ + - DEFAULT_FREE_PLAN, no trial -> users-pr-inappm/y, DEFAULT_FREE_PLAN + - users-free, no trial -> users-pr-inappm/y, DEFAULT_FREE_PLAN, users-free + - users-teamm/y, no trial -> users-pr-inappm/y, DEFAULT_FREE_PLAN, users-teamm/y + - users-pr-inappm/y, no trial -> users-pr-inappm/y, DEFAULT_FREE_PLAN + - sentry customer, DEFAULT_FREE_PLAN, no trial -> users-pr-inappm/y, users-sentrym/y, DEFAULT_FREE_PLAN + - sentry customer, users-teamm/y, no trial -> users-pr-inappm/y, users-sentrym/y, DEFAULT_FREE_PLAN, users-teamm/y + - sentry customer, users-sentrym/y, no trial -> users-pr-inappm/y, users-sentrym/y, DEFAULT_FREE_PLAN + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + + def setUp(self): + self.current_org = OwnerFactory( + trial_start_date=None, + trial_end_date=None, + trial_status=TrialStatus.NOT_STARTED.value, + ) + self.owner = OwnerFactory() + + def test_available_plans_for_developer_plan_non_trial( + self, + ): + self.current_org.plan = DEFAULT_FREE_PLAN + self.current_org.save() + + plan_service = PlanService(current_org=self.current_org) + + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + + assert ( + set(plan.name for plan in plan_service.available_plans(owner=self.owner)) + == expected_result + ) + + def test_available_plans_for_free_plan_non_trial( + self, + ): + self.current_org.plan = PlanName.FREE_PLAN_NAME.value + self.current_org.save() + + plan_service = PlanService(current_org=self.current_org) + + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.FREE_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + + assert ( + set(plan.name for plan in plan_service.available_plans(owner=self.owner)) + == expected_result + ) + + def test_available_plans_for_team_plan_non_trial( + self, + ): + self.current_org.plan = PlanName.TEAM_MONTHLY.value + self.current_org.save() + + plan_service = PlanService(current_org=self.current_org) + + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + + assert ( + set(plan.name for plan in plan_service.available_plans(owner=self.owner)) + == expected_result + ) + + def test_available_plans_for_pro_plan_non_trial(self): + self.current_org.plan = PlanName.CODECOV_PRO_MONTHLY.value + self.current_org.save() + + plan_service = PlanService(current_org=self.current_org) + + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + + assert ( + set(plan.name for plan in plan_service.available_plans(owner=self.owner)) + == expected_result + ) + + @patch("shared.plan.service.is_sentry_user") + def test_available_plans_for_sentry_customer_developer_plan_non_trial( + self, is_sentry_user + ): + is_sentry_user.return_value = True + self.current_org.plan = DEFAULT_FREE_PLAN + self.current_org.save() + + plan_service = PlanService(current_org=self.current_org) + + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + + assert ( + set(plan.name for plan in plan_service.available_plans(owner=self.owner)) + == expected_result + ) + + @patch("shared.plan.service.is_sentry_user") + def test_available_plans_for_sentry_customer_team_plan_non_trial( + self, is_sentry_user + ): + is_sentry_user.return_value = True + self.current_org.plan = PlanName.TEAM_MONTHLY.value + self.current_org.save() + + plan_service = PlanService(current_org=self.current_org) + + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + assert ( + set(plan.name for plan in plan_service.available_plans(owner=self.owner)) + == expected_result + ) + + @patch("shared.plan.service.is_sentry_user") + def test_available_plans_for_sentry_plan_non_trial(self, is_sentry_user): + is_sentry_user.return_value = True + self.current_org.plan = PlanName.SENTRY_MONTHLY.value + self.current_org.save() + + plan_service = PlanService(current_org=self.current_org) + + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + + assert ( + set(plan.name for plan in plan_service.available_plans(owner=self.owner)) + == expected_result + ) + + @patch("shared.plan.service.is_sentry_user") + def test_available_plans_for_sentry_plan_not_sentry_user(self, is_sentry_user): + is_sentry_user.return_value = False + self.current_org.plan = PlanName.SENTRY_MONTHLY.value + self.current_org.save() + + plan_service = PlanService(current_org=self.current_org) + + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + + assert ( + set(plan.name for plan in plan_service.available_plans(owner=self.owner)) + == expected_result + ) + + +@freeze_time("2023-06-19") +class AvailablePlansExpiredTrialLessThanTenUsers(TestCase): + """ + - {DEFAULT_FREE_PLAN}, has trialed, less than 10 users -> users-pr-inappm/y, DEFAULT_FREE_PLAN, users-teamm/y + - users-teamm/y, has trialed, less than 10 users -> users-pr-inappm/y, DEFAULT_FREE_PLAN, users-teamm/y + - users-pr-inappm/y, has trialed, less than 10 users -> users-pr-inappm/y, DEFAULT_FREE_PLAN, users-teamm/y + - sentry customer, DEFAULT_FREE_PLAN, has trialed, less than 10 users -> users-pr-inappm/y, users-sentrym/y, DEFAULT_FREE_PLAN, users-teamm/y + - sentry customer, users-teamm/y, has trialed, less than 10 users -> users-pr-inappm/y, users-sentrym/y, DEFAULT_FREE_PLAN, users-teamm/y + - sentry customer, users-sentrym/y, has trialed, less than 10 users -> users-pr-inappm/y, users-sentrym/y, DEFAULT_FREE_PLAN, users-teamm/y + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + + def setUp(self): + self.current_org = OwnerFactory( + trial_start_date=datetime.utcnow() + timedelta(days=-10), + trial_end_date=datetime.utcnow() + timedelta(days=-3), + trial_status=TrialStatus.EXPIRED.value, + plan_user_count=3, + ) + self.owner = OwnerFactory() + + def test_available_plans_for_developer_plan_expired_trial_less_than_10_users( + self, + ): + self.current_org.plan = DEFAULT_FREE_PLAN + self.current_org.save() + + plan_service = PlanService(current_org=self.current_org) + + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + assert ( + set(plan.name for plan in plan_service.available_plans(owner=self.owner)) + == expected_result + ) + + def test_available_plans_for_team_plan_expired_trial_less_than_10_users( + self, + ): + self.current_org.plan = PlanName.TEAM_MONTHLY.value + self.current_org.save() + + plan_service = PlanService(current_org=self.current_org) + + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + assert ( + set(plan.name for plan in plan_service.available_plans(owner=self.owner)) + == expected_result + ) + + def test_available_plans_for_pro_plan_expired_trial_less_than_10_users(self): + self.current_org.plan = PlanName.CODECOV_PRO_MONTHLY.value + self.current_org.save() + + plan_service = PlanService(current_org=self.current_org) + + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + + assert ( + set(plan.name for plan in plan_service.available_plans(owner=self.owner)) + == expected_result + ) + + @patch("shared.plan.service.is_sentry_user") + def test_available_plans_for_sentry_customer_developer_plan_expired_trial_less_than_10_users( + self, is_sentry_user + ): + is_sentry_user.return_value = True + self.current_org.plan = DEFAULT_FREE_PLAN + self.current_org.save() + + plan_service = PlanService(current_org=self.current_org) + + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + + assert ( + set(plan.name for plan in plan_service.available_plans(owner=self.owner)) + == expected_result + ) + + @patch("shared.plan.service.is_sentry_user") + def test_available_plans_for_sentry_customer_team_plan_expired_trial_less_than_10_users( + self, is_sentry_user + ): + is_sentry_user.return_value = True + self.current_org.plan = PlanName.TEAM_MONTHLY.value + self.current_org.save() + + plan_service = PlanService(current_org=self.current_org) + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + + assert ( + set(plan.name for plan in plan_service.available_plans(owner=self.owner)) + == expected_result + ) + + @patch("shared.plan.service.is_sentry_user") + def test_available_plans_for_sentry_plan_expired_trial_less_than_10_users( + self, is_sentry_user + ): + is_sentry_user.return_value = True + self.current_org.plan = PlanName.SENTRY_MONTHLY.value + self.current_org.save() + + plan_service = PlanService(current_org=self.current_org) + + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + + assert ( + set(plan.name for plan in plan_service.available_plans(owner=self.owner)) + == expected_result + ) + + +@freeze_time("2023-06-19") +class AvailablePlansExpiredTrialMoreThanTenActivatedUsers(TestCase): + """ + - users-pr-inappm/y, has trialed, more than 10 activated users -> users-pr-inappm/y, DEFAULT_FREE_PLAN + - sentry customer, DEFAULT_FREE_PLAN, has trialed, more than 10 activated users -> users-pr-inappm/y, users-sentrym/y, DEFAULT_FREE_PLAN + - sentry customer, users-sentrym/y, has trialed, more than 10 activated users -> users-pr-inappm/y, users-sentrym/y, DEFAULT_FREE_PLAN + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + + def setUp(self): + self.current_org = OwnerFactory( + trial_start_date=datetime.utcnow() + timedelta(days=-10), + trial_end_date=datetime.utcnow() + timedelta(days=-3), + trial_status=TrialStatus.EXPIRED.value, + plan_user_count=1, + plan_activated_users=[i for i in range(13)], + ) + self.owner = OwnerFactory() + + def test_available_plans_for_pro_plan_expired_trial_more_than_10_users(self): + self.current_org.plan = PlanName.CODECOV_PRO_MONTHLY.value + self.current_org.save() + + plan_service = PlanService(current_org=self.current_org) + + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + } + + assert ( + set(plan.name for plan in plan_service.available_plans(owner=self.owner)) + == expected_result + ) + + @patch("shared.plan.service.is_sentry_user") + def test_available_plans_for_sentry_customer_developer_plan_expired_trial_more_than_10_users( + self, is_sentry_user + ): + is_sentry_user.return_value = True + self.current_org.plan = DEFAULT_FREE_PLAN + self.current_org.save() + + plan_service = PlanService(current_org=self.current_org) + + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + } + assert ( + set(plan.name for plan in plan_service.available_plans(owner=self.owner)) + == expected_result + ) + + @patch("shared.plan.service.is_sentry_user") + def test_available_plans_for_sentry_plan_expired_trial_more_than_10_users( + self, is_sentry_user + ): + is_sentry_user.return_value = True + self.current_org.plan = PlanName.SENTRY_MONTHLY.value + self.current_org.save() + + plan_service = PlanService(current_org=self.current_org) + + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + } + + assert ( + set(plan.name for plan in plan_service.available_plans(owner=self.owner)) + == expected_result + ) + + +@freeze_time("2023-06-19") +class AvailablePlansExpiredTrialMoreThanTenSeatsLessThanTenActivatedUsers(TestCase): + """ + Tests that what matters for Team plan is activated users not the total seat count + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + + def setUp(self): + self.expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + + def test_currently_team_plan(self): + self.current_org = OwnerFactory( + plan_user_count=100, + plan_activated_users=[i for i in range(10)], + plan=PlanName.TEAM_MONTHLY.value, + ) + self.owner = OwnerFactory() + self.plan_service = PlanService(current_org=self.current_org) + + assert ( + set( + plan.name + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == self.expected_result + ) + + def test_trial_expired(self): + self.current_org = OwnerFactory( + plan_user_count=100, + plan_activated_users=[i for i in range(10)], + trial_status=TrialStatus.EXPIRED.value, + trial_start_date=datetime.utcnow() + timedelta(days=-10), + trial_end_date=datetime.utcnow() + timedelta(days=-3), + ) + self.owner = OwnerFactory() + self.plan_service = PlanService(current_org=self.current_org) + + assert ( + set( + plan.name + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == self.expected_result + ) + + def test_trial_ongoing(self): + self.current_org = OwnerFactory( + plan_user_count=100, + plan_activated_users=[i for i in range(10)], + trial_status=TrialStatus.ONGOING.value, + trial_start_date=datetime.utcnow() + timedelta(days=-10), + trial_end_date=datetime.utcnow() + timedelta(days=3), + ) + self.owner = OwnerFactory() + self.plan_service = PlanService(current_org=self.current_org) + + assert ( + set( + plan.name + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == self.expected_result + ) + + def test_trial_not_started(self): + self.current_org = OwnerFactory( + plan_user_count=100, + plan_activated_users=[i for i in range(10)], + trial_status=TrialStatus.NOT_STARTED.value, + ) + self.owner = OwnerFactory() + self.plan_service = PlanService(current_org=self.current_org) + + self.expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + + assert ( + set( + plan.name + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == self.expected_result + ) + + +@freeze_time("2023-06-19") +class AvailablePlansOngoingTrial(TestCase): + """ + Non Sentry User is trialing + when <=10 activated seats -> users-pr-inappm/y, DEFAULT_FREE_PLAN, users-teamm/y + when > 10 activated seats -> users-pr-inappm/y, DEFAULT_FREE_PLAN + Sentry User is trialing + when <=10 activated seats -> users-pr-inappm/y, users-sentrym/y, DEFAULT_FREE_PLAN, users-teamm/y + when > 10 activated seats -> users-pr-inappm/y, users-sentrym/y, DEFAULT_FREE_PLAN + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + + def setUp(self): + self.current_org = OwnerFactory( + plan=DEFAULT_FREE_PLAN, + trial_start_date=datetime.utcnow(), + trial_end_date=datetime.utcnow() + timedelta(days=14), + trial_status=TrialStatus.ONGOING.value, + plan_user_count=1000, + plan_activated_users=None, + ) + self.owner = OwnerFactory() + self.plan_service = PlanService(current_org=self.current_org) + + def test_non_sentry_user(self): + # [Developer, Pro Monthly, Pro Yearly, Team Monthly, Team Yearly] + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + + # Can do Team plan when plan_activated_users is null + assert ( + set( + plan.name + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) + + self.current_org.plan_activated_users = [i for i in range(10)] + self.current_org.save() + + # Can do Team plan when at 10 activated users + assert ( + set( + plan.name + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) + + self.current_org.plan_activated_users = [i for i in range(11)] + self.current_org.save() + + # [Developer, Pro Monthly, Pro Yearly] + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.CODECOV_PRO_MONTHLY.value, + } + + # Can not do Team plan when at 11 activated users + assert ( + set( + plan.name + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) + + @patch("shared.plan.service.is_sentry_user") + def test_sentry_user(self, is_sentry_user): + self.current_org.plan = PlanName.SENTRY_MONTHLY.value + self.current_org.save() + + is_sentry_user.return_value = True + + # [Developer, Pro Monthly, Pro Yearly, Sentry Monthly, Sentry Yearly, Team Monthly, Team Yearly] + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } + # Can do Team plan when plan_activated_users is null + assert ( + set( + plan.name + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) + + self.current_org.plan_activated_users = [i for i in range(10)] + self.current_org.save() + + # Can do Team plan when at 10 activated users + assert ( + set( + plan.name + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) + + self.current_org.plan_activated_users = [i for i in range(11)] + self.current_org.save() + + # [Developer, Pro Monthly, Pro Yearly, Sentry Monthly, Sentry Yearly] + expected_result = { + DEFAULT_FREE_PLAN, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + } + + # Can not do Team plan when at 11 activated users + assert ( + set( + plan.name + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) + + +@override_settings(IS_ENTERPRISE=False) +class PlanServiceIs___PlanTests(TestCase): + def test_is_trial_plan(self): + tier = TierFactory(tier_name=TierName.TRIAL.value) + plan = PlanFactory( + tier=tier, + name=PlanName.TRIAL_PLAN_NAME.value, + paid_plan=False, + ) + self.current_org = OwnerFactory( + plan=plan.name, + trial_start_date=datetime.utcnow(), + trial_end_date=datetime.utcnow() + timedelta(days=14), + trial_status=TrialStatus.ONGOING.value, + plan_user_count=1000, + plan_activated_users=None, + ) + self.owner = OwnerFactory() + self.plan_service = PlanService(current_org=self.current_org) + + assert self.plan_service.is_trial_plan == True + assert self.plan_service.is_sentry_plan == False + assert self.plan_service.is_team_plan == False + assert self.plan_service.is_free_plan == False + assert self.plan_service.is_pro_plan == False + assert self.plan_service.is_enterprise_plan == False + assert self.plan_service.is_pr_billing_plan == True + + def test_is_team_plan(self): + tier = TierFactory(tier_name=TierName.TEAM.value) + plan = PlanFactory( + tier=tier, + name=PlanName.TEAM_MONTHLY.value, + paid_plan=True, + ) + self.current_org = OwnerFactory( + plan=plan.name, + trial_status=TrialStatus.EXPIRED.value, + ) + self.owner = OwnerFactory() + self.plan_service = PlanService(current_org=self.current_org) + + assert self.plan_service.is_trial_plan == False + assert self.plan_service.is_sentry_plan == False + assert self.plan_service.is_team_plan == True + assert self.plan_service.is_free_plan == False + assert self.plan_service.is_pro_plan == False + assert self.plan_service.is_enterprise_plan == False + assert self.plan_service.is_pr_billing_plan == True + + def test_is_sentry_plan(self): + tier = TierFactory(tier_name=TierName.SENTRY.value) + plan = PlanFactory( + tier=tier, + name=PlanName.SENTRY_MONTHLY.value, + paid_plan=True, + ) + self.current_org = OwnerFactory( + plan=plan.name, + trial_status=TrialStatus.EXPIRED.value, + ) + self.owner = OwnerFactory() + self.plan_service = PlanService(current_org=self.current_org) + + assert self.plan_service.is_trial_plan == False + assert self.plan_service.is_sentry_plan == True + assert self.plan_service.is_team_plan == False + assert self.plan_service.is_free_plan == False + assert self.plan_service.is_pro_plan == True + assert self.plan_service.is_enterprise_plan == False + assert self.plan_service.is_pr_billing_plan == True + + def test_is_free_plan(self): + tier = TierFactory(tier_name=TierName.BASIC.value) + plan = PlanFactory( + tier=tier, + name=PlanName.FREE_PLAN_NAME.value, + paid_plan=False, + ) + self.current_org = OwnerFactory( + plan=plan.name, + ) + self.owner = OwnerFactory() + self.plan_service = PlanService(current_org=self.current_org) + + assert self.plan_service.is_trial_plan == False + assert self.plan_service.is_sentry_plan == False + assert self.plan_service.is_team_plan == False + assert self.plan_service.is_free_plan == True + assert self.plan_service.is_pro_plan == False + assert self.plan_service.is_enterprise_plan == False + assert self.plan_service.is_pr_billing_plan == True + + def test_is_pro_plan(self): + tier = TierFactory(tier_name=TierName.PRO.value) + plan = PlanFactory( + tier=tier, + name=PlanName.CODECOV_PRO_MONTHLY.value, + paid_plan=True, + ) + + self.current_org = OwnerFactory( + plan=plan.name, + ) + self.owner = OwnerFactory() + self.plan_service = PlanService(current_org=self.current_org) + + assert self.plan_service.is_trial_plan == False + assert self.plan_service.is_sentry_plan == False + assert self.plan_service.is_team_plan == False + assert self.plan_service.is_free_plan == False + assert self.plan_service.is_pro_plan == True + assert self.plan_service.is_enterprise_plan == False + assert self.plan_service.is_pr_billing_plan == True + + def test_is_enterprise_plan(self): + tier = TierFactory(tier_name=TierName.ENTERPRISE.value) + plan = PlanFactory( + tier=tier, + name=PlanName.ENTERPRISE_CLOUD_YEARLY.value, + paid_plan=True, + ) + self.current_org = OwnerFactory( + plan=plan.name, + ) + self.owner = OwnerFactory() + self.plan_service = PlanService(current_org=self.current_org) + + assert self.plan_service.is_trial_plan == False + assert self.plan_service.is_sentry_plan == False + assert self.plan_service.is_team_plan == False + assert self.plan_service.is_free_plan == False + assert self.plan_service.is_pro_plan == False + assert self.plan_service.is_enterprise_plan == True + assert self.plan_service.is_pr_billing_plan == True diff --git a/libs/shared/tests/unit/rate_limits/test_rate_limit.py b/libs/shared/tests/unit/rate_limits/test_rate_limit.py new file mode 100644 index 0000000000..a7b7238878 --- /dev/null +++ b/libs/shared/tests/unit/rate_limits/test_rate_limit.py @@ -0,0 +1,227 @@ +import datetime +from unittest.mock import patch + +import pytest +from mock import MagicMock +from redis import RedisError + +from shared.django_apps.codecov_auth.models import ( + GITHUB_APP_INSTALLATION_DEFAULT_NAME, + GithubAppInstallation, +) +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory +from shared.django_apps.core.tests.factories import RepositoryFactory +from shared.helpers.redis import get_redis_connection +from shared.rate_limits import ( + determine_entity_redis_key, + determine_if_entity_is_rate_limited, + gh_app_key_name, + set_entity_to_rate_limited, +) + + +@pytest.fixture +def mock_configuration(mock_configuration): + custom_params = { + "github": { + "bot": { + "key": "github_key", + }, + } + } + mock_configuration.set_params(custom_params) + return custom_params + + +def get_github_integration_token_side_effect( + service: str, + installation_id: int = None, + app_id: str | None = None, + pem_path: str | None = None, +): + return f"installation_token_{installation_id}_{app_id}" + + +@pytest.fixture +def redis_connection(): + return get_redis_connection() + + +def test_determine_entity_redis_key_github_bot(): + assert determine_entity_redis_key(owner=None, repository=None) == "github_bot" + + +@patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, +) +@pytest.mark.django_db(databases={"default"}) +def test_determine_entity_redis_key_installed_gh_app_via_repository( + mock_get_github_integration_token, +): + owner = OwnerFactory( + service="github", + bot=None, + unencrypted_oauth_token="owner_token: :refresh_token", + ) + owner.save() + gh_app = GithubAppInstallation( + repository_service_ids=None, + installation_id=1200, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=200, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + owner=owner, + ) + gh_app.save() + repository = RepositoryFactory(author=owner, using_integration=None) + repository.save() + assert ( + determine_entity_redis_key(owner=owner, repository=repository) + == f"{gh_app.app_id}_{gh_app.installation_id}" + ) + + +@patch( + "shared.bots.github_apps.get_github_integration_token", + side_effect=get_github_integration_token_side_effect, +) +@pytest.mark.django_db(databases={"default"}) +def test_determine_entity_redis_key_installed_gh_app_via_owner( + mock_get_github_integration_token, +): + owner = OwnerFactory( + service="github", + bot=None, + unencrypted_oauth_token="owner_token: :refresh_token", + ) + owner.save() + gh_app = GithubAppInstallation( + repository_service_ids=None, + installation_id=1200, + name=GITHUB_APP_INSTALLATION_DEFAULT_NAME, + app_id=200, + pem_path="pem_path", + created_at=datetime.datetime.now(datetime.UTC), + owner=owner, + ) + gh_app.save() + repository = None + assert ( + determine_entity_redis_key(owner=owner, repository=repository) + == f"{gh_app.app_id}_{gh_app.installation_id}" + ) + + +@pytest.mark.django_db(databases={"default"}) +def test_determine_entity_redis_key_owner_token_via_repository(): + owner = OwnerFactory( + ownerid=1428, + service="github", + bot=None, + unencrypted_oauth_token="owner_token: :refresh_token", + ) + owner.save() + repository = RepositoryFactory(author=owner, using_integration=None) + repository.save() + assert determine_entity_redis_key(owner=owner, repository=repository) == str( + repository.author.ownerid + ) + + +@pytest.mark.django_db(databases={"default"}) +def test_determine_entity_redis_key_owner_token_via_owner(): + owner = OwnerFactory( + ownerid=1428, + service="github", + bot=None, + unencrypted_oauth_token="owner_token: :refresh_token", + ) + owner.save() + repository = None + assert determine_entity_redis_key(owner=owner, repository=repository) == str( + owner.ownerid + ) + + +def test_determine_if_entity_is_rate_limited_true(redis_connection): + key_name = "owner_id_123" + set_entity_to_rate_limited( + redis_connection=redis_connection, key_name=key_name, ttl_seconds=300 + ) + assert ( + determine_if_entity_is_rate_limited( + redis_connection=redis_connection, key_name=key_name + ) + == True + ) + + +def test_determine_if_entity_is_rate_limited_false(redis_connection): + assert ( + determine_if_entity_is_rate_limited( + redis_connection=redis_connection, + key_name="random_non_existent_key", + ) + == False + ) + + +def test_determine_if_entity_is_rate_limited_error(redis_connection, mocker): + mock_redis = MagicMock(name="fake_redis") + mock_redis.exists.side_effect = RedisError + assert ( + determine_if_entity_is_rate_limited( + redis_connection=redis_connection, + key_name="random_non_existent_key", + ) + == False + ) + + +def test_set_entity_to_rate_limited_success(redis_connection): + key_name = "owner_id_123" + set_entity_to_rate_limited( + redis_connection=redis_connection, key_name=key_name, ttl_seconds=300 + ) + assert redis_connection.get(f"rate_limited_entity_{key_name}") is not None + + +def test_set_entity_to_rate_limited_error(redis_connection, mocker): + mock_redis = MagicMock(name="fake_redis") + mock_redis.set.side_effect = RedisError + key_name = "owner_id_123" + # This actually asserts that the error is not raised + # Despite the call failing + set_entity_to_rate_limited( + redis_connection=redis_connection, key_name=key_name, ttl_seconds=300 + ) + mock_redis.set.assert_not_called() + + +def test_set_entity_to_rate_limited_ttl_zero(redis_connection): + key_name = "owner_id_123" + # This actually asserts that the error is not raised + # Despite the call failing + set_entity_to_rate_limited( + redis_connection=redis_connection, key_name=key_name, ttl_seconds=0 + ) + assert redis_connection.get(f"rate_limited_entity_{key_name}") is not None + + +@pytest.mark.parametrize( + "app_id, installation_id, expected", + [ + (200, 718263, "200_718263"), + (None, 718263, "default_app_718263"), + ], +) +def test_gh_app_key_name_with_or_without_id(app_id, installation_id, expected): + assert ( + gh_app_key_name( + app_id=app_id, + installation_id=installation_id, + ) + == expected + ) diff --git a/libs/shared/tests/unit/reports/samples/chunks_01.txt b/libs/shared/tests/unit/reports/samples/chunks_01.txt new file mode 100644 index 0000000000..1178444143 --- /dev/null +++ b/libs/shared/tests/unit/reports/samples/chunks_01.txt @@ -0,0 +1,39 @@ +{} +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[0, null, [[0, 0], [1, 0]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 1]]] +[1, null, [[0, 1], [1, 1]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 1]]] +[0, null, [[0, 0], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[0, null, [[0, 0], [1, 0]]] \ No newline at end of file diff --git a/libs/shared/tests/unit/reports/samples/chunks_02.txt b/libs/shared/tests/unit/reports/samples/chunks_02.txt new file mode 100644 index 0000000000..2fa4b1164b --- /dev/null +++ b/libs/shared/tests/unit/reports/samples/chunks_02.txt @@ -0,0 +1,22 @@ +{} +[1, null, [[0, 1, null, null, null]]] + +[1, null, [[1, 1, null, null, null]]] + +[1, null, [[0, 1, null, null, null]]] +[1, null, [[1, 1, null, null, null]]] + +[1, null, [[0, 1, null, null, null]]] + +[1, null, [[1, 1, null, null, null]]] + +[1, null, [[0, 1, null, null, null]]] +<<<<< end_of_chunk >>>>> +null +<<<<< end_of_chunk >>>>> +{} +[1, null, [[0, 1, null, null, null]]] +[1, null, [[0, 1, null, null, null]]] + + +[0, null, [[0, 0, null, null, null]]] diff --git a/libs/shared/tests/unit/reports/samples/chunks_03.txt b/libs/shared/tests/unit/reports/samples/chunks_03.txt new file mode 100644 index 0000000000..8464bdc4e6 --- /dev/null +++ b/libs/shared/tests/unit/reports/samples/chunks_03.txt @@ -0,0 +1,40 @@ +{"labels_index":{"0":"special_label","1":"some_test"}} +<<<<< end_of_header >>>>> +[1, null, [[0, 1], [1, 0]], null, null, [[0, 1, null, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]]] + + +[1, null, [[0, 1], [1, 0]], null, null, [[0, 1, null, [0, 1]]]] +[0, null, [[0, 0], [1, 0]], null, null, [[0, 1, null, [2, 3]]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 1]]] +[1, null, [[0, 1], [1, 1]]] +<<<<< end_of_chunk >>>>> +{} +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 1]]] +[0, null, [[0, 0], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] +[1, null, [[0, 1], [1, 0]]] + + +[1, null, [[0, 1], [1, 0]]] +[0, null, [[0, 0], [1, 0]]] \ No newline at end of file diff --git a/libs/shared/tests/unit/reports/samples/test_filtered.py b/libs/shared/tests/unit/reports/samples/test_filtered.py new file mode 100644 index 0000000000..adbb12690a --- /dev/null +++ b/libs/shared/tests/unit/reports/samples/test_filtered.py @@ -0,0 +1,1004 @@ +from unittest.mock import patch + +from shared.reports.filtered import FilteredReport, FilteredReportFile +from shared.reports.resources import Report, ReportFile, ReportTotals, Session +from shared.reports.types import CoverageDatapoint, LineSession, NetworkFile, ReportLine +from shared.utils.sessions import SessionType + + +# This immitates what a report.labels_index looks like +# It's an map idx -> label, so we can go from CoverageDatapoint.label_id to the actual label +# typically via Report.lookup_label_by_id +def lookup_label(label_id: int) -> str: + lookup_table = {1: "simpletest", 2: "complextest", 3: "simple"} + return lookup_table[label_id] + + +class TestFilteredReportFile(object): + def test_name(self): + first_file = ReportFile("file_1.go") + f = FilteredReportFile(first_file, [1]) + assert f.name == "file_1.go" + + def test_eof(self): + first_file = ReportFile("file_1.go") + f = FilteredReportFile(first_file, [1]) + assert f.eof == 1 + assert f.eof == first_file.eof + + @patch("shared.reports.filtered.FilteredReportFile.line_modifier") + def test_lines_cached(self, line_modifier_mock): + line_modifier_mock.return_value = True + + first_file = ReportFile("file_1.go") + first_file.append( + 1, + ReportLine.create( + coverage=1, + sessions=[LineSession(0, 1), LineSession(1, 1), LineSession(2, 1)], + ), + ) + filtered_report_file = FilteredReportFile(first_file, [1]) + + assert filtered_report_file.lines == filtered_report_file.lines + assert line_modifier_mock.call_count == 1 + + def test_totals(self): + first_file = ReportFile("file_1.go") + first_file.append( + 1, + ReportLine.create( + coverage=1, + sessions=[LineSession(0, 1), LineSession(1, 1), LineSession(2, 1)], + ), + ) + first_file.append( + 2, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 0), LineSession(1, 1)] + ), + ) + first_file.append( + 3, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 1), LineSession(1, 0)] + ), + ) + first_file.append( + 5, + ReportLine.create( + coverage=0, sessions=[LineSession(0, 0), LineSession(1, 0)] + ), + ) + first_file.append( + 6, + ReportLine.create( + coverage="1/2", + sessions=[ + LineSession(0, "1/2"), + LineSession(1, 0), + LineSession(2, "1/4"), + ], + ), + ) + f = FilteredReportFile(first_file, [1]) + expected_result = ReportTotals( + files=0, + lines=5, + hits=2, + misses=3, + partials=0, + coverage="40.00000", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + assert f.totals == expected_result + # calling a second time to hit cache + assert f.totals == expected_result + assert f._totals == expected_result + + def test_line_modifier(self): + original_file = ReportFile("file_1.py") + file = FilteredReportFile(original_file, [0, 1, 5]) + res = file.line_modifier( + ReportLine.create( + 1, + sessions=[ + LineSession(0, 1, complexity=5), + LineSession(1, 1, complexity=4), + LineSession(2, 0, complexity=4), + LineSession(10, 0, complexity=3), + ], + datapoints=[ + CoverageDatapoint(0, 0, None, [1]), + CoverageDatapoint(0, 1, None, [2]), + CoverageDatapoint(1, "1/2", None, [1]), + CoverageDatapoint(1, 1, None, [2]), + CoverageDatapoint(2, 0, None, [3]), + CoverageDatapoint(2, 0, None, [2]), + CoverageDatapoint(10, 0, None, [2]), + ], + ) + ) + assert res.coverage == 1 + assert res.type is None + assert res.sessions == [ + LineSession(id=0, coverage=1, branches=None, partials=None, complexity=5), + LineSession(id=1, coverage=1, complexity=4), + ] + assert res.messages is None + assert res.datapoints == [ + CoverageDatapoint(0, 0, None, [1]), + CoverageDatapoint(0, 1, None, [2]), + CoverageDatapoint(1, "1/2", None, [1]), + CoverageDatapoint(1, 1, None, [2]), + ] + assert res.complexity == 5 + + def test_line_modifier_empty(self): + original_file = ReportFile("file_1.py") + file = FilteredReportFile(original_file, [1]) + res = file.line_modifier( + ReportLine.create(1, sessions=[LineSession(0, 1, complexity=5)]) + ) + assert res == "" + + +class TestFilteredReport(object): + def test_no_real_filter(self, sample_report): + assert sample_report.filter(None, None) is sample_report + + def test_get(self, sample_report): + assert sample_report.filter(paths=[".*.go"]).get("location/file_1.py") is None + assert isinstance( + sample_report.filter(paths=[".*.go"]).get("file_1.go"), ReportFile + ) + assert isinstance( + sample_report.filter(paths=[".*.go"], flags=["simple"]).get("file_1.go"), + FilteredReportFile, + ) + assert sample_report.get("location/file_1.py") is not None + assert ( + sample_report.filter(paths=[".*.go"], flags=["simple"]).get("myfile.go") + is None + ) + + def test_get_cached(self, sample_report): + filtered_report = sample_report.filter(paths=[".*.go"], flags=["simple"]) + filtered_report_file_1 = filtered_report.get("file_1.go") + assert isinstance(filtered_report_file_1, FilteredReportFile) + filtered_report_file_2 = filtered_report.get("file_1.go") + assert isinstance(filtered_report_file_2, FilteredReportFile) + assert filtered_report_file_1 == filtered_report_file_2 + + def test_normal_totals(self, sample_report): + assert sample_report.totals == ReportTotals( + files=3, + lines=9, + hits=4, + misses=1, + partials=4, + coverage="44.44444", + branches=3, + methods=0, + messages=0, + sessions=4, + complexity=0, + complexity_total=0, + diff=0, + ) + + def test_totals_already_calculated(self, sample_report, mocker): + v = mocker.MagicMock() + filtered_report = sample_report.filter(paths=[".*.go"]) + filtered_report._totals = v + assert filtered_report.totals is v + + def test_path_filtered_totals(self, sample_report): + assert sample_report.filter(paths=[".*.go"]).totals == ReportTotals( + files=2, + lines=7, + hits=4, + misses=1, + partials=2, + coverage="57.14286", + branches=1, + methods=0, + messages=0, + sessions=4, + complexity=0, + complexity_total=0, + diff=0, + ) + assert sample_report.filter(paths=[".*file.*"]).totals == ReportTotals( + files=3, + lines=9, + hits=4, + misses=1, + partials=4, + coverage="44.44444", + branches=3, + methods=0, + messages=0, + sessions=4, + complexity=0, + complexity_total=0, + diff=0, + ) + assert sample_report.filter( + paths=["location.*", "file_1.go"] + ).totals == ReportTotals( + files=2, + lines=7, + hits=3, + misses=1, + partials=3, + coverage="42.85714", + branches=2, + methods=0, + messages=0, + sessions=4, + complexity=0, + complexity_total=0, + diff=0, + ) + + def test_flag_filtered_totals(self, sample_report): + assert sample_report.filter(flags=["simple"]).totals == ReportTotals( + files=3, + lines=8, + hits=3, + misses=2, + partials=3, + coverage="37.50000", + branches=2, + methods=0, + messages=0, + sessions=2, + complexity=0, + complexity_total=0, + diff=0, + ) + assert sample_report.filter(flags=["complex"]).totals == ReportTotals( + files=2, + lines=6, + hits=2, + misses=2, + partials=2, + coverage="33.33333", + branches=1, + methods=0, + messages=0, + sessions=2, + complexity=0, + complexity_total=0, + diff=0, + ) + assert sample_report.filter(flags=["simple", "complex"]).totals == ReportTotals( + files=3, + lines=8, + hits=4, + misses=1, + partials=3, + coverage="50.00000", + branches=2, + methods=0, + messages=0, + sessions=3, + complexity=0, + complexity_total=0, + diff=0, + ) + + def test_flag_filtered_totals_flag_single_session(self, mocker): + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, + ReportLine.create( + coverage=1, + sessions=[ + LineSession(0, 1), + LineSession(1, 1), + LineSession(2, 1), + LineSession(4, 1), + ], + ), + ) + first_file.append( + 2, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 0), LineSession(1, 1)] + ), + ) + first_file.append( + 3, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 1), LineSession(1, 0)] + ), + ) + first_file.append( + 5, + ReportLine.create( + coverage=0, sessions=[LineSession(0, 0), LineSession(1, 0)] + ), + ) + first_file.append( + 6, + ReportLine.create( + coverage="1/2", + sessions=[ + LineSession(0, "1/2"), + LineSession(1, 0), + LineSession(2, "1/4"), + LineSession(3, 0), + LineSession(4, 0), + ], + ), + ) + report.append(first_file) + report.add_session( + Session( + id=0, + flags=["unit"], + totals=ReportTotals( + files=1, + lines=5, + hits=2, + misses=2, + partials=1, + coverage="40.00000", + branches=0, + methods=0, + messages=3, + sessions=1, + complexity=0, + complexity_total=0, + diff=0, + ), + ) + ) + report.add_session( + Session( + id=1, + totals=ReportTotals( + files=1, + lines=5, + hits=2, + misses=3, + partials=0, + coverage="40.00000", + branches=0, + methods=0, + messages=0, + sessions=1, + complexity=0, + complexity_total=0, + diff=0, + ), + flags=["banana"], + ) + ) + trouble_session = mocker.Mock( + spec=[], + flags=["poultry"], + session_type=SessionType.uploaded, + asdict=mocker.MagicMock(return_value={}), + ) + report.add_session(trouble_session) + report.add_session( + Session( + id=3, + totals=ReportTotals( + files=1, + lines=5, + hits=2, + misses=3, + partials=0, + coverage="80.00000", + branches=0, + methods=0, + messages=0, + sessions=1, + complexity=0, + complexity_total=0, + diff=0, + ), + flags=["super"], + ) + ) + assert report.flags["unit"].totals == ReportTotals( + files=1, + lines=5, + hits=2, + misses=2, + partials=1, + coverage="40.00000", + branches=0, + methods=0, + messages=0, + sessions=1, + complexity=0, + complexity_total=0, + diff=0, + ) + assert report.flags["banana"].totals == ReportTotals( + files=1, + lines=5, + hits=2, + misses=3, + partials=0, + coverage="40.00000", + branches=0, + methods=0, + messages=0, + sessions=1, + complexity=0, + complexity_total=0, + diff=0, + ) + assert report.flags["poultry"].totals == ReportTotals( + files=1, + lines=2, + hits=1, + misses=0, + partials=1, + coverage="50.00000", + branches=0, + methods=0, + messages=0, + sessions=1, + complexity=0, + complexity_total=0, + diff=0, + ) + assert report.flags["super"].totals == ReportTotals( + files=1, + lines=1, + hits=0, + misses=1, + partials=0, + coverage="0", + branches=0, + methods=0, + messages=0, + sessions=1, + complexity=0, + complexity_total=0, + diff=0, + ) + + def test_get_file_totals(self, sample_report): + assert "location/file_1.py" in sample_report + assert "file_1.go" in sample_report + filtered_report = FilteredReport(sample_report, ["location/file_1.py"], []) + + # Go file exists in the raw report but is not included in the filters + assert sample_report.get_file_totals("file_1.go") is not None + assert filtered_report.get_file_totals("file_1.go") is None + + # Python file exists in the raw report and is included in the filters + py_file_totals = sample_report.get_file_totals("location/file_1.py") + assert filtered_report.get_file_totals("location/file_1.py") == py_file_totals + + def test_calculate_diff(self, sample_report): + diff = { + "files": { + "file_1.go": { + "type": "modified", + "segments": [{"header": list("1313"), "lines": list("---+++")}], + }, + "location/file_1.py": { + "type": "modified", + "segments": [ + {"header": ["100", "3", "100", "3"], "lines": list("-+-+-+")} + ], + }, + "deleted.py": {"type": "deleted"}, + } + } + res = sample_report.filter(paths=[".*go"]).calculate_diff(diff) + expected_result = { + "files": { + "file_1.go": ReportTotals( + files=0, lines=3, hits=3, misses=0, partials=0, coverage="100" + ) + }, + "general": ReportTotals( + files=1, + lines=3, + hits=3, + misses=0, + partials=0, + coverage="100", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + } + assert res["files"] == expected_result["files"] + assert res["general"] == expected_result["general"] + assert res == expected_result + second_res = sample_report.filter(paths=["location.*"]).calculate_diff(diff) + second_expected_result = { + "files": { + "location/file_1.py": ReportTotals( + files=0, + lines=2, + hits=0, + misses=0, + partials=2, + coverage="0", + branches=2, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + }, + "general": ReportTotals( + files=1, + lines=2, + hits=0, + misses=0, + partials=2, + coverage="0", + branches=2, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + } + assert ( + sample_report.filter(paths=["location.*"]).apply_diff(diff) + == second_expected_result["general"] + ) + assert second_res["files"] == second_expected_result["files"] + assert second_res["general"] == second_expected_result["general"] + assert second_res == second_expected_result + + def test_calculate_diff_nothing_related_diff(self, sample_report): + diff = { + "files": { + "random.go": { + "type": "modified", + "segments": [{"header": list("1313"), "lines": list("---+++")}], + }, + "random.py": { + "type": "modified", + "segments": [ + {"header": ["100", "3", "100", "3"], "lines": list("-+-+-+")} + ], + }, + } + } + res = sample_report.filter( + paths=["location.*"], flags=["simple"] + ).calculate_diff(diff) + assert res == { + "files": {}, + "general": ReportTotals( + coverage=None, complexity=None, complexity_total=None + ), + } + + def test_calculate_diff_both_filters(self, sample_report): + diff = { + "files": { + "file_1.go": { + "type": "modified", + "segments": [{"header": list("1313"), "lines": list("---+++")}], + }, + "location/file_1.py": { + "type": "modified", + "segments": [ + {"header": ["100", "3", "100", "3"], "lines": list("-+-+-+")} + ], + }, + } + } + third_res = sample_report.filter( + paths=["location.*"], flags=["simple"] + ).calculate_diff(diff) + third_expected_result = { + "files": { + "location/file_1.py": ReportTotals( + files=0, + lines=1, + hits=0, + misses=0, + partials=1, + coverage="0", + branches=1, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + }, + "general": ReportTotals( + files=1, + lines=1, + hits=0, + misses=0, + partials=1, + coverage="0", + branches=1, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + } + assert ( + third_res["files"]["location/file_1.py"] + == third_expected_result["files"]["location/file_1.py"] + ) + assert third_res["files"] == third_expected_result["files"] + assert third_res["general"] == third_expected_result["general"] + assert third_res == third_expected_result + assert ( + sample_report.filter(paths=["location.*"], flags=["simple"]).apply_diff( + diff + ) + == third_expected_result["general"] + ) + assert diff == { + "files": { + "file_1.go": { + "type": "modified", + "segments": [ + { + "header": ["1", "3", "1", "3"], + "lines": ["-", "-", "-", "+", "+", "+"], + } + ], + }, + "location/file_1.py": { + "type": "modified", + "segments": [ + { + "header": ["100", "3", "100", "3"], + "lines": ["-", "+", "-", "+", "-", "+"], + } + ], + "totals": ReportTotals( + files=0, + lines=1, + hits=0, + misses=0, + partials=1, + coverage="0", + branches=1, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + }, + }, + "totals": ReportTotals( + files=1, + lines=1, + hits=0, + misses=0, + partials=1, + coverage="0", + branches=1, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + } + + def test_apply_diff_none(self, sample_report): + assert ( + sample_report.filter(paths=["location.*"], flags=["simple"]).apply_diff( + None + ) + is None + ) + + def test_apply_diff_not_saving(self, sample_report): + diff = { + "files": { + "file_1.go": { + "type": "modified", + "segments": [{"header": list("1313"), "lines": list("---+++")}], + }, + "location/file_1.py": { + "type": "modified", + "segments": [ + {"header": ["100", "3", "100", "3"], "lines": list("-+-+-+")} + ], + }, + } + } + res = sample_report.filter(paths=["location.*"], flags=["simple"]).apply_diff( + diff, _save=False + ) + third_expected_result = ReportTotals( + files=1, + lines=1, + hits=0, + misses=0, + partials=1, + coverage="0", + branches=1, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + + assert res == third_expected_result + assert diff == { + "files": { + "file_1.go": { + "type": "modified", + "segments": [{"header": list("1313"), "lines": list("---+++")}], + }, + "location/file_1.py": { + "type": "modified", + "segments": [ + {"header": ["100", "3", "100", "3"], "lines": list("-+-+-+")} + ], + }, + } + } + + def test_network(self, sample_report): + assert list(sample_report.filter(paths=[".*go"]).network) == [ + ( + "file_1.go", + NetworkFile( + totals=ReportTotals( + files=0, + lines=5, + hits=3, + misses=1, + partials=1, + coverage="60.00000", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + diff_totals=None, + ), + ), + ( + "file_2.go", + NetworkFile( + totals=ReportTotals( + files=0, + lines=2, + hits=1, + misses=0, + partials=1, + coverage="50.00000", + branches=1, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + diff_totals=None, + ), + ), + ] + + def test_iter_path_filter(self, sample_report): + r = list(sample_report.filter(paths=[".*go"])) + assert len(r) == 2 + r = sorted(r, key=lambda r: r.name) + assert r[0].name == "file_1.go" + assert r[0].totals == ReportTotals( + files=0, + lines=5, + hits=3, + misses=1, + partials=1, + coverage="60.00000", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + assert r[1].name == "file_2.go" + assert r[1].totals == ReportTotals( + files=0, + lines=2, + hits=1, + misses=0, + partials=1, + coverage="50.00000", + branches=1, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + + def test_iter_path_and_flag_filter(self, sample_report): + r = list(sample_report.filter(paths=[".*go"], flags=["simple"])) + assert len(r) == 2 + r = sorted(r, key=lambda r: r.name) + assert r[0].name == "file_1.go" + assert r[0].totals == ReportTotals( + files=0, + lines=5, + hits=2, + misses=2, + partials=1, + coverage="40.00000", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + assert r[1].name == "file_2.go" + assert r[1].totals == ReportTotals( + files=0, + lines=2, + hits=1, + misses=0, + partials=1, + coverage="50.00000", + branches=1, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + + def test_filtered_report_flags_substring_each_other(self): + report = Report() + file_1 = ReportFile("filename.py") + for i in range(0, 100): + file_1.append( + i + 1, + ReportLine.create( + coverage=1, + sessions=[ + LineSession(0, 0), + LineSession(1, 1), + LineSession(2, "1/2"), + LineSession(3, 0), + LineSession(4, 1), + ], + ), + ) + file_2 = ReportFile("second.py") + for i in range(0, 100): + file_2.append( + i + 1, + ReportLine.create( + coverage=1, + sessions=[ + LineSession(0, 1), + LineSession(1, 1), + LineSession(2, 0), + LineSession(3, 0), + LineSession(4, 1), + ], + ), + ) + report.append(file_1) + report.append(file_2) + report.add_session(Session(id=0, flags=["banana"])) + report.add_session(Session(id=1, flags=["bananastand"])) + report.add_session(Session(id=2, flags=["banana", "bananastand"])) + report.add_session(Session(id=3, flags=["unrelated"])) + report.add_session(Session(id=4, flags=None)) + assert report.filter(flags=["banana"]).totals == ReportTotals( + files=2, + lines=200, + hits=100, + misses=0, + partials=100, + coverage="50.00000", + branches=0, + methods=0, + messages=0, + sessions=2, + complexity=0, + complexity_total=0, + diff=0, + ) + + def test_filtered_report_flags_substring_each_other_old_style_configuration( + self, mock_configuration + ): + mock_configuration._params["compatibility"] = {"flag_pattern_matching": True} + report = Report() + file_1 = ReportFile("filename.py") + for i in range(0, 100): + file_1.append( + i + 1, + ReportLine.create( + coverage=1, + sessions=[ + LineSession(0, 0), + LineSession(1, 1), + LineSession(2, "1/2"), + LineSession(3, 0), + ], + ), + ) + file_2 = ReportFile("second.py") + for i in range(0, 100): + file_2.append( + i + 1, + ReportLine.create( + coverage=1, + sessions=[ + LineSession(0, 1), + LineSession(1, 1), + LineSession(2, 0), + LineSession(3, 0), + ], + ), + ) + report.append(file_1) + report.append(file_2) + report.add_session(Session(id=0, flags=["banana"])) + report.add_session(Session(id=1, flags=["bananastand"])) + report.add_session(Session(id=2, flags=["banana", "bananastand"])) + report.add_session(Session(id=3, flags=["unrelated"])) + report.add_session(Session(id=4, flags=None)) + assert report.filter(flags=["banana"]).totals == ReportTotals( + files=2, + lines=200, + hits=200, + misses=0, + partials=0, + coverage="100", + branches=0, + methods=0, + messages=0, + sessions=3, + complexity=0, + complexity_total=0, + diff=0, + ) diff --git a/libs/shared/tests/unit/reports/test_carryforward.py b/libs/shared/tests/unit/reports/test_carryforward.py new file mode 100644 index 0000000000..5aa45d9147 --- /dev/null +++ b/libs/shared/tests/unit/reports/test_carryforward.py @@ -0,0 +1,482 @@ +import pytest + +from shared.reports.carryforward import ( + carriedforward_session_name, + generate_carryforward_report, +) +from shared.reports.resources import Report, ReportFile, Session +from shared.reports.types import LineSession, ReportLine +from tests.unit.reports.utils import convert_report_to_better_readable + + +@pytest.fixture +def sample_report(): + report = Report() + first_file = ReportFile("file_1.go") + first_file.append( + 1, + ReportLine.create(coverage=1, sessions=[LineSession(0, 1), LineSession(1, 1)]), + ) + first_file.append( + 2, + ReportLine.create(coverage=1, sessions=[LineSession(0, 0), LineSession(1, 1)]), + ) + first_file.append( + 3, + ReportLine.create(coverage=1, sessions=[LineSession(0, 1), LineSession(1, 0)]), + ) + first_file.append( + 5, + ReportLine.create(coverage=0, sessions=[LineSession(0, 0), LineSession(1, 0)]), + ) + first_file.append( + 6, + ReportLine.create( + coverage="1/2", sessions=[LineSession(0, "1/2"), LineSession(1, 0)] + ), + ) + second_file = ReportFile("file_2.py") + second_file.append(12, ReportLine.create(coverage=1, sessions=[[0, 1]])) + second_file.append( + 51, ReportLine.create(coverage="1/2", type="b", sessions=[[0, "1/2"]]) + ) + report.append(first_file) + report.append(second_file) + report.add_session(Session(id=0, flags=["simple"])) + report.add_session(Session(id=1, flags=["complex"])) + return report + + +class TestCarryfowardFlag(object): + def test_carriedforward_session_name(self): + assert carriedforward_session_name(None) == "Carriedforward" + assert carriedforward_session_name("") == "Carriedforward" + assert carriedforward_session_name("Carriedforward") == "CF[1] - Carriedforward" + assert carriedforward_session_name("Dude") == "CF[1] - Dude" + assert carriedforward_session_name("CF[1] - Dude") == "CF[2] - Dude" + assert carriedforward_session_name("CF[2] - Dude") == "CF[3] - Dude" + assert carriedforward_session_name("CF[9] - Dude") == "CF[10] - Dude" + assert carriedforward_session_name("CF[10] - Dude") == "CF[11] - Dude" + assert carriedforward_session_name("CF CF Dude") == "CF[3] - Dude" + assert carriedforward_session_name("CFCD") == "CF[1] - CFCD" + assert ( + carriedforward_session_name("CF CF CF CF CF CF CF Dude") == "CF[8] - Dude" + ) + + def test_generate_carryforward_report(self, sample_report): + res = generate_carryforward_report(sample_report, flags=["simple"], paths=None) + assert res.files == ["file_1.go", "file_2.py"] + readable_report = convert_report_to_better_readable(res) + expected_result = { + "archive": { + "file_1.go": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + (2, 0, None, [[0, 0, None, None, None]], None, None), + (3, 1, None, [[0, 1, None, None, None]], None, None), + (5, 0, None, [[0, 0, None, None, None]], None, None), + (6, "1/2", None, [[0, "1/2", None, None, None]], None, None), + ], + "file_2.py": [ + (12, 1, None, [[0, 1, None, None, None]], None, None), + (51, "1/2", "b", [[0, "1/2", None, None, None]], None, None), + ], + }, + "report": { + "files": { + "file_1.go": [ + 0, + [0, 5, 2, 2, 1, "40.00000", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "file_2.py": [ + 1, + [0, 2, 1, 0, 1, "50.00000", 1, 0, 0, 0, 0, 0, 0], + None, + None, + ], + }, + "sessions": { + "0": { + "a": None, + "c": None, + "d": readable_report["report"]["sessions"]["0"]["d"], + "e": None, + "f": ["simple"], + "N": "Carriedforward", + "j": None, + "n": None, + "p": None, + "st": "carriedforward", + "se": {}, + "t": None, + "u": None, + } + }, + }, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 1, + "c": "42.85714", + "d": 0, + "diff": None, + "f": 2, + "h": 3, + "m": 2, + "n": 7, + "p": 2, + "s": 1, + }, + } + assert ( + readable_report["archive"]["file_2.py"] + == expected_result["archive"]["file_2.py"] + ) + assert readable_report["archive"] == expected_result["archive"] + assert ( + readable_report["report"]["sessions"] + == expected_result["report"]["sessions"] + ) + assert ( + readable_report["report"]["files"]["file_2.py"] + == expected_result["report"]["files"]["file_2.py"] + ) + assert ( + readable_report["report"]["files"]["file_1.go"] + == expected_result["report"]["files"]["file_1.go"] + ) + assert readable_report["report"]["files"] == expected_result["report"]["files"] + assert readable_report["report"] == expected_result["report"] + assert readable_report["totals"] == expected_result["totals"] + assert readable_report == expected_result + + def test_generate_carryforward_report_with_paths(self, sample_report): + res = generate_carryforward_report( + sample_report, flags=["simple"], paths=["file_1.*"] + ) + assert res.files == ["file_1.go"] + readable_report = convert_report_to_better_readable(res) + expected_result = { + "archive": { + "file_1.go": [ + (1, 1, None, [[0, 1, None, None, None]], None, None), + (2, 0, None, [[0, 0, None, None, None]], None, None), + (3, 1, None, [[0, 1, None, None, None]], None, None), + (5, 0, None, [[0, 0, None, None, None]], None, None), + (6, "1/2", None, [[0, "1/2", None, None, None]], None, None), + ] + }, + "report": { + "files": { + "file_1.go": [ + 0, + [0, 5, 2, 2, 1, "40.00000", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ] + }, + "sessions": { + "0": { + "a": None, + "c": None, + "d": readable_report["report"]["sessions"]["0"]["d"], + "e": None, + "f": ["simple"], + "N": "Carriedforward", + "j": None, + "n": None, + "p": None, + "st": "carriedforward", + "se": {}, + "t": None, + "u": None, + } + }, + }, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "40.00000", + "d": 0, + "diff": None, + "f": 1, + "h": 2, + "m": 2, + "n": 5, + "p": 1, + "s": 1, + }, + } + assert readable_report["archive"] == expected_result["archive"] + assert ( + readable_report["report"]["sessions"] + == expected_result["report"]["sessions"] + ) + assert readable_report["report"] == expected_result["report"] + assert readable_report["totals"] == expected_result["totals"] + assert readable_report == expected_result + + def test_generate_carryforward_report_with_path_none_matches(self, sample_report): + res = generate_carryforward_report( + sample_report, flags=["simple"], paths=["file_\\W.*"] + ) + assert res.files == [] + readable_report = convert_report_to_better_readable(res) + expected_result = { + "archive": {}, + "report": { + "files": {}, + "sessions": { + "0": { + "a": None, + "c": None, + "d": readable_report["report"]["sessions"]["0"]["d"], + "e": None, + "f": ["simple"], + "N": "Carriedforward", + "j": None, + "n": None, + "p": None, + "st": "carriedforward", + "se": {}, + "t": None, + "u": None, + } + }, + }, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": None, + "d": 0, + "diff": None, + "f": 0, + "h": 0, + "m": 0, + "n": 0, + "p": 0, + "s": 1, + }, + } + assert readable_report["archive"] == expected_result["archive"] + assert ( + readable_report["report"]["sessions"] + == expected_result["report"]["sessions"] + ) + assert readable_report["report"] == expected_result["report"] + assert readable_report["totals"] == expected_result["totals"] + assert readable_report == expected_result + + def test_generate_carryforward_report_with_path_two_patterns(self, sample_report): + res = generate_carryforward_report( + sample_report, flags=["simple"], paths=[".*\\.cpp", ".*_2\\..*"] + ) + assert res.files == ["file_2.py"] + readable_report = convert_report_to_better_readable(res) + assert readable_report == { + "archive": { + "file_2.py": [ + (12, 1, None, [[0, 1, None, None, None]], None, None), + (51, "1/2", "b", [[0, "1/2", None, None, None]], None, None), + ] + }, + "report": { + "files": { + "file_2.py": [ + 0, + [0, 2, 1, 0, 1, "50.00000", 1, 0, 0, 0, 0, 0, 0], + None, + None, + ] + }, + "sessions": { + "0": { + "a": None, + "c": None, + "d": readable_report["report"]["sessions"]["0"]["d"], + "e": None, + "f": ["simple"], + "N": "Carriedforward", + "j": None, + "n": None, + "p": None, + "st": "carriedforward", + "se": {}, + "t": None, + "u": None, + } + }, + }, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 1, + "c": "50.00000", + "d": 0, + "diff": None, + "f": 1, + "h": 1, + "m": 0, + "n": 2, + "p": 1, + "s": 1, + }, + } + + def test_generate_carryforward_report_one_file_not_covered(self, sample_report): + res = generate_carryforward_report(sample_report, flags=["complex"], paths=None) + assert res.files == ["file_1.go"] + readable_report = convert_report_to_better_readable(res) + + expected_result = { + "archive": { + "file_1.go": [ + (1, 1, None, [[1, 1, None, None, None]], None, None), + (2, 1, None, [[1, 1, None, None, None]], None, None), + (3, 0, None, [[1, 0, None, None, None]], None, None), + (5, 0, None, [[1, 0, None, None, None]], None, None), + (6, 0, None, [[1, 0, None, None, None]], None, None), + ] + }, + "report": { + "files": { + "file_1.go": [ + 0, + [0, 5, 2, 3, 0, "40.00000", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ] + }, + "sessions": { + "1": { + "a": None, + "c": None, + "d": readable_report["report"]["sessions"]["1"]["d"], + "e": None, + "f": ["complex"], + "N": "Carriedforward", + "j": None, + "n": None, + "p": None, + "st": "carriedforward", + "se": {}, + "t": None, + "u": None, + } + }, + }, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "40.00000", + "d": 0, + "diff": None, + "f": 1, + "h": 2, + "m": 3, + "n": 5, + "p": 0, + "s": 1, + }, + } + assert ( + readable_report["archive"]["file_1.go"] + == expected_result["archive"]["file_1.go"] + ) + assert readable_report["archive"] == expected_result["archive"] + assert readable_report["report"] == expected_result["report"] + assert readable_report["totals"] == expected_result["totals"] + assert readable_report == expected_result + + def test_generate_carryforward_report_session_extras(self, sample_report): + res = generate_carryforward_report( + sample_report, + flags=["complex"], + paths=None, + session_extras={"cfed_parent": "0f9ab1fe6c879bc49a9e559b23f49fd033daadb0"}, + ) + assert res.files == ["file_1.go"] + readable_report = convert_report_to_better_readable(res) + + expected_result = { + "archive": { + "file_1.go": [ + (1, 1, None, [[1, 1, None, None, None]], None, None), + (2, 1, None, [[1, 1, None, None, None]], None, None), + (3, 0, None, [[1, 0, None, None, None]], None, None), + (5, 0, None, [[1, 0, None, None, None]], None, None), + (6, 0, None, [[1, 0, None, None, None]], None, None), + ] + }, + "report": { + "files": { + "file_1.go": [ + 0, + [0, 5, 2, 3, 0, "40.00000", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ] + }, + "sessions": { + "1": { + "a": None, + "c": None, + "d": readable_report["report"]["sessions"]["1"]["d"], + "e": None, + "f": ["complex"], + "N": "Carriedforward", + "j": None, + "n": None, + "p": None, + "st": "carriedforward", + "se": { + "cfed_parent": "0f9ab1fe6c879bc49a9e559b23f49fd033daadb0" + }, + "t": None, + "u": None, + } + }, + }, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": "40.00000", + "d": 0, + "diff": None, + "f": 1, + "h": 2, + "m": 3, + "n": 5, + "p": 0, + "s": 1, + }, + } + assert readable_report["archive"] == expected_result["archive"] + assert readable_report["report"] == expected_result["report"] + assert readable_report["totals"] == expected_result["totals"] + assert readable_report == expected_result + + +def test_generate_carryforward_report_similar_flags(): + r = Report() + r.add_session(Session(id=0, flags=["simple_man"])) + res = generate_carryforward_report(r, flags=["simple"], paths=None) + assert res.sessions == {} + + +def test_generate_carryforward_report_no_flags(): + r = Report() + r.add_session(Session(id=0, flags=None)) + res = generate_carryforward_report(r, flags=["simple"], paths=None) + assert res.sessions == {} diff --git a/libs/shared/tests/unit/reports/test_changes.py b/libs/shared/tests/unit/reports/test_changes.py new file mode 100644 index 0000000000..1abbaf7856 --- /dev/null +++ b/libs/shared/tests/unit/reports/test_changes.py @@ -0,0 +1,206 @@ +from pathlib import Path + +import pytest +from cc_rustyribs import rustify_diff + +from shared.reports.changes import run_comparison_using_rust +from shared.reports.readonly import ReadOnlyReport + +current_file = Path(__file__) + + +@pytest.fixture +def sample_rust_report(): + with open(current_file.parent / "samples" / "chunks_01.txt", "r") as f: + chunks = f.read() + files_dict = { + "tests/__init__.py": [ + 0, + [0, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0], + [[0, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "tests/test_sample.py": [ + 1, + [0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + [[0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "awesome/__init__.py": [ + 2, + [0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0], + [[0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0]], + [0, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + ], + } + sessions_dict = { + "0": { + "N": None, + "a": "v4/raw/2019-01-10/4434BC2A2EC4FCA57F77B473D83F928C/abf6d4df662c47e32460020ab14abf9303581429/9ccc55a1-8b41-4bb1-a946-ee7a33a7fb56.txt", + "c": None, + "d": 1547084427, + "e": None, + "f": ["unit"], + "j": None, + "n": None, + "p": None, + "t": [3, 20, 17, 3, 0, "85.00000", 0, 0, 0, 0, 0, 0, 0], + "": None, + } + } + return ReadOnlyReport.from_chunks( + chunks=chunks, files=files_dict, sessions=sessions_dict + ) + + +def test_run_comparison_using_rust(sample_rust_report): + base_report, head_report = sample_rust_report, sample_rust_report + diff = { + "files": { + "tests/__init__.py": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["1", "3", "1", "5"], + "lines": [ + "+sudo: false", + "+", + " language: python", + " ", + " python:", + ], + } + ], + "stats": {"added": 2, "removed": 0}, + } + } + } + k = run_comparison_using_rust(base_report, head_report, diff) + assert k == { + "files": [ + { + "base_name": "tests/__init__.py", + "head_name": "tests/__init__.py", + "file_was_added_by_diff": False, + "file_was_removed_by_diff": False, + "base_coverage": { + "hits": 2, + "misses": 1, + "partials": 0, + "branches": 0, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 0, + }, + "head_coverage": { + "hits": 2, + "misses": 1, + "partials": 0, + "branches": 0, + "sessions": 0, + "complexity": 0, + "complexity_total": 0, + "methods": 0, + }, + "removed_diff_coverage": [], + "added_diff_coverage": [[1, "h"]], + "unexpected_line_changes": [ + [[1, "h"], [3, None]], + [[2, None], [4, "h"]], + [[3, None], [5, "m"]], + [[4, "h"], [6, None]], + [[5, "m"], [7, None]], + ], + "lines_only_on_base": [], + "lines_only_on_head": [1, 2], + } + ], + "changes_summary": { + "patch_totals": {"hits": 1, "misses": 0, "partials": 0, "coverage": 1} + }, + } + + +class TestRustifyDiff(object): + def test_rustify_diff_empty(self): + assert rustify_diff({}) == {} + assert rustify_diff(None) == {} + + def test_rustify_simple(self): + d = { + "files": { + "file_1.go": { + "type": "modified", + "segments": [{"header": list("1313"), "lines": list("---+++ ")}], + }, + "location/file_1.py": { + "type": "modified", + "segments": [ + { + "header": ["100", "3", "100", "3"], + "lines": ["-lost", "+g", "-sdasdas", "+weq", "-dasda", "+"], + } + ], + }, + "deleted.py": {"type": "deleted"}, + "renamed.py": {"type": "modified", "before": "old_renamed.py"}, + } + } + expected_res = { + "deleted.py": ("deleted", None, []), + "file_1.go": ( + "modified", + None, + [((1, 3, 1, 3), ["-", "-", "-", "+", "+", "+", " "])], + ), + "location/file_1.py": ( + "modified", + None, + [((100, 3, 100, 3), ["-", "+", "-", "+", "-", "+"])], + ), + "renamed.py": ("modified", "old_renamed.py", []), + } + assert rustify_diff(d) == expected_res + + def test_rustify_diff_new_file(self): + user_input = { + "files": { + "a.txt": { + "type": "new", + "before": None, + "segments": [{"header": ["0", "0", "1", ""], "lines": ["+banana"]}], + "stats": {"added": 1, "removed": 0}, + }, + "wildwest/strings.py": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["43", "7", "43", "7"], + "lines": [ + " if len(this_string) == 20:", + " return True", + " if len(set(this_string)) == 7:", + '- print("0043")', + '+ print("0044")', + ' # k = b"Prè áaaaì u aáa. Ponto, dois-pontos e travessão são exemplos de sinais de pontuação que utilizamos"', + " return True", + " return False", + ], + } + ], + "stats": {"added": 1, "removed": 1}, + }, + } + } + expected_result = { + "a.txt": ("new", None, [((0, 0, 1, 0), ["+"])]), + "wildwest/strings.py": ( + "modified", + None, + [((43, 7, 43, 7), [" ", " ", " ", "-", "+", " ", " ", " "])], + ), + } + assert rustify_diff(user_input) == expected_result diff --git a/libs/shared/tests/unit/reports/test_editable.py b/libs/shared/tests/unit/reports/test_editable.py new file mode 100644 index 0000000000..f2df7c453e --- /dev/null +++ b/libs/shared/tests/unit/reports/test_editable.py @@ -0,0 +1,1752 @@ +from fractions import Fraction +from pathlib import Path +from typing import List + +import orjson +import pytest + +from shared.reports.editable import EditableReport, EditableReportFile +from shared.reports.resources import ReportFile, Session +from shared.reports.types import ( + CoverageDatapoint, + LineSession, + ReportLine, + ReportTotals, +) +from shared.utils.merge import merge_coverage +from shared.utils.sessions import SessionType +from tests.unit.reports.utils import convert_report_to_better_readable + +current_file = Path(__file__) + + +# This immitates what a report.labels_index looks like +# It's an map idx -> label, so we can go from CoverageDatapoint.label_id to the actual label +# typically via Report.lookup_label_by_id +def lookup_label(label_id: int) -> str: + lookup_table = { + 1: "simple", + 2: "one_label", + 3: "another_label", + 4: "something", + 5: "label_1", + 6: "label_2", + 7: "label_3", + 8: "label_4", + 9: "label_5", + 10: "label_6", + } + return lookup_table[label_id] + + +def create_sample_line( + *, coverage, sessionid=None, list_of_lists_of_label_ids: List[List[int]] = None +): + datapoints = [ + CoverageDatapoint( + sessionid=sessionid, + coverage=coverage, + coverage_type=None, + label_ids=label_ids, + ) + for label_ids in (list_of_lists_of_label_ids or [[]]) + ] + return ReportLine.create( + coverage=coverage, + sessions=[ + ( + LineSession( + id=sessionid, + coverage=coverage, + ) + ) + ], + datapoints=datapoints, + ) + + +def test_merge_coverage(): + assert merge_coverage(0.5, Fraction(3, 4)) == Fraction(3, 4) + assert merge_coverage(2, Fraction(3, 4)) == 2 + + +def test_change_sessionid(): + line = ReportLine.create( + 1, + sessions=[LineSession(0, 1)], + datapoints=[CoverageDatapoint(0, 1, None, None)], + ) + file = EditableReportFile(name="foo.rs") + file.append(1, line) + report = EditableReport() + report.append(file) + session = Session(0) + report.add_session(session, use_id_from_session=True) + + report.change_sessionid(0, 123) + + def assert_sessionid(report: EditableReport, id: int): + assert 0 not in report.sessions + assert id in report.sessions + + file = report.get("foo.rs") + assert file.details["present_sessions"] == [id] + line = file.get(1) + assert line.sessions[0].id == id + assert line.datapoints[0].sessionid == id + + assert_sessionid(report, 123) + + # also assert a serialization roundtrip: + report_json, chunks, _totals = report.serialize() + report_json = orjson.loads(report_json) + + report = EditableReport( + files=report_json["files"], + sessions=report_json["sessions"], + chunks=chunks.decode(), + ) + assert_sessionid(report, 123) + + report.change_sessionid(123, 234) + assert_sessionid(report, 234) + + +class TestEditableReportHelpers(object): + def test_line_without_session(self): + line = ReportLine.create(1, None, [LineSession(1, 0), LineSession(0, 1)]) + assert EditableReportFile.line_without_multiple_sessions( + line, {1} + ) == ReportLine.create(1, None, [LineSession(0, 1)]) + assert EditableReportFile.line_without_multiple_sessions( + line, {0} + ) == ReportLine.create(0, None, [LineSession(1, 0)]) + assert ( + EditableReportFile.line_without_multiple_sessions( + EditableReportFile.line_without_multiple_sessions(line, {0}), {1} + ) + == "" + ) + + def test_line_without_labels(self): + line = ReportLine.create( + "2/2", + None, + [LineSession(1, 0), LineSession(0, 1)], + datapoints=[ + CoverageDatapoint(1, 0, None, [5, 6]), + CoverageDatapoint(1, 0, None, [7, 6]), + CoverageDatapoint(0, 1, None, [5, 6]), + CoverageDatapoint(0, "1/2", None, [5, 8]), + CoverageDatapoint(0, "1/2", None, [9, 10]), + CoverageDatapoint(0, 0, None, [10, 8]), + ], + ) + assert EditableReportFile.line_without_labels( + line, {1}, {5} + ) == ReportLine.create( + "2/2", + None, + [LineSession(1, 0), LineSession(0, 1)], + datapoints=[ + CoverageDatapoint(1, 0, None, [7, 6]), + CoverageDatapoint(0, 1, None, [5, 6]), + CoverageDatapoint(0, "1/2", None, [5, 8]), + CoverageDatapoint(0, "1/2", None, [9, 10]), + CoverageDatapoint(0, 0, None, [10, 8]), + ], + ) + assert EditableReportFile.line_without_labels( + line, {1, 0}, {5} + ) == ReportLine.create( + "1/2", + None, + [LineSession(1, 0), LineSession(0, 1)], + datapoints=[ + CoverageDatapoint(1, 0, None, [7, 6]), + CoverageDatapoint(0, "1/2", None, [9, 10]), + CoverageDatapoint(0, 0, None, [10, 8]), + ], + ) + assert EditableReportFile.line_without_labels( + line, {1}, {5, 6} + ) == ReportLine.create( + "2/2", + None, + [LineSession(0, 1)], + datapoints=[ + CoverageDatapoint(0, 1, None, [5, 6]), + CoverageDatapoint(0, "1/2", None, [5, 8]), + CoverageDatapoint(0, "1/2", None, [9, 10]), + CoverageDatapoint(0, 0, None, [10, 8]), + ], + ) + assert EditableReportFile.line_without_labels( + line, {0, 1}, {5, 6} + ) == ReportLine.create( + "1/2", + None, + [LineSession(0, 1)], + datapoints=[ + CoverageDatapoint(0, "1/2", None, [9, 10]), + CoverageDatapoint(0, 0, None, [10, 8]), + ], + ) + assert EditableReportFile.line_without_labels(line, {0, 1}, {5, 6, 10}) == "" + assert EditableReportFile.line_without_labels( + line, {0, 1}, {5, 6, 9} + ) == ReportLine.create( + 0, + None, + [LineSession(0, 1)], + datapoints=[ + CoverageDatapoint(0, 0, None, [10, 8]), + ], + ) + assert EditableReportFile.line_without_labels(line, {0, 1}, {5, 6, 10}) == "" + + def test_delete_labels_session_without_datapoints(self): + line = ReportLine.create( + 1, + None, + [LineSession(0, 1), LineSession(1, 1), LineSession(2, 0)], + datapoints=[ + CoverageDatapoint(1, 1, None, [5, 6]), + CoverageDatapoint(1, 0, None, [7, 6]), + CoverageDatapoint(2, 0, None, [10]), + ], + ) + assert EditableReportFile.line_without_labels( + line, {1}, {5, 6, 10} + ) == ReportLine.create( + 1, + None, + [LineSession(0, 1), LineSession(2, 0)], + datapoints=[CoverageDatapoint(2, 0, None, [10])], + ) + + +class TestEditableReportFile(object): + def test_init(self): + chunks = "\n".join( + [ + "{}", + "[1, null, [[0, 1], [1, 0]]]", + "", + "", + "[0, null, [[0, 0], [1, 0]]]", + "[1, null, [[0, 1], [1, 1]]]", + "[1, null, [[0, 0], [1, 1]]]", + "", + "", + '[1, null, [[0, 1], [1, "1/2"]]]', + '[1, null, [[0, "1/2"], [1, 1]]]', + "", + "", + "[1, null, [[0, 1]]]", + "[1, null, [[1, 1]]]", + '["1/2", null, [[0, "1/2"], [1, 0]]]', + '["1/2", null, [[0, 0], [1, "1/2"]]]', + ] + ) + report_file = EditableReportFile(name="file.py", lines=chunks) + expected_result = [ + ( + 1, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 1), LineSession(1, 0)] + ), + ), + ( + 4, + ReportLine.create( + coverage=0, sessions=[LineSession(0, 0), LineSession(1, 0)] + ), + ), + ( + 5, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 1), LineSession(1, 1)] + ), + ), + ( + 6, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 0), LineSession(1, 1)] + ), + ), + ( + 9, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 1), LineSession(1, "1/2")] + ), + ), + ( + 10, + ReportLine.create( + coverage=1, sessions=[LineSession(0, "1/2"), LineSession(1, 1)] + ), + ), + (13, ReportLine.create(coverage=1, sessions=[LineSession(0, 1)])), + (14, ReportLine.create(coverage=1, sessions=[LineSession(1, 1)])), + ( + 15, + ReportLine.create( + coverage="1/2", sessions=[LineSession(0, "1/2"), LineSession(1, 0)] + ), + ), + ( + 16, + ReportLine.create( + coverage="1/2", sessions=[LineSession(0, 0), LineSession(1, "1/2")] + ), + ), + ] + assert list(report_file.lines) == expected_result + + def test_delete_labels_empty_line_deleted(self): + first_file = EditableReportFile("first_file.py") + first_file.append( + 1, + create_sample_line( + coverage=1, + sessionid=2, + list_of_lists_of_label_ids=[[1]], + ), + ) + assert list(first_file.lines) == [ + ( + 1, + ReportLine( + coverage=1, + type=None, + sessions=[ + LineSession( + id=2, + coverage=1, + branches=None, + partials=None, + complexity=None, + ) + ], + messages=None, + complexity=None, + datapoints=[ + CoverageDatapoint( + sessionid=2, + coverage=1, + coverage_type=None, + label_ids=[1], + ) + ], + ), + ) + ] + first_file.delete_labels([2], [1]) + assert list(first_file.lines) == [] + + def test_merge_not_previously_set_sessions_header(self): + chunks = "\n".join( + [ + "{}", + "[1, null, [[0, 1], [1, 0]]]", + "", + "", + "[0, null, [[0, 0], [1, 0]]]", + "[1, null, [[0, 1], [1, 1]]]", + "[1, null, [[0, 0], [1, 1]]]", + "", + "", + '[1, null, [[0, 1], [1, "1/2"]]]', + '[1, null, [[0, "1/2"], [1, 1]]]', + "", + "", + "[1, null, [[0, 1]]]", + "[1, null, [[1, 1]]]", + '["1/2", null, [[0, "1/2"], [1, 0]]]', + '["1/2", null, [[0, 0], [1, "1/2"]]]', + ] + ) + report_file = EditableReportFile(name="file.py", lines=chunks) + assert report_file.details == {"present_sessions": [0, 1]} + + new_chunks = "\n".join( + [ + "{}", + "[1, null, [[0, 1], [2, 0]]]", + "", + "", + "[0, null, [[2, 0], [1, 0]]]", + "[1, null, [[2, 1], [3, 1]]]", + "[1, null, [[0, 0], [1, 1]]]", + "", + "", + '[1, null, [[0, 1], [1, "1/2"]]]', + '[1, null, [[0, "1/2"], [1, 1]]]', + "", + "", + "[1, null, [[0, 1]]]", + "[1, null, [[1, 1]]]", + '["1/2", null, [[0, "1/2"], [1, 0]]]', + '["1/2", null, [[2, 0], [1, "1/2"]]]', + ] + ) + new_report_file = ReportFile(name="file.py", lines=new_chunks) + report_file.merge(new_report_file) + + assert report_file.details == {"present_sessions": [0, 1, 2, 3]} + + def test_details(self): + chunks = "\n".join(['{"some_field": "nah"}', "[1, null, [[0, 1], [1, 0]]]", ""]) + report_file = EditableReportFile(name="file.py", lines=chunks) + assert report_file.details == {"some_field": "nah", "present_sessions": [0, 1]} + + def test_merge_already_previously_set_sessions_header(self): + chunks = "\n".join( + [ + '{"present_sessions":[0,1]}', + "[1, null, [[0, 1], [1, 0]]]", + "", + "", + "[0, null, [[0, 0], [1, 0]]]", + "[1, null, [[0, 1], [1, 1]]]", + "[1, null, [[0, 0], [1, 1]]]", + "", + "", + '[1, null, [[0, 1], [1, "1/2"]]]', + '[1, null, [[0, "1/2"], [1, 1]]]', + "", + "", + "[1, null, [[0, 1]]]", + "[1, null, [[1, 1]]]", + '["1/2", null, [[0, "1/2"], [1, 0]]]', + '["1/2", null, [[0, 0], [1, "1/2"]]]', + ] + ) + report_file = EditableReportFile(name="file.py", lines=chunks) + assert report_file.details == {"present_sessions": [0, 1]} + new_chunks = "\n".join( + [ + "{}", + "[1, null, [[0, 1], [2, 0]]]", + "", + "", + "[0, null, [[2, 0], [1, 0]]]", + "[1, null, [[2, 1], [3, 1]]]", + "[1, null, [[0, 0], [1, 1]]]", + "", + "", + '[1, null, [[0, 1], [1, "1/2"]]]', + '[1, null, [[0, "1/2"], [1, 1]]]', + "", + "", + "[1, null, [[0, 1]]]", + "[1, null, [[1, 1]]]", + '["1/2", null, [[0, "1/2"], [1, 0]]]', + '["1/2", null, [[2, 0], [1, "1/2"]]]', + ] + ) + new_report_file = ReportFile(name="file.py", lines=new_chunks) + report_file.merge(new_report_file) + assert report_file.details == {"present_sessions": [0, 1, 2, 3]} + + def test_delete_session(self): + chunks = "\n".join( + [ + "{}", + "[1, null, [[0, 1], [1, 0]]]", + "", + "", + "[0, null, [[0, 0], [1, 0]]]", + "[1, null, [[0, 1], [1, 1]]]", + "[1, null, [[0, 0], [1, 1]]]", + "", + "", + '[1, null, [[0, 1], [1, "1/2"]]]', + '[1, null, [[0, "1/2"], [1, 1]]]', + "", + "", + "[1, null, [[0, 1]]]", + "[1, null, [[1, 1]]]", + '["1/2", null, [[0, "1/2"], [1, 0]]]', + '["1/2", null, [[0, 0], [1, "1/2"]]]', + ] + ) + report_file = EditableReportFile(name="file.py", lines=chunks) + assert list(report_file.lines) == [ + ( + 1, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 1), LineSession(1, 0)] + ), + ), + ( + 4, + ReportLine.create( + coverage=0, sessions=[LineSession(0, 0), LineSession(1, 0)] + ), + ), + ( + 5, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 1), LineSession(1, 1)] + ), + ), + ( + 6, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 0), LineSession(1, 1)] + ), + ), + ( + 9, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 1), LineSession(1, "1/2")] + ), + ), + ( + 10, + ReportLine.create( + coverage=1, sessions=[LineSession(0, "1/2"), LineSession(1, 1)] + ), + ), + (13, ReportLine.create(coverage=1, sessions=[LineSession(0, 1)])), + (14, ReportLine.create(coverage=1, sessions=[LineSession(1, 1)])), + ( + 15, + ReportLine.create( + coverage="1/2", sessions=[LineSession(0, "1/2"), LineSession(1, 0)] + ), + ), + ( + 16, + ReportLine.create( + coverage="1/2", sessions=[LineSession(0, 0), LineSession(1, "1/2")] + ), + ), + ] + assert report_file.totals == ReportTotals( + files=0, lines=10, hits=7, misses=1, partials=2, coverage="70.00000" + ) + + report_file.delete_multiple_sessions({1}) + + assert list(report_file.lines) == [ + (1, ReportLine.create(coverage=1, sessions=[LineSession(0, 1)])), + (4, ReportLine.create(coverage=0, sessions=[LineSession(0, 0)])), + (5, ReportLine.create(coverage=1, sessions=[LineSession(0, 1)])), + (6, ReportLine.create(coverage=0, sessions=[LineSession(0, 0)])), + (9, ReportLine.create(coverage=1, sessions=[LineSession(0, 1)])), + (10, ReportLine.create(coverage="1/2", sessions=[LineSession(0, "1/2")])), + (13, ReportLine.create(coverage=1, sessions=[LineSession(0, 1)])), + (15, ReportLine.create(coverage="1/2", sessions=[LineSession(0, "1/2")])), + (16, ReportLine.create(coverage=0, sessions=[LineSession(0, 0)])), + ] + + assert report_file.get(1) == ReportLine.create( + coverage=1, sessions=[LineSession(0, 1)] + ) + assert report_file.get(13) == ReportLine.create( + coverage=1, sessions=[LineSession(0, 1)] + ) + assert report_file.get(14) is None + assert report_file.totals == ReportTotals( + files=0, + lines=9, + hits=4, + misses=3, + partials=2, + coverage="44.44444", + ) + + assert report_file.details == {"present_sessions": [0]} + + def test_delete_session_not_present(self): + chunks = "\n".join( + [ + '{"present_sessions":[0,1]}', + "[1, null, [[0, 1], [1, 0]]]", + "", + "", + "[0, null, [[0, 0], [1, 0]]]", + "[1, null, [[0, 1], [1, 1]]]", + "[1, null, [[0, 0], [1, 1]]]", + "", + "", + '[1, null, [[0, 1], [1, "1/2"]]]', + '[1, null, [[0, "1/2"], [1, 1]]]', + "", + "", + "[1, null, [[0, 1]]]", + "[1, null, [[1, 1]]]", + '["1/2", null, [[0, "1/2"], [1, 0]]]', + '["1/2", null, [[0, 0], [1, "1/2"]]]', + ] + ) + report_file = EditableReportFile(name="file.py", lines=chunks) + original_lines = [ + ( + 1, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 1), LineSession(1, 0)] + ), + ), + ( + 4, + ReportLine.create( + coverage=0, sessions=[LineSession(0, 0), LineSession(1, 0)] + ), + ), + ( + 5, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 1), LineSession(1, 1)] + ), + ), + ( + 6, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 0), LineSession(1, 1)] + ), + ), + ( + 9, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 1), LineSession(1, "1/2")] + ), + ), + ( + 10, + ReportLine.create( + coverage=1, sessions=[LineSession(0, "1/2"), LineSession(1, 1)] + ), + ), + (13, ReportLine.create(coverage=1, sessions=[LineSession(0, 1)])), + (14, ReportLine.create(coverage=1, sessions=[LineSession(1, 1)])), + ( + 15, + ReportLine.create( + coverage="1/2", sessions=[LineSession(0, "1/2"), LineSession(1, 0)] + ), + ), + ( + 16, + ReportLine.create( + coverage="1/2", sessions=[LineSession(0, 0), LineSession(1, "1/2")] + ), + ), + ] + assert list(report_file.lines) == original_lines + report_file.delete_multiple_sessions({3}) + assert list(report_file.lines) == original_lines + assert report_file.details == {"present_sessions": [0, 1]} + + def test_delete_multiple_sessions(self): + chunks = "\n".join( + [ + '{"present_sessions":[0,1,2,3]}', + "[1, null, [[0, 1], [1, 0]]]", + "", + "", + "[0, null, [[2, 0], [1, 0]]]", + "[1, null, [[0, 1], [3, 1]]]", + "[1, null, [[0, 0], [1, 1]]]", + "", + "", + '[1, null, [[0, 1], [1, "1/2"], [2, 1], [3, 1]]]', + '[1, null, [[0, "1/2"], [1, 1]]]', + "", + "", + "[1, null, [[0, 1]]]", + "[1, null, [[1, 1]]]", + '["1/2", null, [[0, "1/2"], [1, 0]]]', + '["1/2", null, [[0, 0], [1, "1/2"]]]', + ] + ) + report_file = EditableReportFile(name="file.py", lines=chunks) + + assert list(report_file.lines) == [ + ( + 1, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 1), LineSession(1, 0)] + ), + ), + ( + 4, + ReportLine.create( + coverage=0, sessions=[LineSession(2, 0), LineSession(1, 0)] + ), + ), + ( + 5, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 1), LineSession(3, 1)] + ), + ), + ( + 6, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 0), LineSession(1, 1)] + ), + ), + ( + 9, + ReportLine.create( + coverage=1, + sessions=[ + LineSession(0, 1), + LineSession(1, "1/2"), + LineSession(2, 1), + LineSession(3, 1), + ], + ), + ), + ( + 10, + ReportLine.create( + coverage=1, sessions=[LineSession(0, "1/2"), LineSession(1, 1)] + ), + ), + (13, ReportLine.create(coverage=1, sessions=[LineSession(0, 1)])), + (14, ReportLine.create(coverage=1, sessions=[LineSession(1, 1)])), + ( + 15, + ReportLine.create( + coverage="1/2", sessions=[LineSession(0, "1/2"), LineSession(1, 0)] + ), + ), + ( + 16, + ReportLine.create( + coverage="1/2", sessions=[LineSession(0, 0), LineSession(1, "1/2")] + ), + ), + ] + + report_file.delete_multiple_sessions({1, 3, 5}) + + assert list(report_file.lines) == [ + (1, ReportLine.create(coverage=1, sessions=[LineSession(0, 1)])), + (4, ReportLine.create(coverage=0, sessions=[LineSession(2, 0)])), + (5, ReportLine.create(coverage=1, sessions=[LineSession(0, 1)])), + (6, ReportLine.create(coverage=0, sessions=[LineSession(0, 0)])), + ( + 9, + ReportLine.create( + coverage=1, sessions=[LineSession(0, 1), LineSession(2, 1)] + ), + ), + (10, ReportLine.create(coverage="1/2", sessions=[LineSession(0, "1/2")])), + (13, ReportLine.create(coverage=1, sessions=[LineSession(0, 1)])), + (15, ReportLine.create(coverage="1/2", sessions=[LineSession(0, "1/2")])), + (16, ReportLine.create(coverage=0, sessions=[LineSession(0, 0)])), + ] + + assert report_file.details == {"present_sessions": [0, 2]} + + +@pytest.fixture +def sample_report(): + report = EditableReport() + first_file = EditableReportFile("file_1.go") + first_file.append( + 1, + ReportLine.create( + coverage=1, + sessions=[LineSession(0, 1), LineSession(1, 1), LineSession(2, 1)], + complexity=(10, 2), + ), + ) + first_file.append( + 2, + ReportLine.create( + coverage=1, + sessions=[LineSession(0, 1), LineSession(1, "1/2"), LineSession(2, 1)], + complexity=(10, 2), + ), + ) + first_file.append( + 3, + ReportLine.create(coverage=1, sessions=[LineSession(0, 1)], complexity=(10, 2)), + ) + first_file.append( + 4, + ReportLine.create(coverage=1, sessions=[LineSession(1, 1)], complexity=(10, 2)), + ) + first_file.append( + 5, + ReportLine.create(coverage=1, sessions=[LineSession(2, 1)], complexity=(10, 2)), + ) + first_file.append( + 6, + ReportLine.create( + coverage=1, + sessions=[LineSession(0, 1), LineSession(1, 1)], + complexity=(10, 2), + ), + ) + first_file.append( + 7, + ReportLine.create( + coverage=1, + sessions=[LineSession(1, 1), LineSession(2, 1)], + complexity=(10, 2), + ), + ) + first_file.append( + 8, + ReportLine.create( + coverage=1, + sessions=[LineSession(2, 1), LineSession(0, 1)], + complexity=(10, 2), + ), + ) + second_file = EditableReportFile("file_2.py") + second_file.append( + 12, + ReportLine.create( + coverage=1, + sessions=[LineSession(0, 1), LineSession(1, "1/2"), LineSession(2, 0)], + ), + ) + second_file.append( + 51, + ReportLine.create( + coverage="1/2", + type="b", + sessions=[LineSession(0, "1/3"), LineSession(1, "1/2")], + ), + ) + single_session_file = EditableReportFile("single_session_file.c") + single_session_file.append( + 101, ReportLine.create(coverage="1/2", sessions=[LineSession(1, "1/2")]) + ) + single_session_file.append( + 110, ReportLine.create(coverage=1, sessions=[LineSession(1, 1)]) + ) + single_session_file.append( + 111, ReportLine.create(coverage=0, sessions=[LineSession(1, 0)]) + ) + report.append(first_file) + report.append(second_file) + report.append(single_session_file) + report.add_session(Session(id=0, flags=["unit"])) + report.add_session( + Session(id=1, flags=["integration"], session_type=SessionType.carriedforward) + ) + report.add_session(Session(id=2, flags=None)) + return report + + +class TestEditableReport(object): + @pytest.fixture + def sample_with_labels_report(self): + first_report = EditableReport() + first_report.add_session( + Session( + flags=["enterprise"], + sessionid=0, + session_type=SessionType.carriedforward, + ) + ) + first_report.add_session( + Session( + flags=["enterprise"], sessionid=1, session_type=SessionType.uploaded + ) + ) + first_report.add_session( + Session( + flags=["unit"], sessionid=2, session_type=SessionType.carriedforward + ) + ) + first_report.add_session( + Session(flags=["unrelated"], sessionid=3, session_type=SessionType.uploaded) + ) + first_file = EditableReportFile("first_file.py") + c = 0 + for list_of_lists_of_label_ids in [ + [[2]], + [[3]], + [[3], [2]], + [[3, 2]], + [[4]], + ]: + for sessionid in range(4): + first_file.append( + c % 7 + 1, + create_sample_line( + coverage=c, + sessionid=sessionid, + list_of_lists_of_label_ids=list_of_lists_of_label_ids, + ), + ) + c += 1 + first_file.append(23, ReportLine.create(1, sessions=[LineSession(1, 1)])) + second_file = EditableReportFile("second_file.py") + first_report.append(first_file) + first_report.append(second_file) + assert convert_report_to_better_readable(first_report)["archive"] == { + "first_file.py": [ + ( + 1, + 14, + None, + [ + [0, 0, None, None, None], + [3, 7, None, None, None], + [2, 14, None, None, None], + ], + None, + None, + [ + (0, 0, None, [2]), + (2, 14, None, [3, 2]), + (3, 7, None, [3]), + ], + ), + ( + 2, + 15, + None, + [ + [1, 1, None, None, None], + [0, 8, None, None, None], + [3, 15, None, None, None], + ], + None, + None, + [ + (0, 8, None, [2]), + (0, 8, None, [3]), + (1, 1, None, [2]), + (3, 15, None, [3, 2]), + ], + ), + ( + 3, + 16, + None, + [ + [2, 2, None, None, None], + [1, 9, None, None, None], + [0, 16, None, None, None], + ], + None, + None, + [ + (0, 16, None, [4]), + (1, 9, None, [2]), + (1, 9, None, [3]), + (2, 2, None, [2]), + ], + ), + ( + 4, + 17, + None, + [ + [3, 3, None, None, None], + [2, 10, None, None, None], + [1, 17, None, None, None], + ], + None, + None, + [ + (1, 17, None, [4]), + (2, 10, None, [2]), + (2, 10, None, [3]), + (3, 3, None, [2]), + ], + ), + ( + 5, + 18, + None, + [ + [0, 4, None, None, None], + [3, 11, None, None, None], + [2, 18, None, None, None], + ], + None, + None, + [ + (0, 4, None, [3]), + (2, 18, None, [4]), + (3, 11, None, [2]), + (3, 11, None, [3]), + ], + ), + ( + 6, + 19, + None, + [ + [1, 5, None, None, None], + [0, 12, None, None, None], + [3, 19, None, None, None], + ], + None, + None, + [ + (0, 12, None, [3, 2]), + (1, 5, None, [3]), + (3, 19, None, [4]), + ], + ), + ( + 7, + 13, + None, + [[2, 6, None, None, None], [1, 13, None, None, None]], + None, + None, + [ + (1, 13, None, [3, 2]), + (2, 6, None, [3]), + ], + ), + (23, 1, None, [[1, 1, None, None, None]], None, None), + ] + } + + return first_report + + def test_delete_labels_empty_file_deleted(self): + report = EditableReport() + first_file = EditableReportFile("first_file.py") + some_other_file = EditableReportFile("someother.py") + some_other_file.append(1, ReportLine.create(1, sessions=[LineSession(2, 1)])) + first_file.append( + 1, + create_sample_line( + coverage=1, + sessionid=2, + list_of_lists_of_label_ids=[[1]], + ), + ) + report.append(first_file) + report.append(some_other_file) + assert report.files == ["first_file.py", "someother.py"] + assert convert_report_to_better_readable(report)["archive"] == { + "first_file.py": [ + ( + 1, + 1, + None, + [[2, 1, None, None, None]], + None, + None, + [(2, 1, None, [1])], + ) + ], + "someother.py": [(1, 1, None, [[2, 1, None, None, None]], None, None)], + } + report.delete_labels([2], [1]) + assert convert_report_to_better_readable(report)["archive"] == { + "someother.py": [(1, 1, None, [[2, 1, None, None, None]], None, None)] + } + + def test_delete_session(self, sample_report): + report = sample_report + + assert convert_report_to_better_readable(report) == { + "archive": { + "file_1.go": [ + ( + 1, + 1, + None, + [ + [0, 1, None, None, None], + [1, 1, None, None, None], + [2, 1, None, None, None], + ], + None, + (10, 2), + ), + ( + 2, + 1, + None, + [ + [0, 1, None, None, None], + [1, "1/2", None, None, None], + [2, 1, None, None, None], + ], + None, + (10, 2), + ), + (3, 1, None, [[0, 1, None, None, None]], None, (10, 2)), + (4, 1, None, [[1, 1, None, None, None]], None, (10, 2)), + (5, 1, None, [[2, 1, None, None, None]], None, (10, 2)), + ( + 6, + 1, + None, + [[0, 1, None, None, None], [1, 1, None, None, None]], + None, + (10, 2), + ), + ( + 7, + 1, + None, + [[1, 1, None, None, None], [2, 1, None, None, None]], + None, + (10, 2), + ), + ( + 8, + 1, + None, + [[2, 1, None, None, None], [0, 1, None, None, None]], + None, + (10, 2), + ), + ], + "file_2.py": [ + ( + 12, + 1, + None, + [ + [0, 1, None, None, None], + [1, "1/2", None, None, None], + [2, 0, None, None, None], + ], + None, + None, + ), + ( + 51, + "1/2", + "b", + [[0, "1/3", None, None, None], [1, "1/2", None, None, None]], + None, + None, + ), + ], + "single_session_file.c": [ + (101, "1/2", None, [[1, "1/2", None, None, None]], None, None), + (110, 1, None, [[1, 1, None, None, None]], None, None), + (111, 0, None, [[1, 0, None, None, None]], None, None), + ], + }, + "report": { + "files": { + "file_1.go": [ + 0, + [0, 8, 8, 0, 0, "100", 0, 0, 0, 0, 80, 16, 0], + None, + None, + ], + "file_2.py": [ + 1, + [0, 2, 1, 0, 1, "50.00000", 1, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "single_session_file.c": [ + 2, + [0, 3, 1, 1, 1, "33.33333", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + }, + "sessions": { + "0": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["unit"], + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "se": {}, + "t": None, + "u": None, + }, + "1": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["integration"], + "j": None, + "n": None, + "p": None, + "st": "carriedforward", + "se": {}, + "t": None, + "u": None, + }, + "2": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": None, + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "se": {}, + "t": None, + "u": None, + }, + }, + }, + "totals": { + "C": 80, + "M": 0, + "N": 16, + "b": 1, + "c": "76.92308", + "d": 0, + "diff": None, + "f": 3, + "h": 10, + "m": 1, + "n": 13, + "p": 2, + "s": 3, + }, + } + report.delete_multiple_sessions({1}) + expected_result = { + "archive": { + "file_1.go": [ + ( + 1, + 1, + None, + [[0, 1, None, None, None], [2, 1, None, None, None]], + None, + (10, 2), + ), + ( + 2, + 1, + None, + [[0, 1, None, None, None], [2, 1, None, None, None]], + None, + (10, 2), + ), + (3, 1, None, [[0, 1, None, None, None]], None, (10, 2)), + (5, 1, None, [[2, 1, None, None, None]], None, (10, 2)), + (6, 1, None, [[0, 1, None, None, None]], None, (10, 2)), + (7, 1, None, [[2, 1, None, None, None]], None, (10, 2)), + ( + 8, + 1, + None, + [[2, 1, None, None, None], [0, 1, None, None, None]], + None, + (10, 2), + ), + ], + "file_2.py": [ + ( + 12, + 1, + None, + [[0, 1, None, None, None], [2, 0, None, None, None]], + None, + None, + ), + (51, "1/3", "b", [[0, "1/3", None, None, None]], None, None), + ], + }, + "report": { + "files": { + "file_1.go": [ + 0, + [0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 70, 14, 0], + None, + None, + ], + "file_2.py": [ + 1, + [0, 2, 1, 0, 1, "50.00000", 1, 0, 0, 0, 0, 0, 0], + None, + None, + ], + }, + "sessions": { + "0": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["unit"], + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "se": {}, + "t": None, + "u": None, + }, + "2": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": None, + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "se": {}, + "t": None, + "u": None, + }, + }, + }, + "totals": { + "C": 70, + "M": 0, + "N": 14, + "b": 1, + "c": "88.88889", + "d": 0, + "diff": None, + "f": 2, + "h": 8, + "m": 0, + "n": 9, + "p": 1, + "s": 2, + }, + } + res = convert_report_to_better_readable(report) + assert res["archive"] == expected_result["archive"] + assert res["report"] == expected_result["report"] + assert res["totals"] == expected_result["totals"] + assert res == expected_result + report.delete_multiple_sessions({0, 2}) + assert convert_report_to_better_readable(report) == { + "archive": {}, + "report": {"files": {}, "sessions": {}}, + "totals": { + "C": 0, + "M": 0, + "N": 0, + "b": 0, + "c": None, + "d": 0, + "diff": None, + "f": 0, + "h": 0, + "m": 0, + "n": 0, + "p": 0, + "s": 0, + }, + } + + def test_add_conflicting_session(self, sample_report): + report = sample_report + old_readable = convert_report_to_better_readable(report) + assert old_readable == { + "archive": { + "file_1.go": [ + ( + 1, + 1, + None, + [ + [0, 1, None, None, None], + [1, 1, None, None, None], + [2, 1, None, None, None], + ], + None, + (10, 2), + ), + ( + 2, + 1, + None, + [ + [0, 1, None, None, None], + [1, "1/2", None, None, None], + [2, 1, None, None, None], + ], + None, + (10, 2), + ), + (3, 1, None, [[0, 1, None, None, None]], None, (10, 2)), + (4, 1, None, [[1, 1, None, None, None]], None, (10, 2)), + (5, 1, None, [[2, 1, None, None, None]], None, (10, 2)), + ( + 6, + 1, + None, + [[0, 1, None, None, None], [1, 1, None, None, None]], + None, + (10, 2), + ), + ( + 7, + 1, + None, + [[1, 1, None, None, None], [2, 1, None, None, None]], + None, + (10, 2), + ), + ( + 8, + 1, + None, + [[2, 1, None, None, None], [0, 1, None, None, None]], + None, + (10, 2), + ), + ], + "file_2.py": [ + ( + 12, + 1, + None, + [ + [0, 1, None, None, None], + [1, "1/2", None, None, None], + [2, 0, None, None, None], + ], + None, + None, + ), + ( + 51, + "1/2", + "b", + [[0, "1/3", None, None, None], [1, "1/2", None, None, None]], + None, + None, + ), + ], + "single_session_file.c": [ + (101, "1/2", None, [[1, "1/2", None, None, None]], None, None), + (110, 1, None, [[1, 1, None, None, None]], None, None), + (111, 0, None, [[1, 0, None, None, None]], None, None), + ], + }, + "report": { + "files": { + "file_1.go": [ + 0, + [0, 8, 8, 0, 0, "100", 0, 0, 0, 0, 80, 16, 0], + None, + None, + ], + "file_2.py": [ + 1, + [0, 2, 1, 0, 1, "50.00000", 1, 0, 0, 0, 0, 0, 0], + None, + None, + ], + "single_session_file.c": [ + 2, + [0, 3, 1, 1, 1, "33.33333", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ], + }, + "sessions": { + "0": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["unit"], + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "se": {}, + "t": None, + "u": None, + }, + "1": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": ["integration"], + "j": None, + "n": None, + "p": None, + "st": "carriedforward", + "se": {}, + "t": None, + "u": None, + }, + "2": { + "N": None, + "a": None, + "c": None, + "d": None, + "e": None, + "f": None, + "j": None, + "n": None, + "p": None, + "st": "uploaded", + "se": {}, + "t": None, + "u": None, + }, + }, + }, + "totals": { + "C": 80, + "M": 0, + "N": 16, + "b": 1, + "c": "76.92308", + "d": 0, + "diff": None, + "f": 3, + "h": 10, + "m": 1, + "n": 13, + "p": 2, + "s": 3, + }, + } + report.add_session( + Session( + sessionid=3, session_type=SessionType.uploaded, flags=["integration"] + ) + ) + res = convert_report_to_better_readable(report) + assert res["archive"] == old_readable["archive"] + assert res["report"]["files"] == old_readable["report"]["files"] + assert old_readable["totals"].pop("s") == 3 + assert res["totals"].pop("s") == 4 + assert res["totals"] == old_readable["totals"] + + def test_delete_labels(self, sample_with_labels_report): + sample_with_labels_report.delete_labels([0], [3]) + for file in sample_with_labels_report: + for ln, line in file.lines: + # some lines previously didnt have datapoints + if line.datapoints: + for dp in line.datapoints: + assert dp.sessionid != 0 or 3 not in dp.label_ids + res = convert_report_to_better_readable(sample_with_labels_report) + expected_result = { + "totals": { + "f": 1, + "n": 8, + "h": 8, + "m": 0, + "p": 0, + "c": "100", + "b": 0, + "d": 0, + "M": 0, + "s": 4, + "C": 0, + "N": 0, + "diff": None, + }, + "report": { + "files": { + "first_file.py": [ + 0, + [0, 8, 8, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + None, + None, + ] + }, + "sessions": { + "0": { + "t": None, + "d": None, + "a": None, + "f": ["enterprise"], + "c": None, + "n": None, + "N": None, + "j": None, + "u": None, + "p": None, + "e": None, + "st": "carriedforward", + "se": {}, + }, + "1": { + "t": None, + "d": None, + "a": None, + "f": ["enterprise"], + "c": None, + "n": None, + "N": None, + "j": None, + "u": None, + "p": None, + "e": None, + "st": "uploaded", + "se": {}, + }, + "2": { + "t": None, + "d": None, + "a": None, + "f": ["unit"], + "c": None, + "n": None, + "N": None, + "j": None, + "u": None, + "p": None, + "e": None, + "st": "carriedforward", + "se": {}, + }, + "3": { + "t": None, + "d": None, + "a": None, + "f": ["unrelated"], + "c": None, + "n": None, + "N": None, + "j": None, + "u": None, + "p": None, + "e": None, + "st": "uploaded", + "se": {}, + }, + }, + }, + "archive": { + "first_file.py": [ + ( + 1, + 14, + None, + [ + [0, 0, None, None, None], + [3, 7, None, None, None], + [2, 14, None, None, None], + ], + None, + None, + [ + (0, 0, None, [2]), + (2, 14, None, [3, 2]), + (3, 7, None, [3]), + ], + ), + ( + 2, + 15, + None, + [ + [1, 1, None, None, None], + [0, 8, None, None, None], + [3, 15, None, None, None], + ], + None, + None, + [ + (0, 8, None, [2]), + (1, 1, None, [2]), + (3, 15, None, [3, 2]), + ], + ), + ( + 3, + 16, + None, + [ + [2, 2, None, None, None], + [1, 9, None, None, None], + [0, 16, None, None, None], + ], + None, + None, + [ + (0, 16, None, [4]), + (1, 9, None, [2]), + (1, 9, None, [3]), + (2, 2, None, [2]), + ], + ), + ( + 4, + 17, + None, + [ + [3, 3, None, None, None], + [2, 10, None, None, None], + [1, 17, None, None, None], + ], + None, + None, + [ + (1, 17, None, [4]), + (2, 10, None, [2]), + (2, 10, None, [3]), + (3, 3, None, [2]), + ], + ), + ( + 5, + 18, + None, + [[3, 11, None, None, None], [2, 18, None, None, None]], + None, + None, + [ + (2, 18, None, [4]), + (3, 11, None, [2]), + (3, 11, None, [3]), + ], + ), + ( + 6, + 19, + None, + [[1, 5, None, None, None], [3, 19, None, None, None]], + None, + None, + [(1, 5, None, [3]), (3, 19, None, [4])], + ), + ( + 7, + 13, + None, + [[2, 6, None, None, None], [1, 13, None, None, None]], + None, + None, + [ + (1, 13, None, [3, 2]), + (2, 6, None, [3]), + ], + ), + (23, 1, None, [[1, 1, None, None, None]], None, None), + ] + }, + } + assert res["report"]["sessions"] == expected_result["report"]["sessions"] + assert ( + res["report"]["files"]["first_file.py"] + == expected_result["report"]["files"]["first_file.py"] + ) + assert res["report"]["files"] == expected_result["report"]["files"] + assert res == expected_result diff --git a/libs/shared/tests/unit/reports/test_enums.py b/libs/shared/tests/unit/reports/test_enums.py new file mode 100644 index 0000000000..d827216039 --- /dev/null +++ b/libs/shared/tests/unit/reports/test_enums.py @@ -0,0 +1,13 @@ +from shared.reports.enums import UploadState, UploadType + + +def test_enums(): + assert UploadState.choices() == ( + (1, "UPLOADED"), + (2, "PROCESSED"), + (3, "ERROR"), + (4, "FULLY_OVERWRITTEN"), + (5, "PARTIALLY_OVERWRITTEN"), + # (6, "PARALLEL_PROCESSED"), + ) + assert UploadType.choices() == ((1, "UPLOADED"), (2, "CARRIEDFORWARD")) diff --git a/libs/shared/tests/unit/reports/test_readonly.py b/libs/shared/tests/unit/reports/test_readonly.py new file mode 100644 index 0000000000..1e988e4291 --- /dev/null +++ b/libs/shared/tests/unit/reports/test_readonly.py @@ -0,0 +1,475 @@ +from pathlib import Path + +import pytest + +from shared.reports.readonly import LazyRustReport, ReadOnlyReport +from shared.reports.types import ReportTotals +from shared.utils.sessions import Session, SessionType + +current_file = Path(__file__) + + +@pytest.fixture +def sample_rust_report(): + with open(current_file.parent / "samples" / "chunks_01.txt", "r") as f: + chunks = f.read() + files_dict = { + "awesome/__init__.py": [ + 2, + [0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0], + [[0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0]], + [0, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + ], + "tests/__init__.py": [ + 0, + [0, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0], + [[0, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "tests/test_sample.py": [ + 1, + [0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + [[0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + } + sessions_dict = { + "0": { + "N": None, + "a": "v4/raw/2019-01-10/4434BC2A2EC4FCA57F77B473D83F928C/abf6d4df662c47e32460020ab14abf9303581429/9ccc55a1-8b41-4bb1-a946-ee7a33a7fb56.txt", + "c": None, + "d": 1547084427, + "e": None, + "f": ["unit"], + "j": None, + "n": None, + "p": None, + "t": [3, 20, 17, 3, 0, "85.00000", 0, 0, 0, 0, 0, 0, 0], + "": None, + } + } + return ReadOnlyReport.from_chunks( + chunks=chunks, files=files_dict, sessions=sessions_dict + ) + + +class TestLazyRustReport(object): + @pytest.mark.parametrize("chunks_file", ["chunks_01.txt", "chunks_03.txt"]) + def test_get_report(self, chunks_file): + with open(current_file.parent / "samples" / chunks_file, "r") as f: + chunks = f.read() + filename_mapping = { + "awesome/__init__.py": 2, + "tests/__init__.py": 0, + "tests/test_sample.py": 1, + } + session_mapping = {0: ["unit"]} + r = LazyRustReport(filename_mapping, chunks, session_mapping) + assert r is not None + assert r.get_report() is not None + + +class TestReadOnly(object): + @pytest.mark.parametrize( + "report_header", [{}, {"labels_index": {0: "special_label", 1: "some_test"}}] + ) + def test_create_from_report(self, sample_report, report_header): + sample_report._header = report_header + r = ReadOnlyReport.create_from_report(sample_report) + assert r.rust_report is not None + assert r.totals == sample_report.totals + assert r.sessions[0] == sample_report.sessions[0] + assert r.sessions == sample_report.sessions + assert sorted(r.flags.keys()) == ["complex", "simple"] + assert r.flags["complex"].totals.asdict() == { + "files": 2, + "lines": 6, + "hits": 2, + "misses": 2, + "partials": 2, + "coverage": "33.33333", + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 2, + "complexity": 0, + "complexity_total": 0, + "diff": 0, + } + assert r.apply_diff( + { + "files": { + "file_1.go": { + "type": "modified", + "segments": [{"header": list("1313"), "lines": list("---+++")}], + }, + "location/file_1.py": { + "type": "modified", + "segments": [ + { + "header": ["100", "3", "100", "3"], + "lines": list("-+-+-+"), + } + ], + }, + "deleted.py": {"type": "deleted"}, + } + } + ).asdict() == { + "branches": 2, + "complexity": 0, + "complexity_total": 0, + "coverage": "60.00000", + "diff": 0, + "files": 2, + "hits": 3, + "lines": 5, + "messages": 0, + "methods": 0, + "misses": 0, + "partials": 2, + "sessions": 0, + } + res = r.calculate_diff( + { + "files": { + "file_1.go": { + "type": "modified", + "segments": [{"header": list("1313"), "lines": list("---+++")}], + }, + "location/file_1.py": { + "type": "modified", + "segments": [ + { + "header": ["100", "3", "100", "3"], + "lines": list("-+-+-+"), + } + ], + }, + "deleted.py": {"type": "deleted"}, + } + } + ) + assert res["general"].asdict() == dict( + files=2, + lines=5, + hits=3, + misses=0, + partials=2, + coverage="60.00000", + branches=2, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + assert res["files"]["location/file_1.py"].asdict() == dict( + files=0, + lines=2, + hits=0, + misses=0, + partials=2, + coverage="0", + branches=2, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + assert res["files"]["file_1.go"].asdict() == dict( + files=0, + lines=3, + hits=3, + misses=0, + partials=0, + coverage="100", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + + def test_get_file_totals(self, sample_report, mocker): + r = ReadOnlyReport.create_from_report(sample_report) + assert r.get_file_totals("location/file_1.py") == ReportTotals( + files=0, + lines=2, + hits=0, + misses=0, + partials=2, + coverage="0", + branches=2, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + + def test_from_chunks_with_totals(self, mocker): + mocked_process_totals = mocker.patch.object(ReadOnlyReport, "_process_totals") + with open(current_file.parent / "samples" / "chunks_01.txt", "r") as f: + chunks = f.read() + files_dict = { + "awesome/__init__.py": [ + 2, + [0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0], + [[0, 10, 8, 2, 0, "80.00000", 0, 0, 0, 0, 0, 0, 0]], + [0, 2, 1, 1, 0, "50.00000", 0, 0, 0, 0, 0, 0, 0], + ], + "tests/__init__.py": [ + 0, + [0, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0], + [[0, 3, 2, 1, 0, "66.66667", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + "tests/test_sample.py": [ + 1, + [0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0], + [[0, 7, 7, 0, 0, "100", 0, 0, 0, 0, 0, 0, 0]], + None, + ], + } + sessions_dict = { + "0": { + "N": None, + "a": "v4/raw/2019-01-10/4434BC2A2EC4FCA57F77B473D83F928C/abf6d4df662c47e32460020ab14abf9303581429/9ccc55a1-8b41-4bb1-a946-ee7a33a7fb56.txt", + "c": None, + "d": 1547084427, + "e": None, + "f": ["unit"], + "j": None, + "n": None, + "p": None, + "t": [3, 20, 17, 3, 0, "85.00000", 0, 0, 0, 0, 0, 0, 0], + "": None, + } + } + r = ReadOnlyReport.from_chunks( + chunks=chunks, + files=files_dict, + sessions=sessions_dict, + totals={ + "f": 3, + "n": 20, + "h": 17, + "m": 3, + "p": 0, + "c": "85.00000", + "b": 0, + "d": 0, + "M": 0, + "s": 1, + "C": 0, + "N": 0, + }, + ) + assert r._totals == ReportTotals( + files=3, + lines=20, + hits=17, + misses=3, + partials=0, + coverage="85.00000", + branches=0, + methods=0, + messages=0, + sessions=1, + complexity=0, + complexity_total=0, + diff=0, + ) + assert r.totals == r._totals + assert not mocked_process_totals.called + + def test_filter_totals(self, sample_report): + r = ReadOnlyReport.create_from_report(sample_report) + assert r.filter(paths=[".*.go"]).totals.asdict() == { + "files": 2, + "lines": 7, + "hits": 4, + "misses": 1, + "partials": 2, + "coverage": "57.14286", + "branches": 1, + "methods": 0, + "messages": 0, + "sessions": 4, + "complexity": 0, + "complexity_total": 0, + "diff": 0, + } + + def test_filter_none(self, sample_rust_report): + assert sample_rust_report.rust_report is not None + assert sample_rust_report.rust_report.get_report() is not None + assert sample_rust_report.filter() is sample_rust_report + + def test_init(self, sample_rust_report): + report = sample_rust_report + assert report.totals.asdict() == { + "files": 3, + "lines": 20, + "hits": 17, + "misses": 3, + "partials": 0, + "coverage": "85.00000", + "branches": 0, + "methods": 0, + "messages": 0, + "sessions": 1, + "complexity": 0, + "complexity_total": 0, + "diff": 0, + } + assert report.files == [ + "awesome/__init__.py", + "tests/__init__.py", + "tests/test_sample.py", + ] + assert sorted(f.name for f in report) == [ + "awesome/__init__.py", + "tests/__init__.py", + "tests/test_sample.py", + ] + + def test_get(self, sample_rust_report): + assert sample_rust_report.get("awesome/__init__.py").totals == ReportTotals( + files=0, + lines=10, + hits=8, + misses=2, + partials=0, + coverage="80.00000", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + + def test_differing_totals_calculation(self, mocker, sample_report): + rust_analyzer = mocker.MagicMock( + get_totals=mocker.MagicMock(return_value=ReportTotals(1, 999)) + ) + rust_report = mocker.MagicMock() + r = ReadOnlyReport(rust_analyzer, rust_report, sample_report) + assert r.totals == ReportTotals(1, 999) + + def test_differing_file_count_calculation(self, mocker, sample_report): + rust_analyzer = mocker.MagicMock( + get_totals=mocker.MagicMock( + return_value=ReportTotals( + files=300, + lines=9, + hits=4, + misses=1, + partials=4, + coverage="44.44444", + branches=3, + methods=0, + messages=0, + sessions=4, + complexity=0, + complexity_total=0, + diff=0, + ) + ) + ) + rust_report = mocker.MagicMock() + r = ReadOnlyReport(rust_analyzer, rust_report, sample_report) + assert r.totals == ReportTotals( + files=300, + lines=9, + hits=4, + misses=1, + partials=4, + coverage="44.44444", + branches=3, + methods=0, + messages=0, + sessions=4, + complexity=0, + complexity_total=0, + diff=0, + ) + + def test_already_done_calculation(self, mocker, sample_rust_report): + k = mocker.MagicMock() + sample_rust_report._totals = k + assert sample_rust_report.totals is k + + def test_inner_report_already_precalculated(self, mocker): + inner_totals = mocker.MagicMock() + rust_totals = mocker.MagicMock() + rust_analyzer, rust_report = ( + mocker.MagicMock(get_totals=mocker.MagicMock(return_value=rust_totals)), + mocker.MagicMock(), + ) + inner_report = mocker.MagicMock( + has_precalculated_totals=mocker.MagicMock(return_value=True), + totals=inner_totals, + ) + report = ReadOnlyReport(rust_analyzer, rust_report, inner_report) + assert report.totals is inner_totals + + def test_inner_report_not_precalculated(self, mocker): + inner_totals = mocker.MagicMock() + rust_totals = mocker.MagicMock() + rust_analyzer, rust_report = ( + mocker.MagicMock(get_totals=mocker.MagicMock(return_value=rust_totals)), + mocker.MagicMock(), + ) + inner_report = mocker.MagicMock( + has_precalculated_totals=mocker.MagicMock(return_value=False), + totals=inner_totals, + ) + report = ReadOnlyReport(rust_analyzer, rust_report, inner_report) + res = report.totals + assert isinstance(res, ReportTotals) + assert res.files is rust_totals.files + assert res.lines is rust_totals.lines + assert res.hits is rust_totals.hits + assert res.misses is rust_totals.misses + assert res.partials is rust_totals.partials + assert res.coverage is rust_totals.coverage + assert res.branches is rust_totals.branches + assert res.methods is rust_totals.methods + assert res.messages == 0 + assert res.sessions is rust_totals.sessions + assert res.complexity is rust_totals.complexity + assert res.complexity_total is rust_totals.complexity_total + assert res.diff == 0 + + def test_get_uploaded_flags_only_uploaded(self, sample_report): + r = ReadOnlyReport.create_from_report(sample_report) + assert r.get_uploaded_flags() == set(["complex", "simple"]) + + def test_get_uploaded_flags(self, sample_report): + sample_report.add_session( + Session(flags=["banana", "apple"], session_type=SessionType.carriedforward) + ) + sample_report.add_session( + Session(flags=["sugar"], session_type=SessionType.carriedforward) + ) + sample_report.add_session( + Session(flags=["chocolate", "apple"], session_type=SessionType.uploaded) + ) + r = ReadOnlyReport.create_from_report(sample_report) + assert r.get_uploaded_flags() == set( + ["complex", "simple", "apple", "chocolate"] + ) + # second call to use the cached value + assert r.get_uploaded_flags() == set( + ["complex", "simple", "apple", "chocolate"] + ) diff --git a/libs/shared/tests/unit/reports/test_types.py b/libs/shared/tests/unit/reports/test_types.py new file mode 100644 index 0000000000..4d0cbef4c5 --- /dev/null +++ b/libs/shared/tests/unit/reports/test_types.py @@ -0,0 +1,135 @@ +from decimal import Decimal + +from shared.reports.types import ( + Change, + CoverageDatapoint, + LineSession, + NetworkFile, + ReportLine, + ReportTotals, +) + + +def test_changes_init(): + change = Change( + path="modified.py", + in_diff=True, + totals=ReportTotals( + files=0, + lines=0, + hits=-2, + misses=1, + partials=0, + coverage=-23.333330000000004, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ) + assert isinstance(change.totals, ReportTotals) + assert change.totals == ReportTotals( + files=0, + lines=0, + hits=-2, + misses=1, + partials=0, + coverage=-23.333330000000004, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + + +def test_changes_init_no_internal_types(): + change = Change( + path="modified.py", + in_diff=True, + totals=[0, 0, -2, 1, 0, -23.333330000000004, 0, 0, 0, 0, 0, 0, 0], + ) + assert isinstance(change.totals, ReportTotals) + assert change.totals == ReportTotals( + files=0, + lines=0, + hits=-2, + misses=1, + partials=0, + coverage=-23.333330000000004, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + + +def test_reportline_as_tuple(): + report_line = ReportLine.create( + coverage=Decimal("10"), + type="b", + sessions=[LineSession(1, 0), LineSession(2, "1/2", 1)], + complexity="10", + ) + assert report_line.astuple() == ( + Decimal("10"), + "b", + [(1, 0), (2, "1/2", 1, None, None)], + None, + "10", + None, + ) + + +def test_coverage_datapoint_as_tuple(): + cd = CoverageDatapoint( + sessionid=3, coverage=1, coverage_type="b", label_ids=[1, 2, 3] + ) + assert cd.astuple() == (3, 1, "b", [1, 2, 3]) + cd = CoverageDatapoint( + sessionid=3, coverage=1, coverage_type="b", label_ids=["1", "2", "3"] + ) + assert cd.astuple() == (3, 1, "b", [1, 2, 3]) + + +class TestNetworkFile(object): + def test_networkfile_as_tuple(self): + network_file = NetworkFile( + totals=ReportTotals( + files=0, + lines=0, + hits=-2, + misses=1, + partials=0, + coverage=-23.333330000000004, + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + diff_totals=None, + ) + assert network_file.astuple() == ( + (0, 0, -2, 1, 0, -23.333330000000004, 0, 0, 0, 0, 0, 0, 0), + None, + None, + ) + + +class TestReportTotals(object): + def test_encoded_report_total(self): + obj = ReportTotals(*[0, 35, 35, 0, 0, "100", 5, 0, 0, 0, 0, 0, 0]) + obj_1 = ReportTotals(*[0, 35, 35, 0, 0, "100", 5]) + assert obj == obj_1 + assert obj.to_database() == obj_1.to_database() diff --git a/libs/shared/tests/unit/reports/utils.py b/libs/shared/tests/unit/reports/utils.py new file mode 100644 index 0000000000..22a34428d8 --- /dev/null +++ b/libs/shared/tests/unit/reports/utils.py @@ -0,0 +1,55 @@ +import dataclasses +from json import loads + +from shared.reports.resources import Report +from shared.reports.types import TOTALS_MAP + + +def legacy_totals(report: Report) -> dict: + totals = dict(zip(TOTALS_MAP, report.totals)) + totals["diff"] = report.diff_totals + return totals + + +def convert_report_to_better_readable(report: Report) -> dict: + report_json, _chunks, _totals = report.serialize() + + totals_dict = legacy_totals(report) + report_dict = loads(report_json) + report_dict.pop("totals") + archive_dict = {} + for filename in report.files: + file_report = report.get(filename) + lines = [] + for line_number, line in file_report.lines: + ( + coverage, + line_type, + sessions, + messages, + complexity, + datapoints, + ) = dataclasses.astuple(line) + sessions = [list(s) for s in sessions] + lines.append( + ( + line_number, + coverage, + line_type, + sessions, + messages, + complexity, + datapoints, + ) + if datapoints is not None + else ( + line_number, + coverage, + line_type, + sessions, + messages, + complexity, + ) + ) + archive_dict[filename] = lines + return {"archive": archive_dict, "report": report_dict, "totals": totals_dict} diff --git a/libs/shared/tests/unit/self_hosted/test_self_hosted.py b/libs/shared/tests/unit/self_hosted/test_self_hosted.py new file mode 100644 index 0000000000..0c839b1043 --- /dev/null +++ b/libs/shared/tests/unit/self_hosted/test_self_hosted.py @@ -0,0 +1,260 @@ +from unittest.mock import patch + +import pytest +from django.test import TestCase, override_settings + +from shared.django_apps.codecov_auth.models import Owner +from shared.django_apps.core.tests.factories import OwnerFactory +from shared.license import LicenseInformation +from shared.self_hosted.service import ( + LicenseException, + activate_owner, + activated_owners, + admin_owners, + can_activate_owner, + deactivate_owner, + disable_autoactivation, + enable_autoactivation, + is_activated_owner, + is_admin_owner, + is_autoactivation_enabled, + license_seats, +) + + +@pytest.fixture +def dbsession(db): + return db + + +@override_settings(IS_ENTERPRISE=True) +@patch("shared.self_hosted.service.get_config") +def test_admin_owners(mock_get_config, dbsession): + owner1 = OwnerFactory(service="github", username="foo") + OwnerFactory(service="github", username="bar") + owner3 = OwnerFactory(service="gitlab", username="foo") + + mock_get_config.return_value = [ + {"service": "github", "username": "foo"}, + {"service": "gitlab", "username": "foo"}, + ] + + owners = admin_owners() + assert list(owners) == [owner1, owner3] + + mock_get_config.assert_called_once_with("setup", "admins", default=[]) + + +@override_settings(IS_ENTERPRISE=True) +def test_admin_owners_empty(dbsession): + OwnerFactory(service="github", username="foo") + OwnerFactory(service="github", username="bar") + OwnerFactory(service="gitlab", username="foo") + + owners = admin_owners() + assert list(owners) == [] + + +@override_settings(IS_ENTERPRISE=True) +@patch("shared.self_hosted.service.admin_owners") +def test_is_admin_owner(admin_owners, dbsession): + owner1 = OwnerFactory(service="github", username="foo") + owner2 = OwnerFactory(service="github", username="bar") + owner3 = OwnerFactory(service="gitlab", username="foo") + + admin_owners.return_value = Owner.objects.filter(pk__in=[owner1.pk, owner2.pk]) + + assert is_admin_owner(owner1) == True + assert is_admin_owner(owner2) == True + assert is_admin_owner(owner3) == False + assert is_admin_owner(None) == False + + +@override_settings(IS_ENTERPRISE=True) +def test_activated_owners(dbsession): + user1 = OwnerFactory() + user2 = OwnerFactory() + user3 = OwnerFactory() + OwnerFactory() + OwnerFactory(plan_activated_users=[user1.pk]) + OwnerFactory(plan_activated_users=[user2.pk, user3.pk]) + + owners = activated_owners() + assert list(owners) == [user1, user2, user3] + + +@override_settings(IS_ENTERPRISE=True) +@patch("shared.self_hosted.service.activated_owners") +def test_is_activated_owner(activated_owners, dbsession): + owner1 = OwnerFactory(service="github", username="foo") + owner2 = OwnerFactory(service="github", username="bar") + owner3 = OwnerFactory(service="gitlab", username="foo") + + activated_owners.return_value = Owner.objects.filter(pk__in=[owner1.pk, owner2.pk]) + + assert is_activated_owner(owner1) == True + assert is_activated_owner(owner2) == True + assert is_activated_owner(owner3) == False + + +@override_settings(IS_ENTERPRISE=True) +@patch("shared.license.get_current_license") +def test_license_seats_not_specified(mock_get_current_license, dbsession): + mock_get_current_license.return_value = LicenseInformation(is_valid=True) + assert license_seats() == 0 + + +@override_settings(IS_ENTERPRISE=True) +@patch("shared.self_hosted.service.activated_owners") +@patch("shared.self_hosted.service.license_seats") +def test_can_activate_owner(license_seats, activated_owners, dbsession): + license_seats.return_value = 1 + + owner1 = OwnerFactory(service="github", username="foo") + owner2 = OwnerFactory(service="github", username="bar") + owner3 = OwnerFactory(service="gitlab", username="foo") + + activated_owners.return_value = Owner.objects.filter(pk__in=[owner1.pk, owner2.pk]) + + assert can_activate_owner(owner1) == True + assert can_activate_owner(owner2) == True + assert can_activate_owner(owner3) == False + + license_seats.return_value = 5 + + assert can_activate_owner(owner1) == True + assert can_activate_owner(owner2) == True + assert can_activate_owner(owner3) == True + + +@override_settings(IS_ENTERPRISE=True) +@patch("shared.self_hosted.service.can_activate_owner") +def test_activate_owner(can_activate_owner, dbsession): + can_activate_owner.return_value = True + + other_owner = OwnerFactory() + org1 = OwnerFactory(plan_activated_users=[other_owner.pk]) + org2 = OwnerFactory(plan_activated_users=[]) + org3 = OwnerFactory(plan_activated_users=[other_owner.pk]) + owner = OwnerFactory(organizations=[org1.pk, org2.pk]) + + activate_owner(owner) + + org1.refresh_from_db() + assert org1.plan_activated_users == [other_owner.pk, owner.pk] + org2.refresh_from_db() + assert org2.plan_activated_users == [owner.pk] + org3.refresh_from_db() + assert org3.plan_activated_users == [other_owner.pk] + + activate_owner(owner) + + # does not add duplicate entry + org1.refresh_from_db() + assert org1.plan_activated_users == [other_owner.pk, owner.pk] + org2.refresh_from_db() + assert org2.plan_activated_users == [owner.pk] + org3.refresh_from_db() + assert org3.plan_activated_users == [other_owner.pk] + + +@override_settings(IS_ENTERPRISE=True) +@patch("shared.self_hosted.service.can_activate_owner") +def test_activate_owner_cannot_activate(can_activate_owner, dbsession): + can_activate_owner.return_value = False + + other_owner = OwnerFactory() + org1 = OwnerFactory(plan_activated_users=[other_owner.pk]) + org2 = OwnerFactory(plan_activated_users=[]) + owner = OwnerFactory(organizations=[org2.pk]) + + with TestCase().assertRaises(LicenseException) as e: + activate_owner(owner) + assert e.message == "no more seats available" + + org1.refresh_from_db() + assert org1.plan_activated_users == [other_owner.pk] + org2.refresh_from_db() + assert org2.plan_activated_users == [] + + +@override_settings(IS_ENTERPRISE=True) +def test_deactivate_owner(dbsession): + owner1 = OwnerFactory() + owner2 = OwnerFactory() + org1 = OwnerFactory(plan_activated_users=[owner1.pk, owner2.pk]) + org2 = OwnerFactory(plan_activated_users=[owner1.pk]) + org3 = OwnerFactory(plan_activated_users=[owner2.pk]) + + deactivate_owner(owner1) + + org1.refresh_from_db() + assert org1.plan_activated_users == [owner2.pk] + org2.refresh_from_db() + assert org2.plan_activated_users == [] + org3.refresh_from_db() + assert org3.plan_activated_users == [owner2.pk] + + +@override_settings(IS_ENTERPRISE=True) +def test_autoactivation(dbsession): + disable_autoactivation() + + owner1 = OwnerFactory(plan_auto_activate=False) + owner2 = OwnerFactory(plan_auto_activate=False) + assert is_autoactivation_enabled() == False + + owner1.plan_auto_activate = True + owner1.save() + assert is_autoactivation_enabled() == True + + owner2.plan_auto_activate = True + owner2.save() + assert is_autoactivation_enabled() == True + + +@override_settings(IS_ENTERPRISE=True) +def test_enable_autoactivation(dbsession): + owner = OwnerFactory(plan_auto_activate=False) + enable_autoactivation() + owner.refresh_from_db() + assert owner.plan_auto_activate == True + + +@override_settings(IS_ENTERPRISE=True) +def test_disable_autoactivation(dbsession): + owner = OwnerFactory(plan_auto_activate=True) + disable_autoactivation() + owner.refresh_from_db() + assert owner.plan_auto_activate == False + + +@override_settings(IS_ENTERPRISE=False) +def test_activate_owner_non_enterprise(dbsession): + org = OwnerFactory(plan_activated_users=[]) + owner = OwnerFactory(organizations=[org.pk]) + + with TestCase().assertRaises(Exception): + activate_owner(owner) + + org.refresh_from_db() + assert org.plan_activated_users == [] + + +@override_settings(IS_ENTERPRISE=False) +def test_deactivate_owner_non_enterprise(dbsession): + owner1 = OwnerFactory() + owner2 = OwnerFactory() + org1 = OwnerFactory(plan_activated_users=[owner1.pk, owner2.pk]) + org2 = OwnerFactory(plan_activated_users=[owner1.pk]) + org3 = OwnerFactory(plan_activated_users=[owner2.pk]) + + with TestCase().assertRaises(Exception): + deactivate_owner(owner1) + + org1.refresh_from_db() + assert org1.plan_activated_users == [owner1.pk, owner2.pk] + org2.refresh_from_db() + assert org2.plan_activated_users == [owner1.pk] + org3.refresh_from_db() + assert org3.plan_activated_users == [owner2.pk] diff --git a/libs/shared/tests/unit/static_analysis/__init__.py b/libs/shared/tests/unit/static_analysis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/tests/unit/static_analysis/test_enums.py b/libs/shared/tests/unit/static_analysis/test_enums.py new file mode 100644 index 0000000000..d341e8d992 --- /dev/null +++ b/libs/shared/tests/unit/static_analysis/test_enums.py @@ -0,0 +1,9 @@ +from shared.staticanalysis import StaticAnalysisSingleFileSnapshotState + + +def test_enum_choices(): + assert StaticAnalysisSingleFileSnapshotState.choices() == ( + (1, "CREATED"), + (2, "VALID"), + (3, "REJECTED"), + ) diff --git a/libs/shared/tests/unit/storage/__init__.py b/libs/shared/tests/unit/storage/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/tests/unit/storage/test_init.py b/libs/shared/tests/unit/storage/test_init.py new file mode 100644 index 0000000000..482b73769d --- /dev/null +++ b/libs/shared/tests/unit/storage/test_init.py @@ -0,0 +1,22 @@ +from shared.storage import get_appropriate_storage_service +from shared.storage.minio import MinioStorageService + +minio_config = { + "access_key_id": "codecov-default-key", + "secret_access_key": "codecov-default-secret", + "verify_ssl": False, + "host": "minio", + "port": "9000", + "iam_auth": False, + "iam_endpoint": None, +} + + +class TestStorageInitialization(object): + def test_get_appropriate_storage_service_minio(self, mock_configuration): + mock_configuration.params["services"] = { + "minio": minio_config, + } + res = get_appropriate_storage_service() + assert isinstance(res, MinioStorageService) + assert res.minio_config == minio_config diff --git a/libs/shared/tests/unit/storage/test_memory.py b/libs/shared/tests/unit/storage/test_memory.py new file mode 100644 index 0000000000..cdef08fb83 --- /dev/null +++ b/libs/shared/tests/unit/storage/test_memory.py @@ -0,0 +1,100 @@ +import tempfile +from uuid import uuid4 + +import pytest + +from shared.storage.exceptions import BucketAlreadyExistsError, FileNotInStorageError +from shared.storage.memory import MemoryStorageService + +BUCKET_NAME = "archivetest" + + +def make_storage() -> MemoryStorageService: + return MemoryStorageService({}) + + +def ensure_bucket(storage: MemoryStorageService): + pass + + +def test_create_bucket(): + storage = make_storage() + bucket_name = uuid4().hex + + res = storage.create_root_storage(bucket_name, region="") + assert res == {"name": bucket_name} + + +def test_create_bucket_already_exists(): + storage = make_storage() + bucket_name = uuid4().hex + + storage.create_root_storage(bucket_name) + with pytest.raises(BucketAlreadyExistsError): + storage.create_root_storage(bucket_name) + + +def test_write_then_read_file(): + storage = make_storage() + path = f"test_write_then_read_file/{uuid4().hex}" + data = "lorem ipsum dolor test_write_then_read_file á" + + ensure_bucket(storage) + writing_result = storage.write_file(BUCKET_NAME, path, data) + assert writing_result + reading_result = storage.read_file(BUCKET_NAME, path) + assert reading_result.decode() == data + + +def test_write_then_read_file_obj(): + storage = make_storage() + path = f"test_write_then_read_file_obj/{uuid4().hex}" + data = "lorem ipsum dolor test_write_then_read_file_obj á" + + ensure_bucket(storage) + + _, local_path = tempfile.mkstemp() + with open(local_path, "w") as f: + f.write(data) + with open(local_path, "rb") as f: + writing_result = storage.write_file(BUCKET_NAME, path, f) + assert writing_result + + _, local_path = tempfile.mkstemp() + with open(local_path, "wb") as f: + storage.read_file(BUCKET_NAME, path, file_obj=f) + with open(local_path, "rb") as f: + assert f.read().decode() == data + + +def test_read_file_does_not_exist(): + storage = make_storage() + path = f"test_read_file_does_not_exist/{uuid4().hex}" + + ensure_bucket(storage) + with pytest.raises(FileNotInStorageError): + storage.read_file(BUCKET_NAME, path) + + +def test_write_then_delete_file(): + storage = make_storage() + path = f"test_write_then_delete_file/{uuid4().hex}" + data = "lorem ipsum dolor test_write_then_delete_file á" + + ensure_bucket(storage) + writing_result = storage.write_file(BUCKET_NAME, path, data) + assert writing_result + + deletion_result = storage.delete_file(BUCKET_NAME, path) + assert deletion_result is True + with pytest.raises(FileNotInStorageError): + storage.read_file(BUCKET_NAME, path) + + +def test_delete_file_doesnt_exist(): + storage = make_storage() + path = f"test_delete_file_doesnt_exist/{uuid4().hex}" + + ensure_bucket(storage) + with pytest.raises(FileNotInStorageError): + storage.delete_file(BUCKET_NAME, path) diff --git a/libs/shared/tests/unit/storage/test_minio.py b/libs/shared/tests/unit/storage/test_minio.py new file mode 100644 index 0000000000..04cc3d67b6 --- /dev/null +++ b/libs/shared/tests/unit/storage/test_minio.py @@ -0,0 +1,346 @@ +import gzip +import tempfile +from io import BytesIO +from uuid import uuid4 + +import pytest +import zstandard + +from shared.storage.exceptions import BucketAlreadyExistsError, FileNotInStorageError +from shared.storage.minio import MinioStorageService, zstd_decoded_by_default + +BUCKET_NAME = "archivetest" + + +def test_zstd_by_default(): + assert not zstd_decoded_by_default() + + +def test_gzip_stream_compression(): + data = "lorem ipsum dolor test_write_then_read_file á" + + split_data = [data[i : i + 5] for i in range(0, len(data), 5)] + + compressed_pieces: list[bytes] = [ + gzip.compress(piece.encode()) for piece in split_data + ] + + assert gzip.decompress(b"".join(compressed_pieces)) == data.encode() + + +def make_storage() -> MinioStorageService: + return MinioStorageService( + { + "access_key_id": "codecov-default-key", + "secret_access_key": "codecov-default-secret", + "verify_ssl": False, + "host": "minio", + "port": "9000", + "iam_auth": False, + "iam_endpoint": None, + } + ) + + +def ensure_bucket(storage: MinioStorageService): + try: + storage.create_root_storage(BUCKET_NAME) + except Exception: + pass + + +def test_create_bucket(): + storage = make_storage() + bucket_name = uuid4().hex + + res = storage.create_root_storage(bucket_name, region="") + assert res == {"name": bucket_name} + + +def test_create_bucket_already_exists(): + storage = make_storage() + bucket_name = uuid4().hex + + storage.create_root_storage(bucket_name) + with pytest.raises(BucketAlreadyExistsError): + storage.create_root_storage(bucket_name) + + +def test_write_then_read_file(): + storage = make_storage() + path = f"test_write_then_read_file/{uuid4().hex}" + data = "lorem ipsum dolor test_write_then_read_file á" + + ensure_bucket(storage) + writing_result = storage.write_file(BUCKET_NAME, path, data) + assert writing_result + reading_result = storage.read_file(BUCKET_NAME, path) + assert reading_result.decode() == data + + +def test_write_then_read_file_already_gzipped(): + storage = make_storage() + path = f"test_write_then_read_file_already_gzipped/{uuid4().hex}" + data = BytesIO( + gzip.compress("lorem ipsum dolor test_write_then_read_file á".encode()) + ) + + ensure_bucket(storage) + writing_result = storage.write_file( + BUCKET_NAME, path, data, is_already_gzipped=True + ) + assert writing_result + reading_result = storage.read_file(BUCKET_NAME, path) + assert reading_result.decode() == "lorem ipsum dolor test_write_then_read_file á" + + +def test_write_then_read_file_already_zstd(): + storage = make_storage() + path = f"test_write_then_read_file_already_zstd/{uuid4().hex}" + data = BytesIO( + zstandard.compress("lorem ipsum dolor test_write_then_read_file á".encode()) + ) + + ensure_bucket(storage) + writing_result = storage.write_file( + BUCKET_NAME, path, data, compression_type="zstd", is_compressed=True + ) + assert writing_result + reading_result = storage.read_file(BUCKET_NAME, path) + assert reading_result.decode() == "lorem ipsum dolor test_write_then_read_file á" + + +def test_write_then_read_file_obj(): + storage = make_storage() + path = f"test_write_then_read_file/{uuid4().hex}" + data = "lorem ipsum dolor test_write_then_read_file á" + + ensure_bucket(storage) + + _, local_path = tempfile.mkstemp() + with open(local_path, "w") as f: + f.write(data) + with open(local_path, "rb") as f: + writing_result = storage.write_file(BUCKET_NAME, path, f) + assert writing_result + + _, local_path = tempfile.mkstemp() + with open(local_path, "wb") as f: + storage.read_file(BUCKET_NAME, path, file_obj=f) + with open(local_path, "rb") as f: + assert f.read().decode() == data + + +def test_write_then_read_file_obj_gzip(): + storage = make_storage() + path = f"test_write_then_read_file_gzip/{uuid4().hex}" + data = "lorem ipsum dolor test_write_then_read_file á" + + ensure_bucket(storage) + + _, local_path = tempfile.mkstemp() + with open(local_path, "w") as f: + f.write(data) + with open(local_path, "rb") as f: + writing_result = storage.write_file( + BUCKET_NAME, path, f, compression_type="gzip" + ) + assert writing_result + + _, local_path = tempfile.mkstemp() + with open(local_path, "wb") as f: + storage.read_file(BUCKET_NAME, path, file_obj=f) + with open(local_path, "rb") as f: + assert f.read().decode() == data + + +def test_write_then_read_file_obj_no_compression(): + storage = make_storage() + path = f"test_write_then_read_file_no_compression/{uuid4().hex}" + data = "lorem ipsum dolor test_write_then_read_file á" + + ensure_bucket(storage) + + _, local_path = tempfile.mkstemp() + with open(local_path, "w") as f: + f.write(data) + with open(local_path, "rb") as f: + writing_result = storage.write_file(BUCKET_NAME, path, f, compression_type=None) + assert writing_result + + _, local_path = tempfile.mkstemp() + with open(local_path, "wb") as f: + storage.read_file(BUCKET_NAME, path, file_obj=f) + with open(local_path, "rb") as f: + assert f.read().decode() == data + + +def test_write_then_read_file_obj_x_gzip(): + storage = make_storage() + path = f"test_write_then_read_file_obj_x_gzip/{uuid4().hex}" + compressed = gzip.compress("lorem ipsum dolor test_write_then_read_file á".encode()) + outsize = len(compressed) + data = BytesIO(compressed) + + ensure_bucket(storage) + + headers = {"Content-Encoding": "gzip"} + storage.minio_client.put_object( + BUCKET_NAME, + path, + data, + content_type="application/x-gzip", + metadata=headers, + length=outsize, + ) + + _, local_path = tempfile.mkstemp() + with open(local_path, "wb") as f: + storage.read_file(BUCKET_NAME, path, file_obj=f) + with open(local_path, "rb") as f: + assert f.read().decode() == "lorem ipsum dolor test_write_then_read_file á" + + +def test_write_then_read_file_obj_already_gzipped(): + storage = make_storage() + path = f"test_write_then_read_file_obj_already_gzipped/{uuid4().hex}" + data = BytesIO( + gzip.compress("lorem ipsum dolor test_write_then_read_file á".encode()) + ) + + ensure_bucket(storage) + + _, local_path = tempfile.mkstemp() + with open(local_path, "wb") as f: + f.write(data.getvalue()) + with open(local_path, "rb") as f: + writing_result = storage.write_file( + BUCKET_NAME, path, f, is_already_gzipped=True + ) + assert writing_result + + _, local_path = tempfile.mkstemp() + with open(local_path, "wb") as f: + storage.read_file(BUCKET_NAME, path, file_obj=f) + with open(local_path, "rb") as f: + assert f.read().decode() == "lorem ipsum dolor test_write_then_read_file á" + + +def test_write_then_read_file_obj_already_zstd(): + storage = make_storage() + path = f"test_write_then_read_file_obj_already_zstd/{uuid4().hex}" + data = BytesIO( + zstandard.compress("lorem ipsum dolor test_write_then_read_file á".encode()) + ) + + ensure_bucket(storage) + + _, local_path = tempfile.mkstemp() + with open(local_path, "wb") as f: + f.write(data.getvalue()) + with open(local_path, "rb") as f: + writing_result = storage.write_file( + BUCKET_NAME, path, f, is_compressed=True, compression_type="zstd" + ) + assert writing_result + + _, local_path = tempfile.mkstemp() + with open(local_path, "wb") as f: + storage.read_file(BUCKET_NAME, path, file_obj=f) + with open(local_path, "rb") as f: + assert f.read().decode() == "lorem ipsum dolor test_write_then_read_file á" + + +def test_read_file_does_not_exist(): + storage = make_storage() + path = f"test_read_file_does_not_exist/{uuid4().hex}" + + ensure_bucket(storage) + with pytest.raises(FileNotInStorageError): + storage.read_file(BUCKET_NAME, path) + + +def test_write_then_delete_file(): + storage = make_storage() + path = f"test_write_then_delete_file/{uuid4().hex}" + data = "lorem ipsum dolor test_write_then_delete_file á" + + ensure_bucket(storage) + writing_result = storage.write_file(BUCKET_NAME, path, data) + assert writing_result + + deletion_result = storage.delete_file(BUCKET_NAME, path) + assert deletion_result is True + with pytest.raises(FileNotInStorageError): + storage.read_file(BUCKET_NAME, path) + + +def test_delete_file_doesnt_exist(): + storage = make_storage() + path = f"test_delete_file_doesnt_exist/{uuid4().hex}" + + ensure_bucket(storage) + try: + storage.delete_file(BUCKET_NAME, path) + except FileNotInStorageError: + pass + + +def test_minio_without_ports(): + minio_no_ports_config = { + "access_key_id": "hodor", + "secret_access_key": "haha", + "verify_ssl": False, + "host": "cute_url_no_ports", + "iam_auth": True, + "iam_endpoint": None, + } + storage = MinioStorageService(minio_no_ports_config) + assert storage.minio_config == minio_no_ports_config + assert storage.minio_client._base_url._url.port is None + + +def test_minio_with_ports(): + minio_no_ports_config = { + "access_key_id": "hodor", + "secret_access_key": "haha", + "verify_ssl": False, + "host": "cute_url_no_ports", + "port": "9000", + "iam_auth": True, + "iam_endpoint": None, + } + storage = MinioStorageService(minio_no_ports_config) + assert storage.minio_config == minio_no_ports_config + assert storage.minio_client._base_url.region is None + + +def test_minio_with_region(): + minio_no_ports_config = { + "access_key_id": "hodor", + "secret_access_key": "haha", + "verify_ssl": False, + "host": "cute_url_no_ports", + "port": "9000", + "iam_auth": True, + "iam_endpoint": None, + "region": "example", + } + storage = MinioStorageService(minio_no_ports_config) + assert storage.minio_config == minio_no_ports_config + assert storage.minio_client._base_url.region == "example" + + +def test_write_then_read_file_with_metadata(): + storage = make_storage() + path = f"test_write_then_read_file_with_metadata/{uuid4().hex}" + data = "lorem ipsum dolor test_write_then_read_file_with_metadata á" + + ensure_bucket(storage) + _ = storage.write_file(BUCKET_NAME, path, data, metadata={"test": "test"}) + metadata_container = {} + reading_result = storage.read_file( + BUCKET_NAME, path, metadata_container=metadata_container + ) + assert reading_result.decode() == data + assert metadata_container == {"test": "test"} diff --git a/libs/shared/tests/unit/test_api_archive.py b/libs/shared/tests/unit/test_api_archive.py new file mode 100644 index 0000000000..420d405678 --- /dev/null +++ b/libs/shared/tests/unit/test_api_archive.py @@ -0,0 +1,225 @@ +import json +from base64 import b16encode +from hashlib import md5 + +import pytest + +from shared.api_archive.archive import ArchiveService, MinioEndpoints +from shared.config import ConfigHelper +from shared.django_apps.core.tests.factories import RepositoryFactory +from shared.utils.ReportEncoder import ReportEncoder + +pytestmark = pytest.mark.django_db + + +class TestMinioEndpoints: + def test_get_path(self): + path = MinioEndpoints.chunks.get_path( + version="v4", repo_hash="abc123", commitid="def456" + ) + assert path == "v4/repos/abc123/commits/def456/chunks.txt" + + path = MinioEndpoints.json_data.get_path( + version="v4", + repo_hash="abc123", + commitid="def456", + table="coverage", + field="totals", + external_id="789", + ) + assert ( + path == "v4/repos/abc123/commits/def456/json_data/coverage/totals/789.json" + ) + + path = MinioEndpoints.raw.get_path( + date="2023-01-01", + repo_hash="abc123", + commit_sha="def456", + reportid="report123", + ) + assert path == "v4/raw/2023-01-01/abc123/def456/report123.txt" + + +@pytest.fixture +def mock_config(mocker): + m = mocker.patch("shared.config._get_config_instance") + mock_config = ConfigHelper() + m.return_value = mock_config + our_config = { + "services": { + "minio": { + "host": "minio", + "access_key_id": "codecov-default-key", + "bucket": "test-bucket", + "ttl": "30", + "hash_key": "test-key", + "secret_access_key": "codecov-default-secret", + "verify_ssl": False, + "port": "9000", + }, + }, + } + mock_config.set_params(our_config) + + return mock_config + + +@pytest.fixture +def mock_repo(mocker): + repo = RepositoryFactory(repoid=12345) + repo.author.service = "github" + return repo + + +@pytest.fixture +def archive_service(mock_config, mock_repo): + a = ArchiveService(mock_repo) + try: + a.storage.create_root_storage("test-bucket") + except Exception: + pass + return a + + +class TestArchiveService: + def test_init(self, archive_service): + assert archive_service.root == "test-bucket" + assert archive_service.ttl == 30 + assert archive_service.storage_hash is not None + + def test_init_with_custom_ttl(self, mock_repo): + archive_service = ArchiveService(mock_repo, ttl=60) + assert archive_service.ttl == 60 + + def test_get_archive_hash(self, mock_config, mock_repo): + result = ArchiveService.get_archive_hash(mock_repo) + val = f"{mock_repo.repoid}{mock_repo.service}{mock_repo.service_id}test-key".encode() + assert result == b16encode(md5(val).digest()).decode() + + def test_write_file(self, mock_config, archive_service): + archive_service.write_file("test/path", "test data") + + result = archive_service.read_file("test/path") + assert result == "test data" + + def test_delete_file(self, mock_config, archive_service): + archive_service.write_file("test/path", "test data") + + archive_service.delete_file("test/path") + + with pytest.raises(Exception): + archive_service.read_file("test/path") + + def test_read_chunks(self, mock_config, archive_service): + expected_path = MinioEndpoints.chunks.get_path( + version="v4", repo_hash=archive_service.storage_hash, commitid="commit123" + ) + archive_service.write_file(expected_path, "chunk data") + + result = archive_service.read_chunks("commit123") + + assert result == "chunk data" + + def test_read_chunks_no_hash(self, mocker): + mock_get_config = mocker.patch("shared.api_archive.archive.get_config") + mock_get_config.side_effect = lambda *args, default=None: { + ("services", "minio", "bucket"): "test-bucket", + ("services", "minio", "ttl"): "30", + }.get(args, default) + + mocker.patch( + "shared.api_archive.archive.shared.storage.get_appropriate_storage_service", + ) + + archive_service = ArchiveService(None) + + with pytest.raises(ValueError): + archive_service.read_chunks("commit123") + + def test_create_presigned_put(self, mock_config, archive_service): + result = archive_service.create_presigned_put("test/path") + + assert isinstance(result, str) + assert len(result) > 0 + + def test_write_json_data_to_storage_with_commit(self, mock_config, archive_service): + data = {"key": "value"} + + path = archive_service.write_json_data_to_storage( + "commit123", "table1", "field1", "external1", data + ) + + expected_path = MinioEndpoints.json_data.get_path( + version="v4", + repo_hash=archive_service.storage_hash, + commitid="commit123", + table="table1", + field="field1", + external_id="external1", + ) + assert path == expected_path + + result = archive_service.read_file(path) + expected_data = json.dumps(data, cls=ReportEncoder) + assert result == expected_data + + def test_write_json_data_to_storage_without_commit( + self, mock_config, archive_service + ): + data = {"key": "value"} + + path = archive_service.write_json_data_to_storage( + None, "table1", "field1", "external1", data + ) + + expected_path = MinioEndpoints.json_data_no_commit.get_path( + version="v4", + repo_hash=archive_service.storage_hash, + table="table1", + field="field1", + external_id="external1", + ) + assert path == expected_path + + result = archive_service.read_file(path) + expected_data = json.dumps(data, cls=ReportEncoder) + assert result == expected_data + + def test_write_json_data_to_storage_no_hash(self, mocker): + mock_get_config = mocker.patch("shared.api_archive.archive.get_config") + mock_get_config.side_effect = lambda *args, default=None: { + ("services", "minio", "bucket"): "test-bucket", + ("services", "minio", "ttl"): "30", + }.get(args, default) + + mocker.patch( + "shared.api_archive.archive.shared.storage.get_appropriate_storage_service", + ) + + archive_service = ArchiveService(None) + + with pytest.raises(ValueError): + archive_service.write_json_data_to_storage( + "commit123", "table1", "field1", "external1", {"key": "value"} + ) + + def test_write_json_data_to_storage_custom_encoder( + self, mocker, mock_config, archive_service + ): + mock_json_dumps = mocker.patch("json.dumps") + mock_json_dumps.return_value = '{"key": "value"}' + + data = {"key": "value"} + + class CustomEncoder(ReportEncoder): + pass + + path = archive_service.write_json_data_to_storage( + "commit123", "table1", "field1", "external1", data, encoder=CustomEncoder + ) + + mock_json_dumps.assert_called_once_with(data, cls=CustomEncoder) + + result = archive_service.read_file(path) + + assert result == '{"key": "value"}' diff --git a/libs/shared/tests/unit/test_celery_config.py b/libs/shared/tests/unit/test_celery_config.py new file mode 100644 index 0000000000..3a0523c462 --- /dev/null +++ b/libs/shared/tests/unit/test_celery_config.py @@ -0,0 +1,126 @@ +import pytest + +import shared.celery_config as celery_config +from shared.utils.enums import TaskConfigGroup + + +def test_celery_config(): + # NOTE: This test is fairly limited + # Since all the fields get determined at import time (because they are class attributes), + # it's not possibe to mock `get_config` in order to see their results here. + # The only way to do so would to be to make each field a @classmethod + # and hold the logic inside it. + # It's not a terrible idea, but I am not sure of the impact of reloading those things every time + # So the best we can do is to ensure the fields have a sane structure + config = celery_config.BaseCeleryConfig + assert hasattr(config, "broker_url") + assert hasattr(config, "result_backend") + assert hasattr(config, "task_default_queue") + assert hasattr(config, "task_acks_late") + assert hasattr(config, "worker_prefetch_multiplier") + assert hasattr(config, "task_soft_time_limit") + assert hasattr(config, "task_time_limit") + assert hasattr(config, "notify_soft_time_limit") + assert hasattr(config, "task_annotations") + assert hasattr(config, "task_routes") + assert hasattr(config, "worker_max_memory_per_child") + assert sorted(config.task_routes.keys()) == [ + "app.cron.healthcheck.HealthCheckTask", + "app.cron.profiling.*", + "app.tasks.archive.*", + "app.tasks.cache_rollup.*", + "app.tasks.comment.Comment", + "app.tasks.commit_update.CommitUpdate", + "app.tasks.compute_comparison.ComputeComparison", + "app.tasks.delete_owner.DeleteOwner", + "app.tasks.flakes.*", + "app.tasks.flush_repo.FlushRepo", + "app.tasks.label_analysis.*", + "app.tasks.new_user_activated.NewUserActivated", + "app.tasks.notify.Notify", + "app.tasks.profiling.*", + "app.tasks.pulls.Sync", + "app.tasks.static_analysis.*", + "app.tasks.status.*", + "app.tasks.sync_account.ActivateAccountUser", + "app.tasks.sync_plans.SyncPlans", + "app.tasks.sync_repo_languages.SyncLanguages", + "app.tasks.sync_repo_languages_gql.SyncLanguagesGQL", + "app.tasks.sync_repos.SyncRepos", + "app.tasks.sync_teams.SyncTeams", + "app.tasks.test_results.*", + "app.tasks.timeseries.*", + "app.tasks.upload.*", + ] + assert config.broker_transport_options == {"visibility_timeout": 21600} + assert config.result_extended is True + assert config.imports == ("tasks",) + assert config.task_serializer == "json" + assert config.accept_content == ["json"] + assert config.worker_hijack_root_logger is False + assert config.timezone == "UTC" + assert config.enable_utc is True + assert config.task_ignore_result is True + assert config.worker_disable_rate_limits is True + + +@pytest.mark.parametrize( + "task_name,task_group", + [ + ("app.cron.healthcheck.HealthCheckTask", TaskConfigGroup.healthcheck.value), + ("app.cron.profiling.findinguncollected", TaskConfigGroup.profiling.value), + ("app.tasks.comment.Comment", TaskConfigGroup.comment.value), + ("app.tasks.commit_update.CommitUpdate", TaskConfigGroup.commit_update.value), + ( + "app.tasks.compute_comparison.ComputeComparison", + TaskConfigGroup.compute_comparison.value, + ), + ("app.tasks.delete_owner.DeleteOwner", TaskConfigGroup.delete_owner.value), + ("app.tasks.flush_repo.FlushRepo", TaskConfigGroup.flush_repo.value), + ("app.tasks.sync_plans.SyncPlans", TaskConfigGroup.sync_plans.value), + ( + "app.tasks.new_user_activated.NewUserActivated", + TaskConfigGroup.new_user_activated.value, + ), + ("app.tasks.notify.Notify", TaskConfigGroup.notify.value), + ("app.tasks.profiling.collection", TaskConfigGroup.profiling.value), + ("app.tasks.profiling.normalizer", TaskConfigGroup.profiling.value), + ("app.tasks.profiling.summarization", TaskConfigGroup.profiling.value), + ("app.tasks.pulls.Sync", TaskConfigGroup.pulls.value), + ("app.tasks.status.SetError", TaskConfigGroup.status.value), + ("app.tasks.status.SetPending", TaskConfigGroup.status.value), + ("app.tasks.sync_repos.SyncRepos", TaskConfigGroup.sync_repos.value), + ( + "app.tasks.sync_repo_languages.SyncLanguages", + TaskConfigGroup.sync_repo_languages.value, + ), + ( + "app.tasks.sync_repo_languages_gql.SyncLanguagesGQL", + TaskConfigGroup.sync_repo_languages_gql.value, + ), + ("app.tasks.sync_teams.SyncTeams", TaskConfigGroup.sync_teams.value), + ("app.tasks.timeseries.backfill", TaskConfigGroup.timeseries.value), + ("app.tasks.timeseries.backfill_commits", TaskConfigGroup.timeseries.value), + ("app.tasks.timeseries.backfill_dataset", TaskConfigGroup.timeseries.value), + ("app.tasks.timeseries.delete", TaskConfigGroup.timeseries.value), + ( + "app.tasks.timeseries.save_commit_measurements", + TaskConfigGroup.timeseries.value, + ), + ("app.tasks.upload.Upload", TaskConfigGroup.upload.value), + ("app.tasks.upload.UploadProcessor", TaskConfigGroup.upload.value), + ("app.tasks.upload.UploadFinisher", TaskConfigGroup.upload.value), + ( + "app.tasks.static_analysis.check_suite", + TaskConfigGroup.static_analysis.value, + ), + ( + "app.tasks.label_analysis.process_request", + TaskConfigGroup.label_analysis.value, + ), + ("unknown.task", None), + ("app.tasks.legacy", None), + ], +) +def test_task_group(task_name, task_group): + assert celery_config.get_task_group(task_name) == task_group diff --git a/libs/shared/tests/unit/test_config.py b/libs/shared/tests/unit/test_config.py new file mode 100644 index 0000000000..da47a98364 --- /dev/null +++ b/libs/shared/tests/unit/test_config.py @@ -0,0 +1,523 @@ +import json +import os + +import pytest + +from shared.config import ConfigHelper, get_config + + +class TestConfig(object): + def test_get_config_nothing_user_set(self, mocker): + mocker.patch.dict(os.environ, {}, clear=True) + mocker.patch.object( + ConfigHelper, "load_yaml_file", side_effect=FileNotFoundError() + ) + this_config = ConfigHelper() + mocker.patch("shared.config._get_config_instance", return_value=this_config) + assert ( + get_config("services", "minio", "hash_key") + == "ab164bf3f7d947f2a0681b215404873e" + ) + assert get_config("site", "codecov", "require_ci_to_pass") is True + assert get_config("site", "coverage", "precision") == 2 + assert get_config("site", "coverage", "round") == "down" + assert get_config("site", "coverage", "range") == [60.0, 80.0] + assert get_config("site", "coverage", "status", "project") is True + assert get_config("site", "coverage", "status", "patch") is True + assert get_config("site", "coverage", "status", "changes") is False + assert get_config("site", "comment", "layout") == "reach,diff,flags,tree,reach" + assert get_config("site", "comment", "behavior") == "default" + assert get_config("services") == { + "minio": { + "host": "minio", + "access_key_id": "codecov-default-key", + "secret_access_key": "codecov-default-secret", + "verify_ssl": False, + "iam_auth": False, + "iam_endpoint": None, + "hash_key": "ab164bf3f7d947f2a0681b215404873e", + }, + "database_url": "postgresql://postgres:@postgres:5432/postgres", + } + assert get_config("setup", "timeseries", "enabled") is False + + def test_get_config_production_use_case(self, mocker): + yaml_content = "\n".join( + [ + "setup:", + " codecov_url: https://codecov.io", + "", + " debug: no", + " loglvl: INFO", + " media:", + " assets: assets_link", + " dependancies: assets_link", + " http:", + " force_https: yes", + " timeouts:", + " connect: 10", + " receive: 15", + " tasks:", + " celery:", + " soft_timelimit: 200", + " hard_timelimit: 240", + " upload:", + " queue: uploads", + " cache:", + " yaml: 600 # 10 minutes", + " tree: 600 # 10 minutes", + " diff: 300 # 5 minutes", + " chunks: 300 # 5 minutes", + " uploads: 86400 # 1 day", + "", + "services:", + " minio:", + " bucket: codecov", + " sentry:", + " server_dsn: server_dsn", + " google_analytics_key: UA-google_analytics_key-1", + "", + "github:", + " bot:", + " username: codecov-io", + " integration:", + " id: 254", + " pem: src/certs/github.pem", + "", + "bitbucket:", + " bot:", + " username: codecov-io", + "", + "gitlab:", + " bot:", + " username: codecov-io", + "", + "site:", + " codecov:", + " require_ci_to_pass: yes", + "", + " coverage:", + " precision: 2", + " round: down", + " range: '70...100'", + "", + " status:", + " project: yes", + " patch: yes", + " changes: no", + "", + " parsers:", + " gcov:", + " branch_detection:", + " conditional: yes", + " loop: yes", + " method: no", + " macro: no", + "", + " javascript:", + " enable_partials: no", + "", + " comment:", + " layout: 'reach, diff, flags, files, footer'", + " behavior: default", + " require_changes: no", + " require_base: no", + " require_head: yes", + ] + ) + mocker.patch.object(ConfigHelper, "load_yaml_file", return_value=yaml_content) + this_config = ConfigHelper() + mocker.patch("shared.config._get_config_instance", return_value=this_config) + assert get_config("site", "codecov", "require_ci_to_pass") is True + assert get_config("site", "coverage", "precision") == 2 + assert get_config("site", "coverage", "round") == "down" + assert get_config("site", "coverage", "range") == [70.0, 100.0] + assert get_config("site", "coverage", "status", "project") is True + assert get_config("site", "coverage", "status", "patch") is True + assert get_config("site", "coverage", "status", "changes") is False + assert ( + get_config( + "site", + "coverage", + "status", + "default_rules", + "flag_coverage_not_uploaded_behavior", + ) + == "include" + ) + assert [ + x.strip() for x in get_config("site", "comment", "layout").split(",") + ] == ["reach", "diff", "flags", "files", "footer"] + assert get_config("site", "comment", "behavior") == "default" + assert get_config("site", "comment", "show_carryforward_flags") is False + + def test_get_config_case_with_more_nested_types(self, mocker): + yaml_content = "\n".join( + [ + "setup:", + " codecov_url: https://codecov.io", + "", + " debug: no", + " loglvl: INFO", + " media:", + " assets: assets_link", + " dependancies: assets_link", + " http:", + " force_https: yes", + " timeouts:", + " connect: 10", + " receive: 15", + " tasks:", + " celery:", + " soft_timelimit: 200", + " hard_timelimit: 240", + " upload:", + " queue: uploads", + " cache:", + " yaml: 600 # 10 minutes", + " tree: 600 # 10 minutes", + " diff: 300 # 5 minutes", + " chunks: 300 # 5 minutes", + " uploads: 86400 # 1 day", + "", + "services:", + " minio:", + " bucket: codecov", + " sentry:", + " server_dsn: server_dsn", + " google_analytics_key: UA-google_analytics_key-1", + "", + "github:", + " bot:", + " username: codecov-io", + " integration:", + " id: 254", + " pem: src/certs/github.pem", + "", + "bitbucket:", + " bot:", + " username: codecov-io", + "", + "gitlab:", + " bot:", + " username: codecov-io", + "", + "site:", + " codecov:", + " require_ci_to_pass: yes", + "", + " status:", + " project: yes", + " patch: yes", + " changes: no", + "", + " parsers:", + " gcov:", + " branch_detection:", + " conditional: yes", + " loop: yes", + " method: no", + " macro: no", + "", + " javascript:", + " enable_partials: no", + "", + " coverage:", + " status:", + " project:", + " default:", + " only_pulls: true", + " target: auto", + " threshold: 100%", + " patch:", + " default:", + " only_pulls: true", + " target: auto", + " threshold: 100%", + ] + ) + mocker.patch.object(ConfigHelper, "load_yaml_file", return_value=yaml_content) + this_config = ConfigHelper() + mocker.patch("shared.config._get_config_instance", return_value=this_config) + assert get_config("site", "codecov", "require_ci_to_pass") is True + assert get_config("site", "coverage", "precision") == 2 + assert get_config("site", "coverage", "round") == "down" + assert get_config("site", "coverage", "range") == [60, 80] + assert get_config("site", "coverage", "status") == { + "project": { + "default": {"only_pulls": True, "target": "auto", "threshold": 100.0} + }, + "patch": { + "default": {"only_pulls": True, "target": "auto", "threshold": 100.0} + }, + "changes": False, + "default_rules": {"flag_coverage_not_uploaded_behavior": "include"}, + } + assert get_config("site", "coverage", "status", "project") == { + "default": {"only_pulls": True, "target": "auto", "threshold": 100.0} + } + assert get_config("site", "coverage", "status", "patch") == { + "default": {"only_pulls": True, "target": "auto", "threshold": 100.0} + } + assert get_config("site", "coverage", "status", "changes") is False + assert [ + x.strip() for x in get_config("site", "comment", "layout").split(",") + ] == ["reach", "diff", "flags", "tree", "reach"] + assert get_config("site", "comment", "behavior") == "default" + assert get_config("site", "comment", "show_carryforward_flags") is False + + def test_get_config_minio_without_port(self, mocker): + yaml_content = "\n".join( + [ + "services:", + " minio:", + " host: s3.amazonaws.com", + " bucket: cce-minio-update-test", + " region: us-east-2", + " verify_ssl: true", + " iam_auth: true", + ] + ) + mocker.patch.object(ConfigHelper, "load_yaml_file", return_value=yaml_content) + this_config = ConfigHelper() + mocker.patch("shared.config._get_config_instance", return_value=this_config) + assert get_config("services", "minio") == { + "host": "s3.amazonaws.com", + "access_key_id": "codecov-default-key", + "secret_access_key": "codecov-default-secret", + "verify_ssl": True, + "iam_auth": True, + "iam_endpoint": None, + "bucket": "cce-minio-update-test", + "region": "us-east-2", + "hash_key": "ab164bf3f7d947f2a0681b215404873e", + } + + def test_get_config_minio_with_port(self, mocker): + yaml_content = "\n".join( + [ + "services:", + " minio:", + " host: s3.amazonaws.com", + " port: 9000", + " bucket: cce-minio-update-test", + " region: us-east-2", + " verify_ssl: true", + " iam_auth: true", + ] + ) + mocker.patch.object(ConfigHelper, "load_yaml_file", return_value=yaml_content) + this_config = ConfigHelper() + mocker.patch("shared.config._get_config_instance", return_value=this_config) + assert get_config("services", "minio") == { + "host": "s3.amazonaws.com", + "port": 9000, + "access_key_id": "codecov-default-key", + "secret_access_key": "codecov-default-secret", + "verify_ssl": True, + "iam_auth": True, + "iam_endpoint": None, + "bucket": "cce-minio-update-test", + "region": "us-east-2", + "hash_key": "ab164bf3f7d947f2a0681b215404873e", + } + + def test_parse_path_and_value_from_envvar(self, mocker): + this_config = ConfigHelper() + mock_env = { + "CONVERT__TO__BOOL__TRUE1": ("true", True), + "CONVERT__TO__BOOL__TRUE2": ("True", True), + "CONVERT__TO__BOOL__TRUE3": ("TRUE", True), + "CONVERT__TO__BOOL__TRUE4": ("on", True), + "CONVERT__TO__BOOL__TRUE5": ("On", True), + "CONVERT__TO__BOOL__TRUE6": ("ON", True), + "CONVERT__TO__BOOL__FALSE1": ("false", False), + "CONVERT__TO__BOOL__FALSE2": ("False", False), + "CONVERT__TO__BOOL__FALSE3": ("FALSE", False), + "CONVERT__TO__BOOL__FALSE4": ("off", False), + "CONVERT__TO__BOOL__FALSE5": ("Off", False), + "CONVERT__TO__BOOL__FALSE6": ("OFF", False), + "CONVERT__TO__INT__123": ("123", 123), + "CONVERT__TO__INT__-123": ("-123", -123), + "CONVERT__TO__FLOAT__12.3": ("12.3", 12.3), + "CONVERT__TO__FLOAT__-12.3": ("-12.3", -12.3), + "LEAVE__ALONE__MALFORMED__TRUE1": ("TrUe", "TrUe"), + "LEAVE__ALONE__MALFORMED__TRUE2": ("oN", "oN"), + "LEAVE__ALONE__MALFORMED__FALSE1": ("FaLse", "FaLse"), + "LEAVE__ALONE__MALFORMED__FALSE2": ("oFF", "oFF"), + "LEAVE__ALONE__MALFORMED__FLOAT": ("12.3.4", "12.3.4"), + "LEAVE__ALONE__STRING": ("hello world", "hello world"), + } + + mocker.patch.dict( + os.environ, + {k: v[0] for k, v in mock_env.items()}, + ) + + for k, v in mock_env.items(): + expected_path = k.split("__") + env_val, expected_val = v + assert this_config._parse_path_and_value_from_envvar(k) == ( + expected_path, + expected_val, + ) + + def test_load_env_var(self, mocker): + this_config = ConfigHelper() + mocker.patch.dict( + os.environ, + { + "SERVICES__MINIO__HASH_KEY": "hash_key", + "SERVICES__CHOSEN_STORAGE": "gcp_with_fallback", + "SERVICES__CELERY_BROKER": "broker_url", + "SETUP__ENCRYPTION_SECRET": "secret", + "GITHUB__BOT__KEY": "GITHUB__BOT__KEY", + "SERVICES__AWS__RESOURCE": "s3", + "SERVICES__SENTRY__SERVER_DSN": "dsn", + "SETUP__TASKS__STATUS__QUEUE": "new_tasks", + "SERVICES__MINIO__HOST": "minio-proxy", + "BITBUCKET__BOT__SECRET": "BITBUCKET__BOT__SECRET", + "SERVICES__STRIPE__API_KEY": "SERVICES__STRIPE__API_KEY", + "SERVICES__AWS__AWS_ACCESS_KEY_ID": "SERVICES__AWS__AWS_ACCESS_KEY_ID", + "SERVICES__MINIO__PORT": "9000", + "SERVICES__AWS__AWS_SECRET_ACCESS_KEY": "1/SERVICES__AWS__AWS_SECRET_ACCESS_KEY", + "BITBUCKET__CLIENT_ID": "BITBUCKET__CLIENT_ID", + "BITBUCKET__CLIENT_SECRET": "BITBUCKET__CLIENT_SECRET", + "SERVICES__GCP__GOOGLE_CREDENTIALS_LOCATION": "/secret/gcs-credentials/path.json", + "GITHUB__INTEGRATION__PEM": "/secrets/github-pem/github.pem", + "SERVICES__DATABASE_URL": "postgresql://user:pass@127.0.0.1:5432/postgres", + "SERVICES__TIMESERIES_DATABASE_URL": "postgresql://user:pass@timescale:5432/timescale", + "BITBUCKET__BOT__KEY": "BITBUCKET__BOT__KEY", + "SERVICES__MINIO__ACCESS_KEY_ID": "SERVICES__MINIO__ACCESS_KEY_ID", + "SERVICES__MINIO__SECRET_ACCESS_KEY": "SERVICES__MINIO__SECRET_ACCESS_KEY", + "GITLAB__BOT__KEY": "GITLAB__BOT__KEY", + "SERVICES__REDIS_URL": "SERVICES__REDIS_URL:11234", + "SERVICES__AWS__REGION_NAME": "us-east-1", + "SENTRY_PERCENTAGE": "0.9", + "__BAD__KEY": "GITLAB__BOT__KEY", + "SETUP__TIMESERIES__ENABLED": "True", + "JSONCONFIG___SETUP__MEDIA": json.dumps( + {"assets": "aaa", "dependancies": "bbb"} + ), + }, + ) + expected_res = { + "services": { + "minio": { + "hash_key": "hash_key", + "host": "minio-proxy", + "port": 9000, + "access_key_id": "SERVICES__MINIO__ACCESS_KEY_ID", + "secret_access_key": "SERVICES__MINIO__SECRET_ACCESS_KEY", + }, + "chosen_storage": "gcp_with_fallback", + "celery_broker": "broker_url", + "aws": { + "resource": "s3", + "aws_access_key_id": "SERVICES__AWS__AWS_ACCESS_KEY_ID", + "aws_secret_access_key": "1/SERVICES__AWS__AWS_SECRET_ACCESS_KEY", + "region_name": "us-east-1", + }, + "sentry": {"server_dsn": "dsn"}, + "stripe": {"api_key": "SERVICES__STRIPE__API_KEY"}, + "gcp": { + "google_credentials_location": "/secret/gcs-credentials/path.json" + }, + "database_url": "postgresql://user:pass@127.0.0.1:5432/postgres", + "timeseries_database_url": "postgresql://user:pass@timescale:5432/timescale", + "redis_url": "SERVICES__REDIS_URL:11234", + }, + "setup": { + "media": {"assets": "aaa", "dependancies": "bbb"}, + "encryption_secret": "secret", + "tasks": {"status": {"queue": "new_tasks"}}, + "timeseries": {"enabled": True}, + }, + "github": { + "bot": {"key": "GITHUB__BOT__KEY"}, + "integration": {"pem": "/secrets/github-pem/github.pem"}, + }, + "bitbucket": { + "bot": { + "secret": "BITBUCKET__BOT__SECRET", + "key": "BITBUCKET__BOT__KEY", + }, + "client_id": "BITBUCKET__CLIENT_ID", + "client_secret": "BITBUCKET__CLIENT_SECRET", + }, + "gitlab": {"bot": {"key": "GITLAB__BOT__KEY"}}, + } + assert expected_res == this_config.load_env_var() + + def test_yaml_content(self, mocker): + mocker.patch.object( + ConfigHelper, "load_yaml_file", side_effect=FileNotFoundError() + ) + this_config = ConfigHelper() + assert this_config.yaml_content() == {} + + def test_load_filename_from_path_base64(self, mocker): + mocker.patch.dict(os.environ, {}, clear=True) + mocker.patch.object( + ConfigHelper, "load_yaml_file", side_effect=FileNotFoundError() + ) + this_config = ConfigHelper() + this_config.set_params( + { + "some": { + "githubpem": { + "source_type": "base64env", + "value": "bW9ua2V5YmFuYW5hc29tZXRoaW5nZGFuY2U=", + } + } + } + ) + res = this_config.load_filename_from_path("some", "githubpem") + assert res == "monkeybananasomethingdance" + + def test_load_filename_from_path_inexisting_file_path(self, mocker): + mocker.patch.dict(os.environ, {}, clear=True) + mocker.patch.object( + ConfigHelper, "load_yaml_file", side_effect=FileNotFoundError() + ) + this_config = ConfigHelper() + this_config.set_params( + { + "some": { + "githubpem": { + "source_type": "filepath", + "value": "inexistent/path/on/purpose.hahaha", + } + } + } + ) + with pytest.raises(FileNotFoundError): + this_config.load_filename_from_path("some", "githubpem") + + def test_load_filename_from_path_existing_file_path(self, mocker, tmpdir): + p = tmpdir.mkdir("sub").join("hello.txt") + p.write("This is not a knife") + mocker.patch.dict(os.environ, {}, clear=True) + mocker.patch.object( + ConfigHelper, "load_yaml_file", side_effect=FileNotFoundError() + ) + this_config = ConfigHelper() + this_config.set_params( + {"some": {"githubpem": {"source_type": "filepath", "value": str(p)}}} + ) + res = this_config.load_filename_from_path("some", "githubpem") + assert res == "This is not a knife" + + def test_load_filename_from_path_just_using_string_existing_file_path( + self, mocker, tmpdir + ): + mocker.patch.dict(os.environ, {}, clear=True) + mocker.patch.object( + ConfigHelper, "load_yaml_file", side_effect=FileNotFoundError() + ) + p = tmpdir.mkdir("sub").join("hello.txt") + p.write("This is not a knife. This is a knife") + this_config = ConfigHelper() + this_config.set_params({"some": {"githubpem": str(p)}}) + res = this_config.load_filename_from_path("some", "githubpem") + assert res == "This is not a knife. This is a knife" diff --git a/libs/shared/tests/unit/test_github.py b/libs/shared/tests/unit/test_github.py new file mode 100644 index 0000000000..c17a6ac14a --- /dev/null +++ b/libs/shared/tests/unit/test_github.py @@ -0,0 +1,260 @@ +import pytest +from prometheus_client import REGISTRY + +from shared.github import ( + InvalidInstallationError, + get_github_integration_token, +) + +# DONT WORRY, this is generated for the purposes of validation, and is not the real +# one on which the code ran +fake_private_key = """-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDCFqq2ygFh9UQU/6PoDJ6L9e4ovLPCHtlBt7vzDwyfwr3XGxln +0VbfycVLc6unJDVEGZ/PsFEuS9j1QmBTTEgvCLR6RGpfzmVuMO8wGVEO52pH73h9 +rviojaheX/u3ZqaA0di9RKy8e3L+T0ka3QYgDx5wiOIUu1wGXCs6PhrtEwICBAEC +gYBu9jsi0eVROozSz5dmcZxUAzv7USiUcYrxX007SUpm0zzUY+kPpWLeWWEPaddF +VONCp//0XU8hNhoh0gedw7ZgUTG6jYVOdGlaV95LhgY6yXaQGoKSQNNTY+ZZVT61 +zvHOlPynt3GZcaRJOlgf+3hBF5MCRoWKf+lDA5KiWkqOYQJBAMQp0HNVeTqz+E0O +6E0neqQDQb95thFmmCI7Kgg4PvkS5mz7iAbZa5pab3VuyfmvnVvYLWejOwuYSp0U +9N8QvUsCQQD9StWHaVNM4Lf5zJnB1+lJPTXQsmsuzWvF3HmBkMHYWdy84N/TdCZX +Cxve1LR37lM/Vijer0K77wAx2RAN/ppZAkB8+GwSh5+mxZKydyPaPN29p6nC6aLx +3DV2dpzmhD0ZDwmuk8GN+qc0YRNOzzJ/2UbHH9L/lvGqui8I6WLOi8nDAkEA9CYq +ewfdZ9LcytGz7QwPEeWVhvpm0HQV9moetFWVolYecqBP4QzNyokVnpeUOqhIQAwe +Z0FJEQ9VWsG+Df0noQJBALFjUUZEtv4x31gMlV24oiSWHxIRX4fEND/6LpjleDZ5 +C/tY+lZIEO1Gg/FxSMB+hwwhwfSuE3WohZfEcSy+R48= +-----END RSA PRIVATE KEY-----""" + + +class TestGithubSpecificLogic(object): + def test_get_github_integration_token_enterprise(self, mocker, mock_configuration): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_github_integration_token"}, + ) + e_before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_enterprise_total", + labels={"endpoint": "get_github_integration_token"}, + ) + service = "github_enterprise" + mock_configuration._params[service] = {"url": "http://legit-github"} + integration_id = 1 + mocked_post = mocker.patch("shared.github.requests.post") + mocked_post.return_value.json.return_value = {"token": "arriba"} + mocker.patch("shared.github.get_pem", return_value=fake_private_key) + assert get_github_integration_token(service, integration_id) == "arriba" + mocked_post.assert_called_with( + "http://legit-github/api/v3/app/installations/1/access_tokens", + headers={ + "Accept": "application/vnd.github.machine-man-preview+json", + "Authorization": mocker.ANY, + "User-Agent": "Codecov", + }, + ) + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_github_integration_token"}, + ) + e_after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_enterprise_total", + labels={"endpoint": "get_github_integration_token"}, + ) + assert after - before == 0 + assert e_after - e_before == 1 + + def test_get_github_integration_token_enterprise_host_override( + self, mocker, mock_configuration + ): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_github_integration_token"}, + ) + e_before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_enterprise_total", + labels={"endpoint": "get_github_integration_token"}, + ) + service = "github_enterprise" + mock_configuration._params[service] = { + "url": "https://legit-github", + "api_host_override": "some-other-github.com", + } + integration_id = 1 + mocked_post = mocker.patch("shared.github.requests.post") + mocked_post.return_value.json.return_value = {"token": "arriba"} + mocker.patch("shared.github.get_pem", return_value=fake_private_key) + assert get_github_integration_token(service, integration_id) == "arriba" + mocked_post.assert_called_with( + "https://legit-github/api/v3/app/installations/1/access_tokens", + headers={ + "Accept": "application/vnd.github.machine-man-preview+json", + "Authorization": mocker.ANY, + "User-Agent": "Codecov", + "Host": "some-other-github.com", + }, + ) + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_github_integration_token"}, + ) + e_after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_enterprise_total", + labels={"endpoint": "get_github_integration_token"}, + ) + assert after - before == 0 + assert e_after - e_before == 1 + + def test_get_github_integration_token_production(self, mocker, mock_configuration): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_github_integration_token"}, + ) + e_before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_enterprise_total", + labels={"endpoint": "get_github_integration_token"}, + ) + service = "github" + mock_configuration._params["github_enterprise"] = {"url": "http://legit-github"} + integration_id = 1 + mocked_post = mocker.patch("shared.github.requests.post") + mocked_post.return_value.json.return_value = {"token": "arriba"} + mocker.patch("shared.github.get_pem", return_value=fake_private_key) + assert get_github_integration_token(service, integration_id) == "arriba" + mocked_post.assert_called_with( + "https://api.github.com/app/installations/1/access_tokens", + headers={ + "Accept": "application/vnd.github.machine-man-preview+json", + "Authorization": mocker.ANY, + "User-Agent": "Codecov", + }, + ) + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_github_integration_token"}, + ) + e_after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_enterprise_total", + labels={"endpoint": "get_github_integration_token"}, + ) + assert after - before == 1 + assert e_after - e_before == 0 + + def test_get_github_integration_token_production_host_override( + self, mocker, mock_configuration + ): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_github_integration_token"}, + ) + e_before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_enterprise_total", + labels={"endpoint": "get_github_integration_token"}, + ) + service = "github" + api_url = "https://legit-github" + mock_configuration._params["github"] = { + "api_url": api_url, + "api_host_override": "api.github.com", + } + integration_id = 1 + mocked_post = mocker.patch("shared.github.requests.post") + mocked_post.return_value.json.return_value = {"token": "arriba"} + mocker.patch("shared.github.get_pem", return_value=fake_private_key) + assert get_github_integration_token(service, integration_id) == "arriba" + mocked_post.assert_called_with( + f"{api_url}/app/installations/1/access_tokens", + headers={ + "Accept": "application/vnd.github.machine-man-preview+json", + "Authorization": mocker.ANY, + "User-Agent": "Codecov", + "Host": "api.github.com", + }, + ) + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_github_integration_token"}, + ) + e_after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_enterprise_total", + labels={"endpoint": "get_github_integration_token"}, + ) + assert after - before == 1 + assert e_after - e_before == 0 + + def test_get_github_integration_token_not_found(self, mocker, mock_configuration): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_github_integration_token"}, + ) + e_before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_enterprise_total", + labels={"endpoint": "get_github_integration_token"}, + ) + service = "github" + mock_configuration._params["github_enterprise"] = {"url": "http://legit-github"} + integration_id = 1 + mocked_post = mocker.patch("shared.github.requests.post") + mocked_post.return_value.status_code = 404 + mocker.patch("shared.github.get_pem", return_value=fake_private_key) + with pytest.raises(InvalidInstallationError) as exp: + get_github_integration_token(service, integration_id) + assert exp.value.error_cause == "installation_not_found" + mocked_post.assert_called_with( + "https://api.github.com/app/installations/1/access_tokens", + headers={ + "Accept": "application/vnd.github.machine-man-preview+json", + "Authorization": mocker.ANY, + "User-Agent": "Codecov", + }, + ) + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_github_integration_token"}, + ) + e_after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_enterprise_total", + labels={"endpoint": "get_github_integration_token"}, + ) + assert after - before == 1 + assert e_after - e_before == 0 + + def test_get_github_integration_token_unauthorized( + self, mocker, mock_configuration + ): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_github_integration_token"}, + ) + e_before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_enterprise_total", + labels={"endpoint": "get_github_integration_token"}, + ) + service = "github" + mock_configuration._params["github_enterprise"] = {"url": "http://legit-github"} + integration_id = 1 + mocked_post = mocker.patch("shared.github.requests.post") + mocked_post.return_value.status_code = 403 + mocked_post.return_value.json.return_value = { + "message": "This installation has been suspended", + "documentation_url": "https://docs.github.com/rest/reference/apps#create-an-installation-access-token-for-an-app", + } + mocker.patch("shared.github.get_pem", return_value=fake_private_key) + with pytest.raises(InvalidInstallationError) as exp: + get_github_integration_token(service, integration_id) + assert exp.value.error_cause == "installation_suspended" + mocked_post.assert_called_with( + "https://api.github.com/app/installations/1/access_tokens", + headers={ + "Accept": "application/vnd.github.machine-man-preview+json", + "Authorization": mocker.ANY, + "User-Agent": "Codecov", + }, + ) + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_github_integration_token"}, + ) + e_after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_enterprise_total", + labels={"endpoint": "get_github_integration_token"}, + ) + assert after - before == 1 + assert e_after - e_before == 0 diff --git a/libs/shared/tests/unit/test_license.py b/libs/shared/tests/unit/test_license.py new file mode 100644 index 0000000000..53af14fa7a --- /dev/null +++ b/libs/shared/tests/unit/test_license.py @@ -0,0 +1,195 @@ +import json +from base64 import b64encode +from datetime import datetime +from unittest.mock import patch + +from shared.encryption.standard import EncryptorWithAlreadyGeneratedKey +from shared.license import ( + LICENSE_ERRORS_MESSAGES, + LicenseInformation, + get_current_license, + parse_license, + startup_license_logging, +) +from tests.base import BaseTestCase + +valid_trial_license_encrypted = "8rz8TfoZ1HDR5P2kpXSOaSvihqbHnJ4DANvDTB/J94tMjovTUUmuIX07W9FwB0UiiAp4j9McdH4JH5cloihjKqwluwC03t22/UA+4SHwxHbi6IhBbYXCEggYcrwtyjcdA4y3yARixGEsNEwDqAzxXLOe95nMetpb1u1Jr8E6CWp/2QSqvIUww8qTkegESk+3CiH3bPrA71pW8w9KYDX65g==" +invalid_license_encrypted = ( + "8rz8TfodsdsSOaSvih09nvnasu4DANvdsdsauIX07W9FwB0UiiAp4j9McdH4JH5cloihjKqadsada" +) + + +def test_sample_license_checking(): + encrypted_license = valid_trial_license_encrypted + expected_result = LicenseInformation( + is_valid=True, + is_trial=True, + message=None, + url="https://codeov.mysite.com", + number_allowed_users=None, + number_allowed_repos=None, + expires=datetime(2020, 5, 9, 0, 0), + ) + assert parse_license(encrypted_license) == expected_result + + +def test_sample_license_pr_billing(): + """ + wxWEJyYgIcFpi6nBSyKQZQeaQ9Eqpo3SXyUomAqQOzOFjdYB3A8fFM1rm+kOt2ehy9w95AzrQqrqfxi9HJIb2zLOMOB9tSy52OykVCzFtKPBNsXU/y5pQKOfV7iI3w9CHFh3tDwSwgjg8UsMXwQPOhrpvl2GdHpwEhFdaM2O3vY7iElFgZfk5D9E7qEnp+WysQwHKxDeKLI7jWCnBCBJLDjBJRSz0H7AfU55RQDqtTrnR+rsLDHOzJ80/VxwVYhb + License expires on 2021-01-01 + ---- Internal purposes only ---- + {'company': 'Test Company', 'expires': '2021-01-01 00:00:00', 'url': 'https://codecov.mysite.com', 'trial': False, 'users': 10, 'repos': None, 'pr_billing': True} + """ + encrypted_license = "wxWEJyYgIcFpi6nBSyKQZQeaQ9Eqpo3SXyUomAqQOzOFjdYB3A8fFM1rm+kOt2ehy9w95AzrQqrqfxi9HJIb2zLOMOB9tSy52OykVCzFtKPBNsXU/y5pQKOfV7iI3w9CHFh3tDwSwgjg8UsMXwQPOhrpvl2GdHpwEhFdaM2O3vY7iElFgZfk5D9E7qEnp+WysQwHKxDeKLI7jWCnBCBJLDjBJRSz0H7AfU55RQDqtTrnR+rsLDHOzJ80/VxwVYhb" + expected_result = LicenseInformation( + is_valid=True, + is_trial=False, + message=None, + url="https://codecov.mysite.com", + number_allowed_users=10, + is_pr_billing=True, + number_allowed_repos=None, + expires=datetime(2021, 1, 1, 0, 0), + ) + assert parse_license(encrypted_license) == expected_result + + +def test_sample_license_checking_with_users_and_repos(): + encrypted_license = "0dRbhbzp8TVFQp7P4e2ES9lSfyQlTo8J7LQ/N51yeAE/KcRBCnU+QsVvVMDuLL4xNGXGGk9p4ZTmIl0II3cMr0tIoPHe9Re2UjommalyFYuP8JjjnNR/Ql2DnjOzEnTzsE2Poq9xlNHcIU4F9gC2WOYPnazR6U+t4CelcvIAbEpbOMOiw34nVyd3OEmWusquMNrwkNkk/lwjwCJmj6bTXQ==" + expected_result = LicenseInformation( + is_valid=True, + is_trial=True, + message=None, + url="https://codeov.mysite.com", + number_allowed_users=10, + number_allowed_repos=20, + expires=datetime(2020, 5, 10, 0, 0), + ) + assert parse_license(encrypted_license) == expected_result + + +def test_invalid_license_checking_nonvalid_64encoded(): + encrypted_license = invalid_license_encrypted + expected_result = LicenseInformation( + is_valid=False, + is_trial=False, + message=None, + url=None, + number_allowed_users=None, + number_allowed_repos=None, + expires=None, + ) + assert parse_license(encrypted_license) == expected_result + + +def test_invalid_license_checking_nonvalid_encrypted(): + encrypted_license = b64encode(b"suchabadlicense") + expected_result = LicenseInformation( + is_valid=False, + is_trial=False, + message=None, + url=None, + number_allowed_users=None, + number_allowed_repos=None, + expires=None, + ) + assert parse_license(encrypted_license) == expected_result + + +def test_invalid_license_checking_wrong_key(): + a_good_value = { + "users": None, + "url": "https://codeov.mysite.com", + "company": "name", + "expires": "2020-05-09 00:00:00", + "trial": True, + "repos": None, + } + jsonified_good_value = json.dumps(a_good_value) + wrong_key = b"\xe4n\n\xeb\xaa\xe6\x9d0\xed\xfaL\xe2c\x81h\xaf\xac*Pyq(H\xcc" + wrong_encryptor = EncryptorWithAlreadyGeneratedKey(wrong_key) + encrypted_license = wrong_encryptor.encode(jsonified_good_value) + expected_result = LicenseInformation( + is_valid=False, + is_trial=False, + message=None, + url=None, + number_allowed_users=None, + number_allowed_repos=None, + expires=None, + ) + assert parse_license(encrypted_license) == expected_result + + +def test_get_current_license(mock_configuration): + encrypted_license = valid_trial_license_encrypted + mock_configuration.set_params({"setup": {"enterprise_license": encrypted_license}}) + expected_result = LicenseInformation( + is_valid=True, + is_trial=True, + message=None, + url="https://codeov.mysite.com", + number_allowed_users=None, + number_allowed_repos=None, + expires=datetime(2020, 5, 9, 0, 0), + ) + assert get_current_license() == expected_result + + +def test_get_current_license_no_license(mock_configuration): + mock_configuration.set_params({"setup": None}) + expected_result = LicenseInformation( + is_valid=False, + is_trial=False, + message="No license key found. Please contact enterprise@codecov.io to issue a license key. Thank you!", + url=None, + number_allowed_users=None, + number_allowed_repos=None, + expires=None, + ) + assert get_current_license() == expected_result + + +@patch("builtins.print") +class TestUserGivenSecret(BaseTestCase): + def test_startup_license_logging_valid(self, mock_print, mock_configuration): + encrypted_license = valid_trial_license_encrypted + mock_configuration.set_params( + {"setup": {"enterprise_license": encrypted_license}} + ) + + expected_log = [ + "", + "==> Checking License", + " License is valid", + " License expires 2020-05-09 00:00:00 <==", + "", + ] + + startup_license_logging() + mock_print.assert_called_once_with(*expected_log, sep="\n") + + @patch("shared.license.parse_license") + def test_startup_license_logging_invalid( + self, mock_license_parsing, mock_print, mock_configuration + ): + mock_license_parsing.return_value = LicenseInformation( + is_valid=False, + message=LICENSE_ERRORS_MESSAGES["no-license"], + ) + + mock_configuration.set_params( + {"setup": {"enterprise_license": True}} + ) # value doesn't matter since parse_license is mocked + + expected_log = [ + "", + "==> Checking License", + " License is INVALID", + f" Warning: {LICENSE_ERRORS_MESSAGES['no-license']}", + " License expires NOT FOUND <==", + "", + ] + + startup_license_logging() + mock_print.assert_called_once_with(*expected_log, sep="\n") diff --git a/libs/shared/tests/unit/test_report.py b/libs/shared/tests/unit/test_report.py new file mode 100644 index 0000000000..1698675ea5 --- /dev/null +++ b/libs/shared/tests/unit/test_report.py @@ -0,0 +1,477 @@ +import pytest + +from shared.reports.editable import EditableReport, EditableReportFile +from shared.reports.exceptions import LabelIndexNotFoundError, LabelNotFoundError +from shared.reports.resources import Report, ReportFile +from shared.reports.serde import _encode_chunk +from shared.reports.types import ( + CoverageDatapoint, + LineSession, + ReportHeader, + ReportLine, + ReportTotals, +) +from shared.utils.sessions import Session + + +def report_with_file_summaries(): + return Report( + files={ + "calc/CalcCore.cpp": [ + 0, + ReportTotals( + files=0, + lines=10, + hits=7, + misses=2, + partials=1, + coverage="70.00000", + branches=6, + methods=4, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ], + "calc/CalcCore.h": [ + 1, + ReportTotals( + files=0, + lines=1, + hits=1, + misses=0, + partials=0, + coverage="100", + branches=0, + methods=1, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ], + "calc/Calculator.cpp": [ + 2, + ReportTotals( + files=0, + lines=4, + hits=3, + misses=1, + partials=0, + coverage="75.00000", + branches=1, + methods=1, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ], + }, + totals=ReportTotals( + files=3, + lines=15, + hits=11, + misses=3, + partials=1, + coverage="73.33333", + branches=7, + methods=6, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + ) + + +def test_files(): + r = Report(files={"py.py": [0, ReportTotals(1)]}) + assert r.files == ["py.py"] + + +@pytest.mark.unit +class TestReportHeader(object): + def test_default(self): + r = Report() + assert r.header == ReportHeader() + assert r.labels_index is None + + def test_get(self): + r = Report() + r._header = ReportHeader(labels_index={0: "special_label"}) + assert r.header == ReportHeader(labels_index={0: "special_label"}) + + def test_from_archive(self): + r = Report( + files={"other-file.py": [1, ReportTotals(2)]}, + chunks='{"labels_index":{"0": "special_label"}}\n<<<<< end_of_header >>>>>\nnull\n[1]\n[1]\n[1]\n<<<<< end_of_chunk >>>>>\nnull\n[1]\n[1]\n[1]', + ) + assert r.header == ReportHeader(labels_index={0: "special_label"}) + + def test_setter(self): + r = Report() + header = ReportHeader(labels_index={0: "special_label"}) + r.header = header + assert r.header == header + + def test_get_labels_index(self): + r = Report() + r._header = ReportHeader(labels_index={0: "special_label"}) + assert r.labels_index == {0: "special_label"} + + def test_set_labels_index(self): + r = Report() + assert r.labels_index is None + r.labels_index = {0: "special_label"} + assert r.labels_index == {0: "special_label"} + assert r.header == ReportHeader(labels_index={0: "special_label"}) + + def test_partial_set_labels_index(self): + r = Report() + r._header = ReportHeader(labels_index={0: "special_label"}) + assert r.labels_index == {0: "special_label"} + r.labels_index[1] = "some_test" + r.labels_index == {0: "special_label", 1: "some_test"} + + +@pytest.mark.unit +def test_get_file_totals(mocker): + report = report_with_file_summaries() + + expected_totals = ReportTotals( + files=0, + lines=10, + hits=7, + misses=2, + partials=1, + coverage="70.00000", + branches=6, + methods=4, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + assert report.get_file_totals("calc/CalcCore.cpp") == expected_totals + + +def test_get_label_from_idx(): + report = Report() + label_idx = {0: "Special_global_label", 1: "banana", 2: "cachorro"} + report._header = ReportHeader(labels_index=label_idx) + report_file = ReportFile( + name="something.py", + lines=[ + ReportLine.create( + coverage=1, + type=None, + sessions=[[0, 1]], + datapoints=[ + CoverageDatapoint( + sessionid=0, coverage=1, coverage_type=None, label_ids=[0, 2] + ) + ], + ) + ], + ) + report.append(report_file) + labels_in_report = set() + for file in report: + for line in file: + for datapoint in line.datapoints: + for label_id in datapoint.label_ids: + labels_in_report.add(report.lookup_label_by_id(label_id)) + assert "Special_global_label" in labels_in_report + assert "cachorro" in labels_in_report + assert "banana" not in labels_in_report + + +def test_lookup_label_by_id_fails(): + report = Report() + with pytest.raises(LabelIndexNotFoundError): + report.lookup_label_by_id(0) + + label_idx = {0: "Special_global_label", 1: "banana", 2: "cachorro"} + report._header = ReportHeader(labels_index=label_idx) + + with pytest.raises(LabelNotFoundError): + report.lookup_label_by_id(100) + + +@pytest.mark.unit +def test_merge_into_editable_report(): + editable_report = EditableReport( + files={"file.py": [1, ReportTotals(2)]}, + chunks="null\n[1]\n[1]\n[1]\n<<<<< end_of_chunk >>>>>\nnull\n[1]\n[1]\n[1]", + ) + new_report = Report( + files={"other-file.py": [1, ReportTotals(2)]}, + chunks="null\n[1]\n[1]\n[1]\n<<<<< end_of_chunk >>>>>\nnull\n[1]\n[1]\n[1]", + ) + editable_report.merge(new_report) + assert list(editable_report.files) == ["file.py", "other-file.py"] + for file in editable_report: + assert isinstance(file, EditableReportFile) + + +@pytest.mark.unit +def test_calculate_diff(): + v3 = { + "files": {"a": [0, None], "d": [1, None]}, + "sessions": {}, + "totals": {}, + "chunks": [ + "\n[1, null, null, null]\n[0, null, null, null]", + "\n[1, null, null, null]\n[0, null, null, null]", + ], + } + r = Report(**v3) + diff = { + "files": { + "a": { + "type": "new", + "segments": [{"header": list("1313"), "lines": list("---+++")}], + }, + "b": {"type": "deleted"}, + "c": {"type": "modified"}, + "d": { + "type": "modified", + "segments": [ + {"header": ["10", "3", "10", "3"], "lines": list("---+++")} + ], + }, + } + } + res = r.calculate_diff(diff) + expected_result = { + "files": { + "a": ReportTotals( + files=0, lines=2, hits=1, misses=1, partials=0, coverage="50.00000" + ), + "d": ReportTotals( + files=0, lines=0, hits=0, misses=0, partials=0, coverage=None + ), + }, + "general": ReportTotals( + files=2, + lines=2, + hits=1, + misses=1, + partials=0, + coverage="50.00000", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ), + } + assert res["files"] == expected_result["files"] + assert res == expected_result + + +@pytest.mark.unit +def test_apply_diff_no_diff(): + v3 = { + "files": {"a": [0, None], "d": [1, None]}, + "sessions": {}, + "totals": {}, + "chunks": [ + "\n[1, null, null, null]\n[0, null, null, null]", + "\n[1, null, null, null]\n[0, null, null, null]", + ], + } + r = Report(**v3) + diff = {"files": {}} + res = r.apply_diff(diff) + assert res is None + assert diff == {"files": {}} + + +@pytest.mark.unit +def test_encode_chunk(): + assert _encode_chunk(None) == "null" + assert _encode_chunk(ReportFile(name="name.ply")) == '{"present_sessions":[]}\n' + assert ( + _encode_chunk([ReportLine.create(2), ReportLine.create(1)]) + == "[[2,null,null,null,null,null],[1,null,null,null,null,null]]" + ) + + +@pytest.mark.unit +def test_delete_session(): + chunks = "\n".join( + [ + "{}", + "[1, null, [[0, 1], [1, 0]]]", + "", + "", + "[0, null, [[0, 0], [1, 0]]]", + "[1, null, [[0, 1], [1, 1]]]", + "[1, null, [[0, 0], [1, 1]]]", + "", + "", + '[1, null, [[0, 1], [1, "1/2"]]]', + '[1, null, [[0, "1/2"], [1, 1]]]', + "", + "", + "[1, null, [[0, 1]]]", + "[1, null, [[1, 1]]]", + '["1/2", null, [[0, "1/2"], [1, 0]]]', + '["1/2", null, [[0, 0], [1, "1/2"]]]', + ] + ) + report_file = EditableReportFile(name="file.py", lines=chunks) + assert report_file.totals == ReportTotals( + files=0, + lines=10, + hits=7, + misses=1, + partials=2, + coverage="70.00000", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + report_file.delete_multiple_sessions({1}) + expected_result = [ + (1, ReportLine.create(coverage=1, sessions=[LineSession(0, 1)])), + (4, ReportLine.create(coverage=0, sessions=[LineSession(0, 0)])), + (5, ReportLine.create(coverage=1, sessions=[LineSession(0, 1)])), + (6, ReportLine.create(coverage=0, sessions=[LineSession(0, 0)])), + (9, ReportLine.create(coverage=1, sessions=[LineSession(0, 1)])), + (10, ReportLine.create(coverage="1/2", sessions=[LineSession(0, "1/2")])), + (13, ReportLine.create(coverage=1, sessions=[LineSession(0, 1)])), + (15, ReportLine.create(coverage="1/2", sessions=[LineSession(0, "1/2")])), + (16, ReportLine.create(coverage=0, sessions=[LineSession(0, 0)])), + ] + assert list(report_file.lines) == expected_result + assert report_file.get(1) == ReportLine.create( + coverage=1, sessions=[LineSession(0, 1)] + ) + assert report_file.get(13) == ReportLine.create( + coverage=1, sessions=[LineSession(0, 1)] + ) + assert report_file.get(14) is None + assert report_file.totals == ReportTotals( + files=0, + lines=9, + hits=4, + misses=3, + partials=2, + coverage="44.44444", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) + + +@pytest.mark.unit +def test_get_flag_names(sample_report): + assert sample_report.get_flag_names() == ["complex", "simple"] + + +@pytest.mark.unit +def test_get_flag_names_no_sessions(): + assert Report().get_flag_names() == [] + + +@pytest.mark.unit +def test_get_flag_names_sessions_no_flags(): + s = Session() + r = Report() + r.add_session(s) + assert r.get_flag_names() == [] + + +@pytest.mark.unit +def test_shift_lines_by_diff(): + r = ReportFile("filename", lines=[ReportLine.create(n) for n in range(8)]) + report = Report(sessions={0: Session()}) + report.append(r) + assert list(r.lines) == [ + (1, ReportLine.create(0)), + (2, ReportLine.create(1)), + (3, ReportLine.create(2)), + (4, ReportLine.create(3)), + (5, ReportLine.create(4)), + (6, ReportLine.create(5)), + (7, ReportLine.create(6)), + (8, ReportLine.create(7)), + ] + assert report.totals == ReportTotals( + files=1, + lines=8, + hits=7, + misses=1, + partials=0, + coverage="87.50000", + branches=0, + methods=0, + messages=0, + sessions=1, + complexity=0, + complexity_total=0, + diff=0, + ) + report.shift_lines_by_diff( + { + "files": { + "filename": { + "type": "modified", + "segments": [ + { + # [-, -, POS_to_start, new_lines_added] + "header": [1, 1, 1, 1], + "lines": ["- afefe", "+ fefe", "="], + }, + { + # [-, -, POS_to_start, new_lines_added] + "header": [5, 3, 5, 2], + "lines": ["- ", "- ", "- ", "+ ", "+ ", " ="], + }, + ], + } + } + } + ) + assert report.files == ["filename"] + file = report.get("filename") + assert list(file.lines) == [ + (2, ReportLine.create(1)), + (3, ReportLine.create(2)), + (4, ReportLine.create(3)), + (7, ReportLine.create(7)), + ] + assert file.totals == ReportTotals( + files=0, + lines=4, + hits=4, + misses=0, + partials=0, + coverage="100", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) diff --git a/libs/shared/tests/unit/test_report_file.py b/libs/shared/tests/unit/test_report_file.py new file mode 100644 index 0000000000..514845d126 --- /dev/null +++ b/libs/shared/tests/unit/test_report_file.py @@ -0,0 +1,324 @@ +import pytest + +from shared.reports.resources import ReportFile +from shared.reports.types import ReportLine + + +def format_lines_idx_and_coverage_only(lines): + return [ + tuple( + [ + idx, + f"ReportLine(coverage={report_line.coverage})" + if hasattr(report_line, "coverage") + else report_line, + ] + ) + for idx, report_line in enumerate(lines, start=1) + ] + + +@pytest.mark.unit +def test_shift_lines_by_diff_mixed_changes(): + file = ReportFile("file_1.go") + # Coverage is the line number before the shift + file.append(1, ReportLine.create(coverage=1)) + file.append(2, ReportLine.create(coverage=2)) + file.append(3, ReportLine.create(coverage=3)) + file.append(5, ReportLine.create(coverage=5)) + file.append(6, ReportLine.create(coverage=6)) + file.append(8, ReportLine.create(coverage=8)) + file.append(9, ReportLine.create(coverage=9)) + file.append(10, ReportLine.create(coverage=10)) + + fake_diff = { + "type": "modified", + "before": None, + "segments": [ + { + "header": [3, 3, 3, 4], + "lines": [ + " some go code in line 3", + "-this line was removed", + "+this line was added", + "+this line was also added", + " ", + ], + }, + { + "header": [9, 1, 10, 5], + "lines": [ + " some go code in line 9", + "+add", + "+add", + "+add", + "+add", + ], + }, + ], + } + file.shift_lines_by_diff(fake_diff) + assert format_lines_idx_and_coverage_only(file._lines) == [ + (1, "ReportLine(coverage=1)"), + (2, "ReportLine(coverage=2)"), + (3, "ReportLine(coverage=3)"), + (4, ""), + (5, ""), + (6, "ReportLine(coverage=5)"), + (7, "ReportLine(coverage=6)"), + (8, ""), + (9, "ReportLine(coverage=8)"), + (10, "ReportLine(coverage=9)"), + (11, ""), + (12, ""), + (13, ""), + (14, ""), + (15, "ReportLine(coverage=10)"), + ] + + +@pytest.mark.unit +def test_shift_lines_by_diff_only_adds(): + file = ReportFile("file_1.go") + for i in range(1, 11): + file.append(i, ReportLine.create(coverage=(i))) + fake_diff = { + "type": "modified", + "before": None, + "segments": [ + { + "header": [3, 3, 3, 4], + "lines": [ + " some go code in line 3", + "+this line was added", + " ", + " ", + ], + }, + { + "header": [8, 3, 9, 6], + "lines": [ + " some go code in line 8", + "+add", + " ", + "+add", + "+add", + " ", + ], + }, + ], + } + file.shift_lines_by_diff(fake_diff) + assert format_lines_idx_and_coverage_only(file._lines) == [ + (1, "ReportLine(coverage=1)"), + (2, "ReportLine(coverage=2)"), + (3, "ReportLine(coverage=3)"), + (4, ""), + (5, "ReportLine(coverage=4)"), + (6, "ReportLine(coverage=5)"), + (7, "ReportLine(coverage=6)"), + (8, "ReportLine(coverage=7)"), + (9, "ReportLine(coverage=8)"), + (10, ""), + (11, "ReportLine(coverage=9)"), + (12, ""), + (13, ""), + (14, "ReportLine(coverage=10)"), + ] + + +@pytest.mark.unit +def test_shift_lines_by_diff_only_removals(): + file = ReportFile("file_1.go") + for i in range(1, 11): + file.append(i, ReportLine.create(coverage=(i))) + fake_diff = { + "type": "modified", + "before": None, + "segments": [ + { + "header": [1, 3, 1, 2], + "lines": [ + " some go code in line 1", + "-this line was removed", + " ", + ], + }, + { + "header": [5, 6, 4, 3], + "lines": [ + " some go code in line 5", + "-removed", + " ", + "-removed", + "-removed", + " ", + ], + }, + ], + } + file.shift_lines_by_diff(fake_diff) + assert format_lines_idx_and_coverage_only(file._lines) == [ + (1, "ReportLine(coverage=1)"), + (2, "ReportLine(coverage=3)"), + (3, "ReportLine(coverage=4)"), + (4, "ReportLine(coverage=5)"), + (5, "ReportLine(coverage=7)"), + (6, "ReportLine(coverage=10)"), + ] + + +@pytest.mark.unit +def test_shift_lines_by_diff_wiki_example(): + file = ReportFile("file") + for i in range(1, 31): + file.append(i, ReportLine.create(coverage=(i))) + fake_diff = { + "segments": [ + { + "header": [1, 3, 1, 9], + "lines": [ + "+This is an important", + "+notice! It should", + "+therefore be located at", + "+the beginning of this", + "+document!", + "+", + " This part of the", + " document has stayed the", + " same from version to", + ], + }, + { + "header": [8, 13, 14, 8], + "lines": [ + " compress the size of the", + " changes.", + " ", + "-This paragraph contains", + "-text that is outdated.", + "-It will be deleted in the", + "-near future.", + "-", + " It is important to spell", + "-check this dokument. On", + "+check this document. On", + " the other hand, a", + " misspelled word isn't", + " the end of the world.", + ], + }, + { + "header": [22, 3, 23, 7], + "lines": [ + " this paragraph needs to", + " be changed. Things can", + " be added after it.", + "+", + "+This paragraph contains", + "+important new additions", + "+to this document.", + ], + }, + ] + } + file.shift_lines_by_diff(fake_diff) + assert format_lines_idx_and_coverage_only(file._lines) == [ + (1, ""), + (2, ""), + (3, ""), + (4, ""), + (5, ""), + (6, ""), + (7, "ReportLine(coverage=1)"), + (8, "ReportLine(coverage=2)"), + (9, "ReportLine(coverage=3)"), + (10, "ReportLine(coverage=4)"), + (11, "ReportLine(coverage=5)"), + (12, "ReportLine(coverage=6)"), + (13, "ReportLine(coverage=7)"), + (14, "ReportLine(coverage=8)"), + (15, "ReportLine(coverage=9)"), + (16, "ReportLine(coverage=10)"), + (17, "ReportLine(coverage=16)"), + (18, ""), + (19, "ReportLine(coverage=18)"), + (20, "ReportLine(coverage=19)"), + (21, "ReportLine(coverage=20)"), + (22, "ReportLine(coverage=21)"), + (23, "ReportLine(coverage=22)"), + (24, "ReportLine(coverage=23)"), + (25, "ReportLine(coverage=24)"), + (26, ""), + (27, ""), + (28, ""), + (29, ""), + (30, "ReportLine(coverage=25)"), + (31, "ReportLine(coverage=26)"), + (32, "ReportLine(coverage=27)"), + (33, "ReportLine(coverage=28)"), + (34, "ReportLine(coverage=29)"), + (35, "ReportLine(coverage=30)"), + ] + + +@pytest.mark.unit +def test_shift_lines_by_diff_changes_to_no_code_at_eof(): + file = ReportFile("file") + for i in range(1, 10): + file.append(i, ReportLine.create(coverage=(i))) + fake_diff = { + "segments": [ + { + "header": [1, 25, 1, 20], + "lines": [ + " This is an important", + " notice! It should", + " therefore be located at", + " the beginning of this", + " document!", + " ", + " This part of the", + " document has stayed the", + " same from version to", + " LAST LINE OF CODE IN THE FILE", + " ", + " #comment #comment", + " #comment #comment", + " #comment #comment", + " #comment #comment", + " #comment #comment", + " #comment #comment", + " #comment #comment", + " #comment #comment", + " #comment #comment", + "-#comment #comment", + "-#comment #comment", + "-#comment #comment", + "-#comment #comment", + "-#comment #comment", + ], + }, + ] + } + file.shift_lines_by_diff(fake_diff) + assert format_lines_idx_and_coverage_only(file._lines) == [ + (1, "ReportLine(coverage=1)"), + (2, "ReportLine(coverage=2)"), + (3, "ReportLine(coverage=3)"), + (4, "ReportLine(coverage=4)"), + (5, "ReportLine(coverage=5)"), + (6, "ReportLine(coverage=6)"), + (7, "ReportLine(coverage=7)"), + (8, "ReportLine(coverage=8)"), + (9, "ReportLine(coverage=9)"), + ] + + +@pytest.mark.unit +def test_del_item(): + r = ReportFile("name.h") + with pytest.raises(TypeError): + del r["line"] + with pytest.raises(ValueError): + del r[-1] diff --git a/libs/shared/tests/unit/test_rollouts.py b/libs/shared/tests/unit/test_rollouts.py new file mode 100644 index 0000000000..1b3338affc --- /dev/null +++ b/libs/shared/tests/unit/test_rollouts.py @@ -0,0 +1,384 @@ +import os +from unittest.mock import patch + +from django.core.exceptions import SynchronousOnlyOperation +from django.test import TestCase + +from shared.django_apps.rollouts.models import ( + FeatureFlag, + FeatureFlagVariant, + RolloutUniverse, +) +from shared.rollouts import Feature + + +class TestFeature(TestCase): + def test_buckets(self): + complex = FeatureFlag.objects.create( + name="complex", proportion=0.5, salt="random_salt" + ) + FeatureFlagVariant.objects.create( + name="complex_a", feature_flag=complex, proportion=1 / 3, value=1 + ) + FeatureFlagVariant.objects.create( + name="complex_b", feature_flag=complex, proportion=1 / 3, value=2 + ) + FeatureFlagVariant.objects.create( + name="complex_c", feature_flag=complex, proportion=1 / 3, value=3 + ) + complex_feature = Feature("complex") + + # To make the math simpler, let's pretend our hash function can only + # return 200 different values. + with patch.object(Feature, "HASHSPACE", 200): + complex_feature.check_value( + identifier=1234 + ) # to force fetch values from db + + # Because each feature variant has a proportion of 1/3, our three + # buckets should be [0, 66], [66, 133], [133, 200]. However, our top-level + # feature proportion is only 0.5, so each bucket size should be then + # halved: [0, 33], [66, 99], [133, 166] + + buckets = complex_feature._buckets + assert list(map(lambda x: (x[0], x[1]), buckets)) == [ + (0, 33), + (66, 99), + (133, 166), + ] + + def test_fully_rolled_out(self): + rolled_out = FeatureFlag.objects.create( + name="rolled_out", proportion=1.0, salt="random_salt" + ) + FeatureFlagVariant.objects.create( + name="rolled_out_enabled", + feature_flag=rolled_out, + proportion=1.0, + value=True, + ) + + # If the feature is 100% rolled out and only has one variant, then we + # should skip the hashing and bucket stuff and just return the single + # possible value. + feature = Feature("rolled_out") + assert feature.check_value(123, default=False) == True + assert not hasattr(feature.__dict__, "_buckets") + + def test_overrides_by_owner(self): + overrides = FeatureFlag.objects.create( + name="overrides", + proportion=1.0, + salt="random_salt", + rollout_universe=RolloutUniverse.OWNER_ID, + ) + FeatureFlagVariant.objects.create( + name="overrides_a", + feature_flag=overrides, + proportion=1 / 2, + value=1, + override_owner_ids=[123], + override_repo_ids=[321], + ) + FeatureFlagVariant.objects.create( + name="overrides_b", + feature_flag=overrides, + proportion=1 / 2, + value=2, + override_owner_ids=[321], + override_repo_ids=[123], + ) + + # If an identifier was manually opted into a specific variant, skip the + # hashing/bucket calculation and just return the value for that variant. + feature = Feature( + "overrides", + ) + + assert feature.check_value(321, default=1) == 2 + assert feature.check_value(123, default=2) == 1 + assert not hasattr(feature.__dict__, "_buckets") + + def test_overrides_by_email(self): + overrides = FeatureFlag.objects.create( + name="overrides", + proportion=1.0, + salt="random_salt", + rollout_universe=RolloutUniverse.EMAIL, + ) + FeatureFlagVariant.objects.create( + name="overrides_a", + feature_flag=overrides, + proportion=1 / 2, + value=1, + override_emails=["daniel.yu@sentry.io"], + ) + FeatureFlagVariant.objects.create( + name="overrides_b", + feature_flag=overrides, + proportion=1 / 2, + value=2, + override_emails=["yu.daniel@sentry.io"], + ) + + # If an identifier was manually opted into a specific variant, skip the + # hashing/bucket calculation and just return the value for that variant. + feature = Feature( + "overrides", + ) + + assert feature.check_value("yu.daniel@sentry.io", default=1) == 2 + assert feature.check_value("daniel.yu@sentry.io", default=2) == 1 + assert not hasattr(feature.__dict__, "_buckets") + + def test_overrides_by_repo(self): + overrides = FeatureFlag.objects.create( + name="overrides", + proportion=1.0, + salt="random_salt", + rollout_universe=RolloutUniverse.REPO_ID, + ) + FeatureFlagVariant.objects.create( + name="overrides_a", + feature_flag=overrides, + proportion=1 / 2, + value=1, + override_repo_ids=[123], + override_owner_ids=[321], + ) + FeatureFlagVariant.objects.create( + name="overrides_b", + feature_flag=overrides, + proportion=1 / 2, + value=2, + override_repo_ids=[321], + override_owner_ids=[123], + ) + + # If an identifier was manually opted into a specific variant, skip the + # hashing/bucket calculation and just return the value for that variant. + feature = Feature( + "overrides", + ) + + assert feature.check_value(321, default=1) == 2 + assert feature.check_value(123, default=2) == 1 + assert not hasattr(feature.__dict__, "_buckets") + + def test_overrides_by_org(self): + overrides = FeatureFlag.objects.create( + name="overrides", + proportion=1.0, + salt="random_salt", + rollout_universe=RolloutUniverse.ORG_ID, + ) + FeatureFlagVariant.objects.create( + name="overrides_a", + feature_flag=overrides, + proportion=1 / 2, + value=1, + override_org_ids=[123], + override_owner_ids=[321], + ) + FeatureFlagVariant.objects.create( + name="overrides_b", + feature_flag=overrides, + proportion=1 / 2, + value=2, + override_org_ids=[321], + override_owner_ids=[123], + ) + + # If an identifier was manually opted into a specific variant, skip the + # hashing/bucket calculation and just return the value for that variant. + feature = Feature( + "overrides", + ) + + assert feature.check_value(321, default=1) == 2 + assert feature.check_value(123, default=2) == 1 + assert not hasattr(feature.__dict__, "_buckets") + + def test_override_no_proportion(self): + overrides = FeatureFlag.objects.create( + name="overrides_no_proportion", proportion=0, salt="random_salt" + ) + FeatureFlagVariant.objects.create( + name="single_variant", + feature_flag=overrides, + proportion=0, + value=2, + override_owner_ids=[321, 123], + ) + + feature = Feature("overrides_no_proportion") + + assert feature.check_value(identifier=321, default=1) == 2 + assert feature.check_value(identifier=123, default=1) == 2 + + def test_not_in_test_gets_default(self): + not_in_test = FeatureFlag.objects.create( + name="not_in_test", proportion=0.1, salt="random_salt" + ) + FeatureFlagVariant.objects.create( + name="not_in_test_enabled", + feature_flag=not_in_test, + proportion=1.0, + value=True, + ) + feature = Feature("not_in_test") + # If the feature is only 10% rolled out, 2**128-1 is way past the end of + # the test population and should get a default value back. + with patch("mmh3.hash128", return_value=2**128 - 1): + assert feature.check_value(identifier=123, default="default") == "default" + + def test_return_values_for_each_bucket(self): + return_values_for_each_bucket = FeatureFlag.objects.create( + name="return_values_for_each_bucket", proportion=1.0, salt="random_salt" + ) + FeatureFlagVariant.objects.create( + name="return_values_for_each_bucket_a", + feature_flag=return_values_for_each_bucket, + proportion=1 / 2, + value="first bucket", + ) + FeatureFlagVariant.objects.create( + name="return_values_for_each_bucket_b", + feature_flag=return_values_for_each_bucket, + proportion=1 / 2, + value="second bucket", + ) + + feature = Feature("return_values_for_each_bucket") + # To make the math simpler, let's pretend our hash function can only + # return 100 different values. + with patch.object(Feature, "HASHSPACE", 100): + # The feature is 100% rolled out and has two variants at 50% each. So, + # the buckets are [0..50] and [51..100]. Mock the hash function to + # return a value in the first bucket and then a value in the second. + with patch("mmh3.hash128", side_effect=[33, 66]): + assert ( + feature.check_value(identifier=123, default="c") == "first bucket" + ) + assert ( + feature.check_value(identifier=124, default="c") == "second bucket" + ) + + def test_default_feature_flag_created(self): + name = "my_default_feature" + my_default_feature = Feature(name) + + my_default_feature.check_value(identifier=123123123) + + feature_flag = FeatureFlag.objects.filter(name=name).first() + + assert feature_flag is not None + assert feature_flag.proportion == 0 + + def test_sync_feature_call(self): + testing_feature = Feature("testing_dummy_feature") + assert testing_feature.check_value(identifier="hi", default=False) == False + + async def test_async_feature_call_fail(self): + testing_feature = Feature("testing_dummy_feature") + with self.assertRaises(SynchronousOnlyOperation): + testing_feature.check_value(identifier="hi", default=False) == False + + async def test_async_feature_call_success(self): + testing_feature = Feature("testing_dummy_feature") + await testing_feature.check_value_async(identifier="hi", default=False) == False + + def test_rollout_proportion_changes_dont_affect_variant_assignments(self): + my_feature_flag = FeatureFlag.objects.create( + name="my_feature1", proportion=0.5, salt="random_salt" + ) + FeatureFlagVariant.objects.create( + name="var1", feature_flag=my_feature_flag, proportion=1 / 3, value=1 + ) + FeatureFlagVariant.objects.create( + name="var2", feature_flag=my_feature_flag, proportion=1 / 3, value=2 + ) + FeatureFlagVariant.objects.create( + name="var3", feature_flag=my_feature_flag, proportion=1 / 3, value=3 + ) + my_feature = Feature("my_feature1") + + # The purpose of this test is to ensure that even when the feature_flag proportion + # changes, users assignments to variants never change. ie, if user `a` was ever assigned + # to variant 1, then for any feature_flag proportion, user `a` will either be + # assigned to variant 1, or is not participating in the rollout (default value). + # https://github.com/codecov/engineering-team/issues/1515 + + # To make the math simpler, let's pretend our hash function can only + # return 200 different values. + with patch.object(Feature, "HASHSPACE", 200): + with patch("mmh3.hash128", side_effect=[30, 90, 150, 40, 30, 90, 150, 40]): + a = my_feature.check_value(identifier=123, default="c") + b = my_feature.check_value(identifier=124, default="c") + c = my_feature.check_value(identifier=125, default="c") + d = my_feature.check_value(identifier=126, default="d") + + assert a == 1 + assert b == 2 + assert c == 3 + assert d == "d" + + my_feature_flag.proportion = ( + 1.0 # buckets are now: [(0,66), (66, 133), (133, 200)] + ) + my_feature_flag.save() + + my_feature._fetch_and_set_from_db.cache_clear() # clear the TTL + my_feature._fetch_and_set_from_db() # refresh feature flag proportion and clear caches + + assert a == my_feature.check_value(identifier=123, default="c") + assert b == my_feature.check_value(identifier=124, default="c") + assert c == my_feature.check_value(identifier=125, default="c") + assert 1 == my_feature.check_value( + identifier=126, default="d" + ) # now in variant 1 since 40 \in (0,66) + + @patch.dict(os.environ, {"CODECOV__FEATURE__DISABLE": ""}) + def test_check_value_with_env_disable(self): + feature = Feature("my_feature") + with patch.object(feature, "_fetch_and_set_from_db") as fetch_fn: + assert feature.check_value(identifier=1, default=100) == 100 + fetch_fn.assert_not_called() + assert not hasattr(feature.__dict__, "_buckets") + + @patch.dict(os.environ, {"CODECOV__FEATURE__NUM_FEATURE": "30"}) + @patch.dict(os.environ, {"CODECOV__FEATURE__STR_FEATURE": '"foo"'}) + @patch.dict(os.environ, {"CODECOV__FEATURE__NULL_FEATURE": "null"}) + def test_check_value_with_env_override(self): + feature = Feature("num_feature") + with patch.object(feature, "_fetch_and_set_from_db") as fetch_fn: + assert feature.check_value(identifier=1, default=100) == 30 + fetch_fn.assert_not_called() + assert not hasattr(feature.__dict__, "_buckets") + + feature = Feature("str_feature") + with patch.object(feature, "_fetch_and_set_from_db") as fetch_fn: + assert feature.check_value(identifier=1, default="bar") == "foo" + fetch_fn.assert_not_called() + assert not hasattr(feature.__dict__, "_buckets") + + feature = Feature("null_feature") + with patch.object(feature, "_fetch_and_set_from_db") as fetch_fn: + assert feature.check_value(identifier=1, default=100) is None + fetch_fn.assert_not_called() + assert not hasattr(feature.__dict__, "_buckets") + + @patch.dict(os.environ, {"CODECOV__FEATURE__NUM_FEATURE": "30"}) + @patch.dict(os.environ, {"CODECOV__FEATURE__DISABLE": ""}) + def test_check_value_with_env_disable_and_env_override(self): + feature = Feature("num_feature") + with patch.object(feature, "_fetch_and_set_from_db") as fetch_fn: + assert feature.check_value(identifier=1, default=100) == 30 + fetch_fn.assert_not_called() + assert not hasattr(feature.__dict__, "_buckets") + + feature = Feature("other_feature") + with patch.object(feature, "_fetch_and_set_from_db") as fetch_fn: + assert feature.check_value(identifier=1, default=100) == 100 + fetch_fn.assert_not_called() + assert not hasattr(feature.__dict__, "_buckets") diff --git a/libs/shared/tests/unit/test_router.py b/libs/shared/tests/unit/test_router.py new file mode 100644 index 0000000000..9f82283d45 --- /dev/null +++ b/libs/shared/tests/unit/test_router.py @@ -0,0 +1,73 @@ +from unittest.mock import patch + +import pytest + +from shared.celery_config import timeseries_backfill_task_name, upload_task_name +from shared.celery_router import route_tasks_based_on_user_plan +from shared.plan.constants import DEFAULT_FREE_PLAN, PlanName +from tests.helper import mock_all_plans_and_tiers + + +class TestCeleryRouter: + @pytest.mark.django_db + def test_route_tasks_based_on_user_plan_defaults(self): + mock_all_plans_and_tiers() + assert route_tasks_based_on_user_plan(upload_task_name, DEFAULT_FREE_PLAN) == { + "queue": "celery", + "extra_config": {}, + } + assert route_tasks_based_on_user_plan( + upload_task_name, PlanName.ENTERPRISE_CLOUD_MONTHLY.value + ) == {"queue": "enterprise_celery", "extra_config": {}} + assert route_tasks_based_on_user_plan("misterious_task", DEFAULT_FREE_PLAN) == { + "queue": "celery", + "extra_config": {}, + } + assert route_tasks_based_on_user_plan( + "misterious_task", PlanName.ENTERPRISE_CLOUD_MONTHLY.value + ) == {"queue": "enterprise_celery", "extra_config": {}} + + @pytest.mark.django_db + def test_route_tasks_with_config(self, mock_configuration): + mock_all_plans_and_tiers() + mock_configuration._params["setup"] = { + "tasks": { + "celery": { + "enterprise": {"soft_timelimit": 100, "hard_timelimit": 200} + }, + "timeseries": { + "enterprise": {"soft_timelimit": 400, "hard_timelimit": 500} + }, + } + } + assert route_tasks_based_on_user_plan(upload_task_name, DEFAULT_FREE_PLAN) == { + "queue": "celery", + "extra_config": {}, + } + assert route_tasks_based_on_user_plan( + upload_task_name, PlanName.ENTERPRISE_CLOUD_MONTHLY.value + ) == { + "queue": "enterprise_celery", + "extra_config": {"soft_timelimit": 100, "hard_timelimit": 200}, + } + assert route_tasks_based_on_user_plan( + timeseries_backfill_task_name, PlanName.ENTERPRISE_CLOUD_MONTHLY.value + ) == { + "queue": "enterprise_celery", + "extra_config": {"soft_timelimit": 400, "hard_timelimit": 500}, + } + + @patch.dict( + "shared.celery_config.BaseCeleryConfig.task_routes", + {"app.tasks.upload.*": {"queue": "uploads"}}, + ) + @pytest.mark.django_db + def test_route_tasks_with_glob_config(self, mocker): + mock_all_plans_and_tiers() + assert route_tasks_based_on_user_plan(upload_task_name, DEFAULT_FREE_PLAN) == { + "queue": "uploads", + "extra_config": {}, + } + assert route_tasks_based_on_user_plan( + upload_task_name, PlanName.ENTERPRISE_CLOUD_MONTHLY.value + ) == {"queue": "enterprise_uploads", "extra_config": {}} diff --git a/libs/shared/tests/unit/test_status.py b/libs/shared/tests/unit/test_status.py new file mode 100644 index 0000000000..d1fb10fb7d --- /dev/null +++ b/libs/shared/tests/unit/test_status.py @@ -0,0 +1,191 @@ +import pytest + +from shared.torngit.status import Status + + +@pytest.mark.parametrize( + "statuses, res, why", + [ + ( + [ + { + "url": None, + "state": "pending", + "context": "other", + "time": "2015-12-21T16:54:13Z", + } + ], + "pending", + "just one pending", + ), + ( + [ + { + "url": None, + "state": "pending", + "context": "other", + "time": "2015-12-21T16:54:13Z", + }, + { + "url": None, + "state": "failure", + "context": "ci", + "time": "2015-12-21T16:54:13Z", + }, + ], + "failure", + "other ci should be skipped", + ), + ( + [ + { + "url": None, + "state": "pending", + "context": "ci", + "time": "2015-12-21T16:55:13Z", + }, + { + "url": None, + "state": "failure", + "context": "ci", + "time": "2015-12-21T16:54:13Z", + }, + ], + "pending", + "newest prevails", + ), + ], +) +def test_str(statuses, res, why): + assert str(Status(statuses)) == res, why + + +@pytest.mark.parametrize("out, li", [("demo/*", 2), ("demo/a", 3), ("blah", 4)]) +def test_sub(out, li): + s = Status( + [ + { + "url": None, + "state": "pending", + "context": "demo/a", + "time": "2015-12-21T16:54:13Z", + }, + { + "url": None, + "state": "pending", + "context": "demo/b", + "time": "2015-12-21T16:54:13Z", + }, + { + "url": None, + "state": "pending", + "context": "ci", + "time": "2015-12-21T16:54:13Z", + }, + { + "url": None, + "state": "pending", + "context": "other", + "time": "2015-12-21T16:54:13Z", + }, + ] + ) + new = s - out + assert id(new) != id(s), "original should not be modified" + assert len(new) == li + assert out not in new + + +def test_status_with_none_time(): + s = Status( + [ + { + "url": None, + "state": "pending", + "context": "demo/a", + "time": "2015-12-21T16:54:13Z", + }, + {"url": None, "state": "pending", "context": "demo/b", "time": None}, + { + "url": None, + "state": "pending", + "context": "demo/b", + "time": "2015-12-21T16:54:13Z", + }, + { + "url": None, + "state": "pending", + "context": "ci", + "time": "2015-12-21T16:54:13Z", + }, + { + "url": None, + "state": "pending", + "context": "other", + "time": "2015-12-21T16:54:13Z", + }, + ] + ) + assert len(s) == 4 + new = s - "demo/*" + assert len(new) == 2 + + +def test_fetch_most_relevant_status_per_context(): + statuses = [ + { + "url": None, + "state": "pending", + "context": "demo/a", + "time": "2015-12-21T16:54:13Z", + }, + { + "url": None, + "state": "success", + "context": "demo/a", + "time": "2015-12-21T16:54:13Z", + }, + {"url": None, "state": "pending", "context": "demo/b", "time": None}, + { + "url": None, + "state": "pending", + "context": "demo/b", + "time": "2015-12-21T16:54:13Z", + }, + { + "url": None, + "state": "pending", + "context": "ci", + "time": "2015-12-21T16:54:13Z", + }, + { + "url": None, + "state": "success", + "context": "ci", + "time": "2010-12-21T16:54:13Z", + }, + {"url": None, "state": "pending", "context": "other", "time": None}, + ] + res = Status._fetch_most_relevant_status_per_context(statuses) + expected_res = [ + { + "context": "demo/a", + "state": "success", + "time": "2015-12-21T16:54:13Z", + "url": None, + }, + { + "context": "demo/b", + "state": "pending", + "time": "2015-12-21T16:54:13Z", + "url": None, + }, + { + "context": "ci", + "state": "pending", + "time": "2015-12-21T16:54:13Z", + "url": None, + }, + {"context": "other", "state": "pending", "time": None, "url": None}, + ] + assert expected_res == res diff --git a/libs/shared/tests/unit/torngit/test_base.py b/libs/shared/tests/unit/torngit/test_base.py new file mode 100644 index 0000000000..6fed649ad8 --- /dev/null +++ b/libs/shared/tests/unit/torngit/test_base.py @@ -0,0 +1,126 @@ +import textwrap + +import pytest + +from shared.torngit.base import TokenType, TorngitBaseAdapter + + +class TestTorngitBaseAdapter(object): + def test_get_token_by_type(self): + instance = TorngitBaseAdapter( + token={"key": "token"}, + token_type_mapping={ + TokenType.read: {"key": "read"}, + TokenType.admin: {"key": "admin"}, + TokenType.comment: {"key": "comment"}, + TokenType.status: {"key": "status"}, + }, + ) + assert instance.get_token_by_type(TokenType.read) == {"key": "read"} + assert instance.get_token_by_type(TokenType.admin) == {"key": "admin"} + assert instance.get_token_by_type(TokenType.comment) == {"key": "comment"} + assert instance.get_token_by_type(TokenType.status) == {"key": "status"} + + def test_get_token_by_type_no_mapping(self): + instance = TorngitBaseAdapter(token={"key": "token"}) + assert instance.get_token_by_type(TokenType.read) == {"key": "token"} + assert instance.get_token_by_type(TokenType.admin) == {"key": "token"} + assert instance.get_token_by_type(TokenType.comment) == {"key": "token"} + assert instance.get_token_by_type(TokenType.status) == {"key": "token"} + + def test_get_token_some_mapping(self): + instance = TorngitBaseAdapter( + token={"key": "token"}, + token_type_mapping={ + TokenType.read: {"key": "read"}, + TokenType.admin: {"key": "admin"}, + TokenType.status: None, + }, + ) + assert instance.get_token_by_type(TokenType.read) == {"key": "read"} + assert instance.get_token_by_type(TokenType.admin) == {"key": "admin"} + assert instance.get_token_by_type(TokenType.comment) == {"key": "token"} + assert instance.get_token_by_type(TokenType.status) == {"key": "token"} + + def test_no_token(self): + instance = TorngitBaseAdapter() + with pytest.raises(Exception) as exp: + instance.token + assert str(exp.value) == "Oauth consumer token not present" + + def test_diff_to_json_no_newline(self): + instance = TorngitBaseAdapter() + diff = textwrap.dedent( + """ + diff --git a/test/file.txt b/test/file.txt + index 8695aedf2b..e0d2b1e89d 100644 + --- a/test/file.txt + +++ b/test/file.txt + @@ -1 +1 @@ + -before + \\ No newline at end of file + +after + \\ No newline at end of file + """ + ) + + res = instance.diff_to_json(diff) + assert res == { + "files": { + "test/file.txt": { + "before": None, + "segments": [ + { + "header": ["1", "", "1", ""], + "lines": ["-before", "+after"], + } + ], + "stats": { + "added": 1, + "removed": 1, + }, + "type": "modified", + } + } + } + + def test_diff_to_json_unicode_line_separator(self): + instance = TorngitBaseAdapter() + diff = textwrap.dedent( + """ + diff --git a/test/file.txt b/test/file.txt + index 8695aedf2b..e0d2b1e89d 100644 + --- a/test/file.txt + +++ b/test/file.txt + @@ -2,3 +2,3 @@ + first line + -before\u2028 + +after\u2028 + last line + """ + ) + + res = instance.diff_to_json(diff) + assert res == { + "files": { + "test/file.txt": { + "before": None, + "segments": [ + { + "header": ["2", "3", "2", "3"], + "lines": [ + " first line", + "-before\u2028", + "+after\u2028", + " last line", + ], + } + ], + "stats": { + "added": 1, + "removed": 1, + }, + "type": "modified", + } + } + } diff --git a/libs/shared/tests/unit/torngit/test_bitbucket.py b/libs/shared/tests/unit/torngit/test_bitbucket.py new file mode 100644 index 0000000000..c60c4c441d --- /dev/null +++ b/libs/shared/tests/unit/torngit/test_bitbucket.py @@ -0,0 +1,548 @@ +from urllib.parse import parse_qsl, urlparse + +import httpx +import pytest +import respx + +from shared.torngit.bitbucket import Bitbucket +from shared.torngit.exceptions import ( + TorngitClientError, + TorngitObjectNotFoundError, + TorngitServer5xxCodeError, + TorngitServerUnreachableError, +) + + +@pytest.fixture +def respx_vcr(): + with respx.mock as v: + yield v + + +@pytest.fixture +def valid_handler(): + return Bitbucket( + repo=dict(name="example-python"), + owner=dict( + username="ThiagoCodecov", service_id="6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49" + ), + oauth_consumer_token=dict( + key="oauth_consumer_key_value", secret="oauth_consumer_token_secret_value" + ), + token=dict(secret="somesecret", key="somekey"), + ) + + +class TestUnitBitbucket(object): + @pytest.mark.asyncio + async def test_api_client_error_unreachable(self, valid_handler, mocker): + client = mocker.MagicMock( + request=mocker.AsyncMock(return_value=mocker.MagicMock(status_code=599)) + ) + method = "GET" + url = "random_url" + with pytest.raises(TorngitServerUnreachableError): + await valid_handler.api(client, "2", method, url) + + @pytest.mark.asyncio + async def test_api_client_error_connect_error(self, valid_handler, mocker): + client = mocker.MagicMock( + request=mocker.AsyncMock( + side_effect=httpx.ConnectError("message", request="request") + ) + ) + method = "GET" + url = "random_url" + with pytest.raises(TorngitServerUnreachableError): + await valid_handler.api(client, "2", method, url) + + @pytest.mark.asyncio + async def test_api_client_error_server_error(self, valid_handler, mocker): + client = mocker.MagicMock( + request=mocker.AsyncMock(return_value=mocker.MagicMock(status_code=503)) + ) + method = "GET" + url = "random_url" + with pytest.raises(TorngitServer5xxCodeError): + await valid_handler.api(client, "2", method, url) + + @pytest.mark.asyncio + async def test_api_client_error_client_error(self, valid_handler, mocker): + client = mocker.MagicMock( + request=mocker.AsyncMock(return_value=mocker.MagicMock(status_code=404)) + ) + method = "GET" + url = "random_url" + with pytest.raises(TorngitClientError): + await valid_handler.api(client, "2", method, url) + + @pytest.mark.asyncio + async def test_api_client_proper_params(self, valid_handler, mocker): + client = mocker.MagicMock( + request=mocker.AsyncMock( + return_value=mocker.MagicMock(text="kowabunga", status_code=200) + ) + ) + method = "GET" + url = "/random_url" + res = await valid_handler.api(client, "2", method, url) + assert res == "kowabunga" + assert client.request.call_count == 1 + args, kwargs = client.request.call_args + assert len(args) == 2 + assert args[0] == "GET" + built_url = args[1] + parsed_url = urlparse(built_url) + assert parsed_url.scheme == "https" + assert parsed_url.netloc == "bitbucket.org" + assert parsed_url.path == "/api/2.0/random_url" + assert parsed_url.params == "" + assert parsed_url.fragment == "" + query = dict(parse_qsl(parsed_url.query, keep_blank_values=True)) + assert sorted(query.keys()) == [ + "oauth_consumer_key", + "oauth_nonce", + "oauth_signature", + "oauth_signature_method", + "oauth_timestamp", + "oauth_token", + "oauth_version", + ] + assert ( + query["oauth_consumer_key"] == "oauth_consumer_key_value" + ) # defined on `valid_handler` + assert query["oauth_signature_method"] == "HMAC-SHA1" + assert query["oauth_token"] == "somekey" # defined on `valid_handler` + assert query["oauth_version"] == "1.0" # our class uses + + def test_generate_request_token(self, valid_handler): + with respx.mock: + my_route = respx.get( + "https://bitbucket.org/api/1.0/oauth/request_token" + ).mock( + return_value=httpx.Response( + status_code=200, + content="oauth_token_secret=test7f35jt40fnbz5xanwn9tlsi5ci10&oauth_token=testh3xen5q215b9ex&oauth_callback_confirmed=true", + ) + ) + v = valid_handler.generate_request_token("127.0.0.1/bb") + assert v == { + "oauth_token": "testh3xen5q215b9ex", + "oauth_token_secret": "test7f35jt40fnbz5xanwn9tlsi5ci10", + } + assert my_route.call_count == 1 + + def test_generate_access_token(self, valid_handler): + with respx.mock: + my_route = respx.get( + "https://bitbucket.org/api/1.0/oauth/access_token" + ).mock( + return_value=httpx.Response( + status_code=200, + content="oauth_token_secret=test3j3wxslwkw2j27ncbntpcwq50kzh&oauth_token=testss3hxhcfqf1h6g", + ) + ) + cookie_key, cookie_secret, oauth_verifier = ( + "rz5RKUeSbag6eeGrYj", + "WG8RYGfhMggdj6aKVhHq4qtSUJq4paDX", + "7403692316", + ) + v = valid_handler.generate_access_token( + cookie_key, cookie_secret, oauth_verifier + ) + assert v == { + "key": "testss3hxhcfqf1h6g", + "secret": "test3j3wxslwkw2j27ncbntpcwq50kzh", + } + assert my_route.call_count == 1 + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "permission_name, expected_result", + [("read", (True, False)), ("write", (True, True)), ("admin", (True, True))], + ) + async def test_get_authenticated_private_200_status_some_permissions( + self, mocker, respx_vcr, permission_name, expected_result + ): + respx.get( + "https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python" + ).respond(status_code=200, json={}) + respx.get( + "https://bitbucket.org/api/2.0/user/permissions/repositories" + ).respond( + status_code=200, + json={ + "pagelen": 10, + "values": [ + { + "type": "repository_permission", + "user": { + "display_name": "Thiago Ramos", + "uuid": "{9a01f37b-b1b2-40c5-8c5e-1a39f4b5e645}", + "links": {}, + "nickname": "thiago", + "type": "user", + "account_id": "5bce04c759d0e84f8c7555e9", + }, + "repository": { + "links": {}, + "type": "repository", + "name": "example-python", + "full_name": "ThiagoCodecov/example-python", + "uuid": "{a8c50527-2c3a-480e-afe1-7700e2b00074}", + }, + "permission": permission_name, + } + ], + "page": 1, + }, + ) + handler = Bitbucket( + repo=dict(name="example-python", private=True), + owner=dict( + username="ThiagoCodecov", + service_id="6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49", + ), + oauth_consumer_token=dict( + key="oauth_consumer_key_value", + secret="oauth_consumer_token_secret_value", + ), + token=dict(secret="somesecret", key="somekey"), + ) + res = await handler.get_authenticated() + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_authenticated_private_200_status_no_permission( + self, mocker, respx_vcr + ): + respx.get( + "https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python" + ).respond(status_code=200, json={}) + respx.get( + "https://bitbucket.org/api/2.0/user/permissions/repositories" + ).respond(status_code=200, json={"pagelen": 10, "values": [], "page": 1}) + handler = Bitbucket( + repo=dict(name="example-python", private=True), + owner=dict( + username="ThiagoCodecov", + service_id="6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49", + ), + oauth_consumer_token=dict( + key="oauth_consumer_key_value", + secret="oauth_consumer_token_secret_value", + ), + token=dict(secret="somesecret", key="somekey"), + ) + res = await handler.get_authenticated() + assert res == (True, False) + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "permission_name, expected_result", + [("read", (True, False)), ("write", (True, True)), ("admin", (True, True))], + ) + async def test_get_authenticated_private_404_status( + self, mocker, respx_vcr, permission_name, expected_result + ): + respx.get( + "https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python" + ).respond(status_code=404, json={}) + handler = Bitbucket( + repo=dict(name="example-python", private=True), + owner=dict( + username="ThiagoCodecov", + service_id="6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49", + ), + oauth_consumer_token=dict( + key="oauth_consumer_key_value", + secret="oauth_consumer_token_secret_value", + ), + token=dict(secret="secret", key="key"), + ) + with pytest.raises(TorngitClientError): + await handler.get_authenticated() + + @pytest.mark.asyncio + async def test_list_repos_exception_mid_call(self, valid_handler, respx_vcr): + respx.get("https://bitbucket.org/api/2.0/user/permissions/workspaces").respond( + status_code=200, + json={ + "values": [ + { + "workspace": { + "name": "banana", + "uuid": "[uuid]", + "slug": "specialslug", + } + }, + { + "workspace": { + "name": "apple", + "uuid": "[abcdef]", + "slug": "anotherslug", + } + }, + ] + }, + ) + respx.get( + "https://bitbucket.org/api/2.0/user/permissions/repositories" + ).respond( + status_code=200, + json={ + "values": [ + { + "repository": { + "full_name": "codecov/worker", + "owner": {"username": "differentone"}, + } + } + ] + }, + ) + respx.get("https://bitbucket.org/api/2.0/repositories/specialslug").respond( + status_code=200, json={"values": []} + ) + respx.get("https://bitbucket.org/api/2.0/repositories/ThiagoCodecov").respond( + status_code=200, json={"values": []} + ) + respx.get("https://bitbucket.org/api/2.0/repositories/anotherslug").respond( + status_code=200, + json={ + "values": [ + { + "is_private": True, + "language": "python", + "uuid": "[haja]", + "full_name": "anotherslug/aaaa", + "owner": {"uuid": "[poilk]"}, + }, + { + "is_private": True, + "language": "python", + "uuid": "[haja]", + "full_name": "anotherslug/qwerty", + "owner": {"uuid": "[poilk]"}, + }, + ] + }, + ) + respx.get("https://bitbucket.org/api/2.0/repositories/codecov").respond( + status_code=404, json={"values": []} + ) + res = await valid_handler.list_repos() + assert res == [ + { + "owner": {"service_id": "poilk", "username": "anotherslug"}, + "repo": { + "service_id": "haja", + "name": "aaaa", + "language": "python", + "private": True, + "branch": "main", + }, + }, + { + "owner": {"service_id": "poilk", "username": "anotherslug"}, + "repo": { + "service_id": "haja", + "name": "qwerty", + "language": "python", + "private": True, + "branch": "main", + }, + }, + ] + + @pytest.mark.asyncio + async def test_get_compare(self, valid_handler, respx_vcr): + diff = "\n".join( + [ + "diff --git a/README.md b/README.md", + "index 87f9baa..51c8a2d 100644", + "--- a/README.md", + "+++ b/README.md", + "@@ -14,4 +14,2 @@ Truces luctuque cognovit, cum lanam ordine vereri relinquunt sit munere quidam.", + " Solent **torvi clamare successit** ille memores rogum; serpens egi caelo,", + "-moventem gelido volucrum reddidit fatalia, *in*. Abdit instant et, et hostis", + "-amores, nec pater formosus mortis capiat eripui: ferarum extemplo. Inmeritas", + " favilla qui Dauno portis Aello! Fluit inde magis vinci hastam amore, mihi fama", + ] + ) + base, head = "6ae5f17", "b92edba" + respx.get( + "https://bitbucket.org/api/2.0/repositories/ThiagoCodecov/example-python/diff/b92edba..6ae5f17", + params__contains={"context": "1"}, + ).respond(status_code=200, content=diff, headers={"Content-Type": "aaaa"}) + expected_result = { + "diff": { + "files": { + "README.md": { + "type": "modified", + "before": None, + "segments": [ + { + "header": ["14", "4", "14", "2"], + "lines": [ + " Solent **torvi clamare successit** ille memores rogum; serpens egi caelo,", + "-moventem gelido volucrum reddidit fatalia, *in*. Abdit instant et, et hostis", + "-amores, nec pater formosus mortis capiat eripui: ferarum extemplo. Inmeritas", + " favilla qui Dauno portis Aello! Fluit inde magis vinci hastam amore, mihi fama", + ], + } + ], + "stats": {"added": 0, "removed": 2}, + } + } + }, + "commits": [{"commitid": "b92edba"}, {"commitid": "6ae5f17"}], + } + res = await valid_handler.get_compare(base, head) + assert sorted(list(res.keys())) == sorted(list(expected_result.keys())) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_distance_in_commits(self): + expected_result = { + "behind_by": None, + "behind_by_commit": None, + "status": None, + "ahead_by": None, + } + handler = Bitbucket( + repo=dict(name="example-python", private=True), + ) + res = await handler.get_distance_in_commits("branch", "commit") + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_repo_languages(self): + expected_result = ["javascript"] + handler = Bitbucket( + repo=dict(name="example-python", private=True), + ) + res = await handler.get_repo_languages(None, "JavaScript") + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_repo_no_languages(self): + expected_result = [] + handler = Bitbucket( + repo=dict(name="example-python", private=True), + ) + res = await handler.get_repo_languages(None, None) + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_pull_rquest_files(self, valid_handler): + handler = Bitbucket( + repo=dict(name="test-repo"), + owner=dict(username="e2e-org"), + token=dict(secret="somesecret", key="somekey"), + oauth_consumer_token=dict( + key="oauth_consumer_key_value", + secret="oauth_consumer_token_secret_value", + ), + ) + with respx.mock: + respx.get( + "https://bitbucket.org/api/2.0/repositories/e2e-org/test-repo/pullrequests/1/diffstat" + ).mock( + return_value=httpx.Response( + status_code=200, + json={ + "values": [ + { + "type": "diffstat", + "lines_added": 1, + "lines_removed": 1, + "status": "modified", + "old": { + "path": "README.md", + "type": "commit_file", + "escaped_path": "README.md", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/e2e-org/test-repo/src/d32434b65381acce9709e11234c0ba5ce2a9f515/README.md" + } + }, + }, + "new": { + "path": "README.md", + "type": "commit_file", + "escaped_path": "README.md", + "links": { + "self": { + "href": "https://bitbucket.org/!api/2.0/repositories/e2e-org/test-repo/src/8ed929e26c3e9dd51bb7abefc89f4f5044ff28fe/README.md" + } + }, + }, + } + ], + "pagelen": 500, + "size": 1, + "page": 1, + }, + ) + ) + v = await handler.get_pull_request_files("1") + assert v == [ + "README.md", + ] + + @pytest.mark.asyncio + async def test_get_pull_request_files_404(self): + handler = Bitbucket( + repo=dict(name="test-repo"), + owner=dict(username="e2e-org"), + token=dict(secret="somesecret", key="somekey"), + oauth_consumer_token=dict( + key="oauth_consumer_key_value", + secret="oauth_consumer_token_secret_value", + ), + ) + with respx.mock: + respx.get( + "https://bitbucket.org/api/2.0/repositories/e2e-org/test-repo/pullrequests/4/diffstat" + ).mock( + return_value=httpx.Response( + status_code=404, + headers={ + "X-RateLimit-Remaining": "0", + "X-RateLimit-Reset": "1350085394", + }, + ) + ) + with pytest.raises(TorngitObjectNotFoundError) as excinfo: + await handler.get_pull_request_files(4) + assert excinfo.value.code == 404 + assert excinfo.value.message == "PR with id 4 does not exist" + + @pytest.mark.asyncio + async def test_get_pull_request_files_403(self): + handler = Bitbucket( + repo=dict(name="test-repo"), + owner=dict(username="e2e-org"), + token=dict(secret="somesecret", key="somekey"), + oauth_consumer_token=dict( + key="oauth_consumer_key_value", + secret="oauth_consumer_token_secret_value", + ), + ) + with respx.mock: + respx.get( + "https://bitbucket.org/api/2.0/repositories/e2e-org/test-repo/pullrequests/4/diffstat" + ).mock( + return_value=httpx.Response( + status_code=403, + headers={ + "X-RateLimit-Remaining": "0", + "X-RateLimit-Reset": "1350085394", + }, + ) + ) + with pytest.raises(TorngitClientError) as excinfo: + await handler.get_pull_request_files(4) + assert excinfo.value.code == 403 + assert excinfo.value.message == "Bitbucket API: Forbidden" diff --git a/libs/shared/tests/unit/torngit/test_bitbucket_server.py b/libs/shared/tests/unit/torngit/test_bitbucket_server.py new file mode 100644 index 0000000000..bc9324cecb --- /dev/null +++ b/libs/shared/tests/unit/torngit/test_bitbucket_server.py @@ -0,0 +1,81 @@ +import pytest + +from shared.torngit.bitbucket_server import BitbucketServer +from shared.torngit.exceptions import ( + TorngitClientGeneralError, + TorngitObjectNotFoundError, +) + +MOCK_BASE = "https://bitbucketserver.codecov.dev" + + +@pytest.fixture +def valid_handler(mock_configuration): + mock_configuration._params["bitbucket_server"] = {"url": MOCK_BASE} + return BitbucketServer( + repo=dict(name="example-python"), + owner=dict( + username="ThiagoCodecov", service_id="6ef29b63-aaaa-aaaa-aaaa-aaaa03f5cd49" + ), + oauth_consumer_token=dict( + key="arubajamaicaohiwan", secret="natakeyoubermudabahamacomeonpret" + ), + token=dict(secret="KeyLargoMontegobabywhydontwego", key="waydowntokokomo"), + ) + + +class TestBitbucketServer(object): + def test_service_url(self, mock_configuration): + mock_configuration._params["bitbucket_server"] = { + "url": "https://bitbucketserver.codecov.dev" + } + bbs = BitbucketServer() + assert bbs.service_url == "https://bitbucketserver.codecov.dev" + assert ( + BitbucketServer.get_service_url() == "https://bitbucketserver.codecov.dev" + ) + + @pytest.mark.asyncio + @pytest.mark.respx(base_url=MOCK_BASE) + async def test_fetch_uses_proper_endpoint(self, valid_handler, respx_mock): + respx_mock.post( + "/rest/api/1.0/projects/THIAGOCODECOV/repos/example-python/pull-requests/pullid/comments" + ).respond(status_code=201, json={"id": 198, "version": 3}) + + res = await valid_handler.post_comment("pullid", "body") + assert res == {"id": "198:3"} + + @pytest.mark.asyncio + async def test_api_client_not_found(self, valid_handler, respx_mock): + respx_mock.get("/rest/api/1.0/random_url").respond(status_code=404, json={}) + + with pytest.raises(TorngitClientGeneralError): + await valid_handler.api("GET", "/random_url") + + @pytest.mark.asyncio + async def test_get_repo_languages(self): + expected_result = ["javascript"] + handler = BitbucketServer( + repo=dict(name="example-python", private=True), + ) + res = await handler.get_repo_languages(None, "JavaScript") + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_repo_no_languages(self): + expected_result = [] + handler = BitbucketServer( + repo=dict(name="example-python", private=True), + ) + res = await handler.get_repo_languages(None, None) + assert res == expected_result + + @pytest.mark.asyncio + @pytest.mark.respx(base_url=MOCK_BASE) + async def test_get_source_object_not_found(self, valid_handler, respx_mock): + respx_mock.get( + "/rest/api/1.0/projects/THIAGOCODECOV/repos/example-python/browse/some/path/" + ).respond(status_code=404, json={}) + + with pytest.raises(TorngitObjectNotFoundError): + await valid_handler.get_source("some/path/", "commitsha") diff --git a/libs/shared/tests/unit/torngit/test_exceptions.py b/libs/shared/tests/unit/torngit/test_exceptions.py new file mode 100644 index 0000000000..1637e9e136 --- /dev/null +++ b/libs/shared/tests/unit/torngit/test_exceptions.py @@ -0,0 +1,58 @@ +import pickle + +from shared.torngit.exceptions import ( + TorngitClientGeneralError, + TorngitObjectNotFoundError, + TorngitRateLimitError, + TorngitRepoNotFoundError, + TorngitUnauthorizedError, +) + + +def test_pickle_torngitclientgeneralerror(): + status_code, response, message = 400, "response", "message" + error = TorngitClientGeneralError( + status_code, response_data=response, message=message + ) + text = pickle.dumps(error) + renegerated_error = pickle.loads(text) + assert isinstance(renegerated_error, TorngitClientGeneralError) + assert renegerated_error.code == error.code + + +def test_pickle_torngitreponotfounderror(): + response, message = "response", "message" + error = TorngitRepoNotFoundError(response_data=response, message=message) + text = pickle.dumps(error) + renegerated_error = pickle.loads(text) + assert isinstance(renegerated_error, TorngitRepoNotFoundError) + assert renegerated_error.code == error.code + + +def test_pickle_torngitobjectnotfounderror(): + response, message = "response", "message" + error = TorngitObjectNotFoundError(response_data=response, message=message) + text = pickle.dumps(error) + renegerated_error = pickle.loads(text) + assert isinstance(renegerated_error, TorngitObjectNotFoundError) + assert renegerated_error.code == error.code + + +def test_pickle_torngitratelimiterror(): + reset_time, response, message = 10000000, "response", "message" + error = TorngitRateLimitError( + response_data=response, message=message, reset=reset_time + ) + text = pickle.dumps(error) + renegerated_error = pickle.loads(text) + assert isinstance(renegerated_error, TorngitRateLimitError) + assert renegerated_error.code == error.code + + +def test_pickle_torngitunauthorizederror(): + response, message = "response", "message" + error = TorngitUnauthorizedError(response_data=response, message=message) + text = pickle.dumps(error) + renegerated_error = pickle.loads(text) + assert isinstance(renegerated_error, TorngitUnauthorizedError) + assert renegerated_error.code == error.code diff --git a/libs/shared/tests/unit/torngit/test_github.py b/libs/shared/tests/unit/torngit/test_github.py new file mode 100644 index 0000000000..007436ad39 --- /dev/null +++ b/libs/shared/tests/unit/torngit/test_github.py @@ -0,0 +1,2078 @@ +import asyncio +import datetime +import pickle +from typing import Dict +from urllib.parse import parse_qs, parse_qsl, urlparse + +import httpx +import pytest +import respx +from mock import MagicMock +from prometheus_client import REGISTRY + +from shared.torngit.base import TokenType +from shared.torngit.exceptions import ( + TorngitClientError, + TorngitClientGeneralError, + TorngitMisconfiguredCredentials, + TorngitObjectNotFoundError, + TorngitRateLimitError, + TorngitRefreshTokenFailedError, + TorngitServer5xxCodeError, + TorngitServerUnreachableError, + TorngitUnauthorizedError, +) +from shared.torngit.github import Github +from shared.torngit.github import log as gh_log +from shared.typings.torngit import GithubInstallationInfo + + +@pytest.fixture +def respx_vcr(): + with respx.mock as v: + yield v + + +@pytest.fixture +def valid_handler(): + return Github( + repo=dict(name="example-python"), + owner=dict(username="ThiagoCodecov"), + token=dict(key="some_key", refresh_token="refresh_token", installation_id=0), + oauth_consumer_token=dict( + key="client_id", + secret="client_secret", + ), + ) + + +@pytest.fixture +def ghapp_handler(): + return Github( + repo=dict(name="example-python", using_integration=True), + owner=dict(username="codecov-e2e", integration_id=10000), + token=dict(key="integration_token", installation_id=1111), + oauth_consumer_token=dict( + key="client_id", + secret="client_secret", + refresh_token="refresh_token", + ), + ) + + +# Github needs a refresh_token callback function to try and refresh the token +# so we can add valid_handler._on_token_refresh = token_refresh_fake_callback +# to the tests that we want to see if Github refreshes the tokens +# other tests won't retry to refresh (cause the handlers dont have callbacks by default) +async def token_refresh_fake_callback(new_token: Dict) -> None: + pass + + +class TestUnitGithub(object): + @pytest.mark.asyncio + async def test_api_client_error_unreachable(self, valid_handler, mocker): + client = mocker.MagicMock( + request=mocker.AsyncMock(return_value=mocker.MagicMock(status_code=599)) + ) + mock_refresh = mocker.patch.object(Github, "refresh_token") + method = "GET" + url = "random_url" + with pytest.raises(TorngitServerUnreachableError): + await valid_handler.api(client, method, url) + assert mock_refresh.call_count == 0 + + @pytest.mark.asyncio + async def test_api_client_error_unauthorized(self, valid_handler, mocker): + client = mocker.MagicMock( + request=mocker.AsyncMock(return_value=mocker.MagicMock(status_code=401)) + ) + mock_refresh = mocker.patch.object(Github, "refresh_token") + valid_handler._on_token_refresh = token_refresh_fake_callback + method = "GET" + url = "https://api.github.com/some_endpoint" + assert callable(valid_handler._on_token_refresh) + with pytest.raises(TorngitUnauthorizedError): + await valid_handler.api(client, method, url) + assert mock_refresh.call_count == 1 + + @pytest.mark.parametrize( + "status_code, error_message", [(403, "Forbidden"), (429, "Too Many Requests")] + ) + @pytest.mark.asyncio + async def test_api_client_error_ratelimit_reached_429( + self, status_code, error_message + ): + with respx.mock: + respx.get("https://api.github.com/endpoint").mock( + return_value=httpx.Response( + status_code=status_code, + headers={ + "X-RateLimit-Remaining": "0", + "X-RateLimit-Reset": "1350085394", + }, + ) + ) + handler = Github( + repo=dict(name="aaaaa"), + owner=dict(username="aaaaa"), + token=dict(key="aaaaa"), + ) + with pytest.raises(TorngitRateLimitError) as excinfo: + async with handler.get_client() as client: + await handler.api(client, "get", "/endpoint") + assert excinfo.value.code == 403 + assert ( + excinfo.value.message == f"Github API rate limit error: {error_message}" + ) + assert excinfo.value.reset == "1350085394" + + @pytest.mark.parametrize("status_code", [403, 429]) + @pytest.mark.asyncio + async def test_api_client_error_ratelimit_missing_header(self, status_code): + with respx.mock: + respx.get("https://api.github.com/endpoint").mock( + return_value=httpx.Response( + status_code=status_code, + headers={ + "X-RateLimit-Remaining": "0", + "X-RateLimit-Reset": "1350085394", + }, + ) + ) + handler = Github( + repo=dict(name="aaaaa"), + owner=dict(username="aaaaa"), + token=dict(key="aaaaa"), + ) + with pytest.raises(TorngitClientError) as excinfo: + async with handler.get_client() as client: + await handler.api(client, "get", "/endpoint") + assert excinfo.value.code == 403 + + @pytest.mark.asyncio + async def test_api_client_error_server_error(self, valid_handler, mocker): + client = mocker.MagicMock( + request=mocker.AsyncMock(return_value=mocker.MagicMock(status_code=503)) + ) + method = "GET" + url = "random_url" + with pytest.raises(TorngitServer5xxCodeError): + await valid_handler.api(client, method, url) + + @pytest.mark.asyncio + async def test_api_client_error_client_error(self, valid_handler, mocker): + client = mocker.MagicMock( + request=mocker.AsyncMock(return_value=mocker.MagicMock(status_code=404)) + ) + method = "GET" + url = "random_url" + with pytest.raises(TorngitClientError): + await valid_handler.api(client, method, url) + + @pytest.mark.asyncio + async def test_torngit_server_unreachable_error(self, mocker, valid_handler): + client = mocker.MagicMock( + request=mocker.AsyncMock( + side_effect=[httpx.TimeoutException("message", request="request")] * 3, + ) + ) + with pytest.raises(TorngitServerUnreachableError): + await valid_handler.api( + client, "get", f"/repos/{valid_handler.slug}/branches", per_page=100 + ) + + @pytest.mark.asyncio + async def test_timeout_twice_then_success(self, mocker, valid_handler): + timeout_exception = httpx.TimeoutException("message", request="request") + success_response = mocker.MagicMock(text="kowabunga", status_code=200) + + client = mocker.MagicMock( + request=mocker.AsyncMock( + side_effect=[timeout_exception, timeout_exception, success_response], + ) + ) + + res = await valid_handler.api( + client, "get", f"/repos/{valid_handler.slug}/branches", per_page=100 + ) + assert res == "kowabunga" + + @pytest.mark.asyncio + async def test_api_client_query_params(self, valid_handler, mocker): + client = mocker.MagicMock( + request=mocker.AsyncMock( + return_value=mocker.MagicMock(text="kowabunga", status_code=200) + ) + ) + method = "GET" + url = "/random_url" + query_params = {"qparam1": "a param", "qparam2": "another param"} + res = await valid_handler.api(client, method, url, **query_params) + assert res == "kowabunga" + assert client.request.call_count == 1 + args, kwargs = client.request.call_args + assert len(args) == 2 + built_url = args[1] + parsed_url = urlparse(built_url) + assert parsed_url.scheme == "https" + assert parsed_url.netloc == "api.github.com" + assert parsed_url.path == url + assert parsed_url.params == "" + assert parsed_url.fragment == "" + query = dict(parse_qsl(parsed_url.query, keep_blank_values=True)) + assert query["qparam1"] == query_params["qparam1"] + assert query["qparam2"] == query_params["qparam2"] + + @pytest.mark.asyncio + async def test_api_client_change_api_host( + self, valid_handler, mocker, mock_configuration + ): + mock_host = "legit-github" + mock_configuration._params["github"] = { + "api_url": "https://" + mock_host, + "api_host_override": "api.github.com", + } + client = mocker.MagicMock( + request=mocker.AsyncMock( + return_value=mocker.MagicMock(text="kowabunga", status_code=200) + ) + ) + method = "GET" + url = "/random_url" + query_params = {"qparam1": "a param", "qparam2": "another param"} + res = await valid_handler.api(client, method, url, **query_params) + assert res == "kowabunga" + assert client.request.call_count == 1 + args, kwargs = client.request.call_args + assert kwargs.get("headers") is not None + assert kwargs.get("headers").get("Host") == "api.github.com" + assert len(args) == 2 + built_url = args[1] + parsed_url = urlparse(built_url) + assert parsed_url.scheme == "https" + assert parsed_url.netloc == mock_host + assert parsed_url.path == url + assert parsed_url.params == "" + assert parsed_url.fragment == "" + + @pytest.mark.asyncio + async def test_make_http_call_change_host( + self, valid_handler, mocker, mock_configuration + ): + mock_host = "legit-github" + mock_configuration._params["github"] = { + "url": "https://" + mock_host, + "host_override": "github.com", + } + client = mocker.MagicMock( + request=mocker.AsyncMock( + return_value=mocker.MagicMock(text="kowabunga", status_code=200) + ) + ) + method = "GET" + url = f"https://{mock_host}/random_url" + query_params = {"qparam1": "a param", "qparam2": "another param"} + await valid_handler.make_http_call(client, method, url, **query_params) + assert client.request.call_count == 1 + args, kwargs = client.request.call_args + assert kwargs.get("headers") is not None + assert kwargs.get("headers").get("Host") == "github.com" + assert len(args) == 2 + built_url = args[1] + parsed_url = urlparse(built_url) + assert parsed_url.scheme == "https" + assert parsed_url.netloc == mock_host + assert parsed_url.path == "/random_url" + assert parsed_url.params == "" + assert parsed_url.fragment == "" + + @pytest.mark.parametrize( + "handler, expected", + [ + pytest.param( + Github( + repo=dict(name="example-python"), + owner=dict(username="ThiagoCodecov"), + token=dict(key="some_key"), + ), + "f7CMr", + id="no_username_handler", + ), + pytest.param( + Github( + repo=dict(name="example-python"), + owner=dict(username="ThiagoCodecov"), + token=dict(key="some_key", username="Thiago"), + ), + "Thiago's token", + id="with_username_handler", + ), + pytest.param( + Github( + repo=dict(name="example-python"), + owner=dict(username="ThiagoCodecov"), + token=dict(key=None), + ), + "notoken", + id="no_token_handler", + ), + pytest.param( + Github( + repo=dict(), + owner=dict(username="ThiagoCodecov"), + token=dict(key="some_key"), + ), + "2vwGK", + id="no_repo_handler", + ), + pytest.param( + Github( + repo=dict(), + owner=dict(username="ThiagoCodecov"), + token=dict(key="some_key"), + installation=None, + ), + "2vwGK", + id="installation_None", + ), + pytest.param( + Github( + repo=dict(), + owner=dict(username="ThiagoCodecov"), + token=dict(key="some_key"), + installation={"installation_id": 1234}, + ), + "GitHub_installation_1234", + id="installation_handler", + ), + ], + ) + def test_loggable_token(self, handler, expected): + assert handler.loggable_token(handler.token) == expected + + @pytest.mark.asyncio + async def test_api_retries(self, valid_handler, mocker): + client = mocker.MagicMock( + request=mocker.AsyncMock( + side_effect=[ + mocker.MagicMock(text="NOTHERE", status_code=401), + mocker.MagicMock(text="FOUND", status_code=200), + ] + ) + ) + mock_refresh = mocker.patch.object(Github, "refresh_token") + valid_handler._on_token_refresh = token_refresh_fake_callback + method = "GET" + url = "https://api.github.com/some_endpoint" + res = await valid_handler.api(client, method, url, statuses_to_retry=[401]) + assert res == "FOUND" + assert mock_refresh.call_count == 1 + + @pytest.mark.asyncio + async def test_api_almost_too_many_retries(self, valid_handler, mocker): + client = mocker.MagicMock( + request=mocker.AsyncMock( + side_effect=[ + mocker.MagicMock(text="NOTHERE", status_code=401), + mocker.MagicMock(text="NOTHERE", status_code=401), + mocker.MagicMock(text="FOUND", status_code=200), + ] + ) + ) + mock_refresh = mocker.patch.object(Github, "refresh_token") + valid_handler._on_token_refresh = token_refresh_fake_callback + method = "GET" + url = "https://api.github.com/some_endpoint" + res = await valid_handler.api(client, method, url, statuses_to_retry=[401]) + assert res == "FOUND" + assert mock_refresh.call_count == 1 + + @pytest.mark.asyncio + async def test_api_too_many_retries(self, valid_handler, mocker): + client = mocker.MagicMock( + request=mocker.AsyncMock( + side_effect=[ + mocker.MagicMock(text="NOTHERE", status_code=401), + mocker.MagicMock(text="NOTHERE", status_code=401), + mocker.MagicMock(text="NOTHERE", status_code=401), + mocker.MagicMock(text="FOUND", status_code=200), + ] + ) + ) + mock_refresh = mocker.patch.object(Github, "refresh_token") + method = "GET" + url = "https://api.github.com/some_endpoint" + with pytest.raises(TorngitClientError): + await valid_handler.api(client, method, url, statuses_to_retry=[401]) + # Doesn't try to refresh because there's no on_token_refresh callback function + assert mock_refresh.call_count == 0 + + def test_get_token_by_type_if_none(self): + instance = Github( + token="token", + token_type_mapping={ + TokenType.read: "read", + TokenType.admin: "admin", + TokenType.comment: "comment", + TokenType.status: "status", + }, + ) + assert instance.get_token_by_type_if_none(None, TokenType.read) == "read" + assert instance.get_token_by_type_if_none(None, TokenType.admin) == "admin" + assert instance.get_token_by_type_if_none(None, TokenType.comment) == "comment" + assert instance.get_token_by_type_if_none(None, TokenType.status) == "status" + assert instance.get_token_by_type_if_none( + {"key": "token_set_user"}, TokenType.read + ) == {"key": "token_set_user"} + assert instance.get_token_by_type_if_none( + {"key": "token_set_user"}, TokenType.admin + ) == {"key": "token_set_user"} + assert instance.get_token_by_type_if_none( + {"key": "token_set_user"}, TokenType.comment + ) == {"key": "token_set_user"} + assert instance.get_token_by_type_if_none( + {"key": "token_set_user"}, TokenType.status + ) == {"key": "token_set_user"} + + @pytest.mark.asyncio + async def test_get_commit_diff_bad_encoding(self): + with respx.mock: + respx.get("https://api.github.com/endpoint").mock( + return_value=httpx.Response( + status_code=200, + content="\xc4pple".encode("latin-1"), + headers={ + "Content-Type": "application/vnd.github.v3.diff; charset=utf-8" + }, + ) + ) + handler = Github( + repo=dict(name="aaaaa"), + owner=dict(username="aaaaa"), + token=dict(key="aaaaa"), + ) + async with handler.get_client() as client: + res = await handler.api(client, "get", "/endpoint") + assert res == "\xc4pple".encode("latin-1").decode("utf-8", errors="replace") + + @pytest.mark.asyncio + async def test_find_pull_request_success(self, mocker): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "find_pull_request"}, + ) + handler = Github( + repo=dict(name="repo_name"), + owner=dict(username="username"), + token=dict(key="aaaaa"), + ) + commit_sha = "some_commit_sha" + mock_log = mocker.patch.object( + gh_log, "warning" + ) # Used to check if the log message is fired because there are 2 PRs with the commit + with respx.mock: + respx.get( + url=f"https://api.github.com/repos/{handler.slug}/commits/{commit_sha}/pulls" + ).mock( + return_value=httpx.Response( + status_code=200, + # Response for pulls endpoint returns a list directly + json=[ + { + "id": 575148805, + "node_id": "MDExOlB1bFkSZXF1ZXN0MzgzMzQ4Nzc1", + "number": 13, + "title": "feat/other-pr", + "labels": [], + "state": "closed", + "locked": True, + }, + { + "id": 575148804, + "node_id": "MDExOlB1bGxSZXF1ZXN0MzgzMzQ4Nzc1", + "number": 18, + "title": "Thiago/base no base", + "labels": [], + "state": "open", + "locked": False, + }, + { + "id": 575148804, + "node_id": "MDExOlB1bGxSZXF1ZXN0MzgzMzQ4Nzc1", + "number": 19, + "title": "Thiago/base no base", + "labels": [], + "state": "open", + "locked": False, + }, + { + "id": 575148805, + "node_id": "MDExOlB1bFkSZXF1ZXN0MzgzMzQ4Nzc1", + "number": 22, + "title": "feat/other-pr", + "labels": [], + "state": "closed", + "locked": True, + }, + ], + headers={"Content-Type": "application/json; charset=utf-8"}, + ) + ) + + res = await handler.find_pull_request(commit=commit_sha) + assert res == 18 + mock_log.assert_called_with( + "Commit is referenced in multiple PRs.", + extra=dict( + prs=[18, 19], + commit="some_commit_sha", + slug="username/repo_name", + state="open", + ), + ) + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "find_pull_request"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_find_pr_by_pulls_failfast_if_no_commit(self, mocker): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "find_pull_request"}, + ) + handler = Github( + repo=dict(name="repo_name"), + owner=dict(username="username"), + token=dict(key="aaaaa"), + ) + res = await handler.find_pull_request(commit=None) + assert res is None + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "find_pull_request"}, + ) + assert after - before == 0 + + @pytest.mark.asyncio + async def test_find_pr_by_pulls_failfast_if_no_slug(self, mocker): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "find_pull_request"}, + ) + handler = Github( + owner=dict(username="username"), + token=dict(key="aaaaa"), + ) + commit_sha = "some_commit_sha" + assert handler.slug is None + res = await handler.find_pull_request(None, commit_sha, None) + assert res is None + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "find_pull_request"}, + ) + assert after - before == 0 + + @pytest.mark.asyncio + async def test_find_pr_by_pulls_raise_exp_if_not_422(self, mocker): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "find_pull_request"}, + ) + handler = Github( + repo=dict(name="repo_name"), + owner=dict(username="username"), + token=dict(key="aaaaa"), + ) + commit_sha = "some_commit_sha" + with respx.mock: + respx.get( + url=f"https://api.github.com/repos/{handler.slug}/commits/{commit_sha}/pulls" + ).mock( + return_value=httpx.Response( + status_code=425, + # Response for pulls endpoint returns a list directly + json={"reason_phrase": "Some message"}, + headers={"Content-Type": "application/json; charset=utf-8"}, + ) + ) + token = handler.get_token_by_type(TokenType.read) + with pytest.raises(TorngitClientGeneralError): + await handler.find_pull_request(commit=commit_sha, token=token) + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "find_pull_request"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_distance_in_commits(self, mocker): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_distance_in_commits"}, + ) + handler = Github( + repo=dict(name="repo_name"), + owner=dict(username="username"), + token=dict(key="aaaaa"), + ) + base_commit_sha = "some_commit_sha" + repos_default_branch = "branch" + with respx.mock: + respx.get( + url=f"https://api.github.com/repos/{handler.slug}/compare/{repos_default_branch}...{base_commit_sha}" + ).mock( + return_value=httpx.Response( + status_code=200, + json={ + "status": "behind", + "ahead_by": 0, + "behind_by": 10, + "total_commits": 0, + "commits": [], + "files": [], + "base_commit": { + "sha": "c63a6b7c0dbc9e04a3bc8c109519615098325e41", + }, + }, + headers={"Content-Type": "application/json; charset=utf-8"}, + ) + ) + expected_result = { + "behind_by": 10, + "behind_by_commit": "c63a6b7c0dbc9e04a3bc8c109519615098325e41", + "status": "behind", + "ahead_by": 0, + } + res = await handler.get_distance_in_commits( + repos_default_branch, base_commit_sha + ) + assert res == expected_result + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_distance_in_commits"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_null_distance_in_commits(self, mocker): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_distance_in_commits"}, + ) + handler = Github( + repo=dict(name="repo_name"), + owner=dict(username="username"), + token=dict(key="aaaaa"), + ) + base_commit_sha = "some_commit_sha" + repos_default_branch = "branch" + with respx.mock: + respx.get( + url=f"https://api.github.com/repos/{handler.slug}/compare/{repos_default_branch}...{base_commit_sha}" + ).mock( + return_value=httpx.Response( + status_code=200, + json={ + "status": "behind", + "ahead_by": 0, + "behind_by": 0, + "total_commits": 0, + "commits": [], + "files": [], + }, + headers={"Content-Type": "application/json; charset=utf-8"}, + ) + ) + expected_result = { + "behind_by": None, + "behind_by_commit": None, + "status": "behind", + "ahead_by": 0, + } + res = await handler.get_distance_in_commits( + repos_default_branch, base_commit_sha + ) + assert res == expected_result + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_distance_in_commits"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_post_comment(self, respx_vcr, valid_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "post_comment"} + ) + mocked_response = respx_vcr.post( + url="https://api.github.com/repos/ThiagoCodecov/example-python/issues/1/comments", + json={"body": "Hello world"}, + ).mock( + return_value=httpx.Response( + status_code=201, + json={ + "url": "https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments/708550750", + "html_url": "https://github.com/ThiagoCodecov/example-python/pull/1#issuecomment-708550750", + "issue_url": "https://api.github.com/repos/ThiagoCodecov/example-python/issues/1", + "id": 708550750, + "node_id": "MDEyOklzc3VlQ29tbWVudDcwODU1MDc1MA==", + "user": { + "login": "ThiagoCodecov", + "id": 44379999, + "node_id": "MDQ6VXNlcjQ0Mzc2OTkx", + "avatar_url": "https://avatars1.githubusercontent.com/u/44379999?u=d50e43da66b2dbe47099d854ebd3b489f1162d48&v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ThiagoCodecov", + "html_url": "https://github.com/ThiagoCodecov", + "followers_url": "https://api.github.com/users/ThiagoCodecov/followers", + "following_url": "https://api.github.com/users/ThiagoCodecov/following{/other_user}", + "gists_url": "https://api.github.com/users/ThiagoCodecov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ThiagoCodecov/subscriptions", + "organizations_url": "https://api.github.com/users/ThiagoCodecov/orgs", + "repos_url": "https://api.github.com/users/ThiagoCodecov/repos", + "events_url": "https://api.github.com/users/ThiagoCodecov/events{/privacy}", + "received_events_url": "https://api.github.com/users/ThiagoCodecov/received_events", + "type": "User", + "site_admin": False, + }, + "created_at": "2020-10-14T17:32:01Z", + "updated_at": "2020-10-14T17:32:01Z", + "author_association": "OWNER", + "body": "Hello world", + "performed_via_github_app": None, + }, + headers={"Content-Type": "application/json; charset=utf-8"}, + ) + ) + expected_result = { + "url": "https://api.github.com/repos/ThiagoCodecov/example-python/issues/comments/708550750", + "html_url": "https://github.com/ThiagoCodecov/example-python/pull/1#issuecomment-708550750", + "issue_url": "https://api.github.com/repos/ThiagoCodecov/example-python/issues/1", + "id": 708550750, + "node_id": "MDEyOklzc3VlQ29tbWVudDcwODU1MDc1MA==", + "user": { + "login": "ThiagoCodecov", + "id": 44379999, + "node_id": "MDQ6VXNlcjQ0Mzc2OTkx", + "avatar_url": "https://avatars1.githubusercontent.com/u/44379999?u=d50e43da66b2dbe47099d854ebd3b489f1162d48&v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ThiagoCodecov", + "html_url": "https://github.com/ThiagoCodecov", + "followers_url": "https://api.github.com/users/ThiagoCodecov/followers", + "following_url": "https://api.github.com/users/ThiagoCodecov/following{/other_user}", + "gists_url": "https://api.github.com/users/ThiagoCodecov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ThiagoCodecov/subscriptions", + "organizations_url": "https://api.github.com/users/ThiagoCodecov/orgs", + "repos_url": "https://api.github.com/users/ThiagoCodecov/repos", + "events_url": "https://api.github.com/users/ThiagoCodecov/events{/privacy}", + "received_events_url": "https://api.github.com/users/ThiagoCodecov/received_events", + "type": "User", + "site_admin": False, + }, + "created_at": "2020-10-14T17:32:01Z", + "updated_at": "2020-10-14T17:32:01Z", + "author_association": "OWNER", + "body": "Hello world", + "performed_via_github_app": None, + } + res = await valid_handler.post_comment("1", "Hello world") + assert res == expected_result + assert mocked_response.called is True + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "post_comment"} + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_list_teams(self, valid_handler, respx_vcr): + before_first_call = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "list_teams"} + ) + before_second_call = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "list_teams_org_name"}, + ) + mocked_response = respx_vcr.get( + url="https://api.github.com/user/memberships/orgs?state=active&page=1" + ).respond( + status_code=200, + json=[ + { + "url": "https://api.github.com/orgs/codecov/memberships/ThiagoCodecov", + "state": "active", + "role": "member", + "organization_url": "https://api.github.com/orgs/codecov", + "user": { + "login": "ThiagoCodecov", + "id": 44379999, + "node_id": "MDQ6VXNlcjQ0Mzc2OTkx", + "avatar_url": "https://avatars3.githubusercontent.com/u/44379999?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ThiagoCodecov", + "html_url": "https://github.com/ThiagoCodecov", + "followers_url": "https://api.github.com/users/ThiagoCodecov/followers", + "following_url": "https://api.github.com/users/ThiagoCodecov/following{/other_user}", + "gists_url": "https://api.github.com/users/ThiagoCodecov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ThiagoCodecov/subscriptions", + "organizations_url": "https://api.github.com/users/ThiagoCodecov/orgs", + "repos_url": "https://api.github.com/users/ThiagoCodecov/repos", + "events_url": "https://api.github.com/users/ThiagoCodecov/events{/privacy}", + "received_events_url": "https://api.github.com/users/ThiagoCodecov/received_events", + "type": "User", + "site_admin": False, + }, + "organization": { + "login": "codecov", + "id": 8226999, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=", + "url": "https://api.github.com/orgs/codecov", + "repos_url": "https://api.github.com/orgs/codecov/repos", + "events_url": "https://api.github.com/orgs/codecov/events", + "hooks_url": "https://api.github.com/orgs/codecov/hooks", + "issues_url": "https://api.github.com/orgs/codecov/issues", + "members_url": "https://api.github.com/orgs/codecov/members{/member}", + "public_members_url": "https://api.github.com/orgs/codecov/public_members{/member}", + "avatar_url": "https://avatars3.githubusercontent.com/u/8226999?v=4", + "description": "Empower developers with tools to improve code quality and testing.", + }, + }, + { + "url": "https://api.github.com/orgs/ThiagoCodecovTeam/memberships/ThiagoCodecov", + "state": "active", + "role": "admin", + "organization_url": "https://api.github.com/orgs/ThiagoCodecovTeam", + "user": { + "login": "ThiagoCodecov", + "id": 44379999, + "node_id": "MDQ6VXNlcjQ0Mzc2OTkx", + "avatar_url": "https://avatars3.githubusercontent.com/u/44379999?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ThiagoCodecov", + "html_url": "https://github.com/ThiagoCodecov", + "followers_url": "https://api.github.com/users/ThiagoCodecov/followers", + "following_url": "https://api.github.com/users/ThiagoCodecov/following{/other_user}", + "gists_url": "https://api.github.com/users/ThiagoCodecov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ThiagoCodecov/subscriptions", + "organizations_url": "https://api.github.com/users/ThiagoCodecov/orgs", + "repos_url": "https://api.github.com/users/ThiagoCodecov/repos", + "events_url": "https://api.github.com/users/ThiagoCodecov/events{/privacy}", + "received_events_url": "https://api.github.com/users/ThiagoCodecov/received_events", + "type": "User", + "site_admin": False, + }, + "organization": { + "login": "ThiagoCodecovTeam", + "id": 57222756, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjU3MjIyNzU2", + "url": "https://api.github.com/orgs/ThiagoCodecovTeam", + "repos_url": "https://api.github.com/orgs/ThiagoCodecovTeam/repos", + "events_url": "https://api.github.com/orgs/ThiagoCodecovTeam/events", + "hooks_url": "https://api.github.com/orgs/ThiagoCodecovTeam/hooks", + "issues_url": "https://api.github.com/orgs/ThiagoCodecovTeam/issues", + "members_url": "https://api.github.com/orgs/ThiagoCodecovTeam/members{/member}", + "public_members_url": "https://api.github.com/orgs/ThiagoCodecovTeam/public_members{/member}", + "avatar_url": "https://avatars0.githubusercontent.com/u/57222756?v=4", + "description": False, + }, + }, + ], + headers={"Content-Type": "application/json; charset=utf-8"}, + ) + team_dicts = [ + ( + "https://api.github.com/users/codecov", + { + "login": "codecov", + "id": 8226999, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=", + "avatar_url": "https://avatars3.githubusercontent.com/u/8226999?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/codecov", + "html_url": "https://github.com/codecov", + "followers_url": "https://api.github.com/users/codecov/followers", + "following_url": "https://api.github.com/users/codecov/following{/other_user}", + "gists_url": "https://api.github.com/users/codecov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/codecov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/codecov/subscriptions", + "organizations_url": "https://api.github.com/users/codecov/orgs", + "repos_url": "https://api.github.com/users/codecov/repos", + "events_url": "https://api.github.com/users/codecov/events{/privacy}", + "received_events_url": "https://api.github.com/users/codecov/received_events", + "type": "Organization", + "site_admin": False, + "name": "Codecov", + "company": None, + "blog": "https://codecov.io/", + "location": None, + "email": "hello@codecov.io", + "hireable": None, + "bio": "Empower developers with tools to improve code quality and testing.", + "twitter_username": None, + "public_repos": 97, + "public_gists": 0, + "followers": 0, + "following": 0, + "created_at": "2014-07-21T16:22:31Z", + "updated_at": "2020-10-28T19:29:26Z", + }, + ), + ( + "https://api.github.com/users/ThiagoCodecovTeam", + { + "login": "ThiagoCodecovTeam", + "id": 57222756, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjU3MjIyNzU2", + "avatar_url": "https://avatars0.githubusercontent.com/u/57222756?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ThiagoCodecovTeam", + "html_url": "https://github.com/ThiagoCodecovTeam", + "followers_url": "https://api.github.com/users/ThiagoCodecovTeam/followers", + "following_url": "https://api.github.com/users/ThiagoCodecovTeam/following{/other_user}", + "gists_url": "https://api.github.com/users/ThiagoCodecovTeam/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ThiagoCodecovTeam/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ThiagoCodecovTeam/subscriptions", + "organizations_url": "https://api.github.com/users/ThiagoCodecovTeam/orgs", + "repos_url": "https://api.github.com/users/ThiagoCodecovTeam/repos", + "events_url": "https://api.github.com/users/ThiagoCodecovTeam/events{/privacy}", + "received_events_url": "https://api.github.com/users/ThiagoCodecovTeam/received_events", + "type": "Organization", + "site_admin": False, + "name": None, + "company": None, + "blog": "", + "location": None, + "email": None, + "hireable": None, + "bio": None, + "twitter_username": None, + "public_repos": 0, + "public_gists": 0, + "followers": 0, + "following": 0, + "created_at": "2019-10-31T13:07:24Z", + "updated_at": "2019-10-31T13:07:24Z", + }, + ), + ] + for url, data in team_dicts: + respx_vcr.get(url=url).respond( + status_code=200, + json=data, + headers={"Content-Type": "application/json; charset=utf-8"}, + ) + expected_result = [ + {"email": None, "id": "8226999", "name": "codecov", "username": "codecov"}, + { + "email": None, + "id": "57222756", + "name": "ThiagoCodecovTeam", + "username": "ThiagoCodecovTeam", + }, + ] + res = await valid_handler.list_teams() + assert res == expected_result + assert mocked_response.called is True + after_first_call = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "list_teams"} + ) + assert after_first_call - before_first_call == 1 + after_second_call = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "list_teams_org_name"}, + ) + assert after_second_call - before_second_call == 2 # len(mocked_response[json] + + @pytest.mark.asyncio + async def test_list_team_with_org_response_404(self, valid_handler, respx_vcr): + before_first_call = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "list_teams"} + ) + before_second_call = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "list_teams_org_name"}, + ) + mocked_response = respx_vcr.get( + url="https://api.github.com/user/memberships/orgs?state=active&page=1" + ).respond( + status_code=200, + json=[ + { + "url": "https://api.github.com/orgs/codecov/memberships/ThiagoCodecov", + "state": "active", + "role": "member", + "organization_url": "https://api.github.com/orgs/codecov", + "user": { + "login": "ThiagoCodecov", + "id": 44379999, + "node_id": "MDQ6VXNlcjQ0Mzc2OTkx", + "avatar_url": "https://avatars3.githubusercontent.com/u/44379999?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ThiagoCodecov", + "html_url": "https://github.com/ThiagoCodecov", + "followers_url": "https://api.github.com/users/ThiagoCodecov/followers", + "following_url": "https://api.github.com/users/ThiagoCodecov/following{/other_user}", + "gists_url": "https://api.github.com/users/ThiagoCodecov/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ThiagoCodecov/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ThiagoCodecov/subscriptions", + "organizations_url": "https://api.github.com/users/ThiagoCodecov/orgs", + "repos_url": "https://api.github.com/users/ThiagoCodecov/repos", + "events_url": "https://api.github.com/users/ThiagoCodecov/events{/privacy}", + "received_events_url": "https://api.github.com/users/ThiagoCodecov/received_events", + "type": "User", + "site_admin": False, + }, + "organization": { + "login": "codecov", + "id": 8226999, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=", + "url": "https://api.github.com/orgs/codecov", + "repos_url": "https://api.github.com/orgs/codecov/repos", + "events_url": "https://api.github.com/orgs/codecov/events", + "hooks_url": "https://api.github.com/orgs/codecov/hooks", + "issues_url": "https://api.github.com/orgs/codecov/issues", + "members_url": "https://api.github.com/orgs/codecov/members{/member}", + "public_members_url": "https://api.github.com/orgs/codecov/public_members{/member}", + "avatar_url": "https://avatars3.githubusercontent.com/u/8226999?v=4", + "description": "Empower developers with tools to improve code quality and testing.", + }, + } + ], + headers={"Content-Type": "application/json; charset=utf-8"}, + ) + + respx_vcr.get(url="https://api.github.com/users/codecov").respond( + status_code=404, + json={}, + headers={"Content-Type": "application/json; charset=utf-8"}, + ) + res = await valid_handler.list_teams() + assert res == [] + assert mocked_response.called is True + after_first_call = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "list_teams"} + ) + assert after_first_call - before_first_call == 1 + after_second_call = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "list_teams_org_name"}, + ) + assert after_second_call - before_second_call == 1 # len(mocked_response[json] + + @pytest.mark.asyncio + async def test_update_check_run_no_url(self, valid_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "update_check_run"}, + ) + with respx.mock: + mocked_response = respx.patch( + url="https://api.github.com/repos/ThiagoCodecov/example-python/check-runs/1256232357", + json={"conclusion": "success", "status": "completed", "output": None}, + ).mock( + return_value=httpx.Response( + status_code=200, + # response doesn't matter here + json={}, + headers={"Content-Type": "application/json; charset=utf-8"}, + ) + ) + + await valid_handler.update_check_run(1256232357, "success") + + assert mocked_response.call_count == 1 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "update_check_run"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_update_check_run_url(self, valid_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "update_check_run"}, + ) + url = "https://app.codecov.io/gh/codecov/example-python/compare/1?src=pr" + with respx.mock: + mocked_response = respx.patch( + url="https://api.github.com/repos/ThiagoCodecov/example-python/check-runs/1256232357", + json={ + "conclusion": "success", + "status": "completed", + "output": None, + "details_url": "https://app.codecov.io/gh/codecov/example-python/compare/1?src=pr", + }, + ).mock( + return_value=httpx.Response( + status_code=200, + # response doesn't matter here + json={}, + headers={"Content-Type": "application/json; charset=utf-8"}, + ) + ) + await valid_handler.update_check_run(1256232357, "success", url=url) + assert mocked_response.call_count == 1 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "update_check_run"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_update_check_run_url_fallback(self, mocker): + mock_redis_conn = MagicMock(name="fake_redis") + mocker.patch( + "shared.torngit.github.get_redis_connection", return_value=mock_redis_conn + ) + mock_get_token = mocker.patch( + "shared.torngit.github.get_github_integration_token", + return_value="fallback_token", + ) + + handler = Github( + repo=dict(name="example-python"), + owner=dict(username="ThiagoCodecov"), + token=dict( + key="some_key", + refresh_token="refresh_token", + entity_name="default_app_1500", + ), + oauth_consumer_token=dict( + key="client_id", + secret="client_secret", + ), + installation=GithubInstallationInfo( + installation_id=1500, + ), + fallback_installations=[ + { + "installation_id": 12342, + "app_id": 1200, + "pem_path": "some_path", + "id": 20, + } + ], + ) + + url = "https://app.codecov.io/gh/codecov/example-python/compare/1?src=pr" + five_min_from_now = datetime.datetime.now( + datetime.timezone.utc + ) + datetime.timedelta(minutes=5) + timestamp = int(five_min_from_now.timestamp()) + assert ( + timestamp - int(datetime.datetime.now(datetime.timezone.utc).timestamp()) + == 300 + ) + with respx.mock: + mocked_response = respx.patch( + "https://api.github.com/repos/ThiagoCodecov/example-python/check-runs/1256232357", + json={ + "conclusion": "success", + "status": "completed", + "output": None, + "details_url": "https://app.codecov.io/gh/codecov/example-python/compare/1?src=pr", + }, + headers__contains={"Authorization": "token some_key"}, + ).mock( + return_value=httpx.Response( + status_code=429, + headers={ + "X-RateLimit-Remaining": "0", + "X-RateLimit-Reset": str(timestamp), + }, + ) + ) + mocked_response_fallback = respx.patch( + url="https://api.github.com/repos/ThiagoCodecov/example-python/check-runs/1256232357", + json={ + "conclusion": "success", + "status": "completed", + "output": None, + "details_url": "https://app.codecov.io/gh/codecov/example-python/compare/1?src=pr", + }, + headers__contains={"Authorization": "token fallback_token"}, + ).mock( + return_value=httpx.Response( + status_code=200, + # response doesn't matter here + json={}, + headers={"Content-Type": "application/json; charset=utf-8"}, + ) + ) + await handler.update_check_run(1256232357, "success", url=url) + assert mocked_response.call_count == 1 + assert mocked_response_fallback.call_count == 1 + # The installation from the original token (rate limited) is marked so + mock_redis_conn.set.assert_called_with( + name="rate_limited_entity_default_app_1500", value=True, ex=300 + ) + mock_get_token.assert_called_with( + "github", 12342, app_id=1200, pem_path="some_path" + ) + # The token in the GitHub instance is updated for subsequent requests + assert handler._token == { + "key": "fallback_token", + } + assert handler.data["fallback_installations"] == [] + + @pytest.mark.asyncio + async def test_get_general_exception_pickle(self, valid_handler, mocker): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_pull_requests"}, + ) + mock_refresh = mocker.patch.object(Github, "refresh_token") + valid_handler._on_token_refresh = token_refresh_fake_callback + with respx.mock: + mocked_response = respx.get( + url="https://api.github.com/repos/ThiagoCodecov/example-python/pulls?page=1&per_page=25&state=open" + ).mock( + return_value=httpx.Response( + status_code=404, + # response doesn't matter here + json={}, + headers={"Content-Type": "application/json; charset=utf-8"}, + ) + ) + with pytest.raises(TorngitClientGeneralError) as ex: + await valid_handler.get_pull_requests() + error = ex.value + text = pickle.dumps(error) + renegerated_error = pickle.loads(text) + assert isinstance(renegerated_error, TorngitClientGeneralError) + assert renegerated_error.code == error.code + + assert mocked_response.call_count == 2 + assert mock_refresh.call_count == 1 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_pull_requests"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_api_no_token(self): + c = Github() + with pytest.raises(TorngitMisconfiguredCredentials): + await c.api() + + @pytest.mark.asyncio + async def test_paginated_api_no_token(self, mocker): + c = Github() + with pytest.raises(TorngitMisconfiguredCredentials): + async for page in c.paginated_api_generator( + mocker.MagicMock(), mocker.MagicMock(), mocker.MagicMock() + ): + pass + + @pytest.mark.asyncio + async def test_list_webhook_deliveries(self, ghapp_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "list_webhook_deliveries"}, + ) + + def side_effect(request): + assert request.headers.get("Accept") == "application/vnd.github+json" + assert ( + request.headers.get("Authorization") + == f"Bearer {ghapp_handler.token['key']}" + ) + + return httpx.Response( + status_code=200, + json=[ + { + "id": 17324040107, + "guid": "53c93580-7a6e-11ed-96c9-5e1ce3e5574e", # this value is the same accross redeliveries + "delivered_at": "2022-12-12T22:42:59Z", + "redelivery": False, + "duration": 0.37, + "status": "OK", + "status_code": 200, + "event": "installation_repositories", # when you add / remove repos from installation + "action": "added", + "installation_id": None, + "repository_id": None, + "url": "", + }, + { + "id": 17324018336, + "guid": "40d7f830-7a6e-11ed-8b90-0777e88b1858", + "delivered_at": "2022-12-12T22:42:30Z", + "redelivery": False, + "duration": 2.31, + "status": "OK", + "status_code": 200, + "event": "installation_repositories", + "action": "removed", + "installation_id": None, + "repository_id": None, + "url": "", + }, + { + "id": 17323292984, + "guid": "0498e8e0-7a6c-11ed-8834-c5eb5a4b102a", + "delivered_at": "2022-12-12T22:26:28Z", + "redelivery": False, + "duration": 0.69, + "status": "Invalid HTTP Response: 400", + "status_code": 400, + "event": "installation", # A new installation + "action": "created", + "installation_id": None, + "repository_id": None, + "url": "", + }, + { + "id": 17323228732, + "guid": "d41fa780-7a6b-11ed-8890-0619085a3f97", + "delivered_at": "2022-12-12T22:25:07Z", + "redelivery": False, + "duration": 0.74, + "status": "Invalid HTTP Response: 400", + "status_code": 400, + "event": "installation", + "action": "deleted", + "installation_id": None, + "repository_id": None, + "url": "", + }, + ], + headers={"Content-Type": "application/json; charset=utf-8"}, + ) + + with respx.mock: + mocked_response = respx.get( + url="https://api.github.com/app/hook/deliveries?per_page=50", + ).mock(side_effect=side_effect) + async for res in ghapp_handler.list_webhook_deliveries(): + assert len(res) == 4 + assert mocked_response.call_count == 1 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "list_webhook_deliveries"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_list_webhook_deliveries_multiple_pages(self, ghapp_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "list_webhook_deliveries"}, + ) + with respx.mock: + mocked_response_1 = respx.get( + url="https://api.github.com/app/hook/deliveries?per_page=50" + ).mock( + return_value=httpx.Response( + status_code=200, + headers=dict( + link='; rel="next"' + ), + json=[ + { + "id": 17324040107, + "guid": "53c93580-7a6e-11ed-96c9-5e1ce3e5574e", # this value is the same accross redeliveries + "delivered_at": "2022-12-12T22:42:59Z", + "redelivery": False, + "duration": 0.37, + "status": "OK", + "status_code": 200, + "event": "installation_repositories", # when you add / remove repos from installation + "action": "added", + "installation_id": None, + "repository_id": None, + "url": "", + }, + { + "id": 17324018336, + "guid": "40d7f830-7a6e-11ed-8b90-0777e88b1858", + "delivered_at": "2022-12-12T22:42:30Z", + "redelivery": False, + "duration": 2.31, + "status": "OK", + "status_code": 200, + "event": "installation_repositories", + "action": "removed", + "installation_id": None, + "repository_id": None, + "url": "", + }, + ], + ) + ) + mocked_response_2 = respx.get( + url="https://api.github.com/app/hook/deliveries?per_page=50&cursor=v1_17323292984" + ).mock( + return_value=httpx.Response( + status_code=200, + json=[ + { + "id": 17323292984, + "guid": "0498e8e0-7a6c-11ed-8834-c5eb5a4b102a", + "delivered_at": "2022-12-12T22:26:28Z", + "redelivery": False, + "duration": 0.69, + "status": "Invalid HTTP Response: 400", + "status_code": 400, + "event": "installation", # A new installation + "action": "created", + "installation_id": None, + "repository_id": None, + "url": "", + }, + { + "id": 17323228732, + "guid": "d41fa780-7a6b-11ed-8890-0619085a3f97", + "delivered_at": "2022-12-12T22:25:07Z", + "redelivery": False, + "duration": 0.74, + "status": "Invalid HTTP Response: 400", + "status_code": 400, + "event": "installation", + "action": "deleted", + "installation_id": None, + "repository_id": None, + "url": "", + }, + ], + ) + ) + aggregate_res = [] + async for res in ghapp_handler.list_webhook_deliveries(): + assert len(res) == 2 + aggregate_res += res + assert len(aggregate_res) == 4 + assert mocked_response_1.call_count == 1 + assert mocked_response_2.call_count == 1 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "list_webhook_deliveries"}, + ) + assert after - before == 2 + + @pytest.mark.asyncio + async def test_webhook_redelivery_success(self, ghapp_handler): + delivery_id = 17323228732 + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "request_webhook_redelivery"}, + ) + + def side_effect(request): + assert request.headers.get("Accept") == "application/vnd.github+json" + assert ( + request.headers.get("Authorization") + == f"Bearer {ghapp_handler.token['key']}" + ) + assert request.method == "POST" + return httpx.Response( + status_code=202, headers={"Content-Type": "applicaiton/json"} + ) + + with respx.mock: + mocked_response = respx.post( + url=f"https://api.github.com/app/hook/deliveries/{delivery_id}/attempts" + ).mock(side_effect=side_effect) + ans = await ghapp_handler.request_webhook_redelivery(delivery_id) + assert ans is True + assert mocked_response.call_count == 1 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "request_webhook_redelivery"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_webhook_redelivery_fail(self, ghapp_handler): + delivery_id = 17323228732 + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "request_webhook_redelivery"}, + ) + with respx.mock: + mocked_response = respx.post( + url=f"https://api.github.com/app/hook/deliveries/{delivery_id}/attempts" + ).mock( + return_value=httpx.Response( + status_code=422, headers={"Content-Type": "applicaiton/json"} + ), + ) + ans = await ghapp_handler.request_webhook_redelivery(delivery_id) + assert ans is False + assert mocked_response.call_count == 1 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "request_webhook_redelivery"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_get_pull_request_files_404(self, mocker): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_pull_request_files"}, + ) + mock_refresh = mocker.patch.object(Github, "refresh_token") + with respx.mock: + respx.get( + "https://api.github.com/repos/codecove2e/example-python/pulls/4/files" + ).mock( + return_value=httpx.Response( + status_code=404, + headers={ + "X-RateLimit-Remaining": "0", + "X-RateLimit-Reset": "1350085394", + }, + ) + ) + handler = Github( + repo=dict(name="example-python"), + owner=dict(username="codecove2e"), + token=dict(key=10 * "a280"), + ) + handler._on_token_refresh = token_refresh_fake_callback + with pytest.raises(TorngitObjectNotFoundError) as excinfo: + await handler.get_pull_request_files(4) + assert excinfo.value.code == 404 + assert excinfo.value.message == "PR with id 4 does not exist" + assert mock_refresh.call_count == 1 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_pull_request_files"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_get_pull_request_files_403(self): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_pull_request_files"}, + ) + with respx.mock: + respx.get( + "https://api.github.com/repos/codecove2e/example-python/pulls/4/files" + ).mock( + return_value=httpx.Response( + status_code=403, + headers={ + "X-RateLimit-Remaining": "0", + "X-RateLimit-Reset": "1350085394", + }, + ) + ) + handler = Github( + repo=dict(name="example-python"), + owner=dict(username="codecove2e"), + token=dict(key=10 * "a280"), + ) + with pytest.raises(TorngitClientError) as excinfo: + await handler.get_pull_request_files(4) + assert excinfo.value.code == 403 + assert excinfo.value.message == "Github API rate limit error: Forbidden" + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_pull_request_files"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_get_pull_request_files_403_secondary_limit(self): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_pull_request_files"}, + ) + with respx.mock: + respx.get( + "https://api.github.com/repos/codecove2e/example-python/pulls/4/files" + ).mock( + return_value=httpx.Response( + status_code=403, + headers={ + "Retry-After": "60", + }, + ) + ) + handler = Github( + repo=dict(name="example-python"), + owner=dict(username="codecove2e"), + token=dict(key=10 * "a280"), + ) + with pytest.raises(TorngitRateLimitError) as excinfo: + await handler.get_pull_request_files(4) + assert excinfo.value.code == 403 + assert ( + excinfo.value.message + == "Github API rate limit error: secondary rate limit" + ) + assert excinfo.value.retry_after == 60 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_pull_request_files"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_github_refresh_fail_terminates_unavailable( + self, mocker, valid_handler + ): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "refresh_token"} + ) + with pytest.raises(TorngitRefreshTokenFailedError) as exp: + with respx.mock: + mocked_refresh = respx.post( + "https://github.com/login/oauth/access_token" + ).mock( + return_value=httpx.Response( + status_code=502, content="Service unavailable try again later" + ) + ) + await valid_handler.refresh_token( + valid_handler.get_client(), "original_request_url" + ) + assert exp.code == 555 + assert mocked_refresh.call_count == 1 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "refresh_token"} + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_github_refresh_fail_terminates_unauthorized( + self, mocker, valid_handler + ): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "refresh_token"} + ) + with pytest.raises(TorngitRefreshTokenFailedError) as exp: + with respx.mock: + mocked_refresh = respx.post( + "https://github.com/login/oauth/access_token" + ).mock( + return_value=httpx.Response( + status_code=403, content='{"error": "unauthorized"}' + ) + ) + await valid_handler.refresh_token( + valid_handler.get_client(), "original_request_url" + ) + assert exp.code == 555 + assert mocked_refresh.call_count == 1 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "refresh_token"} + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_github_refresh_fail_terminates_no_refresh_token( + self, mocker, valid_handler + ): + old_token = valid_handler._token + valid_handler._token = {"access_token": "old_token_without_refresh"} + res = await valid_handler.refresh_token( + valid_handler.get_client(), "original_request_url" + ) + assert res is None + valid_handler._token = old_token + + @pytest.mark.asyncio + async def test_github_double_refresh(self, mocker, valid_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "refresh_token"} + ) + + def side_effect(request, *args, **kwargs): + url_parts = urlparse(str(request.url)) + query = url_parts.query + params = parse_qs(query) + refresh_token = params["refresh_token"][0] + if refresh_token == "refresh_token": + return httpx.Response( + status_code=200, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + text="access_token=new_access_token&token_type=bearer&refresh_token=new_refresh_token", + ) + elif refresh_token == "new_refresh_token": + return httpx.Response( + status_code=200, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + text="access_token=newer_access_token&token_type=bearer&refresh_token=newer_refresh_token", + ) + pytest.fail("Wrong token received") + + assert valid_handler._oauth == dict(key="client_id", secret="client_secret") + + with respx.mock: + mocked_refresh = respx.post( + "https://github.com/login/oauth/access_token" + ).mock(side_effect=side_effect) + await valid_handler.refresh_token( + valid_handler.get_client(), "original_request_url" + ) + assert mocked_refresh.call_count == 1 + assert valid_handler._token == dict( + key="new_access_token", refresh_token="new_refresh_token" + ) + + await valid_handler.refresh_token( + valid_handler.get_client(), "original_request_url" + ) + assert mocked_refresh.call_count == 2 + assert valid_handler._token == dict( + key="newer_access_token", refresh_token="newer_refresh_token" + ) + + # Make sure that changing the token doesn't change the _oauth + assert valid_handler._oauth == dict(key="client_id", secret="client_secret") + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "refresh_token"} + ) + assert after - before == 2 + + @pytest.mark.asyncio + async def test_github_is_student_timeout(self, ghapp_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "is_student"} + ) + + def side_effect(*args, **kwargs): + raise httpx.TimeoutException("timeout") + + with respx.mock: + mocked_route = respx.get("https://education.github.com/api/user").mock( + side_effect=side_effect + ) + res = await ghapp_handler.is_student() + assert mocked_route.call_count == 3 + assert res == False + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "is_student"} + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_github_is_student_network_error(self, ghapp_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "is_student"} + ) + + def side_effect(*args, **kwargs): + raise httpx.NetworkError("timeout") + + with respx.mock: + mocked_route = respx.get("https://education.github.com/api/user").mock( + side_effect=side_effect + ) + res = await ghapp_handler.is_student() + assert mocked_route.call_count == 3 + assert res == False + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "is_student"} + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_github_refresh_after_failed_request(self, mocker, valid_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "get_is_admin"} + ) + before_retries = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "make_http_call_retry"}, + ) + before_token_refresh = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "refresh_token"} + ) + + def side_effect(request, *args, **kwargs): + token = request.headers["Authorization"] + if token == "token some_key": + return httpx.Response( + status_code=401, + content='{"message":"Bad Request"}', + ) + elif token == "token new_access_token": + return httpx.Response( + status_code=200, json={"state": "active", "role": "admin"} + ) + pytest.fail(f"Wrong token received ({token})") + + f = asyncio.Future() + f.set_result(True) + mock_refresh_callback: MagicMock = mocker.patch.object( + valid_handler, "_on_token_refresh", create=True, return_value=f + ) + with respx.mock: + mocked_route = respx.get( + "https://api.github.com/orgs/ThiagoCodecov/memberships/John%20Doe" + ).mock(side_effect=side_effect) + mocked_refresh = respx.post( + "https://github.com/login/oauth/access_token" + ).mock( + return_value=httpx.Response( + status_code=200, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + text="access_token=new_access_token&token_type=bearer&refresh_token=new_refresh_token", + ) + ) + await valid_handler.get_is_admin(user={"username": "John Doe"}) + assert mocked_route.call_count == 2 + assert mocked_refresh.call_count == 1 + assert valid_handler._token["key"] == "new_access_token" + assert valid_handler._token["refresh_token"] == "new_refresh_token" + assert mock_refresh_callback.call_count == 1 + mock_refresh_callback.assert_called_with( + { + "key": "new_access_token", + "refresh_token": "new_refresh_token", + } + ) + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "get_is_admin"} + ) + assert after - before == 1 + after_retries = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "make_http_call_retry"}, + ) + assert after_retries - before_retries == 1 + after_token_refresh = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", labels={"endpoint": "refresh_token"} + ) + assert after_token_refresh - before_token_refresh == 1 + + @pytest.mark.asyncio + async def test__get_owner_from_nodeid(self, ghapp_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_owner_from_nodeid_graphql"}, + ) + with respx.mock: + mocked_route = respx.post("https://api.github.com/graphql").mock( + return_value=httpx.Response( + status_code=200, + headers={"Content-Type": "application/json"}, + json={ + "data": { + "node": { + "__typename": "Organization", + "name": "Codecov", + "login": "codecov", + "databaseId": 8226205, + }, + } + }, + ) + ) + node_id = "U_kgDOBfIxWg" + async with ghapp_handler.get_client() as client: + res = await ghapp_handler._get_owner_from_nodeid( + client=client, token=MagicMock(), owner_node_id=node_id + ) + assert res == {"username": "codecov", "service_id": 8226205} + assert mocked_route.call_count == 1 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_owner_from_nodeid_graphql"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + @pytest.mark.django_db(databases={"default"}) + async def test_get_repos_from_nodeids(self, ghapp_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_repos_from_nodeids_generator_graphql"}, + ) + before_get_owner = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_owner_from_nodeid_graphql"}, + ) + + # Mocking different responses from the graphQL API + # because it all goes to the same endpoint but this test expects a 2nd call + # with owner by node_id query + def mock_route_side_effect(request): + content_string = request.content.decode() + if "query GetReposFromNodeIds" in content_string: + return httpx.Response( + status_code=200, + json={ + "data": { + "nodes": [ + { + "__typename": "Repository", + "databaseId": 460565350, + "name": "codecov-cli", + "primaryLanguage": {"name": "Python"}, + "isPrivate": False, + "defaultBranchRef": {"name": "main"}, + "owner": { + "id": "MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=", + "login": "codecov", + }, + }, + None, + { + "__typename": "Repository", + "databaseId": 665728948, + "name": "worker", + "primaryLanguage": {"name": "Python"}, + "isPrivate": False, + "defaultBranchRef": {"name": "main"}, + "owner": { + "id": "MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=", + "login": "codecov", + }, + }, + { + "__typename": "Repository", + "databaseId": 553624697, + "name": "components-demo", + "primaryLanguage": {"name": "Python"}, + "isPrivate": False, + "defaultBranchRef": {"name": "main"}, + "owner": { + "id": "U_kgDOBfIxWg", + "login": "giovanni-guidini", + }, + }, + { + "__typename": "Organization", + "name": "Codecov", + "login": "codecov", + "databaseId": 8226205, + }, + { + "__typename": "User", + "login": "giovanni-guidini", + "databaseId": 99758426, + }, + ] + }, + "extensions": { + "warnings": [ + { + "type": "DEPRECATION", + "message": "The id MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU= is deprecated. Update your cache to use the next_global_id from the data payload.", + "data": { + "legacy_global_id": "MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=", + "next_global_id": "O_kgDOAH2FnQ", + }, + "link": "https://docs.github.com", + } + ] + }, + }, + ) + if "query GetOwnerFromNodeId" in content_string: + return httpx.Response( + status_code=200, + json={ + "data": { + "node": { + "__typename": "Organization", + "name": "Codecov", + "login": "codecov", + "databaseId": 8226205, + }, + } + }, + ) + raise Exception("Unexpected request") + + # Actual test + with respx.mock: + mocked_route = respx.post("https://api.github.com/graphql").mock( + side_effect=mock_route_side_effect + ) + node_ids = [ + "R_kgDOG3OrZg", # codecov/codecov-cli + "R_kgDOJ643tA", # codecov/worker + "R_kgDOIP-keQ", # giovanni-guidini/components-demo + "MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=", # codecov + "U_kgDOBfIxWg", # giovanni-guidini + ] + request = ghapp_handler.get_repos_from_nodeids_generator( + repo_node_ids=node_ids, expected_owner_username="giovanni-guidini" + ) + repos = [repo async for repo in request] + assert repos == [ + { + "branch": "main", + "language": "python", + "name": "codecov-cli", + "owner": { + "is_expected_owner": False, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=", + "service_id": 8226205, + "username": "codecov", + }, + "service_id": 460565350, + "private": False, + }, + { + "branch": "main", + "language": "python", + "name": "worker", + "owner": { + "is_expected_owner": False, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjgyMjYyMDU=", + "service_id": 8226205, + "username": "codecov", + }, + "service_id": 665728948, + "private": False, + }, + { + "branch": "main", + "language": "python", + "name": "components-demo", + "owner": { + "is_expected_owner": True, + "node_id": "U_kgDOBfIxWg", + "username": "giovanni-guidini", + }, + "service_id": 553624697, + "private": False, + }, + ] + assert mocked_route.call_count == 2 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_repos_from_nodeids_generator_graphql"}, + ) + assert after - before == 1 + after_get_owner = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "get_owner_from_nodeid_graphql"}, + ) + assert after_get_owner - before_get_owner == 1 + + @pytest.mark.asyncio + async def test_count_and_get_url_template(self, ghapp_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "make_http_call_retry"}, + ) + res = ghapp_handler.count_and_get_url_template(url_name="make_http_call_retry") + assert res == "" + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_total", + labels={"endpoint": "make_http_call_retry"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_count_and_get_url_template_unrecognized(self, ghapp_handler): + with pytest.raises(KeyError): + ghapp_handler.count_and_get_url_template(url_name="whoops") diff --git a/libs/shared/tests/unit/torngit/test_github_enterprise.py b/libs/shared/tests/unit/torngit/test_github_enterprise.py new file mode 100644 index 0000000000..dd8b8c78f6 --- /dev/null +++ b/libs/shared/tests/unit/torngit/test_github_enterprise.py @@ -0,0 +1,152 @@ +from urllib.parse import urlparse + +import pytest +from prometheus_client import REGISTRY + +from shared.torngit.github_enterprise import GithubEnterprise + + +class TestGithubEnterprise(object): + def test_urls_no_api_url_set(self, mock_configuration): + mock_configuration._params["github_enterprise"] = { + "url": "https://github-enterprise.codecov.dev" + } + gl = GithubEnterprise() + assert gl.service_url == "https://github-enterprise.codecov.dev" + assert gl.api_url == "https://github-enterprise.codecov.dev/api/v3" + assert ( + GithubEnterprise.get_service_url() + == "https://github-enterprise.codecov.dev" + ) + assert ( + GithubEnterprise.get_api_url() + == "https://github-enterprise.codecov.dev/api/v3" + ) + + def test_urls_with_api_url_set(self, mock_configuration): + mock_configuration._params["github_enterprise"] = { + "url": "https://github-enterprise.codecov.dev", + "api_url": "https://api.github.dev", + } + gl = GithubEnterprise() + assert gl.service_url == "https://github-enterprise.codecov.dev" + assert gl.api_url == "https://api.github.dev" + + @pytest.mark.asyncio + async def test_fetch_uses_proper_endpoint(self, mocker, mock_configuration): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_github_enterprise_total", + labels={"endpoint": "post_comment"}, + ) + client = mocker.MagicMock( + __aenter__=mocker.AsyncMock( + return_value=mocker.MagicMock( + request=mocker.AsyncMock( + return_value=mocker.MagicMock( + headers={"Content-Type": "application/json"}, + status_code=201, + json=mocker.MagicMock(return_value={}), + ) + ) + ) + ) + ) + mocker.patch.object(GithubEnterprise, "get_client", return_value=client) + mock_configuration._params["github_enterprise"] = { + "url": "https://github-enterprise.codecov.dev", + "api_url": "https://api.github.dev", + } + gl = GithubEnterprise( + repo=dict(service_id="187725", name="codecov-test"), + owner=dict(username="stevepeak", service_id="109479"), + token=dict(key="fake_token"), + ) + res = await gl.post_comment("pullid", "body") + assert res == {} + client.__aenter__.return_value.request.assert_called_with( + "POST", + "https://api.github.dev/repos/stevepeak/codecov-test/issues/pullid/comments", + json={"body": "body"}, + headers={ + "Accept": "application/json", + "Authorization": "token fake_token", + "User-Agent": "Default", + }, + follow_redirects=False, + ) + after = REGISTRY.get_sample_value( + "git_provider_api_calls_github_enterprise_total", + labels={"endpoint": "post_comment"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_api_client_change_api_host(self, mocker, mock_configuration): + mock_host = "legit-ghe" + mock_configuration._params["github_enterprise"] = { + "api_url": "https://" + mock_host, + "api_host_override": "api.ghe.com", + } + client = mocker.MagicMock( + request=mocker.AsyncMock( + return_value=mocker.MagicMock(text="kowabunga", status_code=200) + ) + ) + mocker.patch.object(GithubEnterprise, "get_client", return_value=client) + gl = GithubEnterprise( + repo=dict(service_id="187725", name="codecov-test"), + owner=dict(username="stevepeak", service_id="109479"), + token=dict(key="fake_token"), + ) + method = "GET" + url = "/random_url" + query_params = {"qparam1": "a param", "qparam2": "another param"} + res = await gl.api(client, method, url, **query_params) + assert res == "kowabunga" + assert client.request.call_count == 1 + args, kwargs = client.request.call_args + assert kwargs.get("headers") is not None + assert kwargs.get("headers").get("Host") == "api.ghe.com" + assert len(args) == 2 + built_url = args[1] + parsed_url = urlparse(built_url) + assert parsed_url.scheme == "https" + assert parsed_url.netloc == mock_host + assert parsed_url.path == url + assert parsed_url.params == "" + assert parsed_url.fragment == "" + + @pytest.mark.asyncio + async def test_make_http_call_change_host(self, mocker, mock_configuration): + mock_host = "legit-ghe" + mock_configuration._params["github_enterprise"] = { + "url": "https://" + mock_host, + "host_override": "ghe.com", + } + client = mocker.MagicMock( + request=mocker.AsyncMock( + return_value=mocker.MagicMock(text="kowabunga", status_code=200) + ) + ) + mocker.patch.object(GithubEnterprise, "get_client", return_value=client) + gl = GithubEnterprise( + repo=dict(service_id="187725", name="codecov-test"), + owner=dict(username="stevepeak", service_id="109479"), + token=dict(key="fake_token"), + ) + method = "GET" + url = f"https://{mock_host}/random_url" + query_params = {"qparam1": "a param", "qparam2": "another param"} + await gl.make_http_call(client, method, url, **query_params) + assert client.request.call_count == 1 + args, kwargs = client.request.call_args + assert kwargs.get("headers") is not None + assert kwargs.get("headers").get("Host") == "ghe.com" + assert len(args) == 2 + built_url = args[1] + parsed_url = urlparse(built_url) + assert parsed_url.scheme == "https" + assert parsed_url.netloc == mock_host + assert parsed_url.path == "/random_url" + assert parsed_url.params == "" + assert parsed_url.fragment == "" diff --git a/libs/shared/tests/unit/torngit/test_gitlab.py b/libs/shared/tests/unit/torngit/test_gitlab.py new file mode 100644 index 0000000000..64d3d220bc --- /dev/null +++ b/libs/shared/tests/unit/torngit/test_gitlab.py @@ -0,0 +1,618 @@ +import asyncio +from unittest.mock import MagicMock +from urllib.parse import parse_qs + +import httpx +import pytest +import respx +from prometheus_client import REGISTRY + +from shared.torngit.base import TokenType +from shared.torngit.exceptions import ( + TorngitCantRefreshTokenError, + TorngitClientError, + TorngitObjectNotFoundError, + TorngitRefreshTokenFailedError, +) +from shared.torngit.gitlab import Gitlab + + +@pytest.fixture +def valid_handler(): + return Gitlab( + repo=dict(service_id="187725", name="codecov-test"), + owner=dict(username="ThiagoCodecov", service_id="109479"), + oauth_consumer_token=dict( + key="client_id", + secret="client_secret", + ), + token=dict(key="access_token", refresh_token="refresh_token"), + ) + + +class TestUnitGitlab(object): + def test_redirect_uri_default(self): + gl = Gitlab() + assert gl.redirect_uri == "https://codecov.io/login/gitlab" + + def test_redirect_uri_custom_redirect(self, mock_configuration): + gl = Gitlab() + mock_configuration._params.update( + {"gitlab": {"redirect_uri": "https://custom_redirect.com"}} + ) + assert gl.redirect_uri == "https://custom_redirect.com" + + def test_redirect_uri_custom_base(self, mock_configuration): + gl = Gitlab() + + mock_configuration._params.update( + {"setup": {"codecov_url": "http://localhost"}} + ) + assert gl.redirect_uri == "http://localhost/login/gitlab" + + @pytest.mark.asyncio + async def test_get_commit_statuses(self, mocker, valid_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "get_commit_statuses"}, + ) + mocker.patch.object( + Gitlab, + "api", + return_value=[ + { + "status": "success", + "description": "Successful status", + "target_url": "url", + "name": "name", + "finished_at": None, + "created_at": None, + }, + { + "status": None, + "description": "None status", + "target_url": "url", + "name": "name", + "created_at": "not none", + }, + ], + ) + res = await valid_handler.get_commit_statuses( + "c739768fcac68144a3a6d82305b9c4106934d31a" + ) + assert res == "failure" + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "get_commit_statuses"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_get_commit_statuses_success(self, mocker, valid_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "get_commit_statuses"}, + ) + mocker.patch.object( + Gitlab, + "api", + return_value=[ + { + "status": "success", + "description": "Successful status", + "target_url": "url", + "name": "name", + "created_at": "not none", + }, + { + "status": "success", + "description": "Another successful status", + "target_url": "url", + "name": "name", + "created_at": "not none", + }, + { + "status": "skipped", + "description": "This was skipped so still counts as success", + "target_url": "url", + "name": "name", + "created_at": "not none", + }, + ], + ) + res = await valid_handler.get_commit_statuses( + "c739768fcac68144a3a6d82305b9c4106934d31a" + ) + assert res == "success" + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "get_commit_statuses"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_get_commit_statuses_pending(self, mocker, valid_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "get_commit_statuses"}, + ) + mocker.patch.object( + Gitlab, + "api", + return_value=[ + { + "status": "created", + "description": "Created means still pending", + "target_url": "url", + "name": "name", + "created_at": "not none", + }, + { + "status": "manual", + "description": "This requires a manual run so we'll consider it pending until then", + "target_url": "url", + "name": "name", + "created_at": "not none", + }, + { + "status": "waiting_for_resource", + "description": "Waiting for a resource", + "target_url": "url", + "name": "name", + "created_at": "not none", + }, + ], + ) + res = await valid_handler.get_commit_statuses( + "c739768fcac68144a3a6d82305b9c4106934d31a" + ) + assert res == "pending" + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "get_commit_statuses"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_find_pull_request_by_commit_endpoint_doesnt_find_old_does( + self, mocker, valid_handler + ): + before_1 = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "find_pull_request_with_commit"}, + ) + before_2 = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "find_pull_request"}, + ) + commit_sha = "c739768fcac68144a3a6d82305b9c4106934d31a" + first_result = [] + second_result = [{"sha": "aaaa", "iid": 123}, {"sha": commit_sha, "iid": 986}] + results = [first_result, second_result] + mocker.patch.object(Gitlab, "api", side_effect=results) + res = await valid_handler.find_pull_request(commit_sha) + assert res == 986 + after_1 = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "find_pull_request_with_commit"}, + ) + after_2 = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "find_pull_request"}, + ) + assert after_1 - before_1 == 1 + assert after_2 - before_2 == 1 + + def test_get_token_by_type_if_none(self): + instance = Gitlab( + token="token", + token_type_mapping={ + TokenType.read: "read", + TokenType.admin: "admin", + TokenType.comment: None, + TokenType.status: "status", + }, + ) + assert instance.get_token_by_type_if_none(None, TokenType.read) == "read" + assert instance.get_token_by_type_if_none(None, TokenType.admin) == "admin" + assert instance.get_token_by_type_if_none(None, TokenType.comment) == "token" + assert instance.get_token_by_type_if_none(None, TokenType.status) == "status" + assert instance.get_token_by_type_if_none( + {"key": "token_set_user"}, TokenType.read + ) == {"key": "token_set_user"} + assert instance.get_token_by_type_if_none( + {"key": "token_set_user"}, TokenType.admin + ) == {"key": "token_set_user"} + assert instance.get_token_by_type_if_none( + {"key": "token_set_user"}, TokenType.comment + ) == {"key": "token_set_user"} + assert instance.get_token_by_type_if_none( + {"key": "token_set_user"}, TokenType.status + ) == {"key": "token_set_user"} + + @pytest.mark.asyncio + async def test_gitlab_url_spaces_percent_encoded(self, mocker, valid_handler): + with respx.mock: + mocked_route = respx.get("https://gitlab.com/api/v4/endpoint%20name").mock( + return_value=httpx.Response(status_code=200, json="{}") + ) + await valid_handler.api("get", "/endpoint name") + + assert mocked_route.call_count == 1 + + @pytest.mark.asyncio + async def test_gitlab_get_source_path_with_spaces(self, mocker, valid_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "get_source"}, + ) + with respx.mock: + mocked_route = respx.get( + "https://gitlab.com/api/v4/projects/187725/repository/files/tests%20with%20space.py?ref=main" + ).mock( + return_value=httpx.Response( + status_code=200, + content='{"commitid": null, "content": "code goes here"}', + ) + ) + await valid_handler.get_source("tests with space.py", "main") + assert mocked_route.call_count == 1 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "get_source"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_gitlab_refresh_fail_terminates_unavailable( + self, mocker, valid_handler + ): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "refresh_token"}, + ) + with pytest.raises(TorngitRefreshTokenFailedError) as exp: + with respx.mock: + mocked_refresh = respx.post("https://gitlab.com/oauth/token").mock( + return_value=httpx.Response( + status_code=502, content="Service unavailable try again later" + ) + ) + await valid_handler.refresh_token(valid_handler.get_client()) + assert exp.code == 555 + mocked_refresh.call_count == 1 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "refresh_token"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_gitlab_refresh_fail_terminates_bad_request( + self, mocker, valid_handler + ): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "refresh_token"}, + ) + with pytest.raises(TorngitRefreshTokenFailedError) as exp: + with respx.mock: + mocked_refresh = respx.post("https://gitlab.com/oauth/token").mock( + return_value=httpx.Response( + status_code=403, content='{"error": "unauthorized"}' + ) + ) + await valid_handler.refresh_token(valid_handler.get_client()) + assert exp.code == 555 + assert mocked_refresh.call_count == 1 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "refresh_token"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_gitlab_refresh_fail_terminates_no_refresh_token_saved( + self, mocker, valid_handler + ): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "refresh_token"}, + ) + valid_handler._token = {"access_token": "old_token_without_refresh"} + with pytest.raises(TorngitCantRefreshTokenError) as exp: + with respx.mock: + mocked_refresh = respx.post("https://gitlab.com/oauth/token").mock( + return_value=httpx.Response( + status_code=403, content='{"error": "unauthorized"}' + ) + ) + await valid_handler.refresh_token(valid_handler.get_client()) + assert exp.code == 555 + assert exp.response_data is None + assert mocked_refresh.call_count == 0 + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "refresh_token"}, + ) + assert after - before == 0 + + @pytest.mark.asyncio + async def test_gitlab_double_refresh(self, mocker, valid_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "refresh_token"}, + ) + + def side_effect(request, *args, **kwargs): + parsed_content = parse_qs(request.content) + refresh_token = parsed_content[b"refresh_token"][0] + if refresh_token == b"refresh_token": + return httpx.Response( + status_code=200, + content='{"access_token": "new_access_token","token_type": "bearer","refresh_token": "new_refresh_token"}', + ) + elif refresh_token == b"new_refresh_token": + return httpx.Response( + status_code=200, + content='{"access_token": "newer_access_token","token_type": "bearer","refresh_token": "newer_refresh_token"}', + ) + pytest.fail("Wrong token received") + + assert valid_handler._oauth == dict(key="client_id", secret="client_secret") + + with respx.mock: + mocked_refresh = respx.post("https://gitlab.com/oauth/token").mock( + side_effect=side_effect + ) + await valid_handler.refresh_token(valid_handler.get_client()) + assert mocked_refresh.call_count == 1 + assert valid_handler._token == dict( + key="new_access_token", refresh_token="new_refresh_token" + ) + + await valid_handler.refresh_token(valid_handler.get_client()) + assert mocked_refresh.call_count == 2 + assert valid_handler._token == dict( + key="newer_access_token", refresh_token="newer_refresh_token" + ) + + # Make sure that changing the token doesn't change the _oauth + assert valid_handler._oauth == dict(key="client_id", secret="client_secret") + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "refresh_token"}, + ) + assert after - before == 2 + + @pytest.mark.asyncio + async def test_gitlab_refresh_after_failed_request(self, mocker, valid_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "refresh_token"}, + ) + + def side_effect(request, *args, **kwargs): + token = request.headers["Authorization"] + if token == "Bearer access_token": + return httpx.Response( + status_code=401, + content='{"error":"invalid_token","error_description":"Token is expired. You can either do re-authorization or token refresh."}', + ) + elif token == "Bearer new_access_token": + return httpx.Response( + status_code=200, + content='{"commitid": null, "content": "code goes here"}', + ) + pytest.fail(f"Wrong token received ({token})") + + f = asyncio.Future() + f.set_result(True) + mock_refresh_callback: MagicMock = mocker.patch.object( + valid_handler, "_on_token_refresh", create=True, return_value=f + ) + with respx.mock: + mocked_route = respx.get( + "https://gitlab.com/api/v4/projects/187725/repository/files/tests%20with%20space.py?ref=main" + ).mock(side_effect=side_effect) + mocked_refresh = respx.post("https://gitlab.com/oauth/token").mock( + return_value=httpx.Response( + status_code=200, + content='{"access_token": "new_access_token","token_type": "bearer","refresh_token": "new_refresh_token"}', + ) + ) + await valid_handler.get_source("tests with space.py", "main") + assert mocked_route.call_count == 2 + assert mocked_refresh.call_count == 1 + assert valid_handler._token["key"] == "new_access_token" + assert valid_handler._token["refresh_token"] == "new_refresh_token" + assert mock_refresh_callback.call_count == 1 + mock_refresh_callback.assert_called_with( + { + "key": "new_access_token", + "refresh_token": "new_refresh_token", + } + ) + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "refresh_token"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_get_distance_in_commits(self): + expected_result = { + "behind_by": None, + "behind_by_commit": None, + "status": None, + "ahead_by": None, + } + handler = Gitlab( + repo=dict(name="example-python", private=True), + ) + res = await handler.get_distance_in_commits("branch", "commit") + assert res == expected_result + + @pytest.mark.asyncio + async def test_get_pull_request_files_404(self): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "get_pull_request_files"}, + ) + with respx.mock: + respx.get( + "https://gitlab.com/api/v4/projects/187725/merge_requests/4/diffs" + ).mock( + return_value=httpx.Response( + status_code=404, + headers={ + "X-RateLimit-Remaining": "0", + "X-RateLimit-Reset": "1350085394", + }, + content='{"error": "unauthorized"}', + ) + ) + handler = Gitlab( + repo=dict(name="example-python", service_id="187725"), + owner=dict(username="codecove2e", service_id="109479"), + token=dict(key=10 * "a280"), + ) + with pytest.raises(TorngitObjectNotFoundError) as excinfo: + await handler.get_pull_request_files(4) + assert excinfo.value.code == 404 + assert excinfo.value.message == "PR with id 4 does not exist" + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "get_pull_request_files"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_get_pull_request_files_403(self): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "get_pull_request_files"}, + ) + with respx.mock: + respx.get( + "https://gitlab.com/api/v4/projects/187725/merge_requests/4/diffs" + ).mock( + return_value=httpx.Response( + status_code=403, + headers={ + "X-RateLimit-Remaining": "0", + "X-RateLimit-Reset": "1350085394", + }, + content='{"error": "unauthorized"}', + ) + ) + handler = Gitlab( + repo=dict(name="example-python", service_id="187725"), + owner=dict(username="codecove2e", service_id="109479"), + token=dict(key=10 * "a280"), + ) + with pytest.raises(TorngitClientError) as excinfo: + await handler.get_pull_request_files(4) + assert excinfo.value.code == 403 + assert excinfo.value.message == "Gitlab API: 403" + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "get_pull_request_files"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_post_webhook(self, mocker): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "post_webhook"}, + ) + m = mocker.patch("shared.torngit.gitlab.Gitlab.api") + + handler = Gitlab( + repo=dict(name="example-python", service_id="187725"), + owner=dict(username="codecove2e", service_id="109479"), + token=dict(key=10 * "a280"), + ) + + await handler.post_webhook( + "Test Webhook", + "http://example.com/gitlab/webhook", + {"test_event": True}, + "supersecret", + ) + + assert m.call_args.args == ("post", "/projects/187725/hooks") + assert m.call_args.kwargs == { + "body": { + "url": "http://example.com/gitlab/webhook", + "enable_ssl_verification": True, + "token": "supersecret", + "test_event": True, + }, + "token": {"key": "a280a280a280a280a280a280a280a280a280a280"}, + } + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "post_webhook"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_edit_webhook(self, mocker): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "edit_webhook"}, + ) + m = mocker.patch("shared.torngit.gitlab.Gitlab.api") + + handler = Gitlab( + repo=dict(name="example-python", service_id="187725"), + owner=dict(username="codecove2e", service_id="109479"), + token=dict(key=10 * "a280"), + ) + + await handler.edit_webhook( + 12345, + "Test Webhook", + "http://example.com/gitlab/webhook", + {"test_event": True}, + "supersecret", + ) + + assert m.call_args.args == ("put", "/projects/187725/hooks/12345") + assert m.call_args.kwargs == { + "body": { + "url": "http://example.com/gitlab/webhook", + "enable_ssl_verification": True, + "token": "supersecret", + "test_event": True, + }, + "token": {"key": "a280a280a280a280a280a280a280a280a280a280"}, + } + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "edit_webhook"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_count_and_get_url_template(self, valid_handler): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "fetch_and_handle_errors_retry"}, + ) + res = valid_handler.count_and_get_url_template( + url_name="fetch_and_handle_errors_retry" + ) + assert res == "" + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "fetch_and_handle_errors_retry"}, + ) + assert after - before == 1 + + @pytest.mark.asyncio + async def test_count_and_get_url_template_unrecognized(self, valid_handler): + with pytest.raises(KeyError): + valid_handler.count_and_get_url_template(url_name="whoops") diff --git a/libs/shared/tests/unit/torngit/test_gitlab_enterprise.py b/libs/shared/tests/unit/torngit/test_gitlab_enterprise.py new file mode 100644 index 0000000000..d7b6bc92ad --- /dev/null +++ b/libs/shared/tests/unit/torngit/test_gitlab_enterprise.py @@ -0,0 +1,103 @@ +import pytest +from mock import MagicMock +from prometheus_client import REGISTRY + +from shared.torngit.gitlab_enterprise import GitlabEnterprise + + +class TestGitlabEnterprise(object): + def test_redirect_uri(self, mocker): + gle = GitlabEnterprise() + assert gle.redirect_uri == "https://codecov.io/login/gle" + + def custom_config(*args, **kwargs): + if args == ("gitlab_enterprise", "redirect_uri"): + return "https://custom_redirect.com" + if args == ("setup", "codecov_url"): + return "http://localhost" + + mocked_config: MagicMock = mocker.patch( + "shared.torngit.gitlab_enterprise.get_config", side_effect=custom_config + ) + assert gle.redirect_uri == "https://custom_redirect.com" + mocked_config.assert_called_with( + "gitlab_enterprise", "redirect_uri", default=None + ) + + def custom_config(*args, **kwargs): + if args == ("gitlab", "redirect_uri"): + return None + if args == ("setup", "codecov_url"): + return "http://localhost" + + mocked_config: MagicMock = mocker.patch( + "shared.torngit.gitlab_enterprise.get_config", side_effect=custom_config + ) + assert gle.redirect_uri == "http://localhost/login/gle" + mocked_config.assert_called_with( + "setup", "codecov_url", default="https://codecov.io" + ) + + def test_urls_no_api_url_set(self, mock_configuration): + mock_configuration._params["gitlab_enterprise"] = { + "url": "https://gitlab-enterprise.codecov.dev" + } + gl = GitlabEnterprise() + assert gl.service_url == "https://gitlab-enterprise.codecov.dev" + assert gl.api_url == "https://gitlab-enterprise.codecov.dev/api/v4" + assert ( + GitlabEnterprise.get_service_url() + == "https://gitlab-enterprise.codecov.dev" + ) + assert ( + GitlabEnterprise.get_api_url() + == "https://gitlab-enterprise.codecov.dev/api/v4" + ) + + def test_urls_with_api_url_set(self, mock_configuration): + mock_configuration._params["gitlab_enterprise"] = { + "url": "https://gitlab-enterprise.codecov.dev", + "api_url": "https://api.gitlab.dev", + } + gl = GitlabEnterprise() + assert gl.service_url == "https://gitlab-enterprise.codecov.dev" + assert gl.api_url == "https://api.gitlab.dev" + + @pytest.mark.asyncio + async def test_fetch_uses_proper_endpoint(self, mocker, mock_configuration): + before = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "post_comment"}, + ) + before_enterprise = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_enterprise_total", + labels={"endpoint": "post_comment"}, + ) + mocked_fetch = mocker.patch.object(GitlabEnterprise, "api", return_value={}) + mock_configuration._params["gitlab_enterprise"] = { + "url": "https://gitlab-enterprise.codecov.dev", + "api_url": "https://api.gitlab.dev", + } + gl = GitlabEnterprise( + repo=dict(service_id="187725", name="codecov-test"), + owner=dict(username="stevepeak", service_id="109479"), + token=dict(key="fake_token"), + ) + res = await gl.post_comment("pullid", "body") + assert res == {} + mocked_fetch.assert_called_with( + "post", + "/projects/187725/merge_requests/pullid/notes", + body={"body": "body"}, + token={"key": "fake_token"}, + ) + after = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_total", + labels={"endpoint": "post_comment"}, + ) + after_enterprise = REGISTRY.get_sample_value( + "git_provider_api_calls_gitlab_enterprise_total", + labels={"endpoint": "post_comment"}, + ) + assert after - before == 0 + assert after_enterprise - before_enterprise == 1 diff --git a/libs/shared/tests/unit/upload/test_utils.py b/libs/shared/tests/unit/upload/test_utils.py new file mode 100644 index 0000000000..b5f97ccb41 --- /dev/null +++ b/libs/shared/tests/unit/upload/test_utils.py @@ -0,0 +1,179 @@ +from datetime import datetime, timedelta +from unittest.mock import PropertyMock, patch + +from django.test import TestCase +from freezegun import freeze_time + +from shared.django_apps.codecov_auth.models import Owner +from shared.django_apps.codecov_auth.tests.factories import OwnerFactory +from shared.django_apps.core.tests.factories import CommitFactory, RepositoryFactory +from shared.django_apps.reports.tests.factories import ( + CommitReportFactory, + UploadFactory, +) +from shared.django_apps.user_measurements.models import UserMeasurement +from shared.plan.service import PlanService +from shared.upload.utils import ( + bulk_insert_coverage_measurements, + insert_coverage_measurement, + query_monthly_coverage_measurements, +) +from tests.helper import mock_all_plans_and_tiers + + +class CoverageMeasurement(TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + + def add_upload_measurements_records( + self, + owner: Owner, + quantity: int, + report_type="coverage", + private=True, + ): + for _ in range(quantity): + repo = RepositoryFactory.create(author=owner, private=private) + commit = CommitFactory.create(repository=repo) + report = CommitReportFactory.create(commit=commit, report_type=report_type) + upload = UploadFactory.create(report=report) + insert_coverage_measurement( + owner_id=owner.ownerid, + repo_id=repo.repoid, + commit_id=commit.id, + upload_id=upload.id, + uploader_used="CLI", + private_repo=repo.private, + report_type=report.report_type, + ) + + def test_query_monthly_coverage_measurements(self): + owner = OwnerFactory() + self.add_upload_measurements_records(owner=owner, quantity=5) + plan_service = PlanService(current_org=owner) + + monthly_measurements = query_monthly_coverage_measurements( + plan_service=plan_service + ) + assert monthly_measurements == 5 + + def test_query_monthly_coverage_measurements_with_a_public_repo(self): + owner = OwnerFactory() + self.add_upload_measurements_records(owner=owner, quantity=3) + self.add_upload_measurements_records(owner=owner, quantity=1, private=False) + + plan_service = PlanService(current_org=owner) + monthly_measurements = query_monthly_coverage_measurements( + plan_service=plan_service + ) + # Doesn't query the last 3 + assert monthly_measurements == 3 + + def test_query_monthly_coverage_measurements_with_non_coverage_report(self): + owner = OwnerFactory() + self.add_upload_measurements_records(owner=owner, quantity=3) + self.add_upload_measurements_records( + owner=owner, quantity=1, report_type="bundle_analysis" + ) + + plan_service = PlanService(current_org=owner) + monthly_measurements = query_monthly_coverage_measurements( + plan_service=plan_service + ) + # Doesn't query the last 3 + assert monthly_measurements == 3 + + def test_query_monthly_coverage_measurements_after_30_days(self): + owner = OwnerFactory() + + # Uploads before 30 days + freezer = freeze_time("2023-10-15T00:00:00") + freezer.start() + self.add_upload_measurements_records(owner=owner, quantity=3) + freezer.stop() + + # Now + freezer = freeze_time("2024-02-10T00:00:00") + self.add_upload_measurements_records(owner=owner, quantity=5) + + all_measurements = UserMeasurement.objects.all() + assert len(all_measurements) == 8 + + plan_service = PlanService(current_org=owner) + monthly_measurements = query_monthly_coverage_measurements( + plan_service=plan_service + ) + assert monthly_measurements == 5 + + def test_query_monthly_coverage_measurements_excluding_uploads_during_trial(self): + freezer = freeze_time("2024-02-01T00:00:00") + freezer.start() + owner = OwnerFactory( + trial_status="expired", + trial_start_date=datetime.utcnow(), + trial_end_date=datetime.utcnow() + timedelta(days=14), + ) + freezer.stop() + + freezer = freeze_time("2024-02-05T00:00:00") + freezer.start() + self.add_upload_measurements_records(owner=owner, quantity=3) + freezer.stop() + + # Now + freezer = freeze_time("2024-02-20T00:00:00") + self.add_upload_measurements_records(owner=owner, quantity=6) + + all_measurements = UserMeasurement.objects.all() + assert len(all_measurements) == 9 + + plan_service = PlanService(current_org=owner) + monthly_measurements = query_monthly_coverage_measurements( + plan_service=plan_service + ) + assert monthly_measurements == 6 + + @patch( + "shared.plan.service.PlanService.monthly_uploads_limit", + new_callable=PropertyMock, + ) + def test_query_monthly_coverage_measurements_beyond_monthly_limit( + self, monthly_uploads_mock + ): + owner = OwnerFactory() + self.add_upload_measurements_records(owner=owner, quantity=10) + + plan_service = PlanService(current_org=owner) + monthly_uploads_mock.return_value = 3 + monthly_measurements = query_monthly_coverage_measurements( + plan_service=plan_service + ) + # 10 uploads total, max 3 returned + assert monthly_measurements == 3 + + def test_bulk_insert_user_measurements(self): + owner = OwnerFactory() + measurements = [] + for _ in range(5): + repo = RepositoryFactory.create(author=owner) + commit = CommitFactory.create(repository=repo) + report = CommitReportFactory.create(commit=commit) + upload = UploadFactory.create(report=report) + measurements.append( + UserMeasurement( + owner_id=owner.ownerid, + repo_id=repo.repoid, + commit_id=commit.id, + upload_id=upload.id, + uploader_used="CLI", + private_repo=repo.private, + report_type=report.report_type, + ) + ) + + inserted_measurements = bulk_insert_coverage_measurements( + measurements=measurements + ) + assert len(inserted_measurements) == 5 diff --git a/libs/shared/tests/unit/utils/__init__.py b/libs/shared/tests/unit/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/tests/unit/utils/test_agg_totals.py b/libs/shared/tests/unit/utils/test_agg_totals.py new file mode 100644 index 0000000000..90ca3c9d9c --- /dev/null +++ b/libs/shared/tests/unit/utils/test_agg_totals.py @@ -0,0 +1,23 @@ +import pytest + +from shared.reports.types import ReportTotals +from shared.utils.totals import agg_totals + + +@pytest.mark.unit +@pytest.mark.parametrize( + "totals, res", + [ + ([None, list(range(6)), list(range(6))], [2, 2, 4, 6, 8, "200.00000"]), + ( + [None, ReportTotals(*list(range(6))), ReportTotals(*list(range(6)))], + [2, 2, 4, 6, 8, "200.00000", 0, 0, 0, 0, 0, 0, 0], + ), + ([], ReportTotals(coverage=None)), + ("", ReportTotals(coverage=None)), + ([ReportTotals()], ReportTotals(files=1, coverage=None)), + ([], ReportTotals(coverage=None)), + ], +) +def test_agg_totals(totals, res): + assert agg_totals(totals) == ReportTotals(*res) diff --git a/libs/shared/tests/unit/utils/test_flare.py b/libs/shared/tests/unit/utils/test_flare.py new file mode 100644 index 0000000000..0d6d4dc58a --- /dev/null +++ b/libs/shared/tests/unit/utils/test_flare.py @@ -0,0 +1,137 @@ +import pytest + +from shared.reports.types import ReportTotals +from shared.utils.flare import report_to_flare + + +@pytest.mark.unit +def test_report_to_flare(): + files = [ + ("a/b/c.py", ReportTotals(None, 100, 47, None, None, "47.00000")), + ("a/b/d.py", ReportTotals(None, 100, 20, None, None, "20.00000")), + ("a/b/e.py", ReportTotals(None, 100, 30, None, None, "30.00000")), + ("x/y/z", ReportTotals(None, 100, 40, None, None, "40.00000")), + ("a/b/c/d/e.py", ReportTotals(None, 100, 40, None, None, "40.00000")), + ("a/b/c/d/g.py", ReportTotals(None, 100, 60, None, None, "60.00000")), + ("a.py", ReportTotals(None, 100, 70, None, None, "70.00000")), + ("b.py", ReportTotals(None, 100, 80, None, None, "80.00000")), + ] + + expected_result = [ + { + "name": "", + "coverage": 48.375, + "color": "#c6b11a", + "_class": None, + "lines": 800, + "children": [ + { + "name": "a/b", + "coverage": 39.4, + "color": "#dfb317", + "_class": None, + "lines": 500, + "children": [ + { + "name": "c/d", + "coverage": 50.0, + "color": "#c0b01b", + "_class": None, + "lines": 200, + "children": [ + { + "color": "#dfb317", + "_class": None, + "lines": 100, + "name": "e.py", + "coverage": "40.00000", + }, + { + "color": "#a4a61d", + "_class": None, + "lines": 100, + "name": "g.py", + "coverage": "60.00000", + }, + ], + }, + { + "color": "#f39a21", + "_class": None, + "lines": 100, + "name": "e.py", + "coverage": "30.00000", + }, + { + "color": "#c9b21a", + "_class": None, + "lines": 100, + "name": "c.py", + "coverage": "47.00000", + }, + { + "color": "#fe7d37", + "_class": None, + "lines": 100, + "name": "d.py", + "coverage": "20.00000", + }, + ], + }, + { + "color": "#97ca00", + "_class": None, + "lines": 100, + "name": "b.py", + "coverage": "80.00000", + }, + { + "color": "#a1b90e", + "_class": None, + "lines": 100, + "name": "a.py", + "coverage": "70.00000", + }, + { + "name": "x/y", + "coverage": 40.0, + "color": "#dfb317", + "_class": None, + "lines": 100, + "children": [ + { + "color": "#dfb317", + "_class": None, + "lines": 100, + "name": "z", + "coverage": "40.00000", + } + ], + }, + ], + } + ] + + def _compare_nested_children_node(result, excpected): + """ + helper method to compare nested dicts, if an item of a dict is a list, the list is sorted and then compared + agaisnt the expected result + """ + if isinstance(result, list): + sorted_list = sorted(result, key=lambda k: k["name"]) + sorted_expected = sorted(excpected, key=lambda k: k["name"]) + for i in range(len(sorted_list)): + _compare_nested_children_node(sorted_list[i], sorted_expected[i]) + else: + for key in result.keys(): + if key == "children": + _compare_nested_children_node( + result["children"], excpected["children"] + ) + else: + assert result[key] == excpected[key] + + report_results = report_to_flare(files, [0, 100]) + assert len(expected_result) == len(report_results) + for i in range(len(expected_result)): + _compare_nested_children_node(expected_result, report_results) diff --git a/libs/shared/tests/unit/utils/test_make_network_file.py b/libs/shared/tests/unit/utils/test_make_network_file.py new file mode 100644 index 0000000000..915a109071 --- /dev/null +++ b/libs/shared/tests/unit/utils/test_make_network_file.py @@ -0,0 +1,37 @@ +import pytest + +from shared.reports.types import NetworkFile, ReportTotals +from shared.utils.make_network_file import make_network_file + + +@pytest.mark.unit +def test_make_network_file(): + assert make_network_file([1, 2, 1, 1]) == NetworkFile( + totals=ReportTotals( + files=1, + lines=2, + hits=1, + misses=1, + partials=0, + complexity=0, + complexity_total=0, + diff=0, + ), + diff_totals=None, + ) + + +def test_make_network_file_with_sessions_encoded(): + assert make_network_file([1, 2, 1, 1]) == NetworkFile( + totals=ReportTotals( + files=1, + lines=2, + hits=1, + misses=1, + partials=0, + complexity=0, + complexity_total=0, + diff=0, + ), + diff_totals=None, + ) diff --git a/libs/shared/tests/unit/utils/test_match.py b/libs/shared/tests/unit/utils/test_match.py new file mode 100644 index 0000000000..cd0121668e --- /dev/null +++ b/libs/shared/tests/unit/utils/test_match.py @@ -0,0 +1,47 @@ +import pytest + +from shared.utils.match import * + + +@pytest.mark.unit +@pytest.mark.parametrize( + "patterns, string, boolean", + [ + (["branch*"], "branch", True), + (["features/.*", "develop"], "features/a", True), + (["feature/.*", "patch.*"], "patch-1", True), + (["feature"], "feature", True), + ([".*.*/.*.py"], "folder/to/path.py", True), + (None, "main", True), + (["main"], "main", True), + ([".*", "!patch"], "patch", False), + (["!patch"], "patch", False), + (["!patch"], "main", True), + (["^!patch"], "main", True), + ([".*", "!patch.*"], "patch-a", False), + (["!wip.*"], "main", True), + (["!wip.*", ".*coverage.*"], "go-coverage", True), + ([".*", "!patch.*"], "patch-a", False), + ([".*", "!patch"], "main", True), + (["!patch", "main"], "main", True), + (["featur$"], "feature", False), + ], +) +def test_match(patterns, string, boolean): + assert match(patterns, string) is boolean + + +@pytest.mark.parametrize( + "patterns, match_any_of_these, boolean", + [ + ([".*"], ["a", Exception()], True), + (["folder/.*"], ["folder/file", Exception()], True), + (["a"], None, False), + (["a"], [], False), + (["folder"], ["file", "folder"], True), + (["folder"], ["file"], False), + (["folder"], ["file", "another"], False), + ], +) +def test_match_any(patterns, match_any_of_these, boolean): + assert match_any(patterns, match_any_of_these) is boolean diff --git a/libs/shared/tests/unit/utils/test_merge.py b/libs/shared/tests/unit/utils/test_merge.py new file mode 100644 index 0000000000..5b7a366fb2 --- /dev/null +++ b/libs/shared/tests/unit/utils/test_merge.py @@ -0,0 +1,408 @@ +from fractions import Fraction + +import pytest + +from shared.utils.merge import ( + CoverageDatapoint, + LineSession, + LineType, + ReportLine, + branch_type, + get_complexity_from_sessions, + get_coverage_from_sessions, + line_type, + merge_all, + merge_branch, + merge_coverage, + merge_datapoints, + merge_line, + merge_line_session, + merge_missed_branches, + merge_partial_line, + partials_to_line, +) + + +@pytest.mark.unit +@pytest.mark.parametrize( + "cov_list, res", + [([0], 0), ([0, 1], 1), ([0, "0/2", "1/2"], "1/2"), ([1, "0/2", "1/2"], "2/2")], +) +def test_merge_all(cov_list, res): + assert merge_all(cov_list) == res + + +@pytest.mark.unit +@pytest.mark.parametrize( + "b1, b2, res", + [ + ("1/2", "2/2", "2/2"), + ("0/2", "2/2", "2/2"), + ("0/2", 1, 1), + ("0/2", -1, -1), + (0, "2/2", "2/2"), + (True, "2/2", "2/2"), + (None, "2/2", "2/2"), + ("1/2", "2/3", "2/3"), + ("1/2", "1/6", "1/6"), + ("0/2", "0/2", "0/2"), + ("0/2", "0/2", "0/2"), + ("0/2", [[1, 2, None]], [[1, 2, None]]), + ([[1, 2, None]], "0/2", [[1, 2, None]]), + ], +) +def test_merge_branch(b1, b2, res): + assert res == merge_branch(b1, b2), "%s <> %s expected %s got %s" % ( + b1, + b2, + res, + str(merge_branch(b1, b2)), + ) + assert res == merge_branch(b2, b1), "%s <> %s expected %s got %s" % ( + b2, + b1, + res, + str(merge_branch(b2, b1)), + ) + + +# This immitates what a report.labels_index looks like +# It's an map idx -> label, so we can go from CoverageDatapoint.label_id to the actual label +# typically via Report.lookup_label_by_id +def lookup_label(label_id: int) -> str: + lookup_table = {1: "banana", 2: "apple", 3: "simpletest"} + return lookup_table[label_id] + + +def test_merge_datapoints(): + c_1 = CoverageDatapoint(sessionid=1, coverage=1, coverage_type=None, label_ids=None) + c_1_copy = CoverageDatapoint( + sessionid=1, coverage=1, coverage_type=None, label_ids=None + ) + c_2 = CoverageDatapoint(sessionid=1, coverage=1, coverage_type=None, label_ids=[1]) + c_2_copy = CoverageDatapoint( + sessionid=1, coverage=1, coverage_type=None, label_ids=[1] + ) + c_2_other_session = CoverageDatapoint( + sessionid=2, coverage=1, coverage_type=None, label_ids=[1] + ) + c_3 = CoverageDatapoint(sessionid=1, coverage=1, coverage_type=None, label_ids=[2]) + assert [c_1, c_2] == merge_datapoints( + [c_1], + [c_2], + ) + assert [c_1, c_2] == merge_datapoints( + [c_2], + [c_1], + ) + assert [c_1, c_2, c_3] == merge_datapoints([c_2, c_1], [c_3]) + assert [c_1, c_2] == merge_datapoints([c_2, c_1], None) + assert [c_1, c_2] == merge_datapoints([c_2, c_1], [None]) + assert [c_1, c_2, c_2_other_session] == merge_datapoints( + [c_1, c_2, c_2_other_session], [c_1_copy, c_2_copy, c_2_other_session] + ) + + +@pytest.mark.unit +@pytest.mark.parametrize( + "p1, p2, res", + [ + ([], [[1, 2, 3]], [[1, 2, 3]]), + ([[1, None, 1]], [], [[1, None, 1]]), # one + ( + [[1, None, 1]], + [[3, 5, 1]], + [[1, None, 1]], + ), # [--++--] inner join 1+&1 3-5&1 => 1+&1 + ( + [[1, None, 1]], + [[1, 2, 0]], + [[1, None, 1]], + ), # [--++--] inner join 1+&1 1-2&0 => 1+&1 + ( + [[None, 10, 1]], + [[None, 20, 1]], + [[0, 20, 1]], + ), # [++--] inner join -10&1 -20&1 => -20&1 + ( + [[None, 10, 0]], + [[None, 20, 0]], + [[0, 20, 0]], + ), # [++--] inner join -10&1 10-20&0 => same + ( + [[1, 5, 0], [6, 10, 1]], + [[10, 15, 1], [18, 20, 0]], + [[1, 5, 0], [6, 15, 1], [18, 20, 0]], + ), # side join + ([[0, 5, 1]], [[10, 20, 0]], [[0, 5, 1], [10, 20, 0]]), # [-- --] outer join + ([[None, 10, 0]], [[5, 20, 1]], [[0, 4, 0], [5, 20, 1]]), # [--/--] reduce left + ( + [[None, 10, 1]], + [[5, 20, 0]], + [[0, 10, 1], [11, 20, 0]], + ), # [--/--] reduce right + ], +) +def test_merge_partial_lines(p1, p2, res): + assert res == merge_partial_line(p1, p2) + assert res == merge_partial_line(p2, p1) + + +@pytest.mark.unit +@pytest.mark.parametrize( + "l1, l2, brm, res", + [ + (1, 5, None, 5), + (1, 0, None, 1), + (True, False, None, True), + (False, True, None, True), + (1, None, None, 1), + ("0/3", "0/3", ["1:12", "1:3", "10:22", "10:12"], "0/3"), + (99999, 10, None, 99999), + (0, [[1, 1, 0]], None, 0), + (0, [[1, 1, 1]], None, 1), + ([[1, 1, 0]], 0, None, 0), + ([[1, 1, 1]], 0, None, 1), + (0, -1, None, -1), + ("1/2", "2/2", None, "2/2"), + ("0/2", "2/2", None, "2/2"), + ("1/2", "1/2", None, "1/2"), + ("1/2", "1/2", [], "2/2"), + ("1/2", "1/2", None, "1/2"), + ("1/2", "1/2", ["56"], "1/2"), + ("1/4", "1/4", ["56"], "3/4"), + ("1/4", 0, None, "1/4"), + ("0/4", "0/4", None, "0/4"), + ("1/4", 1, None, "4/4"), + (0, 0, None, 0), + ], +) +def test_merge_coverage(l1, l2, brm, res): + assert merge_coverage(l1, l2, brm) == res + assert merge_coverage(l2, l1, brm) == res + + +@pytest.mark.unit +@pytest.mark.parametrize( + "sessions, res", + [ + ( + [LineSession(1, 0, [1, 2, 3]), LineSession(1, 0, [2, 3])], + [2, 3], + ), # 2,3 missing + ([LineSession(1, 0, [1, 2]), LineSession(1, 0, [1, 2])], [1, 2]), # 1,2 missing + ([LineSession(1, 0, [1, 2]), LineSession(1, 0, [3, 4])], []), # 1,2 & 3,4 = [] + ( + [LineSession(1, 0, [1, 2]), LineSession(1, 0, [])], + [], + ), # no branches missing on right + ([LineSession("1/3", 0, [1, 2]), LineSession(1, 1)], []), # because its a hit + ( + [LineSession(1, 0, [1, 2]), LineSession(0, 0)], + [1, 2], + ), # missed = carry over branches + ([LineSession(1, 0), LineSession(1, 1)], None), + ([], None), + ([LineSession(1, 0, [1, 2])], [1, 2]), + ], +) +def test_merge_missed_branches(sessions, res): + assert merge_missed_branches(sessions) == res + sessions.reverse() + assert merge_missed_branches(sessions) == res + + +@pytest.mark.unit +@pytest.mark.parametrize( + "l1, l2, expected_res", + [ + (None, (1,), (1,)), # no line to merge + # sessions + ( + ("1/2", None, [[1, "1/2", ["1"]]]), + ("1/2", None, [[1, "1/2", ["2"]]]), + ("2/2", None, [LineSession(1, "2/2", [])]), + ), + # session, w/ single mb + ( + ("1/2", None, [[1, "1/2", ["1"]]]), + ("1/2", None, [[1, "1/2", ["1"]]]), + ("1/2", None, [LineSession(1, "1/2", ["1"])]), + ), + # different sessions + ( + ("1/2", None, [[1, "1/2", ["1"]]]), + ("1/2", None, [[2, "1/2", ["2"]]]), + ("2/2", None, [LineSession(1, "1/2", ["1"]), LineSession(2, "1/2", ["2"])]), + ), + # add coverage + ((1, None, [[1, 1]]), (2, None, [[1, 2]]), (2, None, [LineSession(1, 2)])), + # merge sessions + ( + ("2/2", None, [[1, "2/2"]]), + ("0/2", None, [[2, "0/2", [1, 2]]]), + ("2/2", None, [LineSession(1, "2/2"), LineSession(2, "0/2", [1, 2])]), + ), + # types + ((1, None, [[1, 1]]), (1, "b", [[1, 1]]), (1, "b", [LineSession(1, 1)])), + ( + (1, None, [[1, 1]], None, None, [(1, 1, None, [1])]), + (1, "b", [[1, 1]], None, None, [(1, 1, "b", [3])]), + ( + 1, + "b", + [LineSession(1, 1)], + None, # messages + None, # complexity + [ + CoverageDatapoint( + sessionid=1, + coverage=1, + coverage_type=None, + label_ids=[1], + ), + CoverageDatapoint( + sessionid=1, + coverage=1, + coverage_type="b", + label_ids=[3], + ), + ], + ), + ), + ], +) +def test_merge_line(l1, l2, expected_res): + assert merge_line( + ReportLine.create(*l1) if l1 else None, ReportLine.create(*l2) if l2 else None + ) == ReportLine.create(*expected_res) + res = merge_line( + ReportLine.create(*l2) if l2 else None, ReportLine.create(*l1) if l1 else None + ) + try: + assert res == ReportLine.create(*expected_res) + except Exception: + res.sessions.reverse() + assert res == ReportLine.create(*expected_res) + + +@pytest.mark.xfail(reason='merging "incompatible" branches is broken right now') +def test_merge_with_incompatible_branches(): + s1 = LineSession(id=0, coverage="0/2", branches=["0:5", "0:6"]) + s2 = LineSession(id=0, coverage="0/1", branches=["0"]) + + # If we would ignore the "missing branches", we would generate at least some kind of reasonable output. + assert merge_branch("0/2", "0/1") == "0/2" + + # We expect this to be a miss, as both sessions are misses. + # But the logic to merge coverages taking into account "missed branches" is broken. + # Both coverages have a different number (and format) of missed branches. + # Thus the intersection of missed branches is empty, and `merge_coverage` will + # then just assume the line was hit, and generate a hit coverage accordingly. + merged = merge_line_session(s1, s2) + assert line_type(merged.coverage) == LineType.miss + + # Depending on the order of arguments, we end up with either `2/2` or `1/1`. + # But given they are different formats (and number of branches), it is unclear + # what to output from this as well. + + +@pytest.mark.unit +@pytest.mark.parametrize( + "s1, s2, res", + [ + ( + LineSession(0, "1/2", ["exit"]), + LineSession(0, "2/2"), + LineSession(0, "2/2", []), + ), + (LineSession(0, "1/2"), LineSession(0, "2/2"), LineSession(0, "2/2")), + ( + LineSession(0, "1/2", ["exit"]), + LineSession(0, "1/2", ["1"]), + LineSession(0, "2/2", []), + ), + ( + LineSession(0, "2/3", ["1"]), + LineSession(0, "1/3", ["1", "2"]), + LineSession(0, "2/3", ["1"]), + ), + # (LineSession(0, '1/2'), LineSession(0, '2/2', ['branch']), LineSession(0, '1/2', ['branch'])), + # (LineSession(0, 1, None, [1]), LineSession(0, 1), LineSession(0, 1, None, [1])), + # (LineSession(0, 1, None, [2, 3]), LineSession(0, 1, None, [1, 5]), LineSession()), + ], +) +def test_merge_line_session(s1, s2, res): + assert merge_line_session(s1, s2) == res + assert merge_line_session(s2, s1) == res + + +@pytest.mark.unit +@pytest.mark.parametrize( + "line, _line_type", + [ + (True, 2), + (1, 0), + (0, 1), + (None, None), + (False, None), + (-1, -1), + ("0/2", 1), + (str("1/2"), 2), + ("2/2", 0), + (Fraction(1, 1), LineType.hit), + (Fraction(2, 2), LineType.hit), + (Fraction(1, 2), LineType.partial), + (Fraction(0, 2), LineType.miss), + ], +) +def test_line_type(line, _line_type): + assert line_type(line) == _line_type + + +@pytest.mark.unit +@pytest.mark.parametrize( + "x, y", [("0/2", 1), ("1/2", 2), ("2/2", 0), ("0", 1), ("1", 0)] +) +def test_branch_type(x, y): + assert branch_type(x) == y + assert branch_type(str(x)) == y + + +@pytest.mark.unit +@pytest.mark.parametrize( + "partials, res", + [ + ([(1, 5, 1), (5, 7, 0), (8, 9, 0)], "1/3"), + ([(1, None, 1)], 1), + ([(0, None, 1)], 1), + ([(15, 18, 1)], 1), + ([(1, None, 0)], 0), + ([(0, None, 0)], 0), + ([(0, 1, 0), (2, 3, 0)], "0/2"), + ([(0, 1, 1), (3, 4, 1)], "2/2"), + ], +) +def test_partials_to_line(partials, res): + assert res == partials_to_line(partials) + + +@pytest.mark.unit +@pytest.mark.parametrize( + "sessions, complexity", + [([[1, 2, 3, 4, 5]], 5), ([[[1, 2], [2, 3], [3, 4], [4, 5], [5, 6]]], (5, 6))], +) +def test_get_complexity_from_sessions(sessions, complexity): + sessions = [LineSession(*sess) for sess in sessions] + assert get_complexity_from_sessions(sessions) == complexity + assert get_complexity_from_sessions(sessions) == complexity + + +@pytest.mark.unit +def test_get_coverage_from_sessions(): + assert ( + get_coverage_from_sessions( + [LineSession(1, "2/2"), LineSession(2, "0/2", [1, 2])] + ) + == "2/2" + ) diff --git a/libs/shared/tests/unit/utils/test_migrate.py b/libs/shared/tests/unit/utils/test_migrate.py new file mode 100644 index 0000000000..94bba417b6 --- /dev/null +++ b/libs/shared/tests/unit/utils/test_migrate.py @@ -0,0 +1,42 @@ +import pytest + +from shared.utils.migrate import * + + +@pytest.mark.unit +@pytest.mark.parametrize( + "totals, res", + [ + ( + { + "files": 203, + "hit": 2549, + "methods": 0, + "branches": 574, + "lines": 4076, + "partial": 0, + "missed": 1527, + }, + [203, 4076, 2549, 1527, 0, "62.53680", 574, 0, 0, 0, 0], + ), + ( + { + "f": 203, + "h": 2549, + "b": 574, + "n": 4076, + "p": 0, + "c": "62.53680", + "m": 1527, + }, + [203, 4076, 2549, 1527, 0, "62.53680", 574, 0, 0, 0, 0, 0, 0], + ), + ( + [203, 4076, 2549, 1527, 0, "62.53680", 574, 0, 0, 0, 0, 0], + [203, 4076, 2549, 1527, 0, "62.53680", 574, 0, 0, 0, 0, 0], + ), + (None, []), + ], +) +def test_migrate_totals(totals, res): + assert migrate_totals(totals) == res diff --git a/libs/shared/tests/unit/utils/test_report_encoder.py b/libs/shared/tests/unit/utils/test_report_encoder.py new file mode 100644 index 0000000000..14b89a1622 --- /dev/null +++ b/libs/shared/tests/unit/utils/test_report_encoder.py @@ -0,0 +1,48 @@ +from decimal import Decimal + +import pytest + +from shared.reports.types import ReportTotals +from shared.utils.ReportEncoder import ReportEncoder +from shared.utils.sessions import Session + + +@pytest.mark.unit +@pytest.mark.parametrize( + "obj, res", + [ + (ReportTotals(), (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)), + ( + ReportTotals("files", "lines"), + ("files", "lines", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + ), + ( + Session("id", "totals"), + { + "N": None, + "a": None, + "c": None, + "e": None, + "d": None, + "f": None, + "j": None, + "n": None, + "p": None, + "u": None, + "t": "totals", + "st": "uploaded", + "se": {}, + }, + ), + (Decimal("85.00"), "85.00"), + ], +) +def test_report_encoder(obj, res): + assert ReportEncoder().default(obj) == res + + +@pytest.mark.unit +def test_exception_report_encoder(): + with pytest.raises(Exception) as e_info: + ReportEncoder().default([1, 2]) + assert e_info.type is TypeError diff --git a/libs/shared/tests/unit/utils/test_sessions.py b/libs/shared/tests/unit/utils/test_sessions.py new file mode 100644 index 0000000000..7e86889396 --- /dev/null +++ b/libs/shared/tests/unit/utils/test_sessions.py @@ -0,0 +1,135 @@ +import pytest + +from shared.utils.sessions import ReportTotals, Session, SessionType + + +@pytest.mark.unit +def test_sessions(): + s = Session( + "id", + "totals", + "time", + "archive", + "flags", + "provider", + "build", + "job", + "url", + "state", + "env", + "name", + ) + assert s._encode() == { + "t": "totals", + "d": "time", + "a": "archive", + "f": "flags", + "c": "provider", + "n": "build", + "N": "name", + "j": "job", + "u": "url", + "p": "state", + "e": "env", + "st": "uploaded", + "se": {}, + } + + +def test_parse_session(): + encoded_session = { + "t": [1, 3, 5, 6], + "d": "time", + "a": "archive", + "f": "flags", + "c": "provider", + "n": "build", + "N": "name", + "j": "job", + "u": "url", + "p": "state", + "e": "env", + "st": "uploaded", + "se": {}, + } + sess = Session.parse_session(**encoded_session) + assert sess.totals == ReportTotals(files=1, lines=3, hits=5, misses=6) + assert sess.time == "time" + assert sess.archive == "archive" + assert sess.flags == "flags" + assert sess.provider == "provider" + assert sess.build == "build" + assert sess.job == "job" + assert sess.url == "url" + assert sess.state == "state" + assert sess.env == "env" + assert sess.name == "name" + assert sess.session_type == SessionType.uploaded + assert sess.session_extras == {} + + +def test_parse_session_parsed_report_totals(): + encoded_session = { + "t": ReportTotals(files=1, lines=3, hits=5, misses=6), + "d": "time", + "a": "archive", + "f": "flags", + "c": "provider", + "n": "build", + "N": "name", + "j": "job", + "u": "url", + "p": "state", + "e": "env", + "st": "uploaded", + "se": {}, + } + sess = Session.parse_session(**encoded_session) + assert sess.totals == ReportTotals(files=1, lines=3, hits=5, misses=6) + assert sess.time == "time" + assert sess.archive == "archive" + assert sess.flags == "flags" + assert sess.provider == "provider" + assert sess.build == "build" + assert sess.job == "job" + assert sess.url == "url" + assert sess.state == "state" + assert sess.env == "env" + assert sess.name == "name" + assert sess.session_type == SessionType.uploaded + assert sess.session_extras == {} + + +def test_parse_session_then_encode(): + encoded_session = { + "t": [1, 3, 5, 6], + "d": "time", + "a": "archive", + "f": "flags", + "c": "provider", + "n": "build", + "N": "name", + "j": "job", + "u": "url", + "p": "state", + "e": "env", + "st": "uploaded", + "se": {}, + } + reencoded_session = { + "t": (1, 3, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0), + "d": "time", + "a": "archive", + "f": "flags", + "c": "provider", + "n": "build", + "N": "name", + "j": "job", + "u": "url", + "p": "state", + "e": "env", + "st": "uploaded", + "se": {}, + } + sess = Session.parse_session(**encoded_session) + assert sess._encode() == reencoded_session diff --git a/libs/shared/tests/unit/utils/test_snake_to_camel_case.py b/libs/shared/tests/unit/utils/test_snake_to_camel_case.py new file mode 100644 index 0000000000..dea1c25ecf --- /dev/null +++ b/libs/shared/tests/unit/utils/test_snake_to_camel_case.py @@ -0,0 +1,19 @@ +from shared.utils.snake_to_camel_case import snake_to_camel_case + + +def test_single_word(): + assert snake_to_camel_case("hello") == "hello" + + +def test_empty_string(): + assert snake_to_camel_case("") == "" + + +def test_two_words(): + assert snake_to_camel_case("hello_world") == "helloWorld" + + +def test_many_words(): + assert ( + snake_to_camel_case("hello_world_codecov_is_cool") == "helloWorldCodecovIsCool" + ) diff --git a/libs/shared/tests/unit/utils/test_sum_totals.py b/libs/shared/tests/unit/utils/test_sum_totals.py new file mode 100644 index 0000000000..a6e81d30f4 --- /dev/null +++ b/libs/shared/tests/unit/utils/test_sum_totals.py @@ -0,0 +1,39 @@ +import pytest + +from shared.reports.types import ReportTotals +from shared.utils.totals import agg_totals, sum_totals + + +@pytest.mark.unit +@pytest.mark.parametrize( + "totals, res", + [ + ( + [ReportTotals(), ReportTotals(3, 3, 3, 3), ReportTotals()], + ReportTotals(3, 3, 3, 3, 0, "100"), + ), + ([ReportTotals()], ReportTotals(1, 0, 0, 0, 0, coverage=None)), + ([], ReportTotals(coverage=None)), + ], +) +def test_sum_totals(totals, res): + assert sum_totals(totals) == res + + +def test_agg_totals(): + total_list = [(1, 2, 3), (0, 0, 4), (0, 100, 12, 100000)] + assert agg_totals(total_list) == ReportTotals( + files=3, + lines=102, + hits=19, + misses=0, + partials=0, + coverage="18.62745", + branches=0, + methods=0, + messages=0, + sessions=0, + complexity=0, + complexity_total=0, + diff=0, + ) diff --git a/libs/shared/tests/unit/utils/test_urls.py b/libs/shared/tests/unit/utils/test_urls.py new file mode 100644 index 0000000000..9889408d7f --- /dev/null +++ b/libs/shared/tests/unit/utils/test_urls.py @@ -0,0 +1,74 @@ +# -*- coding: latin-1 -*- + +import pytest + +from shared.utils.urls import escape, make_url, url_concat +from tests.base import BaseTestCase + + +class TestUrlsUtil(BaseTestCase): + @pytest.mark.parametrize( + "string, result", + [ + (("ab" + "\xf1" + "cd", False), b"ab\xc3\xb1cd"), + (("ab" + "\xf1" + "cd", True), "ab%C3%B1cd"), + (("ə/fix-coverage", False), b"\xc3\x89\xc2\x99/fix-coverage"), + (("ə/fix-coverage", True), "%C3%89%C2%99/fix-coverage"), + ((1, False), 1), + ((1, True), "1"), + ((None, False), None), + ((False, False), False), + ((True, False), True), + ], + ) + def test_escape(self, string, result): + assert escape(*string) == result + + def test_make_url_escapes_in_path(self, mock_configuration): + res = make_url(None, "\xa3") + assert "\xa3" not in res + assert "%C2%A3" in res + + def test_make_url_escapes_in_query(self, mock_configuration): + res = make_url(None, param="\xa3") + assert "\xa3" not in res + assert "%C2%A3" in res + + def test_make_url(self, mocker, mock_configuration): + repo = mocker.MagicMock(service="github", slug="owner/repo") + assert ( + make_url(repo, "path", "to", "somewhere") + == "https://codecov.io/gh/owner/repo/path/to/somewhere" + ) + assert ( + make_url(None, "path", "to", "other") == "https://codecov.io/path/to/other" + ) + mock_configuration.set_params({"setup": {"codecov_url": "https://other.com"}}) + assert make_url(None, "path", "to", "here") == "https://other.com/path/to/here" + + @pytest.mark.parametrize( + "url, args, expected", + [ + ("http://example.com/foo", dict(c="d"), "http://example.com/foo?c=d"), + ( + "http://example.com/foo?a=b", + dict(c="d"), + "http://example.com/foo?a=b&c=d", + ), + ( + "http://example.com/foo?a=b", + [("c", "d"), ("c", "d2")], + "http://example.com/foo?a=b&c=d&c=d2", + ), + ], + ) + def test_url_concat(self, url, args, expected): + res = url_concat(url, args) + assert res == expected + + def test_url_concat_err(self): + with pytest.raises( + Exception, match="'args' parameter should be dict, list or tuple" + ): + url = "http://example.com" + url_concat(url, "abc") diff --git a/libs/shared/tests/unit/utils/test_utils/__init__.py b/libs/shared/tests/unit/utils/test_utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/shared/tests/unit/utils/test_utils/test_mock_config_helper.py b/libs/shared/tests/unit/utils/test_utils/test_mock_config_helper.py new file mode 100644 index 0000000000..f85a144edb --- /dev/null +++ b/libs/shared/tests/unit/utils/test_utils/test_mock_config_helper.py @@ -0,0 +1,12 @@ +from shared.config import get_config, load_file_from_path_at_config +from shared.utils.test_utils import mock_config_helper + + +class TestMockConfigHelper(object): + def test_mock_config_helper_get(self, mocker): + mock_config_helper(mocker, configs={"foo.bar": "baz"}) + assert get_config("foo", "bar", default="not baz") == "baz" + + def test_mock_config_helper_load_file(self, mocker): + mock_config_helper(mocker, file_configs={"foo.bar": "baz"}) + assert load_file_from_path_at_config("foo", "bar") == "baz" diff --git a/libs/shared/tests/unit/validation/test_helpers.py b/libs/shared/tests/unit/validation/test_helpers.py new file mode 100644 index 0000000000..318821591c --- /dev/null +++ b/libs/shared/tests/unit/validation/test_helpers.py @@ -0,0 +1,623 @@ +import re + +import pytest + +from shared.validation.helpers import ( + BundleSizeThresholdSchemaField, + ByteSizeSchemaField, + CoverageCommentRequirementSchemaField, + CoverageRangeSchemaField, + CustomFixPathSchemaField, + Invalid, + LayoutStructure, + PathPatternSchemaField, + PercentSchemaField, + UserGivenBranchRegex, + determine_path_pattern_type, + translate_glob_to_regex, +) +from shared.yaml.validation import pre_process_yaml +from tests.base import BaseTestCase + + +class TestPathPatternSchemaField(BaseTestCase): + def test_simple_path_structure_no_star(self): + ps = PathPatternSchemaField() + res = ps.validate("a/b") + compiled = re.compile(res) + assert compiled.match("a/b") is not None + assert compiled.match("a/b/file_1.py") is not None + assert compiled.match("c/a/b") is None + assert compiled.match("a/path/b") is None + assert compiled.match("a/path/path2/b") is None + + def test_simple_path_structure_regex(self): + ps = PathPatternSchemaField() + res = ps.validate("[a-z]+/test_.*") + compiled = re.compile(res) + assert compiled.match("perro/test_folder.py") is not None + assert compiled.match("cachorro/test_folder.py") is not None + assert compiled.match("cachorro/tes_folder.py") is None + assert compiled.match("cachorro/what/test_folder.py") is None + assert compiled.match("[/a/b") is None + + def test_simple_path_structure_one_star_end(self): + ps = PathPatternSchemaField() + res = ps.validate("tests/*") + compiled = re.compile(res) + assert compiled.match("tests/file_1.py") is not None + assert compiled.match("tests/testeststsetetesetsfile_2.py") is not None + assert compiled.match("tests/deep/file_1.py") is None + + def test_simple_path_structure_one_star(self): + ps = PathPatternSchemaField() + res = ps.validate("a/*/b") + compiled = re.compile(res) + assert compiled.match("a/path/b") is not None + assert compiled.match("a/path/b/file_2.py") is None + assert compiled.match("a/path/b/more_path/some_file.py") is None + assert compiled.match("a/b") is None + assert compiled.match("a/path/path2/b") is None + + def test_simple_path_structure_negative(self): + ps = PathPatternSchemaField() + res = ps.validate("!path/to/folder") + assert res.startswith("!") + compiled = re.compile(res[1:]) + # Check the negatives, we want `path/to/folder` files to match so we refuse them later + assert compiled.match("path/to/folder") is not None + assert compiled.match("path/to/folder/file_2.py") is not None + assert compiled.match("path/to/folder/more_path/some_file.py") is not None + assert compiled.match("a/b") is None + assert compiled.match("path/folder") is None + + def test_simple_path_structure_double_star(self): + ps = PathPatternSchemaField() + res = ps.validate("a/**/b") + compiled = re.compile(res) + assert compiled.match("a/path/b") is not None + assert compiled.match("a/path/b/some_file.py") is None + assert compiled.match("a/path/b/more_path/some_file.py") is None + assert compiled.match("a/path/path2/b") is not None + assert compiled.match("a/path/path2/b/some_file.py") is None + assert compiled.match("a/path/path2/b/more_path/some_file.py") is None + assert compiled.match("a/c") is None + + def test_path_with_leading_period_slash(self): + ps = PathPatternSchemaField() + res = ps.validate("./src/register-test-globals.ts") + compiled = re.compile(res) + assert compiled.match("src/register-test-globals.ts") is not None + second_res = ps.validate("./test/*.cc") + second_compiled = re.compile(second_res) + assert second_compiled.match("test/test_SW_Markov.cc") is not None + + def test_star_dot_star_pattern(self): + ps = PathPatternSchemaField() + res = ps.validate("test/**/*.*") + compiled = re.compile(res) + assert compiled.match("test/unit/presenters/goal_sparkline_test.rb") is not None + + def test_double_star_end(self): + user_input = "Snapshots/**" + ps = PathPatternSchemaField() + res = ps.validate(user_input) + compiled = re.compile(res) + assert compiled.match("Snapshots/Snapshots/ViewController.swift") is not None + + def test_double_star_prefix(self): + user_input = "**/*bundle" + ps = PathPatternSchemaField() + res = ps.validate(user_input) + compiled = re.compile(res) + paths_to_match = [ + "app/Bundle/ignoreme.bundle", + "tests/test_bundle/sample_bundle", + ] + paths_not_to_match = [ + "app/Bundle/BundleCart.py", + "app/Bundle/__init__.py", + "app/Bundle/mybundle.py", + "tests/test_bundle/test_bundle_cart.py", + ] + for path in paths_to_match: + assert compiled.match(path) is not None + for path in paths_not_to_match: + assert compiled.match(path) is None + + +class TestLayoutStructure(BaseTestCase): + def test_simple_layout(self): + schema = LayoutStructure() + result = "reach, diff, flags, files, components, footer" + expected_result = "reach, diff, flags, files, components, footer" + assert expected_result == schema.validate(result) + + def test_empty_layout(self): + schema = LayoutStructure() + layout_input = "" + expected_result = "" + assert expected_result == schema.validate(layout_input) + + def test_simple_layout_with_number(self): + schema = LayoutStructure() + result = "reach, diff, flags, files:10, footer" + expected_result = "reach, diff, flags, files:10, footer" + assert expected_result == schema.validate(result) + + def test_simple_layout_with_improper_number(self): + schema = LayoutStructure() + result = "reach, diff, flags, files:twenty, footer" + with pytest.raises(Invalid) as exc: + schema.validate(result) + assert ( + exc.value.error_message + == "Improper pattern for value on layout: files:twenty" + ) + + def test_simple_layout_bad_name(self): + schema = LayoutStructure() + result = "reach, diff, flags, love, files, footer" + with pytest.raises(Invalid) as exc: + schema.validate(result) + assert exc.value.error_message == "Unexpected values on layout: love" + + +class TestCoverageRangeSchemaField(BaseTestCase): + def test_simple_coverage_range(self): + crsf = CoverageRangeSchemaField() + assert crsf.validate([80, 90]) == [80.0, 90.0] + assert crsf.validate("80..90") == [80.0, 90.0] + assert crsf.validate("80...90") == [80.0, 90.0] + assert crsf.validate("80...100") == [80.0, 100.0] + invalid_cases = [ + "80....90", + "80.90", + "90..80", + "90..80..50", + "infinity...90", + "80...?90", + "80...101", + "-80...90", + "80...9f0", + ["arroba", 90], + [10, 20, 30], + [10, 20, 30], + ["infinity", 90], + ] + for invalid in invalid_cases: + with pytest.raises(Invalid): + crsf.validate(invalid) + + +class TestUserGivenBranchRegex(BaseTestCase): + def test_user_givne_branch(self): + a = UserGivenBranchRegex() + assert a.validate(None) is None + assert a.validate(".*") == ".*" + assert a.validate("*") == ".*" + assert a.validate("apple*") == "^apple.*" + assert a.validate("apple") == "^apple$" + + +class TestPercentSchemaField(BaseTestCase): + def test_simple_coverage_range(self): + crsf = PercentSchemaField() + assert crsf.validate(80) == 80.0 + assert crsf.validate("auto", allow_auto=True) == "auto" + assert crsf.validate(80.0) == 80.0 + assert crsf.validate("80%") == 80.0 + assert crsf.validate("80") == 80.0 + assert crsf.validate("0") == 0.0 + assert crsf.validate("150%") == 150.0 + with pytest.raises(Invalid): + crsf.validate("auto") + with pytest.raises(Invalid): + crsf.validate("nana") + with pytest.raises(Invalid): + crsf.validate("%80") + with pytest.raises(Invalid): + crsf.validate("8%0%") + with pytest.raises(Invalid): + crsf.validate("infinity") + with pytest.raises(Invalid): + crsf.validate("nan") + + +class TestPatternTypeDetermination(BaseTestCase): + def test_determine_path_pattern_type(self): + assert determine_path_pattern_type("path/to/folder") == "path_prefix" + assert determine_path_pattern_type("path/*/folder") == "glob" + assert determine_path_pattern_type("path/**/folder") == "glob" + assert determine_path_pattern_type("path/.*/folder") == "regex" + assert determine_path_pattern_type("path/[a-z]*/folder") == "regex" + assert determine_path_pattern_type("*/[a-z]*/folder") == "glob" + assert determine_path_pattern_type("before/test-*::after/") == "glob" + + +class TestPreprocess(BaseTestCase): + def test_preprocess_empty(self): + user_input = {} + expected_result = {} + pre_process_yaml(user_input) + assert expected_result == user_input + + def test_preprocess_none_in_fields(self): + user_input = {"codecov": None} + expected_result = {"codecov": None} + pre_process_yaml(user_input) + assert expected_result == user_input + + +class TestGlobToRegexTranslation(BaseTestCase): + def test_translate_glob_to_regex(self): + assert re.compile(translate_glob_to_regex("a")).match("a") is not None + assert re.compile(translate_glob_to_regex("[abc]*")).match("a") is not None + assert re.compile(translate_glob_to_regex("[abc]*")).match("ab") is not None + assert re.compile(translate_glob_to_regex("[abc]")).match("d") is None + assert re.compile(translate_glob_to_regex("[a-c]")).match("b") is not None + assert translate_glob_to_regex("**/test*.ts") == r"(?s:.*/test[^\/]*\.ts)\Z" + assert ( + re.compile(translate_glob_to_regex("**/test*.ts")).match("src/src2/test.ts") + is not None + ) + assert ( + re.compile(translate_glob_to_regex("a/*/*b*.ts")).match("a/folder/b.ts") + is not None + ) + assert ( + re.compile(translate_glob_to_regex("a/*/*b*.ts")).match( + "a/folder/test_b.ts" + ) + is not None + ) + assert ( + re.compile(translate_glob_to_regex("a/*/*b*.ts")).match( + "a/folder/test_b_test.ts" + ) + is not None + ) + assert re.compile(translate_glob_to_regex("a/*/*b*.ts")).match("a/b.ts") is None + assert ( + re.compile(translate_glob_to_regex("a/*/*b*.ts")).match( + "a/folder1/folder2/b.ts" + ) + is None + ) + assert ( + re.compile(translate_glob_to_regex("a/**/*b*.ts")).match("a/folder/b.ts") + is not None + ) + assert ( + re.compile(translate_glob_to_regex("a/**/*b*.ts")).match( + "a/folder/test_b.ts" + ) + is not None + ) + assert ( + re.compile(translate_glob_to_regex("a/**/*b*.ts")).match( + "a/folder/test_b_test.ts" + ) + is not None + ) + assert ( + re.compile(translate_glob_to_regex("a/**/*b*.ts")).match( + "a/folder1/folder2/b.ts" + ) + is not None + ) + + +class TestCustomFixPathSchemaField(BaseTestCase): + def test_custom_fixpath(self): + cfpsf = CustomFixPathSchemaField() + res = cfpsf.validate("a::b") + assert res == "^a::b" + + def test_custom_fixpath_removal(self): + cfpsf = CustomFixPathSchemaField() + res = cfpsf.validate("a/::") + assert res == "a/::" + + def test_custom_fixpath_removal_no_slashes(self): + cfpsf = CustomFixPathSchemaField() + res = cfpsf.validate("a::") + assert res == "a::" + + def test_custom_fixpath_addition(self): + cfpsf = CustomFixPathSchemaField() + res = cfpsf.validate("::b") + assert res == "::b" + + def test_custom_fixpath_regex(self): + cfpsf = CustomFixPathSchemaField() + res = cfpsf.validate("path-*::b") + assert res == r"(?s:path\-[^\/]*)::b" + + def test_custom_fixpath_docs_example(self): + cfpsf = CustomFixPathSchemaField() + res = cfpsf.validate("before/tests-*::after/") + assert res == r"(?s:before/tests\-[^\/]*)::after/" + + def test_custom_fixpath_invalid_input(self): + cfpsf = CustomFixPathSchemaField() + # No "::" separator + with pytest.raises(Invalid): + cfpsf.validate("beforeafter") + + +class TestCoverageCommentRequirementSchemaField(object): + @pytest.mark.parametrize( + "input, expected", + [ + # Old values + pytest.param(True, [0b001]), + pytest.param(False, [0b000]), + # Individual values + pytest.param("any_change", [0b001]), + pytest.param("coverage_drop", [0b010]), + pytest.param("uncovered_patch", [0b100]), + # Operators + pytest.param( + "uncovered_patch AND uncovered_patch", + [0b100, 0b100], + ), + pytest.param( + "uncovered_patch and uncovered_patch", + [0b100, 0b100], + ), + pytest.param( + "uncovered_patch OR uncovered_patch", + [0b100], + ), + pytest.param( + "uncovered_patch or uncovered_patch", + [0b100], + ), + # Combinations + pytest.param( + "coverage_drop or any_change", + [0b011], + ), + pytest.param( + "coverage_drop and any_change", + [0b010, 0b001], + ), + pytest.param( + "coverage_drop or uncovered_patch", + [0b110], + ), + pytest.param( + "any_change and uncovered_patch", + [0b001, 0b100], + ), + pytest.param( + "any_change and coverage_drop", + [0b001, 0b010], + ), + pytest.param( + "any_change or coverage_drop or uncovered_patch", + [0b111], + ), + pytest.param( + "any_change or coverage_drop and uncovered_patch", + [0b011, 0b100], + ), + pytest.param( + "any_change and coverage_drop and uncovered_patch", + [0b001, 0b010, 0b100], + ), + pytest.param( + "any_change and coverage_drop or uncovered_patch", + [0b001, 0b110], + ), + ], + ) + def test_coverage_comment_requirement_coercion_success(self, input, expected): + validator = CoverageCommentRequirementSchemaField() + assert validator.validate(input) == expected + + @pytest.mark.parametrize( + "input, exception_message", + [ + pytest.param( + None, "Only bool and str are accepted values", id="invalid_input_None" + ), + pytest.param( + 42, "Only bool and str are accepted values", id="invalid_input_int" + ), + pytest.param( + ["coverage_drop"], + "Only bool and str are accepted values", + id="invalid_input_list", + ), + pytest.param( + "coverage_drop+any_change", + "Failed to parse required_changes", + id="invalid_string_no_spaces", + ), + pytest.param( + "coverage_drop + any_change", + "Failed to parse required_changes", + id="invalid_operation", + ), + pytest.param("", "required_changes is empty", id="empty_string"), + pytest.param( + "coverage_drop any_change", + "Failed to parse required_changes", + id="no_operation", + ), + pytest.param( + "coverage_drop AND AND any_change", + "Failed to parse required_changes", + id="too_many_operations", + ), + pytest.param( + "coverage_drop AND any_change AND", + "Failed to parse required_changes", + id="incomplete_operation", + ), + pytest.param( + "coverage_dropANDany_change", + "Failed to parse required_changes", + id="no_spaces", + ), + pytest.param( + "coverage_drop AND OR any_change", + "Failed to parse required_changes", + id="missing_middle_operand", + ), + pytest.param( + "AND", + "Failed to parse required_changes", + id="single_operation_no_operands", + ), + pytest.param( + "AND OR AND", + "Failed to parse required_changes", + id="only_operations_no_operands", + ), + ], + ) + def test_coverage_comment_requirement_coercion_fail(self, input, exception_message): + validator = CoverageCommentRequirementSchemaField() + with pytest.raises(Invalid) as exp: + validator.validate(input) + assert exp.value.error_message == exception_message + + +class TestByteSizeSchemaField(object): + @pytest.mark.parametrize( + "input, expected", + [ + (100, 100), + ("100b", 100), + ("100 mb", 100000000), + ("12KB", 12000), + ("1 GB", 1000000000), + ("12bytes", 12), + ("24b", 24), + ], + ) + def test_byte_size_coercion_success(self, input, expected): + validator = ByteSizeSchemaField() + assert validator.validate(input) == expected + + @pytest.mark.parametrize( + "input, error_message", + [ + pytest.param( + None, "Value should be int or str. Received NoneType", id="None_input" + ), + pytest.param( + [], "Value should be int or str. Received list", id="list_input" + ), + pytest.param( + 12.34, "Value should be int or str. Received float", id="float_input" + ), + pytest.param( + "200", + "Value doesn't match expected regex. Acceptable extensions are mb, kb, gb, b or bytes", + id="no_extension", + ), + pytest.param( + "kb", + "Value doesn't match expected regex. Acceptable extensions are mb, kb, gb, b or bytes", + id="no_number", + ), + pytest.param( + "100kb 100mb", + "Value doesn't match expected regex. Acceptable extensions are mb, kb, gb, b or bytes", + id="multiple_values", + ), + pytest.param( + "200.45mb", + "Value doesn't match expected regex. Acceptable extensions are mb, kb, gb, b or bytes", + id="float_value_in_str", + ), + pytest.param( + "200tb", + "Value doesn't match expected regex. Acceptable extensions are mb, kb, gb, b or bytes", + id="invalid_extension", + ), + pytest.param( + -200, + "Only positive values accepted", + id="negative_number", + ), + pytest.param( + "-200kb", + "Value doesn't match expected regex. Acceptable extensions are mb, kb, gb, b or bytes", + id="negative_number_with_extension", + ), + ], + ) + def test_byte_size_coercion_fail(self, input, error_message): + validator = ByteSizeSchemaField() + with pytest.raises(Invalid) as exp: + validator.validate(input) + assert exp.value.error_message == error_message + + +class TestBundleSizeThresholdSchemaField(object): + @pytest.mark.parametrize( + "input, expected", + [ + (100, ("absolute", 100)), + ("100b", ("absolute", 100)), + ("100 mb", ("absolute", 100000000)), + ("12KB", ("absolute", 12000)), + ("12%", ("percentage", 12.0)), + ("65%", ("percentage", 65.0)), + ("100%", ("percentage", 100.0)), + ("200%", ("percentage", 200.0)), + (5.5, ("percentage", 5.5)), + (60, ("absolute", 60)), + (60.0, ("percentage", 60.0)), + ], + ) + def test_byte_size_coercion_success(self, input, expected): + validator = BundleSizeThresholdSchemaField() + assert validator.validate(input) == expected + + @pytest.mark.parametrize( + "input, error_message", + [ + pytest.param( + None, "Value should be int or str. Received NoneType", id="None_input" + ), + pytest.param( + [], "Value should be int or str. Received list", id="list_input" + ), + pytest.param( + "200", + "Value doesn't match expected regex. Acceptable extensions are mb, kb, gb, b or bytes", + id="no_extension", + ), + pytest.param( + "-200%", + "-200% should be a number", + id="negative_percentage", + ), + pytest.param( + "kb", + "Value doesn't match expected regex. Acceptable extensions are mb, kb, gb, b or bytes", + id="absolute_no_number", + ), + pytest.param( + "%", + "% should be a number", + id="percentage_no_number", + ), + pytest.param( + "100mb%", + "100mb should be a number", + id="percentage_and_absolute", + ), + ], + ) + def test_byte_size_coercion_fail(self, input, error_message): + validator = BundleSizeThresholdSchemaField() + with pytest.raises(Invalid) as exp: + validator.validate(input) + assert exp.value.error_message == error_message diff --git a/libs/shared/tests/unit/validation/test_install_validation.py b/libs/shared/tests/unit/validation/test_install_validation.py new file mode 100644 index 0000000000..964a33f1a4 --- /dev/null +++ b/libs/shared/tests/unit/validation/test_install_validation.py @@ -0,0 +1,495 @@ +from shared.validation.install import log as install_log +from shared.validation.install import validate_install_configuration +from shared.yaml.validation import UserGivenSecret + + +def test_validate_install_configuration_empty(mocker): + mock_warning = mocker.patch.object(install_log, "warning") + assert validate_install_configuration({}) == {} + assert mock_warning.call_count == 0 + + +def test_validate_install_configuration_simple(mocker): + mock_warning = mocker.patch.object(install_log, "warning") + assert validate_install_configuration( + {"setup": {"codecov_url": "http://codecov.company.com"}} + ) == {"setup": {"codecov_url": "http://codecov.company.com"}} + assert mock_warning.call_count == 0 + + +def test_validate_install_configuration_invalid(mocker): + install_log.setLevel("DEBUG") + mock_debug = mocker.patch.object(install_log, "debug") + assert validate_install_configuration( + {"setup": {"codecov_url": "http://codecov.company.com"}, "gitlab": 1} + ) == {"setup": {"codecov_url": "http://codecov.company.com"}, "gitlab": 1} + assert mock_debug.call_count == 1 + + +def test_validate_install_configuration_with_user_yaml(mocker): + user_input = { + "setup": {"codecov_url": "http://codecov.company.com", "guest_access": False}, + "site": { + "coverage": { + "status": { + "project": False, + "patch": { + "default": {"informational": True}, + "ui": {"informational": True}, + }, + "changes": False, + } + }, + "comment": False, + "flags": {"ui": {"paths": ["/ui-v2/"]}}, + "github_checks": {"annotations": False}, + "ignore": [ + "agent/uiserver/bindata_assetfs.go", + "vendor/**/*", + "**/*.pb.go", + ], + }, + } + mock_warning = mocker.patch.object(install_log, "warning") + assert validate_install_configuration(user_input) == { + "setup": {"codecov_url": "http://codecov.company.com", "guest_access": False}, + "site": { + "coverage": { + "status": { + "project": False, + "patch": { + "default": {"informational": True}, + "ui": {"informational": True}, + }, + "changes": False, + } + }, + "comment": False, + "flags": {"ui": {"paths": ["^/ui-v2/.*"]}}, + "github_checks": {"annotations": False}, + "ignore": [ + "^agent/uiserver/bindata_assetfs.go.*", + "(?s:vendor/.*/[^\\/]*)\\Z", + "(?s:.*/[^\\/]*\\.pb\\.go)\\Z", + ], + }, + } + assert mock_warning.call_count == 0 + + +def test_validate_sample_production_config(mocker): + user_input = { + "services": { + "external_dependencies_folder": "./external_deps", + "minio": { + "host": "minio", + "access_key_id": "pokemon01_hmac_id", + "secret_access_key": "pokemon01_hmac_key", + "verify_ssl": False, + "iam_auth": False, + "iam_endpoint": None, + "hash_key": "aabb72b4a26e49a1a2a41bebaaa6a9aa", + "bucket": "codecov", + }, + "google_analytics_key": "UA-63027104-1", + "chosen_storage": "gcp", + "celery_broker": "kaploft-memorystore-celery-url", + "aws": {"resource": "s3", "region_name": "us-east-1"}, + "sentry": {"server_dsn": "pokemon01_worker_sentry_dsn"}, + "stripe": {"api_key": "pokemonuction_services_stripe_api_key"}, + "gcp": { + "google_credentials_location": "/path/to-credentials/application_default_credentials.json" + }, + "database_url": "pokemon01_database_url", + "redis_url": "kaploft-memorystore-url", + "vsc_cache": { + "enabled": True, + "metrics_app": "shared", + "check_duration": 100, + "compare_duration": 110, + "status_duration": 90, + }, + }, + "site": { + "codecov": {"require_ci_to_pass": True}, + "coverage": { + "precision": 2, + "round": "down", + "range": "70...100", + "status": { + "project": True, + "patch": True, + "changes": False, + "default_rules": {"flag_coverage_not_uploaded_behavior": "include"}, + }, + }, + "comment": { + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + "show_carryforward_flags": False, + "require_base": False, + "require_changes": False, + "require_head": True, + }, + "github_checks": {"annotations": True}, + "parsers": { + "gcov": { + "branch_detection": { + "conditional": True, + "loop": True, + "macro": False, + "method": False, + } + }, + "javascript": {"enable_partials": False}, + }, + }, + "setup": { + "cache": {"uploads": 86400}, + "codecov_url": "https://codecov.io", + "debug": False, + "http": {"force_https": True, "timeouts": {"connect": 30, "receive": 60}}, + "loglvl": "INFO", + "media": { + "assets": "https://codecov-cdn.storage.googleapis.com/4.4.8-e33f298", + "dependancies": "https://codecov-cdn.storage.googleapis.com/4.4.8-e33f298", + }, + "tasks": { + "celery": { + "hard_timelimit": 240, + "soft_timelimit": 200, + "enterprise": {"hard_timelimit": 400, "soft_timelimit": 500}, + }, + "upload": { + "queue": "uploads", + "enterprise": {"hard_timelimit": 400, "soft_timelimit": 500}, + }, + "label_analysis": { + "queue": "labelanalysis", + "enterprise": {"hard_timelimit": 401, "soft_timelimit": 501}, + }, + "notify": {"queue": "notify", "timeout": 60}, + }, + "encryption_secret": "encryption_$ecret", + }, + "bitbucket": { + "bot": { + "username": "codecov-io", + "secret": "pokemonuction_bitbucket_bot_secret", + "key": "pokemonuction_bitbucket_bot_key", + }, + "client_id": "pokemonuction_bitbucket_client_id", + "client_secret": "pokemonuction_bitbucket_client_secret", + }, + "github": { + "bot": {"username": "codecov-io", "key": "pokemonuction_github_bot_key"}, + "bots": { + "comment": { + "username": "codecov-commenter", + "key": "pokemonuction_github_commenter_pa_token", + }, + "read": { + "username": "codecov-commenter", + "key": "pokemonuction_github_commenter_pa_token", + }, + "status": { + "username": "codecov-commenter", + "key": "pokemonuction_github_commenter_pa_token", + }, + "tokenless": { + "username": "codecov-commenter", + "key": "pokemonuction_github_commenter_pa_token", + }, + }, + "integration": {"id": 254, "pem": "/secrets/github-pem/github.pem"}, + }, + "gitlab": { + "bot": {"username": "codecov-io", "key": "pokemonuction_gitlab_bot_key"}, + }, + } + expected_result = { + "services": { + "external_dependencies_folder": "./external_deps", + "minio": { + "host": "minio", + "access_key_id": "pokemon01_hmac_id", + "secret_access_key": "pokemon01_hmac_key", + "verify_ssl": False, + "iam_auth": False, + "iam_endpoint": None, + "hash_key": "aabb72b4a26e49a1a2a41bebaaa6a9aa", + "bucket": "codecov", + }, + "google_analytics_key": "UA-63027104-1", + "chosen_storage": "gcp", + "celery_broker": "kaploft-memorystore-celery-url", + "aws": {"resource": "s3", "region_name": "us-east-1"}, + "sentry": {"server_dsn": "pokemon01_worker_sentry_dsn"}, + "stripe": {"api_key": "pokemonuction_services_stripe_api_key"}, + "gcp": { + "google_credentials_location": "/path/to-credentials/application_default_credentials.json" + }, + "database_url": "pokemon01_database_url", + "redis_url": "kaploft-memorystore-url", + "vsc_cache": { + "enabled": True, + "metrics_app": "shared", + "check_duration": 100, + "compare_duration": 110, + "status_duration": 90, + }, + }, + "site": { + "codecov": {"require_ci_to_pass": True}, + "coverage": { + "precision": 2, + "round": "down", + "range": [70.0, 100.0], + "status": { + "project": True, + "patch": True, + "changes": False, + "default_rules": {"flag_coverage_not_uploaded_behavior": "include"}, + }, + }, + "comment": { + "layout": "reach, diff, flags, files, footer", + "behavior": "default", + "show_carryforward_flags": False, + "require_base": False, + "require_changes": [0b000], + "require_head": True, + }, + "github_checks": {"annotations": True}, + "parsers": { + "gcov": { + "branch_detection": { + "conditional": True, + "loop": True, + "macro": False, + "method": False, + } + }, + "javascript": {"enable_partials": False}, + }, + }, + "setup": { + "cache": {"uploads": 86400}, + "codecov_url": "https://codecov.io", + "debug": False, + "http": {"force_https": True, "timeouts": {"connect": 30, "receive": 60}}, + "loglvl": "INFO", + "media": { + "assets": "https://codecov-cdn.storage.googleapis.com/4.4.8-e33f298", + "dependancies": "https://codecov-cdn.storage.googleapis.com/4.4.8-e33f298", + }, + "tasks": { + "celery": { + "hard_timelimit": 240, + "soft_timelimit": 200, + "enterprise": {"hard_timelimit": 400, "soft_timelimit": 500}, + }, + "label_analysis": { + "queue": "labelanalysis", + "enterprise": {"hard_timelimit": 401, "soft_timelimit": 501}, + }, + "upload": { + "queue": "uploads", + "enterprise": {"hard_timelimit": 400, "soft_timelimit": 500}, + }, + "notify": {"queue": "notify", "timeout": 60}, + }, + "encryption_secret": "encryption_$ecret", + }, + "bitbucket": { + "bot": { + "username": "codecov-io", + "secret": "pokemonuction_bitbucket_bot_secret", + "key": "pokemonuction_bitbucket_bot_key", + }, + "client_id": "pokemonuction_bitbucket_client_id", + "client_secret": "pokemonuction_bitbucket_client_secret", + }, + "github": { + "bot": {"username": "codecov-io", "key": "pokemonuction_github_bot_key"}, + "bots": { + "comment": { + "username": "codecov-commenter", + "key": "pokemonuction_github_commenter_pa_token", + }, + "read": { + "username": "codecov-commenter", + "key": "pokemonuction_github_commenter_pa_token", + }, + "status": { + "username": "codecov-commenter", + "key": "pokemonuction_github_commenter_pa_token", + }, + "tokenless": { + "username": "codecov-commenter", + "key": "pokemonuction_github_commenter_pa_token", + }, + }, + "integration": {"id": 254, "pem": "/secrets/github-pem/github.pem"}, + }, + "gitlab": { + "bot": {"username": "codecov-io", "key": "pokemonuction_gitlab_bot_key"}, + }, + } + mock_warning = mocker.patch.object(install_log, "warning") + res = validate_install_configuration(user_input) + assert mock_warning.call_count == 0 + assert res["site"] == expected_result["site"] + assert res == expected_result + + +def test_validate_install_configuration_with_user_yaml_with_user_secret(mocker): + value = "github/11934774/154468867/https://hooks.slack.com/services/first_key/BE7FWCVHV/dkbfscprianc7wrb" + encoded_value = UserGivenSecret.encode(value) + user_yaml_dict = { + "coverage": { + "round": "down", + "precision": 2, + "range": [70.0, 100.0], + "status": {"project": {"default": {"base": "auto"}}}, + "notify": {"irc": {"user_given_title": {"password": encoded_value}}}, + }, + "ignore": ["Pods/.*"], + } + user_input = { + "setup": {"codecov_url": "http://codecov.company.com"}, + "site": user_yaml_dict, + } + mock_warning = mocker.patch.object(install_log, "warning") + assert validate_install_configuration(user_input) == { + "setup": {"codecov_url": "http://codecov.company.com"}, + "site": user_yaml_dict, + } + assert mock_warning.call_count == 0 + + +def test_validate_install_configuration_with_additional_yamls(mocker): + mock_warning = mocker.patch.object(install_log, "warning") + assert validate_install_configuration( + { + "setup": {"codecov_url": "http://codecov.company.com"}, + "additional_user_yamls": [ + { + "percentage": 30, + "name": "banana", + "override": {"comment": False}, + } + ], + } + ) == { + "setup": {"codecov_url": "http://codecov.company.com"}, + "additional_user_yamls": [ + { + "percentage": 30, + "name": "banana", + "override": {"comment": False}, + } + ], + } + assert mock_warning.call_count == 0 + + +def test_pubsub_config(mocker): + mock_warning = mocker.patch.object(install_log, "warning") + assert validate_install_configuration( + { + "setup": { + "pubsub": { + "project_id": "1234", + "topic": "codecov", + "enabled": True, + } + }, + } + ) == { + "setup": { + "pubsub": { + "project_id": "1234", + "topic": "codecov", + "enabled": True, + } + }, + } + assert mock_warning.call_count == 0 + + +def test_admins(mocker): + user_input = { + "setup": { + "admins": [ + { + "service": "github", + "username": "user123", + } + ], + }, + } + expected_result = { + "setup": { + "admins": [ + { + "service": "github", + "username": "user123", + } + ], + }, + } + mock_warning = mocker.patch.object(install_log, "warning") + res = validate_install_configuration(user_input) + assert mock_warning.call_count == 0 + assert res == expected_result + + +def test_validate_install_configuration_raise_warning(mocker): + install_log.setLevel("DEBUG") + mock_debug = mocker.patch.object(install_log, "debug") + input = { + "setup": { + "tasks": { + "celery": { + "hard_timelimit": 240, + "soft_timelimit": 200, + "enterprise": {"hard_timelimit": 400, "soft_timelimit": 500}, + }, + "upload": {"queue": "uploads", "unknown_key": "error"}, + "notify": {"queue": "notify", "timeout": 60}, + "unknown_task": {"queue": "error"}, + } + } + } + validate_install_configuration(input) + mock_debug.assert_called_with( + "Configuration considered invalid, using dict as it is", + extra={ + "errors": { + "setup": [ + { + "tasks": [ + { + "unknown_task": ["Not a valid TaskConfigGroup"], + "upload": [ + "none or more than one rule validate", + { + "oneof definition 0": [ + {"unknown_key": ["unknown field"]} + ], + "oneof definition 1": [ + { + "queue": ["unknown field"], + "unknown_key": ["unknown field"], + } + ], + }, + ], + } + ] + } + ] + } + }, + ) diff --git a/libs/shared/tests/unit/validation/test_validation.py b/libs/shared/tests/unit/validation/test_validation.py new file mode 100644 index 0000000000..75c2fec0d5 --- /dev/null +++ b/libs/shared/tests/unit/validation/test_validation.py @@ -0,0 +1,1453 @@ +import os + +import pytest + +from shared.config import ConfigHelper, get_config +from shared.rollouts.features import BUNDLE_THRESHOLD_FLAG +from shared.validation.exceptions import InvalidYamlException +from shared.yaml.validation import ( + _calculate_error_location_and_message_from_error_dict, + do_actual_validation, + validate_yaml, +) +from tests.base import BaseTestCase + + +class TestUserYamlValidation(BaseTestCase): + def test_empty_case(self): + user_input = {} + expected_result = {} + assert validate_yaml(user_input) == expected_result + + @pytest.mark.parametrize("input_value", ["", 10, [], tuple(), set()]) + def test_wrong_object_type(self, input_value): + with pytest.raises(InvalidYamlException) as exc: + validate_yaml(input_value) + exception = exc.value + assert exception.error_location == [] + assert exception.error_message == "Yaml needs to be a dict" + assert exception.original_exc is None + + @pytest.mark.parametrize( + "user_input, expected_result", + [ + ( + { + "coverage": {"status": {"patch": True, "project": False}}, + "comment": False, + }, + { + "comment": False, + "coverage": {"status": {"patch": True, "project": False}}, + }, + ), + ( + { + "codecov": {"bot": "codecov-io", "require_ci_to_pass": False}, + "coverage": { + "status": { + "project": { + "default": {"target": "78%", "threshold": "5%"} + }, + "patch": {"default": {"target": "75%"}}, + } + }, + }, + { + "codecov": {"bot": "codecov-io", "require_ci_to_pass": False}, + "coverage": { + "status": { + "project": {"default": {"target": 78.0, "threshold": 5.0}}, + "patch": {"default": {"target": 75.0}}, + } + }, + }, + ), + ( + { + "coverage": { + "status": { + "project": False, + "patch": { + "default": {"informational": True}, + "ui": {"informational": True}, + }, + "changes": False, + } + }, + "comment": False, + "flags": {"ui": {"paths": ["/ui-v2/"]}}, + "github_checks": {"annotations": False}, + "ignore": [ + "agent/uiserver/bindata_assetfs.go", + "vendor/**/*", + "**/*.pb.go", + ], + }, + { + "coverage": { + "status": { + "project": False, + "patch": { + "default": {"informational": True}, + "ui": {"informational": True}, + }, + "changes": False, + } + }, + "comment": False, + "flags": {"ui": {"paths": ["^/ui-v2/.*"]}}, + "github_checks": {"annotations": False}, + "ignore": [ + "^agent/uiserver/bindata_assetfs.go.*", + "(?s:vendor/.*/[^\\/]*)\\Z", + "(?s:.*/[^\\/]*\\.pb\\.go)\\Z", + ], + }, + ), + ( + { + "comment": { + "require_head": True, + "require_base": True, + "layout": "diff", + "require_changes": True, + "branches": ["main"], + "behavior": "once", + "after_n_builds": 6, + "hide_project_coverage": True, + }, + "coverage": { + "status": { + "project": {"default": {"threshold": "1%"}}, + "patch": False, + } + }, + "github_checks": {"annotations": False}, + "fixes": [ + "/opt/conda/lib/python3.8/site-packages/::project/", + "C:/Users/circleci/project/build/win_tmp/build/::project/", + ], + "ignore": ["coffee", "party_man", "test"], + "codecov": {"notify": {"after_n_builds": 6}}, + }, + { + "comment": { + "require_head": True, + "require_base": True, + "layout": "diff", + "require_changes": [0b001], + "branches": ["^main$"], + "behavior": "once", + "after_n_builds": 6, + "hide_project_coverage": True, + }, + "coverage": { + "status": { + "project": {"default": {"threshold": 1}}, + "patch": False, + } + }, + "github_checks": {"annotations": False}, + "fixes": [ + "^/opt/conda/lib/python3.8/site-packages/::project/", + "^C:/Users/circleci/project/build/win_tmp/build/::project/", + ], + "ignore": ["^coffee.*", "^party_man.*", "^test.*"], + "codecov": {"notify": {"after_n_builds": 6}}, + }, + ), + ( + { + "ignore": ["js/plugins", "plugins"], + "coverage": { + "notify": { + "slack": { + "default": { + "url": "https://hooks.slack.com/services/testdazzd/testrf4k6py/test72nq0j0ke3prs2fdvfuj", + "only_pulls": False, + "branches": ["main", "qa", "dev"], + } + } + } + }, + }, + { + "ignore": ["^js/plugins.*", "^plugins.*"], + "coverage": { + "notify": { + "slack": { + "default": { + "url": "https://hooks.slack.com/services/testdazzd/testrf4k6py/test72nq0j0ke3prs2fdvfuj", + "only_pulls": False, + "branches": ["^main$", "^qa$", "^dev$"], + } + } + } + }, + }, + ), + ( + { + "codecov": {"notify": {"manual_trigger": True}}, + }, + { + "codecov": {"notify": {"manual_trigger": True}}, + }, + ), + ], + ) + def test_random_real_life_cases(self, user_input, expected_result): + # Some random cases based on real world examples + assert expected_result == validate_yaml(user_input) + + def test_case_with_experimental_turned_on_valid(self, mocker): + mocker.patch.dict(os.environ, {"CERBERUS_VALIDATOR_RATE": "1.0"}) + user_input = {"coverage": {"status": {"patch": True}}} + expected_result = {"coverage": {"status": {"patch": True}}} + assert expected_result == validate_yaml(user_input) + + def test_case_with_experimental_turned_invalid(self, mocker): + mocker.patch.dict(os.environ, {"CERBERUS_VALIDATOR_RATE": "1.0"}) + user_input = {"coverage": {"status": {"patch": "banana"}}} + with pytest.raises(InvalidYamlException) as ex: + validate_yaml(user_input) + assert ex.value.error_location == ["coverage", "status", "patch"] + assert ex.value.error_message == "must be of ['dict', 'boolean'] type" + + def test_many_flags_validation(self): + user_input = { + "codecov": {"max_report_age": False, "notify": {"after_n_builds": 10}}, + "comment": {"layout": "reach,footer"}, + "coverage": { + "status": { + "patch": { + "default": True, + "numbers_only": {"paths": ["wildwest/numbers.py"]}, + "strings_only": {"paths": ["wildwest/strings.py"]}, + "one": {"flags": ["one"]}, + "two": {"flags": ["two"]}, + "three": {"flags": ["three"]}, + "four": {"flags": ["four"]}, + "five": {"flags": ["five"]}, + "six": {"flags": ["six"]}, + "seven": {"flags": ["seven"]}, + "eight": {"flags": ["eight"]}, + "nine": {"flags": ["nine"]}, + "ten": {"flags": ["ten"]}, + "eleven": {"flags": ["eleven"]}, + "one_without_t": {"flags": ["one"], "paths": ["wildwest"]}, + "two_without_t": {"flags": ["two"], "paths": ["wildwest"]}, + "three_without_t": {"flags": ["three"], "paths": ["wildwest"]}, + "four_without_t": {"flags": ["four"], "paths": ["wildwest"]}, + "five_without_t": {"flags": ["five"], "paths": ["wildwest"]}, + "six_without_t": {"flags": ["six"], "paths": ["wildwest"]}, + "seven_without_t": {"flags": ["seven"], "paths": ["wildwest"]}, + "eight_without_t": {"flags": ["eight"], "paths": ["wildwest"]}, + "nine_without_t": {"flags": ["nine"], "paths": ["wildwest"]}, + "ten_without_t": {"flags": ["ten"], "paths": ["wildwest"]}, + "eleven_without_t": { + "flags": ["eleven"], + "paths": ["wildwest"], + }, + }, + "project": { + "default": True, + "numbers_only": {"paths": ["wildwest/numbers.py"]}, + "strings_only": {"paths": ["wildwest/strings.py"]}, + "one": {"flags": ["one"]}, + "two": {"flags": ["two"]}, + "three": {"flags": ["three"]}, + "four": {"flags": ["four"]}, + "five": {"flags": ["five"]}, + "six": {"flags": ["six"]}, + "seven": {"flags": ["seven"]}, + "eight": {"flags": ["eight"]}, + "nine": {"flags": ["nine"]}, + "ten": {"flags": ["ten"]}, + "eleven": {"flags": ["eleven"]}, + "one_without_t": {"flags": ["one"], "paths": ["wildwest"]}, + "two_without_t": {"flags": ["two"], "paths": ["wildwest"]}, + "three_without_t": {"flags": ["three"], "paths": ["wildwest"]}, + "four_without_t": {"flags": ["four"], "paths": ["wildwest"]}, + "five_without_t": {"flags": ["five"], "paths": ["wildwest"]}, + "six_without_t": {"flags": ["six"], "paths": ["wildwest"]}, + "seven_without_t": {"flags": ["seven"], "paths": ["wildwest"]}, + "eight_without_t": {"flags": ["eight"], "paths": ["wildwest"]}, + "nine_without_t": {"flags": ["nine"], "paths": ["wildwest"]}, + "ten_without_t": {"flags": ["ten"], "paths": ["wildwest"]}, + "eleven_without_t": { + "flags": ["eleven"], + "paths": ["wildwest"], + }, + }, + "changes": { + "numbers_only": {"paths": ["wildwest/numbers.py"]}, + "strings_only": {"paths": ["wildwest/strings.py"]}, + "default": True, + "one": {"flags": ["one"]}, + "two": {"flags": ["two"]}, + "three": {"flags": ["three"]}, + "four": {"flags": ["four"]}, + "five": {"flags": ["five"]}, + "six": {"flags": ["six"]}, + "seven": {"flags": ["seven"]}, + "eight": {"flags": ["eight"]}, + "nine": {"flags": ["nine"]}, + "ten": {"flags": ["ten"]}, + "eleven": {"flags": ["eleven"]}, + "one_without_t": {"flags": ["one"], "paths": ["wildwest"]}, + "two_without_t": {"flags": ["two"], "paths": ["wildwest"]}, + "three_without_t": {"flags": ["three"], "paths": ["wildwest"]}, + "four_without_t": {"flags": ["four"], "paths": ["wildwest"]}, + "five_without_t": {"flags": ["five"], "paths": ["wildwest"]}, + "six_without_t": {"flags": ["six"], "paths": ["wildwest"]}, + "seven_without_t": {"flags": ["seven"], "paths": ["wildwest"]}, + "eight_without_t": {"flags": ["eight"], "paths": ["wildwest"]}, + "nine_without_t": {"flags": ["nine"], "paths": ["wildwest"]}, + "ten_without_t": {"flags": ["ten"], "paths": ["wildwest"]}, + "eleven_without_t": { + "flags": ["eleven"], + "paths": ["wildwest"], + }, + }, + } + }, + "flag_management": { + "default_rules": { + "carryforward": False, + "statuses": [{"name_prefix": "aaa", "type": "patch"}], + }, + "individual_flags": [ + {"name": "cawcaw", "paths": ["banana"], "after_n_builds": 3} + ], + }, + } + expected_result = { + "codecov": {"max_report_age": False, "notify": {"after_n_builds": 10}}, + "comment": {"layout": "reach,footer"}, + "coverage": { + "status": { + "patch": { + "default": True, + "numbers_only": {"paths": ["^wildwest/numbers.py.*"]}, + "strings_only": {"paths": ["^wildwest/strings.py.*"]}, + "one": {"flags": ["one"]}, + "two": {"flags": ["two"]}, + "three": {"flags": ["three"]}, + "four": {"flags": ["four"]}, + "five": {"flags": ["five"]}, + "six": {"flags": ["six"]}, + "seven": {"flags": ["seven"]}, + "eight": {"flags": ["eight"]}, + "nine": {"flags": ["nine"]}, + "ten": {"flags": ["ten"]}, + "eleven": {"flags": ["eleven"]}, + "one_without_t": {"flags": ["one"], "paths": ["^wildwest.*"]}, + "two_without_t": {"flags": ["two"], "paths": ["^wildwest.*"]}, + "three_without_t": { + "flags": ["three"], + "paths": ["^wildwest.*"], + }, + "four_without_t": {"flags": ["four"], "paths": ["^wildwest.*"]}, + "five_without_t": {"flags": ["five"], "paths": ["^wildwest.*"]}, + "six_without_t": {"flags": ["six"], "paths": ["^wildwest.*"]}, + "seven_without_t": { + "flags": ["seven"], + "paths": ["^wildwest.*"], + }, + "eight_without_t": { + "flags": ["eight"], + "paths": ["^wildwest.*"], + }, + "nine_without_t": {"flags": ["nine"], "paths": ["^wildwest.*"]}, + "ten_without_t": {"flags": ["ten"], "paths": ["^wildwest.*"]}, + "eleven_without_t": { + "flags": ["eleven"], + "paths": ["^wildwest.*"], + }, + }, + "project": { + "default": True, + "numbers_only": {"paths": ["^wildwest/numbers.py.*"]}, + "strings_only": {"paths": ["^wildwest/strings.py.*"]}, + "one": {"flags": ["one"]}, + "two": {"flags": ["two"]}, + "three": {"flags": ["three"]}, + "four": {"flags": ["four"]}, + "five": {"flags": ["five"]}, + "six": {"flags": ["six"]}, + "seven": {"flags": ["seven"]}, + "eight": {"flags": ["eight"]}, + "nine": {"flags": ["nine"]}, + "ten": {"flags": ["ten"]}, + "eleven": {"flags": ["eleven"]}, + "one_without_t": {"flags": ["one"], "paths": ["^wildwest.*"]}, + "two_without_t": {"flags": ["two"], "paths": ["^wildwest.*"]}, + "three_without_t": { + "flags": ["three"], + "paths": ["^wildwest.*"], + }, + "four_without_t": {"flags": ["four"], "paths": ["^wildwest.*"]}, + "five_without_t": {"flags": ["five"], "paths": ["^wildwest.*"]}, + "six_without_t": {"flags": ["six"], "paths": ["^wildwest.*"]}, + "seven_without_t": { + "flags": ["seven"], + "paths": ["^wildwest.*"], + }, + "eight_without_t": { + "flags": ["eight"], + "paths": ["^wildwest.*"], + }, + "nine_without_t": {"flags": ["nine"], "paths": ["^wildwest.*"]}, + "ten_without_t": {"flags": ["ten"], "paths": ["^wildwest.*"]}, + "eleven_without_t": { + "flags": ["eleven"], + "paths": ["^wildwest.*"], + }, + }, + "changes": { + "numbers_only": {"paths": ["^wildwest/numbers.py.*"]}, + "strings_only": {"paths": ["^wildwest/strings.py.*"]}, + "default": True, + "one": {"flags": ["one"]}, + "two": {"flags": ["two"]}, + "three": {"flags": ["three"]}, + "four": {"flags": ["four"]}, + "five": {"flags": ["five"]}, + "six": {"flags": ["six"]}, + "seven": {"flags": ["seven"]}, + "eight": {"flags": ["eight"]}, + "nine": {"flags": ["nine"]}, + "ten": {"flags": ["ten"]}, + "eleven": {"flags": ["eleven"]}, + "one_without_t": {"flags": ["one"], "paths": ["^wildwest.*"]}, + "two_without_t": {"flags": ["two"], "paths": ["^wildwest.*"]}, + "three_without_t": { + "flags": ["three"], + "paths": ["^wildwest.*"], + }, + "four_without_t": {"flags": ["four"], "paths": ["^wildwest.*"]}, + "five_without_t": {"flags": ["five"], "paths": ["^wildwest.*"]}, + "six_without_t": {"flags": ["six"], "paths": ["^wildwest.*"]}, + "seven_without_t": { + "flags": ["seven"], + "paths": ["^wildwest.*"], + }, + "eight_without_t": { + "flags": ["eight"], + "paths": ["^wildwest.*"], + }, + "nine_without_t": {"flags": ["nine"], "paths": ["^wildwest.*"]}, + "ten_without_t": {"flags": ["ten"], "paths": ["^wildwest.*"]}, + "eleven_without_t": { + "flags": ["eleven"], + "paths": ["^wildwest.*"], + }, + }, + } + }, + "flag_management": { + "default_rules": { + "carryforward": False, + "statuses": [{"name_prefix": "aaa", "type": "patch"}], + }, + "individual_flags": [ + {"name": "cawcaw", "paths": ["^banana.*"], "after_n_builds": 3} + ], + }, + } + assert validate_yaml(user_input) == expected_result + + def test_validate_bot_none(self): + user_input = {"codecov": {"bot": None}} + expected_result = {"codecov": {"bot": None}} + result = validate_yaml(user_input) + assert result == expected_result + + def test_validate_flag_too_long(self): + user_input = {"flags": {"abcdefg" * 500: {"paths": ["banana"]}}} + with pytest.raises(InvalidYamlException) as exc: + validate_yaml(user_input) + assert exc.value.error_location == [ + "flags", + "abcdefg" * 500, + ] + + def test_validate_parser_only_field(self): + user_input = {"parsers": {"go": {"partials_as_hits": True}}} + expected_result = {"parsers": {"go": {"partials_as_hits": True}}} + result = validate_yaml(user_input) + assert result == expected_result + + def test_simple_case(self): + encoded_value = "secret:v1::zsV9A8pHadNle357DGJHbZCTyCYA+TXdUd9TN3IY2DIWcPOtgK3Pg1EgA6OZr9XJ1EsdpL765yWrN4pfR3elRdN2LUwiuv6RkNjpbiruHx45agsgxdu8fi24p5pkCLvjcW0HqdH2PTvmHauIp+ptgA==" + user_input = { + "coverage": { + "precision": 2, + "round": "down", + "range": "70...100", + "status": { + "project": { + "custom_project": { + "carryforward_behavior": "exclude", + "flag_coverage_not_uploaded_behavior": "exclude", + } + }, + "patch": True, + "changes": False, + "default_rules": { + "carryforward_behavior": "pass", + "flag_coverage_not_uploaded_behavior": "pass", + }, + "no_upload_behavior": "pass", + }, + "notify": {"irc": {"user_given_title": {"password": encoded_value}}}, + }, + "codecov": {"notify": {"require_ci_to_pass": True}}, + "comment": { + "behavior": "default", + "layout": "header, diff", + "require_changes": False, + "show_carryforward_flags": False, + }, + "parsers": { + "gcov": { + "branch_detection": { + "conditional": True, + "loop": True, + "macro": False, + "method": False, + } + }, + "jacoco": {"partials_as_hits": True}, + }, + "cli": { + "plugins": {"pycoverage": {"report_type": "json"}}, + }, + } + expected_result = { + "coverage": { + "precision": 2, + "round": "down", + "range": [70, 100], + "status": { + "project": { + "custom_project": { + "carryforward_behavior": "exclude", + "flag_coverage_not_uploaded_behavior": "exclude", + } + }, + "patch": True, + "changes": False, + "default_rules": { + "carryforward_behavior": "pass", + "flag_coverage_not_uploaded_behavior": "pass", + }, + "no_upload_behavior": "pass", + }, + "notify": {"irc": {"user_given_title": {"password": encoded_value}}}, + }, + "codecov": {"notify": {}, "require_ci_to_pass": True}, + "comment": { + "behavior": "default", + "layout": "header, diff", + "require_changes": [0b000], + "show_carryforward_flags": False, + }, + "parsers": { + "gcov": { + "branch_detection": { + "conditional": True, + "loop": True, + "macro": False, + "method": False, + } + }, + "jacoco": {"partials_as_hits": True}, + }, + "cli": { + "plugins": {"pycoverage": {"report_type": "json"}}, + }, + } + assert validate_yaml(user_input) == expected_result + + def test_negative_notify_after_n_builds(self): + user_input = {"codecov": {"notify": {"after_n_builds": -1}}} + with pytest.raises(InvalidYamlException) as exc: + validate_yaml(user_input) + assert exc.value.error_location == ["codecov", "notify", "after_n_builds"] + assert exc.value.error_message == "min value is 0" + + def test_positive_notify_after_n_builds(self): + user_input = {"codecov": {"notify": {"after_n_builds": 1}}} + res = validate_yaml(user_input) + assert res == {"codecov": {"notify": {"after_n_builds": 1}}} + + def test_negative_comments_after_n_builds(self): + user_input = {"comment": {"after_n_builds": -1}} + with pytest.raises(InvalidYamlException) as exc: + validate_yaml(user_input) + assert exc.value.error_location == ["comment", "after_n_builds"] + assert exc.value.error_message == "min value is 0" + + def test_invalid_yaml_case(self): + user_input = { + "coverage": { + "round": "down", + "precision": 2, + "range": "70...100", + "status": {"project": {"base": "auto", "aa": True}}, + }, + "ignore": ["Pods/.*"], + } + with pytest.raises(InvalidYamlException) as exc: + validate_yaml(user_input) + assert exc.value.error_location == ["coverage", "status", "project", "base"] + assert exc.value.error_message == "must be of ['dict', 'boolean'] type" + + def test_invalid_yaml_case_custom_validator(self): + user_input = { + "coverage": { + "round": "down", + "precision": 2, + "range": "70...5000", + "status": {"project": {"percent": "abc"}}, + }, + "ignore": ["Pods/.*"], + } + with pytest.raises(InvalidYamlException) as exc: + validate_yaml(user_input) + assert exc.value.error_location == ["coverage", "range"] + assert exc.value.error_message == "must be of list type" + + def test_invalid_yaml_case_no_upload_behavior(self): + user_input = { + "coverage": { + "round": "down", + "precision": 2, + "range": "70...100", + "status": { + "project": {"percent": "abc"}, + "no_upload_behavior": "no-pass", + }, + }, + "ignore": ["Pods/.*"], + } + with pytest.raises(InvalidYamlException) as exc: + validate_yaml(user_input) + assert exc.value.error_location == ["coverage", "status", "no_upload_behavior"] + assert exc.value.error_message == "unallowed value no-pass" + + def test_yaml_with_null_threshold(self): + user_input = { + "codecov": {"notify": {}, "require_ci_to_pass": True}, + "comment": { + "behavior": "default", + "branches": None, + "layout": "reach, diff, flags, files", + "require_base": False, + "require_changes": False, + "require_head": False, + }, + "coverage": { + "precision": 2, + "range": "50...80", + "round": "down", + "status": { + "changes": False, + "patch": True, + "project": { + "default": {"target": "auto", "threshold": None, "base": "auto"} + }, + }, + }, + } + res = validate_yaml(user_input) + expected_result = { + "codecov": {"notify": {}, "require_ci_to_pass": True}, + "comment": { + "behavior": "default", + "branches": None, + "layout": "reach, diff, flags, files", + "require_base": False, + "require_changes": [0b000], + "require_head": False, + }, + "coverage": { + "precision": 2, + "range": [50.0, 80.0], + "round": "down", + "status": { + "changes": False, + "patch": True, + "project": { + "default": {"target": "auto", "threshold": None, "base": "auto"} + }, + }, + }, + } + assert res == expected_result + + def test_yaml_with_status_case(self): + user_input = { + "coverage": { + "round": "down", + "precision": 2, + "range": "70...100", + "status": {"project": {"default": {"base": "auto"}}}, + }, + "ignore": ["Pods/.*"], + } + expected_result = { + "coverage": { + "round": "down", + "precision": 2, + "range": [70.0, 100.0], + "status": {"project": {"default": {"base": "auto"}}}, + }, + "ignore": ["Pods/.*"], + } + result = validate_yaml(user_input) + assert result == expected_result + + def test_yaml_with_flag_management(self): + user_input = { + "flag_management": { + "default_rules": { + "carryforward": True, + "statuses": [ + { + "type": "project", + "name_prefix": "healthcare", + "threshold": 80, + } + ], + }, + "individual_flags": [ + { + "name": "flag_banana", + "statuses": [ + { + "type": "patch", + "name_prefix": "alliance", + "flag_coverage_not_uploaded_behavior": "include", + } + ], + } + ], + } + } + expected_result = { + "flag_management": { + "individual_flags": [ + { + "name": "flag_banana", + "statuses": [ + { + "type": "patch", + "name_prefix": "alliance", + "flag_coverage_not_uploaded_behavior": "include", + } + ], + } + ], + "default_rules": { + "carryforward": True, + "statuses": [ + { + "type": "project", + "name_prefix": "healthcare", + "threshold": 80.0, + } + ], + }, + } + } + result = validate_yaml(user_input) + assert result == expected_result + + def test_yaml_with_flag_management_statuses_with_flags(self): + user_input = { + "flag_management": { + "default_rules": { + "carryforward": True, + "statuses": [ + { + "type": "project", + "name_prefix": "healthcare", + "threshold": 80, + "flags": ["hahaha"], + } + ], + }, + "individual_flags": [ + { + "name": "flag_banana", + "statuses": [ + { + "type": "patch", + "name_prefix": "alliance", + "flag_coverage_not_uploaded_behavior": "include", + } + ], + } + ], + } + } + with pytest.raises(InvalidYamlException) as exc: + validate_yaml(user_input) + assert exc.value.error_location == [ + "flag_management", + "default_rules", + "statuses", + 0, + "flags", + ] + assert exc.value.error_message == "extra keys not allowed" + + def test_github_checks(self): + user_input = {"github_checks": True} + expected_result = {"github_checks": True} + assert validate_yaml(user_input) == expected_result + user_input = {"github_checks": {"annotations": False}} + expected_result = {"github_checks": {"annotations": False}} + assert validate_yaml(user_input) == expected_result + + def test_validate_jacoco_partials(self): + user_input = {"parsers": {"jacoco": {"partials_as_hits": True}}} + expected_result = {"parsers": {"jacoco": {"partials_as_hits": True}}} + result = validate_yaml(user_input) + assert result == expected_result + + @pytest.mark.parametrize( + "input, expected", + [ + pytest.param( + {"comment": {"require_bundle_changes": False}}, + {"comment": {"require_bundle_changes": False}}, + id="no_bundle_changes_required", + ), + pytest.param( + { + "comment": { + "require_bundle_changes": True, + "bundle_change_threshold": 1200, + } + }, + { + "comment": { + "require_bundle_changes": True, + "bundle_change_threshold": ("absolute", 1200), + } + }, + id="bundle_changes_with_threshold", + ), + pytest.param( + { + "comment": { + "require_bundle_changes": "bundle_increase", + "bundle_change_threshold": "1mb", + } + }, + { + "comment": { + "require_bundle_changes": "bundle_increase", + "bundle_change_threshold": ("absolute", 1000000), + } + }, + id="bundle_increase_required_with_threshold", + ), + pytest.param( + { + "comment": { + "require_bundle_changes": "bundle_increase", + "bundle_change_threshold": "10%", + } + }, + { + "comment": { + "require_bundle_changes": "bundle_increase", + "bundle_change_threshold": ("percentage", 10.0), + } + }, + id="bundle_increase_required_with_percentage_threshold", + ), + ], + ) + def test_bundle_analysis_comment_config(self, input, expected, mocker): + mocker.patch.object(BUNDLE_THRESHOLD_FLAG, "check_value", return_value=True) + result = validate_yaml(input) + assert result == expected + + @pytest.mark.parametrize( + "input, expected", + [ + pytest.param( + { + "bundle_analysis": { + "status": False, + "warning_threshold": "10%", + } + }, + { + "bundle_analysis": { + "status": False, + "warning_threshold": ("percentage", 10.0), + } + }, + id="status_off_percentage_threshold", + ), + pytest.param( + { + "bundle_analysis": { + "status": True, + "warning_threshold": "10kb", + } + }, + { + "bundle_analysis": { + "status": True, + "warning_threshold": ("absolute", 10000), + } + }, + id="status_on_absolute_threshold", + ), + pytest.param( + { + "bundle_analysis": { + "status": "informational", + } + }, + { + "bundle_analysis": { + "status": "informational", + } + }, + id="status_informational_no_threshold", + ), + ], + ) + def test_bundle_analysis_config(self, input, expected, mocker): + mocker.patch.object(BUNDLE_THRESHOLD_FLAG, "check_value", return_value=True) + result = validate_yaml(input) + assert result == expected + + +class TestValidationConfig(object): + def test_validate_default_config_yaml(self, mocker): + mocker.patch.dict(os.environ, {}, clear=True) + mocker.patch.object( + ConfigHelper, "load_yaml_file", side_effect=FileNotFoundError() + ) + this_config = ConfigHelper() + mocker.patch("shared.config._get_config_instance", return_value=this_config) + expected_result = { + "codecov": {"require_ci_to_pass": True, "notify": {"wait_for_ci": True}}, + "coverage": { + "precision": 2, + "round": "down", + "range": [60.0, 80.0], + "status": { + "project": True, + "patch": True, + "changes": False, + "default_rules": {"flag_coverage_not_uploaded_behavior": "include"}, + }, + }, + "comment": { + "layout": "reach,diff,flags,tree,reach", + "behavior": "default", + "show_carryforward_flags": False, + }, + "github_checks": {"annotations": True}, + "slack_app": True, + } + res = validate_yaml( + get_config("site", default={}), + show_secrets_for=("github", "11934774", "154468867"), + ) + assert res == expected_result + + +def test_validation_with_branches(): + user_input = { + "comment": { + "require_head": True, + "require_base": True, + "layout": "diff", + "require_changes": True, + "branches": ["main"], + "behavior": "once", + "after_n_builds": 6, + }, + "coverage": { + "status": {"project": {"default": {"threshold": "1%"}}, "patch": False} + }, + "github_checks": {"annotations": False}, + "fixes": [ + "/opt/conda/lib/python3.8/site-packages/::project/", + "C:/Users/circleci/project/build/win_tmp/build/::project/", + ], + "ignore": ["coffee", "party_man", "test"], + "codecov": {"notify": {"after_n_builds": 6}}, + } + expected_result = { + "comment": { + "require_head": True, + "require_base": True, + "layout": "diff", + "require_changes": [0b001], + "branches": ["^main$"], + "behavior": "once", + "after_n_builds": 6, + }, + "coverage": { + "status": {"project": {"default": {"threshold": 1}}, "patch": False} + }, + "github_checks": {"annotations": False}, + "fixes": [ + "^/opt/conda/lib/python3.8/site-packages/::project/", + "^C:/Users/circleci/project/build/win_tmp/build/::project/", + ], + "ignore": ["^coffee.*", "^party_man.*", "^test.*"], + "codecov": {"notify": {"after_n_builds": 6}}, + } + res = do_actual_validation(user_input, show_secrets_for=None) + assert res == expected_result + + +def test_validation_with_flag_carryforward(): + user_input = { + "flags": { + "old-flag": { + "carryforward": True, + "carryforward_mode": "labels", + }, + "other-old-flag": { + "carryforward": True, + "carryforward_mode": "all", + }, + }, + "flag_management": { + "individual_flags": [ + {"name": "abcdef", "carryforward_mode": "all"}, + {"name": "abcdef", "carryforward_mode": "labels"}, + ] + }, + } + assert do_actual_validation(user_input, show_secrets_for=None) == user_input + + +def test_validation_with_flag_carryforward_invalid_mode(): + user_input = { + "flag_management": { + "individual_flags": [ + {"name": "abcdef", "carryforward_mode": "mario"}, + {"name": "abcdef", "carryforward_mode": "labels"}, + ] + }, + } + with pytest.raises(InvalidYamlException) as exp: + do_actual_validation(user_input, show_secrets_for=None) + assert exp.value.error_dict == { + "flag_management": [ + { + "individual_flags": [ + {0: [{"carryforward_mode": ["unallowed value mario"]}]} + ] + } + ] + } + + +def test_validation_with_null_on_paths(): + user_input = { + "comment": {"require_head": True, "behavior": "once", "after_n_builds": 6}, + "coverage": { + "status": {"project": {"default": {"threshold": "1%"}}, "patch": False}, + "notify": {"slack": {"default": {"paths": None}}}, + }, + "ignore": ["coffee", "test"], + } + expected_result = { + "comment": {"require_head": True, "behavior": "once", "after_n_builds": 6}, + "coverage": { + "status": {"project": {"default": {"threshold": 1.0}}, "patch": False}, + "notify": {"slack": {"default": {"paths": None}}}, + }, + "ignore": ["^coffee.*", "^test.*"], + } + res = do_actual_validation(user_input, show_secrets_for=None) + assert res == expected_result + + +def test_validation_with_null_on_status(): + user_input = { + "coverage": {"status": {"project": {"default": None}, "patch": False}}, + "ignore": ["coffee", "test"], + } + expected_result = { + "coverage": {"status": {"project": {"default": None}, "patch": False}}, + "ignore": ["^coffee.*", "^test.*"], + } + res = do_actual_validation(user_input, show_secrets_for=None) + assert res == expected_result + + +def test_improper_layout(): + user_input = { + "coverage": {"status": {"project": {"default": None}, "patch": False}}, + "comment": {"layout": "banana,apple"}, + } + with pytest.raises(InvalidYamlException) as exc: + do_actual_validation(user_input, show_secrets_for=None) + assert exc.value.error_dict == { + "comment": [{"layout": ["Unexpected values on layout: apple,banana"]}] + } + assert exc.value.error_location == ["comment", "layout"] + + +def test_proper_layout(): + user_input = { + "coverage": {"status": {"project": {"default": None}, "patch": False}}, + "comment": {"layout": "files:10,footer"}, + } + res = do_actual_validation(user_input, show_secrets_for=None) + assert res == { + "coverage": {"status": {"project": {"default": None}, "patch": False}}, + "comment": {"layout": "files:10,footer"}, + } + + +def test_codecov_branch(): + user_input = {"codecov": {"branch": "origin/pterosaur"}} + res = do_actual_validation(user_input, show_secrets_for=None) + assert res == {"codecov": {"branch": "pterosaur"}} + + +def test_calculate_error_location_and_message_from_error_dict(): + error_dict = {"comment": [{"layout": {"deep": [[[[[{"inside": [["value"]]}]]]]]}}]} + assert ( + ["comment", "layout", "deep", "inside"], + "value", + ) == _calculate_error_location_and_message_from_error_dict(error_dict) + # case where the value is just very nested. + # This is not a requirement of any kind. This is just so + # there are no cases where a customer can send some special yaml with loops + # and make us keep parsing this forever + # It might even be overkill + assert ( + ["value", "some", "thing"], + "[[['haha']]]", + ) == _calculate_error_location_and_message_from_error_dict( + {"value": {"some": {"thing": [[[[[[[[[[[[[[[[[[[["haha"]]]]]]]]]]]]]]]]]]]]}}} + ) + + +def test_email_field_with_and_without_secret(): + user_input = { + "coverage": { + "notify": { + "email": { + "default": { + "to": [ + "example@domain.com", + "secret:v1::hfxSizNpugwZzYZXXRxxlszUetU8tyVG1HXCEdK5qeC9XhtxkCsb/Z5nvp70mp4zlbfcinTy9C9lSGXZAmN8uGKuhWnwrFPfYupe7jQ5KQY=", + ], + "threshold": "1%", + "only_pulls": False, + "layout": "reach, diff, flags", + "flags": None, + "paths": None, + } + } + } + } + } + assert do_actual_validation( + user_input, show_secrets_for=("github", "11934774", "154468867") + ) == { + "coverage": { + "notify": { + "email": { + "default": { + "to": ["example@domain.com", "secondexample@seconddomain.com"], + "threshold": 1.0, + "only_pulls": False, + "layout": "reach, diff, flags", + "flags": None, + "paths": None, + } + } + } + } + } + assert do_actual_validation(user_input, show_secrets_for=None) == { + "coverage": { + "notify": { + "email": { + "default": { + "to": [ + "example@domain.com", + "secret:v1::hfxSizNpugwZzYZXXRxxlszUetU8tyVG1HXCEdK5qeC9XhtxkCsb/Z5nvp70mp4zlbfcinTy9C9lSGXZAmN8uGKuhWnwrFPfYupe7jQ5KQY=", + ], + "threshold": 1.0, + "only_pulls": False, + "layout": "reach, diff, flags", + "flags": None, + "paths": None, + } + } + } + } + } + + +def test_assume_flags(): + # It's deprecated, but still + user_input = {"flags": {"some_flag": {"assume": {"branches": ["main"]}}}} + assert do_actual_validation( + user_input, show_secrets_for=("github", "11934774", "154468867") + ) == {"flags": {"some_flag": {"assume": {"branches": ["^main$"]}}}} + + +def test_after_n_builds_flags(): + user_input = {"flags": {"some_flag": {"after_n_builds": 5}}} + assert do_actual_validation( + user_input, show_secrets_for=("github", "11934774", "154468867") + ) == {"flags": {"some_flag": {"after_n_builds": 5}}} + + +def test_profiling_schema(): + user_input = { + "profiling": { + "fixes": ["batata_something::batata.txt"], + "grouping_attributes": ["string", "str"], + "critical_files_paths": [ + "/path/to/file.extension", + "/path/to/dir", + r"/path/{src|bin}/regex.{txt|php|cpp}", + "/path/using/globs/**/file.extension", + ], + } + } + expected_result = { + "profiling": { + "fixes": ["^batata_something::batata.txt"], + "grouping_attributes": ["string", "str"], + "critical_files_paths": [ + "^/path/to/file.extension.*", + "^/path/to/dir.*", + "^/path/{src|bin}/regex.{txt|php|cpp}.*", + "(?s:/path/using/globs/.*/file\\.extension)\\Z", + ], + } + } + result = validate_yaml(user_input) + assert result == expected_result + + +def test_components_schema(): + user_input = { + "component_management": { + "default_rules": { + "flag_regexes": ["global_flag"], + }, + "individual_components": [ + { + "name": "fruits", + "component_id": "app_0", + "flag_regexes": ["fruit_.*", "^specific_flag$"], + "paths": ["src/.*"], + "statuses": [{"type": "patch", "name_prefix": "co", "target": 90}], + } + ], + } + } + expected = { + "component_management": { + "default_rules": { + "flag_regexes": ["global_flag"], + }, + "individual_components": [ + { + "name": "fruits", + "component_id": "app_0", + "flag_regexes": ["fruit_.*", "^specific_flag$"], + "paths": ["src/.*"], + "statuses": [ + {"type": "patch", "name_prefix": "co", "target": 90.0} + ], + } + ], + } + } + result = validate_yaml(user_input) + assert result == expected + + +def test_components_schema_error(): + user_input = { + "component_management": { + "individual_components": [ + { + "key": "extra", + "component_id": "app_0", + "flag_regexes": ["fruit_*", "^specific_flag$"], + "path_filter_regexes": ["src/.*"], + "statuses": [ + {"type": "patch", "name_prefix": "co", "target": 90.0} + ], + }, + { + "component_id": "app_0", + "flag_regexes": ["fruit_*", "^specific_flag$"], + "path_filter_regexes": ["src/.*"], + }, + ], + } + } + with pytest.raises(InvalidYamlException) as exp: + validate_yaml(user_input) + assert exp.error_location == [ + "component_management", + "individual_components", + 0, + "key", + ] + assert exp.error_message == "unknown field" + assert exp.error_dict == { + "component_management": [ + { + "individual_components": [ + { + 0: [{"key": ["unknown field"]}], + 1: [{"component_id": ["required field"]}], + } + ] + } + ] + } + + +def test_removed_code_behavior_config_valid(): + user_input = { + "coverage": { + "status": { + "project": { + "some_status": {"removed_code_behavior": "removals_only"}, + } + } + }, + "flag_management": { + "default_rules": { + "statuses": [ + {"name_prefix": "custom", "removed_code_behavior": "adjust_base"} + ] + }, + "individual_flags": [ + { + "name": "random", + "statuses": [ + { + "name_prefix": "random-custom", + "removed_code_behavior": False, + } + ], + } + ], + }, + "component_management": { + "default_rules": { + "statuses": [ + { + "name_prefix": "custom", + "removed_code_behavior": "fully_covered_patch", + } + ] + }, + "individual_components": [ + { + "component_id": "random", + "statuses": [ + { + "name_prefix": "random-custom", + "removed_code_behavior": "off", + } + ], + } + ], + }, + } + result = validate_yaml(user_input) + # There's no change on the valid yaml + assert result == user_input + + +def test_offset_config_error(): + user_input = { + "flag_management": { + "default_rules": { + "statuses": [ + {"name_prefix": "custom", "removed_code_behavior": "banana"} + ] + } + }, + } + + with pytest.raises(InvalidYamlException) as exp: + validate_yaml(user_input) + assert exp.error_dict == { + "coverage": [ + { + "status": [ + {"patch": [{"some_status": [{"offset": ["unknown field"]}]}]} + ] + } + ], + "flag_management": [ + { + "default_rules": [ + {"statuses": [{0: [{"offset": ["unallowed value banana"]}]}]} + ] + } + ], + } + + +def test_cli_validation(): + user_input = { + "cli": { + "plugins": {"pycoverage": {"report_type": "json"}}, + "runners": { + "custom_runner": { + "module": "my_project.runner", + "class": "MyCustomRunner", + "params": {"randseed": 0}, + } + }, + } + } + result = validate_yaml(user_input) + # There's no change on the valid yaml + assert result == user_input + + +def test_slack_app_validation(): + user_input = {"slack_app": {"enabled": True}} + result = validate_yaml(user_input) + assert result == user_input + + +def test_slack_app_validation_boolean(): + user_input = {"slack_app": True} + result = validate_yaml(user_input) + assert result == user_input + + +def test_to_string_validation(): + user_input = {"to_string": {"abc": 123}} + expected_result = {} + result = validate_yaml(user_input) + assert result == expected_result diff --git a/libs/shared/tests/unit/yaml/test_fetcher.py b/libs/shared/tests/unit/yaml/test_fetcher.py new file mode 100644 index 0000000000..c4f6003dec --- /dev/null +++ b/libs/shared/tests/unit/yaml/test_fetcher.py @@ -0,0 +1,207 @@ +import mock +import pytest + +from shared.torngit.exceptions import TorngitObjectNotFoundError +from shared.yaml import ( + determine_commit_yaml_location, + fetch_current_yaml_from_provider_via_reference, +) + +sample_yaml = """ +codecov: + notify: + require_ci_to_pass: yes +""" + +commitid = "e1ade" + + +class TestYamlSavingService(object): + @pytest.mark.asyncio + async def test_determine_commit_yaml_location(self, mocker): + mocked_result = [ + {"name": ".gitignore", "path": ".gitignore", "type": "file"}, + {"name": ".travis.yml", "path": ".travis.yml", "type": "file"}, + {"name": "README.rst", "path": "README.rst", "type": "file"}, + {"name": "awesome", "path": "awesome", "type": "folder"}, + {"name": "codecov", "path": "codecov", "type": "file"}, + {"name": "codecov.yaml", "path": "codecov.yaml", "type": "file"}, + {"name": "tests", "path": "tests", "type": "folder"}, + ] + f = mocked_result + valid_handler = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=f) + ) + res = await determine_commit_yaml_location(commitid, valid_handler) + assert res == "codecov.yaml" + + @pytest.mark.asyncio + async def test_determine_commit_yaml_location_no_yaml(self, mocker): + mocked_result = [ + {"name": ".gitignore", "path": ".gitignore", "type": "file"}, + {"name": ".travis.yml", "path": ".travis.yml", "type": "file"}, + {"name": "README.rst", "path": "README.rst", "type": "file"}, + {"name": "awesome", "path": "awesome", "type": "folder"}, + {"name": "tests", "path": "tests", "type": "folder"}, + ] + f = mocked_result + valid_handler = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=f) + ) + res = await determine_commit_yaml_location(commitid, valid_handler) + assert res is None + + @pytest.mark.asyncio + async def test_determine_commit_yaml_location_no_name(self, mocker): + mocked_result = [ + {"path": ".gitignore", "type": "file"}, + {"path": ".travis.yml", "type": "file"}, + {"path": "README.rst", "type": "file"}, + {"path": "awesome", "type": "folder"}, + {"path": "codecov", "type": "file"}, + {"path": "codecov.yaml", "type": "file"}, + {"path": "tests", "type": "folder"}, + ] + f = mocked_result + valid_handler = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=f) + ) + res = await determine_commit_yaml_location(commitid, valid_handler) + assert res == "codecov.yaml" + + @pytest.mark.asyncio + async def test_determine_commit_yaml_nested_folder(self, mocker): + mocked_result = [ + {"name": ".gitignore", "path": ".gitignore", "type": "file"}, + {"name": ".travis.yml", "path": ".travis.yml", "type": "file"}, + {"name": "README.rst", "path": "README.rst", "type": "file"}, + {"name": ".github", "path": ".github", "type": "folder"}, + {"name": "codecov", "path": "codecov", "type": "file"}, + {"name": "tests", "path": "tests", "type": "folder"}, + ] + files_inside_folder = [ + {"name": "code.py", "path": ".github/code.py", "type": "file"}, + {"name": "__init__.py", "path": ".github/__init__.py", "type": "file"}, + { + "name": "anotha_folder", + "path": ".github/anotha_folder", + "type": "folder", + }, + {"name": "codecov", "path": ".github/codecov", "type": "folder"}, + {"name": "codecov.yaml", "path": ".github/codecov.yaml", "type": "file"}, + ] + f = mocked_result + list_file_future = files_inside_folder + valid_handler = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=f), + list_files=mock.AsyncMock(return_value=list_file_future), + ) + res = await determine_commit_yaml_location(commitid, valid_handler) + assert res == ".github/codecov.yaml" + + @pytest.mark.asyncio + async def test_determine_commit_yaml_nested_folder_noname(self, mocker): + mocked_result = [ + {"path": ".gitignore", "type": "file"}, + {"path": ".travis.yml", "type": "file"}, + {"path": "README.rst", "type": "file"}, + {"path": ".github", "type": "folder"}, + {"path": "codecov", "type": "file"}, + {"path": "tests", "type": "folder"}, + ] + files_inside_folder = [ + {"path": ".github/code.py", "type": "file"}, + {"path": ".github/__init__.py", "type": "file"}, + {"path": ".github/anotha_folder", "type": "folder"}, + {"path": ".github/codecov", "type": "folder"}, + {"path": ".github/codecov.yaml", "type": "file"}, + ] + f = mocked_result + list_file_future = files_inside_folder + valid_handler = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=f), + list_files=mock.AsyncMock(return_value=list_file_future), + ) + res = await determine_commit_yaml_location(commitid, valid_handler) + assert res == ".github/codecov.yaml" + + @pytest.mark.asyncio + async def test_determine_commit_yaml_location_multiple(self, mocker): + mocked_result = [ + {"name": "READMEs", "path": "README.rst", "type": "folder"}, + {"name": "codecov.yml", "path": "codecov.yml", "type": "file"}, + {"name": ".codecov.yml", "path": ".codecov.yml", "type": "file"}, + {"name": "codecov.yaml", "path": "codecov.yaml", "type": "file"}, + {"name": "tests", "path": "tests", "type": "folder"}, + ] + f = mocked_result + valid_handler = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=f) + ) + res = await determine_commit_yaml_location(commitid, valid_handler) + assert res == "codecov.yml" + + @pytest.mark.asyncio + async def test_fetch_commit_yaml_from_provider(self, mocker): + mocked_list_files_result = [ + {"name": ".gitignore", "path": ".gitignore", "type": "file"}, + {"name": ".travis.yml", "path": ".travis.yml", "type": "file"}, + {"name": "README.rst", "path": "README.rst", "type": "file"}, + {"name": "awesome", "path": "awesome", "type": "folder"}, + {"name": "codecov", "path": "codecov", "type": "file"}, + {"name": "codecov.yaml", "path": "codecov.yaml", "type": "file"}, + {"name": "tests", "path": "tests", "type": "folder"}, + ] + list_files_future = mocked_list_files_result + contents_result = {"content": sample_yaml} + contents_result_future = contents_result + valid_handler = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=list_files_future), + get_source=mock.AsyncMock(return_value=contents_result_future), + ) + res = await fetch_current_yaml_from_provider_via_reference( + commitid, valid_handler + ) + assert res == sample_yaml + valid_handler.list_top_level_files.assert_called_with(commitid) + valid_handler.get_source.assert_called_with("codecov.yaml", commitid) + + @pytest.mark.asyncio + async def test_fetch_commit_yaml_from_no_yaml(self, mocker): + mocked_list_files_result = [] + list_files_future = mocked_list_files_result + contents_result = {"content": sample_yaml} + contents_result_future = contents_result + valid_handler = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=list_files_future), + get_source=mock.AsyncMock(return_value=contents_result_future), + ) + res = await fetch_current_yaml_from_provider_via_reference( + commitid, valid_handler + ) + assert res is None + valid_handler.list_top_level_files.assert_called_with(commitid) + + @pytest.mark.asyncio + async def test_fetch_commit_yaml_from_no_yaml_on_provider(self, mocker): + mocked_list_files_result = [ + {"name": ".gitignore", "path": ".gitignore", "type": "file"}, + {"name": ".travis.yml", "path": ".travis.yml", "type": "file"}, + {"name": "README.rst", "path": "README.rst", "type": "file"}, + {"name": "awesome", "path": "awesome", "type": "folder"}, + {"name": "codecov", "path": "codecov", "type": "file"}, + {"name": "codecov.yaml", "path": "codecov.yaml", "type": "file"}, + {"name": "tests", "path": "tests", "type": "folder"}, + ] + list_files_future = mocked_list_files_result + exception_get_source = TorngitObjectNotFoundError("not found", ":(") + valid_handler = mocker.MagicMock( + list_top_level_files=mock.AsyncMock(return_value=list_files_future), + get_source=mock.AsyncMock(side_effect=exception_get_source), + ) + res = await fetch_current_yaml_from_provider_via_reference( + commitid, valid_handler + ) + assert res is None + valid_handler.list_top_level_files.assert_called_with(commitid) + valid_handler.get_source.assert_called_with("codecov.yaml", commitid) diff --git a/libs/shared/tests/unit/yaml/test_user_yaml.py b/libs/shared/tests/unit/yaml/test_user_yaml.py new file mode 100644 index 0000000000..3a3140eeb0 --- /dev/null +++ b/libs/shared/tests/unit/yaml/test_user_yaml.py @@ -0,0 +1,549 @@ +import datetime + +from freezegun import freeze_time + +from shared.components import Component +from shared.config import ( + LEGACY_DEFAULT_SITE_CONFIG, + PATCH_CENTRIC_DEFAULT_CONFIG, + PATCH_CENTRIC_DEFAULT_TIME_START, +) +from shared.yaml import UserYaml, merge_yamls +from shared.yaml.user_yaml import ( + OwnerContext, + _fix_yaml_defaults_based_on_owner_onboarding_date, + _get_possible_additional_user_yaml, +) + + +class TestYamlMerge(object): + def test_merge_yamls(self): + d1 = { + "key_one": "value", + "key_two": {"sub": "p"}, + "key_three": "super", + "key_four": "pro", + } + d2 = {"key_two": "textnow", "key_three": "super", "key_four": "mega"} + first_expected_result = { + "key_four": "mega", + "key_one": "value", + "key_three": "super", + "key_two": "textnow", + } + assert first_expected_result == merge_yamls(d1, d2) + second_expected_result = { + "key_four": "pro", + "key_one": "value", + "key_three": "super", + "key_two": {"sub": "p"}, + } + assert second_expected_result == merge_yamls(d2, d1) + + +class TestUserYaml(object): + def test_init(self): + d = {"value": "sample"} + v = UserYaml(d) + assert v is not None + assert v["value"] == "sample" + assert str(v) == "UserYaml<{'value': 'sample'}>" + + def test_from_dict_to_dict(self): + d = {"value": "sample"} + v = UserYaml.from_dict(d) + assert v is not None + assert v["value"] == "sample" + assert v.get("value") == "sample" + assert v.get("notthere") is None + assert v.get("hshshshsh", "p") == "p" + dicted = v.to_dict() + assert dicted == d + assert dicted is not d + dicted["l"] = "banana" + assert d == {"value": "sample"} + assert dicted == {"value": "sample", "l": "banana"} + + def test_eq(self): + d1 = { + "key_one": "value", + "key_two": {"sub": "p"}, + "key_three": "super", + "key_four": "pro", + } + d2 = {"key_two": "textnow", "key_three": "super", "key_four": "mega"} + assert UserYaml(d1) == UserYaml(d1) + assert UserYaml(d1) != UserYaml(d2) + assert UserYaml(d1) != d1 + + def test_read_yaml_field(self): + my_dict = { + "key_one": "value", + "key_two": {"sub": "p"}, + "key_three": "super", + "key_four": "pro", + } + subject = UserYaml(my_dict) + assert subject.read_yaml_field("key_two", "sub") == "p" + assert subject.read_yaml_field("key_two", "kowabunga", _else=23) == 23 + + def test_has_any_carryforward(self): + assert UserYaml( + {"flags": {"banana": {"carryforward": True}}, "flag_management": {}} + ).has_any_carryforward() + assert not UserYaml( + {"flags": {"banana": {"carryforward": False}}, "flag_management": {}} + ).has_any_carryforward() + assert UserYaml( + { + "flags": {"banana": {"carryforward": False}}, + "flag_management": {"default_rules": {"carryforward": True}}, + } + ).has_any_carryforward() + assert UserYaml( + { + "flags": {"banana": {"carryforward": False}}, + "flag_management": { + "default_rules": {"carryforward": False}, + "individual_flags": [{"name": "strawberry", "carryforward": True}], + }, + } + ).has_any_carryforward() + + def test_flag_has_carryfoward(self): + assert UserYaml( + {"flags": {"banana": {"carryforward": True}}, "flag_management": {}} + ).flag_has_carryfoward("banana") + assert not UserYaml( + {"flags": {"banana": {"carryforward": False}}, "flag_management": {}} + ).flag_has_carryfoward("banana") + subject = UserYaml( + { + "flags": {"banana": {"carryforward": False}}, + "flag_management": {"default_rules": {"carryforward": True}}, + } + ) + assert not subject.flag_has_carryfoward("banana") + assert subject.flag_has_carryfoward("strawberry") + subject_2 = UserYaml( + { + "flags": {"banana": {"carryforward": False}}, + "flag_management": { + "default_rules": {"carryforward": False}, + "individual_flags": [{"name": "strawberry", "carryforward": True}], + }, + } + ) + assert not subject_2.flag_has_carryfoward("banana") + assert subject_2.flag_has_carryfoward("strawberry") + assert not subject_2.flag_has_carryfoward("pineapple") + + def test_get_flag_configuration(self): + old_style = UserYaml({"flags": {"banana": {"key_one": True}}}) + assert old_style.get_flag_configuration("banana") == {"key_one": True} + assert old_style.get_flag_configuration("pineapple") is None + assert UserYaml( + {"flags": {"banana": {"key_one": True}}, "flag_management": {}} + ).get_flag_configuration("banana") == {"key_one": True} + assert UserYaml( + { + "flag_management": { + "default_rules": {"key_one": False, "key_two": "something"} + } + } + ).get_flag_configuration("banana") == {"key_one": False, "key_two": "something"} + subject = UserYaml( + { + "flags": {"banana": {"carryforward": False}}, + "flag_management": { + "default_rules": {"key_one": False, "key_two": "something"} + }, + } + ) + assert subject.get_flag_configuration("banana") == {"carryforward": False} + assert subject.get_flag_configuration("strawberry") == { + "key_one": False, + "key_two": "something", + } + subject_2 = UserYaml( + { + "flags": {"banana": {"carryforward": False}}, + "flag_management": { + "default_rules": {"key_one": False, "key_two": "something"}, + "individual_flags": [ + { + "name": "strawberry", + "key_one": True, + "key_three": ["array", "values"], + } + ], + }, + } + ) + assert subject_2.get_flag_configuration("banana") == {"carryforward": False} + assert subject_2.get_flag_configuration("strawberry") == { + "key_one": True, + "key_three": ["array", "values"], + "key_two": "something", + "name": "strawberry", + } + assert subject_2.get_flag_configuration("pineapple") == { + "key_one": False, + "key_two": "something", + } + + def test_get_final_yaml(self, mock_configuration): + mock_configuration._params["site"] = {"codecov": {"max_report_age": 86400}} + owner_yaml = {"key": {"value": "one", "a": "b"}} + repo_yaml = {"barber": "shop"} + commit_yaml = {"key": {"value": "two", "c": "d"}} + expected_result = { + "codecov": {"max_report_age": 86400}, + "key": {"a": "b", "c": "d", "value": "two"}, + } + assert ( + UserYaml.get_final_yaml( + owner_yaml=owner_yaml, repo_yaml=repo_yaml, commit_yaml=commit_yaml + ).to_dict() + == expected_result + ) + + def test_get_final_yaml_with_additional_user_yaml(self, mock_configuration): + mock_configuration._params["site"] = {"codecov": {"max_report_age": 86400}} + mock_configuration._params["additional_user_yamls"] = [ + {"percentage": 10, "name": "banana", "override": {"a": 2, "b": 3}}, + {"percentage": 30, "name": "apple", "override": {"d": "klmnop", "b": 3}}, + ] + owner_yaml = {"key": {"value": "one", "a": "b"}} + repo_yaml = {"barber": "shop"} + commit_yaml = {"key": {"value": "two", "c": "d"}} + assert UserYaml.get_final_yaml( + owner_yaml=owner_yaml, repo_yaml=repo_yaml, commit_yaml=commit_yaml + ).to_dict() == { + "codecov": {"max_report_age": 86400}, + "key": {"a": "b", "c": "d", "value": "two"}, + } + assert UserYaml.get_final_yaml( + owner_yaml=owner_yaml, + repo_yaml=repo_yaml, + commit_yaml=commit_yaml, + ownerid=100, + ).to_dict() == { + "codecov": {"max_report_age": 86400}, + "key": {"a": "b", "c": "d", "value": "two"}, + "a": 2, + "b": 3, + } + assert UserYaml.get_final_yaml( + owner_yaml=owner_yaml, + repo_yaml=repo_yaml, + commit_yaml=commit_yaml, + ownerid=121, + ).to_dict() == { + "codecov": {"max_report_age": 86400}, + "key": {"a": "b", "c": "d", "value": "two"}, + "b": 3, + "d": "klmnop", + } + assert UserYaml.get_final_yaml( + owner_yaml=owner_yaml, + repo_yaml=repo_yaml, + commit_yaml=commit_yaml, + ownerid=140, + ).to_dict() == { + "codecov": {"max_report_age": 86400}, + "key": {"a": "b", "c": "d", "value": "two"}, + } + + def test_get_final_yaml_with_additional_user_yaml_via_ownercontext( + self, mock_configuration + ): + mock_configuration._params["site"] = {"codecov": {"max_report_age": 86400}} + mock_configuration._params["additional_user_yamls"] = [ + {"percentage": 10, "name": "banana", "override": {"a": 2, "b": 3}}, + {"percentage": 30, "name": "apple", "override": {"d": "klmnop", "b": 3}}, + ] + owner_yaml = {"key": {"value": "one", "a": "b"}} + repo_yaml = {"barber": "shop"} + commit_yaml = {"key": {"value": "two", "c": "d"}} + assert UserYaml.get_final_yaml( + owner_yaml=owner_yaml, repo_yaml=repo_yaml, commit_yaml=commit_yaml + ).to_dict() == { + "codecov": {"max_report_age": 86400}, + "key": {"a": "b", "c": "d", "value": "two"}, + } + assert UserYaml.get_final_yaml( + owner_yaml=owner_yaml, + repo_yaml=repo_yaml, + commit_yaml=commit_yaml, + owner_context=OwnerContext(ownerid=100), + ).to_dict() == { + "codecov": {"max_report_age": 86400}, + "key": {"a": "b", "c": "d", "value": "two"}, + "a": 2, + "b": 3, + } + assert UserYaml.get_final_yaml( + owner_yaml=owner_yaml, + repo_yaml=repo_yaml, + commit_yaml=commit_yaml, + owner_context=OwnerContext(ownerid=121), + ).to_dict() == { + "codecov": {"max_report_age": 86400}, + "key": {"a": "b", "c": "d", "value": "two"}, + "b": 3, + "d": "klmnop", + } + assert UserYaml.get_final_yaml( + owner_yaml=owner_yaml, + repo_yaml=repo_yaml, + commit_yaml=commit_yaml, + owner_context=OwnerContext(ownerid=140), + ).to_dict() == { + "codecov": {"max_report_age": 86400}, + "key": {"a": "b", "c": "d", "value": "two"}, + } + + def test_get_final_yaml_no_commit_yaml(self, mock_configuration): + mock_configuration._params["site"] = {"codecov": {"max_report_age": 86400}} + owner_yaml = {"key": {"value": "one", "a": "b"}} + repo_yaml = {"barber": "shop"} + expected_result = { + "key": {"a": "b", "value": "one"}, + "barber": "shop", + "codecov": {"max_report_age": 86400}, + } + assert ( + UserYaml.get_final_yaml( + owner_yaml=owner_yaml, repo_yaml=repo_yaml, commit_yaml=None + ).to_dict() + == expected_result + ) + + def test_get_final_yaml_only_owner_yaml(self, mock_configuration): + mock_configuration._params["site"] = {"codecov": {"max_report_age": 86400}} + owner_yaml = {"key": {"value": "one", "a": "b"}} + expected_result = { + "key": {"a": "b", "value": "one"}, + "codecov": {"max_report_age": 86400}, + } + assert ( + UserYaml.get_final_yaml( + owner_yaml=owner_yaml, repo_yaml=None, commit_yaml=None + ).to_dict() + == expected_result + ) + + def test_get_final_yaml_nothing(self, mock_configuration): + mock_configuration._params["site"] = {"codecov": {"max_report_age": 86400}} + expected_result = {"codecov": {"max_report_age": 86400}} + assert ( + UserYaml.get_final_yaml( + owner_yaml=None, repo_yaml=None, commit_yaml=None + ).to_dict() + == expected_result + ) + + @freeze_time("2024-04-30 00:00:00.000") + def test_default_yaml_behavior_change(self): + current_yaml = LEGACY_DEFAULT_SITE_CONFIG + day_timedelta = datetime.timedelta(days=1) + patch_centric_expected_onboarding_date = ( + datetime.datetime.now(datetime.timezone.utc) + day_timedelta + ) + no_change_expected_onboarding_date = ( + datetime.datetime.now(datetime.timezone.utc) - day_timedelta + ) + no_change = _fix_yaml_defaults_based_on_owner_onboarding_date( + current_yaml, no_change_expected_onboarding_date + ) + assert no_change == current_yaml + patch_centric = _fix_yaml_defaults_based_on_owner_onboarding_date( + current_yaml, patch_centric_expected_onboarding_date + ) + assert patch_centric != current_yaml + assert patch_centric == PATCH_CENTRIC_DEFAULT_CONFIG + + def test_get_final_yaml_default_based_on_owner_context(self): + day_timedelta = datetime.timedelta(days=1) + patch_centric_expected_onboarding_date = ( + PATCH_CENTRIC_DEFAULT_TIME_START + day_timedelta + ) + no_change_expected_onboarding_date = ( + PATCH_CENTRIC_DEFAULT_TIME_START - day_timedelta + ) + legacy_default = UserYaml.get_final_yaml( + owner_yaml=None, + repo_yaml=None, + commit_yaml=None, + owner_context=OwnerContext( + owner_onboarding_date=no_change_expected_onboarding_date + ), + ) + assert legacy_default.to_dict() == { + "codecov": {"require_ci_to_pass": True, "notify": {"wait_for_ci": True}}, + "coverage": { + "precision": 2, + "round": "down", + "range": [60.0, 80.0], + "status": { + "project": True, + "patch": True, + "changes": False, + "default_rules": {"flag_coverage_not_uploaded_behavior": "include"}, + }, + }, + "comment": { + "layout": "reach,diff,flags,tree,reach", + "behavior": "default", + "show_carryforward_flags": False, + }, + "slack_app": True, + "github_checks": {"annotations": True}, + } + patch_centric_default = UserYaml.get_final_yaml( + owner_yaml=None, + repo_yaml=None, + commit_yaml=None, + owner_context=OwnerContext( + owner_onboarding_date=patch_centric_expected_onboarding_date + ), + ) + assert patch_centric_default.to_dict() == { + "codecov": {"require_ci_to_pass": True, "notify": {"wait_for_ci": True}}, + "coverage": { + "precision": 2, + "round": "down", + "range": [60.0, 80.0], + "status": { + "project": False, + "patch": True, + "changes": False, + "default_rules": {"flag_coverage_not_uploaded_behavior": "include"}, + }, + }, + "comment": { + "layout": "condensed_header, flags, tree, component", + "behavior": "default", + "show_carryforward_flags": False, + "hide_project_coverage": True, + }, + "slack_app": True, + "github_checks": {"annotations": True}, + } + + def test_get_possible_additional_user_yaml_empty(self, mock_configuration): + assert _get_possible_additional_user_yaml(1) == {} + assert _get_possible_additional_user_yaml(101) == {} + + def test_get_possible_additional_user_yaml(self, mock_configuration): + mock_configuration._params["additional_user_yamls"] = [ + {"percentage": 10, "name": "banana", "override": {"a": 2, "b": 3}}, + {"percentage": 30, "name": "apple", "override": {"d": "kllmnop", "b": 3}}, + ] + assert _get_possible_additional_user_yaml(0) == {"a": 2, "b": 3} + assert _get_possible_additional_user_yaml(1) == {"a": 2, "b": 3} + assert _get_possible_additional_user_yaml(9) == {"a": 2, "b": 3} + assert _get_possible_additional_user_yaml(10) == {"d": "kllmnop", "b": 3} + assert _get_possible_additional_user_yaml(11) == {"d": "kllmnop", "b": 3} + assert _get_possible_additional_user_yaml(39) == {"d": "kllmnop", "b": 3} + assert _get_possible_additional_user_yaml(40) == {} + assert _get_possible_additional_user_yaml(41) == {} + assert _get_possible_additional_user_yaml(100) == {"a": 2, "b": 3} + assert _get_possible_additional_user_yaml(101) == {"a": 2, "b": 3} + + def test_get_components_no_default(self): + user_yaml = UserYaml( + { + "component_management": { + "individual_components": [ + {"component_id": "py_files", "paths": [r".*\.py"]} + ] + } + } + ) + components = user_yaml.get_components() + assert len(components) == 1 + assert components == [ + Component( + component_id="py_files", + paths=[r".*\.py"], + name="", + flag_regexes=[], + statuses=[], + ) + ] + + def test_get_components_default_no_components(self): + user_yaml = UserYaml({"component_management": {}}) + components = user_yaml.get_components() + assert len(components) == 0 + + def test_get_components_default_only(self): + user_yaml = UserYaml( + { + "component_management": { + "default_rules": {"paths": [r".*\.py"], "flag_regexes": [r"flag.*"]} + } + } + ) + components = user_yaml.get_components() + assert len(components) == 0 + + def test_get_components_all(self): + user_yaml = UserYaml( + { + "component_management": { + "default_rules": { + "paths": [r".*\.py"], + "flag_regexes": [r"flag.*"], + }, + "individual_components": [ + {"component_id": "go_files", "paths": [r".*\.go"]}, + {"component_id": "rules_from_default"}, + { + "component_id": "I have my flags", + "flag_regexes": [r"python-.*"], + }, + { + "component_id": "required", + "name": "display", + "flag_regexes": [], + "paths": [r"src/.*"], + }, + ], + } + } + ) + components = user_yaml.get_components() + assert len(components) == 4 + assert components == [ + Component( + component_id="go_files", + paths=[r".*\.go"], + name="", + flag_regexes=[r"flag.*"], + statuses=[], + ), + Component( + component_id="rules_from_default", + paths=[r".*\.py"], + name="", + flag_regexes=[r"flag.*"], + statuses=[], + ), + Component( + component_id="I have my flags", + paths=[r".*\.py"], + name="", + flag_regexes=[r"python-.*"], + statuses=[], + ), + Component( + component_id="required", + name="display", + paths=[r"src/.*"], + flag_regexes=[], + statuses=[], + ), + ] diff --git a/libs/shared/tests/unit/yaml/test_user_yaml_validation.py b/libs/shared/tests/unit/yaml/test_user_yaml_validation.py new file mode 100644 index 0000000000..75668ac3cd --- /dev/null +++ b/libs/shared/tests/unit/yaml/test_user_yaml_validation.py @@ -0,0 +1,81 @@ +from base64 import b64encode + +from shared.yaml.validation import UserGivenSecret, validate_yaml +from tests.base import BaseTestCase + + +def test_show_secret_case(): + value = "github/11934774/154468867/https://hooks.slack.com/services/first_key/BE7FWCVHV/dkbfscprianc7wrb" + encoded_value = UserGivenSecret.encode(value) + user_input = { + "coverage": { + "round": "down", + "precision": 2, + "range": [70.0, 100.0], + "status": {"project": {"default": {"base": "auto"}}}, + "notify": {"irc": {"user_given_title": {"password": encoded_value}}}, + }, + "ignore": ["Pods/.*", "**/*bundle"], + } + expected_result = { + "coverage": { + "round": "down", + "precision": 2, + "range": [70.0, 100.0], + "status": {"project": {"default": {"base": "auto"}}}, + "notify": { + "irc": { + "user_given_title": { + "password": "https://hooks.slack.com/services/first_key/BE7FWCVHV/dkbfscprianc7wrb" + } + } + }, + }, + "ignore": ["Pods/.*", "(?s:.*/[^\\/]*bundle)\\Z"], + } + result = validate_yaml( + user_input, show_secrets_for=("github", "11934774", "154468867") + ) + assert result == expected_result + + +class TestUserGivenSecret(BaseTestCase): + def test_simple_user_given_secret(self): + value = "github/11934774/154468867/https://hooks.slack.com/services/first_key/BE7FWCVHV/dkbfscprianc7wrb" + encoded_value = UserGivenSecret.encode(value) + ugs = UserGivenSecret(show_secrets_for=("github", "11934774", "154468867")) + assert ugs.validate(value) == value + assert ( + ugs.validate(encoded_value) + == "https://hooks.slack.com/services/first_key/BE7FWCVHV/dkbfscprianc7wrb" + ) + bad_ugs = UserGivenSecret(show_secrets_for=("github", "12345", "154468867")) + assert bad_ugs.validate(value) == value + assert bad_ugs.validate(encoded_value) == encoded_value + + def test_simple_user_given_secret_rotated_key(self): + encoded_data = "secret:v1::zsV9A8pHadNle357DGJHbZCTyCYA+TXdUd9TN3IY2DIWcPOtgK3Pg1EgA6OZr9XJ1EsdpL765yWrN4pfR3elRdN2LUwiuv6RkNjpbiruHx45agsgxdu8fi24p5pkCLvjcW0HqdH2PTvmHauIp+ptgA==" + ugs = UserGivenSecret(show_secrets_for=("github", 11934774, 154468867)) + assert ( + ugs.validate(encoded_data) + == "https://hooks.slack.com/services/first_key/BE7FWCVHV/dkbfscprianc7wrb" + ) + + def test_pseudosecret_user_given_secret(self): + value = "secret:arriba" + ugs = UserGivenSecret(show_secrets_for=("github", "12", 98)) + assert ugs.validate(value) == value + + def test_b64encoded_pseudosecret_user_given_secret(self): + encoded_value = b64encode("arriba".encode()) + value = b"secret:" + encoded_value + value = value.decode() + ugs = UserGivenSecret(show_secrets_for=("github", "12", 98)) + assert ugs.validate(value) == value + + def test_simple_user_dont_show_secret(self): + value = "github/11934774/154468867/https://hooks.slack.com/services/first_key/BE7FWCVHV/dkbfscprianc7wrb" + encoded_value = UserGivenSecret.encode(value) + ugs = UserGivenSecret(show_secrets_for=None) + assert ugs.validate(value) == value + assert ugs.validate(encoded_value) == encoded_value diff --git a/libs/shared/uv.lock b/libs/shared/uv.lock new file mode 100644 index 0000000000..1642081b16 --- /dev/null +++ b/libs/shared/uv.lock @@ -0,0 +1,1670 @@ +version = 1 +revision = 1 +requires-python = ">=3.13" +resolution-markers = [ + "platform_python_implementation != 'PyPy'", + "platform_python_implementation == 'PyPy'", +] + +[[package]] +name = "amplitude-analytics" +version = "1.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/e1/9af6812e54cc53e6be090d1324cf7b11ebe93f9613345959f16b4844fed3/amplitude-analytics-1.1.4.tar.gz", hash = "sha256:9f05dc461459cfef15df8795895971745193fb74ab4e8a561e96bb208f11860e", size = 21193 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/d9/9c1b286a2bb83d081b5f142e6992f7f0a8d7c229d10897c040988026e95e/amplitude_analytics-1.1.4-py3-none-any.whl", hash = "sha256:802d9b3a20d095d49074610dcd7e8834e5fcaff5c99d25d3153c03a163d73889", size = 24037 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.6.2.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 }, +] + +[[package]] +name = "argon2-cffi" +version = "23.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/fa/57ec2c6d16ecd2ba0cf15f3c7d1c3c2e7b5fcb83555ff56d7ab10888ec8f/argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08", size = 42798 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea", size = 15124 }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", size = 1779911 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/13/838ce2620025e9666aa8f686431f67a29052241692a3dd1ae9d3692a89d3/argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", size = 29658 }, + { url = "https://files.pythonhosted.org/packages/b3/02/f7f7bb6b6af6031edb11037639c697b912e1dea2db94d436e681aea2f495/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", size = 80583 }, + { url = "https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", size = 86168 }, + { url = "https://files.pythonhosted.org/packages/74/f6/4a34a37a98311ed73bb80efe422fed95f2ac25a4cacc5ae1d7ae6a144505/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", size = 82709 }, + { url = "https://files.pythonhosted.org/packages/74/2b/73d767bfdaab25484f7e7901379d5f8793cccbb86c6e0cbc4c1b96f63896/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", size = 83613 }, + { url = "https://files.pythonhosted.org/packages/4f/fd/37f86deef67ff57c76f137a67181949c2d408077e2e3dd70c6c42912c9bf/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", size = 84583 }, + { url = "https://files.pythonhosted.org/packages/6f/52/5a60085a3dae8fded8327a4f564223029f5f54b0cb0455a31131b5363a01/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", size = 88475 }, + { url = "https://files.pythonhosted.org/packages/8b/95/143cd64feb24a15fa4b189a3e1e7efbaeeb00f39a51e99b26fc62fbacabd/argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", size = 27698 }, + { url = "https://files.pythonhosted.org/packages/37/2c/e34e47c7dee97ba6f01a6203e0383e15b60fb85d78ac9a15cd066f6fe28b/argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", size = 30817 }, + { url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104 }, +] + +[[package]] +name = "asgiref" +version = "3.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 }, +] + +[[package]] +name = "boto3" +version = "1.35.59" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/ca/09fb332e8bab219df4832337978d7c8227571b86fdcfb28355f591cf544f/boto3-1.35.59.tar.gz", hash = "sha256:81f4d8d6eff3e26b82cabd42eda816cfac9482821fdef353f18d2ba2f6e75f2d", size = 111011 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/ad/b8ca386a511b0b3cf1f9e45426b570a83246b01010a18de87786b212d3b1/boto3-1.35.59-py3-none-any.whl", hash = "sha256:8f8ff97cb9cb2e1ec7374209d0c09c1926b75604d6464c34bafaffd6d6cf0529", size = 139178 }, +] + +[[package]] +name = "botocore" +version = "1.35.59" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/19/f4609e3f9ae2c166fd1350e9128b647f9a1d3ecd2e01db08cd0227c2b9e0/botocore-1.35.59.tar.gz", hash = "sha256:de0ce655fedfc02c87869dfaa3b622488a17ff37da316ef8106cbe1573b83c98", size = 12966883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/8c/31ca91afc34b03de36ca560fde66af0c32e205c8a22bf4222b6ae2c424b7/botocore-1.35.59-py3-none-any.whl", hash = "sha256:bcd66d7f55c8d1b6020eb86f2d87893fe591fb4be6a7d2a689c18be586452334", size = 12755348 }, +] + +[[package]] +name = "cachetools" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/38/a0f315319737ecf45b4319a8cd1f3a908e29d9277b46942263292115eee7/cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a", size = 27661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/07/14f8ad37f2d12a5ce41206c21820d8cb6561b728e51fad4530dff0552a67/cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", size = 9524 }, +] + +[[package]] +name = "cerberus" +version = "1.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/20/3ec65289ab0ccf5f41d38d103343f5b609263944238e299598aeba684a82/Cerberus-1.3.5.tar.gz", hash = "sha256:81011e10266ef71b6ec6d50e60171258a5b134d69f8fb387d16e4936d0d47642", size = 29898 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/17/335e0a4daf5475ada5eaa74735f765cc088ace306fbfdd2f4c2285320cc3/Cerberus-1.3.5-py3-none-any.whl", hash = "sha256:7649a5815024d18eb7c6aa5e7a95355c649a53aacfc9b050e9d0bf6bfa2af372", size = 30779 }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, + { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, + { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, + { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, + { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, + { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, + { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, + { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, + { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, + { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, + { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, + { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +] + +[[package]] +name = "codecov-ribs" +version = "0.1.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/0c/da1d2367c86ccebbcd60d77a697a816b9b226031528b92c5e751a82b498d/codecov_ribs-0.1.18.tar.gz", hash = "sha256:004590ee88515aa2ee3e1ad0a05ed616a6caffcedd9effe3454efe9d927c2a46", size = 31935 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/bc/e39377099e929bd4da67e9dde23e9e4b1f765e80174a4fcbd76066ab85bc/codecov_ribs-0.1.18-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:00fe34c01f72cfb967d9abfa0a19ac65485564f1cdfc7ea634362f0f8031a57a", size = 449013 }, + { url = "https://files.pythonhosted.org/packages/cd/26/27b1404572ece21c3cfcaebcacc77accdef87fd7f3799ae75ec9a3a986a6/codecov_ribs-0.1.18-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:509c3b5ffedb75590e5efff9fe44fa38fb9a330f3cfd2cfdbffe03ba9baf2c6f", size = 435904 }, + { url = "https://files.pythonhosted.org/packages/90/e4/aac82889bf1233e65c05d3182f0f009f9fec7c3938143a627082a4f470dd/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:94676137c0a84fe434b6fa9d2e51e8301839b704f0561642e3104def7fdd186a", size = 1277625 }, + { url = "https://files.pythonhosted.org/packages/46/b3/5b9677fa973a25942538c143bb1184bfd13ee838ade1f62836bae1ab4f8e/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:078a3c5b23852142ce0c362e5e61dc3e76be28820000cf8d62f0dfa02e0b9fd1", size = 1230640 }, + { url = "https://files.pythonhosted.org/packages/48/c6/85c6d4ad866d3ed860ec4d371eaa4d55a3937dd21315d7258d6968623748/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ba82a37648c42b568513c5b8c4044c45f1dc4240a9c53259687cc2e869f415b", size = 1243114 }, + { url = "https://files.pythonhosted.org/packages/15/30/919304e23d9599674887ae112d362e6a0b96cdb120a1766c2c0ea30bff17/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb7aeb1fce52ae4e61ad4fa1f16d9d6c5429b77c037800eec8a58586020a3b1", size = 1340437 }, + { url = "https://files.pythonhosted.org/packages/53/55/d71a0ebf5363af6eacaf60cbf2db33d4c2e7962c8734172e6f37e378b153/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7f87052b56e5346ab2d89cad8cb801074faaca4cb9d351097e5a7a54a2af424", size = 1432376 }, + { url = "https://files.pythonhosted.org/packages/44/26/ee24c4f22de09d04d242e21dfb4877469735a6a7d55c052b3bbcb782e541/codecov_ribs-0.1.18-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08db4118a7350f76c53fc0ba04110b606a3178fdd1acb6946cd1c0b9fea777b5", size = 1242811 }, + { url = "https://files.pythonhosted.org/packages/00/ad/e86b5967ee91e93604554a5c9ea35175330d84e52f781a8d1295d63150e9/codecov_ribs-0.1.18-cp38-abi3-win32.whl", hash = "sha256:a7db0d0afa593f155e34ae722703b8b21840c85a94cc90e8550173610d346d0f", size = 290807 }, + { url = "https://files.pythonhosted.org/packages/d1/e3/b1e2b06b1d7e047318608fb2b7268d594da0798b166eb6b38ae7d8c3f41f/codecov_ribs-0.1.18-cp38-abi3-win_amd64.whl", hash = "sha256:dd7a799c1eea76caebf59f4a9a3a5cda0fff7999d49452235c1db5f1d654d4b8", size = 305137 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "colour" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/d4/5911a7618acddc3f594ddf144ecd8a03c29074a540f4494670ad8f153efe/colour-0.1.5.tar.gz", hash = "sha256:af20120fefd2afede8b001fbef2ea9da70ad7d49fafdb6489025dae8745c3aee", size = 24776 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/46/e81907704ab203206769dee1385dc77e1407576ff8f50a0681d0a6b541be/colour-0.1.5-py2.py3-none-any.whl", hash = "sha256:33f6db9d564fadc16e59921a56999b79571160ce09916303d35346dddc17978c", size = 23772 }, +] + +[[package]] +name = "coverage" +version = "7.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/12/3669b6382792783e92046730ad3327f53b2726f0603f4c311c4da4824222/coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", size = 798716 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/4d/2dede4f7cb5a70fb0bb40a57627fddf1dbdc6b9c1db81f7c4dcdcb19e2f4/coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", size = 207039 }, + { url = "https://files.pythonhosted.org/packages/3f/f9/d86368ae8c79e28f1fb458ebc76ae9ff3e8bd8069adc24e8f2fed03c58b7/coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", size = 207298 }, + { url = "https://files.pythonhosted.org/packages/64/c5/b4cc3c3f64622c58fbfd4d8b9a7a8ce9d355f172f91fcabbba1f026852f6/coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", size = 239813 }, + { url = "https://files.pythonhosted.org/packages/8a/86/14c42e60b70a79b26099e4d289ccdfefbc68624d096f4481163085aa614c/coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", size = 236959 }, + { url = "https://files.pythonhosted.org/packages/7f/f8/4436a643631a2fbab4b44d54f515028f6099bfb1cd95b13cfbf701e7f2f2/coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", size = 238950 }, + { url = "https://files.pythonhosted.org/packages/49/50/1571810ddd01f99a0a8be464a4ac8b147f322cd1e8e296a1528984fc560b/coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", size = 238610 }, + { url = "https://files.pythonhosted.org/packages/f3/8c/6312d241fe7cbd1f0cade34a62fea6f333d1a261255d76b9a87074d8703c/coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", size = 236697 }, + { url = "https://files.pythonhosted.org/packages/ce/5f/fef33dfd05d87ee9030f614c857deb6df6556b8f6a1c51bbbb41e24ee5ac/coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", size = 238541 }, + { url = "https://files.pythonhosted.org/packages/a9/64/6a984b6e92e1ea1353b7ffa08e27f707a5e29b044622445859200f541e8c/coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", size = 209707 }, + { url = "https://files.pythonhosted.org/packages/5c/60/ce5a9e942e9543783b3db5d942e0578b391c25cdd5e7f342d854ea83d6b7/coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", size = 210439 }, + { url = "https://files.pythonhosted.org/packages/78/53/6719677e92c308207e7f10561a1b16ab8b5c00e9328efc9af7cfd6fb703e/coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", size = 207784 }, + { url = "https://files.pythonhosted.org/packages/fa/dd/7054928930671fcb39ae6a83bb71d9ab5f0afb733172543ced4b09a115ca/coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", size = 208058 }, + { url = "https://files.pythonhosted.org/packages/b5/7d/fd656ddc2b38301927b9eb3aae3fe827e7aa82e691923ed43721fd9423c9/coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", size = 250772 }, + { url = "https://files.pythonhosted.org/packages/90/d0/eb9a3cc2100b83064bb086f18aedde3afffd7de6ead28f69736c00b7f302/coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", size = 246490 }, + { url = "https://files.pythonhosted.org/packages/45/44/3f64f38f6faab8a0cfd2c6bc6eb4c6daead246b97cf5f8fc23bf3788f841/coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", size = 248848 }, + { url = "https://files.pythonhosted.org/packages/5d/11/4c465a5f98656821e499f4b4619929bd5a34639c466021740ecdca42aa30/coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", size = 248340 }, + { url = "https://files.pythonhosted.org/packages/f1/96/ebecda2d016cce9da812f404f720ca5df83c6b29f65dc80d2000d0078741/coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", size = 246229 }, + { url = "https://files.pythonhosted.org/packages/16/d9/3d820c00066ae55d69e6d0eae11d6149a5ca7546de469ba9d597f01bf2d7/coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", size = 247510 }, + { url = "https://files.pythonhosted.org/packages/8f/c3/4fa1eb412bb288ff6bfcc163c11700ff06e02c5fad8513817186e460ed43/coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", size = 210353 }, + { url = "https://files.pythonhosted.org/packages/7e/77/03fc2979d1538884d921c2013075917fc927f41cd8526909852fe4494112/coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", size = 211502 }, +] + +[[package]] +name = "cryptography" +version = "43.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f3/01fdf26701a26f4b4dbc337a26883ad5bccaa6f1bbbdd29cd89e22f18a1c/cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", size = 6225303 }, + { url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905 }, + { url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271 }, + { url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606 }, + { url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484 }, + { url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131 }, + { url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647 }, + { url = "https://files.pythonhosted.org/packages/56/48/7b6b190f1462818b324e674fa20d1d5ef3e24f2328675b9b16189cbf0b3c/cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", size = 2623873 }, + { url = "https://files.pythonhosted.org/packages/eb/b1/0ebff61a004f7f89e7b65ca95f2f2375679d43d0290672f7713ee3162aff/cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", size = 3068039 }, + { url = "https://files.pythonhosted.org/packages/30/d5/c8b32c047e2e81dd172138f772e81d852c51f0f2ad2ae8a24f1122e9e9a7/cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", size = 6222984 }, + { url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968 }, + { url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754 }, + { url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458 }, + { url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220 }, + { url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898 }, + { url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592 }, + { url = "https://files.pythonhosted.org/packages/81/1e/ffcc41b3cebd64ca90b28fd58141c5f68c83d48563c88333ab660e002cd3/cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", size = 2623145 }, + { url = "https://files.pythonhosted.org/packages/87/5c/3dab83cc4aba1f4b0e733e3f0c3e7d4386440d660ba5b1e3ff995feb734d/cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", size = 3068026 }, +] + +[[package]] +name = "deprecated" +version = "1.2.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/14/1e41f504a246fc224d2ac264c227975427a85caf37c3979979edb9b1b232/Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3", size = 2974416 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/8d/778b7d51b981a96554f29136cd59ca7880bf58094338085bcf2a979a0e6a/Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c", size = 9561 }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + +[[package]] +name = "django" +version = "4.2.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/d8/a607ee443b54a4db4ad28902328b906ae6218aa556fb9b3ac45c0bcb313d/Django-4.2.16.tar.gz", hash = "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad", size = 10436023 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/2c/6b6c7e493d5ea789416918658ebfa16be7a64c77610307497ed09a93c8c4/Django-4.2.16-py3-none-any.whl", hash = "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898", size = 7992936 }, +] + +[[package]] +name = "django-better-admin-arrayfield" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/4f/e8bdf86d5bba2622d585bec983a0b59f373f4ed00eb948a6907ba35e5585/django-better-admin-arrayfield-1.4.2.tar.gz", hash = "sha256:b45423e51bbc0aa31ef658248c058ca8b533a541be4dee9fb8bcd059f8a10a58", size = 13857 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/75/8fc6cfc73c456283c89a80bd8319df61aabcd273cfa37697bc31a00e387b/django_better_admin_arrayfield-1.4.2-py2.py3-none-any.whl", hash = "sha256:bfeaa0fa8210a7ea95ee996a6caaa59ecd0c923269f573e6d8319c28dcac5c88", size = 13931 }, +] + +[[package]] +name = "django-model-utils" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/60/5e232c32a2c977cc1af8c70a38ef436598bc649ad89c2c4568454edde2c9/django_model_utils-5.0.0.tar.gz", hash = "sha256:041cdd6230d2fbf6cd943e1969318bce762272077f4ecd333ab2263924b4e5eb", size = 80559 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/13/87a42048700c54bfce35900a34e2031245132775fb24363fc0e33664aa9c/django_model_utils-5.0.0-py3-none-any.whl", hash = "sha256:fec78e6c323d565a221f7c4edc703f4567d7bb1caeafe1acd16a80c5ff82056b", size = 42630 }, +] + +[[package]] +name = "django-postgres-extra" +version = "2.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/8a/2bf9f570e73058569e48cc3208f007fd7ff45cb28e178ea1da2b188f14e2/django-postgres-extra-2.0.8.tar.gz", hash = "sha256:9efa08c6f18ed34460af41c6f679bb375b93d12544b1105aa348b787a30b46eb", size = 48572 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/22/559559d7894648bd7f2bbfdeedd5ccba4f8d9de68855ced522011d608af5/django_postgres_extra-2.0.8-py3-none-any.whl", hash = "sha256:447d5a971759943ee63a9d4cef9c6c1fa290e518611ea521a38b6732681d2f3a", size = 75205 }, +] + +[[package]] +name = "django-prometheus" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prometheus-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/51/485b4122e00f2b8efec8a6d718ef4ce6b150231e49398e554ce1151f65c3/django-prometheus-2.3.1.tar.gz", hash = "sha256:f9c8b6c780c9419ea01043c63a437d79db2c33353451347894408184ad9c3e1e", size = 24718 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/75/fb3d4f056f9ed4f8848817d5afd7a1d949632ab117452ccd179e3839cfc4/django_prometheus-2.3.1-py2.py3-none-any.whl", hash = "sha256:cf9b26f7ba2e4568f08f8f91480a2882023f5908579681bcf06a4d2465f12168", size = 29081 }, +] + +[[package]] +name = "factory-boy" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "faker" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/3d/8070dde623341401b1c80156583d4c793058fe250450178218bb6e45526c/factory_boy-3.3.1.tar.gz", hash = "sha256:8317aa5289cdfc45f9cae570feb07a6177316c82e34d14df3c2e1f22f26abef0", size = 163924 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/cf/44ec67152f3129d0114c1499dd34f0a0a0faf43d9c2af05bc535746ca482/factory_boy-3.3.1-py2.py3-none-any.whl", hash = "sha256:7b1113c49736e1e9995bc2a18f4dbf2c52cf0f841103517010b1d825712ce3ca", size = 36878 }, +] + +[[package]] +name = "faker" +version = "32.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/2a/dd2c8f55d69013d0eee30ec4c998250fb7da957f5fe860ed077b3df1725b/faker-32.1.0.tar.gz", hash = "sha256:aac536ba04e6b7beb2332c67df78485fc29c1880ff723beac6d1efd45e2f10f5", size = 1850193 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/fa/4a82dea32d6262a96e6841cdd4a45c11ac09eecdff018e745565410ac70e/Faker-32.1.0-py3-none-any.whl", hash = "sha256:c77522577863c264bdc9dad3a2a750ad3f7ee43ff8185072e482992288898814", size = 1889123 }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + +[[package]] +name = "freezegun" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/ef/722b8d71ddf4d48f25f6d78aa2533d505bf3eec000a7cacb8ccc8de61f2f/freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9", size = 33697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/0b/0d7fee5919bccc1fdc1c2a7528b98f65c6f69b223a3fd8f809918c142c36/freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1", size = 17569 }, +] + +[[package]] +name = "google-api-core" +version = "2.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/6b/b98553c2061c4e2186f5bbfb1aa1a6ef13fc0775c096d18595d3c99ba023/google_api_core-2.23.0.tar.gz", hash = "sha256:2ceb087315e6af43f256704b871d99326b1f12a9d6ce99beaedec99ba26a0ace", size = 160094 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/a4/c26886d57d90032c5f74c2e80aefdc38ec58551fc46bd4ce79fb2c9389fa/google_api_core-2.23.0-py3-none-any.whl", hash = "sha256:c20100d4c4c41070cf365f1d8ddf5365915291b5eb11b83829fbd1c999b5122f", size = 156554 }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-auth" +version = "2.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/71/4c5387d8a3e46e3526a8190ae396659484377a73b33030614dd3b28e7ded/google_auth-2.36.0.tar.gz", hash = "sha256:545e9618f2df0bcbb7dcbc45a546485b1212624716975a1ea5ae8149ce769ab1", size = 268336 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3d5087d27865c2f0431b942b5c4500b7d1b744dd3262fdc973a4c39d099e/google_auth-2.36.0-py2.py3-none-any.whl", hash = "sha256:51a15d47028b66fd36e5c64a82d2d57480075bccc7da37cde257fc94177a61fb", size = 209519 }, +] + +[[package]] +name = "google-cloud-core" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/1f/9d1e0ba6919668608570418a9a51e47070ac15aeff64261fb092d8be94c0/google-cloud-core-2.4.1.tar.gz", hash = "sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073", size = 35587 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/0f/2e2061e3fbcb9d535d5da3f58cc8de4947df1786fe6a1355960feb05a681/google_cloud_core-2.4.1-py2.py3-none-any.whl", hash = "sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61", size = 29233 }, +] + +[[package]] +name = "google-cloud-pubsub" +version = "2.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "grpcio-status" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ab/39175fc8a3f7b4094e9c0bf781ea0e54841b8ae5706a169b9dcec84fb6b1/google_cloud_pubsub-2.27.1.tar.gz", hash = "sha256:7119dbc5af4b915ecdfa1289919f791a432927eaaa7bbfbeb740e6d7020c181e", size = 358515 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/af/8930228f1f7e411dac722fd90e4235e87ea5b96ba3c3d1b721b80b8c34e4/google_cloud_pubsub-2.27.1-py2.py3-none-any.whl", hash = "sha256:3ca8980c198a847ee464845ab60f05478d4819cf693c9950ee89da96f0b80a41", size = 289646 }, +] + +[[package]] +name = "google-cloud-storage" +version = "2.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "google-resumable-media" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/b7/1554cdeb55d9626a4b8720746cba8119af35527b12e1780164f9ba0f659a/google_cloud_storage-2.18.2.tar.gz", hash = "sha256:aaf7acd70cdad9f274d29332673fcab98708d0e1f4dceb5a5356aaef06af4d99", size = 5532864 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/da/95db7bd4f0bd1644378ac1702c565c0210b004754d925a74f526a710c087/google_cloud_storage-2.18.2-py2.py3-none-any.whl", hash = "sha256:97a4d45c368b7d401ed48c4fdfe86e1e1cb96401c9e199e419d289e2c0370166", size = 130466 }, +] + +[[package]] +name = "google-crc32c" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/72/c3298da1a3773102359c5a78f20dae8925f5ea876e37354415f68594a6fb/google_crc32c-1.6.0.tar.gz", hash = "sha256:6eceb6ad197656a1ff49ebfbbfa870678c75be4344feb35ac1edf694309413dc", size = 14472 } + +[[package]] +name = "google-resumable-media" +version = "2.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-crc32c" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/5a/0efdc02665dca14e0837b62c8a1a93132c264bd02054a15abb2218afe0ae/google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0", size = 2163099 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/35/b8d3baf8c46695858cb9d8835a53baa1eeb9906ddaf2f728a5f5b640fd1e/google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa", size = 81251 }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.66.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/a7/8e9cccdb1c49870de6faea2a2764fa23f627dd290633103540209f03524c/googleapis_common_protos-1.66.0.tar.gz", hash = "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c", size = 114376 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/0f/c0713fb2b3d28af4b2fded3291df1c4d4f79a00d15c2374a9e010870016c/googleapis_common_protos-1.66.0-py2.py3-none-any.whl", hash = "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed", size = 221682 }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, +] + +[[package]] +name = "greenlet" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 }, + { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 }, + { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 }, + { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 }, + { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 }, + { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 }, + { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 }, + { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 }, + { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 }, + { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 }, + { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 }, + { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 }, + { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 }, + { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 }, + { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 }, + { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, +] + +[[package]] +name = "grpc-google-iam-v1" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", extra = ["grpc"] }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/41/f01bf46bac4034b4750575fe87c80c5a43a8912847307955e22f2125b60c/grpc-google-iam-v1-0.13.1.tar.gz", hash = "sha256:3ff4b2fd9d990965e410965253c0da6f66205d5a8291c4c31c6ebecca18a9001", size = 17664 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/7d/da3875b7728bc700eeb28b513754ce237c04ac7cbf8559d76b0464ee01cb/grpc_google_iam_v1-0.13.1-py2.py3-none-any.whl", hash = "sha256:c3e86151a981811f30d5e7330f271cee53e73bb87755e88cc3b6f0c7b5fe374e", size = 24866 }, +] + +[[package]] +name = "grpcio" +version = "1.67.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/53/d9282a66a5db45981499190b77790570617a604a38f3d103d0400974aeb5/grpcio-1.67.1.tar.gz", hash = "sha256:3dc2ed4cabea4dc14d5e708c2b426205956077cc5de419b4d4079315017e9732", size = 12580022 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/d2/2f032b7a153c7723ea3dea08bffa4bcaca9e0e5bdf643ce565b76da87461/grpcio-1.67.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:aa0162e56fd10a5547fac8774c4899fc3e18c1aa4a4759d0ce2cd00d3696ea6b", size = 5091487 }, + { url = "https://files.pythonhosted.org/packages/d0/ae/ea2ff6bd2475a082eb97db1104a903cf5fc57c88c87c10b3c3f41a184fc0/grpcio-1.67.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:beee96c8c0b1a75d556fe57b92b58b4347c77a65781ee2ac749d550f2a365dc1", size = 10943530 }, + { url = "https://files.pythonhosted.org/packages/07/62/646be83d1a78edf8d69b56647327c9afc223e3140a744c59b25fbb279c3b/grpcio-1.67.1-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:a93deda571a1bf94ec1f6fcda2872dad3ae538700d94dc283c672a3b508ba3af", size = 5589079 }, + { url = "https://files.pythonhosted.org/packages/d0/25/71513d0a1b2072ce80d7f5909a93596b7ed10348b2ea4fdcbad23f6017bf/grpcio-1.67.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e6f255980afef598a9e64a24efce87b625e3e3c80a45162d111a461a9f92955", size = 6213542 }, + { url = "https://files.pythonhosted.org/packages/76/9a/d21236297111052dcb5dc85cd77dc7bf25ba67a0f55ae028b2af19a704bc/grpcio-1.67.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e838cad2176ebd5d4a8bb03955138d6589ce9e2ce5d51c3ada34396dbd2dba8", size = 5850211 }, + { url = "https://files.pythonhosted.org/packages/2d/fe/70b1da9037f5055be14f359026c238821b9bcf6ca38a8d760f59a589aacd/grpcio-1.67.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a6703916c43b1d468d0756c8077b12017a9fcb6a1ef13faf49e67d20d7ebda62", size = 6572129 }, + { url = "https://files.pythonhosted.org/packages/74/0d/7df509a2cd2a54814598caf2fb759f3e0b93764431ff410f2175a6efb9e4/grpcio-1.67.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:917e8d8994eed1d86b907ba2a61b9f0aef27a2155bca6cbb322430fc7135b7bb", size = 6149819 }, + { url = "https://files.pythonhosted.org/packages/0a/08/bc3b0155600898fd10f16b79054e1cca6cb644fa3c250c0fe59385df5e6f/grpcio-1.67.1-cp313-cp313-win32.whl", hash = "sha256:e279330bef1744040db8fc432becc8a727b84f456ab62b744d3fdb83f327e121", size = 3596561 }, + { url = "https://files.pythonhosted.org/packages/5a/96/44759eca966720d0f3e1b105c43f8ad4590c97bf8eb3cd489656e9590baa/grpcio-1.67.1-cp313-cp313-win_amd64.whl", hash = "sha256:fa0c739ad8b1996bd24823950e3cb5152ae91fca1c09cc791190bf1627ffefba", size = 4346042 }, +] + +[[package]] +name = "grpcio-status" +version = "1.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/c7/fe0e79a80ac6346e0c6c0a24e9e3cbc3ae1c2a009acffb59eab484a6f69b/grpcio_status-1.67.1.tar.gz", hash = "sha256:2bf38395e028ceeecfd8866b081f61628114b384da7d51ae064ddc8d766a5d11", size = 13673 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/18/56999a1da3577d8ccc8698a575d6638e15fe25650cc88b2ce0a087f180b9/grpcio_status-1.67.1-py3-none-any.whl", hash = "sha256:16e6c085950bdacac97c779e6a502ea671232385e6e37f258884d6883392c2bd", size = 14427 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/44/ed0fa6a17845fb033bd885c03e842f08c1b9406c86a2e60ac1ae1b9206a6/httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f", size = 85180 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/89/b161908e2f51be56568184aeb4a880fd287178d176fd1c860d2217f41106/httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", size = 78011 }, +] + +[[package]] +name = "httpx" +version = "0.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, +] + +[[package]] +name = "identify" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/79/7a520fc5011e02ca3f3285b5f6820eaf80443eb73e3733f73c02fb42ba0b/identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd", size = 99113 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/86/c4395700f3c5475424fb5c41e20c16be28d10c904aee4d005ba3217fc8e7/identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3", size = 98982 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "ijson" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/83/28e9e93a3a61913e334e3a2e78ea9924bb9f9b1ac45898977f9d9dd6133f/ijson-3.3.0.tar.gz", hash = "sha256:7f172e6ba1bee0d4c8f8ebd639577bfe429dee0f3f96775a067b8bae4492d8a0", size = 60079 } + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "minio" +version = "7.2.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi" }, + { name = "certifi" }, + { name = "pycryptodome" }, + { name = "typing-extensions" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/d8/04b4c8ceaa7bae49a674ccdba53530599e73fb3c6a8f8cf8e26ee0eb390d/minio-7.2.10.tar.gz", hash = "sha256:418c31ac79346a580df04a0e14db1becbc548a6e7cca61f9bc4ef3bcd336c449", size = 135388 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/6f/1b1f5025bf43c2a4ca8112332db586c8077048ec8bcea2deb269eac84577/minio-7.2.10-py3-none-any.whl", hash = "sha256:5961c58192b1d70d3a2a362064b8e027b8232688998a6d1251dadbb02ab57a7d", size = 93943 }, +] + +[[package]] +name = "mmh3" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/08/04ad6419f072ea3f51f9a0f429dd30f5f0a0b02ead7ca11a831117b6f9e8/mmh3-5.0.1.tar.gz", hash = "sha256:7dab080061aeb31a6069a181f27c473a1f67933854e36a3464931f2716508896", size = 32008 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/e0/fb19c46265c18311b422ba5ce3e18046ad45c48cfb213fd6dbec23ae6b51/mmh3-5.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:673e3f1c8d4231d6fb0271484ee34cb7146a6499fc0df80788adb56fd76842da", size = 52909 }, + { url = "https://files.pythonhosted.org/packages/c3/94/54fc591e7a24c7ce2c531ecfc5715cff932f9d320c2936550cc33d67304d/mmh3-5.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f795a306bd16a52ad578b663462cc8e95500b3925d64118ae63453485d67282b", size = 38396 }, + { url = "https://files.pythonhosted.org/packages/1f/9a/142bcc9d0d28fc8ae45bbfb83926adc069f984cdf3495a71534cc22b8e27/mmh3-5.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5ed57a5e28e502a1d60436cc25c76c3a5ba57545f250f2969af231dc1221e0a5", size = 38207 }, + { url = "https://files.pythonhosted.org/packages/f8/5b/f1c9110aa70321bb1ee713f17851b9534586c63bc25e0110e4fc03ae2450/mmh3-5.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:632c28e7612e909dbb6cbe2fe496201ada4695b7715584005689c5dc038e59ad", size = 94988 }, + { url = "https://files.pythonhosted.org/packages/87/e5/4dc67e7e0e716c641ab0a5875a659e37258417439590feff5c3bd3ff4538/mmh3-5.0.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53fd6bd525a5985e391c43384672d9d6b317fcb36726447347c7fc75bfed34ec", size = 99969 }, + { url = "https://files.pythonhosted.org/packages/ac/68/d148327337687c53f04ad9ceaedfa9ad155ee0111d0cb06220f044d66720/mmh3-5.0.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dceacf6b0b961a0e499836af3aa62d60633265607aef551b2a3e3c48cdaa5edd", size = 99662 }, + { url = "https://files.pythonhosted.org/packages/13/79/782adb6df6397947c1097b1e94b7f8d95629a4a73df05cf7207bd5148c1f/mmh3-5.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f0738d478fdfb5d920f6aff5452c78f2c35b0eff72caa2a97dfe38e82f93da2", size = 87606 }, + { url = "https://files.pythonhosted.org/packages/f2/c2/0404383281df049d0e4ccf07fabd659fc1f3da834df6708d934116cbf45d/mmh3-5.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e70285e7391ab88b872e5bef632bad16b9d99a6d3ca0590656a4753d55988af", size = 94836 }, + { url = "https://files.pythonhosted.org/packages/c8/33/fda67c5f28e4c2131891cf8cbc3513cfc55881e3cfe26e49328e38ffacb3/mmh3-5.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:27e5fc6360aa6b828546a4318da1a7da6bf6e5474ccb053c3a6aa8ef19ff97bd", size = 90492 }, + { url = "https://files.pythonhosted.org/packages/64/2f/0ed38aefe2a87f30bb1b12e5b75dc69fcffdc16def40d1752d6fc7cbbf96/mmh3-5.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7989530c3c1e2c17bf5a0ec2bba09fd19819078ba90beedabb1c3885f5040b0d", size = 89594 }, + { url = "https://files.pythonhosted.org/packages/95/ab/6e7a5e765fc78e3dbd0a04a04cfdf72e91eb8e31976228e69d82c741a5b4/mmh3-5.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cdad7bee649950da7ecd3cbbbd12fb81f1161072ecbdb5acfa0018338c5cb9cf", size = 94929 }, + { url = "https://files.pythonhosted.org/packages/74/51/f748f00c072006f4a093d9b08853a0e2e3cd5aeaa91343d4e2d942851978/mmh3-5.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e143b8f184c1bb58cecd85ab4a4fd6dc65a2d71aee74157392c3fddac2a4a331", size = 91317 }, + { url = "https://files.pythonhosted.org/packages/df/a1/21ee8017a7feb0270c49f756ff56da9f99bd150dcfe3b3f6f0d4b243423d/mmh3-5.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5eb12e886f3646dd636f16b76eb23fc0c27e8ff3c1ae73d4391e50ef60b40f6", size = 89861 }, + { url = "https://files.pythonhosted.org/packages/c2/d2/46a6d070de4659bdf91cd6a62d659f8cc547dadee52b6d02bcbacb3262ed/mmh3-5.0.1-cp313-cp313-win32.whl", hash = "sha256:16e6dddfa98e1c2d021268e72c78951234186deb4df6630e984ac82df63d0a5d", size = 39201 }, + { url = "https://files.pythonhosted.org/packages/ed/07/316c062f09019b99b248a4183c5333f8eeebe638345484774908a8f2c9c0/mmh3-5.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:d3ffb792d70b8c4a2382af3598dad6ae0c5bd9cee5b7ffcc99aa2f5fd2c1bf70", size = 39807 }, + { url = "https://files.pythonhosted.org/packages/9d/d3/f7e6d7d062b8d7072c3989a528d9d47486ee5d5ae75250f6e26b4976d098/mmh3-5.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:122fa9ec148383f9124292962bda745f192b47bfd470b2af5fe7bb3982b17896", size = 36539 }, +] + +[[package]] +name = "mock" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/ab/41d09a46985ead5839d8be987acda54b5bb93f713b3969cc0be4f81c455b/mock-5.1.0.tar.gz", hash = "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d", size = 80232 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/20/471f41173930550f279ccb65596a5ac19b9ac974a8d93679bcd3e0c31498/mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744", size = 30938 }, +] + +[[package]] +name = "multidict" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, + { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, + { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, + { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, + { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, + { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, + { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, + { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, + { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, + { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, + { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, + { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, + { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, + { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, + { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +] + +[[package]] +name = "mypy" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 }, + { url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 }, + { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 }, + { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 }, + { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 }, + { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688 }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "importlib-metadata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/f7/5f8771e591f7641ba019904e2a6be151998a6c8f3e1137654773ca060b04/opentelemetry_api-1.28.1.tar.gz", hash = "sha256:6fa7295a12c707f5aebef82da3d9ec5afe6992f3e42bfe7bec0339a44b3518e7", size = 62804 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/39/7a9c2fde8e0309e9fd339aa953110a49ebbdf8797eb497d8357f1933ec5d/opentelemetry_api-1.28.1-py3-none-any.whl", hash = "sha256:bfe86c95576cf19a914497f439fd79c9553a38de0adbdc26f7cfc46b0c00b16c", size = 64316 }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/c8/83996963ca80c149583260c22492022c9b48c854d4ca877aa3b6be8fbd3d/opentelemetry_sdk-1.28.1.tar.gz", hash = "sha256:100fa371b2046ffba6a340c18f0b2a0463acad7461e5177e126693b613a6ca57", size = 157162 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/f3/09e86288ee3aace7306b2778127565f64c53d6ec1634dd67d128848d5a4f/opentelemetry_sdk-1.28.1-py3-none-any.whl", hash = "sha256:72aad7f5fcbe37113c4ab4899f6cdeb6ac77ed3e62f25a85e3627b12583dad0f", size = 118732 }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.49b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "opentelemetry-api" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/61/2715d9d24842ef2250cbd6a44198b6d134b6238d515c6b2f9042ea5aee63/opentelemetry_semantic_conventions-0.49b1.tar.gz", hash = "sha256:91817883b159ffb94c2ca9548509c4fe0aafce7c24f437aa6ac3fc613aa9a758", size = 95221 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/1d/01ad9c2a8f8346258bf87c20fc024c8baa410492e2c6b397140383381a28/opentelemetry_semantic_conventions-0.49b1-py3-none-any.whl", hash = "sha256:dd6f3ac8169d2198c752e1a63f827e5f5e110ae9b0ce33f2aad9a3baf0739743", size = 159213 }, +] + +[[package]] +name = "orjson" +version = "3.10.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/3a/10320029954badc7eaa338a15ee279043436f396e965dafc169610e4933f/orjson-3.10.11.tar.gz", hash = "sha256:e35b6d730de6384d5b2dab5fd23f0d76fae8bbc8c353c2f78210aa5fa4beb3ef", size = 5444879 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/92/400970baf46b987c058469e9e779fb7a40d54a5754914d3634cca417e054/orjson-3.10.11-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c46294faa4e4d0eb73ab68f1a794d2cbf7bab33b1dda2ac2959ffb7c61591899", size = 266402 }, + { url = "https://files.pythonhosted.org/packages/3c/fa/f126fc2d817552bd1f67466205abdcbff64eab16f6844fe6df2853528675/orjson-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52e5834d7d6e58a36846e059d00559cb9ed20410664f3ad156cd2cc239a11230", size = 140826 }, + { url = "https://files.pythonhosted.org/packages/ad/18/9b9664d7d4af5b4fe9fe6600b7654afc0684bba528260afdde10c4a530aa/orjson-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2fc947e5350fdce548bfc94f434e8760d5cafa97fb9c495d2fef6757aa02ec0", size = 142593 }, + { url = "https://files.pythonhosted.org/packages/20/f9/a30c68f12778d5e58e6b5cdd26f86ee2d0babce1a475073043f46fdd8402/orjson-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0efabbf839388a1dab5b72b5d3baedbd6039ac83f3b55736eb9934ea5494d258", size = 146777 }, + { url = "https://files.pythonhosted.org/packages/f2/97/12047b0c0e9b391d589fb76eb40538f522edc664f650f8e352fdaaf77ff5/orjson-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3f29634260708c200c4fe148e42b4aae97d7b9fee417fbdd74f8cfc265f15b0", size = 142961 }, + { url = "https://files.pythonhosted.org/packages/a4/97/d904e26c1cabf2dd6ab1b0909e9b790af28a7f0fcb9d8378d7320d4869eb/orjson-3.10.11-cp313-none-win32.whl", hash = "sha256:1a1222ffcee8a09476bbdd5d4f6f33d06d0d6642df2a3d78b7a195ca880d669b", size = 144486 }, + { url = "https://files.pythonhosted.org/packages/42/62/3760bd1e6e949321d99bab238d08db2b1266564d2f708af668f57109bb36/orjson-3.10.11-cp313-none-win_amd64.whl", hash = "sha256:bc274ac261cc69260913b2d1610760e55d3c0801bb3457ba7b9004420b6b4270", size = 136361 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pre-commit" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, +] + +[[package]] +name = "prometheus-client" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/54/a369868ed7a7f1ea5163030f4fc07d85d22d7a1d270560dab675188fb612/prometheus_client-0.21.0.tar.gz", hash = "sha256:96c83c606b71ff2b0a433c98889d275f51ffec6c5e267de37c7a2b5c9aa9233e", size = 78634 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/2d/46ed6436849c2c88228c3111865f44311cff784b4aabcdef4ea2545dbc3d/prometheus_client-0.21.0-py3-none-any.whl", hash = "sha256:4fa6b4dd0ac16d58bb587c04b1caae65b8c5043e85f778f42f5f632f6af2e166", size = 54686 }, +] + +[[package]] +name = "propcache" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/4d/5e5a60b78dbc1d464f8a7bbaeb30957257afdc8512cbb9dfd5659304f5cd/propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70", size = 40951 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a7/5f37b69197d4f558bfef5b4bceaff7c43cc9b51adf5bd75e9081d7ea80e4/propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7", size = 78120 }, + { url = "https://files.pythonhosted.org/packages/c8/cd/48ab2b30a6b353ecb95a244915f85756d74f815862eb2ecc7a518d565b48/propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763", size = 45127 }, + { url = "https://files.pythonhosted.org/packages/a5/ba/0a1ef94a3412aab057bd996ed5f0ac7458be5bf469e85c70fa9ceb43290b/propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d", size = 44419 }, + { url = "https://files.pythonhosted.org/packages/b4/6c/ca70bee4f22fa99eacd04f4d2f1699be9d13538ccf22b3169a61c60a27fa/propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a", size = 229611 }, + { url = "https://files.pythonhosted.org/packages/19/70/47b872a263e8511ca33718d96a10c17d3c853aefadeb86dc26e8421184b9/propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b", size = 234005 }, + { url = "https://files.pythonhosted.org/packages/4f/be/3b0ab8c84a22e4a3224719099c1229ddfdd8a6a1558cf75cb55ee1e35c25/propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb", size = 237270 }, + { url = "https://files.pythonhosted.org/packages/04/d8/f071bb000d4b8f851d312c3c75701e586b3f643fe14a2e3409b1b9ab3936/propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf", size = 231877 }, + { url = "https://files.pythonhosted.org/packages/93/e7/57a035a1359e542bbb0a7df95aad6b9871ebee6dce2840cb157a415bd1f3/propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2", size = 217848 }, + { url = "https://files.pythonhosted.org/packages/f0/93/d1dea40f112ec183398fb6c42fde340edd7bab202411c4aa1a8289f461b6/propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f", size = 216987 }, + { url = "https://files.pythonhosted.org/packages/62/4c/877340871251145d3522c2b5d25c16a1690ad655fbab7bb9ece6b117e39f/propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136", size = 212451 }, + { url = "https://files.pythonhosted.org/packages/7c/bb/a91b72efeeb42906ef58ccf0cdb87947b54d7475fee3c93425d732f16a61/propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325", size = 212879 }, + { url = "https://files.pythonhosted.org/packages/9b/7f/ee7fea8faac57b3ec5d91ff47470c6c5d40d7f15d0b1fccac806348fa59e/propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44", size = 222288 }, + { url = "https://files.pythonhosted.org/packages/ff/d7/acd67901c43d2e6b20a7a973d9d5fd543c6e277af29b1eb0e1f7bd7ca7d2/propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83", size = 228257 }, + { url = "https://files.pythonhosted.org/packages/8d/6f/6272ecc7a8daad1d0754cfc6c8846076a8cb13f810005c79b15ce0ef0cf2/propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544", size = 221075 }, + { url = "https://files.pythonhosted.org/packages/7c/bd/c7a6a719a6b3dd8b3aeadb3675b5783983529e4a3185946aa444d3e078f6/propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032", size = 39654 }, + { url = "https://files.pythonhosted.org/packages/88/e7/0eef39eff84fa3e001b44de0bd41c7c0e3432e7648ffd3d64955910f002d/propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e", size = 43705 }, + { url = "https://files.pythonhosted.org/packages/3d/b6/e6d98278f2d49b22b4d033c9f792eda783b9ab2094b041f013fc69bcde87/propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036", size = 11603 }, +] + +[[package]] +name = "proto-plus" +version = "1.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/05/74417b2061e1bf1b82776037cad97094228fa1c1b6e82d08a78d3fb6ddb6/proto_plus-1.25.0.tar.gz", hash = "sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91", size = 56124 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/25/0b7cc838ae3d76d46539020ec39fc92bfc9acc29367e58fe912702c2a79e/proto_plus-1.25.0-py3-none-any.whl", hash = "sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961", size = 50126 }, +] + +[[package]] +name = "protobuf" +version = "5.28.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/6e/e69eb906fddcb38f8530a12f4b410699972ab7ced4e21524ece9d546ac27/protobuf-5.28.3.tar.gz", hash = "sha256:64badbc49180a5e401f373f9ce7ab1d18b63f7dd4a9cdc43c92b9f0b481cef7b", size = 422479 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c5/05163fad52d7c43e124a545f1372d18266db36036377ad29de4271134a6a/protobuf-5.28.3-cp310-abi3-win32.whl", hash = "sha256:0c4eec6f987338617072592b97943fdbe30d019c56126493111cf24344c1cc24", size = 419624 }, + { url = "https://files.pythonhosted.org/packages/9c/4c/4563ebe001ff30dca9d7ed12e471fa098d9759712980cde1fd03a3a44fb7/protobuf-5.28.3-cp310-abi3-win_amd64.whl", hash = "sha256:91fba8f445723fcf400fdbe9ca796b19d3b1242cd873907979b9ed71e4afe868", size = 431464 }, + { url = "https://files.pythonhosted.org/packages/1c/f2/baf397f3dd1d3e4af7e3f5a0382b868d25ac068eefe1ebde05132333436c/protobuf-5.28.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a3f6857551e53ce35e60b403b8a27b0295f7d6eb63d10484f12bc6879c715687", size = 414743 }, + { url = "https://files.pythonhosted.org/packages/85/50/cd61a358ba1601f40e7d38bcfba22e053f40ef2c50d55b55926aecc8fec7/protobuf-5.28.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:3fa2de6b8b29d12c61911505d893afe7320ce7ccba4df913e2971461fa36d584", size = 316511 }, + { url = "https://files.pythonhosted.org/packages/5d/ae/3257b09328c0b4e59535e497b0c7537d4954038bdd53a2f0d2f49d15a7c4/protobuf-5.28.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:712319fbdddb46f21abb66cd33cb9e491a5763b2febd8f228251add221981135", size = 316624 }, + { url = "https://files.pythonhosted.org/packages/ad/c3/2377c159e28ea89a91cf1ca223f827ae8deccb2c9c401e5ca233cd73002f/protobuf-5.28.3-py3-none-any.whl", hash = "sha256:cee1757663fa32a1ee673434fcf3bf24dd54763c79690201208bafec62f19eed", size = 169511 }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699 }, + { url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245 }, + { url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631 }, + { url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140 }, + { url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762 }, + { url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967 }, + { url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326 }, + { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712 }, + { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155 }, + { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356 }, + { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/67/6afbf0d507f73c32d21084a79946bfcfca5fbc62a72057e9c23797a737c9/pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c", size = 310028 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/89/bc88a6711935ba795a679ea6ebee07e128050d6382eaa35a0a47c8032bdc/pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", size = 181537 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pycryptodome" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/52/13b9db4a913eee948152a079fe58d035bd3d1a519584155da8e786f767e6/pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297", size = 4818071 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/88/5e83de10450027c96c79dc65ac45e9d0d7a7fef334f39d3789a191f33602/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4", size = 2495937 }, + { url = "https://files.pythonhosted.org/packages/66/e1/8f28cd8cf7f7563319819d1e172879ccce2333781ae38da61c28fe22d6ff/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b", size = 1634629 }, + { url = "https://files.pythonhosted.org/packages/6a/c1/f75a1aaff0c20c11df8dc8e2bf8057e7f73296af7dfd8cbb40077d1c930d/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e", size = 2168708 }, + { url = "https://files.pythonhosted.org/packages/ea/66/6f2b7ddb457b19f73b82053ecc83ba768680609d56dd457dbc7e902c41aa/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8", size = 2254555 }, + { url = "https://files.pythonhosted.org/packages/2c/2b/152c330732a887a86cbf591ed69bd1b489439b5464806adb270f169ec139/pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1", size = 2294143 }, + { url = "https://files.pythonhosted.org/packages/55/92/517c5c498c2980c1b6d6b9965dffbe31f3cd7f20f40d00ec4069559c5902/pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a", size = 2160509 }, + { url = "https://files.pythonhosted.org/packages/39/1f/c74288f54d80a20a78da87df1818c6464ac1041d10988bb7d982c4153fbc/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2", size = 2329480 }, + { url = "https://files.pythonhosted.org/packages/39/1b/d0b013bf7d1af7cf0a6a4fce13f5fe5813ab225313755367b36e714a63f8/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93", size = 2254397 }, + { url = "https://files.pythonhosted.org/packages/14/71/4cbd3870d3e926c34706f705d6793159ac49d9a213e3ababcdade5864663/pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764", size = 1775641 }, + { url = "https://files.pythonhosted.org/packages/43/1d/81d59d228381576b92ecede5cd7239762c14001a828bdba30d64896e9778/pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53", size = 1812863 }, +] + +[[package]] +name = "pydantic" +version = "2.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/7e/fb60e6fee04d0ef8f15e4e01ff187a196fa976eb0f0ab524af4599e5754c/pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06", size = 762094 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/26/3e1bbe954fde7ee22a6e7d31582c642aad9e84ffe4b5fb61e63b87cd326f/pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d", size = 431765 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pyjwt" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/68/ce067f09fca4abeca8771fe667d89cc347d1e99da3e093112ac329c6020e/pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c", size = 78825 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/84/0fdf9b18ba31d69877bd39c9cd6052b47f3761e9910c15de788e519f079f/PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", size = 22344 }, +] + +[[package]] +name = "pyparsing" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/d5/e5aeee5387091148a19e1145f63606619cb5f20b83fccb63efae6474e7b2/pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c", size = 920984 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/ec/2eb3cd785efd67806c46c13a17339708ddc346cbb684eade7a6e6f79536a/pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", size = 106921 }, +] + +[[package]] +name = "pytest" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024 }, +] + +[[package]] +name = "pytest-codspeed" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "pytest" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/98/16fe3895b1b8a6d537a89eecb120b97358df8f0002c6ecd11555d6304dc8/pytest_codspeed-3.2.0.tar.gz", hash = "sha256:f9d1b1a3b2c69cdc0490a1e8b1ced44bffbd0e8e21d81a7160cfdd923f6e8155", size = 18409 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/15/60b18d40da66e7aa2ce4c4c66d5a17de20a2ae4a89ac09a58baa7a5bc535/pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66692506d33453df48b36a84703448cb8b22953eea51f03fbb2eb758dc2bdc4f", size = 27180 }, + { url = "https://files.pythonhosted.org/packages/51/bd/6b164d4ae07d8bea5d02ad664a9762bdb63f83c0805a3c8fe7dc6ec38407/pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:479774f80d0bdfafa16112700df4dbd31bf2a6757fac74795fd79c0a7b3c389b", size = 25923 }, + { url = "https://files.pythonhosted.org/packages/f1/9b/952c70bd1fae9baa58077272e7f191f377c86d812263c21b361195e125e6/pytest_codspeed-3.2.0-py3-none-any.whl", hash = "sha256:54b5c2e986d6a28e7b0af11d610ea57bd5531cec8326abe486f1b55b09d91c39", size = 15007 }, +] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + +[[package]] +name = "pytest-django" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/c0/43c8b2528c24d7f1a48a47e3f7381f5ab2ae8c64634b0c3f4bd843063955/pytest_django-4.9.0.tar.gz", hash = "sha256:8bf7bc358c9ae6f6fc51b6cebb190fe20212196e6807121f11bd6a3b03428314", size = 84067 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/fe/54f387ee1b41c9ad59e48fb8368a361fad0600fe404315e31a12bacaea7d/pytest_django-4.9.0-py3-none-any.whl", hash = "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99", size = 23723 }, +] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-redis-lock" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/d7/a2a97c73d39e68aacce02667885b9e0b575eb9082866a04fbf098b4c4d99/python-redis-lock-4.0.0.tar.gz", hash = "sha256:4abd0bcf49136acad66727bf5486dd2494078ca55e49efa693f794077319091a", size = 162533 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/70/c5dfaec2085d9be10792704f108543ba1802e228bf040632c673066d8e78/python_redis_lock-4.0.0-py3-none-any.whl", hash = "sha256:ff786e587569415f31e64ca9337fce47c4206e832776e9e42b83bfb9ee1af4bd", size = 12165 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "redis" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/17/2f4a87ffa4cd93714cf52edfa3ea94589e9de65f71e9f99cbcfa84347a53/redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0", size = 4607878 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/f5/ffa560ecc4bafbf25f7961c3d6f50d627a90186352e27e7d0ba5b1f6d87d/redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897", size = 261428 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "respx" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/72/979e475ade69bcbb18288604aacbdc77b44b3bd1133e2c16660282a9f4b8/respx-0.21.1.tar.gz", hash = "sha256:0bd7fe21bfaa52106caa1223ce61224cf30786985f17c63c5d71eff0307ee8af", size = 28306 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/5c/428523509b26c243c1e93aa2ae385def597ef1fbdbbd47978430ba19037d/respx-0.21.1-py2.py3-none-any.whl", hash = "sha256:05f45de23f0c785862a2c92a3e173916e8ca88e4caad715dd5f68584d6053c20", size = 25130 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "rsa" +version = "4.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21", size = 29711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315 }, +] + +[[package]] +name = "ruff" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/48/385f276f41e89623a5ea8e4eb9c619a44fdfc2a64849916b3584eca6cb9f/ruff-0.9.0.tar.gz", hash = "sha256:143f68fa5560ecf10fc49878b73cee3eab98b777fcf43b0e62d43d42f5ef9d8b", size = 3489167 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/01/e0885e5519212efc7ab9d868bc39cb9781931c4c6f9b17becafa81193ec4/ruff-0.9.0-py3-none-linux_armv6l.whl", hash = "sha256:949b3513f931741e006cf267bf89611edff04e1f012013424022add3ce78f319", size = 10647069 }, + { url = "https://files.pythonhosted.org/packages/dd/69/510a9a5781dcf84c2ad513c2003936fefc802f39c745d5f2355d77fa45fd/ruff-0.9.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:99fbcb8c7fe94ae1e462ab2a1ef17cb20b25fb6438b9f198b1bcf5207a0a7916", size = 10401936 }, + { url = "https://files.pythonhosted.org/packages/07/9f/37fb86bfdf28c4cbfe94cbcc01fb9ab0cb8128548f243f34d5298b212562/ruff-0.9.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0b022afd8eb0fcfce1e0adec84322abf4d6ce3cd285b3b99c4f17aae7decf749", size = 10010347 }, + { url = "https://files.pythonhosted.org/packages/30/0d/b95121f53c7f7bfb7ba427a35d25f983ed3b476620c5cd69f45caa5b294e/ruff-0.9.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:336567ce92c9ca8ec62780d07b5fa11fbc881dc7bb40958f93a7d621e7ab4589", size = 10882152 }, + { url = "https://files.pythonhosted.org/packages/d4/0b/a955cb6b19eb900c4c594707ab72132ce2d5cd8b5565137fb8fed21b8f08/ruff-0.9.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d338336c44bda602dc8e8766836ac0441e5b0dfeac3af1bd311a97ebaf087a75", size = 10405502 }, + { url = "https://files.pythonhosted.org/packages/1e/fa/9a6c70af74f20edd2519b89eb3322f4bfa399315cf306383443700f2d6b6/ruff-0.9.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9b3ececf523d733e90b540e7afcc0494189e8999847f8855747acd5a9a8c45f", size = 11465069 }, + { url = "https://files.pythonhosted.org/packages/ee/8b/7effac8915470da496be009fe861060baff2692f92801976b2c01cdc8c54/ruff-0.9.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a11c0872a31232e473e2e0e2107f3d294dbadd2f83fb281c3eb1c22a24866924", size = 12176850 }, + { url = "https://files.pythonhosted.org/packages/bd/ed/626179786889eca47b1e821c1582622ac0c1c8f01d60ac974f8b96867a57/ruff-0.9.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5fd06220c17a9cc0dc7fc6552f2ac4db74e8e8bff9c401d160ac59d00566f54", size = 11700963 }, + { url = "https://files.pythonhosted.org/packages/75/79/094c34ddec47fd3c61a0bc5e83ca164344c592949cff91f05961fd40922e/ruff-0.9.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0457e775c74bf3976243f910805242b7dcd389e1d440deccbd1194ca17a5728c", size = 13096560 }, + { url = "https://files.pythonhosted.org/packages/e7/23/ec85dca0dcb329835197401734501bfa1d39e72343df64628c67b72bcbf5/ruff-0.9.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05415599bbcb318f730ea1b46a39e4fbf71f6a63fdbfa1dda92efb55f19d7ecf", size = 11278658 }, + { url = "https://files.pythonhosted.org/packages/6c/17/1b3ea5f06578ea1daa08ac35f9de099d1827eea6e116a8cabbf11235c925/ruff-0.9.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fbf9864b009e43cfc1c8bed1a6a4c529156913105780af4141ca4342148517f5", size = 10879847 }, + { url = "https://files.pythonhosted.org/packages/a6/e5/00bc97d6f419da03c0d898e95cca77311494e7274dc7cc17d94976e32e52/ruff-0.9.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:37b3da222b12e2bb2ce628e02586ab4846b1ed7f31f42a5a0683b213453b2d49", size = 10494220 }, + { url = "https://files.pythonhosted.org/packages/cc/70/d0a23d94f3e40b7ffac0e5506f33bb504672569173781a6c7cab0db6a4ba/ruff-0.9.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:733c0fcf2eb0c90055100b4ed1af9c9d87305b901a8feb6a0451fa53ed88199d", size = 11004182 }, + { url = "https://files.pythonhosted.org/packages/20/8e/367cf8e401890f823d0e4eb33635d0113719d5660b6522b7295376dd95fd/ruff-0.9.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8221a454bfe5ccdf8017512fd6bb60e6ec30f9ea252b8a80e5b73619f6c3cefd", size = 11345761 }, + { url = "https://files.pythonhosted.org/packages/fe/08/4b54e02da73060ebc29368ab15868613f7d2496bde3b01d284d5423646bc/ruff-0.9.0-py3-none-win32.whl", hash = "sha256:d345f2178afd192c7991ddee59155c58145e12ad81310b509bd2e25c5b0247b3", size = 8807005 }, + { url = "https://files.pythonhosted.org/packages/a1/a7/0b422971e897c51bf805f998d75bcfe5d4d858f5002203832875fc91b733/ruff-0.9.0-py3-none-win_amd64.whl", hash = "sha256:0cbc0905d94d21305872f7f8224e30f4bbcd532bc21b2225b2446d8fc7220d19", size = 9689974 }, + { url = "https://files.pythonhosted.org/packages/73/0e/c00f66731e514be3299801b1d9d54efae0abfe8f00a5c14155f2ab9e2920/ruff-0.9.0-py3-none-win_arm64.whl", hash = "sha256:7b1148771c6ca88f820d761350a053a5794bc58e0867739ea93eb5e41ad978cd", size = 9147729 }, +] + +[[package]] +name = "s3transfer" +version = "0.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/a8/e0a98fd7bd874914f0608ef7c90ffde17e116aefad765021de0f012690a2/s3transfer-0.10.3.tar.gz", hash = "sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c", size = 144591 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/c0/b0fba8259b61c938c9733da9346b9f93e00881a9db22aafdd72f6ae0ec05/s3transfer-0.10.3-py3-none-any.whl", hash = "sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d", size = 82625 }, +] + +[[package]] +name = "sentry-sdk" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/41/97f673384dae5ed81cc2a568cc5c28e76deee85f8ba50def862e86150a5a/sentry_sdk-2.13.0.tar.gz", hash = "sha256:8d4a576f7a98eb2fdb40e13106e41f330e5c79d72a68be1316e7852cf4995260", size = 279937 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/7e/e9ca09f24a6c334286631a2d32c267cdc5edad5ac03fd9d20a01a82f1c35/sentry_sdk-2.13.0-py2.py3-none-any.whl", hash = "sha256:6beede8fc2ab4043da7f69d95534e320944690680dd9a963178a49de71d726c6", size = 309078 }, +] + +[[package]] +name = "shared" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "amplitude-analytics" }, + { name = "boto3" }, + { name = "cachetools" }, + { name = "cerberus" }, + { name = "codecov-ribs" }, + { name = "colour" }, + { name = "cryptography" }, + { name = "django" }, + { name = "django-better-admin-arrayfield" }, + { name = "django-model-utils" }, + { name = "django-postgres-extra" }, + { name = "django-prometheus" }, + { name = "google-auth" }, + { name = "google-cloud-pubsub" }, + { name = "google-cloud-storage" }, + { name = "httpx" }, + { name = "ijson" }, + { name = "minio" }, + { name = "mmh3" }, + { name = "oauthlib" }, + { name = "orjson" }, + { name = "prometheus-client" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "pyparsing" }, + { name = "python-redis-lock" }, + { name = "pyyaml" }, + { name = "redis" }, + { name = "requests" }, + { name = "sentry-sdk" }, + { name = "sqlalchemy" }, + { name = "zstandard" }, +] + +[package.dev-dependencies] +dev = [ + { name = "factory-boy" }, + { name = "freezegun" }, + { name = "mock" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "psycopg2-binary" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-codspeed" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "pytest-mock" }, + { name = "respx" }, + { name = "ruff" }, + { name = "types-mock" }, + { name = "types-requests" }, + { name = "urllib3" }, + { name = "vcrpy" }, +] + +[package.metadata] +requires-dist = [ + { name = "amplitude-analytics", specifier = ">=1.1.4" }, + { name = "boto3", specifier = ">=1.20.25" }, + { name = "cachetools", specifier = ">=4.1.1" }, + { name = "cerberus", specifier = ">=1.3.5" }, + { name = "codecov-ribs", specifier = ">=0.1.18" }, + { name = "colour", specifier = ">=0.1.5" }, + { name = "cryptography", specifier = ">=43.0.1" }, + { name = "django", specifier = "<5" }, + { name = "django-better-admin-arrayfield", specifier = ">=1.4.2" }, + { name = "django-model-utils", specifier = ">=4.5.1" }, + { name = "django-postgres-extra", specifier = ">=2.0.8" }, + { name = "django-prometheus", specifier = ">=2.3.1" }, + { name = "google-auth", specifier = ">=2.21.0" }, + { name = "google-cloud-pubsub", specifier = ">=2.18.4" }, + { name = "google-cloud-storage", specifier = ">=2.18.2" }, + { name = "httpx", specifier = ">=0.23.0" }, + { name = "ijson", specifier = ">=3.2.3" }, + { name = "minio", specifier = ">=7.1.13" }, + { name = "mmh3", specifier = ">=4.0.1" }, + { name = "oauthlib", specifier = ">=3.1.0" }, + { name = "orjson", specifier = ">=3.10.9" }, + { name = "prometheus-client", specifier = ">=0.17.1" }, + { name = "pydantic", specifier = ">=2.10.4" }, + { name = "pyjwt", specifier = ">=2.8.0" }, + { name = "pyparsing", specifier = ">=2.4.7" }, + { name = "python-redis-lock", specifier = ">=4.0.0" }, + { name = "pyyaml", specifier = ">=6.0.1" }, + { name = "redis", specifier = ">=4.4.4" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "sentry-sdk", specifier = ">=2.13.0" }, + { name = "sqlalchemy", specifier = "<2" }, + { name = "zstandard", specifier = ">=0.23.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "factory-boy", specifier = ">=3.2.0" }, + { name = "freezegun", specifier = ">=1.1.0" }, + { name = "mock", specifier = ">=4.0.3" }, + { name = "mypy", specifier = ">=1.13.0" }, + { name = "pre-commit", specifier = ">=2.11.1" }, + { name = "psycopg2-binary", specifier = ">=2.9.2" }, + { name = "pytest", specifier = ">=8.1.1" }, + { name = "pytest-asyncio", specifier = ">=0.14.0" }, + { name = "pytest-codspeed", specifier = ">=3.2.0" }, + { name = "pytest-cov", specifier = ">=5.0.0" }, + { name = "pytest-django", specifier = ">=4.7.0" }, + { name = "pytest-mock", specifier = ">=1.13.0" }, + { name = "respx", specifier = ">=0.20.2" }, + { name = "ruff", specifier = ">=0.9.0" }, + { name = "types-mock", specifier = ">=5.1.0.20240425" }, + { name = "types-requests", specifier = ">=2.31.0.6" }, + { name = "urllib3", specifier = "==1.26.19" }, + { name = "vcrpy", specifier = ">=4.1.1" }, +] + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "sqlalchemy" +version = "1.4.54" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/af/20290b55d469e873cba9d41c0206ab5461ff49d759989b3fe65010f9d265/sqlalchemy-1.4.54.tar.gz", hash = "sha256:4470fbed088c35dc20b78a39aaf4ae54fe81790c783b3264872a0224f437c31a", size = 8470350 } + +[[package]] +name = "sqlparse" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/82/dfa23ec2cbed08a801deab02fe7c904bfb00765256b155941d789a338c68/sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e", size = 84502 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a5/b2860373aa8de1e626b2bdfdd6df4355f0565b47e51f7d0c54fe70faf8fe/sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", size = 44156 }, +] + +[[package]] +name = "types-mock" +version = "5.1.0.20240425" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/40/1ed5b983c97161ad1605de42932143bcb24f5e435cc660de4487f78f6a4c/types-mock-5.1.0.20240425.tar.gz", hash = "sha256:5281a645d72e827d70043e3cc144fe33b1c003db084f789dc203aa90e812a5a4", size = 6441 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/6d/7710612643616654ca0094234bce0f0448f4aa9d6f3057e4681143f73e73/types_mock-5.1.0.20240425-py3-none-any.whl", hash = "sha256:d586a01d39ad919d3ddcd73de6cde73ca7f3c69707219f722d1b8d7733641ad7", size = 5714 }, +] + +[[package]] +name = "types-requests" +version = "2.31.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/b8/c1e8d39996b4929b918aba10dba5de07a8b3f4c8487bb61bb79882544e69/types-requests-2.31.0.6.tar.gz", hash = "sha256:cd74ce3b53c461f1228a9b783929ac73a666658f223e28ed29753771477b3bd0", size = 15535 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/a1/6f8dc74d9069e790d604ddae70cb46dcbac668f1bb08136e7b0f2f5cd3bf/types_requests-2.31.0.6-py3-none-any.whl", hash = "sha256:a2db9cb228a81da8348b49ad6db3f5519452dd20a9c1e1a868c83c5fe88fd1a9", size = 14516 }, +] + +[[package]] +name = "types-urllib3" +version = "1.26.25.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/de/b9d7a68ad39092368fb21dd6194b362b98a1daeea5dcfef5e1adb5031c7e/types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f", size = 11239 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/7b/3fc711b2efea5e85a7a0bbfe269ea944aa767bbba5ec52f9ee45d362ccf3/types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e", size = 15377 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "tzdata" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 }, +] + +[[package]] +name = "urllib3" +version = "1.26.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/93/65e479b023bbc46dab3e092bda6b0005424ea3217d711964ccdede3f9b1b/urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429", size = 306068 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/6a/99eaaeae8becaa17a29aeb334a18e5d582d873b6f084c11f02581b8d7f7f/urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3", size = 143933 }, +] + +[[package]] +name = "vcrpy" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "urllib3" }, + { name = "wrapt" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/4e/fff59599826793f9e3460c22c0af0377abb27dc9781a7d5daca8cb03da25/vcrpy-6.0.2.tar.gz", hash = "sha256:88e13d9111846745898411dbc74a75ce85870af96dd320d75f1ee33158addc09", size = 85472 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/ed/25d19705791d3fccc84423d564695421a75b4e08e8ab15a004a49068742d/vcrpy-6.0.2-py2.py3-none-any.whl", hash = "sha256:40370223861181bc76a5e5d4b743a95058bb1ad516c3c08570316ab592f56cad", size = 42431 }, +] + +[[package]] +name = "virtualenv" +version = "20.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/b3/7b6a79c5c8cf6d90ea681310e169cf2db2884f4d583d16c6e1d5a75a4e04/virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba", size = 6491145 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/92/78324ff89391e00c8f4cf6b8526c41c6ef36b4ea2d2c132250b1a6fc2b8d/virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4", size = 3117838 }, +] + +[[package]] +name = "wrapt" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/4c/063a912e20bcef7124e0df97282a8af3ff3e4b603ce84c481d6d7346be0a/wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", size = 53972 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/21/abdedb4cdf6ff41ebf01a74087740a709e2edb146490e4d9beea054b0b7a/wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", size = 23362 }, +] + +[[package]] +name = "yarl" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/9c/9c0a9bfa683fc1be7fdcd9687635151544d992cccd48892dc5e0a5885a29/yarl-1.17.1.tar.gz", hash = "sha256:067a63fcfda82da6b198fa73079b1ca40b7c9b7994995b6ee38acda728b64d47", size = 178163 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/1e/5a93e3743c20eefbc68bd89334d9c9f04f3f2334380f7bbf5e950f29511b/yarl-1.17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5d1d42556b063d579cae59e37a38c61f4402b47d70c29f0ef15cee1acaa64488", size = 139974 }, + { url = "https://files.pythonhosted.org/packages/a1/be/4e0f6919013c7c5eaea5c31811c551ccd599d2fc80aa3dd6962f1bbdcddd/yarl-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0167540094838ee9093ef6cc2c69d0074bbf84a432b4995835e8e5a0d984374", size = 93364 }, + { url = "https://files.pythonhosted.org/packages/73/f0/650f994bc491d0cb85df8bb45392780b90eab1e175f103a5edc61445ff67/yarl-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2f0a6423295a0d282d00e8701fe763eeefba8037e984ad5de44aa349002562ac", size = 91177 }, + { url = "https://files.pythonhosted.org/packages/f3/e8/9945ed555d14b43ede3ae8b1bd73e31068a694cad2b9d3cad0a28486c2eb/yarl-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5b078134f48552c4d9527db2f7da0b5359abd49393cdf9794017baec7506170", size = 333086 }, + { url = "https://files.pythonhosted.org/packages/a6/c0/7d167e48e14d26639ca066825af8da7df1d2fcdba827e3fd6341aaf22a3b/yarl-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d401f07261dc5aa36c2e4efc308548f6ae943bfff20fcadb0a07517a26b196d8", size = 343661 }, + { url = "https://files.pythonhosted.org/packages/fa/81/80a266517531d4e3553aecd141800dbf48d02e23ebd52909e63598a80134/yarl-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5f1ac7359e17efe0b6e5fec21de34145caef22b260e978336f325d5c84e6938", size = 345196 }, + { url = "https://files.pythonhosted.org/packages/b0/77/6adc482ba7f2dc6c0d9b3b492e7cd100edfac4cfc3849c7ffa26fd7beb1a/yarl-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f63d176a81555984e91f2c84c2a574a61cab7111cc907e176f0f01538e9ff6e", size = 338743 }, + { url = "https://files.pythonhosted.org/packages/6d/cc/f0c4c0b92ff3ada517ffde2b127406c001504b225692216d969879ada89a/yarl-1.17.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e275792097c9f7e80741c36de3b61917aebecc08a67ae62899b074566ff8556", size = 326719 }, + { url = "https://files.pythonhosted.org/packages/18/3b/7bfc80d3376b5fa162189993a87a5a6a58057f88315bd0ea00610055b57a/yarl-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:81713b70bea5c1386dc2f32a8f0dab4148a2928c7495c808c541ee0aae614d67", size = 345826 }, + { url = "https://files.pythonhosted.org/packages/2e/66/cf0b0338107a5c370205c1a572432af08f36ca12ecce127f5b558398b4fd/yarl-1.17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:aa46dce75078fceaf7cecac5817422febb4355fbdda440db55206e3bd288cfb8", size = 340335 }, + { url = "https://files.pythonhosted.org/packages/2f/52/b084b0eec0fd4d2490e1d33ace3320fad704c5f1f3deaa709f929d2d87fc/yarl-1.17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1ce36ded585f45b1e9bb36d0ae94765c6608b43bd2e7f5f88079f7a85c61a4d3", size = 345301 }, + { url = "https://files.pythonhosted.org/packages/ef/38/9e2036d948efd3bafcdb4976cb212166fded76615f0dfc6c1492c4ce4784/yarl-1.17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2d374d70fdc36f5863b84e54775452f68639bc862918602d028f89310a034ab0", size = 354205 }, + { url = "https://files.pythonhosted.org/packages/81/c1/13dfe1e70b86811733316221c696580725ceb1c46d4e4db852807e134310/yarl-1.17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2d9f0606baaec5dd54cb99667fcf85183a7477f3766fbddbe3f385e7fc253299", size = 360501 }, + { url = "https://files.pythonhosted.org/packages/91/87/756e05c74cd8bf9e71537df4a2cae7e8211a9ebe0d2350a3e26949e1e41c/yarl-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b0341e6d9a0c0e3cdc65857ef518bb05b410dbd70d749a0d33ac0f39e81a4258", size = 359452 }, + { url = "https://files.pythonhosted.org/packages/06/b2/b2bb09c1e6d59e1c9b1b36a86caa473e22c3dbf26d1032c030e9bfb554dc/yarl-1.17.1-cp313-cp313-win32.whl", hash = "sha256:2e7ba4c9377e48fb7b20dedbd473cbcbc13e72e1826917c185157a137dac9df2", size = 308904 }, + { url = "https://files.pythonhosted.org/packages/f3/27/f084d9a5668853c1f3b246620269b14ee871ef3c3cc4f3a1dd53645b68ec/yarl-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:949681f68e0e3c25377462be4b658500e85ca24323d9619fdc41f68d46a1ffda", size = 314637 }, + { url = "https://files.pythonhosted.org/packages/52/ad/1fe7ff5f3e8869d4c5070f47b96bac2b4d15e67c100a8278d8e7876329fc/yarl-1.17.1-py3-none-any.whl", hash = "sha256:f1790a4b1e8e8e028c391175433b9c8122c39b46e1663228158e61e6f915bf06", size = 44352 }, +] + +[[package]] +name = "zipp" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, +] + +[[package]] +name = "zstandard" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation == 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975 }, + { url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448 }, + { url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269 }, + { url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228 }, + { url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891 }, + { url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310 }, + { url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912 }, + { url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946 }, + { url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994 }, + { url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681 }, + { url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239 }, + { url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149 }, + { url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392 }, + { url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299 }, + { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862 }, + { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578 }, +] diff --git a/tools/absorb-repo/README.md b/tools/absorb-repo/README.md index 2a7f0b0e19..a167c3a820 100644 --- a/tools/absorb-repo/README.md +++ b/tools/absorb-repo/README.md @@ -4,5 +4,13 @@ This script absorbs a repository into the monorepo in a way that (mostly) preser Example usage: ``` +$ git checkout -b absorb-worker $ ./absorb-repo.sh worker git@github.com:codecov/worker.git apps/worker +$ git push origin absorb-worker ``` + +The above invocation will create one or two commits on the `absorb-worker` branch: +- if `apps/worker` already exists (like as a submodule), a commit will be added to delete it +- a merge commit that merges the `absorb-worker` branch with the rewritten default branch of `git@github.com:codecov/worker.git` + +You can run it multiple times to absorb multiple repositories in a single branch. diff --git a/tools/absorb-repo/absorb-repo.sh b/tools/absorb-repo/absorb-repo.sh index 724163c466..2bb01751c3 100755 --- a/tools/absorb-repo/absorb-repo.sh +++ b/tools/absorb-repo/absorb-repo.sh @@ -7,7 +7,6 @@ subdirectory="$3" # Variables used throughout the script local_main_checkout="$repo_name-main" -absorb_branch="absorb-$repo_name" current_branch="$(git rev-parse --abbrev-ref HEAD)" # Assumes this script's directory has a sibling directory called `git-filter-repo` @@ -45,10 +44,18 @@ if [ $# -ne 3 ] || [ "$(git rev-parse --is-inside-work-tree)" != "true" ]; then usage fi +if [ -d "$subdirectory" ]; then + echo "Removing existing $subdirectory to make room to absorb $repo_name..." + git rm "$subdirectory" + git commit -m "Removing existing $subdirectory to make room to absorb $repo_name" +fi + echo "Adding \`$remote_url\` as a remote named \`$repo_name\`..." git remote add $repo_name $remote_url git ls-remote $repo_name | grep main > /dev/null && branch="main" || branch="master" +# TODO delete +branch=matt/overridable-make-vars git fetch $repo_name $branch echo "Found repository with default branch \`$branch\`" echo "" @@ -66,8 +73,7 @@ python "$GIT_FILTER_REPO_DIR/git-filter-repo" --force --refs "$local_main_checko echo "Done" echo "" -echo "Merging the rewritten \`$local_main_checkout\` into our \`$absorb_branch\`" -git checkout -b $absorb_branch $current_branch +echo "Merging the rewritten \`$local_main_checkout\` into \`$current_branch\`" git merge "$local_main_checkout" --allow-unrelated-histories --no-edit echo "Done" echo "" @@ -76,8 +82,3 @@ echo "Cleaning up after ourselves..." git branch -D $local_main_checkout git remote remove $repo_name echo "Done" - -echo "Pushing to GitHub..." -git push origin $absorb_branch -echo "Done. Create a PR!" - diff --git a/tools/devenv/Makefile.devenv b/tools/devenv/Makefile.devenv index 23753b09dc..be5a876594 100644 --- a/tools/devenv/Makefile.devenv +++ b/tools/devenv/Makefile.devenv @@ -1,10 +1,10 @@ .PHONY: devenv.build.worker devenv.build.worker: - $(MAKE) -C apps/worker build + $(MAKE) worker.build .PHONY: devenv.build.api devenv.build.api: - $(MAKE) -C apps/codecov-api build + $(MAKE) api.build .PHONY: devenv.build.shared devenv.build.shared: